[
  {
    "path": ".all-contributorsrc",
    "content": "{\n  \"projectName\": \"OpenPype\",\n  \"projectOwner\": \"ynput\",\n  \"repoType\": \"github\",\n  \"repoHost\": \"https://github.com\",\n  \"files\": [\n    \"README.md\"\n  ],\n  \"imageSize\": 100,\n  \"commit\": true,\n  \"commitConvention\": \"none\",\n  \"contributors\": [\n    {\n      \"login\": \"mkolar\",\n      \"name\": \"Milan Kolar\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/3333008?v=4\",\n      \"profile\": \"http://pype.club/\",\n      \"contributions\": [\n        \"code\",\n        \"doc\",\n        \"infra\",\n        \"business\",\n        \"content\",\n        \"fundingFinding\",\n        \"maintenance\",\n        \"projectManagement\",\n        \"review\",\n        \"mentoring\",\n        \"question\"\n      ]\n    },\n    {\n      \"login\": \"jakubjezek001\",\n      \"name\": \"Jakub Ježek\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/40640033?v=4\",\n      \"profile\": \"https://www.linkedin.com/in/jakubjezek79\",\n      \"contributions\": [\n        \"code\",\n        \"doc\",\n        \"infra\",\n        \"content\",\n        \"review\",\n        \"maintenance\",\n        \"mentoring\",\n        \"projectManagement\",\n        \"question\"\n      ]\n    },\n    {\n      \"login\": \"antirotor\",\n      \"name\": \"Ondřej Samohel\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/33513211?v=4\",\n      \"profile\": \"https://github.com/antirotor\",\n      \"contributions\": [\n        \"code\",\n        \"doc\",\n        \"infra\",\n        \"content\",\n        \"review\",\n        \"maintenance\",\n        \"mentoring\",\n        \"projectManagement\",\n        \"question\"\n      ]\n    },\n    {\n      \"login\": \"iLLiCiTiT\",\n      \"name\": \"Jakub Trllo\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/43494761?v=4\",\n      \"profile\": \"https://github.com/iLLiCiTiT\",\n      \"contributions\": [\n        \"code\",\n        \"doc\",\n        \"infra\",\n        \"review\",\n        \"maintenance\",\n        \"question\"\n      ]\n    },\n    {\n      \"login\": \"kalisp\",\n      \"name\": \"Petr Kalis\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/4457962?v=4\",\n      \"profile\": \"https://github.com/kalisp\",\n      \"contributions\": [\n        \"code\",\n        \"doc\",\n        \"infra\",\n        \"review\",\n        \"maintenance\",\n        \"question\"\n      ]\n    },\n    {\n      \"login\": \"64qam\",\n      \"name\": \"64qam\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/26925793?v=4\",\n      \"profile\": \"https://github.com/64qam\",\n      \"contributions\": [\n        \"code\",\n        \"review\",\n        \"doc\",\n        \"infra\",\n        \"projectManagement\",\n        \"maintenance\",\n        \"content\",\n        \"userTesting\"\n      ]\n    },\n    {\n      \"login\": \"BigRoy\",\n      \"name\": \"Roy Nieterau\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/2439881?v=4\",\n      \"profile\": \"http://www.colorbleed.nl/\",\n      \"contributions\": [\n        \"code\",\n        \"doc\",\n        \"review\",\n        \"mentoring\",\n        \"question\"\n      ]\n    },\n    {\n      \"login\": \"tokejepsen\",\n      \"name\": \"Toke Jepsen\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/1860085?v=4\",\n      \"profile\": \"https://github.com/tokejepsen\",\n      \"contributions\": [\n        \"code\",\n        \"doc\",\n        \"review\",\n        \"mentoring\",\n        \"question\"\n      ]\n    },\n    {\n      \"login\": \"jrsndl\",\n      \"name\": \"Jiri Sindelar\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/45896205?v=4\",\n      \"profile\": \"https://github.com/jrsndl\",\n      \"contributions\": [\n        \"code\",\n        \"review\",\n        \"doc\",\n        \"content\",\n        \"tutorial\",\n        \"userTesting\"\n      ]\n    },\n    {\n      \"login\": \"simonebarbieri\",\n      \"name\": \"Simone Barbieri\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/1087869?v=4\",\n      \"profile\": \"https://barbierisimone.com/\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"karimmozilla\",\n      \"name\": \"karimmozilla\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/82811760?v=4\",\n      \"profile\": \"http://karimmozilla.xyz/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Allan-I\",\n      \"name\": \"Allan I. A.\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/76656700?v=4\",\n      \"profile\": \"https://github.com/Allan-I\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"m-u-r-p-h-y\",\n      \"name\": \"murphy\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/352795?v=4\",\n      \"profile\": \"https://www.linkedin.com/in/mmuurrpphhyy/\",\n      \"contributions\": [\n        \"code\",\n        \"review\",\n        \"userTesting\",\n        \"doc\",\n        \"projectManagement\"\n      ]\n    },\n    {\n      \"login\": \"aardschok\",\n      \"name\": \"Wijnand Koreman\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/26920875?v=4\",\n      \"profile\": \"https://github.com/aardschok\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"zhoub\",\n      \"name\": \"Bo Zhou\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/1798206?v=4\",\n      \"profile\": \"http://jedimaster.cnblogs.com/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"ClementHector\",\n      \"name\": \"Clément Hector\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/7068597?v=4\",\n      \"profile\": \"https://www.linkedin.com/in/clementhector/\",\n      \"contributions\": [\n        \"code\",\n        \"review\"\n      ]\n    },\n    {\n      \"login\": \"davidlatwe\",\n      \"name\": \"David Lai\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/3357009?v=4\",\n      \"profile\": \"https://twitter.com/davidlatwe\",\n      \"contributions\": [\n        \"code\",\n        \"review\"\n      ]\n    },\n    {\n      \"login\": \"2-REC\",\n      \"name\": \"Derek \",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/42170307?v=4\",\n      \"profile\": \"https://github.com/2-REC\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"gabormarinov\",\n      \"name\": \"Gábor Marinov\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/8620515?v=4\",\n      \"profile\": \"https://github.com/gabormarinov\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"icyvapor\",\n      \"name\": \"icyvapor\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/1195278?v=4\",\n      \"profile\": \"https://github.com/icyvapor\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"jlorrain\",\n      \"name\": \"Jérôme LORRAIN\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/7955673?v=4\",\n      \"profile\": \"https://github.com/jlorrain\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"dmo-j-cube\",\n      \"name\": \"David Morris-Oliveros\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/89823400?v=4\",\n      \"profile\": \"https://github.com/dmo-j-cube\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"BenoitConnan\",\n      \"name\": \"BenoitConnan\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/82808268?v=4\",\n      \"profile\": \"https://github.com/BenoitConnan\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Malthaldar\",\n      \"name\": \"Malthaldar\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/33671694?v=4\",\n      \"profile\": \"https://github.com/Malthaldar\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"svenneve\",\n      \"name\": \"Sven Neve\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/2472863?v=4\",\n      \"profile\": \"http://www.svenneve.com/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"zafrs\",\n      \"name\": \"zafrs\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/26890002?v=4\",\n      \"profile\": \"https://github.com/zafrs\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Tilix4\",\n      \"name\": \"Félix David\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/22875539?v=4\",\n      \"profile\": \"http://felixdavid.com/\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"movalex\",\n      \"name\": \"Alexey Bogomolov\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/11698866?v=4\",\n      \"profile\": \"http://abogomolov.com\",\n      \"contributions\": [\n        \"code\"\n      ]\n    }\n  ],\n  \"contributorsPerLine\": 7,\n  \"skipCi\": true,\n  \"commitType\": \"docs\"\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": "# Created by .ignore support plugin (hsz.mobi)\n### Python template\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n.poetry/\n.github/\nvendor/bin/\nvendor/python/\ndocs/\nwebsite/\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto\n*.sh text eol=lf\n*.command eol=lf\n*.bat text eol=crlf\n*.js  eol=lf\n*.c   eol=lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: File a bug report\ntitle: 'Bug: '\nlabels:\n  - 'type: bug'\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n  - type: checkboxes\n    attributes:\n      label: Is there an existing issue for this?\n      description: >-\n        Please search to see if an issue already exists for the bug you\n        encountered.\n      options:\n        - label: I have searched the existing issues\n          required: true\n  - type: textarea\n    attributes:\n      label: 'Current Behavior:'\n      description: A concise description of what you're experiencing.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: 'Expected Behavior:'\n      description: A concise description of what you expected to happen.\n    validations:\n      required: false\n  - type: dropdown\n    id: _version\n    attributes:\n      label: Version\n      description: What version are you running? Look to OpenPype Tray\n      options:\n        - 3.18.12-nightly.26\n        - 3.18.12-nightly.25\n        - 3.18.12-nightly.24\n        - 3.18.12-nightly.23\n        - 3.18.12-nightly.22\n        - 3.18.12-nightly.21\n        - 3.18.12-nightly.20\n        - 3.18.12-nightly.19\n        - 3.18.12-nightly.18\n        - 3.18.12-nightly.17\n        - 3.18.12-nightly.16\n        - 3.18.12-nightly.15\n        - 3.18.12-nightly.14\n        - 3.18.12-nightly.13\n        - 3.18.12-nightly.12\n        - 3.18.12-nightly.11\n        - 3.18.12-nightly.10\n        - 3.18.12-nightly.9\n        - 3.18.12-nightly.8\n        - 3.18.12-nightly.7\n        - 3.18.12-nightly.6\n        - 3.18.12-nightly.5\n        - 3.18.12-nightly.4\n        - 3.18.12-nightly.3\n        - 3.18.12-nightly.2\n        - 3.18.12-nightly.1\n        - 3.18.11\n        - 3.18.11-nightly.10\n        - 3.18.11-nightly.9\n        - 3.18.11-nightly.8\n        - 3.18.11-nightly.7\n        - 3.18.11-nightly.6\n        - 3.18.11-nightly.5\n        - 3.18.11-nightly.4\n        - 3.18.11-nightly.3\n        - 3.18.11-nightly.2\n        - 3.18.11-nightly.1\n        - 3.18.10\n        - 3.18.10-nightly.2\n        - 3.18.10-nightly.1\n        - 3.18.9\n        - 3.18.9-nightly.11\n        - 3.18.9-nightly.10\n        - 3.18.9-nightly.9\n        - 3.18.9-nightly.8\n        - 3.18.9-nightly.7\n        - 3.18.9-nightly.6\n        - 3.18.9-nightly.5\n        - 3.18.9-nightly.4\n        - 3.18.9-nightly.3\n        - 3.18.9-nightly.2\n        - 3.18.9-nightly.1\n        - 3.18.8\n        - 3.18.8-nightly.2\n        - 3.18.8-nightly.1\n        - 3.18.7\n        - 3.18.7-nightly.5\n        - 3.18.7-nightly.4\n        - 3.18.7-nightly.3\n        - 3.18.7-nightly.2\n        - 3.18.7-nightly.1\n        - 3.18.6\n        - 3.18.6-nightly.2\n        - 3.18.6-nightly.1\n        - 3.18.5\n        - 3.18.5-nightly.3\n        - 3.18.5-nightly.2\n        - 3.18.5-nightly.1\n        - 3.18.4\n        - 3.18.4-nightly.1\n        - 3.18.3\n        - 3.18.3-nightly.2\n        - 3.18.3-nightly.1\n        - 3.18.2\n        - 3.18.2-nightly.6\n        - 3.18.2-nightly.5\n        - 3.18.2-nightly.4\n        - 3.18.2-nightly.3\n        - 3.18.2-nightly.2\n        - 3.18.2-nightly.1\n        - 3.18.1\n        - 3.18.1-nightly.1\n        - 3.18.0\n        - 3.17.7\n        - 3.17.7-nightly.7\n        - 3.17.7-nightly.6\n        - 3.17.7-nightly.5\n        - 3.17.7-nightly.4\n        - 3.17.7-nightly.3\n        - 3.17.7-nightly.2\n        - 3.17.7-nightly.1\n        - 3.17.6\n        - 3.17.6-nightly.3\n        - 3.17.6-nightly.2\n        - 3.17.6-nightly.1\n        - 3.17.5\n        - 3.17.5-nightly.3\n        - 3.17.5-nightly.2\n        - 3.17.5-nightly.1\n        - 3.17.4\n    validations:\n      required: true\n  - type: dropdown\n    validations:\n      required: true\n    attributes:\n      label: What platform you are running OpenPype on?\n      description: |\n        Please specify the operating systems you are running OpenPype with.\n      multiple: true\n      options:\n        - Windows\n        - Linux / Centos\n        - Linux / Ubuntu\n        - Linux / RedHat\n        - MacOS\n  - type: textarea\n    id: to-reproduce\n    attributes:\n      label: 'Steps To Reproduce:'\n      description: Steps to reproduce the behavior.\n      placeholder: |\n        1. How did the configuration look like\n        2. What type of action was made\n    validations:\n      required: true\n  - type: checkboxes\n    attributes:\n      label: Are there any labels you wish to add?\n      description: Please search labels and identify those related to your bug.\n      options:\n        - label: I have added the relevant labels to the bug report.\n          required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: 'Relevant log output:'\n      description: >-\n        Please copy and paste any relevant log output. This will be\n        automatically formatted into code, so no need for backticks.\n      render: shell\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: 'Additional context:'\n      description: Add any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Ynput Community Discussions\n    url: https://community.ynput.io\n    about: Please ask and answer questions here.\n  - name: Ynput Discord Server\n    url: https://discord.gg/ynput\n    about: For community quick chats."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/enhancement_request.yml",
    "content": "name: Enhancement Request\ndescription: Create a report to help us enhance a particular feature\ntitle: \"Enhancement: \"\nlabels:\n  - \"type: enhancement\"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this enhancement request report!\n  - type: checkboxes\n    attributes:\n      label: Is there an existing issue for this?\n      description: Please search to see if an issue already exists for the bug you encountered.\n      options:\n      - label: I have searched the existing issues.\n        required: true\n  - type: textarea\n    id: related-feature\n    attributes:\n      label: Please describe the feature you have in mind and explain what the current shortcomings are?\n      description: A clear and concise description of what the problem is.\n    validations:\n      required: true\n  - type: textarea\n    id: enhancement-proposal\n    attributes:\n      label: How would you imagine the implementation of the feature?\n      description: A clear and concise description of what you want to happen.\n    validations:\n      required: true\n  - type: checkboxes\n    attributes:\n      label: Are there any labels you wish to add?\n      description: Please search labels and identify those related to your enhancement.\n      options:\n      - label: I have added the relevant labels to the enhancement request.\n        required: true\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: \"Describe alternatives you've considered:\"\n      description: A clear and concise description of any alternative solutions or features you've considered.\n    validations:\n      required: false\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: \"Additional context:\"\n      description: Add any other context or screenshots about the enhancement request here.\n    validations:\n      required: false"
  },
  {
    "path": ".github/pr-branch-labeler.yml",
    "content": "# Apply label \"feature\" if head matches \"feature/*\"\n'type: feature':\n  head: \"feature/*\"\n\n# Apply label \"feature\" if head matches \"feature/*\"\n'type: enhancement':\n  head: \"enhancement/*\"\n\n# Apply label \"bugfix\" if head matches one of \"bugfix/*\" or \"hotfix/*\"\n'type: bug':\n  head: [\"bugfix/*\", \"hotfix/*\"]\n\n# Apply label \"release\" if base matches \"release/*\"\n'Bump Minor':\n  base: \"release/next-minor\"\n"
  },
  {
    "path": ".github/pr-glob-labeler.yml",
    "content": "# Add type: unittest label if any changes in tests folders\n'type: unittest':\n- '*/*tests*/**/*'\n\n# any changes in documentation structure\n'type: documentation':\n- '*/**/*website*/**/*'\n- '*/**/*docs*/**/*'\n\n# hosts triage\n'host: Nuke':\n- '*/**/*nuke*'\n- '*/**/*nuke*/**/*'\n\n'host: Photoshop':\n- '*/**/*photoshop*'\n- '*/**/*photoshop*/**/*'\n\n'host: Harmony':\n- '*/**/*harmony*'\n- '*/**/*harmony*/**/*'\n\n'host: UE':\n- '*/**/*unreal*'\n- '*/**/*unreal*/**/*'\n\n'host: Houdini':\n- '*/**/*houdini*'\n- '*/**/*houdini*/**/*'\n\n'host: Maya':\n- '*/**/*maya*'\n- '*/**/*maya*/**/*'\n\n'host: Resolve':\n- '*/**/*resolve*'\n- '*/**/*resolve*/**/*'\n\n'host: Blender':\n- '*/**/*blender*'\n- '*/**/*blender*/**/*'\n\n'host: Hiero':\n- '*/**/*hiero*'\n- '*/**/*hiero*/**/*'\n\n'host: Fusion':\n- '*/**/*fusion*'\n- '*/**/*fusion*/**/*'\n\n'host: Flame':\n- '*/**/*flame*'\n- '*/**/*flame*/**/*'\n\n'host: TrayPublisher':\n- '*/**/*traypublisher*'\n- '*/**/*traypublisher*/**/*'\n\n'host: 3dsmax':\n- '*/**/*max*'\n- '*/**/*max*/**/*'\n\n'host: TV Paint':\n- '*/**/*tvpaint*'\n- '*/**/*tvpaint*/**/*'\n\n'host: CelAction':\n- '*/**/*celaction*'\n- '*/**/*celaction*/**/*'\n\n'host: After Effects':\n- '*/**/*aftereffects*'\n- '*/**/*aftereffects*/**/*'\n\n'host: Substance Painter':\n- '*/**/*substancepainter*'\n- '*/**/*substancepainter*/**/*'\n\n# modules triage\n'module: Deadline':\n- '*/**/*deadline*'\n- '*/**/*deadline*/**/*'\n\n'module: RoyalRender':\n- '*/**/*royalrender*'\n- '*/**/*royalrender*/**/*'\n\n'module: Sitesync':\n- '*/**/*sync_server*'\n- '*/**/*sync_server*/**/*'\n\n'module: Ftrack':\n- '*/**/*ftrack*'\n- '*/**/*ftrack*/**/*'\n\n'module: Shotgrid':\n- '*/**/*shotgrid*'\n- '*/**/*shotgrid*/**/*'\n\n'module: Kitsu':\n- '*/**/*kitsu*'\n- '*/**/*kitsu*/**/*'\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Changelog Description\nParagraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation.\n\n## Additional info\nParagraphs of text giving context of additional technical information or code examples.\n\n## Testing notes:\n1. start with this step\n2. follow this step\n"
  },
  {
    "path": ".github/workflows/documentation.yml",
    "content": "name: 📜 Documentation\n\non:\n  pull_request:\n    branches: [develop]\n    types: [review_requested, ready_for_review]\n    paths:\n    - 'website/**'\n  push:\n    branches: [main]\n    paths:\n    - 'website/**'\n  workflow_dispatch:\n\njobs:\n  check-build:\n    if: github.event_name != 'push'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v1\n      - uses: actions/setup-node@v1\n        with:\n          node-version: 14.x\n          cache: yarn\n      - name: Test Build\n        run: |\n          cd website\n          if [ -e yarn.lock ]; then\n          yarn install --frozen-lockfile\n          elif [ -e package-lock.json ]; then\n          npm ci\n          else\n          npm i\n          fi\n          npm run build\n  deploy-website:\n    if: github.event_name != 'pull_request'\n    runs-on: ubuntu-latest\n    steps:\n      - name: 🚚 Get latest code\n        uses: actions/checkout@v2\n\n      - uses: actions/setup-node@v1\n        with:\n          node-version: 14.x\n          cache: yarn\n      - name: 🔨 Build\n        run: |\n          cd website\n          if [ -e yarn.lock ]; then\n          yarn install --frozen-lockfile\n          elif [ -e package-lock.json ]; then\n          npm ci\n          else\n          npm i\n          fi\n          npm run build\n\n      - name: 📂 Sync files\n        uses: SamKirkland/FTP-Deploy-Action@4.0.0\n        with:\n          server: ftp.openpype.io\n          username: ${{ secrets.ftp_user }}\n          password: ${{ secrets.ftp_password }}\n          local-dir: ./website/build/"
  },
  {
    "path": ".github/workflows/milestone_assign.yml",
    "content": "name: 👉🏻 Milestone - assign to PRs\n\non:\n  pull_request_target:\n    types: [closed]\n\njobs:\n  run_if_release:\n    if:  startsWith(github.base_ref, 'release/')\n    runs-on: ubuntu-latest\n    steps:\n      - name: 'Assign Milestone [next-minor]'\n        if: github.event.pull_request.milestone == null\n        uses: zoispag/action-assign-milestone@v1\n        with:\n          repo-token: \"${{ secrets.YNPUT_BOT_TOKEN }}\"\n          milestone: 'next-minor'\n\n  run_if_develop:\n    if:  ${{ github.base_ref == 'develop' }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: 'Assign Milestone [next-patch]'\n        if: github.event.pull_request.milestone == null\n        uses: zoispag/action-assign-milestone@v1\n        with:\n          repo-token: \"${{ secrets.YNPUT_BOT_TOKEN }}\"\n          milestone: 'next-patch'\n"
  },
  {
    "path": ".github/workflows/milestone_create.yml",
    "content": "name: ➕ Milestone - create default\n\non:\n  milestone:\n    types: [closed, edited]\n\njobs:\n  generate-next-patch:\n    runs-on: ubuntu-latest\n    steps:\n      - name: 'Get Milestones'\n        uses: \"WyriHaximus/github-action-get-milestones@master\"\n        id: milestones\n        env:\n          GITHUB_TOKEN: \"${{ secrets.YNPUT_BOT_TOKEN }}\"\n\n      - run: printf \"name=number::%s\" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[]  | select(.title == $MILESTONE) | .number')\n        id: querymilestone\n        env:\n          MILESTONES: ${{ steps.milestones.outputs.milestones }}\n          MILESTONE: \"next-patch\"\n\n      - name: Read output\n        run: |\n          echo \"${{ steps.querymilestone.outputs.number }}\"\n\n      - name: 'Create `next-patch` milestone'\n        if: steps.querymilestone.outputs.number == ''\n        id: createmilestone\n        uses: \"WyriHaximus/github-action-create-milestone@v1\"\n        with:\n          title: 'next-patch'\n        env:\n          GITHUB_TOKEN: \"${{ secrets.YNPUT_BOT_TOKEN }}\"\n\n  generate-next-minor:\n    runs-on: ubuntu-latest\n    steps:\n      - name: 'Get Milestones'\n        uses: \"WyriHaximus/github-action-get-milestones@master\"\n        id: milestones\n        env:\n          GITHUB_TOKEN: \"${{ secrets.YNPUT_BOT_TOKEN }}\"\n\n      - run: printf \"name=number::%s\" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[]  | select(.title == $MILESTONE) | .number')\n        id: querymilestone\n        env:\n          MILESTONES: ${{ steps.milestones.outputs.milestones }}\n          MILESTONE: \"next-minor\"\n\n      - name: Read output\n        run: |\n          echo \"${{ steps.querymilestone.outputs.number }}\"\n\n      - name: 'Create `next-minor` milestone'\n        if: steps.querymilestone.outputs.number == ''\n        id: createmilestone\n        uses: \"WyriHaximus/github-action-create-milestone@v1\"\n        with:\n          title: 'next-minor'\n        env:\n          GITHUB_TOKEN: \"${{ secrets.YNPUT_BOT_TOKEN }}\"\n"
  },
  {
    "path": ".github/workflows/miletone_release_trigger.yml",
    "content": "name: 🚩 Milestone Release [trigger]\n\non:\n  workflow_dispatch:\n    inputs:\n      milestone:\n        required: true\n  milestone:\n    types: closed\n\n\njobs:\n  milestone-title:\n    runs-on: ubuntu-latest\n    outputs:\n      milestone: ${{ steps.milestoneTitle.outputs.value }}\n    steps:\n      - name: Switch input milestone\n        uses: haya14busa/action-cond@v1\n        id: milestoneTitle\n        with:\n          cond: ${{ inputs.milestone == '' }}\n          if_true: ${{ github.event.milestone.title }}\n          if_false: ${{ inputs.milestone }}\n      - name: Print resulted milestone\n        run: |\n          echo \"${{ steps.milestoneTitle.outputs.value }}\"\n\n  call-ci-tools-milestone-release:\n    needs: milestone-title\n    uses: ynput/ci-tools/.github/workflows/milestone_release_ref.yml@main\n    with:\n      milestone: ${{ needs.milestone-title.outputs.milestone }}\n      repo-owner: ${{ github.event.repository.owner.login }}\n      repo-name: ${{ github.event.repository.name }}\n      version-py-path: \"./openpype/version.py\"\n      pyproject-path: \"./pyproject.toml\"\n    secrets:\n      token: ${{ secrets.YNPUT_BOT_TOKEN }}\n      user_email: ${{ secrets.CI_EMAIL }}\n      user_name: ${{ secrets.CI_USER }}\n      cu_api_key: ${{ secrets.CLICKUP_API_KEY }}\n      cu_team_id: ${{ secrets.CLICKUP_TEAM_ID }}\n      cu_field_id: ${{ secrets.CLICKUP_RELEASE_FIELD_ID }}\n"
  },
  {
    "path": ".github/workflows/nightly_merge.yml",
    "content": "name: 🔀 Dev -> Main\n\non:\n  schedule:\n    - cron:  '21 3 * * 3,6'\n  workflow_dispatch:\n\njobs:\n  develop-to-main:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: 🚛 Checkout Code\n      uses: actions/checkout@v2\n\n    - name: 🔨 Merge develop to main\n      uses: everlytic/branch-merge@1.1.0\n      with:\n        github_token: ${{ secrets.YNPUT_BOT_TOKEN }}\n        source_ref: 'develop'\n        target_branch: 'main'\n        commit_message_template: '[Automated] Merged {source_ref} into {target_branch}'\n\n    - name: Invoke pre-release workflow\n      uses: benc-uk/workflow-dispatch@v1\n      with:\n        workflow: prerelease.yml\n        token: ${{ secrets.YNPUT_BOT_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/pr_labels.yml",
    "content": "name: 🔖 PR labels\n\non:\n  pull_request_target:\n    types: [opened, assigned]\n\njobs:\n  size-label:\n    name: pr_size_label\n    runs-on: ubuntu-latest\n    if: github.event.action == 'assigned' || github.event.action == 'opened'\n    steps:\n      - name: Add size label\n        uses: \"pascalgn/size-label-action@v0.4.3\"\n        env:\n          GITHUB_TOKEN: \"${{ secrets.YNPUT_BOT_TOKEN }}\"\n          IGNORED: \".gitignore\\n*.md\\n*.json\"\n        with:\n          sizes: >\n            {\n              \"0\": \"XS\",\n              \"100\": \"S\",\n              \"500\": \"M\",\n              \"1000\": \"L\",\n              \"1500\": \"XL\",\n              \"2500\": \"XXL\"\n            }\n\n  label_prs_branch:\n    name: pr_branch_label\n    runs-on: ubuntu-latest\n    if: github.event.action == 'assigned' || github.event.action == 'opened'\n    steps:\n    - name: Label PRs - Branch name detection\n      uses: ffittschen/pr-branch-labeler@v1\n      with:\n        repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}\n\n  label_prs_globe:\n    name: pr_globe_label\n    runs-on: ubuntu-latest\n    if: github.event.action == 'assigned' || github.event.action == 'opened'\n    steps:\n    - name: Label PRs - Globe detection\n      uses: actions/labeler@v4.0.3\n      with:\n        repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}\n        configuration-path: \".github/pr-glob-labeler.yml\"\n        sync-labels: false\n"
  },
  {
    "path": ".github/workflows/prerelease.yml",
    "content": "name: ⏳ Nightly Prerelease\n\non:\n  workflow_dispatch:\n\n\njobs:\n  create_nightly:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: 🚛 Checkout Code\n      uses: actions/checkout@v2\n      with:\n        fetch-depth: 0\n\n    - name: Set up Python\n      uses: actions/setup-python@v2\n      with:\n        python-version: 3.9\n\n    - name: Install Python requirements\n      run: pip install gitpython semver PyGithub\n\n    - name: 🔎 Determine next version type\n      id: version_type\n      run: |\n        TYPE=$(python ./tools/ci_tools.py --bump --github_token ${{ secrets.YNPUT_BOT_TOKEN }})\n        echo \"type=${TYPE}\" >> $GITHUB_OUTPUT\n\n    - name: 💉 Inject new version into files\n      id: version\n      if: steps.version_type.outputs.type != 'skip'\n      run: |\n        NEW_VERSION_TAG=$(python ./tools/ci_tools.py --nightly --github_token ${{ secrets.YNPUT_BOT_TOKEN }})\n        echo \"next_tag=${NEW_VERSION_TAG}\" >> $GITHUB_OUTPUT\n\n    - name: 💾 Commit and Tag\n      id: git_commit\n      if: steps.version_type.outputs.type != 'skip'\n      run: |\n        git config user.email ${{ secrets.CI_EMAIL }}\n        git config user.name ${{ secrets.CI_USER }}\n        git checkout main\n        git pull\n        git add .\n        git commit -m \"[Automated] Bump version\"\n        tag_name=\"CI/${{ steps.version.outputs.next_tag }}\"\n        echo $tag_name\n        git tag -a $tag_name -m \"nightly build\"\n\n    - name: Push to protected main branch\n      uses: CasperWA/push-protected@v2.10.0\n      with:\n        token: ${{ secrets.YNPUT_BOT_TOKEN }}\n        branch: main\n        tags: true\n        unprotect_reviews: true\n\n    - name: 🔨 Merge main back to develop\n      uses: everlytic/branch-merge@1.1.0\n      if: steps.version_type.outputs.type != 'skip'\n      with:\n        github_token: ${{ secrets.YNPUT_BOT_TOKEN }}\n        source_ref: 'main'\n        target_branch: 'develop'\n        commit_message_template: '[Automated] Merged {source_ref} into {target_branch}'\n\n    - name: Invoke Update bug report workflow\n      uses: benc-uk/workflow-dispatch@v1\n      with:\n        workflow: update_bug_report.yml\n        token: ${{ secrets.YNPUT_BOT_TOKEN }}"
  },
  {
    "path": ".github/workflows/project_task_statuses.yml",
    "content": "name: 📊 Project task statuses\n\non:\n  pull_request_review:\n    types: [submitted]\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n\njobs:\n\n  pr_review_started:\n    name: pr_review_started\n    runs-on: ubuntu-latest\n    # -----------------------------\n    # conditions are:\n    #   - PR issue comment which is not form Ynbot\n    #   - PR review comment which is not Hound (or any other bot)\n    #   - PR review submitted which is not from Hound (or any other bot) and is not 'Changes requested'\n    #   - make sure it only runs if not forked repo\n    # -----------------------------\n    if: |\n      (github.event_name == 'issue_comment' && github.event.pull_request.head.repo.owner.login == 'ynput' && github.event.comment.user.id != 82967070) ||\n      (github.event_name == 'pull_request_review_comment' && github.event.pull_request.head.repo.owner.login == 'ynput' && github.event.comment.user.type != 'Bot') ||\n      (github.event_name == 'pull_request_review' &&\n      github.event.pull_request.head.repo.owner.login == 'ynput' &&\n      github.event.review.state != 'changes_requested' &&\n      github.event.review.state != 'approved' &&\n      github.event.review.user.type != 'Bot')\n    steps:\n      - name: Move PR to 'Review In Progress'\n        uses: leonsteinhaeuser/project-beta-automations@v2.1.0\n        with:\n          gh_token: ${{ secrets.YNPUT_BOT_TOKEN }}\n          organization: ynput\n          project_id: 11\n          resource_node_id: ${{ github.event.pull_request.node_id || github.event.issue.node_id }}\n          status_value: Review In Progress\n\n  pr_review_requested:\n    # -----------------------------\n    # Resets Clickup Task status to 'In Progress' after 'Changes Requested' were submitted to PR\n    # It only runs if custom clickup task id was found in ref branch of PR\n    # -----------------------------\n    name: pr_review_requested\n    runs-on: ubuntu-latest\n    if: github.event_name == 'pull_request_review' && github.event.pull_request.head.repo.owner.login == 'ynput' && github.event.review.state == 'changes_requested'\n    steps:\n      - name: Set branch env\n        run: echo \"BRANCH_NAME=${{ github.event.pull_request.head.ref}}\" >> $GITHUB_ENV\n      - name: Get ClickUp ID from ref head name\n        id: get_cuID\n        run: |\n          echo ${{ env.BRANCH_NAME }}\n          echo \"cuID=$(echo $BRANCH_NAME | sed 's/.*\\/\\(OP\\-[0-9]\\{4\\}\\).*/\\1/')\" >> $GITHUB_OUTPUT\n\n      - name: Print ClickUp ID\n        run: echo ${{ steps.get_cuID.outputs.cuID }}\n\n      - name: Move found Clickup task to 'Review in Progress'\n        if: steps.get_cuID.outputs.cuID\n        run: |\n         curl -i -X PUT \\\n          'https://api.clickup.com/api/v2/task/${{ steps.get_cuID.outputs.cuID }}?custom_task_ids=true&team_id=${{secrets.CLICKUP_TEAM_ID}}' \\\n          -H 'Authorization: ${{secrets.CLICKUP_API_KEY}}' \\\n          -H 'Content-Type: application/json' \\\n          -d '{\n             \"status\": \"in progress\"\n           }'\n"
  },
  {
    "path": ".github/workflows/test_build.yml",
    "content": "# This workflow will upload a Python Package using Twine when a release is created\n# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries\n\nname: 🏗️ Test Build\n\non:\n  pull_request:\n    branches: [develop]\n    types: [review_requested, ready_for_review]\n    paths-ignore:\n    - 'docs/**'\n    - 'website/**'\n    - 'vendor/**'\n\njobs:\n  Windows-latest:\n\n    runs-on: windows-latest\n    strategy:\n      matrix:\n        python-version: [3.9]\n\n    steps:\n    - name: 🚛 Checkout Code\n      uses: actions/checkout@v2\n\n    - name: Set up Python\n      uses: actions/setup-python@v2\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: 🧵 Install Requirements\n      shell: pwsh\n      run: |\n        ./tools/create_env.ps1\n\n    - name: 🔨 Build\n      shell: pwsh\n      run: |\n        $env:SKIP_THIRD_PARTY_VALIDATION=\"1\"\n        ./tools/build.ps1\n\n  Ubuntu-latest:\n\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: [3.9]\n\n    steps:\n    - name: 🚛 Checkout Code\n      uses: actions/checkout@v2\n\n    - name: Set up Python\n      uses: actions/setup-python@v2\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: 🧵 Install Requirements\n      run: |\n        ./tools/create_env.sh\n\n    - name: 🔨 Build\n      run: |\n        export SKIP_THIRD_PARTY_VALIDATION=\"1\"\n        ./tools/build.sh\n"
  },
  {
    "path": ".github/workflows/update_bug_report.yml",
    "content": "name: 🐞 Update Bug Report\n\non:\n  workflow_dispatch:\n  release:\n    # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release\n    types: [published]\n\njobs:\n  update-bug-report:\n    runs-on: ubuntu-latest\n    name: Update bug report\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          ref: ${{ github.event.release.target_commitish }}\n      - name: Update version\n        uses: ynput/gha-populate-form-version@main\n        with:\n          github_token: ${{ secrets.YNPUT_BOT_TOKEN }}\n          registry: github\n          dropdown: _version\n          limit_to: 100\n          form: .github/ISSUE_TEMPLATE/bug_report.yml\n          commit_message: 'chore(): update bug report / version'\n          dry_run: no-push\n\n      - name: Push to protected develop branch\n        uses: CasperWA/push-protected@v2.10.0\n        with:\n          token: ${{ secrets.YNPUT_BOT_TOKEN }}\n          branch: develop\n          unprotect_reviews: true"
  },
  {
    "path": ".gitignore",
    "content": "# Created by .ignore support plugin (hsz.mobi)\n### Python template\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# Mac Stuff\n###########\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n# Icon must end with two \\r\nIcon\n# Thumbnails\n._*\n# rope project dir\n.ropeproject\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n\n# CX_Freeze\n###########\n/build\n/dist/\n/server_addon/packages/*\n\n/vendor/bin/*\n/vendor/python/*\n/.venv\n/venv/\n\n# Documentation\n###############\n/docs/build\n\n# Editor backup files #\n#######################\n*~\n\n# Unit test / coverage reports\n##############################\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n/coverage\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Node JS packages\n##################\nnode_modules\npackage-lock.json\npackage.json\nyarn.lock\n\nopenpype/premiere/ppro/js/debug.log\n\n\n# IDEA\n######\n.idea/\n\n# VScode files\n.vscode/\n.env\ndump.sql\ntest_localsystem.txt\n\n# website\n##########\nwebsite/translated_docs\nwebsite/build/\nwebsite/node_modules\nwebsite/i18n/*\n\nwebsite/debug.log\n\nwebsite/.docusaurus\n\n# Poetry\n########\n\n.poetry/\n.python-version\n.editorconfig\n.pre-commit-config.yaml\nmypy.ini\n\ntools/run_eventserver.*\n\n#  Developer tools\ntools/dev_*\n\n.github_changelog_generator\n\n\n# Addons\n########\n/openpype/addons/*\n!/openpype/addons/README.md\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"tools/modules/powershell/BurntToast\"]\n    path = tools/modules/powershell/BurntToast\n    url = https://github.com/Windos/BurntToast.git\n\n[submodule \"tools/modules/powershell/PSWriteColor\"]\n    path = tools/modules/powershell/PSWriteColor\n    url = https://github.com/EvotecIT/PSWriteColor.git\n[submodule \"openpype/hosts/unreal/integration\"]\n\tpath = openpype/hosts/unreal/integration\n\turl = https://github.com/ynput/ayon-unreal-plugin.git\n"
  },
  {
    "path": ".hound.yml",
    "content": "flake8:\r\n  enabled: true\r\n  config_file: setup.cfg\r\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"tabWidth\": 4\n}\n"
  },
  {
    "path": "ARCHITECTURE.md",
    "content": "# Architecture\n\nOpenPype is a monolithic Python project that bundles several parts, this document will try to give a birds eye overview of the project and, to a certain degree, each of the sub-projects.\nThe current file structure looks like this:\n\n```\n.\n├── common - Code in this folder is backend portion of Addon distribution logic for v4 server.\n├── docs - Documentation of the source code.\n├── igniter - The OpenPype bootstrapper, deals with running version resolution and setting up the connection to the mongodb.\n├── openpype - The actual OpenPype core package.\n├── schema - Collection of JSON files describing schematics of objects. This follows Avalon's convention.\n├── tests - Integration and unit tests.\n├── tools - Conveninece scripts to perform common actions (in both bash and ps1).\n├── vendor - When using the igniter, it deploys third party tools in here, such as ffmpeg.\n└── website - Source files for https://openpype.io/ which is Docusaursus (https://docusaurus.io/).\n```\n\nThe core functionality of the pipeline can be found in `igniter` and `openpype`, which in turn rely on the `schema` files, whenever you build (or download a pre-built) version of OpenPype, these two are bundled in there, and `Igniter` is the entry point.\n\n\n## Igniter\n\nIt's the setup and update tool for OpenPype, unless you want to package `openpype` separately and deal with all the config manually, this will most likely be your entry point.\n\n```\nigniter/\n├── bootstrap_repos.py - Module that will find or install OpenPype versions in the system.\n├── __init__.py - Igniter entry point.\n├── install_dialog.py- Show dialog for choosing central pype repository.\n├── install_thread.py - Threading helpers for the install process.\n├── __main__.py - Like `__init__.py` ?\n├── message_dialog.py - Qt Dialog with a message and \"Ok\" button.\n├── nice_progress_bar.py - Fancy Qt progress bar.\n├── splash.txt - ASCII art for the terminal installer.\n├── stylesheet.css - Installer Qt styles.\n├── terminal_splash.py - Terminal installer animation, relies in `splash.txt`.\n├── tools.py - Collection of methods that don't fit in other modules.\n├── update_thread.py - Threading helper to update existing OpenPype installs.\n├── update_window.py - Qt UI to update OpenPype installs. \n├── user_settings.py - Interface for the OpenPype user settings.\n└── version.py - Igniter's version number.\n```\n\n## OpenPype\n\nThis is the main package of the OpenPype logic, it could be loosely described as a combination of [Avalon](https://getavalon.github.io), [Pyblish](https://pyblish.com/) and glue around those with custom OpenPype only elements, things are in progress of being moved around to better prepare for V4, which will be released under a new name AYON.\n\n```\nopenpype/\n├── client - Interface for the MongoDB.\n├── hooks - Hooks to be executed on certain OpenPype Applications defined in `openpype.lib.applications`.\n├── host - Base class for the different hosts.\n├── hosts - Integration with the different DCCs (hosts) using the `host` base class.\n├── lib - Libraries that stitch together the package, some have been moved into other parts.\n├── modules - OpenPype modules should contain separated logic of specific kind of implementation, such as Ftrack connection and its python API.\n├── pipeline - Core of the OpenPype pipeline, handles creation of data, publishing, etc.\n├── plugins - Global/core plugins for loader and publisher tool.\n├── resources - Icons, fonts, etc.\n├── scripts - Loose scipts that get run by tools/publishers.\n├── settings - OpenPype settings interface.\n├── style - Qt styling.\n├── tests - Unit tests.\n├── tools - Core tools, check out https://openpype.io/docs/artist_tools.\n├── vendor - Vendoring of needed required Python packes.\n├── widgets - Common re-usable Qt Widgets.\n├── action.py - LEGACY: Lives now in `openpype.pipeline.publish.action` Pyblish actions.\n├── cli.py - Command line interface, leverages `click`.\n├── __init__.py - Sets two constants.\n├── __main__.py - Entry point, calls the `cli.py`\n├── plugin.py - Pyblish plugins.\n├── pype_commands.py - Implementation of OpenPype commands.\n└── version.py - Current version number.\n```\n\n\n\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n\n## [3.18.11](https://github.com/ynput/OpenPype/tree/3.18.11)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.10...3.18.11)\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Deadline: Houdini submission settings in OP <a href=\"https://github.com/ynput/OpenPype/pull/6269\">#6269</a></summary>\n\nMake houdini submissions respect pools groups.This is done by:\n- Make collect pools works with some Houdini families/product types.\n- Make Ayon Houdini submitters get group names from Houdini deadline settings.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Ensure unique class name compared to `extract_yeti_cache.py` <a href=\"https://github.com/ynput/OpenPype/pull/6251\">#6251</a></summary>\n\nFix duplicate `ExtractYetiCache` plug-in name.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Correct Alembic export defaults AY-5273 <a href=\"https://github.com/ynput/OpenPype/pull/6268\">#6268</a></summary>\n\nMissing `writeUVs` on the Alembic extraction.\n\n\n___\n\n</details>\n\n\n\n\n## [3.18.10](https://github.com/ynput/OpenPype/tree/3.18.10)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.9...3.18.10)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Arnold Scene Source Raw - OP-8014 <a href=\"https://github.com/ynput/OpenPype/pull/6182\">#6182</a></summary>\n\nThis PR is to try and re-instate some flexibility to the `Arnold Scene Source` family, which got restricted by https://github.com/ynput/OpenPype/pull/4449The proxy workflow introduced was actually broken due to https://github.com/ynput/OpenPype/pull/4460.We can now have any nodes directly in the instance set, which should be backwards compatible of the `Arnold Scene Source` before the overhaul in https://github.com/ynput/OpenPype/pull/4449.The `content` and `proxy` sets works as well, but not at the same time as the raw nodes directly in the instance set. There is a validator in place to prevent using a single instance for both workflows.Now the question is whether we should have this as a single family or split somehow?The workflow of having nodes directly in the instance set, compared to `content` and `proxy` set, can be documented, so I see this as most a matter of terminology.`Arnold Scene Source` makes sense to have as a family, but only if its a the raw output with little to no validation, similar to `Maya Scene`. But then I'm not sure what to call the other family that has more of a workflow in place, which is similar to `Model` and `Pointcache`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Push to project - AY-742 <a href=\"https://github.com/ynput/OpenPype/pull/6245\">#6245</a></summary>\n\nThis introduces the \"Push to Project\" menu item in Nuke.This enables users to push the current workfile to a different project and copy all files from Read nodes to a `resources` folder next to the workfile. Containers will be baked to normal Read nodes.Also gizmos will be baked to groups.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Max: Implementation of Validate Render Passes <a href=\"https://github.com/ynput/OpenPype/pull/6138\">#6138</a></summary>\n\nThis PR is to enhance the current validator of checking the render output before deadline publish. It does the following:\n- The validator `Render Output for Deadline` would be renamed as `Validate Render Passes`\n- The validator would not only check on the invalid render output folder but the invalid filename of render passes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max : Optional validator to check invalid context data <a href=\"https://github.com/ynput/OpenPype/pull/6198\">#6198</a></summary>\n\nAdd optional validator check on invalid context data for asset and task in 3dsMax\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Account for no Alembic overrides. <a href=\"https://github.com/ynput/OpenPype/pull/6267\">#6267</a></summary>\n\nFix for if no overrides are present in `project_settings/maya/publish/ExtractAlembic/overrides`\n\n\n___\n\n</details>\n\n\n\n\n## [3.18.9](https://github.com/ynput/OpenPype/tree/3.18.9)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.8...3.18.9)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Integration: 3DEqualizer integration <a href=\"https://github.com/ynput/OpenPype/pull/5868\">#5868</a></summary>\n\nThis PR is adding basic integration for 3DEqualizer4 from Science-D-Vision. Integration includes:\n- Workfiles\n- Loading plates (cameras)\n- Publishing scripts to Maya and Nuke\n- Publishing of lens data\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: abc options for Pointcache/Animation family - OP-5920 <a href=\"https://github.com/ynput/OpenPype/pull/5173\">#5173</a></summary>\n\nAdd all options for alembic extraction on `pointcache` and `animation` families.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>RoyalRender: environment injection on the server <a href=\"https://github.com/ynput/OpenPype/pull/6160\">#6160</a></summary>\n\nPreviously env vars were injected directly on the client during submission. That could have issues when environment on client machines is different than on workers.This PR tries similar approach as on DL when before job is rendered it queries Ayon to get environment variables for context.These variables are used to create `.rrEnv` file  and attach it to the job. That should provide rendering environment controlled by Ayon.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Hiero: colorspace settings aligned with nuke - AY-978 <a href=\"https://github.com/ynput/OpenPype/pull/6249\">#6249</a></summary>\n\nIn order to share the same colorspaces in the workfile in Hiero and Nuke, we need to bring back the workfile settings for colorspaces in Nuke.In Hiero we also need code to edit the project settings in memory and apply the colorspaces when launching Hiero so any new project gets the correct colorspaces. Due to Foundry not providing Python API methods for setting the project colorspaces, we need to go through the UI widgets to set them, when dealing with in-memory projects.Also small bugfix when saving the workfile without any sequences.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Make sure validators being shown in the Publisher UI when they set to be optional in AYON setting   <a href=\"https://github.com/ynput/OpenPype/pull/6257\">#6257</a></summary>\n\nThis PR is to make sure validators being shown correctly in the Publisher UI when they are being set to be optional in AYON setting.Ported from https://github.com/ynput/ayon-core/pull/201\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix Redshift cryptomatte multipartEXR <a href=\"https://github.com/ynput/OpenPype/pull/6240\">#6240</a></summary>\n\nWhen using Redshift and rendering multipart EXRs, the instances for cryptomatte AOVs are getting falsely marked as multipart EXR even though they are being forced to be separate files by Redshift.Since we cannot query the AOVs multipart individually, we'll need a hardcoded rule.Ideally I guess AOVs should be separate instances in the publishing process but that is too big of a scope atm.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Substance Painter: Allow users to set texture resolutions when loading mesh to create project <a href=\"https://github.com/ynput/OpenPype/pull/6262\">#6262</a></summary>\n\nThis PR is to add the support of template settings in the mesh loaders for Substance project creation. User can customize and add template settings in AYON settings and apply it through the option mode(the button with memo icon).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: Submit Publish job error <a href=\"https://github.com/ynput/OpenPype/pull/6263\">#6263</a></summary>\n\nUse get env to get the value of `AVALON_DB``AVALON_DB` environment variable is not initialized when using OpenPype in Ayon mode. which raise an error when using `os.environ[\"AVALON_DB\"]`This PR changes it to `os.getenv(\"AVALON_DB\")`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fix: Removed double conversion of limit_groups <a href=\"https://github.com/ynput/OpenPype/pull/6265\">#6265</a></summary>\n\n`limit_groups` settings got transformed twice. Kept nicer looking conversion.\n\n\n___\n\n</details>\n\n\n\n\n## [3.18.8](https://github.com/ynput/OpenPype/tree/3.18.8)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.7...3.18.8)\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Max: Implementation of Camera Attributes Validator <a href=\"https://github.com/ynput/OpenPype/pull/6110\">#6110</a></summary>\n\nImplement Validate Camera Attributes in camera family in Max host\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Add missing workfile creator <a href=\"https://github.com/ynput/OpenPype/pull/6203\">#6203</a></summary>\n\nAdd the missing workfile creator in 3dsMax.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: Expose families transfer setting - OP-8268 <a href=\"https://github.com/ynput/OpenPype/pull/6217\">#6217</a></summary>\n\nThis PR exposes the `families_transfer` attribute on the `ProcessSubmittedJobOnFarm` plugin.The use case is to remove `ftrack` from the list if a studio does not want all render passes from Maya to become asset versions in Ftrack.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Deadline: Add AVALON_DB to Deadline submissions - OP-8270 <a href=\"https://github.com/ynput/OpenPype/pull/6218\">#6218</a></summary>\n\nBecause testing uses a different database name https://github.com/ynput/OpenPype/blob/develop/tests/lib/testing_classes.py#L46 we need to add `AVALON_DB` to the environment for Deadline submissions.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: fix default render product name in Vray <a href=\"https://github.com/ynput/OpenPype/pull/6083\">#6083</a></summary>\n\nThis is fixing key name for default render products in VRay. Original name `RGB Color` caused issues during job submission.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Resolve Clip Load - Slate support <a href=\"https://github.com/ynput/OpenPype/pull/6126\">#6126</a></summary>\n\nLoaded clip should ignore the slate, and be trimmed the same regardless of slate presence.closes: https://github.com/ynput/OpenPype/issues/6124#AY-1684\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Use duration from streams as its more precise <a href=\"https://github.com/ynput/OpenPype/pull/6171\">#6171</a></summary>\n\nWhen dealing with 30 fps mov of 2 frames, the duration was reduce to 3 decimal places (0.067) which meant that the flag for ffmpeg `-ss` ended up with a time that was not precise enough for ffmpeg to pick a frame; `0.0335`. Should be `0.0333`.Using the duration from the streams is more precise; `0.066667`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Core: Headless publish failing without GL lib <a href=\"https://github.com/ynput/OpenPype/pull/6205\">#6205</a></summary>\n\nTrying to run a headless publish in the farm I hit another blocker:\n```\n2024-02-07 20:42:45:  0: STDOUT: !!! AYON crashed:\n2024-02-07 20:42:45:  0: STDOUT: Traceback (most recent call last):\n2024-02-07 20:42:45:  0: STDOUT:   File \"start.py\", line 740, in main_cli\n2024-02-07 20:42:45:  0: STDOUT:     ))\n2024-02-07 20:42:45:  0: STDOUT:   File \"/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py\", line 1157, in __call__\n2024-02-07 20:42:45:  0: STDOUT:     return self.main(*args, **kwargs)\n2024-02-07 20:42:45:  0: STDOUT:   File \"/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py\", line 1078, in main\n2024-02-07 20:42:45:  0: STDOUT:     rv = self.invoke(ctx)\n2024-02-07 20:42:45:  0: STDOUT:   File \"/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py\", line 1688, in invoke\n2024-02-07 20:42:45:  0: STDOUT:     return _process_result(sub_ctx.command.invoke(sub_ctx))\n2024-02-07 20:42:45:  0: STDOUT:   File \"/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py\", line 1434, in invoke\n2024-02-07 20:42:45:  0: STDOUT:     return ctx.invoke(self.callback, **ctx.params)\n2024-02-07 20:42:45:  0: STDOUT:   File \"/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py\", line 783, in invoke\n2024-02-07 20:42:45:  0: STDOUT:     return __callback(*args, **kwargs)\n2024-02-07 20:42:45:  0: STDOUT:   File \"/pipe/dev/farrizabalaga/OpenPype/openpype/cli.py\", line 197, in publish\n2024-02-07 20:42:45:  0: STDOUT:     PypeCommands.publish(list(paths), targets, gui)\n2024-02-07 20:42:45:  0: STDOUT:   File \"/pipe/dev/farrizabalaga/OpenPype/openpype/pype_commands.py\", line 100, in publish\n2024-02-07 20:42:45:  0: STDOUT:     from openpype.tools.utils.host_tools import show_publish\n2024-02-07 20:42:45:  0: STDOUT:   File \"/pipe/dev/farrizabalaga/OpenPype/openpype/tools/utils/__init__.py\", line 1, in <module>\n2024-02-07 20:42:45:  0: STDOUT:     from .layouts import FlowLayout\n2024-02-07 20:42:45:  0: STDOUT:   File \"/pipe/dev/farrizabalaga/OpenPype/openpype/tools/utils/layouts.py\", line 1, in <module>\n2024-02-07 20:42:45:  0: STDOUT:     from qtpy import QtWidgets, QtCore\n2024-02-07 20:42:45:  0: STDOUT:   File \"/usr/ayon-launcher/1.0.0+ax/dependencies/qtpy/QtWidgets.py\", line 111, in <module>\n2024-02-07 20:42:45:  0: STDOUT:     from PySide2.QtWidgets import *\n2024-02-07 20:42:45:  0: STDOUT:   File \"/usr/ayon-launcher/1.0.0+ax/vendor/python/shiboken2/files.dir/shibokensupport/__feature__.py\", line 142, in _import\n2024-02-07 20:42:45:  0: STDOUT:     return original_import(name, *args, **kwargs)\n2024-02-07 20:42:45:  0: STDOUT: ImportError: libGL.so.1: cannot open shared object file: No such file or directory\n```\nThe imports of `openpype.tools.utils.host_tools.__init__.py` were throwing an error due to trying to import QtWidgets unnecessarily.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: LoadClip colorspace override - OP-6591 <a href=\"https://github.com/ynput/OpenPype/pull/6215\">#6215</a></summary>\n\nSetting the colorspace from the representation data was not supported.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Hiero: Add OP settings and convert in plugin - OP-8338 <a href=\"https://github.com/ynput/OpenPype/pull/6232\">#6232</a></summary>\n\nMissing settings for https://github.com/ynput/OpenPype/pull/6143.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix Render Instance Collector to use folderPath <a href=\"https://github.com/ynput/OpenPype/pull/6233\">#6233</a></summary>\n\nFix Render Instance Collector to use folderPath instead of just the asset name.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix - Fix \"Action Failed\" window not showing <a href=\"https://github.com/ynput/OpenPype/pull/6236\">#6236</a></summary>\n\nThis PR targets to fix issue #6234.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: render use existing frames with slate offsets the published render - AY-1433 <a href=\"https://github.com/ynput/OpenPype/pull/6239\">#6239</a></summary>\n\nDue to `frameStart` data member on representation for existing frames, the frame indexes would be re-numbered when integrating due to this; https://github.com/ynput/OpenPype/blob/develop/openpype/plugins/publish/integrate.py#L712-L726Removing `frameStart` had no effect on publishing workflows, local or farm.Also fixed an issues with slate collection which could misbehave if the instance node had \"slate\" in the name.Resolves #5883\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON Workfiles tool: Copy and open of published workfile works <a href=\"https://github.com/ynput/OpenPype/pull/6241\">#6241</a></summary>\n\nFix copy and open published workfiles.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: OCIO and python2 compatibility fixes <a href=\"https://github.com/ynput/OpenPype/pull/6242\">#6242</a></summary>\n\nNuke 12 is now fully supported with our OCIO wrapping functionalities.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Tests: Fix failing maya automatic test <a href=\"https://github.com/ynput/OpenPype/pull/6235\">#6235</a></summary>\n\nImprovement on https://github.com/ynput/OpenPype/pull/6231\n\n\n___\n\n</details>\n\n\n\n\n## [3.18.7](https://github.com/ynput/OpenPype/tree/3.18.7)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.6...3.18.7)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Chore: Wrapper for click proposal <a href=\"https://github.com/ynput/OpenPype/pull/5928\">#5928</a></summary>\n\nThis is a proposal how to resolve issues with `click` python module. Issue https://github.com/ynput/OpenPype/issues/5921 reported that in Houdini 20+ is our click clashing with click in houdini, where is expected higher version. We can't update our version to support older pythons (NOTE older Python 3).\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: Add repair action to hidden joints validator <a href=\"https://github.com/ynput/OpenPype/pull/6214\">#6214</a></summary>\n\nJoints Hidden is missing repair action, this adds it back\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: output node and EXR <a href=\"https://github.com/ynput/OpenPype/pull/6086\">#6086</a></summary>\n\nOutput node now works correctly for Multilayer EXR and keeps existing links. The output now is handled entirely by the compositor node tree.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON Switch tool: Keep version after switch <a href=\"https://github.com/ynput/OpenPype/pull/6104\">#6104</a></summary>\n\nKeep version if only representation did change. The AYON variant of https://github.com/ynput/OpenPype/pull/4629\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Loader AYON: Reset loader window on open <a href=\"https://github.com/ynput/OpenPype/pull/6170\">#6170</a></summary>\n\nMake sure loader tool is reset on each show.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Show message with error on action failure <a href=\"https://github.com/ynput/OpenPype/pull/6179\">#6179</a></summary>\n\nThis PR adds support for the publisher to show error message from running actions.Errors from actions will otherwise be hidden from user in various console outputs.Also include card for when action is finished.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON Applications: Remove djvview group from default applications <a href=\"https://github.com/ynput/OpenPype/pull/6188\">#6188</a></summary>\n\nThe djv does not have group defined in models so the values are not used anywhere.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: added fallback for broken ffprobe return <a href=\"https://github.com/ynput/OpenPype/pull/6189\">#6189</a></summary>\n\nCustomer provided .exr returned width and height equal to 0 which caused error in `extract_thumbnail`. This tries to use oiiotool to get metadata about file, in our case it read it correctly.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Photoshop: High scaling in UIs <a href=\"https://github.com/ynput/OpenPype/pull/6190\">#6190</a></summary>\n\nUse `get_openpype_qt_app` to create `QApplication` in Photoshop.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Status update settings are not case insensitive. <a href=\"https://github.com/ynput/OpenPype/pull/6195\">#6195</a></summary>\n\nMake values for project_settings/ftrack/events/status_update case insensitive.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Thumbnail product filtering <a href=\"https://github.com/ynput/OpenPype/pull/6197\">#6197</a></summary>\n\nThis PR introduces subset filtering for thumbnail extraction. This is to skip passes like zdepth which is not needed and can cause issues with extraction. Also speeds up publishing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TimersManager: Idle dialog always on top <a href=\"https://github.com/ynput/OpenPype/pull/6201\">#6201</a></summary>\n\nMake stop timer dialog always on tophttps://app.clickup.com/t/6658547/OP-8033\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AfterEffects: added toggle for applying values from DB during creation <a href=\"https://github.com/ynput/OpenPype/pull/6204\">#6204</a></summary>\n\nPreviously values (resolution, duration) from Asset (eg. DB) were applied explicitly when instance of `render` product type was created. This PR adds toggle to Settings to disable this. (This allows artist to publish non standard length of composition, disabling of `Validate Scene Settings` is still required.)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Update plugin commit <a href=\"https://github.com/ynput/OpenPype/pull/6208\">#6208</a></summary>\n\nUpdated unreal plugin to latest main.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Traypublisher: editorial avoid audio tracks processing <a href=\"https://github.com/ynput/OpenPype/pull/6038\">#6038</a></summary>\n\nAvoiding audio tracks from EDL editorial publishing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Resolve Inventory offsets clips when swapping versions <a href=\"https://github.com/ynput/OpenPype/pull/6128\">#6128</a></summary>\n\nSwapped version retain the offset and IDT of the timelime clip.closes: https://github.com/ynput/OpenPype/issues/6125\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher window as dialog <a href=\"https://github.com/ynput/OpenPype/pull/6176\">#6176</a></summary>\n\nChanging back Publisher window to QDialog.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Validate write node fix error report - OP-8088 <a href=\"https://github.com/ynput/OpenPype/pull/6183\">#6183</a></summary>\n\nReport error was not printing the expected values from settings, but instead the values on the write node, leading to confusing messages like:\n```\nTraceback (most recent call last):\n  File \"C:\\Users\\tokejepsen\\AppData\\Local\\Ynput\\AYON\\dependency_packages\\ayon_2310271602_windows.zip\\dependencies\\pyblish\\plugin.py\", line 527, in __explicit_process\n    runner(*args)\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\hosts\\nuke\\plugins\\publish\\validate_write_nodes.py\", line 135, in process\n    self._make_error(check)\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\hosts\\nuke\\plugins\\publish\\validate_write_nodes.py\", line 149, in _make_error\n    raise PublishXmlValidationError(\nopenpype.pipeline.publish.publish_plugins.PublishXmlValidationError: Write node's knobs values are not correct!\nKnob 'channels' > Correct: `rgb` > Wrong: `rgb`\n```\nThis PR changes the error report to:\n```\nTraceback (most recent call last):\n  File \"C:\\Users\\tokejepsen\\AppData\\Local\\Ynput\\AYON\\dependency_packages\\ayon_2310271602_windows.zip\\dependencies\\pyblish\\plugin.py\", line 527, in __explicit_process\n    runner(*args)\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\hosts\\nuke\\plugins\\publish\\validate_write_nodes.py\", line 135, in process\n    self._make_error(check)\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\hosts\\nuke\\plugins\\publish\\validate_write_nodes.py\", line 149, in _make_error\n    raise PublishXmlValidationError(\nopenpype.pipeline.publish.publish_plugins.PublishXmlValidationError: Write node's knobs values are not correct!\nKnob 'channels' > Expected: `['rg']` > Current: `rgb`\n```\n\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Camera product type loaded is not updating - OP-7973 <a href=\"https://github.com/ynput/OpenPype/pull/6184\">#6184</a></summary>\n\nWhen updating the camera this error would appear:\n```\n(...)openpype/hosts/nuke/plugins/load/load_camera_abc.py\", line 142, in update\n    camera_node = nuke.toNode(object_name)\nTypeError: toNode() argument 1 must be str, not Node\n```\n\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON settings: Use bundle name as variant in dev mode <a href=\"https://github.com/ynput/OpenPype/pull/6187\">#6187</a></summary>\n\nMake sure the bundle name is used in dev mode for settings variant.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: fix unwanted change to field name in Settings <a href=\"https://github.com/ynput/OpenPype/pull/6193\">#6193</a></summary>\n\nIt should be `image_format` but in previous refactoring PR it fell back to original `output_formats` which caused enum not to show up and propagate into plugin.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: AYON menu disappeared when the workspace has been changed in 3dsMax <a href=\"https://github.com/ynput/OpenPype/pull/6200\">#6200</a></summary>\n\nAYON plugins are not correctly registered when switching to different workspaces.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TrayPublisher: adding settings category to base creator classes <a href=\"https://github.com/ynput/OpenPype/pull/6202\">#6202</a></summary>\n\nSettings are resolving correctly as they suppose to.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: expose knobs backward compatibility fix - OP-8164 <a href=\"https://github.com/ynput/OpenPype/pull/6211\">#6211</a></summary>\n\nFix backwards compatibility for settings `project_settings/nuke/create/CreateWriteRender/exposed_knobs`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AE: fix local render doesn't push thumbnail to Ftrack <a href=\"https://github.com/ynput/OpenPype/pull/6212\">#6212</a></summary>\n\nWithout thumbnail review is not clickable from main Versions list\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: openpype expose knobs validator - OP-8166 <a href=\"https://github.com/ynput/OpenPype/pull/6213\">#6213</a></summary>\n\nFix exposed knobs validator for backwards compatibility with missing settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Post-launch hook fix value lowering <a href=\"https://github.com/ynput/OpenPype/pull/6221\">#6221</a></summary>\n\nFix lowerin of values in status mapping.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Maya: Remove `shelf` class and shelf build on maya `userSetup.py` <a href=\"https://github.com/ynput/OpenPype/pull/5837\">#5837</a></summary>\n\nRemove shelf builder logic. It appeared to be unused and had bugs.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Max: updated implementation of save_scene + small QOL improvements to host <a href=\"https://github.com/ynput/OpenPype/pull/6186\">#6186</a></summary>\n\n- Removed `has_unsaved_changes` from Max host as it looks to have been unused and unimplemented.\n- Added and implemented `workfile_has_unsaved_changes` to Max host.\n- Mirrored the Houdini host to implement the above into `save_scene` publish for Max.\n- Added a line to `startup.ms` which opens the usual 'default' menu inside of Max (see screenshots).Current (Likely opens this menu due to one or more of the startup scripts used to insert OP menu):New:\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Use better resolution of Ayon apps on 4k display <a href=\"https://github.com/ynput/OpenPype/pull/6199\">#6199</a></summary>\n\nChanges size (makes it smaller) of Ayon apps (Workfiles, Loader) in Fusion on high definitions displays.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Update CONTRIBUTING.md <a href=\"https://github.com/ynput/OpenPype/pull/6210\">#6210</a></summary>\n\nUpdating contributing guidelines to reflect the EOL state of repository\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: Remove redundant instance_skeleton_data code - OP-8269 <a href=\"https://github.com/ynput/OpenPype/pull/6219\">#6219</a></summary>\n\nThis PR https://github.com/ynput/OpenPype/pull/5186 re-introduced code about for the `instance_skeleton_data` but its actually not used since this variable gets overwritten later.\n\n\n___\n\n</details>\n\n\n\n\n## [3.18.6](https://github.com/ynput/OpenPype/tree/3.18.6)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.5...3.18.6)\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>AYON: Use `SettingsField` from ayon server <a href=\"https://github.com/ynput/OpenPype/pull/6173\">#6173</a></summary>\n\nThis is preparation for new version of pydantic which will require to customize the field class for AYON purposes as raw pydantic Field could not be used.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Expose write knobs - OP-7592 <a href=\"https://github.com/ynput/OpenPype/pull/6137\">#6137</a></summary>\n\nThis PR adds `exposed_knobs` to the creator plugins settings at `ayon+settings://nuke/create/CreateWriteRender/exposed_knobs`.When exposed knobs will be linked from the write node to the outside publish group, for users to adjust.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Remove kitsu addon <a href=\"https://github.com/ynput/OpenPype/pull/6172\">#6172</a></summary>\n\nRemoved kitsu addon from server addons because already has own repository.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Fusion: provide better logging for validate saver crash due type error <a href=\"https://github.com/ynput/OpenPype/pull/6082\">#6082</a></summary>\n\nHandles reported issue for `NoneType` error thrown in conversion `int(tool[\"Comments\"][frame])`. It is most likely happening when saver node has no input connections.There is a validator for that, but it might be not obvious, that this error is caused by missing input connections and it has been already reported by `\"Validate Saver Has Input\"`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Workfile Template Builder: Use correct variable in create placeholder <a href=\"https://github.com/ynput/OpenPype/pull/6141\">#6141</a></summary>\n\nUse correct variable where failed instances are stored for validation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>ExtractOIIOTranscode: Missing product_names to subsets conversion <a href=\"https://github.com/ynput/OpenPype/pull/6159\">#6159</a></summary>\n\nThe `Product Names` filtering should be fixed with this.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Fix missing animation data when updating blend assets <a href=\"https://github.com/ynput/OpenPype/pull/6165\">#6165</a></summary>\n\nFix missing animation data when updating blend assets.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TrayPublisher: Pre-fill of version works in AYON <a href=\"https://github.com/ynput/OpenPype/pull/6180\">#6180</a></summary>\n\nUse `folderPath` instead of `asset` in AYON mode to calculate next available version.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Chore: remove Muster <a href=\"https://github.com/ynput/OpenPype/pull/6085\">#6085</a></summary>\n\nMuster isn't maintained for a long time and it wasn't working anyway. This is removing related code from the code base. If there is renewed interest in Muster, it needs to be re-implemented in modern AYON compatible way.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Maya: change label in the render settings to be more readable <a href=\"https://github.com/ynput/OpenPype/pull/6134\">#6134</a></summary>\n\nAYON replacement for #5713.\n\n\n___\n\n</details>\n\n\n\n\n## [3.18.5](https://github.com/ynput/OpenPype/tree/3.18.5)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.4...3.18.5)\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Chore: Add addons dir only if exists <a href=\"https://github.com/ynput/OpenPype/pull/6140\">#6140</a></summary>\n\nDo not add addons directory path for addons discovery if does not exists.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Hiero: Effect Categories - OP-7397 <a href=\"https://github.com/ynput/OpenPype/pull/6143\">#6143</a></summary>\n\nThis PR introduces `Effect Categories` for the Hiero settings. This allows studios to split effect stacks into meaningful subsets.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Render Workfile Attributes <a href=\"https://github.com/ynput/OpenPype/pull/6146\">#6146</a></summary>\n\n`Workfile Dependency` default value can now be controlled from project settings.`Use Published Workfile` makes using published workfiles for rendering optional.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Attributes are locked after publishing if they are locked in Camera Family <a href=\"https://github.com/ynput/OpenPype/pull/6073\">#6073</a></summary>\n\nThis PR is to make sure unlock attributes only during the bake context, make sure attributes are relocked after to preserve the lock state of the original node being baked.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Missing nuke family Windows arguments <a href=\"https://github.com/ynput/OpenPype/pull/6131\">#6131</a></summary>\n\nDefault Windows arguments for launching the Nuke family was missing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Fix the bug on the limit group not being set correctly in Maya Deadline Setting <a href=\"https://github.com/ynput/OpenPype/pull/6139\">#6139</a></summary>\n\nThis PR is to bug-fix the limit groups from maya deadline settings errored out when the user tries to edit the setting.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Transcoding extensions add missing '.tif' extension <a href=\"https://github.com/ynput/OpenPype/pull/6142\">#6142</a></summary>\n\nImage extensions in transcoding helper was missing `.tif` extension and had `.tiff` twice.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Use the new API for override context <a href=\"https://github.com/ynput/OpenPype/pull/6145\">#6145</a></summary>\n\nBlender 4.0 disabled the old API to override context. This API updates the code to use the new API.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>BugFix: Include Model in FBX Loader in Houdini <a href=\"https://github.com/ynput/OpenPype/pull/6150\">#6150</a></summary>\n\nA quick bugfig where we can't load fbx exported from blender. The bug was reported here.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Restore actions to objects after update <a href=\"https://github.com/ynput/OpenPype/pull/6153\">#6153</a></summary>\n\nRestore the actions assigned to objects after updating assets from blend files.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Collect template data with hierarchy context <a href=\"https://github.com/ynput/OpenPype/pull/6154\">#6154</a></summary>\n\nFixed queue loop where is used wrong variable to pop items from queue.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>OP-6382 - Thumbnail Integration Problem <a href=\"https://github.com/ynput/OpenPype/pull/6156\">#6156</a></summary>\n\nThis ticket alerted to 3 different cases of integration issues;\n- [x] Using the Tray Publisher with the same image format (extension) for representation and review representation.\n- [x] Clash on publish file path from output definitions in `ExtractOIIOTranscode`.\n- [x] Clash on publish file from thumbnail in `ExtractThumbnail`There might be an issue with this fix, if a studio does not use the `{output}` token in their `render` anatomy template. But thinking if they have customized it, they will be responsible to maintain these edge cases.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Bugfix saving camera scene errored out when creating render instance with multi-camera option turned off  <a href=\"https://github.com/ynput/OpenPype/pull/6163\">#6163</a></summary>\n\nThis PR is to make sure the integrator of saving camera scene turned off and the render submitted successfully when multi-camera options being turned off in 3dsmax\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Fix duplicated project name on create project structure <a href=\"https://github.com/ynput/OpenPype/pull/6166\">#6166</a></summary>\n\nSmall fix in project folders. It is not used same variable name to change values which breaks values on any next loop.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Maya: Remove duplicate plugin <a href=\"https://github.com/ynput/OpenPype/pull/6157\">#6157</a></summary>\n\nThe two plugins below are doing the same work, so we can remove the one focused solely on lookdev.https://github.com/ynput/OpenPype/blob/develop/openpype/hosts/maya/plugins/publish/validate_look_members_unique.pyhttps://github.com/ynput/OpenPype/blob/develop/openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publish report viewer: Report items sorting <a href=\"https://github.com/ynput/OpenPype/pull/6092\">#6092</a></summary>\n\nProposal of items sorting in Publish report viewer tool. Items are sorted by report creation time. Creation time is also added to publish report data when saved from publisher tool.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Extended error message <a href=\"https://github.com/ynput/OpenPype/pull/6161\">#6161</a></summary>\n\nAdded more details to message\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Added settings for Fusion creators to legacy OP <a href=\"https://github.com/ynput/OpenPype/pull/6162\">#6162</a></summary>\n\nAdded missing OP variant of setting for new Fusion creator.\n\n\n___\n\n</details>\n\n\n\n\n## [3.18.4](https://github.com/ynput/OpenPype/tree/3.18.4)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.3...3.18.4)\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>multiple render camera supports for 3dsmax <a href=\"https://github.com/ynput/OpenPype/pull/5124\">#5124</a></summary>\n\nSupports for rendering with multiple cameras in 3dsmax\n- [x] Add Batch Render Layers functions\n- [x] Rewrite lib.rendersetting and lib.renderproduct\n- [x] Add multi-camera options in creator.\n- [x] Collector with batch render-layer when multi-camera enabled.\n- [x] Add instance plugin for saving scene files with different cameras respectively by using subprocess\n- [x] Refactor submit_max_deadline\n- [x] Check with metadata.json in submit publish job\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: new creator for image product type <a href=\"https://github.com/ynput/OpenPype/pull/6057\">#6057</a></summary>\n\nIn many DCC `render` product type is expected to be sequence of files. This PR adds new explicit creator for `image` product type which is focused on single frame image. Workflows for both product types might be a bit different, this gives artists more granularity to choose better workflow.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Account and ignore free image planes. <a href=\"https://github.com/ynput/OpenPype/pull/5993\">#5993</a></summary>\n\nFree image planes do not have the `->` path separator, so we need to account for that.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Fix long names for instances <a href=\"https://github.com/ynput/OpenPype/pull/6070\">#6070</a></summary>\n\nChanged naming for instances to use only final part of the `folderPath`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Traypublisher & Chore: Instance version on follow workfile version <a href=\"https://github.com/ynput/OpenPype/pull/6117\">#6117</a></summary>\n\nIf `follow_workfile_version` is enabled but context does not have filled workfile version, a version on instance is used instead.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Substance Painter: Thumbnail errors with PBR Texture Set <a href=\"https://github.com/ynput/OpenPype/pull/6127\">#6127</a></summary>\n\nWhen publishing with PBR Metallic Roughness as Output Template, Emissive Map errors out because of the missing channel in the material and the map can't be generated in Substance Painter. This PR is to make sure `imagestance.data[\"publish\"] = False` so that the related \"empty\" texture instance would be skipped to generate the output.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Transcoding: Fix reading image sequences through oiiotool <a href=\"https://github.com/ynput/OpenPype/pull/6129\">#6129</a></summary>\n\nWhen transcoding image sequences, the second image onwards includes the invalid xml line of `Reading path/to/file.exr` of the oiiotool output.This is most likely not the best solution, but it fixes the issue and illustrates the problem.Error:\n```\nERROR:pyblish.plugin:Traceback (most recent call last):\n  File \"C:\\Users\\tokejepsen\\AppData\\Local\\Ynput\\AYON\\dependency_packages\\ayon_2310271602_windows.zip\\dependencies\\pyblish\\plugin.py\", line 527, in __explicit_process\n    runner(*args)\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\plugins\\publish\\extract_color_transcode.py\", line 152, in process\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\lib\\transcoding.py\", line 1136, in convert_colorspace\n    input_info = get_oiio_info_for_input(input_path, logger=logger)\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\lib\\transcoding.py\", line 124, in get_oiio_info_for_input\n    output.append(parse_oiio_xml_output(xml_text, logger=logger))\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\lib\\transcoding.py\", line 276, in parse_oiio_xml_output\n    tree = xml.etree.ElementTree.fromstring(xml_string)\n  File \"xml\\etree\\ElementTree.py\", line 1347, in XML\nxml.etree.ElementTree.ParseError: syntax error: line 1, column 0\nTraceback (most recent call last):\n  File \"C:\\Users\\tokejepsen\\AppData\\Local\\Ynput\\AYON\\dependency_packages\\ayon_2310271602_windows.zip\\dependencies\\pyblish\\plugin.py\", line 527, in __explicit_process\n    runner(*args)\n  File \"<string>\", line 152, in process\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\lib\\transcoding.py\", line 1136, in convert_colorspace\n    input_info = get_oiio_info_for_input(input_path, logger=logger)\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\lib\\transcoding.py\", line 124, in get_oiio_info_for_input\n    output.append(parse_oiio_xml_output(xml_text, logger=logger))\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\lib\\transcoding.py\", line 276, in parse_oiio_xml_output\n    tree = xml.etree.ElementTree.fromstring(xml_string)\n  File \"xml\\etree\\ElementTree.py\", line 1347, in XML\nxml.etree.ElementTree.ParseError: syntax error: line 1, column 0\n```\n\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Remove 'IntegrateHeroVersion' conversion <a href=\"https://github.com/ynput/OpenPype/pull/6130\">#6130</a></summary>\n\nRemove settings conversion for `IntegrateHeroVersion`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore tools: Make sure style object is not garbage collected <a href=\"https://github.com/ynput/OpenPype/pull/6136\">#6136</a></summary>\n\nMinor fix in tool utils to make sure style C++ object is not garbage collected when not stored into variable.\n\n\n___\n\n</details>\n\n\n\n\n## [3.18.3](https://github.com/ynput/OpenPype/tree/3.18.3)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.2...3.18.3)\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: Apply initial viewport shader for Redshift Proxy after loading <a href=\"https://github.com/ynput/OpenPype/pull/6102\">#6102</a></summary>\n\nWhen the published redshift proxy is being loaded, the shader of the proxy is missing. This is different from the manual load through creating redshift proxy for files. This PR is to assign the default lambert to the redshift proxy, which replicates the same approach when the user manually loads the proxy with filepath.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: We should keep current subset version when we switch only the representation type <a href=\"https://github.com/ynput/OpenPype/pull/4629\">#4629</a></summary>\n\nWhen we switch only the representation type of subsets, we should not get the representation from the last version of the subset.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Add loader for redshift proxy family <a href=\"https://github.com/ynput/OpenPype/pull/5948\">#5948</a></summary>\n\nLoader for Redshift Proxy in Houdini (Thanks for @BigRoy contribution)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AfterEffects: exposing Deadline pools fields in Publisher UI <a href=\"https://github.com/ynput/OpenPype/pull/6079\">#6079</a></summary>\n\nDeadline pools might be adhoc set by an artist during publishing. AfterEffects implementation wasn't providing this.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Event callbacks can have order <a href=\"https://github.com/ynput/OpenPype/pull/6080\">#6080</a></summary>\n\nEvent callbacks can have order in which are called, and fixed issue with getting function name and file when using `partial` function as callback.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: OpenPype addon defines runtime dependencies <a href=\"https://github.com/ynput/OpenPype/pull/6095\">#6095</a></summary>\n\nMoved runtime dependencies from ayon-launcher to openpype addon.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: User's setting for scene unit scale  <a href=\"https://github.com/ynput/OpenPype/pull/6097\">#6097</a></summary>\n\nOptions for users to set the default scene unit scale for their scenes.AYONLegacy OP\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Remove deprecated templates profiles <a href=\"https://github.com/ynput/OpenPype/pull/6103\">#6103</a></summary>\n\nRemove deprecated usage of template profiles from settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Window is not always on top <a href=\"https://github.com/ynput/OpenPype/pull/6107\">#6107</a></summary>\n\nGoal of this PR is to avoid using `WindowStaysOnTopHint` which causes issues, especially in cases when DCC shows a popup dialog that is behind the window, in that case both Publisher and DCC are frozen and there is nothing to do.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: add split job export support for Redshift ROP <a href=\"https://github.com/ynput/OpenPype/pull/6108\">#6108</a></summary>\n\nThis is adding support for splitting of export and render jobs for Redshift as is already implemented for Vray, Mantra and Arnold.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: automatic installation of PySide2 <a href=\"https://github.com/ynput/OpenPype/pull/6111\">#6111</a></summary>\n\nThis PR adds hook which tries to check if PySide2 is installed in Python used by Fusion and if not, it tries to install it automatically.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: OpenPype addon dependencies <a href=\"https://github.com/ynput/OpenPype/pull/6113\">#6113</a></summary>\n\nAdded `click` and `six` to requirements of openpype addon, and removed `Qt.py` requirement, which is not used anywhere.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Thumbnail representation has 'outputName' <a href=\"https://github.com/ynput/OpenPype/pull/6114\">#6114</a></summary>\n\nAdd thumbnail output name to thumbnail representation to prevent same output filename during integration.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Kitsu: Clear credentials is safe <a href=\"https://github.com/ynput/OpenPype/pull/6116\">#6116</a></summary>\n\nDo not remove not existing keyring items.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: bug fix the playblast without textures <a href=\"https://github.com/ynput/OpenPype/pull/5942\">#5942</a></summary>\n\nBug fix the texture not being displayed when users enable texture placement in the OP/AYON setting\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Workfile instance update fix <a href=\"https://github.com/ynput/OpenPype/pull/6048\">#6048</a></summary>\n\nMake sure workfile instance has always available 'instance_node' in transient data.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Fix issue with parenting of widgets <a href=\"https://github.com/ynput/OpenPype/pull/6106\">#6106</a></summary>\n\nDon't use publisher window parent (usually main DCC window) as parent for report widget.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>:wrench: fix and update pydocstyle configuration <a href=\"https://github.com/ynput/OpenPype/pull/6109\">#6109</a></summary>\n\nFix pydocstyle configuration and move it to `pyproject.toml`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Create camera node with the latest camera node class in Nuke 14 <a href=\"https://github.com/ynput/OpenPype/pull/6118\">#6118</a></summary>\n\nCreating instance fails for certain cameras, and it seems to only exist in Nuke 14. The reason of causing that contributes to the new camera node class `Camera4`  while the camera creator is working with the `Camera2` class.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Site Sync: small fixes in Loader <a href=\"https://github.com/ynput/OpenPype/pull/6119\">#6119</a></summary>\n\nResolves issue:\n- local and studio icons were same, they should be different\n- `TypeError: string indices must be integers` error when downloading/uploading workfiles\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Template data for editorial publishing <a href=\"https://github.com/ynput/OpenPype/pull/6120\">#6120</a></summary>\n\nTemplate data for editorial publishing are filled during `CollectInstanceAnatomyData`. The structure for editorial is determined, as it's required for ExtractHierarchy AYON/OpenPype plugins.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>SceneInventory: Fix site sync icon conversion <a href=\"https://github.com/ynput/OpenPype/pull/6123\">#6123</a></summary>\n\nUse 'get_qt_icon' to convert icon definitions from site sync.\n\n\n___\n\n</details>\n\n\n\n\n## [3.18.2](https://github.com/ynput/OpenPype/tree/3.18.2)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.1...3.18.2)\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Testing: Release Maya/Deadline job from pending when testing. <a href=\"https://github.com/ynput/OpenPype/pull/5988\">#5988</a></summary>\n\nWhen testing we wont put the Deadline jobs into pending with dependencies, so the worker can start as soon as possible.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Tweaks on Extractions for the exporters <a href=\"https://github.com/ynput/OpenPype/pull/5814\">#5814</a></summary>\n\nWith this PR\n- Suspend Refresh would be introduced in abc & obj extractors for optimization.\n- Allow users to choose the custom attributes to be included in abc exports\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Optional preserve references. <a href=\"https://github.com/ynput/OpenPype/pull/5994\">#5994</a></summary>\n\nOptional preserve references when publishing Maya scenes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON ftrack: Expect 'ayon' group in custom attributes <a href=\"https://github.com/ynput/OpenPype/pull/6066\">#6066</a></summary>\n\nExpect `ayon` group as one of options to get custom attributes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON Chore: Remove dependencies related to separated addons <a href=\"https://github.com/ynput/OpenPype/pull/6074\">#6074</a></summary>\n\nRemoved dependencies from openpype client pyproject.toml that are already defined by addons which require them.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Editorial & chore: Stop using pathlib2 <a href=\"https://github.com/ynput/OpenPype/pull/6075\">#6075</a></summary>\n\nDo not use `pathlib2` which is Python 2 backport for `pathlib` module in python 3.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Traypublisher: Correct validator label <a href=\"https://github.com/ynput/OpenPype/pull/6084\">#6084</a></summary>\n\nUse correct label for Validate filepaths.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Extract Review Intermediate disabled when both Extract Review Mov and Extract Review Intermediate disabled in setting <a href=\"https://github.com/ynput/OpenPype/pull/6089\">#6089</a></summary>\n\nReport in Discord https://discord.com/channels/517362899170230292/563751989075378201/1187874498234556477\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Bug fix the file from texture node not being collected correctly in Yeti Rig <a href=\"https://github.com/ynput/OpenPype/pull/5990\">#5990</a></summary>\n\nFix the bug of collect Yeti Rig not being able to get the file parameter(s) from the texture node(s), resulting to the failure of publishing the textures to the resource directory.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bug: fix AYON settings for Maya workspace <a href=\"https://github.com/ynput/OpenPype/pull/6069\">#6069</a></summary>\n\nThis is changing bug in default AYON setting for Maya workspace, where missing semicolumn caused workspace not being set. This is also syncing default workspace settings to OpenPype\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Refactor colorspace handling in CollectColorspace plugin <a href=\"https://github.com/ynput/OpenPype/pull/6033\">#6033</a></summary>\n\nTraypublisher is now capable set available colorspaces or roles to publishing images sequence or video. This is fix of new implementation where we allowed to use roles in the enumerator selector.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: Houdini render split bugs <a href=\"https://github.com/ynput/OpenPype/pull/6037\">#6037</a></summary>\n\nThis PR is a follow up PR to https://github.com/ynput/OpenPype/pull/5420This PR does:\n- refactor `get_output_parameter` to what is used to be.\n- fix a bug with split render\n- rename `exportJob` flag to `split_render`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: fix for single frame rendering <a href=\"https://github.com/ynput/OpenPype/pull/6056\">#6056</a></summary>\n\nFixes publishes of single frame of `render` product type.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Photoshop: fix layer publish thumbnail missing in loader <a href=\"https://github.com/ynput/OpenPype/pull/6061\">#6061</a></summary>\n\nThumbnails from any products (either `review` nor separate layer instances) weren't stored in Ayon.This resulted in not showing them in Loader and Server UI. After this PR thumbnails should be shown in the Loader and on the Server (`http://YOUR_AYON_HOSTNAME:5000/projects/YOUR_PROJECT/browser`).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON Chore: Do not use thumbnailSource for thumbnail integration <a href=\"https://github.com/ynput/OpenPype/pull/6063\">#6063</a></summary>\n\nDo not use `thumbnailSource` for thumbnail integration.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Photoshop: fix creation of .mov <a href=\"https://github.com/ynput/OpenPype/pull/6064\">#6064</a></summary>\n\nGeneration of .mov file with 1 frame per published layer was failing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Photoshop: fix Collect Color Coded settings <a href=\"https://github.com/ynput/OpenPype/pull/6065\">#6065</a></summary>\n\nFix for wrong default value for `Collect Color Coded Instances` Settings\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bug: Fix Publisher parent window in Nuke <a href=\"https://github.com/ynput/OpenPype/pull/6067\">#6067</a></summary>\n\nFixing issue where publisher parent window wasn't set because wrong use of version constant.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Python console widget: Save registry fix <a href=\"https://github.com/ynput/OpenPype/pull/6076\">#6076</a></summary>\n\nDo not save registry until there is something to save.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: update asset names for multiple reviewable items <a href=\"https://github.com/ynput/OpenPype/pull/6077\">#6077</a></summary>\n\nMultiple reviewable assetVersion components with better grouping to asset version name.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: DJV action fixes <a href=\"https://github.com/ynput/OpenPype/pull/6098\">#6098</a></summary>\n\nFix bugs in DJV  ftrack action.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON Workfiles tool: Fix arrow to timezone typo <a href=\"https://github.com/ynput/OpenPype/pull/6099\">#6099</a></summary>\n\nFix parenthesis typo with arrow local timezone function.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Chore: Update folder-favorite icon to ayon icon <a href=\"https://github.com/ynput/OpenPype/pull/5718\">#5718</a></summary>\n\nUpdates old \"Pype-2.0-era\" (from ancient greece times) to AYON logo equivalent.I believe it's only used in Nuke.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Chore: Maya / Nuke remove publish gui filters from settings <a href=\"https://github.com/ynput/OpenPype/pull/5570\">#5570</a></summary>\n\n- Remove Publish GUI Filters from Nuke settings\n- Remove Publish GUI Filters from Maya settings\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Project/User option for output format (create_saver) <a href=\"https://github.com/ynput/OpenPype/pull/6045\">#6045</a></summary>\n\nAdds \"Output Image Format\" option which can be set via project settings and overwritten by users in \"Create\" menu. This replaces the current behaviour of being hardcoded to \"exr\". Replacing the need for people to manually edit the saver path if they require a different extension.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Output Image Format Updating Instances (create_saver) <a href=\"https://github.com/ynput/OpenPype/pull/6060\">#6060</a></summary>\n\nAdds the ability to update Saver image output format if changed in the Publish UI.~~Adds an optional validator that compares \"Output Image Format\" in the Publish menu against the one currently found on the saver. It then offers a repair action to update the output extension on the saver.~~\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Tests: Fix representation count for AE legacy test <a href=\"https://github.com/ynput/OpenPype/pull/6072\">#6072</a></summary>\n\n\n___\n\n</details>\n\n\n\n\n## [3.18.1](https://github.com/ynput/OpenPype/tree/3.18.1)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.0...3.18.1)\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>AYON: Update ayon api to 1.0.0-rc.3 <a href=\"https://github.com/ynput/OpenPype/pull/6052\">#6052</a></summary>\n\nUpdated ayon python api to 1.0.0-rc.3.\n\n\n___\n\n</details>\n\n\n\n\n## [3.18.0](https://github.com/ynput/OpenPype/tree/3.18.0)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/...3.18.0)\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Chore: Fix subst paths handling <a href=\"https://github.com/ynput/OpenPype/pull/5702\">#5702</a></summary>\n\nMake sure that source disk ends with `\\` instead of destination disk.\n\n\n___\n\n</details>\n\n\n\n\n## [3.17.7](https://github.com/ynput/OpenPype/tree/3.17.7)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.6...3.17.7)\n\n### **🆕 New features**\n\n\n<details>\n<summary>AYON: Use folder path as unique identifier <a href=\"https://github.com/ynput/OpenPype/pull/5817\">#5817</a></summary>\n\nUse folder path instead of asset name as unique identifier, with OpenPype compatibility.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Farm caching submission to Deadline  <a href=\"https://github.com/ynput/OpenPype/pull/4903\">#4903</a></summary>\n\nImplements functionality to offload instances of the specific families to be processed on Deadline instead of locally. This increases productivity as artist can use local machine could be used for other tasks.Implemented for families:\n- [x] ass\n- [x] redshift proxy\n- [x] ifd\n- [x] abc\n- [x] bgeo\n- [x] vdb\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Add support to split Deadline render tasks in export + render <a href=\"https://github.com/ynput/OpenPype/pull/5420\">#5420</a></summary>\n\nThis adds initial support in Houdini so when submitting render jobs to Deadline it's not running as a single Houdini task but rather it gets split in two different tasks: Export + Render. This way it's more efficient as we only need a Houdini license during the export step and the render tasks can run exclusively with a render license. Moreover, we aren't wasting all the overhead time of opening the render scene in Houdini for every frame.I have also added the corresponding settings json files so we can set some of the default values for the Houdini deadline submitter.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Wrap: new integration <a href=\"https://github.com/ynput/OpenPype/pull/5823\">#5823</a></summary>\n\nThese modifications are necessary for adding Wrap integration (DCC handling scans and textures) .\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Prepare for 'data' via graphql <a href=\"https://github.com/ynput/OpenPype/pull/5923\">#5923</a></summary>\n\nAYON server does support to query 'data' field for hierarchy entities (project > ... > representation) using GraphQl since version 0.5.5. Because of this PR in ayon-python-api it is required to modify custom graphql function in `openpype.client` to support that option.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore AYON: AYON addon class <a href=\"https://github.com/ynput/OpenPype/pull/5937\">#5937</a></summary>\n\nIntroduced base class for AYON addon in openpype modules discovery logic.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Asset Usage Reporter Tool <a href=\"https://github.com/ynput/OpenPype/pull/5946\">#5946</a></summary>\n\nThis adds simple tool for OpenPype mode that will go over all published workfiles and print linked assets and their version:This is created per project and can be exported in csv file or copied to clipboard in _\"ASCII Human readable form\"_.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Testing: dump_databases flag <a href=\"https://github.com/ynput/OpenPype/pull/5955\">#5955</a></summary>\n\nThis introduces a `dump_databases` flag which makes it convenient to output the resulting database of a successful test run. The flag supports two formats; `bson` and `json`.Due to outputting to the test data folder, when dumping the databases, the test data folder will persist.Split from https://github.com/ynput/OpenPype/pull/5644\n\n\n___\n\n</details>\n\n\n<details>\n<summary>SiteSync: implemented in Ayon Loader <a href=\"https://github.com/ynput/OpenPype/pull/5962\">#5962</a></summary>\n\nImplemented `Availability` column in Ayon loader and redo of loaders to `ActionItems` in representation window there.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Workfile template build works <a href=\"https://github.com/ynput/OpenPype/pull/5975\">#5975</a></summary>\n\nModified workfile template builder to work, to some degree, in AYON mode.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: Small Tweaks on Validator for Look Default Shader Connection for Maya 2024 <a href=\"https://github.com/ynput/OpenPype/pull/5957\">#5957</a></summary>\n\nResolve https://github.com/ynput/OpenPype/issues/5269\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Settings: Changes in default settings <a href=\"https://github.com/ynput/OpenPype/pull/5983\">#5983</a></summary>\n\nWe've made some changes in the default settings as several application versions were obsolete (Maya 18, Nuke 11, PS 2020, etc). Also added tools and changed settings for Blender, Maya, and Blender. \n\nAll should work as usual.\n___\n\n</details>\n\n\n<details>\n<summary>Testing: Do not persist data by default in Maya/Deadline. <a href=\"https://github.com/ynput/OpenPype/pull/5987\">#5987</a></summary>\n\nThis is similar to the Maya publishing test.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Validate loaded plugins tweaks <a href=\"https://github.com/ynput/OpenPype/pull/5820\">#5820</a></summary>\n\nIn the current development of 3dsMax, users need to use separate validators to validate if certain plugins being loaded before the extraction. For example, usd extractor in model family, prt/tycache extractor in pointcloud/tycache family.But with the PR where implements optional validate loaded plugin, users just need to put what kind of plugins they want to validate in the settings. They no longer need to go through all the separate plugin validators when publishing, and only one validator would do all the check on the loaded plugins before extraction.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Change context label enhancement <a href=\"https://github.com/ynput/OpenPype/pull/5887\">#5887</a></summary>\n\nUse QAction to change label of context label in Nuke pipeline menu.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Do not use template data as source for context <a href=\"https://github.com/ynput/OpenPype/pull/5918\">#5918</a></summary>\n\nUse available information on context to receive context data instead of using `\"anatomyData\"` during publishing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Add python3.10 libs for Houdini 20 startup <a href=\"https://github.com/ynput/OpenPype/pull/5932\">#5932</a></summary>\n\nAdd python3.10 libs for Houdini 20 startup\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Use colorspace data when creating thumbnail <a href=\"https://github.com/ynput/OpenPype/pull/5938\">#5938</a></summary>\n\nThumbnails with applied colormanagement.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: rewriting component creation to support multiple thumbnails <a href=\"https://github.com/ynput/OpenPype/pull/5939\">#5939</a></summary>\n\nThe creation of Ftrack components needs to allow for multiple thumbnails. This is important in situations where there could be several reviewable streams, like in the case of a nuke intermediate files preset. Customers have asked for unique thumbnails for each data stream.For instance, one stream might contain a baked LUT file along with Display and View. Another stream might only include the baked Display and View. These variations can change the overall look. Thus, we found it necessary to depict these differences via thumbnails.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: PySide6 tree view style <a href=\"https://github.com/ynput/OpenPype/pull/5940\">#5940</a></summary>\n\nDefine solid color for background of branch in QTreeView.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Explicit Thumbnail workflow <a href=\"https://github.com/ynput/OpenPype/pull/5941\">#5941</a></summary>\n\nNuke made a shift from using its own plugin to a global one for thumbnail creation. This was because it had to handle several thumbnail workflows for baking intermediate data streams. To manage this, the global plugin had to be upgraded. Now, each baking stream can set a unique tag 'need_thumbnail'. This tag is used to mark representations that need a thumbnail.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: extract thumbnail with new settings <a href=\"https://github.com/ynput/OpenPype/pull/5944\">#5944</a></summary>\n\nSettings are now configurable for the following:\n- target size of thumbnail - source or constrained to specific\n- where should be frame taken from in sequence or video file\n- if thumbnail should be integrated or not\n- background color for letter boxes\n- added AYON settings\n\n\n___\n\n</details>\n\n\n<details>\n<summary>RoyalRender: inject submitter environment to the royal render job <a href=\"https://github.com/ynput/OpenPype/pull/5958\">#5958</a></summary>\n\nThis is an attempt to solve runtime environment injection for render jobs in RoyalRender as there is no easy way to implement something like `GlobalJobPreload` logic in Deadline. Idea is to inject OpenPype environments directly to the job itself.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Use manual thumbnail if present when publishing <a href=\"https://github.com/ynput/OpenPype/pull/5969\">#5969</a></summary>\n\nUse manual thumbnail added to the publisher instead of using it from published representation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Change of server url should work as expected <a href=\"https://github.com/ynput/OpenPype/pull/5971\">#5971</a></summary>\n\nUsing login action in tray menu to change server url should correctly start new process without issues of missing bundle or previous url.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: make sure the AYON menu bar in 3dsMax is named AYON when AYON launches <a href=\"https://github.com/ynput/OpenPype/pull/5972\">#5972</a></summary>\n\nRenaming the menu bar in 3dsMax for AYON and some cosmetic fix in the docstring\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Resolve: renaming menu to AYON <a href=\"https://github.com/ynput/OpenPype/pull/5974\">#5974</a></summary>\n\nResolve in Ayon is now having aligned name.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Hiero: custom tools menu rename <a href=\"https://github.com/ynput/OpenPype/pull/5976\">#5976</a></summary>\n\n- OpenPype Tools are now Custom Tools menu\n- fixing order of tools. Create should be first.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>nuke: updating name for custom tools menu item <a href=\"https://github.com/ynput/OpenPype/pull/5977\">#5977</a></summary>\n\n- Ayon variant of settings renamed `Custom Tools` menu item\n\n\n___\n\n</details>\n\n\n<details>\n<summary>fusion: AYON renaming menu <a href=\"https://github.com/ynput/OpenPype/pull/5978\">#5978</a></summary>\n\nFusion is having Ayon menu.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Changed the labels for Layout JSON Extractor <a href=\"https://github.com/ynput/OpenPype/pull/5981\">#5981</a></summary>\n\nChanged the labels for Blender's Layout JSON Extractor.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Testing: Skip Arnold license for test rendering. <a href=\"https://github.com/ynput/OpenPype/pull/5984\">#5984</a></summary>\n\nSkip license check when rendering for testing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Testing: Validate errors and failed status from Deadline jobs. <a href=\"https://github.com/ynput/OpenPype/pull/5986\">#5986</a></summary>\n\nWhile waiting for the Deadline jobs to finish, we query the errors on the job and its dependent jobs to fail as early as possible. Plus the failed status.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: rename Openpype Tools as Custom Tools in Maya Host <a href=\"https://github.com/ynput/OpenPype/pull/5991\">#5991</a></summary>\n\nRename Openpype Tools as Custom Tools in Maya Host in\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Use AYON label in ayon mode <a href=\"https://github.com/ynput/OpenPype/pull/5995\">#5995</a></summary>\n\nReplaced OpenPype with AYON in AYON mode and added bundle nam to information.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Update ayon python api <a href=\"https://github.com/ynput/OpenPype/pull/6002\">#6002</a></summary>\n\nUpdated ayon-python-api to '1.0.0-rc.1'.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Add missing repair action in validate resolution setting <a href=\"https://github.com/ynput/OpenPype/pull/6014\">#6014</a></summary>\n\nAdd missing repair action for validate resolution setting\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Add the AYON/OP settings to enable extractor for model family in 3dsmax <a href=\"https://github.com/ynput/OpenPype/pull/6027\">#6027</a></summary>\n\nAdd the AYON/OP settings to enable extractor for model family in 3dsmax\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: Fix error message formatting if ayon executable can't be found by deadline <a href=\"https://github.com/ynput/OpenPype/pull/6028\">#6028</a></summary>\n\nWithout this fix the error message would report executables string with `;` between EACH character, similar to this PR: https://github.com/ynput/OpenPype/pull/5815However that PR apparently missed also fixing it in `GlobalJobPreLoad` and only fixed it in `Ayon.py` plugin.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Show slightly different info in AYON mode <a href=\"https://github.com/ynput/OpenPype/pull/6031\">#6031</a></summary>\n\nThis PR changes what is shown in Tray menu in AYON mode. Previously, it showed version of OpenPype that is very confusing in AYON mode. So this now shows AYON version instead. When clicked, it will opene AYON info window, where OpenPype version is now added, for debugging purposes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON Editorial: Hierarchy context have names as keys <a href=\"https://github.com/ynput/OpenPype/pull/6041\">#6041</a></summary>\n\nUse folder name as keys in `hierarchyContext` and modify hierachy extraction accordingly.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Convert the createAt value to local timezone <a href=\"https://github.com/ynput/OpenPype/pull/6043\">#6043</a></summary>\n\nShow correct create time in UIs.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Render creation - fix broken imports <a href=\"https://github.com/ynput/OpenPype/pull/5893\">#5893</a></summary>\n\nMaya specific imports were moved to specific methods but not in all cases by #5775. This is just quickly restoring functionality without questioning that decision.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: fix crashing model renderset collector <a href=\"https://github.com/ynput/OpenPype/pull/5929\">#5929</a></summary>\n\nThis fix is handling case where model is in some type of render sets but no other connections are made there. Publishing this model would fail with `RuntimeError: Found no items to list the history for.`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Remove duplicated attributes of MTOA verbosity level  <a href=\"https://github.com/ynput/OpenPype/pull/5945\">#5945</a></summary>\n\nRemove duplicated attributes implementation mentioned in https://github.com/ynput/OpenPype/pull/5931#discussion_r1402175289\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Bug fix Redshift Proxy not being successfully published <a href=\"https://github.com/ynput/OpenPype/pull/5956\">#5956</a></summary>\n\nBug fix redshift proxy family not being successfully published due to the error found in integrate.py\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Bug fix load image for texturesetMain <a href=\"https://github.com/ynput/OpenPype/pull/6011\">#6011</a></summary>\n\nBug fix load image with file node for texturesetMain\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: bug fix the repair function in validate_rendersettings <a href=\"https://github.com/ynput/OpenPype/pull/6021\">#6021</a></summary>\n\nThe following error has been encountered below:\n```\n// pyblish.pyblish.plugin.Action : Finding failed instances..\n// pyblish.pyblish.plugin.Action : Attempting repair for instance: renderLookdevMain ...\n// Error: pyblish.plugin : Traceback (most recent call last):\n//   File \"C:\\Users\\lbate\\AppData\\Local\\Ynput\\AYON\\dependency_packages\\ayon_2310271602_windows.zip\\dependencies\\pyblish\\plugin.py\", line 527, in __explicit_process\n//     runner(*args)\n//   File \"C:\\Users\\lbate\\AppData\\Local\\Ynput\\AYON\\addons\\openpype_3.17.7-nightly.6\\openpype\\pipeline\\publish\\publish_plugins.py\", line 241, in process\n//     plugin.repair(instance)\n//   File \"C:\\Users\\lbate\\AppData\\Local\\Ynput\\AYON\\addons\\openpype_3.17.7-nightly.6\\openpype\\hosts\\maya\\plugins\\publish\\validate_rendersettings.py\", line 395, in repair\n//     cmds.setAttr(\"{}.{}\".format(node, prefix_attr),\n// UnboundLocalError: local variable 'node' referenced before assignment\n// Traceback (most recent call last):\n//   File \"C:\\Users\\lbate\\AppData\\Local\\Ynput\\AYON\\dependency_packages\\ayon_2310271602_windows.zip\\dependencies\\pyblish\\plugin.py\", line 527, in __explicit_process\n//     runner(*args)\n//   File \"C:\\Users\\lbate\\AppData\\Local\\Ynput\\AYON\\addons\\openpype_3.17.7-nightly.6\\openpype\\pipeline\\publish\\publish_plugins.py\", line 241, in process\n//     plugin.repair(instance)\n//   File \"C:\\Users\\lbate\\AppData\\Local\\Ynput\\AYON\\addons\\openpype_3.17.7-nightly.6\\openpype\\hosts\\maya\\plugins\\publish\\validate_rendersettings.py\", line 395, in repair\n//     cmds.setAttr(\"{}.{}\".format(node, prefix_attr),\n// UnboundLocalError: local variable 'node' referenced before assignment\n```\nThis PR is a fix for that\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Render avoid unhashable type `BlackmagicFusion.PyRemoteObject` error <a href=\"https://github.com/ynput/OpenPype/pull/5672\">#5672</a></summary>\n\nFix Fusion 18.6+ support: Avoid issues with Fusion's `BlackmagicFusion.PyRemoteObject` instances being unhashable.\n```python\nTraceback (most recent call last):\n  File \"E:\\openpype\\OpenPype\\.venv\\lib\\site-packages\\pyblish\\plugin.py\", line 527, in __explicit_process\n    runner(*args)\n  File \"E:\\openpype\\OpenPype\\openpype\\hosts\\fusion\\plugins\\publish\\extract_render_local.py\", line 61, in process\n    result = self.render(instance)\n  File \"E:\\openpype\\OpenPype\\openpype\\hosts\\fusion\\plugins\\publish\\extract_render_local.py\", line 118, in render\n    with enabled_savers(current_comp, savers_to_render):\n  File \"C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python39\\lib\\contextlib.py\", line 119, in __enter__\n    return next(self.gen)\n  File \"E:\\openpype\\OpenPype\\openpype\\hosts\\fusion\\plugins\\publish\\extract_render_local.py\", line 33, in enabled_savers\n    original_states[saver] = original_state\nTypeError: unhashable type: 'BlackmagicFusion.PyRemoteObject'\n```\n\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Validate Nuke Write Nodes refactor to use variable `node_value` instead of `value` <a href=\"https://github.com/ynput/OpenPype/pull/5764\">#5764</a></summary>\n\nNuke: Validate Nuke Write Nodes refactor to use variable `node_value` instead of `value`The variable `value` only exists as the last variable value in the `for value in values` loop and might not be declared if `values` is an empty iterable.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>resolve: fixing loader handles calculation <a href=\"https://github.com/ynput/OpenPype/pull/5863\">#5863</a></summary>\n\nResolve was not correctly calculating duration of database related duration.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Staging mode determination <a href=\"https://github.com/ynput/OpenPype/pull/5895\">#5895</a></summary>\n\nResources use `is_staging_enabled` function instead of `is_running_staging` to determine if should use staging icon. And fixed comparison bug in `is_running_staging`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Handle staging templates category <a href=\"https://github.com/ynput/OpenPype/pull/5905\">#5905</a></summary>\n\nStaging anatomy templates category is handled during project templates conversion. The keys are stored into `others` with `\"staging_\"` prefix.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: fix the subset name not changing accordingly after the variant name changes <a href=\"https://github.com/ynput/OpenPype/pull/5911\">#5911</a></summary>\n\nResolve #5902\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Loader tool bugs hunt <a href=\"https://github.com/ynput/OpenPype/pull/5915\">#5915</a></summary>\n\nFix issues with invalid representation ids in loaded containers and handle missing product type in server database.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Bugfixes and enhancements <a href=\"https://github.com/ynput/OpenPype/pull/5924\">#5924</a></summary>\n\nSmall fixes/enhancements in publisher UI.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Supports for additional Job Info and Plugin Info in deadline submission <a href=\"https://github.com/ynput/OpenPype/pull/5931\">#5931</a></summary>\n\nThis PR is to resolve some of the attributes such as MTOA's `ArnoldVerbose` are not preserved on farm and users can use the project settings to add the attributes back to either job or plugin Info.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: Houdini license validator missing families <a href=\"https://github.com/ynput/OpenPype/pull/5934\">#5934</a></summary>\n\nAdding missing families to Houdini license validator.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TrayPublisher: adding back `asset_doc` variable <a href=\"https://github.com/ynput/OpenPype/pull/5943\">#5943</a></summary>\n\nReturning variable which had been removed accidentally in previous PR.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Settings: Fix ModulesManager init args <a href=\"https://github.com/ynput/OpenPype/pull/5947\">#5947</a></summary>\n\nRemove usage of kwargs to create ModulesManager.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Fix Deadline Frames per task <a href=\"https://github.com/ynput/OpenPype/pull/5949\">#5949</a></summary>\n\nFixed a problem with Frames per task setting not being applied when publishing a render.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Testing: Fix is_test_failed <a href=\"https://github.com/ynput/OpenPype/pull/5951\">#5951</a></summary>\n\n`is_test_failed` is used (exclusively) on module fixtures to determine whether the tests have failed or not. This determines whether to run tear down code like cleaning up the database and temporary files.But in the module scope `request.node.rep_call` is not available, which results in `is_test_failed` always returning `True`, and no tear down code get executed.The solution was taken from; https://github.com/pytest-dev/pytest/issues/5090\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Harmony: Fix local rendering <a href=\"https://github.com/ynput/OpenPype/pull/5953\">#5953</a></summary>\n\nLocal rendering was throwing warning about license, but didn't fail per se. It just didn't produce anything.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Testing: hou module should be within class code. <a href=\"https://github.com/ynput/OpenPype/pull/5954\">#5954</a></summary>\n\n`hou` module should be within the class code else we'll get pyblish errors from needing to skip the plugin.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Add Label to MayaUSDReferenceLoader <a href=\"https://github.com/ynput/OpenPype/pull/5964\">#5964</a></summary>\n\nAs the create placeholder dialog displays the two distinct loaders with the same name, this PR is to distinguish Maya USD Reference Loaders from the loaders of which inherited from. See the screenshot below:\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Bug fix the resolution not being shown correctly in review burnin  <a href=\"https://github.com/ynput/OpenPype/pull/5965\">#5965</a></summary>\n\nThe resolution is not being shown correctly in review burnin\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Fix thumbnail integration <a href=\"https://github.com/ynput/OpenPype/pull/5970\">#5970</a></summary>\n\nThumbnail integration could cause crash of server if thumbnail id was changed for the same entity id multiple times. Modified the code to avoid that issue.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Photoshop: Updated label in Settings <a href=\"https://github.com/ynput/OpenPype/pull/5980\">#5980</a></summary>\n\nReplaced wrong label from different plugin.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Photoshop: Fix removed unsupported Path <a href=\"https://github.com/ynput/OpenPype/pull/5996\">#5996</a></summary>\n\nPath is not json serializable by default, it is not necessary, better model reused.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Prepare functions for newer ayon-python-api <a href=\"https://github.com/ynput/OpenPype/pull/5997\">#5997</a></summary>\n\nNewer ayon python api will add new filtering options or change order of existing. Kwargs are used in client code to prevent issues on update.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Conversion of the new playblast settings in Maya <a href=\"https://github.com/ynput/OpenPype/pull/6000\">#6000</a></summary>\n\nConversion of the new playblast settings in Maya\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Bug fix for loading Mesh in Substance Painter as new project not working <a href=\"https://github.com/ynput/OpenPype/pull/6004\">#6004</a></summary>\n\nSubstance Painter in AYON can't load mesh for creating a new project\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: correct webservice couldn't be selected in Ayon <a href=\"https://github.com/ynput/OpenPype/pull/6007\">#6007</a></summary>\n\nChanged the Setting model to mimic more OP approach as it needs to live together for time being.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON tools: Fix refresh thread <a href=\"https://github.com/ynput/OpenPype/pull/6008\">#6008</a></summary>\n\nTrigger 'refresh_finished' signal out of 'run' method.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: multiple reviewable components missing variable <a href=\"https://github.com/ynput/OpenPype/pull/6013\">#6013</a></summary>\n\nMissing variable in code for editorial publishing in traypublisher.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TVPaint: Expect legacy instances in metadata <a href=\"https://github.com/ynput/OpenPype/pull/6015\">#6015</a></summary>\n\nDo not expect `\"workfileInstances\"` constains only new type instance data with `creator_identifier`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: handle missing key in Deadline <a href=\"https://github.com/ynput/OpenPype/pull/6019\">#6019</a></summary>\n\nThis quickly fixes bug introduced by #5420\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Revert `extractenvironments` behaviour <a href=\"https://github.com/ynput/OpenPype/pull/6020\">#6020</a></summary>\n\nThis is returning original behaviour of `extractenvironments` command from before #5958 so we restore functionality.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>OP-7535 - Fix renaming composition in AE <a href=\"https://github.com/ynput/OpenPype/pull/6025\">#6025</a></summary>\n\nRemoving of `render` instance caused renaming of composition to `dummyComp` which caused issue in publishing in next attempt.This PR stores original composition name(cleaned up for product name creation) and uses it if instance needs to be removed.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Refactor code to skip instance creation for new assets <a href=\"https://github.com/ynput/OpenPype/pull/6029\">#6029</a></summary>\n\nPublishing effects from hiero during editorial publish is working as expected again.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Refactor code to handle missing \"representations\" key in instance data <a href=\"https://github.com/ynput/OpenPype/pull/6032\">#6032</a></summary>\n\nMinor code change for optimisation of thumbnail workflow.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Traypublisher: editorial preserve clip case sensitivity <a href=\"https://github.com/ynput/OpenPype/pull/6036\">#6036</a></summary>\n\nKeep EDL clip name inheritance with case sensitivity.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix/add missing houdini settings <a href=\"https://github.com/ynput/OpenPype/pull/6039\">#6039</a></summary>\n\nadd missing settings. now, it looks like this:| Ayon | OpenPype || -- | -- | |  |  ||  |  |\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Maya: Remove RenderSetup layer observers <a href=\"https://github.com/ynput/OpenPype/pull/5836\">#5836</a></summary>\n\nRemove RenderSetup layer observers that are not needed since new publisher since Renderlayer Creators manage these themselves on Collect and Save/Update of instances.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Tests: Removed render instance <a href=\"https://github.com/ynput/OpenPype/pull/6026\">#6026</a></summary>\n\nThis test was created as simple model and workfile publish, without Deadline rendering. Cleaned up render elements.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Tests: update after thumbnail default change <a href=\"https://github.com/ynput/OpenPype/pull/6040\">#6040</a></summary>\n\nhttps://github.com/ynput/OpenPype/pull/5944 changed default state of integration of Thumbnails to NOT integrate. This PR updates automatic tests to follow that.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Remove legacy LOPs USD output processors <a href=\"https://github.com/ynput/OpenPype/pull/5861\">#5861</a></summary>\n\nRemove unused/broken legacy code for Houdini Solaris USD LOPs output processors. The code was originally written in Avalon, against early Houdini 18 betas which had a different API for output processors and thus the current state doesn't even work in recent versions of Houdini.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Substance Painter Addons for Ayon <a href=\"https://github.com/ynput/OpenPype/pull/5914\">#5914</a></summary>\n\nSubstance Painter Addons for Ayon \n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ayon: Updated name of Adobe extension to Ayon <a href=\"https://github.com/ynput/OpenPype/pull/5992\">#5992</a></summary>\n\nThis changes name in menu in Adobe extensions to Ayon.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore/houdini update startup log <a href=\"https://github.com/ynput/OpenPype/pull/6003\">#6003</a></summary>\n\nprint `Installing AYON ...` on startup when launching houdini from launcher in ayon mode.also update submenu to `ayon_menu` instead of `openpype_menu`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Revert \"Ayon: Updated name of Adobe extension to Ayon\" <a href=\"https://github.com/ynput/OpenPype/pull/6010\">#6010</a></summary>\n\nReverts ynput/OpenPype#5992\n\nThat PR is only applicable to Ayon.\n___\n\n</details>\n\n\n<details>\n<summary>Standalone/Tray Publisher: Remove simple Unreal texture publishing <a href=\"https://github.com/ynput/OpenPype/pull/6012\">#6012</a></summary>\n\nWe are removing _simple Unreal Texture publishing_ that was just renaming texture files to fit to Unreal naming conventions but without any additional functionality. We might return this functionality back with better texture publishing system.Related to #5983\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: Bump version because of Settings changes for Deadline <a href=\"https://github.com/ynput/OpenPype/pull/6023\">#6023</a></summary>\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Change ASCII art in the Console based on the server mode <a href=\"https://github.com/ynput/OpenPype/pull/6030\">#6030</a></summary>\n\nThis changes ASCII art in the console based on the AYON/OpenPype mode\n\n\n___\n\n</details>\n\n\n\n\n## [3.17.6](https://github.com/ynput/OpenPype/tree/3.17.6)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.5...3.17.6)\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Testing: Validate Maya Logs <a href=\"https://github.com/ynput/OpenPype/pull/5775\">#5775</a></summary>\n\nThis PR adds testing of the logs within Maya such as Python and Pyblish errors.The reason why we need to touch so many files outside of Maya is because of the pyblish errors below;\n```\npyblish (ERROR) (line: 1371) pyblish.plugin:\nSkipped: \"collect_otio_frame_ranges\" (No module named 'opentimelineio')\n# Error: pyblish.plugin : Skipped: \"collect_otio_frame_ranges\" (No module named 'opentimelineio') #\npyblish (ERROR) (line: 1371) pyblish.plugin:\nSkipped: \"collect_otio_review\" (No module named 'opentimelineio')\n# Error: pyblish.plugin : Skipped: \"collect_otio_review\" (No module named 'opentimelineio') #\npyblish (ERROR) (line: 1371) pyblish.plugin:\nSkipped: \"collect_otio_subset_resources\" (No module named 'opentimelineio')\n# Error: pyblish.plugin : Skipped: \"collect_otio_subset_resources\" (No module named 'opentimelineio') #\npyblish (ERROR) (line: 1371) pyblish.plugin:\nSkipped: \"extract_otio_audio_tracks\" (No module named 'opentimelineio')\n# Error: pyblish.plugin : Skipped: \"extract_otio_audio_tracks\" (No module named 'opentimelineio') #\npyblish (ERROR) (line: 1371) pyblish.plugin:\nSkipped: \"extract_otio_file\" (No module named 'opentimelineio')\n# Error: pyblish.plugin : Skipped: \"extract_otio_file\" (No module named 'opentimelineio') #\npyblish (ERROR) (line: 1371) pyblish.plugin:\nSkipped: \"extract_otio_review\" (No module named 'opentimelineio')\n# Error: pyblish.plugin : Skipped: \"extract_otio_review\" (No module named 'opentimelineio') #\npyblish (ERROR) (line: 1371) pyblish.plugin:\nSkipped: \"extract_otio_trimming_video\" (No module named 'opentimelineio')\n# Error: pyblish.plugin : Skipped: \"extract_otio_trimming_video\" (No module named 'opentimelineio') #\npyblish (ERROR) (line: 1371) pyblish.plugin:\nSkipped: \"submit_blender_deadline\" (No module named 'bpy')\n# Error: pyblish.plugin : Skipped: \"submit_blender_deadline\" (No module named 'bpy') #\npyblish (ERROR) (line: 1371) pyblish.plugin:\nSkipped: \"submit_houdini_remote_publish\" (No module named 'hou')\n# Error: pyblish.plugin : Skipped: \"submit_houdini_remote_publish\" (No module named 'hou') #\npyblish (ERROR) (line: 1371) pyblish.plugin:\nSkipped: \"submit_houdini_render_deadline\" (No module named 'hou')\n# Error: pyblish.plugin : Skipped: \"submit_houdini_render_deadline\" (No module named 'hou') #\npyblish (ERROR) (line: 1371) pyblish.plugin:\nSkipped: \"submit_max_deadline\" (No module named 'pymxs')\n# Error: pyblish.plugin : Skipped: \"submit_max_deadline\" (No module named 'pymxs') #\npyblish (ERROR) (line: 1371) pyblish.plugin:\nSkipped: \"submit_nuke_deadline\" (No module named 'nuke')\n# Error: pyblish.plugin : Skipped: \"submit_nuke_deadline\" (No module named 'nuke') #\n```\nWe also needed to `stdout` and `stderr` from the launched application to capture the output.Split from #5644.Dependent on #5734\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Render Settings cleanup remove global `RENDER_ATTRS` <a href=\"https://github.com/ynput/OpenPype/pull/5801\">#5801</a></summary>\n\nRemove global `lib.RENDER_ATTRS` and implement a `RenderSettings.get_padding_attr(renderer)` method instead.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Testing: Ingest expected files and input workfile <a href=\"https://github.com/ynput/OpenPype/pull/5840\">#5840</a></summary>\n\nThis ingests the Maya workfile from the Drive storage. Have changed the format to MayaAscii so its easier to see what changes are happening in a PR. This meant changing the expected files and database entries as well.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Create plugin auto-apply settings <a href=\"https://github.com/ynput/OpenPype/pull/5908\">#5908</a></summary>\n\nCreate plugins can auto-apply settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Resolve: Add save current file button + \"Save\" shortcut when menu is active <a href=\"https://github.com/ynput/OpenPype/pull/5691\">#5691</a></summary>\n\nAdds a \"Save current file\" to the OpenPype menu.Also adds a \"Save\" shortcut key sequence (CTRL+S on Windows) to the button, so that clicking CTRL+S when the menu is active will save the current workfile. However this of course does not work if the menu does not receive the key press event (e.g. when Resolve UI is active instead)Resolves #5684\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Reference USD file as maya native geometry <a href=\"https://github.com/ynput/OpenPype/pull/5781\">#5781</a></summary>\n\nAdd MayaUsdReferenceLoader to reference USD as Maya native geometry using `mayaUSDImport` file translator.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Bug fix on wrong aspect ratio and viewport not being maximized during context in review family <a href=\"https://github.com/ynput/OpenPype/pull/5839\">#5839</a></summary>\n\nThis PR will fix the bug on wrong aspect ratio and viewport not being maximized when creating preview animationBesides, the support of tga image format and the options for AA quality are implemented in this PR\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Incorporate blender \"Collections\" into Publish/Load <a href=\"https://github.com/ynput/OpenPype/pull/5841\">#5841</a></summary>\n\nAllow `blendScene` family to include collections.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Allows user preset the setting of preview animation in OP/AYON Setting <a href=\"https://github.com/ynput/OpenPype/pull/5859\">#5859</a></summary>\n\nAllows user preset the setting of preview animation in OP/AYON Setting for review family.\n- [x] Openpype\n- [x] AYON\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Center publisher window on first show <a href=\"https://github.com/ynput/OpenPype/pull/5877\">#5877</a></summary>\n\nMove publisher window to center of a screen on first show.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Instance context changes confirm works <a href=\"https://github.com/ynput/OpenPype/pull/5881\">#5881</a></summary>\n\nConfirmation of context changes in publisher on existing instances does not cause glitches.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON workfiles tools: Revisit workfiles tool <a href=\"https://github.com/ynput/OpenPype/pull/5897\">#5897</a></summary>\n\nRevisited workfiles tool for AYON mode to reuse common models and widgets.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: updated colorspace settings <a href=\"https://github.com/ynput/OpenPype/pull/5906\">#5906</a></summary>\n\nUpdating nuke colorspace settings into more convenient way with usage of ocio config roles rather then particular colorspace names. This way we should not have troubles to switch between linear Rec709 or ACES configs without any additional settings changes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Refactor to new publisher <a href=\"https://github.com/ynput/OpenPype/pull/5910\">#5910</a></summary>\n\nRefactor Blender integration to use the new publisher\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Some publish logs cosmetics <a href=\"https://github.com/ynput/OpenPype/pull/5917\">#5917</a></summary>\n\nGeneral logging message tweaks:\n- Sort some lists of folder/filenames so they appear sorted in the logs\n- Fix some grammar / typos\n- In some cases provide slightly more information in a log\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Better name of 'asset_name' function <a href=\"https://github.com/ynput/OpenPype/pull/5927\">#5927</a></summary>\n\nRenamed function `asset_name` to `prepare_scene_name`.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Bug fix the fbx animation export errored out when the skeletonAnim set is empty <a href=\"https://github.com/ynput/OpenPype/pull/5875\">#5875</a></summary>\n\nResolve this bug discordIf the skeletonAnim SET is empty and fbx animation collect, the fbx animation extractor would skip the fbx extraction\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: fix few typos in houdini's and Maya's Ayon settings <a href=\"https://github.com/ynput/OpenPype/pull/5882\">#5882</a></summary>\n\nFixing few typos\n- [x] Maya unreal static mesh\n- [x] Houdini static mesh\n- [x] Houdini collect asset handles\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: Ayon Deadline env vars + error message on no executable found <a href=\"https://github.com/ynput/OpenPype/pull/5815\">#5815</a></summary>\n\nFix some Ayon x Deadline issues as came up in this topic:\n- missing Environment Variables issue explained here for `deadlinePlugin.RunProcess` for the AYON _extract environments_ call.\n- wrong error formatting described here with a `;` between each character like this: `Ayon executable was not found in the semicolon separated list \"C;:;/;P;r;o;g;r;a;m; ;F;i;l;e;s;/;Y;n;p;u;t;/;A;Y;O;N; ;1;.;0;.;0;-;b;e;t;a;.;5;/;a;y;o;n;_;c;o;n;s;o;l;e;.;e;x;e\". The path to the render executable can be configured from the Plugin Configuration in the Deadline Monitor.`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Fix bundles access in settings <a href=\"https://github.com/ynput/OpenPype/pull/5856\">#5856</a></summary>\n\nFixed access to bundles data in settings to define correct develop variant.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON 3dsMax settings: 'ValidateAttributes' settings converte only if available <a href=\"https://github.com/ynput/OpenPype/pull/5878\">#5878</a></summary>\n\nConvert `ValidateAttributes` settings only if are available in AYON settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Fix TrayPublisher editorial settings <a href=\"https://github.com/ynput/OpenPype/pull/5880\">#5880</a></summary>\n\nFixing Traypublisher settings for adding task in simple editorial.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TrayPublisher: editorial frame range check not needed <a href=\"https://github.com/ynput/OpenPype/pull/5884\">#5884</a></summary>\n\nValidator for frame ranges is not needed during editorial publishing since entity data are not yet in database.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Update houdini license validator <a href=\"https://github.com/ynput/OpenPype/pull/5886\">#5886</a></summary>\n\nAs reported in this community commentHoudini USD publishing is only restricted in Houdini apprentice.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Fix blend extraction and packed images <a href=\"https://github.com/ynput/OpenPype/pull/5888\">#5888</a></summary>\n\nFixed a with blend extractor and packed images.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Initialize connection with all information <a href=\"https://github.com/ynput/OpenPype/pull/5890\">#5890</a></summary>\n\nCreate global AYON api connection with all informations all the time.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Scene inventory tool without site sync <a href=\"https://github.com/ynput/OpenPype/pull/5896\">#5896</a></summary>\n\nSkip 'get_site_icons' if site sync addon is disabled.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publish report tool: Fix PySide6 <a href=\"https://github.com/ynput/OpenPype/pull/5898\">#5898</a></summary>\n\nUse constants from classes instead of objects.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>fusion: removing hardcoded template name for saver <a href=\"https://github.com/ynput/OpenPype/pull/5907\">#5907</a></summary>\n\nFusion is not hardcoded for `render` anatomy template only anymore. This was blocking AYON deployment.\n\n\n___\n\n</details>\n\n\n\n\n## [3.17.5](https://github.com/ynput/OpenPype/tree/3.17.5)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.4...3.17.5)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Fusion: Add USD loader <a href=\"https://github.com/ynput/OpenPype/pull/4896\">#4896</a></summary>\n\nAdd an OpenPype managed USD loader (`uLoader`) for Fusion.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Resolution validator <a href=\"https://github.com/ynput/OpenPype/pull/5325\">#5325</a></summary>\n\nAdded a resolution validator.The code is from my old PR (https://github.com/ynput/OpenPype/pull/4921) that I closed because the PR also contained a frame range validator that no longer is needed.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Context Selection tool: Refactor Context tool (for AYON) <a href=\"https://github.com/ynput/OpenPype/pull/5766\">#5766</a></summary>\n\nContext selection tool has AYON variant.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Use AYON username for user in template data <a href=\"https://github.com/ynput/OpenPype/pull/5842\">#5842</a></summary>\n\nUse ayon username for template data in AYON mode.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Testing: app_group flag <a href=\"https://github.com/ynput/OpenPype/pull/5869\">#5869</a></summary>\n\n`app_group` command flag. This is for changing which flavour of the host to launch. In the case of Maya, you can launch Maya and MayaPy, but it can be used for the Nuke family as well.Split from #5644\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Enhancement: Fusion fix saver creation + minor Blender/Fusion logging tweaks <a href=\"https://github.com/ynput/OpenPype/pull/5558\">#5558</a></summary>\n\n- Blender change logs to `debug` level in preparation for new publisher artist facing reports (note that it currently still uses the old publisher)\n- Fusion: Create Saver fix redeclaration of default_variants\n- Fusion: Fix saver being created in incorrect state without saving directly after create\n- Fusion: Allow reset frame range on render family\n- Fusion: Tweak logging level for artist-facing report\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Resolve: load clip to timeline at set time <a href=\"https://github.com/ynput/OpenPype/pull/5665\">#5665</a></summary>\n\nIt is possible to load clip to correct place on timeline.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Optional Deadline workfile dependency. <a href=\"https://github.com/ynput/OpenPype/pull/5732\">#5732</a></summary>\n\nAdds option to add the workfile as dependency for the Deadline job.Think it used to have something like this, but it disappeared. Usecase is for remote workflow where the Nuke script needs to be synced before the job can start.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement/houdini rearrange ayon houdini settings files <a href=\"https://github.com/ynput/OpenPype/pull/5748\">#5748</a></summary>\n\nRearranging Houdini Settings to be more readable, easier to edit, update settings (include all families/product types)This PR is mainly for Ayon Settings to have more organized files. For Openpype, I'll make sure that  each Houdini setting in Ayon has an equivalent in Openpype.\n- [x] update Ayon settings, fix typos and remove deprecated settings.\n- [x] Sync with Openpype\n- [x] Test in Openpype\n- [x] Test in Ayon\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: updating create ayon addon script <a href=\"https://github.com/ynput/OpenPype/pull/5822\">#5822</a></summary>\n\nAdding developers environment options.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Implement Validator for Properties/Attributes Value Check <a href=\"https://github.com/ynput/OpenPype/pull/5824\">#5824</a></summary>\n\nAdd optional validator which can check if the property attributes are valid in Max\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Remove unused 'get_render_path' function <a href=\"https://github.com/ynput/OpenPype/pull/5826\">#5826</a></summary>\n\nRemove unused function `get_render_path` from nuke integration.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Limit current context template data function <a href=\"https://github.com/ynput/OpenPype/pull/5845\">#5845</a></summary>\n\nCurrent implementation of `get_current_context_template_data` does return the same values as base template data function `get_template_data`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Make sure Collect Render not ignoring instance asset <a href=\"https://github.com/ynput/OpenPype/pull/5847\">#5847</a></summary>\n\n- Make sure Collect Render is not always using asset from context.\n- Make sure Scene version being collected\n- Clean up unnecessary uses of code in the collector.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Events are not processed if project is not available in OpenPype <a href=\"https://github.com/ynput/OpenPype/pull/5853\">#5853</a></summary>\n\nEvents that happened on project which is not in OpenPype is not processed.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Add Nuke 11.0 as default setting <a href=\"https://github.com/ynput/OpenPype/pull/5855\">#5855</a></summary>\n\nFound I needed Nuke 11.0 in the default settings to help with unit testing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TVPaint: Code cleanup <a href=\"https://github.com/ynput/OpenPype/pull/5857\">#5857</a></summary>\n\nRemoved unused import. Use `AYON` label in ayon mode. Removed unused data in publish context `\"previous_context\"`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON settings: Use correct label for follow workfile version <a href=\"https://github.com/ynput/OpenPype/pull/5874\">#5874</a></summary>\n\nFollow workfile version label was marked as Collect Anatomy Instance Data label.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Nuke: Fix workfile template builder so representations get loaded next to each other <a href=\"https://github.com/ynput/OpenPype/pull/5061\">#5061</a></summary>\n\nRefactor when the cleanup of the placeholder happens for the cases where multiple representations are loaded by a single placeholder.The existing code didn't take into account the case where a template placeholder can load multiple representations so it was trying to do the cleanup of the placeholder node and the re-arrangement of the imported nodes too early. I assume this was designed only for the cases where a single representation can load multiple nodes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Dont update node name on update <a href=\"https://github.com/ynput/OpenPype/pull/5704\">#5704</a></summary>\n\nWhen updating `Image` containers the code is trying to set the name of the node. This results in a warning message from Nuke shown below;Suggesting to not change the node name when updating.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>UIDefLabel can be unique <a href=\"https://github.com/ynput/OpenPype/pull/5827\">#5827</a></summary>\n\n`UILabelDef` have implemented comparison and uniqueness.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Skip kitsu module when creating ayon addons <a href=\"https://github.com/ynput/OpenPype/pull/5828\">#5828</a></summary>\n\nCreate AYON packages is skipping kitsu module in creation of modules/addons and kitsu module is not loaded from modules on start. The addon already has it's repository https://github.com/ynput/ayon-kitsu.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: Collect Rendered Files only collecting first instance <a href=\"https://github.com/ynput/OpenPype/pull/5832\">#5832</a></summary>\n\nCollect all instances from the metadata file - don't return on first instance iteration.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: set frame range for the created composite ROP <a href=\"https://github.com/ynput/OpenPype/pull/5833\">#5833</a></summary>\n\nQuick bug fix for created composite ROP, set its frame range to the frame range of the playbar.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fix registering launcher actions from OpenPypeModules <a href=\"https://github.com/ynput/OpenPype/pull/5843\">#5843</a></summary>\n\nFix typo `actions_dir` -> `path` to fix register launcher actions fromm OpenPypeModule\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix in houdini shelves manager and beautify settings  <a href=\"https://github.com/ynput/OpenPype/pull/5844\">#5844</a></summary>\n\nThis PR fixes the problem in this PR https://github.com/ynput/OpenPype/issues/5457 by using the right function to load a pre-made houdini `.shelf` fileAlso, it beautifies houdini shelves settings to provide better guidance for users which helps with other issue https://github.com/ynput/OpenPype/issues/5458 , Rather adding default shelf and set names, I'll educate users how to use the tool correctly.Users now are able to select between the two options.| OpenPype | Ayon || -- | -- ||  |  |\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Fix missing Grease Pencils in review <a href=\"https://github.com/ynput/OpenPype/pull/5848\">#5848</a></summary>\n\nFix Grease Pencil missing in review when isolating objects.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Fix Render Settings in Ayon <a href=\"https://github.com/ynput/OpenPype/pull/5849\">#5849</a></summary>\n\nFix Render Settings in Ayon for Blender.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: houdini tab menu working as expected <a href=\"https://github.com/ynput/OpenPype/pull/5850\">#5850</a></summary>\n\nThis PR:Tab menu name changes to Ayon when using ayon get_network_categories is checked in all creator plugins.  | Product | Network Category |  | -- | -- |  | Alembic camera | rop, obj | | Arnold Ass | rop | | Arnold ROP | rop | | Bgeo | rop, sop | | composite sequence | cop2, rop | | hda | obj | | Karma ROP | rop | | Mantra ROP | rop | | ABC | rop, sop | | RS proxy | rop, sop|  | RS ROP | rop | | Review | rop | | Static mesh | rop, obj, sop | | USD | lop, rop | | USD Render | rop | | VDB | rop, obj, sop | | V Ray | rop |\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bigfix: Houdini skip frame_range_validator if node has no 'trange' parameter <a href=\"https://github.com/ynput/OpenPype/pull/5851\">#5851</a></summary>\n\nI faced a bug when publishing HDA instance as it has no `trange` parameter. As this PR title says : skip  frame_range_validator  if node has no 'trange' parameter\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: houdini image sequence loading and missing frames <a href=\"https://github.com/ynput/OpenPype/pull/5852\">#5852</a></summary>\n\nI made this PR in to fix issues mentioned here https://github.com/ynput/OpenPype/pull/5833#issuecomment-1789207727in short:\n- image load doesn't work\n- publisher only publish one frame\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: loaders' containers updating as nodes <a href=\"https://github.com/ynput/OpenPype/pull/5854\">#5854</a></summary>\n\nNuke loaded containers are updating correctly even they have been duplicating of originally loaded nodes. This had previously been removed duplicated nodes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>deadline: settings are not blocking extension input <a href=\"https://github.com/ynput/OpenPype/pull/5864\">#5864</a></summary>\n\nSettings are not blocking user input.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Fix loading of blend layouts <a href=\"https://github.com/ynput/OpenPype/pull/5866\">#5866</a></summary>\n\nFix a problem with loading blend layouts.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Launcher refresh issues <a href=\"https://github.com/ynput/OpenPype/pull/5867\">#5867</a></summary>\n\nFixed refresh of projects issue in launcher tool. And renamed Qt models to contain `Qt` in their name (it was really hard to find out where were used). It is not possible to click on disabled item in launcher's projects view.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fix the Wrong key words for tycache workfile template settings in AYON <a href=\"https://github.com/ynput/OpenPype/pull/5870\">#5870</a></summary>\n\nFix the wrong key words for the tycache workfile template settings in AYON(i.e. Instead of families, product_types should be used)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON tools: Handle empty icon definition <a href=\"https://github.com/ynput/OpenPype/pull/5876\">#5876</a></summary>\n\nIgnore if passed icon definition is `None`.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Houdini: Remove on instance toggled callback <a href=\"https://github.com/ynput/OpenPype/pull/5860\">#5860</a></summary>\n\nRemove on instance toggled callback which isn't relevant to the new publisher\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Remove unused `instanceToggled` callbacks <a href=\"https://github.com/ynput/OpenPype/pull/5862\">#5862</a></summary>\n\nThe `instanceToggled` callbacks should be irrelevant for new publisher.\n\n\n___\n\n</details>\n\n\n\n\n## [3.17.4](https://github.com/ynput/OpenPype/tree/3.17.4)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.3...3.17.4)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Add Support for Husk-AYON Integration <a href=\"https://github.com/ynput/OpenPype/pull/5816\">#5816</a></summary>\n\nThis draft pull request introduces support for integrating Husk with AYON within the OpenPype repository.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Push to project tool: Prepare push to project tool for AYON <a href=\"https://github.com/ynput/OpenPype/pull/5770\">#5770</a></summary>\n\nCloned Push to project tool for AYON and modified it.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Max: tycache family support <a href=\"https://github.com/ynput/OpenPype/pull/5624\">#5624</a></summary>\n\nTycache family supports for Tyflow Plugin in Max\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Changed behaviour for updating assets <a href=\"https://github.com/ynput/OpenPype/pull/5670\">#5670</a></summary>\n\nChanged how assets are updated in Unreal.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Improved error reporting for Sequence Frame Validator <a href=\"https://github.com/ynput/OpenPype/pull/5730\">#5730</a></summary>\n\nImproved error reporting for Sequence Frame Validator.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Setting tweaks on Review Family <a href=\"https://github.com/ynput/OpenPype/pull/5744\">#5744</a></summary>\n\n- Bug fix of not being able to publish the preferred visual style when creating preview animation\n- Exposes the parameters after creating instance\n- Add the Quality settings and viewport texture settings for preview animation\n- add use selection for create review\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Add families with frame range extractions back to the frame range validator <a href=\"https://github.com/ynput/OpenPype/pull/5757\">#5757</a></summary>\n\nIn 3dsMax, there are some instances which exports the files in frame range but not being added to the optional frame range validator. In this PR, these instances would have the optional frame range validators to allow users to check if frame range aligns with the context data from DB.The following families have been added to have optional frame range validator:\n- maxrender\n- review\n- camera\n- redshift proxy\n- pointcache\n- point cloud(tyFlow PRT)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TimersManager: Use available data to get context info <a href=\"https://github.com/ynput/OpenPype/pull/5804\">#5804</a></summary>\n\nGet context information from pyblish context data instead of using `legacy_io`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Removed unused variable from `AbstractCollectRender` <a href=\"https://github.com/ynput/OpenPype/pull/5805\">#5805</a></summary>\n\nRemoved unused `_asset` variable from `RenderInstance`.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Bugfix/houdini:  wrong frame calculation with handles <a href=\"https://github.com/ynput/OpenPype/pull/5698\">#5698</a></summary>\n\nThis PR make collect plugins to consider `handleStart` and `handleEnd` when collecting frame range it affects three parts:\n- get frame range in collect plugins\n- expected file in render plugins\n- submit houdini job deadline plugin\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: ayon server settings improvements <a href=\"https://github.com/ynput/OpenPype/pull/5746\">#5746</a></summary>\n\nNuke settings were not aligned with OpenPype settings. Also labels needed to be improved.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Fix pointcache family and fix alembic extractor <a href=\"https://github.com/ynput/OpenPype/pull/5747\">#5747</a></summary>\n\nFixed `pointcache` family and fixed behaviour of the alembic extractor.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Remove 'shotgun_api3' from dependencies <a href=\"https://github.com/ynput/OpenPype/pull/5803\">#5803</a></summary>\n\nRemoved `shotgun_api3` dependency from openpype dependencies for AYON launcher. The dependency is already defined in shotgrid addon and change of version causes clashes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Fix typo in filename <a href=\"https://github.com/ynput/OpenPype/pull/5807\">#5807</a></summary>\n\nMove content of `contants.py` into `constants.py`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Create context respects instance changes <a href=\"https://github.com/ynput/OpenPype/pull/5809\">#5809</a></summary>\n\nFix issue with unrespected change propagation in `CreateContext`. All successfully saved instances are marked as saved so they have no changes. Origin data of an instance are explicitly not handled directly by the object but by the attribute wrappers.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Fix tools handling in AYON mode <a href=\"https://github.com/ynput/OpenPype/pull/5811\">#5811</a></summary>\n\nSkip logic in `before_window_show` in blender when in AYON mode. Most of the stuff called there happes on show automatically.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Include Grease Pencil in review and thumbnails <a href=\"https://github.com/ynput/OpenPype/pull/5812\">#5812</a></summary>\n\nInclude Grease Pencil in review and thumbnails.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Workfiles tool AYON: Fix double click of workfile <a href=\"https://github.com/ynput/OpenPype/pull/5813\">#5813</a></summary>\n\nFix double click on workfiles in workfiles tool to open the file.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Webpublisher: removal of usage of no_of_frames in error message <a href=\"https://github.com/ynput/OpenPype/pull/5819\">#5819</a></summary>\n\nIf it throws exception, `no_of_frames` value  wont be available, so it doesn't make sense to log it.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Attribute Defs: Hide multivalue widget in Number by default <a href=\"https://github.com/ynput/OpenPype/pull/5821\">#5821</a></summary>\n\nFixed default look of `NumberAttrWidget` by hiding its multiselection widget.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Corrected a typo in Readme.md (Top -> To) <a href=\"https://github.com/ynput/OpenPype/pull/5800\">#5800</a></summary>\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Photoshop: Removed redundant copy of extension.zxp <a href=\"https://github.com/ynput/OpenPype/pull/5802\">#5802</a></summary>\n\n`extension.zxp` shouldn't be inside of extension folder.\n\n\n___\n\n</details>\n\n\n\n\n## [3.17.3](https://github.com/ynput/OpenPype/tree/3.17.3)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.2...3.17.3)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Maya: Multi-shot Layout Creator <a href=\"https://github.com/ynput/OpenPype/pull/5710\">#5710</a></summary>\n\nNew Multi-shot Layout creator is a way of automating creation of the new Layout instances in Maya, associated with correct shots, frame ranges and Camera Sequencer in Maya.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Colorspace: ociolook file product type workflow <a href=\"https://github.com/ynput/OpenPype/pull/5541\">#5541</a></summary>\n\nTraypublisher support for publishing of colorspace look files (ociolook) which are json files holding any LUT files. This new product is available for loading in Nuke host at the moment.Added colorspace selector to publisher attribute with better labeling. We are supporting also Roles and Alias (only v2 configs).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Scene Inventory tool: Refactor Scene Inventory tool (for AYON) <a href=\"https://github.com/ynput/OpenPype/pull/5758\">#5758</a></summary>\n\nModified scene inventory tool for AYON. The main difference is in how project name is defined and replacement of assets combobox with folders dialog.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Support dev bundles <a href=\"https://github.com/ynput/OpenPype/pull/5783\">#5783</a></summary>\n\nModules can be loaded in AYON dev mode from different location.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Testing: Ingest Maya userSetup <a href=\"https://github.com/ynput/OpenPype/pull/5734\">#5734</a></summary>\n\nSuggesting to ingest `userSetup.py` startup script for easier collaboration and transparency of testing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Work with pathmaps <a href=\"https://github.com/ynput/OpenPype/pull/5329\">#5329</a></summary>\n\nPath maps are a big part of our Fusion workflow. We map the project folder to a path map within Fusion so all loaders and savers point to the path map variable. This way any computer on any OS can open any comp no matter where the project folder is located.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Add Maya 2024 and remove pre 2022. <a href=\"https://github.com/ynput/OpenPype/pull/5674\">#5674</a></summary>\n\nAdding Maya 2024 as default application variant.Removing Maya 2020 and older, as these are not supported anymore.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Houdini: Allow using template keys in Houdini shelves manager <a href=\"https://github.com/ynput/OpenPype/pull/5727\">#5727</a></summary>\n\nAllow using Template keys in Houdini shelves manager.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Fix Show in usdview loader action <a href=\"https://github.com/ynput/OpenPype/pull/5737\">#5737</a></summary>\n\nFix the \"Show in USD View\" loader to show up in Houdini\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: validator of asset context with repair actions <a href=\"https://github.com/ynput/OpenPype/pull/5749\">#5749</a></summary>\n\nInstance nodes with different context of asset and task can be now validated and repaired via repair action.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Tools enhancements <a href=\"https://github.com/ynput/OpenPype/pull/5753\">#5753</a></summary>\n\nFew enhancements and tweaks of AYON related tools.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Tweaks on ValidateMaxContents <a href=\"https://github.com/ynput/OpenPype/pull/5759\">#5759</a></summary>\n\nThis PR provides enhancements on ValidateMaxContent as follow:\n- Rename `ValidateMaxContents` to `ValidateContainers`\n- Add related families which are required to pass the validation(All families except `Render` as the render instance is the one which only allows empty container)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Nuke refactor `SelectInvalidAction` <a href=\"https://github.com/ynput/OpenPype/pull/5762\">#5762</a></summary>\n\nRefactor `SelectInvalidAction` to behave like other action for other host, create `SelectInstanceNodeAction` as dedicated action to select the instance node for a failed plugin.\n- Note: Selecting Instance Node will still select the instance node even if the user has currently 'fixed' the problem.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Tweak logging for Nuke for artist facing reports <a href=\"https://github.com/ynput/OpenPype/pull/5763\">#5763</a></summary>\n\nTweak logs that are not artist-facing to debug level + in some cases clarify what the logged value is.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON Settings: Disk mapping <a href=\"https://github.com/ynput/OpenPype/pull/5786\">#5786</a></summary>\n\nAdded disk mapping settings to core addon settings.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: add colorspace argument to redshiftTextureProcessor <a href=\"https://github.com/ynput/OpenPype/pull/5645\">#5645</a></summary>\n\nIn color managed Maya, texture processing during Look Extraction wasn't passing texture colorspaces set on textures to `redshiftTextureProcessor` tool. This in effect caused this tool to produce non-zero exit code (even though the texture was converted into wrong colorspace) and therefor crash of the extractor. This PR is passing colorspace to that tool if color management is enabled.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: don't call `cmds.ogs()` in headless mode <a href=\"https://github.com/ynput/OpenPype/pull/5769\">#5769</a></summary>\n\n`cmds.ogs()` is a call that will crash if Maya is running in headless mode (mayabatch, mayapy). This is handling that case.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Resolve: inventory management fix <a href=\"https://github.com/ynput/OpenPype/pull/5673\">#5673</a></summary>\n\nLoaded Timeline item containers are now updating correctly and version management is working as it suppose to.\n- [x] updating loaded timeline items\n- [x] Removing of loaded timeline items\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Remove 'update_hierarchy' <a href=\"https://github.com/ynput/OpenPype/pull/5756\">#5756</a></summary>\n\nRemove `update_hierarchy` function which is causing crashes in scene inventory tool.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: bug fix on the settings in pointcloud family <a href=\"https://github.com/ynput/OpenPype/pull/5768\">#5768</a></summary>\n\nBug fix on the settings being errored out in validate point cloud(see links:https://github.com/ynput/OpenPype/pull/5759#pullrequestreview-1676681705) and passibly in point cloud extractor.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON settings: Fix default factory of tools <a href=\"https://github.com/ynput/OpenPype/pull/5773\">#5773</a></summary>\n\nFix default factory of application tools.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: added missing OPENPYPE_VERSION <a href=\"https://github.com/ynput/OpenPype/pull/5776\">#5776</a></summary>\n\nFusion submission to Deadline was missing OPENPYPE_VERSION env var when submitting from build (not source code directly). This missing env var might break rendering on DL if path to OP executable (openpype_console.exe) is not set explicitly and might cause an issue when different versions of OP are deployed.This PR adds this environment variable.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Skip tasks when looking for asset equivalent entity <a href=\"https://github.com/ynput/OpenPype/pull/5777\">#5777</a></summary>\n\nSkip tasks when looking for asset equivalent entity.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: loading gizmos fixes <a href=\"https://github.com/ynput/OpenPype/pull/5779\">#5779</a></summary>\n\nGizmo product is not offered in Loader as plugin. It is also updating as expected.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: thumbnail extractor as last extractor <a href=\"https://github.com/ynput/OpenPype/pull/5780\">#5780</a></summary>\n\nFixing issue with the order of the `ExtractOIIOTranscode` and `ExtractThumbnail` plugins. The problem was that the `ExtractThumbnail` plugin was processed before the `ExtractOIIOTranscode` plugin. As a result, the `ExtractThumbnail` plugin did not inherit the `review` tag into the representation data. This caused the `ExtractThumbnail` plugin to fail in processing and creating thumbnails.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bug: fix key in application json <a href=\"https://github.com/ynput/OpenPype/pull/5787\">#5787</a></summary>\n\nIn PR #5705 `maya` was wrongly used instead of `mayapy`, breaking AYON defaults in AYON Application Addon.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>'NumberAttrWidget' shows 'Multiselection' label on multiselection <a href=\"https://github.com/ynput/OpenPype/pull/5792\">#5792</a></summary>\n\nAttribute definition widget 'NumberAttrWidget' shows `< Multiselection >`  label on multiselection.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Selection change by enabled checkbox on instance update attributes <a href=\"https://github.com/ynput/OpenPype/pull/5793\">#5793</a></summary>\n\nChange of instance by clicking on enabled checkbox will actually update attributes on right side to match the selection.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Remove `setParms` call since it's responsibility of `self.imprint` to set the values <a href=\"https://github.com/ynput/OpenPype/pull/5796\">#5796</a></summary>\n\nRevert a recent change made in #5621 due to this comment. However the change is faulty as can be seen mentioned here\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON loader: Fix SubsetLoader functionality <a href=\"https://github.com/ynput/OpenPype/pull/5799\">#5799</a></summary>\n\nFix SubsetLoader plugin processing in AYON loader tool.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Houdini: Add self publish button <a href=\"https://github.com/ynput/OpenPype/pull/5621\">#5621</a></summary>\n\nThis PR allows single publishing by adding a publish button to created rop nodes in HoudiniAdmins are much welcomed to enable it from houdini general settingsPublish Button also includes all input publish instances. in this screen shot the alembic instance is ignored because the switch is turned off\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: fixing UNC support for OCIO path <a href=\"https://github.com/ynput/OpenPype/pull/5771\">#5771</a></summary>\n\nUNC paths were broken on windows for custom OCIO path and this is solving the issue with removed double slash at start of path\n\n\n___\n\n</details>\n\n\n\n\n## [3.17.2](https://github.com/ynput/OpenPype/tree/3.17.2)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.1...3.17.2)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Maya: Add MayaPy application. <a href=\"https://github.com/ynput/OpenPype/pull/5705\">#5705</a></summary>\n\nThis adds mayapy to the application to be launched from a task.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Feature: Copy resources when downloading last workfile <a href=\"https://github.com/ynput/OpenPype/pull/4944\">#4944</a></summary>\n\nWhen the last published workfile is downloaded as a prelaunch hook, all resource files referenced in the workfile representation are copied to the `resources` folder, which is inside the local workfile folder.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Deadline support <a href=\"https://github.com/ynput/OpenPype/pull/5438\">#5438</a></summary>\n\nAdd Deadline support for Blender.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: implement toggle to use Deadline plugin FusionCmd <a href=\"https://github.com/ynput/OpenPype/pull/5678\">#5678</a></summary>\n\nFusion 17 doesn't work in DL 10.3, but FusionCmd does. It might be probably better option as headless variant.Fusion plugin seems to be closing and reopening application when worker is running on artist machine, not so with FusionCmdAdded configuration to Project Settings for admin to select appropriate Deadline plugin:\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Loader tool: Refactor loader tool (for AYON) <a href=\"https://github.com/ynput/OpenPype/pull/5729\">#5729</a></summary>\n\nRefactored loader tool to new tool. Separated backend and frontend logic. Refactored logic is AYON-centric and is used only in AYON mode, so it does not affect OpenPype. The tool is also replacing library loader.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: implement matchmove publishing <a href=\"https://github.com/ynput/OpenPype/pull/5445\">#5445</a></summary>\n\nAdd possibility to export multiple cameras in single `matchmove` family instance, both in `abc` and `ma`.Exposed flag 'Keep image planes' to control export of image planes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Add optional Fbx extractors in Rig and Animation family <a href=\"https://github.com/ynput/OpenPype/pull/5589\">#5589</a></summary>\n\nThis PR allows user to export control rigs(optionally with mesh) and animated rig in fbx optionally by attaching the rig objects to the two newly introduced sets.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Optional Resolution Validator for Render <a href=\"https://github.com/ynput/OpenPype/pull/5693\">#5693</a></summary>\n\nAdding optional resolution validator for maya in render family, similar to the one in Max.It checks if the resolution in render setting aligns with that in setting from the db.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Use host's node uniqueness for instance id in new publisher <a href=\"https://github.com/ynput/OpenPype/pull/5490\">#5490</a></summary>\n\nInstead of writing `instance_id` as parm or attributes on the publish instances we can, for some hosts, just rely on a unique name or path within the scene to refer to that particular instance. By doing so we fix #4820 because upon duplicating such a publish instance using the host's (DCC) functionality the uniqueness for the duplicate is then already ensured instead of attributes remaining exact same value as where to were duplicated from, making `instance_id` a non-unique value.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Implementation of OCIO configuration  <a href=\"https://github.com/ynput/OpenPype/pull/5499\">#5499</a></summary>\n\nResolve #5473 Implementation of OCIO configuration for Max 2024 regarding to the update of Max 2024\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Multiple format supports for ExtractReviewDataMov <a href=\"https://github.com/ynput/OpenPype/pull/5623\">#5623</a></summary>\n\nThis PR would fix the bug of  the plugin `ExtractReviewDataMov` not being able to support extensions other than `mov`. The plugin is also renamed to `ExtractReviewDataBakingStreams` as i provides multiple format supoort.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: houdini switching context doesnt update variables <a href=\"https://github.com/ynput/OpenPype/pull/5651\">#5651</a></summary>\n\nAllows admins to have a list of vars (e.g. JOB) with (dynamic) values that will be updated on context changes, e.g. when switching to another asset or task.Using template keys is supported but formatting keys capitalization variants is not, e.g. {Asset} and {ASSET} won't workDisabling Update Houdini vars on context change feature will leave all Houdini vars unmanaged and thus no context update changes will occur.Also, this PR adds a new button in menu to update vars on demand. \n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Fix report maker memory leak + optimize lookups using set <a href=\"https://github.com/ynput/OpenPype/pull/5667\">#5667</a></summary>\n\nFixes a memory leak where resetting publisher does not clear the stored plugins for the Publish Report Maker.Also changes the stored plugins to a `set` to optimize the lookup speeds.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Add openpype_mongo command flag for testing. <a href=\"https://github.com/ynput/OpenPype/pull/5676\">#5676</a></summary>\n\nInstead of changing the environment, this command flag allows for changing the database.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: minor docstring and code tweaks for ExtractReviewMov <a href=\"https://github.com/ynput/OpenPype/pull/5695\">#5695</a></summary>\n\nCode and docstring tweaks on https://github.com/ynput/OpenPype/pull/5623\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Small settings fixes <a href=\"https://github.com/ynput/OpenPype/pull/5699\">#5699</a></summary>\n\nSmall changes/fixes related to AYON settings. All foundry apps variant `13-0` has label `13.0`. Key `\"ExtractReviewIntermediates\"` is not mandatory in settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Alembic Animation loader <a href=\"https://github.com/ynput/OpenPype/pull/5711\">#5711</a></summary>\n\nImplemented loading Alembic Animations in Blender.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Missing \"data\" field and enabling of audio <a href=\"https://github.com/ynput/OpenPype/pull/5618\">#5618</a></summary>\n\nWhen updating audio containers, the field \"data\" was missing and the audio node was not enabled on the timeline.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Bug in validate Plug-in Path Attribute <a href=\"https://github.com/ynput/OpenPype/pull/5687\">#5687</a></summary>\n\nOverwriting list with string is causing `TypeError: string indices must be integers` in subsequent iterations, crashing the validator plugin.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Avoid fallback if value is 0 for handle start/end <a href=\"https://github.com/ynput/OpenPype/pull/5652\">#5652</a></summary>\n\nThere's a bug on the `pyblish_functions.get_time_data_from_instance_or_context` where if `handleStart` or `handleEnd` on the instance are set to value 0 it's falling back to grabbing the handles from the instance context. Instead, the logic should be that it only falls back to the `instance.context` if the key doesn't exist.This change was only affecting me on the `handleStart`/`handleEnd` and it's unlikely it could cause issues on `frameStart`, `frameEnd` or `fps` but regardless, the `get` logic is wrong.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: added missing env vars to Deadline submission <a href=\"https://github.com/ynput/OpenPype/pull/5659\">#5659</a></summary>\n\nEnvironment variables discerning type of job was missing. Without this injection of environment variables won't start.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: workfile version synchronization settings fixed <a href=\"https://github.com/ynput/OpenPype/pull/5662\">#5662</a></summary>\n\nSettings for synchronizing workfile version to published products is fixed.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON Workfiles Tool: Open workfile changes context <a href=\"https://github.com/ynput/OpenPype/pull/5671\">#5671</a></summary>\n\nChange context when workfile is opened.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Fix remove/update in new layout instance <a href=\"https://github.com/ynput/OpenPype/pull/5679\">#5679</a></summary>\n\nFixes an error that occurs when removing or updating an asset in a new layout instance.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON Launcher tool: Fix refresh btn <a href=\"https://github.com/ynput/OpenPype/pull/5685\">#5685</a></summary>\n\nRefresh button does propagate refreshed content properly. Folders and tasks are cached for 60 seconds instead of 10 seconds. Auto-refresh in launcher will refresh only actions and related data which is project and project settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: handle all valid paths in RenderExecutable <a href=\"https://github.com/ynput/OpenPype/pull/5694\">#5694</a></summary>\n\nThis commit enhances the path resolution mechanism in the RenderExecutable function of the Ayon plugin. Previously, the function only considered paths starting with a tilde (~), ignoring other valid paths listed in exe_list. This limitation led to an empty expanded_paths list when none of the paths in exe_list started with a tilde, causing the function to fail in finding the Ayon executable.With this fix, the RenderExecutable function now correctly processes and includes all valid paths from exe_list, improving its reliability and preventing unnecessary errors related to Ayon executable location.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON Launcher tool: Fix skip last workfile boolean <a href=\"https://github.com/ynput/OpenPype/pull/5700\">#5700</a></summary>\n\nSkip last workfile boolean works as expected.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Explore here action can work without task <a href=\"https://github.com/ynput/OpenPype/pull/5703\">#5703</a></summary>\n\nExplore here action does not crash when task is not selected, and change error message a little.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Testing: Inject mongo_url argument earlier <a href=\"https://github.com/ynput/OpenPype/pull/5706\">#5706</a></summary>\n\nFix for https://github.com/ynput/OpenPype/pull/5676The Mongo url is used earlier in the execution.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Add support to auto-install PySide2 in blender 4 <a href=\"https://github.com/ynput/OpenPype/pull/5723\">#5723</a></summary>\n\nChange version regex to support blender 4 subfolder.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fix: Hardcoded main site and wrongly copied workfile <a href=\"https://github.com/ynput/OpenPype/pull/5733\">#5733</a></summary>\n\nFixing these two issues:\n- Hardcoded main site -> Replaced by `anatomy.fill_root`.\n- Workfiles can sometimes be copied while they shouldn't.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: ServerDeleteOperation asset -> folder conversion typo <a href=\"https://github.com/ynput/OpenPype/pull/5735\">#5735</a></summary>\n\nFix ServerDeleteOperation asset -> folder conversion typo\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: loaders are filtering correctly <a href=\"https://github.com/ynput/OpenPype/pull/5739\">#5739</a></summary>\n\nVariable name for filtering by extensions were not correct - it suppose to be plural. It is fixed now and filtering is working as suppose to.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: failing multiple thumbnails integration <a href=\"https://github.com/ynput/OpenPype/pull/5741\">#5741</a></summary>\n\nThis handles the situation when `ExtractReviewIntermediates` (previously `ExtractReviewDataMov`) has multiple outputs, including thumbnails that need to be integrated. Previously, integrating the thumbnail representation was causing an issue in the integration process. However, we have now resolved this issue by no longer integrating thumbnails as loadable representations.NOW default is that thumbnail representation are NOT integrated (eg. they will not show up in DB > couldn't be Loaded in Loader) and no `_thumb.jpg` will be left in `render` (most likely) publish folder.IF there would be need to override this behavior, please use `project_settings/global/publish/PreIntegrateThumbnails`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON Settings: Fix global overrides <a href=\"https://github.com/ynput/OpenPype/pull/5745\">#5745</a></summary>\n\nThe `output` dictionary that gets passed into `ayon_settings._convert_global_project_settings` gets replaced when converting the settings for `ExtractOIIOTranscode`. This results in `global` not being in the output dictionary and thus the defaults being used and not the project overrides.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: AYON query functions arguments <a href=\"https://github.com/ynput/OpenPype/pull/5752\">#5752</a></summary>\n\nFixed how `archived` argument is handled in get subsets/assets function.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Publisher: Refactor Report Maker plugin data storage to be a dict by plugin.id <a href=\"https://github.com/ynput/OpenPype/pull/5668\">#5668</a></summary>\n\nRefactor Report Maker plugin data storage to be a dict by `plugin.id`Also fixes `_current_plugin_data` type on `__init__`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Refactor Resolve into new style HostBase, IWorkfileHost, ILoadHost <a href=\"https://github.com/ynput/OpenPype/pull/5701\">#5701</a></summary>\n\nRefactor Resolve into new style HostBase, IWorkfileHost, ILoadHost\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Chore: Maya reduce get project settings calls <a href=\"https://github.com/ynput/OpenPype/pull/5669\">#5669</a></summary>\n\nRe-use system settings / project settings where we can instead of requerying.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Extended error message when getting subset name <a href=\"https://github.com/ynput/OpenPype/pull/5649\">#5649</a></summary>\n\nEach Creator is using `get_subset_name` functions which collects context data and fills configured template with placeholders.If any key is missing in the template, non descriptive error is thrown.This should provide more verbose message:\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Tests: Remove checks for env var <a href=\"https://github.com/ynput/OpenPype/pull/5696\">#5696</a></summary>\n\nEnv var will be filled in `env_var` fixture, here it is too early to check\n\n\n___\n\n</details>\n\n\n\n\n## [3.17.1](https://github.com/ynput/OpenPype/tree/3.17.1)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.0...3.17.1)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Unreal: Yeti support <a href=\"https://github.com/ynput/OpenPype/pull/5643\">#5643</a></summary>\n\nImplemented Yeti support for Unreal.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Add Static Mesh product-type (family) <a href=\"https://github.com/ynput/OpenPype/pull/5481\">#5481</a></summary>\n\nThis PR adds support to publish Unreal Static Mesh in Houdini as FBXQuick recap\n- [x] Add UE Static Mesh Creator\n- [x] Dynamic subset name like in Maya\n- [x] Collect Static Mesh Type\n- [x] Update collect output node\n- [x] Validate FBX output node\n- [x] Validate mesh is static\n- [x] Validate Unreal Static Mesh Name\n- [x] Validate Subset Name\n- [x] FBX Extractor\n- [x] FBX Loader\n- [x] Update OP Settings\n- [x] Update AYON Settings\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Launcher tool: Refactor launcher tool (for AYON) <a href=\"https://github.com/ynput/OpenPype/pull/5612\">#5612</a></summary>\n\nRefactored launcher tool to new tool. Separated backend and frontend logic. Refactored logic is AYON-centric and is used only in AYON mode, so it does not affect OpenPype.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: Use custom staging dir function for Maya renders - OP-5265 <a href=\"https://github.com/ynput/OpenPype/pull/5186\">#5186</a></summary>\n\nCheck for custom staging dir when setting the renders output folder in Maya.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Colorspace: updating file path detection methods <a href=\"https://github.com/ynput/OpenPype/pull/5273\">#5273</a></summary>\n\nSupport for OCIO v2 file rules integrated into the available color management API\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: add default isort config <a href=\"https://github.com/ynput/OpenPype/pull/5572\">#5572</a></summary>\n\nAdd default configuration for isort tool\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: set PATH environment in deadline jobs by GlobalJobPreLoad <a href=\"https://github.com/ynput/OpenPype/pull/5622\">#5622</a></summary>\n\nThis PR makes `GlobalJobPreLoad` to set `PATH` environment in deadline jobs so that we don't have to use the full executable path for deadline to launch the dcc app. This trick should save us adding logic to pass houdini patch version and modifying Houdini deadline plugin. This trick should work with other DCCs\n\n\n___\n\n</details>\n\n\n<details>\n<summary>nuke: extract review data mov read node with expression <a href=\"https://github.com/ynput/OpenPype/pull/5635\">#5635</a></summary>\n\nSome productions might have set default values for read nodes, those settings are not colliding anymore now.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Support new publisher for colorsets validation. <a href=\"https://github.com/ynput/OpenPype/pull/5630\">#5630</a></summary>\n\nFix `validate_color_sets` for the new publisher.In current `develop` the repair option does not appear due to wrong error raising.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Camera Loader fix mismatch for Maya cameras <a href=\"https://github.com/ynput/OpenPype/pull/5584\">#5584</a></summary>\n\nThis PR adds\n- A workaround to match Maya render mask in Houdini\n- `SetCameraResolution` inventory action\n- set camera resolution when loading or updating camera\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: fix set colorspace on writes <a href=\"https://github.com/ynput/OpenPype/pull/5634\">#5634</a></summary>\n\nColorspace is set correctly to any write node created from publisher.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TVPaint: Fix review family extraction <a href=\"https://github.com/ynput/OpenPype/pull/5637\">#5637</a></summary>\n\nExtractor marks representation of review instance with review tag.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON settings: Extract OIIO transcode settings <a href=\"https://github.com/ynput/OpenPype/pull/5639\">#5639</a></summary>\n\nOutput definitions of Extract OIIO transcode have name to match OpenPype settings, and the settings are converted to dictionary in settings conversion.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Fix task type short name conversion <a href=\"https://github.com/ynput/OpenPype/pull/5641\">#5641</a></summary>\n\nConvert AYON task type short name for OpenPype correctly.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>colorspace: missing `allowed_exts` fix <a href=\"https://github.com/ynput/OpenPype/pull/5646\">#5646</a></summary>\n\nColorspace module is not failing due to missing `allowed_exts` attribute.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Photoshop: remove trailing underscore in subset name <a href=\"https://github.com/ynput/OpenPype/pull/5647\">#5647</a></summary>\n\nIf {layer} placeholder is at the end of subset name template and not used (for example in `auto_image` where separating it by layer doesn't make any sense) trailing '_' was kept. This updates cleaning logic and extracts it as it might be similar in regular `image` instance.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>traypublisher: missing `assetEntity` in context data <a href=\"https://github.com/ynput/OpenPype/pull/5648\">#5648</a></summary>\n\nIssue with missing `assetEnity` key in context data is not problem anymore.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Workfiles tool save button works <a href=\"https://github.com/ynput/OpenPype/pull/5653\">#5653</a></summary>\n\nFix save as button in workfiles tool.(It is mystery why this stopped to work??)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: bug fix delete items from container <a href=\"https://github.com/ynput/OpenPype/pull/5658\">#5658</a></summary>\n\nFix the bug shown when clicking \"Delete Items from Container\" and selecting nothing and press ok.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Chore: Remove unused functions from Fusion integration <a href=\"https://github.com/ynput/OpenPype/pull/5617\">#5617</a></summary>\n\nCleanup unused code from Fusion integration\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Increase timout for deadline test <a href=\"https://github.com/ynput/OpenPype/pull/5654\">#5654</a></summary>\n\nDL picks up jobs quite slow, so bump up delay.\n\n\n___\n\n</details>\n\n\n\n\n## [3.17.0](https://github.com/ynput/OpenPype/tree/3.17.0)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.7...3.17.0)\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Chore: Remove schema from OpenPype root <a href=\"https://github.com/ynput/OpenPype/pull/5355\">#5355</a></summary>\n\nRemove unused schema directory in root of repository which was moved inside openpype/pipeline/schema.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Igniter: Allow custom Qt scale factor rounding policy <a href=\"https://github.com/ynput/OpenPype/pull/5554\">#5554</a></summary>\n\nDo not force `PassThrough` rounding policy if different policy is defined via env variable.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Chore: Lower urllib3 to support older OpenSSL <a href=\"https://github.com/ynput/OpenPype/pull/5538\">#5538</a></summary>\n\nLowered `urllib3` to `1.26.16` to support older OpenSSL.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Do not try to add schema to zip files <a href=\"https://github.com/ynput/OpenPype/pull/5557\">#5557</a></summary>\n\nDo not add `schema` folder to zip file. This fixes issue cause by https://github.com/ynput/OpenPype/pull/5355 .\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Lower click dependency version <a href=\"https://github.com/ynput/OpenPype/pull/5629\">#5629</a></summary>\n\nLower click version to support older versions of python.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Bump certifi from 2023.5.7 to 2023.7.22 <a href=\"https://github.com/ynput/OpenPype/pull/5351\">#5351</a></summary>\n\nBumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22.\n<details>\n<summary>Commits</summary>\n<ul>\n<li><a href=\"https://github.com/certifi/python-certifi/commit/8fb96ed81f71e7097ed11bc4d9b19afd7ea5c909\"><code>8fb96ed</code></a> 2023.07.22</li>\n<li><a href=\"https://github.com/certifi/python-certifi/commit/afe77220e0eaa722593fc5d294213ff5275d1b40\"><code>afe7722</code></a> Bump actions/setup-python from 4.6.1 to 4.7.0 (<a href=\"https://redirect.github.com/certifi/python-certifi/issues/230\">#230</a>)</li>\n<li><a href=\"https://github.com/certifi/python-certifi/commit/2038739ad56abec7aaddfa90ad2ce6b3ed7f5c7b\"><code>2038739</code></a> Bump dessant/lock-threads from 3.0.0 to 4.0.1 (<a href=\"https://redirect.github.com/certifi/python-certifi/issues/229\">#229</a>)</li>\n<li><a href=\"https://github.com/certifi/python-certifi/commit/44df761f4c09d19f32b3cc09208a739043a5e25b\"><code>44df761</code></a> Hash pin Actions and enable dependabot (<a href=\"https://redirect.github.com/certifi/python-certifi/issues/228\">#228</a>)</li>\n<li>See full diff in <a href=\"https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22\">compare view</a></li>\n</ul>\n</details>\n<br />\n\n\n[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=certifi&package-manager=pip&previous-version=2023.5.7&new-version=2023.7.22)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)\n\nYou can trigger a rebase of this PR by commenting `@dependabot rebase`.\n\n[//]: # (dependabot-automerge-start)\n[//]: # (dependabot-automerge-end)\n\n---\n\n<details>\n<summary>Dependabot commands and options</summary>\n<br />\n\nYou can trigger Dependabot actions by commenting on this PR:\n- `@dependabot rebase` will rebase this PR\n- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it\n- `@dependabot merge` will merge this PR after your CI passes on it\n- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it\n- `@dependabot cancel merge` will cancel a previously requested merge and block automerging\n- `@dependabot reopen` will reopen this PR if it is closed\n- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually\n- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)\nYou can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts).\n\n</details>\n\n> **Note**\n> Automatic rebases have been disabled on this pull request as it has been open for over 30 days.\n\n___\n\n</details>\n\n\n\n\n## [3.16.7](https://github.com/ynput/OpenPype/tree/3.16.7)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.6...3.16.7)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Maya: Extract active view as thumbnail when no thumbnail set <a href=\"https://github.com/ynput/OpenPype/pull/5426\">#5426</a></summary>\n\nThis sets the Maya instance's thumbnail to the current active view if no thumbnail was set yet.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Implement USD publish and load using native `mayaUsdPlugin` <a href=\"https://github.com/ynput/OpenPype/pull/5573\">#5573</a></summary>\n\nImplement Creator and Loaders for extraction and loading of USD files using Maya's own `mayaUsdPlugin`.Also adds support to load a `usd` file into an Arnold Standin (`aiStandin`) and assigning looks to it.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Ignore separated modules <a href=\"https://github.com/ynput/OpenPype/pull/5619\">#5619</a></summary>\n\nDo not load already separated modules from default directory.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: Reduce amount of code for Collect Looks <a href=\"https://github.com/ynput/OpenPype/pull/5253\">#5253</a></summary>\n\n- Refactor `get_file_node_files` because popping from `paths` by index should have been done in reversed order anyway. It's now changed to not need popping at all.\n- Removed unused `RENDERER_NODE_TYPES` and if-branch which collected `node_attrs` list which was unused + collected members which was also done outside of the if branch and thus generated no extra data.\n- Collected all materials from look set attributes at once instead of multiple queries\n- Collected all file nodes in history from a single query instead of per type\n- Restructured assignment of `instance.data[\"resources\"]` to be more readable\n- Cached `PXR_NODES` only ones (Note: plugin load is checked on discovery of the collect look plugin) instead of querying plugin load and its nodes per file node per attribute\n- Removed some debug logs or combined some messages\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Mark deprecated settings in Maya <a href=\"https://github.com/ynput/OpenPype/pull/5627\">#5627</a></summary>\n\nAdded deprecated info to docstrings of maya colormanagement settings.Resolves: https://github.com/ynput/OpenPype/issues/5556\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: switching versions of maxScene maintain parentage/links with the loaders <a href=\"https://github.com/ynput/OpenPype/pull/5424\">#5424</a></summary>\n\nWhen using scene inventory to manage or update the version of the loading objects, the linked modifiers or parentage of the objects would be kept.Meanwhile, loaded objects from all loaders no longer parented to the container with OP Data.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>3ds max: small tweaks to obj extractor and model publishing flow <a href=\"https://github.com/ynput/OpenPype/pull/5605\">#5605</a></summary>\n\nThere migh be situation where OBJ Extractor passes without failure, but no obj file is produced. This is adding simple check directly into the extractor to catch it earlier then in the integration phase. Also switched `Validate USD Plugin` to optional, because it was always run no matter if the Extract USD was enabled or not, hindering testing (and publishing).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TVPaint: Plugin can be reopened <a href=\"https://github.com/ynput/OpenPype/pull/5610\">#5610</a></summary>\n\nTVPaint plugin can be reopened.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Remove context prompt <a href=\"https://github.com/ynput/OpenPype/pull/5632\">#5632</a></summary>\n\nMore of a plea than a PR, but could we please remove the context prompt in Maya when switching tasks?\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Create a desktop icon is checked <a href=\"https://github.com/ynput/OpenPype/pull/5636\">#5636</a></summary>\n\nIn OP Installer `Create a desktop icon` is checked by default. \n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Extract look is not AYON compatible - OP-5375 <a href=\"https://github.com/ynput/OpenPype/pull/5341\">#5341</a></summary>\n\nThe textures that would use hardlinking are going through texture processors. Currently all texture processors are hardcoded to copy texture instead of respecting the settings of forcing to copy.The texture processors were last modified 4 months ago, so effectively all clients that are on any pipeline updated in the last 4 months wont be utilizing hardlinking at all, since the hardcoded texture processors will copy texture no matter the OS.This opts for completely disabling the hardlinking feature, while we figure out what to do about it.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Multiverse USD Override inherit from correct new style creator <a href=\"https://github.com/ynput/OpenPype/pull/5566\">#5566</a></summary>\n\nFix Creator for Multiverse USD Override by inheriting from correct new style creator class type\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Bug Fix  Alembic Loaders with Ornatrix <a href=\"https://github.com/ynput/OpenPype/pull/5434\">#5434</a></summary>\n\nBugfix the alembic loader with both ornatrix alembic and max alembic supportsAdd the ornatrix alembic loaders for loading the alembic with Ornatrix-related modifiers.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Avoid creation of duplicated links <a href=\"https://github.com/ynput/OpenPype/pull/5593\">#5593</a></summary>\n\nHandle cases when an existing link should be recreated and do not create the same link multitple times during single publishing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Extract Review: Multilayer specification for ffmpeg <a href=\"https://github.com/ynput/OpenPype/pull/5613\">#5613</a></summary>\n\nExtract review is specifying layer name when exr is multilayer.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fussion: added support for Fusion 17 <a href=\"https://github.com/ynput/OpenPype/pull/5614\">#5614</a></summary>\n\nFusion 17 still uses Python 3.6 which causes issues with some our delivered libraries. Vendorized necessary set for Python 3.6\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Fix screenshot widget <a href=\"https://github.com/ynput/OpenPype/pull/5615\">#5615</a></summary>\n\nUse correct super method name.EDITED:Removed fade animation which is not triggered at some cases, e.g. in Nuke the animation does not start. I do expect that is caused by `exec_` on the dialog, which blocks event processing to the animation, even when I've added the window as parent it still didn't trigger registered callback.Modified how the \"empty\" space is not filled by using paths instead of clear mode on painter. Added render hints to add antialiasing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Photoshop: auto_images without alpha will not fail <a href=\"https://github.com/ynput/OpenPype/pull/5620\">#5620</a></summary>\n\nExtractReview caused issue on `auto_image` instance without alpha channel, this fixes it.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fix - _id key used instead of id in get_last_version_by_subset_name <a href=\"https://github.com/ynput/OpenPype/pull/5626\">#5626</a></summary>\n\nJust 'id' is not returned because value in fields. Caused KeyError.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: create symlinks for ssl libs on Centos 7 <a href=\"https://github.com/ynput/OpenPype/pull/5633\">#5633</a></summary>\n\nDocker build was missing `libssl.1.1.so` and `libcrypto.1.1.so` symlinks needed by the executable itself, because Python is now explicitly built with OpenSSL 1.1.1\n\n\n___\n\n</details>\n\n### **📃 Documentation**\n\n\n<details>\n<summary>Documentation/local settings <a href=\"https://github.com/ynput/OpenPype/pull/5102\">#5102</a></summary>\n\nI completed the \"Working with local settings\" page. I updated the screenshot, wrote an explanation for each empty category, and if available, linked the more detailed pages already existing. I also added the \"Environments\" category.\n\n\n___\n\n</details>\n\n\n\n\n## [3.16.6](https://github.com/ynput/OpenPype/tree/3.16.6)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.5...3.16.6)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Workfiles tool: Refactor workfiles tool (for AYON) <a href=\"https://github.com/ynput/OpenPype/pull/5550\">#5550</a></summary>\n\nRefactored workfiles tool to new tool. Separated backend and frontend logic. Refactored logic is AYON-centric and is used only in AYON mode, so it does not affect OpenPype.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AfterEffects: added validator for missing files in FootageItems <a href=\"https://github.com/ynput/OpenPype/pull/5590\">#5590</a></summary>\n\nPublished composition in AE could contain multiple FootageItems as a layers. If FootageItem contains imported file and it doesn't exist, render triggered by Publish process will silently fail and no output is generated. This could cause failure later in the process with unclear reason. (In `ExtractReview`).This PR adds validation to protect from this.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: Yeti Cache Include viewport preview settings from source <a href=\"https://github.com/ynput/OpenPype/pull/5561\">#5561</a></summary>\n\nWhen publishing and loading yeti caches persist the display output and preview colors + settings to ensure consistency in the view\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: validate colorspace in review rop <a href=\"https://github.com/ynput/OpenPype/pull/5322\">#5322</a></summary>\n\nAdding a validator that checks if 'OCIO Colorspace' parameter on  review rop was set to a valid value.It is a step towards managing colorspace in review ropvalid values are the ones in the dropdown menuthis validator also provides some helper actions This PR is related to #4836 and #4833\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Colorspace: adding abstraction of publishing related functions <a href=\"https://github.com/ynput/OpenPype/pull/5497\">#5497</a></summary>\n\nThe functionality of Colorspace has been abstracted for greater usability.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: removing redundant workfile colorspace attributes <a href=\"https://github.com/ynput/OpenPype/pull/5580\">#5580</a></summary>\n\nNuke root workfile colorspace data type knobs are long time configured automatically via config roles or the default values are also working well. Therefore there is no need for pipeline managed knobs.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Less verbose logs for Ftrack integration in artist facing logs <a href=\"https://github.com/ynput/OpenPype/pull/5596\">#5596</a></summary>\n\n- Reduce artist-facing logs for component integration for Ftrack\n- Avoid \"Comment is not set\" log in artist facing report for Kitsu and Ftrack\n- Remove info log about `ffprobe` inspecting a file (changed to debug log)\n- interesting to see however that it ffprobes the same jpeg twice - but maybe once for thumbnail?\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Fix rig validators for new out_SET and controls_SET names  <a href=\"https://github.com/ynput/OpenPype/pull/5595\">#5595</a></summary>\n\nFix usage of `out_SET` and `controls_SET` since #5310 because they can now be prefixed by the subset name.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TrayPublisher: set default frame values to sequential data <a href=\"https://github.com/ynput/OpenPype/pull/5530\">#5530</a></summary>\n\nWe are inheriting  default frame handles and fps data either from project or setting them to 0. This is just for case a production will decide not to injest the sequential representations with asset based metadata.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Screenshot opacity value fix <a href=\"https://github.com/ynput/OpenPype/pull/5576\">#5576</a></summary>\n\nFix opacity value.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AfterEffects: fix imports of image sequences <a href=\"https://github.com/ynput/OpenPype/pull/5581\">#5581</a></summary>\n\n#4602 broke imports of image sequences.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Fix representation context conversion <a href=\"https://github.com/ynput/OpenPype/pull/5591\">#5591</a></summary>\n\nDo not fix `\"folder\"` key in representation context until it is needed.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>ayon-nuke: default factory to lists  <a href=\"https://github.com/ynput/OpenPype/pull/5594\">#5594</a></summary>\n\nDefault factory were missing in settings schemas for complicated objects like lists and it was causing settings to be failing saving.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix look assigner showing no asset if 'not found' representations are present <a href=\"https://github.com/ynput/OpenPype/pull/5597\">#5597</a></summary>\n\nFix Maya Look assigner failing to show any content if it finds an invalid container for which it can't find the asset in the current project. (This can happen when e.g. loading something from a library project).There was logic already to avoid this but there was a bug where it used variable `_id` which did not exist and likely had to be `asset_id`.I've fixed that and improved the logged message a bit, e.g.:\n```\n// Warning: openpype.hosts.maya.tools.mayalookassigner.commands : Id found on 22 nodes for which no asset is found database, skipping '641d78ec85c3c5b102e836b0'\n```\nExample not found representation in Loader:The issue isn't necessarily related to NOT FOUND representations but in essence boils down to finding nodes with asset ids that do not exist in the current project which could very well just be local meshes in your scene.**Note:**I've excluded logging the nodes themselves because that tends to be a very long list of nodes. Only downside to removing that is that it's unclear which nodes are related to that `id`. If there are any ideas on how to still provide a concise informational message about that that'd be great so I could add it. Things I had considered:\n- Report the containers, issue here is that it's about asset ids on nodes which don't HAVE to be in containers - it could be local geometry\n- Report the namespaces, issue here is that it could be nodes without namespaces (plus potentially not about ALL nodes in a namespace)\n- Report the short names of the nodes; it's shorter and readable but still likely a lot of nodes.@tokejepsen @LiborBatek any other ideas?\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Photoshop: fixed blank Flatten image <a href=\"https://github.com/ynput/OpenPype/pull/5600\">#5600</a></summary>\n\nFlatten image is simplified publishing approach where all visible layers are \"flatten\" and published together. This image could be used as a reference etc.This is implemented by auto creator which wasn't updated after first publish. This would result in missing newly created layers after `auto_image` instance was created.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Remove Hardcoded Subset Name for Reviews <a href=\"https://github.com/ynput/OpenPype/pull/5603\">#5603</a></summary>\n\nFixes hardcoded subset name for Reviews in Blender.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TVPaint: Fix tool callbacks <a href=\"https://github.com/ynput/OpenPype/pull/5608\">#5608</a></summary>\n\nDo not wait for callback to finish.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Chore: Remove unused variables and cleanup <a href=\"https://github.com/ynput/OpenPype/pull/5588\">#5588</a></summary>\n\nRemoving some unused variables. In some cases the unused variables _seemed like they should've been used - maybe?_ so please **double check the code whether it doesn't hint to an already existing bug**.Also tweaked some other small bugs in code + tweaked logging levels.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Chore: Loader log deprecation warning for 'fname' attribute <a href=\"https://github.com/ynput/OpenPype/pull/5587\">#5587</a></summary>\n\nSince https://github.com/ynput/OpenPype/pull/4602 the `fname` attribute on the `LoaderPlugin` should've been deprecated and set for removal over time. However, no deprecation warning was logged whatsoever and thus one usage appears to have sneaked in (fixed with this PR) and a new one tried to sneak in with a recent PR\n\n\n___\n\n</details>\n\n\n\n\n## [3.16.5](https://github.com/ynput/OpenPype/tree/3.16.5)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.4...3.16.5)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Attribute Definitions: Multiselection enum def <a href=\"https://github.com/ynput/OpenPype/pull/5547\">#5547</a></summary>\n\nAdded `multiselection` option to `EnumDef`.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Farm: adding target collector <a href=\"https://github.com/ynput/OpenPype/pull/5494\">#5494</a></summary>\n\nEnhancing farm publishing workflow.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Optimize validate plug-in path attributes <a href=\"https://github.com/ynput/OpenPype/pull/5522\">#5522</a></summary>\n\n- Optimize query (use `cmds.ls` once)\n- Add Select Invalid action\n- Improve validation report\n- Avoid \"Unknown object type\" errors\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Remove Validate Instance Attributes plug-in <a href=\"https://github.com/ynput/OpenPype/pull/5525\">#5525</a></summary>\n\nRemove Validate Instance Attributes plug-in.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Tweak logging for artist facing reports <a href=\"https://github.com/ynput/OpenPype/pull/5537\">#5537</a></summary>\n\nTweak the logging of publishing for global, deadline, maya and a fusion plugin to have a cleaner artist-facing report.\n- Fix context being reported correctly from CollectContext\n- Fix ValidateMeshArnoldAttributes: fix when arnold is not loaded, fix applying settings, fix for when ai attributes do not exist\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Update settings <a href=\"https://github.com/ynput/OpenPype/pull/5544\">#5544</a></summary>\n\nUpdated settings in AYON addons and conversion of AYON settings in OpenPype.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Removed Ass export script <a href=\"https://github.com/ynput/OpenPype/pull/5560\">#5560</a></summary>\n\nRemoved Arnold render script, which was obsolete and unused.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Allow for knob values to be validated against multiple values. <a href=\"https://github.com/ynput/OpenPype/pull/5042\">#5042</a></summary>\n\nKnob values can now be validated against multiple values, so you can allow write nodes to be `exr` and `png`, or `16-bit` and `32-bit`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Cosmetics for Higher version of publish already exists validation error <a href=\"https://github.com/ynput/OpenPype/pull/5190\">#5190</a></summary>\n\nFix double spaces in message.Example output **after** the PR:\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: publish existing frames on farm <a href=\"https://github.com/ynput/OpenPype/pull/5409\">#5409</a></summary>\n\nThis PR proposes adding a fourth option in Nuke render publish called \"Use Existing Frames - Farm\". This would be useful when the farm is busy or when the artist lacks enough farm licenses. Additionally, some artists prefer rendering on the farm but still want to check frames before publishing.By adding the \"Use Existing Frames - Farm\" option, artists will have more flexibility and control over their render publishing process. This enhancement will streamline the workflow and improve efficiency for Nuke users.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Create project in temp location and move to final when done <a href=\"https://github.com/ynput/OpenPype/pull/5476\">#5476</a></summary>\n\nCreate Unreal project in local temporary folder and when done, move it to final destination.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TrayPublisher: adding audio product type into default presets <a href=\"https://github.com/ynput/OpenPype/pull/5489\">#5489</a></summary>\n\nAdding Audio product type into default presets so anybody can publish audio to their shots.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: avoiding cleanup of flagged representation <a href=\"https://github.com/ynput/OpenPype/pull/5502\">#5502</a></summary>\n\nPublishing folder can be flagged as persistent at representation level.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: missing tag could raise error <a href=\"https://github.com/ynput/OpenPype/pull/5511\">#5511</a></summary>\n\n- avoiding potential situation where missing Tag key could raise error\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Queued event system <a href=\"https://github.com/ynput/OpenPype/pull/5514\">#5514</a></summary>\n\nImplemented event system with more expected behavior of event system. If an event is triggered during other event callback, it is not processed immediately but waits until all callbacks of previous events are done. The event system also allows to not trigger events directly once `emit_event` is called which gives option to process events in custom loops.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Tweak log message to provide plugin name after \"Plugin\" <a href=\"https://github.com/ynput/OpenPype/pull/5521\">#5521</a></summary>\n\nFix logged message for settings automatically applied to plugin attributes\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Improve VDB Selection <a href=\"https://github.com/ynput/OpenPype/pull/5523\">#5523</a></summary>\n\nImproves VDB selection if selection is `SopNode`:   return the selected sop nodeif selection is `ObjNode`:   get the output node with the minimum 'outputidx' or the node with display flag\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Refactor/tweak Validate Instance In same Context plug-in <a href=\"https://github.com/ynput/OpenPype/pull/5526\">#5526</a></summary>\n\n- Chore/Refactor: Re-use existing select invalid and repair actions\n- Enhancement: provide more elaborate PublishValidationError report\n- Bugfix: fix \"optional\" support by using `OptionalPyblishPluginMixin` base class.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Update houdini main menu <a href=\"https://github.com/ynput/OpenPype/pull/5527\">#5527</a></summary>\n\nThis PR adds two updates:\n- dynamic main menu\n- dynamic asset name and task\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Reset FPS when clicking Set Frame Range <a href=\"https://github.com/ynput/OpenPype/pull/5528\">#5528</a></summary>\n\n_Similar to Maya,_ Make `Set Frame Range` resets FPS, issue https://github.com/ynput/OpenPype/issues/5516\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Deadline plugins optimize, cleanup and fix optional support for validate deadline pools <a href=\"https://github.com/ynput/OpenPype/pull/5531\">#5531</a></summary>\n\n- Fix optional support of validate deadline pools\n- Query deadline webservice only once per URL for verification, and once for available deadline pools instead of for every instance\n- Use `deadlineUrl` in `instance.data` when validating pools if it is set.\n- Code cleanup: Re-use existing `requests_get` implementation\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: PowerShell script for docker build <a href=\"https://github.com/ynput/OpenPype/pull/5535\">#5535</a></summary>\n\nAdded PowerShell script to run docker build.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Deadline expand userpaths in executables list <a href=\"https://github.com/ynput/OpenPype/pull/5540\">#5540</a></summary>\n\nExpande `~` paths in executables list.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Use correct git url <a href=\"https://github.com/ynput/OpenPype/pull/5542\">#5542</a></summary>\n\nFixed github url in README.md.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Create plugin does not expect system settings <a href=\"https://github.com/ynput/OpenPype/pull/5553\">#5553</a></summary>\n\nSystem settings are not passed to initialization of create plugin initialization (and `apply_settings`).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Allow custom Qt scale factor rounding policy <a href=\"https://github.com/ynput/OpenPype/pull/5555\">#5555</a></summary>\n\nDo not force `PassThrough` rounding policy if different policy is defined via env variable.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Fix outdated containers pop-up on opening last workfile on launch <a href=\"https://github.com/ynput/OpenPype/pull/5567\">#5567</a></summary>\n\nFix Houdini not showing outdated containers pop-up on scene open when launching with last workfile argument\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Improve errors e.g. raise PublishValidationError or cosmetics <a href=\"https://github.com/ynput/OpenPype/pull/5568\">#5568</a></summary>\n\nImprove errors e.g. raise PublishValidationError or cosmeticsThis also fixes the Increment Current File plug-in since due to an invalid import it was previously broken\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Code updates <a href=\"https://github.com/ynput/OpenPype/pull/5569\">#5569</a></summary>\n\nUpdate fusion code which contains obsolete code. Removed `switch_ui.py` script from fusion with related script in scripts.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Validate Shape Zero fix repair action + provide informational artist-facing report <a href=\"https://github.com/ynput/OpenPype/pull/5524\">#5524</a></summary>\n\nRefactor to PublishValidationError to allow the RepairAction to work + provide informational report message\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix attribute definitions for `CreateYetiCache` <a href=\"https://github.com/ynput/OpenPype/pull/5574\">#5574</a></summary>\n\nFix attribute definitions for `CreateYetiCache`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Optional Renderable Camera Validator for Render Instance <a href=\"https://github.com/ynput/OpenPype/pull/5286\">#5286</a></summary>\n\nOptional validation to check on renderable camera being set up correctly for deadline submission.If not being set up correctly, it wont pass the validation and user can perform repair actions.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Adding custom modifiers back to the loaded objects  <a href=\"https://github.com/ynput/OpenPype/pull/5378\">#5378</a></summary>\n\nThe custom parameters OpenpypeData doesn't show in the loaded container when it is being loaded through the loader.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Use default_variant to Houdini Node TAB Creator <a href=\"https://github.com/ynput/OpenPype/pull/5421\">#5421</a></summary>\n\nUse the default variant of the creator plugins on the interactive creator from the TAB node search instead of hard-coding it to `Main`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: adding inherited colorspace from instance <a href=\"https://github.com/ynput/OpenPype/pull/5454\">#5454</a></summary>\n\nThumbnails are extracted with inherited colorspace collected from rendering write node.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Add kitsu credentials to deadline publish job  <a href=\"https://github.com/ynput/OpenPype/pull/5455\">#5455</a></summary>\n\nThis PR hopefully fixes this issue #5440\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Fill entities during editorial <a href=\"https://github.com/ynput/OpenPype/pull/5475\">#5475</a></summary>\n\nFill entities and update template data on instances during extract AYON hierarchy.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Fix version 0 when integrating to Ftrack - OP-6595 <a href=\"https://github.com/ynput/OpenPype/pull/5477\">#5477</a></summary>\n\nFix publishing version 0 to Ftrack.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>OCIO: windows unc path support in Nuke and Hiero <a href=\"https://github.com/ynput/OpenPype/pull/5479\">#5479</a></summary>\n\nHiero and Nuke is not supporting windows unc path formatting in OCIO environment variable.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: Added super call to init <a href=\"https://github.com/ynput/OpenPype/pull/5480\">#5480</a></summary>\n\nDL 10.3 requires plugin inheriting from DeadlinePlugin to call super's **init** explicitly.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: fixing thumbnail and monitor out root attributes <a href=\"https://github.com/ynput/OpenPype/pull/5483\">#5483</a></summary>\n\nNuke Root Colorspace settings for Thumbnail and Monitor Out schema was gradually changed between version 12, 13, 14 and we needed to address those changes individually for particular version.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: fixing missing `instance_id` error <a href=\"https://github.com/ynput/OpenPype/pull/5484\">#5484</a></summary>\n\nWorkfiles with Instances created in old publisher workflow were rising error during converting method since they were missing `instance_id` key introduced in new publisher workflow.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: existing frames validator is repairing render target <a href=\"https://github.com/ynput/OpenPype/pull/5486\">#5486</a></summary>\n\nNuke is now correctly repairing render target after the existing frames validator finds missing frames and repair action is used.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>added UE to extract burnins families <a href=\"https://github.com/ynput/OpenPype/pull/5487\">#5487</a></summary>\n\nThis PR fixes missing burnins in reviewables when rendering from UE.\n___\n\n</details>\n\n\n<details>\n<summary>Harmony: refresh code for current Deadline <a href=\"https://github.com/ynput/OpenPype/pull/5493\">#5493</a></summary>\n\n- Added support in Deadline Plug-in for new versions of Harmony, in particular version 21 and 22.\n- Remove review=False flag on render instance\n- Add farm=True flag on render instance\n- Fix is_in_tests function call in Harmony Deadline submission plugin\n- Force HarmonyOpenPype.py Deadline Python plug-in to py3\n- Fix cosmetics/hound in HarmonyOpenPype.py Deadline Python plug-in\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Fix multiselection value <a href=\"https://github.com/ynput/OpenPype/pull/5505\">#5505</a></summary>\n\nSelection of multiple instances in Publisher does not cause that all instances change all publish attributes to the same value.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Avoid warnings on thumbnails if source image also has alpha channel <a href=\"https://github.com/ynput/OpenPype/pull/5510\">#5510</a></summary>\n\nAvoids the following warning from `ExtractThumbnailFromSource`:\n```\n// pyblish.ExtractThumbnailFromSource : oiiotool WARNING: -o : Can't save 4 channels to jpeg... saving only R,G,B\n```\n\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Update ayon-python-api <a href=\"https://github.com/ynput/OpenPype/pull/5512\">#5512</a></summary>\n\nUpdate ayon python api and related callbacks.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Fixing the bug of falling back to use workfile for Arnold or any renderers except Redshift <a href=\"https://github.com/ynput/OpenPype/pull/5520\">#5520</a></summary>\n\nFix the bug of falling back to use workfile for Arnold\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Fix Validate Publish Dir Validator <a href=\"https://github.com/ynput/OpenPype/pull/5534\">#5534</a></summary>\n\nNonsensical \"family\" key was used instead of real value (as 'render' etc.) which would result in wrong translation of intermediate family names.Updated docstring.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>have the addons loading respect a custom AYON_ADDONS_DIR <a href=\"https://github.com/ynput/OpenPype/pull/5539\">#5539</a></summary>\n\nWhen using a custom AYON_ADDONS_DIR environment variable that variable is used in the launcher correctly and downloads and extracts addons to there, however when running Ayon does not respect this environment variable\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: files on representation cannot be single item list <a href=\"https://github.com/ynput/OpenPype/pull/5545\">#5545</a></summary>\n\nFurther logic expects that single item files will be only 'string' not 'list' (eg. repre[\"files\"] = \"abc.exr\" not repre[\"files\"] = [\"abc.exr\"].This would cause an issue in ExtractReview later.This could happen if DL rendered single frame file with different frame value.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Webpublisher: better encode list values for click <a href=\"https://github.com/ynput/OpenPype/pull/5546\">#5546</a></summary>\n\nTargets could be a list, original implementation pushed it as a separate items, it must be added as `--targets webpulish --targets filepublish`.`wepublish_routes` handles triggering from UI, changes in `publish_functions` handle triggering from cmd (for tests, api access).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Introduce imprint function for correct version in hda loader <a href=\"https://github.com/ynput/OpenPype/pull/5548\">#5548</a></summary>\n\nResolve #5478\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Fill entities during editorial (2) <a href=\"https://github.com/ynput/OpenPype/pull/5549\">#5549</a></summary>\n\nFix changes made in https://github.com/ynput/OpenPype/pull/5475.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: OP Data updates in Loaders <a href=\"https://github.com/ynput/OpenPype/pull/5563\">#5563</a></summary>\n\nFix the bug on the loaders not being able to load the objects when iterating key and values with the dict.Max prefers list over the list in dict.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Create Plugins: Better check of overriden '__init__' method <a href=\"https://github.com/ynput/OpenPype/pull/5571\">#5571</a></summary>\n\nCreate plugins do not log warning messages about each create plugin because of wrong `__init__` method check.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Tests: fix unit tests <a href=\"https://github.com/ynput/OpenPype/pull/5533\">#5533</a></summary>\n\nFixed failing tests.Updated Unreal's validator to match removed general one which had a couple of issues fixed.\n\n\n___\n\n</details>\n\n\n\n\n## [3.16.4](https://github.com/ynput/OpenPype/tree/3.16.4)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.3...3.16.4)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Feature: Download last published workfile specify version <a href=\"https://github.com/ynput/OpenPype/pull/4998\">#4998</a></summary>\n\nSetting `workfile_version` key to hook's `self.launch_context.data` allow you to specify the workfile version you want sync service to download if none is matched locally. This is helpful if the last version hasn't been correctly published/synchronized, and you want to recover the previous one (or some you'd like).Version could be set in two ways:\n- OP's absolute version, matching the `version` index in DB.\n- Relative version in reverse order from the last one: `-2`, `-3`...I don't know where I should write documentation about that.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: allow not creation of group for Import loaders <a href=\"https://github.com/ynput/OpenPype/pull/5427\">#5427</a></summary>\n\nThis PR enhances previous one. All ReferenceLoaders could not wrap imported products into explicit group.Also `Import` Loaders have same options. Control for this is separate in Settings, eg. Reference might wrap loaded items in group, `Import` might not.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>3dsMax: Settings for Ayon <a href=\"https://github.com/ynput/OpenPype/pull/5388\">#5388</a></summary>\n\nMax Addon Setting for Ayon\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Navigation to Folder from Launcher <a href=\"https://github.com/ynput/OpenPype/pull/5404\">#5404</a></summary>\n\nAdds an action in launcher to open the directory of the asset.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Default variant in create plugin  <a href=\"https://github.com/ynput/OpenPype/pull/5429\">#5429</a></summary>\n\nAttribute `default_variant` on create plugins always returns string and if default variant is not filled other ways how to get one are implemented.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Thumbnail widget enhancements <a href=\"https://github.com/ynput/OpenPype/pull/5439\">#5439</a></summary>\n\nThumbnails widget in Publisher has new 3 options to choose from: Paste (from clipboard), Take screenshot and Browse. Clear button and new options are not visible by default, user must expand options button to show them.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Update ayon api to '0.3.5' <a href=\"https://github.com/ynput/OpenPype/pull/5460\">#5460</a></summary>\n\nUpdated ayon-python-api to 0.3.5.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>AYON: Apply unknown ayon settings first <a href=\"https://github.com/ynput/OpenPype/pull/5435\">#5435</a></summary>\n\nSettings of custom addons are available in converted settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix wrong subset name of render family in deadline <a href=\"https://github.com/ynput/OpenPype/pull/5442\">#5442</a></summary>\n\nNew Publisher is creating different subset names than previously which resulted in duplication of `render` string in final subset name of `render` family published on Deadline.This PR solves that, it also fixes issues with legacy instances from old publisher, it matches the subset name as was before.This solves same issue in Max implementation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix setting of version to workfile instance <a href=\"https://github.com/ynput/OpenPype/pull/5452\">#5452</a></summary>\n\nIf there are multiple instances of renderlayer published, previous logic resulted in unpredictable rewrite of instance family to 'workfile' if `Sync render version with workfile` was on.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Context plugin shouldn't be tied to family <a href=\"https://github.com/ynput/OpenPype/pull/5464\">#5464</a></summary>\n\n`Maya Current File` collector was tied to `workfile` unnecessary. It should run even if `workile` instance is not being published.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix loading hero version for static and skeletal meshes <a href=\"https://github.com/ynput/OpenPype/pull/5393\">#5393</a></summary>\n\nFixed a problem with loading hero versions for static ans skeletal meshes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TVPaint: Fix 'repeat' behavior <a href=\"https://github.com/ynput/OpenPype/pull/5412\">#5412</a></summary>\n\nCalculation of frames for repeat behavior is working correctly.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Thumbnails cache and api prep <a href=\"https://github.com/ynput/OpenPype/pull/5437\">#5437</a></summary>\n\nMoved thumbnails cache from ayon python api to OpenPype and prepare AYON thumbnail resolver for new api functions. Current implementation should work with old and new ayon-python-api.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Name of the Read Node should be updated correctly when switching versions or assets. <a href=\"https://github.com/ynput/OpenPype/pull/5444\">#5444</a></summary>\n\nBug fixing of the read node's name not being updated correctly when setting version or switching asset.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Farm publishing: asymmetric handles fixed <a href=\"https://github.com/ynput/OpenPype/pull/5446\">#5446</a></summary>\n\nHandles are now set correctly on farm published product version if asymmetric were set to shot attributes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Scene Inventory: Provider icons fix <a href=\"https://github.com/ynput/OpenPype/pull/5450\">#5450</a></summary>\n\nFix how provider icons are accessed in scene inventory.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fix typo on Deadline OP plugin name <a href=\"https://github.com/ynput/OpenPype/pull/5453\">#5453</a></summary>\n\nSurprised that no one has hit this bug yet... but it seems like there was a typo on the name of the OP Deadline plugin when submitting jobs to it.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Fix version attributes update <a href=\"https://github.com/ynput/OpenPype/pull/5472\">#5472</a></summary>\n\nFixed updates of attribs in AYON mode.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Added missing defaults for import_loader <a href=\"https://github.com/ynput/OpenPype/pull/5447\">#5447</a></summary>\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bug: Local settings don't open on 3.14.7  <a href=\"https://github.com/ynput/OpenPype/pull/5220\">#5220</a></summary>\n\n### Before posting a new ticket, have you looked through the documentation to find an answer?\n\nYes I have\n\n### Have you looked through the existing tickets to find any related issues ?\n\nNot yet\n\n### Author of the bug\n\n@FadyFS\n\n### Version\n\n3.15.11-nightly.3\n\n### What platform you are running OpenPype on?\n\nLinux / Centos\n\n### Current Behavior:\n\nthe previous behavior (bug) : \n![image](https://github.com/quadproduction/OpenPype/assets/135602303/09bff9d5-3f8b-4339-a1e5-30c04ade828c)\n\n\n### Expected Behavior:\n\n![image](https://github.com/quadproduction/OpenPype/assets/135602303/c505a103-7965-4796-bcdf-73bcc48a469b)\n\n\n### What type of bug is it ?\n\nHappened only once in a particular configuration\n\n### Which project / workfile / asset / ...\n\nopen settings with 3.14.7 \n\n### Steps To Reproduce:\n\n1. Run openpype on the 3.15.11-nightly.3 version \n2. Open settings in 3.14.7 version\n\n### Relevant log output:\n\n_No response_\n\n### Additional context:\n\n_No response_\n\n___\n\n</details>\n\n\n<details>\n<summary>Tests: Add automated targets for tests <a href=\"https://github.com/ynput/OpenPype/pull/5443\">#5443</a></summary>\n\nWithout it plugins with 'automated' targets won't be triggered (eg `CloseAE` etc.)\n\n\n___\n\n</details>\n\n\n\n\n## [3.16.3](https://github.com/ynput/OpenPype/tree/3.16.3)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.2...3.16.3)\n\n### **🆕 New features**\n\n\n<details>\n<summary>AYON: 3rd party addon usage <a href=\"https://github.com/ynput/OpenPype/pull/5300\">#5300</a></summary>\n\nPrepare OpenPype code to be able use `ayon-third-party` addon which supply ffmpeg and OpenImageIO executables. Because they both can support to define custom arguments (more than one) a new functions were needed to supply.New functions are `get_ffmpeg_tool_args` and `get_oiio_tool_args`. They work similar to previous but instead of string are returning list of strings. All places using previous functions `get_ffmpeg_tool_path` and `get_oiio_tool_path` are now using new ones. They should be backwards compatible and even with addon if returns single argument.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Addon settings in OpenPype <a href=\"https://github.com/ynput/OpenPype/pull/5347\">#5347</a></summary>\n\nMoved settings addons to OpenPype server addon. Modified create package to create zip files for server for each settings addon and for openpype addon.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Add folder to template data <a href=\"https://github.com/ynput/OpenPype/pull/5417\">#5417</a></summary>\n\nAdded `folder` to template data, so `{folder[name]}` can be used in templates.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Option to start versioning from 0 <a href=\"https://github.com/ynput/OpenPype/pull/5262\">#5262</a></summary>\n\nThis PR adds a settings option to start all versioning from 0.This PR will replace #4455.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ayon: deadline implementation <a href=\"https://github.com/ynput/OpenPype/pull/5321\">#5321</a></summary>\n\nQuick implementation of deadline in Ayon. New Ayon plugin added for Deadline repository\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Remove AYON launch logic from OpenPype <a href=\"https://github.com/ynput/OpenPype/pull/5348\">#5348</a></summary>\n\nRemoved AYON launch logic from OpenPype. The logic is outdated at this moment and is replaced by `ayon-launcher`.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Bug: Error on multiple instance rig with maya <a href=\"https://github.com/ynput/OpenPype/pull/5310\">#5310</a></summary>\n\nI change endswith method by startswith method because the set are automacaly name out_SET, out_SET1, out_SET2 ...\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Applications: Use prelaunch hooks to extract environments <a href=\"https://github.com/ynput/OpenPype/pull/5387\">#5387</a></summary>\n\nEnvironment variable preparation is based on prelaunch hooks. This should allow to pass OCIO environment variables to farm jobs.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Applications: Launch hooks cleanup <a href=\"https://github.com/ynput/OpenPype/pull/5395\">#5395</a></summary>\n\nUse `set` instead of `list` for filtering attributes in launch hooks. Celaction hooks dir does not contain `__init__.py`. Celaction prelaunch hook is reusing `CELACTION_ROOT_DIR`. Launch hooks are using full import from `openpype.lib.applications`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Applications: Environment variables order <a href=\"https://github.com/ynput/OpenPype/pull/5245\">#5245</a></summary>\n\nChanged order of set environment variables. First are set context environment variables and then project environment overrides. Also asset and task environemnt variables are optional.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Autosave preferences can be read after Nuke opens the script <a href=\"https://github.com/ynput/OpenPype/pull/5295\">#5295</a></summary>\n\nLooks like I need to open the script in Nuke to be able to correctly load the autosave preferences.This PR reads the Nuke script in context, and offers owerwriting the current script with autosaved one if autosave exists.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Resolve: Update with compatible resolve version and latest docs <a href=\"https://github.com/ynput/OpenPype/pull/5317\">#5317</a></summary>\n\nMissing information about compatible Resolve version and latest docs from https://github.com/ynput/OpenPype/tree/develop/openpype/hosts/resolve\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Remove deprecated functions <a href=\"https://github.com/ynput/OpenPype/pull/5323\">#5323</a></summary>\n\nRemoved functions/classes that are deprecated and marked to be removed.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke Render and Prerender nodes Process Order - OP-3555 <a href=\"https://github.com/ynput/OpenPype/pull/5332\">#5332</a></summary>\n\nThis PR exposes control over the order of processing of the instances, by sorting the instances created. The sorting happens on the `render_order` and subset name. If the knob `render_order` is found on the instance, we'll sort by that first before sorting by subset name.`render_order` instances are processed before nodes without `render_order`. This could be extended in the future by querying other knobs but I dont know of a usecase for this.Hardcoded the creator `order` attribute of the `prerender` class to be before the `render`. Could be exposed to the user/studio but dont know of a use case for this.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Python Environment Improvements <a href=\"https://github.com/ynput/OpenPype/pull/5344\">#5344</a></summary>\n\nAutomatically set `UE_PYTHONPATH` as `PYTHONPATH` when launching Unreal.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Custom location for Unreal Ayon Plugin <a href=\"https://github.com/ynput/OpenPype/pull/5346\">#5346</a></summary>\n\nAdded a new environment variable `AYON_BUILT_UNREAL_PLUGIN` to set an already existing and built Ayon Plugin for Unreal.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Better handling of Exceptions in UE Worker threads <a href=\"https://github.com/ynput/OpenPype/pull/5349\">#5349</a></summary>\n\nImplemented a new `UEWorker` base class to handle exception during the execution of UE Workers.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Add farm toggle on creation menu <a href=\"https://github.com/ynput/OpenPype/pull/5350\">#5350</a></summary>\n\nDeadline Farm publishing and Rendering for Houdini was possible with this PR #4825 farm publishing is enabled by default some ROP nodes which may surprise new users (like me).I think adding a toggle (on by default) on creation UI is better so that users will be aware that there's a farm option for this publish instance.ROPs Modified :\n- [x] Mantra ROP\n- [x] Karma ROP\n- [x] Arnold ROP\n- [x] Redshift ROP\n- [x] Vray ROP\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Sync to avalon settings <a href=\"https://github.com/ynput/OpenPype/pull/5353\">#5353</a></summary>\n\nAdded roles settings for sync to avalon action.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Schemas inside OpenPype <a href=\"https://github.com/ynput/OpenPype/pull/5354\">#5354</a></summary>\n\nMoved/copied schemas from repository root inside openpype/pipeline.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Addons creation enhancements <a href=\"https://github.com/ynput/OpenPype/pull/5356\">#5356</a></summary>\n\nEnhanced AYON addons creation. Fix issue with `Pattern` typehint. Zip filenames contain version. OpenPype package is skipping modules that are already separated in AYON. Updated settings of addons.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Update staging icons <a href=\"https://github.com/ynput/OpenPype/pull/5372\">#5372</a></summary>\n\nUpdated staging icons for staging mode.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Houdini Update pointcache labels <a href=\"https://github.com/ynput/OpenPype/pull/5373\">#5373</a></summary>\n\nTo me it's logical to find pointcaches types listed one after another, but they were named differentlySo, I made this PR to update their labels\n\n\n___\n\n</details>\n\n\n<details>\n<summary>nuke: split write node product instance features <a href=\"https://github.com/ynput/OpenPype/pull/5389\">#5389</a></summary>\n\nImproving Write node product instances by allowing precise activation of specific features.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Use the empty modifiers in container to store AYON Parameter <a href=\"https://github.com/ynput/OpenPype/pull/5396\">#5396</a></summary>\n\nInstead of adding AYON/OP Parameter along with other attributes inside the container, empty modifiers would be created to store AYON/OP custom attributes\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AfterEffects: Removed unused imports <a href=\"https://github.com/ynput/OpenPype/pull/5397\">#5397</a></summary>\n\nRemoved unused import from extract local render plugin file.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: adding BBox knob type to settings <a href=\"https://github.com/ynput/OpenPype/pull/5405\">#5405</a></summary>\n\nNuke knob types in settings having new `Box` type for reposition nodes like Crop or Reformat.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>SyncServer: Existence of module is optional <a href=\"https://github.com/ynput/OpenPype/pull/5413\">#5413</a></summary>\n\nExistence of SyncServer module is optional and not required. Added `sync_server` module back to ignored modules when openpype addon is created for AYON. Command `syncserver` is marked as deprecated and redirected to sync server cli.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Webpublisher: Self contain test publish logic <a href=\"https://github.com/ynput/OpenPype/pull/5414\">#5414</a></summary>\n\nMoved test logic of publishing to webpublisher. Simplified `remote_publish` to remove webpublisher specific logic.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Webpublisher: Cleanup targets <a href=\"https://github.com/ynput/OpenPype/pull/5418\">#5418</a></summary>\n\nRemoved `remote` target from webpublisher and replaced it with 2 targets `webpublisher` and `automated`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>nuke: update server addon settings with box <a href=\"https://github.com/ynput/OpenPype/pull/5419\">#5419</a></summary>\n\nupdtaing nuke ayon server settings for Box option in knob types.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: fix validate frame range on review attached to other instances <a href=\"https://github.com/ynput/OpenPype/pull/5296\">#5296</a></summary>\n\nFixes situation where frame range validator can't be turned off on models if they are attached to reviewable camera in Maya.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Apply project settings to creators <a href=\"https://github.com/ynput/OpenPype/pull/5303\">#5303</a></summary>\n\nProject settings were not applied to the creators.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Validate Model Content <a href=\"https://github.com/ynput/OpenPype/pull/5336\">#5336</a></summary>\n\n`assemblies` in `cmds.ls` does not seem to work;\n```python\n\nfrom maya import cmds\n\n\ncontent_instance = ['|group2|pSphere1_GEO', '|group2|pSphere1_GEO|pSphere1_GEOShape', '|group1|pSphere1_GEO', '|group1|pSphere1_GEO|pSphere1_GEOShape']\nassemblies = cmds.ls(content_instance, assemblies=True, long=True)\nprint(assemblies)\n```\n\nFixing with string splitting instead.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: Maya update defaults variable <a href=\"https://github.com/ynput/OpenPype/pull/5368\">#5368</a></summary>\n\nSo, something was forgotten while moving out from `LegacyCreator` to `NewCreator``LegacyCreator` used `defaults` to list suggested subset names which was changed into `default_variants` in the the `NewCreator`and setting `defaults` to any values has no effect!This update affects:\n- [x] Model\n- [x] Set Dress\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Python 2 support fix <a href=\"https://github.com/ynput/OpenPype/pull/5375\">#5375</a></summary>\n\nFix Python 2 support by adding `click` into python 2 dependencies and removing f-string from maya.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: do not create top level group on reference <a href=\"https://github.com/ynput/OpenPype/pull/5402\">#5402</a></summary>\n\nThis PR allows to not wrapping loaded referenced assets in top level group either explicitly for artist or by configuration in Settings.Artists can control group creation in ReferenceLoader options.Default no group creation could be set by emptying `Group Name` in `project_settings/maya/load/reference_loader`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Settings: Houdini & Maya create plugin settings <a href=\"https://github.com/ynput/OpenPype/pull/5436\">#5436</a></summary>\n\nFixes related to Maya and Houdini settings. Renamed `defaults` to `default_variants` in plugin settings to match attribute name on create plugin in both OpenPype and AYON settings. Fixed Houdini AYON settings where were missing settings for defautlt varaints and fixed Maya AYON settings where default factory had wrong assignment.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Hide CreateAnimation <a href=\"https://github.com/ynput/OpenPype/pull/5297\">#5297</a></summary>\n\nWhen converting `animation` family or loading a `rig` family, need to include the `animation` creator but hide it in creator context.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke Anamorphic slate - Read pixel aspect from input <a href=\"https://github.com/ynput/OpenPype/pull/5304\">#5304</a></summary>\n\nWhen asset pixel aspect differs from rendered pixel aspect, Nuke slate pixel aspect is not longer taken from asset, but is readed via ffprobe.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke - Allow ExtractReviewDataMov with no timecode knob <a href=\"https://github.com/ynput/OpenPype/pull/5305\">#5305</a></summary>\n\nExtractReviewDataMov allows to specify file type. Trying to write some other extension than mov fails on generate_mov assuming that mov64_write_timecode knob exists.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: removing settings schema with defaults for OpenPype <a href=\"https://github.com/ynput/OpenPype/pull/5306\">#5306</a></summary>\n\ncontinuation of https://github.com/ynput/OpenPype/pull/5275\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: Dependency without 'inputLinks' not downloaded <a href=\"https://github.com/ynput/OpenPype/pull/5337\">#5337</a></summary>\n\nRemove condition that avoids downloading dependency without `inputLinks`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: Houdini Creator use selection even if it was toggled off <a href=\"https://github.com/ynput/OpenPype/pull/5359\">#5359</a></summary>\n\nWhen creating many product types (families) one after another without refreshing the creator window manually if you toggled `Use selection` once, all the later product types will use selection even if it was toggled offHere's Before it will keep use selection even if it was toggled off, unless you refresh window manuallyhttps://github.com/ynput/OpenPype/assets/20871534/8b890122-5b53-4c6b-897d-6a2f3aa3388aHere's After it works as expectedhttps://github.com/ynput/OpenPype/assets/20871534/6b1db990-de1b-428e-8828-04ab59a44e28\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Correct camera selection for karma renderer when using selected node <a href=\"https://github.com/ynput/OpenPype/pull/5360\">#5360</a></summary>\n\nWhen user creates the karma rop with selected camera by use selection, it will give the error message of \"no render camera found in selection\".This PR is to fix the bug of creating karma rop when using selected camera node in Houdini\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Environment variables and functions <a href=\"https://github.com/ynput/OpenPype/pull/5361\">#5361</a></summary>\n\nPrepare code for ayon-launcher compatibility. Fix ayon launcher subprocess calls, added more checks for `AYON_SERVER_ENABLED`, use ayon launcher suitable environment variables in AYON mode and changed outputs of some functions. Replaced usages of `OPENPYPE_REPOS_ROOT` environment variable with `PACKAGE_DIR` variable -> correct paths are used.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: farm rendering of prerender ignore roots in nuke <a href=\"https://github.com/ynput/OpenPype/pull/5366\">#5366</a></summary>\n\n`prerender` family was using wrong subset, same as `render` which should be different.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: Houdini update defaults variable <a href=\"https://github.com/ynput/OpenPype/pull/5367\">#5367</a></summary>\n\nSo, something was forgotten while moving out from `LegacyCreator` to `NewCreator``LegacyCreator` used `defaults` to list suggested subset names which was changed into `default_variants` in the the `NewCreator`and setting `defaults` to any values has no effect!This update affects:\n- [x] Arnold ASS\n- [x] Arnold ROP\n- [x] Karma ROP\n- [x] Mantra ROP\n- [x] Redshift ROP\n- [x] VRay ROP\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Fix create/publish animation <a href=\"https://github.com/ynput/OpenPype/pull/5369\">#5369</a></summary>\n\nUse geometry movement instead of changing min/max width.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Move unreal splash screen to unreal <a href=\"https://github.com/ynput/OpenPype/pull/5370\">#5370</a></summary>\n\nMoved splash screen code to unreal integration and removed import from Igniter.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: returned not cleaning of renders folder on the farm <a href=\"https://github.com/ynput/OpenPype/pull/5374\">#5374</a></summary>\n\nPrevious PR enabled explicit cleanup of `renders` folder after farm publishing. This is not matching customer's workflows. Customer wants to have access to files in `renders` folder and potentially redo some frames for long frame sequences.This PR extends logic of marking rendered files for deletion only if instance doesn't have `stagingDir_persistent`.For backwards compatibility all Nuke instances have `stagingDir_persistent` set to True, eg. `renders` folder won't be cleaned after farm publish.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: loading sequences is working <a href=\"https://github.com/ynput/OpenPype/pull/5376\">#5376</a></summary>\n\nLoading image sequences was broken after the latest release, version 3.16. However, I am pleased to inform you that it is now functioning as expected.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Fix settings conversion for ayon addons <a href=\"https://github.com/ynput/OpenPype/pull/5377\">#5377</a></summary>\n\nAYON addon settings are available in system settings and does not have available the same values in `\"modules\"` subkey.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: OCIO env var workflow <a href=\"https://github.com/ynput/OpenPype/pull/5379\">#5379</a></summary>\n\nThe OCIO environment variable needs to be consistently handled across all platforms. Nuke resolves the custom OCIO config path differently depending on the platform, so we included the ocio config path in the workfile with a partial replacement using an environment variable. Additionally, for Windows sessions, we replaced backward slashes with a TCL expression.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix Unreal build script <a href=\"https://github.com/ynput/OpenPype/pull/5381\">#5381</a></summary>\n\nDefine 'AYON_UNREAL_ROOT' environment variable in unreal addon.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>3dsMax: Use relative path to MAX_HOST_DIR <a href=\"https://github.com/ynput/OpenPype/pull/5382\">#5382</a></summary>\n\nUse `MAX_HOST_DIR` to calculate startup script path instead of use relative path to `OPENPYPE_ROOT` environment variable.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: Houdini abc validator error message <a href=\"https://github.com/ynput/OpenPype/pull/5386\">#5386</a></summary>\n\nWhen ABC path validator fails, it prints node objects not node paths or namesThis bug happened because of updating `get_invalid` method to return nodes instead of node pathsBeforeAfter\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: node name influence product (subset) name <a href=\"https://github.com/ynput/OpenPype/pull/5392\">#5392</a></summary>\n\nNuke now allows users to duplicate publishing instances, making the workflow easier. By duplicating a node and changing its name, users can set the product (subset) name in the publishing context.Users now have the ability to change the variant name in Publisher, which will automatically rename the associated instance node.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: delete redundant bgeo sop validator <a href=\"https://github.com/ynput/OpenPype/pull/5394\">#5394</a></summary>\n\nI found out that this `Validate BGEO SOP Path` validator is redundant, it catches two cases that are already implemented in \"Validate Output Node\". \"Validate Output Node\" works with `bgeo` as well as `abc` because `\"pointcache\"` is listed in its families\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: workfile is not reopening after change of context <a href=\"https://github.com/ynput/OpenPype/pull/5399\">#5399</a></summary>\n\nNuke no longer reopens the latest workfile when the context is changed to a different task using the Workfile tool. The issue also affected the Script Clean (from Nuke File menu) and Close feature, but it has now been fixed.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: houdini hard coded project settings <a href=\"https://github.com/ynput/OpenPype/pull/5400\">#5400</a></summary>\n\nI made this PR to solve the issue with hard-coded settings in houdini\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: 3dsMax settings <a href=\"https://github.com/ynput/OpenPype/pull/5401\">#5401</a></summary>\n\nKeep `adsk_3dsmax` group in applications settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix: update defaults to default_variants in maya and houdini OP DCC settings <a href=\"https://github.com/ynput/OpenPype/pull/5407\">#5407</a></summary>\n\nOn moving out to new creator in Maya and Houdini updating settings was missed.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Applications: Attributes creation <a href=\"https://github.com/ynput/OpenPype/pull/5408\">#5408</a></summary>\n\nApplications addon does not cause infinite server restart loop.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: fix the bug of handling Object deletion in OP Parameter <a href=\"https://github.com/ynput/OpenPype/pull/5410\">#5410</a></summary>\n\nIf the object is added to the OP parameter and user delete it in the scene thereafter, it will error out the container with OP attributes. This PR resolves the bug.This PR also fixes the bug of not adding the attribute into OP parameter correctly when the user enables \"use selections\" to link the object into the OP parameter.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Colorspace: including environments from launcher process <a href=\"https://github.com/ynput/OpenPype/pull/5411\">#5411</a></summary>\n\nFixed bug in GitHub PR where the OCIO config template was not properly formatting environment variables from System Settings `general/environment`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: workfile template fixes <a href=\"https://github.com/ynput/OpenPype/pull/5428\">#5428</a></summary>\n\nSome bunch of small bugs needed to be fixed\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini, Max: Fix missed function interface change <a href=\"https://github.com/ynput/OpenPype/pull/5430\">#5430</a></summary>\n\nThis PR https://github.com/ynput/OpenPype/pull/5321/files from @kalisp missed updating the `add_render_job_env_var` in Houdini and Max as they are passing an extra arg:\n```\nTypeError: add_render_job_env_var() takes 1 positional argument but 2 were given\n```\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Scene Inventory: Fix issue with 'sync_server' <a href=\"https://github.com/ynput/OpenPype/pull/5431\">#5431</a></summary>\n\nFix accesss to `sync_server` attribute in scene inventory.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unpack project: Fix import issue <a href=\"https://github.com/ynput/OpenPype/pull/5433\">#5433</a></summary>\n\nAdded `load_json_file`, `replace_project_documents` and `store_project_documents` to mongo init.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Chore: Versions post fixes <a href=\"https://github.com/ynput/OpenPype/pull/5441\">#5441</a></summary>\n\nFixed issues caused by my fault. Filled right version value to anatomy data.\n\n\n___\n\n</details>\n\n### **📃 Testing**\n\n\n<details>\n<summary>Tests: Copy file_handler as it will be removed by purging ayon code <a href=\"https://github.com/ynput/OpenPype/pull/5357\">#5357</a></summary>\n\nAyon code will get purged in the future from this repo/addon, therefore all `ayon_common` will be gone. `file_handler` gets internalized to tests as it is not used anywhere else.\n\n\n___\n\n</details>\n\n\n\n\n## [3.16.2](https://github.com/ynput/OpenPype/tree/3.16.2)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.1...3.16.2)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Fusion - Set selected tool to active <a href=\"https://github.com/ynput/OpenPype/pull/5327\">#5327</a></summary>\n\nWhen you run the action to select a node, this PR makes the node-flow show the selected node + you'll see the nodes controls in the inspector.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: All base create plugins <a href=\"https://github.com/ynput/OpenPype/pull/5326\">#5326</a></summary>\n\nPrepared base classes for each creator type in Maya. Extended `MayaCreatorBase` to have default implementations of common logic with instances which is used in each type of plugin.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Windows: Support long paths on zip updates. <a href=\"https://github.com/ynput/OpenPype/pull/5265\">#5265</a></summary>\n\nSupport long paths for version extract on Windows.Use case is when having long paths in for example an addon. You can install to the C drive but because the zip files are extracted in the local users folder, it'll add additional sub directories to the paths and quickly get too long paths for Windows to handle the zip updates.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Added setting to set resolution and start/end frames at startup <a href=\"https://github.com/ynput/OpenPype/pull/5338\">#5338</a></summary>\n\nThis PR adds `set_resolution_startup`and `set_frames_startup` settings. They automatically set respectively the resolution and start/end frames and FPS in Blender when opening a file or creating a new one.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Support for ExtractBurnin <a href=\"https://github.com/ynput/OpenPype/pull/5339\">#5339</a></summary>\n\nThis PR adds support for ExtractBurnin for Blender, when publishing a Review.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Extract Camera as Alembic <a href=\"https://github.com/ynput/OpenPype/pull/5343\">#5343</a></summary>\n\nAdded support to extract Alembic Cameras in Blender.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Validate Instance In Context <a href=\"https://github.com/ynput/OpenPype/pull/5335\">#5335</a></summary>\n\nMissing new publisher error so the repair action shows up.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Settings: Fix default settings <a href=\"https://github.com/ynput/OpenPype/pull/5311\">#5311</a></summary>\n\nFixed defautl settings for shotgrid. Renamed `FarmRootEnumEntity` to `DynamicEnumEntity` and removed doubled ABC metaclass definition (all settings entities have abstract metaclass).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: missing context argument <a href=\"https://github.com/ynput/OpenPype/pull/5312\">#5312</a></summary>\n\nUpdated function arguments\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Qt UI: Multiselection combobox PySide6 compatibility <a href=\"https://github.com/ynput/OpenPype/pull/5314\">#5314</a></summary>\n\n- The check states are replaced with the values for PySide6\n- `QtCore.Qt.ItemIsUserTristate` is used instead of `QtCore.Qt.ItemIsTristate` to avoid crashes on PySide6\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Docker: handle openssl 1.1.1 for centos 7 docker build <a href=\"https://github.com/ynput/OpenPype/pull/5319\">#5319</a></summary>\n\nMove to python 3.9 has added need to use openssl 1.1.x - but it is not by default available on centos 7 image. This is fixing it.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>houdini: fix typo in redshift proxy <a href=\"https://github.com/ynput/OpenPype/pull/5320\">#5320</a></summary>\n\nI believe there's a typo in `create_redshift_proxy.py`   ( extra ` )  in filename, and I made this PR to suggest a fix\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: fix wrong creator identifier in pointCache workflow <a href=\"https://github.com/ynput/OpenPype/pull/5324\">#5324</a></summary>\n\nFIxing a bug in publishing alembics, were invalid creator identifier caused missing family association.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fix colorspace compatibility check <a href=\"https://github.com/ynput/OpenPype/pull/5334\">#5334</a></summary>\n\nfor some reason a user may have `PyOpenColorIO` installed to his machine,  _in my case it came with renderman._it can trick the compatibility check as `import PyOpenColorIO` won't raise an error however it may be an old version _like my case_Beforecompatibility check was true and It used wrapper directly After Fix It will use wrapper via subprocess instead\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Remove forgotten dev logging <a href=\"https://github.com/ynput/OpenPype/pull/5315\">#5315</a></summary>\n\n\n___\n\n</details>\n\n\n\n\n## [3.16.1](https://github.com/ynput/OpenPype/tree/3.16.1)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.0...3.16.1)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Royal Render: Maya and Nuke support <a href=\"https://github.com/ynput/OpenPype/pull/5191\">#5191</a></summary>\n\nBasic working implementation of Royal Render support in Maya.It expects New publisher implemented in Maya.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Blend File Family <a href=\"https://github.com/ynput/OpenPype/pull/4321\">#4321</a></summary>\n\n<strong>Implementation of the Blend File family analogue to the Maya Scene one.\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: simple bgeo publishing <a href=\"https://github.com/ynput/OpenPype/pull/4588\">#4588</a></summary>\n\n<strong>Support for simple publishing of bgeo files.\n\n</strong>This is adding basic support for bgeo publishing in Houdini. It will allow publishing bgeo in all supported formats (selectable in the creator options).  If selected node has `output` on sop level, it will be used automatically as path in file node.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>General: delivery action add renamed frame number in Loader <a href=\"https://github.com/ynput/OpenPype/pull/5024\">#5024</a></summary>\n\nFrame Offset options for delivery in Openpype loader\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement/houdini add path action for abc validator <a href=\"https://github.com/ynput/OpenPype/pull/5237\">#5237</a></summary>\n\nAdd a default path attribute Action.it's a helper action more than a repair action, which used to add a default single value.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: auto apply all settings after template build <a href=\"https://github.com/ynput/OpenPype/pull/5277\">#5277</a></summary>\n\nAdding auto run of Apply All Settings after template is builder is finishing its process. This will apply Frame-range, Image size, Colorspace found in context of a task shot.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Harmony:Removed loader settings for Harmony <a href=\"https://github.com/ynput/OpenPype/pull/5289\">#5289</a></summary>\n\nIt shouldn't be configurable, it is internal logic. By adding additional extension it wouldn't start to work magically.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>AYON: Make appdirs case sensitive <a href=\"https://github.com/ynput/OpenPype/pull/5298\">#5298</a></summary>\n\nAppdirs for AYON are case sensitive for linux and mac so we needed to change them to match ayon launcher. Changed 'ayon' to 'AYON' and 'ynput' to 'Ynput'.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Traypublisher: Fix plugin order <a href=\"https://github.com/ynput/OpenPype/pull/5299\">#5299</a></summary>\n\nFrame range collector for traypublisher was moved to traypublisher plugins and changed order to make sure `assetEntity` is filled in `instance.data`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: removing OPENPYPE_VERSION from some host submitters <a href=\"https://github.com/ynput/OpenPype/pull/5302\">#5302</a></summary>\n\nRemoving deprecated method of adding OPENPYPE_VERSION to job environment. It was leftover and other hosts have already been cleared.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Fix args for workfile conversion util <a href=\"https://github.com/ynput/OpenPype/pull/5308\">#5308</a></summary>\n\nWorkfile update conversion util function have right expected arguments.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Maya: Refactor imports to `lib.get_reference_node` since the other function… <a href=\"https://github.com/ynput/OpenPype/pull/5258\">#5258</a></summary>\n\nRefactor imports to `lib.get_reference_node` since the other function is deprecated.\n\n\n___\n\n</details>\n\n\n\n\n## [3.16.0](https://github.com/ynput/OpenPype/tree/3.16.0)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/...3.16.0)\n\n### **🆕 New features**\n\n\n<details>\n<summary>General: Reduce usage of legacy io <a href=\"https://github.com/ynput/OpenPype/pull/4723\">#4723</a></summary>\n\nReplace usages of `legacy_io` with getter methods or reuse already available information. Create plugins using CreateContext are using context from CreateContext object. Loaders are usign getter function from context tools. Publish plugin are using information instance.data or context.data. In some cases were pieces of code refactored a little e.g. fps getter in maya.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Documentation: API docs reborn - yet again <a href=\"https://github.com/ynput/OpenPype/pull/4419\">#4419</a></summary>\n\n## Feature\n\nAdd functional base for API Documentation using Sphinx and AutoAPI.\n\nAfter unsuccessful #2512, #834 and #210 this is yet another try. But this time without ambition to solve the whole issue. This is making Shinx script to work and nothing else. Any changes and improvements in API docs should be made in subsequent PRs.\n\n## How to use it\n\nYou can run:\n\n```sh\ncd .\\docs\nmake.bat html\n```\n\nor\n\n```sh\ncd ./docs\nmake html\n```\n\nThis will go over our code and generate **.rst** files in `/docs/source/autoapi` and from those it will generate full html documentation in `/docs/build/html`.\n\nDuring the build you'll see tons of red errors that are pointing to our issues:\n\n1) **Wrong imports**\n    Invalid import are usually wrong relative imports (too deep) or circular imports.\n\n2) **Invalid doc-strings**\n   Doc-strings to be processed into documentation needs to follow some syntax - this can be checked by running\n   `pydocstyle` that is already included with OpenPype\n3) **Invalid markdown/rst files**\n    md/rst files can be included inside rst files using `.. include::` directive. But they have to be properly formatted.\n\n\n## Editing rst templates\n\nEverything starts with `/docs/source/index.rst` - this file should be properly edited, Right now it just includes `readme.rst` that in turn include and parse main `README.md`. This is entrypoint to API documentation. All templates generated by AutoAPI are in `/docs/source/autoapi`. They should be eventually commited to repository and edited too.\n\n## Steps for enhancing API documentation\n\n1) Run `/docs/make.bat html`\n2) Read the red errors/warnings - fix it in the code\n3) Run `/docs/make.bat html` again until  there are not red lines\n4) Edit rst files and add some meaningfull content there\n\n> **Note**\n> This can (should) be merged as is without doc-string fixes in the code or changes in templates. All additional improvements on API documentation should be made in new PRs.\n\n> **Warning**\n> You need to add new dependencies to use it. Run `create_venv`.\n\nConnected to #2490\n___\n\n</details>\n\n\n<details>\n<summary>Global: custom location for OP local versions <a href=\"https://github.com/ynput/OpenPype/pull/4673\">#4673</a></summary>\n\nThis provides configurable location to unzip Openpype version zips. By default, it was hardcoded to artist's app data folder, which might be problematic/slow with roaming profiles.Location must be accessible by user running OP Tray with write permissions (so `Program Files` might be problematic)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Update settings conversion <a href=\"https://github.com/ynput/OpenPype/pull/4837\">#4837</a></summary>\n\nUpdated conversion script of AYON settings to v3 settings. PR is related to changes in addons repository https://github.com/ynput/ayon-addons/pull/6 . Changed how the conversion happens -> conversion output does not start with openpype defaults but as empty dictionary.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Implement integrate links publish plugin <a href=\"https://github.com/ynput/OpenPype/pull/4842\">#4842</a></summary>\n\nImplemented entity links get/create functions. Added new integrator which replaces v3 integrator for links.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Version attributes integration <a href=\"https://github.com/ynput/OpenPype/pull/4991\">#4991</a></summary>\n\nImplemented unified integrate plugin to update version attributes after all integrations for AYON. The goal is to be able update attribute values in a unified way to a version when all addon integrators are done, so e.g. ftrack can add ftrack id to matching version in AYON server etc.The can be stored under `\"versionAttributes\"` key.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Staging versions can be used <a href=\"https://github.com/ynput/OpenPype/pull/4992\">#4992</a></summary>\n\nAdded ability to use staging versions in AYON mode.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Preparation for products <a href=\"https://github.com/ynput/OpenPype/pull/5038\">#5038</a></summary>\n\nPrepare ayon settings conversion script for `product` settings conversion.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Loader: Hide inactive versions in UI <a href=\"https://github.com/ynput/OpenPype/pull/5101\">#5101</a></summary>\n\nAdded support for `active` argument to hide versions with active set to False in Loader UI when in AYON mode.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: CLI addon command <a href=\"https://github.com/ynput/OpenPype/pull/5109\">#5109</a></summary>\n\nAdded `addon` alias for `module` in OpenPype cli commands.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: OpenPype as server addon <a href=\"https://github.com/ynput/OpenPype/pull/5199\">#5199</a></summary>\n\nOpenPype repository can be converted to AYON addon for distribution. Addon has defined dependencies that are required to use it and are not in base ayon-launcher (desktop application).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Runtime dependencies <a href=\"https://github.com/ynput/OpenPype/pull/5206\">#5206</a></summary>\n\nDefined runtime dependencies in pyproject toml. Moved python ocio and otio modules there.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Bundle distribution <a href=\"https://github.com/ynput/OpenPype/pull/5209\">#5209</a></summary>\n\nSince AYON server 0.3.0 are addon versions defined by bundles which affects how addons, dependency packages and installers are handled. Only source of truth, about any version of anything that should be used, is server bundle.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Feature/blender handle q application <a href=\"https://github.com/ynput/OpenPype/pull/5264\">#5264</a></summary>\n\nThis edit is to change the way the QApplication is run for Blender. It calls in the singleton (QApplication) during the register. This is made so that other Qt applications and addons are able to run on Blender. In its current implementation, if a QApplication is already running, all functionality of OpenPype becomes unavailable.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>General: Connect to AYON server (base) <a href=\"https://github.com/ynput/OpenPype/pull/3924\">#3924</a></summary>\n\n<strong>Initial implementation of being able use AYON server in current OpenPype client. Added ability to connect to AYON server and use base queries.\n\n</strong>AYON mode has it's own executable (and start script). To start in AYON mode just replace `start.py` with `ayon_start.py` (added tray start script to tools). Added constant `AYON_SERVER_ENABLED` to `openpype/__init__.py` to know if ayon mode is enabled. In that case Mongo is not used at all and any attempts will cause crashes.I had to modify `~/openpype/client` content to be able do this switch. Mongo implementation was moved to `mongo` subfolder and use \"star imports\" in files from where current imports are used. Logic of any tool or query in code was not changed at all. Since functions were based on mongo queries they don't use full potential of AYON server abilities.ATM implementation has login UI, distribution of files from server and replacement of mongo queries. For queries is used `ayon_api` module. Which is in live development so the versions may change from day to day.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement kitsu note with exceptions <a href=\"https://github.com/ynput/OpenPype/pull/4537\">#4537</a></summary>\n\nAdding a setting to choose some exceptions to IntegrateKitsuNote task status changes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Environment variable for default OCIO configs <a href=\"https://github.com/ynput/OpenPype/pull/4670\">#4670</a></summary>\n\nDefine environment variable which lead to root of builtin ocio configs to be able change the root without changing settings. For the path in settings was used `\"{OPENPYPE_ROOT}/vendor/bin/ocioconfig/OpenColorIOConfig\"` which disallow to change the root somewhere else. That will be needed in AYON where configs won't be part of desktop application but downloaded from server.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Editorial hierarchy creation <a href=\"https://github.com/ynput/OpenPype/pull/4699\">#4699</a></summary>\n\nImplemented extract hierarchy to AYON plugin which created entities in AYON using ayon api.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Vendorize ayon api <a href=\"https://github.com/ynput/OpenPype/pull/4753\">#4753</a></summary>\n\nVendorize ayon api into openpype vendor directory. The reason is that `ayon-python-api` is in live development and will fix/add features often in next few weeks/months, and because update of dependency requires new release -> new build, we want to avoid the need of doing that as it would affect OpenPype development.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Update PySide 6 for MacOs <a href=\"https://github.com/ynput/OpenPype/pull/4764\">#4764</a></summary>\n\nNew version of PySide6 does not have issues with settings UI. It is still breaking UI stylesheets so it is not changed for other plaforms but it is enhancement from previous state.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Removed unused cli commands <a href=\"https://github.com/ynput/OpenPype/pull/4902\">#4902</a></summary>\n\nRemoved `texturecopy` and `launch` cli commands from cli commands.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Linux & MacOS launch script <a href=\"https://github.com/ynput/OpenPype/pull/4970\">#4970</a></summary>\n\nAdded shell script to launch tray in AYON mode.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Qt scale enhancement <a href=\"https://github.com/ynput/OpenPype/pull/5059\">#5059</a></summary>\n\nSet ~~'QT_SCALE_FACTOR_ROUNDING_POLICY'~~ scale factor rounding policy of QApplication to `PassThrough` so the scaling can be 'float' number and not just 'int' (150% -> 1.5 scale).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>CI: WPS linting instead of Hound (rebase) 2 <a href=\"https://github.com/ynput/OpenPype/pull/5115\">#5115</a></summary>\n\nBecause Hound currently used to lint the code on GH ships with really old flake8 support, it fails miserably on any newer Python syntax. This PR is adding WPS linter to GitHub workflows that should step in.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: OP parameters only displays what is attached to the container <a href=\"https://github.com/ynput/OpenPype/pull/5229\">#5229</a></summary>\n\nThe OP parameter in 3dsmax only displays what is currently attached to the container while deleting while you can see the items which is not added when you are adding to the container.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Testing: improving logging during testing <a href=\"https://github.com/ynput/OpenPype/pull/5271\">#5271</a></summary>\n\nUnit testing logging was crashing on more then one nested layers of inherited loggers.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: removing deprecated settings in baking <a href=\"https://github.com/ynput/OpenPype/pull/5275\">#5275</a></summary>\n\nRemoving deprecated settings for baking with reformat. This option was only for single reformat node and it had been substituted with multiple reposition nodes.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>AYON: General fixes and updates <a href=\"https://github.com/ynput/OpenPype/pull/4975\">#4975</a></summary>\n\nFew smaller fixes related to AYON connection. Some of fixes were taken from this PR.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Start script: Change returncode on validate or list versions <a href=\"https://github.com/ynput/OpenPype/pull/4515\">#4515</a></summary>\n\n<strong>Change exit code from `1` to `0` when versions  are printed or when version is validated.\n\n</strong>Return code `1` is indicating error but there didn't happen any error.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Change login UI works <a href=\"https://github.com/ynput/OpenPype/pull/4754\">#4754</a></summary>\n\nFixed change of login UI. Logic change UI did show up, new login was successful, but after restart was used the previous login. This change fix the issue.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: General issues <a href=\"https://github.com/ynput/OpenPype/pull/4763\">#4763</a></summary>\n\nVendorized `ayon_api` from PR broke OpenPype launch, because `ayon_api` is not available. Moved `ayon_api` from ayon specific subforlder to `common` python vendor in OpenPype, and removed login in ayon start script (which was invalid anyway). Also made fixed compatibility with PySide6 by using `qtpy` instead of `Qt` and changing code which is not PySide6 compatible.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Small fixes <a href=\"https://github.com/ynput/OpenPype/pull/4841\">#4841</a></summary>\n\nBugsfixes and enhancements related to AYON logic. Define `BUILTIN_OCIO_ROOT` environment variable so OCIO configs are working. Use constants from ayon api instead of hardcoding them in codebase. Change process name from \"openpype\" to \"ayon\". Don't execute login dialog when application is not yet running but use `open` method instead. Fixed missing modules settings which were not taken from openpype defaults. Updated ayon api to `0.1.17`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix - Update gazu to 0.9.3 <a href=\"https://github.com/ynput/OpenPype/pull/4845\">#4845</a></summary>\n\nThis updates Gazu to 0.9.3 to make sure Gazu works with Kitsu and Zou 0.16.x+\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Igniter: fix error reports in silent mode <a href=\"https://github.com/ynput/OpenPype/pull/4909\">#4909</a></summary>\n\nSome errors in silent mode commands in Igniter were suppressed and not visible for example in Deadline log.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Remove ayon api from poetry lock <a href=\"https://github.com/ynput/OpenPype/pull/4964\">#4964</a></summary>\n\nRemove AYON python api from pyproject.toml and poetry.lock again.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Fix AYON settings conversion <a href=\"https://github.com/ynput/OpenPype/pull/4967\">#4967</a></summary>\n\nFix conversion of ftrack settings in AYON mode.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: ISO date format conversion issues <a href=\"https://github.com/ynput/OpenPype/pull/4981\">#4981</a></summary>\n\nFunction `datetime.fromisoformat` was replaced with `arrow.get` to be used instead.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Missing files on representations <a href=\"https://github.com/ynput/OpenPype/pull/4989\">#4989</a></summary>\n\nFix integration of files into representation in server database.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Fix Python 2 vendor for arrow <a href=\"https://github.com/ynput/OpenPype/pull/4993\">#4993</a></summary>\n\nMoved remaining dependencies for arrow from ftrack to python 2 vendor.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Fix new load plugins for next minor relase <a href=\"https://github.com/ynput/OpenPype/pull/5000\">#5000</a></summary>\n\nFix access to `fname` attribute which is not available on load plugin anymore.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Fix mongo secure connection <a href=\"https://github.com/ynput/OpenPype/pull/5031\">#5031</a></summary>\n\nFix `ssl` and `tls` keys checks in mongo uri query string.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Fix site sync settings <a href=\"https://github.com/ynput/OpenPype/pull/5069\">#5069</a></summary>\n\nFixed settings for AYON variant of sync server.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Replace deprecated keyword argument in PyMongo <a href=\"https://github.com/ynput/OpenPype/pull/5080\">#5080</a></summary>\n\nUse argument `tlsCAFile` instead of `ssl_ca_certs` to avoid deprecation warnings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Igniter: QApplication is created <a href=\"https://github.com/ynput/OpenPype/pull/5081\">#5081</a></summary>\n\nFunction `_get_qt_app` actually creates new `QApplication` if was not created yet.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Lower unidecode version <a href=\"https://github.com/ynput/OpenPype/pull/5090\">#5090</a></summary>\n\nUse older version of Unidecode module to support Python 2.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Lower cryptography to 39.0.0 <a href=\"https://github.com/ynput/OpenPype/pull/5099\">#5099</a></summary>\n\nLower cryptography to 39.0.0 to avoid breaking of DCCs like Maya and Nuke.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AYON: Global environments key fix <a href=\"https://github.com/ynput/OpenPype/pull/5118\">#5118</a></summary>\n\nSeems that when converting ayon settings to OP settings the `environments` setting is put under the `environments` key in `general` however when populating the environment the `environment` key gets picked up, which does not contain the environment variables from the `core/environments` setting\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Add collector to tray publisher for getting frame range data <a href=\"https://github.com/ynput/OpenPype/pull/5152\">#5152</a></summary>\n\nAdd collector to tray publisher to get frame range data. User can choose to enable this collector if they need this in the publisher.Resolve #5136\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: get current project settings not using unreal project name <a href=\"https://github.com/ynput/OpenPype/pull/5170\">#5170</a></summary>\n\nThere was a bug where Unreal project name was used to query project settings. But Unreal project name can differ from the \"real\" one because of naming convention rules set by Unreal. This is fixing it by asking for current project settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Substance Painter: Fix Collect Texture Set Images unable to copy.deepcopy due to QMenu <a href=\"https://github.com/ynput/OpenPype/pull/5238\">#5238</a></summary>\n\nFix `copy.deepcopy` of `instance.data`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ayon: server returns different key <a href=\"https://github.com/ynput/OpenPype/pull/5251\">#5251</a></summary>\n\nPackage returned from server has `filename` instead of `name`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Substance Painter: Fix default color management settings <a href=\"https://github.com/ynput/OpenPype/pull/5259\">#5259</a></summary>\n\nThe default settings for color management for Substance Painter were invalid, it was set to override the global config by default but specified no valid config paths of its own - and thus errored that the paths were not correct.This sets the defaults correctly to match other hosts._I quickly checked - this seems to be the only host with the wrong default settings_\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: fixing container data if windows path in value <a href=\"https://github.com/ynput/OpenPype/pull/5267\">#5267</a></summary>\n\nWindows path in container data are reformatted. Previously it was reported that Nuke was rising `utf8 0xc0` error if backward slashes were in data values.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: fix typo error in collect arnold rop <a href=\"https://github.com/ynput/OpenPype/pull/5281\">#5281</a></summary>\n\nFixing a typo error in `collect_arnold_rop.py`Reference: #5280\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Slack - enhanced logging and protection against failure <a href=\"https://github.com/ynput/OpenPype/pull/5287\">#5287</a></summary>\n\nCovered issues found in production on customer site. SlackAPI exception doesn't need to have 'error', covered uncaught exception.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Removed unnecessary import of pyblish.cli <a href=\"https://github.com/ynput/OpenPype/pull/5292\">#5292</a></summary>\n\nThis import resulted in adding additional logging handler which lead to duplication of logs in hosts with plugins containing `is_in_tests` method. Import is unnecessary for testing functionality.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Loader: Remove `context` argument from Loader.__init__() <a href=\"https://github.com/ynput/OpenPype/pull/4602\">#4602</a></summary>\n\nRemove the previously required `context` argument.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: Remove legacy integrator <a href=\"https://github.com/ynput/OpenPype/pull/4786\">#4786</a></summary>\n\nRemove the legacy integrator.\n\n\n___\n\n</details>\n\n### **📃 Documentation**\n\n\n<details>\n<summary>Next Minor Release <a href=\"https://github.com/ynput/OpenPype/pull/5291\">#5291</a></summary>\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Maya: Refactor to new publisher <a href=\"https://github.com/ynput/OpenPype/pull/4388\">#4388</a></summary>\n\n<strong>**Refactor Maya to use the new publisher with new creators.**\n\n</strong>\n- [x] Legacy instance can be converted in UI using `SubsetConvertorPlugin`\n- [x] Fix support for old style \"render\" and \"vrayscene\" instance to the new per layer format.\n- [x] Context data is stored with scene\n- [x] Workfile instance converted to AutoCreator\n- [x] Converted Creator classes\n- [x] Create animation\n- [x] Create ass\n- [x] Create assembly\n- [x] Create camera\n- [x] Create layout\n- [x] Create look\n- [x] Create mayascene\n- [x] Create model\n- [x] Create multiverse look\n- [x] Create multiverse usd\n- [x] Create multiverse usd comp\n- [x] Create multiverse usd over\n- [x] Create pointcache\n- [x] Create proxy abc\n- [x] Create redshift proxy\n- [x] Create render\n- [x] Create rendersetup\n- [x] Create review\n- [x] Create rig\n- [x] Create setdress\n- [x] Create unreal skeletalmesh\n- [x] Create unreal staticmesh\n- [x] Create vrayproxy\n- [x] Create vrayscene\n- [x] Create xgen\n- [x] Create yeti cache\n- [x] Create yeti rig\n- [ ] Tested new Creator publishes\n- [x] Publish animation\n- [x] Publish ass\n- [x] Publish assembly\n- [x] Publish camera\n- [x] Publish layout\n- [x] Publish look\n- [x] Publish mayascene\n- [x] Publish model\n- [ ] Publish multiverse look\n- [ ] Publish multiverse usd\n- [ ] Publish multiverse usd comp\n- [ ] Publish multiverse usd over\n- [x] Publish pointcache\n- [x] Publish proxy abc\n- [x] Publish redshift proxy\n- [x] Publish render\n- [x] Publish rendersetup\n- [x] Publish review\n- [x] Publish rig\n- [x] Publish setdress\n- [x] Publish unreal skeletalmesh\n- [x] Publish unreal staticmesh\n- [x] Publish vrayproxy\n- [x] Publish vrayscene\n- [x] Publish xgen\n- [x] Publish yeti cache\n- [x] Publish yeti rig\n- [x] Publish workfile\n- [x] Rig loader correctly generates a new style animation creator instance\n- [ ] Validations / Error messages for common validation failures look nice and usable as a report.\n- [ ] Make Create Animation hidden to the user (should not create manually?)\n- [x] Correctly detect difference between **'creator_attributes'** and **'instance_data'** since both are \"flattened\" to the top node.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Start script: Fix possible issues with destination drive path <a href=\"https://github.com/ynput/OpenPype/pull/4478\">#4478</a></summary>\n\n<strong>Drive paths for windows are fixing possibly missing slash at the end of destination path.\n\n</strong>Windows `subst` command require to have destination path with slash if it's a drive (it should be `G:\\` not `G:`).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: Move PyOpenColorIO to vendor/python  <a href=\"https://github.com/ynput/OpenPype/pull/4946\">#4946</a></summary>\n\nSo that DCCs don't conflict with their own.\n\nSee https://github.com/ynput/OpenPype/pull/4267#issuecomment-1537153263 for the issue with Gaffer.\n\nI'm not sure if this is the correct approach, but I assume PySide/Shiboken is under `vendor/python` for this reason as well...\n___\n\n</details>\n\n\n<details>\n<summary>RuntimeError with Click on deadline publish <a href=\"https://github.com/ynput/OpenPype/pull/5065\">#5065</a></summary>\n\nI changed Click to version 8.0 instead of 7.1.2 to solve this error:\n```\n2023-05-30 16:16:51:  0: STDOUT: Traceback (most recent call last):\n2023-05-30 16:16:51:  0: STDOUT:   File \"start.py\", line 1126, in boot\n2023-05-30 16:16:51:  0: STDOUT:   File \"/prod/softprod/apps/openpype/LINUX/3.15/dependencies/click/core.py\", line 829, in __call__\n2023-05-30 16:16:51:  0: STDOUT:     return self.main(*args, **kwargs)\n2023-05-30 16:16:51:  0: STDOUT:   File \"/prod/softprod/apps/openpype/LINUX/3.15/dependencies/click/core.py\", line 760, in main\n2023-05-30 16:16:51:  0: STDOUT:     _verify_python3_env()\n2023-05-30 16:16:51:  0: STDOUT:   File \"/prod/softprod/apps/openpype/LINUX/3.15/dependencies/click/_unicodefun.py\", line 126, in _verify_python3_env\n2023-05-30 16:16:51:  0: STDOUT:     raise RuntimeError(\n2023-05-30 16:16:51:  0: STDOUT: RuntimeError: Click will abort further execution because Python 3 was configured to use ASCII as encoding for the environment. Consult https://click.palletsprojects.com/python3/ for mitigation steps.\n```\n\n\n___\n\n</details>\n\n\n\n\n## [3.15.12](https://github.com/ynput/OpenPype/tree/3.15.12)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.11...3.15.12)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Tray Publisher: User can set colorspace per instance explicitly <a href=\"https://github.com/ynput/OpenPype/pull/4901\">#4901</a></summary>\n\nWith this feature a user can set/override the colorspace for the representations of an instance explicitly instead of relying on the File Rules from project settings or alike. This way you can ingest any file and explicitly say \"this file is colorspace X\".\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Review Family in Max <a href=\"https://github.com/ynput/OpenPype/pull/5001\">#5001</a></summary>\n\nReview Feature by creating preview animation in 3dsmax(The code is still cleaning up so there is going to be some updates until it is ready for review)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AfterEffects: support for workfile template builder <a href=\"https://github.com/ynput/OpenPype/pull/5163\">#5163</a></summary>\n\nThis PR add functionality of templated workfile builder. It allows someone to prepare AE workfile with placeholders as for automatically loading particular representation of particular subset of particular asset from context where workfile is opened.Selection from multiple prepared workfiles is provided with usage of templates, specific type of tasks could use particular workfile template etc.Artists then can build workfile from template when opening new workfile.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>CreatePlugin: Get next version helper <a href=\"https://github.com/ynput/OpenPype/pull/5242\">#5242</a></summary>\n\nImplemented helper functions to get next available versions for create instances.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: Improve Templates <a href=\"https://github.com/ynput/OpenPype/pull/4854\">#4854</a></summary>\n\nUse library method for fetching reference node and support parent in hierarchy.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bug: Maya - xgen sidecar files arent moved when saving workfile as an new asset workfile changing context - OP-6222 <a href=\"https://github.com/ynput/OpenPype/pull/5215\">#5215</a></summary>\n\nThis PR manages the Xgen files when switching context in the Workfiles app.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>node references to check for duplicates in Max <a href=\"https://github.com/ynput/OpenPype/pull/5192\">#5192</a></summary>\n\nNo duplicates for node references in Max when users trying to select nodes before publishing\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Tweak profiles logging to debug level <a href=\"https://github.com/ynput/OpenPype/pull/5194\">#5194</a></summary>\n\nTweak profiles logging to debug level since they aren't artist facing logs.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Reduce more visual clutter for artists in new publisher reports <a href=\"https://github.com/ynput/OpenPype/pull/5208\">#5208</a></summary>\n\nGot this from one of our artists' reports - figured some of these logs were definitely not for the artist, reduced those logs to debug level.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Cosmetics: Tweak pyblish repair actions (icon, logs, docstring) <a href=\"https://github.com/ynput/OpenPype/pull/5213\">#5213</a></summary>\n\n- Add icon to RepairContextAction\n- logs to debug level\n- also add attempt repair for RepairAction for consistency\n- fix RepairContextAction docstring to mention correct argument name\n\n#### Additional info\n\nWe should not forget to remove this [\"deprecated\" actions.py file](https://github.com/ynput/OpenPype/blob/3501d0d23a78fbaef106da2fffe946cb49bef855/openpype/action.py) in 3.16 (next-minor)\n\n## Testing notes:\n\n1. Run some fabulous repairs!\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: fix save file prompt on launch last workfile with color management enabled + restructure `set_colorspace` <a href=\"https://github.com/ynput/OpenPype/pull/5225\">#5225</a></summary>\n\n- Only set `configFilePath` when OCIO env var is not set since it doesn't do anything if OCIO var is set anyway.\n- Set the Maya 2022+ default OCIO path using the resources path instead of \"\" to avoid Maya Save File on new file after launch\n- **Bugfix: This is what fixes the Save prompt on open last workfile feature with Global color management enabled**\n- Move all code related to applying the maya settings together after querying the settings\n- Swap around the `if use_workfile_settings` since the check was reversed\n- Use `get_current_project_name()` instead of environment vars\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: More descriptive error messages for Loaders <a href=\"https://github.com/ynput/OpenPype/pull/5227\">#5227</a></summary>\n\nTweak raised errors and error messages for loader errors.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: add select invalid action for ValidateSopOutputNode <a href=\"https://github.com/ynput/OpenPype/pull/5231\">#5231</a></summary>\n\nThis PR adds `SelectROPAction` action to `houdini\\api\\action.py`and it's used in `Validate Output Node``SelectROPAction` is used to select the associated ROPs with the errored instances.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Remove new lines from the delivery template string <a href=\"https://github.com/ynput/OpenPype/pull/5235\">#5235</a></summary>\n\nIf the delivery template has a new line symbol at the end, say it was copied from the text editor, the delivery process will fail with `OSError` due to incorrect destination path. To avoid that I added `rstrip()` to the `delivery_path` processing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: better selection on pointcache creation  <a href=\"https://github.com/ynput/OpenPype/pull/5250\">#5250</a></summary>\n\nHoudini  allows `ObjNode` path as `sop_path` in the `ROP` unlike OP/ Ayon require `sop_path` to be set to a sop node path explicitly In this code, better selection is used to filter out invalid selections from OP/ Ayon  point of viewValid selections are\n- `SopNode` that has parent of type `geo` or `subnet`\n- `ObjNode` of type `geo` that has\n- `SopNode` of type `output`\n- `SopNode` with render flag `on` (if no `Sopnode` of type `output`)this effectively  filter\n- empty `ObjNode`\n- `ObjNode`(s) of other types like `cam` and `dopnet`\n- `SopNode`(s) that thier parents of other types like `cam` and `sop solver`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Update scene inventory even if any errors occurred during update <a href=\"https://github.com/ynput/OpenPype/pull/5252\">#5252</a></summary>\n\nWhen selecting many items in the scene inventory to update versions and one of the items would error out the updating stops. However, before this PR the scene inventory would also NOT refresh making you think it did nothing.Also implemented as method to allow some code deduplication.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Convert frame values to integers <a href=\"https://github.com/ynput/OpenPype/pull/5188\">#5188</a></summary>\n\nConvert frame values to integers.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: fix the register_event_callback correctly collecting workfile save after <a href=\"https://github.com/ynput/OpenPype/pull/5214\">#5214</a></summary>\n\nfixing the bug of register_event_callback not being able to collect action of \"workfile_save_after\" for lock file action\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: aligning default settings to distributed aces 1.2 config <a href=\"https://github.com/ynput/OpenPype/pull/5233\">#5233</a></summary>\n\nMaya colorspace setttings defaults are set the way they align our distributed ACES 1.2 config file set in global colorspace configs.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>RepairAction and SelectInvalidAction filter instances failed on the exact plugin <a href=\"https://github.com/ynput/OpenPype/pull/5240\">#5240</a></summary>\n\nRepairAction and SelectInvalidAction actually filter to instances that failed on the exact plugin - not on \"any failure\"\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Bugfix look update nodes by id with non-unique shape names (query with `fullPath`) <a href=\"https://github.com/ynput/OpenPype/pull/5257\">#5257</a></summary>\n\nFixes a bug where updating attributes on nodes with assigned shader if shape name existed more than once in the scene due to `cmds.listRelatives` call not being done with the `fullPath=True` flag.Original error:\n```python\n# Traceback (most recent call last):\n#   File \"E:\\openpype\\OpenPype\\openpype\\tools\\sceneinventory\\view.py\", line 264, in <lambda>\n#     lambda: self._show_version_dialog(items))\n#   File \"E:\\openpype\\OpenPype\\openpype\\tools\\sceneinventory\\view.py\", line 722, in _show_version_dialog\n#     self._update_containers(items, version)\n#   File \"E:\\openpype\\OpenPype\\openpype\\tools\\sceneinventory\\view.py\", line 849, in _update_containers\n#     update_container(item, item_version)\n#   File \"E:\\openpype\\OpenPype\\openpype\\pipeline\\load\\utils.py\", line 502, in update_container\n#     return loader.update(container, new_representation)\n#   File \"E:\\openpype\\OpenPype\\openpype\\hosts\\maya\\plugins\\load\\load_look.py\", line 119, in update\n#     nodes_by_id[lib.get_id(n)].append(n)\n#   File \"E:\\openpype\\OpenPype\\openpype\\hosts\\maya\\api\\lib.py\", line 1420, in get_id\n#     sel.add(node)\n```\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Create nodes with inpanel=False <a href=\"https://github.com/ynput/OpenPype/pull/5051\">#5051</a></summary>\n\nThis PR is meant to remove the annoyance of the UI changing focus to the properties window just for the property window of the newly created node to disappear. Instead of using node.hideControlPanel I'm implementing the concealment during the creation of the node which will not change the focus of the current window.\n___\n\n</details>\n\n\n<details>\n<summary>Fix the reset frame range not setting up the right timeline in Max <a href=\"https://github.com/ynput/OpenPype/pull/5187\">#5187</a></summary>\n\nResolve #5181\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Resolve: after launch automatization fixes <a href=\"https://github.com/ynput/OpenPype/pull/5193\">#5193</a></summary>\n\nWorkfile is no correctly created and aligned witch actual project. Also the launching mechanism is now fixed so even no workfile had been saved yet it will open OpenPype menu automatically.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Revert backward incompatible change of path to template to multiplatform <a href=\"https://github.com/ynput/OpenPype/pull/5197\">#5197</a></summary>\n\nNow platformity is still handed by usage of `work[root]` (or any other root that is accessible across platforms.)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: root set format updating in node graph <a href=\"https://github.com/ynput/OpenPype/pull/5198\">#5198</a></summary>\n\nNuke root node needs to be reset on some values so any knobs could be updated in node graph. This works the same way as an user would change frame number so expressions would update its values in knobs.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Hiero: fixing otio current project and cosmetics <a href=\"https://github.com/ynput/OpenPype/pull/5200\">#5200</a></summary>\n\nOtio were not returning correct current project once additional Untitled project was open in project manager stack.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: Publisher instances dont hold its enabled disabled states when Publisher reopened again <a href=\"https://github.com/ynput/OpenPype/pull/5202\">#5202</a></summary>\n\nResolve #5183, general maxscript conversion issue to python (e.g. bool conversion, true in maxscript while True in Python)(Also resolve the ValueError when you change the subset to publish into list view menu)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Burnins: Filter script is defined only for video streams <a href=\"https://github.com/ynput/OpenPype/pull/5205\">#5205</a></summary>\n\nBurnins are working for inputs with audio.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Colorspace lib fix compatible python version comparison <a href=\"https://github.com/ynput/OpenPype/pull/5212\">#5212</a></summary>\n\nFix python version comparison.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Fix `get_color_management_preferences` <a href=\"https://github.com/ynput/OpenPype/pull/5217\">#5217</a></summary>\n\nFix the issue described here where the logic for retrieving the current OCIO display and view was incorrectly trying to apply a regex to it.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Redshift ROP image format bug <a href=\"https://github.com/ynput/OpenPype/pull/5218\">#5218</a></summary>\n\nProblem :\n\"RS_outputFileFormat\" parm value was missing\nand there were more \"image_format\" than redshift rop supports\n\nFix:\n1)  removed unnecessary formats from `image_format_enum`\n2) add the selected format value to `RS_outputFileFormat`\n___\n\n</details>\n\n\n<details>\n<summary>Colorspace: check PyOpenColorIO rather then python version <a href=\"https://github.com/ynput/OpenPype/pull/5223\">#5223</a></summary>\n\nFixing previously merged PR (https://github.com/ynput/OpenPype/pull/5212) And applying better way to check compatibility with PyOpenColorIO python api.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Validate delivery action representations status <a href=\"https://github.com/ynput/OpenPype/pull/5228\">#5228</a></summary>\n\n- disable delivery button if no representations checked\n- fix macos combobox layout\n- add error message if no delivery templates found\n\n\n___\n\n</details>\n\n\n<details>\n<summary> Houdini: Add geometry check for pointcache family <a href=\"https://github.com/ynput/OpenPype/pull/5230\">#5230</a></summary>\n\nWhen `sop_path`  on ABC ROP node points to a  non `SopNode`, these validators `validate_abc_primitive_to_detail.py`, `validate_primitive_hierarchy_paths.py` will error and crash when this line is executed `geo = output_node.geometryAtFrame(frame)`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Add geometry check for VDB family <a href=\"https://github.com/ynput/OpenPype/pull/5232\">#5232</a></summary>\n\nWhen `sop_path` on Geometry ROP node points to a non SopNode, this validator `validate_vdb_output_node.py` will error and crash when this line is executed`sop_node.geometryAtFrame(frame)`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Substance Painter: Include the setting only in publish tab <a href=\"https://github.com/ynput/OpenPype/pull/5234\">#5234</a></summary>\n\nInstead of having two settings in both create and publish tab, there is solely one setting in the publish tab for users to set up the parameters.Resolve #5172\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix collecting arnold prefix when none <a href=\"https://github.com/ynput/OpenPype/pull/5243\">#5243</a></summary>\n\nWhen no prefix is specified in render settings, the renderlayer collector would error.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: OPENPYPE_VERSION should only be added when running from build <a href=\"https://github.com/ynput/OpenPype/pull/5244\">#5244</a></summary>\n\nWhen running from source the environment variable `OPENPYPE_VERSION` should not be added. This is a bugfix for the feature #4489\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fix no prompt for \"unsaved changes\" showing when opening workfile in Houdini <a href=\"https://github.com/ynput/OpenPype/pull/5246\">#5246</a></summary>\n\nFix no prompt for \"unsaved changes\" showing when opening workfile in Houdini.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fix no prompt for \"unsaved changes\" showing when opening workfile in Substance Painter <a href=\"https://github.com/ynput/OpenPype/pull/5248\">#5248</a></summary>\n\nFix no prompt for \"unsaved changes\" showing when opening workfile in Substance Painter.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: add the os library before os.environ.get <a href=\"https://github.com/ynput/OpenPype/pull/5249\">#5249</a></summary>\n\nAdding os library into `creator_plugins.py` due to `os.environ.get` in line 667\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix set_attribute for enum attributes <a href=\"https://github.com/ynput/OpenPype/pull/5261\">#5261</a></summary>\n\nFix for #5260\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Move Qt imports away from module init <a href=\"https://github.com/ynput/OpenPype/pull/5268\">#5268</a></summary>\n\nImporting `Window` creates errors in headless mode.\n```\n*** WRN: >>> { ModulesLoader }: [  FAILED to import host folder unreal  ]\n=============================\nNo Qt bindings could be found\n=============================\nTraceback (most recent call last):\n  File \"C:\\Users\\tokejepsen\\OpenPype\\.venv\\lib\\site-packages\\qtpy\\__init__.py\", line 252, in <module>\n    from PySide6 import __version__ as PYSIDE_VERSION  # analysis:ignore\nModuleNotFoundERROR: No module named 'PySide6'\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\modules\\base.py\", line 385, in _load_modules\n    default_module = __import__(\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\hosts\\unreal\\__init__.py\", line 1, in <module>\n    from .addon import UnrealAddon\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\hosts\\unreal\\addon.py\", line 4, in <module>\n    from openpype.widgets.message_window import Window\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\widgets\\__init__.py\", line 1, in <module>\n    from .password_dialog import PasswordDialog\n  File \"C:\\Users\\tokejepsen\\OpenPype\\openpype\\widgets\\password_dialog.py\", line 1, in <module>\n    from qtpy import QtWidgets, QtCore, QtGui\n  File \"C:\\Users\\tokejepsen\\OpenPype\\.venv\\lib\\site-packages\\qtpy\\__init__.py\", line 259, in <module>\n    raise QtBindingsNotFoundERROR()\nqtpy.QtBindingsNotFoundERROR: No Qt bindings could be found\n```\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Maya: Minor refactoring and code cleanup <a href=\"https://github.com/ynput/OpenPype/pull/5226\">#5226</a></summary>\n\nSome small cleanup and refactoring of logic. Removing old comments, unused imports and some minor optimization. Also removed the prints of the loader names of each container the scene in `fix_incompatible_containers` + optimizing by using `set` and defining only once. Moved some UI related code/tweaks to run `on_init` only if not in headless mode. Removed an empty `obj.py` file.Each commit message kind of describes why the change was made.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Bug: Template builder fails when loading data without outliner representation <a href=\"https://github.com/ynput/OpenPype/pull/5222\">#5222</a></summary>\n\nI add an assertion management in case the container does not have a represention in outliner.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AfterEffects - add container check validator to AE settings <a href=\"https://github.com/ynput/OpenPype/pull/5203\">#5203</a></summary>\n\nAdds check if scene contains only latest version of loaded containers.\n\n\n___\n\n</details>\n\n\n\n\n## [3.15.11](https://github.com/ynput/OpenPype/tree/3.15.11)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.10...3.15.11)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Ftrack: Task status during publishing <a href=\"https://github.com/ynput/OpenPype/pull/5123\">#5123</a></summary>\n\nAdded option to change task status during publishing for 3 possible cases: \"sending to farm\", \"local integration\" and \"on farm integration\".\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Allow for more complex temp rendering paths <a href=\"https://github.com/ynput/OpenPype/pull/5132\">#5132</a></summary>\n\nWhen changing the temporary rendering template (i.e., add `{asset}` to the path) to something a bit more complex the formatting was erroring due to missing keys.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Add support for custom path for app templates <a href=\"https://github.com/ynput/OpenPype/pull/5137\">#5137</a></summary>\n\nThis PR adds support for a custom App Templates path in Blender by setting the `BLENDER_USER_SCRIPTS` environment variable to the path specified in `OPENPYPE_APP_TEMPLATES_PATH`. This allows users to use their own custom app templates in Blender.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TrayPublisher & StandalonePublisher: Specify version <a href=\"https://github.com/ynput/OpenPype/pull/5142\">#5142</a></summary>\n\nSimple creators in TrayPublisher can affect which version will be integrated. Standalone publisher respects the version change from UI.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Workfile Builder UI: Workfile builder window is not modal <a href=\"https://github.com/ynput/OpenPype/pull/5131\">#5131</a></summary>\n\nWorkfile Templates Builder:\n- Create dialog is not a modal dialog\n- Create dialog remains open after create, so you can directly create a new placeholder with similar settings\n- In Maya allow to create root level placeholders (no selection during create) - **this felt more like a bugfix than anything else.**\n\n\n___\n\n</details>\n\n\n<details>\n<summary>3dsmax: Use custom modifiers to hold instance members <a href=\"https://github.com/ynput/OpenPype/pull/4931\">#4931</a></summary>\n\nMoving logic to handle members of publishing instance from children/parent relationship on Container to tracking via custom attribute on modifier. This eliminates limitations where you couldn't have one node multiple times under one Container and because it stores those relationships as weak references, they are easily transferable even when original nodes are renamed.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Add height, width and fps setup to project manager <a href=\"https://github.com/ynput/OpenPype/pull/5075\">#5075</a></summary>\n\nAdd Width, Height, FPS, Pixel Aspect and Frame Start/End to the Project creation dialogue in the Project Manager.I understand that the Project manager will be replaced in the upcoming Ayon, but for the time being I believe setting new project with these options available would be more fun.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: connect custom write node script to the OP setting <a href=\"https://github.com/ynput/OpenPype/pull/5113\">#5113</a></summary>\n\nAllows user to customize the values of knobs attribute in the OP setting and use it in custom write node\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Keep `publisher.create_widget` variant when creating subsets <a href=\"https://github.com/ynput/OpenPype/pull/5119\">#5119</a></summary>\n\nWhenever a person is creating a subset to publish, the \"creator\" widget resets (where you choose the variant, product, etc.) so if the person is publishing several images of the a variant which is not the default one, they have to keep selecting the correct one after every \"create\".\n\nThis commit resets the original variant upon successful creation of a subset for publishing.\n\nDemo:\n[Screencast from 2023-06-08 10-46-40.webm](https://github.com/ynput/OpenPype/assets/1800151/ca1c91d4-b8f3-43d2-a7b7-35987f5b6a3f)\n\n## Testing notes:\n1. Launch AYON/OP\n2. Launch the publisher (select a project, shot, etc.)\n3. Crete a publish type (any works)\n4. Choose a variant for the publish that is not the default\n5. \"Create >>\"\n\nThe Variant fields should still have the variant you choose.\n\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Color Management- added color management support for simple expected files on Deadline <a href=\"https://github.com/ynput/OpenPype/pull/5122\">#5122</a></summary>\n\nRunning of `ExtractOIIOTranscode` during Deadline publish was previously implemented only on DCCs with AOVs (Maya, Max).This PR extends this for other DCCs with flat structure of expected files.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>hide macos dock icon on build <a href=\"https://github.com/ynput/OpenPype/pull/5133\">#5133</a></summary>\n\nSet `LSUIElement` to `1` in the `Info.plist` to hide OP icon from the macos dock by default.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Pack project: Raise exception with reasonable message <a href=\"https://github.com/ynput/OpenPype/pull/5145\">#5145</a></summary>\n\nPack project crashes with relevant message when destination directory is not set.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Allow \"inventory\" actions to be supplied by a Module/Addon. <a href=\"https://github.com/ynput/OpenPype/pull/5146\">#5146</a></summary>\n\nAdds \"inventory\" as a possible key to the plugin paths to be returned from a module.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>3dsmax: make code compatible with 3dsmax 2022 <a href=\"https://github.com/ynput/OpenPype/pull/5164\">#5164</a></summary>\n\nPython 3.7 in 3dsmax 2022 is not supporting walrus operator. This is removing it from the code for the sake of compatibility\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Support same attribute names on different node types. <a href=\"https://github.com/ynput/OpenPype/pull/5054\">#5054</a></summary>\n\nWhen validating render settings attributes, support same attribute names on different node types.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: bug fix the standin being not loaded when they are first loaded <a href=\"https://github.com/ynput/OpenPype/pull/5143\">#5143</a></summary>\n\nfix the bug of raising error when the first two standins are loaded through the loaderThe bug mentioned in the related issue: https://github.com/ynput/OpenPype/issues/5129For some reason, `defaultArnoldRenderOptions.operator` is not listed in the connection node attribute even if `cmds.loadPlugin(\"mtoa\", quiet=True)` executed before loading the object as standins for the first time.But if you manually turn on mtoa through plugin preference and load the standins for the first time, it won't raise the related  `defaultArnoldRenderOptions.operator` error.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: bug fix arnoldExportAss unable to export selected set members <a href=\"https://github.com/ynput/OpenPype/pull/5150\">#5150</a></summary>\n\nSee #5108 fix the bug arnoldExportAss being not able to export and error out during extraction.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Xgen multiple descriptions on single shape - OP-6039 <a href=\"https://github.com/ynput/OpenPype/pull/5160\">#5160</a></summary>\n\nWhen having multiple descriptions on the same geometry, the extraction would produce redundant duplicate geometries.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Xgen export of Abc's during Render Publishing - OP-6206 <a href=\"https://github.com/ynput/OpenPype/pull/5167\">#5167</a></summary>\n\nShading assignments was missing duplicating the setup for Xgen publishing and the exporting of patches was getting the end frame incorrectly.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Include handles - OP-6236 <a href=\"https://github.com/ynput/OpenPype/pull/5175\">#5175</a></summary>\n\nRender range was missing the handles.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>OCIO: Support working with single frame renders <a href=\"https://github.com/ynput/OpenPype/pull/5053\">#5053</a></summary>\n\nWhen there is only 1 file, the datamember `files` on the representation should be a string.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Burnins: Refactored burnins script <a href=\"https://github.com/ynput/OpenPype/pull/5094\">#5094</a></summary>\n\nRefactored list value for burnins and fixed command length limit by using temp file for filters string.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: open_file function can open autosave script <a href=\"https://github.com/ynput/OpenPype/pull/5107\">#5107</a></summary>\n\nFix the bug of the workfile dialog being unable to open autosave nuke script\n\n\n___\n\n</details>\n\n\n<details>\n<summary>ImageIO: Minor fixes <a href=\"https://github.com/ynput/OpenPype/pull/5147\">#5147</a></summary>\n\nResolve few minor fixes related to latest image io changes from PR.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Fix save shortcut <a href=\"https://github.com/ynput/OpenPype/pull/5148\">#5148</a></summary>\n\nSave shortcut should work for both PySide2 and PySide6.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Pack Project: Fix files packing <a href=\"https://github.com/ynput/OpenPype/pull/5154\">#5154</a></summary>\n\nPacking of project with files does work again.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Xgen version mismatch after publish - OP-6204 <a href=\"https://github.com/ynput/OpenPype/pull/5161\">#5161</a></summary>\n\nXgen was not updating correctly when for example adding or removing descriptions. This resolve the issue by overwritting the workspace xgen file.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Edge case fixes <a href=\"https://github.com/ynput/OpenPype/pull/5165\">#5165</a></summary>\n\nFix few edge case issues that may cause issues in Publisher UI.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Colorspace: host config path backward compatibility <a href=\"https://github.com/ynput/OpenPype/pull/5166\">#5166</a></summary>\n\nOld project settings overrides are now fully backward compatible. The issue with host config paths overrides were solved and now once a project used to be set to ocio_config **enabled** with found filepaths - this is now considered as activated host ocio_config paths overrides.Nuke is having an popup dialogue which is letting know to a user that settings for config path were changed.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: import workfile missing - OP-6233 <a href=\"https://github.com/ynput/OpenPype/pull/5174\">#5174</a></summary>\n\nMissing `workfile` family to import.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Fix ignore sync filter <a href=\"https://github.com/ynput/OpenPype/pull/5176\">#5176</a></summary>\n\nFtrack ignore filter does not crash because of dictionary modifications during it's iteration.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Webpublisher - headless publish shouldn't be blocking operation <a href=\"https://github.com/ynput/OpenPype/pull/5177\">#5177</a></summary>\n\n`subprocess.call` was blocking, which resulted in UI non responsiveness as it was waiting for publish to finish.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Fix disappearing actions <a href=\"https://github.com/ynput/OpenPype/pull/5184\">#5184</a></summary>\n\nPyblish plugin actions are visible as expected.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Enhancement:animation family loaded as standing (abc) uses \"use file sequence\" <a href=\"https://github.com/ynput/OpenPype/pull/5110\">#5110</a></summary>\n\nThe changes are the following. We started by updating the the is_sequence(files) function allowing it to return True for a list of files which has only one file, since our animation in this provides just one alembic file. For the correct FPS number, we got the fps from the published ass/abc from the version data.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>add label to matching family <a href=\"https://github.com/ynput/OpenPype/pull/5128\">#5128</a></summary>\n\nI added the possibility to filter the `family smart select` with the label in addition to the family.\n\n\n___\n\n</details>\n\n\n\n\n## [3.15.10](https://github.com/ynput/OpenPype/tree/3.15.10)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.9...3.15.10)\n\n### **🆕 New features**\n\n\n<details>\n<summary>ImageIO: Adding ImageIO activation toggle to all hosts <a href=\"https://github.com/ynput/OpenPype/pull/4700\">#4700</a></summary>\n\nColorspace management can now be enabled at the project level, although it is disabled by default. Once enabled, all hosts will use the OCIO config file defined in the settings. If settings are disabled, the system switches to DCC's native color space management, and we do not store colorspace information at the representative level.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Redshift Proxy Support in 3dsMax <a href=\"https://github.com/ynput/OpenPype/pull/4625\">#4625</a></summary>\n\nRedshift Proxy Support for 3dsMax.\n- [x] Creator\n- [x] Loader\n- [x] Extractor\n- [x] Validator\n- [x]  Add documentation\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini farm publishing and rendering <a href=\"https://github.com/ynput/OpenPype/pull/4825\">#4825</a></summary>\n\nDeadline Farm publishing and Rendering for Houdini\n- [x] Mantra\n- [x] Karma(including usd renders)\n- [x] Arnold\n- [x] Elaborate Redshift ROP for deadline submission\n- [x]  fix the existing bug in Redshift ROP\n- [x] Vray\n- [x] add docs\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Feature: Blender hook to execute python scripts at launch <a href=\"https://github.com/ynput/OpenPype/pull/4905\">#4905</a></summary>\n\nHook to allow hooks to add path to a python script that will be executed when Blender starts.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Feature: Resolve: Open last workfile on launch through .scriptlib  <a href=\"https://github.com/ynput/OpenPype/pull/5047\">#5047</a></summary>\n\nAdded implementation to Resolve integration to open last workfile on launch.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Remove default windowFlags from publisher <a href=\"https://github.com/ynput/OpenPype/pull/5089\">#5089</a></summary>\n\nThe default windowFlags is making the publisher window (in Linux at least) only show the close button and it's frustrating as many times you just want to minimize the window and get back to the validation after. Removing that line I get what I'd expect.**Before:****After:**\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Show user who created the workfile on the details pane of workfile manager <a href=\"https://github.com/ynput/OpenPype/pull/5093\">#5093</a></summary>\n\nNew PR for https://github.com/ynput/OpenPype/pull/5087, which was closed after merging `next-minor` branch and then realizing we don't need to target it as it was decided it's not required to support windows. More info on that PR discussion.Small addition to add name of the `user` who created the workfile on the details pane of the workfile manager:\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Loader: Hide inactive versions in UI <a href=\"https://github.com/ynput/OpenPype/pull/5100\">#5100</a></summary>\n\nHide versions with `active` set to `False` in Loader UI.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: Repair RenderPass token when merging AOVs. <a href=\"https://github.com/ynput/OpenPype/pull/5055\">#5055</a></summary>\n\nValidator was flagging that `<RenderPass>` was in the image prefix, but did not repair the issue.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Improve error feedback when no renderable cameras exist for ASS family. <a href=\"https://github.com/ynput/OpenPype/pull/5092\">#5092</a></summary>\n\nWhen collecting cameras for `ass` family, this improves the error message when no cameras are renderable.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Custom script to set frame range of read nodes <a href=\"https://github.com/ynput/OpenPype/pull/5039\">#5039</a></summary>\n\nAdding option to set frame range specifically for the read nodes in Openpype Panel. User can set up their preferred frame range with the frame range dialog, which can be showed after clicking `Set Frame Range (Read Node)` in Openpype Tools\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Update extract review letterbox docs <a href=\"https://github.com/ynput/OpenPype/pull/5074\">#5074</a></summary>\n\nUpdate Extract Review - Letter Box section in Docs. Letterbox type description is removed.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Project pack: Documents only skips roots validation <a href=\"https://github.com/ynput/OpenPype/pull/5082\">#5082</a></summary>\n\nSingle roots validation is skipped if only documents are extracted.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: custom settings for write node without publish <a href=\"https://github.com/ynput/OpenPype/pull/5084\">#5084</a></summary>\n\nSet Render Output and other settings to write nodes for non-publish purposes.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Deadline servers <a href=\"https://github.com/ynput/OpenPype/pull/5052\">#5052</a></summary>\n\nFix working with multiple Deadline servers in Maya.\n- Pools (primary and secondary) attributes were not recreated correctly.\n- Order of collector plugins were wrong, so collected data was not injected into render instances.\n- Server attribute was not converted to string so comparing with settings was incorrect.\n- Improve debug logging for where the webservice url is getting fetched from.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix Load Reference. <a href=\"https://github.com/ynput/OpenPype/pull/5091\">#5091</a></summary>\n\nFix bug introduced with https://github.com/ynput/OpenPype/pull/4751 where `cmds.ls` returns a list.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>3dsmax: Publishing Deadline jobs from RedShift  <a href=\"https://github.com/ynput/OpenPype/pull/4960\">#4960</a></summary>\n\nFix the bug of being uable to publish deadline jobs from RedshiftUse Current File instead of Published Scene for just Redshift.\n- add save scene before rendering to ensure the scene is saved after the modification.\n- add separated aov files option to allow users to choose to have aovs in render output\n- add validator for render publish to aovid overriding the previous renders\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Fix missing frame range for pointcache and camera exports <a href=\"https://github.com/ynput/OpenPype/pull/5026\">#5026</a></summary>\n\nFix missing frame range for pointcache and camera exports on published version.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: collect_frame_fix plugin fix and cleanup <a href=\"https://github.com/ynput/OpenPype/pull/5064\">#5064</a></summary>\n\nPrevious implementation https://github.com/ynput/OpenPype/pull/5036 was broken this is fixing the issue where attribute is found in instance data although the settings were disabled for the plugin.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Hiero: Fix apply settings Clip Load <a href=\"https://github.com/ynput/OpenPype/pull/5073\">#5073</a></summary>\n\nChanged `apply_settings` to classmethod which fixes the issue with settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Resolve: Make sure scripts dir exists <a href=\"https://github.com/ynput/OpenPype/pull/5078\">#5078</a></summary>\n\nMake sure the scripts directory exists before looping over it's content.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>removing info knob from nuke creators <a href=\"https://github.com/ynput/OpenPype/pull/5083\">#5083</a></summary>\n\n- removing instance node if removed via publisher\n- removing info knob since it is not needed any more (was there only for the transition phase)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Tray: Fix restart arguments on update <a href=\"https://github.com/ynput/OpenPype/pull/5085\">#5085</a></summary>\n\nFix arguments on restart.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: bug fix on repair action in Arnold Scene Source CBID Validator <a href=\"https://github.com/ynput/OpenPype/pull/5096\">#5096</a></summary>\n\nFix the bug of not being able to use repair action  in Arnold Scene Source CBID Validator\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: batch of small fixes <a href=\"https://github.com/ynput/OpenPype/pull/5103\">#5103</a></summary>\n\n- default settings for `imageio.requiredNodes` **CreateWriteImage**\n- default settings for **LoadImage** representations\n- **Create** and **Publish** menu items with `parent=main_window` (version > 14)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: make prerender check safer <a href=\"https://github.com/ynput/OpenPype/pull/5104\">#5104</a></summary>\n\nPrerender wasn't correctly recognized and was replaced with just 'render' family.In Nuke it is correctly `prerender.farm` in families, which wasn't handled here. It resulted into using `render` in templates even if `render` and `prerender` templates were split.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Sort launcher actions alphabetically <a href=\"https://github.com/ynput/OpenPype/pull/5106\">#5106</a></summary>\n\nThe launcher actions weren't being sorted by its label but its name (which on the case of the apps it's the version number) and thus the order wasn't consistent and we kept getting a different order on every launch. From my debugging session, this was the result of what the `actions` variable held after the `filter_compatible_actions` function before these changes:\n```\n(Pdb) for p in actions: print(p.order, p.name)\n0 14-02\n0 14-02\n0 14-02\n0 14-02\n0 14-02\n0 19-5-493\n0 2023\n0 3-41\n0 6-01\n```This caused already a couple bugs from our artists thinking they had launched Nuke X and instead launched Nuke and telling us their Nuke was missing nodes**Before:****After:**\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TrayPublisher: Editorial video stream discovery <a href=\"https://github.com/ynput/OpenPype/pull/5120\">#5120</a></summary>\n\nEditorial create plugin in traypublisher does not expect that first stream in input is video.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>3dsmax: Move from deprecated interface <a href=\"https://github.com/ynput/OpenPype/pull/5117\">#5117</a></summary>\n\n`INewPublisher` interface is deprecated, this PR is changing the use to `IPublishHost` instead.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>add movalex as a contributor for code <a href=\"https://github.com/ynput/OpenPype/pull/5076\">#5076</a></summary>\n\nAdds @movalex as a contributor for code.\n\nThis was requested by mkolar [in this comment](https://github.com/ynput/OpenPype/pull/4916#issuecomment-1571498425)\n\n[skip ci]\n___\n\n</details>\n\n\n<details>\n<summary>3dsmax: refactor load plugins <a href=\"https://github.com/ynput/OpenPype/pull/5079\">#5079</a></summary>\n\n\n___\n\n</details>\n\n\n\n\n## [3.15.9](https://github.com/ynput/OpenPype/tree/3.15.9)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.8...3.15.9)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Blender: Implemented Loading of Alembic Camera <a href=\"https://github.com/ynput/OpenPype/pull/4990\">#4990</a></summary>\n\nImplemented loading of Alembic cameras in Blender.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Implemented Creator, Loader and Extractor for Levels <a href=\"https://github.com/ynput/OpenPype/pull/5008\">#5008</a></summary>\n\nCreator, Loader and Extractor for Unreal Levels have been implemented.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Blender: Added setting for base unit scale <a href=\"https://github.com/ynput/OpenPype/pull/4987\">#4987</a></summary>\n\nA setting for the base unit scale has been added for Blender.The unit scale is automatically applied when opening a file or creating a new one.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Changed naming and path of Camera Levels <a href=\"https://github.com/ynput/OpenPype/pull/5010\">#5010</a></summary>\n\nThe levels created for the camera in Unreal now include `_camera` in the name, to be better identifiable, and are placed in the camera folder.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Settings: Added option to nest settings templates <a href=\"https://github.com/ynput/OpenPype/pull/5022\">#5022</a></summary>\n\nIt is possible to nest settings templates in another templates.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement/publisher: Remove \"hit play to continue\" label on continue <a href=\"https://github.com/ynput/OpenPype/pull/5029\">#5029</a></summary>\n\nRemove \"hit play to continue\" message on continue so that it doesn't show anymore when play was clicked.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Limit number of ftrack events to query at once <a href=\"https://github.com/ynput/OpenPype/pull/5033\">#5033</a></summary>\n\nLimit the amount of ftrack events received from mongo at once to 100.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Small code cleanups <a href=\"https://github.com/ynput/OpenPype/pull/5034\">#5034</a></summary>\n\nSmall code cleanup and updates.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: collect frames to fix with settings <a href=\"https://github.com/ynput/OpenPype/pull/5036\">#5036</a></summary>\n\nSettings for `Collect Frames to Fix` will allow disable per project the plugin. Also `Rewriting latest version` attribute is hiddable from settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Publish plugin apply settings can expect only project settings <a href=\"https://github.com/ynput/OpenPype/pull/5037\">#5037</a></summary>\n\nOnly project settings are passed to optional `apply_settings` method, if the method expects only one argument.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Load Assembly fix invalid imports  <a href=\"https://github.com/ynput/OpenPype/pull/4859\">#4859</a></summary>\n\nRefactors imports so they are now correct.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Skipping rendersetup for members. <a href=\"https://github.com/ynput/OpenPype/pull/4973\">#4973</a></summary>\n\nWhen publishing a `rendersetup`, the objectset is and should be empty.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Validate Rig Output IDs <a href=\"https://github.com/ynput/OpenPype/pull/5016\">#5016</a></summary>\n\nAbsolute names of node were not used, so plugin did not fetch the nodes properly.Also missed pymel command.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: escape rootless path in publish job <a href=\"https://github.com/ynput/OpenPype/pull/4910\">#4910</a></summary>\n\nIf the publish path on Deadline job contains spaces or other characters, command was failing because the path wasn't properly escaped. This is fixing it.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Company name and URL changed <a href=\"https://github.com/ynput/OpenPype/pull/4974\">#4974</a></summary>\n\nThe current records were obsolete in inno_setup, changed to the up-to-date.\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix usage of 'get_full_path' function <a href=\"https://github.com/ynput/OpenPype/pull/5014\">#5014</a></summary>\n\nThis PR changes all the occurrences of `get_full_path` functions to alternatives to get the path of the objects.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix sequence frames validator to use correct data <a href=\"https://github.com/ynput/OpenPype/pull/5021\">#5021</a></summary>\n\nFix sequence frames validator to use clipIn and clipOut data instead of frameStart and frameEnd.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix render instances collection to use correct data <a href=\"https://github.com/ynput/OpenPype/pull/5023\">#5023</a></summary>\n\nFix render instances collection to use `frameStart` and `frameEnd` from the Project Manager, instead of the sequence's ones.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Resolve: loader is opening even if no timeline in project <a href=\"https://github.com/ynput/OpenPype/pull/5025\">#5025</a></summary>\n\nLoader is opening now even no timeline is available in a project.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>nuke: callback for dirmapping is on demand <a href=\"https://github.com/ynput/OpenPype/pull/5030\">#5030</a></summary>\n\nNuke was slowed down on processing due this callback. Since it is disabled by default it made sense to add it only on demand.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: UI works with instances without label <a href=\"https://github.com/ynput/OpenPype/pull/5032\">#5032</a></summary>\n\nPublisher UI does not crash if instance don't have filled 'label' key in instance data.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Call explicitly prepared tab methods <a href=\"https://github.com/ynput/OpenPype/pull/5044\">#5044</a></summary>\n\nIt is not possible to go to Create tab during publishing from OpenPype menu.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Role names are not case sensitive in ftrack event server status action <a href=\"https://github.com/ynput/OpenPype/pull/5058\">#5058</a></summary>\n\nEvent server status action is not case sensitive for role names of user.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Fix border widget <a href=\"https://github.com/ynput/OpenPype/pull/5063\">#5063</a></summary>\n\nFixed border lines in Publisher UI to be painted correctly with correct indentation and size.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix Commandlet Project and Permissions <a href=\"https://github.com/ynput/OpenPype/pull/5066\">#5066</a></summary>\n\nFix problem when creating an Unreal Project when Commandlet Project is in a protected location.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Added verification for Unreal app name format <a href=\"https://github.com/ynput/OpenPype/pull/5070\">#5070</a></summary>\n\nThe Unreal app name is used to determine the Unreal version folder, so it is necessary that if follows the format `x-x`, where `x` is any integer. This PR adds a verification that the app name follows that format.\n\n\n___\n\n</details>\n\n### **📃 Documentation**\n\n\n<details>\n<summary>Docs: Display wrong image in ExtractOIIOTranscode <a href=\"https://github.com/ynput/OpenPype/pull/5045\">#5045</a></summary>\n\nWrong image display in `https://openpype.io/docs/project_settings/settings_project_global#extract-oiio-transcode`.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Drop-down menu to list all families in create placeholder <a href=\"https://github.com/ynput/OpenPype/pull/4928\">#4928</a></summary>\n\nCurrently in the create placeholder window, we need to write the family manually. This replace the text field by an enum field with all families for the current software.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>add sync to specific projects or listen only <a href=\"https://github.com/ynput/OpenPype/pull/4919\">#4919</a></summary>\n\nExtend kitsu sync service with additional arguments to sync specific projects.\n\n\n___\n\n</details>\n\n\n\n\n## [3.15.8](https://github.com/ynput/OpenPype/tree/3.15.8)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.7...3.15.8)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Publisher: Show instances in report page <a href=\"https://github.com/ynput/OpenPype/pull/4915\">#4915</a></summary>\n\nShow publish instances in report page. Also added basic log view with logs grouped by instance. Validation error detail now have 2 colums, one with erro details second with logs. Crashed state shows fast access to report action buttons. Success will show only logs. Publish frame is shrunked automatically on publish stop.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion - Loader plugins updates <a href=\"https://github.com/ynput/OpenPype/pull/4920\">#4920</a></summary>\n\nUpdate to some Fusion loader plugins:The sequence loader can now load footage from the image and online family.The FBX loader can now import all formats Fusions FBX node can read.You can now import the content of another workfile into your current comp with the workfile loader.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: deadline farm rendering <a href=\"https://github.com/ynput/OpenPype/pull/4955\">#4955</a></summary>\n\nEnabling Fusion for deadline farm rendering.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AfterEffects: set frame range and resolution <a href=\"https://github.com/ynput/OpenPype/pull/4983\">#4983</a></summary>\n\nFrame information (frame start, duration, fps) and resolution (width and height) is applied to selected composition from Asset Management System (Ftrack or DB) automatically when published instance is created.It is also possible explicitly propagate both values from DB to selected composition by newly added menu buttons.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publish: Enhance automated publish plugin settings <a href=\"https://github.com/ynput/OpenPype/pull/4986\">#4986</a></summary>\n\nAdded plugins option to define settings category where to look for settings of a plugin and added public helper functions to apply settings `get_plugin_settings` and `apply_plugin_settings_automatically`.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Load Rig References - Change Rig to Animation in Animation instance <a href=\"https://github.com/ynput/OpenPype/pull/4877\">#4877</a></summary>\n\nWe are using the template builder to build an animation scene. All the rig placeholders are imported correctly, but the automatically created animation instances retain the rig family in their names and subsets. In our example, we need animationMain instead of rigMain, because this name will be used in the following steps like lighting.Here is the result we need. I checked, and it's not a template builder problem, because even if I load a rig as a reference, the result is the same. For me, since we are in the animation instance, it makes more sense to have animation instead of rig in the name. The naming is just fine if we use create from the Openpype menu.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Resolve prelaunch code refactoring and update defaults <a href=\"https://github.com/ynput/OpenPype/pull/4916\">#4916</a></summary>\n\nThe main reason of this PR is wrong default settings in `openpype/settings/defaults/system_settings/applications.json` for Resolve host. The `bin` folder should not be a part of the macos and Linux `RESOLVE_PYTHON3_PATH` variable.The rest of this PR is some code cleanups for Resolve prelaunch hook to simplify further development.Also added a .gitignore for vscode workspace files.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: 🚚 move Unreal plugin to separate repository <a href=\"https://github.com/ynput/OpenPype/pull/4980\">#4980</a></summary>\n\nTo support Epic Marketplace have to move AYON Unreal integration plugins to separate repository. This is replacing current files with git submodule, so the change should be functionally without impact.New repository lives here: https://github.com/ynput/ayon-unreal-plugin\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Lib code cleanup <a href=\"https://github.com/ynput/OpenPype/pull/5003\">#5003</a></summary>\n\nSmall cleanup in lib files in openpype.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Allow to open with djv by extension instead of representation name <a href=\"https://github.com/ynput/OpenPype/pull/5004\">#5004</a></summary>\n\nFilter open in djv action by extension instead of representation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>DJV open action `extensions` as `set` <a href=\"https://github.com/ynput/OpenPype/pull/5005\">#5005</a></summary>\n\nChange `extensions` attribute to `set`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: extract thumbnail with multiple reposition nodes <a href=\"https://github.com/ynput/OpenPype/pull/5011\">#5011</a></summary>\n\nAdded support for multiple reposition nodes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Improve logging levels and messages for artist facing publish reports <a href=\"https://github.com/ynput/OpenPype/pull/5018\">#5018</a></summary>\n\nTweak the logging levels and messages to try and only show those logs that an artist should see and could understand. Move anything that's slightly more involved into a \"debug\" message instead.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Bugfix/frame variable fix <a href=\"https://github.com/ynput/OpenPype/pull/4978\">#4978</a></summary>\n\nRenamed variables to match OpenPype terminology to reduce confusion and add consistency.\n___\n\n</details>\n\n\n<details>\n<summary>Global: plugins cleanup plugin will leave beauty rendered files <a href=\"https://github.com/ynput/OpenPype/pull/4790\">#4790</a></summary>\n\nAttempt to mark more files to be cleaned up explicitly in intermediate `renders` folder in work area for farm jobs.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fix: Download last workfile doesn't work if not already downloaded <a href=\"https://github.com/ynput/OpenPype/pull/4942\">#4942</a></summary>\n\nSome optimization condition is messing with the feature: if the published workfile is not already downloaded, it won't download it...\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix transform when loading layout to match existing assets <a href=\"https://github.com/ynput/OpenPype/pull/4972\">#4972</a></summary>\n\nFixed transform when loading layout to match existing assets.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>fix the bug of fbx loaders in Max <a href=\"https://github.com/ynput/OpenPype/pull/4977\">#4977</a></summary>\n\nbug fix of fbx loaders for not being able to parent to the CON instances while importing cameras(and models) which is published from other DCCs such as Maya.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AfterEffects: allow returning stub with not saved workfile <a href=\"https://github.com/ynput/OpenPype/pull/4984\">#4984</a></summary>\n\nAllows to use Workfile app to Save first empty workfile.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Blender: Fix Alembic loading <a href=\"https://github.com/ynput/OpenPype/pull/4985\">#4985</a></summary>\n\nFixed problem occurring when trying to load an Alembic model in Blender.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Addon Py2 compatibility <a href=\"https://github.com/ynput/OpenPype/pull/4994\">#4994</a></summary>\n\nFixed Python 2 compatibility of unreal addon.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: fixed missing files key in representation <a href=\"https://github.com/ynput/OpenPype/pull/4999\">#4999</a></summary>\n\nIssue with missing keys once rendering target set to existing frames is fixed. Instance has to be evaluated in validation for missing files.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix the frame range when loading camera <a href=\"https://github.com/ynput/OpenPype/pull/5002\">#5002</a></summary>\n\nThe keyframes of the camera, when loaded, were not using the correct frame range.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: fixing frame range targeting <a href=\"https://github.com/ynput/OpenPype/pull/5013\">#5013</a></summary>\n\nFrame range targeting at Rendering instances is now following configured options.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: fix selection from multiple webservices <a href=\"https://github.com/ynput/OpenPype/pull/5015\">#5015</a></summary>\n\nMultiple different DL webservice could be configured. First they must by configured in System Settings., then they could be configured per project in `project_settings/deadline/deadline_servers`.Only single webservice could be a target of publish though.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>3dsmax: Refactored publish plugins to use proper implementation of pymxs <a href=\"https://github.com/ynput/OpenPype/pull/4988\">#4988</a></summary>\n\n\n___\n\n</details>\n\n\n\n\n## [3.15.7](https://github.com/ynput/OpenPype/tree/3.15.7)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.6...3.15.7)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Addons directory <a href=\"https://github.com/ynput/OpenPype/pull/4893\">#4893</a></summary>\n\nThis adds a directory for Addons, for easier distribution of studio specific code.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Kitsu - Add \"image\", \"online\" and \"plate\" to review families <a href=\"https://github.com/ynput/OpenPype/pull/4923\">#4923</a></summary>\n\nThis PR adds \"image\", \"online\" and \"plate\" to the review families so they also can be uploaded to Kitsu.It also adds the `Add review to Kitsu` tag to the default png review. Without it the user would manually need to add it for single image uploads to Kitsu and might confuse users (it confused me first for a while as movies did work).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Feature/remove and load inv action <a href=\"https://github.com/ynput/OpenPype/pull/4930\">#4930</a></summary>\n\nAdded the ability to remove and load a container, as a way to reset it.This can be useful in cases where a container breaks in a way that can be fixed by removing it, then reloading it.Also added the ability to add `InventoryAction` plugins by placing them in `openpype/plugins/inventory`.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Load Rig References - Change Rig to Animation in Animation instance <a href=\"https://github.com/ynput/OpenPype/pull/4877\">#4877</a></summary>\n\nWe are using the template builder to build an animation scene. All the rig placeholders are imported correctly, but the automatically created animation instances retain the rig family in their names and subsets. In our example, we need animationMain instead of rigMain, because this name will be used in the following steps like lighting.Here is the result we need. I checked, and it's not a template builder problem, because even if I load a rig as a reference, the result is the same. For me, since we are in the animation instance, it makes more sense to have animation instead of rig in the name. The naming is just fine if we use create from the Openpype menu.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya template builder - preserve all references when importing a template <a href=\"https://github.com/ynput/OpenPype/pull/4797\">#4797</a></summary>\n\nWhen building a template with Maya template builder, we import the template and also the references inside the template file. This causes some problems:\n- We cannot use the references to version assets imported by the template.\n- When we import the file, the internal reference files are also imported. As a side effect, Maya complains about a reference that no longer exists.`// Error: file: /xxx/maya/2023.3/linux/scripts/AETemplates/AEtransformRelated.mel line 58: Reference node 'turntable_mayaSceneMain_01_RN' is not associated with a reference file.`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Renaming the integration plugin to Ayon. <a href=\"https://github.com/ynput/OpenPype/pull/4646\">#4646</a></summary>\n\nRenamed the .h, and .cpp files to Ayon. Also renamed the classes to with the Ayon keyword.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>3dsMax: render dialogue needs to be closed <a href=\"https://github.com/ynput/OpenPype/pull/4729\">#4729</a></summary>\n\nMake sure the render setup dialog is in a closed state for the update of resolution and other render settings\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya Template Builder - Remove default cameras from renderable cameras <a href=\"https://github.com/ynput/OpenPype/pull/4815\">#4815</a></summary>\n\nWhen we build an asset workfile with build workfile from template inside Maya, we load our turntable camera. But then we end up with 2 renderables camera : **persp** the one imported from the template.We need to remove the **persp** camera (or any other default camera) from renderable cameras when building the work file.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Validators for Frame Range in Max <a href=\"https://github.com/ynput/OpenPype/pull/4914\">#4914</a></summary>\n\nSwitch Render Frame Range Type to 3 for specific ranges (initial setup for the range type is 4)Reset Frame Range will also set the frame range for render settingsRender Collector won't take the frame range from context data but take the range directly from render settingAdd validators for render frame range type and frame range respectively with repair action\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Saver creator settings <a href=\"https://github.com/ynput/OpenPype/pull/4943\">#4943</a></summary>\n\nAdding Saver creator settings and enhanced rendering path with template.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Project Anatomy on creators <a href=\"https://github.com/ynput/OpenPype/pull/4962\">#4962</a></summary>\n\nAnatomy object of current project is available on `CreateContext` and create plugins.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Validate shader name - OP-5903 <a href=\"https://github.com/ynput/OpenPype/pull/4971\">#4971</a></summary>\n\nRunning the plugin would error with:\n```\n// TypeError: 'str' object cannot be interpreted as an integer\n```Fixed and added setting `active`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Fix slow Houdini launch due to shelves generation <a href=\"https://github.com/ynput/OpenPype/pull/4829\">#4829</a></summary>\n\nShelf generation during Houdini startup would add an insane amount of delay for the Houdini UI to launch correctly. By deferring the shelf generation this takes away the 5+ minutes of delay for the Houdini UI to launch.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion - Fixed \"optional validation\" <a href=\"https://github.com/ynput/OpenPype/pull/4912\">#4912</a></summary>\n\nAdded OptionalPyblishPluginMixin and is_active checks for all publish tools that should be optional\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bug: add missing `pyblish.util` import <a href=\"https://github.com/ynput/OpenPype/pull/4937\">#4937</a></summary>\n\nremote publishing was missing import of `remote_publish`. This is adding it back.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix missing 'object_path' property <a href=\"https://github.com/ynput/OpenPype/pull/4938\">#4938</a></summary>\n\nEpic removed the `object_path` property from `AssetData`. This PR fixes usages of that property.Fixes #4936\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Remove obsolete global validator <a href=\"https://github.com/ynput/OpenPype/pull/4939\">#4939</a></summary>\n\nRemoving `Validate Sequence Frames` validator from global plugins as it wasn't handling correctly many things and was by mistake enabled, breaking functionality on Deadline.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: fix build_workfile get_linked_assets missing project_name arg <a href=\"https://github.com/ynput/OpenPype/pull/4940\">#4940</a></summary>\n\nLinked assets collection don't work within `build_workfile` because `get_linked_assets` function call has a missing `project_name`argument.\n- Added the `project_name` arg to the `get_linked_assets` function call.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: fix Scene Inventory switch version error dialog missing parent arg on init <a href=\"https://github.com/ynput/OpenPype/pull/4941\">#4941</a></summary>\n\nQuickFix for the switch version error dialog to set inventory widget as parent.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix camera frame range <a href=\"https://github.com/ynput/OpenPype/pull/4956\">#4956</a></summary>\n\nFix the frame range of the level sequence for the Camera in Unreal.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix missing parameter when updating Alembic StaticMesh <a href=\"https://github.com/ynput/OpenPype/pull/4957\">#4957</a></summary>\n\nFix an error when updating an Alembic StaticMesh in Unreal, due to a missing parameter in a function call.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Fix render extraction <a href=\"https://github.com/ynput/OpenPype/pull/4963\">#4963</a></summary>\n\nFix a problem with the extraction of renders in Unreal.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Remove Python 3.8 syntax from addon <a href=\"https://github.com/ynput/OpenPype/pull/4965\">#4965</a></summary>\n\nRemoved Python 3.8 syntax from addon.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Fix editorial task creation <a href=\"https://github.com/ynput/OpenPype/pull/4966\">#4966</a></summary>\n\nFix key assignment on instance data during editorial publishing in ftrack hierarchy integration.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Add \"shortcut\" to Scripts Menu Definition <a href=\"https://github.com/ynput/OpenPype/pull/4927\">#4927</a></summary>\n\nAdd the possibility to associate a shorcut for an entry in the script menu definition with the key \"shortcut\"\n\n\n___\n\n</details>\n\n\n\n\n## [3.15.6](https://github.com/ynput/OpenPype/tree/3.15.6)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.5...3.15.6)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Substance Painter Integration <a href=\"https://github.com/ynput/OpenPype/pull/4283\">#4283</a></summary>\n\n<strong>This implements a part of #4205 by implementing a Substance Painter integration\n\n</strong>Status:\n- [x] Implement Host\n- [x] start substance with last workfile using `AddLastWorkfileToLaunchArgs` prelaunch hook\n- [x] Implement Qt tools\n- [x] Implement loaders\n- [x] Implemented a Set project mesh loader (this is relatively special case because a Project will always have exactly one mesh - a Substance Painter project cannot exist without a mesh).\n- [x] Implement project open callback\n- [x] On project open it notifies the user if the loaded model is outdated\n- [x] Implement publishing logic\n- [x] Workfile publishing\n- [x] Export Texture Sets\n- [x] Support OCIO using #4195 (draft brach is set up - see comment)\n- [ ] Likely needs more testing on the OCIO front\n- [x] Validate all outputs of the Export template are exported/generated\n- [x] Allow validation to be optional **(issue: there's no API method to detect what maps will be exported without doing an actual export to disk)**\n- [x] Support extracting/integration if not all outputs are generated\n- [x] Support multiple materials/texture sets per instance\n- [ ] Add validator that can enforce only a single texture set output if studio prefers that.\n- [ ] Implement Export File Format (extensions) override in Creator\n- [ ] Add settings so Admin can choose which extensions are available.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Data Exchange: Geometry in 3dsMax <a href=\"https://github.com/ynput/OpenPype/pull/4555\">#4555</a></summary>\n\n<strong>Introduces and updates a creator, extractors and loaders for model family\n\n</strong>Introduces new creator, extractors and loaders for model family while adding model families into the existing max scene loader and extractor\n- [x] creators\n- [x]  adding model family into max scene loader and extractor\n- [x]  fbx loader\n- [x]  fbx extractor\n- [x]  usd loader\n- [x]  usd extractor\n- [x] validator for model family\n- [x]  obj loader(update function)\n- [x]  fix the update function of the loader as #4675\n- [x]  Add documentation\n\n\n___\n\n</details>\n\n\n<details>\n<summary>AfterEffects: add review flag to each instance <a href=\"https://github.com/ynput/OpenPype/pull/4884\">#4884</a></summary>\n\nAdds `mark_for_review` flag to the Creator to allow artists to disable review if necessary.Exposed this flag in Settings, by default set to True (eg. same behavior as previously).\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Houdini: Fix Validate Output Node (VDB) <a href=\"https://github.com/ynput/OpenPype/pull/4819\">#4819</a></summary>\n\n- Removes plug-in that was a duplicate of this plug-in.\n- Optimize logging of many prims slightly\n- Fix error reporting like https://github.com/ynput/OpenPype/pull/4818 did\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Add null node as output indicator when using TAB search <a href=\"https://github.com/ynput/OpenPype/pull/4834\">#4834</a></summary>\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Don't error in collect review if camera is not set correctly <a href=\"https://github.com/ynput/OpenPype/pull/4874\">#4874</a></summary>\n\nDo not raise an error in collector when invalid path is set as camera path. Allow camera path to not be set correctly in review instance until validation so it's nicely shown in a validation report.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Project packager: Backup and restore can store only database <a href=\"https://github.com/ynput/OpenPype/pull/4879\">#4879</a></summary>\n\nPack project functionality have option to zip only project database without project files. Unpack project can skip project copy if the folder is not found.Added helper functions to `openpype.client.mongo` that can be also used for tests as replacement of mongo dump.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: ExtractOpenGL for Review instance not optional <a href=\"https://github.com/ynput/OpenPype/pull/4881\">#4881</a></summary>\n\nDon't make ExtractOpenGL optional for review instance optional.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Small style changes <a href=\"https://github.com/ynput/OpenPype/pull/4894\">#4894</a></summary>\n\nSmall changes in styles and form of publisher UI.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Workfile icon in new publisher <a href=\"https://github.com/ynput/OpenPype/pull/4898\">#4898</a></summary>\n\nFix icon for the workfile instance in new publisher\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Simplify creator icons code <a href=\"https://github.com/ynput/OpenPype/pull/4899\">#4899</a></summary>\n\nSimplify code for setting the icons for the Fusion creators\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Enhancement: Fix PySide 6.5 support for loader <a href=\"https://github.com/ynput/OpenPype/pull/4900\">#4900</a></summary>\n\nFixes PySide 6.5 support in Loader.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Validate Attributes <a href=\"https://github.com/ynput/OpenPype/pull/4917\">#4917</a></summary>\n\nThis plugin was broken due to bad fetching of data and wrong repair action.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fix: Locally copied version of last published workfile is not incremented <a href=\"https://github.com/ynput/OpenPype/pull/4722\">#4722</a></summary>\n\n### Fix 1\nWhen copied, the local workfile version keeps the published version number, when it must be +1 to follow OP's naming convention.\n\n### Fix 2\nLocal workfile version's name is built from anatomy. This avoids to get workfiles with their publish template naming.\n\n### Fix 3\nIn the case a subset has at least two tasks with published workfiles, for example `Modeling` and `Rigging`, launching `Rigging` was getting the first one with the `next` and trying to find representations, therefore `workfileModeling` and trying to match the current `task_name` (`Rigging`) with the `representation[\"context\"][\"task\"][\"name\"]` of a Modeling representation, which was ending up to a `workfile_representation` to `None`, and exiting the process.\n\nTrying to find the `task_name` in the `subset['name']` fixes it.\n\n### Fix 4\nFetch input dependencies of workfile.\n\nReplacing https://github.com/ynput/OpenPype/pull/4102 for changes to bring this home.\n___\n\n</details>\n\n\n<details>\n<summary>Maya: soft-fail when pan/zoom locked on camera when playblasting <a href=\"https://github.com/ynput/OpenPype/pull/4929\">#4929</a></summary>\n\nWhen pan/zoom enabled attribute on camera is locked, playblasting with pan/zoom fails because it is trying to restore it. This is fixing it by skipping over with warning.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Maya Load References - Add Display Handle Setting <a href=\"https://github.com/ynput/OpenPype/pull/4904\">#4904</a></summary>\n\nWhen we load a reference in Maya using OpenPype loader, display handle is checked by default and prevent us to select easily the object in the viewport. I understand that some productions like to keep this option, so I propose to add display handle to the reference loader settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Photoshop: add autocreators for review and flat image <a href=\"https://github.com/ynput/OpenPype/pull/4871\">#4871</a></summary>\n\nReview and flatten image (produced when no instance of `image` family was created) were created somehow magically. This PRintroduces two new auto creators which allow artists to disable review or flatten image.For all `image` instances `Review` flag was added to provide functionality to create separate review per `image` instance. Previously was possible only to have separate instance of `review` family.Review is not enabled on `image` family by default. (Eg. follows original behavior)Review auto creator is enabled by default as it was before.Flatten image creator must be set in Settings in `project_settings/photoshop/create/AutoImageCreator`.\n\n\n___\n\n</details>\n\n\n\n\n## [3.15.5](https://github.com/ynput/OpenPype/tree/3.15.5)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.4...3.15.5)\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: Playblast profiles <a href=\"https://github.com/ynput/OpenPype/pull/4777\">#4777</a></summary>\n\nSupport playblast profiles.This enables studios to customize what playblast settings should be on a per task and/or subset basis. For example `modeling` should have `Wireframe On Shaded` enabled, while all other tasks should have it disabled.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Support .abc files directly for Arnold standin look assignment <a href=\"https://github.com/ynput/OpenPype/pull/4856\">#4856</a></summary>\n\nIf `.abc` file is loaded into arnold standin support look assignment through the `cbId` attributes in the alembic file.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Hide animation instance in creator <a href=\"https://github.com/ynput/OpenPype/pull/4872\">#4872</a></summary>\n\n- Hide animation instance in creator\n- Add inventory action to recreate animation publish instance for loaded rigs\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Render Creator enhancements <a href=\"https://github.com/ynput/OpenPype/pull/4477\">#4477</a></summary>\n\n<strong>Improvements to the creator for render family\n\n</strong>This PR introduces some enhancements to the creator for the render family in Unreal Engine:\n- Added the option to create a new, empty sequence for the render.\n- Added the option to not include the whole hierarchy for the selected sequence.\n- Improvements of the error messages.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Added settings for rendering <a href=\"https://github.com/ynput/OpenPype/pull/4575\">#4575</a></summary>\n\n<strong>Added settings for rendering in Unreal Engine.\n\n</strong>Two settings has been added:\n- Pre roll frames, to set how many frames are used to load the scene before starting the actual rendering.\n- Configuration path, to allow to save a preset of settings from Unreal, and use it for rendering.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: Optimize anatomy formatting by only formatting used templates instead <a href=\"https://github.com/ynput/OpenPype/pull/4784\">#4784</a></summary>\n\nOptimization to not format full anatomy when only a single template is used. Instead format only the single template instead.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Patchelf version locked <a href=\"https://github.com/ynput/OpenPype/pull/4853\">#4853</a></summary>\n\nFor Centos dockerfile it is necessary to lock the patchelf version to the older, otherwise the build process fails.\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Implement `switch` method on loaders <a href=\"https://github.com/ynput/OpenPype/pull/4866\">#4866</a></summary>\n\nImplement `switch` method on loaders\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Code: Tweak docstrings and return type hints <a href=\"https://github.com/ynput/OpenPype/pull/4875\">#4875</a></summary>\n\nTweak docstrings and return type hints for functions in `openpype.client.entities`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Clear comment on successful publish and on window close <a href=\"https://github.com/ynput/OpenPype/pull/4885\">#4885</a></summary>\n\nClear comment text field on successful publish and on window close.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Make sure to reset asset widget when hidden and reshown <a href=\"https://github.com/ynput/OpenPype/pull/4886\">#4886</a></summary>\n\nMake sure to reset asset widget when hidden and reshown. Without this the asset list would never refresh in the set asset widget when changing context on an existing instance and thus would not show new assets from after the first time launching that widget.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Fix nested model instances. <a href=\"https://github.com/ynput/OpenPype/pull/4852\">#4852</a></summary>\n\nFix nested model instance under review instance, where data collection was not including \"Display Lights\" and \"Focal Length\".\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Make default namespace naming backwards compatible <a href=\"https://github.com/ynput/OpenPype/pull/4873\">#4873</a></summary>\n\nNamespaces of loaded references are now _by default_ back to what they were before #4511\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Legacy convertor skips deprecation warnings <a href=\"https://github.com/ynput/OpenPype/pull/4846\">#4846</a></summary>\n\nNuke legacy convertor was triggering deprecated function which is causing a lot of logs which slows down whole process. Changed the convertor to skip all nodes without `AVALON_TAB` to avoid the warnings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>3dsmax: move startup script logic to hook <a href=\"https://github.com/ynput/OpenPype/pull/4849\">#4849</a></summary>\n\nStartup script for OpenPype was interfering with Open Last Workfile feature. Moving this loggic from simple command line argument in the Settings to pre-launch hook is solving the order of command line arguments and making both features work.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Don't change time slider ranges in `get_frame_range` <a href=\"https://github.com/ynput/OpenPype/pull/4858\">#4858</a></summary>\n\nDon't change time slider ranges in `get_frame_range`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Looks - calculate hash for tx texture <a href=\"https://github.com/ynput/OpenPype/pull/4878\">#4878</a></summary>\n\nTexture hash is calculated for textures used in published look and it is used as key in dictionary. In recent changes, this hash is not calculated for TX files, resulting in `None` value as key in dictionary, crashing publishing. This PR is adding texture hash for TX files to solve that issue.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Collect `currentFile` context data separate from workfile instance <a href=\"https://github.com/ynput/OpenPype/pull/4883\">#4883</a></summary>\n\nFix publishing without an active workfile instance due to missing `currentFile` data.Now collect `currentFile` into context in houdini through context plugin no matter the active instances.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: fixed broken slate workflow once published on deadline <a href=\"https://github.com/ynput/OpenPype/pull/4887\">#4887</a></summary>\n\nSlate workflow is now working as expected and Validate Sequence Frames is not raising the once slate frame is included.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Add fps as instance.data in collect review in Houdini. <a href=\"https://github.com/ynput/OpenPype/pull/4888\">#4888</a></summary>\n\nfix the bug of failing to publish extract review in HoudiniOriginal error:\n```python\n  File \"OpenPype\\build\\exe.win-amd64-3.9\\openpype\\plugins\\publish\\extract_review.py\", line 516, in prepare_temp_data\n    \"fps\": float(instance.data[\"fps\"]),\nKeyError: 'fps'\n```\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TrayPublisher: Fill missing data for instances with review <a href=\"https://github.com/ynput/OpenPype/pull/4891\">#4891</a></summary>\n\nFill required data to instance in traypublisher if instance has review family. The data are required by ExtractReview and it would be complicated to do proper fix at this moment! The collector does for review instances what did https://github.com/ynput/OpenPype/pull/4383\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Keep track about current context and fix context selection widget <a href=\"https://github.com/ynput/OpenPype/pull/4892\">#4892</a></summary>\n\nChange selected context to current context on reset. Fix bug when context widget is re-enabled.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Scene inventory: Model refresh fix with cherry picking <a href=\"https://github.com/ynput/OpenPype/pull/4895\">#4895</a></summary>\n\nFix cherry pick issue in scene inventory.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Pre-render and missing review flag on instance causing crash <a href=\"https://github.com/ynput/OpenPype/pull/4897\">#4897</a></summary>\n\nIf instance created in nuke was missing `review` flag, collector crashed.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>After Effects: fix handles KeyError <a href=\"https://github.com/ynput/OpenPype/pull/4727\">#4727</a></summary>\n\nSometimes when publishing with AE (we only saw this error on AE 2023), we got a KeyError for the handles in the \"Collect Workfile\" step. So I did get the handles from the context if ther's no handles in the asset entity.\n\n\n___\n\n</details>\n\n\n\n\n## [3.15.4](https://github.com/ynput/OpenPype/tree/3.15.4)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.3...3.15.4)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Maya: Cant assign shaders to the ass file - OP-4859  <a href=\"https://github.com/ynput/OpenPype/pull/4460\">#4460</a></summary>\n\n<strong>Support AiStandIn nodes for look assignment.\n\n</strong>Using operators we assign shaders and attribute/parameters to nodes within standins. Initially there is only support for a limited mount of attributes but we can add support as needed;\n```\nprimaryVisibility\ncastsShadows\nreceiveShadows\naiSelfShadows\naiOpaque\naiMatte\naiVisibleInDiffuseTransmission\naiVisibleInSpecularTransmission\naiVisibleInVolume\naiVisibleInDiffuseReflection\naiVisibleInSpecularReflection\naiSubdivUvSmoothing\naiDispHeight\naiDispPadding\naiDispZeroValue\naiStepSize\naiVolumePadding\naiSubdivType\naiSubdivIterations\n```\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: GPU cache representation <a href=\"https://github.com/ynput/OpenPype/pull/4649\">#4649</a></summary>\n\nImplement GPU cache for model, animation and pointcache.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Implement review family with opengl node <a href=\"https://github.com/ynput/OpenPype/pull/3839\">#3839</a></summary>\n\n<strong>Implements a first pass for Reviews publishing in Houdini. Resolves #2720\n\n</strong>Uses the `opengl` ROP node to produce PNG images.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Camera focal length visible in review - OP-3278 <a href=\"https://github.com/ynput/OpenPype/pull/4531\">#4531</a></summary>\n\n<strong>Camera focal length visible in review.\n\n</strong>Support camera focal length in review; static and dynamic.Resolves #3220\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Defining plugins to load on Maya start - OP-4994 <a href=\"https://github.com/ynput/OpenPype/pull/4714\">#4714</a></summary>\n\nFeature to define plugins to load on Maya launch.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke, DL: Returning Suspended Publishing attribute <a href=\"https://github.com/ynput/OpenPype/pull/4715\">#4715</a></summary>\n\nOld Nuke Publisher's feature for suspended publishing job on render farm was added back to the current Publisher.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Settings UI: Allow setting a size hint for text fields <a href=\"https://github.com/ynput/OpenPype/pull/4821\">#4821</a></summary>\n\nText entity have `minimum_lines_count` which allows to change minimum size hint of UI input.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TrayPublisher: Move 'BatchMovieCreator' settings to 'create' subcategory <a href=\"https://github.com/ynput/OpenPype/pull/4827\">#4827</a></summary>\n\nMoved settings for `BatchMoviewCreator` into subcategory `create` in settings. Changes are made to match other hosts settings chema and structure.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya looks: support for native Redshift texture format <a href=\"https://github.com/ynput/OpenPype/pull/2971\">#2971</a></summary>\n\n<strong>Add support for native Redshift textures handling. Closes #2599\n\n</strong>Uses Redshift's Texture Processor executable to convert textures being used in renders to the Redshift \".rstexbin\" format.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: custom namespace for references <a href=\"https://github.com/ynput/OpenPype/pull/4511\">#4511</a></summary>\n\n<strong>Adding an option in Project Settings > Maya > Loader plugins to set custom namespace. If no namespace is set, the default one is used.\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Set correct framerange with handles on file opening <a href=\"https://github.com/ynput/OpenPype/pull/4664\">#4664</a></summary>\n\nSet the range of playback from the asset data, counting handles, to get the correct data when calling the \"collect_animation_data\" function.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix camera update <a href=\"https://github.com/ynput/OpenPype/pull/4751\">#4751</a></summary>\n\nFix resetting any modelPanel to a different camera when loading a camera and updating.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Remove single assembly validation for animation instances <a href=\"https://github.com/ynput/OpenPype/pull/4840\">#4840</a></summary>\n\nRig groups may now be parented to others groups when `includeParentHierarchy` attribute on the instance is \"off\".\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Optional control of display lights on playblast. <a href=\"https://github.com/ynput/OpenPype/pull/4145\">#4145</a></summary>\n\n<strong>Optional control of display lights on playblast.\n\n</strong>Giving control to what display lights are on the playblasts.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Kitsu: note family requirements <a href=\"https://github.com/ynput/OpenPype/pull/4551\">#4551</a></summary>\n\n<strong>Allowing to add family requirements to `IntegrateKitsuNote` task status change.\n\n</strong>Adds a `Family requirements` setting to `Integrate Kitsu Note`, so you can add requirements to determine if kitsu task status should be changed based on which families are published or not. For instance you could have the status change only if another subset than workfile is published (but workfile can still be included) by adding an item set to `Not equal` and `workfile`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deactivate closed Kitsu projects on OP <a href=\"https://github.com/ynput/OpenPype/pull/4619\">#4619</a></summary>\n\nDeactivate project on OP when the project is closed on Kitsu.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Suggestion to change capture labels. <a href=\"https://github.com/ynput/OpenPype/pull/4691\">#4691</a></summary>\n\nChange capture labels.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Change node type for OpenPypeContext `null` -> `subnet` <a href=\"https://github.com/ynput/OpenPype/pull/4745\">#4745</a></summary>\n\nChange the node type for OpenPype's hidden context node in Houdini from `null` to `subnet`. This fixes #4734\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Extract burnin hosts filters <a href=\"https://github.com/ynput/OpenPype/pull/4749\">#4749</a></summary>\n\nRemoved hosts filter from ExtractBurnin plugin. Instance without representations won't cause crash but just skip the instance. We've discovered because Blender already has review but did not create burnins.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: Improve speed of Collect Custom Staging Directory <a href=\"https://github.com/ynput/OpenPype/pull/4768\">#4768</a></summary>\n\nImprove speed of Collect Custom Staging Directory.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Anatomy templates formatting <a href=\"https://github.com/ynput/OpenPype/pull/4773\">#4773</a></summary>\n\nAdded option to format only single template from anatomy instead of formatting all of them all the time. Formatting of all templates is causing slowdowns e.g. during publishing of hundreds of instances.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Harmony: Handle zip files with deeper structure <a href=\"https://github.com/ynput/OpenPype/pull/4782\">#4782</a></summary>\n\nExternal Harmony zip files might contain one additional level with scene name.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Use common logic to configure executable <a href=\"https://github.com/ynput/OpenPype/pull/4788\">#4788</a></summary>\n\nUnreal Editor location and version was autodetected. This easied configuration in some cases but was not flexible enought. This PR is changing the way Unreal Editor location is set, unifying it with the logic other hosts are using.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Github: Grammar tweaks + uppercase issue title <a href=\"https://github.com/ynput/OpenPype/pull/4813\">#4813</a></summary>\n\nTweak some of the grammar in the issue form templates.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Allow creation of publish instances via Houdini TAB menu <a href=\"https://github.com/ynput/OpenPype/pull/4831\">#4831</a></summary>\n\nRegister the available Creator's as houdini tools so an artist can add publish instances via the Houdini TAB node search menu from within the network editor.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Fix Collect Render for V-Ray, Redshift and Renderman for missing colorspace <a href=\"https://github.com/ynput/OpenPype/pull/4650\">#4650</a></summary>\n\nFix Collect Render not working for Redshift, V-Ray and Renderman due to missing `colorspace` argument to `RenderProduct` dataclass.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Xgen fixes <a href=\"https://github.com/ynput/OpenPype/pull/4707\">#4707</a></summary>\n\nFix for Xgen extraction of world parented nodes and validation for required namespace.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix extract review and thumbnail for Maya 2020 <a href=\"https://github.com/ynput/OpenPype/pull/4744\">#4744</a></summary>\n\nFix playblasting in Maya 2020 with override viewport options enabled. Fixes #4730.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: local variable 'arnold_standins' referenced before assignment - OP-5542 <a href=\"https://github.com/ynput/OpenPype/pull/4778\">#4778</a></summary>\n\nMayaLookAssigner erroring when MTOA is not loaded:\n```\n# Traceback (most recent call last):\n#   File \"\\openpype\\hosts\\maya\\tools\\mayalookassigner\\app.py\", line 272, in on_process_selected\n#     nodes = list(set(item[\"nodes\"]).difference(arnold_standins))\n# UnboundLocalError: local variable 'arnold_standins' referenced before assignment\n```\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix getting view and display in Maya 2020 - OP-5035 <a href=\"https://github.com/ynput/OpenPype/pull/4795\">#4795</a></summary>\n\nThe `view_transform` returns a different format in Maya 2020. Fixes #4540 (hopefully).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix Look Maya 2020 Py2 support for Extract Look <a href=\"https://github.com/ynput/OpenPype/pull/4808\">#4808</a></summary>\n\nFix Extract Look supporting python 2.7 for Maya 2020.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix Validate Mesh Overlapping UVs plugin <a href=\"https://github.com/ynput/OpenPype/pull/4816\">#4816</a></summary>\n\nFix typo in the code where a maya command returns a `list` instead of `str`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix tile rendering with Vray - OP-5566 <a href=\"https://github.com/ynput/OpenPype/pull/4832\">#4832</a></summary>\n\nFixes tile rendering with Vray.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: checking existing frames fails when there is number in file name <a href=\"https://github.com/ynput/OpenPype/pull/4698\">#4698</a></summary>\n\nPrevious implementation of validator failed on files with any other number in rendered file names.Used regular expression pattern now handles numbers in the file names  (eg \"Main_beauty.v001.1001.exr\", \"Main_beauty_v001.1001.exr\", \"Main_beauty.1001.1001.exr\") but not numbers behind frames (eg. \"Main_beauty.1001.v001.exr\")\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Validate Render Settings. <a href=\"https://github.com/ynput/OpenPype/pull/4735\">#4735</a></summary>\n\nFixes error message when using attribute validation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Hero version sites recalculation <a href=\"https://github.com/ynput/OpenPype/pull/4737\">#4737</a></summary>\n\nSites recalculation in integrate hero version did expect that it is integrated exactly same amount of files as in previous integration. This is not the case in many cases, so the sites recalculation happens in a different way, first are prepared all sites from previous representation files, and all of them are added to each file in new representation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Fix collect current file <a href=\"https://github.com/ynput/OpenPype/pull/4739\">#4739</a></summary>\n\nFixes the Workfile publishing getting added into every instance being published from Houdini\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: Fix Extract Burnin + Colorspace functions for conflicting python environments with PYTHONHOME <a href=\"https://github.com/ynput/OpenPype/pull/4740\">#4740</a></summary>\n\nThis fixes the running of openpype processes from e.g. a host with conflicting python versions that had `PYTHONHOME` said additionally to `PYTHONPATH`, like e.g. Houdini Py3.7 together with OpenPype Py3.9 when using Extract Burnin for a review in #3839This fix applies to Extract Burnin and some of the colorspace functions that use `run_openpype_process`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Harmony: render what is in timeline in Harmony locally <a href=\"https://github.com/ynput/OpenPype/pull/4741\">#4741</a></summary>\n\nPreviously it wasn't possible to render according to what was set in Timeline in scene start/end, just by what it was set in whole timeline.This allows artist to override what is in DB with what they require (with disabled `Validate Scene Settings`). Now artist can extend scene by additional frames, that shouldn't be rendered, but which might be desired.Removed explicit set scene settings (eg. applying frames and resolution directly to the scene after launch), added separate menu item to allow artist to do it themselves.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Extract Review settings add Use Background Gradient <a href=\"https://github.com/ynput/OpenPype/pull/4747\">#4747</a></summary>\n\nAdd Display Gradient Background toggle in settings to fix support for setting flat background color for reviews.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: publisher is offering review on write families on demand <a href=\"https://github.com/ynput/OpenPype/pull/4755\">#4755</a></summary>\n\nOriginal idea where reviewable toggle will be offered in publisher on demand is fixed and now `review` attribute can be disabled in settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Workfiles: keep Browse always enabled <a href=\"https://github.com/ynput/OpenPype/pull/4766\">#4766</a></summary>\n\nBrowse might make sense even if there are no workfiles present, actually in that case it makes the most sense (eg. I want to locate workfile from outside - from Desktop for example).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: label key in instance data is optional <a href=\"https://github.com/ynput/OpenPype/pull/4779\">#4779</a></summary>\n\nCollect OTIO review plugin is not crashing if `label` key is missing in instance data.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Loader: Fix missing variable <a href=\"https://github.com/ynput/OpenPype/pull/4781\">#4781</a></summary>\n\nThere is missing variable `handles` in loader tool after https://github.com/ynput/OpenPype/pull/4746. The variable was renamed to `handles_label` and is initialized to `None` if handles are not available.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Workfile Template builder fixes <a href=\"https://github.com/ynput/OpenPype/pull/4783\">#4783</a></summary>\n\nPopup window after Nuke start is not showing. Knobs with X/Y coordination on nodes where were converted from placeholders are not added if `keepPlaceholders` is witched off.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Add family filter 'review' to burnin profile with focal length <a href=\"https://github.com/ynput/OpenPype/pull/4791\">#4791</a></summary>\n\nAvoid profile burnin with `focalLength` key for renders, but use only for playblast reviews.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>add farm instance to the render collector in 3dsMax <a href=\"https://github.com/ynput/OpenPype/pull/4794\">#4794</a></summary>\n\nbug fix for the failure of submitting publish job in 3dsmax\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Plugin active attribute is respected <a href=\"https://github.com/ynput/OpenPype/pull/4798\">#4798</a></summary>\n\nPublisher consider plugin's `active` attribute, so the plugin is not processed when `active` is set to `False`. But we use the attribute in `OptionalPyblishPluginMixin` for different purposes, so I've added hack bypass of the active state validation when plugin inherit from the mixin. This is temporary solution which cannot be changed until all hosts use Publisher otherwise global plugins would be broken. Also plugins which have `enabled` set to `False` are filtered out -> this happened only when automated settings were applied and the settings contained `\"enabled\"` key se to `False`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: settings and optional attribute in publisher for some validators <a href=\"https://github.com/ynput/OpenPype/pull/4811\">#4811</a></summary>\n\nNew publisher is supporting optional switch for plugins which is offered in Publisher in Right panel. Some plugins were missing this switch and also settings which would offer the optionality.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Settings: Version settings popup fix <a href=\"https://github.com/ynput/OpenPype/pull/4822\">#4822</a></summary>\n\nVersion completer popup have issues on some platforms, this should fix those edge cases. Also fixed issue when completer stayed shown fater reset (save).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Hiero/Nuke: adding monitorOut key to settings <a href=\"https://github.com/ynput/OpenPype/pull/4826\">#4826</a></summary>\n\nNew versions of Hiero were introduced with new colorspace property for Monitor Out. It have been added into project settings. Also added new config names into settings enumerator option.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: removed default workfile template builder preset <a href=\"https://github.com/ynput/OpenPype/pull/4835\">#4835</a></summary>\n\nDefault for workfile template builder should have been empty.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TVPaint: Review can be made from any instance <a href=\"https://github.com/ynput/OpenPype/pull/4843\">#4843</a></summary>\n\nAdd `\"review\"` tag to output of extract sequence if instance is marked for review. At this moment only instances with family `\"review\"` were able to define input for `ExtractReview` plugin which is not right.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Deadline: Remove unused FramesPerTask job info submission <a href=\"https://github.com/ynput/OpenPype/pull/4657\">#4657</a></summary>\n\nRemove unused `FramesPerTask` job info submission to Deadline.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Remove pymel dependency <a href=\"https://github.com/ynput/OpenPype/pull/4724\">#4724</a></summary>\n\nRefactors code written using `pymel` to use standard maya python libraries instead like `maya.cmds` or `maya.api.OpenMaya`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Remove \"preview\" data from representation <a href=\"https://github.com/ynput/OpenPype/pull/4759\">#4759</a></summary>\n\nRemove \"preview\" data from representation\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Collect Review cleanup code for attached subsets <a href=\"https://github.com/ynput/OpenPype/pull/4720\">#4720</a></summary>\n\nRefactor some code for Maya: Collect Review for attached subsets.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Refactor: Remove `handles`, `edit_in` and `edit_out` backwards compatibility <a href=\"https://github.com/ynput/OpenPype/pull/4746\">#4746</a></summary>\n\nRemoves backward compatibiliy fallback for data called `handles`, `edit_in` and `edit_out`.\n\n\n___\n\n</details>\n\n### **📃 Documentation**\n\n\n<details>\n<summary>Bump webpack from 5.69.1 to 5.76.1 in /website <a href=\"https://github.com/ynput/OpenPype/pull/4624\">#4624</a></summary>\n\nBumps [webpack](https://github.com/webpack/webpack) from 5.69.1 to 5.76.1.\n<details>\n<summary>Release notes</summary>\n<p><em>Sourced from <a href=\"https://github.com/webpack/webpack/releases\">webpack's releases</a>.</em></p>\n<blockquote>\n<h2>v5.76.1</h2>\n<h2>Fixed</h2>\n<ul>\n<li>Added <code>assert/strict</code> built-in to <code>NodeTargetPlugin</code></li>\n</ul>\n<h2>Revert</h2>\n<ul>\n<li>Improve performance of <code>hashRegExp</code> lookup by <a href=\"https://github.com/ryanwilsonperkin\"><code>@​ryanwilsonperkin</code></a> in <a href=\"https://redirect.github.com/webpack/webpack/pull/16759\">webpack/webpack#16759</a></li>\n</ul>\n<h2>v5.76.0</h2>\n<h2>Bugfixes</h2>\n<ul>\n<li>Avoid cross-realm object access by <a href=\"https://github.com/Jack-Works\"><code>@​Jack-Works</code></a> in <a href=\"https://redirect.github.com/webpack/webpack/pull/16500\">webpack/webpack#16500</a></li>\n<li>Improve hash performance via conditional initialization by <a href=\"https://github.com/lvivski\"><code>@​lvivski</code></a> in <a href=\"https://redirect.github.com/webpack/webpack/pull/16491\">webpack/webpack#16491</a></li>\n<li>Serialize <code>generatedCode</code> info to fix bug in asset module cache restoration by <a href=\"https://github.com/ryanwilsonperkin\"><code>@​ryanwilsonperkin</code></a> in <a href=\"https://redirect.github.com/webpack/webpack/pull/16703\">webpack/webpack#16703</a></li>\n<li>Improve performance of <code>hashRegExp</code> lookup by <a href=\"https://github.com/ryanwilsonperkin\"><code>@​ryanwilsonperkin</code></a> in <a href=\"https://redirect.github.com/webpack/webpack/pull/16759\">webpack/webpack#16759</a></li>\n</ul>\n<h2>Features</h2>\n<ul>\n<li>add <code>target</code> to <code>LoaderContext</code> type by <a href=\"https://github.com/askoufis\"><code>@​askoufis</code></a> in <a href=\"https://redirect.github.com/webpack/webpack/pull/16781\">webpack/webpack#16781</a></li>\n</ul>\n<h2>Security</h2>\n<ul>\n<li><a href=\"https://github.com/advisories/GHSA-3rfm-jhwj-7488\">CVE-2022-37603</a> fixed by <a href=\"https://github.com/akhilgkrishnan\"><code>@​akhilgkrishnan</code></a> in <a href=\"https://redirect.github.com/webpack/webpack/pull/16446\">webpack/webpack#16446</a></li>\n</ul>\n<h2>Repo Changes</h2>\n<ul>\n<li>Fix HTML5 logo in README by <a href=\"https://github.com/jakebailey\"><code>@​jakebailey</code></a> in <a href=\"https://redirect.github.com/webpack/webpack/pull/16614\">webpack/webpack#16614</a></li>\n<li>Replace TypeScript logo in README by <a href=\"https://github.com/jakebailey\"><code>@​jakebailey</code></a> in <a href=\"https://redirect.github.com/webpack/webpack/pull/16613\">webpack/webpack#16613</a></li>\n<li>Update actions/cache dependencies by <a href=\"https://github.com/piwysocki\"><code>@​piwysocki</code></a> in <a href=\"https://redirect.github.com/webpack/webpack/pull/16493\">webpack/webpack#16493</a></li>\n</ul>\n<h2>New Contributors</h2>\n<ul>\n<li><a href=\"https://github.com/Jack-Works\"><code>@​Jack-Works</code></a> made their first contribution in <a href=\"https://redirect.github.com/webpack/webpack/pull/16500\">webpack/webpack#16500</a></li>\n<li><a href=\"https://github.com/lvivski\"><code>@​lvivski</code></a> made their first contribution in <a href=\"https://redirect.github.com/webpack/webpack/pull/16491\">webpack/webpack#16491</a></li>\n<li><a href=\"https://github.com/jakebailey\"><code>@​jakebailey</code></a> made their first contribution in <a href=\"https://redirect.github.com/webpack/webpack/pull/16614\">webpack/webpack#16614</a></li>\n<li><a href=\"https://github.com/akhilgkrishnan\"><code>@​akhilgkrishnan</code></a> made their first contribution in <a href=\"https://redirect.github.com/webpack/webpack/pull/16446\">webpack/webpack#16446</a></li>\n<li><a href=\"https://github.com/ryanwilsonperkin\"><code>@​ryanwilsonperkin</code></a> made their first contribution in <a href=\"https://redirect.github.com/webpack/webpack/pull/16703\">webpack/webpack#16703</a></li>\n<li><a href=\"https://github.com/piwysocki\"><code>@​piwysocki</code></a> made their first contribution in <a href=\"https://redirect.github.com/webpack/webpack/pull/16493\">webpack/webpack#16493</a></li>\n<li><a href=\"https://github.com/askoufis\"><code>@​askoufis</code></a> made their first contribution in <a href=\"https://redirect.github.com/webpack/webpack/pull/16781\">webpack/webpack#16781</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/webpack/webpack/compare/v5.75.0...v5.76.0\">https://github.com/webpack/webpack/compare/v5.75.0...v5.76.0</a></p>\n<h2>v5.75.0</h2>\n<h1>Bugfixes</h1>\n<ul>\n<li><code>experiments.*</code> normalize to <code>false</code> when opt-out</li>\n<li>avoid <code>NaN%</code></li>\n<li>show the correct error when using a conflicting chunk name in code</li>\n<li>HMR code tests existance of <code>window</code> before trying to access it</li>\n<li>fix <code>eval-nosources-*</code> actually exclude sources</li>\n<li>fix race condition where no module is returned from processing module</li>\n<li>fix position of standalong semicolon in runtime code</li>\n</ul>\n<h1>Features</h1>\n<ul>\n<li>add support for <code>@import</code> to extenal CSS when using experimental CSS in node</li>\n</ul>\n<!-- raw HTML omitted -->\n</blockquote>\n<p>... (truncated)</p>\n</details>\n<details>\n<summary>Commits</summary>\n<ul>\n<li><a href=\"https://github.com/webpack/webpack/commit/21be52b681c477f8ebc41c1b0e7a7a8ac4fa7008\"><code>21be52b</code></a> Merge pull request <a href=\"https://redirect.github.com/webpack/webpack/issues/16804\">#16804</a> from webpack/chore-patch-release</li>\n<li><a href=\"https://github.com/webpack/webpack/commit/1cce945dd6c3576d37d3940a0233fd087ce3f6ff\"><code>1cce945</code></a> chore(release): 5.76.1</li>\n<li><a href=\"https://github.com/webpack/webpack/commit/e76ad9e724410f10209caa2ba86875ca8cf5ed61\"><code>e76ad9e</code></a> Merge pull request <a href=\"https://redirect.github.com/webpack/webpack/issues/16803\">#16803</a> from ryanwilsonperkin/revert-16759-real-content-has...</li>\n<li><a href=\"https://github.com/webpack/webpack/commit/52b1b0e4ada7c11e7f1b4f3d69b50684938c684e\"><code>52b1b0e</code></a> Revert &quot;Improve performance of hashRegExp lookup&quot;</li>\n<li><a href=\"https://github.com/webpack/webpack/commit/c989143379d344543e4161fec60f3a21beb9e3ce\"><code>c989143</code></a> Merge pull request <a href=\"https://redirect.github.com/webpack/webpack/issues/16766\">#16766</a> from piranna/patch-1</li>\n<li><a href=\"https://github.com/webpack/webpack/commit/710eaf4ddaea505e040a24beeb45a769f9e3761b\"><code>710eaf4</code></a> Merge pull request <a href=\"https://redirect.github.com/webpack/webpack/issues/16789\">#16789</a> from dmichon-msft/contenthash-hashsalt</li>\n<li><a href=\"https://github.com/webpack/webpack/commit/5d6446822aff579a5d3d9503ec2a16437d2f71d1\"><code>5d64468</code></a> Merge pull request <a href=\"https://redirect.github.com/webpack/webpack/issues/16792\">#16792</a> from webpack/update-version</li>\n<li><a href=\"https://github.com/webpack/webpack/commit/67af5ec1f05fb7cf06be6acf27353aef105ddcbc\"><code>67af5ec</code></a> chore(release): 5.76.0</li>\n<li><a href=\"https://github.com/webpack/webpack/commit/97b1718720c33f1b17302a74c5284b01e02ec001\"><code>97b1718</code></a> Merge pull request <a href=\"https://redirect.github.com/webpack/webpack/issues/16781\">#16781</a> from askoufis/loader-context-target-type</li>\n<li><a href=\"https://github.com/webpack/webpack/commit/b84efe6224b276bf72e4c5e2f4e76acddfaeef07\"><code>b84efe6</code></a> Merge pull request <a href=\"https://redirect.github.com/webpack/webpack/issues/16759\">#16759</a> from ryanwilsonperkin/real-content-hash-regex-perf</li>\n<li>Additional commits viewable in <a href=\"https://github.com/webpack/webpack/compare/v5.69.1...v5.76.1\">compare view</a></li>\n</ul>\n</details>\n<details>\n<summary>Maintainer changes</summary>\n<p>This version was pushed to npm by <a href=\"https://www.npmjs.com/~evilebottnawi\">evilebottnawi</a>, a new releaser for webpack since your current version.</p>\n</details>\n<br />\n\n\n[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=webpack&package-manager=npm_and_yarn&previous-version=5.69.1&new-version=5.76.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)\n\nDependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.\n\n[//]: # (dependabot-automerge-start)\n[//]: # (dependabot-automerge-end)\n\n---\n\n<details>\n<summary>Dependabot commands and options</summary>\n<br />\n\nYou can trigger Dependabot actions by commenting on this PR:\n- `@dependabot rebase` will rebase this PR\n- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it\n- `@dependabot merge` will merge this PR after your CI passes on it\n- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it\n- `@dependabot cancel merge` will cancel a previously requested merge and block automerging\n- `@dependabot reopen` will reopen this PR if it is closed\n- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually\n- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language\n- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language\n- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language\n- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language\n\nYou can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts).\n\n</details>\n___\n\n</details>\n\n\n<details>\n<summary>Documentation: Add Extract Burnin documentation <a href=\"https://github.com/ynput/OpenPype/pull/4765\">#4765</a></summary>\n\nAdd documentation for Extract Burnin global plugin settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Documentation: Move publisher related tips to publisher area <a href=\"https://github.com/ynput/OpenPype/pull/4772\">#4772</a></summary>\n\nMove publisher related tips for After Effects artist documentation to the correct position.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Documentation: Add extra terminology to the key concepts glossary <a href=\"https://github.com/ynput/OpenPype/pull/4838\">#4838</a></summary>\n\nTweak some of the key concepts in the documentation.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Maya: Refactor Extract Look with dedicated processors for maketx <a href=\"https://github.com/ynput/OpenPype/pull/4711\">#4711</a></summary>\n\nRefactor Maya extract look to fix some issues:\n- [x] Allow Extraction with maketx with OCIO Color Management enabled in Maya.\n- [x] Fix file hashing so it includes arguments to maketx, so that when arguments change it correctly generates a new hash\n- [x] Fix maketx destination colorspace when OCIO is enabled\n- [x] Use pre-collected colorspaces of the resources instead of trying to retrieve again in Extract Look\n- [x] Fix colorspace attributes being reinterpreted by maya on export (fix remapping) - goal is to resolve #2337\n- [x] Fix support for checking config path of maya default OCIO config (due to using `lib.get_color_management_preferences` which remaps that path)\n- [x] Merged in #2971 to refactor MakeTX into TextureProcessor and also support generating Redshift `.rstexbin` files. - goal is to resolve #2599\n- [x] Allow custom arguments to `maketx` from OpenPype Settings like mentioned here by @fabiaserra for arguments like: `--monochrome-detect`, `--opaque-detect`, `--checknan`.\n- [x] Actually fix the code and make it work. :) (I'll try to keep below checkboxes in sync with my code changes)\n- [x] Publishing without texture processor should work (no maketx + no rstexbin)\n- [x] Publishing with maketx should work\n- [x] Publishing with  rstexbin should work\n- [x] Test it. (This is just me doing some test-runs, please still test the PR!)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya template builder load all assets linked to the shot <a href=\"https://github.com/ynput/OpenPype/pull/4761\">#4761</a></summary>\n\nProblem\nAll the assets of the ftrack project are loaded and not those linked to the shot\n\nHow get error\nOpen maya in the context of shot, then build a new scene with the \"Build Workfile from template\" button in \"OpenPype\" menu.\n![image](https://user-images.githubusercontent.com/7068597/229124652-573a23d7-a2b2-4d50-81bf-7592c00d24dc.png)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: Do not force instance data with frame ranges of the asset <a href=\"https://github.com/ynput/OpenPype/pull/4383\">#4383</a></summary>\n\n<strong>This aims to resolve #4317\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Cosmetics: Fix some grammar in docstrings and messages (and some code) <a href=\"https://github.com/ynput/OpenPype/pull/4752\">#4752</a></summary>\n\nTweak some grammar in codebase\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: Submit publish job fails due root work hardcode - OP-5528 <a href=\"https://github.com/ynput/OpenPype/pull/4775\">#4775</a></summary>\n\nGenerating config templates was hardcoded to `root[work]`. This PR fixes that.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>CreateContext: Added option to remove Unknown attributes <a href=\"https://github.com/ynput/OpenPype/pull/4776\">#4776</a></summary>\n\nAdded option to remove attributes with UnkownAttrDef on instances. Pop of key will also remove the attribute definition from attribute values, so they're not recreated again.\n\n\n___\n\n</details>\n\n\n\n## [3.15.3](https://github.com/ynput/OpenPype/tree/3.15.3)\n\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.2...3.15.3)\n\n### **🆕 New features**\n\n\n<details>\n<summary>Blender: Extract Review <a href=\"https://github.com/ynput/OpenPype/pull/3616\">#3616</a></summary>\n\n<strong>Added Review to Blender.\n\n</strong>This implementation is based on #3508 but made compatible for the current implementation of OpenPype for Blender.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Data Exchanges: Point Cloud for 3dsMax <a href=\"https://github.com/ynput/OpenPype/pull/4532\">#4532</a></summary>\n\n<strong>Publish PRT format with tyFlow in 3dsmax\n\n</strong>Publish PRT format with tyFlow in 3dsmax and possibly set up loader to load the format too.\n- [x] creator\n- [x] extractor\n- [x] validator\n- [x] loader\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: persistent staging directory for renders <a href=\"https://github.com/ynput/OpenPype/pull/4583\">#4583</a></summary>\n\n<strong>Allows configure if staging directory (`stagingDir`) should be persistent with use of profiles.\n\n</strong>With this feature, users can specify a transient data folder path based on presets, which can be used during the creation and publishing stages. In some cases, these DCCs automatically add a rendering path during the creation stage, which is then used in publishing.One of the key advantages of this feature is that it allows users to take advantage of faster storages for rendering, which can help improve workflow efficiency. Additionally, this feature allows users to keep their rendered data persistent, and use their own infrastructure for regular cleaning.However, it should be noted that some productions may want to use this feature without persistency. Furthermore, there may be a need for retargeting the rendering folder to faster storages, which is also not supported at the moment.It is studio responsibility to clean up obsolete folders with data.Location of the folder is configured in `project_anatomy/templates/others`. ('transient' key is expected, with 'folder' key, could be more templates)Which family/task type/subset is applicable is configured in:`project_settings/global/tools/publish/transient_dir_profiles`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Kitsu custom comment template <a href=\"https://github.com/ynput/OpenPype/pull/4599\">#4599</a></summary>\n\nKitsu allows to write markdown in its comment field. This can be something very powerful to deliver dynamic comments with the help the data from the instance.This feature is defaults to off so the admin have to manually set up the comment field the way they want.I have added a basic example on how the comment can look like as the comment-fields default value.To this I want to add some documentation also but that's on its way when the code itself looks good for the reviewers.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>MaxScene Family  <a href=\"https://github.com/ynput/OpenPype/pull/4615\">#4615</a></summary>\n\nIntroduction of the Max Scene Family\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: Multiple values on single render attribute - OP-4131 <a href=\"https://github.com/ynput/OpenPype/pull/4631\">#4631</a></summary>\n\nWhen validating render attributes, this adds support for multiple values. When repairing first value in list is used.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: enable 2D Pan/Zoom for playblasts - OP-5213 <a href=\"https://github.com/ynput/OpenPype/pull/4687\">#4687</a></summary>\n\nSetting for enabling 2D Pan/Zoom on reviews.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Copy existing or generate new Fusion profile on prelaunch <a href=\"https://github.com/ynput/OpenPype/pull/4572\">#4572</a></summary>\n\n<strong>Fusion preferences will be copied to the predefined `~/.openpype/hosts/fusion/prefs` folder (or any other folder set in system settings) on launch.\n\n</strong>The idea is to create a copy of existing Fusion profile, adding an OpenPype menu to the Fusion instance.By default the copy setting is turned off, so no file copying is performed. Instead the clean Fusion profile is created by Fusion in the predefined folder. The default locaion is set to `~/.openpype/hosts/fusion/prefs`, to better comply with the other os platforms. After creating the default profile, some modifications are applied:\n- forced Python3\n- forced English interface\n- setup Openpype specific path maps.If the `copy_prefs` checkbox is toggled, a copy of existing Fusion profile folder will be placed in the mentioned location. Then they are altered the same way as described above. The operation is run only once, on the first launch, unless the `force_sync [Resync profile on each launch]` is toggled.English interface is forced because the `FUSION16_PROFILE_DIR` environment variable is not read otherwise (seems to be a Fusion bug).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Houdini: Create button open new publisher's \"create\" tab <a href=\"https://github.com/ynput/OpenPype/pull/4601\">#4601</a></summary>\n\nDuring a talk with @maxpareschi he mentioned that the new publisher in Houdini felt super confusing due to \"Create\" going to the older creator but now being completely empty and the publish button directly went to the publish tab.This resolves that by fixing the Create button to now open the new publisher but on the Create tab.Also made publish button enforce going to the \"publish\" tab for consistency in usage.@antirotor I think changing the Create button's callback was just missed in this commit or was there a specific reason to not change that around yet?\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Clockify: refresh and fix the integration <a href=\"https://github.com/ynput/OpenPype/pull/4607\">#4607</a></summary>\n\nDue to recent API changes, Clockify requires `user_id` to operate with the timers. I updated this part and currently it is a WIP for making it fully functional. Most functions, such as start and stop timer, and projects sync are currently working. For the rate limiting task new dependency is added: https://pypi.org/project/ratelimiter/\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion publish existing frames <a href=\"https://github.com/ynput/OpenPype/pull/4611\">#4611</a></summary>\n\nThis PR adds the function to publish existing frames instead of having to re-render all of them for each new publish.I have split the render_locally plugin so the review-part is its own plugin now.I also change the saver-creator-plugin's label from Saver to Render (saver) as I intend to add a Prerender creator like in Nuke.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Resolution settings referenced from DB record for 3dsMax <a href=\"https://github.com/ynput/OpenPype/pull/4652\">#4652</a></summary>\n\n- Add Callback for setting the resolution according to DB after the new scene is created.\n- Add a new Action into openpype menu which allows the user to reset the resolution in 3dsMax\n\n\n___\n\n</details>\n\n\n<details>\n<summary>3dsmax: render instance settings in Publish tab <a href=\"https://github.com/ynput/OpenPype/pull/4658\">#4658</a></summary>\n\nAllows user preset the pools, group and use_published settings in Render Creator in the Max Hosts.User can set the settings before or after creating instance in the new publisher\n\n\n___\n\n</details>\n\n\n<details>\n<summary>scene length setting referenced from DB record for 3dsMax <a href=\"https://github.com/ynput/OpenPype/pull/4665\">#4665</a></summary>\n\nSetting the timeline length based on DB record in 3dsMax Hosts\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Windows reduce command window pop-ups during Publishing <a href=\"https://github.com/ynput/OpenPype/pull/4672\">#4672</a></summary>\n\nReduce the command line pop-ups that show on Windows during publishing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Explicit save <a href=\"https://github.com/ynput/OpenPype/pull/4676\">#4676</a></summary>\n\nPublisher have explicit button to save changes, so reset can happen without saving any changes. Save still happens automatically when publishing is started or on publisher window close. But a popup is shown if context of host has changed. Important context was enhanced by workfile path (if host integration supports it) so workfile changes are captured too. In that case a dialog with confirmation is shown to user. All callbacks that may require save of context were moved to main window to be able handle dialog show at one place. Save changes now returns success so the rest of logic is skipped -> publishing won't start, when save of instances fails.Save and reset buttons have shortcuts (Ctrl + s and Ctrls + r).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>CelAction: conditional workfile parameters from settings <a href=\"https://github.com/ynput/OpenPype/pull/4677\">#4677</a></summary>\n\nSince some productions were requesting excluding some workfile parameters from publishing submission, we needed to move them to settings so those could be altered per project.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Improve logging of used app + tool envs on application launch <a href=\"https://github.com/ynput/OpenPype/pull/4682\">#4682</a></summary>\n\nImprove logging of what apps + tool environments got loaded for an application launch.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fix name and docstring for Create Workdir Extra Folders prelaunch hook <a href=\"https://github.com/ynput/OpenPype/pull/4683\">#4683</a></summary>\n\nFix class name and docstring for Create Workdir Extra Folders prelaunch hookThe class name and docstring were originally copied from another plug-in and didn't match the plug-in logic.This also fixes potentially seeing this twice in your logs. Before:After:Where it was actually running both this prelaunch hook and the actual `AddLastWorkfileToLaunchArgs` plugin.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Application launch context: Include app group name in logger <a href=\"https://github.com/ynput/OpenPype/pull/4684\">#4684</a></summary>\n\nClarify in logs better what app group the ApplicationLaunchContext belongs to and what application is being launched.Before:After:\n\n\n___\n\n</details>\n\n\n<details>\n<summary>increment workfile version 3dsmax <a href=\"https://github.com/ynput/OpenPype/pull/4685\">#4685</a></summary>\n\nincrement workfile version in 3dsmax as if in blender and maya hosts.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>Maya: Fix getting non-active model panel. <a href=\"https://github.com/ynput/OpenPype/pull/2968\">#2968</a></summary>\n\n<strong>When capturing multiple cameras with image planes that have file sequences playing, only the active (first) camera will play through the file sequence.\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix broken review publishing. <a href=\"https://github.com/ynput/OpenPype/pull/4549\">#4549</a></summary>\n\n<strong>Resolves #4547\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Avoid error on right click in Loader if `mtoa` is not loaded <a href=\"https://github.com/ynput/OpenPype/pull/4616\">#4616</a></summary>\n\nFix an error on right clicking in the Loader when `mtoa` is not a loaded plug-in.Additionally if `mtoa` isn't loaded the loader will now load the plug-in before trying to create the arnold standin.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix extract look colorspace detection <a href=\"https://github.com/ynput/OpenPype/pull/4618\">#4618</a></summary>\n\nFix the logic which guesses the colorspace using `arnold` python library.\n- Previously it'd error if `mtoa` was not available on path so it still required `mtoa` to be available.\n- The guessing colorspace logic doesn't actually require `mtoa` to be loaded, but just the `arnold` python library to be available. This changes the logic so it doesn't require the `mtoa` plugin to get loaded to guess the colorspace.\n- The if/else branch was likely not doing what was intended `cmds.loadPlugin(\"mtoa\", quiet=True)` returns None if the plug-in was already loaded. So this would only ever be true if it ends up loading the `mtoa` plugin the first time.\n```python\n# Tested in Maya 2022.1\nprint(cmds.loadPlugin(\"mtoa\", quiet=True))\n# ['mtoa']\nprint(cmds.loadPlugin(\"mtoa\", quiet=True))\n# None\n```\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Maya Playblast Options overrides - OP-3847 <a href=\"https://github.com/ynput/OpenPype/pull/4634\">#4634</a></summary>\n\nWhen publishing a review in Maya, the extractor would fail due to wrong (long) panel name.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix/op 2834 fix extract playblast <a href=\"https://github.com/ynput/OpenPype/pull/4701\">#4701</a></summary>\n\nParagraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix/op 2834 fix extract playblast <a href=\"https://github.com/ynput/OpenPype/pull/4704\">#4704</a></summary>\n\nParagraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: bug fix for passing zoom settings if review is attached to subset <a href=\"https://github.com/ynput/OpenPype/pull/4716\">#4716</a></summary>\n\nFix for attaching review to subset with pan/zoom option.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: tile assembly fail in draft - OP-4820 <a href=\"https://github.com/ynput/OpenPype/pull/4416\">#4416</a></summary>\n\n<strong>Tile assembly in Deadline was broken.\n\n</strong>Initial bug report revealed other areas of the tile assembly that needed fixing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Yeti Validate Rig Input - OP-3454 <a href=\"https://github.com/ynput/OpenPype/pull/4554\">#4554</a></summary>\n\n<strong>Fix Yeti Validate Rig Input\n\n</strong>Existing workflow was broken due to this #3297.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Scene inventory: Fix code errors when \"not found\" entries are found <a href=\"https://github.com/ynput/OpenPype/pull/4594\">#4594</a></summary>\n\nWhenever a \"NOT FOUND\" entry is present a lot of errors happened in the Scene Inventory:\n- It started spamming a lot of errors for the VersionDelegate since it had no numeric version (no version at all).Error reported on Discord:\n```python\nTraceback (most recent call last):\n  File \"C:\\Users\\videopro\\Documents\\github\\OpenPype\\openpype\\tools\\utils\\delegates.py\", line 65, in paint\n    text = self.displayText(\n  File \"C:\\Users\\videopro\\Documents\\github\\OpenPype\\openpype\\tools\\utils\\delegates.py\", line 33, in displayText\n    assert isinstance(value, numbers.Integral), (\nAssertionError: Version is not integer. \"None\" <class 'NoneType'>\n```\n- Right click menu would error on NOT FOUND entries, and thus not show. With this PR it will now _disregard_ not found items for \"Set version\" and \"Remove\" but still allow actions.This PR resolves those.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Kitsu: Sync OP with zou, make sure value-data is int or float <a href=\"https://github.com/ynput/OpenPype/pull/4596\">#4596</a></summary>\n\nCurrently the data zou pulls is a string and not a value causing some bugs in the pipe where a value is expected (like `Set frame range` in Fusion).\n\n\n\nThis PR makes sure each value is set with int() or float() so these bugs can't happen later on.\n\n\n\n_(A request to cgwire has also bin sent to allow force values only for some metadata columns, but currently the user can enter what ever they want in there)_\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Max: fix the bug of removing an instance <a href=\"https://github.com/ynput/OpenPype/pull/4617\">#4617</a></summary>\n\nfix the bug of removing an instance in 3dsMax\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global | Nuke: fixing farm publishing workflow <a href=\"https://github.com/ynput/OpenPype/pull/4623\">#4623</a></summary>\n\nAfter Nuke had adopted new publisher with new creators new issues were introduced. Those issues were addressed with this PR. Those are for example broken reviewable video files publishing if published via farm. Also fixed local publishing.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Ftrack additional families filtering <a href=\"https://github.com/ynput/OpenPype/pull/4633\">#4633</a></summary>\n\nFtrack family collector makes sure the subset family is also in instance families for additional families filtering.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Hierarchical <> Non-Hierarchical attributes sync fix <a href=\"https://github.com/ynput/OpenPype/pull/4635\">#4635</a></summary>\n\nSync between hierarchical and non-hierarchical attributes should be fixed and work as expected. Action should sync the values as expected and event handler should do it too and only on newly created entities.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>bugfix for 3dsmax publishing error <a href=\"https://github.com/ynput/OpenPype/pull/4637\">#4637</a></summary>\n\nfix the bug of failing publishing job in 3dsMax\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Use right validation for ffmpeg executable <a href=\"https://github.com/ynput/OpenPype/pull/4640\">#4640</a></summary>\n\nUse ffmpeg exec validation for ffmpeg executables instead of oiio exec validation. The validation is used as last possible source of ffmpeg from `PATH` environment variables, which is an edge case but can cause issues.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>3dsmax: opening last workfile <a href=\"https://github.com/ynput/OpenPype/pull/4644\">#4644</a></summary>\n\nSupports opening last saved workfile in 3dsmax host.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fixed a bug where a QThread in the splash screen could be destroyed before finishing execution <a href=\"https://github.com/ynput/OpenPype/pull/4647\">#4647</a></summary>\n\nThis should fix the occasional behavior of the QThread being destroyed before even its worker returns from the `run()` function.After quiting, it should wait for the QThread object to properly close itself.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Use right plugin class for Collect Comment <a href=\"https://github.com/ynput/OpenPype/pull/4653\">#4653</a></summary>\n\nCollect Comment plugin is instance plugin so should inherit from `InstancePlugin` instead of `ContextPlugin`.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: add tags field to thumbnail representation <a href=\"https://github.com/ynput/OpenPype/pull/4660\">#4660</a></summary>\n\nThumbnail representation might be missing tags field.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Integrator: Enforce unique destination transfers, disallow overwrites in queued transfers <a href=\"https://github.com/ynput/OpenPype/pull/4662\">#4662</a></summary>\n\nFix #4656 by enforcing unique destination transfers in the Integrator. It's now disallowed to a destination in the file transaction queue with a new source path during the publish.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Hiero: Creator with correct workfile numeric padding input <a href=\"https://github.com/ynput/OpenPype/pull/4666\">#4666</a></summary>\n\nCreator was showing 99 in workfile input for long time, even if users set default value to 1001 in studio settings. This has been fixed now.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Nukenodes family instance without frame range <a href=\"https://github.com/ynput/OpenPype/pull/4669\">#4669</a></summary>\n\nNo need to add frame range data into `nukenodes` (backdrop) family publishes - since those are timeless.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TVPaint: Optional Validation plugins can be de/activated by user <a href=\"https://github.com/ynput/OpenPype/pull/4674\">#4674</a></summary>\n\nAdded `OptionalPyblishPluginMixin` to TVpaint plugins that can be optional.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Kitsu: Slightly less strict with instance data <a href=\"https://github.com/ynput/OpenPype/pull/4678\">#4678</a></summary>\n\n- Allow to take task name from context if asset doesn't have any. Fixes an issue with Photoshop's review instance not having `task` in data.\n- Allow to match \"review\" against both `instance.data[\"family\"]` and `instance.data[\"families\"]` because some instances don't have the primary family in families, e.g. in Photoshop and TVPaint.\n- Do not error on Integrate Kitsu Review whenever for whatever reason Integrate Kitsu Note did not created a comment but just log the message that it was unable to connect a review.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Fix reset shortcut sequence <a href=\"https://github.com/ynput/OpenPype/pull/4694\">#4694</a></summary>\n\nFix bug created in https://github.com/ynput/OpenPype/pull/4676 where key sequence is checked using unsupported method. The check was changed to convert event into `QKeySequence` object which can be compared to prepared sequence.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Refactor _capture <a href=\"https://github.com/ynput/OpenPype/pull/4702\">#4702</a></summary>\n\nParagraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Hiero: correct container colors if UpToDate <a href=\"https://github.com/ynput/OpenPype/pull/4708\">#4708</a></summary>\n\nColors on loaded containers are now correctly identifying real state of version. `Red` for out of date and `green` for up to date.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Look Assigner: Move Look Assigner tool since it's Maya only <a href=\"https://github.com/ynput/OpenPype/pull/4604\">#4604</a></summary>\n\nFix #4357: Move Look Assigner tool to maya since it's Maya only\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Remove unused functions from Extract Look <a href=\"https://github.com/ynput/OpenPype/pull/4671\">#4671</a></summary>\n\nRemove unused functions from Maya Extract Look plug-in\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Extract Review code refactor <a href=\"https://github.com/ynput/OpenPype/pull/3930\">#3930</a></summary>\n\n<strong>Trying to reduce complexity of Extract Review plug-in\n- Re-use profile filtering from lib\n- Remove \"combination families\" additional filtering which supposedly was from OP v2\n- Simplify 'formatting' for filling gaps\n- Use `legacy_io.Session` over `os.environ`\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Replace last usages of Qt module <a href=\"https://github.com/ynput/OpenPype/pull/4610\">#4610</a></summary>\n\nReplace last usage of `Qt` module with `qtpy`. This change is needed for `PySide6` support. All changes happened in Maya loader plugins.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Update tests and documentation for  `ColormanagedPyblishPluginMixin` <a href=\"https://github.com/ynput/OpenPype/pull/4612\">#4612</a></summary>\n\nRefactor `ExtractorColormanaged` to `ColormanagedPyblishPluginMixin` in tests and documentation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Improve logging of used app + tool envs on application launch (minor tweak) <a href=\"https://github.com/ynput/OpenPype/pull/4686\">#4686</a></summary>\n\nUse `app.full_name` for change done in #4682\n\n\n___\n\n</details>\n\n### **📃 Documentation**\n\n\n<details>\n<summary>Docs/add architecture document <a href=\"https://github.com/ynput/OpenPype/pull/4344\">#4344</a></summary>\n\n<strong>Add `ARCHITECTURE.md` document.\n\n</strong>his document attemps to give a quick overview of the project to help onboarding, it's not an extensive documentation but more of a elevator pitch one-line descriptions of files/directories and what the attempt to do.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Documentation: Tweak grammar and fix some typos <a href=\"https://github.com/ynput/OpenPype/pull/4613\">#4613</a></summary>\n\nThis resolves some grammar and typos in the documentation.Also fixes the extension of some images in after effects docs which used uppercase extension even though files were lowercase extension.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Docs: Fix some minor grammar/typos <a href=\"https://github.com/ynput/OpenPype/pull/4680\">#4680</a></summary>\n\nTypo/grammar fixes in documentation.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>Maya: Implement image file node loader <a href=\"https://github.com/ynput/OpenPype/pull/4313\">#4313</a></summary>\n\n<strong>Implements a loader for loading texture image into a `file` node in Maya.\n\n</strong>Similar to Maya's hypershade creation of textures on load you have the option to choose for three modes of creating:\n- Texture\n- Projection\n- StencilThese should match what Maya generates if you create those in Maya.\n- [x] Load and manage file nodes\n- [x] Apply color spaces after #4195\n- [x] Support for _either_ UDIM or image sequence - currently it seems to always load sequences as UDIM automatically.\n- [ ] Add support for animation sequences of UDIM textures using the `<f>.<udim>.exr` path format?\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya Look Assigner: Don't rely on containers for get all assets <a href=\"https://github.com/ynput/OpenPype/pull/4600\">#4600</a></summary>\n\nThis resolves #4044 by not actually relying on containers in the scene but instead just rely on finding nodes with `cbId` attributes. As such, imported nodes would also be found and a shader can be assigned (similar to when using get from selection).**Please take into consideration the potential downsides below**Potential downsides would be:\n- IF an already loaded look has any dagNodes, say a 3D Projection node - then that will also show up as a loaded asset where previously nodes from loaded looks were ignored.\n- If any dag nodes were created locally - they would have gotten `cbId` attributes on scene save and thus the current asset would almost always show?\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Unify menu labels for \"Set Frame Range\" and \"Set Resolution\" <a href=\"https://github.com/ynput/OpenPype/pull/4605\">#4605</a></summary>\n\nFix #4109: Unify menu labels for \"Set Frame Range\" and \"Set Resolution\"This also tweaks it in Houdini from Reset Frame Range to Set Frame Range.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Resolve missing OPENPYPE_MONGO in deadline global job preload  <a href=\"https://github.com/ynput/OpenPype/pull/4484\">#4484</a></summary>\n\n<strong>In the GlobalJobPreLoad plugin, we propose to replace the SpawnProcess by a sub-process and to pass the environment variables in the parameters, since the SpawnProcess under Centos Linux does not pass the environment variables.\n\n</strong>In the GlobalJobPreLoad plugin, the Deadline SpawnProcess is used to start the OpenPype process. The problem is that the SpawnProcess does not pass environment variables, including OPENPYPE_MONGO, to the process when it is under Centos7 linux, and the process gets stuck. We propose to replace it by a subprocess and to pass the variable in the parameters.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Tests: Added setup_only to tests <a href=\"https://github.com/ynput/OpenPype/pull/4591\">#4591</a></summary>\n\nAllows to download test zip, unzip and restore DB in preparation for new test.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Arnold don't reset maya timeline frame range on render creation (or setting render settings) <a href=\"https://github.com/ynput/OpenPype/pull/4603\">#4603</a></summary>\n\nFix #4429: Do not reset fps or playback timeline on applying or creating render settings\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Bump @sideway/formula from 3.0.0 to 3.0.1 in /website <a href=\"https://github.com/ynput/OpenPype/pull/4609\">#4609</a></summary>\n\nBumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1.\n<details>\n<summary>Commits</summary>\n<ul>\n<li><a href=\"https://github.com/hapijs/formula/commit/5b44c1bffc38135616fb91d5ad46eaf64f03d23b\"><code>5b44c1b</code></a> 3.0.1</li>\n<li><a href=\"https://github.com/hapijs/formula/commit/9fbc20a02d75ae809c37a610a57802cd1b41b3fe\"><code>9fbc20a</code></a> chore: better number regex</li>\n<li><a href=\"https://github.com/hapijs/formula/commit/41ae98e0421913b100886adb0107a25d552d9e1a\"><code>41ae98e</code></a> Cleanup</li>\n<li><a href=\"https://github.com/hapijs/formula/commit/c59f35ec401e18cead10e0cedfb44291517610b1\"><code>c59f35e</code></a> Move to Sideway</li>\n<li>See full diff in <a href=\"https://github.com/sideway/formula/compare/v3.0.0...v3.0.1\">compare view</a></li>\n</ul>\n</details>\n<details>\n<summary>Maintainer changes</summary>\n<p>This version was pushed to npm by <a href=\"https://www.npmjs.com/~marsup\">marsup</a>, a new releaser for <code>@​sideway/formula</code> since your current version.</p>\n</details>\n<br />\n\n\n[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@sideway/formula&package-manager=npm_and_yarn&previous-version=3.0.0&new-version=3.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)\n\nDependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.\n\n[//]: # (dependabot-automerge-start)\n[//]: # (dependabot-automerge-end)\n\n---\n\n<details>\n<summary>Dependabot commands and options</summary>\n<br />\n\nYou can trigger Dependabot actions by commenting on this PR:\n- `@dependabot rebase` will rebase this PR\n- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it\n- `@dependabot merge` will merge this PR after your CI passes on it\n- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it\n- `@dependabot cancel merge` will cancel a previously requested merge and block automerging\n- `@dependabot reopen` will reopen this PR if it is closed\n- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually\n- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language\n- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language\n- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language\n- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language\n\nYou can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts).\n\n</details>\n___\n\n</details>\n\n\n<details>\n<summary>Update artist_hosts_maya_arnold.md <a href=\"https://github.com/ynput/OpenPype/pull/4626\">#4626</a></summary>\n\nCorrect Arnold docs.\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Add \"Include Parent Hierarchy\" option in animation creator plugin <a href=\"https://github.com/ynput/OpenPype/pull/4645\">#4645</a></summary>\n\nAdd an option in Project Settings > Maya > Creator Plugins > Create Animation to include (or not) parent hierarchy. This is to avoid artists to check manually the option for all create animation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Filter available applications <a href=\"https://github.com/ynput/OpenPype/pull/4667\">#4667</a></summary>\n\nAdded option to filter applications that don't have valid executable available in settings in launcher and ftrack actions. This option can be disabled in new settings category `Applications`. The filtering is by default disabled.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>3dsmax: make sure that startup script executes <a href=\"https://github.com/ynput/OpenPype/pull/4695\">#4695</a></summary>\n\nFixing reliability of OpenPype startup in 3dsmax.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Project Manager: Change minimum frame start/end to '0' <a href=\"https://github.com/ynput/OpenPype/pull/4719\">#4719</a></summary>\n\nProject manager can have frame start/end set to `0`.\n\n\n___\n\n</details>\n\n\n\n## [3.15.2](https://github.com/ynput/OpenPype/tree/3.15.2)\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.1...3.15.2)\n\n### **🆕 New features**\n\n\n<details>\n<summary>maya gltf texture convertor and validator <a href=\"https://github.com/ynput/OpenPype/pull/4261\">#4261</a></summary>\n\n<strong>Continuity of the gltf extractor implementation\n\n</strong>Continuity of the gltf extractor https://github.com/pypeclub/OpenPype/pull/4192UPDATE:**Validator for GLSL Shader**:  Validate whether the mesh uses GLSL Shader. If not it will error out. The user can choose to perform the repair action and it will help to assign glsl shader. If the mesh with Stringray PBS, the repair action will also check to see if there is any linked texture such as Color, Occulsion, and Normal Map. If yes, it will help to relink the related textures to the glsl shader.*****If the mesh uses the PBS Shader,\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: New Publisher <a href=\"https://github.com/ynput/OpenPype/pull/4370\">#4370</a></summary>\n\n<strong>Implementation of the new publisher for Unreal.\n\n</strong>The implementation of the new publisher for Unreal. This PR includes the changes for all the existing creators to be compatible with the new publisher.The basic creator has been split in two distinct creators:\n- `UnrealAssetCreator`, works with assets in the Content Browser.\n- `UnrealActorCreator` that works with actors in the scene.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Implementation of a new splash screen <a href=\"https://github.com/ynput/OpenPype/pull/4592\">#4592</a></summary>\n\nImplemented a new splash screen widget to reflect a process running in the background. This widget can be used for other tasks than UE. **Also fixed the compilation error of the AssetContainer.cpp when trying to build the plugin in UE 5.0**\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline for 3dsMax <a href=\"https://github.com/ynput/OpenPype/pull/4439\">#4439</a></summary>\n\n<strong>Setting up deadline for 3dsmax\n\n</strong>Setting up deadline for 3dsmax by setting render outputs and viewport camera\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: adding nukeassist  <a href=\"https://github.com/ynput/OpenPype/pull/4494\">#4494</a></summary>\n\n<strong>Adding support for NukeAssist\n\n</strong>For support of NukeAssist we had to limit some Nuke features since NukeAssist itself Nuke with limitations. We do not support Creator and Publisher. User can only Load versions with version control. User can also set Framerange and Colorspace.\n\n\n___\n\n</details>\n\n### **🚀 Enhancements**\n\n\n<details>\n<summary>Maya: OP-2630 acescg maya <a href=\"https://github.com/ynput/OpenPype/pull/4340\">#4340</a></summary>\n\n<strong>Resolves #2712\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Default Ftrack Family on RenderLayer <a href=\"https://github.com/ynput/OpenPype/pull/4458\">#4458</a></summary>\n\n<strong>With default settings, renderlayers in Maya were not being tagged with the Ftrack family leading to confusion when doing reviews.\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Maya Playblast Options - OP-3783 <a href=\"https://github.com/ynput/OpenPype/pull/4487\">#4487</a></summary>\n\n<strong>Replacement PR for #3912. Adds more options for playblasts to preferences/settings.\n\n</strong>Adds the following as options in generating playblasts, matching viewport settings.\n- Use default material\n- Wireframe on shaded\n- X-ray\n- X-ray Joints\n- X-ray active component\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Passing custom attributes to alembic - OP-4111 <a href=\"https://github.com/ynput/OpenPype/pull/4516\">#4516</a></summary>\n\n<strong>Passing custom attributes to alembic\n\n</strong>This PR makes it possible to pass all user defined attributes along to the alembic representation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Options for VrayProxy output - OP-2010 <a href=\"https://github.com/ynput/OpenPype/pull/4525\">#4525</a></summary>\n\n<strong>Options for output of VrayProxy.\n\n</strong>Client requested more granular control of output from VrayProxy instance. Exposed options on the instance and settings for vrmesh and alembic.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Validate missing instance attributes <a href=\"https://github.com/ynput/OpenPype/pull/4559\">#4559</a></summary>\n\n<strong>Validate missing instance attributes.\n\n</strong>New attributes can be introduced as new features come in. Old instances will need to be updated with these attributes for the documentation to make sense, and users do not have to recreate the instances.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Refactored Generation of UE Projects, installation of plugins moved to the engine <a href=\"https://github.com/ynput/OpenPype/pull/4369\">#4369</a></summary>\n\n<strong>Improved the way how OpenPype works with generation of UE projects. Also the installation of the plugin has been altered to install into the engine\n\n</strong>OpenPype now uses the appropriate tools to generate UE projects. Unreal Build Tool (UBT) and a \"Commandlet Project\" is needed to properly generate a BP project, or C++ code in case that `dev_mode = True`, folders, the .uproject file and many other resources.On the plugin's side, it is built seperately with the UnrealAutomationTool (UAT) and then it's contents are moved under the `Engine/Plugins/Marketplace/OpenPype` directory.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: Use client functions in Layout loader  <a href=\"https://github.com/ynput/OpenPype/pull/4578\">#4578</a></summary>\n\n<strong>Use 'get_representations' instead of 'legacy_io' query in layout loader.\n\n</strong>This is removing usage of `find_one` called on `legacy_io` and use rather client functions as preparation for AYON connection. Also all representations are queried at once instead of one by one.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Support for extensions filtering in loaders <a href=\"https://github.com/ynput/OpenPype/pull/4492\">#4492</a></summary>\n\n<strong>Added extensions filtering support to loader plugins.\n\n</strong>To avoid possible backwards compatibility break is filtering exactly the same and filtering by extensions is enabled only if class attribute 'extensions' is set.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: multiple reformat in baking review profiles <a href=\"https://github.com/ynput/OpenPype/pull/4514\">#4514</a></summary>\n\n<strong>Added support for multiple reformat nodes in baking profiles.\n\n</strong>Old settings for single reformat node is supported and prioritised just in case studios are using it and backward compatibility is needed. Warnings in Nuke terminal are notifying users to switch settings to new workflow. Settings are also explaining the migration way.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Add option to use new creating system in workfile template builder <a href=\"https://github.com/ynput/OpenPype/pull/4545\">#4545</a></summary>\n\n<strong>Nuke workfile template builder can use new creators instead of legacy creators.\n\n</strong>Modified workfile template builder to have option to say if legacy creators should be used or new creators. Legacy creators are disabled by default, so Maya has changed the value.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global, Nuke: Workfile first version with template processing <a href=\"https://github.com/ynput/OpenPype/pull/4579\">#4579</a></summary>\n\n<strong>Supporting new template workfile builder with toggle for creation of first version of workfile in case there is none yet.\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: New Publisher <a href=\"https://github.com/ynput/OpenPype/pull/4523\">#4523</a></summary>\n\n<strong>This is an updated PR for @BigRoy 's old PR (https://github.com/ynput/OpenPype/pull/3892).I have merged it with code from OP 3.15.1-nightly.6 and made sure it works as expected.This converts the old publishing system to the new one. It implements Fusion as a new host addon.\n\n</strong>\n- Create button removed in OpenPype menu in favor of the new Publisher\n- Draft refactor validations to raise PublishValidationError\n- Implement Creator for New Publisher\n- Implement Fusion as Host addon\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TVPaint: Use Publisher tool <a href=\"https://github.com/ynput/OpenPype/pull/4471\">#4471</a></summary>\n\n<strong>Use Publisher tool and new creation system in TVPaint integration.\n\n</strong>Using new creation system makes TVPaint integration a little bit easier to maintain for artists. Removed unneeded tools Creator and Subset Manager tools. Goal is to keep the integration work as close as possible to previous integration. Some changes were made but primarilly because they were not right using previous system.All creators create instance with final family instead of changing the family during extraction. Render passes are not related to group id but to render layer instance. Render layer is still related to group. Workfile, review and scene render instances are created using autocreators instead of auto-collection during publishing. Subset names are fully filled during publishing but instance labels are filled on refresh with the last known right value. Implemented basic of legacy convertor which should convert render layers and render passes.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TVPaint: Auto-detect render creation <a href=\"https://github.com/ynput/OpenPype/pull/4496\">#4496</a></summary>\n\n<strong>Create plugin which will create Render Layer and Render Pass instances based on information in the scene.\n\n</strong>Added new creator that must be triggered by artist. The create plugin will first create Render Layer instances if were not created yet. For variant is used color group name. The creator has option to rename color groups by template defined in settings -> Template may use index of group by it's usage in scene (from bottom to top). After Render Layers will create Render Passes. Render Pass is created for each individual TVPaint layer in any group that had created Render Layer. It's name is used as variant (pass).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TVPaint: Small enhancements <a href=\"https://github.com/ynput/OpenPype/pull/4501\">#4501</a></summary>\n\n<strong>Small enhancements in TVPaint integration which did not get to https://github.com/ynput/OpenPype/pull/4471.\n\n</strong>It was found out that `opacity` returned from `tv_layerinfo` is always empty and is dangerous to add it to layer information. Added information about \"current\" layer to layers information. Disable review of Render Layer and Render Pass instances by default. In most of productions is used only \"scene review\". Skip usage of `\"enabled\"` key from settings in automated layer/pass creation.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Global: color v3 global oiio transcoder plugin <a href=\"https://github.com/ynput/OpenPype/pull/4291\">#4291</a></summary>\n\n<strong>Implements possibility to use `oiiotool` to transcode image sequences from one color space to another(s).\n\n</strong>Uses collected `colorspaceData` information about source color spaces, these information needs to be collected previously in each DCC interested in color management.Uses profiles configured in Settings to create single or multiple new representations (and file extensions) with different color spaces.New representations might replace existing one, each new representation might contain different tags and custom tags to control its integration step.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: Added support for multiple install dirs in Deadline <a href=\"https://github.com/ynput/OpenPype/pull/4451\">#4451</a></summary>\n\n<strong>SearchDirectoryList returns FIRST existing so if you would have multiple OP install dirs, it won't search for appropriate version in later ones.\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Ftrack: Upload reviewables with original name <a href=\"https://github.com/ynput/OpenPype/pull/4483\">#4483</a></summary>\n\n<strong>Ftrack can integrate reviewables with original filenames.\n\n</strong>As ftrack have restrictions about names of components the only way how to achieve the result was to upload the same file twice, one with required name and one with origin name.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>TVPaint: Ignore transparency in Render Pass <a href=\"https://github.com/ynput/OpenPype/pull/4499\">#4499</a></summary>\n\n<strong>It is possible to ignore layers transparency during Render Pass extraction.\n\n</strong>Render pass extraction does not respect opacity of TVPaint layers set in scene during extraction. It can be enabled/disabled in settings.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Anatomy: Preparation for different root overrides <a href=\"https://github.com/ynput/OpenPype/pull/4521\">#4521</a></summary>\n\n<strong>Prepare Anatomy to handle only 'studio' site override on it's own.\n\n</strong>Change how Anatomy fill root overrides based on requested site name. The logic which decide what is active site was moved to sync server addon and the same for receiving root overrides of local site. The Anatomy resolve only studio site overrides anything else is handled by sync server. BaseAnatomy only expect root overrides value and does not need site name. Validation of site name happens in sync server same as resolving if site name is local or not.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke | Global: colormanaged plugin in collection <a href=\"https://github.com/ynput/OpenPype/pull/4556\">#4556</a></summary>\n\n<strong>Colormanaged extractor had changed to Mixin class so it can be added to any stage of publishing rather then just to Exctracting.Nuke is no collecting colorspaceData to representation collected on already rendered images.\n\n</strong>Mixin class can no be used as secondary  parent in publishing plugins.\n\n\n___\n\n</details>\n\n### **🐛 Bug fixes**\n\n\n<details>\n<summary>look publishing and srgb colorspace in maya  <a href=\"https://github.com/ynput/OpenPype/pull/4276\">#4276</a></summary>\n\n<strong>Check the OCIO color management is enabled before doing linearize colorspace for converting the texture maps into tx files.\n\n</strong>Check whether the OCIO color management is enabled before the condition of converting the texture to tx extension.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: extract Thumbnail \"No active model panel found\" - OP-3849 <a href=\"https://github.com/ynput/OpenPype/pull/4421\">#4421</a></summary>\n\n<strong>Error when extracting playblast with no model panel.\n\n</strong>If `project_settings/maya/publish/ExtractPlayblast/capture_preset/Viewport Options/override_viewport_options` were off and publishing without showing any model panel, the extraction would fail.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix setting scene fps with float input <a href=\"https://github.com/ynput/OpenPype/pull/4488\">#4488</a></summary>\n\n<strong>Returned value of float fps on integer values would return float.\n\n</strong>This PR fixes the case when switching between integer fps values for example 24 > 25. Issue was when setting the scene fps, the original float value was used which makes it unpredictable whether the value is float or integer when mapping the fps values.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Multipart fix <a href=\"https://github.com/ynput/OpenPype/pull/4497\">#4497</a></summary>\n\n<strong>Fix multipart logic in render products.\n\n</strong>Each renderer has a different way of defining whether output images is multipart, so we need to define it for each renderer. Also before the `multipart` class variable was defined multiple times in several places, which made it tricky to debug where `multipart` was defined. Now its created on initialization and referenced as `self.multipart`\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Set pool on tile assembly - OP-2012 <a href=\"https://github.com/ynput/OpenPype/pull/4520\">#4520</a></summary>\n\n<strong>Set pool on tile assembly\n\n</strong>Pool for publishing and tiling jobs, need to use the settings (`project_settings/deadline/publish/ProcessSubmittedJobOnFarm/deadline_pool`) else fallback on primary pool (`project_settings/deadline/publish/CollectDeadlinePools/primary_pool`)\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Extract review with handles <a href=\"https://github.com/ynput/OpenPype/pull/4527\">#4527</a></summary>\n\n<strong>Review was not extracting properly with/without handles.\n\n</strong>Review instance was not created properly resulting in the frame range on the instance including handles.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix broken lib. <a href=\"https://github.com/ynput/OpenPype/pull/4529\">#4529</a></summary>\n\n<strong>Fix broken lib.\n\n</strong>This commit from this PR broke the Maya lib module.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Validate model name - OP-4983 <a href=\"https://github.com/ynput/OpenPype/pull/4539\">#4539</a></summary>\n\n<strong>Validate model name issues.\n\n</strong>Couple of issues with validate model name;\n- missing platform extraction from settings\n- map function should be list comprehension\n- code cosmetics\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: SkeletalMesh family loadable as reference <a href=\"https://github.com/ynput/OpenPype/pull/4573\">#4573</a></summary>\n\n<strong>In Maya, fix the SkeletalMesh family not loadable as reference.\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Unreal: fix loaders because of missing AssetContainer <a href=\"https://github.com/ynput/OpenPype/pull/4536\">#4536</a></summary>\n\n<strong>Fixing Unreal loaders, where changes in OpenPype Unreal integration plugin deleted AssetContainer.\n\n</strong>`AssetContainer` and `AssetContainerFactory` are still used to mark loaded instances. Because of optimizations in Integration plugin we've accidentally removed them but that broke loader.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>3dsmax unable to delete loaded asset in the scene inventory <a href=\"https://github.com/ynput/OpenPype/pull/4507\">#4507</a></summary>\n\n<strong>Fix the bug of being unable to delete loaded asset in the Scene Inventory\n\n</strong>Fix the bug of being unable to delete loaded asset in the Scene Inventory\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Hiero/Nuke: originalBasename editorial publishing and loading <a href=\"https://github.com/ynput/OpenPype/pull/4453\">#4453</a></summary>\n\n<strong>Publishing and loading `originalBasename` is working as expected\n\n</strong>Frame-ranges on version document is now correctly defined to fit original media frame range which is published. It means loading is now correctly identifying frame start and end on clip loader in Nuke.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: Fix workfile template placeholder creation <a href=\"https://github.com/ynput/OpenPype/pull/4512\">#4512</a></summary>\n\n<strong>Template placeholder creation was erroring out in Nuke due to the Workfile template builder not being able to find any of the plugins for the Nuke host.\n\n</strong>Move `get_workfile_build_placeholder_plugins` function to NukeHost class as workfile template builder expects.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: creator farm attributes from deadline submit plugin settings <a href=\"https://github.com/ynput/OpenPype/pull/4519\">#4519</a></summary>\n\n<strong>Defaults in farm attributes are sourced from settings.\n\n</strong>Settings for deadline nuke submitter are now used during nuke render and prerender creator plugins.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Nuke: fix clip sequence loading <a href=\"https://github.com/ynput/OpenPype/pull/4574\">#4574</a></summary>\n\n<strong>Nuke is loading correctly clip  from image sequence created without \"{originalBasename}\" token in anatomy template.\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Fix files collection and small bug-fixes <a href=\"https://github.com/ynput/OpenPype/pull/4423\">#4423</a></summary>\n\n<strong>Fixed Fusion review-representation and small bug-fixes\n\n</strong>This fixes the problem with review-file generation that stopped the publishing on second publish before the fix.The problem was that Fusion simply looked at all the files in the render-folder instead of only gathering the needed frames for the review.Also includes a fix to get the handle start/end that before throw an error if the data didn't exist (like from a kitsu sync).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Updated render_local.py to not only process the first instance <a href=\"https://github.com/ynput/OpenPype/pull/4522\">#4522</a></summary>\n\nMoved the `__hasRun` to `render_once()` so the check only happens with the rendering. Currently only the first render node gets the representations added.Critical PR\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Load sequence fix filepath resolving from representation <a href=\"https://github.com/ynput/OpenPype/pull/4580\">#4580</a></summary>\n\n<strong>Resolves issue mentioned on discord by @movalex:The loader was incorrectly trying to find the file in the publish folder which resulted in just picking 'any first file'.\n\n</strong>This gets the filepath from representation instead of taking the first file from listing files from publish folder.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: Fix review burnin start and end frame <a href=\"https://github.com/ynput/OpenPype/pull/4590\">#4590</a></summary>\n\nFix the burnin start and end frame for reviews. Without this the asset document's start and end handle would've been added to the _burnin_ frame range even though that would've been incorrect since the handles are based on the comp saver's render range instead.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Harmony: missing set of frame range when opening scene <a href=\"https://github.com/ynput/OpenPype/pull/4485\">#4485</a></summary>\n\n<strong>Frame range gets set from DB everytime scene is opened.\n\n</strong>Added also check for not up-to-date loaded containers.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Photoshop: context is not changed in publisher <a href=\"https://github.com/ynput/OpenPype/pull/4570\">#4570</a></summary>\n\n<strong>When PS is already open and artists launch new task, it should keep only opened PS open, but change context.\n\n</strong>Problem were occurring in Workfile app where under new task files from old task were shown. This fixes this and adds opening of last workfile for new context if workfile exists.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>hiero: fix effect item node class <a href=\"https://github.com/ynput/OpenPype/pull/4543\">#4543</a></summary>\n\n<strong>Collected effect name after renaming is saving correct class name.\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Bugfix/OP-4616 vray multipart <a href=\"https://github.com/ynput/OpenPype/pull/4297\">#4297</a></summary>\n\n<strong>This fixes a bug where multipart vray renders would not make a review in Ftrack.\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Maya: Fix changed location of reset_frame_range <a href=\"https://github.com/ynput/OpenPype/pull/4491\">#4491</a></summary>\n\n<strong>Location in commands caused cyclic import\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>global: source template fixed frame duplication <a href=\"https://github.com/ynput/OpenPype/pull/4503\">#4503</a></summary>\n\n<strong>Duplication is not happening.\n\n</strong>Template is using `originalBasename` which already assume all necessary elements are part of the file name so there was no need for additional optional name elements.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: Hint to use Python 3 <a href=\"https://github.com/ynput/OpenPype/pull/4518\">#4518</a></summary>\n\n<strong>Added shebank to give deadline hint which python should be used.\n\n</strong>Deadline has issues with Python 2 (especially with `os.scandir`). When a shebank is added to file header deadline will use python 3 mode instead of python 2 which fix the issue.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Publisher: Prevent access to create tab after publish start <a href=\"https://github.com/ynput/OpenPype/pull/4528\">#4528</a></summary>\n\n<strong>Prevent access to create tab after publish start.\n\n</strong>Disable create button in instance view on publish start and enable it again on reset. Even with that make sure that it is not possible to go to create tab if the tab is disabled.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Color Transcoding: store target_colorspace as new colorspace <a href=\"https://github.com/ynput/OpenPype/pull/4544\">#4544</a></summary>\n\n<strong>When transcoding into new colorspace, representation must carry this information instead original color space.\n\n</strong>\n___\n\n</details>\n\n\n<details>\n<summary>Deadline: fix submit_publish_job <a href=\"https://github.com/ynput/OpenPype/pull/4552\">#4552</a></summary>\n\n<strong>Fix submit_publish_job\n\n</strong>Resolves #4541\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Kitsu: Fix task itteration in update-op-with-zou <a href=\"https://github.com/ynput/OpenPype/pull/4577\">#4577</a></summary>\n\nFrom the last PR (https://github.com/ynput/OpenPype/pull/4425) a comment-commit last second messed up the code and resulted in two lines being the same, crashing the script. This PR fixes that.\n___\n\n</details>\n\n\n<details>\n<summary>AttrDefs: Fix type for PySide6 <a href=\"https://github.com/ynput/OpenPype/pull/4584\">#4584</a></summary>\n\n<strong>Use right type in signal emit for value change of attribute definitions.\n\n</strong>Changed `UUID` type to `str`. This is not an issue with PySide2 but it is with PySide6.\n\n\n___\n\n</details>\n\n### **🔀 Refactored code**\n\n\n<details>\n<summary>Scene Inventory: Avoid using ObjectId <a href=\"https://github.com/ynput/OpenPype/pull/4524\">#4524</a></summary>\n\n<strong>Avoid using conversion to ObjectId type in scene inventory tool.\n\n</strong>Preparation for AYON compatibility where ObjectId won't be used for ids. Representation ids from loaded containers are not converted to ObjectId but kept as strings which also required some changes when working with representation documents.\n\n\n___\n\n</details>\n\n### **Merged pull requests**\n\n\n<details>\n<summary>SiteSync: host dirmap is not working properly <a href=\"https://github.com/ynput/OpenPype/pull/4563\">#4563</a></summary>\n\n<strong>If artists uses SiteSync with real remote (gdrive, dropbox, sftp) drive, Local Settings were throwing error `string indices must be integers`.\n\n</strong>Logic was reworked to provide only `local_drive` values to be overrriden by Local Settings. If remote site is `gdrive` etc. mapping to `studio` is provided as it is expected that workfiles will have imported from `studio` location and not from `gdrive` folder.Also Nuke dirmap was reworked to be less verbose and much faster.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>General: Input representation ids are not ObjectIds <a href=\"https://github.com/ynput/OpenPype/pull/4576\">#4576</a></summary>\n\n<strong>Don't use `ObjectId` as representation ids during publishing.\n\n</strong>Representation ids are kept as strings during publishing instead of converting them to `ObjectId`. This change is pre-requirement for AYON connection.Inputs are used for integration of links and for farm publishing (or at least it looks like).\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Shotgrid: Fixes on Deadline submissions <a href=\"https://github.com/ynput/OpenPype/pull/4498\">#4498</a></summary>\n\n<strong>A few other bug fixes for getting Nuke submission to Deadline work smoothly using Shotgrid integration.\n\n</strong>Continuing on the work done on this other PR this fixes a few other bugs I came across with further tests.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Fusion: New Publisher <a href=\"https://github.com/ynput/OpenPype/pull/3892\">#3892</a></summary>\n\n<strong>This converts the old publishing system to the new one. It implements Fusion as a new host addon.\n\n</strong>\n- Create button removed in OpenPype menu in favor of the new Publisher\n- Draft refactor validations to raise `PublishValidationError`\n- Implement Creator for New Publisher\n- Implement Fusion as Host addon\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Make Kitsu work with Tray Publisher, added kitsureview tag, fixed sync-problems. <a href=\"https://github.com/ynput/OpenPype/pull/4425\">#4425</a></summary>\n\n<strong>Make Kitsu work with Tray Publisher, added kitsureview tag, fixed sync-problems.\n\n</strong>This PR updates the way the module gather info for the current publish so it now works with Tray Publisher.It fixes the data that gets synced from Kitsu to OP so all needed data gets registered even if it doesn't exist on Kitsus side.It also adds the tag \"Add review to Kitsu\" and adds it to Burn In so previews gets generated by default to Kitsu.\n\n\n___\n\n</details>\n\n\n<details>\n<summary>Maya: V-Ray Set Image Format from settings <a href=\"https://github.com/ynput/OpenPype/pull/4566\">#4566</a></summary>\n\n<strong>Resolves #4565\n\n</strong>Set V-Ray Image Format using settings.\n\n\n___\n\n</details>\n\n\n\n\n## [3.15.1](https://github.com/ynput/OpenPype/tree/3.15.1)\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.0...3.15.1)\n\n### **🆕 New features**\n\n\n\n\n<details>\n<summary>Maya: Xgen (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ maya</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4256\">#4256</a></summary>\n\n___\n\n#### Brief description\n\nInitial Xgen implementation.\n\n\n\n#### Description\n\nClient request of Xgen pipeline.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Data exchange cameras for 3d Studio Max (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ 3dsmax</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4376\">#4376</a></summary>\n\n___\n\n#### Brief description\n\nAdd Camera Family into the 3d Studio Max\n\n\n\n#### Description\n\nAdding Camera Extractors(extract abc camera and extract fbx camera) and validators(for camera contents) into 3dMaxAlso add the extractor for exporting 3d max raw scene (which is also related to 3dMax Scene Family) for camera family\n\n\n\n\n___\n\n</details>\n\n\n### **🚀 Enhancements**\n\n\n\n\n<details>\n<summary>Adding path validator for non-maya nodes (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ maya</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4271\">#4271</a></summary>\n\n___\n\n#### Brief description\n\nAdding a path validator for filepaths from non-maya nodes, which are created by plugins such as Renderman, Yeti and abcImport.\n\n\n\n#### Description\n\nAs File Path Editor cannot catch the wrong filenpaths from non-maya nodes such as AlembicNodes, It is neccessary to have a new validator to ensure the existence of the filepaths from the nodes.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Deadline: Allow disabling strict error check in Maya submissions (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ maya</font></i> <i><font style='color:#1E1B7B';>/ deadline</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4420\">#4420</a></summary>\n\n___\n\n#### Brief description\n\nDL by default has Strict error checking, but some errors are not fatal.\n\n\n\n#### Description\n\nThis allows to set profile based on Task and Subset values to temporarily disable Strict Error Checks.Subset and task names should support regular expressions. (not wildcard notation though).\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Houdini: New publisher code tweak (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ houdini</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4374\">#4374</a></summary>\n\n___\n\n#### Brief description\n\nThis is cosmetics only - the previous code to me felt quite unreadable due to the lengthy strings being used.\n\n\n\n#### Description\n\nCode should do roughly the same, but just be reformatted.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>3dsmax: enhance alembic loader update function (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ 3dsmax</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4387\">#4387</a></summary>\n\n___\n\n## Enhancement\n\n\n\nThis PR is adding update/switch ability to pointcache/alembic loader in 3dsmax and fixing wrong tool shown when clicking on \"Manage\" item on OpenPype menu, that is now correctly Scene Inventory (but was Subset Manager).\n\n\n\nAlembic update has still one caveat - it doesn't cope with changed number of object inside alembic, since loading alembic in max involves creating all those objects as first class nodes. So it will keep the objects in scene, just update path to alembic file on them.\n___\n\n</details>\n\n\n\n<details>\n<summary>Global: supporting `OPENPYPE_TMPDIR` in staging dir maker (<i><font color='#367F6C';>editorial</font> </i> <i><font style='color:#365E7F';>/ hiero</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4398\">#4398</a></summary>\n\n___\n\n#### Brief description\n\nProductions can use OPENPYPE_TMPDIR for staging temp publishing directory\n\n\n\n#### Description\n\nStudios were demanding to be able to configure their own shared storages as temporary staging directories. Template formatting is also supported with optional keys formatting and following anatomy keys:    - root[work | <root name key>]    - project[name | code]\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>General: Functions for current context (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4324\">#4324</a></summary>\n\n___\n\n#### Brief description\n\nDefined more functions to receive current context information and added the methods to host integration so host can affect the result.\n\n\n\n#### Description\n\nThis is one of steps to reduce usage of `legacy_io.Session`. This change define how to receive current context information -> call functions instead of accessing `legacy_io.Session` or `os.environ` directly. Plus, direct access on session or environments is unfortunatelly not enough for some DCCs where multiple workfiles can be opened at one time which can heavily affect the context but host integration sometimes can't affect that at all.`HostBase` already had implemented `get_current_context`, that was enhanced by adding more specific methods `get_current_project_name`, `get_current_asset_name` and `get_current_task_name`. The same functions were added to `~/openpype/pipeline/cotext_tools.py`. The functions in context tools are calling host integration methods (if are available) otherwise are using environent variables as default implementation does. Also was added `get_current_host_name` to receive host name from registered host if is available or from environment variable.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Houdini: Do not visualize the hidden OpenPypeContext node (<i><font color='#367F6C';>other</font> </i> <i><font style='color:#365E7F';>/ houdini</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4382\">#4382</a></summary>\n\n___\n\n#### Brief description\n\nUsing the new publisher UI would generate a visible 'null' locator at the origin. It's confusing to the user since it's supposed to be 'hidden'.\n\n\n\n#### Description\n\nBefore this PR the user would see a locator/null at the origin which was the 'hidden' `/obj/OpenPypeContext` node. This null would suddenly appear if the user would've ever opened the Publisher UI once.After this PR it will not show:Nice and tidy.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Maya + Blender: Pyblish plugins removed unused `version` and `category` attributes (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4402\">#4402</a></summary>\n\n___\n\n#### Brief description\n\nOnce upon a time in a land far far away there lived a few plug-ins who felt like they didn't belong in generic boxes and felt they needed to be versioned well above others. They tried, but with no success.\n\n\n\n#### Description\n\nEven though they now lived in a universe with elaborate `version` and `category` attributes embedded into their tiny little plug-in DNA this particular deviation has been greatly unused. There is nothing special about the version, nothing special about the category.It does nothing.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>General: Fix original basename frame issues (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4452\">#4452</a></summary>\n\n___\n\n#### Brief description\n\nTreat `{originalBasename}` in different way then standard files processing. In case template should use `{originalBasename}` the transfers will use them as they are without any changes or handling of frames.\n\n\n\n#### Description\n\nFrames handling is problematic with original basename because their padding can't be defined to match padding in source filenames. Also it limits the usage of functionality to \"must have frame at end of fiename\". This is proposal how that could be solved by simply ignoring frame handling and using filenames as are on representation. First frame is still stored to representation context but is not used in formatting part. This way we don't have to care about padding of frames at all.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Publisher: Report also crashed creators and convertors (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4473\">#4473</a></summary>\n\n___\n\n#### Brief description\n\nAdded crashes of creators and convertos discovery (lazy solution).\n\n\n\n#### Description\n\nReport in Publisher also contains information about crashed files caused during creator plugin discovery and convertor plugin discovery. They're not separated into categroies and there is no other information in the report about them, but this helps a lot during development. This change does not need to change format/schema of the report nor UI logic.\n\n\n\n\n___\n\n</details>\n\n\n### **🐛 Bug fixes**\n\n\n\n\n<details>\n<summary>Maya: Fix Validate Attributes plugin (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ maya</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4401\">#4401</a></summary>\n\n___\n\n#### Brief description\n\nCode was broken. So either plug-in was unused or it had gone unnoticed.\n\n\n\n#### Description\n\nLooking at the commit history of the plug-in itself it seems this might have been broken somewhere between two to three years. I think it's broken since two years since this commit.Should this plug-in be removed completely?@tokejepsen Is there still a use case where we should have this plug-in? (You created the original one)\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Maya: Ignore workfile lock in Untitled scene (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ maya</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4414\">#4414</a></summary>\n\n___\n\n#### Brief description\n\nSkip workfile lock check if current scene is 'Untitled'.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Maya: fps rounding - OP-2549 (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ maya</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4424\">#4424</a></summary>\n\n___\n\n#### Brief description\n\nWhen FPS is registered in for example Ftrack and round either down or up (floor/ceil), comparing to Maya FPS can fail. Example:23.97 (Ftrack/Mongo) != 23.976023976023978 (Maya)\n\n\n\n#### Description\n\nSince Maya only has a select number of supported framerates, I've taken the approach of converting any fps to supported framerates in Maya. We validate the input fps to make sure they are supported in Maya in two ways:Whole Numbers - are validated straight against the supported framerates in Maya.Demical Numbers - we find the closest supported framerate in Maya. If the difference to the closest supported framerate, is more than 0.5 we'll throw an error.If Maya ever supports arbitrary framerates, then we might have a problem but I'm not holding my breath...\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Strict Error Checking Default (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ maya</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4457\">#4457</a></summary>\n\n___\n\n#### Brief description\n\nProvide default of strict error checking for instances created prior to PR.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Create: Enhance instance & context changes (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ houdini,after effects,3dsmax</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4375\">#4375</a></summary>\n\n___\n\n#### Brief description\n\nChanges of instances and context have complex, hard to get structure. The structure did not change but instead of complex dictionaries are used objected data.\n\n\n\n#### Description\n\nThis is poposal of changes data improvement for creators. Implemented `TrackChangesItem` which handles the changes for us. The item is creating changes based on old and new value and can provide information about changed keys or access to full old or new value. Can give the values on any \"sub-dictionary\".Used this new approach to fix change in houdini and 3ds max and also modified one aftereffects plugin using changes.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Houdini: hotfix condition (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ houdini</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4391\">#4391</a></summary>\n\n___\n\n## Hotfix\n\n\n\nThis is fixing bug introduced int #4374\n___\n\n</details>\n\n\n\n<details>\n<summary>Houdini: Houdini shelf tools fixes (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ houdini</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4428\">#4428</a></summary>\n\n___\n\n#### Brief description\n\nFix Houdini shelf tools.\n\n\n\n#### Description\n\nUse `label` as mandatory key instead of `name`. Changed how shelves are created. If the script is empty it is gracefully skipping it instead of crashing.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>3dsmax: startup fixes (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ 3dsmax</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4412\">#4412</a></summary>\n\n___\n\n#### Brief description\n\nThis is fixing various issues that can occur on some of the 3dsmax versions.\n\n\n\n#### Description\n\nOn displays with +4K resolution UI was broken, some 3dsmax versions couldn't process `PYTHONPATH` correctly. This PR is forcing `sys.path` and disabling `QT_AUTO_SCREEN_SCALE_FACTOR`\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Fix features for gizmo menu (<i><font color='#367F6C';>2d</font> </i> <i><font style='color:#365E7F';>/ nuke</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4280\">#4280</a></summary>\n\n___\n\n#### Brief description\n\nFix features for the Gizmo Menu project settings (shortcut for python type of usage and file type of usage functionality)\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Photoshop: fix missing legacy io for legacy instances (<i><font color='#367F6C';>2d</font> </i> <i><font style='color:#365E7F';>/ photoshop,after effects</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4467\">#4467</a></summary>\n\n___\n\n#### Brief description\n\n`legacy_io` import was removed, but usage stayed.\n\n\n\n#### Description\n\nUsage of `legacy_io` should be eradicated, in creators it should be replaced by `self.create_context.get_current_project_name/asset_name/task_name`.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Fix - addSite loader handles hero version (<i><font color='#367F6C';>other</font> </i> <i><font style='color:#1E1B7B';>/ sitesync</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4359\">#4359</a></summary>\n\n___\n\n#### Brief description\n\nIf adding site to representation presence of hero version is checked, if found hero version is marked to be donwloaded too.Replacing https://github.com/ynput/OpenPype/pull/4191\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Remove OIIO build for macos (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4381\">#4381</a></summary>\n\n___\n\n## Fix\n\n\n\nSince we are not able to provide OpenImageIO tools binaries for macos, we should remove the item from th `pyproject.toml`. This PR is taking care of it.\n\n\n\nIt is also changing the way `fetch_thirdparty_libs` script works in that it doesn't crash when lib cannot be processed, it only issue warning.\n\n\n\n\n\nResolves #3858\n___\n\n</details>\n\n\n\n<details>\n<summary>General: Attribute definitions fixes (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4392\">#4392</a></summary>\n\n___\n\n#### Brief description\n\nFix possible issues with attribute definitions in publisher if there is unknown attribute on an instance.\n\n\n\n#### Description\n\nSource of the issue is that attribute definitions from creator plugin could be \"expanded\" during `CreatedInstance` initialization. Which would affect all other instances using the same list of attributes -> literally object of list. If the same list object is used in \"BaseClass\" for other creators it would affect all instances (because of 1 instance). There had to be implemented other changes to fix the issue and keep behavior the same.Object of `CreatedInstance` can be created without reference to creator object. `CreatedInstance` is responsible to give UI attribute definitions (technically is prepared for cases when each instance may have different attribute definitions -> not yet).Attribute definition has added more conditions for `__eq__` method and have implemented `__ne__` method (which is required for Py 2 compatibility). Renamed `AbtractAttrDef` to `AbstractAttrDef` (fix typo).\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Ftrack: Don't force ftrackapp endpoint (<i><font color='#367F6C';>other</font> </i> <i><font style='color:#1E1B7B';>/ ftrack</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4411\">#4411</a></summary>\n\n___\n\n#### Brief description\n\nAuto-fill of ftrack url don't break custom urls. Custom urls couldn't be used as `ftrackapp.com` is added if is not in the url.\n\n\n\n#### Description\n\nThe code was changed in a way that auto-fill is still supported but before `ftrackapp` is added it will try to use url as is. If the connection works as is it is used.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Fix: DL on MacOS (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4418\">#4418</a></summary>\n\n___\n\n#### Brief description\n\nThis works if DL Openpype plugin Installation Directories is set to level of app bundle (eg. '/Applications/OpenPype 3.15.0.app')\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Photoshop: make usage of layer name in subset name more controllable (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4432\">#4432</a></summary>\n\n___\n\n#### Brief description\n\nLayer name was previously used in subset name only if multiple instances were being created in single step. This adds explicit toggle.\n\n\n\n#### Description\n\nToggling this button allows to use layer name in created subset name even if single instance is being created.This follows more closely implementation if AE.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>SiteSync: fix dirmap (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4436\">#4436</a></summary>\n\n___\n\n#### Brief description\n\nFixed issue in dirmap in Maya and Nuke\n\n\n\n#### Description\n\nLoads of error were thrown in Nuke console about dictionary value.`AttributeError: 'dict' object has no attribute 'lower'`\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>General: Ignore decode error of stdout/stderr in run_subprocess (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4446\">#4446</a></summary>\n\n___\n\n#### Brief description\n\nIgnore decode errors and replace invalid character (byte) with escaped byte character.\n\n\n\n#### Description\n\nCalling of `run_subprocess` may cause crashes if output contains some unicode character which (for example Polish name of encoder handler).\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Publisher: Fix reopen bug (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4463\">#4463</a></summary>\n\n___\n\n#### Brief description\n\nUse right name of constant 'ActiveWindow' -> 'WindowActive'.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Publisher: Fix compatibility of QAction in Publisher (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4474\">#4474</a></summary>\n\n___\n\n#### Brief description\n\nFix `QAction` for older version of Qt bindings where QAction requires a parent on initialization.\n\n\n\n#### Description\n\nThis bug was discovered in Nuke 11. Fixed by creating QAction when QMenu is already available and can be used as parent.\n\n\n\n\n___\n\n</details>\n\n\n### **🔀 Refactored code**\n\n\n\n\n<details>\n<summary>General: Remove 'openpype.api' (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4413\">#4413</a></summary>\n\n___\n\n#### Brief description\n\nPR is removing `openpype/api.py` file which is causing a lot of troubles and cross-imports.\n\n\n\n#### Description\n\nI wanted to remove the file slowly function by function but it always reappear somewhere in codebase even if most of the functionality imported from there is triggering deprecation warnings. This is small change which may have huge impact.There shouldn't be anything in openpype codebase which is using `openpype.api` anymore so only possible issues are in customized repositories or custom addons.\n\n\n\n\n___\n\n</details>\n\n\n### **📃 Documentation**\n\n\n\n\n<details>\n<summary>docs-user-Getting Started adjustments (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4365\">#4365</a></summary>\n\n___\n\n#### Brief description\n\nSmall typo fixes here and there, additional info on install/ running OP.\n\n\n\n\n___\n\n</details>\n\n\n### **Merged pull requests**\n\n\n\n\n<details>\n<summary>Renderman support for sample and display filters (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ maya</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4003\">#4003</a></summary>\n\n___\n\n#### Brief description\n\nUser can set up both sample and display filters in Openpype settings if they are using Renderman as renderer.\n\n\n\n#### Description\n\nYou can preset which sample and display filters for renderman , including the cryptomatte renderpass, in Openpype settings. Once you select which filters to be included in openpype settings and then create render instance for your camera in maya, it would automatically tell the system to generate your selected filters in render settings.The place you can find for setting up the filters: _Maya > Render Settings > Renderman Renderer > Display Filters/ Sample Filters_\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Maya: Create Arnold options on repair. (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ maya</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4448\">#4448</a></summary>\n\n___\n\n#### Brief description\n\nWhen validating/repairing we previously required users to open render settings to create the Arnold options. This is done through code now.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Update Asset field of creator Instances in Maya Template Builder (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ maya</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4470\">#4470</a></summary>\n\n___\n\n#### Brief description\n\nWhen we build a template with Maya Template Builder, it will update the asset field of the sets (creator instances) that are imported from the template.\n\n\n\n#### Description\n\nWhen building a template, we also want to define the publishable content in advance: create an instance of a model, or look, etc., to speed up the workflow and reduce the number of questions we are asked. After building a work file from a saved template that contains pre-created instances, the template builder should update the asset field to the current asset.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Blender: fix import workfile all families (<i><font color='#367F6C';>3d</font> </i> <i><font style='color:#365E7F';>/ blender</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4405\">#4405</a></summary>\n\n___\n\n#### Brief description\n\nHaving this feature related to workfile available for any family is absurd.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Nuke: update rendered frames in latest version (<i><font color='#367F6C';>2d</font> </i> <i><font style='color:#365E7F';>/ nuke</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4362\">#4362</a></summary>\n\n___\n\n#### Brief description\n\nIntroduced new field to insert frame(s) to rerender only.\n\n\n\n#### Description\n\nRendering is expensive, sometimes it is helpful only to re-render changed frames and reuse existing.Artists can in Publisher fill which frame(s) should be re-rendered.If there is already published version of currently publishing subset, all representation files are collected (currently for `render` family only) and then when Nuke is rendering (locally only for now), old published files are copied into into temporary render folder where will be rewritten only by frames explicitly set in new field.That way review/burnin process could also reuse old files and recreate reviews/burnins.New version is produced during this process!\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Feature: Keep synced hero representations up-to-date. (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4343\">#4343</a></summary>\n\n___\n\n#### Brief description\n\nKeep previously synchronized sites up-to-date by comparing old and new sites and adding old sites if missing in new ones.Fix #4331\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Maya: Fix template builder bug where assets are not put in the right hierarchy (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4367\">#4367</a></summary>\n\n___\n\n#### Brief description\n\nWhen buiding scene from template, the assets loaded from the placeholders are not put in the hierarchy. Plus, the assets are loaded in double.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Bump ua-parser-js from 0.7.31 to 0.7.33 in /website (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4371\">#4371</a></summary>\n\n___\n\nBumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33.\n<details>\n<summary>Changelog</summary>\n<p><em>Sourced from <a href=\"https://github.com/faisalman/ua-parser-js/blob/master/changelog.md\">ua-parser-js's changelog</a>.</em></p>\n<blockquote>\n<h2>Version 0.7.31 / 1.0.2</h2>\n<ul>\n<li>Fix OPPO Reno A5 incorrect detection</li>\n<li>Fix TypeError Bug</li>\n<li>Use AST to extract regexes and verify them with safe-regex</li>\n</ul>\n<h2>Version 0.7.32 / 1.0.32</h2>\n<ul>\n<li>Add new browser : DuckDuckGo, Huawei Browser, LinkedIn</li>\n<li>Add new OS : HarmonyOS</li>\n<li>Add some Huawei models</li>\n<li>Add Sharp Aquos TV</li>\n<li>Improve detection Xiaomi Mi CC9</li>\n<li>Fix Sony Xperia 1 III misidentified as Acer tablet</li>\n<li>Fix Detect Sony BRAVIA as SmartTV</li>\n<li>Fix Detect Xiaomi Mi TV as SmartTV</li>\n<li>Fix Detect Galaxy Tab S8 as tablet</li>\n<li>Fix WeGame mistakenly identified as WeChat</li>\n<li>Fix included commas in Safari / Mobile Safari version</li>\n<li>Increase UA_MAX_LENGTH to 350</li>\n</ul>\n<h2>Version 0.7.33 / 1.0.33</h2>\n<ul>\n<li>Add new browser : Cobalt</li>\n<li>Identify Macintosh as an Apple device</li>\n<li>Fix ReDoS vulnerability</li>\n</ul>\n<h1>Version 0.8</h1>\n<p>Version 0.8 was created by accident. This version is now deprecated and no longer maintained, please update to version 0.7 / 1.0.</p>\n</blockquote>\n</details>\n<details>\n<summary>Commits</summary>\n<ul>\n<li><a href=\"https://github.com/faisalman/ua-parser-js/commit/f2d0db001d87da15de7b9b1df7be9f2eacefd8c5\"><code>f2d0db0</code></a> Bump version 0.7.33</li>\n<li><a href=\"https://github.com/faisalman/ua-parser-js/commit/a6140a17dd0300a35cfc9cff999545f267889411\"><code>a6140a1</code></a> Remove unsafe regex in trim() function</li>\n<li><a href=\"https://github.com/faisalman/ua-parser-js/commit/a88660493568d6144a551424a8139d6c876635f6\"><code>a886604</code></a> Fix <a href=\"https://github-redirect.dependabot.com/faisalman/ua-parser-js/issues/605\">#605</a> - Identify Macintosh as Apple device</li>\n<li><a href=\"https://github.com/faisalman/ua-parser-js/commit/b814bcd79198e730936c82462e2d729eb5423e3c\"><code>b814bcd</code></a> Merge pull request <a href=\"https://github-redirect.dependabot.com/faisalman/ua-parser-js/issues/606\">#606</a> from rileyjshaw/patch-1</li>\n<li><a href=\"https://github.com/faisalman/ua-parser-js/commit/7f71024161399b7aa5d5cd10dba9e059f0218262\"><code>7f71024</code></a> Fix documentation</li>\n<li><a href=\"https://github.com/faisalman/ua-parser-js/commit/c239ac5167abd574a635cb809a2b4fa35810d23b\"><code>c239ac5</code></a> Merge pull request <a href=\"https://github-redirect.dependabot.com/faisalman/ua-parser-js/issues/604\">#604</a> from obecerra3/master</li>\n<li><a href=\"https://github.com/faisalman/ua-parser-js/commit/8d3c2d327cf540ff2c050f1cc67bca8c6f8e4458\"><code>8d3c2d3</code></a> Add new browser: Cobalt</li>\n<li><a href=\"https://github.com/faisalman/ua-parser-js/commit/d11fc47dc9b6acc0f89fc10c120cea08e10cd31a\"><code>d11fc47</code></a> Bump version 0.7.32</li>\n<li><a href=\"https://github.com/faisalman/ua-parser-js/commit/b490110109de586deab96c775c9ef0dfc9c919c4\"><code>b490110</code></a> Merge branch 'develop' of github.com:faisalman/ua-parser-js</li>\n<li><a href=\"https://github.com/faisalman/ua-parser-js/commit/cb5da5ea4b220d5b60fe209e123b7f911d8e0d4a\"><code>cb5da5e</code></a> Merge pull request <a href=\"https://github-redirect.dependabot.com/faisalman/ua-parser-js/issues/600\">#600</a> from moekm/develop</li>\n<li>Additional commits viewable in <a href=\"https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33\">compare view</a></li>\n</ul>\n</details>\n<br />\n\n\n[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ua-parser-js&package-manager=npm_and_yarn&previous-version=0.7.31&new-version=0.7.33)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)\n\nDependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.\n\n[//]: # (dependabot-automerge-start)\n[//]: # (dependabot-automerge-end)\n\n---\n\n<details>\n<summary>Dependabot commands and options</summary>\n<br />\n\nYou can trigger Dependabot actions by commenting on this PR:\n- `@dependabot rebase` will rebase this PR\n- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it\n- `@dependabot merge` will merge this PR after your CI passes on it\n- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it\n- `@dependabot cancel merge` will cancel a previously requested merge and block automerging\n- `@dependabot reopen` will reopen this PR if it is closed\n- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually\n- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language\n- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language\n- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language\n- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language\n\nYou can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts).\n\n</details>\n___\n\n</details>\n\n\n\n<details>\n<summary>Docs: Question about renaming in Kitsu (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4384\">#4384</a></summary>\n\n___\n\n#### Brief description\n\nTo keep memory of this discussion: https://discord.com/channels/517362899170230292/563751989075378201/1068112668491255818\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>New Publisher: Fix Creator error typo (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4396\">#4396</a></summary>\n\n___\n\n#### Brief description\n\nFixes typo in error message.\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Chore: pyproject.toml version because of Poetry (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4408\">#4408</a></summary>\n\n___\n\n#### Brief description\n\nAutomatization injects wrong format\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Fix - remove minor part in toml (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4437\">#4437</a></summary>\n\n___\n\n#### Brief description\n\nCauses issue in create_env and new Poetry\n\n\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>General: Add project code to anatomy (<i><font color='#367F6C';>other</font> </i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4445\">#4445</a></summary>\n\n___\n\n#### Brief description\n\nAdded attribute `project_code` to `Anatomy` object.\n\n\n\n#### Description\n\nAnatomy already have access to almost all attributes from project anatomy except project code. This PR changing it. Technically `Anatomy` is everything what would be needed to get fill data of project.\n\n```\n\n{\n\n    \"project\": {\n\n        \"name\": anatomy.project_name,\n\n        \"code\": anatomy.project_code\n\n    }\n\n}\n\n```\n\n\n___\n\n</details>\n\n\n\n<details>\n<summary>Maya: Arnold Scene Source overhaul - OP-4865 (<i><font color='#367F6C';>other</font> </i> <i><font style='color:#365E7F';>/ maya</font></i> ) - <a href=\"https://github.com/ynput/OpenPype/pull/4449\">#4449</a></summary>\n\n___\n\n#### Brief description\n\nGeneral overhaul of the Arnold Scene Source (ASS) workflow.\n\n\n\n#### Description\n\nThis originally was to support static files (non-sequencial) ASS publishing, but digging deeper whole workflow needed an update to get ready for further issues. During this overhaul the following changes were made:\n\n- Generalized Arnold Standin workflow to a single loader.\n\n- Support multiple nodes as proxies.\n\n- Support proxies for `pointcache` family.\n\n- Generalized approach to proxies as resources, so they can be the same file format as the original.This workflow should allow further expansion to utilize operators and eventually USD.\n\n\n\n\n___\n\n</details>\n\n\n\n\n## [3.15.0](https://github.com/ynput/OpenPype/tree/3.15.0)\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.14.10...3.15.0)\n\n**Deprecated:**\n\n- General: Fill default values of new publish template profiles [\\#4245](https://github.com/ynput/OpenPype/pull/4245)\n\n### 📖 Documentation\n\n- documentation: Split tools into separate entries [\\#4342](https://github.com/ynput/OpenPype/pull/4342)\n- Documentation: Fix harmony docs [\\#4301](https://github.com/ynput/OpenPype/pull/4301)\n- Remove staging logic set by OpenPype version [\\#3979](https://github.com/ynput/OpenPype/pull/3979)\n\n**🆕 New features**\n\n- General: Push to studio library [\\#4284](https://github.com/ynput/OpenPype/pull/4284)\n- Colorspace Management and Distribution [\\#4195](https://github.com/ynput/OpenPype/pull/4195)\n- Nuke: refactor to latest publisher workfow [\\#4006](https://github.com/ynput/OpenPype/pull/4006)\n- Update to Python 3.9 [\\#3546](https://github.com/ynput/OpenPype/pull/3546)\n\n**🚀 Enhancements**\n\n- Unreal: Don't use mongo queries in 'ExistingLayoutLoader' [\\#4356](https://github.com/ynput/OpenPype/pull/4356)\n- General: Loader and Creator plugins can be disabled [\\#4310](https://github.com/ynput/OpenPype/pull/4310)\n- General: Unbind poetry version [\\#4306](https://github.com/ynput/OpenPype/pull/4306)\n- General: Enhanced enum def items [\\#4295](https://github.com/ynput/OpenPype/pull/4295)\n- Git: add pre-commit hooks [\\#4289](https://github.com/ynput/OpenPype/pull/4289)\n- Tray Publisher: Improve Online family functionality [\\#4263](https://github.com/ynput/OpenPype/pull/4263)\n- General: Update MacOs to PySide6 [\\#4255](https://github.com/ynput/OpenPype/pull/4255)\n- Build: update to Gazu in toml [\\#4208](https://github.com/ynput/OpenPype/pull/4208)\n- Global: adding imageio to settings [\\#4158](https://github.com/ynput/OpenPype/pull/4158)\n- Blender: added project settings for validator no colons in name [\\#4149](https://github.com/ynput/OpenPype/pull/4149)\n- Dockerfile for Debian Bullseye [\\#4108](https://github.com/ynput/OpenPype/pull/4108)\n- AfterEffects: publish multiple compositions [\\#4092](https://github.com/ynput/OpenPype/pull/4092)\n- AfterEffects: make new publisher default [\\#4056](https://github.com/ynput/OpenPype/pull/4056)\n- Photoshop: make new publisher default [\\#4051](https://github.com/ynput/OpenPype/pull/4051)\n- Feature/multiverse [\\#4046](https://github.com/ynput/OpenPype/pull/4046)\n- Tests: add support for deadline for automatic tests [\\#3989](https://github.com/ynput/OpenPype/pull/3989)\n- Add version to shortcut name [\\#3906](https://github.com/ynput/OpenPype/pull/3906)\n- TrayPublisher: Removed from experimental tools [\\#3667](https://github.com/ynput/OpenPype/pull/3667)\n\n**🐛 Bug fixes**\n\n- change 3.7 to 3.9 in folder name [\\#4354](https://github.com/ynput/OpenPype/pull/4354)\n- PushToProject: Fix hierarchy of project change [\\#4350](https://github.com/ynput/OpenPype/pull/4350)\n- Fix photoshop workfile save-as [\\#4347](https://github.com/ynput/OpenPype/pull/4347)\n- Nuke Input process node sourcing improvements [\\#4341](https://github.com/ynput/OpenPype/pull/4341)\n- New publisher: Some validation plugin tweaks [\\#4339](https://github.com/ynput/OpenPype/pull/4339)\n- Harmony: fix unable to change workfile on Mac [\\#4334](https://github.com/ynput/OpenPype/pull/4334)\n- Global: fixing in-place source publishing for editorial [\\#4333](https://github.com/ynput/OpenPype/pull/4333)\n- General: Use class constants of QMessageBox [\\#4332](https://github.com/ynput/OpenPype/pull/4332)\n- TVPaint: Fix plugin for TVPaint 11.7 [\\#4328](https://github.com/ynput/OpenPype/pull/4328)\n- Exctract OTIO review has improved quality [\\#4325](https://github.com/ynput/OpenPype/pull/4325)\n- Ftrack: fix typos causing bugs in sync [\\#4322](https://github.com/ynput/OpenPype/pull/4322)\n- General: Python 2 compatibility of instance collector [\\#4320](https://github.com/ynput/OpenPype/pull/4320)\n- Slack: user groups speedup [\\#4318](https://github.com/ynput/OpenPype/pull/4318)\n- Maya: Bug - Multiverse extractor executed on plain animation family [\\#4315](https://github.com/ynput/OpenPype/pull/4315)\n- Fix run\\_documentation.ps1 [\\#4312](https://github.com/ynput/OpenPype/pull/4312)\n- Nuke: new creators fixes [\\#4308](https://github.com/ynput/OpenPype/pull/4308)\n- General: missing comment on standalone and tray publisher [\\#4303](https://github.com/ynput/OpenPype/pull/4303)\n- AfterEffects: Fix for audio from mp4 layer [\\#4296](https://github.com/ynput/OpenPype/pull/4296)\n- General: Update gazu in poetry lock [\\#4247](https://github.com/ynput/OpenPype/pull/4247)\n- Bug: Fixing version detection and filtering in Igniter [\\#3914](https://github.com/ynput/OpenPype/pull/3914)\n- Bug: Create missing version dir [\\#3903](https://github.com/ynput/OpenPype/pull/3903)\n\n**🔀 Refactored code**\n\n- Remove redundant export\\_alembic method. [\\#4293](https://github.com/ynput/OpenPype/pull/4293)\n- Igniter: Use qtpy modules instead of Qt [\\#4237](https://github.com/ynput/OpenPype/pull/4237)\n\n**Merged pull requests:**\n\n- Sort families by alphabetical order in the Create plugin [\\#4346](https://github.com/ynput/OpenPype/pull/4346)\n- Global: Validate unique subsets [\\#4336](https://github.com/ynput/OpenPype/pull/4336)\n- Maya: Collect instances preserve handles even if frameStart + frameEnd matches context [\\#3437](https://github.com/ynput/OpenPype/pull/3437)\n\n## [3.14.10](https://github.com/ynput/OpenPype/tree/HEAD)\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.14.9...3.14.10)\n\n**🆕 New features**\n\n- Global | Nuke: Creator placeholders in workfile template builder [\\#4266](https://github.com/ynput/OpenPype/pull/4266)\n- Slack: Added dynamic message [\\#4265](https://github.com/ynput/OpenPype/pull/4265)\n- Blender: Workfile Loader [\\#4234](https://github.com/ynput/OpenPype/pull/4234)\n- Unreal: Publishing and Loading for UAssets [\\#4198](https://github.com/ynput/OpenPype/pull/4198)\n- Publish: register publishes without copying them [\\#4157](https://github.com/ynput/OpenPype/pull/4157)\n\n**🚀 Enhancements**\n\n- General: Added install method with docstring to HostBase [\\#4298](https://github.com/ynput/OpenPype/pull/4298)\n- Traypublisher: simple editorial multiple edl [\\#4248](https://github.com/ynput/OpenPype/pull/4248)\n- General: Extend 'IPluginPaths' to have more available methods [\\#4214](https://github.com/ynput/OpenPype/pull/4214)\n- Refactorization of folder coloring [\\#4211](https://github.com/ynput/OpenPype/pull/4211)\n- Flame - loading multilayer with controlled layer names [\\#4204](https://github.com/ynput/OpenPype/pull/4204)\n\n**🐛 Bug fixes**\n\n- Unreal: fix missing `maintained_selection` call [\\#4300](https://github.com/ynput/OpenPype/pull/4300)\n- Ftrack: Fix receive of host ip on MacOs [\\#4288](https://github.com/ynput/OpenPype/pull/4288)\n- SiteSync: sftp connection failing when shouldnt be tested [\\#4278](https://github.com/ynput/OpenPype/pull/4278)\n- Deadline: fix default value for passing mongo url [\\#4275](https://github.com/ynput/OpenPype/pull/4275)\n- Scene Manager: Fix variable name [\\#4268](https://github.com/ynput/OpenPype/pull/4268)\n- Slack: notification fails because of missing published path [\\#4264](https://github.com/ynput/OpenPype/pull/4264)\n- hiero: creator gui with min max  [\\#4257](https://github.com/ynput/OpenPype/pull/4257)\n- NiceCheckbox: Fix checker positioning in Python 2 [\\#4253](https://github.com/ynput/OpenPype/pull/4253)\n- Publisher: Fix 'CreatorType' not equal for Python 2 DCCs [\\#4249](https://github.com/ynput/OpenPype/pull/4249)\n- Deadline: fix dependencies [\\#4242](https://github.com/ynput/OpenPype/pull/4242)\n- Houdini: hotfix instance data access [\\#4236](https://github.com/ynput/OpenPype/pull/4236)\n- bugfix/image plane load error [\\#4222](https://github.com/ynput/OpenPype/pull/4222)\n- Hiero: thumbnail from multilayer exr [\\#4209](https://github.com/ynput/OpenPype/pull/4209)\n\n**🔀 Refactored code**\n\n- Resolve: Use qtpy in Resolve [\\#4254](https://github.com/ynput/OpenPype/pull/4254)\n- Houdini: Use qtpy in Houdini [\\#4252](https://github.com/ynput/OpenPype/pull/4252)\n- Max: Use qtpy in Max [\\#4251](https://github.com/ynput/OpenPype/pull/4251)\n- Maya: Use qtpy in Maya [\\#4250](https://github.com/ynput/OpenPype/pull/4250)\n- Hiero: Use qtpy in Hiero [\\#4240](https://github.com/ynput/OpenPype/pull/4240)\n- Nuke: Use qtpy in Nuke [\\#4239](https://github.com/ynput/OpenPype/pull/4239)\n- Flame: Use qtpy in flame [\\#4238](https://github.com/ynput/OpenPype/pull/4238)\n- General: Legacy io not used in global plugins [\\#4134](https://github.com/ynput/OpenPype/pull/4134)\n\n**Merged pull requests:**\n\n- Bump json5 from 1.0.1 to 1.0.2 in /website [\\#4292](https://github.com/ynput/OpenPype/pull/4292)\n- Maya: Fix validate frame range repair + fix create render with deadline disabled [\\#4279](https://github.com/ynput/OpenPype/pull/4279)\n\n\n## [3.14.9](https://github.com/pypeclub/OpenPype/tree/3.14.9)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.8...3.14.9)\n\n### 📖 Documentation\n\n- Documentation: Testing on Deadline [\\#4185](https://github.com/pypeclub/OpenPype/pull/4185)\n- Consistent Python version [\\#4160](https://github.com/pypeclub/OpenPype/pull/4160)\n\n**🆕 New features**\n\n- Feature/op 4397 gl tf extractor for maya [\\#4192](https://github.com/pypeclub/OpenPype/pull/4192)\n- Maya: Extractor for Unreal SkeletalMesh [\\#4174](https://github.com/pypeclub/OpenPype/pull/4174)\n- 3dsmax: integration [\\#4168](https://github.com/pypeclub/OpenPype/pull/4168)\n- Blender: Extract Alembic Animations [\\#4128](https://github.com/pypeclub/OpenPype/pull/4128)\n- Unreal: Load Alembic Animations [\\#4127](https://github.com/pypeclub/OpenPype/pull/4127)\n\n**🚀 Enhancements**\n\n- Houdini: Use new interface class name for publish host [\\#4220](https://github.com/pypeclub/OpenPype/pull/4220)\n- General: Default command for headless mode is interactive [\\#4203](https://github.com/pypeclub/OpenPype/pull/4203)\n- Maya: Enhanced ASS publishing [\\#4196](https://github.com/pypeclub/OpenPype/pull/4196)\n- Feature/op 3924 implement ass extractor [\\#4188](https://github.com/pypeclub/OpenPype/pull/4188)\n- File transactions: Source path is destination path [\\#4184](https://github.com/pypeclub/OpenPype/pull/4184)\n- Deadline: improve environment processing [\\#4182](https://github.com/pypeclub/OpenPype/pull/4182)\n- General: Comment per instance in Publisher [\\#4178](https://github.com/pypeclub/OpenPype/pull/4178)\n- Ensure Mongo database directory exists in Windows. [\\#4166](https://github.com/pypeclub/OpenPype/pull/4166)\n- Note about unrestricted execution on Windows. [\\#4161](https://github.com/pypeclub/OpenPype/pull/4161)\n- Maya: Enable thumbnail transparency on extraction. [\\#4147](https://github.com/pypeclub/OpenPype/pull/4147)\n- Maya: Disable viewport Pan/Zoom on playblast extraction. [\\#4146](https://github.com/pypeclub/OpenPype/pull/4146)\n- Maya: Optional viewport refresh on pointcache extraction [\\#4144](https://github.com/pypeclub/OpenPype/pull/4144)\n- CelAction: refactory integration to current openpype [\\#4140](https://github.com/pypeclub/OpenPype/pull/4140)\n- Maya: create and publish bounding box geometry [\\#4131](https://github.com/pypeclub/OpenPype/pull/4131)\n- Changed the UOpenPypePublishInstance to use the UDataAsset class [\\#4124](https://github.com/pypeclub/OpenPype/pull/4124)\n- General: Collection Audio speed up [\\#4110](https://github.com/pypeclub/OpenPype/pull/4110)\n- Maya: keep existing AOVs when creating render instance [\\#4087](https://github.com/pypeclub/OpenPype/pull/4087)\n- General: Oiio conversion multipart fix [\\#4060](https://github.com/pypeclub/OpenPype/pull/4060)\n\n**🐛 Bug fixes**\n\n- Publisher: Signal type issues in Python 2 DCCs [\\#4230](https://github.com/pypeclub/OpenPype/pull/4230)\n- Blender: Fix Layout Family Versioning [\\#4228](https://github.com/pypeclub/OpenPype/pull/4228)\n- Blender: Fix Create Camera \"Use selection\" [\\#4226](https://github.com/pypeclub/OpenPype/pull/4226)\n- TrayPublisher - join needs list [\\#4224](https://github.com/pypeclub/OpenPype/pull/4224)\n- General: Event callbacks pass event to callbacks as expected [\\#4210](https://github.com/pypeclub/OpenPype/pull/4210)\n- Build:Revert .toml update of Gazu [\\#4207](https://github.com/pypeclub/OpenPype/pull/4207)\n- Nuke: fixed imageio node overrides subset filter [\\#4202](https://github.com/pypeclub/OpenPype/pull/4202)\n- Maya: pointcache [\\#4201](https://github.com/pypeclub/OpenPype/pull/4201)\n- Unreal: Support for Unreal Engine 5.1 [\\#4199](https://github.com/pypeclub/OpenPype/pull/4199)\n- General: Integrate thumbnail looks for thumbnail to multiple places [\\#4181](https://github.com/pypeclub/OpenPype/pull/4181)\n- Various minor bugfixes [\\#4172](https://github.com/pypeclub/OpenPype/pull/4172)\n- Nuke/Hiero: Remove tkinter library paths before launch [\\#4171](https://github.com/pypeclub/OpenPype/pull/4171)\n- Flame: vertical alignment of layers [\\#4169](https://github.com/pypeclub/OpenPype/pull/4169)\n- Nuke: correct detection of viewer and display [\\#4165](https://github.com/pypeclub/OpenPype/pull/4165)\n- Settings UI: Don't create QApplication if already exists [\\#4156](https://github.com/pypeclub/OpenPype/pull/4156)\n- General: Extract review handle start offset of sequences [\\#4152](https://github.com/pypeclub/OpenPype/pull/4152)\n- Maya: Maintain time connections on Alembic update. [\\#4143](https://github.com/pypeclub/OpenPype/pull/4143)\n\n**🔀 Refactored code**\n\n- General: Use qtpy in modules and hosts UIs which are running in OpenPype process [\\#4225](https://github.com/pypeclub/OpenPype/pull/4225)\n- Tools: Use qtpy instead of Qt in standalone tools [\\#4223](https://github.com/pypeclub/OpenPype/pull/4223)\n- General: Use qtpy in settings UI [\\#4215](https://github.com/pypeclub/OpenPype/pull/4215)\n\n**Merged pull requests:**\n\n- layout publish more than one container issue [\\#4098](https://github.com/pypeclub/OpenPype/pull/4098)\n\n## [3.14.8](https://github.com/pypeclub/OpenPype/tree/3.14.8)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.7...3.14.8)\n\n**🚀 Enhancements**\n\n- General: Refactored extract hierarchy plugin [\\#4139](https://github.com/pypeclub/OpenPype/pull/4139)\n- General: Find executable enhancement [\\#4137](https://github.com/pypeclub/OpenPype/pull/4137)\n- Ftrack: Reset session before instance processing [\\#4129](https://github.com/pypeclub/OpenPype/pull/4129)\n- Ftrack: Editorial asset sync issue [\\#4126](https://github.com/pypeclub/OpenPype/pull/4126)\n- Deadline: Build version resolving [\\#4115](https://github.com/pypeclub/OpenPype/pull/4115)\n- Houdini: New Publisher [\\#3046](https://github.com/pypeclub/OpenPype/pull/3046)\n- Fix: Standalone Publish Directories [\\#4148](https://github.com/pypeclub/OpenPype/pull/4148)\n\n**🐛 Bug fixes**\n\n- Ftrack: Fix occational double parents issue [\\#4153](https://github.com/pypeclub/OpenPype/pull/4153)\n- General: Maketx executable issue [\\#4136](https://github.com/pypeclub/OpenPype/pull/4136)\n- Maya: Looks - add all connections [\\#4135](https://github.com/pypeclub/OpenPype/pull/4135)\n- General: Fix variable check in collect anatomy instance data [\\#4117](https://github.com/pypeclub/OpenPype/pull/4117)\n\n## [3.14.7](https://github.com/pypeclub/OpenPype/tree/3.14.7)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.6...3.14.7)\n\n**🆕 New features**\n\n- Hiero: loading effect family to timeline [\\#4055](https://github.com/pypeclub/OpenPype/pull/4055)\n\n**🚀 Enhancements**\n\n- Photoshop: bug with pop-up window on Instance Creator [\\#4121](https://github.com/pypeclub/OpenPype/pull/4121)\n- Publisher: Open on specific tab [\\#4120](https://github.com/pypeclub/OpenPype/pull/4120)\n- Publisher: Hide unknown publish values [\\#4116](https://github.com/pypeclub/OpenPype/pull/4116)\n- Ftrack: Event server status give more information about version locations [\\#4112](https://github.com/pypeclub/OpenPype/pull/4112)\n- General: Allow higher numbers in frames and clips [\\#4101](https://github.com/pypeclub/OpenPype/pull/4101)\n- Publisher: Settings for validate frame range [\\#4097](https://github.com/pypeclub/OpenPype/pull/4097)\n- Publisher: Ignore escape button [\\#4090](https://github.com/pypeclub/OpenPype/pull/4090)\n- Flame: Loading clip with native colorspace resolved from mapping [\\#4079](https://github.com/pypeclub/OpenPype/pull/4079)\n- General: Extract review single frame output [\\#4064](https://github.com/pypeclub/OpenPype/pull/4064)\n- Publisher: Prepared common function for instance data cache [\\#4063](https://github.com/pypeclub/OpenPype/pull/4063)\n- Publisher: Easy access to publish page from create page [\\#4058](https://github.com/pypeclub/OpenPype/pull/4058)\n- General/TVPaint: Attribute defs dialog [\\#4052](https://github.com/pypeclub/OpenPype/pull/4052)\n- Publisher: Better reset defer [\\#4048](https://github.com/pypeclub/OpenPype/pull/4048)\n- Publisher: Add thumbnail sources [\\#4042](https://github.com/pypeclub/OpenPype/pull/4042)\n\n**🐛 Bug fixes**\n\n- General: Move default settings for template name [\\#4119](https://github.com/pypeclub/OpenPype/pull/4119)\n- Slack: notification fail in new tray publisher [\\#4118](https://github.com/pypeclub/OpenPype/pull/4118)\n- Nuke: loaded nodes set to first tab [\\#4114](https://github.com/pypeclub/OpenPype/pull/4114)\n- Nuke: load image first frame [\\#4113](https://github.com/pypeclub/OpenPype/pull/4113)\n- Files Widget: Ignore case sensitivity of extensions [\\#4096](https://github.com/pypeclub/OpenPype/pull/4096)\n- Webpublisher: extension is lowercased in Setting and in uploaded files [\\#4095](https://github.com/pypeclub/OpenPype/pull/4095)\n- Publish Report Viewer: Fix small bugs [\\#4086](https://github.com/pypeclub/OpenPype/pull/4086)\n- Igniter: fix regex to match semver better [\\#4085](https://github.com/pypeclub/OpenPype/pull/4085)\n- Maya: aov filtering [\\#4083](https://github.com/pypeclub/OpenPype/pull/4083)\n- Flame/Flare: Loading to multiple batches [\\#4080](https://github.com/pypeclub/OpenPype/pull/4080)\n- hiero: creator from settings with set maximum [\\#4077](https://github.com/pypeclub/OpenPype/pull/4077)\n- Nuke: resolve hashes in file name only for frame token [\\#4074](https://github.com/pypeclub/OpenPype/pull/4074)\n- Publisher: Fix cache of asset docs [\\#4070](https://github.com/pypeclub/OpenPype/pull/4070)\n- Webpublisher: cleanup wp extract thumbnail [\\#4067](https://github.com/pypeclub/OpenPype/pull/4067)\n- Settings UI: Locked setting can't bypass lock [\\#4066](https://github.com/pypeclub/OpenPype/pull/4066)\n- Loader: Fix comparison of repre name [\\#4053](https://github.com/pypeclub/OpenPype/pull/4053)\n- Deadline: Extract environment subprocess failure [\\#4050](https://github.com/pypeclub/OpenPype/pull/4050)\n\n**🔀 Refactored code**\n\n- General: Collect entities plugin minor changes [\\#4089](https://github.com/pypeclub/OpenPype/pull/4089)\n- General: Direct interfaces import [\\#4065](https://github.com/pypeclub/OpenPype/pull/4065)\n\n**Merged pull requests:**\n\n- Bump loader-utils from 1.4.1 to 1.4.2 in /website [\\#4100](https://github.com/pypeclub/OpenPype/pull/4100)\n- Online family for Tray Publisher [\\#4093](https://github.com/pypeclub/OpenPype/pull/4093)\n- Bump loader-utils from 1.4.0 to 1.4.1 in /website [\\#4081](https://github.com/pypeclub/OpenPype/pull/4081)\n- remove underscore from subset name [\\#4059](https://github.com/pypeclub/OpenPype/pull/4059)\n- Alembic Loader as Arnold Standin [\\#4047](https://github.com/pypeclub/OpenPype/pull/4047)\n\n## [3.14.6](https://github.com/pypeclub/OpenPype/tree/3.14.6)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...3.14.6)\n\n### 📖 Documentation\n\n- Documentation: Minor updates to dev\\_requirements.md [\\#4025](https://github.com/pypeclub/OpenPype/pull/4025)\n\n**🆕 New features**\n\n- Nuke: add 13.2 variant [\\#4041](https://github.com/pypeclub/OpenPype/pull/4041)\n\n**🚀 Enhancements**\n\n- Publish Report Viewer: Store reports locally on machine [\\#4040](https://github.com/pypeclub/OpenPype/pull/4040)\n- General: More specific error in burnins script [\\#4026](https://github.com/pypeclub/OpenPype/pull/4026)\n- General: Extract review does not crash with old settings overrides [\\#4023](https://github.com/pypeclub/OpenPype/pull/4023)\n- Publisher: Convertors for legacy instances [\\#4020](https://github.com/pypeclub/OpenPype/pull/4020)\n- workflows: adding milestone creator and assigner [\\#4018](https://github.com/pypeclub/OpenPype/pull/4018)\n- Publisher: Catch creator errors [\\#4015](https://github.com/pypeclub/OpenPype/pull/4015)\n\n**🐛 Bug fixes**\n\n- Hiero - effect collection fixes [\\#4038](https://github.com/pypeclub/OpenPype/pull/4038)\n- Nuke - loader clip correct hash conversion in path [\\#4037](https://github.com/pypeclub/OpenPype/pull/4037)\n- Maya: Soft fail when applying capture preset [\\#4034](https://github.com/pypeclub/OpenPype/pull/4034)\n- Igniter: handle missing directory [\\#4032](https://github.com/pypeclub/OpenPype/pull/4032)\n- StandalonePublisher: Fix thumbnail publishing [\\#4029](https://github.com/pypeclub/OpenPype/pull/4029)\n- Experimental Tools: Fix publisher import [\\#4027](https://github.com/pypeclub/OpenPype/pull/4027)\n- Houdini: fix wrong path in ASS loader [\\#4016](https://github.com/pypeclub/OpenPype/pull/4016)\n\n**🔀 Refactored code**\n\n- General: Import lib functions from lib [\\#4017](https://github.com/pypeclub/OpenPype/pull/4017)\n\n## [3.14.5](https://github.com/pypeclub/OpenPype/tree/3.14.5) (2022-10-24)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...3.14.5)\n\n**🚀 Enhancements**\n\n- Maya: add OBJ extractor to model family [\\#4021](https://github.com/pypeclub/OpenPype/pull/4021)\n- Publish report viewer tool [\\#4010](https://github.com/pypeclub/OpenPype/pull/4010)\n- Nuke | Global: adding custom tags representation filtering [\\#4009](https://github.com/pypeclub/OpenPype/pull/4009)\n- Publisher: Create context has shared data for collection phase [\\#3995](https://github.com/pypeclub/OpenPype/pull/3995)\n- Resolve: updating to v18 compatibility [\\#3986](https://github.com/pypeclub/OpenPype/pull/3986)\n\n**🐛 Bug fixes**\n\n- TrayPublisher: Fix missing argument [\\#4019](https://github.com/pypeclub/OpenPype/pull/4019)\n- General: Fix python 2 compatibility of ffmpeg and oiio tools discovery [\\#4011](https://github.com/pypeclub/OpenPype/pull/4011)\n\n**🔀 Refactored code**\n\n- Maya: Removed unused imports [\\#4008](https://github.com/pypeclub/OpenPype/pull/4008)\n- Unreal: Fix import of moved function [\\#4007](https://github.com/pypeclub/OpenPype/pull/4007)\n- Houdini: Change import of RepairAction [\\#4005](https://github.com/pypeclub/OpenPype/pull/4005)\n- Nuke/Hiero: Refactor openpype.api imports [\\#4000](https://github.com/pypeclub/OpenPype/pull/4000)\n- TVPaint: Defined with HostBase [\\#3994](https://github.com/pypeclub/OpenPype/pull/3994)\n\n**Merged pull requests:**\n\n- Unreal: Remove redundant Creator stub [\\#4012](https://github.com/pypeclub/OpenPype/pull/4012)\n- Unreal: add `uproject` extension to Unreal project template [\\#4004](https://github.com/pypeclub/OpenPype/pull/4004)\n- Unreal: fix order of includes [\\#4002](https://github.com/pypeclub/OpenPype/pull/4002)\n- Fusion: Implement backwards compatibility \\(+/- Fusion 17.2\\) [\\#3958](https://github.com/pypeclub/OpenPype/pull/3958)\n\n## [3.14.4](https://github.com/pypeclub/OpenPype/tree/3.14.4) (2022-10-19)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...3.14.4)\n\n**🆕 New features**\n\n- Webpublisher: use max next published version number  for all items in batch [\\#3961](https://github.com/pypeclub/OpenPype/pull/3961)\n- General: Control Thumbnail integration via explicit configuration profiles [\\#3951](https://github.com/pypeclub/OpenPype/pull/3951)\n\n**🚀 Enhancements**\n\n- Publisher: Multiselection in card view [\\#3993](https://github.com/pypeclub/OpenPype/pull/3993)\n- TrayPublisher: Original Basename cause crash too early [\\#3990](https://github.com/pypeclub/OpenPype/pull/3990)\n- Tray Publisher: add `originalBasename` data to simple creators [\\#3988](https://github.com/pypeclub/OpenPype/pull/3988)\n- General: Custom paths to ffmpeg and OpenImageIO tools [\\#3982](https://github.com/pypeclub/OpenPype/pull/3982)\n- Integrate: Preserve existing subset group if instance does not set it for new version [\\#3976](https://github.com/pypeclub/OpenPype/pull/3976)\n- Publisher: Prepare publisher controller  for remote publishing [\\#3972](https://github.com/pypeclub/OpenPype/pull/3972)\n- Maya: new style dataclasses in maya deadline submitter plugin [\\#3968](https://github.com/pypeclub/OpenPype/pull/3968)\n- Maya: Define preffered Qt bindings for Qt.py and qtpy [\\#3963](https://github.com/pypeclub/OpenPype/pull/3963)\n- Settings: Move imageio from project anatomy to project settings \\[pypeclub\\] [\\#3959](https://github.com/pypeclub/OpenPype/pull/3959)\n- TrayPublisher: Extract thumbnail for other families [\\#3952](https://github.com/pypeclub/OpenPype/pull/3952)\n- Publisher: Pass instance to subset name method on update [\\#3949](https://github.com/pypeclub/OpenPype/pull/3949)\n- General: Set root environments before DCC launch [\\#3947](https://github.com/pypeclub/OpenPype/pull/3947)\n- Refactor: changed legacy way to update database for Hero version integrate [\\#3941](https://github.com/pypeclub/OpenPype/pull/3941)\n- Maya: Moved plugin from global to maya [\\#3939](https://github.com/pypeclub/OpenPype/pull/3939)\n- Publisher: Create dialog is part of main window [\\#3936](https://github.com/pypeclub/OpenPype/pull/3936)\n- Fusion: Implement Alembic and FBX mesh loader [\\#3927](https://github.com/pypeclub/OpenPype/pull/3927)\n\n**🐛 Bug fixes**\n\n- TrayPublisher: Disable sequences in batch mov creator [\\#3996](https://github.com/pypeclub/OpenPype/pull/3996)\n- Fix - tags might be missing on representation [\\#3985](https://github.com/pypeclub/OpenPype/pull/3985)\n- Resolve: Fix usage of functions from lib [\\#3983](https://github.com/pypeclub/OpenPype/pull/3983)\n- Maya: remove invalid prefix token for non-multipart outputs [\\#3981](https://github.com/pypeclub/OpenPype/pull/3981)\n- Ftrack: Fix schema cache for Python 2 [\\#3980](https://github.com/pypeclub/OpenPype/pull/3980)\n- Maya: add object to attr.s declaration [\\#3973](https://github.com/pypeclub/OpenPype/pull/3973)\n- Maya: Deadline OutputFilePath hack regression for Renderman [\\#3950](https://github.com/pypeclub/OpenPype/pull/3950)\n- Houdini: Fix validate workfile paths for non-parm file references [\\#3948](https://github.com/pypeclub/OpenPype/pull/3948)\n- Photoshop: missed sync published version of workfile with workfile [\\#3946](https://github.com/pypeclub/OpenPype/pull/3946)\n- Maya: Set default value for RenderSetupIncludeLights option [\\#3944](https://github.com/pypeclub/OpenPype/pull/3944)\n- Maya: fix regression of Renderman Deadline hack [\\#3943](https://github.com/pypeclub/OpenPype/pull/3943)\n- Kitsu: 2 fixes, nb\\_frames and Shot type error [\\#3940](https://github.com/pypeclub/OpenPype/pull/3940)\n- Tray: Change order of attribute changes [\\#3938](https://github.com/pypeclub/OpenPype/pull/3938)\n- AttributeDefs: Fix crashing multivalue of files widget [\\#3937](https://github.com/pypeclub/OpenPype/pull/3937)\n- General: Fix links query on hero version [\\#3900](https://github.com/pypeclub/OpenPype/pull/3900)\n- Publisher: Files Drag n Drop cleanup [\\#3888](https://github.com/pypeclub/OpenPype/pull/3888)\n\n**🔀 Refactored code**\n\n- Flame: Import lib functions from lib [\\#3992](https://github.com/pypeclub/OpenPype/pull/3992)\n- General: Fix deprecated warning in legacy creator [\\#3978](https://github.com/pypeclub/OpenPype/pull/3978)\n- Blender: Remove openpype api imports [\\#3977](https://github.com/pypeclub/OpenPype/pull/3977)\n- General: Use direct import of resources [\\#3964](https://github.com/pypeclub/OpenPype/pull/3964)\n- General: Direct settings imports [\\#3934](https://github.com/pypeclub/OpenPype/pull/3934)\n- General: import 'Logger' from 'openpype.lib' [\\#3926](https://github.com/pypeclub/OpenPype/pull/3926)\n- General: Remove deprecated functions from lib [\\#3907](https://github.com/pypeclub/OpenPype/pull/3907)\n\n**Merged pull requests:**\n\n- Maya + Yeti: Load Yeti Cache fix frame number recognition [\\#3942](https://github.com/pypeclub/OpenPype/pull/3942)\n- Fusion: Implement callbacks to Fusion's event system thread [\\#3928](https://github.com/pypeclub/OpenPype/pull/3928)\n- Photoshop: create single frame image in Ftrack as review [\\#3908](https://github.com/pypeclub/OpenPype/pull/3908)\n\n## [3.14.3](https://github.com/pypeclub/OpenPype/tree/3.14.3) (2022-10-03)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.2...3.14.3)\n\n**🚀 Enhancements**\n\n- Publisher: Enhancement proposals [\\#3897](https://github.com/pypeclub/OpenPype/pull/3897)\n\n**🐛 Bug fixes**\n\n- Maya: Fix Render single camera validator [\\#3929](https://github.com/pypeclub/OpenPype/pull/3929)\n- Flame: loading multilayer exr to batch/reel is working [\\#3901](https://github.com/pypeclub/OpenPype/pull/3901)\n- Hiero: Fix inventory check on launch [\\#3895](https://github.com/pypeclub/OpenPype/pull/3895)\n- WebPublisher: Fix import after refactor [\\#3891](https://github.com/pypeclub/OpenPype/pull/3891)\n\n**🔀 Refactored code**\n\n- Maya: Remove unused 'openpype.api' imports in plugins [\\#3925](https://github.com/pypeclub/OpenPype/pull/3925)\n- Resolve: Use new Extractor location [\\#3918](https://github.com/pypeclub/OpenPype/pull/3918)\n- Unreal: Use new Extractor location [\\#3917](https://github.com/pypeclub/OpenPype/pull/3917)\n- Flame: Use new Extractor location [\\#3916](https://github.com/pypeclub/OpenPype/pull/3916)\n- Houdini: Use new Extractor location [\\#3894](https://github.com/pypeclub/OpenPype/pull/3894)\n- Harmony: Use new Extractor location [\\#3893](https://github.com/pypeclub/OpenPype/pull/3893)\n\n**Merged pull requests:**\n\n- Maya: Fix Scene Inventory possibly starting off-screen due to maya preferences [\\#3923](https://github.com/pypeclub/OpenPype/pull/3923)\n\n## [3.14.2](https://github.com/pypeclub/OpenPype/tree/3.14.2) (2022-09-12)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.1...3.14.2)\n\n### 📖 Documentation\n\n- Documentation: Anatomy templates [\\#3618](https://github.com/pypeclub/OpenPype/pull/3618)\n\n**🆕 New features**\n\n- Nuke: Build workfile by template [\\#3763](https://github.com/pypeclub/OpenPype/pull/3763)\n- Houdini: Publishing workfiles [\\#3697](https://github.com/pypeclub/OpenPype/pull/3697)\n- Global: making collect audio plugin global [\\#3679](https://github.com/pypeclub/OpenPype/pull/3679)\n\n**🚀 Enhancements**\n\n- Flame: Adding Creator's retimed shot and handles switch [\\#3826](https://github.com/pypeclub/OpenPype/pull/3826)\n- Flame: OpenPype submenu to batch and media manager [\\#3825](https://github.com/pypeclub/OpenPype/pull/3825)\n- General: Better pixmap scaling [\\#3809](https://github.com/pypeclub/OpenPype/pull/3809)\n- Photoshop: attempt to speed up ExtractImage [\\#3793](https://github.com/pypeclub/OpenPype/pull/3793)\n- SyncServer: Added cli commands for sync server [\\#3765](https://github.com/pypeclub/OpenPype/pull/3765)\n- Kitsu: Drop 'entities root' setting. [\\#3739](https://github.com/pypeclub/OpenPype/pull/3739)\n- git: update gitignore [\\#3722](https://github.com/pypeclub/OpenPype/pull/3722)\n- Blender: Publisher collect workfile representation [\\#3670](https://github.com/pypeclub/OpenPype/pull/3670)\n- Maya: move set render settings menu entry [\\#3669](https://github.com/pypeclub/OpenPype/pull/3669)\n- Scene Inventory: Maya add actions to select from or to scene [\\#3659](https://github.com/pypeclub/OpenPype/pull/3659)\n- Scene Inventory: Add subsetGroup column [\\#3658](https://github.com/pypeclub/OpenPype/pull/3658)\n\n**🐛 Bug fixes**\n\n- General: Fix Pattern access in client code [\\#3828](https://github.com/pypeclub/OpenPype/pull/3828)\n- Launcher: Skip opening last work file works for groups [\\#3822](https://github.com/pypeclub/OpenPype/pull/3822)\n- Maya: Publishing data key change [\\#3811](https://github.com/pypeclub/OpenPype/pull/3811)\n- Igniter: Fix status handling when version is already installed [\\#3804](https://github.com/pypeclub/OpenPype/pull/3804)\n- Resolve: Addon import is Python 2 compatible [\\#3798](https://github.com/pypeclub/OpenPype/pull/3798)\n- Hiero: retimed clip publishing is working [\\#3792](https://github.com/pypeclub/OpenPype/pull/3792)\n- nuke: validate write node is not failing due wrong type [\\#3780](https://github.com/pypeclub/OpenPype/pull/3780)\n- Fix - changed format of version string in pyproject.toml [\\#3777](https://github.com/pypeclub/OpenPype/pull/3777)\n- Ftrack status fix typo prgoress -\\> progress [\\#3761](https://github.com/pypeclub/OpenPype/pull/3761)\n- Fix version resolution [\\#3757](https://github.com/pypeclub/OpenPype/pull/3757)\n- Maya: `containerise` dont skip empty values [\\#3674](https://github.com/pypeclub/OpenPype/pull/3674)\n\n**🔀 Refactored code**\n\n- Photoshop: Use new Extractor location [\\#3789](https://github.com/pypeclub/OpenPype/pull/3789)\n- Blender: Use new Extractor location [\\#3787](https://github.com/pypeclub/OpenPype/pull/3787)\n- AfterEffects: Use new Extractor location [\\#3784](https://github.com/pypeclub/OpenPype/pull/3784)\n- General: Remove unused teshost [\\#3773](https://github.com/pypeclub/OpenPype/pull/3773)\n- General: Copied 'Extractor' plugin to publish pipeline [\\#3771](https://github.com/pypeclub/OpenPype/pull/3771)\n- General: Move queries of asset and representation links [\\#3770](https://github.com/pypeclub/OpenPype/pull/3770)\n- General: Move create project folders to pipeline [\\#3768](https://github.com/pypeclub/OpenPype/pull/3768)\n- General: Create project function moved to client code [\\#3766](https://github.com/pypeclub/OpenPype/pull/3766)\n- Maya: Refactor submit deadline to use AbstractSubmitDeadline [\\#3759](https://github.com/pypeclub/OpenPype/pull/3759)\n- General: Change publish template settings location [\\#3755](https://github.com/pypeclub/OpenPype/pull/3755)\n- General: Move hostdirname functionality into host [\\#3749](https://github.com/pypeclub/OpenPype/pull/3749)\n- General: Move publish utils to pipeline [\\#3745](https://github.com/pypeclub/OpenPype/pull/3745)\n- Houdini: Define houdini as addon [\\#3735](https://github.com/pypeclub/OpenPype/pull/3735)\n- Fusion: Defined fusion as addon [\\#3733](https://github.com/pypeclub/OpenPype/pull/3733)\n- Flame: Defined flame as addon [\\#3732](https://github.com/pypeclub/OpenPype/pull/3732)\n- Resolve: Define resolve as addon [\\#3727](https://github.com/pypeclub/OpenPype/pull/3727)\n\n**Merged pull requests:**\n\n- Standalone Publisher: Ignore empty labels, then still use name like other asset models [\\#3779](https://github.com/pypeclub/OpenPype/pull/3779)\n- Kitsu - sync\\_all\\_project - add list ignore\\_projects [\\#3776](https://github.com/pypeclub/OpenPype/pull/3776)\n\n## [3.14.1](https://github.com/pypeclub/OpenPype/tree/3.14.1) (2022-08-30)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.0...3.14.1)\n\n### 📖 Documentation\n\n- Documentation: Few updates [\\#3698](https://github.com/pypeclub/OpenPype/pull/3698)\n- Documentation: Settings development [\\#3660](https://github.com/pypeclub/OpenPype/pull/3660)\n\n**🆕 New features**\n\n- Webpublisher:change create flatten image into tri state [\\#3678](https://github.com/pypeclub/OpenPype/pull/3678)\n- Blender: validators code correction with settings and defaults [\\#3662](https://github.com/pypeclub/OpenPype/pull/3662)\n\n**🚀 Enhancements**\n\n- General: Thumbnail can use project roots [\\#3750](https://github.com/pypeclub/OpenPype/pull/3750)\n- Settings: Remove settings lock on tray exit [\\#3720](https://github.com/pypeclub/OpenPype/pull/3720)\n- General: Added helper getters to modules manager [\\#3712](https://github.com/pypeclub/OpenPype/pull/3712)\n- Unreal: Define unreal as module and use host class [\\#3701](https://github.com/pypeclub/OpenPype/pull/3701)\n- Settings: Lock settings UI session [\\#3700](https://github.com/pypeclub/OpenPype/pull/3700)\n- General: Benevolent context label collector [\\#3686](https://github.com/pypeclub/OpenPype/pull/3686)\n- Ftrack: Store ftrack entities on hierarchy integration to instances [\\#3677](https://github.com/pypeclub/OpenPype/pull/3677)\n- Ftrack: More logs related to auto sync value change [\\#3671](https://github.com/pypeclub/OpenPype/pull/3671)\n- Blender: ops refresh manager after process events [\\#3663](https://github.com/pypeclub/OpenPype/pull/3663)\n\n**🐛 Bug fixes**\n\n- Maya: Fix typo in getPanel argument `with_focus` -\\> `withFocus` [\\#3753](https://github.com/pypeclub/OpenPype/pull/3753)\n- General: Smaller fixes of imports [\\#3748](https://github.com/pypeclub/OpenPype/pull/3748)\n- General: Logger tweaks [\\#3741](https://github.com/pypeclub/OpenPype/pull/3741)\n- Nuke: missing job dependency if multiple bake streams [\\#3737](https://github.com/pypeclub/OpenPype/pull/3737)\n- Nuke: color-space settings from anatomy is working [\\#3721](https://github.com/pypeclub/OpenPype/pull/3721)\n- Settings: Fix studio default anatomy save [\\#3716](https://github.com/pypeclub/OpenPype/pull/3716)\n- Maya: Use project name instead of project code [\\#3709](https://github.com/pypeclub/OpenPype/pull/3709)\n- Settings: Fix project overrides save [\\#3708](https://github.com/pypeclub/OpenPype/pull/3708)\n- Workfiles tool: Fix published workfile filtering [\\#3704](https://github.com/pypeclub/OpenPype/pull/3704)\n- PS, AE: Provide default variant value for workfile subset [\\#3703](https://github.com/pypeclub/OpenPype/pull/3703)\n- RoyalRender: handle host name that is not set [\\#3695](https://github.com/pypeclub/OpenPype/pull/3695)\n- Flame: retime is working on clip publishing [\\#3684](https://github.com/pypeclub/OpenPype/pull/3684)\n- Webpublisher: added check for empty context [\\#3682](https://github.com/pypeclub/OpenPype/pull/3682)\n\n**🔀 Refactored code**\n\n- General: Move delivery logic to pipeline [\\#3751](https://github.com/pypeclub/OpenPype/pull/3751)\n- General: Host addons cleanup [\\#3744](https://github.com/pypeclub/OpenPype/pull/3744)\n- Webpublisher: Webpublisher is used as addon [\\#3740](https://github.com/pypeclub/OpenPype/pull/3740)\n- Photoshop: Defined photoshop as addon [\\#3736](https://github.com/pypeclub/OpenPype/pull/3736)\n- Harmony: Defined harmony as addon [\\#3734](https://github.com/pypeclub/OpenPype/pull/3734)\n- General: Module interfaces cleanup [\\#3731](https://github.com/pypeclub/OpenPype/pull/3731)\n- AfterEffects: Move AE functions from general lib [\\#3730](https://github.com/pypeclub/OpenPype/pull/3730)\n- Blender: Define blender as module [\\#3729](https://github.com/pypeclub/OpenPype/pull/3729)\n- AfterEffects: Define AfterEffects as module [\\#3728](https://github.com/pypeclub/OpenPype/pull/3728)\n- General: Replace PypeLogger with Logger [\\#3725](https://github.com/pypeclub/OpenPype/pull/3725)\n- Nuke: Define nuke as module [\\#3724](https://github.com/pypeclub/OpenPype/pull/3724)\n- General: Move subset name functionality [\\#3723](https://github.com/pypeclub/OpenPype/pull/3723)\n- General: Move creators plugin getter [\\#3714](https://github.com/pypeclub/OpenPype/pull/3714)\n- General: Move constants from lib to client [\\#3713](https://github.com/pypeclub/OpenPype/pull/3713)\n- Loader: Subset groups using client operations [\\#3710](https://github.com/pypeclub/OpenPype/pull/3710)\n- TVPaint: Defined as module [\\#3707](https://github.com/pypeclub/OpenPype/pull/3707)\n- StandalonePublisher: Define StandalonePublisher as module [\\#3706](https://github.com/pypeclub/OpenPype/pull/3706)\n- TrayPublisher: Define TrayPublisher as module [\\#3705](https://github.com/pypeclub/OpenPype/pull/3705)\n- General: Move context specific functions to context tools [\\#3702](https://github.com/pypeclub/OpenPype/pull/3702)\n\n**Merged pull requests:**\n\n- Hiero: Define hiero as module [\\#3717](https://github.com/pypeclub/OpenPype/pull/3717)\n- Deadline: better logging for DL webservice failures [\\#3694](https://github.com/pypeclub/OpenPype/pull/3694)\n- Photoshop: resize saved images in ExtractReview for ffmpeg [\\#3676](https://github.com/pypeclub/OpenPype/pull/3676)\n- Nuke: Validation refactory to new publisher [\\#3567](https://github.com/pypeclub/OpenPype/pull/3567)\n\n## [3.14.0](https://github.com/pypeclub/OpenPype/tree/3.14.0) (2022-08-18)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.13.0...3.14.0)\n\n**🆕 New features**\n\n- Maya: Build workfile by template [\\#3578](https://github.com/pypeclub/OpenPype/pull/3578)\n- Maya: Implementation of JSON layout for Unreal workflow [\\#3353](https://github.com/pypeclub/OpenPype/pull/3353)\n- Maya: Build workfile by template [\\#3315](https://github.com/pypeclub/OpenPype/pull/3315)\n\n**🚀 Enhancements**\n\n- Ftrack: Addiotional component metadata [\\#3685](https://github.com/pypeclub/OpenPype/pull/3685)\n- Ftrack: Set task status on farm publishing [\\#3680](https://github.com/pypeclub/OpenPype/pull/3680)\n- Ftrack: Set task status on task creation in integrate hierarchy [\\#3675](https://github.com/pypeclub/OpenPype/pull/3675)\n- Maya: Disable rendering of all lights for render instances submitted through Deadline. [\\#3661](https://github.com/pypeclub/OpenPype/pull/3661)\n- General: Optimized OCIO configs [\\#3650](https://github.com/pypeclub/OpenPype/pull/3650)\n\n**🐛 Bug fixes**\n\n- General: Switch from hero version to versioned works [\\#3691](https://github.com/pypeclub/OpenPype/pull/3691)\n- General: Fix finding of last version [\\#3656](https://github.com/pypeclub/OpenPype/pull/3656)\n- General: Extract Review can scale with pixel aspect ratio [\\#3644](https://github.com/pypeclub/OpenPype/pull/3644)\n- Maya: Refactor moved usage of CreateRender settings [\\#3643](https://github.com/pypeclub/OpenPype/pull/3643)\n- General: Hero version representations have full context [\\#3638](https://github.com/pypeclub/OpenPype/pull/3638)\n- Nuke: color settings for render write node is working now [\\#3632](https://github.com/pypeclub/OpenPype/pull/3632)\n- Maya: FBX support for update in reference loader [\\#3631](https://github.com/pypeclub/OpenPype/pull/3631)\n\n**🔀 Refactored code**\n\n- General: Use client projects getter [\\#3673](https://github.com/pypeclub/OpenPype/pull/3673)\n- Resolve: Match folder structure to other hosts [\\#3653](https://github.com/pypeclub/OpenPype/pull/3653)\n- Maya: Hosts as modules [\\#3647](https://github.com/pypeclub/OpenPype/pull/3647)\n- TimersManager: Plugins are in timers manager module [\\#3639](https://github.com/pypeclub/OpenPype/pull/3639)\n- General: Move workfiles functions into pipeline [\\#3637](https://github.com/pypeclub/OpenPype/pull/3637)\n- General: Workfiles builder using query functions [\\#3598](https://github.com/pypeclub/OpenPype/pull/3598)\n\n**Merged pull requests:**\n\n- Deadline: Global job pre load is not Pype 2 compatible [\\#3666](https://github.com/pypeclub/OpenPype/pull/3666)\n- Maya: Remove unused get current renderer logic [\\#3645](https://github.com/pypeclub/OpenPype/pull/3645)\n- Kitsu|Fix: Movie project type fails & first loop children names [\\#3636](https://github.com/pypeclub/OpenPype/pull/3636)\n- fix the bug of failing to extract look when UDIMs format used in AiImage [\\#3628](https://github.com/pypeclub/OpenPype/pull/3628)\n\n## [3.13.0](https://github.com/pypeclub/OpenPype/tree/3.13.0) (2022-08-09)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.2...3.13.0)\n\n**🆕 New features**\n\n- Support for mutliple installed versions - 3.13 [\\#3605](https://github.com/pypeclub/OpenPype/pull/3605)\n- Traypublisher: simple editorial publishing [\\#3492](https://github.com/pypeclub/OpenPype/pull/3492)\n\n**🚀 Enhancements**\n\n- Editorial: Mix audio use side file for ffmpeg filters [\\#3630](https://github.com/pypeclub/OpenPype/pull/3630)\n- Ftrack: Comment template can contain optional keys [\\#3615](https://github.com/pypeclub/OpenPype/pull/3615)\n- Ftrack: Add more metadata to ftrack components [\\#3612](https://github.com/pypeclub/OpenPype/pull/3612)\n- General: Add context to pyblish context [\\#3594](https://github.com/pypeclub/OpenPype/pull/3594)\n- Kitsu: Shot&Sequence name with prefix over appends [\\#3593](https://github.com/pypeclub/OpenPype/pull/3593)\n- Photoshop: implemented {layer} placeholder in subset template [\\#3591](https://github.com/pypeclub/OpenPype/pull/3591)\n- General: Python module appdirs from git [\\#3589](https://github.com/pypeclub/OpenPype/pull/3589)\n- Ftrack: Update ftrack api to 2.3.3 [\\#3588](https://github.com/pypeclub/OpenPype/pull/3588)\n- General: New Integrator small fixes [\\#3583](https://github.com/pypeclub/OpenPype/pull/3583)\n- Maya: Render Creator has configurable options. [\\#3097](https://github.com/pypeclub/OpenPype/pull/3097)\n\n**🐛 Bug fixes**\n\n- Maya:  fix aov separator in Redshift [\\#3625](https://github.com/pypeclub/OpenPype/pull/3625)\n- Fix for multi-version build on Mac [\\#3622](https://github.com/pypeclub/OpenPype/pull/3622)\n- Ftrack: Sync hierarchical attributes can handle new created entities [\\#3621](https://github.com/pypeclub/OpenPype/pull/3621)\n- General: Extract review aspect ratio scale is calculated by ffmpeg [\\#3620](https://github.com/pypeclub/OpenPype/pull/3620)\n- Maya: Fix types of default settings [\\#3617](https://github.com/pypeclub/OpenPype/pull/3617)\n- Integrator: Don't force to have dot before frame [\\#3611](https://github.com/pypeclub/OpenPype/pull/3611)\n- AfterEffects: refactored integrate doesnt work formulti frame publishes [\\#3610](https://github.com/pypeclub/OpenPype/pull/3610)\n- Maya look data contents fails with custom attribute on group [\\#3607](https://github.com/pypeclub/OpenPype/pull/3607)\n- TrayPublisher: Fix wrong conflict merge [\\#3600](https://github.com/pypeclub/OpenPype/pull/3600)\n- Bugfix: Add OCIO as submodule to prepare for handling `maketx` color space conversion. [\\#3590](https://github.com/pypeclub/OpenPype/pull/3590)\n- Fix general settings environment variables resolution [\\#3587](https://github.com/pypeclub/OpenPype/pull/3587)\n- Editorial publishing workflow improvements [\\#3580](https://github.com/pypeclub/OpenPype/pull/3580)\n- General: Update imports in start script [\\#3579](https://github.com/pypeclub/OpenPype/pull/3579)\n- Nuke: render family integration consistency  [\\#3576](https://github.com/pypeclub/OpenPype/pull/3576)\n- Ftrack: Handle missing published path in integrator [\\#3570](https://github.com/pypeclub/OpenPype/pull/3570)\n- Nuke: publish existing frames with slate with correct range [\\#3555](https://github.com/pypeclub/OpenPype/pull/3555)\n\n**🔀 Refactored code**\n\n- General: Plugin settings handled by plugins [\\#3623](https://github.com/pypeclub/OpenPype/pull/3623)\n- General: Naive implementation of document create, update, delete [\\#3601](https://github.com/pypeclub/OpenPype/pull/3601)\n- General: Use query functions in general code [\\#3596](https://github.com/pypeclub/OpenPype/pull/3596)\n- General: Separate extraction of template data into more functions [\\#3574](https://github.com/pypeclub/OpenPype/pull/3574)\n- General: Lib cleanup [\\#3571](https://github.com/pypeclub/OpenPype/pull/3571)\n\n**Merged pull requests:**\n\n- Webpublisher: timeout for PS studio processing [\\#3619](https://github.com/pypeclub/OpenPype/pull/3619)\n- Core: translated validate\\_containers.py into New publisher style [\\#3614](https://github.com/pypeclub/OpenPype/pull/3614)\n- Enable write color sets on animation publish automatically [\\#3582](https://github.com/pypeclub/OpenPype/pull/3582)\n\n## [3.12.2](https://github.com/pypeclub/OpenPype/tree/3.12.2) (2022-07-27)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.1...3.12.2)\n\n### 📖 Documentation\n\n- Update website with more studios [\\#3554](https://github.com/pypeclub/OpenPype/pull/3554)\n- Documentation: Update publishing dev docs [\\#3549](https://github.com/pypeclub/OpenPype/pull/3549)\n\n**🚀 Enhancements**\n\n- General: Global thumbnail extractor is ready for more cases [\\#3561](https://github.com/pypeclub/OpenPype/pull/3561)\n- Maya: add additional validators to Settings [\\#3540](https://github.com/pypeclub/OpenPype/pull/3540)\n- General: Interactive console in cli [\\#3526](https://github.com/pypeclub/OpenPype/pull/3526)\n- Ftrack: Automatic daily review session creation can define trigger hour [\\#3516](https://github.com/pypeclub/OpenPype/pull/3516)\n- Ftrack: add source into Note [\\#3509](https://github.com/pypeclub/OpenPype/pull/3509)\n- Ftrack: Trigger custom ftrack topic of project structure creation [\\#3506](https://github.com/pypeclub/OpenPype/pull/3506)\n- Settings UI: Add extract to file action on project view [\\#3505](https://github.com/pypeclub/OpenPype/pull/3505)\n- Add pack and unpack convenience scripts [\\#3502](https://github.com/pypeclub/OpenPype/pull/3502)\n- General: Event system [\\#3499](https://github.com/pypeclub/OpenPype/pull/3499)\n- NewPublisher: Keep plugins with mismatch target in report [\\#3498](https://github.com/pypeclub/OpenPype/pull/3498)\n- Nuke: load clip with options from settings [\\#3497](https://github.com/pypeclub/OpenPype/pull/3497)\n- TrayPublisher: implemented render\\_mov\\_batch  [\\#3486](https://github.com/pypeclub/OpenPype/pull/3486)\n- Migrate basic families to the new Tray Publisher [\\#3469](https://github.com/pypeclub/OpenPype/pull/3469)\n- Enhance powershell build scripts [\\#1827](https://github.com/pypeclub/OpenPype/pull/1827)\n\n**🐛 Bug fixes**\n\n- Maya: fix Review image plane attribute  [\\#3569](https://github.com/pypeclub/OpenPype/pull/3569)\n- Maya: Fix animated attributes \\(ie. overscan\\) on loaded cameras breaking review publishing. [\\#3562](https://github.com/pypeclub/OpenPype/pull/3562)\n- NewPublisher: Python 2 compatible html escape [\\#3559](https://github.com/pypeclub/OpenPype/pull/3559)\n- Remove invalid submodules from `/vendor` [\\#3557](https://github.com/pypeclub/OpenPype/pull/3557)\n- General: Remove hosts filter on integrator plugins [\\#3556](https://github.com/pypeclub/OpenPype/pull/3556)\n- Settings: Clean default values of environments [\\#3550](https://github.com/pypeclub/OpenPype/pull/3550)\n- Module interfaces: Fix import error [\\#3547](https://github.com/pypeclub/OpenPype/pull/3547)\n- Workfiles tool: Show of tool and it's flags [\\#3539](https://github.com/pypeclub/OpenPype/pull/3539)\n- General: Create workfile documents works again [\\#3538](https://github.com/pypeclub/OpenPype/pull/3538)\n- Additional fixes for powershell scripts [\\#3525](https://github.com/pypeclub/OpenPype/pull/3525)\n- Maya: Added wrapper around cmds.setAttr [\\#3523](https://github.com/pypeclub/OpenPype/pull/3523)\n- Nuke: double slate [\\#3521](https://github.com/pypeclub/OpenPype/pull/3521)\n- General: Fix hash of centos oiio archive [\\#3519](https://github.com/pypeclub/OpenPype/pull/3519)\n- Maya: Renderman display output fix [\\#3514](https://github.com/pypeclub/OpenPype/pull/3514)\n- TrayPublisher: Simple creation enhancements and fixes [\\#3513](https://github.com/pypeclub/OpenPype/pull/3513)\n- NewPublisher: Publish attributes are properly collected [\\#3510](https://github.com/pypeclub/OpenPype/pull/3510)\n- TrayPublisher: Make sure host name is filled [\\#3504](https://github.com/pypeclub/OpenPype/pull/3504)\n- NewPublisher: Groups work and enum multivalue [\\#3501](https://github.com/pypeclub/OpenPype/pull/3501)\n\n**🔀 Refactored code**\n\n- General: Use query functions in integrator [\\#3563](https://github.com/pypeclub/OpenPype/pull/3563)\n- General: Mongo core connection moved to client [\\#3531](https://github.com/pypeclub/OpenPype/pull/3531)\n- Refactor Integrate Asset [\\#3530](https://github.com/pypeclub/OpenPype/pull/3530)\n- General: Client docstrings cleanup [\\#3529](https://github.com/pypeclub/OpenPype/pull/3529)\n- General: Move load related functions into pipeline [\\#3527](https://github.com/pypeclub/OpenPype/pull/3527)\n- General: Get current context document functions [\\#3522](https://github.com/pypeclub/OpenPype/pull/3522)\n- Kitsu: Use query function from client [\\#3496](https://github.com/pypeclub/OpenPype/pull/3496)\n- TimersManager: Use query functions [\\#3495](https://github.com/pypeclub/OpenPype/pull/3495)\n- Deadline: Use query functions [\\#3466](https://github.com/pypeclub/OpenPype/pull/3466)\n- Refactor Integrate Asset [\\#2898](https://github.com/pypeclub/OpenPype/pull/2898)\n\n**Merged pull requests:**\n\n- Maya: fix active pane loss [\\#3566](https://github.com/pypeclub/OpenPype/pull/3566)\n\n## [3.12.1](https://github.com/pypeclub/OpenPype/tree/3.12.1) (2022-07-13)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.0...3.12.1)\n\n### 📖 Documentation\n\n- Docs: Added minimal permissions for MongoDB [\\#3441](https://github.com/pypeclub/OpenPype/pull/3441)\n\n**🆕 New features**\n\n- Maya: Add VDB to Arnold loader [\\#3433](https://github.com/pypeclub/OpenPype/pull/3433)\n\n**🚀 Enhancements**\n\n- TrayPublisher: Added more options for grouping of instances [\\#3494](https://github.com/pypeclub/OpenPype/pull/3494)\n- NewPublisher: Align creator attributes from top to bottom [\\#3487](https://github.com/pypeclub/OpenPype/pull/3487)\n- NewPublisher: Added ability to use label of instance [\\#3484](https://github.com/pypeclub/OpenPype/pull/3484)\n- General: Creator Plugins have access to project [\\#3476](https://github.com/pypeclub/OpenPype/pull/3476)\n- General: Better arguments order in creator init [\\#3475](https://github.com/pypeclub/OpenPype/pull/3475)\n- Ftrack: Trigger custom ftrack events on project creation and preparation [\\#3465](https://github.com/pypeclub/OpenPype/pull/3465)\n- Windows installer: Clean old files and add version subfolder [\\#3445](https://github.com/pypeclub/OpenPype/pull/3445)\n- Blender: Bugfix - Set fps properly on open [\\#3426](https://github.com/pypeclub/OpenPype/pull/3426)\n- Hiero: Add custom scripts menu [\\#3425](https://github.com/pypeclub/OpenPype/pull/3425)\n- Blender: pre pyside install for all platforms [\\#3400](https://github.com/pypeclub/OpenPype/pull/3400)\n- Maya: Add additional playblast options to review Extractor. [\\#3384](https://github.com/pypeclub/OpenPype/pull/3384)\n- Maya: Ability to set resolution for playblasts from asset, and override through review instance. [\\#3360](https://github.com/pypeclub/OpenPype/pull/3360)\n- Maya: Redshift Volume Loader Implement update, remove, switch + fix vdb sequence support [\\#3197](https://github.com/pypeclub/OpenPype/pull/3197)\n- Maya: Implement `iter_visible_nodes_in_range` for extracting Alembics [\\#3100](https://github.com/pypeclub/OpenPype/pull/3100)\n\n**🐛 Bug fixes**\n\n- TrayPublisher: Keep use instance label in list view [\\#3493](https://github.com/pypeclub/OpenPype/pull/3493)\n- General: Extract review use first frame of input sequence [\\#3491](https://github.com/pypeclub/OpenPype/pull/3491)\n- General: Fix Plist loading for application launch [\\#3485](https://github.com/pypeclub/OpenPype/pull/3485)\n- Nuke: Workfile tools open on start [\\#3479](https://github.com/pypeclub/OpenPype/pull/3479)\n- New Publisher: Disabled context change allows creation [\\#3478](https://github.com/pypeclub/OpenPype/pull/3478)\n- General: thumbnail extractor fix [\\#3474](https://github.com/pypeclub/OpenPype/pull/3474)\n- Kitsu: bugfix with sync-service ans publish plugins [\\#3473](https://github.com/pypeclub/OpenPype/pull/3473)\n- Flame: solved problem with multi-selected loading [\\#3470](https://github.com/pypeclub/OpenPype/pull/3470)\n- General: Fix query function in update logic [\\#3468](https://github.com/pypeclub/OpenPype/pull/3468)\n- Resolve: removed few bugs [\\#3464](https://github.com/pypeclub/OpenPype/pull/3464)\n- General: Delete old versions is safer when ftrack is disabled [\\#3462](https://github.com/pypeclub/OpenPype/pull/3462)\n- Nuke: fixing metadata slate TC difference [\\#3455](https://github.com/pypeclub/OpenPype/pull/3455)\n- Nuke: prerender reviewable fails [\\#3450](https://github.com/pypeclub/OpenPype/pull/3450)\n- Maya: fix hashing in Python 3 for tile rendering [\\#3447](https://github.com/pypeclub/OpenPype/pull/3447)\n- LogViewer: Escape html characters in log message [\\#3443](https://github.com/pypeclub/OpenPype/pull/3443)\n- Nuke: Slate frame is integrated [\\#3427](https://github.com/pypeclub/OpenPype/pull/3427)\n- Maya: Camera extra data - additional fix for \\#3304 [\\#3386](https://github.com/pypeclub/OpenPype/pull/3386)\n- Maya: Handle excluding `model` family from frame range validator. [\\#3370](https://github.com/pypeclub/OpenPype/pull/3370)\n\n**🔀 Refactored code**\n\n- Maya: Merge animation + pointcache extractor logic [\\#3461](https://github.com/pypeclub/OpenPype/pull/3461)\n- Maya: Re-use `maintained_time` from lib [\\#3460](https://github.com/pypeclub/OpenPype/pull/3460)\n- General: Use query functions in global plugins [\\#3459](https://github.com/pypeclub/OpenPype/pull/3459)\n- Clockify: Use query functions in clockify actions [\\#3458](https://github.com/pypeclub/OpenPype/pull/3458)\n- General: Use query functions in rest api calls [\\#3457](https://github.com/pypeclub/OpenPype/pull/3457)\n- General: Use query functions in openpype lib functions [\\#3454](https://github.com/pypeclub/OpenPype/pull/3454)\n- General: Use query functions in load utils [\\#3446](https://github.com/pypeclub/OpenPype/pull/3446)\n- General: Move publish plugin and publish render abstractions [\\#3442](https://github.com/pypeclub/OpenPype/pull/3442)\n- General: Use Anatomy after move to pipeline [\\#3436](https://github.com/pypeclub/OpenPype/pull/3436)\n- General: Anatomy moved to pipeline [\\#3435](https://github.com/pypeclub/OpenPype/pull/3435)\n- Fusion: Use client query functions [\\#3380](https://github.com/pypeclub/OpenPype/pull/3380)\n- Resolve: Use client query functions [\\#3379](https://github.com/pypeclub/OpenPype/pull/3379)\n- General: Host implementation defined with class [\\#3337](https://github.com/pypeclub/OpenPype/pull/3337)\n\n## [3.12.0](https://github.com/pypeclub/OpenPype/tree/3.12.0) (2022-06-28)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.11.1...3.12.0)\n\n### 📖 Documentation\n\n- Fix typo in documentation: pyenv on mac [\\#3417](https://github.com/pypeclub/OpenPype/pull/3417)\n- Linux: update OIIO package [\\#3401](https://github.com/pypeclub/OpenPype/pull/3401)\n\n**🆕 New features**\n\n- Shotgrid: Add production beta of shotgrid integration [\\#2921](https://github.com/pypeclub/OpenPype/pull/2921)\n\n**🚀 Enhancements**\n\n- Webserver: Added CORS middleware [\\#3422](https://github.com/pypeclub/OpenPype/pull/3422)\n- Attribute Defs UI: Files widget show what is allowed to drop in [\\#3411](https://github.com/pypeclub/OpenPype/pull/3411)\n- General: Add ability to change user value for templates [\\#3366](https://github.com/pypeclub/OpenPype/pull/3366)\n- Hosts: More options for in-host callbacks [\\#3357](https://github.com/pypeclub/OpenPype/pull/3357)\n- Multiverse: expose some settings to GUI [\\#3350](https://github.com/pypeclub/OpenPype/pull/3350)\n- Maya: Allow more data to be published along camera 🎥  [\\#3304](https://github.com/pypeclub/OpenPype/pull/3304)\n- Add root keys and project keys to create starting folder [\\#2755](https://github.com/pypeclub/OpenPype/pull/2755)\n\n**🐛 Bug fixes**\n\n- NewPublisher: Fix subset name change on change of creator plugin [\\#3420](https://github.com/pypeclub/OpenPype/pull/3420)\n- Bug: fix invalid avalon import [\\#3418](https://github.com/pypeclub/OpenPype/pull/3418)\n- Nuke: Fix keyword argument in query function [\\#3414](https://github.com/pypeclub/OpenPype/pull/3414)\n- Houdini: fix loading and updating vbd/bgeo sequences [\\#3408](https://github.com/pypeclub/OpenPype/pull/3408)\n- Nuke: Collect representation files based on Write [\\#3407](https://github.com/pypeclub/OpenPype/pull/3407)\n- General: Filter representations before integration start [\\#3398](https://github.com/pypeclub/OpenPype/pull/3398)\n- Maya: look collector typo [\\#3392](https://github.com/pypeclub/OpenPype/pull/3392)\n- TVPaint: Make sure exit code is set to not None [\\#3382](https://github.com/pypeclub/OpenPype/pull/3382)\n- Maya: vray device aspect ratio fix [\\#3381](https://github.com/pypeclub/OpenPype/pull/3381)\n- Flame: bunch of publishing issues [\\#3377](https://github.com/pypeclub/OpenPype/pull/3377)\n- Harmony: added unc path to zifile command in Harmony [\\#3372](https://github.com/pypeclub/OpenPype/pull/3372)\n- Standalone: settings improvements [\\#3355](https://github.com/pypeclub/OpenPype/pull/3355)\n- Nuke: Load full model hierarchy by default [\\#3328](https://github.com/pypeclub/OpenPype/pull/3328)\n- Nuke: multiple baking streams with correct slate [\\#3245](https://github.com/pypeclub/OpenPype/pull/3245)\n- Maya: fix image prefix warning in validator [\\#3128](https://github.com/pypeclub/OpenPype/pull/3128)\n\n**🔀 Refactored code**\n\n- Unreal: Use client query functions [\\#3421](https://github.com/pypeclub/OpenPype/pull/3421)\n- General: Move editorial lib to pipeline [\\#3419](https://github.com/pypeclub/OpenPype/pull/3419)\n- Kitsu: renaming to plural func sync\\_all\\_projects [\\#3397](https://github.com/pypeclub/OpenPype/pull/3397)\n- Houdini: Use client query functions [\\#3395](https://github.com/pypeclub/OpenPype/pull/3395)\n- Hiero: Use client query functions [\\#3393](https://github.com/pypeclub/OpenPype/pull/3393)\n- Nuke: Use client query functions [\\#3391](https://github.com/pypeclub/OpenPype/pull/3391)\n- Maya: Use client query functions [\\#3385](https://github.com/pypeclub/OpenPype/pull/3385)\n- Harmony: Use client query functions [\\#3378](https://github.com/pypeclub/OpenPype/pull/3378)\n- Celaction: Use client query functions [\\#3376](https://github.com/pypeclub/OpenPype/pull/3376)\n- Photoshop: Use client query functions [\\#3375](https://github.com/pypeclub/OpenPype/pull/3375)\n- AfterEffects: Use client query functions [\\#3374](https://github.com/pypeclub/OpenPype/pull/3374)\n- TVPaint: Use client query functions [\\#3340](https://github.com/pypeclub/OpenPype/pull/3340)\n- Ftrack: Use client query functions [\\#3339](https://github.com/pypeclub/OpenPype/pull/3339)\n- Standalone Publisher: Use client query functions [\\#3330](https://github.com/pypeclub/OpenPype/pull/3330)\n\n**Merged pull requests:**\n\n- Sync Queue: Added far future value for null values for dates [\\#3371](https://github.com/pypeclub/OpenPype/pull/3371)\n- Maya - added support for single frame playblast review [\\#3369](https://github.com/pypeclub/OpenPype/pull/3369)\n- Houdini: Implement Redshift Proxy Export [\\#3196](https://github.com/pypeclub/OpenPype/pull/3196)\n\n## [3.11.1](https://github.com/pypeclub/OpenPype/tree/3.11.1) (2022-06-20)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.11.0...3.11.1)\n\n**🆕 New features**\n\n- Flame: custom export temp folder [\\#3346](https://github.com/pypeclub/OpenPype/pull/3346)\n- Nuke: removing third-party plugins   [\\#3344](https://github.com/pypeclub/OpenPype/pull/3344)\n\n**🚀 Enhancements**\n\n- Pyblish Pype: Hiding/Close issues [\\#3367](https://github.com/pypeclub/OpenPype/pull/3367)\n- Ftrack: Removed requirement of pypeclub role from default settings [\\#3354](https://github.com/pypeclub/OpenPype/pull/3354)\n- Kitsu: Prevent crash on missing frames information [\\#3352](https://github.com/pypeclub/OpenPype/pull/3352)\n- Ftrack: Open browser from tray [\\#3320](https://github.com/pypeclub/OpenPype/pull/3320)\n- Enhancement: More control over thumbnail processing. [\\#3259](https://github.com/pypeclub/OpenPype/pull/3259)\n\n**🐛 Bug fixes**\n\n- Nuke: bake streams with slate on farm [\\#3368](https://github.com/pypeclub/OpenPype/pull/3368)\n- Harmony: audio validator has wrong logic [\\#3364](https://github.com/pypeclub/OpenPype/pull/3364)\n- Nuke: Fix missing variable in extract thumbnail [\\#3363](https://github.com/pypeclub/OpenPype/pull/3363)\n- Nuke: Fix precollect writes [\\#3361](https://github.com/pypeclub/OpenPype/pull/3361)\n- AE- fix validate\\_scene\\_settings and renderLocal [\\#3358](https://github.com/pypeclub/OpenPype/pull/3358)\n- deadline: fixing misidentification of revieables [\\#3356](https://github.com/pypeclub/OpenPype/pull/3356)\n- General: Create only one thumbnail per instance [\\#3351](https://github.com/pypeclub/OpenPype/pull/3351)\n- nuke: adding extract thumbnail settings 3.10 [\\#3347](https://github.com/pypeclub/OpenPype/pull/3347)\n- General: Fix last version function [\\#3345](https://github.com/pypeclub/OpenPype/pull/3345)\n- Deadline: added OPENPYPE\\_MONGO to filter [\\#3336](https://github.com/pypeclub/OpenPype/pull/3336)\n- Nuke: fixing farm publishing if review is disabled [\\#3306](https://github.com/pypeclub/OpenPype/pull/3306)\n- Maya: Fix Yeti errors on Create, Publish and Load [\\#3198](https://github.com/pypeclub/OpenPype/pull/3198)\n\n**🔀 Refactored code**\n\n- Webpublisher: Use client query functions [\\#3333](https://github.com/pypeclub/OpenPype/pull/3333)\n\n## [3.11.0](https://github.com/pypeclub/OpenPype/tree/3.11.0) (2022-06-17)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.10.0...3.11.0)\n\n### 📖 Documentation\n\n- Documentation: Add app key to template documentation [\\#3299](https://github.com/pypeclub/OpenPype/pull/3299)\n- doc: adding royal render and multiverse to the web site [\\#3285](https://github.com/pypeclub/OpenPype/pull/3285)\n- Module: Kitsu module [\\#2650](https://github.com/pypeclub/OpenPype/pull/2650)\n\n**🆕 New features**\n\n- Multiverse: fixed composition write, full docs, cosmetics [\\#3178](https://github.com/pypeclub/OpenPype/pull/3178)\n\n**🚀 Enhancements**\n\n- Settings: Settings can be extracted from UI [\\#3323](https://github.com/pypeclub/OpenPype/pull/3323)\n- updated poetry installation source [\\#3316](https://github.com/pypeclub/OpenPype/pull/3316)\n- Ftrack: Action to easily create daily review session [\\#3310](https://github.com/pypeclub/OpenPype/pull/3310)\n- TVPaint: Extractor use mark in/out range to render [\\#3309](https://github.com/pypeclub/OpenPype/pull/3309)\n- Ftrack: Delivery action can work on ReviewSessions [\\#3307](https://github.com/pypeclub/OpenPype/pull/3307)\n- Maya: Look assigner UI improvements [\\#3298](https://github.com/pypeclub/OpenPype/pull/3298)\n- Ftrack: Action to transfer values of hierarchical attributes [\\#3284](https://github.com/pypeclub/OpenPype/pull/3284)\n- Maya: better handling of legacy review subsets names [\\#3269](https://github.com/pypeclub/OpenPype/pull/3269)\n- General: Updated windows oiio tool [\\#3268](https://github.com/pypeclub/OpenPype/pull/3268)\n- Unreal: add support for skeletalMesh and staticMesh to loaders [\\#3267](https://github.com/pypeclub/OpenPype/pull/3267)\n- Maya: reference loaders could store placeholder in referenced url [\\#3264](https://github.com/pypeclub/OpenPype/pull/3264)\n- TVPaint: Init file for TVPaint worker also handle guideline images [\\#3250](https://github.com/pypeclub/OpenPype/pull/3250)\n- Nuke: Change default icon path in settings [\\#3247](https://github.com/pypeclub/OpenPype/pull/3247)\n- Maya: publishing of animation and pointcache on a farm [\\#3225](https://github.com/pypeclub/OpenPype/pull/3225)\n- Maya: Look assigner UI improvements [\\#3208](https://github.com/pypeclub/OpenPype/pull/3208)\n- Nuke: add pointcache and animation to loader [\\#3186](https://github.com/pypeclub/OpenPype/pull/3186)\n- Nuke: Add a gizmo menu [\\#3172](https://github.com/pypeclub/OpenPype/pull/3172)\n- Support for Unreal 5 [\\#3122](https://github.com/pypeclub/OpenPype/pull/3122)\n\n**🐛 Bug fixes**\n\n- General: Handle empty source key on instance [\\#3342](https://github.com/pypeclub/OpenPype/pull/3342)\n- Houdini: Fix Houdini VDB manage update wrong file attribute name [\\#3322](https://github.com/pypeclub/OpenPype/pull/3322)\n- Nuke: anatomy compatibility issue hacks [\\#3321](https://github.com/pypeclub/OpenPype/pull/3321)\n- hiero: otio p3 compatibility issue - metadata on effect use update 3.11 [\\#3314](https://github.com/pypeclub/OpenPype/pull/3314)\n- General: Vendorized modules for Python 2 and update poetry lock [\\#3305](https://github.com/pypeclub/OpenPype/pull/3305)\n- Fix - added local targets to install host [\\#3303](https://github.com/pypeclub/OpenPype/pull/3303)\n- Settings: Add missing default settings for nuke gizmo [\\#3301](https://github.com/pypeclub/OpenPype/pull/3301)\n- Maya: Fix swaped width and height in reviews [\\#3300](https://github.com/pypeclub/OpenPype/pull/3300)\n- Maya: point cache publish handles Maya instances [\\#3297](https://github.com/pypeclub/OpenPype/pull/3297)\n- Global: extract review slate issues [\\#3286](https://github.com/pypeclub/OpenPype/pull/3286)\n- Webpublisher: return only active projects in ProjectsEndpoint [\\#3281](https://github.com/pypeclub/OpenPype/pull/3281)\n- Hiero: add support for task tags 3.10.x [\\#3279](https://github.com/pypeclub/OpenPype/pull/3279)\n- General: Fix Oiio tool path resolving [\\#3278](https://github.com/pypeclub/OpenPype/pull/3278)\n- Maya: Fix udim support for e.g. uppercase \\<UDIM\\> tag [\\#3266](https://github.com/pypeclub/OpenPype/pull/3266)\n- Nuke: bake reformat was failing on string type [\\#3261](https://github.com/pypeclub/OpenPype/pull/3261)\n- Maya: hotfix Pxr multitexture in looks [\\#3260](https://github.com/pypeclub/OpenPype/pull/3260)\n- Unreal: Fix Camera Loading if Layout is missing [\\#3255](https://github.com/pypeclub/OpenPype/pull/3255)\n- Unreal: Fixed Animation loading in UE5 [\\#3240](https://github.com/pypeclub/OpenPype/pull/3240)\n- Unreal: Fixed Render creation in UE5 [\\#3239](https://github.com/pypeclub/OpenPype/pull/3239)\n- Unreal: Fixed Camera loading in UE5 [\\#3238](https://github.com/pypeclub/OpenPype/pull/3238)\n- Flame: debugging [\\#3224](https://github.com/pypeclub/OpenPype/pull/3224)\n- add silent audio to slate [\\#3162](https://github.com/pypeclub/OpenPype/pull/3162)\n- Add timecode to slate [\\#2929](https://github.com/pypeclub/OpenPype/pull/2929)\n\n**🔀 Refactored code**\n\n- Blender: Use client query functions [\\#3331](https://github.com/pypeclub/OpenPype/pull/3331)\n- General: Define query functions [\\#3288](https://github.com/pypeclub/OpenPype/pull/3288)\n\n**Merged pull requests:**\n\n- Maya: add pointcache family to gpu cache loader [\\#3318](https://github.com/pypeclub/OpenPype/pull/3318)\n- Maya look: skip empty file attributes [\\#3274](https://github.com/pypeclub/OpenPype/pull/3274)\n\n## [3.10.0](https://github.com/pypeclub/OpenPype/tree/3.10.0) (2022-05-26)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.8...3.10.0)\n\n### 📖 Documentation\n\n- Docs: add all-contributors config and initial list [\\#3094](https://github.com/pypeclub/OpenPype/pull/3094)\n- Nuke docs with videos [\\#3052](https://github.com/pypeclub/OpenPype/pull/3052)\n\n**🆕 New features**\n\n- General: OpenPype modules publish plugins are registered in host [\\#3180](https://github.com/pypeclub/OpenPype/pull/3180)\n- General: Creator plugins from addons can be registered [\\#3179](https://github.com/pypeclub/OpenPype/pull/3179)\n- Ftrack: Single image reviewable [\\#3157](https://github.com/pypeclub/OpenPype/pull/3157)\n- Nuke: Expose write attributes to settings [\\#3123](https://github.com/pypeclub/OpenPype/pull/3123)\n- Hiero: Initial frame publish support [\\#3106](https://github.com/pypeclub/OpenPype/pull/3106)\n- Unreal: Render Publishing [\\#2917](https://github.com/pypeclub/OpenPype/pull/2917)\n- AfterEffects: Implemented New Publisher [\\#2838](https://github.com/pypeclub/OpenPype/pull/2838)\n- Unreal: Rendering implementation [\\#2410](https://github.com/pypeclub/OpenPype/pull/2410)\n\n**🚀 Enhancements**\n\n- Maya: FBX camera export [\\#3253](https://github.com/pypeclub/OpenPype/pull/3253)\n- General: updating common vendor `scriptmenu` to 1.5.2 [\\#3246](https://github.com/pypeclub/OpenPype/pull/3246)\n- Project Manager: Allow to paste Tasks into multiple assets at the same time [\\#3226](https://github.com/pypeclub/OpenPype/pull/3226)\n- Project manager: Sped up project load [\\#3216](https://github.com/pypeclub/OpenPype/pull/3216)\n- Loader UI: Speed issues of loader with sync server [\\#3199](https://github.com/pypeclub/OpenPype/pull/3199)\n- Looks: add basic support for Renderman [\\#3190](https://github.com/pypeclub/OpenPype/pull/3190)\n- Maya: added clean\\_import option to Import loader [\\#3181](https://github.com/pypeclub/OpenPype/pull/3181)\n- Add the scripts menu definition to nuke [\\#3168](https://github.com/pypeclub/OpenPype/pull/3168)\n- Maya: add maya 2023 to default applications [\\#3167](https://github.com/pypeclub/OpenPype/pull/3167)\n- Compressed bgeo publishing in SAP and Houdini loader [\\#3153](https://github.com/pypeclub/OpenPype/pull/3153)\n- General: Add 'dataclasses' to required python modules [\\#3149](https://github.com/pypeclub/OpenPype/pull/3149)\n- Hooks: Tweak logging grammar [\\#3147](https://github.com/pypeclub/OpenPype/pull/3147)\n- Nuke: settings for reformat node in CreateWriteRender node [\\#3143](https://github.com/pypeclub/OpenPype/pull/3143)\n- Houdini: Add loader for alembic through Alembic Archive node [\\#3140](https://github.com/pypeclub/OpenPype/pull/3140)\n- Publisher: UI Modifications and fixes [\\#3139](https://github.com/pypeclub/OpenPype/pull/3139)\n- General: Simplified OP modules/addons import [\\#3137](https://github.com/pypeclub/OpenPype/pull/3137)\n- Terminal: Tweak coloring of TrayModuleManager logging enabled states [\\#3133](https://github.com/pypeclub/OpenPype/pull/3133)\n- General: Cleanup some Loader docstrings [\\#3131](https://github.com/pypeclub/OpenPype/pull/3131)\n- Nuke: render instance with subset name filtered overrides [\\#3117](https://github.com/pypeclub/OpenPype/pull/3117)\n- Unreal: Layout and Camera update and remove functions reimplemented and improvements [\\#3116](https://github.com/pypeclub/OpenPype/pull/3116)\n- Settings: Remove environment groups from settings [\\#3115](https://github.com/pypeclub/OpenPype/pull/3115)\n- TVPaint: Match renderlayer key with other hosts [\\#3110](https://github.com/pypeclub/OpenPype/pull/3110)\n- Ftrack: AssetVersion status on publish [\\#3108](https://github.com/pypeclub/OpenPype/pull/3108)\n- Tray publisher: Simple families from settings [\\#3105](https://github.com/pypeclub/OpenPype/pull/3105)\n- Local Settings UI: Overlay messages on save and reset [\\#3104](https://github.com/pypeclub/OpenPype/pull/3104)\n- General: Remove repos related logic [\\#3087](https://github.com/pypeclub/OpenPype/pull/3087)\n- Standalone publisher: add support for bgeo and vdb [\\#3080](https://github.com/pypeclub/OpenPype/pull/3080)\n- Houdini: Fix FPS + outdated content pop-ups [\\#3079](https://github.com/pypeclub/OpenPype/pull/3079)\n- General: Add global log verbose arguments [\\#3070](https://github.com/pypeclub/OpenPype/pull/3070)\n- Flame: extract presets distribution [\\#3063](https://github.com/pypeclub/OpenPype/pull/3063)\n- Update collect\\_render.py [\\#3055](https://github.com/pypeclub/OpenPype/pull/3055)\n- SiteSync: Added compute\\_resource\\_sync\\_sites to sync\\_server\\_module [\\#2983](https://github.com/pypeclub/OpenPype/pull/2983)\n- Maya: Implement Hardware Renderer 2.0 support for Render Products [\\#2611](https://github.com/pypeclub/OpenPype/pull/2611)\n\n**🐛 Bug fixes**\n\n- nuke: use framerange issue [\\#3254](https://github.com/pypeclub/OpenPype/pull/3254)\n- Ftrack: Chunk sizes for queries has minimal condition [\\#3244](https://github.com/pypeclub/OpenPype/pull/3244)\n- Maya: renderman displays needs to be filtered [\\#3242](https://github.com/pypeclub/OpenPype/pull/3242)\n- Ftrack: Validate that the user exists on ftrack [\\#3237](https://github.com/pypeclub/OpenPype/pull/3237)\n- Maya: Fix support for multiple resolutions [\\#3236](https://github.com/pypeclub/OpenPype/pull/3236)\n- TVPaint: Look for more groups than 12 [\\#3228](https://github.com/pypeclub/OpenPype/pull/3228)\n- Hiero: debugging frame range and other 3.10 [\\#3222](https://github.com/pypeclub/OpenPype/pull/3222)\n- Project Manager: Fix persistent editors on project change [\\#3218](https://github.com/pypeclub/OpenPype/pull/3218)\n- Deadline: instance data overwrite fix [\\#3214](https://github.com/pypeclub/OpenPype/pull/3214)\n- Ftrack: Push hierarchical attributes action works [\\#3210](https://github.com/pypeclub/OpenPype/pull/3210)\n- Standalone Publisher: Always create new representation for thumbnail [\\#3203](https://github.com/pypeclub/OpenPype/pull/3203)\n- Photoshop: skip collector when automatic testing [\\#3202](https://github.com/pypeclub/OpenPype/pull/3202)\n- Nuke: render/workfile version sync doesn't work on farm [\\#3185](https://github.com/pypeclub/OpenPype/pull/3185)\n- Ftrack: Review image only if there are no mp4 reviews [\\#3183](https://github.com/pypeclub/OpenPype/pull/3183)\n- Ftrack: Locations deepcopy issue [\\#3177](https://github.com/pypeclub/OpenPype/pull/3177)\n- General: Avoid creating multiple thumbnails [\\#3176](https://github.com/pypeclub/OpenPype/pull/3176)\n- General/Hiero: better clip duration calculation [\\#3169](https://github.com/pypeclub/OpenPype/pull/3169)\n- General: Oiio conversion for ffmpeg checks for invalid characters [\\#3166](https://github.com/pypeclub/OpenPype/pull/3166)\n- Fix for attaching render to subset [\\#3164](https://github.com/pypeclub/OpenPype/pull/3164)\n- Harmony: fixed missing task name in render instance [\\#3163](https://github.com/pypeclub/OpenPype/pull/3163)\n- Ftrack: Action delete old versions formatting works [\\#3152](https://github.com/pypeclub/OpenPype/pull/3152)\n- Deadline: fix the output directory [\\#3144](https://github.com/pypeclub/OpenPype/pull/3144)\n- General: New Session schema [\\#3141](https://github.com/pypeclub/OpenPype/pull/3141)\n- General: Missing version on headless mode crash properly [\\#3136](https://github.com/pypeclub/OpenPype/pull/3136)\n- TVPaint: Composite layers in reversed order [\\#3135](https://github.com/pypeclub/OpenPype/pull/3135)\n- Nuke: fixing default settings for workfile builder loaders [\\#3120](https://github.com/pypeclub/OpenPype/pull/3120)\n- Nuke: fix anatomy imageio regex default [\\#3119](https://github.com/pypeclub/OpenPype/pull/3119)\n- General: Python 3 compatibility in queries [\\#3112](https://github.com/pypeclub/OpenPype/pull/3112)\n- General: TemplateResult can be copied [\\#3099](https://github.com/pypeclub/OpenPype/pull/3099)\n- General: Collect loaded versions skips not existing representations [\\#3095](https://github.com/pypeclub/OpenPype/pull/3095)\n- RoyalRender Control Submission - AVALON\\_APP\\_NAME default [\\#3091](https://github.com/pypeclub/OpenPype/pull/3091)\n- Ftrack: Update Create Folders action [\\#3089](https://github.com/pypeclub/OpenPype/pull/3089)\n- Maya: Collect Render fix any render cameras check [\\#3088](https://github.com/pypeclub/OpenPype/pull/3088)\n- Project Manager: Avoid unnecessary updates of asset documents [\\#3083](https://github.com/pypeclub/OpenPype/pull/3083)\n- Standalone publisher: Fix plugins install [\\#3077](https://github.com/pypeclub/OpenPype/pull/3077)\n- General: Extract review sequence is not converted with same names [\\#3076](https://github.com/pypeclub/OpenPype/pull/3076)\n- Webpublisher: Use variant value [\\#3068](https://github.com/pypeclub/OpenPype/pull/3068)\n- Nuke: Add aov matching even for remainder and prerender [\\#3060](https://github.com/pypeclub/OpenPype/pull/3060)\n- Fix support for Renderman in Maya [\\#3006](https://github.com/pypeclub/OpenPype/pull/3006)\n\n**🔀 Refactored code**\n\n- Avalon repo removed from Jobs workflow [\\#3193](https://github.com/pypeclub/OpenPype/pull/3193)\n- General: Remove remaining imports from avalon [\\#3130](https://github.com/pypeclub/OpenPype/pull/3130)\n- General: Move mongo db logic and remove avalon repository [\\#3066](https://github.com/pypeclub/OpenPype/pull/3066)\n- General: Move host install [\\#3009](https://github.com/pypeclub/OpenPype/pull/3009)\n\n**Merged pull requests:**\n\n- Harmony: message length in 21.1 [\\#3257](https://github.com/pypeclub/OpenPype/pull/3257)\n- Harmony: 21.1 fix [\\#3249](https://github.com/pypeclub/OpenPype/pull/3249)\n- Maya: added jpg to filter for Image Plane Loader [\\#3223](https://github.com/pypeclub/OpenPype/pull/3223)\n- Webpublisher: replace space by underscore in subset names [\\#3160](https://github.com/pypeclub/OpenPype/pull/3160)\n- StandalonePublisher: removed Extract Background plugins [\\#3093](https://github.com/pypeclub/OpenPype/pull/3093)\n- Nuke: added suspend\\_publish knob [\\#3078](https://github.com/pypeclub/OpenPype/pull/3078)\n- Bump async from 2.6.3 to 2.6.4 in /website [\\#3065](https://github.com/pypeclub/OpenPype/pull/3065)\n- SiteSync: Download all workfile inputs [\\#2966](https://github.com/pypeclub/OpenPype/pull/2966)\n- Photoshop: New Publisher [\\#2933](https://github.com/pypeclub/OpenPype/pull/2933)\n- Bump pillow from 9.0.0 to 9.0.1 [\\#2880](https://github.com/pypeclub/OpenPype/pull/2880)\n- AfterEffects: Allow configuration of default variant via Settings [\\#2856](https://github.com/pypeclub/OpenPype/pull/2856)\n\n## [3.9.8](https://github.com/pypeclub/OpenPype/tree/3.9.8) (2022-05-19)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.7...3.9.8)\n\n## [3.9.7](https://github.com/pypeclub/OpenPype/tree/3.9.7) (2022-05-11)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.6...3.9.7)\n\n## [3.9.6](https://github.com/pypeclub/OpenPype/tree/3.9.6) (2022-05-03)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.5...3.9.6)\n\n## [3.9.5](https://github.com/pypeclub/OpenPype/tree/3.9.5) (2022-04-25)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.4...3.9.5)\n\n## [3.9.4](https://github.com/pypeclub/OpenPype/tree/3.9.4) (2022-04-15)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.3...3.9.4)\n\n### 📖 Documentation\n\n- Documentation: more info about Tasks [\\#3062](https://github.com/pypeclub/OpenPype/pull/3062)\n- Documentation: Python requirements to 3.7.9 [\\#3035](https://github.com/pypeclub/OpenPype/pull/3035)\n- Website Docs: Remove unused pages [\\#2974](https://github.com/pypeclub/OpenPype/pull/2974)\n\n**🆕 New features**\n\n- General: Local overrides for environment variables [\\#3045](https://github.com/pypeclub/OpenPype/pull/3045)\n- Flame: Flare integration preparation [\\#2928](https://github.com/pypeclub/OpenPype/pull/2928)\n\n**🚀 Enhancements**\n\n- TVPaint: Added init file for worker to triggers missing sound file dialog [\\#3053](https://github.com/pypeclub/OpenPype/pull/3053)\n- Ftrack: Custom attributes can be filled in slate values [\\#3036](https://github.com/pypeclub/OpenPype/pull/3036)\n- Resolve environment variable in google drive credential path  [\\#3008](https://github.com/pypeclub/OpenPype/pull/3008)\n\n**🐛 Bug fixes**\n\n- GitHub: Updated push-protected action in github workflow [\\#3064](https://github.com/pypeclub/OpenPype/pull/3064)\n- Nuke: Typos in imports from Nuke implementation [\\#3061](https://github.com/pypeclub/OpenPype/pull/3061)\n- Hotfix: fixing deadline job publishing [\\#3059](https://github.com/pypeclub/OpenPype/pull/3059)\n- General: Extract Review handle invalid characters for ffmpeg [\\#3050](https://github.com/pypeclub/OpenPype/pull/3050)\n- Slate Review: Support to keep format on slate concatenation [\\#3049](https://github.com/pypeclub/OpenPype/pull/3049)\n- Webpublisher: fix processing of workfile [\\#3048](https://github.com/pypeclub/OpenPype/pull/3048)\n- Ftrack: Integrate ftrack api fix [\\#3044](https://github.com/pypeclub/OpenPype/pull/3044)\n- Webpublisher - removed wrong hardcoded family [\\#3043](https://github.com/pypeclub/OpenPype/pull/3043)\n- LibraryLoader: Use current project for asset query in families filter [\\#3042](https://github.com/pypeclub/OpenPype/pull/3042)\n- SiteSync: Providers ignore that site is disabled [\\#3041](https://github.com/pypeclub/OpenPype/pull/3041)\n- Unreal: Creator import fixes [\\#3040](https://github.com/pypeclub/OpenPype/pull/3040)\n- SiteSync: fix transitive alternate sites, fix dropdown in Local Settings [\\#3018](https://github.com/pypeclub/OpenPype/pull/3018)\n- Maya: invalid review flag on rendered AOVs [\\#2915](https://github.com/pypeclub/OpenPype/pull/2915)\n\n**Merged pull requests:**\n\n- Deadline: reworked pools assignment [\\#3051](https://github.com/pypeclub/OpenPype/pull/3051)\n- Houdini: Avoid ImportError on `hdefereval` when Houdini runs without UI [\\#2987](https://github.com/pypeclub/OpenPype/pull/2987)\n\n## [3.9.3](https://github.com/pypeclub/OpenPype/tree/3.9.3) (2022-04-07)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.2...3.9.3)\n\n### 📖 Documentation\n\n- Documentation: Added mention of adding My Drive as a root [\\#2999](https://github.com/pypeclub/OpenPype/pull/2999)\n- Website Docs: Manager Ftrack fix broken links [\\#2979](https://github.com/pypeclub/OpenPype/pull/2979)\n- Docs: Added MongoDB requirements [\\#2951](https://github.com/pypeclub/OpenPype/pull/2951)\n- Documentation: New publisher develop docs [\\#2896](https://github.com/pypeclub/OpenPype/pull/2896)\n\n**🆕 New features**\n\n- Ftrack: Add description integrator [\\#3027](https://github.com/pypeclub/OpenPype/pull/3027)\n- nuke: bypass baking [\\#2992](https://github.com/pypeclub/OpenPype/pull/2992)\n- Publishing textures for Unreal [\\#2988](https://github.com/pypeclub/OpenPype/pull/2988)\n- Maya to Unreal: Static and Skeletal Meshes [\\#2978](https://github.com/pypeclub/OpenPype/pull/2978)\n- Multiverse: Initial Support [\\#2908](https://github.com/pypeclub/OpenPype/pull/2908)\n\n**🚀 Enhancements**\n\n- General: default workfile subset name for workfile [\\#3011](https://github.com/pypeclub/OpenPype/pull/3011)\n- Ftrack: Add more options for note text of integrate ftrack note [\\#3025](https://github.com/pypeclub/OpenPype/pull/3025)\n- Console Interpreter: Changed how console splitter size are reused on show [\\#3016](https://github.com/pypeclub/OpenPype/pull/3016)\n- Deadline: Use more suitable name for sequence review logic [\\#3015](https://github.com/pypeclub/OpenPype/pull/3015)\n- Nuke: add concurrency attr to deadline job [\\#3005](https://github.com/pypeclub/OpenPype/pull/3005)\n- Photoshop: create image without instance [\\#3001](https://github.com/pypeclub/OpenPype/pull/3001)\n- TVPaint: Render scene family [\\#3000](https://github.com/pypeclub/OpenPype/pull/3000)\n- Deadline: priority configurable in Maya jobs [\\#2995](https://github.com/pypeclub/OpenPype/pull/2995)\n- Nuke: ReviewDataMov Read RAW attribute [\\#2985](https://github.com/pypeclub/OpenPype/pull/2985)\n- General: `METADATA_KEYS` constant as `frozenset` for optimal immutable lookup [\\#2980](https://github.com/pypeclub/OpenPype/pull/2980)\n- General: Tools with host filters [\\#2975](https://github.com/pypeclub/OpenPype/pull/2975)\n- Hero versions: Use custom templates [\\#2967](https://github.com/pypeclub/OpenPype/pull/2967)\n- Slack: Added configurable maximum file size of review upload to Slack [\\#2945](https://github.com/pypeclub/OpenPype/pull/2945)\n- NewPublisher: Prepared implementation of optional pyblish plugin [\\#2943](https://github.com/pypeclub/OpenPype/pull/2943)\n- TVPaint: Extractor to convert PNG into EXR [\\#2942](https://github.com/pypeclub/OpenPype/pull/2942)\n- Workfiles tool: Save as published workfiles [\\#2937](https://github.com/pypeclub/OpenPype/pull/2937)\n- Workfiles: Open published workfiles [\\#2925](https://github.com/pypeclub/OpenPype/pull/2925)\n- General: Default modules loaded dynamically [\\#2923](https://github.com/pypeclub/OpenPype/pull/2923)\n- CI: change the version bump logic [\\#2919](https://github.com/pypeclub/OpenPype/pull/2919)\n- Deadline: Add headless argument [\\#2916](https://github.com/pypeclub/OpenPype/pull/2916)\n- Nuke: Add no-audio Tag [\\#2911](https://github.com/pypeclub/OpenPype/pull/2911)\n- Ftrack: Fill workfile in custom attribute [\\#2906](https://github.com/pypeclub/OpenPype/pull/2906)\n- Nuke: improving readability [\\#2903](https://github.com/pypeclub/OpenPype/pull/2903)\n- Settings UI: Add simple tooltips for settings entities [\\#2901](https://github.com/pypeclub/OpenPype/pull/2901)\n\n**🐛 Bug fixes**\n\n- General: Fix validate asset docs plug-in filename and class name [\\#3029](https://github.com/pypeclub/OpenPype/pull/3029)\n- Deadline: Fixed default value of use sequence for review [\\#3033](https://github.com/pypeclub/OpenPype/pull/3033)\n- Settings UI: Version column can be extended so version are visible [\\#3032](https://github.com/pypeclub/OpenPype/pull/3032)\n- General: Fix import after movements [\\#3028](https://github.com/pypeclub/OpenPype/pull/3028)\n- Harmony: Added creating subset name for workfile from template [\\#3024](https://github.com/pypeclub/OpenPype/pull/3024)\n- AfterEffects: Added creating subset name for workfile from template [\\#3023](https://github.com/pypeclub/OpenPype/pull/3023)\n- General: Add example addons to ignored [\\#3022](https://github.com/pypeclub/OpenPype/pull/3022)\n- Maya: Remove missing import [\\#3017](https://github.com/pypeclub/OpenPype/pull/3017)\n- Ftrack: multiple  reviewable componets [\\#3012](https://github.com/pypeclub/OpenPype/pull/3012)\n- Tray publisher: Fixes after code movement [\\#3010](https://github.com/pypeclub/OpenPype/pull/3010)\n- Hosts: Remove path existence checks in 'add\\_implementation\\_envs' [\\#3004](https://github.com/pypeclub/OpenPype/pull/3004)\n- Nuke: fixing unicode type detection in effect loaders [\\#3002](https://github.com/pypeclub/OpenPype/pull/3002)\n- Fix - remove doubled dot in workfile created from template [\\#2998](https://github.com/pypeclub/OpenPype/pull/2998)\n- Nuke: removing redundant Ftrack asset when farm publishing [\\#2996](https://github.com/pypeclub/OpenPype/pull/2996)\n- PS: fix renaming subset incorrectly in PS [\\#2991](https://github.com/pypeclub/OpenPype/pull/2991)\n- Fix: Disable setuptools auto discovery [\\#2990](https://github.com/pypeclub/OpenPype/pull/2990)\n- AEL: fix opening existing workfile if no scene opened [\\#2989](https://github.com/pypeclub/OpenPype/pull/2989)\n- Maya: Don't do hardlinks on windows for look publishing [\\#2986](https://github.com/pypeclub/OpenPype/pull/2986)\n- Settings UI: Fix version completer on linux [\\#2981](https://github.com/pypeclub/OpenPype/pull/2981)\n- Photoshop: Fix creation of subset names in PS review and workfile [\\#2969](https://github.com/pypeclub/OpenPype/pull/2969)\n- Slack: Added default for review\\_upload\\_limit for Slack [\\#2965](https://github.com/pypeclub/OpenPype/pull/2965)\n- General: OIIO conversion for ffmeg can handle sequences [\\#2958](https://github.com/pypeclub/OpenPype/pull/2958)\n- Settings: Conditional dictionary avoid invalid logs [\\#2956](https://github.com/pypeclub/OpenPype/pull/2956)\n- General: Smaller fixes and typos [\\#2950](https://github.com/pypeclub/OpenPype/pull/2950)\n- LogViewer: Don't refresh on initialization [\\#2949](https://github.com/pypeclub/OpenPype/pull/2949)\n- nuke: python3 compatibility issue with `iteritems` [\\#2948](https://github.com/pypeclub/OpenPype/pull/2948)\n- General: anatomy data with correct task short key [\\#2947](https://github.com/pypeclub/OpenPype/pull/2947)\n- SceneInventory: Fix imports in UI [\\#2944](https://github.com/pypeclub/OpenPype/pull/2944)\n- Slack: add generic exception [\\#2941](https://github.com/pypeclub/OpenPype/pull/2941)\n- General: Python specific vendor paths on env injection [\\#2939](https://github.com/pypeclub/OpenPype/pull/2939)\n- General: More fail safe delete old versions [\\#2936](https://github.com/pypeclub/OpenPype/pull/2936)\n- Settings UI: Collapsed of collapsible wrapper works as expected [\\#2934](https://github.com/pypeclub/OpenPype/pull/2934)\n- Maya: Do not pass `set` to maya commands \\(fixes support for older maya versions\\) [\\#2932](https://github.com/pypeclub/OpenPype/pull/2932)\n- General: Don't print log record on OSError [\\#2926](https://github.com/pypeclub/OpenPype/pull/2926)\n- Hiero: Fix import of 'register\\_event\\_callback' [\\#2924](https://github.com/pypeclub/OpenPype/pull/2924)\n- Flame: centos related debugging [\\#2922](https://github.com/pypeclub/OpenPype/pull/2922)\n- Ftrack: Missing Ftrack id after editorial publish [\\#2905](https://github.com/pypeclub/OpenPype/pull/2905)\n- AfterEffects: Fix rendering for single frame in DL [\\#2875](https://github.com/pypeclub/OpenPype/pull/2875)\n\n**🔀 Refactored code**\n\n- General: Move plugins register and discover [\\#2935](https://github.com/pypeclub/OpenPype/pull/2935)\n- General: Move Attribute Definitions from pipeline [\\#2931](https://github.com/pypeclub/OpenPype/pull/2931)\n- General: Removed silo references and terminal splash [\\#2927](https://github.com/pypeclub/OpenPype/pull/2927)\n- General: Move pipeline constants to OpenPype [\\#2918](https://github.com/pypeclub/OpenPype/pull/2918)\n- General: Move formatting and workfile functions [\\#2914](https://github.com/pypeclub/OpenPype/pull/2914)\n- General: Move remaining plugins from avalon [\\#2912](https://github.com/pypeclub/OpenPype/pull/2912)\n\n**Merged pull requests:**\n\n- Maya: Allow to select invalid camera contents if no cameras found [\\#3030](https://github.com/pypeclub/OpenPype/pull/3030)\n- Bump paramiko from 2.9.2 to 2.10.1 [\\#2973](https://github.com/pypeclub/OpenPype/pull/2973)\n- Bump minimist from 1.2.5 to 1.2.6 in /website [\\#2954](https://github.com/pypeclub/OpenPype/pull/2954)\n- Bump node-forge from 1.2.1 to 1.3.0 in /website [\\#2953](https://github.com/pypeclub/OpenPype/pull/2953)\n- Maya - added transparency into review creator [\\#2952](https://github.com/pypeclub/OpenPype/pull/2952)\n\n## [3.9.2](https://github.com/pypeclub/OpenPype/tree/3.9.2) (2022-04-04)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.1...3.9.2)\n\n## [3.9.1](https://github.com/pypeclub/OpenPype/tree/3.9.1) (2022-03-18)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.0...3.9.1)\n\n**🚀 Enhancements**\n\n- General: Change how OPENPYPE\\_DEBUG value is handled [\\#2907](https://github.com/pypeclub/OpenPype/pull/2907)\n- nuke: imageio adding ocio config version 1.2 [\\#2897](https://github.com/pypeclub/OpenPype/pull/2897)\n- Flame: support for comment with xml attribute overrides [\\#2892](https://github.com/pypeclub/OpenPype/pull/2892)\n- Nuke: ExtractReviewSlate can handle more codes and profiles [\\#2879](https://github.com/pypeclub/OpenPype/pull/2879)\n- Flame: sequence used for reference video [\\#2869](https://github.com/pypeclub/OpenPype/pull/2869)\n\n**🐛 Bug fixes**\n\n- General: Fix use of Anatomy roots [\\#2904](https://github.com/pypeclub/OpenPype/pull/2904)\n- Fixing gap detection in extract review [\\#2902](https://github.com/pypeclub/OpenPype/pull/2902)\n- Pyblish Pype - ensure current state is correct when entering new group order [\\#2899](https://github.com/pypeclub/OpenPype/pull/2899)\n- SceneInventory: Fix import of load function [\\#2894](https://github.com/pypeclub/OpenPype/pull/2894)\n- Harmony - fixed creator issue [\\#2891](https://github.com/pypeclub/OpenPype/pull/2891)\n- General: Remove forgotten use of avalon Creator [\\#2885](https://github.com/pypeclub/OpenPype/pull/2885)\n- General: Avoid circular import [\\#2884](https://github.com/pypeclub/OpenPype/pull/2884)\n- Fixes for attaching loaded containers \\(\\#2837\\) [\\#2874](https://github.com/pypeclub/OpenPype/pull/2874)\n- Maya: Deformer node ids validation plugin [\\#2826](https://github.com/pypeclub/OpenPype/pull/2826)\n- Flame Babypublisher optimalization [\\#2806](https://github.com/pypeclub/OpenPype/pull/2806)\n- hotfix: OIIO tool path - add extension on windows [\\#2618](https://github.com/pypeclub/OpenPype/pull/2618)\n\n**🔀 Refactored code**\n\n- General: Reduce style usage to OpenPype repository [\\#2889](https://github.com/pypeclub/OpenPype/pull/2889)\n- General: Move loader logic from avalon to openpype [\\#2886](https://github.com/pypeclub/OpenPype/pull/2886)\n\n## [3.9.0](https://github.com/pypeclub/OpenPype/tree/3.9.0) (2022-03-14)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.2...3.9.0)\n\n**Deprecated:**\n\n- Houdini: Remove unused code [\\#2779](https://github.com/pypeclub/OpenPype/pull/2779)\n- Loader: Remove default family states for hosts from code [\\#2706](https://github.com/pypeclub/OpenPype/pull/2706)\n- AssetCreator: Remove the tool [\\#2845](https://github.com/pypeclub/OpenPype/pull/2845)\n\n### 📖 Documentation\n\n- Documentation: fixed broken links [\\#2799](https://github.com/pypeclub/OpenPype/pull/2799)\n- Documentation: broken link fix [\\#2785](https://github.com/pypeclub/OpenPype/pull/2785)\n- Documentation: link fixes [\\#2772](https://github.com/pypeclub/OpenPype/pull/2772)\n- Update docusaurus to latest version [\\#2760](https://github.com/pypeclub/OpenPype/pull/2760)\n- Various testing updates [\\#2726](https://github.com/pypeclub/OpenPype/pull/2726)\n- documentation: add example to `repack-version` command [\\#2669](https://github.com/pypeclub/OpenPype/pull/2669)\n- Update docusaurus [\\#2639](https://github.com/pypeclub/OpenPype/pull/2639)\n- Documentation: Fixed relative links [\\#2621](https://github.com/pypeclub/OpenPype/pull/2621)\n- Documentation: Change Photoshop & AfterEffects plugin path [\\#2878](https://github.com/pypeclub/OpenPype/pull/2878)\n\n**🆕 New features**\n\n- Flame: loading clips to reels [\\#2622](https://github.com/pypeclub/OpenPype/pull/2622)\n- General: Store settings by OpenPype version [\\#2570](https://github.com/pypeclub/OpenPype/pull/2570)\n\n**🚀 Enhancements**\n\n- New: Validation exceptions [\\#2841](https://github.com/pypeclub/OpenPype/pull/2841)\n- General: Set context environments for non host applications [\\#2803](https://github.com/pypeclub/OpenPype/pull/2803)\n- Houdini: Remove duplicate ValidateOutputNode plug-in [\\#2780](https://github.com/pypeclub/OpenPype/pull/2780)\n- Tray publisher: New Tray Publisher host \\(beta\\) [\\#2778](https://github.com/pypeclub/OpenPype/pull/2778)\n- Slack: Added regex for filtering on subset names [\\#2775](https://github.com/pypeclub/OpenPype/pull/2775)\n- Houdini: Implement Reset Frame Range [\\#2770](https://github.com/pypeclub/OpenPype/pull/2770)\n- Pyblish Pype: Remove redundant new line in installed fonts printing [\\#2758](https://github.com/pypeclub/OpenPype/pull/2758)\n- Flame: use Shot Name on segment for asset name [\\#2751](https://github.com/pypeclub/OpenPype/pull/2751)\n- Flame: adding validator source clip [\\#2746](https://github.com/pypeclub/OpenPype/pull/2746)\n- Work Files: Preserve subversion comment of current filename by default [\\#2734](https://github.com/pypeclub/OpenPype/pull/2734)\n- Maya: set Deadline job/batch name to original source workfile name instead of published workfile [\\#2733](https://github.com/pypeclub/OpenPype/pull/2733)\n- Ftrack: Disable ftrack module by default [\\#2732](https://github.com/pypeclub/OpenPype/pull/2732)\n- Project Manager: Disable add task, add asset and save button when not in a project [\\#2727](https://github.com/pypeclub/OpenPype/pull/2727)\n- dropbox handle big file [\\#2718](https://github.com/pypeclub/OpenPype/pull/2718)\n- Fusion Move PR: Minor tweaks to Fusion integration [\\#2716](https://github.com/pypeclub/OpenPype/pull/2716)\n- RoyalRender: Minor enhancements [\\#2700](https://github.com/pypeclub/OpenPype/pull/2700)\n- Nuke: prerender with review knob [\\#2691](https://github.com/pypeclub/OpenPype/pull/2691)\n- Maya configurable unit validator [\\#2680](https://github.com/pypeclub/OpenPype/pull/2680)\n- General: Add settings for CleanUpFarm and disable the plugin by default [\\#2679](https://github.com/pypeclub/OpenPype/pull/2679)\n- Project Manager: Only allow scroll wheel edits when spinbox is active [\\#2678](https://github.com/pypeclub/OpenPype/pull/2678)\n- Ftrack: Sync description to assets [\\#2670](https://github.com/pypeclub/OpenPype/pull/2670)\n- Houdini: Moved to OpenPype [\\#2658](https://github.com/pypeclub/OpenPype/pull/2658)\n- Maya: Move implementation to OpenPype [\\#2649](https://github.com/pypeclub/OpenPype/pull/2649)\n- General: FFmpeg conversion also check attribute string length [\\#2635](https://github.com/pypeclub/OpenPype/pull/2635)\n- Houdini: Load Arnold .ass procedurals into Houdini [\\#2606](https://github.com/pypeclub/OpenPype/pull/2606)\n- Deadline: Simplify GlobalJobPreLoad logic [\\#2605](https://github.com/pypeclub/OpenPype/pull/2605)\n- Houdini: Implement Arnold .ass standin extraction from Houdini \\(also support .ass.gz\\) [\\#2603](https://github.com/pypeclub/OpenPype/pull/2603)\n- New Publisher: New features and preparations for new standalone publisher [\\#2556](https://github.com/pypeclub/OpenPype/pull/2556)\n- Fix Maya 2022 Python 3 compatibility [\\#2445](https://github.com/pypeclub/OpenPype/pull/2445)\n- TVPaint: Use new publisher exceptions in validators [\\#2435](https://github.com/pypeclub/OpenPype/pull/2435)\n- Harmony: Added new style validations for New Publisher [\\#2434](https://github.com/pypeclub/OpenPype/pull/2434)\n- Aftereffects: New style validations for New publisher [\\#2430](https://github.com/pypeclub/OpenPype/pull/2430)\n- Farm publishing: New cleanup plugin for Maya renders on farm [\\#2390](https://github.com/pypeclub/OpenPype/pull/2390)\n- General: Subset name filtering in ExtractReview outpus [\\#2872](https://github.com/pypeclub/OpenPype/pull/2872)\n- NewPublisher: Descriptions and Icons in creator dialog [\\#2867](https://github.com/pypeclub/OpenPype/pull/2867)\n- NewPublisher: Changing task on publishing instance [\\#2863](https://github.com/pypeclub/OpenPype/pull/2863)\n- TrayPublisher: Choose project widget is more clear [\\#2859](https://github.com/pypeclub/OpenPype/pull/2859)\n- Maya: add loaded containers to published instance [\\#2837](https://github.com/pypeclub/OpenPype/pull/2837)\n- Ftrack: Can sync fps as string [\\#2836](https://github.com/pypeclub/OpenPype/pull/2836)\n- General: Custom function for find executable [\\#2822](https://github.com/pypeclub/OpenPype/pull/2822)\n- General: Color dialog UI fixes [\\#2817](https://github.com/pypeclub/OpenPype/pull/2817)\n- global: letter box calculated on output as last process [\\#2812](https://github.com/pypeclub/OpenPype/pull/2812)\n- Nuke: adding Reformat to baking mov plugin  [\\#2811](https://github.com/pypeclub/OpenPype/pull/2811)\n- Manager: Update all to latest button [\\#2805](https://github.com/pypeclub/OpenPype/pull/2805)\n- Houdini: Move Houdini Save Current File to beginning of ExtractorOrder [\\#2747](https://github.com/pypeclub/OpenPype/pull/2747)\n- Global: adding studio name/code to anatomy template formatting data [\\#2630](https://github.com/pypeclub/OpenPype/pull/2630)\n\n**🐛 Bug fixes**\n\n- Settings UI: Search case sensitivity [\\#2810](https://github.com/pypeclub/OpenPype/pull/2810)\n- resolve: fixing fusion module loading [\\#2802](https://github.com/pypeclub/OpenPype/pull/2802)\n- Ftrack: Unset task ids from asset versions before tasks are removed [\\#2800](https://github.com/pypeclub/OpenPype/pull/2800)\n- Slack: fail gracefully if slack exception [\\#2798](https://github.com/pypeclub/OpenPype/pull/2798)\n- Flame: Fix version string in default settings [\\#2783](https://github.com/pypeclub/OpenPype/pull/2783)\n- After Effects: Fix typo in name `afftereffects` -\\> `aftereffects` [\\#2768](https://github.com/pypeclub/OpenPype/pull/2768)\n- Houdini: Fix open last workfile [\\#2767](https://github.com/pypeclub/OpenPype/pull/2767)\n- Avoid renaming udim indexes [\\#2765](https://github.com/pypeclub/OpenPype/pull/2765)\n- Maya: Fix `unique_namespace` when in an namespace that is empty [\\#2759](https://github.com/pypeclub/OpenPype/pull/2759)\n- Loader UI: Fix right click in representation widget [\\#2757](https://github.com/pypeclub/OpenPype/pull/2757)\n- Harmony: Rendering in Deadline didn't work in other machines than submitter [\\#2754](https://github.com/pypeclub/OpenPype/pull/2754)\n- Aftereffects 2022 and Deadline [\\#2748](https://github.com/pypeclub/OpenPype/pull/2748)\n- Flame: bunch of bugs [\\#2745](https://github.com/pypeclub/OpenPype/pull/2745)\n- Maya: Save current scene on workfile publish [\\#2744](https://github.com/pypeclub/OpenPype/pull/2744)\n- Version Up: Preserve parts of filename after version number \\(like subversion\\) on version\\_up [\\#2741](https://github.com/pypeclub/OpenPype/pull/2741)\n- Loader UI: Multiple asset selection and underline colors fixed [\\#2731](https://github.com/pypeclub/OpenPype/pull/2731)\n- General: Fix loading of unused chars in xml format [\\#2729](https://github.com/pypeclub/OpenPype/pull/2729)\n- TVPaint: Set objectName with members [\\#2725](https://github.com/pypeclub/OpenPype/pull/2725)\n- General: Don't use 'objectName' from loaded references [\\#2715](https://github.com/pypeclub/OpenPype/pull/2715)\n- Settings: Studio Project anatomy is queried using right keys [\\#2711](https://github.com/pypeclub/OpenPype/pull/2711)\n- Local Settings: Additional applications don't break UI [\\#2710](https://github.com/pypeclub/OpenPype/pull/2710)\n- Maya: Remove some unused code [\\#2709](https://github.com/pypeclub/OpenPype/pull/2709)\n- Houdini: Fix refactor of Houdini host move for CreateArnoldAss [\\#2704](https://github.com/pypeclub/OpenPype/pull/2704)\n- LookAssigner: Fix imports after moving code to OpenPype repository [\\#2701](https://github.com/pypeclub/OpenPype/pull/2701)\n- Multiple hosts: unify menu style across hosts [\\#2693](https://github.com/pypeclub/OpenPype/pull/2693)\n- Maya Redshift fixes [\\#2692](https://github.com/pypeclub/OpenPype/pull/2692)\n- Maya: fix fps validation popup [\\#2685](https://github.com/pypeclub/OpenPype/pull/2685)\n- Houdini Explicitly collect correct frame name even in case of single frame render when `frameStart` is provided [\\#2676](https://github.com/pypeclub/OpenPype/pull/2676)\n- hiero: fix effect collector name and order [\\#2673](https://github.com/pypeclub/OpenPype/pull/2673)\n- Maya: Fix menu callbacks [\\#2671](https://github.com/pypeclub/OpenPype/pull/2671)\n- hiero: removing obsolete unsupported plugin [\\#2667](https://github.com/pypeclub/OpenPype/pull/2667)\n- Launcher: Fix access to 'data' attribute on actions [\\#2659](https://github.com/pypeclub/OpenPype/pull/2659)\n- Maya `vrscene` loader fixes [\\#2633](https://github.com/pypeclub/OpenPype/pull/2633)\n- Houdini: fix usd family in loader and integrators [\\#2631](https://github.com/pypeclub/OpenPype/pull/2631)\n- Maya: Add only reference node to look family container like with other families [\\#2508](https://github.com/pypeclub/OpenPype/pull/2508)\n- General: Missing time function [\\#2877](https://github.com/pypeclub/OpenPype/pull/2877)\n- Deadline: Fix plugin name for tile assemble [\\#2868](https://github.com/pypeclub/OpenPype/pull/2868)\n- Nuke: gizmo precollect fix [\\#2866](https://github.com/pypeclub/OpenPype/pull/2866)\n- General: Fix hardlink for windows [\\#2864](https://github.com/pypeclub/OpenPype/pull/2864)\n- General: ffmpeg was crashing on slate merge [\\#2860](https://github.com/pypeclub/OpenPype/pull/2860)\n- WebPublisher: Video file was published with one too many frame [\\#2858](https://github.com/pypeclub/OpenPype/pull/2858)\n- New Publisher: Error dialog got right styles [\\#2857](https://github.com/pypeclub/OpenPype/pull/2857)\n- General: Fix getattr clalback on dynamic modules [\\#2855](https://github.com/pypeclub/OpenPype/pull/2855)\n- Nuke: slate resolution to input video resolution [\\#2853](https://github.com/pypeclub/OpenPype/pull/2853)\n- WebPublisher: Fix username stored in DB [\\#2852](https://github.com/pypeclub/OpenPype/pull/2852)\n- WebPublisher: Fix wrong number of frames for video file [\\#2851](https://github.com/pypeclub/OpenPype/pull/2851)\n- Nuke: Fix family test in validate\\_write\\_legacy to work with stillImage [\\#2847](https://github.com/pypeclub/OpenPype/pull/2847)\n- Nuke: fix multiple baking profile farm publishing [\\#2842](https://github.com/pypeclub/OpenPype/pull/2842)\n- Blender: Fixed parameters for FBX export of the camera [\\#2840](https://github.com/pypeclub/OpenPype/pull/2840)\n- Maya: Stop creation of reviews for Cryptomattes [\\#2832](https://github.com/pypeclub/OpenPype/pull/2832)\n- Deadline: Remove recreated event [\\#2828](https://github.com/pypeclub/OpenPype/pull/2828)\n- Deadline: Added missing events folder [\\#2827](https://github.com/pypeclub/OpenPype/pull/2827)\n- Settings: Missing document with OP versions may break start of OpenPype [\\#2825](https://github.com/pypeclub/OpenPype/pull/2825)\n- Deadline: more detailed temp file name for environment json [\\#2824](https://github.com/pypeclub/OpenPype/pull/2824)\n- General: Host name was formed from obsolete code [\\#2821](https://github.com/pypeclub/OpenPype/pull/2821)\n- Settings UI: Fix \"Apply from\" action [\\#2820](https://github.com/pypeclub/OpenPype/pull/2820)\n- Ftrack: Job killer with missing user [\\#2819](https://github.com/pypeclub/OpenPype/pull/2819)\n- Nuke: Use AVALON\\_APP to get value for \"app\" key [\\#2818](https://github.com/pypeclub/OpenPype/pull/2818)\n- StandalonePublisher: use dynamic groups in subset names [\\#2816](https://github.com/pypeclub/OpenPype/pull/2816)\n\n**🔀 Refactored code**\n\n- Ftrack: Moved module one hierarchy level higher [\\#2792](https://github.com/pypeclub/OpenPype/pull/2792)\n- SyncServer: Moved module one hierarchy level higher [\\#2791](https://github.com/pypeclub/OpenPype/pull/2791)\n- Royal render: Move module one hierarchy level higher [\\#2790](https://github.com/pypeclub/OpenPype/pull/2790)\n- Deadline: Move module one hierarchy level higher [\\#2789](https://github.com/pypeclub/OpenPype/pull/2789)\n- Refactor: move webserver tool to openpype [\\#2876](https://github.com/pypeclub/OpenPype/pull/2876)\n- General: Move create logic from avalon to OpenPype [\\#2854](https://github.com/pypeclub/OpenPype/pull/2854)\n- General: Add vendors from avalon [\\#2848](https://github.com/pypeclub/OpenPype/pull/2848)\n- General: Basic event system [\\#2846](https://github.com/pypeclub/OpenPype/pull/2846)\n- General: Move change context functions [\\#2839](https://github.com/pypeclub/OpenPype/pull/2839)\n- Tools: Don't use avalon tools code [\\#2829](https://github.com/pypeclub/OpenPype/pull/2829)\n- Move Unreal Implementation to OpenPype [\\#2823](https://github.com/pypeclub/OpenPype/pull/2823)\n- General: Extract template formatting from anatomy [\\#2766](https://github.com/pypeclub/OpenPype/pull/2766)\n\n**Merged pull requests:**\n\n- Fusion: Moved implementation into OpenPype [\\#2713](https://github.com/pypeclub/OpenPype/pull/2713)\n- TVPaint: Plugin build without dependencies [\\#2705](https://github.com/pypeclub/OpenPype/pull/2705)\n- Webpublisher: Photoshop create a beauty png [\\#2689](https://github.com/pypeclub/OpenPype/pull/2689)\n- Ftrack: Hierarchical attributes are queried properly [\\#2682](https://github.com/pypeclub/OpenPype/pull/2682)\n- Maya: Add Validate Frame Range settings [\\#2661](https://github.com/pypeclub/OpenPype/pull/2661)\n- Harmony: move to Openpype [\\#2657](https://github.com/pypeclub/OpenPype/pull/2657)\n- Maya: cleanup duplicate rendersetup code [\\#2642](https://github.com/pypeclub/OpenPype/pull/2642)\n- Deadline: Be able to pass Mongo url to job [\\#2616](https://github.com/pypeclub/OpenPype/pull/2616)\n\n## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.1...3.8.2)\n\n### 📖 Documentation\n\n- Cosmetics: Fix common typos in openpype/website [\\#2617](https://github.com/pypeclub/OpenPype/pull/2617)\n\n**🚀 Enhancements**\n\n- TVPaint: Image loaders also work on review family [\\#2638](https://github.com/pypeclub/OpenPype/pull/2638)\n- General: Project backup tools [\\#2629](https://github.com/pypeclub/OpenPype/pull/2629)\n- nuke: adding clear button to write nodes [\\#2627](https://github.com/pypeclub/OpenPype/pull/2627)\n- Ftrack: Family to Asset type mapping is in settings [\\#2602](https://github.com/pypeclub/OpenPype/pull/2602)\n- Nuke: load color space from representation data [\\#2576](https://github.com/pypeclub/OpenPype/pull/2576)\n\n**🐛 Bug fixes**\n\n- Fix pulling of cx\\_freeze 6.10 [\\#2628](https://github.com/pypeclub/OpenPype/pull/2628)\n- Global: fix broken otio review extractor [\\#2590](https://github.com/pypeclub/OpenPype/pull/2590)\n\n**Merged pull requests:**\n\n- WebPublisher: fix instance duplicates [\\#2641](https://github.com/pypeclub/OpenPype/pull/2641)\n- Fix - safer pulling of task name for webpublishing from PS [\\#2613](https://github.com/pypeclub/OpenPype/pull/2613)\n\n## [3.8.1](https://github.com/pypeclub/OpenPype/tree/3.8.1) (2022-02-01)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.0...3.8.1)\n\n**🚀 Enhancements**\n\n- Webpublisher: Thumbnail extractor [\\#2600](https://github.com/pypeclub/OpenPype/pull/2600)\n- Loader: Allow to toggle default family filters between \"include\" or \"exclude\" filtering [\\#2541](https://github.com/pypeclub/OpenPype/pull/2541)\n- Launcher: Added context menu to to skip opening last workfile [\\#2536](https://github.com/pypeclub/OpenPype/pull/2536)\n- Unreal: JSON Layout Loading support [\\#2066](https://github.com/pypeclub/OpenPype/pull/2066)\n\n**🐛 Bug fixes**\n\n- Release/3.8.0 [\\#2619](https://github.com/pypeclub/OpenPype/pull/2619)\n- Settings: Enum does not store empty string if has single item to select [\\#2615](https://github.com/pypeclub/OpenPype/pull/2615)\n- switch distutils to sysconfig for `get_platform()` [\\#2594](https://github.com/pypeclub/OpenPype/pull/2594)\n- Fix poetry index and speedcopy update [\\#2589](https://github.com/pypeclub/OpenPype/pull/2589)\n- Webpublisher: Fix - subset names from processed .psd used wrong value for task [\\#2586](https://github.com/pypeclub/OpenPype/pull/2586)\n- `vrscene` creator Deadline webservice URL handling [\\#2580](https://github.com/pypeclub/OpenPype/pull/2580)\n- global: track name was failing if duplicated root word in name [\\#2568](https://github.com/pypeclub/OpenPype/pull/2568)\n- Validate Maya Rig produces no cycle errors [\\#2484](https://github.com/pypeclub/OpenPype/pull/2484)\n\n**Merged pull requests:**\n\n- Bump pillow from 8.4.0 to 9.0.0 [\\#2595](https://github.com/pypeclub/OpenPype/pull/2595)\n- Webpublisher: Skip version collect [\\#2591](https://github.com/pypeclub/OpenPype/pull/2591)\n- build\\(deps\\): bump pillow from 8.4.0 to 9.0.0 [\\#2523](https://github.com/pypeclub/OpenPype/pull/2523)\n\n## [3.8.0](https://github.com/pypeclub/OpenPype/tree/3.8.0) (2022-01-24)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.7.0...3.8.0)\n\n### 📖 Documentation\n\n- Variable in docs renamed to proper name [\\#2546](https://github.com/pypeclub/OpenPype/pull/2546)\n\n**🆕 New features**\n\n- Flame: extracting segments with trans-coding  [\\#2547](https://github.com/pypeclub/OpenPype/pull/2547)\n- Maya : V-Ray Proxy - load all ABC files via proxy [\\#2544](https://github.com/pypeclub/OpenPype/pull/2544)\n- Maya to Unreal: Extended static mesh workflow [\\#2537](https://github.com/pypeclub/OpenPype/pull/2537)\n- Flame: collecting publishable instances [\\#2519](https://github.com/pypeclub/OpenPype/pull/2519)\n- Flame: create publishable clips [\\#2495](https://github.com/pypeclub/OpenPype/pull/2495)\n- Flame: OpenTimelineIO Export Modul [\\#2398](https://github.com/pypeclub/OpenPype/pull/2398)\n\n**🚀 Enhancements**\n\n- Webpublisher: Moved error at the beginning of the log [\\#2559](https://github.com/pypeclub/OpenPype/pull/2559)\n- Ftrack: Use ApplicationManager to get DJV path [\\#2558](https://github.com/pypeclub/OpenPype/pull/2558)\n- Webpublisher: Added endpoint to reprocess batch through UI [\\#2555](https://github.com/pypeclub/OpenPype/pull/2555)\n- Settings: PathInput strip passed string [\\#2550](https://github.com/pypeclub/OpenPype/pull/2550)\n- Global: Exctract Review anatomy fill data with output name [\\#2548](https://github.com/pypeclub/OpenPype/pull/2548)\n- Cosmetics: Clean up some cosmetics / typos [\\#2542](https://github.com/pypeclub/OpenPype/pull/2542)\n- General: Validate if current process OpenPype version is requested version [\\#2529](https://github.com/pypeclub/OpenPype/pull/2529)\n- General: Be able to use anatomy data in ffmpeg output arguments [\\#2525](https://github.com/pypeclub/OpenPype/pull/2525)\n- Expose toggle publish plug-in settings for Maya Look Shading Engine Naming [\\#2521](https://github.com/pypeclub/OpenPype/pull/2521)\n- Photoshop: Move implementation to OpenPype [\\#2510](https://github.com/pypeclub/OpenPype/pull/2510)\n- TimersManager: Move module one hierarchy higher [\\#2501](https://github.com/pypeclub/OpenPype/pull/2501)\n- Slack: notifications are sent with Openpype logo and bot name [\\#2499](https://github.com/pypeclub/OpenPype/pull/2499)\n- Slack: Add review to notification message [\\#2498](https://github.com/pypeclub/OpenPype/pull/2498)\n- Ftrack: Event handlers settings [\\#2496](https://github.com/pypeclub/OpenPype/pull/2496)\n- Tools: Fix style and modality of errors in loader and creator [\\#2489](https://github.com/pypeclub/OpenPype/pull/2489)\n- Maya: Collect 'fps' animation data only for \"review\" instances [\\#2486](https://github.com/pypeclub/OpenPype/pull/2486)\n- Project Manager: Remove project button cleanup [\\#2482](https://github.com/pypeclub/OpenPype/pull/2482)\n- Tools: Be able to change models of tasks and assets widgets [\\#2475](https://github.com/pypeclub/OpenPype/pull/2475)\n- Publish pype: Reduce publish process defering [\\#2464](https://github.com/pypeclub/OpenPype/pull/2464)\n- Maya: Improve speed of Collect History logic [\\#2460](https://github.com/pypeclub/OpenPype/pull/2460)\n- Maya: Validate Rig Controllers - fix Error: in script editor [\\#2459](https://github.com/pypeclub/OpenPype/pull/2459)\n- Maya: Validate NGONs simplify and speed-up [\\#2458](https://github.com/pypeclub/OpenPype/pull/2458)\n- Maya: Optimize Validate Locked Normals speed for dense polymeshes [\\#2457](https://github.com/pypeclub/OpenPype/pull/2457)\n- Maya: Refactor missing \\_get\\_reference\\_node method [\\#2455](https://github.com/pypeclub/OpenPype/pull/2455)\n- Houdini: Remove broken unique name counter [\\#2450](https://github.com/pypeclub/OpenPype/pull/2450)\n- Maya: Improve lib.polyConstraint performance when Select tool is not the active tool context [\\#2447](https://github.com/pypeclub/OpenPype/pull/2447)\n- General: Validate third party before build [\\#2425](https://github.com/pypeclub/OpenPype/pull/2425)\n- Maya : add option to not group reference in ReferenceLoader [\\#2383](https://github.com/pypeclub/OpenPype/pull/2383)\n\n**🐛 Bug fixes**\n\n- AfterEffects: Fix - removed obsolete import [\\#2577](https://github.com/pypeclub/OpenPype/pull/2577)\n- General: OpenPype version updates [\\#2575](https://github.com/pypeclub/OpenPype/pull/2575)\n- Ftrack: Delete action revision [\\#2563](https://github.com/pypeclub/OpenPype/pull/2563)\n- Webpublisher: ftrack shows incorrect user names [\\#2560](https://github.com/pypeclub/OpenPype/pull/2560)\n- General: Do not validate version if build does not support it [\\#2557](https://github.com/pypeclub/OpenPype/pull/2557)\n- Webpublisher: Fixed progress reporting [\\#2553](https://github.com/pypeclub/OpenPype/pull/2553)\n- Fix Maya AssProxyLoader version switch [\\#2551](https://github.com/pypeclub/OpenPype/pull/2551)\n- General: Fix install thread in igniter [\\#2549](https://github.com/pypeclub/OpenPype/pull/2549)\n- Houdini: vdbcache family preserve frame numbers on publish integration + enable validate version for Houdini [\\#2535](https://github.com/pypeclub/OpenPype/pull/2535)\n- Maya: Fix Load VDB to V-Ray [\\#2533](https://github.com/pypeclub/OpenPype/pull/2533)\n- Maya: ReferenceLoader fix not unique group name error for attach to root [\\#2532](https://github.com/pypeclub/OpenPype/pull/2532)\n- Maya: namespaced context go back to original namespace when started from inside a namespace [\\#2531](https://github.com/pypeclub/OpenPype/pull/2531)\n- Fix create zip tool - path argument [\\#2522](https://github.com/pypeclub/OpenPype/pull/2522)\n- Maya: Fix Extract Look with space in names [\\#2518](https://github.com/pypeclub/OpenPype/pull/2518)\n- Fix published frame content for sequence starting with 0 [\\#2513](https://github.com/pypeclub/OpenPype/pull/2513)\n- Maya: reset empty string attributes correctly to \"\" instead of \"None\" [\\#2506](https://github.com/pypeclub/OpenPype/pull/2506)\n- Improve FusionPreLaunch hook errors [\\#2505](https://github.com/pypeclub/OpenPype/pull/2505)\n- General: Settings work if OpenPypeVersion is available [\\#2494](https://github.com/pypeclub/OpenPype/pull/2494)\n- General: PYTHONPATH may break OpenPype dependencies [\\#2493](https://github.com/pypeclub/OpenPype/pull/2493)\n- General: Modules import function output fix [\\#2492](https://github.com/pypeclub/OpenPype/pull/2492)\n- AE: fix hiding of alert window below Publish [\\#2491](https://github.com/pypeclub/OpenPype/pull/2491)\n- Workfiles tool: Files widget show files on first show [\\#2488](https://github.com/pypeclub/OpenPype/pull/2488)\n- General: Custom template paths filter fix [\\#2483](https://github.com/pypeclub/OpenPype/pull/2483)\n- Loader: Remove always on top flag in tray [\\#2480](https://github.com/pypeclub/OpenPype/pull/2480)\n- General: Anatomy does not return root envs as unicode [\\#2465](https://github.com/pypeclub/OpenPype/pull/2465)\n- Maya: Validate Shape Zero do not keep fixed geometry vertices selected/active after repair [\\#2456](https://github.com/pypeclub/OpenPype/pull/2456)\n\n**Merged pull requests:**\n\n- AfterEffects: Move implementation to OpenPype [\\#2543](https://github.com/pypeclub/OpenPype/pull/2543)\n- Maya: Remove Maya Look Assigner check on startup [\\#2540](https://github.com/pypeclub/OpenPype/pull/2540)\n- build\\(deps\\): bump shelljs from 0.8.4 to 0.8.5 in /website [\\#2538](https://github.com/pypeclub/OpenPype/pull/2538)\n- build\\(deps\\): bump follow-redirects from 1.14.4 to 1.14.7 in /website [\\#2534](https://github.com/pypeclub/OpenPype/pull/2534)\n- Nuke: Merge avalon's implementation into OpenPype [\\#2514](https://github.com/pypeclub/OpenPype/pull/2514)\n- Maya: Vray fix proxies look assignment [\\#2392](https://github.com/pypeclub/OpenPype/pull/2392)\n- Bump algoliasearch-helper from 3.4.4 to 3.6.2 in /website [\\#2297](https://github.com/pypeclub/OpenPype/pull/2297)\n\n## [3.7.0](https://github.com/pypeclub/OpenPype/tree/3.7.0) (2022-01-04)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.4...3.7.0)\n\n**Deprecated:**\n\n- General: Default modules hierarchy n2 [\\#2368](https://github.com/pypeclub/OpenPype/pull/2368)\n\n### 📖 Documentation\n\n- docs\\[website\\]: Add Ellipse Studio \\(logo\\) as an OpenPype contributor [\\#2324](https://github.com/pypeclub/OpenPype/pull/2324)\n\n**🆕 New features**\n\n- Settings UI use OpenPype styles [\\#2296](https://github.com/pypeclub/OpenPype/pull/2296)\n- Store typed version dependencies for workfiles [\\#2192](https://github.com/pypeclub/OpenPype/pull/2192)\n- OpenPypeV3: add key task type, task shortname and user to path templating construction [\\#2157](https://github.com/pypeclub/OpenPype/pull/2157)\n- Nuke: Alembic model workflow [\\#2140](https://github.com/pypeclub/OpenPype/pull/2140)\n- TVPaint: Load workfile from published. [\\#1980](https://github.com/pypeclub/OpenPype/pull/1980)\n\n**🚀 Enhancements**\n\n- General: Workdir extra folders [\\#2462](https://github.com/pypeclub/OpenPype/pull/2462)\n- Photoshop: New style validations for New publisher [\\#2429](https://github.com/pypeclub/OpenPype/pull/2429)\n- General: Environment variables groups [\\#2424](https://github.com/pypeclub/OpenPype/pull/2424)\n- Unreal: Dynamic menu created in Python [\\#2422](https://github.com/pypeclub/OpenPype/pull/2422)\n- Settings UI: Hyperlinks to settings [\\#2420](https://github.com/pypeclub/OpenPype/pull/2420)\n- Modules: JobQueue module moved one hierarchy level higher [\\#2419](https://github.com/pypeclub/OpenPype/pull/2419)\n- TimersManager: Start timer post launch hook [\\#2418](https://github.com/pypeclub/OpenPype/pull/2418)\n- General: Run applications as separate processes under linux [\\#2408](https://github.com/pypeclub/OpenPype/pull/2408)\n- Ftrack: Check existence of object type on recreation [\\#2404](https://github.com/pypeclub/OpenPype/pull/2404)\n- Enhancement: Global cleanup plugin that explicitly remove paths from context [\\#2402](https://github.com/pypeclub/OpenPype/pull/2402)\n- General: MongoDB ability to specify replica set groups [\\#2401](https://github.com/pypeclub/OpenPype/pull/2401)\n- Flame: moving `utility_scripts` to api folder also with `scripts` [\\#2385](https://github.com/pypeclub/OpenPype/pull/2385)\n- Centos 7 dependency compatibility [\\#2384](https://github.com/pypeclub/OpenPype/pull/2384)\n- Enhancement: Settings: Use project settings values from another project [\\#2382](https://github.com/pypeclub/OpenPype/pull/2382)\n- Blender 3: Support auto install for new blender version [\\#2377](https://github.com/pypeclub/OpenPype/pull/2377)\n- Maya add render image path to settings [\\#2375](https://github.com/pypeclub/OpenPype/pull/2375)\n- Settings: Webpublisher in hosts enum [\\#2367](https://github.com/pypeclub/OpenPype/pull/2367)\n- Hiero: python3 compatibility [\\#2365](https://github.com/pypeclub/OpenPype/pull/2365)\n- Burnins: Be able recognize mxf OPAtom format [\\#2361](https://github.com/pypeclub/OpenPype/pull/2361)\n- Maya: Add is\\_static\\_image\\_plane and is\\_in\\_all\\_views option in imagePlaneLoader [\\#2356](https://github.com/pypeclub/OpenPype/pull/2356)\n- Local settings: Copyable studio paths [\\#2349](https://github.com/pypeclub/OpenPype/pull/2349)\n- Assets Widget: Clear model on project change [\\#2345](https://github.com/pypeclub/OpenPype/pull/2345)\n- General: OpenPype default modules hierarchy [\\#2338](https://github.com/pypeclub/OpenPype/pull/2338)\n- TVPaint: Move implementation to OpenPype [\\#2336](https://github.com/pypeclub/OpenPype/pull/2336)\n- General: FFprobe error exception contain original error message [\\#2328](https://github.com/pypeclub/OpenPype/pull/2328)\n- Resolve: Add experimental button to menu [\\#2325](https://github.com/pypeclub/OpenPype/pull/2325)\n- Hiero: Add experimental tools action [\\#2323](https://github.com/pypeclub/OpenPype/pull/2323)\n- Input links: Cleanup and unification of differences [\\#2322](https://github.com/pypeclub/OpenPype/pull/2322)\n- General: Don't validate vendor bin with executing them [\\#2317](https://github.com/pypeclub/OpenPype/pull/2317)\n- General: Multilayer EXRs support [\\#2315](https://github.com/pypeclub/OpenPype/pull/2315)\n- General: Run process log stderr as info log level [\\#2309](https://github.com/pypeclub/OpenPype/pull/2309)\n- General: Reduce vendor imports [\\#2305](https://github.com/pypeclub/OpenPype/pull/2305)\n- Tools: Cleanup of unused classes [\\#2304](https://github.com/pypeclub/OpenPype/pull/2304)\n- Project Manager: Added ability to delete project [\\#2298](https://github.com/pypeclub/OpenPype/pull/2298)\n- Ftrack: Synchronize input links [\\#2287](https://github.com/pypeclub/OpenPype/pull/2287)\n- StandalonePublisher: Remove unused plugin ExtractHarmonyZip [\\#2277](https://github.com/pypeclub/OpenPype/pull/2277)\n- Ftrack: Support multiple reviews [\\#2271](https://github.com/pypeclub/OpenPype/pull/2271)\n- Ftrack: Remove unused clean component plugin [\\#2269](https://github.com/pypeclub/OpenPype/pull/2269)\n- Royal Render: Support for rr channels in separate dirs [\\#2268](https://github.com/pypeclub/OpenPype/pull/2268)\n- Houdini: Add experimental tools action [\\#2267](https://github.com/pypeclub/OpenPype/pull/2267)\n- Nuke: extract baked review videos presets [\\#2248](https://github.com/pypeclub/OpenPype/pull/2248)\n- TVPaint: Workers rendering [\\#2209](https://github.com/pypeclub/OpenPype/pull/2209)\n- OpenPypeV3: Add key parent asset to path templating construction [\\#2186](https://github.com/pypeclub/OpenPype/pull/2186)\n\n**🐛 Bug fixes**\n\n- TVPaint: Create render layer dialog is in front [\\#2471](https://github.com/pypeclub/OpenPype/pull/2471)\n- Short Pyblish plugin path [\\#2428](https://github.com/pypeclub/OpenPype/pull/2428)\n- PS: Introduced settings for invalid characters to use in ValidateNaming plugin [\\#2417](https://github.com/pypeclub/OpenPype/pull/2417)\n- Settings UI: Breadcrumbs path does not create new entities [\\#2416](https://github.com/pypeclub/OpenPype/pull/2416)\n- AfterEffects: Variant 2022 is in defaults but missing in schemas [\\#2412](https://github.com/pypeclub/OpenPype/pull/2412)\n- Nuke: baking representations was not additive [\\#2406](https://github.com/pypeclub/OpenPype/pull/2406)\n- General: Fix access to environments from default settings [\\#2403](https://github.com/pypeclub/OpenPype/pull/2403)\n- Fix: Placeholder Input color set fix [\\#2399](https://github.com/pypeclub/OpenPype/pull/2399)\n- Settings: Fix state change of wrapper label [\\#2396](https://github.com/pypeclub/OpenPype/pull/2396)\n- Flame: fix ftrack publisher [\\#2381](https://github.com/pypeclub/OpenPype/pull/2381)\n- hiero: solve custom ocio path  [\\#2379](https://github.com/pypeclub/OpenPype/pull/2379)\n- hiero: fix workio and flatten [\\#2378](https://github.com/pypeclub/OpenPype/pull/2378)\n- Nuke: fixing menu re-drawing during context change  [\\#2374](https://github.com/pypeclub/OpenPype/pull/2374)\n- Webpublisher: Fix assignment of families of TVpaint instances [\\#2373](https://github.com/pypeclub/OpenPype/pull/2373)\n- Nuke: fixing node name based on switched asset name [\\#2369](https://github.com/pypeclub/OpenPype/pull/2369)\n- JobQueue: Fix loading of settings [\\#2362](https://github.com/pypeclub/OpenPype/pull/2362)\n- Tools: Placeholder color [\\#2359](https://github.com/pypeclub/OpenPype/pull/2359)\n- Launcher: Minimize button on MacOs [\\#2355](https://github.com/pypeclub/OpenPype/pull/2355)\n- StandalonePublisher: Fix import of constant [\\#2354](https://github.com/pypeclub/OpenPype/pull/2354)\n- Houdini: Fix HDA creation [\\#2350](https://github.com/pypeclub/OpenPype/pull/2350)\n- Adobe products show issue [\\#2347](https://github.com/pypeclub/OpenPype/pull/2347)\n- Maya Look Assigner: Fix Python 3 compatibility [\\#2343](https://github.com/pypeclub/OpenPype/pull/2343)\n- Remove wrongly used host for hook [\\#2342](https://github.com/pypeclub/OpenPype/pull/2342)\n- Tools: Use Qt context on tools show [\\#2340](https://github.com/pypeclub/OpenPype/pull/2340)\n- Flame: Fix default argument value in custom dictionary [\\#2339](https://github.com/pypeclub/OpenPype/pull/2339)\n- Timers Manager: Disable auto stop timer on linux platform [\\#2334](https://github.com/pypeclub/OpenPype/pull/2334)\n- nuke: bake preset single input exception  [\\#2331](https://github.com/pypeclub/OpenPype/pull/2331)\n- Hiero: fixing multiple templates at a hierarchy parent [\\#2330](https://github.com/pypeclub/OpenPype/pull/2330)\n- Fix - provider icons are pulled from a folder [\\#2326](https://github.com/pypeclub/OpenPype/pull/2326)\n- InputLinks: Typo in \"inputLinks\" key [\\#2314](https://github.com/pypeclub/OpenPype/pull/2314)\n- Deadline timeout and logging [\\#2312](https://github.com/pypeclub/OpenPype/pull/2312)\n- nuke: do not multiply representation on class method [\\#2311](https://github.com/pypeclub/OpenPype/pull/2311)\n- Workfiles tool: Fix task formatting [\\#2306](https://github.com/pypeclub/OpenPype/pull/2306)\n- Delivery: Fix delivery paths created on windows [\\#2302](https://github.com/pypeclub/OpenPype/pull/2302)\n- Maya: Deadline - fix limit groups [\\#2295](https://github.com/pypeclub/OpenPype/pull/2295)\n- Royal Render: Fix plugin order and OpenPype auto-detection [\\#2291](https://github.com/pypeclub/OpenPype/pull/2291)\n- New Publisher: Fix mapping of indexes [\\#2285](https://github.com/pypeclub/OpenPype/pull/2285)\n- Alternate site for site sync doesnt work for sequences [\\#2284](https://github.com/pypeclub/OpenPype/pull/2284)\n- FFmpeg: Execute ffprobe using list of arguments instead of string command [\\#2281](https://github.com/pypeclub/OpenPype/pull/2281)\n- Nuke: Anatomy fill data use task as dictionary [\\#2278](https://github.com/pypeclub/OpenPype/pull/2278)\n- Bug: fix variable name \\_asset\\_id in workfiles application [\\#2274](https://github.com/pypeclub/OpenPype/pull/2274)\n- Version handling fixes [\\#2272](https://github.com/pypeclub/OpenPype/pull/2272)\n\n**Merged pull requests:**\n\n- Maya: Replaced PATH usage with vendored oiio path for maketx utility [\\#2405](https://github.com/pypeclub/OpenPype/pull/2405)\n- \\[Fix\\]\\[MAYA\\] Handle message type attribute within CollectLook [\\#2394](https://github.com/pypeclub/OpenPype/pull/2394)\n- Add validator to check correct version of extension for PS and AE [\\#2387](https://github.com/pypeclub/OpenPype/pull/2387)\n- Maya: configurable model top level validation [\\#2321](https://github.com/pypeclub/OpenPype/pull/2321)\n- Create test publish class for After Effects [\\#2270](https://github.com/pypeclub/OpenPype/pull/2270)\n\n## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.3...3.6.4)\n\n**🐛 Bug fixes**\n\n- Nuke: inventory update removes all loaded read nodes [\\#2294](https://github.com/pypeclub/OpenPype/pull/2294)\n\n## [3.6.3](https://github.com/pypeclub/OpenPype/tree/3.6.3) (2021-11-19)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.2...3.6.3)\n\n**🐛 Bug fixes**\n\n- Deadline: Fix publish targets [\\#2280](https://github.com/pypeclub/OpenPype/pull/2280)\n\n## [3.6.2](https://github.com/pypeclub/OpenPype/tree/3.6.2) (2021-11-18)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.1...3.6.2)\n\n**🚀 Enhancements**\n\n- Tools: Assets widget [\\#2265](https://github.com/pypeclub/OpenPype/pull/2265)\n- SceneInventory: Choose loader in asset switcher [\\#2262](https://github.com/pypeclub/OpenPype/pull/2262)\n- Style: New fonts in OpenPype style [\\#2256](https://github.com/pypeclub/OpenPype/pull/2256)\n- Tools: SceneInventory in OpenPype  [\\#2255](https://github.com/pypeclub/OpenPype/pull/2255)\n- Tools: Tasks widget [\\#2251](https://github.com/pypeclub/OpenPype/pull/2251)\n- Tools: Creator in OpenPype [\\#2244](https://github.com/pypeclub/OpenPype/pull/2244)\n- Added endpoint for configured extensions [\\#2221](https://github.com/pypeclub/OpenPype/pull/2221)\n\n**🐛 Bug fixes**\n\n- Tools: Parenting of tools in Nuke and Hiero [\\#2266](https://github.com/pypeclub/OpenPype/pull/2266)\n- limiting validator to specific editorial hosts [\\#2264](https://github.com/pypeclub/OpenPype/pull/2264)\n- Tools: Select Context dialog attribute fix [\\#2261](https://github.com/pypeclub/OpenPype/pull/2261)\n- Maya: Render publishing fails on linux [\\#2260](https://github.com/pypeclub/OpenPype/pull/2260)\n- LookAssigner: Fix tool reopen [\\#2259](https://github.com/pypeclub/OpenPype/pull/2259)\n- Standalone: editorial not publishing thumbnails on all subsets [\\#2258](https://github.com/pypeclub/OpenPype/pull/2258)\n- Burnins: Support mxf metadata [\\#2247](https://github.com/pypeclub/OpenPype/pull/2247)\n- Maya: Support for configurable AOV separator characters [\\#2197](https://github.com/pypeclub/OpenPype/pull/2197)\n- Maya: texture colorspace modes in looks [\\#2195](https://github.com/pypeclub/OpenPype/pull/2195)\n\n## [3.6.1](https://github.com/pypeclub/OpenPype/tree/3.6.1) (2021-11-16)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.0...3.6.1)\n\n**🐛 Bug fixes**\n\n- Loader doesn't allow changing of version before loading [\\#2254](https://github.com/pypeclub/OpenPype/pull/2254)\n\n## [3.6.0](https://github.com/pypeclub/OpenPype/tree/3.6.0) (2021-11-15)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.5.0...3.6.0)\n\n### 📖 Documentation\n\n- Add alternative sites for Site Sync [\\#2206](https://github.com/pypeclub/OpenPype/pull/2206)\n- Add command line way of running site sync server [\\#2188](https://github.com/pypeclub/OpenPype/pull/2188)\n\n**🆕 New features**\n\n- Add validate active site button to sync queue on a project [\\#2176](https://github.com/pypeclub/OpenPype/pull/2176)\n- Maya : Colorspace configuration  [\\#2170](https://github.com/pypeclub/OpenPype/pull/2170)\n- Blender: Added support for audio [\\#2168](https://github.com/pypeclub/OpenPype/pull/2168)\n- Flame: a host basic integration [\\#2165](https://github.com/pypeclub/OpenPype/pull/2165)\n- Houdini: simple HDA workflow [\\#2072](https://github.com/pypeclub/OpenPype/pull/2072)\n- Basic Royal Render Integration ✨ [\\#2061](https://github.com/pypeclub/OpenPype/pull/2061)\n- Camera handling between Blender and Unreal [\\#1988](https://github.com/pypeclub/OpenPype/pull/1988)\n- switch PyQt5 for PySide2 [\\#1744](https://github.com/pypeclub/OpenPype/pull/1744)\n\n**🚀 Enhancements**\n\n- Tools: Subset manager in OpenPype [\\#2243](https://github.com/pypeclub/OpenPype/pull/2243)\n- General: Skip module directories without init file [\\#2239](https://github.com/pypeclub/OpenPype/pull/2239)\n- General: Static interfaces [\\#2238](https://github.com/pypeclub/OpenPype/pull/2238)\n- Style: Fix transparent image in style [\\#2235](https://github.com/pypeclub/OpenPype/pull/2235)\n- Add a \"following workfile versioning\" option on publish [\\#2225](https://github.com/pypeclub/OpenPype/pull/2225)\n- Modules: Module can add cli commands [\\#2224](https://github.com/pypeclub/OpenPype/pull/2224)\n- Webpublisher: Separate webpublisher logic [\\#2222](https://github.com/pypeclub/OpenPype/pull/2222)\n- Add both side availability on Site Sync sites to Loader [\\#2220](https://github.com/pypeclub/OpenPype/pull/2220)\n- Tools: Center loader and library loader on show [\\#2219](https://github.com/pypeclub/OpenPype/pull/2219)\n- Maya : Validate shape zero [\\#2212](https://github.com/pypeclub/OpenPype/pull/2212)\n- Maya : validate unique names [\\#2211](https://github.com/pypeclub/OpenPype/pull/2211)\n- Tools: OpenPype stylesheet in workfiles tool [\\#2208](https://github.com/pypeclub/OpenPype/pull/2208)\n- Ftrack: Replace Queue with deque in event handlers logic [\\#2204](https://github.com/pypeclub/OpenPype/pull/2204)\n- Tools: New select context dialog [\\#2200](https://github.com/pypeclub/OpenPype/pull/2200)\n- Maya : Validate mesh ngons [\\#2199](https://github.com/pypeclub/OpenPype/pull/2199)\n- Dirmap in Nuke [\\#2198](https://github.com/pypeclub/OpenPype/pull/2198)\n- Delivery: Check 'frame' key in template for sequence delivery [\\#2196](https://github.com/pypeclub/OpenPype/pull/2196)\n- Settings: Site sync project settings improvement [\\#2193](https://github.com/pypeclub/OpenPype/pull/2193)\n- Usage of tools code [\\#2185](https://github.com/pypeclub/OpenPype/pull/2185)\n- Settings: Dictionary based on project roots [\\#2184](https://github.com/pypeclub/OpenPype/pull/2184)\n- Subset name: Be able to pass asset document to get subset name [\\#2179](https://github.com/pypeclub/OpenPype/pull/2179)\n- Tools: Experimental tools [\\#2167](https://github.com/pypeclub/OpenPype/pull/2167)\n- Loader: Refactor and use OpenPype stylesheets [\\#2166](https://github.com/pypeclub/OpenPype/pull/2166)\n- Add loader for linked smart objects in photoshop [\\#2149](https://github.com/pypeclub/OpenPype/pull/2149)\n- Burnins: DNxHD profiles handling [\\#2142](https://github.com/pypeclub/OpenPype/pull/2142)\n- Tools: Single access point for host tools [\\#2139](https://github.com/pypeclub/OpenPype/pull/2139)\n\n**🐛 Bug fixes**\n\n- Ftrack: Sync project ftrack id cache issue [\\#2250](https://github.com/pypeclub/OpenPype/pull/2250)\n- Ftrack: Session creation and Prepare project [\\#2245](https://github.com/pypeclub/OpenPype/pull/2245)\n- Added queue for studio processing in PS [\\#2237](https://github.com/pypeclub/OpenPype/pull/2237)\n- Python 2: Unicode to string conversion [\\#2236](https://github.com/pypeclub/OpenPype/pull/2236)\n- Fix - enum for color coding in PS [\\#2234](https://github.com/pypeclub/OpenPype/pull/2234)\n- Pyblish Tool: Fix targets handling [\\#2232](https://github.com/pypeclub/OpenPype/pull/2232)\n- Ftrack: Base event fix of 'get\\_project\\_from\\_entity' method [\\#2214](https://github.com/pypeclub/OpenPype/pull/2214)\n- Maya : multiple subsets review broken [\\#2210](https://github.com/pypeclub/OpenPype/pull/2210)\n- Fix - different command used for Linux and Mac OS [\\#2207](https://github.com/pypeclub/OpenPype/pull/2207)\n- Tools: Workfiles tool don't use avalon widgets [\\#2205](https://github.com/pypeclub/OpenPype/pull/2205)\n- Ftrack: Fill missing ftrack id on mongo project [\\#2203](https://github.com/pypeclub/OpenPype/pull/2203)\n- Project Manager: Fix copying of tasks [\\#2191](https://github.com/pypeclub/OpenPype/pull/2191)\n- StandalonePublisher: Source validator don't expect representations [\\#2190](https://github.com/pypeclub/OpenPype/pull/2190)\n- Blender: Fix trying to pack an image when the shader node has no texture [\\#2183](https://github.com/pypeclub/OpenPype/pull/2183)\n- Maya: review viewport settings [\\#2177](https://github.com/pypeclub/OpenPype/pull/2177)\n- MacOS: Launching of applications may cause Permissions error [\\#2175](https://github.com/pypeclub/OpenPype/pull/2175)\n- Maya: Aspect ratio [\\#2174](https://github.com/pypeclub/OpenPype/pull/2174)\n- Blender: Fix 'Deselect All' with object not in 'Object Mode' [\\#2163](https://github.com/pypeclub/OpenPype/pull/2163)\n- Tools: Stylesheets are applied after tool show [\\#2161](https://github.com/pypeclub/OpenPype/pull/2161)\n- Maya: Collect render - fix UNC path support 🐛 [\\#2158](https://github.com/pypeclub/OpenPype/pull/2158)\n- Maya: Fix hotbox broken by scriptsmenu [\\#2151](https://github.com/pypeclub/OpenPype/pull/2151)\n- Ftrack: Ignore save warnings exception in Prepare project action [\\#2150](https://github.com/pypeclub/OpenPype/pull/2150)\n- Loader thumbnails with smooth edges [\\#2147](https://github.com/pypeclub/OpenPype/pull/2147)\n- Added validator for source files for Standalone Publisher [\\#2138](https://github.com/pypeclub/OpenPype/pull/2138)\n\n**Merged pull requests:**\n\n- Bump pillow from 8.2.0 to 8.3.2 [\\#2162](https://github.com/pypeclub/OpenPype/pull/2162)\n- Bump axios from 0.21.1 to 0.21.4 in /website [\\#2059](https://github.com/pypeclub/OpenPype/pull/2059)\n\n## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...3.5.0)\n\n**Deprecated:**\n\n- Maya: Change mayaAscii family to mayaScene [\\#2106](https://github.com/pypeclub/OpenPype/pull/2106)\n\n**🆕 New features**\n\n- Added project and task into context change message in Maya [\\#2131](https://github.com/pypeclub/OpenPype/pull/2131)\n- Add ExtractBurnin to photoshop review [\\#2124](https://github.com/pypeclub/OpenPype/pull/2124)\n- PYPE-1218 - changed namespace to contain subset name in Maya [\\#2114](https://github.com/pypeclub/OpenPype/pull/2114)\n- Added running configurable disk mapping command before start of OP [\\#2091](https://github.com/pypeclub/OpenPype/pull/2091)\n- SFTP provider [\\#2073](https://github.com/pypeclub/OpenPype/pull/2073)\n- Maya: Validate setdress top group [\\#2068](https://github.com/pypeclub/OpenPype/pull/2068)\n- Maya: Enable publishing render attrib sets \\(e.g. V-Ray Displacement\\) with model [\\#1955](https://github.com/pypeclub/OpenPype/pull/1955)\n\n**🚀 Enhancements**\n\n- Maya: make rig validators configurable in settings [\\#2137](https://github.com/pypeclub/OpenPype/pull/2137)\n- Settings: Updated readme for entity types in settings [\\#2132](https://github.com/pypeclub/OpenPype/pull/2132)\n- Nuke: unified clip loader [\\#2128](https://github.com/pypeclub/OpenPype/pull/2128)\n- Settings UI: Project model refreshing and sorting [\\#2104](https://github.com/pypeclub/OpenPype/pull/2104)\n- Create Read From Rendered - Disable Relative paths by default [\\#2093](https://github.com/pypeclub/OpenPype/pull/2093)\n- Added choosing different dirmap mapping if workfile synched locally [\\#2088](https://github.com/pypeclub/OpenPype/pull/2088)\n- General: Remove IdleManager module [\\#2084](https://github.com/pypeclub/OpenPype/pull/2084)\n- Tray UI: Message box about missing settings defaults [\\#2080](https://github.com/pypeclub/OpenPype/pull/2080)\n- Tray UI: Show menu where first click happened [\\#2079](https://github.com/pypeclub/OpenPype/pull/2079)\n- Global: add global validators to settings [\\#2078](https://github.com/pypeclub/OpenPype/pull/2078)\n- Use CRF for burnin when available [\\#2070](https://github.com/pypeclub/OpenPype/pull/2070)\n- Project manager: Filter first item after selection of project [\\#2069](https://github.com/pypeclub/OpenPype/pull/2069)\n- Nuke: Adding `still` image family workflow [\\#2064](https://github.com/pypeclub/OpenPype/pull/2064)\n- Maya: validate authorized loaded plugins [\\#2062](https://github.com/pypeclub/OpenPype/pull/2062)\n- Tools: add support for pyenv on windows [\\#2051](https://github.com/pypeclub/OpenPype/pull/2051)\n- SyncServer: Dropbox Provider [\\#1979](https://github.com/pypeclub/OpenPype/pull/1979)\n- Burnin: Get data from context with defined keys. [\\#1897](https://github.com/pypeclub/OpenPype/pull/1897)\n- Timers manager: Get task time [\\#1896](https://github.com/pypeclub/OpenPype/pull/1896)\n- TVPaint: Option to stop timer on application exit. [\\#1887](https://github.com/pypeclub/OpenPype/pull/1887)\n\n**🐛 Bug fixes**\n\n- Maya: fix model publishing [\\#2130](https://github.com/pypeclub/OpenPype/pull/2130)\n- Fix - oiiotool wasn't recognized even if present [\\#2129](https://github.com/pypeclub/OpenPype/pull/2129)\n- General: Disk mapping group [\\#2120](https://github.com/pypeclub/OpenPype/pull/2120)\n- Hiero: publishing effect first time makes wrong resources path [\\#2115](https://github.com/pypeclub/OpenPype/pull/2115)\n- Add startup script for Houdini Core.  [\\#2110](https://github.com/pypeclub/OpenPype/pull/2110)\n- TVPaint: Behavior name of loop also accept repeat [\\#2109](https://github.com/pypeclub/OpenPype/pull/2109)\n- Ftrack: Project settings save custom attributes skip unknown attributes [\\#2103](https://github.com/pypeclub/OpenPype/pull/2103)\n- Blender: Fix NoneType error when animation\\_data is missing for a rig [\\#2101](https://github.com/pypeclub/OpenPype/pull/2101)\n- Fix broken import in sftp provider [\\#2100](https://github.com/pypeclub/OpenPype/pull/2100)\n- Global: Fix docstring on publish plugin extract review [\\#2097](https://github.com/pypeclub/OpenPype/pull/2097)\n- Delivery Action Files Sequence fix [\\#2096](https://github.com/pypeclub/OpenPype/pull/2096)\n- General: Cloud mongo ca certificate issue [\\#2095](https://github.com/pypeclub/OpenPype/pull/2095)\n- TVPaint: Creator use context from workfile [\\#2087](https://github.com/pypeclub/OpenPype/pull/2087)\n- Blender: fix texture missing when publishing blend files [\\#2085](https://github.com/pypeclub/OpenPype/pull/2085)\n- General: Startup validations oiio tool path fix on linux [\\#2083](https://github.com/pypeclub/OpenPype/pull/2083)\n- Deadline: Collect deadline server does not check existence of deadline key [\\#2082](https://github.com/pypeclub/OpenPype/pull/2082)\n- Blender: fixed Curves with modifiers in Rigs [\\#2081](https://github.com/pypeclub/OpenPype/pull/2081)\n- Nuke UI scaling [\\#2077](https://github.com/pypeclub/OpenPype/pull/2077)\n- Maya: Fix multi-camera renders [\\#2065](https://github.com/pypeclub/OpenPype/pull/2065)\n- Fix Sync Queue when project disabled [\\#2063](https://github.com/pypeclub/OpenPype/pull/2063)\n\n**Merged pull requests:**\n\n- Bump pywin32 from 300 to 301 [\\#2086](https://github.com/pypeclub/OpenPype/pull/2086)\n\n## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.0...3.4.1)\n\n**🆕 New features**\n\n- Settings: Flag project as deactivated and hide from tools' view [\\#2008](https://github.com/pypeclub/OpenPype/pull/2008)\n\n**🚀 Enhancements**\n\n- General: Startup validations [\\#2054](https://github.com/pypeclub/OpenPype/pull/2054)\n- Nuke: proxy mode validator [\\#2052](https://github.com/pypeclub/OpenPype/pull/2052)\n- Ftrack: Removed ftrack interface [\\#2049](https://github.com/pypeclub/OpenPype/pull/2049)\n- Settings UI: Deffered set value on entity [\\#2044](https://github.com/pypeclub/OpenPype/pull/2044)\n- Loader: Families filtering [\\#2043](https://github.com/pypeclub/OpenPype/pull/2043)\n- Settings UI: Project view enhancements [\\#2042](https://github.com/pypeclub/OpenPype/pull/2042)\n- Settings for Nuke IncrementScriptVersion [\\#2039](https://github.com/pypeclub/OpenPype/pull/2039)\n- Loader & Library loader: Use tools from OpenPype [\\#2038](https://github.com/pypeclub/OpenPype/pull/2038)\n- Adding predefined project folders creation in PM [\\#2030](https://github.com/pypeclub/OpenPype/pull/2030)\n- WebserverModule: Removed interface of webserver module [\\#2028](https://github.com/pypeclub/OpenPype/pull/2028)\n- TimersManager: Removed interface of timers manager [\\#2024](https://github.com/pypeclub/OpenPype/pull/2024)\n- Feature Maya import asset from scene inventory [\\#2018](https://github.com/pypeclub/OpenPype/pull/2018)\n\n**🐛 Bug fixes**\n\n- Timers manger: Typo fix [\\#2058](https://github.com/pypeclub/OpenPype/pull/2058)\n- Hiero: Editorial fixes [\\#2057](https://github.com/pypeclub/OpenPype/pull/2057)\n- Differentiate jpg sequences from thumbnail [\\#2056](https://github.com/pypeclub/OpenPype/pull/2056)\n- FFmpeg: Split command to list does not work [\\#2046](https://github.com/pypeclub/OpenPype/pull/2046)\n- Removed shell flag in subprocess call [\\#2045](https://github.com/pypeclub/OpenPype/pull/2045)\n\n**Merged pull requests:**\n\n- Bump prismjs from 1.24.0 to 1.25.0 in /website [\\#2050](https://github.com/pypeclub/OpenPype/pull/2050)\n\n## [3.4.0](https://github.com/pypeclub/OpenPype/tree/3.4.0) (2021-09-17)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...3.4.0)\n\n### 📖 Documentation\n\n- Documentation: Ftrack launch argsuments update [\\#2014](https://github.com/pypeclub/OpenPype/pull/2014)\n- Nuke Quick Start / Tutorial [\\#1952](https://github.com/pypeclub/OpenPype/pull/1952)\n- Houdini: add Camera, Point Cache, Composite, Redshift ROP and VDB Cache support [\\#1821](https://github.com/pypeclub/OpenPype/pull/1821)\n\n**🆕 New features**\n\n- Nuke: Compatibility with Nuke 13 [\\#2003](https://github.com/pypeclub/OpenPype/pull/2003)\n- Maya: Add Xgen family support [\\#1947](https://github.com/pypeclub/OpenPype/pull/1947)\n- Feature/webpublisher backend [\\#1876](https://github.com/pypeclub/OpenPype/pull/1876)\n- Blender: Improved assets handling [\\#1615](https://github.com/pypeclub/OpenPype/pull/1615)\n\n**🚀 Enhancements**\n\n- Added possibility to configure of synchronization of workfile version… [\\#2041](https://github.com/pypeclub/OpenPype/pull/2041)\n- General: Task types in profiles [\\#2036](https://github.com/pypeclub/OpenPype/pull/2036)\n- Console interpreter: Handle invalid sizes on initialization [\\#2022](https://github.com/pypeclub/OpenPype/pull/2022)\n- Ftrack: Show OpenPype versions in event server status [\\#2019](https://github.com/pypeclub/OpenPype/pull/2019)\n- General: Staging icon [\\#2017](https://github.com/pypeclub/OpenPype/pull/2017)\n- Ftrack: Sync to avalon actions have jobs [\\#2015](https://github.com/pypeclub/OpenPype/pull/2015)\n- Modules: Connect method is not required [\\#2009](https://github.com/pypeclub/OpenPype/pull/2009)\n- Settings UI: Number with configurable steps [\\#2001](https://github.com/pypeclub/OpenPype/pull/2001)\n- Moving project folder structure creation out of ftrack module \\#1989 [\\#1996](https://github.com/pypeclub/OpenPype/pull/1996)\n- Configurable items for providers without Settings [\\#1987](https://github.com/pypeclub/OpenPype/pull/1987)\n- Global: Example addons [\\#1986](https://github.com/pypeclub/OpenPype/pull/1986)\n- Standalone Publisher: Extract harmony zip handle workfile template [\\#1982](https://github.com/pypeclub/OpenPype/pull/1982)\n- Settings UI: Number sliders [\\#1978](https://github.com/pypeclub/OpenPype/pull/1978)\n- Workfiles: Support more workfile templates [\\#1966](https://github.com/pypeclub/OpenPype/pull/1966)\n- Launcher: Fix crashes on action click [\\#1964](https://github.com/pypeclub/OpenPype/pull/1964)\n- Settings: Minor fixes in UI and missing default values [\\#1963](https://github.com/pypeclub/OpenPype/pull/1963)\n- Blender: Toggle system console works on windows [\\#1962](https://github.com/pypeclub/OpenPype/pull/1962)\n- Global: Settings defined by Addons/Modules [\\#1959](https://github.com/pypeclub/OpenPype/pull/1959)\n- CI: change release numbering triggers [\\#1954](https://github.com/pypeclub/OpenPype/pull/1954)\n- Global: Avalon Host name collector [\\#1949](https://github.com/pypeclub/OpenPype/pull/1949)\n- Global: Define hosts in CollectSceneVersion [\\#1948](https://github.com/pypeclub/OpenPype/pull/1948)\n- Add face sets to exported alembics [\\#1942](https://github.com/pypeclub/OpenPype/pull/1942)\n- OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\\#1939](https://github.com/pypeclub/OpenPype/pull/1939)\n- \\#1894 - adds host to template\\_name\\_profiles for filtering [\\#1915](https://github.com/pypeclub/OpenPype/pull/1915)\n- Environments: Tool environments in alphabetical order [\\#1910](https://github.com/pypeclub/OpenPype/pull/1910)\n- Disregard publishing time. [\\#1888](https://github.com/pypeclub/OpenPype/pull/1888)\n- Dynamic modules [\\#1872](https://github.com/pypeclub/OpenPype/pull/1872)\n\n**🐛 Bug fixes**\n\n- Workfiles tool: Task selection [\\#2040](https://github.com/pypeclub/OpenPype/pull/2040)\n- Ftrack: Delete old versions missing settings key [\\#2037](https://github.com/pypeclub/OpenPype/pull/2037)\n- Nuke: typo on a button [\\#2034](https://github.com/pypeclub/OpenPype/pull/2034)\n- Hiero: Fix \"none\" named tags [\\#2033](https://github.com/pypeclub/OpenPype/pull/2033)\n- FFmpeg: Subprocess arguments as list [\\#2032](https://github.com/pypeclub/OpenPype/pull/2032)\n- General: Fix Python 2 breaking line [\\#2016](https://github.com/pypeclub/OpenPype/pull/2016)\n- Bugfix/webpublisher task type [\\#2006](https://github.com/pypeclub/OpenPype/pull/2006)\n- Nuke thumbnails generated from middle of the sequence [\\#1992](https://github.com/pypeclub/OpenPype/pull/1992)\n- Nuke: last version from path gets correct version [\\#1990](https://github.com/pypeclub/OpenPype/pull/1990)\n- nuke, resolve, hiero: precollector order lest then 0.5 [\\#1984](https://github.com/pypeclub/OpenPype/pull/1984)\n- Last workfile with multiple work templates [\\#1981](https://github.com/pypeclub/OpenPype/pull/1981)\n- Collectors order [\\#1977](https://github.com/pypeclub/OpenPype/pull/1977)\n- Stop timer was within validator order range. [\\#1975](https://github.com/pypeclub/OpenPype/pull/1975)\n- Ftrack: arrow submodule has https url source [\\#1974](https://github.com/pypeclub/OpenPype/pull/1974)\n- Ftrack: Fix hosts attribute in collect ftrack username [\\#1972](https://github.com/pypeclub/OpenPype/pull/1972)\n- Deadline: Houdini plugins in different hierarchy [\\#1970](https://github.com/pypeclub/OpenPype/pull/1970)\n- Removed deprecated submodules [\\#1967](https://github.com/pypeclub/OpenPype/pull/1967)\n- Global: ExtractJpeg can handle filepaths with spaces [\\#1961](https://github.com/pypeclub/OpenPype/pull/1961)\n- Resolve path when adding to zip [\\#1960](https://github.com/pypeclub/OpenPype/pull/1960)\n\n**Merged pull requests:**\n\n- Bump url-parse from 1.5.1 to 1.5.3 in /website [\\#1958](https://github.com/pypeclub/OpenPype/pull/1958)\n- Bump path-parse from 1.0.6 to 1.0.7 in /website [\\#1933](https://github.com/pypeclub/OpenPype/pull/1933)\n\n## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.0...3.3.1)\n\n**🐛 Bug fixes**\n\n- TVPaint: Fixed rendered frame indexes [\\#1946](https://github.com/pypeclub/OpenPype/pull/1946)\n- Maya: Menu actions fix [\\#1945](https://github.com/pypeclub/OpenPype/pull/1945)\n- standalone: editorial shared object problem [\\#1941](https://github.com/pypeclub/OpenPype/pull/1941)\n- Bugfix nuke deadline app name [\\#1928](https://github.com/pypeclub/OpenPype/pull/1928)\n\n## [3.3.0](https://github.com/pypeclub/OpenPype/tree/3.3.0) (2021-08-17)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...3.3.0)\n\n### 📖 Documentation\n\n- Standalone Publish of textures family [\\#1834](https://github.com/pypeclub/OpenPype/pull/1834)\n\n**🆕 New features**\n\n- Settings UI: Breadcrumbs in settings [\\#1932](https://github.com/pypeclub/OpenPype/pull/1932)\n- Maya: Scene patching 🩹on submission to Deadline [\\#1923](https://github.com/pypeclub/OpenPype/pull/1923)\n- Feature AE local render [\\#1901](https://github.com/pypeclub/OpenPype/pull/1901)\n\n**🚀 Enhancements**\n\n- Python console interpreter [\\#1940](https://github.com/pypeclub/OpenPype/pull/1940)\n- Global: Updated logos and Default settings [\\#1927](https://github.com/pypeclub/OpenPype/pull/1927)\n- Check for missing ✨ Python when using `pyenv` [\\#1925](https://github.com/pypeclub/OpenPype/pull/1925)\n- Settings: Default values for enum [\\#1920](https://github.com/pypeclub/OpenPype/pull/1920)\n- Settings UI: Modifiable dict view enhance [\\#1919](https://github.com/pypeclub/OpenPype/pull/1919)\n- submodules: avalon-core update [\\#1911](https://github.com/pypeclub/OpenPype/pull/1911)\n- Ftrack: Where I run action enhancement [\\#1900](https://github.com/pypeclub/OpenPype/pull/1900)\n- Ftrack: Private project server actions [\\#1899](https://github.com/pypeclub/OpenPype/pull/1899)\n- Support nested studio plugins paths. [\\#1898](https://github.com/pypeclub/OpenPype/pull/1898)\n- Settings: global validators with options [\\#1892](https://github.com/pypeclub/OpenPype/pull/1892)\n- Settings: Conditional dict enum positioning [\\#1891](https://github.com/pypeclub/OpenPype/pull/1891)\n- Expose stop timer through rest api. [\\#1886](https://github.com/pypeclub/OpenPype/pull/1886)\n- TVPaint: Increment workfile [\\#1885](https://github.com/pypeclub/OpenPype/pull/1885)\n- Allow Multiple Notes to run on tasks. [\\#1882](https://github.com/pypeclub/OpenPype/pull/1882)\n- Prepare for pyside2 [\\#1869](https://github.com/pypeclub/OpenPype/pull/1869)\n- Filter hosts in settings host-enum [\\#1868](https://github.com/pypeclub/OpenPype/pull/1868)\n- Local actions with process identifier [\\#1867](https://github.com/pypeclub/OpenPype/pull/1867)\n- Workfile tool start at host launch support [\\#1865](https://github.com/pypeclub/OpenPype/pull/1865)\n- Anatomy schema validation [\\#1864](https://github.com/pypeclub/OpenPype/pull/1864)\n- Ftrack prepare project structure [\\#1861](https://github.com/pypeclub/OpenPype/pull/1861)\n- Maya: support for configurable `dirmap` 🗺️ [\\#1859](https://github.com/pypeclub/OpenPype/pull/1859)\n- Independent general environments [\\#1853](https://github.com/pypeclub/OpenPype/pull/1853)\n- TVPaint Start Frame [\\#1844](https://github.com/pypeclub/OpenPype/pull/1844)\n- Ftrack push attributes action adds traceback to job [\\#1843](https://github.com/pypeclub/OpenPype/pull/1843)\n- Prepare project action enhance [\\#1838](https://github.com/pypeclub/OpenPype/pull/1838)\n- nuke: settings create missing default subsets [\\#1829](https://github.com/pypeclub/OpenPype/pull/1829)\n- Update poetry lock [\\#1823](https://github.com/pypeclub/OpenPype/pull/1823)\n- Settings: settings for plugins [\\#1819](https://github.com/pypeclub/OpenPype/pull/1819)\n- Settings list can use template or schema as object type [\\#1815](https://github.com/pypeclub/OpenPype/pull/1815)\n- Maya: Deadline custom settings  [\\#1797](https://github.com/pypeclub/OpenPype/pull/1797)\n- Maya: Shader name validation [\\#1762](https://github.com/pypeclub/OpenPype/pull/1762)\n\n**🐛 Bug fixes**\n\n- Fix - ftrack family was added incorrectly in some cases [\\#1935](https://github.com/pypeclub/OpenPype/pull/1935)\n- Fix - Deadline publish on Linux started Tray instead of headless publishing [\\#1930](https://github.com/pypeclub/OpenPype/pull/1930)\n- Maya: Validate Model Name - repair accident deletion in settings defaults [\\#1929](https://github.com/pypeclub/OpenPype/pull/1929)\n- Nuke: submit to farm failed due `ftrack` family remove [\\#1926](https://github.com/pypeclub/OpenPype/pull/1926)\n- Fix - validate takes repre\\[\"files\"\\] as list all the time [\\#1922](https://github.com/pypeclub/OpenPype/pull/1922)\n- standalone: validator asset parents [\\#1917](https://github.com/pypeclub/OpenPype/pull/1917)\n- Nuke: update video file crassing [\\#1916](https://github.com/pypeclub/OpenPype/pull/1916)\n- Fix - texture validators for workfiles triggers only for textures workfiles [\\#1914](https://github.com/pypeclub/OpenPype/pull/1914)\n- Settings UI: List order works as expected [\\#1906](https://github.com/pypeclub/OpenPype/pull/1906)\n- Hiero: loaded clip was not set colorspace from version data [\\#1904](https://github.com/pypeclub/OpenPype/pull/1904)\n- Pyblish UI: Fix collecting stage processing [\\#1903](https://github.com/pypeclub/OpenPype/pull/1903)\n- Burnins: Use input's bitrate in h624 [\\#1902](https://github.com/pypeclub/OpenPype/pull/1902)\n- Bug: fixed python detection [\\#1893](https://github.com/pypeclub/OpenPype/pull/1893)\n- global: integrate name missing default template [\\#1890](https://github.com/pypeclub/OpenPype/pull/1890)\n- publisher: editorial plugins fixes [\\#1889](https://github.com/pypeclub/OpenPype/pull/1889)\n- Normalize path returned from Workfiles. [\\#1880](https://github.com/pypeclub/OpenPype/pull/1880)\n- Workfiles tool event arguments fix [\\#1862](https://github.com/pypeclub/OpenPype/pull/1862)\n- imageio: fix grouping  [\\#1856](https://github.com/pypeclub/OpenPype/pull/1856)\n- Maya: don't add reference members as connections to the container set 📦 [\\#1855](https://github.com/pypeclub/OpenPype/pull/1855)\n- publisher: missing version in subset prop [\\#1849](https://github.com/pypeclub/OpenPype/pull/1849)\n- Ftrack type error fix in sync to avalon event handler [\\#1845](https://github.com/pypeclub/OpenPype/pull/1845)\n- Nuke: updating effects subset fail [\\#1841](https://github.com/pypeclub/OpenPype/pull/1841)\n- nuke: write render node skipped with crop [\\#1836](https://github.com/pypeclub/OpenPype/pull/1836)\n- Project folder structure overrides [\\#1813](https://github.com/pypeclub/OpenPype/pull/1813)\n- Maya: fix yeti settings path in extractor [\\#1809](https://github.com/pypeclub/OpenPype/pull/1809)\n- Failsafe for cross project containers. [\\#1806](https://github.com/pypeclub/OpenPype/pull/1806)\n- Houdini colector formatting keys fix [\\#1802](https://github.com/pypeclub/OpenPype/pull/1802)\n- Settings error dialog on show [\\#1798](https://github.com/pypeclub/OpenPype/pull/1798)\n- Application launch stdout/stderr in GUI build [\\#1684](https://github.com/pypeclub/OpenPype/pull/1684)\n- Nuke: re-use instance nodes output path [\\#1577](https://github.com/pypeclub/OpenPype/pull/1577)\n\n**Merged pull requests:**\n\n- Fix - make AE workfile publish to Ftrack configurable [\\#1937](https://github.com/pypeclub/OpenPype/pull/1937)\n- Add support for multiple Deadline ☠️➖ servers [\\#1905](https://github.com/pypeclub/OpenPype/pull/1905)\n- Maya: add support for `RedshiftNormalMap` node, fix `tx` linear space 🚀 [\\#1863](https://github.com/pypeclub/OpenPype/pull/1863)\n- Maya: expected files -\\> render products ⚙️ overhaul [\\#1812](https://github.com/pypeclub/OpenPype/pull/1812)\n- PS, AE - send actual context when another webserver is running [\\#1811](https://github.com/pypeclub/OpenPype/pull/1811)\n\n## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...3.2.0)\n\n### 📖 Documentation\n\n- Fix: staging and `--use-version` option [\\#1786](https://github.com/pypeclub/OpenPype/pull/1786)\n- Subset template and TVPaint subset template docs [\\#1717](https://github.com/pypeclub/OpenPype/pull/1717)\n- Overscan color extract review [\\#1701](https://github.com/pypeclub/OpenPype/pull/1701)\n\n**🚀 Enhancements**\n\n- Nuke: ftrack family plugin settings preset [\\#1805](https://github.com/pypeclub/OpenPype/pull/1805)\n- Standalone publisher last project [\\#1799](https://github.com/pypeclub/OpenPype/pull/1799)\n- Ftrack Multiple notes as server action [\\#1795](https://github.com/pypeclub/OpenPype/pull/1795)\n- Settings conditional dict [\\#1777](https://github.com/pypeclub/OpenPype/pull/1777)\n- Settings application use python 2 only where needed [\\#1776](https://github.com/pypeclub/OpenPype/pull/1776)\n- Settings UI copy/paste [\\#1769](https://github.com/pypeclub/OpenPype/pull/1769)\n- Workfile tool widths [\\#1766](https://github.com/pypeclub/OpenPype/pull/1766)\n- Push hierarchical attributes care about task parent changes [\\#1763](https://github.com/pypeclub/OpenPype/pull/1763)\n- Application executables with environment variables [\\#1757](https://github.com/pypeclub/OpenPype/pull/1757)\n- Deadline: Nuke submission additional attributes [\\#1756](https://github.com/pypeclub/OpenPype/pull/1756)\n- Settings schema without prefill [\\#1753](https://github.com/pypeclub/OpenPype/pull/1753)\n- Settings Hosts enum [\\#1739](https://github.com/pypeclub/OpenPype/pull/1739)\n- Validate containers settings [\\#1736](https://github.com/pypeclub/OpenPype/pull/1736)\n- PS - added loader from sequence [\\#1726](https://github.com/pypeclub/OpenPype/pull/1726)\n- Autoupdate launcher [\\#1725](https://github.com/pypeclub/OpenPype/pull/1725)\n- Toggle Ftrack upload in StandalonePublisher [\\#1708](https://github.com/pypeclub/OpenPype/pull/1708)\n- Nuke: Prerender Frame Range by default [\\#1699](https://github.com/pypeclub/OpenPype/pull/1699)\n- Smoother edges of color triangle [\\#1695](https://github.com/pypeclub/OpenPype/pull/1695)\n\n**🐛 Bug fixes**\n\n- nuke: fixing wrong name of family folder when `used existing frames` [\\#1803](https://github.com/pypeclub/OpenPype/pull/1803)\n- Collect ftrack family bugs [\\#1801](https://github.com/pypeclub/OpenPype/pull/1801)\n- Invitee email can be None which break the Ftrack commit. [\\#1788](https://github.com/pypeclub/OpenPype/pull/1788)\n- Otio unrelated error on import [\\#1782](https://github.com/pypeclub/OpenPype/pull/1782)\n- FFprobe streams order [\\#1775](https://github.com/pypeclub/OpenPype/pull/1775)\n- Fix - single file files are str only, cast it to list to count properly [\\#1772](https://github.com/pypeclub/OpenPype/pull/1772)\n- Environments in app executable for MacOS [\\#1768](https://github.com/pypeclub/OpenPype/pull/1768)\n- Project specific environments [\\#1767](https://github.com/pypeclub/OpenPype/pull/1767)\n- Settings UI with refresh button [\\#1764](https://github.com/pypeclub/OpenPype/pull/1764)\n- Standalone publisher thumbnail extractor fix [\\#1761](https://github.com/pypeclub/OpenPype/pull/1761)\n- Anatomy others templates don't cause crash [\\#1758](https://github.com/pypeclub/OpenPype/pull/1758)\n- Backend acre module commit update [\\#1745](https://github.com/pypeclub/OpenPype/pull/1745)\n- hiero: precollect instances failing when audio selected [\\#1743](https://github.com/pypeclub/OpenPype/pull/1743)\n- Hiero: creator instance error [\\#1742](https://github.com/pypeclub/OpenPype/pull/1742)\n- Nuke: fixing render creator for no selection format failing [\\#1741](https://github.com/pypeclub/OpenPype/pull/1741)\n- StandalonePublisher: failing collector for editorial [\\#1738](https://github.com/pypeclub/OpenPype/pull/1738)\n- Local settings UI crash on missing defaults [\\#1737](https://github.com/pypeclub/OpenPype/pull/1737)\n- TVPaint white background on thumbnail [\\#1735](https://github.com/pypeclub/OpenPype/pull/1735)\n- Ftrack missing custom attribute message [\\#1734](https://github.com/pypeclub/OpenPype/pull/1734)\n- Launcher project changes [\\#1733](https://github.com/pypeclub/OpenPype/pull/1733)\n- Ftrack sync status [\\#1732](https://github.com/pypeclub/OpenPype/pull/1732)\n- TVPaint use layer name for default variant [\\#1724](https://github.com/pypeclub/OpenPype/pull/1724)\n- Default subset template for TVPaint review and workfile families [\\#1716](https://github.com/pypeclub/OpenPype/pull/1716)\n- Maya: Extract review hotfix [\\#1714](https://github.com/pypeclub/OpenPype/pull/1714)\n- Settings: Imageio improving granularity [\\#1711](https://github.com/pypeclub/OpenPype/pull/1711)\n- Application without executables [\\#1679](https://github.com/pypeclub/OpenPype/pull/1679)\n- Unreal: launching on Linux [\\#1672](https://github.com/pypeclub/OpenPype/pull/1672)\n\n**Merged pull requests:**\n\n- Bump prismjs from 1.23.0 to 1.24.0 in /website [\\#1773](https://github.com/pypeclub/OpenPype/pull/1773)\n- TVPaint ftrack family [\\#1755](https://github.com/pypeclub/OpenPype/pull/1755)\n\n## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4)\n\n## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-23)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.2...2.18.3)\n\n## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2)\n\n## [3.1.0](https://github.com/pypeclub/OpenPype/tree/3.1.0) (2021-06-15)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...3.1.0)\n\n### 📖 Documentation\n\n- Feature Slack integration [\\#1657](https://github.com/pypeclub/OpenPype/pull/1657)\n\n**🚀 Enhancements**\n\n- Log Viewer with OpenPype style [\\#1703](https://github.com/pypeclub/OpenPype/pull/1703)\n- Scrolling in OpenPype info widget [\\#1702](https://github.com/pypeclub/OpenPype/pull/1702)\n- OpenPype style in modules [\\#1694](https://github.com/pypeclub/OpenPype/pull/1694)\n- Sort applications and tools alphabetically in Settings UI [\\#1689](https://github.com/pypeclub/OpenPype/pull/1689)\n- \\#683 - Validate Frame Range in Standalone Publisher [\\#1683](https://github.com/pypeclub/OpenPype/pull/1683)\n- Hiero: old container versions identify with red color [\\#1682](https://github.com/pypeclub/OpenPype/pull/1682)\n- Project Manger: Default name column width [\\#1669](https://github.com/pypeclub/OpenPype/pull/1669)\n- Remove outline in stylesheet [\\#1667](https://github.com/pypeclub/OpenPype/pull/1667)\n- TVPaint: Creator take layer name as default value for subset variant [\\#1663](https://github.com/pypeclub/OpenPype/pull/1663)\n- TVPaint custom subset template [\\#1662](https://github.com/pypeclub/OpenPype/pull/1662)\n- Editorial: conform assets validator [\\#1659](https://github.com/pypeclub/OpenPype/pull/1659)\n- Nuke - Publish simplification [\\#1653](https://github.com/pypeclub/OpenPype/pull/1653)\n- \\#1333 - added tooltip hints to Pyblish buttons [\\#1649](https://github.com/pypeclub/OpenPype/pull/1649)\n\n**🐛 Bug fixes**\n\n- Nuke: broken publishing rendered frames [\\#1707](https://github.com/pypeclub/OpenPype/pull/1707)\n- Standalone publisher Thumbnail export args [\\#1705](https://github.com/pypeclub/OpenPype/pull/1705)\n- Bad zip can break OpenPype start [\\#1691](https://github.com/pypeclub/OpenPype/pull/1691)\n- Hiero: published whole edit mov [\\#1687](https://github.com/pypeclub/OpenPype/pull/1687)\n- Ftrack subprocess handle of stdout/stderr [\\#1675](https://github.com/pypeclub/OpenPype/pull/1675)\n- Settings list race condifiton and mutable dict list conversion [\\#1671](https://github.com/pypeclub/OpenPype/pull/1671)\n- Mac launch arguments fix [\\#1660](https://github.com/pypeclub/OpenPype/pull/1660)\n- Fix missing dbm python module [\\#1652](https://github.com/pypeclub/OpenPype/pull/1652)\n- Transparent branches in view on Mac [\\#1648](https://github.com/pypeclub/OpenPype/pull/1648)\n- Add asset on task item [\\#1646](https://github.com/pypeclub/OpenPype/pull/1646)\n- Project manager save and queue [\\#1645](https://github.com/pypeclub/OpenPype/pull/1645)\n- New project anatomy values [\\#1644](https://github.com/pypeclub/OpenPype/pull/1644)\n- Farm publishing: check if published items do exist [\\#1573](https://github.com/pypeclub/OpenPype/pull/1573)\n\n**Merged pull requests:**\n\n- Bump normalize-url from 4.5.0 to 4.5.1 in /website [\\#1686](https://github.com/pypeclub/OpenPype/pull/1686)\n\n\n## [3.0.0](https://github.com/pypeclub/openpype/tree/3.0.0)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.1...3.0.0)\n\n### Configuration\n- Studio Settings GUI: no more json configuration files.\n- OpenPype Modules can be turned on and off.\n- Easy to add Application versions.\n- Per Project Environment and plugin management.\n- Robust profile system for creating reviewables and burnins, with filtering based on Application, Task and data family.\n- Configurable publish plugins.\n- Options to make any validator or extractor, optional or disabled.\n- Color Management is now unified under anatomy settings.\n- Subset naming and grouping is fully configurable.\n- All project attributes can now be set directly in OpenPype settings.\n- Studio Setting can be locked to prevent unwanted artist changes.\n- You can now add per project and per task type templates for workfile initialization in most hosts.\n- Too many other individual configurable option to list in this changelog :)\n\n### Local Settings\n- Local Settings GUI where users can change certain option on individual basis.\n    - Application executables.\n    - Project roots.\n    - Project site sync settings.\n\n### Build, Installation and Deployments\n- No requirements on artist machine.\n- Fully distributed workflow possible.\n- Self-contained installation.\n- Available on all three major platforms.\n- Automatic artist OpenPype updates.\n- Studio OpenPype repository for updates distribution.\n- Robust Build system.\n- Safe studio update versioning with staging and production options.\n- MacOS build generates .app and .dmg installer.\n- Windows build with installer creation script.\n\n### Misc\n- System and diagnostic info tool in the tray.\n- Launching application from Launcher indicates activity.\n- All project roots are now named. Single root project are now achieved by having a single named root in the project anatomy.\n- Every project root is cast into environment variable as well, so it can be used in DCC instead of absolute path (depends on DCC support for env vars).\n- Basic support for task types, on top of task names.\n- Timer now change automatically when the context is switched inside running application.\n- 'Master\" versions have been renamed to \"Hero\".\n- Extract Burnins now supports file sequences and color settings.\n- Extract Review support overscan cropping, better letterboxes and background colour fill.\n- Delivery tool for copying and renaming any published assets in bulk.\n- Harmony, Photoshop and After Effects now connect directly with OpenPype tray instead of spawning their own terminal.\n\n### Project Manager GUI\n- Create Projects.\n- Create Shots and Assets.\n- Create Tasks and assign task types.\n- Fill required asset attributes.\n- Validations for duplicated or unsupported names.\n- Archive Assets.\n- Move Asset within hierarchy.\n\n### Site Sync (beta)\n- Synchronization of published files between workstations and central storage.\n- Ability to add arbitrary storage providers to the Site Sync system.\n- Default setup includes Disk and Google Drive providers as examples.\n- Access to availability information from Loader and Scene Manager.\n- Sync queue GUI with filtering, error and status reporting.\n- Site sync can be configured on a per-project basis.\n- Bulk upload and download from the loader.\n\n### Ftrack\n- Actions have customisable roles.\n- Settings on all actions are updated live and don't need openpype restart.\n- Ftrack module can now be turned off completely.\n- It is enough to specify ftrack server name and the URL will be formed correctly. So instead of mystudio.ftrackapp.com, it's possible to use simply: \"mystudio\".\n\n### Editorial\n- Fully OTIO based editorial publishing.\n- Completely re-done Hiero publishing to be a lot simpler and faster.\n- Consistent conforming from Resolve, Hiero and Standalone Publisher.\n\n### Backend\n- OpenPype and Avalon now always share the same database (in 2.x is was possible to split them).\n- Major codebase refactoring to allow for better CI, versioning and control of individual integrations.\n- OTIO is bundled with build.\n- OIIO is bundled with build.\n- FFMPEG is bundled with build.\n- Rest API and host WebSocket servers have been unified into a single local webserver.\n- Maya look assigner has been integrated into the main codebase.\n- Publish GUI has been integrated into the main codebase.\n- Studio and Project settings overrides are now stored in Mongo.\n- Too many other backend fixes and tweaks to list :), you can see full changelog on github for those.\n- OpenPype uses Poetry to manage it's virtual environment when running from code.\n- all applications can be marked as python 2 or 3 compatible to make the switch a bit easier.\n\n\n### Pull Requests since 3.0.0-rc.6\n\n\n**Implemented enhancements:**\n\n- settings: task types enum entity [\\#1605](https://github.com/pypeclub/OpenPype/issues/1605)\n- Settings: ignore keys in referenced schema [\\#1600](https://github.com/pypeclub/OpenPype/issues/1600)\n- Maya: support for frame steps and frame lists [\\#1585](https://github.com/pypeclub/OpenPype/issues/1585)\n- TVPaint: Publish workfile. [\\#1548](https://github.com/pypeclub/OpenPype/issues/1548)\n- Loader: Current Asset Button [\\#1448](https://github.com/pypeclub/OpenPype/issues/1448)\n- Hiero: publish with retiming [\\#1377](https://github.com/pypeclub/OpenPype/issues/1377)\n- Ask user to restart after changing global environments in settings [\\#910](https://github.com/pypeclub/OpenPype/issues/910)\n- add option to define paht to workfile template [\\#895](https://github.com/pypeclub/OpenPype/issues/895)\n- Harmony: move server console to system tray [\\#676](https://github.com/pypeclub/OpenPype/issues/676)\n- Standalone style [\\#1630](https://github.com/pypeclub/OpenPype/pull/1630) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Faster hierarchical values push [\\#1627](https://github.com/pypeclub/OpenPype/pull/1627) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Launcher tool style [\\#1624](https://github.com/pypeclub/OpenPype/pull/1624) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Loader and Library loader enhancements [\\#1623](https://github.com/pypeclub/OpenPype/pull/1623) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Tray style [\\#1622](https://github.com/pypeclub/OpenPype/pull/1622) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Maya schemas cleanup [\\#1610](https://github.com/pypeclub/OpenPype/pull/1610) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Settings: ignore keys in referenced schema [\\#1608](https://github.com/pypeclub/OpenPype/pull/1608) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- settings: task types enum entity [\\#1606](https://github.com/pypeclub/OpenPype/pull/1606) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Openpype style [\\#1604](https://github.com/pypeclub/OpenPype/pull/1604) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- TVPaint: Publish workfile. [\\#1597](https://github.com/pypeclub/OpenPype/pull/1597) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Nuke: add option to define path to workfile template [\\#1571](https://github.com/pypeclub/OpenPype/pull/1571) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Crop overscan in Extract Review [\\#1569](https://github.com/pypeclub/OpenPype/pull/1569) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Unreal and Blender: Material Workflow [\\#1562](https://github.com/pypeclub/OpenPype/pull/1562) ([simonebarbieri](https://github.com/simonebarbieri))\n- Harmony: move server console to system tray [\\#1560](https://github.com/pypeclub/OpenPype/pull/1560) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Ask user to restart after changing global environments in settings [\\#1550](https://github.com/pypeclub/OpenPype/pull/1550) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Hiero: publish with retiming [\\#1545](https://github.com/pypeclub/OpenPype/pull/1545) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n\n**Fixed bugs:**\n\n- Library loader load asset documents on OpenPype start [\\#1603](https://github.com/pypeclub/OpenPype/issues/1603)\n- Resolve: unable to load the same footage twice [\\#1317](https://github.com/pypeclub/OpenPype/issues/1317)\n- Resolve: unable to load footage [\\#1316](https://github.com/pypeclub/OpenPype/issues/1316)\n- Add required Python 2 modules [\\#1291](https://github.com/pypeclub/OpenPype/issues/1291)\n- GUi scaling with hires displays [\\#705](https://github.com/pypeclub/OpenPype/issues/705)\n- Maya: non unicode string in publish validation [\\#673](https://github.com/pypeclub/OpenPype/issues/673)\n- Nuke: Rendered Frame validation is triggered by multiple collections [\\#156](https://github.com/pypeclub/OpenPype/issues/156)\n- avalon-core debugging failing [\\#80](https://github.com/pypeclub/OpenPype/issues/80)\n- Only check arnold shading group if arnold is used [\\#72](https://github.com/pypeclub/OpenPype/issues/72)\n- Sync server Qt layout fix [\\#1621](https://github.com/pypeclub/OpenPype/pull/1621) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Console Listener on Python 2 fix [\\#1620](https://github.com/pypeclub/OpenPype/pull/1620) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Bug: Initialize blessed term only in console mode [\\#1619](https://github.com/pypeclub/OpenPype/pull/1619) ([antirotor](https://github.com/antirotor))\n- Settings template skip paths support wrappers [\\#1618](https://github.com/pypeclub/OpenPype/pull/1618) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Maya capture 'isolate\\_view' fix + minor corrections [\\#1617](https://github.com/pypeclub/OpenPype/pull/1617) ([2-REC](https://github.com/2-REC))\n- MacOs Fix launch of standalone publisher [\\#1616](https://github.com/pypeclub/OpenPype/pull/1616) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- 'Delivery action' report fix + typos [\\#1612](https://github.com/pypeclub/OpenPype/pull/1612) ([2-REC](https://github.com/2-REC))\n- List append fix in mutable dict settings [\\#1599](https://github.com/pypeclub/OpenPype/pull/1599) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Documentation: Maya: fix review [\\#1598](https://github.com/pypeclub/OpenPype/pull/1598) ([antirotor](https://github.com/antirotor))\n- Bugfix: Set certifi CA bundle for all platforms [\\#1596](https://github.com/pypeclub/OpenPype/pull/1596) ([antirotor](https://github.com/antirotor))\n\n**Merged pull requests:**\n\n- Bump dns-packet from 1.3.1 to 1.3.4 in /website [\\#1611](https://github.com/pypeclub/OpenPype/pull/1611) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Maya: Render workflow fixes [\\#1607](https://github.com/pypeclub/OpenPype/pull/1607) ([antirotor](https://github.com/antirotor))\n- Maya: support for frame steps and frame lists [\\#1586](https://github.com/pypeclub/OpenPype/pull/1586) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- 3.0.0 - curated changelog [\\#1284](https://github.com/pypeclub/OpenPype/pull/1284) ([mkolar](https://github.com/mkolar))\n\n## [2.18.1](https://github.com/pypeclub/openpype/tree/2.18.1) (2021-06-03)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.0...2.18.1)\n\n**Enhancements:**\n\n- Faster hierarchical values push [\\#1626](https://github.com/pypeclub/OpenPype/pull/1626)\n- Feature Delivery in library loader [\\#1549](https://github.com/pypeclub/OpenPype/pull/1549)\n- Hiero: Initial frame publish support. [\\#1172](https://github.com/pypeclub/OpenPype/pull/1172)\n\n**Fixed bugs:**\n\n- Maya capture 'isolate\\_view' fix + minor corrections [\\#1614](https://github.com/pypeclub/OpenPype/pull/1614)\n- 'Delivery action' report fix +typos [\\#1613](https://github.com/pypeclub/OpenPype/pull/1613)\n- Delivery in LibraryLoader - fixed sequence issue [\\#1590](https://github.com/pypeclub/OpenPype/pull/1590)\n- FFmpeg filters in quote marks [\\#1588](https://github.com/pypeclub/OpenPype/pull/1588)\n- Ftrack delete action cause circular error [\\#1581](https://github.com/pypeclub/OpenPype/pull/1581)\n- Fix Maya playblast. [\\#1566](https://github.com/pypeclub/OpenPype/pull/1566)\n- More failsafes prevent errored runs. [\\#1554](https://github.com/pypeclub/OpenPype/pull/1554)\n- Celaction publishing [\\#1539](https://github.com/pypeclub/OpenPype/pull/1539)\n- celaction: app not starting [\\#1533](https://github.com/pypeclub/OpenPype/pull/1533)\n\n**Merged pull requests:**\n\n- Maya: Render workflow fixes - 2.0 backport [\\#1609](https://github.com/pypeclub/OpenPype/pull/1609)\n- Maya Hardware support [\\#1553](https://github.com/pypeclub/OpenPype/pull/1553)\n\n\n## [CI/3.0.0-rc.6](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.6) (2021-05-27)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.5...CI/3.0.0-rc.6)\n\n**Implemented enhancements:**\n\n- Hiero: publish color and transformation soft-effects [\\#1376](https://github.com/pypeclub/OpenPype/issues/1376)\n- Get rid of `AVALON\\_HIERARCHY` and `hiearchy` key on asset [\\#432](https://github.com/pypeclub/OpenPype/issues/432)\n- Sync to avalon do not store hierarchy key [\\#1582](https://github.com/pypeclub/OpenPype/pull/1582) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Tools: launcher scripts for project manager [\\#1557](https://github.com/pypeclub/OpenPype/pull/1557) ([antirotor](https://github.com/antirotor))\n- Simple tvpaint publish [\\#1555](https://github.com/pypeclub/OpenPype/pull/1555) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Feature Delivery in library loader [\\#1546](https://github.com/pypeclub/OpenPype/pull/1546) ([kalisp](https://github.com/kalisp))\n- Documentation: Dev and system build documentation [\\#1543](https://github.com/pypeclub/OpenPype/pull/1543) ([antirotor](https://github.com/antirotor))\n- Color entity [\\#1542](https://github.com/pypeclub/OpenPype/pull/1542) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Extract review bg color [\\#1534](https://github.com/pypeclub/OpenPype/pull/1534) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- TVPaint loader settings [\\#1530](https://github.com/pypeclub/OpenPype/pull/1530) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Blender can initialize differente user script paths [\\#1528](https://github.com/pypeclub/OpenPype/pull/1528) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Blender and Unreal: Improved Animation Workflow [\\#1514](https://github.com/pypeclub/OpenPype/pull/1514) ([simonebarbieri](https://github.com/simonebarbieri))\n- Hiero: publish color and transformation soft-effects [\\#1511](https://github.com/pypeclub/OpenPype/pull/1511) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n\n**Fixed bugs:**\n\n- OpenPype specific version issues [\\#1583](https://github.com/pypeclub/OpenPype/issues/1583)\n- Ftrack login server can't work without stderr [\\#1576](https://github.com/pypeclub/OpenPype/issues/1576)\n- Mac application launch [\\#1575](https://github.com/pypeclub/OpenPype/issues/1575)\n- Settings are not propagated to Nuke write nodes [\\#1538](https://github.com/pypeclub/OpenPype/issues/1538)\n- Subset names settings not applied for publishing [\\#1537](https://github.com/pypeclub/OpenPype/issues/1537)\n- Nuke: callback at start not setting colorspace [\\#1412](https://github.com/pypeclub/OpenPype/issues/1412)\n- Pype 3: Missing icon for Settings [\\#1272](https://github.com/pypeclub/OpenPype/issues/1272)\n- Blender: cannot initialize Avalon if BLENDER\\_USER\\_SCRIPTS is already used [\\#1050](https://github.com/pypeclub/OpenPype/issues/1050)\n- Ftrack delete action cause circular error [\\#206](https://github.com/pypeclub/OpenPype/issues/206)\n- Build: stop cleaning of pyc files in build directory [\\#1592](https://github.com/pypeclub/OpenPype/pull/1592) ([antirotor](https://github.com/antirotor))\n- Ftrack login server can't work without stderr [\\#1591](https://github.com/pypeclub/OpenPype/pull/1591) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- FFmpeg filters in quote marks [\\#1589](https://github.com/pypeclub/OpenPype/pull/1589) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- OpenPype specific version issues [\\#1584](https://github.com/pypeclub/OpenPype/pull/1584) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Mac application launch [\\#1580](https://github.com/pypeclub/OpenPype/pull/1580) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Ftrack delete action cause circular error [\\#1579](https://github.com/pypeclub/OpenPype/pull/1579) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Hiero: publishing issues  [\\#1578](https://github.com/pypeclub/OpenPype/pull/1578) ([jezscha](https://github.com/jezscha))\n- Nuke: callback at start not setting colorspace [\\#1561](https://github.com/pypeclub/OpenPype/pull/1561) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Bugfix  PS subset and quick review [\\#1541](https://github.com/pypeclub/OpenPype/pull/1541) ([kalisp](https://github.com/kalisp))\n- Settings are not propagated to Nuke write nodes [\\#1540](https://github.com/pypeclub/OpenPype/pull/1540) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- OpenPype: Powershell scripts polishing [\\#1536](https://github.com/pypeclub/OpenPype/pull/1536) ([antirotor](https://github.com/antirotor))\n- Host name collecting fix [\\#1535](https://github.com/pypeclub/OpenPype/pull/1535) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Handle duplicated task names in project manager [\\#1531](https://github.com/pypeclub/OpenPype/pull/1531) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Validate is file attribute in settings schema [\\#1529](https://github.com/pypeclub/OpenPype/pull/1529) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n\n**Merged pull requests:**\n\n- Bump postcss from 8.2.8 to 8.3.0 in /website [\\#1593](https://github.com/pypeclub/OpenPype/pull/1593) ([dependabot[bot]](https://github.com/apps/dependabot))\n- User installation documentation [\\#1532](https://github.com/pypeclub/OpenPype/pull/1532) ([64qam](https://github.com/64qam))\n\n## [CI/3.0.0-rc.5](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.5) (2021-05-19)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.0...CI/3.0.0-rc.5)\n\n**Implemented enhancements:**\n\n- OpenPype: Build - Add progress bars [\\#1524](https://github.com/pypeclub/OpenPype/pull/1524) ([antirotor](https://github.com/antirotor))\n- Default environments per host imlementation [\\#1522](https://github.com/pypeclub/OpenPype/pull/1522) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- OpenPype: use `semver` module for version resolution [\\#1513](https://github.com/pypeclub/OpenPype/pull/1513) ([antirotor](https://github.com/antirotor))\n- Feature Aftereffects setting cleanup documentation [\\#1510](https://github.com/pypeclub/OpenPype/pull/1510) ([kalisp](https://github.com/kalisp))\n- Feature Sync server settings enhancement [\\#1501](https://github.com/pypeclub/OpenPype/pull/1501) ([kalisp](https://github.com/kalisp))\n- Project manager [\\#1396](https://github.com/pypeclub/OpenPype/pull/1396) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n\n**Fixed bugs:**\n\n- Unified schema definition [\\#874](https://github.com/pypeclub/OpenPype/issues/874)\n- Maya: fix look assignment [\\#1526](https://github.com/pypeclub/OpenPype/pull/1526) ([antirotor](https://github.com/antirotor))\n- Bugfix Sync server local site issues [\\#1523](https://github.com/pypeclub/OpenPype/pull/1523) ([kalisp](https://github.com/kalisp))\n- Store as list dictionary check initial value with right type [\\#1520](https://github.com/pypeclub/OpenPype/pull/1520) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Maya: wrong collection of playblasted frames [\\#1515](https://github.com/pypeclub/OpenPype/pull/1515) ([mkolar](https://github.com/mkolar))\n- Convert pyblish logs to string at the moment of logging [\\#1512](https://github.com/pypeclub/OpenPype/pull/1512) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- 3.0 | nuke: fixing start\\_at with option gui [\\#1509](https://github.com/pypeclub/OpenPype/pull/1509) ([jezscha](https://github.com/jezscha))\n- Tests: fix pype -\\> openpype to make tests work again [\\#1508](https://github.com/pypeclub/OpenPype/pull/1508) ([antirotor](https://github.com/antirotor))\n\n**Merged pull requests:**\n\n- OpenPype: disable submodule update with `--no-submodule-update` [\\#1525](https://github.com/pypeclub/OpenPype/pull/1525) ([antirotor](https://github.com/antirotor))\n- Ftrack without autosync in Pype 3 [\\#1519](https://github.com/pypeclub/OpenPype/pull/1519) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Feature Harmony setting cleanup documentation [\\#1506](https://github.com/pypeclub/OpenPype/pull/1506) ([kalisp](https://github.com/kalisp))\n- Sync Server beginning of documentation [\\#1471](https://github.com/pypeclub/OpenPype/pull/1471) ([kalisp](https://github.com/kalisp))\n- Blender: publish layout json [\\#1348](https://github.com/pypeclub/OpenPype/pull/1348) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n\n## [2.18.0](https://github.com/pypeclub/openpype/tree/2.18.0) (2021-05-18)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.4...2.18.0)\n\n**Implemented enhancements:**\n\n- Default environments per host imlementation [\\#1405](https://github.com/pypeclub/OpenPype/issues/1405)\n- Blender: publish layout json [\\#1346](https://github.com/pypeclub/OpenPype/issues/1346)\n- Ftrack without autosync in Pype 3 [\\#1128](https://github.com/pypeclub/OpenPype/issues/1128)\n- Launcher: started action indicator [\\#1102](https://github.com/pypeclub/OpenPype/issues/1102)\n- Launch arguments of applications [\\#1094](https://github.com/pypeclub/OpenPype/issues/1094)\n- Publish: instance info [\\#724](https://github.com/pypeclub/OpenPype/issues/724)\n- Review: ability to control review length [\\#482](https://github.com/pypeclub/OpenPype/issues/482)\n- Colorized recognition of creator result [\\#394](https://github.com/pypeclub/OpenPype/issues/394)\n- event assign user to started task [\\#49](https://github.com/pypeclub/OpenPype/issues/49)\n- rebuild containers from reference in maya [\\#55](https://github.com/pypeclub/OpenPype/issues/55)\n- nuke Load metadata [\\#66](https://github.com/pypeclub/OpenPype/issues/66)\n- Maya: Safer handling of expected render output names [\\#1496](https://github.com/pypeclub/OpenPype/pull/1496) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- TVPaint: Increment workfile version on successfull publish. [\\#1489](https://github.com/pypeclub/OpenPype/pull/1489) ([tokejepsen](https://github.com/tokejepsen))\n- Use SubsetLoader and multiple contexts for delete\\_old\\_versions [\\#1484](https://github.com/pypeclub/OpenPype/pull/1484) ([tokejepsen](https://github.com/tokejepsen))\n- Maya: Use of multiple deadline servers [\\#1483](https://github.com/pypeclub/OpenPype/pull/1483) ([antirotor](https://github.com/antirotor))\n\n**Fixed bugs:**\n\n- Igniter version resolution doesn't consider it's own version [\\#1505](https://github.com/pypeclub/OpenPype/issues/1505)\n- Maya: Safer handling of expected render output names [\\#1159](https://github.com/pypeclub/OpenPype/issues/1159)\n- Harmony: Invalid render output from non-conventionally named instance [\\#871](https://github.com/pypeclub/OpenPype/issues/871)\n- Existing subsets hints in creator [\\#1503](https://github.com/pypeclub/OpenPype/pull/1503) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- nuke: space in node name breaking process [\\#1494](https://github.com/pypeclub/OpenPype/pull/1494) ([jezscha](https://github.com/jezscha))\n-  Maya: wrong collection of playblasted frames [\\#1517](https://github.com/pypeclub/OpenPype/pull/1517) ([mkolar](https://github.com/mkolar))\n- Existing subsets hints in creator [\\#1502](https://github.com/pypeclub/OpenPype/pull/1502) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Use instance frame start instead of timeline. [\\#1486](https://github.com/pypeclub/OpenPype/pull/1486) ([tokejepsen](https://github.com/tokejepsen))\n- Maya: Redshift - set proper start frame on proxy [\\#1480](https://github.com/pypeclub/OpenPype/pull/1480) ([antirotor](https://github.com/antirotor))\n\n**Closed issues:**\n\n- Nuke: wrong \"star at\" value on render load [\\#1352](https://github.com/pypeclub/OpenPype/issues/1352)\n- DV Resolve - loading/updating - image video [\\#915](https://github.com/pypeclub/OpenPype/issues/915)\n\n**Merged pull requests:**\n\n- nuke: fixing start\\_at with option gui [\\#1507](https://github.com/pypeclub/OpenPype/pull/1507) ([jezscha](https://github.com/jezscha))\n\n## [CI/3.0.0-rc.4](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.4) (2021-05-12)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...CI/3.0.0-rc.4)\n\n**Implemented enhancements:**\n\n- Resolve: documentation [\\#1490](https://github.com/pypeclub/OpenPype/issues/1490)\n- Hiero: audio to review [\\#1378](https://github.com/pypeclub/OpenPype/issues/1378)\n- nks color clips after publish [\\#44](https://github.com/pypeclub/OpenPype/issues/44)\n- Store data from modifiable dict as list [\\#1504](https://github.com/pypeclub/OpenPype/pull/1504) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Use SubsetLoader and multiple contexts for delete\\_old\\_versions [\\#1497](https://github.com/pypeclub/OpenPype/pull/1497) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Hiero: publish audio and add to review [\\#1493](https://github.com/pypeclub/OpenPype/pull/1493) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Resolve: documentation [\\#1491](https://github.com/pypeclub/OpenPype/pull/1491) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Change integratenew template profiles setting [\\#1487](https://github.com/pypeclub/OpenPype/pull/1487) ([kalisp](https://github.com/kalisp))\n- Settings tool cleanup [\\#1477](https://github.com/pypeclub/OpenPype/pull/1477) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Sorted Applications and Tools in Custom attribute [\\#1476](https://github.com/pypeclub/OpenPype/pull/1476) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- PS - group all published instances [\\#1416](https://github.com/pypeclub/OpenPype/pull/1416) ([kalisp](https://github.com/kalisp))\n- OpenPype: Support for Docker [\\#1289](https://github.com/pypeclub/OpenPype/pull/1289) ([antirotor](https://github.com/antirotor))\n\n**Fixed bugs:**\n\n- Harmony: palettes publishing [\\#1439](https://github.com/pypeclub/OpenPype/issues/1439)\n- Photoshop: validation for already created images [\\#1435](https://github.com/pypeclub/OpenPype/issues/1435)\n- Nuke Extracts Thumbnail from frame out of shot range [\\#963](https://github.com/pypeclub/OpenPype/issues/963)\n- Instance in same Context repairing [\\#390](https://github.com/pypeclub/OpenPype/issues/390)\n- User Inactivity - Start timers sets wrong time [\\#91](https://github.com/pypeclub/OpenPype/issues/91)\n- Use instance frame start instead of timeline [\\#1499](https://github.com/pypeclub/OpenPype/pull/1499) ([mkolar](https://github.com/mkolar))\n- Various smaller fixes [\\#1498](https://github.com/pypeclub/OpenPype/pull/1498) ([mkolar](https://github.com/mkolar))\n- nuke: space in node name breaking process [\\#1495](https://github.com/pypeclub/OpenPype/pull/1495) ([jezscha](https://github.com/jezscha))\n- Codec determination in extract burnin [\\#1492](https://github.com/pypeclub/OpenPype/pull/1492) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Undefined constant in subprocess module [\\#1485](https://github.com/pypeclub/OpenPype/pull/1485) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- List entity catch add/remove item changes properly [\\#1482](https://github.com/pypeclub/OpenPype/pull/1482) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Resolve: additional fixes of publishing workflow [\\#1481](https://github.com/pypeclub/OpenPype/pull/1481) ([jezscha](https://github.com/jezscha))\n- Photoshop: validation for already created images [\\#1436](https://github.com/pypeclub/OpenPype/pull/1436) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n\n**Merged pull requests:**\n\n- Maya: Support for looks on VRay Proxies [\\#1443](https://github.com/pypeclub/OpenPype/pull/1443) ([antirotor](https://github.com/antirotor))\n\n## [2.17.3](https://github.com/pypeclub/openpype/tree/2.17.3) (2021-05-06)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.3...2.17.3)\n\n**Fixed bugs:**\n\n- Nuke: workfile version synced to db version always  [\\#1479](https://github.com/pypeclub/OpenPype/pull/1479) ([jezscha](https://github.com/jezscha))\n\n## [CI/3.0.0-rc.3](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.3) (2021-05-05)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.2...CI/3.0.0-rc.3)\n\n**Implemented enhancements:**\n\n- Path entity with placeholder [\\#1473](https://github.com/pypeclub/OpenPype/pull/1473) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Burnin custom font filepath [\\#1472](https://github.com/pypeclub/OpenPype/pull/1472) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Poetry: Move to OpenPype [\\#1449](https://github.com/pypeclub/OpenPype/pull/1449) ([antirotor](https://github.com/antirotor))\n\n**Fixed bugs:**\n\n- Mac SSL path needs to be relative to pype\\_root [\\#1469](https://github.com/pypeclub/OpenPype/issues/1469)\n- Resolve: fix loading clips to timeline [\\#1421](https://github.com/pypeclub/OpenPype/issues/1421)\n- Wrong handling of slashes when loading on mac [\\#1411](https://github.com/pypeclub/OpenPype/issues/1411)\n- Nuke openpype3 [\\#1342](https://github.com/pypeclub/OpenPype/issues/1342)\n- Houdini launcher [\\#1171](https://github.com/pypeclub/OpenPype/issues/1171)\n- Fix SyncServer get\\_enabled\\_projects should handle global state [\\#1475](https://github.com/pypeclub/OpenPype/pull/1475) ([kalisp](https://github.com/kalisp))\n- Igniter buttons enable/disable fix [\\#1474](https://github.com/pypeclub/OpenPype/pull/1474) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Mac SSL path needs to be relative to pype\\_root [\\#1470](https://github.com/pypeclub/OpenPype/pull/1470) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Resolve: 17 compatibility issues and load image sequences [\\#1422](https://github.com/pypeclub/OpenPype/pull/1422) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n\n## [CI/3.0.0-rc.2](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.2) (2021-05-04)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.2...CI/3.0.0-rc.2)\n\n**Implemented enhancements:**\n\n- Extract burnins with sequences [\\#1467](https://github.com/pypeclub/OpenPype/pull/1467) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Extract burnins with color setting [\\#1466](https://github.com/pypeclub/OpenPype/pull/1466) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n\n**Fixed bugs:**\n\n- Fix groups check in Python 2 [\\#1468](https://github.com/pypeclub/OpenPype/pull/1468) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n\n## [2.17.2](https://github.com/pypeclub/openpype/tree/2.17.2) (2021-05-04)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.1...2.17.2)\n\n**Implemented enhancements:**\n\n- Forward/Backward compatible apps and tools with OpenPype 3 [\\#1463](https://github.com/pypeclub/OpenPype/pull/1463) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n\n## [CI/3.0.0-rc.1](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.1) (2021-05-04)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.1...CI/3.0.0-rc.1)\n\n**Implemented enhancements:**\n\n- Only show studio settings to admins [\\#1406](https://github.com/pypeclub/OpenPype/issues/1406)\n- Ftrack specific settings save warning messages [\\#1458](https://github.com/pypeclub/OpenPype/pull/1458) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Faster settings actions [\\#1446](https://github.com/pypeclub/OpenPype/pull/1446) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Feature/sync server priority [\\#1444](https://github.com/pypeclub/OpenPype/pull/1444) ([kalisp](https://github.com/kalisp))\n- Faster settings UI loading [\\#1442](https://github.com/pypeclub/OpenPype/pull/1442) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Igniter re-write [\\#1441](https://github.com/pypeclub/OpenPype/pull/1441) ([mkolar](https://github.com/mkolar))\n- Wrap openpype build into installers [\\#1419](https://github.com/pypeclub/OpenPype/pull/1419) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Extract review first documentation [\\#1404](https://github.com/pypeclub/OpenPype/pull/1404) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Blender PySide2 install guide [\\#1403](https://github.com/pypeclub/OpenPype/pull/1403) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Nuke: deadline submission with gpu [\\#1394](https://github.com/pypeclub/OpenPype/pull/1394) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Igniter: Reverse item filter for OpenPype version [\\#1349](https://github.com/pypeclub/OpenPype/pull/1349) ([antirotor](https://github.com/antirotor))\n\n**Fixed bugs:**\n\n- OpenPype Mongo URL definition [\\#1450](https://github.com/pypeclub/OpenPype/issues/1450)\n- Various typos and smaller fixes [\\#1464](https://github.com/pypeclub/OpenPype/pull/1464) ([mkolar](https://github.com/mkolar))\n- Validation of dynamic items in settings [\\#1462](https://github.com/pypeclub/OpenPype/pull/1462) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- List can handle new items correctly [\\#1459](https://github.com/pypeclub/OpenPype/pull/1459) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Settings actions process fix [\\#1457](https://github.com/pypeclub/OpenPype/pull/1457) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Add to overrides actions fix [\\#1456](https://github.com/pypeclub/OpenPype/pull/1456) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- OpenPype Mongo URL definition [\\#1455](https://github.com/pypeclub/OpenPype/pull/1455) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Global settings save/load out of system settings [\\#1447](https://github.com/pypeclub/OpenPype/pull/1447) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Keep metadata on remove overrides [\\#1445](https://github.com/pypeclub/OpenPype/pull/1445) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Nuke: fixing undo for loaded mov and sequence [\\#1432](https://github.com/pypeclub/OpenPype/pull/1432) ([jezscha](https://github.com/jezscha))\n- ExtractReview skip empty strings from settings [\\#1431](https://github.com/pypeclub/OpenPype/pull/1431) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Bugfix Sync server tweaks [\\#1430](https://github.com/pypeclub/OpenPype/pull/1430) ([kalisp](https://github.com/kalisp))\n- Hiero: missing thumbnail in review [\\#1429](https://github.com/pypeclub/OpenPype/pull/1429) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Bugfix Maya in deadline for OpenPype [\\#1428](https://github.com/pypeclub/OpenPype/pull/1428) ([kalisp](https://github.com/kalisp))\n- AE - validation for duration was 1 frame shorter [\\#1427](https://github.com/pypeclub/OpenPype/pull/1427) ([kalisp](https://github.com/kalisp))\n- Houdini menu filename [\\#1418](https://github.com/pypeclub/OpenPype/pull/1418) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Fix Avalon plugins attribute overrides [\\#1413](https://github.com/pypeclub/OpenPype/pull/1413) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Nuke: submit to Deadline fails [\\#1409](https://github.com/pypeclub/OpenPype/pull/1409) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Validate MongoDB Url on start [\\#1407](https://github.com/pypeclub/OpenPype/pull/1407) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Nuke: fix set colorspace with new settings [\\#1386](https://github.com/pypeclub/OpenPype/pull/1386) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- MacOs build and install issues [\\#1380](https://github.com/pypeclub/OpenPype/pull/1380) ([mkolar](https://github.com/mkolar))\n\n**Closed issues:**\n\n- test [\\#1452](https://github.com/pypeclub/OpenPype/issues/1452)\n\n**Merged pull requests:**\n\n- TVPaint frame range definition [\\#1425](https://github.com/pypeclub/OpenPype/pull/1425) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Only show studio settings to admins [\\#1420](https://github.com/pypeclub/OpenPype/pull/1420) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- TVPaint documentation [\\#1305](https://github.com/pypeclub/OpenPype/pull/1305) ([64qam](https://github.com/64qam))\n\n## [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) (2021-04-30)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.0...2.17.1)\n\n**Enhancements:**\n\n- Nuke: deadline submission with gpu [\\#1414](https://github.com/pypeclub/OpenPype/pull/1414)\n- TVPaint frame range definition [\\#1424](https://github.com/pypeclub/OpenPype/pull/1424)\n- PS - group all published instances [\\#1415](https://github.com/pypeclub/OpenPype/pull/1415)\n- Add task name to context pop up. [\\#1383](https://github.com/pypeclub/OpenPype/pull/1383)\n- Enhance review letterbox feature. [\\#1371](https://github.com/pypeclub/OpenPype/pull/1371)\n\n**Fixed bugs:**\n\n- Houdini menu filename [\\#1417](https://github.com/pypeclub/OpenPype/pull/1417)\n- AE - validation for duration was 1 frame shorter [\\#1426](https://github.com/pypeclub/OpenPype/pull/1426)\n\n**Merged pull requests:**\n\n- Maya: Vray - problem getting all file nodes for look publishing [\\#1399](https://github.com/pypeclub/OpenPype/pull/1399)\n- Maya: Support for Redshift proxies [\\#1360](https://github.com/pypeclub/OpenPype/pull/1360)\n\n## [2.17.0](https://github.com/pypeclub/openpype/tree/2.17.0) (2021-04-20)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-beta.2...2.17.0)\n\n**Enhancements:**\n\n- Forward compatible ftrack group [\\#1243](https://github.com/pypeclub/OpenPype/pull/1243)\n- Settings in mongo as dict [\\#1221](https://github.com/pypeclub/OpenPype/pull/1221)\n- Maya: Make tx option configurable with presets [\\#1328](https://github.com/pypeclub/OpenPype/pull/1328)\n- TVPaint asset name validation [\\#1302](https://github.com/pypeclub/OpenPype/pull/1302)\n- TV Paint: Set initial project settings. [\\#1299](https://github.com/pypeclub/OpenPype/pull/1299)\n- TV Paint: Validate mark in and out. [\\#1298](https://github.com/pypeclub/OpenPype/pull/1298)\n- Validate project settings [\\#1297](https://github.com/pypeclub/OpenPype/pull/1297)\n- After Effects: added SubsetManager [\\#1234](https://github.com/pypeclub/OpenPype/pull/1234)\n- Show error message in pyblish UI [\\#1206](https://github.com/pypeclub/OpenPype/pull/1206)\n\n**Fixed bugs:**\n\n- Hiero: fixing source frame from correct object [\\#1362](https://github.com/pypeclub/OpenPype/pull/1362)\n- Nuke: fix colourspace, prerenders and nuke panes opening [\\#1308](https://github.com/pypeclub/OpenPype/pull/1308)\n- AE remove orphaned instance from workfile - fix self.stub [\\#1282](https://github.com/pypeclub/OpenPype/pull/1282)\n- Nuke: deadline submission with search replaced env values from preset [\\#1194](https://github.com/pypeclub/OpenPype/pull/1194)\n- Ftrack custom attributes in bulks [\\#1312](https://github.com/pypeclub/OpenPype/pull/1312)\n- Ftrack optional pypclub role [\\#1303](https://github.com/pypeclub/OpenPype/pull/1303)\n- After Effects: remove orphaned instances [\\#1275](https://github.com/pypeclub/OpenPype/pull/1275)\n- Avalon schema names [\\#1242](https://github.com/pypeclub/OpenPype/pull/1242)\n- Handle duplication of Task name [\\#1226](https://github.com/pypeclub/OpenPype/pull/1226)\n- Modified path of plugin loads for Harmony and TVPaint [\\#1217](https://github.com/pypeclub/OpenPype/pull/1217)\n- Regex checks in profiles filtering [\\#1214](https://github.com/pypeclub/OpenPype/pull/1214)\n- Bulk mov strict task [\\#1204](https://github.com/pypeclub/OpenPype/pull/1204)\n- Update custom ftrack session attributes [\\#1202](https://github.com/pypeclub/OpenPype/pull/1202)\n- Nuke: write node colorspace ignore `default\\(\\)` label [\\#1199](https://github.com/pypeclub/OpenPype/pull/1199)\n- Nuke: reverse search to make it more versatile [\\#1178](https://github.com/pypeclub/OpenPype/pull/1178)\n\n\n\n## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.16.0)\n\n**Enhancements:**\n\n- Nuke: deadline submit limit group filter [\\#1167](https://github.com/pypeclub/pype/pull/1167)\n- Maya: support for Deadline Group and Limit Groups - backport 2.x [\\#1156](https://github.com/pypeclub/pype/pull/1156)\n- Maya: fixes for Redshift support [\\#1152](https://github.com/pypeclub/pype/pull/1152)\n- Nuke: adding preset for a Read node name to all img and mov Loaders [\\#1146](https://github.com/pypeclub/pype/pull/1146)\n- nuke deadline submit with environ var from presets overrides [\\#1142](https://github.com/pypeclub/pype/pull/1142)\n- Change timers after task change [\\#1138](https://github.com/pypeclub/pype/pull/1138)\n- Nuke: shortcuts for Pype menu [\\#1127](https://github.com/pypeclub/pype/pull/1127)\n- Nuke: workfile template [\\#1124](https://github.com/pypeclub/pype/pull/1124)\n- Sites local settings by site name [\\#1117](https://github.com/pypeclub/pype/pull/1117)\n- Reset loader's asset selection on context change [\\#1106](https://github.com/pypeclub/pype/pull/1106)\n- Bulk mov render publishing [\\#1101](https://github.com/pypeclub/pype/pull/1101)\n- Photoshop: mark publishable instances [\\#1093](https://github.com/pypeclub/pype/pull/1093)\n- Added ability to define BG color for extract review [\\#1088](https://github.com/pypeclub/pype/pull/1088)\n- TVPaint extractor enhancement [\\#1080](https://github.com/pypeclub/pype/pull/1080)\n- Photoshop: added support for .psb in workfiles [\\#1078](https://github.com/pypeclub/pype/pull/1078)\n- Optionally add task to subset name [\\#1072](https://github.com/pypeclub/pype/pull/1072)\n- Only extend clip range when collecting. [\\#1008](https://github.com/pypeclub/pype/pull/1008)\n- Collect audio for farm reviews. [\\#1073](https://github.com/pypeclub/pype/pull/1073)\n\n\n**Fixed bugs:**\n\n- Fix path spaces in jpeg extractor [\\#1174](https://github.com/pypeclub/pype/pull/1174)\n- Maya: Bugfix: superclass for CreateCameraRig [\\#1166](https://github.com/pypeclub/pype/pull/1166)\n- Maya: Submit to Deadline - fix typo in condition [\\#1163](https://github.com/pypeclub/pype/pull/1163)\n- Avoid dot in repre extension [\\#1125](https://github.com/pypeclub/pype/pull/1125)\n- Fix versions variable usage in standalone publisher [\\#1090](https://github.com/pypeclub/pype/pull/1090)\n- Collect instance data fix subset query [\\#1082](https://github.com/pypeclub/pype/pull/1082)\n- Fix getting the camera name. [\\#1067](https://github.com/pypeclub/pype/pull/1067)\n- Nuke: Ensure \"NUKE\\_TEMP\\_DIR\" is not part of the Deadline job environment. [\\#1064](https://github.com/pypeclub/pype/pull/1064)\n\n## [2.15.3](https://github.com/pypeclub/pype/tree/2.15.3) (2021-02-26)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.2...2.15.3)\n\n**Enhancements:**\n\n- Maya: speedup renderable camera collection [\\#1053](https://github.com/pypeclub/pype/pull/1053)\n- Harmony - add regex search to filter allowed task names for collectin… [\\#1047](https://github.com/pypeclub/pype/pull/1047)\n\n**Fixed bugs:**\n\n- Ftrack integrate hierarchy fix [\\#1085](https://github.com/pypeclub/pype/pull/1085)\n- Explicit subset filter in anatomy instance data [\\#1059](https://github.com/pypeclub/pype/pull/1059)\n- TVPaint frame offset [\\#1057](https://github.com/pypeclub/pype/pull/1057)\n- Auto fix unicode strings [\\#1046](https://github.com/pypeclub/pype/pull/1046)\n\n## [2.15.2](https://github.com/pypeclub/pype/tree/2.15.2) (2021-02-19)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.1...2.15.2)\n\n**Enhancements:**\n\n- Maya: Vray scene publishing [\\#1013](https://github.com/pypeclub/pype/pull/1013)\n\n**Fixed bugs:**\n\n- Fix entity move under project [\\#1040](https://github.com/pypeclub/pype/pull/1040)\n- smaller nuke fixes from production [\\#1036](https://github.com/pypeclub/pype/pull/1036)\n- TVPaint thumbnail extract fix [\\#1031](https://github.com/pypeclub/pype/pull/1031)\n\n## [2.15.1](https://github.com/pypeclub/pype/tree/2.15.1) (2021-02-12)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.0...2.15.1)\n\n**Enhancements:**\n\n- Delete version as loader action [\\#1011](https://github.com/pypeclub/pype/pull/1011)\n- Delete old versions [\\#445](https://github.com/pypeclub/pype/pull/445)\n\n**Fixed bugs:**\n\n- PS - remove obsolete functions from pywin32 [\\#1006](https://github.com/pypeclub/pype/pull/1006)\n- Clone description of review session objects. [\\#922](https://github.com/pypeclub/pype/pull/922)\n\n## [2.15.0](https://github.com/pypeclub/pype/tree/2.15.0) (2021-02-09)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.6...2.15.0)\n\n**Enhancements:**\n\n- Resolve - loading and updating clips [\\#932](https://github.com/pypeclub/pype/pull/932)\n- Release/2.15.0 [\\#926](https://github.com/pypeclub/pype/pull/926)\n- Photoshop: add option for template.psd and prelaunch hook  [\\#894](https://github.com/pypeclub/pype/pull/894)\n- Nuke: deadline presets [\\#993](https://github.com/pypeclub/pype/pull/993)\n- Maya: Alembic only set attributes that exists. [\\#986](https://github.com/pypeclub/pype/pull/986)\n- Harmony: render local and handle fixes [\\#981](https://github.com/pypeclub/pype/pull/981)\n- PSD Bulk export of ANIM group [\\#965](https://github.com/pypeclub/pype/pull/965)\n- AE - added prelaunch hook for opening last or workfile from template [\\#944](https://github.com/pypeclub/pype/pull/944)\n- PS - safer handling of loading of workfile [\\#941](https://github.com/pypeclub/pype/pull/941)\n- Maya: Handling Arnold referenced AOVs [\\#938](https://github.com/pypeclub/pype/pull/938)\n- TVPaint: switch layer IDs for layer names during identification [\\#903](https://github.com/pypeclub/pype/pull/903)\n- TVPaint audio/sound loader [\\#893](https://github.com/pypeclub/pype/pull/893)\n- Clone review session with children. [\\#891](https://github.com/pypeclub/pype/pull/891)\n- Simple compositing data packager for freelancers [\\#884](https://github.com/pypeclub/pype/pull/884)\n- Harmony deadline submission [\\#881](https://github.com/pypeclub/pype/pull/881)\n- Maya: Optionally hide image planes from reviews. [\\#840](https://github.com/pypeclub/pype/pull/840)\n- Maya: handle referenced AOVs for Vray [\\#824](https://github.com/pypeclub/pype/pull/824)\n- DWAA/DWAB support on windows [\\#795](https://github.com/pypeclub/pype/pull/795)\n- Unreal: animation, layout and setdress updates [\\#695](https://github.com/pypeclub/pype/pull/695)\n\n**Fixed bugs:**\n\n- Maya: Looks - disable hardlinks [\\#995](https://github.com/pypeclub/pype/pull/995)\n- Fix Ftrack custom attribute update [\\#982](https://github.com/pypeclub/pype/pull/982)\n- Prores ks in burnin script [\\#960](https://github.com/pypeclub/pype/pull/960)\n- terminal.py crash on import [\\#839](https://github.com/pypeclub/pype/pull/839)\n- Extract review handle bizarre pixel aspect ratio [\\#990](https://github.com/pypeclub/pype/pull/990)\n- Nuke: add nuke related env var to sumbission  [\\#988](https://github.com/pypeclub/pype/pull/988)\n- Nuke: missing preset's variable  [\\#984](https://github.com/pypeclub/pype/pull/984)\n- Get creator by name fix [\\#979](https://github.com/pypeclub/pype/pull/979)\n- Fix update of project's tasks on Ftrack sync [\\#972](https://github.com/pypeclub/pype/pull/972)\n- nuke: wrong frame offset in mov loader  [\\#971](https://github.com/pypeclub/pype/pull/971)\n- Create project structure action fix multiroot [\\#967](https://github.com/pypeclub/pype/pull/967)\n- PS: remove pywin installation from hook [\\#964](https://github.com/pypeclub/pype/pull/964)\n- Prores ks in burnin script [\\#959](https://github.com/pypeclub/pype/pull/959)\n- Subset family is now stored in subset document [\\#956](https://github.com/pypeclub/pype/pull/956)\n- DJV new version arguments [\\#954](https://github.com/pypeclub/pype/pull/954)\n- TV Paint: Fix single frame Sequence [\\#953](https://github.com/pypeclub/pype/pull/953)\n- nuke: missing `file` knob update  [\\#933](https://github.com/pypeclub/pype/pull/933)\n- Photoshop: Create from single layer was failing [\\#920](https://github.com/pypeclub/pype/pull/920)\n- Nuke: baking mov with correct colorspace inherited from write  [\\#909](https://github.com/pypeclub/pype/pull/909)\n- Launcher fix actions discover [\\#896](https://github.com/pypeclub/pype/pull/896)\n- Get the correct file path for the updated mov. [\\#889](https://github.com/pypeclub/pype/pull/889)\n- Maya: Deadline submitter - shared data access violation [\\#831](https://github.com/pypeclub/pype/pull/831)\n- Maya: Take into account vray master AOV switch [\\#822](https://github.com/pypeclub/pype/pull/822)\n\n**Merged pull requests:**\n\n- Refactor blender to 3.0 format [\\#934](https://github.com/pypeclub/pype/pull/934)\n\n## [2.14.6](https://github.com/pypeclub/pype/tree/2.14.6) (2021-01-15)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.5...2.14.6)\n\n**Fixed bugs:**\n\n- Nuke: improving of hashing path  [\\#885](https://github.com/pypeclub/pype/pull/885)\n\n**Merged pull requests:**\n\n- Hiero: cut videos with correct secons  [\\#892](https://github.com/pypeclub/pype/pull/892)\n- Faster sync to avalon preparation [\\#869](https://github.com/pypeclub/pype/pull/869)\n\n## [2.14.5](https://github.com/pypeclub/pype/tree/2.14.5) (2021-01-06)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.4...2.14.5)\n\n**Merged pull requests:**\n\n- Pype logger refactor [\\#866](https://github.com/pypeclub/pype/pull/866)\n\n## [2.14.4](https://github.com/pypeclub/pype/tree/2.14.4) (2020-12-18)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.3...2.14.4)\n\n**Merged pull requests:**\n\n- Fix - AE - added explicit cast to int [\\#837](https://github.com/pypeclub/pype/pull/837)\n\n## [2.14.3](https://github.com/pypeclub/pype/tree/2.14.3) (2020-12-16)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.2...2.14.3)\n\n**Fixed bugs:**\n\n- TVPaint repair invalid metadata [\\#809](https://github.com/pypeclub/pype/pull/809)\n- Feature/push hier value to nonhier action [\\#807](https://github.com/pypeclub/pype/pull/807)\n- Harmony: fix palette and image sequence loader [\\#806](https://github.com/pypeclub/pype/pull/806)\n\n**Merged pull requests:**\n\n- respecting space in path [\\#823](https://github.com/pypeclub/pype/pull/823)\n\n## [2.14.2](https://github.com/pypeclub/pype/tree/2.14.2) (2020-12-04)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.1...2.14.2)\n\n**Enhancements:**\n\n- Collapsible wrapper in settings [\\#767](https://github.com/pypeclub/pype/pull/767)\n\n**Fixed bugs:**\n\n- Harmony: template extraction and palettes thumbnails on mac [\\#768](https://github.com/pypeclub/pype/pull/768)\n- TVPaint store context to workfile metadata \\(764\\) [\\#766](https://github.com/pypeclub/pype/pull/766)\n- Extract review audio cut fix [\\#763](https://github.com/pypeclub/pype/pull/763)\n\n**Merged pull requests:**\n\n- AE: fix publish after background load [\\#781](https://github.com/pypeclub/pype/pull/781)\n- TVPaint store members key [\\#769](https://github.com/pypeclub/pype/pull/769)\n\n## [2.14.1](https://github.com/pypeclub/pype/tree/2.14.1) (2020-11-27)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.0...2.14.1)\n\n**Enhancements:**\n\n- Settings required keys in modifiable dict [\\#770](https://github.com/pypeclub/pype/pull/770)\n- Extract review may not add audio to output [\\#761](https://github.com/pypeclub/pype/pull/761)\n\n**Fixed bugs:**\n\n- After Effects: frame range, file format and render source scene fixes [\\#760](https://github.com/pypeclub/pype/pull/760)\n- Hiero: trimming review with clip event number  [\\#754](https://github.com/pypeclub/pype/pull/754)\n- TVPaint: fix updating of loaded subsets [\\#752](https://github.com/pypeclub/pype/pull/752)\n- Maya: Vray handling of default aov [\\#748](https://github.com/pypeclub/pype/pull/748)\n- Maya: multiple renderable cameras in layer didn't work [\\#744](https://github.com/pypeclub/pype/pull/744)\n- Ftrack integrate custom attributes fix [\\#742](https://github.com/pypeclub/pype/pull/742)\n\n## [2.14.0](https://github.com/pypeclub/pype/tree/2.14.0) (2020-11-23)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.7...2.14.0)\n\n**Enhancements:**\n\n- Render publish plugins abstraction [\\#687](https://github.com/pypeclub/pype/pull/687)\n- Shot asset build trigger status [\\#736](https://github.com/pypeclub/pype/pull/736)\n- Maya: add camera rig publishing option [\\#721](https://github.com/pypeclub/pype/pull/721)\n- Sort instances by label in pyblish gui  [\\#719](https://github.com/pypeclub/pype/pull/719)\n- Synchronize ftrack hierarchical and shot attributes [\\#716](https://github.com/pypeclub/pype/pull/716)\n- 686 standalonepublisher editorial from image sequences [\\#699](https://github.com/pypeclub/pype/pull/699)\n- Ask user to select non-default camera from scene or create a new. [\\#678](https://github.com/pypeclub/pype/pull/678)\n- TVPaint: image loader with options [\\#675](https://github.com/pypeclub/pype/pull/675)\n- Maya: Camera name can be added to burnins. [\\#674](https://github.com/pypeclub/pype/pull/674)\n- After Effects: base integration with loaders [\\#667](https://github.com/pypeclub/pype/pull/667)\n- Harmony: Javascript refactoring and overall stability improvements [\\#666](https://github.com/pypeclub/pype/pull/666)\n\n**Fixed bugs:**\n\n- Bugfix Hiero Review / Plate representation publish [\\#743](https://github.com/pypeclub/pype/pull/743)\n- Asset fetch second fix [\\#726](https://github.com/pypeclub/pype/pull/726)\n- TVPaint extract review fix [\\#740](https://github.com/pypeclub/pype/pull/740)\n- After Effects: Review were not being sent to ftrack [\\#738](https://github.com/pypeclub/pype/pull/738)\n- Maya: vray proxy was not loading [\\#722](https://github.com/pypeclub/pype/pull/722)\n- Maya: Vray expected file fixes [\\#682](https://github.com/pypeclub/pype/pull/682)\n- Missing audio on farm submission. [\\#639](https://github.com/pypeclub/pype/pull/639)\n\n**Deprecated:**\n\n- Removed artist view from pyblish gui [\\#717](https://github.com/pypeclub/pype/pull/717)\n- Maya: disable legacy override check for cameras [\\#715](https://github.com/pypeclub/pype/pull/715)\n\n**Merged pull requests:**\n\n- Application manager [\\#728](https://github.com/pypeclub/pype/pull/728)\n- Feature \\#664 3.0 lib refactor [\\#706](https://github.com/pypeclub/pype/pull/706)\n- Lib from illicit part 2 [\\#700](https://github.com/pypeclub/pype/pull/700)\n- 3.0 lib refactor - path tools [\\#697](https://github.com/pypeclub/pype/pull/697)\n\n## [2.13.7](https://github.com/pypeclub/pype/tree/2.13.7) (2020-11-19)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.6...2.13.7)\n\n**Fixed bugs:**\n\n- Standalone Publisher: getting fps from context instead of nonexistent entity  [\\#729](https://github.com/pypeclub/pype/pull/729)\n\n## [2.13.6](https://github.com/pypeclub/pype/tree/2.13.6) (2020-11-15)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.5...2.13.6)\n\n**Fixed bugs:**\n\n- Maya workfile version wasn't syncing with renders properly [\\#711](https://github.com/pypeclub/pype/pull/711)\n- Maya: Fix for publishing multiple cameras with review from the same scene [\\#710](https://github.com/pypeclub/pype/pull/710)\n\n## [2.13.5](https://github.com/pypeclub/pype/tree/2.13.5) (2020-11-12)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.4...2.13.5)\n\n**Enhancements:**\n\n- 3.0 lib refactor [\\#664](https://github.com/pypeclub/pype/issues/664)\n\n**Fixed bugs:**\n\n- Wrong thumbnail file was picked when publishing sequence in standalone publisher [\\#703](https://github.com/pypeclub/pype/pull/703)\n- Fix: Burnin data pass and FFmpeg tool check [\\#701](https://github.com/pypeclub/pype/pull/701)\n\n## [2.13.4](https://github.com/pypeclub/pype/tree/2.13.4) (2020-11-09)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.3...2.13.4)\n\n**Enhancements:**\n\n- AfterEffects integration with Websocket [\\#663](https://github.com/pypeclub/pype/issues/663)\n\n**Fixed bugs:**\n\n- Photoshop uhiding hidden layers [\\#688](https://github.com/pypeclub/pype/issues/688)\n- \\#688 - Fix publishing hidden layers [\\#692](https://github.com/pypeclub/pype/pull/692)\n\n**Closed issues:**\n\n- Nuke Favorite directories \"shot dir\" \"project dir\" - not working [\\#684](https://github.com/pypeclub/pype/issues/684)\n\n**Merged pull requests:**\n\n- Nuke Favorite directories \"shot dir\" \"project dir\" - not working \\#684 [\\#685](https://github.com/pypeclub/pype/pull/685)\n\n## [2.13.3](https://github.com/pypeclub/pype/tree/2.13.3) (2020-11-03)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.2...2.13.3)\n\n**Enhancements:**\n\n- TV paint base integration [\\#612](https://github.com/pypeclub/pype/issues/612)\n\n**Fixed bugs:**\n\n- Fix ffmpeg executable path with spaces [\\#680](https://github.com/pypeclub/pype/pull/680)\n- Hotfix: Added default version number [\\#679](https://github.com/pypeclub/pype/pull/679)\n\n## [2.13.2](https://github.com/pypeclub/pype/tree/2.13.2) (2020-10-28)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.1...2.13.2)\n\n**Fixed bugs:**\n\n- Nuke: wrong conditions when fixing legacy write nodes [\\#665](https://github.com/pypeclub/pype/pull/665)\n\n## [2.13.1](https://github.com/pypeclub/pype/tree/2.13.1) (2020-10-23)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.0...2.13.1)\n\n**Enhancements:**\n\n- move maya look assigner to pype menu [\\#292](https://github.com/pypeclub/pype/issues/292)\n\n**Fixed bugs:**\n\n- Layer name is not propagating to metadata in Photoshop [\\#654](https://github.com/pypeclub/pype/issues/654)\n- Loader in Photoshop fails with \"can't set attribute\" [\\#650](https://github.com/pypeclub/pype/issues/650)\n- Nuke Load mp4 wrong frame range [\\#661](https://github.com/pypeclub/pype/issues/661)\n- Hiero: Review video file adding one frame to the end [\\#659](https://github.com/pypeclub/pype/issues/659)\n\n## [2.13.0](https://github.com/pypeclub/pype/tree/2.13.0) (2020-10-18)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.5...2.13.0)\n\n**Enhancements:**\n\n- Deadline Output Folder [\\#636](https://github.com/pypeclub/pype/issues/636)\n- Nuke Camera Loader [\\#565](https://github.com/pypeclub/pype/issues/565)\n- Deadline publish job shows publishing output folder [\\#649](https://github.com/pypeclub/pype/pull/649)\n- Get latest version in lib [\\#642](https://github.com/pypeclub/pype/pull/642)\n- Improved publishing of multiple representation from SP [\\#638](https://github.com/pypeclub/pype/pull/638)\n- Launch TvPaint shot work file from within Ftrack [\\#631](https://github.com/pypeclub/pype/pull/631)\n- Add mp4 support for RV action. [\\#628](https://github.com/pypeclub/pype/pull/628)\n- Maya: allow renders to have version synced with workfile [\\#618](https://github.com/pypeclub/pype/pull/618)\n- Renaming nukestudio host folder to hiero [\\#617](https://github.com/pypeclub/pype/pull/617)\n- Harmony: More efficient publishing [\\#615](https://github.com/pypeclub/pype/pull/615)\n- Ftrack server action improvement [\\#608](https://github.com/pypeclub/pype/pull/608)\n- Deadline user defaults to pype username if present [\\#607](https://github.com/pypeclub/pype/pull/607)\n- Standalone publisher now has icon [\\#606](https://github.com/pypeclub/pype/pull/606)\n- Nuke render write targeting knob improvement [\\#603](https://github.com/pypeclub/pype/pull/603)\n- Animated pyblish gui [\\#602](https://github.com/pypeclub/pype/pull/602)\n- Maya: Deadline - make use of asset dependencies optional [\\#591](https://github.com/pypeclub/pype/pull/591)\n- Nuke: Publishing, loading and updating alembic cameras [\\#575](https://github.com/pypeclub/pype/pull/575)\n- Maya: add look assigner to pype menu even if scriptsmenu is not available [\\#573](https://github.com/pypeclub/pype/pull/573)\n- Store task types in the database [\\#572](https://github.com/pypeclub/pype/pull/572)\n- Maya: Tiled EXRs to scanline EXRs render option [\\#512](https://github.com/pypeclub/pype/pull/512)\n- Fusion basic integration [\\#452](https://github.com/pypeclub/pype/pull/452)\n\n**Fixed bugs:**\n\n- Burnin script did not propagate ffmpeg output [\\#640](https://github.com/pypeclub/pype/issues/640)\n- Pyblish-pype spacer in terminal wasn't transparent [\\#646](https://github.com/pypeclub/pype/pull/646)\n- Lib subprocess without logger [\\#645](https://github.com/pypeclub/pype/pull/645)\n- Nuke: prevent crash if we only have single frame in sequence [\\#644](https://github.com/pypeclub/pype/pull/644)\n- Burnin script logs better output [\\#641](https://github.com/pypeclub/pype/pull/641)\n- Missing audio on farm submission. [\\#639](https://github.com/pypeclub/pype/pull/639)\n- review from imagesequence error [\\#633](https://github.com/pypeclub/pype/pull/633)\n- Hiero: wrong order of fps clip instance data collecting  [\\#627](https://github.com/pypeclub/pype/pull/627)\n- Add source for review instances. [\\#625](https://github.com/pypeclub/pype/pull/625)\n- Task processing in event sync [\\#623](https://github.com/pypeclub/pype/pull/623)\n- sync to avalon doesn t remove renamed task [\\#619](https://github.com/pypeclub/pype/pull/619)\n- Intent publish setting wasn't working with default value [\\#562](https://github.com/pypeclub/pype/pull/562)\n- Maya: Updating a look where the shader name changed, leaves the geo without a shader [\\#514](https://github.com/pypeclub/pype/pull/514)\n\n**Merged pull requests:**\n\n- Avalon module without Qt [\\#581](https://github.com/pypeclub/pype/pull/581)\n- Ftrack module without Qt [\\#577](https://github.com/pypeclub/pype/pull/577)\n\n## [2.12.5](https://github.com/pypeclub/pype/tree/2.12.5) (2020-10-14)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.4...2.12.5)\n\n**Enhancements:**\n\n- Launch TvPaint shot work file from within Ftrack [\\#629](https://github.com/pypeclub/pype/issues/629)\n\n**Merged pull requests:**\n\n- Harmony: Disable application launch logic [\\#637](https://github.com/pypeclub/pype/pull/637)\n\n## [2.12.4](https://github.com/pypeclub/pype/tree/2.12.4) (2020-10-08)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.3...2.12.4)\n\n**Enhancements:**\n\n- convert nukestudio to hiero host [\\#616](https://github.com/pypeclub/pype/issues/616)\n- Fusion basic integration  [\\#451](https://github.com/pypeclub/pype/issues/451)\n\n**Fixed bugs:**\n\n- Sync to avalon doesn't remove renamed task [\\#605](https://github.com/pypeclub/pype/issues/605)\n- NukeStudio: FPS collecting into clip instances [\\#624](https://github.com/pypeclub/pype/pull/624)\n\n**Merged pull requests:**\n\n- NukeStudio: small fixes [\\#622](https://github.com/pypeclub/pype/pull/622)\n- NukeStudio: broken order of plugins [\\#620](https://github.com/pypeclub/pype/pull/620)\n\n## [2.12.3](https://github.com/pypeclub/pype/tree/2.12.3) (2020-10-06)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.2...2.12.3)\n\n**Enhancements:**\n\n- Nuke Publish Camera [\\#567](https://github.com/pypeclub/pype/issues/567)\n- Harmony: open xstage file no matter of its name [\\#526](https://github.com/pypeclub/pype/issues/526)\n- Stop integration of unwanted data [\\#387](https://github.com/pypeclub/pype/issues/387)\n- Move avalon-launcher functionality to pype [\\#229](https://github.com/pypeclub/pype/issues/229)\n- avalon workfiles api [\\#214](https://github.com/pypeclub/pype/issues/214)\n- Store task types [\\#180](https://github.com/pypeclub/pype/issues/180)\n- Avalon Mongo Connection split [\\#136](https://github.com/pypeclub/pype/issues/136)\n- nk camera workflow [\\#71](https://github.com/pypeclub/pype/issues/71)\n- Hiero integration added [\\#590](https://github.com/pypeclub/pype/pull/590)\n- Anatomy instance data collection is substantially faster for many instances [\\#560](https://github.com/pypeclub/pype/pull/560)\n\n**Fixed bugs:**\n\n- test issue [\\#596](https://github.com/pypeclub/pype/issues/596)\n- Harmony: empty scene contamination [\\#583](https://github.com/pypeclub/pype/issues/583)\n- Edit publishing in SP doesn't respect shot selection for publishing [\\#542](https://github.com/pypeclub/pype/issues/542)\n- Pathlib breaks compatibility with python2 hosts [\\#281](https://github.com/pypeclub/pype/issues/281)\n- Updating a look where the shader name changed leaves the geo without a shader [\\#237](https://github.com/pypeclub/pype/issues/237)\n- Better error handling [\\#84](https://github.com/pypeclub/pype/issues/84)\n- Harmony: function signature [\\#609](https://github.com/pypeclub/pype/pull/609)\n- Nuke: gizmo publishing error [\\#594](https://github.com/pypeclub/pype/pull/594)\n- Harmony: fix clashing namespace of called js functions [\\#584](https://github.com/pypeclub/pype/pull/584)\n- Maya: fix maya scene type preset exception [\\#569](https://github.com/pypeclub/pype/pull/569)\n\n**Closed issues:**\n\n- Nuke Gizmo publishing [\\#597](https://github.com/pypeclub/pype/issues/597)\n- nuke gizmo publishing error [\\#592](https://github.com/pypeclub/pype/issues/592)\n- Publish EDL [\\#579](https://github.com/pypeclub/pype/issues/579)\n- Publish render from SP [\\#576](https://github.com/pypeclub/pype/issues/576)\n- rename ftrack custom attribute group to `pype` [\\#184](https://github.com/pypeclub/pype/issues/184)\n\n**Merged pull requests:**\n\n- Audio file existence check [\\#614](https://github.com/pypeclub/pype/pull/614)\n- NKS small fixes [\\#587](https://github.com/pypeclub/pype/pull/587)\n- Standalone publisher editorial plugins interfering [\\#580](https://github.com/pypeclub/pype/pull/580)\n\n## [2.12.2](https://github.com/pypeclub/pype/tree/2.12.2) (2020-09-25)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.1...2.12.2)\n\n**Enhancements:**\n\n- pype config GUI [\\#241](https://github.com/pypeclub/pype/issues/241)\n\n**Fixed bugs:**\n\n- Harmony: Saving heavy scenes will crash [\\#507](https://github.com/pypeclub/pype/issues/507)\n- Extract review a representation name with `\\*\\_burnin` [\\#388](https://github.com/pypeclub/pype/issues/388)\n- Hierarchy data was not considering active isntances [\\#551](https://github.com/pypeclub/pype/pull/551)\n\n## [2.12.1](https://github.com/pypeclub/pype/tree/2.12.1) (2020-09-15)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.0...2.12.1)\n\n**Fixed bugs:**\n\n- Pype: changelog.md is outdated [\\#503](https://github.com/pypeclub/pype/issues/503)\n- dependency security alert ! [\\#484](https://github.com/pypeclub/pype/issues/484)\n- Maya: RenderSetup is missing update [\\#106](https://github.com/pypeclub/pype/issues/106)\n- \\<pyblish plugin\\> extract effects creates new instance [\\#78](https://github.com/pypeclub/pype/issues/78)\n\n## [2.12.0](https://github.com/pypeclub/pype/tree/2.12.0) (2020-09-10)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.8...2.12.0)\n\n**Enhancements:**\n\n- Less mongo connections [\\#509](https://github.com/pypeclub/pype/pull/509)\n- Nuke: adding image loader  [\\#499](https://github.com/pypeclub/pype/pull/499)\n- Move launcher window to top if launcher action is clicked [\\#450](https://github.com/pypeclub/pype/pull/450)\n- Maya:  better tile rendering support in Pype [\\#446](https://github.com/pypeclub/pype/pull/446)\n- Implementation of non QML launcher [\\#443](https://github.com/pypeclub/pype/pull/443)\n- Optional skip review on renders. [\\#441](https://github.com/pypeclub/pype/pull/441)\n- Ftrack: Option to push status from task to latest version [\\#440](https://github.com/pypeclub/pype/pull/440)\n- Properly containerize image plane loads. [\\#434](https://github.com/pypeclub/pype/pull/434)\n- Option to keep the review files. [\\#426](https://github.com/pypeclub/pype/pull/426)\n- Isolate view on instance members. [\\#425](https://github.com/pypeclub/pype/pull/425)\n- Maya: Publishing of tile renderings on Deadline [\\#398](https://github.com/pypeclub/pype/pull/398)\n- Feature/little bit better logging gui [\\#383](https://github.com/pypeclub/pype/pull/383)\n\n**Fixed bugs:**\n\n- Maya: Fix tile order for Draft Tile Assembler [\\#511](https://github.com/pypeclub/pype/pull/511)\n- Remove extra dash [\\#501](https://github.com/pypeclub/pype/pull/501)\n- Fix: strip dot from repre names in single frame renders [\\#498](https://github.com/pypeclub/pype/pull/498)\n- Better handling of destination during integrating [\\#485](https://github.com/pypeclub/pype/pull/485)\n- Fix: allow thumbnail creation for single frame renders [\\#460](https://github.com/pypeclub/pype/pull/460)\n- added missing argument to launch\\_application in ftrack app handler [\\#453](https://github.com/pypeclub/pype/pull/453)\n- Burnins: Copy bit rate of input video to match quality. [\\#448](https://github.com/pypeclub/pype/pull/448)\n- Standalone publisher is now independent from tray [\\#442](https://github.com/pypeclub/pype/pull/442)\n- Bugfix/empty enumerator attributes [\\#436](https://github.com/pypeclub/pype/pull/436)\n- Fixed wrong order of \"other\" category collapssing in publisher [\\#435](https://github.com/pypeclub/pype/pull/435)\n- Multiple reviews where being overwritten to one. [\\#424](https://github.com/pypeclub/pype/pull/424)\n- Cleanup plugin fail on instances without staging dir [\\#420](https://github.com/pypeclub/pype/pull/420)\n- deprecated -intra parameter in ffmpeg to new `-g` [\\#417](https://github.com/pypeclub/pype/pull/417)\n- Delivery action can now work with entered path [\\#397](https://github.com/pypeclub/pype/pull/397)\n\n**Merged pull requests:**\n\n- Review on instance.data  [\\#473](https://github.com/pypeclub/pype/pull/473)\n\n## [2.11.8](https://github.com/pypeclub/pype/tree/2.11.8) (2020-08-27)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.7...2.11.8)\n\n**Enhancements:**\n\n- DWAA support for Maya [\\#382](https://github.com/pypeclub/pype/issues/382)\n- Isolate View on Playblast [\\#367](https://github.com/pypeclub/pype/issues/367)\n- Maya: Tile rendering [\\#297](https://github.com/pypeclub/pype/issues/297)\n- single pype instance running [\\#47](https://github.com/pypeclub/pype/issues/47)\n- PYPE-649: projects don't guarantee backwards compatible environment [\\#8](https://github.com/pypeclub/pype/issues/8)\n- PYPE-663: separate venv for each deployed version [\\#7](https://github.com/pypeclub/pype/issues/7)\n\n**Fixed bugs:**\n\n- pyblish pype - other group is collapsed before plugins are done [\\#431](https://github.com/pypeclub/pype/issues/431)\n- Alpha white edges in harmony on PNGs [\\#412](https://github.com/pypeclub/pype/issues/412)\n- harmony image loader picks wrong representations [\\#404](https://github.com/pypeclub/pype/issues/404)\n- Clockify crash when response contain symbol not allowed by UTF-8 [\\#81](https://github.com/pypeclub/pype/issues/81)\n\n## [2.11.7](https://github.com/pypeclub/pype/tree/2.11.7) (2020-08-21)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.6...2.11.7)\n\n**Fixed bugs:**\n\n- Clean Up Baked Movie [\\#369](https://github.com/pypeclub/pype/issues/369)\n- celaction last workfile [\\#459](https://github.com/pypeclub/pype/pull/459)\n\n## [2.11.6](https://github.com/pypeclub/pype/tree/2.11.6) (2020-08-18)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.5...2.11.6)\n\n**Enhancements:**\n\n- publisher app [\\#56](https://github.com/pypeclub/pype/issues/56)\n\n## [2.11.5](https://github.com/pypeclub/pype/tree/2.11.5) (2020-08-13)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.4...2.11.5)\n\n**Enhancements:**\n\n- Switch from master to equivalent [\\#220](https://github.com/pypeclub/pype/issues/220)\n- Standalone publisher now only groups sequence if the extension is known [\\#439](https://github.com/pypeclub/pype/pull/439)\n\n**Fixed bugs:**\n\n- Logs have been disable for editorial by default to speed up publishing [\\#433](https://github.com/pypeclub/pype/pull/433)\n- additional fixes for celaction [\\#430](https://github.com/pypeclub/pype/pull/430)\n- Harmony: invalid variable scope in validate scene settings [\\#428](https://github.com/pypeclub/pype/pull/428)\n- new representation name for audio was not accepted [\\#427](https://github.com/pypeclub/pype/pull/427)\n\n## [2.11.4](https://github.com/pypeclub/pype/tree/2.11.4) (2020-08-10)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.3...2.11.4)\n\n**Enhancements:**\n\n- WebSocket server [\\#135](https://github.com/pypeclub/pype/issues/135)\n- standalonepublisher: editorial family features expansion \\[master branch\\] [\\#411](https://github.com/pypeclub/pype/pull/411)\n\n## [2.11.3](https://github.com/pypeclub/pype/tree/2.11.3) (2020-08-04)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.2...2.11.3)\n\n**Fixed bugs:**\n\n- Harmony: publishing performance issues [\\#408](https://github.com/pypeclub/pype/pull/408)\n\n## [2.11.2](https://github.com/pypeclub/pype/tree/2.11.2) (2020-07-31)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.1...2.11.2)\n\n**Fixed bugs:**\n\n- Ftrack to Avalon bug [\\#406](https://github.com/pypeclub/pype/issues/406)\n\n## [2.11.1](https://github.com/pypeclub/pype/tree/2.11.1) (2020-07-29)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.0...2.11.1)\n\n**Merged pull requests:**\n\n- Celaction: metadata json folder fixes on path  [\\#393](https://github.com/pypeclub/pype/pull/393)\n- CelAction - version up method taken fro pype.lib  [\\#391](https://github.com/pypeclub/pype/pull/391)\n\n<a name=\"2.11.0\"></a>\n## 2.11.0 ##\n\n_**release date:** 27 July 2020_\n\n**new:**\n- _(blender)_ namespace support [\\#341](https://github.com/pypeclub/pype/pull/341)\n- _(blender)_ start end frames [\\#330](https://github.com/pypeclub/pype/pull/330)\n- _(blender)_ camera asset [\\#322](https://github.com/pypeclub/pype/pull/322)\n- _(pype)_ toggle instances per family in pyblish GUI [\\#320](https://github.com/pypeclub/pype/pull/320)\n- _(pype)_ current release version is now shown in the tray menu [#379](https://github.com/pypeclub/pype/pull/379)\n\n\n**improved:**\n- _(resolve)_ tagging for publish [\\#239](https://github.com/pypeclub/pype/issues/239)\n- _(pype)_ Support publishing a subset of shots with standalone editorial [\\#336](https://github.com/pypeclub/pype/pull/336)\n- _(harmony)_ Basic support for palettes [\\#324](https://github.com/pypeclub/pype/pull/324)\n- _(photoshop)_ Flag outdated containers on startup and publish. [\\#309](https://github.com/pypeclub/pype/pull/309)\n- _(harmony)_ Flag Outdated containers [\\#302](https://github.com/pypeclub/pype/pull/302)\n- _(photoshop)_ Publish review [\\#298](https://github.com/pypeclub/pype/pull/298)\n- _(pype)_ Optional Last workfile launch [\\#365](https://github.com/pypeclub/pype/pull/365)\n\n\n**fixed:**\n- _(premiere)_ workflow fixes [\\#346](https://github.com/pypeclub/pype/pull/346)\n- _(pype)_ pype-setup does not work with space in path [\\#327](https://github.com/pypeclub/pype/issues/327)\n- _(ftrack)_ Ftrack delete action cause circular error [\\#206](https://github.com/pypeclub/pype/issues/206)\n- _(nuke)_ Priority was forced to 50 [\\#345](https://github.com/pypeclub/pype/pull/345)\n- _(nuke)_ Fix ValidateNukeWriteKnobs [\\#340](https://github.com/pypeclub/pype/pull/340)\n- _(maya)_ If camera attributes are connected, we can ignore them. [\\#339](https://github.com/pypeclub/pype/pull/339)\n- _(pype)_ stop appending of tools environment to existing env [\\#337](https://github.com/pypeclub/pype/pull/337)\n- _(ftrack)_ Ftrack timeout needs to look at AVALON\\_TIMEOUT [\\#325](https://github.com/pypeclub/pype/pull/325)\n- _(harmony)_ Only zip files are supported. [\\#310](https://github.com/pypeclub/pype/pull/310)\n- _(pype)_ hotfix/Fix event server mongo uri [\\#305](https://github.com/pypeclub/pype/pull/305)\n- _(photoshop)_ Subset was not named or validated correctly. [\\#304](https://github.com/pypeclub/pype/pull/304)\n\n\n\n<a name=\"2.10.0\"></a>\n## 2.10.0 ##\n\n_**release date:** 17 June 2020_\n\n**new:**\n- _(harmony)_ **Toon Boom Harmony** has been greatly extended to support rigging, scene build, animation and rendering workflows. [#270](https://github.com/pypeclub/pype/issues/270) [#271](https://github.com/pypeclub/pype/issues/271) [#190](https://github.com/pypeclub/pype/issues/190) [#191](https://github.com/pypeclub/pype/issues/191) [#172](https://github.com/pypeclub/pype/issues/172) [#168](https://github.com/pypeclub/pype/issues/168)\n- _(pype)_ Added support for rudimentary **edl publishing** into individual shots. [#265](https://github.com/pypeclub/pype/issues/265)\n- _(celaction)_ Simple **Celaction** integration has been added with support for workfiles and rendering. [#255](https://github.com/pypeclub/pype/issues/255)\n- _(maya)_ Support for multiple job types when submitting to the farm. We can now render Maya or Standalone render jobs for Vray and Arnold (limited support for arnold) [#204](https://github.com/pypeclub/pype/issues/204)\n- _(photoshop)_ Added initial support for Photoshop [#232](https://github.com/pypeclub/pype/issues/232)\n\n**improved:**\n- _(blender)_ Updated support for rigs and added support Layout family [#233](https://github.com/pypeclub/pype/issues/233) [#226](https://github.com/pypeclub/pype/issues/226)\n- _(premiere)_ It is now possible to choose different storage root for workfiles of different task types. [#255](https://github.com/pypeclub/pype/issues/255)\n- _(maya)_ Support for unmerged AOVs in Redshift multipart EXRs [#197](https://github.com/pypeclub/pype/issues/197)\n- _(pype)_ Pype repository has been refactored in preparation for 3.0 release [#169](https://github.com/pypeclub/pype/issues/169)\n- _(deadline)_ All file dependencies are now passed to deadline from maya to prevent premature start of rendering if caches or textures haven't been coppied over yet. [#195](https://github.com/pypeclub/pype/issues/195)\n- _(nuke)_ Script validation can now be made optional. [#194](https://github.com/pypeclub/pype/issues/194)\n- _(pype)_ Publishing can now be stopped at any time. [#194](https://github.com/pypeclub/pype/issues/194)\n\n**fix:**\n- _(pype)_ Pyblish-lite has been integrated into pype repository, plus various publishing GUI fixes. [#274](https://github.com/pypeclub/pype/issues/274) [#275](https://github.com/pypeclub/pype/issues/275) [#268](https://github.com/pypeclub/pype/issues/268) [#227](https://github.com/pypeclub/pype/issues/227) [#238](https://github.com/pypeclub/pype/issues/238)\n- _(maya)_ Alembic extractor was getting wrong frame range type in certain scenarios [#254](https://github.com/pypeclub/pype/issues/254)\n- _(maya)_ Attaching a render to subset in maya was not passing validation in certain scenarios  [#256](https://github.com/pypeclub/pype/issues/256)\n- _(ftrack)_ Various small fixes to ftrack sync [#263](https://github.com/pypeclub/pype/issues/263) [#259](https://github.com/pypeclub/pype/issues/259)\n- _(maya)_ Look extraction is now able to skp invalid connections in shaders [#207](https://github.com/pypeclub/pype/issues/207)\n\n\n\n<a name=\"2.9.0\"></a>\n## 2.9.0 ##\n\n_**release date:** 25 May 2020_\n\n**new:**\n- _(pype)_ Support for **Multiroot projects**. You can now store project data on multiple physical or virtual storages and target individual publishes to these locations. For instance render can be stored on a faster storage than the rest of the project. [#145](https://github.com/pypeclub/pype/issues/145), [#38](https://github.com/pypeclub/pype/issues/38)\n- _(harmony)_ Basic implementation of **Toon Boom Harmony** has been added. [#142](https://github.com/pypeclub/pype/issues/142)\n- _(pype)_ OSX support is in public beta now. There are issues to be expected, but the main implementation should be functional. [#141](https://github.com/pypeclub/pype/issues/141)\n\n\n**improved:**\n\n- _(pype)_ **Review extractor** has been completely rebuilt. It now supports granular filtering so you can create **multiple outputs** for different tasks, families or hosts. [#103](https://github.com/pypeclub/pype/issues/103), [#166](https://github.com/pypeclub/pype/issues/166), [#165](https://github.com/pypeclub/pype/issues/165)\n- _(pype)_ **Burnin** generation had been extended to **support same multi-output filtering** as review extractor [#103](https://github.com/pypeclub/pype/issues/103)\n- _(pype)_ Publishing file templates can now be specified in config for each individual family [#114](https://github.com/pypeclub/pype/issues/114)\n- _(pype)_ Studio specific plugins can now be appended to pype standard publishing plugins. [#112](https://github.com/pypeclub/pype/issues/112)\n- _(nukestudio)_ Reviewable clips no longer need to be previously cut, exported and re-imported to timeline. **Pype can now dynamically cut reviewable quicktimes** from continuous offline footage during publishing. [#23](https://github.com/pypeclub/pype/issues/23)\n- _(deadline)_ Deadline can now correctly differentiate between staging and production pype. [#154](https://github.com/pypeclub/pype/issues/154)\n- _(deadline)_ `PYPE_PYTHON_EXE` env variable can now be used to direct publishing to explicit python installation. [#120](https://github.com/pypeclub/pype/issues/120)\n- _(nuke)_ Nuke now check for new version of loaded data on file open. [#140](https://github.com/pypeclub/pype/issues/140)\n- _(nuke)_ frame range and limit checkboxes are now exposed on write node. [#119](https://github.com/pypeclub/pype/issues/119)\n\n\n\n**fix:**\n\n- _(nukestudio)_ Project Location was using backslashes which was breaking nukestudio native exporting in certains configurations [#82](https://github.com/pypeclub/pype/issues/82)\n- _(nukestudio)_ Duplicity in hierarchy tags was prone to throwing publishing error [#130](https://github.com/pypeclub/pype/issues/130), [#144](https://github.com/pypeclub/pype/issues/144)\n- _(ftrack)_ multiple stability improvements [#157](https://github.com/pypeclub/pype/issues/157), [#159](https://github.com/pypeclub/pype/issues/159), [#128](https://github.com/pypeclub/pype/issues/128), [#118](https://github.com/pypeclub/pype/issues/118), [#127](https://github.com/pypeclub/pype/issues/127)\n- _(deadline)_ multipart EXRs were stopping review publishing on the farm. They are still not supported for automatic review generation, but the publish will go through correctly without the quicktime. [#155](https://github.com/pypeclub/pype/issues/155)\n- _(deadline)_ If deadline is non-responsive it will no longer freeze host when publishing [#149](https://github.com/pypeclub/pype/issues/149)\n- _(deadline)_ Sometimes deadline was trying to launch render before all the source data was coppied over. [#137](https://github.com/pypeclub/pype/issues/137) _(harmony)_ Basic implementation of **Toon Boom Harmony** has been added. [#142](https://github.com/pypeclub/pype/issues/142)\n- _(nuke)_ Filepath knob wasn't updated properly. [#131](https://github.com/pypeclub/pype/issues/131)\n- _(maya)_ When extracting animation, the \"Write Color Set\" options on the instance were not respected. [#108](https://github.com/pypeclub/pype/issues/108)\n- _(maya)_ Attribute overrides for AOV only worked for the legacy render layers. Now it works for new render setup as well [#132](https://github.com/pypeclub/pype/issues/132)\n- _(maya)_ Stability and usability improvements in yeti workflow [#104](https://github.com/pypeclub/pype/issues/104)\n\n\n\n<a name=\"2.8.0\"></a>\n## 2.8.0 ##\n\n_**release date:** 20 April 2020_\n\n**new:**\n\n- _(pype)_ Option to generate slates from json templates. [PYPE-628] [#26](https://github.com/pypeclub/pype/issues/26)\n- _(pype)_ It is now possible to automate loading of published subsets into any scene. Documentation will follow :). [PYPE-611] [#24](https://github.com/pypeclub/pype/issues/24)\n\n**fix:**\n\n- _(maya)_ Some Redshift render tokens could break publishing. [PYPE-778] [#33](https://github.com/pypeclub/pype/issues/33)\n- _(maya)_ Publish was not preserving maya file extension. [#39](https://github.com/pypeclub/pype/issues/39)\n- _(maya)_ Rig output validator was failing on nodes without shapes. [#40](https://github.com/pypeclub/pype/issues/40)\n- _(maya)_ Yeti caches can now be properly versioned up in the scene inventory. [#40](https://github.com/pypeclub/pype/issues/40)\n- _(nuke)_ Build first workfiles was not accepting jpeg sequences. [#34](https://github.com/pypeclub/pype/issues/34)\n- _(deadline)_ Trying to generate ffmpeg review from multipart EXRs no longer crashes publishing. [PYPE-781]\n- _(deadline)_ Render publishing is more stable in multiplatform environments. [PYPE-775]\n\n\n\n<a name=\"2.7.0\"></a>\n## 2.7.0 ##\n\n_**release date:** 30 March 2020_\n\n**new:**\n\n- _(maya)_ Artist can now choose to load multiple references of the same subset at once [PYPE-646, PYPS-81]\n- _(nuke)_ Option to use named OCIO colorspaces for review colour baking. [PYPS-82]\n- _(pype)_ Pype can now work with `master` versions for publishing and loading. These are non-versioned publishes that are overwritten with the latest version during publish. These are now supported in all the GUIs, but their publishing is deactivated by default. [PYPE-653]\n- _(blender)_ Added support for basic blender workflow. We currently support `rig`, `model` and `animation` families. [PYPE-768]\n- _(pype)_ Source timecode can now be used in burn-ins. [PYPE-777]\n- _(pype)_ Review outputs profiles can now specify delivery resolution different than project setting [PYPE-759]\n- _(nuke)_ Bookmark to current context is now added automatically to all nuke browser windows. [PYPE-712]\n\n**change:**\n\n- _(maya)_ It is now possible to publish camera without. baking. Keep in mind that unbaked cameras can't be guaranteed to work in other hosts. [PYPE-595]\n- _(maya)_ All the renders from maya are now grouped in the loader by their Layer name. [PYPE-482]\n- _(nuke/hiero)_ Any publishes from nuke and hiero can now be versioned independently of the workfile. [PYPE-728]\n\n\n**fix:**\n\n- _(nuke)_ Mixed slashes caused issues in ocio config path.\n- _(pype)_ Intent field in pyblish GUI was passing label instead of value to ftrack. [PYPE-733]\n- _(nuke)_ Publishing of pre-renders was inconsistent. [PYPE-766]\n- _(maya)_ Handles and frame ranges were inconsistent in various places during publishing.\n- _(nuke)_ Nuke was crashing if it ran into certain missing knobs. For example DPX output missing `autocrop` [PYPE-774]\n- _(deadline)_ Project overrides were not working properly with farm render publishing.\n- _(hiero)_ Problems with single frame plates publishing.\n- _(maya)_ Redshift RenderPass token were breaking render publishing. [PYPE-778]\n- _(nuke)_ Build first workfile was not accepting jpeg sequences.\n- _(maya)_ Multipart (Multilayer) EXRs were breaking review publishing due to FFMPEG incompatiblity [PYPE-781]\n\n\n<a name=\"2.6.0\"></a>\n## 2.6.0 ##\n\n_**release date:** 9 March 2020_\n\n**change:**\n- _(maya)_ render publishing has been simplified and made more robust. Render setup layers are now automatically added to publishing subsets and `render globals` family has been replaced with simple `render` [PYPE-570]\n- _(avalon)_ change context and workfiles apps, have been merged into one, that allows both actions to be performed at the same time. [PYPE-747]\n- _(pype)_ thumbnails are now automatically propagate to asset from the last published subset in the loader\n- _(ftrack)_ publishing comment and intent are now being published to ftrack note as well as describtion. [PYPE-727]\n- _(pype)_ when overriding existing version new old representations are now overriden, instead of the new ones just being appended. (to allow this behaviour, the version validator need to be disabled. [PYPE-690])\n- _(pype)_ burnin preset has been significantly simplified. It now doesn't require passing function to each field, but only need the actual text template. to use this, all the current burnin PRESETS MUST BE UPDATED for all the projects.\n- _(ftrack)_ credentials are now stored on a per server basis, so it's possible to switch between ftrack servers without having to log in and out. [PYPE-723]\n\n\n**new:**\n- _(pype)_ production and development deployments now have different colour of the tray icon. Orange for Dev and Green for production [PYPE-718]\n- _(maya)_ renders can now be attached to a publishable subset rather than creating their own subset. For example it is possible to create a reviewable `look` or `model` render and have it correctly attached as a representation of the subsets [PYPE-451]\n- _(maya)_ after saving current scene into a new context (as a new shot for instance), all the scene publishing subsets data gets re-generated automatically to match the new context [PYPE-532]\n- _(pype)_ we now support project specific publish, load and create plugins [PYPE-740]\n- _(ftrack)_ new action that allow archiving/deleting old published versions. User can keep how many of the latest version to keep when the action is ran. [PYPE-748, PYPE-715]\n- _(ftrack)_ it is now possible to monitor and restart ftrack event server using ftrack action. [PYPE-658]\n- _(pype)_ validator that prevent accidental overwrites of previously published versions. [PYPE-680]\n- _(avalon)_ avalon core updated to version 5.6.0\n- _(maya)_ added validator to make sure that relative paths are used when publishing arnold standins.\n- _(nukestudio)_ it is now possible to extract and publish audio family from clip in nuke studio [PYPE-682]\n\n**fix**:\n- _(maya)_ maya set framerange button was ignoring handles [PYPE-719]\n- _(ftrack)_ sync to avalon was sometime crashing when ran on empty project\n- _(nukestudio)_ publishing same shots after they've been previously archived/deleted would result in a crash. [PYPE-737]\n- _(nuke)_ slate workflow was breaking in certain scenarios. [PYPE-730]\n- _(pype)_ rendering publish workflow has been significantly improved to prevent error resulting from implicit render collection. [PYPE-665, PYPE-746]\n- _(pype)_ launching application on a non-synced project resulted in obscure [PYPE-528]\n- _(pype)_ missing keys in burnins no longer result in an error. [PYPE-706]\n- _(ftrack)_ create folder structure action was sometimes failing for project managers due to wrong permissions.\n- _(Nukestudio)_ using `source` in the start frame tag could result in wrong frame range calculation\n- _(ftrack)_ sync to avalon action and event have been improved by catching more edge cases and provessing them properly.\n\n\n<a name=\"2.5\"></a>\n## 2.5.0 ##\n\n_**release date:** 11 Feb 2020_\n\n**change:**\n- _(pype)_ added many logs for easier debugging\n- _(pype)_ review presets can now be separated between 2d and 3d renders [PYPE-693]\n- _(pype)_ anatomy module has been greatly improved to allow for more dynamic pulblishing and faster debugging [PYPE-685]\n- _(pype)_ avalon schemas have been moved from `pype-config` to `pype` repository, for simplification. [PYPE-670]\n- _(ftrack)_ updated to latest ftrack API\n- _(ftrack)_ publishing comments now appear in ftrack also as a note on version with customisable category [PYPE-645]\n- _(ftrack)_ delete asset/subset action had been improved. It is now able to remove multiple entities and descendants of the selected entities [PYPE-361, PYPS-72]\n- _(workfiles)_ added date field to workfiles app [PYPE-603]\n- _(maya)_ old deprecated loader have been removed in favour of a single unified reference loader (old scenes will upgrade automatically to the new loader upon opening) [PYPE-633, PYPE-697]\n- _(avalon)_ core updated to 5.5.15 [PYPE-671]\n- _(nuke)_ library loader is now available in nuke [PYPE-698]\n\n\n**new:**\n- _(pype)_ added pype render wrapper to allow rendering on mixed platform farms. [PYPE-634]\n- _(pype)_ added `pype launch` command. It let's admin run applications with dynamically built environment based on the given context. [PYPE-634]\n- _(pype)_ added support for extracting review sequences with burnins [PYPE-657]\n- _(publish)_ users can now set intent next to a comment when publishing. This will then be reflected on an attribute in ftrack. [PYPE-632]\n- _(burnin)_ timecode can now be added to burnin\n- _(burnin)_ datetime keys can now be added to burnin and anatomy [PYPE-651]\n- _(burnin)_ anatomy templates can now be used in burnins. [PYPE=626]\n- _(nuke)_ new validator for render resolution\n- _(nuke)_ support for attach slate to nuke renders [PYPE-630]\n- _(nuke)_ png sequences were added to loaders\n- _(maya)_ added maya 2020 compatibility [PYPE-677]\n- _(maya)_ ability to publish and load .ASS standin sequences [PYPS-54]\n- _(pype)_ thumbnails can now be published and are visible in the loader. `AVALON_THUMBNAIL_ROOT` environment variable needs to be set for this to work  [PYPE-573, PYPE-132]\n- _(blender)_ base implementation of blender was added with publishing and loading of .blend files [PYPE-612]\n- _(ftrack)_ new action for preparing deliveries [PYPE-639]\n\n\n**fix**:\n- _(burnin)_ more robust way of finding ffmpeg for burnins.\n- _(pype)_ improved UNC paths remapping when sending to farm.\n- _(pype)_ float frames sometimes made their way to representation context in database, breaking loaders [PYPE-668]\n- _(pype)_ `pype install --force` was failing sometimes [PYPE-600]\n- _(pype)_ padding in published files got calculated wrongly sometimes. It is now instead being always read from project anatomy. [PYPE-667]\n- _(publish)_ comment publishing was failing in certain situations\n- _(ftrack)_ multiple edge case scenario fixes in auto sync and sync-to-avalon action\n- _(ftrack)_ sync to avalon now works on empty projects\n- _(ftrack)_ thumbnail update event was failing when deleting entities [PYPE-561]\n- _(nuke)_ loader applies proper colorspaces from Presets\n- _(nuke)_ publishing handles didn't always work correctly [PYPE-686]\n- _(maya)_ assembly publishing and loading wasn't working correctly\n\n\n\n\n<a name=\"2.4.0\"></a>\n## 2.4.0 ##\n\n_**release date:** 9 Dec 2019_\n\n**change:**\n- _(ftrack)_ version to status ftrack event can now be configured from Presets\n  - based on preset `presets/ftracc/ftrack_config.json[\"status_version_to_task\"]`\n- _(ftrack)_ sync to avalon event has been completely re-written. It now supports most of the project management situations on ftrack including moving, renaming and deleting entities, updating attributes and working with tasks.\n- _(ftrack)_ sync to avalon action has been also re-writen. It is now much faster (up to 100 times depending on a project structure), has much better logging and reporting on encountered problems, and is able to handle much more complex situations.\n- _(ftrack)_ sync to avalon trigger by checking `auto-sync` toggle on ftrack [PYPE-504]\n- _(pype)_ various new features in the REST api\n- _(pype)_ new visual identity used across pype\n- _(pype)_ started moving all requirements to pip installation rather than vendorising them in pype repository. Due to a few yet unreleased packages, this means that pype can temporarily be only installed in the offline mode.\n\n**new:**\n- _(nuke)_ support for publishing gizmos and loading them as viewer processes\n- _(nuke)_ support for publishing nuke nodes from backdrops and loading them back\n- _(pype)_ burnins can now work with start and end frames as keys\n  - use keys `{frame_start}`, `{frame_end}` and `{current_frame}` in burnin preset to use them. [PYPS-44,PYPS-73, PYPE-602]\n- _(pype)_ option to filter logs by user and level in loggin GUI\n- _(pype)_ image family added to standalone publisher [PYPE-574]\n- _(pype)_ matchmove family added to standalone publisher [PYPE-574]\n- _(nuke)_ validator for comparing arbitrary knobs with values from presets\n- _(maya)_ option to force maya to copy textures in the new look publish rather than hardlinking them\n- _(pype)_ comments from pyblish GUI are now being added to ftrack version\n- _(maya)_ validator for checking outdated containers in the scene\n- _(maya)_ option to publish and load arnold standin sequence [PYPE-579, PYPS-54]\n\n**fix**:\n- _(pype)_ burnins were not respecting codec of the input video\n- _(nuke)_ lot's of various nuke and nuke studio fixes across the board [PYPS-45]\n- _(pype)_ workfiles app is not launching with the start of the app by default [PYPE-569]\n- _(ftrack)_ ftrack integration during publishing was failing under certain situations [PYPS-66]\n- _(pype)_ minor fixes in REST api\n- _(ftrack)_ status change event was crashing when the target status was missing [PYPS-68]\n- _(ftrack)_ actions will try to reconnect if they fail for some reason\n- _(maya)_ problems with fps mapping when using float FPS values\n- _(deadline)_ overall improvements to deadline publishing\n- _(setup)_ environment variables are now remapped on the fly based on the platform pype is running on. This fixes many issues in mixed platform environments.\n\n\n<a name=\"2.3.6\"></a>\n## 2.3.6 #\n\n_**release date:** 27 Nov 2019_\n\n**hotfix**:\n- _(ftrack)_ was hiding important debug logo\n- _(nuke)_ crashes during workfile publishing\n- _(ftrack)_ event server crashes because of signal problems\n- _(muster)_ problems with muster render submissions\n- _(ftrack)_ thumbnail update event syntax errors\n\n\n## 2.3.0 ##\n_release date: 6 Oct 2019_\n\n**new**:\n- _(maya)_ support for yeti rigs and yeti caches\n- _(maya)_ validator for comparing arbitrary attributes against ftrack\n- _(pype)_ burnins can now show current date and time\n- _(muster)_ pools can now be set in render globals in maya\n- _(pype)_ Rest API has been implemented in beta stage\n- _(nuke)_ LUT loader has been added\n- _(pype)_ rudimentary user module has been added as preparation for user management\n- _(pype)_ a simple logging GUI has been added to pype tray\n- _(nuke)_ nuke can now bake input process into mov\n- _(maya)_ imported models now have selection handle displayed by defaulting\n- _(avalon)_ it's is now possible to load multiple assets at once using loader\n- _(maya)_ added ability to automatically connect yeti rig to a mesh upon loading\n\n**changed**:\n- _(ftrack)_ event server now runs two parallel processes and is able to keep queue of events to process.\n- _(nuke)_ task name is now added to all rendered subsets\n- _(pype)_ adding more families to standalone publisher\n- _(pype)_ standalone publisher now uses pyblish-lite\n- _(pype)_ standalone publisher can now create review quicktimes\n- _(ftrack)_ queries to ftrack were sped up\n- _(ftrack)_ multiple ftrack action have been deprecated\n- _(avalon)_ avalon upstream has been updated to 5.5.0\n- _(nukestudio)_ published transforms can now be animated\n-\n\n**fix**:\n- _(maya)_ fps popup button didn't work in some cases\n- _(maya)_ geometry instances and references in maya were losing shader assignments\n- _(muster)_ muster rendering templates were not working correctly\n- _(maya)_ arnold tx texture conversion wasn't respecting colorspace set by the artist\n- _(pype)_ problems with avalon db sync\n- _(maya)_ ftrack was rounding FPS making it inconsistent\n- _(pype)_ wrong icon names in Creator\n- _(maya)_ scene inventory wasn't showing anything if representation was removed from database after it's been loaded to the scene\n- _(nukestudio)_ multiple bugs squashed\n- _(loader)_ loader was taking long time to show all the loading action when first launcher in maya\n\n## 2.2.0 ##\n_release date: 8 Sept 2019_\n\n**new**:\n- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts\n- _(nuke)_ option to choose deadline chunk size on write nodes\n- _(nukestudio)_ added option to publish soft effects (subTrackItems) from NukeStudio as subsets including LUT files. these can then be loaded in nuke or NukeStudio\n- _(nuke)_ option to build nuke script from previously published latest versions of plate and render subsets.\n- _(nuke)_ nuke writes now have deadline tab.\n- _(ftrack)_ Prepare Project action can now be used for creating the base folder structure on disk and in ftrack, setting up all the initial project attributes and it automatically prepares `pype_project_config` folder for the given project.\n- _(clockify)_ Added support for time tracking in clockify. This currently in addition to ftrack time logs, but does not completely replace them.\n- _(pype)_ any attributes in Creator and Loader plugins can now be customised using pype preset system\n\n**changed**:\n- nukestudio now uses workio API for workfiles\n- _(maya)_ \"FIX FPS\" prompt in maya now appears in the middle of the screen\n- _(muster)_ can now be configured with custom templates\n- _(pype)_ global publishing plugins can now be configured using presets as well as host specific ones\n\n\n**fix**:\n- wrong version retrieval from path in certain scenarios\n- nuke reset resolution wasn't working in certain scenarios\n\n## 2.1.0 ##\n_release date: 6 Aug 2019_\n\nA large cleanup release. Most of the change are under the hood.\n\n**new**:\n- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts\n- _(pype)_ Added configurable option to add burnins to any generated quicktimes\n- _(ftrack)_ Action that identifies what machines pype is running on.\n- _(system)_ unify subprocess calls\n- _(maya)_ add audio to review quicktimes\n- _(nuke)_ add crop before write node to prevent overscan problems in ffmpeg\n- **Nuke Studio** publishing and workfiles support\n- **Muster** render manager support\n- _(nuke)_ Framerange, FPS and Resolution are set automatically at startup\n- _(maya)_ Ability to load published sequences as image planes\n- _(system)_ Ftrack event that sets asset folder permissions based on task assignees in ftrack.\n- _(maya)_ Pyblish plugin that allow validation of maya attributes\n- _(system)_ added better startup logging to tray debug, including basic connection information\n- _(avalon)_ option to group published subsets to groups in the loader\n- _(avalon)_ loader family filters are working now\n\n**changed**:\n- change multiple key attributes to unify their behaviour across the pipeline\n  - `frameRate` to `fps`\n  - `startFrame` to `frameStart`\n  - `endFrame` to `frameEnd`\n  - `fstart` to `frameStart`\n  - `fend` to `frameEnd`\n  - `handle_start` to `handleStart`\n  - `handle_end` to `handleEnd`\n  - `resolution_width` to `resolutionWidth`\n  - `resolution_height` to `resolutionHeight`\n  - `pixel_aspect` to `pixelAspect`\n\n- _(nuke)_ write nodes are now created inside group with only some attributes editable by the artist\n- rendered frames are now deleted from temporary location after their publishing is finished.\n- _(ftrack)_ RV action can now be launched from any entity\n- after publishing only refresh button is now available in pyblish UI\n- added context instance pyblish-lite so that artist knows if context plugin fails\n- _(avalon)_ allow opening selected files using enter key\n- _(avalon)_ core updated to v5.2.9 with our forked changes on top\n\n**fix**:\n- faster hierarchy retrieval from db\n- _(nuke)_ A lot of stability enhancements\n- _(nuke studio)_ A lot of stability enhancements\n- _(nuke)_ now only renders a single write node on farm\n- _(ftrack)_ pype would crash when launcher project level task\n- work directory was sometimes not being created correctly\n- major pype.lib cleanup. Removing of unused functions, merging those that were doing the same and general house cleaning.\n- _(avalon)_ subsets in maya 2019 weren't behaving correctly in the outliner\n\n\n\\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at info@pype.club. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## How to contribute to OpenPype\n\nOpenPype has reached the end of its life and is now in a limited maintenance mode (read more at https://community.ynput.io/t/openpype-end-of-life-timeline/877). As such we're no longer accepting contributions unless they are also ported to AYON at the same time. \n\n## Getting my PR merged during this period\n\n- Each OpenPype PR MUST have a corresponding AYON PR in github. Without AYON compatibility features will not be merged! Luckily most of the code is compatible, albeit sometimes in a different place after refactor. Porting from OpenPype to AYON should be really easy.\n- Please keep the corresponding OpenPype and AYON PR names the same so they can be easily identified.\n\nInside each PR, put a link to the corresponding PR from the other product. OpenPype PRs should point to AYON PR and vice versa.\n\nAYON repository structure is a lot more granular compared to OpenPype. If you're unsure what repository your AYON equivalent PR should target, feel free to make OpenPype PR first and ask.\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Build Pype docker image\nFROM ubuntu:focal AS builder\nARG OPENPYPE_PYTHON_VERSION=3.9.12\nARG BUILD_DATE\nARG VERSION\n\nLABEL maintainer=\"info@openpype.io\"\nLABEL description=\"Docker Image to build and run OpenPype under Ubuntu 20.04\"\nLABEL org.opencontainers.image.name=\"pypeclub/openpype\"\nLABEL org.opencontainers.image.title=\"OpenPype Docker Image\"\nLABEL org.opencontainers.image.url=\"https://openpype.io/\"\nLABEL org.opencontainers.image.source=\"https://github.com/pypeclub/OpenPype\"\nLABEL org.opencontainers.image.documentation=\"https://openpype.io/docs/system_introduction\"\nLABEL org.opencontainers.image.created=$BUILD_DATE\nLABEL org.opencontainers.image.version=$VERSION\n\nUSER root\n\nARG DEBIAN_FRONTEND=noninteractive\n\n# update base\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n        ca-certificates \\\n        bash \\\n        git \\\n        cmake \\\n        make \\\n        curl \\\n        wget \\\n        build-essential \\\n        checkinstall \\\n        libssl-dev \\\n        zlib1g-dev \\\n        libbz2-dev \\\n        libreadline-dev \\\n        libsqlite3-dev \\\n        llvm \\\n        libncursesw5-dev \\\n        xz-utils \\\n        tk-dev \\\n        libxml2-dev \\\n        libxmlsec1-dev \\\n        libffi-dev \\\n        liblzma-dev \\\n        patchelf\n\nSHELL [\"/bin/bash\", \"-c\"]\n\n\nRUN mkdir /opt/openpype\n\n# download and install pyenv\nRUN curl https://pyenv.run | bash \\\n    && echo 'export PATH=\"$HOME/.pyenv/bin:$PATH\"'>> $HOME/init_pyenv.sh \\\n    && echo 'eval \"$(pyenv init -)\"' >> $HOME/init_pyenv.sh \\\n    && echo 'eval \"$(pyenv virtualenv-init -)\"' >> $HOME/init_pyenv.sh \\\n    && echo 'eval \"$(pyenv init --path)\"' >> $HOME/init_pyenv.sh\n\n# install python with pyenv\nRUN source $HOME/init_pyenv.sh \\\n    && pyenv install ${OPENPYPE_PYTHON_VERSION}\n\nCOPY . /opt/openpype/\n\nRUN chmod +x /opt/openpype/tools/create_env.sh && chmod +x /opt/openpype/tools/build.sh\n\nWORKDIR /opt/openpype\n\n# set local python version\nRUN cd /opt/openpype \\\n    && source $HOME/init_pyenv.sh \\\n    && pyenv local ${OPENPYPE_PYTHON_VERSION}\n\n# fetch third party tools/libraries\nRUN source $HOME/init_pyenv.sh \\\n    && ./tools/create_env.sh \\\n    && ./tools/fetch_thirdparty_libs.sh\n\n# build openpype\nRUN source $HOME/init_pyenv.sh \\\n    && bash ./tools/build.sh\n"
  },
  {
    "path": "Dockerfile.centos7",
    "content": "# Build Pype docker image\nFROM centos:7 AS builder\nARG OPENPYPE_PYTHON_VERSION=3.9.12\n\nLABEL org.opencontainers.image.name=\"pypeclub/openpype\"\nLABEL org.opencontainers.image.title=\"OpenPype Docker Image\"\nLABEL org.opencontainers.image.url=\"https://openpype.io/\"\nLABEL org.opencontainers.image.source=\"https://github.com/pypeclub/pype\"\nLABEL org.opencontainers.image.documentation=\"https://openpype.io/docs/system_introduction\"\nLABEL org.opencontainers.image.created=$BUILD_DATE\nLABEL org.opencontainers.image.version=$VERSION\n\n\nUSER root\n\n# update base\nRUN yum -y install deltarpm \\\n    && yum -y update \\\n    && yum clean all\n\n# add tools we need\nRUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \\\n    && yum -y install centos-release-scl \\\n    && yum -y install \\\n        bash \\\n        which \\\n        git \\\n        make \\\n        devtoolset-7 \\\n        cmake \\\n        curl \\\n        wget \\\n        gcc \\\n        zlib-devel \\\n        pcre-devel \\\n        perl-core \\\n        bzip2 \\\n        bzip2-devel \\\n        readline-devel \\\n        sqlite sqlite-devel \\\n        openssl-devel \\\n        openssl-libs \\\n        openssl11-devel \\\n        openssl11-libs \\\n        tk-devel libffi-devel \\\n        patchelf \\\n        automake \\\n        autoconf \\\n        patch \\\n        ncurses \\\n\t    ncurses-devel \\\n        qt5-qtbase-devel \\\n        xcb-util-wm \\\n        xcb-util-renderutil \\\n    && yum clean all\n\n# we need to build our own patchelf\nWORKDIR /temp-patchelf\nRUN git clone -b 0.17.0 --single-branch https://github.com/NixOS/patchelf.git . \\\n    && source scl_source enable devtoolset-7 \\\n    && ./bootstrap.sh \\\n    && ./configure \\\n    && make \\\n    && make install\n\nRUN mkdir /opt/openpype\n# RUN useradd -m pype\n# RUN chown pype /opt/openpype\n# USER pype\n\nRUN curl https://pyenv.run | bash\n# ENV PYTHON_CONFIGURE_OPTS --enable-shared\n\nRUN echo 'export PATH=\"$HOME/.pyenv/bin:$PATH\"'>> $HOME/.bashrc \\\n    && echo 'eval \"$(pyenv init -)\"' >> $HOME/.bashrc \\\n    && echo 'eval \"$(pyenv virtualenv-init -)\"' >> $HOME/.bashrc \\\n    && echo 'eval \"$(pyenv init --path)\"' >> $HOME/.bashrc\nRUN source $HOME/.bashrc \\\n    && export CPPFLAGS=\"-I/usr/include/openssl11\" \\\n    && export LDFLAGS=\"-L/usr/lib64/openssl11 -lssl -lcrypto\" \\\n    && export PATH=/usr/local/openssl/bin:$PATH \\\n    && export LD_LIBRARY_PATH=/usr/local/openssl/lib:$LD_LIBRARY_PATH \\\n    && pyenv install ${OPENPYPE_PYTHON_VERSION}\n\nCOPY . /opt/openpype/\nRUN rm -rf /openpype/.poetry || echo \"No Poetry installed yet.\"\n# USER root\n# RUN chown -R pype /opt/openpype\nRUN chmod +x /opt/openpype/tools/create_env.sh && chmod +x /opt/openpype/tools/build.sh\n\n# USER pype\n\nWORKDIR /opt/openpype\n\nRUN cd /opt/openpype \\\n    && source $HOME/.bashrc \\\n    && pyenv local ${OPENPYPE_PYTHON_VERSION}\n\nRUN source $HOME/.bashrc \\\n    && ./tools/create_env.sh\n\nRUN source $HOME/.bashrc \\\n    && ./tools/fetch_thirdparty_libs.sh\n\nRUN echo 'export PYTHONPATH=\"/opt/openpype/vendor/python:$PYTHONPATH\"'>> $HOME/.bashrc\nRUN source $HOME/.bashrc \\\n    && bash ./tools/build.sh\n\nRUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.9/lib \\\n    && cp /usr/lib64/openssl11/libssl* ./build/exe.linux-x86_64-3.9/lib \\\n    && cp /usr/lib64/openssl11/libcrypto* ./build/exe.linux-x86_64-3.9/lib \\\n    && ln -sr ./build/exe.linux-x86_64-3.9/lib/libssl.so ./build/exe.linux-x86_64-3.9/lib/libssl.1.1.so \\\n    && ln -sr ./build/exe.linux-x86_64-3.9/lib/libcrypto.so ./build/exe.linux-x86_64-3.9/lib/libcrypto.1.1.so \\\n    && cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.9/lib \\\n    && cp /usr/lib64/libxcb* ./build/exe.linux-x86_64-3.9/vendor/python/PySide2/Qt/lib\n\nRUN cd /opt/openpype \\\n    rm -rf ./vendor/bin\n"
  },
  {
    "path": "Dockerfile.debian",
    "content": "# Build Pype docker image\nFROM debian:bullseye AS builder\nARG OPENPYPE_PYTHON_VERSION=3.9.12\nARG BUILD_DATE\nARG VERSION\n\nLABEL maintainer=\"info@openpype.io\"\nLABEL description=\"Docker Image to build and run OpenPype under Ubuntu 20.04\"\nLABEL org.opencontainers.image.name=\"pypeclub/openpype\"\nLABEL org.opencontainers.image.title=\"OpenPype Docker Image\"\nLABEL org.opencontainers.image.url=\"https://openpype.io/\"\nLABEL org.opencontainers.image.source=\"https://github.com/pypeclub/OpenPype\"\nLABEL org.opencontainers.image.documentation=\"https://openpype.io/docs/system_introduction\"\nLABEL org.opencontainers.image.created=$BUILD_DATE\nLABEL org.opencontainers.image.version=$VERSION\n\nUSER root\n\nARG DEBIAN_FRONTEND=noninteractive\n\n# update base\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n    ca-certificates \\\n    bash \\\n    git \\\n    cmake \\\n    make \\\n    curl \\\n    wget \\\n    build-essential \\\n    libssl-dev \\\n    zlib1g-dev \\\n    libbz2-dev \\\n    libreadline-dev \\\n    libsqlite3-dev \\\n    llvm \\\n    libncursesw5-dev \\\n    xz-utils \\\n    tk-dev \\\n    libxml2-dev \\\n    libxmlsec1-dev \\\n    libffi-dev \\\n    liblzma-dev \\\n    patchelf\n\nSHELL [\"/bin/bash\", \"-c\"]\n\n\nRUN mkdir /opt/openpype\n\n# download and install pyenv\nRUN curl https://pyenv.run | bash \\\n    && echo 'export PATH=\"$HOME/.pyenv/bin:$PATH\"'>> $HOME/init_pyenv.sh \\\n    && echo 'eval \"$(pyenv init -)\"' >> $HOME/init_pyenv.sh \\\n    && echo 'eval \"$(pyenv virtualenv-init -)\"' >> $HOME/init_pyenv.sh \\\n    && echo 'eval \"$(pyenv init --path)\"' >> $HOME/init_pyenv.sh\n\n# install python with pyenv\nRUN source $HOME/init_pyenv.sh \\\n    && pyenv install ${OPENPYPE_PYTHON_VERSION}\n\nCOPY . /opt/openpype/\n\nRUN chmod +x /opt/openpype/tools/create_env.sh && chmod +x /opt/openpype/tools/build.sh\n\nWORKDIR /opt/openpype\n\n# set local python version\nRUN cd /opt/openpype \\\n    && source $HOME/init_pyenv.sh \\\n    && pyenv local ${OPENPYPE_PYTHON_VERSION}\n\n# fetch third party tools/libraries\nRUN source $HOME/init_pyenv.sh \\\n    && ./tools/create_env.sh \\\n    && ./tools/fetch_thirdparty_libs.sh\n\n# build openpype\nRUN source $HOME/init_pyenv.sh \\\n    && bash ./tools/build.sh\n"
  },
  {
    "path": "HISTORY.md",
    "content": "# Changelog\n\n## [3.15.0](https://github.com/ynput/OpenPype/tree/3.15.0)\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.14.10...3.15.0)\n\n**Deprecated:**\n\n- General: Fill default values of new publish template profiles [\\#4245](https://github.com/ynput/OpenPype/pull/4245)\n\n### 📖 Documentation\n\n- documentation: Split tools into separate entries [\\#4342](https://github.com/ynput/OpenPype/pull/4342)\n- Documentation: Fix harmony docs [\\#4301](https://github.com/ynput/OpenPype/pull/4301)\n- Remove staging logic set by OpenPype version [\\#3979](https://github.com/ynput/OpenPype/pull/3979)\n\n**🆕 New features**\n\n- General: Push to studio library [\\#4284](https://github.com/ynput/OpenPype/pull/4284)\n- Colorspace Management and Distribution [\\#4195](https://github.com/ynput/OpenPype/pull/4195)\n- Nuke: refactor to latest publisher workfow [\\#4006](https://github.com/ynput/OpenPype/pull/4006)\n- Update to Python 3.9 [\\#3546](https://github.com/ynput/OpenPype/pull/3546)\n\n**🚀 Enhancements**\n\n- Unreal: Don't use mongo queries in 'ExistingLayoutLoader' [\\#4356](https://github.com/ynput/OpenPype/pull/4356)\n- General: Loader and Creator plugins can be disabled [\\#4310](https://github.com/ynput/OpenPype/pull/4310)\n- General: Unbind poetry version [\\#4306](https://github.com/ynput/OpenPype/pull/4306)\n- General: Enhanced enum def items [\\#4295](https://github.com/ynput/OpenPype/pull/4295)\n- Git: add pre-commit hooks [\\#4289](https://github.com/ynput/OpenPype/pull/4289)\n- Tray Publisher: Improve Online family functionality [\\#4263](https://github.com/ynput/OpenPype/pull/4263)\n- General: Update MacOs to PySide6 [\\#4255](https://github.com/ynput/OpenPype/pull/4255)\n- Build: update to Gazu in toml [\\#4208](https://github.com/ynput/OpenPype/pull/4208)\n- Global: adding imageio to settings [\\#4158](https://github.com/ynput/OpenPype/pull/4158)\n- Blender: added project settings for validator no colons in name [\\#4149](https://github.com/ynput/OpenPype/pull/4149)\n- Dockerfile for Debian Bullseye [\\#4108](https://github.com/ynput/OpenPype/pull/4108)\n- AfterEffects: publish multiple compositions [\\#4092](https://github.com/ynput/OpenPype/pull/4092)\n- AfterEffects: make new publisher default [\\#4056](https://github.com/ynput/OpenPype/pull/4056)\n- Photoshop: make new publisher default [\\#4051](https://github.com/ynput/OpenPype/pull/4051)\n- Feature/multiverse [\\#4046](https://github.com/ynput/OpenPype/pull/4046)\n- Tests: add support for deadline for automatic tests [\\#3989](https://github.com/ynput/OpenPype/pull/3989)\n- Add version to shortcut name [\\#3906](https://github.com/ynput/OpenPype/pull/3906)\n- TrayPublisher: Removed from experimental tools [\\#3667](https://github.com/ynput/OpenPype/pull/3667)\n\n**🐛 Bug fixes**\n\n- change 3.7 to 3.9 in folder name [\\#4354](https://github.com/ynput/OpenPype/pull/4354)\n- PushToProject: Fix hierarchy of project change [\\#4350](https://github.com/ynput/OpenPype/pull/4350)\n- Fix photoshop workfile save-as [\\#4347](https://github.com/ynput/OpenPype/pull/4347)\n- Nuke Input process node sourcing improvements [\\#4341](https://github.com/ynput/OpenPype/pull/4341)\n- New publisher: Some validation plugin tweaks [\\#4339](https://github.com/ynput/OpenPype/pull/4339)\n- Harmony: fix unable to change workfile on Mac [\\#4334](https://github.com/ynput/OpenPype/pull/4334)\n- Global: fixing in-place source publishing for editorial [\\#4333](https://github.com/ynput/OpenPype/pull/4333)\n- General: Use class constants of QMessageBox [\\#4332](https://github.com/ynput/OpenPype/pull/4332)\n- TVPaint: Fix plugin for TVPaint 11.7 [\\#4328](https://github.com/ynput/OpenPype/pull/4328)\n- Exctract OTIO review has improved quality [\\#4325](https://github.com/ynput/OpenPype/pull/4325)\n- Ftrack: fix typos causing bugs in sync [\\#4322](https://github.com/ynput/OpenPype/pull/4322)\n- General: Python 2 compatibility of instance collector [\\#4320](https://github.com/ynput/OpenPype/pull/4320)\n- Slack: user groups speedup [\\#4318](https://github.com/ynput/OpenPype/pull/4318)\n- Maya: Bug - Multiverse extractor executed on plain animation family [\\#4315](https://github.com/ynput/OpenPype/pull/4315)\n- Fix run\\_documentation.ps1 [\\#4312](https://github.com/ynput/OpenPype/pull/4312)\n- Nuke: new creators fixes [\\#4308](https://github.com/ynput/OpenPype/pull/4308)\n- General: missing comment on standalone and tray publisher [\\#4303](https://github.com/ynput/OpenPype/pull/4303)\n- AfterEffects: Fix for audio from mp4 layer [\\#4296](https://github.com/ynput/OpenPype/pull/4296)\n- General: Update gazu in poetry lock [\\#4247](https://github.com/ynput/OpenPype/pull/4247)\n- Bug: Fixing version detection and filtering in Igniter [\\#3914](https://github.com/ynput/OpenPype/pull/3914)\n- Bug: Create missing version dir [\\#3903](https://github.com/ynput/OpenPype/pull/3903)\n\n**🔀 Refactored code**\n\n- Remove redundant export\\_alembic method. [\\#4293](https://github.com/ynput/OpenPype/pull/4293)\n- Igniter: Use qtpy modules instead of Qt [\\#4237](https://github.com/ynput/OpenPype/pull/4237)\n\n**Merged pull requests:**\n\n- Sort families by alphabetical order in the Create plugin [\\#4346](https://github.com/ynput/OpenPype/pull/4346)\n- Global: Validate unique subsets [\\#4336](https://github.com/ynput/OpenPype/pull/4336)\n- Maya: Collect instances preserve handles even if frameStart + frameEnd matches context [\\#3437](https://github.com/ynput/OpenPype/pull/3437)\n\n\n## [3.14.10](https://github.com/ynput/OpenPype/tree/3.14.10)\n\n[Full Changelog](https://github.com/ynput/OpenPype/compare/3.14.9...3.14.10)\n\n**🆕 New features**\n\n- Global | Nuke: Creator placeholders in workfile template builder [\\#4266](https://github.com/ynput/OpenPype/pull/4266)\n- Slack: Added dynamic message [\\#4265](https://github.com/ynput/OpenPype/pull/4265)\n- Blender: Workfile Loader [\\#4234](https://github.com/ynput/OpenPype/pull/4234)\n- Unreal: Publishing and Loading for UAssets [\\#4198](https://github.com/ynput/OpenPype/pull/4198)\n- Publish: register publishes without copying them [\\#4157](https://github.com/ynput/OpenPype/pull/4157)\n\n**🚀 Enhancements**\n\n- General: Added install method with docstring to HostBase [\\#4298](https://github.com/ynput/OpenPype/pull/4298)\n- Traypublisher: simple editorial multiple edl [\\#4248](https://github.com/ynput/OpenPype/pull/4248)\n- General: Extend 'IPluginPaths' to have more available methods [\\#4214](https://github.com/ynput/OpenPype/pull/4214)\n- Refactorization of folder coloring [\\#4211](https://github.com/ynput/OpenPype/pull/4211)\n- Flame - loading multilayer with controlled layer names [\\#4204](https://github.com/ynput/OpenPype/pull/4204)\n\n**🐛 Bug fixes**\n\n- Unreal: fix missing `maintained_selection` call [\\#4300](https://github.com/ynput/OpenPype/pull/4300)\n- Ftrack: Fix receive of host ip on MacOs [\\#4288](https://github.com/ynput/OpenPype/pull/4288)\n- SiteSync: sftp connection failing when shouldnt be tested [\\#4278](https://github.com/ynput/OpenPype/pull/4278)\n- Deadline: fix default value for passing mongo url [\\#4275](https://github.com/ynput/OpenPype/pull/4275)\n- Scene Manager: Fix variable name [\\#4268](https://github.com/ynput/OpenPype/pull/4268)\n- Slack: notification fails because of missing published path [\\#4264](https://github.com/ynput/OpenPype/pull/4264)\n- hiero: creator gui with min max  [\\#4257](https://github.com/ynput/OpenPype/pull/4257)\n- NiceCheckbox: Fix checker positioning in Python 2 [\\#4253](https://github.com/ynput/OpenPype/pull/4253)\n- Publisher: Fix 'CreatorType' not equal for Python 2 DCCs [\\#4249](https://github.com/ynput/OpenPype/pull/4249)\n- Deadline: fix dependencies [\\#4242](https://github.com/ynput/OpenPype/pull/4242)\n- Houdini: hotfix instance data access [\\#4236](https://github.com/ynput/OpenPype/pull/4236)\n- bugfix/image plane load error [\\#4222](https://github.com/ynput/OpenPype/pull/4222)\n- Hiero: thumbnail from multilayer exr [\\#4209](https://github.com/ynput/OpenPype/pull/4209)\n\n**🔀 Refactored code**\n\n- Resolve: Use qtpy in Resolve [\\#4254](https://github.com/ynput/OpenPype/pull/4254)\n- Houdini: Use qtpy in Houdini [\\#4252](https://github.com/ynput/OpenPype/pull/4252)\n- Max: Use qtpy in Max [\\#4251](https://github.com/ynput/OpenPype/pull/4251)\n- Maya: Use qtpy in Maya [\\#4250](https://github.com/ynput/OpenPype/pull/4250)\n- Hiero: Use qtpy in Hiero [\\#4240](https://github.com/ynput/OpenPype/pull/4240)\n- Nuke: Use qtpy in Nuke [\\#4239](https://github.com/ynput/OpenPype/pull/4239)\n- Flame: Use qtpy in flame [\\#4238](https://github.com/ynput/OpenPype/pull/4238)\n- General: Legacy io not used in global plugins [\\#4134](https://github.com/ynput/OpenPype/pull/4134)\n\n**Merged pull requests:**\n\n- Bump json5 from 1.0.1 to 1.0.2 in /website [\\#4292](https://github.com/ynput/OpenPype/pull/4292)\n- Maya: Fix validate frame range repair + fix create render with deadline disabled [\\#4279](https://github.com/ynput/OpenPype/pull/4279)\n\n\n## [3.14.9](https://github.com/pypeclub/OpenPype/tree/3.14.9)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.8...3.14.9)\n\n### 📖 Documentation\n\n- Documentation: Testing on Deadline [\\#4185](https://github.com/pypeclub/OpenPype/pull/4185)\n- Consistent Python version [\\#4160](https://github.com/pypeclub/OpenPype/pull/4160)\n\n**🆕 New features**\n\n- Feature/op 4397 gl tf extractor for maya [\\#4192](https://github.com/pypeclub/OpenPype/pull/4192)\n- Maya: Extractor for Unreal SkeletalMesh [\\#4174](https://github.com/pypeclub/OpenPype/pull/4174)\n- 3dsmax: integration [\\#4168](https://github.com/pypeclub/OpenPype/pull/4168)\n- Blender: Extract Alembic Animations [\\#4128](https://github.com/pypeclub/OpenPype/pull/4128)\n- Unreal: Load Alembic Animations [\\#4127](https://github.com/pypeclub/OpenPype/pull/4127)\n\n**🚀 Enhancements**\n\n- Houdini: Use new interface class name for publish host [\\#4220](https://github.com/pypeclub/OpenPype/pull/4220)\n- General: Default command for headless mode is interactive [\\#4203](https://github.com/pypeclub/OpenPype/pull/4203)\n- Maya: Enhanced ASS publishing [\\#4196](https://github.com/pypeclub/OpenPype/pull/4196)\n- Feature/op 3924 implement ass extractor [\\#4188](https://github.com/pypeclub/OpenPype/pull/4188)\n- File transactions: Source path is destination path [\\#4184](https://github.com/pypeclub/OpenPype/pull/4184)\n- Deadline: improve environment processing [\\#4182](https://github.com/pypeclub/OpenPype/pull/4182)\n- General: Comment per instance in Publisher [\\#4178](https://github.com/pypeclub/OpenPype/pull/4178)\n- Ensure Mongo database directory exists in Windows. [\\#4166](https://github.com/pypeclub/OpenPype/pull/4166)\n- Note about unrestricted execution on Windows. [\\#4161](https://github.com/pypeclub/OpenPype/pull/4161)\n- Maya: Enable thumbnail transparency on extraction. [\\#4147](https://github.com/pypeclub/OpenPype/pull/4147)\n- Maya: Disable viewport Pan/Zoom on playblast extraction. [\\#4146](https://github.com/pypeclub/OpenPype/pull/4146)\n- Maya: Optional viewport refresh on pointcache extraction [\\#4144](https://github.com/pypeclub/OpenPype/pull/4144)\n- CelAction: refactory integration to current openpype [\\#4140](https://github.com/pypeclub/OpenPype/pull/4140)\n- Maya: create and publish bounding box geometry [\\#4131](https://github.com/pypeclub/OpenPype/pull/4131)\n- Changed the UOpenPypePublishInstance to use the UDataAsset class [\\#4124](https://github.com/pypeclub/OpenPype/pull/4124)\n- General: Collection Audio speed up [\\#4110](https://github.com/pypeclub/OpenPype/pull/4110)\n- Maya: keep existing AOVs when creating render instance [\\#4087](https://github.com/pypeclub/OpenPype/pull/4087)\n- General: Oiio conversion multipart fix [\\#4060](https://github.com/pypeclub/OpenPype/pull/4060)\n\n**🐛 Bug fixes**\n\n- Publisher: Signal type issues in Python 2 DCCs [\\#4230](https://github.com/pypeclub/OpenPype/pull/4230)\n- Blender: Fix Layout Family Versioning [\\#4228](https://github.com/pypeclub/OpenPype/pull/4228)\n- Blender: Fix Create Camera \"Use selection\" [\\#4226](https://github.com/pypeclub/OpenPype/pull/4226)\n- TrayPublisher - join needs list [\\#4224](https://github.com/pypeclub/OpenPype/pull/4224)\n- General: Event callbacks pass event to callbacks as expected [\\#4210](https://github.com/pypeclub/OpenPype/pull/4210)\n- Build:Revert .toml update of Gazu [\\#4207](https://github.com/pypeclub/OpenPype/pull/4207)\n- Nuke: fixed imageio node overrides subset filter [\\#4202](https://github.com/pypeclub/OpenPype/pull/4202)\n- Maya: pointcache [\\#4201](https://github.com/pypeclub/OpenPype/pull/4201)\n- Unreal: Support for Unreal Engine 5.1 [\\#4199](https://github.com/pypeclub/OpenPype/pull/4199)\n- General: Integrate thumbnail looks for thumbnail to multiple places [\\#4181](https://github.com/pypeclub/OpenPype/pull/4181)\n- Various minor bugfixes [\\#4172](https://github.com/pypeclub/OpenPype/pull/4172)\n- Nuke/Hiero: Remove tkinter library paths before launch [\\#4171](https://github.com/pypeclub/OpenPype/pull/4171)\n- Flame: vertical alignment of layers [\\#4169](https://github.com/pypeclub/OpenPype/pull/4169)\n- Nuke: correct detection of viewer and display [\\#4165](https://github.com/pypeclub/OpenPype/pull/4165)\n- Settings UI: Don't create QApplication if already exists [\\#4156](https://github.com/pypeclub/OpenPype/pull/4156)\n- General: Extract review handle start offset of sequences [\\#4152](https://github.com/pypeclub/OpenPype/pull/4152)\n- Maya: Maintain time connections on Alembic update. [\\#4143](https://github.com/pypeclub/OpenPype/pull/4143)\n\n**🔀 Refactored code**\n\n- General: Use qtpy in modules and hosts UIs which are running in OpenPype process [\\#4225](https://github.com/pypeclub/OpenPype/pull/4225)\n- Tools: Use qtpy instead of Qt in standalone tools [\\#4223](https://github.com/pypeclub/OpenPype/pull/4223)\n- General: Use qtpy in settings UI [\\#4215](https://github.com/pypeclub/OpenPype/pull/4215)\n\n**Merged pull requests:**\n\n- layout publish more than one container issue [\\#4098](https://github.com/pypeclub/OpenPype/pull/4098)\n\n## [3.14.8](https://github.com/pypeclub/OpenPype/tree/3.14.8)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.7...3.14.8)\n\n**🚀 Enhancements**\n\n- General: Refactored extract hierarchy plugin [\\#4139](https://github.com/pypeclub/OpenPype/pull/4139)\n- General: Find executable enhancement [\\#4137](https://github.com/pypeclub/OpenPype/pull/4137)\n- Ftrack: Reset session before instance processing [\\#4129](https://github.com/pypeclub/OpenPype/pull/4129)\n- Ftrack: Editorial asset sync issue [\\#4126](https://github.com/pypeclub/OpenPype/pull/4126)\n- Deadline: Build version resolving [\\#4115](https://github.com/pypeclub/OpenPype/pull/4115)\n- Houdini: New Publisher [\\#3046](https://github.com/pypeclub/OpenPype/pull/3046)\n- Fix: Standalone Publish Directories [\\#4148](https://github.com/pypeclub/OpenPype/pull/4148)\n\n**🐛 Bug fixes**\n\n- Ftrack: Fix occational double parents issue [\\#4153](https://github.com/pypeclub/OpenPype/pull/4153)\n- General: Maketx executable issue [\\#4136](https://github.com/pypeclub/OpenPype/pull/4136)\n- Maya: Looks - add all connections [\\#4135](https://github.com/pypeclub/OpenPype/pull/4135)\n- General: Fix variable check in collect anatomy instance data [\\#4117](https://github.com/pypeclub/OpenPype/pull/4117)\n\n## [3.14.7](https://github.com/pypeclub/OpenPype/tree/3.14.7)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.6...3.14.7)\n\n**🆕 New features**\n\n- Hiero: loading effect family to timeline [\\#4055](https://github.com/pypeclub/OpenPype/pull/4055)\n\n**🚀 Enhancements**\n\n- Photoshop: bug with pop-up window on Instance Creator [\\#4121](https://github.com/pypeclub/OpenPype/pull/4121)\n- Publisher: Open on specific tab [\\#4120](https://github.com/pypeclub/OpenPype/pull/4120)\n- Publisher: Hide unknown publish values [\\#4116](https://github.com/pypeclub/OpenPype/pull/4116)\n- Ftrack: Event server status give more information about version locations [\\#4112](https://github.com/pypeclub/OpenPype/pull/4112)\n- General: Allow higher numbers in frames and clips [\\#4101](https://github.com/pypeclub/OpenPype/pull/4101)\n- Publisher: Settings for validate frame range [\\#4097](https://github.com/pypeclub/OpenPype/pull/4097)\n- Publisher: Ignore escape button [\\#4090](https://github.com/pypeclub/OpenPype/pull/4090)\n- Flame: Loading clip with native colorspace resolved from mapping [\\#4079](https://github.com/pypeclub/OpenPype/pull/4079)\n- General: Extract review single frame output [\\#4064](https://github.com/pypeclub/OpenPype/pull/4064)\n- Publisher: Prepared common function for instance data cache [\\#4063](https://github.com/pypeclub/OpenPype/pull/4063)\n- Publisher: Easy access to publish page from create page [\\#4058](https://github.com/pypeclub/OpenPype/pull/4058)\n- General/TVPaint: Attribute defs dialog [\\#4052](https://github.com/pypeclub/OpenPype/pull/4052)\n- Publisher: Better reset defer [\\#4048](https://github.com/pypeclub/OpenPype/pull/4048)\n- Publisher: Add thumbnail sources [\\#4042](https://github.com/pypeclub/OpenPype/pull/4042)\n\n**🐛 Bug fixes**\n\n- General: Move default settings for template name [\\#4119](https://github.com/pypeclub/OpenPype/pull/4119)\n- Slack: notification fail in new tray publisher [\\#4118](https://github.com/pypeclub/OpenPype/pull/4118)\n- Nuke: loaded nodes set to first tab [\\#4114](https://github.com/pypeclub/OpenPype/pull/4114)\n- Nuke: load image first frame [\\#4113](https://github.com/pypeclub/OpenPype/pull/4113)\n- Files Widget: Ignore case sensitivity of extensions [\\#4096](https://github.com/pypeclub/OpenPype/pull/4096)\n- Webpublisher: extension is lowercased in Setting and in uploaded files [\\#4095](https://github.com/pypeclub/OpenPype/pull/4095)\n- Publish Report Viewer: Fix small bugs [\\#4086](https://github.com/pypeclub/OpenPype/pull/4086)\n- Igniter: fix regex to match semver better [\\#4085](https://github.com/pypeclub/OpenPype/pull/4085)\n- Maya: aov filtering [\\#4083](https://github.com/pypeclub/OpenPype/pull/4083)\n- Flame/Flare: Loading to multiple batches [\\#4080](https://github.com/pypeclub/OpenPype/pull/4080)\n- hiero: creator from settings with set maximum [\\#4077](https://github.com/pypeclub/OpenPype/pull/4077)\n- Nuke: resolve hashes in file name only for frame token [\\#4074](https://github.com/pypeclub/OpenPype/pull/4074)\n- Publisher: Fix cache of asset docs [\\#4070](https://github.com/pypeclub/OpenPype/pull/4070)\n- Webpublisher: cleanup wp extract thumbnail [\\#4067](https://github.com/pypeclub/OpenPype/pull/4067)\n- Settings UI: Locked setting can't bypass lock [\\#4066](https://github.com/pypeclub/OpenPype/pull/4066)\n- Loader: Fix comparison of repre name [\\#4053](https://github.com/pypeclub/OpenPype/pull/4053)\n- Deadline: Extract environment subprocess failure [\\#4050](https://github.com/pypeclub/OpenPype/pull/4050)\n\n**🔀 Refactored code**\n\n- General: Collect entities plugin minor changes [\\#4089](https://github.com/pypeclub/OpenPype/pull/4089)\n- General: Direct interfaces import [\\#4065](https://github.com/pypeclub/OpenPype/pull/4065)\n\n**Merged pull requests:**\n\n- Bump loader-utils from 1.4.1 to 1.4.2 in /website [\\#4100](https://github.com/pypeclub/OpenPype/pull/4100)\n- Online family for Tray Publisher [\\#4093](https://github.com/pypeclub/OpenPype/pull/4093)\n- Bump loader-utils from 1.4.0 to 1.4.1 in /website [\\#4081](https://github.com/pypeclub/OpenPype/pull/4081)\n- remove underscore from subset name [\\#4059](https://github.com/pypeclub/OpenPype/pull/4059)\n- Alembic Loader as Arnold Standin [\\#4047](https://github.com/pypeclub/OpenPype/pull/4047)\n\n## [3.14.6](https://github.com/pypeclub/OpenPype/tree/3.14.6)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...3.14.6)\n\n### 📖 Documentation\n\n- Documentation: Minor updates to dev\\_requirements.md [\\#4025](https://github.com/pypeclub/OpenPype/pull/4025)\n\n**🆕 New features**\n\n- Nuke: add 13.2 variant [\\#4041](https://github.com/pypeclub/OpenPype/pull/4041)\n\n**🚀 Enhancements**\n\n- Publish Report Viewer: Store reports locally on machine [\\#4040](https://github.com/pypeclub/OpenPype/pull/4040)\n- General: More specific error in burnins script [\\#4026](https://github.com/pypeclub/OpenPype/pull/4026)\n- General: Extract review does not crash with old settings overrides [\\#4023](https://github.com/pypeclub/OpenPype/pull/4023)\n- Publisher: Convertors for legacy instances [\\#4020](https://github.com/pypeclub/OpenPype/pull/4020)\n- workflows: adding milestone creator and assigner [\\#4018](https://github.com/pypeclub/OpenPype/pull/4018)\n- Publisher: Catch creator errors [\\#4015](https://github.com/pypeclub/OpenPype/pull/4015)\n\n**🐛 Bug fixes**\n\n- Hiero - effect collection fixes [\\#4038](https://github.com/pypeclub/OpenPype/pull/4038)\n- Nuke - loader clip correct hash conversion in path [\\#4037](https://github.com/pypeclub/OpenPype/pull/4037)\n- Maya: Soft fail when applying capture preset [\\#4034](https://github.com/pypeclub/OpenPype/pull/4034)\n- Igniter: handle missing directory [\\#4032](https://github.com/pypeclub/OpenPype/pull/4032)\n- StandalonePublisher: Fix thumbnail publishing [\\#4029](https://github.com/pypeclub/OpenPype/pull/4029)\n- Experimental Tools: Fix publisher import [\\#4027](https://github.com/pypeclub/OpenPype/pull/4027)\n- Houdini: fix wrong path in ASS loader [\\#4016](https://github.com/pypeclub/OpenPype/pull/4016)\n\n**🔀 Refactored code**\n\n- General: Import lib functions from lib [\\#4017](https://github.com/pypeclub/OpenPype/pull/4017)\n\n## [3.14.5](https://github.com/pypeclub/OpenPype/tree/3.14.5) (2022-10-24)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...3.14.5)\n\n**🚀 Enhancements**\n\n- Maya: add OBJ extractor to model family [\\#4021](https://github.com/pypeclub/OpenPype/pull/4021)\n- Publish report viewer tool [\\#4010](https://github.com/pypeclub/OpenPype/pull/4010)\n- Nuke | Global: adding custom tags representation filtering [\\#4009](https://github.com/pypeclub/OpenPype/pull/4009)\n- Publisher: Create context has shared data for collection phase [\\#3995](https://github.com/pypeclub/OpenPype/pull/3995)\n- Resolve: updating to v18 compatibility [\\#3986](https://github.com/pypeclub/OpenPype/pull/3986)\n\n**🐛 Bug fixes**\n\n- TrayPublisher: Fix missing argument [\\#4019](https://github.com/pypeclub/OpenPype/pull/4019)\n- General: Fix python 2 compatibility of ffmpeg and oiio tools discovery [\\#4011](https://github.com/pypeclub/OpenPype/pull/4011)\n\n**🔀 Refactored code**\n\n- Maya: Removed unused imports [\\#4008](https://github.com/pypeclub/OpenPype/pull/4008)\n- Unreal: Fix import of moved function [\\#4007](https://github.com/pypeclub/OpenPype/pull/4007)\n- Houdini: Change import of RepairAction [\\#4005](https://github.com/pypeclub/OpenPype/pull/4005)\n- Nuke/Hiero: Refactor openpype.api imports [\\#4000](https://github.com/pypeclub/OpenPype/pull/4000)\n- TVPaint: Defined with HostBase [\\#3994](https://github.com/pypeclub/OpenPype/pull/3994)\n\n**Merged pull requests:**\n\n- Unreal: Remove redundant Creator stub [\\#4012](https://github.com/pypeclub/OpenPype/pull/4012)\n- Unreal: add `uproject` extension to Unreal project template [\\#4004](https://github.com/pypeclub/OpenPype/pull/4004)\n- Unreal: fix order of includes [\\#4002](https://github.com/pypeclub/OpenPype/pull/4002)\n- Fusion: Implement backwards compatibility \\(+/- Fusion 17.2\\) [\\#3958](https://github.com/pypeclub/OpenPype/pull/3958)\n\n## [3.14.4](https://github.com/pypeclub/OpenPype/tree/3.14.4) (2022-10-19)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...3.14.4)\n\n**🆕 New features**\n\n- Webpublisher: use max next published version number  for all items in batch [\\#3961](https://github.com/pypeclub/OpenPype/pull/3961)\n- General: Control Thumbnail integration via explicit configuration profiles [\\#3951](https://github.com/pypeclub/OpenPype/pull/3951)\n\n**🚀 Enhancements**\n\n- Publisher: Multiselection in card view [\\#3993](https://github.com/pypeclub/OpenPype/pull/3993)\n- TrayPublisher: Original Basename cause crash too early [\\#3990](https://github.com/pypeclub/OpenPype/pull/3990)\n- Tray Publisher: add `originalBasename` data to simple creators [\\#3988](https://github.com/pypeclub/OpenPype/pull/3988)\n- General: Custom paths to ffmpeg and OpenImageIO tools [\\#3982](https://github.com/pypeclub/OpenPype/pull/3982)\n- Integrate: Preserve existing subset group if instance does not set it for new version [\\#3976](https://github.com/pypeclub/OpenPype/pull/3976)\n- Publisher: Prepare publisher controller  for remote publishing [\\#3972](https://github.com/pypeclub/OpenPype/pull/3972)\n- Maya: new style dataclasses in maya deadline submitter plugin [\\#3968](https://github.com/pypeclub/OpenPype/pull/3968)\n- Maya: Define preffered Qt bindings for Qt.py and qtpy [\\#3963](https://github.com/pypeclub/OpenPype/pull/3963)\n- Settings: Move imageio from project anatomy to project settings \\[pypeclub\\] [\\#3959](https://github.com/pypeclub/OpenPype/pull/3959)\n- TrayPublisher: Extract thumbnail for other families [\\#3952](https://github.com/pypeclub/OpenPype/pull/3952)\n- Publisher: Pass instance to subset name method on update [\\#3949](https://github.com/pypeclub/OpenPype/pull/3949)\n- General: Set root environments before DCC launch [\\#3947](https://github.com/pypeclub/OpenPype/pull/3947)\n- Refactor: changed legacy way to update database for Hero version integrate [\\#3941](https://github.com/pypeclub/OpenPype/pull/3941)\n- Maya: Moved plugin from global to maya [\\#3939](https://github.com/pypeclub/OpenPype/pull/3939)\n- Publisher: Create dialog is part of main window [\\#3936](https://github.com/pypeclub/OpenPype/pull/3936)\n- Fusion: Implement Alembic and FBX mesh loader [\\#3927](https://github.com/pypeclub/OpenPype/pull/3927)\n\n**🐛 Bug fixes**\n\n- TrayPublisher: Disable sequences in batch mov creator [\\#3996](https://github.com/pypeclub/OpenPype/pull/3996)\n- Fix - tags might be missing on representation [\\#3985](https://github.com/pypeclub/OpenPype/pull/3985)\n- Resolve: Fix usage of functions from lib [\\#3983](https://github.com/pypeclub/OpenPype/pull/3983)\n- Maya: remove invalid prefix token for non-multipart outputs [\\#3981](https://github.com/pypeclub/OpenPype/pull/3981)\n- Ftrack: Fix schema cache for Python 2 [\\#3980](https://github.com/pypeclub/OpenPype/pull/3980)\n- Maya: add object to attr.s declaration [\\#3973](https://github.com/pypeclub/OpenPype/pull/3973)\n- Maya: Deadline OutputFilePath hack regression for Renderman [\\#3950](https://github.com/pypeclub/OpenPype/pull/3950)\n- Houdini: Fix validate workfile paths for non-parm file references [\\#3948](https://github.com/pypeclub/OpenPype/pull/3948)\n- Photoshop: missed sync published version of workfile with workfile [\\#3946](https://github.com/pypeclub/OpenPype/pull/3946)\n- Maya: Set default value for RenderSetupIncludeLights option [\\#3944](https://github.com/pypeclub/OpenPype/pull/3944)\n- Maya: fix regression of Renderman Deadline hack [\\#3943](https://github.com/pypeclub/OpenPype/pull/3943)\n- Kitsu: 2 fixes, nb\\_frames and Shot type error [\\#3940](https://github.com/pypeclub/OpenPype/pull/3940)\n- Tray: Change order of attribute changes [\\#3938](https://github.com/pypeclub/OpenPype/pull/3938)\n- AttributeDefs: Fix crashing multivalue of files widget [\\#3937](https://github.com/pypeclub/OpenPype/pull/3937)\n- General: Fix links query on hero version [\\#3900](https://github.com/pypeclub/OpenPype/pull/3900)\n- Publisher: Files Drag n Drop cleanup [\\#3888](https://github.com/pypeclub/OpenPype/pull/3888)\n\n**🔀 Refactored code**\n\n- Flame: Import lib functions from lib [\\#3992](https://github.com/pypeclub/OpenPype/pull/3992)\n- General: Fix deprecated warning in legacy creator [\\#3978](https://github.com/pypeclub/OpenPype/pull/3978)\n- Blender: Remove openpype api imports [\\#3977](https://github.com/pypeclub/OpenPype/pull/3977)\n- General: Use direct import of resources [\\#3964](https://github.com/pypeclub/OpenPype/pull/3964)\n- General: Direct settings imports [\\#3934](https://github.com/pypeclub/OpenPype/pull/3934)\n- General: import 'Logger' from 'openpype.lib' [\\#3926](https://github.com/pypeclub/OpenPype/pull/3926)\n- General: Remove deprecated functions from lib [\\#3907](https://github.com/pypeclub/OpenPype/pull/3907)\n\n**Merged pull requests:**\n\n- Maya + Yeti: Load Yeti Cache fix frame number recognition [\\#3942](https://github.com/pypeclub/OpenPype/pull/3942)\n- Fusion: Implement callbacks to Fusion's event system thread [\\#3928](https://github.com/pypeclub/OpenPype/pull/3928)\n- Photoshop: create single frame image in Ftrack as review [\\#3908](https://github.com/pypeclub/OpenPype/pull/3908)\n\n## [3.14.3](https://github.com/pypeclub/OpenPype/tree/3.14.3) (2022-10-03)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.2...3.14.3)\n\n**🚀 Enhancements**\n\n- Publisher: Enhancement proposals [\\#3897](https://github.com/pypeclub/OpenPype/pull/3897)\n\n**🐛 Bug fixes**\n\n- Maya: Fix Render single camera validator [\\#3929](https://github.com/pypeclub/OpenPype/pull/3929)\n- Flame: loading multilayer exr to batch/reel is working [\\#3901](https://github.com/pypeclub/OpenPype/pull/3901)\n- Hiero: Fix inventory check on launch [\\#3895](https://github.com/pypeclub/OpenPype/pull/3895)\n- WebPublisher: Fix import after refactor [\\#3891](https://github.com/pypeclub/OpenPype/pull/3891)\n\n**🔀 Refactored code**\n\n- Maya: Remove unused 'openpype.api' imports in plugins [\\#3925](https://github.com/pypeclub/OpenPype/pull/3925)\n- Resolve: Use new Extractor location [\\#3918](https://github.com/pypeclub/OpenPype/pull/3918)\n- Unreal: Use new Extractor location [\\#3917](https://github.com/pypeclub/OpenPype/pull/3917)\n- Flame: Use new Extractor location [\\#3916](https://github.com/pypeclub/OpenPype/pull/3916)\n- Houdini: Use new Extractor location [\\#3894](https://github.com/pypeclub/OpenPype/pull/3894)\n- Harmony: Use new Extractor location [\\#3893](https://github.com/pypeclub/OpenPype/pull/3893)\n\n**Merged pull requests:**\n\n- Maya: Fix Scene Inventory possibly starting off-screen due to maya preferences [\\#3923](https://github.com/pypeclub/OpenPype/pull/3923)\n\n## [3.14.2](https://github.com/pypeclub/OpenPype/tree/3.14.2) (2022-09-12)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.1...3.14.2)\n\n### 📖 Documentation\n\n- Documentation: Anatomy templates [\\#3618](https://github.com/pypeclub/OpenPype/pull/3618)\n\n**🆕 New features**\n\n- Nuke: Build workfile by template [\\#3763](https://github.com/pypeclub/OpenPype/pull/3763)\n- Houdini: Publishing workfiles [\\#3697](https://github.com/pypeclub/OpenPype/pull/3697)\n- Global: making collect audio plugin global [\\#3679](https://github.com/pypeclub/OpenPype/pull/3679)\n\n**🚀 Enhancements**\n\n- Flame: Adding Creator's retimed shot and handles switch [\\#3826](https://github.com/pypeclub/OpenPype/pull/3826)\n- Flame: OpenPype submenu to batch and media manager [\\#3825](https://github.com/pypeclub/OpenPype/pull/3825)\n- General: Better pixmap scaling [\\#3809](https://github.com/pypeclub/OpenPype/pull/3809)\n- Photoshop: attempt to speed up ExtractImage [\\#3793](https://github.com/pypeclub/OpenPype/pull/3793)\n- SyncServer: Added cli commands for sync server [\\#3765](https://github.com/pypeclub/OpenPype/pull/3765)\n- Kitsu: Drop 'entities root' setting. [\\#3739](https://github.com/pypeclub/OpenPype/pull/3739)\n- git: update gitignore [\\#3722](https://github.com/pypeclub/OpenPype/pull/3722)\n- Blender: Publisher collect workfile representation [\\#3670](https://github.com/pypeclub/OpenPype/pull/3670)\n- Maya: move set render settings menu entry [\\#3669](https://github.com/pypeclub/OpenPype/pull/3669)\n- Scene Inventory: Maya add actions to select from or to scene [\\#3659](https://github.com/pypeclub/OpenPype/pull/3659)\n- Scene Inventory: Add subsetGroup column [\\#3658](https://github.com/pypeclub/OpenPype/pull/3658)\n\n**🐛 Bug fixes**\n\n- General: Fix Pattern access in client code [\\#3828](https://github.com/pypeclub/OpenPype/pull/3828)\n- Launcher: Skip opening last work file works for groups [\\#3822](https://github.com/pypeclub/OpenPype/pull/3822)\n- Maya: Publishing data key change [\\#3811](https://github.com/pypeclub/OpenPype/pull/3811)\n- Igniter: Fix status handling when version is already installed [\\#3804](https://github.com/pypeclub/OpenPype/pull/3804)\n- Resolve: Addon import is Python 2 compatible [\\#3798](https://github.com/pypeclub/OpenPype/pull/3798)\n- Hiero: retimed clip publishing is working [\\#3792](https://github.com/pypeclub/OpenPype/pull/3792)\n- nuke: validate write node is not failing due wrong type [\\#3780](https://github.com/pypeclub/OpenPype/pull/3780)\n- Fix - changed format of version string in pyproject.toml [\\#3777](https://github.com/pypeclub/OpenPype/pull/3777)\n- Ftrack status fix typo prgoress -\\> progress [\\#3761](https://github.com/pypeclub/OpenPype/pull/3761)\n- Fix version resolution [\\#3757](https://github.com/pypeclub/OpenPype/pull/3757)\n- Maya: `containerise` dont skip empty values [\\#3674](https://github.com/pypeclub/OpenPype/pull/3674)\n\n**🔀 Refactored code**\n\n- Photoshop: Use new Extractor location [\\#3789](https://github.com/pypeclub/OpenPype/pull/3789)\n- Blender: Use new Extractor location [\\#3787](https://github.com/pypeclub/OpenPype/pull/3787)\n- AfterEffects: Use new Extractor location [\\#3784](https://github.com/pypeclub/OpenPype/pull/3784)\n- General: Remove unused teshost [\\#3773](https://github.com/pypeclub/OpenPype/pull/3773)\n- General: Copied 'Extractor' plugin to publish pipeline [\\#3771](https://github.com/pypeclub/OpenPype/pull/3771)\n- General: Move queries of asset and representation links [\\#3770](https://github.com/pypeclub/OpenPype/pull/3770)\n- General: Move create project folders to pipeline [\\#3768](https://github.com/pypeclub/OpenPype/pull/3768)\n- General: Create project function moved to client code [\\#3766](https://github.com/pypeclub/OpenPype/pull/3766)\n- Maya: Refactor submit deadline to use AbstractSubmitDeadline [\\#3759](https://github.com/pypeclub/OpenPype/pull/3759)\n- General: Change publish template settings location [\\#3755](https://github.com/pypeclub/OpenPype/pull/3755)\n- General: Move hostdirname functionality into host [\\#3749](https://github.com/pypeclub/OpenPype/pull/3749)\n- General: Move publish utils to pipeline [\\#3745](https://github.com/pypeclub/OpenPype/pull/3745)\n- Houdini: Define houdini as addon [\\#3735](https://github.com/pypeclub/OpenPype/pull/3735)\n- Fusion: Defined fusion as addon [\\#3733](https://github.com/pypeclub/OpenPype/pull/3733)\n- Flame: Defined flame as addon [\\#3732](https://github.com/pypeclub/OpenPype/pull/3732)\n- Resolve: Define resolve as addon [\\#3727](https://github.com/pypeclub/OpenPype/pull/3727)\n\n**Merged pull requests:**\n\n- Standalone Publisher: Ignore empty labels, then still use name like other asset models [\\#3779](https://github.com/pypeclub/OpenPype/pull/3779)\n- Kitsu - sync\\_all\\_project - add list ignore\\_projects [\\#3776](https://github.com/pypeclub/OpenPype/pull/3776)\n\n## [3.14.1](https://github.com/pypeclub/OpenPype/tree/3.14.1) (2022-08-30)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.0...3.14.1)\n\n### 📖 Documentation\n\n- Documentation: Few updates [\\#3698](https://github.com/pypeclub/OpenPype/pull/3698)\n- Documentation: Settings development [\\#3660](https://github.com/pypeclub/OpenPype/pull/3660)\n\n**🆕 New features**\n\n- Webpublisher:change create flatten image into tri state [\\#3678](https://github.com/pypeclub/OpenPype/pull/3678)\n- Blender: validators code correction with settings and defaults [\\#3662](https://github.com/pypeclub/OpenPype/pull/3662)\n\n**🚀 Enhancements**\n\n- General: Thumbnail can use project roots [\\#3750](https://github.com/pypeclub/OpenPype/pull/3750)\n- Settings: Remove settings lock on tray exit [\\#3720](https://github.com/pypeclub/OpenPype/pull/3720)\n- General: Added helper getters to modules manager [\\#3712](https://github.com/pypeclub/OpenPype/pull/3712)\n- Unreal: Define unreal as module and use host class [\\#3701](https://github.com/pypeclub/OpenPype/pull/3701)\n- Settings: Lock settings UI session [\\#3700](https://github.com/pypeclub/OpenPype/pull/3700)\n- General: Benevolent context label collector [\\#3686](https://github.com/pypeclub/OpenPype/pull/3686)\n- Ftrack: Store ftrack entities on hierarchy integration to instances [\\#3677](https://github.com/pypeclub/OpenPype/pull/3677)\n- Ftrack: More logs related to auto sync value change [\\#3671](https://github.com/pypeclub/OpenPype/pull/3671)\n- Blender: ops refresh manager after process events [\\#3663](https://github.com/pypeclub/OpenPype/pull/3663)\n\n**🐛 Bug fixes**\n\n- Maya: Fix typo in getPanel argument `with_focus` -\\> `withFocus` [\\#3753](https://github.com/pypeclub/OpenPype/pull/3753)\n- General: Smaller fixes of imports [\\#3748](https://github.com/pypeclub/OpenPype/pull/3748)\n- General: Logger tweaks [\\#3741](https://github.com/pypeclub/OpenPype/pull/3741)\n- Nuke: missing job dependency if multiple bake streams [\\#3737](https://github.com/pypeclub/OpenPype/pull/3737)\n- Nuke: color-space settings from anatomy is working [\\#3721](https://github.com/pypeclub/OpenPype/pull/3721)\n- Settings: Fix studio default anatomy save [\\#3716](https://github.com/pypeclub/OpenPype/pull/3716)\n- Maya: Use project name instead of project code [\\#3709](https://github.com/pypeclub/OpenPype/pull/3709)\n- Settings: Fix project overrides save [\\#3708](https://github.com/pypeclub/OpenPype/pull/3708)\n- Workfiles tool: Fix published workfile filtering [\\#3704](https://github.com/pypeclub/OpenPype/pull/3704)\n- PS, AE: Provide default variant value for workfile subset [\\#3703](https://github.com/pypeclub/OpenPype/pull/3703)\n- RoyalRender: handle host name that is not set [\\#3695](https://github.com/pypeclub/OpenPype/pull/3695)\n- Flame: retime is working on clip publishing [\\#3684](https://github.com/pypeclub/OpenPype/pull/3684)\n- Webpublisher: added check for empty context [\\#3682](https://github.com/pypeclub/OpenPype/pull/3682)\n\n**🔀 Refactored code**\n\n- General: Move delivery logic to pipeline [\\#3751](https://github.com/pypeclub/OpenPype/pull/3751)\n- General: Host addons cleanup [\\#3744](https://github.com/pypeclub/OpenPype/pull/3744)\n- Webpublisher: Webpublisher is used as addon [\\#3740](https://github.com/pypeclub/OpenPype/pull/3740)\n- Photoshop: Defined photoshop as addon [\\#3736](https://github.com/pypeclub/OpenPype/pull/3736)\n- Harmony: Defined harmony as addon [\\#3734](https://github.com/pypeclub/OpenPype/pull/3734)\n- General: Module interfaces cleanup [\\#3731](https://github.com/pypeclub/OpenPype/pull/3731)\n- AfterEffects: Move AE functions from general lib [\\#3730](https://github.com/pypeclub/OpenPype/pull/3730)\n- Blender: Define blender as module [\\#3729](https://github.com/pypeclub/OpenPype/pull/3729)\n- AfterEffects: Define AfterEffects as module [\\#3728](https://github.com/pypeclub/OpenPype/pull/3728)\n- General: Replace PypeLogger with Logger [\\#3725](https://github.com/pypeclub/OpenPype/pull/3725)\n- Nuke: Define nuke as module [\\#3724](https://github.com/pypeclub/OpenPype/pull/3724)\n- General: Move subset name functionality [\\#3723](https://github.com/pypeclub/OpenPype/pull/3723)\n- General: Move creators plugin getter [\\#3714](https://github.com/pypeclub/OpenPype/pull/3714)\n- General: Move constants from lib to client [\\#3713](https://github.com/pypeclub/OpenPype/pull/3713)\n- Loader: Subset groups using client operations [\\#3710](https://github.com/pypeclub/OpenPype/pull/3710)\n- TVPaint: Defined as module [\\#3707](https://github.com/pypeclub/OpenPype/pull/3707)\n- StandalonePublisher: Define StandalonePublisher as module [\\#3706](https://github.com/pypeclub/OpenPype/pull/3706)\n- TrayPublisher: Define TrayPublisher as module [\\#3705](https://github.com/pypeclub/OpenPype/pull/3705)\n- General: Move context specific functions to context tools [\\#3702](https://github.com/pypeclub/OpenPype/pull/3702)\n\n**Merged pull requests:**\n\n- Hiero: Define hiero as module [\\#3717](https://github.com/pypeclub/OpenPype/pull/3717)\n- Deadline: better logging for DL webservice failures [\\#3694](https://github.com/pypeclub/OpenPype/pull/3694)\n- Photoshop: resize saved images in ExtractReview for ffmpeg [\\#3676](https://github.com/pypeclub/OpenPype/pull/3676)\n- Nuke: Validation refactory to new publisher [\\#3567](https://github.com/pypeclub/OpenPype/pull/3567)\n\n## [3.14.0](https://github.com/pypeclub/OpenPype/tree/3.14.0) (2022-08-18)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.13.0...3.14.0)\n\n**🆕 New features**\n\n- Maya: Build workfile by template [\\#3578](https://github.com/pypeclub/OpenPype/pull/3578)\n- Maya: Implementation of JSON layout for Unreal workflow [\\#3353](https://github.com/pypeclub/OpenPype/pull/3353)\n- Maya: Build workfile by template [\\#3315](https://github.com/pypeclub/OpenPype/pull/3315)\n\n**🚀 Enhancements**\n\n- Ftrack: Addiotional component metadata [\\#3685](https://github.com/pypeclub/OpenPype/pull/3685)\n- Ftrack: Set task status on farm publishing [\\#3680](https://github.com/pypeclub/OpenPype/pull/3680)\n- Ftrack: Set task status on task creation in integrate hierarchy [\\#3675](https://github.com/pypeclub/OpenPype/pull/3675)\n- Maya: Disable rendering of all lights for render instances submitted through Deadline. [\\#3661](https://github.com/pypeclub/OpenPype/pull/3661)\n- General: Optimized OCIO configs [\\#3650](https://github.com/pypeclub/OpenPype/pull/3650)\n\n**🐛 Bug fixes**\n\n- General: Switch from hero version to versioned works [\\#3691](https://github.com/pypeclub/OpenPype/pull/3691)\n- General: Fix finding of last version [\\#3656](https://github.com/pypeclub/OpenPype/pull/3656)\n- General: Extract Review can scale with pixel aspect ratio [\\#3644](https://github.com/pypeclub/OpenPype/pull/3644)\n- Maya: Refactor moved usage of CreateRender settings [\\#3643](https://github.com/pypeclub/OpenPype/pull/3643)\n- General: Hero version representations have full context [\\#3638](https://github.com/pypeclub/OpenPype/pull/3638)\n- Nuke: color settings for render write node is working now [\\#3632](https://github.com/pypeclub/OpenPype/pull/3632)\n- Maya: FBX support for update in reference loader [\\#3631](https://github.com/pypeclub/OpenPype/pull/3631)\n\n**🔀 Refactored code**\n\n- General: Use client projects getter [\\#3673](https://github.com/pypeclub/OpenPype/pull/3673)\n- Resolve: Match folder structure to other hosts [\\#3653](https://github.com/pypeclub/OpenPype/pull/3653)\n- Maya: Hosts as modules [\\#3647](https://github.com/pypeclub/OpenPype/pull/3647)\n- TimersManager: Plugins are in timers manager module [\\#3639](https://github.com/pypeclub/OpenPype/pull/3639)\n- General: Move workfiles functions into pipeline [\\#3637](https://github.com/pypeclub/OpenPype/pull/3637)\n- General: Workfiles builder using query functions [\\#3598](https://github.com/pypeclub/OpenPype/pull/3598)\n\n**Merged pull requests:**\n\n- Deadline: Global job pre load is not Pype 2 compatible [\\#3666](https://github.com/pypeclub/OpenPype/pull/3666)\n- Maya: Remove unused get current renderer logic [\\#3645](https://github.com/pypeclub/OpenPype/pull/3645)\n- Kitsu|Fix: Movie project type fails & first loop children names [\\#3636](https://github.com/pypeclub/OpenPype/pull/3636)\n- fix the bug of failing to extract look when UDIMs format used in AiImage [\\#3628](https://github.com/pypeclub/OpenPype/pull/3628)\n\n## [3.13.0](https://github.com/pypeclub/OpenPype/tree/3.13.0) (2022-08-09)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.2...3.13.0)\n\n**🆕 New features**\n\n- Support for mutliple installed versions - 3.13 [\\#3605](https://github.com/pypeclub/OpenPype/pull/3605)\n- Traypublisher: simple editorial publishing [\\#3492](https://github.com/pypeclub/OpenPype/pull/3492)\n\n**🚀 Enhancements**\n\n- Editorial: Mix audio use side file for ffmpeg filters [\\#3630](https://github.com/pypeclub/OpenPype/pull/3630)\n- Ftrack: Comment template can contain optional keys [\\#3615](https://github.com/pypeclub/OpenPype/pull/3615)\n- Ftrack: Add more metadata to ftrack components [\\#3612](https://github.com/pypeclub/OpenPype/pull/3612)\n- General: Add context to pyblish context [\\#3594](https://github.com/pypeclub/OpenPype/pull/3594)\n- Kitsu: Shot&Sequence name with prefix over appends [\\#3593](https://github.com/pypeclub/OpenPype/pull/3593)\n- Photoshop: implemented {layer} placeholder in subset template [\\#3591](https://github.com/pypeclub/OpenPype/pull/3591)\n- General: Python module appdirs from git [\\#3589](https://github.com/pypeclub/OpenPype/pull/3589)\n- Ftrack: Update ftrack api to 2.3.3 [\\#3588](https://github.com/pypeclub/OpenPype/pull/3588)\n- General: New Integrator small fixes [\\#3583](https://github.com/pypeclub/OpenPype/pull/3583)\n- Maya: Render Creator has configurable options. [\\#3097](https://github.com/pypeclub/OpenPype/pull/3097)\n\n**🐛 Bug fixes**\n\n- Maya:  fix aov separator in Redshift [\\#3625](https://github.com/pypeclub/OpenPype/pull/3625)\n- Fix for multi-version build on Mac [\\#3622](https://github.com/pypeclub/OpenPype/pull/3622)\n- Ftrack: Sync hierarchical attributes can handle new created entities [\\#3621](https://github.com/pypeclub/OpenPype/pull/3621)\n- General: Extract review aspect ratio scale is calculated by ffmpeg [\\#3620](https://github.com/pypeclub/OpenPype/pull/3620)\n- Maya: Fix types of default settings [\\#3617](https://github.com/pypeclub/OpenPype/pull/3617)\n- Integrator: Don't force to have dot before frame [\\#3611](https://github.com/pypeclub/OpenPype/pull/3611)\n- AfterEffects: refactored integrate doesnt work formulti frame publishes [\\#3610](https://github.com/pypeclub/OpenPype/pull/3610)\n- Maya look data contents fails with custom attribute on group [\\#3607](https://github.com/pypeclub/OpenPype/pull/3607)\n- TrayPublisher: Fix wrong conflict merge [\\#3600](https://github.com/pypeclub/OpenPype/pull/3600)\n- Bugfix: Add OCIO as submodule to prepare for handling `maketx` color space conversion. [\\#3590](https://github.com/pypeclub/OpenPype/pull/3590)\n- Fix general settings environment variables resolution [\\#3587](https://github.com/pypeclub/OpenPype/pull/3587)\n- Editorial publishing workflow improvements [\\#3580](https://github.com/pypeclub/OpenPype/pull/3580)\n- General: Update imports in start script [\\#3579](https://github.com/pypeclub/OpenPype/pull/3579)\n- Nuke: render family integration consistency  [\\#3576](https://github.com/pypeclub/OpenPype/pull/3576)\n- Ftrack: Handle missing published path in integrator [\\#3570](https://github.com/pypeclub/OpenPype/pull/3570)\n- Nuke: publish existing frames with slate with correct range [\\#3555](https://github.com/pypeclub/OpenPype/pull/3555)\n\n**🔀 Refactored code**\n\n- General: Plugin settings handled by plugins [\\#3623](https://github.com/pypeclub/OpenPype/pull/3623)\n- General: Naive implementation of document create, update, delete [\\#3601](https://github.com/pypeclub/OpenPype/pull/3601)\n- General: Use query functions in general code [\\#3596](https://github.com/pypeclub/OpenPype/pull/3596)\n- General: Separate extraction of template data into more functions [\\#3574](https://github.com/pypeclub/OpenPype/pull/3574)\n- General: Lib cleanup [\\#3571](https://github.com/pypeclub/OpenPype/pull/3571)\n\n**Merged pull requests:**\n\n- Webpublisher: timeout for PS studio processing [\\#3619](https://github.com/pypeclub/OpenPype/pull/3619)\n- Core: translated validate\\_containers.py into New publisher style [\\#3614](https://github.com/pypeclub/OpenPype/pull/3614)\n- Enable write color sets on animation publish automatically [\\#3582](https://github.com/pypeclub/OpenPype/pull/3582)\n\n## [3.12.2](https://github.com/pypeclub/OpenPype/tree/3.12.2) (2022-07-27)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.1...3.12.2)\n\n### 📖 Documentation\n\n- Update website with more studios [\\#3554](https://github.com/pypeclub/OpenPype/pull/3554)\n- Documentation: Update publishing dev docs [\\#3549](https://github.com/pypeclub/OpenPype/pull/3549)\n\n**🚀 Enhancements**\n\n- General: Global thumbnail extractor is ready for more cases [\\#3561](https://github.com/pypeclub/OpenPype/pull/3561)\n- Maya: add additional validators to Settings [\\#3540](https://github.com/pypeclub/OpenPype/pull/3540)\n- General: Interactive console in cli [\\#3526](https://github.com/pypeclub/OpenPype/pull/3526)\n- Ftrack: Automatic daily review session creation can define trigger hour [\\#3516](https://github.com/pypeclub/OpenPype/pull/3516)\n- Ftrack: add source into Note [\\#3509](https://github.com/pypeclub/OpenPype/pull/3509)\n- Ftrack: Trigger custom ftrack topic of project structure creation [\\#3506](https://github.com/pypeclub/OpenPype/pull/3506)\n- Settings UI: Add extract to file action on project view [\\#3505](https://github.com/pypeclub/OpenPype/pull/3505)\n- Add pack and unpack convenience scripts [\\#3502](https://github.com/pypeclub/OpenPype/pull/3502)\n- General: Event system [\\#3499](https://github.com/pypeclub/OpenPype/pull/3499)\n- NewPublisher: Keep plugins with mismatch target in report [\\#3498](https://github.com/pypeclub/OpenPype/pull/3498)\n- Nuke: load clip with options from settings [\\#3497](https://github.com/pypeclub/OpenPype/pull/3497)\n- TrayPublisher: implemented render\\_mov\\_batch  [\\#3486](https://github.com/pypeclub/OpenPype/pull/3486)\n- Migrate basic families to the new Tray Publisher [\\#3469](https://github.com/pypeclub/OpenPype/pull/3469)\n- Enhance powershell build scripts [\\#1827](https://github.com/pypeclub/OpenPype/pull/1827)\n\n**🐛 Bug fixes**\n\n- Maya: fix Review image plane attribute  [\\#3569](https://github.com/pypeclub/OpenPype/pull/3569)\n- Maya: Fix animated attributes \\(ie. overscan\\) on loaded cameras breaking review publishing. [\\#3562](https://github.com/pypeclub/OpenPype/pull/3562)\n- NewPublisher: Python 2 compatible html escape [\\#3559](https://github.com/pypeclub/OpenPype/pull/3559)\n- Remove invalid submodules from `/vendor` [\\#3557](https://github.com/pypeclub/OpenPype/pull/3557)\n- General: Remove hosts filter on integrator plugins [\\#3556](https://github.com/pypeclub/OpenPype/pull/3556)\n- Settings: Clean default values of environments [\\#3550](https://github.com/pypeclub/OpenPype/pull/3550)\n- Module interfaces: Fix import error [\\#3547](https://github.com/pypeclub/OpenPype/pull/3547)\n- Workfiles tool: Show of tool and it's flags [\\#3539](https://github.com/pypeclub/OpenPype/pull/3539)\n- General: Create workfile documents works again [\\#3538](https://github.com/pypeclub/OpenPype/pull/3538)\n- Additional fixes for powershell scripts [\\#3525](https://github.com/pypeclub/OpenPype/pull/3525)\n- Maya: Added wrapper around cmds.setAttr [\\#3523](https://github.com/pypeclub/OpenPype/pull/3523)\n- Nuke: double slate [\\#3521](https://github.com/pypeclub/OpenPype/pull/3521)\n- General: Fix hash of centos oiio archive [\\#3519](https://github.com/pypeclub/OpenPype/pull/3519)\n- Maya: Renderman display output fix [\\#3514](https://github.com/pypeclub/OpenPype/pull/3514)\n- TrayPublisher: Simple creation enhancements and fixes [\\#3513](https://github.com/pypeclub/OpenPype/pull/3513)\n- NewPublisher: Publish attributes are properly collected [\\#3510](https://github.com/pypeclub/OpenPype/pull/3510)\n- TrayPublisher: Make sure host name is filled [\\#3504](https://github.com/pypeclub/OpenPype/pull/3504)\n- NewPublisher: Groups work and enum multivalue [\\#3501](https://github.com/pypeclub/OpenPype/pull/3501)\n\n**🔀 Refactored code**\n\n- General: Use query functions in integrator [\\#3563](https://github.com/pypeclub/OpenPype/pull/3563)\n- General: Mongo core connection moved to client [\\#3531](https://github.com/pypeclub/OpenPype/pull/3531)\n- Refactor Integrate Asset [\\#3530](https://github.com/pypeclub/OpenPype/pull/3530)\n- General: Client docstrings cleanup [\\#3529](https://github.com/pypeclub/OpenPype/pull/3529)\n- General: Move load related functions into pipeline [\\#3527](https://github.com/pypeclub/OpenPype/pull/3527)\n- General: Get current context document functions [\\#3522](https://github.com/pypeclub/OpenPype/pull/3522)\n- Kitsu: Use query function from client [\\#3496](https://github.com/pypeclub/OpenPype/pull/3496)\n- TimersManager: Use query functions [\\#3495](https://github.com/pypeclub/OpenPype/pull/3495)\n- Deadline: Use query functions [\\#3466](https://github.com/pypeclub/OpenPype/pull/3466)\n- Refactor Integrate Asset [\\#2898](https://github.com/pypeclub/OpenPype/pull/2898)\n\n**Merged pull requests:**\n\n- Maya: fix active pane loss [\\#3566](https://github.com/pypeclub/OpenPype/pull/3566)\n\n## [3.12.1](https://github.com/pypeclub/OpenPype/tree/3.12.1) (2022-07-13)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.0...3.12.1)\n\n### 📖 Documentation\n\n- Docs: Added minimal permissions for MongoDB [\\#3441](https://github.com/pypeclub/OpenPype/pull/3441)\n\n**🆕 New features**\n\n- Maya: Add VDB to Arnold loader [\\#3433](https://github.com/pypeclub/OpenPype/pull/3433)\n\n**🚀 Enhancements**\n\n- TrayPublisher: Added more options for grouping of instances [\\#3494](https://github.com/pypeclub/OpenPype/pull/3494)\n- NewPublisher: Align creator attributes from top to bottom [\\#3487](https://github.com/pypeclub/OpenPype/pull/3487)\n- NewPublisher: Added ability to use label of instance [\\#3484](https://github.com/pypeclub/OpenPype/pull/3484)\n- General: Creator Plugins have access to project [\\#3476](https://github.com/pypeclub/OpenPype/pull/3476)\n- General: Better arguments order in creator init [\\#3475](https://github.com/pypeclub/OpenPype/pull/3475)\n- Ftrack: Trigger custom ftrack events on project creation and preparation [\\#3465](https://github.com/pypeclub/OpenPype/pull/3465)\n- Windows installer: Clean old files and add version subfolder [\\#3445](https://github.com/pypeclub/OpenPype/pull/3445)\n- Blender: Bugfix - Set fps properly on open [\\#3426](https://github.com/pypeclub/OpenPype/pull/3426)\n- Hiero: Add custom scripts menu [\\#3425](https://github.com/pypeclub/OpenPype/pull/3425)\n- Blender: pre pyside install for all platforms [\\#3400](https://github.com/pypeclub/OpenPype/pull/3400)\n- Maya: Add additional playblast options to review Extractor. [\\#3384](https://github.com/pypeclub/OpenPype/pull/3384)\n- Maya: Ability to set resolution for playblasts from asset, and override through review instance. [\\#3360](https://github.com/pypeclub/OpenPype/pull/3360)\n- Maya: Redshift Volume Loader Implement update, remove, switch + fix vdb sequence support [\\#3197](https://github.com/pypeclub/OpenPype/pull/3197)\n- Maya: Implement `iter_visible_nodes_in_range` for extracting Alembics [\\#3100](https://github.com/pypeclub/OpenPype/pull/3100)\n\n**🐛 Bug fixes**\n\n- TrayPublisher: Keep use instance label in list view [\\#3493](https://github.com/pypeclub/OpenPype/pull/3493)\n- General: Extract review use first frame of input sequence [\\#3491](https://github.com/pypeclub/OpenPype/pull/3491)\n- General: Fix Plist loading for application launch [\\#3485](https://github.com/pypeclub/OpenPype/pull/3485)\n- Nuke: Workfile tools open on start [\\#3479](https://github.com/pypeclub/OpenPype/pull/3479)\n- New Publisher: Disabled context change allows creation [\\#3478](https://github.com/pypeclub/OpenPype/pull/3478)\n- General: thumbnail extractor fix [\\#3474](https://github.com/pypeclub/OpenPype/pull/3474)\n- Kitsu: bugfix with sync-service ans publish plugins [\\#3473](https://github.com/pypeclub/OpenPype/pull/3473)\n- Flame: solved problem with multi-selected loading [\\#3470](https://github.com/pypeclub/OpenPype/pull/3470)\n- General: Fix query function in update logic [\\#3468](https://github.com/pypeclub/OpenPype/pull/3468)\n- Resolve: removed few bugs [\\#3464](https://github.com/pypeclub/OpenPype/pull/3464)\n- General: Delete old versions is safer when ftrack is disabled [\\#3462](https://github.com/pypeclub/OpenPype/pull/3462)\n- Nuke: fixing metadata slate TC difference [\\#3455](https://github.com/pypeclub/OpenPype/pull/3455)\n- Nuke: prerender reviewable fails [\\#3450](https://github.com/pypeclub/OpenPype/pull/3450)\n- Maya: fix hashing in Python 3 for tile rendering [\\#3447](https://github.com/pypeclub/OpenPype/pull/3447)\n- LogViewer: Escape html characters in log message [\\#3443](https://github.com/pypeclub/OpenPype/pull/3443)\n- Nuke: Slate frame is integrated [\\#3427](https://github.com/pypeclub/OpenPype/pull/3427)\n- Maya: Camera extra data - additional fix for \\#3304 [\\#3386](https://github.com/pypeclub/OpenPype/pull/3386)\n- Maya: Handle excluding `model` family from frame range validator. [\\#3370](https://github.com/pypeclub/OpenPype/pull/3370)\n\n**🔀 Refactored code**\n\n- Maya: Merge animation + pointcache extractor logic [\\#3461](https://github.com/pypeclub/OpenPype/pull/3461)\n- Maya: Re-use `maintained_time` from lib [\\#3460](https://github.com/pypeclub/OpenPype/pull/3460)\n- General: Use query functions in global plugins [\\#3459](https://github.com/pypeclub/OpenPype/pull/3459)\n- Clockify: Use query functions in clockify actions [\\#3458](https://github.com/pypeclub/OpenPype/pull/3458)\n- General: Use query functions in rest api calls [\\#3457](https://github.com/pypeclub/OpenPype/pull/3457)\n- General: Use query functions in openpype lib functions [\\#3454](https://github.com/pypeclub/OpenPype/pull/3454)\n- General: Use query functions in load utils [\\#3446](https://github.com/pypeclub/OpenPype/pull/3446)\n- General: Move publish plugin and publish render abstractions [\\#3442](https://github.com/pypeclub/OpenPype/pull/3442)\n- General: Use Anatomy after move to pipeline [\\#3436](https://github.com/pypeclub/OpenPype/pull/3436)\n- General: Anatomy moved to pipeline [\\#3435](https://github.com/pypeclub/OpenPype/pull/3435)\n- Fusion: Use client query functions [\\#3380](https://github.com/pypeclub/OpenPype/pull/3380)\n- Resolve: Use client query functions [\\#3379](https://github.com/pypeclub/OpenPype/pull/3379)\n- General: Host implementation defined with class [\\#3337](https://github.com/pypeclub/OpenPype/pull/3337)\n\n## [3.12.0](https://github.com/pypeclub/OpenPype/tree/3.12.0) (2022-06-28)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.11.1...3.12.0)\n\n### 📖 Documentation\n\n- Fix typo in documentation: pyenv on mac [\\#3417](https://github.com/pypeclub/OpenPype/pull/3417)\n- Linux: update OIIO package [\\#3401](https://github.com/pypeclub/OpenPype/pull/3401)\n\n**🆕 New features**\n\n- Shotgrid: Add production beta of shotgrid integration [\\#2921](https://github.com/pypeclub/OpenPype/pull/2921)\n\n**🚀 Enhancements**\n\n- Webserver: Added CORS middleware [\\#3422](https://github.com/pypeclub/OpenPype/pull/3422)\n- Attribute Defs UI: Files widget show what is allowed to drop in [\\#3411](https://github.com/pypeclub/OpenPype/pull/3411)\n- General: Add ability to change user value for templates [\\#3366](https://github.com/pypeclub/OpenPype/pull/3366)\n- Hosts: More options for in-host callbacks [\\#3357](https://github.com/pypeclub/OpenPype/pull/3357)\n- Multiverse: expose some settings to GUI [\\#3350](https://github.com/pypeclub/OpenPype/pull/3350)\n- Maya: Allow more data to be published along camera 🎥  [\\#3304](https://github.com/pypeclub/OpenPype/pull/3304)\n- Add root keys and project keys to create starting folder [\\#2755](https://github.com/pypeclub/OpenPype/pull/2755)\n\n**🐛 Bug fixes**\n\n- NewPublisher: Fix subset name change on change of creator plugin [\\#3420](https://github.com/pypeclub/OpenPype/pull/3420)\n- Bug: fix invalid avalon import [\\#3418](https://github.com/pypeclub/OpenPype/pull/3418)\n- Nuke: Fix keyword argument in query function [\\#3414](https://github.com/pypeclub/OpenPype/pull/3414)\n- Houdini: fix loading and updating vbd/bgeo sequences [\\#3408](https://github.com/pypeclub/OpenPype/pull/3408)\n- Nuke: Collect representation files based on Write [\\#3407](https://github.com/pypeclub/OpenPype/pull/3407)\n- General: Filter representations before integration start [\\#3398](https://github.com/pypeclub/OpenPype/pull/3398)\n- Maya: look collector typo [\\#3392](https://github.com/pypeclub/OpenPype/pull/3392)\n- TVPaint: Make sure exit code is set to not None [\\#3382](https://github.com/pypeclub/OpenPype/pull/3382)\n- Maya: vray device aspect ratio fix [\\#3381](https://github.com/pypeclub/OpenPype/pull/3381)\n- Flame: bunch of publishing issues [\\#3377](https://github.com/pypeclub/OpenPype/pull/3377)\n- Harmony: added unc path to zifile command in Harmony [\\#3372](https://github.com/pypeclub/OpenPype/pull/3372)\n- Standalone: settings improvements [\\#3355](https://github.com/pypeclub/OpenPype/pull/3355)\n- Nuke: Load full model hierarchy by default [\\#3328](https://github.com/pypeclub/OpenPype/pull/3328)\n- Nuke: multiple baking streams with correct slate [\\#3245](https://github.com/pypeclub/OpenPype/pull/3245)\n- Maya: fix image prefix warning in validator [\\#3128](https://github.com/pypeclub/OpenPype/pull/3128)\n\n**🔀 Refactored code**\n\n- Unreal: Use client query functions [\\#3421](https://github.com/pypeclub/OpenPype/pull/3421)\n- General: Move editorial lib to pipeline [\\#3419](https://github.com/pypeclub/OpenPype/pull/3419)\n- Kitsu: renaming to plural func sync\\_all\\_projects [\\#3397](https://github.com/pypeclub/OpenPype/pull/3397)\n- Houdini: Use client query functions [\\#3395](https://github.com/pypeclub/OpenPype/pull/3395)\n- Hiero: Use client query functions [\\#3393](https://github.com/pypeclub/OpenPype/pull/3393)\n- Nuke: Use client query functions [\\#3391](https://github.com/pypeclub/OpenPype/pull/3391)\n- Maya: Use client query functions [\\#3385](https://github.com/pypeclub/OpenPype/pull/3385)\n- Harmony: Use client query functions [\\#3378](https://github.com/pypeclub/OpenPype/pull/3378)\n- Celaction: Use client query functions [\\#3376](https://github.com/pypeclub/OpenPype/pull/3376)\n- Photoshop: Use client query functions [\\#3375](https://github.com/pypeclub/OpenPype/pull/3375)\n- AfterEffects: Use client query functions [\\#3374](https://github.com/pypeclub/OpenPype/pull/3374)\n- TVPaint: Use client query functions [\\#3340](https://github.com/pypeclub/OpenPype/pull/3340)\n- Ftrack: Use client query functions [\\#3339](https://github.com/pypeclub/OpenPype/pull/3339)\n- Standalone Publisher: Use client query functions [\\#3330](https://github.com/pypeclub/OpenPype/pull/3330)\n\n**Merged pull requests:**\n\n- Sync Queue: Added far future value for null values for dates [\\#3371](https://github.com/pypeclub/OpenPype/pull/3371)\n- Maya - added support for single frame playblast review [\\#3369](https://github.com/pypeclub/OpenPype/pull/3369)\n- Houdini: Implement Redshift Proxy Export [\\#3196](https://github.com/pypeclub/OpenPype/pull/3196)\n\n## [3.11.1](https://github.com/pypeclub/OpenPype/tree/3.11.1) (2022-06-20)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.11.0...3.11.1)\n\n**🆕 New features**\n\n- Flame: custom export temp folder [\\#3346](https://github.com/pypeclub/OpenPype/pull/3346)\n- Nuke: removing third-party plugins   [\\#3344](https://github.com/pypeclub/OpenPype/pull/3344)\n\n**🚀 Enhancements**\n\n- Pyblish Pype: Hiding/Close issues [\\#3367](https://github.com/pypeclub/OpenPype/pull/3367)\n- Ftrack: Removed requirement of pypeclub role from default settings [\\#3354](https://github.com/pypeclub/OpenPype/pull/3354)\n- Kitsu: Prevent crash on missing frames information [\\#3352](https://github.com/pypeclub/OpenPype/pull/3352)\n- Ftrack: Open browser from tray [\\#3320](https://github.com/pypeclub/OpenPype/pull/3320)\n- Enhancement: More control over thumbnail processing. [\\#3259](https://github.com/pypeclub/OpenPype/pull/3259)\n\n**🐛 Bug fixes**\n\n- Nuke: bake streams with slate on farm [\\#3368](https://github.com/pypeclub/OpenPype/pull/3368)\n- Harmony: audio validator has wrong logic [\\#3364](https://github.com/pypeclub/OpenPype/pull/3364)\n- Nuke: Fix missing variable in extract thumbnail [\\#3363](https://github.com/pypeclub/OpenPype/pull/3363)\n- Nuke: Fix precollect writes [\\#3361](https://github.com/pypeclub/OpenPype/pull/3361)\n- AE- fix validate\\_scene\\_settings and renderLocal [\\#3358](https://github.com/pypeclub/OpenPype/pull/3358)\n- deadline: fixing misidentification of revieables [\\#3356](https://github.com/pypeclub/OpenPype/pull/3356)\n- General: Create only one thumbnail per instance [\\#3351](https://github.com/pypeclub/OpenPype/pull/3351)\n- nuke: adding extract thumbnail settings 3.10 [\\#3347](https://github.com/pypeclub/OpenPype/pull/3347)\n- General: Fix last version function [\\#3345](https://github.com/pypeclub/OpenPype/pull/3345)\n- Deadline: added OPENPYPE\\_MONGO to filter [\\#3336](https://github.com/pypeclub/OpenPype/pull/3336)\n- Nuke: fixing farm publishing if review is disabled [\\#3306](https://github.com/pypeclub/OpenPype/pull/3306)\n- Maya: Fix Yeti errors on Create, Publish and Load [\\#3198](https://github.com/pypeclub/OpenPype/pull/3198)\n\n**🔀 Refactored code**\n\n- Webpublisher: Use client query functions [\\#3333](https://github.com/pypeclub/OpenPype/pull/3333)\n\n## [3.11.0](https://github.com/pypeclub/OpenPype/tree/3.11.0) (2022-06-17)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.10.0...3.11.0)\n\n### 📖 Documentation\n\n- Documentation: Add app key to template documentation [\\#3299](https://github.com/pypeclub/OpenPype/pull/3299)\n- doc: adding royal render and multiverse to the web site [\\#3285](https://github.com/pypeclub/OpenPype/pull/3285)\n- Module: Kitsu module [\\#2650](https://github.com/pypeclub/OpenPype/pull/2650)\n\n**🆕 New features**\n\n- Multiverse: fixed composition write, full docs, cosmetics [\\#3178](https://github.com/pypeclub/OpenPype/pull/3178)\n\n**🚀 Enhancements**\n\n- Settings: Settings can be extracted from UI [\\#3323](https://github.com/pypeclub/OpenPype/pull/3323)\n- updated poetry installation source [\\#3316](https://github.com/pypeclub/OpenPype/pull/3316)\n- Ftrack: Action to easily create daily review session [\\#3310](https://github.com/pypeclub/OpenPype/pull/3310)\n- TVPaint: Extractor use mark in/out range to render [\\#3309](https://github.com/pypeclub/OpenPype/pull/3309)\n- Ftrack: Delivery action can work on ReviewSessions [\\#3307](https://github.com/pypeclub/OpenPype/pull/3307)\n- Maya: Look assigner UI improvements [\\#3298](https://github.com/pypeclub/OpenPype/pull/3298)\n- Ftrack: Action to transfer values of hierarchical attributes [\\#3284](https://github.com/pypeclub/OpenPype/pull/3284)\n- Maya: better handling of legacy review subsets names [\\#3269](https://github.com/pypeclub/OpenPype/pull/3269)\n- General: Updated windows oiio tool [\\#3268](https://github.com/pypeclub/OpenPype/pull/3268)\n- Unreal: add support for skeletalMesh and staticMesh to loaders [\\#3267](https://github.com/pypeclub/OpenPype/pull/3267)\n- Maya: reference loaders could store placeholder in referenced url [\\#3264](https://github.com/pypeclub/OpenPype/pull/3264)\n- TVPaint: Init file for TVPaint worker also handle guideline images [\\#3250](https://github.com/pypeclub/OpenPype/pull/3250)\n- Nuke: Change default icon path in settings [\\#3247](https://github.com/pypeclub/OpenPype/pull/3247)\n- Maya: publishing of animation and pointcache on a farm [\\#3225](https://github.com/pypeclub/OpenPype/pull/3225)\n- Maya: Look assigner UI improvements [\\#3208](https://github.com/pypeclub/OpenPype/pull/3208)\n- Nuke: add pointcache and animation to loader [\\#3186](https://github.com/pypeclub/OpenPype/pull/3186)\n- Nuke: Add a gizmo menu [\\#3172](https://github.com/pypeclub/OpenPype/pull/3172)\n- Support for Unreal 5 [\\#3122](https://github.com/pypeclub/OpenPype/pull/3122)\n\n**🐛 Bug fixes**\n\n- General: Handle empty source key on instance [\\#3342](https://github.com/pypeclub/OpenPype/pull/3342)\n- Houdini: Fix Houdini VDB manage update wrong file attribute name [\\#3322](https://github.com/pypeclub/OpenPype/pull/3322)\n- Nuke: anatomy compatibility issue hacks [\\#3321](https://github.com/pypeclub/OpenPype/pull/3321)\n- hiero: otio p3 compatibility issue - metadata on effect use update 3.11 [\\#3314](https://github.com/pypeclub/OpenPype/pull/3314)\n- General: Vendorized modules for Python 2 and update poetry lock [\\#3305](https://github.com/pypeclub/OpenPype/pull/3305)\n- Fix - added local targets to install host [\\#3303](https://github.com/pypeclub/OpenPype/pull/3303)\n- Settings: Add missing default settings for nuke gizmo [\\#3301](https://github.com/pypeclub/OpenPype/pull/3301)\n- Maya: Fix swaped width and height in reviews [\\#3300](https://github.com/pypeclub/OpenPype/pull/3300)\n- Maya: point cache publish handles Maya instances [\\#3297](https://github.com/pypeclub/OpenPype/pull/3297)\n- Global: extract review slate issues [\\#3286](https://github.com/pypeclub/OpenPype/pull/3286)\n- Webpublisher: return only active projects in ProjectsEndpoint [\\#3281](https://github.com/pypeclub/OpenPype/pull/3281)\n- Hiero: add support for task tags 3.10.x [\\#3279](https://github.com/pypeclub/OpenPype/pull/3279)\n- General: Fix Oiio tool path resolving [\\#3278](https://github.com/pypeclub/OpenPype/pull/3278)\n- Maya: Fix udim support for e.g. uppercase \\<UDIM\\> tag [\\#3266](https://github.com/pypeclub/OpenPype/pull/3266)\n- Nuke: bake reformat was failing on string type [\\#3261](https://github.com/pypeclub/OpenPype/pull/3261)\n- Maya: hotfix Pxr multitexture in looks [\\#3260](https://github.com/pypeclub/OpenPype/pull/3260)\n- Unreal: Fix Camera Loading if Layout is missing [\\#3255](https://github.com/pypeclub/OpenPype/pull/3255)\n- Unreal: Fixed Animation loading in UE5 [\\#3240](https://github.com/pypeclub/OpenPype/pull/3240)\n- Unreal: Fixed Render creation in UE5 [\\#3239](https://github.com/pypeclub/OpenPype/pull/3239)\n- Unreal: Fixed Camera loading in UE5 [\\#3238](https://github.com/pypeclub/OpenPype/pull/3238)\n- Flame: debugging [\\#3224](https://github.com/pypeclub/OpenPype/pull/3224)\n- add silent audio to slate [\\#3162](https://github.com/pypeclub/OpenPype/pull/3162)\n- Add timecode to slate [\\#2929](https://github.com/pypeclub/OpenPype/pull/2929)\n\n**🔀 Refactored code**\n\n- Blender: Use client query functions [\\#3331](https://github.com/pypeclub/OpenPype/pull/3331)\n- General: Define query functions [\\#3288](https://github.com/pypeclub/OpenPype/pull/3288)\n\n**Merged pull requests:**\n\n- Maya: add pointcache family to gpu cache loader [\\#3318](https://github.com/pypeclub/OpenPype/pull/3318)\n- Maya look: skip empty file attributes [\\#3274](https://github.com/pypeclub/OpenPype/pull/3274)\n\n## [3.10.0](https://github.com/pypeclub/OpenPype/tree/3.10.0) (2022-05-26)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.8...3.10.0)\n\n### 📖 Documentation\n\n- Docs: add all-contributors config and initial list [\\#3094](https://github.com/pypeclub/OpenPype/pull/3094)\n- Nuke docs with videos [\\#3052](https://github.com/pypeclub/OpenPype/pull/3052)\n\n**🆕 New features**\n\n- General: OpenPype modules publish plugins are registered in host [\\#3180](https://github.com/pypeclub/OpenPype/pull/3180)\n- General: Creator plugins from addons can be registered [\\#3179](https://github.com/pypeclub/OpenPype/pull/3179)\n- Ftrack: Single image reviewable [\\#3157](https://github.com/pypeclub/OpenPype/pull/3157)\n- Nuke: Expose write attributes to settings [\\#3123](https://github.com/pypeclub/OpenPype/pull/3123)\n- Hiero: Initial frame publish support [\\#3106](https://github.com/pypeclub/OpenPype/pull/3106)\n- Unreal: Render Publishing [\\#2917](https://github.com/pypeclub/OpenPype/pull/2917)\n- AfterEffects: Implemented New Publisher [\\#2838](https://github.com/pypeclub/OpenPype/pull/2838)\n- Unreal: Rendering implementation [\\#2410](https://github.com/pypeclub/OpenPype/pull/2410)\n\n**🚀 Enhancements**\n\n- Maya: FBX camera export [\\#3253](https://github.com/pypeclub/OpenPype/pull/3253)\n- General: updating common vendor `scriptmenu` to 1.5.2 [\\#3246](https://github.com/pypeclub/OpenPype/pull/3246)\n- Project Manager: Allow to paste Tasks into multiple assets at the same time [\\#3226](https://github.com/pypeclub/OpenPype/pull/3226)\n- Project manager: Sped up project load [\\#3216](https://github.com/pypeclub/OpenPype/pull/3216)\n- Loader UI: Speed issues of loader with sync server [\\#3199](https://github.com/pypeclub/OpenPype/pull/3199)\n- Looks: add basic support for Renderman [\\#3190](https://github.com/pypeclub/OpenPype/pull/3190)\n- Maya: added clean\\_import option to Import loader [\\#3181](https://github.com/pypeclub/OpenPype/pull/3181)\n- Add the scripts menu definition to nuke [\\#3168](https://github.com/pypeclub/OpenPype/pull/3168)\n- Maya: add maya 2023 to default applications [\\#3167](https://github.com/pypeclub/OpenPype/pull/3167)\n- Compressed bgeo publishing in SAP and Houdini loader [\\#3153](https://github.com/pypeclub/OpenPype/pull/3153)\n- General: Add 'dataclasses' to required python modules [\\#3149](https://github.com/pypeclub/OpenPype/pull/3149)\n- Hooks: Tweak logging grammar [\\#3147](https://github.com/pypeclub/OpenPype/pull/3147)\n- Nuke: settings for reformat node in CreateWriteRender node [\\#3143](https://github.com/pypeclub/OpenPype/pull/3143)\n- Houdini: Add loader for alembic through Alembic Archive node [\\#3140](https://github.com/pypeclub/OpenPype/pull/3140)\n- Publisher: UI Modifications and fixes [\\#3139](https://github.com/pypeclub/OpenPype/pull/3139)\n- General: Simplified OP modules/addons import [\\#3137](https://github.com/pypeclub/OpenPype/pull/3137)\n- Terminal: Tweak coloring of TrayModuleManager logging enabled states [\\#3133](https://github.com/pypeclub/OpenPype/pull/3133)\n- General: Cleanup some Loader docstrings [\\#3131](https://github.com/pypeclub/OpenPype/pull/3131)\n- Nuke: render instance with subset name filtered overrides [\\#3117](https://github.com/pypeclub/OpenPype/pull/3117)\n- Unreal: Layout and Camera update and remove functions reimplemented and improvements [\\#3116](https://github.com/pypeclub/OpenPype/pull/3116)\n- Settings: Remove environment groups from settings [\\#3115](https://github.com/pypeclub/OpenPype/pull/3115)\n- TVPaint: Match renderlayer key with other hosts [\\#3110](https://github.com/pypeclub/OpenPype/pull/3110)\n- Ftrack: AssetVersion status on publish [\\#3108](https://github.com/pypeclub/OpenPype/pull/3108)\n- Tray publisher: Simple families from settings [\\#3105](https://github.com/pypeclub/OpenPype/pull/3105)\n- Local Settings UI: Overlay messages on save and reset [\\#3104](https://github.com/pypeclub/OpenPype/pull/3104)\n- General: Remove repos related logic [\\#3087](https://github.com/pypeclub/OpenPype/pull/3087)\n- Standalone publisher: add support for bgeo and vdb [\\#3080](https://github.com/pypeclub/OpenPype/pull/3080)\n- Houdini: Fix FPS + outdated content pop-ups [\\#3079](https://github.com/pypeclub/OpenPype/pull/3079)\n- General: Add global log verbose arguments [\\#3070](https://github.com/pypeclub/OpenPype/pull/3070)\n- Flame: extract presets distribution [\\#3063](https://github.com/pypeclub/OpenPype/pull/3063)\n- Update collect\\_render.py [\\#3055](https://github.com/pypeclub/OpenPype/pull/3055)\n- SiteSync: Added compute\\_resource\\_sync\\_sites to sync\\_server\\_module [\\#2983](https://github.com/pypeclub/OpenPype/pull/2983)\n- Maya: Implement Hardware Renderer 2.0 support for Render Products [\\#2611](https://github.com/pypeclub/OpenPype/pull/2611)\n\n**🐛 Bug fixes**\n\n- nuke: use framerange issue [\\#3254](https://github.com/pypeclub/OpenPype/pull/3254)\n- Ftrack: Chunk sizes for queries has minimal condition [\\#3244](https://github.com/pypeclub/OpenPype/pull/3244)\n- Maya: renderman displays needs to be filtered [\\#3242](https://github.com/pypeclub/OpenPype/pull/3242)\n- Ftrack: Validate that the user exists on ftrack [\\#3237](https://github.com/pypeclub/OpenPype/pull/3237)\n- Maya: Fix support for multiple resolutions [\\#3236](https://github.com/pypeclub/OpenPype/pull/3236)\n- TVPaint: Look for more groups than 12 [\\#3228](https://github.com/pypeclub/OpenPype/pull/3228)\n- Hiero: debugging frame range and other 3.10 [\\#3222](https://github.com/pypeclub/OpenPype/pull/3222)\n- Project Manager: Fix persistent editors on project change [\\#3218](https://github.com/pypeclub/OpenPype/pull/3218)\n- Deadline: instance data overwrite fix [\\#3214](https://github.com/pypeclub/OpenPype/pull/3214)\n- Ftrack: Push hierarchical attributes action works [\\#3210](https://github.com/pypeclub/OpenPype/pull/3210)\n- Standalone Publisher: Always create new representation for thumbnail [\\#3203](https://github.com/pypeclub/OpenPype/pull/3203)\n- Photoshop: skip collector when automatic testing [\\#3202](https://github.com/pypeclub/OpenPype/pull/3202)\n- Nuke: render/workfile version sync doesn't work on farm [\\#3185](https://github.com/pypeclub/OpenPype/pull/3185)\n- Ftrack: Review image only if there are no mp4 reviews [\\#3183](https://github.com/pypeclub/OpenPype/pull/3183)\n- Ftrack: Locations deepcopy issue [\\#3177](https://github.com/pypeclub/OpenPype/pull/3177)\n- General: Avoid creating multiple thumbnails [\\#3176](https://github.com/pypeclub/OpenPype/pull/3176)\n- General/Hiero: better clip duration calculation [\\#3169](https://github.com/pypeclub/OpenPype/pull/3169)\n- General: Oiio conversion for ffmpeg checks for invalid characters [\\#3166](https://github.com/pypeclub/OpenPype/pull/3166)\n- Fix for attaching render to subset [\\#3164](https://github.com/pypeclub/OpenPype/pull/3164)\n- Harmony: fixed missing task name in render instance [\\#3163](https://github.com/pypeclub/OpenPype/pull/3163)\n- Ftrack: Action delete old versions formatting works [\\#3152](https://github.com/pypeclub/OpenPype/pull/3152)\n- Deadline: fix the output directory [\\#3144](https://github.com/pypeclub/OpenPype/pull/3144)\n- General: New Session schema [\\#3141](https://github.com/pypeclub/OpenPype/pull/3141)\n- General: Missing version on headless mode crash properly [\\#3136](https://github.com/pypeclub/OpenPype/pull/3136)\n- TVPaint: Composite layers in reversed order [\\#3135](https://github.com/pypeclub/OpenPype/pull/3135)\n- Nuke: fixing default settings for workfile builder loaders [\\#3120](https://github.com/pypeclub/OpenPype/pull/3120)\n- Nuke: fix anatomy imageio regex default [\\#3119](https://github.com/pypeclub/OpenPype/pull/3119)\n- General: Python 3 compatibility in queries [\\#3112](https://github.com/pypeclub/OpenPype/pull/3112)\n- General: TemplateResult can be copied [\\#3099](https://github.com/pypeclub/OpenPype/pull/3099)\n- General: Collect loaded versions skips not existing representations [\\#3095](https://github.com/pypeclub/OpenPype/pull/3095)\n- RoyalRender Control Submission - AVALON\\_APP\\_NAME default [\\#3091](https://github.com/pypeclub/OpenPype/pull/3091)\n- Ftrack: Update Create Folders action [\\#3089](https://github.com/pypeclub/OpenPype/pull/3089)\n- Maya: Collect Render fix any render cameras check [\\#3088](https://github.com/pypeclub/OpenPype/pull/3088)\n- Project Manager: Avoid unnecessary updates of asset documents [\\#3083](https://github.com/pypeclub/OpenPype/pull/3083)\n- Standalone publisher: Fix plugins install [\\#3077](https://github.com/pypeclub/OpenPype/pull/3077)\n- General: Extract review sequence is not converted with same names [\\#3076](https://github.com/pypeclub/OpenPype/pull/3076)\n- Webpublisher: Use variant value [\\#3068](https://github.com/pypeclub/OpenPype/pull/3068)\n- Nuke: Add aov matching even for remainder and prerender [\\#3060](https://github.com/pypeclub/OpenPype/pull/3060)\n- Fix support for Renderman in Maya [\\#3006](https://github.com/pypeclub/OpenPype/pull/3006)\n\n**🔀 Refactored code**\n\n- Avalon repo removed from Jobs workflow [\\#3193](https://github.com/pypeclub/OpenPype/pull/3193)\n- General: Remove remaining imports from avalon [\\#3130](https://github.com/pypeclub/OpenPype/pull/3130)\n- General: Move mongo db logic and remove avalon repository [\\#3066](https://github.com/pypeclub/OpenPype/pull/3066)\n- General: Move host install [\\#3009](https://github.com/pypeclub/OpenPype/pull/3009)\n\n**Merged pull requests:**\n\n- Harmony: message length in 21.1 [\\#3257](https://github.com/pypeclub/OpenPype/pull/3257)\n- Harmony: 21.1 fix [\\#3249](https://github.com/pypeclub/OpenPype/pull/3249)\n- Maya: added jpg to filter for Image Plane Loader [\\#3223](https://github.com/pypeclub/OpenPype/pull/3223)\n- Webpublisher: replace space by underscore in subset names [\\#3160](https://github.com/pypeclub/OpenPype/pull/3160)\n- StandalonePublisher: removed Extract Background plugins [\\#3093](https://github.com/pypeclub/OpenPype/pull/3093)\n- Nuke: added suspend\\_publish knob [\\#3078](https://github.com/pypeclub/OpenPype/pull/3078)\n- Bump async from 2.6.3 to 2.6.4 in /website [\\#3065](https://github.com/pypeclub/OpenPype/pull/3065)\n- SiteSync: Download all workfile inputs [\\#2966](https://github.com/pypeclub/OpenPype/pull/2966)\n- Photoshop: New Publisher [\\#2933](https://github.com/pypeclub/OpenPype/pull/2933)\n- Bump pillow from 9.0.0 to 9.0.1 [\\#2880](https://github.com/pypeclub/OpenPype/pull/2880)\n- AfterEffects: Allow configuration of default variant via Settings [\\#2856](https://github.com/pypeclub/OpenPype/pull/2856)\n\n## [3.9.8](https://github.com/pypeclub/OpenPype/tree/3.9.8) (2022-05-19)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.7...3.9.8)\n\n## [3.9.7](https://github.com/pypeclub/OpenPype/tree/3.9.7) (2022-05-11)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.6...3.9.7)\n\n## [3.9.6](https://github.com/pypeclub/OpenPype/tree/3.9.6) (2022-05-03)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.5...3.9.6)\n\n## [3.9.5](https://github.com/pypeclub/OpenPype/tree/3.9.5) (2022-04-25)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.4...3.9.5)\n\n## [3.9.4](https://github.com/pypeclub/OpenPype/tree/3.9.4) (2022-04-15)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.3...3.9.4)\n\n### 📖 Documentation\n\n- Documentation: more info about Tasks [\\#3062](https://github.com/pypeclub/OpenPype/pull/3062)\n- Documentation: Python requirements to 3.7.9 [\\#3035](https://github.com/pypeclub/OpenPype/pull/3035)\n- Website Docs: Remove unused pages [\\#2974](https://github.com/pypeclub/OpenPype/pull/2974)\n\n**🆕 New features**\n\n- General: Local overrides for environment variables [\\#3045](https://github.com/pypeclub/OpenPype/pull/3045)\n- Flame: Flare integration preparation [\\#2928](https://github.com/pypeclub/OpenPype/pull/2928)\n\n**🚀 Enhancements**\n\n- TVPaint: Added init file for worker to triggers missing sound file dialog [\\#3053](https://github.com/pypeclub/OpenPype/pull/3053)\n- Ftrack: Custom attributes can be filled in slate values [\\#3036](https://github.com/pypeclub/OpenPype/pull/3036)\n- Resolve environment variable in google drive credential path  [\\#3008](https://github.com/pypeclub/OpenPype/pull/3008)\n\n**🐛 Bug fixes**\n\n- GitHub: Updated push-protected action in github workflow [\\#3064](https://github.com/pypeclub/OpenPype/pull/3064)\n- Nuke: Typos in imports from Nuke implementation [\\#3061](https://github.com/pypeclub/OpenPype/pull/3061)\n- Hotfix: fixing deadline job publishing [\\#3059](https://github.com/pypeclub/OpenPype/pull/3059)\n- General: Extract Review handle invalid characters for ffmpeg [\\#3050](https://github.com/pypeclub/OpenPype/pull/3050)\n- Slate Review: Support to keep format on slate concatenation [\\#3049](https://github.com/pypeclub/OpenPype/pull/3049)\n- Webpublisher: fix processing of workfile [\\#3048](https://github.com/pypeclub/OpenPype/pull/3048)\n- Ftrack: Integrate ftrack api fix [\\#3044](https://github.com/pypeclub/OpenPype/pull/3044)\n- Webpublisher - removed wrong hardcoded family [\\#3043](https://github.com/pypeclub/OpenPype/pull/3043)\n- LibraryLoader: Use current project for asset query in families filter [\\#3042](https://github.com/pypeclub/OpenPype/pull/3042)\n- SiteSync: Providers ignore that site is disabled [\\#3041](https://github.com/pypeclub/OpenPype/pull/3041)\n- Unreal: Creator import fixes [\\#3040](https://github.com/pypeclub/OpenPype/pull/3040)\n- SiteSync: fix transitive alternate sites, fix dropdown in Local Settings [\\#3018](https://github.com/pypeclub/OpenPype/pull/3018)\n- Maya: invalid review flag on rendered AOVs [\\#2915](https://github.com/pypeclub/OpenPype/pull/2915)\n\n**Merged pull requests:**\n\n- Deadline: reworked pools assignment [\\#3051](https://github.com/pypeclub/OpenPype/pull/3051)\n- Houdini: Avoid ImportError on `hdefereval` when Houdini runs without UI [\\#2987](https://github.com/pypeclub/OpenPype/pull/2987)\n\n## [3.9.3](https://github.com/pypeclub/OpenPype/tree/3.9.3) (2022-04-07)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.2...3.9.3)\n\n### 📖 Documentation\n\n- Documentation: Added mention of adding My Drive as a root [\\#2999](https://github.com/pypeclub/OpenPype/pull/2999)\n- Website Docs: Manager Ftrack fix broken links [\\#2979](https://github.com/pypeclub/OpenPype/pull/2979)\n- Docs: Added MongoDB requirements [\\#2951](https://github.com/pypeclub/OpenPype/pull/2951)\n- Documentation: New publisher develop docs [\\#2896](https://github.com/pypeclub/OpenPype/pull/2896)\n\n**🆕 New features**\n\n- Ftrack: Add description integrator [\\#3027](https://github.com/pypeclub/OpenPype/pull/3027)\n- nuke: bypass baking [\\#2992](https://github.com/pypeclub/OpenPype/pull/2992)\n- Publishing textures for Unreal [\\#2988](https://github.com/pypeclub/OpenPype/pull/2988)\n- Maya to Unreal: Static and Skeletal Meshes [\\#2978](https://github.com/pypeclub/OpenPype/pull/2978)\n- Multiverse: Initial Support [\\#2908](https://github.com/pypeclub/OpenPype/pull/2908)\n\n**🚀 Enhancements**\n\n- General: default workfile subset name for workfile [\\#3011](https://github.com/pypeclub/OpenPype/pull/3011)\n- Ftrack: Add more options for note text of integrate ftrack note [\\#3025](https://github.com/pypeclub/OpenPype/pull/3025)\n- Console Interpreter: Changed how console splitter size are reused on show [\\#3016](https://github.com/pypeclub/OpenPype/pull/3016)\n- Deadline: Use more suitable name for sequence review logic [\\#3015](https://github.com/pypeclub/OpenPype/pull/3015)\n- Nuke: add concurrency attr to deadline job [\\#3005](https://github.com/pypeclub/OpenPype/pull/3005)\n- Photoshop: create image without instance [\\#3001](https://github.com/pypeclub/OpenPype/pull/3001)\n- TVPaint: Render scene family [\\#3000](https://github.com/pypeclub/OpenPype/pull/3000)\n- Deadline: priority configurable in Maya jobs [\\#2995](https://github.com/pypeclub/OpenPype/pull/2995)\n- Nuke: ReviewDataMov Read RAW attribute [\\#2985](https://github.com/pypeclub/OpenPype/pull/2985)\n- General: `METADATA_KEYS` constant as `frozenset` for optimal immutable lookup [\\#2980](https://github.com/pypeclub/OpenPype/pull/2980)\n- General: Tools with host filters [\\#2975](https://github.com/pypeclub/OpenPype/pull/2975)\n- Hero versions: Use custom templates [\\#2967](https://github.com/pypeclub/OpenPype/pull/2967)\n- Slack: Added configurable maximum file size of review upload to Slack [\\#2945](https://github.com/pypeclub/OpenPype/pull/2945)\n- NewPublisher: Prepared implementation of optional pyblish plugin [\\#2943](https://github.com/pypeclub/OpenPype/pull/2943)\n- TVPaint: Extractor to convert PNG into EXR [\\#2942](https://github.com/pypeclub/OpenPype/pull/2942)\n- Workfiles tool: Save as published workfiles [\\#2937](https://github.com/pypeclub/OpenPype/pull/2937)\n- Workfiles: Open published workfiles [\\#2925](https://github.com/pypeclub/OpenPype/pull/2925)\n- General: Default modules loaded dynamically [\\#2923](https://github.com/pypeclub/OpenPype/pull/2923)\n- CI: change the version bump logic [\\#2919](https://github.com/pypeclub/OpenPype/pull/2919)\n- Deadline: Add headless argument [\\#2916](https://github.com/pypeclub/OpenPype/pull/2916)\n- Nuke: Add no-audio Tag [\\#2911](https://github.com/pypeclub/OpenPype/pull/2911)\n- Ftrack: Fill workfile in custom attribute [\\#2906](https://github.com/pypeclub/OpenPype/pull/2906)\n- Nuke: improving readability [\\#2903](https://github.com/pypeclub/OpenPype/pull/2903)\n- Settings UI: Add simple tooltips for settings entities [\\#2901](https://github.com/pypeclub/OpenPype/pull/2901)\n\n**🐛 Bug fixes**\n\n- General: Fix validate asset docs plug-in filename and class name [\\#3029](https://github.com/pypeclub/OpenPype/pull/3029)\n- Deadline: Fixed default value of use sequence for review [\\#3033](https://github.com/pypeclub/OpenPype/pull/3033)\n- Settings UI: Version column can be extended so version are visible [\\#3032](https://github.com/pypeclub/OpenPype/pull/3032)\n- General: Fix import after movements [\\#3028](https://github.com/pypeclub/OpenPype/pull/3028)\n- Harmony: Added creating subset name for workfile from template [\\#3024](https://github.com/pypeclub/OpenPype/pull/3024)\n- AfterEffects: Added creating subset name for workfile from template [\\#3023](https://github.com/pypeclub/OpenPype/pull/3023)\n- General: Add example addons to ignored [\\#3022](https://github.com/pypeclub/OpenPype/pull/3022)\n- Maya: Remove missing import [\\#3017](https://github.com/pypeclub/OpenPype/pull/3017)\n- Ftrack: multiple  reviewable componets [\\#3012](https://github.com/pypeclub/OpenPype/pull/3012)\n- Tray publisher: Fixes after code movement [\\#3010](https://github.com/pypeclub/OpenPype/pull/3010)\n- Hosts: Remove path existence checks in 'add\\_implementation\\_envs' [\\#3004](https://github.com/pypeclub/OpenPype/pull/3004)\n- Nuke: fixing unicode type detection in effect loaders [\\#3002](https://github.com/pypeclub/OpenPype/pull/3002)\n- Fix - remove doubled dot in workfile created from template [\\#2998](https://github.com/pypeclub/OpenPype/pull/2998)\n- Nuke: removing redundant Ftrack asset when farm publishing [\\#2996](https://github.com/pypeclub/OpenPype/pull/2996)\n- PS: fix renaming subset incorrectly in PS [\\#2991](https://github.com/pypeclub/OpenPype/pull/2991)\n- Fix: Disable setuptools auto discovery [\\#2990](https://github.com/pypeclub/OpenPype/pull/2990)\n- AEL: fix opening existing workfile if no scene opened [\\#2989](https://github.com/pypeclub/OpenPype/pull/2989)\n- Maya: Don't do hardlinks on windows for look publishing [\\#2986](https://github.com/pypeclub/OpenPype/pull/2986)\n- Settings UI: Fix version completer on linux [\\#2981](https://github.com/pypeclub/OpenPype/pull/2981)\n- Photoshop: Fix creation of subset names in PS review and workfile [\\#2969](https://github.com/pypeclub/OpenPype/pull/2969)\n- Slack: Added default for review\\_upload\\_limit for Slack [\\#2965](https://github.com/pypeclub/OpenPype/pull/2965)\n- General: OIIO conversion for ffmeg can handle sequences [\\#2958](https://github.com/pypeclub/OpenPype/pull/2958)\n- Settings: Conditional dictionary avoid invalid logs [\\#2956](https://github.com/pypeclub/OpenPype/pull/2956)\n- General: Smaller fixes and typos [\\#2950](https://github.com/pypeclub/OpenPype/pull/2950)\n- LogViewer: Don't refresh on initialization [\\#2949](https://github.com/pypeclub/OpenPype/pull/2949)\n- nuke: python3 compatibility issue with `iteritems` [\\#2948](https://github.com/pypeclub/OpenPype/pull/2948)\n- General: anatomy data with correct task short key [\\#2947](https://github.com/pypeclub/OpenPype/pull/2947)\n- SceneInventory: Fix imports in UI [\\#2944](https://github.com/pypeclub/OpenPype/pull/2944)\n- Slack: add generic exception [\\#2941](https://github.com/pypeclub/OpenPype/pull/2941)\n- General: Python specific vendor paths on env injection [\\#2939](https://github.com/pypeclub/OpenPype/pull/2939)\n- General: More fail safe delete old versions [\\#2936](https://github.com/pypeclub/OpenPype/pull/2936)\n- Settings UI: Collapsed of collapsible wrapper works as expected [\\#2934](https://github.com/pypeclub/OpenPype/pull/2934)\n- Maya: Do not pass `set` to maya commands \\(fixes support for older maya versions\\) [\\#2932](https://github.com/pypeclub/OpenPype/pull/2932)\n- General: Don't print log record on OSError [\\#2926](https://github.com/pypeclub/OpenPype/pull/2926)\n- Hiero: Fix import of 'register\\_event\\_callback' [\\#2924](https://github.com/pypeclub/OpenPype/pull/2924)\n- Flame: centos related debugging [\\#2922](https://github.com/pypeclub/OpenPype/pull/2922)\n- Ftrack: Missing Ftrack id after editorial publish [\\#2905](https://github.com/pypeclub/OpenPype/pull/2905)\n- AfterEffects: Fix rendering for single frame in DL [\\#2875](https://github.com/pypeclub/OpenPype/pull/2875)\n\n**🔀 Refactored code**\n\n- General: Move plugins register and discover [\\#2935](https://github.com/pypeclub/OpenPype/pull/2935)\n- General: Move Attribute Definitions from pipeline [\\#2931](https://github.com/pypeclub/OpenPype/pull/2931)\n- General: Removed silo references and terminal splash [\\#2927](https://github.com/pypeclub/OpenPype/pull/2927)\n- General: Move pipeline constants to OpenPype [\\#2918](https://github.com/pypeclub/OpenPype/pull/2918)\n- General: Move formatting and workfile functions [\\#2914](https://github.com/pypeclub/OpenPype/pull/2914)\n- General: Move remaining plugins from avalon [\\#2912](https://github.com/pypeclub/OpenPype/pull/2912)\n\n**Merged pull requests:**\n\n- Maya: Allow to select invalid camera contents if no cameras found [\\#3030](https://github.com/pypeclub/OpenPype/pull/3030)\n- Bump paramiko from 2.9.2 to 2.10.1 [\\#2973](https://github.com/pypeclub/OpenPype/pull/2973)\n- Bump minimist from 1.2.5 to 1.2.6 in /website [\\#2954](https://github.com/pypeclub/OpenPype/pull/2954)\n- Bump node-forge from 1.2.1 to 1.3.0 in /website [\\#2953](https://github.com/pypeclub/OpenPype/pull/2953)\n- Maya - added transparency into review creator [\\#2952](https://github.com/pypeclub/OpenPype/pull/2952)\n\n## [3.9.2](https://github.com/pypeclub/OpenPype/tree/3.9.2) (2022-04-04)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.1...3.9.2)\n\n## [3.9.1](https://github.com/pypeclub/OpenPype/tree/3.9.1) (2022-03-18)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.0...3.9.1)\n\n**🚀 Enhancements**\n\n- General: Change how OPENPYPE\\_DEBUG value is handled [\\#2907](https://github.com/pypeclub/OpenPype/pull/2907)\n- nuke: imageio adding ocio config version 1.2 [\\#2897](https://github.com/pypeclub/OpenPype/pull/2897)\n- Flame: support for comment with xml attribute overrides [\\#2892](https://github.com/pypeclub/OpenPype/pull/2892)\n- Nuke: ExtractReviewSlate can handle more codes and profiles [\\#2879](https://github.com/pypeclub/OpenPype/pull/2879)\n- Flame: sequence used for reference video [\\#2869](https://github.com/pypeclub/OpenPype/pull/2869)\n\n**🐛 Bug fixes**\n\n- General: Fix use of Anatomy roots [\\#2904](https://github.com/pypeclub/OpenPype/pull/2904)\n- Fixing gap detection in extract review [\\#2902](https://github.com/pypeclub/OpenPype/pull/2902)\n- Pyblish Pype - ensure current state is correct when entering new group order [\\#2899](https://github.com/pypeclub/OpenPype/pull/2899)\n- SceneInventory: Fix import of load function [\\#2894](https://github.com/pypeclub/OpenPype/pull/2894)\n- Harmony - fixed creator issue [\\#2891](https://github.com/pypeclub/OpenPype/pull/2891)\n- General: Remove forgotten use of avalon Creator [\\#2885](https://github.com/pypeclub/OpenPype/pull/2885)\n- General: Avoid circular import [\\#2884](https://github.com/pypeclub/OpenPype/pull/2884)\n- Fixes for attaching loaded containers \\(\\#2837\\) [\\#2874](https://github.com/pypeclub/OpenPype/pull/2874)\n- Maya: Deformer node ids validation plugin [\\#2826](https://github.com/pypeclub/OpenPype/pull/2826)\n- Flame Babypublisher optimalization [\\#2806](https://github.com/pypeclub/OpenPype/pull/2806)\n- hotfix: OIIO tool path - add extension on windows [\\#2618](https://github.com/pypeclub/OpenPype/pull/2618)\n\n**🔀 Refactored code**\n\n- General: Reduce style usage to OpenPype repository [\\#2889](https://github.com/pypeclub/OpenPype/pull/2889)\n- General: Move loader logic from avalon to openpype [\\#2886](https://github.com/pypeclub/OpenPype/pull/2886)\n\n## [3.9.0](https://github.com/pypeclub/OpenPype/tree/3.9.0) (2022-03-14)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.2...3.9.0)\n\n**Deprecated:**\n\n- Houdini: Remove unused code [\\#2779](https://github.com/pypeclub/OpenPype/pull/2779)\n- Loader: Remove default family states for hosts from code [\\#2706](https://github.com/pypeclub/OpenPype/pull/2706)\n- AssetCreator: Remove the tool [\\#2845](https://github.com/pypeclub/OpenPype/pull/2845)\n\n### 📖 Documentation\n\n- Documentation: fixed broken links [\\#2799](https://github.com/pypeclub/OpenPype/pull/2799)\n- Documentation: broken link fix [\\#2785](https://github.com/pypeclub/OpenPype/pull/2785)\n- Documentation: link fixes [\\#2772](https://github.com/pypeclub/OpenPype/pull/2772)\n- Update docusaurus to latest version [\\#2760](https://github.com/pypeclub/OpenPype/pull/2760)\n- Various testing updates [\\#2726](https://github.com/pypeclub/OpenPype/pull/2726)\n- documentation: add example to `repack-version` command [\\#2669](https://github.com/pypeclub/OpenPype/pull/2669)\n- Update docusaurus [\\#2639](https://github.com/pypeclub/OpenPype/pull/2639)\n- Documentation: Fixed relative links [\\#2621](https://github.com/pypeclub/OpenPype/pull/2621)\n- Documentation: Change Photoshop & AfterEffects plugin path [\\#2878](https://github.com/pypeclub/OpenPype/pull/2878)\n\n**🆕 New features**\n\n- Flame: loading clips to reels [\\#2622](https://github.com/pypeclub/OpenPype/pull/2622)\n- General: Store settings by OpenPype version [\\#2570](https://github.com/pypeclub/OpenPype/pull/2570)\n\n**🚀 Enhancements**\n\n- New: Validation exceptions [\\#2841](https://github.com/pypeclub/OpenPype/pull/2841)\n- General: Set context environments for non host applications [\\#2803](https://github.com/pypeclub/OpenPype/pull/2803)\n- Houdini: Remove duplicate ValidateOutputNode plug-in [\\#2780](https://github.com/pypeclub/OpenPype/pull/2780)\n- Tray publisher: New Tray Publisher host \\(beta\\) [\\#2778](https://github.com/pypeclub/OpenPype/pull/2778)\n- Slack: Added regex for filtering on subset names [\\#2775](https://github.com/pypeclub/OpenPype/pull/2775)\n- Houdini: Implement Reset Frame Range [\\#2770](https://github.com/pypeclub/OpenPype/pull/2770)\n- Pyblish Pype: Remove redundant new line in installed fonts printing [\\#2758](https://github.com/pypeclub/OpenPype/pull/2758)\n- Flame: use Shot Name on segment for asset name [\\#2751](https://github.com/pypeclub/OpenPype/pull/2751)\n- Flame: adding validator source clip [\\#2746](https://github.com/pypeclub/OpenPype/pull/2746)\n- Work Files: Preserve subversion comment of current filename by default [\\#2734](https://github.com/pypeclub/OpenPype/pull/2734)\n- Maya: set Deadline job/batch name to original source workfile name instead of published workfile [\\#2733](https://github.com/pypeclub/OpenPype/pull/2733)\n- Ftrack: Disable ftrack module by default [\\#2732](https://github.com/pypeclub/OpenPype/pull/2732)\n- Project Manager: Disable add task, add asset and save button when not in a project [\\#2727](https://github.com/pypeclub/OpenPype/pull/2727)\n- dropbox handle big file [\\#2718](https://github.com/pypeclub/OpenPype/pull/2718)\n- Fusion Move PR: Minor tweaks to Fusion integration [\\#2716](https://github.com/pypeclub/OpenPype/pull/2716)\n- RoyalRender: Minor enhancements [\\#2700](https://github.com/pypeclub/OpenPype/pull/2700)\n- Nuke: prerender with review knob [\\#2691](https://github.com/pypeclub/OpenPype/pull/2691)\n- Maya configurable unit validator [\\#2680](https://github.com/pypeclub/OpenPype/pull/2680)\n- General: Add settings for CleanUpFarm and disable the plugin by default [\\#2679](https://github.com/pypeclub/OpenPype/pull/2679)\n- Project Manager: Only allow scroll wheel edits when spinbox is active [\\#2678](https://github.com/pypeclub/OpenPype/pull/2678)\n- Ftrack: Sync description to assets [\\#2670](https://github.com/pypeclub/OpenPype/pull/2670)\n- Houdini: Moved to OpenPype [\\#2658](https://github.com/pypeclub/OpenPype/pull/2658)\n- Maya: Move implementation to OpenPype [\\#2649](https://github.com/pypeclub/OpenPype/pull/2649)\n- General: FFmpeg conversion also check attribute string length [\\#2635](https://github.com/pypeclub/OpenPype/pull/2635)\n- Houdini: Load Arnold .ass procedurals into Houdini [\\#2606](https://github.com/pypeclub/OpenPype/pull/2606)\n- Deadline: Simplify GlobalJobPreLoad logic [\\#2605](https://github.com/pypeclub/OpenPype/pull/2605)\n- Houdini: Implement Arnold .ass standin extraction from Houdini \\(also support .ass.gz\\) [\\#2603](https://github.com/pypeclub/OpenPype/pull/2603)\n- New Publisher: New features and preparations for new standalone publisher [\\#2556](https://github.com/pypeclub/OpenPype/pull/2556)\n- Fix Maya 2022 Python 3 compatibility [\\#2445](https://github.com/pypeclub/OpenPype/pull/2445)\n- TVPaint: Use new publisher exceptions in validators [\\#2435](https://github.com/pypeclub/OpenPype/pull/2435)\n- Harmony: Added new style validations for New Publisher [\\#2434](https://github.com/pypeclub/OpenPype/pull/2434)\n- Aftereffects: New style validations for New publisher [\\#2430](https://github.com/pypeclub/OpenPype/pull/2430)\n- Farm publishing: New cleanup plugin for Maya renders on farm [\\#2390](https://github.com/pypeclub/OpenPype/pull/2390)\n- General: Subset name filtering in ExtractReview outpus [\\#2872](https://github.com/pypeclub/OpenPype/pull/2872)\n- NewPublisher: Descriptions and Icons in creator dialog [\\#2867](https://github.com/pypeclub/OpenPype/pull/2867)\n- NewPublisher: Changing task on publishing instance [\\#2863](https://github.com/pypeclub/OpenPype/pull/2863)\n- TrayPublisher: Choose project widget is more clear [\\#2859](https://github.com/pypeclub/OpenPype/pull/2859)\n- Maya: add loaded containers to published instance [\\#2837](https://github.com/pypeclub/OpenPype/pull/2837)\n- Ftrack: Can sync fps as string [\\#2836](https://github.com/pypeclub/OpenPype/pull/2836)\n- General: Custom function for find executable [\\#2822](https://github.com/pypeclub/OpenPype/pull/2822)\n- General: Color dialog UI fixes [\\#2817](https://github.com/pypeclub/OpenPype/pull/2817)\n- global: letter box calculated on output as last process [\\#2812](https://github.com/pypeclub/OpenPype/pull/2812)\n- Nuke: adding Reformat to baking mov plugin  [\\#2811](https://github.com/pypeclub/OpenPype/pull/2811)\n- Manager: Update all to latest button [\\#2805](https://github.com/pypeclub/OpenPype/pull/2805)\n- Houdini: Move Houdini Save Current File to beginning of ExtractorOrder [\\#2747](https://github.com/pypeclub/OpenPype/pull/2747)\n- Global: adding studio name/code to anatomy template formatting data [\\#2630](https://github.com/pypeclub/OpenPype/pull/2630)\n\n**🐛 Bug fixes**\n\n- Settings UI: Search case sensitivity [\\#2810](https://github.com/pypeclub/OpenPype/pull/2810)\n- resolve: fixing fusion module loading [\\#2802](https://github.com/pypeclub/OpenPype/pull/2802)\n- Ftrack: Unset task ids from asset versions before tasks are removed [\\#2800](https://github.com/pypeclub/OpenPype/pull/2800)\n- Slack: fail gracefully if slack exception [\\#2798](https://github.com/pypeclub/OpenPype/pull/2798)\n- Flame: Fix version string in default settings [\\#2783](https://github.com/pypeclub/OpenPype/pull/2783)\n- After Effects: Fix typo in name `afftereffects` -\\> `aftereffects` [\\#2768](https://github.com/pypeclub/OpenPype/pull/2768)\n- Houdini: Fix open last workfile [\\#2767](https://github.com/pypeclub/OpenPype/pull/2767)\n- Avoid renaming udim indexes [\\#2765](https://github.com/pypeclub/OpenPype/pull/2765)\n- Maya: Fix `unique_namespace` when in an namespace that is empty [\\#2759](https://github.com/pypeclub/OpenPype/pull/2759)\n- Loader UI: Fix right click in representation widget [\\#2757](https://github.com/pypeclub/OpenPype/pull/2757)\n- Harmony: Rendering in Deadline didn't work in other machines than submitter [\\#2754](https://github.com/pypeclub/OpenPype/pull/2754)\n- Aftereffects 2022 and Deadline [\\#2748](https://github.com/pypeclub/OpenPype/pull/2748)\n- Flame: bunch of bugs [\\#2745](https://github.com/pypeclub/OpenPype/pull/2745)\n- Maya: Save current scene on workfile publish [\\#2744](https://github.com/pypeclub/OpenPype/pull/2744)\n- Version Up: Preserve parts of filename after version number \\(like subversion\\) on version\\_up [\\#2741](https://github.com/pypeclub/OpenPype/pull/2741)\n- Loader UI: Multiple asset selection and underline colors fixed [\\#2731](https://github.com/pypeclub/OpenPype/pull/2731)\n- General: Fix loading of unused chars in xml format [\\#2729](https://github.com/pypeclub/OpenPype/pull/2729)\n- TVPaint: Set objectName with members [\\#2725](https://github.com/pypeclub/OpenPype/pull/2725)\n- General: Don't use 'objectName' from loaded references [\\#2715](https://github.com/pypeclub/OpenPype/pull/2715)\n- Settings: Studio Project anatomy is queried using right keys [\\#2711](https://github.com/pypeclub/OpenPype/pull/2711)\n- Local Settings: Additional applications don't break UI [\\#2710](https://github.com/pypeclub/OpenPype/pull/2710)\n- Maya: Remove some unused code [\\#2709](https://github.com/pypeclub/OpenPype/pull/2709)\n- Houdini: Fix refactor of Houdini host move for CreateArnoldAss [\\#2704](https://github.com/pypeclub/OpenPype/pull/2704)\n- LookAssigner: Fix imports after moving code to OpenPype repository [\\#2701](https://github.com/pypeclub/OpenPype/pull/2701)\n- Multiple hosts: unify menu style across hosts [\\#2693](https://github.com/pypeclub/OpenPype/pull/2693)\n- Maya Redshift fixes [\\#2692](https://github.com/pypeclub/OpenPype/pull/2692)\n- Maya: fix fps validation popup [\\#2685](https://github.com/pypeclub/OpenPype/pull/2685)\n- Houdini Explicitly collect correct frame name even in case of single frame render when `frameStart` is provided [\\#2676](https://github.com/pypeclub/OpenPype/pull/2676)\n- hiero: fix effect collector name and order [\\#2673](https://github.com/pypeclub/OpenPype/pull/2673)\n- Maya: Fix menu callbacks [\\#2671](https://github.com/pypeclub/OpenPype/pull/2671)\n- hiero: removing obsolete unsupported plugin [\\#2667](https://github.com/pypeclub/OpenPype/pull/2667)\n- Launcher: Fix access to 'data' attribute on actions [\\#2659](https://github.com/pypeclub/OpenPype/pull/2659)\n- Maya `vrscene` loader fixes [\\#2633](https://github.com/pypeclub/OpenPype/pull/2633)\n- Houdini: fix usd family in loader and integrators [\\#2631](https://github.com/pypeclub/OpenPype/pull/2631)\n- Maya: Add only reference node to look family container like with other families [\\#2508](https://github.com/pypeclub/OpenPype/pull/2508)\n- General: Missing time function [\\#2877](https://github.com/pypeclub/OpenPype/pull/2877)\n- Deadline: Fix plugin name for tile assemble [\\#2868](https://github.com/pypeclub/OpenPype/pull/2868)\n- Nuke: gizmo precollect fix [\\#2866](https://github.com/pypeclub/OpenPype/pull/2866)\n- General: Fix hardlink for windows [\\#2864](https://github.com/pypeclub/OpenPype/pull/2864)\n- General: ffmpeg was crashing on slate merge [\\#2860](https://github.com/pypeclub/OpenPype/pull/2860)\n- WebPublisher: Video file was published with one too many frame [\\#2858](https://github.com/pypeclub/OpenPype/pull/2858)\n- New Publisher: Error dialog got right styles [\\#2857](https://github.com/pypeclub/OpenPype/pull/2857)\n- General: Fix getattr clalback on dynamic modules [\\#2855](https://github.com/pypeclub/OpenPype/pull/2855)\n- Nuke: slate resolution to input video resolution [\\#2853](https://github.com/pypeclub/OpenPype/pull/2853)\n- WebPublisher: Fix username stored in DB [\\#2852](https://github.com/pypeclub/OpenPype/pull/2852)\n- WebPublisher: Fix wrong number of frames for video file [\\#2851](https://github.com/pypeclub/OpenPype/pull/2851)\n- Nuke: Fix family test in validate\\_write\\_legacy to work with stillImage [\\#2847](https://github.com/pypeclub/OpenPype/pull/2847)\n- Nuke: fix multiple baking profile farm publishing [\\#2842](https://github.com/pypeclub/OpenPype/pull/2842)\n- Blender: Fixed parameters for FBX export of the camera [\\#2840](https://github.com/pypeclub/OpenPype/pull/2840)\n- Maya: Stop creation of reviews for Cryptomattes [\\#2832](https://github.com/pypeclub/OpenPype/pull/2832)\n- Deadline: Remove recreated event [\\#2828](https://github.com/pypeclub/OpenPype/pull/2828)\n- Deadline: Added missing events folder [\\#2827](https://github.com/pypeclub/OpenPype/pull/2827)\n- Settings: Missing document with OP versions may break start of OpenPype [\\#2825](https://github.com/pypeclub/OpenPype/pull/2825)\n- Deadline: more detailed temp file name for environment json [\\#2824](https://github.com/pypeclub/OpenPype/pull/2824)\n- General: Host name was formed from obsolete code [\\#2821](https://github.com/pypeclub/OpenPype/pull/2821)\n- Settings UI: Fix \"Apply from\" action [\\#2820](https://github.com/pypeclub/OpenPype/pull/2820)\n- Ftrack: Job killer with missing user [\\#2819](https://github.com/pypeclub/OpenPype/pull/2819)\n- Nuke: Use AVALON\\_APP to get value for \"app\" key [\\#2818](https://github.com/pypeclub/OpenPype/pull/2818)\n- StandalonePublisher: use dynamic groups in subset names [\\#2816](https://github.com/pypeclub/OpenPype/pull/2816)\n\n**🔀 Refactored code**\n\n- Ftrack: Moved module one hierarchy level higher [\\#2792](https://github.com/pypeclub/OpenPype/pull/2792)\n- SyncServer: Moved module one hierarchy level higher [\\#2791](https://github.com/pypeclub/OpenPype/pull/2791)\n- Royal render: Move module one hierarchy level higher [\\#2790](https://github.com/pypeclub/OpenPype/pull/2790)\n- Deadline: Move module one hierarchy level higher [\\#2789](https://github.com/pypeclub/OpenPype/pull/2789)\n- Refactor: move webserver tool to openpype [\\#2876](https://github.com/pypeclub/OpenPype/pull/2876)\n- General: Move create logic from avalon to OpenPype [\\#2854](https://github.com/pypeclub/OpenPype/pull/2854)\n- General: Add vendors from avalon [\\#2848](https://github.com/pypeclub/OpenPype/pull/2848)\n- General: Basic event system [\\#2846](https://github.com/pypeclub/OpenPype/pull/2846)\n- General: Move change context functions [\\#2839](https://github.com/pypeclub/OpenPype/pull/2839)\n- Tools: Don't use avalon tools code [\\#2829](https://github.com/pypeclub/OpenPype/pull/2829)\n- Move Unreal Implementation to OpenPype [\\#2823](https://github.com/pypeclub/OpenPype/pull/2823)\n- General: Extract template formatting from anatomy [\\#2766](https://github.com/pypeclub/OpenPype/pull/2766)\n\n**Merged pull requests:**\n\n- Fusion: Moved implementation into OpenPype [\\#2713](https://github.com/pypeclub/OpenPype/pull/2713)\n- TVPaint: Plugin build without dependencies [\\#2705](https://github.com/pypeclub/OpenPype/pull/2705)\n- Webpublisher: Photoshop create a beauty png [\\#2689](https://github.com/pypeclub/OpenPype/pull/2689)\n- Ftrack: Hierarchical attributes are queried properly [\\#2682](https://github.com/pypeclub/OpenPype/pull/2682)\n- Maya: Add Validate Frame Range settings [\\#2661](https://github.com/pypeclub/OpenPype/pull/2661)\n- Harmony: move to Openpype [\\#2657](https://github.com/pypeclub/OpenPype/pull/2657)\n- Maya: cleanup duplicate rendersetup code [\\#2642](https://github.com/pypeclub/OpenPype/pull/2642)\n- Deadline: Be able to pass Mongo url to job [\\#2616](https://github.com/pypeclub/OpenPype/pull/2616)\n\n## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.1...3.8.2)\n\n### 📖 Documentation\n\n- Cosmetics: Fix common typos in openpype/website [\\#2617](https://github.com/pypeclub/OpenPype/pull/2617)\n\n**🚀 Enhancements**\n\n- TVPaint: Image loaders also work on review family [\\#2638](https://github.com/pypeclub/OpenPype/pull/2638)\n- General: Project backup tools [\\#2629](https://github.com/pypeclub/OpenPype/pull/2629)\n- nuke: adding clear button to write nodes [\\#2627](https://github.com/pypeclub/OpenPype/pull/2627)\n- Ftrack: Family to Asset type mapping is in settings [\\#2602](https://github.com/pypeclub/OpenPype/pull/2602)\n- Nuke: load color space from representation data [\\#2576](https://github.com/pypeclub/OpenPype/pull/2576)\n\n**🐛 Bug fixes**\n\n- Fix pulling of cx\\_freeze 6.10 [\\#2628](https://github.com/pypeclub/OpenPype/pull/2628)\n- Global: fix broken otio review extractor [\\#2590](https://github.com/pypeclub/OpenPype/pull/2590)\n\n**Merged pull requests:**\n\n- WebPublisher: fix instance duplicates [\\#2641](https://github.com/pypeclub/OpenPype/pull/2641)\n- Fix - safer pulling of task name for webpublishing from PS [\\#2613](https://github.com/pypeclub/OpenPype/pull/2613)\n\n## [3.8.1](https://github.com/pypeclub/OpenPype/tree/3.8.1) (2022-02-01)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.0...3.8.1)\n\n**🚀 Enhancements**\n\n- Webpublisher: Thumbnail extractor [\\#2600](https://github.com/pypeclub/OpenPype/pull/2600)\n- Loader: Allow to toggle default family filters between \"include\" or \"exclude\" filtering [\\#2541](https://github.com/pypeclub/OpenPype/pull/2541)\n- Launcher: Added context menu to to skip opening last workfile [\\#2536](https://github.com/pypeclub/OpenPype/pull/2536)\n- Unreal: JSON Layout Loading support [\\#2066](https://github.com/pypeclub/OpenPype/pull/2066)\n\n**🐛 Bug fixes**\n\n- Release/3.8.0 [\\#2619](https://github.com/pypeclub/OpenPype/pull/2619)\n- Settings: Enum does not store empty string if has single item to select [\\#2615](https://github.com/pypeclub/OpenPype/pull/2615)\n- switch distutils to sysconfig for `get_platform()` [\\#2594](https://github.com/pypeclub/OpenPype/pull/2594)\n- Fix poetry index and speedcopy update [\\#2589](https://github.com/pypeclub/OpenPype/pull/2589)\n- Webpublisher: Fix - subset names from processed .psd used wrong value for task [\\#2586](https://github.com/pypeclub/OpenPype/pull/2586)\n- `vrscene` creator Deadline webservice URL handling [\\#2580](https://github.com/pypeclub/OpenPype/pull/2580)\n- global: track name was failing if duplicated root word in name [\\#2568](https://github.com/pypeclub/OpenPype/pull/2568)\n- Validate Maya Rig produces no cycle errors [\\#2484](https://github.com/pypeclub/OpenPype/pull/2484)\n\n**Merged pull requests:**\n\n- Bump pillow from 8.4.0 to 9.0.0 [\\#2595](https://github.com/pypeclub/OpenPype/pull/2595)\n- Webpublisher: Skip version collect [\\#2591](https://github.com/pypeclub/OpenPype/pull/2591)\n- build\\(deps\\): bump pillow from 8.4.0 to 9.0.0 [\\#2523](https://github.com/pypeclub/OpenPype/pull/2523)\n\n## [3.8.0](https://github.com/pypeclub/OpenPype/tree/3.8.0) (2022-01-24)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.7.0...3.8.0)\n\n### 📖 Documentation\n\n- Variable in docs renamed to proper name [\\#2546](https://github.com/pypeclub/OpenPype/pull/2546)\n\n**🆕 New features**\n\n- Flame: extracting segments with trans-coding  [\\#2547](https://github.com/pypeclub/OpenPype/pull/2547)\n- Maya : V-Ray Proxy - load all ABC files via proxy [\\#2544](https://github.com/pypeclub/OpenPype/pull/2544)\n- Maya to Unreal: Extended static mesh workflow [\\#2537](https://github.com/pypeclub/OpenPype/pull/2537)\n- Flame: collecting publishable instances [\\#2519](https://github.com/pypeclub/OpenPype/pull/2519)\n- Flame: create publishable clips [\\#2495](https://github.com/pypeclub/OpenPype/pull/2495)\n- Flame: OpenTimelineIO Export Modul [\\#2398](https://github.com/pypeclub/OpenPype/pull/2398)\n\n**🚀 Enhancements**\n\n- Webpublisher: Moved error at the beginning of the log [\\#2559](https://github.com/pypeclub/OpenPype/pull/2559)\n- Ftrack: Use ApplicationManager to get DJV path [\\#2558](https://github.com/pypeclub/OpenPype/pull/2558)\n- Webpublisher: Added endpoint to reprocess batch through UI [\\#2555](https://github.com/pypeclub/OpenPype/pull/2555)\n- Settings: PathInput strip passed string [\\#2550](https://github.com/pypeclub/OpenPype/pull/2550)\n- Global: Exctract Review anatomy fill data with output name [\\#2548](https://github.com/pypeclub/OpenPype/pull/2548)\n- Cosmetics: Clean up some cosmetics / typos [\\#2542](https://github.com/pypeclub/OpenPype/pull/2542)\n- General: Validate if current process OpenPype version is requested version [\\#2529](https://github.com/pypeclub/OpenPype/pull/2529)\n- General: Be able to use anatomy data in ffmpeg output arguments [\\#2525](https://github.com/pypeclub/OpenPype/pull/2525)\n- Expose toggle publish plug-in settings for Maya Look Shading Engine Naming [\\#2521](https://github.com/pypeclub/OpenPype/pull/2521)\n- Photoshop: Move implementation to OpenPype [\\#2510](https://github.com/pypeclub/OpenPype/pull/2510)\n- TimersManager: Move module one hierarchy higher [\\#2501](https://github.com/pypeclub/OpenPype/pull/2501)\n- Slack: notifications are sent with Openpype logo and bot name [\\#2499](https://github.com/pypeclub/OpenPype/pull/2499)\n- Slack: Add review to notification message [\\#2498](https://github.com/pypeclub/OpenPype/pull/2498)\n- Ftrack: Event handlers settings [\\#2496](https://github.com/pypeclub/OpenPype/pull/2496)\n- Tools: Fix style and modality of errors in loader and creator [\\#2489](https://github.com/pypeclub/OpenPype/pull/2489)\n- Maya: Collect 'fps' animation data only for \"review\" instances [\\#2486](https://github.com/pypeclub/OpenPype/pull/2486)\n- Project Manager: Remove project button cleanup [\\#2482](https://github.com/pypeclub/OpenPype/pull/2482)\n- Tools: Be able to change models of tasks and assets widgets [\\#2475](https://github.com/pypeclub/OpenPype/pull/2475)\n- Publish pype: Reduce publish process defering [\\#2464](https://github.com/pypeclub/OpenPype/pull/2464)\n- Maya: Improve speed of Collect History logic [\\#2460](https://github.com/pypeclub/OpenPype/pull/2460)\n- Maya: Validate Rig Controllers - fix Error: in script editor [\\#2459](https://github.com/pypeclub/OpenPype/pull/2459)\n- Maya: Validate NGONs simplify and speed-up [\\#2458](https://github.com/pypeclub/OpenPype/pull/2458)\n- Maya: Optimize Validate Locked Normals speed for dense polymeshes [\\#2457](https://github.com/pypeclub/OpenPype/pull/2457)\n- Maya: Refactor missing \\_get\\_reference\\_node method [\\#2455](https://github.com/pypeclub/OpenPype/pull/2455)\n- Houdini: Remove broken unique name counter [\\#2450](https://github.com/pypeclub/OpenPype/pull/2450)\n- Maya: Improve lib.polyConstraint performance when Select tool is not the active tool context [\\#2447](https://github.com/pypeclub/OpenPype/pull/2447)\n- General: Validate third party before build [\\#2425](https://github.com/pypeclub/OpenPype/pull/2425)\n- Maya : add option to not group reference in ReferenceLoader [\\#2383](https://github.com/pypeclub/OpenPype/pull/2383)\n\n**🐛 Bug fixes**\n\n- AfterEffects: Fix - removed obsolete import [\\#2577](https://github.com/pypeclub/OpenPype/pull/2577)\n- General: OpenPype version updates [\\#2575](https://github.com/pypeclub/OpenPype/pull/2575)\n- Ftrack: Delete action revision [\\#2563](https://github.com/pypeclub/OpenPype/pull/2563)\n- Webpublisher: ftrack shows incorrect user names [\\#2560](https://github.com/pypeclub/OpenPype/pull/2560)\n- General: Do not validate version if build does not support it [\\#2557](https://github.com/pypeclub/OpenPype/pull/2557)\n- Webpublisher: Fixed progress reporting [\\#2553](https://github.com/pypeclub/OpenPype/pull/2553)\n- Fix Maya AssProxyLoader version switch [\\#2551](https://github.com/pypeclub/OpenPype/pull/2551)\n- General: Fix install thread in igniter [\\#2549](https://github.com/pypeclub/OpenPype/pull/2549)\n- Houdini: vdbcache family preserve frame numbers on publish integration + enable validate version for Houdini [\\#2535](https://github.com/pypeclub/OpenPype/pull/2535)\n- Maya: Fix Load VDB to V-Ray [\\#2533](https://github.com/pypeclub/OpenPype/pull/2533)\n- Maya: ReferenceLoader fix not unique group name error for attach to root [\\#2532](https://github.com/pypeclub/OpenPype/pull/2532)\n- Maya: namespaced context go back to original namespace when started from inside a namespace [\\#2531](https://github.com/pypeclub/OpenPype/pull/2531)\n- Fix create zip tool - path argument [\\#2522](https://github.com/pypeclub/OpenPype/pull/2522)\n- Maya: Fix Extract Look with space in names [\\#2518](https://github.com/pypeclub/OpenPype/pull/2518)\n- Fix published frame content for sequence starting with 0 [\\#2513](https://github.com/pypeclub/OpenPype/pull/2513)\n- Maya: reset empty string attributes correctly to \"\" instead of \"None\" [\\#2506](https://github.com/pypeclub/OpenPype/pull/2506)\n- Improve FusionPreLaunch hook errors [\\#2505](https://github.com/pypeclub/OpenPype/pull/2505)\n- General: Settings work if OpenPypeVersion is available [\\#2494](https://github.com/pypeclub/OpenPype/pull/2494)\n- General: PYTHONPATH may break OpenPype dependencies [\\#2493](https://github.com/pypeclub/OpenPype/pull/2493)\n- General: Modules import function output fix [\\#2492](https://github.com/pypeclub/OpenPype/pull/2492)\n- AE: fix hiding of alert window below Publish [\\#2491](https://github.com/pypeclub/OpenPype/pull/2491)\n- Workfiles tool: Files widget show files on first show [\\#2488](https://github.com/pypeclub/OpenPype/pull/2488)\n- General: Custom template paths filter fix [\\#2483](https://github.com/pypeclub/OpenPype/pull/2483)\n- Loader: Remove always on top flag in tray [\\#2480](https://github.com/pypeclub/OpenPype/pull/2480)\n- General: Anatomy does not return root envs as unicode [\\#2465](https://github.com/pypeclub/OpenPype/pull/2465)\n- Maya: Validate Shape Zero do not keep fixed geometry vertices selected/active after repair [\\#2456](https://github.com/pypeclub/OpenPype/pull/2456)\n\n**Merged pull requests:**\n\n- AfterEffects: Move implementation to OpenPype [\\#2543](https://github.com/pypeclub/OpenPype/pull/2543)\n- Maya: Remove Maya Look Assigner check on startup [\\#2540](https://github.com/pypeclub/OpenPype/pull/2540)\n- build\\(deps\\): bump shelljs from 0.8.4 to 0.8.5 in /website [\\#2538](https://github.com/pypeclub/OpenPype/pull/2538)\n- build\\(deps\\): bump follow-redirects from 1.14.4 to 1.14.7 in /website [\\#2534](https://github.com/pypeclub/OpenPype/pull/2534)\n- Nuke: Merge avalon's implementation into OpenPype [\\#2514](https://github.com/pypeclub/OpenPype/pull/2514)\n- Maya: Vray fix proxies look assignment [\\#2392](https://github.com/pypeclub/OpenPype/pull/2392)\n- Bump algoliasearch-helper from 3.4.4 to 3.6.2 in /website [\\#2297](https://github.com/pypeclub/OpenPype/pull/2297)\n\n## [3.7.0](https://github.com/pypeclub/OpenPype/tree/3.7.0) (2022-01-04)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.4...3.7.0)\n\n**Deprecated:**\n\n- General: Default modules hierarchy n2 [\\#2368](https://github.com/pypeclub/OpenPype/pull/2368)\n\n### 📖 Documentation\n\n- docs\\[website\\]: Add Ellipse Studio \\(logo\\) as an OpenPype contributor [\\#2324](https://github.com/pypeclub/OpenPype/pull/2324)\n\n**🆕 New features**\n\n- Settings UI use OpenPype styles [\\#2296](https://github.com/pypeclub/OpenPype/pull/2296)\n- Store typed version dependencies for workfiles [\\#2192](https://github.com/pypeclub/OpenPype/pull/2192)\n- OpenPypeV3: add key task type, task shortname and user to path templating construction [\\#2157](https://github.com/pypeclub/OpenPype/pull/2157)\n- Nuke: Alembic model workflow [\\#2140](https://github.com/pypeclub/OpenPype/pull/2140)\n- TVPaint: Load workfile from published. [\\#1980](https://github.com/pypeclub/OpenPype/pull/1980)\n\n**🚀 Enhancements**\n\n- General: Workdir extra folders [\\#2462](https://github.com/pypeclub/OpenPype/pull/2462)\n- Photoshop: New style validations for New publisher [\\#2429](https://github.com/pypeclub/OpenPype/pull/2429)\n- General: Environment variables groups [\\#2424](https://github.com/pypeclub/OpenPype/pull/2424)\n- Unreal: Dynamic menu created in Python [\\#2422](https://github.com/pypeclub/OpenPype/pull/2422)\n- Settings UI: Hyperlinks to settings [\\#2420](https://github.com/pypeclub/OpenPype/pull/2420)\n- Modules: JobQueue module moved one hierarchy level higher [\\#2419](https://github.com/pypeclub/OpenPype/pull/2419)\n- TimersManager: Start timer post launch hook [\\#2418](https://github.com/pypeclub/OpenPype/pull/2418)\n- General: Run applications as separate processes under linux [\\#2408](https://github.com/pypeclub/OpenPype/pull/2408)\n- Ftrack: Check existence of object type on recreation [\\#2404](https://github.com/pypeclub/OpenPype/pull/2404)\n- Enhancement: Global cleanup plugin that explicitly remove paths from context [\\#2402](https://github.com/pypeclub/OpenPype/pull/2402)\n- General: MongoDB ability to specify replica set groups [\\#2401](https://github.com/pypeclub/OpenPype/pull/2401)\n- Flame: moving `utility_scripts` to api folder also with `scripts` [\\#2385](https://github.com/pypeclub/OpenPype/pull/2385)\n- Centos 7 dependency compatibility [\\#2384](https://github.com/pypeclub/OpenPype/pull/2384)\n- Enhancement: Settings: Use project settings values from another project [\\#2382](https://github.com/pypeclub/OpenPype/pull/2382)\n- Blender 3: Support auto install for new blender version [\\#2377](https://github.com/pypeclub/OpenPype/pull/2377)\n- Maya add render image path to settings [\\#2375](https://github.com/pypeclub/OpenPype/pull/2375)\n- Settings: Webpublisher in hosts enum [\\#2367](https://github.com/pypeclub/OpenPype/pull/2367)\n- Hiero: python3 compatibility [\\#2365](https://github.com/pypeclub/OpenPype/pull/2365)\n- Burnins: Be able recognize mxf OPAtom format [\\#2361](https://github.com/pypeclub/OpenPype/pull/2361)\n- Maya: Add is\\_static\\_image\\_plane and is\\_in\\_all\\_views option in imagePlaneLoader [\\#2356](https://github.com/pypeclub/OpenPype/pull/2356)\n- Local settings: Copyable studio paths [\\#2349](https://github.com/pypeclub/OpenPype/pull/2349)\n- Assets Widget: Clear model on project change [\\#2345](https://github.com/pypeclub/OpenPype/pull/2345)\n- General: OpenPype default modules hierarchy [\\#2338](https://github.com/pypeclub/OpenPype/pull/2338)\n- TVPaint: Move implementation to OpenPype [\\#2336](https://github.com/pypeclub/OpenPype/pull/2336)\n- General: FFprobe error exception contain original error message [\\#2328](https://github.com/pypeclub/OpenPype/pull/2328)\n- Resolve: Add experimental button to menu [\\#2325](https://github.com/pypeclub/OpenPype/pull/2325)\n- Hiero: Add experimental tools action [\\#2323](https://github.com/pypeclub/OpenPype/pull/2323)\n- Input links: Cleanup and unification of differences [\\#2322](https://github.com/pypeclub/OpenPype/pull/2322)\n- General: Don't validate vendor bin with executing them [\\#2317](https://github.com/pypeclub/OpenPype/pull/2317)\n- General: Multilayer EXRs support [\\#2315](https://github.com/pypeclub/OpenPype/pull/2315)\n- General: Run process log stderr as info log level [\\#2309](https://github.com/pypeclub/OpenPype/pull/2309)\n- General: Reduce vendor imports [\\#2305](https://github.com/pypeclub/OpenPype/pull/2305)\n- Tools: Cleanup of unused classes [\\#2304](https://github.com/pypeclub/OpenPype/pull/2304)\n- Project Manager: Added ability to delete project [\\#2298](https://github.com/pypeclub/OpenPype/pull/2298)\n- Ftrack: Synchronize input links [\\#2287](https://github.com/pypeclub/OpenPype/pull/2287)\n- StandalonePublisher: Remove unused plugin ExtractHarmonyZip [\\#2277](https://github.com/pypeclub/OpenPype/pull/2277)\n- Ftrack: Support multiple reviews [\\#2271](https://github.com/pypeclub/OpenPype/pull/2271)\n- Ftrack: Remove unused clean component plugin [\\#2269](https://github.com/pypeclub/OpenPype/pull/2269)\n- Royal Render: Support for rr channels in separate dirs [\\#2268](https://github.com/pypeclub/OpenPype/pull/2268)\n- Houdini: Add experimental tools action [\\#2267](https://github.com/pypeclub/OpenPype/pull/2267)\n- Nuke: extract baked review videos presets [\\#2248](https://github.com/pypeclub/OpenPype/pull/2248)\n- TVPaint: Workers rendering [\\#2209](https://github.com/pypeclub/OpenPype/pull/2209)\n- OpenPypeV3: Add key parent asset to path templating construction [\\#2186](https://github.com/pypeclub/OpenPype/pull/2186)\n\n**🐛 Bug fixes**\n\n- TVPaint: Create render layer dialog is in front [\\#2471](https://github.com/pypeclub/OpenPype/pull/2471)\n- Short Pyblish plugin path [\\#2428](https://github.com/pypeclub/OpenPype/pull/2428)\n- PS: Introduced settings for invalid characters to use in ValidateNaming plugin [\\#2417](https://github.com/pypeclub/OpenPype/pull/2417)\n- Settings UI: Breadcrumbs path does not create new entities [\\#2416](https://github.com/pypeclub/OpenPype/pull/2416)\n- AfterEffects: Variant 2022 is in defaults but missing in schemas [\\#2412](https://github.com/pypeclub/OpenPype/pull/2412)\n- Nuke: baking representations was not additive [\\#2406](https://github.com/pypeclub/OpenPype/pull/2406)\n- General: Fix access to environments from default settings [\\#2403](https://github.com/pypeclub/OpenPype/pull/2403)\n- Fix: Placeholder Input color set fix [\\#2399](https://github.com/pypeclub/OpenPype/pull/2399)\n- Settings: Fix state change of wrapper label [\\#2396](https://github.com/pypeclub/OpenPype/pull/2396)\n- Flame: fix ftrack publisher [\\#2381](https://github.com/pypeclub/OpenPype/pull/2381)\n- hiero: solve custom ocio path  [\\#2379](https://github.com/pypeclub/OpenPype/pull/2379)\n- hiero: fix workio and flatten [\\#2378](https://github.com/pypeclub/OpenPype/pull/2378)\n- Nuke: fixing menu re-drawing during context change  [\\#2374](https://github.com/pypeclub/OpenPype/pull/2374)\n- Webpublisher: Fix assignment of families of TVpaint instances [\\#2373](https://github.com/pypeclub/OpenPype/pull/2373)\n- Nuke: fixing node name based on switched asset name [\\#2369](https://github.com/pypeclub/OpenPype/pull/2369)\n- JobQueue: Fix loading of settings [\\#2362](https://github.com/pypeclub/OpenPype/pull/2362)\n- Tools: Placeholder color [\\#2359](https://github.com/pypeclub/OpenPype/pull/2359)\n- Launcher: Minimize button on MacOs [\\#2355](https://github.com/pypeclub/OpenPype/pull/2355)\n- StandalonePublisher: Fix import of constant [\\#2354](https://github.com/pypeclub/OpenPype/pull/2354)\n- Houdini: Fix HDA creation [\\#2350](https://github.com/pypeclub/OpenPype/pull/2350)\n- Adobe products show issue [\\#2347](https://github.com/pypeclub/OpenPype/pull/2347)\n- Maya Look Assigner: Fix Python 3 compatibility [\\#2343](https://github.com/pypeclub/OpenPype/pull/2343)\n- Remove wrongly used host for hook [\\#2342](https://github.com/pypeclub/OpenPype/pull/2342)\n- Tools: Use Qt context on tools show [\\#2340](https://github.com/pypeclub/OpenPype/pull/2340)\n- Flame: Fix default argument value in custom dictionary [\\#2339](https://github.com/pypeclub/OpenPype/pull/2339)\n- Timers Manager: Disable auto stop timer on linux platform [\\#2334](https://github.com/pypeclub/OpenPype/pull/2334)\n- nuke: bake preset single input exception  [\\#2331](https://github.com/pypeclub/OpenPype/pull/2331)\n- Hiero: fixing multiple templates at a hierarchy parent [\\#2330](https://github.com/pypeclub/OpenPype/pull/2330)\n- Fix - provider icons are pulled from a folder [\\#2326](https://github.com/pypeclub/OpenPype/pull/2326)\n- InputLinks: Typo in \"inputLinks\" key [\\#2314](https://github.com/pypeclub/OpenPype/pull/2314)\n- Deadline timeout and logging [\\#2312](https://github.com/pypeclub/OpenPype/pull/2312)\n- nuke: do not multiply representation on class method [\\#2311](https://github.com/pypeclub/OpenPype/pull/2311)\n- Workfiles tool: Fix task formatting [\\#2306](https://github.com/pypeclub/OpenPype/pull/2306)\n- Delivery: Fix delivery paths created on windows [\\#2302](https://github.com/pypeclub/OpenPype/pull/2302)\n- Maya: Deadline - fix limit groups [\\#2295](https://github.com/pypeclub/OpenPype/pull/2295)\n- Royal Render: Fix plugin order and OpenPype auto-detection [\\#2291](https://github.com/pypeclub/OpenPype/pull/2291)\n- New Publisher: Fix mapping of indexes [\\#2285](https://github.com/pypeclub/OpenPype/pull/2285)\n- Alternate site for site sync doesnt work for sequences [\\#2284](https://github.com/pypeclub/OpenPype/pull/2284)\n- FFmpeg: Execute ffprobe using list of arguments instead of string command [\\#2281](https://github.com/pypeclub/OpenPype/pull/2281)\n- Nuke: Anatomy fill data use task as dictionary [\\#2278](https://github.com/pypeclub/OpenPype/pull/2278)\n- Bug: fix variable name \\_asset\\_id in workfiles application [\\#2274](https://github.com/pypeclub/OpenPype/pull/2274)\n- Version handling fixes [\\#2272](https://github.com/pypeclub/OpenPype/pull/2272)\n\n**Merged pull requests:**\n\n- Maya: Replaced PATH usage with vendored oiio path for maketx utility [\\#2405](https://github.com/pypeclub/OpenPype/pull/2405)\n- \\[Fix\\]\\[MAYA\\] Handle message type attribute within CollectLook [\\#2394](https://github.com/pypeclub/OpenPype/pull/2394)\n- Add validator to check correct version of extension for PS and AE [\\#2387](https://github.com/pypeclub/OpenPype/pull/2387)\n- Maya: configurable model top level validation [\\#2321](https://github.com/pypeclub/OpenPype/pull/2321)\n- Create test publish class for After Effects [\\#2270](https://github.com/pypeclub/OpenPype/pull/2270)\n\n## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.3...3.6.4)\n\n**🐛 Bug fixes**\n\n- Nuke: inventory update removes all loaded read nodes [\\#2294](https://github.com/pypeclub/OpenPype/pull/2294)\n\n## [3.6.3](https://github.com/pypeclub/OpenPype/tree/3.6.3) (2021-11-19)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.2...3.6.3)\n\n**🐛 Bug fixes**\n\n- Deadline: Fix publish targets [\\#2280](https://github.com/pypeclub/OpenPype/pull/2280)\n\n## [3.6.2](https://github.com/pypeclub/OpenPype/tree/3.6.2) (2021-11-18)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.1...3.6.2)\n\n**🚀 Enhancements**\n\n- Tools: Assets widget [\\#2265](https://github.com/pypeclub/OpenPype/pull/2265)\n- SceneInventory: Choose loader in asset switcher [\\#2262](https://github.com/pypeclub/OpenPype/pull/2262)\n- Style: New fonts in OpenPype style [\\#2256](https://github.com/pypeclub/OpenPype/pull/2256)\n- Tools: SceneInventory in OpenPype  [\\#2255](https://github.com/pypeclub/OpenPype/pull/2255)\n- Tools: Tasks widget [\\#2251](https://github.com/pypeclub/OpenPype/pull/2251)\n- Tools: Creator in OpenPype [\\#2244](https://github.com/pypeclub/OpenPype/pull/2244)\n- Added endpoint for configured extensions [\\#2221](https://github.com/pypeclub/OpenPype/pull/2221)\n\n**🐛 Bug fixes**\n\n- Tools: Parenting of tools in Nuke and Hiero [\\#2266](https://github.com/pypeclub/OpenPype/pull/2266)\n- limiting validator to specific editorial hosts [\\#2264](https://github.com/pypeclub/OpenPype/pull/2264)\n- Tools: Select Context dialog attribute fix [\\#2261](https://github.com/pypeclub/OpenPype/pull/2261)\n- Maya: Render publishing fails on linux [\\#2260](https://github.com/pypeclub/OpenPype/pull/2260)\n- LookAssigner: Fix tool reopen [\\#2259](https://github.com/pypeclub/OpenPype/pull/2259)\n- Standalone: editorial not publishing thumbnails on all subsets [\\#2258](https://github.com/pypeclub/OpenPype/pull/2258)\n- Burnins: Support mxf metadata [\\#2247](https://github.com/pypeclub/OpenPype/pull/2247)\n- Maya: Support for configurable AOV separator characters [\\#2197](https://github.com/pypeclub/OpenPype/pull/2197)\n- Maya: texture colorspace modes in looks [\\#2195](https://github.com/pypeclub/OpenPype/pull/2195)\n\n## [3.6.1](https://github.com/pypeclub/OpenPype/tree/3.6.1) (2021-11-16)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.0...3.6.1)\n\n**🐛 Bug fixes**\n\n- Loader doesn't allow changing of version before loading [\\#2254](https://github.com/pypeclub/OpenPype/pull/2254)\n\n## [3.6.0](https://github.com/pypeclub/OpenPype/tree/3.6.0) (2021-11-15)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.5.0...3.6.0)\n\n### 📖 Documentation\n\n- Add alternative sites for Site Sync [\\#2206](https://github.com/pypeclub/OpenPype/pull/2206)\n- Add command line way of running site sync server [\\#2188](https://github.com/pypeclub/OpenPype/pull/2188)\n\n**🆕 New features**\n\n- Add validate active site button to sync queue on a project [\\#2176](https://github.com/pypeclub/OpenPype/pull/2176)\n- Maya : Colorspace configuration  [\\#2170](https://github.com/pypeclub/OpenPype/pull/2170)\n- Blender: Added support for audio [\\#2168](https://github.com/pypeclub/OpenPype/pull/2168)\n- Flame: a host basic integration [\\#2165](https://github.com/pypeclub/OpenPype/pull/2165)\n- Houdini: simple HDA workflow [\\#2072](https://github.com/pypeclub/OpenPype/pull/2072)\n- Basic Royal Render Integration ✨ [\\#2061](https://github.com/pypeclub/OpenPype/pull/2061)\n- Camera handling between Blender and Unreal [\\#1988](https://github.com/pypeclub/OpenPype/pull/1988)\n- switch PyQt5 for PySide2 [\\#1744](https://github.com/pypeclub/OpenPype/pull/1744)\n\n**🚀 Enhancements**\n\n- Tools: Subset manager in OpenPype [\\#2243](https://github.com/pypeclub/OpenPype/pull/2243)\n- General: Skip module directories without init file [\\#2239](https://github.com/pypeclub/OpenPype/pull/2239)\n- General: Static interfaces [\\#2238](https://github.com/pypeclub/OpenPype/pull/2238)\n- Style: Fix transparent image in style [\\#2235](https://github.com/pypeclub/OpenPype/pull/2235)\n- Add a \"following workfile versioning\" option on publish [\\#2225](https://github.com/pypeclub/OpenPype/pull/2225)\n- Modules: Module can add cli commands [\\#2224](https://github.com/pypeclub/OpenPype/pull/2224)\n- Webpublisher: Separate webpublisher logic [\\#2222](https://github.com/pypeclub/OpenPype/pull/2222)\n- Add both side availability on Site Sync sites to Loader [\\#2220](https://github.com/pypeclub/OpenPype/pull/2220)\n- Tools: Center loader and library loader on show [\\#2219](https://github.com/pypeclub/OpenPype/pull/2219)\n- Maya : Validate shape zero [\\#2212](https://github.com/pypeclub/OpenPype/pull/2212)\n- Maya : validate unique names [\\#2211](https://github.com/pypeclub/OpenPype/pull/2211)\n- Tools: OpenPype stylesheet in workfiles tool [\\#2208](https://github.com/pypeclub/OpenPype/pull/2208)\n- Ftrack: Replace Queue with deque in event handlers logic [\\#2204](https://github.com/pypeclub/OpenPype/pull/2204)\n- Tools: New select context dialog [\\#2200](https://github.com/pypeclub/OpenPype/pull/2200)\n- Maya : Validate mesh ngons [\\#2199](https://github.com/pypeclub/OpenPype/pull/2199)\n- Dirmap in Nuke [\\#2198](https://github.com/pypeclub/OpenPype/pull/2198)\n- Delivery: Check 'frame' key in template for sequence delivery [\\#2196](https://github.com/pypeclub/OpenPype/pull/2196)\n- Settings: Site sync project settings improvement [\\#2193](https://github.com/pypeclub/OpenPype/pull/2193)\n- Usage of tools code [\\#2185](https://github.com/pypeclub/OpenPype/pull/2185)\n- Settings: Dictionary based on project roots [\\#2184](https://github.com/pypeclub/OpenPype/pull/2184)\n- Subset name: Be able to pass asset document to get subset name [\\#2179](https://github.com/pypeclub/OpenPype/pull/2179)\n- Tools: Experimental tools [\\#2167](https://github.com/pypeclub/OpenPype/pull/2167)\n- Loader: Refactor and use OpenPype stylesheets [\\#2166](https://github.com/pypeclub/OpenPype/pull/2166)\n- Add loader for linked smart objects in photoshop [\\#2149](https://github.com/pypeclub/OpenPype/pull/2149)\n- Burnins: DNxHD profiles handling [\\#2142](https://github.com/pypeclub/OpenPype/pull/2142)\n- Tools: Single access point for host tools [\\#2139](https://github.com/pypeclub/OpenPype/pull/2139)\n\n**🐛 Bug fixes**\n\n- Ftrack: Sync project ftrack id cache issue [\\#2250](https://github.com/pypeclub/OpenPype/pull/2250)\n- Ftrack: Session creation and Prepare project [\\#2245](https://github.com/pypeclub/OpenPype/pull/2245)\n- Added queue for studio processing in PS [\\#2237](https://github.com/pypeclub/OpenPype/pull/2237)\n- Python 2: Unicode to string conversion [\\#2236](https://github.com/pypeclub/OpenPype/pull/2236)\n- Fix - enum for color coding in PS [\\#2234](https://github.com/pypeclub/OpenPype/pull/2234)\n- Pyblish Tool: Fix targets handling [\\#2232](https://github.com/pypeclub/OpenPype/pull/2232)\n- Ftrack: Base event fix of 'get\\_project\\_from\\_entity' method [\\#2214](https://github.com/pypeclub/OpenPype/pull/2214)\n- Maya : multiple subsets review broken [\\#2210](https://github.com/pypeclub/OpenPype/pull/2210)\n- Fix - different command used for Linux and Mac OS [\\#2207](https://github.com/pypeclub/OpenPype/pull/2207)\n- Tools: Workfiles tool don't use avalon widgets [\\#2205](https://github.com/pypeclub/OpenPype/pull/2205)\n- Ftrack: Fill missing ftrack id on mongo project [\\#2203](https://github.com/pypeclub/OpenPype/pull/2203)\n- Project Manager: Fix copying of tasks [\\#2191](https://github.com/pypeclub/OpenPype/pull/2191)\n- StandalonePublisher: Source validator don't expect representations [\\#2190](https://github.com/pypeclub/OpenPype/pull/2190)\n- Blender: Fix trying to pack an image when the shader node has no texture [\\#2183](https://github.com/pypeclub/OpenPype/pull/2183)\n- Maya: review viewport settings [\\#2177](https://github.com/pypeclub/OpenPype/pull/2177)\n- MacOS: Launching of applications may cause Permissions error [\\#2175](https://github.com/pypeclub/OpenPype/pull/2175)\n- Maya: Aspect ratio [\\#2174](https://github.com/pypeclub/OpenPype/pull/2174)\n- Blender: Fix 'Deselect All' with object not in 'Object Mode' [\\#2163](https://github.com/pypeclub/OpenPype/pull/2163)\n- Tools: Stylesheets are applied after tool show [\\#2161](https://github.com/pypeclub/OpenPype/pull/2161)\n- Maya: Collect render - fix UNC path support 🐛 [\\#2158](https://github.com/pypeclub/OpenPype/pull/2158)\n- Maya: Fix hotbox broken by scriptsmenu [\\#2151](https://github.com/pypeclub/OpenPype/pull/2151)\n- Ftrack: Ignore save warnings exception in Prepare project action [\\#2150](https://github.com/pypeclub/OpenPype/pull/2150)\n- Loader thumbnails with smooth edges [\\#2147](https://github.com/pypeclub/OpenPype/pull/2147)\n- Added validator for source files for Standalone Publisher [\\#2138](https://github.com/pypeclub/OpenPype/pull/2138)\n\n**Merged pull requests:**\n\n- Bump pillow from 8.2.0 to 8.3.2 [\\#2162](https://github.com/pypeclub/OpenPype/pull/2162)\n- Bump axios from 0.21.1 to 0.21.4 in /website [\\#2059](https://github.com/pypeclub/OpenPype/pull/2059)\n\n## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...3.5.0)\n\n**Deprecated:**\n\n- Maya: Change mayaAscii family to mayaScene [\\#2106](https://github.com/pypeclub/OpenPype/pull/2106)\n\n**🆕 New features**\n\n- Added project and task into context change message in Maya [\\#2131](https://github.com/pypeclub/OpenPype/pull/2131)\n- Add ExtractBurnin to photoshop review [\\#2124](https://github.com/pypeclub/OpenPype/pull/2124)\n- PYPE-1218 - changed namespace to contain subset name in Maya [\\#2114](https://github.com/pypeclub/OpenPype/pull/2114)\n- Added running configurable disk mapping command before start of OP [\\#2091](https://github.com/pypeclub/OpenPype/pull/2091)\n- SFTP provider [\\#2073](https://github.com/pypeclub/OpenPype/pull/2073)\n- Maya: Validate setdress top group [\\#2068](https://github.com/pypeclub/OpenPype/pull/2068)\n- Maya: Enable publishing render attrib sets \\(e.g. V-Ray Displacement\\) with model [\\#1955](https://github.com/pypeclub/OpenPype/pull/1955)\n\n**🚀 Enhancements**\n\n- Maya: make rig validators configurable in settings [\\#2137](https://github.com/pypeclub/OpenPype/pull/2137)\n- Settings: Updated readme for entity types in settings [\\#2132](https://github.com/pypeclub/OpenPype/pull/2132)\n- Nuke: unified clip loader [\\#2128](https://github.com/pypeclub/OpenPype/pull/2128)\n- Settings UI: Project model refreshing and sorting [\\#2104](https://github.com/pypeclub/OpenPype/pull/2104)\n- Create Read From Rendered - Disable Relative paths by default [\\#2093](https://github.com/pypeclub/OpenPype/pull/2093)\n- Added choosing different dirmap mapping if workfile synched locally [\\#2088](https://github.com/pypeclub/OpenPype/pull/2088)\n- General: Remove IdleManager module [\\#2084](https://github.com/pypeclub/OpenPype/pull/2084)\n- Tray UI: Message box about missing settings defaults [\\#2080](https://github.com/pypeclub/OpenPype/pull/2080)\n- Tray UI: Show menu where first click happened [\\#2079](https://github.com/pypeclub/OpenPype/pull/2079)\n- Global: add global validators to settings [\\#2078](https://github.com/pypeclub/OpenPype/pull/2078)\n- Use CRF for burnin when available [\\#2070](https://github.com/pypeclub/OpenPype/pull/2070)\n- Project manager: Filter first item after selection of project [\\#2069](https://github.com/pypeclub/OpenPype/pull/2069)\n- Nuke: Adding `still` image family workflow [\\#2064](https://github.com/pypeclub/OpenPype/pull/2064)\n- Maya: validate authorized loaded plugins [\\#2062](https://github.com/pypeclub/OpenPype/pull/2062)\n- Tools: add support for pyenv on windows [\\#2051](https://github.com/pypeclub/OpenPype/pull/2051)\n- SyncServer: Dropbox Provider [\\#1979](https://github.com/pypeclub/OpenPype/pull/1979)\n- Burnin: Get data from context with defined keys. [\\#1897](https://github.com/pypeclub/OpenPype/pull/1897)\n- Timers manager: Get task time [\\#1896](https://github.com/pypeclub/OpenPype/pull/1896)\n- TVPaint: Option to stop timer on application exit. [\\#1887](https://github.com/pypeclub/OpenPype/pull/1887)\n\n**🐛 Bug fixes**\n\n- Maya: fix model publishing [\\#2130](https://github.com/pypeclub/OpenPype/pull/2130)\n- Fix - oiiotool wasn't recognized even if present [\\#2129](https://github.com/pypeclub/OpenPype/pull/2129)\n- General: Disk mapping group [\\#2120](https://github.com/pypeclub/OpenPype/pull/2120)\n- Hiero: publishing effect first time makes wrong resources path [\\#2115](https://github.com/pypeclub/OpenPype/pull/2115)\n- Add startup script for Houdini Core.  [\\#2110](https://github.com/pypeclub/OpenPype/pull/2110)\n- TVPaint: Behavior name of loop also accept repeat [\\#2109](https://github.com/pypeclub/OpenPype/pull/2109)\n- Ftrack: Project settings save custom attributes skip unknown attributes [\\#2103](https://github.com/pypeclub/OpenPype/pull/2103)\n- Blender: Fix NoneType error when animation\\_data is missing for a rig [\\#2101](https://github.com/pypeclub/OpenPype/pull/2101)\n- Fix broken import in sftp provider [\\#2100](https://github.com/pypeclub/OpenPype/pull/2100)\n- Global: Fix docstring on publish plugin extract review [\\#2097](https://github.com/pypeclub/OpenPype/pull/2097)\n- Delivery Action Files Sequence fix [\\#2096](https://github.com/pypeclub/OpenPype/pull/2096)\n- General: Cloud mongo ca certificate issue [\\#2095](https://github.com/pypeclub/OpenPype/pull/2095)\n- TVPaint: Creator use context from workfile [\\#2087](https://github.com/pypeclub/OpenPype/pull/2087)\n- Blender: fix texture missing when publishing blend files [\\#2085](https://github.com/pypeclub/OpenPype/pull/2085)\n- General: Startup validations oiio tool path fix on linux [\\#2083](https://github.com/pypeclub/OpenPype/pull/2083)\n- Deadline: Collect deadline server does not check existence of deadline key [\\#2082](https://github.com/pypeclub/OpenPype/pull/2082)\n- Blender: fixed Curves with modifiers in Rigs [\\#2081](https://github.com/pypeclub/OpenPype/pull/2081)\n- Nuke UI scaling [\\#2077](https://github.com/pypeclub/OpenPype/pull/2077)\n- Maya: Fix multi-camera renders [\\#2065](https://github.com/pypeclub/OpenPype/pull/2065)\n- Fix Sync Queue when project disabled [\\#2063](https://github.com/pypeclub/OpenPype/pull/2063)\n\n**Merged pull requests:**\n\n- Bump pywin32 from 300 to 301 [\\#2086](https://github.com/pypeclub/OpenPype/pull/2086)\n\n## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.0...3.4.1)\n\n**🆕 New features**\n\n- Settings: Flag project as deactivated and hide from tools' view [\\#2008](https://github.com/pypeclub/OpenPype/pull/2008)\n\n**🚀 Enhancements**\n\n- General: Startup validations [\\#2054](https://github.com/pypeclub/OpenPype/pull/2054)\n- Nuke: proxy mode validator [\\#2052](https://github.com/pypeclub/OpenPype/pull/2052)\n- Ftrack: Removed ftrack interface [\\#2049](https://github.com/pypeclub/OpenPype/pull/2049)\n- Settings UI: Deffered set value on entity [\\#2044](https://github.com/pypeclub/OpenPype/pull/2044)\n- Loader: Families filtering [\\#2043](https://github.com/pypeclub/OpenPype/pull/2043)\n- Settings UI: Project view enhancements [\\#2042](https://github.com/pypeclub/OpenPype/pull/2042)\n- Settings for Nuke IncrementScriptVersion [\\#2039](https://github.com/pypeclub/OpenPype/pull/2039)\n- Loader & Library loader: Use tools from OpenPype [\\#2038](https://github.com/pypeclub/OpenPype/pull/2038)\n- Adding predefined project folders creation in PM [\\#2030](https://github.com/pypeclub/OpenPype/pull/2030)\n- WebserverModule: Removed interface of webserver module [\\#2028](https://github.com/pypeclub/OpenPype/pull/2028)\n- TimersManager: Removed interface of timers manager [\\#2024](https://github.com/pypeclub/OpenPype/pull/2024)\n- Feature Maya import asset from scene inventory [\\#2018](https://github.com/pypeclub/OpenPype/pull/2018)\n\n**🐛 Bug fixes**\n\n- Timers manger: Typo fix [\\#2058](https://github.com/pypeclub/OpenPype/pull/2058)\n- Hiero: Editorial fixes [\\#2057](https://github.com/pypeclub/OpenPype/pull/2057)\n- Differentiate jpg sequences from thumbnail [\\#2056](https://github.com/pypeclub/OpenPype/pull/2056)\n- FFmpeg: Split command to list does not work [\\#2046](https://github.com/pypeclub/OpenPype/pull/2046)\n- Removed shell flag in subprocess call [\\#2045](https://github.com/pypeclub/OpenPype/pull/2045)\n\n**Merged pull requests:**\n\n- Bump prismjs from 1.24.0 to 1.25.0 in /website [\\#2050](https://github.com/pypeclub/OpenPype/pull/2050)\n\n## [3.4.0](https://github.com/pypeclub/OpenPype/tree/3.4.0) (2021-09-17)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...3.4.0)\n\n### 📖 Documentation\n\n- Documentation: Ftrack launch argsuments update [\\#2014](https://github.com/pypeclub/OpenPype/pull/2014)\n- Nuke Quick Start / Tutorial [\\#1952](https://github.com/pypeclub/OpenPype/pull/1952)\n- Houdini: add Camera, Point Cache, Composite, Redshift ROP and VDB Cache support [\\#1821](https://github.com/pypeclub/OpenPype/pull/1821)\n\n**🆕 New features**\n\n- Nuke: Compatibility with Nuke 13 [\\#2003](https://github.com/pypeclub/OpenPype/pull/2003)\n- Maya: Add Xgen family support [\\#1947](https://github.com/pypeclub/OpenPype/pull/1947)\n- Feature/webpublisher backend [\\#1876](https://github.com/pypeclub/OpenPype/pull/1876)\n- Blender: Improved assets handling [\\#1615](https://github.com/pypeclub/OpenPype/pull/1615)\n\n**🚀 Enhancements**\n\n- Added possibility to configure of synchronization of workfile version… [\\#2041](https://github.com/pypeclub/OpenPype/pull/2041)\n- General: Task types in profiles [\\#2036](https://github.com/pypeclub/OpenPype/pull/2036)\n- Console interpreter: Handle invalid sizes on initialization [\\#2022](https://github.com/pypeclub/OpenPype/pull/2022)\n- Ftrack: Show OpenPype versions in event server status [\\#2019](https://github.com/pypeclub/OpenPype/pull/2019)\n- General: Staging icon [\\#2017](https://github.com/pypeclub/OpenPype/pull/2017)\n- Ftrack: Sync to avalon actions have jobs [\\#2015](https://github.com/pypeclub/OpenPype/pull/2015)\n- Modules: Connect method is not required [\\#2009](https://github.com/pypeclub/OpenPype/pull/2009)\n- Settings UI: Number with configurable steps [\\#2001](https://github.com/pypeclub/OpenPype/pull/2001)\n- Moving project folder structure creation out of ftrack module \\#1989 [\\#1996](https://github.com/pypeclub/OpenPype/pull/1996)\n- Configurable items for providers without Settings [\\#1987](https://github.com/pypeclub/OpenPype/pull/1987)\n- Global: Example addons [\\#1986](https://github.com/pypeclub/OpenPype/pull/1986)\n- Standalone Publisher: Extract harmony zip handle workfile template [\\#1982](https://github.com/pypeclub/OpenPype/pull/1982)\n- Settings UI: Number sliders [\\#1978](https://github.com/pypeclub/OpenPype/pull/1978)\n- Workfiles: Support more workfile templates [\\#1966](https://github.com/pypeclub/OpenPype/pull/1966)\n- Launcher: Fix crashes on action click [\\#1964](https://github.com/pypeclub/OpenPype/pull/1964)\n- Settings: Minor fixes in UI and missing default values [\\#1963](https://github.com/pypeclub/OpenPype/pull/1963)\n- Blender: Toggle system console works on windows [\\#1962](https://github.com/pypeclub/OpenPype/pull/1962)\n- Global: Settings defined by Addons/Modules [\\#1959](https://github.com/pypeclub/OpenPype/pull/1959)\n- CI: change release numbering triggers [\\#1954](https://github.com/pypeclub/OpenPype/pull/1954)\n- Global: Avalon Host name collector [\\#1949](https://github.com/pypeclub/OpenPype/pull/1949)\n- Global: Define hosts in CollectSceneVersion [\\#1948](https://github.com/pypeclub/OpenPype/pull/1948)\n- Add face sets to exported alembics [\\#1942](https://github.com/pypeclub/OpenPype/pull/1942)\n- OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\\#1939](https://github.com/pypeclub/OpenPype/pull/1939)\n- \\#1894 - adds host to template\\_name\\_profiles for filtering [\\#1915](https://github.com/pypeclub/OpenPype/pull/1915)\n- Environments: Tool environments in alphabetical order [\\#1910](https://github.com/pypeclub/OpenPype/pull/1910)\n- Disregard publishing time. [\\#1888](https://github.com/pypeclub/OpenPype/pull/1888)\n- Dynamic modules [\\#1872](https://github.com/pypeclub/OpenPype/pull/1872)\n\n**🐛 Bug fixes**\n\n- Workfiles tool: Task selection [\\#2040](https://github.com/pypeclub/OpenPype/pull/2040)\n- Ftrack: Delete old versions missing settings key [\\#2037](https://github.com/pypeclub/OpenPype/pull/2037)\n- Nuke: typo on a button [\\#2034](https://github.com/pypeclub/OpenPype/pull/2034)\n- Hiero: Fix \"none\" named tags [\\#2033](https://github.com/pypeclub/OpenPype/pull/2033)\n- FFmpeg: Subprocess arguments as list [\\#2032](https://github.com/pypeclub/OpenPype/pull/2032)\n- General: Fix Python 2 breaking line [\\#2016](https://github.com/pypeclub/OpenPype/pull/2016)\n- Bugfix/webpublisher task type [\\#2006](https://github.com/pypeclub/OpenPype/pull/2006)\n- Nuke thumbnails generated from middle of the sequence [\\#1992](https://github.com/pypeclub/OpenPype/pull/1992)\n- Nuke: last version from path gets correct version [\\#1990](https://github.com/pypeclub/OpenPype/pull/1990)\n- nuke, resolve, hiero: precollector order lest then 0.5 [\\#1984](https://github.com/pypeclub/OpenPype/pull/1984)\n- Last workfile with multiple work templates [\\#1981](https://github.com/pypeclub/OpenPype/pull/1981)\n- Collectors order [\\#1977](https://github.com/pypeclub/OpenPype/pull/1977)\n- Stop timer was within validator order range. [\\#1975](https://github.com/pypeclub/OpenPype/pull/1975)\n- Ftrack: arrow submodule has https url source [\\#1974](https://github.com/pypeclub/OpenPype/pull/1974)\n- Ftrack: Fix hosts attribute in collect ftrack username [\\#1972](https://github.com/pypeclub/OpenPype/pull/1972)\n- Deadline: Houdini plugins in different hierarchy [\\#1970](https://github.com/pypeclub/OpenPype/pull/1970)\n- Removed deprecated submodules [\\#1967](https://github.com/pypeclub/OpenPype/pull/1967)\n- Global: ExtractJpeg can handle filepaths with spaces [\\#1961](https://github.com/pypeclub/OpenPype/pull/1961)\n- Resolve path when adding to zip [\\#1960](https://github.com/pypeclub/OpenPype/pull/1960)\n\n**Merged pull requests:**\n\n- Bump url-parse from 1.5.1 to 1.5.3 in /website [\\#1958](https://github.com/pypeclub/OpenPype/pull/1958)\n- Bump path-parse from 1.0.6 to 1.0.7 in /website [\\#1933](https://github.com/pypeclub/OpenPype/pull/1933)\n\n## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.0...3.3.1)\n\n**🐛 Bug fixes**\n\n- TVPaint: Fixed rendered frame indexes [\\#1946](https://github.com/pypeclub/OpenPype/pull/1946)\n- Maya: Menu actions fix [\\#1945](https://github.com/pypeclub/OpenPype/pull/1945)\n- standalone: editorial shared object problem [\\#1941](https://github.com/pypeclub/OpenPype/pull/1941)\n- Bugfix nuke deadline app name [\\#1928](https://github.com/pypeclub/OpenPype/pull/1928)\n\n## [3.3.0](https://github.com/pypeclub/OpenPype/tree/3.3.0) (2021-08-17)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...3.3.0)\n\n### 📖 Documentation\n\n- Standalone Publish of textures family [\\#1834](https://github.com/pypeclub/OpenPype/pull/1834)\n\n**🆕 New features**\n\n- Settings UI: Breadcrumbs in settings [\\#1932](https://github.com/pypeclub/OpenPype/pull/1932)\n- Maya: Scene patching 🩹on submission to Deadline [\\#1923](https://github.com/pypeclub/OpenPype/pull/1923)\n- Feature AE local render [\\#1901](https://github.com/pypeclub/OpenPype/pull/1901)\n\n**🚀 Enhancements**\n\n- Python console interpreter [\\#1940](https://github.com/pypeclub/OpenPype/pull/1940)\n- Global: Updated logos and Default settings [\\#1927](https://github.com/pypeclub/OpenPype/pull/1927)\n- Check for missing ✨ Python when using `pyenv` [\\#1925](https://github.com/pypeclub/OpenPype/pull/1925)\n- Settings: Default values for enum [\\#1920](https://github.com/pypeclub/OpenPype/pull/1920)\n- Settings UI: Modifiable dict view enhance [\\#1919](https://github.com/pypeclub/OpenPype/pull/1919)\n- submodules: avalon-core update [\\#1911](https://github.com/pypeclub/OpenPype/pull/1911)\n- Ftrack: Where I run action enhancement [\\#1900](https://github.com/pypeclub/OpenPype/pull/1900)\n- Ftrack: Private project server actions [\\#1899](https://github.com/pypeclub/OpenPype/pull/1899)\n- Support nested studio plugins paths. [\\#1898](https://github.com/pypeclub/OpenPype/pull/1898)\n- Settings: global validators with options [\\#1892](https://github.com/pypeclub/OpenPype/pull/1892)\n- Settings: Conditional dict enum positioning [\\#1891](https://github.com/pypeclub/OpenPype/pull/1891)\n- Expose stop timer through rest api. [\\#1886](https://github.com/pypeclub/OpenPype/pull/1886)\n- TVPaint: Increment workfile [\\#1885](https://github.com/pypeclub/OpenPype/pull/1885)\n- Allow Multiple Notes to run on tasks. [\\#1882](https://github.com/pypeclub/OpenPype/pull/1882)\n- Prepare for pyside2 [\\#1869](https://github.com/pypeclub/OpenPype/pull/1869)\n- Filter hosts in settings host-enum [\\#1868](https://github.com/pypeclub/OpenPype/pull/1868)\n- Local actions with process identifier [\\#1867](https://github.com/pypeclub/OpenPype/pull/1867)\n- Workfile tool start at host launch support [\\#1865](https://github.com/pypeclub/OpenPype/pull/1865)\n- Anatomy schema validation [\\#1864](https://github.com/pypeclub/OpenPype/pull/1864)\n- Ftrack prepare project structure [\\#1861](https://github.com/pypeclub/OpenPype/pull/1861)\n- Maya: support for configurable `dirmap` 🗺️ [\\#1859](https://github.com/pypeclub/OpenPype/pull/1859)\n- Independent general environments [\\#1853](https://github.com/pypeclub/OpenPype/pull/1853)\n- TVPaint Start Frame [\\#1844](https://github.com/pypeclub/OpenPype/pull/1844)\n- Ftrack push attributes action adds traceback to job [\\#1843](https://github.com/pypeclub/OpenPype/pull/1843)\n- Prepare project action enhance [\\#1838](https://github.com/pypeclub/OpenPype/pull/1838)\n- nuke: settings create missing default subsets [\\#1829](https://github.com/pypeclub/OpenPype/pull/1829)\n- Update poetry lock [\\#1823](https://github.com/pypeclub/OpenPype/pull/1823)\n- Settings: settings for plugins [\\#1819](https://github.com/pypeclub/OpenPype/pull/1819)\n- Settings list can use template or schema as object type [\\#1815](https://github.com/pypeclub/OpenPype/pull/1815)\n- Maya: Deadline custom settings  [\\#1797](https://github.com/pypeclub/OpenPype/pull/1797)\n- Maya: Shader name validation [\\#1762](https://github.com/pypeclub/OpenPype/pull/1762)\n\n**🐛 Bug fixes**\n\n- Fix - ftrack family was added incorrectly in some cases [\\#1935](https://github.com/pypeclub/OpenPype/pull/1935)\n- Fix - Deadline publish on Linux started Tray instead of headless publishing [\\#1930](https://github.com/pypeclub/OpenPype/pull/1930)\n- Maya: Validate Model Name - repair accident deletion in settings defaults [\\#1929](https://github.com/pypeclub/OpenPype/pull/1929)\n- Nuke: submit to farm failed due `ftrack` family remove [\\#1926](https://github.com/pypeclub/OpenPype/pull/1926)\n- Fix - validate takes repre\\[\"files\"\\] as list all the time [\\#1922](https://github.com/pypeclub/OpenPype/pull/1922)\n- standalone: validator asset parents [\\#1917](https://github.com/pypeclub/OpenPype/pull/1917)\n- Nuke: update video file crassing [\\#1916](https://github.com/pypeclub/OpenPype/pull/1916)\n- Fix - texture validators for workfiles triggers only for textures workfiles [\\#1914](https://github.com/pypeclub/OpenPype/pull/1914)\n- Settings UI: List order works as expected [\\#1906](https://github.com/pypeclub/OpenPype/pull/1906)\n- Hiero: loaded clip was not set colorspace from version data [\\#1904](https://github.com/pypeclub/OpenPype/pull/1904)\n- Pyblish UI: Fix collecting stage processing [\\#1903](https://github.com/pypeclub/OpenPype/pull/1903)\n- Burnins: Use input's bitrate in h624 [\\#1902](https://github.com/pypeclub/OpenPype/pull/1902)\n- Bug: fixed python detection [\\#1893](https://github.com/pypeclub/OpenPype/pull/1893)\n- global: integrate name missing default template [\\#1890](https://github.com/pypeclub/OpenPype/pull/1890)\n- publisher: editorial plugins fixes [\\#1889](https://github.com/pypeclub/OpenPype/pull/1889)\n- Normalize path returned from Workfiles. [\\#1880](https://github.com/pypeclub/OpenPype/pull/1880)\n- Workfiles tool event arguments fix [\\#1862](https://github.com/pypeclub/OpenPype/pull/1862)\n- imageio: fix grouping  [\\#1856](https://github.com/pypeclub/OpenPype/pull/1856)\n- Maya: don't add reference members as connections to the container set 📦 [\\#1855](https://github.com/pypeclub/OpenPype/pull/1855)\n- publisher: missing version in subset prop [\\#1849](https://github.com/pypeclub/OpenPype/pull/1849)\n- Ftrack type error fix in sync to avalon event handler [\\#1845](https://github.com/pypeclub/OpenPype/pull/1845)\n- Nuke: updating effects subset fail [\\#1841](https://github.com/pypeclub/OpenPype/pull/1841)\n- nuke: write render node skipped with crop [\\#1836](https://github.com/pypeclub/OpenPype/pull/1836)\n- Project folder structure overrides [\\#1813](https://github.com/pypeclub/OpenPype/pull/1813)\n- Maya: fix yeti settings path in extractor [\\#1809](https://github.com/pypeclub/OpenPype/pull/1809)\n- Failsafe for cross project containers. [\\#1806](https://github.com/pypeclub/OpenPype/pull/1806)\n- Houdini colector formatting keys fix [\\#1802](https://github.com/pypeclub/OpenPype/pull/1802)\n- Settings error dialog on show [\\#1798](https://github.com/pypeclub/OpenPype/pull/1798)\n- Application launch stdout/stderr in GUI build [\\#1684](https://github.com/pypeclub/OpenPype/pull/1684)\n- Nuke: re-use instance nodes output path [\\#1577](https://github.com/pypeclub/OpenPype/pull/1577)\n\n**Merged pull requests:**\n\n- Fix - make AE workfile publish to Ftrack configurable [\\#1937](https://github.com/pypeclub/OpenPype/pull/1937)\n- Add support for multiple Deadline ☠️➖ servers [\\#1905](https://github.com/pypeclub/OpenPype/pull/1905)\n- Maya: add support for `RedshiftNormalMap` node, fix `tx` linear space 🚀 [\\#1863](https://github.com/pypeclub/OpenPype/pull/1863)\n- Maya: expected files -\\> render products ⚙️ overhaul [\\#1812](https://github.com/pypeclub/OpenPype/pull/1812)\n- PS, AE - send actual context when another webserver is running [\\#1811](https://github.com/pypeclub/OpenPype/pull/1811)\n\n## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...3.2.0)\n\n### 📖 Documentation\n\n- Fix: staging and `--use-version` option [\\#1786](https://github.com/pypeclub/OpenPype/pull/1786)\n- Subset template and TVPaint subset template docs [\\#1717](https://github.com/pypeclub/OpenPype/pull/1717)\n- Overscan color extract review [\\#1701](https://github.com/pypeclub/OpenPype/pull/1701)\n\n**🚀 Enhancements**\n\n- Nuke: ftrack family plugin settings preset [\\#1805](https://github.com/pypeclub/OpenPype/pull/1805)\n- Standalone publisher last project [\\#1799](https://github.com/pypeclub/OpenPype/pull/1799)\n- Ftrack Multiple notes as server action [\\#1795](https://github.com/pypeclub/OpenPype/pull/1795)\n- Settings conditional dict [\\#1777](https://github.com/pypeclub/OpenPype/pull/1777)\n- Settings application use python 2 only where needed [\\#1776](https://github.com/pypeclub/OpenPype/pull/1776)\n- Settings UI copy/paste [\\#1769](https://github.com/pypeclub/OpenPype/pull/1769)\n- Workfile tool widths [\\#1766](https://github.com/pypeclub/OpenPype/pull/1766)\n- Push hierarchical attributes care about task parent changes [\\#1763](https://github.com/pypeclub/OpenPype/pull/1763)\n- Application executables with environment variables [\\#1757](https://github.com/pypeclub/OpenPype/pull/1757)\n- Deadline: Nuke submission additional attributes [\\#1756](https://github.com/pypeclub/OpenPype/pull/1756)\n- Settings schema without prefill [\\#1753](https://github.com/pypeclub/OpenPype/pull/1753)\n- Settings Hosts enum [\\#1739](https://github.com/pypeclub/OpenPype/pull/1739)\n- Validate containers settings [\\#1736](https://github.com/pypeclub/OpenPype/pull/1736)\n- PS - added loader from sequence [\\#1726](https://github.com/pypeclub/OpenPype/pull/1726)\n- Autoupdate launcher [\\#1725](https://github.com/pypeclub/OpenPype/pull/1725)\n- Toggle Ftrack upload in StandalonePublisher [\\#1708](https://github.com/pypeclub/OpenPype/pull/1708)\n- Nuke: Prerender Frame Range by default [\\#1699](https://github.com/pypeclub/OpenPype/pull/1699)\n- Smoother edges of color triangle [\\#1695](https://github.com/pypeclub/OpenPype/pull/1695)\n\n**🐛 Bug fixes**\n\n- nuke: fixing wrong name of family folder when `used existing frames` [\\#1803](https://github.com/pypeclub/OpenPype/pull/1803)\n- Collect ftrack family bugs [\\#1801](https://github.com/pypeclub/OpenPype/pull/1801)\n- Invitee email can be None which break the Ftrack commit. [\\#1788](https://github.com/pypeclub/OpenPype/pull/1788)\n- Otio unrelated error on import [\\#1782](https://github.com/pypeclub/OpenPype/pull/1782)\n- FFprobe streams order [\\#1775](https://github.com/pypeclub/OpenPype/pull/1775)\n- Fix - single file files are str only, cast it to list to count properly [\\#1772](https://github.com/pypeclub/OpenPype/pull/1772)\n- Environments in app executable for MacOS [\\#1768](https://github.com/pypeclub/OpenPype/pull/1768)\n- Project specific environments [\\#1767](https://github.com/pypeclub/OpenPype/pull/1767)\n- Settings UI with refresh button [\\#1764](https://github.com/pypeclub/OpenPype/pull/1764)\n- Standalone publisher thumbnail extractor fix [\\#1761](https://github.com/pypeclub/OpenPype/pull/1761)\n- Anatomy others templates don't cause crash [\\#1758](https://github.com/pypeclub/OpenPype/pull/1758)\n- Backend acre module commit update [\\#1745](https://github.com/pypeclub/OpenPype/pull/1745)\n- hiero: precollect instances failing when audio selected [\\#1743](https://github.com/pypeclub/OpenPype/pull/1743)\n- Hiero: creator instance error [\\#1742](https://github.com/pypeclub/OpenPype/pull/1742)\n- Nuke: fixing render creator for no selection format failing [\\#1741](https://github.com/pypeclub/OpenPype/pull/1741)\n- StandalonePublisher: failing collector for editorial [\\#1738](https://github.com/pypeclub/OpenPype/pull/1738)\n- Local settings UI crash on missing defaults [\\#1737](https://github.com/pypeclub/OpenPype/pull/1737)\n- TVPaint white background on thumbnail [\\#1735](https://github.com/pypeclub/OpenPype/pull/1735)\n- Ftrack missing custom attribute message [\\#1734](https://github.com/pypeclub/OpenPype/pull/1734)\n- Launcher project changes [\\#1733](https://github.com/pypeclub/OpenPype/pull/1733)\n- Ftrack sync status [\\#1732](https://github.com/pypeclub/OpenPype/pull/1732)\n- TVPaint use layer name for default variant [\\#1724](https://github.com/pypeclub/OpenPype/pull/1724)\n- Default subset template for TVPaint review and workfile families [\\#1716](https://github.com/pypeclub/OpenPype/pull/1716)\n- Maya: Extract review hotfix [\\#1714](https://github.com/pypeclub/OpenPype/pull/1714)\n- Settings: Imageio improving granularity [\\#1711](https://github.com/pypeclub/OpenPype/pull/1711)\n- Application without executables [\\#1679](https://github.com/pypeclub/OpenPype/pull/1679)\n- Unreal: launching on Linux [\\#1672](https://github.com/pypeclub/OpenPype/pull/1672)\n\n**Merged pull requests:**\n\n- Bump prismjs from 1.23.0 to 1.24.0 in /website [\\#1773](https://github.com/pypeclub/OpenPype/pull/1773)\n- TVPaint ftrack family [\\#1755](https://github.com/pypeclub/OpenPype/pull/1755)\n\n## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4)\n\n## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-23)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.2...2.18.3)\n\n## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2)\n\n## [3.1.0](https://github.com/pypeclub/OpenPype/tree/3.1.0) (2021-06-15)\n\n[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...3.1.0)\n\n### 📖 Documentation\n\n- Feature Slack integration [\\#1657](https://github.com/pypeclub/OpenPype/pull/1657)\n\n**🚀 Enhancements**\n\n- Log Viewer with OpenPype style [\\#1703](https://github.com/pypeclub/OpenPype/pull/1703)\n- Scrolling in OpenPype info widget [\\#1702](https://github.com/pypeclub/OpenPype/pull/1702)\n- OpenPype style in modules [\\#1694](https://github.com/pypeclub/OpenPype/pull/1694)\n- Sort applications and tools alphabetically in Settings UI [\\#1689](https://github.com/pypeclub/OpenPype/pull/1689)\n- \\#683 - Validate Frame Range in Standalone Publisher [\\#1683](https://github.com/pypeclub/OpenPype/pull/1683)\n- Hiero: old container versions identify with red color [\\#1682](https://github.com/pypeclub/OpenPype/pull/1682)\n- Project Manger: Default name column width [\\#1669](https://github.com/pypeclub/OpenPype/pull/1669)\n- Remove outline in stylesheet [\\#1667](https://github.com/pypeclub/OpenPype/pull/1667)\n- TVPaint: Creator take layer name as default value for subset variant [\\#1663](https://github.com/pypeclub/OpenPype/pull/1663)\n- TVPaint custom subset template [\\#1662](https://github.com/pypeclub/OpenPype/pull/1662)\n- Editorial: conform assets validator [\\#1659](https://github.com/pypeclub/OpenPype/pull/1659)\n- Nuke - Publish simplification [\\#1653](https://github.com/pypeclub/OpenPype/pull/1653)\n- \\#1333 - added tooltip hints to Pyblish buttons [\\#1649](https://github.com/pypeclub/OpenPype/pull/1649)\n\n**🐛 Bug fixes**\n\n- Nuke: broken publishing rendered frames [\\#1707](https://github.com/pypeclub/OpenPype/pull/1707)\n- Standalone publisher Thumbnail export args [\\#1705](https://github.com/pypeclub/OpenPype/pull/1705)\n- Bad zip can break OpenPype start [\\#1691](https://github.com/pypeclub/OpenPype/pull/1691)\n- Hiero: published whole edit mov [\\#1687](https://github.com/pypeclub/OpenPype/pull/1687)\n- Ftrack subprocess handle of stdout/stderr [\\#1675](https://github.com/pypeclub/OpenPype/pull/1675)\n- Settings list race condifiton and mutable dict list conversion [\\#1671](https://github.com/pypeclub/OpenPype/pull/1671)\n- Mac launch arguments fix [\\#1660](https://github.com/pypeclub/OpenPype/pull/1660)\n- Fix missing dbm python module [\\#1652](https://github.com/pypeclub/OpenPype/pull/1652)\n- Transparent branches in view on Mac [\\#1648](https://github.com/pypeclub/OpenPype/pull/1648)\n- Add asset on task item [\\#1646](https://github.com/pypeclub/OpenPype/pull/1646)\n- Project manager save and queue [\\#1645](https://github.com/pypeclub/OpenPype/pull/1645)\n- New project anatomy values [\\#1644](https://github.com/pypeclub/OpenPype/pull/1644)\n- Farm publishing: check if published items do exist [\\#1573](https://github.com/pypeclub/OpenPype/pull/1573)\n\n**Merged pull requests:**\n\n- Bump normalize-url from 4.5.0 to 4.5.1 in /website [\\#1686](https://github.com/pypeclub/OpenPype/pull/1686)\n\n\n## [3.0.0](https://github.com/pypeclub/openpype/tree/3.0.0)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.1...3.0.0)\n\n### Configuration\n- Studio Settings GUI: no more json configuration files.\n- OpenPype Modules can be turned on and off.\n- Easy to add Application versions.\n- Per Project Environment and plugin management.\n- Robust profile system for creating reviewables and burnins, with filtering based on Application, Task and data family.\n- Configurable publish plugins.\n- Options to make any validator or extractor, optional or disabled.\n- Color Management is now unified under anatomy settings.\n- Subset naming and grouping is fully configurable.\n- All project attributes can now be set directly in OpenPype settings.\n- Studio Setting can be locked to prevent unwanted artist changes.\n- You can now add per project and per task type templates for workfile initialization in most hosts.\n- Too many other individual configurable option to list in this changelog :)\n\n### Local Settings\n- Local Settings GUI where users can change certain option on individual basis.\n    - Application executables.\n    - Project roots.\n    - Project site sync settings.\n\n### Build, Installation and Deployments\n- No requirements on artist machine.\n- Fully distributed workflow possible.\n- Self-contained installation.\n- Available on all three major platforms.\n- Automatic artist OpenPype updates.\n- Studio OpenPype repository for updates distribution.\n- Robust Build system.\n- Safe studio update versioning with staging and production options.\n- MacOS build generates .app and .dmg installer.\n- Windows build with installer creation script.\n\n### Misc\n- System and diagnostic info tool in the tray.\n- Launching application from Launcher indicates activity.\n- All project roots are now named. Single root project are now achieved by having a single named root in the project anatomy.\n- Every project root is cast into environment variable as well, so it can be used in DCC instead of absolute path (depends on DCC support for env vars).\n- Basic support for task types, on top of task names.\n- Timer now change automatically when the context is switched inside running application.\n- 'Master\" versions have been renamed to \"Hero\".\n- Extract Burnins now supports file sequences and color settings.\n- Extract Review support overscan cropping, better letterboxes and background colour fill.\n- Delivery tool for copying and renaming any published assets in bulk.\n- Harmony, Photoshop and After Effects now connect directly with OpenPype tray instead of spawning their own terminal.\n\n### Project Manager GUI\n- Create Projects.\n- Create Shots and Assets.\n- Create Tasks and assign task types.\n- Fill required asset attributes.\n- Validations for duplicated or unsupported names.\n- Archive Assets.\n- Move Asset within hierarchy.\n\n### Site Sync (beta)\n- Synchronization of published files between workstations and central storage.\n- Ability to add arbitrary storage providers to the Site Sync system.\n- Default setup includes Disk and Google Drive providers as examples.\n- Access to availability information from Loader and Scene Manager.\n- Sync queue GUI with filtering, error and status reporting.\n- Site sync can be configured on a per-project basis.\n- Bulk upload and download from the loader.\n\n### Ftrack\n- Actions have customisable roles.\n- Settings on all actions are updated live and don't need openpype restart.\n- Ftrack module can now be turned off completely.\n- It is enough to specify ftrack server name and the URL will be formed correctly. So instead of mystudio.ftrackapp.com, it's possible to use simply: \"mystudio\".\n\n### Editorial\n- Fully OTIO based editorial publishing.\n- Completely re-done Hiero publishing to be a lot simpler and faster.\n- Consistent conforming from Resolve, Hiero and Standalone Publisher.\n\n### Backend\n- OpenPype and Avalon now always share the same database (in 2.x is was possible to split them).\n- Major codebase refactoring to allow for better CI, versioning and control of individual integrations.\n- OTIO is bundled with build.\n- OIIO is bundled with build.\n- FFMPEG is bundled with build.\n- Rest API and host WebSocket servers have been unified into a single local webserver.\n- Maya look assigner has been integrated into the main codebase.\n- Publish GUI has been integrated into the main codebase.\n- Studio and Project settings overrides are now stored in Mongo.\n- Too many other backend fixes and tweaks to list :), you can see full changelog on github for those.\n- OpenPype uses Poetry to manage it's virtual environment when running from code.\n- all applications can be marked as python 2 or 3 compatible to make the switch a bit easier.\n\n\n### Pull Requests since 3.0.0-rc.6\n\n\n**Implemented enhancements:**\n\n- settings: task types enum entity [\\#1605](https://github.com/pypeclub/OpenPype/issues/1605)\n- Settings: ignore keys in referenced schema [\\#1600](https://github.com/pypeclub/OpenPype/issues/1600)\n- Maya: support for frame steps and frame lists [\\#1585](https://github.com/pypeclub/OpenPype/issues/1585)\n- TVPaint: Publish workfile. [\\#1548](https://github.com/pypeclub/OpenPype/issues/1548)\n- Loader: Current Asset Button [\\#1448](https://github.com/pypeclub/OpenPype/issues/1448)\n- Hiero: publish with retiming [\\#1377](https://github.com/pypeclub/OpenPype/issues/1377)\n- Ask user to restart after changing global environments in settings [\\#910](https://github.com/pypeclub/OpenPype/issues/910)\n- add option to define paht to workfile template [\\#895](https://github.com/pypeclub/OpenPype/issues/895)\n- Harmony: move server console to system tray [\\#676](https://github.com/pypeclub/OpenPype/issues/676)\n- Standalone style [\\#1630](https://github.com/pypeclub/OpenPype/pull/1630) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Faster hierarchical values push [\\#1627](https://github.com/pypeclub/OpenPype/pull/1627) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Launcher tool style [\\#1624](https://github.com/pypeclub/OpenPype/pull/1624) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Loader and Library loader enhancements [\\#1623](https://github.com/pypeclub/OpenPype/pull/1623) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Tray style [\\#1622](https://github.com/pypeclub/OpenPype/pull/1622) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Maya schemas cleanup [\\#1610](https://github.com/pypeclub/OpenPype/pull/1610) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Settings: ignore keys in referenced schema [\\#1608](https://github.com/pypeclub/OpenPype/pull/1608) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- settings: task types enum entity [\\#1606](https://github.com/pypeclub/OpenPype/pull/1606) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Openpype style [\\#1604](https://github.com/pypeclub/OpenPype/pull/1604) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- TVPaint: Publish workfile. [\\#1597](https://github.com/pypeclub/OpenPype/pull/1597) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Nuke: add option to define path to workfile template [\\#1571](https://github.com/pypeclub/OpenPype/pull/1571) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Crop overscan in Extract Review [\\#1569](https://github.com/pypeclub/OpenPype/pull/1569) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Unreal and Blender: Material Workflow [\\#1562](https://github.com/pypeclub/OpenPype/pull/1562) ([simonebarbieri](https://github.com/simonebarbieri))\n- Harmony: move server console to system tray [\\#1560](https://github.com/pypeclub/OpenPype/pull/1560) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Ask user to restart after changing global environments in settings [\\#1550](https://github.com/pypeclub/OpenPype/pull/1550) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Hiero: publish with retiming [\\#1545](https://github.com/pypeclub/OpenPype/pull/1545) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n\n**Fixed bugs:**\n\n- Library loader load asset documents on OpenPype start [\\#1603](https://github.com/pypeclub/OpenPype/issues/1603)\n- Resolve: unable to load the same footage twice [\\#1317](https://github.com/pypeclub/OpenPype/issues/1317)\n- Resolve: unable to load footage [\\#1316](https://github.com/pypeclub/OpenPype/issues/1316)\n- Add required Python 2 modules [\\#1291](https://github.com/pypeclub/OpenPype/issues/1291)\n- GUi scaling with hires displays [\\#705](https://github.com/pypeclub/OpenPype/issues/705)\n- Maya: non unicode string in publish validation [\\#673](https://github.com/pypeclub/OpenPype/issues/673)\n- Nuke: Rendered Frame validation is triggered by multiple collections [\\#156](https://github.com/pypeclub/OpenPype/issues/156)\n- avalon-core debugging failing [\\#80](https://github.com/pypeclub/OpenPype/issues/80)\n- Only check arnold shading group if arnold is used [\\#72](https://github.com/pypeclub/OpenPype/issues/72)\n- Sync server Qt layout fix [\\#1621](https://github.com/pypeclub/OpenPype/pull/1621) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Console Listener on Python 2 fix [\\#1620](https://github.com/pypeclub/OpenPype/pull/1620) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Bug: Initialize blessed term only in console mode [\\#1619](https://github.com/pypeclub/OpenPype/pull/1619) ([antirotor](https://github.com/antirotor))\n- Settings template skip paths support wrappers [\\#1618](https://github.com/pypeclub/OpenPype/pull/1618) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Maya capture 'isolate\\_view' fix + minor corrections [\\#1617](https://github.com/pypeclub/OpenPype/pull/1617) ([2-REC](https://github.com/2-REC))\n- MacOs Fix launch of standalone publisher [\\#1616](https://github.com/pypeclub/OpenPype/pull/1616) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- 'Delivery action' report fix + typos [\\#1612](https://github.com/pypeclub/OpenPype/pull/1612) ([2-REC](https://github.com/2-REC))\n- List append fix in mutable dict settings [\\#1599](https://github.com/pypeclub/OpenPype/pull/1599) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Documentation: Maya: fix review [\\#1598](https://github.com/pypeclub/OpenPype/pull/1598) ([antirotor](https://github.com/antirotor))\n- Bugfix: Set certifi CA bundle for all platforms [\\#1596](https://github.com/pypeclub/OpenPype/pull/1596) ([antirotor](https://github.com/antirotor))\n\n**Merged pull requests:**\n\n- Bump dns-packet from 1.3.1 to 1.3.4 in /website [\\#1611](https://github.com/pypeclub/OpenPype/pull/1611) ([dependabot[bot]](https://github.com/apps/dependabot))\n- Maya: Render workflow fixes [\\#1607](https://github.com/pypeclub/OpenPype/pull/1607) ([antirotor](https://github.com/antirotor))\n- Maya: support for frame steps and frame lists [\\#1586](https://github.com/pypeclub/OpenPype/pull/1586) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- 3.0.0 - curated changelog [\\#1284](https://github.com/pypeclub/OpenPype/pull/1284) ([mkolar](https://github.com/mkolar))\n\n## [2.18.1](https://github.com/pypeclub/openpype/tree/2.18.1) (2021-06-03)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.0...2.18.1)\n\n**Enhancements:**\n\n- Faster hierarchical values push [\\#1626](https://github.com/pypeclub/OpenPype/pull/1626)\n- Feature Delivery in library loader [\\#1549](https://github.com/pypeclub/OpenPype/pull/1549)\n- Hiero: Initial frame publish support. [\\#1172](https://github.com/pypeclub/OpenPype/pull/1172)\n\n**Fixed bugs:**\n\n- Maya capture 'isolate\\_view' fix + minor corrections [\\#1614](https://github.com/pypeclub/OpenPype/pull/1614)\n- 'Delivery action' report fix +typos [\\#1613](https://github.com/pypeclub/OpenPype/pull/1613)\n- Delivery in LibraryLoader - fixed sequence issue [\\#1590](https://github.com/pypeclub/OpenPype/pull/1590)\n- FFmpeg filters in quote marks [\\#1588](https://github.com/pypeclub/OpenPype/pull/1588)\n- Ftrack delete action cause circular error [\\#1581](https://github.com/pypeclub/OpenPype/pull/1581)\n- Fix Maya playblast. [\\#1566](https://github.com/pypeclub/OpenPype/pull/1566)\n- More failsafes prevent errored runs. [\\#1554](https://github.com/pypeclub/OpenPype/pull/1554)\n- Celaction publishing [\\#1539](https://github.com/pypeclub/OpenPype/pull/1539)\n- celaction: app not starting [\\#1533](https://github.com/pypeclub/OpenPype/pull/1533)\n\n**Merged pull requests:**\n\n- Maya: Render workflow fixes - 2.0 backport [\\#1609](https://github.com/pypeclub/OpenPype/pull/1609)\n- Maya Hardware support [\\#1553](https://github.com/pypeclub/OpenPype/pull/1553)\n\n\n## [CI/3.0.0-rc.6](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.6) (2021-05-27)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.5...CI/3.0.0-rc.6)\n\n**Implemented enhancements:**\n\n- Hiero: publish color and transformation soft-effects [\\#1376](https://github.com/pypeclub/OpenPype/issues/1376)\n- Get rid of `AVALON\\_HIERARCHY` and `hiearchy` key on asset [\\#432](https://github.com/pypeclub/OpenPype/issues/432)\n- Sync to avalon do not store hierarchy key [\\#1582](https://github.com/pypeclub/OpenPype/pull/1582) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Tools: launcher scripts for project manager [\\#1557](https://github.com/pypeclub/OpenPype/pull/1557) ([antirotor](https://github.com/antirotor))\n- Simple tvpaint publish [\\#1555](https://github.com/pypeclub/OpenPype/pull/1555) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Feature Delivery in library loader [\\#1546](https://github.com/pypeclub/OpenPype/pull/1546) ([kalisp](https://github.com/kalisp))\n- Documentation: Dev and system build documentation [\\#1543](https://github.com/pypeclub/OpenPype/pull/1543) ([antirotor](https://github.com/antirotor))\n- Color entity [\\#1542](https://github.com/pypeclub/OpenPype/pull/1542) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Extract review bg color [\\#1534](https://github.com/pypeclub/OpenPype/pull/1534) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- TVPaint loader settings [\\#1530](https://github.com/pypeclub/OpenPype/pull/1530) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Blender can initialize differente user script paths [\\#1528](https://github.com/pypeclub/OpenPype/pull/1528) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Blender and Unreal: Improved Animation Workflow [\\#1514](https://github.com/pypeclub/OpenPype/pull/1514) ([simonebarbieri](https://github.com/simonebarbieri))\n- Hiero: publish color and transformation soft-effects [\\#1511](https://github.com/pypeclub/OpenPype/pull/1511) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n\n**Fixed bugs:**\n\n- OpenPype specific version issues [\\#1583](https://github.com/pypeclub/OpenPype/issues/1583)\n- Ftrack login server can't work without stderr [\\#1576](https://github.com/pypeclub/OpenPype/issues/1576)\n- Mac application launch [\\#1575](https://github.com/pypeclub/OpenPype/issues/1575)\n- Settings are not propagated to Nuke write nodes [\\#1538](https://github.com/pypeclub/OpenPype/issues/1538)\n- Subset names settings not applied for publishing [\\#1537](https://github.com/pypeclub/OpenPype/issues/1537)\n- Nuke: callback at start not setting colorspace [\\#1412](https://github.com/pypeclub/OpenPype/issues/1412)\n- Pype 3: Missing icon for Settings [\\#1272](https://github.com/pypeclub/OpenPype/issues/1272)\n- Blender: cannot initialize Avalon if BLENDER\\_USER\\_SCRIPTS is already used [\\#1050](https://github.com/pypeclub/OpenPype/issues/1050)\n- Ftrack delete action cause circular error [\\#206](https://github.com/pypeclub/OpenPype/issues/206)\n- Build: stop cleaning of pyc files in build directory [\\#1592](https://github.com/pypeclub/OpenPype/pull/1592) ([antirotor](https://github.com/antirotor))\n- Ftrack login server can't work without stderr [\\#1591](https://github.com/pypeclub/OpenPype/pull/1591) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- FFmpeg filters in quote marks [\\#1589](https://github.com/pypeclub/OpenPype/pull/1589) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- OpenPype specific version issues [\\#1584](https://github.com/pypeclub/OpenPype/pull/1584) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Mac application launch [\\#1580](https://github.com/pypeclub/OpenPype/pull/1580) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Ftrack delete action cause circular error [\\#1579](https://github.com/pypeclub/OpenPype/pull/1579) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Hiero: publishing issues  [\\#1578](https://github.com/pypeclub/OpenPype/pull/1578) ([jezscha](https://github.com/jezscha))\n- Nuke: callback at start not setting colorspace [\\#1561](https://github.com/pypeclub/OpenPype/pull/1561) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Bugfix  PS subset and quick review [\\#1541](https://github.com/pypeclub/OpenPype/pull/1541) ([kalisp](https://github.com/kalisp))\n- Settings are not propagated to Nuke write nodes [\\#1540](https://github.com/pypeclub/OpenPype/pull/1540) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- OpenPype: Powershell scripts polishing [\\#1536](https://github.com/pypeclub/OpenPype/pull/1536) ([antirotor](https://github.com/antirotor))\n- Host name collecting fix [\\#1535](https://github.com/pypeclub/OpenPype/pull/1535) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Handle duplicated task names in project manager [\\#1531](https://github.com/pypeclub/OpenPype/pull/1531) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Validate is file attribute in settings schema [\\#1529](https://github.com/pypeclub/OpenPype/pull/1529) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n\n**Merged pull requests:**\n\n- Bump postcss from 8.2.8 to 8.3.0 in /website [\\#1593](https://github.com/pypeclub/OpenPype/pull/1593) ([dependabot[bot]](https://github.com/apps/dependabot))\n- User installation documentation [\\#1532](https://github.com/pypeclub/OpenPype/pull/1532) ([64qam](https://github.com/64qam))\n\n## [CI/3.0.0-rc.5](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.5) (2021-05-19)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.0...CI/3.0.0-rc.5)\n\n**Implemented enhancements:**\n\n- OpenPype: Build - Add progress bars [\\#1524](https://github.com/pypeclub/OpenPype/pull/1524) ([antirotor](https://github.com/antirotor))\n- Default environments per host imlementation [\\#1522](https://github.com/pypeclub/OpenPype/pull/1522) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- OpenPype: use `semver` module for version resolution [\\#1513](https://github.com/pypeclub/OpenPype/pull/1513) ([antirotor](https://github.com/antirotor))\n- Feature Aftereffects setting cleanup documentation [\\#1510](https://github.com/pypeclub/OpenPype/pull/1510) ([kalisp](https://github.com/kalisp))\n- Feature Sync server settings enhancement [\\#1501](https://github.com/pypeclub/OpenPype/pull/1501) ([kalisp](https://github.com/kalisp))\n- Project manager [\\#1396](https://github.com/pypeclub/OpenPype/pull/1396) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n\n**Fixed bugs:**\n\n- Unified schema definition [\\#874](https://github.com/pypeclub/OpenPype/issues/874)\n- Maya: fix look assignment [\\#1526](https://github.com/pypeclub/OpenPype/pull/1526) ([antirotor](https://github.com/antirotor))\n- Bugfix Sync server local site issues [\\#1523](https://github.com/pypeclub/OpenPype/pull/1523) ([kalisp](https://github.com/kalisp))\n- Store as list dictionary check initial value with right type [\\#1520](https://github.com/pypeclub/OpenPype/pull/1520) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Maya: wrong collection of playblasted frames [\\#1515](https://github.com/pypeclub/OpenPype/pull/1515) ([mkolar](https://github.com/mkolar))\n- Convert pyblish logs to string at the moment of logging [\\#1512](https://github.com/pypeclub/OpenPype/pull/1512) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- 3.0 | nuke: fixing start\\_at with option gui [\\#1509](https://github.com/pypeclub/OpenPype/pull/1509) ([jezscha](https://github.com/jezscha))\n- Tests: fix pype -\\> openpype to make tests work again [\\#1508](https://github.com/pypeclub/OpenPype/pull/1508) ([antirotor](https://github.com/antirotor))\n\n**Merged pull requests:**\n\n- OpenPype: disable submodule update with `--no-submodule-update` [\\#1525](https://github.com/pypeclub/OpenPype/pull/1525) ([antirotor](https://github.com/antirotor))\n- Ftrack without autosync in Pype 3 [\\#1519](https://github.com/pypeclub/OpenPype/pull/1519) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Feature Harmony setting cleanup documentation [\\#1506](https://github.com/pypeclub/OpenPype/pull/1506) ([kalisp](https://github.com/kalisp))\n- Sync Server beginning of documentation [\\#1471](https://github.com/pypeclub/OpenPype/pull/1471) ([kalisp](https://github.com/kalisp))\n- Blender: publish layout json [\\#1348](https://github.com/pypeclub/OpenPype/pull/1348) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n\n## [2.18.0](https://github.com/pypeclub/openpype/tree/2.18.0) (2021-05-18)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.4...2.18.0)\n\n**Implemented enhancements:**\n\n- Default environments per host imlementation [\\#1405](https://github.com/pypeclub/OpenPype/issues/1405)\n- Blender: publish layout json [\\#1346](https://github.com/pypeclub/OpenPype/issues/1346)\n- Ftrack without autosync in Pype 3 [\\#1128](https://github.com/pypeclub/OpenPype/issues/1128)\n- Launcher: started action indicator [\\#1102](https://github.com/pypeclub/OpenPype/issues/1102)\n- Launch arguments of applications [\\#1094](https://github.com/pypeclub/OpenPype/issues/1094)\n- Publish: instance info [\\#724](https://github.com/pypeclub/OpenPype/issues/724)\n- Review: ability to control review length [\\#482](https://github.com/pypeclub/OpenPype/issues/482)\n- Colorized recognition of creator result [\\#394](https://github.com/pypeclub/OpenPype/issues/394)\n- event assign user to started task [\\#49](https://github.com/pypeclub/OpenPype/issues/49)\n- rebuild containers from reference in maya [\\#55](https://github.com/pypeclub/OpenPype/issues/55)\n- nuke Load metadata [\\#66](https://github.com/pypeclub/OpenPype/issues/66)\n- Maya: Safer handling of expected render output names [\\#1496](https://github.com/pypeclub/OpenPype/pull/1496) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- TVPaint: Increment workfile version on successfull publish. [\\#1489](https://github.com/pypeclub/OpenPype/pull/1489) ([tokejepsen](https://github.com/tokejepsen))\n- Use SubsetLoader and multiple contexts for delete\\_old\\_versions [\\#1484](https://github.com/pypeclub/OpenPype/pull/1484) ([tokejepsen](https://github.com/tokejepsen))\n- Maya: Use of multiple deadline servers [\\#1483](https://github.com/pypeclub/OpenPype/pull/1483) ([antirotor](https://github.com/antirotor))\n\n**Fixed bugs:**\n\n- Igniter version resolution doesn't consider it's own version [\\#1505](https://github.com/pypeclub/OpenPype/issues/1505)\n- Maya: Safer handling of expected render output names [\\#1159](https://github.com/pypeclub/OpenPype/issues/1159)\n- Harmony: Invalid render output from non-conventionally named instance [\\#871](https://github.com/pypeclub/OpenPype/issues/871)\n- Existing subsets hints in creator [\\#1503](https://github.com/pypeclub/OpenPype/pull/1503) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- nuke: space in node name breaking process [\\#1494](https://github.com/pypeclub/OpenPype/pull/1494) ([jezscha](https://github.com/jezscha))\n-  Maya: wrong collection of playblasted frames [\\#1517](https://github.com/pypeclub/OpenPype/pull/1517) ([mkolar](https://github.com/mkolar))\n- Existing subsets hints in creator [\\#1502](https://github.com/pypeclub/OpenPype/pull/1502) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Use instance frame start instead of timeline. [\\#1486](https://github.com/pypeclub/OpenPype/pull/1486) ([tokejepsen](https://github.com/tokejepsen))\n- Maya: Redshift - set proper start frame on proxy [\\#1480](https://github.com/pypeclub/OpenPype/pull/1480) ([antirotor](https://github.com/antirotor))\n\n**Closed issues:**\n\n- Nuke: wrong \"star at\" value on render load [\\#1352](https://github.com/pypeclub/OpenPype/issues/1352)\n- DV Resolve - loading/updating - image video [\\#915](https://github.com/pypeclub/OpenPype/issues/915)\n\n**Merged pull requests:**\n\n- nuke: fixing start\\_at with option gui [\\#1507](https://github.com/pypeclub/OpenPype/pull/1507) ([jezscha](https://github.com/jezscha))\n\n## [CI/3.0.0-rc.4](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.4) (2021-05-12)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...CI/3.0.0-rc.4)\n\n**Implemented enhancements:**\n\n- Resolve: documentation [\\#1490](https://github.com/pypeclub/OpenPype/issues/1490)\n- Hiero: audio to review [\\#1378](https://github.com/pypeclub/OpenPype/issues/1378)\n- nks color clips after publish [\\#44](https://github.com/pypeclub/OpenPype/issues/44)\n- Store data from modifiable dict as list [\\#1504](https://github.com/pypeclub/OpenPype/pull/1504) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Use SubsetLoader and multiple contexts for delete\\_old\\_versions [\\#1497](https://github.com/pypeclub/OpenPype/pull/1497) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Hiero: publish audio and add to review [\\#1493](https://github.com/pypeclub/OpenPype/pull/1493) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Resolve: documentation [\\#1491](https://github.com/pypeclub/OpenPype/pull/1491) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Change integratenew template profiles setting [\\#1487](https://github.com/pypeclub/OpenPype/pull/1487) ([kalisp](https://github.com/kalisp))\n- Settings tool cleanup [\\#1477](https://github.com/pypeclub/OpenPype/pull/1477) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Sorted Applications and Tools in Custom attribute [\\#1476](https://github.com/pypeclub/OpenPype/pull/1476) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- PS - group all published instances [\\#1416](https://github.com/pypeclub/OpenPype/pull/1416) ([kalisp](https://github.com/kalisp))\n- OpenPype: Support for Docker [\\#1289](https://github.com/pypeclub/OpenPype/pull/1289) ([antirotor](https://github.com/antirotor))\n\n**Fixed bugs:**\n\n- Harmony: palettes publishing [\\#1439](https://github.com/pypeclub/OpenPype/issues/1439)\n- Photoshop: validation for already created images [\\#1435](https://github.com/pypeclub/OpenPype/issues/1435)\n- Nuke Extracts Thumbnail from frame out of shot range [\\#963](https://github.com/pypeclub/OpenPype/issues/963)\n- Instance in same Context repairing [\\#390](https://github.com/pypeclub/OpenPype/issues/390)\n- User Inactivity - Start timers sets wrong time [\\#91](https://github.com/pypeclub/OpenPype/issues/91)\n- Use instance frame start instead of timeline [\\#1499](https://github.com/pypeclub/OpenPype/pull/1499) ([mkolar](https://github.com/mkolar))\n- Various smaller fixes [\\#1498](https://github.com/pypeclub/OpenPype/pull/1498) ([mkolar](https://github.com/mkolar))\n- nuke: space in node name breaking process [\\#1495](https://github.com/pypeclub/OpenPype/pull/1495) ([jezscha](https://github.com/jezscha))\n- Codec determination in extract burnin [\\#1492](https://github.com/pypeclub/OpenPype/pull/1492) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Undefined constant in subprocess module [\\#1485](https://github.com/pypeclub/OpenPype/pull/1485) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- List entity catch add/remove item changes properly [\\#1482](https://github.com/pypeclub/OpenPype/pull/1482) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Resolve: additional fixes of publishing workflow [\\#1481](https://github.com/pypeclub/OpenPype/pull/1481) ([jezscha](https://github.com/jezscha))\n- Photoshop: validation for already created images [\\#1436](https://github.com/pypeclub/OpenPype/pull/1436) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n\n**Merged pull requests:**\n\n- Maya: Support for looks on VRay Proxies [\\#1443](https://github.com/pypeclub/OpenPype/pull/1443) ([antirotor](https://github.com/antirotor))\n\n## [2.17.3](https://github.com/pypeclub/openpype/tree/2.17.3) (2021-05-06)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.3...2.17.3)\n\n**Fixed bugs:**\n\n- Nuke: workfile version synced to db version always  [\\#1479](https://github.com/pypeclub/OpenPype/pull/1479) ([jezscha](https://github.com/jezscha))\n\n## [CI/3.0.0-rc.3](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.3) (2021-05-05)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.2...CI/3.0.0-rc.3)\n\n**Implemented enhancements:**\n\n- Path entity with placeholder [\\#1473](https://github.com/pypeclub/OpenPype/pull/1473) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Burnin custom font filepath [\\#1472](https://github.com/pypeclub/OpenPype/pull/1472) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Poetry: Move to OpenPype [\\#1449](https://github.com/pypeclub/OpenPype/pull/1449) ([antirotor](https://github.com/antirotor))\n\n**Fixed bugs:**\n\n- Mac SSL path needs to be relative to pype\\_root [\\#1469](https://github.com/pypeclub/OpenPype/issues/1469)\n- Resolve: fix loading clips to timeline [\\#1421](https://github.com/pypeclub/OpenPype/issues/1421)\n- Wrong handling of slashes when loading on mac [\\#1411](https://github.com/pypeclub/OpenPype/issues/1411)\n- Nuke openpype3 [\\#1342](https://github.com/pypeclub/OpenPype/issues/1342)\n- Houdini launcher [\\#1171](https://github.com/pypeclub/OpenPype/issues/1171)\n- Fix SyncServer get\\_enabled\\_projects should handle global state [\\#1475](https://github.com/pypeclub/OpenPype/pull/1475) ([kalisp](https://github.com/kalisp))\n- Igniter buttons enable/disable fix [\\#1474](https://github.com/pypeclub/OpenPype/pull/1474) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Mac SSL path needs to be relative to pype\\_root [\\#1470](https://github.com/pypeclub/OpenPype/pull/1470) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Resolve: 17 compatibility issues and load image sequences [\\#1422](https://github.com/pypeclub/OpenPype/pull/1422) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n\n## [CI/3.0.0-rc.2](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.2) (2021-05-04)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.2...CI/3.0.0-rc.2)\n\n**Implemented enhancements:**\n\n- Extract burnins with sequences [\\#1467](https://github.com/pypeclub/OpenPype/pull/1467) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Extract burnins with color setting [\\#1466](https://github.com/pypeclub/OpenPype/pull/1466) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n\n**Fixed bugs:**\n\n- Fix groups check in Python 2 [\\#1468](https://github.com/pypeclub/OpenPype/pull/1468) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n\n## [2.17.2](https://github.com/pypeclub/openpype/tree/2.17.2) (2021-05-04)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.1...2.17.2)\n\n**Implemented enhancements:**\n\n- Forward/Backward compatible apps and tools with OpenPype 3 [\\#1463](https://github.com/pypeclub/OpenPype/pull/1463) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n\n## [CI/3.0.0-rc.1](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.1) (2021-05-04)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.1...CI/3.0.0-rc.1)\n\n**Implemented enhancements:**\n\n- Only show studio settings to admins [\\#1406](https://github.com/pypeclub/OpenPype/issues/1406)\n- Ftrack specific settings save warning messages [\\#1458](https://github.com/pypeclub/OpenPype/pull/1458) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Faster settings actions [\\#1446](https://github.com/pypeclub/OpenPype/pull/1446) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Feature/sync server priority [\\#1444](https://github.com/pypeclub/OpenPype/pull/1444) ([kalisp](https://github.com/kalisp))\n- Faster settings UI loading [\\#1442](https://github.com/pypeclub/OpenPype/pull/1442) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Igniter re-write [\\#1441](https://github.com/pypeclub/OpenPype/pull/1441) ([mkolar](https://github.com/mkolar))\n- Wrap openpype build into installers [\\#1419](https://github.com/pypeclub/OpenPype/pull/1419) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Extract review first documentation [\\#1404](https://github.com/pypeclub/OpenPype/pull/1404) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Blender PySide2 install guide [\\#1403](https://github.com/pypeclub/OpenPype/pull/1403) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Nuke: deadline submission with gpu [\\#1394](https://github.com/pypeclub/OpenPype/pull/1394) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Igniter: Reverse item filter for OpenPype version [\\#1349](https://github.com/pypeclub/OpenPype/pull/1349) ([antirotor](https://github.com/antirotor))\n\n**Fixed bugs:**\n\n- OpenPype Mongo URL definition [\\#1450](https://github.com/pypeclub/OpenPype/issues/1450)\n- Various typos and smaller fixes [\\#1464](https://github.com/pypeclub/OpenPype/pull/1464) ([mkolar](https://github.com/mkolar))\n- Validation of dynamic items in settings [\\#1462](https://github.com/pypeclub/OpenPype/pull/1462) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- List can handle new items correctly [\\#1459](https://github.com/pypeclub/OpenPype/pull/1459) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Settings actions process fix [\\#1457](https://github.com/pypeclub/OpenPype/pull/1457) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Add to overrides actions fix [\\#1456](https://github.com/pypeclub/OpenPype/pull/1456) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- OpenPype Mongo URL definition [\\#1455](https://github.com/pypeclub/OpenPype/pull/1455) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Global settings save/load out of system settings [\\#1447](https://github.com/pypeclub/OpenPype/pull/1447) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Keep metadata on remove overrides [\\#1445](https://github.com/pypeclub/OpenPype/pull/1445) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Nuke: fixing undo for loaded mov and sequence [\\#1432](https://github.com/pypeclub/OpenPype/pull/1432) ([jezscha](https://github.com/jezscha))\n- ExtractReview skip empty strings from settings [\\#1431](https://github.com/pypeclub/OpenPype/pull/1431) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Bugfix Sync server tweaks [\\#1430](https://github.com/pypeclub/OpenPype/pull/1430) ([kalisp](https://github.com/kalisp))\n- Hiero: missing thumbnail in review [\\#1429](https://github.com/pypeclub/OpenPype/pull/1429) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Bugfix Maya in deadline for OpenPype [\\#1428](https://github.com/pypeclub/OpenPype/pull/1428) ([kalisp](https://github.com/kalisp))\n- AE - validation for duration was 1 frame shorter [\\#1427](https://github.com/pypeclub/OpenPype/pull/1427) ([kalisp](https://github.com/kalisp))\n- Houdini menu filename [\\#1418](https://github.com/pypeclub/OpenPype/pull/1418) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Fix Avalon plugins attribute overrides [\\#1413](https://github.com/pypeclub/OpenPype/pull/1413) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Nuke: submit to Deadline fails [\\#1409](https://github.com/pypeclub/OpenPype/pull/1409) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- Validate MongoDB Url on start [\\#1407](https://github.com/pypeclub/OpenPype/pull/1407) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Nuke: fix set colorspace with new settings [\\#1386](https://github.com/pypeclub/OpenPype/pull/1386) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- MacOs build and install issues [\\#1380](https://github.com/pypeclub/OpenPype/pull/1380) ([mkolar](https://github.com/mkolar))\n\n**Closed issues:**\n\n- test [\\#1452](https://github.com/pypeclub/OpenPype/issues/1452)\n\n**Merged pull requests:**\n\n- TVPaint frame range definition [\\#1425](https://github.com/pypeclub/OpenPype/pull/1425) ([iLLiCiTiT](https://github.com/iLLiCiTiT))\n- Only show studio settings to admins [\\#1420](https://github.com/pypeclub/OpenPype/pull/1420) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))\n- TVPaint documentation [\\#1305](https://github.com/pypeclub/OpenPype/pull/1305) ([64qam](https://github.com/64qam))\n\n## [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) (2021-04-30)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.0...2.17.1)\n\n**Enhancements:**\n\n- Nuke: deadline submission with gpu [\\#1414](https://github.com/pypeclub/OpenPype/pull/1414)\n- TVPaint frame range definition [\\#1424](https://github.com/pypeclub/OpenPype/pull/1424)\n- PS - group all published instances [\\#1415](https://github.com/pypeclub/OpenPype/pull/1415)\n- Add task name to context pop up. [\\#1383](https://github.com/pypeclub/OpenPype/pull/1383)\n- Enhance review letterbox feature. [\\#1371](https://github.com/pypeclub/OpenPype/pull/1371)\n\n**Fixed bugs:**\n\n- Houdini menu filename [\\#1417](https://github.com/pypeclub/OpenPype/pull/1417)\n- AE - validation for duration was 1 frame shorter [\\#1426](https://github.com/pypeclub/OpenPype/pull/1426)\n\n**Merged pull requests:**\n\n- Maya: Vray - problem getting all file nodes for look publishing [\\#1399](https://github.com/pypeclub/OpenPype/pull/1399)\n- Maya: Support for Redshift proxies [\\#1360](https://github.com/pypeclub/OpenPype/pull/1360)\n\n## [2.17.0](https://github.com/pypeclub/openpype/tree/2.17.0) (2021-04-20)\n\n[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-beta.2...2.17.0)\n\n**Enhancements:**\n\n- Forward compatible ftrack group [\\#1243](https://github.com/pypeclub/OpenPype/pull/1243)\n- Settings in mongo as dict [\\#1221](https://github.com/pypeclub/OpenPype/pull/1221)\n- Maya: Make tx option configurable with presets [\\#1328](https://github.com/pypeclub/OpenPype/pull/1328)\n- TVPaint asset name validation [\\#1302](https://github.com/pypeclub/OpenPype/pull/1302)\n- TV Paint: Set initial project settings. [\\#1299](https://github.com/pypeclub/OpenPype/pull/1299)\n- TV Paint: Validate mark in and out. [\\#1298](https://github.com/pypeclub/OpenPype/pull/1298)\n- Validate project settings [\\#1297](https://github.com/pypeclub/OpenPype/pull/1297)\n- After Effects: added SubsetManager [\\#1234](https://github.com/pypeclub/OpenPype/pull/1234)\n- Show error message in pyblish UI [\\#1206](https://github.com/pypeclub/OpenPype/pull/1206)\n\n**Fixed bugs:**\n\n- Hiero: fixing source frame from correct object [\\#1362](https://github.com/pypeclub/OpenPype/pull/1362)\n- Nuke: fix colourspace, prerenders and nuke panes opening [\\#1308](https://github.com/pypeclub/OpenPype/pull/1308)\n- AE remove orphaned instance from workfile - fix self.stub [\\#1282](https://github.com/pypeclub/OpenPype/pull/1282)\n- Nuke: deadline submission with search replaced env values from preset [\\#1194](https://github.com/pypeclub/OpenPype/pull/1194)\n- Ftrack custom attributes in bulks [\\#1312](https://github.com/pypeclub/OpenPype/pull/1312)\n- Ftrack optional pypclub role [\\#1303](https://github.com/pypeclub/OpenPype/pull/1303)\n- After Effects: remove orphaned instances [\\#1275](https://github.com/pypeclub/OpenPype/pull/1275)\n- Avalon schema names [\\#1242](https://github.com/pypeclub/OpenPype/pull/1242)\n- Handle duplication of Task name [\\#1226](https://github.com/pypeclub/OpenPype/pull/1226)\n- Modified path of plugin loads for Harmony and TVPaint [\\#1217](https://github.com/pypeclub/OpenPype/pull/1217)\n- Regex checks in profiles filtering [\\#1214](https://github.com/pypeclub/OpenPype/pull/1214)\n- Bulk mov strict task [\\#1204](https://github.com/pypeclub/OpenPype/pull/1204)\n- Update custom ftrack session attributes [\\#1202](https://github.com/pypeclub/OpenPype/pull/1202)\n- Nuke: write node colorspace ignore `default\\(\\)` label [\\#1199](https://github.com/pypeclub/OpenPype/pull/1199)\n- Nuke: reverse search to make it more versatile [\\#1178](https://github.com/pypeclub/OpenPype/pull/1178)\n\n\n\n## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.16.0)\n\n**Enhancements:**\n\n- Nuke: deadline submit limit group filter [\\#1167](https://github.com/pypeclub/pype/pull/1167)\n- Maya: support for Deadline Group and Limit Groups - backport 2.x [\\#1156](https://github.com/pypeclub/pype/pull/1156)\n- Maya: fixes for Redshift support [\\#1152](https://github.com/pypeclub/pype/pull/1152)\n- Nuke: adding preset for a Read node name to all img and mov Loaders [\\#1146](https://github.com/pypeclub/pype/pull/1146)\n- nuke deadline submit with environ var from presets overrides [\\#1142](https://github.com/pypeclub/pype/pull/1142)\n- Change timers after task change [\\#1138](https://github.com/pypeclub/pype/pull/1138)\n- Nuke: shortcuts for Pype menu [\\#1127](https://github.com/pypeclub/pype/pull/1127)\n- Nuke: workfile template [\\#1124](https://github.com/pypeclub/pype/pull/1124)\n- Sites local settings by site name [\\#1117](https://github.com/pypeclub/pype/pull/1117)\n- Reset loader's asset selection on context change [\\#1106](https://github.com/pypeclub/pype/pull/1106)\n- Bulk mov render publishing [\\#1101](https://github.com/pypeclub/pype/pull/1101)\n- Photoshop: mark publishable instances [\\#1093](https://github.com/pypeclub/pype/pull/1093)\n- Added ability to define BG color for extract review [\\#1088](https://github.com/pypeclub/pype/pull/1088)\n- TVPaint extractor enhancement [\\#1080](https://github.com/pypeclub/pype/pull/1080)\n- Photoshop: added support for .psb in workfiles [\\#1078](https://github.com/pypeclub/pype/pull/1078)\n- Optionally add task to subset name [\\#1072](https://github.com/pypeclub/pype/pull/1072)\n- Only extend clip range when collecting. [\\#1008](https://github.com/pypeclub/pype/pull/1008)\n- Collect audio for farm reviews. [\\#1073](https://github.com/pypeclub/pype/pull/1073)\n\n\n**Fixed bugs:**\n\n- Fix path spaces in jpeg extractor [\\#1174](https://github.com/pypeclub/pype/pull/1174)\n- Maya: Bugfix: superclass for CreateCameraRig [\\#1166](https://github.com/pypeclub/pype/pull/1166)\n- Maya: Submit to Deadline - fix typo in condition [\\#1163](https://github.com/pypeclub/pype/pull/1163)\n- Avoid dot in repre extension [\\#1125](https://github.com/pypeclub/pype/pull/1125)\n- Fix versions variable usage in standalone publisher [\\#1090](https://github.com/pypeclub/pype/pull/1090)\n- Collect instance data fix subset query [\\#1082](https://github.com/pypeclub/pype/pull/1082)\n- Fix getting the camera name. [\\#1067](https://github.com/pypeclub/pype/pull/1067)\n- Nuke: Ensure \"NUKE\\_TEMP\\_DIR\" is not part of the Deadline job environment. [\\#1064](https://github.com/pypeclub/pype/pull/1064)\n\n## [2.15.3](https://github.com/pypeclub/pype/tree/2.15.3) (2021-02-26)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.2...2.15.3)\n\n**Enhancements:**\n\n- Maya: speedup renderable camera collection [\\#1053](https://github.com/pypeclub/pype/pull/1053)\n- Harmony - add regex search to filter allowed task names for collectin… [\\#1047](https://github.com/pypeclub/pype/pull/1047)\n\n**Fixed bugs:**\n\n- Ftrack integrate hierarchy fix [\\#1085](https://github.com/pypeclub/pype/pull/1085)\n- Explicit subset filter in anatomy instance data [\\#1059](https://github.com/pypeclub/pype/pull/1059)\n- TVPaint frame offset [\\#1057](https://github.com/pypeclub/pype/pull/1057)\n- Auto fix unicode strings [\\#1046](https://github.com/pypeclub/pype/pull/1046)\n\n## [2.15.2](https://github.com/pypeclub/pype/tree/2.15.2) (2021-02-19)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.1...2.15.2)\n\n**Enhancements:**\n\n- Maya: Vray scene publishing [\\#1013](https://github.com/pypeclub/pype/pull/1013)\n\n**Fixed bugs:**\n\n- Fix entity move under project [\\#1040](https://github.com/pypeclub/pype/pull/1040)\n- smaller nuke fixes from production [\\#1036](https://github.com/pypeclub/pype/pull/1036)\n- TVPaint thumbnail extract fix [\\#1031](https://github.com/pypeclub/pype/pull/1031)\n\n## [2.15.1](https://github.com/pypeclub/pype/tree/2.15.1) (2021-02-12)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.0...2.15.1)\n\n**Enhancements:**\n\n- Delete version as loader action [\\#1011](https://github.com/pypeclub/pype/pull/1011)\n- Delete old versions [\\#445](https://github.com/pypeclub/pype/pull/445)\n\n**Fixed bugs:**\n\n- PS - remove obsolete functions from pywin32 [\\#1006](https://github.com/pypeclub/pype/pull/1006)\n- Clone description of review session objects. [\\#922](https://github.com/pypeclub/pype/pull/922)\n\n## [2.15.0](https://github.com/pypeclub/pype/tree/2.15.0) (2021-02-09)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.6...2.15.0)\n\n**Enhancements:**\n\n- Resolve - loading and updating clips [\\#932](https://github.com/pypeclub/pype/pull/932)\n- Release/2.15.0 [\\#926](https://github.com/pypeclub/pype/pull/926)\n- Photoshop: add option for template.psd and prelaunch hook  [\\#894](https://github.com/pypeclub/pype/pull/894)\n- Nuke: deadline presets [\\#993](https://github.com/pypeclub/pype/pull/993)\n- Maya: Alembic only set attributes that exists. [\\#986](https://github.com/pypeclub/pype/pull/986)\n- Harmony: render local and handle fixes [\\#981](https://github.com/pypeclub/pype/pull/981)\n- PSD Bulk export of ANIM group [\\#965](https://github.com/pypeclub/pype/pull/965)\n- AE - added prelaunch hook for opening last or workfile from template [\\#944](https://github.com/pypeclub/pype/pull/944)\n- PS - safer handling of loading of workfile [\\#941](https://github.com/pypeclub/pype/pull/941)\n- Maya: Handling Arnold referenced AOVs [\\#938](https://github.com/pypeclub/pype/pull/938)\n- TVPaint: switch layer IDs for layer names during identification [\\#903](https://github.com/pypeclub/pype/pull/903)\n- TVPaint audio/sound loader [\\#893](https://github.com/pypeclub/pype/pull/893)\n- Clone review session with children. [\\#891](https://github.com/pypeclub/pype/pull/891)\n- Simple compositing data packager for freelancers [\\#884](https://github.com/pypeclub/pype/pull/884)\n- Harmony deadline submission [\\#881](https://github.com/pypeclub/pype/pull/881)\n- Maya: Optionally hide image planes from reviews. [\\#840](https://github.com/pypeclub/pype/pull/840)\n- Maya: handle referenced AOVs for Vray [\\#824](https://github.com/pypeclub/pype/pull/824)\n- DWAA/DWAB support on windows [\\#795](https://github.com/pypeclub/pype/pull/795)\n- Unreal: animation, layout and setdress updates [\\#695](https://github.com/pypeclub/pype/pull/695)\n\n**Fixed bugs:**\n\n- Maya: Looks - disable hardlinks [\\#995](https://github.com/pypeclub/pype/pull/995)\n- Fix Ftrack custom attribute update [\\#982](https://github.com/pypeclub/pype/pull/982)\n- Prores ks in burnin script [\\#960](https://github.com/pypeclub/pype/pull/960)\n- terminal.py crash on import [\\#839](https://github.com/pypeclub/pype/pull/839)\n- Extract review handle bizarre pixel aspect ratio [\\#990](https://github.com/pypeclub/pype/pull/990)\n- Nuke: add nuke related env var to sumbission  [\\#988](https://github.com/pypeclub/pype/pull/988)\n- Nuke: missing preset's variable  [\\#984](https://github.com/pypeclub/pype/pull/984)\n- Get creator by name fix [\\#979](https://github.com/pypeclub/pype/pull/979)\n- Fix update of project's tasks on Ftrack sync [\\#972](https://github.com/pypeclub/pype/pull/972)\n- nuke: wrong frame offset in mov loader  [\\#971](https://github.com/pypeclub/pype/pull/971)\n- Create project structure action fix multiroot [\\#967](https://github.com/pypeclub/pype/pull/967)\n- PS: remove pywin installation from hook [\\#964](https://github.com/pypeclub/pype/pull/964)\n- Prores ks in burnin script [\\#959](https://github.com/pypeclub/pype/pull/959)\n- Subset family is now stored in subset document [\\#956](https://github.com/pypeclub/pype/pull/956)\n- DJV new version arguments [\\#954](https://github.com/pypeclub/pype/pull/954)\n- TV Paint: Fix single frame Sequence [\\#953](https://github.com/pypeclub/pype/pull/953)\n- nuke: missing `file` knob update  [\\#933](https://github.com/pypeclub/pype/pull/933)\n- Photoshop: Create from single layer was failing [\\#920](https://github.com/pypeclub/pype/pull/920)\n- Nuke: baking mov with correct colorspace inherited from write  [\\#909](https://github.com/pypeclub/pype/pull/909)\n- Launcher fix actions discover [\\#896](https://github.com/pypeclub/pype/pull/896)\n- Get the correct file path for the updated mov. [\\#889](https://github.com/pypeclub/pype/pull/889)\n- Maya: Deadline submitter - shared data access violation [\\#831](https://github.com/pypeclub/pype/pull/831)\n- Maya: Take into account vray master AOV switch [\\#822](https://github.com/pypeclub/pype/pull/822)\n\n**Merged pull requests:**\n\n- Refactor blender to 3.0 format [\\#934](https://github.com/pypeclub/pype/pull/934)\n\n## [2.14.6](https://github.com/pypeclub/pype/tree/2.14.6) (2021-01-15)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.5...2.14.6)\n\n**Fixed bugs:**\n\n- Nuke: improving of hashing path  [\\#885](https://github.com/pypeclub/pype/pull/885)\n\n**Merged pull requests:**\n\n- Hiero: cut videos with correct secons  [\\#892](https://github.com/pypeclub/pype/pull/892)\n- Faster sync to avalon preparation [\\#869](https://github.com/pypeclub/pype/pull/869)\n\n## [2.14.5](https://github.com/pypeclub/pype/tree/2.14.5) (2021-01-06)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.4...2.14.5)\n\n**Merged pull requests:**\n\n- Pype logger refactor [\\#866](https://github.com/pypeclub/pype/pull/866)\n\n## [2.14.4](https://github.com/pypeclub/pype/tree/2.14.4) (2020-12-18)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.3...2.14.4)\n\n**Merged pull requests:**\n\n- Fix - AE - added explicit cast to int [\\#837](https://github.com/pypeclub/pype/pull/837)\n\n## [2.14.3](https://github.com/pypeclub/pype/tree/2.14.3) (2020-12-16)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.2...2.14.3)\n\n**Fixed bugs:**\n\n- TVPaint repair invalid metadata [\\#809](https://github.com/pypeclub/pype/pull/809)\n- Feature/push hier value to nonhier action [\\#807](https://github.com/pypeclub/pype/pull/807)\n- Harmony: fix palette and image sequence loader [\\#806](https://github.com/pypeclub/pype/pull/806)\n\n**Merged pull requests:**\n\n- respecting space in path [\\#823](https://github.com/pypeclub/pype/pull/823)\n\n## [2.14.2](https://github.com/pypeclub/pype/tree/2.14.2) (2020-12-04)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.1...2.14.2)\n\n**Enhancements:**\n\n- Collapsible wrapper in settings [\\#767](https://github.com/pypeclub/pype/pull/767)\n\n**Fixed bugs:**\n\n- Harmony: template extraction and palettes thumbnails on mac [\\#768](https://github.com/pypeclub/pype/pull/768)\n- TVPaint store context to workfile metadata \\(764\\) [\\#766](https://github.com/pypeclub/pype/pull/766)\n- Extract review audio cut fix [\\#763](https://github.com/pypeclub/pype/pull/763)\n\n**Merged pull requests:**\n\n- AE: fix publish after background load [\\#781](https://github.com/pypeclub/pype/pull/781)\n- TVPaint store members key [\\#769](https://github.com/pypeclub/pype/pull/769)\n\n## [2.14.1](https://github.com/pypeclub/pype/tree/2.14.1) (2020-11-27)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.0...2.14.1)\n\n**Enhancements:**\n\n- Settings required keys in modifiable dict [\\#770](https://github.com/pypeclub/pype/pull/770)\n- Extract review may not add audio to output [\\#761](https://github.com/pypeclub/pype/pull/761)\n\n**Fixed bugs:**\n\n- After Effects: frame range, file format and render source scene fixes [\\#760](https://github.com/pypeclub/pype/pull/760)\n- Hiero: trimming review with clip event number  [\\#754](https://github.com/pypeclub/pype/pull/754)\n- TVPaint: fix updating of loaded subsets [\\#752](https://github.com/pypeclub/pype/pull/752)\n- Maya: Vray handling of default aov [\\#748](https://github.com/pypeclub/pype/pull/748)\n- Maya: multiple renderable cameras in layer didn't work [\\#744](https://github.com/pypeclub/pype/pull/744)\n- Ftrack integrate custom attributes fix [\\#742](https://github.com/pypeclub/pype/pull/742)\n\n## [2.14.0](https://github.com/pypeclub/pype/tree/2.14.0) (2020-11-23)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.7...2.14.0)\n\n**Enhancements:**\n\n- Render publish plugins abstraction [\\#687](https://github.com/pypeclub/pype/pull/687)\n- Shot asset build trigger status [\\#736](https://github.com/pypeclub/pype/pull/736)\n- Maya: add camera rig publishing option [\\#721](https://github.com/pypeclub/pype/pull/721)\n- Sort instances by label in pyblish gui  [\\#719](https://github.com/pypeclub/pype/pull/719)\n- Synchronize ftrack hierarchical and shot attributes [\\#716](https://github.com/pypeclub/pype/pull/716)\n- 686 standalonepublisher editorial from image sequences [\\#699](https://github.com/pypeclub/pype/pull/699)\n- Ask user to select non-default camera from scene or create a new. [\\#678](https://github.com/pypeclub/pype/pull/678)\n- TVPaint: image loader with options [\\#675](https://github.com/pypeclub/pype/pull/675)\n- Maya: Camera name can be added to burnins. [\\#674](https://github.com/pypeclub/pype/pull/674)\n- After Effects: base integration with loaders [\\#667](https://github.com/pypeclub/pype/pull/667)\n- Harmony: Javascript refactoring and overall stability improvements [\\#666](https://github.com/pypeclub/pype/pull/666)\n\n**Fixed bugs:**\n\n- Bugfix Hiero Review / Plate representation publish [\\#743](https://github.com/pypeclub/pype/pull/743)\n- Asset fetch second fix [\\#726](https://github.com/pypeclub/pype/pull/726)\n- TVPaint extract review fix [\\#740](https://github.com/pypeclub/pype/pull/740)\n- After Effects: Review were not being sent to ftrack [\\#738](https://github.com/pypeclub/pype/pull/738)\n- Maya: vray proxy was not loading [\\#722](https://github.com/pypeclub/pype/pull/722)\n- Maya: Vray expected file fixes [\\#682](https://github.com/pypeclub/pype/pull/682)\n- Missing audio on farm submission. [\\#639](https://github.com/pypeclub/pype/pull/639)\n\n**Deprecated:**\n\n- Removed artist view from pyblish gui [\\#717](https://github.com/pypeclub/pype/pull/717)\n- Maya: disable legacy override check for cameras [\\#715](https://github.com/pypeclub/pype/pull/715)\n\n**Merged pull requests:**\n\n- Application manager [\\#728](https://github.com/pypeclub/pype/pull/728)\n- Feature \\#664 3.0 lib refactor [\\#706](https://github.com/pypeclub/pype/pull/706)\n- Lib from illicit part 2 [\\#700](https://github.com/pypeclub/pype/pull/700)\n- 3.0 lib refactor - path tools [\\#697](https://github.com/pypeclub/pype/pull/697)\n\n## [2.13.7](https://github.com/pypeclub/pype/tree/2.13.7) (2020-11-19)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.6...2.13.7)\n\n**Fixed bugs:**\n\n- Standalone Publisher: getting fps from context instead of nonexistent entity  [\\#729](https://github.com/pypeclub/pype/pull/729)\n\n## [2.13.6](https://github.com/pypeclub/pype/tree/2.13.6) (2020-11-15)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.5...2.13.6)\n\n**Fixed bugs:**\n\n- Maya workfile version wasn't syncing with renders properly [\\#711](https://github.com/pypeclub/pype/pull/711)\n- Maya: Fix for publishing multiple cameras with review from the same scene [\\#710](https://github.com/pypeclub/pype/pull/710)\n\n## [2.13.5](https://github.com/pypeclub/pype/tree/2.13.5) (2020-11-12)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.4...2.13.5)\n\n**Enhancements:**\n\n- 3.0 lib refactor [\\#664](https://github.com/pypeclub/pype/issues/664)\n\n**Fixed bugs:**\n\n- Wrong thumbnail file was picked when publishing sequence in standalone publisher [\\#703](https://github.com/pypeclub/pype/pull/703)\n- Fix: Burnin data pass and FFmpeg tool check [\\#701](https://github.com/pypeclub/pype/pull/701)\n\n## [2.13.4](https://github.com/pypeclub/pype/tree/2.13.4) (2020-11-09)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.3...2.13.4)\n\n**Enhancements:**\n\n- AfterEffects integration with Websocket [\\#663](https://github.com/pypeclub/pype/issues/663)\n\n**Fixed bugs:**\n\n- Photoshop uhiding hidden layers [\\#688](https://github.com/pypeclub/pype/issues/688)\n- \\#688 - Fix publishing hidden layers [\\#692](https://github.com/pypeclub/pype/pull/692)\n\n**Closed issues:**\n\n- Nuke Favorite directories \"shot dir\" \"project dir\" - not working [\\#684](https://github.com/pypeclub/pype/issues/684)\n\n**Merged pull requests:**\n\n- Nuke Favorite directories \"shot dir\" \"project dir\" - not working \\#684 [\\#685](https://github.com/pypeclub/pype/pull/685)\n\n## [2.13.3](https://github.com/pypeclub/pype/tree/2.13.3) (2020-11-03)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.2...2.13.3)\n\n**Enhancements:**\n\n- TV paint base integration [\\#612](https://github.com/pypeclub/pype/issues/612)\n\n**Fixed bugs:**\n\n- Fix ffmpeg executable path with spaces [\\#680](https://github.com/pypeclub/pype/pull/680)\n- Hotfix: Added default version number [\\#679](https://github.com/pypeclub/pype/pull/679)\n\n## [2.13.2](https://github.com/pypeclub/pype/tree/2.13.2) (2020-10-28)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.1...2.13.2)\n\n**Fixed bugs:**\n\n- Nuke: wrong conditions when fixing legacy write nodes [\\#665](https://github.com/pypeclub/pype/pull/665)\n\n## [2.13.1](https://github.com/pypeclub/pype/tree/2.13.1) (2020-10-23)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.0...2.13.1)\n\n**Enhancements:**\n\n- move maya look assigner to pype menu [\\#292](https://github.com/pypeclub/pype/issues/292)\n\n**Fixed bugs:**\n\n- Layer name is not propagating to metadata in Photoshop [\\#654](https://github.com/pypeclub/pype/issues/654)\n- Loader in Photoshop fails with \"can't set attribute\" [\\#650](https://github.com/pypeclub/pype/issues/650)\n- Nuke Load mp4 wrong frame range [\\#661](https://github.com/pypeclub/pype/issues/661)\n- Hiero: Review video file adding one frame to the end [\\#659](https://github.com/pypeclub/pype/issues/659)\n\n## [2.13.0](https://github.com/pypeclub/pype/tree/2.13.0) (2020-10-18)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.5...2.13.0)\n\n**Enhancements:**\n\n- Deadline Output Folder [\\#636](https://github.com/pypeclub/pype/issues/636)\n- Nuke Camera Loader [\\#565](https://github.com/pypeclub/pype/issues/565)\n- Deadline publish job shows publishing output folder [\\#649](https://github.com/pypeclub/pype/pull/649)\n- Get latest version in lib [\\#642](https://github.com/pypeclub/pype/pull/642)\n- Improved publishing of multiple representation from SP [\\#638](https://github.com/pypeclub/pype/pull/638)\n- Launch TvPaint shot work file from within Ftrack [\\#631](https://github.com/pypeclub/pype/pull/631)\n- Add mp4 support for RV action. [\\#628](https://github.com/pypeclub/pype/pull/628)\n- Maya: allow renders to have version synced with workfile [\\#618](https://github.com/pypeclub/pype/pull/618)\n- Renaming nukestudio host folder to hiero [\\#617](https://github.com/pypeclub/pype/pull/617)\n- Harmony: More efficient publishing [\\#615](https://github.com/pypeclub/pype/pull/615)\n- Ftrack server action improvement [\\#608](https://github.com/pypeclub/pype/pull/608)\n- Deadline user defaults to pype username if present [\\#607](https://github.com/pypeclub/pype/pull/607)\n- Standalone publisher now has icon [\\#606](https://github.com/pypeclub/pype/pull/606)\n- Nuke render write targeting knob improvement [\\#603](https://github.com/pypeclub/pype/pull/603)\n- Animated pyblish gui [\\#602](https://github.com/pypeclub/pype/pull/602)\n- Maya: Deadline - make use of asset dependencies optional [\\#591](https://github.com/pypeclub/pype/pull/591)\n- Nuke: Publishing, loading and updating alembic cameras [\\#575](https://github.com/pypeclub/pype/pull/575)\n- Maya: add look assigner to pype menu even if scriptsmenu is not available [\\#573](https://github.com/pypeclub/pype/pull/573)\n- Store task types in the database [\\#572](https://github.com/pypeclub/pype/pull/572)\n- Maya: Tiled EXRs to scanline EXRs render option [\\#512](https://github.com/pypeclub/pype/pull/512)\n- Fusion basic integration [\\#452](https://github.com/pypeclub/pype/pull/452)\n\n**Fixed bugs:**\n\n- Burnin script did not propagate ffmpeg output [\\#640](https://github.com/pypeclub/pype/issues/640)\n- Pyblish-pype spacer in terminal wasn't transparent [\\#646](https://github.com/pypeclub/pype/pull/646)\n- Lib subprocess without logger [\\#645](https://github.com/pypeclub/pype/pull/645)\n- Nuke: prevent crash if we only have single frame in sequence [\\#644](https://github.com/pypeclub/pype/pull/644)\n- Burnin script logs better output [\\#641](https://github.com/pypeclub/pype/pull/641)\n- Missing audio on farm submission. [\\#639](https://github.com/pypeclub/pype/pull/639)\n- review from imagesequence error [\\#633](https://github.com/pypeclub/pype/pull/633)\n- Hiero: wrong order of fps clip instance data collecting  [\\#627](https://github.com/pypeclub/pype/pull/627)\n- Add source for review instances. [\\#625](https://github.com/pypeclub/pype/pull/625)\n- Task processing in event sync [\\#623](https://github.com/pypeclub/pype/pull/623)\n- sync to avalon doesn t remove renamed task [\\#619](https://github.com/pypeclub/pype/pull/619)\n- Intent publish setting wasn't working with default value [\\#562](https://github.com/pypeclub/pype/pull/562)\n- Maya: Updating a look where the shader name changed, leaves the geo without a shader [\\#514](https://github.com/pypeclub/pype/pull/514)\n\n**Merged pull requests:**\n\n- Avalon module without Qt [\\#581](https://github.com/pypeclub/pype/pull/581)\n- Ftrack module without Qt [\\#577](https://github.com/pypeclub/pype/pull/577)\n\n## [2.12.5](https://github.com/pypeclub/pype/tree/2.12.5) (2020-10-14)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.4...2.12.5)\n\n**Enhancements:**\n\n- Launch TvPaint shot work file from within Ftrack [\\#629](https://github.com/pypeclub/pype/issues/629)\n\n**Merged pull requests:**\n\n- Harmony: Disable application launch logic [\\#637](https://github.com/pypeclub/pype/pull/637)\n\n## [2.12.4](https://github.com/pypeclub/pype/tree/2.12.4) (2020-10-08)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.3...2.12.4)\n\n**Enhancements:**\n\n- convert nukestudio to hiero host [\\#616](https://github.com/pypeclub/pype/issues/616)\n- Fusion basic integration  [\\#451](https://github.com/pypeclub/pype/issues/451)\n\n**Fixed bugs:**\n\n- Sync to avalon doesn't remove renamed task [\\#605](https://github.com/pypeclub/pype/issues/605)\n- NukeStudio: FPS collecting into clip instances [\\#624](https://github.com/pypeclub/pype/pull/624)\n\n**Merged pull requests:**\n\n- NukeStudio: small fixes [\\#622](https://github.com/pypeclub/pype/pull/622)\n- NukeStudio: broken order of plugins [\\#620](https://github.com/pypeclub/pype/pull/620)\n\n## [2.12.3](https://github.com/pypeclub/pype/tree/2.12.3) (2020-10-06)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.2...2.12.3)\n\n**Enhancements:**\n\n- Nuke Publish Camera [\\#567](https://github.com/pypeclub/pype/issues/567)\n- Harmony: open xstage file no matter of its name [\\#526](https://github.com/pypeclub/pype/issues/526)\n- Stop integration of unwanted data [\\#387](https://github.com/pypeclub/pype/issues/387)\n- Move avalon-launcher functionality to pype [\\#229](https://github.com/pypeclub/pype/issues/229)\n- avalon workfiles api [\\#214](https://github.com/pypeclub/pype/issues/214)\n- Store task types [\\#180](https://github.com/pypeclub/pype/issues/180)\n- Avalon Mongo Connection split [\\#136](https://github.com/pypeclub/pype/issues/136)\n- nk camera workflow [\\#71](https://github.com/pypeclub/pype/issues/71)\n- Hiero integration added [\\#590](https://github.com/pypeclub/pype/pull/590)\n- Anatomy instance data collection is substantially faster for many instances [\\#560](https://github.com/pypeclub/pype/pull/560)\n\n**Fixed bugs:**\n\n- test issue [\\#596](https://github.com/pypeclub/pype/issues/596)\n- Harmony: empty scene contamination [\\#583](https://github.com/pypeclub/pype/issues/583)\n- Edit publishing in SP doesn't respect shot selection for publishing [\\#542](https://github.com/pypeclub/pype/issues/542)\n- Pathlib breaks compatibility with python2 hosts [\\#281](https://github.com/pypeclub/pype/issues/281)\n- Updating a look where the shader name changed leaves the geo without a shader [\\#237](https://github.com/pypeclub/pype/issues/237)\n- Better error handling [\\#84](https://github.com/pypeclub/pype/issues/84)\n- Harmony: function signature [\\#609](https://github.com/pypeclub/pype/pull/609)\n- Nuke: gizmo publishing error [\\#594](https://github.com/pypeclub/pype/pull/594)\n- Harmony: fix clashing namespace of called js functions [\\#584](https://github.com/pypeclub/pype/pull/584)\n- Maya: fix maya scene type preset exception [\\#569](https://github.com/pypeclub/pype/pull/569)\n\n**Closed issues:**\n\n- Nuke Gizmo publishing [\\#597](https://github.com/pypeclub/pype/issues/597)\n- nuke gizmo publishing error [\\#592](https://github.com/pypeclub/pype/issues/592)\n- Publish EDL [\\#579](https://github.com/pypeclub/pype/issues/579)\n- Publish render from SP [\\#576](https://github.com/pypeclub/pype/issues/576)\n- rename ftrack custom attribute group to `pype` [\\#184](https://github.com/pypeclub/pype/issues/184)\n\n**Merged pull requests:**\n\n- Audio file existence check [\\#614](https://github.com/pypeclub/pype/pull/614)\n- NKS small fixes [\\#587](https://github.com/pypeclub/pype/pull/587)\n- Standalone publisher editorial plugins interfering [\\#580](https://github.com/pypeclub/pype/pull/580)\n\n## [2.12.2](https://github.com/pypeclub/pype/tree/2.12.2) (2020-09-25)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.1...2.12.2)\n\n**Enhancements:**\n\n- pype config GUI [\\#241](https://github.com/pypeclub/pype/issues/241)\n\n**Fixed bugs:**\n\n- Harmony: Saving heavy scenes will crash [\\#507](https://github.com/pypeclub/pype/issues/507)\n- Extract review a representation name with `\\*\\_burnin` [\\#388](https://github.com/pypeclub/pype/issues/388)\n- Hierarchy data was not considering active isntances [\\#551](https://github.com/pypeclub/pype/pull/551)\n\n## [2.12.1](https://github.com/pypeclub/pype/tree/2.12.1) (2020-09-15)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.0...2.12.1)\n\n**Fixed bugs:**\n\n- Pype: changelog.md is outdated [\\#503](https://github.com/pypeclub/pype/issues/503)\n- dependency security alert ! [\\#484](https://github.com/pypeclub/pype/issues/484)\n- Maya: RenderSetup is missing update [\\#106](https://github.com/pypeclub/pype/issues/106)\n- \\<pyblish plugin\\> extract effects creates new instance [\\#78](https://github.com/pypeclub/pype/issues/78)\n\n## [2.12.0](https://github.com/pypeclub/pype/tree/2.12.0) (2020-09-10)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.8...2.12.0)\n\n**Enhancements:**\n\n- Less mongo connections [\\#509](https://github.com/pypeclub/pype/pull/509)\n- Nuke: adding image loader  [\\#499](https://github.com/pypeclub/pype/pull/499)\n- Move launcher window to top if launcher action is clicked [\\#450](https://github.com/pypeclub/pype/pull/450)\n- Maya:  better tile rendering support in Pype [\\#446](https://github.com/pypeclub/pype/pull/446)\n- Implementation of non QML launcher [\\#443](https://github.com/pypeclub/pype/pull/443)\n- Optional skip review on renders. [\\#441](https://github.com/pypeclub/pype/pull/441)\n- Ftrack: Option to push status from task to latest version [\\#440](https://github.com/pypeclub/pype/pull/440)\n- Properly containerize image plane loads. [\\#434](https://github.com/pypeclub/pype/pull/434)\n- Option to keep the review files. [\\#426](https://github.com/pypeclub/pype/pull/426)\n- Isolate view on instance members. [\\#425](https://github.com/pypeclub/pype/pull/425)\n- Maya: Publishing of tile renderings on Deadline [\\#398](https://github.com/pypeclub/pype/pull/398)\n- Feature/little bit better logging gui [\\#383](https://github.com/pypeclub/pype/pull/383)\n\n**Fixed bugs:**\n\n- Maya: Fix tile order for Draft Tile Assembler [\\#511](https://github.com/pypeclub/pype/pull/511)\n- Remove extra dash [\\#501](https://github.com/pypeclub/pype/pull/501)\n- Fix: strip dot from repre names in single frame renders [\\#498](https://github.com/pypeclub/pype/pull/498)\n- Better handling of destination during integrating [\\#485](https://github.com/pypeclub/pype/pull/485)\n- Fix: allow thumbnail creation for single frame renders [\\#460](https://github.com/pypeclub/pype/pull/460)\n- added missing argument to launch\\_application in ftrack app handler [\\#453](https://github.com/pypeclub/pype/pull/453)\n- Burnins: Copy bit rate of input video to match quality. [\\#448](https://github.com/pypeclub/pype/pull/448)\n- Standalone publisher is now independent from tray [\\#442](https://github.com/pypeclub/pype/pull/442)\n- Bugfix/empty enumerator attributes [\\#436](https://github.com/pypeclub/pype/pull/436)\n- Fixed wrong order of \"other\" category collapssing in publisher [\\#435](https://github.com/pypeclub/pype/pull/435)\n- Multiple reviews where being overwritten to one. [\\#424](https://github.com/pypeclub/pype/pull/424)\n- Cleanup plugin fail on instances without staging dir [\\#420](https://github.com/pypeclub/pype/pull/420)\n- deprecated -intra parameter in ffmpeg to new `-g` [\\#417](https://github.com/pypeclub/pype/pull/417)\n- Delivery action can now work with entered path [\\#397](https://github.com/pypeclub/pype/pull/397)\n\n**Merged pull requests:**\n\n- Review on instance.data  [\\#473](https://github.com/pypeclub/pype/pull/473)\n\n## [2.11.8](https://github.com/pypeclub/pype/tree/2.11.8) (2020-08-27)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.7...2.11.8)\n\n**Enhancements:**\n\n- DWAA support for Maya [\\#382](https://github.com/pypeclub/pype/issues/382)\n- Isolate View on Playblast [\\#367](https://github.com/pypeclub/pype/issues/367)\n- Maya: Tile rendering [\\#297](https://github.com/pypeclub/pype/issues/297)\n- single pype instance running [\\#47](https://github.com/pypeclub/pype/issues/47)\n- PYPE-649: projects don't guarantee backwards compatible environment [\\#8](https://github.com/pypeclub/pype/issues/8)\n- PYPE-663: separate venv for each deployed version [\\#7](https://github.com/pypeclub/pype/issues/7)\n\n**Fixed bugs:**\n\n- pyblish pype - other group is collapsed before plugins are done [\\#431](https://github.com/pypeclub/pype/issues/431)\n- Alpha white edges in harmony on PNGs [\\#412](https://github.com/pypeclub/pype/issues/412)\n- harmony image loader picks wrong representations [\\#404](https://github.com/pypeclub/pype/issues/404)\n- Clockify crash when response contain symbol not allowed by UTF-8 [\\#81](https://github.com/pypeclub/pype/issues/81)\n\n## [2.11.7](https://github.com/pypeclub/pype/tree/2.11.7) (2020-08-21)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.6...2.11.7)\n\n**Fixed bugs:**\n\n- Clean Up Baked Movie [\\#369](https://github.com/pypeclub/pype/issues/369)\n- celaction last workfile [\\#459](https://github.com/pypeclub/pype/pull/459)\n\n## [2.11.6](https://github.com/pypeclub/pype/tree/2.11.6) (2020-08-18)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.5...2.11.6)\n\n**Enhancements:**\n\n- publisher app [\\#56](https://github.com/pypeclub/pype/issues/56)\n\n## [2.11.5](https://github.com/pypeclub/pype/tree/2.11.5) (2020-08-13)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.4...2.11.5)\n\n**Enhancements:**\n\n- Switch from master to equivalent [\\#220](https://github.com/pypeclub/pype/issues/220)\n- Standalone publisher now only groups sequence if the extension is known [\\#439](https://github.com/pypeclub/pype/pull/439)\n\n**Fixed bugs:**\n\n- Logs have been disable for editorial by default to speed up publishing [\\#433](https://github.com/pypeclub/pype/pull/433)\n- additional fixes for celaction [\\#430](https://github.com/pypeclub/pype/pull/430)\n- Harmony: invalid variable scope in validate scene settings [\\#428](https://github.com/pypeclub/pype/pull/428)\n- new representation name for audio was not accepted [\\#427](https://github.com/pypeclub/pype/pull/427)\n\n## [2.11.4](https://github.com/pypeclub/pype/tree/2.11.4) (2020-08-10)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.3...2.11.4)\n\n**Enhancements:**\n\n- WebSocket server [\\#135](https://github.com/pypeclub/pype/issues/135)\n- standalonepublisher: editorial family features expansion \\[master branch\\] [\\#411](https://github.com/pypeclub/pype/pull/411)\n\n## [2.11.3](https://github.com/pypeclub/pype/tree/2.11.3) (2020-08-04)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.2...2.11.3)\n\n**Fixed bugs:**\n\n- Harmony: publishing performance issues [\\#408](https://github.com/pypeclub/pype/pull/408)\n\n## [2.11.2](https://github.com/pypeclub/pype/tree/2.11.2) (2020-07-31)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.1...2.11.2)\n\n**Fixed bugs:**\n\n- Ftrack to Avalon bug [\\#406](https://github.com/pypeclub/pype/issues/406)\n\n## [2.11.1](https://github.com/pypeclub/pype/tree/2.11.1) (2020-07-29)\n\n[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.0...2.11.1)\n\n**Merged pull requests:**\n\n- Celaction: metadata json folder fixes on path  [\\#393](https://github.com/pypeclub/pype/pull/393)\n- CelAction - version up method taken fro pype.lib  [\\#391](https://github.com/pypeclub/pype/pull/391)\n\n<a name=\"2.11.0\"></a>\n## 2.11.0 ##\n\n_**release date:** 27 July 2020_\n\n**new:**\n- _(blender)_ namespace support [\\#341](https://github.com/pypeclub/pype/pull/341)\n- _(blender)_ start end frames [\\#330](https://github.com/pypeclub/pype/pull/330)\n- _(blender)_ camera asset [\\#322](https://github.com/pypeclub/pype/pull/322)\n- _(pype)_ toggle instances per family in pyblish GUI [\\#320](https://github.com/pypeclub/pype/pull/320)\n- _(pype)_ current release version is now shown in the tray menu [#379](https://github.com/pypeclub/pype/pull/379)\n\n\n**improved:**\n- _(resolve)_ tagging for publish [\\#239](https://github.com/pypeclub/pype/issues/239)\n- _(pype)_ Support publishing a subset of shots with standalone editorial [\\#336](https://github.com/pypeclub/pype/pull/336)\n- _(harmony)_ Basic support for palettes [\\#324](https://github.com/pypeclub/pype/pull/324)\n- _(photoshop)_ Flag outdated containers on startup and publish. [\\#309](https://github.com/pypeclub/pype/pull/309)\n- _(harmony)_ Flag Outdated containers [\\#302](https://github.com/pypeclub/pype/pull/302)\n- _(photoshop)_ Publish review [\\#298](https://github.com/pypeclub/pype/pull/298)\n- _(pype)_ Optional Last workfile launch [\\#365](https://github.com/pypeclub/pype/pull/365)\n\n\n**fixed:**\n- _(premiere)_ workflow fixes [\\#346](https://github.com/pypeclub/pype/pull/346)\n- _(pype)_ pype-setup does not work with space in path [\\#327](https://github.com/pypeclub/pype/issues/327)\n- _(ftrack)_ Ftrack delete action cause circular error [\\#206](https://github.com/pypeclub/pype/issues/206)\n- _(nuke)_ Priority was forced to 50 [\\#345](https://github.com/pypeclub/pype/pull/345)\n- _(nuke)_ Fix ValidateNukeWriteKnobs [\\#340](https://github.com/pypeclub/pype/pull/340)\n- _(maya)_ If camera attributes are connected, we can ignore them. [\\#339](https://github.com/pypeclub/pype/pull/339)\n- _(pype)_ stop appending of tools environment to existing env [\\#337](https://github.com/pypeclub/pype/pull/337)\n- _(ftrack)_ Ftrack timeout needs to look at AVALON\\_TIMEOUT [\\#325](https://github.com/pypeclub/pype/pull/325)\n- _(harmony)_ Only zip files are supported. [\\#310](https://github.com/pypeclub/pype/pull/310)\n- _(pype)_ hotfix/Fix event server mongo uri [\\#305](https://github.com/pypeclub/pype/pull/305)\n- _(photoshop)_ Subset was not named or validated correctly. [\\#304](https://github.com/pypeclub/pype/pull/304)\n\n\n\n<a name=\"2.10.0\"></a>\n## 2.10.0 ##\n\n_**release date:** 17 June 2020_\n\n**new:**\n- _(harmony)_ **Toon Boom Harmony** has been greatly extended to support rigging, scene build, animation and rendering workflows. [#270](https://github.com/pypeclub/pype/issues/270) [#271](https://github.com/pypeclub/pype/issues/271) [#190](https://github.com/pypeclub/pype/issues/190) [#191](https://github.com/pypeclub/pype/issues/191) [#172](https://github.com/pypeclub/pype/issues/172) [#168](https://github.com/pypeclub/pype/issues/168)\n- _(pype)_ Added support for rudimentary **edl publishing** into individual shots. [#265](https://github.com/pypeclub/pype/issues/265)\n- _(celaction)_ Simple **Celaction** integration has been added with support for workfiles and rendering. [#255](https://github.com/pypeclub/pype/issues/255)\n- _(maya)_ Support for multiple job types when submitting to the farm. We can now render Maya or Standalone render jobs for Vray and Arnold (limited support for arnold) [#204](https://github.com/pypeclub/pype/issues/204)\n- _(photoshop)_ Added initial support for Photoshop [#232](https://github.com/pypeclub/pype/issues/232)\n\n**improved:**\n- _(blender)_ Updated support for rigs and added support Layout family [#233](https://github.com/pypeclub/pype/issues/233) [#226](https://github.com/pypeclub/pype/issues/226)\n- _(premiere)_ It is now possible to choose different storage root for workfiles of different task types. [#255](https://github.com/pypeclub/pype/issues/255)\n- _(maya)_ Support for unmerged AOVs in Redshift multipart EXRs [#197](https://github.com/pypeclub/pype/issues/197)\n- _(pype)_ Pype repository has been refactored in preparation for 3.0 release [#169](https://github.com/pypeclub/pype/issues/169)\n- _(deadline)_ All file dependencies are now passed to deadline from maya to prevent premature start of rendering if caches or textures haven't been coppied over yet. [#195](https://github.com/pypeclub/pype/issues/195)\n- _(nuke)_ Script validation can now be made optional. [#194](https://github.com/pypeclub/pype/issues/194)\n- _(pype)_ Publishing can now be stopped at any time. [#194](https://github.com/pypeclub/pype/issues/194)\n\n**fix:**\n- _(pype)_ Pyblish-lite has been integrated into pype repository, plus various publishing GUI fixes. [#274](https://github.com/pypeclub/pype/issues/274) [#275](https://github.com/pypeclub/pype/issues/275) [#268](https://github.com/pypeclub/pype/issues/268) [#227](https://github.com/pypeclub/pype/issues/227) [#238](https://github.com/pypeclub/pype/issues/238)\n- _(maya)_ Alembic extractor was getting wrong frame range type in certain scenarios [#254](https://github.com/pypeclub/pype/issues/254)\n- _(maya)_ Attaching a render to subset in maya was not passing validation in certain scenarios  [#256](https://github.com/pypeclub/pype/issues/256)\n- _(ftrack)_ Various small fixes to ftrack sync [#263](https://github.com/pypeclub/pype/issues/263) [#259](https://github.com/pypeclub/pype/issues/259)\n- _(maya)_ Look extraction is now able to skp invalid connections in shaders [#207](https://github.com/pypeclub/pype/issues/207)\n\n\n\n<a name=\"2.9.0\"></a>\n## 2.9.0 ##\n\n_**release date:** 25 May 2020_\n\n**new:**\n- _(pype)_ Support for **Multiroot projects**. You can now store project data on multiple physical or virtual storages and target individual publishes to these locations. For instance render can be stored on a faster storage than the rest of the project. [#145](https://github.com/pypeclub/pype/issues/145), [#38](https://github.com/pypeclub/pype/issues/38)\n- _(harmony)_ Basic implementation of **Toon Boom Harmony** has been added. [#142](https://github.com/pypeclub/pype/issues/142)\n- _(pype)_ OSX support is in public beta now. There are issues to be expected, but the main implementation should be functional. [#141](https://github.com/pypeclub/pype/issues/141)\n\n\n**improved:**\n\n- _(pype)_ **Review extractor** has been completely rebuilt. It now supports granular filtering so you can create **multiple outputs** for different tasks, families or hosts. [#103](https://github.com/pypeclub/pype/issues/103), [#166](https://github.com/pypeclub/pype/issues/166), [#165](https://github.com/pypeclub/pype/issues/165)\n- _(pype)_ **Burnin** generation had been extended to **support same multi-output filtering** as review extractor [#103](https://github.com/pypeclub/pype/issues/103)\n- _(pype)_ Publishing file templates can now be specified in config for each individual family [#114](https://github.com/pypeclub/pype/issues/114)\n- _(pype)_ Studio specific plugins can now be appended to pype standard publishing plugins. [#112](https://github.com/pypeclub/pype/issues/112)\n- _(nukestudio)_ Reviewable clips no longer need to be previously cut, exported and re-imported to timeline. **Pype can now dynamically cut reviewable quicktimes** from continuous offline footage during publishing. [#23](https://github.com/pypeclub/pype/issues/23)\n- _(deadline)_ Deadline can now correctly differentiate between staging and production pype. [#154](https://github.com/pypeclub/pype/issues/154)\n- _(deadline)_ `PYPE_PYTHON_EXE` env variable can now be used to direct publishing to explicit python installation. [#120](https://github.com/pypeclub/pype/issues/120)\n- _(nuke)_ Nuke now check for new version of loaded data on file open. [#140](https://github.com/pypeclub/pype/issues/140)\n- _(nuke)_ frame range and limit checkboxes are now exposed on write node. [#119](https://github.com/pypeclub/pype/issues/119)\n\n\n\n**fix:**\n\n- _(nukestudio)_ Project Location was using backslashes which was breaking nukestudio native exporting in certains configurations [#82](https://github.com/pypeclub/pype/issues/82)\n- _(nukestudio)_ Duplicity in hierarchy tags was prone to throwing publishing error [#130](https://github.com/pypeclub/pype/issues/130), [#144](https://github.com/pypeclub/pype/issues/144)\n- _(ftrack)_ multiple stability improvements [#157](https://github.com/pypeclub/pype/issues/157), [#159](https://github.com/pypeclub/pype/issues/159), [#128](https://github.com/pypeclub/pype/issues/128), [#118](https://github.com/pypeclub/pype/issues/118), [#127](https://github.com/pypeclub/pype/issues/127)\n- _(deadline)_ multipart EXRs were stopping review publishing on the farm. They are still not supported for automatic review generation, but the publish will go through correctly without the quicktime. [#155](https://github.com/pypeclub/pype/issues/155)\n- _(deadline)_ If deadline is non-responsive it will no longer freeze host when publishing [#149](https://github.com/pypeclub/pype/issues/149)\n- _(deadline)_ Sometimes deadline was trying to launch render before all the source data was coppied over. [#137](https://github.com/pypeclub/pype/issues/137) _(harmony)_ Basic implementation of **Toon Boom Harmony** has been added. [#142](https://github.com/pypeclub/pype/issues/142)\n- _(nuke)_ Filepath knob wasn't updated properly. [#131](https://github.com/pypeclub/pype/issues/131)\n- _(maya)_ When extracting animation, the \"Write Color Set\" options on the instance were not respected. [#108](https://github.com/pypeclub/pype/issues/108)\n- _(maya)_ Attribute overrides for AOV only worked for the legacy render layers. Now it works for new render setup as well [#132](https://github.com/pypeclub/pype/issues/132)\n- _(maya)_ Stability and usability improvements in yeti workflow [#104](https://github.com/pypeclub/pype/issues/104)\n\n\n\n<a name=\"2.8.0\"></a>\n## 2.8.0 ##\n\n_**release date:** 20 April 2020_\n\n**new:**\n\n- _(pype)_ Option to generate slates from json templates. [PYPE-628] [#26](https://github.com/pypeclub/pype/issues/26)\n- _(pype)_ It is now possible to automate loading of published subsets into any scene. Documentation will follow :). [PYPE-611] [#24](https://github.com/pypeclub/pype/issues/24)\n\n**fix:**\n\n- _(maya)_ Some Redshift render tokens could break publishing. [PYPE-778] [#33](https://github.com/pypeclub/pype/issues/33)\n- _(maya)_ Publish was not preserving maya file extension. [#39](https://github.com/pypeclub/pype/issues/39)\n- _(maya)_ Rig output validator was failing on nodes without shapes. [#40](https://github.com/pypeclub/pype/issues/40)\n- _(maya)_ Yeti caches can now be properly versioned up in the scene inventory. [#40](https://github.com/pypeclub/pype/issues/40)\n- _(nuke)_ Build first workfiles was not accepting jpeg sequences. [#34](https://github.com/pypeclub/pype/issues/34)\n- _(deadline)_ Trying to generate ffmpeg review from multipart EXRs no longer crashes publishing. [PYPE-781]\n- _(deadline)_ Render publishing is more stable in multiplatform environments. [PYPE-775]\n\n\n\n<a name=\"2.7.0\"></a>\n## 2.7.0 ##\n\n_**release date:** 30 March 2020_\n\n**new:**\n\n- _(maya)_ Artist can now choose to load multiple references of the same subset at once [PYPE-646, PYPS-81]\n- _(nuke)_ Option to use named OCIO colorspaces for review colour baking. [PYPS-82]\n- _(pype)_ Pype can now work with `master` versions for publishing and loading. These are non-versioned publishes that are overwritten with the latest version during publish. These are now supported in all the GUIs, but their publishing is deactivated by default. [PYPE-653]\n- _(blender)_ Added support for basic blender workflow. We currently support `rig`, `model` and `animation` families. [PYPE-768]\n- _(pype)_ Source timecode can now be used in burn-ins. [PYPE-777]\n- _(pype)_ Review outputs profiles can now specify delivery resolution different than project setting [PYPE-759]\n- _(nuke)_ Bookmark to current context is now added automatically to all nuke browser windows. [PYPE-712]\n\n**change:**\n\n- _(maya)_ It is now possible to publish camera without. baking. Keep in mind that unbaked cameras can't be guaranteed to work in other hosts. [PYPE-595]\n- _(maya)_ All the renders from maya are now grouped in the loader by their Layer name. [PYPE-482]\n- _(nuke/hiero)_ Any publishes from nuke and hiero can now be versioned independently of the workfile. [PYPE-728]\n\n\n**fix:**\n\n- _(nuke)_ Mixed slashes caused issues in ocio config path.\n- _(pype)_ Intent field in pyblish GUI was passing label instead of value to ftrack. [PYPE-733]\n- _(nuke)_ Publishing of pre-renders was inconsistent. [PYPE-766]\n- _(maya)_ Handles and frame ranges were inconsistent in various places during publishing.\n- _(nuke)_ Nuke was crashing if it ran into certain missing knobs. For example DPX output missing `autocrop` [PYPE-774]\n- _(deadline)_ Project overrides were not working properly with farm render publishing.\n- _(hiero)_ Problems with single frame plates publishing.\n- _(maya)_ Redshift RenderPass token were breaking render publishing. [PYPE-778]\n- _(nuke)_ Build first workfile was not accepting jpeg sequences.\n- _(maya)_ Multipart (Multilayer) EXRs were breaking review publishing due to FFMPEG incompatiblity [PYPE-781]\n\n\n<a name=\"2.6.0\"></a>\n## 2.6.0 ##\n\n_**release date:** 9 March 2020_\n\n**change:**\n- _(maya)_ render publishing has been simplified and made more robust. Render setup layers are now automatically added to publishing subsets and `render globals` family has been replaced with simple `render` [PYPE-570]\n- _(avalon)_ change context and workfiles apps, have been merged into one, that allows both actions to be performed at the same time. [PYPE-747]\n- _(pype)_ thumbnails are now automatically propagate to asset from the last published subset in the loader\n- _(ftrack)_ publishing comment and intent are now being published to ftrack note as well as describtion. [PYPE-727]\n- _(pype)_ when overriding existing version new old representations are now overriden, instead of the new ones just being appended. (to allow this behaviour, the version validator need to be disabled. [PYPE-690])\n- _(pype)_ burnin preset has been significantly simplified. It now doesn't require passing function to each field, but only need the actual text template. to use this, all the current burnin PRESETS MUST BE UPDATED for all the projects.\n- _(ftrack)_ credentials are now stored on a per server basis, so it's possible to switch between ftrack servers without having to log in and out. [PYPE-723]\n\n\n**new:**\n- _(pype)_ production and development deployments now have different colour of the tray icon. Orange for Dev and Green for production [PYPE-718]\n- _(maya)_ renders can now be attached to a publishable subset rather than creating their own subset. For example it is possible to create a reviewable `look` or `model` render and have it correctly attached as a representation of the subsets [PYPE-451]\n- _(maya)_ after saving current scene into a new context (as a new shot for instance), all the scene publishing subsets data gets re-generated automatically to match the new context [PYPE-532]\n- _(pype)_ we now support project specific publish, load and create plugins [PYPE-740]\n- _(ftrack)_ new action that allow archiving/deleting old published versions. User can keep how many of the latest version to keep when the action is ran. [PYPE-748, PYPE-715]\n- _(ftrack)_ it is now possible to monitor and restart ftrack event server using ftrack action. [PYPE-658]\n- _(pype)_ validator that prevent accidental overwrites of previously published versions. [PYPE-680]\n- _(avalon)_ avalon core updated to version 5.6.0\n- _(maya)_ added validator to make sure that relative paths are used when publishing arnold standins.\n- _(nukestudio)_ it is now possible to extract and publish audio family from clip in nuke studio [PYPE-682]\n\n**fix**:\n- _(maya)_ maya set framerange button was ignoring handles [PYPE-719]\n- _(ftrack)_ sync to avalon was sometime crashing when ran on empty project\n- _(nukestudio)_ publishing same shots after they've been previously archived/deleted would result in a crash. [PYPE-737]\n- _(nuke)_ slate workflow was breaking in certain scenarios. [PYPE-730]\n- _(pype)_ rendering publish workflow has been significantly improved to prevent error resulting from implicit render collection. [PYPE-665, PYPE-746]\n- _(pype)_ launching application on a non-synced project resulted in obscure [PYPE-528]\n- _(pype)_ missing keys in burnins no longer result in an error. [PYPE-706]\n- _(ftrack)_ create folder structure action was sometimes failing for project managers due to wrong permissions.\n- _(Nukestudio)_ using `source` in the start frame tag could result in wrong frame range calculation\n- _(ftrack)_ sync to avalon action and event have been improved by catching more edge cases and provessing them properly.\n\n\n<a name=\"2.5\"></a>\n## 2.5.0 ##\n\n_**release date:** 11 Feb 2020_\n\n**change:**\n- _(pype)_ added many logs for easier debugging\n- _(pype)_ review presets can now be separated between 2d and 3d renders [PYPE-693]\n- _(pype)_ anatomy module has been greatly improved to allow for more dynamic pulblishing and faster debugging [PYPE-685]\n- _(pype)_ avalon schemas have been moved from `pype-config` to `pype` repository, for simplification. [PYPE-670]\n- _(ftrack)_ updated to latest ftrack API\n- _(ftrack)_ publishing comments now appear in ftrack also as a note on version with customisable category [PYPE-645]\n- _(ftrack)_ delete asset/subset action had been improved. It is now able to remove multiple entities and descendants of the selected entities [PYPE-361, PYPS-72]\n- _(workfiles)_ added date field to workfiles app [PYPE-603]\n- _(maya)_ old deprecated loader have been removed in favour of a single unified reference loader (old scenes will upgrade automatically to the new loader upon opening) [PYPE-633, PYPE-697]\n- _(avalon)_ core updated to 5.5.15 [PYPE-671]\n- _(nuke)_ library loader is now available in nuke [PYPE-698]\n\n\n**new:**\n- _(pype)_ added pype render wrapper to allow rendering on mixed platform farms. [PYPE-634]\n- _(pype)_ added `pype launch` command. It let's admin run applications with dynamically built environment based on the given context. [PYPE-634]\n- _(pype)_ added support for extracting review sequences with burnins [PYPE-657]\n- _(publish)_ users can now set intent next to a comment when publishing. This will then be reflected on an attribute in ftrack. [PYPE-632]\n- _(burnin)_ timecode can now be added to burnin\n- _(burnin)_ datetime keys can now be added to burnin and anatomy [PYPE-651]\n- _(burnin)_ anatomy templates can now be used in burnins. [PYPE=626]\n- _(nuke)_ new validator for render resolution\n- _(nuke)_ support for attach slate to nuke renders [PYPE-630]\n- _(nuke)_ png sequences were added to loaders\n- _(maya)_ added maya 2020 compatibility [PYPE-677]\n- _(maya)_ ability to publish and load .ASS standin sequences [PYPS-54]\n- _(pype)_ thumbnails can now be published and are visible in the loader. `AVALON_THUMBNAIL_ROOT` environment variable needs to be set for this to work  [PYPE-573, PYPE-132]\n- _(blender)_ base implementation of blender was added with publishing and loading of .blend files [PYPE-612]\n- _(ftrack)_ new action for preparing deliveries [PYPE-639]\n\n\n**fix**:\n- _(burnin)_ more robust way of finding ffmpeg for burnins.\n- _(pype)_ improved UNC paths remapping when sending to farm.\n- _(pype)_ float frames sometimes made their way to representation context in database, breaking loaders [PYPE-668]\n- _(pype)_ `pype install --force` was failing sometimes [PYPE-600]\n- _(pype)_ padding in published files got calculated wrongly sometimes. It is now instead being always read from project anatomy. [PYPE-667]\n- _(publish)_ comment publishing was failing in certain situations\n- _(ftrack)_ multiple edge case scenario fixes in auto sync and sync-to-avalon action\n- _(ftrack)_ sync to avalon now works on empty projects\n- _(ftrack)_ thumbnail update event was failing when deleting entities [PYPE-561]\n- _(nuke)_ loader applies proper colorspaces from Presets\n- _(nuke)_ publishing handles didn't always work correctly [PYPE-686]\n- _(maya)_ assembly publishing and loading wasn't working correctly\n\n\n\n\n<a name=\"2.4.0\"></a>\n## 2.4.0 ##\n\n_**release date:** 9 Dec 2019_\n\n**change:**\n- _(ftrack)_ version to status ftrack event can now be configured from Presets\n  - based on preset `presets/ftracc/ftrack_config.json[\"status_version_to_task\"]`\n- _(ftrack)_ sync to avalon event has been completely re-written. It now supports most of the project management situations on ftrack including moving, renaming and deleting entities, updating attributes and working with tasks.\n- _(ftrack)_ sync to avalon action has been also re-writen. It is now much faster (up to 100 times depending on a project structure), has much better logging and reporting on encountered problems, and is able to handle much more complex situations.\n- _(ftrack)_ sync to avalon trigger by checking `auto-sync` toggle on ftrack [PYPE-504]\n- _(pype)_ various new features in the REST api\n- _(pype)_ new visual identity used across pype\n- _(pype)_ started moving all requirements to pip installation rather than vendorising them in pype repository. Due to a few yet unreleased packages, this means that pype can temporarily be only installed in the offline mode.\n\n**new:**\n- _(nuke)_ support for publishing gizmos and loading them as viewer processes\n- _(nuke)_ support for publishing nuke nodes from backdrops and loading them back\n- _(pype)_ burnins can now work with start and end frames as keys\n  - use keys `{frame_start}`, `{frame_end}` and `{current_frame}` in burnin preset to use them. [PYPS-44,PYPS-73, PYPE-602]\n- _(pype)_ option to filter logs by user and level in loggin GUI\n- _(pype)_ image family added to standalone publisher [PYPE-574]\n- _(pype)_ matchmove family added to standalone publisher [PYPE-574]\n- _(nuke)_ validator for comparing arbitrary knobs with values from presets\n- _(maya)_ option to force maya to copy textures in the new look publish rather than hardlinking them\n- _(pype)_ comments from pyblish GUI are now being added to ftrack version\n- _(maya)_ validator for checking outdated containers in the scene\n- _(maya)_ option to publish and load arnold standin sequence [PYPE-579, PYPS-54]\n\n**fix**:\n- _(pype)_ burnins were not respecting codec of the input video\n- _(nuke)_ lot's of various nuke and nuke studio fixes across the board [PYPS-45]\n- _(pype)_ workfiles app is not launching with the start of the app by default [PYPE-569]\n- _(ftrack)_ ftrack integration during publishing was failing under certain situations [PYPS-66]\n- _(pype)_ minor fixes in REST api\n- _(ftrack)_ status change event was crashing when the target status was missing [PYPS-68]\n- _(ftrack)_ actions will try to reconnect if they fail for some reason\n- _(maya)_ problems with fps mapping when using float FPS values\n- _(deadline)_ overall improvements to deadline publishing\n- _(setup)_ environment variables are now remapped on the fly based on the platform pype is running on. This fixes many issues in mixed platform environments.\n\n\n<a name=\"2.3.6\"></a>\n## 2.3.6 #\n\n_**release date:** 27 Nov 2019_\n\n**hotfix**:\n- _(ftrack)_ was hiding important debug logo\n- _(nuke)_ crashes during workfile publishing\n- _(ftrack)_ event server crashes because of signal problems\n- _(muster)_ problems with muster render submissions\n- _(ftrack)_ thumbnail update event syntax errors\n\n\n## 2.3.0 ##\n_release date: 6 Oct 2019_\n\n**new**:\n- _(maya)_ support for yeti rigs and yeti caches\n- _(maya)_ validator for comparing arbitrary attributes against ftrack\n- _(pype)_ burnins can now show current date and time\n- _(muster)_ pools can now be set in render globals in maya\n- _(pype)_ Rest API has been implemented in beta stage\n- _(nuke)_ LUT loader has been added\n- _(pype)_ rudimentary user module has been added as preparation for user management\n- _(pype)_ a simple logging GUI has been added to pype tray\n- _(nuke)_ nuke can now bake input process into mov\n- _(maya)_ imported models now have selection handle displayed by defaulting\n- _(avalon)_ it's is now possible to load multiple assets at once using loader\n- _(maya)_ added ability to automatically connect yeti rig to a mesh upon loading\n\n**changed**:\n- _(ftrack)_ event server now runs two parallel processes and is able to keep queue of events to process.\n- _(nuke)_ task name is now added to all rendered subsets\n- _(pype)_ adding more families to standalone publisher\n- _(pype)_ standalone publisher now uses pyblish-lite\n- _(pype)_ standalone publisher can now create review quicktimes\n- _(ftrack)_ queries to ftrack were sped up\n- _(ftrack)_ multiple ftrack action have been deprecated\n- _(avalon)_ avalon upstream has been updated to 5.5.0\n- _(nukestudio)_ published transforms can now be animated\n-\n\n**fix**:\n- _(maya)_ fps popup button didn't work in some cases\n- _(maya)_ geometry instances and references in maya were losing shader assignments\n- _(muster)_ muster rendering templates were not working correctly\n- _(maya)_ arnold tx texture conversion wasn't respecting colorspace set by the artist\n- _(pype)_ problems with avalon db sync\n- _(maya)_ ftrack was rounding FPS making it inconsistent\n- _(pype)_ wrong icon names in Creator\n- _(maya)_ scene inventory wasn't showing anything if representation was removed from database after it's been loaded to the scene\n- _(nukestudio)_ multiple bugs squashed\n- _(loader)_ loader was taking long time to show all the loading action when first launcher in maya\n\n## 2.2.0 ##\n_release date: 8 Sept 2019_\n\n**new**:\n- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts\n- _(nuke)_ option to choose deadline chunk size on write nodes\n- _(nukestudio)_ added option to publish soft effects (subTrackItems) from NukeStudio as subsets including LUT files. these can then be loaded in nuke or NukeStudio\n- _(nuke)_ option to build nuke script from previously published latest versions of plate and render subsets.\n- _(nuke)_ nuke writes now have deadline tab.\n- _(ftrack)_ Prepare Project action can now be used for creating the base folder structure on disk and in ftrack, setting up all the initial project attributes and it automatically prepares `pype_project_config` folder for the given project.\n- _(clockify)_ Added support for time tracking in clockify. This currently in addition to ftrack time logs, but does not completely replace them.\n- _(pype)_ any attributes in Creator and Loader plugins can now be customised using pype preset system\n\n**changed**:\n- nukestudio now uses workio API for workfiles\n- _(maya)_ \"FIX FPS\" prompt in maya now appears in the middle of the screen\n- _(muster)_ can now be configured with custom templates\n- _(pype)_ global publishing plugins can now be configured using presets as well as host specific ones\n\n\n**fix**:\n- wrong version retrieval from path in certain scenarios\n- nuke reset resolution wasn't working in certain scenarios\n\n## 2.1.0 ##\n_release date: 6 Aug 2019_\n\nA large cleanup release. Most of the change are under the hood.\n\n**new**:\n- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts\n- _(pype)_ Added configurable option to add burnins to any generated quicktimes\n- _(ftrack)_ Action that identifies what machines pype is running on.\n- _(system)_ unify subprocess calls\n- _(maya)_ add audio to review quicktimes\n- _(nuke)_ add crop before write node to prevent overscan problems in ffmpeg\n- **Nuke Studio** publishing and workfiles support\n- **Muster** render manager support\n- _(nuke)_ Framerange, FPS and Resolution are set automatically at startup\n- _(maya)_ Ability to load published sequences as image planes\n- _(system)_ Ftrack event that sets asset folder permissions based on task assignees in ftrack.\n- _(maya)_ Pyblish plugin that allow validation of maya attributes\n- _(system)_ added better startup logging to tray debug, including basic connection information\n- _(avalon)_ option to group published subsets to groups in the loader\n- _(avalon)_ loader family filters are working now\n\n**changed**:\n- change multiple key attributes to unify their behaviour across the pipeline\n  - `frameRate` to `fps`\n  - `startFrame` to `frameStart`\n  - `endFrame` to `frameEnd`\n  - `fstart` to `frameStart`\n  - `fend` to `frameEnd`\n  - `handle_start` to `handleStart`\n  - `handle_end` to `handleEnd`\n  - `resolution_width` to `resolutionWidth`\n  - `resolution_height` to `resolutionHeight`\n  - `pixel_aspect` to `pixelAspect`\n\n- _(nuke)_ write nodes are now created inside group with only some attributes editable by the artist\n- rendered frames are now deleted from temporary location after their publishing is finished.\n- _(ftrack)_ RV action can now be launched from any entity\n- after publishing only refresh button is now available in pyblish UI\n- added context instance pyblish-lite so that artist knows if context plugin fails\n- _(avalon)_ allow opening selected files using enter key\n- _(avalon)_ core updated to v5.2.9 with our forked changes on top\n\n**fix**:\n- faster hierarchy retrieval from db\n- _(nuke)_ A lot of stability enhancements\n- _(nuke studio)_ A lot of stability enhancements\n- _(nuke)_ now only renders a single write node on farm\n- _(ftrack)_ pype would crash when launcher project level task\n- work directory was sometimes not being created correctly\n- major pype.lib cleanup. Removing of unused functions, merging those that were doing the same and general house cleaning.\n- _(avalon)_ subsets in maya 2019 weren't behaving correctly in the outliner\n\n\n\\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Orbi Tools s.r.o.\n\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "\n<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->\n[![All Contributors](https://img.shields.io/badge/all_contributors-28-orange.svg?style=flat-square)](#contributors-)\n<!-- ALL-CONTRIBUTORS-BADGE:END -->\nOpenPype\n========\n\n[![documentation](https://github.com/pypeclub/pype/actions/workflows/documentation.yml/badge.svg)](https://github.com/pypeclub/pype/actions/workflows/documentation.yml) ![GitHub VFX Platform](https://img.shields.io/badge/vfx%20platform-2022-lightgrey?labelColor=303846)\n\n## Important Notice!\n\nOpenPype as a standalone product has reach end of it's life and this repository has now been archived in favour of [ayon-core](https://github.com/ynput/ayon-core). You can read more details about the end of life process here https://community.ynput.io/t/openpype-end-of-life-timeline/877\n\nThank you all for years of amazing contributions and see  you in AYON!\n\n```\nPlease refer to https://github.com/ynput/OpenPype/blob/develop/CONTRIBUTING.md for more information about the current PR process. \n```\n\nIntroduction\n------------\n\nOpen-source pipeline for visual effects and animation built on top of the [Avalon](https://getavalon.github.io/) framework, expanding it with extra features and integrations. OpenPype connects your DCCs, asset database, project management and time tracking into a single system. It has a tight integration with [ftrack](https://www.ftrack.com/en/), but can also run independently or be integrated into a different project management solution.\n\nOpenPype provides a robust platform for your studio, without the worry of a vendor lock. You will always have full access to the source-code and your project database will run locally or in the cloud of your choice.\n\n\nTo get all the information about the project, go to [OpenPype.io](http://openpype.io)\n\nRequirements\n------------\n\nWe aim to closely follow [**VFX Reference Platform**](https://vfxplatform.com/)\n\nOpenPype is written in Python 3 with specific elements still running in Python2 until all DCCs are fully updated. To see the list of those, that are not quite there yet, go to [VFX Python3 tracker](https://vfxpy.com/)\n\nThe main things you will need to run and build OpenPype are:\n\n- **Terminal** in your OS\n    - PowerShell 5.0+ (Windows)\n    - Bash (Linux)\n- [**Python 3.9.6**](#python) or higher\n- [**MongoDB**](#database) (needed only for local development)\n\n\nIt can be built and ran on all common platforms. We develop and test on the following:\n\n- **Windows** 10\n- **Linux**\n    - **Ubuntu** 20.04 LTS\n    - **Centos** 7\n- **Mac OSX**\n    - **10.15** Catalina\n    - **11.1** Big Sur (using Rosetta2)\n\nFor more details on requirements visit [requirements documentation](https://openpype.io/docs/dev_requirements)\n\nBuilding OpenPype\n-----------------\n\nTo build OpenPype you currently need [Python 3.9](https://www.python.org/downloads/) as we are following\n[vfx platform](https://vfxplatform.com). Because of some Linux distros comes with newer Python version\nalready, you need to install **3.9** version and make use of it. You can use perhaps [pyenv](https://github.com/pyenv/pyenv) for this on Linux.\n**Note**: We do not support 3.9.0 because of [this bug](https://github.com/python/cpython/pull/22670). Please, use higher versions of 3.9.x.\n\n### Windows\n\nYou will need [Python >= 3.9.1](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads).\nMore tools might be needed for installing dependencies (for example for **OpenTimelineIO**) - mostly\ndevelopment tools like [CMake](https://cmake.org/) and [Visual Studio](https://visualstudio.microsoft.com/cs/downloads/)\n\n#### Clone repository:\n```sh\ngit clone --recurse-submodules git@github.com:ynput/OpenPype.git\n```\n\n#### To build OpenPype:\n\n1) Run `.\\tools\\create_env.ps1` to create virtual environment in `.\\venv`.\n2) Run `.\\tools\\fetch_thirdparty_libs.ps1` to download third-party dependencies like ffmpeg and oiio. Those will be included in build.\n3) Run `.\\tools\\build.ps1` to build OpenPype executables in `.\\build\\`.\n\nTo create distributable OpenPype versions, run `./tools/create_zip.ps1` - that will\ncreate zip file with name `openpype-vx.x.x.zip` parsed from current OpenPype repository and\ncopy it to user data dir, or you can specify `--path /path/to/zip` to force it there.\n\nYou can then point **Igniter** - OpenPype setup tool - to directory containing this zip and\nit will install it on current computer.\n\nOpenPype is build using [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest) to freeze itself and all dependencies.\n\n### macOS\n\nYou will need [Python >= 3.9](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll need also other tools to build\nsome OpenPype dependencies like [CMake](https://cmake.org/) and **XCode Command Line Tools** (or some other build system).\n\nEasy way of installing everything necessary is to use [Homebrew](https://brew.sh):\n\n1) Install **Homebrew**:\n   ```sh\n    /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n    ```\n\n2) Install **cmake**:\n   ```sh\n   brew install cmake\n   ```\n\n3) Install [pyenv](https://github.com/pyenv/pyenv):\n   ```sh\n   brew install pyenv\n   echo 'eval \"$(pyenv init -)\"' >> ~/.zshrc\n   pyenv init\n   exec \"$SHELL\"\n   PATH=$(pyenv root)/shims:$PATH\n   ```\n\n4) Pull in required Python version 3.9.x:\n   ```sh\n   # install Python build dependences\n   brew install openssl readline sqlite3 xz zlib\n\n   # replace with up-to-date 3.9.x version\n   pyenv install 3.9.6\n   ```\n\n5) Set local Python version:\n   ```sh\n   # switch to OpenPype source directory\n   pyenv local 3.9.6\n   ```\n\n#### To build OpenPype:\n\n1) Run `.\\tools\\create_env.sh` to create virtual environment in `.\\venv`\n2) Run `.\\tools\\fetch_thirdparty_libs.sh` to download third-party dependencies like ffmpeg and oiio. Those will be included in build.\n3) Run `.\\tools\\build.sh` to build OpenPype executables in `.\\build\\`\n\n### Linux\n\n#### Docker\nEasiest way to build OpenPype on Linux is using [Docker](https://www.docker.com/). Just run:\n\n```sh\nsudo ./tools/docker_build.sh\n```\n\nThis will by default use Debian as base image. If you need to make Centos 7 compatible build, please run:\n\n```sh\nsudo ./tools/docker_build.sh centos7\n```\n\nIf all is successful, you'll find built OpenPype in `./build/` folder.\n\nDocker build can be also started from Windows machine, just use `./tools/docker_build.ps1` instead of shell script.\n\nThis could be used even for building linux build (with argument `centos7` or `debian`)\n\n#### Manual build\nYou will need [Python >= 3.9](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll also need [curl](https://curl.se) on systems that doesn't have one preinstalled.\n\nTo build Python related stuff, you need Python header files installed (`python3-dev` on Ubuntu for example).\n\nYou'll need also other tools to build\nsome OpenPype dependencies like [CMake](https://cmake.org/). Python 3 should be part of all modern distributions. You can use your package manager to install **git** and **cmake**.\n\n<details>\n<summary>Details for Ubuntu</summary>\nInstall git, cmake and curl\n\n```sh\nsudo apt install build-essential checkinstall\nsudo apt install git cmake curl\n```\n#### Note:\nIn case you run in error about `xcb` when running OpenPype,\nyou'll need also additional libraries for Qt5:\n\n```sh\nsudo apt install qt5-default\n```\nor if you are on Ubuntu > 20.04, there is no `qt5-default` packages so you need to install its content individually:\n\n```sh\nsudo apt-get install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools\n```\n</details>\n\n<details>\n<summary>Details for Centos</summary>\nInstall git, cmake and curl\n\n```sh\nsudo yum install qit cmake\n```\n\n#### Note:\nIn case you run in error about `xcb` when running OpenPype,\nyou'll need also additional libraries for Qt5:\n\n```sh\nsudo yum install qt5-qtbase-devel\n```\n</details>\n\n<details>\n<summary>Use pyenv to install Python version for OpenPype build</summary>\n\nYou will need **bzip2**, **readline**, **sqlite3** and other libraries.\n\nFor more details about Python build environments see:\n\nhttps://github.com/pyenv/pyenv/wiki#suggested-build-environment\n\n**For Ubuntu:**\n```sh\nsudo apt-get update; sudo apt-get install --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev\n```\n\n**For Centos:**\n```sh\nyum install gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel\n```\n\n**install pyenv**\n```sh\ncurl https://pyenv.run | bash\n\n# you can add those to ~/.bashrc\nexport PATH=\"$HOME/.pyenv/bin:$PATH\"\neval \"$(pyenv init -)\"\neval \"$(pyenv virtualenv-init -)\"\n\n# reload shell\nexec $SHELL\n\n# install Python 3.9.x\npyenv install -v 3.9.6\n\n# change path to OpenPype 3\ncd /path/to/openpype-3\n\n# set local python version\npyenv local 3.9.6\n\n```\n</details>\n\n#### To build OpenPype:\n\n1) Run `.\\tools\\create_env.sh` to create virtual environment in `.\\venv`\n2) Run `.\\tools\\build.sh` to build OpenPype executables in `.\\build\\`\n\n\nRunning OpenPype\n----------------\n\nOpenPype can by executed either from live sources (this repository) or from\n*\"frozen code\"* - executables that can be build using steps described above.\n\nIf OpenPype is executed from live sources, it will use OpenPype version included in them. If\nit is executed from frozen code it will try to find latest OpenPype version installed locally\non current computer and if it is not found, it will ask for its location. On that location\nOpenPype can be either in directories or zip files. OpenPype will try to find latest version and\ninstall it to user data directory (on Windows to `%LOCALAPPDATA%\\pypeclub\\openpype`, on Linux\n`~/.local/share/openpype` and on macOS in `~/Library/Application Support/openpype`).\n\n### From sources\nOpenPype can be run directly from sources by activating virtual environment:\n\n```sh\npoetry run python start.py tray\n```\n\nThis will use current OpenPype version with sources. You can override this with `--use-version=x.x.x` and\nthen OpenPype will try to find locally installed specified version (present in user data directory).\n\n### From frozen code\n\nYou need to build OpenPype first. This will produce two executables - `openpype_gui(.exe)` and `openpype_console(.exe)`.\nFirst one will act as GUI application and will not create console (useful in production environments).\nThe second one will create console and will write output there - useful for headless application and\ndebugging purposes. If you need OpenPype version installed, just run `./tools/create_zip(.ps1|.sh)` without\narguments and it will create zip file that OpenPype can use.\n\n\nBuilding documentation\n----------------------\n\nTo build API documentation, run `.\\tools\\make_docs(.ps1|.sh)`. It will create html documentation\nfrom current sources in `.\\docs\\build`.\n\n**Note that it needs existing virtual environment.**\n\nRunning tests\n-------------\n\nTo run tests, execute `.\\tools\\run_tests(.ps1|.sh)`.\n\n**Note that it needs existing virtual environment.**\n\n\nDeveloper tools\n---------------\n\nIn case you wish to add your own tools to  `.\\tools` folder without git tracking, it is possible by adding it with `dev_*` suffix (example: `dev_clear_pyc(.ps1|.sh)`).\n\n\n\n## Contributors ✨\n\nThanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\n\n<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->\n<!-- prettier-ignore-start -->\n<!-- markdownlint-disable -->\n<table>\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://pype.club/\"><img src=\"https://avatars.githubusercontent.com/u/3333008?v=4?s=100\" width=\"100px;\" alt=\"Milan Kolar\"/><br /><sub><b>Milan Kolar</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=mkolar\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=mkolar\" title=\"Documentation\">📖</a> <a href=\"#infra-mkolar\" title=\"Infrastructure (Hosting, Build-Tools, etc)\">🚇</a> <a href=\"#business-mkolar\" title=\"Business development\">💼</a> <a href=\"#content-mkolar\" title=\"Content\">🖋</a> <a href=\"#fundingFinding-mkolar\" title=\"Funding Finding\">🔍</a> <a href=\"#maintenance-mkolar\" title=\"Maintenance\">🚧</a> <a href=\"#projectManagement-mkolar\" title=\"Project Management\">📆</a> <a href=\"https://github.com/ynput/OpenPype/pulls?q=is%3Apr+reviewed-by%3Amkolar\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"#mentoring-mkolar\" title=\"Mentoring\">🧑‍🏫</a> <a href=\"#question-mkolar\" title=\"Answering Questions\">💬</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://www.linkedin.com/in/jakubjezek79\"><img src=\"https://avatars.githubusercontent.com/u/40640033?v=4?s=100\" width=\"100px;\" alt=\"Jakub Ježek\"/><br /><sub><b>Jakub Ježek</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=jakubjezek001\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=jakubjezek001\" title=\"Documentation\">📖</a> <a href=\"#infra-jakubjezek001\" title=\"Infrastructure (Hosting, Build-Tools, etc)\">🚇</a> <a href=\"#content-jakubjezek001\" title=\"Content\">🖋</a> <a href=\"https://github.com/ynput/OpenPype/pulls?q=is%3Apr+reviewed-by%3Ajakubjezek001\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"#maintenance-jakubjezek001\" title=\"Maintenance\">🚧</a> <a href=\"#mentoring-jakubjezek001\" title=\"Mentoring\">🧑‍🏫</a> <a href=\"#projectManagement-jakubjezek001\" title=\"Project Management\">📆</a> <a href=\"#question-jakubjezek001\" title=\"Answering Questions\">💬</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/antirotor\"><img src=\"https://avatars.githubusercontent.com/u/33513211?v=4?s=100\" width=\"100px;\" alt=\"Ondřej Samohel\"/><br /><sub><b>Ondřej Samohel</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=antirotor\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=antirotor\" title=\"Documentation\">📖</a> <a href=\"#infra-antirotor\" title=\"Infrastructure (Hosting, Build-Tools, etc)\">🚇</a> <a href=\"#content-antirotor\" title=\"Content\">🖋</a> <a href=\"https://github.com/ynput/OpenPype/pulls?q=is%3Apr+reviewed-by%3Aantirotor\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"#maintenance-antirotor\" title=\"Maintenance\">🚧</a> <a href=\"#mentoring-antirotor\" title=\"Mentoring\">🧑‍🏫</a> <a href=\"#projectManagement-antirotor\" title=\"Project Management\">📆</a> <a href=\"#question-antirotor\" title=\"Answering Questions\">💬</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/iLLiCiTiT\"><img src=\"https://avatars.githubusercontent.com/u/43494761?v=4?s=100\" width=\"100px;\" alt=\"Jakub Trllo\"/><br /><sub><b>Jakub Trllo</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=iLLiCiTiT\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=iLLiCiTiT\" title=\"Documentation\">📖</a> <a href=\"#infra-iLLiCiTiT\" title=\"Infrastructure (Hosting, Build-Tools, etc)\">🚇</a> <a href=\"https://github.com/ynput/OpenPype/pulls?q=is%3Apr+reviewed-by%3AiLLiCiTiT\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"#maintenance-iLLiCiTiT\" title=\"Maintenance\">🚧</a> <a href=\"#question-iLLiCiTiT\" title=\"Answering Questions\">💬</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/kalisp\"><img src=\"https://avatars.githubusercontent.com/u/4457962?v=4?s=100\" width=\"100px;\" alt=\"Petr Kalis\"/><br /><sub><b>Petr Kalis</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=kalisp\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=kalisp\" title=\"Documentation\">📖</a> <a href=\"#infra-kalisp\" title=\"Infrastructure (Hosting, Build-Tools, etc)\">🚇</a> <a href=\"https://github.com/ynput/OpenPype/pulls?q=is%3Apr+reviewed-by%3Akalisp\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"#maintenance-kalisp\" title=\"Maintenance\">🚧</a> <a href=\"#question-kalisp\" title=\"Answering Questions\">💬</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/64qam\"><img src=\"https://avatars.githubusercontent.com/u/26925793?v=4?s=100\" width=\"100px;\" alt=\"64qam\"/><br /><sub><b>64qam</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=64qam\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/pulls?q=is%3Apr+reviewed-by%3A64qam\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=64qam\" title=\"Documentation\">📖</a> <a href=\"#infra-64qam\" title=\"Infrastructure (Hosting, Build-Tools, etc)\">🚇</a> <a href=\"#projectManagement-64qam\" title=\"Project Management\">📆</a> <a href=\"#maintenance-64qam\" title=\"Maintenance\">🚧</a> <a href=\"#content-64qam\" title=\"Content\">🖋</a> <a href=\"#userTesting-64qam\" title=\"User Testing\">📓</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://www.colorbleed.nl/\"><img src=\"https://avatars.githubusercontent.com/u/2439881?v=4?s=100\" width=\"100px;\" alt=\"Roy Nieterau\"/><br /><sub><b>Roy Nieterau</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=BigRoy\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=BigRoy\" title=\"Documentation\">📖</a> <a href=\"https://github.com/ynput/OpenPype/pulls?q=is%3Apr+reviewed-by%3ABigRoy\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"#mentoring-BigRoy\" title=\"Mentoring\">🧑‍🏫</a> <a href=\"#question-BigRoy\" title=\"Answering Questions\">💬</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/tokejepsen\"><img src=\"https://avatars.githubusercontent.com/u/1860085?v=4?s=100\" width=\"100px;\" alt=\"Toke Jepsen\"/><br /><sub><b>Toke Jepsen</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=tokejepsen\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=tokejepsen\" title=\"Documentation\">📖</a> <a href=\"https://github.com/ynput/OpenPype/pulls?q=is%3Apr+reviewed-by%3Atokejepsen\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"#mentoring-tokejepsen\" title=\"Mentoring\">🧑‍🏫</a> <a href=\"#question-tokejepsen\" title=\"Answering Questions\">💬</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/jrsndl\"><img src=\"https://avatars.githubusercontent.com/u/45896205?v=4?s=100\" width=\"100px;\" alt=\"Jiri Sindelar\"/><br /><sub><b>Jiri Sindelar</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=jrsndl\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/pulls?q=is%3Apr+reviewed-by%3Ajrsndl\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=jrsndl\" title=\"Documentation\">📖</a> <a href=\"#content-jrsndl\" title=\"Content\">🖋</a> <a href=\"#tutorial-jrsndl\" title=\"Tutorials\">✅</a> <a href=\"#userTesting-jrsndl\" title=\"User Testing\">📓</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://barbierisimone.com/\"><img src=\"https://avatars.githubusercontent.com/u/1087869?v=4?s=100\" width=\"100px;\" alt=\"Simone Barbieri\"/><br /><sub><b>Simone Barbieri</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=simonebarbieri\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=simonebarbieri\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://karimmozilla.xyz/\"><img src=\"https://avatars.githubusercontent.com/u/82811760?v=4?s=100\" width=\"100px;\" alt=\"karimmozilla\"/><br /><sub><b>karimmozilla</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=karimmozilla\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/Allan-I\"><img src=\"https://avatars.githubusercontent.com/u/76656700?v=4?s=100\" width=\"100px;\" alt=\"Allan I. A.\"/><br /><sub><b>Allan I. A.</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=Allan-I\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://www.linkedin.com/in/mmuurrpphhyy/\"><img src=\"https://avatars.githubusercontent.com/u/352795?v=4?s=100\" width=\"100px;\" alt=\"murphy\"/><br /><sub><b>murphy</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=m-u-r-p-h-y\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/pulls?q=is%3Apr+reviewed-by%3Am-u-r-p-h-y\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"#userTesting-m-u-r-p-h-y\" title=\"User Testing\">📓</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=m-u-r-p-h-y\" title=\"Documentation\">📖</a> <a href=\"#projectManagement-m-u-r-p-h-y\" title=\"Project Management\">📆</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/aardschok\"><img src=\"https://avatars.githubusercontent.com/u/26920875?v=4?s=100\" width=\"100px;\" alt=\"Wijnand Koreman\"/><br /><sub><b>Wijnand Koreman</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=aardschok\" title=\"Code\">💻</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://jedimaster.cnblogs.com/\"><img src=\"https://avatars.githubusercontent.com/u/1798206?v=4?s=100\" width=\"100px;\" alt=\"Bo Zhou\"/><br /><sub><b>Bo Zhou</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=zhoub\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://www.linkedin.com/in/clementhector/\"><img src=\"https://avatars.githubusercontent.com/u/7068597?v=4?s=100\" width=\"100px;\" alt=\"Clément Hector\"/><br /><sub><b>Clément Hector</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=ClementHector\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/pulls?q=is%3Apr+reviewed-by%3AClementHector\" title=\"Reviewed Pull Requests\">👀</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://twitter.com/davidlatwe\"><img src=\"https://avatars.githubusercontent.com/u/3357009?v=4?s=100\" width=\"100px;\" alt=\"David Lai\"/><br /><sub><b>David Lai</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=davidlatwe\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/pulls?q=is%3Apr+reviewed-by%3Adavidlatwe\" title=\"Reviewed Pull Requests\">👀</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/2-REC\"><img src=\"https://avatars.githubusercontent.com/u/42170307?v=4?s=100\" width=\"100px;\" alt=\"Derek \"/><br /><sub><b>Derek </b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=2-REC\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=2-REC\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/gabormarinov\"><img src=\"https://avatars.githubusercontent.com/u/8620515?v=4?s=100\" width=\"100px;\" alt=\"Gábor Marinov\"/><br /><sub><b>Gábor Marinov</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=gabormarinov\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=gabormarinov\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/icyvapor\"><img src=\"https://avatars.githubusercontent.com/u/1195278?v=4?s=100\" width=\"100px;\" alt=\"icyvapor\"/><br /><sub><b>icyvapor</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=icyvapor\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=icyvapor\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/jlorrain\"><img src=\"https://avatars.githubusercontent.com/u/7955673?v=4?s=100\" width=\"100px;\" alt=\"Jérôme LORRAIN\"/><br /><sub><b>Jérôme LORRAIN</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=jlorrain\" title=\"Code\">💻</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/dmo-j-cube\"><img src=\"https://avatars.githubusercontent.com/u/89823400?v=4?s=100\" width=\"100px;\" alt=\"David Morris-Oliveros\"/><br /><sub><b>David Morris-Oliveros</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=dmo-j-cube\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/BenoitConnan\"><img src=\"https://avatars.githubusercontent.com/u/82808268?v=4?s=100\" width=\"100px;\" alt=\"BenoitConnan\"/><br /><sub><b>BenoitConnan</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=BenoitConnan\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/Malthaldar\"><img src=\"https://avatars.githubusercontent.com/u/33671694?v=4?s=100\" width=\"100px;\" alt=\"Malthaldar\"/><br /><sub><b>Malthaldar</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=Malthaldar\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://www.svenneve.com/\"><img src=\"https://avatars.githubusercontent.com/u/2472863?v=4?s=100\" width=\"100px;\" alt=\"Sven Neve\"/><br /><sub><b>Sven Neve</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=svenneve\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/zafrs\"><img src=\"https://avatars.githubusercontent.com/u/26890002?v=4?s=100\" width=\"100px;\" alt=\"zafrs\"/><br /><sub><b>zafrs</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=zafrs\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://felixdavid.com/\"><img src=\"https://avatars.githubusercontent.com/u/22875539?v=4?s=100\" width=\"100px;\" alt=\"Félix David\"/><br /><sub><b>Félix David</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=Tilix4\" title=\"Code\">💻</a> <a href=\"https://github.com/ynput/OpenPype/commits?author=Tilix4\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://abogomolov.com\"><img src=\"https://avatars.githubusercontent.com/u/11698866?v=4?s=100\" width=\"100px;\" alt=\"Alexey Bogomolov\"/><br /><sub><b>Alexey Bogomolov</b></sub></a><br /><a href=\"https://github.com/ynput/OpenPype/commits?author=movalex\" title=\"Code\">💻</a></td>\n    </tr>\n  </tbody>\n</table>\n\n<!-- markdownlint-restore -->\n<!-- prettier-ignore-end -->\n\n<!-- ALL-CONTRIBUTORS-LIST:END -->\n\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!\n"
  },
  {
    "path": "app_launcher.py",
    "content": "\"\"\"Launch process that is not child process of python or OpenPype.\n\nThis is written for linux distributions where process tree may affect what\nis when closed or blocked to be closed.\n\"\"\"\n\nimport os\nimport sys\nimport subprocess\nimport json\n\n\ndef main(input_json_path):\n    \"\"\"Read launch arguments from json file and launch the process.\n\n    Expected that json contains \"args\" key with string or list of strings.\n\n    Arguments are converted to string using `list2cmdline`. At the end is added\n    `&` which will cause that launched process is detached and running as\n    \"background\" process.\n\n    ## Notes\n    @iLLiCiT: This should be possible to do with 'disown' or double forking but\n        I didn't find a way how to do it properly. Disown didn't work as\n        expected for me and double forking killed parent process which is\n        unexpected too.\n    \"\"\"\n    with open(input_json_path, \"r\") as stream:\n        data = json.load(stream)\n\n    # Change environment variables\n    env = data.get(\"env\") or {}\n    for key, value in env.items():\n        os.environ[key] = value\n\n    # Prepare launch arguments\n    args = data[\"args\"]\n    if isinstance(args, list):\n        args = subprocess.list2cmdline(args)\n\n    # Run the command as background process\n    shell_cmd = args + \" &\"\n    os.system(shell_cmd)\n    sys.exit(0)\n\n\nif __name__ == \"__main__\":\n    # Expect that last argument is path to a json with launch args information\n    main(sys.argv[-1])\n"
  },
  {
    "path": "conftest.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Conftest.\"\"\"\n...\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSOURCEDIR     = source\nBUILDDIR      = build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "docs/README.md",
    "content": "API Documentation\n=================\n\nThis documents the way how to build and modify API documentation using Sphinx and AutoAPI. Ground for documentation\nshould be directly in sources - in docstrings and markdowns. Sphinx and AutoAPI will crawl over them and generate\nRST files that are in turn used to generate HTML documentation. For docstrings we prefer \"Napoleon\" or \"Google\" style\ndocstrings, but RST is also acceptable mainly in cases where you need to use Sphinx directives.\n\nUsing only docstrings is not really viable as some documentation should be done on higher level - like overview of\nsome modules/functionality and so on. This should be done directly in RST files and committed to repository.\n\nConfiguration\n-------------\nConfiguration is done in `/docs/source/conf.py`. The most important settings are:\n\n- `autodoc_mock_imports`: add modules that can't be actually imported by Sphinx in running environment, like `nuke`, `maya`, etc.\n- `autoapi_ignore`: add directories that shouldn't be processed by **AutoAPI**, like vendor dirs, etc.\n- `html_theme_options`: you can use these options to influence how the html theme of the generated files will look.\n- `myst_gfm_only`: are Myst parser option for Markdown setting what flavour of Markdown should be used.\n\nHow to build it\n---------------\n\nYou can run:\n\n```sh\ncd .\\docs\nmake.bat html\n```\n\non linux/macOS:\n\n```sh\ncd ./docs\nmake html\n```\n\nThis will go over our code and generate **.rst** files in `/docs/source/autoapi` and from those it will generate\nfull html documentation in `/docs/build/html`.\n\nDuring the build you may see tons of red errors that are pointing to our issues:\n\n1) **Wrong imports** -\nInvalid import are usually wrong relative imports (too deep) or circular imports.\n2) **Invalid docstrings** -\nDocstrings to be processed into documentation needs to follow some syntax - this can be checked by running\n`pydocstyle` that is already included with OpenPype\n3) **Invalid markdown/rst files** -\nMarkdown/RST files can be included inside RST files using `.. include::` directive. But they have to be properly\nformatted.\n\nEditing RST templates\n---------------------\nEverything starts with `/docs/source/index.rst` - this file should be properly edited, Right now it just\nincludes `readme.rst` that in turn include and parse main `README.md`. This is entrypoint to API documentation.\nAll templates generated by AutoAPI are in `/docs/source/autoapi`. They should be eventually committed to repository\nand edited too.\n\nSteps for enhancing API documentation\n-------------------------------------\n\n1) Run `/docs/make.bat html`\n2) Read the red errors/warnings - fix it in the code\n3) Run `/docs/make.bat html` - again until there are no red lines\n4) Edit RST files and add some meaningful content there\n\nResources\n=========\n\n- [ReStructuredText on Wikipedia](https://en.wikipedia.org/wiki/ReStructuredText)\n- [RST Quick Reference](https://docutils.sourceforge.io/docs/user/rst/quickref.html)\n- [Sphinx AutoAPI Documentation](https://sphinx-autoapi.readthedocs.io/en/latest/)\n- [Example of Google Style Python Docstrings](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html)\n- [Sphinx Directives](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html)\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=..\\.poetry\\bin\\poetry run sphinx-build\r\n)\r\nset SOURCEDIR=source\r\nset BUILDDIR=build\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.http://sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "docs/source/_static/README.md",
    "content": ""
  },
  {
    "path": "docs/source/_templates/autoapi/index.rst",
    "content": "API Reference\n=============\n\nThis page contains auto-generated API reference documentation [#f1]_.\n\n.. toctree::\n   :titlesonly:\n\n   {% for page in pages %}\n   {% if page.top_level_object and page.display %}\n   {{ page.include_path }}\n   {% endif %}\n   {% endfor %}\n\n.. [#f1] Created with `sphinx-autoapi <https://github.com/readthedocs/sphinx-autoapi>`_\n"
  },
  {
    "path": "docs/source/_templates/autoapi/python/attribute.rst",
    "content": "{% extends \"python/data.rst\" %}\n"
  },
  {
    "path": "docs/source/_templates/autoapi/python/class.rst",
    "content": "{% if obj.display %}\n.. py:{{ obj.type }}:: {{ obj.short_name }}{% if obj.args %}({{ obj.args }}){% endif %}\n{% for (args, return_annotation) in obj.overloads %}\n   {{ \" \" * (obj.type | length) }}   {{ obj.short_name }}{% if args %}({{ args }}){% endif %}\n{% endfor %}\n\n\n   {% if obj.bases %}\n   {% if \"show-inheritance\" in autoapi_options %}\n   Bases: {% for base in obj.bases %}{{ base|link_objs }}{% if not loop.last %}, {% endif %}{% endfor %}\n   {% endif %}\n\n\n   {% if \"show-inheritance-diagram\" in autoapi_options and obj.bases != [\"object\"] %}\n   .. autoapi-inheritance-diagram:: {{ obj.obj[\"full_name\"] }}\n      :parts: 1\n      {% if \"private-members\" in autoapi_options %}\n      :private-bases:\n      {% endif %}\n\n   {% endif %}\n   {% endif %}\n   {% if obj.docstring %}\n   {{ obj.docstring|indent(3) }}\n   {% endif %}\n   {% if \"inherited-members\" in autoapi_options %}\n   {% set visible_classes = obj.classes|selectattr(\"display\")|list %}\n   {% else %}\n   {% set visible_classes = obj.classes|rejectattr(\"inherited\")|selectattr(\"display\")|list %}\n   {% endif %}\n   {% for klass in visible_classes %}\n   {{ klass.render()|indent(3) }}\n   {% endfor %}\n   {% if \"inherited-members\" in autoapi_options %}\n   {% set visible_properties = obj.properties|selectattr(\"display\")|list %}\n   {% else %}\n   {% set visible_properties = obj.properties|rejectattr(\"inherited\")|selectattr(\"display\")|list %}\n   {% endif %}\n   {% for property in visible_properties %}\n   {{ property.render()|indent(3) }}\n   {% endfor %}\n   {% if \"inherited-members\" in autoapi_options %}\n   {% set visible_attributes = obj.attributes|selectattr(\"display\")|list %}\n   {% else %}\n   {% set visible_attributes = obj.attributes|rejectattr(\"inherited\")|selectattr(\"display\")|list %}\n   {% endif %}\n   {% for attribute in visible_attributes %}\n   {{ attribute.render()|indent(3) }}\n   {% endfor %}\n   {% if \"inherited-members\" in autoapi_options %}\n   {% set visible_methods = obj.methods|selectattr(\"display\")|list %}\n   {% else %}\n   {% set visible_methods = obj.methods|rejectattr(\"inherited\")|selectattr(\"display\")|list %}\n   {% endif %}\n   {% for method in visible_methods %}\n   {{ method.render()|indent(3) }}\n   {% endfor %}\n{% endif %}\n"
  },
  {
    "path": "docs/source/_templates/autoapi/python/data.rst",
    "content": "{% if obj.display %}\n.. py:{{ obj.type }}:: {{ obj.name }}\n   {%- if obj.annotation is not none %}\n\n   :type: {%- if obj.annotation %} {{ obj.annotation }}{%- endif %}\n\n   {%- endif %}\n\n   {%- if obj.value is not none %}\n\n   :value: {% if obj.value is string and obj.value.splitlines()|count > 1 -%}\n                Multiline-String\n\n    .. raw:: html\n\n        <details><summary>Show Value</summary>\n\n    .. code-block:: python\n\n        \"\"\"{{ obj.value|indent(width=8,blank=true) }}\"\"\"\n\n    .. raw:: html\n\n        </details>\n\n            {%- else -%}\n              {%- if obj.value is string -%}\n                {{ \"%r\" % obj.value|string|truncate(100) }}\n              {%- else -%}\n                {{ obj.value|string|truncate(100) }}\n              {%- endif -%}\n            {%- endif %}\n   {%- endif %}\n\n\n   {{ obj.docstring|indent(3) }}\n{% endif %}\n"
  },
  {
    "path": "docs/source/_templates/autoapi/python/exception.rst",
    "content": "{% extends \"python/class.rst\" %}\n"
  },
  {
    "path": "docs/source/_templates/autoapi/python/function.rst",
    "content": "{% if obj.display %}\n.. py:function:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %}\n\n{% for (args, return_annotation) in obj.overloads %}\n              {{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %}\n\n{% endfor %}\n   {% for property in obj.properties %}\n   :{{ property }}:\n   {% endfor %}\n\n   {% if obj.docstring %}\n   {{ obj.docstring|indent(3) }}\n   {% endif %}\n{% endif %}\n"
  },
  {
    "path": "docs/source/_templates/autoapi/python/method.rst",
    "content": "{%- if obj.display %}\n.. py:method:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %}\n\n{% for (args, return_annotation) in obj.overloads %}\n            {{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %}\n\n{% endfor %}\n   {% if obj.properties %}\n   {% for property in obj.properties %}\n   :{{ property }}:\n   {% endfor %}\n\n   {% else %}\n\n   {% endif %}\n   {% if obj.docstring %}\n   {{ obj.docstring|indent(3) }}\n   {% endif %}\n{% endif %}\n"
  },
  {
    "path": "docs/source/_templates/autoapi/python/module.rst",
    "content": "{% if not obj.display %}\n:orphan:\n\n{% endif %}\n:py:mod:`{{ obj.name }}`\n=========={{ \"=\" * obj.name|length }}\n\n.. py:module:: {{ obj.name }}\n\n{% if obj.docstring %}\n.. autoapi-nested-parse::\n\n   {{ obj.docstring|indent(3) }}\n\n{% endif %}\n\n{% block subpackages %}\n{% set visible_subpackages = obj.subpackages|selectattr(\"display\")|list %}\n{% if visible_subpackages %}\nSubpackages\n-----------\n.. toctree::\n   :titlesonly:\n   :maxdepth: 3\n\n{% for subpackage in visible_subpackages %}\n   {{ subpackage.short_name }}/index.rst\n{% endfor %}\n\n\n{% endif %}\n{% endblock %}\n{% block submodules %}\n{% set visible_submodules = obj.submodules|selectattr(\"display\")|list %}\n{% if visible_submodules %}\nSubmodules\n----------\n.. toctree::\n   :titlesonly:\n   :maxdepth: 1\n\n{% for submodule in visible_submodules %}\n   {{ submodule.short_name }}/index.rst\n{% endfor %}\n\n\n{% endif %}\n{% endblock %}\n{% block content %}\n{% if obj.all is not none %}\n{% set visible_children = obj.children|selectattr(\"short_name\", \"in\", obj.all)|list %}\n{% elif obj.type is equalto(\"package\") %}\n{% set visible_children = obj.children|selectattr(\"display\")|list %}\n{% else %}\n{% set visible_children = obj.children|selectattr(\"display\")|rejectattr(\"imported\")|list %}\n{% endif %}\n{% if visible_children %}\n{{ obj.type|title }} Contents\n{{ \"-\" * obj.type|length }}---------\n\n{% set visible_classes = visible_children|selectattr(\"type\", \"equalto\", \"class\")|list %}\n{% set visible_functions = visible_children|selectattr(\"type\", \"equalto\", \"function\")|list %}\n{% set visible_attributes = visible_children|selectattr(\"type\", \"equalto\", \"data\")|list %}\n{% if \"show-module-summary\" in autoapi_options and (visible_classes or visible_functions) %}\n{% block classes scoped %}\n{% if visible_classes %}\nClasses\n~~~~~~~\n\n.. autoapisummary::\n\n{% for klass in visible_classes %}\n   {{ klass.id }}\n{% endfor %}\n\n\n{% endif %}\n{% endblock %}\n\n{% block functions scoped %}\n{% if visible_functions %}\nFunctions\n~~~~~~~~~\n\n.. autoapisummary::\n\n{% for function in visible_functions %}\n   {{ function.id }}\n{% endfor %}\n\n\n{% endif %}\n{% endblock %}\n\n{% block attributes scoped %}\n{% if visible_attributes %}\nAttributes\n~~~~~~~~~~\n\n.. autoapisummary::\n\n{% for attribute in visible_attributes %}\n   {{ attribute.id }}\n{% endfor %}\n\n\n{% endif %}\n{% endblock %}\n{% endif %}\n{% for obj_item in visible_children %}\n{{ obj_item.render()|indent(0) }}\n{% endfor %}\n{% endif %}\n{% endblock %}\n"
  },
  {
    "path": "docs/source/_templates/autoapi/python/package.rst",
    "content": "{% extends \"python/module.rst\" %}\n"
  },
  {
    "path": "docs/source/_templates/autoapi/python/property.rst",
    "content": "{%- if obj.display %}\n.. py:property:: {{ obj.short_name }}\n   {% if obj.annotation %}\n   :type: {{ obj.annotation }}\n   {% endif %}\n   {% if obj.properties %}\n   {% for property in obj.properties %}\n   :{{ property }}:\n   {% endfor %}\n   {% endif %}\n\n   {% if obj.docstring %}\n   {{ obj.docstring|indent(3) }}\n   {% endif %}\n{% endif %}\n"
  },
  {
    "path": "docs/source/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Configuration file for the Sphinx documentation builder.\n#\n# This file does only contain a selection of the most common options. For a\n# full list see the documentation:\n# http://www.sphinx-doc.org/en/master/config\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n# import sys\n\nimport os\nimport sys\nimport revitron_sphinx_theme\n\nopenpype_root = os.path.abspath('../..')\nsys.path.insert(0, openpype_root)\n# app = QApplication([])\n\n\"\"\"\nrepos = os.listdir(os.path.abspath(\"../../repos\"))\nrepos = [os.path.join(openpype_root, \"repos\", repo) for repo in repos]\nfor repo in repos:\n    sys.path.append(repo)\n\"\"\"\n\ntodo_include_todos = True\nautodoc_mock_imports = [\"maya\", \"pymel\", \"nuke\", \"nukestudio\", \"nukescripts\",\n                        \"hiero\", \"bpy\", \"fusion\", \"houdini\", \"hou\", \"unreal\",\n                        \"__builtin__\", \"resolve\", \"pysync\", \"DaVinciResolveScript\"]\n\n# -- Project information -----------------------------------------------------\n\nproject = 'OpenPype'\ncopyright = '2023 Ynput'\nauthor = 'Ynput'\n\n# The short X.Y version\nversion = ''\n# The full version, including alpha/beta/rc tags\nrelease = ''\n\n\n# -- General configuration ---------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.napoleon',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.mathjax',\n    'sphinx.ext.autosummary',\n    'revitron_sphinx_theme',\n    'autoapi.extension',\n    'myst_parser'\n]\n\n##############################\n# Autoapi settings\n##############################\n\nautoapi_dirs = ['../../openpype', '../../igniter']\n\n# bypass modules with a lot of python2 content for now\nautoapi_ignore = [\n    \"*vendor*\",\n    \"*schemas*\",\n    \"*startup/*\",\n    \"*/website*\",\n    \"*openpype/hooks*\",\n    \"*openpype/style*\",\n    \"openpype/tests*\",\n    # to many levels of relative import:\n    \"*/modules/sync_server/*\"\n]\nautoapi_keep_files = True\nautoapi_options = [\n    'members',\n    'undoc-members',\n    'show-inheritance',\n    'show-module-summary'\n]\nautoapi_add_toctree_entry = True\nautoapi_template_dir = '_templates/autoapi'\n\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = ['.rst', '.md']\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = \"English\"\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = [\n    \"openpype.hosts.resolve.*\",\n    \"openpype.tools.*\"\n    ]\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'friendly'\n\n# -- Options for autodoc -----------------------------------------------------\nautodoc_default_flags = ['members']\nautosummary_generate = True\n\n\n# -- Options for HTML output -------------------------------------------------\n\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = 'revitron_sphinx_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\nhtml_theme_options = {\n    'collapse_navigation': True,\n    'sticky_navigation': True,\n    'navigation_depth': 4,\n    'includehidden': True,\n    'titles_only': False,\n    'github_url': '',\n}\nhtml_logo = '_static/AYON_tight_G.svg'\n\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# Custom sidebar templates, must be a dictionary that maps document names\n# to template names.\n#\n# The default sidebars (for documents that don't match any pattern) are\n# defined by theme itself.  Builtin themes are using these templates by\n# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',\n# 'searchbox.html']``.\n#\n# html_sidebars = {}\n\n\n# -- Options for HTMLHelp output ---------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'pypedoc'\n\n\n# -- Options for LaTeX output ------------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'openpype.tex', 'OpenPype Documentation',\n     'Ynput', 'manual'),\n]\n\n\n# -- Options for manual page output ------------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'openpype', 'OpenPype Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output ----------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'OpenPype', 'OpenPype Documentation',\n     author, 'OpenPype', 'Pipeline for studios',\n     'Miscellaneous'),\n]\n\n\n# -- Options for Epub output -------------------------------------------------\n\n# Bibliographic Dublin Core info.\nepub_title = project\n\n# The unique identifier of the text. This can be a ISBN number\n# or the project homepage.\n#\n# epub_identifier = ''\n\n# A unique identification for the text.\n#\n# epub_uid = ''\n\n# A list of files that should not be packed into the epub file.\nepub_exclude_files = ['search.html']\n\n\n# -- Extension configuration -------------------------------------------------\n\n# -- Options for intersphinx extension ---------------------------------------\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'https://docs.python.org/3/': None\n}\n\nmyst_gfm_only = True\n"
  },
  {
    "path": "docs/source/index.rst",
    "content": ".. openpype documentation master file, created by\n   sphinx-quickstart on Mon May 13 17:18:23 2019.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to OpenPype's API documentation!\n========================================\n\n.. toctree::\n\n   Readme <readme.rst>\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "docs/source/readme.rst",
    "content": "===============\nOpenPype Readme\n===============\n\n.. include:: ../../README.md\n   :parser: myst_parser.sphinx_\n"
  },
  {
    "path": "igniter/Poppins/OFL.txt",
    "content": "Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttp://scripts.sil.org/OFL\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "igniter/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Open install dialog.\"\"\"\n\nimport os\nimport sys\n\nos.chdir(os.path.dirname(__file__))  # for override sys.path in Deadline\n\nfrom .bootstrap_repos import (\n    BootstrapRepos,\n    OpenPypeVersion\n)\nfrom .version import __version__ as version\n\n# Store OpenPypeVersion to 'sys.modules'\n#   - this makes it available in OpenPype processes without modifying\n#       'sys.path' or 'PYTHONPATH'\nif \"OpenPypeVersion\" not in sys.modules:\n    sys.modules[\"OpenPypeVersion\"] = OpenPypeVersion\n\n\ndef _get_qt_app():\n    from qtpy import QtWidgets, QtCore\n\n    app = QtWidgets.QApplication.instance()\n    if app is not None:\n        return app\n\n    for attr_name in (\n        \"AA_EnableHighDpiScaling\",\n        \"AA_UseHighDpiPixmaps\",\n    ):\n        attr = getattr(QtCore.Qt, attr_name, None)\n        if attr is not None:\n            QtWidgets.QApplication.setAttribute(attr)\n\n    policy = os.getenv(\"QT_SCALE_FACTOR_ROUNDING_POLICY\")\n    if (\n        hasattr(QtWidgets.QApplication, \"setHighDpiScaleFactorRoundingPolicy\")\n        and not policy\n    ):\n        QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy(\n            QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough\n        )\n\n    return QtWidgets.QApplication(sys.argv)\n\n\ndef open_dialog():\n    \"\"\"Show Igniter dialog.\"\"\"\n    if os.getenv(\"OPENPYPE_HEADLESS_MODE\"):\n        print(\"!!! Can't open dialog in headless mode. Exiting.\")\n        sys.exit(1)\n    from .install_dialog import InstallDialog\n\n    app = _get_qt_app()\n\n    d = InstallDialog()\n    d.open()\n\n    app.exec_()\n    return d.result()\n\n\ndef open_update_window(openpype_version):\n    \"\"\"Open update window.\"\"\"\n    if os.getenv(\"OPENPYPE_HEADLESS_MODE\"):\n        print(\"!!! Can't open dialog in headless mode. Exiting.\")\n        sys.exit(1)\n\n    from .update_window import UpdateWindow\n\n    app = _get_qt_app()\n\n    d = UpdateWindow(version=openpype_version)\n    d.open()\n\n    app.exec_()\n    version_path = d.get_version_path()\n    return version_path\n\n\ndef show_message_dialog(title, message):\n    \"\"\"Show dialog with a message and title to user.\"\"\"\n    if os.getenv(\"OPENPYPE_HEADLESS_MODE\"):\n        print(\"!!! Can't open dialog in headless mode. Exiting.\")\n        sys.exit(1)\n\n    from .message_dialog import MessageDialog\n\n    app = _get_qt_app()\n\n    dialog = MessageDialog(title, message)\n    dialog.open()\n\n    app.exec_()\n\n\n__all__ = [\n    \"BootstrapRepos\",\n    \"open_dialog\",\n    \"open_update_window\",\n    \"show_message_dialog\",\n    \"version\"\n]\n"
  },
  {
    "path": "igniter/__main__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Open install dialog.\"\"\"\n\nimport sys\nfrom qtpy import QtWidgets\n\nfrom .install_dialog import InstallDialog\n\n\nRESULT = 0\n\n\ndef get_result(res: int):\n    \"\"\"Sets result returned from dialog.\"\"\"\n    global RESULT\n    RESULT = res\n\n\napp = QtWidgets.QApplication(sys.argv)\n\nd = InstallDialog()\nd.finished.connect(get_result)\nd.open()\napp.exec()\nsys.exit(RESULT)\n"
  },
  {
    "path": "igniter/bootstrap_repos.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Bootstrap OpenPype repositories.\"\"\"\nfrom __future__ import annotations\nimport logging as log\nimport os\nimport re\nimport shutil\nimport sys\nimport tempfile\nfrom pathlib import Path\nfrom typing import Union, Callable, List, Tuple\nimport hashlib\nimport platform\n\nfrom zipfile import ZipFile, BadZipFile\n\nfrom appdirs import user_data_dir\nfrom speedcopy import copyfile\nimport semver\n\nfrom .user_settings import (\n    OpenPypeSecureRegistry,\n    OpenPypeSettingsRegistry\n)\nfrom .tools import (\n    get_openpype_global_settings,\n    get_openpype_path_from_settings,\n    get_expected_studio_version_str,\n    get_local_openpype_path_from_settings\n)\n\n\nLOG_INFO = 0\nLOG_WARNING = 1\nLOG_ERROR = 3\n\n\ndef sanitize_long_path(path):\n    \"\"\"Sanitize long paths (260 characters) when on Windows.\n\n    Long paths are not capatible with ZipFile or reading a file, so we can\n    shorten the path to use.\n\n    Args:\n        path (str): path to either directory or file.\n\n    Returns:\n        str: sanitized path\n    \"\"\"\n    if platform.system().lower() != \"windows\":\n        return path\n    path = os.path.abspath(path)\n\n    if path.startswith(\"\\\\\\\\\"):\n        path = \"\\\\\\\\?\\\\UNC\\\\\" + path[2:]\n    else:\n        path = \"\\\\\\\\?\\\\\" + path\n    return path\n\n\ndef sha256sum(filename):\n    \"\"\"Calculate sha256 for content of the file.\n\n    Args:\n         filename (str): Path to file.\n\n    Returns:\n        str: hex encoded sha256\n\n    \"\"\"\n    h = hashlib.sha256()\n    b = bytearray(128 * 1024)\n    mv = memoryview(b)\n    with open(filename, 'rb', buffering=0) as f:\n        for n in iter(lambda: f.readinto(mv), 0):\n            h.update(mv[:n])\n    return h.hexdigest()\n\n\nclass ZipFileLongPaths(ZipFile):\n    def _extract_member(self, member, targetpath, pwd):\n        return ZipFile._extract_member(\n            self, member, sanitize_long_path(targetpath), pwd\n        )\n\n\nclass OpenPypeVersion(semver.VersionInfo):\n    \"\"\"Class for storing information about OpenPype version.\n\n    Attributes:\n        path (str): path to OpenPype\n\n    \"\"\"\n    path = None\n\n    _local_openpype_path = None\n    # this should match any string complying with https://semver.org/\n    _VERSION_REGEX = re.compile(r\"(?P<major>0|[1-9]\\d*)\\.(?P<minor>0|[1-9]\\d*)\\.(?P<patch>0|[1-9]\\d*)(?:-(?P<prerelease>[a-zA-Z\\d\\-.]*))?(?:\\+(?P<buildmetadata>[a-zA-Z\\d\\-.]*))?\")  # noqa: E501\n    _installed_version = None\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"Create OpenPype version.\n\n        .. deprecated:: 3.0.0-rc.2\n            `client` and `variant` are removed.\n\n\n        Args:\n            major (int): version when you make incompatible API changes.\n            minor (int): version when you add functionality in a\n                backwards-compatible manner.\n            patch (int): version when you make backwards-compatible bug fixes.\n            prerelease (str): an optional prerelease string\n            build (str): an optional build string\n            version (str): if set, it will be parsed and will override\n                parameters like `major`, `minor` and so on.\n            path (Path): path to version location.\n\n        \"\"\"\n        self.path = None\n\n        if \"version\" in kwargs.keys():\n            if not kwargs.get(\"version\"):\n                raise ValueError(\"Invalid version specified\")\n            v = OpenPypeVersion.parse(kwargs.get(\"version\"))\n            kwargs[\"major\"] = v.major\n            kwargs[\"minor\"] = v.minor\n            kwargs[\"patch\"] = v.patch\n            kwargs[\"prerelease\"] = v.prerelease\n            kwargs[\"build\"] = v.build\n            kwargs.pop(\"version\")\n\n        if kwargs.get(\"path\"):\n            if isinstance(kwargs.get(\"path\"), str):\n                self.path = Path(kwargs.get(\"path\"))\n            elif isinstance(kwargs.get(\"path\"), Path):\n                self.path = kwargs.get(\"path\")\n            else:\n                raise TypeError(\"Path must be str or Path\")\n            kwargs.pop(\"path\")\n\n        if \"path\" in kwargs.keys():\n            kwargs.pop(\"path\")\n\n        super().__init__(*args, **kwargs)\n\n    def __repr__(self):\n        return f\"<{self.__class__.__name__}: {str(self)} - path={self.path}>\"\n\n    def __lt__(self, other: OpenPypeVersion):\n        result = super().__lt__(other)\n        # prefer path over no path\n        if self == other and not self.path and other.path:\n            return True\n\n        if self == other and self.path and other.path and \\\n                other.path.is_dir() and self.path.is_file():\n            return True\n\n        if self.finalize_version() == other.finalize_version() and \\\n                self.prerelease == other.prerelease:\n            return True\n\n        return result\n\n    def get_main_version(self) -> str:\n        \"\"\"Return main version component.\n\n        This returns x.x.x part of version from possibly more complex one\n        like x.x.x-foo-bar.\n\n        .. deprecated:: 3.0.0-rc.2\n            use `finalize_version()` instead.\n        Returns:\n            str: main version component\n\n        \"\"\"\n        return str(self.finalize_version())\n\n    @staticmethod\n    def version_in_str(string: str) -> Union[None, OpenPypeVersion]:\n        \"\"\"Find OpenPype version in given string.\n\n        Args:\n            string (str):  string to search.\n\n        Returns:\n            OpenPypeVersion: of detected or None.\n\n        \"\"\"\n        # strip .zip ext if present\n        string = re.sub(r\"\\.zip$\", \"\", string, flags=re.IGNORECASE)\n        m = re.search(OpenPypeVersion._VERSION_REGEX, string)\n        if not m:\n            return None\n        version = OpenPypeVersion.parse(string[m.start():m.end()])\n        return version\n\n    def __hash__(self):\n        return hash(self.path) if self.path else hash(str(self))\n\n    @staticmethod\n    def is_version_in_dir(\n            dir_item: Path, version: OpenPypeVersion) -> Tuple[bool, str]:\n        \"\"\"Test if path item is OpenPype version matching detected version.\n\n        If item is directory that might (based on it's name)\n        contain OpenPype version, check if it really does contain\n        OpenPype and that their versions matches.\n\n        Args:\n            dir_item (Path): Directory to test.\n            version (OpenPypeVersion): OpenPype version detected\n                from name.\n\n        Returns:\n            Tuple: State and reason, True if it is valid OpenPype version,\n                   False otherwise.\n\n        \"\"\"\n        try:\n            # add one 'openpype' level as inside dir there should\n            # be many other repositories.\n            version_str = OpenPypeVersion.get_version_string_from_directory(\n                dir_item)  # noqa: E501\n            version_check = OpenPypeVersion(version=version_str)\n        except ValueError:\n            return False, f\"cannot determine version from {dir_item}\"\n\n        version_main = version_check.get_main_version()\n        detected_main = version.get_main_version()\n        if version_main != detected_main:\n            return False, (f\"dir version ({version}) and \"\n                           f\"its content version ({version_check}) \"\n                           \"doesn't match. Skipping.\")\n        return True, \"Versions match\"\n\n    @staticmethod\n    def is_version_in_zip(\n            zip_item: Path, version: OpenPypeVersion) -> Tuple[bool, str]:\n        \"\"\"Test if zip path is OpenPype version matching detected version.\n\n        Open zip file, look inside and parse version from OpenPype\n        inside it. If there is none, or it is different from\n        version specified in file name, skip it.\n\n        Args:\n            zip_item (Path): Zip file to test.\n            version (OpenPypeVersion): Pype version detected\n                from name.\n\n        Returns:\n           Tuple: State and reason, True if it is valid OpenPype version,\n                False otherwise.\n\n        \"\"\"\n        # skip non-zip files\n        if zip_item.suffix.lower() != \".zip\":\n            return False, \"Not a zip\"\n\n        try:\n            with ZipFile(zip_item, \"r\") as zip_file:\n                with zip_file.open(\n                        \"openpype/version.py\") as version_file:\n                    zip_version = {}\n                    exec(version_file.read(), zip_version)\n                    try:\n                        version_check = OpenPypeVersion(\n                            version=zip_version[\"__version__\"])\n                    except ValueError as e:\n                        return False, str(e)\n\n                    version_main = version_check.get_main_version()  #\n                    # noqa: E501\n                    detected_main = version.get_main_version()\n                    # noqa: E501\n\n                    if version_main != detected_main:\n                        return False, (f\"zip version ({version}) \"\n                                       f\"and its content version \"\n                                       f\"({version_check}) \"\n                                       \"doesn't match. Skipping.\")\n        except BadZipFile:\n            return False, f\"{zip_item} is not a zip file\"\n        except KeyError:\n            return False, \"Zip does not contain OpenPype\"\n        return True, \"Versions match\"\n\n    @staticmethod\n    def get_version_string_from_directory(repo_dir: Path) -> Union[str, None]:\n        \"\"\"Get version of OpenPype in given directory.\n\n        Note: in frozen OpenPype installed in user data dir, this must point\n        one level deeper as it is:\n        `openpype-version-v3.0.0/openpype/version.py`\n\n        Args:\n            repo_dir (Path): Path to OpenPype repo.\n\n        Returns:\n            str: version string.\n            None: if OpenPype is not found.\n\n        \"\"\"\n        # try to find version\n        version_file = Path(repo_dir) / \"openpype\" / \"version.py\"\n        if not version_file.exists():\n            return None\n\n        version = {}\n        with version_file.open(\"r\") as fp:\n            exec(fp.read(), version)\n\n        return version['__version__']\n\n    @classmethod\n    def get_openpype_path(cls):\n        \"\"\"Path to openpype zip directory.\n\n        Path can be set through environment variable 'OPENPYPE_PATH' which\n        is set during start of OpenPype if is not available.\n        \"\"\"\n        return os.getenv(\"OPENPYPE_PATH\")\n\n    @classmethod\n    def get_local_openpype_path(cls):\n        \"\"\"Path to unzipped versions.\n\n        By default it should be user appdata, but could be overridden by\n        settings.\n        \"\"\"\n        if cls._local_openpype_path:\n            return cls._local_openpype_path\n\n        settings = get_openpype_global_settings(os.environ[\"OPENPYPE_MONGO\"])\n        data_dir = get_local_openpype_path_from_settings(settings)\n        if not data_dir:\n            data_dir = Path(user_data_dir(\"openpype\", \"pypeclub\"))\n        cls._local_openpype_path = data_dir\n        return data_dir\n\n    @classmethod\n    def openpype_path_is_set(cls):\n        \"\"\"Path to OpenPype zip directory is set.\"\"\"\n        if cls.get_openpype_path():\n            return True\n        return False\n\n    @classmethod\n    def openpype_path_is_accessible(cls):\n        \"\"\"Path to OpenPype zip directory is accessible.\n\n        Exists for this machine.\n        \"\"\"\n        # First check if is set\n        if not cls.openpype_path_is_set():\n            return False\n\n        # Validate existence\n        if Path(cls.get_openpype_path()).exists():\n            return True\n        return False\n\n    @classmethod\n    def get_local_versions(cls) -> List:\n        \"\"\"Get all versions available on this machine.\n\n        Returns:\n            list: of compatible versions available on the machine.\n\n        \"\"\"\n        dir_to_search = cls.get_local_openpype_path()\n        versions = cls.get_versions_from_directory(dir_to_search)\n\n        return list(sorted(set(versions)))\n\n    @classmethod\n    def get_remote_versions(cls) -> List:\n        \"\"\"Get all versions available in OpenPype Path.\n\n        Returns:\n            list of OpenPypeVersions: Versions found in OpenPype path.\n\n        \"\"\"\n        # Return all local versions if arguments are set to None\n\n        dir_to_search = None\n        if cls.openpype_path_is_accessible():\n            dir_to_search = Path(cls.get_openpype_path())\n        else:\n            registry = OpenPypeSettingsRegistry()\n            try:\n                registry_dir = Path(str(registry.get_item(\"openPypePath\")))\n                if registry_dir.exists():\n                    dir_to_search = registry_dir\n\n            except ValueError:\n                # nothing found in registry, we'll use data dir\n                pass\n\n        if not dir_to_search:\n            return []\n\n        versions = cls.get_versions_from_directory(dir_to_search)\n\n        return list(sorted(set(versions)))\n\n    @staticmethod\n    def get_versions_from_directory(\n            openpype_dir: Path) -> List:\n        \"\"\"Get all detected OpenPype versions in directory.\n\n        Args:\n            openpype_dir (Path): Directory to scan.\n\n        Returns:\n            list of OpenPypeVersion\n\n        Throws:\n            ValueError: if invalid path is specified.\n\n        \"\"\"\n        openpype_versions = []\n        if not openpype_dir.exists() and not openpype_dir.is_dir():\n            return openpype_versions\n\n        # iterate over directory in first level and find all that might\n        # contain OpenPype.\n        for item in openpype_dir.iterdir():\n            # if the item is directory with major.minor version, dive deeper\n\n            if item.is_dir() and re.match(r\"^\\d+\\.\\d+$\", item.name):\n                _versions = OpenPypeVersion.get_versions_from_directory(\n                    item)\n                if _versions:\n                    openpype_versions += _versions\n\n            # if file exists, strip extension, in case of dir don't.\n            name = item.name if item.is_dir() else item.stem\n            result = OpenPypeVersion.version_in_str(name)\n\n            if result:\n                detected_version: OpenPypeVersion\n                detected_version = result\n\n                if item.is_dir() and not OpenPypeVersion.is_version_in_dir(\n                        item, detected_version\n                )[0]:\n                    continue\n\n                if item.is_file() and not OpenPypeVersion.is_version_in_zip(\n                        item, detected_version\n                )[0]:\n                    continue\n\n                detected_version.path = item\n                openpype_versions.append(detected_version)\n\n        return sorted(openpype_versions)\n\n    @staticmethod\n    def get_installed_version_str() -> str:\n        \"\"\"Get version of local OpenPype.\"\"\"\n\n        version = {}\n        path = Path(os.environ[\"OPENPYPE_ROOT\"]) / \"openpype\" / \"version.py\"\n        with open(path, \"r\") as fp:\n            exec(fp.read(), version)\n        return version[\"__version__\"]\n\n    @classmethod\n    def get_installed_version(cls):\n        \"\"\"Get version of OpenPype inside build.\"\"\"\n        if cls._installed_version is None:\n            installed_version_str = cls.get_installed_version_str()\n            if installed_version_str:\n                cls._installed_version = OpenPypeVersion(\n                    version=installed_version_str,\n                    path=Path(os.environ[\"OPENPYPE_ROOT\"])\n                )\n        return cls._installed_version\n\n    @staticmethod\n    def get_latest_version(\n        local: bool = None,\n        remote: bool = None\n    ) -> Union[OpenPypeVersion, None]:\n        \"\"\"Get the latest available version.\n\n        The version does not contain information about path and source.\n\n        This is utility version to get the latest version from all found.\n\n        Arguments 'local' and 'remote' define if local and remote repository\n        versions are used. All versions are used if both are not set (or set\n        to 'None'). If only one of them is set to 'True' the other is disabled.\n        It is possible to set both to 'True' (same as both set to None) and to\n        'False' in that case only build version can be used.\n\n        Args:\n            local (bool, optional): List local versions if True.\n            remote (bool, optional): List remote versions if True.\n\n        Returns:\n            Latest OpenPypeVersion or None\n\n        \"\"\"\n        if local is None and remote is None:\n            local = True\n            remote = True\n\n        elif local is None and not remote:\n            local = True\n\n        elif remote is None and not local:\n            remote = True\n\n        installed_version = OpenPypeVersion.get_installed_version()\n        local_versions = OpenPypeVersion.get_local_versions() if local else []\n        remote_versions = OpenPypeVersion.get_remote_versions() if remote else []  # noqa: E501\n        all_versions = local_versions + remote_versions + [installed_version]\n\n        all_versions.sort()\n        return all_versions[-1]\n\n    @classmethod\n    def get_expected_studio_version(cls, staging=False, global_settings=None):\n        \"\"\"Expected OpenPype version that should be used at the moment.\n\n        If version is not defined in settings the latest found version is\n        used.\n\n        Using precached global settings is needed for usage inside OpenPype.\n\n        Args:\n            staging (bool): Staging version or production version.\n            global_settings (dict): Optional precached global settings.\n\n        Returns:\n            OpenPypeVersion: Version that should be used.\n        \"\"\"\n        result = get_expected_studio_version_str(staging, global_settings)\n        if not result:\n            return None\n        return OpenPypeVersion(version=result)\n\n    def is_compatible(self, version: OpenPypeVersion):\n        \"\"\"Test build compatibility.\n\n        This will simply compare major and minor versions (ignoring patch\n        and the rest).\n\n        Args:\n            version (OpenPypeVersion): Version to check compatibility with.\n\n        Returns:\n            bool: if the version is compatible\n\n        \"\"\"\n        return self.major == version.major and self.minor == version.minor\n\n\nclass BootstrapRepos:\n    \"\"\"Class for bootstrapping local OpenPype installation.\n\n    Attributes:\n        data_dir (Path): local OpenPype installation directory.\n        registry (OpenPypeSettingsRegistry): OpenPype registry object.\n        zip_filter (list): List of files to exclude from zip\n        openpype_filter (list): list of top level directories to\n            include in zip in OpenPype repository.\n\n    \"\"\"\n\n    def __init__(self, progress_callback: Callable = None, message=None):\n        \"\"\"Constructor.\n\n        Args:\n            progress_callback (callable): Optional callback method to report\n                progress.\n            message (QtCore.Signal, optional): Signal to report messages back.\n\n        \"\"\"\n        # vendor and app used to construct user data dir\n        self._message = message\n        self._log = log.getLogger(str(__class__))\n        self.set_data_dir(None)\n        self.secure_registry = OpenPypeSecureRegistry(\"mongodb\")\n        self.registry = OpenPypeSettingsRegistry()\n        self.zip_filter = [\".pyc\", \"__pycache__\"]\n        self.openpype_filter = [\n            \"openpype\", \"LICENSE\"\n        ]\n\n        # dummy progress reporter\n        def empty_progress(x: int):\n            \"\"\"Progress callback dummy.\"\"\"\n            return x\n\n        if not progress_callback:\n            progress_callback = empty_progress\n        self._progress_callback = progress_callback\n\n    def set_data_dir(self, data_dir):\n        if not data_dir:\n            self.data_dir = Path(user_data_dir(\"openpype\", \"pypeclub\"))\n        else:\n            self._print(f\"overriding local folder: {data_dir}\")\n            self.data_dir = data_dir\n\n    @staticmethod\n    def get_version_path_from_list(\n            version: str, version_list: list) -> Union[Path, None]:\n        \"\"\"Get path for specific version in list of OpenPype versions.\n\n        Args:\n            version (str): Version string to look for (1.2.4-nightly.1+test)\n            version_list (list of OpenPypeVersion): list of version to search.\n\n        Returns:\n            Path: Path to given version.\n\n        \"\"\"\n        for v in version_list:\n            if str(v) == version:\n                return v.path\n        return None\n\n    @staticmethod\n    def get_version(repo_dir: Path) -> Union[str, None]:\n        \"\"\"Get version of OpenPype in given directory.\n\n        Note: in frozen OpenPype installed in user data dir, this must point\n        one level deeper as it is:\n        `openpype-version-v3.0.0/openpype/version.py`\n\n        Args:\n            repo_dir (Path): Path to OpenPype repo.\n\n        Returns:\n            str: version string.\n            None: if OpenPype is not found.\n\n        \"\"\"\n        # try to find version\n        version_file = Path(repo_dir) / \"openpype\" / \"version.py\"\n        if not version_file.exists():\n            return None\n\n        version = {}\n        with version_file.open(\"r\") as fp:\n            exec(fp.read(), version)\n\n        return version['__version__']\n\n    def create_version_from_live_code(\n            self, repo_dir: Path = None) -> Union[OpenPypeVersion, None]:\n        \"\"\"Copy zip created from OpenPype repositories to user data dir.\n\n        This detects OpenPype version either in local \"live\" OpenPype\n        repository or in user provided path. Then it will zip it in temporary\n        directory, and finally it will move it to destination which is user\n        data directory. Existing files will be replaced.\n\n        Args:\n            repo_dir (Path, optional): Path to OpenPype repository.\n\n        Returns:\n            Path: path of installed repository file.\n\n        \"\"\"\n        # if repo dir is not set, we detect local \"live\" OpenPype repository\n        # version and use it as a source. Otherwise, repo_dir is user\n        # entered location.\n        if repo_dir:\n            version = self.get_version(repo_dir)\n        else:\n            installed_version = OpenPypeVersion.get_installed_version()\n            version = str(installed_version)\n            repo_dir = installed_version.path\n\n        if not version:\n            self._print(\"OpenPype not found.\", LOG_ERROR)\n            return\n\n        # create destination directory\n        destination = self.data_dir / f\"{installed_version.major}.{installed_version.minor}\"  # noqa\n        if not destination.exists():\n            destination.mkdir(parents=True)\n\n        # create zip inside temporary directory.\n        with tempfile.TemporaryDirectory() as temp_dir:\n            temp_zip = \\\n                Path(temp_dir) / f\"openpype-v{version}.zip\"\n            self._print(f\"creating zip: {temp_zip}\")\n\n            self._create_openpype_zip(temp_zip, repo_dir)\n            if not os.path.exists(temp_zip):\n                self._print(\"make archive failed.\", LOG_ERROR)\n                return None\n\n            destination = self._move_zip_to_data_dir(temp_zip)\n\n        return OpenPypeVersion(version=version, path=Path(destination))\n\n    def _move_zip_to_data_dir(self, zip_file) -> Union[None, Path]:\n        \"\"\"Move zip with OpenPype version to user data directory.\n\n        Args:\n            zip_file (Path): Path to zip file.\n\n        Returns:\n            None if move fails.\n            Path to moved zip on success.\n\n        \"\"\"\n        version = OpenPypeVersion.version_in_str(zip_file.name)\n        destination_dir = self.data_dir / f\"{version.major}.{version.minor}\"\n        if not destination_dir.exists():\n            destination_dir.mkdir(parents=True)\n        destination = destination_dir / zip_file.name\n\n        if destination.exists():\n            self._print(\n                f\"Destination file {destination} exists, removing.\",\n                LOG_WARNING)\n            try:\n                destination.unlink()\n            except Exception as e:\n                self._print(str(e), LOG_ERROR, exc_info=True)\n                return None\n        if not destination_dir.exists():\n            destination_dir.mkdir(parents=True)\n        elif not destination_dir.is_dir():\n            self._print(\n                \"Destination exists but is not directory.\", LOG_ERROR)\n            return None\n\n        try:\n            shutil.move(zip_file.as_posix(), destination_dir.as_posix())\n        except shutil.Error as e:\n            self._print(str(e), LOG_ERROR, exc_info=True)\n            return None\n\n        return destination\n\n    def _filter_dir(self, path: Path, path_filter: List) -> List[Path]:\n        \"\"\"Recursively crawl over path and filter.\"\"\"\n        result = []\n        for item in path.iterdir():\n            if item.name in path_filter:\n                continue\n            if item.name.startswith('.'):\n                continue\n            if item.is_dir():\n                result.extend(self._filter_dir(item, path_filter))\n            else:\n                result.append(item)\n        return result\n\n    def create_version_from_frozen_code(self) -> Union[None, OpenPypeVersion]:\n        \"\"\"Create OpenPype version from *frozen* code distributed by installer.\n\n        This should be real edge case for those wanting to try out OpenPype\n        without setting up whole infrastructure but is strongly discouraged\n        in studio setup as this use local version independent of others\n        that can be out of date.\n\n        Returns:\n            :class:`OpenPypeVersion` zip file to be installed.\n\n        \"\"\"\n        frozen_root = Path(sys.executable).parent\n\n        openpype_list = []\n        for f in self.openpype_filter:\n            if (frozen_root / f).is_dir():\n                openpype_list += self._filter_dir(\n                    frozen_root / f, self.zip_filter)\n            else:\n                openpype_list.append(frozen_root / f)\n\n        version = self.get_version(frozen_root)\n\n        # create zip inside temporary directory.\n        with tempfile.TemporaryDirectory() as temp_dir:\n            temp_zip = \\\n                Path(temp_dir) / f\"openpype-v{version}.zip\"\n            self._print(f\"creating zip: {temp_zip}\")\n\n            with ZipFile(temp_zip, \"w\") as zip_file:\n                progress = 0\n                openpype_inc = 98.0 / float(len(openpype_list))\n                file: Path\n                for file in openpype_list:\n                    progress += openpype_inc\n                    self._progress_callback(int(progress))\n\n                    arc_name = file.relative_to(frozen_root.parent)\n                    # we need to replace first part of path which starts with\n                    # something like `exe.win/linux....` with `openpype` as\n                    # this is expected by OpenPype in zip archive.\n                    arc_name = Path().joinpath(*arc_name.parts[1:])\n                    zip_file.write(file, arc_name)\n\n            destination = self._move_zip_to_data_dir(temp_zip)\n\n        return OpenPypeVersion(version=version, path=destination)\n\n    def _create_openpype_zip(self, zip_path: Path, openpype_path: Path) -> None:\n        \"\"\"Pack repositories and OpenPype into zip.\n\n        We are using :mod:`ZipFile` instead :meth:`shutil.make_archive`\n        because we need to decide what file and directories to include in zip\n        and what not. They are determined by :attr:`zip_filter` on file level\n        and :attr:`openpype_filter` on top level directory in OpenPype\n        repository.\n\n        Args:\n            zip_path (Path): Path to zip file.\n            openpype_path (Path): Path to OpenPype sources.\n\n        \"\"\"\n        # get filtered list of file in Pype repository\n        # openpype_list = self._filter_dir(openpype_path, self.zip_filter)\n        openpype_list = []\n        for f in self.openpype_filter:\n            if (openpype_path / f).is_dir():\n                openpype_list += self._filter_dir(\n                    openpype_path / f, self.zip_filter)\n            else:\n                openpype_list.append(openpype_path / f)\n\n        openpype_files = len(openpype_list)\n\n        openpype_inc = 98.0 / float(openpype_files)\n\n        with ZipFile(zip_path, \"w\") as zip_file:\n            progress = 0\n            openpype_root = openpype_path.resolve()\n            # generate list of filtered paths\n            dir_filter = [openpype_root / f for f in self.openpype_filter]\n            checksums = []\n\n            file: Path\n            for file in openpype_list:\n                progress += openpype_inc\n                self._progress_callback(int(progress))\n\n                # if file resides in filtered path, skip it\n                is_inside = None\n                df: Path\n                for df in dir_filter:\n                    try:\n                        is_inside = file.resolve().relative_to(df)\n                    except ValueError:\n                        pass\n\n                if not is_inside:\n                    continue\n\n                processed_path = file\n                self._print(f\"- processing {processed_path}\")\n\n                checksums.append(\n                    (\n                        sha256sum(sanitize_long_path(file.as_posix())),\n                        file.resolve().relative_to(openpype_root)\n                    )\n                )\n                zip_file.write(\n                    file, file.resolve().relative_to(openpype_root))\n\n            checksums_str = \"\"\n            for c in checksums:\n                file_str = c[1]\n                if platform.system().lower() == \"windows\":\n                    file_str = c[1].as_posix().replace(\"\\\\\", \"/\")\n                checksums_str += \"{}:{}\\n\".format(c[0], file_str)\n            zip_file.writestr(\"checksums\", checksums_str)\n            # test if zip is ok\n            zip_file.testzip()\n            self._progress_callback(100)\n\n    def validate_openpype_version(self, path: Path) -> tuple:\n        \"\"\"Validate version directory or zip file.\n\n        This will load `checksums` file if present, calculate checksums\n        of existing files in given path and compare. It will also compare\n        lists of files together for missing files.\n\n        Args:\n            path (Path): Path to OpenPype version to validate.\n\n        Returns:\n            tuple(bool, str): with version validity as first item\n                and string with reason as second.\n\n        \"\"\"\n        if os.getenv(\"OPENPYPE_DONT_VALIDATE_VERSION\"):\n            return True, \"Disabled validation\"\n        if not path.exists():\n            return False, \"Path doesn't exist\"\n\n        if path.is_file():\n            return self._validate_zip(path)\n        return self._validate_dir(path)\n\n    @staticmethod\n    def _validate_zip(path: Path) -> tuple:\n        \"\"\"Validate content of zip file.\"\"\"\n        with ZipFile(path, \"r\") as zip_file:\n            # read checksums\n            try:\n                checksums_data = str(zip_file.read(\"checksums\"))\n            except IOError:\n                # FIXME: This should be set to False sometimes in the future\n                return True, \"Cannot read checksums for archive.\"\n\n            # split it to the list of tuples\n            checksums = [\n                tuple(line.split(\":\"))\n                for line in checksums_data.split(\"\\n\") if line\n            ]\n\n            # get list of files in zip minus `checksums` file itself\n            # and turn in to set to compare against list of files\n            # from checksum file. If difference exists, something is\n            # wrong\n            files_in_zip = set(zip_file.namelist())\n            files_in_zip.remove(\"checksums\")\n            files_in_checksum = {file[1] for file in checksums}\n            diff = files_in_zip.difference(files_in_checksum)\n            if diff:\n                return False, f\"Missing files {diff}\"\n\n            # calculate and compare checksums in the zip file\n            for file_checksum, file_name in checksums:\n                if platform.system().lower() == \"windows\":\n                    file_name = file_name.replace(\"/\", \"\\\\\")\n                h = hashlib.sha256()\n                try:\n                    h.update(zip_file.read(file_name))\n                except FileNotFoundError:\n                    return False, f\"Missing file [ {file_name} ]\"\n                if h.hexdigest() != file_checksum:\n                    return False, f\"Invalid checksum on {file_name}\"\n\n        return True, \"All ok\"\n\n    @staticmethod\n    def _validate_dir(path: Path) -> tuple:\n        \"\"\"Validate checksums in a given path.\n\n        Args:\n            path (Path): path to folder to validate.\n\n        Returns:\n            tuple(bool, str): returns status and reason as a bool\n                and str in a tuple.\n\n        \"\"\"\n        checksums_file = Path(path / \"checksums\")\n        if not checksums_file.exists():\n            # FIXME: This should be set to False sometimes in the future\n            return True, \"Cannot read checksums for archive.\"\n        checksums_data = checksums_file.read_text()\n        checksums = [\n            tuple(line.split(\":\"))\n            for line in checksums_data.split(\"\\n\") if line\n        ]\n\n        # compare file list against list of files from checksum file.\n        # If difference exists, something is wrong and we invalidate directly\n        files_in_dir = set(\n            file.relative_to(path).as_posix()\n            for file in path.iterdir() if file.is_file()\n        )\n        files_in_dir.remove(\"checksums\")\n        files_in_checksum = {file[1] for file in checksums}\n\n        diff = files_in_dir.difference(files_in_checksum)\n        if diff:\n            return False, f\"Missing files {diff}\"\n\n        # calculate and compare checksums\n        for file_checksum, file_name in checksums:\n            if platform.system().lower() == \"windows\":\n                file_name = file_name.replace(\"/\", \"\\\\\")\n            try:\n                current = sha256sum(\n                    sanitize_long_path((path / file_name).as_posix())\n                )\n            except FileNotFoundError:\n                return False, f\"Missing file [ {file_name} ]\"\n\n            if file_checksum != current:\n                return False, f\"Invalid checksum on {file_name}\"\n\n        return True, \"All ok\"\n\n    @staticmethod\n    def add_paths_from_archive(archive: Path) -> None:\n        \"\"\"Add first-level directory and 'repos' as paths to :mod:`sys.path`.\n\n        This will enable Python to import OpenPype and modules in `repos`\n        submodule directory in zip file.\n\n        Adding to both `sys.path` and `PYTHONPATH`, skipping duplicates.\n\n        Args:\n            archive (Path): path to archive.\n\n        .. deprecated:: 3.0\n            we don't use zip archives directly\n\n        \"\"\"\n        if not archive.is_file() and not archive.exists():\n            raise ValueError(\"Archive is not file.\")\n\n        archive_path = str(archive)\n        sys.path.insert(0, archive_path)\n        pythonpath = os.getenv(\"PYTHONPATH\", \"\")\n        python_paths = pythonpath.split(os.pathsep)\n        python_paths.insert(0, archive_path)\n\n        os.environ[\"PYTHONPATH\"] = os.pathsep.join(python_paths)\n\n    @staticmethod\n    def add_paths_from_directory(directory: Path) -> None:\n        \"\"\"Add repos first level directories as paths to :mod:`sys.path`.\n\n        This works the same as :meth:`add_paths_from_archive` but in\n        specified directory.\n\n        Adding to both `sys.path` and `PYTHONPATH`, skipping duplicates.\n\n        Args:\n            directory (Path): path to directory.\n\n        \"\"\"\n\n        sys.path.insert(0, directory.as_posix())\n\n    @staticmethod\n    def find_openpype_version(\n            version: Union[str, OpenPypeVersion]\n    ) -> Union[OpenPypeVersion, None]:\n        \"\"\"Find location of specified OpenPype version.\n\n        Args:\n            version (Union[str, OpenPypeVersion): Version to find.\n\n        Returns:\n            requested OpenPypeVersion.\n\n        \"\"\"\n        installed_version = OpenPypeVersion.get_installed_version()\n        if isinstance(version, str):\n            version = OpenPypeVersion(version=version)\n\n        if installed_version == version:\n            return installed_version\n\n        local_versions = OpenPypeVersion.get_local_versions()\n        zip_version = None\n        for local_version in local_versions:\n            if local_version == version:\n                if local_version.path.suffix.lower() == \".zip\":\n                    zip_version = local_version\n                else:\n                    return local_version\n\n        if zip_version is not None:\n            return zip_version\n\n        remote_versions = OpenPypeVersion.get_remote_versions()\n        return next(\n            (\n                remote_version for remote_version in remote_versions\n                if remote_version == version\n            ), None)\n\n    @staticmethod\n    def find_latest_openpype_version() -> Union[OpenPypeVersion, None]:\n        \"\"\"Find the latest available OpenPype version in all location.\n\n        Returns:\n            Latest OpenPype version on None if nothing was found.\n\n        \"\"\"\n        installed_version = OpenPypeVersion.get_installed_version()\n        local_versions = OpenPypeVersion.get_local_versions()\n        remote_versions = OpenPypeVersion.get_remote_versions()\n        all_versions = local_versions + remote_versions + [installed_version]\n\n        if not all_versions:\n            return None\n\n        all_versions.sort()\n        latest_version = all_versions[-1]\n        if latest_version == installed_version:\n            return latest_version\n\n        if not latest_version.path.is_dir():\n            for version in local_versions:\n                if version == latest_version and version.path.is_dir():\n                    latest_version = version\n                    break\n        return latest_version\n\n    def find_openpype(\n            self,\n            openpype_path: Union[Path, str] = None,\n            include_zips: bool = False\n    ) -> Union[List[OpenPypeVersion], None]:\n        \"\"\"Get ordered dict of detected OpenPype version.\n\n        Resolution order for OpenPype is following:\n\n            1) First we test for ``OPENPYPE_PATH`` environment variable\n            2) We try to find ``openPypePath`` in registry setting\n            3) We use user data directory\n\n        Args:\n            openpype_path (Path or str, optional): Try to find OpenPype on\n                the given path or url.\n            include_zips (bool, optional): If set True it will try to find\n                OpenPype in zip files in given directory.\n\n        Returns:\n            dict of Path: Dictionary of detected OpenPype version.\n                 Key is version, value is path to zip file.\n\n            None: if OpenPype is not found.\n\n        Todo:\n            implement git/url support as OpenPype location, so it would be\n            possible to enter git url, OpenPype would check it out and if it is\n            ok install it as normal version.\n\n        \"\"\"\n        if openpype_path and not isinstance(openpype_path, Path):\n            raise NotImplementedError(\n                (\"Finding OpenPype in non-filesystem locations is\"\n                 \" not implemented yet.\"))\n\n        # if checks bellow for OPENPYPE_PATH and registry fails, use data_dir\n        # DEPRECATED: lookup in root of this folder is deprecated in favour\n        #             of major.minor sub-folders.\n        dirs_to_search = [self.data_dir]\n\n        if openpype_path:\n            dirs_to_search = [openpype_path]\n        elif os.getenv(\"OPENPYPE_PATH\") \\\n                and Path(os.getenv(\"OPENPYPE_PATH\")).exists():\n            # first try OPENPYPE_PATH and if that is not available,\n            # try registry.\n            dirs_to_search = [Path(os.getenv(\"OPENPYPE_PATH\"))]\n        else:\n            try:\n                registry_dir = Path(\n                    str(self.registry.get_item(\"openPypePath\")))\n                if registry_dir.exists():\n                    dirs_to_search = [registry_dir]\n\n            except ValueError:\n                # nothing found in registry, we'll use data dir\n                pass\n\n        openpype_versions = []\n        for dir_to_search in dirs_to_search:\n            try:\n                openpype_versions += self.get_openpype_versions(\n                    dir_to_search)\n            except ValueError:\n                # location is invalid, skip it\n                pass\n\n        if not include_zips:\n            openpype_versions = [\n                v for v in openpype_versions if v.path.suffix != \".zip\"\n            ]\n\n        # remove duplicates\n        openpype_versions = sorted(list(set(openpype_versions)))\n\n        return openpype_versions\n\n    def process_entered_location(self, location: str) -> Union[Path, None]:\n        \"\"\"Process user entered location string.\n\n        It decides if location string is mongodb url or path.\n        If it is mongodb url, it will connect and load ``OPENPYPE_PATH`` from\n        there and use it as path to OpenPype. In it is _not_ mongodb url, it\n        is assumed we have a path, this is tested and zip file is\n        produced and installed using :meth:`create_version_from_live_code`.\n\n        Args:\n            location (str): User entered location.\n\n        Returns:\n            Path: to OpenPype zip produced from this location.\n            None: Zipping failed.\n\n        \"\"\"\n        openpype_path = None\n        # try to get OpenPype path from mongo.\n        if location.startswith(\"mongodb\"):\n            global_settings = get_openpype_global_settings(location)\n            openpype_path = get_openpype_path_from_settings(global_settings)\n            if not openpype_path:\n                self._print(\"cannot find OPENPYPE_PATH in settings.\")\n                return None\n\n        # if not successful, consider location to be fs path.\n        if not openpype_path:\n            openpype_path = Path(location)\n\n        # test if this path does exist.\n        if not openpype_path.exists():\n            self._print(f\"{openpype_path} doesn't exists.\")\n            return None\n\n        # test if entered path isn't user data dir\n        if self.data_dir == openpype_path:\n            self._print(\"cannot point to user data dir\", LOG_ERROR)\n            return None\n\n        # find openpype zip files in location. There can be\n        # either \"live\" OpenPype repository, or multiple zip files or even\n        # multiple OpenPype version directories. This process looks into zip\n        # files and directories and tries to parse `version.py` file.\n        versions = self.find_openpype(openpype_path, include_zips=True)\n        if versions:\n            self._print(f\"found OpenPype in [ {openpype_path} ]\")\n            self._print(f\"latest version found is [ {versions[-1]} ]\")\n\n            return self.install_version(versions[-1])\n\n        # if we got here, it means that location is \"live\"\n        # OpenPype repository. We'll create zip from it and move it to user\n        # data dir.\n        live_openpype = self.create_version_from_live_code(openpype_path)\n        if not live_openpype.path.exists():\n            self._print(f\"installing zip {live_openpype} failed.\", LOG_ERROR)\n            return None\n        # install it\n        return self.install_version(live_openpype)\n\n    def _print(self,\n               message: str,\n               level: int = LOG_INFO,\n               exc_info: bool = False):\n        \"\"\"Helper function passing logs to UI and to logger.\n\n        Supporting 3 levels of logs defined with `LOG_INFO`, `LOG_WARNING` and\n        `LOG_ERROR` constants.\n\n        Args:\n            message (str): Message to log.\n            level (int, optional): Log level to use.\n            exc_info (bool, optional): Exception info object to pass to logger.\n\n        \"\"\"\n        if self._message:\n            self._message.emit(message, level == LOG_ERROR)\n\n        if level == LOG_WARNING:\n            self._log.warning(message, exc_info=exc_info)\n            return\n        if level == LOG_ERROR:\n            self._log.error(message, exc_info=exc_info)\n            return\n        self._log.info(message, exc_info=exc_info)\n\n    def extract_openpype(self, version: OpenPypeVersion) -> Union[Path, None]:\n        \"\"\"Extract zipped OpenPype version to user data directory.\n\n        Args:\n            version (OpenPypeVersion): Version of OpenPype.\n\n        Returns:\n            Path: path to extracted version.\n            None: if something failed.\n\n        \"\"\"\n        if not version.path:\n            raise ValueError(\n                f\"version {version} is not associated with any file\")\n\n        destination = self.data_dir / f\"{version.major}.{version.minor}\" / version.path.stem  # noqa\n        if destination.exists() and destination.is_dir():\n            try:\n                shutil.rmtree(destination)\n            except OSError as e:\n                msg = f\"!!! Cannot remove already existing {destination}\"\n                self._print(msg, LOG_ERROR, exc_info=True)\n                raise e\n\n        destination.mkdir(parents=True)\n\n        # extract zip there\n        self._print(\"Extracting zip to destination ...\")\n        with ZipFileLongPaths(version.path, \"r\") as zip_ref:\n            zip_ref.extractall(destination)\n\n        self._print(f\"Installed as {version.path.stem}\")\n\n        return destination\n\n    def is_inside_user_data(self, path: Path) -> bool:\n        \"\"\"Test if version is located in user data dir.\n\n        Args:\n            path (Path) Path to test.\n\n        Returns:\n            True if path is inside user data dir.\n\n        \"\"\"\n        is_inside = False\n        try:\n            is_inside = path.resolve().relative_to(\n                self.data_dir)\n        except ValueError:\n            # if relative path cannot be calculated, OpenPype version is not\n            # inside user data dir\n            pass\n        return is_inside\n\n    def install_version(self,\n                        openpype_version: OpenPypeVersion,\n                        force: bool = False) -> Path:\n        \"\"\"Install OpenPype version to user data directory.\n\n        Args:\n            openpype_version (OpenPypeVersion): OpenPype version to install.\n            force (bool, optional): Force overwrite existing version.\n\n        Returns:\n            Path: Path to installed OpenPype.\n\n        Raises:\n            OpenPypeVersionExists: If not forced and this version already exist\n                in user data directory.\n            OpenPypeVersionInvalid: If version to install is invalid.\n            OpenPypeVersionIOError: If copying or zipping fail.\n\n        \"\"\"\n        if self.is_inside_user_data(openpype_version.path) and not openpype_version.path.is_file():  # noqa\n            raise OpenPypeVersionExists(\n                \"OpenPype already inside user data dir\")\n\n        # determine destination directory name\n        # for zip file strip suffix, in case of dir use whole dir name\n        if openpype_version.path.is_dir():\n            dir_name = openpype_version.path.name\n        else:\n            dir_name = openpype_version.path.stem\n\n        destination = self.data_dir / f\"{openpype_version.major}.{openpype_version.minor}\" / dir_name  # noqa\n\n        # test if destination directory already exist, if so lets delete it.\n        if destination.exists() and force:\n            self._print(\"removing existing directory\")\n            try:\n                shutil.rmtree(destination)\n            except OSError as e:\n                self._print(\n                    f\"cannot remove already existing {destination}\",\n                    LOG_ERROR, exc_info=True)\n                raise OpenPypeVersionIOError(\n                    f\"cannot remove existing {destination}\") from e\n        elif destination.exists() and not force:\n            self._print(\"destination directory already exists\")\n            raise OpenPypeVersionExists(f\"{destination} already exist.\")\n        else:\n            # create destination parent directories even if they don't exist.\n            destination.mkdir(parents=True)\n\n        remove_source_file = False\n        # version is directory\n        if openpype_version.path.is_dir():\n            # create zip inside temporary directory.\n            self._print(\"Creating zip from directory ...\")\n            self._progress_callback(0)\n            with tempfile.TemporaryDirectory() as temp_dir:\n                temp_zip = \\\n                    Path(temp_dir) / f\"openpype-v{openpype_version}.zip\"\n                self._print(f\"creating zip: {temp_zip}\")\n\n                self._create_openpype_zip(temp_zip, openpype_version.path)\n                if not os.path.exists(temp_zip):\n                    self._print(\"make archive failed.\", LOG_ERROR)\n                    raise OpenPypeVersionIOError(\"Zip creation failed.\")\n\n                # set zip as version source\n                openpype_version.path = temp_zip\n\n                if self.is_inside_user_data(openpype_version.path):\n                    raise OpenPypeVersionInvalid(\n                        \"Version is in user data dir.\")\n                openpype_version.path = self._copy_zip(\n                    openpype_version.path, destination)\n\n        elif openpype_version.path.is_file():\n            # check if file is zip (by extension)\n            if openpype_version.path.suffix.lower() != \".zip\":\n                raise OpenPypeVersionInvalid(\"Invalid file format\")\n\n            if not self.is_inside_user_data(openpype_version.path):\n                self._progress_callback(35)\n                openpype_version.path = self._copy_zip(\n                    openpype_version.path, destination)\n                # Mark zip to be deleted when done\n                remove_source_file = True\n\n        # extract zip there\n        self._print(\"extracting zip to destination ...\")\n        with ZipFileLongPaths(openpype_version.path, \"r\") as zip_ref:\n            self._progress_callback(75)\n            zip_ref.extractall(destination)\n            self._progress_callback(100)\n\n        # Remove zip file copied to local app data\n        if remove_source_file:\n            os.remove(openpype_version.path)\n\n        return destination\n\n    def _copy_zip(self, source: Path, destination: Path) -> Path:\n        try:\n            # copy file to destination\n            self._print(\"Copying zip to destination ...\")\n            _destination_zip = destination.parent / source.name  # noqa: E501\n            copyfile(\n                source.as_posix(),\n                _destination_zip.as_posix())\n        except OSError as e:\n            self._print(\n                \"cannot copy version to user data directory\", LOG_ERROR,\n                exc_info=True)\n            raise OpenPypeVersionIOError((\n                f\"can't copy version {source.as_posix()} \"\n                f\"to destination {destination.parent.as_posix()}\")) from e\n        return _destination_zip\n\n    def _is_openpype_in_dir(self,\n                            dir_item: Path,\n                            detected_version: OpenPypeVersion) -> bool:\n        \"\"\"Test if path item is OpenPype version matching detected version.\n\n        If item is directory that might (based on it's name)\n        contain OpenPype version, check if it really does contain\n        OpenPype and that their versions matches.\n\n        Args:\n            dir_item (Path): Directory to test.\n            detected_version (OpenPypeVersion): OpenPype version detected\n                from name.\n\n        Returns:\n            True if it is valid OpenPype version, False otherwise.\n\n        \"\"\"\n        try:\n            # add one 'openpype' level as inside dir there should\n            # be many other repositories.\n            version_str = BootstrapRepos.get_version(dir_item)\n            version_check = OpenPypeVersion(version=version_str)\n        except ValueError:\n            self._print(\n                f\"cannot determine version from {dir_item}\", True)\n            return False\n\n        version_main = version_check.get_main_version()\n        detected_main = detected_version.get_main_version()\n        if version_main != detected_main:\n            self._print(\n                (f\"dir version ({detected_version}) and \"\n                 f\"its content version ({version_check}) \"\n                 \"doesn't match. Skipping.\"))\n            return False\n        return True\n\n    def _is_openpype_in_zip(self,\n                            zip_item: Path,\n                            detected_version: OpenPypeVersion) -> bool:\n        \"\"\"Test if zip path is OpenPype version matching detected version.\n\n        Open zip file, look inside and parse version from OpenPype\n        inside it. If there is none, or it is different from\n        version specified in file name, skip it.\n\n        Args:\n            zip_item (Path): Zip file to test.\n            detected_version (OpenPypeVersion): Pype version detected from\n                name.\n\n        Returns:\n           True if it is valid OpenPype version, False otherwise.\n\n        \"\"\"\n        # skip non-zip files\n        if zip_item.suffix.lower() != \".zip\":\n            return False\n\n        try:\n            with ZipFile(zip_item, \"r\") as zip_file:\n                with zip_file.open(\n                        \"openpype/version.py\") as version_file:\n                    zip_version = {}\n                    exec(version_file.read(), zip_version)\n                    try:\n                        version_check = OpenPypeVersion(\n                            version=zip_version[\"__version__\"])\n                    except ValueError as e:\n                        self._print(str(e), True)\n                        return False\n\n                    version_main = version_check.get_main_version()  # noqa: E501\n                    detected_main = detected_version.get_main_version()  # noqa: E501\n\n                    if version_main != detected_main:\n                        self._print(\n                            (f\"zip version ({detected_version}) \"\n                             f\"and its content version \"\n                             f\"({version_check}) \"\n                             \"doesn't match. Skipping.\"), True)\n                        return False\n        except BadZipFile:\n            self._print(f\"{zip_item} is not a zip file\", True)\n            return False\n        except KeyError:\n            self._print(\"Zip does not contain OpenPype\", True)\n            return False\n        return True\n\n    def get_openpype_versions(self, openpype_dir: Path) -> list:\n        \"\"\"Get all detected OpenPype versions in directory.\n\n        Args:\n            openpype_dir (Path): Directory to scan.\n\n        Returns:\n            list of OpenPypeVersion\n\n        Throws:\n            ValueError: if invalid path is specified.\n\n        \"\"\"\n        if not openpype_dir.exists() and not openpype_dir.is_dir():\n            raise ValueError(f\"specified directory {openpype_dir} is invalid\")\n\n        openpype_versions = []\n        # iterate over directory in first level and find all that might\n        # contain OpenPype.\n        for item in openpype_dir.iterdir():\n            # if the item is directory with major.minor version, dive deeper\n            if item.is_dir() and re.match(r\"^\\d+\\.\\d+$\", item.name):\n                _versions = self.get_openpype_versions(item)\n                if _versions:\n                    openpype_versions += _versions\n\n            # if it is file, strip extension, in case of dir don't.\n            name = item.name if item.is_dir() else item.stem\n            result = OpenPypeVersion.version_in_str(name)\n\n            if result:\n                detected_version: OpenPypeVersion\n                detected_version = result\n\n                if item.is_dir() and not self._is_openpype_in_dir(\n                    item, detected_version\n                ):\n                    continue\n\n                if item.is_file() and not self._is_openpype_in_zip(\n                    item, detected_version\n                ):\n                    continue\n\n                detected_version.path = item\n                openpype_versions.append(detected_version)\n\n        return sorted(openpype_versions)\n\n\nclass OpenPypeVersionExists(Exception):\n    \"\"\"Exception for handling existing OpenPype version.\"\"\"\n    pass\n\n\nclass OpenPypeVersionInvalid(Exception):\n    \"\"\"Exception for handling invalid OpenPype version.\"\"\"\n    pass\n\n\nclass OpenPypeVersionIOError(Exception):\n    \"\"\"Exception for handling IO errors in OpenPype version.\"\"\"\n    pass\n"
  },
  {
    "path": "igniter/install_dialog.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Show dialog for choosing central pype repository.\"\"\"\nimport os\nimport sys\nimport re\nimport collections\n\nfrom qtpy import QtCore, QtGui, QtWidgets\n\nfrom .install_thread import InstallThread\nfrom .tools import (\n    validate_mongo_connection,\n    get_openpype_icon_path\n)\n\nfrom .nice_progress_bar import NiceProgressBar\nfrom .user_settings import OpenPypeSecureRegistry\nfrom .tools import load_stylesheet\nfrom .version import __version__\n\n\nclass ButtonWithOptions(QtWidgets.QFrame):\n    option_clicked = QtCore.Signal(str)\n\n    def __init__(self, commands, parent=None):\n        super(ButtonWithOptions, self).__init__(parent)\n\n        self.setObjectName(\"ButtonWithOptions\")\n\n        options_btn = QtWidgets.QToolButton(self)\n        options_btn.setArrowType(QtCore.Qt.DownArrow)\n        options_btn.setIconSize(QtCore.QSize(12, 12))\n\n        default = None\n        default_label = None\n        options_menu = QtWidgets.QMenu(self)\n        for option, option_label in commands.items():\n            if default is None:\n                default = option\n                default_label = option_label\n                continue\n            action = QtWidgets.QAction(option_label, options_menu)\n            action.setData(option)\n            options_menu.addAction(action)\n\n        main_btn = QtWidgets.QPushButton(default_label, self)\n        main_btn.setFlat(True)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(1)\n\n        main_layout.addWidget(main_btn, 1, QtCore.Qt.AlignVCenter)\n        main_layout.addWidget(options_btn, 0, QtCore.Qt.AlignVCenter)\n\n        main_btn.clicked.connect(self._on_main_button)\n        options_btn.clicked.connect(self._on_options_click)\n        options_menu.triggered.connect(self._on_trigger)\n\n        self.main_btn = main_btn\n        self.options_btn = options_btn\n        self.options_menu = options_menu\n\n        options_btn.setEnabled(not options_menu.isEmpty())\n\n        self._default_value = default\n\n    def resizeEvent(self, event):\n        super(ButtonWithOptions, self).resizeEvent(event)\n        self.options_btn.setFixedHeight(self.main_btn.height())\n\n    def _on_options_click(self):\n        pos = self.main_btn.rect().bottomLeft()\n        point = self.main_btn.mapToGlobal(pos)\n        self.options_menu.popup(point)\n\n    def _on_trigger(self, action):\n        self.option_clicked.emit(action.data())\n\n    def _on_main_button(self):\n        self.option_clicked.emit(self._default_value)\n\n\nclass ConsoleWidget(QtWidgets.QWidget):\n    def __init__(self, parent=None):\n        super(ConsoleWidget, self).__init__(parent)\n\n        # style for normal and error console text\n        default_console_style = QtGui.QTextCharFormat()\n        error_console_style = QtGui.QTextCharFormat()\n        default_console_style.setForeground(\n            QtGui.QColor.fromRgb(72, 200, 150)\n        )\n        error_console_style.setForeground(\n            QtGui.QColor.fromRgb(184, 54, 19)\n        )\n\n        label = QtWidgets.QLabel(\"Console:\", self)\n\n        console_output = QtWidgets.QPlainTextEdit(self)\n        console_output.setMinimumSize(QtCore.QSize(300, 200))\n        console_output.setReadOnly(True)\n        console_output.setCurrentCharFormat(default_console_style)\n        console_output.setObjectName(\"Console\")\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(label, 0)\n        main_layout.addWidget(console_output, 1)\n\n        self.default_console_style = default_console_style\n        self.error_console_style = error_console_style\n\n        self.label = label\n        self.console_output = console_output\n\n        self.hide_console()\n\n    def hide_console(self):\n        self.label.setVisible(False)\n        self.console_output.setVisible(False)\n\n        self.updateGeometry()\n\n    def show_console(self):\n        self.label.setVisible(True)\n        self.console_output.setVisible(True)\n\n        self.updateGeometry()\n\n    def update_console(self, msg: str, error: bool = False) -> None:\n        if not error:\n            self.console_output.setCurrentCharFormat(\n                self.default_console_style\n            )\n        else:\n            self.console_output.setCurrentCharFormat(\n                self.error_console_style\n            )\n        self.console_output.appendPlainText(msg)\n\n\nclass MongoUrlInput(QtWidgets.QLineEdit):\n    \"\"\"Widget to input mongodb URL.\"\"\"\n\n    def set_valid(self):\n        \"\"\"Set valid state on mongo url input.\"\"\"\n        self.setProperty(\"state\", \"valid\")\n        self.style().polish(self)\n\n    def remove_state(self):\n        \"\"\"Set invalid state on mongo url input.\"\"\"\n        self.setProperty(\"state\", \"\")\n        self.style().polish(self)\n\n    def set_invalid(self):\n        \"\"\"Set invalid state on mongo url input.\"\"\"\n        self.setProperty(\"state\", \"invalid\")\n        self.style().polish(self)\n\n\nclass InstallDialog(QtWidgets.QDialog):\n    \"\"\"Main Igniter dialog window.\"\"\"\n\n    mongo_url_regex = re.compile(r\"^(mongodb|mongodb\\+srv)://.*?\")\n\n    _width = 500\n    _height = 200\n    commands = collections.OrderedDict([\n        (\"run\", \"Start\"),\n        (\"run_from_code\", \"Run from code\")\n    ])\n\n    def __init__(self, parent=None):\n        super(InstallDialog, self).__init__(parent)\n\n        self.setWindowTitle(\n            f\"OpenPype Igniter {__version__}\"\n        )\n        self.setWindowFlags(\n            QtCore.Qt.WindowCloseButtonHint\n            | QtCore.Qt.WindowMinimizeButtonHint\n        )\n\n        current_dir = os.path.dirname(os.path.abspath(__file__))\n        roboto_font_path = os.path.join(current_dir, \"RobotoMono-Regular.ttf\")\n        poppins_font_path = os.path.join(current_dir, \"Poppins\")\n\n        # Install roboto font\n        QtGui.QFontDatabase.addApplicationFont(roboto_font_path)\n        for filename in os.listdir(poppins_font_path):\n            if os.path.splitext(filename)[1] == \".ttf\":\n                QtGui.QFontDatabase.addApplicationFont(filename)\n\n        # Load logo\n        icon_path = get_openpype_icon_path()\n        pixmap_openpype_logo = QtGui.QPixmap(icon_path)\n        # Set logo as icon of window\n        self.setWindowIcon(QtGui.QIcon(pixmap_openpype_logo))\n\n        secure_registry = OpenPypeSecureRegistry(\"mongodb\")\n        mongo_url = \"\"\n        try:\n            mongo_url = (\n                os.getenv(\"OPENPYPE_MONGO\", \"\")\n                or secure_registry.get_item(\"openPypeMongo\")\n            )\n        except ValueError:\n            pass\n\n        self.mongo_url = mongo_url\n        self._pixmap_openpype_logo = pixmap_openpype_logo\n\n        self._secure_registry = secure_registry\n        self._controls_disabled = False\n        self._install_thread = None\n\n        self.resize(QtCore.QSize(self._width, self._height))\n        self._init_ui()\n\n        # Set stylesheet\n        self.setStyleSheet(load_stylesheet())\n\n        # Trigger Mongo URL validation\n        self._mongo_input.setText(self.mongo_url)\n\n    def _init_ui(self):\n        # basic visual style - dark background, light text\n\n        # Main info\n        # --------------------------------------------------------------------\n        main_label = QtWidgets.QLabel(\"Welcome to <b>OpenPype</b>\", self)\n        main_label.setWordWrap(True)\n        main_label.setObjectName(\"MainLabel\")\n\n        # Mongo box | OK button\n        # --------------------------------------------------------------------\n        mongo_input = MongoUrlInput(self)\n        mongo_input.setPlaceholderText(\n            \"Enter your database Address. Example: mongodb://192.168.1.10:2707\"\n        )\n\n        mongo_messages_widget = QtWidgets.QWidget(self)\n\n        mongo_connection_msg = QtWidgets.QLabel(mongo_messages_widget)\n        mongo_connection_msg.setVisible(True)\n        mongo_connection_msg.setTextInteractionFlags(\n            QtCore.Qt.TextSelectableByMouse\n        )\n\n        mongo_messages_layout = QtWidgets.QVBoxLayout(mongo_messages_widget)\n        mongo_messages_layout.setContentsMargins(0, 0, 0, 0)\n        mongo_messages_layout.addWidget(mongo_connection_msg)\n\n        # Progress bar\n        # --------------------------------------------------------------------\n        progress_bar = NiceProgressBar(self)\n        progress_bar.setAlignment(QtCore.Qt.AlignCenter)\n        progress_bar.setTextVisible(False)\n\n        # Console\n        # --------------------------------------------------------------------\n        console_widget = ConsoleWidget(self)\n\n        # Bottom button bar\n        # --------------------------------------------------------------------\n        bottom_widget = QtWidgets.QWidget(self)\n\n        btns_widget = QtWidgets.QWidget(bottom_widget)\n\n        openpype_logo_label = QtWidgets.QLabel(\"openpype logo\", bottom_widget)\n        openpype_logo_label.setPixmap(self._pixmap_openpype_logo)\n\n        run_button = ButtonWithOptions(\n            self.commands,\n            btns_widget\n        )\n        run_button.setMinimumSize(64, 24)\n        run_button.setToolTip(\"Run OpenPype\")\n\n        # install button - - - - - - - - - - - - - - - - - - - - - - - - - - -\n        exit_button = QtWidgets.QPushButton(\"Exit\", btns_widget)\n        exit_button.setObjectName(\"ExitBtn\")\n        exit_button.setFlat(True)\n        exit_button.setMinimumSize(64, 24)\n        exit_button.setToolTip(\"Exit\")\n\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n        btns_layout.addWidget(run_button, 0)\n        btns_layout.addWidget(exit_button, 0)\n\n        bottom_layout = QtWidgets.QHBoxLayout(bottom_widget)\n        bottom_layout.setContentsMargins(0, 0, 0, 0)\n        bottom_layout.setAlignment(QtCore.Qt.AlignHCenter)\n        bottom_layout.addWidget(openpype_logo_label, 0)\n        bottom_layout.addStretch(1)\n        bottom_layout.addWidget(btns_widget, 0)\n\n        # add all to main\n        main = QtWidgets.QVBoxLayout(self)\n        main.addSpacing(15)\n        main.addWidget(main_label, 0)\n        main.addSpacing(15)\n        main.addWidget(mongo_input, 0)\n        main.addWidget(mongo_messages_widget, 0)\n\n        main.addWidget(progress_bar, 0)\n        main.addSpacing(15)\n\n        main.addWidget(console_widget, 1)\n\n        main.addWidget(bottom_widget, 0)\n\n        run_button.option_clicked.connect(self._on_run_btn_click)\n        exit_button.clicked.connect(self._on_exit_clicked)\n        mongo_input.textChanged.connect(self._on_mongo_url_change)\n\n        self._console_widget = console_widget\n\n        self.main_label = main_label\n\n        self._mongo_input = mongo_input\n\n        self._mongo_connection_msg = mongo_connection_msg\n\n        self._run_button = run_button\n        self._exit_button = exit_button\n        self._progress_bar = progress_bar\n\n    def _on_run_btn_click(self, option):\n        # Disable buttons\n        self._disable_buttons()\n        # Set progress to any value\n        self._update_progress(1)\n        self._progress_bar.repaint()\n        # Add label to show that is connecting to mongo\n        self.set_invalid_mongo_connection(self.mongo_url, True)\n\n        # Process events to repaint changes\n        QtWidgets.QApplication.processEvents()\n\n        if not self.validate_url():\n            self._enable_buttons()\n            self._update_progress(0)\n            # Update any messages\n            self._mongo_input.setText(self.mongo_url)\n            return\n\n        if option == \"run\":\n            self._run_openpype()\n        elif option == \"run_from_code\":\n            self._run_openpype_from_code()\n        else:\n            raise AssertionError(\"BUG: Unknown variant \\\"{}\\\"\".format(option))\n\n    def _run_openpype_from_code(self):\n        os.environ[\"OPENPYPE_MONGO\"] = self.mongo_url\n        try:\n            self._secure_registry.set_item(\"openPypeMongo\", self.mongo_url)\n        except ValueError:\n            print(\"Couldn't save Mongo URL to keyring\")\n\n        self.done(2)\n\n    def _run_openpype(self):\n        \"\"\"Start install process.\n\n        This will once again validate entered path and mongo if ok, start\n        working thread that will do actual job.\n        \"\"\"\n        # Check if install thread is not already running\n        if self._install_thread and self._install_thread.isRunning():\n            return\n\n        self._mongo_input.set_valid()\n\n        install_thread = InstallThread(self)\n        install_thread.message.connect(self.update_console)\n        install_thread.progress.connect(self._update_progress)\n        install_thread.finished.connect(self._installation_finished)\n        install_thread.set_mongo(self.mongo_url)\n\n        self._install_thread = install_thread\n\n        install_thread.start()\n\n    def _installation_finished(self):\n        # TODO we should find out why status can be set to 'None'?\n        # - 'InstallThread.run' should handle all cases so not sure where\n        #       that come from\n        status = self._install_thread.result()\n        if status is not None and status >= 0:\n            self._update_progress(100)\n            QtWidgets.QApplication.processEvents()\n            self.done(3)\n        else:\n            self._enable_buttons()\n            self._show_console()\n\n    def _update_progress(self, progress: int):\n        self._progress_bar.setValue(progress)\n        text_visible = self._progress_bar.isTextVisible()\n        if progress == 0:\n            if text_visible:\n                self._progress_bar.setTextVisible(False)\n        elif not text_visible:\n            self._progress_bar.setTextVisible(True)\n\n    def _on_exit_clicked(self):\n        self.reject()\n\n    def _on_mongo_url_change(self, new_value):\n        # Strip the value\n        new_value = new_value.strip()\n        # Store new mongo url to variable\n        self.mongo_url = new_value\n\n        msg = None\n        # Change style of input\n        if not new_value:\n            self._mongo_input.remove_state()\n        elif not self.mongo_url_regex.match(new_value):\n            self._mongo_input.set_invalid()\n            msg = (\n                \"Mongo URL should start with\"\n                \" <b>\\\"mongodb://\\\"</b> or <b>\\\"mongodb+srv://\\\"</b>\"\n            )\n        else:\n            self._mongo_input.set_valid()\n\n        self.set_invalid_mongo_url(msg)\n\n    def validate_url(self):\n        \"\"\"Validate if entered url is ok.\n\n        Returns:\n            True if url is valid monogo string.\n\n        \"\"\"\n        if self.mongo_url == \"\":\n            return False\n\n        is_valid, reason_str = validate_mongo_connection(self.mongo_url)\n        if not is_valid:\n            self.set_invalid_mongo_connection(self.mongo_url)\n            self._mongo_input.set_invalid()\n            self.update_console(f\"!!! {reason_str}\", True)\n            return False\n\n        self.set_invalid_mongo_connection(None)\n        self._mongo_input.set_valid()\n        return True\n\n    def set_invalid_mongo_url(self, reason):\n        if reason is None:\n            self._mongo_connection_msg.setText(\"\")\n        else:\n            self._mongo_connection_msg.setText(\"- {}\".format(reason))\n\n    def set_invalid_mongo_connection(self, mongo_url, connecting=False):\n        if mongo_url is None:\n            self.set_invalid_mongo_url(mongo_url)\n            return\n\n        if connecting:\n            msg = \"Connecting to: <b>{}</b>\".format(mongo_url)\n        else:\n            msg = \"Can't connect to: <b>{}</b>\".format(mongo_url)\n\n        self.set_invalid_mongo_url(msg)\n\n    def update_console(self, msg: str, error: bool = False) -> None:\n        \"\"\"Display message in console.\n\n        Args:\n            msg (str): message.\n            error (bool): if True, print it red.\n        \"\"\"\n        self._console_widget.update_console(msg, error)\n\n    def _show_console(self):\n        self._console_widget.show_console()\n        self.updateGeometry()\n\n    def _disable_buttons(self):\n        \"\"\"Disable buttons so user interaction doesn't interfere.\"\"\"\n        self._exit_button.setEnabled(False)\n        self._run_button.setEnabled(False)\n        self._controls_disabled = True\n\n    def _enable_buttons(self):\n        \"\"\"Enable buttons after operation is complete.\"\"\"\n        self._exit_button.setEnabled(True)\n        self._run_button.setEnabled(True)\n        self._controls_disabled = False\n\n    def closeEvent(self, event):  # noqa\n        \"\"\"Prevent closing if window when controls are disabled.\"\"\"\n        if self._controls_disabled:\n            return event.ignore()\n        return super(InstallDialog, self).closeEvent(event)\n\n\nif __name__ == \"__main__\":\n    app = QtWidgets.QApplication(sys.argv)\n    d = InstallDialog()\n    d.show()\n    sys.exit(app.exec_())\n"
  },
  {
    "path": "igniter/install_thread.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Working thread for installer.\"\"\"\nimport os\nimport sys\nfrom pathlib import Path\n\nfrom qtpy import QtCore\n\nfrom .bootstrap_repos import (\n    BootstrapRepos,\n    OpenPypeVersionInvalid,\n    OpenPypeVersionIOError,\n    OpenPypeVersionExists,\n    OpenPypeVersion\n)\n\nfrom .tools import (\n    get_openpype_global_settings,\n    get_local_openpype_path_from_settings,\n    validate_mongo_connection\n)\n\n\nclass InstallThread(QtCore.QThread):\n    \"\"\"Install Worker thread.\n\n    This class takes care of finding OpenPype version on user entered path\n    (or loading this path from database). If nothing is entered by user,\n    OpenPype will create its zip files from repositories that comes with it.\n\n    If path contains plain repositories, they are zipped and installed to\n    user data dir.\n\n    \"\"\"\n    progress = QtCore.Signal(int)\n    message = QtCore.Signal((str, bool))\n\n    def __init__(self, parent=None,):\n        self._mongo = None\n        self._result = None\n\n        super().__init__(parent)\n\n    def result(self):\n        \"\"\"Result of finished installation.\"\"\"\n        return self._result\n\n    def _set_result(self, value):\n        if self._result is not None:\n            raise AssertionError(\"BUG: Result was set more than once!\")\n        self._result = value\n\n    def run(self):\n        \"\"\"Thread entry point.\n\n        Using :class:`BootstrapRepos` to either install OpenPype as zip files\n        or copy them from location specified by user or retrieved from\n        database.\n\n        \"\"\"\n        self.message.emit(\"Installing OpenPype ...\", False)\n\n        # find local version of OpenPype\n        bs = BootstrapRepos(\n            progress_callback=self.set_progress, message=self.message)\n        local_version = OpenPypeVersion.get_installed_version_str()\n\n        # user did not entered url\n        if self._mongo:\n            self.message.emit(\"Saving mongo connection string ...\", False)\n            bs.secure_registry.set_item(\"openPypeMongo\", self._mongo)\n\n        elif os.getenv(\"OPENPYPE_MONGO\"):\n            self._mongo = os.getenv(\"OPENPYPE_MONGO\")\n        else:\n            # try to get it from settings registry\n            try:\n                self._mongo = bs.secure_registry.get_item(\n                    \"openPypeMongo\")\n            except ValueError:\n                self.message.emit(\n                    \"!!! We need MongoDB URL to proceed.\", True)\n                self._set_result(-1)\n                return\n        os.environ[\"OPENPYPE_MONGO\"] = self._mongo\n\n        if not validate_mongo_connection(self._mongo):\n            self.message.emit(f\"Cannot connect to {self._mongo}\", True)\n            self._set_result(-1)\n            return\n\n        global_settings = get_openpype_global_settings(self._mongo)\n        data_dir = get_local_openpype_path_from_settings(global_settings)\n        bs.set_data_dir(data_dir)\n\n        self.message.emit(\n            f\"Detecting installed OpenPype versions in {bs.data_dir}\",\n            False)\n        detected = bs.find_openpype(include_zips=True)\n        if not detected and getattr(sys, 'frozen', False):\n            self.message.emit(\"None detected.\", True)\n            self.message.emit((\"We will use OpenPype coming with \"\n                               \"installer.\"), False)\n            openpype_version = bs.create_version_from_frozen_code()\n            if not openpype_version:\n                self.message.emit(\n                    f\"!!! Install failed - {openpype_version}\", True)\n                self._set_result(-1)\n                return\n            self.message.emit(f\"Using: {openpype_version}\", False)\n            bs.install_version(openpype_version)\n            self.message.emit(f\"Installed as {openpype_version}\", False)\n            self.progress.emit(100)\n            self._set_result(1)\n            return\n\n        if detected and not OpenPypeVersion.get_installed_version().is_compatible(detected[-1]):  # noqa: E501\n            self.message.emit((\n                f\"Latest detected version {detected[-1]} \"\n                \"is not compatible with the currently running \"\n                f\"{local_version}\"\n            ), True)\n            self.message.emit((\n                \"Filtering detected versions to compatible ones...\"\n            ), False)\n\n        # filter results to get only compatible versions\n        detected = [\n            version for version in detected\n            if version.is_compatible(\n                OpenPypeVersion.get_installed_version())\n        ]\n\n        if detected:\n            if OpenPypeVersion(\n                    version=local_version, path=Path()) < detected[-1]:\n                self.message.emit((\n                    f\"Latest installed version {detected[-1]} is newer \"\n                    f\"then currently running {local_version}\"\n                ), False)\n                self.message.emit(\"Skipping OpenPype install ...\", False)\n                if detected[-1].path.suffix.lower() == \".zip\":\n                    bs.extract_openpype(detected[-1])\n                self._set_result(0)\n                return\n\n            if OpenPypeVersion(version=local_version).get_main_version() == detected[-1].get_main_version():  # noqa: E501\n                self.message.emit((\n                    f\"Latest installed version is the same as \"\n                    f\"currently running {local_version}\"\n                ), False)\n                self.message.emit(\"Skipping OpenPype install ...\", False)\n                self._set_result(0)\n                return\n\n        self.message.emit((\n            \"All installed versions are older then \"\n            f\"currently running one {local_version}\"\n        ), False)\n\n        self.message.emit(\"None detected.\", False)\n\n        self.message.emit(\n            f\"We will use local OpenPype version {local_version}\", False)\n\n        local_openpype = bs.create_version_from_live_code()\n        if not local_openpype:\n            self.message.emit(\n                f\"!!! Install failed - {local_openpype}\", True)\n            self._set_result(-1)\n            return\n\n        try:\n            bs.install_version(local_openpype)\n        except (OpenPypeVersionExists,\n                OpenPypeVersionInvalid,\n                OpenPypeVersionIOError) as e:\n            self.message.emit(f\"Installed failed: \", True)\n            self.message.emit(str(e), True)\n            self._set_result(-1)\n            return\n\n        self.message.emit(f\"Installed as {local_openpype}\", False)\n        self.progress.emit(100)\n        self._set_result(1)\n        return\n\n        self.progress.emit(100)\n        self._set_result(1)\n        return\n\n    def set_path(self, path: str) -> None:\n        \"\"\"Helper to set path.\n\n        Args:\n            path (str): Path to set.\n\n        \"\"\"\n        self._path = path\n\n    def set_mongo(self, mongo: str) -> None:\n        \"\"\"Helper to set mongo url.\n\n        Args:\n            mongo (str): Mongodb url.\n\n        \"\"\"\n        self._mongo = mongo\n\n    def set_progress(self, progress: int) -> None:\n        \"\"\"Helper to set progress bar.\n\n        Args:\n            progress (int): Progress in percents.\n\n        \"\"\"\n        self.progress.emit(progress)\n"
  },
  {
    "path": "igniter/message_dialog.py",
    "content": "from qtpy import QtWidgets, QtGui\n\nfrom .tools import (\n    load_stylesheet,\n    get_openpype_icon_path\n)\n\n\nclass MessageDialog(QtWidgets.QDialog):\n    \"\"\"Simple message dialog with title, message and OK button.\"\"\"\n    def __init__(self, title, message):\n        super(MessageDialog, self).__init__()\n\n        # Set logo as icon of window\n        icon_path = get_openpype_icon_path()\n        pixmap_openpype_logo = QtGui.QPixmap(icon_path)\n        self.setWindowIcon(QtGui.QIcon(pixmap_openpype_logo))\n\n        # Set title\n        self.setWindowTitle(title)\n\n        # Set message\n        label_widget = QtWidgets.QLabel(message, self)\n\n        ok_btn = QtWidgets.QPushButton(\"OK\", self)\n        btns_layout = QtWidgets.QHBoxLayout()\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(ok_btn, 0)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(label_widget, 1)\n        layout.addLayout(btns_layout, 0)\n\n        ok_btn.clicked.connect(self._on_ok_clicked)\n\n        self._label_widget = label_widget\n        self._ok_btn = ok_btn\n\n    def _on_ok_clicked(self):\n        self.close()\n\n    def showEvent(self, event):\n        super(MessageDialog, self).showEvent(event)\n        self.setStyleSheet(load_stylesheet())\n"
  },
  {
    "path": "igniter/nice_progress_bar.py",
    "content": "from qtpy import QtWidgets\n\n\nclass NiceProgressBar(QtWidgets.QProgressBar):\n    def __init__(self, parent=None):\n        super(NiceProgressBar, self).__init__(parent)\n        self._real_value = 0\n\n    def setValue(self, value):\n        self._real_value = value\n        if value != 0 and value < 11:\n            value = 11\n\n        super(NiceProgressBar, self).setValue(value)\n\n    def value(self):\n        return self._real_value\n\n    def text(self):\n        return \"{} %\".format(self._real_value)\n"
  },
  {
    "path": "igniter/splash.txt",
    "content": "\n\n\n   *\n\n\n\n\n\n\n    .*\n\n\n\n\n\n       *\n     .*\n       *\n\n\n\n         .\n        *\n       .*\n        *\n         .\n\n          .\n         *\n        .*\n        .*\n        .*\n         *\n          .\n            .\n           *\n          .*\n          .*\n          .*\n           *\n            .\n            _.\n           /**\n           \\ *\n            \\*\n             *\n             *\n             .\n            __.\n           ---*\n           \\ \\*\n            \\ *\n             \\*\n              *\n              .\n           \\___.\n           /*  *\n           \\ \\ *\n            \\ \\*\n             \\ *\n              \\*\n               .\n            |____.\n           /*    *\n           \\|\\   *\n            \\ \\  *\n             \\ \\ *\n              \\ \\*\n               \\/.\n            _/_____.\n           /*      *\n           / \\     *\n            \\ \\    *\n             \\ \\   *\n              \\ \\__*\n               \\/__.\n            __________.\n          --*--    ___*\n           \\ \\     \\/_*\n            \\ \\     __*\n             \\ \\    \\_*\n              \\ \\____\\*\n               \\/____/.\n           \\____________ .\n           /*      ___  \\*\n           \\ \\     \\/_\\  *\n            \\ \\     _____*\n             \\ \\    \\___/*\n              \\ \\____\\   *\n               \\/____/   .\n            |___________    .\n           /*      ___  \\   *\n           \\|\\     \\/_\\  \\  *\n            \\ \\     _____/  *\n             \\ \\    \\___/   *\n              \\ \\____\\    / *\n               \\/____/     \\.\n            _/__________       .\n           /*      ___  \\      *\n           / \\     \\/_\\  \\     *\n            \\ \\     _____/     *\n             \\ \\    \\___/   ---*\n              \\ \\____\\    / \\__*\n               \\/____/     \\/__.\n            ____________          .\n          --*--    ___  \\         *\n           \\ \\     \\/_\\  \\        *\n            \\ \\     _____/        *\n             \\ \\    \\___/   ----  *\n              \\ \\____\\    / \\____\\*\n               \\/____/     \\/____/.\n            ____________                \n           /\\      ___  \\            .\n           \\ \\     \\/_\\  \\           *\n            \\ \\     _____/           *\n             \\ \\    \\___/   ----     *\n              \\ \\____\\    / \\____\\   .\n               \\/____/     \\/____/    \n            ____________\n           /\\      ___  \\                \n           \\ \\     \\/_\\  \\             .\n            \\ \\     _____/             *\n             \\ \\    \\___/   ----       *\n              \\ \\____\\    / \\____\\     .\n               \\/____/     \\/____/      \n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\                 \n            \\ \\     _____/               .\n             \\ \\    \\___/   ----         *\n              \\ \\____\\    / \\____\\       .\n               \\/____/     \\/____/        \n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/                    \n             \\ \\    \\___/   ----           *\n              \\ \\____\\    / \\____\\           \n               \\/____/     \\/____/          \n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/\n             \\ \\    \\___/   ----             .\n              \\ \\____\\    / \\____\\            \n               \\/____/     \\/____/            \n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         _                \n             \\ \\    \\___/   ----\n              \\ \\____\\    / \\____\\\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___\n             \\ \\    \\___/   ----\n              \\ \\____\\    / \\____\\\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___\n             \\ \\    \\___/   ----      \\\n              \\ \\____\\    / \\____\\\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___\n             \\ \\    \\___/   ----      \\\n              \\ \\____\\    / \\____\\     \\\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___\n             \\ \\    \\___/   ----      \\\n              \\ \\____\\    / \\____\\   __\\\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___\n             \\ \\    \\___/   ----      \\\n              \\ \\____\\    / \\____\\  \\__\\\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___\n             \\ \\    \\___/   ----   \\  \\\n              \\ \\____\\    / \\____\\  \\__\\\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___\n             \\ \\    \\___/   ----   \\  \\\n              \\ \\____\\    / \\____\\  \\__\\\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___.\n             \\ \\    \\___/   ----   \\  \\\\\n              \\ \\____\\    / \\____\\  \\__\\,\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ .\n             \\ \\    \\___/   ----   \\  \\\\\n              \\ \\____\\    / \\____\\  \\__\\\\,\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ _.\n             \\ \\    \\___/   ----   \\  \\\\\\\n              \\ \\____\\    / \\____\\  \\__\\\\\\\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ __.\n             \\ \\    \\___/   ----   \\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\_/.\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___.\n             \\ \\    \\___/   ----   \\  \\\\  \\\\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\.\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ .\n             \\ \\    \\___/   ----   \\  \\\\  \\\\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\.\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ _.\n             \\ \\    \\___/   ----   \\  \\\\  \\\\\\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\.\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ __.\n             \\ \\    \\___/   ----   \\  \\\\  \\\\ \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\_.\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ __.\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__.\n               \\/____/     \\/____/\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/ .\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  *\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  O*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  .oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  ..oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . .oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . p.oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . Py.oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . PYp.oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . PYPe.oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . PYPE .oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . PYPE c.oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . PYPE C1.oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . PYPE ClU.oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . PYPE CluB.oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . PYPE Club .oO*\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . PYPE Club . ..\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . PYPE Club .  ..\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . PYPE Club .    .\n            ____________\n           /\\      ___  \\\n           \\ \\     \\/_\\  \\\n            \\ \\     _____/         ___ ___ ___\n             \\ \\    \\___/   ----   \\  \\\\  \\\\  \\\n              \\ \\____\\    / \\____\\  \\__\\\\__\\\\__\\\n               \\/____/     \\/____/  . PYPE Club .      \n"
  },
  {
    "path": "igniter/stylesheet.css",
    "content": "*{\n    font-size: 10pt;\n    font-family: \"Poppins\";\n}\n\nQWidget {\n    color: #bfccd6;\n    background-color: #282C34;\n    border-radius: 0px;\n}\n\nQMenu {\n    border: 1px solid #555555;\n    background-color: #21252B;\n}\n\nQMenu::item {\n    padding: 5px 10px 5px 10px;\n    border-left: 5px solid #313741;;\n}\n\nQMenu::item:selected {\n    border-left-color: rgb(84, 209, 178);\n    background-color: #222d37;\n}\n\nQLineEdit, QPlainTextEdit {\n    border: 1px solid #464b54;\n    border-radius: 3px;\n    background-color: #21252B;\n    padding: 0.5em;\n}\n\nQLineEdit[state=\"valid\"] {\n    background-color: rgb(19, 19, 19);\n    color: rgb(64, 230, 132);\n    border-color: rgb(32, 64, 32);\n}\n\nQLineEdit[state=\"invalid\"] {\n    background-color: rgb(32, 19, 19);\n    color: rgb(255, 69, 0);\n    border-color: rgb(64, 32, 32);\n}\n\nQLabel {\n    background: transparent;\n    color: #969b9e;\n}\n\nQLabel:hover {color: #b8c1c5;}\n\nQPushButton {\n    border: 1px solid #aaaaaa;\n    border-radius: 3px;\n    padding: 5px;\n}\n\nQPushButton:hover {\n    background-color: #333840;\n    border: 1px solid #fff;\n    color: #fff;\n}\n\nQTableView {\n    border: 1px solid #444;\n    gridline-color: #6c6c6c;\n    background-color: #201F1F;\n    alternate-background-color:#21252B;\n}\n\nQTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed  {\n    background: #78879b;\n    color: #FFFFFF;\n}\n\nQTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active  {\n    background: #3d8ec9;\n}\n\nQProgressBar {\n    border: 1px solid grey;\n    border-radius: 10px;\n    color: #222222;\n    font-weight: bold;\n}\nQProgressBar:horizontal {\n    height: 20px;\n}\n\nQProgressBar::chunk {\n    border-radius: 10px;\n    background-color: qlineargradient(\n        x1: 0,\n        y1: 0.5,\n        x2: 1,\n        y2: 0.5,\n        stop: 0 rgb(72, 200, 150),\n        stop: 1 rgb(82, 172, 215)\n    );\n}\n\n\nQScrollBar:horizontal {\n    height: 15px;\n    margin: 3px 15px 3px 15px;\n    border: 1px transparent #21252B;\n    border-radius: 4px;\n    background-color: #21252B;\n}\n\nQScrollBar::handle:horizontal {\n    background-color: #4B5362;\n    min-width: 5px;\n    border-radius: 4px;\n}\n\nQScrollBar::add-line:horizontal {\n    margin: 0px 3px 0px 3px;\n    border-image: url(:/qss_icons/rc/right_arrow_disabled.png);\n    width: 10px;\n    height: 10px;\n    subcontrol-position: right;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::sub-line:horizontal {\n    margin: 0px 3px 0px 3px;\n    border-image: url(:/qss_icons/rc/left_arrow_disabled.png);\n    height: 10px;\n    width: 10px;\n    subcontrol-position: left;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on {\n    border-image: url(:/qss_icons/rc/right_arrow.png);\n    height: 10px;\n    width: 10px;\n    subcontrol-position: right;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on {\n    border-image: url(:/qss_icons/rc/left_arrow.png);\n    height: 10px;\n    width: 10px;\n    subcontrol-position: left;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal {\n    background: none;\n}\n\nQScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {\n    background: none;\n}\n\nQScrollBar:vertical {\n    background-color: #21252B;\n    width: 15px;\n    margin: 15px 3px 15px 3px;\n    border: 1px transparent #21252B;\n    border-radius: 4px;\n}\n\nQScrollBar::handle:vertical {\n    background-color: #4B5362;\n    min-height: 5px;\n    border-radius: 4px;\n}\n\nQScrollBar::sub-line:vertical {\n    margin: 3px 0px 3px 0px;\n    border-image: url(:/qss_icons/rc/up_arrow_disabled.png);\n    height: 10px;\n    width: 10px;\n    subcontrol-position: top;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::add-line:vertical {\n    margin: 3px 0px 3px 0px;\n    border-image: url(:/qss_icons/rc/down_arrow_disabled.png);\n    height: 10px;\n    width: 10px;\n    subcontrol-position: bottom;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on {\n\n    border-image: url(:/qss_icons/rc/up_arrow.png);\n    height: 10px;\n    width: 10px;\n    subcontrol-position: top;\n    subcontrol-origin: margin;\n}\n\n\nQScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on {\n    border-image: url(:/qss_icons/rc/down_arrow.png);\n    height: 10px;\n    width: 10px;\n    subcontrol-position: bottom;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {\n    background: none;\n}\n\n\nQScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {\n    background: none;\n}\n\n#MainLabel {\n    color: rgb(200, 200, 200);\n    font-size: 12pt;\n}\n\n#Console {\n    background-color: #21252B;\n    color: rgb(72, 200, 150);\n    font-family: \"Roboto Mono\";\n    font-size: 8pt;\n}\n\n#ExitBtn {\n    /* `border` must be set to background of flat button is painted .*/\n    border: none;\n    color: rgb(39, 39, 39);\n    background-color: #828a97;\n    padding: 0.5em;\n    font-weight: 400;\n}\n\n#ExitBtn:hover{\n    background-color: #b2bece\n}\n#ExitBtn:disabled {\n    background-color: rgba(185, 185, 185, 31);\n    color: rgba(64, 64, 64, 63);\n}\n\n#ButtonWithOptions QPushButton{\n    border-top-right-radius: 0px;\n    border-bottom-right-radius: 0px;\n    border: none;\n    background-color: rgb(84, 209, 178);\n    color: rgb(39, 39, 39);\n    font-weight: 400;\n    padding: 0.5em;\n}\n#ButtonWithOptions QPushButton:hover{\n    background-color: rgb(85, 224, 189)\n}\n#ButtonWithOptions QPushButton:disabled {\n    background-color: rgba(72, 200, 150, 31);\n    color: rgba(64, 64, 64, 63);\n}\n\n#ButtonWithOptions QToolButton{\n    border: none;\n    border-top-left-radius: 0px;\n    border-bottom-left-radius: 0px;\n    border-top-right-radius: 3px;\n    border-bottom-right-radius: 3px;\n    background-color: rgb(84, 209, 178);\n    color: rgb(39, 39, 39);\n}\n#ButtonWithOptions QToolButton:hover{\n    background-color: rgb(85, 224, 189)\n}\n#ButtonWithOptions QToolButton:disabled {\n    background-color: rgba(72, 200, 150, 31);\n    color: rgba(64, 64, 64, 63);\n}\n"
  },
  {
    "path": "igniter/terminal_splash.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"OpenPype terminal animation.\"\"\"\nimport blessed\nfrom pathlib import Path\nfrom time import sleep\n\nNO_TERMINAL = False\n\ntry:\n    term = blessed.Terminal()\nexcept AttributeError:\n    # this happens when blessed cannot find proper terminal.\n    # If so, skip printing ascii art animation.\n    NO_TERMINAL = True\n\n\ndef play_animation():\n    \"\"\"Play ASCII art OpenPype animation.\"\"\"\n    if NO_TERMINAL:\n        return\n    print(term.home + term.clear)\n    frame_size = 7\n    splash_file = Path(__file__).parent / \"splash.txt\"\n    with splash_file.open(\"r\") as sf:\n        animation = sf.readlines()\n\n    animation_length = int(len(animation) / frame_size)\n    current_frame = 0\n    for _ in range(animation_length):\n        frame = \"\".join(\n            scanline\n            for y, scanline in enumerate(\n                animation[current_frame : current_frame + frame_size]\n            )\n        )\n\n        with term.location(0, 0):\n            # term.aquamarine3_bold(frame)\n            print(f\"{term.bold}{term.aquamarine3}{frame}{term.normal}\")\n\n        sleep(0.02)\n        current_frame += frame_size\n    print(term.move_y(7))\n"
  },
  {
    "path": "igniter/tools.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tools used in **Igniter** GUI.\"\"\"\nimport os\nfrom typing import Union\nfrom urllib.parse import urlparse, parse_qs\nfrom pathlib import Path\nimport platform\n\nimport certifi\nfrom pymongo import MongoClient\nfrom pymongo.errors import (\n    ServerSelectionTimeoutError,\n    InvalidURI,\n    ConfigurationError,\n    OperationFailure\n)\n\n\nclass OpenPypeVersionNotFound(Exception):\n    \"\"\"OpenPype version was not found in remote and local repository.\"\"\"\n    pass\n\n\nclass OpenPypeVersionIncompatible(Exception):\n    \"\"\"OpenPype version is not compatible with the installed one (build).\"\"\"\n    pass\n\n\ndef should_add_certificate_path_to_mongo_url(mongo_url):\n    \"\"\"Check if should add ca certificate to mongo url.\n\n    Since 30.9.2021 cloud mongo requires newer certificates that are not\n    available on most of workstation. This adds path to certifi certificate\n    which is valid for it. To add the certificate path url must have scheme\n    'mongodb+srv' or has 'ssl=true' or 'tls=true' in url query.\n    \"\"\"\n    parsed = urlparse(mongo_url)\n    query = parse_qs(parsed.query)\n    lowered_query_keys = set(key.lower() for key in query.keys())\n    add_certificate = False\n    # Check if url 'ssl' or 'tls' are set to 'true'\n    for key in (\"ssl\", \"tls\"):\n        if key in query and \"true\" in query[key]:\n            add_certificate = True\n            break\n\n    # Check if url contains 'mongodb+srv'\n    if not add_certificate and parsed.scheme == \"mongodb+srv\":\n        add_certificate = True\n\n    # Check if url does already contain certificate path\n    if add_certificate and \"tlscafile\" in lowered_query_keys:\n        add_certificate = False\n    return add_certificate\n\n\ndef validate_mongo_connection(cnx: str) -> (bool, str):\n    \"\"\"Check if provided mongodb URL is valid.\n\n    Args:\n        cnx (str): URL to validate.\n\n    Returns:\n        (bool, str): True if ok, False if not and reason in str.\n\n    \"\"\"\n    parsed = urlparse(cnx)\n    if parsed.scheme not in [\"mongodb\", \"mongodb+srv\"]:\n        return False, \"Not mongodb schema\"\n\n    kwargs = {\n        \"serverSelectionTimeoutMS\": os.environ.get(\"AVALON_TIMEOUT\", 2000)\n    }\n    # Add certificate path if should be required\n    if should_add_certificate_path_to_mongo_url(cnx):\n        kwargs[\"tlsCAFile\"] = certifi.where()\n\n    try:\n        client = MongoClient(cnx, **kwargs)\n        client.server_info()\n        with client.start_session():\n            pass\n        client.close()\n    except ServerSelectionTimeoutError as e:\n        return False, f\"Cannot connect to server {cnx} - {e}\"\n    except ValueError:\n        return False, f\"Invalid port specified {parsed.port}\"\n    except (ConfigurationError, OperationFailure, InvalidURI) as exc:\n        return False, str(exc)\n    else:\n        return True, \"Connection is successful\"\n\n\ndef validate_mongo_string(mongo: str) -> (bool, str):\n    \"\"\"Validate string if it is mongo url acceptable by **Igniter**..\n\n    Args:\n        mongo (str): String to validate.\n\n    Returns:\n        (bool, str):\n            True if valid, False if not and in second part of tuple\n            the reason why it failed.\n\n    \"\"\"\n    if not mongo:\n        return True, \"empty string\"\n    return validate_mongo_connection(mongo)\n\n\ndef validate_path_string(path: str) -> (bool, str):\n    \"\"\"Validate string if it is path to OpenPype repository.\n\n    Args:\n        path (str): Path to validate.\n\n\n    Returns:\n        (bool, str):\n            True if valid, False if not and in second part of tuple\n            the reason why it failed.\n\n    \"\"\"\n    if not path:\n        return False, \"empty string\"\n\n    if not Path(path).exists():\n        return False, \"path doesn't exists\"\n\n    if not Path(path).is_dir():\n        return False, \"path is not directory\"\n\n    return True, \"valid path\"\n\n\ndef get_openpype_global_settings(url: str) -> dict:\n    \"\"\"Load global settings from Mongo database.\n\n    We are loading data from database `openpype` and collection `settings`.\n    There we expect document type `global_settings`.\n\n    Args:\n        url (str): MongoDB url.\n\n    Returns:\n        dict: With settings data. Empty dictionary is returned if not found.\n    \"\"\"\n    kwargs = {}\n    if should_add_certificate_path_to_mongo_url(url):\n        kwargs[\"tlsCAFile\"] = certifi.where()\n\n    try:\n        # Create mongo connection\n        client = MongoClient(url, **kwargs)\n        # Access settings collection\n        openpype_db = os.environ.get(\"OPENPYPE_DATABASE_NAME\") or \"openpype\"\n        col = client[openpype_db][\"settings\"]\n        # Query global settings\n        global_settings = col.find_one({\"type\": \"global_settings\"}) or {}\n        # Close Mongo connection\n        client.close()\n\n    except Exception:\n        # TODO log traceback or message\n        return {}\n\n    return global_settings.get(\"data\") or {}\n\n\ndef get_openpype_path_from_settings(settings: dict) -> Union[str, None]:\n    \"\"\"Get OpenPype path from global settings.\n\n    Args:\n        settings (dict): mongodb url.\n\n    Returns:\n        path to OpenPype or None if not found\n    \"\"\"\n    paths = (\n        settings\n        .get(\"openpype_path\", {})\n        .get(platform.system().lower())\n    ) or []\n    # For cases when `openpype_path` is a single path\n    if paths and isinstance(paths, str):\n        paths = [paths]\n\n    return next((path for path in paths if os.path.exists(path)), None)\n\n\ndef get_local_openpype_path_from_settings(settings: dict) -> Union[str, None]:\n    \"\"\"Get OpenPype local path from global settings.\n\n    Used to download and unzip OP versions.\n    Args:\n        settings (dict): settings from DB.\n\n    Returns:\n        path to OpenPype or None if not found\n    \"\"\"\n    path = (\n        settings\n        .get(\"local_openpype_path\", {})\n        .get(platform.system().lower())\n    )\n    if path:\n        return Path(path)\n    return None\n\n\ndef get_expected_studio_version_str(\n    staging=False, global_settings=None\n) -> str:\n    \"\"\"Version that should be currently used in studio.\n\n    Args:\n        staging (bool): Get current version for staging.\n        global_settings (dict): Optional precached global settings.\n\n    Returns:\n        str: OpenPype version which should be used. Empty string means latest.\n    \"\"\"\n    mongo_url = os.environ.get(\"OPENPYPE_MONGO\")\n    if global_settings is None:\n        global_settings = get_openpype_global_settings(mongo_url)\n    key = \"staging_version\" if staging else \"production_version\"\n    return global_settings.get(key) or \"\"\n\n\ndef load_stylesheet() -> str:\n    \"\"\"Load css style sheet.\n\n    Returns:\n        str: content of the stylesheet\n\n    \"\"\"\n    stylesheet_path = Path(__file__).parent.resolve() / \"stylesheet.css\"\n\n    return stylesheet_path.read_text()\n\n\ndef get_openpype_icon_path() -> str:\n    \"\"\"Path to OpenPype icon png file.\"\"\"\n    return os.path.join(\n        os.path.dirname(os.path.abspath(__file__)),\n        \"openpype_icon.png\"\n    )\n"
  },
  {
    "path": "igniter/update_thread.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Working thread for update.\"\"\"\nfrom qtpy import QtCore\n\nfrom .bootstrap_repos import (\n    BootstrapRepos,\n    OpenPypeVersion\n)\n\n\nclass UpdateThread(QtCore.QThread):\n    \"\"\"Install Worker thread.\n\n    This class takes care of finding OpenPype version on user entered path\n    (or loading this path from database). If nothing is entered by user,\n    OpenPype will create its zip files from repositories that comes with it.\n\n    If path contains plain repositories, they are zipped and installed to\n    user data dir.\n\n    \"\"\"\n    progress = QtCore.Signal(int)\n    message = QtCore.Signal((str, bool))\n\n    def __init__(self, parent=None):\n        self._result = None\n        self._openpype_version = None\n        super().__init__(parent)\n\n    def set_version(self, openpype_version: OpenPypeVersion):\n        self._openpype_version = openpype_version\n\n    def result(self):\n        \"\"\"Result of finished installation.\"\"\"\n        return self._result\n\n    def _set_result(self, value):\n        if self._result is not None:\n            raise AssertionError(\"BUG: Result was set more than once!\")\n        self._result = value\n\n    def run(self):\n        \"\"\"Thread entry point.\n\n        Using :class:`BootstrapRepos` to either install OpenPype as zip files\n        or copy them from location specified by user or retrieved from\n        database.\n        \"\"\"\n        bs = BootstrapRepos(\n            progress_callback=self.set_progress, message=self.message)\n\n        bs.set_data_dir(OpenPypeVersion.get_local_openpype_path())\n        version_path = bs.install_version(self._openpype_version)\n        self._set_result(version_path)\n\n    def set_progress(self, progress: int) -> None:\n        \"\"\"Helper to set progress bar.\n\n        Args:\n            progress (int): Progress in percents.\n\n        \"\"\"\n        self.progress.emit(progress)\n"
  },
  {
    "path": "igniter/update_window.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Progress window to show when OpenPype is updating/installing locally.\"\"\"\nimport os\n\nfrom qtpy import QtCore, QtGui, QtWidgets\n\nfrom .update_thread import UpdateThread\nfrom .bootstrap_repos import OpenPypeVersion\nfrom .nice_progress_bar import NiceProgressBar\nfrom .tools import load_stylesheet\n\n\nclass UpdateWindow(QtWidgets.QDialog):\n    \"\"\"OpenPype update window.\"\"\"\n\n    _width = 500\n    _height = 100\n\n    def __init__(self, version: OpenPypeVersion, parent=None):\n        super(UpdateWindow, self).__init__(parent)\n        self._openpype_version = version\n        self._result_version_path = None\n\n        self.setWindowTitle(\n            f\"OpenPype is updating ...\"\n        )\n        self.setModal(True)\n        self.setWindowFlags(\n            QtCore.Qt.WindowMinimizeButtonHint\n        )\n\n        current_dir = os.path.dirname(os.path.abspath(__file__))\n        roboto_font_path = os.path.join(current_dir, \"RobotoMono-Regular.ttf\")\n        poppins_font_path = os.path.join(current_dir, \"Poppins\")\n        icon_path = os.path.join(current_dir, \"openpype_icon.png\")\n\n        # Install roboto font\n        QtGui.QFontDatabase.addApplicationFont(roboto_font_path)\n        for filename in os.listdir(poppins_font_path):\n            if os.path.splitext(filename)[1] == \".ttf\":\n                QtGui.QFontDatabase.addApplicationFont(filename)\n\n        # Load logo\n        pixmap_openpype_logo = QtGui.QPixmap(icon_path)\n        # Set logo as icon of window\n        self.setWindowIcon(QtGui.QIcon(pixmap_openpype_logo))\n\n        self._pixmap_openpype_logo = pixmap_openpype_logo\n\n        self._update_thread = None\n\n        self._init_ui()\n\n        # Set stylesheet\n        self.setStyleSheet(load_stylesheet())\n        self._run_update()\n\n    def _init_ui(self):\n\n        # Main info\n        # --------------------------------------------------------------------\n        main_label = QtWidgets.QLabel(\n            f\"<b>OpenPype</b> is updating to {self._openpype_version}\", self)\n        main_label.setWordWrap(True)\n        main_label.setObjectName(\"MainLabel\")\n\n        # Progress bar\n        # --------------------------------------------------------------------\n        progress_bar = NiceProgressBar(self)\n        progress_bar.setAlignment(QtCore.Qt.AlignCenter)\n        progress_bar.setTextVisible(False)\n\n        # add all to main\n        main = QtWidgets.QVBoxLayout(self)\n        main.addSpacing(15)\n        main.addWidget(main_label, 0)\n        main.addSpacing(15)\n        main.addWidget(progress_bar, 0)\n        main.addSpacing(15)\n\n        self._progress_bar = progress_bar\n\n    def showEvent(self, event):\n        super().showEvent(event)\n        current_size = self.size()\n        new_size = QtCore.QSize(\n            max(current_size.width(), self._width),\n            max(current_size.height(), self._height)\n        )\n        if current_size != new_size:\n            self.resize(new_size)\n\n    def _run_update(self):\n        \"\"\"Start install process.\n\n        This will once again validate entered path and mongo if ok, start\n        working thread that will do actual job.\n        \"\"\"\n        # Check if install thread is not already running\n        if self._update_thread and self._update_thread.isRunning():\n            return\n        self._progress_bar.setRange(0, 0)\n        update_thread = UpdateThread(self)\n        update_thread.set_version(self._openpype_version)\n        update_thread.message.connect(self.update_console)\n        update_thread.progress.connect(self._update_progress)\n        update_thread.finished.connect(self._installation_finished)\n\n        self._update_thread = update_thread\n\n        update_thread.start()\n\n    def get_version_path(self):\n        return self._result_version_path\n\n    def _installation_finished(self):\n        status = self._update_thread.result()\n        self._result_version_path = status\n        self._progress_bar.setRange(0, 1)\n        self._update_progress(100)\n        QtWidgets.QApplication.processEvents()\n        self.done(0)\n\n    def _update_progress(self, progress: int):\n        # not updating progress as we are not able to determine it\n        # correctly now. Progress bar is set to un-deterministic mode\n        # until we are able to get progress in better way.\n        \"\"\"\n        self._progress_bar.setRange(0, 0)\n        self._progress_bar.setValue(progress)\n        text_visible = self._progress_bar.isTextVisible()\n        if progress == 0:\n            if text_visible:\n                self._progress_bar.setTextVisible(False)\n        elif not text_visible:\n            self._progress_bar.setTextVisible(True)\n        \"\"\"\n        return\n\n    def update_console(self, msg: str, error: bool = False) -> None:\n        \"\"\"Display message in console.\n\n        Args:\n            msg (str): message.\n            error (bool): if True, print it red.\n        \"\"\"\n        print(msg)\n"
  },
  {
    "path": "igniter/user_settings.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Package to deal with saving and retrieving user specific settings.\"\"\"\nimport os\nfrom datetime import datetime\nfrom abc import ABCMeta, abstractmethod\nimport json\n\n# disable lru cache in Python 2\ntry:\n    from functools import lru_cache\nexcept ImportError:\n    def lru_cache(maxsize):\n        def max_size(func):\n            def wrapper(*args, **kwargs):\n                value = func(*args, **kwargs)\n                return value\n            return wrapper\n        return max_size\n\n# ConfigParser was renamed in python3 to configparser\ntry:\n    import configparser\nexcept ImportError:\n    import ConfigParser as configparser\n\nimport platform\n\nimport six\nimport appdirs\n\n_PLACEHOLDER = object()\n\n\nclass OpenPypeSecureRegistry:\n    \"\"\"Store information using keyring.\n\n    Registry should be used for private data that should be available only for\n    user.\n\n    All passed registry names will have added prefix `OpenPype/` to easier\n    identify which data were created by OpenPype.\n\n    Args:\n        name(str): Name of registry used as identifier for data.\n    \"\"\"\n    def __init__(self, name):\n        try:\n            import keyring\n\n        except Exception:\n            raise NotImplementedError(\n                \"Python module `keyring` is not available.\"\n            )\n\n        # hack for cx_freeze and Windows keyring backend\n        if platform.system().lower() == \"windows\":\n            from keyring.backends import Windows\n\n            keyring.set_keyring(Windows.WinVaultKeyring())\n\n        # Force \"OpenPype\" prefix\n        self._name = \"/\".join((\"OpenPype\", name))\n\n    def set_item(self, name, value):\n        # type: (str, str) -> None\n        \"\"\"Set sensitive item into system's keyring.\n\n        This uses `Keyring module`_ to save sensitive stuff into system's\n        keyring.\n\n        Args:\n            name (str): Name of the item.\n            value (str): Value of the item.\n\n        .. _Keyring module:\n            https://github.com/jaraco/keyring\n\n        \"\"\"\n        import keyring\n\n        keyring.set_password(self._name, name, value)\n\n    @lru_cache(maxsize=32)\n    def get_item(self, name, default=_PLACEHOLDER):\n        \"\"\"Get value of sensitive item from system's keyring.\n\n        See also `Keyring module`_\n\n        Args:\n            name (str): Name of the item.\n            default (Any): Default value if item is not available.\n\n        Returns:\n            value (str): Value of the item.\n\n        Raises:\n            ValueError: If item doesn't exist and default is not defined.\n\n        .. _Keyring module:\n            https://github.com/jaraco/keyring\n\n        \"\"\"\n        import keyring\n\n        value = keyring.get_password(self._name, name)\n        if value:\n            return value\n\n        if default is not _PLACEHOLDER:\n            return default\n\n        # NOTE Should raise `KeyError`\n        raise ValueError(\n            \"Item {}:{} does not exist in keyring.\".format(self._name, name)\n        )\n\n    def delete_item(self, name):\n        # type: (str) -> None\n        \"\"\"Delete value stored in system's keyring.\n\n        See also `Keyring module`_\n\n        Args:\n            name (str): Name of the item to be deleted.\n\n        .. _Keyring module:\n            https://github.com/jaraco/keyring\n\n        \"\"\"\n        import keyring\n\n        self.get_item.cache_clear()\n        keyring.delete_password(self._name, name)\n\n\n@six.add_metaclass(ABCMeta)\nclass ASettingRegistry():\n    \"\"\"Abstract class defining structure of **SettingRegistry** class.\n\n    It is implementing methods to store secure items into keyring, otherwise\n    mechanism for storing common items must be implemented in abstract\n    methods.\n\n    Attributes:\n        _name (str): Registry names.\n\n    \"\"\"\n\n    def __init__(self, name):\n        # type: (str) -> ASettingRegistry\n        super(ASettingRegistry, self).__init__()\n\n        self._name = name\n        self._items = {}\n\n    def set_item(self, name, value):\n        # type: (str, str) -> None\n        \"\"\"Set item to settings registry.\n\n        Args:\n            name (str): Name of the item.\n            value (str): Value of the item.\n\n        \"\"\"\n        self._set_item(name, value)\n\n    @abstractmethod\n    def _set_item(self, name, value):\n        # type: (str, str) -> None\n        # Implement it\n        pass\n\n    def __setitem__(self, name, value):\n        self._items[name] = value\n        self._set_item(name, value)\n\n    def get_item(self, name):\n        # type: (str) -> str\n        \"\"\"Get item from settings registry.\n\n        Args:\n            name (str): Name of the item.\n\n        Returns:\n            value (str): Value of the item.\n\n        Raises:\n            ValueError: If item doesn't exist.\n\n        \"\"\"\n        return self._get_item(name)\n\n    @abstractmethod\n    def _get_item(self, name):\n        # type: (str) -> str\n        # Implement it\n        pass\n\n    def __getitem__(self, name):\n        return self._get_item(name)\n\n    def delete_item(self, name):\n        # type: (str) -> None\n        \"\"\"Delete item from settings registry.\n\n        Args:\n            name (str): Name of the item.\n\n        \"\"\"\n        self._delete_item(name)\n\n    @abstractmethod\n    def _delete_item(self, name):\n        # type: (str) -> None\n        \"\"\"Delete item from settings.\n\n        Note:\n            see :meth:`openpype.lib.user_settings.ARegistrySettings.delete_item`\n\n        \"\"\"\n        pass\n\n    def __delitem__(self, name):\n        del self._items[name]\n        self._delete_item(name)\n\n\nclass IniSettingRegistry(ASettingRegistry):\n    \"\"\"Class using :mod:`configparser`.\n\n    This class is using :mod:`configparser` (ini) files to store items.\n\n    \"\"\"\n\n    def __init__(self, name, path):\n        # type: (str, str) -> IniSettingRegistry\n        super(IniSettingRegistry, self).__init__(name)\n        # get registry file\n        version = os.getenv(\"OPENPYPE_VERSION\", \"N/A\")\n        self._registry_file = os.path.join(path, \"{}.ini\".format(name))\n        if not os.path.exists(self._registry_file):\n            with open(self._registry_file, mode=\"w\") as cfg:\n                print(\"# Settings registry\", cfg)\n                print(\"# Generated by OpenPype {}\".format(version), cfg)\n                now = datetime.now().strftime(\"%d/%m/%Y %H:%M:%S\")\n                print(\"# {}\".format(now), cfg)\n\n    def set_item_section(\n            self, section, name, value):\n        # type: (str, str, str) -> None\n        \"\"\"Set item to specific section of ini registry.\n\n        If section doesn't exists, it is created.\n\n        Args:\n            section (str): Name of section.\n            name (str): Name of the item.\n            value (str): Value of the item.\n\n        \"\"\"\n        value = str(value)\n        config = configparser.ConfigParser()\n\n        config.read(self._registry_file)\n        if not config.has_section(section):\n            config.add_section(section)\n        current = config[section]\n        current[name] = value\n\n        with open(self._registry_file, mode=\"w\") as cfg:\n            config.write(cfg)\n\n    def _set_item(self, name, value):\n        # type: (str, str) -> None\n        self.set_item_section(\"MAIN\", name, value)\n\n    def set_item(self, name, value):\n        # type: (str, str) -> None\n        \"\"\"Set item to settings ini file.\n\n        This saves item to ``DEFAULT`` section of ini as each item there\n        must reside in some section.\n\n        Args:\n            name (str): Name of the item.\n            value (str): Value of the item.\n\n        \"\"\"\n        # this does the some, overridden just for different docstring.\n        # we cast value to str as ini options values must be strings.\n        super(IniSettingRegistry, self).set_item(name, str(value))\n\n    def get_item(self, name):\n        # type: (str) -> str\n        \"\"\"Gets item from settings ini file.\n\n        This gets settings from ``DEFAULT`` section of ini file as each item\n        there must reside in some section.\n\n        Args:\n            name (str): Name of the item.\n\n        Returns:\n            str: Value of item.\n\n        Raises:\n            ValueError: If value doesn't exist.\n\n        \"\"\"\n        return super(IniSettingRegistry, self).get_item(name)\n\n    @lru_cache(maxsize=32)\n    def get_item_from_section(self, section, name):\n        # type: (str, str) -> str\n        \"\"\"Get item from section of ini file.\n\n        This will read ini file and try to get item value from specified\n        section. If that section or item doesn't exist, :exc:`ValueError`\n        is risen.\n\n        Args:\n            section (str): Name of ini section.\n            name (str): Name of the item.\n\n        Returns:\n            str: Item value.\n\n        Raises:\n            ValueError: If value doesn't exist.\n\n        \"\"\"\n        config = configparser.ConfigParser()\n        config.read(self._registry_file)\n        try:\n            value = config[section][name]\n        except KeyError:\n            raise ValueError(\n                \"Registry doesn't contain value {}:{}\".format(section, name))\n        return value\n\n    def _get_item(self, name):\n        # type: (str) -> str\n        return self.get_item_from_section(\"MAIN\", name)\n\n    def delete_item_from_section(self, section, name):\n        # type: (str, str) -> None\n        \"\"\"Delete item from section in ini file.\n\n        Args:\n            section (str): Section name.\n            name (str): Name of the item.\n\n        Raises:\n            ValueError: If item doesn't exist.\n\n        \"\"\"\n        self.get_item_from_section.cache_clear()\n        config = configparser.ConfigParser()\n        config.read(self._registry_file)\n        try:\n            _ = config[section][name]\n        except KeyError:\n            raise ValueError(\n                \"Registry doesn't contain value {}:{}\".format(section, name))\n        config.remove_option(section, name)\n\n        # if section is empty, delete it\n        if len(config[section].keys()) == 0:\n            config.remove_section(section)\n\n        with open(self._registry_file, mode=\"w\") as cfg:\n            config.write(cfg)\n\n    def _delete_item(self, name):\n        \"\"\"Delete item from default section.\n\n        Note:\n            See :meth:`~openpype.lib.IniSettingsRegistry.delete_item_from_section`\n\n        \"\"\"\n        self.delete_item_from_section(\"MAIN\", name)\n\n\nclass JSONSettingRegistry(ASettingRegistry):\n    \"\"\"Class using json file as storage.\"\"\"\n\n    def __init__(self, name, path):\n        # type: (str, str) -> JSONSettingRegistry\n        super(JSONSettingRegistry, self).__init__(name)\n        #: str: name of registry file\n        self._registry_file = os.path.join(path, \"{}.json\".format(name))\n        now = datetime.now().strftime(\"%d/%m/%Y %H:%M:%S\")\n        header = {\n            \"__metadata__\": {\n                \"openpype-version\": os.getenv(\"OPENPYPE_VERSION\", \"N/A\"),\n                \"generated\": now\n            },\n            \"registry\": {}\n        }\n\n        if not os.path.exists(os.path.dirname(self._registry_file)):\n            os.makedirs(os.path.dirname(self._registry_file), exist_ok=True)\n        if not os.path.exists(self._registry_file):\n            with open(self._registry_file, mode=\"w\") as cfg:\n                json.dump(header, cfg, indent=4)\n\n    @lru_cache(maxsize=32)\n    def _get_item(self, name):\n        # type: (str) -> object\n        \"\"\"Get item value from registry json.\n\n        Note:\n            See :meth:`openpype.lib.JSONSettingRegistry.get_item`\n\n        \"\"\"\n        with open(self._registry_file, mode=\"r\") as cfg:\n            data = json.load(cfg)\n            try:\n                value = data[\"registry\"][name]\n            except KeyError:\n                raise ValueError(\n                    \"Registry doesn't contain value {}\".format(name))\n        return value\n\n    def get_item(self, name):\n        # type: (str) -> object\n        \"\"\"Get item value from registry json.\n\n        Args:\n            name (str): Name of the item.\n\n        Returns:\n            value of the item\n\n        Raises:\n            ValueError: If item is not found in registry file.\n\n        \"\"\"\n        return self._get_item(name)\n\n    def _set_item(self, name, value):\n        # type: (str, object) -> None\n        \"\"\"Set item value to registry json.\n\n        Note:\n            See :meth:`openpype.lib.JSONSettingRegistry.set_item`\n\n        \"\"\"\n        with open(self._registry_file, \"r+\") as cfg:\n            data = json.load(cfg)\n            data[\"registry\"][name] = value\n            cfg.truncate(0)\n            cfg.seek(0)\n            json.dump(data, cfg, indent=4)\n\n    def set_item(self, name, value):\n        # type: (str, object) -> None\n        \"\"\"Set item and its value into json registry file.\n\n        Args:\n            name (str): name of the item.\n            value (Any): value of the item.\n\n        \"\"\"\n        self._set_item(name, value)\n\n    def _delete_item(self, name):\n        # type: (str) -> None\n        self._get_item.cache_clear()\n        with open(self._registry_file, \"r+\") as cfg:\n            data = json.load(cfg)\n            del data[\"registry\"][name]\n            cfg.truncate(0)\n            cfg.seek(0)\n            json.dump(data, cfg, indent=4)\n\n\nclass OpenPypeSettingsRegistry(JSONSettingRegistry):\n    \"\"\"Class handling OpenPype general settings registry.\n\n    Attributes:\n        vendor (str): Name used for path construction.\n        product (str): Additional name used for path construction.\n\n    \"\"\"\n\n    def __init__(self, name=None):\n        self.vendor = \"pypeclub\"\n        self.product = \"openpype\"\n        if not name:\n            name = \"openpype_settings\"\n        path = appdirs.user_data_dir(self.product, self.vendor)\n        super(OpenPypeSettingsRegistry, self).__init__(name, path)\n"
  },
  {
    "path": "igniter/version.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Definition of Igniter version.\"\"\"\n\n__version__ = \"1.0.2\"\n"
  },
  {
    "path": "inno_setup.iss",
    "content": "; Script generated by the Inno Setup Script Wizard.\n; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!\n\n\n#define MyAppName \"OpenPype\"\n#define Build GetEnv(\"BUILD_DIR\")\n#define AppVer GetEnv(\"BUILD_VERSION\")\n\n\n[Setup]\n; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.\n; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)\nAppId={{B9E9DF6A-5BDA-42DD-9F35-C09D564C4D93}\nAppName={#MyAppName}\nAppVersion={#AppVer}\nAppVerName={#MyAppName} version {#AppVer}\nAppPublisher=Ynput s.r.o\nAppPublisherURL=https://ynput.io\nAppSupportURL=https://ynput.io\nAppUpdatesURL=https://ynput.io\nDefaultDirName={autopf}\\{#MyAppName}\\{#AppVer}\nUsePreviousAppDir=no\nDisableProgramGroupPage=yes\nOutputBaseFilename={#MyAppName}-{#AppVer}-install\nAllowCancelDuringInstall=yes\n; Uncomment the following line to run in non administrative install mode (install for current user only.)\n;PrivilegesRequired=lowest\nPrivilegesRequiredOverridesAllowed=dialog\nSetupIconFile=igniter\\openpype.ico\nOutputDir=build\\\nCompression=lzma2\nSolidCompression=yes\nWizardStyle=modern\n\n[Languages]\nName: \"english\"; MessagesFile: \"compiler:Default.isl\"\n\n[Tasks]\nName: \"desktopicon\"; Description: \"{cm:CreateDesktopIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"\n\n[InstallDelete]\n; clean everything in previous installation folder\nType: filesandordirs; Name: \"{app}\\*\"\n\n\n[Files]\nSource: \"build\\{#build}\\*\"; DestDir: \"{app}\"; Flags: ignoreversion recursesubdirs createallsubdirs\n; NOTE: Don't use \"Flags: ignoreversion\" on any shared system files\n\n[Icons]\nName: \"{autoprograms}\\{#MyAppName} {#AppVer}\"; Filename: \"{app}\\openpype_gui.exe\"\nName: \"{autodesktop}\\{#MyAppName} {#AppVer}\"; Filename: \"{app}\\openpype_gui.exe\"; Tasks: desktopicon\n\n[Run]\nFilename: \"{app}\\openpype_gui.exe\"; Description: \"{cm:LaunchProgram,OpenPype}\"; Flags: nowait postinstall skipifsilent\n"
  },
  {
    "path": "openpype/__init__.py",
    "content": "import os\n\n\nPACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))\nPLUGINS_DIR = os.path.join(PACKAGE_DIR, \"plugins\")\n\nAYON_SERVER_ENABLED = os.environ.get(\"USE_AYON_SERVER\") == \"1\"\n"
  },
  {
    "path": "openpype/__main__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Main entry point for Pype command.\"\"\"\nfrom . import cli\nimport sys\nimport traceback\n\nif __name__ == '__main__':\n    try:\n        cli.main(obj={}, prog_name=\"pype\")\n    except Exception:\n        exc_info = sys.exc_info()\n        print(\"!!! Pype crashed:\")\n        traceback.print_exception(*exc_info)\n        sys.exit(1)\n"
  },
  {
    "path": "openpype/addons/README.md",
    "content": "This directory is for storing external addons that needs to be included in the pipeline when distributed.\n\nThe directory is ignored by Git, but included in the zip and installation files.\n"
  },
  {
    "path": "openpype/cli.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Package for handling pype command line arguments.\"\"\"\nimport os\nimport sys\nimport code\nimport click\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom .pype_commands import PypeCommands\n\n\nclass AliasedGroup(click.Group):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._aliases = {}\n\n    def set_alias(self, src_name, dst_name):\n        self._aliases[dst_name] = src_name\n\n    def get_command(self, ctx, cmd_name):\n        if cmd_name in self._aliases:\n            cmd_name = self._aliases[cmd_name]\n        return super().get_command(ctx, cmd_name)\n\n\n@click.group(cls=AliasedGroup, invoke_without_command=True)\n@click.pass_context\n@click.option(\"--use-version\",\n              expose_value=False, help=\"use specified version\")\n@click.option(\"--use-staging\", is_flag=True,\n              expose_value=False, help=\"use staging variants\")\n@click.option(\"--list-versions\", is_flag=True, expose_value=False,\n              help=\"list all detected versions.\")\n@click.option(\"--validate-version\", expose_value=False,\n              help=\"validate given version integrity\")\n@click.option(\"--debug\", is_flag=True, expose_value=False,\n              help=\"Enable debug\")\n@click.option(\"--verbose\", expose_value=False,\n              help=(\"Change OpenPype log level (debug - critical or 0-50)\"))\n@click.option(\"--automatic-tests\", is_flag=True, expose_value=False,\n              help=(\"Run in automatic tests mode\"))\ndef main(ctx):\n    \"\"\"Pype is main command serving as entry point to pipeline system.\n\n    It wraps different commands together.\n    \"\"\"\n\n    if ctx.invoked_subcommand is None:\n        # Print help if headless mode is used\n        if AYON_SERVER_ENABLED:\n            is_headless = os.getenv(\"AYON_HEADLESS_MODE\") == \"1\"\n        else:\n            is_headless = os.getenv(\"OPENPYPE_HEADLESS_MODE\") == \"1\"\n        if is_headless:\n            print(ctx.get_help())\n            sys.exit(0)\n        else:\n            ctx.invoke(tray)\n\n\n@main.command()\n@click.option(\"-d\", \"--dev\", is_flag=True, help=\"Settings in Dev mode\")\ndef settings(dev):\n    \"\"\"Show Pype Settings UI.\"\"\"\n\n    if AYON_SERVER_ENABLED:\n        raise RuntimeError(\"AYON does not support 'settings' command.\")\n    PypeCommands().launch_settings_gui(dev)\n\n\n@main.command()\ndef tray():\n    \"\"\"Launch pype tray.\n\n    Default action of pype command is to launch tray widget to control basic\n    aspects of pype. See documentation for more information.\n    \"\"\"\n    PypeCommands().launch_tray()\n\n\n@PypeCommands.add_modules\n@main.group(help=\"Run command line arguments of OpenPype addons\")\n@click.pass_context\ndef module(ctx):\n    \"\"\"Addon specific commands created dynamically.\n\n    These commands are generated dynamically by currently loaded addons.\n    \"\"\"\n    pass\n\n\n# Add 'addon' as alias for module\nmain.set_alias(\"module\", \"addon\")\n\n\n@main.command()\n@click.option(\"--ftrack-url\", envvar=\"FTRACK_SERVER\",\n              help=\"Ftrack server url\")\n@click.option(\"--ftrack-user\", envvar=\"FTRACK_API_USER\",\n              help=\"Ftrack api user\")\n@click.option(\"--ftrack-api-key\", envvar=\"FTRACK_API_KEY\",\n              help=\"Ftrack api key\")\n@click.option(\"--legacy\", is_flag=True,\n              help=\"run event server without mongo storing\")\n@click.option(\"--clockify-api-key\", envvar=\"CLOCKIFY_API_KEY\",\n              help=\"Clockify API key.\")\n@click.option(\"--clockify-workspace\", envvar=\"CLOCKIFY_WORKSPACE\",\n              help=\"Clockify workspace\")\ndef eventserver(ftrack_url,\n                ftrack_user,\n                ftrack_api_key,\n                legacy,\n                clockify_api_key,\n                clockify_workspace):\n    \"\"\"Launch ftrack event server.\n\n    This should be ideally used by system service (such us systemd or upstart\n    on linux and window service).\n    \"\"\"\n\n    if AYON_SERVER_ENABLED:\n        raise RuntimeError(\"AYON does not support 'eventserver' command.\")\n    PypeCommands().launch_eventservercli(\n        ftrack_url,\n        ftrack_user,\n        ftrack_api_key,\n        legacy,\n        clockify_api_key,\n        clockify_workspace\n    )\n\n\n@main.command()\n@click.option(\"-h\", \"--host\", help=\"Host\", default=None)\n@click.option(\"-p\", \"--port\", help=\"Port\", default=None)\n@click.option(\"-e\", \"--executable\", help=\"Executable\")\n@click.option(\"-u\", \"--upload_dir\", help=\"Upload dir\")\ndef webpublisherwebserver(executable, upload_dir, host=None, port=None):\n    \"\"\"Starts webserver for communication with Webpublish FR via command line\n\n        OP must be congigured on a machine, eg. OPENPYPE_MONGO filled AND\n        FTRACK_BOT_API_KEY provided with api key from Ftrack.\n\n        Expect \"pype.club\" user created on Ftrack.\n    \"\"\"\n\n    if AYON_SERVER_ENABLED:\n        raise RuntimeError(\n            \"AYON does not support 'webpublisherwebserver' command.\"\n        )\n    PypeCommands().launch_webpublisher_webservercli(\n        upload_dir=upload_dir,\n        executable=executable,\n        host=host,\n        port=port\n    )\n\n\n@main.command()\n@click.argument(\"output_json_path\")\n@click.option(\"--project\", help=\"Project name\", default=None)\n@click.option(\"--asset\", help=\"Asset name\", default=None)\n@click.option(\"--task\", help=\"Task name\", default=None)\n@click.option(\"--app\", help=\"Application name\", default=None)\n@click.option(\n    \"--envgroup\", help=\"Environment group (e.g. \\\"farm\\\")\", default=None\n)\ndef extractenvironments(output_json_path, project, asset, task, app, envgroup):\n    \"\"\"Extract environment variables for entered context to a json file.\n\n    Entered output filepath will be created if does not exists.\n\n    All context options must be passed otherwise only pype's global\n    environments will be extracted.\n\n    Context options are \"project\", \"asset\", \"task\", \"app\"\n    \"\"\"\n    PypeCommands.extractenvironments(\n        output_json_path, project, asset, task, app, envgroup\n    )\n\n\n@main.command()\n@click.argument(\"paths\", nargs=-1)\n@click.option(\"-t\", \"--targets\", help=\"Targets module\", default=None,\n              multiple=True)\n@click.option(\"-g\", \"--gui\", is_flag=True,\n              help=\"Show Publish UI\", default=False)\ndef publish(paths, targets, gui):\n    \"\"\"Start CLI publishing.\n\n    Publish collects json from paths provided as an argument.\n    More than one path is allowed.\n    \"\"\"\n\n    PypeCommands.publish(list(paths), targets, gui)\n\n\n@main.command(context_settings={\"ignore_unknown_options\": True})\ndef projectmanager():\n    if AYON_SERVER_ENABLED:\n        raise RuntimeError(\"AYON does not support 'projectmanager' command.\")\n    PypeCommands().launch_project_manager()\n\n\n@main.command(context_settings={\"ignore_unknown_options\": True})\ndef publish_report_viewer():\n    from openpype.tools.publisher.publish_report_viewer import main\n\n    sys.exit(main())\n\n\n@main.command()\n@click.argument(\"output_path\")\n@click.option(\"--project\", help=\"Define project context\")\n@click.option(\"--asset\", help=\"Define asset in project (project must be set)\")\n@click.option(\n    \"--strict\",\n    is_flag=True,\n    help=\"Full context must be set otherwise dialog can't be closed.\"\n)\ndef contextselection(\n    output_path,\n    project,\n    asset,\n    strict\n):\n    \"\"\"Show Qt dialog to select context.\n\n    Context is project name, asset name and task name. The result is stored\n    into json file which path is passed in first argument.\n    \"\"\"\n    PypeCommands.contextselection(\n        output_path,\n        project,\n        asset,\n        strict\n    )\n\n\n@main.command(\n    context_settings=dict(\n        ignore_unknown_options=True,\n        allow_extra_args=True))\n@click.argument(\"script\", required=True, type=click.Path(exists=True))\ndef run(script):\n    \"\"\"Run python script in Pype context.\"\"\"\n    import runpy\n\n    if not script:\n        print(\"Error: missing path to script file.\")\n    else:\n\n        args = sys.argv\n        args.remove(\"run\")\n        args.remove(script)\n        sys.argv = args\n        args_string = \" \".join(args[1:])\n        print(f\"... running: {script} {args_string}\")\n        runpy.run_path(script, run_name=\"__main__\", )\n\n\n@main.command()\n@click.argument(\"folder\", nargs=-1)\n@click.option(\"-m\",\n              \"--mark\",\n              help=\"Run tests marked by\",\n              default=None)\n@click.option(\"-p\",\n              \"--pyargs\",\n              help=\"Run tests from package\",\n              default=None)\n@click.option(\"-t\",\n              \"--test_data_folder\",\n              help=\"Unzipped directory path of test file\",\n              default=None)\n@click.option(\"-s\",\n              \"--persist\",\n              help=\"Persist test DB and published files after test end\",\n              default=None)\n@click.option(\"-a\",\n              \"--app_variant\",\n              help=\"Provide specific app variant for test, empty for latest\",\n              default=None)\n@click.option(\"--app_group\",\n              help=\"Provide specific app group for test, empty for default\",\n              default=None)\n@click.option(\"-t\",\n              \"--timeout\",\n              help=\"Provide specific timeout value for test case\",\n              default=None)\n@click.option(\"-so\",\n              \"--setup_only\",\n              help=\"Only create dbs, do not run tests\",\n              default=None)\n@click.option(\"--mongo_url\",\n              help=\"MongoDB for testing.\",\n              default=None)\n@click.option(\"--dump_databases\",\n              help=\"Dump all databases to data folder.\",\n              default=None)\ndef runtests(folder, mark, pyargs, test_data_folder, persist, app_variant,\n             timeout, setup_only, mongo_url, app_group, dump_databases):\n    \"\"\"Run all automatic tests after proper initialization via start.py\"\"\"\n    PypeCommands().run_tests(folder, mark, pyargs, test_data_folder,\n                             persist, app_variant, timeout, setup_only,\n                             mongo_url, app_group, dump_databases)\n\n\n@main.command(help=\"DEPRECATED - run sync server\")\n@click.pass_context\n@click.option(\"-a\", \"--active_site\", required=True,\n              help=\"Name of active site\")\ndef syncserver(ctx, active_site):\n    \"\"\"Run sync site server in background.\n\n    Deprecated:\n        This command is deprecated and will be removed in future versions.\n        Use '~/openpype_console module sync_server syncservice' instead.\n\n    Details:\n        Some Site Sync use cases need to expose site to another one.\n        For example if majority of artists work in studio, they are not using\n        SS at all, but if you want to expose published assets to 'studio' site\n        to SFTP for only a couple of artists, some background process must\n        mark published assets to live on multiple sites (they might be\n        physically in same location - mounted shared disk).\n\n        Process mimics OP Tray with specific 'active_site' name, all\n        configuration for this \"dummy\" user comes from Setting or Local\n        Settings (configured by starting OP Tray with env\n        var OPENPYPE_LOCAL_ID set to 'active_site'.\n    \"\"\"\n\n    if AYON_SERVER_ENABLED:\n        raise RuntimeError(\"AYON does not support 'syncserver' command.\")\n\n    from openpype.modules.sync_server.sync_server_module import (\n        syncservice)\n    ctx.invoke(syncservice, active_site=active_site)\n\n\n@main.command()\n@click.argument(\"directory\")\ndef repack_version(directory):\n    \"\"\"Repack OpenPype version from directory.\n\n    This command will re-create zip file from specified directory,\n    recalculating file checksums. It will try to use version detected in\n    directory name.\n    \"\"\"\n    if AYON_SERVER_ENABLED:\n        raise RuntimeError(\"AYON does not support 'repack-version' command.\")\n    PypeCommands().repack_version(directory)\n\n\n@main.command()\n@click.option(\"--project\", help=\"Project name\")\n@click.option(\n    \"--dirpath\", help=\"Directory where package is stored\", default=None)\n@click.option(\n    \"--dbonly\", help=\"Store only Database data\", default=False, is_flag=True)\ndef pack_project(project, dirpath, dbonly):\n    \"\"\"Create a package of project with all files and database dump.\"\"\"\n\n    if AYON_SERVER_ENABLED:\n        raise RuntimeError(\"AYON does not support 'pack-project' command.\")\n    PypeCommands().pack_project(project, dirpath, dbonly)\n\n\n@main.command()\n@click.option(\"--zipfile\", help=\"Path to zip file\")\n@click.option(\n    \"--root\", help=\"Replace root which was stored in project\", default=None\n)\n@click.option(\n    \"--dbonly\", help=\"Store only Database data\", default=False, is_flag=True)\ndef unpack_project(zipfile, root, dbonly):\n    \"\"\"Create a package of project with all files and database dump.\"\"\"\n    if AYON_SERVER_ENABLED:\n        raise RuntimeError(\"AYON does not support 'unpack-project' command.\")\n    PypeCommands().unpack_project(zipfile, root, dbonly)\n\n\n@main.command()\ndef interactive():\n    \"\"\"Interactive (Python like) console.\n\n    Helpful command not only for development to directly work with python\n    interpreter.\n\n    Warning:\n        Executable 'openpype_gui' on Windows won't work.\n    \"\"\"\n\n    if AYON_SERVER_ENABLED:\n        version = os.environ[\"AYON_VERSION\"]\n        banner = (\n            f\"AYON launcher {version}\\nPython {sys.version} on {sys.platform}\"\n        )\n    else:\n        from openpype.version import __version__\n\n        banner = (\n            f\"OpenPype {__version__}\\nPython {sys.version} on {sys.platform}\"\n        )\n    code.interact(banner)\n\n\n@main.command()\n@click.option(\"--build\", help=\"Print only build version\",\n              is_flag=True, default=False)\ndef version(build):\n    \"\"\"Print OpenPype version.\"\"\"\n    if AYON_SERVER_ENABLED:\n        print(os.environ[\"AYON_VERSION\"])\n        return\n\n    from openpype.version import __version__\n    from igniter.bootstrap_repos import BootstrapRepos, OpenPypeVersion\n    from pathlib import Path\n\n    if getattr(sys, 'frozen', False):\n        local_version = BootstrapRepos.get_version(\n            Path(os.getenv(\"OPENPYPE_ROOT\")))\n    else:\n        local_version = OpenPypeVersion.get_installed_version_str()\n\n    if build:\n        print(local_version)\n        return\n    print(f\"{__version__} (booted: {local_version})\")\n"
  },
  {
    "path": "openpype/client/__init__.py",
    "content": "from .mongo import (\n    OpenPypeMongoConnection,\n)\nfrom .server.utils import get_ayon_server_api_connection\n\nfrom .entities import (\n    get_projects,\n    get_project,\n    get_whole_project,\n\n    get_asset_by_id,\n    get_asset_by_name,\n    get_assets,\n    get_archived_assets,\n    get_asset_ids_with_subsets,\n\n    get_subset_by_id,\n    get_subset_by_name,\n    get_subsets,\n    get_subset_families,\n\n    get_version_by_id,\n    get_version_by_name,\n    get_versions,\n    get_hero_version_by_id,\n    get_hero_version_by_subset_id,\n    get_hero_versions,\n    get_last_versions,\n    get_last_version_by_subset_id,\n    get_last_version_by_subset_name,\n    get_output_link_versions,\n\n    version_is_latest,\n\n    get_representation_by_id,\n    get_representation_by_name,\n    get_representations,\n    get_representation_parents,\n    get_representations_parents,\n    get_archived_representations,\n\n    get_thumbnail,\n    get_thumbnails,\n    get_thumbnail_id_from_source,\n\n    get_workfile_info,\n\n    get_asset_name_identifier,\n)\n\nfrom .entity_links import (\n    get_linked_asset_ids,\n    get_linked_assets,\n    get_linked_representation_id,\n)\n\nfrom .operations import (\n    create_project,\n)\n\n\n__all__ = (\n    \"OpenPypeMongoConnection\",\n\n    \"get_ayon_server_api_connection\",\n\n    \"get_projects\",\n    \"get_project\",\n    \"get_whole_project\",\n\n    \"get_asset_by_id\",\n    \"get_asset_by_name\",\n    \"get_assets\",\n    \"get_archived_assets\",\n    \"get_asset_ids_with_subsets\",\n\n    \"get_subset_by_id\",\n    \"get_subset_by_name\",\n    \"get_subsets\",\n    \"get_subset_families\",\n\n    \"get_version_by_id\",\n    \"get_version_by_name\",\n    \"get_versions\",\n    \"get_hero_version_by_id\",\n    \"get_hero_version_by_subset_id\",\n    \"get_hero_versions\",\n    \"get_last_versions\",\n    \"get_last_version_by_subset_id\",\n    \"get_last_version_by_subset_name\",\n    \"get_output_link_versions\",\n\n    \"version_is_latest\",\n\n    \"get_representation_by_id\",\n    \"get_representation_by_name\",\n    \"get_representations\",\n    \"get_representation_parents\",\n    \"get_representations_parents\",\n    \"get_archived_representations\",\n\n    \"get_thumbnail\",\n    \"get_thumbnails\",\n    \"get_thumbnail_id_from_source\",\n\n    \"get_workfile_info\",\n\n    \"get_linked_asset_ids\",\n    \"get_linked_assets\",\n    \"get_linked_representation_id\",\n\n    \"create_project\",\n\n    \"get_asset_name_identifier\",\n)\n"
  },
  {
    "path": "openpype/client/entities.py",
    "content": "from openpype import AYON_SERVER_ENABLED\n\nif not AYON_SERVER_ENABLED:\n    from .mongo.entities import *\nelse:\n    from .server.entities import *\n\n\ndef get_asset_name_identifier(asset_doc):\n    \"\"\"Get asset name identifier by asset document.\n\n    This function is added because of AYON implementation where name\n        identifier is not just a name but full path.\n\n    Asset document must have \"name\" key, and \"data.parents\" when in AYON mode.\n\n    Args:\n        asset_doc (dict[str, Any]): Asset document.\n    \"\"\"\n\n    if not AYON_SERVER_ENABLED:\n        return asset_doc[\"name\"]\n    parents = list(asset_doc[\"data\"][\"parents\"])\n    parents.append(asset_doc[\"name\"])\n    return \"/\" + \"/\".join(parents)\n"
  },
  {
    "path": "openpype/client/entity_links.py",
    "content": "from openpype import AYON_SERVER_ENABLED\n\nif not AYON_SERVER_ENABLED:\n    from .mongo.entity_links import *\nelse:\n    from .server.entity_links import *\n"
  },
  {
    "path": "openpype/client/mongo/__init__.py",
    "content": "from .mongo import (\n    MongoEnvNotSet,\n    get_default_components,\n    should_add_certificate_path_to_mongo_url,\n    validate_mongo_connection,\n    OpenPypeMongoConnection,\n    get_project_database,\n    get_project_connection,\n    load_json_file,\n    replace_project_documents,\n    store_project_documents,\n)\n\n\n__all__ = (\n    \"MongoEnvNotSet\",\n    \"get_default_components\",\n    \"should_add_certificate_path_to_mongo_url\",\n    \"validate_mongo_connection\",\n    \"OpenPypeMongoConnection\",\n    \"get_project_database\",\n    \"get_project_connection\",\n    \"load_json_file\",\n    \"replace_project_documents\",\n    \"store_project_documents\",\n)\n"
  },
  {
    "path": "openpype/client/mongo/entities.py",
    "content": "\"\"\"Unclear if these will have public functions like these.\n\nGoal is that most of functions here are called on (or with) an object\nthat has project name as a context (e.g. on 'ProjectEntity'?).\n\n+ We will need more specific functions doing very specific queries really fast.\n\"\"\"\n\nimport re\nimport collections\n\nimport six\nfrom bson.objectid import ObjectId\n\nfrom .mongo import get_project_database, get_project_connection\n\nPatternType = type(re.compile(\"\"))\n\n\ndef _prepare_fields(fields, required_fields=None):\n    if not fields:\n        return None\n\n    output = {\n        field: True\n        for field in fields\n    }\n    if \"_id\" not in output:\n        output[\"_id\"] = True\n\n    if required_fields:\n        for key in required_fields:\n            output[key] = True\n    return output\n\n\ndef convert_id(in_id):\n    \"\"\"Helper function for conversion of id from string to ObjectId.\n\n    Args:\n        in_id (Union[str, ObjectId, Any]): Entity id that should be converted\n            to right type for queries.\n\n    Returns:\n        Union[ObjectId, Any]: Converted ids to ObjectId or in type.\n    \"\"\"\n\n    if isinstance(in_id, six.string_types):\n        return ObjectId(in_id)\n    return in_id\n\n\ndef convert_ids(in_ids):\n    \"\"\"Helper function for conversion of ids from string to ObjectId.\n\n    Args:\n        in_ids (Iterable[Union[str, ObjectId, Any]]): List of entity ids that\n            should be converted to right type for queries.\n\n    Returns:\n        List[ObjectId]: Converted ids to ObjectId.\n    \"\"\"\n\n    _output = set()\n    for in_id in in_ids:\n        if in_id is not None:\n            _output.add(convert_id(in_id))\n    return list(_output)\n\n\ndef get_projects(active=True, inactive=False, fields=None):\n    \"\"\"Yield all project entity documents.\n\n    Args:\n        active (Optional[bool]): Include active projects. Defaults to True.\n        inactive (Optional[bool]): Include inactive projects.\n            Defaults to False.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Yields:\n        dict: Project entity data which can be reduced to specified 'fields'.\n            None is returned if project with specified filters was not found.\n    \"\"\"\n    mongodb = get_project_database()\n    for project_name in mongodb.collection_names():\n        if project_name in (\"system.indexes\",):\n            continue\n        project_doc = get_project(\n            project_name, active=active, inactive=inactive, fields=fields\n        )\n        if project_doc is not None:\n            yield project_doc\n\n\ndef get_project(project_name, active=True, inactive=True, fields=None):\n    \"\"\"Return project entity document by project name.\n\n    Args:\n        project_name (str): Name of project.\n        active (Optional[bool]): Allow active project. Defaults to True.\n        inactive (Optional[bool]): Allow inactive project. Defaults to True.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Project entity data which can be reduced to\n            specified 'fields'. None is returned if project with specified\n            filters was not found.\n    \"\"\"\n    # Skip if both are disabled\n    if not active and not inactive:\n        return None\n\n    query_filter = {\"type\": \"project\"}\n    # Keep query untouched if both should be available\n    if active and inactive:\n        pass\n\n    # Add filter to keep only active\n    elif active:\n        query_filter[\"$or\"] = [\n            {\"data.active\": {\"$exists\": False}},\n            {\"data.active\": True},\n        ]\n\n    # Add filter to keep only inactive\n    elif inactive:\n        query_filter[\"$or\"] = [\n            {\"data.active\": {\"$exists\": False}},\n            {\"data.active\": False},\n        ]\n\n    conn = get_project_connection(project_name)\n    return conn.find_one(query_filter, _prepare_fields(fields))\n\n\ndef get_whole_project(project_name):\n    \"\"\"Receive all documents from project.\n\n    Helper that can be used to get all document from whole project. For example\n    for backups etc.\n\n    Returns:\n        Cursor: Query cursor as iterable which returns all documents from\n            project collection.\n    \"\"\"\n\n    conn = get_project_connection(project_name)\n    return conn.find({})\n\n\ndef get_asset_by_id(project_name, asset_id, fields=None):\n    \"\"\"Receive asset data by its id.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        asset_id (Union[str, ObjectId]): Asset's id.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Asset entity data which can be reduced to\n            specified 'fields'. None is returned if asset with specified\n            filters was not found.\n    \"\"\"\n\n    asset_id = convert_id(asset_id)\n    if not asset_id:\n        return None\n\n    query_filter = {\"type\": \"asset\", \"_id\": asset_id}\n    conn = get_project_connection(project_name)\n    return conn.find_one(query_filter, _prepare_fields(fields))\n\n\ndef get_asset_by_name(project_name, asset_name, fields=None):\n    \"\"\"Receive asset data by its name.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        asset_name (str): Asset's name.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Asset entity data which can be reduced to\n            specified 'fields'. None is returned if asset with specified\n            filters was not found.\n    \"\"\"\n\n    if not asset_name:\n        return None\n\n    query_filter = {\"type\": \"asset\", \"name\": asset_name}\n    conn = get_project_connection(project_name)\n    return conn.find_one(query_filter, _prepare_fields(fields))\n\n\n# NOTE this could be just public function?\n# - any better variable name instead of 'standard'?\n# - same approach can be used for rest of types\ndef _get_assets(\n    project_name,\n    asset_ids=None,\n    asset_names=None,\n    parent_ids=None,\n    standard=True,\n    archived=False,\n    fields=None\n):\n    \"\"\"Assets for specified project by passed filters.\n\n    Passed filters (ids and names) are always combined so all conditions must\n    match.\n\n    To receive all assets from project just keep filters empty.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        asset_ids (Iterable[Union[str, ObjectId]]): Asset ids that should\n            be found.\n        asset_names (Iterable[str]): Name assets that should be found.\n        parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids.\n        standard (bool): Query standard assets (type 'asset').\n        archived (bool): Query archived assets (type 'archived_asset').\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Cursor: Query cursor as iterable which returns asset documents matching\n            passed filters.\n    \"\"\"\n\n    asset_types = []\n    if standard:\n        asset_types.append(\"asset\")\n    if archived:\n        asset_types.append(\"archived_asset\")\n\n    if not asset_types:\n        return []\n\n    if len(asset_types) == 1:\n        query_filter = {\"type\": asset_types[0]}\n    else:\n        query_filter = {\"type\": {\"$in\": asset_types}}\n\n    if asset_ids is not None:\n        asset_ids = convert_ids(asset_ids)\n        if not asset_ids:\n            return []\n        query_filter[\"_id\"] = {\"$in\": asset_ids}\n\n    if asset_names is not None:\n        if not asset_names:\n            return []\n        query_filter[\"name\"] = {\"$in\": list(asset_names)}\n\n    if parent_ids is not None:\n        parent_ids = convert_ids(parent_ids)\n        if not parent_ids:\n            return []\n        query_filter[\"data.visualParent\"] = {\"$in\": parent_ids}\n\n    conn = get_project_connection(project_name)\n\n    return conn.find(query_filter, _prepare_fields(fields))\n\n\ndef get_assets(\n    project_name,\n    asset_ids=None,\n    asset_names=None,\n    parent_ids=None,\n    archived=False,\n    fields=None\n):\n    \"\"\"Assets for specified project by passed filters.\n\n    Passed filters (ids and names) are always combined so all conditions must\n    match.\n\n    To receive all assets from project just keep filters empty.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        asset_ids (Iterable[Union[str, ObjectId]]): Asset ids that should\n            be found.\n        asset_names (Iterable[str]): Name assets that should be found.\n        parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids.\n        archived (bool): Add also archived assets.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Cursor: Query cursor as iterable which returns asset documents matching\n            passed filters.\n    \"\"\"\n\n    return _get_assets(\n        project_name,\n        asset_ids,\n        asset_names,\n        parent_ids,\n        True,\n        archived,\n        fields\n    )\n\n\ndef get_archived_assets(\n    project_name,\n    asset_ids=None,\n    asset_names=None,\n    parent_ids=None,\n    fields=None\n):\n    \"\"\"Archived assets for specified project by passed filters.\n\n    Passed filters (ids and names) are always combined so all conditions must\n    match.\n\n    To receive all archived assets from project just keep filters empty.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        asset_ids (Iterable[Union[str, ObjectId]]): Asset ids that should\n            be found.\n        asset_names (Iterable[str]): Name assets that should be found.\n        parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Cursor: Query cursor as iterable which returns asset documents matching\n            passed filters.\n    \"\"\"\n\n    return _get_assets(\n        project_name, asset_ids, asset_names, parent_ids, False, True, fields\n    )\n\n\ndef get_asset_ids_with_subsets(project_name, asset_ids=None):\n    \"\"\"Find out which assets have existing subsets.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        asset_ids (Iterable[Union[str, ObjectId]]): Look only for entered\n            asset ids.\n\n    Returns:\n        Iterable[ObjectId]: Asset ids that have existing subsets.\n    \"\"\"\n\n    subset_query = {\n        \"type\": \"subset\"\n    }\n    if asset_ids is not None:\n        asset_ids = convert_ids(asset_ids)\n        if not asset_ids:\n            return []\n        subset_query[\"parent\"] = {\"$in\": asset_ids}\n\n    conn = get_project_connection(project_name)\n    result = conn.aggregate([\n        {\n            \"$match\": subset_query\n        },\n        {\n            \"$group\": {\n                \"_id\": \"$parent\",\n                \"count\": {\"$sum\": 1}\n            }\n        }\n    ])\n    asset_ids_with_subsets = []\n    for item in result:\n        asset_id = item[\"_id\"]\n        count = item[\"count\"]\n        if count > 0:\n            asset_ids_with_subsets.append(asset_id)\n    return asset_ids_with_subsets\n\n\ndef get_subset_by_id(project_name, subset_id, fields=None):\n    \"\"\"Single subset entity data by its id.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        subset_id (Union[str, ObjectId]): Id of subset which should be found.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Subset entity data which can be reduced to\n            specified 'fields'. None is returned if subset with specified\n            filters was not found.\n    \"\"\"\n\n    subset_id = convert_id(subset_id)\n    if not subset_id:\n        return None\n\n    query_filters = {\"type\": \"subset\", \"_id\": subset_id}\n    conn = get_project_connection(project_name)\n    return conn.find_one(query_filters, _prepare_fields(fields))\n\n\ndef get_subset_by_name(project_name, subset_name, asset_id, fields=None):\n    \"\"\"Single subset entity data by its name and its version id.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        subset_name (str): Name of subset.\n        asset_id (Union[str, ObjectId]): Id of parent asset.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Subset entity data which can be reduced to\n            specified 'fields'. None is returned if subset with specified\n            filters was not found.\n    \"\"\"\n    if not subset_name:\n        return None\n\n    asset_id = convert_id(asset_id)\n    if not asset_id:\n        return None\n\n    query_filters = {\n        \"type\": \"subset\",\n        \"name\": subset_name,\n        \"parent\": asset_id\n    }\n    conn = get_project_connection(project_name)\n    return conn.find_one(query_filters, _prepare_fields(fields))\n\n\ndef get_subsets(\n    project_name,\n    subset_ids=None,\n    subset_names=None,\n    asset_ids=None,\n    names_by_asset_ids=None,\n    archived=False,\n    fields=None\n):\n    \"\"\"Subset entities data from one project filtered by entered filters.\n\n    Filters are additive (all conditions must pass to return subset).\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        subset_ids (Iterable[Union[str, ObjectId]]): Subset ids that should be\n            queried. Filter ignored if 'None' is passed.\n        subset_names (Iterable[str]): Subset names that should be queried.\n            Filter ignored if 'None' is passed.\n        asset_ids (Iterable[Union[str, ObjectId]]): Asset ids under which\n            should look for the subsets. Filter ignored if 'None' is passed.\n        names_by_asset_ids (dict[ObjectId, List[str]]): Complex filtering\n            using asset ids and list of subset names under the asset.\n        archived (bool): Look for archived subsets too.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Cursor: Iterable cursor yielding all matching subsets.\n    \"\"\"\n\n    subset_types = [\"subset\"]\n    if archived:\n        subset_types.append(\"archived_subset\")\n\n    if len(subset_types) == 1:\n        query_filter = {\"type\": subset_types[0]}\n    else:\n        query_filter = {\"type\": {\"$in\": subset_types}}\n\n    if asset_ids is not None:\n        asset_ids = convert_ids(asset_ids)\n        if not asset_ids:\n            return []\n        query_filter[\"parent\"] = {\"$in\": asset_ids}\n\n    if subset_ids is not None:\n        subset_ids = convert_ids(subset_ids)\n        if not subset_ids:\n            return []\n        query_filter[\"_id\"] = {\"$in\": subset_ids}\n\n    if subset_names is not None:\n        if not subset_names:\n            return []\n        query_filter[\"name\"] = {\"$in\": list(subset_names)}\n\n    if names_by_asset_ids is not None:\n        or_query = []\n        for asset_id, names in names_by_asset_ids.items():\n            if asset_id and names:\n                or_query.append({\n                    \"parent\": convert_id(asset_id),\n                    \"name\": {\"$in\": list(names)}\n                })\n        if not or_query:\n            return []\n        query_filter[\"$or\"] = or_query\n\n    conn = get_project_connection(project_name)\n    return conn.find(query_filter, _prepare_fields(fields))\n\n\ndef get_subset_families(project_name, subset_ids=None):\n    \"\"\"Set of main families of subsets.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        subset_ids (Iterable[Union[str, ObjectId]]): Subset ids that should\n            be queried. All subsets from project are used if 'None' is passed.\n\n    Returns:\n         set[str]: Main families of matching subsets.\n    \"\"\"\n\n    subset_filter = {\n        \"type\": \"subset\"\n    }\n    if subset_ids is not None:\n        if not subset_ids:\n            return set()\n        subset_filter[\"_id\"] = {\"$in\": list(subset_ids)}\n\n    conn = get_project_connection(project_name)\n    result = list(conn.aggregate([\n        {\"$match\": subset_filter},\n        {\"$project\": {\n            \"family\": {\"$arrayElemAt\": [\"$data.families\", 0]}\n        }},\n        {\"$group\": {\n            \"_id\": \"family_group\",\n            \"families\": {\"$addToSet\": \"$family\"}\n        }}\n    ]))\n    if result:\n        return set(result[0][\"families\"])\n    return set()\n\n\ndef get_version_by_id(project_name, version_id, fields=None):\n    \"\"\"Single version entity data by its id.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        version_id (Union[str, ObjectId]): Id of version which should be found.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Version entity data which can be reduced to\n            specified 'fields'. None is returned if version with specified\n            filters was not found.\n    \"\"\"\n\n    version_id = convert_id(version_id)\n    if not version_id:\n        return None\n\n    query_filter = {\n        \"type\": {\"$in\": [\"version\", \"hero_version\"]},\n        \"_id\": version_id\n    }\n    conn = get_project_connection(project_name)\n    return conn.find_one(query_filter, _prepare_fields(fields))\n\n\ndef get_version_by_name(project_name, version, subset_id, fields=None):\n    \"\"\"Single version entity data by its name and subset id.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        version (int): name of version entity (its version).\n        subset_id (Union[str, ObjectId]): Id of version which should be found.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Version entity data which can be reduced to\n            specified 'fields'. None is returned if version with specified\n            filters was not found.\n    \"\"\"\n\n    subset_id = convert_id(subset_id)\n    if not subset_id:\n        return None\n\n    conn = get_project_connection(project_name)\n    query_filter = {\n        \"type\": \"version\",\n        \"parent\": subset_id,\n        \"name\": version\n    }\n    return conn.find_one(query_filter, _prepare_fields(fields))\n\n\ndef version_is_latest(project_name, version_id):\n    \"\"\"Is version the latest from its subset.\n\n    Note:\n        Hero versions are considered as latest.\n\n    Todo:\n        Maybe raise exception when version was not found?\n\n    Args:\n        project_name (str):Name of project where to look for queried entities.\n        version_id (Union[str, ObjectId]): Version id which is checked.\n\n    Returns:\n        bool: True if is latest version from subset else False.\n    \"\"\"\n\n    version_id = convert_id(version_id)\n    if not version_id:\n        return False\n    version_doc = get_version_by_id(\n        project_name, version_id, fields=[\"_id\", \"type\", \"parent\"]\n    )\n    # What to do when version is not found?\n    if not version_doc:\n        return False\n\n    if version_doc[\"type\"] == \"hero_version\":\n        return True\n\n    last_version = get_last_version_by_subset_id(\n        project_name, version_doc[\"parent\"], fields=[\"_id\"]\n    )\n    return last_version[\"_id\"] == version_id\n\n\ndef _get_versions(\n    project_name,\n    subset_ids=None,\n    version_ids=None,\n    versions=None,\n    standard=True,\n    hero=False,\n    fields=None\n):\n    version_types = []\n    if standard:\n        version_types.append(\"version\")\n\n    if hero:\n        version_types.append(\"hero_version\")\n\n    if not version_types:\n        return []\n    elif len(version_types) == 1:\n        query_filter = {\"type\": version_types[0]}\n    else:\n        query_filter = {\"type\": {\"$in\": version_types}}\n\n    if subset_ids is not None:\n        subset_ids = convert_ids(subset_ids)\n        if not subset_ids:\n            return []\n        query_filter[\"parent\"] = {\"$in\": subset_ids}\n\n    if version_ids is not None:\n        version_ids = convert_ids(version_ids)\n        if not version_ids:\n            return []\n        query_filter[\"_id\"] = {\"$in\": version_ids}\n\n    if versions is not None:\n        versions = list(versions)\n        if not versions:\n            return []\n\n        if len(versions) == 1:\n            query_filter[\"name\"] = versions[0]\n        else:\n            query_filter[\"name\"] = {\"$in\": versions}\n\n    conn = get_project_connection(project_name)\n\n    return conn.find(query_filter, _prepare_fields(fields))\n\n\ndef get_versions(\n    project_name,\n    version_ids=None,\n    subset_ids=None,\n    versions=None,\n    hero=False,\n    fields=None\n):\n    \"\"\"Version entities data from one project filtered by entered filters.\n\n    Filters are additive (all conditions must pass to return subset).\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        version_ids (Iterable[Union[str, ObjectId]]): Version ids that will\n            be queried. Filter ignored if 'None' is passed.\n        subset_ids (Iterable[str]): Subset ids that will be queried.\n            Filter ignored if 'None' is passed.\n        versions (Iterable[int]): Version names (as integers).\n            Filter ignored if 'None' is passed.\n        hero (bool): Look also for hero versions.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Cursor: Iterable cursor yielding all matching versions.\n    \"\"\"\n\n    return _get_versions(\n        project_name,\n        subset_ids,\n        version_ids,\n        versions,\n        standard=True,\n        hero=hero,\n        fields=fields\n    )\n\n\ndef get_hero_version_by_subset_id(project_name, subset_id, fields=None):\n    \"\"\"Hero version by subset id.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        subset_id (Union[str, ObjectId]): Subset id under which\n            is hero version.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Hero version entity data which can be reduced to\n            specified 'fields'. None is returned if hero version with specified\n            filters was not found.\n    \"\"\"\n\n    subset_id = convert_id(subset_id)\n    if not subset_id:\n        return None\n\n    versions = list(_get_versions(\n        project_name,\n        subset_ids=[subset_id],\n        standard=False,\n        hero=True,\n        fields=fields\n    ))\n    if versions:\n        return versions[0]\n    return None\n\n\ndef get_hero_version_by_id(project_name, version_id, fields=None):\n    \"\"\"Hero version by its id.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        version_id (Union[str, ObjectId]): Hero version id.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Hero version entity data which can be reduced to\n            specified 'fields'. None is returned if hero version with specified\n            filters was not found.\n    \"\"\"\n\n    version_id = convert_id(version_id)\n    if not version_id:\n        return None\n\n    versions = list(_get_versions(\n        project_name,\n        version_ids=[version_id],\n        standard=False,\n        hero=True,\n        fields=fields\n    ))\n    if versions:\n        return versions[0]\n    return None\n\n\ndef get_hero_versions(\n    project_name,\n    subset_ids=None,\n    version_ids=None,\n    fields=None\n):\n    \"\"\"Hero version entities data from one project filtered by entered filters.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        subset_ids (Iterable[Union[str, ObjectId]]): Subset ids for which\n            should look for hero versions. Filter ignored if 'None' is passed.\n        version_ids (Iterable[Union[str, ObjectId]]): Hero version ids. Filter\n            ignored if 'None' is passed.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Cursor|list: Iterable yielding hero versions matching passed filters.\n    \"\"\"\n\n    return _get_versions(\n        project_name,\n        subset_ids,\n        version_ids,\n        standard=False,\n        hero=True,\n        fields=fields\n    )\n\n\ndef get_output_link_versions(project_name, version_id, fields=None):\n    \"\"\"Versions where passed version was used as input.\n\n    Question:\n        Not 100% sure about the usage of the function so the name and docstring\n            maybe does not match what it does?\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        version_id (Union[str, ObjectId]): Version id which can be used\n            as input link for other versions.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Iterable: Iterable cursor yielding versions that are used as input\n            links for passed version.\n    \"\"\"\n\n    version_id = convert_id(version_id)\n    if not version_id:\n        return []\n\n    conn = get_project_connection(project_name)\n    # Does make sense to look for hero versions?\n    query_filter = {\n        \"type\": \"version\",\n        \"data.inputLinks.id\": version_id\n    }\n    return conn.find(query_filter, _prepare_fields(fields))\n\n\ndef get_last_versions(project_name, subset_ids, active=None, fields=None):\n    \"\"\"Latest versions for entered subset_ids.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        subset_ids (Iterable[Union[str, ObjectId]]): List of subset ids.\n        active (Optional[bool]): If True only active versions are returned.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        dict[ObjectId, int]: Key is subset id and value is last version name.\n    \"\"\"\n\n    subset_ids = convert_ids(subset_ids)\n    if not subset_ids:\n        return {}\n\n    if fields is not None:\n        fields = list(fields)\n        if not fields:\n            return {}\n\n    # Avoid double query if only name and _id are requested\n    name_needed = False\n    limit_query = False\n    if fields:\n        fields_s = set(fields)\n        if \"name\" in fields_s:\n            name_needed = True\n            fields_s.remove(\"name\")\n\n        for field in (\"_id\", \"parent\"):\n            if field in fields_s:\n                fields_s.remove(field)\n        limit_query = len(fields_s) == 0\n\n    group_item = {\n        \"_id\": \"$parent\",\n        \"_version_id\": {\"$last\": \"$_id\"}\n    }\n    # Add name if name is needed (only for limit query)\n    if name_needed:\n        group_item[\"name\"] = {\"$last\": \"$name\"}\n\n    aggregate_filter = {\n        \"type\": \"version\",\n        \"parent\": {\"$in\": subset_ids}\n    }\n    if active is False:\n        aggregate_filter[\"data.active\"] = active\n    elif active is True:\n        aggregate_filter[\"$or\"] = [\n            {\"data.active\": {\"$exists\": 0}},\n            {\"data.active\": active},\n        ]\n\n    aggregation_pipeline = [\n        # Find all versions of those subsets\n        {\"$match\": aggregate_filter},\n        # Sorting versions all together\n        {\"$sort\": {\"name\": 1}},\n        # Group them by \"parent\", but only take the last\n        {\"$group\": group_item}\n    ]\n\n    conn = get_project_connection(project_name)\n    aggregate_result = conn.aggregate(aggregation_pipeline)\n    if limit_query:\n        output = {}\n        for item in aggregate_result:\n            subset_id = item[\"_id\"]\n            item_data = {\"_id\": item[\"_version_id\"], \"parent\": subset_id}\n            if name_needed:\n                item_data[\"name\"] = item[\"name\"]\n            output[subset_id] = item_data\n        return output\n\n    version_ids = [\n        doc[\"_version_id\"]\n        for doc in aggregate_result\n    ]\n\n    fields = _prepare_fields(fields, [\"parent\"])\n\n    version_docs = get_versions(\n        project_name, version_ids=version_ids, fields=fields\n    )\n\n    return {\n        version_doc[\"parent\"]: version_doc\n        for version_doc in version_docs\n    }\n\n\ndef get_last_version_by_subset_id(project_name, subset_id, fields=None):\n    \"\"\"Last version for passed subset id.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        subset_id (Union[str, ObjectId]): Id of version which should be found.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Version entity data which can be reduced to\n            specified 'fields'. None is returned if version with specified\n            filters was not found.\n    \"\"\"\n\n    subset_id = convert_id(subset_id)\n    if not subset_id:\n        return None\n\n    last_versions = get_last_versions(\n        project_name, subset_ids=[subset_id], fields=fields\n    )\n    return last_versions.get(subset_id)\n\n\ndef get_last_version_by_subset_name(\n    project_name, subset_name, asset_id=None, asset_name=None, fields=None\n):\n    \"\"\"Last version for passed subset name under asset id/name.\n\n    It is required to pass 'asset_id' or 'asset_name'. Asset id is recommended\n    if is available.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        subset_name (str): Name of subset.\n        asset_id (Union[str, ObjectId]): Asset id which is parent of passed\n            subset name.\n        asset_name (str): Asset name which is parent of passed subset name.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Version entity data which can be reduced to\n            specified 'fields'. None is returned if version with specified\n            filters was not found.\n    \"\"\"\n\n    if not asset_id and not asset_name:\n        return None\n\n    if not asset_id:\n        asset_doc = get_asset_by_name(project_name, asset_name, fields=[\"_id\"])\n        if not asset_doc:\n            return None\n        asset_id = asset_doc[\"_id\"]\n    subset_doc = get_subset_by_name(\n        project_name, subset_name, asset_id, fields=[\"_id\"]\n    )\n    if not subset_doc:\n        return None\n    return get_last_version_by_subset_id(\n        project_name, subset_doc[\"_id\"], fields=fields\n    )\n\n\ndef get_representation_by_id(project_name, representation_id, fields=None):\n    \"\"\"Representation entity data by its id.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        representation_id (Union[str, ObjectId]): Representation id.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Representation entity data which can be reduced to\n            specified 'fields'. None is returned if representation with\n            specified filters was not found.\n    \"\"\"\n\n    if not representation_id:\n        return None\n\n    repre_types = [\"representation\", \"archived_representation\"]\n    query_filter = {\n        \"type\": {\"$in\": repre_types}\n    }\n    if representation_id is not None:\n        query_filter[\"_id\"] = convert_id(representation_id)\n\n    conn = get_project_connection(project_name)\n\n    return conn.find_one(query_filter, _prepare_fields(fields))\n\n\ndef get_representation_by_name(\n    project_name, representation_name, version_id, fields=None\n):\n    \"\"\"Representation entity data by its name and its version id.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        representation_name (str): Representation name.\n        version_id (Union[str, ObjectId]): Id of parent version entity.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[dict[str, Any], None]: Representation entity data which can be\n            reduced to specified 'fields'. None is returned if representation\n            with specified filters was not found.\n    \"\"\"\n\n    version_id = convert_id(version_id)\n    if not version_id or not representation_name:\n        return None\n    repre_types = [\"representation\", \"archived_representations\"]\n    query_filter = {\n        \"type\": {\"$in\": repre_types},\n        \"name\": representation_name,\n        \"parent\": version_id\n    }\n\n    conn = get_project_connection(project_name)\n    return conn.find_one(query_filter, _prepare_fields(fields))\n\n\ndef _flatten_dict(data):\n    flatten_queue = collections.deque()\n    flatten_queue.append(data)\n    output = {}\n    while flatten_queue:\n        item = flatten_queue.popleft()\n        for key, value in item.items():\n            if not isinstance(value, dict):\n                output[key] = value\n                continue\n\n            tmp = {}\n            for subkey, subvalue in value.items():\n                new_key = \"{}.{}\".format(key, subkey)\n                tmp[new_key] = subvalue\n            flatten_queue.append(tmp)\n    return output\n\n\ndef _regex_filters(filters):\n    output = []\n    for key, value in filters.items():\n        regexes = []\n        a_values = []\n        if isinstance(value, PatternType):\n            regexes.append(value)\n        elif isinstance(value, (list, tuple, set)):\n            for item in value:\n                if isinstance(item, PatternType):\n                    regexes.append(item)\n                else:\n                    a_values.append(item)\n        else:\n            a_values.append(value)\n\n        key_filters = []\n        if len(a_values) == 1:\n            key_filters.append({key: a_values[0]})\n        elif a_values:\n            key_filters.append({key: {\"$in\": a_values}})\n\n        for regex in regexes:\n            key_filters.append({key: {\"$regex\": regex}})\n\n        if len(key_filters) == 1:\n            output.append(key_filters[0])\n        else:\n            output.append({\"$or\": key_filters})\n\n    return output\n\n\ndef _get_representations(\n    project_name,\n    representation_ids,\n    representation_names,\n    version_ids,\n    context_filters,\n    names_by_version_ids,\n    standard,\n    archived,\n    fields\n):\n    default_output = []\n    repre_types = []\n    if standard:\n        repre_types.append(\"representation\")\n    if archived:\n        repre_types.append(\"archived_representation\")\n\n    if not repre_types:\n        return default_output\n\n    if len(repre_types) == 1:\n        query_filter = {\"type\": repre_types[0]}\n    else:\n        query_filter = {\"type\": {\"$in\": repre_types}}\n\n    if representation_ids is not None:\n        representation_ids = convert_ids(representation_ids)\n        if not representation_ids:\n            return default_output\n        query_filter[\"_id\"] = {\"$in\": representation_ids}\n\n    if representation_names is not None:\n        if not representation_names:\n            return default_output\n        query_filter[\"name\"] = {\"$in\": list(representation_names)}\n\n    if version_ids is not None:\n        version_ids = convert_ids(version_ids)\n        if not version_ids:\n            return default_output\n        query_filter[\"parent\"] = {\"$in\": version_ids}\n\n    or_queries = []\n    if names_by_version_ids is not None:\n        or_query = []\n        for version_id, names in names_by_version_ids.items():\n            if version_id and names:\n                or_query.append({\n                    \"parent\": convert_id(version_id),\n                    \"name\": {\"$in\": list(names)}\n                })\n        if not or_query:\n            return default_output\n        or_queries.append(or_query)\n\n    if context_filters is not None:\n        if not context_filters:\n            return []\n        _flatten_filters = _flatten_dict(context_filters)\n        flatten_filters = {}\n        for key, value in _flatten_filters.items():\n            if not key.startswith(\"context\"):\n                key = \"context.{}\".format(key)\n            flatten_filters[key] = value\n\n        for item in _regex_filters(flatten_filters):\n            for key, value in item.items():\n                if key != \"$or\":\n                    query_filter[key] = value\n\n                elif value:\n                    or_queries.append(value)\n\n    if len(or_queries) == 1:\n        query_filter[\"$or\"] = or_queries[0]\n    elif or_queries:\n        and_query = []\n        for or_query in or_queries:\n            if isinstance(or_query, list):\n                or_query = {\"$or\": or_query}\n            and_query.append(or_query)\n        query_filter[\"$and\"] = and_query\n\n    conn = get_project_connection(project_name)\n\n    return conn.find(query_filter, _prepare_fields(fields))\n\n\ndef get_representations(\n    project_name,\n    representation_ids=None,\n    representation_names=None,\n    version_ids=None,\n    context_filters=None,\n    names_by_version_ids=None,\n    archived=False,\n    standard=True,\n    fields=None\n):\n    \"\"\"Representation entities data from one project filtered by filters.\n\n    Filters are additive (all conditions must pass to return subset).\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        representation_ids (Iterable[Union[str, ObjectId]]): Representation ids\n            used as filter. Filter ignored if 'None' is passed.\n        representation_names (Iterable[str]): Representations names used\n            as filter. Filter ignored if 'None' is passed.\n        version_ids (Iterable[str]): Subset ids used as parent filter. Filter\n            ignored if 'None' is passed.\n        context_filters (Dict[str, List[str, PatternType]]): Filter by\n            representation context fields.\n        names_by_version_ids (dict[ObjectId, list[str]]): Complex filtering\n            using version ids and list of names under the version.\n        archived (bool): Output will also contain archived representations.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Cursor: Iterable cursor yielding all matching representations.\n    \"\"\"\n\n    return _get_representations(\n        project_name=project_name,\n        representation_ids=representation_ids,\n        representation_names=representation_names,\n        version_ids=version_ids,\n        context_filters=context_filters,\n        names_by_version_ids=names_by_version_ids,\n        standard=standard,\n        archived=archived,\n        fields=fields\n    )\n\n\ndef get_archived_representations(\n    project_name,\n    representation_ids=None,\n    representation_names=None,\n    version_ids=None,\n    context_filters=None,\n    names_by_version_ids=None,\n    fields=None\n):\n    \"\"\"Archived representation entities data from project with applied filters.\n\n    Filters are additive (all conditions must pass to return subset).\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        representation_ids (Iterable[Union[str, ObjectId]]): Representation ids\n            used as filter. Filter ignored if 'None' is passed.\n        representation_names (Iterable[str]): Representations names used\n            as filter. Filter ignored if 'None' is passed.\n        version_ids (Iterable[str]): Subset ids used as parent filter. Filter\n            ignored if 'None' is passed.\n        context_filters (Dict[str, List[str, PatternType]]): Filter by\n            representation context fields.\n        names_by_version_ids (dict[ObjectId, List[str]]): Complex filtering\n            using version ids and list of names under the version.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Cursor: Iterable cursor yielding all matching representations.\n    \"\"\"\n\n    return _get_representations(\n        project_name=project_name,\n        representation_ids=representation_ids,\n        representation_names=representation_names,\n        version_ids=version_ids,\n        context_filters=context_filters,\n        names_by_version_ids=names_by_version_ids,\n        standard=False,\n        archived=True,\n        fields=fields\n    )\n\n\ndef get_representations_parents(project_name, representations):\n    \"\"\"Prepare parents of representation entities.\n\n    Each item of returned dictionary contains version, subset, asset\n    and project in that order.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        representations (List[dict]): Representation entities with at least\n            '_id' and 'parent' keys.\n\n    Returns:\n        dict[ObjectId, tuple]: Parents by representation id.\n    \"\"\"\n\n    repre_docs_by_version_id = collections.defaultdict(list)\n    version_docs_by_version_id = {}\n    version_docs_by_subset_id = collections.defaultdict(list)\n    subset_docs_by_subset_id = {}\n    subset_docs_by_asset_id = collections.defaultdict(list)\n    output = {}\n    for repre_doc in representations:\n        repre_id = repre_doc[\"_id\"]\n        version_id = repre_doc[\"parent\"]\n        output[repre_id] = (None, None, None, None)\n        repre_docs_by_version_id[version_id].append(repre_doc)\n\n    version_docs = get_versions(\n        project_name,\n        version_ids=repre_docs_by_version_id.keys(),\n        hero=True\n    )\n    for version_doc in version_docs:\n        version_id = version_doc[\"_id\"]\n        subset_id = version_doc[\"parent\"]\n        version_docs_by_version_id[version_id] = version_doc\n        version_docs_by_subset_id[subset_id].append(version_doc)\n\n    subset_docs = get_subsets(\n        project_name, subset_ids=version_docs_by_subset_id.keys()\n    )\n    for subset_doc in subset_docs:\n        subset_id = subset_doc[\"_id\"]\n        asset_id = subset_doc[\"parent\"]\n        subset_docs_by_subset_id[subset_id] = subset_doc\n        subset_docs_by_asset_id[asset_id].append(subset_doc)\n\n    asset_docs = get_assets(\n        project_name, asset_ids=subset_docs_by_asset_id.keys()\n    )\n    asset_docs_by_id = {\n        asset_doc[\"_id\"]: asset_doc\n        for asset_doc in asset_docs\n    }\n\n    project_doc = get_project(project_name)\n\n    for version_id, repre_docs in repre_docs_by_version_id.items():\n        asset_doc = None\n        subset_doc = None\n        version_doc = version_docs_by_version_id.get(version_id)\n        if version_doc:\n            subset_id = version_doc[\"parent\"]\n            subset_doc = subset_docs_by_subset_id.get(subset_id)\n            if subset_doc:\n                asset_id = subset_doc[\"parent\"]\n                asset_doc = asset_docs_by_id.get(asset_id)\n\n        for repre_doc in repre_docs:\n            repre_id = repre_doc[\"_id\"]\n            output[repre_id] = (\n                version_doc, subset_doc, asset_doc, project_doc\n            )\n    return output\n\n\ndef get_representation_parents(project_name, representation):\n    \"\"\"Prepare parents of representation entity.\n\n    Each item of returned dictionary contains version, subset, asset\n    and project in that order.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        representation (dict): Representation entities with at least\n            '_id' and 'parent' keys.\n\n    Returns:\n        dict[ObjectId, tuple]: Parents by representation id.\n    \"\"\"\n\n    if not representation:\n        return None\n\n    repre_id = representation[\"_id\"]\n    parents_by_repre_id = get_representations_parents(\n        project_name, [representation]\n    )\n    return parents_by_repre_id[repre_id]\n\n\ndef get_thumbnail_id_from_source(project_name, src_type, src_id):\n    \"\"\"Receive thumbnail id from source entity.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        src_type (str): Type of source entity ('asset', 'version').\n        src_id (Union[str, ObjectId]): Id of source entity.\n\n    Returns:\n        Union[ObjectId, None]: Thumbnail id assigned to entity. If Source\n            entity does not have any thumbnail id assigned.\n    \"\"\"\n\n    if not src_type or not src_id:\n        return None\n\n    query_filter = {\"_id\": convert_id(src_id)}\n\n    conn = get_project_connection(project_name)\n    src_doc = conn.find_one(query_filter, {\"data.thumbnail_id\"})\n    if src_doc:\n        return src_doc.get(\"data\", {}).get(\"thumbnail_id\")\n    return None\n\n\ndef get_thumbnails(project_name, thumbnail_ids, fields=None):\n    \"\"\"Receive thumbnails entity data.\n\n    Thumbnail entity can be used to receive binary content of thumbnail based\n    on its content and ThumbnailResolvers.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        thumbnail_ids (Iterable[Union[str, ObjectId]]): Ids of thumbnail\n            entities.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        cursor: Cursor of queried documents.\n    \"\"\"\n\n    if thumbnail_ids:\n        thumbnail_ids = convert_ids(thumbnail_ids)\n\n    if not thumbnail_ids:\n        return []\n    query_filter = {\n        \"type\": \"thumbnail\",\n        \"_id\": {\"$in\": thumbnail_ids}\n    }\n    conn = get_project_connection(project_name)\n    return conn.find(query_filter, _prepare_fields(fields))\n\n\ndef get_thumbnail(\n    project_name, thumbnail_id, entity_type, entity_id, fields=None\n):\n    \"\"\"Receive thumbnail entity data.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        thumbnail_id (Union[str, ObjectId]): Id of thumbnail entity.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Thumbnail entity data which can be reduced to\n            specified 'fields'.None is returned if thumbnail with specified\n            filters was not found.\n    \"\"\"\n\n    if not thumbnail_id:\n        return None\n    query_filter = {\"type\": \"thumbnail\", \"_id\": convert_id(thumbnail_id)}\n    conn = get_project_connection(project_name)\n    return conn.find_one(query_filter, _prepare_fields(fields))\n\n\ndef get_workfile_info(\n    project_name, asset_id, task_name, filename, fields=None\n):\n    \"\"\"Document with workfile information.\n\n    Warning:\n        Query is based on filename and context which does not meant it will\n        find always right and expected result. Information have limited usage\n        and is not recommended to use it as source information about workfile.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        asset_id (Union[str, ObjectId]): Id of asset entity.\n        task_name (str): Task name on asset.\n        fields (Optional[Iterable[str]]): Fields that should be returned. All\n            fields are returned if 'None' is passed.\n\n    Returns:\n        Union[Dict, None]: Workfile entity data which can be reduced to\n            specified 'fields'.None is returned if workfile with specified\n            filters was not found.\n    \"\"\"\n\n    if not asset_id or not task_name or not filename:\n        return None\n\n    query_filter = {\n        \"type\": \"workfile\",\n        \"parent\": convert_id(asset_id),\n        \"task_name\": task_name,\n        \"filename\": filename\n    }\n    conn = get_project_connection(project_name)\n    return conn.find_one(query_filter, _prepare_fields(fields))\n\n\n\"\"\"\n## Custom data storage:\n- Settings - OP settings overrides and local settings\n- Logging - logs from Logger\n- Webpublisher - jobs\n- Ftrack - events\n- Maya - Shaders\n    - openpype/hosts/maya/api/shader_definition_editor.py\n    - openpype/hosts/maya/plugins/publish/validate_model_name.py\n\n## Global publish plugins\n- openpype/plugins/publish/extract_hierarchy_avalon.py\n    Create:\n    - asset\n    Update:\n    - asset\n\n## Lib\n- openpype/lib/avalon_context.py\n    Update:\n    - workfile data\n- openpype/lib/project_backpack.py\n    Update:\n    - project\n\"\"\"\n"
  },
  {
    "path": "openpype/client/mongo/entity_links.py",
    "content": "from .mongo import get_project_connection\nfrom .entities import (\n    get_assets,\n    get_asset_by_id,\n    get_version_by_id,\n    get_representation_by_id,\n    convert_id,\n)\n\n\ndef get_linked_asset_ids(project_name, asset_doc=None, asset_id=None):\n    \"\"\"Extract linked asset ids from asset document.\n\n    One of asset document or asset id must be passed.\n\n    Note:\n        Asset links now works only from asset to assets.\n\n    Args:\n        asset_doc (dict): Asset document from DB.\n\n    Returns:\n        List[Union[ObjectId, str]]: Asset ids of input links.\n    \"\"\"\n\n    output = []\n    if not asset_doc and not asset_id:\n        return output\n\n    if not asset_doc:\n        asset_doc = get_asset_by_id(\n            project_name, asset_id, fields=[\"data.inputLinks\"]\n        )\n\n    input_links = asset_doc[\"data\"].get(\"inputLinks\")\n    if not input_links:\n        return output\n\n    for item in input_links:\n        # Backwards compatibility for \"_id\" key which was replaced with\n        #   \"id\"\n        if \"_id\" in item:\n            link_id = item[\"_id\"]\n        else:\n            link_id = item[\"id\"]\n        output.append(link_id)\n    return output\n\n\ndef get_linked_assets(\n    project_name, asset_doc=None, asset_id=None, fields=None\n):\n    \"\"\"Return linked assets based on passed asset document.\n\n    One of asset document or asset id must be passed.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        asset_doc (Dict[str, Any]): Asset document from database.\n        asset_id (Union[ObjectId, str]): Asset id. Can be used instead of\n            asset document.\n        fields (Iterable[str]): Fields that should be returned. All fields are\n            returned if 'None' is passed.\n\n    Returns:\n        List[Dict[str, Any]]: Asset documents of input links for passed\n            asset doc.\n    \"\"\"\n\n    if not asset_doc:\n        if not asset_id:\n            return []\n        asset_doc = get_asset_by_id(\n            project_name,\n            asset_id,\n            fields=[\"data.inputLinks\"]\n        )\n        if not asset_doc:\n            return []\n\n    link_ids = get_linked_asset_ids(project_name, asset_doc=asset_doc)\n    if not link_ids:\n        return []\n\n    return list(get_assets(project_name, asset_ids=link_ids, fields=fields))\n\n\ndef get_linked_representation_id(\n    project_name, repre_doc=None, repre_id=None, link_type=None, max_depth=None\n):\n    \"\"\"Returns list of linked ids of particular type (if provided).\n\n    One of representation document or representation id must be passed.\n    Note:\n        Representation links now works only from representation through version\n            back to representations.\n\n    Args:\n        project_name (str): Name of project where look for links.\n        repre_doc (Dict[str, Any]): Representation document.\n        repre_id (Union[ObjectId, str]): Representation id.\n        link_type (str): Type of link (e.g. 'reference', ...).\n        max_depth (int): Limit recursion level. Default: 0\n\n    Returns:\n        List[ObjectId] Linked representation ids.\n    \"\"\"\n\n    if repre_doc:\n        repre_id = repre_doc[\"_id\"]\n\n    if repre_id:\n        repre_id = convert_id(repre_id)\n\n    if not repre_id and not repre_doc:\n        return []\n\n    version_id = None\n    if repre_doc:\n        version_id = repre_doc.get(\"parent\")\n\n    if not version_id:\n        repre_doc = get_representation_by_id(\n            project_name, repre_id, fields=[\"parent\"]\n        )\n        version_id = repre_doc[\"parent\"]\n\n    if not version_id:\n        return []\n\n    version_doc = get_version_by_id(\n        project_name, version_id, fields=[\"type\", \"version_id\"]\n    )\n    if version_doc[\"type\"] == \"hero_version\":\n        version_id = version_doc[\"version_id\"]\n\n    if max_depth is None:\n        max_depth = 0\n\n    match = {\n        \"_id\": version_id,\n        # Links are not stored to hero versions at this moment so filter\n        #   is limited to just versions\n        \"type\": \"version\"\n    }\n\n    graph_lookup = {\n        \"from\": project_name,\n        \"startWith\": \"$data.inputLinks.id\",\n        \"connectFromField\": \"data.inputLinks.id\",\n        \"connectToField\": \"_id\",\n        \"as\": \"outputs_recursive\",\n        \"depthField\": \"depth\"\n    }\n    if max_depth != 0:\n        # We offset by -1 since 0 basically means no recursion\n        # but the recursion only happens after the initial lookup\n        # for outputs.\n        graph_lookup[\"maxDepth\"] = max_depth - 1\n\n    query_pipeline = [\n        # Match\n        {\"$match\": match},\n        # Recursive graph lookup for inputs\n        {\"$graphLookup\": graph_lookup}\n    ]\n\n    conn = get_project_connection(project_name)\n    result = conn.aggregate(query_pipeline)\n    referenced_version_ids = _process_referenced_pipeline_result(\n        result, link_type\n    )\n    if not referenced_version_ids:\n        return []\n\n    ref_ids = conn.distinct(\n        \"_id\",\n        filter={\n            \"parent\": {\"$in\": list(referenced_version_ids)},\n            \"type\": \"representation\"\n        }\n    )\n\n    return list(ref_ids)\n\n\ndef _process_referenced_pipeline_result(result, link_type):\n    \"\"\"Filters result from pipeline for particular link_type.\n\n    Pipeline cannot use link_type directly in a query.\n\n    Returns:\n        (list)\n    \"\"\"\n\n    referenced_version_ids = set()\n    correctly_linked_ids = set()\n    for item in result:\n        input_links = item.get(\"data\", {}).get(\"inputLinks\")\n        if not input_links:\n            continue\n\n        _filter_input_links(\n            input_links,\n            link_type,\n            correctly_linked_ids\n        )\n\n        # outputs_recursive in random order, sort by depth\n        outputs_recursive = item.get(\"outputs_recursive\")\n        if not outputs_recursive:\n            continue\n\n        for output in sorted(outputs_recursive, key=lambda o: o[\"depth\"]):\n            # Leaf\n            if output[\"_id\"] not in correctly_linked_ids:\n                continue\n\n            _filter_input_links(\n                output.get(\"data\", {}).get(\"inputLinks\"),\n                link_type,\n                correctly_linked_ids\n            )\n\n            referenced_version_ids.add(output[\"_id\"])\n\n    return referenced_version_ids\n\n\ndef _filter_input_links(input_links, link_type, correctly_linked_ids):\n    if not input_links:  # to handle hero versions\n        return\n\n    for input_link in input_links:\n        if link_type and input_link[\"type\"] != link_type:\n            continue\n\n        link_id = input_link.get(\"id\") or input_link.get(\"_id\")\n        if link_id is not None:\n            correctly_linked_ids.add(link_id)\n"
  },
  {
    "path": "openpype/client/mongo/mongo.py",
    "content": "import os\nimport sys\nimport time\nimport logging\nimport pymongo\nimport certifi\n\nfrom bson.json_util import (\n    loads,\n    dumps,\n    CANONICAL_JSON_OPTIONS\n)\n\nfrom openpype import AYON_SERVER_ENABLED\nif sys.version_info[0] == 2:\n    from urlparse import urlparse, parse_qs\nelse:\n    from urllib.parse import urlparse, parse_qs\n\n\nclass MongoEnvNotSet(Exception):\n    pass\n\n\ndef documents_to_json(docs):\n    \"\"\"Convert documents to json string.\n\n    Args:\n        Union[list[dict[str, Any]], dict[str, Any]]: Document/s to convert to\n            json string.\n\n    Returns:\n        str: Json string with mongo documents.\n    \"\"\"\n\n    return dumps(docs, json_options=CANONICAL_JSON_OPTIONS)\n\n\ndef load_json_file(filepath):\n    \"\"\"Load mongo documents from a json file.\n\n    Args:\n        filepath (str): Path to a json file.\n\n    Returns:\n        Union[dict[str, Any], list[dict[str, Any]]]: Loaded content from a\n            json file.\n    \"\"\"\n\n    if not os.path.exists(filepath):\n        raise ValueError(\"Path {} was not found\".format(filepath))\n\n    with open(filepath, \"r\") as stream:\n        content = stream.read()\n    return loads(\"\".join(content))\n\n\ndef get_project_database_name():\n    \"\"\"Name of database name where projects are available.\n\n    Returns:\n        str: Name of database name where projects are.\n    \"\"\"\n\n    return os.environ.get(\"AVALON_DB\") or \"avalon\"\n\n\ndef _decompose_url(url):\n    \"\"\"Decompose mongo url to basic components.\n\n    Used for creation of MongoHandler which expect mongo url components as\n    separated kwargs. Components are at the end not used as we're setting\n    connection directly this is just a dumb components for MongoHandler\n    validation pass.\n    \"\"\"\n\n    # Use first url from passed url\n    #   - this is because it is possible to pass multiple urls for multiple\n    #       replica sets which would crash on urlparse otherwise\n    #   - please don't use comma in username of password\n    url = url.split(\",\")[0]\n    components = {\n        \"scheme\": None,\n        \"host\": None,\n        \"port\": None,\n        \"username\": None,\n        \"password\": None,\n        \"auth_db\": None\n    }\n\n    result = urlparse(url)\n    if result.scheme is None:\n        _url = \"mongodb://{}\".format(url)\n        result = urlparse(_url)\n\n    components[\"scheme\"] = result.scheme\n    components[\"host\"] = result.hostname\n    try:\n        components[\"port\"] = result.port\n    except ValueError:\n        raise RuntimeError(\"invalid port specified\")\n    components[\"username\"] = result.username\n    components[\"password\"] = result.password\n\n    try:\n        components[\"auth_db\"] = parse_qs(result.query)['authSource'][0]\n    except KeyError:\n        # no auth db provided, mongo will use the one we are connecting to\n        pass\n\n    return components\n\n\ndef get_default_components():\n    mongo_url = os.environ.get(\"OPENPYPE_MONGO\")\n    if mongo_url is None:\n        raise MongoEnvNotSet(\n            \"URL for Mongo logging connection is not set.\"\n        )\n    return _decompose_url(mongo_url)\n\n\ndef should_add_certificate_path_to_mongo_url(mongo_url):\n    \"\"\"Check if should add ca certificate to mongo url.\n\n    Since 30.9.2021 cloud mongo requires newer certificates that are not\n    available on most of workstation. This adds path to certifi certificate\n    which is valid for it. To add the certificate path url must have scheme\n    'mongodb+srv' or has 'ssl=true' or 'tls=true' in url query.\n    \"\"\"\n\n    parsed = urlparse(mongo_url)\n    query = parse_qs(parsed.query)\n    lowered_query_keys = set(key.lower() for key in query.keys())\n    add_certificate = False\n    # Check if url 'ssl' or 'tls' are set to 'true'\n    for key in (\"ssl\", \"tls\"):\n        if key in query and \"true\" in query[key]:\n            add_certificate = True\n            break\n\n    # Check if url contains 'mongodb+srv'\n    if not add_certificate and parsed.scheme == \"mongodb+srv\":\n        add_certificate = True\n\n    # Check if url does already contain certificate path\n    if add_certificate and \"tlscafile\" in lowered_query_keys:\n        add_certificate = False\n\n    return add_certificate\n\n\ndef validate_mongo_connection(mongo_uri):\n    \"\"\"Check if provided mongodb URL is valid.\n\n    Args:\n        mongo_uri (str): URL to validate.\n\n    Raises:\n        ValueError: When port in mongo uri is not valid.\n        pymongo.errors.InvalidURI: If passed mongo is invalid.\n        pymongo.errors.ServerSelectionTimeoutError: If connection timeout\n            passed so probably couldn't connect to mongo server.\n\n    \"\"\"\n\n    client = OpenPypeMongoConnection.create_connection(\n        mongo_uri, retry_attempts=1\n    )\n    client.close()\n\n\nclass OpenPypeMongoConnection:\n    \"\"\"Singleton MongoDB connection.\n\n    Keeps MongoDB connections by url.\n    \"\"\"\n\n    mongo_clients = {}\n    log = logging.getLogger(\"OpenPypeMongoConnection\")\n\n    @staticmethod\n    def get_default_mongo_url():\n        return os.environ[\"OPENPYPE_MONGO\"]\n\n    @classmethod\n    def get_mongo_client(cls, mongo_url=None):\n        if mongo_url is None:\n            mongo_url = cls.get_default_mongo_url()\n\n        connection = cls.mongo_clients.get(mongo_url)\n        if connection:\n            # Naive validation of existing connection\n            try:\n                connection.server_info()\n                with connection.start_session():\n                    pass\n            except Exception:\n                connection = None\n\n        if not connection:\n            cls.log.debug(\"Creating mongo connection to {}\".format(mongo_url))\n            connection = cls.create_connection(mongo_url)\n            cls.mongo_clients[mongo_url] = connection\n\n        return connection\n\n    @classmethod\n    def create_connection(cls, mongo_url, timeout=None, retry_attempts=None):\n        if AYON_SERVER_ENABLED:\n            raise RuntimeError(\"Created mongo connection  in AYON mode\")\n        parsed = urlparse(mongo_url)\n        # Force validation of scheme\n        if parsed.scheme not in [\"mongodb\", \"mongodb+srv\"]:\n            raise pymongo.errors.InvalidURI((\n                \"Invalid URI scheme:\"\n                \" URI must begin with 'mongodb://' or 'mongodb+srv://'\"\n            ))\n\n        if timeout is None:\n            timeout = int(os.environ.get(\"AVALON_TIMEOUT\") or 1000)\n\n        kwargs = {\n            \"serverSelectionTimeoutMS\": timeout\n        }\n        if should_add_certificate_path_to_mongo_url(mongo_url):\n            kwargs[\"tlsCAFile\"] = certifi.where()\n\n        mongo_client = pymongo.MongoClient(mongo_url, **kwargs)\n\n        if retry_attempts is None:\n            retry_attempts = 3\n\n        elif not retry_attempts:\n            retry_attempts = 1\n\n        last_exc = None\n        valid = False\n        t1 = time.time()\n        for attempt in range(1, retry_attempts + 1):\n            try:\n                mongo_client.server_info()\n                with mongo_client.start_session():\n                    pass\n                valid = True\n                break\n\n            except Exception as exc:\n                last_exc = exc\n                if attempt < retry_attempts:\n                    cls.log.warning(\n                        \"Attempt {} failed. Retrying... \".format(attempt)\n                    )\n                    time.sleep(1)\n\n        if not valid:\n            raise last_exc\n\n        cls.log.info(\"Connected to {}, delay {:.3f}s\".format(\n            mongo_url, time.time() - t1\n        ))\n        return mongo_client\n\n\n# ------ Helper Mongo functions ------\n# Functions can be helpful with custom tools to backup/restore mongo state.\n# Not meant as API functionality that should be used in production codebase!\ndef get_collection_documents(database_name, collection_name, as_json=False):\n    \"\"\"Query all documents from a collection.\n\n    Args:\n        database_name (str): Name of database where to look for collection.\n        collection_name (str): Name of collection where to look for collection.\n        as_json (Optional[bool]): Output should be a json string.\n            Default: 'False'\n\n    Returns:\n        Union[list[dict[str, Any]], str]: Queried documents.\n    \"\"\"\n\n    client = OpenPypeMongoConnection.get_mongo_client()\n    output = list(client[database_name][collection_name].find({}))\n    if as_json:\n        output = documents_to_json(output)\n    return output\n\n\ndef store_collection(filepath, database_name, collection_name):\n    \"\"\"Store collection documents to a json file.\n\n    Args:\n        filepath (str): Path to a json file where documents will be stored.\n        database_name (str): Name of database where to look for collection.\n        collection_name (str): Name of collection to store.\n    \"\"\"\n\n    # Make sure directory for output file exists\n    dirpath = os.path.dirname(filepath)\n    if not os.path.isdir(dirpath):\n        os.makedirs(dirpath)\n\n    content = get_collection_documents(database_name, collection_name, True)\n    with open(filepath, \"w\") as stream:\n        stream.write(content)\n\n\ndef replace_collection_documents(docs, database_name, collection_name):\n    \"\"\"Replace all documents in a collection with passed documents.\n\n    Warnings:\n        All existing documents in collection will be removed if there are any.\n\n    Args:\n        docs (list[dict[str, Any]]): New documents.\n        database_name (str): Name of database where to look for collection.\n        collection_name (str): Name of collection where new documents are\n            uploaded.\n    \"\"\"\n\n    client = OpenPypeMongoConnection.get_mongo_client()\n    database = client[database_name]\n    if collection_name in database.list_collection_names():\n        database.drop_collection(collection_name)\n    col = database[collection_name]\n    col.insert_many(docs)\n\n\ndef restore_collection(filepath, database_name, collection_name):\n    \"\"\"Restore/replace collection from a json filepath.\n\n    Warnings:\n        All existing documents in collection will be removed if there are any.\n\n    Args:\n        filepath (str): Path to a json with documents.\n        database_name (str): Name of database where to look for collection.\n        collection_name (str): Name of collection where new documents are\n            uploaded.\n    \"\"\"\n\n    docs = load_json_file(filepath)\n    replace_collection_documents(docs, database_name, collection_name)\n\n\ndef get_project_database(database_name=None):\n    \"\"\"Database object where project collections are.\n\n    Args:\n        database_name (Optional[str]): Custom name of database.\n\n    Returns:\n        pymongo.database.Database: Collection related to passed project.\n    \"\"\"\n\n    if not database_name:\n        database_name = get_project_database_name()\n    return OpenPypeMongoConnection.get_mongo_client()[database_name]\n\n\ndef get_project_connection(project_name, database_name=None):\n    \"\"\"Direct access to mongo collection.\n\n    We're trying to avoid using direct access to mongo. This should be used\n    only for Create, Update and Remove operations until there are implemented\n    api calls for that.\n\n    Args:\n        project_name (str): Project name for which collection should be\n            returned.\n        database_name (Optional[str]): Custom name of database.\n\n    Returns:\n        pymongo.collection.Collection: Collection related to passed project.\n    \"\"\"\n\n    if not project_name:\n        raise ValueError(\"Invalid project name {}\".format(str(project_name)))\n    return get_project_database(database_name)[project_name]\n\n\ndef get_project_documents(project_name, database_name=None):\n    \"\"\"Query all documents from project collection.\n\n    Args:\n        project_name (str): Name of project.\n        database_name (Optional[str]): Name of mongo database where to look for\n            project.\n\n    Returns:\n        list[dict[str, Any]]: Documents in project collection.\n    \"\"\"\n\n    if not database_name:\n        database_name = get_project_database_name()\n    return get_collection_documents(database_name, project_name)\n\n\ndef store_project_documents(project_name, filepath, database_name=None):\n    \"\"\"Store project documents to a file as json string.\n\n    Args:\n        project_name (str): Name of project to store.\n        filepath (str): Path to a json file where output will be stored.\n        database_name (Optional[str]): Name of mongo database where to look for\n            project.\n    \"\"\"\n\n    if not database_name:\n        database_name = get_project_database_name()\n\n    store_collection(filepath, database_name, project_name)\n\n\ndef replace_project_documents(project_name, docs, database_name=None):\n    \"\"\"Replace documents in mongo with passed documents.\n\n    Warnings:\n        Existing project collection is removed if exists in mongo.\n\n    Args:\n        project_name (str): Name of project.\n        docs (list[dict[str, Any]]): Documents to restore.\n        database_name (Optional[str]): Name of mongo database where project\n            collection will be created.\n    \"\"\"\n\n    if not database_name:\n        database_name = get_project_database_name()\n    replace_collection_documents(docs, database_name, project_name)\n\n\ndef restore_project_documents(project_name, filepath, database_name=None):\n    \"\"\"Replace documents in mongo with passed documents.\n\n    Warnings:\n        Existing project collection is removed if exists in mongo.\n\n    Args:\n        project_name (str): Name of project.\n        filepath (str): File to json file with project documents.\n        database_name (Optional[str]): Name of mongo database where project\n            collection will be created.\n    \"\"\"\n\n    if not database_name:\n        database_name = get_project_database_name()\n    restore_collection(filepath, database_name, project_name)\n"
  },
  {
    "path": "openpype/client/mongo/operations.py",
    "content": "import re\nimport copy\nimport collections\n\nfrom bson.objectid import ObjectId\nfrom pymongo import DeleteOne, InsertOne, UpdateOne\n\nfrom openpype.client.operations_base import (\n    REMOVED_VALUE,\n    CreateOperation,\n    UpdateOperation,\n    DeleteOperation,\n    BaseOperationsSession\n)\nfrom .mongo import get_project_connection\nfrom .entities import get_project\n\n\nPROJECT_NAME_ALLOWED_SYMBOLS = \"a-zA-Z0-9_\"\nPROJECT_NAME_REGEX = re.compile(\n    \"^[{}]+$\".format(PROJECT_NAME_ALLOWED_SYMBOLS)\n)\n\nCURRENT_PROJECT_SCHEMA = \"openpype:project-3.0\"\nCURRENT_PROJECT_CONFIG_SCHEMA = \"openpype:config-2.0\"\nCURRENT_ASSET_DOC_SCHEMA = \"openpype:asset-3.0\"\nCURRENT_SUBSET_SCHEMA = \"openpype:subset-3.0\"\nCURRENT_VERSION_SCHEMA = \"openpype:version-3.0\"\nCURRENT_HERO_VERSION_SCHEMA = \"openpype:hero_version-1.0\"\nCURRENT_REPRESENTATION_SCHEMA = \"openpype:representation-2.0\"\nCURRENT_WORKFILE_INFO_SCHEMA = \"openpype:workfile-1.0\"\nCURRENT_THUMBNAIL_SCHEMA = \"openpype:thumbnail-1.0\"\n\n\ndef _create_or_convert_to_mongo_id(mongo_id):\n    if mongo_id is None:\n        return ObjectId()\n    return ObjectId(mongo_id)\n\n\ndef new_project_document(\n    project_name, project_code, config, data=None, entity_id=None\n):\n    \"\"\"Create skeleton data of project document.\n\n    Args:\n        project_name (str): Name of project. Used as identifier of a project.\n        project_code (str): Shorter version of projet without spaces and\n            special characters (in most of cases). Should be also considered\n            as unique name across projects.\n        config (Dic[str, Any]): Project config consist of roots, templates,\n            applications and other project Anatomy related data.\n        data (Dict[str, Any]): Project data with information about it's\n            attributes (e.g. 'fps' etc.) or integration specific keys.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of project document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n\n    data[\"code\"] = project_code\n\n    return {\n        \"_id\": _create_or_convert_to_mongo_id(entity_id),\n        \"name\": project_name,\n        \"type\": CURRENT_PROJECT_SCHEMA,\n        \"entity_data\": data,\n        \"config\": config\n    }\n\n\ndef new_asset_document(\n    name, project_id, parent_id, parents, data=None, entity_id=None\n):\n    \"\"\"Create skeleton data of asset document.\n\n    Args:\n        name (str): Is considered as unique identifier of asset in project.\n        project_id (Union[str, ObjectId]): Id of project doument.\n        parent_id (Union[str, ObjectId]): Id of parent asset.\n        parents (List[str]): List of parent assets names.\n        data (Dict[str, Any]): Asset document data. Empty dictionary is used\n            if not passed. Value of 'parent_id' is used to fill 'visualParent'.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of asset document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n    if parent_id is not None:\n        parent_id = ObjectId(parent_id)\n    data[\"visualParent\"] = parent_id\n    data[\"parents\"] = parents\n\n    return {\n        \"_id\": _create_or_convert_to_mongo_id(entity_id),\n        \"type\": \"asset\",\n        \"name\": name,\n        \"parent\": ObjectId(project_id),\n        \"data\": data,\n        \"schema\": CURRENT_ASSET_DOC_SCHEMA\n    }\n\n\ndef new_subset_document(name, family, asset_id, data=None, entity_id=None):\n    \"\"\"Create skeleton data of subset document.\n\n    Args:\n        name (str): Is considered as unique identifier of subset under asset.\n        family (str): Subset's family.\n        asset_id (Union[str, ObjectId]): Id of parent asset.\n        data (Dict[str, Any]): Subset document data. Empty dictionary is used\n            if not passed. Value of 'family' is used to fill 'family'.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of subset document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n    data[\"family\"] = family\n    return {\n        \"_id\": _create_or_convert_to_mongo_id(entity_id),\n        \"schema\": CURRENT_SUBSET_SCHEMA,\n        \"type\": \"subset\",\n        \"name\": name,\n        \"data\": data,\n        \"parent\": asset_id\n    }\n\n\ndef new_version_doc(version, subset_id, data=None, entity_id=None):\n    \"\"\"Create skeleton data of version document.\n\n    Args:\n        version (int): Is considered as unique identifier of version\n            under subset.\n        subset_id (Union[str, ObjectId]): Id of parent subset.\n        data (Dict[str, Any]): Version document data.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of version document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n\n    return {\n        \"_id\": _create_or_convert_to_mongo_id(entity_id),\n        \"schema\": CURRENT_VERSION_SCHEMA,\n        \"type\": \"version\",\n        \"name\": int(version),\n        \"parent\": subset_id,\n        \"data\": data\n    }\n\n\ndef new_hero_version_doc(version_id, subset_id, data=None, entity_id=None):\n    \"\"\"Create skeleton data of hero version document.\n\n    Args:\n        version_id (ObjectId): Is considered as unique identifier of version\n            under subset.\n        subset_id (Union[str, ObjectId]): Id of parent subset.\n        data (Dict[str, Any]): Version document data.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of version document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n\n    return {\n        \"_id\": _create_or_convert_to_mongo_id(entity_id),\n        \"schema\": CURRENT_HERO_VERSION_SCHEMA,\n        \"type\": \"hero_version\",\n        \"version_id\": version_id,\n        \"parent\": subset_id,\n        \"data\": data\n    }\n\n\ndef new_representation_doc(\n    name, version_id, context, data=None, entity_id=None\n):\n    \"\"\"Create skeleton data of asset document.\n\n    Args:\n        version (int): Is considered as unique identifier of version\n            under subset.\n        version_id (Union[str, ObjectId]): Id of parent version.\n        context (Dict[str, Any]): Representation context used for fill template\n            of to query.\n        data (Dict[str, Any]): Representation document data.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of version document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n\n    return {\n        \"_id\": _create_or_convert_to_mongo_id(entity_id),\n        \"schema\": CURRENT_REPRESENTATION_SCHEMA,\n        \"type\": \"representation\",\n        \"parent\": version_id,\n        \"name\": name,\n        \"data\": data,\n\n        # Imprint shortcut to context for performance reasons.\n        \"context\": context\n    }\n\n\ndef new_thumbnail_doc(data=None, entity_id=None):\n    \"\"\"Create skeleton data of thumbnail document.\n\n    Args:\n        data (Dict[str, Any]): Thumbnail document data.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of thumbnail document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n\n    return {\n        \"_id\": _create_or_convert_to_mongo_id(entity_id),\n        \"type\": \"thumbnail\",\n        \"schema\": CURRENT_THUMBNAIL_SCHEMA,\n        \"data\": data\n    }\n\n\ndef new_workfile_info_doc(\n    filename, asset_id, task_name, files, data=None, entity_id=None\n):\n    \"\"\"Create skeleton data of workfile info document.\n\n    Workfile document is at this moment used primarily for artist notes.\n\n    Args:\n        filename (str): Filename of workfile.\n        asset_id (Union[str, ObjectId]): Id of asset under which workfile live.\n        task_name (str): Task under which was workfile created.\n        files (List[str]): List of rootless filepaths related to workfile.\n        data (Dict[str, Any]): Additional metadata.\n\n    Returns:\n        Dict[str, Any]: Skeleton of workfile info document.\n    \"\"\"\n\n    if not data:\n        data = {}\n\n    return {\n        \"_id\": _create_or_convert_to_mongo_id(entity_id),\n        \"type\": \"workfile\",\n        \"parent\": ObjectId(asset_id),\n        \"task_name\": task_name,\n        \"filename\": filename,\n        \"data\": data,\n        \"files\": files\n    }\n\n\ndef _prepare_update_data(old_doc, new_doc, replace):\n    changes = {}\n    for key, value in new_doc.items():\n        if key not in old_doc or value != old_doc[key]:\n            changes[key] = value\n\n    if replace:\n        for key in old_doc.keys():\n            if key not in new_doc:\n                changes[key] = REMOVED_VALUE\n    return changes\n\n\ndef prepare_subset_update_data(old_doc, new_doc, replace=True):\n    \"\"\"Compare two subset documents and prepare update data.\n\n    Based on compared values will create update data for\n    'MongoUpdateOperation'.\n\n    Empty output means that documents are identical.\n\n    Returns:\n        Dict[str, Any]: Changes between old and new document.\n    \"\"\"\n\n    return _prepare_update_data(old_doc, new_doc, replace)\n\n\ndef prepare_version_update_data(old_doc, new_doc, replace=True):\n    \"\"\"Compare two version documents and prepare update data.\n\n    Based on compared values will create update data for\n    'MongoUpdateOperation'.\n\n    Empty output means that documents are identical.\n\n    Returns:\n        Dict[str, Any]: Changes between old and new document.\n    \"\"\"\n\n    return _prepare_update_data(old_doc, new_doc, replace)\n\n\ndef prepare_hero_version_update_data(old_doc, new_doc, replace=True):\n    \"\"\"Compare two hero version documents and prepare update data.\n\n    Based on compared values will create update data for 'UpdateOperation'.\n\n    Empty output means that documents are identical.\n\n    Returns:\n        Dict[str, Any]: Changes between old and new document.\n    \"\"\"\n\n    return _prepare_update_data(old_doc, new_doc, replace)\n\n\ndef prepare_representation_update_data(old_doc, new_doc, replace=True):\n    \"\"\"Compare two representation documents and prepare update data.\n\n    Based on compared values will create update data for\n    'MongoUpdateOperation'.\n\n    Empty output means that documents are identical.\n\n    Returns:\n        Dict[str, Any]: Changes between old and new document.\n    \"\"\"\n\n    return _prepare_update_data(old_doc, new_doc, replace)\n\n\ndef prepare_workfile_info_update_data(old_doc, new_doc, replace=True):\n    \"\"\"Compare two workfile info documents and prepare update data.\n\n    Based on compared values will create update data for\n    'MongoUpdateOperation'.\n\n    Empty output means that documents are identical.\n\n    Returns:\n        Dict[str, Any]: Changes between old and new document.\n    \"\"\"\n\n    return _prepare_update_data(old_doc, new_doc, replace)\n\n\nclass MongoCreateOperation(CreateOperation):\n    \"\"\"Operation to create an entity.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'asset', 'representation' etc.\n        data (Dict[str, Any]): Data of entity that will be created.\n    \"\"\"\n\n    operation_name = \"create\"\n\n    def __init__(self, project_name, entity_type, data):\n        super(MongoCreateOperation, self).__init__(\n            project_name, entity_type, data\n        )\n\n        if \"_id\" not in self._data:\n            self._data[\"_id\"] = ObjectId()\n        else:\n            self._data[\"_id\"] = ObjectId(self._data[\"_id\"])\n\n    @property\n    def entity_id(self):\n        return self._data[\"_id\"]\n\n    def to_mongo_operation(self):\n        return InsertOne(copy.deepcopy(self._data))\n\n\nclass MongoUpdateOperation(UpdateOperation):\n    \"\"\"Operation to update an entity.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'asset', 'representation' etc.\n        entity_id (Union[str, ObjectId]): Identifier of an entity.\n        update_data (Dict[str, Any]): Key -> value changes that will be set in\n            database. If value is set to 'REMOVED_VALUE' the key will be\n            removed. Only first level of dictionary is checked (on purpose).\n    \"\"\"\n\n    operation_name = \"update\"\n\n    def __init__(self, project_name, entity_type, entity_id, update_data):\n        super(MongoUpdateOperation, self).__init__(\n            project_name, entity_type, entity_id, update_data\n        )\n\n        self._entity_id = ObjectId(self._entity_id)\n\n    def to_mongo_operation(self):\n        unset_data = {}\n        set_data = {}\n        for key, value in self._update_data.items():\n            if value is REMOVED_VALUE:\n                unset_data[key] = None\n            else:\n                set_data[key] = value\n\n        op_data = {}\n        if unset_data:\n            op_data[\"$unset\"] = unset_data\n        if set_data:\n            op_data[\"$set\"] = set_data\n\n        if not op_data:\n            return None\n\n        return UpdateOne(\n            {\"_id\": self.entity_id},\n            op_data\n        )\n\n\nclass MongoDeleteOperation(DeleteOperation):\n    \"\"\"Operation to delete an entity.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'asset', 'representation' etc.\n        entity_id (Union[str, ObjectId]): Entity id that will be removed.\n    \"\"\"\n\n    operation_name = \"delete\"\n\n    def __init__(self, project_name, entity_type, entity_id):\n        super(MongoDeleteOperation, self).__init__(\n            project_name, entity_type, entity_id\n        )\n\n        self._entity_id = ObjectId(self._entity_id)\n\n    def to_mongo_operation(self):\n        return DeleteOne({\"_id\": self.entity_id})\n\n\nclass MongoOperationsSession(BaseOperationsSession):\n    \"\"\"Session storing operations that should happen in an order.\n\n    At this moment does not handle anything special can be sonsidered as\n    stupid list of operations that will happen after each other. If creation\n    of same entity is there multiple times it's handled in any way and document\n    values are not validated.\n\n    All operations must be related to single project.\n\n    Args:\n        project_name (str): Project name to which are operations related.\n    \"\"\"\n\n    def commit(self):\n        \"\"\"Commit session operations.\"\"\"\n\n        operations, self._operations = self._operations, []\n        if not operations:\n            return\n\n        operations_by_project = collections.defaultdict(list)\n        for operation in operations:\n            operations_by_project[operation.project_name].append(operation)\n\n        for project_name, operations in operations_by_project.items():\n            bulk_writes = []\n            for operation in operations:\n                mongo_op = operation.to_mongo_operation()\n                if mongo_op is not None:\n                    bulk_writes.append(mongo_op)\n\n            if bulk_writes:\n                collection = get_project_connection(project_name)\n                collection.bulk_write(bulk_writes)\n\n    def create_entity(self, project_name, entity_type, data):\n        \"\"\"Fast access to 'MongoCreateOperation'.\n\n        Returns:\n            MongoCreateOperation: Object of update operation.\n        \"\"\"\n\n        operation = MongoCreateOperation(project_name, entity_type, data)\n        self.add(operation)\n        return operation\n\n    def update_entity(self, project_name, entity_type, entity_id, update_data):\n        \"\"\"Fast access to 'MongoUpdateOperation'.\n\n        Returns:\n            MongoUpdateOperation: Object of update operation.\n        \"\"\"\n\n        operation = MongoUpdateOperation(\n            project_name, entity_type, entity_id, update_data\n        )\n        self.add(operation)\n        return operation\n\n    def delete_entity(self, project_name, entity_type, entity_id):\n        \"\"\"Fast access to 'MongoDeleteOperation'.\n\n        Returns:\n            MongoDeleteOperation: Object of delete operation.\n        \"\"\"\n\n        operation = MongoDeleteOperation(project_name, entity_type, entity_id)\n        self.add(operation)\n        return operation\n\n\ndef create_project(\n    project_name,\n    project_code,\n    library_project=False,\n):\n    \"\"\"Create project using OpenPype settings.\n\n    This project creation function is not validating project document on\n    creation. It is because project document is created blindly with only\n    minimum required information about project which is it's name, code, type\n    and schema.\n\n    Entered project name must be unique and project must not exist yet.\n\n    Note:\n        This function is here to be OP v4 ready but in v3 has more logic\n            to do. That's why inner imports are in the body.\n\n    Args:\n        project_name(str): New project name. Should be unique.\n        project_code(str): Project's code should be unique too.\n        library_project(bool): Project is library project.\n\n    Raises:\n        ValueError: When project name already exists in MongoDB.\n\n    Returns:\n        dict: Created project document.\n    \"\"\"\n\n    from openpype.settings import ProjectSettings, SaveWarningExc\n    from openpype.pipeline.schema import validate\n\n    if get_project(project_name, fields=[\"name\"]):\n        raise ValueError(\"Project with name \\\"{}\\\" already exists\".format(\n            project_name\n        ))\n\n    if not PROJECT_NAME_REGEX.match(project_name):\n        raise ValueError((\n            \"Project name \\\"{}\\\" contain invalid characters\"\n        ).format(project_name))\n\n    project_doc = {\n        \"type\": \"project\",\n        \"name\": project_name,\n        \"data\": {\n            \"code\": project_code,\n            \"library_project\": library_project\n        },\n        \"schema\": CURRENT_PROJECT_SCHEMA\n    }\n\n    op_session = MongoOperationsSession()\n    # Insert document with basic data\n    create_op = op_session.create_entity(\n        project_name, project_doc[\"type\"], project_doc\n    )\n    op_session.commit()\n\n    # Load ProjectSettings for the project and save it to store all attributes\n    #   and Anatomy\n    try:\n        project_settings_entity = ProjectSettings(project_name)\n        project_settings_entity.save()\n    except SaveWarningExc as exc:\n        print(str(exc))\n    except Exception:\n        op_session.delete_entity(\n            project_name, project_doc[\"type\"], create_op.entity_id\n        )\n        op_session.commit()\n        raise\n\n    project_doc = get_project(project_name)\n\n    try:\n        # Validate created project document\n        validate(project_doc)\n    except Exception:\n        # Remove project if is not valid\n        op_session.delete_entity(\n            project_name, project_doc[\"type\"], create_op.entity_id\n        )\n        op_session.commit()\n        raise\n\n    return project_doc\n"
  },
  {
    "path": "openpype/client/notes.md",
    "content": "# Client functionality\n## Reason\nPreparation for OpenPype v4 server. Goal is to remove direct mongo calls in code to prepare a little bit for different source of data for code before. To start think about database calls less as mongo calls but more universally. To do so was implemented simple wrapper around database calls to not use pymongo specific code.\n\nCurrent goal is not to make universal database model which can be easily replaced with any different source of data but to make it close as possible. Current implementation of OpenPype is too tightly connected to pymongo and it's abilities so we're trying to get closer with long term changes that can be used even in current state.\n\n## Queries\nQuery functions don't use full potential of mongo queries like very specific queries based on subdictionaries or unknown structures. We try to avoid these calls as much as possible because they'll probably won't be available in future. If it's really necessary a new function can be added but only if it's reasonable for overall logic. All query functions were moved to `~/client/entities.py`. Each function has arguments with available filters and possible reduce of returned keys for each entity.\n\n## Changes\nChanges are a little bit complicated. Mongo has many options how update can happen which had to be reduced also it would be at this stage complicated to validate values which are created or updated thus automation is at this point almost none. Changes can be made using operations available in `~/client/operations.py`. Each operation require project name and entity type, but may require operation specific data.\n\n### Create\nCreate operations expect already prepared document data, for that are prepared functions creating skeletal structures of documents (do not fill all required data), except `_id` all data should be right. Existence of entity is not validated so if the same creation operation is send n times it will create the entity n times which can cause issues.\n\n### Update\nUpdate operation require entity id and keys that should be changed, update dictionary must have {\"key\": value}. If value should be set in nested dictionary the key must have also all subkeys joined with dot `.` (e.g. `{\"data\": {\"fps\": 25}}` -> `{\"data.fps\": 25}`). To simplify update dictionaries were prepared functions which does that for you, their name has template `prepare_<entity type>_update_data` - they work on comparison of previous document and new document. If there is missing function for requested entity type it is because we didn't need it yet and require implementation.\n\n### Delete\nDelete operation need entity id. Entity will be deleted from mongo.\n\n\n## What (probably) won't be replaced\nSome parts of code are still using direct mongo calls. In most of cases it is for very specific calls that are module specific or their usage will completely change in future.\n- Mongo calls that are not project specific (out of `avalon` collection) will be removed or will have to use different mechanism how the data are stored. At this moment it is related to OpenPype settings and logs, ftrack server events, some other data.\n- Sync server queries. They're complex and very specific for sync server module. Their replacement will require specific calls to OpenPype server in v4 thus their abstraction with wrapper is irrelevant and would complicate production in v3.\n- Project managers (ftrack, kitsu, shotgrid, embedded Project Manager, etc.). Project managers are creating, updating or removing assets in v3, but in v4 will create folders with different structure. Wrapping creation of assets would not help to prepare for v4 because of new data structures. The same can be said about editorial Extract Hierarchy Avalon plugin which create project structure.\n- Code parts that is marked as deprecated in v3 or will be deprecated in v4.\n    - integrate asset legacy publish plugin - already is legacy kept for safety\n    - integrate thumbnail - thumbnails will be stored in different way in v4\n    - input links - link will be stored in different way and will have different mechanism of linking. In v3 are links limited to same entity type \"asset <-> asset\" or \"representation <-> representation\".\n\n## Known missing replacements\n- change subset group in loader tool\n- integrate subset group\n- query input links in openpype lib\n- create project in openpype lib\n- save/create workfile doc in openpype lib\n- integrate hero version\n"
  },
  {
    "path": "openpype/client/operations.py",
    "content": "from openpype import AYON_SERVER_ENABLED\n\nfrom .operations_base import REMOVED_VALUE\nif not AYON_SERVER_ENABLED:\n    from .mongo.operations import *\n    OperationsSession = MongoOperationsSession\n\nelse:\n    from ayon_api.server_api import (\n        PROJECT_NAME_ALLOWED_SYMBOLS,\n        PROJECT_NAME_REGEX,\n    )\n    from .server.operations import *\n    from .mongo.operations import (\n        CURRENT_PROJECT_SCHEMA,\n        CURRENT_PROJECT_CONFIG_SCHEMA,\n        CURRENT_ASSET_DOC_SCHEMA,\n        CURRENT_SUBSET_SCHEMA,\n        CURRENT_VERSION_SCHEMA,\n        CURRENT_HERO_VERSION_SCHEMA,\n        CURRENT_REPRESENTATION_SCHEMA,\n        CURRENT_WORKFILE_INFO_SCHEMA,\n        CURRENT_THUMBNAIL_SCHEMA\n    )\n"
  },
  {
    "path": "openpype/client/operations_base.py",
    "content": "import uuid\nimport copy\nfrom abc import ABCMeta, abstractmethod, abstractproperty\nimport six\n\nREMOVED_VALUE = object()\n\n\n@six.add_metaclass(ABCMeta)\nclass AbstractOperation(object):\n    \"\"\"Base operation class.\n\n    Operation represent a call into database. The call can create, change or\n    remove data.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'asset', 'representation' etc.\n    \"\"\"\n\n    def __init__(self, project_name, entity_type):\n        self._project_name = project_name\n        self._entity_type = entity_type\n        self._id = str(uuid.uuid4())\n\n    @property\n    def project_name(self):\n        return self._project_name\n\n    @property\n    def id(self):\n        \"\"\"Identifier of operation.\"\"\"\n\n        return self._id\n\n    @property\n    def entity_type(self):\n        return self._entity_type\n\n    @abstractproperty\n    def operation_name(self):\n        \"\"\"Stringified type of operation.\"\"\"\n\n        pass\n\n    def to_data(self):\n        \"\"\"Convert operation to data that can be converted to json or others.\n\n        Warning:\n            Current state returns ObjectId objects which cannot be parsed by\n                json.\n\n        Returns:\n            Dict[str, Any]: Description of operation.\n        \"\"\"\n\n        return {\n            \"id\": self._id,\n            \"entity_type\": self.entity_type,\n            \"project_name\": self.project_name,\n            \"operation\": self.operation_name\n        }\n\n\nclass CreateOperation(AbstractOperation):\n    \"\"\"Operation to create an entity.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'asset', 'representation' etc.\n        data (Dict[str, Any]): Data of entity that will be created.\n    \"\"\"\n\n    operation_name = \"create\"\n\n    def __init__(self, project_name, entity_type, data):\n        super(CreateOperation, self).__init__(project_name, entity_type)\n\n        if not data:\n            data = {}\n        else:\n            data = copy.deepcopy(dict(data))\n        self._data = data\n\n    def __setitem__(self, key, value):\n        self.set_value(key, value)\n\n    def __getitem__(self, key):\n        return self.data[key]\n\n    def set_value(self, key, value):\n        self.data[key] = value\n\n    def get(self, key, *args, **kwargs):\n        return self.data.get(key, *args, **kwargs)\n\n    @abstractproperty\n    def entity_id(self):\n        pass\n\n    @property\n    def data(self):\n        return self._data\n\n    def to_data(self):\n        output = super(CreateOperation, self).to_data()\n        output[\"data\"] = copy.deepcopy(self.data)\n        return output\n\n\nclass UpdateOperation(AbstractOperation):\n    \"\"\"Operation to update an entity.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'asset', 'representation' etc.\n        entity_id (Union[str, ObjectId]): Identifier of an entity.\n        update_data (Dict[str, Any]): Key -> value changes that will be set in\n            database. If value is set to 'REMOVED_VALUE' the key will be\n            removed. Only first level of dictionary is checked (on purpose).\n    \"\"\"\n\n    operation_name = \"update\"\n\n    def __init__(self, project_name, entity_type, entity_id, update_data):\n        super(UpdateOperation, self).__init__(project_name, entity_type)\n\n        self._entity_id = entity_id\n        self._update_data = update_data\n\n    @property\n    def entity_id(self):\n        return self._entity_id\n\n    @property\n    def update_data(self):\n        return self._update_data\n\n    def to_data(self):\n        changes = {}\n        for key, value in self._update_data.items():\n            if value is REMOVED_VALUE:\n                value = None\n            changes[key] = value\n\n        output = super(UpdateOperation, self).to_data()\n        output.update({\n            \"entity_id\": self.entity_id,\n            \"changes\": changes\n        })\n        return output\n\n\nclass DeleteOperation(AbstractOperation):\n    \"\"\"Operation to delete an entity.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'asset', 'representation' etc.\n        entity_id (Union[str, ObjectId]): Entity id that will be removed.\n    \"\"\"\n\n    operation_name = \"delete\"\n\n    def __init__(self, project_name, entity_type, entity_id):\n        super(DeleteOperation, self).__init__(project_name, entity_type)\n\n        self._entity_id = entity_id\n\n    @property\n    def entity_id(self):\n        return self._entity_id\n\n    def to_data(self):\n        output = super(DeleteOperation, self).to_data()\n        output[\"entity_id\"] = self.entity_id\n        return output\n\n\nclass BaseOperationsSession(object):\n    \"\"\"Session storing operations that should happen in an order.\n\n    At this moment does not handle anything special can be considered as\n    stupid list of operations that will happen after each other. If creation\n    of same entity is there multiple times it's handled in any way and document\n    values are not validated.\n    \"\"\"\n\n    def __init__(self):\n        self._operations = []\n\n    def __len__(self):\n        return len(self._operations)\n\n    def add(self, operation):\n        \"\"\"Add operation to be processed.\n\n        Args:\n            operation (BaseOperation): Operation that should be processed.\n        \"\"\"\n        if not isinstance(\n            operation,\n            (CreateOperation, UpdateOperation, DeleteOperation)\n        ):\n            raise TypeError(\"Expected Operation object got {}\".format(\n                str(type(operation))\n            ))\n\n        self._operations.append(operation)\n\n    def append(self, operation):\n        \"\"\"Add operation to be processed.\n\n        Args:\n            operation (BaseOperation): Operation that should be processed.\n        \"\"\"\n\n        self.add(operation)\n\n    def extend(self, operations):\n        \"\"\"Add operations to be processed.\n\n        Args:\n            operations (List[BaseOperation]): Operations that should be\n                processed.\n        \"\"\"\n\n        for operation in operations:\n            self.add(operation)\n\n    def remove(self, operation):\n        \"\"\"Remove operation.\"\"\"\n\n        self._operations.remove(operation)\n\n    def clear(self):\n        \"\"\"Clear all registered operations.\"\"\"\n\n        self._operations = []\n\n    def to_data(self):\n        return [\n            operation.to_data()\n            for operation in self._operations\n        ]\n\n    @abstractmethod\n    def commit(self):\n        \"\"\"Commit session operations.\"\"\"\n        pass\n\n    def create_entity(self, project_name, entity_type, data):\n        \"\"\"Fast access to 'CreateOperation'.\n\n        Returns:\n            CreateOperation: Object of update operation.\n        \"\"\"\n\n        operation = CreateOperation(project_name, entity_type, data)\n        self.add(operation)\n        return operation\n\n    def update_entity(self, project_name, entity_type, entity_id, update_data):\n        \"\"\"Fast access to 'UpdateOperation'.\n\n        Returns:\n            UpdateOperation: Object of update operation.\n        \"\"\"\n\n        operation = UpdateOperation(\n            project_name, entity_type, entity_id, update_data\n        )\n        self.add(operation)\n        return operation\n\n    def delete_entity(self, project_name, entity_type, entity_id):\n        \"\"\"Fast access to 'DeleteOperation'.\n\n        Returns:\n            DeleteOperation: Object of delete operation.\n        \"\"\"\n\n        operation = DeleteOperation(project_name, entity_type, entity_id)\n        self.add(operation)\n        return operation\n"
  },
  {
    "path": "openpype/client/server/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/client/server/constants.py",
    "content": "# --- Folders ---\nDEFAULT_FOLDER_FIELDS = {\n    \"id\",\n    \"name\",\n    \"path\",\n    \"parentId\",\n    \"active\",\n    \"parents\",\n    \"thumbnailId\"\n}\n\nREPRESENTATION_FILES_FIELDS = {\n    \"files.name\",\n    \"files.hash\",\n    \"files.id\",\n    \"files.path\",\n    \"files.size\",\n}\n"
  },
  {
    "path": "openpype/client/server/conversion_utils.py",
    "content": "import os\nimport arrow\nimport collections\nimport json\n\nimport six\n\nfrom openpype.client.operations_base import REMOVED_VALUE\nfrom openpype.client.mongo.operations import (\n    CURRENT_PROJECT_SCHEMA,\n    CURRENT_ASSET_DOC_SCHEMA,\n    CURRENT_SUBSET_SCHEMA,\n    CURRENT_VERSION_SCHEMA,\n    CURRENT_HERO_VERSION_SCHEMA,\n    CURRENT_REPRESENTATION_SCHEMA,\n    CURRENT_WORKFILE_INFO_SCHEMA,\n)\nfrom .constants import REPRESENTATION_FILES_FIELDS\nfrom .utils import create_entity_id, prepare_entity_changes\n\n# --- Project entity ---\nPROJECT_FIELDS_MAPPING_V3_V4 = {\n    \"_id\": {\"name\"},\n    \"name\": {\"name\"},\n    \"data\": {\"data\", \"code\"},\n    \"data.library_project\": {\"library\"},\n    \"data.code\": {\"code\"},\n    \"data.active\": {\"active\"},\n}\n\n# TODO this should not be hardcoded but received from server!!!\n# --- Folder entity ---\nFOLDER_FIELDS_MAPPING_V3_V4 = {\n    \"_id\": {\"id\"},\n    \"name\": {\"name\"},\n    \"label\": {\"label\"},\n    \"data\": {\n        \"parentId\", \"parents\", \"active\", \"tasks\", \"thumbnailId\"\n    },\n    \"data.visualParent\": {\"parentId\"},\n    \"data.parents\": {\"parents\"},\n    \"data.active\": {\"active\"},\n    \"data.thumbnail_id\": {\"thumbnailId\"},\n    \"data.entityType\": {\"folderType\"}\n}\n\n# --- Subset entity ---\nSUBSET_FIELDS_MAPPING_V3_V4 = {\n    \"_id\": {\"id\"},\n    \"name\": {\"name\"},\n    \"data.active\": {\"active\"},\n    \"parent\": {\"folderId\"}\n}\n\n# --- Version entity ---\nVERSION_FIELDS_MAPPING_V3_V4 = {\n    \"_id\": {\"id\"},\n    \"name\": {\"version\"},\n    \"parent\": {\"productId\"}\n}\n\n# --- Representation entity ---\nREPRESENTATION_FIELDS_MAPPING_V3_V4 = {\n    \"_id\": {\"id\"},\n    \"name\": {\"name\"},\n    \"parent\": {\"versionId\"},\n    \"context\": {\"context\"},\n    \"files\": {\"files\"},\n}\n\n\ndef project_fields_v3_to_v4(fields, con):\n    \"\"\"Convert project fields from v3 to v4 structure.\n\n    Args:\n        fields (Union[Iterable(str), None]): fields to be converted.\n\n    Returns:\n        Union[Set(str), None]: Converted fields to v4 fields.\n    \"\"\"\n\n    # TODO config fields\n    # - config.apps\n    # - config.groups\n    if not fields:\n        return None\n\n    project_attribs = con.get_attributes_for_type(\"project\")\n    output = set()\n    for field in fields:\n        # If config is needed the rest api call must be used\n        if field.startswith(\"config\"):\n            return None\n\n        if field in PROJECT_FIELDS_MAPPING_V3_V4:\n            output |= PROJECT_FIELDS_MAPPING_V3_V4[field]\n            if field == \"data\":\n                output |= {\n                    \"attrib.{}\".format(attr)\n                    for attr in project_attribs\n                }\n\n        elif field.startswith(\"data\"):\n            field_parts = field.split(\".\")\n            field_parts.pop(0)\n            data_key = \".\".join(field_parts)\n            if data_key in project_attribs:\n                output.add(\"attrib.{}\".format(data_key))\n            else:\n                output.add(\"data\")\n                print(\"Requested specific key from data {}\".format(data_key))\n\n        else:\n            raise ValueError(\"Unknown field mapping for {}\".format(field))\n\n    if \"name\" not in output:\n        output.add(\"name\")\n    return output\n\n\ndef _get_default_template_name(templates):\n    default_template = None\n    for name, template in templates.items():\n        if name == \"default\":\n            return \"default\"\n\n        if default_template is None:\n            default_template = name\n\n    return default_template\n\n\ndef _template_replacements_to_v3(template):\n    return (\n        template\n        .replace(\"{product[name]}\", \"{subset}\")\n        .replace(\"{product[type]}\", \"{family}\")\n    )\n\n\ndef _convert_template_item(template_item):\n    for key, value in tuple(template_item.items()):\n        template_item[key] = _template_replacements_to_v3(value)\n\n    # Change 'directory' to 'folder'\n    if \"directory\" in template_item:\n        template_item[\"folder\"] = template_item.pop(\"directory\")\n\n    if (\n        \"path\" not in template_item\n        and \"file\" in template_item\n        and \"folder\" in template_item\n    ):\n        template_item[\"path\"] = \"/\".join(\n            (template_item[\"folder\"], template_item[\"file\"])\n        )\n\n\ndef _fill_template_category(templates, cat_templates, cat_key):\n    default_template_name = _get_default_template_name(cat_templates)\n    for template_name, cat_template in cat_templates.items():\n        _convert_template_item(cat_template)\n        if template_name == default_template_name:\n            templates[cat_key] = cat_template\n        else:\n            new_name = \"{}_{}\".format(cat_key, template_name)\n            templates[\"others\"][new_name] = cat_template\n\n\ndef convert_v4_project_to_v3(project):\n    \"\"\"Convert Project entity data from v4 structure to v3 structure.\n\n    Args:\n        project (Dict[str, Any]): Project entity queried from v4 server.\n\n    Returns:\n        Dict[str, Any]: Project converted to v3 structure.\n    \"\"\"\n\n    if not project:\n        return project\n\n    project_name = project[\"name\"]\n    output = {\n        \"_id\": project_name,\n        \"name\": project_name,\n        \"schema\": CURRENT_PROJECT_SCHEMA,\n        \"type\": \"project\"\n    }\n\n    data = project.get(\"data\") or {}\n    attribs = project.get(\"attrib\") or {}\n    apps_attr = attribs.pop(\"applications\", None) or []\n    applications = [\n        {\"name\": app_name}\n        for app_name in apps_attr\n    ]\n    data.update(attribs)\n    if \"tools\" in data:\n        data[\"tools_env\"] = data.pop(\"tools\")\n\n    data[\"entityType\"] = \"Project\"\n\n    config = {}\n    project_config = project.get(\"config\")\n\n    if project_config:\n        config[\"apps\"] = applications\n        config[\"roots\"] = project_config[\"roots\"]\n\n        templates = project_config[\"templates\"]\n        templates[\"defaults\"] = templates.pop(\"common\", None) or {}\n\n        others_templates = templates.pop(\"others\", None) or {}\n        new_others_templates = {}\n        templates[\"others\"] = new_others_templates\n        for name, template in others_templates.items():\n            _convert_template_item(template)\n            new_others_templates[name] = template\n\n        staging_templates = templates.pop(\"staging\", None)\n        # Key 'staging_directories' is legacy key that changed\n        #   to 'staging_dir'\n        _legacy_staging_templates = templates.pop(\"staging_directories\", None)\n        if staging_templates is None:\n            staging_templates = _legacy_staging_templates\n\n        if staging_templates is None:\n            staging_templates = {}\n\n        # Prefix all staging template names with 'staging_' prefix\n        #   and add them to 'others'\n        for name, template in staging_templates.items():\n            _convert_template_item(template)\n            new_name = \"staging_{}\".format(name)\n            new_others_templates[new_name] = template\n\n        for key in (\n            \"work\",\n            \"publish\",\n            \"hero\",\n        ):\n            cat_templates = templates.pop(key)\n            _fill_template_category(templates, cat_templates, key)\n\n        delivery_templates = templates.pop(\"delivery\", None) or {}\n        new_delivery_templates = {}\n        for name, delivery_template in delivery_templates.items():\n            new_delivery_templates[name] = \"/\".join(\n                (delivery_template[\"directory\"], delivery_template[\"file\"])\n            )\n        templates[\"delivery\"] = new_delivery_templates\n\n        config[\"templates\"] = templates\n\n    if \"taskTypes\" in project:\n        task_types = project[\"taskTypes\"]\n        new_task_types = {}\n        for task_type in task_types:\n            name = task_type.pop(\"name\")\n            # Change 'shortName' to 'short_name'\n            task_type[\"short_name\"] = task_type.pop(\"shortName\", None)\n            new_task_types[name] = task_type\n\n        config[\"tasks\"] = new_task_types\n\n    if config:\n        output[\"config\"] = config\n\n    for data_key, key in (\n        (\"library_project\", \"library\"),\n        (\"code\", \"code\"),\n        (\"active\", \"active\")\n    ):\n        if key in project:\n            data[data_key] = project[key]\n\n    if \"attrib\" in project:\n        for key, value in project[\"attrib\"].items():\n            data[key] = value\n\n    if data:\n        output[\"data\"] = data\n    return output\n\n\ndef folder_fields_v3_to_v4(fields, con):\n    \"\"\"Convert folder fields from v3 to v4 structure.\n\n    Args:\n        fields (Union[Iterable(str), None]): fields to be converted.\n\n    Returns:\n        Union[Set(str), None]: Converted fields to v4 fields.\n    \"\"\"\n\n    if not fields:\n        return None\n\n    folder_attributes = con.get_attributes_for_type(\"folder\")\n    output = set()\n    for field in fields:\n        if field in (\"schema\", \"type\", \"parent\"):\n            continue\n\n        if field in FOLDER_FIELDS_MAPPING_V3_V4:\n            output |= FOLDER_FIELDS_MAPPING_V3_V4[field]\n            if field == \"data\":\n                output |= {\n                    \"attrib.{}\".format(attr)\n                    for attr in folder_attributes\n                }\n\n        elif field.startswith(\"data\"):\n            field_parts = field.split(\".\")\n            field_parts.pop(0)\n            data_key = \".\".join(field_parts)\n            if data_key == \"label\":\n                output.add(\"name\")\n\n            elif data_key in (\"icon\", \"color\"):\n                continue\n\n            elif data_key.startswith(\"tasks\"):\n                output.add(\"tasks\")\n\n            elif data_key in folder_attributes:\n                output.add(\"attrib.{}\".format(data_key))\n\n            else:\n                output.add(\"data\")\n                print(\"Requested specific key from data {}\".format(data_key))\n\n        else:\n            raise ValueError(\"Unknown field mapping for {}\".format(field))\n\n    if \"id\" not in output:\n        output.add(\"id\")\n    return output\n\n\ndef convert_v4_tasks_to_v3(tasks):\n    \"\"\"Convert v4 task item to v3 task.\n\n    Args:\n        tasks (List[Dict[str, Any]]): Task entites.\n\n    Returns:\n        Dict[str, Dict[str, Any]]: Tasks in v3 variant ready for v3 asset.\n    \"\"\"\n\n    output = {}\n    for task in tasks:\n        task_name = task[\"name\"]\n        new_task = {\n            \"type\": task[\"taskType\"]\n        }\n        output[task_name] = new_task\n    return output\n\n\ndef convert_v4_folder_to_v3(folder, project_name):\n    \"\"\"Convert v4 folder to v3 asset.\n\n    Args:\n        folder (Dict[str, Any]): Folder entity data.\n        project_name (str): Project name from which folder was queried.\n\n    Returns:\n        Dict[str, Any]: Converted v4 folder to v3 asset.\n    \"\"\"\n\n    output = {\n        \"_id\": folder[\"id\"],\n        \"parent\": project_name,\n        \"type\": \"asset\",\n        \"schema\": CURRENT_ASSET_DOC_SCHEMA\n    }\n\n    output_data = folder.get(\"data\") or {}\n\n    if \"name\" in folder:\n        output[\"name\"] = folder[\"name\"]\n        output_data[\"label\"] = folder[\"name\"]\n\n    if \"folderType\" in folder:\n        output_data[\"entityType\"] = folder[\"folderType\"]\n\n    for src_key, dst_key in (\n        (\"parentId\", \"visualParent\"),\n        (\"active\", \"active\"),\n        (\"thumbnailId\", \"thumbnail_id\"),\n        (\"parents\", \"parents\"),\n    ):\n        if src_key in folder:\n            output_data[dst_key] = folder[src_key]\n\n    if \"attrib\" in folder:\n        output_data.update(folder[\"attrib\"])\n\n    if \"tools\" in output_data:\n        output_data[\"tools_env\"] = output_data.pop(\"tools\")\n\n    if \"tasks\" in folder:\n        output_data[\"tasks\"] = convert_v4_tasks_to_v3(folder[\"tasks\"])\n\n    output[\"data\"] = output_data\n\n    return output\n\n\ndef subset_fields_v3_to_v4(fields, con):\n    \"\"\"Convert subset fields from v3 to v4 structure.\n\n    Args:\n        fields (Union[Iterable(str), None]): fields to be converted.\n\n    Returns:\n        Union[Set(str), None]: Converted fields to v4 fields.\n    \"\"\"\n\n    if not fields:\n        return None\n\n    product_attributes = con.get_attributes_for_type(\"product\")\n\n    output = set()\n    for field in fields:\n        if field in (\"schema\", \"type\"):\n            continue\n\n        if field in SUBSET_FIELDS_MAPPING_V3_V4:\n            output |= SUBSET_FIELDS_MAPPING_V3_V4[field]\n\n        elif field == \"data\":\n            output.add(\"productType\")\n            output.add(\"active\")\n            output |= {\n                \"attrib.{}\".format(attr)\n                for attr in product_attributes\n            }\n\n        elif field.startswith(\"data\"):\n            field_parts = field.split(\".\")\n            field_parts.pop(0)\n            data_key = \".\".join(field_parts)\n            if data_key in (\"family\", \"families\"):\n                output.add(\"productType\")\n\n            elif data_key in product_attributes:\n                output.add(\"attrib.{}\".format(data_key))\n\n            else:\n                output.add(\"data\")\n                print(\"Requested specific key from data {}\".format(data_key))\n\n        else:\n            raise ValueError(\"Unknown field mapping for {}\".format(field))\n\n    if \"id\" not in output:\n        output.add(\"id\")\n    return output\n\n\ndef convert_v4_subset_to_v3(subset):\n    output = {\n        \"_id\": subset[\"id\"],\n        \"type\": \"subset\",\n        \"schema\": CURRENT_SUBSET_SCHEMA\n    }\n    if \"folderId\" in subset:\n        output[\"parent\"] = subset[\"folderId\"]\n\n    output_data = subset.get(\"data\") or {}\n\n    if \"name\" in subset:\n        output[\"name\"] = subset[\"name\"]\n\n    if \"active\" in subset:\n        output_data[\"active\"] = subset[\"active\"]\n\n    if \"attrib\" in subset:\n        attrib = subset[\"attrib\"]\n        if \"productGroup\" in attrib:\n            attrib[\"subsetGroup\"] = attrib.pop(\"productGroup\")\n        output_data.update(attrib)\n\n    family = subset.get(\"productType\")\n    if family:\n        output_data[\"family\"] = family\n        output_data[\"families\"] = [family]\n\n    output[\"data\"] = output_data\n\n    return output\n\n\ndef version_fields_v3_to_v4(fields, con):\n    \"\"\"Convert version fields from v3 to v4 structure.\n\n    Args:\n        fields (Union[Iterable(str), None]): fields to be converted.\n\n    Returns:\n        Union[Set(str), None]: Converted fields to v4 fields.\n    \"\"\"\n\n    if not fields:\n        return None\n\n    version_attributes = con.get_attributes_for_type(\"version\")\n\n    output = set()\n    for field in fields:\n        if field in (\"type\", \"schema\", \"version_id\"):\n            continue\n\n        if field in VERSION_FIELDS_MAPPING_V3_V4:\n            output |= VERSION_FIELDS_MAPPING_V3_V4[field]\n\n        elif field == \"data\":\n            output |= {\n                \"attrib.{}\".format(attr)\n                for attr in version_attributes\n            }\n            output |= {\n                \"author\",\n                \"createdAt\",\n                \"thumbnailId\",\n            }\n\n        elif field.startswith(\"data\"):\n            field_parts = field.split(\".\")\n            field_parts.pop(0)\n            data_key = \".\".join(field_parts)\n            if data_key in version_attributes:\n                output.add(\"attrib.{}\".format(data_key))\n\n            elif data_key == \"thumbnail_id\":\n                output.add(\"thumbnailId\")\n\n            elif data_key == \"time\":\n                output.add(\"createdAt\")\n\n            elif data_key == \"author\":\n                output.add(\"author\")\n\n            elif data_key in (\"tags\", ):\n                continue\n\n            else:\n                output.add(\"data\")\n                print(\"Requested specific key from data {}\".format(data_key))\n\n        else:\n            raise ValueError(\"Unknown field mapping for {}\".format(field))\n\n    if \"id\" not in output:\n        output.add(\"id\")\n    return output\n\n\ndef convert_v4_version_to_v3(version):\n    \"\"\"Convert v4 version entity to v4 version.\n\n    Args:\n        version (Dict[str, Any]): Queried v4 version entity.\n\n    Returns:\n        Dict[str, Any]: Conveted version entity to v3 structure.\n    \"\"\"\n\n    version_num = version[\"version\"]\n    if version_num < 0:\n        output = {\n            \"_id\": version[\"id\"],\n            \"type\": \"hero_version\",\n            \"schema\": CURRENT_HERO_VERSION_SCHEMA,\n        }\n        if \"productId\" in version:\n            output[\"parent\"] = version[\"productId\"]\n\n        if \"data\" in version:\n            output[\"data\"] = version[\"data\"]\n        return output\n\n    output = {\n        \"_id\": version[\"id\"],\n        \"type\": \"version\",\n        \"name\": version_num,\n        \"schema\": CURRENT_VERSION_SCHEMA\n    }\n    if \"productId\" in version:\n        output[\"parent\"] = version[\"productId\"]\n\n    output_data = version.get(\"data\") or {}\n    if \"attrib\" in version:\n        output_data.update(version[\"attrib\"])\n\n    for src_key, dst_key in (\n        (\"active\", \"active\"),\n        (\"thumbnailId\", \"thumbnail_id\"),\n        (\"author\", \"author\")\n    ):\n        if src_key in version:\n            output_data[dst_key] = version[src_key]\n\n    if \"createdAt\" in version:\n        created_at = arrow.get(version[\"createdAt\"]).to(\"local\")\n        output_data[\"time\"] = created_at.strftime(\"%Y%m%dT%H%M%SZ\")\n\n    output[\"data\"] = output_data\n\n    return output\n\n\ndef representation_fields_v3_to_v4(fields, con):\n    \"\"\"Convert representation fields from v3 to v4 structure.\n\n    Args:\n        fields (Union[Iterable(str), None]): fields to be converted.\n\n    Returns:\n        Union[Set(str), None]: Converted fields to v4 fields.\n    \"\"\"\n\n    if not fields:\n        return None\n\n    representation_attributes = con.get_attributes_for_type(\"representation\")\n\n    output = set()\n    for field in fields:\n        if field in (\"type\", \"schema\"):\n            continue\n\n        if field in REPRESENTATION_FIELDS_MAPPING_V3_V4:\n            output |= REPRESENTATION_FIELDS_MAPPING_V3_V4[field]\n\n        elif field.startswith(\"context\"):\n            output.add(\"context\")\n\n        # TODO: 'files' can have specific attributes but the keys in v3 and v4\n        #   are not the same (content is not the same)\n        elif field.startswith(\"files\"):\n            output |= REPRESENTATION_FILES_FIELDS\n\n        elif field.startswith(\"data\"):\n            output |= {\n                \"attrib.{}\".format(attr)\n                for attr in representation_attributes\n            }\n\n        else:\n            raise ValueError(\"Unknown field mapping for {}\".format(field))\n\n    if \"id\" not in output:\n        output.add(\"id\")\n    return output\n\n\ndef convert_v4_representation_to_v3(representation):\n    \"\"\"Convert v4 representation to v3 representation.\n\n    Args:\n        representation (Dict[str, Any]): Queried representation from v4 server.\n\n    Returns:\n        Dict[str, Any]: Converted representation to v3 structure.\n    \"\"\"\n\n    output = {\n        \"type\": \"representation\",\n        \"schema\": CURRENT_REPRESENTATION_SCHEMA,\n    }\n    if \"id\" in representation:\n        output[\"_id\"] = representation[\"id\"]\n\n    for v3_key, v4_key in (\n        (\"name\", \"name\"),\n        (\"parent\", \"versionId\")\n    ):\n        if v4_key in representation:\n            output[v3_key] = representation[v4_key]\n\n    if \"context\" in representation:\n        context = representation[\"context\"]\n        if isinstance(context, six.string_types):\n            context = json.loads(context)\n\n        if \"asset\" not in context and \"folder\" in context:\n            _c_folder = context[\"folder\"]\n            context[\"asset\"] = _c_folder[\"name\"]\n\n        elif \"asset\" in context and \"folder\" not in context:\n            context[\"folder\"] = {\"name\": context[\"asset\"]}\n\n        if \"product\" in context:\n            _c_product = context.pop(\"product\")\n            context[\"family\"] = _c_product[\"type\"]\n            context[\"subset\"] = _c_product[\"name\"]\n\n        output[\"context\"] = context\n\n    if \"files\" in representation:\n        files = representation[\"files\"]\n        new_files = []\n        # From GraphQl is list\n        if isinstance(files, list):\n            for file_info in files:\n                file_info[\"_id\"] = file_info[\"id\"]\n                new_files.append(file_info)\n\n        # From RestPoint is dictionary\n        elif isinstance(files, dict):\n            for file_id, file_info in files:\n                file_info[\"_id\"] = file_id\n                new_files.append(file_info)\n\n        for file_info in new_files:\n            if not file_info.get(\"sites\"):\n                file_info[\"sites\"] = [{\n                    \"name\": \"studio\"\n                }]\n\n        output[\"files\"] = new_files\n\n    if representation.get(\"active\") is False:\n        output[\"type\"] = \"archived_representation\"\n        output[\"old_id\"] = output[\"_id\"]\n\n    output_data = representation.get(\"data\") or {}\n    if \"attrib\" in representation:\n        output_data.update(representation[\"attrib\"])\n\n    for key, data_key in (\n        (\"active\", \"active\"),\n    ):\n        if key in representation:\n            output_data[data_key] = representation[key]\n\n    if \"template\" in output_data:\n        output_data[\"template\"] = (\n            output_data[\"template\"]\n            .replace(\"{product[name]}\", \"{subset}\")\n            .replace(\"{product[type]}\", \"{family}\")\n        )\n\n    output[\"data\"] = output_data\n\n    return output\n\n\ndef workfile_info_fields_v3_to_v4(fields):\n    if not fields:\n        return None\n\n    new_fields = set()\n    fields = set(fields)\n    for v3_key, v4_key in (\n        (\"_id\", \"id\"),\n        (\"files\", \"path\"),\n        (\"filename\", \"name\"),\n        (\"data\", \"data\"),\n    ):\n        if v3_key in fields:\n            new_fields.add(v4_key)\n\n    if \"parent\" in fields or \"task_name\" in fields:\n        new_fields.add(\"taskId\")\n\n    return new_fields\n\n\ndef convert_v4_workfile_info_to_v3(workfile_info, task):\n    output = {\n        \"type\": \"workfile\",\n        \"schema\": CURRENT_WORKFILE_INFO_SCHEMA,\n    }\n    if \"id\" in workfile_info:\n        output[\"_id\"] = workfile_info[\"id\"]\n\n    if \"path\" in workfile_info:\n        output[\"files\"] = [workfile_info[\"path\"]]\n\n    if \"name\" in workfile_info:\n        output[\"filename\"] = workfile_info[\"name\"]\n\n    if \"taskId\" in workfile_info:\n        output[\"task_name\"] = task[\"name\"]\n        output[\"parent\"] = task[\"folderId\"]\n\n    return output\n\n\ndef convert_create_asset_to_v4(asset, project, con):\n    folder_attributes = con.get_attributes_for_type(\"folder\")\n\n    asset_data = asset[\"data\"]\n    parent_id = asset_data[\"visualParent\"]\n\n    folder = {\n        \"name\": asset[\"name\"],\n        \"parentId\": parent_id,\n    }\n    entity_id = asset.get(\"_id\")\n    if entity_id:\n        folder[\"id\"] = entity_id\n\n    attribs = {}\n    data = {}\n    for key, value in asset_data.items():\n        if key in (\n            \"visualParent\",\n            \"thumbnail_id\",\n            \"parents\",\n            \"inputLinks\",\n            \"avalon_mongo_id\",\n        ):\n            continue\n\n        if key not in folder_attributes:\n            data[key] = value\n        elif value is not None:\n            attribs[key] = value\n\n    if attribs:\n        folder[\"attrib\"] = attribs\n\n    if data:\n        folder[\"data\"] = data\n    return folder\n\n\ndef convert_create_task_to_v4(task, project, con):\n    if not project[\"taskTypes\"]:\n        raise ValueError(\n            \"Project \\\"{}\\\" does not have any task types\".format(\n                project[\"name\"]))\n\n    task_type = task[\"type\"]\n    if task_type not in project[\"taskTypes\"]:\n        task_type = tuple(project[\"taskTypes\"].keys())[0]\n\n    return {\n        \"name\": task[\"name\"],\n        \"taskType\": task_type,\n        \"folderId\": task[\"folderId\"]\n    }\n\n\ndef convert_create_subset_to_v4(subset, con):\n    product_attributes = con.get_attributes_for_type(\"product\")\n\n    subset_data = subset[\"data\"]\n    product_type = subset_data.get(\"family\")\n    if not product_type:\n        product_type = subset_data[\"families\"][0]\n\n    converted_product = {\n        \"name\": subset[\"name\"],\n        \"productType\": product_type,\n        \"folderId\": subset[\"parent\"],\n    }\n    entity_id = subset.get(\"_id\")\n    if entity_id:\n        converted_product[\"id\"] = entity_id\n\n    attribs = {}\n    data = {}\n    if \"subsetGroup\" in subset_data:\n        subset_data[\"productGroup\"] = subset_data.pop(\"subsetGroup\")\n    for key, value in subset_data.items():\n        if key not in product_attributes:\n            data[key] = value\n        elif value is not None:\n            attribs[key] = value\n\n    if attribs:\n        converted_product[\"attrib\"] = attribs\n\n    if data:\n        converted_product[\"data\"] = data\n\n    return converted_product\n\n\ndef convert_create_version_to_v4(version, con):\n    version_attributes = con.get_attributes_for_type(\"version\")\n    converted_version = {\n        \"version\": version[\"name\"],\n        \"productId\": version[\"parent\"],\n    }\n    entity_id = version.get(\"_id\")\n    if entity_id:\n        converted_version[\"id\"] = entity_id\n\n    version_data = version[\"data\"]\n    attribs = {}\n    data = {}\n    for key, value in version_data.items():\n        if key not in version_attributes:\n            data[key] = value\n        elif value is not None:\n            attribs[key] = value\n\n    if attribs:\n        converted_version[\"attrib\"] = attribs\n\n    if data:\n        converted_version[\"data\"] = attribs\n\n    return converted_version\n\n\ndef convert_create_hero_version_to_v4(hero_version, project_name, con):\n    if \"version_id\" in hero_version:\n        version_id = hero_version[\"version_id\"]\n        version = con.get_version_by_id(project_name, version_id)\n        version[\"version\"] = - version[\"version\"]\n\n        for auto_key in (\n            \"name\",\n            \"createdAt\",\n            \"updatedAt\",\n            \"author\",\n        ):\n            version.pop(auto_key, None)\n\n        return version\n\n    version_attributes = con.get_attributes_for_type(\"version\")\n    converted_version = {\n        \"version\": hero_version[\"version\"],\n        \"productId\": hero_version[\"parent\"],\n    }\n    entity_id = hero_version.get(\"_id\")\n    if entity_id:\n        converted_version[\"id\"] = entity_id\n\n    version_data = hero_version[\"data\"]\n    attribs = {}\n    data = {}\n    for key, value in version_data.items():\n        if key not in version_attributes:\n            data[key] = value\n        elif value is not None:\n            attribs[key] = value\n\n    if attribs:\n        converted_version[\"attrib\"] = attribs\n\n    if data:\n        converted_version[\"data\"] = attribs\n\n    return converted_version\n\n\ndef convert_create_representation_to_v4(representation, con):\n    representation_attributes = con.get_attributes_for_type(\"representation\")\n\n    converted_representation = {\n        \"name\": representation[\"name\"],\n        \"versionId\": representation[\"parent\"],\n    }\n    entity_id = representation.get(\"_id\")\n    if entity_id:\n        converted_representation[\"id\"] = entity_id\n\n    if representation.get(\"type\") == \"archived_representation\":\n        converted_representation[\"active\"] = False\n\n    new_files = []\n    for file_item in representation[\"files\"]:\n        new_file_item = {\n            key: value\n            for key, value in file_item.items()\n            if key in (\"hash\", \"path\", \"size\")\n        }\n        new_file_item.update({\n            \"id\": create_entity_id(),\n            \"hash_type\": \"op3\",\n            \"name\": os.path.basename(new_file_item[\"path\"])\n        })\n        new_files.append(new_file_item)\n\n    converted_representation[\"files\"] = new_files\n\n    context = representation[\"context\"]\n    if \"folder\" not in context:\n        context[\"folder\"] = {\n            \"name\": context.get(\"asset\")\n        }\n\n    context[\"product\"] = {\n        \"type\": context.pop(\"family\", None),\n        \"name\": context.pop(\"subset\", None),\n    }\n\n    attribs = {}\n    data = {\n        \"context\": context,\n    }\n\n    representation_data = representation[\"data\"]\n    representation_data[\"template\"] = (\n        representation_data[\"template\"]\n        .replace(\"{subset}\", \"{product[name]}\")\n        .replace(\"{family}\", \"{product[type]}\")\n    )\n\n    for key, value in representation_data.items():\n        if key not in representation_attributes:\n            data[key] = value\n        elif value is not None:\n            attribs[key] = value\n\n    if attribs:\n        converted_representation[\"attrib\"] = attribs\n\n    if data:\n        converted_representation[\"data\"] = data\n\n    return converted_representation\n\n\ndef convert_create_workfile_info_to_v4(data, project_name, con):\n    folder_id = data[\"parent\"]\n    task_name = data[\"task_name\"]\n    task = con.get_task_by_name(project_name, folder_id, task_name)\n    if not task:\n        return None\n\n    workfile_attributes = con.get_attributes_for_type(\"workfile\")\n    filename = data[\"filename\"]\n    possible_attribs = {\n        \"extension\": os.path.splitext(filename)[-1]\n    }\n    attribs = {}\n    for attr in workfile_attributes:\n        if attr in possible_attribs:\n            attribs[attr] = possible_attribs[attr]\n\n    output = {\n        \"path\": data[\"files\"][0],\n        \"name\": filename,\n        \"taskId\": task[\"id\"]\n    }\n    if \"_id\" in data:\n        output[\"id\"] = data[\"_id\"]\n\n    if attribs:\n        output[\"attrib\"] = attribs\n\n    output_data = data.get(\"data\")\n    if output_data:\n        output[\"data\"] = output_data\n    return output\n\n\ndef _from_flat_dict(data):\n    output = {}\n    for key, value in data.items():\n        output_value = output\n        subkeys = key.split(\".\")\n        last_key = subkeys.pop(-1)\n        for subkey in subkeys:\n            if subkey not in output_value:\n                output_value[subkey] = {}\n            output_value = output_value[subkey]\n\n        output_value[last_key] = value\n    return output\n\n\ndef _to_flat_dict(data):\n    output = {}\n    flat_queue = collections.deque()\n    flat_queue.append(([], data))\n    while flat_queue:\n        item = flat_queue.popleft()\n        parent_keys, data = item\n        for key, value in data.items():\n            keys = list(parent_keys)\n            keys.append(key)\n            if isinstance(value, dict):\n                flat_queue.append((keys, value))\n            else:\n                full_key = \".\".join(keys)\n                output[full_key] = value\n\n    return output\n\n\ndef convert_update_folder_to_v4(project_name, asset_id, update_data, con):\n    new_update_data = {}\n\n    folder_attributes = con.get_attributes_for_type(\"folder\")\n    full_update_data = _from_flat_dict(update_data)\n    data = full_update_data.get(\"data\")\n\n    has_new_parent = False\n    has_task_changes = False\n    parent_id = None\n    tasks = None\n    new_data = {}\n    attribs = full_update_data.pop(\"attrib\", {})\n    if \"type\" in update_data:\n        new_update_data[\"active\"] = update_data[\"type\"] == \"asset\"\n\n    if data:\n        if \"thumbnail_id\" in data:\n            new_update_data[\"thumbnailId\"] = data.pop(\"thumbnail_id\")\n\n        if \"tasks\" in data:\n            tasks = data.pop(\"tasks\")\n            has_task_changes = True\n\n        if \"visualParent\" in data:\n            has_new_parent = True\n            parent_id = data.pop(\"visualParent\")\n\n        for key, value in data.items():\n            if key in folder_attributes:\n                attribs[key] = value\n            else:\n                new_data[key] = value\n\n    if \"name\" in update_data:\n        new_update_data[\"name\"] = update_data[\"name\"]\n\n    if \"type\" in update_data:\n        new_type = update_data[\"type\"]\n        if new_type == \"asset\":\n            new_update_data[\"active\"] = True\n        elif new_type == \"archived_asset\":\n            new_update_data[\"active\"] = False\n\n    if has_new_parent:\n        new_update_data[\"parentId\"] = parent_id\n\n    if new_data:\n        print(\"Folder has new data: {}\".format(new_data))\n        new_update_data[\"data\"] = new_data\n\n    if attribs:\n        new_update_data[\"attrib\"] = attribs\n\n    if has_task_changes:\n        raise ValueError(\"Task changes of folder are not implemented\")\n\n    return _to_flat_dict(new_update_data)\n\n\ndef convert_update_subset_to_v4(project_name, subset_id, update_data, con):\n    new_update_data = {}\n\n    product_attributes = con.get_attributes_for_type(\"product\")\n    full_update_data = _from_flat_dict(update_data)\n    data = full_update_data.get(\"data\")\n    new_data = {}\n    attribs = full_update_data.pop(\"attrib\", {})\n    if data:\n        if \"family\" in data:\n            family = data.pop(\"family\")\n            new_update_data[\"productType\"] = family\n\n        if \"families\" in data:\n            families = data.pop(\"families\")\n            if \"productType\" not in new_update_data:\n                new_update_data[\"productType\"] = families[0]\n\n        if \"subsetGroup\" in data:\n            data[\"productGroup\"] = data.pop(\"subsetGroup\")\n        for key, value in data.items():\n            if key in product_attributes:\n                if value is REMOVED_VALUE:\n                    value = None\n                attribs[key] = value\n\n            elif value is not REMOVED_VALUE:\n                new_data[key] = value\n\n    if \"name\" in update_data:\n        new_update_data[\"name\"] = update_data[\"name\"]\n\n    if \"type\" in update_data:\n        new_type = update_data[\"type\"]\n        if new_type == \"subset\":\n            new_update_data[\"active\"] = True\n        elif new_type == \"archived_subset\":\n            new_update_data[\"active\"] = False\n\n    if \"parent\" in update_data:\n        new_update_data[\"folderId\"] = update_data[\"parent\"]\n\n    flat_data = _to_flat_dict(new_update_data)\n    if attribs:\n        flat_data[\"attrib\"] = attribs\n\n    if new_data:\n        print(\"Subset has new data: {}\".format(new_data))\n        flat_data[\"data\"] = new_data\n\n    return flat_data\n\n\ndef convert_update_version_to_v4(project_name, version_id, update_data, con):\n    new_update_data = {}\n\n    version_attributes = con.get_attributes_for_type(\"version\")\n    full_update_data = _from_flat_dict(update_data)\n    data = full_update_data.get(\"data\")\n    new_data = {}\n    attribs = full_update_data.pop(\"attrib\", {})\n    if data:\n        if \"author\" in data:\n            new_update_data[\"author\"] = data.pop(\"author\")\n\n        if \"thumbnail_id\" in data:\n            new_update_data[\"thumbnailId\"] = data.pop(\"thumbnail_id\")\n\n        for key, value in data.items():\n            if key in version_attributes:\n                if value is REMOVED_VALUE:\n                    value = None\n                attribs[key] = value\n\n            elif value is not REMOVED_VALUE:\n                new_data[key] = value\n\n    if \"name\" in update_data:\n        new_update_data[\"version\"] = update_data[\"name\"]\n\n    if \"type\" in update_data:\n        new_type = update_data[\"type\"]\n        if new_type == \"version\":\n            new_update_data[\"active\"] = True\n        elif new_type == \"archived_version\":\n            new_update_data[\"active\"] = False\n\n    if \"parent\" in update_data:\n        new_update_data[\"productId\"] = update_data[\"parent\"]\n\n    flat_data = _to_flat_dict(new_update_data)\n    if attribs:\n        flat_data[\"attrib\"] = attribs\n\n    if new_data:\n        print(\"Version has new data: {}\".format(new_data))\n        flat_data[\"data\"] = new_data\n    return flat_data\n\n\ndef convert_update_hero_version_to_v4(\n    project_name, hero_version_id, update_data, con\n):\n    if \"version_id\" not in update_data:\n        return None\n\n    version_id = update_data[\"version_id\"]\n    hero_version = con.get_hero_version_by_id(project_name, hero_version_id)\n    version = con.get_version_by_id(project_name, version_id)\n    version[\"version\"] = - version[\"version\"]\n    version[\"id\"] = hero_version_id\n\n    for auto_key in (\n        \"name\",\n        \"createdAt\",\n        \"updatedAt\",\n        \"author\",\n    ):\n        version.pop(auto_key, None)\n\n    return prepare_entity_changes(hero_version, version)\n\n\ndef convert_update_representation_to_v4(\n    project_name, repre_id, update_data, con\n):\n    new_update_data = {}\n\n    folder_attributes = con.get_attributes_for_type(\"folder\")\n    full_update_data = _from_flat_dict(update_data)\n    data = full_update_data.get(\"data\")\n\n    new_data = {}\n    attribs = full_update_data.pop(\"attrib\", {})\n    if data:\n        for key, value in data.items():\n            if key in folder_attributes:\n                attribs[key] = value\n            else:\n                new_data[key] = value\n\n    if \"template\" in attribs:\n        attribs[\"template\"] = (\n            attribs[\"template\"]\n            .replace(\"{family}\", \"{product[type]}\")\n            .replace(\"{subset}\", \"{product[name]}\")\n        )\n\n    if \"name\" in update_data:\n        new_update_data[\"name\"] = update_data[\"name\"]\n\n    if \"type\" in update_data:\n        new_type = update_data[\"type\"]\n        if new_type == \"representation\":\n            new_update_data[\"active\"] = True\n        elif new_type == \"archived_representation\":\n            new_update_data[\"active\"] = False\n\n    if \"parent\" in update_data:\n        new_update_data[\"versionId\"] = update_data[\"parent\"]\n\n    if \"context\" in update_data:\n        context = update_data[\"context\"]\n        if \"folder\" not in context and \"asset\" in context:\n            context[\"folder\"] = {\"name\": context.pop(\"asset\")}\n\n        if \"family\" in context or \"subset\" in context:\n            context[\"product\"] = {\n                \"name\": context.pop(\"subset\"),\n                \"type\": context.pop(\"family\"),\n            }\n        new_data[\"context\"] = context\n\n    if \"files\" in update_data:\n        new_files = update_data[\"files\"]\n        if isinstance(new_files, dict):\n            new_files = list(new_files.values())\n\n        for item in new_files:\n            for key in tuple(item.keys()):\n                if key not in (\"hash\", \"path\", \"size\"):\n                    item.pop(key)\n            item.update({\n                \"id\": create_entity_id(),\n                \"name\": os.path.basename(item[\"path\"]),\n                \"hash_type\": \"op3\",\n            })\n        new_update_data[\"files\"] = new_files\n\n    flat_data = _to_flat_dict(new_update_data)\n    if attribs:\n        flat_data[\"attrib\"] = attribs\n\n    if new_data:\n        print(\"Representation has new data: {}\".format(new_data))\n        flat_data[\"data\"] = new_data\n\n    return flat_data\n\n\ndef convert_update_workfile_info_to_v4(\n    project_name, workfile_id, update_data, con\n):\n    return {\n        key: value\n        for key, value in update_data.items()\n        if key.startswith(\"data\")\n    }\n"
  },
  {
    "path": "openpype/client/server/entities.py",
    "content": "import collections\n\nfrom openpype.client.mongo.operations import CURRENT_THUMBNAIL_SCHEMA\n\nfrom .utils import get_ayon_server_api_connection\nfrom .openpype_comp import get_folders_with_tasks\nfrom .conversion_utils import (\n    project_fields_v3_to_v4,\n    convert_v4_project_to_v3,\n\n    folder_fields_v3_to_v4,\n    convert_v4_folder_to_v3,\n\n    subset_fields_v3_to_v4,\n    convert_v4_subset_to_v3,\n\n    version_fields_v3_to_v4,\n    convert_v4_version_to_v3,\n\n    representation_fields_v3_to_v4,\n    convert_v4_representation_to_v3,\n\n    workfile_info_fields_v3_to_v4,\n    convert_v4_workfile_info_to_v3,\n)\n\n\ndef get_projects(active=True, inactive=False, library=None, fields=None):\n    if not active and not inactive:\n        return\n\n    if active and inactive:\n        active = None\n    elif active:\n        active = True\n    elif inactive:\n        active = False\n\n    con = get_ayon_server_api_connection()\n    fields = project_fields_v3_to_v4(fields, con)\n    for project in con.get_projects(active, library, fields=fields):\n        yield convert_v4_project_to_v3(project)\n\n\ndef get_project(project_name, active=True, inactive=False, fields=None):\n    # Skip if both are disabled\n    con = get_ayon_server_api_connection()\n    fields = project_fields_v3_to_v4(fields, con)\n    return convert_v4_project_to_v3(\n        con.get_project(project_name, fields=fields)\n    )\n\n\ndef get_whole_project(*args, **kwargs):\n    raise NotImplementedError(\"'get_whole_project' not implemented\")\n\n\ndef _get_subsets(\n    project_name,\n    subset_ids=None,\n    subset_names=None,\n    folder_ids=None,\n    names_by_folder_ids=None,\n    archived=False,\n    fields=None\n):\n    # Convert fields and add minimum required fields\n    con = get_ayon_server_api_connection()\n    fields = subset_fields_v3_to_v4(fields, con)\n    if fields is not None:\n        for key in (\n            \"id\",\n            \"active\"\n        ):\n            fields.add(key)\n\n    active = True\n    if archived:\n        active = None\n\n    for subset in con.get_products(\n        project_name,\n        product_ids=subset_ids,\n        product_names=subset_names,\n        folder_ids=folder_ids,\n        names_by_folder_ids=names_by_folder_ids,\n        active=active,\n        fields=fields,\n    ):\n        yield convert_v4_subset_to_v3(subset)\n\n\ndef _get_versions(\n    project_name,\n    version_ids=None,\n    subset_ids=None,\n    versions=None,\n    hero=True,\n    standard=True,\n    latest=None,\n    active=None,\n    fields=None\n):\n    con = get_ayon_server_api_connection()\n\n    fields = version_fields_v3_to_v4(fields, con)\n\n    # Make sure 'productId' and 'version' are available when hero versions\n    #   are queried\n    if fields and hero:\n        fields = set(fields)\n        fields |= {\"productId\", \"version\"}\n\n    queried_versions = con.get_versions(\n        project_name,\n        version_ids=version_ids,\n        product_ids=subset_ids,\n        versions=versions,\n        hero=hero,\n        standard=standard,\n        latest=latest,\n        active=active,\n        fields=fields\n    )\n\n    version_entities = []\n    hero_versions = []\n    for version in queried_versions:\n        if version[\"version\"] < 0:\n            hero_versions.append(version)\n        else:\n            version_entities.append(convert_v4_version_to_v3(version))\n\n    if hero_versions:\n        subset_ids = set()\n        versions_nums = set()\n        for hero_version in hero_versions:\n            versions_nums.add(abs(hero_version[\"version\"]))\n            subset_ids.add(hero_version[\"productId\"])\n\n        hero_eq_versions = con.get_versions(\n            project_name,\n            product_ids=subset_ids,\n            versions=versions_nums,\n            hero=False,\n            fields=[\"id\", \"version\", \"productId\"]\n        )\n        hero_eq_by_subset_id = collections.defaultdict(list)\n        for version in hero_eq_versions:\n            hero_eq_by_subset_id[version[\"productId\"]].append(version)\n\n        for hero_version in hero_versions:\n            abs_version = abs(hero_version[\"version\"])\n            subset_id = hero_version[\"productId\"]\n            version_id = None\n            for version in hero_eq_by_subset_id.get(subset_id, []):\n                if version[\"version\"] == abs_version:\n                    version_id = version[\"id\"]\n                    break\n            conv_hero = convert_v4_version_to_v3(hero_version)\n            conv_hero[\"version_id\"] = version_id\n            version_entities.append(conv_hero)\n\n    return version_entities\n\n\ndef get_asset_by_id(project_name, asset_id, fields=None):\n    assets = get_assets(\n        project_name, asset_ids=[asset_id], fields=fields\n    )\n    for asset in assets:\n        return asset\n    return None\n\n\ndef get_asset_by_name(project_name, asset_name, fields=None):\n    assets = get_assets(\n        project_name, asset_names=[asset_name], fields=fields\n    )\n    for asset in assets:\n        return asset\n    return None\n\n\ndef _folders_query(project_name, con, fields, **kwargs):\n    if fields is None or \"tasks\" in fields:\n        folders = get_folders_with_tasks(\n            con, project_name, fields=fields, **kwargs\n        )\n\n    else:\n        folders = con.get_folders(project_name, fields=fields, **kwargs)\n\n    for folder in folders:\n        yield folder\n\n\ndef get_assets(\n    project_name,\n    asset_ids=None,\n    asset_names=None,\n    parent_ids=None,\n    archived=False,\n    fields=None\n):\n    if not project_name:\n        return\n\n    active = True\n    if archived:\n        active = None\n\n    con = get_ayon_server_api_connection()\n    fields = folder_fields_v3_to_v4(fields, con)\n    kwargs = dict(\n        folder_ids=asset_ids,\n        parent_ids=parent_ids,\n        active=active,\n    )\n    if not asset_names:\n        for folder in _folders_query(project_name, con, fields, **kwargs):\n            yield convert_v4_folder_to_v3(folder, project_name)\n        return\n\n    new_asset_names = set()\n    folder_paths = set()\n    for name in asset_names:\n        if \"/\" in name:\n            folder_paths.add(name)\n        else:\n            new_asset_names.add(name)\n\n    yielded_ids = set()\n    if folder_paths:\n        for folder in _folders_query(\n            project_name, con, fields, folder_paths=folder_paths, **kwargs\n        ):\n            yielded_ids.add(folder[\"id\"])\n            yield convert_v4_folder_to_v3(folder, project_name)\n\n    if not new_asset_names:\n        return\n\n    for folder in _folders_query(\n        project_name, con, fields, folder_names=new_asset_names, **kwargs\n    ):\n        if folder[\"id\"] not in yielded_ids:\n            yielded_ids.add(folder[\"id\"])\n            yield convert_v4_folder_to_v3(folder, project_name)\n\n\ndef get_archived_assets(\n    project_name,\n    asset_ids=None,\n    asset_names=None,\n    parent_ids=None,\n    fields=None\n):\n    return get_assets(\n        project_name,\n        asset_ids,\n        asset_names,\n        parent_ids,\n        True,\n        fields\n    )\n\n\ndef get_asset_ids_with_subsets(project_name, asset_ids=None):\n    con = get_ayon_server_api_connection()\n    return con.get_folder_ids_with_products(project_name, asset_ids)\n\n\ndef get_subset_by_id(project_name, subset_id, fields=None):\n    subsets = get_subsets(\n        project_name, subset_ids=[subset_id], fields=fields\n    )\n    for subset in subsets:\n        return subset\n    return None\n\n\ndef get_subset_by_name(project_name, subset_name, asset_id, fields=None):\n    subsets = get_subsets(\n        project_name,\n        subset_names=[subset_name],\n        asset_ids=[asset_id],\n        fields=fields\n    )\n    for subset in subsets:\n        return subset\n    return None\n\n\ndef get_subsets(\n    project_name,\n    subset_ids=None,\n    subset_names=None,\n    asset_ids=None,\n    names_by_asset_ids=None,\n    archived=False,\n    fields=None\n):\n    return _get_subsets(\n        project_name,\n        subset_ids,\n        subset_names,\n        asset_ids,\n        names_by_asset_ids,\n        archived,\n        fields=fields\n    )\n\n\ndef get_subset_families(project_name, subset_ids=None):\n    con = get_ayon_server_api_connection()\n    return con.get_product_type_names(project_name, subset_ids)\n\n\ndef get_version_by_id(project_name, version_id, fields=None):\n    versions = get_versions(\n        project_name,\n        version_ids=[version_id],\n        fields=fields,\n        hero=True\n    )\n    for version in versions:\n        return version\n    return None\n\n\ndef get_version_by_name(project_name, version, subset_id, fields=None):\n    versions = get_versions(\n        project_name,\n        subset_ids=[subset_id],\n        versions=[version],\n        fields=fields\n    )\n    for version in versions:\n        return version\n    return None\n\n\ndef get_versions(\n    project_name,\n    version_ids=None,\n    subset_ids=None,\n    versions=None,\n    hero=False,\n    fields=None\n):\n    return _get_versions(\n        project_name,\n        version_ids,\n        subset_ids,\n        versions,\n        hero=hero,\n        standard=True,\n        fields=fields\n    )\n\n\ndef get_hero_version_by_id(project_name, version_id, fields=None):\n    versions = get_hero_versions(\n        project_name,\n        version_ids=[version_id],\n        fields=fields\n    )\n    for version in versions:\n        return version\n    return None\n\n\ndef get_hero_version_by_subset_id(\n    project_name, subset_id, fields=None\n):\n    versions = get_hero_versions(\n        project_name,\n        subset_ids=[subset_id],\n        fields=fields\n    )\n    for version in versions:\n        return version\n    return None\n\n\ndef get_hero_versions(\n    project_name, subset_ids=None, version_ids=None, fields=None\n):\n    return _get_versions(\n        project_name,\n        version_ids=version_ids,\n        subset_ids=subset_ids,\n        hero=True,\n        standard=False,\n        fields=fields\n    )\n\n\ndef get_last_versions(project_name, subset_ids, active=None, fields=None):\n    if fields:\n        fields = set(fields)\n        fields.add(\"parent\")\n\n    versions = _get_versions(\n        project_name,\n        subset_ids=subset_ids,\n        latest=True,\n        hero=False,\n        active=active,\n        fields=fields\n    )\n    return {\n        version[\"parent\"]: version\n        for version in versions\n    }\n\n\ndef get_last_version_by_subset_id(project_name, subset_id, fields=None):\n    versions = _get_versions(\n        project_name,\n        subset_ids=[subset_id],\n        latest=True,\n        hero=False,\n        fields=fields\n    )\n    if not versions:\n        return None\n    return versions[0]\n\n\ndef get_last_version_by_subset_name(\n    project_name,\n    subset_name,\n    asset_id=None,\n    asset_name=None,\n    fields=None\n):\n    if not asset_id and not asset_name:\n        return None\n\n    if not asset_id:\n        asset = get_asset_by_name(\n            project_name, asset_name, fields=[\"_id\"]\n        )\n        if not asset:\n            return None\n        asset_id = asset[\"_id\"]\n\n    subset = get_subset_by_name(\n        project_name, subset_name, asset_id, fields=[\"_id\"]\n    )\n    if not subset:\n        return None\n    return get_last_version_by_subset_id(\n        project_name, subset[\"_id\"], fields=fields\n    )\n\n\ndef get_output_link_versions(project_name, version_id, fields=None):\n    if not version_id:\n        return []\n\n    con = get_ayon_server_api_connection()\n    version_links = con.get_version_links(\n        project_name, version_id, link_direction=\"out\")\n\n    version_ids = {\n        link[\"entityId\"]\n        for link in version_links\n        if link[\"entityType\"] == \"version\"\n    }\n    if not version_ids:\n        return []\n\n    return get_versions(project_name, version_ids=version_ids, fields=fields)\n\n\ndef version_is_latest(project_name, version_id):\n    con = get_ayon_server_api_connection()\n    return con.version_is_latest(project_name, version_id)\n\n\ndef get_representation_by_id(project_name, representation_id, fields=None):\n    representations = get_representations(\n        project_name,\n        representation_ids=[representation_id],\n        fields=fields\n    )\n    for representation in representations:\n        return representation\n    return None\n\n\ndef get_representation_by_name(\n    project_name, representation_name, version_id, fields=None\n):\n    representations = get_representations(\n        project_name,\n        representation_names=[representation_name],\n        version_ids=[version_id],\n        fields=fields\n    )\n    for representation in representations:\n        return representation\n    return None\n\n\ndef get_representations(\n    project_name,\n    representation_ids=None,\n    representation_names=None,\n    version_ids=None,\n    context_filters=None,\n    names_by_version_ids=None,\n    archived=False,\n    standard=True,\n    fields=None\n):\n    if context_filters is not None:\n        # TODO should we add the support?\n        # - there was ability to fitler using regex\n        raise ValueError(\"OP v4 can't filter by representation context.\")\n\n    if not archived and not standard:\n        return\n\n    if archived and not standard:\n        active = False\n    elif not archived and standard:\n        active = True\n    else:\n        active = None\n\n    con = get_ayon_server_api_connection()\n    fields = representation_fields_v3_to_v4(fields, con)\n    if fields and active is not None:\n        fields.add(\"active\")\n\n    representations = con.get_representations(\n        project_name,\n        representation_ids=representation_ids,\n        representation_names=representation_names,\n        version_ids=version_ids,\n        names_by_version_ids=names_by_version_ids,\n        active=active,\n        fields=fields\n    )\n    for representation in representations:\n        yield convert_v4_representation_to_v3(representation)\n\n\ndef get_representation_parents(project_name, representation):\n    if not representation:\n        return None\n\n    repre_id = representation[\"_id\"]\n    parents_by_repre_id = get_representations_parents(\n        project_name, [representation]\n    )\n    return parents_by_repre_id[repre_id]\n\n\ndef get_representations_parents(project_name, representations):\n    repre_ids = {\n        repre[\"_id\"]\n        for repre in representations\n    }\n    con = get_ayon_server_api_connection()\n    parents_by_repre_id = con.get_representations_parents(project_name,\n                                                          repre_ids)\n    folder_ids = set()\n    for parents in parents_by_repre_id .values():\n        folder_ids.add(parents[2][\"id\"])\n\n    tasks_by_folder_id = {}\n\n    new_parents = {}\n    for repre_id, parents in parents_by_repre_id .items():\n        version, subset, folder, project = parents\n        folder_tasks = tasks_by_folder_id.get(folder[\"id\"]) or {}\n        folder[\"tasks\"] = folder_tasks\n        new_parents[repre_id] = (\n            convert_v4_version_to_v3(version),\n            convert_v4_subset_to_v3(subset),\n            convert_v4_folder_to_v3(folder, project_name),\n            project\n        )\n    return new_parents\n\n\ndef get_archived_representations(\n    project_name,\n    representation_ids=None,\n    representation_names=None,\n    version_ids=None,\n    context_filters=None,\n    names_by_version_ids=None,\n    fields=None\n):\n    return get_representations(\n        project_name,\n        representation_ids=representation_ids,\n        representation_names=representation_names,\n        version_ids=version_ids,\n        context_filters=context_filters,\n        names_by_version_ids=names_by_version_ids,\n        archived=True,\n        standard=False,\n        fields=fields\n    )\n\n\ndef get_thumbnail(\n    project_name, thumbnail_id, entity_type, entity_id, fields=None\n):\n    \"\"\"Receive thumbnail entity data.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        thumbnail_id (Union[str, ObjectId]): Id of thumbnail entity.\n        entity_type (str): Type of entity for which the thumbnail should be\n            received.\n        entity_id (str): Id of entity for which the thumbnail should be\n            received.\n        fields (Iterable[str]): Fields that should be returned. All fields are\n            returned if 'None' is passed.\n\n    Returns:\n        None: If thumbnail with specified id was not found.\n        Dict: Thumbnail entity data which can be reduced to specified 'fields'.\n    \"\"\"\n\n    if not thumbnail_id or not entity_type or not entity_id:\n        return None\n\n    if entity_type == \"asset\":\n        entity_type = \"folder\"\n\n    elif entity_type == \"hero_version\":\n        entity_type = \"version\"\n\n    return {\n        \"_id\": thumbnail_id,\n        \"type\": \"thumbnail\",\n        \"schema\": CURRENT_THUMBNAIL_SCHEMA,\n        \"data\": {\n            \"entity_type\": entity_type,\n            \"entity_id\": entity_id\n        }\n    }\n\n\ndef get_thumbnails(project_name, thumbnail_contexts, fields=None):\n    \"\"\"Get thumbnail entities.\n\n    Warning:\n        This function is not OpenPype compatible. There is none usage of this\n            function in codebase so there is nothing to convert. The previous\n            implementation cannot be AYON compatible without entity types.\n    \"\"\"\n\n    thumbnail_items = set()\n    for thumbnail_context in thumbnail_contexts:\n        thumbnail_id, entity_type, entity_id = thumbnail_context\n        thumbnail_item = get_thumbnail(\n            project_name, thumbnail_id, entity_type, entity_id\n        )\n        if thumbnail_item:\n            thumbnail_items.add(thumbnail_item)\n    return list(thumbnail_items)\n\n\ndef get_thumbnail_id_from_source(project_name, src_type, src_id):\n    \"\"\"Receive thumbnail id from source entity.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        src_type (str): Type of source entity ('asset', 'version').\n        src_id (Union[str, ObjectId]): Id of source entity.\n\n    Returns:\n        ObjectId: Thumbnail id assigned to entity.\n        None: If Source entity does not have any thumbnail id assigned.\n    \"\"\"\n\n    if not src_type or not src_id:\n        return None\n\n    if src_type == \"version\":\n        version = get_version_by_id(\n            project_name, src_id, fields=[\"data.thumbnail_id\"]\n        ) or {}\n        return version.get(\"data\", {}).get(\"thumbnail_id\")\n\n    if src_type == \"asset\":\n        asset = get_asset_by_id(\n            project_name, src_id, fields=[\"data.thumbnail_id\"]\n        ) or {}\n        return asset.get(\"data\", {}).get(\"thumbnail_id\")\n\n    return None\n\n\ndef get_workfile_info(\n    project_name, asset_id, task_name, filename, fields=None\n):\n    if not asset_id or not task_name or not filename:\n        return None\n\n    con = get_ayon_server_api_connection()\n    task = con.get_task_by_name(\n        project_name, asset_id, task_name, fields=[\"id\", \"name\", \"folderId\"]\n    )\n    if not task:\n        return None\n\n    fields = workfile_info_fields_v3_to_v4(fields)\n\n    for workfile_info in con.get_workfiles_info(\n        project_name, task_ids=[task[\"id\"]], fields=fields\n    ):\n        if workfile_info[\"name\"] == filename:\n            return convert_v4_workfile_info_to_v3(workfile_info, task)\n    return None\n"
  },
  {
    "path": "openpype/client/server/entity_links.py",
    "content": "from .utils import get_ayon_server_api_connection\nfrom .entities import get_assets, get_representation_by_id\n\n\ndef get_linked_asset_ids(project_name, asset_doc=None, asset_id=None):\n    \"\"\"Extract linked asset ids from asset document.\n\n    One of asset document or asset id must be passed.\n\n    Note:\n        Asset links now works only from asset to assets.\n\n    Args:\n        project_name (str): Project where to look for asset.\n        asset_doc (dict): Asset document from DB.\n        asset_id (str): Asset id to find its document.\n\n    Returns:\n        List[Union[ObjectId, str]]: Asset ids of input links.\n    \"\"\"\n\n    output = []\n    if not asset_doc and not asset_id:\n        return output\n\n    if not asset_id:\n        asset_id = asset_doc[\"_id\"]\n\n    con = get_ayon_server_api_connection()\n    links = con.get_folder_links(project_name, asset_id, link_direction=\"in\")\n    return [\n        link[\"entityId\"]\n        for link in links\n        if link[\"entityType\"] == \"folder\"\n    ]\n\n\ndef get_linked_assets(\n    project_name, asset_doc=None, asset_id=None, fields=None\n):\n    \"\"\"Return linked assets based on passed asset document.\n\n    One of asset document or asset id must be passed.\n\n    Args:\n        project_name (str): Name of project where to look for queried entities.\n        asset_doc (Dict[str, Any]): Asset document from database.\n        asset_id (Union[ObjectId, str]): Asset id. Can be used instead of\n            asset document.\n        fields (Iterable[str]): Fields that should be returned. All fields are\n            returned if 'None' is passed.\n\n    Returns:\n        List[Dict[str, Any]]: Asset documents of input links for passed\n            asset doc.\n    \"\"\"\n\n    link_ids = get_linked_asset_ids(project_name, asset_doc, asset_id)\n    if not link_ids:\n        return []\n    return list(get_assets(project_name, asset_ids=link_ids, fields=fields))\n\n\n\ndef get_linked_representation_id(\n    project_name, repre_doc=None, repre_id=None, link_type=None, max_depth=None\n):\n    \"\"\"Returns list of linked ids of particular type (if provided).\n\n    One of representation document or representation id must be passed.\n    Note:\n        Representation links now works only from representation through version\n            back to representations.\n\n    Todos:\n        Missing depth query. Not sure how it did find more representations in\n            depth, probably links to version?\n\n    Args:\n        project_name (str): Name of project where look for links.\n        repre_doc (Dict[str, Any]): Representation document.\n        repre_id (Union[ObjectId, str]): Representation id.\n        link_type (str): Type of link (e.g. 'reference', ...).\n        max_depth (int): Limit recursion level. Default: 0\n\n    Returns:\n        List[ObjectId] Linked representation ids.\n    \"\"\"\n\n    if repre_doc:\n        repre_id = repre_doc[\"_id\"]\n\n    if not repre_id and not repre_doc:\n        return []\n\n    version_id = None\n    if repre_doc:\n        version_id = repre_doc.get(\"parent\")\n\n    if not version_id:\n        repre_doc = get_representation_by_id(\n            project_name, repre_id, fields=[\"parent\"]\n        )\n        if repre_doc:\n            version_id = repre_doc[\"parent\"]\n\n    if not version_id:\n        return []\n\n    if max_depth is None or max_depth == 0:\n        max_depth = 1\n\n    link_types = None\n    if link_type:\n        link_types = [link_type]\n\n    con = get_ayon_server_api_connection()\n    # Store already found version ids to avoid recursion, and also to store\n    #   output -> Don't forget to remove 'version_id' at the end!!!\n    linked_version_ids = {version_id}\n    # Each loop of depth will reset this variable\n    versions_to_check = {version_id}\n    for _ in range(max_depth):\n        if not versions_to_check:\n            break\n\n        versions_links = con.get_versions_links(\n            project_name,\n            versions_to_check,\n            link_types=link_types,\n            link_direction=\"out\")\n\n        versions_to_check = set()\n        for links in versions_links.values():\n            for link in links:\n                # Care only about version links\n                if link[\"entityType\"] != \"version\":\n                    continue\n                entity_id = link[\"entityId\"]\n                # Skip already found linked version ids\n                if entity_id in linked_version_ids:\n                    continue\n                linked_version_ids.add(entity_id)\n                versions_to_check.add(entity_id)\n\n    linked_version_ids.remove(version_id)\n    if not linked_version_ids:\n        return []\n    con = get_ayon_server_api_connection()\n    representations = con.get_representations(\n        project_name,\n        version_ids=linked_version_ids,\n        fields=[\"id\"])\n    return [\n        repre[\"id\"]\n        for repre in representations\n    ]\n"
  },
  {
    "path": "openpype/client/server/openpype_comp.py",
    "content": "import collections\nimport json\n\nimport six\nfrom ayon_api.graphql import GraphQlQuery, FIELD_VALUE, fields_to_dict\n\nfrom .constants import DEFAULT_FOLDER_FIELDS\n\n\ndef folders_tasks_graphql_query(fields):\n    query = GraphQlQuery(\"FoldersQuery\")\n    project_name_var = query.add_variable(\"projectName\", \"String!\")\n    folder_ids_var = query.add_variable(\"folderIds\", \"[String!]\")\n    parent_folder_ids_var = query.add_variable(\"parentFolderIds\", \"[String!]\")\n    folder_paths_var = query.add_variable(\"folderPaths\", \"[String!]\")\n    folder_names_var = query.add_variable(\"folderNames\", \"[String!]\")\n    has_products_var = query.add_variable(\"folderHasProducts\", \"Boolean!\")\n\n    project_field = query.add_field(\"project\")\n    project_field.set_filter(\"name\", project_name_var)\n\n    folders_field = project_field.add_field_with_edges(\"folders\")\n    folders_field.set_filter(\"ids\", folder_ids_var)\n    folders_field.set_filter(\"parentIds\", parent_folder_ids_var)\n    folders_field.set_filter(\"names\", folder_names_var)\n    folders_field.set_filter(\"paths\", folder_paths_var)\n    folders_field.set_filter(\"hasProducts\", has_products_var)\n\n    fields = set(fields)\n    fields.discard(\"tasks\")\n    tasks_field = folders_field.add_field_with_edges(\"tasks\")\n    tasks_field.add_field(\"name\")\n    tasks_field.add_field(\"taskType\")\n\n    nested_fields = fields_to_dict(fields)\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, folders_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n\n\ndef get_folders_with_tasks(\n    con,\n    project_name,\n    folder_ids=None,\n    folder_paths=None,\n    folder_names=None,\n    parent_ids=None,\n    active=True,\n    fields=None\n):\n    \"\"\"Query folders with tasks from server.\n\n    This is for v4 compatibility where tasks were stored on assets. This is\n    an inefficient way how folders and tasks are queried so it was added only\n    as compatibility function.\n\n    Todos:\n        Folder name won't be unique identifier, so we should add folder path\n            filtering.\n\n    Notes:\n        Filter 'active' don't have direct filter in GraphQl.\n\n    Args:\n        con (ServerAPI): Connection to server.\n        project_name (str): Name of project where folders are.\n        folder_ids (Iterable[str]): Folder ids to filter.\n        folder_paths (Iterable[str]): Folder paths used for filtering.\n        folder_names (Iterable[str]): Folder names used for filtering.\n        parent_ids (Iterable[str]): Ids of folder parents. Use 'None'\n            if folder is direct child of project.\n        active (Union[bool, None]): Filter active/inactive folders. Both\n            are returned if is set to None.\n        fields (Union[Iterable(str), None]): Fields to be queried\n            for folder. All possible folder fields are returned if 'None'\n            is passed.\n\n    Yields:\n        Dict[str, Any]: Queried folder entities.\n    \"\"\"\n\n    if not project_name:\n        return\n\n    filters = {\n        \"projectName\": project_name\n    }\n    if folder_ids is not None:\n        folder_ids = set(folder_ids)\n        if not folder_ids:\n            return\n        filters[\"folderIds\"] = list(folder_ids)\n\n    if folder_paths is not None:\n        folder_paths = set(folder_paths)\n        if not folder_paths:\n            return\n        filters[\"folderPaths\"] = list(folder_paths)\n\n    if folder_names is not None:\n        folder_names = set(folder_names)\n        if not folder_names:\n            return\n        filters[\"folderNames\"] = list(folder_names)\n\n    if parent_ids is not None:\n        parent_ids = set(parent_ids)\n        if not parent_ids:\n            return\n        if None in parent_ids:\n            # Replace 'None' with '\"root\"' which is used during GraphQl\n            #   query for parent ids filter for folders without folder\n            #   parent\n            parent_ids.remove(None)\n            parent_ids.add(\"root\")\n\n        if project_name in parent_ids:\n            # Replace project name with '\"root\"' which is used during\n            #   GraphQl query for parent ids filter for folders without\n            #   folder parent\n            parent_ids.remove(project_name)\n            parent_ids.add(\"root\")\n\n        filters[\"parentFolderIds\"] = list(parent_ids)\n\n    if fields:\n        fields = set(fields)\n    else:\n        fields = con.get_default_fields_for_type(\"folder\")\n        fields |= DEFAULT_FOLDER_FIELDS\n\n    if active is not None:\n        fields.add(\"active\")\n\n    query = folders_tasks_graphql_query(fields)\n    for attr, filter_value in filters.items():\n        query.set_variable_value(attr, filter_value)\n\n    parsed_data = query.query(con)\n    folders = parsed_data[\"project\"][\"folders\"]\n    for folder in folders:\n        if active is not None and folder[\"active\"] is not active:\n            continue\n        folder_data = folder.get(\"data\")\n        if isinstance(folder_data, six.string_types):\n            folder[\"data\"] = json.loads(folder_data)\n        yield folder\n"
  },
  {
    "path": "openpype/client/server/operations.py",
    "content": "import copy\nimport json\nimport collections\nimport uuid\nimport datetime\n\nfrom bson.objectid import ObjectId\n\nfrom openpype.client.operations_base import (\n    REMOVED_VALUE,\n    CreateOperation,\n    UpdateOperation,\n    DeleteOperation,\n    BaseOperationsSession\n)\n\nfrom openpype.client.mongo.operations import (\n    CURRENT_THUMBNAIL_SCHEMA,\n    CURRENT_REPRESENTATION_SCHEMA,\n    CURRENT_HERO_VERSION_SCHEMA,\n    CURRENT_VERSION_SCHEMA,\n    CURRENT_SUBSET_SCHEMA,\n    CURRENT_ASSET_DOC_SCHEMA,\n    CURRENT_PROJECT_SCHEMA,\n)\n\nfrom .conversion_utils import (\n    convert_create_asset_to_v4,\n    convert_create_task_to_v4,\n    convert_create_subset_to_v4,\n    convert_create_version_to_v4,\n    convert_create_hero_version_to_v4,\n    convert_create_representation_to_v4,\n    convert_create_workfile_info_to_v4,\n\n    convert_update_folder_to_v4,\n    convert_update_subset_to_v4,\n    convert_update_version_to_v4,\n    convert_update_hero_version_to_v4,\n    convert_update_representation_to_v4,\n    convert_update_workfile_info_to_v4,\n)\nfrom .utils import create_entity_id, get_ayon_server_api_connection\n\n\ndef _create_or_convert_to_id(entity_id=None):\n    if entity_id is None:\n        return create_entity_id()\n\n    if isinstance(entity_id, ObjectId):\n        raise TypeError(\"Type of 'ObjectId' is not supported anymore.\")\n\n    # Validate if can be converted to uuid\n    uuid.UUID(entity_id)\n    return entity_id\n\n\ndef new_project_document(\n    project_name, project_code, config, data=None, entity_id=None\n):\n    \"\"\"Create skeleton data of project document.\n\n    Args:\n        project_name (str): Name of project. Used as identifier of a project.\n        project_code (str): Shorter version of projet without spaces and\n            special characters (in most of cases). Should be also considered\n            as unique name across projects.\n        config (Dic[str, Any]): Project config consist of roots, templates,\n            applications and other project Anatomy related data.\n        data (Dict[str, Any]): Project data with information about it's\n            attributes (e.g. 'fps' etc.) or integration specific keys.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of project document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n\n    data[\"code\"] = project_code\n\n    return {\n        \"_id\": _create_or_convert_to_id(entity_id),\n        \"name\": project_name,\n        \"type\": CURRENT_PROJECT_SCHEMA,\n        \"entity_data\": data,\n        \"config\": config\n    }\n\n\ndef new_asset_document(\n    name, project_id, parent_id, parents, data=None, entity_id=None\n):\n    \"\"\"Create skeleton data of asset document.\n\n    Args:\n        name (str): Is considered as unique identifier of asset in project.\n        project_id (Union[str, ObjectId]): Id of project doument.\n        parent_id (Union[str, ObjectId]): Id of parent asset.\n        parents (List[str]): List of parent assets names.\n        data (Dict[str, Any]): Asset document data. Empty dictionary is used\n            if not passed. Value of 'parent_id' is used to fill 'visualParent'.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of asset document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n    if parent_id is not None:\n        parent_id = _create_or_convert_to_id(parent_id)\n    data[\"visualParent\"] = parent_id\n    data[\"parents\"] = parents\n\n    return {\n        \"_id\": _create_or_convert_to_id(entity_id),\n        \"type\": \"asset\",\n        \"name\": name,\n        # This will be ignored\n        \"parent\": project_id,\n        \"data\": data,\n        \"schema\": CURRENT_ASSET_DOC_SCHEMA\n    }\n\n\ndef new_subset_document(name, family, asset_id, data=None, entity_id=None):\n    \"\"\"Create skeleton data of subset document.\n\n    Args:\n        name (str): Is considered as unique identifier of subset under asset.\n        family (str): Subset's family.\n        asset_id (Union[str, ObjectId]): Id of parent asset.\n        data (Dict[str, Any]): Subset document data. Empty dictionary is used\n            if not passed. Value of 'family' is used to fill 'family'.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of subset document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n    data[\"family\"] = family\n    return {\n        \"_id\": _create_or_convert_to_id(entity_id),\n        \"schema\": CURRENT_SUBSET_SCHEMA,\n        \"type\": \"subset\",\n        \"name\": name,\n        \"data\": data,\n        \"parent\": _create_or_convert_to_id(asset_id)\n    }\n\n\ndef new_version_doc(version, subset_id, data=None, entity_id=None):\n    \"\"\"Create skeleton data of version document.\n\n    Args:\n        version (int): Is considered as unique identifier of version\n            under subset.\n        subset_id (Union[str, ObjectId]): Id of parent subset.\n        data (Dict[str, Any]): Version document data.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of version document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n\n    return {\n        \"_id\": _create_or_convert_to_id(entity_id),\n        \"schema\": CURRENT_VERSION_SCHEMA,\n        \"type\": \"version\",\n        \"name\": int(version),\n        \"parent\": _create_or_convert_to_id(subset_id),\n        \"data\": data\n    }\n\n\ndef new_hero_version_doc(subset_id, data, version=None, entity_id=None):\n    \"\"\"Create skeleton data of hero version document.\n\n    Args:\n        subset_id (Union[str, ObjectId]): Id of parent subset.\n        data (Dict[str, Any]): Version document data.\n        version (int): Version of source version.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of version document.\n    \"\"\"\n\n    if version is None:\n        version = -1\n    elif version > 0:\n        version = -version\n\n    return {\n        \"_id\": _create_or_convert_to_id(entity_id),\n        \"schema\": CURRENT_HERO_VERSION_SCHEMA,\n        \"type\": \"hero_version\",\n        \"version\": version,\n        \"parent\": _create_or_convert_to_id(subset_id),\n        \"data\": data\n    }\n\n\ndef new_representation_doc(\n    name, version_id, context, data=None, entity_id=None\n):\n    \"\"\"Create skeleton data of representation document.\n\n    Args:\n        name (str): Representation name considered as unique identifier\n            of representation under version.\n        version_id (Union[str, ObjectId]): Id of parent version.\n        context (Dict[str, Any]): Representation context used for fill template\n            of to query.\n        data (Dict[str, Any]): Representation document data.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of version document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n\n    return {\n        \"_id\": _create_or_convert_to_id(entity_id),\n        \"schema\": CURRENT_REPRESENTATION_SCHEMA,\n        \"type\": \"representation\",\n        \"parent\": _create_or_convert_to_id(version_id),\n        \"name\": name,\n        \"data\": data,\n\n        # Imprint shortcut to context for performance reasons.\n        \"context\": context\n    }\n\n\ndef new_thumbnail_doc(data=None, entity_id=None):\n    \"\"\"Create skeleton data of thumbnail document.\n\n    Args:\n        data (Dict[str, Any]): Thumbnail document data.\n        entity_id (Union[str, ObjectId]): Predefined id of document. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of thumbnail document.\n    \"\"\"\n\n    if data is None:\n        data = {}\n\n    return {\n        \"_id\": _create_or_convert_to_id(entity_id),\n        \"type\": \"thumbnail\",\n        \"schema\": CURRENT_THUMBNAIL_SCHEMA,\n        \"data\": data\n    }\n\n\ndef new_workfile_info_doc(\n    filename, asset_id, task_name, files, data=None, entity_id=None\n):\n    \"\"\"Create skeleton data of workfile info document.\n\n    Workfile document is at this moment used primarily for artist notes.\n\n    Args:\n        filename (str): Filename of workfile.\n        asset_id (Union[str, ObjectId]): Id of asset under which workfile live.\n        task_name (str): Task under which was workfile created.\n        files (List[str]): List of rootless filepaths related to workfile.\n        data (Dict[str, Any]): Additional metadata.\n\n    Returns:\n        Dict[str, Any]: Skeleton of workfile info document.\n    \"\"\"\n\n    if not data:\n        data = {}\n\n    return {\n        \"_id\": _create_or_convert_to_id(entity_id),\n        \"type\": \"workfile\",\n        \"parent\": _create_or_convert_to_id(asset_id),\n        \"task_name\": task_name,\n        \"filename\": filename,\n        \"data\": data,\n        \"files\": files\n    }\n\n\ndef _prepare_update_data(old_doc, new_doc, replace):\n    changes = {}\n    for key, value in new_doc.items():\n        if key not in old_doc or value != old_doc[key]:\n            changes[key] = value\n\n    if replace:\n        for key in old_doc.keys():\n            if key not in new_doc:\n                changes[key] = REMOVED_VALUE\n    return changes\n\n\ndef prepare_subset_update_data(old_doc, new_doc, replace=True):\n    \"\"\"Compare two subset documents and prepare update data.\n\n    Based on compared values will create update data for\n    'MongoUpdateOperation'.\n\n    Empty output means that documents are identical.\n\n    Returns:\n        Dict[str, Any]: Changes between old and new document.\n    \"\"\"\n\n    return _prepare_update_data(old_doc, new_doc, replace)\n\n\ndef prepare_version_update_data(old_doc, new_doc, replace=True):\n    \"\"\"Compare two version documents and prepare update data.\n\n    Based on compared values will create update data for\n    'MongoUpdateOperation'.\n\n    Empty output means that documents are identical.\n\n    Returns:\n        Dict[str, Any]: Changes between old and new document.\n    \"\"\"\n\n    return _prepare_update_data(old_doc, new_doc, replace)\n\n\ndef prepare_hero_version_update_data(old_doc, new_doc, replace=True):\n    \"\"\"Compare two hero version documents and prepare update data.\n\n    Based on compared values will create update data for 'UpdateOperation'.\n\n    Empty output means that documents are identical.\n\n    Returns:\n        Dict[str, Any]: Changes between old and new document.\n    \"\"\"\n\n    changes = _prepare_update_data(old_doc, new_doc, replace)\n    changes.pop(\"version_id\", None)\n    return changes\n\n\ndef prepare_representation_update_data(old_doc, new_doc, replace=True):\n    \"\"\"Compare two representation documents and prepare update data.\n\n    Based on compared values will create update data for\n    'MongoUpdateOperation'.\n\n    Empty output means that documents are identical.\n\n    Returns:\n        Dict[str, Any]: Changes between old and new document.\n    \"\"\"\n\n    changes = _prepare_update_data(old_doc, new_doc, replace)\n    context = changes.get(\"data\", {}).get(\"context\")\n    # Make sure that both 'family' and 'subset' are in changes if\n    #   one of them changed (they'll both become 'product').\n    if (\n        context\n        and (\"family\" in context or \"subset\" in context)\n    ):\n        context[\"family\"] = new_doc[\"data\"][\"context\"][\"family\"]\n        context[\"subset\"] = new_doc[\"data\"][\"context\"][\"subset\"]\n\n    return changes\n\n\ndef prepare_workfile_info_update_data(old_doc, new_doc, replace=True):\n    \"\"\"Compare two workfile info documents and prepare update data.\n\n    Based on compared values will create update data for\n    'MongoUpdateOperation'.\n\n    Empty output means that documents are identical.\n\n    Returns:\n        Dict[str, Any]: Changes between old and new document.\n    \"\"\"\n\n    return _prepare_update_data(old_doc, new_doc, replace)\n\n\nclass FailedOperations(Exception):\n    pass\n\n\ndef entity_data_json_default(value):\n    if isinstance(value, datetime.datetime):\n        return int(value.timestamp())\n\n    raise TypeError(\n        \"Object of type {} is not JSON serializable\".format(str(type(value)))\n    )\n\n\ndef failed_json_default(value):\n    return \"< Failed value {} > {}\".format(type(value), str(value))\n\n\nclass ServerCreateOperation(CreateOperation):\n    \"\"\"Operation to create an entity.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'asset', 'representation' etc.\n        data (Dict[str, Any]): Data of entity that will be created.\n    \"\"\"\n\n    def __init__(self, project_name, entity_type, data, session):\n        self._session = session\n\n        if not data:\n            data = {}\n        data = copy.deepcopy(data)\n        if entity_type == \"project\":\n            raise ValueError(\"Project cannot be created using operations\")\n\n        tasks = None\n        if entity_type in \"asset\":\n            # TODO handle tasks\n            entity_type = \"folder\"\n            if \"data\" in data:\n                tasks = data[\"data\"].get(\"tasks\")\n\n            project = self._session.get_project(project_name)\n            new_data = convert_create_asset_to_v4(data, project, self.con)\n\n        elif entity_type == \"task\":\n            project = self._session.get_project(project_name)\n            new_data = convert_create_task_to_v4(data, project, self.con)\n\n        elif entity_type == \"subset\":\n            new_data = convert_create_subset_to_v4(data, self.con)\n            entity_type = \"product\"\n\n        elif entity_type == \"version\":\n            new_data = convert_create_version_to_v4(data, self.con)\n\n        elif entity_type == \"hero_version\":\n            new_data = convert_create_hero_version_to_v4(\n                data, project_name, self.con\n            )\n            entity_type = \"version\"\n\n        elif entity_type in (\"representation\", \"archived_representation\"):\n            new_data = convert_create_representation_to_v4(data, self.con)\n            entity_type = \"representation\"\n\n        elif entity_type == \"workfile\":\n            new_data = convert_create_workfile_info_to_v4(\n                data, project_name, self.con\n            )\n\n        else:\n            raise ValueError(\n                \"Unhandled entity type \\\"{}\\\"\".format(entity_type)\n            )\n\n        # Simple check if data can be dumped into json\n        #   - should raise error on 'ObjectId' object\n        try:\n            new_data = json.loads(\n                json.dumps(new_data, default=entity_data_json_default)\n            )\n\n        except:\n            raise ValueError(\"Couldn't json parse body: {}\".format(\n                json.dumps(new_data, default=failed_json_default)\n            ))\n\n        super(ServerCreateOperation, self).__init__(\n            project_name, entity_type, new_data\n        )\n\n        if \"id\" not in self._data:\n            self._data[\"id\"] = create_entity_id()\n\n        if tasks:\n            copied_tasks = copy.deepcopy(tasks)\n            for task_name, task in copied_tasks.items():\n                task[\"name\"] = task_name\n                task[\"folderId\"] = self._data[\"id\"]\n                self.session.create_entity(\n                    project_name, \"task\", task, nested_id=self.id\n                )\n\n    @property\n    def con(self):\n        return self.session.con\n\n    @property\n    def session(self):\n        return self._session\n\n    @property\n    def entity_id(self):\n        return self._data[\"id\"]\n\n    def to_server_operation(self):\n        return {\n            \"id\": self.id,\n            \"type\": \"create\",\n            \"entityType\": self.entity_type,\n            \"entityId\": self.entity_id,\n            \"data\": self._data\n        }\n\n\nclass ServerUpdateOperation(UpdateOperation):\n    \"\"\"Operation to update an entity.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'asset', 'representation' etc.\n        entity_id (Union[str, ObjectId]): Identifier of an entity.\n        update_data (Dict[str, Any]): Key -> value changes that will be set in\n            database. If value is set to 'REMOVED_VALUE' the key will be\n            removed. Only first level of dictionary is checked (on purpose).\n    \"\"\"\n\n    def __init__(\n        self, project_name, entity_type, entity_id, update_data, session\n    ):\n        self._session = session\n\n        update_data = copy.deepcopy(update_data)\n        if entity_type == \"project\":\n            raise ValueError(\"Project cannot be created using operations\")\n\n        if entity_type in (\"asset\", \"archived_asset\"):\n            new_update_data = convert_update_folder_to_v4(\n                project_name, entity_id, update_data, self.con\n            )\n            entity_type = \"folder\"\n\n        elif entity_type == \"subset\":\n            new_update_data = convert_update_subset_to_v4(\n                project_name, entity_id, update_data, self.con\n            )\n            entity_type = \"product\"\n\n        elif entity_type == \"version\":\n            new_update_data = convert_update_version_to_v4(\n                project_name, entity_id, update_data, self.con\n            )\n\n        elif entity_type == \"hero_version\":\n            new_update_data = convert_update_hero_version_to_v4(\n                project_name, entity_id, update_data, self.con\n            )\n            entity_type = \"version\"\n\n        elif entity_type in (\"representation\", \"archived_representation\"):\n            new_update_data = convert_update_representation_to_v4(\n                project_name, entity_id, update_data, self.con\n            )\n            entity_type = \"representation\"\n\n        elif entity_type == \"workfile\":\n            new_update_data = convert_update_workfile_info_to_v4(\n                project_name, entity_id, update_data, self.con\n            )\n\n        else:\n            raise ValueError(\n                \"Unhandled entity type \\\"{}\\\"\".format(entity_type)\n            )\n\n        try:\n            new_update_data = json.loads(\n                json.dumps(new_update_data, default=entity_data_json_default)\n            )\n\n        except:\n            raise ValueError(\"Couldn't json parse body: {}\".format(\n                json.dumps(new_update_data, default=failed_json_default)\n            ))\n\n        super(ServerUpdateOperation, self).__init__(\n            project_name, entity_type, entity_id, new_update_data\n        )\n\n    @property\n    def con(self):\n        return self.session.con\n\n    @property\n    def session(self):\n        return self._session\n\n    def to_server_operation(self):\n        if not self._update_data:\n            return None\n\n        update_data = {}\n        for key, value in self._update_data.items():\n            if value is REMOVED_VALUE:\n                value = None\n            update_data[key] = value\n\n        return {\n            \"id\": self.id,\n            \"type\": \"update\",\n            \"entityType\": self.entity_type,\n            \"entityId\": self.entity_id,\n            \"data\": update_data\n        }\n\n\nclass ServerDeleteOperation(DeleteOperation):\n    \"\"\"Operation to delete an entity.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'asset', 'representation' etc.\n        entity_id (Union[str, ObjectId]): Entity id that will be removed.\n    \"\"\"\n\n    def __init__(self, project_name, entity_type, entity_id, session):\n        self._session = session\n\n        if entity_type == \"asset\":\n            entity_type = \"folder\"\n\n        elif entity_type == \"hero_version\":\n            entity_type = \"version\"\n\n        elif entity_type == \"subset\":\n            entity_type = \"product\"\n\n        super(ServerDeleteOperation, self).__init__(\n            project_name, entity_type, entity_id\n        )\n\n    @property\n    def con(self):\n        return self.session.con\n\n    @property\n    def session(self):\n        return self._session\n\n    def to_server_operation(self):\n        return {\n            \"id\": self.id,\n            \"type\": self.operation_name,\n            \"entityId\": self.entity_id,\n            \"entityType\": self.entity_type,\n        }\n\n\nclass OperationsSession(BaseOperationsSession):\n    def __init__(self, con=None, *args, **kwargs):\n        super(OperationsSession, self).__init__(*args, **kwargs)\n        if con is None:\n            con = get_ayon_server_api_connection()\n        self._con = con\n        self._project_cache = {}\n        self._nested_operations = collections.defaultdict(list)\n\n    @property\n    def con(self):\n        return self._con\n\n    def get_project(self, project_name):\n        if project_name not in self._project_cache:\n            self._project_cache[project_name] = self.con.get_project(\n                project_name)\n        return copy.deepcopy(self._project_cache[project_name])\n\n    def commit(self):\n        \"\"\"Commit session operations.\"\"\"\n\n        operations, self._operations = self._operations, []\n        if not operations:\n            return\n\n        operations_by_project = collections.defaultdict(list)\n        for operation in operations:\n            operations_by_project[operation.project_name].append(operation)\n\n        body_by_id = {}\n        results = []\n        for project_name, operations in operations_by_project.items():\n            operations_body = []\n            for operation in operations:\n                body = operation.to_server_operation()\n                if body is not None:\n                    try:\n                        json.dumps(body)\n                    except:\n                        raise ValueError(\"Couldn't json parse body: {}\".format(\n                            json.dumps(\n                                body, indent=4, default=failed_json_default\n                            )\n                        ))\n\n                    body_by_id[operation.id] = body\n                    operations_body.append(body)\n\n            if operations_body:\n                result = self._con.post(\n                    \"projects/{}/operations\".format(project_name),\n                    operations=operations_body,\n                    canFail=False\n                )\n                results.append(result.data)\n\n        for result in results:\n            if result.get(\"success\"):\n                continue\n\n            if \"operations\" not in result:\n                raise FailedOperations(\n                    \"Operation failed. Content: {}\".format(str(result))\n                )\n\n            for op_result in result[\"operations\"]:\n                if not op_result[\"success\"]:\n                    operation_id = op_result[\"id\"]\n                    raise FailedOperations((\n                        \"Operation \\\"{}\\\" failed with data:\\n{}\\nError: {}.\"\n                    ).format(\n                        operation_id,\n                        json.dumps(body_by_id[operation_id], indent=4),\n                        op_result.get(\"error\", \"unknown\"),\n                    ))\n\n    def create_entity(self, project_name, entity_type, data, nested_id=None):\n        \"\"\"Fast access to 'ServerCreateOperation'.\n\n        Args:\n            project_name (str): On which project the creation happens.\n            entity_type (str): Which entity type will be created.\n            data (Dicst[str, Any]): Entity data.\n            nested_id (str): Id of other operation from which is triggered\n                operation -> Operations can trigger suboperations but they\n                must be added to operations list after it's parent is added.\n\n        Returns:\n            ServerCreateOperation: Object of update operation.\n        \"\"\"\n\n        operation = ServerCreateOperation(\n            project_name, entity_type, data, self\n        )\n\n        if nested_id:\n            self._nested_operations[nested_id].append(operation)\n        else:\n            self.add(operation)\n            if operation.id in self._nested_operations:\n                self.extend(self._nested_operations.pop(operation.id))\n\n        return operation\n\n    def update_entity(\n        self, project_name, entity_type, entity_id, update_data, nested_id=None\n    ):\n        \"\"\"Fast access to 'ServerUpdateOperation'.\n\n        Returns:\n            ServerUpdateOperation: Object of update operation.\n        \"\"\"\n\n        operation = ServerUpdateOperation(\n            project_name, entity_type, entity_id, update_data, self\n        )\n        if nested_id:\n            self._nested_operations[nested_id].append(operation)\n        else:\n            self.add(operation)\n            if operation.id in self._nested_operations:\n                self.extend(self._nested_operations.pop(operation.id))\n        return operation\n\n    def delete_entity(\n        self, project_name, entity_type, entity_id, nested_id=None\n    ):\n        \"\"\"Fast access to 'ServerDeleteOperation'.\n\n        Returns:\n            ServerDeleteOperation: Object of delete operation.\n        \"\"\"\n\n        operation = ServerDeleteOperation(\n            project_name, entity_type, entity_id, self\n        )\n        if nested_id:\n            self._nested_operations[nested_id].append(operation)\n        else:\n            self.add(operation)\n            if operation.id in self._nested_operations:\n                self.extend(self._nested_operations.pop(operation.id))\n        return operation\n\n\ndef create_project(\n    project_name,\n    project_code,\n    library_project=False,\n    preset_name=None,\n    con=None\n):\n    \"\"\"Create project using OpenPype settings.\n\n    This project creation function is not validating project document on\n    creation. It is because project document is created blindly with only\n    minimum required information about project which is it's name, code, type\n    and schema.\n\n    Entered project name must be unique and project must not exist yet.\n\n    Note:\n        This function is here to be OP v4 ready but in v3 has more logic\n            to do. That's why inner imports are in the body.\n\n    Args:\n        project_name (str): New project name. Should be unique.\n        project_code (str): Project's code should be unique too.\n        library_project (bool): Project is library project.\n        preset_name (str): Name of anatomy preset. Default is used if not\n            passed.\n        con (ServerAPI): Connection to server with logged user.\n\n    Raises:\n        ValueError: When project name already exists in MongoDB.\n\n    Returns:\n        dict: Created project document.\n    \"\"\"\n\n    if con is None:\n        con = get_ayon_server_api_connection()\n\n    return con.create_project(\n        project_name,\n        project_code,\n        library_project,\n        preset_name\n    )\n\n\ndef delete_project(project_name, con=None):\n    if con is None:\n        con = get_ayon_server_api_connection()\n\n    return con.delete_project(project_name)\n\n\ndef create_thumbnail(project_name, src_filepath, thumbnail_id=None, con=None):\n    if con is None:\n        con = get_ayon_server_api_connection()\n    return con.create_thumbnail(project_name, src_filepath, thumbnail_id)\n"
  },
  {
    "path": "openpype/client/server/thumbnails.py",
    "content": "\"\"\"Cache of thumbnails downloaded from AYON server.\n\nThumbnails are cached to appdirs to predefined directory.\n\nThis should be moved to thumbnails logic in pipeline but because it would\noverflow OpenPype logic it's here for now.\n\"\"\"\n\nimport os\nimport time\nimport collections\n\nimport appdirs\n\nFileInfo = collections.namedtuple(\n    \"FileInfo\",\n    (\"path\", \"size\", \"modification_time\")\n)\n\n\nclass AYONThumbnailCache:\n    \"\"\"Cache of thumbnails on local storage.\n\n    Thumbnails are cached to appdirs to predefined directory. Each project has\n    own subfolder with thumbnails -> that's because each project has own\n    thumbnail id validation and file names are thumbnail ids with matching\n    extension. Extensions are predefined (.png and .jpeg).\n\n    Cache has cleanup mechanism which is triggered on initialized by default.\n\n    The cleanup has 2 levels:\n    1. soft cleanup which remove all files that are older then 'days_alive'\n    2. max size cleanup which remove all files until the thumbnails folder\n        contains less then 'max_filesize'\n        - this is time consuming so it's not triggered automatically\n\n    Args:\n        cleanup (bool): Trigger soft cleanup (Cleanup expired thumbnails).\n    \"\"\"\n\n    # Lifetime of thumbnails (in seconds)\n    # - default 3 days\n    days_alive = 3\n    # Max size of thumbnail directory (in bytes)\n    # - default 2 Gb\n    max_filesize = 2 * 1024 * 1024 * 1024\n\n    def __init__(self, cleanup=True):\n        self._thumbnails_dir = None\n        self._days_alive_secs = self.days_alive * 24 * 60 * 60\n        if cleanup:\n            self.cleanup()\n\n    def get_thumbnails_dir(self):\n        \"\"\"Root directory where thumbnails are stored.\n\n        Returns:\n            str: Path to thumbnails root.\n        \"\"\"\n\n        if self._thumbnails_dir is None:\n            # TODO use generic function\n            directory = appdirs.user_data_dir(\"AYON\", \"Ynput\")\n            self._thumbnails_dir = os.path.join(directory, \"thumbnails\")\n        return self._thumbnails_dir\n\n    thumbnails_dir = property(get_thumbnails_dir)\n\n    def get_thumbnails_dir_file_info(self):\n        \"\"\"Get information about all files in thumbnails directory.\n\n        Returns:\n            List[FileInfo]: List of file information about all files.\n        \"\"\"\n\n        thumbnails_dir = self.thumbnails_dir\n        files_info = []\n        if not os.path.exists(thumbnails_dir):\n            return files_info\n\n        for root, _, filenames in os.walk(thumbnails_dir):\n            for filename in filenames:\n                path = os.path.join(root, filename)\n                files_info.append(FileInfo(\n                    path, os.path.getsize(path), os.path.getmtime(path)\n                ))\n        return files_info\n\n    def get_thumbnails_dir_size(self, files_info=None):\n        \"\"\"Got full size of thumbnail directory.\n\n        Args:\n            files_info (List[FileInfo]): Prepared file information about\n                files in thumbnail directory.\n\n        Returns:\n            int: File size of all files in thumbnail directory.\n        \"\"\"\n\n        if files_info is None:\n            files_info = self.get_thumbnails_dir_file_info()\n\n        if not files_info:\n            return 0\n\n        return sum(\n            file_info.size\n            for file_info in files_info\n        )\n\n    def cleanup(self, check_max_size=False):\n        \"\"\"Cleanup thumbnails directory.\n\n        Args:\n            check_max_size (bool): Also cleanup files to match max size of\n                thumbnails directory.\n        \"\"\"\n\n        thumbnails_dir = self.get_thumbnails_dir()\n        # Skip if thumbnails dir does not exists yet\n        if not os.path.exists(thumbnails_dir):\n            return\n\n        self._soft_cleanup(thumbnails_dir)\n        if check_max_size:\n            self._max_size_cleanup(thumbnails_dir)\n\n    def _soft_cleanup(self, thumbnails_dir):\n        current_time = time.time()\n        for root, _, filenames in os.walk(thumbnails_dir):\n            for filename in filenames:\n                path = os.path.join(root, filename)\n                modification_time = os.path.getmtime(path)\n                if current_time - modification_time > self._days_alive_secs:\n                    os.remove(path)\n\n    def _max_size_cleanup(self, thumbnails_dir):\n        files_info = self.get_thumbnails_dir_file_info()\n        size = self.get_thumbnails_dir_size(files_info)\n        if size < self.max_filesize:\n            return\n\n        sorted_file_info = collections.deque(\n            sorted(files_info, key=lambda item: item.modification_time)\n        )\n        diff = size - self.max_filesize\n        while diff > 0:\n            if not sorted_file_info:\n                break\n\n            file_info = sorted_file_info.popleft()\n            diff -= file_info.size\n            os.remove(file_info.path)\n\n    def get_thumbnail_filepath(self, project_name, thumbnail_id):\n        \"\"\"Get thumbnail by thumbnail id.\n\n        Args:\n            project_name (str): Name of project.\n            thumbnail_id (str): Thumbnail id.\n\n        Returns:\n            Union[str, None]: Path to thumbnail image or None if thumbnail\n                is not cached yet.\n        \"\"\"\n\n        if not thumbnail_id:\n            return None\n\n        for ext in (\n            \".png\",\n            \".jpeg\",\n        ):\n            filepath = os.path.join(\n                self.thumbnails_dir, project_name, thumbnail_id + ext\n            )\n            if os.path.exists(filepath):\n                return filepath\n        return None\n\n    def get_project_dir(self, project_name):\n        \"\"\"Path to root directory for specific project.\n\n        Args:\n            project_name (str): Name of project for which root directory path\n                should be returned.\n\n        Returns:\n            str: Path to root of project's thumbnails.\n        \"\"\"\n\n        return os.path.join(self.thumbnails_dir, project_name)\n\n    def make_sure_project_dir_exists(self, project_name):\n        project_dir = self.get_project_dir(project_name)\n        if not os.path.exists(project_dir):\n            os.makedirs(project_dir)\n        return project_dir\n\n    def store_thumbnail(self, project_name, thumbnail_id, content, mime_type):\n        \"\"\"Store thumbnail to cache folder.\n\n        Args:\n            project_name (str): Project where the thumbnail belong to.\n            thumbnail_id (str): Id of thumbnail.\n            content (bytes): Byte content of thumbnail file.\n            mime_data (str): Type of content.\n\n        Returns:\n            str: Path to cached thumbnail image file.\n        \"\"\"\n\n        if mime_type == \"image/png\":\n            ext = \".png\"\n        elif mime_type == \"image/jpeg\":\n            ext = \".jpeg\"\n        else:\n            raise ValueError(\n                \"Unknown mime type for thumbnail \\\"{}\\\"\".format(mime_type))\n\n        project_dir = self.make_sure_project_dir_exists(project_name)\n        thumbnail_path = os.path.join(project_dir, thumbnail_id + ext)\n        with open(thumbnail_path, \"wb\") as stream:\n            stream.write(content)\n\n        current_time = time.time()\n        os.utime(thumbnail_path, (current_time, current_time))\n\n        return thumbnail_path\n"
  },
  {
    "path": "openpype/client/server/utils.py",
    "content": "import os\nimport uuid\n\nimport ayon_api\n\nfrom openpype.client.operations_base import REMOVED_VALUE\n\n\nclass _GlobalCache:\n    initialized = False\n\n\ndef get_ayon_server_api_connection():\n    if _GlobalCache.initialized:\n        con = ayon_api.get_server_api_connection()\n    else:\n        from openpype.lib.local_settings import get_local_site_id\n\n        _GlobalCache.initialized = True\n        site_id = get_local_site_id()\n        version = os.getenv(\"AYON_VERSION\")\n        if ayon_api.is_connection_created():\n            con = ayon_api.get_server_api_connection()\n            con.set_site_id(site_id)\n            con.set_client_version(version)\n        else:\n            con = ayon_api.create_connection(site_id, version)\n    return con\n\n\ndef create_entity_id():\n    return uuid.uuid1().hex\n\n\ndef prepare_attribute_changes(old_entity, new_entity, replace=False):\n    \"\"\"Prepare changes of attributes on entities.\n\n    Compare 'attrib' of old and new entity data to prepare only changed\n    values that should be sent to server for update.\n\n    Example:\n        >>> # Limited entity data to 'attrib'\n        >>> old_entity = {\n        ...     \"attrib\": {\"attr_1\": 1, \"attr_2\": \"MyString\", \"attr_3\": True}\n        ... }\n        >>> new_entity = {\n        ...     \"attrib\": {\"attr_1\": 2, \"attr_3\": True, \"attr_4\": 3}\n        ... }\n        >>> # Changes if replacement should not happen\n        >>> expected_changes = {\n        ...   \"attr_1\": 2,\n        ...   \"attr_4\": 3\n        ... }\n        >>> changes = prepare_attribute_changes(old_entity, new_entity)\n        >>> changes == expected_changes\n        True\n\n        >>> # Changes if replacement should happen\n        >>> expected_changes_replace = {\n        ...   \"attr_1\": 2,\n        ...   \"attr_2\": REMOVED_VALUE,\n        ...   \"attr_4\": 3\n        ... }\n        >>> changes_replace = prepare_attribute_changes(\n        ...     old_entity, new_entity, True)\n        >>> changes_replace == expected_changes_replace\n        True\n\n    Args:\n        old_entity (dict[str, Any]): Data of entity queried from server.\n        new_entity (dict[str, Any]): Entity data with applied changes.\n        replace (bool): New entity should fully replace all old entity values.\n\n    Returns:\n        Dict[str, Any]: Values from new entity only if value has changed.\n    \"\"\"\n\n    attrib_changes = {}\n    new_attrib = new_entity.get(\"attrib\")\n    old_attrib = old_entity.get(\"attrib\")\n    if new_attrib is None:\n        if not replace:\n            return attrib_changes\n        new_attrib = {}\n\n    if old_attrib is None:\n        return new_attrib\n\n    for attr, new_attr_value in new_attrib.items():\n        old_attr_value = old_attrib.get(attr)\n        if old_attr_value != new_attr_value:\n            attrib_changes[attr] = new_attr_value\n\n    if replace:\n        for attr in old_attrib:\n            if attr not in new_attrib:\n                attrib_changes[attr] = REMOVED_VALUE\n\n    return attrib_changes\n\n\ndef prepare_entity_changes(old_entity, new_entity, replace=False):\n    \"\"\"Prepare changes of AYON entities.\n\n    Compare old and new entity to filter values from new data that changed.\n\n    Args:\n        old_entity (dict[str, Any]): Data of entity queried from server.\n        new_entity (dict[str, Any]): Entity data with applied changes.\n        replace (bool): All attributes should be replaced by new values. So\n            all attribute values that are not on new entity will be removed.\n\n    Returns:\n        Dict[str, Any]: Only values from new entity that changed.\n    \"\"\"\n\n    changes = {}\n    for key, new_value in new_entity.items():\n        if key == \"attrib\":\n            continue\n\n        old_value = old_entity.get(key)\n        if old_value != new_value:\n            changes[key] = new_value\n\n    if replace:\n        for key in old_entity:\n            if key not in new_entity:\n                changes[key] = REMOVED_VALUE\n\n    attr_changes = prepare_attribute_changes(old_entity, new_entity, replace)\n    if attr_changes:\n        changes[\"attrib\"] = attr_changes\n    return changes\n"
  },
  {
    "path": "openpype/hooks/pre_add_last_workfile_arg.py",
    "content": "import os\n\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass AddLastWorkfileToLaunchArgs(PreLaunchHook):\n    \"\"\"Add last workfile path to launch arguments.\n\n    This is not possible to do for all applications the same way.\n    Checks 'start_last_workfile', if set to False, it will not open last\n    workfile. This property is set explicitly in Launcher.\n    \"\"\"\n\n    # Execute after workfile template copy\n    order = 10\n    app_groups = {\n        \"3dsmax\", \"adsk_3dsmax\",\n        \"maya\",\n        \"nuke\",\n        \"nukex\",\n        \"hiero\",\n        \"houdini\",\n        \"nukestudio\",\n        \"fusion\",\n        \"blender\",\n        \"photoshop\",\n        \"tvpaint\",\n        \"substancepainter\",\n        \"aftereffects\",\n        \"wrap\"\n    }\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        if not self.data.get(\"start_last_workfile\"):\n            self.log.info(\"It is set to not start last workfile on start.\")\n            return\n\n        last_workfile = self.data.get(\"last_workfile_path\")\n        if not last_workfile:\n            self.log.warning(\"Last workfile was not collected.\")\n            return\n\n        if not os.path.exists(last_workfile):\n            self.log.info(\"Current context does not have any workfile yet.\")\n            return\n\n        # Add path to workfile to arguments\n        self.launch_context.launch_args.append(last_workfile)\n"
  },
  {
    "path": "openpype/hooks/pre_copy_template_workfile.py",
    "content": "import os\nimport shutil\nfrom openpype.settings import get_project_settings\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\nfrom openpype.pipeline.workfile import (\n    get_custom_workfile_template,\n    get_custom_workfile_template_by_string_context\n)\n\n\nclass CopyTemplateWorkfile(PreLaunchHook):\n    \"\"\"Copy workfile template.\n\n    This is not possible to do for all applications the same way.\n\n    Prelaunch hook works only if last workfile leads to not existing file.\n        - That is possible only if it's first version.\n    \"\"\"\n\n    # Before `AddLastWorkfileToLaunchArgs`\n    order = 0\n    app_groups = {\"blender\", \"photoshop\", \"tvpaint\", \"aftereffects\",\n                  \"wrap\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        \"\"\"Check if can copy template for context and do it if possible.\n\n        First check if host for current project should create first workfile.\n        Second check is if template is reachable and can be copied.\n\n        Args:\n            last_workfile(str): Path where template will be copied.\n\n        Returns:\n            None: This is a void method.\n        \"\"\"\n\n        last_workfile = self.data.get(\"last_workfile_path\")\n        if not last_workfile:\n            self.log.warning((\n                \"Last workfile was not collected.\"\n                \" Can't add it to launch arguments or determine if should\"\n                \" copy template.\"\n            ))\n            return\n\n        if os.path.exists(last_workfile):\n            self.log.debug(\"Last workfile exists. Skipping {} process.\".format(\n                self.__class__.__name__\n            ))\n            return\n\n        self.log.info(\"Last workfile does not exist.\")\n\n        project_name = self.data[\"project_name\"]\n        asset_name = self.data[\"asset_name\"]\n        task_name = self.data[\"task_name\"]\n        host_name = self.application.host_name\n\n        project_settings = get_project_settings(project_name)\n\n        project_doc = self.data.get(\"project_doc\")\n        asset_doc = self.data.get(\"asset_doc\")\n        anatomy = self.data.get(\"anatomy\")\n        if project_doc and asset_doc:\n            self.log.debug(\"Started filtering of custom template paths.\")\n            template_path = get_custom_workfile_template(\n                project_doc,\n                asset_doc,\n                task_name,\n                host_name,\n                anatomy,\n                project_settings\n            )\n\n        else:\n            self.log.warning((\n                \"Global data collection probably did not execute.\"\n                \" Using backup solution.\"\n            ))\n            template_path = get_custom_workfile_template_by_string_context(\n                project_name,\n                asset_name,\n                task_name,\n                host_name,\n                anatomy,\n                project_settings\n            )\n\n        if not template_path:\n            self.log.info(\n                \"Registered custom templates didn't match current context.\"\n            )\n            return\n\n        if not os.path.exists(template_path):\n            self.log.warning(\n                \"Couldn't find workfile template file \\\"{}\\\"\".format(\n                    template_path\n                )\n            )\n            return\n\n        self.log.info(\n            f\"Creating workfile from template: \\\"{template_path}\\\"\"\n        )\n\n        # Copy template workfile to new destination\n        shutil.copy2(\n            os.path.normpath(template_path),\n            os.path.normpath(last_workfile)\n        )\n"
  },
  {
    "path": "openpype/hooks/pre_create_extra_workdir_folders.py",
    "content": "import os\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\nfrom openpype.pipeline.workfile import create_workdir_extra_folders\n\n\nclass CreateWorkdirExtraFolders(PreLaunchHook):\n    \"\"\"Create extra folders for the work directory.\n\n    Based on setting `project_settings/global/tools/Workfiles/extra_folders`\n    profile filtering will decide whether extra folders need to be created in\n    the work directory.\n\n    \"\"\"\n\n    # Execute after workfile template copy\n    order = 15\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        if not self.application.is_host:\n            return\n\n        env = self.data.get(\"env\") or {}\n        workdir = env.get(\"AVALON_WORKDIR\")\n        if not workdir or not os.path.exists(workdir):\n            return\n\n        host_name = self.application.host_name\n        task_type = self.data[\"task_type\"]\n        task_name = self.data[\"task_name\"]\n        project_name = self.data[\"project_name\"]\n\n        create_workdir_extra_folders(\n            workdir, host_name, task_type, task_name, project_name,\n        )\n"
  },
  {
    "path": "openpype/hooks/pre_global_host_data.py",
    "content": "from openpype.client import get_project, get_asset_by_name\nfrom openpype.lib.applications import (\n    PreLaunchHook,\n    EnvironmentPrepData,\n    prepare_app_environments,\n    prepare_context_environments\n)\nfrom openpype.pipeline import Anatomy\n\n\nclass GlobalHostDataHook(PreLaunchHook):\n    order = -100\n    launch_types = set()\n\n    def execute(self):\n        \"\"\"Prepare global objects to `data` that will be used for sure.\"\"\"\n        self.prepare_global_data()\n\n        if not self.data.get(\"asset_doc\"):\n            return\n\n        app = self.launch_context.application\n        temp_data = EnvironmentPrepData({\n            \"project_name\": self.data[\"project_name\"],\n            \"asset_name\": self.data[\"asset_name\"],\n            \"task_name\": self.data[\"task_name\"],\n\n            \"app\": app,\n\n            \"project_doc\": self.data[\"project_doc\"],\n            \"asset_doc\": self.data[\"asset_doc\"],\n\n            \"anatomy\": self.data[\"anatomy\"],\n\n            \"env\": self.launch_context.env,\n\n            \"start_last_workfile\": self.data.get(\"start_last_workfile\"),\n            \"last_workfile_path\": self.data.get(\"last_workfile_path\"),\n\n            \"log\": self.log\n        })\n\n        prepare_app_environments(temp_data, self.launch_context.env_group)\n        prepare_context_environments(temp_data)\n\n        temp_data.pop(\"log\")\n\n        self.data.update(temp_data)\n\n    def prepare_global_data(self):\n        \"\"\"Prepare global objects to `data` that will be used for sure.\"\"\"\n        # Mongo documents\n        project_name = self.data.get(\"project_name\")\n        if not project_name:\n            self.log.info(\n                \"Skipping global data preparation.\"\n                \" Key `project_name` was not found in launch context.\"\n            )\n            return\n\n        self.log.debug(\"Project name is set to \\\"{}\\\"\".format(project_name))\n        # Anatomy\n        self.data[\"anatomy\"] = Anatomy(project_name)\n\n        # Project document\n        project_doc = get_project(project_name)\n        self.data[\"project_doc\"] = project_doc\n\n        asset_name = self.data.get(\"asset_name\")\n        if not asset_name:\n            self.log.warning(\n                \"Asset name was not set. Skipping asset document query.\"\n            )\n            return\n\n        asset_doc = get_asset_by_name(project_name, asset_name)\n        self.data[\"asset_doc\"] = asset_doc\n"
  },
  {
    "path": "openpype/hooks/pre_mac_launch.py",
    "content": "import os\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass LaunchWithTerminal(PreLaunchHook):\n    \"\"\"Mac specific pre arguments for application.\n\n    Mac applications should be launched using \"open\" argument which is internal\n    callbacks to open executable. We also add argument \"-a\" to tell it's\n    application open. This is used only for executables ending with \".app\". It\n    is expected that these executables lead to app packages.\n    \"\"\"\n    order = 1000\n\n    platforms = {\"darwin\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        executable = str(self.launch_context.executable)\n        # Skip executables not ending with \".app\" or that are not folder\n        if not executable.endswith(\".app\") or not os.path.isdir(executable):\n            return\n\n        # Check if first argument match executable path\n        # - Few applications are not executed directly but through OpenPype\n        #   process (Photoshop, AfterEffects, Harmony, ...). These should not\n        #   use `open`.\n        if self.launch_context.launch_args[0] != executable:\n            return\n\n        # Tell `open` to pass arguments if there are any\n        if len(self.launch_context.launch_args) > 1:\n            self.launch_context.launch_args.insert(1, \"--args\")\n        # Prepend open arguments\n        self.launch_context.launch_args.insert(0, [\"open\", \"-na\"])\n"
  },
  {
    "path": "openpype/hooks/pre_new_console_apps.py",
    "content": "import subprocess\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass LaunchNewConsoleApps(PreLaunchHook):\n    \"\"\"Foundry applications have specific way how to launch them.\n\n    Nuke is executed \"like\" python process so it is required to pass\n    `CREATE_NEW_CONSOLE` flag on windows to trigger creation of new console.\n    At the same time the newly created console won't create its own stdout\n    and stderr handlers so they should not be redirected to DEVNULL.\n    \"\"\"\n\n    # Should be as last hook because must change launch arguments to string\n    order = 1000\n    app_groups = {\n        \"nuke\", \"nukeassist\", \"nukex\", \"hiero\", \"nukestudio\", \"mayapy\"\n    }\n    platforms = {\"windows\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        # Change `creationflags` to CREATE_NEW_CONSOLE\n        # - on Windows some apps will create new window using its console\n        # Set `stdout` and `stderr` to None so new created console does not\n        #   have redirected output to DEVNULL in build\n        self.launch_context.kwargs.update({\n            \"creationflags\": subprocess.CREATE_NEW_CONSOLE,\n            \"stdout\": None,\n            \"stderr\": None\n        })\n"
  },
  {
    "path": "openpype/hooks/pre_non_python_host_launch.py",
    "content": "import os\n\nfrom openpype.lib import get_openpype_execute_args\nfrom openpype.lib.applications import (\n    get_non_python_host_kwargs,\n    PreLaunchHook,\n    LaunchTypes,\n)\n\nfrom openpype import PACKAGE_DIR as OPENPYPE_DIR\n\n\nclass NonPythonHostHook(PreLaunchHook):\n    \"\"\"Launch arguments preparation.\n\n    Non python host implementation do not launch host directly but use\n    python script which launch the host. For these cases it is necessary to\n    prepend python (or openpype) executable and script path before application's.\n    \"\"\"\n    app_groups = {\"harmony\", \"photoshop\", \"aftereffects\"}\n\n    order = 20\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        # Pop executable\n        executable_path = self.launch_context.launch_args.pop(0)\n\n        # Pop rest of launch arguments - There should not be other arguments!\n        remainders = []\n        while self.launch_context.launch_args:\n            remainders.append(self.launch_context.launch_args.pop(0))\n\n        script_path = os.path.join(\n            OPENPYPE_DIR,\n            \"scripts\",\n            \"non_python_host_launch.py\"\n        )\n\n        new_launch_args = get_openpype_execute_args(\n            \"run\", script_path, executable_path\n        )\n        # Add workfile path if exists\n        workfile_path = self.data[\"last_workfile_path\"]\n        if (\n                self.data.get(\"start_last_workfile\")\n                and workfile_path\n                and os.path.exists(workfile_path)):\n            new_launch_args.append(workfile_path)\n\n        # Append as whole list as these areguments should not be separated\n        self.launch_context.launch_args.append(new_launch_args)\n\n        if remainders:\n            self.launch_context.launch_args.extend(remainders)\n\n        self.launch_context.kwargs = \\\n            get_non_python_host_kwargs(self.launch_context.kwargs)\n"
  },
  {
    "path": "openpype/hooks/pre_ocio_hook.py",
    "content": "from openpype.lib.applications import PreLaunchHook\n\nfrom openpype.pipeline.colorspace import get_imageio_config\nfrom openpype.pipeline.template_data import get_template_data_with_names\n\n\nclass OCIOEnvHook(PreLaunchHook):\n    \"\"\"Set OCIO environment variable for hosts that use OpenColorIO.\"\"\"\n\n    order = 0\n    hosts = {\n        \"substancepainter\",\n        \"fusion\",\n        \"blender\",\n        \"aftereffects\",\n        \"3dsmax\",\n        \"houdini\",\n        \"maya\",\n        \"nuke\",\n        \"hiero\",\n        \"resolve\",\n    }\n    launch_types = set()\n\n    def execute(self):\n        \"\"\"Hook entry method.\"\"\"\n\n        template_data = get_template_data_with_names(\n            project_name=self.data[\"project_name\"],\n            asset_name=self.data[\"asset_name\"],\n            task_name=self.data[\"task_name\"],\n            host_name=self.host_name,\n            system_settings=self.data[\"system_settings\"]\n        )\n\n        config_data = get_imageio_config(\n            project_name=self.data[\"project_name\"],\n            host_name=self.host_name,\n            project_settings=self.data[\"project_settings\"],\n            anatomy_data=template_data,\n            anatomy=self.data[\"anatomy\"],\n            env=self.launch_context.env,\n        )\n\n        if config_data:\n            ocio_path = config_data[\"path\"]\n\n            if self.host_name in [\"nuke\", \"hiero\"]:\n                ocio_path = ocio_path.replace(\"\\\\\", \"/\")\n\n            self.log.info(\n                f\"Setting OCIO environment to config path: {ocio_path}\")\n\n            self.launch_context.env[\"OCIO\"] = ocio_path\n        else:\n            self.log.debug(\"OCIO not set or enabled\")\n"
  },
  {
    "path": "openpype/host/__init__.py",
    "content": "from .host import (\n    HostBase,\n)\n\nfrom .interfaces import (\n    IWorkfileHost,\n    ILoadHost,\n    IPublishHost,\n    INewPublisher,\n)\n\nfrom .dirmap import HostDirmap\n\n\n__all__ = (\n    \"HostBase\",\n\n    \"IWorkfileHost\",\n    \"ILoadHost\",\n    \"IPublishHost\",\n    \"INewPublisher\",\n\n    \"HostDirmap\",\n)\n"
  },
  {
    "path": "openpype/host/dirmap.py",
    "content": "\"\"\"Dirmap functionality used in host integrations inside DCCs.\n\nIdea for current dirmap implementation was used from Maya where is possible to\nenter source and destination roots and maya will try each found source\nin referenced file replace with each destination paths. First path which\nexists is used.\n\"\"\"\n\nimport os\nfrom abc import ABCMeta, abstractmethod\nimport platform\n\nimport six\n\nfrom openpype.lib import Logger\nfrom openpype.modules import ModulesManager\nfrom openpype.settings import get_project_settings\nfrom openpype.settings.lib import get_site_local_overrides\n\n\n@six.add_metaclass(ABCMeta)\nclass HostDirmap(object):\n    \"\"\"Abstract class for running dirmap on a workfile in a host.\n\n    Dirmap is used to translate paths inside of host workfile from one\n    OS to another. (Eg. arstist created workfile on Win, different artists\n    opens same file on Linux.)\n\n    Expects methods to be implemented inside of host:\n        on_dirmap_enabled: run host code for enabling dirmap\n        do_dirmap: run host code to do actual remapping\n    \"\"\"\n\n    def __init__(\n        self,\n        host_name,\n        project_name,\n        project_settings=None,\n        sync_module=None\n    ):\n        self.host_name = host_name\n        self.project_name = project_name\n        self._project_settings = project_settings\n        self._sync_module = sync_module\n        # to limit reinit of Modules\n        self._sync_module_discovered = sync_module is not None\n        self._log = None\n\n    @property\n    def sync_module(self):\n        if not self._sync_module_discovered:\n            self._sync_module_discovered = True\n            manager = ModulesManager()\n            self._sync_module = manager.get(\"sync_server\")\n        return self._sync_module\n\n    @property\n    def project_settings(self):\n        if self._project_settings is None:\n            self._project_settings = get_project_settings(self.project_name)\n        return self._project_settings\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    @abstractmethod\n    def on_enable_dirmap(self):\n        \"\"\"Run host dependent operation for enabling dirmap if necessary.\"\"\"\n        pass\n\n    @abstractmethod\n    def dirmap_routine(self, source_path, destination_path):\n        \"\"\"Run host dependent remapping from source_path to destination_path\"\"\"\n        pass\n\n    def process_dirmap(self, mapping=None):\n        # type: (dict) -> None\n        \"\"\"Go through all paths in Settings and set them using `dirmap`.\n\n            If artists has Site Sync enabled, take dirmap mapping directly from\n            Local Settings when artist is syncing workfile locally.\n\n        \"\"\"\n\n        if not mapping:\n            mapping = self.get_mappings()\n        if not mapping:\n            return\n\n        self.on_enable_dirmap()\n\n        for k, sp in enumerate(mapping[\"source-path\"]):\n            dst = mapping[\"destination-path\"][k]\n            try:\n                # add trailing slash if missing\n                sp = os.path.join(sp, '')\n                dst = os.path.join(dst, '')\n                print(\"{} -> {}\".format(sp, dst))\n                self.dirmap_routine(sp, dst)\n            except IndexError:\n                # missing corresponding destination path\n                self.log.error((\n                    \"invalid dirmap mapping, missing corresponding\"\n                    \" destination directory.\"\n                ))\n                break\n            except RuntimeError:\n                self.log.error(\n                    \"invalid path {} -> {}, mapping not registered\".format(\n                        sp, dst\n                    )\n                )\n                continue\n\n    def get_mappings(self):\n        \"\"\"Get translation from source-path to destination-path.\n\n            It checks if Site Sync is enabled and user chose to use local\n            site, in that case configuration in Local Settings takes precedence\n        \"\"\"\n\n        dirmap_label = \"{}-dirmap\".format(self.host_name)\n        mapping_sett = self.project_settings[self.host_name].get(dirmap_label,\n                                                                 {})\n        local_mapping = self._get_local_sync_dirmap()\n        mapping_enabled = mapping_sett.get(\"enabled\") or bool(local_mapping)\n        if not mapping_enabled:\n            return {}\n\n        mapping = (\n            local_mapping\n            or mapping_sett[\"paths\"]\n            or {}\n        )\n\n        if (\n            not mapping\n            or not mapping.get(\"destination-path\")\n            or not mapping.get(\"source-path\")\n        ):\n            return {}\n        self.log.info(\"Processing directory mapping ...\")\n        self.log.info(\"mapping:: {}\".format(mapping))\n        return mapping\n\n    def _get_local_sync_dirmap(self):\n        \"\"\"\n            Returns dirmap if synch to local project is enabled.\n\n            Only valid mapping is from roots of remote site to local site set\n            in Local Settings.\n\n            Returns:\n                dict : { \"source-path\": [XXX], \"destination-path\": [YYYY]}\n        \"\"\"\n        project_name = self.project_name\n\n        sync_module = self.sync_module\n        mapping = {}\n        if (\n            sync_module is None\n            or not sync_module.enabled\n            or project_name not in sync_module.get_enabled_projects()\n        ):\n            return mapping\n\n        active_site = sync_module.get_local_normalized_site(\n            sync_module.get_active_site(project_name))\n        remote_site = sync_module.get_local_normalized_site(\n            sync_module.get_remote_site(project_name))\n        self.log.debug(\n            \"active {} - remote {}\".format(active_site, remote_site)\n        )\n\n        if active_site == \"local\" and active_site != remote_site:\n            sync_settings = sync_module.get_sync_project_setting(\n                project_name,\n                exclude_locals=False,\n                cached=False)\n\n            active_overrides = get_site_local_overrides(\n                project_name, active_site)\n            remote_overrides = get_site_local_overrides(\n                project_name, remote_site)\n\n            self.log.debug(\"local overrides {}\".format(active_overrides))\n            self.log.debug(\"remote overrides {}\".format(remote_overrides))\n\n            current_platform = platform.system().lower()\n            remote_provider = sync_module.get_provider_for_site(\n                project_name, remote_site\n            )\n            # dirmap has sense only with regular disk provider, in the workfile\n            # won't be root on cloud or sftp provider\n            if remote_provider != \"local_drive\":\n                remote_site = \"studio\"\n            for root_name, active_site_dir in active_overrides.items():\n                remote_site_dir = (\n                    remote_overrides.get(root_name)\n                    or sync_settings[\"sites\"][remote_site][\"root\"][root_name]\n                )\n\n                if isinstance(remote_site_dir, dict):\n                    remote_site_dir = remote_site_dir.get(current_platform)\n\n                if not remote_site_dir:\n                    continue\n\n                if os.path.isdir(active_site_dir):\n                    if \"destination-path\" not in mapping:\n                        mapping[\"destination-path\"] = []\n                    mapping[\"destination-path\"].append(active_site_dir)\n\n                    if \"source-path\" not in mapping:\n                        mapping[\"source-path\"] = []\n                    mapping[\"source-path\"].append(remote_site_dir)\n\n            self.log.debug(\"local sync mapping:: {}\".format(mapping))\n        return mapping\n"
  },
  {
    "path": "openpype/host/host.py",
    "content": "import os\nimport logging\nimport contextlib\nfrom abc import ABCMeta, abstractproperty\nimport six\n\n# NOTE can't import 'typing' because of issues in Maya 2020\n#   - shiboken crashes on 'typing' module import\n\n\n@six.add_metaclass(ABCMeta)\nclass HostBase(object):\n    \"\"\"Base of host implementation class.\n\n    Host is pipeline implementation of DCC application. This class should help\n    to identify what must/should/can be implemented for specific functionality.\n\n    Compared to 'avalon' concept:\n    What was before considered as functions in host implementation folder. The\n    host implementation should primarily care about adding ability of creation\n    (mark subsets to be published) and optionally about referencing published\n    representations as containers.\n\n    Host may need extend some functionality like working with workfiles\n    or loading. Not all host implementations may allow that for those purposes\n    can be logic extended with implementing functions for the purpose. There\n    are prepared interfaces to be able identify what must be implemented to\n    be able use that functionality.\n    - current statement is that it is not required to inherit from interfaces\n        but all of the methods are validated (only their existence!)\n\n    # Installation of host before (avalon concept):\n    ```python\n    from openpype.pipeline import install_host\n    import openpype.hosts.maya.api as host\n\n    install_host(host)\n    ```\n\n    # Installation of host now:\n    ```python\n    from openpype.pipeline import install_host\n    from openpype.hosts.maya.api import MayaHost\n\n    host = MayaHost()\n    install_host(host)\n    ```\n\n    Todo:\n        - move content of 'install_host' as method of this class\n            - register host object\n            - install legacy_io\n            - install global plugin paths\n        - store registered plugin paths to this object\n        - handle current context (project, asset, task)\n            - this must be done in many separated steps\n        - have it's object of host tools instead of using globals\n\n    This implementation will probably change over time when more\n        functionality and responsibility will be added.\n    \"\"\"\n\n    _log = None\n\n    def __init__(self):\n        \"\"\"Initialization of host.\n\n        Register DCC callbacks, host specific plugin paths, targets etc.\n        (Part of what 'install' did in 'avalon' concept.)\n\n        Note:\n            At this moment global \"installation\" must happen before host\n            installation. Because of this current limitation it is recommended\n            to implement 'install' method which is triggered after global\n            'install'.\n        \"\"\"\n\n        pass\n\n    def install(self):\n        \"\"\"Install host specific functionality.\n\n        This is where should be added menu with tools, registered callbacks\n        and other host integration initialization.\n\n        It is called automatically when 'openpype.pipeline.install_host' is\n        triggered.\n        \"\"\"\n\n        pass\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = logging.getLogger(self.__class__.__name__)\n        return self._log\n\n    @abstractproperty\n    def name(self):\n        \"\"\"Host name.\"\"\"\n\n        pass\n\n    def get_current_project_name(self):\n        \"\"\"\n        Returns:\n            Union[str, None]: Current project name.\n        \"\"\"\n\n        return os.environ.get(\"AVALON_PROJECT\")\n\n    def get_current_asset_name(self):\n        \"\"\"\n        Returns:\n            Union[str, None]: Current asset name.\n        \"\"\"\n\n        return os.environ.get(\"AVALON_ASSET\")\n\n    def get_current_task_name(self):\n        \"\"\"\n        Returns:\n            Union[str, None]: Current task name.\n        \"\"\"\n\n        return os.environ.get(\"AVALON_TASK\")\n\n    def get_current_context(self):\n        \"\"\"Get current context information.\n\n        This method should be used to get current context of host. Usage of\n        this method can be crucial for host implementations in DCCs where\n        can be opened multiple workfiles at one moment and change of context\n        can't be caught properly.\n\n        Default implementation returns values from 'legacy_io.Session'.\n\n        Returns:\n            Dict[str, Union[str, None]]: Context with 3 keys 'project_name',\n                'asset_name' and 'task_name'. All of them can be 'None'.\n        \"\"\"\n\n        return {\n            \"project_name\": self.get_current_project_name(),\n            \"asset_name\": self.get_current_asset_name(),\n            \"task_name\": self.get_current_task_name()\n        }\n\n    def get_context_title(self):\n        \"\"\"Context title shown for UI purposes.\n\n        Should return current context title if possible.\n\n        Note:\n            This method is used only for UI purposes so it is possible to\n                return some logical title for contextless cases.\n            Is not meant for \"Context menu\" label.\n\n        Returns:\n            str: Context title.\n            None: Default title is used based on UI implementation.\n        \"\"\"\n\n        # Use current context to fill the context title\n        current_context = self.get_current_context()\n        project_name = current_context[\"project_name\"]\n        asset_name = current_context[\"asset_name\"]\n        task_name = current_context[\"task_name\"]\n        items = []\n        if project_name:\n            items.append(project_name)\n            if asset_name:\n                items.append(asset_name.lstrip(\"/\"))\n                if task_name:\n                    items.append(task_name)\n        if items:\n            return \"/\".join(items)\n        return None\n\n    @contextlib.contextmanager\n    def maintained_selection(self):\n        \"\"\"Some functionlity will happen but selection should stay same.\n\n        This is DCC specific. Some may not allow to implement this ability\n        that is reason why default implementation is empty context manager.\n\n        Yields:\n            None: Yield when is ready to restore selected at the end.\n        \"\"\"\n\n        try:\n            yield\n        finally:\n            pass\n"
  },
  {
    "path": "openpype/host/interfaces.py",
    "content": "from abc import ABCMeta, abstractmethod\nimport six\n\n\nclass MissingMethodsError(ValueError):\n    \"\"\"Exception when host miss some required methods for specific workflow.\n\n    Args:\n        host (HostBase): Host implementation where are missing methods.\n        missing_methods (list[str]): List of missing methods.\n    \"\"\"\n\n    def __init__(self, host, missing_methods):\n        joined_missing = \", \".join(\n            ['\"{}\"'.format(item) for item in missing_methods]\n        )\n        host_name = getattr(host, \"name\", None)\n        if not host_name:\n            try:\n                host_name = host.__file__.replace(\"\\\\\", \"/\").split(\"/\")[-3]\n            except Exception:\n                host_name = str(host)\n        message = (\n            \"Host \\\"{}\\\" miss methods {}\".format(host_name, joined_missing)\n        )\n        super(MissingMethodsError, self).__init__(message)\n\n\nclass ILoadHost:\n    \"\"\"Implementation requirements to be able use reference of representations.\n\n    The load plugins can do referencing even without implementation of methods\n    here, but switch and removement of containers would not be possible.\n\n    Questions:\n        - Is list container dependency of host or load plugins?\n        - Should this be directly in HostBase?\n            - how to find out if referencing is available?\n            - do we need to know that?\n    \"\"\"\n\n    @staticmethod\n    def get_missing_load_methods(host):\n        \"\"\"Look for missing methods on \"old type\" host implementation.\n\n        Method is used for validation of implemented functions related to\n        loading. Checks only existence of methods.\n\n        Args:\n            Union[ModuleType, HostBase]: Object of host where to look for\n                required methods.\n\n        Returns:\n            list[str]: Missing method implementations for loading workflow.\n        \"\"\"\n\n        if isinstance(host, ILoadHost):\n            return []\n\n        required = [\"ls\"]\n        missing = []\n        for name in required:\n            if not hasattr(host, name):\n                missing.append(name)\n        return missing\n\n    @staticmethod\n    def validate_load_methods(host):\n        \"\"\"Validate implemented methods of \"old type\" host for load workflow.\n\n        Args:\n            Union[ModuleType, HostBase]: Object of host to validate.\n\n        Raises:\n            MissingMethodsError: If there are missing methods on host\n                implementation.\n        \"\"\"\n        missing = ILoadHost.get_missing_load_methods(host)\n        if missing:\n            raise MissingMethodsError(host, missing)\n\n    @abstractmethod\n    def get_containers(self):\n        \"\"\"Retrieve referenced containers from scene.\n\n        This can be implemented in hosts where referencing can be used.\n\n        Todo:\n            Rename function to something more self explanatory.\n                Suggestion: 'get_containers'\n\n        Returns:\n            list[dict]: Information about loaded containers.\n        \"\"\"\n\n        pass\n\n    # --- Deprecated method names ---\n    def ls(self):\n        \"\"\"Deprecated variant of 'get_containers'.\n\n        Todo:\n            Remove when all usages are replaced.\n        \"\"\"\n\n        return self.get_containers()\n\n\n@six.add_metaclass(ABCMeta)\nclass IWorkfileHost:\n    \"\"\"Implementation requirements to be able use workfile utils and tool.\"\"\"\n\n    @staticmethod\n    def get_missing_workfile_methods(host):\n        \"\"\"Look for missing methods on \"old type\" host implementation.\n\n        Method is used for validation of implemented functions related to\n        workfiles. Checks only existence of methods.\n\n        Args:\n            Union[ModuleType, HostBase]: Object of host where to look for\n                required methods.\n\n        Returns:\n            list[str]: Missing method implementations for workfiles workflow.\n        \"\"\"\n\n        if isinstance(host, IWorkfileHost):\n            return []\n\n        required = [\n            \"open_file\",\n            \"save_file\",\n            \"current_file\",\n            \"has_unsaved_changes\",\n            \"file_extensions\",\n            \"work_root\",\n        ]\n        missing = []\n        for name in required:\n            if not hasattr(host, name):\n                missing.append(name)\n        return missing\n\n    @staticmethod\n    def validate_workfile_methods(host):\n        \"\"\"Validate methods of \"old type\" host for workfiles workflow.\n\n        Args:\n            Union[ModuleType, HostBase]: Object of host to validate.\n\n        Raises:\n            MissingMethodsError: If there are missing methods on host\n                implementation.\n        \"\"\"\n\n        missing = IWorkfileHost.get_missing_workfile_methods(host)\n        if missing:\n            raise MissingMethodsError(host, missing)\n\n    @abstractmethod\n    def get_workfile_extensions(self):\n        \"\"\"Extensions that can be used as save.\n\n        Questions:\n            This could potentially use 'HostDefinition'.\n        \"\"\"\n\n        return []\n\n    @abstractmethod\n    def save_workfile(self, dst_path=None):\n        \"\"\"Save currently opened scene.\n\n        Args:\n            dst_path (str): Where the current scene should be saved. Or use\n                current path if 'None' is passed.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def open_workfile(self, filepath):\n        \"\"\"Open passed filepath in the host.\n\n        Args:\n            filepath (str): Path to workfile.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_current_workfile(self):\n        \"\"\"Retrieve path to current opened file.\n\n        Returns:\n            str: Path to file which is currently opened.\n            None: If nothing is opened.\n        \"\"\"\n\n        return None\n\n    def workfile_has_unsaved_changes(self):\n        \"\"\"Currently opened scene is saved.\n\n        Not all hosts can know if current scene is saved because the API of\n        DCC does not support it.\n\n        Returns:\n            bool: True if scene is saved and False if has unsaved\n                modifications.\n            None: Can't tell if workfiles has modifications.\n        \"\"\"\n\n        return None\n\n    def work_root(self, session):\n        \"\"\"Modify workdir per host.\n\n        Default implementation keeps workdir untouched.\n\n        Warnings:\n            We must handle this modification with more sophisticated way\n            because this can't be called out of DCC so opening of last workfile\n            (calculated before DCC is launched) is complicated. Also breaking\n            defined work template is not a good idea.\n            Only place where it's really used and can make sense is Maya. There\n            workspace.mel can modify subfolders where to look for maya files.\n\n        Args:\n            session (dict): Session context data.\n\n        Returns:\n            str: Path to new workdir.\n        \"\"\"\n\n        return session[\"AVALON_WORKDIR\"]\n\n    # --- Deprecated method names ---\n    def file_extensions(self):\n        \"\"\"Deprecated variant of 'get_workfile_extensions'.\n\n        Todo:\n            Remove when all usages are replaced.\n        \"\"\"\n        return self.get_workfile_extensions()\n\n    def save_file(self, dst_path=None):\n        \"\"\"Deprecated variant of 'save_workfile'.\n\n        Todo:\n            Remove when all usages are replaced.\n        \"\"\"\n\n        self.save_workfile(dst_path)\n\n    def open_file(self, filepath):\n        \"\"\"Deprecated variant of 'open_workfile'.\n\n        Todo:\n            Remove when all usages are replaced.\n        \"\"\"\n\n        return self.open_workfile(filepath)\n\n    def current_file(self):\n        \"\"\"Deprecated variant of 'get_current_workfile'.\n\n        Todo:\n            Remove when all usages are replaced.\n        \"\"\"\n\n        return self.get_current_workfile()\n\n    def has_unsaved_changes(self):\n        \"\"\"Deprecated variant of 'workfile_has_unsaved_changes'.\n\n        Todo:\n            Remove when all usages are replaced.\n        \"\"\"\n\n        return self.workfile_has_unsaved_changes()\n\n\nclass IPublishHost:\n    \"\"\"Functions related to new creation system in new publisher.\n\n    New publisher is not storing information only about each created instance\n    but also some global data. At this moment are data related only to context\n    publish plugins but that can extend in future.\n    \"\"\"\n\n    @staticmethod\n    def get_missing_publish_methods(host):\n        \"\"\"Look for missing methods on \"old type\" host implementation.\n\n        Method is used for validation of implemented functions related to\n        new publish creation. Checks only existence of methods.\n\n        Args:\n            Union[ModuleType, HostBase]: Host module where to look for\n                required methods.\n\n        Returns:\n            list[str]: Missing method implementations for new publisher\n                workflow.\n        \"\"\"\n\n        if isinstance(host, IPublishHost):\n            return []\n\n        required = [\n            \"get_context_data\",\n            \"update_context_data\",\n            \"get_context_title\",\n            \"get_current_context\",\n        ]\n        missing = []\n        for name in required:\n            if not hasattr(host, name):\n                missing.append(name)\n        return missing\n\n    @staticmethod\n    def validate_publish_methods(host):\n        \"\"\"Validate implemented methods of \"old type\" host.\n\n        Args:\n            Union[ModuleType, HostBase]: Host module to validate.\n\n        Raises:\n            MissingMethodsError: If there are missing methods on host\n                implementation.\n        \"\"\"\n        missing = IPublishHost.get_missing_publish_methods(host)\n        if missing:\n            raise MissingMethodsError(host, missing)\n\n    @abstractmethod\n    def get_context_data(self):\n        \"\"\"Get global data related to creation-publishing from workfile.\n\n        These data are not related to any created instance but to whole\n        publishing context. Not saving/returning them will cause that each\n        reset of publishing resets all values to default ones.\n\n        Context data can contain information about enabled/disabled publish\n        plugins or other values that can be filled by artist.\n\n        Returns:\n            dict: Context data stored using 'update_context_data'.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def update_context_data(self, data, changes):\n        \"\"\"Store global context data to workfile.\n\n        Called when some values in context data has changed.\n\n        Without storing the values in a way that 'get_context_data' would\n        return them will each reset of publishing cause loose of filled values\n        by artist. Best practice is to store values into workfile, if possible.\n\n        Args:\n            data (dict): New data as are.\n            changes (dict): Only data that has been changed. Each value has\n                tuple with '(<old>, <new>)' value.\n        \"\"\"\n\n        pass\n\n\nclass INewPublisher(IPublishHost):\n    \"\"\"Legacy interface replaced by 'IPublishHost'.\n\n    Deprecated:\n        'INewPublisher' is replaced by 'IPublishHost' please change your\n        imports.\n        There is no \"reasonable\" way hot mark these classes as deprecated\n        to show warning of wrong import. Deprecated since 3.14.* will be\n        removed in 3.15.*\n    \"\"\"\n\n    pass\n"
  },
  {
    "path": "openpype/hosts/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/aftereffects/__init__.py",
    "content": "from .addon import AfterEffectsAddon\n\n\n__all__ = (\n    \"AfterEffectsAddon\",\n)\n"
  },
  {
    "path": "openpype/hosts/aftereffects/addon.py",
    "content": "from openpype.modules import OpenPypeModule, IHostAddon\n\n\nclass AfterEffectsAddon(OpenPypeModule, IHostAddon):\n    name = \"aftereffects\"\n    host_name = \"aftereffects\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        \"\"\"Modify environments to contain all required for implementation.\"\"\"\n        defaults = {\n            \"OPENPYPE_LOG_NO_COLORS\": \"True\",\n            \"WEBSOCKET_URL\": \"ws://localhost:8097/ws/\"\n        }\n        for key, value in defaults.items():\n            if not env.get(key):\n                env[key] = value\n\n    def get_workfile_extensions(self):\n        return [\".aep\"]\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/README.md",
    "content": "# AfterEffects Integration\n\nRequirements: This extension requires use of Javascript engine, which is\navailable since CC 16.0.\nPlease check your File>Project Settings>Expressions>Expressions Engine\n\n## Setup\n\nThe After Effects integration requires two components to work; `extension` and `server`.\n\n### Extension\n\nTo install the extension download [Extension Manager Command Line tool (ExManCmd)](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#option-2---exmancmd).\n\n```\nExManCmd /install {path to addon}/api/extension.zxp\n```\nOR\ndownload [Anastasiy’s Extension Manager](https://install.anastasiy.com/)\n\n`{path to addon}` will be most likely in your AppData (on Windows, in your user data folder in Linux and MacOS.)\n\n### Server\n\nThe easiest way to get the server and After Effects launch is with:\n\n```\npython -c ^\"import openpype.hosts.photoshop;openpype.hosts..aftereffects.launch(\"\"c:\\Program Files\\Adobe\\Adobe After Effects 2020\\Support Files\\AfterFX.exe\"\")^\"\n```\n\n`avalon.aftereffects.launch` launches the application and server, and also closes the server when After Effects exists.\n\n## Usage\n\nThe After Effects extension can be found under `Window > Extensions > AYON`. Once launched you should be presented with a panel like this:\n\n![Ayon Panel](panel.png \"Ayon Panel\")\n\n\n## Developing\n\n### Extension\nWhen developing the extension you can load it [unsigned](https://github.com/Adobe-CEP/CEP-Resources/blob/master/CEP_9.x/Documentation/CEP%209.0%20HTML%20Extension%20Cookbook.md#debugging-unsigned-extensions).\n\nWhen signing the extension you can use this [guide](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#package-distribute-install-guide).\n\n```\nZXPSignCmd -selfSignedCert NA NA Ayon Avalon-After-Effects Ayon extension.p12\nZXPSignCmd -sign {path to addon}/api/extension {path to addon}/api/extension.zxp extension.p12 Ayon\n```\n\n### Plugin Examples\n\nThese plugins were made with the [polly config](https://github.com/mindbender-studio/config). To fully integrate and load, you will have to use this config and add `image` to the [integration plugin](https://github.com/mindbender-studio/config/blob/master/polly/plugins/publish/integrate_asset.py).\n\nExpected deployed extension location on default Windows:\n`c:\\Program Files (x86)\\Common Files\\Adobe\\CEP\\extensions\\io.ynput.AE.panel`\n\nFor easier debugging of Javascript:\nhttps://community.adobe.com/t5/download-install/adobe-extension-debuger-problem/td-p/10911704?page=1\nAdd (optional) --enable-blink-features=ShadowDOMV0,CustomElementsV0 when starting Chrome\nthen localhost:8092\n\nOr use Visual Studio Code https://medium.com/adobetech/extendscript-debugger-for-visual-studio-code-public-release-a2ff6161fa01\n## Resources\n  - https://javascript-tools-guide.readthedocs.io/introduction/index.html\n  - https://github.com/Adobe-CEP/Getting-Started-guides\n  - https://github.com/Adobe-CEP/CEP-Resources\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/__init__.py",
    "content": "\"\"\"Public API\n\nAnything that isn't defined here is INTERNAL and unreliable for external use.\n\n\"\"\"\n\nfrom .ws_stub import (\n    get_stub,\n)\n\nfrom .pipeline import (\n    AfterEffectsHost,\n    ls,\n    containerise\n)\n\nfrom .lib import (\n    maintained_selection,\n    get_extension_manifest_path,\n    get_asset_settings,\n    set_settings\n)\n\nfrom .plugin import (\n    AfterEffectsLoader\n)\n\n\n__all__ = [\n    # ws_stub\n    \"get_stub\",\n\n    # pipeline\n    \"ls\",\n    \"containerise\",\n\n    # lib\n    \"maintained_selection\",\n    \"get_extension_manifest_path\",\n    \"get_asset_settings\",\n    \"set_settings\",\n\n    # plugin\n    \"AfterEffectsLoader\"\n]\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/extension/.debug",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ExtensionList>\n    <Extension Id=\"io.ynput.AE.panel\">\n        <HostList>\n\n            <!-- Comment Host tags according to the apps you want your panel to support -->\n\n            <!-- Photoshop -->\n            <Host Name=\"PHXS\" Port=\"8088\"/>\n\n            <!-- Illustrator -->\n            <Host Name=\"ILST\" Port=\"8089\"/>\n\n            <!-- InDesign -->\n            <Host Name=\"IDSN\" Port=\"8090\" />\n\n            <!-- Premiere -->\n            <Host Name=\"PPRO\" Port=\"8091\" />\n\n            <!-- AfterEffects -->\n            <Host Name=\"AEFT\" Port=\"8092\" />\n\n            <!-- PRELUDE -->\n            <Host Name=\"PRLD\" Port=\"8093\" />\n\n            <!-- FLASH Pro -->\n            <Host Name=\"FLPR\" Port=\"8094\" />\n\n        </HostList>\n    </Extension>\n</ExtensionList>\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ExtensionManifest Version=\"8.0\" ExtensionBundleId=\"io.ynput.AE.panel\" ExtensionBundleVersion=\"1.1.0\"\n\t\tExtensionBundleName=\"io.ynput.AE.panel\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n\t<ExtensionList>\n\t\t<Extension Id=\"io.ynput.AE.panel\" Version=\"1.0\" />\n\t</ExtensionList>\n\t<ExecutionEnvironment>\n\t\t<HostList>\n            <!-- Uncomment Host tags according to the apps you want your panel to support -->\n            <!-- Photoshop -->\n            <!--<Host Name=\"PHXS\" Version=\"[14.0,19.0]\" /> -->\n            <!-- <Host Name=\"PHSP\" Version=\"[14.0,19.0]\" /> -->\n\n            <!-- Illustrator -->\n            <!-- <Host Name=\"ILST\" Version=\"[18.0,22.0]\" /> -->\n\n            <!-- InDesign -->\n            <!-- <Host Name=\"IDSN\" Version=\"[10.0,13.0]\" /> -->\n\n            <!-- Premiere -->\n            <!-- <Host Name=\"PPRO\" Version=\"[8.0,12.0]\" /> -->\n\n            <!-- AfterEffects -->\n            <Host Name=\"AEFT\" Version=\"[13.0,99.0]\" />\n\n            <!-- PRELUDE -->\n            <!-- <Host Name=\"PRLD\" Version=\"[3.0,7.0]\" />   -->\n\n            <!-- FLASH Pro -->\n            <!-- <Host Name=\"FLPR\" Version=\"[14.0,18.0]\" /> -->\n\n\t\t</HostList>\n\t\t<LocaleList>\n\t\t\t<Locale Code=\"All\" />\n\t\t</LocaleList>\n\t\t<RequiredRuntimeList>\n\t\t\t<RequiredRuntime Name=\"CSXS\" Version=\"9.0\" />\n\t\t</RequiredRuntimeList>\n\t</ExecutionEnvironment>\n\t<DispatchInfoList>\n\t\t<Extension Id=\"io.ynput.AE.panel\">\n\t\t\t<DispatchInfo >\n\t\t\t\t<Resources>\n\t\t\t\t<MainPath>./index.html</MainPath>\n\t\t\t\t<ScriptPath>./jsx/hostscript.jsx</ScriptPath>\n\t\t\t\t</Resources>\n\t\t\t\t<Lifecycle>\n\t\t\t\t\t<AutoVisible>true</AutoVisible>\n\t\t\t\t</Lifecycle>\n\t\t\t\t<UI>\n\t\t\t\t\t<Type>Panel</Type>\n\t\t\t\t\t<Menu>AYON</Menu>\n\t\t\t\t\t<Geometry>\n\t\t\t\t\t\t<Size>\n\t\t\t\t\t\t\t<Height>200</Height>\n\t\t\t\t\t\t\t<Width>100</Width>\n\t\t\t\t\t\t</Size>\n                    <!--<MinSize>\n                            <Height>550</Height>\n                            <Width>400</Width>\n                        </MinSize>\n                        <MaxSize>\n                            <Height>550</Height>\n                            <Width>400</Width>\n                        </MaxSize>-->\n\n\t\t\t\t\t</Geometry>\n\t\t\t\t\t<Icons>\n\t\t\t\t\t\t<Icon Type=\"Normal\">./icons/ayon_logo.png</Icon>\n\t\t\t\t\t\t<Icon Type=\"RollOver\">./icons/iconRollover.png</Icon>\n\t\t\t\t\t\t<Icon Type=\"Disabled\">./icons/iconDisabled.png</Icon>\n\t\t\t\t\t\t<Icon Type=\"DarkNormal\">./icons/iconDarkNormal.png</Icon>\n\t\t\t\t\t\t<Icon Type=\"DarkRollOver\">./icons/iconDarkRollover.png</Icon>\n\t\t\t\t\t</Icons>\n\t\t\t\t</UI>\n\t\t\t</DispatchInfo>\n\t\t</Extension>\n\t</DispatchInfoList>\n</ExtensionManifest>\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/extension/css/boilerplate.css",
    "content": "/*\n * HTML5 ✰ Boilerplate\n *\n * What follows is the result of much research on cross-browser styling.\n * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal,\n * Kroc Camen, and the H5BP dev community and team.\n *\n * Detailed information about this CSS: h5bp.com/css\n *\n * ==|== normalize ==========================================================\n */\n\n\n/* =============================================================================\n   HTML5 display definitions\n   ========================================================================== */\n\narticle, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; }\naudio, canvas, video { display: inline-block; *display: inline; *zoom: 1; }\naudio:not([controls]) { display: none; }\n[hidden] { display: none; }\n\n\n/* =============================================================================\n   Base\n   ========================================================================== */\n\n/*\n * 1. Correct text resizing oddly in IE6/7 when body font-size is set using em units\n * 2. Force vertical scrollbar in non-IE\n * 3. Prevent iOS text size adjust on device orientation change, without disabling user zoom: h5bp.com/g\n */\n\nhtml { font-size: 100%; overflow-y: scroll; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }\n\nbody { margin: 0; font-size: 100%; line-height: 1.231; }\n\nbody, button, input, select, textarea { font-family: helvetica, arial,\"lucida grande\", verdana, \"メイリオ\", \"ＭＳ Ｐゴシック\", sans-serif; color: #222; }\n/*\n * Remove text-shadow in selection highlight: h5bp.com/i\n * These selection declarations have to be separate\n * Also: hot pink! (or customize the background color to match your design)\n */\n\n::selection { text-shadow: none; background-color: highlight; color: highlighttext; }\n\n\n/* =============================================================================\n   Links\n   ========================================================================== */\n\na { color: #00e; }\na:visited { color: #551a8b; }\na:hover { color: #06e; }\na:focus { outline: thin dotted; }\n\n/* Improve readability when focused and hovered in all browsers: h5bp.com/h */\na:hover, a:active { outline: 0; }\n\n\n/* =============================================================================\n   Typography\n   ========================================================================== */\n\nabbr[title] { border-bottom: 1px dotted; }\n\nb, strong { font-weight: bold; }\n\nblockquote { margin: 1em 40px; }\n\ndfn { font-style: italic; }\n\nhr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }\n\nins { background: #ff9; color: #000; text-decoration: none; }\n\nmark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }\n\n/* Redeclare monospace font family: h5bp.com/j */\npre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; }\n\n/* Improve readability of pre-formatted text in all browsers */\npre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }\n\nq { quotes: none; }\nq:before, q:after { content: \"\"; content: none; }\n\nsmall { font-size: 85%; }\n\n/* Position subscript and superscript content without affecting line-height: h5bp.com/k */\nsub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }\nsup { top: -0.5em; }\nsub { bottom: -0.25em; }\n\n\n/* =============================================================================\n   Lists\n   ========================================================================== */\n\nul, ol { margin: 1em 0; padding: 0 0 0 40px; }\ndd { margin: 0 0 0 40px; }\nnav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; }\n\n\n/* =============================================================================\n   Embedded content\n   ========================================================================== */\n\n/*\n * 1. Improve image quality when scaled in IE7: h5bp.com/d\n * 2. Remove the gap between images and borders on image containers: h5bp.com/e\n */\n\nimg { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }\n\n/*\n * Correct overflow not hidden in IE9\n */\n\nsvg:not(:root) { overflow: hidden; }\n\n\n/* =============================================================================\n   Figures\n   ========================================================================== */\n\nfigure { margin: 0; }\n\n\n/* =============================================================================\n   Forms\n   ========================================================================== */\n\nform { margin: 0; }\nfieldset { border: 0; margin: 0; padding: 0; }\n\n/* Indicate that 'label' will shift focus to the associated form element */\nlabel { cursor: pointer; }\n\n/*\n * 1. Correct color not inheriting in IE6/7/8/9\n * 2. Correct alignment displayed oddly in IE6/7\n */\n\nlegend { border: 0; *margin-left: -7px; padding: 0; }\n\n/*\n * 1. Correct font-size not inheriting in all browsers\n * 2. Remove margins in FF3/4 S5 Chrome\n * 3. Define consistent vertical alignment display in all browsers\n */\n\nbutton, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; }\n\n/*\n * 1. Define line-height as normal to match FF3/4 (set using !important in the UA stylesheet)\n */\n\nbutton, input { line-height: normal; }\n\n/*\n * 1. Display hand cursor for clickable form elements\n * 2. Allow styling of clickable form elements in iOS\n * 3. Correct inner spacing displayed oddly in IE7 (doesn't effect IE6)\n */\n\nbutton, input[type=\"button\"], input[type=\"reset\"], input[type=\"submit\"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; }\n\n/*\n * Consistent box sizing and appearance\n */\n\ninput[type=\"checkbox\"], input[type=\"radio\"] { box-sizing: border-box; padding: 0; }\ninput[type=\"search\"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; }\ninput[type=\"search\"]::-webkit-search-decoration { -webkit-appearance: none; }\n\n/*\n * Remove inner padding and border in FF3/4: h5bp.com/l\n */\n\nbutton::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }\n\n/*\n * 1. Remove default vertical scrollbar in IE6/7/8/9\n * 2. Allow only vertical resizing\n */\n\ntextarea { overflow: auto; vertical-align: top; resize: vertical; }\n\n/* Colors for form validity */\ninput:valid, textarea:valid {  }\ninput:invalid, textarea:invalid { background-color: #f0dddd; }\n\n\n/* =============================================================================\n   Tables\n   ========================================================================== */\n\ntable { border-collapse: collapse; border-spacing: 0; }\ntd { vertical-align: top; }\n\n\n/* ==|== primary styles =====================================================\n   Author:\n   ========================================================================== */\n\n/* ==|== media queries ======================================================\n   PLACEHOLDER Media Queries for Responsive Design.\n   These override the primary ('mobile first') styles\n   Modify as content requires.\n   ========================================================================== */\n\n@media only screen and (min-width: 480px) {\n  /* Style adjustments for viewports 480px and over go here */\n\n}\n\n@media only screen and (min-width: 768px) {\n  /* Style adjustments for viewports 768px and over go here */\n\n}\n\n\n\n/* ==|== non-semantic helper classes ========================================\n   Please define your styles before this section.\n   ========================================================================== */\n\n/* For image replacement */\n.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; }\n.ir br { display: none; }\n\n/* Hide from both screenreaders and browsers: h5bp.com/u */\n.hidden { display: none !important; visibility: hidden; }\n\n/* Hide only visually, but have it available for screenreaders: h5bp.com/v */\n.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }\n\n/* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: h5bp.com/p */\n.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; }\n\n/* Hide visually and from screenreaders, but maintain layout */\n.invisible { visibility: hidden; }\n\n/* Contain floats: h5bp.com/q */\n.clearfix:before, .clearfix:after { content: \"\"; display: table; }\n.clearfix:after { clear: both; }\n.clearfix { *zoom: 1; }\n\n\n\n/* ==|== print styles =======================================================\n   Print styles.\n   Inlined to avoid required HTTP connection: h5bp.com/r\n   ========================================================================== */\n\n@media print {\n    * { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } /* Black prints faster: h5bp.com/s */\n    a, a:visited { text-decoration: underline; }\n    a[href]:after { content: \" (\" attr(href) \")\"; }\n    abbr[title]:after { content: \" (\" attr(title) \")\"; }\n    .ir a:after, a[href^=\"javascript:\"]:after, a[href^=\"#\"]:after { content: \"\"; }  /* Don't show links for images, or javascript/internal links */\n    pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }\n    table { display: table-header-group; } /* h5bp.com/t */\n    tr, img { page-break-inside: avoid; }\n    img { max-width: 100% !important; }\n    @page { margin: 0.5cm; }\n    p, h2, h3 { orphans: 3; widows: 3; }\n    h2, h3 { page-break-after: avoid; }\n}\n\n/* reflow reset for -webkit-margin-before: 1em */\np { margin: 0; }\n\nhtml {\n    overflow-y: auto;\n    background-color: transparent;\n    height: 100%;\n}\n\nbody {\n    background: #fff;\n    font: normal 100%;\n    position: relative;\n    height: 100%;\n}\n\nbody, div, img, p, button, input, select, textarea {\n    box-sizing: border-box;\n}\n\n.image {\n    display: block;\n}\n\ninput {\n    cursor: default;\n    display: block;\n}\n\ninput[type=button] {\n    background-color: #e5e9e8;\n    border: 1px solid #9daca9;\n    border-radius: 4px;\n    box-shadow: inset 0 1px #fff;\n    font: inherit;\n    letter-spacing: inherit;\n    text-indent: inherit;\n    color: inherit;\n}\n\ninput[type=button]:hover {\n    background-color: #eff1f1;\n}\n\ninput[type=button]:active {\n    background-color: #d2d6d6;\n    border: 1px solid #9daca9;\n    box-shadow: inset 0 1px rgba(0,0,0,0.1);\n}\n\n/* Reset anchor styles to an unstyled default to be in parity with design surface. It \n   is presumed that most link styles in real-world designs are custom (non-default). */\na, a:visited, a:hover, a:active {\n    color: inherit;\n    text-decoration: inherit;\n}"
  },
  {
    "path": "openpype/hosts/aftereffects/api/extension/css/styles.css",
    "content": "/*Your styles*/\n\n body {\n    margin: 10px;\n}\n\n\n#content {\n    margin-right:auto;\n    margin-left:auto;\n    vertical-align:middle;\n    width:100%;\n}\n\n\n#btn_test{\n    width: 100%;\n}\n\n\n\n\n/*\nThose classes will be edited at runtime with values specified\nby the settings of the CC application\n*/\n.hostFontColor{}\n.hostFontFamily{}\n.hostFontSize{}\n\n/*font family, color and size*/\n.hostFont{}\n/*background color*/\n.hostBgd{}\n/*lighter background color*/\n.hostBgdLight{}\n/*darker background color*/\n.hostBgdDark{}\n/*background color and font*/\n.hostElt{}\n\n\n.hostButton{\n    border:1px solid;\n    border-radius:2px;\n    height:20px;\n    vertical-align:bottom;\n    font-family:inherit;\n    color:inherit;\n    font-size:inherit;\n}"
  },
  {
    "path": "openpype/hosts/aftereffects/api/extension/index.html",
    "content": "<!doctype html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n\n<link rel=\"stylesheet\" href=\"css/topcoat-desktop-dark.min.css\"/>\n<link  id=\"hostStyle\" rel=\"stylesheet\" href=\"css/styles.css\"/>\n\n<style type=\"text/css\">\n  html, body, iframe {\n    width: 100%;\n    height: 100%;\n    border: 0px;\n    margin: 0px;\n    overflow: hidden;\n  }\n  button {width: 100%;}\n</style>\n\n<style>\n  button {width: 100%;}\n  body {margin:0; padding:0; height: 100%;}\n  html {height: 100%;}\n</style>\n\n<title></title>\n    <script src=\"js/libs/jquery-2.0.2.min.js\"></script>\n\n    <script type=text/javascript>\n            $(function() {\n              $(\"a#workfiles-button\").bind(\"click\", function() {\n\n                RPC.call('AfterEffects.workfiles_route').then(function (data) {\n                }, function (error) {\n                    alert(error);\n                });\n              });\n            });\n    </script>\n\n    <script type=text/javascript>\n            $(function() {\n              $(\"a#loader-button\").bind(\"click\", function() {\n                RPC.call('AfterEffects.loader_route').then(function (data) {\n                }, function (error) {\n                    alert(error);\n                });\n              });\n            });\n    </script>\n\n    <script type=text/javascript>\n            $(function() {\n              $(\"a#publish-button\").bind(\"click\", function() {\n                RPC.call('AfterEffects.publish_route').then(function (data) {\n                }, function (error) {\n                    alert(error);\n                });\n              });\n            });\n    </script>\n\n    <script type=text/javascript>\n            $(function() {\n              $(\"a#sceneinventory-button\").bind(\"click\", function() {\n                RPC.call('AfterEffects.sceneinventory_route').then(function (data) {\n                }, function (error) {\n                    alert(error);\n                });\n              });\n            });\n    </script>\n\n    <script type=text/javascript>\n      $(function() {\n        $(\"a#setresolution-button\").bind(\"click\", function() {\n          RPC.call('AfterEffects.setresolution_route').then(function (data) {\n          }, function (error) {\n              alert(error);\n          });\n        });\n      });\n    </script>\n\n    <script type=text/javascript>\n      $(function() {\n        $(\"a#setframes-button\").bind(\"click\", function() {\n          RPC.call('AfterEffects.setframes_route').then(function (data) {\n          }, function (error) {\n              alert(error);\n          });\n        });\n      });\n    </script>\n\n    <script type=text/javascript>\n      $(function() {\n        $(\"a#setall-button\").bind(\"click\", function() {\n          RPC.call('AfterEffects.setall_route').then(function (data) {\n          }, function (error) {\n              alert(error);\n          });\n        });\n      });\n    </script>\n\n    <script type=text/javascript>\n      $(function() {\n        $(\"a#create-placeholder-button\").bind(\"click\", function() {\n          RPC.call('AfterEffects.create_placeholder_route').then(function (data) {\n          }, function (error) {\n              alert(error);\n          });\n        });\n      });\n    </script>\n\n    <script type=text/javascript>\n      $(function() {\n        $(\"a#update-placeholder-button\").bind(\"click\", function() {\n          RPC.call('AfterEffects.update_placeholder_route').then(function (data) {\n          }, function (error) {\n              alert(error);\n          });\n        });\n      });\n    </script>\n\n    <script type=text/javascript>\n      $(function() {\n        $(\"a#build-workfile-button\").bind(\"click\", function() {\n          RPC.call('AfterEffects.build_workfile_template_route').then(function (data) {\n          }, function (error) {\n              alert(error);\n          });\n        });\n      });\n    </script>\n\n    <script type=text/javascript>\n            $(function() {\n              $(\"a#experimental-button\").bind(\"click\", function() {\n                RPC.call('AfterEffects.experimental_tools_route').then(function (data) {\n                }, function (error) {\n                    alert(error);\n                });\n              });\n            });\n    </script>\n\n\n</head>\n\n<body class=\"hostElt\">\n\n    <div id=\"content\">\n\n        <div>\n            <div></div><a href=# id=workfiles-button><button class=\"hostFontSize\">Workfiles...</button></a></div>\n            <div><a href=# id=loader-button><button class=\"hostFontSize\">Load...</button></a></div>\n            <div><a href=# id=publish-button><button class=\"hostFontSize\">Publish...</button></a></div>\n            <div><a href=# id=sceneinventory-button><button class=\"hostFontSize\">Manage...</button></a></div>\n            <div><a href=# id=separator0><button class=\"hostFontSize\">&nbsp;</button></a></div>\n            <div><a href=# id=setresolution-button><button class=\"hostFontSize\">Set Resolution</button></a></div>\n            <div><a href=# id=setframes-button><button class=\"hostFontSize\">Set Frame Range</button></a></div>\n            <div><a href=# id=setall-button><button class=\"hostFontSize\">Apply All Settings</button></a></div>\n            <div><a href=# id=separator1><button class=\"hostFontSize\">&nbsp;</button></a></div>\n            <div><a href=# id=create-placeholder-button><button class=\"hostFontSize\">Create placeholder</button></a></div>\n            <div><a href=# id=update-placeholder-button><button class=\"hostFontSize\">Update placeholder</button></a></div>\n            <div><a href=# id=build-workfile-button><button class=\"hostFontSize\">Build Workfile from template</button></a></div>\n            <div><a href=# id=separator3><button class=\"hostFontSize\">&nbsp;</button></a></div>\n            <div><a href=# id=experimental-button><button class=\"hostFontSize\">Experimental Tools...</button></a></div>\n        </div>\n\n    </div>\n\n    <!-- <script src=\"js/libs/PlayerDebugMode\"></script> -->\n    <script src=\"js/libs/wsrpc.js\"></script>\n    <script src=\"js/libs/loglevel.min.js\"></script>\n    <script  src=\"js/libs/CSInterface.js\"></script>\n\n    <script src=\"js/themeManager.js\"></script>\n    <script src=\"js/main.js\"></script>\n\n\n</body>\n</html>\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/extension/js/libs/CSInterface.js",
    "content": "/**************************************************************************************************\n*\n* ADOBE SYSTEMS INCORPORATED\n* Copyright 2013 Adobe Systems Incorporated\n* All Rights Reserved.\n*\n* NOTICE:  Adobe permits you to use, modify, and distribute this file in accordance with the\n* terms of the Adobe license agreement accompanying it.  If you have received this file from a\n* source other than Adobe, then your use, modification, or distribution of it requires the prior\n* written permission of Adobe.\n*\n**************************************************************************************************/\n\n/** CSInterface - v8.0.0 */\n\n/**\n * Stores constants for the window types supported by the CSXS infrastructure.\n */\nfunction CSXSWindowType()\n{\n}\n\n/** Constant for the CSXS window type Panel. */\nCSXSWindowType._PANEL = \"Panel\";\n\n/** Constant for the CSXS window type Modeless. */\nCSXSWindowType._MODELESS = \"Modeless\";\n\n/** Constant for the CSXS window type ModalDialog. */\nCSXSWindowType._MODAL_DIALOG = \"ModalDialog\";\n\n/** EvalScript error message */\nEvalScript_ErrMessage = \"EvalScript error.\";\n\n/**\n * @class Version\n * Defines a version number with major, minor, micro, and special\n * components. The major, minor and micro values are numeric; the special\n * value can be any string.\n *\n * @param major   The major version component, a positive integer up to nine digits long.\n * @param minor   The minor version component, a positive integer up to nine digits long.\n * @param micro   The micro version component, a positive integer up to nine digits long.\n * @param special The special version component, an arbitrary string.\n *\n * @return A new \\c Version object.\n */\nfunction Version(major, minor, micro, special)\n{\n    this.major = major;\n    this.minor = minor;\n    this.micro = micro;\n    this.special = special;\n}\n\n/**\n * The maximum value allowed for a numeric version component.\n * This reflects the maximum value allowed in PlugPlug and the manifest schema.\n */\nVersion.MAX_NUM = 999999999;\n\n/**\n * @class VersionBound\n * Defines a boundary for a version range, which associates a \\c Version object\n * with a flag for whether it is an inclusive or exclusive boundary.\n *\n * @param version   The \\c #Version object.\n * @param inclusive True if this boundary is inclusive, false if it is exclusive.\n *\n * @return A new \\c VersionBound object.\n */\nfunction VersionBound(version, inclusive)\n{\n    this.version = version;\n    this.inclusive = inclusive;\n}\n\n/**\n * @class VersionRange\n * Defines a range of versions using a lower boundary and optional upper boundary.\n *\n * @param lowerBound The \\c #VersionBound object.\n * @param upperBound The \\c #VersionBound object, or null for a range with no upper boundary.\n *\n * @return A new \\c VersionRange object.\n */\nfunction VersionRange(lowerBound, upperBound)\n{\n    this.lowerBound = lowerBound;\n    this.upperBound = upperBound;\n}\n\n/**\n * @class Runtime\n * Represents a runtime related to the CEP infrastructure.\n * Extensions can declare dependencies on particular\n * CEP runtime versions in the extension manifest.\n *\n * @param name    The runtime name.\n * @param version A \\c #VersionRange object that defines a range of valid versions.\n *\n * @return A new \\c Runtime object.\n */\nfunction Runtime(name, versionRange)\n{\n    this.name = name;\n    this.versionRange = versionRange;\n}\n\n/**\n* @class Extension\n* Encapsulates a CEP-based extension to an Adobe application.\n*\n* @param id              The unique identifier of this extension.\n* @param name            The localizable display name of this extension.\n* @param mainPath        The path of the \"index.html\" file.\n* @param basePath        The base path of this extension.\n* @param windowType          The window type of the main window of this extension.\n                 Valid values are defined by \\c #CSXSWindowType.\n* @param width           The default width in pixels of the main window of this extension.\n* @param height          The default height in pixels of the main window of this extension.\n* @param minWidth        The minimum width in pixels of the main window of this extension.\n* @param minHeight       The minimum height in pixels of the main window of this extension.\n* @param maxWidth        The maximum width in pixels of the main window of this extension.\n* @param maxHeight       The maximum height in pixels of the main window of this extension.\n* @param defaultExtensionDataXml The extension data contained in the default \\c ExtensionDispatchInfo section of the extension manifest.\n* @param specialExtensionDataXml The extension data contained in the application-specific \\c ExtensionDispatchInfo section of the extension manifest.\n* @param requiredRuntimeList     An array of \\c Runtime objects for runtimes required by this extension.\n* @param isAutoVisible       True if this extension is visible on loading.\n* @param isPluginExtension   True if this extension has been deployed in the Plugins folder of the host application.\n*\n* @return A new \\c Extension object.\n*/\nfunction Extension(id, name, mainPath, basePath, windowType, width, height, minWidth, minHeight, maxWidth, maxHeight,\n                   defaultExtensionDataXml, specialExtensionDataXml, requiredRuntimeList, isAutoVisible, isPluginExtension)\n{\n    this.id = id;\n    this.name = name;\n    this.mainPath = mainPath;\n    this.basePath = basePath;\n    this.windowType = windowType;\n    this.width = width;\n    this.height = height;\n    this.minWidth = minWidth;\n    this.minHeight = minHeight;\n    this.maxWidth = maxWidth;\n    this.maxHeight = maxHeight;\n    this.defaultExtensionDataXml = defaultExtensionDataXml;\n    this.specialExtensionDataXml = specialExtensionDataXml;\n    this.requiredRuntimeList = requiredRuntimeList;\n    this.isAutoVisible = isAutoVisible;\n    this.isPluginExtension = isPluginExtension;\n}\n\n/**\n * @class CSEvent\n * A standard JavaScript event, the base class for CEP events.\n *\n * @param type        The name of the event type.\n * @param scope       The scope of event, can be \"GLOBAL\" or \"APPLICATION\".\n * @param appId       The unique identifier of the application that generated the event.\n * @param extensionId     The unique identifier of the extension that generated the event.\n *\n * @return A new \\c CSEvent object\n */\nfunction CSEvent(type, scope, appId, extensionId)\n{\n    this.type = type;\n    this.scope = scope;\n    this.appId = appId;\n    this.extensionId = extensionId;\n}\n\n/** Event-specific data. */\nCSEvent.prototype.data = \"\";\n\n/**\n * @class SystemPath\n * Stores operating-system-specific location constants for use in the\n * \\c #CSInterface.getSystemPath() method.\n * @return A new \\c SystemPath object.\n */\nfunction SystemPath()\n{\n}\n\n/** The path to user data.  */\nSystemPath.USER_DATA = \"userData\";\n\n/** The path to common files for Adobe applications.  */\nSystemPath.COMMON_FILES = \"commonFiles\";\n\n/** The path to the user's default document folder.  */\nSystemPath.MY_DOCUMENTS = \"myDocuments\";\n\n/** @deprecated. Use \\c #SystemPath.Extension.  */\nSystemPath.APPLICATION = \"application\";\n\n/** The path to current extension.  */\nSystemPath.EXTENSION = \"extension\";\n\n/** The path to hosting application's executable.  */\nSystemPath.HOST_APPLICATION = \"hostApplication\";\n\n/**\n * @class ColorType\n * Stores color-type constants.\n */\nfunction ColorType()\n{\n}\n\n/** RGB color type. */\nColorType.RGB = \"rgb\";\n\n/** Gradient color type. */\nColorType.GRADIENT = \"gradient\";\n\n/** Null color type. */\nColorType.NONE = \"none\";\n\n/**\n * @class RGBColor\n * Stores an RGB color with red, green, blue, and alpha values.\n * All values are in the range [0.0 to 255.0]. Invalid numeric values are\n * converted to numbers within this range.\n *\n * @param red   The red value, in the range [0.0 to 255.0].\n * @param green The green value, in the range [0.0 to 255.0].\n * @param blue  The blue value, in the range [0.0 to 255.0].\n * @param alpha The alpha (transparency) value, in the range [0.0 to 255.0].\n *      The default, 255.0, means that the color is fully opaque.\n *\n * @return A new RGBColor object.\n */\nfunction RGBColor(red, green, blue, alpha)\n{\n    this.red = red;\n    this.green = green;\n    this.blue = blue;\n    this.alpha = alpha;\n}\n\n/**\n * @class Direction\n * A point value  in which the y component is 0 and the x component\n * is positive or negative for a right or left direction,\n * or the x component is 0 and the y component is positive or negative for\n * an up or down direction.\n *\n * @param x     The horizontal component of the point.\n * @param y     The vertical component of the point.\n *\n * @return A new \\c Direction object.\n */\nfunction Direction(x, y)\n{\n    this.x = x;\n    this.y = y;\n}\n\n/**\n * @class GradientStop\n * Stores gradient stop information.\n *\n * @param offset   The offset of the gradient stop, in the range [0.0 to 1.0].\n * @param rgbColor The color of the gradient at this point, an \\c #RGBColor object.\n *\n * @return GradientStop object.\n */\nfunction GradientStop(offset, rgbColor)\n{\n    this.offset = offset;\n    this.rgbColor = rgbColor;\n}\n\n/**\n * @class GradientColor\n * Stores gradient color information.\n *\n * @param type          The gradient type, must be \"linear\".\n * @param direction     A \\c #Direction object for the direction of the gradient\n                (up, down, right, or left).\n * @param numStops          The number of stops in the gradient.\n * @param gradientStopList  An array of \\c #GradientStop objects.\n *\n * @return A new \\c GradientColor object.\n */\nfunction GradientColor(type, direction, numStops, arrGradientStop)\n{\n    this.type = type;\n    this.direction = direction;\n    this.numStops = numStops;\n    this.arrGradientStop = arrGradientStop;\n}\n\n/**\n * @class UIColor\n * Stores color information, including the type, anti-alias level, and specific color\n * values in a color object of an appropriate type.\n *\n * @param type          The color type, 1 for \"rgb\" and 2 for \"gradient\".\n                The supplied color object must correspond to this type.\n * @param antialiasLevel    The anti-alias level constant.\n * @param color         A \\c #RGBColor or \\c #GradientColor object containing specific color information.\n *\n * @return A new \\c UIColor object.\n */\nfunction UIColor(type, antialiasLevel, color)\n{\n    this.type = type;\n    this.antialiasLevel = antialiasLevel;\n    this.color = color;\n}\n\n/**\n * @class AppSkinInfo\n * Stores window-skin properties, such as color and font. All color parameter values are \\c #UIColor objects except that systemHighlightColor is \\c #RGBColor object.\n *\n * @param baseFontFamily        The base font family of the application.\n * @param baseFontSize          The base font size of the application.\n * @param appBarBackgroundColor     The application bar background color.\n * @param panelBackgroundColor      The background color of the extension panel.\n * @param appBarBackgroundColorSRGB     The application bar background color, as sRGB.\n * @param panelBackgroundColorSRGB      The background color of the extension panel, as sRGB.\n * @param systemHighlightColor          The highlight color of the extension panel, if provided by the host application. Otherwise, the operating-system highlight color. \n *\n * @return AppSkinInfo object.\n */\nfunction AppSkinInfo(baseFontFamily, baseFontSize, appBarBackgroundColor, panelBackgroundColor, appBarBackgroundColorSRGB, panelBackgroundColorSRGB, systemHighlightColor)\n{\n    this.baseFontFamily = baseFontFamily;\n    this.baseFontSize = baseFontSize;\n    this.appBarBackgroundColor = appBarBackgroundColor;\n    this.panelBackgroundColor = panelBackgroundColor;\n    this.appBarBackgroundColorSRGB = appBarBackgroundColorSRGB;\n    this.panelBackgroundColorSRGB = panelBackgroundColorSRGB;\n    this.systemHighlightColor = systemHighlightColor;\n}\n\n/**\n * @class HostEnvironment\n * Stores information about the environment in which the extension is loaded.\n *\n * @param appName   The application's name.\n * @param appVersion    The application's version.\n * @param appLocale The application's current license locale.\n * @param appUILocale   The application's current UI locale.\n * @param appId     The application's unique identifier.\n * @param isAppOnline  True if the application is currently online.\n * @param appSkinInfo   An \\c #AppSkinInfo object containing the application's default color and font styles.\n *\n * @return A new \\c HostEnvironment object.\n */\nfunction HostEnvironment(appName, appVersion, appLocale, appUILocale, appId, isAppOnline, appSkinInfo)\n{\n    this.appName = appName;\n    this.appVersion = appVersion;\n    this.appLocale = appLocale;\n    this.appUILocale = appUILocale;\n    this.appId = appId;\n    this.isAppOnline = isAppOnline;\n    this.appSkinInfo = appSkinInfo;\n}\n\n/**\n * @class HostCapabilities\n * Stores information about the host capabilities.\n *\n * @param EXTENDED_PANEL_MENU True if the application supports panel menu.\n * @param EXTENDED_PANEL_ICONS True if the application supports panel icon.\n * @param DELEGATE_APE_ENGINE True if the application supports delegated APE engine.\n * @param SUPPORT_HTML_EXTENSIONS True if the application supports HTML extensions.\n * @param DISABLE_FLASH_EXTENSIONS True if the application disables FLASH extensions.\n *\n * @return A new \\c HostCapabilities object.\n */\nfunction HostCapabilities(EXTENDED_PANEL_MENU, EXTENDED_PANEL_ICONS, DELEGATE_APE_ENGINE, SUPPORT_HTML_EXTENSIONS, DISABLE_FLASH_EXTENSIONS)\n{\n    this.EXTENDED_PANEL_MENU = EXTENDED_PANEL_MENU;\n    this.EXTENDED_PANEL_ICONS = EXTENDED_PANEL_ICONS;\n    this.DELEGATE_APE_ENGINE = DELEGATE_APE_ENGINE;\n    this.SUPPORT_HTML_EXTENSIONS = SUPPORT_HTML_EXTENSIONS;\n\tthis.DISABLE_FLASH_EXTENSIONS = DISABLE_FLASH_EXTENSIONS; // Since 5.0.0\n}\n\n/**\n * @class ApiVersion\n * Stores current api version.\n *\n * Since 4.2.0\n *\n * @param major  The major version\n * @param minor  The minor version.\n * @param micro  The micro version.\n *\n * @return ApiVersion object.\n */\nfunction ApiVersion(major, minor, micro)\n{\n    this.major = major;\n    this.minor = minor;\n    this.micro = micro;\n}\n\n/**\n * @class MenuItemStatus\n * Stores flyout menu item status\n *\n * Since 5.2.0\n *\n * @param menuItemLabel  The menu item label.\n * @param enabled  \t\t True if user wants to enable the menu item.\n * @param checked  \t\t True if user wants to check the menu item.\n *\n * @return MenuItemStatus object.\n */\nfunction MenuItemStatus(menuItemLabel, enabled, checked)\n{\n\tthis.menuItemLabel = menuItemLabel;\n\tthis.enabled = enabled;\n\tthis.checked = checked;\n}\n\n/**\n * @class ContextMenuItemStatus\n * Stores the status of the context menu item.\n *\n * Since 5.2.0\n *\n * @param menuItemID     The menu item id.\n * @param enabled  \t\t True if user wants to enable the menu item.\n * @param checked  \t\t True if user wants to check the menu item.\n *\n * @return MenuItemStatus object.\n */\nfunction ContextMenuItemStatus(menuItemID, enabled, checked)\n{\n\tthis.menuItemID = menuItemID;\n\tthis.enabled = enabled;\n\tthis.checked = checked;\n}\n//------------------------------ CSInterface ----------------------------------\n\n/**\n * @class CSInterface\n * This is the entry point to the CEP extensibility infrastructure.\n * Instantiate this object and use it to:\n * <ul>\n * <li>Access information about the host application in which an extension is running</li>\n * <li>Launch an extension</li>\n * <li>Register interest in event notifications, and dispatch events</li>\n * </ul>\n *\n * @return A new \\c CSInterface object\n */\nfunction CSInterface()\n{\n}\n\n/**\n * User can add this event listener to handle native application theme color changes.\n * Callback function gives extensions ability to fine-tune their theme color after the\n * global theme color has been changed.\n * The callback function should be like below:\n *\n * @example\n * // event is a CSEvent object, but user can ignore it.\n * function OnAppThemeColorChanged(event)\n * {\n *    // Should get a latest HostEnvironment object from application.\n *    var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo;\n *    // Gets the style information such as color info from the skinInfo,\n *    // and redraw all UI controls of your extension according to the style info.\n * }\n */\nCSInterface.THEME_COLOR_CHANGED_EVENT = \"com.adobe.csxs.events.ThemeColorChanged\";\n\n/** The host environment data object. */\nCSInterface.prototype.hostEnvironment = window.__adobe_cep__ ? JSON.parse(window.__adobe_cep__.getHostEnvironment()) : null;\n\n/** Retrieves information about the host environment in which the\n *  extension is currently running.\n *\n *   @return A \\c #HostEnvironment object.\n */\nCSInterface.prototype.getHostEnvironment = function()\n{\n    this.hostEnvironment = JSON.parse(window.__adobe_cep__.getHostEnvironment());\n    return this.hostEnvironment;\n};\n\n/** Closes this extension. */\nCSInterface.prototype.closeExtension = function()\n{\n    window.__adobe_cep__.closeExtension();\n};\n\n/**\n * Retrieves a path for which a constant is defined in the system.\n *\n * @param pathType The path-type constant defined in \\c #SystemPath ,\n *\n * @return The platform-specific system path string.\n */\nCSInterface.prototype.getSystemPath = function(pathType)\n{\n    var path = decodeURI(window.__adobe_cep__.getSystemPath(pathType));\n    var OSVersion = this.getOSInformation();\n    if (OSVersion.indexOf(\"Windows\") >= 0)\n    {\n      path = path.replace(\"file:///\", \"\");\n    }\n    else if (OSVersion.indexOf(\"Mac\") >= 0)\n    {\n      path = path.replace(\"file://\", \"\");\n    }\n    return path;\n};\n\n/**\n * Evaluates a JavaScript script, which can use the JavaScript DOM\n * of the host application.\n *\n * @param script    The JavaScript script.\n * @param callback  Optional. A callback function that receives the result of execution.\n *          If execution fails, the callback function receives the error message \\c EvalScript_ErrMessage.\n */\nCSInterface.prototype.evalScript = function(script, callback)\n{\n    if(callback === null || callback === undefined)\n    {\n        callback = function(result){};\n    }\n    window.__adobe_cep__.evalScript(script, callback);\n};\n\n/**\n * Retrieves the unique identifier of the application.\n * in which the extension is currently running.\n *\n * @return The unique ID string.\n */\nCSInterface.prototype.getApplicationID = function()\n{\n    var appId = this.hostEnvironment.appId;\n    return appId;\n};\n\n/**\n * Retrieves host capability information for the application\n * in which the extension is currently running.\n *\n * @return A \\c #HostCapabilities object.\n */\nCSInterface.prototype.getHostCapabilities = function()\n{\n    var hostCapabilities = JSON.parse(window.__adobe_cep__.getHostCapabilities() );\n    return hostCapabilities;\n};\n\n/**\n * Triggers a CEP event programmatically. Yoy can use it to dispatch\n * an event of a predefined type, or of a type you have defined.\n *\n * @param event A \\c CSEvent object.\n */\nCSInterface.prototype.dispatchEvent = function(event)\n{\n    if (typeof event.data == \"object\")\n    {\n        event.data = JSON.stringify(event.data);\n    }\n\n    window.__adobe_cep__.dispatchEvent(event);\n};\n\n/**\n * Registers an interest in a CEP event of a particular type, and\n * assigns an event handler.\n * The event infrastructure notifies your extension when events of this type occur,\n * passing the event object to the registered handler function.\n *\n * @param type     The name of the event type of interest.\n * @param listener The JavaScript handler function or method.\n * @param obj      Optional, the object containing the handler method, if any.\n *         Default is null.\n */\nCSInterface.prototype.addEventListener = function(type, listener, obj)\n{\n    window.__adobe_cep__.addEventListener(type, listener, obj);\n};\n\n/**\n * Removes a registered event listener.\n *\n * @param type      The name of the event type of interest.\n * @param listener  The JavaScript handler function or method that was registered.\n * @param obj       Optional, the object containing the handler method, if any.\n *          Default is null.\n */\nCSInterface.prototype.removeEventListener = function(type, listener, obj)\n{\n    window.__adobe_cep__.removeEventListener(type, listener, obj);\n};\n\n/**\n * Loads and launches another extension, or activates the extension if it is already loaded.\n *\n * @param extensionId       The extension's unique identifier.\n * @param startupParams     Not currently used, pass \"\".\n *\n * @example\n * To launch the extension \"help\" with ID \"HLP\" from this extension, call:\n * <code>requestOpenExtension(\"HLP\", \"\"); </code>\n *\n */\nCSInterface.prototype.requestOpenExtension = function(extensionId, params)\n{\n    window.__adobe_cep__.requestOpenExtension(extensionId, params);\n};\n\n/**\n * Retrieves the list of extensions currently loaded in the current host application.\n * The extension list is initialized once, and remains the same during the lifetime\n * of the CEP session.\n *\n * @param extensionIds  Optional, an array of unique identifiers for extensions of interest.\n *          If omitted, retrieves data for all extensions.\n *\n * @return Zero or more \\c #Extension objects.\n */\nCSInterface.prototype.getExtensions = function(extensionIds)\n{\n    var extensionIdsStr = JSON.stringify(extensionIds);\n    var extensionsStr = window.__adobe_cep__.getExtensions(extensionIdsStr);\n\n    var extensions = JSON.parse(extensionsStr);\n    return extensions;\n};\n\n/**\n * Retrieves network-related preferences.\n *\n * @return A JavaScript object containing network preferences.\n */\nCSInterface.prototype.getNetworkPreferences = function()\n{\n    var result = window.__adobe_cep__.getNetworkPreferences();\n    var networkPre = JSON.parse(result);\n\n    return networkPre;\n};\n\n/**\n * Initializes the resource bundle for this extension with property values\n * for the current application and locale.\n * To support multiple locales, you must define a property file for each locale,\n * containing keyed display-string values for that locale.\n * See localization documentation for Extension Builder and related products.\n *\n * Keys can be in the\n * form <code>key.value=\"localized string\"</code>, for use in HTML text elements.\n * For example, in this input element, the localized \\c key.value string is displayed\n * instead of the empty \\c value string:\n *\n * <code><input type=\"submit\" value=\"\" data-locale=\"key\"/></code>\n *\n * @return An object containing the resource bundle information.\n */\nCSInterface.prototype.initResourceBundle = function()\n{\n    var resourceBundle = JSON.parse(window.__adobe_cep__.initResourceBundle());\n    var resElms = document.querySelectorAll('[data-locale]');\n    for (var n = 0; n < resElms.length; n++)\n    {\n       var resEl = resElms[n];\n       // Get the resource key from the element.\n       var resKey = resEl.getAttribute('data-locale');\n       if (resKey)\n       {\n           // Get all the resources that start with the key.\n           for (var key in resourceBundle)\n           {\n               if (key.indexOf(resKey) === 0)\n               {\n                   var resValue = resourceBundle[key];\n                   if (key.length == resKey.length)\n                   {\n                        resEl.innerHTML = resValue;\n                   }\n                   else if ('.' == key.charAt(resKey.length))\n                   {\n                        var attrKey = key.substring(resKey.length + 1);\n                        resEl[attrKey] = resValue;\n                   }\n               }\n           }\n       }\n    }\n    return resourceBundle;\n};\n\n/**\n * Writes installation information to a file.\n *\n * @return The file path.\n */\nCSInterface.prototype.dumpInstallationInfo = function()\n{\n    return window.__adobe_cep__.dumpInstallationInfo();\n};\n\n/**\n * Retrieves version information for the current Operating System,\n * See http://www.useragentstring.com/pages/Chrome/ for Chrome \\c navigator.userAgent values.\n *\n * @return A string containing the OS version, or \"unknown Operation System\".\n * If user customizes the User Agent by setting CEF command parameter \"--user-agent\", only\n * \"Mac OS X\" or \"Windows\" will be returned. \n */\nCSInterface.prototype.getOSInformation = function()\n{\n    var userAgent = navigator.userAgent;\n\n    if ((navigator.platform == \"Win32\") || (navigator.platform == \"Windows\"))\n    {\n        var winVersion = \"Windows\";\n        var winBit = \"\";\n        if (userAgent.indexOf(\"Windows\") > -1)\n        {\n            if (userAgent.indexOf(\"Windows NT 5.0\") > -1)\n            {\n                winVersion = \"Windows 2000\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 5.1\") > -1)\n            {\n                winVersion = \"Windows XP\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 5.2\") > -1)\n            {\n                winVersion = \"Windows Server 2003\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 6.0\") > -1)\n            {\n                winVersion = \"Windows Vista\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 6.1\") > -1)\n            {\n                winVersion = \"Windows 7\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 6.2\") > -1)\n            {\n                winVersion = \"Windows 8\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 6.3\") > -1)\n            {\n                winVersion = \"Windows 8.1\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 10\") > -1)\n            {\n                winVersion = \"Windows 10\";\n            }\n\n            if (userAgent.indexOf(\"WOW64\") > -1 || userAgent.indexOf(\"Win64\") > -1)\n            {\n                winBit = \" 64-bit\";\n            }\n            else\n            {\n                winBit = \" 32-bit\";\t\t\t\n            }\n        }\n\n        return winVersion + winBit;\n    }\n    else if ((navigator.platform == \"MacIntel\") || (navigator.platform == \"Macintosh\"))\n    {        \n        var result = \"Mac OS X\";\n\n        if (userAgent.indexOf(\"Mac OS X\") > -1)\n        {\n            result = userAgent.substring(userAgent.indexOf(\"Mac OS X\"), userAgent.indexOf(\")\"));\n            result = result.replace(/_/g, \".\");\n        }\n\n        return result;        \n    }\n\n    return \"Unknown Operation System\";\n};\n\n/**\n * Opens a page in the default system browser.\n *\n * Since 4.2.0\n *\n * @param url  The URL of the page/file to open, or the email address.\n * Must use HTTP/HTTPS/file/mailto protocol. For example:\n *   \"http://www.adobe.com\"\n *   \"https://github.com\"\n *   \"file:///C:/log.txt\"\n *   \"mailto:test@adobe.com\"\n *\n * @return One of these error codes:\\n\n *      <ul>\\n\n *          <li>NO_ERROR - 0</li>\\n\n *          <li>ERR_UNKNOWN - 1</li>\\n\n *          <li>ERR_INVALID_PARAMS - 2</li>\\n\n *          <li>ERR_INVALID_URL - 201</li>\\n\n *      </ul>\\n\n */\nCSInterface.prototype.openURLInDefaultBrowser = function(url)\n{\n    return cep.util.openURLInDefaultBrowser(url);\n};\n\n/**\n * Retrieves extension ID.\n *\n * Since 4.2.0\n *\n * @return extension ID.\n */\nCSInterface.prototype.getExtensionID = function()\n{\n     return window.__adobe_cep__.getExtensionId();\n};\n\n/**\n * Retrieves the scale factor of screen. \n * On Windows platform, the value of scale factor might be different from operating system's scale factor,\n * since host application may use its self-defined scale factor.\n *\n * Since 4.2.0\n *\n * @return One of the following float number.\n *      <ul>\\n\n *          <li> -1.0 when error occurs </li>\\n\n *          <li> 1.0 means normal screen </li>\\n\n *          <li> >1.0 means HiDPI screen </li>\\n\n *      </ul>\\n\n */\nCSInterface.prototype.getScaleFactor = function()\n{\n    return window.__adobe_cep__.getScaleFactor();\n};\n\n/**\n * Set a handler to detect any changes of scale factor. This only works on Mac.\n *\n * Since 4.2.0\n *\n * @param handler   The function to be called when scale factor is changed.\n *\n */\nCSInterface.prototype.setScaleFactorChangedHandler = function(handler)\n{\n    window.__adobe_cep__.setScaleFactorChangedHandler(handler);\n};\n\n/**\n * Retrieves current API version.\n *\n * Since 4.2.0\n *\n * @return ApiVersion object.\n *\n */\nCSInterface.prototype.getCurrentApiVersion = function()\n{\n    var apiVersion = JSON.parse(window.__adobe_cep__.getCurrentApiVersion());\n    return apiVersion;\n};\n\n/**\n * Set panel flyout menu by an XML.\n *\n * Since 5.2.0\n *\n * Register a callback function for \"com.adobe.csxs.events.flyoutMenuClicked\" to get notified when a \n * menu item is clicked.\n * The \"data\" attribute of event is an object which contains \"menuId\" and \"menuName\" attributes. \n *\n * Register callback functions for \"com.adobe.csxs.events.flyoutMenuOpened\" and \"com.adobe.csxs.events.flyoutMenuClosed\"\n * respectively to get notified when flyout menu is opened or closed.\n *\n * @param menu     A XML string which describes menu structure.\n * An example menu XML:\n * <Menu>\n *   <MenuItem Id=\"menuItemId1\" Label=\"TestExample1\" Enabled=\"true\" Checked=\"false\"/>\n *   <MenuItem Label=\"TestExample2\">\n *     <MenuItem Label=\"TestExample2-1\" >\n *       <MenuItem Label=\"TestExample2-1-1\" Enabled=\"false\" Checked=\"true\"/>\n *     </MenuItem>\n *     <MenuItem Label=\"TestExample2-2\" Enabled=\"true\" Checked=\"true\"/>\n *   </MenuItem>\n *   <MenuItem Label=\"---\" />\n *   <MenuItem Label=\"TestExample3\" Enabled=\"false\" Checked=\"false\"/>\n * </Menu>\n *\n */\nCSInterface.prototype.setPanelFlyoutMenu = function(menu)\n{\n    if (\"string\" != typeof menu)\n    {\n        return;\t\n    }\n\n\twindow.__adobe_cep__.invokeSync(\"setPanelFlyoutMenu\", menu);\n};\n\n/**\n * Updates a menu item in the extension window's flyout menu, by setting the enabled\n * and selection status.\n *  \n * Since 5.2.0\n *\n * @param menuItemLabel\tThe menu item label. \n * @param enabled\t\tTrue to enable the item, false to disable it (gray it out).\n * @param checked\t\tTrue to select the item, false to deselect it.\n *\n * @return false when the host application does not support this functionality (HostCapabilities.EXTENDED_PANEL_MENU is false). \n *         Fails silently if menu label is invalid.\n *\n * @see HostCapabilities.EXTENDED_PANEL_MENU\n */\nCSInterface.prototype.updatePanelMenuItem = function(menuItemLabel, enabled, checked)\n{\n\tvar ret = false;\n\tif (this.getHostCapabilities().EXTENDED_PANEL_MENU) \n\t{\n\t\tvar itemStatus = new MenuItemStatus(menuItemLabel, enabled, checked);\n\t\tret = window.__adobe_cep__.invokeSync(\"updatePanelMenuItem\", JSON.stringify(itemStatus));\n\t}\n\treturn ret;\n};\n\n\n/**\n * Set context menu by XML string.\n *\n * Since 5.2.0\n *\n * There are a number of conventions used to communicate what type of menu item to create and how it should be handled.\n * - an item without menu ID or menu name is disabled and is not shown.\n * - if the item name is \"---\" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL.\n * - Checkable attribute takes precedence over Checked attribute.\n * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. \n     The Chrome extension contextMenus API was taken as a reference. \n     https://developer.chrome.com/extensions/contextMenus\n * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter.\n *\n * @param menu      A XML string which describes menu structure.\n * @param callback  The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item.\n *\n * @description An example menu XML:\n * <Menu>\n *   <MenuItem Id=\"menuItemId1\" Label=\"TestExample1\" Enabled=\"true\" Checkable=\"true\" Checked=\"false\" Icon=\"./image/small_16X16.png\"/>\n *   <MenuItem Id=\"menuItemId2\" Label=\"TestExample2\">\n *     <MenuItem Id=\"menuItemId2-1\" Label=\"TestExample2-1\" >\n *       <MenuItem Id=\"menuItemId2-1-1\" Label=\"TestExample2-1-1\" Enabled=\"false\" Checkable=\"true\" Checked=\"true\"/>\n *     </MenuItem>\n *     <MenuItem Id=\"menuItemId2-2\" Label=\"TestExample2-2\" Enabled=\"true\" Checkable=\"true\" Checked=\"true\"/>\n *   </MenuItem>\n *   <MenuItem Label=\"---\" />\n *   <MenuItem Id=\"menuItemId3\" Label=\"TestExample3\" Enabled=\"false\" Checkable=\"true\" Checked=\"false\"/>\n * </Menu>\n */\nCSInterface.prototype.setContextMenu = function(menu, callback)\n{\n    if (\"string\" != typeof menu)\n    {\n        return;\n    }\n    \n\twindow.__adobe_cep__.invokeAsync(\"setContextMenu\", menu, callback);\n};\n\n/**\n * Set context menu by JSON string.\n *\n * Since 6.0.0\n *\n * There are a number of conventions used to communicate what type of menu item to create and how it should be handled.\n * - an item without menu ID or menu name is disabled and is not shown.\n * - if the item label is \"---\" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL.\n * - Checkable attribute takes precedence over Checked attribute.\n * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. \n     The Chrome extension contextMenus API was taken as a reference.\n * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter.\n     https://developer.chrome.com/extensions/contextMenus\n *\n * @param menu      A JSON string which describes menu structure.\n * @param callback  The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item.\n *\n * @description An example menu JSON:\n *\n * { \n *      \"menu\": [\n *          {\n *              \"id\": \"menuItemId1\",\n *              \"label\": \"testExample1\",\n *              \"enabled\": true,\n *              \"checkable\": true,\n *              \"checked\": false,\n *              \"icon\": \"./image/small_16X16.png\"\n *          },\n *          {\n *              \"id\": \"menuItemId2\",\n *              \"label\": \"testExample2\",\n *              \"menu\": [\n *                  {\n *                      \"id\": \"menuItemId2-1\",\n *                      \"label\": \"testExample2-1\",\n *                      \"menu\": [\n *                          {\n *                              \"id\": \"menuItemId2-1-1\",\n *                              \"label\": \"testExample2-1-1\",\n *                              \"enabled\": false,\n *                              \"checkable\": true,\n *                              \"checked\": true\n *                          }\n *                      ]\n *                  },\n *                  {\n *                      \"id\": \"menuItemId2-2\",\n *                      \"label\": \"testExample2-2\",\n *                      \"enabled\": true,\n *                      \"checkable\": true,\n *                      \"checked\": true\n *                  }\n *              ]\n *          },\n *          {\n *              \"label\": \"---\"\n *          },\n *          {\n *              \"id\": \"menuItemId3\",\n *              \"label\": \"testExample3\",\n *              \"enabled\": false,\n *              \"checkable\": true,\n *              \"checked\": false\n *          }\n *      ]\n *  }\n *\n */\nCSInterface.prototype.setContextMenuByJSON = function(menu, callback)\n{\n    if (\"string\" != typeof menu)\n    {\n        return;\t\n    }\n    \n\twindow.__adobe_cep__.invokeAsync(\"setContextMenuByJSON\", menu, callback);\n};\n\n/**\n * Updates a context menu item by setting the enabled and selection status.\n *  \n * Since 5.2.0\n *\n * @param menuItemID\tThe menu item ID. \n * @param enabled\t\tTrue to enable the item, false to disable it (gray it out).\n * @param checked\t\tTrue to select the item, false to deselect it.\n */\nCSInterface.prototype.updateContextMenuItem = function(menuItemID, enabled, checked)\n{\n\tvar itemStatus = new ContextMenuItemStatus(menuItemID, enabled, checked);\n\tret = window.__adobe_cep__.invokeSync(\"updateContextMenuItem\", JSON.stringify(itemStatus));\n};\n\n/**\n * Get the visibility status of an extension window. \n *  \n * Since 6.0.0\n *\n * @return true if the extension window is visible; false if the extension window is hidden.\n */\nCSInterface.prototype.isWindowVisible = function()\n{\n\treturn window.__adobe_cep__.invokeSync(\"isWindowVisible\", \"\");\n};\n\n/**\n * Resize extension's content to the specified dimensions.\n * 1. Works with modal and modeless extensions in all Adobe products.\n * 2. Extension's manifest min/max size constraints apply and take precedence. \n * 3. For panel extensions\n *    3.1 This works in all Adobe products except:\n *        * Premiere Pro\n *        * Prelude\n *        * After Effects\n *    3.2 When the panel is in certain states (especially when being docked),\n *        it will not change to the desired dimensions even when the\n *        specified size satisfies min/max constraints.\n *\n * Since 6.0.0\n *\n * @param width  The new width\n * @param height The new height\n */\nCSInterface.prototype.resizeContent = function(width, height)\n{\n    window.__adobe_cep__.resizeContent(width, height);\n};\n\n/**\n * Register the invalid certificate callback for an extension. \n * This callback will be triggered when the extension tries to access the web site that contains the invalid certificate on the main frame.\n * But if the extension does not call this function and tries to access the web site containing the invalid certificate, a default error page will be shown.\n *  \n * Since 6.1.0\n *\n * @param callback the callback function\n */\nCSInterface.prototype.registerInvalidCertificateCallback = function(callback)\n{\n    return window.__adobe_cep__.registerInvalidCertificateCallback(callback);\n};\n\n/**\n * Register an interest in some key events to prevent them from being sent to the host application.\n *\n * This function works with modeless extensions and panel extensions. \n * Generally all the key events will be sent to the host application for these two extensions if the current focused element\n * is not text input or dropdown,\n * If you want to intercept some key events and want them to be handled in the extension, please call this function\n * in advance to prevent them being sent to the host application.\n *\n * Since 6.1.0\n *\n * @param keyEventsInterest      A JSON string describing those key events you are interested in. A null object or\n                                 an empty string will lead to removing the interest\n *\n * This JSON string should be an array, each object has following keys:\n *\n * keyCode:  [Required] represents an OS system dependent virtual key code identifying\n *           the unmodified value of the pressed key.\n * ctrlKey:  [optional] a Boolean that indicates if the control key was pressed (true) or not (false) when the event occurred.\n * altKey:   [optional] a Boolean that indicates if the alt key was pressed (true) or not (false) when the event occurred.\n * shiftKey: [optional] a Boolean that indicates if the shift key was pressed (true) or not (false) when the event occurred.\n * metaKey:  [optional] (Mac Only) a Boolean that indicates if the Meta key was pressed (true) or not (false) when the event occurred.\n *                      On Macintosh keyboards, this is the command key. To detect Windows key on Windows, please use keyCode instead.\n * An example JSON string:\n *\n * [\n *     {\n *         \"keyCode\": 48\n *     },\n *     {\n *         \"keyCode\": 123,\n *         \"ctrlKey\": true\n *     },\n *     {\n *         \"keyCode\": 123,\n *         \"ctrlKey\": true,\n *         \"metaKey\": true\n *     }\n * ]\n *\n */\nCSInterface.prototype.registerKeyEventsInterest = function(keyEventsInterest)\n{\n    return window.__adobe_cep__.registerKeyEventsInterest(keyEventsInterest);\n};\n\n/**\n * Set the title of the extension window. \n * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver.\n *\n * Since 6.1.0\n *\n * @param title The window title.\n */\nCSInterface.prototype.setWindowTitle = function(title)\n{\n    window.__adobe_cep__.invokeSync(\"setWindowTitle\", title);\n};\n\n/**\n * Get the title of the extension window. \n * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver.\n *\n * Since 6.1.0\n *\n * @return The window title.\n */\nCSInterface.prototype.getWindowTitle = function()\n{\n    return window.__adobe_cep__.invokeSync(\"getWindowTitle\", \"\");\n};\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/extension/js/libs/json.js",
    "content": "//  json2.js\n//  2017-06-12\n//  Public Domain.\n//  NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.\n\n//  USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO\n//  NOT CONTROL.\n\n//  This file creates a global JSON object containing two methods: stringify\n//  and parse. This file provides the ES5 JSON capability to ES3 systems.\n//  If a project might run on IE8 or earlier, then this file should be included.\n//  This file does nothing on ES5 systems.\n\n//      JSON.stringify(value, replacer, space)\n//          value       any JavaScript value, usually an object or array.\n//          replacer    an optional parameter that determines how object\n//                      values are stringified for objects. It can be a\n//                      function or an array of strings.\n//          space       an optional parameter that specifies the indentation\n//                      of nested structures. If it is omitted, the text will\n//                      be packed without extra whitespace. If it is a number,\n//                      it will specify the number of spaces to indent at each\n//                      level. If it is a string (such as \"\\t\" or \"&nbsp;\"),\n//                      it contains the characters used to indent at each level.\n//          This method produces a JSON text from a JavaScript value.\n//          When an object value is found, if the object contains a toJSON\n//          method, its toJSON method will be called and the result will be\n//          stringified. A toJSON method does not serialize: it returns the\n//          value represented by the name/value pair that should be serialized,\n//          or undefined if nothing should be serialized. The toJSON method\n//          will be passed the key associated with the value, and this will be\n//          bound to the value.\n\n//          For example, this would serialize Dates as ISO strings.\n\n//              Date.prototype.toJSON = function (key) {\n//                  function f(n) {\n//                      // Format integers to have at least two digits.\n//                      return (n < 10)\n//                          ? \"0\" + n\n//                          : n;\n//                  }\n//                  return this.getUTCFullYear()   + \"-\" +\n//                       f(this.getUTCMonth() + 1) + \"-\" +\n//                       f(this.getUTCDate())      + \"T\" +\n//                       f(this.getUTCHours())     + \":\" +\n//                       f(this.getUTCMinutes())   + \":\" +\n//                       f(this.getUTCSeconds())   + \"Z\";\n//              };\n\n//          You can provide an optional replacer method. It will be passed the\n//          key and value of each member, with this bound to the containing\n//          object. The value that is returned from your method will be\n//          serialized. If your method returns undefined, then the member will\n//          be excluded from the serialization.\n\n//          If the replacer parameter is an array of strings, then it will be\n//          used to select the members to be serialized. It filters the results\n//          such that only members with keys listed in the replacer array are\n//          stringified.\n\n//          Values that do not have JSON representations, such as undefined or\n//          functions, will not be serialized. Such values in objects will be\n//          dropped; in arrays they will be replaced with null. You can use\n//          a replacer function to replace those with JSON values.\n\n//          JSON.stringify(undefined) returns undefined.\n\n//          The optional space parameter produces a stringification of the\n//          value that is filled with line breaks and indentation to make it\n//          easier to read.\n\n//          If the space parameter is a non-empty string, then that string will\n//          be used for indentation. If the space parameter is a number, then\n//          the indentation will be that many spaces.\n\n//          Example:\n\n//          text = JSON.stringify([\"e\", {pluribus: \"unum\"}]);\n//          // text is '[\"e\",{\"pluribus\":\"unum\"}]'\n\n//          text = JSON.stringify([\"e\", {pluribus: \"unum\"}], null, \"\\t\");\n//          // text is '[\\n\\t\"e\",\\n\\t{\\n\\t\\t\"pluribus\": \"unum\"\\n\\t}\\n]'\n\n//          text = JSON.stringify([new Date()], function (key, value) {\n//              return this[key] instanceof Date\n//                  ? \"Date(\" + this[key] + \")\"\n//                  : value;\n//          });\n//          // text is '[\"Date(---current time---)\"]'\n\n//      JSON.parse(text, reviver)\n//          This method parses a JSON text to produce an object or array.\n//          It can throw a SyntaxError exception.\n\n//          The optional reviver parameter is a function that can filter and\n//          transform the results. It receives each of the keys and values,\n//          and its return value is used instead of the original value.\n//          If it returns what it received, then the structure is not modified.\n//          If it returns undefined then the member is deleted.\n\n//          Example:\n\n//          // Parse the text. Values that look like ISO date strings will\n//          // be converted to Date objects.\n\n//          myData = JSON.parse(text, function (key, value) {\n//              var a;\n//              if (typeof value === \"string\") {\n//                  a =\n//   /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2}(?:\\.\\d*)?)Z$/.exec(value);\n//                  if (a) {\n//                      return new Date(Date.UTC(\n//                         +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]\n//                      ));\n//                  }\n//                  return value;\n//              }\n//          });\n\n//          myData = JSON.parse(\n//              \"[\\\"Date(09/09/2001)\\\"]\",\n//              function (key, value) {\n//                  var d;\n//                  if (\n//                      typeof value === \"string\"\n//                      && value.slice(0, 5) === \"Date(\"\n//                      && value.slice(-1) === \")\"\n//                  ) {\n//                      d = new Date(value.slice(5, -1));\n//                      if (d) {\n//                          return d;\n//                      }\n//                  }\n//                  return value;\n//              }\n//          );\n\n//  This is a reference implementation. You are free to copy, modify, or\n//  redistribute.\n\n/*jslint\n    eval, for, this\n*/\n\n/*property\n    JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,\n    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,\n    lastIndex, length, parse, prototype, push, replace, slice, stringify,\n    test, toJSON, toString, valueOf\n*/\n\n\n// Create a JSON object only if one does not already exist. We create the\n// methods in a closure to avoid creating global variables.\n\nif (typeof JSON !== \"object\") {\n    JSON = {};\n}\n\n(function () {\n    \"use strict\";\n\n    var rx_one = /^[\\],:{}\\s]*$/;\n    var rx_two = /\\\\(?:[\"\\\\\\/bfnrt]|u[0-9a-fA-F]{4})/g;\n    var rx_three = /\"[^\"\\\\\\n\\r]*\"|true|false|null|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?/g;\n    var rx_four = /(?:^|:|,)(?:\\s*\\[)+/g;\n    var rx_escapable = /[\\\\\"\\u0000-\\u001f\\u007f-\\u009f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g;\n    var rx_dangerous = /[\\u0000\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g;\n\n    function f(n) {\n        // Format integers to have at least two digits.\n        return (n < 10)\n            ? \"0\" + n\n            : n;\n    }\n\n    function this_value() {\n        return this.valueOf();\n    }\n\n    if (typeof Date.prototype.toJSON !== \"function\") {\n\n        Date.prototype.toJSON = function () {\n\n            return isFinite(this.valueOf())\n                ? (\n                    this.getUTCFullYear()\n                    + \"-\"\n                    + f(this.getUTCMonth() + 1)\n                    + \"-\"\n                    + f(this.getUTCDate())\n                    + \"T\"\n                    + f(this.getUTCHours())\n                    + \":\"\n                    + f(this.getUTCMinutes())\n                    + \":\"\n                    + f(this.getUTCSeconds())\n                    + \"Z\"\n                )\n                : null;\n        };\n\n        Boolean.prototype.toJSON = this_value;\n        Number.prototype.toJSON = this_value;\n        String.prototype.toJSON = this_value;\n    }\n\n    var gap;\n    var indent;\n    var meta;\n    var rep;\n\n\n    function quote(string) {\n\n// If the string contains no control characters, no quote characters, and no\n// backslash characters, then we can safely slap some quotes around it.\n// Otherwise we must also replace the offending characters with safe escape\n// sequences.\n\n        rx_escapable.lastIndex = 0;\n        return rx_escapable.test(string)\n            ? \"\\\"\" + string.replace(rx_escapable, function (a) {\n                var c = meta[a];\n                return typeof c === \"string\"\n                    ? c\n                    : \"\\\\u\" + (\"0000\" + a.charCodeAt(0).toString(16)).slice(-4);\n            }) + \"\\\"\"\n            : \"\\\"\" + string + \"\\\"\";\n    }\n\n\n    function str(key, holder) {\n\n// Produce a string from holder[key].\n\n        var i;          // The loop counter.\n        var k;          // The member key.\n        var v;          // The member value.\n        var length;\n        var mind = gap;\n        var partial;\n        var value = holder[key];\n\n// If the value has a toJSON method, call it to obtain a replacement value.\n\n        if (\n            value\n            && typeof value === \"object\"\n            && typeof value.toJSON === \"function\"\n        ) {\n            value = value.toJSON(key);\n        }\n\n// If we were called with a replacer function, then call the replacer to\n// obtain a replacement value.\n\n        if (typeof rep === \"function\") {\n            value = rep.call(holder, key, value);\n        }\n\n// What happens next depends on the value's type.\n\n        switch (typeof value) {\n        case \"string\":\n            return quote(value);\n\n        case \"number\":\n\n// JSON numbers must be finite. Encode non-finite numbers as null.\n\n            return (isFinite(value))\n                ? String(value)\n                : \"null\";\n\n        case \"boolean\":\n        case \"null\":\n\n// If the value is a boolean or null, convert it to a string. Note:\n// typeof null does not produce \"null\". The case is included here in\n// the remote chance that this gets fixed someday.\n\n            return String(value);\n\n// If the type is \"object\", we might be dealing with an object or an array or\n// null.\n\n        case \"object\":\n\n// Due to a specification blunder in ECMAScript, typeof null is \"object\",\n// so watch out for that case.\n\n            if (!value) {\n                return \"null\";\n            }\n\n// Make an array to hold the partial results of stringifying this object value.\n\n            gap += indent;\n            partial = [];\n\n// Is the value an array?\n\n            if (Object.prototype.toString.apply(value) === \"[object Array]\") {\n\n// The value is an array. Stringify every element. Use null as a placeholder\n// for non-JSON values.\n\n                length = value.length;\n                for (i = 0; i < length; i += 1) {\n                    partial[i] = str(i, value) || \"null\";\n                }\n\n// Join all of the elements together, separated with commas, and wrap them in\n// brackets.\n\n                v = partial.length === 0\n                    ? \"[]\"\n                    : gap\n                        ? (\n                            \"[\\n\"\n                            + gap\n                            + partial.join(\",\\n\" + gap)\n                            + \"\\n\"\n                            + mind\n                            + \"]\"\n                        )\n                        : \"[\" + partial.join(\",\") + \"]\";\n                gap = mind;\n                return v;\n            }\n\n// If the replacer is an array, use it to select the members to be stringified.\n\n            if (rep && typeof rep === \"object\") {\n                length = rep.length;\n                for (i = 0; i < length; i += 1) {\n                    if (typeof rep[i] === \"string\") {\n                        k = rep[i];\n                        v = str(k, value);\n                        if (v) {\n                            partial.push(quote(k) + (\n                                (gap)\n                                    ? \": \"\n                                    : \":\"\n                            ) + v);\n                        }\n                    }\n                }\n            } else {\n\n// Otherwise, iterate through all of the keys in the object.\n\n                for (k in value) {\n                    if (Object.prototype.hasOwnProperty.call(value, k)) {\n                        v = str(k, value);\n                        if (v) {\n                            partial.push(quote(k) + (\n                                (gap)\n                                    ? \": \"\n                                    : \":\"\n                            ) + v);\n                        }\n                    }\n                }\n            }\n\n// Join all of the member texts together, separated with commas,\n// and wrap them in braces.\n\n            v = partial.length === 0\n                ? \"{}\"\n                : gap\n                    ? \"{\\n\" + gap + partial.join(\",\\n\" + gap) + \"\\n\" + mind + \"}\"\n                    : \"{\" + partial.join(\",\") + \"}\";\n            gap = mind;\n            return v;\n        }\n    }\n\n// If the JSON object does not yet have a stringify method, give it one.\n\n    if (typeof JSON.stringify !== \"function\") {\n        meta = {    // table of character substitutions\n            \"\\b\": \"\\\\b\",\n            \"\\t\": \"\\\\t\",\n            \"\\n\": \"\\\\n\",\n            \"\\f\": \"\\\\f\",\n            \"\\r\": \"\\\\r\",\n            \"\\\"\": \"\\\\\\\"\",\n            \"\\\\\": \"\\\\\\\\\"\n        };\n        JSON.stringify = function (value, replacer, space) {\n\n// The stringify method takes a value and an optional replacer, and an optional\n// space parameter, and returns a JSON text. The replacer can be a function\n// that can replace values, or an array of strings that will select the keys.\n// A default replacer method can be provided. Use of the space parameter can\n// produce text that is more easily readable.\n\n            var i;\n            gap = \"\";\n            indent = \"\";\n\n// If the space parameter is a number, make an indent string containing that\n// many spaces.\n\n            if (typeof space === \"number\") {\n                for (i = 0; i < space; i += 1) {\n                    indent += \" \";\n                }\n\n// If the space parameter is a string, it will be used as the indent string.\n\n            } else if (typeof space === \"string\") {\n                indent = space;\n            }\n\n// If there is a replacer, it must be a function or an array.\n// Otherwise, throw an error.\n\n            rep = replacer;\n            if (replacer && typeof replacer !== \"function\" && (\n                typeof replacer !== \"object\"\n                || typeof replacer.length !== \"number\"\n            )) {\n                throw new Error(\"JSON.stringify\");\n            }\n\n// Make a fake root object containing our value under the key of \"\".\n// Return the result of stringifying the value.\n\n            return str(\"\", {\"\": value});\n        };\n    }\n\n\n// If the JSON object does not yet have a parse method, give it one.\n\n    if (typeof JSON.parse !== \"function\") {\n        JSON.parse = function (text, reviver) {\n\n// The parse method takes a text and an optional reviver function, and returns\n// a JavaScript value if the text is a valid JSON text.\n\n            var j;\n\n            function walk(holder, key) {\n\n// The walk method is used to recursively walk the resulting structure so\n// that modifications can be made.\n\n                var k;\n                var v;\n                var value = holder[key];\n                if (value && typeof value === \"object\") {\n                    for (k in value) {\n                        if (Object.prototype.hasOwnProperty.call(value, k)) {\n                            v = walk(value, k);\n                            if (v !== undefined) {\n                                value[k] = v;\n                            } else {\n                                delete value[k];\n                            }\n                        }\n                    }\n                }\n                return reviver.call(holder, key, value);\n            }\n\n\n// Parsing happens in four stages. In the first stage, we replace certain\n// Unicode characters with escape sequences. JavaScript handles many characters\n// incorrectly, either silently deleting them, or treating them as line endings.\n\n            text = String(text);\n            rx_dangerous.lastIndex = 0;\n            if (rx_dangerous.test(text)) {\n                text = text.replace(rx_dangerous, function (a) {\n                    return (\n                        \"\\\\u\"\n                        + (\"0000\" + a.charCodeAt(0).toString(16)).slice(-4)\n                    );\n                });\n            }\n\n// In the second stage, we run the text against regular expressions that look\n// for non-JSON patterns. We are especially concerned with \"()\" and \"new\"\n// because they can cause invocation, and \"=\" because it can cause mutation.\n// But just to be safe, we want to reject all unexpected forms.\n\n// We split the second stage into 4 regexp operations in order to work around\n// crippling inefficiencies in IE's and Safari's regexp engines. First we\n// replace the JSON backslash pairs with \"@\" (a non-JSON character). Second, we\n// replace all simple value tokens with \"]\" characters. Third, we delete all\n// open brackets that follow a colon or comma or that begin the text. Finally,\n// we look to see that the remaining characters are only whitespace or \"]\" or\n// \",\" or \":\" or \"{\" or \"}\". If that is so, then the text is safe for eval.\n\n            if (\n                rx_one.test(\n                    text\n                        .replace(rx_two, \"@\")\n                        .replace(rx_three, \"]\")\n                        .replace(rx_four, \"\")\n                )\n            ) {\n\n// In the third stage we use the eval function to compile the text into a\n// JavaScript structure. The \"{\" operator is subject to a syntactic ambiguity\n// in JavaScript: it can begin a block or an object literal. We wrap the text\n// in parens to eliminate the ambiguity.\n\n                j = eval(\"(\" + text + \")\");\n\n// In the optional fourth stage, we recursively walk the new structure, passing\n// each name/value pair to a reviver function for possible transformation.\n\n                return (typeof reviver === \"function\")\n                    ? walk({\"\": j}, \"\")\n                    : j;\n            }\n\n// If the text is not JSON parseable, then a SyntaxError is thrown.\n\n            throw new SyntaxError(\"JSON.parse\");\n        };\n    }\n}());"
  },
  {
    "path": "openpype/hosts/aftereffects/api/extension/js/libs/wsrpc.js",
    "content": "(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n  typeof define === 'function' && define.amd ? define(factory) :\n  (global = global || self, global.WSRPC = factory());\n}(this, function () { 'use strict';\n\n  function _classCallCheck(instance, Constructor) {\n    if (!(instance instanceof Constructor)) {\n      throw new TypeError(\"Cannot call a class as a function\");\n    }\n  }\n\n  var Deferred = function Deferred() {\n    _classCallCheck(this, Deferred);\n\n    var self = this;\n    self.resolve = null;\n    self.reject = null;\n    self.done = false;\n\n    function wrapper(func) {\n      return function () {\n        if (self.done) throw new Error('Promise already done');\n        self.done = true;\n        return func.apply(this, arguments);\n      };\n    }\n\n    self.promise = new Promise(function (resolve, reject) {\n      self.resolve = wrapper(resolve);\n      self.reject = wrapper(reject);\n    });\n\n    self.promise.isPending = function () {\n      return !self.done;\n    };\n\n    return self;\n  };\n\n  function logGroup(group, level, args) {\n    console.group(group);\n    console[level].apply(this, args);\n    console.groupEnd();\n  }\n\n  function log() {\n    if (!WSRPC.DEBUG) return;\n    logGroup('WSRPC.DEBUG', 'trace', arguments);\n  }\n\n  function trace(msg) {\n    if (!WSRPC.TRACE) return;\n    var payload = msg;\n    if ('data' in msg) payload = JSON.parse(msg.data);\n    logGroup(\"WSRPC.TRACE\", 'trace', [payload]);\n  }\n\n  function getAbsoluteWsUrl(url) {\n    if (/^\\w+:\\/\\//.test(url)) return url;\n    if (typeof window == 'undefined' && window.location.host.length < 1) throw new Error(\"Can not construct absolute URL from \".concat(window.location));\n    var scheme = window.location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n    var port = window.location.port === '' ? \":\".concat(window.location.port) : '';\n    var host = window.location.host;\n    var path = url.replace(/^\\/+/gm, '');\n    return \"\".concat(scheme, \"//\").concat(host).concat(port, \"/\").concat(path);\n  }\n\n  var readyState = Object.freeze({\n    0: 'CONNECTING',\n    1: 'OPEN',\n    2: 'CLOSING',\n    3: 'CLOSED'\n  });\n\n  var WSRPC = function WSRPC(URL) {\n    var reconnectTimeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1000;\n\n    _classCallCheck(this, WSRPC);\n\n    var self = this;\n    URL = getAbsoluteWsUrl(URL);\n    self.id = 1;\n    self.eventId = 0;\n    self.socketStarted = false;\n    self.eventStore = {\n      onconnect: {},\n      onerror: {},\n      onclose: {},\n      onchange: {}\n    };\n    self.connectionNumber = 0;\n    self.oneTimeEventStore = {\n      onconnect: [],\n      onerror: [],\n      onclose: [],\n      onchange: []\n    };\n    self.callQueue = [];\n\n    function createSocket() {\n      var ws = new WebSocket(URL);\n\n      var rejectQueue = function rejectQueue() {\n        self.connectionNumber++; // rejects incoming calls\n\n        var deferred; //reject all pending calls\n\n        while (0 < self.callQueue.length) {\n          var callObj = self.callQueue.shift();\n          deferred = self.store[callObj.id];\n          delete self.store[callObj.id];\n\n          if (deferred && deferred.promise.isPending()) {\n            deferred.reject('WebSocket error occurred');\n          }\n        } // reject all from the store\n\n\n        for (var key in self.store) {\n          if (!self.store.hasOwnProperty(key)) continue;\n          deferred = self.store[key];\n\n          if (deferred && deferred.promise.isPending()) {\n            deferred.reject('WebSocket error occurred');\n          }\n        }\n      };\n\n      function reconnect(callEvents) {\n        setTimeout(function () {\n          try {\n            self.socket = createSocket();\n            self.id = 1;\n          } catch (exc) {\n            callEvents('onerror', exc);\n            delete self.socket;\n            console.error(exc);\n          }\n        }, reconnectTimeout);\n      }\n\n      ws.onclose = function (err) {\n        log('ONCLOSE CALLED', 'STATE', self.public.state());\n        trace(err);\n\n        for (var serial in self.store) {\n          if (!self.store.hasOwnProperty(serial)) continue;\n\n          if (self.store[serial].hasOwnProperty('reject')) {\n            self.store[serial].reject('Connection closed');\n          }\n        }\n\n        rejectQueue();\n        callEvents('onclose', err);\n        callEvents('onchange', err);\n        reconnect(callEvents);\n      };\n\n      ws.onerror = function (err) {\n        log('ONERROR CALLED', 'STATE', self.public.state());\n        trace(err);\n        rejectQueue();\n        callEvents('onerror', err);\n        callEvents('onchange', err);\n        log('WebSocket has been closed by error: ', err);\n      };\n\n      function tryCallEvent(func, event) {\n        try {\n          return func(event);\n        } catch (e) {\n          if (e.hasOwnProperty('stack')) {\n            log(e.stack);\n          } else {\n            log('Event function', func, 'raised unknown error:', e);\n          }\n\n          console.error(e);\n        }\n      }\n\n      function callEvents(evName, event) {\n        while (0 < self.oneTimeEventStore[evName].length) {\n          var deferred = self.oneTimeEventStore[evName].shift();\n          if (deferred.hasOwnProperty('resolve') && deferred.promise.isPending()) deferred.resolve();\n        }\n\n        for (var i in self.eventStore[evName]) {\n          if (!self.eventStore[evName].hasOwnProperty(i)) continue;\n          var cur = self.eventStore[evName][i];\n          tryCallEvent(cur, event);\n        }\n      }\n\n      ws.onopen = function (ev) {\n        log('ONOPEN CALLED', 'STATE', self.public.state());\n        trace(ev);\n\n        while (0 < self.callQueue.length) {\n          // noinspection JSUnresolvedFunction\n          self.socket.send(JSON.stringify(self.callQueue.shift(), 0, 1));\n        }\n\n        callEvents('onconnect', ev);\n        callEvents('onchange', ev);\n      };\n\n      function handleCall(self, data) {\n        if (!self.routes.hasOwnProperty(data.method)) throw new Error('Route not found');\n        var connectionNumber = self.connectionNumber;\n        var deferred = new Deferred();\n        deferred.promise.then(function (result) {\n          if (connectionNumber !== self.connectionNumber) return;\n          self.socket.send(JSON.stringify({\n            id: data.id,\n            result: result\n          }));\n        }, function (error) {\n          if (connectionNumber !== self.connectionNumber) return;\n          self.socket.send(JSON.stringify({\n            id: data.id,\n            error: error\n          }));\n        });\n        var func = self.routes[data.method];\n        if (self.asyncRoutes[data.method]) return func.apply(deferred, [data.params]);\n\n        function badPromise() {\n          throw new Error(\"You should register route with async flag.\");\n        }\n\n        var promiseMock = {\n          resolve: badPromise,\n          reject: badPromise\n        };\n\n        try {\n          deferred.resolve(func.apply(promiseMock, [data.params]));\n        } catch (e) {\n          deferred.reject(e);\n          console.error(e);\n        }\n      }\n\n      function handleError(self, data) {\n        if (!self.store.hasOwnProperty(data.id)) return log('Unknown callback');\n        var deferred = self.store[data.id];\n        if (typeof deferred === 'undefined') return log('Confirmation without handler');\n        delete self.store[data.id];\n        log('REJECTING', data.error);\n        deferred.reject(data.error);\n      }\n\n      function handleResult(self, data) {\n        var deferred = self.store[data.id];\n        if (typeof deferred === 'undefined') return log('Confirmation without handler');\n        delete self.store[data.id];\n\n        if (data.hasOwnProperty('result')) {\n          return deferred.resolve(data.result);\n        }\n\n        return deferred.reject(data.error);\n      }\n\n      ws.onmessage = function (message) {\n        log('ONMESSAGE CALLED', 'STATE', self.public.state());\n        trace(message);\n        if (message.type !== 'message') return;\n        var data;\n\n        try {\n          data = JSON.parse(message.data);\n          log(data);\n\n          if (data.hasOwnProperty('method')) {\n            return handleCall(self, data);\n          } else if (data.hasOwnProperty('error') && data.error === null) {\n            return handleError(self, data);\n          } else {\n            return handleResult(self, data);\n          }\n        } catch (exception) {\n          var err = {\n            error: exception.message,\n            result: null,\n            id: data ? data.id : null\n          };\n          self.socket.send(JSON.stringify(err));\n          console.error(exception);\n        }\n      };\n\n      return ws;\n    }\n\n    function makeCall(func, args, params) {\n      self.id += 2;\n      var deferred = new Deferred();\n      var callObj = Object.freeze({\n        id: self.id,\n        method: func,\n        params: args\n      });\n      var state = self.public.state();\n\n      if (state === 'OPEN') {\n        self.store[self.id] = deferred;\n        self.socket.send(JSON.stringify(callObj));\n      } else if (state === 'CONNECTING') {\n        log('SOCKET IS', state);\n        self.store[self.id] = deferred;\n        self.callQueue.push(callObj);\n      } else {\n        log('SOCKET IS', state);\n\n        if (params && params['noWait']) {\n          deferred.reject(\"Socket is: \".concat(state));\n        } else {\n          self.store[self.id] = deferred;\n          self.callQueue.push(callObj);\n        }\n      }\n\n      return deferred.promise;\n    }\n\n    self.asyncRoutes = {};\n    self.routes = {};\n    self.store = {};\n    self.public = Object.freeze({\n      call: function call(func, args, params) {\n        return makeCall(func, args, params);\n      },\n      addRoute: function addRoute(route, callback, isAsync) {\n        self.asyncRoutes[route] = isAsync || false;\n        self.routes[route] = callback;\n      },\n      deleteRoute: function deleteRoute(route) {\n        delete self.asyncRoutes[route];\n        return delete self.routes[route];\n      },\n      addEventListener: function addEventListener(event, func) {\n        var eventId = self.eventId++;\n        self.eventStore[event][eventId] = func;\n        return eventId;\n      },\n      removeEventListener: function removeEventListener(event, index) {\n        if (self.eventStore[event].hasOwnProperty(index)) {\n          delete self.eventStore[event][index];\n          return true;\n        } else {\n          return false;\n        }\n      },\n      onEvent: function onEvent(event) {\n        var deferred = new Deferred();\n        self.oneTimeEventStore[event].push(deferred);\n        return deferred.promise;\n      },\n      destroy: function destroy() {\n        return self.socket.close();\n      },\n      state: function state() {\n        return readyState[this.stateCode()];\n      },\n      stateCode: function stateCode() {\n        if (self.socketStarted && self.socket) return self.socket.readyState;\n        return 3;\n      },\n      connect: function connect() {\n        self.socketStarted = true;\n        self.socket = createSocket();\n      }\n    });\n    self.public.addRoute('log', function (argsObj) {\n      //console.info(\"Websocket sent: \".concat(argsObj));\n    });\n    self.public.addRoute('ping', function (data) {\n      return data;\n    });\n    return self.public;\n  };\n\n  WSRPC.DEBUG = false;\n  WSRPC.TRACE = false;\n\n  return WSRPC;\n\n}));\n//# sourceMappingURL=wsrpc.js.map\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/extension/js/main.js",
    "content": "/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true,\nindent: 4, maxerr: 50 */\n/*global $, window, location, CSInterface, SystemPath, themeManager*/\n\n\nvar csInterface = new CSInterface();\n\nlog.warn(\"script start\");\n\nWSRPC.DEBUG = false;\nWSRPC.TRACE = false;\n\n// get websocket server url from environment value\nasync function startUp(url){\n    promis = runEvalScript(\"getEnv('\" + url + \"')\");\n\n    var res = await promis;\n    log.warn(\"res: \" + res);\n\n    promis = runEvalScript(\"getEnv('OPENPYPE_DEBUG')\");\n    var debug = await promis;\n    log.warn(\"debug: \" + debug);\n    if (debug && debug.toString() == '3'){\n        WSRPC.DEBUG = true;\n        WSRPC.TRACE = true;\n    }\n    // run rest only after resolved promise\n    main(res);\n}\n\nfunction get_extension_version(){\n    /** Returns version number from extension manifest.xml **/\n    log.debug(\"get_extension_version\")\n    var path = csInterface.getSystemPath(SystemPath.EXTENSION);\n    log.debug(\"extension path \" + path);\n\n    var result = window.cep.fs.readFile(path + \"/CSXS/manifest.xml\");\n    var version = undefined;\n    if(result.err === 0){\n        if (window.DOMParser) {\n            const parser = new DOMParser();\n            const xmlDoc = parser.parseFromString(result.data.toString(),\n                                                  'text/xml');\n            const children = xmlDoc.children;\n\n            for (let i = 0; i <= children.length; i++) {\n                if (children[i] &&\n                children[i].getAttribute('ExtensionBundleVersion')) {\n                    version =\n                        children[i].getAttribute('ExtensionBundleVersion');\n                }\n            }\n        }\n    }\n    return '{\"result\":\"' + version + '\"}'\n}\n\nfunction main(websocket_url){\n    // creates connection to 'websocket_url', registers routes\n    var default_url = 'ws://localhost:8099/ws/';\n\n    if  (websocket_url == ''){\n         websocket_url = default_url;\n    }\n    RPC = new WSRPC(websocket_url, 5000); // spin connection\n\n    RPC.connect();\n\n    log.warn(\"connected\");\n\n    RPC.addRoute('AfterEffects.open', function (data) {\n        log.warn('Server called client route \"open\":', data);\n        var escapedPath = EscapeStringForJSX(data.path);\n        return runEvalScript(\"fileOpen('\" + escapedPath +\"')\")\n            .then(function(result){\n                log.warn(\"open: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.get_metadata', function (data) {\n        log.warn('Server called client route \"get_metadata\":', data);\n        return runEvalScript(\"getMetadata()\")\n            .then(function(result){\n                log.warn(\"getMetadata: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.get_active_document_name', function (data) {\n        log.warn('Server called client route ' +\n            '\"get_active_document_name\":', data);\n        return runEvalScript(\"getActiveDocumentName()\")\n            .then(function(result){\n                log.warn(\"get_active_document_name: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.get_active_document_full_name', function (data){\n        log.warn('Server called client route ' +\n            '\"get_active_document_full_name\":', data);\n        return runEvalScript(\"getActiveDocumentFullName()\")\n            .then(function(result){\n                log.warn(\"get_active_document_full_name: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.add_item', function (data) {\n        log.warn('Server called client route \"add_item\":', data);\n        var escapedName = EscapeStringForJSX(data.name);\n        return runEvalScript(\"addItem('\" + escapedName +\"', \" +\n                                     \"'\" + data.item_type + \"')\")\n            .then(function(result){\n                log.warn(\"get_items: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.get_items', function (data) {\n        log.warn('Server called client route \"get_items\":', data);\n        return runEvalScript(\"getItems(\"  + data.comps + \",\" +\n                                            data.folders + \",\" +\n                                            data.footages + \")\")\n            .then(function(result){\n                log.warn(\"get_items: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.select_items', function (data) {\n        log.warn('Server called client route \"select_items\":', data);\n        return runEvalScript(\"selectItems(\"  + JSON.stringify(data.items) + \")\")\n            .then(function(result){\n                log.warn(\"select_items: \" + result);\n                return result;\n            });\n    });\n\n\n    RPC.addRoute('AfterEffects.get_selected_items', function (data) {\n        log.warn('Server called client route \"get_selected_items\":', data);\n        return runEvalScript(\"getSelectedItems(\" + data.comps + \",\" +\n                                                   data.folders + \",\" +\n                                                   data.footages  + \")\")\n            .then(function(result){\n                log.warn(\"get_items: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.import_file', function (data) {\n        log.warn('Server called client route \"import_file\":', data);\n        var escapedPath = EscapeStringForJSX(data.path);\n        return runEvalScript(\"importFile('\" + escapedPath +\"', \" +\n                                         \"'\" + data.item_name + \"',\" +\n                                         \"'\" + JSON.stringify(\n                                         data.import_options) + \"')\")\n            .then(function(result){\n                log.warn(\"importFile: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.replace_item', function (data) {\n        log.warn('Server called client route \"replace_item\":', data);\n        var escapedPath = EscapeStringForJSX(data.path);\n        return runEvalScript(\"replaceItem(\" + data.item_id + \", \" +\n                                     \"'\" + escapedPath + \"', \" +\n                                     \"'\" + data.item_name + \"')\")\n            .then(function(result){\n                log.warn(\"replaceItem: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.rename_item', function (data) {\n        log.warn('Server called client route \"rename_item\":', data);\n        return runEvalScript(\"renameItem(\" + data.item_id + \", \" +\n                                         \"'\" + data.item_name + \"')\")\n            .then(function(result){\n                log.warn(\"renameItem: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.delete_item', function (data) {\n        log.warn('Server called client route \"delete_item\":', data);\n        return runEvalScript(\"deleteItem(\" + data.item_id + \")\")\n            .then(function(result){\n                log.warn(\"deleteItem: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.imprint', function (data) {\n        log.warn('Server called client route \"imprint\":', data);\n        var escaped = data.payload.replace(/\\n/g, \"\\\\n\");\n        return runEvalScript(\"imprint('\" + escaped +\"')\")\n            .then(function(result){\n                log.warn(\"imprint: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.set_label_color', function (data) {\n        log.warn('Server called client route \"set_label_color\":', data);\n        return runEvalScript(\"setLabelColor(\" + data.item_id + \",\" +\n                                                data.color_idx + \")\")\n            .then(function(result){\n                log.warn(\"imprint: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.get_comp_properties', function (data) {\n        log.warn('Server called client route \"get_comp_properties\":', data);\n        return runEvalScript(\"getCompProperties(\" + data.item_id + \")\")\n            .then(function(result){\n                log.warn(\"get_comp_properties: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.set_comp_properties', function (data) {\n        log.warn('Server called client route \"set_work_area\":', data);\n        return runEvalScript(\"setCompProperties(\" + data.item_id + ',' +\n                                              data.start + ',' +\n                                              data.duration + ',' +\n                                              data.frame_rate + ',' +\n                                              data.width + ',' +\n                                              data.height + \")\")\n            .then(function(result){\n                log.warn(\"set_comp_properties: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.saveAs', function (data) {\n        log.warn('Server called client route \"saveAs\":', data);\n        var escapedPath = EscapeStringForJSX(data.image_path);\n        return runEvalScript(\"saveAs('\" + escapedPath + \"', \" +\n                                     data.as_copy + \")\")\n            .then(function(result){\n                log.warn(\"saveAs: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.save', function (data) {\n        log.warn('Server called client route \"save\":', data);\n        return runEvalScript(\"save()\")\n            .then(function(result){\n                log.warn(\"save: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.get_render_info', function (data) {\n        log.warn('Server called client route \"get_render_info\":', data);\n        return runEvalScript(\"getRenderInfo(\" + data.comp_id +\")\")\n            .then(function(result){\n                log.warn(\"get_render_info: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.get_audio_url', function (data) {\n        log.warn('Server called client route \"get_audio_url\":', data);\n        return runEvalScript(\"getAudioUrlForComp(\" + data.item_id + \")\")\n            .then(function(result){\n                log.warn(\"getAudioUrlForComp: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.import_background', function (data) {\n        log.warn('Server called client route \"import_background\":', data);\n        return runEvalScript(\"importBackground(\" + data.comp_id + \", \" +\n                                               \"'\" + data.comp_name + \"', \" +\n                                               JSON.stringify(data.files) + \")\")\n            .then(function(result){\n                log.warn(\"importBackground: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.reload_background', function (data) {\n        log.warn('Server called client route \"reload_background\":', data);\n        return runEvalScript(\"reloadBackground(\" + data.comp_id + \", \" +\n                                               \"'\" + data.comp_name + \"', \" +\n                                               JSON.stringify(data.files) + \")\")\n            .then(function(result){\n                log.warn(\"reloadBackground: \" + result);\n                return result;\n            });\n    });\n\n   RPC.addRoute('AfterEffects.add_item_as_layer', function (data) {\n       log.warn('Server called client route \"add_item_as_layer\":', data);\n       return runEvalScript(\"addItemAsLayerToComp(\" + data.comp_id + \", \" +\n                                                      data.item_id + \",\" +\n                                                  \" null )\")\n           .then(function(result){\n               log.warn(\"addItemAsLayerToComp: \" + result);\n               return result;\n           });\n   });\n\n   RPC.addRoute('AfterEffects.add_item_instead_placeholder', function (data) {\n    log.warn('Server called client route \"add_item_instead_placeholder\":', data);\n    return runEvalScript(\"addItemInstead(\" + data.placeholder_item_id + \", \" +\n                                             data.item_id + \")\")\n        .then(function(result){\n            log.warn(\"add_item_instead_placeholder: \" + result);\n            return result;\n        });\n});\n\n   RPC.addRoute('AfterEffects.render', function (data) {\n    log.warn('Server called client route \"render\":', data);\n    var escapedPath = EscapeStringForJSX(data.folder_url);\n    return runEvalScript(\"render('\" + escapedPath +\"', \" + data.comp_id + \")\")\n        .then(function(result){\n            log.warn(\"render: \" + result);\n            return result;\n        });\n    });\n\n    RPC.addRoute('AfterEffects.get_extension_version', function (data) {\n      log.warn('Server called client route \"get_extension_version\":', data);\n      return get_extension_version();\n    });\n\n    RPC.addRoute('AfterEffects.get_app_version', function (data) {\n        log.warn('Server called client route \"get_app_version\":', data);\n        return runEvalScript(\"getAppVersion()\")\n            .then(function(result){\n                log.warn(\"get_app_version: \" + result);\n                return result;\n            });\n    });\n\n    RPC.addRoute('AfterEffects.add_placeholder', function (data) {\n        log.warn('Server called client route \"add_placeholder\":', data);\n        var escapedName = EscapeStringForJSX(data.name);\n        return runEvalScript(\"addPlaceholder('\" + escapedName +\"',\"+\n                                              data.width + ',' +\n                                              data.height + ',' +\n                                              data.fps + ',' +\n                                              data.duration + \")\")\n            .then(function(result){\n                log.warn(\"add_placeholder: \" + result);\n                return result;\n            });\n    });\n\n     RPC.addRoute('AfterEffects.close', function (data) {\n        log.warn('Server called client route \"close\":', data);\n        return runEvalScript(\"close()\");\n    });\n\n    RPC.addRoute('AfterEffects.print_msg', function (data) {\n        log.warn('Server called client route \"print_msg\":', data);\n        var escaped_msg = EscapeStringForJSX(data.msg);\n        return runEvalScript(\"printMsg('\" + escaped_msg +\"')\")\n            .then(function(result){\n                log.warn(\"print_msg: \" + result);\n                return result;\n            });\n    });\n}\n\n/** main entry point **/\nstartUp(\"WEBSOCKET_URL\");\n\n(function () {\n    'use strict';\n\n    var csInterface = new CSInterface();\n\n\n    function init() {\n\n        themeManager.init();\n\n        $(\"#btn_test\").click(function () {\n            csInterface.evalScript('sayHello()');\n        });\n    }\n\n    init();\n\n}());\n\nfunction EscapeStringForJSX(str){\n    // Replaces:\n    //  \\ with \\\\\n    //  ' with \\'\n    //  \" with \\\"\n    // See: https://stackoverflow.com/a/3967927/5285364\n    return str.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\").replace(/\"/g,'\\\\\"');\n}\n\nfunction runEvalScript(script) {\n    // because of asynchronous nature of functions in jsx\n    // this waits for response\n    return new Promise(function(resolve, reject){\n        csInterface.evalScript(script, resolve);\n    });\n}\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/extension/js/themeManager.js",
    "content": "/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */\n/*global window, document, CSInterface*/\n\n\n/*\n\n    Responsible for overwriting CSS at runtime according to CC app\n    settings as defined by the end user.\n\n*/\n\nvar themeManager = (function () {\n    'use strict';\n     \n    /**\n     * Convert the Color object to string in hexadecimal format;\n     */\n    function toHex(color, delta) {\n        \n        function computeValue(value, delta) {\n            var computedValue = !isNaN(delta) ? value + delta : value;\n            if (computedValue < 0) {\n                computedValue = 0;\n            } else if (computedValue > 255) {\n                computedValue = 255;\n            }\n            \n            computedValue = Math.floor(computedValue);\n    \n            computedValue = computedValue.toString(16);\n            return computedValue.length === 1 ? \"0\" + computedValue : computedValue;\n        }\n    \n        var hex = \"\";\n        if (color) {\n            hex = computeValue(color.red, delta) + computeValue(color.green, delta) + computeValue(color.blue, delta);\n        }\n        return hex;\n    }\n\n\n    function reverseColor(color, delta) {\n        return toHex({\n            red: Math.abs(255 - color.red),\n            green: Math.abs(255 - color.green),\n            blue: Math.abs(255 - color.blue)\n        },\n            delta);\n    }\n            \n\n    function addRule(stylesheetId, selector, rule) {\n        var stylesheet = document.getElementById(stylesheetId);\n        \n        if (stylesheet) {\n            stylesheet = stylesheet.sheet;\n            if (stylesheet.addRule) {\n                stylesheet.addRule(selector, rule);\n            } else if (stylesheet.insertRule) {\n                stylesheet.insertRule(selector + ' { ' + rule + ' }', stylesheet.cssRules.length);\n            }\n        }\n    }\n        \n        \n                \n    /**\n     * Update the theme with the AppSkinInfo retrieved from the host product.\n     */\n    function updateThemeWithAppSkinInfo(appSkinInfo) {\n        \n        var panelBgColor = appSkinInfo.panelBackgroundColor.color;\n        var bgdColor = toHex(panelBgColor);\n       \n        var darkBgdColor =  toHex(panelBgColor, 20);\n        \n        var fontColor = \"F0F0F0\";\n        if (panelBgColor.red > 122) {\n            fontColor = \"000000\";\n        }\n        var lightBgdColor = toHex(panelBgColor, -100);\n                \n        var styleId = \"hostStyle\";\n        \n        addRule(styleId, \".hostElt\", \"background-color:\" + \"#\" + bgdColor);\n        addRule(styleId, \".hostElt\", \"font-size:\" + appSkinInfo.baseFontSize + \"px;\");\n        addRule(styleId, \".hostElt\", \"font-family:\" + appSkinInfo.baseFontFamily);\n        addRule(styleId, \".hostElt\", \"color:\" + \"#\" + fontColor);\n\n        addRule(styleId, \".hostBgd\", \"background-color:\" + \"#\" + bgdColor);\n        addRule(styleId, \".hostBgdDark\", \"background-color: \" + \"#\" + darkBgdColor);\n        addRule(styleId, \".hostBgdLight\", \"background-color: \" + \"#\" + lightBgdColor);\n        addRule(styleId, \".hostFontSize\", \"font-size:\" + appSkinInfo.baseFontSize + \"px;\");\n        addRule(styleId, \".hostFontFamily\", \"font-family:\" + appSkinInfo.baseFontFamily);\n        addRule(styleId, \".hostFontColor\", \"color:\" + \"#\" + fontColor);\n        \n        addRule(styleId, \".hostFont\", \"font-size:\" + appSkinInfo.baseFontSize + \"px;\");\n        addRule(styleId, \".hostFont\", \"font-family:\" + appSkinInfo.baseFontFamily);\n        addRule(styleId, \".hostFont\", \"color:\" + \"#\" + fontColor);\n        \n        addRule(styleId, \".hostButton\", \"background-color:\" + \"#\" + darkBgdColor);\n        addRule(styleId, \".hostButton:hover\", \"background-color:\" + \"#\" + bgdColor);\n        addRule(styleId, \".hostButton:active\", \"background-color:\" + \"#\" + darkBgdColor);\n        addRule(styleId, \".hostButton\", \"border-color: \" + \"#\" + lightBgdColor);        \n\n    }\n    \n    \n    function onAppThemeColorChanged(event) {\n        var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo;\n        updateThemeWithAppSkinInfo(skinInfo);\n    }\n\n\n    function init() {\n        \n        var csInterface = new CSInterface();\n    \n        updateThemeWithAppSkinInfo(csInterface.hostEnvironment.appSkinInfo);\n        \n        csInterface.addEventListener(CSInterface.THEME_COLOR_CHANGED_EVENT, onAppThemeColorChanged);\n    }\n    \n    return {\n        init: init\n    };\n    \n}());\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx",
    "content": "/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true,\nindent: 4, maxerr: 50 */\n/*global $, Folder*/\n//@include \"../js/libs/json.js\"\n\n/* All public API function should return JSON! */\n\napp.preferences.savePrefAsBool(\"General Section\", \"Show Welcome Screen\", false) ;\n\nif(!Array.prototype.indexOf) {\n    Array.prototype.indexOf = function ( item ) {\n        var index = 0, length = this.length;\n        for ( ; index < length; index++ ) {\n                  if ( this[index] === item )\n                        return index;\n        }\n        return -1;\n        };\n}\n\nfunction sayHello(){\n    alert(\"hello from ExtendScript\");\n}\n\nfunction getEnv(variable){\n    return $.getenv(variable);\n}\n\nfunction getMetadata(){\n    /**\n     *  Returns payload in 'Label' field of project's metadata\n     *\n     **/\n    if (ExternalObject.AdobeXMPScript === undefined){\n        ExternalObject.AdobeXMPScript =\n            new ExternalObject('lib:AdobeXMPScript');\n    }\n\n    var proj = app.project;\n    var meta = new XMPMeta(app.project.xmpPacket);\n    var schemaNS = XMPMeta.getNamespaceURI(\"xmp\");\n    var label = \"xmp:Label\";\n\n    if (meta.doesPropertyExist(schemaNS, label)){\n        var prop = meta.getProperty(schemaNS, label);\n        return prop.value;\n    }\n\n    return _prepareSingleValue([]);\n\n}\n\nfunction imprint(payload){\n    /**\n     * Stores payload in 'Label' field of project's metadata\n     *\n     * Args:\n     *     payload (string): json content\n     */\n    if (ExternalObject.AdobeXMPScript === undefined){\n        ExternalObject.AdobeXMPScript =\n            new ExternalObject('lib:AdobeXMPScript');\n    }\n\n    var proj = app.project;\n    var meta = new XMPMeta(app.project.xmpPacket);\n    var schemaNS = XMPMeta.getNamespaceURI(\"xmp\");\n    var label = \"xmp:Label\";\n\n    meta.setProperty(schemaNS, label, payload);\n\n    app.project.xmpPacket = meta.serialize();\n\n}\n\n\nfunction fileOpen(path){\n    /**\n     * Opens (project) file on 'path'\n     */\n    fp = new File(path);\n    return _prepareSingleValue(app.open(fp))\n}\n\nfunction getActiveDocumentName(){\n    /**\n     *   Returns file name of active document\n     * */\n    var file = app.project.file;\n\n    if (file){\n        return _prepareSingleValue(file.name)\n    }\n\n    return _prepareError(\"No file open currently\");\n}\n\nfunction getActiveDocumentFullName(){\n    /**\n     *   Returns absolute path to current project\n     * */\n    var file = app.project.file;\n\n    if (file){\n        var f = new File(file.fullName);\n        var path = f.fsName;\n        f.close();\n\n        return _prepareSingleValue(path)\n    }\n\n    return _prepareError(\"No file open currently\");\n}\n\n\nfunction addItem(name, item_type){\n    /**\n     * Adds comp or folder to project items.\n     *\n     * Could be called when creating publishable instance to prepare\n     * composition (and render queue).\n     *\n     * Args:\n     *      name (str): composition name\n     *      item_type (str): COMP|FOLDER\n     * Returns:\n     *      SingleItemValue: eg {\"result\": VALUE}\n     */\n    if (item_type == \"COMP\"){\n        // dummy values, will be rewritten later\n        item = app.project.items.addComp(name, 1920, 1060, 1, 10, 25);\n    }else if (item_type == \"FOLDER\"){\n        item = app.project.items.addFolder(name);\n    }else{\n        return _prepareError(\"Only 'COMP' or 'FOLDER' can be created\");\n    }\n    return _prepareSingleValue(item.id);\n\n}\n\nfunction getItems(comps, folders, footages){\n    /**\n     * Returns JSON representation of compositions and\n     * if 'collectLayers' then layers in comps too.\n     *\n     * Args:\n     *     comps (bool): return selected compositions\n     *     folders (bool): return folders\n     *     footages (bool): return FootageItem\n     * Returns:\n     *     (list) of JSON items\n     */\n    var items = []\n    for (i = 1; i <= app.project.items.length; ++i){\n        var item = app.project.items[i];\n        if (!item){\n            continue;\n        }\n        var ret = _getItem(item, comps, folders, footages);\n        if (ret){\n            items.push(ret);\n        }\n    }\n    return '[' + items.join() + ']';\n\n}\n\nfunction selectItems(items){\n    /**\n     * Select all items from `items`, deselect other.\n     *\n     * Args:\n     *      items (list)\n     */\n    for (i = 1; i <= app.project.items.length; ++i){\n        item = app.project.items[i];\n        if (items.indexOf(item.id) > -1){\n            item.selected = true;\n        }else{\n            item.selected = false;\n        }\n    }\n\n}\n\nfunction getSelectedItems(comps, folders, footages){\n    /**\n     * Returns list of selected items from Project menu\n     *\n     * Args:\n     *     comps (bool): return selected compositions\n     *     folders (bool): return folders\n     *     footages (bool): return FootageItem\n     * Returns:\n     *     (list) of JSON items\n     */\n    var items = []\n    for (i = 0; i < app.project.selection.length; ++i){\n        var item = app.project.selection[i];\n        if (!item){\n            continue;\n        }\n        var ret = _getItem(item, comps, folders, footages);\n        if (ret){\n            items.push(ret);\n        }\n    }\n    return '[' + items.join() + ']';\n}\n\nfunction _getItem(item, comps, folders, footages){\n    /**\n     * Auxiliary function as project items and selections\n     * are indexed in different way :/\n     * Refactor\n     */\n    var item_type = '';\n    var path = '';\n    var containing_comps = [];\n    if (item instanceof FolderItem){\n        item_type = 'folder';\n        if (!folders){\n            return \"{}\";\n        }\n    }\n    if (item instanceof FootageItem){\n        if (!footages){\n            return \"{}\";\n        }\n        item_type = 'footage';\n        if (item.file){\n            path = item.file.fsName;\n        }\n        if (item.usedIn){\n            for (j = 0; j < item.usedIn.length; ++j){\n                containing_comps.push(item.usedIn[j].id);\n            }\n        }\n    }\n    if (item instanceof CompItem){\n        item_type = 'comp';\n        if (!comps){\n            return \"{}\";\n        }\n    }\n\n    var item = {\"name\": item.name,\n                \"id\": item.id,\n                \"type\": item_type,\n                \"path\": path,\n                \"containing_comps\": containing_comps};\n    return JSON.stringify(item);\n}\n\nfunction importFile(path, item_name, import_options){\n    /**\n     * Imports file (image tested for now) as a FootageItem.\n     * Creates new composition\n     *\n     * Args:\n     *    path (string): absolute path to image file\n     *    item_name (string): label for composition\n     * Returns:\n     *    JSON {name, id}\n     */\n    var comp;\n    var ret = {};\n    try{\n        import_options = JSON.parse(import_options);\n    } catch (e){\n        return _prepareError(\"Couldn't parse import options \" + import_options);\n    }\n\n    app.beginUndoGroup(\"Import File\");\n    fp = new File(path);\n    if (fp.exists){\n        try {\n            im_opt = new ImportOptions(fp);\n            importAsType = import_options[\"ImportAsType\"];\n\n            if ('ImportAsType' in import_options){ // refactor\n                if (importAsType.indexOf('COMP') > 0){\n                    im_opt.importAs = ImportAsType.COMP;\n                }\n                if (importAsType.indexOf('FOOTAGE') > 0){\n                    im_opt.importAs = ImportAsType.FOOTAGE;\n                }\n                if (importAsType.indexOf('COMP_CROPPED_LAYERS') > 0){\n                    im_opt.importAs = ImportAsType.COMP_CROPPED_LAYERS;\n                }\n                if (importAsType.indexOf('PROJECT') > 0){\n                    im_opt.importAs = ImportAsType.PROJECT;\n                }\n\n            }\n            if ('sequence' in import_options){\n                im_opt.sequence = true;\n            }\n\n            comp = app.project.importFile(im_opt);\n\n            if (app.project.selection.length == 2 &&\n                app.project.selection[0] instanceof FolderItem){\n                 comp.parentFolder = app.project.selection[0]\n            }\n        } catch (error) {\n            return _prepareError(error.toString() + importOptions.file.fsName);\n        } finally {\n            fp.close();\n        }\n    }else{\n\t    return _prepareError(\"File \" + path + \" not found.\");\n    }\n    if (comp){\n        comp.name = item_name;\n        comp.label = 9; // Green\n        ret = {\"name\": comp.name, \"id\": comp.id}\n    }\n    app.endUndoGroup();\n\n    return JSON.stringify(ret);\n}\n\nfunction setLabelColor(comp_id, color_idx){\n    /**\n     * Set item_id label to 'color_idx' color\n     * Args:\n     *     item_id (int): item id\n     *     color_idx (int): 0-16 index from Label\n     */\n    var item = app.project.itemByID(comp_id);\n    if (item){\n        item.label = color_idx;\n    }else{\n        return _prepareError(\"There is no composition with \"+ comp_id);\n    }\n}\n\nfunction replaceItem(item_id, path, item_name){\n    /**\n     * Replaces loaded file with new file and updates name\n     *\n     * Args:\n     *    item_id (int): id of composition, not a index!\n     *    path (string): absolute path to new file\n     *    item_name (string): new composition name\n     */\n    app.beginUndoGroup(\"Replace File\");\n\n    fp = new File(path);\n    if (!fp.exists){\n        return _prepareError(\"File \" + path + \" not found.\");\n    }\n    var item = app.project.itemByID(item_id);\n    if (item){\n        try{\n            if (isFileSequence(item)) {\n                item.replaceWithSequence(fp, false);\n            }else{\n                item.replace(fp);\n            }\n\n            item.name = item_name;\n        } catch (error) {\n            return _prepareError(error.toString() + path);\n        } finally {\n            fp.close();\n        }\n    }else{\n        return _prepareError(\"There is no item with \"+ item_id);\n    }\n    app.endUndoGroup();\n}\n\nfunction renameItem(item_id, new_name){\n    /**\n     * Renames item with 'item_id' to 'new_name'\n     *\n     * Args:\n     *    item_id (int): id to search item\n     *    new_name (str)\n     */\n    var item = app.project.itemByID(item_id);\n    if (item){\n        item.name = new_name;\n    }else{\n        return _prepareError(\"There is no composition with \"+ comp_id);\n    }\n}\n\nfunction deleteItem(item_id){\n    /**\n     *  Delete any 'item_id'\n     *\n     *  Not restricted only to comp, it could delete\n     *  any item with 'id'\n     */\n    var item = app.project.itemByID(item_id);\n    if (item){\n        item.remove();\n    }else{\n        return _prepareError(\"There is no composition with \"+ comp_id);\n    }\n}\n\nfunction getCompProperties(comp_id){\n    /**\n     * Returns information about composition - are that will be\n     * rendered.\n     *\n     * Returns\n     *     (dict)\n     */\n    var comp = app.project.itemByID(comp_id);\n    if (!comp){\n        return _prepareError(\"There is no composition with \"+ comp_id);\n    }\n\n    return JSON.stringify({\n        \"id\": comp.id,\n        \"name\": comp.name,\n        \"frameStart\": comp.displayStartFrame,\n        \"framesDuration\": comp.duration * comp.frameRate,\n        \"frameRate\": comp.frameRate,\n        \"width\": comp.width,\n        \"height\": comp.height});\n}\n\nfunction setCompProperties(comp_id, frameStart, framesCount, frameRate,\n                           width, height){\n    /**\n     * Sets work area info from outside (from Ftrack via OpenPype)\n     */\n    var comp = app.project.itemByID(comp_id);\n    if (!comp){\n        return _prepareError(\"There is no composition with \"+ comp_id);\n    }\n\n    app.beginUndoGroup('change comp properties');\n        if (frameStart && framesCount && frameRate){\n            comp.displayStartFrame = frameStart;\n            comp.duration = framesCount / frameRate;\n            comp.frameRate = frameRate;\n        }\n        if (width && height){\n            var widthOld = comp.width;\n            var widthNew = width;\n            var widthDelta = widthNew - widthOld;\n\n            var heightOld = comp.height;\n            var heightNew = height;\n            var heightDelta = heightNew - heightOld;\n\n            var offset = [widthDelta / 2, heightDelta / 2];\n\n            comp.width = widthNew;\n            comp.height = heightNew;\n\n            for (var i = 1, il = comp.numLayers; i <= il; i++) {\n                var layer = comp.layer(i);\n                var positionProperty = layer.property('ADBE Transform Group').property('ADBE Position');\n\n                if (positionProperty.numKeys > 0) {\n                    for (var j = 1, jl = positionProperty.numKeys; j <= jl; j++) {\n                        var keyValue = positionProperty.keyValue(j);\n                        positionProperty.setValueAtKey(j, keyValue + offset);\n                    }\n                } else {\n                    var positionValue = positionProperty.value;\n                    positionProperty.setValue(positionValue + offset);\n                }\n            }\n        }\n\n    app.endUndoGroup();\n}\n\nfunction save(){\n    /**\n     * Saves current project\n     */\n    app.project.save();  //TODO path is wrong, File instead\n}\n\nfunction saveAs(path){\n    /**\n     *   Saves current project as 'path'\n     * */\n    app.project.save(fp = new File(path));\n}\n\nfunction getRenderInfo(comp_id){\n    /***\n        Get info from render queue.\n        Currently pulls only file name to parse extension and\n        if it is sequence in Python\n    Args:\n        comp_id (int): id of composition\n     Return:\n        (list) [{file_name:\"xx.png\", width:00, height:00}]\n    **/\n    var item = app.project.itemByID(comp_id);\n    if (!item){\n        return _prepareError(\"Composition with '\" + comp_id + \"' wasn't found! Recreate publishable instance(s)\")\n    }\n\n    var comp_name = item.name;\n    var output_metadata = []\n    try{\n        // render_item.duplicate() should create new item on renderQueue\n        // BUT it works only sometimes, there are some weird synchronization issue\n        // this method will be called always before render, so prepare items here\n        // for render to spare the hassle\n        for (i = 1; i <= app.project.renderQueue.numItems; ++i){\n            var render_item = app.project.renderQueue.item(i);\n            if (render_item.comp.id != comp_id){\n                continue;\n            }\n\n            if (render_item.status == RQItemStatus.DONE){\n                render_item.duplicate();  // create new, cannot change status if DONE\n                render_item.remove();  // remove existing to limit duplications\n                continue;\n            }\n        }\n\n        // properly validate as `numItems` won't change magically\n        var comp_id_count = 0;\n        for (i = 1; i <= app.project.renderQueue.numItems; ++i){\n            var render_item = app.project.renderQueue.item(i);\n            if (render_item.comp.id != comp_id){\n                continue;\n            }\n            comp_id_count += 1;\n            var item = render_item.outputModule(1);\n\n            for (j = 1; j<= render_item.numOutputModules; ++j){\n                var file_url = item.file.toString();\n                output_metadata.push(\n                    JSON.stringify({\n                        \"file_name\": file_url,\n                        \"width\": render_item.comp.width,\n                        \"height\": render_item.comp.height\n                    })\n                );\n            }\n        }\n    } catch (error) {\n        return _prepareError(\"There is no render queue, create one\");\n    }\n\n    if (comp_id_count > 1){\n        return _prepareError(\"There cannot be more items in Render Queue for '\" + comp_name + \"'!\")\n    }\n\n    if (comp_id_count == 0){\n        return _prepareError(\"There is no item in Render Queue for '\" + comp_name + \"'! Add composition to Render Queue.\")\n    }\n\n    return '[' + output_metadata.join() + ']';\n}\n\nfunction getAudioUrlForComp(comp_id){\n    /**\n     * Searches composition for audio layer\n     *\n     * Only single AVLayer is expected!\n     * Used for collecting Audio\n     *\n     * Args:\n     *    comp_id (int): id of composition\n     * Return:\n     *    (str) with url to audio content\n     */\n    var item = app.project.itemByID(comp_id);\n    if (item){\n        for (i = 1; i <= item.numLayers; ++i){\n            var layer = item.layers[i];\n            if (layer instanceof AVLayer){\n                if (layer.hasAudio){\n                    source_url = layer.source.file.fsName.toString()\n                    return _prepareSingleValue(source_url);\n                }\n            }\n\n        }\n    }else{\n        return _prepareError(\"There is no composition with \"+ comp_id);\n    }\n\n}\n\nfunction addItemAsLayerToComp(comp_id, item_id, found_comp){\n    /**\n     * Adds already imported FootageItem ('item_id') as a new\n     * layer to composition ('comp_id').\n     *\n     * Args:\n     *  comp_id (int): id of target composition\n     *  item_id (int): FootageItem.id\n     *  found_comp (CompItem, optional): to limit quering if\n     *      comp already found previously\n     */\n    var comp = found_comp || app.project.itemByID(comp_id);\n    if (comp){\n        item = app.project.itemByID(item_id);\n        if (item){\n            comp.layers.add(item);\n        }else{\n            return _prepareError(\"There is no item with \" + item_id);\n        }\n    }else{\n        return _prepareError(\"There is no composition with \"+ comp_id);\n    }\n}\n\nfunction importBackground(comp_id, composition_name, files_to_import){\n    /**\n     * Imports backgrounds images to existing or new composition.\n     *\n     * If comp_id is not provided, new composition is created, basic\n     * values (width, heights, frameRatio) takes from first imported\n     * image.\n     *\n     * Args:\n     *   comp_id (int): id of existing composition (null if new)\n     *   composition_name (str): used when new composition\n     *   files_to_import (list): list of absolute paths to import and\n     *      add as layers\n     *\n     * Returns:\n     *  (str): json representation (id, name, members)\n     */\n    var comp;\n    var folder;\n    var imported_ids = [];\n    if (comp_id){\n        comp = app.project.itemByID(comp_id);\n        folder = comp.parentFolder;\n    }else{\n        if (app.project.selection.length > 1){\n            return _prepareError(\n                \"Too many items selected, select only target composition!\");\n        }else{\n            selected_item = app.project.activeItem;\n            if (selected_item instanceof Folder){\n                comp = selected_item;\n                folder = selected_item;\n            }\n        }\n    }\n\n    if (files_to_import){\n        for (i = 0; i < files_to_import.length; ++i){\n            item = _importItem(files_to_import[i]);\n            if (!item){\n                return _prepareError(\n                    \"No item for \" + item_json[\"id\"] +\n                    \". Import background failed.\")\n            }\n            if (!comp){\n                folder = app.project.items.addFolder(composition_name);\n                imported_ids.push(folder.id);\n                comp = app.project.items.addComp(composition_name, item.width,\n                    item.height, item.pixelAspect,\n                    1, 26.7);  // hardcode defaults\n                imported_ids.push(comp.id);\n                comp.parentFolder = folder;\n            }\n            imported_ids.push(item.id)\n            item.parentFolder = folder;\n\n            addItemAsLayerToComp(comp.id, item.id, comp);\n        }\n    }\n    var item = {\"name\": comp.name,\n                \"id\": folder.id,\n                \"members\": imported_ids};\n    return JSON.stringify(item);\n}\n\nfunction reloadBackground(comp_id, composition_name, files_to_import){\n    /**\n     * Reloads existing composition.\n     *\n     * It deletes complete composition with encompassing folder, recreates\n     * from scratch via 'importBackground' functionality.\n     *\n     * Args:\n     *   comp_id (int): id of existing composition (null if new)\n     *   composition_name (str): used when new composition\n     *   files_to_import (list): list of absolute paths to import and\n     *      add as layers\n     *\n     * Returns:\n     *  (str): json representation (id, name, members)\n     *\n     */\n    var imported_ids = []; // keep track of members of composition\n    comp = app.project.itemByID(comp_id);\n    folder = comp.parentFolder;\n    if (folder){\n        renameItem(folder.id, composition_name);\n        imported_ids.push(folder.id);\n    }\n    if (comp){\n        renameItem(comp.id, composition_name);\n        imported_ids.push(comp.id);\n    }\n\n    var existing_layer_names = [];\n    var existing_layer_ids = []; // because ExtendedScript doesnt have keys()\n    for (i = 1; i <= folder.items.length; ++i){\n        layer = folder.items[i];\n        //because comp.layers[i] doesnt have 'id' accessible\n        if (layer instanceof CompItem){\n            continue;\n        }\n        existing_layer_names.push(layer.name);\n        existing_layer_ids.push(layer.id);\n    }\n\n    var new_filenames = [];\n    if (files_to_import){\n        for (i = 0; i < files_to_import.length; ++i){\n            file_name = _get_file_name(files_to_import[i]);\n            new_filenames.push(file_name);\n\n            idx = existing_layer_names.indexOf(file_name);\n            if (idx >= 0){  // update\n                var layer_id = existing_layer_ids[idx];\n                replaceItem(layer_id, files_to_import[i], file_name);\n                imported_ids.push(layer_id);\n            }else{ // new layer\n                item = _importItem(files_to_import[i]);\n                if (!item){\n                    return _prepareError(\n                        \"No item for \" + files_to_import[i] +\n                        \". Reload background failed.\");\n                }\n                imported_ids.push(item.id);\n                item.parentFolder = folder;\n                addItemAsLayerToComp(comp.id, item.id, comp);\n            }\n        }\n    }\n\n    _delete_obsolete_items(folder, new_filenames);\n\n    var item = {\"name\": comp.name,\n                \"id\": folder.id,\n                \"members\": imported_ids};\n\n    return JSON.stringify(item);\n}\n\nfunction _get_file_name(file_url){\n    /**\n     * Returns file name without extension from 'file_url'\n     *\n     * Args:\n     *    file_url (str): full absolute url\n     * Returns:\n     *    (str)\n     */\n    fp = new File(file_url);\n    file_name = fp.name.substring(0, fp.name.lastIndexOf(\".\"));\n    return file_name;\n}\n\nfunction _delete_obsolete_items(folder, new_filenames){\n    /***\n     * Goes through 'folder' and removes layers not in new\n     * background\n     *\n     * Args:\n     *   folder (FolderItem)\n     *   new_filenames (array): list of layer names in new bg\n     */\n    // remove items in old, but not in new\n    delete_ids = []\n    for (i = 1; i <= folder.items.length; ++i){\n        layer = folder.items[i];\n        //because comp.layers[i] doesnt have 'id' accessible\n        if (layer instanceof CompItem){\n            continue;\n        }\n        if (new_filenames.indexOf(layer.name) < 0){\n            delete_ids.push(layer.id);\n        }\n    }\n    for (i = 0; i < delete_ids.length; ++i){\n        deleteItem(delete_ids[i]);\n    }\n}\n\nfunction _importItem(file_url){\n    /**\n     * Imports 'file_url' as new FootageItem\n     *\n     * Args:\n     *    file_url (str): file url with content\n     * Returns:\n     *    (FootageItem)\n     */\n    file_name = _get_file_name(file_url);\n\n    //importFile prepared previously to return json\n    item_json = importFile(file_url, file_name, JSON.stringify({\"ImportAsType\":\"FOOTAGE\"}));\n    item_json = JSON.parse(item_json);\n    item = app.project.itemByID(item_json[\"id\"]);\n\n    return item;\n}\n\nfunction isFileSequence (item){\n    /**\n     * Check that item is a recognizable sequence\n     */\n    if (item instanceof FootageItem && item.mainSource instanceof FileSource && !(item.mainSource.isStill) && item.hasVideo){\n        var extname = item.mainSource.file.fsName.split('.').pop();\n\n        return extname.match(new RegExp(\"(ai|bmp|bw|cin|cr2|crw|dcr|dng|dib|dpx|eps|erf|exr|gif|hdr|ico|icb|iff|jpe|jpeg|jpg|mos|mrw|nef|orf|pbm|pef|pct|pcx|pdf|pic|pict|png|ps|psd|pxr|raf|raw|rgb|rgbe|rla|rle|rpf|sgi|srf|tdi|tga|tif|tiff|vda|vst|x3f|xyze)\", \"i\")) !== null;\n    }\n\n    return false;\n}\n\nfunction render(target_folder, comp_id){\n    var out_dir = new Folder(target_folder);\n    var out_dir = out_dir.fsName;\n    for (i = 1; i <= app.project.renderQueue.numItems; ++i){\n        var render_item = app.project.renderQueue.item(i);\n        var composition = render_item.comp;\n        if (composition.id == comp_id){\n            if (render_item.status == RQItemStatus.DONE){\n                var new_item = render_item.duplicate();\n                render_item.remove();\n                render_item = new_item;\n            }\n\n            render_item.render = true;\n\n            var om1 = app.project.renderQueue.item(i).outputModule(1);\n            var file_name = File.decode( om1.file.name ).replace('℗', ''); // Name contains special character, space?\n\n            var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE );\n\n            var targetFolder = new Folder(target_folder);\n            if (!targetFolder.exists) {\n                targetFolder.create();\n            }\n\n            om1.file = new File(targetFolder.fsName + '/' + file_name);\n        }else{\n            if (render_item.status != RQItemStatus.DONE){\n                render_item.render = false;\n            }\n        }\n\n    }\n    app.beginSuppressDialogs();\n    app.project.renderQueue.render();\n    app.endSuppressDialogs(false);\n}\n\nfunction close(){\n    app.project.close(CloseOptions.DO_NOT_SAVE_CHANGES);\n    app.quit();\n}\n\nfunction getAppVersion(){\n    return _prepareSingleValue(app.version);\n}\n\nfunction printMsg(msg){\n    alert(msg);\n}\n\nfunction addPlaceholder(name, width, height, fps, duration){\n    /** Add AE PlaceholderItem to Project list.\n     *\n     * PlaceholderItem chosen as it doesn't require existing file and\n     * might potentially allow nice functionality in the future.\n     *\n     */\n    app.beginUndoGroup('change comp properties');\n    try{\n        item = app.project.importPlaceholder(name, width, height,\n                                             fps, duration);\n\n        return _prepareSingleValue(item.id);\n    }catch (error) {\n        writeLn(_prepareError(\"Cannot add placeholder \" + error.toString()));\n    }\n    app.endUndoGroup();\n}\n\nfunction addItemInstead(placeholder_item_id, item_id){\n    /** Add new loaded item in place of load placeholder.\n     *\n     * Each placeholder could be placed multiple times into multiple\n     * composition. This loops through all compositions and\n     * places loaded item under placeholder.\n     * Placeholder item gets deleted later separately according\n     * to configuration in Settings.\n     *\n     * Args:\n     *      placeholder_item_id (int)\n     *      item_id (int)\n    */\n    var item = app.project.itemByID(item_id);\n    if (!item){\n        return _prepareError(\"There is no item with \"+ item_id);\n    }\n\n    app.beginUndoGroup('Add loaded items');\n    for (i = 1; i <= app.project.items.length; ++i){\n        var comp = app.project.items[i];\n        if (!(comp instanceof CompItem)){\n            continue\n        }\n\n        var i = 1;\n        while (i <= comp.numLayers) {\n            var layer = comp.layer(i);\n            var layer_source = layer.source;\n            if (layer_source && layer_source.id == placeholder_item_id){\n                var new_layer = comp.layers.add(item);\n                new_layer.moveAfter(layer);\n                // copy all(?) properties to new layer\n                layer.property(\"ADBE Transform Group\").copyToComp(new_layer);\n                i = i + 1;\n            }\n            i = i + 1;\n        }\n    }\n    app.endUndoGroup();\n}\n\nfunction _prepareSingleValue(value){\n    return JSON.stringify({\"result\": value})\n}\nfunction _prepareError(error_msg){\n    return JSON.stringify({\"error\": error_msg})\n}\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/launch_logic.py",
    "content": "import os\nimport sys\nimport subprocess\nimport collections\nimport logging\nimport asyncio\nimport functools\nimport traceback\n\n\nfrom wsrpc_aiohttp import (\n    WebSocketRoute,\n    WebSocketAsync\n)\n\nfrom qtpy import QtCore\n\nfrom openpype.lib import Logger\nfrom openpype.tests.lib import is_in_tests\nfrom openpype.pipeline import install_host, legacy_io\nfrom openpype.modules import ModulesManager\nfrom openpype.tools.utils import host_tools, get_openpype_qt_app\nfrom openpype.tools.adobe_webserver.app import WebServerTool\n\nfrom .ws_stub import get_stub\nfrom .lib import set_settings\n\nlog = logging.getLogger(__name__)\nlog.setLevel(logging.DEBUG)\n\n\ndef safe_excepthook(*args):\n    traceback.print_exception(*args)\n\n\ndef main(*subprocess_args):\n    \"\"\"Main entrypoint to AE launching, called from pre hook.\"\"\"\n    sys.excepthook = safe_excepthook\n\n    from openpype.hosts.aftereffects.api import AfterEffectsHost\n\n    host = AfterEffectsHost()\n    install_host(host)\n\n    os.environ[\"OPENPYPE_LOG_NO_COLORS\"] = \"False\"\n    app = get_openpype_qt_app()\n    app.setQuitOnLastWindowClosed(False)\n\n    launcher = ProcessLauncher(subprocess_args)\n    launcher.start()\n\n    if os.environ.get(\"HEADLESS_PUBLISH\"):\n        manager = ModulesManager()\n        webpublisher_addon = manager[\"webpublisher\"]\n\n        launcher.execute_in_main_thread(\n            functools.partial(\n                webpublisher_addon.headless_publish,\n                log,\n                \"CloseAE\",\n                is_in_tests()\n            )\n        )\n\n    elif os.environ.get(\"AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH\", True):\n        save = False\n        if os.getenv(\"WORKFILES_SAVE_AS\"):\n            save = True\n\n        launcher.execute_in_main_thread(\n            lambda: host_tools.show_tool_by_name(\"workfiles\", save=save)\n        )\n\n    sys.exit(app.exec_())\n\n\ndef show_tool_by_name(tool_name):\n    kwargs = {}\n    if tool_name == \"loader\":\n        kwargs[\"use_context\"] = True\n\n    host_tools.show_tool_by_name(tool_name, **kwargs)\n\n\nclass ProcessLauncher(QtCore.QObject):\n    \"\"\"Launches webserver, connects to it, runs main thread.\"\"\"\n    route_name = \"AfterEffects\"\n    _main_thread_callbacks = collections.deque()\n\n    def __init__(self, subprocess_args):\n        self._subprocess_args = subprocess_args\n        self._log = None\n\n        super(ProcessLauncher, self).__init__()\n\n        # Keep track if launcher was alreadu started\n        self._started = False\n\n        self._process = None\n        self._websocket_server = None\n\n        start_process_timer = QtCore.QTimer()\n        start_process_timer.setInterval(100)\n\n        loop_timer = QtCore.QTimer()\n        loop_timer.setInterval(200)\n\n        start_process_timer.timeout.connect(self._on_start_process_timer)\n        loop_timer.timeout.connect(self._on_loop_timer)\n\n        self._start_process_timer = start_process_timer\n        self._loop_timer = loop_timer\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(\"{}-launcher\".format(\n                self.route_name))\n        return self._log\n\n    @property\n    def websocket_server_is_running(self):\n        if self._websocket_server is not None:\n            return self._websocket_server.is_running\n        return False\n\n    @property\n    def is_process_running(self):\n        if self._process is not None:\n            return self._process.poll() is None\n        return False\n\n    @property\n    def is_host_connected(self):\n        \"\"\"Returns True if connected, False if app is not running at all.\"\"\"\n        if not self.is_process_running:\n            return False\n\n        try:\n\n            _stub = get_stub()\n            if _stub:\n                return True\n        except Exception:\n            pass\n\n        return None\n\n    @classmethod\n    def execute_in_main_thread(cls, callback):\n        cls._main_thread_callbacks.append(callback)\n\n    def start(self):\n        if self._started:\n            return\n        self.log.info(\"Started launch logic of AfterEffects\")\n        self._started = True\n        self._start_process_timer.start()\n\n    def exit(self):\n        \"\"\" Exit whole application. \"\"\"\n        if self._start_process_timer.isActive():\n            self._start_process_timer.stop()\n        if self._loop_timer.isActive():\n            self._loop_timer.stop()\n\n        if self._websocket_server is not None:\n            self._websocket_server.stop()\n\n        if self._process:\n            self._process.kill()\n            self._process.wait()\n\n        QtCore.QCoreApplication.exit()\n\n    def _on_loop_timer(self):\n        # TODO find better way and catch errors\n        # Run only callbacks that are in queue at the moment\n        cls = self.__class__\n        for _ in range(len(cls._main_thread_callbacks)):\n            if cls._main_thread_callbacks:\n                callback = cls._main_thread_callbacks.popleft()\n                callback()\n\n        if not self.is_process_running:\n            self.log.info(\"Host process is not running. Closing\")\n            self.exit()\n\n        elif not self.websocket_server_is_running:\n            self.log.info(\"Websocket server is not running. Closing\")\n            self.exit()\n\n    def _on_start_process_timer(self):\n        # TODO add try except validations for each part in this method\n        # Start server as first thing\n        if self._websocket_server is None:\n            self._init_server()\n            return\n\n        # TODO add waiting time\n        # Wait for webserver\n        if not self.websocket_server_is_running:\n            return\n\n        # Start application process\n        if self._process is None:\n            self._start_process()\n            self.log.info(\"Waiting for host to connect\")\n            return\n\n        # TODO add waiting time\n        # Wait until host is connected\n        if self.is_host_connected:\n            self._start_process_timer.stop()\n            self._loop_timer.start()\n        elif (\n            not self.is_process_running\n            or not self.websocket_server_is_running\n        ):\n            self.exit()\n\n    def _init_server(self):\n        if self._websocket_server is not None:\n            return\n\n        self.log.debug(\n            \"Initialization of websocket server for host communication\"\n        )\n\n        self._websocket_server = websocket_server = WebServerTool()\n        if websocket_server.port_occupied(\n            websocket_server.host_name,\n            websocket_server.port\n        ):\n            self.log.info(\n                \"Server already running, sending actual context and exit.\"\n            )\n            asyncio.run(websocket_server.send_context_change(self.route_name))\n            self.exit()\n            return\n\n        # Add Websocket route\n        websocket_server.add_route(\"*\", \"/ws/\", WebSocketAsync)\n        # Add after effects route to websocket handler\n\n        print(\"Adding {} route\".format(self.route_name))\n        WebSocketAsync.add_route(\n            self.route_name, AfterEffectsRoute\n        )\n        self.log.info(\"Starting websocket server for host communication\")\n        websocket_server.start_server()\n\n    def _start_process(self):\n        if self._process is not None:\n            return\n        self.log.info(\"Starting host process\")\n        try:\n            self._process = subprocess.Popen(\n                self._subprocess_args,\n                stdout=subprocess.DEVNULL,\n                stderr=subprocess.DEVNULL\n            )\n        except Exception:\n            self.log.info(\"exce\", exc_info=True)\n            self.exit()\n\n\nclass AfterEffectsRoute(WebSocketRoute):\n    \"\"\"\n        One route, mimicking external application (like Harmony, etc).\n        All functions could be called from client.\n        'do_notify' function calls function on the client - mimicking\n            notification after long running job on the server or similar\n    \"\"\"\n    instance = None\n\n    def init(self, **kwargs):\n        # Python __init__ must be return \"self\".\n        # This method might return anything.\n        log.debug(\"someone called AfterEffects route\")\n        self.instance = self\n        return kwargs\n\n    # server functions\n    async def ping(self):\n        log.debug(\"someone called AfterEffects route ping\")\n\n    # This method calls function on the client side\n    # client functions\n    async def set_context(self, project, asset, task):\n        \"\"\"\n            Sets 'project' and 'asset' to envs, eg. setting context\n\n            Args:\n                project (str)\n                asset (str)\n        \"\"\"\n        log.info(\"Setting context change\")\n        log.info(\"project {} asset {} \".format(project, asset))\n        if project:\n            legacy_io.Session[\"AVALON_PROJECT\"] = project\n            os.environ[\"AVALON_PROJECT\"] = project\n        if asset:\n            legacy_io.Session[\"AVALON_ASSET\"] = asset\n            os.environ[\"AVALON_ASSET\"] = asset\n        if task:\n            legacy_io.Session[\"AVALON_TASK\"] = task\n            os.environ[\"AVALON_TASK\"] = task\n\n    async def read(self):\n        log.debug(\"aftereffects.read client calls server server calls \"\n                  \"aftereffects client\")\n        return await self.socket.call('aftereffects.read')\n\n    # panel routes for tools\n    async def workfiles_route(self):\n        self._tool_route(\"workfiles\")\n\n    async def loader_route(self):\n        self._tool_route(\"loader\")\n\n    async def publish_route(self):\n        self._tool_route(\"publisher\")\n\n    async def sceneinventory_route(self):\n        self._tool_route(\"sceneinventory\")\n\n    async def setresolution_route(self):\n        self._settings_route(False, True)\n\n    async def setframes_route(self):\n        self._settings_route(True, False)\n\n    async def setall_route(self):\n        self._settings_route(True, True)\n\n    async def experimental_tools_route(self):\n        self._tool_route(\"experimental_tools\")\n\n    def _tool_route(self, _tool_name):\n        \"\"\"The address accessed when clicking on the buttons.\"\"\"\n\n        partial_method = functools.partial(show_tool_by_name,\n                                           _tool_name)\n\n        ProcessLauncher.execute_in_main_thread(partial_method)\n\n        # Required return statement.\n        return \"nothing\"\n\n    def _settings_route(self, frames, resolution):\n        partial_method = functools.partial(set_settings,\n                                           frames,\n                                           resolution)\n\n        ProcessLauncher.execute_in_main_thread(partial_method)\n\n        # Required return statement.\n        return \"nothing\"\n\n    def create_placeholder_route(self):\n        from openpype.hosts.aftereffects.api.workfile_template_builder import \\\n            create_placeholder\n        partial_method = functools.partial(create_placeholder)\n\n        ProcessLauncher.execute_in_main_thread(partial_method)\n\n        # Required return statement.\n        return \"nothing\"\n\n    def update_placeholder_route(self):\n        from openpype.hosts.aftereffects.api.workfile_template_builder import \\\n            update_placeholder\n        partial_method = functools.partial(update_placeholder)\n\n        ProcessLauncher.execute_in_main_thread(partial_method)\n\n        # Required return statement.\n        return \"nothing\"\n\n    def build_workfile_template_route(self):\n        from openpype.hosts.aftereffects.api.workfile_template_builder import \\\n            build_workfile_template\n        partial_method = functools.partial(build_workfile_template)\n\n        ProcessLauncher.execute_in_main_thread(partial_method)\n\n        # Required return statement.\n        return \"nothing\"\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/lib.py",
    "content": "import os\nimport re\nimport json\nimport contextlib\nimport logging\n\nfrom openpype.pipeline.context_tools import get_current_context\nfrom openpype.client import get_asset_by_name\nfrom .ws_stub import get_stub\n\nlog = logging.getLogger(__name__)\nlog.setLevel(logging.DEBUG)\n\n\n@contextlib.contextmanager\ndef maintained_selection():\n    \"\"\"Maintain selection during context.\"\"\"\n    selection = get_stub().get_selected_items(True, False, False)\n    try:\n        yield selection\n    finally:\n        pass\n\n\ndef get_extension_manifest_path():\n    return os.path.join(\n        os.path.dirname(os.path.abspath(__file__)),\n        \"extension\",\n        \"CSXS\",\n        \"manifest.xml\"\n    )\n\n\ndef get_unique_layer_name(layers, name):\n    \"\"\"\n        Gets all layer names and if 'name' is present in them, increases\n        suffix by 1 (eg. creates unique layer name - for Loader)\n    Args:\n        layers (list): of strings, names only\n        name (string):  checked value\n\n    Returns:\n        (string): name_00X (without version)\n    \"\"\"\n    names = {}\n    for layer in layers:\n        layer_name = re.sub(r'_\\d{3}$', '', layer)\n        if layer_name in names.keys():\n            names[layer_name] = names[layer_name] + 1\n        else:\n            names[layer_name] = 1\n    occurrences = names.get(name, 0)\n\n    return \"{}_{:0>3d}\".format(name, occurrences + 1)\n\n\ndef get_background_layers(file_url):\n    \"\"\"\n        Pulls file name from background json file, enrich with folder url for\n        AE to be able import files.\n\n        Order is important, follows order in json.\n\n        Args:\n            file_url (str): abs url of background json\n\n        Returns:\n            (list): of abs paths to images\n    \"\"\"\n    with open(file_url) as json_file:\n        data = json.load(json_file)\n\n    layers = list()\n    bg_folder = os.path.dirname(file_url)\n    for child in data['children']:\n        if child.get(\"filename\"):\n            layers.append(os.path.join(bg_folder, child.get(\"filename\")).\n                          replace(\"\\\\\", \"/\"))\n        else:\n            for layer in child['children']:\n                if layer.get(\"filename\"):\n                    layers.append(os.path.join(bg_folder,\n                                               layer.get(\"filename\")).\n                                  replace(\"\\\\\", \"/\"))\n    return layers\n\n\ndef get_asset_settings(asset_doc):\n    \"\"\"Get settings on current asset from database.\n\n    Returns:\n        dict: Scene data.\n\n    \"\"\"\n    asset_data = asset_doc[\"data\"]\n    fps = asset_data.get(\"fps\", 0)\n    frame_start = asset_data.get(\"frameStart\", 0)\n    frame_end = asset_data.get(\"frameEnd\", 0)\n    handle_start = asset_data.get(\"handleStart\", 0)\n    handle_end = asset_data.get(\"handleEnd\", 0)\n    resolution_width = asset_data.get(\"resolutionWidth\", 0)\n    resolution_height = asset_data.get(\"resolutionHeight\", 0)\n    duration = (frame_end - frame_start + 1) + handle_start + handle_end\n\n    return {\n        \"fps\": fps,\n        \"frameStart\": frame_start,\n        \"frameEnd\": frame_end,\n        \"handleStart\": handle_start,\n        \"handleEnd\": handle_end,\n        \"resolutionWidth\": resolution_width,\n        \"resolutionHeight\": resolution_height,\n        \"duration\": duration\n    }\n\n\ndef set_settings(frames, resolution, comp_ids=None, print_msg=True):\n    \"\"\"Sets number of frames and resolution to selected comps.\n\n    Args:\n        frames (bool): True if set frame info\n        resolution (bool): True if set resolution\n        comp_ids (list): specific composition ids, if empty\n            it tries to look for currently selected\n        print_msg (bool): True throw JS alert with msg\n    \"\"\"\n    frame_start = frames_duration = fps = width = height = None\n    current_context = get_current_context()\n\n    asset_doc = get_asset_by_name(current_context[\"project_name\"],\n                                  current_context[\"asset_name\"])\n    settings = get_asset_settings(asset_doc)\n\n    msg = ''\n    if frames:\n        frame_start = settings[\"frameStart\"] - settings[\"handleStart\"]\n        frames_duration = settings[\"duration\"]\n        fps = settings[\"fps\"]\n        msg += f\"frame start:{frame_start}, duration:{frames_duration}, \"\\\n               f\"fps:{fps}\"\n    if resolution:\n        width = settings[\"resolutionWidth\"]\n        height = settings[\"resolutionHeight\"]\n        msg += f\"width:{width} and height:{height}\"\n\n    stub = get_stub()\n    if not comp_ids:\n        comps = stub.get_selected_items(True, False, False)\n        comp_ids = [comp.id for comp in comps]\n    if not comp_ids:\n        stub.print_msg(\"Select at least one composition to apply settings.\")\n        return\n\n    for comp_id in comp_ids:\n        msg = f\"Setting for comp {comp_id} \" + msg\n        log.debug(msg)\n        stub.set_comp_properties(comp_id, frame_start, frames_duration,\n                                 fps, width, height)\n        if print_msg:\n            stub.print_msg(msg)\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/pipeline.py",
    "content": "import os\n\nfrom qtpy import QtWidgets\n\nimport pyblish.api\n\nfrom openpype.lib import Logger, register_event_callback\nfrom openpype.pipeline import (\n    register_loader_plugin_path,\n    register_creator_plugin_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.hosts.aftereffects.api.workfile_template_builder import (\n    AEPlaceholderLoadPlugin,\n    AEPlaceholderCreatePlugin\n)\nfrom openpype.pipeline.load import any_outdated_containers\nimport openpype.hosts.aftereffects\n\nfrom openpype.host import (\n    HostBase,\n    IWorkfileHost,\n    ILoadHost,\n    IPublishHost\n)\nfrom openpype.tools.utils import get_openpype_qt_app\n\nfrom .launch_logic import get_stub\nfrom .ws_stub import ConnectionNotEstablishedYet\n\nlog = Logger.get_logger(__name__)\n\n\nHOST_DIR = os.path.dirname(\n    os.path.abspath(openpype.hosts.aftereffects.__file__)\n)\nPLUGINS_DIR = os.path.join(HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\n\n\nclass AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):\n    name = \"aftereffects\"\n\n    def __init__(self):\n        self._stub = None\n        super(AfterEffectsHost, self).__init__()\n\n    @property\n    def stub(self):\n        \"\"\"\n            Handle pulling stub from PS to run operations on host\n        Returns:\n            (AEServerStub) or None\n        \"\"\"\n        if self._stub:\n            return self._stub\n\n        try:\n            stub = get_stub()  # only after Photoshop is up\n        except ConnectionNotEstablishedYet:\n            print(\"Not connected yet, ignoring\")\n            return\n\n        self._stub = stub\n        return self._stub\n\n    def install(self):\n        print(\"Installing Pype config...\")\n\n        pyblish.api.register_host(\"aftereffects\")\n        pyblish.api.register_plugin_path(PUBLISH_PATH)\n\n        register_loader_plugin_path(LOAD_PATH)\n        register_creator_plugin_path(CREATE_PATH)\n\n        register_event_callback(\"application.launched\", application_launch)\n\n    def get_workfile_extensions(self):\n        return [\".aep\"]\n\n    def save_workfile(self, dst_path=None):\n        self.stub.saveAs(dst_path, True)\n\n    def open_workfile(self, filepath):\n        self.stub.open(filepath)\n\n        return True\n\n    def get_current_workfile(self):\n        try:\n            full_name = get_stub().get_active_document_full_name()\n            if full_name and full_name != \"null\":\n                return os.path.normpath(full_name).replace(\"\\\\\", \"/\")\n        except ValueError:\n            print(\"Nothing opened\")\n            pass\n\n        return None\n\n    def get_containers(self):\n        return ls()\n\n    def get_context_data(self):\n        meta = self.stub.get_metadata()\n        for item in meta:\n            if item.get(\"id\") == \"publish_context\":\n                item.pop(\"id\")\n                return item\n\n        return {}\n\n    def update_context_data(self, data, changes):\n        item = data\n        item[\"id\"] = \"publish_context\"\n        self.stub.imprint(item[\"id\"], item)\n\n    def get_workfile_build_placeholder_plugins(self):\n        return [\n            AEPlaceholderLoadPlugin,\n            AEPlaceholderCreatePlugin\n        ]\n\n    # created instances section\n    def list_instances(self):\n        \"\"\"List all created instances from current workfile which\n        will be published.\n\n        Pulls from File > File Info\n\n        For SubsetManager\n\n        Returns:\n            (list) of dictionaries matching instances format\n        \"\"\"\n        stub = self.stub\n        if not stub:\n            return []\n\n        instances = []\n        layers_meta = stub.get_metadata()\n\n        for instance in layers_meta:\n            if instance.get(\"id\") == \"pyblish.avalon.instance\":\n                instances.append(instance)\n        return instances\n\n    def remove_instance(self, instance):\n        \"\"\"Remove instance from current workfile metadata.\n\n        Updates metadata of current file in File > File Info and removes\n        icon highlight on group layer.\n\n        For SubsetManager\n\n        Args:\n            instance (dict): instance representation from subsetmanager model\n        \"\"\"\n        stub = self.stub\n\n        if not stub:\n            return\n\n        inst_id = instance.get(\"instance_id\") or instance.get(\"uuid\")  # legacy\n        if not inst_id:\n            log.warning(\"No instance identifier for {}\".format(instance))\n            return\n\n        stub.remove_instance(inst_id)\n\n        if instance.get(\"members\"):\n            item = stub.get_item(instance[\"members\"][0])\n            if item:\n                stub.rename_item(item.id,\n                                 item.name.replace(stub.PUBLISH_ICON, ''))\n\n\ndef application_launch():\n    \"\"\"Triggered after start of app\"\"\"\n    check_inventory()\n\n\ndef ls():\n    \"\"\"Yields containers from active AfterEffects document.\n\n    This is the host-equivalent of api.ls(), but instead of listing\n    assets on disk, it lists assets already loaded in AE; once loaded\n    they are called 'containers'. Used in Manage tool.\n\n    Containers could be on multiple levels, single images/videos/was as a\n    FootageItem, or multiple items - backgrounds (folder with automatically\n    created composition and all imported layers).\n\n    Yields:\n        dict: container\n\n    \"\"\"\n    try:\n        stub = get_stub()  # only after AfterEffects is up\n    except ConnectionNotEstablishedYet:\n        print(\"Not connected yet, ignoring\")\n        return\n\n    layers_meta = stub.get_metadata()\n    for item in stub.get_items(comps=True,\n                               folders=True,\n                               footages=True):\n        data = stub.read(item, layers_meta)\n        # Skip non-tagged layers.\n        if not data:\n            continue\n\n        # Filter to only containers.\n        if \"container\" not in data[\"id\"]:\n            continue\n\n        # Append transient data\n        data[\"objectName\"] = item.name.replace(stub.LOADED_ICON, '')\n        data[\"layer\"] = item\n        yield data\n\n\ndef check_inventory():\n    \"\"\"Checks loaded containers if they are of highest version\"\"\"\n    if not any_outdated_containers():\n        return\n\n    # Warn about outdated containers.\n    _app = get_openpype_qt_app()\n\n    message_box = QtWidgets.QMessageBox()\n    message_box.setIcon(QtWidgets.QMessageBox.Warning)\n    msg = \"There are outdated containers in the scene.\"\n    message_box.setText(msg)\n    message_box.exec_()\n\n\ndef containerise(name,\n                 namespace,\n                 comp,\n                 context,\n                 loader=None,\n                 suffix=\"_CON\"):\n    \"\"\"\n    Containerisation enables a tracking of version, author and origin\n    for loaded assets.\n\n    Creates dictionary payloads that gets saved into file metadata. Each\n    container contains of who loaded (loader) and members (single or multiple\n    in case of background).\n\n    Arguments:\n        name (str): Name of resulting assembly\n        namespace (str): Namespace under which to host container\n        comp (AEItem): Composition to containerise\n        context (dict): Asset information\n        loader (str, optional): Name of loader used to produce this container.\n        suffix (str, optional): Suffix of container, defaults to `_CON`.\n\n    Returns:\n        container (str): Name of container assembly\n    \"\"\"\n    data = {\n        \"schema\": \"openpype:container-2.0\",\n        \"id\": AVALON_CONTAINER_ID,\n        \"name\": name,\n        \"namespace\": namespace,\n        \"loader\": str(loader),\n        \"representation\": str(context[\"representation\"][\"_id\"]),\n        \"members\": comp.members or [comp.id]\n    }\n\n    stub = get_stub()\n    stub.imprint(comp.id, data)\n\n    return comp\n\n\ndef cache_and_get_instances(creator):\n    \"\"\"Cache instances in shared data.\n\n    Storing all instances as a list as legacy instances might be still present.\n    Args:\n        creator (Creator): Plugin which would like to get instances from host.\n    Returns:\n        List[]: list of all instances stored in metadata\n    \"\"\"\n    shared_key = \"openpype.photoshop.instances\"\n    if shared_key not in creator.collection_shared_data:\n        creator.collection_shared_data[shared_key] = \\\n            creator.host.list_instances()\n    return creator.collection_shared_data[shared_key]\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/plugin.py",
    "content": "import six\nfrom abc import ABCMeta\n\nfrom openpype.pipeline import LoaderPlugin\nfrom .launch_logic import get_stub\n\n\n@six.add_metaclass(ABCMeta)\nclass AfterEffectsLoader(LoaderPlugin):\n    @staticmethod\n    def get_stub():\n        return get_stub()\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/workfile_template_builder.py",
    "content": "import os.path\nimport uuid\nimport shutil\n\nfrom openpype.pipeline import registered_host\nfrom openpype.tools.workfile_template_build import (\n    WorkfileBuildPlaceholderDialog,\n)\nfrom openpype.pipeline.workfile.workfile_template_builder import (\n    AbstractTemplateBuilder,\n    PlaceholderPlugin,\n    LoadPlaceholderItem,\n    CreatePlaceholderItem,\n    PlaceholderLoadMixin,\n    PlaceholderCreateMixin\n)\nfrom openpype.hosts.aftereffects.api import get_stub\nfrom openpype.hosts.aftereffects.api.lib import set_settings\n\nPLACEHOLDER_SET = \"PLACEHOLDERS_SET\"\nPLACEHOLDER_ID = \"openpype.placeholder\"\n\n\nclass AETemplateBuilder(AbstractTemplateBuilder):\n    \"\"\"Concrete implementation of AbstractTemplateBuilder for AE\"\"\"\n\n    def import_template(self, path):\n        \"\"\"Import template into current scene.\n        Block if a template is already loaded.\n\n        Args:\n            path (str): A path to current template (usually given by\n            get_template_preset implementation)\n\n        Returns:\n            bool: Whether the template was successfully imported or not\n        \"\"\"\n        stub = get_stub()\n        if not os.path.exists(path):\n            stub.print_msg(f\"Template file on {path} doesn't exist.\")\n            return\n\n        stub.save()\n        workfile_path = stub.get_active_document_full_name()\n        shutil.copy2(path, workfile_path)\n        stub.open(workfile_path)\n\n        return True\n\n\nclass AEPlaceholderPlugin(PlaceholderPlugin):\n    \"\"\"Contains generic methods for all PlaceholderPlugins.\"\"\"\n\n    def collect_placeholders(self):\n        \"\"\"Collect info from file metadata about created placeholders.\n\n        Returns:\n            (list) (LoadPlaceholderItem)\n        \"\"\"\n        output = []\n        scene_placeholders = self._collect_scene_placeholders()\n        for item in scene_placeholders:\n            if item.get(\"plugin_identifier\") != self.identifier:\n                continue\n\n            if isinstance(self, AEPlaceholderLoadPlugin):\n                item = LoadPlaceholderItem(item[\"uuid\"],\n                                           item[\"data\"],\n                                           self)\n            elif isinstance(self, AEPlaceholderCreatePlugin):\n                item = CreatePlaceholderItem(item[\"uuid\"],\n                                             item[\"data\"],\n                                             self)\n            else:\n                raise NotImplementedError(f\"Not implemented for {type(self)}\")\n\n            output.append(item)\n\n        return output\n\n    def update_placeholder(self, placeholder_item, placeholder_data):\n        \"\"\"Resave changed properties for placeholders\"\"\"\n        item_id, metadata_item = self._get_item(placeholder_item)\n        stub = get_stub()\n        if not item_id:\n            stub.print_msg(\"Cannot find item for \"\n                           f\"{placeholder_item.scene_identifier}\")\n            return\n        metadata_item[\"data\"] = placeholder_data\n        stub.imprint(item_id, metadata_item)\n\n    def _get_item(self, placeholder_item):\n        \"\"\"Returns item id and item metadata for placeholder from file meta\"\"\"\n        stub = get_stub()\n        placeholder_uuid = placeholder_item.scene_identifier\n        for metadata_item in stub.get_metadata():\n            if not metadata_item.get(\"is_placeholder\"):\n                continue\n            if placeholder_uuid in metadata_item.get(\"uuid\"):\n                return metadata_item[\"members\"][0], metadata_item\n        return None, None\n\n    def _collect_scene_placeholders(self):\n        \"\"\"\" Cache placeholder data to shared data.\n        Returns:\n            (list) of dicts\n        \"\"\"\n        placeholder_items = self.builder.get_shared_populate_data(\n            \"placeholder_items\"\n        )\n        if not placeholder_items:\n            placeholder_items = []\n            for item in get_stub().get_metadata():\n                if not item.get(\"is_placeholder\"):\n                    continue\n                placeholder_items.append(item)\n\n            self.builder.set_shared_populate_data(\n                \"placeholder_items\", placeholder_items\n            )\n        return placeholder_items\n\n    def _imprint_item(self, item_id, name, placeholder_data, stub):\n        if not item_id:\n            raise ValueError(\"Couldn't create a placeholder\")\n        container_data = {\n            \"id\": \"openpype.placeholder\",\n            \"name\": name,\n            \"is_placeholder\": True,\n            \"plugin_identifier\": self.identifier,\n            \"uuid\": str(uuid.uuid4()),  # scene_identifier\n            \"data\": placeholder_data,\n            \"members\": [item_id]\n        }\n        stub.imprint(item_id, container_data)\n\n\nclass AEPlaceholderCreatePlugin(AEPlaceholderPlugin, PlaceholderCreateMixin):\n    \"\"\"Adds Create placeholder.\n\n    This adds composition and runs Create\n    \"\"\"\n    identifier = \"aftereffects.create\"\n    label = \"AfterEffects create\"\n\n    def create_placeholder(self, placeholder_data):\n        stub = get_stub()\n        name = \"CREATEPLACEHOLDER\"\n        item_id = stub.add_item(name, \"COMP\")\n\n        self._imprint_item(item_id, name, placeholder_data, stub)\n\n    def populate_placeholder(self, placeholder):\n        \"\"\"Replace 'placeholder' with publishable instance.\n\n        Renames prepared composition name, creates publishable instance, sets\n        frame/duration settings according to DB.\n        \"\"\"\n        pre_create_data = {\"use_selection\": True}\n        item_id, item = self._get_item(placeholder)\n        get_stub().select_items([item_id])\n        self.populate_create_placeholder(placeholder, pre_create_data)\n\n        # apply settings for populated composition\n        item_id, metadata_item = self._get_item(placeholder)\n        set_settings(True, True, [item_id])\n\n    def get_placeholder_options(self, options=None):\n        return self.get_create_plugin_options(options)\n\n\nclass AEPlaceholderLoadPlugin(AEPlaceholderPlugin, PlaceholderLoadMixin):\n    identifier = \"aftereffects.load\"\n    label = \"AfterEffects load\"\n\n    def create_placeholder(self, placeholder_data):\n        \"\"\"Creates AE's Placeholder item in Project items list.\n\n         Sets dummy resolution/duration/fps settings, will be replaced when\n         populated.\n         \"\"\"\n        stub = get_stub()\n        name = \"LOADERPLACEHOLDER\"\n        item_id = stub.add_placeholder(name, 1920, 1060, 25, 10)\n\n        self._imprint_item(item_id, name, placeholder_data, stub)\n\n    def populate_placeholder(self, placeholder):\n        \"\"\"Use Openpype Loader from `placeholder` to create new FootageItems\n\n        New FootageItems are created, files are imported.\n        \"\"\"\n        self.populate_load_placeholder(placeholder)\n        errors = placeholder.get_errors()\n        stub = get_stub()\n        if errors:\n            stub.print_msg(\"\\n\".join(errors))\n        else:\n            if not placeholder.data[\"keep_placeholder\"]:\n                metadata = stub.get_metadata()\n                for item in metadata:\n                    if not item.get(\"is_placeholder\"):\n                        continue\n                    scene_identifier = item.get(\"uuid\")\n                    if (scene_identifier and\n                            scene_identifier == placeholder.scene_identifier):\n                        stub.delete_item(item[\"members\"][0])\n                stub.remove_instance(placeholder.scene_identifier, metadata)\n\n    def get_placeholder_options(self, options=None):\n        return self.get_load_plugin_options(options)\n\n    def load_succeed(self, placeholder, container):\n        placeholder_item_id, _ = self._get_item(placeholder)\n        item_id = container.id\n        get_stub().add_item_instead_placeholder(placeholder_item_id, item_id)\n\n\ndef build_workfile_template(*args, **kwargs):\n    builder = AETemplateBuilder(registered_host())\n    builder.build_template(*args, **kwargs)\n\n\ndef update_workfile_template(*args):\n    builder = AETemplateBuilder(registered_host())\n    builder.rebuild_template()\n\n\ndef create_placeholder(*args):\n    \"\"\"Called when new workile placeholder should be created.\"\"\"\n    host = registered_host()\n    builder = AETemplateBuilder(host)\n    window = WorkfileBuildPlaceholderDialog(host, builder)\n    window.exec_()\n\n\ndef update_placeholder(*args):\n    \"\"\"Called after placeholder item is selected to modify it.\"\"\"\n    host = registered_host()\n    builder = AETemplateBuilder(host)\n\n    stub = get_stub()\n    selected_items = stub.get_selected_items(True, True, True)\n\n    if len(selected_items) != 1:\n        stub.print_msg(\"Please select just 1 placeholder\")\n        return\n\n    selected_id = selected_items[0].id\n    placeholder_item = None\n\n    placeholder_items_by_id = {\n        placeholder_item.scene_identifier: placeholder_item\n        for placeholder_item in builder.get_placeholders()\n    }\n    for metadata_item in stub.get_metadata():\n        if not metadata_item.get(\"is_placeholder\"):\n            continue\n        if selected_id in metadata_item.get(\"members\"):\n            placeholder_item = placeholder_items_by_id.get(\n                metadata_item[\"uuid\"])\n            break\n\n    if not placeholder_item:\n        stub.print_msg(\"Didn't find placeholder metadata. \"\n                       \"Remove and re-create placeholder.\")\n        return\n\n    window = WorkfileBuildPlaceholderDialog(host, builder)\n    window.set_update_mode(placeholder_item)\n    window.exec_()\n"
  },
  {
    "path": "openpype/hosts/aftereffects/api/ws_stub.py",
    "content": "\"\"\"\n    Stub handling connection from server to client.\n    Used anywhere solution is calling client methods.\n\"\"\"\nimport json\nimport logging\n\nimport attr\n\nfrom wsrpc_aiohttp import WebSocketAsync\nfrom openpype.tools.adobe_webserver.app import WebServerTool\n\n\nclass ConnectionNotEstablishedYet(Exception):\n    pass\n\n\n@attr.s\nclass AEItem(object):\n    \"\"\"\n        Object denoting Item in AE. Each item is created in AE by any Loader,\n        but contains same fields, which are being used in later processing.\n    \"\"\"\n    # metadata\n    id = attr.ib()  # id created by AE, could be used for querying\n    name = attr.ib()  # name of item\n    item_type = attr.ib(default=None)  # item type (footage, folder, comp)\n    # all imported elements, single for\n    # regular image, array for Backgrounds\n    members = attr.ib(factory=list)\n    frameStart = attr.ib(default=None)\n    framesDuration = attr.ib(default=None)\n    frameRate = attr.ib(default=None)\n    file_name = attr.ib(default=None)\n    instance_id = attr.ib(default=None)  # New Publisher\n    width = attr.ib(default=None)\n    height = attr.ib(default=None)\n    is_placeholder = attr.ib(default=False)\n    uuid = attr.ib(default=False)\n    path = attr.ib(default=False)  # path to FootageItem to validate\n    # list of composition Footage is in\n    containing_comps = attr.ib(factory=list)\n\n\nclass AfterEffectsServerStub():\n    \"\"\"\n        Stub for calling function on client (Photoshop js) side.\n        Expects that client is already connected (started when avalon menu\n        is opened).\n        'self.websocketserver.call' is used as async wrapper\n    \"\"\"\n    PUBLISH_ICON = '\\u2117 '\n    LOADED_ICON = '\\u25bc'\n\n    def __init__(self):\n        self.websocketserver = WebServerTool.get_instance()\n        self.client = self.get_client()\n        self.log = logging.getLogger(self.__class__.__name__)\n\n    @staticmethod\n    def get_client():\n        \"\"\"\n            Return first connected client to WebSocket\n            TODO implement selection by Route\n        :return: <WebSocketAsync> client\n        \"\"\"\n        clients = WebSocketAsync.get_clients()\n        client = None\n        if len(clients) > 0:\n            key = list(clients.keys())[0]\n            client = clients.get(key)\n\n        return client\n\n    def open(self, path):\n        \"\"\"\n            Open file located at 'path' (local).\n        Args:\n            path(string): file path locally\n        Returns: None\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.open', path=path))\n\n        return self._handle_return(res)\n\n    def get_metadata(self):\n        \"\"\"\n            Get complete stored JSON with metadata from AE.Metadata.Label\n            field.\n\n            It contains containers loaded by any Loader OR instances created\n            by Creator.\n\n        Returns:\n            (list)\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.get_metadata'))\n        metadata = self._handle_return(res)\n\n        return metadata or []\n\n    def read(self, item, layers_meta=None):\n        \"\"\"\n            Parses item metadata from Label field of active document.\n            Used as filter to pick metadata for specific 'item' only.\n\n        Args:\n            item (AEItem): pulled info from AE\n            layers_meta (dict): full list from Headline\n                (load and inject for better performance in loops)\n        Returns:\n            (dict):\n        \"\"\"\n        if layers_meta is None:\n            layers_meta = self.get_metadata()\n        for item_meta in layers_meta:\n            if 'container' in item_meta.get('id') and \\\n                    str(item.id) == str(item_meta.get('members')[0]):\n                return item_meta\n\n        self.log.debug(\"Couldn't find layer metadata\")\n\n    def imprint(self, item_id, data, all_items=None, items_meta=None):\n        \"\"\"\n            Save item metadata to Label field of metadata of active document\n        Args:\n            item_id (int|str): id of FootageItem or instance_id for workfiles\n            data(string): json representation for single layer\n            all_items (list of item): for performance, could be\n                injected for usage in loop, if not, single call will be\n                triggered\n            items_meta(string): json representation from Headline\n                           (for performance - provide only if imprint is in\n                           loop - value should be same)\n        Returns: None\n        \"\"\"\n        if not items_meta:\n            items_meta = self.get_metadata()\n\n        result_meta = []\n        # fix existing\n        is_new = True\n\n        for item_meta in items_meta:\n            if ((item_meta.get('members') and\n                    str(item_id) == str(item_meta.get('members')[0])) or\n                    item_meta.get(\"instance_id\") == item_id):\n                is_new = False\n                if data:\n                    item_meta.update(data)\n                    result_meta.append(item_meta)\n            else:\n                result_meta.append(item_meta)\n\n        if is_new:\n            result_meta.append(data)\n\n        # Ensure only valid ids are stored.\n        if not all_items:\n            # loaders create FootageItem now\n            all_items = self.get_items(comps=True,\n                                       folders=True,\n                                       footages=True)\n        item_ids = [int(item.id) for item in all_items]\n        cleaned_data = []\n        for meta in result_meta:\n            # do not added instance with nonexistend item id\n            if meta.get(\"members\"):\n                if int(meta[\"members\"][0]) not in item_ids:\n                    continue\n\n            cleaned_data.append(meta)\n\n        payload = json.dumps(cleaned_data, indent=4)\n\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.imprint',\n                                         payload=payload))\n        return self._handle_return(res)\n\n    def get_active_document_full_name(self):\n        \"\"\"\n            Returns absolute path of active document via ws call\n        Returns(string): file name\n        \"\"\"\n        res = self.websocketserver.call(self.client.call(\n            'AfterEffects.get_active_document_full_name'))\n\n        return self._handle_return(res)\n\n    def get_active_document_name(self):\n        \"\"\"\n            Returns just a name of active document via ws call\n        Returns(string): file name\n        \"\"\"\n        res = self.websocketserver.call(self.client.call(\n            'AfterEffects.get_active_document_name'))\n\n        return self._handle_return(res)\n\n    def get_items(self, comps, folders=False, footages=False):\n        \"\"\"\n            Get all items from Project panel according to arguments.\n            There are multiple different types:\n                CompItem (could have multiple layers - source for Creator,\n                    will be rendered)\n                FolderItem (collection type, currently used for Background\n                    loading)\n                FootageItem (imported file - created by Loader)\n        Args:\n            comps (bool): return CompItems\n            folders (bool): return FolderItem\n            footages (bool: return FootageItem\n\n        Returns:\n            (list) of namedtuples\n        \"\"\"\n        res = self.websocketserver.call(\n            self.client.call('AfterEffects.get_items',\n                             comps=comps,\n                             folders=folders,\n                             footages=footages)\n              )\n        return self._to_records(self._handle_return(res))\n\n    def select_items(self, items):\n        \"\"\"\n            Select items in Project list\n        Args:\n            items (list): of int item ids\n        \"\"\"\n        self.websocketserver.call(\n            self.client.call('AfterEffects.select_items', items=items))\n\n\n    def get_selected_items(self, comps, folders=False, footages=False):\n        \"\"\"\n            Same as get_items but using selected items only\n        Args:\n            comps (bool): return CompItems\n            folders (bool): return FolderItem\n            footages (bool: return FootageItem\n\n        Returns:\n            (list) of namedtuples\n\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.get_selected_items',\n                                         comps=comps,\n                                         folders=folders,\n                                         footages=footages)\n                                        )\n        return self._to_records(self._handle_return(res))\n\n    def add_item(self, name, item_type):\n        \"\"\"\n            Adds either composition or folder to project item list.\n\n            Args:\n                name (str)\n                item_type (str): COMP|FOLDER\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.add_item',\n                                         name=name,\n                                         item_type=item_type))\n\n        return self._handle_return(res)\n\n    def get_item(self, item_id):\n        \"\"\"\n            Returns metadata for particular 'item_id' or None\n\n            Args:\n                item_id (int, or string)\n        \"\"\"\n        for item in self.get_items(True, True, True):\n            if str(item.id) == str(item_id):\n                return item\n\n        return None\n\n    def import_file(self, path, item_name, import_options=None):\n        \"\"\"\n            Imports file as a FootageItem. Used in Loader\n        Args:\n            path (string): absolute path for asset file\n            item_name (string): label for created FootageItem\n            import_options (dict): different files (img vs psd) need different\n                config\n\n        \"\"\"\n        res = self.websocketserver.call(\n            self.client.call('AfterEffects.import_file',\n                             path=path,\n                             item_name=item_name,\n                             import_options=import_options)\n            )\n        records = self._to_records(self._handle_return(res))\n        if records:\n            return records.pop()\n\n    def replace_item(self, item_id, path, item_name):\n        \"\"\" Replace FootageItem with new file\n\n            Args:\n                item_id (int):\n                path (string):absolute path\n                item_name (string): label on item in Project list\n\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.replace_item',\n                                         item_id=item_id,\n                                         path=path, item_name=item_name))\n\n        return self._handle_return(res)\n\n    def rename_item(self, item_id, item_name):\n        \"\"\" Replace item with item_name\n\n            Args:\n                item_id (int):\n                item_name (string): label on item in Project list\n\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.rename_item',\n                                         item_id=item_id,\n                                         item_name=item_name))\n\n        return self._handle_return(res)\n\n    def delete_item(self, item_id):\n        \"\"\" Deletes *Item in a file\n            Args:\n                item_id (int):\n\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.delete_item',\n                                         item_id=item_id))\n\n        return self._handle_return(res)\n\n    def remove_instance(self, instance_id, metadata=None):\n        \"\"\"\n            Removes instance with 'instance_id' from file's metadata and\n            saves them.\n\n            Keep matching item in file though.\n\n            Args:\n                instance_id(string): instance id\n        \"\"\"\n        cleaned_data = []\n\n        if metadata is None:\n            metadata = self.get_metadata()\n\n        for instance in metadata:\n            inst_id = instance.get(\"instance_id\") or instance.get(\"uuid\")\n            if inst_id != instance_id:\n                cleaned_data.append(instance)\n\n        payload = json.dumps(cleaned_data, indent=4)\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.imprint',\n                                         payload=payload))\n\n        return self._handle_return(res)\n\n    def is_saved(self):\n        # TODO\n        return True\n\n    def set_label_color(self, item_id, color_idx):\n        \"\"\"\n            Used for highlight additional information in Project panel.\n            Green color is loaded asset, blue is created asset\n        Args:\n            item_id (int):\n            color_idx (int): 0-16 Label colors from AE Project view\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.set_label_color',\n                                         item_id=item_id,\n                                         color_idx=color_idx))\n\n        return self._handle_return(res)\n\n    def get_comp_properties(self, comp_id):\n        \"\"\" Get composition information for render purposes\n\n            Returns startFrame, frameDuration, fps, width, height.\n\n            Args:\n                comp_id (int):\n\n            Returns:\n                (AEItem)\n\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.get_comp_properties',\n                                         item_id=comp_id\n                                         ))\n\n        records = self._to_records(self._handle_return(res))\n        if records:\n            return records.pop()\n\n    def set_comp_properties(self, comp_id, start, duration, frame_rate,\n                            width, height):\n        \"\"\"\n            Set work area to predefined values (from Ftrack).\n            Work area directs what gets rendered.\n            Beware of rounding, AE expects seconds, not frames directly.\n\n        Args:\n            comp_id (int):\n            start (int): workAreaStart in frames\n            duration (int): in frames\n            frame_rate (float): frames in seconds\n            width (int): resolution width\n            height (int): resolution height\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.set_comp_properties',\n                                         item_id=comp_id,\n                                         start=start,\n                                         duration=duration,\n                                         frame_rate=frame_rate,\n                                         width=width,\n                                         height=height))\n        return self._handle_return(res)\n\n    def save(self):\n        \"\"\"\n            Saves active document\n        Returns: None\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.save'))\n\n        return self._handle_return(res)\n\n    def saveAs(self, project_path, as_copy):\n        \"\"\"\n            Saves active project to aep (copy) or png or jpg\n        Args:\n            project_path(string): full local path\n            as_copy: <boolean>\n        Returns: None\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.saveAs',\n                                         image_path=project_path,\n                                         as_copy=as_copy))\n\n        return self._handle_return(res)\n\n    def get_render_info(self, comp_id):\n        \"\"\" Get render queue info for render purposes\n\n            Returns:\n               (list) of (AEItem): with 'file_name' field\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.get_render_info',\n                                         comp_id=comp_id))\n\n        records = self._to_records(self._handle_return(res))\n        return records\n\n    def get_audio_url(self, item_id):\n        \"\"\" Get audio layer absolute url for comp\n\n            Args:\n                item_id (int): composition id\n            Returns:\n                (str): absolute path url\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.get_audio_url',\n                                         item_id=item_id))\n\n        return self._handle_return(res)\n\n    def import_background(self, comp_id, comp_name, files):\n        \"\"\"\n            Imports backgrounds images to existing or new composition.\n\n            If comp_id is not provided, new composition is created, basic\n            values (width, heights, frameRatio) takes from first imported\n            image.\n\n            All images from background json are imported as a FootageItem and\n            separate layer is created for each of them under composition.\n\n            Order of imported 'files' is important.\n\n            Args:\n                comp_id (int): id of existing composition (null if new)\n                comp_name (str): used when new composition\n                files (list): list of absolute paths to import and\n                add as layers\n\n            Returns:\n                (AEItem): object with id of created folder, all imported images\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.import_background',\n                                         comp_id=comp_id,\n                                         comp_name=comp_name,\n                                         files=files))\n\n        records = self._to_records(self._handle_return(res))\n        if records:\n            return records.pop()\n\n    def reload_background(self, comp_id, comp_name, files):\n        \"\"\"\n            Reloads backgrounds images to existing composition.\n\n            It actually deletes complete folder with imported images and\n            created composition for safety.\n\n            Args:\n                comp_id (int): id of existing composition to be overwritten\n                comp_name (str): new name of composition (could be same as old\n                    if version up only)\n                files (list): list of absolute paths to import and\n                    add as layers\n            Returns:\n                (AEItem): object with id of created folder, all imported images\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.reload_background',\n                                         comp_id=comp_id,\n                                         comp_name=comp_name,\n                                         files=files))\n\n        records = self._to_records(self._handle_return(res))\n        if records:\n            return records.pop()\n\n    def add_item_as_layer(self, comp_id, item_id):\n        \"\"\"\n            Adds already imported FootageItem ('item_id') as a new\n            layer to composition ('comp_id').\n\n            Args:\n                comp_id (int): id of target composition\n                item_id (int): FootageItem.id\n                comp already found previously\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.add_item_as_layer',\n                                         comp_id=comp_id,\n                                         item_id=item_id))\n\n        records = self._to_records(self._handle_return(res))\n        if records:\n            return records.pop()\n\n    def add_item_instead_placeholder(self, placeholder_item_id, item_id):\n        \"\"\"\n            Adds item_id to layers where plaeholder_item_id is present.\n\n            1 placeholder could result in multiple loaded containers (eg items)\n\n            Args:\n                placeholder_item_id (int): id of placeholder item\n                item_id (int): loaded FootageItem id\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.add_item_instead_placeholder',  # noqa\n                                         placeholder_item_id=placeholder_item_id,  # noqa\n                                         item_id=item_id))\n\n        return self._handle_return(res)\n\n    def add_placeholder(self, name, width, height, fps, duration):\n        \"\"\"\n            Adds new FootageItem as a placeholder for workfile builder\n\n            Placeholder requires width etc, currently probably only hardcoded\n            values.\n\n            Args:\n                name (str)\n                width (int)\n                height (int)\n                fps (float)\n                duration (int)\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.add_placeholder',\n                                         name=name,\n                                         width=width,\n                                         height=height,\n                                         fps=fps,\n                                         duration=duration))\n\n        return self._handle_return(res)\n\n    def render(self, folder_url, comp_id):\n        \"\"\"\n            Render all renderqueueitem to 'folder_url'\n        Args:\n            folder_url(string): local folder path for collecting\n        Returns: None\n        \"\"\"\n        res = self.websocketserver.call(self.client.call\n                                        ('AfterEffects.render',\n                                         folder_url=folder_url,\n                                         comp_id=comp_id))\n        return self._handle_return(res)\n\n    def get_extension_version(self):\n        \"\"\"Returns version number of installed extension.\"\"\"\n        res = self.websocketserver.call(self.client.call(\n            'AfterEffects.get_extension_version'))\n\n        return self._handle_return(res)\n\n    def get_app_version(self):\n        \"\"\"Returns version number of installed application (17.5...).\"\"\"\n        res = self.websocketserver.call(self.client.call(\n            'AfterEffects.get_app_version'))\n\n        return self._handle_return(res)\n\n    def close(self):\n        res = self.websocketserver.call(self.client.call('AfterEffects.close'))\n\n        return self._handle_return(res)\n\n    def print_msg(self, msg):\n        \"\"\"Triggers Javascript alert dialog.\"\"\"\n        self.websocketserver.call(self.client.call\n                                  ('AfterEffects.print_msg',\n                                   msg=msg))\n\n    def _handle_return(self, res):\n        \"\"\"Wraps return, throws ValueError if 'error' key is present.\"\"\"\n        if res and isinstance(res, str) and res != \"undefined\":\n            try:\n                parsed = json.loads(res)\n            except json.decoder.JSONDecodeError:\n                raise ValueError(\"Received broken JSON {}\".format(res))\n\n            if not parsed:  # empty list\n                return parsed\n\n            first_item = parsed\n            if isinstance(parsed, list):\n                first_item = parsed[0]\n\n            if first_item:\n                if first_item.get(\"error\"):\n                    raise ValueError(first_item[\"error\"])\n                # singular values (file name etc)\n                if first_item.get(\"result\") is not None:\n                    return first_item[\"result\"]\n            return parsed  # parsed\n        return res\n\n    def _to_records(self, payload):\n        \"\"\"\n            Converts string json representation into list of AEItem\n            dot notation access to work.\n        Returns: <list of AEItem>\n            payload(dict): - dictionary from json representation, expected to\n                come from _handle_return\n        \"\"\"\n        if not payload:\n            return []\n\n        if isinstance(payload, str):  # safety fallback\n            try:\n                payload = json.loads(payload)\n            except json.decoder.JSONDecodeError:\n                raise ValueError(\"Received broken JSON {}\".format(payload))\n\n        if isinstance(payload, dict):\n            payload = [payload]\n\n        ret = []\n        # convert to AEItem to use dot donation\n        for d in payload:\n            if not d:\n                continue\n            # currently implemented and expected fields\n            item = AEItem(d.get('id'),\n                          d.get('name'),\n                          d.get('type'),\n                          d.get('members'),\n                          d.get('frameStart'),\n                          d.get('framesDuration'),\n                          d.get('frameRate'),\n                          d.get('file_name'),\n                          d.get(\"instance_id\"),\n                          d.get(\"width\"),\n                          d.get(\"height\"),\n                          d.get(\"is_placeholder\"),\n                          d.get(\"uuid\"),\n                          d.get(\"path\"),\n                          d.get(\"containing_comps\"),)\n\n            ret.append(item)\n        return ret\n\n\ndef get_stub():\n    \"\"\"\n        Convenience function to get server RPC stub to call methods directed\n        for host (Photoshop).\n        It expects already created connection, started from client.\n        Currently created when panel is opened (PS: Window>Extensions>Avalon)\n    :return: <PhotoshopClientStub> where functions could be called from\n    \"\"\"\n    ae_stub = AfterEffectsServerStub()\n    if not ae_stub.client:\n        raise ConnectionNotEstablishedYet(\"Connection is not created yet\")\n\n    return ae_stub\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/create/create_render.py",
    "content": "import re\n\nfrom openpype import resources\nfrom openpype.lib import BoolDef, UISeparatorDef\nfrom openpype.hosts.aftereffects import api\nfrom openpype.pipeline import (\n    Creator,\n    CreatedInstance,\n    CreatorError\n)\nfrom openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances\nfrom openpype.hosts.aftereffects.api.lib import set_settings\nfrom openpype.lib import prepare_template_data\nfrom openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS\n\n\nclass RenderCreator(Creator):\n    \"\"\"Creates 'render' instance for publishing.\n\n    Result of 'render' instance is video or sequence of images for particular\n    composition based of configuration in its RenderQueue.\n    \"\"\"\n    identifier = \"render\"\n    label = \"Render\"\n    family = \"render\"\n    description = \"Render creator\"\n\n    create_allow_context_change = True\n\n    # Settings\n    mark_for_review = True\n    force_setting_values = True\n\n    def create(self, subset_name_from_ui, data, pre_create_data):\n        stub = api.get_stub()  # only after After Effects is up\n\n        try:\n            _ = stub.get_active_document_full_name()\n        except ValueError:\n            raise CreatorError(\n                \"Please save workfile via Workfile app first!\"\n            )\n\n        if pre_create_data.get(\"use_selection\"):\n            comps = stub.get_selected_items(\n                comps=True, folders=False, footages=False\n            )\n        else:\n            comps = stub.get_items(comps=True, folders=False, footages=False)\n\n        if not comps:\n            raise CreatorError(\n                \"Nothing to create. Select composition in Project Bin if \"\n                \"'Use selection' is toggled or create at least \"\n                \"one composition.\"\n            )\n        use_composition_name = (pre_create_data.get(\"use_composition_name\") or\n                                len(comps) > 1)\n        for comp in comps:\n            composition_name = re.sub(\n                \"[^{}]+\".format(SUBSET_NAME_ALLOWED_SYMBOLS),\n                \"\",\n                comp.name\n            )\n            if use_composition_name:\n                if \"{composition}\" not in subset_name_from_ui.lower():\n                    subset_name_from_ui += \"{Composition}\"\n\n                dynamic_fill = prepare_template_data({\"composition\":\n                                                      composition_name})\n                subset_name = subset_name_from_ui.format(**dynamic_fill)\n                data[\"composition_name\"] = composition_name\n            else:\n                subset_name = subset_name_from_ui\n                subset_name = re.sub(r\"\\{composition\\}\", '', subset_name,\n                                     flags=re.IGNORECASE)\n\n            for inst in self.create_context.instances:\n                if subset_name == inst.subset_name:\n                    raise CreatorError(\"{} already exists\".format(\n                        inst.subset_name))\n\n            data[\"members\"] = [comp.id]\n            data[\"orig_comp_name\"] = composition_name\n\n            new_instance = CreatedInstance(self.family, subset_name, data,\n                                           self)\n            if \"farm\" in pre_create_data:\n                use_farm = pre_create_data[\"farm\"]\n                new_instance.creator_attributes[\"farm\"] = use_farm\n\n            review = pre_create_data[\"mark_for_review\"]\n            new_instance. creator_attributes[\"mark_for_review\"] = review\n\n            api.get_stub().imprint(new_instance.id,\n                                   new_instance.data_to_store())\n            self._add_instance_to_context(new_instance)\n\n            stub.rename_item(comp.id, subset_name)\n\n            if self.force_setting_values:\n                set_settings(True, True, [comp.id], print_msg=False)\n\n    def get_pre_create_attr_defs(self):\n        output = [\n            BoolDef(\"use_selection\",\n                    tooltip=\"Composition for publishable instance should be \"\n                            \"selected by default.\",\n                    default=True, label=\"Use selection\"),\n            BoolDef(\"use_composition_name\",\n                    label=\"Use composition name in subset\"),\n            UISeparatorDef(),\n            BoolDef(\"farm\", label=\"Render on farm\"),\n            BoolDef(\n                \"mark_for_review\",\n                label=\"Review\",\n                default=self.mark_for_review\n            )\n        ]\n        return output\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\"farm\", label=\"Render on farm\"),\n            BoolDef(\n                \"mark_for_review\",\n                label=\"Review\",\n                default=False\n            )\n        ]\n\n    def get_icon(self):\n        return resources.get_openpype_splash_filepath()\n\n    def collect_instances(self):\n        for instance_data in cache_and_get_instances(self):\n            # legacy instances have family=='render' or 'renderLocal', use them\n            creator_id = (instance_data.get(\"creator_identifier\") or\n                          instance_data.get(\"family\", '').replace(\"Local\", ''))\n            if creator_id == self.identifier:\n                instance_data = self._handle_legacy(instance_data)\n                instance = CreatedInstance.from_existing(\n                    instance_data, self\n                )\n                self._add_instance_to_context(instance)\n\n    def update_instances(self, update_list):\n        for created_inst, _changes in update_list:\n            api.get_stub().imprint(created_inst.get(\"instance_id\"),\n                                   created_inst.data_to_store())\n            subset_change = _changes.get(\"subset\")\n            if subset_change:\n                api.get_stub().rename_item(created_inst.data[\"members\"][0],\n                                           subset_change.new_value)\n\n    def remove_instances(self, instances):\n        \"\"\"Removes metadata and renames to original comp name if available.\"\"\"\n        for instance in instances:\n            self._remove_instance_from_context(instance)\n            self.host.remove_instance(instance)\n\n            comp_id = instance.data[\"members\"][0]\n            comp = api.get_stub().get_item(comp_id)\n            orig_comp_name = instance.data.get(\"orig_comp_name\")\n            if comp:\n                if orig_comp_name:\n                    new_comp_name = orig_comp_name\n                else:\n                    new_comp_name = \"dummyCompName\"\n                api.get_stub().rename_item(comp_id,\n                                           new_comp_name)\n\n    def apply_settings(self, project_settings):\n        plugin_settings = (\n            project_settings[\"aftereffects\"][\"create\"][\"RenderCreator\"]\n        )\n\n        self.mark_for_review = plugin_settings[\"mark_for_review\"]\n        self.force_setting_values = plugin_settings[\"force_setting_values\"]\n        self.default_variants = plugin_settings.get(\n            \"default_variants\",\n            plugin_settings.get(\"defaults\") or []\n        )\n\n    def get_detail_description(self):\n        return \"\"\"Creator for Render instances\n\n        Main publishable item in AfterEffects will be of `render` family.\n        Result of this item (instance) is picture sequence or video that could\n        be a final delivery product or loaded and used in another DCCs.\n\n        Select single composition and create instance of 'render' family or\n        turn off 'Use selection' to create instance for all compositions.\n\n        'Use composition name in subset' allows to explicitly add composition\n        name into created subset name.\n\n        Position of composition name could be set in\n        `project_settings/global/tools/creator/subset_name_profiles` with some\n        form of '{composition}' placeholder.\n\n        Composition name will be used implicitly if multiple composition should\n        be handled at same time.\n\n        If {composition} placeholder is not us 'subset_name_profiles'\n        composition name will be capitalized and set at the end of subset name\n        if necessary.\n\n        If composition name should be used, it will be cleaned up of characters\n        that would cause an issue in published file names.\n        \"\"\"\n\n    def get_dynamic_data(self, variant, task_name, asset_doc,\n                         project_name, host_name, instance):\n        dynamic_data = {}\n        if instance is not None:\n            composition_name = instance.get(\"composition_name\")\n            if composition_name:\n                dynamic_data[\"composition\"] = composition_name\n        else:\n            dynamic_data[\"composition\"] = \"{composition}\"\n\n        return dynamic_data\n\n    def _handle_legacy(self, instance_data):\n        \"\"\"Converts old instances to new format.\"\"\"\n        if not instance_data.get(\"members\"):\n            instance_data[\"members\"] = [instance_data.get(\"uuid\")]\n\n        if instance_data.get(\"uuid\"):\n            # uuid not needed, replaced with unique instance_id\n            api.get_stub().remove_instance(instance_data.get(\"uuid\"))\n            instance_data.pop(\"uuid\")\n\n        if not instance_data.get(\"task\"):\n            instance_data[\"task\"] = self.create_context.get_current_task_name()\n\n        if not instance_data.get(\"creator_attributes\"):\n            is_old_farm = instance_data[\"family\"] != \"renderLocal\"\n            instance_data[\"creator_attributes\"] = {\"farm\": is_old_farm}\n            instance_data[\"family\"] = self.family\n\n        if instance_data[\"creator_attributes\"].get(\"mark_for_review\") is None:\n            instance_data[\"creator_attributes\"][\"mark_for_review\"] = True\n\n        return instance_data\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/create/workfile_creator.py",
    "content": "from openpype import AYON_SERVER_ENABLED\nimport openpype.hosts.aftereffects.api as api\nfrom openpype.client import get_asset_by_name\nfrom openpype.pipeline import (\n    AutoCreator,\n    CreatedInstance\n)\nfrom openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances\n\n\nclass AEWorkfileCreator(AutoCreator):\n    identifier = \"workfile\"\n    family = \"workfile\"\n\n    default_variant = \"Main\"\n\n    def get_instance_attr_defs(self):\n        return []\n\n    def collect_instances(self):\n        for instance_data in cache_and_get_instances(self):\n            creator_id = instance_data.get(\"creator_identifier\")\n            if creator_id == self.identifier:\n                subset_name = instance_data[\"subset\"]\n                instance = CreatedInstance(\n                    self.family, subset_name, instance_data, self\n                )\n                self._add_instance_to_context(instance)\n\n    def update_instances(self, update_list):\n        # nothing to change on workfiles\n        pass\n\n    def create(self, options=None):\n        existing_instance = None\n        for instance in self.create_context.instances:\n            if instance.family == self.family:\n                existing_instance = instance\n                break\n\n        context = self.create_context\n        project_name = context.get_current_project_name()\n        asset_name = context.get_current_asset_name()\n        task_name = context.get_current_task_name()\n        host_name = context.host_name\n\n        existing_asset_name = None\n        if existing_instance is not None:\n            if AYON_SERVER_ENABLED:\n                existing_asset_name = existing_instance.get(\"folderPath\")\n\n            if existing_asset_name is None:\n                existing_asset_name = existing_instance[\"asset\"]\n\n        if existing_instance is None:\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                self.default_variant, task_name, asset_doc,\n                project_name, host_name\n            )\n            data = {\n                \"task\": task_name,\n                \"variant\": self.default_variant\n            }\n            if AYON_SERVER_ENABLED:\n                data[\"folderPath\"] = asset_name\n            else:\n                data[\"asset\"] = asset_name\n            data.update(self.get_dynamic_data(\n                self.default_variant, task_name, asset_doc,\n                project_name, host_name, None\n            ))\n\n            new_instance = CreatedInstance(\n                self.family, subset_name, data, self\n            )\n            self._add_instance_to_context(new_instance)\n\n            api.get_stub().imprint(new_instance.get(\"instance_id\"),\n                                   new_instance.data_to_store())\n\n        elif (\n            existing_asset_name != asset_name\n            or existing_instance[\"task\"] != task_name\n        ):\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                self.default_variant, task_name, asset_doc,\n                project_name, host_name\n            )\n            if AYON_SERVER_ENABLED:\n                existing_instance[\"folderPath\"] = asset_name\n            else:\n                existing_instance[\"asset\"] = asset_name\n\n            existing_instance[\"task\"] = task_name\n            existing_instance[\"subset\"] = subset_name\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/load/load_background.py",
    "content": "import re\n\nfrom openpype.pipeline import get_representation_path\nfrom openpype.hosts.aftereffects import api\n\nfrom openpype.hosts.aftereffects.api.lib import (\n    get_background_layers,\n    get_unique_layer_name,\n)\n\n\nclass BackgroundLoader(api.AfterEffectsLoader):\n    \"\"\"\n        Load images from Background family\n        Creates for each background separate folder with all imported images\n        from background json AND automatically created composition with layers,\n        each layer for separate image.\n\n        For each load container is created and stored in project (.aep)\n        metadata\n    \"\"\"\n    label = \"Load JSON Background\"\n    families = [\"background\"]\n    representations = [\"json\"]\n\n    def load(self, context, name=None, namespace=None, data=None):\n        stub = self.get_stub()\n        items = stub.get_items(comps=True)\n        existing_items = [layer.name.replace(stub.LOADED_ICON, '')\n                          for layer in items]\n\n        comp_name = get_unique_layer_name(\n            existing_items,\n            \"{}_{}\".format(context[\"asset\"][\"name\"], name))\n\n        path = self.filepath_from_context(context)\n        layers = get_background_layers(path)\n        if not layers:\n            raise ValueError(\"No layers found in {}\".format(path))\n\n        comp = stub.import_background(None, stub.LOADED_ICON + comp_name,\n                                      layers)\n\n        if not comp:\n            raise ValueError(\"Import background failed. \"\n                             \"Please contact support\")\n\n        self[:] = [comp]\n        namespace = namespace or comp_name\n\n        return api.containerise(\n            name,\n            namespace,\n            comp,\n            context,\n            self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n        \"\"\" Switch asset or change version \"\"\"\n        stub = self.get_stub()\n        context = representation.get(\"context\", {})\n        _ = container.pop(\"layer\")\n\n        # without iterator number (_001, 002...)\n        namespace_from_container = re.sub(r'_\\d{3}$', '',\n                                          container[\"namespace\"])\n        comp_name = \"{}_{}\".format(context[\"asset\"], context[\"subset\"])\n\n        # switching assets\n        if namespace_from_container != comp_name:\n            items = stub.get_items(comps=True)\n            existing_items = [layer.name for layer in items]\n            comp_name = get_unique_layer_name(\n                existing_items,\n                \"{}_{}\".format(context[\"asset\"], context[\"subset\"]))\n        else:  # switching version - keep same name\n            comp_name = container[\"namespace\"]\n\n        path = get_representation_path(representation)\n\n        layers = get_background_layers(path)\n        comp = stub.reload_background(container[\"members\"][1],\n                                      stub.LOADED_ICON + comp_name,\n                                      layers)\n\n        # update container\n        container[\"representation\"] = str(representation[\"_id\"])\n        container[\"name\"] = context[\"subset\"]\n        container[\"namespace\"] = comp_name\n        container[\"members\"] = comp.members\n\n        stub.imprint(comp.id, container)\n\n    def remove(self, container):\n        \"\"\"\n            Removes element from scene: deletes layer + removes from file\n            metadata.\n        Args:\n            container (dict): container to be removed - used to get layer_id\n        \"\"\"\n        stub = self.get_stub()\n        layer = container.pop(\"layer\")\n        stub.imprint(layer.id, {})\n        stub.delete_item(layer.id)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/load/load_file.py",
    "content": "import re\n\nfrom openpype.pipeline import get_representation_path\nfrom openpype.hosts.aftereffects import api\nfrom openpype.hosts.aftereffects.api.lib import get_unique_layer_name\n\n\nclass FileLoader(api.AfterEffectsLoader):\n    \"\"\"Load images\n\n    Stores the imported asset in a container named after the asset.\n    \"\"\"\n    label = \"Load file\"\n\n    families = [\"image\",\n                \"plate\",\n                \"render\",\n                \"prerender\",\n                \"review\",\n                \"audio\"]\n    representations = [\"*\"]\n\n    def load(self, context, name=None, namespace=None, data=None):\n        stub = self.get_stub()\n        layers = stub.get_items(comps=True, folders=True, footages=True)\n        existing_layers = [layer.name for layer in layers]\n        comp_name = get_unique_layer_name(\n            existing_layers, \"{}_{}\".format(context[\"asset\"][\"name\"], name))\n\n        import_options = {}\n\n        path = self.filepath_from_context(context)\n\n        if len(context[\"representation\"][\"files\"]) > 1:\n            import_options['sequence'] = True\n\n        if not path:\n            repr_id = context[\"representation\"][\"_id\"]\n            self.log.warning(\n                \"Representation id `{}` is failing to load\".format(repr_id))\n            return\n\n        path = path.replace(\"\\\\\", \"/\")\n        if '.psd' in path:\n            import_options['ImportAsType'] = 'ImportAsType.COMP'\n\n        comp = stub.import_file(path, stub.LOADED_ICON + comp_name,\n                                import_options)\n\n        if not comp:\n            self.log.warning(\n                \"Representation `{}` is failing to load\".format(path))\n            self.log.warning(\"Check host app for alert error.\")\n            return\n\n        self[:] = [comp]\n        namespace = namespace or comp_name\n\n        return api.containerise(\n            name,\n            namespace,\n            comp,\n            context,\n            self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n        \"\"\" Switch asset or change version \"\"\"\n        stub = self.get_stub()\n        layer = container.pop(\"layer\")\n\n        context = representation.get(\"context\", {})\n\n        namespace_from_container = re.sub(r'_\\d{3}$', '',\n                                          container[\"namespace\"])\n        layer_name = \"{}_{}\".format(context[\"asset\"], context[\"subset\"])\n        # switching assets\n        if namespace_from_container != layer_name:\n            layers = stub.get_items(comps=True)\n            existing_layers = [layer.name for layer in layers]\n            layer_name = get_unique_layer_name(\n                existing_layers,\n                \"{}_{}\".format(context[\"asset\"], context[\"subset\"]))\n        else:  # switching version - keep same name\n            layer_name = container[\"namespace\"]\n        path = get_representation_path(representation)\n        # with aftereffects.maintained_selection():  # TODO\n        stub.replace_item(layer.id, path, stub.LOADED_ICON + layer_name)\n        stub.imprint(\n            layer.id, {\"representation\": str(representation[\"_id\"]),\n                       \"name\": context[\"subset\"],\n                       \"namespace\": layer_name}\n        )\n\n    def remove(self, container):\n        \"\"\"\n            Removes element from scene: deletes layer + removes from Headline\n        Args:\n            container (dict): container to be removed - used to get layer_id\n        \"\"\"\n        stub = self.get_stub()\n        layer = container.pop(\"layer\")\n        stub.imprint(layer.id, {})\n        stub.delete_item(layer.id)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/add_publish_highlight.py",
    "content": "import pyblish.api\n\nfrom openpype.hosts.aftereffects.api import get_stub\n\n\nclass AddPublishHighlight(pyblish.api.InstancePlugin):\n    \"\"\"\n        Revert back rendered comp name and add publish highlight\n    \"\"\"\n\n    label = \"Add render highlight\"\n    order = pyblish.api.IntegratorOrder + 8.0\n    hosts = [\"aftereffects\"]\n    families = [\"render.farm\"]\n    optional = True\n\n    def process(self, instance):\n        stub = get_stub()\n        item = instance.data\n        # comp name contains highlight icon\n        stub.rename_item(item[\"comp_id\"], item[\"comp_name\"])\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/closeAE.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Close AE after publish. For Webpublishing only.\"\"\"\nimport pyblish.api\n\nfrom openpype.hosts.aftereffects.api import get_stub\n\n\nclass CloseAE(pyblish.api.ContextPlugin):\n    \"\"\"Close AE after publish. For Webpublishing only.\n    \"\"\"\n\n    order = pyblish.api.IntegratorOrder + 14\n    label = \"Close AE\"\n    optional = True\n    active = True\n\n    hosts = [\"aftereffects\"]\n    targets = [\"automated\"]\n\n    def process(self, context):\n        self.log.info(\"CloseAE\")\n\n        stub = get_stub()\n        self.log.info(\"Shutting down AE\")\n        stub.save()\n        stub.close()\n        self.log.info(\"AE closed\")\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/collect_audio.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.hosts.aftereffects.api import get_stub\n\n\nclass CollectAudio(pyblish.api.ContextPlugin):\n    \"\"\"Inject audio file url for rendered composition into context.\n        Needs to run AFTER 'collect_render'. Use collected comp_id to check\n        if there is an AVLayer in this composition\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.499\n    label = \"Collect Audio\"\n    hosts = [\"aftereffects\"]\n\n    def process(self, context):\n        for instance in context:\n            if 'render.farm' in instance.data.get(\"families\", []):\n                comp_id = instance.data[\"comp_id\"]\n                if not comp_id:\n                    self.log.debug(\"No comp_id filled in instance\")\n                    continue\n                context.data[\"audioFile\"] = os.path.normpath(\n                    get_stub().get_audio_url(comp_id)\n                ).replace(\"\\\\\", \"/\")\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/collect_current_file.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.hosts.aftereffects.api import get_stub\n\n\nclass CollectCurrentFile(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current working file into context\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.49\n    label = \"Current File\"\n    hosts = [\"aftereffects\"]\n\n    def process(self, context):\n        context.data[\"currentFile\"] = os.path.normpath(\n            get_stub().get_active_document_full_name()\n        ).replace(\"\\\\\", \"/\")\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/collect_extension_version.py",
    "content": "import os\nimport re\nimport pyblish.api\n\nfrom openpype.hosts.aftereffects.api import (\n    get_stub,\n    get_extension_manifest_path\n)\n\n\nclass CollectExtensionVersion(pyblish.api.ContextPlugin):\n    \"\"\" Pulls and compares version of installed extension.\n\n        It is recommended to use same extension as in provided Openpype code.\n\n        Please use Anastasiy’s Extension Manager or ZXPInstaller to update\n        extension in case of an error.\n\n        You can locate extension.zxp in your installed Openpype code in\n        `repos/avalon-core/avalon/aftereffects`\n    \"\"\"\n    # This technically should be a validator, but other collectors might be\n    # impacted with usage of obsolete extension, so collector that runs first\n    # was chosen\n    order = pyblish.api.CollectorOrder - 0.5\n    label = \"Collect extension version\"\n    hosts = [\"aftereffects\"]\n\n    optional = True\n    active = True\n\n    def process(self, context):\n        installed_version = get_stub().get_extension_version()\n\n        if not installed_version:\n            raise ValueError(\"Unknown version, probably old extension\")\n\n        manifest_url = get_extension_manifest_path()\n\n        if not os.path.exists(manifest_url):\n            self.log.debug(\"Unable to locate extension manifest, not checking\")\n            return\n\n        expected_version = None\n        with open(manifest_url) as fp:\n            content = fp.read()\n            found = re.findall(r'(ExtensionBundleVersion=\")([0-9\\.]+)(\")',\n                               content)\n            if found:\n                expected_version = found[0][1]\n\n        if expected_version != installed_version:\n            msg = (\n                \"Expected version '{}' found '{}'\\n Please update\"\n                \" your installed extension, it might not work properly.\"\n            ).format(expected_version, installed_version)\n\n            raise ValueError(msg)\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/collect_render.py",
    "content": "import os\nimport re\nimport tempfile\nimport attr\n\nimport pyblish.api\n\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import publish\nfrom openpype.pipeline.publish import RenderInstance\n\nfrom openpype.hosts.aftereffects.api import get_stub\n\n\n@attr.s\nclass AERenderInstance(RenderInstance):\n    # extend generic, composition name is needed\n    comp_name = attr.ib(default=None)\n    comp_id = attr.ib(default=None)\n    fps = attr.ib(default=None)\n    projectEntity = attr.ib(default=None)\n    stagingDir = attr.ib(default=None)\n    app_version = attr.ib(default=None)\n    publish_attributes = attr.ib(default={})\n    file_names = attr.ib(default=[])\n\n\nclass CollectAERender(publish.AbstractCollectRender):\n\n    order = pyblish.api.CollectorOrder + 0.405\n    label = \"Collect After Effects Render Layers\"\n    hosts = [\"aftereffects\"]\n\n    padding_width = 6\n    rendered_extension = 'png'\n\n    _stub = None\n\n    @classmethod\n    def get_stub(cls):\n        if not cls._stub:\n            cls._stub = get_stub()\n        return cls._stub\n\n    def get_instances(self, context):\n        instances = []\n        instances_to_remove = []\n\n        app_version = CollectAERender.get_stub().get_app_version()\n        app_version = app_version[0:4]\n\n        current_file = context.data[\"currentFile\"]\n        version = context.data[\"version\"]\n\n        project_entity = context.data[\"projectEntity\"]\n\n        compositions = CollectAERender.get_stub().get_items(True)\n        compositions_by_id = {item.id: item for item in compositions}\n        for inst in context:\n            if not inst.data.get(\"active\", True):\n                continue\n\n            family = inst.data[\"family\"]\n            if family not in [\"render\", \"renderLocal\"]:  # legacy\n                continue\n\n            comp_id = int(inst.data[\"members\"][0])\n\n            comp_info = CollectAERender.get_stub().get_comp_properties(\n                comp_id)\n\n            if not comp_info:\n                self.log.warning(\"Orphaned instance, deleting metadata\")\n                inst_id = inst.data.get(\"instance_id\") or str(comp_id)\n                CollectAERender.get_stub().remove_instance(inst_id)\n                continue\n\n            frame_start = comp_info.frameStart\n            frame_end = round(comp_info.frameStart +\n                              comp_info.framesDuration) - 1\n            fps = comp_info.frameRate\n            # TODO add resolution when supported by extension\n\n            task_name = inst.data.get(\"task\")  # legacy\n\n            render_q = CollectAERender.get_stub().get_render_info(comp_id)\n            if not render_q:\n                raise ValueError(\"No file extension set in Render Queue\")\n            render_item = render_q[0]\n\n            instance_families = inst.data.get(\"families\", [])\n            subset_name = inst.data[\"subset\"]\n            instance = AERenderInstance(\n                family=\"render\",\n                families=instance_families,\n                version=version,\n                time=\"\",\n                source=current_file,\n                label=\"{} - {}\".format(subset_name, family),\n                subset=subset_name,\n                asset=inst.data[\"asset\"],\n                task=task_name,\n                attachTo=False,\n                setMembers='',\n                publish=True,\n                name=subset_name,\n                resolutionWidth=render_item.width,\n                resolutionHeight=render_item.height,\n                pixelAspect=1,\n                tileRendering=False,\n                tilesX=0,\n                tilesY=0,\n                review=\"review\" in instance_families,\n                frameStart=frame_start,\n                frameEnd=frame_end,\n                frameStep=1,\n                fps=fps,\n                app_version=app_version,\n                publish_attributes=inst.data.get(\"publish_attributes\", {}),\n                file_names=[item.file_name for item in render_q]\n            )\n\n            comp = compositions_by_id.get(comp_id)\n            if not comp:\n                raise ValueError(\"There is no composition for item {}\".\n                                 format(comp_id))\n            instance.outputDir = self._get_output_dir(instance)\n            instance.comp_name = comp.name\n            instance.comp_id = comp_id\n\n            is_local = \"renderLocal\" in inst.data[\"family\"]  # legacy\n            if inst.data.get(\"creator_attributes\"):\n                is_local = not inst.data[\"creator_attributes\"].get(\"farm\")\n            if is_local:\n                # for local renders\n                instance = self._update_for_local(instance, project_entity)\n            else:\n                fam = \"render.farm\"\n                if fam not in instance.families:\n                    instance.families.append(fam)\n                instance.renderer = \"aerender\"\n                instance.farm = True  # to skip integrate\n                if \"review\" in instance.families:\n                    # to skip ExtractReview locally\n                    instance.families.remove(\"review\")\n\n            instances.append(instance)\n            instances_to_remove.append(inst)\n\n        for instance in instances_to_remove:\n            context.remove(instance)\n        return instances\n\n    def get_expected_files(self, render_instance):\n        \"\"\"\n            Returns list of rendered files that should be created by\n            Deadline. These are not published directly, they are source\n            for later 'submit_publish_job'.\n\n        Args:\n            render_instance (RenderInstance): to pull anatomy and parts used\n                in url\n\n        Returns:\n            (list) of absolute urls to rendered file\n        \"\"\"\n        start = render_instance.frameStart\n        end = render_instance.frameEnd\n\n        base_dir = self._get_output_dir(render_instance)\n        expected_files = []\n        for file_name in render_instance.file_names:\n            _, ext = os.path.splitext(os.path.basename(file_name))\n            ext = ext.replace('.', '')\n            version_str = \"v{:03d}\".format(render_instance.version)\n            if \"#\" not in file_name:  # single frame (mov)W\n                path = os.path.join(base_dir, \"{}_{}_{}.{}\".format(\n                    render_instance.asset,\n                    render_instance.subset,\n                    version_str,\n                    ext\n                ))\n                expected_files.append(path)\n            else:\n                for frame in range(start, end + 1):\n                    path = os.path.join(base_dir, \"{}_{}_{}.{}.{}\".format(\n                        render_instance.asset,\n                        render_instance.subset,\n                        version_str,\n                        str(frame).zfill(self.padding_width),\n                        ext\n                    ))\n                    expected_files.append(path)\n        return expected_files\n\n    def _get_output_dir(self, render_instance):\n        \"\"\"\n            Returns dir path of rendered files, used in submit_publish_job\n            for metadata.json location.\n            Should be in separate folder inside of work area.\n\n        Args:\n            render_instance (RenderInstance):\n\n        Returns:\n            (str): absolute path to rendered files\n        \"\"\"\n        # render to folder of workfile\n        base_dir = os.path.dirname(render_instance.source)\n        file_name, _ = os.path.splitext(\n            os.path.basename(render_instance.source))\n        base_dir = os.path.join(base_dir, 'renders', 'aftereffects', file_name)\n\n        # for submit_publish_job\n        return base_dir\n\n    def _update_for_local(self, instance, project_entity):\n        \"\"\"Update old saved instances to current publishing format\"\"\"\n        instance.stagingDir = tempfile.mkdtemp()\n        instance.projectEntity = project_entity\n        fam = \"render.local\"\n        if fam not in instance.families:\n            instance.families.append(fam)\n\n        return instance\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/collect_review.py",
    "content": "\"\"\"\nRequires:\n    None\n\nProvides:\n    instance     -> family (\"review\")\n\"\"\"\nimport pyblish.api\n\n\nclass CollectReview(pyblish.api.ContextPlugin):\n    \"\"\"Add review to families if instance created with 'mark_for_review' flag\n    \"\"\"\n    label = \"Collect Review\"\n    hosts = [\"aftereffects\"]\n    order = pyblish.api.CollectorOrder + 0.1\n\n    def process(self, context):\n        for instance in context:\n            creator_attributes = instance.data.get(\"creator_attributes\") or {}\n            if (\n                creator_attributes.get(\"mark_for_review\")\n                and \"review\" not in instance.data[\"families\"]\n            ):\n                instance.data[\"families\"].append(\"review\")\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/collect_workfile.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.client import get_asset_name_identifier\nfrom openpype.pipeline.create import get_subset_name\n\n\nclass CollectWorkfile(pyblish.api.ContextPlugin):\n    \"\"\" Adds the AE render instances \"\"\"\n\n    label = \"Collect After Effects Workfile Instance\"\n    order = pyblish.api.CollectorOrder + 0.1\n\n    default_variant = \"Main\"\n\n    def process(self, context):\n        existing_instance = None\n        for instance in context:\n            if instance.data[\"family\"] == \"workfile\":\n                self.log.debug(\"Workfile instance found, won't create new\")\n                existing_instance = instance\n                break\n\n        current_file = context.data[\"currentFile\"]\n        staging_dir = os.path.dirname(current_file)\n        scene_file = os.path.basename(current_file)\n        if existing_instance is None:  # old publish\n            instance = self._get_new_instance(context, scene_file)\n        else:\n            instance = existing_instance\n\n        # creating representation\n        representation = {\n            'name': 'aep',\n            'ext': 'aep',\n            'files': scene_file,\n            \"stagingDir\": staging_dir,\n        }\n\n        if not instance.data.get(\"representations\"):\n            instance.data[\"representations\"] = []\n        instance.data[\"representations\"].append(representation)\n\n        instance.data[\"publish\"] = instance.data[\"active\"]  # for DL\n\n    def _get_new_instance(self, context, scene_file):\n        task = context.data[\"task\"]\n        version = context.data[\"version\"]\n        asset_entity = context.data[\"assetEntity\"]\n        project_entity = context.data[\"projectEntity\"]\n\n        asset_name = get_asset_name_identifier(asset_entity)\n\n        instance_data = {\n            \"active\": True,\n            \"asset\": asset_name,\n            \"task\": task,\n            \"frameStart\": context.data['frameStart'],\n            \"frameEnd\": context.data['frameEnd'],\n            \"handleStart\": context.data['handleStart'],\n            \"handleEnd\": context.data['handleEnd'],\n            \"fps\": asset_entity[\"data\"][\"fps\"],\n            \"resolutionWidth\": asset_entity[\"data\"].get(\n                \"resolutionWidth\",\n                project_entity[\"data\"][\"resolutionWidth\"]),\n            \"resolutionHeight\": asset_entity[\"data\"].get(\n                \"resolutionHeight\",\n                project_entity[\"data\"][\"resolutionHeight\"]),\n            \"pixelAspect\": 1,\n            \"step\": 1,\n            \"version\": version\n        }\n\n        # workfile instance\n        family = \"workfile\"\n        subset = get_subset_name(\n            family,\n            self.default_variant,\n            context.data[\"anatomyData\"][\"task\"][\"name\"],\n            context.data[\"assetEntity\"],\n            context.data[\"anatomyData\"][\"project\"][\"name\"],\n            host_name=context.data[\"hostName\"],\n            project_settings=context.data[\"project_settings\"]\n        )\n        # Create instance\n        instance = context.create_instance(subset)\n\n        # creating instance data\n        instance.data.update({\n            \"subset\": subset,\n            \"label\": scene_file,\n            \"family\": family,\n            \"families\": [family],\n            \"representations\": list()\n        })\n\n        instance.data.update(instance_data)\n\n        return instance\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/extract_local_render.py",
    "content": "import os\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.aftereffects.api import get_stub\n\n\nclass ExtractLocalRender(publish.Extractor):\n    \"\"\"Render RenderQueue locally.\"\"\"\n\n    order = publish.Extractor.order - 0.47\n    label = \"Extract Local Render\"\n    hosts = [\"aftereffects\"]\n    families = [\"renderLocal\", \"render.local\"]\n\n    def process(self, instance):\n        stub = get_stub()\n        staging_dir = instance.data[\"stagingDir\"]\n        self.log.debug(\"staging_dir::{}\".format(staging_dir))\n\n        # pull file name collected value from Render Queue Output module\n        if not instance.data[\"file_names\"]:\n            raise ValueError(\"No file extension set in Render Queue\")\n\n        comp_id = instance.data['comp_id']\n        stub.render(staging_dir, comp_id)\n\n        representations = []\n        for file_name in instance.data[\"file_names\"]:\n            _, ext = os.path.splitext(os.path.basename(file_name))\n            ext = ext[1:]\n\n            first_file_path = None\n            files = []\n            for found_file_name in os.listdir(staging_dir):\n                if not found_file_name.endswith(ext):\n                    continue\n\n                files.append(found_file_name)\n                if first_file_path is None:\n                    first_file_path = os.path.join(staging_dir,\n                                                   found_file_name)\n\n            if not files:\n                self.log.info(\"no files\")\n                return\n\n            # single file cannot be wrapped in array\n            resulting_files = files\n            if len(files) == 1:\n                resulting_files = files[0]\n\n            repre_data = {\n                \"frameStart\": instance.data[\"frameStart\"],\n                \"frameEnd\": instance.data[\"frameEnd\"],\n                \"name\": ext,\n                \"ext\": ext,\n                \"files\": resulting_files,\n                \"stagingDir\": staging_dir\n            }\n            first_repre = not representations\n            if instance.data[\"review\"] and first_repre:\n                repre_data[\"tags\"] = [\"review\"]\n                # TODO return back when Extract from source same as regular\n                # thumbnail_path = os.path.join(staging_dir, files[0])\n                # instance.data[\"thumbnailSource\"] = thumbnail_path\n\n            representations.append(repre_data)\n\n        instance.data[\"representations\"] = representations\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/extract_save_scene.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.aftereffects.api import get_stub\n\n\nclass ExtractSaveScene(pyblish.api.ContextPlugin):\n    \"\"\"Save scene before extraction.\"\"\"\n\n    order = publish.Extractor.order - 0.48\n    label = \"Extract Save Scene\"\n    hosts = [\"aftereffects\"]\n\n    def process(self, context):\n        stub = get_stub()\n        stub.save()\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/help/validate_footage_items.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Footage item missing</title>\n<description>\n## Footage item missing\n\n FootageItem `{name}` contains missing `{path}`. Render will not produce any frames and AE will stop react to any integration\n### How to repair?\n\nRemove `{name}` or provide missing file.\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/help/validate_instance_asset.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Subset context</title>\n<description>\n## Invalid subset context\n\nContext of the given subset doesn't match your current scene.\n\n### How to repair?\n\nYou can fix this with \"repair\" button on the right and refresh Publish at the bottom right.\n</description>\n<detail>\n### __Detailed Info__ (optional)\n\nThis might happen if you are reuse old workfile and open it in different context.\n(Eg. you created subset \"renderCompositingDefault\" from asset \"Robot' in \"your_project_Robot_compositing.aep\", now you opened this workfile in a context \"Sloth\" but existing subset for \"Robot\" asset stayed in the workfile.)\n</detail>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/help/validate_scene_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Scene setting</title>\n<description>\n## Invalid scene setting found\n\nOne of the settings in a scene doesn't match to asset settings in database.\n\n{invalid_setting_str}\n\n### How to repair?\n\nChange values for {invalid_keys_str} in the scene OR change them in the asset database if they are wrong there.\n\n    In the scene it is right mouse click on published composition > `Composition Settings`.\n</description>\n<detail>\n### __Detailed Info__ (optional)\n\nThis error is shown when for example resolution in the scene doesn't match to resolution set on the asset in the database.\nEither value in the database or in the scene is wrong.\n</detail>\n</error>\n<error id=\"file_not_found\">\n<title>Scene file doesn't exist</title>\n<description>\n## Scene file doesn't exist\n\nCollected scene {scene_url} doesn't exist.\n\n### How to repair?\n\nRe-save file, start publish from the beginning again.\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/increment_workfile.py",
    "content": "import pyblish.api\nfrom openpype.lib import version_up\nfrom openpype.pipeline.publish import get_errored_plugins_from_context\n\nfrom openpype.hosts.aftereffects.api import get_stub\n\n\nclass IncrementWorkfile(pyblish.api.InstancePlugin):\n    \"\"\"Increment the current workfile.\n\n    Saves the current scene with an increased version number.\n    \"\"\"\n\n    label = \"Increment Workfile\"\n    order = pyblish.api.IntegratorOrder + 9.0\n    hosts = [\"aftereffects\"]\n    families = [\"workfile\"]\n    optional = True\n\n    def process(self, instance):\n        errored_plugins = get_errored_plugins_from_context(instance.context)\n        if errored_plugins:\n            raise RuntimeError(\n                \"Skipping incrementing current file because publishing failed.\"\n            )\n\n        scene_path = version_up(instance.context.data[\"currentFile\"])\n        get_stub().saveAs(scene_path, True)\n\n        self.log.info(\"Incremented workfile to: {}\".format(scene_path))\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py",
    "content": "import json\nimport pyblish.api\nfrom openpype.hosts.aftereffects.api import AfterEffectsHost\n\n\nclass PreCollectRender(pyblish.api.ContextPlugin):\n    \"\"\"\n    Checks if render instance is of old type, adds to families to both\n    existing collectors work same way.\n\n    Could be removed in the future when no one uses old publish.\n    \"\"\"\n\n    label = \"PreCollect Render\"\n    order = pyblish.api.CollectorOrder + 0.400\n    hosts = [\"aftereffects\"]\n\n    family_remapping = {\n        \"render\": (\"render.farm\", \"farm\"),   # (family, label)\n        \"renderLocal\": (\"render.local\", \"local\")\n    }\n\n    def process(self, context):\n        if context.data.get(\"newPublishing\"):\n            self.log.debug(\"Not applicable for New Publisher, skip\")\n            return\n\n        for inst in AfterEffectsHost().list_instances():\n            if inst.get(\"creator_attributes\"):\n                raise ValueError(\"Instance created in New publisher, \"\n                                 \"cannot be published in Pyblish.\\n\"\n                                 \"Please publish in New Publisher \"\n                                 \"or recreate instances with legacy Creators\")\n\n            if inst[\"family\"] not in self.family_remapping.keys():\n                continue\n\n            if not inst[\"members\"]:\n                raise ValueError(\"Couldn't find id, unable to publish. \" +\n                                 \"Please recreate instance.\")\n\n            instance = context.create_instance(inst[\"subset\"])\n            inst[\"families\"] = [self.family_remapping[inst[\"family\"]][0]]\n            instance.data.update(inst)\n\n            self._debug_log(instance)\n\n    def _debug_log(self, instance):\n        def _default_json(value):\n            return str(value)\n\n        self.log.info(\n            json.dumps(instance.data, indent=4, default=_default_json)\n        )\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/remove_publish_highlight.py",
    "content": "from openpype.pipeline import publish\nfrom openpype.hosts.aftereffects.api import get_stub\n\n\nclass RemovePublishHighlight(publish.Extractor):\n    \"\"\"Clean utf characters which are not working in DL\n\n        Published compositions are marked with unicode icon which causes\n        problems on specific render environments. Clean it first, sent to\n        rendering, add it later back to avoid confusion.\n    \"\"\"\n\n    order = publish.Extractor.order - 0.49  # just before save\n    label = \"Clean render comp\"\n    hosts = [\"aftereffects\"]\n    families = [\"render.farm\"]\n\n    def process(self, instance):\n        stub = get_stub()\n        self.log.debug(\"instance::{}\".format(instance.data))\n        item = instance.data\n        comp_name = item[\"comp_name\"].replace(stub.PUBLISH_ICON, '')\n        stub.rename_item(item[\"comp_id\"], comp_name)\n        instance.data[\"comp_name\"] = comp_name\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/validate_footage_items.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validate presence of footage items in composition\nRequires:\n\"\"\"\nimport os\n\nimport pyblish.api\n\nfrom openpype.pipeline import (\n    PublishXmlValidationError\n)\nfrom openpype.hosts.aftereffects.api import get_stub\n\n\nclass ValidateFootageItems(pyblish.api.InstancePlugin):\n    \"\"\"\n        Validates if FootageItems contained in composition exist.\n\n    AE fails silently and doesn't render anything if footage item file is\n    missing. This will result in nonresponsiveness of AE UI as it expects\n    reaction from user, but it will not provide dialog.\n    This validator tries to check existence of the files.\n    It will not protect from missing frame in multiframes though\n    (as AE api doesn't provide this information and it cannot be told how many\n    frames should be there easily). Missing frame is replaced by placeholder.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Footage Items\"\n    families = [\"render.farm\", \"render.local\", \"render\"]\n    hosts = [\"aftereffects\"]\n    optional = True\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n\n        comp_id = instance.data[\"comp_id\"]\n        for footage_item in get_stub().get_items(comps=False, folders=False,\n                                                 footages=True):\n            self.log.info(footage_item)\n            if comp_id not in footage_item.containing_comps:\n                continue\n\n            path = footage_item.path\n            if path and not os.path.exists(path):\n                msg = f\"File {path} not found.\"\n                formatting = {\"name\": footage_item.name, \"path\": path}\n                raise PublishXmlValidationError(self, msg,\n                                                formatting_data=formatting)\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import get_current_asset_name\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n)\nfrom openpype.hosts.aftereffects.api import get_stub\n\n\nclass ValidateInstanceAssetRepair(pyblish.api.Action):\n    \"\"\"Repair the instance asset with value from Context.\"\"\"\n\n    label = \"Repair\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n\n        # Get the errored instances\n        failed = []\n        for result in context.data[\"results\"]:\n            if (result[\"error\"] is not None and result[\"instance\"] is not None\n                    and result[\"instance\"] not in failed):\n                failed.append(result[\"instance\"])\n\n        # Apply pyblish.logic to get the instances for the plug-in\n        instances = pyblish.api.instances_by_plugin(failed, plugin)\n        stub = get_stub()\n        for instance in instances:\n            data = stub.read(instance[0])\n\n            data[\"asset\"] = get_current_asset_name()\n            stub.imprint(instance[0].instance_id, data)\n\n\nclass ValidateInstanceAsset(pyblish.api.InstancePlugin):\n    \"\"\"Validate the instance asset is the current selected context asset.\n\n        As it might happen that multiple worfiles are opened at same time,\n        switching between them would mess with selected context. (From Launcher\n        or Ftrack).\n\n        In that case outputs might be output under wrong asset!\n\n        Repair action will use Context asset value (from Workfiles or Launcher)\n        Closing and reopening with Workfiles will refresh  Context value.\n    \"\"\"\n\n    label = \"Validate Instance Asset\"\n    hosts = [\"aftereffects\"]\n    actions = [ValidateInstanceAssetRepair]\n    order = ValidateContentsOrder\n\n    def process(self, instance):\n        instance_asset = instance.data[\"asset\"]\n        current_asset = get_current_asset_name()\n        msg = (\n            f\"Instance asset {instance_asset} is not the same \"\n            f\"as current context {current_asset}.\"\n        )\n\n        if instance_asset != current_asset:\n            raise PublishXmlValidationError(self, msg)\n"
  },
  {
    "path": "openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validate scene settings.\nRequires:\n    instance    -> assetEntity\n    instance    -> anatomyData\n\"\"\"\nimport os\nimport re\n\nimport pyblish.api\n\nfrom openpype.pipeline import (\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.hosts.aftereffects.api import get_asset_settings\n\n\nclass ValidateSceneSettings(OptionalPyblishPluginMixin,\n                            pyblish.api.InstancePlugin):\n    \"\"\"\n        Ensures that Composition Settings (right mouse on comp) are same as\n        in FTrack on task.\n\n        By default checks only duration - how many frames should be rendered.\n        Compares:\n            Frame start - Frame end + 1 from FTrack\n                against\n            Duration in Composition Settings.\n\n        If this complains:\n            Check error message where is discrepancy.\n            Check FTrack task 'pype' section of task attributes for expected\n            values.\n            Check/modify rendered Composition Settings.\n\n        If you know what you are doing run publishing again, uncheck this\n        validation before Validation phase.\n    \"\"\"\n\n    \"\"\"\n        Dev docu:\n        Could be configured by 'presets/plugins/aftereffects/publish'\n\n        skip_timelines_check - fill task name for which skip validation of\n            frameStart\n            frameEnd\n            fps\n            handleStart\n            handleEnd\n        skip_resolution_check - fill entity type ('asset') to skip validation\n            resolutionWidth\n            resolutionHeight\n            TODO support in extension is missing for now\n\n         By defaults validates duration (how many frames should be published)\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Scene Settings\"\n    families = [\"render.farm\", \"render.local\", \"render\"]\n    hosts = [\"aftereffects\"]\n    optional = True\n\n    skip_timelines_check = [\".*\"]  # * >> skip for all\n    skip_resolution_check = [\".*\"]\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        # Skip the instance if is not active by data on the instance\n        if not self.is_active(instance.data):\n            return\n\n        asset_doc = instance.data[\"assetEntity\"]\n        expected_settings = get_asset_settings(asset_doc)\n        self.log.info(\"config from DB::{}\".format(expected_settings))\n\n        task_name = instance.data[\"anatomyData\"][\"task\"][\"name\"]\n        if any(re.search(pattern, task_name)\n                for pattern in self.skip_resolution_check):\n            expected_settings.pop(\"resolutionWidth\")\n            expected_settings.pop(\"resolutionHeight\")\n\n        if any(re.search(pattern, task_name)\n                for pattern in self.skip_timelines_check):\n            expected_settings.pop('fps', None)\n            expected_settings.pop('frameStart', None)\n            expected_settings.pop('frameEnd', None)\n            expected_settings.pop('handleStart', None)\n            expected_settings.pop('handleEnd', None)\n\n        # handle case where ftrack uses only two decimal places\n        # 23.976023976023978 vs. 23.98\n        fps = instance.data.get(\"fps\")\n        if fps:\n            if isinstance(fps, float):\n                fps = float(\n                    \"{:.2f}\".format(fps))\n            expected_settings[\"fps\"] = fps\n\n        duration = instance.data.get(\"frameEndHandle\") - \\\n            instance.data.get(\"frameStartHandle\") + 1\n\n        self.log.debug(\"validated items::{}\".format(expected_settings))\n\n        current_settings = {\n            \"fps\": fps,\n            \"frameStart\": instance.data.get(\"frameStart\"),\n            \"frameEnd\": instance.data.get(\"frameEnd\"),\n            \"handleStart\": instance.data.get(\"handleStart\"),\n            \"handleEnd\": instance.data.get(\"handleEnd\"),\n            \"frameStartHandle\": instance.data.get(\"frameStartHandle\"),\n            \"frameEndHandle\": instance.data.get(\"frameEndHandle\"),\n            \"resolutionWidth\": instance.data.get(\"resolutionWidth\"),\n            \"resolutionHeight\": instance.data.get(\"resolutionHeight\"),\n            \"duration\": duration\n        }\n        self.log.info(\"current_settings:: {}\".format(current_settings))\n\n        invalid_settings = []\n        invalid_keys = set()\n        for key, value in expected_settings.items():\n            if value != current_settings[key]:\n                msg = \"'{}' expected: '{}'  found: '{}'\".format(\n                    key, value, current_settings[key])\n\n                if key == \"duration\" and expected_settings.get(\"handleStart\"):\n                    msg += \"Handles included in calculation. Remove \" \\\n                           \"handles in DB or extend frame range in \" \\\n                           \"Composition Setting.\"\n\n                invalid_settings.append(msg)\n                invalid_keys.add(key)\n\n        if invalid_settings:\n            msg = \"Found invalid settings:\\n{}\".format(\n                \"\\n\".join(invalid_settings)\n            )\n\n            invalid_keys_str = \",\".join(invalid_keys)\n            break_str = \"<br/>\"\n            invalid_setting_str = \"<b>Found invalid settings:</b><br/>{}\".\\\n                format(break_str.join(invalid_settings))\n\n            formatting_data = {\n                \"invalid_setting_str\": invalid_setting_str,\n                \"invalid_keys_str\": invalid_keys_str\n            }\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n\n        if not os.path.exists(instance.data.get(\"source\")):\n            scene_url = instance.data.get(\"source\")\n            msg = \"Scene file {} not found (saved under wrong name)\".format(\n                scene_url\n            )\n            formatting_data = {\n                \"scene_url\": scene_url\n            }\n            raise PublishXmlValidationError(self, msg, key=\"file_not_found\",\n                                            formatting_data=formatting_data)\n"
  },
  {
    "path": "openpype/hosts/blender/__init__.py",
    "content": "from .addon import BlenderAddon\n\n\n__all__ = (\n    \"BlenderAddon\",\n)\n"
  },
  {
    "path": "openpype/hosts/blender/addon.py",
    "content": "import os\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nBLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass BlenderAddon(OpenPypeModule, IHostAddon):\n    name = \"blender\"\n    host_name = \"blender\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        \"\"\"Modify environments to contain all required for implementation.\"\"\"\n        # Prepare path to implementation script\n        implementation_user_script_path = os.path.join(\n            BLENDER_ROOT_DIR,\n            \"blender_addon\"\n        )\n\n        # Add blender implementation script path to PYTHONPATH\n        python_path = env.get(\"PYTHONPATH\") or \"\"\n        python_path_parts = [\n            path\n            for path in python_path.split(os.pathsep)\n            if path\n        ]\n        python_path_parts.insert(0, implementation_user_script_path)\n        env[\"PYTHONPATH\"] = os.pathsep.join(python_path_parts)\n\n        # Modify Blender user scripts path\n        previous_user_scripts = set()\n        # Implementation path is added to set for easier paths check inside\n        #   loops - will be removed at the end\n        previous_user_scripts.add(implementation_user_script_path)\n\n        openpype_blender_user_scripts = (\n            env.get(\"OPENPYPE_BLENDER_USER_SCRIPTS\") or \"\"\n        )\n        for path in openpype_blender_user_scripts.split(os.pathsep):\n            if path:\n                previous_user_scripts.add(os.path.normpath(path))\n\n        blender_user_scripts = env.get(\"BLENDER_USER_SCRIPTS\") or \"\"\n        for path in blender_user_scripts.split(os.pathsep):\n            if path:\n                previous_user_scripts.add(os.path.normpath(path))\n\n        # Remove implementation path from user script paths as is set to\n        #   `BLENDER_USER_SCRIPTS`\n        previous_user_scripts.remove(implementation_user_script_path)\n        env[\"BLENDER_USER_SCRIPTS\"] = implementation_user_script_path\n\n        # Set custom user scripts env\n        env[\"OPENPYPE_BLENDER_USER_SCRIPTS\"] = os.pathsep.join(\n            previous_user_scripts\n        )\n\n        # Define Qt binding if not defined\n        if not env.get(\"QT_PREFERRED_BINDING\"):\n            env[\"QT_PREFERRED_BINDING\"] = \"PySide2\"\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [\n            os.path.join(BLENDER_ROOT_DIR, \"hooks\")\n        ]\n\n    def get_workfile_extensions(self):\n        return [\".blend\"]\n"
  },
  {
    "path": "openpype/hosts/blender/api/__init__.py",
    "content": "\"\"\"Public API\n\nAnything that isn't defined here is INTERNAL and unreliable for external use.\n\n\"\"\"\n\nfrom .pipeline import (\n    install,\n    uninstall,\n    ls,\n    publish,\n    containerise,\n    BlenderHost,\n)\n\nfrom .plugin import (\n    Creator,\n    Loader,\n)\n\nfrom .workio import (\n    open_file,\n    save_file,\n    current_file,\n    has_unsaved_changes,\n    file_extensions,\n    work_root,\n)\n\nfrom .lib import (\n    lsattr,\n    lsattrs,\n    read,\n    maintained_selection,\n    maintained_time,\n    get_selection,\n    # unique_name,\n)\n\nfrom .capture import capture\n\nfrom .render_lib import prepare_rendering\n\n\n__all__ = [\n    \"install\",\n    \"uninstall\",\n    \"ls\",\n    \"publish\",\n    \"containerise\",\n    \"BlenderHost\",\n\n    \"Creator\",\n    \"Loader\",\n\n    # Workfiles API\n    \"open_file\",\n    \"save_file\",\n    \"current_file\",\n    \"has_unsaved_changes\",\n    \"file_extensions\",\n    \"work_root\",\n\n    # Utility functions\n    \"maintained_selection\",\n    \"maintained_time\",\n    \"lsattr\",\n    \"lsattrs\",\n    \"read\",\n    \"get_selection\",\n    \"capture\",\n    # \"unique_name\",\n    \"prepare_rendering\",\n]\n"
  },
  {
    "path": "openpype/hosts/blender/api/action.py",
    "content": "import bpy\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import get_errored_instances_from_context\n\n\nclass SelectInvalidAction(pyblish.api.Action):\n    \"\"\"Select invalid objects in Blender when a publish plug-in failed.\"\"\"\n    label = \"Select Invalid\"\n    on = \"failed\"\n    icon = \"search\"\n\n    def process(self, context, plugin):\n        errored_instances = get_errored_instances_from_context(context,\n                                                               plugin=plugin)\n\n        # Get the invalid nodes for the plug-ins\n        self.log.info(\"Finding invalid nodes...\")\n        invalid = list()\n        for instance in errored_instances:\n            invalid_nodes = plugin.get_invalid(instance)\n            if invalid_nodes:\n                if isinstance(invalid_nodes, (list, tuple)):\n                    invalid.extend(invalid_nodes)\n                else:\n                    self.log.warning(\n                        \"Failed plug-in doesn't have any selectable objects.\"\n                    )\n\n        bpy.ops.object.select_all(action='DESELECT')\n\n        # Make sure every node is only processed once\n        invalid = list(set(invalid))\n        if not invalid:\n            self.log.info(\"No invalid nodes found.\")\n            return\n\n        invalid_names = [obj.name for obj in invalid]\n        self.log.info(\n            \"Selecting invalid objects: %s\", \", \".join(invalid_names)\n        )\n        # Select the objects and also make the last one the active object.\n        for obj in invalid:\n            obj.select_set(True)\n\n        bpy.context.view_layer.objects.active = invalid[-1]\n"
  },
  {
    "path": "openpype/hosts/blender/api/capture.py",
    "content": "\n\"\"\"Blender Capture\nPlayblasting with independent viewport, camera and display options\n\"\"\"\nimport contextlib\nimport bpy\n\nfrom .lib import maintained_time\nfrom .plugin import deselect_all, create_blender_context\n\n\ndef capture(\n    camera=None,\n    width=None,\n    height=None,\n    filename=None,\n    start_frame=None,\n    end_frame=None,\n    step_frame=None,\n    sound=None,\n    isolate=None,\n    maintain_aspect_ratio=True,\n    overwrite=False,\n    image_settings=None,\n    display_options=None\n):\n    \"\"\"Playblast in an independent windows\n    Arguments:\n        camera (str, optional): Name of camera, defaults to \"Camera\"\n        width (int, optional): Width of output in pixels\n        height (int, optional): Height of output in pixels\n        filename (str, optional): Name of output file path. Defaults to current\n            render output path.\n        start_frame (int, optional): Defaults to current start frame.\n        end_frame (int, optional): Defaults to current end frame.\n        step_frame (int, optional): Defaults to 1.\n        sound (str, optional):  Specify the sound node to be used during\n            playblast. When None (default) no sound will be used.\n        isolate (list): List of nodes to isolate upon capturing\n        maintain_aspect_ratio (bool, optional): Modify height in order to\n            maintain aspect ratio.\n        overwrite (bool, optional): Whether or not to overwrite if file\n            already exists. If disabled and file exists and error will be\n            raised.\n        image_settings (dict, optional): Supplied image settings for render,\n            using `ImageSettings`\n        display_options (dict, optional): Supplied display options for render\n    \"\"\"\n\n    scene = bpy.context.scene\n    camera = camera or \"Camera\"\n\n    # Ensure camera exists.\n    if camera not in scene.objects and camera != \"AUTO\":\n        raise RuntimeError(\"Camera does not exist: {0}\".format(camera))\n\n    # Ensure resolution.\n    if width and height:\n        maintain_aspect_ratio = False\n    width = width or scene.render.resolution_x\n    height = height or scene.render.resolution_y\n    if maintain_aspect_ratio:\n        ratio = scene.render.resolution_x / scene.render.resolution_y\n        height = round(width / ratio)\n\n    # Get frame range.\n    if start_frame is None:\n        start_frame = scene.frame_start\n    if end_frame is None:\n        end_frame = scene.frame_end\n    if step_frame is None:\n        step_frame = 1\n    frame_range = (start_frame, end_frame, step_frame)\n\n    if filename is None:\n        filename = scene.render.filepath\n\n    render_options = {\n        \"filepath\": \"{}.\".format(filename.rstrip(\".\")),\n        \"resolution_x\": width,\n        \"resolution_y\": height,\n        \"use_overwrite\": overwrite,\n    }\n\n    with _independent_window() as window:\n\n        applied_view(window, camera, isolate, options=display_options)\n\n        with contextlib.ExitStack() as stack:\n            stack.enter_context(maintain_camera(window, camera))\n            stack.enter_context(applied_frame_range(window, *frame_range))\n            stack.enter_context(applied_render_options(window, render_options))\n            stack.enter_context(applied_image_settings(window, image_settings))\n            stack.enter_context(maintained_time())\n\n            bpy.ops.render.opengl(\n                animation=True,\n                render_keyed_only=False,\n                sequencer=False,\n                write_still=False,\n                view_context=True\n            )\n\n    return filename\n\n\nImageSettings = {\n    \"file_format\": \"FFMPEG\",\n    \"color_mode\": \"RGB\",\n    \"ffmpeg\": {\n        \"format\": \"QUICKTIME\",\n        \"use_autosplit\": False,\n        \"codec\": \"H264\",\n        \"constant_rate_factor\": \"MEDIUM\",\n        \"gopsize\": 18,\n        \"use_max_b_frames\": False,\n    },\n}\n\n\ndef isolate_objects(window, objects):\n    \"\"\"Isolate selection\"\"\"\n    deselect_all()\n\n    for obj in objects:\n        obj.select_set(True)\n\n    context = create_blender_context(selected=objects, window=window)\n\n    with bpy.context.temp_override(**context):\n        bpy.ops.view3d.view_axis(type=\"FRONT\")\n        bpy.ops.view3d.localview()\n\n    deselect_all()\n\n\ndef _apply_options(entity, options):\n    for option, value in options.items():\n        if isinstance(value, dict):\n            _apply_options(getattr(entity, option), value)\n        else:\n            setattr(entity, option, value)\n\n\ndef applied_view(window, camera, isolate=None, options=None):\n    \"\"\"Apply view options to window.\"\"\"\n    area = window.screen.areas[0]\n    space = area.spaces[0]\n\n    area.ui_type = \"VIEW_3D\"\n\n    types = {\"MESH\", \"GPENCIL\"}\n    objects = [obj for obj in window.scene.objects if obj.type in types]\n\n    if camera == \"AUTO\":\n        space.region_3d.view_perspective = \"ORTHO\"\n        isolate_objects(window, isolate or objects)\n    else:\n        isolate_objects(window, isolate or objects)\n        space.camera = window.scene.objects.get(camera)\n        space.region_3d.view_perspective = \"CAMERA\"\n\n    if isinstance(options, dict):\n        _apply_options(space, options)\n    else:\n        space.shading.type = \"SOLID\"\n        space.shading.color_type = \"MATERIAL\"\n        space.show_gizmo = False\n        space.overlay.show_overlays = False\n\n\n@contextlib.contextmanager\ndef applied_frame_range(window, start, end, step):\n    \"\"\"Context manager for setting frame range.\"\"\"\n    # Store current frame range\n    current_frame_start = window.scene.frame_start\n    current_frame_end = window.scene.frame_end\n    current_frame_step = window.scene.frame_step\n    # Apply frame range\n    window.scene.frame_start = start\n    window.scene.frame_end = end\n    window.scene.frame_step = step\n    try:\n        yield\n    finally:\n        # Restore frame range\n        window.scene.frame_start = current_frame_start\n        window.scene.frame_end = current_frame_end\n        window.scene.frame_step = current_frame_step\n\n\n@contextlib.contextmanager\ndef applied_render_options(window, options):\n    \"\"\"Context manager for setting render options.\"\"\"\n    render = window.scene.render\n\n    # Store current settings\n    original = {}\n    for opt in options.copy():\n        try:\n            original[opt] = getattr(render, opt)\n        except ValueError:\n            options.pop(opt)\n\n    # Apply settings\n    _apply_options(render, options)\n\n    try:\n        yield\n    finally:\n        # Restore previous settings\n        _apply_options(render, original)\n\n\n@contextlib.contextmanager\ndef applied_image_settings(window, options):\n    \"\"\"Context manager to override image settings.\"\"\"\n\n    options = options or ImageSettings.copy()\n    ffmpeg = options.pop(\"ffmpeg\", {})\n    render = window.scene.render\n\n    # Store current image settings\n    original = {}\n    for opt in options.copy():\n        try:\n            original[opt] = getattr(render.image_settings, opt)\n        except ValueError:\n            options.pop(opt)\n\n    # Store current ffmpeg settings\n    original_ffmpeg = {}\n    for opt in ffmpeg.copy():\n        try:\n            original_ffmpeg[opt] = getattr(render.ffmpeg, opt)\n        except ValueError:\n            ffmpeg.pop(opt)\n\n    # Apply image settings\n    for opt, value in options.items():\n        setattr(render.image_settings, opt, value)\n\n    # Apply ffmpeg settings\n    for opt, value in ffmpeg.items():\n        setattr(render.ffmpeg, opt, value)\n\n    try:\n        yield\n    finally:\n        # Restore previous settings\n        for opt, value in original.items():\n            setattr(render.image_settings, opt, value)\n        for opt, value in original_ffmpeg.items():\n            setattr(render.ffmpeg, opt, value)\n\n\n@contextlib.contextmanager\ndef maintain_camera(window, camera):\n    \"\"\"Context manager to override camera.\"\"\"\n    current_camera = window.scene.camera\n    if camera in window.scene.objects:\n        window.scene.camera = window.scene.objects.get(camera)\n    try:\n        yield\n    finally:\n        window.scene.camera = current_camera\n\n\n@contextlib.contextmanager\ndef _independent_window():\n    \"\"\"Create capture-window context.\"\"\"\n    context = create_blender_context()\n    current_windows = set(bpy.context.window_manager.windows)\n    with bpy.context.temp_override(**context):\n        bpy.ops.wm.window_new()\n        window = list(\n            set(bpy.context.window_manager.windows) - current_windows)[0]\n        context[\"window\"] = window\n        try:\n            yield window\n        finally:\n            bpy.ops.wm.window_close()\n"
  },
  {
    "path": "openpype/hosts/blender/api/colorspace.py",
    "content": "import attr\n\nimport bpy\n\n\n@attr.s\nclass LayerMetadata(object):\n    \"\"\"Data class for Render Layer metadata.\"\"\"\n    frameStart = attr.ib()\n    frameEnd = attr.ib()\n\n\n@attr.s\nclass RenderProduct(object):\n    \"\"\"\n    Getting Colorspace as Specific Render Product Parameter for submitting\n    publish job.\n    \"\"\"\n    colorspace = attr.ib()  # colorspace\n    view = attr.ib()        # OCIO view transform\n    productName = attr.ib(default=None)\n\n\nclass ARenderProduct(object):\n    def __init__(self):\n        \"\"\"Constructor.\"\"\"\n        # Initialize\n        self.layer_data = self._get_layer_data()\n        self.layer_data.products = self.get_render_products()\n\n    def _get_layer_data(self):\n        scene = bpy.context.scene\n\n        return LayerMetadata(\n            frameStart=int(scene.frame_start),\n            frameEnd=int(scene.frame_end),\n        )\n\n    def get_render_products(self):\n        \"\"\"To be implemented by renderer class.\n        This should return a list of RenderProducts.\n        Returns:\n            list: List of RenderProduct\n        \"\"\"\n        return [\n            RenderProduct(\n                colorspace=\"sRGB\",\n                view=\"ACES 1.0\",\n                productName=\"\"\n            )\n        ]\n"
  },
  {
    "path": "openpype/hosts/blender/api/lib.py",
    "content": "import os\nimport traceback\nimport importlib\nimport contextlib\nfrom typing import Dict, List, Union\n\nimport bpy\nimport addon_utils\nfrom openpype.lib import Logger\n\nfrom . import pipeline\n\nlog = Logger.get_logger(__name__)\n\n\ndef load_scripts(paths):\n    \"\"\"Copy of `load_scripts` from Blender's implementation.\n\n    It is possible that this function will be changed in future and usage will\n    be based on Blender version.\n    \"\"\"\n    import bpy_types\n\n    loaded_modules = set()\n\n    previous_classes = [\n        cls\n        for cls in bpy.types.bpy_struct.__subclasses__()\n    ]\n\n    def register_module_call(mod):\n        register = getattr(mod, \"register\", None)\n        if register:\n            try:\n                register()\n            except:\n                traceback.print_exc()\n        else:\n            print(\"\\nWarning! '%s' has no register function, \"\n                  \"this is now a requirement for registerable scripts\" %\n                  mod.__file__)\n\n    def unregister_module_call(mod):\n        unregister = getattr(mod, \"unregister\", None)\n        if unregister:\n            try:\n                unregister()\n            except:\n                traceback.print_exc()\n\n    def test_reload(mod):\n        # reloading this causes internal errors\n        # because the classes from this module are stored internally\n        # possibly to refresh internal references too but for now, best not to.\n        if mod == bpy_types:\n            return mod\n\n        try:\n            return importlib.reload(mod)\n        except:\n            traceback.print_exc()\n\n    def test_register(mod):\n        if mod:\n            register_module_call(mod)\n            bpy.utils._global_loaded_modules.append(mod.__name__)\n\n    from bpy_restrict_state import RestrictBlend\n\n    with RestrictBlend():\n        for base_path in paths:\n            for path_subdir in bpy.utils._script_module_dirs:\n                path = os.path.join(base_path, path_subdir)\n                if not os.path.isdir(path):\n                    continue\n\n                bpy.utils._sys_path_ensure_prepend(path)\n\n                # Only add to 'sys.modules' unless this is 'startup'.\n                if path_subdir != \"startup\":\n                    continue\n                for mod in bpy.utils.modules_from_path(path, loaded_modules):\n                    test_register(mod)\n\n    addons_paths = []\n    for base_path in paths:\n        addons_path = os.path.join(base_path, \"addons\")\n        if not os.path.exists(addons_path):\n            continue\n        addons_paths.append(addons_path)\n        addons_module_path = os.path.join(addons_path, \"modules\")\n        if os.path.exists(addons_module_path):\n            bpy.utils._sys_path_ensure_prepend(addons_module_path)\n\n    if addons_paths:\n        # Fake addons\n        origin_paths = addon_utils.paths\n\n        def new_paths():\n            paths = origin_paths() + addons_paths\n            return paths\n\n        addon_utils.paths = new_paths\n        addon_utils.modules_refresh()\n\n    # load template (if set)\n    if any(bpy.utils.app_template_paths()):\n        import bl_app_template_utils\n        bl_app_template_utils.reset(reload_scripts=False)\n        del bl_app_template_utils\n\n    for cls in bpy.types.bpy_struct.__subclasses__():\n        if cls in previous_classes:\n            continue\n        if not getattr(cls, \"is_registered\", False):\n            continue\n        for subcls in cls.__subclasses__():\n            if not subcls.is_registered:\n                print(\n                    \"Warning, unregistered class: %s(%s)\" %\n                    (subcls.__name__, cls.__name__)\n                )\n\n\ndef append_user_scripts():\n    user_scripts = os.environ.get(\"OPENPYPE_BLENDER_USER_SCRIPTS\")\n    if not user_scripts:\n        return\n\n    try:\n        load_scripts(user_scripts.split(os.pathsep))\n    except Exception:\n        print(\"Couldn't load user scripts \\\"{}\\\"\".format(user_scripts))\n        traceback.print_exc()\n\n\ndef set_app_templates_path():\n    # Blender requires the app templates to be in `BLENDER_USER_SCRIPTS`.\n    # After running Blender, we set that variable to our custom path, so\n    # that the user can use their custom app templates.\n\n    # We look among the scripts paths for one of the paths that contains\n    # the app templates. The path must contain the subfolder\n    # `startup/bl_app_templates_user`.\n    paths = os.environ.get(\"OPENPYPE_BLENDER_USER_SCRIPTS\").split(os.pathsep)\n\n    app_templates_path = None\n    for path in paths:\n        if os.path.isdir(\n                os.path.join(path, \"startup\", \"bl_app_templates_user\")):\n            app_templates_path = path\n            break\n\n    if app_templates_path and os.path.isdir(app_templates_path):\n        os.environ[\"BLENDER_USER_SCRIPTS\"] = app_templates_path\n\n\ndef imprint(node: bpy.types.bpy_struct_meta_idprop, data: Dict):\n    r\"\"\"Write `data` to `node` as userDefined attributes\n\n    Arguments:\n        node: Long name of node\n        data: Dictionary of key/value pairs\n\n    Example:\n        >>> import bpy\n        >>> def compute():\n        ...   return 6\n        ...\n        >>> bpy.ops.mesh.primitive_cube_add()\n        >>> cube = bpy.context.view_layer.objects.active\n        >>> imprint(cube, {\n        ...   \"regularString\": \"myFamily\",\n        ...   \"computedValue\": lambda: compute()\n        ... })\n        ...\n        >>> cube['avalon']['computedValue']\n        6\n    \"\"\"\n\n    imprint_data = dict()\n\n    for key, value in data.items():\n        if value is None:\n            continue\n\n        if callable(value):\n            # Support values evaluated at imprint\n            value = value()\n\n        if not isinstance(value, (int, float, bool, str, list, dict)):\n            raise TypeError(f\"Unsupported type: {type(value)}\")\n\n        imprint_data[key] = value\n\n    pipeline.metadata_update(node, imprint_data)\n\n\ndef lsattr(attr: str,\n           value: Union[str, int, bool, List, Dict, None] = None) -> List:\n    r\"\"\"Return nodes matching `attr` and `value`\n\n    Arguments:\n        attr: Name of Blender property\n        value: Value of attribute. If none\n            is provided, return all nodes with this attribute.\n\n    Example:\n        >>> lsattr(\"id\", \"myId\")\n        ...   [bpy.data.objects[\"myNode\"]\n        >>> lsattr(\"id\")\n        ...   [bpy.data.objects[\"myNode\"], bpy.data.objects[\"myOtherNode\"]]\n\n    Returns:\n        list\n    \"\"\"\n\n    return lsattrs({attr: value})\n\n\ndef lsattrs(attrs: Dict) -> List:\n    r\"\"\"Return nodes with the given attribute(s).\n\n    Arguments:\n        attrs: Name and value pairs of expected matches\n\n    Example:\n        >>> lsattrs({\"age\": 5})  # Return nodes with an `age` of 5\n        # Return nodes with both `age` and `color` of 5 and blue\n        >>> lsattrs({\"age\": 5, \"color\": \"blue\"})\n\n    Returns a list.\n\n    \"\"\"\n\n    # For now return all objects, not filtered by scene/collection/view_layer.\n    matches = set()\n    for coll in dir(bpy.data):\n        if not isinstance(\n                getattr(bpy.data, coll),\n                bpy.types.bpy_prop_collection,\n        ):\n            continue\n        for node in getattr(bpy.data, coll):\n            for attr, value in attrs.items():\n                avalon_prop = node.get(pipeline.AVALON_PROPERTY)\n                if not avalon_prop:\n                    continue\n                if (avalon_prop.get(attr)\n                        and (value is None or avalon_prop.get(attr) == value)):\n                    matches.add(node)\n    return list(matches)\n\n\ndef read(node: bpy.types.bpy_struct_meta_idprop):\n    \"\"\"Return user-defined attributes from `node`\"\"\"\n\n    data = dict(node.get(pipeline.AVALON_PROPERTY, {}))\n\n    # Ignore hidden/internal data\n    data = {\n        key: value\n        for key, value in data.items() if not key.startswith(\"_\")\n    }\n\n    return data\n\n\ndef get_selected_collections():\n    \"\"\"\n    Returns a list of the currently selected collections in the outliner.\n\n    Raises:\n        RuntimeError: If the outliner cannot be found in the main Blender\n        window.\n\n    Returns:\n        list: A list of `bpy.types.Collection` objects that are currently\n        selected in the outliner.\n    \"\"\"\n    window = bpy.context.window or bpy.context.window_manager.windows[0]\n\n    try:\n        area = next(\n            area for area in window.screen.areas\n            if area.type == 'OUTLINER')\n        region = next(\n            region for region in area.regions\n            if region.type == 'WINDOW')\n    except StopIteration as e:\n        raise RuntimeError(\"Could not find outliner. An outliner space \"\n                           \"must be in the main Blender window.\") from e\n\n    with bpy.context.temp_override(\n        window=window,\n        area=area,\n        region=region,\n        screen=window.screen\n    ):\n        ids = bpy.context.selected_ids\n\n    return [id for id in ids if isinstance(id, bpy.types.Collection)]\n\n\ndef get_selection(include_collections: bool = False) -> List[bpy.types.Object]:\n    \"\"\"\n    Returns a list of selected objects in the current Blender scene.\n\n    Args:\n        include_collections (bool, optional): Whether to include selected\n        collections in the result. Defaults to False.\n\n    Returns:\n        List[bpy.types.Object]: A list of selected objects.\n    \"\"\"\n    selection = [obj for obj in bpy.context.scene.objects if obj.select_get()]\n\n    if include_collections:\n        selection.extend(get_selected_collections())\n\n    return selection\n\n\n@contextlib.contextmanager\ndef maintained_selection():\n    r\"\"\"Maintain selection during context\n\n    Example:\n        >>> with maintained_selection():\n        ...     # Modify selection\n        ...     bpy.ops.object.select_all(action='DESELECT')\n        >>> # Selection restored\n    \"\"\"\n\n    previous_selection = get_selection()\n    previous_active = bpy.context.view_layer.objects.active\n    try:\n        yield\n    finally:\n        # Clear the selection\n        for node in get_selection():\n            node.select_set(state=False)\n        if previous_selection:\n            for node in previous_selection:\n                try:\n                    node.select_set(state=True)\n                except ReferenceError:\n                    # This could happen if a selected node was deleted during\n                    # the context.\n                    log.exception(\"Failed to reselect\")\n                    continue\n        try:\n            bpy.context.view_layer.objects.active = previous_active\n        except ReferenceError:\n            # This could happen if the active node was deleted during the\n            # context.\n            log.exception(\"Failed to set active object.\")\n\n\n@contextlib.contextmanager\ndef maintained_time():\n    \"\"\"Maintain current frame during context.\"\"\"\n    current_time = bpy.context.scene.frame_current\n    try:\n        yield\n    finally:\n        bpy.context.scene.frame_current = current_time\n"
  },
  {
    "path": "openpype/hosts/blender/api/ops.py",
    "content": "\"\"\"Blender operators and menus for use with Avalon.\"\"\"\n\nimport os\nimport sys\nimport platform\nimport time\nimport traceback\nimport collections\nfrom pathlib import Path\nfrom types import ModuleType\nfrom typing import Dict, List, Optional, Union\n\nfrom qtpy import QtWidgets, QtCore\n\nimport bpy\nimport bpy.utils.previews\n\nfrom openpype import style\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import get_current_asset_name, get_current_task_name\nfrom openpype.tools.utils import host_tools\n\nfrom .workio import OpenFileCacher\nfrom . import pipeline\n\nPREVIEW_COLLECTIONS: Dict = dict()\n\n# This seems like a good value to keep the Qt app responsive and doesn't slow\n# down Blender. At least on macOS I the interface of Blender gets very laggy if\n# you make it smaller.\nTIMER_INTERVAL: float = 0.01 if platform.system() == \"Windows\" else 0.1\n\n\ndef execute_function_in_main_thread(f):\n    \"\"\"Decorator to move a function call into main thread items\"\"\"\n    def wrapper(*args, **kwargs):\n        mti = MainThreadItem(f, *args, **kwargs)\n        execute_in_main_thread(mti)\n    return wrapper\n\n\nclass BlenderApplication(QtWidgets.QApplication):\n    _instance = None\n    blender_windows = {}\n\n    def __init__(self, *args, **kwargs):\n        super(BlenderApplication, self).__init__(*args, **kwargs)\n        self.setQuitOnLastWindowClosed(False)\n\n        self.setStyleSheet(style.load_stylesheet())\n        self.lastWindowClosed.connect(self.__class__.reset)\n\n    @classmethod\n    def get_app(cls):\n        if cls._instance is None:\n            cls._instance = cls(sys.argv)\n        return cls._instance\n\n    @classmethod\n    def reset(cls):\n        cls._instance = None\n\n    @classmethod\n    def store_window(cls, identifier, window):\n        current_window = cls.get_window(identifier)\n        cls.blender_windows[identifier] = window\n        if current_window:\n            current_window.close()\n            # current_window.deleteLater()\n\n    @classmethod\n    def get_window(cls, identifier):\n        return cls.blender_windows.get(identifier)\n\n\nclass MainThreadItem:\n    \"\"\"Structure to store information about callback in main thread.\n\n    Item should be used to execute callback in main thread which may be needed\n    for execution of Qt objects.\n\n    Item store callback (callable variable), arguments and keyword arguments\n    for the callback. Item hold information about it's process.\n    \"\"\"\n    not_set = object()\n    sleep_time = 0.1\n\n    def __init__(self, callback, *args, **kwargs):\n        self.done = False\n        self.exception = self.not_set\n        self.result = self.not_set\n        self.callback = callback\n        self.args = args\n        self.kwargs = kwargs\n\n    def execute(self):\n        \"\"\"Execute callback and store its result.\n\n        Method must be called from main thread. Item is marked as `done`\n        when callback execution finished. Store output of callback of exception\n        information when callback raises one.\n        \"\"\"\n        print(\"Executing process in main thread\")\n        if self.done:\n            print(\"- item is already processed\")\n            return\n\n        callback = self.callback\n        args = self.args\n        kwargs = self.kwargs\n        print(\"Running callback: {}\".format(str(callback)))\n        try:\n            result = callback(*args, **kwargs)\n            self.result = result\n\n        except Exception:\n            self.exception = sys.exc_info()\n\n        finally:\n            print(\"Done\")\n            self.done = True\n\n    def wait(self):\n        \"\"\"Wait for result from main thread.\n\n        This method stops current thread until callback is executed.\n\n        Returns:\n            object: Output of callback. May be any type or object.\n\n        Raises:\n            Exception: Reraise any exception that happened during callback\n                execution.\n        \"\"\"\n        while not self.done:\n            print(self.done)\n            time.sleep(self.sleep_time)\n\n        if self.exception is self.not_set:\n            return self.result\n        raise self.exception\n\n\nclass GlobalClass:\n    app = None\n    main_thread_callbacks = collections.deque()\n    is_windows = platform.system().lower() == \"windows\"\n\n\ndef execute_in_main_thread(main_thead_item):\n    print(\"execute_in_main_thread\")\n    GlobalClass.main_thread_callbacks.append(main_thead_item)\n\n\ndef _process_app_events() -> Optional[float]:\n    \"\"\"Process the events of the Qt app if the window is still visible.\n\n    If the app has any top level windows and at least one of them is visible\n    return the time after which this function should be run again. Else return\n    None, so the function is not run again and will be unregistered.\n    \"\"\"\n    while GlobalClass.main_thread_callbacks:\n        main_thread_item = GlobalClass.main_thread_callbacks.popleft()\n        main_thread_item.execute()\n        if main_thread_item.exception is not MainThreadItem.not_set:\n            _clc, val, tb = main_thread_item.exception\n            msg = str(val)\n            detail = \"\\n\".join(traceback.format_exception(_clc, val, tb))\n            dialog = QtWidgets.QMessageBox(\n                QtWidgets.QMessageBox.Warning,\n                \"Error\",\n                msg)\n            dialog.setMinimumWidth(500)\n            dialog.setDetailedText(detail)\n            dialog.exec_()\n\n        # Refresh Manager\n        if GlobalClass.app:\n            manager = GlobalClass.app.get_window(\"WM_OT_avalon_manager\")\n            if manager:\n                manager.refresh()\n\n    if not GlobalClass.is_windows:\n        if OpenFileCacher.opening_file:\n            return TIMER_INTERVAL\n\n        app = GlobalClass.app\n        if app._instance:\n            app.processEvents()\n            return TIMER_INTERVAL\n    return TIMER_INTERVAL\n\n\nclass LaunchQtApp(bpy.types.Operator):\n    \"\"\"A Base class for opertors to launch a Qt app.\"\"\"\n\n    _app: QtWidgets.QApplication\n    _window = Union[QtWidgets.QDialog, ModuleType]\n    _tool_name: str = None\n    _init_args: Optional[List] = list()\n    _init_kwargs: Optional[Dict] = dict()\n    bl_idname: str = None\n\n    def __init__(self):\n        if self.bl_idname is None:\n            raise NotImplementedError(\"Attribute `bl_idname` must be set!\")\n        print(f\"Initialising {self.bl_idname}...\")\n        self._app = BlenderApplication.get_app()\n        GlobalClass.app = self._app\n\n        if not bpy.app.timers.is_registered(_process_app_events):\n            bpy.app.timers.register(\n                _process_app_events,\n                persistent=True\n            )\n\n    def execute(self, context):\n        \"\"\"Execute the operator.\n\n        The child class must implement `execute()` where it only has to set\n        `self._window` to the desired Qt window and then simply run\n        `return super().execute(context)`.\n        `self._window` is expected to have a `show` method.\n        If the `show` method requires arguments, you can set `self._show_args`\n        and `self._show_kwargs`. `args` should be a list, `kwargs` a\n        dictionary.\n        \"\"\"\n\n        if self._tool_name is None:\n            if self._window is None:\n                raise AttributeError(\"`self._window` is not set.\")\n\n        else:\n            window = self._app.get_window(self.bl_idname)\n            if window is None:\n                window = host_tools.get_tool_by_name(self._tool_name)\n                self._app.store_window(self.bl_idname, window)\n            self._window = window\n\n        if not isinstance(self._window, (QtWidgets.QWidget, ModuleType)):\n            raise AttributeError(\n                \"`window` should be a `QWidget or module`. Got: {}\".format(\n                    str(type(window))\n                )\n            )\n\n        self.before_window_show()\n\n        def pull_to_front(window):\n            \"\"\"Pull window forward to screen.\n\n            If Window is minimized this will un-minimize, then it can be raised\n            and activated to the front.\n            \"\"\"\n            window.setWindowState(\n                (window.windowState() & ~QtCore.Qt.WindowMinimized) |\n                QtCore.Qt.WindowActive\n            )\n            window.raise_()\n            window.activateWindow()\n\n        if isinstance(self._window, ModuleType):\n            self._window.show()\n            pull_to_front(self._window)\n\n            # Pull window to the front\n            window = None\n            if hasattr(self._window, \"window\"):\n                window = self._window.window\n            elif hasattr(self._window, \"_window\"):\n                window = self._window.window\n\n            if window:\n                self._app.store_window(self.bl_idname, window)\n\n        else:\n            origin_flags = self._window.windowFlags()\n            on_top_flags = origin_flags | QtCore.Qt.WindowStaysOnTopHint\n            self._window.setWindowFlags(on_top_flags)\n            self._window.show()\n            pull_to_front(self._window)\n\n            # if on_top_flags != origin_flags:\n            #     self._window.setWindowFlags(origin_flags)\n            #     self._window.show()\n\n        return {'FINISHED'}\n\n    def before_window_show(self):\n        return\n\n\nclass LaunchCreator(LaunchQtApp):\n    \"\"\"Launch Avalon Creator.\"\"\"\n\n    bl_idname = \"wm.avalon_creator\"\n    bl_label = \"Create...\"\n    _tool_name = \"creator\"\n\n    def before_window_show(self):\n        self._window.refresh()\n\n    def execute(self, context):\n        host_tools.show_publisher(tab=\"create\")\n        return {\"FINISHED\"}\n\n\nclass LaunchLoader(LaunchQtApp):\n    \"\"\"Launch Avalon Loader.\"\"\"\n\n    bl_idname = \"wm.avalon_loader\"\n    bl_label = \"Load...\"\n    _tool_name = \"loader\"\n\n    def before_window_show(self):\n        if AYON_SERVER_ENABLED:\n            return\n        self._window.set_context(\n            {\"asset\": get_current_asset_name()},\n            refresh=True\n        )\n\n\nclass LaunchPublisher(LaunchQtApp):\n    \"\"\"Launch Avalon Publisher.\"\"\"\n\n    bl_idname = \"wm.avalon_publisher\"\n    bl_label = \"Publish...\"\n\n    def execute(self, context):\n        host_tools.show_publisher(tab=\"publish\")\n        return {\"FINISHED\"}\n\n\nclass LaunchManager(LaunchQtApp):\n    \"\"\"Launch Avalon Manager.\"\"\"\n\n    bl_idname = \"wm.avalon_manager\"\n    bl_label = \"Manage...\"\n    _tool_name = \"sceneinventory\"\n\n    def before_window_show(self):\n        if AYON_SERVER_ENABLED:\n            return\n        self._window.refresh()\n\n\nclass LaunchLibrary(LaunchQtApp):\n    \"\"\"Launch Library Loader.\"\"\"\n\n    bl_idname = \"wm.library_loader\"\n    bl_label = \"Library...\"\n    _tool_name = \"libraryloader\"\n\n    def before_window_show(self):\n        if AYON_SERVER_ENABLED:\n            return\n        self._window.refresh()\n\n\nclass LaunchWorkFiles(LaunchQtApp):\n    \"\"\"Launch Avalon Work Files.\"\"\"\n\n    bl_idname = \"wm.avalon_workfiles\"\n    bl_label = \"Work Files...\"\n    _tool_name = \"workfiles\"\n\n    def execute(self, context):\n        result = super().execute(context)\n        if not AYON_SERVER_ENABLED:\n            self._window.set_context({\n                \"asset\": get_current_asset_name(),\n                \"task\": get_current_task_name()\n            })\n        return result\n\n    def before_window_show(self):\n        if AYON_SERVER_ENABLED:\n            return\n        self._window.root = str(Path(\n            os.environ.get(\"AVALON_WORKDIR\", \"\"),\n            os.environ.get(\"AVALON_SCENEDIR\", \"\"),\n        ))\n        self._window.refresh()\n\n\nclass SetFrameRange(bpy.types.Operator):\n    bl_idname = \"wm.ayon_set_frame_range\"\n    bl_label = \"Set Frame Range\"\n\n    def execute(self, context):\n        data = pipeline.get_asset_data()\n        pipeline.set_frame_range(data)\n        return {\"FINISHED\"}\n\n\nclass SetResolution(bpy.types.Operator):\n    bl_idname = \"wm.ayon_set_resolution\"\n    bl_label = \"Set Resolution\"\n\n    def execute(self, context):\n        data = pipeline.get_asset_data()\n        pipeline.set_resolution(data)\n        return {\"FINISHED\"}\n\n\nclass TOPBAR_MT_avalon(bpy.types.Menu):\n    \"\"\"Avalon menu.\"\"\"\n\n    bl_idname = \"TOPBAR_MT_avalon\"\n    bl_label = os.environ.get(\"AVALON_LABEL\")\n\n    def draw(self, context):\n        \"\"\"Draw the menu in the UI.\"\"\"\n\n        layout = self.layout\n\n        pcoll = PREVIEW_COLLECTIONS.get(\"avalon\")\n        if pcoll:\n            pyblish_menu_icon = pcoll[\"pyblish_menu_icon\"]\n            pyblish_menu_icon_id = pyblish_menu_icon.icon_id\n        else:\n            pyblish_menu_icon_id = 0\n\n        asset = get_current_asset_name()\n        task = get_current_task_name()\n        context_label = f\"{asset}, {task}\"\n        context_label_item = layout.row()\n        context_label_item.operator(\n            LaunchWorkFiles.bl_idname, text=context_label\n        )\n        context_label_item.enabled = False\n        layout.separator()\n        layout.operator(LaunchCreator.bl_idname, text=\"Create...\")\n        layout.operator(LaunchLoader.bl_idname, text=\"Load...\")\n        layout.operator(\n            LaunchPublisher.bl_idname,\n            text=\"Publish...\",\n            icon_value=pyblish_menu_icon_id,\n        )\n        layout.operator(LaunchManager.bl_idname, text=\"Manage...\")\n        layout.operator(LaunchLibrary.bl_idname, text=\"Library...\")\n        layout.separator()\n        layout.operator(SetFrameRange.bl_idname, text=\"Set Frame Range\")\n        layout.operator(SetResolution.bl_idname, text=\"Set Resolution\")\n        layout.separator()\n        layout.operator(LaunchWorkFiles.bl_idname, text=\"Work Files...\")\n\n\ndef draw_avalon_menu(self, context):\n    \"\"\"Draw the Avalon menu in the top bar.\"\"\"\n\n    self.layout.menu(TOPBAR_MT_avalon.bl_idname)\n\n\nclasses = [\n    LaunchCreator,\n    LaunchLoader,\n    LaunchPublisher,\n    LaunchManager,\n    LaunchLibrary,\n    LaunchWorkFiles,\n    SetFrameRange,\n    SetResolution,\n    TOPBAR_MT_avalon,\n]\n\n\ndef register():\n    \"Register the operators and menu.\"\n\n    pcoll = bpy.utils.previews.new()\n    pyblish_icon_file = Path(__file__).parent / \"icons\" / \"pyblish-32x32.png\"\n    pcoll.load(\"pyblish_menu_icon\", str(pyblish_icon_file.absolute()), 'IMAGE')\n    PREVIEW_COLLECTIONS[\"avalon\"] = pcoll\n\n    BlenderApplication.get_app()\n    for cls in classes:\n        bpy.utils.register_class(cls)\n    bpy.types.TOPBAR_MT_editor_menus.append(draw_avalon_menu)\n\n\ndef unregister():\n    \"\"\"Unregister the operators and menu.\"\"\"\n\n    pcoll = PREVIEW_COLLECTIONS.pop(\"avalon\")\n    bpy.utils.previews.remove(pcoll)\n    bpy.types.TOPBAR_MT_editor_menus.remove(draw_avalon_menu)\n    for cls in reversed(classes):\n        bpy.utils.unregister_class(cls)\n"
  },
  {
    "path": "openpype/hosts/blender/api/pipeline.py",
    "content": "import os\nimport sys\nimport traceback\nfrom typing import Callable, Dict, Iterator, List, Optional\n\nimport bpy\n\nfrom . import lib\nfrom . import ops\n\nimport pyblish.api\n\nfrom openpype.host import (\n    HostBase,\n    IWorkfileHost,\n    IPublishHost,\n    ILoadHost\n)\nfrom openpype.client import get_asset_by_name\nfrom openpype.pipeline import (\n    schema,\n    legacy_io,\n    get_current_project_name,\n    get_current_asset_name,\n    register_loader_plugin_path,\n    register_creator_plugin_path,\n    deregister_loader_plugin_path,\n    deregister_creator_plugin_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.lib import (\n    Logger,\n    register_event_callback,\n    emit_event\n)\nimport openpype.hosts.blender\nfrom openpype.settings import get_project_settings\nfrom .workio import (\n    open_file,\n    save_file,\n    current_file,\n    has_unsaved_changes,\n    file_extensions,\n    work_root,\n)\n\n\nHOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__))\nPLUGINS_DIR = os.path.join(HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\n\nORIGINAL_EXCEPTHOOK = sys.excepthook\n\nAVALON_INSTANCES = \"AVALON_INSTANCES\"\nAVALON_CONTAINERS = \"AVALON_CONTAINERS\"\nAVALON_PROPERTY = 'avalon'\nIS_HEADLESS = bpy.app.background\n\nlog = Logger.get_logger(__name__)\n\n\nclass BlenderHost(HostBase, IWorkfileHost, IPublishHost, ILoadHost):\n    name = \"blender\"\n\n    def install(self):\n        \"\"\"Override install method from HostBase.\n        Install Blender host functionality.\"\"\"\n        install()\n\n    def get_containers(self) -> Iterator:\n        \"\"\"List containers from active Blender scene.\"\"\"\n        return ls()\n\n    def get_workfile_extensions(self) -> List[str]:\n        \"\"\"Override get_workfile_extensions method from IWorkfileHost.\n        Get workfile possible extensions.\n\n        Returns:\n            List[str]: Workfile extensions.\n        \"\"\"\n        return file_extensions()\n\n    def save_workfile(self, dst_path: str = None):\n        \"\"\"Override save_workfile method from IWorkfileHost.\n        Save currently opened workfile.\n\n        Args:\n            dst_path (str): Where the current scene should be saved. Or use\n                current path if `None` is passed.\n        \"\"\"\n        save_file(dst_path if dst_path else bpy.data.filepath)\n\n    def open_workfile(self, filepath: str):\n        \"\"\"Override open_workfile method from IWorkfileHost.\n        Open workfile at specified filepath in the host.\n\n        Args:\n            filepath (str): Path to workfile.\n        \"\"\"\n        open_file(filepath)\n\n    def get_current_workfile(self) -> str:\n        \"\"\"Override get_current_workfile method from IWorkfileHost.\n        Retrieve currently opened workfile path.\n\n        Returns:\n            str: Path to currently opened workfile.\n        \"\"\"\n        return current_file()\n\n    def workfile_has_unsaved_changes(self) -> bool:\n        \"\"\"Override wokfile_has_unsaved_changes method from IWorkfileHost.\n        Returns True if opened workfile has no unsaved changes.\n\n        Returns:\n            bool: True if scene is saved and False if it has unsaved\n                modifications.\n        \"\"\"\n        return has_unsaved_changes()\n\n    def work_root(self, session) -> str:\n        \"\"\"Override work_root method from IWorkfileHost.\n        Modify workdir per host.\n\n        Args:\n            session (dict): Session context data.\n\n        Returns:\n            str: Path to new workdir.\n        \"\"\"\n        return work_root(session)\n\n    def get_context_data(self) -> dict:\n        \"\"\"Override abstract method from IPublishHost.\n        Get global data related to creation-publishing from workfile.\n\n        Returns:\n            dict: Context data stored using 'update_context_data'.\n        \"\"\"\n        property = bpy.context.scene.get(AVALON_PROPERTY)\n        if property:\n            return property.to_dict()\n        return {}\n\n    def update_context_data(self, data: dict, changes: dict):\n        \"\"\"Override abstract method from IPublishHost.\n        Store global context data to workfile.\n\n        Args:\n            data (dict): New data as are.\n            changes (dict): Only data that has been changed. Each value has\n                tuple with '(<old>, <new>)' value.\n        \"\"\"\n        bpy.context.scene[AVALON_PROPERTY] = data\n\n\ndef pype_excepthook_handler(*args):\n    traceback.print_exception(*args)\n\n\ndef install():\n    \"\"\"Install Blender configuration for Avalon.\"\"\"\n    sys.excepthook = pype_excepthook_handler\n\n    pyblish.api.register_host(\"blender\")\n    pyblish.api.register_plugin_path(str(PUBLISH_PATH))\n\n    register_loader_plugin_path(str(LOAD_PATH))\n    register_creator_plugin_path(str(CREATE_PATH))\n\n    lib.append_user_scripts()\n    lib.set_app_templates_path()\n\n    register_event_callback(\"new\", on_new)\n    register_event_callback(\"open\", on_open)\n\n    _register_callbacks()\n    _register_events()\n\n    if not IS_HEADLESS:\n        ops.register()\n\n\ndef uninstall():\n    \"\"\"Uninstall Blender configuration for Avalon.\"\"\"\n    sys.excepthook = ORIGINAL_EXCEPTHOOK\n\n    pyblish.api.deregister_host(\"blender\")\n    pyblish.api.deregister_plugin_path(str(PUBLISH_PATH))\n\n    deregister_loader_plugin_path(str(LOAD_PATH))\n    deregister_creator_plugin_path(str(CREATE_PATH))\n\n    if not IS_HEADLESS:\n        ops.unregister()\n\n\ndef show_message(title, message):\n    from openpype.widgets.message_window import Window\n    from .ops import BlenderApplication\n\n    BlenderApplication.get_app()\n\n    Window(\n        parent=None,\n        title=title,\n        message=message,\n        level=\"warning\")\n\n\ndef message_window(title, message):\n    from .ops import (\n        MainThreadItem,\n        execute_in_main_thread,\n        _process_app_events\n    )\n\n    mti = MainThreadItem(show_message, title, message)\n    execute_in_main_thread(mti)\n    _process_app_events()\n\n\ndef get_asset_data():\n    project_name = get_current_project_name()\n    asset_name = get_current_asset_name()\n    asset_doc = get_asset_by_name(project_name, asset_name)\n\n    return asset_doc.get(\"data\")\n\n\ndef set_frame_range(data):\n    scene = bpy.context.scene\n\n    # Default scene settings\n    frameStart = scene.frame_start\n    frameEnd = scene.frame_end\n    fps = scene.render.fps / scene.render.fps_base\n\n    if not data:\n        return\n\n    if data.get(\"frameStart\"):\n        frameStart = data.get(\"frameStart\")\n    if data.get(\"frameEnd\"):\n        frameEnd = data.get(\"frameEnd\")\n    if data.get(\"fps\"):\n        fps = data.get(\"fps\")\n\n    scene.frame_start = frameStart\n    scene.frame_end = frameEnd\n    scene.render.fps = round(fps)\n    scene.render.fps_base = round(fps) / fps\n\n\ndef set_resolution(data):\n    scene = bpy.context.scene\n\n    # Default scene settings\n    resolution_x = scene.render.resolution_x\n    resolution_y = scene.render.resolution_y\n\n    if not data:\n        return\n\n    if data.get(\"resolutionWidth\"):\n        resolution_x = data.get(\"resolutionWidth\")\n    if data.get(\"resolutionHeight\"):\n        resolution_y = data.get(\"resolutionHeight\")\n\n    scene.render.resolution_x = resolution_x\n    scene.render.resolution_y = resolution_y\n\n\ndef on_new():\n    project = os.environ.get(\"AVALON_PROJECT\")\n    settings = get_project_settings(project).get(\"blender\")\n\n    set_resolution_startup = settings.get(\"set_resolution_startup\")\n    set_frames_startup = settings.get(\"set_frames_startup\")\n\n    data = get_asset_data()\n\n    if set_resolution_startup:\n        set_resolution(data)\n    if set_frames_startup:\n        set_frame_range(data)\n\n    unit_scale_settings = settings.get(\"unit_scale_settings\")\n    unit_scale_enabled = unit_scale_settings.get(\"enabled\")\n    if unit_scale_enabled:\n        unit_scale = unit_scale_settings.get(\"base_file_unit_scale\")\n        bpy.context.scene.unit_settings.scale_length = unit_scale\n\n\ndef on_open():\n    project = os.environ.get(\"AVALON_PROJECT\")\n    settings = get_project_settings(project).get(\"blender\")\n\n    set_resolution_startup = settings.get(\"set_resolution_startup\")\n    set_frames_startup = settings.get(\"set_frames_startup\")\n\n    data = get_asset_data()\n\n    if set_resolution_startup:\n        set_resolution(data)\n    if set_frames_startup:\n        set_frame_range(data)\n\n    unit_scale_settings = settings.get(\"unit_scale_settings\")\n    unit_scale_enabled = unit_scale_settings.get(\"enabled\")\n    apply_on_opening = unit_scale_settings.get(\"apply_on_opening\")\n    if unit_scale_enabled and apply_on_opening:\n        unit_scale = unit_scale_settings.get(\"base_file_unit_scale\")\n        prev_unit_scale = bpy.context.scene.unit_settings.scale_length\n\n        if unit_scale != prev_unit_scale:\n            bpy.context.scene.unit_settings.scale_length = unit_scale\n\n            message_window(\n                \"Base file unit scale changed\",\n                \"Base file unit scale changed to match the project settings.\")\n\n\n@bpy.app.handlers.persistent\ndef _on_save_pre(*args):\n    emit_event(\"before.save\")\n\n\n@bpy.app.handlers.persistent\ndef _on_save_post(*args):\n    emit_event(\"save\")\n\n\n@bpy.app.handlers.persistent\ndef _on_load_post(*args):\n    # Detect new file or opening an existing file\n    if bpy.data.filepath:\n        # Likely this was an open operation since it has a filepath\n        emit_event(\"open\")\n    else:\n        emit_event(\"new\")\n\n    ops.OpenFileCacher.post_load()\n\n\ndef _register_callbacks():\n    \"\"\"Register callbacks for certain events.\"\"\"\n    def _remove_handler(handlers: List, callback: Callable):\n        \"\"\"Remove the callback from the given handler list.\"\"\"\n\n        try:\n            handlers.remove(callback)\n        except ValueError:\n            pass\n\n    # TODO (jasper): implement on_init callback?\n\n    # Be sure to remove existig ones first.\n    _remove_handler(bpy.app.handlers.save_pre, _on_save_pre)\n    _remove_handler(bpy.app.handlers.save_post, _on_save_post)\n    _remove_handler(bpy.app.handlers.load_post, _on_load_post)\n\n    bpy.app.handlers.save_pre.append(_on_save_pre)\n    bpy.app.handlers.save_post.append(_on_save_post)\n    bpy.app.handlers.load_post.append(_on_load_post)\n\n    log.info(\"Installed event handler _on_save_pre...\")\n    log.info(\"Installed event handler _on_save_post...\")\n    log.info(\"Installed event handler _on_load_post...\")\n\n\ndef _on_task_changed():\n    \"\"\"Callback for when the task in the context is changed.\"\"\"\n\n    # TODO (jasper): Blender has no concept of projects or workspace.\n    # It would be nice to override 'bpy.ops.wm.open_mainfile' so it takes the\n    # workdir as starting directory.  But I don't know if that is possible.\n    # Another option would be to create a custom 'File Selector' and add the\n    # `directory` attribute, so it opens in that directory (does it?).\n    # https://docs.blender.org/api/blender2.8/bpy.types.Operator.html#calling-a-file-selector\n    # https://docs.blender.org/api/blender2.8/bpy.types.WindowManager.html#bpy.types.WindowManager.fileselect_add\n    workdir = legacy_io.Session[\"AVALON_WORKDIR\"]\n    log.debug(\"New working directory: %s\", workdir)\n\n\ndef _register_events():\n    \"\"\"Install callbacks for specific events.\"\"\"\n\n    register_event_callback(\"taskChanged\", _on_task_changed)\n    log.info(\"Installed event callback for 'taskChanged'...\")\n\n\ndef _discover_gui() -> Optional[Callable]:\n    \"\"\"Return the most desirable of the currently registered GUIs\"\"\"\n\n    # Prefer last registered\n    guis = reversed(pyblish.api.registered_guis())\n\n    for gui in guis:\n        try:\n            gui = __import__(gui).show\n        except (ImportError, AttributeError):\n            continue\n        else:\n            return gui\n\n    return None\n\n\ndef add_to_avalon_container(container: bpy.types.Collection):\n    \"\"\"Add the container to the Avalon container.\"\"\"\n\n    avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)\n    if not avalon_container:\n        avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)\n\n        # Link the container to the scene so it's easily visible to the artist\n        # and can be managed easily. Otherwise it's only found in \"Blender\n        # File\" view and it will be removed by Blenders garbage collection,\n        # unless you set a 'fake user'.\n        bpy.context.scene.collection.children.link(avalon_container)\n\n    avalon_container.children.link(container)\n\n    # Disable Avalon containers for the view layers.\n    for view_layer in bpy.context.scene.view_layers:\n        for child in view_layer.layer_collection.children:\n            if child.collection == avalon_container:\n                child.exclude = True\n\n\ndef metadata_update(node: bpy.types.bpy_struct_meta_idprop, data: Dict):\n    \"\"\"Imprint the node with metadata.\n\n    Existing metadata will be updated.\n    \"\"\"\n\n    if not node.get(AVALON_PROPERTY):\n        node[AVALON_PROPERTY] = dict()\n    for key, value in data.items():\n        if value is None:\n            continue\n        node[AVALON_PROPERTY][key] = value\n\n\ndef containerise(name: str,\n                 namespace: str,\n                 nodes: List,\n                 context: Dict,\n                 loader: Optional[str] = None,\n                 suffix: Optional[str] = \"CON\") -> bpy.types.Collection:\n    \"\"\"Bundle `nodes` into an assembly and imprint it with metadata\n\n    Containerisation enables a tracking of version, author and origin\n    for loaded assets.\n\n    Arguments:\n        name: Name of resulting assembly\n        namespace: Namespace under which to host container\n        nodes: Long names of nodes to containerise\n        context: Asset information\n        loader: Name of loader used to produce this container.\n        suffix: Suffix of container, defaults to `_CON`.\n\n    Returns:\n        The container assembly\n\n    \"\"\"\n\n    node_name = f\"{context['asset']['name']}_{name}\"\n    if namespace:\n        node_name = f\"{namespace}:{node_name}\"\n    if suffix:\n        node_name = f\"{node_name}_{suffix}\"\n    container = bpy.data.collections.new(name=node_name)\n    # Link the children nodes\n    for obj in nodes:\n        container.objects.link(obj)\n\n    data = {\n        \"schema\": \"openpype:container-2.0\",\n        \"id\": AVALON_CONTAINER_ID,\n        \"name\": name,\n        \"namespace\": namespace or '',\n        \"loader\": str(loader),\n        \"representation\": str(context[\"representation\"][\"_id\"]),\n    }\n\n    metadata_update(container, data)\n    add_to_avalon_container(container)\n\n    return container\n\n\ndef containerise_existing(\n        container: bpy.types.Collection,\n        name: str,\n        namespace: str,\n        context: Dict,\n        loader: Optional[str] = None,\n        suffix: Optional[str] = \"CON\") -> bpy.types.Collection:\n    \"\"\"Imprint or update container with metadata.\n\n    Arguments:\n        name: Name of resulting assembly\n        namespace: Namespace under which to host container\n        context: Asset information\n        loader: Name of loader used to produce this container.\n        suffix: Suffix of container, defaults to `_CON`.\n\n    Returns:\n        The container assembly\n    \"\"\"\n\n    node_name = container.name\n    if suffix:\n        node_name = f\"{node_name}_{suffix}\"\n    container.name = node_name\n    data = {\n        \"schema\": \"openpype:container-2.0\",\n        \"id\": AVALON_CONTAINER_ID,\n        \"name\": name,\n        \"namespace\": namespace or '',\n        \"loader\": str(loader),\n        \"representation\": str(context[\"representation\"][\"_id\"]),\n    }\n\n    metadata_update(container, data)\n    add_to_avalon_container(container)\n\n    return container\n\n\ndef parse_container(container: bpy.types.Collection,\n                    validate: bool = True) -> Dict:\n    \"\"\"Return the container node's full container data.\n\n    Args:\n        container: A container node name.\n        validate: turn the validation for the container on or off\n\n    Returns:\n        The container schema data for this container node.\n\n    \"\"\"\n\n    data = lib.read(container)\n\n    # Append transient data\n    data[\"objectName\"] = container.name\n\n    if validate:\n        schema.validate(data)\n\n    return data\n\n\ndef ls() -> Iterator:\n    \"\"\"List containers from active Blender scene.\n\n    This is the host-equivalent of api.ls(), but instead of listing assets on\n    disk, it lists assets already loaded in Blender; once loaded they are\n    called containers.\n    \"\"\"\n\n    for container in lib.lsattr(\"id\", AVALON_CONTAINER_ID):\n        yield parse_container(container)\n\n\ndef publish():\n    \"\"\"Shorthand to publish from within host.\"\"\"\n\n    return pyblish.util.publish()\n"
  },
  {
    "path": "openpype/hosts/blender/api/plugin.py",
    "content": "\"\"\"Shared functionality for pipeline plugins for Blender.\"\"\"\n\nimport itertools\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nimport bpy\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import (\n    Creator,\n    CreatedInstance,\n    LoaderPlugin,\n)\nfrom openpype.lib import BoolDef\n\nfrom .pipeline import (\n    AVALON_CONTAINERS,\n    AVALON_INSTANCES,\n    AVALON_PROPERTY,\n)\nfrom .ops import (\n    MainThreadItem,\n    execute_in_main_thread\n)\nfrom .lib import imprint\n\nVALID_EXTENSIONS = [\".blend\", \".json\", \".abc\", \".fbx\"]\n\n\ndef prepare_scene_name(\n    asset: str, subset: str, namespace: Optional[str] = None\n) -> str:\n    \"\"\"Return a consistent name for an asset.\"\"\"\n    name = f\"{asset}\"\n    if namespace:\n        name = f\"{name}_{namespace}\"\n    name = f\"{name}_{subset}\"\n\n    # Blender name for a collection or object cannot be longer than 63\n    # characters. If the name is longer, it will raise an error.\n    if len(name) > 63:\n        raise ValueError(f\"Scene name '{name}' would be too long.\")\n\n    return name\n\n\ndef get_unique_number(\n    asset: str, subset: str\n) -> str:\n    \"\"\"Return a unique number based on the asset name.\"\"\"\n    avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)\n    if not avalon_container:\n        return \"01\"\n    # Check the names of both object and collection containers\n    obj_asset_groups = avalon_container.objects\n    obj_group_names = {\n        c.name for c in obj_asset_groups\n        if c.type == 'EMPTY' and c.get(AVALON_PROPERTY)}\n    coll_asset_groups = avalon_container.children\n    coll_group_names = {\n        c.name for c in coll_asset_groups\n        if c.get(AVALON_PROPERTY)}\n    container_names = obj_group_names.union(coll_group_names)\n    count = 1\n    name = f\"{asset}_{count:0>2}_{subset}\"\n    while name in container_names:\n        count += 1\n        name = f\"{asset}_{count:0>2}_{subset}\"\n    return f\"{count:0>2}\"\n\n\ndef prepare_data(data, container_name=None):\n    name = data.name\n    local_data = data.make_local()\n    if container_name:\n        local_data.name = f\"{container_name}:{name}\"\n    else:\n        local_data.name = f\"{name}\"\n    return local_data\n\n\ndef create_blender_context(active: Optional[bpy.types.Object] = None,\n                           selected: Optional[bpy.types.Object] = None,\n                           window: Optional[bpy.types.Window] = None):\n    \"\"\"Create a new Blender context. If an object is passed as\n    parameter, it is set as selected and active.\n    \"\"\"\n\n    if not isinstance(selected, list):\n        selected = [selected]\n\n    override_context = bpy.context.copy()\n\n    windows = [window] if window else bpy.context.window_manager.windows\n\n    for win in windows:\n        for area in win.screen.areas:\n            if area.type == 'VIEW_3D':\n                for region in area.regions:\n                    if region.type == 'WINDOW':\n                        override_context['window'] = win\n                        override_context['screen'] = win.screen\n                        override_context['area'] = area\n                        override_context['region'] = region\n                        override_context['scene'] = bpy.context.scene\n                        override_context['active_object'] = active\n                        override_context['selected_objects'] = selected\n                        return override_context\n    raise Exception(\"Could not create a custom Blender context.\")\n\n\ndef get_parent_collection(collection):\n    \"\"\"Get the parent of the input collection\"\"\"\n    check_list = [bpy.context.scene.collection]\n\n    for c in check_list:\n        if collection.name in c.children.keys():\n            return c\n        check_list.extend(c.children)\n\n    return None\n\n\ndef get_local_collection_with_name(name):\n    for collection in bpy.data.collections:\n        if collection.name == name and collection.library is None:\n            return collection\n    return None\n\n\ndef deselect_all():\n    \"\"\"Deselect all objects in the scene.\n\n    Blender gives context error if trying to deselect object that it isn't\n    in object mode.\n    \"\"\"\n    modes = []\n    active = bpy.context.view_layer.objects.active\n\n    for obj in bpy.data.objects:\n        if obj.mode != 'OBJECT':\n            modes.append((obj, obj.mode))\n            bpy.context.view_layer.objects.active = obj\n            bpy.ops.object.mode_set(mode='OBJECT')\n\n    bpy.ops.object.select_all(action='DESELECT')\n\n    for p in modes:\n        bpy.context.view_layer.objects.active = p[0]\n        bpy.ops.object.mode_set(mode=p[1])\n\n    bpy.context.view_layer.objects.active = active\n\n\nclass BaseCreator(Creator):\n    \"\"\"Base class for Blender Creator plug-ins.\"\"\"\n    defaults = ['Main']\n\n    create_as_asset_group = False\n\n    @staticmethod\n    def cache_subsets(shared_data):\n        \"\"\"Cache instances for Creators shared data.\n\n        Create `blender_cached_subsets` key when needed in shared data and\n        fill it with all collected instances from the scene under its\n        respective creator identifiers.\n\n        If legacy instances are detected in the scene, create\n        `blender_cached_legacy_subsets` key and fill it with\n        all legacy subsets from this family as a value.  # key or value?\n\n        Args:\n            shared_data(Dict[str, Any]): Shared data.\n\n        Return:\n            Dict[str, Any]: Shared data with cached subsets.\n        \"\"\"\n        if not shared_data.get('blender_cached_subsets'):\n            cache = {}\n            cache_legacy = {}\n\n            avalon_instances = bpy.data.collections.get(AVALON_INSTANCES)\n            avalon_instance_objs = (\n                avalon_instances.objects if avalon_instances else []\n            )\n\n            for obj_or_col in itertools.chain(\n                    avalon_instance_objs,\n                    bpy.data.collections\n            ):\n                avalon_prop = obj_or_col.get(AVALON_PROPERTY, {})\n                if not avalon_prop:\n                    continue\n\n                if avalon_prop.get('id') != 'pyblish.avalon.instance':\n                    continue\n\n                creator_id = avalon_prop.get('creator_identifier')\n                if creator_id:\n                    # Creator instance\n                    cache.setdefault(creator_id, []).append(obj_or_col)\n                else:\n                    family = avalon_prop.get('family')\n                    if family:\n                        # Legacy creator instance\n                        cache_legacy.setdefault(family, []).append(obj_or_col)\n\n            shared_data[\"blender_cached_subsets\"] = cache\n            shared_data[\"blender_cached_legacy_subsets\"] = cache_legacy\n\n        return shared_data\n\n    def create(\n        self, subset_name: str, instance_data: dict, pre_create_data: dict\n    ):\n        \"\"\"Override abstract method from Creator.\n        Create new instance and store it.\n\n        Args:\n            subset_name(str): Subset name of created instance.\n            instance_data(dict): Instance base data.\n            pre_create_data(dict): Data based on pre creation attributes.\n                Those may affect how creator works.\n        \"\"\"\n        # Get Instance Container or create it if it does not exist\n        instances = bpy.data.collections.get(AVALON_INSTANCES)\n        if not instances:\n            instances = bpy.data.collections.new(name=AVALON_INSTANCES)\n            bpy.context.scene.collection.children.link(instances)\n\n        # Create asset group\n        if AYON_SERVER_ENABLED:\n            asset_name = instance_data[\"folderPath\"].split(\"/\")[-1]\n        else:\n            asset_name = instance_data[\"asset\"]\n\n        name = prepare_scene_name(asset_name, subset_name)\n        if self.create_as_asset_group:\n            # Create instance as empty\n            instance_node = bpy.data.objects.new(name=name, object_data=None)\n            instance_node.empty_display_type = 'SINGLE_ARROW'\n            instances.objects.link(instance_node)\n        else:\n            # Create instance collection\n            instance_node = bpy.data.collections.new(name=name)\n            instances.children.link(instance_node)\n\n        self.set_instance_data(subset_name, instance_data)\n\n        instance = CreatedInstance(\n            self.family, subset_name, instance_data, self\n        )\n        instance.transient_data[\"instance_node\"] = instance_node\n        self._add_instance_to_context(instance)\n\n        imprint(instance_node, instance_data)\n\n        return instance_node\n\n    def collect_instances(self):\n        \"\"\"Override abstract method from BaseCreator.\n        Collect existing instances related to this creator plugin.\"\"\"\n\n        # Cache subsets in shared data\n        self.cache_subsets(self.collection_shared_data)\n\n        # Get cached subsets\n        cached_subsets = self.collection_shared_data.get(\n            \"blender_cached_subsets\"\n        )\n        if not cached_subsets:\n            return\n\n        # Process only instances that were created by this creator\n        for instance_node in cached_subsets.get(self.identifier, []):\n            property = instance_node.get(AVALON_PROPERTY)\n            # Create instance object from existing data\n            instance = CreatedInstance.from_existing(\n                instance_data=property.to_dict(),\n                creator=self\n            )\n            instance.transient_data[\"instance_node\"] = instance_node\n\n            # Add instance to create context\n            self._add_instance_to_context(instance)\n\n    def update_instances(self, update_list):\n        \"\"\"Override abstract method from BaseCreator.\n        Store changes of existing instances so they can be recollected.\n\n        Args:\n            update_list(List[UpdateData]): Changed instances\n                and their changes, as a list of tuples.\n        \"\"\"\n\n        if AYON_SERVER_ENABLED:\n            asset_name_key = \"folderPath\"\n        else:\n            asset_name_key = \"asset\"\n\n        for created_instance, changes in update_list:\n            data = created_instance.data_to_store()\n            node = created_instance.transient_data[\"instance_node\"]\n            if not node:\n                # We can't update if we don't know the node\n                self.log.error(\n                    f\"Unable to update instance {created_instance} \"\n                    f\"without instance node.\"\n                )\n                return\n\n            # Rename the instance node in the scene if subset or asset changed.\n            # Do not rename the instance if the family is workfile, as the\n            # workfile instance is included in the AVALON_CONTAINER collection.\n            if (\n                \"subset\" in changes.changed_keys\n                or asset_name_key in changes.changed_keys\n            ) and created_instance.family != \"workfile\":\n                asset_name = data[asset_name_key]\n                if AYON_SERVER_ENABLED:\n                    asset_name = asset_name.split(\"/\")[-1]\n                name = prepare_scene_name(\n                    asset=asset_name, subset=data[\"subset\"]\n                )\n                node.name = name\n\n            imprint(node, data)\n\n    def remove_instances(self, instances: List[CreatedInstance]):\n\n        for instance in instances:\n            node = instance.transient_data[\"instance_node\"]\n\n            if isinstance(node, bpy.types.Collection):\n                for children in node.children_recursive:\n                    if isinstance(children, bpy.types.Collection):\n                        bpy.data.collections.remove(children)\n                    else:\n                        bpy.data.objects.remove(children)\n\n                bpy.data.collections.remove(node)\n            elif isinstance(node, bpy.types.Object):\n                bpy.data.objects.remove(node)\n\n            self._remove_instance_from_context(instance)\n\n    def set_instance_data(\n        self,\n        subset_name: str,\n        instance_data: dict\n    ):\n        \"\"\"Fill instance data with required items.\n\n        Args:\n            subset_name(str): Subset name of created instance.\n            instance_data(dict): Instance base data.\n            instance_node(bpy.types.ID): Instance node in blender scene.\n        \"\"\"\n        if not instance_data:\n            instance_data = {}\n\n        instance_data.update(\n            {\n                \"id\": \"pyblish.avalon.instance\",\n                \"creator_identifier\": self.identifier,\n                \"subset\": subset_name,\n            }\n        )\n\n    def get_pre_create_attr_defs(self):\n        return [\n            BoolDef(\"use_selection\",\n                    label=\"Use selection\",\n                    default=True)\n        ]\n\n\nclass Loader(LoaderPlugin):\n    \"\"\"Base class for Loader plug-ins.\"\"\"\n\n    hosts = [\"blender\"]\n\n\nclass AssetLoader(LoaderPlugin):\n    \"\"\"A basic AssetLoader for Blender\n\n    This will implement the basic logic for linking/appending assets\n    into another Blender scene.\n\n    The `update` method should be implemented by a sub-class, because\n    it's different for different types (e.g. model, rig, animation,\n    etc.).\n    \"\"\"\n\n    @staticmethod\n    def _get_instance_empty(instance_name: str, nodes: List) -> Optional[bpy.types.Object]:\n        \"\"\"Get the 'instance empty' that holds the collection instance.\"\"\"\n        for node in nodes:\n            if not isinstance(node, bpy.types.Object):\n                continue\n            if (node.type == 'EMPTY' and node.instance_type == 'COLLECTION'\n                    and node.instance_collection and node.name == instance_name):\n                return node\n        return None\n\n    @staticmethod\n    def _get_instance_collection(instance_name: str, nodes: List) -> Optional[bpy.types.Collection]:\n        \"\"\"Get the 'instance collection' (container) for this asset.\"\"\"\n        for node in nodes:\n            if not isinstance(node, bpy.types.Collection):\n                continue\n            if node.name == instance_name:\n                return node\n        return None\n\n    @staticmethod\n    def _get_library_from_container(container: bpy.types.Collection) -> bpy.types.Library:\n        \"\"\"Find the library file from the container.\n\n        It traverses the objects from this collection, checks if there is only\n        1 library from which the objects come from and returns the library.\n\n        Warning:\n            No nested collections are supported at the moment!\n        \"\"\"\n        assert not container.children, \"Nested collections are not supported.\"\n        assert container.objects, \"The collection doesn't contain any objects.\"\n        libraries = set()\n        for obj in container.objects:\n            assert obj.library, f\"'{obj.name}' is not linked.\"\n            libraries.add(obj.library)\n\n        assert len(\n            libraries) == 1, \"'{container.name}' contains objects from more then 1 library.\"\n\n        return list(libraries)[0]\n\n    def process_asset(self,\n                      context: dict,\n                      name: str,\n                      namespace: Optional[str] = None,\n                      options: Optional[Dict] = None):\n        \"\"\"Must be implemented by a sub-class\"\"\"\n        raise NotImplementedError(\"Must be implemented by a sub-class\")\n\n    def load(self,\n             context: dict,\n             name: Optional[str] = None,\n             namespace: Optional[str] = None,\n             options: Optional[Dict] = None) -> Optional[bpy.types.Collection]:\n        \"\"\" Run the loader on Blender main thread\"\"\"\n        mti = MainThreadItem(self._load, context, name, namespace, options)\n        execute_in_main_thread(mti)\n\n    def _load(self,\n              context: dict,\n              name: Optional[str] = None,\n              namespace: Optional[str] = None,\n              options: Optional[Dict] = None\n    ) -> Optional[bpy.types.Collection]:\n        \"\"\"Load asset via database\n\n        Arguments:\n            context: Full parenthood of representation to load\n            name: Use pre-defined name\n            namespace: Use pre-defined namespace\n            options: Additional settings dictionary\n        \"\"\"\n        # TODO: make it possible to add the asset several times by\n        # just re-using the collection\n        filepath = self.filepath_from_context(context)\n        assert Path(filepath).exists(), f\"{filepath} doesn't exist.\"\n\n        asset = context[\"asset\"][\"name\"]\n        subset = context[\"subset\"][\"name\"]\n        unique_number = get_unique_number(\n            asset, subset\n        )\n        namespace = namespace or f\"{asset}_{unique_number}\"\n        name = name or prepare_scene_name(\n            asset, subset, unique_number\n        )\n\n        nodes = self.process_asset(\n            context=context,\n            name=name,\n            namespace=namespace,\n            options=options,\n        )\n\n        # Only containerise if anything was loaded by the Loader.\n        if not nodes:\n            return None\n\n        # Only containerise if it's not already a collection from a .blend file.\n        # representation = context[\"representation\"][\"name\"]\n        # if representation != \"blend\":\n        #     from openpype.hosts.blender.api.pipeline import containerise\n        #     return containerise(\n        #         name=name,\n        #         namespace=namespace,\n        #         nodes=nodes,\n        #         context=context,\n        #         loader=self.__class__.__name__,\n        #     )\n\n        # asset = context[\"asset\"][\"name\"]\n        # subset = context[\"subset\"][\"name\"]\n        # instance_name = prepare_scene_name(\n        #     asset, subset, unique_number\n        # ) + '_CON'\n\n        # return self._get_instance_collection(instance_name, nodes)\n\n    def exec_update(self, container: Dict, representation: Dict):\n        \"\"\"Must be implemented by a sub-class\"\"\"\n        raise NotImplementedError(\"Must be implemented by a sub-class\")\n\n    def update(self, container: Dict, representation: Dict):\n        \"\"\" Run the update on Blender main thread\"\"\"\n        mti = MainThreadItem(self.exec_update, container, representation)\n        execute_in_main_thread(mti)\n\n    def exec_remove(self, container: Dict) -> bool:\n        \"\"\"Must be implemented by a sub-class\"\"\"\n        raise NotImplementedError(\"Must be implemented by a sub-class\")\n\n    def remove(self, container: Dict) -> bool:\n        \"\"\" Run the remove on Blender main thread\"\"\"\n        mti = MainThreadItem(self.exec_remove, container)\n        execute_in_main_thread(mti)\n"
  },
  {
    "path": "openpype/hosts/blender/api/render_lib.py",
    "content": "from pathlib import Path\n\nimport bpy\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import get_current_project_name\n\n\ndef get_default_render_folder(settings):\n    \"\"\"Get default render folder from blender settings.\"\"\"\n\n    return (settings[\"blender\"]\n                    [\"RenderSettings\"]\n                    [\"default_render_image_folder\"])\n\n\ndef get_aov_separator(settings):\n    \"\"\"Get aov separator from blender settings.\"\"\"\n\n    aov_sep = (settings[\"blender\"]\n                       [\"RenderSettings\"]\n                       [\"aov_separator\"])\n\n    if aov_sep == \"dash\":\n        return \"-\"\n    elif aov_sep == \"underscore\":\n        return \"_\"\n    elif aov_sep == \"dot\":\n        return \".\"\n    else:\n        raise ValueError(f\"Invalid aov separator: {aov_sep}\")\n\n\ndef get_image_format(settings):\n    \"\"\"Get image format from blender settings.\"\"\"\n\n    return (settings[\"blender\"]\n                    [\"RenderSettings\"]\n                    [\"image_format\"])\n\n\ndef get_multilayer(settings):\n    \"\"\"Get multilayer from blender settings.\"\"\"\n\n    return (settings[\"blender\"]\n                    [\"RenderSettings\"]\n                    [\"multilayer_exr\"])\n\n\ndef get_renderer(settings):\n    \"\"\"Get renderer from blender settings.\"\"\"\n\n    return (settings[\"blender\"]\n                    [\"RenderSettings\"]\n                    [\"renderer\"])\n\n\ndef get_compositing(settings):\n    \"\"\"Get compositing from blender settings.\"\"\"\n\n    return (settings[\"blender\"]\n                    [\"RenderSettings\"]\n                    [\"compositing\"])\n\n\ndef get_render_product(output_path, name, aov_sep):\n    \"\"\"\n    Generate the path to the render product. Blender interprets the `#`\n    as the frame number, when it renders.\n\n    Args:\n        file_path (str): The path to the blender scene.\n        render_folder (str): The render folder set in settings.\n        file_name (str): The name of the blender scene.\n        instance (pyblish.api.Instance): The instance to publish.\n        ext (str): The image format to render.\n    \"\"\"\n    filepath = output_path / name.lstrip(\"/\")\n    render_product = f\"{filepath}{aov_sep}beauty.####\"\n    render_product = render_product.replace(\"\\\\\", \"/\")\n\n    return render_product\n\n\ndef set_render_format(ext, multilayer):\n    # Set Blender to save the file with the right extension\n    bpy.context.scene.render.use_file_extension = True\n\n    image_settings = bpy.context.scene.render.image_settings\n\n    if ext == \"exr\":\n        image_settings.file_format = (\n            \"OPEN_EXR_MULTILAYER\" if multilayer else \"OPEN_EXR\")\n    elif ext == \"bmp\":\n        image_settings.file_format = \"BMP\"\n    elif ext == \"rgb\":\n        image_settings.file_format = \"IRIS\"\n    elif ext == \"png\":\n        image_settings.file_format = \"PNG\"\n    elif ext == \"jpeg\":\n        image_settings.file_format = \"JPEG\"\n    elif ext == \"jp2\":\n        image_settings.file_format = \"JPEG2000\"\n    elif ext == \"tga\":\n        image_settings.file_format = \"TARGA\"\n    elif ext == \"tif\":\n        image_settings.file_format = \"TIFF\"\n\n\ndef set_render_passes(settings, renderer):\n    aov_list = set(settings[\"blender\"][\"RenderSettings\"][\"aov_list\"])\n    custom_passes = settings[\"blender\"][\"RenderSettings\"][\"custom_passes\"]\n\n    # Common passes for both renderers\n    vl = bpy.context.view_layer\n\n    # Data Passes\n    vl.use_pass_combined = \"combined\" in aov_list\n    vl.use_pass_z = \"z\" in aov_list\n    vl.use_pass_mist = \"mist\" in aov_list\n    vl.use_pass_normal = \"normal\" in aov_list\n\n    # Light Passes\n    vl.use_pass_diffuse_direct = \"diffuse_light\" in aov_list\n    vl.use_pass_diffuse_color = \"diffuse_color\" in aov_list\n    vl.use_pass_glossy_direct = \"specular_light\" in aov_list\n    vl.use_pass_glossy_color = \"specular_color\" in aov_list\n    vl.use_pass_emit = \"emission\" in aov_list\n    vl.use_pass_environment = \"environment\" in aov_list\n    vl.use_pass_ambient_occlusion = \"ao\" in aov_list\n\n    # Cryptomatte Passes\n    vl.use_pass_cryptomatte_object = \"cryptomatte_object\" in aov_list\n    vl.use_pass_cryptomatte_material = \"cryptomatte_material\" in aov_list\n    vl.use_pass_cryptomatte_asset = \"cryptomatte_asset\" in aov_list\n\n    if renderer == \"BLENDER_EEVEE\":\n        # Eevee exclusive passes\n        eevee = vl.eevee\n\n        # Light Passes\n        vl.use_pass_shadow = \"shadow\" in aov_list\n        eevee.use_pass_volume_direct = \"volume_light\" in aov_list\n\n        # Effects Passes\n        eevee.use_pass_bloom = \"bloom\" in aov_list\n        eevee.use_pass_transparent = \"transparent\" in aov_list\n\n        # Cryptomatte Passes\n        vl.use_pass_cryptomatte_accurate = \"cryptomatte_accurate\" in aov_list\n    elif renderer == \"CYCLES\":\n        # Cycles exclusive passes\n        cycles = vl.cycles\n\n        # Data Passes\n        vl.use_pass_position = \"position\" in aov_list\n        vl.use_pass_vector = \"vector\" in aov_list\n        vl.use_pass_uv = \"uv\" in aov_list\n        cycles.denoising_store_passes = \"denoising\" in aov_list\n        vl.use_pass_object_index = \"object_index\" in aov_list\n        vl.use_pass_material_index = \"material_index\" in aov_list\n        cycles.pass_debug_sample_count = \"sample_count\" in aov_list\n\n        # Light Passes\n        vl.use_pass_diffuse_indirect = \"diffuse_indirect\" in aov_list\n        vl.use_pass_glossy_indirect = \"specular_indirect\" in aov_list\n        vl.use_pass_transmission_direct = \"transmission_direct\" in aov_list\n        vl.use_pass_transmission_indirect = \"transmission_indirect\" in aov_list\n        vl.use_pass_transmission_color = \"transmission_color\" in aov_list\n        cycles.use_pass_volume_direct = \"volume_light\" in aov_list\n        cycles.use_pass_volume_indirect = \"volume_indirect\" in aov_list\n        cycles.use_pass_shadow_catcher = \"shadow\" in aov_list\n\n    aovs_names = [aov.name for aov in vl.aovs]\n    for cp in custom_passes:\n        cp_name = cp[\"attribute\"] if AYON_SERVER_ENABLED else cp[0]\n        if cp_name not in aovs_names:\n            aov = vl.aovs.add()\n            aov.name = cp_name\n        else:\n            aov = vl.aovs[cp_name]\n        aov.type = (cp[\"value\"]\n                    if AYON_SERVER_ENABLED else cp[1].get(\"type\", \"VALUE\"))\n\n    return list(aov_list), custom_passes\n\n\ndef _create_aov_slot(name, aov_sep, slots, rpass_name, multi_exr, output_path):\n    filename = f\"{name}{aov_sep}{rpass_name}.####\"\n    slot = slots.new(rpass_name if multi_exr else filename)\n    filepath = str(output_path / filename.lstrip(\"/\"))\n\n    return slot, filepath\n\n\ndef set_node_tree(\n    output_path, render_product, name, aov_sep, ext, multilayer, compositing\n):\n    # Set the scene to use the compositor node tree to render\n    bpy.context.scene.use_nodes = True\n\n    tree = bpy.context.scene.node_tree\n\n    comp_layer_type = \"CompositorNodeRLayers\"\n    output_type = \"CompositorNodeOutputFile\"\n    compositor_type = \"CompositorNodeComposite\"\n\n    # Get the Render Layer, Composite and the previous output nodes\n    render_layer_node = None\n    composite_node = None\n    old_output_node = None\n    for node in tree.nodes:\n        if node.bl_idname == comp_layer_type:\n            render_layer_node = node\n        elif node.bl_idname == compositor_type:\n            composite_node = node\n        elif node.bl_idname == output_type and \"AYON\" in node.name:\n            old_output_node = node\n        if render_layer_node and composite_node and old_output_node:\n            break\n\n    # If there's not a Render Layers node, we create it\n    if not render_layer_node:\n        render_layer_node = tree.nodes.new(comp_layer_type)\n\n    # Get the enabled output sockets, that are the active passes for the\n    # render.\n    # We also exclude some layers.\n    exclude_sockets = [\"Image\", \"Alpha\", \"Noisy Image\"]\n    passes = [\n        socket\n        for socket in render_layer_node.outputs\n        if socket.enabled and socket.name not in exclude_sockets\n    ]\n\n    # Create a new output node\n    output = tree.nodes.new(output_type)\n\n    image_settings = bpy.context.scene.render.image_settings\n    output.format.file_format = image_settings.file_format\n\n    slots = None\n\n    # In case of a multilayer exr, we don't need to use the output node,\n    # because the blender render already outputs a multilayer exr.\n    multi_exr = ext == \"exr\" and multilayer\n    slots = output.layer_slots if multi_exr else output.file_slots\n    output.base_path = render_product if multi_exr else str(output_path)\n\n    slots.clear()\n\n    aov_file_products = []\n\n    old_links = {\n        link.from_socket.name: link for link in tree.links\n        if link.to_node == old_output_node}\n\n    # Create a new socket for the beauty output\n    pass_name = \"rgba\" if multi_exr else \"beauty\"\n    slot, _ = _create_aov_slot(\n        name, aov_sep, slots, pass_name, multi_exr, output_path)\n    tree.links.new(render_layer_node.outputs[\"Image\"], slot)\n\n    if compositing:\n        # Create a new socket for the composite output\n        pass_name = \"composite\"\n        comp_socket, filepath = _create_aov_slot(\n            name, aov_sep, slots, pass_name, multi_exr, output_path)\n        aov_file_products.append((\"Composite\", filepath))\n\n    # For each active render pass, we add a new socket to the output node\n    # and link it\n    for rpass in passes:\n        slot, filepath = _create_aov_slot(\n            name, aov_sep, slots, rpass.name, multi_exr, output_path)\n        aov_file_products.append((rpass.name, filepath))\n\n        # If the rpass was not connected with the old output node, we connect\n        # it with the new one.\n        if not old_links.get(rpass.name):\n            tree.links.new(rpass, slot)\n\n    for link in list(old_links.values()):\n        # Check if the socket is still available in the new output node.\n        socket = output.inputs.get(link.to_socket.name)\n        # If it is, we connect it with the new output node.\n        if socket:\n            tree.links.new(link.from_socket, socket)\n        # Then, we remove the old link.\n        tree.links.remove(link)\n\n    # If there's a composite node, we connect its input with the new output\n    if compositing and composite_node:\n        for link in tree.links:\n            if link.to_node == composite_node:\n                tree.links.new(link.from_socket, comp_socket)\n                break\n\n    if old_output_node:\n        output.location = old_output_node.location\n        tree.nodes.remove(old_output_node)\n\n    output.name = \"AYON File Output\"\n    output.label = \"AYON File Output\"\n\n    return [] if multi_exr else aov_file_products\n\n\ndef imprint_render_settings(node, data):\n    RENDER_DATA = \"render_data\"\n    if not node.get(RENDER_DATA):\n        node[RENDER_DATA] = {}\n    for key, value in data.items():\n        if value is None:\n            continue\n        node[RENDER_DATA][key] = value\n\n\ndef prepare_rendering(asset_group):\n    name = asset_group.name\n\n    filepath = Path(bpy.data.filepath)\n    assert filepath, \"Workfile not saved. Please save the file first.\"\n\n    dirpath = filepath.parent\n    file_name = Path(filepath.name).stem\n\n    project = get_current_project_name()\n    settings = get_project_settings(project)\n\n    render_folder = get_default_render_folder(settings)\n    aov_sep = get_aov_separator(settings)\n    ext = get_image_format(settings)\n    multilayer = get_multilayer(settings)\n    renderer = get_renderer(settings)\n    compositing = get_compositing(settings)\n\n    set_render_format(ext, multilayer)\n    bpy.context.scene.render.engine = renderer\n    aov_list, custom_passes = set_render_passes(settings, renderer)\n\n    output_path = Path.joinpath(dirpath, render_folder, file_name)\n\n    render_product = get_render_product(output_path, name, aov_sep)\n    aov_file_product = set_node_tree(\n        output_path, render_product, name, aov_sep,\n        ext, multilayer, compositing)\n\n    # Clear the render filepath, so that the output is handled only by the\n    # output node in the compositor.\n    bpy.context.scene.render.filepath = \"\"\n\n    render_settings = {\n        \"render_folder\": render_folder,\n        \"aov_separator\": aov_sep,\n        \"image_format\": ext,\n        \"multilayer_exr\": multilayer,\n        \"aov_list\": aov_list,\n        \"custom_passes\": custom_passes,\n        \"render_product\": render_product,\n        \"aov_file_product\": aov_file_product,\n        \"review\": True,\n    }\n\n    imprint_render_settings(asset_group, render_settings)\n"
  },
  {
    "path": "openpype/hosts/blender/api/workio.py",
    "content": "\"\"\"Host API required for Work Files.\"\"\"\n\nfrom pathlib import Path\nfrom typing import List, Optional\n\nimport bpy\n\n\nclass OpenFileCacher:\n    \"\"\"Store information about opening file.\n\n    When file is opening QApplcation events should not be processed.\n    \"\"\"\n    opening_file = False\n\n    @classmethod\n    def post_load(cls):\n        cls.opening_file = False\n\n    @classmethod\n    def set_opening(cls):\n        cls.opening_file = True\n\n\ndef open_file(filepath: str) -> Optional[str]:\n    \"\"\"Open the scene file in Blender.\"\"\"\n    OpenFileCacher.set_opening()\n\n    preferences = bpy.context.preferences\n    load_ui = preferences.filepaths.use_load_ui\n    use_scripts = preferences.filepaths.use_scripts_auto_execute\n    result = bpy.ops.wm.open_mainfile(\n        filepath=filepath,\n        load_ui=load_ui,\n        use_scripts=use_scripts,\n    )\n\n    if result == {'FINISHED'}:\n        return filepath\n    return None\n\n\ndef save_file(filepath: str, copy: bool = False) -> Optional[str]:\n    \"\"\"Save the open scene file.\"\"\"\n\n    preferences = bpy.context.preferences\n    compress = preferences.filepaths.use_file_compression\n    relative_remap = preferences.filepaths.use_relative_paths\n    result = bpy.ops.wm.save_as_mainfile(\n        filepath=filepath,\n        compress=compress,\n        relative_remap=relative_remap,\n        copy=copy,\n    )\n\n    if result == {'FINISHED'}:\n        return filepath\n    return None\n\n\ndef current_file() -> Optional[str]:\n    \"\"\"Return the path of the open scene file.\"\"\"\n\n    current_filepath = bpy.data.filepath\n    if Path(current_filepath).is_file():\n        return current_filepath\n    return None\n\n\ndef has_unsaved_changes() -> bool:\n    \"\"\"Does the open scene file have unsaved changes?\"\"\"\n\n    return bpy.data.is_dirty\n\n\ndef file_extensions() -> List[str]:\n    \"\"\"Return the supported file extensions for Blender scene files.\"\"\"\n\n    return [\".blend\"]\n\n\ndef work_root(session: dict) -> str:\n    \"\"\"Return the default root to browse for work files.\"\"\"\n\n    work_dir = session[\"AVALON_WORKDIR\"]\n    scene_dir = session.get(\"AVALON_SCENEDIR\")\n    if scene_dir:\n        return str(Path(work_dir, scene_dir))\n    return work_dir\n"
  },
  {
    "path": "openpype/hosts/blender/blender_addon/startup/init.py",
    "content": "from openpype.pipeline import install_host\nfrom openpype.hosts.blender.api import BlenderHost\n\n\ndef register():\n    install_host(BlenderHost())\n\n\ndef unregister():\n    pass\n"
  },
  {
    "path": "openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py",
    "content": "from pathlib import Path\n\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass AddPythonScriptToLaunchArgs(PreLaunchHook):\n    \"\"\"Add python script to be executed before Blender launch.\"\"\"\n\n    # Append after file argument\n    order = 15\n    app_groups = {\"blender\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        if not self.launch_context.data.get(\"python_scripts\"):\n            return\n\n        # Add path to workfile to arguments\n        for python_script_path in self.launch_context.data[\"python_scripts\"]:\n            self.log.info(\n                f\"Adding python script {python_script_path} to launch\"\n            )\n            # Test script path exists\n            python_script_path = Path(python_script_path)\n            if not python_script_path.exists():\n                self.log.warning(\n                    f\"Python script {python_script_path} doesn't exist. \"\n                    \"Skipped...\"\n                )\n                continue\n\n            if \"--\" in self.launch_context.launch_args:\n                # Insert before separator\n                separator_index = self.launch_context.launch_args.index(\"--\")\n                self.launch_context.launch_args.insert(\n                    separator_index,\n                    \"-P\",\n                )\n                self.launch_context.launch_args.insert(\n                    separator_index + 1,\n                    python_script_path.as_posix(),\n                )\n            else:\n                self.launch_context.launch_args.extend(\n                    [\"-P\", python_script_path.as_posix()]\n                )\n\n        # Ensure separator\n        if \"--\" not in self.launch_context.launch_args:\n            self.launch_context.launch_args.append(\"--\")\n\n        self.launch_context.launch_args.extend(\n            [*self.launch_context.data.get(\"script_args\", [])]\n        )\n"
  },
  {
    "path": "openpype/hosts/blender/hooks/pre_pyside_install.py",
    "content": "import os\nimport re\nimport subprocess\nfrom platform import system\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass InstallPySideToBlender(PreLaunchHook):\n    \"\"\"Install Qt binding to blender's python packages.\n\n    Prelaunch hook does 2 things:\n    1.) Blender's python packages are pushed to the beginning of PYTHONPATH.\n    2.) Check if blender has installed PySide2 and will try to install if not.\n\n    For pipeline implementation is required to have Qt binding installed in\n    blender's python packages.\n    \"\"\"\n\n    app_groups = {\"blender\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        # Prelaunch hook is not crucial\n        try:\n            self.inner_execute()\n        except Exception:\n            self.log.warning(\n                \"Processing of {} crashed.\".format(self.__class__.__name__),\n                exc_info=True\n            )\n\n    def inner_execute(self):\n        # Get blender's python directory\n        version_regex = re.compile(r\"^[2-4]\\.[0-9]+$\")\n\n        platform = system().lower()\n        executable = self.launch_context.executable.executable_path\n        expected_executable = \"blender\"\n        if platform == \"windows\":\n            expected_executable += \".exe\"\n\n        if os.path.basename(executable).lower() != expected_executable:\n            self.log.info((\n                f\"Executable does not lead to {expected_executable} file.\"\n                \"Can't determine blender's python to check/install PySide2.\"\n            ))\n            return\n\n        versions_dir = os.path.dirname(executable)\n        if platform == \"darwin\":\n            versions_dir = os.path.join(\n                os.path.dirname(versions_dir), \"Resources\"\n            )\n        version_subfolders = []\n        for dir_entry in os.scandir(versions_dir):\n            if dir_entry.is_dir() and version_regex.match(dir_entry.name):\n                version_subfolders.append(dir_entry.name)\n\n        if not version_subfolders:\n            self.log.info(\n                \"Didn't find version subfolder next to Blender executable\"\n            )\n            return\n\n        if len(version_subfolders) > 1:\n            self.log.info((\n                \"Found more than one version subfolder next\"\n                \" to blender executable. {}\"\n            ).format(\", \".join([\n                '\"./{}\"'.format(name)\n                for name in version_subfolders\n            ])))\n            return\n\n        version_subfolder = version_subfolders[0]\n\n        python_dir = os.path.join(versions_dir, version_subfolder, \"python\")\n        python_lib = os.path.join(python_dir, \"lib\")\n        python_version = \"python\"\n\n        if platform != \"windows\":\n            for dir_entry in os.scandir(python_lib):\n                if dir_entry.is_dir() and dir_entry.name.startswith(\"python\"):\n                    python_lib = dir_entry.path\n                    python_version = dir_entry.name\n                    break\n\n        # Change PYTHONPATH to contain blender's packages as first\n        python_paths = [\n            python_lib,\n            os.path.join(python_lib, \"site-packages\"),\n        ]\n        python_path = self.launch_context.env.get(\"PYTHONPATH\") or \"\"\n        for path in python_path.split(os.pathsep):\n            if path:\n                python_paths.append(path)\n\n        self.launch_context.env[\"PYTHONPATH\"] = os.pathsep.join(python_paths)\n\n        # Get blender's python executable\n        python_bin = os.path.join(python_dir, \"bin\")\n        if platform == \"windows\":\n            python_executable = os.path.join(python_bin, \"python.exe\")\n        else:\n            python_executable = os.path.join(python_bin, python_version)\n            # Check for python with enabled 'pymalloc'\n            if not os.path.exists(python_executable):\n                python_executable += \"m\"\n\n        if not os.path.exists(python_executable):\n            self.log.warning(\n                \"Couldn't find python executable for blender. {}\".format(\n                    executable\n                )\n            )\n            return\n\n        # Check if PySide2 is installed and skip if yes\n        if self.is_pyside_installed(python_executable):\n            self.log.debug(\"Blender has already installed PySide2.\")\n            return\n\n        # Install PySide2 in blender's python\n        if platform == \"windows\":\n            result = self.install_pyside_windows(python_executable)\n        else:\n            result = self.install_pyside(python_executable)\n\n        if result:\n            self.log.info(\"Successfully installed PySide2 module to blender.\")\n        else:\n            self.log.warning(\"Failed to install PySide2 module to blender.\")\n\n    def install_pyside_windows(self, python_executable):\n        \"\"\"Install PySide2 python module to blender's python.\n\n        Installation requires administration rights that's why it is required\n        to use \"pywin32\" module which can execute command's and ask for\n        administration rights.\n        \"\"\"\n        try:\n            import win32api\n            import win32con\n            import win32process\n            import win32event\n            import pywintypes\n            from win32comext.shell.shell import ShellExecuteEx\n            from win32comext.shell import shellcon\n        except Exception:\n            self.log.warning(\"Couldn't import \\\"pywin32\\\" modules\")\n            return\n\n        try:\n            # Parameters\n            # - use \"-m pip\" as module pip to install PySide2 and argument\n            #   \"--ignore-installed\" is to force install module to blender's\n            #   site-packages and make sure it is binary compatible\n            parameters = \"-m pip install --ignore-installed PySide2\"\n\n            # Execute command and ask for administrator's rights\n            process_info = ShellExecuteEx(\n                nShow=win32con.SW_SHOWNORMAL,\n                fMask=shellcon.SEE_MASK_NOCLOSEPROCESS,\n                lpVerb=\"runas\",\n                lpFile=python_executable,\n                lpParameters=parameters,\n                lpDirectory=os.path.dirname(python_executable)\n            )\n            process_handle = process_info[\"hProcess\"]\n            win32event.WaitForSingleObject(process_handle, win32event.INFINITE)\n            returncode = win32process.GetExitCodeProcess(process_handle)\n            return returncode == 0\n        except pywintypes.error:\n            pass\n\n    def install_pyside(self, python_executable):\n        \"\"\"Install PySide2 python module to blender's python.\"\"\"\n        try:\n            # Parameters\n            # - use \"-m pip\" as module pip to install PySide2 and argument\n            #   \"--ignore-installed\" is to force install module to blender's\n            #   site-packages and make sure it is binary compatible\n            args = [\n                python_executable,\n                \"-m\",\n                \"pip\",\n                \"install\",\n                \"--ignore-installed\",\n                \"PySide2\",\n            ]\n            process = subprocess.Popen(\n                args, stdout=subprocess.PIPE, universal_newlines=True\n            )\n            process.communicate()\n            return process.returncode == 0\n        except PermissionError:\n            self.log.warning(\n                \"Permission denied with command:\"\n                \"\\\"{}\\\".\".format(\" \".join(args))\n            )\n        except OSError as error:\n            self.log.warning(f\"OS error has occurred: \\\"{error}\\\".\")\n        except subprocess.SubprocessError:\n            pass\n\n    def is_pyside_installed(self, python_executable):\n        \"\"\"Check if PySide2 module is in blender's pip list.\n\n        Check that PySide2 is installed directly in blender's site-packages.\n        It is possible that it is installed in user's site-packages but that\n        may be incompatible with blender's python.\n        \"\"\"\n        # Get pip list from blender's python executable\n        args = [python_executable, \"-m\", \"pip\", \"list\"]\n        process = subprocess.Popen(args, stdout=subprocess.PIPE)\n        stdout, _ = process.communicate()\n        lines = stdout.decode().split(os.linesep)\n        # Second line contain dashes that define maximum length of module name.\n        #   Second column of dashes define maximum length of module version.\n        package_dashes, *_ = lines[1].split(\" \")\n        package_len = len(package_dashes)\n\n        # Got through printed lines starting at line 3\n        for idx in range(2, len(lines)):\n            line = lines[idx]\n            if not line:\n                continue\n            package_name = line[0:package_len].strip()\n            if package_name.lower() == \"pyside2\":\n                return True\n        return False\n"
  },
  {
    "path": "openpype/hosts/blender/hooks/pre_windows_console.py",
    "content": "import subprocess\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass BlenderConsoleWindows(PreLaunchHook):\n    \"\"\"Foundry applications have specific way how to launch them.\n\n    Blender is executed \"like\" python process so it is required to pass\n    `CREATE_NEW_CONSOLE` flag on windows to trigger creation of new console.\n    At the same time the newly created console won't create it's own stdout\n    and stderr handlers so they should not be redirected to DEVNULL.\n    \"\"\"\n\n    # Should be as last hook because must change launch arguments to string\n    order = 1000\n    app_groups = {\"blender\"}\n    platforms = {\"windows\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        # Change `creationflags` to CREATE_NEW_CONSOLE\n        # - on Windows will blender create new window using it's console\n        # Set `stdout` and `stderr` to None so new created console does not\n        #   have redirected output to DEVNULL in build\n        self.launch_context.kwargs.update({\n            \"creationflags\": subprocess.CREATE_NEW_CONSOLE,\n            \"stdout\": None,\n            \"stderr\": None\n        })\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/create/convert_legacy.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Converter for legacy Houdini subsets.\"\"\"\nfrom openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin\nfrom openpype.hosts.blender.api.lib import imprint\n\n\nclass BlenderLegacyConvertor(SubsetConvertorPlugin):\n    \"\"\"Find and convert any legacy subsets in the scene.\n\n    This Converter will find all legacy subsets in the scene and will\n    transform them to the current system. Since the old subsets doesn't\n    retain any information about their original creators, the only mapping\n    we can do is based on their families.\n\n    Its limitation is that you can have multiple creators creating subset\n    of the same family and there is no way to handle it. This code should\n    nevertheless cover all creators that came with OpenPype.\n\n    \"\"\"\n    identifier = \"io.openpype.creators.blender.legacy\"\n    family_to_id = {\n        \"action\": \"io.openpype.creators.blender.action\",\n        \"camera\": \"io.openpype.creators.blender.camera\",\n        \"animation\": \"io.openpype.creators.blender.animation\",\n        \"blendScene\": \"io.openpype.creators.blender.blendscene\",\n        \"layout\": \"io.openpype.creators.blender.layout\",\n        \"model\": \"io.openpype.creators.blender.model\",\n        \"pointcache\": \"io.openpype.creators.blender.pointcache\",\n        \"render\": \"io.openpype.creators.blender.render\",\n        \"review\": \"io.openpype.creators.blender.review\",\n        \"rig\": \"io.openpype.creators.blender.rig\",\n    }\n\n    def __init__(self, *args, **kwargs):\n        super(BlenderLegacyConvertor, self).__init__(*args, **kwargs)\n        self.legacy_subsets = {}\n\n    def find_instances(self):\n        \"\"\"Find legacy subsets in the scene.\n\n        Legacy subsets are the ones that doesn't have `creator_identifier`\n        parameter on them.\n\n        This is using cached entries done in\n        :py:meth:`~BaseCreator.cache_subsets()`\n\n        \"\"\"\n        self.legacy_subsets = self.collection_shared_data.get(\n            \"blender_cached_legacy_subsets\")\n        if not self.legacy_subsets:\n            return\n        self.add_convertor_item(\n            \"Found {} incompatible subset{}\".format(\n                len(self.legacy_subsets),\n                \"s\" if len(self.legacy_subsets) > 1 else \"\"\n            )\n        )\n\n    def convert(self):\n        \"\"\"Convert all legacy subsets to current.\n\n        It is enough to add `creator_identifier` and `instance_node`.\n\n        \"\"\"\n        if not self.legacy_subsets:\n            return\n\n        for family, instance_nodes in self.legacy_subsets.items():\n            if family in self.family_to_id:\n                for instance_node in instance_nodes:\n                    creator_identifier = self.family_to_id[family]\n                    self.log.info(\n                        \"Converting {} to {}\".format(instance_node.name,\n                                                     creator_identifier)\n                    )\n                    imprint(instance_node, data={\n                        \"creator_identifier\": creator_identifier\n                    })\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/create/create_action.py",
    "content": "\"\"\"Create an animation asset.\"\"\"\n\nimport bpy\n\nfrom openpype.hosts.blender.api import lib, plugin\n\n\nclass CreateAction(plugin.BaseCreator):\n    \"\"\"Action output for character rigs.\"\"\"\n\n    identifier = \"io.openpype.creators.blender.action\"\n    label = \"Action\"\n    family = \"action\"\n    icon = \"male\"\n\n    def create(\n        self, subset_name: str, instance_data: dict, pre_create_data: dict\n    ):\n        # Run parent create method\n        collection = super().create(\n            subset_name, instance_data, pre_create_data\n        )\n\n        # Get instance name\n        name = plugin.prepare_scene_name(instance_data[\"asset\"], subset_name)\n\n        if pre_create_data.get(\"use_selection\"):\n            for obj in lib.get_selection():\n                if (obj.animation_data is not None\n                        and obj.animation_data.action is not None):\n\n                    empty_obj = bpy.data.objects.new(name=name,\n                                                     object_data=None)\n                    empty_obj.animation_data_create()\n                    empty_obj.animation_data.action = obj.animation_data.action\n                    empty_obj.animation_data.action.name = name\n                    collection.objects.link(empty_obj)\n\n        return collection\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/create/create_animation.py",
    "content": "\"\"\"Create an animation asset.\"\"\"\n\nfrom openpype.hosts.blender.api import plugin, lib\n\n\nclass CreateAnimation(plugin.BaseCreator):\n    \"\"\"Animation output for character rigs.\"\"\"\n\n    identifier = \"io.openpype.creators.blender.animation\"\n    label = \"Animation\"\n    family = \"animation\"\n    icon = \"male\"\n\n    def create(\n        self, subset_name: str, instance_data: dict, pre_create_data: dict\n    ):\n        # Run parent create method\n        collection = super().create(\n            subset_name, instance_data, pre_create_data\n        )\n\n        if pre_create_data.get(\"use_selection\"):\n            selected = lib.get_selection()\n            for obj in selected:\n                collection.objects.link(obj)\n        elif pre_create_data.get(\"asset_group\"):\n            # Use for Load Blend automated creation of animation instances\n            # upon loading rig files\n            obj = pre_create_data.get(\"asset_group\")\n            collection.objects.link(obj)\n\n        return collection\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/create/create_blendScene.py",
    "content": "\"\"\"Create a Blender scene asset.\"\"\"\n\nimport bpy\n\nfrom openpype.hosts.blender.api import plugin, lib\n\n\nclass CreateBlendScene(plugin.BaseCreator):\n    \"\"\"Generic group of assets.\"\"\"\n\n    identifier = \"io.openpype.creators.blender.blendscene\"\n    label = \"Blender Scene\"\n    family = \"blendScene\"\n    icon = \"cubes\"\n\n    maintain_selection = False\n\n    def create(\n        self, subset_name: str, instance_data: dict, pre_create_data: dict\n    ):\n\n        instance_node = super().create(subset_name,\n                                       instance_data,\n                                       pre_create_data)\n\n        if pre_create_data.get(\"use_selection\"):\n            selection = lib.get_selection(include_collections=True)\n            for data in selection:\n                if isinstance(data, bpy.types.Collection):\n                    instance_node.children.link(data)\n                elif isinstance(data, bpy.types.Object):\n                    instance_node.objects.link(data)\n\n        return instance_node\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/create/create_camera.py",
    "content": "\"\"\"Create a camera asset.\"\"\"\n\nimport bpy\n\nfrom openpype.hosts.blender.api import plugin, lib\nfrom openpype.hosts.blender.api.pipeline import AVALON_INSTANCES\n\n\nclass CreateCamera(plugin.BaseCreator):\n    \"\"\"Polygonal static geometry.\"\"\"\n\n    identifier = \"io.openpype.creators.blender.camera\"\n    label = \"Camera\"\n    family = \"camera\"\n    icon = \"video-camera\"\n\n    create_as_asset_group = True\n\n    def create(\n        self, subset_name: str, instance_data: dict, pre_create_data: dict\n    ):\n\n        asset_group = super().create(subset_name,\n                                     instance_data,\n                                     pre_create_data)\n\n        bpy.context.view_layer.objects.active = asset_group\n        if pre_create_data.get(\"use_selection\"):\n            for obj in lib.get_selection():\n                obj.parent = asset_group\n        else:\n            plugin.deselect_all()\n            camera = bpy.data.cameras.new(subset_name)\n            camera_obj = bpy.data.objects.new(subset_name, camera)\n\n            instances = bpy.data.collections.get(AVALON_INSTANCES)\n            instances.objects.link(camera_obj)\n\n            bpy.context.view_layer.objects.active = asset_group\n            camera_obj.parent = asset_group\n\n        return asset_group\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/create/create_layout.py",
    "content": "\"\"\"Create a layout asset.\"\"\"\n\nimport bpy\n\nfrom openpype.hosts.blender.api import plugin, lib\n\n\nclass CreateLayout(plugin.BaseCreator):\n    \"\"\"Layout output for character rigs.\"\"\"\n\n    identifier = \"io.openpype.creators.blender.layout\"\n    label = \"Layout\"\n    family = \"layout\"\n    icon = \"cubes\"\n\n    create_as_asset_group = True\n\n    def create(\n        self, subset_name: str, instance_data: dict, pre_create_data: dict\n    ):\n\n        asset_group = super().create(subset_name,\n                                     instance_data,\n                                     pre_create_data)\n\n        # Add selected objects to instance\n        if pre_create_data.get(\"use_selection\"):\n            bpy.context.view_layer.objects.active = asset_group\n            for obj in lib.get_selection():\n                obj.parent = asset_group\n\n        return asset_group\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/create/create_model.py",
    "content": "\"\"\"Create a model asset.\"\"\"\n\nimport bpy\n\nfrom openpype.hosts.blender.api import plugin, lib\n\n\nclass CreateModel(plugin.BaseCreator):\n    \"\"\"Polygonal static geometry.\"\"\"\n\n    identifier = \"io.openpype.creators.blender.model\"\n    label = \"Model\"\n    family = \"model\"\n    icon = \"cube\"\n\n    create_as_asset_group = True\n\n    def create(\n        self, subset_name: str, instance_data: dict, pre_create_data: dict\n    ):\n        asset_group = super().create(subset_name,\n                                     instance_data,\n                                     pre_create_data)\n\n        # Add selected objects to instance\n        if pre_create_data.get(\"use_selection\"):\n            bpy.context.view_layer.objects.active = asset_group\n            for obj in lib.get_selection():\n                obj.parent = asset_group\n\n        return asset_group\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/create/create_pointcache.py",
    "content": "\"\"\"Create a pointcache asset.\"\"\"\n\nfrom openpype.hosts.blender.api import plugin, lib\n\n\nclass CreatePointcache(plugin.BaseCreator):\n    \"\"\"Polygonal static geometry.\"\"\"\n\n    identifier = \"io.openpype.creators.blender.pointcache\"\n    label = \"Point Cache\"\n    family = \"pointcache\"\n    icon = \"gears\"\n\n    def create(\n        self, subset_name: str, instance_data: dict, pre_create_data: dict\n    ):\n        # Run parent create method\n        collection = super().create(\n            subset_name, instance_data, pre_create_data\n        )\n\n        if pre_create_data.get(\"use_selection\"):\n            objects = lib.get_selection()\n            for obj in objects:\n                collection.objects.link(obj)\n                if obj.type == 'EMPTY':\n                    objects.extend(obj.children)\n\n        return collection\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/create/create_render.py",
    "content": "\"\"\"Create render.\"\"\"\nimport bpy\n\nfrom openpype.lib import version_up\nfrom openpype.hosts.blender.api import plugin\nfrom openpype.hosts.blender.api.render_lib import prepare_rendering\nfrom openpype.hosts.blender.api.workio import save_file\n\n\nclass CreateRenderlayer(plugin.BaseCreator):\n    \"\"\"Single baked camera.\"\"\"\n\n    identifier = \"io.openpype.creators.blender.render\"\n    label = \"Render\"\n    family = \"render\"\n    icon = \"eye\"\n\n    def create(\n        self, subset_name: str, instance_data: dict, pre_create_data: dict\n    ):\n        try:\n            # Run parent create method\n            collection = super().create(\n                subset_name, instance_data, pre_create_data\n            )\n\n            prepare_rendering(collection)\n        except Exception:\n            # Remove the instance if there was an error\n            bpy.data.collections.remove(collection)\n            raise\n\n        # TODO: this is undesiderable, but it's the only way to be sure that\n        # the file is saved before the render starts.\n        # Blender, by design, doesn't set the file as dirty if modifications\n        # happen by script. So, when creating the instance and setting the\n        # render settings, the file is not marked as dirty. This means that\n        # there is the risk of sending to deadline a file without the right\n        # settings. Even the validator to check that the file is saved will\n        # detect the file as saved, even if it isn't. The only solution for\n        # now it is to force the file to be saved.\n        filepath = version_up(bpy.data.filepath)\n        save_file(filepath, copy=False)\n\n        return collection\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/create/create_review.py",
    "content": "\"\"\"Create review.\"\"\"\n\nfrom openpype.hosts.blender.api import plugin, lib\n\n\nclass CreateReview(plugin.BaseCreator):\n    \"\"\"Single baked camera.\"\"\"\n\n    identifier = \"io.openpype.creators.blender.review\"\n    label = \"Review\"\n    family = \"review\"\n    icon = \"video-camera\"\n\n    def create(\n        self, subset_name: str, instance_data: dict, pre_create_data: dict\n    ):\n        # Run parent create method\n        collection = super().create(\n            subset_name, instance_data, pre_create_data\n        )\n\n        if pre_create_data.get(\"use_selection\"):\n            selected = lib.get_selection()\n            for obj in selected:\n                collection.objects.link(obj)\n\n        return collection\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/create/create_rig.py",
    "content": "\"\"\"Create a rig asset.\"\"\"\n\nimport bpy\n\nfrom openpype.hosts.blender.api import plugin, lib\n\n\nclass CreateRig(plugin.BaseCreator):\n    \"\"\"Artist-friendly rig with controls to direct motion.\"\"\"\n\n    identifier = \"io.openpype.creators.blender.rig\"\n    label = \"Rig\"\n    family = \"rig\"\n    icon = \"wheelchair\"\n\n    create_as_asset_group = True\n\n    def create(\n        self, subset_name: str, instance_data: dict, pre_create_data: dict\n    ):\n        asset_group = super().create(subset_name,\n                                     instance_data,\n                                     pre_create_data)\n\n        # Add selected objects to instance\n        if pre_create_data.get(\"use_selection\"):\n            bpy.context.view_layer.objects.active = asset_group\n            for obj in lib.get_selection():\n                obj.parent = asset_group\n\n        return asset_group\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/create/create_workfile.py",
    "content": "import bpy\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import CreatedInstance, AutoCreator\nfrom openpype.client import get_asset_by_name\nfrom openpype.hosts.blender.api.plugin import BaseCreator\nfrom openpype.hosts.blender.api.pipeline import (\n    AVALON_PROPERTY,\n    AVALON_CONTAINERS\n)\n\n\nclass CreateWorkfile(BaseCreator, AutoCreator):\n    \"\"\"Workfile auto-creator.\n\n    The workfile instance stores its data on the `AVALON_CONTAINERS` collection\n    as custom attributes, because unlike other instances it doesn't have an\n    instance node of its own.\n\n    \"\"\"\n    identifier = \"io.openpype.creators.blender.workfile\"\n    label = \"Workfile\"\n    family = \"workfile\"\n    icon = \"fa5.file\"\n\n    def create(self):\n        \"\"\"Create workfile instances.\"\"\"\n        workfile_instance = next(\n            (\n                instance for instance in self.create_context.instances\n                if instance.creator_identifier == self.identifier\n            ),\n            None,\n        )\n\n        project_name = self.project_name\n        asset_name = self.create_context.get_current_asset_name()\n        task_name = self.create_context.get_current_task_name()\n        host_name = self.create_context.host_name\n\n        existing_asset_name = None\n        if workfile_instance is not None:\n            if AYON_SERVER_ENABLED:\n                existing_asset_name = workfile_instance.get(\"folderPath\")\n\n            if existing_asset_name is None:\n                existing_asset_name = workfile_instance[\"asset\"]\n\n        if not workfile_instance:\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                task_name, task_name, asset_doc, project_name, host_name\n            )\n            data = {\n                \"task\": task_name,\n                \"variant\": task_name,\n            }\n            if AYON_SERVER_ENABLED:\n                data[\"folderPath\"] = asset_name\n            else:\n                data[\"asset\"] = asset_name\n            data.update(\n                self.get_dynamic_data(\n                    task_name,\n                    task_name,\n                    asset_doc,\n                    project_name,\n                    host_name,\n                    workfile_instance,\n                )\n            )\n            self.log.info(\"Auto-creating workfile instance...\")\n            workfile_instance = CreatedInstance(\n                self.family, subset_name, data, self\n            )\n            self._add_instance_to_context(workfile_instance)\n\n        elif (\n            existing_asset_name != asset_name\n            or workfile_instance[\"task\"] != task_name\n        ):\n            # Update instance context if it's different\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                task_name, task_name, asset_doc, project_name, host_name\n            )\n            if AYON_SERVER_ENABLED:\n                workfile_instance[\"folderPath\"] = asset_name\n            else:\n                workfile_instance[\"asset\"] = asset_name\n\n            workfile_instance[\"task\"] = task_name\n            workfile_instance[\"subset\"] = subset_name\n\n        instance_node = bpy.data.collections.get(AVALON_CONTAINERS)\n        if not instance_node:\n            instance_node = bpy.data.collections.new(name=AVALON_CONTAINERS)\n        workfile_instance.transient_data[\"instance_node\"] = instance_node\n\n    def collect_instances(self):\n\n        instance_node = bpy.data.collections.get(AVALON_CONTAINERS)\n        if not instance_node:\n            return\n\n        property = instance_node.get(AVALON_PROPERTY)\n        if not property:\n            return\n\n        # Create instance object from existing data\n        instance = CreatedInstance.from_existing(\n            instance_data=property.to_dict(),\n            creator=self\n        )\n        instance.transient_data[\"instance_node\"] = instance_node\n\n        # Add instance to create context\n        self._add_instance_to_context(instance)\n\n    def remove_instances(self, instances):\n        for instance in instances:\n            node = instance.transient_data[\"instance_node\"]\n            del node[AVALON_PROPERTY]\n\n            self._remove_instance_from_context(instance)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/load/import_workfile.py",
    "content": "import bpy\n\nfrom openpype.hosts.blender.api import plugin\n\n\ndef append_workfile(context, fname, do_import):\n    asset = context['asset']['name']\n    subset = context['subset']['name']\n\n    group_name = plugin.prepare_scene_name(asset, subset)\n\n    # We need to preserve the original names of the scenes, otherwise,\n    # if there are duplicate names in the current workfile, the imported\n    # scenes will be renamed by Blender to avoid conflicts.\n    original_scene_names = []\n\n    with bpy.data.libraries.load(fname) as (data_from, data_to):\n        for attr in dir(data_to):\n            if attr == \"scenes\":\n                for scene in data_from.scenes:\n                    original_scene_names.append(scene)\n            setattr(data_to, attr, getattr(data_from, attr))\n\n    current_scene = bpy.context.scene\n\n    for scene, s_name in zip(data_to.scenes, original_scene_names):\n        scene.name = f\"{group_name}_{s_name}\"\n        if do_import:\n            collection = bpy.data.collections.new(f\"{group_name}_{s_name}\")\n            for obj in scene.objects:\n                collection.objects.link(obj)\n            current_scene.collection.children.link(collection)\n            for coll in scene.collection.children:\n                collection.children.link(coll)\n\n\nclass AppendBlendLoader(plugin.AssetLoader):\n    \"\"\"Append workfile in Blender (unmanaged)\n\n    Warning:\n        The loaded content will be unmanaged and is *not* visible in the\n        scene inventory. It's purely intended to merge content into your scene\n        so you could also use it as a new base.\n    \"\"\"\n\n    representations = [\"blend\"]\n    families = [\"workfile\"]\n\n    label = \"Append Workfile\"\n    order = 9\n    icon = \"arrow-circle-down\"\n    color = \"#775555\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        path = self.filepath_from_context(context)\n        append_workfile(context, path, False)\n\n        # We do not containerize imported content, it remains unmanaged\n        return\n\n\nclass ImportBlendLoader(plugin.AssetLoader):\n    \"\"\"Import workfile in the current Blender scene (unmanaged)\n\n    Warning:\n        The loaded content will be unmanaged and is *not* visible in the\n        scene inventory. It's purely intended to merge content into your scene\n        so you could also use it as a new base.\n    \"\"\"\n\n    representations = [\"blend\"]\n    families = [\"workfile\"]\n\n    label = \"Import Workfile\"\n    order = 9\n    icon = \"arrow-circle-down\"\n    color = \"#775555\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        path = self.filepath_from_context(context)\n        append_workfile(context, path, True)\n\n        # We do not containerize imported content, it remains unmanaged\n        return\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/load/load_abc.py",
    "content": "\"\"\"Load an asset in Blender from an Alembic file.\"\"\"\n\nfrom pathlib import Path\nfrom pprint import pformat\nfrom typing import Dict, List, Optional\n\nimport bpy\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AVALON_CONTAINER_ID,\n)\n\nfrom openpype.hosts.blender.api.pipeline import (\n    AVALON_CONTAINERS,\n    AVALON_PROPERTY,\n)\nfrom openpype.hosts.blender.api import plugin, lib\n\n\nclass CacheModelLoader(plugin.AssetLoader):\n    \"\"\"Load cache models.\n\n    Stores the imported asset in a collection named after the asset.\n\n    Note:\n        At least for now it only supports Alembic files.\n    \"\"\"\n    families = [\"model\", \"pointcache\", \"animation\"]\n    representations = [\"abc\"]\n\n    label = \"Load Alembic\"\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def _remove(self, asset_group):\n        objects = list(asset_group.children)\n        empties = []\n\n        for obj in objects:\n            if obj.type == 'MESH':\n                for material_slot in list(obj.material_slots):\n                    bpy.data.materials.remove(material_slot.material)\n                bpy.data.meshes.remove(obj.data)\n            elif obj.type == 'EMPTY':\n                objects.extend(obj.children)\n                empties.append(obj)\n\n        for empty in empties:\n            bpy.data.objects.remove(empty)\n\n    def _process(self, libpath, asset_group, group_name):\n        plugin.deselect_all()\n\n        relative = bpy.context.preferences.filepaths.use_relative_paths\n        bpy.ops.wm.alembic_import(\n            filepath=libpath,\n            relative_path=relative\n        )\n\n        imported = lib.get_selection()\n\n        # Use first EMPTY without parent as container\n        container = next(\n            (obj for obj in imported\n             if obj.type == \"EMPTY\" and not obj.parent),\n            None\n        )\n\n        objects = []\n        if container:\n            nodes = list(container.children)\n\n            for obj in nodes:\n                obj.parent = asset_group\n\n            bpy.data.objects.remove(container)\n\n            objects.extend(nodes)\n            for obj in nodes:\n                objects.extend(obj.children_recursive)\n        else:\n            for obj in imported:\n                obj.parent = asset_group\n            objects = imported\n\n        for obj in objects:\n            # Unlink the object from all collections\n            collections = obj.users_collection\n            for collection in collections:\n                collection.objects.unlink(obj)\n            name = obj.name\n            obj.name = f\"{group_name}:{name}\"\n            if obj.type != 'EMPTY':\n                name_data = obj.data.name\n                obj.data.name = f\"{group_name}:{name_data}\"\n\n                for material_slot in obj.material_slots:\n                    name_mat = material_slot.material.name\n                    material_slot.material.name = f\"{group_name}:{name_mat}\"\n\n            if not obj.get(AVALON_PROPERTY):\n                obj[AVALON_PROPERTY] = {}\n\n            avalon_info = obj[AVALON_PROPERTY]\n            avalon_info.update({\"container_name\": group_name})\n\n        plugin.deselect_all()\n\n        return objects\n\n    def _link_objects(self, objects, collection, containers, asset_group):\n        # Link the imported objects to any collection where the asset group is\n        # linked to, except the AVALON_CONTAINERS collection\n        group_collections = [\n            collection\n            for collection in asset_group.users_collection\n            if collection != containers]\n\n        for obj in objects:\n            for collection in group_collections:\n                collection.objects.link(obj)\n\n    def process_asset(\n        self, context: dict, name: str, namespace: Optional[str] = None,\n        options: Optional[Dict] = None\n    ) -> Optional[List]:\n        \"\"\"\n        Arguments:\n            name: Use pre-defined name\n            namespace: Use pre-defined namespace\n            context: Full parenthood of representation to load\n            options: Additional settings dictionary\n        \"\"\"\n\n        libpath = self.filepath_from_context(context)\n        asset = context[\"asset\"][\"name\"]\n        subset = context[\"subset\"][\"name\"]\n\n        asset_name = plugin.prepare_scene_name(asset, subset)\n        unique_number = plugin.get_unique_number(asset, subset)\n        group_name = plugin.prepare_scene_name(asset, subset, unique_number)\n        namespace = namespace or f\"{asset}_{unique_number}\"\n\n        containers = bpy.data.collections.get(AVALON_CONTAINERS)\n        if not containers:\n            containers = bpy.data.collections.new(name=AVALON_CONTAINERS)\n            bpy.context.scene.collection.children.link(containers)\n\n        asset_group = bpy.data.objects.new(group_name, object_data=None)\n        asset_group.empty_display_type = 'SINGLE_ARROW'\n        containers.objects.link(asset_group)\n\n        objects = self._process(libpath, asset_group, group_name)\n\n        # Link the asset group to the active collection\n        collection = bpy.context.view_layer.active_layer_collection.collection\n        collection.objects.link(asset_group)\n\n        self._link_objects(objects, asset_group, containers, asset_group)\n\n        asset_group[AVALON_PROPERTY] = {\n            \"schema\": \"openpype:container-2.0\",\n            \"id\": AVALON_CONTAINER_ID,\n            \"name\": name,\n            \"namespace\": namespace or '',\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": str(context[\"representation\"][\"_id\"]),\n            \"libpath\": libpath,\n            \"asset_name\": asset_name,\n            \"parent\": str(context[\"representation\"][\"parent\"]),\n            \"family\": context[\"representation\"][\"context\"][\"family\"],\n            \"objectName\": group_name\n        }\n\n        self[:] = objects\n        return objects\n\n    def exec_update(self, container: Dict, representation: Dict):\n        \"\"\"Update the loaded asset.\n\n        This will remove all objects of the current collection, load the new\n        ones and add them to the collection.\n        If the objects of the collection are used in another collection they\n        will not be removed, only unlinked. Normally this should not be the\n        case though.\n\n        Warning:\n            No nested collections are supported at the moment!\n        \"\"\"\n        object_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(object_name)\n        libpath = Path(get_representation_path(representation))\n        extension = libpath.suffix.lower()\n\n        self.log.info(\n            \"Container: %s\\nRepresentation: %s\",\n            pformat(container, indent=2),\n            pformat(representation, indent=2),\n        )\n\n        assert asset_group, (\n            f\"The asset is not loaded: {container['objectName']}\"\n        )\n        assert libpath, (\n            \"No existing library file found for {container['objectName']}\"\n        )\n        assert libpath.is_file(), (\n            f\"The file doesn't exist: {libpath}\"\n        )\n        assert extension in plugin.VALID_EXTENSIONS, (\n            f\"Unsupported file: {libpath}\"\n        )\n\n        metadata = asset_group.get(AVALON_PROPERTY)\n        group_libpath = metadata[\"libpath\"]\n\n        normalized_group_libpath = (\n            str(Path(bpy.path.abspath(group_libpath)).resolve())\n        )\n        normalized_libpath = (\n            str(Path(bpy.path.abspath(str(libpath))).resolve())\n        )\n        self.log.debug(\n            \"normalized_group_libpath:\\n  %s\\nnormalized_libpath:\\n  %s\",\n            normalized_group_libpath,\n            normalized_libpath,\n        )\n        if normalized_group_libpath == normalized_libpath:\n            self.log.info(\"Library already loaded, not updating...\")\n            return\n\n        mat = asset_group.matrix_basis.copy()\n        self._remove(asset_group)\n\n        objects = self._process(str(libpath), asset_group, object_name)\n\n        containers = bpy.data.collections.get(AVALON_CONTAINERS)\n        self._link_objects(objects, asset_group, containers, asset_group)\n\n        asset_group.matrix_basis = mat\n\n        metadata[\"libpath\"] = str(libpath)\n        metadata[\"representation\"] = str(representation[\"_id\"])\n\n    def exec_remove(self, container: Dict) -> bool:\n        \"\"\"Remove an existing container from a Blender scene.\n\n        Arguments:\n            container (openpype:container-1.0): Container to remove,\n                from `host.ls()`.\n\n        Returns:\n            bool: Whether the container was deleted.\n\n        Warning:\n            No nested collections are supported at the moment!\n        \"\"\"\n        object_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(object_name)\n\n        if not asset_group:\n            return False\n\n        self._remove(asset_group)\n\n        bpy.data.objects.remove(asset_group)\n\n        return True\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/load/load_action.py",
    "content": "\"\"\"Load an action in Blender.\"\"\"\n\nimport logging\nfrom pathlib import Path\nfrom pprint import pformat\nfrom typing import Dict, List, Optional\n\nimport bpy\nfrom openpype.pipeline import get_representation_path\nfrom openpype.hosts.blender.api import plugin\nfrom openpype.hosts.blender.api.pipeline import (\n    containerise_existing,\n    AVALON_PROPERTY,\n)\n\nlogger = logging.getLogger(\"openpype\").getChild(\"blender\").getChild(\"load_action\")\n\n\nclass BlendActionLoader(plugin.AssetLoader):\n    \"\"\"Load action from a .blend file.\n\n    Warning:\n        Loading the same asset more then once is not properly supported at the\n        moment.\n    \"\"\"\n\n    families = [\"action\"]\n    representations = [\"blend\"]\n\n    label = \"Link Action\"\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def process_asset(\n        self, context: dict, name: str, namespace: Optional[str] = None,\n        options: Optional[Dict] = None\n    ) -> Optional[List]:\n        \"\"\"\n        Arguments:\n            name: Use pre-defined name\n            namespace: Use pre-defined namespace\n            context: Full parenthood of representation to load\n            options: Additional settings dictionary\n        \"\"\"\n\n        libpath = self.filepath_from_context(context)\n        asset = context[\"asset\"][\"name\"]\n        subset = context[\"subset\"][\"name\"]\n        lib_container = plugin.prepare_scene_name(asset, subset)\n        container_name = plugin.prepare_scene_name(\n            asset, subset, namespace\n        )\n\n        container = bpy.data.collections.new(lib_container)\n        container.name = container_name\n        containerise_existing(\n            container,\n            name,\n            namespace,\n            context,\n            self.__class__.__name__,\n        )\n\n        container_metadata = container.get(AVALON_PROPERTY)\n\n        container_metadata[\"libpath\"] = libpath\n        container_metadata[\"lib_container\"] = lib_container\n\n        relative = bpy.context.preferences.filepaths.use_relative_paths\n        with bpy.data.libraries.load(\n            libpath, link=True, relative=relative\n        ) as (_, data_to):\n            data_to.collections = [lib_container]\n\n        collection = bpy.context.scene.collection\n\n        collection.children.link(bpy.data.collections[lib_container])\n\n        animation_container = collection.children[lib_container].make_local()\n\n        objects_list = []\n\n        # Link meshes first, then armatures.\n        # The armature is unparented for all the non-local meshes,\n        # when it is made local.\n        for obj in animation_container.objects:\n\n            obj = obj.make_local()\n\n            anim_data = obj.animation_data\n\n            if anim_data is not None and anim_data.action is not None:\n\n                anim_data.action.make_local()\n\n            if not obj.get(AVALON_PROPERTY):\n\n                obj[AVALON_PROPERTY] = dict()\n\n            avalon_info = obj[AVALON_PROPERTY]\n            avalon_info.update({\"container_name\": container_name})\n\n            objects_list.append(obj)\n\n        animation_container.pop(AVALON_PROPERTY)\n\n        # Save the list of objects in the metadata container\n        container_metadata[\"objects\"] = objects_list\n\n        bpy.ops.object.select_all(action='DESELECT')\n\n        nodes = list(container.objects)\n        nodes.append(container)\n        self[:] = nodes\n        return nodes\n\n    def update(self, container: Dict, representation: Dict):\n        \"\"\"Update the loaded asset.\n\n        This will remove all objects of the current collection, load the new\n        ones and add them to the collection.\n        If the objects of the collection are used in another collection they\n        will not be removed, only unlinked. Normally this should not be the\n        case though.\n\n        Warning:\n            No nested collections are supported at the moment!\n        \"\"\"\n\n        collection = bpy.data.collections.get(\n            container[\"objectName\"]\n        )\n\n        libpath = Path(get_representation_path(representation))\n        extension = libpath.suffix.lower()\n\n        logger.info(\n            \"Container: %s\\nRepresentation: %s\",\n            pformat(container, indent=2),\n            pformat(representation, indent=2),\n        )\n\n        assert collection, (\n            f\"The asset is not loaded: {container['objectName']}\"\n        )\n        assert not (collection.children), (\n            \"Nested collections are not supported.\"\n        )\n        assert libpath, (\n            \"No existing library file found for {container['objectName']}\"\n        )\n        assert libpath.is_file(), (\n            f\"The file doesn't exist: {libpath}\"\n        )\n        assert extension in plugin.VALID_EXTENSIONS, (\n            f\"Unsupported file: {libpath}\"\n        )\n\n        collection_metadata = collection.get(AVALON_PROPERTY)\n\n        collection_libpath = collection_metadata[\"libpath\"]\n        normalized_collection_libpath = (\n            str(Path(bpy.path.abspath(collection_libpath)).resolve())\n        )\n        normalized_libpath = (\n            str(Path(bpy.path.abspath(str(libpath))).resolve())\n        )\n        logger.debug(\n            \"normalized_collection_libpath:\\n  %s\\nnormalized_libpath:\\n  %s\",\n            normalized_collection_libpath,\n            normalized_libpath,\n        )\n        if normalized_collection_libpath == normalized_libpath:\n            logger.info(\"Library already loaded, not updating...\")\n            return\n\n        strips = []\n\n        for obj in list(collection_metadata[\"objects\"]):\n            # Get all the strips that use the action\n            arm_objs = [\n                arm for arm in bpy.data.objects if arm.type == 'ARMATURE']\n\n            for armature_obj in arm_objs:\n                if armature_obj.animation_data is not None:\n                    for track in armature_obj.animation_data.nla_tracks:\n                        for strip in track.strips:\n                            if strip.action == obj.animation_data.action:\n                                strips.append(strip)\n\n            bpy.data.actions.remove(obj.animation_data.action)\n            bpy.data.objects.remove(obj)\n\n        lib_container = collection_metadata[\"lib_container\"]\n\n        bpy.data.collections.remove(bpy.data.collections[lib_container])\n\n        relative = bpy.context.preferences.filepaths.use_relative_paths\n        with bpy.data.libraries.load(\n            str(libpath), link=True, relative=relative\n        ) as (_, data_to):\n            data_to.collections = [lib_container]\n\n        scene = bpy.context.scene\n\n        scene.collection.children.link(bpy.data.collections[lib_container])\n\n        anim_container = scene.collection.children[lib_container].make_local()\n\n        objects_list = []\n\n        # Link meshes first, then armatures.\n        # The armature is unparented for all the non-local meshes,\n        # when it is made local.\n        for obj in anim_container.objects:\n\n            obj = obj.make_local()\n\n            anim_data = obj.animation_data\n\n            if anim_data is not None and anim_data.action is not None:\n\n                anim_data.action.make_local()\n\n                for strip in strips:\n\n                    strip.action = anim_data.action\n                    strip.action_frame_end = anim_data.action.frame_range[1]\n\n            if not obj.get(AVALON_PROPERTY):\n\n                obj[AVALON_PROPERTY] = dict()\n\n            avalon_info = obj[AVALON_PROPERTY]\n            avalon_info.update({\"container_name\": collection.name})\n\n            objects_list.append(obj)\n\n        anim_container.pop(AVALON_PROPERTY)\n\n        # Save the list of objects in the metadata container\n        collection_metadata[\"objects\"] = objects_list\n        collection_metadata[\"libpath\"] = str(libpath)\n        collection_metadata[\"representation\"] = str(representation[\"_id\"])\n\n        bpy.ops.object.select_all(action='DESELECT')\n\n    def remove(self, container: Dict) -> bool:\n        \"\"\"Remove an existing container from a Blender scene.\n\n        Arguments:\n            container (openpype:container-1.0): Container to remove,\n                from `host.ls()`.\n\n        Returns:\n            bool: Whether the container was deleted.\n\n        Warning:\n            No nested collections are supported at the moment!\n        \"\"\"\n\n        collection = bpy.data.collections.get(\n            container[\"objectName\"]\n        )\n        if not collection:\n            return False\n        assert not (collection.children), (\n            \"Nested collections are not supported.\"\n        )\n\n        collection_metadata = collection.get(AVALON_PROPERTY)\n        objects = collection_metadata[\"objects\"]\n        lib_container = collection_metadata[\"lib_container\"]\n\n        for obj in list(objects):\n            # Get all the strips that use the action\n            arm_objs = [\n                arm for arm in bpy.data.objects if arm.type == 'ARMATURE']\n\n            for armature_obj in arm_objs:\n                if armature_obj.animation_data is not None:\n                    for track in armature_obj.animation_data.nla_tracks:\n                        for strip in track.strips:\n                            if strip.action == obj.animation_data.action:\n                                track.strips.remove(strip)\n\n            bpy.data.actions.remove(obj.animation_data.action)\n            bpy.data.objects.remove(obj)\n\n        bpy.data.collections.remove(bpy.data.collections[lib_container])\n        bpy.data.collections.remove(collection)\n\n        return True\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/load/load_animation.py",
    "content": "\"\"\"Load an animation in Blender.\"\"\"\n\nfrom typing import Dict, List, Optional\n\nimport bpy\n\nfrom openpype.hosts.blender.api import plugin\nfrom openpype.hosts.blender.api.pipeline import AVALON_PROPERTY\n\n\nclass BlendAnimationLoader(plugin.AssetLoader):\n    \"\"\"Load animations from a .blend file.\n\n    Warning:\n        Loading the same asset more then once is not properly supported at the\n        moment.\n    \"\"\"\n\n    families = [\"animation\"]\n    representations = [\"blend\"]\n\n    label = \"Link Animation\"\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def process_asset(\n        self, context: dict, name: str, namespace: Optional[str] = None,\n        options: Optional[Dict] = None\n    ) -> Optional[List]:\n        \"\"\"\n        Arguments:\n            name: Use pre-defined name\n            namespace: Use pre-defined namespace\n            context: Full parenthood of representation to load\n            options: Additional settings dictionary\n        \"\"\"\n        libpath = self.filepath_from_context(context)\n\n        with bpy.data.libraries.load(\n            libpath, link=True, relative=False\n        ) as (data_from, data_to):\n            data_to.objects = data_from.objects\n            data_to.actions = data_from.actions\n\n        container = data_to.objects[0]\n\n        assert container, \"No asset group found\"\n\n        target_namespace = container.get(AVALON_PROPERTY).get('namespace')\n\n        action = data_to.actions[0].make_local().copy()\n\n        for obj in bpy.data.objects:\n            if obj.get(AVALON_PROPERTY) and obj.get(AVALON_PROPERTY).get(\n                    'namespace') == target_namespace:\n                if obj.children[0]:\n                    if not obj.children[0].animation_data:\n                        obj.children[0].animation_data_create()\n                    obj.children[0].animation_data.action = action\n                break\n\n        bpy.data.objects.remove(container)\n\n        filename = bpy.path.basename(libpath)\n        # Blender has a limit of 63 characters for any data name.\n        # If the filename is longer, it will be truncated.\n        if len(filename) > 63:\n            filename = filename[:63]\n        library = bpy.data.libraries.get(filename)\n        bpy.data.libraries.remove(library)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/load/load_audio.py",
    "content": "\"\"\"Load audio in Blender.\"\"\"\n\nfrom pathlib import Path\nfrom pprint import pformat\nfrom typing import Dict, List, Optional\n\nimport bpy\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.hosts.blender.api import plugin\nfrom openpype.hosts.blender.api.pipeline import (\n    AVALON_CONTAINERS,\n    AVALON_PROPERTY,\n)\n\n\nclass AudioLoader(plugin.AssetLoader):\n    \"\"\"Load audio in Blender.\"\"\"\n\n    families = [\"audio\"]\n    representations = [\"wav\"]\n\n    label = \"Load Audio\"\n    icon = \"volume-up\"\n    color = \"orange\"\n\n    def process_asset(\n        self, context: dict, name: str, namespace: Optional[str] = None,\n        options: Optional[Dict] = None\n    ) -> Optional[List]:\n        \"\"\"\n        Arguments:\n            name: Use pre-defined name\n            namespace: Use pre-defined namespace\n            context: Full parenthood of representation to load\n            options: Additional settings dictionary\n        \"\"\"\n        libpath = self.filepath_from_context(context)\n        asset = context[\"asset\"][\"name\"]\n        subset = context[\"subset\"][\"name\"]\n\n        asset_name = plugin.prepare_scene_name(asset, subset)\n        unique_number = plugin.get_unique_number(asset, subset)\n        group_name = plugin.prepare_scene_name(asset, subset, unique_number)\n        namespace = namespace or f\"{asset}_{unique_number}\"\n\n        avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)\n        if not avalon_container:\n            avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)\n            bpy.context.scene.collection.children.link(avalon_container)\n\n        asset_group = bpy.data.objects.new(group_name, object_data=None)\n        avalon_container.objects.link(asset_group)\n\n        # Blender needs the Sequence Editor in the current window, to be able\n        # to load the audio. We take one of the areas in the window, save its\n        # type, and switch to the Sequence Editor. After loading the audio,\n        # we switch back to the previous area.\n        window_manager = bpy.context.window_manager\n        old_type = window_manager.windows[-1].screen.areas[0].type\n        window_manager.windows[-1].screen.areas[0].type = \"SEQUENCE_EDITOR\"\n\n        # We override the context to load the audio in the sequence editor.\n        oc = bpy.context.copy()\n        oc[\"area\"] = window_manager.windows[-1].screen.areas[0]\n\n        with bpy.context.temp_override(**oc):\n            bpy.ops.sequencer.sound_strip_add(filepath=libpath, frame_start=1)\n\n        window_manager.windows[-1].screen.areas[0].type = old_type\n\n        p = Path(libpath)\n        audio = p.name\n\n        asset_group[AVALON_PROPERTY] = {\n            \"schema\": \"openpype:container-2.0\",\n            \"id\": AVALON_CONTAINER_ID,\n            \"name\": name,\n            \"namespace\": namespace or '',\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": str(context[\"representation\"][\"_id\"]),\n            \"libpath\": libpath,\n            \"asset_name\": asset_name,\n            \"parent\": str(context[\"representation\"][\"parent\"]),\n            \"family\": context[\"representation\"][\"context\"][\"family\"],\n            \"objectName\": group_name,\n            \"audio\": audio\n        }\n\n        objects = []\n        self[:] = objects\n        return [objects]\n\n    def exec_update(self, container: Dict, representation: Dict):\n        \"\"\"Update an audio strip in the sequence editor.\n\n        Arguments:\n            container (openpype:container-1.0): Container to update,\n                from `host.ls()`.\n            representation (openpype:representation-1.0): Representation to\n                update, from `host.ls()`.\n        \"\"\"\n        object_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(object_name)\n        libpath = Path(get_representation_path(representation))\n\n        self.log.info(\n            \"Container: %s\\nRepresentation: %s\",\n            pformat(container, indent=2),\n            pformat(representation, indent=2),\n        )\n\n        assert asset_group, (\n            f\"The asset is not loaded: {container['objectName']}\"\n        )\n        assert libpath, (\n            \"No existing library file found for {container['objectName']}\"\n        )\n        assert libpath.is_file(), (\n            f\"The file doesn't exist: {libpath}\"\n        )\n\n        metadata = asset_group.get(AVALON_PROPERTY)\n        group_libpath = metadata[\"libpath\"]\n\n        normalized_group_libpath = (\n            str(Path(bpy.path.abspath(group_libpath)).resolve())\n        )\n        normalized_libpath = (\n            str(Path(bpy.path.abspath(str(libpath))).resolve())\n        )\n        self.log.debug(\n            \"normalized_group_libpath:\\n  %s\\nnormalized_libpath:\\n  %s\",\n            normalized_group_libpath,\n            normalized_libpath,\n        )\n        if normalized_group_libpath == normalized_libpath:\n            self.log.info(\"Library already loaded, not updating...\")\n            return\n\n        old_audio = container[\"audio\"]\n        p = Path(libpath)\n        new_audio = p.name\n\n        # Blender needs the Sequence Editor in the current window, to be able\n        # to update the audio. We take one of the areas in the window, save its\n        # type, and switch to the Sequence Editor. After updating the audio,\n        # we switch back to the previous area.\n        window_manager = bpy.context.window_manager\n        old_type = window_manager.windows[-1].screen.areas[0].type\n        window_manager.windows[-1].screen.areas[0].type = \"SEQUENCE_EDITOR\"\n\n        # We override the context to load the audio in the sequence editor.\n        oc = bpy.context.copy()\n        oc[\"area\"] = window_manager.windows[-1].screen.areas[0]\n\n        with bpy.context.temp_override(**oc):\n            # We deselect all sequencer strips, and then select the one we\n            # need to remove.\n            bpy.ops.sequencer.select_all(action='DESELECT')\n            scene = bpy.context.scene\n            scene.sequence_editor.sequences_all[old_audio].select = True\n\n            bpy.ops.sequencer.delete()\n            bpy.data.sounds.remove(bpy.data.sounds[old_audio])\n\n            bpy.ops.sequencer.sound_strip_add(\n                filepath=str(libpath), frame_start=1)\n\n        window_manager.windows[-1].screen.areas[0].type = old_type\n\n        metadata[\"libpath\"] = str(libpath)\n        metadata[\"representation\"] = str(representation[\"_id\"])\n        metadata[\"parent\"] = str(representation[\"parent\"])\n        metadata[\"audio\"] = new_audio\n\n    def exec_remove(self, container: Dict) -> bool:\n        \"\"\"Remove an audio strip from the sequence editor and the container.\n\n        Arguments:\n            container (openpype:container-1.0): Container to remove,\n                from `host.ls()`.\n\n        Returns:\n            bool: Whether the container was deleted.\n        \"\"\"\n        object_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(object_name)\n\n        if not asset_group:\n            return False\n\n        audio = container[\"audio\"]\n\n        # Blender needs the Sequence Editor in the current window, to be able\n        # to remove the audio. We take one of the areas in the window, save its\n        # type, and switch to the Sequence Editor. After removing the audio,\n        # we switch back to the previous area.\n        window_manager = bpy.context.window_manager\n        old_type = window_manager.windows[-1].screen.areas[0].type\n        window_manager.windows[-1].screen.areas[0].type = \"SEQUENCE_EDITOR\"\n\n        # We override the context to load the audio in the sequence editor.\n        oc = bpy.context.copy()\n        oc[\"area\"] = window_manager.windows[-1].screen.areas[0]\n\n        with bpy.context.temp_override(**oc):\n            # We deselect all sequencer strips, and then select the one we\n            # need to remove.\n            bpy.ops.sequencer.select_all(action='DESELECT')\n            scene = bpy.context.scene\n            scene.sequence_editor.sequences_all[audio].select = True\n            bpy.ops.sequencer.delete()\n\n        window_manager.windows[-1].screen.areas[0].type = old_type\n\n        bpy.data.sounds.remove(bpy.data.sounds[audio])\n\n        bpy.data.objects.remove(asset_group)\n\n        return True\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/load/load_blend.py",
    "content": "from typing import Dict, List, Optional\nfrom pathlib import Path\n\nimport bpy\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AVALON_CONTAINER_ID,\n    registered_host\n)\nfrom openpype.pipeline.create import CreateContext\nfrom openpype.hosts.blender.api import plugin\nfrom openpype.hosts.blender.api.lib import imprint\nfrom openpype.hosts.blender.api.pipeline import (\n    AVALON_CONTAINERS,\n    AVALON_PROPERTY,\n)\n\n\nclass BlendLoader(plugin.AssetLoader):\n    \"\"\"Load assets from a .blend file.\"\"\"\n\n    families = [\"model\", \"rig\", \"layout\", \"camera\"]\n    representations = [\"blend\"]\n\n    label = \"Append Blend\"\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    @staticmethod\n    def _get_asset_container(objects):\n        empties = [obj for obj in objects if obj.type == 'EMPTY']\n\n        for empty in empties:\n            if empty.get(AVALON_PROPERTY) and empty.parent is None:\n                return empty\n\n        return None\n\n    @staticmethod\n    def get_all_container_parents(asset_group):\n        parent_containers = []\n        parent = asset_group.parent\n        while parent:\n            if parent.get(AVALON_PROPERTY):\n                parent_containers.append(parent)\n            parent = parent.parent\n\n        return parent_containers\n\n    def _post_process_layout(self, container, asset, representation):\n        rigs = [\n            obj for obj in container.children_recursive\n            if (\n                obj.type == 'EMPTY' and\n                obj.get(AVALON_PROPERTY) and\n                obj.get(AVALON_PROPERTY).get('family') == 'rig'\n            )\n        ]\n        if not rigs:\n            return\n\n        # Create animation instances for each rig\n        creator_identifier = \"io.openpype.creators.blender.animation\"\n        host = registered_host()\n        create_context = CreateContext(host)\n\n        for rig in rigs:\n            create_context.create(\n                creator_identifier=creator_identifier,\n                variant=rig.name.split(':')[-1],\n                pre_create_data={\n                    \"use_selection\": False,\n                    \"asset_group\": rig\n                }\n            )\n\n    def _process_data(self, libpath, group_name):\n        # Append all the data from the .blend file\n        with bpy.data.libraries.load(\n            libpath, link=False, relative=False\n        ) as (data_from, data_to):\n            for attr in dir(data_to):\n                setattr(data_to, attr, getattr(data_from, attr))\n\n        members = []\n\n        # Rename the object to add the asset name\n        for attr in dir(data_to):\n            for data in getattr(data_to, attr):\n                data.name = f\"{group_name}:{data.name}\"\n                members.append(data)\n\n        container = self._get_asset_container(data_to.objects)\n        assert container, \"No asset group found\"\n\n        container.name = group_name\n        container.empty_display_type = 'SINGLE_ARROW'\n\n        # Link the collection to the scene\n        bpy.context.scene.collection.objects.link(container)\n\n        # Link all the container children to the collection\n        for obj in container.children_recursive:\n            bpy.context.scene.collection.objects.link(obj)\n\n        # Remove the library from the blend file\n        filepath = bpy.path.basename(libpath)\n        # Blender has a limit of 63 characters for any data name.\n        # If the filepath is longer, it will be truncated.\n        if len(filepath) > 63:\n            filepath = filepath[:63]\n        library = bpy.data.libraries.get(filepath)\n        bpy.data.libraries.remove(library)\n\n        return container, members\n\n    def process_asset(\n        self, context: dict, name: str, namespace: Optional[str] = None,\n        options: Optional[Dict] = None\n    ) -> Optional[List]:\n        \"\"\"\n        Arguments:\n            name: Use pre-defined name\n            namespace: Use pre-defined namespace\n            context: Full parenthood of representation to load\n            options: Additional settings dictionary\n        \"\"\"\n        libpath = self.filepath_from_context(context)\n        asset = context[\"asset\"][\"name\"]\n        subset = context[\"subset\"][\"name\"]\n\n        try:\n            family = context[\"representation\"][\"context\"][\"family\"]\n        except ValueError:\n            family = \"model\"\n\n        representation = str(context[\"representation\"][\"_id\"])\n\n        asset_name = plugin.prepare_scene_name(asset, subset)\n        unique_number = plugin.get_unique_number(asset, subset)\n        group_name = plugin.prepare_scene_name(asset, subset, unique_number)\n        namespace = namespace or f\"{asset}_{unique_number}\"\n\n        avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)\n        if not avalon_container:\n            avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)\n            bpy.context.scene.collection.children.link(avalon_container)\n\n        container, members = self._process_data(libpath, group_name)\n\n        if family == \"layout\":\n            self._post_process_layout(container, asset, representation)\n\n        avalon_container.objects.link(container)\n\n        data = {\n            \"schema\": \"openpype:container-2.0\",\n            \"id\": AVALON_CONTAINER_ID,\n            \"name\": name,\n            \"namespace\": namespace or '',\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": str(context[\"representation\"][\"_id\"]),\n            \"libpath\": libpath,\n            \"asset_name\": asset_name,\n            \"parent\": str(context[\"representation\"][\"parent\"]),\n            \"family\": context[\"representation\"][\"context\"][\"family\"],\n            \"objectName\": group_name,\n            \"members\": members,\n        }\n\n        container[AVALON_PROPERTY] = data\n\n        objects = [\n            obj for obj in bpy.data.objects\n            if obj.name.startswith(f\"{group_name}:\")\n        ]\n\n        self[:] = objects\n        return objects\n\n    def exec_update(self, container: Dict, representation: Dict):\n        \"\"\"\n        Update the loaded asset.\n        \"\"\"\n        group_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(group_name)\n        libpath = Path(get_representation_path(representation)).as_posix()\n\n        assert asset_group, (\n            f\"The asset is not loaded: {container['objectName']}\"\n        )\n\n        transform = asset_group.matrix_basis.copy()\n        old_data = dict(asset_group.get(AVALON_PROPERTY))\n        old_members = old_data.get(\"members\", [])\n        parent = asset_group.parent\n\n        actions = {}\n        objects_with_anim = [\n            obj for obj in asset_group.children_recursive\n            if obj.animation_data]\n        for obj in objects_with_anim:\n            # Check if the object has an action and, if so, add it to a dict\n            # so we can restore it later. Save and restore the action only\n            # if it wasn't originally loaded from the current asset.\n            if obj.animation_data.action not in old_members:\n                actions[obj.name] = obj.animation_data.action\n\n        self.exec_remove(container)\n\n        asset_group, members = self._process_data(libpath, group_name)\n\n        avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)\n        avalon_container.objects.link(asset_group)\n\n        asset_group.matrix_basis = transform\n        asset_group.parent = parent\n\n        # Restore the actions\n        for obj in asset_group.children_recursive:\n            if obj.name in actions:\n                if not obj.animation_data:\n                    obj.animation_data_create()\n                obj.animation_data.action = actions[obj.name]\n\n        # Restore the old data, but reset memebers, as they don't exist anymore\n        # This avoids a crash, because the memory addresses of those members\n        # are not valid anymore\n        old_data[\"members\"] = []\n        asset_group[AVALON_PROPERTY] = old_data\n\n        new_data = {\n            \"libpath\": libpath,\n            \"representation\": str(representation[\"_id\"]),\n            \"parent\": str(representation[\"parent\"]),\n            \"members\": members,\n        }\n\n        imprint(asset_group, new_data)\n\n        # We need to update all the parent container members\n        parent_containers = self.get_all_container_parents(asset_group)\n\n        for parent_container in parent_containers:\n            parent_members = parent_container[AVALON_PROPERTY][\"members\"]\n            parent_container[AVALON_PROPERTY][\"members\"] = (\n                parent_members + members)\n\n    def exec_remove(self, container: Dict) -> bool:\n        \"\"\"\n        Remove an existing container from a Blender scene.\n        \"\"\"\n        group_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(group_name)\n\n        attrs = [\n            attr for attr in dir(bpy.data)\n            if isinstance(\n                getattr(bpy.data, attr),\n                bpy.types.bpy_prop_collection\n            )\n        ]\n\n        members = asset_group.get(AVALON_PROPERTY).get(\"members\", [])\n\n        # We need to update all the parent container members\n        parent_containers = self.get_all_container_parents(asset_group)\n\n        for parent in parent_containers:\n            parent.get(AVALON_PROPERTY)[\"members\"] = list(filter(\n                lambda i: i not in members,\n                parent.get(AVALON_PROPERTY).get(\"members\", [])))\n\n        for attr in attrs:\n            for data in getattr(bpy.data, attr):\n                if data in members:\n                    # Skip the asset group\n                    if data == asset_group:\n                        continue\n                    getattr(bpy.data, attr).remove(data)\n\n        bpy.data.objects.remove(asset_group)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/load/load_blendscene.py",
    "content": "from typing import Dict, List, Optional\nfrom pathlib import Path\n\nimport bpy\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.hosts.blender.api import plugin\nfrom openpype.hosts.blender.api.lib import imprint\nfrom openpype.hosts.blender.api.pipeline import (\n    AVALON_CONTAINERS,\n    AVALON_PROPERTY,\n)\n\n\nclass BlendSceneLoader(plugin.AssetLoader):\n    \"\"\"Load assets from a .blend file.\"\"\"\n\n    families = [\"blendScene\"]\n    representations = [\"blend\"]\n\n    label = \"Append Blend\"\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    @staticmethod\n    def _get_asset_container(collections):\n        for coll in collections:\n            parents = [c for c in collections if c.user_of_id(coll)]\n            if coll.get(AVALON_PROPERTY) and not parents:\n                return coll\n\n        return None\n\n    def _process_data(self, libpath, group_name, family):\n        # Append all the data from the .blend file\n        with bpy.data.libraries.load(\n            libpath, link=False, relative=False\n        ) as (data_from, data_to):\n            for attr in dir(data_to):\n                setattr(data_to, attr, getattr(data_from, attr))\n\n        members = []\n\n        # Rename the object to add the asset name\n        for attr in dir(data_to):\n            for data in getattr(data_to, attr):\n                data.name = f\"{group_name}:{data.name}\"\n                members.append(data)\n\n        container = self._get_asset_container(\n            data_to.collections)\n        assert container, \"No asset group found\"\n\n        container.name = group_name\n\n        # Link the group to the scene\n        bpy.context.scene.collection.children.link(container)\n\n        # Remove the library from the blend file\n        filepath = bpy.path.basename(libpath)\n        # Blender has a limit of 63 characters for any data name.\n        # If the filepath is longer, it will be truncated.\n        if len(filepath) > 63:\n            filepath = filepath[:63]\n        library = bpy.data.libraries.get(filepath)\n        bpy.data.libraries.remove(library)\n\n        return container, members\n\n    def process_asset(\n        self, context: dict, name: str, namespace: Optional[str] = None,\n        options: Optional[Dict] = None\n    ) -> Optional[List]:\n        \"\"\"\n        Arguments:\n            name: Use pre-defined name\n            namespace: Use pre-defined namespace\n            context: Full parenthood of representation to load\n            options: Additional settings dictionary\n        \"\"\"\n        libpath = self.filepath_from_context(context)\n        asset = context[\"asset\"][\"name\"]\n        subset = context[\"subset\"][\"name\"]\n\n        try:\n            family = context[\"representation\"][\"context\"][\"family\"]\n        except ValueError:\n            family = \"model\"\n\n        asset_name = plugin.prepare_scene_name(asset, subset)\n        unique_number = plugin.get_unique_number(asset, subset)\n        group_name = plugin.prepare_scene_name(asset, subset, unique_number)\n        namespace = namespace or f\"{asset}_{unique_number}\"\n\n        avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)\n        if not avalon_container:\n            avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)\n            bpy.context.scene.collection.children.link(avalon_container)\n\n        container, members = self._process_data(libpath, group_name, family)\n\n        avalon_container.children.link(container)\n\n        data = {\n            \"schema\": \"openpype:container-2.0\",\n            \"id\": AVALON_CONTAINER_ID,\n            \"name\": name,\n            \"namespace\": namespace or '',\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": str(context[\"representation\"][\"_id\"]),\n            \"libpath\": libpath,\n            \"asset_name\": asset_name,\n            \"parent\": str(context[\"representation\"][\"parent\"]),\n            \"family\": context[\"representation\"][\"context\"][\"family\"],\n            \"objectName\": group_name,\n            \"members\": members,\n        }\n\n        container[AVALON_PROPERTY] = data\n\n        objects = [\n            obj for obj in bpy.data.objects\n            if obj.name.startswith(f\"{group_name}:\")\n        ]\n\n        self[:] = objects\n        return objects\n\n    def exec_update(self, container: Dict, representation: Dict):\n        \"\"\"\n        Update the loaded asset.\n        \"\"\"\n        group_name = container[\"objectName\"]\n        asset_group = bpy.data.collections.get(group_name)\n        libpath = Path(get_representation_path(representation)).as_posix()\n\n        assert asset_group, (\n            f\"The asset is not loaded: {container['objectName']}\"\n        )\n\n        # Get the parents of the members of the asset group, so we can\n        # re-link them after the update.\n        # Also gets the transform for each object to reapply after the update.\n        collection_parents = {}\n        member_transforms = {}\n        members = asset_group.get(AVALON_PROPERTY).get(\"members\", [])\n        loaded_collections = {c for c in bpy.data.collections if c in members}\n        loaded_collections.add(bpy.data.collections.get(AVALON_CONTAINERS))\n        for member in members:\n            if isinstance(member, bpy.types.Object):\n                member_parents = set(member.users_collection)\n                member_transforms[member.name] = member.matrix_basis.copy()\n            elif isinstance(member, bpy.types.Collection):\n                member_parents = {\n                    c for c in bpy.data.collections if c.user_of_id(member)}\n            else:\n                continue\n\n            member_parents = member_parents.difference(loaded_collections)\n            if member_parents:\n                collection_parents[member.name] = list(member_parents)\n\n        old_data = dict(asset_group.get(AVALON_PROPERTY))\n\n        self.exec_remove(container)\n\n        family = container[\"family\"]\n        asset_group, members = self._process_data(libpath, group_name, family)\n\n        for member in members:\n            if member.name in collection_parents:\n                for parent in collection_parents[member.name]:\n                    if isinstance(member, bpy.types.Object):\n                        parent.objects.link(member)\n                    elif isinstance(member, bpy.types.Collection):\n                        parent.children.link(member)\n            if member.name in member_transforms and isinstance(\n                member, bpy.types.Object\n            ):\n                member.matrix_basis = member_transforms[member.name]\n\n        avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)\n        avalon_container.children.link(asset_group)\n\n        # Restore the old data, but reset members, as they don't exist anymore\n        # This avoids a crash, because the memory addresses of those members\n        # are not valid anymore\n        old_data[\"members\"] = []\n        asset_group[AVALON_PROPERTY] = old_data\n\n        new_data = {\n            \"libpath\": libpath,\n            \"representation\": str(representation[\"_id\"]),\n            \"parent\": str(representation[\"parent\"]),\n            \"members\": members,\n        }\n\n        imprint(asset_group, new_data)\n\n    def exec_remove(self, container: Dict) -> bool:\n        \"\"\"\n        Remove an existing container from a Blender scene.\n        \"\"\"\n        group_name = container[\"objectName\"]\n        asset_group = bpy.data.collections.get(group_name)\n\n        members = set(asset_group.get(AVALON_PROPERTY).get(\"members\", []))\n\n        if members:\n            for attr_name in dir(bpy.data):\n                attr = getattr(bpy.data, attr_name)\n                if not isinstance(attr, bpy.types.bpy_prop_collection):\n                    continue\n\n                # ensure to make a list copy because we\n                # we remove members as we iterate\n                for data in list(attr):\n                    if data not in members or data == asset_group:\n                        continue\n\n                    attr.remove(data)\n\n        bpy.data.collections.remove(asset_group)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/load/load_camera_abc.py",
    "content": "\"\"\"Load an asset in Blender from an Alembic file.\"\"\"\n\nfrom pathlib import Path\nfrom pprint import pformat\nfrom typing import Dict, List, Optional\n\nimport bpy\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.hosts.blender.api import plugin, lib\nfrom openpype.hosts.blender.api.pipeline import (\n    AVALON_CONTAINERS,\n    AVALON_PROPERTY,\n)\n\n\nclass AbcCameraLoader(plugin.AssetLoader):\n    \"\"\"Load a camera from Alembic file.\n\n    Stores the imported asset in an empty named after the asset.\n    \"\"\"\n\n    families = [\"camera\"]\n    representations = [\"abc\"]\n\n    label = \"Load Camera (ABC)\"\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def _remove(self, asset_group):\n        objects = list(asset_group.children)\n\n        for obj in objects:\n            if obj.type == \"CAMERA\":\n                bpy.data.cameras.remove(obj.data)\n            elif obj.type == \"EMPTY\":\n                objects.extend(obj.children)\n                bpy.data.objects.remove(obj)\n\n    def _process(self, libpath, asset_group, group_name):\n        plugin.deselect_all()\n\n        bpy.ops.wm.alembic_import(filepath=libpath)\n\n        objects = lib.get_selection()\n\n        for obj in objects:\n            obj.parent = asset_group\n\n        for obj in objects:\n            name = obj.name\n            obj.name = f\"{group_name}:{name}\"\n            if obj.type != \"EMPTY\":\n                name_data = obj.data.name\n                obj.data.name = f\"{group_name}:{name_data}\"\n\n            if not obj.get(AVALON_PROPERTY):\n                obj[AVALON_PROPERTY] = dict()\n\n            avalon_info = obj[AVALON_PROPERTY]\n            avalon_info.update({\"container_name\": group_name})\n\n        plugin.deselect_all()\n\n        return objects\n\n    def process_asset(\n        self,\n        context: dict,\n        name: str,\n        namespace: Optional[str] = None,\n        options: Optional[Dict] = None,\n    ) -> Optional[List]:\n        \"\"\"\n        Arguments:\n            name: Use pre-defined name\n            namespace: Use pre-defined namespace\n            context: Full parenthood of representation to load\n            options: Additional settings dictionary\n        \"\"\"\n\n        libpath = self.filepath_from_context(context)\n\n        asset = context[\"asset\"][\"name\"]\n        subset = context[\"subset\"][\"name\"]\n\n        asset_name = plugin.prepare_scene_name(asset, subset)\n        unique_number = plugin.get_unique_number(asset, subset)\n        group_name = plugin.prepare_scene_name(asset, subset, unique_number)\n        namespace = namespace or f\"{asset}_{unique_number}\"\n\n        avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)\n        if not avalon_container:\n            avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)\n            bpy.context.scene.collection.children.link(avalon_container)\n\n        asset_group = bpy.data.objects.new(group_name, object_data=None)\n        avalon_container.objects.link(asset_group)\n\n        self._process(libpath, asset_group, group_name)\n\n        objects = []\n        nodes = list(asset_group.children)\n\n        for obj in nodes:\n            objects.append(obj)\n            nodes.extend(list(obj.children))\n\n        bpy.context.scene.collection.objects.link(asset_group)\n\n        asset_group[AVALON_PROPERTY] = {\n            \"schema\": \"openpype:container-2.0\",\n            \"id\": AVALON_CONTAINER_ID,\n            \"name\": name,\n            \"namespace\": namespace or \"\",\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": str(context[\"representation\"][\"_id\"]),\n            \"libpath\": libpath,\n            \"asset_name\": asset_name,\n            \"parent\": str(context[\"representation\"][\"parent\"]),\n            \"family\": context[\"representation\"][\"context\"][\"family\"],\n            \"objectName\": group_name,\n        }\n\n        self[:] = objects\n        return objects\n\n    def exec_update(self, container: Dict, representation: Dict):\n        \"\"\"Update the loaded asset.\n\n        This will remove all objects of the current collection, load the new\n        ones and add them to the collection.\n        If the objects of the collection are used in another collection they\n        will not be removed, only unlinked. Normally this should not be the\n        case though.\n\n        Warning:\n            No nested collections are supported at the moment!\n        \"\"\"\n        object_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(object_name)\n        libpath = Path(get_representation_path(representation))\n        extension = libpath.suffix.lower()\n\n        self.log.info(\n            \"Container: %s\\nRepresentation: %s\",\n            pformat(container, indent=2),\n            pformat(representation, indent=2),\n        )\n\n        assert asset_group, (\n            f\"The asset is not loaded: {container['objectName']}\")\n        assert libpath, (\n            f\"No existing library file found for {container['objectName']}\")\n        assert libpath.is_file(), f\"The file doesn't exist: {libpath}\"\n        assert extension in plugin.VALID_EXTENSIONS, (\n            f\"Unsupported file: {libpath}\")\n\n        metadata = asset_group.get(AVALON_PROPERTY)\n        group_libpath = metadata[\"libpath\"]\n\n        normalized_group_libpath = str(\n            Path(bpy.path.abspath(group_libpath)).resolve())\n        normalized_libpath = str(\n            Path(bpy.path.abspath(str(libpath))).resolve())\n        self.log.debug(\n            \"normalized_group_libpath:\\n  %s\\nnormalized_libpath:\\n  %s\",\n            normalized_group_libpath,\n            normalized_libpath,\n        )\n        if normalized_group_libpath == normalized_libpath:\n            self.log.info(\"Library already loaded, not updating...\")\n            return\n\n        mat = asset_group.matrix_basis.copy()\n\n        self._remove(asset_group)\n        self._process(str(libpath), asset_group, object_name)\n\n        asset_group.matrix_basis = mat\n\n        metadata[\"libpath\"] = str(libpath)\n        metadata[\"representation\"] = str(representation[\"_id\"])\n\n    def exec_remove(self, container: Dict) -> bool:\n        \"\"\"Remove an existing container from a Blender scene.\n\n        Arguments:\n            container (openpype:container-1.0): Container to remove,\n                from `host.ls()`.\n\n        Returns:\n            bool: Whether the container was deleted.\n\n        Warning:\n            No nested collections are supported at the moment!\n        \"\"\"\n        object_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(object_name)\n\n        if not asset_group:\n            return False\n\n        self._remove(asset_group)\n\n        bpy.data.objects.remove(asset_group)\n\n        return True\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/load/load_camera_fbx.py",
    "content": "\"\"\"Load an asset in Blender from an Alembic file.\"\"\"\n\nfrom pathlib import Path\nfrom pprint import pformat\nfrom typing import Dict, List, Optional\n\nimport bpy\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.hosts.blender.api import plugin, lib\nfrom openpype.hosts.blender.api.pipeline import (\n    AVALON_CONTAINERS,\n    AVALON_PROPERTY,\n)\n\n\nclass FbxCameraLoader(plugin.AssetLoader):\n    \"\"\"Load a camera from FBX.\n\n    Stores the imported asset in an empty named after the asset.\n    \"\"\"\n\n    families = [\"camera\"]\n    representations = [\"fbx\"]\n\n    label = \"Load Camera (FBX)\"\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def _remove(self, asset_group):\n        objects = list(asset_group.children)\n\n        for obj in objects:\n            if obj.type == 'CAMERA':\n                bpy.data.cameras.remove(obj.data)\n            elif obj.type == 'EMPTY':\n                objects.extend(obj.children)\n                bpy.data.objects.remove(obj)\n\n    def _process(self, libpath, asset_group, group_name):\n        plugin.deselect_all()\n\n        collection = bpy.context.view_layer.active_layer_collection.collection\n\n        bpy.ops.import_scene.fbx(filepath=libpath)\n\n        parent = bpy.context.scene.collection\n\n        objects = lib.get_selection()\n\n        for obj in objects:\n            obj.parent = asset_group\n\n        for obj in objects:\n            parent.objects.link(obj)\n            collection.objects.unlink(obj)\n\n        for obj in objects:\n            name = obj.name\n            obj.name = f\"{group_name}:{name}\"\n            if obj.type != 'EMPTY':\n                name_data = obj.data.name\n                obj.data.name = f\"{group_name}:{name_data}\"\n\n            if not obj.get(AVALON_PROPERTY):\n                obj[AVALON_PROPERTY] = dict()\n\n            avalon_info = obj[AVALON_PROPERTY]\n            avalon_info.update({\"container_name\": group_name})\n\n        plugin.deselect_all()\n\n        return objects\n\n    def process_asset(\n        self, context: dict, name: str, namespace: Optional[str] = None,\n        options: Optional[Dict] = None\n    ) -> Optional[List]:\n        \"\"\"\n        Arguments:\n            name: Use pre-defined name\n            namespace: Use pre-defined namespace\n            context: Full parenthood of representation to load\n            options: Additional settings dictionary\n        \"\"\"\n        libpath = self.filepath_from_context(context)\n        asset = context[\"asset\"][\"name\"]\n        subset = context[\"subset\"][\"name\"]\n\n        asset_name = plugin.prepare_scene_name(asset, subset)\n        unique_number = plugin.get_unique_number(asset, subset)\n        group_name = plugin.prepare_scene_name(asset, subset, unique_number)\n        namespace = namespace or f\"{asset}_{unique_number}\"\n\n        avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)\n        if not avalon_container:\n            avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)\n            bpy.context.scene.collection.children.link(avalon_container)\n\n        asset_group = bpy.data.objects.new(group_name, object_data=None)\n        avalon_container.objects.link(asset_group)\n\n        self._process(libpath, asset_group, group_name)\n\n        objects = []\n        nodes = list(asset_group.children)\n\n        for obj in nodes:\n            objects.append(obj)\n            nodes.extend(list(obj.children))\n\n        bpy.context.scene.collection.objects.link(asset_group)\n\n        asset_group[AVALON_PROPERTY] = {\n            \"schema\": \"openpype:container-2.0\",\n            \"id\": AVALON_CONTAINER_ID,\n            \"name\": name,\n            \"namespace\": namespace or '',\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": str(context[\"representation\"][\"_id\"]),\n            \"libpath\": libpath,\n            \"asset_name\": asset_name,\n            \"parent\": str(context[\"representation\"][\"parent\"]),\n            \"family\": context[\"representation\"][\"context\"][\"family\"],\n            \"objectName\": group_name\n        }\n\n        self[:] = objects\n        return objects\n\n    def exec_update(self, container: Dict, representation: Dict):\n        \"\"\"Update the loaded asset.\n\n        This will remove all objects of the current collection, load the new\n        ones and add them to the collection.\n        If the objects of the collection are used in another collection they\n        will not be removed, only unlinked. Normally this should not be the\n        case though.\n\n        Warning:\n            No nested collections are supported at the moment!\n        \"\"\"\n        object_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(object_name)\n        libpath = Path(get_representation_path(representation))\n        extension = libpath.suffix.lower()\n\n        self.log.info(\n            \"Container: %s\\nRepresentation: %s\",\n            pformat(container, indent=2),\n            pformat(representation, indent=2),\n        )\n\n        assert asset_group, (\n            f\"The asset is not loaded: {container['objectName']}\"\n        )\n        assert libpath, (\n            \"No existing library file found for {container['objectName']}\"\n        )\n        assert libpath.is_file(), (\n            f\"The file doesn't exist: {libpath}\"\n        )\n        assert extension in plugin.VALID_EXTENSIONS, (\n            f\"Unsupported file: {libpath}\"\n        )\n\n        metadata = asset_group.get(AVALON_PROPERTY)\n        group_libpath = metadata[\"libpath\"]\n\n        normalized_group_libpath = (\n            str(Path(bpy.path.abspath(group_libpath)).resolve())\n        )\n        normalized_libpath = (\n            str(Path(bpy.path.abspath(str(libpath))).resolve())\n        )\n        self.log.debug(\n            \"normalized_group_libpath:\\n  %s\\nnormalized_libpath:\\n  %s\",\n            normalized_group_libpath,\n            normalized_libpath,\n        )\n        if normalized_group_libpath == normalized_libpath:\n            self.log.info(\"Library already loaded, not updating...\")\n            return\n\n        mat = asset_group.matrix_basis.copy()\n\n        self._remove(asset_group)\n        self._process(str(libpath), asset_group, object_name)\n\n        asset_group.matrix_basis = mat\n\n        metadata[\"libpath\"] = str(libpath)\n        metadata[\"representation\"] = str(representation[\"_id\"])\n\n    def exec_remove(self, container: Dict) -> bool:\n        \"\"\"Remove an existing container from a Blender scene.\n\n        Arguments:\n            container (openpype:container-1.0): Container to remove,\n                from `host.ls()`.\n\n        Returns:\n            bool: Whether the container was deleted.\n\n        Warning:\n            No nested collections are supported at the moment!\n        \"\"\"\n        object_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(object_name)\n\n        if not asset_group:\n            return False\n\n        self._remove(asset_group)\n\n        bpy.data.objects.remove(asset_group)\n\n        return True\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/load/load_fbx.py",
    "content": "\"\"\"Load an asset in Blender from an Alembic file.\"\"\"\n\nfrom pathlib import Path\nfrom pprint import pformat\nfrom typing import Dict, List, Optional\n\nimport bpy\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.hosts.blender.api import plugin, lib\nfrom openpype.hosts.blender.api.pipeline import (\n    AVALON_CONTAINERS,\n    AVALON_PROPERTY,\n)\n\n\nclass FbxModelLoader(plugin.AssetLoader):\n    \"\"\"Load FBX models.\n\n    Stores the imported asset in an empty named after the asset.\n    \"\"\"\n\n    families = [\"model\", \"rig\"]\n    representations = [\"fbx\"]\n\n    label = \"Load FBX\"\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def _remove(self, asset_group):\n        objects = list(asset_group.children)\n\n        for obj in objects:\n            if obj.type == 'MESH':\n                for material_slot in list(obj.material_slots):\n                    if material_slot.material:\n                        bpy.data.materials.remove(material_slot.material)\n                bpy.data.meshes.remove(obj.data)\n            elif obj.type == 'ARMATURE':\n                objects.extend(obj.children)\n                bpy.data.armatures.remove(obj.data)\n            elif obj.type == 'CURVE':\n                bpy.data.curves.remove(obj.data)\n            elif obj.type == 'EMPTY':\n                objects.extend(obj.children)\n                bpy.data.objects.remove(obj)\n\n    def _process(self, libpath, asset_group, group_name, action):\n        plugin.deselect_all()\n\n        collection = bpy.context.view_layer.active_layer_collection.collection\n\n        bpy.ops.import_scene.fbx(filepath=libpath)\n\n        parent = bpy.context.scene.collection\n\n        imported = lib.get_selection()\n\n        empties = [obj for obj in imported if obj.type == 'EMPTY']\n\n        container = None\n\n        for empty in empties:\n            if not empty.parent:\n                container = empty\n                break\n\n        assert container, \"No asset group found\"\n\n        # Children must be linked before parents,\n        # otherwise the hierarchy will break\n        objects = []\n        nodes = list(container.children)\n\n        for obj in nodes:\n            obj.parent = asset_group\n\n        bpy.data.objects.remove(container)\n\n        for obj in nodes:\n            objects.append(obj)\n            nodes.extend(list(obj.children))\n\n        objects.reverse()\n\n        for obj in objects:\n            parent.objects.link(obj)\n            collection.objects.unlink(obj)\n\n        for obj in objects:\n            name = obj.name\n            obj.name = f\"{group_name}:{name}\"\n            if obj.type != 'EMPTY':\n                name_data = obj.data.name\n                obj.data.name = f\"{group_name}:{name_data}\"\n\n            if obj.type == 'MESH':\n                for material_slot in obj.material_slots:\n                    name_mat = material_slot.material.name\n                    material_slot.material.name = f\"{group_name}:{name_mat}\"\n            elif obj.type == 'ARMATURE':\n                anim_data = obj.animation_data\n                if action is not None:\n                    anim_data.action = action\n                elif anim_data.action is not None:\n                    name_action = anim_data.action.name\n                    anim_data.action.name = f\"{group_name}:{name_action}\"\n\n            if not obj.get(AVALON_PROPERTY):\n                obj[AVALON_PROPERTY] = dict()\n\n            avalon_info = obj[AVALON_PROPERTY]\n            avalon_info.update({\"container_name\": group_name})\n\n        plugin.deselect_all()\n\n        return objects\n\n    def process_asset(\n        self, context: dict, name: str, namespace: Optional[str] = None,\n        options: Optional[Dict] = None\n    ) -> Optional[List]:\n        \"\"\"\n        Arguments:\n            name: Use pre-defined name\n            namespace: Use pre-defined namespace\n            context: Full parenthood of representation to load\n            options: Additional settings dictionary\n        \"\"\"\n        libpath = self.filepath_from_context(context)\n        asset = context[\"asset\"][\"name\"]\n        subset = context[\"subset\"][\"name\"]\n\n        asset_name = plugin.prepare_scene_name(asset, subset)\n        unique_number = plugin.get_unique_number(asset, subset)\n        group_name = plugin.prepare_scene_name(asset, subset, unique_number)\n        namespace = namespace or f\"{asset}_{unique_number}\"\n\n        avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)\n        if not avalon_container:\n            avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)\n            bpy.context.scene.collection.children.link(avalon_container)\n\n        asset_group = bpy.data.objects.new(group_name, object_data=None)\n        avalon_container.objects.link(asset_group)\n\n        objects = self._process(libpath, asset_group, group_name, None)\n\n        objects = []\n        nodes = list(asset_group.children)\n\n        for obj in nodes:\n            objects.append(obj)\n            nodes.extend(list(obj.children))\n\n        bpy.context.scene.collection.objects.link(asset_group)\n\n        asset_group[AVALON_PROPERTY] = {\n            \"schema\": \"openpype:container-2.0\",\n            \"id\": AVALON_CONTAINER_ID,\n            \"name\": name,\n            \"namespace\": namespace or '',\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": str(context[\"representation\"][\"_id\"]),\n            \"libpath\": libpath,\n            \"asset_name\": asset_name,\n            \"parent\": str(context[\"representation\"][\"parent\"]),\n            \"family\": context[\"representation\"][\"context\"][\"family\"],\n            \"objectName\": group_name\n        }\n\n        self[:] = objects\n        return objects\n\n    def exec_update(self, container: Dict, representation: Dict):\n        \"\"\"Update the loaded asset.\n\n        This will remove all objects of the current collection, load the new\n        ones and add them to the collection.\n        If the objects of the collection are used in another collection they\n        will not be removed, only unlinked. Normally this should not be the\n        case though.\n\n        Warning:\n            No nested collections are supported at the moment!\n        \"\"\"\n        object_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(object_name)\n        libpath = Path(get_representation_path(representation))\n        extension = libpath.suffix.lower()\n\n        self.log.info(\n            \"Container: %s\\nRepresentation: %s\",\n            pformat(container, indent=2),\n            pformat(representation, indent=2),\n        )\n\n        assert asset_group, (\n            f\"The asset is not loaded: {container['objectName']}\"\n        )\n        assert libpath, (\n            \"No existing library file found for {container['objectName']}\"\n        )\n        assert libpath.is_file(), (\n            f\"The file doesn't exist: {libpath}\"\n        )\n        assert extension in plugin.VALID_EXTENSIONS, (\n            f\"Unsupported file: {libpath}\"\n        )\n\n        metadata = asset_group.get(AVALON_PROPERTY)\n        group_libpath = metadata[\"libpath\"]\n\n        normalized_group_libpath = (\n            str(Path(bpy.path.abspath(group_libpath)).resolve())\n        )\n        normalized_libpath = (\n            str(Path(bpy.path.abspath(str(libpath))).resolve())\n        )\n        self.log.debug(\n            \"normalized_group_libpath:\\n  %s\\nnormalized_libpath:\\n  %s\",\n            normalized_group_libpath,\n            normalized_libpath,\n        )\n        if normalized_group_libpath == normalized_libpath:\n            self.log.info(\"Library already loaded, not updating...\")\n            return\n\n        # Get the armature of the rig\n        objects = asset_group.children\n        armatures = [obj for obj in objects if obj.type == 'ARMATURE']\n        action = None\n\n        if armatures:\n            armature = armatures[0]\n\n            if armature.animation_data and armature.animation_data.action:\n                action = armature.animation_data.action\n\n        mat = asset_group.matrix_basis.copy()\n        self._remove(asset_group)\n\n        self._process(str(libpath), asset_group, object_name, action)\n\n        asset_group.matrix_basis = mat\n\n        metadata[\"libpath\"] = str(libpath)\n        metadata[\"representation\"] = str(representation[\"_id\"])\n\n    def exec_remove(self, container: Dict) -> bool:\n        \"\"\"Remove an existing container from a Blender scene.\n\n        Arguments:\n            container (openpype:container-1.0): Container to remove,\n                from `host.ls()`.\n\n        Returns:\n            bool: Whether the container was deleted.\n\n        Warning:\n            No nested collections are supported at the moment!\n        \"\"\"\n        object_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(object_name)\n\n        if not asset_group:\n            return False\n\n        self._remove(asset_group)\n\n        bpy.data.objects.remove(asset_group)\n\n        return True\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/load/load_layout_json.py",
    "content": "\"\"\"Load a layout in Blender.\"\"\"\n\nimport json\nfrom pathlib import Path\nfrom pprint import pformat\nfrom typing import Dict, Optional\n\nimport bpy\n\nfrom openpype.pipeline import (\n    discover_loader_plugins,\n    remove_container,\n    load_container,\n    get_representation_path,\n    loaders_from_representation,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.hosts.blender.api.pipeline import (\n    AVALON_INSTANCES,\n    AVALON_CONTAINERS,\n    AVALON_PROPERTY,\n)\nfrom openpype.hosts.blender.api import plugin\n\n\nclass JsonLayoutLoader(plugin.AssetLoader):\n    \"\"\"Load layout published from Unreal.\"\"\"\n\n    families = [\"layout\"]\n    representations = [\"json\"]\n\n    label = \"Load Layout\"\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    animation_creator_name = \"CreateAnimation\"\n\n    def _remove(self, asset_group):\n        objects = list(asset_group.children)\n\n        for obj in objects:\n            remove_container(obj.get(AVALON_PROPERTY))\n\n    def _remove_animation_instances(self, asset_group):\n        instances = bpy.data.collections.get(AVALON_INSTANCES)\n        if instances:\n            for obj in list(asset_group.children):\n                anim_collection = instances.children.get(\n                    obj.name + \"_animation\")\n                if anim_collection:\n                    bpy.data.collections.remove(anim_collection)\n\n    def _get_loader(self, loaders, family):\n        name = \"\"\n        if family == 'rig':\n            name = \"BlendRigLoader\"\n        elif family == 'model':\n            name = \"BlendModelLoader\"\n\n        if name == \"\":\n            return None\n\n        for loader in loaders:\n            if loader.__name__ == name:\n                return loader\n\n        return None\n\n    def _process(self, libpath, asset, asset_group, actions):\n        plugin.deselect_all()\n\n        with open(libpath, \"r\") as fp:\n            data = json.load(fp)\n\n        all_loaders = discover_loader_plugins()\n\n        for element in data:\n            reference = element.get('reference')\n            family = element.get('family')\n\n            loaders = loaders_from_representation(all_loaders, reference)\n            loader = self._get_loader(loaders, family)\n\n            if not loader:\n                continue\n\n            instance_name = element.get('instance_name')\n\n            action = None\n\n            if actions:\n                action = actions.get(instance_name, None)\n\n            options = {\n                'parent': asset_group,\n                'transform': element.get('transform'),\n                'action': action,\n                'create_animation': True if family == 'rig' else False,\n                'animation_asset': asset\n            }\n\n            if element.get('animation'):\n                options['animation_file'] = str(Path(libpath).with_suffix(\n                    '')) + \".\" + element.get('animation')\n\n            # This should return the loaded asset, but the load call will be\n            # added to the queue to run in the Blender main thread, so\n            # at this time it will not return anything. The assets will be\n            # loaded in the next Blender cycle, so we use the options to\n            # set the transform, parent and assign the action, if there is one.\n            load_container(\n                loader,\n                reference,\n                namespace=instance_name,\n                options=options\n            )\n\n        # Camera creation when loading a layout is not necessary for now,\n        # but the code is worth keeping in case we need it in the future.\n        # # Create the camera asset and the camera instance\n        # creator_plugin = get_legacy_creator_by_name(\"CreateCamera\")\n        # if not creator_plugin:\n        #     raise ValueError(\"Creator plugin \\\"CreateCamera\\\" was \"\n        #                      \"not found.\")\n\n        # TODO: Refactor legacy create usage to new style creators\n        # legacy_create(\n        #     creator_plugin,\n        #     name=\"camera\",\n        #     # name=f\"{unique_number}_{subset}_animation\",\n        #     asset=asset,\n        #     options={\"useSelection\": False}\n        #     # data={\"dependencies\": str(context[\"representation\"][\"_id\"])}\n        # )\n\n    def process_asset(self,\n                      context: dict,\n                      name: str,\n                      namespace: Optional[str] = None,\n                      options: Optional[Dict] = None):\n        \"\"\"\n        Arguments:\n            name: Use pre-defined name\n            namespace: Use pre-defined namespace\n            context: Full parenthood of representation to load\n            options: Additional settings dictionary\n        \"\"\"\n        libpath = self.filepath_from_context(context)\n        asset = context[\"asset\"][\"name\"]\n        subset = context[\"subset\"][\"name\"]\n\n        asset_name = plugin.prepare_scene_name(asset, subset)\n        unique_number = plugin.get_unique_number(asset, subset)\n        group_name = plugin.prepare_scene_name(asset, subset, unique_number)\n        namespace = namespace or f\"{asset}_{unique_number}\"\n\n        avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)\n        if not avalon_container:\n            avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)\n            bpy.context.scene.collection.children.link(avalon_container)\n\n        asset_group = bpy.data.objects.new(group_name, object_data=None)\n        asset_group.empty_display_type = 'SINGLE_ARROW'\n        avalon_container.objects.link(asset_group)\n\n        self._process(libpath, asset, asset_group, None)\n\n        bpy.context.scene.collection.objects.link(asset_group)\n\n        asset_group[AVALON_PROPERTY] = {\n            \"schema\": \"openpype:container-2.0\",\n            \"id\": AVALON_CONTAINER_ID,\n            \"name\": name,\n            \"namespace\": namespace or '',\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": str(context[\"representation\"][\"_id\"]),\n            \"libpath\": libpath,\n            \"asset_name\": asset_name,\n            \"parent\": str(context[\"representation\"][\"parent\"]),\n            \"family\": context[\"representation\"][\"context\"][\"family\"],\n            \"objectName\": group_name\n        }\n\n        self[:] = asset_group.children\n        return asset_group.children\n\n    def exec_update(self, container: Dict, representation: Dict):\n        \"\"\"Update the loaded asset.\n\n        This will remove all objects of the current collection, load the new\n        ones and add them to the collection.\n        If the objects of the collection are used in another collection they\n        will not be removed, only unlinked. Normally this should not be the\n        case though.\n        \"\"\"\n        object_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(object_name)\n        libpath = Path(get_representation_path(representation))\n        extension = libpath.suffix.lower()\n\n        self.log.info(\n            \"Container: %s\\nRepresentation: %s\",\n            pformat(container, indent=2),\n            pformat(representation, indent=2),\n        )\n\n        assert asset_group, (\n            f\"The asset is not loaded: {container['objectName']}\"\n        )\n        assert libpath, (\n            \"No existing library file found for {container['objectName']}\"\n        )\n        assert libpath.is_file(), (\n            f\"The file doesn't exist: {libpath}\"\n        )\n        assert extension in plugin.VALID_EXTENSIONS, (\n            f\"Unsupported file: {libpath}\"\n        )\n\n        metadata = asset_group.get(AVALON_PROPERTY)\n        group_libpath = metadata[\"libpath\"]\n\n        normalized_group_libpath = (\n            str(Path(bpy.path.abspath(group_libpath)).resolve())\n        )\n        normalized_libpath = (\n            str(Path(bpy.path.abspath(str(libpath))).resolve())\n        )\n        self.log.debug(\n            \"normalized_group_libpath:\\n  %s\\nnormalized_libpath:\\n  %s\",\n            normalized_group_libpath,\n            normalized_libpath,\n        )\n        if normalized_group_libpath == normalized_libpath:\n            self.log.info(\"Library already loaded, not updating...\")\n            return\n\n        actions = {}\n\n        for obj in asset_group.children:\n            obj_meta = obj.get(AVALON_PROPERTY)\n            if obj_meta.get('family') == 'rig':\n                rig = None\n                for child in obj.children:\n                    if child.type == 'ARMATURE':\n                        rig = child\n                        break\n                if not rig:\n                    raise Exception(\"No armature in the rig asset group.\")\n                if rig.animation_data and rig.animation_data.action:\n                    namespace = obj_meta.get('namespace')\n                    actions[namespace] = rig.animation_data.action\n\n        mat = asset_group.matrix_basis.copy()\n\n        self._remove_animation_instances(asset_group)\n\n        self._remove(asset_group)\n\n        self._process(str(libpath), asset_group, actions)\n\n        asset_group.matrix_basis = mat\n\n        metadata[\"libpath\"] = str(libpath)\n        metadata[\"representation\"] = str(representation[\"_id\"])\n\n    def exec_remove(self, container: Dict) -> bool:\n        \"\"\"Remove an existing container from a Blender scene.\n\n        Arguments:\n            container (openpype:container-1.0): Container to remove,\n                from `host.ls()`.\n\n        Returns:\n            bool: Whether the container was deleted.\n        \"\"\"\n        object_name = container[\"objectName\"]\n        asset_group = bpy.data.objects.get(object_name)\n\n        if not asset_group:\n            return False\n\n        self._remove_animation_instances(asset_group)\n\n        self._remove(asset_group)\n\n        bpy.data.objects.remove(asset_group)\n\n        return True\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/load/load_look.py",
    "content": "\"\"\"Load a model asset in Blender.\"\"\"\n\nfrom pathlib import Path\nfrom pprint import pformat\nfrom typing import Dict, List, Optional\n\nimport os\nimport json\nimport bpy\n\nfrom openpype.pipeline import get_representation_path\nfrom openpype.hosts.blender.api import plugin\nfrom openpype.hosts.blender.api.pipeline import (\n    containerise_existing,\n    AVALON_PROPERTY\n)\n\n\nclass BlendLookLoader(plugin.AssetLoader):\n    \"\"\"Load models from a .blend file.\n\n    Because they come from a .blend file we can simply link the collection that\n    contains the model. There is no further need to 'containerise' it.\n    \"\"\"\n\n    families = [\"look\"]\n    representations = [\"json\"]\n\n    label = \"Load Look\"\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def get_all_children(self, obj):\n        children = list(obj.children)\n\n        for child in children:\n            children.extend(child.children)\n\n        return children\n\n    def _process(self, libpath, container_name, objects):\n        with open(libpath, \"r\") as fp:\n            data = json.load(fp)\n\n        path = os.path.dirname(libpath)\n        materials_path = f\"{path}/resources\"\n\n        materials = []\n\n        for entry in data:\n            file = entry.get('fbx_filename')\n            if file is None:\n                continue\n\n            bpy.ops.import_scene.fbx(filepath=f\"{materials_path}/{file}\")\n\n            mesh = [o for o in bpy.context.scene.objects if o.select_get()][0]\n            material = mesh.data.materials[0]\n            material.name = f\"{material.name}:{container_name}\"\n\n            texture_file = entry.get('tga_filename')\n            if texture_file:\n                node_tree = material.node_tree\n                pbsdf = node_tree.nodes['Principled BSDF']\n                base_color = pbsdf.inputs[0]\n                tex_node = base_color.links[0].from_node\n                tex_node.image.filepath = f\"{materials_path}/{texture_file}\"\n\n            materials.append(material)\n\n            for obj in objects:\n                for child in self.get_all_children(obj):\n                    mesh_name = child.name.split(':')[0]\n                    if mesh_name == material.name.split(':')[0]:\n                        child.data.materials.clear()\n                        child.data.materials.append(material)\n                        break\n\n            bpy.data.objects.remove(mesh)\n\n        return materials, objects\n\n    def process_asset(\n        self, context: dict, name: str, namespace: Optional[str] = None,\n        options: Optional[Dict] = None\n    ) -> Optional[List]:\n        \"\"\"\n        Arguments:\n            name: Use pre-defined name\n            namespace: Use pre-defined namespace\n            context: Full parenthood of representation to load\n            options: Additional settings dictionary\n        \"\"\"\n\n        libpath = self.filepath_from_context(context)\n        asset = context[\"asset\"][\"name\"]\n        subset = context[\"subset\"][\"name\"]\n\n        lib_container = plugin.prepare_scene_name(\n            asset, subset\n        )\n        unique_number = plugin.get_unique_number(\n            asset, subset\n        )\n        namespace = namespace or f\"{asset}_{unique_number}\"\n        container_name = plugin.prepare_scene_name(\n            asset, subset, unique_number\n        )\n\n        container = bpy.data.collections.new(lib_container)\n        container.name = container_name\n        containerise_existing(\n            container,\n            name,\n            namespace,\n            context,\n            self.__class__.__name__,\n        )\n\n        metadata = container.get(AVALON_PROPERTY)\n\n        metadata[\"libpath\"] = libpath\n        metadata[\"lib_container\"] = lib_container\n\n        selected = [o for o in bpy.context.scene.objects if o.select_get()]\n\n        materials, objects = self._process(libpath, container_name, selected)\n\n        # Save the list of imported materials in the metadata container\n        metadata[\"objects\"] = objects\n        metadata[\"materials\"] = materials\n\n        metadata[\"parent\"] = str(context[\"representation\"][\"parent\"])\n        metadata[\"family\"] = context[\"representation\"][\"context\"][\"family\"]\n\n        nodes = list(container.objects)\n        nodes.append(container)\n        self[:] = nodes\n        return nodes\n\n    def update(self, container: Dict, representation: Dict):\n        collection = bpy.data.collections.get(container[\"objectName\"])\n        libpath = Path(get_representation_path(representation))\n        extension = libpath.suffix.lower()\n\n        self.log.info(\n            \"Container: %s\\nRepresentation: %s\",\n            pformat(container, indent=2),\n            pformat(representation, indent=2),\n        )\n\n        assert collection, (\n            f\"The asset is not loaded: {container['objectName']}\"\n        )\n        assert not (collection.children), (\n            \"Nested collections are not supported.\"\n        )\n        assert libpath, (\n            \"No existing library file found for {container['objectName']}\"\n        )\n        assert libpath.is_file(), (\n            f\"The file doesn't exist: {libpath}\"\n        )\n        assert extension in plugin.VALID_EXTENSIONS, (\n            f\"Unsupported file: {libpath}\"\n        )\n\n        collection_metadata = collection.get(AVALON_PROPERTY)\n        collection_libpath = collection_metadata[\"libpath\"]\n\n        normalized_collection_libpath = (\n            str(Path(bpy.path.abspath(collection_libpath)).resolve())\n        )\n        normalized_libpath = (\n            str(Path(bpy.path.abspath(str(libpath))).resolve())\n        )\n        self.log.debug(\n            \"normalized_collection_libpath:\\n  %s\\nnormalized_libpath:\\n  %s\",\n            normalized_collection_libpath,\n            normalized_libpath,\n        )\n        if normalized_collection_libpath == normalized_libpath:\n            self.log.info(\"Library already loaded, not updating...\")\n            return\n\n        for obj in collection_metadata['objects']:\n            for child in self.get_all_children(obj):\n                child.data.materials.clear()\n\n        for material in collection_metadata['materials']:\n            bpy.data.materials.remove(material)\n\n        namespace = collection_metadata['namespace']\n        name = collection_metadata['name']\n\n        container_name = f\"{namespace}_{name}\"\n\n        materials, objects = self._process(\n            libpath, container_name, collection_metadata['objects'])\n\n        collection_metadata[\"objects\"] = objects\n        collection_metadata[\"materials\"] = materials\n        collection_metadata[\"libpath\"] = str(libpath)\n        collection_metadata[\"representation\"] = str(representation[\"_id\"])\n\n    def remove(self, container: Dict) -> bool:\n        collection = bpy.data.collections.get(container[\"objectName\"])\n        if not collection:\n            return False\n\n        collection_metadata = collection.get(AVALON_PROPERTY)\n\n        for obj in collection_metadata['objects']:\n            for child in self.get_all_children(obj):\n                child.data.materials.clear()\n\n        for material in collection_metadata['materials']:\n            bpy.data.materials.remove(material)\n\n        bpy.data.collections.remove(collection)\n\n        return True\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/collect_current_file.py",
    "content": "import pyblish.api\nfrom openpype.hosts.blender.api import workio\n\n\nclass CollectBlenderCurrentFile(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current working file into context\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.5\n    label = \"Blender Current File\"\n    hosts = [\"blender\"]\n\n    def process(self, context):\n        \"\"\"Inject the current working file\"\"\"\n        current_file = workio.current_file()\n        context.data[\"currentFile\"] = current_file\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/collect_instance.py",
    "content": "import bpy\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import KnownPublishError\nfrom openpype.hosts.blender.api.pipeline import AVALON_PROPERTY\n\n\nclass CollectBlenderInstanceData(pyblish.api.InstancePlugin):\n    \"\"\"Validator to verify that the instance is not empty\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    hosts = [\"blender\"]\n    families = [\"model\", \"pointcache\", \"animation\", \"rig\", \"camera\", \"layout\",\n                \"blendScene\"]\n    label = \"Collect Instance\"\n\n    def process(self, instance):\n        instance_node = instance.data[\"transientData\"][\"instance_node\"]\n\n        # Collect members of the instance\n        members = [instance_node]\n        if isinstance(instance_node, bpy.types.Collection):\n            members.extend(instance_node.objects)\n            members.extend(instance_node.children)\n\n            # Special case for animation instances, include armatures\n            if instance.data[\"family\"] == \"animation\":\n                for obj in instance_node.objects:\n                    if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY):\n                        members.extend(\n                            child for child in obj.children\n                            if child.type == 'ARMATURE'\n                        )\n        elif isinstance(instance_node, bpy.types.Object):\n            members.extend(instance_node.children_recursive)\n        else:\n            raise KnownPublishError(\n                f\"Unsupported instance node type '{type(instance_node)}' \"\n                f\"for instance '{instance}'\"\n            )\n\n        instance[:] = members\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/collect_render.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect render data.\"\"\"\n\nimport os\nimport re\n\nimport bpy\n\nfrom openpype.hosts.blender.api import colorspace\nimport pyblish.api\n\n\nclass CollectBlenderRender(pyblish.api.InstancePlugin):\n    \"\"\"Gather all publishable render instances.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.01\n    hosts = [\"blender\"]\n    families = [\"render\"]\n    label = \"Collect Render\"\n    sync_workfile_version = False\n\n    @staticmethod\n    def generate_expected_beauty(\n        render_product, frame_start, frame_end, frame_step, ext\n    ):\n        \"\"\"\n        Generate the expected files for the render product for the beauty\n        render. This returns a list of files that should be rendered. It\n        replaces the sequence of `#` with the frame number.\n        \"\"\"\n        path = os.path.dirname(render_product)\n        file = os.path.basename(render_product)\n\n        expected_files = []\n\n        for frame in range(frame_start, frame_end + 1, frame_step):\n            frame_str = str(frame).rjust(4, \"0\")\n            filename = re.sub(\"#+\", frame_str, file)\n            expected_file = f\"{os.path.join(path, filename)}.{ext}\"\n            expected_files.append(expected_file.replace(\"\\\\\", \"/\"))\n\n        return {\n            \"beauty\": expected_files\n        }\n\n    @staticmethod\n    def generate_expected_aovs(\n        aov_file_product, frame_start, frame_end, frame_step, ext\n    ):\n        \"\"\"\n        Generate the expected files for the render product for the beauty\n        render. This returns a list of files that should be rendered. It\n        replaces the sequence of `#` with the frame number.\n        \"\"\"\n        expected_files = {}\n\n        for aov_name, aov_file in aov_file_product:\n            path = os.path.dirname(aov_file)\n            file = os.path.basename(aov_file)\n\n            aov_files = []\n\n            for frame in range(frame_start, frame_end + 1, frame_step):\n                frame_str = str(frame).rjust(4, \"0\")\n                filename = re.sub(\"#+\", frame_str, file)\n                expected_file = f\"{os.path.join(path, filename)}.{ext}\"\n                aov_files.append(expected_file.replace(\"\\\\\", \"/\"))\n\n            expected_files[aov_name] = aov_files\n\n        return expected_files\n\n    def process(self, instance):\n        context = instance.context\n\n        instance_node = instance.data[\"transientData\"][\"instance_node\"]\n        render_data = instance_node.get(\"render_data\")\n\n        assert render_data, \"No render data found.\"\n\n        render_product = render_data.get(\"render_product\")\n        aov_file_product = render_data.get(\"aov_file_product\")\n        ext = render_data.get(\"image_format\")\n        multilayer = render_data.get(\"multilayer_exr\")\n\n        frame_start = context.data[\"frameStart\"]\n        frame_end = context.data[\"frameEnd\"]\n        frame_handle_start = context.data[\"frameStartHandle\"]\n        frame_handle_end = context.data[\"frameEndHandle\"]\n\n        expected_beauty = self.generate_expected_beauty(\n            render_product, int(frame_start), int(frame_end),\n            int(bpy.context.scene.frame_step), ext)\n\n        expected_aovs = self.generate_expected_aovs(\n            aov_file_product, int(frame_start), int(frame_end),\n            int(bpy.context.scene.frame_step), ext)\n\n        expected_files = expected_beauty | expected_aovs\n\n        instance.data.update({\n            \"families\": [\"render\", \"render.farm\"],\n            \"frameStart\": frame_start,\n            \"frameEnd\": frame_end,\n            \"frameStartHandle\": frame_handle_start,\n            \"frameEndHandle\": frame_handle_end,\n            \"fps\": context.data[\"fps\"],\n            \"byFrameStep\": bpy.context.scene.frame_step,\n            \"review\": render_data.get(\"review\", False),\n            \"multipartExr\": ext == \"exr\" and multilayer,\n            \"farm\": True,\n            \"expectedFiles\": [expected_files],\n            # OCIO not currently implemented in Blender, but the following\n            # settings are required by the schema, so it is hardcoded.\n            # TODO: Implement OCIO in Blender\n            \"colorspaceConfig\": \"\",\n            \"colorspaceDisplay\": \"sRGB\",\n            \"colorspaceView\": \"ACES 1.0 SDR-video\",\n            \"renderProducts\": colorspace.ARenderProduct(),\n        })\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/collect_review.py",
    "content": "import bpy\n\nimport pyblish.api\n\n\nclass CollectReview(pyblish.api.InstancePlugin):\n    \"\"\"Collect Review data\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.3\n    label = \"Collect Review Data\"\n    families = [\"review\"]\n\n    def process(self, instance):\n\n        self.log.debug(f\"instance: {instance}\")\n\n        datablock = instance.data[\"transientData\"][\"instance_node\"]\n\n        # get cameras\n        cameras = [\n            obj\n            for obj in datablock.all_objects\n            if isinstance(obj, bpy.types.Object) and obj.type == \"CAMERA\"\n        ]\n\n        assert len(cameras) == 1, (\n            f\"Not a single camera found in extraction: {cameras}\"\n        )\n        camera = cameras[0].name\n        self.log.debug(f\"camera: {camera}\")\n\n        focal_length = cameras[0].data.lens\n\n        # get isolate objects list from meshes instance members.\n        types = {\"MESH\", \"GPENCIL\"}\n        isolate_objects = [\n            obj\n            for obj in instance\n            if isinstance(obj, bpy.types.Object) and obj.type in types\n        ]\n\n        if not instance.data.get(\"remove\"):\n            # Store focal length in `burninDataMembers`\n            burninData = instance.data.setdefault(\"burninDataMembers\", {})\n            burninData[\"focalLength\"] = focal_length\n\n            instance.data.update({\n                \"review_camera\": camera,\n                \"frameStart\": instance.context.data[\"frameStart\"],\n                \"frameEnd\": instance.context.data[\"frameEnd\"],\n                \"fps\": instance.context.data[\"fps\"],\n                \"isolate\": isolate_objects,\n            })\n\n            self.log.debug(f\"instance data: {instance.data}\")\n\n            # TODO : Collect audio\n            audio_tracks = []\n            instance.data[\"audio\"] = []\n            for track in audio_tracks:\n                instance.data[\"audio\"].append(\n                    {\n                        \"offset\": track.offset.get(),\n                        \"filename\": track.filename.get(),\n                    }\n                )\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/collect_workfile.py",
    "content": "from pathlib import Path\n\nfrom pyblish.api import InstancePlugin, CollectorOrder\n\n\nclass CollectWorkfile(InstancePlugin):\n    \"\"\"Inject workfile data into its instance.\"\"\"\n\n    order = CollectorOrder\n    label = \"Collect Workfile\"\n    hosts = [\"blender\"]\n    families = [\"workfile\"]\n\n    def process(self, instance):\n        \"\"\"Process collector.\"\"\"\n\n        context = instance.context\n        filepath = Path(context.data[\"currentFile\"])\n        ext = filepath.suffix\n\n        instance.data.update(\n            {\n                \"setMembers\": [filepath.as_posix()],\n                \"frameStart\": context.data.get(\"frameStart\", 1),\n                \"frameEnd\": context.data.get(\"frameEnd\", 1),\n                \"handleStart\": context.data.get(\"handleStart\", 1),\n                \"handledEnd\": context.data.get(\"handleEnd\", 1),\n                \"representations\": [\n                    {\n                        \"name\": ext.lstrip(\".\"),\n                        \"ext\": ext.lstrip(\".\"),\n                        \"files\": filepath.name,\n                        \"stagingDir\": filepath.parent,\n                    }\n                ],\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/extract_abc.py",
    "content": "import os\n\nimport bpy\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.blender.api import plugin\n\n\nclass ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin):\n    \"\"\"Extract as ABC.\"\"\"\n\n    label = \"Extract ABC\"\n    hosts = [\"blender\"]\n    families = [\"pointcache\"]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # Define extract output file path\n        stagingdir = self.staging_dir(instance)\n        asset_name = instance.data[\"assetEntity\"][\"name\"]\n        subset = instance.data[\"subset\"]\n        instance_name = f\"{asset_name}_{subset}\"\n        filename = f\"{instance_name}.abc\"\n        filepath = os.path.join(stagingdir, filename)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction..\")\n\n        plugin.deselect_all()\n\n        asset_group = instance.data[\"transientData\"][\"instance_node\"]\n\n        selected = []\n        for obj in instance:\n            if isinstance(obj, bpy.types.Object):\n                obj.select_set(True)\n                selected.append(obj)\n\n        context = plugin.create_blender_context(\n            active=asset_group, selected=selected)\n\n        with bpy.context.temp_override(**context):\n            # We export the abc\n            bpy.ops.wm.alembic_export(\n                filepath=filepath,\n                selected=True,\n                flatten=False\n            )\n\n        plugin.deselect_all()\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'abc',\n            'ext': 'abc',\n            'files': filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\",\n                       instance.name, representation)\n\n\nclass ExtractModelABC(ExtractABC):\n    \"\"\"Extract model as ABC.\"\"\"\n\n    label = \"Extract Model ABC\"\n    hosts = [\"blender\"]\n    families = [\"model\"]\n    optional = True\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/extract_abc_animation.py",
    "content": "import os\n\nimport bpy\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.blender.api import plugin\n\n\nclass ExtractAnimationABC(\n        publish.Extractor,\n        publish.OptionalPyblishPluginMixin,\n):\n    \"\"\"Extract as ABC.\"\"\"\n\n    label = \"Extract Animation ABC\"\n    hosts = [\"blender\"]\n    families = [\"animation\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # Define extract output file path\n        stagingdir = self.staging_dir(instance)\n        asset_name = instance.data[\"assetEntity\"][\"name\"]\n        subset = instance.data[\"subset\"]\n        instance_name = f\"{asset_name}_{subset}\"\n        filename = f\"{instance_name}.abc\"\n\n        filepath = os.path.join(stagingdir, filename)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction..\")\n\n        plugin.deselect_all()\n\n        selected = []\n        asset_group = instance.data[\"transientData\"][\"instance_node\"]\n\n        objects = []\n        for obj in instance:\n            if isinstance(obj, bpy.types.Collection):\n                for child in obj.all_objects:\n                    objects.append(child)\n        for obj in objects:\n            children = [o for o in bpy.data.objects if o.parent == obj]\n            for child in children:\n                objects.append(child)\n\n        for obj in objects:\n            obj.select_set(True)\n            selected.append(obj)\n\n        context = plugin.create_blender_context(\n            active=asset_group, selected=selected)\n\n        with bpy.context.temp_override(**context):\n            # We export the abc\n            bpy.ops.wm.alembic_export(\n                filepath=filepath,\n                selected=True,\n                flatten=False\n            )\n\n        plugin.deselect_all()\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'abc',\n            'ext': 'abc',\n            'files': filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\",\n                       instance.name, representation)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/extract_blend.py",
    "content": "import os\n\nimport bpy\n\nfrom openpype.pipeline import publish\n\n\nclass ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin):\n    \"\"\"Extract a blend file.\"\"\"\n\n    label = \"Extract Blend\"\n    hosts = [\"blender\"]\n    families = [\"model\", \"camera\", \"rig\", \"action\", \"layout\", \"blendScene\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # Define extract output file path\n\n        stagingdir = self.staging_dir(instance)\n        asset_name = instance.data[\"assetEntity\"][\"name\"]\n        subset = instance.data[\"subset\"]\n        instance_name = f\"{asset_name}_{subset}\"\n        filename = f\"{instance_name}.blend\"\n        filepath = os.path.join(stagingdir, filename)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction..\")\n\n        data_blocks = set()\n\n        for data in instance:\n            data_blocks.add(data)\n            # Pack used images in the blend files.\n            if not (\n                isinstance(data, bpy.types.Object) and data.type == 'MESH'\n            ):\n                continue\n            for material_slot in data.material_slots:\n                mat = material_slot.material\n                if not (mat and mat.use_nodes):\n                    continue\n                tree = mat.node_tree\n                if tree.type != 'SHADER':\n                    continue\n                for node in tree.nodes:\n                    if node.bl_idname != 'ShaderNodeTexImage':\n                        continue\n                    # Check if image is not packed already\n                    # and pack it if not.\n                    if node.image and node.image.packed_file is None:\n                        node.image.pack()\n\n        bpy.data.libraries.write(filepath, data_blocks)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'blend',\n            'ext': 'blend',\n            'files': filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\",\n                       instance.name, representation)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/extract_blend_animation.py",
    "content": "import os\n\nimport bpy\n\nfrom openpype.pipeline import publish\n\n\nclass ExtractBlendAnimation(\n        publish.Extractor,\n        publish.OptionalPyblishPluginMixin,\n):\n    \"\"\"Extract a blend file.\"\"\"\n\n    label = \"Extract Blend\"\n    hosts = [\"blender\"]\n    families = [\"animation\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # Define extract output file path\n\n        stagingdir = self.staging_dir(instance)\n        asset_name = instance.data[\"assetEntity\"][\"name\"]\n        subset = instance.data[\"subset\"]\n        instance_name = f\"{asset_name}_{subset}\"\n        filename = f\"{instance_name}.blend\"\n        filepath = os.path.join(stagingdir, filename)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction..\")\n\n        data_blocks = set()\n\n        for obj in instance:\n            if isinstance(obj, bpy.types.Object) and obj.type == 'EMPTY':\n                child = obj.children[0]\n                if child and child.type == 'ARMATURE':\n                    if child.animation_data and child.animation_data.action:\n                        if not obj.animation_data:\n                            obj.animation_data_create()\n                        obj.animation_data.action = child.animation_data.action\n                        obj.animation_data_clear()\n                        data_blocks.add(child.animation_data.action)\n                        data_blocks.add(obj)\n\n        bpy.data.libraries.write(filepath, data_blocks)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'blend',\n            'ext': 'blend',\n            'files': filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\",\n                       instance.name, representation)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/extract_camera_abc.py",
    "content": "import os\n\nimport bpy\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.blender.api import plugin\nfrom openpype.hosts.blender.api.pipeline import AVALON_PROPERTY\n\n\nclass ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin):\n    \"\"\"Extract camera as ABC.\"\"\"\n\n    label = \"Extract Camera (ABC)\"\n    hosts = [\"blender\"]\n    families = [\"camera\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # Define extract output file path\n        stagingdir = self.staging_dir(instance)\n        asset_name = instance.data[\"assetEntity\"][\"name\"]\n        subset = instance.data[\"subset\"]\n        instance_name = f\"{asset_name}_{subset}\"\n        filename = f\"{instance_name}.abc\"\n        filepath = os.path.join(stagingdir, filename)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction..\")\n\n        plugin.deselect_all()\n\n        asset_group = instance.data[\"transientData\"][\"instance_node\"]\n\n        # Need to cast to list because children is a tuple\n        selected = list(asset_group.children)\n        active = selected[0]\n\n        for obj in selected:\n            obj.select_set(True)\n\n        context = plugin.create_blender_context(\n            active=active, selected=selected)\n\n        with bpy.context.temp_override(**context):\n            # We export the abc\n            bpy.ops.wm.alembic_export(\n                filepath=filepath,\n                selected=True,\n                flatten=True\n            )\n\n        plugin.deselect_all()\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'abc',\n            'ext': 'abc',\n            'files': filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\",\n                       instance.name, representation)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/extract_camera_fbx.py",
    "content": "import os\n\nimport bpy\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.blender.api import plugin\n\n\nclass ExtractCamera(publish.Extractor, publish.OptionalPyblishPluginMixin):\n    \"\"\"Extract as the camera as FBX.\"\"\"\n\n    label = \"Extract Camera (FBX)\"\n    hosts = [\"blender\"]\n    families = [\"camera\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # Define extract output file path\n        stagingdir = self.staging_dir(instance)\n        asset_name = instance.data[\"assetEntity\"][\"name\"]\n        subset = instance.data[\"subset\"]\n        instance_name = f\"{asset_name}_{subset}\"\n        filename = f\"{instance_name}.fbx\"\n        filepath = os.path.join(stagingdir, filename)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction..\")\n\n        plugin.deselect_all()\n\n        selected = []\n\n        camera = None\n\n        for obj in instance:\n            if obj.type == \"CAMERA\":\n                obj.select_set(True)\n                selected.append(obj)\n                camera = obj\n                break\n\n        assert camera, \"No camera found\"\n\n        context = plugin.create_blender_context(\n            active=camera, selected=selected)\n\n        scale_length = bpy.context.scene.unit_settings.scale_length\n        bpy.context.scene.unit_settings.scale_length = 0.01\n\n        with bpy.context.temp_override(**context):\n            # We export the fbx\n            bpy.ops.export_scene.fbx(\n                filepath=filepath,\n                use_active_collection=False,\n                use_selection=True,\n                bake_anim_use_nla_strips=False,\n                bake_anim_use_all_actions=False,\n                add_leaf_bones=False,\n                armature_nodetype='ROOT',\n                object_types={'CAMERA'},\n                bake_anim_simplify_factor=0.0\n            )\n\n        bpy.context.scene.unit_settings.scale_length = scale_length\n\n        plugin.deselect_all()\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'fbx',\n            'ext': 'fbx',\n            'files': filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\",\n                       instance.name, representation)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/extract_fbx.py",
    "content": "import os\n\nimport bpy\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.blender.api import plugin\nfrom openpype.hosts.blender.api.pipeline import AVALON_PROPERTY\n\n\nclass ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin):\n    \"\"\"Extract as FBX.\"\"\"\n\n    label = \"Extract FBX\"\n    hosts = [\"blender\"]\n    families = [\"model\", \"rig\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # Define extract output file path\n        stagingdir = self.staging_dir(instance)\n        asset_name = instance.data[\"assetEntity\"][\"name\"]\n        subset = instance.data[\"subset\"]\n        instance_name = f\"{asset_name}_{subset}\"\n        filename = f\"{instance_name}.fbx\"\n        filepath = os.path.join(stagingdir, filename)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction..\")\n\n        plugin.deselect_all()\n\n        asset_group = instance.data[\"transientData\"][\"instance_node\"]\n\n        selected = []\n        for obj in instance:\n            obj.select_set(True)\n            selected.append(obj)\n\n        context = plugin.create_blender_context(\n            active=asset_group, selected=selected)\n\n        new_materials = []\n        new_materials_objs = []\n        objects = list(asset_group.children)\n\n        for obj in objects:\n            objects.extend(obj.children)\n            if obj.type == 'MESH' and len(obj.data.materials) == 0:\n                mat = bpy.data.materials.new(obj.name)\n                obj.data.materials.append(mat)\n                new_materials.append(mat)\n                new_materials_objs.append(obj)\n\n        scale_length = bpy.context.scene.unit_settings.scale_length\n        bpy.context.scene.unit_settings.scale_length = 0.01\n\n        with bpy.context.temp_override(**context):\n            # We export the fbx\n            bpy.ops.export_scene.fbx(\n                filepath=filepath,\n                use_active_collection=False,\n                use_selection=True,\n                mesh_smooth_type='FACE',\n                add_leaf_bones=False\n            )\n\n        bpy.context.scene.unit_settings.scale_length = scale_length\n\n        plugin.deselect_all()\n\n        for mat in new_materials:\n            bpy.data.materials.remove(mat)\n\n        for obj in new_materials_objs:\n            obj.data.materials.pop()\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'fbx',\n            'ext': 'fbx',\n            'files': filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\",\n                       instance.name, representation)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/extract_fbx_animation.py",
    "content": "import os\nimport json\n\nimport bpy\nimport bpy_extras\nimport bpy_extras.anim_utils\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.blender.api import plugin\nfrom openpype.hosts.blender.api.pipeline import AVALON_PROPERTY\n\n\ndef get_all_parents(obj):\n    \"\"\"Get all recursive parents of object\"\"\"\n    result = []\n    while True:\n        obj = obj.parent\n        if not obj:\n            break\n        result.append(obj)\n    return result\n\n\ndef get_highest_root(objects):\n    # Get the highest object that is also in the collection\n    included_objects = {obj.name_full for obj in objects}\n    num_parents_to_obj = {}\n    for obj in objects:\n        if isinstance(obj, bpy.types.Object):\n            parents = get_all_parents(obj)\n            # included parents\n            parents = [parent for parent in parents if\n                       parent.name_full in included_objects]\n            if not parents:\n                # A node without parents must be a highest root\n                return obj\n\n            num_parents_to_obj.setdefault(len(parents), obj)\n\n    minimum_parent = min(num_parents_to_obj)\n    return num_parents_to_obj[minimum_parent]\n\n\nclass ExtractAnimationFBX(\n        publish.Extractor,\n        publish.OptionalPyblishPluginMixin,\n):\n    \"\"\"Extract as animation.\"\"\"\n\n    label = \"Extract FBX\"\n    hosts = [\"blender\"]\n    families = [\"animation\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # Define extract output file path\n        stagingdir = self.staging_dir(instance)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction..\")\n\n        asset_group = instance.data[\"transientData\"][\"instance_node\"]\n\n        # Get objects in this collection (but not in children collections)\n        # and for those objects include the children hierarchy\n        # TODO: Would it make more sense for the Collect Instance collector\n        #   to also always retrieve all the children?\n        objects = set(asset_group.objects)\n\n        # From the direct children of the collection find the 'root' node\n        # that we want to export - it is the 'highest' node in a hierarchy\n        root = get_highest_root(objects)\n\n        for obj in list(objects):\n            objects.update(obj.children_recursive)\n\n        # Find all armatures among the objects, assume to find only one\n        armatures = [obj for obj in objects if obj.type == \"ARMATURE\"]\n        if not armatures:\n            raise RuntimeError(\n                f\"Unable to find ARMATURE in collection: \"\n                f\"{asset_group.name}\"\n            )\n        elif len(armatures) > 1:\n            self.log.warning(\n                \"Found more than one ARMATURE, using \"\n                f\"only first of: {armatures}\"\n            )\n        armature = armatures[0]\n\n        object_action_pairs = []\n        original_actions = []\n\n        starting_frames = []\n        ending_frames = []\n\n        # For each armature, we make a copy of the current action\n        if armature.animation_data and armature.animation_data.action:\n            curr_action = armature.animation_data.action\n            copy_action = curr_action.copy()\n\n            curr_frame_range = curr_action.frame_range\n\n            starting_frames.append(curr_frame_range[0])\n            ending_frames.append(curr_frame_range[1])\n        else:\n            self.log.info(\n                f\"Armature '{armature.name}' has no animation, \"\n                f\"skipping FBX animation extraction for {instance}.\"\n            )\n            return\n\n        asset_group_name = asset_group.name\n        asset_name = asset_group.get(AVALON_PROPERTY).get(\"asset_name\")\n        if asset_name:\n            # Rename for the export; this data is only present when loaded\n            # from a JSON Layout (layout family)\n            asset_group.name = asset_name\n\n        # Remove : from the armature name for the export\n        armature_name = armature.name\n        original_name = armature_name.split(':')[1]\n        armature.name = original_name\n\n        object_action_pairs.append((armature, copy_action))\n        original_actions.append(curr_action)\n\n        # We compute the starting and ending frames\n        max_frame = min(starting_frames)\n        min_frame = max(ending_frames)\n\n        # We bake the copy of the current action for each object\n        bpy_extras.anim_utils.bake_action_objects(\n            object_action_pairs,\n            frames=range(int(min_frame), int(max_frame)),\n            do_object=False,\n            do_clean=False\n        )\n\n        for obj in bpy.data.objects:\n            obj.select_set(False)\n\n        root.select_set(True)\n        armature.select_set(True)\n        asset_name = instance.data[\"assetEntity\"][\"name\"]\n        subset = instance.data[\"subset\"]\n        instance_name = f\"{asset_name}_{subset}\"\n        fbx_filename = f\"{instance_name}_{armature.name}.fbx\"\n        filepath = os.path.join(stagingdir, fbx_filename)\n\n        override = plugin.create_blender_context(\n            active=root, selected=[root, armature])\n\n        with bpy.context.temp_override(**override):\n            # We export the fbx\n            bpy.ops.export_scene.fbx(\n                filepath=filepath,\n                use_active_collection=False,\n                use_selection=True,\n                bake_anim_use_nla_strips=False,\n                bake_anim_use_all_actions=False,\n                add_leaf_bones=False,\n                armature_nodetype='ROOT',\n                object_types={'EMPTY', 'ARMATURE'}\n            )\n\n        armature.name = armature_name\n        asset_group.name = asset_group_name\n        root.select_set(True)\n        armature.select_set(False)\n\n        # We delete the baked action and set the original one back\n        for i in range(0, len(object_action_pairs)):\n            pair = object_action_pairs[i]\n            action = original_actions[i]\n\n            if action:\n                pair[0].animation_data.action = action\n\n            if pair[1]:\n                pair[1].user_clear()\n                bpy.data.actions.remove(pair[1])\n\n        json_filename = f\"{instance_name}.json\"\n        json_path = os.path.join(stagingdir, json_filename)\n\n        json_dict = {\n            \"instance_name\": asset_group.get(AVALON_PROPERTY).get(\"objectName\")\n        }\n\n        # collection = instance.data.get(\"name\")\n        # container = None\n        # for obj in bpy.data.collections[collection].objects:\n        #     if obj.type == \"ARMATURE\":\n        #         container_name = obj.get(\"avalon\").get(\"container_name\")\n        #         container = bpy.data.collections[container_name]\n        # if container:\n        #     json_dict = {\n        #         \"instance_name\": container.get(\"avalon\").get(\"instance_name\")\n        #     }\n\n        with open(json_path, \"w+\") as file:\n            json.dump(json_dict, fp=file, indent=2)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        fbx_representation = {\n            'name': 'fbx',\n            'ext': 'fbx',\n            'files': fbx_filename,\n            \"stagingDir\": stagingdir,\n        }\n        json_representation = {\n            'name': 'json',\n            'ext': 'json',\n            'files': json_filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(fbx_representation)\n        instance.data[\"representations\"].append(json_representation)\n\n        self.log.debug(\"Extracted instance '{}' to: {}\".format(\n                       instance.name, fbx_representation))\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/extract_layout.py",
    "content": "import os\nimport json\n\nimport bpy\nimport bpy_extras\nimport bpy_extras.anim_utils\n\nfrom openpype.client import get_representation_by_name\nfrom openpype.pipeline import publish\nfrom openpype.hosts.blender.api import plugin\nfrom openpype.hosts.blender.api.pipeline import AVALON_PROPERTY\n\n\nclass ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin):\n    \"\"\"Extract a layout.\"\"\"\n\n    label = \"Extract Layout (JSON)\"\n    hosts = [\"blender\"]\n    families = [\"layout\"]\n    optional = True\n\n    def _export_animation(self, asset, instance, stagingdir, fbx_count):\n        n = fbx_count\n\n        for obj in asset.children:\n            if obj.type != \"ARMATURE\":\n                continue\n\n            object_action_pairs = []\n            original_actions = []\n\n            starting_frames = []\n            ending_frames = []\n\n            # For each armature, we make a copy of the current action\n            curr_action = None\n            copy_action = None\n\n            if obj.animation_data and obj.animation_data.action:\n                curr_action = obj.animation_data.action\n                copy_action = curr_action.copy()\n\n                curr_frame_range = curr_action.frame_range\n\n                starting_frames.append(curr_frame_range[0])\n                ending_frames.append(curr_frame_range[1])\n            else:\n                self.log.info(\"Object has no animation.\")\n                continue\n\n            asset_group_name = asset.name\n            asset.name = asset.get(AVALON_PROPERTY).get(\"asset_name\")\n\n            armature_name = obj.name\n            original_name = armature_name.split(':')[1]\n            obj.name = original_name\n\n            object_action_pairs.append((obj, copy_action))\n            original_actions.append(curr_action)\n\n            # We compute the starting and ending frames\n            max_frame = min(starting_frames)\n            min_frame = max(ending_frames)\n\n            # We bake the copy of the current action for each object\n            bpy_extras.anim_utils.bake_action_objects(\n                object_action_pairs,\n                frames=range(int(min_frame), int(max_frame)),\n                do_object=False,\n                do_clean=False\n            )\n\n            for o in bpy.data.objects:\n                o.select_set(False)\n\n            asset.select_set(True)\n            obj.select_set(True)\n            fbx_filename = f\"{n:03d}.fbx\"\n            filepath = os.path.join(stagingdir, fbx_filename)\n\n            override = plugin.create_blender_context(\n                active=asset, selected=[asset, obj])\n            with bpy.context.temp_override(**override):\n                # We export the fbx\n                bpy.ops.export_scene.fbx(\n                    filepath=filepath,\n                    use_active_collection=False,\n                    use_selection=True,\n                    bake_anim_use_nla_strips=False,\n                    bake_anim_use_all_actions=False,\n                    add_leaf_bones=False,\n                    armature_nodetype='ROOT',\n                    object_types={'EMPTY', 'ARMATURE'}\n                )\n            obj.name = armature_name\n            asset.name = asset_group_name\n            asset.select_set(False)\n            obj.select_set(False)\n\n            # We delete the baked action and set the original one back\n            for i in range(0, len(object_action_pairs)):\n                pair = object_action_pairs[i]\n                action = original_actions[i]\n\n                if action:\n                    pair[0].animation_data.action = action\n\n                if pair[1]:\n                    pair[1].user_clear()\n                    bpy.data.actions.remove(pair[1])\n\n            return fbx_filename, n + 1\n\n        return None, n\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # Define extract output file path\n        stagingdir = self.staging_dir(instance)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction..\")\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        json_data = []\n        fbx_files = []\n\n        asset_group = instance.data[\"transientData\"][\"instance_node\"]\n\n        fbx_count = 0\n\n        project_name = instance.context.data[\"projectEntity\"][\"name\"]\n        for asset in asset_group.children:\n            metadata = asset.get(AVALON_PROPERTY)\n            if not metadata:\n                # Avoid raising error directly if there's just invalid data\n                # inside the instance; better to log it to the artist\n                # TODO: This should actually be validated in a validator\n                self.log.warning(\n                    f\"Found content in layout that is not a loaded \"\n                    f\"asset, skipping: {asset.name_full}\"\n                )\n                continue\n\n            version_id = metadata[\"parent\"]\n            family = metadata[\"family\"]\n\n            self.log.debug(\"Parent: {}\".format(version_id))\n            # Get blend reference\n            blend = get_representation_by_name(\n                project_name, \"blend\", version_id, fields=[\"_id\"]\n            )\n            blend_id = None\n            if blend:\n                blend_id = blend[\"_id\"]\n            # Get fbx reference\n            fbx = get_representation_by_name(\n                project_name, \"fbx\", version_id, fields=[\"_id\"]\n            )\n            fbx_id = None\n            if fbx:\n                fbx_id = fbx[\"_id\"]\n            # Get abc reference\n            abc = get_representation_by_name(\n                project_name, \"abc\", version_id, fields=[\"_id\"]\n            )\n            abc_id = None\n            if abc:\n                abc_id = abc[\"_id\"]\n\n            json_element = {}\n            if blend_id:\n                json_element[\"reference\"] = str(blend_id)\n            if fbx_id:\n                json_element[\"reference_fbx\"] = str(fbx_id)\n            if abc_id:\n                json_element[\"reference_abc\"] = str(abc_id)\n            json_element[\"family\"] = family\n            json_element[\"instance_name\"] = asset.name\n            json_element[\"asset_name\"] = metadata[\"asset_name\"]\n            json_element[\"file_path\"] = metadata[\"libpath\"]\n\n            json_element[\"transform\"] = {\n                \"translation\": {\n                    \"x\": asset.location.x,\n                    \"y\": asset.location.y,\n                    \"z\": asset.location.z\n                },\n                \"rotation\": {\n                    \"x\": asset.rotation_euler.x,\n                    \"y\": asset.rotation_euler.y,\n                    \"z\": asset.rotation_euler.z\n                },\n                \"scale\": {\n                    \"x\": asset.scale.x,\n                    \"y\": asset.scale.y,\n                    \"z\": asset.scale.z\n                }\n            }\n\n            json_element[\"transform_matrix\"] = []\n\n            for row in list(asset.matrix_world.transposed()):\n                json_element[\"transform_matrix\"].append(list(row))\n\n            json_element[\"basis\"] = [\n                [1, 0, 0, 0],\n                [0, -1, 0, 0],\n                [0, 0, 1, 0],\n                [0, 0, 0, 1]\n            ]\n\n            # Extract the animation as well\n            if family == \"rig\":\n                f, n = self._export_animation(\n                    asset, instance, stagingdir, fbx_count)\n                if f:\n                    fbx_files.append(f)\n                    json_element[\"animation\"] = f\n                    fbx_count = n\n\n            json_data.append(json_element)\n\n        asset_name = instance.data[\"assetEntity\"][\"name\"]\n        subset = instance.data[\"subset\"]\n        instance_name = f\"{asset_name}_{subset}\"\n        json_filename = f\"{instance_name}.json\"\n\n        json_path = os.path.join(stagingdir, json_filename)\n\n        with open(json_path, \"w+\") as file:\n            json.dump(json_data, fp=file, indent=2)\n\n        json_representation = {\n            'name': 'json',\n            'ext': 'json',\n            'files': json_filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(json_representation)\n\n        self.log.debug(fbx_files)\n\n        if len(fbx_files) == 1:\n            fbx_representation = {\n                'name': 'fbx',\n                'ext': '000.fbx',\n                'files': fbx_files[0],\n                \"stagingDir\": stagingdir,\n            }\n            instance.data[\"representations\"].append(fbx_representation)\n        elif len(fbx_files) > 1:\n            fbx_representation = {\n                'name': 'fbx',\n                'ext': 'fbx',\n                'files': fbx_files,\n                \"stagingDir\": stagingdir,\n            }\n            instance.data[\"representations\"].append(fbx_representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\",\n                       instance.name, json_representation)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/extract_playblast.py",
    "content": "import os\nimport clique\n\nimport bpy\n\nimport pyblish.api\nfrom openpype.pipeline import publish\nfrom openpype.hosts.blender.api import capture\nfrom openpype.hosts.blender.api.lib import maintained_time\n\n\nclass ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin):\n    \"\"\"\n    Extract viewport playblast.\n\n    Takes review camera and creates review Quicktime video based on viewport\n    capture.\n    \"\"\"\n\n    label = \"Extract Playblast\"\n    hosts = [\"blender\"]\n    families = [\"review\"]\n    optional = True\n    order = pyblish.api.ExtractorOrder + 0.01\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # get scene fps\n        fps = instance.data.get(\"fps\")\n        if fps is None:\n            fps = bpy.context.scene.render.fps\n            instance.data[\"fps\"] = fps\n\n        self.log.debug(f\"fps: {fps}\")\n\n        # If start and end frames cannot be determined,\n        # get them from Blender timeline.\n        start = instance.data.get(\"frameStart\", bpy.context.scene.frame_start)\n        end = instance.data.get(\"frameEnd\", bpy.context.scene.frame_end)\n\n        self.log.debug(f\"start: {start}, end: {end}\")\n        assert end > start, \"Invalid time range !\"\n\n        # get cameras\n        camera = instance.data(\"review_camera\", None)\n\n        # get isolate objects list\n        isolate = instance.data(\"isolate\", None)\n\n        # get output path\n        stagingdir = self.staging_dir(instance)\n        asset_name = instance.data[\"assetEntity\"][\"name\"]\n        subset = instance.data[\"subset\"]\n        filename = f\"{asset_name}_{subset}\"\n\n        path = os.path.join(stagingdir, filename)\n\n        self.log.debug(f\"Outputting images to {path}\")\n\n        project_settings = instance.context.data[\"project_settings\"][\"blender\"]\n        presets = project_settings[\"publish\"][\"ExtractPlayblast\"][\"presets\"]\n        preset = presets.get(\"default\")\n        preset.update({\n            \"camera\": camera,\n            \"start_frame\": start,\n            \"end_frame\": end,\n            \"filename\": path,\n            \"overwrite\": True,\n            \"isolate\": isolate,\n        })\n        preset.setdefault(\n            \"image_settings\",\n            {\n                \"file_format\": \"PNG\",\n                \"color_mode\": \"RGB\",\n                \"color_depth\": \"8\",\n                \"compression\": 15,\n            },\n        )\n\n        with maintained_time():\n            path = capture(**preset)\n\n        self.log.debug(f\"playblast path {path}\")\n\n        collected_files = os.listdir(stagingdir)\n        collections, remainder = clique.assemble(\n            collected_files,\n            patterns=[f\"{filename}\\\\.{clique.DIGITS_PATTERN}\\\\.png$\"],\n        )\n\n        if len(collections) > 1:\n            raise RuntimeError(\n                f\"More than one collection found in stagingdir: {stagingdir}\"\n            )\n        elif len(collections) == 0:\n            raise RuntimeError(\n                f\"No collection found in stagingdir: {stagingdir}\"\n            )\n\n        frame_collection = collections[0]\n\n        self.log.debug(f\"Found collection of interest {frame_collection}\")\n\n        instance.data.setdefault(\"representations\", [])\n\n        tags = [\"review\"]\n        if not instance.data.get(\"keepImages\"):\n            tags.append(\"delete\")\n\n        representation = {\n            \"name\": \"png\",\n            \"ext\": \"png\",\n            \"files\": list(frame_collection),\n            \"stagingDir\": stagingdir,\n            \"frameStart\": start,\n            \"frameEnd\": end,\n            \"fps\": fps,\n            \"tags\": tags,\n            \"camera_name\": camera\n        }\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/extract_thumbnail.py",
    "content": "import os\nimport glob\n\nimport pyblish.api\nfrom openpype.pipeline import publish\nfrom openpype.hosts.blender.api import capture\nfrom openpype.hosts.blender.api.lib import maintained_time\n\nimport bpy\n\n\nclass ExtractThumbnail(publish.Extractor):\n    \"\"\"Extract viewport thumbnail.\n\n    Takes review camera and creates a thumbnail based on viewport\n    capture.\n\n    \"\"\"\n\n    label = \"Extract Thumbnail\"\n    hosts = [\"blender\"]\n    families = [\"review\"]\n    order = pyblish.api.ExtractorOrder + 0.01\n    presets = {}\n\n    def process(self, instance):\n        self.log.debug(\"Extracting capture..\")\n\n        if instance.data.get(\"thumbnailSource\"):\n            self.log.debug(\"Thumbnail source found, skipping...\")\n            return\n\n        stagingdir = self.staging_dir(instance)\n        asset_name = instance.data[\"assetEntity\"][\"name\"]\n        subset = instance.data[\"subset\"]\n        filename = f\"{asset_name}_{subset}\"\n\n        path = os.path.join(stagingdir, filename)\n\n        self.log.debug(f\"Outputting images to {path}\")\n\n        camera = instance.data.get(\"review_camera\", \"AUTO\")\n        start = instance.data.get(\"frameStart\", bpy.context.scene.frame_start)\n        family = instance.data.get(\"family\")\n        isolate = instance.data(\"isolate\", None)\n\n        preset = self.presets.get(family, {})\n\n        preset.update({\n            \"camera\": camera,\n            \"start_frame\": start,\n            \"end_frame\": start,\n            \"filename\": path,\n            \"overwrite\": True,\n            \"isolate\": isolate,\n        })\n        preset.setdefault(\n            \"image_settings\",\n            {\n                \"file_format\": \"JPEG\",\n                \"color_mode\": \"RGB\",\n                \"quality\": 100,\n            },\n        )\n\n        with maintained_time():\n            path = capture(**preset)\n\n        thumbnail = os.path.basename(self._fix_output_path(path))\n\n        self.log.debug(f\"thumbnail: {thumbnail}\")\n\n        instance.data.setdefault(\"representations\", [])\n\n        representation = {\n            \"name\": \"thumbnail\",\n            \"ext\": \"jpg\",\n            \"files\": thumbnail,\n            \"stagingDir\": stagingdir,\n            \"thumbnail\": True\n        }\n        instance.data[\"representations\"].append(representation)\n\n    def _fix_output_path(self, filepath):\n        \"\"\"\"Workaround to return correct filepath.\n\n        To workaround this we just glob.glob() for any file extensions and\n        assume the latest modified file is the correct file and return it.\n\n        \"\"\"\n        # Catch cancelled playblast\n        if filepath is None:\n            self.log.warning(\n                \"Playblast did not result in output path. \"\n                \"Playblast is probably interrupted.\"\n            )\n            return None\n\n        if not os.path.exists(filepath):\n            files = glob.glob(f\"{filepath}.*.jpg\")\n\n            if not files:\n                raise RuntimeError(f\"Couldn't find playblast from: {filepath}\")\n            filepath = max(files, key=os.path.getmtime)\n\n        return filepath\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/increment_workfile_version.py",
    "content": "import pyblish.api\nfrom openpype.pipeline.publish import OptionalPyblishPluginMixin\nfrom openpype.hosts.blender.api.workio import save_file\n\n\nclass IncrementWorkfileVersion(\n        pyblish.api.ContextPlugin,\n        OptionalPyblishPluginMixin\n):\n    \"\"\"Increment current workfile version.\"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.9\n    label = \"Increment Workfile Version\"\n    optional = True\n    hosts = [\"blender\"]\n    families = [\"animation\", \"model\", \"rig\", \"action\", \"layout\", \"blendScene\",\n                \"pointcache\", \"render.farm\"]\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            return\n\n        assert all(result[\"success\"] for result in context.data[\"results\"]), (\n            \"Publishing not successful so version is not increased.\")\n\n        from openpype.lib import version_up\n        path = context.data[\"currentFile\"]\n        filepath = version_up(path)\n\n        save_file(filepath, copy=False)\n\n        self.log.debug('Incrementing blender workfile version')\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/integrate_animation.py",
    "content": "import json\n\nimport pyblish.api\nfrom openpype.pipeline.publish import OptionalPyblishPluginMixin\n\n\nclass IntegrateAnimation(\n        pyblish.api.InstancePlugin,\n        OptionalPyblishPluginMixin,\n):\n    \"\"\"Generate a JSON file for animation.\"\"\"\n\n    label = \"Integrate Animation\"\n    order = pyblish.api.IntegratorOrder + 0.1\n    optional = True\n    hosts = [\"blender\"]\n    families = [\"setdress\"]\n\n    def process(self, instance):\n        self.log.debug(\"Integrate Animation\")\n\n        representation = instance.data.get('representations')[0]\n        json_path = representation.get('publishedFiles')[0]\n\n        with open(json_path, \"r\") as file:\n            data = json.load(file)\n\n        # Update the json file for the setdress to add the published\n        # representations of the animations\n        for json_dict in data:\n            i = None\n            for elem in instance.context:\n                if elem.data.get('subset') == json_dict['subset']:\n                    i = elem\n                    break\n            if not i:\n                continue\n            rep = None\n            pub_repr = i.data.get('published_representations')\n            for elem in pub_repr:\n                if pub_repr.get(elem).get('representation').get('name') == \"fbx\":\n                    rep = pub_repr.get(elem)\n                    break\n            if not rep:\n                continue\n            obj_id = rep.get('representation').get('_id')\n\n            if obj_id:\n                json_dict['_id'] = str(obj_id)\n\n        with open(json_path, \"w\") as file:\n            json.dump(data, fp=file, indent=2)\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py",
    "content": "from typing import List\n\nimport bpy\n\nimport pyblish.api\n\nimport openpype.hosts.blender.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin,\n                                 OptionalPyblishPluginMixin):\n    \"\"\"Camera must have a keyframe at frame 0.\n\n    Unreal shifts the first keyframe to frame 0. Forcing the camera to have\n    a keyframe at frame 0 will ensure that the animation will be the same\n    in Unreal and Blender.\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"blender\"]\n    families = [\"camera\"]\n    label = \"Zero Keyframe\"\n    actions = [openpype.hosts.blender.api.action.SelectInvalidAction]\n\n    @staticmethod\n    def get_invalid(instance) -> List:\n        invalid = []\n        for obj in instance:\n            if isinstance(obj, bpy.types.Object) and obj.type == \"CAMERA\":\n                if obj.animation_data and obj.animation_data.action:\n                    action = obj.animation_data.action\n                    frames_set = set()\n                    for fcu in action.fcurves:\n                        for kp in fcu.keyframe_points:\n                            frames_set.add(kp.co[0])\n                    frames = list(frames_set)\n                    frames.sort()\n                    if frames[0] != 0.0:\n                        invalid.append(obj)\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            names = \", \".join(obj.name for obj in invalid)\n            raise PublishValidationError(\n                f\"Camera must have a keyframe at frame 0: {names}\"\n            )\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/validate_deadline_publish.py",
    "content": "import os\n\nimport bpy\n\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.hosts.blender.api.render_lib import prepare_rendering\n\n\nclass ValidateDeadlinePublish(pyblish.api.InstancePlugin,\n                              OptionalPyblishPluginMixin):\n    \"\"\"Validates Render File Directory is\n    not the same in every submission\n    \"\"\"\n\n    order = ValidateContentsOrder\n    families = [\"render\"]\n    hosts = [\"blender\"]\n    label = \"Validate Render Output for Deadline\"\n    optional = True\n    actions = [RepairAction]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        tree = bpy.context.scene.node_tree\n        output_type = \"CompositorNodeOutputFile\"\n        output_node = None\n        # Remove all output nodes that inlcude \"AYON\" in the name.\n        # There should be only one.\n        for node in tree.nodes:\n            if node.bl_idname == output_type and \"AYON\" in node.name:\n                output_node = node\n                break\n        if not output_node:\n            raise PublishValidationError(\n                \"No output node found in the compositor tree.\"\n            )\n        filepath = bpy.data.filepath\n        file = os.path.basename(filepath)\n        filename, ext = os.path.splitext(file)\n        if filename not in output_node.base_path:\n            raise PublishValidationError(\n                \"Render output folder doesn't match the blender scene name! \"\n                \"Use Repair action to fix the folder file path.\"\n            )\n\n    @classmethod\n    def repair(cls, instance):\n        container = instance.data[\"transientData\"][\"instance_node\"]\n        prepare_rendering(container)\n        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\n        cls.log.debug(\"Reset the render output folder...\")\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/validate_file_saved.py",
    "content": "import bpy\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\nclass SaveWorkfileAction(pyblish.api.Action):\n    \"\"\"Save Workfile.\"\"\"\n    label = \"Save Workfile\"\n    on = \"failed\"\n    icon = \"save\"\n\n    def process(self, context, plugin):\n        bpy.ops.wm.avalon_workfiles()\n\n\nclass ValidateFileSaved(pyblish.api.ContextPlugin,\n                        OptionalPyblishPluginMixin):\n    \"\"\"Validate that the workfile has been saved.\"\"\"\n\n    order = pyblish.api.ValidatorOrder - 0.01\n    hosts = [\"blender\"]\n    label = \"Validate File Saved\"\n    optional = False\n    exclude_families = []\n    actions = [SaveWorkfileAction]\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            return\n\n        if not context.data[\"currentFile\"]:\n            # File has not been saved at all and has no filename\n            raise PublishValidationError(\n                \"Current file is empty. Save the file before continuing.\"\n            )\n\n        # Do not validate workfile has unsaved changes if only instances\n        # present of families that should be excluded\n        families = {\n            instance.data[\"family\"] for instance in context\n            # Consider only enabled instances\n            if instance.data.get(\"publish\", True)\n            and instance.data.get(\"active\", True)\n        }\n\n        def is_excluded(family):\n            return any(family in exclude_family\n                       for exclude_family in self.exclude_families)\n\n        if all(is_excluded(family) for family in families):\n            self.log.debug(\"Only excluded families found, skipping workfile \"\n                           \"unsaved changes validation..\")\n            return\n\n        if bpy.data.is_dirty:\n            raise PublishValidationError(\"Workfile has unsaved changes.\")\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/validate_instance_empty.py",
    "content": "import pyblish.api\nfrom openpype.pipeline.publish import PublishValidationError\n\n\nclass ValidateInstanceEmpty(pyblish.api.InstancePlugin):\n    \"\"\"Validator to verify that the instance is not empty\"\"\"\n\n    order = pyblish.api.ValidatorOrder - 0.01\n    hosts = [\"blender\"]\n    families = [\"model\", \"pointcache\", \"rig\", \"camera\" \"layout\", \"blendScene\"]\n    label = \"Validate Instance is not Empty\"\n    optional = False\n\n    def process(self, instance):\n        # Members are collected by `collect_instance` so we only need to check\n        # whether any member is included. The instance node will be included\n        # as a member as well, hence we will check for at least 2 members\n        if len(instance) < 2:\n            raise PublishValidationError(f\"Instance {instance.name} is empty.\")\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py",
    "content": "from typing import List\n\nimport bpy\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\nimport openpype.hosts.blender.api.action\n\n\nclass ValidateMeshHasUvs(\n        pyblish.api.InstancePlugin,\n        OptionalPyblishPluginMixin,\n):\n    \"\"\"Validate that the current mesh has UV's.\"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"blender\"]\n    families = [\"model\"]\n    label = \"Mesh Has UVs\"\n    actions = [openpype.hosts.blender.api.action.SelectInvalidAction]\n    optional = True\n\n    @staticmethod\n    def has_uvs(obj: bpy.types.Object) -> bool:\n        \"\"\"Check if an object has uv's.\"\"\"\n        if not obj.data.uv_layers:\n            return False\n        for uv_layer in obj.data.uv_layers:\n            for polygon in obj.data.polygons:\n                for loop_index in polygon.loop_indices:\n                    if (\n                        loop_index >= len(uv_layer.data)\n                        or not uv_layer.data[loop_index].uv\n                    ):\n                        return False\n\n        return True\n\n    @classmethod\n    def get_invalid(cls, instance) -> List:\n        invalid = []\n        for obj in instance:\n            if isinstance(obj, bpy.types.Object) and obj.type == 'MESH':\n                if obj.mode != \"OBJECT\":\n                    cls.log.warning(\n                        f\"Mesh object {obj.name} should be in 'OBJECT' mode\"\n                        \" to be properly checked.\"\n                    )\n                if not cls.has_uvs(obj):\n                    invalid.append(obj)\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                f\"Meshes found in instance without valid UV's: {invalid}\"\n            )\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py",
    "content": "from typing import List\n\nimport bpy\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\nimport openpype.hosts.blender.api.action\n\n\nclass ValidateMeshNoNegativeScale(pyblish.api.Validator,\n                                  OptionalPyblishPluginMixin):\n    \"\"\"Ensure that meshes don't have a negative scale.\"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"blender\"]\n    families = [\"model\"]\n    label = \"Mesh No Negative Scale\"\n    actions = [openpype.hosts.blender.api.action.SelectInvalidAction]\n\n    @staticmethod\n    def get_invalid(instance) -> List:\n        invalid = []\n        for obj in instance:\n            if isinstance(obj, bpy.types.Object) and obj.type == 'MESH':\n                if any(v < 0 for v in obj.scale):\n                    invalid.append(obj)\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            names = \", \".join(obj.name for obj in invalid)\n            raise PublishValidationError(\n                f\"Meshes found in instance with negative scale: {names}\"\n            )\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py",
    "content": "from typing import List\n\nimport bpy\n\nimport pyblish.api\n\nimport openpype.hosts.blender.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\nclass ValidateNoColonsInName(pyblish.api.InstancePlugin,\n                             OptionalPyblishPluginMixin):\n    \"\"\"There cannot be colons in names\n\n    Object or bone names cannot include colons. Other software do not\n    handle colons correctly.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"blender\"]\n    families = [\"model\", \"rig\"]\n    label = \"No Colons in names\"\n    actions = [openpype.hosts.blender.api.action.SelectInvalidAction]\n\n    @staticmethod\n    def get_invalid(instance) -> List:\n        invalid = []\n        for obj in instance:\n            if ':' in obj.name:\n                invalid.append(obj)\n            if isinstance(obj, bpy.types.Object) and obj.type == 'ARMATURE':\n                for bone in obj.data.bones:\n                    if ':' in bone.name:\n                        invalid.append(obj)\n                        break\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            names = \", \".join(obj.name for obj in invalid)\n            raise PublishValidationError(\n                f\"Objects found with colon in name: {names}\"\n            )\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/validate_object_mode.py",
    "content": "from typing import List\n\nimport bpy\n\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\nimport openpype.hosts.blender.api.action\n\n\nclass ValidateObjectIsInObjectMode(\n        pyblish.api.InstancePlugin,\n        OptionalPyblishPluginMixin,\n):\n    \"\"\"Validate that the objects in the instance are in Object Mode.\"\"\"\n\n    order = pyblish.api.ValidatorOrder - 0.01\n    hosts = [\"blender\"]\n    families = [\"model\", \"rig\", \"layout\"]\n    label = \"Validate Object Mode\"\n    actions = [openpype.hosts.blender.api.action.SelectInvalidAction]\n    optional = False\n\n    @staticmethod\n    def get_invalid(instance) -> List:\n        invalid = []\n        for obj in instance:\n            if isinstance(obj, bpy.types.Object) and obj.mode != \"OBJECT\":\n                invalid.append(obj)\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            names = \", \".join(obj.name for obj in invalid)\n            raise PublishValidationError(\n                f\"Object found in instance is not in Object Mode: {names}\"\n            )\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py",
    "content": "import bpy\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\nclass ValidateRenderCameraIsSet(pyblish.api.InstancePlugin,\n                                OptionalPyblishPluginMixin):\n    \"\"\"Validate that there is a camera set as active for rendering.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    hosts = [\"blender\"]\n    families = [\"render\"]\n    label = \"Validate Render Camera Is Set\"\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        if not bpy.context.scene.camera:\n            raise PublishValidationError(\"No camera is active for rendering.\")\n"
  },
  {
    "path": "openpype/hosts/blender/plugins/publish/validate_transform_zero.py",
    "content": "from typing import List\n\nimport mathutils\nimport bpy\n\nimport pyblish.api\n\nimport openpype.hosts.blender.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\nclass ValidateTransformZero(pyblish.api.InstancePlugin,\n                            OptionalPyblishPluginMixin):\n    \"\"\"Transforms can't have any values\n\n    To solve this issue, try freezing the transforms. So long\n    as the transforms, rotation and scale values are zero,\n    you're all good.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"blender\"]\n    families = [\"model\"]\n    label = \"Transform Zero\"\n    actions = [openpype.hosts.blender.api.action.SelectInvalidAction]\n\n    _identity = mathutils.Matrix()\n\n    @classmethod\n    def get_invalid(cls, instance) -> List:\n        invalid = []\n        for obj in instance:\n            if (\n                isinstance(obj, bpy.types.Object)\n                and obj.matrix_basis != cls._identity\n            ):\n                invalid.append(obj)\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            names = \", \".join(obj.name for obj in invalid)\n            raise PublishValidationError(\n                \"Objects found in instance which do not\"\n                f\" have transform set to zero: {names}\"\n            )\n"
  },
  {
    "path": "openpype/hosts/celaction/__init__.py",
    "content": "from .addon import (\n    CELACTION_ROOT_DIR,\n    CelactionAddon,\n)\n\n\n__all__ = (\n    \"CELACTION_ROOT_DIR\",\n    \"CelactionAddon\",\n)\n"
  },
  {
    "path": "openpype/hosts/celaction/addon.py",
    "content": "import os\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nCELACTION_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass CelactionAddon(OpenPypeModule, IHostAddon):\n    name = \"celaction\"\n    host_name = \"celaction\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [\n            os.path.join(CELACTION_ROOT_DIR, \"hooks\")\n        ]\n\n    def add_implementation_envs(self, env, _app):\n        # Set default values if are not already set via settings\n        defaults = {\n            \"LOGLEVEL\": \"DEBUG\"\n        }\n        for key, value in defaults.items():\n            if not env.get(key):\n                env[key] = value\n\n    def get_workfile_extensions(self):\n        return [\".scn\"]\n"
  },
  {
    "path": "openpype/hosts/celaction/hooks/pre_celaction_setup.py",
    "content": "import os\nimport shutil\nimport winreg\nimport subprocess\nfrom openpype.lib import get_openpype_execute_args\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\nfrom openpype.hosts.celaction import CELACTION_ROOT_DIR\n\n\nclass CelactionPrelaunchHook(PreLaunchHook):\n    \"\"\"\n    Bootstrap celacion with pype\n    \"\"\"\n    app_groups = {\"celaction\"}\n    platforms = {\"windows\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        asset_doc = self.data[\"asset_doc\"]\n        width = asset_doc[\"data\"][\"resolutionWidth\"]\n        height = asset_doc[\"data\"][\"resolutionHeight\"]\n\n        # Add workfile path to launch arguments\n        workfile_path = self.workfile_path()\n        if workfile_path:\n            self.launch_context.launch_args.append(workfile_path)\n\n        # setting output parameters\n        path_user_settings = \"\\\\\".join([\n            \"Software\", \"CelAction\", \"CelAction2D\", \"User Settings\"\n        ])\n        winreg.CreateKey(winreg.HKEY_CURRENT_USER, path_user_settings)\n        hKey = winreg.OpenKey(\n            winreg.HKEY_CURRENT_USER, path_user_settings, 0,\n            winreg.KEY_ALL_ACCESS\n        )\n\n        path_to_cli = os.path.join(\n            CELACTION_ROOT_DIR, \"scripts\", \"publish_cli.py\"\n        )\n        subprocess_args = get_openpype_execute_args(\"run\", path_to_cli)\n        openpype_executable = subprocess_args.pop(0)\n        workfile_settings = self.get_workfile_settings()\n\n        winreg.SetValueEx(\n            hKey,\n            \"SubmitAppTitle\",\n            0,\n            winreg.REG_SZ,\n            openpype_executable\n        )\n\n        # add required arguments for workfile path\n        parameters = subprocess_args + [\n            \"--currentFile\", \"*SCENE*\"\n        ]\n\n        # Add custom parameters from workfile settings\n        if \"render_chunk\" in workfile_settings[\"submission_overrides\"]:\n            parameters += [\n                \"--chunk\", \"*CHUNK*\"\n           ]\n        if \"resolution\" in workfile_settings[\"submission_overrides\"]:\n            parameters += [\n                \"--resolutionWidth\", \"*X*\",\n                \"--resolutionHeight\", \"*Y*\"\n            ]\n        if \"frame_range\" in workfile_settings[\"submission_overrides\"]:\n            parameters += [\n                \"--frameStart\", \"*START*\",\n                \"--frameEnd\", \"*END*\"\n            ]\n\n        winreg.SetValueEx(\n            hKey, \"SubmitParametersTitle\", 0, winreg.REG_SZ,\n            subprocess.list2cmdline(parameters)\n        )\n\n        self.log.debug(f\"__ parameters: \\\"{parameters}\\\"\")\n\n        # setting resolution parameters\n        path_submit = \"\\\\\".join([\n            path_user_settings, \"Dialogs\", \"SubmitOutput\"\n        ])\n        winreg.CreateKey(winreg.HKEY_CURRENT_USER, path_submit)\n        hKey = winreg.OpenKey(\n            winreg.HKEY_CURRENT_USER, path_submit, 0,\n            winreg.KEY_ALL_ACCESS\n        )\n        winreg.SetValueEx(hKey, \"SaveScene\", 0, winreg.REG_DWORD, 1)\n        winreg.SetValueEx(hKey, \"CustomX\", 0, winreg.REG_DWORD, width)\n        winreg.SetValueEx(hKey, \"CustomY\", 0, winreg.REG_DWORD, height)\n\n        # making sure message dialogs don't appear when overwriting\n        path_overwrite_scene = \"\\\\\".join([\n            path_user_settings, \"Messages\", \"OverwriteScene\"\n        ])\n        winreg.CreateKey(winreg.HKEY_CURRENT_USER, path_overwrite_scene)\n        hKey = winreg.OpenKey(\n            winreg.HKEY_CURRENT_USER, path_overwrite_scene, 0,\n            winreg.KEY_ALL_ACCESS\n        )\n        winreg.SetValueEx(hKey, \"Result\", 0, winreg.REG_DWORD, 6)\n        winreg.SetValueEx(hKey, \"Valid\", 0, winreg.REG_DWORD, 1)\n\n        # set scane as not saved\n        path_scene_saved = \"\\\\\".join([\n            path_user_settings, \"Messages\", \"SceneSaved\"\n        ])\n        winreg.CreateKey(winreg.HKEY_CURRENT_USER, path_scene_saved)\n        hKey = winreg.OpenKey(\n            winreg.HKEY_CURRENT_USER, path_scene_saved, 0,\n            winreg.KEY_ALL_ACCESS\n        )\n        winreg.SetValueEx(hKey, \"Result\", 0, winreg.REG_DWORD, 1)\n        winreg.SetValueEx(hKey, \"Valid\", 0, winreg.REG_DWORD, 1)\n\n    def workfile_path(self):\n        workfile_path = self.data[\"last_workfile_path\"]\n\n        # copy workfile from template if doesnt exist any on path\n        if not os.path.exists(workfile_path):\n            # TODO add ability to set different template workfile path via\n            # settings\n            template_path = os.path.join(\n                CELACTION_ROOT_DIR,\n                \"resources\",\n                \"celaction_template_scene.scn\"\n            )\n\n            if not os.path.exists(template_path):\n                self.log.warning(\n                    \"Couldn't find workfile template file in {}\".format(\n                        template_path\n                    )\n                )\n                return\n\n            self.log.info(\n                f\"Creating workfile from template: \\\"{template_path}\\\"\"\n            )\n\n            # Copy template workfile to new destinantion\n            shutil.copy2(\n                os.path.normpath(template_path),\n                os.path.normpath(workfile_path)\n            )\n\n        self.log.info(f\"Workfile to open: \\\"{workfile_path}\\\"\")\n\n        return workfile_path\n\n    def get_workfile_settings(self):\n        return self.data[\"project_settings\"][\"celaction\"][\"workfile\"]\n"
  },
  {
    "path": "openpype/hosts/celaction/plugins/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/celaction/plugins/publish/collect_celaction_cli_kwargs.py",
    "content": "import pyblish.api\nimport sys\nfrom pprint import pformat\n\n\nclass CollectCelactionCliKwargs(pyblish.api.Collector):\n    \"\"\" Collects all keyword arguments passed from the terminal \"\"\"\n\n    label = \"Collect Celaction Cli Kwargs\"\n    order = pyblish.api.Collector.order - 0.1\n\n    def process(self, context):\n        args = list(sys.argv[1:])\n        self.log.info(str(args))\n        missing_kwargs = []\n        passing_kwargs = {}\n        for key in (\n            \"chunk\",\n            \"frameStart\",\n            \"frameEnd\",\n            \"resolutionWidth\",\n            \"resolutionHeight\",\n            \"currentFile\",\n        ):\n            arg_key = f\"--{key}\"\n            if arg_key not in args:\n                missing_kwargs.append(key)\n                continue\n            arg_idx = args.index(arg_key)\n            args.pop(arg_idx)\n            if key != \"currentFile\":\n                value = args.pop(arg_idx)\n            else:\n                path_parts = []\n                while arg_idx < len(args):\n                    path_parts.append(args.pop(arg_idx))\n                value = \" \".join(path_parts).strip('\"')\n\n            passing_kwargs[key] = value\n\n        if missing_kwargs:\n            self.log.debug(\"Missing arguments {}\".format(\n                \", \".join(\n                    [f'\"{key}\"' for key in missing_kwargs]\n                )\n            ))\n\n        self.log.info(\"Storing kwargs ...\")\n        self.log.debug(\"_ passing_kwargs: {}\".format(pformat(passing_kwargs)))\n\n        # set kwargs to context data\n        context.set_data(\"passingKwargs\", passing_kwargs)\n\n        # get kwargs onto context data as keys with values\n        for k, v in passing_kwargs.items():\n            self.log.info(f\"Setting `{k}` to instance.data with value: `{v}`\")\n            if k in [\"frameStart\", \"frameEnd\"]:\n                context.data[k] = passing_kwargs[k] = int(v)\n            else:\n                context.data[k] = v\n"
  },
  {
    "path": "openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py",
    "content": "import os\nimport pyblish.api\n\nfrom openpype.client import get_asset_name_identifier\n\n\nclass CollectCelactionInstances(pyblish.api.ContextPlugin):\n    \"\"\" Adds the celaction render instances \"\"\"\n\n    label = \"Collect Celaction Instances\"\n    order = pyblish.api.CollectorOrder + 0.1\n\n    def process(self, context):\n        task = context.data[\"task\"]\n        current_file = context.data[\"currentFile\"]\n        staging_dir = os.path.dirname(current_file)\n        scene_file = os.path.basename(current_file)\n        version = context.data[\"version\"]\n        asset_entity = context.data[\"assetEntity\"]\n        project_entity = context.data[\"projectEntity\"]\n\n        asset_name = get_asset_name_identifier(asset_entity)\n\n        shared_instance_data = {\n            \"asset\": asset_name,\n            \"frameStart\": asset_entity[\"data\"][\"frameStart\"],\n            \"frameEnd\": asset_entity[\"data\"][\"frameEnd\"],\n            \"handleStart\": asset_entity[\"data\"][\"handleStart\"],\n            \"handleEnd\": asset_entity[\"data\"][\"handleEnd\"],\n            \"fps\": asset_entity[\"data\"][\"fps\"],\n            \"resolutionWidth\": asset_entity[\"data\"].get(\n                \"resolutionWidth\",\n                project_entity[\"data\"][\"resolutionWidth\"]),\n            \"resolutionHeight\": asset_entity[\"data\"].get(\n                \"resolutionHeight\",\n                project_entity[\"data\"][\"resolutionHeight\"]),\n            \"pixelAspect\": 1,\n            \"step\": 1,\n            \"version\": version\n        }\n\n        celaction_kwargs = context.data.get(\n            \"passingKwargs\", {})\n\n        if celaction_kwargs:\n            shared_instance_data.update(celaction_kwargs)\n\n        # workfile instance\n        family = \"workfile\"\n        subset = family + task.capitalize()\n        # Create instance\n        instance = context.create_instance(subset)\n\n        # creating instance data\n        instance.data.update({\n            \"subset\": subset,\n            \"label\": scene_file,\n            \"family\": family,\n            \"families\": [],\n            \"representations\": []\n        })\n\n        # adding basic script data\n        instance.data.update(shared_instance_data)\n\n        # creating representation\n        representation = {\n            'name': 'scn',\n            'ext': 'scn',\n            'files': scene_file,\n            \"stagingDir\": staging_dir,\n        }\n\n        instance.data[\"representations\"].append(representation)\n\n        self.log.info('Publishing Celaction workfile')\n\n        # render instance\n        subset = f\"render{task}Main\"\n        instance = context.create_instance(name=subset)\n        # getting instance state\n        instance.data[\"publish\"] = True\n\n        # add assetEntity data into instance\n        instance.data.update({\n            \"label\": \"{} - farm\".format(subset),\n            \"family\": \"render.farm\",\n            \"families\": [],\n            \"subset\": subset\n        })\n\n        # adding basic script data\n        instance.data.update(shared_instance_data)\n\n        self.log.info('Publishing Celaction render instance')\n        self.log.debug(f\"Instance data: `{instance.data}`\")\n\n        for i in context:\n            self.log.debug(f\"{i.data['families']}\")\n"
  },
  {
    "path": "openpype/hosts/celaction/plugins/publish/collect_render_path.py",
    "content": "import os\nimport pyblish.api\nimport copy\n\n\nclass CollectRenderPath(pyblish.api.InstancePlugin):\n    \"\"\"Generate file and directory path where rendered images will be\"\"\"\n\n    label = \"Collect Render Path\"\n    order = pyblish.api.CollectorOrder + 0.495\n    families = [\"render.farm\"]\n\n    # Presets\n    output_extension = \"png\"\n    anatomy_template_key_render_files = None\n    anatomy_template_key_metadata = None\n\n    def process(self, instance):\n        anatomy = instance.context.data[\"anatomy\"]\n        anatomy_data = copy.deepcopy(instance.data[\"anatomyData\"])\n        padding = anatomy.templates.get(\"frame_padding\", 4)\n        anatomy_data.update({\n            \"frame\": f\"%0{padding}d\",\n            \"family\": \"render\",\n            \"representation\": self.output_extension,\n            \"ext\": self.output_extension\n        })\n\n        anatomy_filled = anatomy.format(anatomy_data)\n\n        # get anatomy rendering keys\n        r_anatomy_key = self.anatomy_template_key_render_files\n        m_anatomy_key = self.anatomy_template_key_metadata\n\n        # get folder and path for rendering images from celaction\n        render_dir = anatomy_filled[r_anatomy_key][\"folder\"]\n        render_path = anatomy_filled[r_anatomy_key][\"path\"]\n        self.log.debug(\"__ render_path: `{}`\".format(render_path))\n\n        # create dir if it doesnt exists\n        try:\n            if not os.path.isdir(render_dir):\n                os.makedirs(render_dir, exist_ok=True)\n        except OSError:\n            # directory is not available\n            self.log.warning(\"Path is unreachable: `{}`\".format(render_dir))\n\n        # add rendering path to instance data\n        instance.data[\"path\"] = render_path\n\n        # get anatomy for published renders folder path\n        if anatomy_filled.get(m_anatomy_key):\n            instance.data[\"publishRenderMetadataFolder\"] = anatomy_filled[\n                m_anatomy_key][\"folder\"]\n            self.log.info(\"Metadata render path: `{}`\".format(\n                instance.data[\"publishRenderMetadataFolder\"]\n            ))\n\n        self.log.info(f\"Render output path set to: `{render_path}`\")\n"
  },
  {
    "path": "openpype/hosts/celaction/plugins/publish/integrate_version_up.py",
    "content": "import shutil\nimport openpype\nimport pyblish.api\n\n\nclass VersionUpScene(pyblish.api.ContextPlugin):\n    order = pyblish.api.IntegratorOrder + 0.5\n    label = 'Version Up Scene'\n    families = ['workfile']\n    optional = True\n    active = True\n\n    def process(self, context):\n        current_file = context.data.get('currentFile')\n        v_up = openpype.lib.version_up(current_file)\n        self.log.debug('Current file is: {}'.format(current_file))\n        self.log.debug('Version up: {}'.format(v_up))\n\n        shutil.copy2(current_file, v_up)\n        self.log.info('Scene saved into new version: {}'.format(v_up))\n"
  },
  {
    "path": "openpype/hosts/celaction/scripts/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/celaction/scripts/publish_cli.py",
    "content": "import os\nimport sys\n\nimport pyblish.api\nimport pyblish.util\n\nimport openpype.hosts.celaction\nfrom openpype.lib import Logger\nfrom openpype.tools.utils import host_tools\nfrom openpype.pipeline import install_openpype_plugins\n\n\nlog = Logger.get_logger(\"celaction\")\n\nPUBLISH_HOST = \"celaction\"\nHOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.celaction.__file__))\nPLUGINS_DIR = os.path.join(HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\n\n\ndef main():\n    # Registers pype's Global pyblish plugins\n    install_openpype_plugins()\n\n    if os.path.exists(PUBLISH_PATH):\n        log.info(f\"Registering path: {PUBLISH_PATH}\")\n        pyblish.api.register_plugin_path(PUBLISH_PATH)\n\n    pyblish.api.register_host(PUBLISH_HOST)\n    pyblish.api.register_target(\"local\")\n\n    return host_tools.show_publish()\n\n\nif __name__ == \"__main__\":\n    result = main()\n    sys.exit(not bool(result))\n"
  },
  {
    "path": "openpype/hosts/equalizer/__init__.py",
    "content": "from .addon import (\n    EqualizerAddon,\n    EQUALIZER_HOST_DIR,\n)\n\n__all__ = [\n    \"EqualizerAddon\",\n    \"EQUALIZER_HOST_DIR\",\n]\n"
  },
  {
    "path": "openpype/hosts/equalizer/addon.py",
    "content": "import os\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nEQUALIZER_HOST_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass EqualizerAddon(OpenPypeModule, IHostAddon):\n    name = \"equalizer\"\n    host_name = \"equalizer\"\n    heartbeat = 500\n\n    def initialize(self, module_settings):\n        self.heartbeat = module_settings.get(\"heartbeat_interval\", 500)\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        # 3dEqualizer utilize TDE4_ROOT for its root directory\n        # and PYTHON_CUSTOM_SCRIPTS_3DE4 as a colon separated list of\n        # directories to look for additional python scripts.\n        # (Windows: list is separated by semicolons).\n        # Ad\n\n        startup_path = os.path.join(EQUALIZER_HOST_DIR, \"startup\")\n        if \"PYTHON_CUSTOM_SCRIPTS_3DE4\" in env:\n            startup_path = os.path.join(\n                env[\"PYTHON_CUSTOM_SCRIPTS_3DE4\"],\n                startup_path)\n\n        env[\"PYTHON_CUSTOM_SCRIPTS_3DE4\"] = startup_path\n        env[\"AYON_TDE4_HEARTBEAT_INTERVAL\"] = str(self.heartbeat)\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [\n            os.path.join(EQUALIZER_HOST_DIR, \"hooks\")\n        ]\n\n    def get_workfile_extensions(self):\n        return [\".3de\"]\n"
  },
  {
    "path": "openpype/hosts/equalizer/api/__init__.py",
    "content": "from .host import EqualizerHost\nfrom .plugin import EqualizerCreator, ExtractScriptBase\nfrom .pipeline import Container, maintained_model_selection\n\n__all__ = [\n    \"EqualizerHost\",\n    \"EqualizerCreator\",\n    \"Container\",\n    \"ExtractScriptBase\",\n    \"maintained_model_selection\",\n]\n"
  },
  {
    "path": "openpype/hosts/equalizer/api/host.py",
    "content": "\"\"\"3dequalizer host implementation.\n\nnote:\n    3dequalizer 7.1v2 uses Python 3.7.9\n\n\"\"\"\nimport json\nimport os\nimport re\n\nimport pyblish.api\nimport tde4  # noqa: F401\nfrom attrs import asdict\nfrom attrs.exceptions import NotAnAttrsClassError\nfrom qtpy import QtCore, QtWidgets\n\nfrom openpype.host import HostBase, ILoadHost, IPublishHost, IWorkfileHost\nfrom openpype.hosts.equalizer import EQUALIZER_HOST_DIR\nfrom openpype.hosts.equalizer.api.pipeline import Container\nfrom openpype.pipeline import (\n    register_creator_plugin_path,\n    register_loader_plugin_path,\n)\n\nCONTEXT_REGEX = re.compile(\n    r\"AYON_CONTEXT::(?P<context>.*?)::AYON_CONTEXT_END\",\n    re.DOTALL)\nPLUGINS_DIR = os.path.join(EQUALIZER_HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\nINVENTORY_PATH = os.path.join(PLUGINS_DIR, \"inventory\")\n\n\nclass EqualizerHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):\n    name = \"equalizer\"\n    _instance = None\n\n    def __new__(cls):\n        # singleton - ensure only one instance of the host is created.\n        # This is necessary because 3DEqualizer doesn't have a way to\n        # store custom data, so we need to store it in the project notes.\n        if not hasattr(cls, \"_instance\") or not cls._instance:\n            cls._instance = super(EqualizerHost, cls).__new__(cls)\n        return cls._instance\n\n    def __init__(self):\n        self._qapp = None\n        super(EqualizerHost, self).__init__()\n\n    def workfile_has_unsaved_changes(self):\n        \"\"\"Return the state of the current workfile.\n\n        3DEqualizer returns state as 1 or zero, so we need to invert it.\n\n        Returns:\n            bool: True if the current workfile has unsaved changes.\n        \"\"\"\n        return not bool(tde4.isProjectUpToDate())\n\n    def get_workfile_extensions(self):\n        return [\".3de\"]\n\n    def save_workfile(self, dst_path=None):\n        if not dst_path:\n            dst_path = tde4.getProjectPath()\n        result = tde4.saveProject(dst_path, True)\n        if not bool(result):\n            raise RuntimeError(f\"Failed to save workfile {dst_path}.\")\n\n        return dst_path\n\n    def open_workfile(self, filepath):\n        result = tde4.loadProject(filepath, True)\n        if not bool(result):\n            raise RuntimeError(f\"Failed to open workfile {filepath}.\")\n\n        return filepath\n\n    def get_current_workfile(self):\n        return tde4.getProjectPath()\n\n    def get_containers(self):\n        context = self.get_context_data()\n        if context:\n            return context.get(\"containers\", [])\n        return []\n\n    def add_container(self, container: Container):\n        context_data = self.get_context_data()\n        containers = self.get_containers()\n\n        for _container in containers:\n            if _container[\"name\"] == container.name and _container[\"namespace\"] == container.namespace:  # noqa: E501\n                containers.remove(_container)\n                break\n\n        try:\n            containers.append(asdict(container))\n        except NotAnAttrsClassError:\n            print(\"not an attrs class\")\n            containers.append(container)\n\n        context_data[\"containers\"] = containers\n        self.update_context_data(context_data, changes={})\n\n    def get_context_data(self) -> dict:\n        \"\"\"Get context data from the current workfile.\n\n        3Dequalizer doesn't have any custom node or other\n        place to store metadata, so we store context data in\n        the project notes encoded as JSON and wrapped in a\n        special guard string `AYON_CONTEXT::...::AYON_CONTEXT_END`.\n\n        Returns:\n            dict: Context data.\n        \"\"\"\n\n        # sourcery skip: use-named-expression\n        m = re.search(CONTEXT_REGEX, tde4.getProjectNotes())\n        try:\n            context = json.loads(m[\"context\"]) if m else {}\n        except ValueError:\n            self.log.debug(\"context data is not valid json\")\n            context = {}\n\n        return context\n\n    def update_context_data(self, data, changes):\n        \"\"\"Update context data in the current workfile.\n\n        Serialize context data as json and store it in the\n        project notes. If the context data is not found, create\n        a placeholder there. See `get_context_data` for more info.\n\n        Args:\n            data (dict): Context data.\n            changes (dict): Changes to the context data.\n\n        Raises:\n            RuntimeError: If the context data is not found.\n        \"\"\"\n        notes = tde4.getProjectNotes()\n        m = re.search(CONTEXT_REGEX, notes)\n        if not m:\n            # context data not found, create empty placeholder\n            tde4.setProjectNotes(\n                f\"{tde4.getProjectNotes()}\\n\"\n                f\"AYON_CONTEXT::::AYON_CONTEXT_END\\n\")\n\n        original_data = self.get_context_data()\n\n        updated_data = original_data.copy()\n        updated_data.update(data)\n        update_str = json.dumps(updated_data or {}, indent=4)\n\n        tde4.setProjectNotes(\n            re.sub(\n                CONTEXT_REGEX,\n                f\"AYON_CONTEXT::{update_str}::AYON_CONTEXT_END\",\n                tde4.getProjectNotes()\n            )\n        )\n        tde4.updateGUI()\n\n    def install(self):\n        if not QtCore.QCoreApplication.instance():\n            app = QtWidgets.QApplication([])\n            self._qapp = app\n            self._qapp.setQuitOnLastWindowClosed(False)\n\n        pyblish.api.register_host(\"equalizer\")\n\n        pyblish.api.register_plugin_path(PUBLISH_PATH)\n        register_loader_plugin_path(LOAD_PATH)\n        register_creator_plugin_path(CREATE_PATH)\n\n        heartbeat_interval = os.getenv(\"AYON_TDE4_HEARTBEAT_INTERVAL\") or 500\n        tde4.setTimerCallbackFunction(\n            \"EqualizerHost._timer\", int(heartbeat_interval))\n\n    @staticmethod\n    def _timer():\n        QtWidgets.QApplication.instance().processEvents(\n            QtCore.QEventLoop.AllEvents)\n\n    @classmethod\n    def get_host(cls):\n        return cls._instance\n\n    def get_main_window(self):\n        return self._qapp.activeWindow()\n"
  },
  {
    "path": "openpype/hosts/equalizer/api/pipeline.py",
    "content": "from attrs import field, define\nfrom openpype.pipeline import AVALON_CONTAINER_ID\nimport contextlib\nimport tde4\n\n\n@define\nclass Container(object):\n\n    name: str = field(default=None)\n    id: str = field(init=False, default=AVALON_CONTAINER_ID)\n    namespace: str = field(default=\"\")\n    loader: str = field(default=None)\n    representation: str = field(default=None)\n\n\n@contextlib.contextmanager\ndef maintained_model_selection():\n    \"\"\"Maintain model selection during context.\"\"\"\n\n    point_groups = tde4.getPGroupList()\n    point_group = next(\n        (\n            pg for pg in point_groups\n            if tde4.getPGroupType(pg) == \"CAMERA\"\n        ), None\n    )\n    selected_models = tde4.get3DModelList(point_group, 1)\\\n        if point_group else []\n    try:\n        yield\n    finally:\n        if point_group:\n            # 3 restore model selection\n            for model in tde4.get3DModelList(point_group, 0):\n                if model in selected_models:\n                    tde4.set3DModelSelectionFlag(point_group, model, 1)\n                else:\n                    tde4.set3DModelSelectionFlag(point_group, model, 0)\n"
  },
  {
    "path": "openpype/hosts/equalizer/api/plugin.py",
    "content": "\"\"\"Base plugin class for 3DEqualizer.\n\nnote:\n    3dequalizer 7.1v2 uses Python 3.7.9\n\n\"\"\"\nfrom abc import ABC\nfrom typing import Dict, List\n\nfrom openpype.hosts.equalizer.api import EqualizerHost\nfrom openpype.lib import BoolDef, EnumDef, NumberDef\nfrom openpype.pipeline import (\n    CreatedInstance,\n    Creator,\n    OptionalPyblishPluginMixin,\n)\n\n\nclass EqualizerCreator(ABC, Creator):\n\n    @property\n    def host(self) -> EqualizerHost:\n        \"\"\"Return the host application.\"\"\"\n        # We need to cast the host to EqualizerHost, because the Creator\n        # class is not aware of the host application.\n        return super().host\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        \"\"\"Create a subset in the host application.\n\n        Args:\n            subset_name (str): Name of the subset to create.\n            instance_data (dict): Data of the instance to create.\n            pre_create_data (dict): Data from the pre-create step.\n\n        Returns:\n            openpype.pipeline.CreatedInstance: Created instance.\n        \"\"\"\n        self.log.debug(\"EqualizerCreator.create\")\n        instance = CreatedInstance(\n            self.family,\n            subset_name,\n            instance_data,\n            self)\n        self._add_instance_to_context(instance)\n        return instance\n\n    def collect_instances(self):\n        \"\"\"Collect instances from the host application.\n\n        Returns:\n            list[openpype.pipeline.CreatedInstance]: List of instances.\n        \"\"\"\n        for instance_data in self.host.get_context_data().get(\n                \"publish_instances\", []):\n            created_instance = CreatedInstance.from_existing(\n                instance_data, self\n            )\n            self._add_instance_to_context(created_instance)\n\n    def update_instances(self, update_list):\n\n        # if not update_list:\n        #     return\n        context = self.host.get_context_data()\n        if not context.get(\"publish_instances\"):\n            context[\"publish_instances\"] = []\n\n        instances_by_id = {}\n        for instance in context.get(\"publish_instances\"):\n            # sourcery skip: use-named-expression\n            instance_id = instance.get(\"instance_id\")\n            if instance_id:\n                instances_by_id[instance_id] = instance\n\n        for instance, changes in update_list:\n            new_instance_data = changes.new_value\n            instance_data = instances_by_id.get(instance.id)\n            # instance doesn't exist, append everything\n            if instance_data is None:\n                context[\"publish_instances\"].append(new_instance_data)\n                continue\n\n            # update only changed values on instance\n            for key in set(instance_data) - set(new_instance_data):\n                instance_data.pop(key)\n                instance_data.update(new_instance_data)\n\n        self.host.update_context_data(context, changes=update_list)\n\n    def remove_instances(self, instances: List[Dict]):\n        context = self.host.get_context_data()\n        if not context.get(\"publish_instances\"):\n            context[\"publish_instances\"] = []\n\n        ids_to_remove = [\n            instance.get(\"instance_id\")\n            for instance in instances\n        ]\n        for instance in context.get(\"publish_instances\"):\n            if instance.get(\"instance_id\") in ids_to_remove:\n                context[\"publish_instances\"].remove(instance)\n\n        self.host.update_context_data(context, changes={})\n\n\nclass ExtractScriptBase(OptionalPyblishPluginMixin):\n    \"\"\"Base class for extract script plugins.\"\"\"\n\n    hide_reference_frame = False\n    export_uv_textures = False\n    overscan_percent_width = 100\n    overscan_percent_height = 100\n    units = \"mm\"\n\n    @classmethod\n    def apply_settings(cls, project_settings, system_settings):\n        settings = project_settings[\"equalizer\"][\"publish\"][\n            \"ExtractMatchmoveScriptMaya\"]  # noqa\n\n        cls.hide_reference_frame = settings.get(\n            \"hide_reference_frame\", cls.hide_reference_frame)\n        cls.export_uv_textures = settings.get(\n            \"export_uv_textures\", cls.export_uv_textures)\n        cls.overscan_percent_width = settings.get(\n            \"overscan_percent_width\", cls.overscan_percent_width)\n        cls.overscan_percent_height = settings.get(\n            \"overscan_percent_height\", cls.overscan_percent_height)\n        cls.units = settings.get(\"units\", cls.units)\n\n    @classmethod\n    def get_attribute_defs(cls):\n        defs = super(ExtractScriptBase, cls).get_attribute_defs()\n\n        defs.extend([\n            BoolDef(\"hide_reference_frame\",\n                    label=\"Hide Reference Frame\",\n                    default=cls.hide_reference_frame),\n            BoolDef(\"export_uv_textures\",\n                    label=\"Export UV Textures\",\n                    default=cls.export_uv_textures),\n            NumberDef(\"overscan_percent_width\",\n                      label=\"Overscan Width %\",\n                      default=cls.overscan_percent_width,\n                      decimals=0,\n                      minimum=1,\n                      maximum=1000),\n            NumberDef(\"overscan_percent_height\",\n                      label=\"Overscan Height %\",\n                      default=cls.overscan_percent_height,\n                      decimals=0,\n                      minimum=1,\n                      maximum=1000),\n            EnumDef(\"units\",\n                    [\"mm\", \"cm\", \"m\", \"in\", \"ft\", \"yd\"],\n                    default=cls.units,\n                    label=\"Units\"),\n        ])\n        return defs\n"
  },
  {
    "path": "openpype/hosts/equalizer/hooks/pre_pyside2_install.py",
    "content": "\"\"\"Install PySide2 python module to 3dequalizer's python.\n\nIf 3dequalizer doesn't have PySide2 module installed, it will try to install\nit.\n\nNote:\n    This needs to be changed in the future so the UI is decoupled from the\n    host application.\n\n\"\"\"\n\nimport contextlib\nimport os\nimport subprocess\nfrom pathlib import Path\nfrom platform import system\n\nfrom openpype.lib.applications import LaunchTypes, PreLaunchHook\n\n\nclass InstallPySide2(PreLaunchHook):\n    \"\"\"Install Qt binding to 3dequalizer's python packages.\"\"\"\n\n    app_groups = {\"3dequalizer\", \"sdv_3dequalizer\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        try:\n            self._execute()\n        except Exception:\n            self.log.warning((\n                f\"Processing of {self.__class__.__name__} \"\n                \"crashed.\"), exc_info=True\n            )\n\n    def _execute(self):\n        platform = system().lower()\n        executable = Path(self.launch_context.executable.executable_path)\n        expected_executable = \"3de4\"\n        if platform == \"windows\":\n            expected_executable += \".exe\"\n\n        if not self.launch_context.env.get(\"TDE4_HOME\"):\n            if executable.name.lower() != expected_executable:\n                self.log.warning((\n                    f\"Executable {executable.as_posix()} does not lead \"\n                    f\"to {expected_executable} file. \"\n                    \"Can't determine 3dequalizer's python to \"\n                    f\"check/install PySide2. {executable.name}\"\n                ))\n                return\n            python_dir = executable.parent.parent / \"sys_data\" / \"py37_inst\"\n        else:\n            python_dir = Path(self.launch_context.env[\"TDE4_HOME\"]) / \"sys_data\" / \"py37_inst\"  # noqa: E501\n\n        if platform == \"windows\":\n            python_executable = python_dir / \"python.exe\"\n        else:\n            python_executable = python_dir / \"python\"\n            # Check for python with enabled 'pymalloc'\n            if not python_executable.exists():\n                python_executable = python_dir / \"pythonm\"\n\n        if not python_executable.exists():\n            self.log.warning(\n                \"Couldn't find python executable \"\n                f\"for 3de4 {python_executable.as_posix()}\"\n            )\n\n            return\n\n        # Check if PySide2 is installed and skip if yes\n        if self.is_pyside_installed(python_executable):\n            self.log.debug(\"3Dequalizer has already installed PySide2.\")\n            return\n\n        # Install PySide2 in 3de4's python\n        if platform == \"windows\":\n            result = self.install_pyside_windows(python_executable)\n        else:\n            result = self.install_pyside(python_executable)\n\n        if result:\n            self.log.info(\"Successfully installed PySide2 module to 3de4.\")\n        else:\n            self.log.warning(\"Failed to install PySide2 module to 3de4.\")\n\n    def install_pyside_windows(self, python_executable: Path):\n        \"\"\"Install PySide2 python module to 3de4's python.\n\n        Installation requires administration rights that's why it is required\n        to use \"pywin32\" module which can execute command's and ask for\n        administration rights.\n\n        Note:\n            This is asking for administrative right always, no matter if\n            it is actually needed or not. Unfortunately getting\n            correct permissions for directory on Windows isn't that trivial.\n            You can either use `win32security` module or run `icacls` command\n            in subprocess and parse its output.\n\n        \"\"\"\n        try:\n            import pywintypes\n            import win32con\n            import win32event\n            import win32process\n            from win32comext.shell import shellcon\n            from win32comext.shell.shell import ShellExecuteEx\n        except Exception:\n            self.log.warning(\"Couldn't import 'pywin32' modules\")\n            return\n\n        with contextlib.suppress(pywintypes.error):\n            # Parameters\n            # - use \"-m pip\" as module pip to install PySide2 and argument\n            #   \"--ignore-installed\" is to force install module to 3de4's\n            #   site-packages and make sure it is binary compatible\n            parameters = \"-m pip install --ignore-installed PySide2\"\n\n            # Execute command and ask for administrator's rights\n            process_info = ShellExecuteEx(\n                nShow=win32con.SW_SHOWNORMAL,\n                fMask=shellcon.SEE_MASK_NOCLOSEPROCESS,\n                lpVerb=\"runas\",\n                lpFile=python_executable.as_posix(),\n                lpParameters=parameters,\n                lpDirectory=python_executable.parent.as_posix()\n            )\n            process_handle = process_info[\"hProcess\"]\n            win32event.WaitForSingleObject(\n                process_handle, win32event.INFINITE)\n            return_code = win32process.GetExitCodeProcess(process_handle)\n            return return_code == 0\n\n    def install_pyside(self, python_executable: Path):\n        \"\"\"Install PySide2 python module to 3de4's python.\"\"\"\n\n        args = [\n            python_executable.as_posix(),\n            \"-m\",\n            \"pip\",\n            \"install\",\n            \"--ignore-installed\",\n            \"PySide2\",\n        ]\n\n        try:\n            # Parameters\n            # - use \"-m pip\" as module pip to install PySide2 and argument\n            #   \"--ignore-installed\" is to force install module to 3de4\n            #   site-packages and make sure it is binary compatible\n\n            process = subprocess.Popen(\n                args, stdout=subprocess.PIPE, universal_newlines=True\n            )\n            process.communicate()\n            return process.returncode == 0\n        except PermissionError:\n            self.log.warning(\n                f'Permission denied with command:\\\"{\" \".join(args)}\\\".')\n        except OSError as error:\n            self.log.warning(f\"OS error has occurred: \\\"{error}\\\".\")\n        except subprocess.SubprocessError:\n            pass\n\n    @staticmethod\n    def is_pyside_installed(python_executable: Path) -> bool:\n        \"\"\"Check if PySide2 module is in 3de4 python env.\n\n        Args:\n            python_executable (Path): Path to python executable.\n\n        Returns:\n            bool: True if PySide2 is installed, False otherwise.\n\n        \"\"\"\n        # Get pip list from 3de4's python executable\n        args = [python_executable.as_posix(), \"-m\", \"pip\", \"list\"]\n        process = subprocess.Popen(args, stdout=subprocess.PIPE)\n        stdout, _ = process.communicate()\n        lines = stdout.decode().split(os.linesep)\n        # Second line contain dashes that define maximum length of module name.\n        #   Second column of dashes define maximum length of module version.\n        package_dashes, *_ = lines[1].split(\" \")\n        package_len = len(package_dashes)\n\n        # Got through printed lines starting at line 3\n        for idx in range(2, len(lines)):\n            line = lines[idx]\n            if not line:\n                continue\n            package_name = line[:package_len].strip()\n            if package_name.lower() == \"pyside2\":\n                return True\n        return False\n"
  },
  {
    "path": "openpype/hosts/equalizer/plugins/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/equalizer/plugins/create/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/equalizer/plugins/create/create_lens_distortion_data.py",
    "content": "from openpype.hosts.equalizer.api import EqualizerCreator\n\n\nclass CreateLensDistortionData(EqualizerCreator):\n    identifier = \"io.openpype.creators.equalizer.lens_distortion\"\n    label = \"Lens Distortion\"\n    family = \"lensDistortion\"\n    icon = \"glasses\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        super().create(subset_name, instance_data, pre_create_data)\n"
  },
  {
    "path": "openpype/hosts/equalizer/plugins/create/create_matchmove.py",
    "content": "import tde4\n\nfrom openpype.hosts.equalizer.api import EqualizerCreator\nfrom openpype.lib import EnumDef\n\n\nclass CreateMatchMove(EqualizerCreator):\n    identifier = \"io.openpype.creators.equalizer.matchmove\"\n    label = \"Match Move\"\n    family = \"matchmove\"\n    icon = \"camera\"\n\n    def get_instance_attr_defs(self):\n        camera_enum = [\n            {\"value\": \"__all__\", \"label\": \"All Cameras\"},\n            {\"value\": \"__current__\", \"label\": \"Current Camera\"},\n            {\"value\": \"__ref__\", \"label\": \"Reference Cameras\"},\n            {\"value\": \"__seq__\", \"label\": \"Sequence Cameras\"},\n        ]\n        camera_list = tde4.getCameraList()\n        camera_enum.extend(\n            {\"label\": tde4.getCameraName(camera), \"value\": camera}\n            for camera in camera_list\n            if tde4.getCameraEnabledFlag(camera)\n        )\n        # try to get list of models\n        model_enum = [\n            {\"value\": \"__none__\", \"label\": \"No 3D Models At All\"},\n            {\"value\": \"__all__\", \"label\": \"All 3D Models\"},\n        ]\n        point_groups = tde4.getPGroupList()\n        for point_group in point_groups:\n            model_list = tde4.get3DModelList(point_group, 0)\n            model_enum.extend(\n                {\n                    \"label\": tde4.get3DModelName(point_group, model),\n                    \"value\": model\n                } for model in model_list\n            )\n        return [\n            EnumDef(\"camera_selection\",\n                    items=camera_enum,\n                    default=\"__current__\",\n                    label=\"Camera(s) to publish\",\n                    tooltip=\"Select cameras to publish\"),\n            EnumDef(\"model_selection\",\n                    items=model_enum,\n                    default=\"__none__\",\n                    label=\"Model(s) to publish\",\n                    tooltip=\"Select models to publish\"),\n        ]\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        self.log.debug(\"CreateMatchMove.create\")\n        super().create(subset_name, instance_data, pre_create_data)\n"
  },
  {
    "path": "openpype/hosts/equalizer/plugins/load/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/equalizer/plugins/load/load_plate.py",
    "content": "\"\"\"Loader for image sequences.\n\nThis loads published sequence to the current camera\nbecause this workflow is the most common in production.\n\nIf current camera is not defined, it will try to use first camera and\nif there is no camera at all, it will create new one.\n\nTODO:\n    * Support for setting handles, calculation frame ranges, EXR\n      options, etc.\n    * Add support for color management - at least put correct gamma\n      to image corrections.\n\n\"\"\"\nimport tde4\n\nimport openpype.pipeline.load as load\nfrom openpype.client import get_version_by_id\nfrom openpype.hosts.equalizer.api import Container, EqualizerHost\nfrom openpype.lib.transcoding import IMAGE_EXTENSIONS\nfrom openpype.pipeline import (\n    get_current_project_name,\n    get_representation_context,\n)\n\n\nclass LoadPlate(load.LoaderPlugin):\n    families = [\n        \"imagesequence\",\n        \"review\",\n        \"render\",\n        \"plate\",\n        \"image\",\n        \"online\",\n    ]\n\n    representations = [\"*\"]\n    extensions = {ext.lstrip(\".\") for ext in IMAGE_EXTENSIONS}\n\n    label = \"Load sequence\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, options=None):\n        representation = context[\"representation\"]\n        project_name = get_current_project_name()\n        version = get_version_by_id(project_name, representation[\"parent\"])\n\n        file_path = self.file_path(representation, context)\n\n        camera = tde4.createCamera(\"SEQUENCE\")\n        tde4.setCameraName(camera, name)\n        camera_name = tde4.getCameraName(camera)\n\n        print(\n            f\"Loading: {file_path} into {camera_name}\")\n\n        # set the path to sequence on the camera\n        tde4.setCameraPath(camera, file_path)\n\n        # set the sequence attributes star/end/step\n        tde4.setCameraSequenceAttr(\n            camera, int(version[\"data\"].get(\"frameStart\")),\n            int(version[\"data\"].get(\"frameEnd\")), 1)\n\n        container = Container(\n            name=name,\n            namespace=camera_name,\n            loader=self.__class__.__name__,\n            representation=str(representation[\"_id\"]),\n        )\n        print(container)\n        EqualizerHost.get_host().add_container(container)\n\n    def update(self, container, representation):\n        camera_list = tde4.getCameraList()\n        try:\n            camera = [\n                c for c in camera_list if\n                tde4.getCameraName(c) == container[\"namespace\"]\n            ][0]\n        except IndexError:\n            self.log.error(f'Cannot find camera {container[\"namespace\"]}')\n            print(f'Cannot find camera {container[\"namespace\"]}')\n            return\n\n        context = get_representation_context(representation)\n        file_path = self.file_path(representation, context)\n\n        # set the path to sequence on the camera\n        tde4.setCameraPath(camera, file_path)\n\n        version = get_version_by_id(\n            get_current_project_name(), representation[\"parent\"])\n\n        # set the sequence attributes star/end/step\n        tde4.setCameraSequenceAttr(\n            camera, int(version[\"data\"].get(\"frameStart\")),\n            int(version[\"data\"].get(\"frameEnd\")), 1)\n\n        print(container)\n        EqualizerHost.get_host().add_container(container)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def file_path(self, representation, context):\n        is_sequence = len(representation[\"files\"]) > 1\n        print(f\"is sequence {is_sequence}\")\n        if is_sequence:\n            frame = representation[\"context\"][\"frame\"]\n            hashes = \"#\" * len(str(frame))\n            if (\n                    \"{originalBasename}\" in representation[\"data\"][\"template\"]\n            ):\n                origin_basename = context[\"originalBasename\"]\n                context[\"originalBasename\"] = origin_basename.replace(\n                    frame, hashes\n                )\n\n            # Replace the frame with the hash in the frame\n            representation[\"context\"][\"frame\"] = hashes\n\n        return self.filepath_from_context(context)\n"
  },
  {
    "path": "openpype/hosts/equalizer/plugins/publish/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/equalizer/plugins/publish/collect_3de_installation_dir.py",
    "content": "\"\"\"Collect camera data from the scene.\"\"\"\nimport pyblish.api\nimport tde4\nfrom pathlib import Path\n\n\nclass Collect3DE4InstallationDir(pyblish.api.InstancePlugin):\n    \"\"\"Collect camera data from the scene.\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    hosts = [\"equalizer\"]\n    label = \"Collect 3Dequalizer directory\"\n\n    def process(self, instance):\n        tde4_path = Path(tde4.get3DEInstallPath())\n        instance.data[\"tde4_path\"] = tde4_path\n"
  },
  {
    "path": "openpype/hosts/equalizer/plugins/publish/collect_camera_data.py",
    "content": "\"\"\"Collect camera data from the scene.\"\"\"\nimport pyblish.api\nimport tde4\n\n\nclass CollectCameraData(pyblish.api.InstancePlugin):\n    \"\"\"Collect camera data from the scene.\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    families = [\"matchmove\"]\n    hosts = [\"equalizer\"]\n    label = \"Collect camera data\"\n\n    def process(self, instance: pyblish.api.Instance):\n        # handle camera selection.\n        # possible values are:\n        #   - __current__ - current camera\n        #   - __ref__ - reference cameras\n        #   - __seq__ - sequence cameras\n        #   - __all__ - all cameras\n        #   - camera_id - specific camera\n\n        try:\n            camera_sel = instance.data[\"creator_attributes\"][\"camera_selection\"]  # noqa: E501\n        except KeyError:\n            self.log.warning(\"No camera defined\")\n            return\n\n        if camera_sel == \"__all__\":\n            cameras = tde4.getCameraList()\n        elif camera_sel == \"__current__\":\n            cameras = [tde4.getCurrentCamera()]\n        elif camera_sel in [\"__ref__\", \"__seq__\"]:\n            cameras = [\n                c for c in tde4.getCameraList()\n                if tde4.getCameraType(c) == \"REF_FRAME\"\n            ]\n        else:\n            if camera_sel not in tde4.getCameraList():\n                self.log.warning(\"Invalid camera found\")\n                return\n            cameras = [camera_sel]\n\n        data = []\n\n        for camera in cameras:\n            camera_name = tde4.getCameraName(camera)\n            enabled = tde4.getCameraEnabledFlag(camera)\n            # calculation range\n            c_range_start, c_range_end = tde4.getCameraCalculationRange(\n                camera)\n            p_range_start, p_range_end = tde4.getCameraPlaybackRange(camera)\n            fov = tde4.getCameraFOV(camera)\n            fps = tde4.getCameraFPS(camera)\n            # focal length is time based, so lets skip it for now\n            # focal_length = tde4.getCameraFocalLength(camera, frame)\n            path = tde4.getCameraPath(camera)\n\n            camera_data = {\n                \"name\": camera_name,\n                \"id\": camera,\n                \"enabled\": enabled,\n                \"calculation_range\": (c_range_start, c_range_end),\n                \"playback_range\": (p_range_start, p_range_end),\n                \"fov\": fov,\n                \"fps\": fps,\n                # \"focal_length\": focal_length,\n                \"path\": path\n            }\n            data.append(camera_data)\n        instance.data[\"cameras\"] = data\n"
  },
  {
    "path": "openpype/hosts/equalizer/plugins/publish/collect_workfile.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect current work file.\"\"\"\nfrom pathlib import Path\n\nimport pyblish.api\nimport tde4\n\n\nclass CollectWorkfile(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current working file into context\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.01\n    label = \"Collect 3DE4 Workfile\"\n    hosts = ['equalizer']\n\n    def process(self, context: pyblish.api.Context):\n        \"\"\"Inject the current working file.\"\"\"\n        project_file = Path(tde4.getProjectPath())\n        current_file = project_file.as_posix()\n\n        context.data['currentFile'] = current_file\n\n        filename = project_file.stem\n        ext = project_file.suffix\n\n        task = context.data[\"task\"]\n\n        data = {}\n\n        # create instance\n        instance = context.create_instance(name=filename)\n        subset = f'workfile{task.capitalize()}'\n\n        data = {\n            \"subset\": subset,\n            \"asset\": context.data[\"asset\"],\n            \"label\": subset,\n            \"publish\": True,\n            \"family\": 'workfile',\n            \"families\": ['workfile'],\n            \"setMembers\": [current_file],\n            \"frameStart\": context.data['frameStart'],\n            \"frameEnd\": context.data['frameEnd'],\n            \"handleStart\": context.data['handleStart'],\n            \"handleEnd\": context.data['handleEnd'],\n            \"representations\": [\n                {\n                    \"name\": ext.lstrip(\".\"),\n                    \"ext\": ext.lstrip(\".\"),\n                    \"files\": project_file.name,\n                    \"stagingDir\": project_file.parent.as_posix(),\n                }\n            ]\n        }\n\n        instance.data.update(data)\n\n        self.log.info(f'Collected instance: {project_file.name}')\n        self.log.info(f'Scene path: {current_file}')\n        self.log.info(f'staging Dir: {project_file.parent.as_posix()}')\n        self.log.info(f'subset: {subset}')\n"
  },
  {
    "path": "openpype/hosts/equalizer/plugins/publish/extract_lens_distortion_nuke.py",
    "content": "from pathlib import Path\n\nimport pyblish.api\nimport tde4  # noqa: F401\n\nfrom openpype.lib import import_filepath\nfrom openpype.pipeline import OptionalPyblishPluginMixin, publish\n\n\nclass ExtractLensDistortionNuke(publish.Extractor,\n                                OptionalPyblishPluginMixin):\n    \"\"\"Extract Nuke script for matchmove.\n\n    Unfortunately built-in export script from 3DEqualizer is bound to its UI,\n    and it is not possible to call it directly from Python. Because of that,\n    we are executing the script in the same way as artist would do it, but\n    we are patching the UI to silence it and to avoid any user interaction.\n\n    TODO: Utilize attributes defined in ExtractScriptBase\n    \"\"\"\n\n    label = \"Extract Lens Distortion Nuke node\"\n    families = [\"lensDistortion\"]\n    hosts = [\"equalizer\"]\n\n    order = pyblish.api.ExtractorOrder\n\n    def process(self, instance: pyblish.api.Instance):\n\n        if not self.is_active(instance.data):\n            return\n\n        cam = tde4.getCurrentCamera()\n        offset = tde4.getCameraFrameOffset(cam)\n        staging_dir = self.staging_dir(instance)\n        file_path = Path(staging_dir) / \"nuke_ld_export.nk\"\n\n        # import export script from 3DEqualizer\n        exporter_path = instance.data[\"tde4_path\"] / \"sys_data\" / \"py_scripts\" / \"export_nuke_LD_3DE4_Lens_Distortion_Node.py\"  # noqa: E501\n        self.log.debug(f\"Importing {exporter_path.as_posix()}\")\n        exporter = import_filepath(exporter_path.as_posix())\n        exporter.exportNukeDewarpNode(cam, offset, file_path.as_posix())\n\n        # create representation data\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': \"lensDistortion\",\n            'ext': \"nk\",\n            'files': file_path.name,\n            \"stagingDir\": staging_dir,\n        }\n        self.log.debug(f\"output: {file_path.as_posix()}\")\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/equalizer/plugins/publish/extract_matchmove_script_maya.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract project for Maya\"\"\"\n\nfrom pathlib import Path\n\nimport pyblish.api\nimport tde4\n\nfrom openpype.hosts.equalizer.api import (\n    ExtractScriptBase,\n    maintained_model_selection,\n)\nfrom openpype.lib import import_filepath\nfrom openpype.pipeline import (\n    KnownPublishError,\n    OptionalPyblishPluginMixin,\n    publish,\n)\n\n\nclass ExtractMatchmoveScriptMaya(publish.Extractor,\n                                 ExtractScriptBase,\n                                 OptionalPyblishPluginMixin):\n    \"\"\"Extract Maya MEL script for matchmove.\n\n    This is using built-in export script from 3DEqualizer.\n    \"\"\"\n\n    label = \"Extract Maya Script\"\n    families = [\"matchmove\"]\n    hosts = [\"equalizer\"]\n\n    order = pyblish.api.ExtractorOrder\n\n    def process(self, instance: pyblish.api.Instance):\n        \"\"\"Extracts Maya script from 3DEqualizer.\n\n        This method is using export script shipped with 3DEqualizer to\n        maintain as much compatibility as possible. Instead of invoking it\n        from the UI, it calls directly the function that is doing the export.\n        For that it needs to pass some data that are collected in 3dequalizer\n        from the UI, so we need to determine them from the instance itself and\n        from the state of the project.\n\n        \"\"\"\n        if not self.is_active(instance.data):\n            return\n        attr_data = self.get_attr_values_from_data(instance.data)\n\n        # import maya export script from 3DEqualizer\n        exporter_path = instance.data[\"tde4_path\"] / \"sys_data\" / \"py_scripts\" / \"export_maya.py\"  # noqa: E501\n        self.log.debug(f\"Importing {exporter_path.as_posix()}\")\n        exporter = import_filepath(exporter_path.as_posix())\n\n        # get camera point group\n        point_group = None\n        point_groups = tde4.getPGroupList()\n        for pg in point_groups:\n            if tde4.getPGroupType(pg) == \"CAMERA\":\n                point_group = pg\n                break\n        else:\n            # this should never happen as it should be handled by validator\n            raise RuntimeError(\"No camera point group found.\")\n\n        offset = tde4.getCameraFrameOffset(tde4.getCurrentCamera())\n        overscan_width = attr_data[\"overscan_percent_width\"] / 100.0\n        overscan_height = attr_data[\"overscan_percent_height\"] / 100.0\n\n        staging_dir = self.staging_dir(instance)\n\n        unit_scales = {\n            \"mm\": 10.0,  # cm -> mm\n            \"cm\": 1.0,  # cm -> cm\n            \"m\": 0.01,  # cm -> m\n            \"in\": 0.393701,  # cm -> in\n            \"ft\": 0.0328084,  # cm -> ft\n            \"yd\": 0.0109361  # cm -> yd\n        }\n        scale_factor = unit_scales[attr_data[\"units\"]]\n        model_selection_enum = instance.data[\"creator_attributes\"][\"model_selection\"]  # noqa: E501\n\n        with maintained_model_selection():\n            # handle model selection\n            # We are passing it to existing function that is expecting\n            # this value to be an index of selection type.\n            # 1 - No models\n            # 2 - Selected models\n            # 3 - All models\n            if model_selection_enum == \"__none__\":\n                model_selection = 1\n            elif model_selection_enum == \"__all__\":\n                model_selection = 3\n            else:\n                # take model from instance and set its selection flag on\n                # turn off all others\n                model_selection = 2\n                point_groups = tde4.getPGroupList()\n                for point_group in point_groups:\n                    model_list = tde4.get3DModelList(point_group, 0)\n                    if model_selection_enum in model_list:\n                        model_selection = 2\n                        tde4.set3DModelSelectionFlag(\n                            point_group, instance.data[\"model_selection\"], 1)\n                        break\n                    else:\n                        # clear all other model selections\n                        for model in model_list:\n                            tde4.set3DModelSelectionFlag(point_group, model, 0)\n\n            file_path = Path(staging_dir) / \"maya_export.mel\"\n            status = exporter._maya_export_mel_file(\n                file_path.as_posix(),  # staging path\n                point_group,  # camera point group\n                [c[\"id\"] for c in instance.data[\"cameras\"] if c[\"enabled\"]],\n                model_selection,  # model selection mode\n                overscan_width,\n                overscan_height,\n                1 if attr_data[\"export_uv_textures\"] else 0,\n                scale_factor,\n                offset,  # start frame\n                1 if attr_data[\"hide_reference_frame\"] else 0)\n\n        if status != 1:\n            raise KnownPublishError(\"Export failed.\")\n\n        # create representation data\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': \"mel\",\n            'ext': \"mel\",\n            'files': file_path.name,\n            \"stagingDir\": staging_dir,\n        }\n        self.log.debug(f\"output: {file_path.as_posix()}\")\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/equalizer/plugins/publish/extract_matchmove_script_nuke.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract project for Nuke.\n\nBecause original extractor script is intermingled with UI, we had to resort\nto this hacky solution. This is monkey-patching 3DEqualizer UI to silence it\nduring the export. Advantage is that it is still using \"vanilla\" built-in\nexport script, so it should be more compatible with future versions of the\nsoftware.\n\nTODO: This can be refactored even better, split to multiple methods, etc.\n\n\"\"\"\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nimport pyblish.api\nimport tde4  # noqa: F401\n\n\nfrom openpype.pipeline import OptionalPyblishPluginMixin\nfrom openpype.pipeline import publish\n\n\nclass ExtractMatchmoveScriptNuke(publish.Extractor,\n                                 OptionalPyblishPluginMixin):\n    \"\"\"Extract Nuke script for matchmove.\n\n    Unfortunately built-in export script from 3DEqualizer is bound to its UI,\n    and it is not possible to call it directly from Python. Because of that,\n    we are executing the script in the same way as artist would do it, but\n    we are patching the UI to silence it and to avoid any user interaction.\n\n    TODO: Utilize attributes defined in ExtractScriptBase\n    \"\"\"\n\n    label = \"Extract Nuke Script\"\n    families = [\"matchmove\"]\n    hosts = [\"equalizer\"]\n\n    order = pyblish.api.ExtractorOrder\n\n    def process(self, instance: pyblish.api.Instance):\n\n        if not self.is_active(instance.data):\n            return\n\n        cam = tde4.getCurrentCamera()\n        frame0 = tde4.getCameraFrameOffset(cam)\n        frame0 -= 1\n\n        staging_dir = self.staging_dir(instance)\n        file_path = Path(staging_dir) / \"nuke_export.nk\"\n\n        # these patched methods are used to silence 3DEqualizer UI:\n        def patched_getWidgetValue(requester, key: str):  # noqa: N802\n            \"\"\"Return value for given key in widget.\"\"\"\n            if key == \"file_browser\":\n                return file_path.as_posix()\n            elif key == \"startframe_field\":\n                return tde4.getCameraFrameOffset(cam)\n            return \"\"\n\n        # This is simulating artist clicking on \"OK\" button\n        # in the export dialog.\n        def patched_postCustomRequester(*args, **kwargs):  # noqa: N802\n            return 1\n\n        # This is silencing success/error message after the script\n        # is exported.\n        def patched_postQuestionRequester(*args, **kwargs):  # noqa: N802\n            return None\n\n        # import maya export script from 3DEqualizer\n        exporter_path = instance.data[\"tde4_path\"] / \"sys_data\" / \"py_scripts\" / \"export_nuke.py\"  # noqa: E501\n        self.log.debug(\"Patching 3dequalizer requester objects ...\")\n\n        with patch(\"tde4.getWidgetValue\", patched_getWidgetValue), \\\n             patch(\"tde4.postCustomRequester\", patched_postCustomRequester), \\\n             patch(\"tde4.postQuestionRequester\", patched_postQuestionRequester):  # noqa: E501\n            with exporter_path.open() as f:\n                script = f.read()\n            self.log.debug(f\"Importing {exporter_path.as_posix()}\")\n            exec(script)\n\n        # create representation data\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': \"nk\",\n            'ext': \"nk\",\n            'files': file_path.name,\n            \"stagingDir\": staging_dir,\n        }\n        self.log.debug(f\"output: {file_path.as_posix()}\")\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/equalizer/plugins/publish/validate_camera_pointgroup.py",
    "content": "import pyblish.api\nimport tde4\n\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    ValidateContentsOrder,\n)\n\n\nclass ValidateCameraPoingroup(pyblish.api.InstancePlugin):\n    \"\"\"Validate Camera Point Group.\n\n    There must be a camera point group in the scene.\n    \"\"\"\n    order = ValidateContentsOrder\n    hosts = [\"equalizer\"]\n    families = [\"matchmove\"]\n    label = \"Validate Camera Point Group\"\n\n    def process(self, instance):\n        valid = False\n        for point_group in tde4.getPGroupList():\n            if tde4.getPGroupType(point_group) == \"CAMERA\":\n                valid = True\n                break\n\n        if not valid:\n            raise PublishValidationError(\"Missing Camera Point Group\")\n"
  },
  {
    "path": "openpype/hosts/equalizer/plugins/publish/validate_instance_camera_data.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import PublishValidationError\nfrom openpype.pipeline.publish import ValidateContentsOrder\n\n\nclass ValidateInstanceCameraData(pyblish.api.InstancePlugin):\n    \"\"\"Check if instance has camera data.\n\n    There might not be any camera associated with the instance\n    and without it, the instance is not valid.\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"equalizer\"]\n    families = [\"matchmove\"]\n    label = \"Validate Instance has Camera data\"\n\n    def process(self, instance):\n        try:\n            _ = instance.data[\"cameras\"]\n        except KeyError as e:\n            raise PublishValidationError(\"No camera data found\") from e\n"
  },
  {
    "path": "openpype/hosts/equalizer/startup/ayon_create.py",
    "content": "#\n# 3DE4.script.name:     Create ...\n# 3DE4.script.gui:      Main Window::Ayon\n# 3DE4.script.comment:  Open AYON Publisher tool\n#\n\nfrom openpype.pipeline import install_host, is_installed\nfrom openpype.hosts.equalizer.api import EqualizerHost\nfrom openpype.tools.utils import host_tools\n\n\ndef install_3de_host():\n    print(\"Running AYON integration ...\")\n    install_host(EqualizerHost())\n\n\nif not is_installed():\n    install_3de_host()\n\n# show the UI\nprint(\"Opening publisher window ...\")\nhost_tools.show_publisher(\n    tab=\"create\", parent=EqualizerHost.get_host().get_main_window())\n"
  },
  {
    "path": "openpype/hosts/equalizer/startup/ayon_load.py",
    "content": "#\n# 3DE4.script.name:     Load ...\n# 3DE4.script.gui:      Main Window::Ayon\n# 3DE4.script.comment:  Open AYON Loader tool\n#\n\nfrom openpype.pipeline import install_host, is_installed\nfrom openpype.hosts.equalizer.api import EqualizerHost\nfrom openpype.tools.utils import host_tools\n\n\ndef install_3de_host():\n    print(\"Running AYON integration ...\")\n    install_host(EqualizerHost())\n\n\nif not is_installed():\n    install_3de_host()\n\n# show the UI\nprint(\"Opening loader window ...\")\nhost_tools.show_loader(\n    parent=EqualizerHost.get_host().get_main_window(),\n    use_context=True)\n"
  },
  {
    "path": "openpype/hosts/equalizer/startup/ayon_manage.py",
    "content": "#\n# 3DE4.script.name:     Manage ...\n# 3DE4.script.gui:      Main Window::Ayon\n# 3DE4.script.comment:  Open AYON Publisher tool\n#\n\nfrom openpype.pipeline import install_host, is_installed\nfrom openpype.hosts.equalizer.api import EqualizerHost\nfrom openpype.tools.utils import host_tools\n\n\ndef install_3de_host():\n    print(\"Running AYON integration ...\")\n    install_host(EqualizerHost())\n\n\nif not is_installed():\n    install_3de_host()\n\n# show the UI\nprint(\"Opening Scene Manager window ...\")\nhost_tools.show_scene_inventory(\n    parent=EqualizerHost.get_host().get_main_window())\n"
  },
  {
    "path": "openpype/hosts/equalizer/startup/ayon_publish.py",
    "content": "#\n# 3DE4.script.name:     Publish ...\n# 3DE4.script.gui:      Main Window::Ayon\n# 3DE4.script.comment:  Open AYON Publisher tool\n#\n\nfrom openpype.pipeline import install_host, is_installed\nfrom openpype.hosts.equalizer.api import EqualizerHost\nfrom openpype.tools.utils import host_tools\n\n\ndef install_3de_host():\n    print(\"Running AYON integration ...\")\n    install_host(EqualizerHost())\n\n\nif not is_installed():\n    install_3de_host()\n\n# show the UI\nprint(\"Opening publisher window ...\")\nhost_tools.show_publisher(\n    tab=\"publish\", parent=EqualizerHost.get_host().get_main_window())\n"
  },
  {
    "path": "openpype/hosts/equalizer/startup/ayon_workfile.py",
    "content": "#\n# 3DE4.script.name:     Work files ...\n# 3DE4.script.gui:      Main Window::Ayon\n# 3DE4.script.comment:  Open AYON Publisher tool\n#\n\nfrom openpype.pipeline import install_host, is_installed\nfrom openpype.hosts.equalizer.api import EqualizerHost\nfrom openpype.tools.utils import host_tools\n\n\ndef install_3de_host():\n    print(\"Running AYON integration ...\")\n    install_host(EqualizerHost())\n\n\nif not is_installed():\n    install_3de_host()\n\n# show the UI\nprint(\"Opening Workfile tool window ...\")\nhost_tools.show_workfiles(\n    parent=EqualizerHost.get_host().get_main_window())\n"
  },
  {
    "path": "openpype/hosts/equalizer/tests/test_plugin.py",
    "content": "\"\"\"\n3DEqualizer plugin tests\n\nThese test need to be run in 3DEqualizer.\n\n\"\"\"\nimport json\nimport re\nimport unittest\n\nfrom attrs import asdict, define, field\nfrom attrs.exceptions import NotAnAttrsClassError\n\nAVALON_CONTAINER_ID = \"test.container\"\n\nCONTEXT_REGEX = re.compile(\n    r\"AYON_CONTEXT::(?P<context>.*?)::AYON_CONTEXT_END\",\n    re.DOTALL)\n\n\n@define\nclass Container(object):\n\n    name: str = field(default=None)\n    id: str = field(init=False, default=AVALON_CONTAINER_ID)\n    namespace: str = field(default=\"\")\n    loader: str = field(default=None)\n    representation: str = field(default=None)\n\n\nclass Tde4Mock:\n    \"\"\"Simple class to mock few 3dequalizer functions.\n\n    Just to run the test outside the host itself.\n    \"\"\"\n\n    _notes = \"\"\n\n    def isProjectUpToDate(self):\n        return True\n\n    def setProjectNotes(self, notes):\n        self._notes = notes\n\n    def getProjectNotes(self):\n        return self._notes\n\n\ntde4 = Tde4Mock()\n\n\ndef get_context_data():\n    m = re.search(CONTEXT_REGEX, tde4.getProjectNotes())\n    return json.loads(m[\"context\"]) if m else {}\n\n\ndef update_context_data(data, _):\n    m = re.search(CONTEXT_REGEX, tde4.getProjectNotes())\n    if not m:\n        tde4.setProjectNotes(\"AYON_CONTEXT::::AYON_CONTEXT_END\")\n    update = json.dumps(data, indent=4)\n    tde4.setProjectNotes(\n        re.sub(\n            CONTEXT_REGEX,\n            f\"AYON_CONTEXT::{update}::AYON_CONTEXT_END\",\n            tde4.getProjectNotes()\n        )\n    )\n\n\ndef get_containers():\n    return get_context_data().get(\"containers\", [])\n\n\ndef add_container(container: Container):\n    context_data = get_context_data()\n    containers = get_context_data().get(\"containers\", [])\n\n    for _container in containers:\n        if _container[\"name\"] == container.name and _container[\"namespace\"] == container.namespace:  # noqa: E501\n            containers.remove(_container)\n            break\n\n    try:\n        containers.append(asdict(container))\n    except NotAnAttrsClassError:\n        print(\"not an attrs class\")\n        containers.append(container)\n\n    context_data[\"containers\"] = containers\n    update_context_data(context_data, changes={})\n\n\nclass TestEqualizer(unittest.TestCase):\n    def test_context_data(self):\n        # ensure empty project notest\n\n        data = get_context_data()\n        self.assertEqual({}, data, \"context data are not empty\")\n\n        # add container\n        add_container(\n            Container(name=\"test\", representation=\"test_A\")\n        )\n\n        self.assertEqual(\n            1, len(get_containers()), \"container not added\")\n        self.assertEqual(\n            get_containers()[0][\"name\"],\n            \"test\", \"container name is not correct\")\n\n        # add another container\n        add_container(\n            Container(name=\"test2\", representation=\"test_B\")\n        )\n\n        self.assertEqual(\n            2, len(get_containers()), \"container not added\")\n        self.assertEqual(\n            get_containers()[1][\"name\"],\n            \"test2\", \"container name is not correct\")\n\n        # update container\n        add_container(\n            Container(name=\"test2\", representation=\"test_C\")\n        )\n        self.assertEqual(\n            2, len(get_containers()), \"container not updated\")\n        self.assertEqual(\n            get_containers()[1][\"representation\"],\n            \"test_C\", \"container name is not correct\")\n\n        print(f\"--4: {tde4.getProjectNotes()}\")\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "openpype/hosts/flame/__init__.py",
    "content": "from .addon import (\n    HOST_DIR,\n    FlameAddon,\n)\n\n\n__all__ = (\n    \"HOST_DIR\",\n    \"FlameAddon\",\n)\n"
  },
  {
    "path": "openpype/hosts/flame/addon.py",
    "content": "import os\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nHOST_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass FlameAddon(OpenPypeModule, IHostAddon):\n    name = \"flame\"\n    host_name = \"flame\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        # Add requirements to DL_PYTHON_HOOK_PATH\n        env[\"DL_PYTHON_HOOK_PATH\"] = os.path.join(HOST_DIR, \"startup\")\n        env.pop(\"QT_AUTO_SCREEN_SCALE_FACTOR\", None)\n\n        # Set default values if are not already set via settings\n        defaults = {\n            \"LOGLEVEL\": \"DEBUG\"\n        }\n        for key, value in defaults.items():\n            if not env.get(key):\n                env[key] = value\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [\n            os.path.join(HOST_DIR, \"hooks\")\n        ]\n\n    def get_workfile_extensions(self):\n        return [\".otoc\"]\n"
  },
  {
    "path": "openpype/hosts/flame/api/__init__.py",
    "content": "\"\"\"\nOpenPype Autodesk Flame api\n\"\"\"\nfrom .constants import (\n    COLOR_MAP,\n    MARKER_NAME,\n    MARKER_COLOR,\n    MARKER_DURATION,\n    MARKER_PUBLISH_DEFAULT\n)\nfrom .lib import (\n    CTX,\n    FlameAppFramework,\n    get_current_project,\n    get_current_sequence,\n    create_segment_data_marker,\n    get_segment_data_marker,\n    set_segment_data_marker,\n    set_publish_attribute,\n    get_publish_attribute,\n    get_sequence_segments,\n    maintained_segment_selection,\n    reset_segment_selection,\n    get_segment_attributes,\n    get_clips_in_reels,\n    get_reformated_filename,\n    get_frame_from_filename,\n    get_padding_from_filename,\n    maintained_object_duplication,\n    maintained_temp_file_path,\n    get_clip_segment,\n    get_batch_group_from_desktop,\n    MediaInfoFile,\n    TimeEffectMetadata\n)\nfrom .utils import (\n    setup,\n    get_flame_version,\n    get_flame_install_root\n)\nfrom .pipeline import (\n    install,\n    uninstall,\n    ls,\n    containerise,\n    update_container,\n    remove_instance,\n    list_instances,\n    imprint,\n    maintained_selection\n)\nfrom .menu import (\n    FlameMenuProjectConnect,\n    FlameMenuTimeline,\n    FlameMenuUniversal\n)\nfrom .plugin import (\n    Creator,\n    PublishableClip,\n    ClipLoader,\n    OpenClipSolver\n)\nfrom .workio import (\n    open_file,\n    save_file,\n    current_file,\n    has_unsaved_changes,\n    file_extensions,\n    work_root\n)\nfrom .render_utils import (\n    export_clip,\n    get_preset_path_by_xml_name,\n    modify_preset_file\n)\nfrom .batch_utils import (\n    create_batch_group,\n    create_batch_group_conent\n)\n\n__all__ = [\n    # constants\n    \"COLOR_MAP\",\n    \"MARKER_NAME\",\n    \"MARKER_COLOR\",\n    \"MARKER_DURATION\",\n    \"MARKER_PUBLISH_DEFAULT\",\n\n    # lib\n    \"CTX\",\n    \"FlameAppFramework\",\n    \"get_current_project\",\n    \"get_current_sequence\",\n    \"create_segment_data_marker\",\n    \"get_segment_data_marker\",\n    \"set_segment_data_marker\",\n    \"set_publish_attribute\",\n    \"get_publish_attribute\",\n    \"get_sequence_segments\",\n    \"maintained_segment_selection\",\n    \"reset_segment_selection\",\n    \"get_segment_attributes\",\n    \"get_clips_in_reels\",\n    \"get_reformated_filename\",\n    \"get_frame_from_filename\",\n    \"get_padding_from_filename\",\n    \"maintained_object_duplication\",\n    \"maintained_temp_file_path\",\n    \"get_clip_segment\",\n    \"get_batch_group_from_desktop\",\n    \"MediaInfoFile\",\n    \"TimeEffectMetadata\",\n\n    # pipeline\n    \"install\",\n    \"uninstall\",\n    \"ls\",\n    \"containerise\",\n    \"update_container\",\n    \"reload_pipeline\",\n    \"maintained_selection\",\n    \"remove_instance\",\n    \"list_instances\",\n    \"imprint\",\n    \"maintained_selection\",\n\n    # utils\n    \"setup\",\n    \"get_flame_version\",\n    \"get_flame_install_root\",\n\n    # menu\n    \"FlameMenuProjectConnect\",\n    \"FlameMenuTimeline\",\n    \"FlameMenuUniversal\",\n\n    # plugin\n    \"Creator\",\n    \"PublishableClip\",\n    \"ClipLoader\",\n    \"OpenClipSolver\",\n\n    # workio\n    \"open_file\",\n    \"save_file\",\n    \"current_file\",\n    \"has_unsaved_changes\",\n    \"file_extensions\",\n    \"work_root\",\n\n    # render utils\n    \"export_clip\",\n    \"get_preset_path_by_xml_name\",\n    \"modify_preset_file\",\n\n    # batch utils\n    \"create_batch_group\",\n    \"create_batch_group_conent\"\n]\n"
  },
  {
    "path": "openpype/hosts/flame/api/batch_utils.py",
    "content": "import flame\n\n\ndef create_batch_group(\n    name,\n    frame_start,\n    frame_duration,\n    update_batch_group=None,\n    **kwargs\n):\n    \"\"\"Create Batch Group in active project's Desktop\n\n    Args:\n        name (str): name of batch group to be created\n        frame_start (int): start frame of batch\n        frame_end (int): end frame of batch\n        update_batch_group (PyBatch)[optional]: batch group to update\n\n    Return:\n        PyBatch: active flame batch group\n    \"\"\"\n    # make sure some batch obj is present\n    batch_group = update_batch_group or flame.batch\n\n    schematic_reels = kwargs.get(\"shematic_reels\") or ['LoadedReel1']\n    shelf_reels = kwargs.get(\"shelf_reels\") or ['ShelfReel1']\n\n    handle_start = kwargs.get(\"handleStart\") or 0\n    handle_end = kwargs.get(\"handleEnd\") or 0\n\n    frame_start -= handle_start\n    frame_duration += handle_start + handle_end\n\n    if not update_batch_group:\n        # Create batch group with name, start_frame value, duration value,\n        # set of schematic reel names, set of shelf reel names\n        batch_group = batch_group.create_batch_group(\n            name,\n            start_frame=frame_start,\n            duration=frame_duration,\n            reels=schematic_reels,\n            shelf_reels=shelf_reels\n        )\n    else:\n        batch_group.name = name\n        batch_group.start_frame = frame_start\n        batch_group.duration = frame_duration\n\n        # add reels to batch group\n        _add_reels_to_batch_group(\n            batch_group, schematic_reels, shelf_reels)\n\n        # TODO: also update write node if there is any\n        # TODO: also update loaders to start from correct frameStart\n\n    if kwargs.get(\"switch_batch_tab\"):\n        # use this command to switch to the batch tab\n        batch_group.go_to()\n\n    return batch_group\n\n\ndef _add_reels_to_batch_group(batch_group, reels, shelf_reels):\n    # update or create defined reels\n    # helper variables\n    reel_names = [\n        r.name.get_value()\n        for r in batch_group.reels\n    ]\n    shelf_reel_names = [\n        r.name.get_value()\n        for r in batch_group.shelf_reels\n    ]\n    # add schematic reels\n    for _r in reels:\n        if _r in reel_names:\n            continue\n        batch_group.create_reel(_r)\n\n    # add shelf reels\n    for _sr in shelf_reels:\n        if _sr in shelf_reel_names:\n            continue\n        batch_group.create_shelf_reel(_sr)\n\n\ndef create_batch_group_conent(batch_nodes, batch_links, batch_group=None):\n    \"\"\"Creating batch group with links\n\n    Args:\n        batch_nodes (list of dict): each dict is node definition\n        batch_links (list of dict): each dict is link definition\n        batch_group (PyBatch, optional): batch group. Defaults to None.\n\n    Return:\n        dict: all batch nodes {name or id: PyNode}\n    \"\"\"\n    # make sure some batch obj is present\n    batch_group = batch_group or flame.batch\n    all_batch_nodes = {\n        b.name.get_value(): b\n        for b in batch_group.nodes\n    }\n    for node in batch_nodes:\n        # NOTE: node_props needs to be ideally OrederDict type\n        node_id, node_type, node_props = (\n            node[\"id\"], node[\"type\"], node[\"properties\"])\n\n        # get node name for checking if exists\n        node_name = node_props.pop(\"name\", None) or node_id\n\n        if all_batch_nodes.get(node_name):\n            # update existing batch node\n            batch_node = all_batch_nodes[node_name]\n        else:\n            # create new batch node\n            batch_node = batch_group.create_node(node_type)\n\n            # set name\n            batch_node.name.set_value(node_name)\n\n        # set attributes found in node props\n        for key, value in node_props.items():\n            if not hasattr(batch_node, key):\n                continue\n            setattr(batch_node, key, value)\n\n        # add created node for possible linking\n        all_batch_nodes[node_id] = batch_node\n\n    # link nodes to each other\n    for link in batch_links:\n        _from_n, _to_n = link[\"from_node\"], link[\"to_node\"]\n\n        # check if all linking nodes are available\n        if not all([\n            all_batch_nodes.get(_from_n[\"id\"]),\n            all_batch_nodes.get(_to_n[\"id\"])\n        ]):\n            continue\n\n        # link nodes in defined link\n        batch_group.connect_nodes(\n            all_batch_nodes[_from_n[\"id\"]], _from_n[\"connector\"],\n            all_batch_nodes[_to_n[\"id\"]], _to_n[\"connector\"]\n        )\n\n    # sort batch nodes\n    batch_group.organize()\n\n    return all_batch_nodes\n"
  },
  {
    "path": "openpype/hosts/flame/api/constants.py",
    "content": "\n\"\"\"\nOpenPype Flame api constances\n\"\"\"\n# OpenPype marker workflow variables\nMARKER_NAME = \"OpenPypeData\"\nMARKER_DURATION = 0\nMARKER_COLOR = \"cyan\"\nMARKER_PUBLISH_DEFAULT = False\n\n# OpenPype color definitions\nCOLOR_MAP = {\n    \"red\": (1.0, 0.0, 0.0),\n    \"orange\": (1.0, 0.5, 0.0),\n    \"yellow\": (1.0, 1.0, 0.0),\n    \"pink\": (1.0, 0.5, 1.0),\n    \"white\": (1.0, 1.0, 1.0),\n    \"green\": (0.0, 1.0, 0.0),\n    \"cyan\": (0.0, 1.0, 1.0),\n    \"blue\": (0.0, 0.0, 1.0),\n    \"purple\": (0.5, 0.0, 0.5),\n    \"magenta\": (0.5, 0.0, 1.0),\n    \"black\": (0.0, 0.0, 0.0)\n}\n"
  },
  {
    "path": "openpype/hosts/flame/api/lib.py",
    "content": "import sys\nimport os\nimport re\nimport json\nimport pickle\nimport clique\nimport tempfile\nimport traceback\nimport itertools\nimport contextlib\nimport xml.etree.cElementTree as cET\nfrom copy import deepcopy, copy\nfrom xml.etree import ElementTree as ET\nfrom pprint import pformat\n\nfrom openpype.lib import Logger, run_subprocess\n\nfrom .constants import (\n    MARKER_COLOR,\n    MARKER_DURATION,\n    MARKER_NAME,\n    COLOR_MAP,\n    MARKER_PUBLISH_DEFAULT\n)\n\nlog = Logger.get_logger(__name__)\n\nFRAME_PATTERN = re.compile(r\"[\\._](\\d+)[\\.]\")\n\n\nclass CTX:\n    # singleton used for passing data between api modules\n    app_framework = None\n    flame_apps = []\n    selection = None\n\n\n@contextlib.contextmanager\ndef io_preferences_file(klass, filepath, write=False):\n    try:\n        flag = \"w\" if write else \"r\"\n        yield open(filepath, flag)\n\n    except IOError as _error:\n        klass.log.info(\"Unable to work with preferences `{}`: {}\".format(\n            filepath, _error))\n\n\nclass FlameAppFramework(object):\n    # flameAppFramework class takes care of preferences\n\n    class prefs_dict(dict):\n\n        def __init__(self, master, name, **kwargs):\n            self.name = name\n            self.master = master\n            if not self.master.get(self.name):\n                self.master[self.name] = {}\n            self.master[self.name].__init__()\n\n        def __getitem__(self, k):\n            return self.master[self.name].__getitem__(k)\n\n        def __setitem__(self, k, v):\n            return self.master[self.name].__setitem__(k, v)\n\n        def __delitem__(self, k):\n            return self.master[self.name].__delitem__(k)\n\n        def get(self, k, default=None):\n            return self.master[self.name].get(k, default)\n\n        def setdefault(self, k, default=None):\n            return self.master[self.name].setdefault(k, default)\n\n        def pop(self, *args, **kwargs):\n            return self.master[self.name].pop(*args, **kwargs)\n\n        def update(self, mapping=(), **kwargs):\n            self.master[self.name].update(mapping, **kwargs)\n\n        def __contains__(self, k):\n            return self.master[self.name].__contains__(k)\n\n        def copy(self):  # don\"t delegate w/ super - dict.copy() -> dict :(\n            return type(self)(self)\n\n        def keys(self):\n            return self.master[self.name].keys()\n\n        @classmethod\n        def fromkeys(cls, keys, v=None):\n            return cls.master[cls.name].fromkeys(keys, v)\n\n        def __repr__(self):\n            return \"{0}({1})\".format(\n                type(self).__name__, self.master[self.name].__repr__())\n\n        def master_keys(self):\n            return self.master.keys()\n\n    def __init__(self):\n        self.name = self.__class__.__name__\n        self.bundle_name = \"OpenPypeFlame\"\n        # self.prefs scope is limited to flame project and user\n        self.prefs = {}\n        self.prefs_user = {}\n        self.prefs_global = {}\n        self.log = log\n\n        try:\n            import flame\n            self.flame = flame\n            self.flame_project_name = self.flame.project.current_project.name\n            self.flame_user_name = flame.users.current_user.name\n        except Exception:\n            self.flame = None\n            self.flame_project_name = None\n            self.flame_user_name = None\n\n        import socket\n        self.hostname = socket.gethostname()\n\n        if sys.platform == \"darwin\":\n            self.prefs_folder = os.path.join(\n                os.path.expanduser(\"~\"),\n                \"Library\",\n                \"Caches\",\n                \"OpenPype\",\n                self.bundle_name\n            )\n        elif sys.platform.startswith(\"linux\"):\n            self.prefs_folder = os.path.join(\n                os.path.expanduser(\"~\"),\n                \".OpenPype\",\n                self.bundle_name)\n\n        self.prefs_folder = os.path.join(\n            self.prefs_folder,\n            self.hostname,\n        )\n\n        self.log.info(\"[{}] waking up\".format(self.__class__.__name__))\n\n        try:\n            self.load_prefs()\n        except RuntimeError:\n            self.save_prefs()\n\n        # menu auto-refresh defaults\n        if not self.prefs_global.get(\"menu_auto_refresh\"):\n            self.prefs_global[\"menu_auto_refresh\"] = {\n                \"media_panel\": True,\n                \"batch\": True,\n                \"main_menu\": True,\n                \"timeline_menu\": True\n            }\n\n        self.apps = []\n\n    def get_pref_file_paths(self):\n\n        prefix = self.prefs_folder + os.path.sep + self.bundle_name\n        prefs_file_path = \"_\".join([\n            prefix, self.flame_user_name,\n            self.flame_project_name]) + \".prefs\"\n        prefs_user_file_path = \"_\".join([\n            prefix, self.flame_user_name]) + \".prefs\"\n        prefs_global_file_path = prefix + \".prefs\"\n\n        return (prefs_file_path, prefs_user_file_path, prefs_global_file_path)\n\n    def load_prefs(self):\n\n        (proj_pref_path, user_pref_path,\n         glob_pref_path) = self.get_pref_file_paths()\n\n        with io_preferences_file(self, proj_pref_path) as prefs_file:\n            self.prefs = pickle.load(prefs_file)\n            self.log.info(\n                \"Project - preferences contents:\\n{}\".format(\n                    pformat(self.prefs)\n                ))\n\n        with io_preferences_file(self, user_pref_path) as prefs_file:\n            self.prefs_user = pickle.load(prefs_file)\n            self.log.info(\n                \"User - preferences contents:\\n{}\".format(\n                    pformat(self.prefs_user)\n                ))\n\n        with io_preferences_file(self, glob_pref_path) as prefs_file:\n            self.prefs_global = pickle.load(prefs_file)\n            self.log.info(\n                \"Global - preferences contents:\\n{}\".format(\n                    pformat(self.prefs_global)\n                ))\n\n        return True\n\n    def save_prefs(self):\n        # make sure the preference folder is available\n        if not os.path.isdir(self.prefs_folder):\n            try:\n                os.makedirs(self.prefs_folder)\n            except Exception:\n                self.log.info(\"Unable to create folder {}\".format(\n                    self.prefs_folder))\n                return False\n\n        # get all pref file paths\n        (proj_pref_path, user_pref_path,\n         glob_pref_path) = self.get_pref_file_paths()\n\n        with io_preferences_file(self, proj_pref_path, True) as prefs_file:\n            pickle.dump(self.prefs, prefs_file)\n            self.log.info(\n                \"Project - preferences contents:\\n{}\".format(\n                    pformat(self.prefs)\n                ))\n\n        with io_preferences_file(self, user_pref_path, True) as prefs_file:\n            pickle.dump(self.prefs_user, prefs_file)\n            self.log.info(\n                \"User - preferences contents:\\n{}\".format(\n                    pformat(self.prefs_user)\n                ))\n\n        with io_preferences_file(self, glob_pref_path, True) as prefs_file:\n            pickle.dump(self.prefs_global, prefs_file)\n            self.log.info(\n                \"Global - preferences contents:\\n{}\".format(\n                    pformat(self.prefs_global)\n                ))\n\n        return True\n\n\ndef get_current_project():\n    import flame\n    return flame.project.current_project\n\n\ndef get_current_sequence(selection):\n    import flame\n\n    def segment_to_sequence(_segment):\n        track = _segment.parent\n        version = track.parent\n        return version.parent\n\n    process_timeline = None\n\n    if len(selection) == 1:\n        if isinstance(selection[0], flame.PySequence):\n            process_timeline = selection[0]\n        if isinstance(selection[0], flame.PySegment):\n            process_timeline = segment_to_sequence(selection[0])\n    else:\n        for segment in selection:\n            if isinstance(segment, flame.PySegment):\n                process_timeline = segment_to_sequence(segment)\n                break\n\n    return process_timeline\n\n\ndef rescan_hooks():\n    import flame\n    try:\n        flame.execute_shortcut(\"Rescan Python Hooks\")\n    except Exception:\n        pass\n\n\ndef get_metadata(project_name, _log=None):\n    # TODO: can be replaced by MediaInfoFile class method\n    from adsk.libwiretapPythonClientAPI import (\n        WireTapClient,\n        WireTapServerHandle,\n        WireTapNodeHandle,\n        WireTapStr\n    )\n\n    class GetProjectColorPolicy(object):\n        def __init__(self, host_name=None, _log=None):\n            # Create a connection to the Backburner manager using the Wiretap\n            # python API.\n            #\n            self.log = _log or log\n            self.host_name = host_name or \"localhost\"\n            self._wiretap_client = WireTapClient()\n            if not self._wiretap_client.init():\n                raise Exception(\"Could not initialize Wiretap Client\")\n            self._server = WireTapServerHandle(\n                \"{}:IFFFS\".format(self.host_name))\n\n        def process(self, project_name):\n            policy_node_handle = WireTapNodeHandle(\n                self._server,\n                \"/projects/{}/syncolor/policy\".format(project_name)\n            )\n            self.log.info(policy_node_handle)\n\n            policy = WireTapStr()\n            if not policy_node_handle.getNodeTypeStr(policy):\n                self.log.warning(\n                    \"Could not retrieve policy of '%s': %s\" % (\n                        policy_node_handle.getNodeId().id(),\n                        policy_node_handle.lastError()\n                    )\n                )\n\n            return policy.c_str()\n\n    policy_wiretap = GetProjectColorPolicy(_log=_log)\n    return policy_wiretap.process(project_name)\n\n\ndef get_segment_data_marker(segment, with_marker=None):\n    \"\"\"\n    Get openpype track item tag created by creator or loader plugin.\n\n    Attributes:\n        segment (flame.PySegment): flame api object\n        with_marker (bool)[optional]: if true it will return also marker object\n\n    Returns:\n        dict: openpype tag data\n\n    Returns(with_marker=True):\n        flame.PyMarker, dict\n    \"\"\"\n    for marker in segment.markers:\n        comment = marker.comment.get_value()\n        color = marker.colour.get_value()\n        name = marker.name.get_value()\n\n        if (name == MARKER_NAME) and (\n                color == COLOR_MAP[MARKER_COLOR]):\n            if not with_marker:\n                return json.loads(comment)\n            else:\n                return marker, json.loads(comment)\n\n\ndef set_segment_data_marker(segment, data=None):\n    \"\"\"\n    Set openpype track item tag to input segment.\n\n    Attributes:\n        segment (flame.PySegment): flame api object\n\n    Returns:\n        dict: json loaded data\n    \"\"\"\n    data = data or dict()\n\n    marker_data = get_segment_data_marker(segment, True)\n\n    if marker_data:\n        # get available openpype tag if any\n        marker, tag_data = marker_data\n        # update tag data with new data\n        tag_data.update(data)\n        # update marker with tag data\n        marker.comment = json.dumps(tag_data)\n    else:\n        # update tag data with new data\n        marker = create_segment_data_marker(segment)\n        # add tag data to marker's comment\n        marker.comment = json.dumps(data)\n\n\ndef set_publish_attribute(segment, value):\n    \"\"\" Set Publish attribute in input Tag object\n\n    Attribute:\n        segment (flame.PySegment)): flame api object\n        value (bool): True or False\n    \"\"\"\n    tag_data = get_segment_data_marker(segment)\n    tag_data[\"publish\"] = value\n\n    # set data to the publish attribute\n    set_segment_data_marker(segment, tag_data)\n\n\ndef get_publish_attribute(segment):\n    \"\"\" Get Publish attribute from input Tag object\n\n    Attribute:\n        segment (flame.PySegment)): flame api object\n\n    Returns:\n        bool: True or False\n    \"\"\"\n    tag_data = get_segment_data_marker(segment)\n\n    if not tag_data:\n        set_publish_attribute(segment, MARKER_PUBLISH_DEFAULT)\n        return MARKER_PUBLISH_DEFAULT\n\n    return tag_data[\"publish\"]\n\n\ndef create_segment_data_marker(segment):\n    \"\"\" Create openpype marker on a segment.\n\n    Attributes:\n        segment (flame.PySegment): flame api object\n\n    Returns:\n        flame.PyMarker: flame api object\n    \"\"\"\n    # get duration of segment\n    duration = segment.record_duration.relative_frame\n    # calculate start frame of the new marker\n    start_frame = int(segment.record_in.relative_frame) + int(duration / 2)\n    # create marker\n    marker = segment.create_marker(start_frame)\n    # set marker name\n    marker.name = MARKER_NAME\n    # set duration\n    marker.duration = MARKER_DURATION\n    # set colour\n    marker.colour = COLOR_MAP[MARKER_COLOR]  # Red\n\n    return marker\n\n\ndef get_sequence_segments(sequence, selected=False):\n    segments = []\n    # loop versions in sequence\n    for ver in sequence.versions:\n        # loop track in versions\n        for track in ver.tracks:\n            # ignore all empty tracks and hidden too\n            if len(track.segments) == 0 and track.hidden:\n                continue\n            # loop all segment in remaining tracks\n            for segment in track.segments:\n                if segment.name.get_value() == \"\":\n                    continue\n                if segment.hidden.get_value() is True:\n                    continue\n                if (\n                    selected is True\n                    and segment.selected.get_value() is not True\n                ):\n                    continue\n                # add it to original selection\n                segments.append(segment)\n    return segments\n\n\n@contextlib.contextmanager\ndef maintained_segment_selection(sequence):\n    \"\"\"Maintain selection during context\n\n    Attributes:\n        sequence (flame.PySequence): python api object\n\n    Yield:\n        list of flame.PySegment\n\n    Example:\n        >>> with maintained_segment_selection(sequence) as selected_segments:\n        ...     for segment in selected_segments:\n        ...         segment.selected = False\n        >>> print(segment.selected)\n        True\n    \"\"\"\n    selected_segments = get_sequence_segments(sequence, True)\n    try:\n        # do the operation on selected segments\n        yield selected_segments\n    finally:\n        # reset all selected clips\n        reset_segment_selection(sequence)\n        # select only original selection of segments\n        for segment in selected_segments:\n            segment.selected = True\n\n\ndef reset_segment_selection(sequence):\n    \"\"\"Deselect all selected nodes\n    \"\"\"\n    for ver in sequence.versions:\n        for track in ver.tracks:\n            if len(track.segments) == 0 and track.hidden:\n                continue\n            for segment in track.segments:\n                segment.selected = False\n\n\ndef _get_shot_tokens_values(clip, tokens):\n    old_value = None\n    output = {}\n\n    if not clip.shot_name:\n        return output\n\n    old_value = clip.shot_name.get_value()\n\n    for token in tokens:\n        clip.shot_name.set_value(token)\n        _key = str(re.sub(\"[<>]\", \"\", token)).replace(\" \", \"_\")\n\n        try:\n            output[_key] = int(clip.shot_name.get_value())\n        except ValueError:\n            output[_key] = clip.shot_name.get_value()\n\n    clip.shot_name.set_value(old_value)\n\n    return output\n\n\ndef get_segment_attributes(segment):\n    if segment.name.get_value() == \"\":\n        return None\n\n    # Add timeline segment to tree\n    clip_data = {\n        \"shot_name\": segment.shot_name.get_value(),\n        \"segment_name\": segment.name.get_value(),\n        \"segment_comment\": segment.comment.get_value(),\n        \"tape_name\": segment.tape_name,\n        \"source_name\": segment.source_name,\n        \"fpath\": segment.file_path,\n        \"PySegment\": segment\n    }\n\n    # head and tail with forward compatibility\n    if segment.head:\n        # `infinite` can be also returned\n        if isinstance(segment.head, str):\n            clip_data[\"segment_head\"] = 0\n        else:\n            clip_data[\"segment_head\"] = int(segment.head)\n    if segment.tail:\n        # `infinite` can be also returned\n        if isinstance(segment.tail, str):\n            clip_data[\"segment_tail\"] = 0\n        else:\n            clip_data[\"segment_tail\"] = int(segment.tail)\n\n    # add all available shot tokens\n    shot_tokens = _get_shot_tokens_values(segment, [\n        \"<colour space>\", \"<width>\", \"<height>\", \"<depth>\", \"<segment>\",\n        \"<track>\", \"<track name>\"\n    ])\n    clip_data.update(shot_tokens)\n\n    # populate shot source metadata\n    segment_attrs = [\n        \"record_duration\", \"record_in\", \"record_out\",\n        \"source_duration\", \"source_in\", \"source_out\"\n    ]\n    segment_attrs_data = {}\n    for attr_name in segment_attrs:\n        if not hasattr(segment, attr_name):\n            continue\n        attr = getattr(segment, attr_name)\n        segment_attrs_data[attr_name] = str(attr).replace(\"+\", \":\")\n\n        if attr_name in [\"record_in\", \"record_out\"]:\n            clip_data[attr_name] = attr.relative_frame\n        else:\n            clip_data[attr_name] = attr.frame\n\n    clip_data[\"segment_timecodes\"] = segment_attrs_data\n\n    return clip_data\n\n\ndef get_clips_in_reels(project):\n    output_clips = []\n    project_desktop = project.current_workspace.desktop\n\n    for reel_group in project_desktop.reel_groups:\n        for reel in reel_group.reels:\n            for clip in reel.clips:\n                clip_data = {\n                    \"PyClip\": clip,\n                    \"fps\": float(str(clip.frame_rate)[:-4])\n                }\n\n                attrs = [\n                    \"name\", \"width\", \"height\",\n                    \"ratio\", \"sample_rate\", \"bit_depth\"\n                ]\n\n                for attr in attrs:\n                    val = getattr(clip, attr)\n                    clip_data[attr] = val\n\n                version = clip.versions[-1]\n                track = version.tracks[-1]\n                for segment in track.segments:\n                    segment_data = get_segment_attributes(segment)\n                    clip_data.update(segment_data)\n\n                output_clips.append(clip_data)\n\n    return output_clips\n\n\ndef get_reformated_filename(filename, padded=True):\n    \"\"\"\n    Return fixed python expression path\n\n    Args:\n        filename (str): file name\n\n    Returns:\n        type: string with reformated path\n\n    Example:\n        get_reformated_filename(\"plate.1001.exr\") > plate.%04d.exr\n\n    \"\"\"\n    found = FRAME_PATTERN.search(filename)\n\n    if not found:\n        log.info(\"File name is not sequence: {}\".format(filename))\n        return filename\n\n    padding = get_padding_from_filename(filename)\n\n    replacement = \"%0{}d\".format(padding) if padded else \"%d\"\n    start_idx, end_idx = found.span(1)\n\n    return replacement.join(\n        [filename[:start_idx], filename[end_idx:]]\n    )\n\n\ndef get_padding_from_filename(filename):\n    \"\"\"\n    Return padding number from Flame path style\n\n    Args:\n        filename (str): file name\n\n    Returns:\n        int: padding number\n\n    Example:\n        get_padding_from_filename(\"plate.0001.exr\") > 4\n\n    \"\"\"\n    found = get_frame_from_filename(filename)\n\n    return len(found) if found else None\n\n\ndef get_frame_from_filename(filename):\n    \"\"\"\n    Return sequence number from Flame path style\n\n    Args:\n        filename (str): file name\n\n    Returns:\n        int: sequence frame number\n\n    Example:\n        def get_frame_from_filename(path):\n            (\"plate.0001.exr\") > 0001\n\n    \"\"\"\n\n    found = re.findall(FRAME_PATTERN, filename)\n\n    return found.pop() if found else None\n\n\n@contextlib.contextmanager\ndef maintained_object_duplication(item):\n    \"\"\"Maintain input item duplication\n\n    Attributes:\n        item (any flame.PyObject): python api object\n\n    Yield:\n        duplicate input PyObject type\n    \"\"\"\n    import flame\n    # Duplicate the clip to avoid modifying the original clip\n    duplicate = flame.duplicate(item)\n\n    try:\n        # do the operation on selected segments\n        yield duplicate\n    finally:\n        # delete the item at the end\n        flame.delete(duplicate)\n\n\n@contextlib.contextmanager\ndef maintained_temp_file_path(suffix=None):\n    _suffix = suffix or \"\"\n\n    try:\n        # Store dumped json to temporary file\n        temporary_file = tempfile.mktemp(\n            suffix=_suffix, prefix=\"flame_maintained_\")\n        yield temporary_file.replace(\"\\\\\", \"/\")\n\n    except IOError as _error:\n        raise IOError(\n            \"Not able to create temp json file: {}\".format(_error))\n\n    finally:\n        # Remove the temporary json\n        os.remove(temporary_file)\n\n\ndef get_clip_segment(flame_clip):\n    name = flame_clip.name.get_value()\n    version = flame_clip.versions[0]\n    track = version.tracks[0]\n    segments = track.segments\n\n    if len(segments) < 1:\n        raise ValueError(\"Clip `{}` has no segments!\".format(name))\n\n    if len(segments) > 1:\n        raise ValueError(\"Clip `{}` has too many segments!\".format(name))\n\n    return segments[0]\n\n\ndef get_batch_group_from_desktop(name):\n    project = get_current_project()\n    project_desktop = project.current_workspace.desktop\n\n    for bgroup in project_desktop.batch_groups:\n        if bgroup.name.get_value() in name:\n            return bgroup\n\n\nclass MediaInfoFile(object):\n    \"\"\"Class to get media info file clip data\n\n    Raises:\n        IOError: MEDIA_SCRIPT_PATH path doesn't exists\n        TypeError: Not able to generate clip xml data file\n        ET.ParseError: Missing clip in xml clip data\n        IOError: Not able to save xml clip data to file\n\n    Attributes:\n        str: `MEDIA_SCRIPT_PATH` path to flame binary\n        logging.Logger: `log` logger\n\n    TODO: add method for getting metadata to dict\n    \"\"\"\n    MEDIA_SCRIPT_PATH = \"/opt/Autodesk/mio/current/dl_get_media_info\"\n\n    log = log\n\n    _clip_data = None\n    _start_frame = None\n    _fps = None\n    _drop_mode = None\n    _file_pattern = None\n\n    def __init__(self, path, logger=None):\n\n        # replace log if any\n        if logger:\n            self.log = logger\n\n        # test if `dl_get_media_info` path exists\n        self._validate_media_script_path()\n\n        # derivate other feed variables\n        feed_basename = os.path.basename(path)\n        feed_dir = os.path.dirname(path)\n        feed_ext = os.path.splitext(feed_basename)[1][1:].lower()\n\n        with maintained_temp_file_path(\".clip\") as tmp_path:\n            self.log.info(\"Temp File: {}\".format(tmp_path))\n            self._generate_media_info_file(tmp_path, feed_ext, feed_dir)\n\n            # get collection containing feed_basename from path\n            self.file_pattern = self._get_collection(\n                feed_basename, feed_dir, feed_ext)\n\n            if (\n                not self.file_pattern\n                and os.path.exists(os.path.join(feed_dir, feed_basename))\n            ):\n                self.file_pattern = feed_basename\n\n            # get clip data and make them single if there is multiple\n            # clips data\n            xml_data = self._make_single_clip_media_info(\n                tmp_path, feed_basename, self.file_pattern)\n            self.log.debug(\"xml_data: {}\".format(xml_data))\n            self.log.debug(\"type: {}\".format(type(xml_data)))\n\n            # get all time related data and assign them\n            self._get_time_info_from_origin(xml_data)\n            self.log.debug(\"start_frame: {}\".format(self.start_frame))\n            self.log.debug(\"fps: {}\".format(self.fps))\n            self.log.debug(\"drop frame: {}\".format(self.drop_mode))\n            self.clip_data = xml_data\n\n    def _get_collection(self, feed_basename, feed_dir, feed_ext):\n        \"\"\" Get collection string\n\n        Args:\n            feed_basename (str): file base name\n            feed_dir (str): file's directory\n            feed_ext (str): file extension\n\n        Raises:\n            AttributeError: feed_ext is not matching feed_basename\n\n        Returns:\n            str: collection basename with range of sequence\n        \"\"\"\n        partialname = self._separate_file_head(feed_basename, feed_ext)\n        self.log.debug(\"__ partialname: {}\".format(partialname))\n\n        # make sure partial input basename is having correct extensoon\n        if not partialname:\n            raise AttributeError(\n                \"Wrong input attributes. Basename - {}, Ext - {}\".format(\n                    feed_basename, feed_ext\n                )\n            )\n\n        # get all related files\n        files = [\n            f for f in os.listdir(feed_dir)\n            if partialname == self._separate_file_head(f, feed_ext)\n        ]\n\n        # ignore reminders as we dont need them\n        collections = clique.assemble(files)[0]\n\n        # in case no collection found return None\n        # it is probably just single file\n        if not collections:\n            return\n\n        # we expect only one collection\n        collection = collections[0]\n\n        self.log.debug(\"__ collection: {}\".format(collection))\n\n        if collection.is_contiguous():\n            return self._format_collection(collection)\n\n        # add `[` in front to make sure it want capture\n        # shot name with the same number\n        number_from_path = self._separate_number(feed_basename, feed_ext)\n        search_number_pattern = \"[\" + number_from_path\n        # convert to multiple collections\n        _continues_colls = collection.separate()\n        for _coll in _continues_colls:\n            coll_to_text = self._format_collection(\n                _coll, len(number_from_path))\n            self.log.debug(\"__ coll_to_text: {}\".format(coll_to_text))\n            if search_number_pattern in coll_to_text:\n                return coll_to_text\n\n    @staticmethod\n    def _format_collection(collection, padding=None):\n        padding = padding or collection.padding\n        # if no holes then return collection\n        head = collection.format(\"{head}\")\n        tail = collection.format(\"{tail}\")\n        range_template = \"[{{:0{0}d}}-{{:0{0}d}}]\".format(\n            padding)\n        ranges = range_template.format(\n            min(collection.indexes),\n            max(collection.indexes)\n        )\n        # if no holes then return collection\n        return \"{}{}{}\".format(head, ranges, tail)\n\n    def _separate_file_head(self, basename, extension):\n        \"\"\" Get only head with out sequence and extension\n\n        Args:\n            basename (str): file base name\n            extension (str): file extension\n\n        Returns:\n            str: file head\n        \"\"\"\n        # in case sequence file\n        found = re.findall(\n            r\"(.*)[._][\\d]*(?=.{})\".format(extension),\n            basename,\n        )\n        if found:\n            return found.pop()\n\n        # in case single file\n        name, ext = os.path.splitext(basename)\n\n        if extension == ext[1:]:\n            return name\n\n    def _separate_number(self, basename, extension):\n        \"\"\" Get only sequence number as string\n\n        Args:\n            basename (str): file base name\n            extension (str): file extension\n\n        Returns:\n            str: number with padding\n        \"\"\"\n        # in case sequence file\n        found = re.findall(\n            r\"[._]([\\d]*)(?=.{})\".format(extension),\n            basename,\n        )\n        if found:\n            return found.pop()\n\n    @property\n    def clip_data(self):\n        \"\"\"Clip's xml clip data\n\n        Returns:\n            xml.etree.ElementTree: xml data\n        \"\"\"\n        return self._clip_data\n\n    @clip_data.setter\n    def clip_data(self, data):\n        self._clip_data = data\n\n    @property\n    def start_frame(self):\n        \"\"\" Clip's starting frame found in timecode\n\n        Returns:\n            int: number of frames\n        \"\"\"\n        return self._start_frame\n\n    @start_frame.setter\n    def start_frame(self, number):\n        self._start_frame = int(number)\n\n    @property\n    def fps(self):\n        \"\"\" Clip's frame rate\n\n        Returns:\n            float: frame rate\n        \"\"\"\n        return self._fps\n\n    @fps.setter\n    def fps(self, fl_number):\n        self._fps = float(fl_number)\n\n    @property\n    def drop_mode(self):\n        \"\"\" Clip's drop frame mode\n\n        Returns:\n            str: drop frame flag\n        \"\"\"\n        return self._drop_mode\n\n    @drop_mode.setter\n    def drop_mode(self, text):\n        self._drop_mode = str(text)\n\n    @property\n    def file_pattern(self):\n        \"\"\"Clips file patter\n\n        Returns:\n            str: file pattern. ex. file.[1-2].exr\n        \"\"\"\n        return self._file_pattern\n\n    @file_pattern.setter\n    def file_pattern(self, fpattern):\n        self._file_pattern = fpattern\n\n    def _validate_media_script_path(self):\n        if not os.path.isfile(self.MEDIA_SCRIPT_PATH):\n            raise IOError(\"Media Script does not exist: `{}`\".format(\n                self.MEDIA_SCRIPT_PATH))\n\n    def _generate_media_info_file(self, fpath, feed_ext, feed_dir):\n        \"\"\" Generate media info xml .clip file\n\n        Args:\n            fpath (str): .clip file path\n            feed_ext (str): file extension to be filtered\n            feed_dir (str): look up directory\n\n        Raises:\n            TypeError: Type error if it fails\n        \"\"\"\n        # Create cmd arguments for gettig xml file info file\n        cmd_args = [\n            self.MEDIA_SCRIPT_PATH,\n            \"-e\", feed_ext,\n            \"-o\", fpath,\n            feed_dir\n        ]\n\n        try:\n            # execute creation of clip xml template data\n            run_subprocess(cmd_args)\n        except TypeError as error:\n            raise TypeError(\n                \"Error creating `{}` due: {}\".format(fpath, error))\n\n    def _make_single_clip_media_info(self, fpath, feed_basename, path_pattern):\n        \"\"\" Separate only relative clip object form .clip file\n\n        Args:\n            fpath (str): clip file path\n            feed_basename (str): search basename\n            path_pattern (str): search file pattern (file.[1-2].exr)\n\n        Raises:\n            ET.ParseError: if nothing found\n\n        Returns:\n            ET.Element: xml element data of matching clip\n        \"\"\"\n        with open(fpath) as f:\n            lines = f.readlines()\n            _added_root = itertools.chain(\n                \"<root>\", deepcopy(lines)[1:], \"</root>\")\n            new_root = ET.fromstringlist(_added_root)\n\n        # find the clip which is matching to my input name\n        xml_clips = new_root.findall(\"clip\")\n        matching_clip = None\n        for xml_clip in xml_clips:\n            clip_name = xml_clip.find(\"name\").text\n            self.log.debug(\"__ clip_name: `{}`\".format(clip_name))\n            if clip_name not in feed_basename:\n                continue\n\n            # test path pattern\n            for out_track in xml_clip.iter(\"track\"):\n                for out_feed in out_track.iter(\"feed\"):\n                    for span in out_feed.iter(\"span\"):\n                        # start frame\n                        span_path = span.find(\"path\")\n                        self.log.debug(\n                            \"__ span_path.text: {}, path_pattern: {}\".format(\n                                span_path.text, path_pattern\n                            )\n                        )\n                        if path_pattern in span_path.text:\n                            matching_clip = xml_clip\n\n        if matching_clip is None:\n            # return warning there is missing clip\n            raise ET.ParseError(\n                \"Missing clip in `{}`. Available clips {}\".format(\n                    feed_basename, [\n                        xml_clip.find(\"name\").text\n                        for xml_clip in xml_clips\n                    ]\n                ))\n\n        return matching_clip\n\n    def _get_time_info_from_origin(self, xml_data):\n        \"\"\"Set time info to class attributes\n\n        Args:\n            xml_data (ET.Element): clip data\n        \"\"\"\n        try:\n            for out_track in xml_data.iter(\"track\"):\n                for out_feed in out_track.iter(\"feed\"):\n                    # start frame\n                    out_feed_nb_ticks_obj = out_feed.find(\n                        \"startTimecode/nbTicks\")\n                    self.start_frame = out_feed_nb_ticks_obj.text\n\n                    # fps\n                    out_feed_fps_obj = out_feed.find(\n                        \"startTimecode/rate\")\n                    self.fps = out_feed_fps_obj.text\n\n                    # drop frame mode\n                    out_feed_drop_mode_obj = out_feed.find(\n                        \"startTimecode/dropMode\")\n                    self.drop_mode = out_feed_drop_mode_obj.text\n                    break\n        except Exception as msg:\n            self.log.warning(msg)\n\n    @staticmethod\n    def write_clip_data_to_file(fpath, xml_element_data):\n        \"\"\" Write xml element of clip data to file\n\n        Args:\n            fpath (string): file path\n            xml_element_data (xml.etree.ElementTree.Element): xml data\n\n        Raises:\n            IOError: If data could not be written to file\n        \"\"\"\n        try:\n            # save it as new file\n            tree = cET.ElementTree(xml_element_data)\n            tree.write(\n                fpath, xml_declaration=True,\n                method=\"xml\", encoding=\"UTF-8\"\n            )\n        except IOError as error:\n            raise IOError(\n                \"Not able to write data to file: {}\".format(error))\n\n\nclass TimeEffectMetadata(object):\n    log = log\n    _data = {}\n    _retime_modes = {\n        0: \"speed\",\n        1: \"timewarp\",\n        2: \"duration\"\n    }\n\n    def __init__(self, segment, logger=None):\n        if logger:\n            self.log = logger\n\n        self._data = self._get_metadata(segment)\n\n    @property\n    def data(self):\n        \"\"\" Returns timewarp effect data\n\n        Returns:\n            dict: retime data\n        \"\"\"\n        return self._data\n\n    def _get_metadata(self, segment):\n        effects = segment.effects or []\n        for effect in effects:\n            if effect.type == \"Timewarp\":\n                with maintained_temp_file_path(\".timewarp_node\") as tmp_path:\n                    self.log.info(\"Temp File: {}\".format(tmp_path))\n                    effect.save_setup(tmp_path)\n                    return self._get_attributes_from_xml(tmp_path)\n\n        return {}\n\n    def _get_attributes_from_xml(self, tmp_path):\n        with open(tmp_path, \"r\") as tw_setup_file:\n            tw_setup_string = tw_setup_file.read()\n            tw_setup_file.close()\n\n        tw_setup_xml = ET.fromstring(tw_setup_string)\n        tw_setup = self._dictify(tw_setup_xml)\n        # pprint(tw_setup)\n        try:\n            tw_setup_state = tw_setup[\"Setup\"][\"State\"][0]\n            mode = int(\n                tw_setup_state[\"TW_RetimerMode\"][0][\"_text\"]\n            )\n            r_data = {\n                \"type\": self._retime_modes[mode],\n                \"effectStart\": int(\n                    tw_setup[\"Setup\"][\"Base\"][0][\"Range\"][0][\"Start\"]),\n                \"effectEnd\": int(\n                    tw_setup[\"Setup\"][\"Base\"][0][\"Range\"][0][\"End\"])\n            }\n\n            if mode == 0:  # speed\n                r_data[self._retime_modes[mode]] = float(\n                    tw_setup_state[\"TW_Speed\"]\n                    [0][\"Channel\"][0][\"Value\"][0][\"_text\"]\n                ) / 100\n            elif mode == 1:  # timewarp\n                print(\"timing\")\n                r_data[self._retime_modes[mode]] = self._get_anim_keys(\n                    tw_setup_state[\"TW_Timing\"]\n                )\n            elif mode == 2:  # duration\n                r_data[self._retime_modes[mode]] = {\n                    \"start\": {\n                        \"source\": int(\n                            tw_setup_state[\"TW_DurationTiming\"][0][\"Channel\"]\n                            [0][\"KFrames\"][0][\"Key\"][0][\"Value\"][0][\"_text\"]\n                        ),\n                        \"timeline\": int(\n                            tw_setup_state[\"TW_DurationTiming\"][0][\"Channel\"]\n                            [0][\"KFrames\"][0][\"Key\"][0][\"Frame\"][0][\"_text\"]\n                        )\n                    },\n                    \"end\": {\n                        \"source\": int(\n                            tw_setup_state[\"TW_DurationTiming\"][0][\"Channel\"]\n                            [0][\"KFrames\"][0][\"Key\"][1][\"Value\"][0][\"_text\"]\n                        ),\n                        \"timeline\": int(\n                            tw_setup_state[\"TW_DurationTiming\"][0][\"Channel\"]\n                            [0][\"KFrames\"][0][\"Key\"][1][\"Frame\"][0][\"_text\"]\n                        )\n                    }\n                }\n        except Exception:\n            lines = traceback.format_exception(*sys.exc_info())\n            self.log.error(\"\\n\".join(lines))\n            return\n\n        return r_data\n\n    def _get_anim_keys(self, setup_cat, index=None):\n        return_data = {\n            \"extrapolation\": (\n                setup_cat[0][\"Channel\"][0][\"Extrap\"][0][\"_text\"]\n            ),\n            \"animKeys\": []\n        }\n        for key in setup_cat[0][\"Channel\"][0][\"KFrames\"][0][\"Key\"]:\n            if index and int(key[\"Index\"]) != index:\n                continue\n            key_data = {\n                \"source\": float(key[\"Value\"][0][\"_text\"]),\n                \"timeline\": float(key[\"Frame\"][0][\"_text\"]),\n                \"index\": int(key[\"Index\"]),\n                \"curveMode\": key[\"CurveMode\"][0][\"_text\"],\n                \"curveOrder\": key[\"CurveOrder\"][0][\"_text\"]\n            }\n            if key.get(\"TangentMode\"):\n                key_data[\"tangentMode\"] = key[\"TangentMode\"][0][\"_text\"]\n\n            return_data[\"animKeys\"].append(key_data)\n\n        return return_data\n\n    def _dictify(self, xml_, root=True):\n        \"\"\" Convert xml object to dictionary\n\n        Args:\n            xml_ (xml.etree.ElementTree.Element): xml data\n            root (bool, optional): is root available. Defaults to True.\n\n        Returns:\n            dict: dictionarized xml\n        \"\"\"\n\n        if root:\n            return {xml_.tag: self._dictify(xml_, False)}\n\n        d = copy(xml_.attrib)\n        if xml_.text:\n            d[\"_text\"] = xml_.text\n\n        for x in xml_.findall(\"./*\"):\n            if x.tag not in d:\n                d[x.tag] = []\n            d[x.tag].append(self._dictify(x, False))\n        return d\n"
  },
  {
    "path": "openpype/hosts/flame/api/menu.py",
    "content": "from copy import deepcopy\nfrom pprint import pformat\n\nfrom qtpy import QtWidgets\n\nfrom openpype.pipeline import get_current_project_name\nfrom openpype.tools.utils.host_tools import HostToolsHelper\n\nmenu_group_name = 'OpenPype'\n\ndefault_flame_export_presets = {\n    'Publish': {\n        'PresetVisibility': 2,\n        'PresetType': 0,\n        'PresetFile': 'OpenEXR/OpenEXR (16-bit fp PIZ).xml'\n    },\n    'Preview': {\n        'PresetVisibility': 3,\n        'PresetType': 2,\n        'PresetFile': 'Generate Preview.xml'\n    },\n    'Thumbnail': {\n        'PresetVisibility': 3,\n        'PresetType': 0,\n        'PresetFile': 'Generate Thumbnail.xml'\n    }\n}\n\n\ndef callback_selection(selection, function):\n    import openpype.hosts.flame.api as opfapi\n    opfapi.CTX.selection = selection\n    print(\"Hook Selection: \\n\\t{}\".format(\n        pformat({\n            index: (type(item), item.name)\n            for index, item in enumerate(opfapi.CTX.selection)})\n    ))\n    function()\n\n\nclass _FlameMenuApp(object):\n    def __init__(self, framework):\n        self.name = self.__class__.__name__\n        self.framework = framework\n        self.log = framework.log\n        self.menu_group_name = menu_group_name\n        self.dynamic_menu_data = {}\n\n        # flame module is only available when a\n        # flame project is loaded and initialized\n        self.flame = None\n        try:\n            import flame\n            self.flame = flame\n        except ImportError:\n            self.flame = None\n\n        self.flame_project_name = flame.project.current_project.name\n        self.prefs = self.framework.prefs_dict(self.framework.prefs, self.name)\n        self.prefs_user = self.framework.prefs_dict(\n            self.framework.prefs_user, self.name)\n        self.prefs_global = self.framework.prefs_dict(\n            self.framework.prefs_global, self.name)\n\n        self.mbox = QtWidgets.QMessageBox()\n        project_name = get_current_project_name()\n        self.menu = {\n            \"actions\": [{\n                'name': project_name or \"project\",\n                'isEnabled': False\n            }],\n            \"name\": self.menu_group_name\n        }\n        self.tools_helper = HostToolsHelper()\n\n    def __getattr__(self, name):\n        def method(*args, **kwargs):\n            print('calling %s' % name)\n        return method\n\n    def rescan(self, *args, **kwargs):\n        if not self.flame:\n            try:\n                import flame\n                self.flame = flame\n            except ImportError:\n                self.flame = None\n\n        if self.flame:\n            self.flame.execute_shortcut('Rescan Python Hooks')\n            self.log.info('Rescan Python Hooks')\n\n\nclass FlameMenuProjectConnect(_FlameMenuApp):\n\n    # flameMenuProjectconnect app takes care of the preferences dialog as well\n\n    def __init__(self, framework):\n        _FlameMenuApp.__init__(self, framework)\n\n    def __getattr__(self, name):\n        def method(*args, **kwargs):\n            project = self.dynamic_menu_data.get(name)\n            if project:\n                self.link_project(project)\n        return method\n\n    def build_menu(self):\n        if not self.flame:\n            return []\n\n        menu = deepcopy(self.menu)\n\n        menu['actions'].append({\n            \"name\": \"Workfiles...\",\n            \"execute\": lambda x: self.tools_helper.show_workfiles()\n        })\n        menu['actions'].append({\n            \"name\": \"Load...\",\n            \"execute\": lambda x: self.tools_helper.show_loader()\n        })\n        menu['actions'].append({\n            \"name\": \"Manage...\",\n            \"execute\": lambda x: self.tools_helper.show_scene_inventory()\n        })\n        menu['actions'].append({\n            \"name\": \"Library...\",\n            \"execute\": lambda x: self.tools_helper.show_library_loader()\n        })\n        return menu\n\n    def refresh(self, *args, **kwargs):\n        self.rescan()\n\n    def rescan(self, *args, **kwargs):\n        if not self.flame:\n            try:\n                import flame\n                self.flame = flame\n            except ImportError:\n                self.flame = None\n\n        if self.flame:\n            self.flame.execute_shortcut('Rescan Python Hooks')\n            self.log.info('Rescan Python Hooks')\n\n\nclass FlameMenuTimeline(_FlameMenuApp):\n\n    # flameMenuProjectconnect app takes care of the preferences dialog as well\n\n    def __init__(self, framework):\n        _FlameMenuApp.__init__(self, framework)\n\n    def __getattr__(self, name):\n        def method(*args, **kwargs):\n            project = self.dynamic_menu_data.get(name)\n            if project:\n                self.link_project(project)\n        return method\n\n    def build_menu(self):\n        if not self.flame:\n            return []\n\n        menu = deepcopy(self.menu)\n\n        menu['actions'].append({\n            \"name\": \"Create...\",\n            \"execute\": lambda x: callback_selection(\n                x, self.tools_helper.show_creator)\n        })\n        menu['actions'].append({\n            \"name\": \"Publish...\",\n            \"execute\": lambda x: callback_selection(\n                x, self.tools_helper.show_publish)\n        })\n        menu['actions'].append({\n            \"name\": \"Load...\",\n            \"execute\": lambda x: self.tools_helper.show_loader()\n        })\n        menu['actions'].append({\n            \"name\": \"Manage...\",\n            \"execute\": lambda x: self.tools_helper.show_scene_inventory()\n        })\n        menu['actions'].append({\n            \"name\": \"Library...\",\n            \"execute\": lambda x: self.tools_helper.show_library_loader()\n        })\n        return menu\n\n    def refresh(self, *args, **kwargs):\n        self.rescan()\n\n    def rescan(self, *args, **kwargs):\n        if not self.flame:\n            try:\n                import flame\n                self.flame = flame\n            except ImportError:\n                self.flame = None\n\n        if self.flame:\n            self.flame.execute_shortcut('Rescan Python Hooks')\n            self.log.info('Rescan Python Hooks')\n\n\nclass FlameMenuUniversal(_FlameMenuApp):\n\n    # flameMenuProjectconnect app takes care of the preferences dialog as well\n\n    def __init__(self, framework):\n        _FlameMenuApp.__init__(self, framework)\n\n    def __getattr__(self, name):\n        def method(*args, **kwargs):\n            project = self.dynamic_menu_data.get(name)\n            if project:\n                self.link_project(project)\n        return method\n\n    def build_menu(self):\n        if not self.flame:\n            return []\n\n        menu = deepcopy(self.menu)\n\n        menu['actions'].append({\n            \"name\": \"Load...\",\n            \"execute\": lambda x: callback_selection(\n                x, self.tools_helper.show_loader)\n        })\n        menu['actions'].append({\n            \"name\": \"Manage...\",\n            \"execute\": lambda x: self.tools_helper.show_scene_inventory()\n        })\n        menu['actions'].append({\n            \"name\": \"Library...\",\n            \"execute\": lambda x: self.tools_helper.show_library_loader()\n        })\n        return menu\n\n    def refresh(self, *args, **kwargs):\n        self.rescan()\n\n    def rescan(self, *args, **kwargs):\n        if not self.flame:\n            try:\n                import flame\n                self.flame = flame\n            except ImportError:\n                self.flame = None\n\n        if self.flame:\n            self.flame.execute_shortcut('Rescan Python Hooks')\n            self.log.info('Rescan Python Hooks')\n"
  },
  {
    "path": "openpype/hosts/flame/api/pipeline.py",
    "content": "\"\"\"\nBasic avalon integration\n\"\"\"\nimport os\nimport contextlib\nfrom pyblish import api as pyblish\n\nfrom openpype.lib import Logger\nfrom openpype.pipeline import (\n    register_loader_plugin_path,\n    register_creator_plugin_path,\n    deregister_loader_plugin_path,\n    deregister_creator_plugin_path,\n    AVALON_CONTAINER_ID,\n)\nfrom .lib import (\n    set_segment_data_marker,\n    set_publish_attribute,\n    maintained_segment_selection,\n    get_current_sequence,\n    reset_segment_selection\n)\nfrom .. import HOST_DIR\n\nAPI_DIR = os.path.join(HOST_DIR, \"api\")\nPLUGINS_DIR = os.path.join(HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\n\nAVALON_CONTAINERS = \"AVALON_CONTAINERS\"\n\nlog = Logger.get_logger(__name__)\n\n\ndef install():\n    pyblish.register_host(\"flame\")\n    pyblish.register_plugin_path(PUBLISH_PATH)\n    register_loader_plugin_path(LOAD_PATH)\n    register_creator_plugin_path(CREATE_PATH)\n    log.info(\"OpenPype Flame plug-ins registered ...\")\n\n    # register callback for switching publishable\n    pyblish.register_callback(\"instanceToggled\", on_pyblish_instance_toggled)\n\n    log.info(\"OpenPype Flame host installed ...\")\n\n\ndef uninstall():\n    pyblish.deregister_host(\"flame\")\n\n    log.info(\"Deregistering Flame plug-ins..\")\n    pyblish.deregister_plugin_path(PUBLISH_PATH)\n    deregister_loader_plugin_path(LOAD_PATH)\n    deregister_creator_plugin_path(CREATE_PATH)\n\n    # register callback for switching publishable\n    pyblish.deregister_callback(\"instanceToggled\", on_pyblish_instance_toggled)\n\n    log.info(\"OpenPype Flame host uninstalled ...\")\n\n\ndef containerise(flame_clip_segment,\n                 name,\n                 namespace,\n                 context,\n                 loader=None,\n                 data=None):\n\n    data_imprint = {\n        \"schema\": \"openpype:container-2.0\",\n        \"id\": AVALON_CONTAINER_ID,\n        \"name\": str(name),\n        \"namespace\": str(namespace),\n        \"loader\": str(loader),\n        \"representation\": str(context[\"representation\"][\"_id\"]),\n    }\n\n    if data:\n        for k, v in data.items():\n            data_imprint[k] = v\n\n    log.debug(\"_ data_imprint: {}\".format(data_imprint))\n\n    set_segment_data_marker(flame_clip_segment, data_imprint)\n\n    return True\n\n\ndef ls():\n    \"\"\"List available containers.\n    \"\"\"\n    return []\n\n\ndef parse_container(tl_segment, validate=True):\n    \"\"\"Return container data from timeline_item's openpype tag.\n    \"\"\"\n    # TODO: parse_container\n    pass\n\n\ndef update_container(tl_segment, data=None):\n    \"\"\"Update container data to input timeline_item's openpype tag.\n    \"\"\"\n    # TODO: update_container\n    pass\n\n\ndef on_pyblish_instance_toggled(instance, old_value, new_value):\n    \"\"\"Toggle node passthrough states on instance toggles.\"\"\"\n\n    log.info(\"instance toggle: {}, old_value: {}, new_value:{} \".format(\n        instance, old_value, new_value))\n\n    # from openpype.hosts.resolve import (\n    #     set_publish_attribute\n    # )\n\n    # # Whether instances should be passthrough based on new value\n    # timeline_item = instance.data[\"item\"]\n    # set_publish_attribute(timeline_item, new_value)\n\n\ndef remove_instance(instance):\n    \"\"\"Remove instance marker from track item.\"\"\"\n    # TODO: remove_instance\n    pass\n\n\ndef list_instances():\n    \"\"\"List all created instances from current workfile.\"\"\"\n    # TODO: list_instances\n    pass\n\n\ndef imprint(segment, data=None):\n    \"\"\"\n    Adding openpype data to Flame timeline segment.\n\n    Also including publish attribute into tag.\n\n    Arguments:\n        segment (flame.PySegment)): flame api object\n        data (dict): Any data which needst to be imprinted\n\n    Examples:\n        data = {\n            'asset': 'sq020sh0280',\n            'family': 'render',\n            'subset': 'subsetMain'\n        }\n    \"\"\"\n    data = data or {}\n\n    set_segment_data_marker(segment, data)\n\n    # add publish attribute\n    set_publish_attribute(segment, True)\n\n\n@contextlib.contextmanager\ndef maintained_selection():\n    import flame\n    from .lib import CTX\n\n    # check if segment is selected\n    if isinstance(CTX.selection[0], flame.PySegment):\n        sequence = get_current_sequence(CTX.selection)\n\n        try:\n            with maintained_segment_selection(sequence) as selected:\n                yield\n        finally:\n            # reset all selected clips\n            reset_segment_selection(sequence)\n            # select only original selection of segments\n            for segment in selected:\n                segment.selected = True\n"
  },
  {
    "path": "openpype/hosts/flame/api/plugin.py",
    "content": "import os\nimport re\nimport shutil\nfrom copy import deepcopy\nfrom xml.etree import ElementTree as ET\n\nimport qargparse\nfrom qtpy import QtCore, QtWidgets\n\nfrom openpype import style\nfrom openpype.lib import Logger, StringTemplate\nfrom openpype.pipeline import LegacyCreator, LoaderPlugin\nfrom openpype.pipeline.colorspace import get_remapped_colorspace_to_native\nfrom openpype.settings import get_current_project_settings\n\nfrom . import constants\nfrom . import lib as flib\nfrom . import pipeline as fpipeline\n\nlog = Logger.get_logger(__name__)\n\n\nclass CreatorWidget(QtWidgets.QDialog):\n\n    # output items\n    items = dict()\n    _results_back = None\n\n    def __init__(self, name, info, ui_inputs, parent=None):\n        super(CreatorWidget, self).__init__(parent)\n\n        self.setObjectName(name)\n\n        self.setWindowFlags(\n            QtCore.Qt.Window\n            | QtCore.Qt.CustomizeWindowHint\n            | QtCore.Qt.WindowTitleHint\n            | QtCore.Qt.WindowCloseButtonHint\n            | QtCore.Qt.WindowStaysOnTopHint\n        )\n        self.setWindowTitle(name or \"Pype Creator Input\")\n        self.resize(500, 700)\n\n        # Where inputs and labels are set\n        self.content_widget = [QtWidgets.QWidget(self)]\n        top_layout = QtWidgets.QFormLayout(self.content_widget[0])\n        top_layout.setObjectName(\"ContentLayout\")\n        top_layout.addWidget(Spacer(5, self))\n\n        # first add widget tag line\n        top_layout.addWidget(QtWidgets.QLabel(info))\n\n        # main dynamic layout\n        self.scroll_area = QtWidgets.QScrollArea(self, widgetResizable=True)\n        self.scroll_area.setVerticalScrollBarPolicy(\n            QtCore.Qt.ScrollBarAsNeeded)\n        self.scroll_area.setVerticalScrollBarPolicy(\n            QtCore.Qt.ScrollBarAlwaysOn)\n        self.scroll_area.setHorizontalScrollBarPolicy(\n            QtCore.Qt.ScrollBarAlwaysOff)\n        self.scroll_area.setWidgetResizable(True)\n\n        self.content_widget.append(self.scroll_area)\n\n        scroll_widget = QtWidgets.QWidget(self)\n        in_scroll_area = QtWidgets.QVBoxLayout(scroll_widget)\n        self.content_layout = [in_scroll_area]\n\n        # add preset data into input widget layout\n        self.items = self.populate_widgets(ui_inputs)\n        self.scroll_area.setWidget(scroll_widget)\n\n        # Confirmation buttons\n        btns_widget = QtWidgets.QWidget(self)\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\")\n        btns_layout.addWidget(cancel_btn)\n\n        ok_btn = QtWidgets.QPushButton(\"Ok\")\n        btns_layout.addWidget(ok_btn)\n\n        # Main layout of the dialog\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(10, 10, 10, 10)\n        main_layout.setSpacing(0)\n\n        # adding content widget\n        for w in self.content_widget:\n            main_layout.addWidget(w)\n\n        main_layout.addWidget(btns_widget)\n\n        ok_btn.clicked.connect(self._on_ok_clicked)\n        cancel_btn.clicked.connect(self._on_cancel_clicked)\n\n        self.setStyleSheet(style.load_stylesheet())\n\n    @classmethod\n    def set_results_back(cls, value):\n        cls._results_back = value\n\n    @classmethod\n    def get_results_back(cls):\n        return cls._results_back\n\n    def _on_ok_clicked(self):\n        log.debug(\"ok is clicked: {}\".format(self.items))\n        results_back = self._values(self.items)\n        self.set_results_back(results_back)\n        self.close()\n\n    def _on_cancel_clicked(self):\n        self.set_results_back(None)\n        self.close()\n\n    def showEvent(self, event):\n        self.set_results_back(None)\n        super(CreatorWidget, self).showEvent(event)\n\n    def _values(self, data, new_data=None):\n        new_data = new_data or dict()\n        for k, v in data.items():\n            new_data[k] = {\n                \"target\": None,\n                \"value\": None\n            }\n            if v[\"type\"] == \"dict\":\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = self._values(v[\"value\"])\n            if v[\"type\"] == \"section\":\n                new_data.pop(k)\n                new_data = self._values(v[\"value\"], new_data)\n            elif getattr(v[\"value\"], \"currentText\", None):\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = v[\"value\"].currentText()\n            elif getattr(v[\"value\"], \"isChecked\", None):\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = v[\"value\"].isChecked()\n            elif getattr(v[\"value\"], \"value\", None):\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = v[\"value\"].value()\n            elif getattr(v[\"value\"], \"text\", None):\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = v[\"value\"].text()\n\n        return new_data\n\n    def camel_case_split(self, text):\n        matches = re.finditer(\n            '.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', text)\n        return \" \".join([str(m.group(0)).capitalize() for m in matches])\n\n    def create_row(self, layout, type_name, text, **kwargs):\n        # get type attribute from qwidgets\n        attr = getattr(QtWidgets, type_name)\n\n        # convert label text to normal capitalized text with spaces\n        label_text = self.camel_case_split(text)\n\n        # assign the new text to label widget\n        label = QtWidgets.QLabel(label_text)\n        label.setObjectName(\"LineLabel\")\n\n        # create attribute name text strip of spaces\n        attr_name = text.replace(\" \", \"\")\n\n        # create attribute and assign default values\n        setattr(\n            self,\n            attr_name,\n            attr(parent=self))\n\n        # assign the created attribute to variable\n        item = getattr(self, attr_name)\n        for func, val in kwargs.items():\n            if getattr(item, func):\n                func_attr = getattr(item, func)\n                func_attr(val)\n\n        # add to layout\n        layout.addRow(label, item)\n\n        return item\n\n    def populate_widgets(self, data, content_layout=None):\n        \"\"\"\n        Populate widget from input dict.\n\n        Each plugin has its own set of widget rows defined in dictionary\n        each row values should have following keys: `type`, `target`,\n        `label`, `order`, `value` and optionally also `toolTip`.\n\n        Args:\n            data (dict): widget rows or organized groups defined\n                         by types `dict` or `section`\n            content_layout (QtWidgets.QFormLayout)[optional]: used when nesting\n\n        Returns:\n            dict: redefined data dict updated with created widgets\n\n        \"\"\"\n\n        content_layout = content_layout or self.content_layout[-1]\n        # fix order of process by defined order value\n        ordered_keys = list(data.keys())\n        for k, v in data.items():\n            try:\n                # try removing a key from index which should\n                # be filled with new\n                ordered_keys.pop(v[\"order\"])\n            except IndexError:\n                pass\n            # add key into correct order\n            ordered_keys.insert(v[\"order\"], k)\n\n        # process ordered\n        for k in ordered_keys:\n            v = data[k]\n            tool_tip = v.get(\"toolTip\", \"\")\n            if v[\"type\"] == \"dict\":\n                self.content_layout.append(QtWidgets.QWidget(self))\n                content_layout.addWidget(self.content_layout[-1])\n                self.content_layout[-1].setObjectName(\"sectionHeadline\")\n\n                headline = QtWidgets.QVBoxLayout(self.content_layout[-1])\n                headline.addWidget(Spacer(20, self))\n                headline.addWidget(QtWidgets.QLabel(v[\"label\"]))\n\n                # adding nested layout with label\n                self.content_layout.append(QtWidgets.QWidget(self))\n                self.content_layout[-1].setObjectName(\"sectionContent\")\n\n                nested_content_layout = QtWidgets.QFormLayout(\n                    self.content_layout[-1])\n                nested_content_layout.setObjectName(\"NestedContentLayout\")\n                content_layout.addWidget(self.content_layout[-1])\n\n                # add nested key as label\n                data[k][\"value\"] = self.populate_widgets(\n                    v[\"value\"], nested_content_layout)\n\n            if v[\"type\"] == \"section\":\n                self.content_layout.append(QtWidgets.QWidget(self))\n                content_layout.addWidget(self.content_layout[-1])\n                self.content_layout[-1].setObjectName(\"sectionHeadline\")\n\n                headline = QtWidgets.QVBoxLayout(self.content_layout[-1])\n                headline.addWidget(Spacer(20, self))\n                headline.addWidget(QtWidgets.QLabel(v[\"label\"]))\n\n                # adding nested layout with label\n                self.content_layout.append(QtWidgets.QWidget(self))\n                self.content_layout[-1].setObjectName(\"sectionContent\")\n\n                nested_content_layout = QtWidgets.QFormLayout(\n                    self.content_layout[-1])\n                nested_content_layout.setObjectName(\"NestedContentLayout\")\n                content_layout.addWidget(self.content_layout[-1])\n\n                # add nested key as label\n                data[k][\"value\"] = self.populate_widgets(\n                    v[\"value\"], nested_content_layout)\n\n            elif v[\"type\"] == \"QLineEdit\":\n                data[k][\"value\"] = self.create_row(\n                    content_layout, \"QLineEdit\", v[\"label\"],\n                    setText=v[\"value\"], setToolTip=tool_tip)\n            elif v[\"type\"] == \"QComboBox\":\n                data[k][\"value\"] = self.create_row(\n                    content_layout, \"QComboBox\", v[\"label\"],\n                    addItems=v[\"value\"], setToolTip=tool_tip)\n            elif v[\"type\"] == \"QCheckBox\":\n                data[k][\"value\"] = self.create_row(\n                    content_layout, \"QCheckBox\", v[\"label\"],\n                    setChecked=v[\"value\"], setToolTip=tool_tip)\n            elif v[\"type\"] == \"QSpinBox\":\n                data[k][\"value\"] = self.create_row(\n                    content_layout, \"QSpinBox\", v[\"label\"],\n                    setValue=v[\"value\"], setMinimum=0,\n                    setMaximum=100000, setToolTip=tool_tip)\n        return data\n\n\nclass Spacer(QtWidgets.QWidget):\n    def __init__(self, height, *args, **kwargs):\n        super(self.__class__, self).__init__(*args, **kwargs)\n\n        self.setFixedHeight(height)\n\n        real_spacer = QtWidgets.QWidget(self)\n        real_spacer.setObjectName(\"Spacer\")\n        real_spacer.setFixedHeight(height)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(real_spacer)\n\n        self.setLayout(layout)\n\n\nclass Creator(LegacyCreator):\n    \"\"\"Creator class wrapper\n    \"\"\"\n    clip_color = constants.COLOR_MAP[\"purple\"]\n    rename_index = None\n\n    def __init__(self, *args, **kwargs):\n        super(Creator, self).__init__(*args, **kwargs)\n        self.presets = get_current_project_settings()[\n            \"flame\"][\"create\"].get(self.__class__.__name__, {})\n\n        # adding basic current context flame objects\n        self.project = flib.get_current_project()\n        self.sequence = flib.get_current_sequence(flib.CTX.selection)\n\n        if (self.options or {}).get(\"useSelection\"):\n            self.selected = flib.get_sequence_segments(self.sequence, True)\n        else:\n            self.selected = flib.get_sequence_segments(self.sequence)\n\n    def create_widget(self, *args, **kwargs):\n        widget = CreatorWidget(*args, **kwargs)\n        widget.exec_()\n        return widget.get_results_back()\n\n\nclass PublishableClip:\n    \"\"\"\n    Convert a segment to publishable instance\n\n    Args:\n        segment (flame.PySegment): flame api object\n        kwargs (optional): additional data needed for rename=True (presets)\n\n    Returns:\n        flame.PySegment: flame api object\n    \"\"\"\n    vertical_clip_match = {}\n    marker_data = {}\n    types = {\n        \"shot\": \"shot\",\n        \"folder\": \"folder\",\n        \"episode\": \"episode\",\n        \"sequence\": \"sequence\",\n        \"track\": \"sequence\",\n    }\n\n    # parents search pattern\n    parents_search_pattern = r\"\\{([a-z]*?)\\}\"\n\n    # default templates for non-ui use\n    rename_default = False\n    hierarchy_default = \"{_folder_}/{_sequence_}/{_track_}\"\n    clip_name_default = \"shot_{_trackIndex_:0>3}_{_clipIndex_:0>4}\"\n    subset_name_default = \"[ track name ]\"\n    review_track_default = \"[ none ]\"\n    subset_family_default = \"plate\"\n    count_from_default = 10\n    count_steps_default = 10\n    vertical_sync_default = False\n    driving_layer_default = \"\"\n    index_from_segment_default = False\n    use_shot_name_default = False\n    include_handles_default = False\n    retimed_handles_default = True\n    retimed_framerange_default = True\n\n    def __init__(self, segment, **kwargs):\n        self.rename_index = kwargs[\"rename_index\"]\n        self.family = kwargs[\"family\"]\n        self.log = kwargs[\"log\"]\n\n        # get main parent objects\n        self.current_segment = segment\n        sequence_name = flib.get_current_sequence([segment]).name.get_value()\n        self.sequence_name = str(sequence_name).replace(\" \", \"_\")\n\n        self.clip_data = flib.get_segment_attributes(segment)\n        # segment (clip) main attributes\n        self.cs_name = self.clip_data[\"segment_name\"]\n        self.cs_index = int(self.clip_data[\"segment\"])\n        self.shot_name = self.clip_data[\"shot_name\"]\n\n        # get track name and index\n        self.track_index = int(self.clip_data[\"track\"])\n        track_name = self.clip_data[\"track_name\"]\n        self.track_name = str(track_name).replace(\" \", \"_\").replace(\n            \"*\", \"noname{}\".format(self.track_index))\n\n        # adding tag.family into tag\n        if kwargs.get(\"avalon\"):\n            self.marker_data.update(kwargs[\"avalon\"])\n\n        # add publish attribute to marker data\n        self.marker_data.update({\"publish\": True})\n\n        # adding ui inputs if any\n        self.ui_inputs = kwargs.get(\"ui_inputs\", {})\n\n        self.log.info(\"Inside of plugin: {}\".format(\n            self.marker_data\n        ))\n        # populate default data before we get other attributes\n        self._populate_segment_default_data()\n\n        # use all populated default data to create all important attributes\n        self._populate_attributes()\n\n        # create parents with correct types\n        self._create_parents()\n\n    def convert(self):\n\n        # solve segment data and add them to marker data\n        self._convert_to_marker_data()\n\n        # if track name is in review track name and also if driving track name\n        # is not in review track name: skip tag creation\n        if (self.track_name in self.review_layer) and (\n                self.driving_layer not in self.review_layer):\n            return\n\n        # deal with clip name\n        new_name = self.marker_data.pop(\"newClipName\")\n\n        if self.rename and not self.use_shot_name:\n            # rename segment\n            self.current_segment.name = str(new_name)\n            self.marker_data[\"asset\"] = str(new_name)\n        elif self.use_shot_name:\n            self.marker_data[\"asset\"] = self.shot_name\n            self.marker_data[\"hierarchyData\"][\"shot\"] = self.shot_name\n        else:\n            self.marker_data[\"asset\"] = self.cs_name\n            self.marker_data[\"hierarchyData\"][\"shot\"] = self.cs_name\n\n        if self.marker_data[\"heroTrack\"] and self.review_layer:\n            self.marker_data[\"reviewTrack\"] = self.review_layer\n        else:\n            self.marker_data[\"reviewTrack\"] = None\n\n        # create pype tag on track_item and add data\n        fpipeline.imprint(self.current_segment, self.marker_data)\n\n        return self.current_segment\n\n    def _populate_segment_default_data(self):\n        \"\"\" Populate default formatting data from segment. \"\"\"\n\n        self.current_segment_default_data = {\n            \"_folder_\": \"shots\",\n            \"_sequence_\": self.sequence_name,\n            \"_track_\": self.track_name,\n            \"_clip_\": self.cs_name,\n            \"_trackIndex_\": self.track_index,\n            \"_clipIndex_\": self.cs_index\n        }\n\n    def _populate_attributes(self):\n        \"\"\" Populate main object attributes. \"\"\"\n        # segment frame range and parent track name for vertical sync check\n        self.clip_in = int(self.clip_data[\"record_in\"])\n        self.clip_out = int(self.clip_data[\"record_out\"])\n\n        # define ui inputs if non gui mode was used\n        self.shot_num = self.cs_index\n        self.log.debug(\n            \"____ self.shot_num: {}\".format(self.shot_num))\n\n        # ui_inputs data or default values if gui was not used\n        self.rename = self.ui_inputs.get(\n            \"clipRename\", {}).get(\"value\") or self.rename_default\n        self.use_shot_name = self.ui_inputs.get(\n            \"useShotName\", {}).get(\"value\") or self.use_shot_name_default\n        self.clip_name = self.ui_inputs.get(\n            \"clipName\", {}).get(\"value\") or self.clip_name_default\n        self.hierarchy = self.ui_inputs.get(\n            \"hierarchy\", {}).get(\"value\") or self.hierarchy_default\n        self.hierarchy_data = self.ui_inputs.get(\n            \"hierarchyData\", {}).get(\"value\") or \\\n            self.current_segment_default_data.copy()\n        self.index_from_segment = self.ui_inputs.get(\n            \"segmentIndex\", {}).get(\"value\") or self.index_from_segment_default\n        self.count_from = self.ui_inputs.get(\n            \"countFrom\", {}).get(\"value\") or self.count_from_default\n        self.count_steps = self.ui_inputs.get(\n            \"countSteps\", {}).get(\"value\") or self.count_steps_default\n        self.subset_name = self.ui_inputs.get(\n            \"subsetName\", {}).get(\"value\") or self.subset_name_default\n        self.subset_family = self.ui_inputs.get(\n            \"subsetFamily\", {}).get(\"value\") or self.subset_family_default\n        self.vertical_sync = self.ui_inputs.get(\n            \"vSyncOn\", {}).get(\"value\") or self.vertical_sync_default\n        self.driving_layer = self.ui_inputs.get(\n            \"vSyncTrack\", {}).get(\"value\") or self.driving_layer_default\n        self.review_track = self.ui_inputs.get(\n            \"reviewTrack\", {}).get(\"value\") or self.review_track_default\n        self.audio = self.ui_inputs.get(\n            \"audio\", {}).get(\"value\") or False\n        self.include_handles = self.ui_inputs.get(\n            \"includeHandles\", {}).get(\"value\") or self.include_handles_default\n        self.retimed_handles = (\n            self.ui_inputs.get(\"retimedHandles\", {}).get(\"value\")\n            or self.retimed_handles_default\n        )\n        self.retimed_framerange = (\n            self.ui_inputs.get(\"retimedFramerange\", {}).get(\"value\")\n            or self.retimed_framerange_default\n        )\n\n        # build subset name from layer name\n        if self.subset_name == \"[ track name ]\":\n            self.subset_name = self.track_name\n\n        # create subset for publishing\n        self.subset = self.subset_family + self.subset_name.capitalize()\n\n    def _replace_hash_to_expression(self, name, text):\n        \"\"\" Replace hash with number in correct padding. \"\"\"\n        _spl = text.split(\"#\")\n        _len = (len(_spl) - 1)\n        _repl = \"{{{0}:0>{1}}}\".format(name, _len)\n        return text.replace((\"#\" * _len), _repl)\n\n    def _convert_to_marker_data(self):\n        \"\"\" Convert internal data to marker data.\n\n        Populating the marker data into internal variable self.marker_data\n        \"\"\"\n        # define vertical sync attributes\n        hero_track = True\n        self.review_layer = \"\"\n        if self.vertical_sync and self.track_name not in self.driving_layer:\n            # if it is not then define vertical sync as None\n            hero_track = False\n\n        # increasing steps by index of rename iteration\n        if not self.index_from_segment:\n            self.count_steps *= self.rename_index\n\n        hierarchy_formatting_data = {}\n        hierarchy_data = deepcopy(self.hierarchy_data)\n        _data = self.current_segment_default_data.copy()\n        if self.ui_inputs:\n            # adding tag metadata from ui\n            for _k, _v in self.ui_inputs.items():\n                if _v[\"target\"] == \"tag\":\n                    self.marker_data[_k] = _v[\"value\"]\n\n            # driving layer is set as positive match\n            if hero_track or self.vertical_sync:\n                # mark review layer\n                if self.review_track and (\n                        self.review_track not in self.review_track_default):\n                    # if review layer is defined and not the same as default\n                    self.review_layer = self.review_track\n\n                # shot num calculate\n                if self.index_from_segment:\n                    # use clip index from timeline\n                    self.shot_num = self.count_steps * self.cs_index\n                else:\n                    if self.rename_index == 0:\n                        self.shot_num = self.count_from\n                    else:\n                        self.shot_num = self.count_from + self.count_steps\n\n            # clip name sequence number\n            _data.update({\"shot\": self.shot_num})\n\n            # solve # in test to pythonic expression\n            for _k, _v in hierarchy_data.items():\n                if \"#\" not in _v[\"value\"]:\n                    continue\n                hierarchy_data[\n                    _k][\"value\"] = self._replace_hash_to_expression(\n                        _k, _v[\"value\"])\n\n            # fill up pythonic expresisons in hierarchy data\n            for k, _v in hierarchy_data.items():\n                hierarchy_formatting_data[k] = _v[\"value\"].format(**_data)\n        else:\n            # if no gui mode then just pass default data\n            hierarchy_formatting_data = hierarchy_data\n\n        tag_hierarchy_data = self._solve_tag_hierarchy_data(\n            hierarchy_formatting_data\n        )\n\n        tag_hierarchy_data.update({\"heroTrack\": True})\n        if hero_track and self.vertical_sync:\n            self.vertical_clip_match.update({\n                (self.clip_in, self.clip_out): tag_hierarchy_data\n            })\n\n        if not hero_track and self.vertical_sync:\n            # driving layer is set as negative match\n            for (_in, _out), hero_data in self.vertical_clip_match.items():\n                \"\"\"\n                Since only one instance of hero clip is expected in\n                `self.vertical_clip_match`, this will loop only once\n                until none hero clip will be matched with hero clip.\n\n                `tag_hierarchy_data` will be set only once for every\n                clip which is not hero clip.\n                \"\"\"\n                _hero_data = deepcopy(hero_data)\n                _hero_data.update({\"heroTrack\": False})\n                if _in <= self.clip_in and _out >= self.clip_out:\n                    data_subset = hero_data[\"subset\"]\n                    # add track index in case duplicity of names in hero data\n                    if self.subset in data_subset:\n                        _hero_data[\"subset\"] = self.subset + str(\n                            self.track_index)\n                    # in case track name and subset name is the same then add\n                    if self.subset_name == self.track_name:\n                        _hero_data[\"subset\"] = self.subset\n                    # assign data to return hierarchy data to tag\n                    tag_hierarchy_data = _hero_data\n                    break\n\n        # add data to return data dict\n        self.marker_data.update(tag_hierarchy_data)\n\n    def _solve_tag_hierarchy_data(self, hierarchy_formatting_data):\n        \"\"\" Solve marker data from hierarchy data and templates. \"\"\"\n        # fill up clip name and hierarchy keys\n        hierarchy_filled = self.hierarchy.format(**hierarchy_formatting_data)\n        clip_name_filled = self.clip_name.format(**hierarchy_formatting_data)\n\n        # remove shot from hierarchy data: is not needed anymore\n        hierarchy_formatting_data.pop(\"shot\")\n\n        return {\n            \"newClipName\": clip_name_filled,\n            \"hierarchy\": hierarchy_filled,\n            \"parents\": self.parents,\n            \"hierarchyData\": hierarchy_formatting_data,\n            \"subset\": self.subset,\n            \"family\": self.subset_family,\n            \"families\": [self.family]\n        }\n\n    def _convert_to_entity(self, type, template):\n        \"\"\" Converting input key to key with type. \"\"\"\n        # convert to entity type\n        entity_type = self.types.get(type, None)\n\n        assert entity_type, \"Missing entity type for `{}`\".format(\n            type\n        )\n\n        # first collect formatting data to use for formatting template\n        formatting_data = {}\n        for _k, _v in self.hierarchy_data.items():\n            value = _v[\"value\"].format(\n                **self.current_segment_default_data)\n            formatting_data[_k] = value\n\n        return {\n            \"entity_type\": entity_type,\n            \"entity_name\": template.format(\n                **formatting_data\n            )\n        }\n\n    def _create_parents(self):\n        \"\"\" Create parents and return it in list. \"\"\"\n        self.parents = []\n\n        pattern = re.compile(self.parents_search_pattern)\n\n        par_split = [(pattern.findall(t).pop(), t)\n                     for t in self.hierarchy.split(\"/\")]\n\n        for type, template in par_split:\n            parent = self._convert_to_entity(type, template)\n            self.parents.append(parent)\n\n\n# Publishing plugin functions\n\n# Loader plugin functions\nclass ClipLoader(LoaderPlugin):\n    \"\"\"A basic clip loader for Flame\n\n    This will implement the basic behavior for a loader to inherit from that\n    will containerize the reference and will implement the `remove` and\n    `update` logic.\n\n    \"\"\"\n    log = log\n\n    options = [\n        qargparse.Boolean(\n            \"handles\",\n            label=\"Set handles\",\n            default=0,\n            help=\"Also set handles to clip as In/Out marks\"\n        )\n    ]\n\n    _mapping = None\n    _host_settings = None\n\n    def apply_settings(cls, project_settings, system_settings):\n\n        plugin_type_settings = (\n            project_settings\n            .get(\"flame\", {})\n            .get(\"load\", {})\n        )\n\n        if not plugin_type_settings:\n            return\n\n        plugin_name = cls.__name__\n\n        plugin_settings = None\n        # Look for plugin settings in host specific settings\n        if plugin_name in plugin_type_settings:\n            plugin_settings = plugin_type_settings[plugin_name]\n\n        if not plugin_settings:\n            return\n\n        print(\">>> We have preset for {}\".format(plugin_name))\n        for option, value in plugin_settings.items():\n            if option == \"enabled\" and value is False:\n                print(\"  - is disabled by preset\")\n            elif option == \"representations\":\n                continue\n            else:\n                print(\"  - setting `{}`: `{}`\".format(option, value))\n            setattr(cls, option, value)\n\n    def get_colorspace(self, context):\n        \"\"\"Get colorspace name\n\n        Look either to version data or representation data.\n\n        Args:\n            context (dict): version context data\n\n        Returns:\n            str: colorspace name or None\n        \"\"\"\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        colorspace = version_data.get(\n            \"colorspace\", None\n        )\n\n        if (\n            not colorspace\n            or colorspace == \"Unknown\"\n        ):\n            colorspace = context[\"representation\"][\"data\"].get(\n                \"colorspace\", None)\n\n        return colorspace\n\n    @classmethod\n    def get_native_colorspace(cls, input_colorspace):\n        \"\"\"Return native colorspace name.\n\n        Args:\n            input_colorspace (str | None): colorspace name\n\n        Returns:\n            str: native colorspace name defined in mapping or None\n        \"\"\"\n        # TODO: rewrite to support only pipeline's remapping\n        if not cls._host_settings:\n            cls._host_settings = get_current_project_settings()[\"flame\"]\n\n        # [Deprecated] way of remapping\n        if not cls._mapping:\n            mapping = (\n                cls._host_settings[\"imageio\"][\"profilesMapping\"][\"inputs\"])\n            cls._mapping = {\n                input[\"ocioName\"]: input[\"flameName\"]\n                for input in mapping\n            }\n\n        native_name = cls._mapping.get(input_colorspace)\n\n        if not native_name:\n            native_name = get_remapped_colorspace_to_native(\n                input_colorspace, \"flame\", cls._host_settings[\"imageio\"])\n\n        return native_name\n\n\nclass OpenClipSolver(flib.MediaInfoFile):\n    create_new_clip = False\n\n    log = log\n\n    def __init__(self, openclip_file_path, feed_data, logger=None):\n        self.out_file = openclip_file_path\n\n        # replace log if any\n        if logger:\n            self.log = logger\n\n        # new feed variables:\n        feed_path = feed_data.pop(\"path\")\n\n        # initialize parent class\n        super(OpenClipSolver, self).__init__(\n            feed_path,\n            logger=logger\n        )\n\n        # get other metadata\n        self.feed_version_name = feed_data[\"version\"]\n        self.feed_colorspace = feed_data.get(\"colorspace\")\n        self.log.debug(\"feed_version_name: {}\".format(self.feed_version_name))\n\n        # layer rename variables\n        self.layer_rename_template = feed_data[\"layer_rename_template\"]\n        self.layer_rename_patterns = feed_data[\"layer_rename_patterns\"]\n        self.context_data = feed_data[\"context_data\"]\n\n        # derivate other feed variables\n        self.feed_basename = os.path.basename(feed_path)\n        self.feed_dir = os.path.dirname(feed_path)\n        self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower()\n        self.log.debug(\"feed_ext: {}\".format(self.feed_ext))\n        self.log.debug(\"out_file: {}\".format(self.out_file))\n        if not self._is_valid_tmp_file(self.out_file):\n            self.create_new_clip = True\n\n    def _is_valid_tmp_file(self, file):\n        # check if file exists\n        if os.path.isfile(file):\n            # test also if file is not empty\n            with open(file) as f:\n                lines = f.readlines()\n\n            if len(lines) > 2:\n                return True\n\n            # file is probably corrupted\n            os.remove(file)\n            return False\n\n    def make(self):\n\n        if self.create_new_clip:\n            # New openClip\n            self._create_new_open_clip()\n        else:\n            self._update_open_clip()\n\n    def _clear_handler(self, xml_object):\n        for handler in xml_object.findall(\"./handler\"):\n            self.log.info(\"Handler found\")\n            xml_object.remove(handler)\n\n    def _create_new_open_clip(self):\n        self.log.info(\"Building new openClip\")\n\n        for tmp_xml_track in self.clip_data.iter(\"track\"):\n            # solve track (layer) name\n            self._rename_track_name(tmp_xml_track)\n\n            tmp_xml_feeds = tmp_xml_track.find('feeds')\n            tmp_xml_feeds.set('currentVersion', self.feed_version_name)\n\n            for tmp_feed in tmp_xml_track.iter(\"feed\"):\n                tmp_feed.set('vuid', self.feed_version_name)\n\n                # add colorspace if any is set\n                if self.feed_colorspace:\n                    self._add_colorspace(tmp_feed, self.feed_colorspace)\n\n                self._clear_handler(tmp_feed)\n\n        tmp_xml_versions_obj = self.clip_data.find('versions')\n        tmp_xml_versions_obj.set('currentVersion', self.feed_version_name)\n        for xml_new_version in tmp_xml_versions_obj:\n            xml_new_version.set('uid', self.feed_version_name)\n            xml_new_version.set('type', 'version')\n\n        self._clear_handler(self.clip_data)\n        self.log.info(\"Adding feed version: {}\".format(self.feed_basename))\n\n        self.write_clip_data_to_file(self.out_file, self.clip_data)\n\n    def _get_xml_track_obj_by_uid(self, xml_data, uid):\n        # loop all tracks of input xml data\n        for xml_track in xml_data.iter(\"track\"):\n            track_uid = xml_track.get(\"uid\")\n            self.log.debug(\n                \">> track_uid:uid: {}:{}\".format(track_uid, uid))\n\n            # get matching uids\n            if uid == track_uid:\n                return xml_track\n\n    def _rename_track_name(self, xml_track_data):\n        layer_uid = xml_track_data.get(\"uid\")\n        name_obj = xml_track_data.find(\"name\")\n        layer_name = name_obj.text\n\n        if (\n            self.layer_rename_patterns\n            and not any(\n                re.search(lp_.lower(), layer_name.lower())\n                for lp_ in self.layer_rename_patterns\n            )\n        ):\n            return\n\n        formatting_data = self._update_formatting_data(\n            layerName=layer_name,\n            layerUID=layer_uid\n        )\n        name_obj.text = StringTemplate(\n            self.layer_rename_template\n        ).format(formatting_data)\n\n    def _update_formatting_data(self, **kwargs):\n        \"\"\" Updating formatting data for layer rename\n\n        Attributes:\n            key=value (optional): will be included to formatting data\n                                  as {key: value}\n        Returns:\n            dict: anatomy context data for formatting\n        \"\"\"\n        self.log.debug(\">> self.clip_data: {}\".format(self.clip_data))\n        clip_name_obj = self.clip_data.find(\"name\")\n        data = {\n            \"originalBasename\": clip_name_obj.text\n        }\n        # include version context data\n        data.update(self.context_data)\n        # include input kwargs data\n        data.update(kwargs)\n        return data\n\n    def _update_open_clip(self):\n        self.log.info(\"Updating openClip ..\")\n\n        out_xml = ET.parse(self.out_file)\n        out_xml = out_xml.getroot()\n\n        self.log.debug(\">> out_xml: {}\".format(out_xml))\n        # loop tmp tracks\n        updated_any = False\n        for tmp_xml_track in self.clip_data.iter(\"track\"):\n            # solve track (layer) name\n            self._rename_track_name(tmp_xml_track)\n\n            # get tmp track uid\n            tmp_track_uid = tmp_xml_track.get(\"uid\")\n            self.log.debug(\">> tmp_track_uid: {}\".format(tmp_track_uid))\n\n            # get out data track by uid\n            out_track_element = self._get_xml_track_obj_by_uid(\n                out_xml, tmp_track_uid)\n            self.log.debug(\n                \">> out_track_element: {}\".format(out_track_element))\n\n            # loop tmp feeds\n            for tmp_xml_feed in tmp_xml_track.iter(\"feed\"):\n                new_path_obj = tmp_xml_feed.find(\n                    \"spans/span/path\")\n                new_path = new_path_obj.text\n\n                # check if feed path already exists in track's feeds\n                if (\n                    out_track_element is not None\n                    and self._feed_exists(out_track_element, new_path)\n                ):\n                    continue\n\n                # rename versions on feeds\n                tmp_xml_feed.set('vuid', self.feed_version_name)\n                self._clear_handler(tmp_xml_feed)\n\n                # update fps from MediaInfoFile class\n                if self.fps is not None:\n                    tmp_feed_fps_obj = tmp_xml_feed.find(\n                        \"startTimecode/rate\")\n                    tmp_feed_fps_obj.text = str(self.fps)\n\n                # update start_frame from MediaInfoFile class\n                if self.start_frame is not None:\n                    tmp_feed_nb_ticks_obj = tmp_xml_feed.find(\n                        \"startTimecode/nbTicks\")\n                    tmp_feed_nb_ticks_obj.text = str(self.start_frame)\n\n                # update drop_mode from MediaInfoFile class\n                if self.drop_mode is not None:\n                    tmp_feed_drop_mode_obj = tmp_xml_feed.find(\n                        \"startTimecode/dropMode\")\n                    tmp_feed_drop_mode_obj.text = str(self.drop_mode)\n\n                # add colorspace if any is set\n                if self.feed_colorspace is not None:\n                    self._add_colorspace(tmp_xml_feed, self.feed_colorspace)\n\n                # then append/update feed to correct track in output\n                if out_track_element:\n                    self.log.debug(\"updating track element ..\")\n                    # update already present track\n                    out_feeds = out_track_element.find('feeds')\n                    out_feeds.set('currentVersion', self.feed_version_name)\n                    out_feeds.append(tmp_xml_feed)\n\n                    self.log.info(\n                        \"Appending new feed: {}\".format(\n                            self.feed_version_name))\n                else:\n                    self.log.debug(\"adding new track element ..\")\n                    # create new track as it doesnt exists yet\n                    # set current version to feeds on tmp\n                    tmp_xml_feeds = tmp_xml_track.find('feeds')\n                    tmp_xml_feeds.set('currentVersion', self.feed_version_name)\n                    out_tracks = out_xml.find(\"tracks\")\n                    out_tracks.append(tmp_xml_track)\n\n                updated_any = True\n\n        if updated_any:\n            # Append vUID to versions\n            out_xml_versions_obj = out_xml.find('versions')\n            out_xml_versions_obj.set(\n                'currentVersion', self.feed_version_name)\n            new_version_obj = ET.Element(\n                \"version\", {\"type\": \"version\", \"uid\": self.feed_version_name})\n            out_xml_versions_obj.insert(0, new_version_obj)\n\n            self._clear_handler(out_xml)\n\n            # fist create backup\n            self._create_openclip_backup_file(self.out_file)\n\n            self.log.info(\"Adding feed version: {}\".format(\n                self.feed_version_name))\n\n            self.write_clip_data_to_file(self.out_file, out_xml)\n\n            self.log.debug(\"OpenClip Updated: {}\".format(self.out_file))\n\n    def _feed_exists(self, xml_data, path):\n        # loop all available feed paths and check if\n        # the path is not already in file\n        for src_path in xml_data.iter('path'):\n            if path == src_path.text:\n                self.log.warning(\n                    \"Not appending file as it already is in .clip file\")\n                return True\n\n    def _create_openclip_backup_file(self, file):\n        bck_file = \"{}.bak\".format(file)\n        # if backup does not exist\n        if not os.path.isfile(bck_file):\n            shutil.copy2(file, bck_file)\n        else:\n            # in case it exists and is already multiplied\n            created = False\n            for _i in range(1, 99):\n                bck_file = \"{name}.bak.{idx:0>2}\".format(\n                    name=file,\n                    idx=_i)\n                # create numbered backup file\n                if not os.path.isfile(bck_file):\n                    shutil.copy2(file, bck_file)\n                    created = True\n                    break\n            # in case numbered does not exists\n            if not created:\n                bck_file = \"{}.bak.last\".format(file)\n                shutil.copy2(file, bck_file)\n\n    def _add_colorspace(self, feed_obj, profile_name):\n        feed_storage_obj = feed_obj.find(\"storageFormat\")\n        feed_clr_obj = feed_storage_obj.find(\"colourSpace\")\n        if feed_clr_obj is not None:\n            feed_clr_obj = ET.Element(\n                \"colourSpace\", {\"type\": \"string\"})\n            feed_clr_obj.text = profile_name\n            feed_storage_obj.append(feed_clr_obj)\n"
  },
  {
    "path": "openpype/hosts/flame/api/render_utils.py",
    "content": "import os\nfrom xml.etree import ElementTree as ET\nfrom openpype.lib import Logger\n\nlog = Logger.get_logger(__name__)\n\n\ndef export_clip(export_path, clip, preset_path, **kwargs):\n    \"\"\"Flame exported wrapper\n\n    Args:\n        export_path (str): exporting directory path\n        clip (PyClip): flame api object\n        preset_path (str): full export path to xml file\n\n    Kwargs:\n        thumb_frame_number (int)[optional]: source frame number\n        in_mark (int)[optional]: cut in mark\n        out_mark (int)[optional]: cut out mark\n\n    Raises:\n        KeyError: Missing input kwarg `thumb_frame_number`\n                  in case `thumbnail` in `export_preset`\n        FileExistsError: Missing export preset in shared folder\n    \"\"\"\n    import flame\n\n    in_mark = out_mark = None\n\n    # Set exporter\n    exporter = flame.PyExporter()\n    exporter.foreground = True\n    exporter.export_between_marks = True\n\n    if kwargs.get(\"thumb_frame_number\"):\n        thumb_frame_number = kwargs[\"thumb_frame_number\"]\n        # make sure it exists in kwargs\n        if not thumb_frame_number:\n            raise KeyError(\n                \"Missing key `thumb_frame_number` in input kwargs\")\n\n        in_mark = int(thumb_frame_number)\n        out_mark = int(thumb_frame_number) + 1\n\n    elif kwargs.get(\"in_mark\") and kwargs.get(\"out_mark\"):\n        in_mark = int(kwargs[\"in_mark\"])\n        out_mark = int(kwargs[\"out_mark\"])\n    else:\n        exporter.export_between_marks = False\n\n    try:\n        # set in and out marks if they are available\n        if in_mark and out_mark:\n            clip.in_mark = in_mark\n            clip.out_mark = out_mark\n\n        # export with exporter\n        exporter.export(clip, preset_path, export_path)\n    finally:\n        print('Exported: {} at {}-{}'.format(\n            clip.name.get_value(),\n            clip.in_mark,\n            clip.out_mark\n        ))\n\n\ndef get_preset_path_by_xml_name(xml_preset_name):\n    def _search_path(root):\n        output = []\n        for root, _dirs, files in os.walk(root):\n            for f in files:\n                if f != xml_preset_name:\n                    continue\n                file_path = os.path.join(root, f)\n                output.append(file_path)\n        return output\n\n    def _validate_results(results):\n        if results and len(results) == 1:\n            return results.pop()\n        elif results and len(results) > 1:\n            print((\n                \"More matching presets for `{}`: /n\"\n                \"{}\").format(xml_preset_name, results))\n            return results.pop()\n        else:\n            return None\n\n    from .utils import (\n        get_flame_install_root,\n        get_flame_version\n    )\n\n    # get actual flame version and install path\n    _version = get_flame_version()[\"full\"]\n    _install_root = get_flame_install_root()\n\n    # search path templates\n    shared_search_root = \"{install_root}/shared/export/presets\"\n    install_search_root = (\n        \"{install_root}/presets/{version}/export/presets/flame\")\n\n    # fill templates\n    shared_search_root = shared_search_root.format(\n        install_root=_install_root\n    )\n    install_search_root = install_search_root.format(\n        install_root=_install_root,\n        version=_version\n    )\n\n    # get search results\n    shared_results = _search_path(shared_search_root)\n    installed_results = _search_path(install_search_root)\n\n    # first try to return shared results\n    shared_preset_path = _validate_results(shared_results)\n\n    if shared_preset_path:\n        return os.path.dirname(shared_preset_path)\n\n    # then try installed results\n    installed_preset_path = _validate_results(installed_results)\n\n    if installed_preset_path:\n        return os.path.dirname(installed_preset_path)\n\n    # if nothing found then return False\n    return False\n\n\ndef modify_preset_file(xml_path, staging_dir, data):\n    \"\"\"Modify xml preset with input data\n\n    Args:\n        xml_path (str ): path for input xml preset\n        staging_dir (str): staging dir path\n        data (dict): data where key is xmlTag and value as string\n\n    Returns:\n        str: _description_\n    \"\"\"\n    # create temp path\n    dirname, basename = os.path.split(xml_path)\n    temp_path = os.path.join(staging_dir, basename)\n\n    # change xml following data keys\n    with open(xml_path, \"r\") as datafile:\n        _root = ET.parse(datafile)\n\n        for key, value in data.items():\n            try:\n                if \"/\" in key:\n                    if not key.startswith(\"./\"):\n                        key = \".//\" + key\n\n                    split_key_path = key.split(\"/\")\n                    element_key = split_key_path[-1]\n                    parent_obj_path = \"/\".join(split_key_path[:-1])\n\n                    parent_obj = _root.find(parent_obj_path)\n                    element_obj = parent_obj.find(element_key)\n                    if not element_obj:\n                        append_element(parent_obj, element_key, value)\n                else:\n                    finds = _root.findall(\".//{}\".format(key))\n                    if not finds:\n                        raise AttributeError\n                    for element in finds:\n                        element.text = str(value)\n            except AttributeError:\n                log.warning(\n                    \"Cannot create attribute: {}: {}. Skipping\".format(\n                        key, value\n                    ))\n        _root.write(temp_path)\n\n    return temp_path\n\n\ndef append_element(root_element_obj, key, value):\n    new_element_obj = ET.Element(key)\n    log.debug(\"__ new_element_obj: {}\".format(new_element_obj))\n    new_element_obj.text = str(value)\n    root_element_obj.insert(0, new_element_obj)\n"
  },
  {
    "path": "openpype/hosts/flame/api/scripts/wiretap_com.py",
    "content": "#!/usr/bin/env python2.7\n# -*- coding: utf-8 -*-\n\nfrom __future__ import absolute_import\nimport os\nimport sys\nimport subprocess\nimport json\nimport xml.dom.minidom as minidom\nfrom copy import deepcopy\nimport datetime\nfrom libwiretapPythonClientAPI import (  # noqa\n    WireTapClientInit,\n    WireTapClientUninit,\n    WireTapNodeHandle,\n    WireTapServerHandle,\n    WireTapInt,\n    WireTapStr\n)\n\n\nclass WireTapCom(object):\n    \"\"\"\n    Comunicator class wrapper for talking to WireTap db.\n\n    This way we are able to set new project with settings and\n    correct colorspace policy. Also we are able to create new user\n    or get actual user with similar name (users are usually cloning\n    their profiles and adding date stamp into suffix).\n    \"\"\"\n\n    def __init__(self, host_name=None, volume_name=None, group_name=None):\n        \"\"\"Initialisation of WireTap communication class\n\n        Args:\n            host_name (str, optional): Name of host server. Defaults to None.\n            volume_name (str, optional): Name of volume. Defaults to None.\n            group_name (str, optional): Name of user group. Defaults to None.\n        \"\"\"\n        # set main attributes of server\n        # if there are none set the default installation\n        self.host_name = host_name or \"localhost\"\n        self.volume_name = volume_name or \"stonefs\"\n        self.group_name = group_name or \"staff\"\n\n        # wiretap tools dir path\n        self.wiretap_tools_dir = os.getenv(\"OPENPYPE_WIRETAP_TOOLS\")\n\n        # initialize WireTap client\n        WireTapClientInit()\n\n        # add the server to shared variable\n        self._server = WireTapServerHandle(\"{}:IFFFS\".format(self.host_name))\n        print(\"WireTap connected at '{}'...\".format(\n            self.host_name))\n\n    def close(self):\n        self._server = None\n        WireTapClientUninit()\n        print(\"WireTap closed...\")\n\n    def get_launch_args(\n            self, project_name, project_data, user_name, *args, **kwargs):\n        \"\"\"Forming launch arguments for OpenPype launcher.\n\n        Args:\n            project_name (str): name of project\n            project_data (dict): Flame compatible project data\n            user_name (str): name of user\n\n        Returns:\n            list: arguments\n        \"\"\"\n\n        workspace_name = kwargs.get(\"workspace_name\")\n        color_policy = kwargs.get(\"color_policy\")\n\n        project_exists = self._project_prep(project_name)\n        if not project_exists:\n            self._set_project_settings(project_name, project_data)\n            self._set_project_colorspace(project_name, color_policy)\n\n        user_name = self._user_prep(user_name)\n\n        if workspace_name is None:\n            # default workspace\n            print(\"Using a default workspace\")\n            return [\n                \"--start-project={}\".format(project_name),\n                \"--start-user={}\".format(user_name),\n                \"--create-workspace\"\n            ]\n\n        else:\n            print(\n                \"Using a custom workspace '{}'\".format(workspace_name))\n\n            self._workspace_prep(project_name, workspace_name)\n            return [\n                \"--start-project={}\".format(project_name),\n                \"--start-user={}\".format(user_name),\n                \"--create-workspace\",\n                \"--start-workspace={}\".format(workspace_name)\n            ]\n\n    def _workspace_prep(self, project_name, workspace_name):\n        \"\"\"Preparing a workspace\n\n        In case it doesn not exists it will create one\n\n        Args:\n            project_name (str): project name\n            workspace_name (str): workspace name\n\n        Raises:\n            AttributeError: unable to create workspace\n        \"\"\"\n        workspace_exists = self._child_is_in_parent_path(\n            \"/projects/{}\".format(project_name), workspace_name, \"WORKSPACE\"\n        )\n        if not workspace_exists:\n            project = WireTapNodeHandle(\n                self._server, \"/projects/{}\".format(project_name))\n\n            workspace_node = WireTapNodeHandle()\n            created_workspace = project.createNode(\n                workspace_name, \"WORKSPACE\", workspace_node)\n\n            if not created_workspace:\n                raise AttributeError(\n                    \"Cannot create workspace `{}` in \"\n                    \"project `{}`: `{}`\".format(\n                        workspace_name, project_name, project.lastError())\n                )\n\n        print(\n            \"Workspace `{}` is successfully created\".format(workspace_name))\n\n    def _project_prep(self, project_name):\n        \"\"\"Preparing a project\n\n        In case it doesn not exists it will create one\n\n        Args:\n            project_name (str): project name\n\n        Raises:\n            AttributeError: unable to create project\n        \"\"\"\n        # test if projeft exists\n        project_exists = self._child_is_in_parent_path(\n            \"/projects\", project_name, \"PROJECT\")\n\n        if not project_exists:\n            volumes = self._get_all_volumes()\n\n            if len(volumes) == 0:\n                raise AttributeError(\n                    \"Not able to create new project. No Volumes existing\"\n                )\n\n            # check if volumes exists\n            if self.volume_name not in volumes:\n                raise AttributeError(\n                    (\"Volume '{}' does not exist in '{}'\").format(\n                        self.volume_name, volumes)\n                )\n\n            # form cmd arguments\n            project_create_cmd = [\n                os.path.join(\n                    self.wiretap_tools_dir,\n                    \"wiretap_create_node\"\n                ),\n                '-n',\n                os.path.join(\"/volumes\", self.volume_name),\n                '-d',\n                project_name,\n                '-g',\n            ]\n\n            project_create_cmd.append(self.group_name)\n\n            print(project_create_cmd)\n\n            exit_code = subprocess.call(\n                project_create_cmd,\n                cwd=os.path.expanduser('~'),\n                preexec_fn=_subprocess_preexec_fn\n            )\n\n            if exit_code != 0:\n                RuntimeError(\"Cannot create project in flame db\")\n\n            print(\n                \"A new project '{}' is created.\".format(project_name))\n        return project_exists\n\n    def _get_all_volumes(self):\n        \"\"\"Request all available volumens from WireTap\n\n        Returns:\n            list: all available volumes in server\n\n        Rises:\n            AttributeError: unable to get any volumes children from server\n        \"\"\"\n        root = WireTapNodeHandle(self._server, \"/volumes\")\n        children_num = WireTapInt(0)\n\n        get_children_num = root.getNumChildren(children_num)\n        if not get_children_num:\n            raise AttributeError(\n                \"Cannot get number of volumes: {}\".format(root.lastError())\n            )\n\n        volumes = []\n\n        # go through all children and get volume names\n        child_obj = WireTapNodeHandle()\n        for child_idx in range(children_num):\n\n            # get a child\n            if not root.getChild(child_idx, child_obj):\n                raise AttributeError(\n                    \"Unable to get child: {}\".format(root.lastError()))\n\n            node_name = WireTapStr()\n            get_children_name = child_obj.getDisplayName(node_name)\n\n            if not get_children_name:\n                raise AttributeError(\n                    \"Unable to get child name: {}\".format(\n                        child_obj.lastError())\n                )\n\n            volumes.append(node_name.c_str())\n\n        return volumes\n\n    def _user_prep(self, user_name):\n        \"\"\"Ensuring user does exists in user's stack\n\n        Args:\n            user_name (str): name of a user\n\n        Raises:\n            AttributeError: unable to create user\n        \"\"\"\n\n        # get all used usernames in db\n        used_names = self._get_usernames()\n        print(\">> used_names: {}\".format(used_names))\n\n        # filter only those which are sharing input user name\n        filtered_users = [user for user in used_names if user_name in user]\n\n        if filtered_users:\n            # TODO: need to find lastly created following regex pattern for\n            # date used in name\n            return filtered_users.pop()\n\n        # create new user name with date in suffix\n        now = datetime.datetime.now()  # current date and time\n        date = now.strftime(\"%Y%m%d\")\n        new_user_name = \"{}_{}\".format(user_name, date)\n        print(new_user_name)\n\n        if not self._child_is_in_parent_path(\"/users\", new_user_name, \"USER\"):\n            # Create the new user\n            users = WireTapNodeHandle(self._server, \"/users\")\n\n            user_node = WireTapNodeHandle()\n            created_user = users.createNode(new_user_name, \"USER\", user_node)\n            if not created_user:\n                raise AttributeError(\n                    \"User {} cannot be created: {}\".format(\n                        new_user_name, users.lastError())\n                )\n\n            print(\"User `{}` is created\".format(new_user_name))\n            return new_user_name\n\n    def _get_usernames(self):\n        \"\"\"Requesting all available users from WireTap\n\n        Returns:\n            list: all available user names\n\n        Raises:\n            AttributeError: there are no users in server\n        \"\"\"\n        root = WireTapNodeHandle(self._server, \"/users\")\n        children_num = WireTapInt(0)\n\n        get_children_num = root.getNumChildren(children_num)\n        if not get_children_num:\n            raise AttributeError(\n                \"Cannot get number of volumes: {}\".format(root.lastError())\n            )\n\n        usernames = []\n\n        # go through all children and get volume names\n        child_obj = WireTapNodeHandle()\n        for child_idx in range(children_num):\n\n            # get a child\n            if not root.getChild(child_idx, child_obj):\n                raise AttributeError(\n                    \"Unable to get child: {}\".format(root.lastError()))\n\n            node_name = WireTapStr()\n            get_children_name = child_obj.getDisplayName(node_name)\n\n            if not get_children_name:\n                raise AttributeError(\n                    \"Unable to get child name: {}\".format(\n                        child_obj.lastError())\n                )\n\n            usernames.append(node_name.c_str())\n\n        return usernames\n\n    def _child_is_in_parent_path(self, parent_path, child_name, child_type):\n        \"\"\"Checking if a given child is in parent path.\n\n        Args:\n            parent_path (str): db path to parent\n            child_name (str): name of child\n            child_type (str): type of child\n\n        Raises:\n            AttributeError: Not able to get number of children\n            AttributeError: Not able to get children form parent\n            AttributeError: Not able to get children name\n            AttributeError: Not able to get children type\n\n        Returns:\n            bool: True if child is in parent path\n        \"\"\"\n        parent = WireTapNodeHandle(self._server, parent_path)\n\n        # iterate number of children\n        children_num = WireTapInt(0)\n        requested = parent.getNumChildren(children_num)\n        if not requested:\n            raise AttributeError((\n                \"Error: Cannot request number of \"\n                \"children from the node {}. Make sure your \"\n                \"wiretap service is running: {}\").format(\n                    parent_path, parent.lastError())\n            )\n\n        # iterate children\n        child_obj = WireTapNodeHandle()\n        for child_idx in range(children_num):\n            if not parent.getChild(child_idx, child_obj):\n                raise AttributeError(\n                    \"Cannot get child: {}\".format(\n                        parent.lastError()))\n\n            node_name = WireTapStr()\n            node_type = WireTapStr()\n\n            if not child_obj.getDisplayName(node_name):\n                raise AttributeError(\n                    \"Unable to get child name: %s\" % child_obj.lastError()\n                )\n            if not child_obj.getNodeTypeStr(node_type):\n                raise AttributeError(\n                    \"Unable to obtain child type: %s\" % child_obj.lastError()\n                )\n\n            if (node_name.c_str() == child_name) and (\n                    node_type.c_str() == child_type):\n                return True\n\n        return False\n\n    def _set_project_settings(self, project_name, project_data):\n        \"\"\"Setting project attributes.\n\n        Args:\n            project_name (str): name of project\n            project_data (dict): data with project attributes\n                                 (flame compatible)\n\n        Raises:\n            AttributeError: Not able to set project attributes\n        \"\"\"\n        # generated xml from project_data dict\n        _xml = \"<Project>\"\n        for key, value in project_data.items():\n            _xml += \"<{}>{}</{}>\".format(key, value, key)\n        _xml += \"</Project>\"\n\n        pretty_xml = minidom.parseString(_xml).toprettyxml()\n        print(\"__ xml: {}\".format(pretty_xml))\n\n        # set project data to wiretap\n        project_node = WireTapNodeHandle(\n            self._server, \"/projects/{}\".format(project_name))\n\n        if not project_node.setMetaData(\"XML\", _xml):\n            raise AttributeError(\n                \"Not able to set project attributes {}. Error: {}\".format(\n                    project_name, project_node.lastError())\n            )\n\n        print(\"Project settings successfully set.\")\n\n    def _set_project_colorspace(self, project_name, color_policy):\n        \"\"\"Set project's colorspace policy.\n\n        Args:\n            project_name (str): name of project\n            color_policy (str): name of policy\n\n        Raises:\n            RuntimeError: Not able to set colorspace policy\n        \"\"\"\n        color_policy = color_policy or \"Legacy\"\n\n        # check if the colour policy in custom dir\n        if \"/\" in color_policy:\n            # if unlikelly full path was used make it redundant\n            color_policy = color_policy.replace(\"/syncolor/policies/\", \"\")\n            # expecting input is `Shared/NameOfPolicy`\n            color_policy = \"/syncolor/policies/{}\".format(\n                color_policy)\n        else:\n            color_policy = \"/syncolor/policies/Autodesk/{}\".format(\n                color_policy)\n\n        # create arguments\n        project_colorspace_cmd = [\n            os.path.join(\n                self.wiretap_tools_dir,\n                \"wiretap_duplicate_node\"\n            ),\n            \"-s\",\n            color_policy,\n            \"-n\",\n            \"/projects/{}/syncolor\".format(project_name)\n        ]\n\n        print(project_colorspace_cmd)\n\n        exit_code = subprocess.call(\n            project_colorspace_cmd,\n            cwd=os.path.expanduser('~'),\n            preexec_fn=_subprocess_preexec_fn\n        )\n\n        if exit_code != 0:\n            RuntimeError(\"Cannot set colorspace {} on project {}\".format(\n                color_policy, project_name\n            ))\n\n\ndef _subprocess_preexec_fn():\n    \"\"\" Helper function\n\n    Setting permission mask to 0777\n    \"\"\"\n    os.setpgrp()\n    os.umask(0o000)\n\n\nif __name__ == \"__main__\":\n    # get json exchange data\n    json_path = sys.argv[-1]\n    json_data = open(json_path).read()\n    in_data = json.loads(json_data)\n    out_data = deepcopy(in_data)\n\n    # get main server attributes\n    host_name = in_data.pop(\"host_name\")\n    volume_name = in_data.pop(\"volume_name\")\n    group_name = in_data.pop(\"group_name\")\n\n    # initialize class\n    wiretap_handler = WireTapCom(host_name, volume_name, group_name)\n\n    try:\n        app_args = wiretap_handler.get_launch_args(\n            project_name=in_data.pop(\"project_name\"),\n            project_data=in_data.pop(\"project_data\"),\n            user_name=in_data.pop(\"user_name\"),\n            **in_data\n        )\n    finally:\n        wiretap_handler.close()\n\n    # set returned args back to out data\n    out_data.update({\n        \"app_args\": app_args\n    })\n\n    # write it out back to the exchange json file\n    with open(json_path, \"w\") as file_stream:\n        json.dump(out_data, file_stream, indent=4)\n"
  },
  {
    "path": "openpype/hosts/flame/api/utils.py",
    "content": "\"\"\"\nFlame utils for syncing scripts\n\"\"\"\n\nimport os\nimport shutil\nfrom openpype.lib import Logger\nlog = Logger.get_logger(__name__)\n\n\ndef _sync_utility_scripts(env=None):\n    \"\"\" Synchronizing basic utlility scripts for flame.\n\n    To be able to run start OpenPype within Flame we have to copy\n    all utility_scripts and additional FLAME_SCRIPT_DIR into\n    `/opt/Autodesk/shared/python`. This will be always synchronizing those\n    folders.\n    \"\"\"\n    from .. import HOST_DIR\n\n    env = env or os.environ\n\n    # initiate inputs\n    scripts = {}\n    fsd_env = env.get(\"FLAME_SCRIPT_DIRS\", \"\")\n    flame_shared_dir = \"/opt/Autodesk/shared/python\"\n\n    fsd_paths = [os.path.join(\n        HOST_DIR,\n        \"api\",\n        \"utility_scripts\"\n    )]\n\n    # collect script dirs\n    log.info(\"FLAME_SCRIPT_DIRS: `{fsd_env}`\".format(**locals()))\n    log.info(\"fsd_paths: `{fsd_paths}`\".format(**locals()))\n\n    # add application environment setting for FLAME_SCRIPT_DIR\n    # to script path search\n    for _dirpath in fsd_env.split(os.pathsep):\n        if not os.path.isdir(_dirpath):\n            log.warning(\"Path is not a valid dir: `{_dirpath}`\".format(\n                **locals()))\n            continue\n        fsd_paths.append(_dirpath)\n\n    # collect scripts from dirs\n    for path in fsd_paths:\n        scripts.update({path: os.listdir(path)})\n\n    remove_black_list = []\n    for _k, s_list in scripts.items():\n        remove_black_list += s_list\n\n    log.info(\"remove_black_list: `{remove_black_list}`\".format(**locals()))\n    log.info(\"Additional Flame script paths: `{fsd_paths}`\".format(**locals()))\n    log.info(\"Flame Scripts: `{scripts}`\".format(**locals()))\n\n    # make sure no script file is in folder\n    if next(iter(os.listdir(flame_shared_dir)), None):\n        for _itm in os.listdir(flame_shared_dir):\n            skip = False\n\n            # skip all scripts and folders which are not maintained\n            if _itm not in remove_black_list:\n                skip = True\n\n            # do not skip if pyc in extension\n            if not os.path.isdir(_itm) and \"pyc\" in os.path.splitext(_itm)[-1]:\n                skip = False\n\n            # continue if skip in true\n            if skip:\n                continue\n\n            path = os.path.join(flame_shared_dir, _itm)\n            log.info(\"Removing `{path}`...\".format(**locals()))\n\n            try:\n                if os.path.isdir(path):\n                    shutil.rmtree(path, onerror=None)\n                else:\n                    os.remove(path)\n            except PermissionError as msg:\n                log.warning(\n                    \"Not able to remove: `{}`, Problem with: `{}`\".format(\n                        path,\n                        msg\n                    )\n                )\n\n    # copy scripts into Resolve's utility scripts dir\n    for dirpath, scriptlist in scripts.items():\n        # directory and scripts list\n        for _script in scriptlist:\n            # script in script list\n            src = os.path.join(dirpath, _script)\n            dst = os.path.join(flame_shared_dir, _script)\n            log.info(\"Copying `{src}` to `{dst}`...\".format(**locals()))\n\n            try:\n                if os.path.isdir(src):\n                    shutil.copytree(\n                        src, dst, symlinks=False,\n                        ignore=None, ignore_dangling_symlinks=False\n                    )\n                else:\n                    shutil.copy2(src, dst)\n            except (PermissionError, FileExistsError) as msg:\n                log.warning(\n                    \"Not able to copy to: `{}`, Problem with: `{}`\".format(\n                        dst,\n                        msg\n                    )\n                )\n\n\ndef setup(env=None):\n    \"\"\" Wrapper installer started from\n    `flame/hooks/pre_flame_setup.py`\n    \"\"\"\n    env = env or os.environ\n\n    # synchronize resolve utility scripts\n    _sync_utility_scripts(env)\n\n    log.info(\"Flame OpenPype wrapper has been installed\")\n\n\ndef get_flame_version():\n    import flame\n\n    return {\n        \"full\": flame.get_version(),\n        \"major\": flame.get_version_major(),\n        \"minor\": flame.get_version_minor(),\n        \"patch\": flame.get_version_patch()\n    }\n\n\ndef get_flame_install_root():\n    return \"/opt/Autodesk\"\n"
  },
  {
    "path": "openpype/hosts/flame/api/workio.py",
    "content": "\"\"\"Host API required Work Files tool\"\"\"\n\nimport os\nfrom openpype.lib import Logger\n# from .. import (\n#     get_project_manager,\n#     get_current_project\n# )\n\n\nlog = Logger.get_logger(__name__)\n\nexported_projet_ext = \".otoc\"\n\n\ndef file_extensions():\n    return [exported_projet_ext]\n\n\ndef has_unsaved_changes():\n    pass\n\n\ndef save_file(filepath):\n    pass\n\n\ndef open_file(filepath):\n    pass\n\n\ndef current_file():\n    pass\n\n\ndef work_root(session):\n    return os.path.normpath(session[\"AVALON_WORKDIR\"]).replace(\"\\\\\", \"/\")\n"
  },
  {
    "path": "openpype/hosts/flame/hooks/pre_flame_setup.py",
    "content": "import os\nimport json\nimport tempfile\nimport contextlib\nimport socket\nfrom pprint import pformat\n\nfrom openpype.lib import (\n    get_openpype_username,\n    run_subprocess,\n)\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\nfrom openpype.hosts import flame as opflame\n\n\nclass FlamePrelaunch(PreLaunchHook):\n    \"\"\" Flame prelaunch hook\n\n    Will make sure flame_script_dirs are copied to user's folder defined\n    in environment var FLAME_SCRIPT_DIR.\n    \"\"\"\n    app_groups = {\"flame\"}\n    permissions = 0o777\n\n    wtc_script_path = os.path.join(\n        opflame.HOST_DIR, \"api\", \"scripts\", \"wiretap_com.py\")\n    launch_types = {LaunchTypes.local}\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.signature = \"( {} )\".format(self.__class__.__name__)\n\n    def execute(self):\n        _env = self.launch_context.env\n        self.flame_python_exe = _env[\"OPENPYPE_FLAME_PYTHON_EXEC\"]\n        self.flame_pythonpath = _env[\"OPENPYPE_FLAME_PYTHONPATH\"]\n\n        \"\"\"Hook entry method.\"\"\"\n        project_doc = self.data[\"project_doc\"]\n        project_name = project_doc[\"name\"]\n        volume_name = _env.get(\"FLAME_WIRETAP_VOLUME\")\n\n        # get image io\n        project_settings = self.data[\"project_settings\"]\n\n        imageio_flame = project_settings[\"flame\"][\"imageio\"]\n\n        # Check whether 'enabled' key from host imageio settings exists\n        # so we can tell if host is using the new colormanagement framework.\n        # If the 'enabled' isn't found we want 'colormanaged' set to True\n        # because prior to the key existing we always did colormanagement for\n        # Flame\n        colormanaged = imageio_flame.get(\"enabled\")\n        # if key was not found, set to True\n        # ensuring backward compatibility\n        if colormanaged is None:\n            colormanaged = True\n\n        # get user name and host name\n        user_name = get_openpype_username()\n        user_name = user_name.replace(\".\", \"_\")\n\n        hostname = socket.gethostname()  # not returning wiretap host name\n\n        self.log.debug(\"Collected user \\\"{}\\\"\".format(user_name))\n        self.log.info(pformat(project_doc))\n        _db_p_data = project_doc[\"data\"]\n        width = _db_p_data[\"resolutionWidth\"]\n        height = _db_p_data[\"resolutionHeight\"]\n        fps = float(_db_p_data[\"fps\"])\n\n        project_data = {\n            \"Name\": project_doc[\"name\"],\n            \"Nickname\": _db_p_data[\"code\"],\n            \"Description\": \"Created by OpenPype\",\n            \"SetupDir\": project_doc[\"name\"],\n            \"FrameWidth\": int(width),\n            \"FrameHeight\": int(height),\n            \"AspectRatio\": float((width / height) * _db_p_data[\"pixelAspect\"]),\n            \"FrameRate\": self._get_flame_fps(fps)\n        }\n\n        data_to_script = {\n            # from settings\n            \"host_name\": _env.get(\"FLAME_WIRETAP_HOSTNAME\") or hostname,\n            \"volume_name\": volume_name,\n            \"group_name\": _env.get(\"FLAME_WIRETAP_GROUP\"),\n\n            # from project\n            \"project_name\": project_name,\n            \"user_name\": user_name,\n            \"project_data\": project_data\n        }\n\n        # add color management data\n        if colormanaged:\n            project_data.update({\n                \"FrameDepth\": str(imageio_flame[\"project\"][\"frameDepth\"]),\n                \"FieldDominance\": str(\n                    imageio_flame[\"project\"][\"fieldDominance\"])\n            })\n            data_to_script[\"color_policy\"] = str(\n                imageio_flame[\"project\"][\"colourPolicy\"])\n\n        self.log.info(pformat(dict(_env)))\n        self.log.info(pformat(data_to_script))\n\n        # add to python path from settings\n        self._add_pythonpath()\n\n        app_arguments = self._get_launch_arguments(data_to_script)\n\n        # fix project data permission issue\n        self._fix_permissions(project_name, volume_name)\n\n        self.launch_context.launch_args.extend(app_arguments)\n\n    def _fix_permissions(self, project_name, volume_name):\n        \"\"\"Work around for project data permissions\n\n        Reported issue: when project is created locally on one machine,\n        it is impossible to migrate it to other machine. Autodesk Flame\n        is crating some unmanagable files which needs to be opened to 0o777.\n\n        Args:\n            project_name (str): project name\n            volume_name (str): studio volume\n        \"\"\"\n        dirs_to_modify = [\n            \"/usr/discreet/project/{}\".format(project_name),\n            \"/opt/Autodesk/clip/{}/{}.prj\".format(volume_name, project_name),\n            \"/usr/discreet/clip/{}/{}.prj\".format(volume_name, project_name)\n        ]\n\n        for dirtm in dirs_to_modify:\n            for root, dirs, files in os.walk(dirtm):\n                try:\n                    for name in set(dirs) | set(files):\n                        path = os.path.join(root, name)\n                        st = os.stat(path)\n                        if oct(st.st_mode) != self.permissions:\n                            os.chmod(path, self.permissions)\n\n                except OSError as exc:\n                    self.log.warning(\"Not able to open files: {}\".format(exc))\n\n    def _get_flame_fps(self, fps_num):\n        fps_table = {\n            float(23.976): \"23.976 fps\",\n            int(25): \"25 fps\",\n            int(24): \"24 fps\",\n            float(29.97): \"29.97 fps DF\",\n            int(30): \"30 fps\",\n            int(50): \"50 fps\",\n            float(59.94): \"59.94 fps DF\",\n            int(60): \"60 fps\"\n        }\n\n        match_key = min(fps_table.keys(), key=lambda x: abs(x - fps_num))\n\n        try:\n            return fps_table[match_key]\n        except KeyError as msg:\n            raise KeyError((\n                \"Missing FPS key in conversion table. \"\n                \"Following keys are available: {}\".format(fps_table.keys())\n            )) from msg\n\n    def _add_pythonpath(self):\n        pythonpath = self.launch_context.env.get(\"PYTHONPATH\")\n\n        # separate it explicitly by `;` that is what we use in settings\n        new_pythonpath = self.flame_pythonpath.split(os.pathsep)\n        new_pythonpath += pythonpath.split(os.pathsep)\n\n        self.launch_context.env[\"PYTHONPATH\"] = os.pathsep.join(new_pythonpath)\n\n    def _get_launch_arguments(self, script_data):\n        # Dump data to string\n        dumped_script_data = json.dumps(script_data)\n\n        with make_temp_file(dumped_script_data) as tmp_json_path:\n            # Prepare subprocess arguments\n            args = [\n                self.flame_python_exe.format(\n                    **self.launch_context.env\n                ),\n                self.wtc_script_path,\n                tmp_json_path\n            ]\n            self.log.info(\"Executing: {}\".format(\" \".join(args)))\n\n            process_kwargs = {\n                \"logger\": self.log,\n                \"env\": self.launch_context.env\n            }\n\n            run_subprocess(args, **process_kwargs)\n\n            # process returned json file to pass launch args\n            return_json_data = open(tmp_json_path).read()\n            returned_data = json.loads(return_json_data)\n            app_args = returned_data.get(\"app_args\")\n            self.log.info(\"____ app_args: `{}`\".format(app_args))\n\n            if not app_args:\n                RuntimeError(\"App arguments were not solved\")\n\n        return app_args\n\n\n@contextlib.contextmanager\ndef make_temp_file(data):\n    try:\n        # Store dumped json to temporary file\n        temporary_json_file = tempfile.NamedTemporaryFile(\n            mode=\"w\", suffix=\".json\", delete=False\n        )\n        temporary_json_file.write(data)\n        temporary_json_file.close()\n        temporary_json_filepath = temporary_json_file.name.replace(\n            \"\\\\\", \"/\"\n        )\n\n        yield temporary_json_filepath\n\n    except IOError as _error:\n        raise IOError(\n            \"Not able to create temp json file: {}\".format(\n                _error\n            )\n        )\n\n    finally:\n        # Remove the temporary json\n        os.remove(temporary_json_filepath)\n"
  },
  {
    "path": "openpype/hosts/flame/otio/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/flame/otio/flame_export.py",
    "content": "\"\"\" compatibility OpenTimelineIO 0.12.0 and newer\n\"\"\"\n\nimport os\nimport re\nimport json\nimport logging\nimport opentimelineio as otio\nfrom . import utils\n\nimport flame\nfrom pprint import pformat\n\nlog = logging.getLogger(__name__)\n\n\nTRACK_TYPES = {\n    \"video\": otio.schema.TrackKind.Video,\n    \"audio\": otio.schema.TrackKind.Audio\n}\nMARKERS_COLOR_MAP = {\n    (1.0, 0.0, 0.0): otio.schema.MarkerColor.RED,\n    (1.0, 0.5, 0.0): otio.schema.MarkerColor.ORANGE,\n    (1.0, 1.0, 0.0): otio.schema.MarkerColor.YELLOW,\n    (1.0, 0.5, 1.0): otio.schema.MarkerColor.PINK,\n    (1.0, 1.0, 1.0): otio.schema.MarkerColor.WHITE,\n    (0.0, 1.0, 0.0): otio.schema.MarkerColor.GREEN,\n    (0.0, 1.0, 1.0): otio.schema.MarkerColor.CYAN,\n    (0.0, 0.0, 1.0): otio.schema.MarkerColor.BLUE,\n    (0.5, 0.0, 0.5): otio.schema.MarkerColor.PURPLE,\n    (0.5, 0.0, 1.0): otio.schema.MarkerColor.MAGENTA,\n    (0.0, 0.0, 0.0): otio.schema.MarkerColor.BLACK\n}\nMARKERS_INCLUDE = True\n\n\nclass CTX:\n    _fps = None\n    _tl_start_frame = None\n    project = None\n    clips = None\n\n    @classmethod\n    def set_fps(cls, new_fps):\n        if not isinstance(new_fps, float):\n            raise TypeError(\"Invalid fps type {}\".format(type(new_fps)))\n        if cls._fps != new_fps:\n            cls._fps = new_fps\n\n    @classmethod\n    def get_fps(cls):\n        return cls._fps\n\n    @classmethod\n    def set_tl_start_frame(cls, number):\n        if not isinstance(number, int):\n            raise TypeError(\"Invalid timeline start frame type {}\".format(\n                type(number)))\n        if cls._tl_start_frame != number:\n            cls._tl_start_frame = number\n\n    @classmethod\n    def get_tl_start_frame(cls):\n        return cls._tl_start_frame\n\n\ndef flatten(_list):\n    for item in _list:\n        if isinstance(item, (list, tuple)):\n            for sub_item in flatten(item):\n                yield sub_item\n        else:\n            yield item\n\n\ndef get_current_flame_project():\n    project = flame.project.current_project\n    return project\n\n\ndef create_otio_rational_time(frame, fps):\n    return otio.opentime.RationalTime(\n        float(frame),\n        float(fps)\n    )\n\n\ndef create_otio_time_range(start_frame, frame_duration, fps):\n    return otio.opentime.TimeRange(\n        start_time=create_otio_rational_time(start_frame, fps),\n        duration=create_otio_rational_time(frame_duration, fps)\n    )\n\n\ndef _get_metadata(item):\n    if hasattr(item, 'metadata'):\n        return dict(item.metadata) if item.metadata else {}\n    return {}\n\n\ndef create_time_effects(otio_clip, speed):\n    otio_effect = None\n\n    # retime on track item\n    if speed != 1.:\n        # make effect\n        otio_effect = otio.schema.LinearTimeWarp()\n        otio_effect.name = \"Speed\"\n        otio_effect.time_scalar = speed\n        otio_effect.metadata = {}\n\n    # freeze frame effect\n    if speed == 0.:\n        otio_effect = otio.schema.FreezeFrame()\n        otio_effect.name = \"FreezeFrame\"\n        otio_effect.metadata = {}\n\n    if otio_effect:\n        # add otio effect to clip effects\n        otio_clip.effects.append(otio_effect)\n\n\ndef _get_marker_color(flame_colour):\n    # clamp colors to closes half numbers\n    _flame_colour = [\n        (lambda x: round(x * 2) / 2)(c)\n        for c in flame_colour]\n\n    for color, otio_color_type in MARKERS_COLOR_MAP.items():\n        if _flame_colour == list(color):\n            return otio_color_type\n\n    return otio.schema.MarkerColor.RED\n\n\ndef _get_flame_markers(item):\n    output_markers = []\n\n    time_in = item.record_in.relative_frame\n\n    for marker in item.markers:\n        log.debug(marker)\n        start_frame = marker.location.get_value().relative_frame\n\n        start_frame = (start_frame - time_in) + 1\n\n        marker_data = {\n            \"name\": marker.name.get_value(),\n            \"duration\": marker.duration.get_value().relative_frame,\n            \"comment\": marker.comment.get_value(),\n            \"start_frame\": start_frame,\n            \"colour\": marker.colour.get_value()\n        }\n\n        output_markers.append(marker_data)\n\n    return output_markers\n\n\ndef create_otio_markers(otio_item, item):\n    markers = _get_flame_markers(item)\n    for marker in markers:\n        frame_rate = CTX.get_fps()\n\n        marked_range = otio.opentime.TimeRange(\n            start_time=otio.opentime.RationalTime(\n                marker[\"start_frame\"],\n                frame_rate\n            ),\n            duration=otio.opentime.RationalTime(\n                marker[\"duration\"],\n                frame_rate\n            )\n        )\n\n        # testing the comment if it is not containing json string\n        check_if_json = re.findall(\n            re.compile(r\"[{:}]\"),\n            marker[\"comment\"]\n        )\n\n        # to identify this as json, at least 3 items in the list should\n        # be present [\"{\", \":\", \"}\"]\n        metadata = {}\n        if len(check_if_json) >= 3:\n            # this is json string\n            try:\n                # capture exceptions which are related to strings only\n                metadata.update(\n                    json.loads(marker[\"comment\"])\n                )\n            except ValueError as msg:\n                log.error(\"Marker json conversion: {}\".format(msg))\n        else:\n            metadata[\"comment\"] = marker[\"comment\"]\n\n        otio_marker = otio.schema.Marker(\n            name=marker[\"name\"],\n            color=_get_marker_color(\n                marker[\"colour\"]),\n            marked_range=marked_range,\n            metadata=metadata\n        )\n\n        otio_item.markers.append(otio_marker)\n\n\ndef create_otio_reference(clip_data, fps=None):\n    metadata = _get_metadata(clip_data)\n    duration = int(clip_data[\"source_duration\"])\n\n    # get file info for path and start frame\n    frame_start = 0\n    fps = fps or CTX.get_fps()\n\n    path = clip_data[\"fpath\"]\n\n    file_name = os.path.basename(path)\n    file_head, extension = os.path.splitext(file_name)\n\n    # get padding and other file infos\n    log.debug(\"_ path: {}\".format(path))\n\n    otio_ex_ref_item = None\n\n    is_sequence = frame_number = utils.get_frame_from_filename(file_name)\n    if is_sequence:\n        file_head = file_name.split(frame_number)[:-1]\n        frame_start = int(frame_number)\n        padding = len(frame_number)\n\n        metadata.update({\n            \"isSequence\": True,\n            \"padding\": padding\n        })\n\n        # if it is file sequence try to create `ImageSequenceReference`\n        # the OTIO might not be compatible so return nothing and do it old way\n        try:\n            dirname = os.path.dirname(path)\n            otio_ex_ref_item = otio.schema.ImageSequenceReference(\n                target_url_base=dirname + os.sep,\n                name_prefix=file_head,\n                name_suffix=extension,\n                start_frame=frame_start,\n                frame_zero_padding=padding,\n                rate=fps,\n                available_range=create_otio_time_range(\n                    frame_start,\n                    duration,\n                    fps\n                )\n            )\n        except AttributeError:\n            pass\n\n    if not otio_ex_ref_item:\n        dirname, file_name = os.path.split(path)\n        file_name = utils.get_reformated_filename(file_name, padded=False)\n        reformated_path = os.path.join(dirname, file_name)\n        # in case old OTIO or video file create `ExternalReference`\n        otio_ex_ref_item = otio.schema.ExternalReference(\n            target_url=reformated_path,\n            available_range=create_otio_time_range(\n                frame_start,\n                duration,\n                fps\n            )\n        )\n\n    # add metadata to otio item\n    add_otio_metadata(otio_ex_ref_item, clip_data, **metadata)\n\n    return otio_ex_ref_item\n\n\ndef create_otio_clip(clip_data):\n    from openpype.hosts.flame.api import MediaInfoFile, TimeEffectMetadata\n\n    segment = clip_data[\"PySegment\"]\n\n    # calculate source in\n    media_info = MediaInfoFile(clip_data[\"fpath\"], logger=log)\n    media_timecode_start = media_info.start_frame\n    media_fps = media_info.fps\n\n    # Timewarp metadata\n    tw_data = TimeEffectMetadata(segment, logger=log).data\n    log.debug(\"__ tw_data: {}\".format(tw_data))\n\n    # define first frame\n    file_first_frame = utils.get_frame_from_filename(\n        clip_data[\"fpath\"])\n    if file_first_frame:\n        file_first_frame = int(file_first_frame)\n\n    first_frame = media_timecode_start or file_first_frame or 0\n\n    _clip_source_in = int(clip_data[\"source_in\"])\n    _clip_source_out = int(clip_data[\"source_out\"])\n    _clip_record_in = clip_data[\"record_in\"]\n    _clip_record_out = clip_data[\"record_out\"]\n    _clip_record_duration = int(clip_data[\"record_duration\"])\n\n    log.debug(\"_ file_first_frame: {}\".format(file_first_frame))\n    log.debug(\"_ first_frame: {}\".format(first_frame))\n    log.debug(\"_ _clip_source_in: {}\".format(_clip_source_in))\n    log.debug(\"_ _clip_source_out: {}\".format(_clip_source_out))\n    log.debug(\"_ _clip_record_in: {}\".format(_clip_record_in))\n    log.debug(\"_ _clip_record_out: {}\".format(_clip_record_out))\n\n    # first solve if the reverse timing\n    speed = 1\n    if clip_data[\"source_in\"] > clip_data[\"source_out\"]:\n        source_in = _clip_source_out - int(first_frame)\n        source_out = _clip_source_in - int(first_frame)\n        speed = -1\n    else:\n        source_in = _clip_source_in - int(first_frame)\n        source_out = _clip_source_out - int(first_frame)\n\n    log.debug(\"_ source_in: {}\".format(source_in))\n    log.debug(\"_ source_out: {}\".format(source_out))\n\n    if file_first_frame:\n        log.debug(\"_ file_source_in: {}\".format(\n            file_first_frame + source_in))\n        log.debug(\"_ file_source_in: {}\".format(\n            file_first_frame + source_out))\n\n    source_duration = (source_out - source_in + 1)\n\n    # secondly check if any change of speed\n    if source_duration != _clip_record_duration:\n        retime_speed = float(source_duration) / float(_clip_record_duration)\n        log.debug(\"_ calculated speed: {}\".format(retime_speed))\n        speed *= retime_speed\n\n    # get speed from metadata if available\n    if tw_data.get(\"speed\"):\n        speed = tw_data[\"speed\"]\n        log.debug(\"_ metadata speed: {}\".format(speed))\n\n    log.debug(\"_ speed: {}\".format(speed))\n    log.debug(\"_ source_duration: {}\".format(source_duration))\n    log.debug(\"_ _clip_record_duration: {}\".format(_clip_record_duration))\n\n    # create media reference\n    media_reference = create_otio_reference(\n        clip_data, media_fps)\n\n    # creatae source range\n    source_range = create_otio_time_range(\n        source_in,\n        _clip_record_duration,\n        CTX.get_fps()\n    )\n\n    otio_clip = otio.schema.Clip(\n        name=clip_data[\"segment_name\"],\n        source_range=source_range,\n        media_reference=media_reference\n    )\n\n    # Add markers\n    if MARKERS_INCLUDE:\n        create_otio_markers(otio_clip, segment)\n\n    if speed != 1:\n        create_time_effects(otio_clip, speed)\n\n    return otio_clip\n\n\ndef create_otio_gap(gap_start, clip_start, tl_start_frame, fps):\n    return otio.schema.Gap(\n        source_range=create_otio_time_range(\n            gap_start,\n            (clip_start - tl_start_frame) - gap_start,\n            fps\n        )\n    )\n\n\ndef _get_colourspace_policy():\n\n    output = {}\n    # get policies project path\n    policy_dir = \"/opt/Autodesk/project/{}/synColor/policy\".format(\n        CTX.project.name\n    )\n    log.debug(policy_dir)\n    policy_fp = os.path.join(policy_dir, \"policy.cfg\")\n\n    if not os.path.exists(policy_fp):\n        return output\n\n    with open(policy_fp) as file:\n        dict_conf = dict(line.strip().split(' = ', 1) for line in file)\n        output.update(\n            {\"openpype.flame.{}\".format(k): v for k, v in dict_conf.items()}\n        )\n    return output\n\n\ndef _create_otio_timeline(sequence):\n\n    metadata = _get_metadata(sequence)\n\n    # find colour policy files and add them to metadata\n    colorspace_policy = _get_colourspace_policy()\n    metadata.update(colorspace_policy)\n\n    metadata.update({\n        \"openpype.timeline.width\": int(sequence.width),\n        \"openpype.timeline.height\": int(sequence.height),\n        \"openpype.timeline.pixelAspect\": 1\n    })\n\n    rt_start_time = create_otio_rational_time(\n        CTX.get_tl_start_frame(), CTX.get_fps())\n\n    return otio.schema.Timeline(\n        name=str(sequence.name)[1:-1],\n        global_start_time=rt_start_time,\n        metadata=metadata\n    )\n\n\ndef create_otio_track(track_type, track_name):\n    return otio.schema.Track(\n        name=track_name,\n        kind=TRACK_TYPES[track_type]\n    )\n\n\ndef add_otio_gap(clip_data, otio_track, prev_out):\n    gap_length = clip_data[\"record_in\"] - prev_out\n    if prev_out != 0:\n        gap_length -= 1\n\n    gap = otio.opentime.TimeRange(\n        duration=otio.opentime.RationalTime(\n            gap_length,\n            CTX.get_fps()\n        )\n    )\n    otio_gap = otio.schema.Gap(source_range=gap)\n    otio_track.append(otio_gap)\n\n\ndef add_otio_metadata(otio_item, item, **kwargs):\n    metadata = _get_metadata(item)\n\n    # add additional metadata from kwargs\n    if kwargs:\n        metadata.update(kwargs)\n\n    # add metadata to otio item metadata\n    for key, value in metadata.items():\n        otio_item.metadata.update({key: value})\n\n\ndef _get_shot_tokens_values(clip, tokens):\n    old_value = None\n    output = {}\n\n    old_value = clip.shot_name.get_value()\n\n    for token in tokens:\n        clip.shot_name.set_value(token)\n        _key = re.sub(\"[ <>]\", \"\", token)\n\n        try:\n            output[_key] = int(clip.shot_name.get_value())\n        except ValueError:\n            output[_key] = clip.shot_name.get_value()\n\n    clip.shot_name.set_value(old_value)\n\n    return output\n\n\ndef _get_segment_attributes(segment):\n\n    log.debug(\"Segment name|hidden: {}|{}\".format(\n        segment.name.get_value(), segment.hidden\n    ))\n    if (\n        segment.name.get_value() == \"\"\n        or segment.hidden.get_value()\n    ):\n        return None\n\n    # Add timeline segment to tree\n    clip_data = {\n        \"segment_name\": segment.name.get_value(),\n        \"segment_comment\": segment.comment.get_value(),\n        \"shot_name\": segment.shot_name.get_value(),\n        \"tape_name\": segment.tape_name,\n        \"source_name\": segment.source_name,\n        \"fpath\": segment.file_path,\n        \"PySegment\": segment\n    }\n\n    # add all available shot tokens\n    shot_tokens = _get_shot_tokens_values(\n        segment,\n        [\"<colour space>\", \"<width>\", \"<height>\", \"<depth>\"]\n    )\n    clip_data.update(shot_tokens)\n\n    # populate shot source metadata\n    segment_attrs = [\n        \"record_duration\", \"record_in\", \"record_out\",\n        \"source_duration\", \"source_in\", \"source_out\"\n    ]\n    segment_attrs_data = {}\n    for attr in segment_attrs:\n        if not hasattr(segment, attr):\n            continue\n        _value = getattr(segment, attr)\n        segment_attrs_data[attr] = str(_value).replace(\"+\", \":\")\n\n        if attr in [\"record_in\", \"record_out\"]:\n            clip_data[attr] = _value.relative_frame\n        else:\n            clip_data[attr] = _value.frame\n\n    clip_data[\"segment_timecodes\"] = segment_attrs_data\n\n    return clip_data\n\n\ndef create_otio_timeline(sequence):\n    log.info(dir(sequence))\n    log.info(sequence.attributes)\n\n    CTX.project = get_current_flame_project()\n\n    # get current timeline\n    CTX.set_fps(\n        float(str(sequence.frame_rate)[:-4]))\n\n    tl_start_frame = utils.timecode_to_frames(\n        str(sequence.start_time).replace(\"+\", \":\"),\n        CTX.get_fps()\n    )\n    CTX.set_tl_start_frame(tl_start_frame)\n\n    # convert timeline to otio\n    otio_timeline = _create_otio_timeline(sequence)\n\n    # create otio tracks and clips\n    for ver in sequence.versions:\n        for track in ver.tracks:\n            # avoid all empty tracks\n            # or hidden tracks\n            if (\n                len(track.segments) == 0\n                or track.hidden.get_value()\n            ):\n                continue\n\n            # convert track to otio\n            otio_track = create_otio_track(\n                \"video\", str(track.name)[1:-1])\n\n            all_segments = []\n            for segment in track.segments:\n                clip_data = _get_segment_attributes(segment)\n                if not clip_data:\n                    continue\n                all_segments.append(clip_data)\n\n            segments_ordered = dict(enumerate(all_segments))\n            log.debug(\"_ segments_ordered: {}\".format(\n                pformat(segments_ordered)\n            ))\n            if not segments_ordered:\n                continue\n\n            for itemindex, segment_data in segments_ordered.items():\n                log.debug(\"_ itemindex: {}\".format(itemindex))\n\n                # Add Gap if needed\n                prev_item = (\n                    segment_data\n                    if itemindex == 0\n                    else segments_ordered[itemindex - 1]\n                )\n                log.debug(\"_ segment_data: {}\".format(segment_data))\n\n                # calculate clip frame range difference from each other\n                clip_diff = segment_data[\"record_in\"] - prev_item[\"record_out\"]\n\n                # add gap if first track item is not starting\n                # at first timeline frame\n                if itemindex == 0 and segment_data[\"record_in\"] > 0:\n                    add_otio_gap(segment_data, otio_track, 0)\n\n                # or add gap if following track items are having\n                # frame range differences from each other\n                elif itemindex and clip_diff != 1:\n                    add_otio_gap(\n                        segment_data, otio_track, prev_item[\"record_out\"])\n\n                # create otio clip and add it to track\n                otio_clip = create_otio_clip(segment_data)\n                otio_track.append(otio_clip)\n\n                log.debug(\"_ otio_clip: {}\".format(otio_clip))\n\n                # create otio marker\n                # create otio metadata\n\n            # add track to otio timeline\n            otio_timeline.tracks.append(otio_track)\n\n    return otio_timeline\n\n\ndef write_to_file(otio_timeline, path):\n    otio.adapters.write_to_file(otio_timeline, path)\n"
  },
  {
    "path": "openpype/hosts/flame/otio/utils.py",
    "content": "import re\nimport opentimelineio as otio\nimport logging\nlog = logging.getLogger(__name__)\n\nFRAME_PATTERN = re.compile(r\"[\\._](\\d+)[\\.]\")\n\n\ndef timecode_to_frames(timecode, framerate):\n    rt = otio.opentime.from_timecode(timecode, framerate)\n    return int(otio.opentime.to_frames(rt))\n\n\ndef frames_to_timecode(frames, framerate):\n    rt = otio.opentime.from_frames(frames, framerate)\n    return otio.opentime.to_timecode(rt)\n\n\ndef frames_to_seconds(frames, framerate):\n    rt = otio.opentime.from_frames(frames, framerate)\n    return otio.opentime.to_seconds(rt)\n\n\ndef get_reformated_filename(filename, padded=True):\n    \"\"\"\n    Return fixed python expression path\n\n    Args:\n        filename (str): file name\n\n    Returns:\n        type: string with reformated path\n\n    Example:\n        get_reformated_filename(\"plate.1001.exr\") > plate.%04d.exr\n\n    \"\"\"\n    found = FRAME_PATTERN.search(filename)\n\n    if not found:\n        log.info(\"File name is not sequence: {}\".format(filename))\n        return filename\n\n    padding = get_padding_from_filename(filename)\n\n    replacement = \"%0{}d\".format(padding) if padded else \"%d\"\n    start_idx, end_idx = found.span(1)\n\n    return replacement.join(\n        [filename[:start_idx], filename[end_idx:]]\n    )\n\n\ndef get_padding_from_filename(filename):\n    \"\"\"\n    Return padding number from Flame path style\n\n    Args:\n        filename (str): file name\n\n    Returns:\n        int: padding number\n\n    Example:\n        get_padding_from_filename(\"plate.0001.exr\") > 4\n\n    \"\"\"\n    found = get_frame_from_filename(filename)\n\n    return len(found) if found else None\n\n\ndef get_frame_from_filename(filename):\n    \"\"\"\n    Return sequence number from Flame path style\n\n    Args:\n        filename (str): file name\n\n    Returns:\n        int: sequence frame number\n\n    Example:\n        def get_frame_from_filename(path):\n            (\"plate.0001.exr\") > 0001\n\n    \"\"\"\n\n    found = re.findall(FRAME_PATTERN, filename)\n\n    return found.pop() if found else None\n"
  },
  {
    "path": "openpype/hosts/flame/plugins/create/create_shot_clip.py",
    "content": "from copy import deepcopy\nimport openpype.hosts.flame.api as opfapi\n\n\nclass CreateShotClip(opfapi.Creator):\n    \"\"\"Publishable clip\"\"\"\n\n    label = \"Create Publishable Clip\"\n    family = \"clip\"\n    icon = \"film\"\n    defaults = [\"Main\"]\n\n    presets = None\n\n    def process(self):\n        # Creator copy of object attributes that are modified during `process`\n        presets = deepcopy(self.presets)\n        gui_inputs = self.get_gui_inputs()\n\n        # get key pares from presets and match it on ui inputs\n        for k, v in gui_inputs.items():\n            if v[\"type\"] in (\"dict\", \"section\"):\n                # nested dictionary (only one level allowed\n                # for sections and dict)\n                for _k, _v in v[\"value\"].items():\n                    if presets.get(_k) is not None:\n                        gui_inputs[k][\n                            \"value\"][_k][\"value\"] = presets[_k]\n\n            if presets.get(k) is not None:\n                gui_inputs[k][\"value\"] = presets[k]\n\n        # open widget for plugins inputs\n        results_back = self.create_widget(\n            \"Pype publish attributes creator\",\n            \"Define sequential rename and fill hierarchy data.\",\n            gui_inputs\n        )\n\n        if len(self.selected) < 1:\n            return\n\n        if not results_back:\n            print(\"Operation aborted\")\n            return\n\n        # get ui output for track name for vertical sync\n        v_sync_track = results_back[\"vSyncTrack\"][\"value\"]\n\n        # sort selected trackItems by\n        sorted_selected_segments = []\n        unsorted_selected_segments = []\n        for _segment in self.selected:\n            if _segment.parent.name.get_value() in v_sync_track:\n                sorted_selected_segments.append(_segment)\n            else:\n                unsorted_selected_segments.append(_segment)\n\n        sorted_selected_segments.extend(unsorted_selected_segments)\n\n        kwargs = {\n            \"log\": self.log,\n            \"ui_inputs\": results_back,\n            \"avalon\": self.data,\n            \"family\": self.data[\"family\"]\n        }\n\n        for i, segment in enumerate(sorted_selected_segments):\n            kwargs[\"rename_index\"] = i\n            # convert track item to timeline media pool item\n            opfapi.PublishableClip(segment, **kwargs).convert()\n\n    def get_gui_inputs(self):\n        gui_tracks = self._get_video_track_names(\n            opfapi.get_current_sequence(opfapi.CTX.selection)\n        )\n        return deepcopy({\n            \"renameHierarchy\": {\n                \"type\": \"section\",\n                \"label\": \"Shot Hierarchy And Rename Settings\",\n                \"target\": \"ui\",\n                \"order\": 0,\n                \"value\": {\n                    \"hierarchy\": {\n                        \"value\": \"{folder}/{sequence}\",\n                        \"type\": \"QLineEdit\",\n                        \"label\": \"Shot Parent Hierarchy\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"Parents folder for shot root folder, Template filled with `Hierarchy Data` section\",  # noqa\n                        \"order\": 0},\n                    \"useShotName\": {\n                        \"value\": True,\n                        \"type\": \"QCheckBox\",\n                        \"label\": \"Use Shot Name\",\n                        \"target\": \"ui\",\n                        \"toolTip\": \"Use name form Shot name clip attribute\",  # noqa\n                        \"order\": 1},\n                    \"clipRename\": {\n                        \"value\": False,\n                        \"type\": \"QCheckBox\",\n                        \"label\": \"Rename clips\",\n                        \"target\": \"ui\",\n                        \"toolTip\": \"Renaming selected clips on fly\",  # noqa\n                        \"order\": 2},\n                    \"clipName\": {\n                        \"value\": \"{sequence}{shot}\",\n                        \"type\": \"QLineEdit\",\n                        \"label\": \"Clip Name Template\",\n                        \"target\": \"ui\",\n                        \"toolTip\": \"template for creating shot namespaused for renaming (use rename: on)\",  # noqa\n                        \"order\": 3},\n                    \"segmentIndex\": {\n                        \"value\": True,\n                        \"type\": \"QCheckBox\",\n                        \"label\": \"Segment index\",\n                        \"target\": \"ui\",\n                        \"toolTip\": \"Take number from segment index\",  # noqa\n                        \"order\": 4},\n                    \"countFrom\": {\n                        \"value\": 10,\n                        \"type\": \"QSpinBox\",\n                        \"label\": \"Count sequence from\",\n                        \"target\": \"ui\",\n                        \"toolTip\": \"Set when the sequence number stafrom\",  # noqa\n                        \"order\": 5},\n                    \"countSteps\": {\n                        \"value\": 10,\n                        \"type\": \"QSpinBox\",\n                        \"label\": \"Stepping number\",\n                        \"target\": \"ui\",\n                        \"toolTip\": \"What number is adding every new step\",  # noqa\n                        \"order\": 6},\n                }\n            },\n            \"hierarchyData\": {\n                \"type\": \"dict\",\n                \"label\": \"Shot Template Keywords\",\n                \"target\": \"tag\",\n                \"order\": 1,\n                \"value\": {\n                    \"folder\": {\n                        \"value\": \"shots\",\n                        \"type\": \"QLineEdit\",\n                        \"label\": \"{folder}\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"Name of folder used for root of generated shots.\\nUsable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                        \"order\": 0},\n                    \"episode\": {\n                        \"value\": \"ep01\",\n                        \"type\": \"QLineEdit\",\n                        \"label\": \"{episode}\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"Name of episode.\\nUsable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                        \"order\": 1},\n                    \"sequence\": {\n                        \"value\": \"sq01\",\n                        \"type\": \"QLineEdit\",\n                        \"label\": \"{sequence}\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"Name of sequence of shots.\\nUsable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                        \"order\": 2},\n                    \"track\": {\n                        \"value\": \"{_track_}\",\n                        \"type\": \"QLineEdit\",\n                        \"label\": \"{track}\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"Name of sequence of shots.\\nUsable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                        \"order\": 3},\n                    \"shot\": {\n                        \"value\": \"sh###\",\n                        \"type\": \"QLineEdit\",\n                        \"label\": \"{shot}\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"Name of shot. `#` is converted to paded number. \\nAlso could be used with usable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                        \"order\": 4}\n                }\n            },\n            \"verticalSync\": {\n                \"type\": \"section\",\n                \"label\": \"Vertical Synchronization Of Attributes\",\n                \"target\": \"ui\",\n                \"order\": 2,\n                \"value\": {\n                    \"vSyncOn\": {\n                        \"value\": True,\n                        \"type\": \"QCheckBox\",\n                        \"label\": \"Enable Vertical Sync\",\n                        \"target\": \"ui\",\n                        \"toolTip\": \"Switch on if you want clips above each other to share its attributes\",  # noqa\n                        \"order\": 0},\n                    \"vSyncTrack\": {\n                        \"value\": gui_tracks,  # noqa\n                        \"type\": \"QComboBox\",\n                        \"label\": \"Hero track\",\n                        \"target\": \"ui\",\n                        \"toolTip\": \"Select driving track name which should be hero for all others\",  # noqa\n                        \"order\": 1}\n                }\n            },\n            \"publishSettings\": {\n                \"type\": \"section\",\n                \"label\": \"Publish Settings\",\n                \"target\": \"ui\",\n                \"order\": 3,\n                \"value\": {\n                    \"subsetName\": {\n                        \"value\": [\"[ track name ]\", \"main\", \"bg\", \"fg\", \"bg\",\n                                \"animatic\"],\n                        \"type\": \"QComboBox\",\n                        \"label\": \"Subset Name\",\n                        \"target\": \"ui\",\n                        \"toolTip\": \"chose subset name pattern, if [ track name ] is selected, name of track layer will be used\",  # noqa\n                        \"order\": 0},\n                    \"subsetFamily\": {\n                        \"value\": [\"plate\", \"take\"],\n                        \"type\": \"QComboBox\",\n                        \"label\": \"Subset Family\",\n                        \"target\": \"ui\", \"toolTip\": \"What use of this subset is for\",  # noqa\n                        \"order\": 1},\n                    \"reviewTrack\": {\n                        \"value\": [\"< none >\"] + gui_tracks,\n                        \"type\": \"QComboBox\",\n                        \"label\": \"Use Review Track\",\n                        \"target\": \"ui\",\n                        \"toolTip\": \"Generate preview videos on fly, if `< none >` is defined nothing will be generated.\",  # noqa\n                        \"order\": 2},\n                    \"audio\": {\n                        \"value\": False,\n                        \"type\": \"QCheckBox\",\n                        \"label\": \"Include audio\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"Process subsets with corresponding audio\",  # noqa\n                        \"order\": 3},\n                    \"sourceResolution\": {\n                        \"value\": False,\n                        \"type\": \"QCheckBox\",\n                        \"label\": \"Source resolution\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"Is resloution taken from timeline or source?\",  # noqa\n                        \"order\": 4},\n                }\n            },\n            \"frameRangeAttr\": {\n                \"type\": \"section\",\n                \"label\": \"Shot Attributes\",\n                \"target\": \"ui\",\n                \"order\": 4,\n                \"value\": {\n                    \"workfileFrameStart\": {\n                        \"value\": 1001,\n                        \"type\": \"QSpinBox\",\n                        \"label\": \"Workfiles Start Frame\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"Set workfile starting frame number\",  # noqa\n                        \"order\": 0\n                    },\n                    \"handleStart\": {\n                        \"value\": 0,\n                        \"type\": \"QSpinBox\",\n                        \"label\": \"Handle Start\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"Handle at start of clip\",  # noqa\n                        \"order\": 1\n                    },\n                    \"handleEnd\": {\n                        \"value\": 0,\n                        \"type\": \"QSpinBox\",\n                        \"label\": \"Handle End\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"Handle at end of clip\",  # noqa\n                        \"order\": 2\n                    },\n                    \"includeHandles\": {\n                        \"value\": False,\n                        \"type\": \"QCheckBox\",\n                        \"label\": \"Include handles\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"By default handles are excluded\",  # noqa\n                        \"order\": 3\n                    },\n                    \"retimedHandles\": {\n                        \"value\": True,\n                        \"type\": \"QCheckBox\",\n                        \"label\": \"Retimed handles\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"By default handles are retimed.\",  # noqa\n                        \"order\": 4\n                    },\n                    \"retimedFramerange\": {\n                        \"value\": True,\n                        \"type\": \"QCheckBox\",\n                        \"label\": \"Retimed framerange\",\n                        \"target\": \"tag\",\n                        \"toolTip\": \"By default framerange is retimed.\",  # noqa\n                        \"order\": 5\n                    }\n                }\n            }\n        })\n\n    def _get_video_track_names(self, sequence):\n        track_names = []\n        for ver in sequence.versions:\n            for track in ver.tracks:\n                track_names.append(track.name.get_value())\n\n        return track_names\n"
  },
  {
    "path": "openpype/hosts/flame/plugins/load/load_clip.py",
    "content": "from copy import deepcopy\nimport os\nimport flame\nfrom pprint import pformat\nimport openpype.hosts.flame.api as opfapi\nfrom openpype.lib import StringTemplate\nfrom openpype.lib.transcoding import (\n    VIDEO_EXTENSIONS,\n    IMAGE_EXTENSIONS\n)\n\n\nclass LoadClip(opfapi.ClipLoader):\n    \"\"\"Load a subset to timeline as clip\n\n    Place clip to timeline on its asset origin timings collected\n    during conforming to project\n    \"\"\"\n\n    families = [\"render2d\", \"source\", \"plate\", \"render\", \"review\"]\n    representations = [\"*\"]\n    extensions = set(\n        ext.lstrip(\".\") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)\n    )\n\n    label = \"Load as clip\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    # settings\n    reel_group_name = \"OpenPype_Reels\"\n    reel_name = \"Loaded\"\n    clip_name_template = \"{asset}_{subset}<_{output}>\"\n\n    \"\"\" Anatomy keys from version context data and dynamically added:\n        - {layerName} - original layer name token\n        - {layerUID} - original layer UID token\n        - {originalBasename} - original clip name taken from file\n    \"\"\"\n    layer_rename_template = \"{asset}_{subset}<_{output}>\"\n    layer_rename_patterns = []\n\n    def load(self, context, name, namespace, options):\n\n        # get flame objects\n        fproject = flame.project.current_project\n        self.fpd = fproject.current_workspace.desktop\n\n        # load clip to timeline and get main variables\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        version_name = version.get(\"name\", None)\n        colorspace = self.get_colorspace(context)\n\n        # in case output is not in context replace key to representation\n        if not context[\"representation\"][\"context\"].get(\"output\"):\n            self.clip_name_template = self.clip_name_template.replace(\n                \"output\", \"representation\")\n            self.layer_rename_template = self.layer_rename_template.replace(\n                \"output\", \"representation\")\n\n        formatting_data = deepcopy(context[\"representation\"][\"context\"])\n        clip_name = StringTemplate(self.clip_name_template).format(\n            formatting_data)\n\n        # convert colorspace with ocio to flame mapping\n        # in imageio flame section\n        colorspace = self.get_native_colorspace(colorspace)\n        self.log.info(\"Loading with colorspace: `{}`\".format(colorspace))\n\n        # create workfile path\n        workfile_dir = os.environ[\"AVALON_WORKDIR\"]\n        openclip_dir = os.path.join(\n            workfile_dir, clip_name\n        )\n        openclip_path = os.path.join(\n            openclip_dir, clip_name + \".clip\"\n        )\n        if not os.path.exists(openclip_dir):\n            os.makedirs(openclip_dir)\n\n        # prepare clip data from context ad send it to openClipLoader\n        path = self.filepath_from_context(context)\n        loading_context = {\n            \"path\": path.replace(\"\\\\\", \"/\"),\n            \"colorspace\": colorspace,\n            \"version\": \"v{:0>3}\".format(version_name),\n            \"layer_rename_template\": self.layer_rename_template,\n            \"layer_rename_patterns\": self.layer_rename_patterns,\n            \"context_data\": formatting_data\n        }\n        self.log.debug(pformat(\n            loading_context\n        ))\n        self.log.debug(openclip_path)\n\n        # make openpype clip file\n        opfapi.OpenClipSolver(\n            openclip_path, loading_context, logger=self.log).make()\n\n        # prepare Reel group in actual desktop\n        opc = self._get_clip(\n            clip_name,\n            openclip_path\n        )\n\n        # add additional metadata from the version to imprint Avalon knob\n        add_keys = [\n            \"frameStart\", \"frameEnd\", \"source\", \"author\",\n            \"fps\", \"handleStart\", \"handleEnd\"\n        ]\n\n        # move all version data keys to tag data\n        data_imprint = {}\n        for key in add_keys:\n            data_imprint.update({\n                key: version_data.get(key, str(None))\n            })\n\n        # add variables related to version context\n        data_imprint.update({\n            \"version\": version_name,\n            \"colorspace\": colorspace,\n            \"objectName\": clip_name\n        })\n\n        # TODO: finish the containerisation\n        # opc_segment = opfapi.get_clip_segment(opc)\n\n        # return opfapi.containerise(\n        #     opc_segment,\n        #     name, namespace, context,\n        #     self.__class__.__name__,\n        #     data_imprint)\n\n        return opc\n\n    def _get_clip(self, name, clip_path):\n        reel = self._get_reel()\n        # with maintained openclip as opc\n        matching_clip = [cl for cl in reel.clips\n                         if cl.name.get_value() == name]\n        if matching_clip:\n            return matching_clip.pop()\n        else:\n            created_clips = flame.import_clips(str(clip_path), reel)\n            return created_clips.pop()\n\n    def _get_reel(self):\n\n        matching_rgroup = [\n            rg for rg in self.fpd.reel_groups\n            if rg.name.get_value() == self.reel_group_name\n        ]\n\n        if not matching_rgroup:\n            reel_group = self.fpd.create_reel_group(str(self.reel_group_name))\n            for _r in reel_group.reels:\n                if \"reel\" not in _r.name.get_value().lower():\n                    continue\n                self.log.debug(\"Removing: {}\".format(_r.name))\n                flame.delete(_r)\n        else:\n            reel_group = matching_rgroup.pop()\n\n        matching_reel = [\n            re for re in reel_group.reels\n            if re.name.get_value() == self.reel_name\n        ]\n\n        if not matching_reel:\n            reel_group = reel_group.create_reel(str(self.reel_name))\n        else:\n            reel_group = matching_reel.pop()\n\n        return reel_group\n\n    def _get_segment_from_clip(self, clip):\n        # unwrapping segment from input clip\n        pass\n\n    # def switch(self, container, representation):\n    #     self.update(container, representation)\n\n    # def update(self, container, representation):\n    #     \"\"\" Updating previously loaded clips\n    #     \"\"\"\n\n    #     # load clip to timeline and get main variables\n    #     name = container['name']\n    #     namespace = container['namespace']\n    #     track_item = phiero.get_track_items(\n    #         track_item_name=namespace)\n    #     version = io.find_one({\n    #         \"type\": \"version\",\n    #         \"_id\": representation[\"parent\"]\n    #     })\n    #     version_data = version.get(\"data\", {})\n    #     version_name = version.get(\"name\", None)\n    #     colorspace = version_data.get(\"colorspace\", None)\n    #     object_name = \"{}_{}\".format(name, namespace)\n    #     file = get_representation_path(representation).replace(\"\\\\\", \"/\")\n    #     clip = track_item.source()\n\n    #     # reconnect media to new path\n    #     clip.reconnectMedia(file)\n\n    #     # set colorspace\n    #     if colorspace:\n    #         clip.setSourceMediaColourTransform(colorspace)\n\n    #     # add additional metadata from the version to imprint Avalon knob\n    #     add_keys = [\n    #         \"frameStart\", \"frameEnd\", \"source\", \"author\",\n    #         \"fps\", \"handleStart\", \"handleEnd\"\n    #     ]\n\n    #     # move all version data keys to tag data\n    #     data_imprint = {}\n    #     for key in add_keys:\n    #         data_imprint.update({\n    #             key: version_data.get(key, str(None))\n    #         })\n\n    #     # add variables related to version context\n    #     data_imprint.update({\n    #         \"representation\": str(representation[\"_id\"]),\n    #         \"version\": version_name,\n    #         \"colorspace\": colorspace,\n    #         \"objectName\": object_name\n    #     })\n\n    #     # update color of clip regarding the version order\n    #     self.set_item_color(track_item, version)\n\n    #     return phiero.update_container(track_item, data_imprint)\n\n    # def remove(self, container):\n    #     \"\"\" Removing previously loaded clips\n    #     \"\"\"\n    #     # load clip to timeline and get main variables\n    #     namespace = container['namespace']\n    #     track_item = phiero.get_track_items(\n    #         track_item_name=namespace)\n    #     track = track_item.parent()\n\n    #     # remove track item from track\n    #     track.removeItem(track_item)\n\n    # @classmethod\n    # def multiselection(cls, track_item):\n    #     if not cls.track:\n    #         cls.track = track_item.parent()\n    #         cls.sequence = cls.track.parent()\n\n    # @classmethod\n    # def set_item_color(cls, track_item, version):\n\n    #     clip = track_item.source()\n    #     # define version name\n    #     version_name = version.get(\"name\", None)\n    #     # get all versions in list\n    #     versions = io.find({\n    #         \"type\": \"version\",\n    #         \"parent\": version[\"parent\"]\n    #     }).distinct('name')\n\n    #     max_version = max(versions)\n\n    #     # set clip colour\n    #     if version_name == max_version:\n    #         clip.binItem().setColor(cls.clip_color_last)\n    #     else:\n    #         clip.binItem().setColor(cls.clip_color)\n"
  },
  {
    "path": "openpype/hosts/flame/plugins/load/load_clip_batch.py",
    "content": "from copy import deepcopy\nimport os\nimport flame\nfrom pprint import pformat\nimport openpype.hosts.flame.api as opfapi\nfrom openpype.lib import StringTemplate\nfrom openpype.lib.transcoding import (\n    VIDEO_EXTENSIONS,\n    IMAGE_EXTENSIONS\n)\n\nclass LoadClipBatch(opfapi.ClipLoader):\n    \"\"\"Load a subset to timeline as clip\n\n    Place clip to timeline on its asset origin timings collected\n    during conforming to project\n    \"\"\"\n\n    families = [\"render2d\", \"source\", \"plate\", \"render\", \"review\"]\n    representations = [\"*\"]\n    extensions = set(\n        ext.lstrip(\".\") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)\n    )\n\n    label = \"Load as clip to current batch\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    # settings\n    reel_name = \"OP_LoadedReel\"\n    clip_name_template = \"{batch}_{asset}_{subset}<_{output}>\"\n\n    \"\"\" Anatomy keys from version context data and dynamically added:\n        - {layerName} - original layer name token\n        - {layerUID} - original layer UID token\n        - {originalBasename} - original clip name taken from file\n    \"\"\"\n    layer_rename_template = \"{asset}_{subset}<_{output}>\"\n    layer_rename_patterns = []\n\n    def load(self, context, name, namespace, options):\n\n        # get flame objects\n        self.batch = options.get(\"batch\") or flame.batch\n\n        # load clip to timeline and get main variables\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        version_name = version.get(\"name\", None)\n        colorspace = self.get_colorspace(context)\n\n        # in case output is not in context replace key to representation\n        if not context[\"representation\"][\"context\"].get(\"output\"):\n            self.clip_name_template = self.clip_name_template.replace(\n                \"output\", \"representation\")\n            self.layer_rename_template = self.layer_rename_template.replace(\n                \"output\", \"representation\")\n\n        formatting_data = deepcopy(context[\"representation\"][\"context\"])\n        formatting_data[\"batch\"] = self.batch.name.get_value()\n\n        clip_name = StringTemplate(self.clip_name_template).format(\n            formatting_data)\n\n        # convert colorspace with ocio to flame mapping\n        # in imageio flame section\n        colorspace = self.get_native_colorspace(colorspace)\n        self.log.info(\"Loading with colorspace: `{}`\".format(colorspace))\n\n        # create workfile path\n        workfile_dir = options.get(\"workdir\") or os.environ[\"AVALON_WORKDIR\"]\n        openclip_dir = os.path.join(\n            workfile_dir, clip_name\n        )\n        openclip_path = os.path.join(\n            openclip_dir, clip_name + \".clip\"\n        )\n\n        if not os.path.exists(openclip_dir):\n            os.makedirs(openclip_dir)\n\n        # prepare clip data from context and send it to openClipLoader\n        path = self.filepath_from_context(context)\n        loading_context = {\n            \"path\": path.replace(\"\\\\\", \"/\"),\n            \"colorspace\": colorspace,\n            \"version\": \"v{:0>3}\".format(version_name),\n            \"layer_rename_template\": self.layer_rename_template,\n            \"layer_rename_patterns\": self.layer_rename_patterns,\n            \"context_data\": formatting_data\n        }\n        self.log.debug(pformat(\n            loading_context\n        ))\n        self.log.debug(openclip_path)\n\n        # make openpype clip file\n        opfapi.OpenClipSolver(\n            openclip_path, loading_context, logger=self.log).make()\n\n        # prepare Reel group in actual desktop\n        opc = self._get_clip(\n            clip_name,\n            openclip_path\n        )\n\n        # add additional metadata from the version to imprint Avalon knob\n        add_keys = [\n            \"frameStart\", \"frameEnd\", \"source\", \"author\",\n            \"fps\", \"handleStart\", \"handleEnd\"\n        ]\n\n        # move all version data keys to tag data\n        data_imprint = {\n            key: version_data.get(key, str(None))\n            for key in add_keys\n        }\n        # add variables related to version context\n        data_imprint.update({\n            \"version\": version_name,\n            \"colorspace\": colorspace,\n            \"objectName\": clip_name\n        })\n\n        # TODO: finish the containerisation\n        # opc_segment = opfapi.get_clip_segment(opc)\n\n        # return opfapi.containerise(\n        #     opc_segment,\n        #     name, namespace, context,\n        #     self.__class__.__name__,\n        #     data_imprint)\n\n        return opc\n\n    def _get_clip(self, name, clip_path):\n        reel = self._get_reel()\n\n        # with maintained openclip as opc\n        matching_clip = None\n        for cl in reel.clips:\n            if cl.name.get_value() != name:\n                continue\n            matching_clip = cl\n\n        if not matching_clip:\n            created_clips = flame.import_clips(str(clip_path), reel)\n            return created_clips.pop()\n\n        return matching_clip\n\n    def _get_reel(self):\n\n        matching_reel = [\n            rg for rg in self.batch.reels\n            if rg.name.get_value() == self.reel_name\n        ]\n\n        return (\n            matching_reel.pop()\n            if matching_reel\n            else self.batch.create_reel(str(self.reel_name))\n        )\n"
  },
  {
    "path": "openpype/hosts/flame/plugins/publish/collect_test_selection.py",
    "content": "import os\nimport pyblish.api\nimport tempfile\nimport openpype.hosts.flame.api as opfapi\nfrom openpype.hosts.flame.otio import flame_export as otio_export\nimport opentimelineio as otio\nfrom pprint import pformat\nreload(otio_export)  # noqa\n\n\n@pyblish.api.log\nclass CollectTestSelection(pyblish.api.ContextPlugin):\n    \"\"\"testing selection sharing\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = \"test selection\"\n    hosts = [\"flame\"]\n    active = False\n\n    def process(self, context):\n        self.log.info(\n            \"Active Selection: {}\".format(opfapi.CTX.selection))\n\n        sequence = opfapi.get_current_sequence(opfapi.CTX.selection)\n\n        self.test_imprint_data(sequence)\n        self.test_otio_export(sequence)\n\n    def test_otio_export(self, sequence):\n        test_dir = os.path.normpath(\n            tempfile.mkdtemp(prefix=\"test_pyblish_tmp_\")\n        )\n        export_path = os.path.normpath(\n            os.path.join(\n                test_dir, \"otio_timeline_export.otio\"\n            )\n        )\n        otio_timeline = otio_export.create_otio_timeline(sequence)\n        otio_export.write_to_file(\n            otio_timeline, export_path\n        )\n        read_timeline_otio = otio.adapters.read_from_file(export_path)\n\n        if otio_timeline != read_timeline_otio:\n            raise Exception(\"Exported timeline is different from original\")\n\n        self.log.info(pformat(otio_timeline))\n        self.log.info(\"Otio exported to: {}\".format(export_path))\n\n    def test_imprint_data(self, sequence):\n        with opfapi.maintained_segment_selection(sequence) as sel_segments:\n            for segment in sel_segments:\n                if str(segment.name)[1:-1] == \"\":\n                    continue\n\n                self.log.debug(\"Segment with OpenPypeData: {}\".format(\n                    segment.name))\n\n                opfapi.imprint(segment, {\n                    'asset': segment.name.get_value(),\n                    'family': 'render',\n                    'subset': 'subsetMain'\n                })\n"
  },
  {
    "path": "openpype/hosts/flame/plugins/publish/collect_timeline_instances.py",
    "content": "import re\nfrom types import NoneType\nimport pyblish\nimport openpype.hosts.flame.api as opfapi\nfrom openpype.hosts.flame.otio import flame_export\nfrom openpype.pipeline.editorial import (\n    is_overlapping_otio_ranges,\n    get_media_range_with_retimes\n)\n\n# # developer reload modules\nfrom pprint import pformat\n\n# constatns\nNUM_PATERN = re.compile(r\"([0-9\\.]+)\")\nTXT_PATERN = re.compile(r\"([a-zA-Z]+)\")\n\n\nclass CollectTimelineInstances(pyblish.api.ContextPlugin):\n    \"\"\"Collect all Timeline segment selection.\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.09\n    label = \"Collect timeline Instances\"\n    hosts = [\"flame\"]\n\n    audio_track_items = []\n\n    # settings\n    xml_preset_attrs_from_comments = []\n    add_tasks = []\n\n    def process(self, context):\n        selected_segments = context.data[\"flameSelectedSegments\"]\n        self.log.debug(\"__ selected_segments: {}\".format(selected_segments))\n\n        self.otio_timeline = context.data[\"otioTimeline\"]\n        self.fps = context.data[\"fps\"]\n\n        # process all sellected\n        for segment in selected_segments:\n            # get openpype tag data\n            marker_data = opfapi.get_segment_data_marker(segment)\n\n            self.log.debug(\"__ marker_data: {}\".format(\n                pformat(marker_data)))\n\n            if not marker_data:\n                continue\n\n            if marker_data.get(\"id\") != \"pyblish.avalon.instance\":\n                continue\n\n            self.log.debug(\"__ segment.name: {}\".format(\n                segment.name\n            ))\n\n            comment_attributes = self._get_comment_attributes(segment)\n\n            self.log.debug(\"_ comment_attributes: {}\".format(\n                pformat(comment_attributes)))\n\n            clip_data = opfapi.get_segment_attributes(segment)\n            clip_name = clip_data[\"segment_name\"]\n            self.log.debug(\"clip_name: {}\".format(clip_name))\n\n            # get otio clip data\n            otio_data = self._get_otio_clip_instance_data(clip_data) or {}\n            self.log.debug(\"__ otio_data: {}\".format(pformat(otio_data)))\n\n            # get file path\n            file_path = clip_data[\"fpath\"]\n\n            first_frame = opfapi.get_frame_from_filename(file_path) or 0\n\n            head, tail = self._get_head_tail(\n                clip_data,\n                otio_data[\"otioClip\"],\n                marker_data[\"handleStart\"],\n                marker_data[\"handleEnd\"]\n            )\n\n            # make sure there is not NoneType rather 0\n            if isinstance(head, NoneType):\n                head = 0\n            if isinstance(tail, NoneType):\n                tail = 0\n\n            # make sure value is absolute\n            if head != 0:\n                head = abs(head)\n            if tail != 0:\n                tail = abs(tail)\n\n            # solve handles length\n            marker_data[\"handleStart\"] = min(\n                marker_data[\"handleStart\"], head)\n            marker_data[\"handleEnd\"] = min(\n                marker_data[\"handleEnd\"], tail)\n\n            workfile_start = self._set_workfile_start(marker_data)\n\n            with_audio = bool(marker_data.pop(\"audio\"))\n\n            # add marker data to instance data\n            inst_data = dict(marker_data.items())\n\n            # add ocio_data to instance data\n            inst_data.update(otio_data)\n\n            asset = marker_data[\"asset\"]\n            subset = marker_data[\"subset\"]\n\n            # insert family into families\n            family = marker_data[\"family\"]\n            families = [str(f) for f in marker_data[\"families\"]]\n            families.insert(0, str(family))\n\n            # form label\n            label = asset\n            if asset != clip_name:\n                label += \" ({})\".format(clip_name)\n            label += \" {} [{}]\".format(subset, \", \".join(families))\n\n            inst_data.update({\n                \"name\": \"{}_{}\".format(asset, subset),\n                \"label\": label,\n                \"asset\": asset,\n                \"item\": segment,\n                \"families\": families,\n                \"publish\": marker_data[\"publish\"],\n                \"fps\": self.fps,\n                \"workfileFrameStart\": workfile_start,\n                \"sourceFirstFrame\": int(first_frame),\n                \"retimedHandles\": marker_data.get(\"retimedHandles\"),\n                \"shotDurationFromSource\": (\n                    not marker_data.get(\"retimedFramerange\")),\n                \"path\": file_path,\n                \"flameAddTasks\": self.add_tasks,\n                \"tasks\": {\n                    task[\"name\"]: {\"type\": task[\"type\"]}\n                    for task in self.add_tasks},\n                \"representations\": [],\n                \"newAssetPublishing\": True\n            })\n            self.log.debug(\"__ inst_data: {}\".format(pformat(inst_data)))\n\n            # add resolution\n            self._get_resolution_to_data(inst_data, context)\n\n            # add comment attributes if any\n            inst_data.update(comment_attributes)\n\n            # create instance\n            instance = context.create_instance(**inst_data)\n\n            # add colorspace data\n            instance.data.update({\n                \"versionData\": {\n                    \"colorspace\": clip_data[\"colour_space\"],\n                }\n            })\n\n            # create shot instance for shot attributes create/update\n            self._create_shot_instance(context, clip_name, **inst_data)\n\n            self.log.info(\"Creating instance: {}\".format(instance))\n            self.log.info(\n                \"_ instance.data: {}\".format(pformat(instance.data)))\n\n            if not with_audio:\n                continue\n\n            # add audioReview attribute to plate instance data\n            # if reviewTrack is on\n            if marker_data.get(\"reviewTrack\") is not None:\n                instance.data[\"reviewAudio\"] = True\n\n    @staticmethod\n    def _set_workfile_start(data):\n        include_handles = data.get(\"includeHandles\")\n        workfile_start = data[\"workfileFrameStart\"]\n        handle_start = data[\"handleStart\"]\n\n        if include_handles:\n            workfile_start += handle_start\n\n        return workfile_start\n\n    def _get_comment_attributes(self, segment):\n        comment = segment.comment.get_value()\n\n        # try to find attributes\n        attributes = {\n            \"xml_overrides\": {\n                \"pixelRatio\": 1.00}\n        }\n        # search for `:`\n        for split in self._split_comments(comment):\n            # make sure we ignore if not `:` in key\n            if \":\" not in split:\n                continue\n\n            self._get_xml_preset_attrs(\n                attributes, split)\n\n        # add xml overrides resolution to instance data\n        xml_overrides = attributes[\"xml_overrides\"]\n        if xml_overrides.get(\"width\"):\n            attributes.update({\n                \"resolutionWidth\": xml_overrides[\"width\"],\n                \"resolutionHeight\": xml_overrides[\"height\"],\n                \"pixelAspect\": xml_overrides[\"pixelRatio\"]\n            })\n\n        return attributes\n\n    def _get_xml_preset_attrs(self, attributes, split):\n\n        # split to key and value\n        key, value = split.split(\":\")\n\n        for attr_data in self.xml_preset_attrs_from_comments:\n            a_name = attr_data[\"name\"]\n            a_type = attr_data[\"type\"]\n\n            # exclude all not related attributes\n            if a_name.lower() not in key.lower():\n                continue\n\n            # get pattern defined by type\n            pattern = TXT_PATERN\n            if a_type in (\"number\", \"float\"):\n                pattern = NUM_PATERN\n\n            res_goup = pattern.findall(value)\n\n            # raise if nothing is found as it is not correctly defined\n            if not res_goup:\n                raise ValueError((\n                    \"Value for `{}` attribute is not \"\n                    \"set correctly: `{}`\").format(a_name, split))\n\n            if \"string\" in a_type:\n                _value = res_goup[0]\n            if \"float\" in a_type:\n                _value = float(res_goup[0])\n            if \"number\" in a_type:\n                _value = int(res_goup[0])\n\n            attributes[\"xml_overrides\"][a_name] = _value\n\n        # condition for resolution in key\n        if \"resolution\" in key.lower():\n            res_goup = NUM_PATERN.findall(value)\n            # check if axpect was also defined\n            # 1920x1080x1.5\n            aspect = res_goup[2] if len(res_goup) > 2 else 1\n\n            width = int(res_goup[0])\n            height = int(res_goup[1])\n            pixel_ratio = float(aspect)\n            attributes[\"xml_overrides\"].update({\n                \"width\": width,\n                \"height\": height,\n                \"pixelRatio\": pixel_ratio\n            })\n\n    def _split_comments(self, comment_string):\n        # first split comment by comma\n        split_comments = []\n        if \",\" in comment_string:\n            split_comments.extend(comment_string.split(\",\"))\n        elif \";\" in comment_string:\n            split_comments.extend(comment_string.split(\";\"))\n        else:\n            split_comments.append(comment_string)\n\n        return split_comments\n\n    def _get_head_tail(self, clip_data, otio_clip, handle_start, handle_end):\n        # calculate head and tail with forward compatibility\n        head = clip_data.get(\"segment_head\")\n        tail = clip_data.get(\"segment_tail\")\n        self.log.debug(\"__ head: `{}`\".format(head))\n        self.log.debug(\"__ tail: `{}`\".format(tail))\n\n        # HACK: it is here to serve for versions below 2021.1\n        if not any([head, tail]):\n            retimed_attributes = get_media_range_with_retimes(\n                otio_clip, handle_start, handle_end)\n            self.log.debug(\n                \">> retimed_attributes: {}\".format(retimed_attributes))\n\n            # retimed head and tail\n            head = int(retimed_attributes[\"handleStart\"])\n            tail = int(retimed_attributes[\"handleEnd\"])\n\n        return head, tail\n\n    def _get_resolution_to_data(self, data, context):\n        assert data.get(\"otioClip\"), \"Missing `otioClip` data\"\n\n        # solve source resolution option\n        if data.get(\"sourceResolution\", None):\n            otio_clip_metadata = data[\n                \"otioClip\"].media_reference.metadata\n            data.update({\n                \"resolutionWidth\": otio_clip_metadata[\n                        \"openpype.source.width\"],\n                \"resolutionHeight\": otio_clip_metadata[\n                    \"openpype.source.height\"],\n                \"pixelAspect\": otio_clip_metadata[\n                    \"openpype.source.pixelAspect\"]\n            })\n        else:\n            otio_tl_metadata = context.data[\"otioTimeline\"].metadata\n            data.update({\n                \"resolutionWidth\": otio_tl_metadata[\"openpype.timeline.width\"],\n                \"resolutionHeight\": otio_tl_metadata[\n                    \"openpype.timeline.height\"],\n                \"pixelAspect\": otio_tl_metadata[\n                    \"openpype.timeline.pixelAspect\"]\n            })\n\n    def _create_shot_instance(self, context, clip_name, **data):\n        master_layer = data.get(\"heroTrack\")\n        hierarchy_data = data.get(\"hierarchyData\")\n\n        if not master_layer:\n            return\n\n        if not hierarchy_data:\n            return\n\n        asset = data[\"asset\"]\n        subset = \"shotMain\"\n\n        # insert family into families\n        family = \"shot\"\n\n        # form label\n        label = asset\n        if asset != clip_name:\n            label += \" ({}) \".format(clip_name)\n        label += \" {}\".format(subset)\n        label += \" [{}]\".format(family)\n\n        data.update({\n            \"name\": \"{}_{}\".format(asset, subset),\n            \"label\": label,\n            \"subset\": subset,\n            \"asset\": asset,\n            \"family\": family,\n            \"families\": []\n        })\n\n        instance = context.create_instance(**data)\n        self.log.info(\"Creating instance: {}\".format(instance))\n        self.log.debug(\n            \"_ instance.data: {}\".format(pformat(instance.data)))\n\n    def _get_otio_clip_instance_data(self, clip_data):\n        \"\"\"\n        Return otio objects for timeline, track and clip\n\n        Args:\n            timeline_item_data (dict): timeline_item_data from list returned by\n                                    resolve.get_current_timeline_items()\n            otio_timeline (otio.schema.Timeline): otio object\n\n        Returns:\n            dict: otio clip object\n\n        \"\"\"\n        segment = clip_data[\"PySegment\"]\n        s_track_name = segment.parent.name.get_value()\n        timeline_range = self._create_otio_time_range_from_timeline_item_data(\n            clip_data)\n\n        for otio_clip in self.otio_timeline.each_clip():\n            track_name = otio_clip.parent().name\n            parent_range = otio_clip.range_in_parent()\n            if s_track_name not in track_name:\n                continue\n            if otio_clip.name not in segment.name.get_value():\n                continue\n            if is_overlapping_otio_ranges(\n                    parent_range, timeline_range, strict=True):\n\n                # add pypedata marker to otio_clip metadata\n                for marker in otio_clip.markers:\n                    if opfapi.MARKER_NAME in marker.name:\n                        otio_clip.metadata.update(marker.metadata)\n                return {\"otioClip\": otio_clip}\n\n        return None\n\n    def _create_otio_time_range_from_timeline_item_data(self, clip_data):\n        frame_start = int(clip_data[\"record_in\"])\n        frame_duration = int(clip_data[\"record_duration\"])\n\n        return flame_export.create_otio_time_range(\n            frame_start, frame_duration, self.fps)\n"
  },
  {
    "path": "openpype/hosts/flame/plugins/publish/collect_timeline_otio.py",
    "content": "import pyblish.api\n\nfrom openpype.client import get_asset_name_identifier\nimport openpype.hosts.flame.api as opfapi\nfrom openpype.hosts.flame.otio import flame_export\nfrom openpype.pipeline.create import get_subset_name\n\n\nclass CollecTimelineOTIO(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current working context into publish context\"\"\"\n\n    label = \"Collect Timeline OTIO\"\n    order = pyblish.api.CollectorOrder - 0.099\n\n    def process(self, context):\n        # plugin defined\n        family = \"workfile\"\n        variant = \"otioTimeline\"\n\n        # main\n        asset_doc = context.data[\"assetEntity\"]\n        task_name = context.data[\"task\"]\n        project = opfapi.get_current_project()\n        sequence = opfapi.get_current_sequence(opfapi.CTX.selection)\n\n        # create subset name\n        subset_name = get_subset_name(\n            family,\n            variant,\n            task_name,\n            asset_doc,\n            context.data[\"projectName\"],\n            context.data[\"hostName\"],\n            project_settings=context.data[\"project_settings\"]\n        )\n\n        asset_name = get_asset_name_identifier(asset_doc)\n\n        # adding otio timeline to context\n        with opfapi.maintained_segment_selection(sequence) as selected_seg:\n            otio_timeline = flame_export.create_otio_timeline(sequence)\n\n            instance_data = {\n                \"name\": subset_name,\n                \"asset\": asset_name,\n                \"subset\": subset_name,\n                \"family\": \"workfile\",\n                \"families\": []\n            }\n\n            # create instance with workfile\n            instance = context.create_instance(**instance_data)\n            self.log.info(\"Creating instance: {}\".format(instance))\n\n            # update context with main project attributes\n            context.data.update({\n                \"flameProject\": project,\n                \"flameSequence\": sequence,\n                \"otioTimeline\": otio_timeline,\n                \"currentFile\": \"Flame/{}/{}\".format(\n                    project.name, sequence.name\n                ),\n                \"flameSelectedSegments\": selected_seg,\n                \"fps\": float(str(sequence.frame_rate)[:-4])\n            })\n"
  },
  {
    "path": "openpype/hosts/flame/plugins/publish/extract_otio_file.py",
    "content": "import os\nimport pyblish.api\nimport opentimelineio as otio\nfrom openpype.pipeline import publish\n\n\nclass ExtractOTIOFile(publish.Extractor):\n    \"\"\"\n    Extractor export OTIO file\n    \"\"\"\n\n    label = \"Extract OTIO file\"\n    order = pyblish.api.ExtractorOrder - 0.45\n    families = [\"workfile\"]\n    hosts = [\"flame\"]\n\n    def process(self, instance):\n        # create representation data\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        name = instance.data[\"name\"]\n        staging_dir = self.staging_dir(instance)\n\n        otio_timeline = instance.context.data[\"otioTimeline\"]\n        # create otio timeline representation\n        otio_file_name = name + \".otio\"\n        otio_file_path = os.path.join(staging_dir, otio_file_name)\n\n        # export otio file to temp dir\n        otio.adapters.write_to_file(otio_timeline, otio_file_path)\n\n        representation_otio = {\n            'name': \"otio\",\n            'ext': \"otio\",\n            'files': otio_file_name,\n            \"stagingDir\": staging_dir,\n        }\n\n        instance.data[\"representations\"].append(representation_otio)\n\n        self.log.info(\"Added OTIO file representation: {}\".format(\n            representation_otio))\n"
  },
  {
    "path": "openpype/hosts/flame/plugins/publish/extract_subset_resources.py",
    "content": "import os\nimport re\nimport tempfile\nfrom copy import deepcopy\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.flame import api as opfapi\nfrom openpype.hosts.flame.api import MediaInfoFile\nfrom openpype.pipeline.editorial import (\n    get_media_range_with_retimes\n)\n\nimport flame\n\n\nclass ExtractSubsetResources(publish.Extractor):\n    \"\"\"\n    Extractor for transcoding files from Flame clip\n    \"\"\"\n\n    label = \"Extract subset resources\"\n    order = pyblish.api.ExtractorOrder\n    families = [\"clip\"]\n    hosts = [\"flame\"]\n\n    # plugin defaults\n    keep_original_representation = False\n\n    default_presets = {\n        \"thumbnail\": {\n            \"active\": True,\n            \"ext\": \"jpg\",\n            \"xml_preset_file\": \"Jpeg (8-bit).xml\",\n            \"xml_preset_dir\": \"\",\n            \"export_type\": \"File Sequence\",\n            \"parsed_comment_attrs\": False,\n            \"colorspace_out\": \"Output - sRGB\",\n            \"representation_add_range\": False,\n            \"representation_tags\": [\"thumbnail\"],\n            \"path_regex\": \".*\"\n        }\n    }\n\n    # hide publisher during exporting\n    hide_ui_on_process = True\n\n    # settings\n    export_presets_mapping = {}\n\n    def process(self, instance):\n        if not self.keep_original_representation:\n            # remove previeous representation if not needed\n            instance.data[\"representations\"] = []\n\n        # flame objects\n        segment = instance.data[\"item\"]\n        asset_name = instance.data[\"asset\"]\n        segment_name = segment.name.get_value()\n        clip_path = instance.data[\"path\"]\n        sequence_clip = instance.context.data[\"flameSequence\"]\n\n        # segment's parent track name\n        s_track_name = segment.parent.name.get_value()\n\n        # get configured workfile frame start/end (handles excluded)\n        frame_start = instance.data[\"frameStart\"]\n        # get media source first frame\n        source_first_frame = instance.data[\"sourceFirstFrame\"]\n\n        self.log.debug(\"_ frame_start: {}\".format(frame_start))\n        self.log.debug(\"_ source_first_frame: {}\".format(source_first_frame))\n\n        # get timeline in/out of segment\n        clip_in = instance.data[\"clipIn\"]\n        clip_out = instance.data[\"clipOut\"]\n\n        # get retimed attributres\n        retimed_data = self._get_retimed_attributes(instance)\n\n        # get individual keys\n        retimed_handle_start = retimed_data[\"handle_start\"]\n        retimed_handle_end = retimed_data[\"handle_end\"]\n        retimed_source_duration = retimed_data[\"source_duration\"]\n        retimed_speed = retimed_data[\"speed\"]\n\n        # get handles value - take only the max from both\n        handle_start = instance.data[\"handleStart\"]\n        handle_end = instance.data[\"handleEnd\"]\n        handles = max(handle_start, handle_end)\n        include_handles = instance.data.get(\"includeHandles\")\n        retimed_handles = instance.data.get(\"retimedHandles\")\n\n        # get media source range with handles\n        source_start_handles = instance.data[\"sourceStartH\"]\n        source_end_handles = instance.data[\"sourceEndH\"]\n\n        # retime if needed\n        if retimed_speed != 1.0:\n            if retimed_handles:\n                # handles are retimed\n                source_start_handles = (\n                    instance.data[\"sourceStart\"] - retimed_handle_start)\n                source_end_handles = (\n                    source_start_handles\n                    + (retimed_source_duration - 1)\n                    + retimed_handle_start\n                    + retimed_handle_end\n                )\n\n            else:\n                # handles are not retimed\n                source_end_handles = (\n                    source_start_handles\n                    + (retimed_source_duration - 1)\n                    + handle_start\n                    + handle_end\n                )\n\n        # get frame range with handles for representation range\n        frame_start_handle = frame_start - handle_start\n        repre_frame_start = frame_start_handle\n        if include_handles:\n            if retimed_speed == 1.0 or not retimed_handles:\n                frame_start_handle = frame_start\n            else:\n                frame_start_handle = (\n                    frame_start - handle_start) + retimed_handle_start\n\n        self.log.debug(\"_ frame_start_handle: {}\".format(\n            frame_start_handle))\n        self.log.debug(\"_ repre_frame_start: {}\".format(\n            repre_frame_start))\n\n        # calculate duration with handles\n        source_duration_handles = (\n            source_end_handles - source_start_handles) + 1\n\n        self.log.debug(\"_ source_duration_handles: {}\".format(\n            source_duration_handles))\n\n        # create staging dir path\n        staging_dir = self.staging_dir(instance)\n\n        # append staging dir for later cleanup\n        instance.context.data[\"cleanupFullPaths\"].append(staging_dir)\n\n        # add default preset type for thumbnail and reviewable video\n        # update them with settings and override in case the same\n        # are found in there\n        _preset_keys = [k.split('_')[0] for k in self.export_presets_mapping]\n        export_presets = {\n            k: v for k, v in deepcopy(self.default_presets).items()\n            if k not in _preset_keys\n        }\n        export_presets.update(self.export_presets_mapping)\n\n        if not instance.data.get(\"versionData\"):\n            instance.data[\"versionData\"] = {}\n\n        # set versiondata if any retime\n        version_data = retimed_data.get(\"version_data\")\n        self.log.debug(\"_ version_data: {}\".format(version_data))\n\n        if version_data:\n            instance.data[\"versionData\"].update(version_data)\n\n        # version data start frame\n        version_frame_start = frame_start\n        if include_handles:\n            version_frame_start = frame_start_handle\n        if retimed_speed != 1.0:\n            if retimed_handles:\n                instance.data[\"versionData\"].update({\n                    \"frameStart\": version_frame_start,\n                    \"frameEnd\": (\n                        (version_frame_start + source_duration_handles - 1)\n                        - (retimed_handle_start + retimed_handle_end)\n                    )\n                })\n            else:\n                instance.data[\"versionData\"].update({\n                    \"handleStart\": handle_start,\n                    \"handleEnd\": handle_end,\n                    \"frameStart\": version_frame_start,\n                    \"frameEnd\": (\n                        (version_frame_start + source_duration_handles - 1)\n                        - (handle_start + handle_end)\n                    )\n                })\n        self.log.debug(\"_ version_data: {}\".format(\n            instance.data[\"versionData\"]\n        ))\n\n        # loop all preset names and\n        for unique_name, preset_config in export_presets.items():\n            modify_xml_data = {}\n\n            if self._should_skip(preset_config, clip_path, unique_name):\n                continue\n\n            # get all presets attributes\n            extension = preset_config[\"ext\"]\n            preset_file = preset_config[\"xml_preset_file\"]\n            preset_dir = preset_config[\"xml_preset_dir\"]\n            export_type = preset_config[\"export_type\"]\n            repre_tags = preset_config[\"representation_tags\"]\n            parsed_comment_attrs = preset_config[\"parsed_comment_attrs\"]\n            color_out = preset_config[\"colorspace_out\"]\n\n            self.log.info(\n                \"Processing `{}` as `{}` to `{}` type...\".format(\n                    preset_file, export_type, extension\n                )\n            )\n\n            exporting_clip = None\n            name_patern_xml = \"<name>_{}.\".format(\n                unique_name)\n\n            if export_type == \"Sequence Publish\":\n                # change export clip to sequence\n                exporting_clip = flame.duplicate(sequence_clip)\n\n                # only keep visible layer where instance segment is child\n                self.hide_others(\n                    exporting_clip, segment_name, s_track_name)\n\n                # change name pattern\n                name_patern_xml = (\n                    \"<segment name>_<shot name>_{}.\").format(\n                        unique_name)\n\n                # only for h264 with baked retime\n                in_mark = clip_in\n                out_mark = clip_out + 1\n                modify_xml_data.update({\n                    \"exportHandles\": True,\n                    \"nbHandles\": handles\n                })\n            else:\n                in_mark = (source_start_handles - source_first_frame) + 1\n                out_mark = in_mark + source_duration_handles\n                exporting_clip = self.import_clip(clip_path)\n                exporting_clip.name.set_value(\"{}_{}\".format(\n                    asset_name, segment_name))\n\n            # add xml tags modifications\n            modify_xml_data.update({\n                # enum position low start from 0\n                \"frameIndex\": 0,\n                \"startFrame\": repre_frame_start,\n                \"namePattern\": name_patern_xml\n            })\n\n            if parsed_comment_attrs:\n                # add any xml overrides collected form segment.comment\n                modify_xml_data.update(instance.data[\"xml_overrides\"])\n\n            self.log.debug(\"_ in_mark: {}\".format(in_mark))\n            self.log.debug(\"_ out_mark: {}\".format(out_mark))\n\n            export_kwargs = {}\n            # validate xml preset file is filled\n            if preset_file == \"\":\n                raise ValueError(\n                    (\"Check Settings for {} preset: \"\n                        \"`XML preset file` is not filled\").format(\n                        unique_name)\n                )\n\n            # resolve xml preset dir if not filled\n            if preset_dir == \"\":\n                preset_dir = opfapi.get_preset_path_by_xml_name(\n                    preset_file)\n\n                if not preset_dir:\n                    raise ValueError(\n                        (\"Check Settings for {} preset: \"\n                            \"`XML preset file` {} is not found\").format(\n                            unique_name, preset_file)\n                    )\n\n            # create preset path\n            preset_orig_xml_path = str(os.path.join(\n                preset_dir, preset_file\n            ))\n\n            # define kwargs based on preset type\n            if \"thumbnail\" in unique_name:\n                modify_xml_data.update({\n                    \"video/posterFrame\": True,\n                    \"video/useFrameAsPoster\": 1,\n                    \"namePattern\": \"__thumbnail\"\n                })\n                thumb_frame_number = int(in_mark + (\n                    (out_mark - in_mark + 1) / 2))\n\n                self.log.debug(\"__ thumb_frame_number: {}\".format(\n                    thumb_frame_number\n                ))\n\n                export_kwargs[\"thumb_frame_number\"] = thumb_frame_number\n            else:\n                export_kwargs.update({\n                    \"in_mark\": in_mark,\n                    \"out_mark\": out_mark\n                })\n\n            preset_path = opfapi.modify_preset_file(\n                preset_orig_xml_path, staging_dir, modify_xml_data)\n\n            # get and make export dir paths\n            export_dir_path = str(os.path.join(\n                staging_dir, unique_name\n            ))\n            os.makedirs(export_dir_path)\n\n            # export\n            opfapi.export_clip(\n                export_dir_path, exporting_clip, preset_path, **export_kwargs)\n\n            repr_name = unique_name\n            # make sure only first segment is used if underscore in name\n            # HACK: `ftrackreview_withLUT` will result only in `ftrackreview`\n            if (\n                \"thumbnail\" in unique_name\n                or \"ftrackreview\" in unique_name\n            ):\n                repr_name = unique_name.split(\"_\")[0]\n\n            # create representation data\n            representation_data = {\n                \"name\": repr_name,\n                \"outputName\": repr_name,\n                \"ext\": extension,\n                \"stagingDir\": export_dir_path,\n                \"tags\": repre_tags,\n                \"data\": {\n                    \"colorspace\": color_out\n                },\n                \"load_to_batch_group\": preset_config.get(\n                    \"load_to_batch_group\"),\n                \"batch_group_loader_name\": preset_config.get(\n                    \"batch_group_loader_name\") or None\n            }\n\n            # collect all available content of export dir\n            files = os.listdir(export_dir_path)\n\n            # make sure no nested folders inside\n            n_stage_dir, n_files = self._unfolds_nested_folders(\n                export_dir_path, files, extension)\n\n            # fix representation in case of nested folders\n            if n_stage_dir:\n                representation_data[\"stagingDir\"] = n_stage_dir\n                files = n_files\n\n            # add files to representation but add\n            # imagesequence as list\n            if (\n                # first check if path in files is not mov extension\n                [\n                    f for f in files\n                    if os.path.splitext(f)[-1] == \".mov\"\n                ]\n                # then try if thumbnail is not in unique name\n                or repr_name == \"thumbnail\"\n            ):\n                representation_data[\"files\"] = files.pop()\n            else:\n                representation_data[\"files\"] = files\n\n            # add frame range\n            if preset_config[\"representation_add_range\"]:\n                representation_data.update({\n                    \"frameStart\": repre_frame_start,\n                    \"frameEnd\": (\n                        repre_frame_start + source_duration_handles) - 1,\n                    \"fps\": instance.data[\"fps\"]\n                })\n\n            instance.data[\"representations\"].append(representation_data)\n\n            # add review family if found in tags\n            if \"review\" in repre_tags:\n                instance.data[\"families\"].append(\"review\")\n\n            self.log.info(\"Added representation: {}\".format(\n                representation_data))\n\n            if export_type == \"Sequence Publish\":\n                # at the end remove the duplicated clip\n                flame.delete(exporting_clip)\n\n    def _get_retimed_attributes(self, instance):\n        handle_start = instance.data[\"handleStart\"]\n        handle_end = instance.data[\"handleEnd\"]\n\n        # get basic variables\n        otio_clip = instance.data[\"otioClip\"]\n\n        # get available range trimmed with processed retimes\n        retimed_attributes = get_media_range_with_retimes(\n            otio_clip, handle_start, handle_end)\n        self.log.debug(\n            \">> retimed_attributes: {}\".format(retimed_attributes))\n\n        r_media_in = int(retimed_attributes[\"mediaIn\"])\n        r_media_out = int(retimed_attributes[\"mediaOut\"])\n        version_data = retimed_attributes.get(\"versionData\")\n\n        return {\n            \"version_data\": version_data,\n            \"handle_start\": int(retimed_attributes[\"handleStart\"]),\n            \"handle_end\": int(retimed_attributes[\"handleEnd\"]),\n            \"source_duration\": (\n                (r_media_out - r_media_in) + 1\n            ),\n            \"speed\": float(retimed_attributes[\"speed\"])\n        }\n\n    def _should_skip(self, preset_config, clip_path, unique_name):\n        # get activating attributes\n        activated_preset = preset_config[\"active\"]\n        filter_path_regex = preset_config.get(\"filter_path_regex\")\n\n        self.log.info(\n            \"Preset `{}` is active `{}` with filter `{}`\".format(\n                unique_name, activated_preset, filter_path_regex\n            )\n        )\n\n        # skip if not activated presete\n        if not activated_preset:\n            return True\n\n        # exclude by regex filter if any\n        if (\n            filter_path_regex\n            and not re.search(filter_path_regex, clip_path)\n        ):\n            return True\n\n    def _unfolds_nested_folders(self, stage_dir, files_list, ext):\n        \"\"\"Unfolds nested folders\n\n        Args:\n            stage_dir (str): path string with directory\n            files_list (list): list of file names\n            ext (str): extension (jpg)[without dot]\n\n        Raises:\n            IOError: in case no files were collected form any directory\n\n        Returns:\n            str, list: new staging dir path, new list of file names\n            or\n            None, None: In case single file in `files_list`\n        \"\"\"\n        # exclude single files which are having extension\n        # the same as input ext attr\n        if (\n            # only one file in list\n            len(files_list) == 1\n            # file is having extension as input\n            and ext in os.path.splitext(files_list[0])[-1]\n        ):\n            return None, None\n        elif (\n            # more then one file in list\n            len(files_list) >= 1\n            # extension is correct\n            and ext in os.path.splitext(files_list[0])[-1]\n            # test file exists\n            and os.path.exists(\n                os.path.join(stage_dir, files_list[0])\n            )\n        ):\n            return None, None\n\n        new_stage_dir = None\n        new_files_list = []\n        for file in files_list:\n            search_path = os.path.join(stage_dir, file)\n            if not os.path.isdir(search_path):\n                continue\n            for root, _dirs, files in os.walk(search_path):\n                for _file in files:\n                    _fn, _ext = os.path.splitext(_file)\n                    if ext.lower() != _ext[1:].lower():\n                        continue\n                    new_files_list.append(_file)\n                    if not new_stage_dir:\n                        new_stage_dir = root\n\n        if not new_stage_dir:\n            raise AssertionError(\n                \"Files in `{}` are not correct! Check `{}`\".format(\n                    files_list, stage_dir)\n            )\n\n        return new_stage_dir, new_files_list\n\n    def hide_others(self, sequence_clip, segment_name, track_name):\n        \"\"\"Helper method used only if sequence clip is used\n\n        Args:\n            sequence_clip (flame.Clip): sequence clip\n            segment_name (str): segment name\n            track_name (str): track name\n        \"\"\"\n        # create otio tracks and clips\n        for ver in sequence_clip.versions:\n            for track in ver.tracks:\n                if len(track.segments) == 0 and track.hidden.get_value():\n                    continue\n\n                # hide tracks which are not parent track\n                if track.name.get_value() != track_name:\n                    track.hidden = True\n                    continue\n\n                # hidde all other segments\n                for segment in track.segments:\n                    if segment.name.get_value() != segment_name:\n                        segment.hidden = True\n\n    def import_clip(self, path):\n        \"\"\"\n        Import clip from path\n        \"\"\"\n        dir_path = os.path.dirname(path)\n        media_info = MediaInfoFile(path, logger=self.log)\n        file_pattern = media_info.file_pattern\n        self.log.debug(\"__ file_pattern: {}\".format(file_pattern))\n\n        # rejoin the pattern to dir path\n        new_path = os.path.join(dir_path, file_pattern)\n\n        clips = flame.import_clips(new_path)\n        self.log.info(\"Clips [{}] imported from `{}`\".format(clips, path))\n\n        if not clips:\n            self.log.warning(\"Path `{}` is not having any clips\".format(path))\n            return None\n        elif len(clips) > 1:\n            self.log.warning(\n                \"Path `{}` is containing more that one clip\".format(path)\n            )\n        return clips[0]\n"
  },
  {
    "path": "openpype/hosts/flame/plugins/publish/integrate_batch_group.py",
    "content": "import os\nimport copy\nfrom collections import OrderedDict\nfrom pprint import pformat\nimport pyblish\nimport openpype.hosts.flame.api as opfapi\nimport openpype.pipeline as op_pipeline\nfrom openpype.pipeline.workfile import get_workdir\n\n\nclass IntegrateBatchGroup(pyblish.api.InstancePlugin):\n    \"\"\"Integrate published shot to batch group\"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.45\n    label = \"Integrate Batch Groups\"\n    hosts = [\"flame\"]\n    families = [\"clip\"]\n\n    # settings\n    default_loader = \"LoadClip\"\n\n    def process(self, instance):\n        add_tasks = instance.data[\"flameAddTasks\"]\n\n        # iterate all tasks from settings\n        for task_data in add_tasks:\n            # exclude batch group\n            if not task_data[\"create_batch_group\"]:\n                continue\n\n            # create or get already created batch group\n            bgroup = self._get_batch_group(instance, task_data)\n\n            # add batch group content\n            all_batch_nodes = self._add_nodes_to_batch_with_links(\n                instance, task_data, bgroup)\n\n            for name, node in all_batch_nodes.items():\n                self.log.debug(\"name: {}, dir: {}\".format(\n                    name, dir(node)\n                ))\n                self.log.debug(\"__ node.attributes: {}\".format(\n                    node.attributes\n                ))\n\n            # load plate to batch group\n            self.log.info(\"Loading subset `{}` into batch `{}`\".format(\n                instance.data[\"subset\"], bgroup.name.get_value()\n            ))\n            self._load_clip_to_context(instance, bgroup)\n\n    def _add_nodes_to_batch_with_links(self, instance, task_data, batch_group):\n        # get write file node properties > OrederDict because order does matter\n        write_pref_data = self._get_write_prefs(instance, task_data)\n\n        batch_nodes = [\n            {\n                \"type\": \"comp\",\n                \"properties\": {},\n                \"id\": \"comp_node01\"\n            },\n            {\n                \"type\": \"Write File\",\n                \"properties\": write_pref_data,\n                \"id\": \"write_file_node01\"\n            }\n        ]\n        batch_links = [\n            {\n                \"from_node\": {\n                    \"id\": \"comp_node01\",\n                    \"connector\": \"Result\"\n                },\n                \"to_node\": {\n                    \"id\": \"write_file_node01\",\n                    \"connector\": \"Front\"\n                }\n            }\n        ]\n\n        # add nodes into batch group\n        return opfapi.create_batch_group_conent(\n            batch_nodes, batch_links, batch_group)\n\n    def _load_clip_to_context(self, instance, bgroup):\n        # get all loaders for host\n        loaders_by_name = {\n            loader.__name__: loader\n            for loader in op_pipeline.discover_loader_plugins()\n        }\n\n        # get all published representations\n        published_representations = instance.data[\"published_representations\"]\n        repres_db_id_by_name = {\n            repre_info[\"representation\"][\"name\"]: repre_id\n            for repre_id, repre_info in published_representations.items()\n        }\n\n        # get all loadable representations\n        repres_by_name = {\n            repre[\"name\"]: repre for repre in instance.data[\"representations\"]\n        }\n\n        # get repre_id for the loadable representations\n        loader_name_by_repre_id = {\n            repres_db_id_by_name[repr_name]: {\n                \"loader\": repr_data[\"batch_group_loader_name\"],\n                # add repre data for exception logging\n                \"_repre_data\": repr_data\n            }\n            for repr_name, repr_data in repres_by_name.items()\n            if repr_data.get(\"load_to_batch_group\")\n        }\n\n        self.log.debug(\"__ loader_name_by_repre_id: {}\".format(pformat(\n            loader_name_by_repre_id)))\n\n        # get representation context from the repre_id\n        repre_contexts = op_pipeline.load.get_repres_contexts(\n            loader_name_by_repre_id.keys())\n\n        self.log.debug(\"__ repre_contexts: {}\".format(pformat(\n            repre_contexts)))\n\n        # loop all returned repres from repre_context dict\n        for repre_id, repre_context in repre_contexts.items():\n            self.log.debug(\"__ repre_id: {}\".format(repre_id))\n            # get loader name by representation id\n            loader_name = (\n                loader_name_by_repre_id[repre_id][\"loader\"]\n                # if nothing was added to settings fallback to default\n                or self.default_loader\n            )\n\n            # get loader plugin\n            loader_plugin = loaders_by_name.get(loader_name)\n            if loader_plugin:\n                # load to flame by representation context\n                try:\n                    op_pipeline.load.load_with_repre_context(\n                        loader_plugin, repre_context, **{\n                            \"data\": {\n                                \"workdir\": self.task_workdir,\n                                \"batch\": bgroup\n                            }\n                        })\n                except op_pipeline.load.IncompatibleLoaderError as msg:\n                    self.log.error(\n                        \"Check allowed representations for Loader `{}` \"\n                        \"in settings > error: {}\".format(\n                            loader_plugin.__name__, msg))\n                    self.log.error(\n                        \"Representaton context >>{}<< is not compatible \"\n                        \"with loader `{}`\".format(\n                            pformat(repre_context), loader_plugin.__name__\n                        )\n                    )\n            else:\n                self.log.warning(\n                    \"Something got wrong and there is not Loader found for \"\n                    \"following data: {}\".format(\n                        pformat(loader_name_by_repre_id))\n                )\n\n    def _get_batch_group(self, instance, task_data):\n        frame_start = instance.data[\"frameStart\"]\n        frame_end = instance.data[\"frameEnd\"]\n        handle_start = instance.data[\"handleStart\"]\n        handle_end = instance.data[\"handleEnd\"]\n        frame_duration = (frame_end - frame_start) + 1\n        asset_name = instance.data[\"asset\"]\n\n        task_name = task_data[\"name\"]\n        batchgroup_name = \"{}_{}\".format(asset_name, task_name)\n\n        batch_data = {\n            \"shematic_reels\": [\n                \"OP_LoadedReel\"\n            ],\n            \"handleStart\": handle_start,\n            \"handleEnd\": handle_end\n        }\n        self.log.debug(\n            \"__ batch_data: {}\".format(pformat(batch_data)))\n\n        # check if the batch group already exists\n        bgroup = opfapi.get_batch_group_from_desktop(batchgroup_name)\n\n        if not bgroup:\n            self.log.info(\n                \"Creating new batch group: {}\".format(batchgroup_name))\n            # create batch with utils\n            bgroup = opfapi.create_batch_group(\n                batchgroup_name,\n                frame_start,\n                frame_duration,\n                **batch_data\n            )\n\n        else:\n            self.log.info(\n                \"Updating batch group: {}\".format(batchgroup_name))\n            # update already created batch group\n            bgroup = opfapi.create_batch_group(\n                batchgroup_name,\n                frame_start,\n                frame_duration,\n                update_batch_group=bgroup,\n                **batch_data\n            )\n\n        return bgroup\n\n    def _get_anamoty_data_with_current_task(self, instance, task_data):\n        anatomy_data = copy.deepcopy(instance.data[\"anatomyData\"])\n        task_name = task_data[\"name\"]\n        task_type = task_data[\"type\"]\n        anatomy_obj = instance.context.data[\"anatomy\"]\n\n        # update task data in anatomy data\n        project_task_types = anatomy_obj[\"tasks\"]\n        task_code = project_task_types.get(task_type, {}).get(\"short_name\")\n        anatomy_data.update({\n            \"task\": {\n                \"name\": task_name,\n                \"type\": task_type,\n                \"short\": task_code\n            }\n        })\n        return anatomy_data\n\n    def _get_write_prefs(self, instance, task_data):\n        # update task in anatomy data\n        anatomy_data = self._get_anamoty_data_with_current_task(\n            instance, task_data)\n\n        self.task_workdir = self._get_shot_task_dir_path(\n            instance, task_data)\n        self.log.debug(\"__ task_workdir: {}\".format(\n            self.task_workdir))\n\n        # TODO: this might be done with template in settings\n        render_dir_path = os.path.join(\n            self.task_workdir, \"render\", \"flame\")\n\n        if not os.path.exists(render_dir_path):\n            os.makedirs(render_dir_path, mode=0o777)\n\n        # TODO: add most of these to `imageio/flame/batch/write_node`\n        name = \"{project[code]}_{asset}_{task[name]}\".format(\n            **anatomy_data\n        )\n\n        # The path attribute where the rendered clip is exported\n        # /path/to/file.[0001-0010].exr\n        media_path = render_dir_path\n        # name of file represented by tokens\n        media_path_pattern = (\n            \"<name>_v<iteration###>/<name>_v<iteration###>.<frame><ext>\")\n        # The Create Open Clip attribute of the Write File node. \\\n        # Determines if an Open Clip is created by the Write File node.\n        create_clip = True\n        # The Include Setup attribute of the Write File node.\n        # Determines if a Batch Setup file is created by the Write File node.\n        include_setup = True\n        # The path attribute where the Open Clip file is exported by\n        # the Write File node.\n        create_clip_path = \"<name>\"\n        # The path attribute where the Batch setup file\n        # is exported by the Write File node.\n        include_setup_path = \"./<name>_v<iteration###>\"\n        # The file type for the files written by the Write File node.\n        # Setting this attribute also overwrites format_extension,\n        # bit_depth and compress_mode to match the defaults for\n        # this file type.\n        file_type = \"OpenEXR\"\n        # The file extension for the files written by the Write File node.\n        # This attribute resets to match file_type whenever file_type\n        # is set. If you require a specific extension, you must\n        # set format_extension after setting file_type.\n        format_extension = \"exr\"\n        # The bit depth for the files written by the Write File node.\n        # This attribute resets to match file_type whenever file_type is set.\n        bit_depth = \"16\"\n        # The compressing attribute for the files exported by the Write\n        # File node. Only relevant when file_type in 'OpenEXR', 'Sgi', 'Tiff'\n        compress = True\n        # The compression format attribute for the specific File Types\n        # export by the Write File node. You must set compress_mode\n        # after setting file_type.\n        compress_mode = \"DWAB\"\n        # The frame index mode attribute of the Write File node.\n        # Value range: `Use Timecode` or `Use Start Frame`\n        frame_index_mode = \"Use Start Frame\"\n        frame_padding = 6\n        # The versioning mode of the Open Clip exported by the Write File node.\n        # Only available if create_clip = True.\n        version_mode = \"Follow Iteration\"\n        version_name = \"v<version>\"\n        version_padding = 3\n\n        # need to make sure the order of keys is correct\n        return OrderedDict((\n            (\"name\", name),\n            (\"media_path\", media_path),\n            (\"media_path_pattern\", media_path_pattern),\n            (\"create_clip\", create_clip),\n            (\"include_setup\", include_setup),\n            (\"create_clip_path\", create_clip_path),\n            (\"include_setup_path\", include_setup_path),\n            (\"file_type\", file_type),\n            (\"format_extension\", format_extension),\n            (\"bit_depth\", bit_depth),\n            (\"compress\", compress),\n            (\"compress_mode\", compress_mode),\n            (\"frame_index_mode\", frame_index_mode),\n            (\"frame_padding\", frame_padding),\n            (\"version_mode\", version_mode),\n            (\"version_name\", version_name),\n            (\"version_padding\", version_padding)\n        ))\n\n    def _get_shot_task_dir_path(self, instance, task_data):\n        project_doc = instance.data[\"projectEntity\"]\n        asset_entity = instance.data[\"assetEntity\"]\n        anatomy = instance.context.data[\"anatomy\"]\n        project_settings = instance.context.data[\"project_settings\"]\n\n        return get_workdir(\n            project_doc,\n            asset_entity,\n            task_data[\"name\"],\n            \"flame\",\n            anatomy,\n            project_settings=project_settings\n        )\n"
  },
  {
    "path": "openpype/hosts/flame/startup/openpype_babypublisher/export_preset/openpype_seg_thumbnails_jpg.xml",
    "content": "<?xml version=\"1.0\"?>\n<preset version=\"9\">\n    <type>sequence</type>\n    <comment>Creates a 8-bit Jpeg file per segment. </comment>\n    <sequence>\n        <fileType>NONE</fileType>\n        <namePattern></namePattern>\n        <composition>&lt;name&gt;</composition>\n        <includeVideo>True</includeVideo>\n        <exportVideo>True</exportVideo>\n        <videoMedia>\n            <mediaFileType>image</mediaFileType>\n            <commit>FX</commit>\n            <flatten>NoChange</flatten>\n            <exportHandles>False</exportHandles>\n            <nbHandles>10</nbHandles>\n        </videoMedia>\n        <includeAudio>True</includeAudio>\n        <exportAudio>False</exportAudio>\n        <audioMedia>\n            <mediaFileType>audio</mediaFileType>\n            <commit>FX</commit>\n            <flatten>FlattenTracks</flatten>\n            <exportHandles>True</exportHandles>\n            <nbHandles>10</nbHandles>\n        </audioMedia>\n    </sequence>\n    <video>\n        <fileType>Jpeg</fileType>\n        <codec>923688</codec>\n        <codecProfile></codecProfile>\n        <namePattern>&lt;shot name&gt;</namePattern>\n        <compressionQuality>100</compressionQuality>\n        <transferCharacteristic>2</transferCharacteristic>\n        <colorimetricSpecification>4</colorimetricSpecification>\n        <includeAlpha>False</includeAlpha>\n        <overwriteWithVersions>False</overwriteWithVersions>\n        <posterFrame>True</posterFrame>\n        <useFrameAsPoster>1</useFrameAsPoster>\n        <resize>\n            <resizeType>fit</resizeType>\n            <resizeFilter>lanczos</resizeFilter>\n            <width>1920</width>\n            <height>1080</height>\n            <bitsPerChannel>8</bitsPerChannel>\n            <numChannels>3</numChannels>\n            <floatingPoint>False</floatingPoint>\n            <bigEndian>True</bigEndian>\n            <pixelRatio>1</pixelRatio>\n            <scanFormat>P</scanFormat>\n        </resize>\n    </video>\n    <name>\n        <framePadding>4</framePadding>\n        <startFrame>1</startFrame>\n        <frameIndex>2</frameIndex>\n    </name>\n</preset>\n"
  },
  {
    "path": "openpype/hosts/flame/startup/openpype_babypublisher/export_preset/openpype_seg_video_h264.xml",
    "content": "<?xml version=\"1.0\"?>\n<preset version=\"10\">\n    <type>sequence</type>\n    <comment>Create MOV H264 files per segment with thumbnail</comment>\n    <sequence>\n        <fileType>NONE</fileType>\n        <namePattern></namePattern>\n        <composition>&lt;name&gt;</composition>\n        <includeVideo>True</includeVideo>\n        <exportVideo>True</exportVideo>\n        <videoMedia>\n            <mediaFileType>movie</mediaFileType>\n            <commit>FX</commit>\n            <flatten>FlattenTracks</flatten>\n            <exportHandles>True</exportHandles>\n            <nbHandles>5</nbHandles>\n        </videoMedia>\n        <includeAudio>True</includeAudio>\n        <exportAudio>False</exportAudio>\n        <audioMedia>\n            <mediaFileType>audio</mediaFileType>\n            <commit>Original</commit>\n            <flatten>NoChange</flatten>\n            <exportHandles>True</exportHandles>\n            <nbHandles>5</nbHandles>\n        </audioMedia>\n    </sequence>\n    <movie>\n        <fileType>QuickTime</fileType>\n        <namePattern>&lt;shot name&gt;</namePattern>\n        <yuvHeadroom>0</yuvHeadroom>\n        <yuvColourSpace>PCS_709</yuvColourSpace>\n        <operationalPattern>None</operationalPattern>\n        <companyName>Autodesk</companyName>\n        <productName>Flame</productName>\n        <versionName>2021</versionName>\n    </movie>\n    <video>\n        <fileType>QuickTime</fileType>\n        <codec>33622016</codec>\n        <codecProfile>\n            <rootPath>/opt/Autodesk/mediaconverter/</rootPath>\n            <targetVersion>2021</targetVersion>\n            <pathSuffix>/profiles/.33622016/HDTV_720p_8Mbits.cdxprof</pathSuffix>\n        </codecProfile>\n        <namePattern>&lt;shot name&gt;_&lt;video codec&gt;</namePattern>\n        <compressionQuality>50</compressionQuality>\n        <transferCharacteristic>2</transferCharacteristic>\n        <colorimetricSpecification>4</colorimetricSpecification>\n        <includeAlpha>False</includeAlpha>\n        <overwriteWithVersions>False</overwriteWithVersions>\n        <posterFrame>False</posterFrame>\n        <useFrameAsPoster>1</useFrameAsPoster>\n        <resize>\n            <resizeType>fit</resizeType>\n            <resizeFilter>gaussian</resizeFilter>\n            <width>1920</width>\n            <height>1080</height>\n            <bitsPerChannel>8</bitsPerChannel>\n            <numChannels>3</numChannels>\n            <floatingPoint>False</floatingPoint>\n            <bigEndian>True</bigEndian>\n            <pixelRatio>1</pixelRatio>\n            <scanFormat>P</scanFormat>\n        </resize>\n    </video>\n    <name>\n        <framePadding>4</framePadding>\n        <startFrame>1</startFrame>\n        <frameIndex>2</frameIndex>\n    </name>\n</preset>"
  },
  {
    "path": "openpype/hosts/flame/startup/openpype_babypublisher/modules/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/flame/startup/openpype_babypublisher/modules/app_utils.py",
    "content": "import os\nimport io\nimport ConfigParser as CP\nfrom xml.etree import ElementTree as ET\nfrom contextlib import contextmanager\n\nPLUGIN_DIR = os.path.dirname(os.path.dirname(__file__))\nEXPORT_PRESETS_DIR = os.path.join(PLUGIN_DIR, \"export_preset\")\n\nCONFIG_DIR = os.path.join(os.path.expanduser(\n    \"~/.openpype\"), \"openpype_babypublisher\")\n\n\n@contextmanager\ndef make_temp_dir():\n    import tempfile\n\n    try:\n        dirpath = tempfile.mkdtemp()\n\n        yield dirpath\n\n    except IOError as _error:\n        raise IOError(\"Not able to create temp dir file: {}\".format(_error))\n\n    finally:\n        pass\n\n\n@contextmanager\ndef get_config(section=None):\n    cfg_file_path = os.path.join(CONFIG_DIR, \"settings.ini\")\n\n    # create config dir\n    if not os.path.exists(CONFIG_DIR):\n        print(\"making dirs at: `{}`\".format(CONFIG_DIR))\n        os.makedirs(CONFIG_DIR, mode=0o777)\n\n    # write default data to settings.ini\n    if not os.path.exists(cfg_file_path):\n        default_cfg = cfg_default()\n        config = CP.RawConfigParser()\n        config.readfp(io.BytesIO(default_cfg))\n        with open(cfg_file_path, 'wb') as cfg_file:\n            config.write(cfg_file)\n\n    try:\n        config = CP.RawConfigParser()\n        config.read(cfg_file_path)\n        if section:\n            _cfg_data = {\n                k: v\n                for s in config.sections()\n                for k, v in config.items(s)\n                if s == section\n            }\n        else:\n            _cfg_data = {s: dict(config.items(s)) for s in config.sections()}\n\n        yield _cfg_data\n\n    except IOError as _error:\n        raise IOError('Not able to read settings.ini file: {}'.format(_error))\n\n    finally:\n        pass\n\n\ndef set_config(cfg_data, section=None):\n    cfg_file_path = os.path.join(CONFIG_DIR, \"settings.ini\")\n\n    config = CP.RawConfigParser()\n    config.read(cfg_file_path)\n\n    try:\n        if not section:\n            for section in cfg_data:\n                for key, value in cfg_data[section].items():\n                    config.set(section, key, value)\n        else:\n            for key, value in cfg_data.items():\n                config.set(section, key, value)\n\n        with open(cfg_file_path, 'wb') as cfg_file:\n            config.write(cfg_file)\n\n    except IOError as _error:\n        raise IOError('Not able to write settings.ini file: {}'.format(_error))\n\n\ndef cfg_default():\n    return \"\"\"\n[main]\nworkfile_start_frame = 1001\nshot_handles = 0\nshot_name_template = {sequence}_{shot}\nhierarchy_template = shots[Folder]/{sequence}[Sequence]\ncreate_task_type = Compositing\n\"\"\"\n\n\ndef configure_preset(file_path, data):\n    split_fp = os.path.splitext(file_path)\n    new_file_path = split_fp[0] + \"_tmp\" + split_fp[-1]\n    with open(file_path, \"r\") as datafile:\n        tree = ET.parse(datafile)\n        for key, value in data.items():\n            for element in tree.findall(\".//{}\".format(key)):\n                print(element)\n                element.text = str(value)\n        tree.write(new_file_path)\n\n    return new_file_path\n\n\ndef export_thumbnail(sequence, tempdir_path, data):\n    import flame\n    export_preset = os.path.join(\n        EXPORT_PRESETS_DIR,\n        \"openpype_seg_thumbnails_jpg.xml\"\n    )\n    new_path = configure_preset(export_preset, data)\n    poster_frame_exporter = flame.PyExporter()\n    poster_frame_exporter.foreground = True\n    poster_frame_exporter.export(sequence, new_path, tempdir_path)\n\n\ndef export_video(sequence, tempdir_path, data):\n    import flame\n    export_preset = os.path.join(\n        EXPORT_PRESETS_DIR,\n        \"openpype_seg_video_h264.xml\"\n    )\n    new_path = configure_preset(export_preset, data)\n    poster_frame_exporter = flame.PyExporter()\n    poster_frame_exporter.foreground = True\n    poster_frame_exporter.export(sequence, new_path, tempdir_path)\n\n\ndef timecode_to_frames(timecode, framerate):\n    def _seconds(value):\n        if isinstance(value, str):\n            _zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':'))\n            return sum(f * float(t) for f, t in _zip_ft)\n        elif isinstance(value, (int, float)):\n            return value / framerate\n        return 0\n\n    def _frames(seconds):\n        return seconds * framerate\n\n    def tc_to_frames(_timecode, start=None):\n        return _frames(_seconds(_timecode) - _seconds(start))\n\n    if '+' in timecode:\n        timecode = timecode.replace('+', ':')\n    elif '#' in timecode:\n        timecode = timecode.replace('#', ':')\n\n    frames = int(round(tc_to_frames(timecode, start='00:00:00:00')))\n\n    return frames\n"
  },
  {
    "path": "openpype/hosts/flame/startup/openpype_babypublisher/modules/ftrack_lib.py",
    "content": "import os\nimport sys\nimport six\nimport re\nimport json\n\nimport app_utils\n\n# Fill following constants or set them via environment variable\nFTRACK_MODULE_PATH = None\nFTRACK_API_KEY = None\nFTRACK_API_USER = None\nFTRACK_SERVER = None\n\n\ndef import_ftrack_api():\n    try:\n        import ftrack_api\n        return ftrack_api\n    except ImportError:\n        import sys\n        ftrk_m_p = FTRACK_MODULE_PATH or os.getenv(\"FTRACK_MODULE_PATH\")\n        sys.path.append(ftrk_m_p)\n        import ftrack_api\n        return ftrack_api\n\n\ndef get_ftrack_session():\n    import os\n    ftrack_api = import_ftrack_api()\n\n    # fill your own credentials\n    url = FTRACK_SERVER or os.getenv(\"FTRACK_SERVER\") or \"\"\n    user = FTRACK_API_USER or os.getenv(\"FTRACK_API_USER\") or \"\"\n    api = FTRACK_API_KEY or os.getenv(\"FTRACK_API_KEY\") or \"\"\n\n    first_validation = True\n    if not user:\n        print('- Ftrack Username is not set')\n        first_validation = False\n    if not api:\n        print('- Ftrack API key is not set')\n        first_validation = False\n    if not first_validation:\n        return False\n\n    try:\n        return ftrack_api.Session(\n            server_url=url,\n            api_user=user,\n            api_key=api\n        )\n    except Exception as _e:\n        print(\"Can't log into Ftrack with used credentials: {}\".format(_e))\n        ftrack_cred = {\n            'Ftrack server': str(url),\n            'Username': str(user),\n            'API key': str(api),\n        }\n\n        item_lens = [len(key) + 1 for key in ftrack_cred]\n        justify_len = max(*item_lens)\n        for key, value in ftrack_cred.items():\n            print('{} {}'.format((key + ':').ljust(justify_len, ' '), value))\n        return False\n\n\ndef get_project_task_types(project_entity):\n    tasks = {}\n    proj_template = project_entity['project_schema']\n    temp_task_types = proj_template['_task_type_schema']['types']\n\n    for type in temp_task_types:\n        if type['name'] not in tasks:\n            tasks[type['name']] = type\n\n    return tasks\n\n\nclass FtrackComponentCreator:\n    default_location = \"ftrack.server\"\n    ftrack_locations = {}\n    thumbnails = []\n    videos = []\n    temp_dir = None\n\n    def __init__(self, session):\n        self.session = session\n        self._get_ftrack_location()\n\n    def generate_temp_data(self, selection, change_preset_data):\n        with app_utils.make_temp_dir() as tempdir_path:\n            for seq in selection:\n                app_utils.export_thumbnail(\n                    seq, tempdir_path, change_preset_data)\n                app_utils.export_video(seq, tempdir_path, change_preset_data)\n\n        return tempdir_path\n\n    def collect_generated_data(self, tempdir_path):\n        temp_files = os.listdir(tempdir_path)\n        self.thumbnails = [f for f in temp_files if \"jpg\" in f]\n        self.videos = [f for f in temp_files if \"mov\" in f]\n        self.temp_dir = tempdir_path\n\n    def get_thumb_path(self, shot_name):\n        # get component files\n        thumb_f = next((f for f in self.thumbnails if shot_name in f), None)\n        return os.path.join(self.temp_dir, thumb_f)\n\n    def get_video_path(self, shot_name):\n        # get component files\n        video_f = next((f for f in self.videos if shot_name in f), None)\n        return os.path.join(self.temp_dir, video_f)\n\n    def close(self):\n        self.ftrack_locations = {}\n        self.session = None\n\n    def create_comonent(self, shot_entity, data, assetversion_entity=None):\n        self.shot_entity = shot_entity\n        location = self._get_ftrack_location()\n\n        file_path = data[\"file_path\"]\n\n        # get extension\n        file = os.path.basename(file_path)\n        _n, ext = os.path.splitext(file)\n\n        name = \"ftrackreview-mp4\" if \"mov\" in ext else \"thumbnail\"\n\n        component_data = {\n            \"name\": name,\n            \"file_path\": file_path,\n            \"file_type\": ext,\n            \"location\": location\n        }\n\n        if name == \"ftrackreview-mp4\":\n            duration = data[\"duration\"]\n            handles = data[\"handles\"]\n            fps = data[\"fps\"]\n            component_data[\"metadata\"] = {\n                'ftr_meta': json.dumps({\n                    'frameIn': int(0),\n                    'frameOut': int(duration + (handles * 2)),\n                    'frameRate': float(fps)\n                })\n            }\n        if not assetversion_entity:\n            # get assettype entity from session\n            assettype_entity = self._get_assettype({\"short\": \"reference\"})\n\n            # get or create asset entity from session\n            asset_entity = self._get_asset({\n                \"name\": \"plateReference\",\n                \"type\": assettype_entity,\n                \"parent\": self.shot_entity\n            })\n\n            # get or create assetversion entity from session\n            assetversion_entity = self._get_assetversion({\n                \"version\": 0,\n                \"asset\": asset_entity\n            })\n\n        # get or create component entity\n        self._set_component(component_data, {\n            \"name\": name,\n            \"version\": assetversion_entity,\n        })\n\n        return assetversion_entity\n\n    def _overwrite_members(self, entity, data):\n        origin_location = self._get_ftrack_location(\"ftrack.origin\")\n        location = data.pop(\"location\")\n\n        self._remove_component_from_location(entity, location)\n\n        entity[\"file_type\"] = data[\"file_type\"]\n\n        try:\n            origin_location.add_component(\n                entity, data[\"file_path\"]\n            )\n            # Add components to location.\n            location.add_component(\n                entity, origin_location, recursive=True)\n        except Exception as __e:\n            print(\"Error: {}\".format(__e))\n            self._remove_component_from_location(entity, origin_location)\n            origin_location.add_component(\n                entity, data[\"file_path\"]\n            )\n            # Add components to location.\n            location.add_component(\n                entity, origin_location, recursive=True)\n\n    def _remove_component_from_location(self, entity, location):\n        print(location)\n        # Removing existing members from location\n        components = list(entity.get(\"members\", []))\n        components += [entity]\n        for component in components:\n            for loc in component.get(\"component_locations\", []):\n                if location[\"id\"] == loc[\"location_id\"]:\n                    print(\"<< Removing component: {}\".format(component))\n                    location.remove_component(\n                        component, recursive=False\n                    )\n\n        # Deleting existing members on component entity\n        for member in entity.get(\"members\", []):\n            self.session.delete(member)\n            print(\"<< Deleting member: {}\".format(member))\n            del(member)\n\n        self._commit()\n\n        # Reset members in memory\n        if \"members\" in entity.keys():\n            entity[\"members\"] = []\n\n    def _get_assettype(self, data):\n        return self.session.query(\n            self._query(\"AssetType\", data)).first()\n\n    def _set_component(self, comp_data, base_data):\n        component_metadata = comp_data.pop(\"metadata\", {})\n\n        component_entity = self.session.query(\n            self._query(\"Component\", base_data)\n        ).first()\n\n        if component_entity:\n            # overwrite existing members in component entity\n            # - get data for member from `ftrack.origin` location\n            self._overwrite_members(component_entity, comp_data)\n\n            # Adding metadata\n            existing_component_metadata = component_entity[\"metadata\"]\n            existing_component_metadata.update(component_metadata)\n            component_entity[\"metadata\"] = existing_component_metadata\n            return\n\n        assetversion_entity = base_data[\"version\"]\n        location = comp_data.pop(\"location\")\n\n        component_entity = assetversion_entity.create_component(\n            comp_data[\"file_path\"],\n            data=comp_data,\n            location=location\n        )\n\n        # Adding metadata\n        existing_component_metadata = component_entity[\"metadata\"]\n        existing_component_metadata.update(component_metadata)\n        component_entity[\"metadata\"] = existing_component_metadata\n\n        if comp_data[\"name\"] == \"thumbnail\":\n            self.shot_entity[\"thumbnail_id\"] = component_entity[\"id\"]\n            assetversion_entity[\"thumbnail_id\"] = component_entity[\"id\"]\n\n        self._commit()\n\n    def _get_asset(self, data):\n        # first find already created\n        asset_entity = self.session.query(\n            self._query(\"Asset\", data)\n        ).first()\n\n        if asset_entity:\n            return asset_entity\n\n        asset_entity = self.session.create(\"Asset\", data)\n\n        # _commit if created\n        self._commit()\n\n        return asset_entity\n\n    def _get_assetversion(self, data):\n        assetversion_entity = self.session.query(\n            self._query(\"AssetVersion\", data)\n        ).first()\n\n        if assetversion_entity:\n            return assetversion_entity\n\n        assetversion_entity = self.session.create(\"AssetVersion\", data)\n\n        # _commit if created\n        self._commit()\n\n        return assetversion_entity\n\n    def _commit(self):\n        try:\n            self.session.commit()\n        except Exception:\n            tp, value, tb = sys.exc_info()\n            # self.session.rollback()\n            # self.session._configure_locations()\n            six.reraise(tp, value, tb)\n\n    def _get_ftrack_location(self, name=None):\n        name = name or self.default_location\n\n        if name in self.ftrack_locations:\n            return self.ftrack_locations[name]\n\n        location = self.session.query(\n            'Location where name is \"{}\"'.format(name)\n        ).one()\n        self.ftrack_locations[name] = location\n        return location\n\n    def _query(self, entitytype, data):\n        \"\"\" Generate a query expression from data supplied.\n\n        If a value is not a string, we'll add the id of the entity to the\n        query.\n\n        Args:\n            entitytype (str): The type of entity to query.\n            data (dict): The data to identify the entity.\n            exclusions (list): All keys to exclude from the query.\n\n        Returns:\n            str: String query to use with \"session.query\"\n        \"\"\"\n        queries = []\n        if sys.version_info[0] < 3:\n            for key, value in data.items():\n                if not isinstance(value, (str, int)):\n                    print(\"value: {}\".format(value))\n                    if \"id\" in value.keys():\n                        queries.append(\n                            \"{0}.id is \\\"{1}\\\"\".format(key, value[\"id\"])\n                        )\n                else:\n                    queries.append(\"{0} is \\\"{1}\\\"\".format(key, value))\n        else:\n            for key, value in data.items():\n                if not isinstance(value, (str, int)):\n                    print(\"value: {}\".format(value))\n                    if \"id\" in value.keys():\n                        queries.append(\n                            \"{0}.id is \\\"{1}\\\"\".format(key, value[\"id\"])\n                        )\n                else:\n                    queries.append(\"{0} is \\\"{1}\\\"\".format(key, value))\n\n        query = (\n            \"select id from \" + entitytype + \" where \" + \" and \".join(queries)\n        )\n        print(query)\n        return query\n\n\nclass FtrackEntityOperator:\n    existing_tasks = []\n\n    def __init__(self, session, project_entity):\n        self.session = session\n        self.project_entity = project_entity\n\n    def commit(self):\n        try:\n            self.session.commit()\n        except Exception:\n            tp, value, tb = sys.exc_info()\n            self.session.rollback()\n            self.session._configure_locations()\n            six.reraise(tp, value, tb)\n\n    def create_ftrack_entity(self, session, type, name, parent=None):\n        parent = parent or self.project_entity\n        entity = session.create(type, {\n            'name': name,\n            'parent': parent\n        })\n        try:\n            session.commit()\n        except Exception:\n            tp, value, tb = sys.exc_info()\n            session.rollback()\n            session._configure_locations()\n            six.reraise(tp, value, tb)\n        return entity\n\n    def get_ftrack_entity(self, session, type, name, parent):\n        query = '{} where name is \"{}\" and project_id is \"{}\"'.format(\n            type, name, self.project_entity[\"id\"])\n\n        entity = session.query(query).first()\n\n        # if entity doesnt exist then create one\n        if not entity:\n            entity = self.create_ftrack_entity(\n                session,\n                type,\n                name,\n                parent\n            )\n\n        return entity\n\n    def create_parents(self, template):\n        parents = []\n        t_split = template.split(\"/\")\n        replace_patern = re.compile(r\"(\\[.*\\])\")\n        type_patern = re.compile(r\"\\[(.*)\\]\")\n\n        for t_s in t_split:\n            match_type = type_patern.findall(t_s)\n            if not match_type:\n                raise Exception((\n                    \"Missing correct type flag in : {}\"\n                    \"/n Example: name[Type]\").format(\n                        t_s)\n                )\n            new_name = re.sub(replace_patern, \"\", t_s)\n            f_type = match_type.pop()\n\n            parents.append((new_name, f_type))\n\n        return parents\n\n    def create_task(self, task_type, task_types, parent):\n        _exising_tasks = [\n            child for child in parent['children']\n            if child.entity_type.lower() == 'task'\n        ]\n\n        # add task into existing tasks if they are not already there\n        for _t in _exising_tasks:\n            if _t in self.existing_tasks:\n                continue\n            self.existing_tasks.append(_t)\n\n        existing_task = [\n            task for task in self.existing_tasks\n            if task['name'].lower() in task_type.lower()\n            if task['parent'] == parent\n        ]\n\n        if existing_task:\n            return existing_task.pop()\n\n        task = self.session.create('Task', {\n            \"name\": task_type.lower(),\n            \"parent\": parent\n        })\n        task[\"type\"] = task_types[task_type]\n\n        self.existing_tasks.append(task)\n        return task\n"
  },
  {
    "path": "openpype/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py",
    "content": "from qtpy import QtWidgets, QtCore\n\nimport uiwidgets\nimport app_utils\nimport ftrack_lib\n\n\ndef clear_inner_modules():\n    import sys\n\n    if \"ftrack_lib\" in sys.modules.keys():\n        del sys.modules[\"ftrack_lib\"]\n        print(\"Ftrack Lib module removed from sys.modules\")\n\n    if \"app_utils\" in sys.modules.keys():\n        del sys.modules[\"app_utils\"]\n        print(\"app_utils module removed from sys.modules\")\n\n    if \"uiwidgets\" in sys.modules.keys():\n        del sys.modules[\"uiwidgets\"]\n        print(\"uiwidgets module removed from sys.modules\")\n\n\nclass MainWindow(QtWidgets.QWidget):\n\n    def __init__(self, klass, *args, **kwargs):\n        super(MainWindow, self).__init__(*args, **kwargs)\n        self.panel_class = klass\n\n    def closeEvent(self, event):\n        # clear all temp data\n        print(\"Removing temp data\")\n        self.panel_class.clear_temp_data()\n        self.panel_class.close()\n        clear_inner_modules()\n        ftrack_lib.FtrackEntityOperator.existing_tasks = []\n        # now the panel can be closed\n        event.accept()\n\n\nclass FlameBabyPublisherPanel(object):\n    session = None\n    temp_data_dir = None\n    processed_components = []\n    project_entity = None\n    task_types = {}\n    all_task_types = {}\n\n    # TreeWidget\n    columns = {\n        \"Sequence name\": {\n            \"columnWidth\": 200,\n            \"order\": 0\n        },\n        \"Shot name\": {\n            \"columnWidth\": 200,\n            \"order\": 1\n        },\n        \"Clip duration\": {\n            \"columnWidth\": 100,\n            \"order\": 2\n        },\n        \"Shot description\": {\n            \"columnWidth\": 500,\n            \"order\": 3\n        },\n        \"Task description\": {\n            \"columnWidth\": 500,\n            \"order\": 4\n        },\n    }\n\n    def __init__(self, selection):\n        print(selection)\n\n        self.session = ftrack_lib.get_ftrack_session()\n        self.selection = selection\n        self.window = MainWindow(self)\n\n        # creating ui\n        self.window.setMinimumSize(1500, 600)\n        self.window.setWindowTitle('OpenPype: Baby-publisher')\n        self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)\n        self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose)\n        self.window.setFocusPolicy(QtCore.Qt.StrongFocus)\n        self.window.setStyleSheet('background-color: #313131')\n\n        self._create_project_widget()\n        self._create_tree_widget()\n        self._set_sequence_params()\n        self._generate_widgets()\n        self._generate_layouts()\n        self._timeline_info()\n        self._fix_resolution()\n\n        self.window.show()\n\n    def _generate_widgets(self):\n        with app_utils.get_config(\"main\") as cfg_data:\n            cfg_d = cfg_data\n\n        self._create_task_type_widget(cfg_d)\n\n        # input fields\n        self.shot_name_label = uiwidgets.FlameLabel(\n            'Shot name template', 'normal', self.window)\n        self.shot_name_template_input = uiwidgets.FlameLineEdit(\n            cfg_d[\"shot_name_template\"], self.window)\n\n        self.hierarchy_label = uiwidgets.FlameLabel(\n            'Parents template', 'normal', self.window)\n        self.hierarchy_template_input = uiwidgets.FlameLineEdit(\n            cfg_d[\"hierarchy_template\"], self.window)\n\n        self.start_frame_label = uiwidgets.FlameLabel(\n            'Workfile start frame', 'normal', self.window)\n        self.start_frame_input = uiwidgets.FlameLineEdit(\n            cfg_d[\"workfile_start_frame\"], self.window)\n\n        self.handles_label = uiwidgets.FlameLabel(\n            'Shot handles', 'normal', self.window)\n        self.handles_input = uiwidgets.FlameLineEdit(\n            cfg_d[\"shot_handles\"], self.window)\n\n        self.width_label = uiwidgets.FlameLabel(\n            'Sequence width', 'normal', self.window)\n        self.width_input = uiwidgets.FlameLineEdit(\n            str(self.seq_width), self.window)\n\n        self.height_label = uiwidgets.FlameLabel(\n            'Sequence height', 'normal', self.window)\n        self.height_input = uiwidgets.FlameLineEdit(\n            str(self.seq_height), self.window)\n\n        self.pixel_aspect_label = uiwidgets.FlameLabel(\n            'Pixel aspect ratio', 'normal', self.window)\n        self.pixel_aspect_input = uiwidgets.FlameLineEdit(\n            str(1.00), self.window)\n\n        self.fps_label = uiwidgets.FlameLabel(\n            'Frame rate', 'normal', self.window)\n        self.fps_input = uiwidgets.FlameLineEdit(\n            str(self.fps), self.window)\n\n        # Button\n        self.select_all_btn = uiwidgets.FlameButton(\n            'Select All', self.select_all, self.window)\n\n        self.remove_temp_data_btn = uiwidgets.FlameButton(\n            'Remove temp data', self.clear_temp_data, self.window)\n\n        self.ftrack_send_btn = uiwidgets.FlameButton(\n            'Send to Ftrack', self._send_to_ftrack, self.window)\n\n    def _generate_layouts(self):\n        # left props\n        v_shift = 0\n        prop_layout_l = QtWidgets.QGridLayout()\n        prop_layout_l.setHorizontalSpacing(30)\n        if self.project_selector_enabled:\n            prop_layout_l.addWidget(self.project_select_label, v_shift, 0)\n            prop_layout_l.addWidget(self.project_select_input, v_shift, 1)\n            v_shift += 1\n        prop_layout_l.addWidget(self.shot_name_label, (v_shift + 0), 0)\n        prop_layout_l.addWidget(\n            self.shot_name_template_input, (v_shift + 0), 1)\n        prop_layout_l.addWidget(self.hierarchy_label, (v_shift + 1), 0)\n        prop_layout_l.addWidget(\n            self.hierarchy_template_input, (v_shift + 1), 1)\n        prop_layout_l.addWidget(self.start_frame_label, (v_shift + 2), 0)\n        prop_layout_l.addWidget(self.start_frame_input, (v_shift + 2), 1)\n        prop_layout_l.addWidget(self.handles_label, (v_shift + 3), 0)\n        prop_layout_l.addWidget(self.handles_input, (v_shift + 3), 1)\n        prop_layout_l.addWidget(self.task_type_label, (v_shift + 4), 0)\n        prop_layout_l.addWidget(\n            self.task_type_input, (v_shift + 4), 1)\n\n        # right props\n        prop_widget_r = QtWidgets.QWidget(self.window)\n        prop_layout_r = QtWidgets.QGridLayout(prop_widget_r)\n        prop_layout_r.setHorizontalSpacing(30)\n        prop_layout_r.setAlignment(\n            QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)\n        prop_layout_r.setContentsMargins(0, 0, 0, 0)\n        prop_layout_r.addWidget(self.width_label, 1, 0)\n        prop_layout_r.addWidget(self.width_input, 1, 1)\n        prop_layout_r.addWidget(self.height_label, 2, 0)\n        prop_layout_r.addWidget(self.height_input, 2, 1)\n        prop_layout_r.addWidget(self.pixel_aspect_label, 3, 0)\n        prop_layout_r.addWidget(self.pixel_aspect_input, 3, 1)\n        prop_layout_r.addWidget(self.fps_label, 4, 0)\n        prop_layout_r.addWidget(self.fps_input, 4, 1)\n\n        # prop layout\n        prop_main_layout = QtWidgets.QHBoxLayout()\n        prop_main_layout.addLayout(prop_layout_l, 1)\n        prop_main_layout.addSpacing(20)\n        prop_main_layout.addWidget(prop_widget_r, 1)\n\n        # buttons layout\n        hbox = QtWidgets.QHBoxLayout()\n        hbox.addWidget(self.remove_temp_data_btn)\n        hbox.addWidget(self.select_all_btn)\n        hbox.addWidget(self.ftrack_send_btn)\n\n        # put all layouts together\n        main_frame = QtWidgets.QVBoxLayout(self.window)\n        main_frame.setMargin(20)\n        main_frame.addLayout(prop_main_layout)\n        main_frame.addWidget(self.tree)\n        main_frame.addLayout(hbox)\n\n    def _set_sequence_params(self):\n        for select in self.selection:\n            self.seq_height = select.height\n            self.seq_width = select.width\n            self.fps = float(str(select.frame_rate)[:-4])\n            break\n\n    def _create_task_type_widget(self, cfg_d):\n        print(self.project_entity)\n        self.task_types = ftrack_lib.get_project_task_types(\n            self.project_entity)\n\n        self.task_type_label = uiwidgets.FlameLabel(\n            'Create Task (type)', 'normal', self.window)\n        self.task_type_input = uiwidgets.FlamePushButtonMenu(\n            cfg_d[\"create_task_type\"], self.task_types.keys(), self.window)\n\n    def _create_project_widget(self):\n        import flame\n        # get project name from flame current project\n        self.project_name = flame.project.current_project.name\n\n        # get project from ftrack -\n        # ftrack project name has to be the same as flame project!\n        query = 'Project where full_name is \"{}\"'.format(self.project_name)\n\n        # globally used variables\n        self.project_entity = self.session.query(query).first()\n\n        self.project_selector_enabled = bool(not self.project_entity)\n\n        if self.project_selector_enabled:\n            self.all_projects = self.session.query(\n                \"Project where status is active\").all()\n            self.project_entity = self.all_projects[0]\n            project_names = [p[\"full_name\"] for p in self.all_projects]\n            self.all_task_types = {\n                p[\"full_name\"]: ftrack_lib.get_project_task_types(p).keys()\n                for p in self.all_projects\n            }\n            self.project_select_label = uiwidgets.FlameLabel(\n                'Select Ftrack project', 'normal', self.window)\n            self.project_select_input = uiwidgets.FlamePushButtonMenu(\n                self.project_entity[\"full_name\"], project_names, self.window)\n            self.project_select_input.selection_changed.connect(\n                self._on_project_changed)\n\n    def _create_tree_widget(self):\n        ordered_column_labels = self.columns.keys()\n        for _name, _value in self.columns.items():\n            ordered_column_labels.pop(_value[\"order\"])\n            ordered_column_labels.insert(_value[\"order\"], _name)\n\n        self.tree = uiwidgets.FlameTreeWidget(\n            ordered_column_labels, self.window)\n\n        # Allow multiple items in tree to be selected\n        self.tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)\n\n        # Set tree column width\n        for _name, _val in self.columns.items():\n            self.tree.setColumnWidth(\n                _val[\"order\"],\n                _val[\"columnWidth\"]\n            )\n\n        # Prevent weird characters when shrinking tree columns\n        self.tree.setTextElideMode(QtCore.Qt.ElideNone)\n\n    def _resolve_project_entity(self):\n        if self.project_selector_enabled:\n            selected_project_name = self.project_select_input.text()\n            self.project_entity = next(\n                (p for p in self.all_projects\n                    if p[\"full_name\"] in selected_project_name),\n                None\n            )\n\n    def _save_ui_state_to_cfg(self):\n        _cfg_data_back = {\n            \"shot_name_template\": self.shot_name_template_input.text(),\n            \"workfile_start_frame\": self.start_frame_input.text(),\n            \"shot_handles\": self.handles_input.text(),\n            \"hierarchy_template\": self.hierarchy_template_input.text(),\n            \"create_task_type\": self.task_type_input.text()\n        }\n\n        # add cfg data back to settings.ini\n        app_utils.set_config(_cfg_data_back, \"main\")\n\n    def _send_to_ftrack(self):\n        # resolve active project and add it to self.project_entity\n        self._resolve_project_entity()\n        self._save_ui_state_to_cfg()\n\n        # get handles from gui input\n        handles = self.handles_input.text()\n\n        # get frame start from gui input\n        frame_start = int(self.start_frame_input.text())\n\n        # get task type from gui input\n        task_type = self.task_type_input.text()\n\n        # get resolution from gui inputs\n        fps = self.fps_input.text()\n\n        entity_operator = ftrack_lib.FtrackEntityOperator(\n            self.session, self.project_entity)\n        component_creator = ftrack_lib.FtrackComponentCreator(self.session)\n\n        if not self.temp_data_dir:\n            self.window.hide()\n            self.temp_data_dir = component_creator.generate_temp_data(\n                self.selection,\n                {\n                    \"nbHandles\": handles\n                }\n            )\n            self.window.show()\n\n        # collect generated files to list data for farther use\n        component_creator.collect_generated_data(self.temp_data_dir)\n\n        # Get all selected items from treewidget\n        for item in self.tree.selectedItems():\n            # frame ranges\n            frame_duration = int(item.text(2))\n            frame_end = frame_start + frame_duration\n\n            # description\n            shot_description = item.text(3)\n            task_description = item.text(4)\n\n            # other\n            sequence_name = item.text(0)\n            shot_name = item.text(1)\n\n            thumb_fp = component_creator.get_thumb_path(shot_name)\n            video_fp = component_creator.get_video_path(shot_name)\n\n            print(\"processed comps: {}\".format(self.processed_components))\n            print(\"processed thumb_fp: {}\".format(thumb_fp))\n\n            processed = False\n            if thumb_fp not in self.processed_components:\n                self.processed_components.append(thumb_fp)\n            else:\n                processed = True\n\n            print(\"processed: {}\".format(processed))\n\n            # populate full shot info\n            shot_attributes = {\n                \"sequence\": sequence_name,\n                \"shot\": shot_name,\n                \"task\": task_type\n            }\n\n            # format shot name template\n            _shot_name = self.shot_name_template_input.text().format(\n                **shot_attributes)\n\n            # format hierarchy template\n            _hierarchy_text = self.hierarchy_template_input.text().format(\n                **shot_attributes)\n            print(_hierarchy_text)\n\n            # solve parents\n            parents = entity_operator.create_parents(_hierarchy_text)\n            print(parents)\n\n            # obtain shot parents entities\n            _parent = None\n            for _name, _type in parents:\n                p_entity = entity_operator.get_ftrack_entity(\n                    self.session,\n                    _type,\n                    _name,\n                    _parent\n                )\n                print(p_entity)\n                _parent = p_entity\n\n            # obtain shot ftrack entity\n            f_s_entity = entity_operator.get_ftrack_entity(\n                self.session,\n                \"Shot\",\n                _shot_name,\n                _parent\n            )\n            print(\"Shot entity is: {}\".format(f_s_entity))\n\n            if not processed:\n                # first create thumbnail and get version entity\n                assetversion_entity = component_creator.create_comonent(\n                    f_s_entity, {\n                        \"file_path\": thumb_fp\n                    }\n                )\n\n                # secondly add video to version entity\n                component_creator.create_comonent(\n                    f_s_entity, {\n                        \"file_path\": video_fp,\n                        \"duration\": frame_duration,\n                        \"handles\": int(handles),\n                        \"fps\": float(fps)\n                    }, assetversion_entity\n                )\n\n            # create custom attributtes\n            custom_attrs = {\n                \"frameStart\": frame_start,\n                \"frameEnd\": frame_end,\n                \"handleStart\": int(handles),\n                \"handleEnd\": int(handles),\n                \"resolutionWidth\": int(self.width_input.text()),\n                \"resolutionHeight\": int(self.height_input.text()),\n                \"pixelAspect\": float(self.pixel_aspect_input.text()),\n                \"fps\": float(fps)\n            }\n\n            # update custom attributes on shot entity\n            for key in custom_attrs:\n                f_s_entity['custom_attributes'][key] = custom_attrs[key]\n\n            task_entity = entity_operator.create_task(\n                task_type, self.task_types, f_s_entity)\n\n            # Create notes.\n            user = self.session.query(\n                \"User where username is \\\"{}\\\"\".format(self.session.api_user)\n            ).first()\n\n            f_s_entity.create_note(shot_description, author=user)\n\n            if task_description:\n                task_entity.create_note(task_description, user)\n\n            entity_operator.commit()\n\n        component_creator.close()\n\n    def _fix_resolution(self):\n        # Center window in linux\n        resolution = QtWidgets.QDesktopWidget().screenGeometry()\n        self.window.move(\n            (resolution.width() / 2) - (self.window.frameSize().width() / 2),\n            (resolution.height() / 2) - (self.window.frameSize().height() / 2))\n\n    def _on_project_changed(self):\n        task_types = self.all_task_types[self.project_name]\n        self.task_type_input.set_menu_options(task_types)\n\n    def _timeline_info(self):\n        # identificar as informacoes dos segmentos na timeline\n        for sequence in self.selection:\n            frame_rate = float(str(sequence.frame_rate)[:-4])\n            for ver in sequence.versions:\n                for track in ver.tracks:\n                    if len(track.segments) == 0 and track.hidden:\n                        continue\n                    for segment in track.segments:\n                        print(segment.attributes)\n                        if segment.name.get_value() == \"\":\n                            continue\n                        if segment.hidden.get_value() is True:\n                            continue\n                        # get clip frame duration\n                        record_duration = str(segment.record_duration)[1:-1]\n                        clip_duration = app_utils.timecode_to_frames(\n                            record_duration, frame_rate)\n\n                        # populate shot source metadata\n                        shot_description = \"\"\n                        for attr in [\"tape_name\", \"source_name\", \"head\",\n                                     \"tail\", \"file_path\"]:\n                            if not hasattr(segment, attr):\n                                continue\n                            _value = getattr(segment, attr)\n                            _label = attr.replace(\"_\", \" \").capitalize()\n                            row = \"{}: {}\\n\".format(_label, _value)\n                            shot_description += row\n\n                        # Add timeline segment to tree\n                        QtWidgets.QTreeWidgetItem(self.tree, [\n                            sequence.name.get_value(),  # seq name\n                            segment.shot_name.get_value(),  # shot name\n                            str(clip_duration),  # clip duration\n                            shot_description,  # shot description\n                            segment.comment.get_value()  # task description\n                        ]).setFlags(\n                            QtCore.Qt.ItemIsEditable\n                            | QtCore.Qt.ItemIsEnabled\n                            | QtCore.Qt.ItemIsSelectable\n                        )\n\n        # Select top item in tree\n        self.tree.setCurrentItem(self.tree.topLevelItem(0))\n\n    def select_all(self, ):\n        self.tree.selectAll()\n\n    def clear_temp_data(self):\n        import shutil\n\n        self.processed_components = []\n\n        if self.temp_data_dir:\n            shutil.rmtree(self.temp_data_dir)\n        self.temp_data_dir = None\n        print(\"All Temp data were destroyed ...\")\n\n    def close(self):\n        self._save_ui_state_to_cfg()\n        self.session.close()\n"
  },
  {
    "path": "openpype/hosts/flame/startup/openpype_babypublisher/modules/uiwidgets.py",
    "content": "from qtpy import QtWidgets, QtCore\n\n\nclass FlameLabel(QtWidgets.QLabel):\n    \"\"\"\n    Custom Qt Flame Label Widget\n\n    For different label looks set label_type as:\n        'normal', 'background', or 'outline'\n\n    To use:\n\n    label = FlameLabel('Label Name', 'normal', window)\n    \"\"\"\n\n    def __init__(self, label_name, label_type, parent_window, *args, **kwargs):\n        super(FlameLabel, self).__init__(*args, **kwargs)\n\n        self.setText(label_name)\n        self.setParent(parent_window)\n        self.setMinimumSize(130, 28)\n        self.setMaximumHeight(28)\n        self.setFocusPolicy(QtCore.Qt.NoFocus)\n\n        # Set label stylesheet based on label_type\n\n        if label_type == 'normal':\n            self.setStyleSheet(\n                'QLabel {color: #9a9a9a; border-bottom: 1px inset #282828; font: 14px \"Discreet\"}'  # noqa\n                'QLabel:disabled {color: #6a6a6a}'\n            )\n        elif label_type == 'background':\n            self.setAlignment(QtCore.Qt.AlignCenter)\n            self.setStyleSheet(\n                'color: #9a9a9a; background-color: #393939; font: 14px \"Discreet\"'  # noqa\n            )\n        elif label_type == 'outline':\n            self.setAlignment(QtCore.Qt.AlignCenter)\n            self.setStyleSheet(\n                'color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px \"Discreet\"'  # noqa\n            )\n\n\nclass FlameLineEdit(QtWidgets.QLineEdit):\n    \"\"\"\n    Custom Qt Flame Line Edit Widget\n\n    Main window should include this:\n        window.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n    To use:\n\n    line_edit = FlameLineEdit('Some text here', window)\n    \"\"\"\n\n    def __init__(self, text, parent_window, *args, **kwargs):\n        super(FlameLineEdit, self).__init__(*args, **kwargs)\n\n        self.setText(text)\n        self.setParent(parent_window)\n        self.setMinimumHeight(28)\n        self.setMinimumWidth(110)\n        self.setStyleSheet(\n            'QLineEdit {color: #9a9a9a; background-color: #373e47; selection-color: #262626; selection-background-color: #b8b1a7; font: 14px \"Discreet\"}'  # noqa\n            'QLineEdit:focus {background-color: #474e58}'  # noqa\n            'QLineEdit:disabled {color: #6a6a6a; background-color: #373737}'\n        )\n\n\nclass FlameTreeWidget(QtWidgets.QTreeWidget):\n    \"\"\"\n    Custom Qt Flame Tree Widget\n\n    To use:\n\n    tree_headers = ['Header1', 'Header2', 'Header3', 'Header4']\n    tree = FlameTreeWidget(tree_headers, window)\n    \"\"\"\n\n    def __init__(self, tree_headers, parent_window, *args, **kwargs):\n        super(FlameTreeWidget, self).__init__(*args, **kwargs)\n\n        self.setMinimumWidth(1000)\n        self.setMinimumHeight(300)\n        self.setSortingEnabled(True)\n        self.sortByColumn(0, QtCore.Qt.AscendingOrder)\n        self.setAlternatingRowColors(True)\n        self.setFocusPolicy(QtCore.Qt.NoFocus)\n        self.setStyleSheet(\n            'QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px \"Discreet\"}'  # noqa\n            'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}'  # noqa\n            'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px \"Discreet\"}'  # noqa\n            'QTreeWidget::item:selected {selection-background-color: #111111}'\n            'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px \"Discreet\"}'  # noqa\n            'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}'\n        )\n        self.verticalScrollBar().setStyleSheet('color: #818181')\n        self.horizontalScrollBar().setStyleSheet('color: #818181')\n        self.setHeaderLabels(tree_headers)\n\n\nclass FlameButton(QtWidgets.QPushButton):\n    \"\"\"\n    Custom Qt Flame Button Widget\n\n    To use:\n\n    button = FlameButton('Button Name', do_this_when_pressed, window)\n    \"\"\"\n\n    def __init__(self, button_name, do_when_pressed, parent_window,\n                 *args, **kwargs):\n        super(FlameButton, self).__init__(*args, **kwargs)\n\n        self.setText(button_name)\n        self.setParent(parent_window)\n        self.setMinimumSize(QtCore.QSize(110, 28))\n        self.setMaximumSize(QtCore.QSize(110, 28))\n        self.setFocusPolicy(QtCore.Qt.NoFocus)\n        self.clicked.connect(do_when_pressed)\n        self.setStyleSheet(\n            'QPushButton {color: #9a9a9a; background-color: #424142; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px \"Discreet\"}'  # noqa\n            'QPushButton:pressed {color: #d9d9d9; background-color: #4f4f4f; border-top: 1px inset #666666; font: italic}'  # noqa\n           'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}'  # noqa\n        )\n\n\nclass FlamePushButton(QtWidgets.QPushButton):\n    \"\"\"\n    Custom Qt Flame Push Button Widget\n\n    To use:\n\n    pushbutton = FlamePushButton(' Button Name', True_or_False, window)\n    \"\"\"\n\n    def __init__(self, button_name, button_checked, parent_window,\n                 *args, **kwargs):\n        super(FlamePushButton, self).__init__(*args, **kwargs)\n\n        self.setText(button_name)\n        self.setParent(parent_window)\n        self.setCheckable(True)\n        self.setChecked(button_checked)\n        self.setMinimumSize(155, 28)\n        self.setMaximumSize(155, 28)\n        self.setFocusPolicy(QtCore.Qt.NoFocus)\n        self.setStyleSheet(\n            'QPushButton {color: #9a9a9a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #424142, stop: .94 #2e3b48); text-align: left; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px \"Discreet\"}'  # noqa\n            'QPushButton:checked {color: #d9d9d9; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #4f4f4f, stop: .94 #5a7fb4); font: italic; border: 1px inset black; border-bottom: 1px inset #404040; border-right: 1px inset #404040}'  # noqa\n            'QPushButton:disabled {color: #6a6a6a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #383838, stop: .94 #353535); font: light; border-top: 1px solid #575757; border-bottom: 1px solid #242424; border-right: 1px solid #353535; border-left: 1px solid #353535}'  # noqa\n            'QToolTip {color: black; background-color: #ffffde; border: black solid 1px}'  # noqa\n        )\n\n\nclass FlamePushButtonMenu(QtWidgets.QPushButton):\n    \"\"\"\n    Custom Qt Flame Menu Push Button Widget\n\n    To use:\n\n    push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4']\n    menu_push_button = FlamePushButtonMenu('push_button_name',\n    push_button_menu_options, window)\n\n    or\n\n    push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4']\n    menu_push_button = FlamePushButtonMenu(push_button_menu_options[0],\n    push_button_menu_options, window)\n    \"\"\"\n    selection_changed = QtCore.Signal(str)\n\n    def __init__(self, button_name, menu_options, parent_window,\n                 *args, **kwargs):\n        super(FlamePushButtonMenu, self).__init__(*args, **kwargs)\n\n        self.setParent(parent_window)\n        self.setMinimumHeight(28)\n        self.setMinimumWidth(110)\n        self.setFocusPolicy(QtCore.Qt.NoFocus)\n        self.setStyleSheet(\n            'QPushButton {color: #9a9a9a; background-color: #24303d; font: 14px \"Discreet\"}'  # noqa\n            'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}'  # noqa\n        )\n\n        pushbutton_menu = QtWidgets.QMenu(parent_window)\n        pushbutton_menu.setFocusPolicy(QtCore.Qt.NoFocus)\n        pushbutton_menu.setStyleSheet(\n            'QMenu {color: #9a9a9a; background-color:#24303d; font: 14px \"Discreet\"}'  # noqa\n            'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}'\n        )\n\n        self._pushbutton_menu = pushbutton_menu\n        self.setMenu(pushbutton_menu)\n        self.set_menu_options(menu_options, button_name)\n\n    def set_menu_options(self, menu_options, current_option=None):\n        self._pushbutton_menu.clear()\n        current_option = current_option or menu_options[0]\n\n        for option in menu_options:\n            action = self._pushbutton_menu.addAction(option)\n            action.triggered.connect(self._on_action_trigger)\n\n        if current_option is not None:\n            self.setText(current_option)\n\n    def _on_action_trigger(self):\n        action = self.sender()\n        self.setText(action.text())\n        self.selection_changed.emit(action.text())\n"
  },
  {
    "path": "openpype/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py",
    "content": "from __future__ import print_function\n\nimport os\nimport sys\n\n# only testing dependency for nested modules in package\nimport six  # noqa\n\n\nSCRIPT_DIR = os.path.dirname(__file__)\nPACKAGE_DIR = os.path.join(SCRIPT_DIR, \"modules\")\nsys.path.append(PACKAGE_DIR)\n\n\ndef flame_panel_executor(selection):\n    if \"panel_app\" in sys.modules.keys():\n        print(\"panel_app module is already loaded\")\n        del sys.modules[\"panel_app\"]\n        import panel_app\n        reload(panel_app)  # noqa\n        print(\"panel_app module removed from sys.modules\")\n\n    panel_app.FlameBabyPublisherPanel(selection)\n\n\ndef scope_sequence(selection):\n    import flame\n    return any(isinstance(item, flame.PySequence) for item in selection)\n\n\ndef get_media_panel_custom_ui_actions():\n    return [\n        {\n            \"name\": \"OpenPype: Baby-publisher\",\n            \"actions\": [\n                {\n                    \"name\": \"Create Shots\",\n                    \"isVisible\": scope_sequence,\n                    \"execute\": flame_panel_executor\n                }\n            ]\n        }\n    ]\n"
  },
  {
    "path": "openpype/hosts/flame/startup/openpype_in_flame.py",
    "content": "from __future__ import print_function\nimport sys\nfrom qtpy import QtWidgets\nfrom pprint import pformat\nimport atexit\n\nimport openpype.hosts.flame.api as opfapi\nfrom openpype.pipeline import (\n    install_host,\n    registered_host,\n)\n\n\ndef openpype_install():\n    \"\"\"Registering OpenPype in context\n    \"\"\"\n    install_host(opfapi)\n    print(\"Registered host: {}\".format(registered_host()))\n\n\n# Exception handler\ndef exeption_handler(exctype, value, _traceback):\n    \"\"\"Exception handler for improving UX\n\n    Args:\n        exctype (str): type of exception\n        value (str): exception value\n        tb (str): traceback to show\n    \"\"\"\n    import traceback\n    msg = \"OpenPype: Python exception {} in {}\".format(value, exctype)\n    mbox = QtWidgets.QMessageBox()\n    mbox.setText(msg)\n    mbox.setDetailedText(\n        pformat(traceback.format_exception(exctype, value, _traceback)))\n    mbox.setStyleSheet('QLabel{min-width: 800px;}')\n    mbox.exec_()\n    sys.__excepthook__(exctype, value, _traceback)\n\n\n# add exception handler into sys module\nsys.excepthook = exeption_handler\n\n\n# register clean up logic to be called at Flame exit\ndef cleanup():\n    \"\"\"Cleaning up Flame framework context\n    \"\"\"\n    if opfapi.CTX.flame_apps:\n        print('`{}` cleaning up flame_apps:\\n {}\\n'.format(\n            __file__, pformat(opfapi.CTX.flame_apps)))\n        while len(opfapi.CTX.flame_apps):\n            app = opfapi.CTX.flame_apps.pop()\n            print('`{}` removing : {}'.format(__file__, app.name))\n            del app\n        opfapi.CTX.flame_apps = []\n\n    if opfapi.CTX.app_framework:\n        print('openpype\\t: {} cleaning up'.format(\n            opfapi.CTX.app_framework.bundle_name)\n        )\n        opfapi.CTX.app_framework.save_prefs()\n        opfapi.CTX.app_framework = None\n\n\natexit.register(cleanup)\n\n\ndef load_apps():\n    \"\"\"Load available flame_apps into Flame framework\n    \"\"\"\n    opfapi.CTX.flame_apps.append(\n        opfapi.FlameMenuProjectConnect(opfapi.CTX.app_framework))\n    opfapi.CTX.flame_apps.append(\n        opfapi.FlameMenuTimeline(opfapi.CTX.app_framework))\n    opfapi.CTX.flame_apps.append(\n        opfapi.FlameMenuUniversal(opfapi.CTX.app_framework))\n    opfapi.CTX.app_framework.log.info(\"Apps are loaded\")\n\n\ndef project_changed_dict(info):\n    \"\"\"Hook for project change action\n\n    Args:\n        info (str): info text\n    \"\"\"\n    cleanup()\n\n\ndef app_initialized(parent=None):\n    \"\"\"Inicialization of Framework\n\n    Args:\n        parent (obj, optional): Parent object. Defaults to None.\n    \"\"\"\n    opfapi.CTX.app_framework = opfapi.FlameAppFramework()\n\n    print(\"{} initializing\".format(\n        opfapi.CTX.app_framework.bundle_name))\n\n    load_apps()\n\n\n\"\"\"\nInitialisation of the hook is starting from here\n\nFirst it needs to test if it can import the flame module.\nThis will happen only in case a project has been loaded.\nThen `app_initialized` will load main Framework which will load\nall menu objects as flame_apps.\n\"\"\"\n\ntry:\n    import flame  # noqa\n    app_initialized(parent=None)\nexcept ImportError:\n    print(\"!!!! not able to import flame module !!!!\")\n\n\ndef rescan_hooks():\n    import flame  # noqa\n    flame.execute_shortcut('Rescan Python Hooks')\n\n\ndef _build_app_menu(app_name):\n    \"\"\"Flame menu object generator\n\n    Args:\n        app_name (str): name of menu object app\n\n    Returns:\n        list: menu object\n    \"\"\"\n    menu = []\n\n    # first find the relative appname\n    app = None\n    for _app in opfapi.CTX.flame_apps:\n        if _app.__class__.__name__ == app_name:\n            app = _app\n\n    if app:\n        menu.append(app.build_menu())\n\n    if opfapi.CTX.app_framework:\n        menu_auto_refresh = opfapi.CTX.app_framework.prefs_global.get(\n            'menu_auto_refresh', {})\n        if menu_auto_refresh.get('timeline_menu', True):\n            try:\n                import flame  # noqa\n                flame.schedule_idle_event(rescan_hooks)\n            except ImportError:\n                print(\"!-!!! not able to import flame module !!!!\")\n\n    return menu\n\n\n\"\"\" Flame hooks are starting here\n\"\"\"\n\n\ndef project_saved(project_name, save_time, is_auto_save):\n    \"\"\"Hook to activate when project is saved\n\n    Args:\n        project_name (str): name of project\n        save_time (str): time when it was saved\n        is_auto_save (bool): autosave is on or off\n    \"\"\"\n    if opfapi.CTX.app_framework:\n        opfapi.CTX.app_framework.save_prefs()\n\n\ndef get_main_menu_custom_ui_actions():\n    \"\"\"Hook to create submenu in start menu\n\n    Returns:\n        list: menu object\n    \"\"\"\n    # install openpype and the host\n    openpype_install()\n\n    return _build_app_menu(\"FlameMenuProjectConnect\")\n\n\ndef get_timeline_custom_ui_actions():\n    \"\"\"Hook to create submenu in timeline\n\n    Returns:\n        list: menu object\n    \"\"\"\n    # install openpype and the host\n    openpype_install()\n\n    return _build_app_menu(\"FlameMenuTimeline\")\n\n\ndef get_batch_custom_ui_actions():\n    \"\"\"Hook to create submenu in batch\n\n    Returns:\n        list: menu object\n    \"\"\"\n    # install openpype and the host\n    openpype_install()\n\n    return _build_app_menu(\"FlameMenuUniversal\")\n\n\ndef get_media_panel_custom_ui_actions():\n    \"\"\"Hook to create submenu in desktop\n\n    Returns:\n        list: menu object\n    \"\"\"\n    # install openpype and the host\n    openpype_install()\n\n    return _build_app_menu(\"FlameMenuUniversal\")\n"
  },
  {
    "path": "openpype/hosts/fusion/__init__.py",
    "content": "from .addon import (\n    get_fusion_version,\n    FusionAddon,\n    FUSION_HOST_DIR,\n    FUSION_VERSIONS_DICT,\n)\n\n\n__all__ = (\n    \"get_fusion_version\",\n    \"FusionAddon\",\n    \"FUSION_HOST_DIR\",\n    \"FUSION_VERSIONS_DICT\",\n)\n"
  },
  {
    "path": "openpype/hosts/fusion/addon.py",
    "content": "import os\nimport re\nfrom openpype.modules import OpenPypeModule, IHostAddon\nfrom openpype.lib import Logger\n\nFUSION_HOST_DIR = os.path.dirname(os.path.abspath(__file__))\n\n# FUSION_VERSIONS_DICT is used by the pre-launch hooks\n# The keys correspond to all currently supported Fusion versions\n# Each value is a list of corresponding Python home variables and a profile\n# number, which is used by the profile hook to set Fusion profile variables.\nFUSION_VERSIONS_DICT = {\n    9: (\"FUSION_PYTHON36_HOME\", 9),\n    16: (\"FUSION16_PYTHON36_HOME\", 16),\n    17: (\"FUSION16_PYTHON36_HOME\", 16),\n    18: (\"FUSION_PYTHON3_HOME\", 16),\n}\n\n\ndef get_fusion_version(app_name):\n    \"\"\"\n    The function is triggered by the prelaunch hooks to get the fusion version.\n\n    `app_name` is obtained by prelaunch hooks from the\n    `launch_context.env.get(\"AVALON_APP_NAME\")`.\n\n    To get a correct Fusion version, a version number should be present\n    in the `applications/fusion/variants` key\n    of the Blackmagic Fusion Application Settings.\n    \"\"\"\n\n    log = Logger.get_logger(__name__)\n\n    if not app_name:\n        return\n\n    app_version_candidates = re.findall(r\"\\d+\", app_name)\n    if not app_version_candidates:\n        return\n    for app_version in app_version_candidates:\n        if int(app_version) in FUSION_VERSIONS_DICT:\n            return int(app_version)\n        else:\n            log.info(\n                \"Unsupported Fusion version: {app_version}\".format(\n                    app_version=app_version\n                )\n            )\n\n\nclass FusionAddon(OpenPypeModule, IHostAddon):\n    name = \"fusion\"\n    host_name = \"fusion\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [os.path.join(FUSION_HOST_DIR, \"hooks\")]\n\n    def add_implementation_envs(self, env, app):\n        # Set default values if are not already set via settings\n\n        defaults = {\"OPENPYPE_LOG_NO_COLORS\": \"Yes\"}\n        for key, value in defaults.items():\n            if not env.get(key):\n                env[key] = value\n\n    def get_workfile_extensions(self):\n        return [\".comp\"]\n"
  },
  {
    "path": "openpype/hosts/fusion/api/__init__.py",
    "content": "from .pipeline import (\n    FusionHost,\n    ls,\n\n    imprint_container,\n    parse_container\n)\n\nfrom .lib import (\n    maintained_selection,\n    update_frame_range,\n    set_asset_framerange,\n    get_current_comp,\n    get_bmd_library,\n    comp_lock_and_undo_chunk\n)\n\nfrom .menu import launch_openpype_menu\n\n\n__all__ = [\n    # pipeline\n    \"FusionHost\",\n    \"ls\",\n\n    \"imprint_container\",\n    \"parse_container\",\n\n    # lib\n    \"maintained_selection\",\n    \"update_frame_range\",\n    \"set_asset_framerange\",\n    \"get_current_comp\",\n    \"get_bmd_library\",\n    \"comp_lock_and_undo_chunk\",\n\n    # menu\n    \"launch_openpype_menu\",\n]\n"
  },
  {
    "path": "openpype/hosts/fusion/api/action.py",
    "content": "import pyblish.api\n\n\nfrom openpype.hosts.fusion.api.lib import get_current_comp\nfrom openpype.pipeline.publish import get_errored_instances_from_context\n\n\nclass SelectInvalidAction(pyblish.api.Action):\n    \"\"\"Select invalid nodes in Fusion when plug-in failed.\n\n    To retrieve the invalid nodes this assumes a static `get_invalid()`\n    method is available on the plugin.\n\n    \"\"\"\n\n    label = \"Select invalid\"\n    on = \"failed\"  # This action is only available on a failed plug-in\n    icon = \"search\"  # Icon from Awesome Icon\n\n    def process(self, context, plugin):\n        errored_instances = get_errored_instances_from_context(\n            context,\n            plugin=plugin,\n        )\n\n        # Get the invalid nodes for the plug-ins\n        self.log.info(\"Finding invalid nodes..\")\n        invalid = list()\n        for instance in errored_instances:\n            invalid_nodes = plugin.get_invalid(instance)\n            if invalid_nodes:\n                if isinstance(invalid_nodes, (list, tuple)):\n                    invalid.extend(invalid_nodes)\n                else:\n                    self.log.warning(\n                        \"Plug-in returned to be invalid, \"\n                        \"but has no selectable nodes.\"\n                    )\n\n        if not invalid:\n            # Assume relevant comp is current comp and clear selection\n            self.log.info(\"No invalid tools found.\")\n            comp = get_current_comp()\n            flow = comp.CurrentFrame.FlowView\n            flow.Select()  # No args equals clearing selection\n            return\n\n        # Assume a single comp\n        first_tool = invalid[0]\n        comp = first_tool.Comp()\n        flow = comp.CurrentFrame.FlowView\n        flow.Select()  # No args equals clearing selection\n        names = set()\n        for tool in invalid:\n            flow.Select(tool, True)\n            comp.SetActiveTool(tool)\n            names.add(tool.Name)\n        self.log.info(\n            \"Selecting invalid tools: %s\" % \", \".join(sorted(names))\n        )\n"
  },
  {
    "path": "openpype/hosts/fusion/api/lib.py",
    "content": "import os\nimport sys\nimport re\nimport contextlib\n\nfrom openpype.lib import Logger\nfrom openpype.client import (\n    get_asset_by_name,\n    get_subset_by_name,\n    get_last_version_by_subset_id,\n    get_representation_by_id,\n    get_representation_by_name,\n    get_representation_parents,\n)\nfrom openpype.pipeline import (\n    switch_container,\n    get_current_project_name,\n)\nfrom openpype.pipeline.context_tools import get_current_project_asset\n\nself = sys.modules[__name__]\nself._project = None\n\n\ndef update_frame_range(start, end, comp=None, set_render_range=True,\n                       handle_start=0, handle_end=0):\n    \"\"\"Set Fusion comp's start and end frame range\n\n    Args:\n        start (float, int): start frame\n        end (float, int): end frame\n        comp (object, Optional): comp object from fusion\n        set_render_range (bool, Optional): When True this will also set the\n            composition's render start and end frame.\n        handle_start (float, int, Optional): frame handles before start frame\n        handle_end (float, int, Optional): frame handles after end frame\n\n    Returns:\n        None\n\n    \"\"\"\n\n    if not comp:\n        comp = get_current_comp()\n\n    # Convert any potential none type to zero\n    handle_start = handle_start or 0\n    handle_end = handle_end or 0\n\n    attrs = {\n        \"COMPN_GlobalStart\": start - handle_start,\n        \"COMPN_GlobalEnd\": end + handle_end\n    }\n\n    # set frame range\n    if set_render_range:\n        attrs.update({\n            \"COMPN_RenderStart\": start,\n            \"COMPN_RenderEnd\": end\n        })\n\n    with comp_lock_and_undo_chunk(comp):\n        comp.SetAttrs(attrs)\n\n\ndef set_asset_framerange():\n    \"\"\"Set Comp's frame range based on current asset\"\"\"\n    asset_doc = get_current_project_asset()\n    start = asset_doc[\"data\"][\"frameStart\"]\n    end = asset_doc[\"data\"][\"frameEnd\"]\n    handle_start = asset_doc[\"data\"][\"handleStart\"]\n    handle_end = asset_doc[\"data\"][\"handleEnd\"]\n    update_frame_range(start, end, set_render_range=True,\n                       handle_start=handle_start,\n                       handle_end=handle_end)\n\n\ndef set_asset_resolution():\n    \"\"\"Set Comp's resolution width x height default based on current asset\"\"\"\n    asset_doc = get_current_project_asset()\n    width = asset_doc[\"data\"][\"resolutionWidth\"]\n    height = asset_doc[\"data\"][\"resolutionHeight\"]\n    comp = get_current_comp()\n\n    print(\"Setting comp frame format resolution to {}x{}\".format(width,\n                                                                 height))\n    comp.SetPrefs({\n        \"Comp.FrameFormat.Width\": width,\n        \"Comp.FrameFormat.Height\": height,\n    })\n\n\ndef validate_comp_prefs(comp=None, force_repair=False):\n    \"\"\"Validate current comp defaults with asset settings.\n\n    Validates fps, resolutionWidth, resolutionHeight, aspectRatio.\n\n    This does *not* validate frameStart, frameEnd, handleStart and handleEnd.\n    \"\"\"\n\n    if comp is None:\n        comp = get_current_comp()\n\n    log = Logger.get_logger(\"validate_comp_prefs\")\n\n    fields = [\n        \"name\",\n        \"data.fps\",\n        \"data.resolutionWidth\",\n        \"data.resolutionHeight\",\n        \"data.pixelAspect\"\n    ]\n    asset_doc = get_current_project_asset(fields=fields)\n    asset_data = asset_doc[\"data\"]\n\n    comp_frame_format_prefs = comp.GetPrefs(\"Comp.FrameFormat\")\n\n    # Pixel aspect ratio in Fusion is set as AspectX and AspectY so we convert\n    # the data to something that is more sensible to Fusion\n    asset_data[\"pixelAspectX\"] = asset_data.pop(\"pixelAspect\")\n    asset_data[\"pixelAspectY\"] = 1.0\n\n    validations = [\n        (\"fps\", \"Rate\", \"FPS\"),\n        (\"resolutionWidth\", \"Width\", \"Resolution Width\"),\n        (\"resolutionHeight\", \"Height\", \"Resolution Height\"),\n        (\"pixelAspectX\", \"AspectX\", \"Pixel Aspect Ratio X\"),\n        (\"pixelAspectY\", \"AspectY\", \"Pixel Aspect Ratio Y\")\n    ]\n\n    invalid = []\n    for key, comp_key, label in validations:\n        asset_value = asset_data[key]\n        comp_value = comp_frame_format_prefs.get(comp_key)\n        if asset_value != comp_value:\n            invalid_msg = \"{} {} should be {}\".format(label,\n                                                      comp_value,\n                                                      asset_value)\n            invalid.append(invalid_msg)\n\n            if not force_repair:\n                # Do not log warning if we force repair anyway\n                log.warning(\n                    \"Comp {pref} {value} does not match asset \"\n                    \"'{asset_name}' {pref} {asset_value}\".format(\n                        pref=label,\n                        value=comp_value,\n                        asset_name=asset_doc[\"name\"],\n                        asset_value=asset_value)\n                )\n\n    if invalid:\n\n        def _on_repair():\n            attributes = dict()\n            for key, comp_key, _label in validations:\n                value = asset_data[key]\n                comp_key_full = \"Comp.FrameFormat.{}\".format(comp_key)\n                attributes[comp_key_full] = value\n            comp.SetPrefs(attributes)\n\n        if force_repair:\n            log.info(\"Applying default Comp preferences..\")\n            _on_repair()\n            return\n\n        from . import menu\n        from openpype.widgets import popup\n        from openpype.style import load_stylesheet\n        dialog = popup.Popup(parent=menu.menu)\n        dialog.setWindowTitle(\"Fusion comp has invalid configuration\")\n\n        msg = \"Comp preferences mismatches '{}'\".format(asset_doc[\"name\"])\n        msg += \"\\n\" + \"\\n\".join(invalid)\n        dialog.setMessage(msg)\n        dialog.setButtonText(\"Repair\")\n        dialog.on_clicked.connect(_on_repair)\n        dialog.show()\n        dialog.raise_()\n        dialog.activateWindow()\n        dialog.setStyleSheet(load_stylesheet())\n\n\n@contextlib.contextmanager\ndef maintained_selection(comp=None):\n    \"\"\"Reset comp selection from before the context after the context\"\"\"\n    if comp is None:\n        comp = get_current_comp()\n\n    previous_selection = comp.GetToolList(True).values()\n    try:\n        yield\n    finally:\n        flow = comp.CurrentFrame.FlowView\n        flow.Select()  # No args equals clearing selection\n        if previous_selection:\n            for tool in previous_selection:\n                flow.Select(tool, True)\n\n\n@contextlib.contextmanager\ndef maintained_comp_range(comp=None,\n                          global_start=True,\n                          global_end=True,\n                          render_start=True,\n                          render_end=True):\n    \"\"\"Reset comp frame ranges from before the context after the context\"\"\"\n    if comp is None:\n        comp = get_current_comp()\n\n    comp_attrs = comp.GetAttrs()\n    preserve_attrs = {}\n    if global_start:\n        preserve_attrs[\"COMPN_GlobalStart\"] = comp_attrs[\"COMPN_GlobalStart\"]\n    if global_end:\n        preserve_attrs[\"COMPN_GlobalEnd\"] = comp_attrs[\"COMPN_GlobalEnd\"]\n    if render_start:\n        preserve_attrs[\"COMPN_RenderStart\"] = comp_attrs[\"COMPN_RenderStart\"]\n    if render_end:\n        preserve_attrs[\"COMPN_RenderEnd\"] = comp_attrs[\"COMPN_RenderEnd\"]\n\n    try:\n        yield\n    finally:\n        comp.SetAttrs(preserve_attrs)\n\n\ndef get_frame_path(path):\n    \"\"\"Get filename for the Fusion Saver with padded number as '#'\n\n    >>> get_frame_path(\"C:/test.exr\")\n    ('C:/test', 4, '.exr')\n\n    >>> get_frame_path(\"filename.00.tif\")\n    ('filename.', 2, '.tif')\n\n    >>> get_frame_path(\"foobar35.tif\")\n    ('foobar', 2, '.tif')\n\n    Args:\n        path (str): The path to render to.\n\n    Returns:\n        tuple: head, padding, tail (extension)\n\n    \"\"\"\n    filename, ext = os.path.splitext(path)\n\n    # Find a final number group\n    match = re.match('.*?([0-9]+)$', filename)\n    if match:\n        padding = len(match.group(1))\n        # remove number from end since fusion\n        # will swap it with the frame number\n        filename = filename[:-padding]\n    else:\n        padding = 4  # default Fusion padding\n\n    return filename, padding, ext\n\n\ndef get_fusion_module():\n    \"\"\"Get current Fusion instance\"\"\"\n    fusion = getattr(sys.modules[\"__main__\"], \"fusion\", None)\n    return fusion\n\n\ndef get_bmd_library():\n    \"\"\"Get bmd library\"\"\"\n    bmd = getattr(sys.modules[\"__main__\"], \"bmd\", None)\n    return bmd\n\n\ndef get_current_comp():\n    \"\"\"Get current comp in this session\"\"\"\n    fusion = get_fusion_module()\n    if fusion is not None:\n        comp = fusion.CurrentComp\n        return comp\n\n\n@contextlib.contextmanager\ndef comp_lock_and_undo_chunk(\n    comp,\n    undo_queue_name=\"Script CMD\",\n    keep_undo=True,\n):\n    \"\"\"Lock comp and open an undo chunk during the context\"\"\"\n    try:\n        comp.Lock()\n        comp.StartUndo(undo_queue_name)\n        yield\n    finally:\n        comp.Unlock()\n        comp.EndUndo(keep_undo)\n"
  },
  {
    "path": "openpype/hosts/fusion/api/menu.py",
    "content": "import os\nimport sys\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.tools.utils import host_tools\nfrom openpype.style import load_stylesheet\nfrom openpype.lib import register_event_callback\nfrom openpype.hosts.fusion.scripts import (\n    duplicate_with_inputs,\n)\nfrom openpype.hosts.fusion.api.lib import (\n    set_asset_framerange,\n    set_asset_resolution,\n)\nfrom openpype.pipeline import get_current_asset_name\nfrom openpype.resources import get_openpype_icon_filepath\nfrom openpype.tools.utils import get_qt_app\n\nfrom .pipeline import FusionEventHandler\nfrom .pulse import FusionPulse\n\n\nMENU_LABEL = os.environ[\"AVALON_LABEL\"]\n\n\nself = sys.modules[__name__]\nself.menu = None\n\n\nclass OpenPypeMenu(QtWidgets.QWidget):\n    def __init__(self, *args, **kwargs):\n        super(OpenPypeMenu, self).__init__(*args, **kwargs)\n\n        self.setObjectName(f\"{MENU_LABEL}Menu\")\n\n        icon_path = get_openpype_icon_filepath()\n        icon = QtGui.QIcon(icon_path)\n        self.setWindowIcon(icon)\n\n        self.setWindowFlags(\n            QtCore.Qt.Window\n            | QtCore.Qt.CustomizeWindowHint\n            | QtCore.Qt.WindowTitleHint\n            | QtCore.Qt.WindowMinimizeButtonHint\n            | QtCore.Qt.WindowCloseButtonHint\n            | QtCore.Qt.WindowStaysOnTopHint\n        )\n        self.render_mode_widget = None\n        self.setWindowTitle(MENU_LABEL)\n\n        asset_label = QtWidgets.QLabel(\"Context\", self)\n        asset_label.setStyleSheet(\n            \"\"\"QLabel {\n            font-size: 14px;\n            font-weight: 600;\n            color: #5f9fb8;\n        }\"\"\"\n        )\n        asset_label.setAlignment(QtCore.Qt.AlignHCenter)\n\n        workfiles_btn = QtWidgets.QPushButton(\"Workfiles...\", self)\n        create_btn = QtWidgets.QPushButton(\"Create...\", self)\n        load_btn = QtWidgets.QPushButton(\"Load...\", self)\n        publish_btn = QtWidgets.QPushButton(\"Publish...\", self)\n        manager_btn = QtWidgets.QPushButton(\"Manage...\", self)\n        libload_btn = QtWidgets.QPushButton(\"Library...\", self)\n        set_framerange_btn = QtWidgets.QPushButton(\"Set Frame Range\", self)\n        set_resolution_btn = QtWidgets.QPushButton(\"Set Resolution\", self)\n        duplicate_with_inputs_btn = QtWidgets.QPushButton(\n            \"Duplicate with input connections\", self\n        )\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(10, 20, 10, 20)\n\n        layout.addWidget(asset_label)\n\n        layout.addSpacing(20)\n\n        layout.addWidget(workfiles_btn)\n\n        layout.addSpacing(20)\n\n        layout.addWidget(create_btn)\n        layout.addWidget(load_btn)\n        layout.addWidget(publish_btn)\n        layout.addWidget(manager_btn)\n\n        layout.addSpacing(20)\n\n        layout.addWidget(libload_btn)\n\n        layout.addSpacing(20)\n\n        layout.addWidget(set_framerange_btn)\n        layout.addWidget(set_resolution_btn)\n\n        layout.addSpacing(20)\n\n        layout.addWidget(duplicate_with_inputs_btn)\n\n        self.setLayout(layout)\n\n        # Store reference so we can update the label\n        self.asset_label = asset_label\n\n        workfiles_btn.clicked.connect(self.on_workfile_clicked)\n        create_btn.clicked.connect(self.on_create_clicked)\n        publish_btn.clicked.connect(self.on_publish_clicked)\n        load_btn.clicked.connect(self.on_load_clicked)\n        manager_btn.clicked.connect(self.on_manager_clicked)\n        libload_btn.clicked.connect(self.on_libload_clicked)\n        duplicate_with_inputs_btn.clicked.connect(\n            self.on_duplicate_with_inputs_clicked\n        )\n        set_resolution_btn.clicked.connect(self.on_set_resolution_clicked)\n        set_framerange_btn.clicked.connect(self.on_set_framerange_clicked)\n\n        self._callbacks = []\n        self.register_callback(\"taskChanged\", self.on_task_changed)\n        self.on_task_changed()\n\n        # Force close current process if Fusion is closed\n        self._pulse = FusionPulse(parent=self)\n        self._pulse.start()\n\n        # Detect Fusion events as OpenPype events\n        self._event_handler = FusionEventHandler(parent=self)\n        self._event_handler.start()\n\n    def on_task_changed(self):\n        # Update current context label\n        label = get_current_asset_name()\n        self.asset_label.setText(label)\n\n    def register_callback(self, name, fn):\n        # Create a wrapper callback that we only store\n        # for as long as we want it to persist as callback\n        def _callback(*args):\n            fn()\n\n        self._callbacks.append(_callback)\n        register_event_callback(name, _callback)\n\n    def deregister_all_callbacks(self):\n        self._callbacks[:] = []\n\n    def on_workfile_clicked(self):\n        host_tools.show_workfiles()\n\n    def on_create_clicked(self):\n        host_tools.show_publisher(tab=\"create\")\n\n    def on_publish_clicked(self):\n        host_tools.show_publisher(tab=\"publish\")\n\n    def on_load_clicked(self):\n        host_tools.show_loader(use_context=True)\n\n    def on_manager_clicked(self):\n        host_tools.show_scene_inventory()\n\n    def on_libload_clicked(self):\n        host_tools.show_library_loader()\n\n    def on_duplicate_with_inputs_clicked(self):\n        duplicate_with_inputs.duplicate_with_input_connections()\n\n    def on_set_resolution_clicked(self):\n        set_asset_resolution()\n\n    def on_set_framerange_clicked(self):\n        set_asset_framerange()\n\n\ndef launch_openpype_menu():\n\n    app = get_qt_app()\n\n    pype_menu = OpenPypeMenu()\n\n    stylesheet = load_stylesheet()\n    pype_menu.setStyleSheet(stylesheet)\n\n    pype_menu.show()\n    self.menu = pype_menu\n\n    result = app.exec_()\n    print(\"Shutting down..\")\n    sys.exit(result)\n"
  },
  {
    "path": "openpype/hosts/fusion/api/pipeline.py",
    "content": "\"\"\"\nBasic avalon integration\n\"\"\"\nimport os\nimport sys\nimport logging\nimport contextlib\n\nimport pyblish.api\nfrom qtpy import QtCore\n\nfrom openpype.lib import (\n    Logger,\n    register_event_callback,\n    emit_event\n)\nfrom openpype.pipeline import (\n    register_loader_plugin_path,\n    register_creator_plugin_path,\n    register_inventory_action_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.pipeline.load import any_outdated_containers\nfrom openpype.hosts.fusion import FUSION_HOST_DIR\nfrom openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost\nfrom openpype.tools.utils import host_tools\n\n\nfrom .lib import (\n    get_current_comp,\n    comp_lock_and_undo_chunk,\n    validate_comp_prefs\n)\n\nlog = Logger.get_logger(__name__)\n\nPLUGINS_DIR = os.path.join(FUSION_HOST_DIR, \"plugins\")\n\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\nINVENTORY_PATH = os.path.join(PLUGINS_DIR, \"inventory\")\n\n\nclass FusionLogHandler(logging.Handler):\n    # Keep a reference to fusion's Print function (Remote Object)\n    _print = None\n\n    @property\n    def print(self):\n        if self._print is not None:\n            # Use cached\n            return self._print\n\n        _print = getattr(sys.modules[\"__main__\"], \"fusion\").Print\n        if _print is None:\n            # Backwards compatibility: Print method on Fusion instance was\n            # added around Fusion 17.4 and wasn't available on PyRemote Object\n            # before\n            _print = get_current_comp().Print\n        self._print = _print\n        return _print\n\n    def emit(self, record):\n        entry = self.format(record)\n        self.print(entry)\n\n\nclass FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):\n    name = \"fusion\"\n\n    def install(self):\n        \"\"\"Install fusion-specific functionality of OpenPype.\n\n        This is where you install menus and register families, data\n        and loaders into fusion.\n\n        It is called automatically when installing via\n        `openpype.pipeline.install_host(openpype.hosts.fusion.api)`\n\n        See the Maya equivalent for inspiration on how to implement this.\n\n        \"\"\"\n        # Remove all handlers associated with the root logger object, because\n        # that one always logs as \"warnings\" incorrectly.\n        for handler in logging.root.handlers[:]:\n            logging.root.removeHandler(handler)\n\n        # Attach default logging handler that prints to active comp\n        logger = logging.getLogger()\n        formatter = logging.Formatter(fmt=\"%(message)s\\n\")\n        handler = FusionLogHandler()\n        handler.setFormatter(formatter)\n        logger.addHandler(handler)\n        logger.setLevel(logging.DEBUG)\n\n        pyblish.api.register_host(\"fusion\")\n        pyblish.api.register_plugin_path(PUBLISH_PATH)\n        log.info(\"Registering Fusion plug-ins..\")\n\n        register_loader_plugin_path(LOAD_PATH)\n        register_creator_plugin_path(CREATE_PATH)\n        register_inventory_action_path(INVENTORY_PATH)\n\n        # Register events\n        register_event_callback(\"open\", on_after_open)\n        register_event_callback(\"save\", on_save)\n        register_event_callback(\"new\", on_new)\n\n    # region workfile io api\n    def has_unsaved_changes(self):\n        comp = get_current_comp()\n        return comp.GetAttrs()[\"COMPB_Modified\"]\n\n    def get_workfile_extensions(self):\n        return [\".comp\"]\n\n    def save_workfile(self, dst_path=None):\n        comp = get_current_comp()\n        comp.Save(dst_path)\n\n    def open_workfile(self, filepath):\n        # Hack to get fusion, see\n        #   openpype.hosts.fusion.api.pipeline.get_current_comp()\n        fusion = getattr(sys.modules[\"__main__\"], \"fusion\", None)\n\n        return fusion.LoadComp(filepath)\n\n    def get_current_workfile(self):\n        comp = get_current_comp()\n        current_filepath = comp.GetAttrs()[\"COMPS_FileName\"]\n        if not current_filepath:\n            return None\n\n        return current_filepath\n\n    def work_root(self, session):\n        work_dir = session[\"AVALON_WORKDIR\"]\n        scene_dir = session.get(\"AVALON_SCENEDIR\")\n        if scene_dir:\n            return os.path.join(work_dir, scene_dir)\n        else:\n            return work_dir\n    # endregion\n\n    @contextlib.contextmanager\n    def maintained_selection(self):\n        from .lib import maintained_selection\n        return maintained_selection()\n\n    def get_containers(self):\n        return ls()\n\n    def update_context_data(self, data, changes):\n        comp = get_current_comp()\n        comp.SetData(\"openpype\", data)\n\n    def get_context_data(self):\n        comp = get_current_comp()\n        return comp.GetData(\"openpype\") or {}\n\n\ndef on_new(event):\n    comp = event[\"Rets\"][\"comp\"]\n    validate_comp_prefs(comp, force_repair=True)\n\n\ndef on_save(event):\n    comp = event[\"sender\"]\n    validate_comp_prefs(comp)\n\n\ndef on_after_open(event):\n    comp = event[\"sender\"]\n    validate_comp_prefs(comp)\n\n    if any_outdated_containers():\n        log.warning(\"Scene has outdated content.\")\n\n        # Find OpenPype menu to attach to\n        from . import menu\n\n        def _on_show_scene_inventory():\n            # ensure that comp is active\n            frame = comp.CurrentFrame\n            if not frame:\n                print(\"Comp is closed, skipping show scene inventory\")\n                return\n            frame.ActivateFrame()   # raise comp window\n            host_tools.show_scene_inventory()\n\n        from openpype.widgets import popup\n        from openpype.style import load_stylesheet\n        dialog = popup.Popup(parent=menu.menu)\n        dialog.setWindowTitle(\"Fusion comp has outdated content\")\n        dialog.setMessage(\"There are outdated containers in \"\n                          \"your Fusion comp.\")\n        dialog.on_clicked.connect(_on_show_scene_inventory)\n        dialog.show()\n        dialog.raise_()\n        dialog.activateWindow()\n        dialog.setStyleSheet(load_stylesheet())\n\n\ndef ls():\n    \"\"\"List containers from active Fusion scene\n\n    This is the host-equivalent of api.ls(), but instead of listing\n    assets on disk, it lists assets already loaded in Fusion; once loaded\n    they are called 'containers'\n\n    Yields:\n        dict: container\n\n    \"\"\"\n\n    comp = get_current_comp()\n    tools = comp.GetToolList(False).values()\n\n    for tool in tools:\n        container = parse_container(tool)\n        if container:\n            yield container\n\n\ndef imprint_container(tool,\n                      name,\n                      namespace,\n                      context,\n                      loader=None):\n    \"\"\"Imprint a Loader with metadata\n\n    Containerisation enables a tracking of version, author and origin\n    for loaded assets.\n\n    Arguments:\n        tool (object): The node in Fusion to imprint as container, usually a\n            Loader.\n        name (str): Name of resulting assembly\n        namespace (str): Namespace under which to host container\n        context (dict): Asset information\n        loader (str, optional): Name of loader used to produce this container.\n\n    Returns:\n        None\n\n    \"\"\"\n\n    data = [\n        (\"schema\", \"openpype:container-2.0\"),\n        (\"id\", AVALON_CONTAINER_ID),\n        (\"name\", str(name)),\n        (\"namespace\", str(namespace)),\n        (\"loader\", str(loader)),\n        (\"representation\", str(context[\"representation\"][\"_id\"])),\n    ]\n\n    for key, value in data:\n        tool.SetData(\"avalon.{}\".format(key), value)\n\n\ndef parse_container(tool):\n    \"\"\"Returns imprinted container data of a tool\n\n    This reads the imprinted data from `imprint_container`.\n\n    \"\"\"\n\n    data = tool.GetData('avalon')\n    if not isinstance(data, dict):\n        return\n\n    # If not all required data return the empty container\n    required = ['schema', 'id', 'name',\n                'namespace', 'loader', 'representation']\n    if not all(key in data for key in required):\n        return\n\n    container = {key: data[key] for key in required}\n\n    # Store the tool's name\n    container[\"objectName\"] = tool.Name\n\n    # Store reference to the tool object\n    container[\"_tool\"] = tool\n\n    return container\n\n\nclass FusionEventThread(QtCore.QThread):\n    \"\"\"QThread which will periodically ping Fusion app for any events.\n    The fusion.UIManager must be set up to be notified of events before they'll\n    be reported by this thread, for example:\n        fusion.UIManager.AddNotify(\"Comp_Save\", None)\n\n    \"\"\"\n\n    on_event = QtCore.Signal(dict)\n\n    def run(self):\n\n        app = getattr(sys.modules[\"__main__\"], \"app\", None)\n        if app is None:\n            # No Fusion app found\n            return\n\n        # As optimization store the GetEvent method directly because every\n        # getattr of UIManager.GetEvent tries to resolve the Remote Function\n        # through the PyRemoteObject\n        get_event = app.UIManager.GetEvent\n        delay = int(os.environ.get(\"OPENPYPE_FUSION_CALLBACK_INTERVAL\", 1000))\n        while True:\n            if self.isInterruptionRequested():\n                return\n\n            # Process all events that have been queued up until now\n            while True:\n                event = get_event(False)\n                if not event:\n                    break\n                self.on_event.emit(event)\n\n            # Wait some time before processing events again\n            # to not keep blocking the UI\n            self.msleep(delay)\n\n\nclass FusionEventHandler(QtCore.QObject):\n    \"\"\"Emits OpenPype events based on Fusion events captured in a QThread.\n\n    This will emit the following OpenPype events based on Fusion actions:\n        save: Comp_Save, Comp_SaveAs\n        open: Comp_Opened\n        new: Comp_New\n\n    To use this you can attach it to you Qt UI so it runs in the background.\n    E.g.\n        >>> handler = FusionEventHandler(parent=window)\n        >>> handler.start()\n\n\n    \"\"\"\n    ACTION_IDS = [\n        \"Comp_Save\",\n        \"Comp_SaveAs\",\n        \"Comp_New\",\n        \"Comp_Opened\"\n    ]\n\n    def __init__(self, parent=None):\n        super(FusionEventHandler, self).__init__(parent=parent)\n\n        # Set up Fusion event callbacks\n        fusion = getattr(sys.modules[\"__main__\"], \"fusion\", None)\n        ui = fusion.UIManager\n\n        # Add notifications for the ones we want to listen to\n        notifiers = []\n        for action_id in self.ACTION_IDS:\n            notifier = ui.AddNotify(action_id, None)\n            notifiers.append(notifier)\n\n        # TODO: Not entirely sure whether these must be kept to avoid\n        #       garbage collection\n        self._notifiers = notifiers\n\n        self._event_thread = FusionEventThread(parent=self)\n        self._event_thread.on_event.connect(self._on_event)\n\n    def start(self):\n        self._event_thread.start()\n\n    def stop(self):\n        self._event_thread.stop()\n\n    def _on_event(self, event):\n        \"\"\"Handle Fusion events to emit OpenPype events\"\"\"\n        if not event:\n            return\n\n        what = event[\"what\"]\n\n        # Comp Save\n        if what in {\"Comp_Save\", \"Comp_SaveAs\"}:\n            if not event[\"Rets\"].get(\"success\"):\n                # If the Save action is cancelled it will still emit an\n                # event but with \"success\": False so we ignore those cases\n                return\n            # Comp was saved\n            emit_event(\"save\", data=event)\n            return\n\n        # Comp New\n        elif what in {\"Comp_New\"}:\n            emit_event(\"new\", data=event)\n\n        # Comp Opened\n        elif what in {\"Comp_Opened\"}:\n            emit_event(\"open\", data=event)\n"
  },
  {
    "path": "openpype/hosts/fusion/api/plugin.py",
    "content": "from copy import deepcopy\nimport os\n\nfrom openpype.hosts.fusion.api import (\n    get_current_comp,\n    comp_lock_and_undo_chunk,\n)\n\nfrom openpype.lib import (\n    BoolDef,\n    EnumDef,\n)\nfrom openpype.pipeline import (\n    legacy_io,\n    Creator,\n    CreatedInstance\n)\n\n\nclass GenericCreateSaver(Creator):\n    default_variants = [\"Main\", \"Mask\"]\n    description = \"Fusion Saver to generate image sequence\"\n    icon = \"fa5.eye\"\n\n    instance_attributes = [\n        \"reviewable\"\n    ]\n\n    settings_category = \"fusion\"\n\n    image_format = \"exr\"\n\n    # TODO: This should be renamed together with Nuke so it is aligned\n    temp_rendering_path_template = (\n        \"{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}\")\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        self.pass_pre_attributes_to_instance(instance_data, pre_create_data)\n\n        instance = CreatedInstance(\n            family=self.family,\n            subset_name=subset_name,\n            data=instance_data,\n            creator=self,\n        )\n        data = instance.data_to_store()\n        comp = get_current_comp()\n        with comp_lock_and_undo_chunk(comp):\n            args = (-32768, -32768)  # Magical position numbers\n            saver = comp.AddTool(\"Saver\", *args)\n\n            self._update_tool_with_data(saver, data=data)\n\n        # Register the CreatedInstance\n        self._imprint(saver, data)\n\n        # Insert the transient data\n        instance.transient_data[\"tool\"] = saver\n\n        self._add_instance_to_context(instance)\n\n        return instance\n\n    def collect_instances(self):\n        comp = get_current_comp()\n        tools = comp.GetToolList(False, \"Saver\").values()\n        for tool in tools:\n            data = self.get_managed_tool_data(tool)\n            if not data:\n                continue\n\n            # Add instance\n            created_instance = CreatedInstance.from_existing(data, self)\n\n            # Collect transient data\n            created_instance.transient_data[\"tool\"] = tool\n\n            self._add_instance_to_context(created_instance)\n\n    def update_instances(self, update_list):\n        for created_inst, _changes in update_list:\n            new_data = created_inst.data_to_store()\n            tool = created_inst.transient_data[\"tool\"]\n            self._update_tool_with_data(tool, new_data)\n            self._imprint(tool, new_data)\n\n    def remove_instances(self, instances):\n        for instance in instances:\n            # Remove the tool from the scene\n\n            tool = instance.transient_data[\"tool\"]\n            if tool:\n                tool.Delete()\n\n            # Remove the collected CreatedInstance to remove from UI directly\n            self._remove_instance_from_context(instance)\n\n    def _imprint(self, tool, data):\n        # Save all data in a \"openpype.{key}\" = value data\n\n        # Instance id is the tool's name so we don't need to imprint as data\n        data.pop(\"instance_id\", None)\n\n        active = data.pop(\"active\", None)\n        if active is not None:\n            # Use active value to set the passthrough state\n            tool.SetAttrs({\"TOOLB_PassThrough\": not active})\n\n        for key, value in data.items():\n            tool.SetData(f\"openpype.{key}\", value)\n\n    def _update_tool_with_data(self, tool, data):\n        \"\"\"Update tool node name and output path based on subset data\"\"\"\n        if \"subset\" not in data:\n            return\n\n        original_subset = tool.GetData(\"openpype.subset\")\n        original_format = tool.GetData(\n            \"openpype.creator_attributes.image_format\"\n        )\n\n        subset = data[\"subset\"]\n        if (\n            original_subset != subset\n            or original_format != data[\"creator_attributes\"][\"image_format\"]\n        ):\n            self._configure_saver_tool(data, tool, subset)\n\n    def _configure_saver_tool(self, data, tool, subset):\n        formatting_data = deepcopy(data)\n\n        # get frame padding from anatomy templates\n        frame_padding = self.project_anatomy.templates[\"frame_padding\"]\n\n        # get output format\n        ext = data[\"creator_attributes\"][\"image_format\"]\n\n        # Subset change detected\n        workdir = os.path.normpath(legacy_io.Session[\"AVALON_WORKDIR\"])\n        formatting_data.update({\n            \"workdir\": workdir,\n            \"frame\": \"0\" * frame_padding,\n            \"ext\": ext,\n            \"product\": {\n                \"name\": formatting_data[\"subset\"],\n                \"type\": formatting_data[\"family\"],\n            },\n        })\n\n        # build file path to render\n        filepath = self.temp_rendering_path_template.format(**formatting_data)\n\n        comp = get_current_comp()\n        tool[\"Clip\"] = comp.ReverseMapPath(os.path.normpath(filepath))\n\n        # Rename tool\n        if tool.Name != subset:\n            print(f\"Renaming {tool.Name} -> {subset}\")\n            tool.SetAttrs({\"TOOLS_Name\": subset})\n\n    def get_managed_tool_data(self, tool):\n        \"\"\"Return data of the tool if it matches creator identifier\"\"\"\n        data = tool.GetData(\"openpype\")\n        if not isinstance(data, dict):\n            return\n\n        required = {\n            \"id\": \"pyblish.avalon.instance\",\n            \"creator_identifier\": self.identifier,\n        }\n        for key, value in required.items():\n            if key not in data or data[key] != value:\n                return\n\n        # Get active state from the actual tool state\n        attrs = tool.GetAttrs()\n        passthrough = attrs[\"TOOLB_PassThrough\"]\n        data[\"active\"] = not passthrough\n\n        # Override publisher's UUID generation because tool names are\n        # already unique in Fusion in a comp\n        data[\"instance_id\"] = tool.Name\n\n        return data\n\n    def get_instance_attr_defs(self):\n        \"\"\"Settings for publish page\"\"\"\n        return self.get_pre_create_attr_defs()\n\n    def pass_pre_attributes_to_instance(self, instance_data, pre_create_data):\n        creator_attrs = instance_data[\"creator_attributes\"] = {}\n        for pass_key in pre_create_data.keys():\n            creator_attrs[pass_key] = pre_create_data[pass_key]\n\n    def _get_render_target_enum(self):\n        rendering_targets = {\n            \"local\": \"Local machine rendering\",\n            \"frames\": \"Use existing frames\",\n        }\n        if \"farm_rendering\" in self.instance_attributes:\n            rendering_targets[\"farm\"] = \"Farm rendering\"\n\n        return EnumDef(\n            \"render_target\", items=rendering_targets, label=\"Render target\"\n        )\n\n    def _get_reviewable_bool(self):\n        return BoolDef(\n            \"review\",\n            default=(\"reviewable\" in self.instance_attributes),\n            label=\"Review\",\n        )\n\n    def _get_image_format_enum(self):\n        image_format_options = [\"exr\", \"tga\", \"tif\", \"png\", \"jpg\"]\n        return EnumDef(\n            \"image_format\",\n            items=image_format_options,\n            default=self.image_format,\n            label=\"Output Image Format\",\n        )\n"
  },
  {
    "path": "openpype/hosts/fusion/api/pulse.py",
    "content": "import os\nimport sys\n\nfrom qtpy import QtCore\n\n\nclass PulseThread(QtCore.QThread):\n    no_response = QtCore.Signal()\n\n    def __init__(self, parent=None):\n        super(PulseThread, self).__init__(parent=parent)\n\n    def run(self):\n        app = getattr(sys.modules[\"__main__\"], \"app\", None)\n\n        # Interval in milliseconds\n        interval = os.environ.get(\"OPENPYPE_FUSION_PULSE_INTERVAL\", 1000)\n\n        while True:\n            if self.isInterruptionRequested():\n                return\n\n            # We don't need to call Test because PyRemoteObject of the app\n            # will actually fail to even resolve the Test function if it has\n            # gone down. So we can actually already just check by confirming\n            # the method is still getting resolved. (Optimization)\n            if app.Test is None:\n                self.no_response.emit()\n\n            self.msleep(interval)\n\n\nclass FusionPulse(QtCore.QObject):\n    \"\"\"A Timer that checks whether host app is still alive.\n\n    This checks whether the Fusion process is still active at a certain\n    interval. This is useful due to how Fusion runs its scripts. Each script\n    runs in its own environment and process (a `fusionscript` process each).\n    If Fusion would go down and we have a UI process running at the same time\n    then it can happen that the `fusionscript.exe` will remain running in the\n    background in limbo due to e.g. a Qt interface's QApplication that keeps\n    running infinitely.\n\n    Warning:\n        When the host is not detected this will automatically exit\n        the current process.\n\n    \"\"\"\n\n    def __init__(self, parent=None):\n        super(FusionPulse, self).__init__(parent=parent)\n        self._thread = PulseThread(parent=self)\n        self._thread.no_response.connect(self.on_no_response)\n\n    def on_no_response(self):\n        print(\"Pulse detected no response from Fusion..\")\n        sys.exit(1)\n\n    def start(self):\n        self._thread.start()\n\n    def stop(self):\n        self._thread.requestInterruption()\n"
  },
  {
    "path": "openpype/hosts/fusion/deploy/MenuScripts/README.md",
    "content": "### OpenPype deploy MenuScripts\n\nNote that this `MenuScripts` is not an official Fusion folder.\nOpenPype only uses this folder in `{fusion}/deploy/` to trigger the OpenPype menu actions.\n\nThey are used in the actions defined in `.fu` files in `{fusion}/deploy/Config`."
  },
  {
    "path": "openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py",
    "content": "# This is just a quick hack for users running Py3 locally but having no\n# Qt library installed\nimport os\nimport subprocess\nimport importlib\n\n\ntry:\n    from qtpy import API_NAME\n\n    print(f\"Qt binding: {API_NAME}\")\n    mod = importlib.import_module(API_NAME)\n    print(f\"Qt path: {mod.__file__}\")\n    print(\"Qt library found, nothing to do..\")\n\nexcept ImportError:\n    print(\"Assuming no Qt library is installed..\")\n    print('Installing PySide2 for Python 3.6: '\n          f'{os.environ[\"FUSION16_PYTHON36_HOME\"]}')\n\n    # Get full path to python executable\n    exe = \"python.exe\" if os.name == 'nt' else \"python\"\n    python = os.path.join(os.environ[\"FUSION16_PYTHON36_HOME\"], exe)\n    assert os.path.exists(python), f\"Python doesn't exist: {python}\"\n\n    # Do python -m pip install PySide2\n    args = [python, \"-m\", \"pip\", \"install\", \"PySide2\"]\n    print(f\"Args: {args}\")\n    subprocess.Popen(args)\n"
  },
  {
    "path": "openpype/hosts/fusion/deploy/MenuScripts/launch_menu.py",
    "content": "import os\nimport sys\n\nif sys.version_info < (3, 7):\n    # hack to handle discrepancy between distributed libraries and Python 3.6\n    # mostly because wrong version of urllib3\n    # TODO remove when not necessary\n    from openpype import PACKAGE_DIR\n    FUSION_HOST_DIR = os.path.join(PACKAGE_DIR, \"hosts\", \"fusion\")\n\n    vendor_path = os.path.join(FUSION_HOST_DIR, \"vendor\")\n    if vendor_path not in sys.path:\n        sys.path.insert(0, vendor_path)\n\n    print(f\"Added vendorized libraries from {vendor_path}\")\n\nfrom openpype.lib import Logger\nfrom openpype.pipeline import (\n    install_host,\n    registered_host,\n)\n\n\ndef main(env):\n    # This script working directory starts in Fusion application folder.\n    # However the contents of that folder can conflict with Qt library dlls\n    # so we make sure to move out of it to avoid DLL Load Failed errors.\n    os.chdir(\"..\")\n    from openpype.hosts.fusion.api import FusionHost\n    from openpype.hosts.fusion.api import menu\n\n    # activate resolve from pype\n    install_host(FusionHost())\n\n    log = Logger.get_logger(__name__)\n    log.info(f\"Registered host: {registered_host()}\")\n\n    menu.launch_openpype_menu()\n\n    # Initiate a QTimer to check if Fusion is still alive every X interval\n    # If Fusion is not found - kill itself\n    # todo(roy): Implement timer that ensures UI doesn't remain when e.g.\n    #            Fusion closes down\n\n\nif __name__ == \"__main__\":\n    result = main(os.environ)\n    sys.exit(not bool(result))\n"
  },
  {
    "path": "openpype/hosts/fusion/deploy/ayon/Config/menu.fu",
    "content": "{\n    Action\n    {\n        ID = \"AYON_Menu\",\n        Category = \"AYON\",\n        Name = \"AYON Menu\",\n\n        Targets =\n        {\n            Composition =\n            {\n                Execute = _Lua [=[\n                    local scriptPath = app:MapPath(\"AYON:../MenuScripts/launch_menu.py\")\n                    if bmd.fileexists(scriptPath) == false then\n                        print(\"[AYON Error] Can't run file: \" .. scriptPath)\n                    else\n                        target:RunScript(scriptPath)\n                    end\n                ]=],\n            },\n        },\n    },\n    Action\n    {\n        ID = \"AYON_Install_PySide2\",\n        Category = \"AYON\",\n        Name = \"Install PySide2\",\n\n        Targets =\n        {\n            Composition =\n            {\n                Execute = _Lua [=[\n                    local scriptPath = app:MapPath(\"AYON:../MenuScripts/install_pyside2.py\")\n                    if bmd.fileexists(scriptPath) == false then\n                        print(\"[AYON Error] Can't run file: \" .. scriptPath)\n                    else\n                        target:RunScript(scriptPath)\n                    end\n                ]=],\n            },\n        },\n    },\n    Menus\n    {\n        Target = \"ChildFrame\",\n\n        Before \"Help\"\n        {\n            Sub \"AYON\"\n            {\n                \"AYON_Menu{}\",\n                \"_\",\n                Sub \"Admin\" {\n                    \"AYON_Install_PySide2{}\"\n                }\n            }\n        },\n    },\n}\n"
  },
  {
    "path": "openpype/hosts/fusion/deploy/ayon/fusion_shared.prefs",
    "content": "{\nLocked = true,\nGlobal = {\n  Paths = {\n    Map = {\n      [\"AYON:\"] = \"$(OPENPYPE_FUSION)/deploy/ayon\",\n      [\"Config:\"] = \"UserPaths:Config;AYON:Config\",\n      [\"Scripts:\"] = \"UserPaths:Scripts;Reactor:System/Scripts\",\n    },\n  },\n  Script = {\n    PythonVersion = 3,\n    Python3Forced = true\n    },\n  UserInterface = {\n    Language = \"en_US\"\n    },\n  },\n}\n"
  },
  {
    "path": "openpype/hosts/fusion/deploy/openpype/Config/menu.fu",
    "content": "{\n    Action\n    {\n        ID = \"OpenPype_Menu\",\n        Category = \"OpenPype\",\n        Name = \"OpenPype Menu\",\n\n        Targets =\n        {\n            Composition =\n            {\n                Execute = _Lua [=[\n                    local scriptPath = app:MapPath(\"OpenPype:../MenuScripts/launch_menu.py\")\n                    if bmd.fileexists(scriptPath) == false then\n                        print(\"[OpenPype Error] Can't run file: \" .. scriptPath)\n                    else\n                        target:RunScript(scriptPath)\n                    end\n                ]=],\n            },\n        },\n    },\n    Action\n    {\n        ID = \"OpenPype_Install_PySide2\",\n        Category = \"OpenPype\",\n        Name = \"Install PySide2\",\n\n        Targets =\n        {\n            Composition =\n            {\n                Execute = _Lua [=[\n                    local scriptPath = app:MapPath(\"OpenPype:../MenuScripts/install_pyside2.py\")\n                    if bmd.fileexists(scriptPath) == false then\n                        print(\"[OpenPype Error] Can't run file: \" .. scriptPath)\n                    else\n                        target:RunScript(scriptPath)\n                    end\n                ]=],\n            },\n        },\n    },\n    Menus\n    {\n        Target = \"ChildFrame\",\n\n        Before \"Help\"\n        {\n            Sub \"OpenPype\"\n            {\n                \"OpenPype_Menu{}\",\n                \"_\",\n                Sub \"Admin\" {\n                    \"OpenPype_Install_PySide2{}\"\n                }\n            }\n        },\n    },\n}\n"
  },
  {
    "path": "openpype/hosts/fusion/deploy/openpype/fusion_shared.prefs",
    "content": "{\nLocked = true,\nGlobal = {\n  Paths = {\n    Map = {\n      [\"OpenPype:\"] = \"$(OPENPYPE_FUSION)/deploy/openpype\",\n      [\"Config:\"] = \"UserPaths:Config;OpenPype:Config\",\n      [\"Scripts:\"] = \"UserPaths:Scripts;Reactor:System/Scripts\",\n    },\n  },\n  Script = {\n    PythonVersion = 3,\n    Python3Forced = true\n    },\n  UserInterface = {\n    Language = \"en_US\"\n    },\n  },\n}\n"
  },
  {
    "path": "openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py",
    "content": "import os\nimport shutil\nimport platform\nfrom pathlib import Path\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.hosts.fusion import (\n    FUSION_HOST_DIR,\n    FUSION_VERSIONS_DICT,\n    get_fusion_version,\n)\nfrom openpype.lib.applications import (\n    PreLaunchHook,\n    LaunchTypes,\n    ApplicationLaunchFailed,\n)\n\n\nclass FusionCopyPrefsPrelaunch(PreLaunchHook):\n    \"\"\"\n    Prepares local Fusion profile directory, copies existing Fusion profile.\n    This also sets FUSION MasterPrefs variable, which is used\n    to apply Master.prefs file to override some Fusion profile settings to:\n        - enable the OpenPype menu\n        - force Python 3 over Python 2\n        - force English interface\n    Master.prefs is defined in openpype/hosts/fusion/deploy/fusion_shared.prefs\n    \"\"\"\n\n    app_groups = {\"fusion\"}\n    order = 2\n    launch_types = {LaunchTypes.local}\n\n    def get_fusion_profile_name(self, profile_version) -> str:\n        # Returns 'Default', unless FUSION16_PROFILE is set\n        return os.getenv(f\"FUSION{profile_version}_PROFILE\", \"Default\")\n\n    def get_fusion_profile_dir(self, profile_version) -> Path:\n        # Get FUSION_PROFILE_DIR variable\n        fusion_profile = self.get_fusion_profile_name(profile_version)\n        fusion_var_prefs_dir = os.getenv(\n            f\"FUSION{profile_version}_PROFILE_DIR\"\n        )\n\n        # Check if FUSION_PROFILE_DIR exists\n        if fusion_var_prefs_dir and Path(fusion_var_prefs_dir).is_dir():\n            fu_prefs_dir = Path(fusion_var_prefs_dir, fusion_profile)\n            self.log.info(f\"{fusion_var_prefs_dir} is set to {fu_prefs_dir}\")\n            return fu_prefs_dir\n\n    def get_profile_source(self, profile_version) -> Path:\n        \"\"\"Get Fusion preferences profile location.\n        See Per-User_Preferences_and_Paths on VFXpedia for reference.\n        \"\"\"\n        fusion_profile = self.get_fusion_profile_name(profile_version)\n        profile_source = self.get_fusion_profile_dir(profile_version)\n        if profile_source:\n            return profile_source\n        # otherwise get default location of the profile folder\n        fu_prefs_dir = f\"Blackmagic Design/Fusion/Profiles/{fusion_profile}\"\n        if platform.system() == \"Windows\":\n            profile_source = Path(os.getenv(\"AppData\"), fu_prefs_dir)\n        elif platform.system() == \"Darwin\":\n            profile_source = Path(\n                \"~/Library/Application Support/\", fu_prefs_dir\n            ).expanduser()\n        elif platform.system() == \"Linux\":\n            profile_source = Path(\"~/.fusion\", fu_prefs_dir).expanduser()\n        self.log.info(\n            f\"Locating source Fusion prefs directory: {profile_source}\"\n        )\n        return profile_source\n\n    def get_copy_fusion_prefs_settings(self):\n        # Get copy preferences options from the global application settings\n\n        copy_fusion_settings = self.data[\"project_settings\"][\"fusion\"].get(\n            \"copy_fusion_settings\", {}\n        )\n        if not copy_fusion_settings:\n            self.log.error(\"Copy prefs settings not found\")\n        copy_status = copy_fusion_settings.get(\"copy_status\", False)\n        force_sync = copy_fusion_settings.get(\"force_sync\", False)\n        copy_path = copy_fusion_settings.get(\"copy_path\") or None\n        if copy_path:\n            copy_path = Path(copy_path).expanduser()\n        return copy_status, copy_path, force_sync\n\n    def copy_fusion_profile(\n        self, copy_from: Path, copy_to: Path, force_sync: bool\n    ) -> None:\n        \"\"\"On the first Fusion launch copy the contents of Fusion profile\n        directory to the working predefined location. If the Openpype profile\n        folder exists, skip copying, unless re-sync is checked.\n        If the prefs were not copied on the first launch,\n        clean Fusion profile will be created in fu_profile_dir.\n        \"\"\"\n        if copy_to.exists() and not force_sync:\n            self.log.info(\n                \"Destination Fusion preferences folder already exists: \"\n                f\"{copy_to} \"\n            )\n            return\n        self.log.info(\"Starting copying Fusion preferences\")\n        self.log.debug(f\"force_sync option is set to {force_sync}\")\n        try:\n            copy_to.mkdir(exist_ok=True, parents=True)\n        except PermissionError:\n            self.log.warning(f\"Creating the folder not permitted at {copy_to}\")\n            return\n        if not copy_from.exists():\n            self.log.warning(f\"Fusion preferences not found in {copy_from}\")\n            return\n        for file in copy_from.iterdir():\n            if file.suffix in (\n                \".prefs\",\n                \".def\",\n                \".blocklist\",\n                \".fu\",\n                \".toolbars\",\n            ):\n                # convert Path to str to be compatible with Python 3.6+\n                shutil.copy(str(file), str(copy_to))\n        self.log.info(\n            f\"Successfully copied preferences: {copy_from} to {copy_to}\"\n        )\n\n    def execute(self):\n        (\n            copy_status,\n            fu_profile_dir,\n            force_sync,\n        ) = self.get_copy_fusion_prefs_settings()\n\n        # Get launched application context and return correct app version\n        app_name = self.launch_context.env.get(\"AVALON_APP_NAME\")\n        app_version = get_fusion_version(app_name)\n        if app_version is None:\n            version_names = \", \".join(str(x) for x in FUSION_VERSIONS_DICT)\n            raise ApplicationLaunchFailed(\n                \"Unable to detect valid Fusion version number from app \"\n                f\"name: {app_name}.\\nMake sure to include at least a digit \"\n                \"to indicate the Fusion version like '18'.\\n\"\n                f\"Detectable Fusion versions are: {version_names}\"\n            )\n\n        _, profile_version = FUSION_VERSIONS_DICT[app_version]\n        fu_profile = self.get_fusion_profile_name(profile_version)\n\n        # do a copy of Fusion profile if copy_status toggle is enabled\n        if copy_status and fu_profile_dir is not None:\n            profile_source = self.get_profile_source(profile_version)\n            dest_folder = Path(fu_profile_dir, fu_profile)\n            self.copy_fusion_profile(profile_source, dest_folder, force_sync)\n\n        # Add temporary profile directory variables to customize Fusion\n        # to define where it can read custom scripts and tools from\n        fu_profile_dir_variable = f\"FUSION{profile_version}_PROFILE_DIR\"\n        self.log.info(f\"Setting {fu_profile_dir_variable}: {fu_profile_dir}\")\n        self.launch_context.env[fu_profile_dir_variable] = str(fu_profile_dir)\n\n        # Add custom Fusion Master Prefs and the temporary\n        # profile directory variables to customize Fusion\n        # to define where it can read custom scripts and tools from\n        master_prefs_variable = f\"FUSION{profile_version}_MasterPrefs\"\n\n        if AYON_SERVER_ENABLED:\n            master_prefs = Path(\n                FUSION_HOST_DIR, \"deploy\", \"ayon\", \"fusion_shared.prefs\")\n        else:\n            master_prefs = Path(\n                FUSION_HOST_DIR, \"deploy\", \"openpype\", \"fusion_shared.prefs\")\n\n        self.log.info(f\"Setting {master_prefs_variable}: {master_prefs}\")\n        self.launch_context.env[master_prefs_variable] = str(master_prefs)\n"
  },
  {
    "path": "openpype/hosts/fusion/hooks/pre_fusion_setup.py",
    "content": "import os\nfrom openpype.lib.applications import (\n    PreLaunchHook,\n    LaunchTypes,\n    ApplicationLaunchFailed,\n)\nfrom openpype.hosts.fusion import (\n    FUSION_HOST_DIR,\n    FUSION_VERSIONS_DICT,\n    get_fusion_version,\n)\n\n\nclass FusionPrelaunch(PreLaunchHook):\n    \"\"\"\n    Prepares OpenPype Fusion environment.\n    Requires correct Python home variable to be defined in the environment\n    settings for Fusion to point at a valid Python 3 build for Fusion.\n    Python3 versions that are supported by Fusion:\n    Fusion 9, 16, 17 : Python 3.6\n    Fusion 18        : Python 3.6 - 3.10\n    \"\"\"\n\n    app_groups = {\"fusion\"}\n    order = 1\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        # making sure python 3 is installed at provided path\n        # Py 3.3-3.10 for Fusion 18+ or Py 3.6 for Fu 16-17\n        app_data = self.launch_context.env.get(\"AVALON_APP_NAME\")\n        app_version = get_fusion_version(app_data)\n        if not app_version:\n            raise ApplicationLaunchFailed(\n                \"Fusion version information not found in System settings.\\n\"\n                \"The key field in the 'applications/fusion/variants' should \"\n                \"consist a number, corresponding to major Fusion version.\"\n            )\n        py3_var, _ = FUSION_VERSIONS_DICT[app_version]\n        fusion_python3_home = self.launch_context.env.get(py3_var, \"\")\n\n        for path in fusion_python3_home.split(os.pathsep):\n            # Allow defining multiple paths, separated by os.pathsep,\n            # to allow \"fallback\" to other path.\n            # But make to set only a single path as final variable.\n            py3_dir = os.path.normpath(path)\n            if os.path.isdir(py3_dir):\n                break\n        else:\n            raise ApplicationLaunchFailed(\n                \"Python 3 is not installed at the provided path.\\n\"\n                \"Make sure the environment in fusion settings has \"\n                \"'FUSION_PYTHON3_HOME' set correctly and make sure \"\n                \"Python 3 is installed in the given path.\"\n                f\"\\n\\nPYTHON PATH: {fusion_python3_home}\"\n            )\n\n        self.log.info(f\"Setting {py3_var}: '{py3_dir}'...\")\n        self.launch_context.env[py3_var] = py3_dir\n\n        # Fusion 18+ requires FUSION_PYTHON3_HOME to also be on PATH\n        if app_version >= 18:\n            self.launch_context.env[\"PATH\"] += os.pathsep + py3_dir\n\n        self.launch_context.env[py3_var] = py3_dir\n\n        # for hook installing PySide2\n        self.data[\"fusion_python3_home\"] = py3_dir\n\n        self.log.info(f\"Setting OPENPYPE_FUSION: {FUSION_HOST_DIR}\")\n        self.launch_context.env[\"OPENPYPE_FUSION\"] = FUSION_HOST_DIR\n"
  },
  {
    "path": "openpype/hosts/fusion/hooks/pre_pyside_install.py",
    "content": "import os\nimport subprocess\nimport platform\nimport uuid\n\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass InstallPySideToFusion(PreLaunchHook):\n    \"\"\"Automatically installs Qt binding to fusion's python packages.\n\n    Check if fusion has installed PySide2 and will try to install if not.\n\n    For pipeline implementation is required to have Qt binding installed in\n    fusion's python packages.\n    \"\"\"\n\n    app_groups = {\"fusion\"}\n    order = 2\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        # Prelaunch hook is not crucial\n        try:\n            settings = self.data[\"project_settings\"][self.host_name]\n            if not settings[\"hooks\"][\"InstallPySideToFusion\"][\"enabled\"]:\n                return\n            self.inner_execute()\n        except Exception:\n            self.log.warning(\n                \"Processing of {} crashed.\".format(self.__class__.__name__),\n                exc_info=True\n            )\n\n    def inner_execute(self):\n        self.log.debug(\"Check for PySide2 installation.\")\n\n        fusion_python3_home = self.data.get(\"fusion_python3_home\")\n        if not fusion_python3_home:\n            self.log.warning(\"'fusion_python3_home' was not provided. \"\n                             \"Installation of PySide2 not possible\")\n            return\n\n        if platform.system().lower() == \"windows\":\n            exe_filenames = [\"python.exe\"]\n        else:\n            exe_filenames = [\"python3\", \"python\"]\n\n        for exe_filename in exe_filenames:\n            python_executable = os.path.join(fusion_python3_home, exe_filename)\n            if os.path.exists(python_executable):\n                break\n\n        if not os.path.exists(python_executable):\n            self.log.warning(\n                \"Couldn't find python executable for fusion. {}\".format(\n                    python_executable\n                )\n            )\n            return\n\n        # Check if PySide2 is installed and skip if yes\n        if self._is_pyside_installed(python_executable):\n            self.log.debug(\"Fusion has already installed PySide2.\")\n            return\n\n        self.log.debug(\"Installing PySide2.\")\n        # Install PySide2 in fusion's python\n        if self._windows_require_permissions(\n                os.path.dirname(python_executable)):\n            result = self._install_pyside_windows(python_executable)\n        else:\n            result = self._install_pyside(python_executable)\n\n        if result:\n            self.log.info(\"Successfully installed PySide2 module to fusion.\")\n        else:\n            self.log.warning(\"Failed to install PySide2 module to fusion.\")\n\n    def _install_pyside_windows(self, python_executable):\n        \"\"\"Install PySide2 python module to fusion's python.\n\n        Installation requires administration rights that's why it is required\n        to use \"pywin32\" module which can execute command's and ask for\n        administration rights.\n        \"\"\"\n        try:\n            import win32api\n            import win32con\n            import win32process\n            import win32event\n            import pywintypes\n            from win32comext.shell.shell import ShellExecuteEx\n            from win32comext.shell import shellcon\n        except Exception:\n            self.log.warning(\"Couldn't import \\\"pywin32\\\" modules\")\n            return False\n\n        try:\n            # Parameters\n            # - use \"-m pip\" as module pip to install PySide2 and argument\n            #   \"--ignore-installed\" is to force install module to fusion's\n            #   site-packages and make sure it is binary compatible\n            parameters = \"-m pip install --ignore-installed PySide2\"\n\n            # Execute command and ask for administrator's rights\n            process_info = ShellExecuteEx(\n                nShow=win32con.SW_SHOWNORMAL,\n                fMask=shellcon.SEE_MASK_NOCLOSEPROCESS,\n                lpVerb=\"runas\",\n                lpFile=python_executable,\n                lpParameters=parameters,\n                lpDirectory=os.path.dirname(python_executable)\n            )\n            process_handle = process_info[\"hProcess\"]\n            win32event.WaitForSingleObject(process_handle,\n                                           win32event.INFINITE)\n            returncode = win32process.GetExitCodeProcess(process_handle)\n            return returncode == 0\n        except pywintypes.error:\n            return False\n\n    def _install_pyside(self, python_executable):\n        \"\"\"Install PySide2 python module to fusion's python.\"\"\"\n        try:\n            # Parameters\n            # - use \"-m pip\" as module pip to install PySide2 and argument\n            #   \"--ignore-installed\" is to force install module to fusion's\n            #   site-packages and make sure it is binary compatible\n            env = dict(os.environ)\n            del env['PYTHONPATH']\n            args = [\n                python_executable,\n                \"-m\",\n                \"pip\",\n                \"install\",\n                \"--ignore-installed\",\n                \"PySide2\",\n            ]\n            process = subprocess.Popen(\n                args, stdout=subprocess.PIPE, universal_newlines=True,\n                env=env\n            )\n            process.communicate()\n            return process.returncode == 0\n        except PermissionError:\n            self.log.warning(\n                \"Permission denied with command:\"\n                \"\\\"{}\\\".\".format(\" \".join(args))\n            )\n        except OSError as error:\n            self.log.warning(f\"OS error has occurred: \\\"{error}\\\".\")\n        except subprocess.SubprocessError:\n            pass\n\n    def _is_pyside_installed(self, python_executable):\n        \"\"\"Check if PySide2 module is in fusion's pip list.\"\"\"\n        args = [python_executable, \"-c\", \"from qtpy import QtWidgets\"]\n        process = subprocess.Popen(args,\n                                   stdout=subprocess.PIPE,\n                                   stderr=subprocess.PIPE)\n        _, stderr = process.communicate()\n        stderr = stderr.decode()\n        if stderr:\n            return False\n        return True\n\n    def _windows_require_permissions(self, dirpath):\n        if platform.system().lower() != \"windows\":\n            return False\n\n        try:\n            # Attempt to create a temporary file in the folder\n            temp_file_path = os.path.join(dirpath, uuid.uuid4().hex)\n            with open(temp_file_path, \"w\"):\n                pass\n            os.remove(temp_file_path)  # Clean up temporary file\n            return False\n\n        except PermissionError:\n            return True\n\n        except BaseException as exc:\n            print((\"Failed to determine if root requires permissions.\"\n                   \"Unexpected error: {}\").format(exc))\n            return False\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/create/create_image_saver.py",
    "content": "from openpype.lib import NumberDef\n\nfrom openpype.hosts.fusion.api.plugin import GenericCreateSaver\nfrom openpype.hosts.fusion.api import get_current_comp\n\n\nclass CreateImageSaver(GenericCreateSaver):\n    \"\"\"Fusion Saver to generate single image.\n\n     Created to explicitly separate single ('image') or\n        multi frame('render) outputs.\n\n    This might be temporary creator until 'alias' functionality will be\n    implemented to limit creation of additional product types with similar, but\n    not the same workflows.\n    \"\"\"\n    identifier = \"io.openpype.creators.fusion.imagesaver\"\n    label = \"Image (saver)\"\n    name = \"image\"\n    family = \"image\"\n    description = \"Fusion Saver to generate image\"\n\n    default_frame = 0\n\n    def get_detail_description(self):\n        return \"\"\"Fusion Saver to generate single image.\n\n        This creator is expected for publishing of single frame `image` product\n        type.\n\n        Artist should provide frame number (integer) to specify which frame\n        should be published. It must be inside of global timeline frame range.\n\n        Supports local and deadline rendering.\n\n        Supports selection from predefined set of output file extensions:\n        - exr\n        - tga\n        - png\n        - tif\n        - jpg\n\n        Created to explicitly separate single frame ('image') or\n        multi frame ('render') outputs.\n        \"\"\"\n\n    def get_pre_create_attr_defs(self):\n        \"\"\"Settings for create page\"\"\"\n        attr_defs = [\n            self._get_render_target_enum(),\n            self._get_reviewable_bool(),\n            self._get_frame_int(),\n            self._get_image_format_enum(),\n        ]\n        return attr_defs\n\n    def _get_frame_int(self):\n        return NumberDef(\n            \"frame\",\n            default=self.default_frame,\n            label=\"Frame\",\n            tooltip=\"Set frame to be rendered, must be inside of global \"\n                    \"timeline range\"\n        )\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/create/create_saver.py",
    "content": "from openpype.lib import EnumDef\n\nfrom openpype.hosts.fusion.api.plugin import GenericCreateSaver\n\n\nclass CreateSaver(GenericCreateSaver):\n    \"\"\"Fusion Saver to generate image sequence of 'render' product type.\n\n     Original Saver creator targeted for 'render' product type. It uses\n     original not to descriptive name because of values in Settings.\n    \"\"\"\n    identifier = \"io.openpype.creators.fusion.saver\"\n    label = \"Render (saver)\"\n    name = \"render\"\n    family = \"render\"\n    description = \"Fusion Saver to generate image sequence\"\n\n    default_frame_range_option = \"asset_db\"\n\n    def get_detail_description(self):\n        return \"\"\"Fusion Saver to generate image sequence.\n\n        This creator is expected for publishing of image sequences for 'render'\n        product type. (But can publish even single frame 'render'.)\n\n        Select what should be source of render range:\n        - \"Current asset context\" - values set on Asset in DB (Ftrack)\n        - \"From render in/out\" - from node itself\n        - \"From composition timeline\" - from timeline\n\n        Supports local and farm rendering.\n\n        Supports selection from predefined set of output file extensions:\n        - exr\n        - tga\n        - png\n        - tif\n        - jpg\n        \"\"\"\n\n    def get_pre_create_attr_defs(self):\n        \"\"\"Settings for create page\"\"\"\n        attr_defs = [\n            self._get_render_target_enum(),\n            self._get_reviewable_bool(),\n            self._get_frame_range_enum(),\n            self._get_image_format_enum(),\n        ]\n        return attr_defs\n\n    def _get_frame_range_enum(self):\n        frame_range_options = {\n            \"asset_db\": \"Current asset context\",\n            \"render_range\": \"From render in/out\",\n            \"comp_range\": \"From composition timeline\",\n        }\n\n        return EnumDef(\n            \"frame_range_source\",\n            items=frame_range_options,\n            label=\"Frame range source\",\n            default=self.default_frame_range_option\n        )\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/create/create_workfile.py",
    "content": "from openpype.hosts.fusion.api import (\n    get_current_comp\n)\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import get_asset_by_name\nfrom openpype.pipeline import (\n    AutoCreator,\n    CreatedInstance,\n)\n\n\nclass FusionWorkfileCreator(AutoCreator):\n    identifier = \"workfile\"\n    family = \"workfile\"\n    label = \"Workfile\"\n    icon = \"fa5.file\"\n\n    default_variant = \"Main\"\n\n    create_allow_context_change = False\n\n    data_key = \"openpype_workfile\"\n\n    def collect_instances(self):\n\n        comp = get_current_comp()\n        data = comp.GetData(self.data_key)\n        if not data:\n            return\n\n        instance = CreatedInstance(\n            family=self.family,\n            subset_name=data[\"subset\"],\n            data=data,\n            creator=self\n        )\n        instance.transient_data[\"comp\"] = comp\n\n        self._add_instance_to_context(instance)\n\n    def update_instances(self, update_list):\n        for created_inst, _changes in update_list:\n            comp = created_inst.transient_data[\"comp\"]\n            if not hasattr(comp, \"SetData\"):\n                # Comp is not alive anymore, likely closed by the user\n                self.log.error(\"Workfile comp not found for existing instance.\"\n                               \" Comp might have been closed in the meantime.\")\n                continue\n\n            # Imprint data into the comp\n            data = created_inst.data_to_store()\n            comp.SetData(self.data_key, data)\n\n    def create(self, options=None):\n\n        comp = get_current_comp()\n        if not comp:\n            self.log.error(\"Unable to find current comp\")\n            return\n\n        existing_instance = None\n        for instance in self.create_context.instances:\n            if instance.family == self.family:\n                existing_instance = instance\n                break\n\n        project_name = self.create_context.get_current_project_name()\n        asset_name = self.create_context.get_current_asset_name()\n        task_name = self.create_context.get_current_task_name()\n        host_name = self.create_context.host_name\n\n        if existing_instance is None:\n            existing_instance_asset = None\n        elif AYON_SERVER_ENABLED:\n            existing_instance_asset = existing_instance[\"folderPath\"]\n        else:\n            existing_instance_asset = existing_instance[\"asset\"]\n\n        if existing_instance is None:\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                self.default_variant, task_name, asset_doc,\n                project_name, host_name\n            )\n            data = {\n                \"task\": task_name,\n                \"variant\": self.default_variant\n            }\n            if AYON_SERVER_ENABLED:\n                data[\"folderPath\"] = asset_name\n            else:\n                data[\"asset\"] = asset_name\n            data.update(self.get_dynamic_data(\n                self.default_variant, task_name, asset_doc,\n                project_name, host_name, None\n            ))\n\n            new_instance = CreatedInstance(\n                self.family, subset_name, data, self\n            )\n            new_instance.transient_data[\"comp\"] = comp\n            self._add_instance_to_context(new_instance)\n\n        elif (\n            existing_instance_asset != asset_name\n            or existing_instance[\"task\"] != task_name\n        ):\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                self.default_variant, task_name, asset_doc,\n                project_name, host_name\n            )\n            if AYON_SERVER_ENABLED:\n                existing_instance[\"folderPath\"] = asset_name\n            else:\n                existing_instance[\"asset\"] = asset_name\n            existing_instance[\"task\"] = task_name\n            existing_instance[\"subset\"] = subset_name\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/inventory/select_containers.py",
    "content": "from openpype.pipeline import InventoryAction\n\n\nclass FusionSelectContainers(InventoryAction):\n\n    label = \"Select Containers\"\n    icon = \"mouse-pointer\"\n    color = \"#d8d8d8\"\n\n    def process(self, containers):\n        from openpype.hosts.fusion.api import (\n            get_current_comp,\n            comp_lock_and_undo_chunk\n        )\n\n        tools = [i[\"_tool\"] for i in containers]\n\n        comp = get_current_comp()\n        flow = comp.CurrentFrame.FlowView\n\n        with comp_lock_and_undo_chunk(comp, self.label):\n            # Clear selection\n            flow.Select()\n\n            # Select tool\n            for tool in tools:\n                flow.Select(tool)\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/inventory/set_tool_color.py",
    "content": "from qtpy import QtGui, QtWidgets\n\nfrom openpype.pipeline import InventoryAction\nfrom openpype import style\nfrom openpype.hosts.fusion.api import (\n    get_current_comp,\n    comp_lock_and_undo_chunk\n)\n\n\nclass FusionSetToolColor(InventoryAction):\n    \"\"\"Update the color of the selected tools\"\"\"\n\n    label = \"Set Tool Color\"\n    icon = \"plus\"\n    color = \"#d8d8d8\"\n    _fallback_color = QtGui.QColor(1.0, 1.0, 1.0)\n\n    def process(self, containers):\n        \"\"\"Color all selected tools the selected colors\"\"\"\n\n        result = []\n        comp = get_current_comp()\n\n        # Get tool color\n        first = containers[0]\n        tool = first[\"_tool\"]\n        color = tool.TileColor\n\n        if color is not None:\n            qcolor = QtGui.QColor().fromRgbF(color[\"R\"], color[\"G\"], color[\"B\"])\n        else:\n            qcolor = self._fallback_color\n\n        # Launch pick color\n        picked_color = self.get_color_picker(qcolor)\n        if not picked_color:\n            return\n\n        with comp_lock_and_undo_chunk(comp):\n            for container in containers:\n                # Convert color to RGB 0-1 floats\n                rgb_f = picked_color.getRgbF()\n                rgb_f_table = {\"R\": rgb_f[0], \"G\": rgb_f[1], \"B\": rgb_f[2]}\n\n                # Update tool\n                tool = container[\"_tool\"]\n                tool.TileColor = rgb_f_table\n\n                result.append(container)\n\n        return result\n\n    def get_color_picker(self, color):\n        \"\"\"Launch color picker and return chosen color\n\n        Args:\n            color(QtGui.QColor): Start color to display\n\n        Returns:\n            QtGui.QColor\n\n        \"\"\"\n\n        color_dialog = QtWidgets.QColorDialog(color)\n        color_dialog.setStyleSheet(style.load_stylesheet())\n\n        accepted = color_dialog.exec_()\n        if not accepted:\n            return\n\n        return color_dialog.selectedColor()\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/load/actions.py",
    "content": "\"\"\"A module containing generic loader actions that will display in the Loader.\n\n\"\"\"\n\nfrom openpype.pipeline import load\n\n\nclass FusionSetFrameRangeLoader(load.LoaderPlugin):\n    \"\"\"Set frame range excluding pre- and post-handles\"\"\"\n\n    families = [\"animation\",\n                \"camera\",\n                \"imagesequence\",\n                \"render\",\n                \"yeticache\",\n                \"pointcache\",\n                \"render\"]\n    representations = [\"*\"]\n    extensions = {\"*\"}\n\n    label = \"Set frame range\"\n    order = 11\n    icon = \"clock-o\"\n    color = \"white\"\n\n    def load(self, context, name, namespace, data):\n\n        from openpype.hosts.fusion.api import lib\n\n        version = context['version']\n        version_data = version.get(\"data\", {})\n\n        start = version_data.get(\"frameStart\", None)\n        end = version_data.get(\"frameEnd\", None)\n\n        if start is None or end is None:\n            print(\"Skipping setting frame range because start or \"\n                  \"end frame data is missing..\")\n            return\n\n        lib.update_frame_range(start, end)\n\n\nclass FusionSetFrameRangeWithHandlesLoader(load.LoaderPlugin):\n    \"\"\"Set frame range including pre- and post-handles\"\"\"\n\n    families = [\"animation\",\n                \"camera\",\n                \"imagesequence\",\n                \"render\",\n                \"yeticache\",\n                \"pointcache\",\n                \"render\"]\n    representations = [\"*\"]\n\n    label = \"Set frame range (with handles)\"\n    order = 12\n    icon = \"clock-o\"\n    color = \"white\"\n\n    def load(self, context, name, namespace, data):\n\n        from openpype.hosts.fusion.api import lib\n\n        version = context['version']\n        version_data = version.get(\"data\", {})\n\n        start = version_data.get(\"frameStart\", None)\n        end = version_data.get(\"frameEnd\", None)\n\n        if start is None or end is None:\n            print(\"Skipping setting frame range because start or \"\n                  \"end frame data is missing..\")\n            return\n\n        # Include handles\n        start -= version_data.get(\"handleStart\", 0)\n        end += version_data.get(\"handleEnd\", 0)\n\n        lib.update_frame_range(start, end)\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/load/load_alembic.py",
    "content": "from openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.fusion.api import (\n    imprint_container,\n    get_current_comp,\n    comp_lock_and_undo_chunk\n)\n\n\nclass FusionLoadAlembicMesh(load.LoaderPlugin):\n    \"\"\"Load Alembic mesh into Fusion\"\"\"\n\n    families = [\"pointcache\", \"model\"]\n    representations = [\"*\"]\n    extensions = {\"abc\"}\n\n    label = \"Load alembic mesh\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    tool_type = \"SurfaceAlembicMesh\"\n\n    def load(self, context, name, namespace, data):\n        # Fallback to asset name when namespace is None\n        if namespace is None:\n            namespace = context['asset']['name']\n\n        # Create the Loader with the filename path set\n        comp = get_current_comp()\n        with comp_lock_and_undo_chunk(comp, \"Create tool\"):\n\n            path = self.filepath_from_context(context)\n\n            args = (-32768, -32768)\n            tool = comp.AddTool(self.tool_type, *args)\n            tool[\"Filename\"] = path\n\n            imprint_container(tool,\n                              name=name,\n                              namespace=namespace,\n                              context=context,\n                              loader=self.__class__.__name__)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n        \"\"\"Update Alembic path\"\"\"\n\n        tool = container[\"_tool\"]\n        assert tool.ID == self.tool_type, f\"Must be {self.tool_type}\"\n        comp = tool.Comp()\n\n        path = get_representation_path(representation)\n\n        with comp_lock_and_undo_chunk(comp, \"Update tool\"):\n            tool[\"Filename\"] = path\n\n            # Update the imprinted representation\n            tool.SetData(\"avalon.representation\", str(representation[\"_id\"]))\n\n    def remove(self, container):\n        tool = container[\"_tool\"]\n        assert tool.ID == self.tool_type, f\"Must be {self.tool_type}\"\n        comp = tool.Comp()\n\n        with comp_lock_and_undo_chunk(comp, \"Remove tool\"):\n            tool.Delete()\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/load/load_fbx.py",
    "content": "from openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.fusion.api import (\n    imprint_container,\n    get_current_comp,\n    comp_lock_and_undo_chunk,\n)\n\n\nclass FusionLoadFBXMesh(load.LoaderPlugin):\n    \"\"\"Load FBX mesh into Fusion\"\"\"\n\n    families = [\"*\"]\n    representations = [\"*\"]\n    extensions = {\n        \"3ds\",\n        \"amc\",\n        \"aoa\",\n        \"asf\",\n        \"bvh\",\n        \"c3d\",\n        \"dae\",\n        \"dxf\",\n        \"fbx\",\n        \"htr\",\n        \"mcd\",\n        \"obj\",\n        \"trc\",\n    }\n\n    label = \"Load FBX mesh\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    tool_type = \"SurfaceFBXMesh\"\n\n    def load(self, context, name, namespace, data):\n        # Fallback to asset name when namespace is None\n        if namespace is None:\n            namespace = context[\"asset\"][\"name\"]\n\n        # Create the Loader with the filename path set\n        comp = get_current_comp()\n        with comp_lock_and_undo_chunk(comp, \"Create tool\"):\n            path = self.filepath_from_context(context)\n\n            args = (-32768, -32768)\n            tool = comp.AddTool(self.tool_type, *args)\n            tool[\"ImportFile\"] = path\n\n            imprint_container(\n                tool,\n                name=name,\n                namespace=namespace,\n                context=context,\n                loader=self.__class__.__name__,\n            )\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n        \"\"\"Update path\"\"\"\n\n        tool = container[\"_tool\"]\n        assert tool.ID == self.tool_type, f\"Must be {self.tool_type}\"\n        comp = tool.Comp()\n\n        path = get_representation_path(representation)\n\n        with comp_lock_and_undo_chunk(comp, \"Update tool\"):\n            tool[\"ImportFile\"] = path\n\n            # Update the imprinted representation\n            tool.SetData(\"avalon.representation\", str(representation[\"_id\"]))\n\n    def remove(self, container):\n        tool = container[\"_tool\"]\n        assert tool.ID == self.tool_type, f\"Must be {self.tool_type}\"\n        comp = tool.Comp()\n\n        with comp_lock_and_undo_chunk(comp, \"Remove tool\"):\n            tool.Delete()\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/load/load_sequence.py",
    "content": "import contextlib\n\nimport openpype.pipeline.load as load\nfrom openpype.pipeline.load import get_representation_context\nfrom openpype.hosts.fusion.api import (\n    imprint_container,\n    get_current_comp,\n    comp_lock_and_undo_chunk,\n)\nfrom openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS\n\ncomp = get_current_comp()\n\n\n@contextlib.contextmanager\ndef preserve_inputs(tool, inputs):\n    \"\"\"Preserve the tool's inputs after context\"\"\"\n\n    comp = tool.Comp()\n\n    values = {}\n    for name in inputs:\n        tool_input = getattr(tool, name)\n        value = tool_input[comp.TIME_UNDEFINED]\n        values[name] = value\n\n    try:\n        yield\n    finally:\n        for name, value in values.items():\n            tool_input = getattr(tool, name)\n            tool_input[comp.TIME_UNDEFINED] = value\n\n\n@contextlib.contextmanager\ndef preserve_trim(loader, log=None):\n    \"\"\"Preserve the relative trim of the Loader tool.\n\n    This tries to preserve the loader's trim (trim in and trim out) after\n    the context by reapplying the \"amount\" it trims on the clip's length at\n    start and end.\n\n    \"\"\"\n\n    # Get original trim as amount of \"trimming\" from length\n    time = loader.Comp().TIME_UNDEFINED\n    length = loader.GetAttrs()[\"TOOLIT_Clip_Length\"][1] - 1\n    trim_from_start = loader[\"ClipTimeStart\"][time]\n    trim_from_end = length - loader[\"ClipTimeEnd\"][time]\n\n    try:\n        yield\n    finally:\n        length = loader.GetAttrs()[\"TOOLIT_Clip_Length\"][1] - 1\n        if trim_from_start > length:\n            trim_from_start = length\n            if log:\n                log.warning(\n                    \"Reducing trim in to %d \"\n                    \"(because of less frames)\" % trim_from_start\n                )\n\n        remainder = length - trim_from_start\n        if trim_from_end > remainder:\n            trim_from_end = remainder\n            if log:\n                log.warning(\n                    \"Reducing trim in to %d \"\n                    \"(because of less frames)\" % trim_from_end\n                )\n\n        loader[\"ClipTimeStart\"][time] = trim_from_start\n        loader[\"ClipTimeEnd\"][time] = length - trim_from_end\n\n\ndef loader_shift(loader, frame, relative=True):\n    \"\"\"Shift global in time by i preserving duration\n\n    This moves the loader by i frames preserving global duration. When relative\n    is False it will shift the global in to the start frame.\n\n    Args:\n        loader (tool): The fusion loader tool.\n        frame (int): The amount of frames to move.\n        relative (bool): When True the shift is relative, else the shift will\n            change the global in to frame.\n\n    Returns:\n        int: The resulting relative frame change (how much it moved)\n\n    \"\"\"\n    comp = loader.Comp()\n    time = comp.TIME_UNDEFINED\n\n    old_in = loader[\"GlobalIn\"][time]\n    old_out = loader[\"GlobalOut\"][time]\n\n    if relative:\n        shift = frame\n    else:\n        shift = frame - old_in\n\n    if not shift:\n        return 0\n\n    # Shifting global in will try to automatically compensate for the change\n    # in the \"ClipTimeStart\" and \"HoldFirstFrame\" inputs, so we preserve those\n    # input values to \"just shift\" the clip\n    with preserve_inputs(\n        loader,\n        inputs=[\n            \"ClipTimeStart\",\n            \"ClipTimeEnd\",\n            \"HoldFirstFrame\",\n            \"HoldLastFrame\",\n        ],\n    ):\n        # GlobalIn cannot be set past GlobalOut or vice versa\n        # so we must apply them in the order of the shift.\n        if shift > 0:\n            loader[\"GlobalOut\"][time] = old_out + shift\n            loader[\"GlobalIn\"][time] = old_in + shift\n        else:\n            loader[\"GlobalIn\"][time] = old_in + shift\n            loader[\"GlobalOut\"][time] = old_out + shift\n\n    return int(shift)\n\n\nclass FusionLoadSequence(load.LoaderPlugin):\n    \"\"\"Load image sequence into Fusion\"\"\"\n\n    families = [\n        \"imagesequence\",\n        \"review\",\n        \"render\",\n        \"plate\",\n        \"image\",\n        \"onilne\",\n    ]\n    representations = [\"*\"]\n    extensions = set(\n        ext.lstrip(\".\") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)\n    )\n\n    label = \"Load sequence\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data):\n        # Fallback to asset name when namespace is None\n        if namespace is None:\n            namespace = context[\"asset\"][\"name\"]\n\n        # Use the first file for now\n        path = self.filepath_from_context(context)\n\n        # Create the Loader with the filename path set\n        comp = get_current_comp()\n        with comp_lock_and_undo_chunk(comp, \"Create Loader\"):\n            args = (-32768, -32768)\n            tool = comp.AddTool(\"Loader\", *args)\n            tool[\"Clip\"] = comp.ReverseMapPath(path)\n\n            # Set global in point to start frame (if in version.data)\n            start = self._get_start(context[\"version\"], tool)\n            loader_shift(tool, start, relative=False)\n\n            imprint_container(\n                tool,\n                name=name,\n                namespace=namespace,\n                context=context,\n                loader=self.__class__.__name__,\n            )\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n        \"\"\"Update the Loader's path\n\n        Fusion automatically tries to reset some variables when changing\n        the loader's path to a new file. These automatic changes are to its\n        inputs:\n            - ClipTimeStart: Fusion reset to 0 if duration changes\n              - We keep the trim in as close as possible to the previous value.\n                When there are less frames then the amount of trim we reduce\n                it accordingly.\n\n            - ClipTimeEnd: Fusion reset to 0 if duration changes\n              - We keep the trim out as close as possible to the previous value\n                within new amount of frames after trim in (ClipTimeStart) has\n                been set.\n\n            - GlobalIn: Fusion reset to comp's global in if duration changes\n              - We change it to the \"frameStart\"\n\n            - GlobalEnd: Fusion resets to globalIn + length if duration changes\n              - We do the same like Fusion - allow fusion to take control.\n\n            - HoldFirstFrame: Fusion resets this to 0\n              - We preserve the value.\n\n            - HoldLastFrame: Fusion resets this to 0\n              - We preserve the value.\n\n            - Reverse: Fusion resets to disabled if \"Loop\" is not enabled.\n              - We preserve the value.\n\n            - Depth: Fusion resets to \"Format\"\n              - We preserve the value.\n\n            - KeyCode: Fusion resets to \"\"\n              - We preserve the value.\n\n            - TimeCodeOffset: Fusion resets to 0\n              - We preserve the value.\n\n        \"\"\"\n\n        tool = container[\"_tool\"]\n        assert tool.ID == \"Loader\", \"Must be Loader\"\n        comp = tool.Comp()\n\n        context = get_representation_context(representation)\n        path = self.filepath_from_context(context)\n\n        # Get start frame from version data\n        start = self._get_start(context[\"version\"], tool)\n\n        with comp_lock_and_undo_chunk(comp, \"Update Loader\"):\n            # Update the loader's path whilst preserving some values\n            with preserve_trim(tool, log=self.log):\n                with preserve_inputs(\n                    tool,\n                    inputs=(\n                        \"HoldFirstFrame\",\n                        \"HoldLastFrame\",\n                        \"Reverse\",\n                        \"Depth\",\n                        \"KeyCode\",\n                        \"TimeCodeOffset\",\n                    ),\n                ):\n                    tool[\"Clip\"] = comp.ReverseMapPath(path)\n\n            # Set the global in to the start frame of the sequence\n            global_in_changed = loader_shift(tool, start, relative=False)\n            if global_in_changed:\n                # Log this change to the user\n                self.log.debug(\n                    \"Changed '%s' global in: %d\" % (tool.Name, start)\n                )\n\n            # Update the imprinted representation\n            tool.SetData(\"avalon.representation\", str(representation[\"_id\"]))\n\n    def remove(self, container):\n        tool = container[\"_tool\"]\n        assert tool.ID == \"Loader\", \"Must be Loader\"\n        comp = tool.Comp()\n\n        with comp_lock_and_undo_chunk(comp, \"Remove Loader\"):\n            tool.Delete()\n\n    def _get_start(self, version_doc, tool):\n        \"\"\"Return real start frame of published files (incl. handles)\"\"\"\n        data = version_doc[\"data\"]\n\n        # Get start frame directly with handle if it's in data\n        start = data.get(\"frameStartHandle\")\n        if start is not None:\n            return start\n\n        # Get frame start without handles\n        start = data.get(\"frameStart\")\n        if start is None:\n            self.log.warning(\n                \"Missing start frame for version \"\n                \"assuming starts at frame 0 for: \"\n                \"{}\".format(tool.Name)\n            )\n            return 0\n\n        # Use `handleStart` if the data is available\n        handle_start = data.get(\"handleStart\")\n        if handle_start:\n            start -= handle_start\n\n        return start\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/load/load_usd.py",
    "content": "from openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.fusion.api import (\n    imprint_container,\n    get_current_comp,\n    comp_lock_and_undo_chunk\n)\nfrom openpype.hosts.fusion.api.lib import get_fusion_module\n\n\nclass FusionLoadUSD(load.LoaderPlugin):\n    \"\"\"Load USD into Fusion\n\n    Support for USD was added since Fusion 18.5\n    \"\"\"\n\n    families = [\"*\"]\n    representations = [\"*\"]\n    extensions = {\"usd\", \"usda\", \"usdz\"}\n\n    label = \"Load USD\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    tool_type = \"uLoader\"\n\n    @classmethod\n    def apply_settings(cls, project_settings, system_settings):\n        super(FusionLoadUSD, cls).apply_settings(project_settings,\n                                                 system_settings)\n        if cls.enabled:\n            # Enable only in Fusion 18.5+\n            fusion = get_fusion_module()\n            version = fusion.GetVersion()\n            major = version[1]\n            minor = version[2]\n            is_usd_supported = (major, minor) >= (18, 5)\n            cls.enabled = is_usd_supported\n\n    def load(self, context, name, namespace, data):\n        # Fallback to asset name when namespace is None\n        if namespace is None:\n            namespace = context['asset']['name']\n\n        # Create the Loader with the filename path set\n        comp = get_current_comp()\n        with comp_lock_and_undo_chunk(comp, \"Create tool\"):\n\n            path = self.fname\n\n            args = (-32768, -32768)\n            tool = comp.AddTool(self.tool_type, *args)\n            tool[\"Filename\"] = path\n\n            imprint_container(tool,\n                              name=name,\n                              namespace=namespace,\n                              context=context,\n                              loader=self.__class__.__name__)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n\n        tool = container[\"_tool\"]\n        assert tool.ID == self.tool_type, f\"Must be {self.tool_type}\"\n        comp = tool.Comp()\n\n        path = get_representation_path(representation)\n\n        with comp_lock_and_undo_chunk(comp, \"Update tool\"):\n            tool[\"Filename\"] = path\n\n            # Update the imprinted representation\n            tool.SetData(\"avalon.representation\", str(representation[\"_id\"]))\n\n    def remove(self, container):\n        tool = container[\"_tool\"]\n        assert tool.ID == self.tool_type, f\"Must be {self.tool_type}\"\n        comp = tool.Comp()\n\n        with comp_lock_and_undo_chunk(comp, \"Remove tool\"):\n            tool.Delete()\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/load/load_workfile.py",
    "content": "\"\"\"Import workfiles into your current comp.\nAs all imported nodes are free floating and will probably be changed there\nis no update or reload function added for this plugin\n\"\"\"\n\nfrom openpype.pipeline import load\n\nfrom openpype.hosts.fusion.api import (\n    get_current_comp,\n    get_bmd_library,\n)\n\n\nclass FusionLoadWorkfile(load.LoaderPlugin):\n    \"\"\"Load the content of a workfile into Fusion\"\"\"\n\n    families = [\"workfile\"]\n    representations = [\"*\"]\n    extensions = {\"comp\"}\n\n    label = \"Load Workfile\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data):\n        # Get needed elements\n        bmd = get_bmd_library()\n        comp = get_current_comp()\n        path = self.filepath_from_context(context)\n\n        # Paste the content of the file into the current comp\n        comp.Paste(bmd.readfile(path))\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/collect_comp.py",
    "content": "import pyblish.api\n\nfrom openpype.hosts.fusion.api import get_current_comp\n\n\nclass CollectCurrentCompFusion(pyblish.api.ContextPlugin):\n    \"\"\"Collect current comp\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.4\n    label = \"Collect Current Comp\"\n    hosts = [\"fusion\"]\n\n    def process(self, context):\n        \"\"\"Collect all image sequence tools\"\"\"\n\n        current_comp = get_current_comp()\n        assert current_comp, \"Must have active Fusion composition\"\n        context.data[\"currentComp\"] = current_comp\n\n        # Store path to current file\n        filepath = current_comp.GetAttrs().get(\"COMPS_FileName\", \"\")\n        context.data['currentFile'] = filepath\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py",
    "content": "import pyblish.api\n\n\ndef get_comp_render_range(comp):\n    \"\"\"Return comp's start-end render range and global start-end range.\"\"\"\n    comp_attrs = comp.GetAttrs()\n    start = comp_attrs[\"COMPN_RenderStart\"]\n    end = comp_attrs[\"COMPN_RenderEnd\"]\n    global_start = comp_attrs[\"COMPN_GlobalStart\"]\n    global_end = comp_attrs[\"COMPN_GlobalEnd\"]\n\n    # Whenever render ranges are undefined fall back\n    # to the comp's global start and end\n    if start == -1000000000:\n        start = global_start\n    if end == -1000000000:\n        end = global_end\n\n    return start, end, global_start, global_end\n\n\nclass CollectFusionCompFrameRanges(pyblish.api.ContextPlugin):\n    \"\"\"Collect current comp\"\"\"\n\n    # We run this after CollectorOrder - 0.1 otherwise it gets\n    # overridden by global plug-in `CollectContextEntities`\n    order = pyblish.api.CollectorOrder - 0.05\n    label = \"Collect Comp Frame Ranges\"\n    hosts = [\"fusion\"]\n\n    def process(self, context):\n        \"\"\"Collect all image sequence tools\"\"\"\n\n        comp = context.data[\"currentComp\"]\n\n        # Store comp render ranges\n        start, end, global_start, global_end = get_comp_render_range(comp)\n\n        context.data.update({\n            \"renderFrameStart\": int(start),\n            \"renderFrameEnd\": int(end),\n            \"compFrameStart\": int(global_start),\n            \"compFrameEnd\": int(global_end)\n        })\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/collect_inputs.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import registered_host\n\n\ndef collect_input_containers(tools):\n    \"\"\"Collect containers that contain any of the node in `nodes`.\n\n    This will return any loaded Avalon container that contains at least one of\n    the nodes. As such, the Avalon container is an input for it. Or in short,\n    there are member nodes of that container.\n\n    Returns:\n        list: Input avalon containers\n\n    \"\"\"\n\n    # Lookup by node ids\n    lookup = frozenset([tool.Name for tool in tools])\n\n    containers = []\n    host = registered_host()\n    for container in host.ls():\n\n        name = container[\"_tool\"].Name\n\n        # We currently assume no \"groups\" as containers but just single tools\n        # like a single \"Loader\" operator. As such we just check whether the\n        # Loader is part of the processing queue.\n        if name in lookup:\n            containers.append(container)\n\n    return containers\n\n\ndef iter_upstream(tool):\n    \"\"\"Yields all upstream inputs for the current tool.\n\n    Yields:\n        tool: The input tools.\n\n    \"\"\"\n\n    def get_connected_input_tools(tool):\n        \"\"\"Helper function that returns connected input tools for a tool.\"\"\"\n        inputs = []\n\n        # Filter only to actual types that will have sensible upstream\n        # connections. So we ignore just \"Number\" inputs as they can be\n        # many to iterate, slowing things down quite a bit - and in practice\n        # they don't have upstream connections.\n        VALID_INPUT_TYPES = ['Image', 'Particles', 'Mask', 'DataType3D']\n        for type_ in VALID_INPUT_TYPES:\n            for input_ in tool.GetInputList(type_).values():\n                output = input_.GetConnectedOutput()\n                if output:\n                    input_tool = output.GetTool()\n                    inputs.append(input_tool)\n\n        return inputs\n\n    # Initialize process queue with the node's inputs itself\n    queue = get_connected_input_tools(tool)\n\n    # We keep track of which node names we have processed so far, to ensure we\n    # don't process the same hierarchy again. We are not pushing the tool\n    # itself into the set as that doesn't correctly recognize the same tool.\n    # Since tool names are unique in a comp in Fusion we rely on that.\n    collected = set(tool.Name for tool in queue)\n\n    # Traverse upstream references for all nodes and yield them as we\n    # process the queue.\n    while queue:\n        upstream_tool = queue.pop()\n        yield upstream_tool\n\n        # Find upstream tools that are not collected yet.\n        upstream_inputs = get_connected_input_tools(upstream_tool)\n        upstream_inputs = [t for t in upstream_inputs if\n                           t.Name not in collected]\n\n        queue.extend(upstream_inputs)\n        collected.update(tool.Name for tool in upstream_inputs)\n\n\nclass CollectUpstreamInputs(pyblish.api.InstancePlugin):\n    \"\"\"Collect source input containers used for this publish.\n\n    This will include `inputs` data of which loaded publishes were used in the\n    generation of this publish. This leaves an upstream trace to what was used\n    as input.\n\n    \"\"\"\n\n    label = \"Collect Inputs\"\n    order = pyblish.api.CollectorOrder + 0.2\n    hosts = [\"fusion\"]\n    families = [\"render\", \"image\"]\n\n    def process(self, instance):\n\n        # Get all upstream and include itself\n        if not any(instance[:]):\n            self.log.debug(\"No tool found in instance, skipping..\")\n            return\n\n        tool = instance[0]\n        nodes = list(iter_upstream(tool))\n        nodes.append(tool)\n\n        # Collect containers for the given set of nodes\n        containers = collect_input_containers(nodes)\n\n        inputs = [c[\"representation\"] for c in containers]\n        instance.data[\"inputRepresentations\"] = inputs\n        self.log.debug(\"Collected inputs: %s\" % inputs)\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/collect_instances.py",
    "content": "import pyblish.api\n\n\nclass CollectInstanceData(pyblish.api.InstancePlugin):\n    \"\"\"Collect Fusion saver instances\n\n    This additionally stores the Comp start and end render range in the\n    current context's data as \"frameStart\" and \"frameEnd\".\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = \"Collect Instances Data\"\n    hosts = [\"fusion\"]\n\n    def process(self, instance):\n        \"\"\"Collect all image sequence tools\"\"\"\n\n        context = instance.context\n\n        # Include creator attributes directly as instance data\n        creator_attributes = instance.data[\"creator_attributes\"]\n        instance.data.update(creator_attributes)\n\n        frame_range_source = creator_attributes.get(\"frame_range_source\")\n        instance.data[\"frame_range_source\"] = frame_range_source\n\n        # get asset frame ranges to all instances\n        # render family instances `asset_db` render target\n        start = context.data[\"frameStart\"]\n        end = context.data[\"frameEnd\"]\n        handle_start = context.data[\"handleStart\"]\n        handle_end = context.data[\"handleEnd\"]\n        start_with_handle = start - handle_start\n        end_with_handle = end + handle_end\n\n        # conditions for render family instances\n        if frame_range_source == \"render_range\":\n            # set comp render frame ranges\n            start = context.data[\"renderFrameStart\"]\n            end = context.data[\"renderFrameEnd\"]\n            handle_start = 0\n            handle_end = 0\n            start_with_handle = start\n            end_with_handle = end\n\n        if frame_range_source == \"comp_range\":\n            comp_start = context.data[\"compFrameStart\"]\n            comp_end = context.data[\"compFrameEnd\"]\n            render_start = context.data[\"renderFrameStart\"]\n            render_end = context.data[\"renderFrameEnd\"]\n            # set comp frame ranges\n            start = render_start\n            end = render_end\n            handle_start = render_start - comp_start\n            handle_end = comp_end - render_end\n            start_with_handle = comp_start\n            end_with_handle = comp_end\n\n        frame = instance.data[\"creator_attributes\"].get(\"frame\")\n        # explicitly publishing only single frame\n        if frame is not None:\n            frame = int(frame)\n\n            start = frame\n            end = frame\n            handle_start = 0\n            handle_end = 0\n            start_with_handle = frame\n            end_with_handle = frame\n\n        # Include start and end render frame in label\n        subset = instance.data[\"subset\"]\n        label = (\n            \"{subset} ({start}-{end}) [{handle_start}-{handle_end}]\"\n        ).format(\n            subset=subset,\n            start=int(start),\n            end=int(end),\n            handle_start=int(handle_start),\n            handle_end=int(handle_end)\n        )\n\n        instance.data.update({\n            \"label\": label,\n\n            # todo: Allow custom frame range per instance\n            \"frameStart\": start,\n            \"frameEnd\": end,\n            \"frameStartHandle\": start_with_handle,\n            \"frameEndHandle\": end_with_handle,\n            \"handleStart\": handle_start,\n            \"handleEnd\": handle_end,\n            \"fps\": context.data[\"fps\"],\n        })\n\n        # Add review family if the instance is marked as 'review'\n        # This could be done through a 'review' Creator attribute.\n        if instance.data.get(\"review\", False):\n            self.log.debug(\"Adding review family..\")\n            instance.data[\"families\"].append(\"review\")\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/collect_render.py",
    "content": "import os\nimport attr\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.pipeline.publish import RenderInstance\nfrom openpype.hosts.fusion.api.lib import get_frame_path\n\n\n@attr.s\nclass FusionRenderInstance(RenderInstance):\n    # extend generic, composition name is needed\n    fps = attr.ib(default=None)\n    projectEntity = attr.ib(default=None)\n    stagingDir = attr.ib(default=None)\n    app_version = attr.ib(default=None)\n    tool = attr.ib(default=None)\n    workfileComp = attr.ib(default=None)\n    publish_attributes = attr.ib(default={})\n    frameStartHandle = attr.ib(default=None)\n    frameEndHandle = attr.ib(default=None)\n\n\nclass CollectFusionRender(\n    publish.AbstractCollectRender,\n    publish.ColormanagedPyblishPluginMixin\n):\n\n    order = pyblish.api.CollectorOrder + 0.09\n    label = \"Collect Fusion Render\"\n    hosts = [\"fusion\"]\n\n    def get_instances(self, context):\n\n        comp = context.data.get(\"currentComp\")\n        comp_frame_format_prefs = comp.GetPrefs(\"Comp.FrameFormat\")\n        aspect_x = comp_frame_format_prefs[\"AspectX\"]\n        aspect_y = comp_frame_format_prefs[\"AspectY\"]\n\n        instances = []\n        instances_to_remove = []\n\n        current_file = context.data[\"currentFile\"]\n        version = context.data[\"version\"]\n\n        project_entity = context.data[\"projectEntity\"]\n\n        for inst in context:\n            if not inst.data.get(\"active\", True):\n                continue\n\n            family = inst.data[\"family\"]\n            if family not in [\"render\", \"image\"]:\n                continue\n\n            task_name = context.data[\"task\"]\n            tool = inst.data[\"transientData\"][\"tool\"]\n\n            instance_families = inst.data.get(\"families\", [])\n            subset_name = inst.data[\"subset\"]\n            instance = FusionRenderInstance(\n                family=family,\n                tool=tool,\n                workfileComp=comp,\n                families=instance_families,\n                version=version,\n                time=\"\",\n                source=current_file,\n                label=inst.data[\"label\"],\n                subset=subset_name,\n                asset=inst.data[\"asset\"],\n                task=task_name,\n                attachTo=False,\n                setMembers='',\n                publish=True,\n                name=subset_name,\n                resolutionWidth=comp_frame_format_prefs.get(\"Width\"),\n                resolutionHeight=comp_frame_format_prefs.get(\"Height\"),\n                pixelAspect=aspect_x / aspect_y,\n                tileRendering=False,\n                tilesX=0,\n                tilesY=0,\n                review=\"review\" in instance_families,\n                frameStart=inst.data[\"frameStart\"],\n                frameEnd=inst.data[\"frameEnd\"],\n                handleStart=inst.data[\"handleStart\"],\n                handleEnd=inst.data[\"handleEnd\"],\n                frameStartHandle=inst.data[\"frameStartHandle\"],\n                frameEndHandle=inst.data[\"frameEndHandle\"],\n                frameStep=1,\n                fps=comp_frame_format_prefs.get(\"Rate\"),\n                app_version=comp.GetApp().Version,\n                publish_attributes=inst.data.get(\"publish_attributes\", {})\n            )\n\n            render_target = inst.data[\"creator_attributes\"][\"render_target\"]\n\n            # Add render target family\n            render_target_family = f\"render.{render_target}\"\n            if render_target_family not in instance.families:\n                instance.families.append(render_target_family)\n\n            # Add render target specific data\n            if render_target in {\"local\", \"frames\"}:\n                instance.projectEntity = project_entity\n\n            if render_target == \"farm\":\n                fam = \"render.farm\"\n                if fam not in instance.families:\n                    instance.families.append(fam)\n                instance.farm = True  # to skip integrate\n                if \"review\" in instance.families:\n                    # to skip ExtractReview locally\n                    instance.families.remove(\"review\")\n\n            # add new instance to the list and remove the original\n            # instance since it is not needed anymore\n            instances.append(instance)\n            instances_to_remove.append(inst)\n\n        for instance in instances_to_remove:\n            context.remove(instance)\n\n        return instances\n\n    def post_collecting_action(self):\n        for instance in self._context:\n            if \"render.frames\" in instance.data.get(\"families\", []):\n                # adding representation data to the instance\n                self._update_for_frames(instance)\n\n    def get_expected_files(self, render_instance):\n        \"\"\"\n            Returns list of rendered files that should be created by\n            Deadline. These are not published directly, they are source\n            for later 'submit_publish_job'.\n\n        Args:\n            render_instance (RenderInstance): to pull anatomy and parts used\n                in url\n\n        Returns:\n            (list) of absolute urls to rendered file\n        \"\"\"\n        start = render_instance.frameStart - render_instance.handleStart\n        end = render_instance.frameEnd + render_instance.handleEnd\n\n        comp = render_instance.workfileComp\n        path = comp.MapPath(\n            render_instance.tool[\"Clip\"][\n                render_instance.workfileComp.TIME_UNDEFINED\n            ]\n        )\n        output_dir = os.path.dirname(path)\n        render_instance.outputDir = output_dir\n\n        basename = os.path.basename(path)\n\n        head, padding, ext = get_frame_path(basename)\n\n        expected_files = []\n        for frame in range(start, end + 1):\n            expected_files.append(\n                os.path.join(\n                    output_dir,\n                    f\"{head}{str(frame).zfill(padding)}{ext}\"\n                )\n            )\n\n        return expected_files\n\n    def _update_for_frames(self, instance):\n        \"\"\"Updating instance for render.frames family\n\n        Adding representation data to the instance. Also setting\n        colorspaceData to the representation based on file rules.\n        \"\"\"\n\n        expected_files = instance.data[\"expectedFiles\"]\n\n        start = instance.data[\"frameStart\"] - instance.data[\"handleStart\"]\n\n        path = expected_files[0]\n        basename = os.path.basename(path)\n        staging_dir = os.path.dirname(path)\n        _, padding, ext = get_frame_path(basename)\n\n        repre = {\n            \"name\": ext[1:],\n            \"ext\": ext[1:],\n            \"frameStart\": f\"%0{padding}d\" % start,\n            \"files\": [os.path.basename(f) for f in expected_files],\n            \"stagingDir\": staging_dir,\n        }\n\n        self.set_representation_colorspace(\n            representation=repre,\n            context=instance.context,\n        )\n\n        # review representation\n        if instance.data.get(\"review\", False):\n            repre[\"tags\"] = [\"review\"]\n\n        # add the repre to the instance\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n        instance.data[\"representations\"].append(repre)\n\n        return instance\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/collect_workfile.py",
    "content": "import os\n\nimport pyblish.api\n\n\nclass CollectFusionWorkfile(pyblish.api.InstancePlugin):\n    \"\"\"Collect Fusion workfile representation.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.1\n    label = \"Collect Workfile\"\n    hosts = [\"fusion\"]\n    families = [\"workfile\"]\n\n    def process(self, instance):\n\n        current_file = instance.context.data[\"currentFile\"]\n\n        folder, file = os.path.split(current_file)\n        filename, ext = os.path.splitext(file)\n\n        instance.data['representations'] = [{\n            'name': ext.lstrip(\".\"),\n            'ext': ext.lstrip(\".\"),\n            'files': file,\n            \"stagingDir\": folder,\n        }]\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/extract_render_local.py",
    "content": "import os\nimport logging\nimport contextlib\nimport collections\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.fusion.api import comp_lock_and_undo_chunk\nfrom openpype.hosts.fusion.api.lib import get_frame_path, maintained_comp_range\n\nlog = logging.getLogger(__name__)\n\n\n@contextlib.contextmanager\ndef enabled_savers(comp, savers):\n    \"\"\"Enable only the `savers` in Comp during the context.\n\n    Any Saver tool in the passed composition that is not in the savers list\n    will be set to passthrough during the context.\n\n    Args:\n        comp (object): Fusion composition object.\n        savers (list): List of Saver tool objects.\n\n    \"\"\"\n    passthrough_key = \"TOOLB_PassThrough\"\n    original_states = {}\n    enabled_saver_names = {saver.Name for saver in savers}\n\n    all_savers = comp.GetToolList(False, \"Saver\").values()\n    savers_by_name = {saver.Name: saver for saver in all_savers}\n\n    try:\n        for saver in all_savers:\n            original_state = saver.GetAttrs()[passthrough_key]\n            original_states[saver.Name] = original_state\n\n            # The passthrough state we want to set (passthrough != enabled)\n            state = saver.Name not in enabled_saver_names\n            if state != original_state:\n                saver.SetAttrs({passthrough_key: state})\n        yield\n    finally:\n        for saver_name, original_state in original_states.items():\n            saver = savers_by_name[saver_name]\n            saver.SetAttrs({\"TOOLB_PassThrough\": original_state})\n\n\nclass FusionRenderLocal(\n    pyblish.api.InstancePlugin,\n    publish.ColormanagedPyblishPluginMixin\n):\n    \"\"\"Render the current Fusion composition locally.\"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.2\n    label = \"Render Local\"\n    hosts = [\"fusion\"]\n    families = [\"render.local\"]\n\n    is_rendered_key = \"_fusionrenderlocal_has_rendered\"\n\n    def process(self, instance):\n\n        # Start render\n        result = self.render(instance)\n        if result is False:\n            raise RuntimeError(f\"Comp render failed for {instance}\")\n\n        self._add_representation(instance)\n\n        # Log render status\n        self.log.info(\n            \"Rendered '{nm}' for asset '{ast}' under the task '{tsk}'\".format(\n                nm=instance.data[\"name\"],\n                ast=instance.data[\"asset\"],\n                tsk=instance.data[\"task\"],\n            )\n        )\n\n    def render(self, instance):\n        \"\"\"Render instance.\n\n        We try to render the minimal amount of times by combining the instances\n        that have a matching frame range in one Fusion render. Then for the\n        batch of instances we store whether the render succeeded or failed.\n\n        \"\"\"\n\n        if self.is_rendered_key in instance.data:\n            # This instance was already processed in batch with another\n            # instance, so we just return the render result directly\n            self.log.debug(f\"Instance {instance} was already rendered\")\n            return instance.data[self.is_rendered_key]\n\n        instances_by_frame_range = self.get_render_instances_by_frame_range(\n            instance.context\n        )\n\n        # Render matching batch of instances that share the same frame range\n        frame_range = self.get_instance_render_frame_range(instance)\n        render_instances = instances_by_frame_range[frame_range]\n\n        # We initialize render state false to indicate it wasn't successful\n        # yet to keep track of whether Fusion succeeded. This is for cases\n        # where an error below this might cause the comp render result not\n        # to be stored for the instances of this batch\n        for render_instance in render_instances:\n            render_instance.data[self.is_rendered_key] = False\n\n        savers_to_render = [inst.data[\"tool\"] for inst in render_instances]\n        current_comp = instance.context.data[\"currentComp\"]\n        frame_start, frame_end = frame_range\n\n        self.log.info(\n            f\"Starting Fusion render frame range {frame_start}-{frame_end}\"\n        )\n        saver_names = \", \".join(saver.Name for saver in savers_to_render)\n        self.log.info(f\"Rendering tools: {saver_names}\")\n\n        with comp_lock_and_undo_chunk(current_comp):\n            with maintained_comp_range(current_comp):\n                with enabled_savers(current_comp, savers_to_render):\n                    result = current_comp.Render(\n                        {\n                            \"Start\": frame_start,\n                            \"End\": frame_end,\n                            \"Wait\": True,\n                        }\n                    )\n\n        # Store the render state for all the rendered instances\n        for render_instance in render_instances:\n            render_instance.data[self.is_rendered_key] = bool(result)\n\n        return result\n\n    def _add_representation(self, instance):\n        \"\"\"Add representation to instance\"\"\"\n\n        expected_files = instance.data[\"expectedFiles\"]\n\n        start = instance.data[\"frameStart\"] - instance.data[\"handleStart\"]\n\n        path = expected_files[0]\n        _, padding, ext = get_frame_path(path)\n\n        staging_dir = os.path.dirname(path)\n\n        files = [os.path.basename(f) for f in expected_files]\n        if len(expected_files) == 1:\n            files = files[0]\n\n        repre = {\n            \"name\": ext[1:],\n            \"ext\": ext[1:],\n            \"frameStart\": f\"%0{padding}d\" % start,\n            \"files\": files,\n            \"stagingDir\": staging_dir,\n        }\n\n        self.set_representation_colorspace(\n            representation=repre,\n            context=instance.context,\n        )\n\n        # review representation\n        if instance.data.get(\"review\", False):\n            repre[\"tags\"] = [\"review\"]\n\n        # add the repre to the instance\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n        instance.data[\"representations\"].append(repre)\n\n        return instance\n\n    def get_render_instances_by_frame_range(self, context):\n        \"\"\"Return enabled render.local instances grouped by their frame range.\n\n        Arguments:\n            context (pyblish.Context): The pyblish context\n\n        Returns:\n            dict: (start, end): instances mapping\n\n        \"\"\"\n\n        instances_to_render = [\n            instance for instance in context if\n            # Only active instances\n            instance.data.get(\"publish\", True) and\n            # Only render.local instances\n            \"render.local\" in instance.data.get(\"families\", [])\n        ]\n\n        # Instances by frame ranges\n        instances_by_frame_range = collections.defaultdict(list)\n        for instance in instances_to_render:\n            start, end = self.get_instance_render_frame_range(instance)\n            instances_by_frame_range[(start, end)].append(instance)\n\n        return dict(instances_by_frame_range)\n\n    def get_instance_render_frame_range(self, instance):\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n        return start, end\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/increment_current_file.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import OptionalPyblishPluginMixin\nfrom openpype.pipeline import KnownPublishError\n\n\nclass FusionIncrementCurrentFile(\n    pyblish.api.ContextPlugin, OptionalPyblishPluginMixin\n):\n    \"\"\"Increment the current file.\n\n    Saves the current file with an increased version number.\n\n    \"\"\"\n\n    label = \"Increment workfile version\"\n    order = pyblish.api.IntegratorOrder + 9.0\n    hosts = [\"fusion\"]\n    optional = True\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            return\n\n        from openpype.lib import version_up\n        from openpype.pipeline.publish import get_errored_plugins_from_context\n\n        errored_plugins = get_errored_plugins_from_context(context)\n        if any(\n            plugin.__name__ == \"FusionSubmitDeadline\"\n            for plugin in errored_plugins\n        ):\n            raise KnownPublishError(\n                \"Skipping incrementing current file because \"\n                \"submission to render farm failed.\"\n            )\n\n        comp = context.data.get(\"currentComp\")\n        assert comp, \"Must have comp\"\n\n        current_filepath = context.data[\"currentFile\"]\n        new_filepath = version_up(current_filepath)\n\n        comp.Save(new_filepath)\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/save_scene.py",
    "content": "import pyblish.api\n\n\nclass FusionSaveComp(pyblish.api.ContextPlugin):\n    \"\"\"Save current comp\"\"\"\n\n    label = \"Save current file\"\n    order = pyblish.api.ExtractorOrder - 0.49\n    hosts = [\"fusion\"]\n    families = [\"render\", \"image\", \"workfile\"]\n\n    def process(self, context):\n\n        comp = context.data.get(\"currentComp\")\n        assert comp, \"Must have comp\"\n\n        current = comp.GetAttrs().get(\"COMPS_FileName\", \"\")\n        assert context.data['currentFile'] == current\n\n        self.log.info(\"Saving current file: {}\".format(current))\n        comp.Save()\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/validate_background_depth.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import (\n    publish,\n    OptionalPyblishPluginMixin,\n    PublishValidationError,\n)\n\nfrom openpype.hosts.fusion.api.action import SelectInvalidAction\n\n\nclass ValidateBackgroundDepth(\n    pyblish.api.InstancePlugin, OptionalPyblishPluginMixin\n):\n    \"\"\"Validate if all Background tool are set to float32 bit\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Background Depth 32 bit\"\n    hosts = [\"fusion\"]\n    families = [\"render\", \"image\"]\n    optional = True\n\n    actions = [SelectInvalidAction, publish.RepairAction]\n\n    @classmethod\n    def get_invalid(cls, instance):\n        context = instance.context\n        comp = context.data.get(\"currentComp\")\n        assert comp, \"Must have Comp object\"\n\n        backgrounds = comp.GetToolList(False, \"Background\").values()\n        if not backgrounds:\n            return []\n\n        return [i for i in backgrounds if i.GetInput(\"Depth\") != 4.0]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Found {} Backgrounds tools which\"\n                \" are not set to float32\".format(len(invalid)),\n                title=self.label,\n            )\n\n    @classmethod\n    def repair(cls, instance):\n        comp = instance.context.data.get(\"currentComp\")\n        invalid = cls.get_invalid(instance)\n        for i in invalid:\n            i.SetInput(\"Depth\", 4.0, comp.TIME_UNDEFINED)\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/validate_comp_saved.py",
    "content": "import os\n\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateFusionCompSaved(pyblish.api.ContextPlugin):\n    \"\"\"Ensure current comp is saved\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Comp Saved\"\n    families = [\"render\", \"image\"]\n    hosts = [\"fusion\"]\n\n    def process(self, context):\n\n        comp = context.data.get(\"currentComp\")\n        assert comp, \"Must have Comp object\"\n        attrs = comp.GetAttrs()\n\n        filename = attrs[\"COMPS_FileName\"]\n        if not filename:\n            raise PublishValidationError(\"Comp is not saved.\",\n                                         title=self.label)\n\n        if not os.path.exists(filename):\n            raise PublishValidationError(\n                \"Comp file does not exist: %s\" % filename, title=self.label)\n\n        if attrs[\"COMPB_Modified\"]:\n            self.log.warning(\"Comp is modified. Save your comp to ensure your \"\n                             \"changes propagate correctly.\")\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import RepairAction\nfrom openpype.pipeline import PublishValidationError\n\nfrom openpype.hosts.fusion.api.action import SelectInvalidAction\n\n\nclass ValidateCreateFolderChecked(pyblish.api.InstancePlugin):\n    \"\"\"Valid if all savers have the input attribute CreateDir checked on\n\n    This attribute ensures that the folders to which the saver will write\n    will be created.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Create Folder Checked\"\n    families = [\"render\", \"image\"]\n    hosts = [\"fusion\"]\n    actions = [RepairAction, SelectInvalidAction]\n\n    @classmethod\n    def get_invalid(cls, instance):\n        tool = instance.data[\"tool\"]\n        create_dir = tool.GetInput(\"CreateDir\")\n        if create_dir == 0.0:\n            cls.log.error(\n                \"%s has Create Folder turned off\" % instance[0].Name\n            )\n            return [tool]\n\n    def process(self, instance):\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Found Saver with Create Folder During Render checked off\",\n                title=self.label,\n            )\n\n    @classmethod\n    def repair(cls, instance):\n        invalid = cls.get_invalid(instance)\n        for tool in invalid:\n            tool.SetInput(\"CreateDir\", 1.0)\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py",
    "content": "import os\nimport pyblish.api\n\nfrom openpype.pipeline.publish import RepairAction\nfrom openpype.pipeline import PublishValidationError\n\nfrom openpype.hosts.fusion.api.action import SelectInvalidAction\n\n\nclass ValidateLocalFramesExistence(pyblish.api.InstancePlugin):\n    \"\"\"Checks if files for savers that's set\n    to publish expected frames exists\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Expected Frames Exists\"\n    families = [\"render.frames\"]\n    hosts = [\"fusion\"]\n    actions = [RepairAction, SelectInvalidAction]\n\n    @classmethod\n    def get_invalid(cls, instance, non_existing_frames=None):\n        if non_existing_frames is None:\n            non_existing_frames = []\n\n        tool = instance.data[\"tool\"]\n\n        expected_files = instance.data[\"expectedFiles\"]\n\n        for file in expected_files:\n            if not os.path.exists(file):\n                cls.log.error(\n                    f\"Missing file: {file}\"\n                )\n                non_existing_frames.append(file)\n\n        if len(non_existing_frames) > 0:\n            cls.log.error(f\"Some of {tool.Name}'s files does not exist\")\n            return [tool]\n\n    def process(self, instance):\n        non_existing_frames = []\n        invalid = self.get_invalid(instance, non_existing_frames)\n        if invalid:\n            raise PublishValidationError(\n                \"{} is set to publish existing frames but \"\n                \"some frames are missing. \"\n                \"The missing file(s) are:\\n\\n{}\".format(\n                    invalid[0].Name,\n                    \"\\n\\n\".join(non_existing_frames),\n                ),\n                title=self.label,\n            )\n\n    @classmethod\n    def repair(cls, instance):\n        invalid = cls.get_invalid(instance)\n        if invalid:\n            tool = instance.data[\"tool\"]\n            # Change render target to local to render locally\n            tool.SetData(\"openpype.creator_attributes.render_target\", \"local\")\n\n            cls.log.info(\n                f\"Reload the publisher and {tool.Name} \"\n                \"will be set to render locally\"\n            )\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py",
    "content": "import os\n\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\nfrom openpype.hosts.fusion.api.action import SelectInvalidAction\n\n\nclass ValidateFilenameHasExtension(pyblish.api.InstancePlugin):\n    \"\"\"Ensure the Saver has an extension in the filename path\n\n    This disallows files written as `filename` instead of `filename.frame.ext`.\n    Fusion does not always set an extension for your filename when\n    changing the file format of the saver.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Filename Has Extension\"\n    families = [\"render\", \"image\"]\n    hosts = [\"fusion\"]\n    actions = [SelectInvalidAction]\n\n    def process(self, instance):\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\"Found Saver without an extension\",\n                                         title=self.label)\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        path = instance.data[\"expectedFiles\"][0]\n        fname, ext = os.path.splitext(path)\n\n        if not ext:\n            tool = instance.data[\"tool\"]\n            cls.log.error(\"%s has no extension specified\" % tool.Name)\n            return [tool]\n\n        return []\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/validate_image_frame.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateImageFrame(pyblish.api.InstancePlugin):\n    \"\"\"Validates that `image` product type contains only single frame.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Image Frame\"\n    families = [\"image\"]\n    hosts = [\"fusion\"]\n\n    def process(self, instance):\n        render_start = instance.data[\"frameStartHandle\"]\n        render_end = instance.data[\"frameEndHandle\"]\n        too_many_frames = (isinstance(instance.data[\"expectedFiles\"], list)\n                           and len(instance.data[\"expectedFiles\"]) > 1)\n\n        if render_end - render_start > 0 or too_many_frames:\n            desc = (\"Trying to render multiple frames. 'image' product type \"\n                    \"is meant for single frame. Please use 'render' creator.\")\n            raise PublishValidationError(\n                title=\"Frame range outside of comp range\",\n                message=desc,\n                description=desc\n            )\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateInstanceFrameRange(pyblish.api.InstancePlugin):\n    \"\"\"Validate instance frame range is within comp's global render range.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Frame Range\"\n    families = [\"render\", \"image\"]\n    hosts = [\"fusion\"]\n\n    def process(self, instance):\n\n        context = instance.context\n        global_start = context.data[\"compFrameStart\"]\n        global_end = context.data[\"compFrameEnd\"]\n\n        render_start = instance.data[\"frameStartHandle\"]\n        render_end = instance.data[\"frameEndHandle\"]\n\n        if render_start < global_start or render_end > global_end:\n\n            message = (\n                f\"Instance {instance} render frame range \"\n                f\"({render_start}-{render_end}) is outside of the comp's \"\n                f\"global render range ({global_start}-{global_end}) and thus \"\n                f\"can't be rendered. \"\n            )\n            description = (\n                f\"{message}\\n\\n\"\n                f\"Either update the comp's global range or the instance's \"\n                f\"frame range to ensure the comp's frame range includes the \"\n                f\"to render frame range for the instance.\"\n            )\n            raise PublishValidationError(\n                title=\"Frame range outside of comp range\",\n                message=message,\n                description=description\n            )\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\nfrom openpype.hosts.fusion.api.action import SelectInvalidAction\n\n\nclass ValidateSaverHasInput(pyblish.api.InstancePlugin):\n    \"\"\"Validate saver has incoming connection\n\n    This ensures a Saver has at least an input connection.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Saver Has Input\"\n    families = [\"render\", \"image\"]\n    hosts = [\"fusion\"]\n    actions = [SelectInvalidAction]\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        saver = instance.data[\"tool\"]\n        if not saver.Input.GetConnectedOutput():\n            return [saver]\n\n        return []\n\n    def process(self, instance):\n        invalid = self.get_invalid(instance)\n        if invalid:\n            saver_name = invalid[0].Name\n            raise PublishValidationError(\n                \"Saver has no incoming connection: {} ({})\".format(instance,\n                                                                   saver_name),\n                title=self.label)\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\nfrom openpype.hosts.fusion.api.action import SelectInvalidAction\n\n\nclass ValidateSaverPassthrough(pyblish.api.ContextPlugin):\n    \"\"\"Validate saver passthrough is similar to Pyblish publish state\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Saver Passthrough\"\n    families = [\"render\", \"image\"]\n    hosts = [\"fusion\"]\n    actions = [SelectInvalidAction]\n\n    def process(self, context):\n\n        # Workaround for ContextPlugin always running, even if no instance\n        # is present with the family\n        instances = pyblish.api.instances_by_plugin(instances=list(context),\n                                                    plugin=self)\n        if not instances:\n            self.log.debug(\"Ignoring plugin.. (bugfix)\")\n\n        invalid_instances = []\n        for instance in instances:\n            invalid = self.is_invalid(instance)\n            if invalid:\n                invalid_instances.append(instance)\n\n        if invalid_instances:\n            self.log.info(\"Reset pyblish to collect your current scene state, \"\n                          \"that should fix error.\")\n            raise PublishValidationError(\n                \"Invalid instances: {0}\".format(invalid_instances),\n                title=self.label)\n\n    def is_invalid(self, instance):\n\n        saver = instance.data[\"tool\"]\n        attr = saver.GetAttrs()\n        active = not attr[\"TOOLB_PassThrough\"]\n\n        if active != instance.data.get(\"publish\", True):\n            self.log.info(\"Saver has different passthrough state than \"\n                          \"Pyblish: {} ({})\".format(instance, saver.Name))\n            return [saver]\n\n        return []\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin,\n)\n\nfrom openpype.hosts.fusion.api.action import SelectInvalidAction\nfrom openpype.hosts.fusion.api import comp_lock_and_undo_chunk\n\n\nclass ValidateSaverResolution(\n    pyblish.api.InstancePlugin, OptionalPyblishPluginMixin\n):\n    \"\"\"Validate that the saver input resolution matches the asset resolution\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Asset Resolution\"\n    families = [\"render\", \"image\"]\n    hosts = [\"fusion\"]\n    optional = True\n    actions = [SelectInvalidAction]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        resolution = self.get_resolution(instance)\n        expected_resolution = self.get_expected_resolution(instance)\n        if resolution != expected_resolution:\n            raise PublishValidationError(\n                \"The input's resolution does not match \"\n                \"the asset's resolution {}x{}.\\n\\n\"\n                \"The input's resolution is {}x{}.\".format(\n                    expected_resolution[0], expected_resolution[1],\n                    resolution[0], resolution[1]\n                )\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n        saver = instance.data[\"tool\"]\n        try:\n            resolution = cls.get_resolution(instance)\n        except PublishValidationError:\n            resolution = None\n        expected_resolution = cls.get_expected_resolution(instance)\n        if resolution != expected_resolution:\n            return [saver]\n\n    @classmethod\n    def get_resolution(cls, instance):\n        saver = instance.data[\"tool\"]\n        first_frame = instance.data[\"frameStartHandle\"]\n        return cls.get_tool_resolution(saver, frame=first_frame)\n\n    @classmethod\n    def get_expected_resolution(cls, instance):\n        data = instance.data[\"assetEntity\"][\"data\"]\n        return data[\"resolutionWidth\"], data[\"resolutionHeight\"]\n\n    @classmethod\n    def get_tool_resolution(cls, tool, frame):\n        \"\"\"Return the 2D input resolution to a Fusion tool\n\n        If the current tool hasn't been rendered its input resolution\n        hasn't been saved. To combat this, add an expression in\n        the comments field to read the resolution\n\n        Args\n            tool (Fusion Tool): The tool to query input resolution\n            frame (int): The frame to query the resolution on.\n\n        Returns:\n            tuple: width, height as 2-tuple of integers\n\n        \"\"\"\n        comp = tool.Composition\n\n        # False undo removes the undo-stack from the undo list\n        with comp_lock_and_undo_chunk(comp, \"Read resolution\", False):\n            # Save old comment\n            old_comment = \"\"\n            has_expression = False\n\n            if tool[\"Comments\"][frame] not in [\"\", None]:\n                if tool[\"Comments\"].GetExpression() is not None:\n                    has_expression = True\n                    old_comment = tool[\"Comments\"].GetExpression()\n                    tool[\"Comments\"].SetExpression(None)\n                else:\n                    old_comment = tool[\"Comments\"][frame]\n                    tool[\"Comments\"][frame] = \"\"\n            # Get input width\n            tool[\"Comments\"].SetExpression(\"self.Input.OriginalWidth\")\n            if tool[\"Comments\"][frame] is None:\n                raise PublishValidationError(\n                    \"Cannot get resolution info for frame '{}'.\\n\\n \"\n                    \"Please check that saver has connected input.\".format(\n                        frame\n                    )\n                )\n\n            width = int(tool[\"Comments\"][frame])\n\n            # Get input height\n            tool[\"Comments\"].SetExpression(\"self.Input.OriginalHeight\")\n            height = int(tool[\"Comments\"][frame])\n\n            # Reset old comment\n            tool[\"Comments\"].SetExpression(None)\n            if has_expression:\n                tool[\"Comments\"].SetExpression(old_comment)\n            else:\n                tool[\"Comments\"][frame] = old_comment\n\n            return width, height\n"
  },
  {
    "path": "openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py",
    "content": "from collections import defaultdict\n\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\nfrom openpype.hosts.fusion.api.action import SelectInvalidAction\n\n\nclass ValidateUniqueSubsets(pyblish.api.ContextPlugin):\n    \"\"\"Ensure all instances have a unique subset name\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Unique Subsets\"\n    families = [\"render\", \"image\"]\n    hosts = [\"fusion\"]\n    actions = [SelectInvalidAction]\n\n    @classmethod\n    def get_invalid(cls, context):\n\n        # Collect instances per subset per asset\n        instances_per_subset_asset = defaultdict(lambda: defaultdict(list))\n        for instance in context:\n            asset = instance.data.get(\"asset\", context.data.get(\"asset\"))\n            subset = instance.data.get(\"subset\", context.data.get(\"subset\"))\n            instances_per_subset_asset[asset][subset].append(instance)\n\n        # Find which asset + subset combination has more than one instance\n        # Those are considered invalid because they'd integrate to the same\n        # destination.\n        invalid = []\n        for asset, instances_per_subset in instances_per_subset_asset.items():\n            for subset, instances in instances_per_subset.items():\n                if len(instances) > 1:\n                    cls.log.warning(\n                        \"{asset} > {subset} used by more than \"\n                        \"one instance: {instances}\".format(\n                            asset=asset,\n                            subset=subset,\n                            instances=instances\n                        )\n                    )\n                    invalid.extend(instances)\n\n        # Return tools for the invalid instances so they can be selected\n        invalid = [instance.data[\"tool\"] for instance in invalid]\n\n        return invalid\n\n    def process(self, context):\n        invalid = self.get_invalid(context)\n        if invalid:\n            raise PublishValidationError(\"Multiple instances are set to \"\n                                         \"the same asset > subset.\",\n                                         title=self.label)\n"
  },
  {
    "path": "openpype/hosts/fusion/scripts/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/fusion/scripts/duplicate_with_inputs.py",
    "content": "from openpype.hosts.fusion.api import (\n    comp_lock_and_undo_chunk,\n    get_current_comp\n)\n\n\ndef is_connected(input):\n    \"\"\"Return whether an input has incoming connection\"\"\"\n    return input.GetAttrs()[\"INPB_Connected\"]\n\n\ndef duplicate_with_input_connections():\n    \"\"\"Duplicate selected tools with incoming connections.\"\"\"\n\n    comp = get_current_comp()\n    original_tools = comp.GetToolList(True).values()\n    if not original_tools:\n        return  # nothing selected\n\n    with comp_lock_and_undo_chunk(\n            comp, \"Duplicate With Input Connections\"):\n\n        # Generate duplicates\n        comp.Copy()\n        comp.SetActiveTool()\n        comp.Paste()\n        duplicate_tools = comp.GetToolList(True).values()\n\n        # Copy connections\n        for original, new in zip(original_tools, duplicate_tools):\n\n            original_inputs = original.GetInputList().values()\n            new_inputs = new.GetInputList().values()\n            assert len(original_inputs) == len(new_inputs)\n\n            for original_input, new_input in zip(original_inputs, new_inputs):\n\n                if is_connected(original_input):\n\n                    if is_connected(new_input):\n                        # Already connected if it is between the copied tools\n                        continue\n\n                    new_input.ConnectTo(original_input.GetConnectedOutput())\n                    assert is_connected(new_input), \"Must be connected now\"\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/__init__.py",
    "content": "from __future__ import absolute_import, division, print_function\n\nimport sys\n\nfrom functools import partial\n\nfrom . import converters, exceptions, filters, setters, validators\nfrom ._cmp import cmp_using\nfrom ._config import get_run_validators, set_run_validators\nfrom ._funcs import asdict, assoc, astuple, evolve, has, resolve_types\nfrom ._make import (\n    NOTHING,\n    Attribute,\n    Factory,\n    attrib,\n    attrs,\n    fields,\n    fields_dict,\n    make_class,\n    validate,\n)\nfrom ._version_info import VersionInfo\n\n\n__version__ = \"21.2.0\"\n__version_info__ = VersionInfo._from_version_string(__version__)\n\n__title__ = \"attrs\"\n__description__ = \"Classes Without Boilerplate\"\n__url__ = \"https://www.attrs.org/\"\n__uri__ = __url__\n__doc__ = __description__ + \" <\" + __uri__ + \">\"\n\n__author__ = \"Hynek Schlawack\"\n__email__ = \"hs@ox.cx\"\n\n__license__ = \"MIT\"\n__copyright__ = \"Copyright (c) 2015 Hynek Schlawack\"\n\n\ns = attributes = attrs\nib = attr = attrib\ndataclass = partial(attrs, auto_attribs=True)  # happy Easter ;)\n\n__all__ = [\n    \"Attribute\",\n    \"Factory\",\n    \"NOTHING\",\n    \"asdict\",\n    \"assoc\",\n    \"astuple\",\n    \"attr\",\n    \"attrib\",\n    \"attributes\",\n    \"attrs\",\n    \"cmp_using\",\n    \"converters\",\n    \"evolve\",\n    \"exceptions\",\n    \"fields\",\n    \"fields_dict\",\n    \"filters\",\n    \"get_run_validators\",\n    \"has\",\n    \"ib\",\n    \"make_class\",\n    \"resolve_types\",\n    \"s\",\n    \"set_run_validators\",\n    \"setters\",\n    \"validate\",\n    \"validators\",\n]\n\nif sys.version_info[:2] >= (3, 6):\n    from ._next_gen import define, field, frozen, mutable\n\n    __all__.extend((define, field, frozen, mutable))\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/__init__.pyi",
    "content": "import sys\n\nfrom typing import (\n    Any,\n    Callable,\n    Dict,\n    Generic,\n    List,\n    Mapping,\n    Optional,\n    Sequence,\n    Tuple,\n    Type,\n    TypeVar,\n    Union,\n    overload,\n)\n\n# `import X as X` is required to make these public\nfrom . import converters as converters\nfrom . import exceptions as exceptions\nfrom . import filters as filters\nfrom . import setters as setters\nfrom . import validators as validators\nfrom ._version_info import VersionInfo\n\n\n__version__: str\n__version_info__: VersionInfo\n__title__: str\n__description__: str\n__url__: str\n__uri__: str\n__author__: str\n__email__: str\n__license__: str\n__copyright__: str\n\n_T = TypeVar(\"_T\")\n_C = TypeVar(\"_C\", bound=type)\n\n_EqOrderType = Union[bool, Callable[[Any], Any]]\n_ValidatorType = Callable[[Any, Attribute[_T], _T], Any]\n_ConverterType = Callable[[Any], Any]\n_FilterType = Callable[[Attribute[_T], _T], bool]\n_ReprType = Callable[[Any], str]\n_ReprArgType = Union[bool, _ReprType]\n_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any]\n_OnSetAttrArgType = Union[\n    _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType\n]\n_FieldTransformer = Callable[[type, List[Attribute[Any]]], List[Attribute[Any]]]\n# FIXME: in reality, if multiple validators are passed they must be in a list\n# or tuple, but those are invariant and so would prevent subtypes of\n# _ValidatorType from working when passed in a list or tuple.\n_ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]]\n\n# _make --\n\nNOTHING: object\n\n# NOTE: Factory lies about its return type to make this possible:\n# `x: List[int] # = Factory(list)`\n# Work around mypy issue #4554 in the common case by using an overload.\nif sys.version_info >= (3, 8):\n    from typing import Literal\n\n    @overload\n    def Factory(factory: Callable[[], _T]) -> _T: ...\n    @overload\n    def Factory(\n        factory: Callable[[Any], _T],\n        takes_self: Literal[True],\n    ) -> _T: ...\n    @overload\n    def Factory(\n        factory: Callable[[], _T],\n        takes_self: Literal[False],\n    ) -> _T: ...\nelse:\n    @overload\n    def Factory(factory: Callable[[], _T]) -> _T: ...\n    @overload\n    def Factory(\n        factory: Union[Callable[[Any], _T], Callable[[], _T]],\n        takes_self: bool = ...,\n    ) -> _T: ...\n\n# Static type inference support via __dataclass_transform__ implemented as per:\n# https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md\n# This annotation must be applied to all overloads of \"define\" and \"attrs\"\n#\n# NOTE: This is a typing construct and does not exist at runtime.  Extensions\n# wrapping attrs decorators should declare a separate __dataclass_transform__\n# signature in the extension module using the specification linked above to\n# provide pyright support.\ndef __dataclass_transform__(\n    *,\n    eq_default: bool = True,\n    order_default: bool = False,\n    kw_only_default: bool = False,\n    field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()),\n) -> Callable[[_T], _T]: ...\n\nclass Attribute(Generic[_T]):\n    name: str\n    default: Optional[_T]\n    validator: Optional[_ValidatorType[_T]]\n    repr: _ReprArgType\n    cmp: _EqOrderType\n    eq: _EqOrderType\n    order: _EqOrderType\n    hash: Optional[bool]\n    init: bool\n    converter: Optional[_ConverterType]\n    metadata: Dict[Any, Any]\n    type: Optional[Type[_T]]\n    kw_only: bool\n    on_setattr: _OnSetAttrType\n\n    def evolve(self, **changes: Any) -> \"Attribute[Any]\": ...\n\n# NOTE: We had several choices for the annotation to use for type arg:\n# 1) Type[_T]\n#   - Pros: Handles simple cases correctly\n#   - Cons: Might produce less informative errors in the case of conflicting\n#     TypeVars e.g. `attr.ib(default='bad', type=int)`\n# 2) Callable[..., _T]\n#   - Pros: Better error messages than #1 for conflicting TypeVars\n#   - Cons: Terrible error messages for validator checks.\n#   e.g. attr.ib(type=int, validator=validate_str)\n#        -> error: Cannot infer function type argument\n# 3) type (and do all of the work in the mypy plugin)\n#   - Pros: Simple here, and we could customize the plugin with our own errors.\n#   - Cons: Would need to write mypy plugin code to handle all the cases.\n# We chose option #1.\n\n# `attr` lies about its return type to make the following possible:\n#     attr()    -> Any\n#     attr(8)   -> int\n#     attr(validator=<some callable>)  -> Whatever the callable expects.\n# This makes this type of assignments possible:\n#     x: int = attr(8)\n#\n# This form catches explicit None or no default but with no other arguments\n# returns Any.\n@overload\ndef attrib(\n    default: None = ...,\n    validator: None = ...,\n    repr: _ReprArgType = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    type: None = ...,\n    converter: None = ...,\n    factory: None = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> Any: ...\n\n# This form catches an explicit None or no default and infers the type from the\n# other arguments.\n@overload\ndef attrib(\n    default: None = ...,\n    validator: Optional[_ValidatorArgType[_T]] = ...,\n    repr: _ReprArgType = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    type: Optional[Type[_T]] = ...,\n    converter: Optional[_ConverterType] = ...,\n    factory: Optional[Callable[[], _T]] = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> _T: ...\n\n# This form catches an explicit default argument.\n@overload\ndef attrib(\n    default: _T,\n    validator: Optional[_ValidatorArgType[_T]] = ...,\n    repr: _ReprArgType = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    type: Optional[Type[_T]] = ...,\n    converter: Optional[_ConverterType] = ...,\n    factory: Optional[Callable[[], _T]] = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> _T: ...\n\n# This form covers type=non-Type: e.g. forward references (str), Any\n@overload\ndef attrib(\n    default: Optional[_T] = ...,\n    validator: Optional[_ValidatorArgType[_T]] = ...,\n    repr: _ReprArgType = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    type: object = ...,\n    converter: Optional[_ConverterType] = ...,\n    factory: Optional[Callable[[], _T]] = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> Any: ...\n@overload\ndef field(\n    *,\n    default: None = ...,\n    validator: None = ...,\n    repr: _ReprArgType = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    converter: None = ...,\n    factory: None = ...,\n    kw_only: bool = ...,\n    eq: Optional[bool] = ...,\n    order: Optional[bool] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> Any: ...\n\n# This form catches an explicit None or no default and infers the type from the\n# other arguments.\n@overload\ndef field(\n    *,\n    default: None = ...,\n    validator: Optional[_ValidatorArgType[_T]] = ...,\n    repr: _ReprArgType = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    converter: Optional[_ConverterType] = ...,\n    factory: Optional[Callable[[], _T]] = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> _T: ...\n\n# This form catches an explicit default argument.\n@overload\ndef field(\n    *,\n    default: _T,\n    validator: Optional[_ValidatorArgType[_T]] = ...,\n    repr: _ReprArgType = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    converter: Optional[_ConverterType] = ...,\n    factory: Optional[Callable[[], _T]] = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> _T: ...\n\n# This form covers type=non-Type: e.g. forward references (str), Any\n@overload\ndef field(\n    *,\n    default: Optional[_T] = ...,\n    validator: Optional[_ValidatorArgType[_T]] = ...,\n    repr: _ReprArgType = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    converter: Optional[_ConverterType] = ...,\n    factory: Optional[Callable[[], _T]] = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> Any: ...\n@overload\n@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))\ndef attrs(\n    maybe_cls: _C,\n    these: Optional[Dict[str, Any]] = ...,\n    repr_ns: Optional[str] = ...,\n    repr: bool = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    slots: bool = ...,\n    frozen: bool = ...,\n    weakref_slot: bool = ...,\n    str: bool = ...,\n    auto_attribs: bool = ...,\n    kw_only: bool = ...,\n    cache_hash: bool = ...,\n    auto_exc: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    auto_detect: bool = ...,\n    collect_by_mro: bool = ...,\n    getstate_setstate: Optional[bool] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n    field_transformer: Optional[_FieldTransformer] = ...,\n) -> _C: ...\n@overload\n@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))\ndef attrs(\n    maybe_cls: None = ...,\n    these: Optional[Dict[str, Any]] = ...,\n    repr_ns: Optional[str] = ...,\n    repr: bool = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    slots: bool = ...,\n    frozen: bool = ...,\n    weakref_slot: bool = ...,\n    str: bool = ...,\n    auto_attribs: bool = ...,\n    kw_only: bool = ...,\n    cache_hash: bool = ...,\n    auto_exc: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    auto_detect: bool = ...,\n    collect_by_mro: bool = ...,\n    getstate_setstate: Optional[bool] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n    field_transformer: Optional[_FieldTransformer] = ...,\n) -> Callable[[_C], _C]: ...\n@overload\n@__dataclass_transform__(field_descriptors=(attrib, field))\ndef define(\n    maybe_cls: _C,\n    *,\n    these: Optional[Dict[str, Any]] = ...,\n    repr: bool = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    slots: bool = ...,\n    frozen: bool = ...,\n    weakref_slot: bool = ...,\n    str: bool = ...,\n    auto_attribs: bool = ...,\n    kw_only: bool = ...,\n    cache_hash: bool = ...,\n    auto_exc: bool = ...,\n    eq: Optional[bool] = ...,\n    order: Optional[bool] = ...,\n    auto_detect: bool = ...,\n    getstate_setstate: Optional[bool] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n    field_transformer: Optional[_FieldTransformer] = ...,\n) -> _C: ...\n@overload\n@__dataclass_transform__(field_descriptors=(attrib, field))\ndef define(\n    maybe_cls: None = ...,\n    *,\n    these: Optional[Dict[str, Any]] = ...,\n    repr: bool = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    slots: bool = ...,\n    frozen: bool = ...,\n    weakref_slot: bool = ...,\n    str: bool = ...,\n    auto_attribs: bool = ...,\n    kw_only: bool = ...,\n    cache_hash: bool = ...,\n    auto_exc: bool = ...,\n    eq: Optional[bool] = ...,\n    order: Optional[bool] = ...,\n    auto_detect: bool = ...,\n    getstate_setstate: Optional[bool] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n    field_transformer: Optional[_FieldTransformer] = ...,\n) -> Callable[[_C], _C]: ...\n\nmutable = define\nfrozen = define  # they differ only in their defaults\n\n# TODO: add support for returning NamedTuple from the mypy plugin\nclass _Fields(Tuple[Attribute[Any], ...]):\n    def __getattr__(self, name: str) -> Attribute[Any]: ...\n\ndef fields(cls: type) -> _Fields: ...\ndef fields_dict(cls: type) -> Dict[str, Attribute[Any]]: ...\ndef validate(inst: Any) -> None: ...\ndef resolve_types(\n    cls: _C,\n    globalns: Optional[Dict[str, Any]] = ...,\n    localns: Optional[Dict[str, Any]] = ...,\n    attribs: Optional[List[Attribute[Any]]] = ...,\n) -> _C: ...\n\n# TODO: add support for returning a proper attrs class from the mypy plugin\n# we use Any instead of _CountingAttr so that e.g. `make_class('Foo',\n# [attr.ib()])` is valid\ndef make_class(\n    name: str,\n    attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]],\n    bases: Tuple[type, ...] = ...,\n    repr_ns: Optional[str] = ...,\n    repr: bool = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    slots: bool = ...,\n    frozen: bool = ...,\n    weakref_slot: bool = ...,\n    str: bool = ...,\n    auto_attribs: bool = ...,\n    kw_only: bool = ...,\n    cache_hash: bool = ...,\n    auto_exc: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    collect_by_mro: bool = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n    field_transformer: Optional[_FieldTransformer] = ...,\n) -> type: ...\n\n# _funcs --\n\n# TODO: add support for returning TypedDict from the mypy plugin\n# FIXME: asdict/astuple do not honor their factory args. Waiting on one of\n# these:\n# https://github.com/python/mypy/issues/4236\n# https://github.com/python/typing/issues/253\ndef asdict(\n    inst: Any,\n    recurse: bool = ...,\n    filter: Optional[_FilterType[Any]] = ...,\n    dict_factory: Type[Mapping[Any, Any]] = ...,\n    retain_collection_types: bool = ...,\n    value_serializer: Optional[Callable[[type, Attribute[Any], Any], Any]] = ...,\n) -> Dict[str, Any]: ...\n\n# TODO: add support for returning NamedTuple from the mypy plugin\ndef astuple(\n    inst: Any,\n    recurse: bool = ...,\n    filter: Optional[_FilterType[Any]] = ...,\n    tuple_factory: Type[Sequence[Any]] = ...,\n    retain_collection_types: bool = ...,\n) -> Tuple[Any, ...]: ...\ndef has(cls: type) -> bool: ...\ndef assoc(inst: _T, **changes: Any) -> _T: ...\ndef evolve(inst: _T, **changes: Any) -> _T: ...\n\n# _config --\n\ndef set_run_validators(run: bool) -> None: ...\ndef get_run_validators() -> bool: ...\n\n# aliases --\n\ns = attributes = attrs\nib = attr = attrib\ndataclass = attrs  # Technically, partial(attrs, auto_attribs=True) ;)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/_cmp.py",
    "content": "from __future__ import absolute_import, division, print_function\n\nimport functools\n\nfrom ._compat import new_class\nfrom ._make import _make_ne\n\n\n_operation_names = {\"eq\": \"==\", \"lt\": \"<\", \"le\": \"<=\", \"gt\": \">\", \"ge\": \">=\"}\n\n\ndef cmp_using(\n    eq=None,\n    lt=None,\n    le=None,\n    gt=None,\n    ge=None,\n    require_same_type=True,\n    class_name=\"Comparable\",\n):\n    \"\"\"\n    Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and\n    ``cmp`` arguments to customize field comparison.\n\n    The resulting class will have a full set of ordering methods if\n    at least one of ``{lt, le, gt, ge}`` and ``eq``  are provided.\n\n    :param Optional[callable] eq: `callable` used to evaluate equality\n        of two objects.\n    :param Optional[callable] lt: `callable` used to evaluate whether\n        one object is less than another object.\n    :param Optional[callable] le: `callable` used to evaluate whether\n        one object is less than or equal to another object.\n    :param Optional[callable] gt: `callable` used to evaluate whether\n        one object is greater than another object.\n    :param Optional[callable] ge: `callable` used to evaluate whether\n        one object is greater than or equal to another object.\n\n    :param bool require_same_type: When `True`, equality and ordering methods\n        will return `NotImplemented` if objects are not of the same type.\n\n    :param Optional[str] class_name: Name of class. Defaults to 'Comparable'.\n\n    See `comparison` for more details.\n\n    .. versionadded:: 21.1.0\n    \"\"\"\n\n    body = {\n        \"__slots__\": [\"value\"],\n        \"__init__\": _make_init(),\n        \"_requirements\": [],\n        \"_is_comparable_to\": _is_comparable_to,\n    }\n\n    # Add operations.\n    num_order_functions = 0\n    has_eq_function = False\n\n    if eq is not None:\n        has_eq_function = True\n        body[\"__eq__\"] = _make_operator(\"eq\", eq)\n        body[\"__ne__\"] = _make_ne()\n\n    if lt is not None:\n        num_order_functions += 1\n        body[\"__lt__\"] = _make_operator(\"lt\", lt)\n\n    if le is not None:\n        num_order_functions += 1\n        body[\"__le__\"] = _make_operator(\"le\", le)\n\n    if gt is not None:\n        num_order_functions += 1\n        body[\"__gt__\"] = _make_operator(\"gt\", gt)\n\n    if ge is not None:\n        num_order_functions += 1\n        body[\"__ge__\"] = _make_operator(\"ge\", ge)\n\n    type_ = new_class(class_name, (object,), {}, lambda ns: ns.update(body))\n\n    # Add same type requirement.\n    if require_same_type:\n        type_._requirements.append(_check_same_type)\n\n    # Add total ordering if at least one operation was defined.\n    if 0 < num_order_functions < 4:\n        if not has_eq_function:\n            # functools.total_ordering requires __eq__ to be defined,\n            # so raise early error here to keep a nice stack.\n            raise ValueError(\n                \"eq must be define is order to complete ordering from \"\n                \"lt, le, gt, ge.\"\n            )\n        type_ = functools.total_ordering(type_)\n\n    return type_\n\n\ndef _make_init():\n    \"\"\"\n    Create __init__ method.\n    \"\"\"\n\n    def __init__(self, value):\n        \"\"\"\n        Initialize object with *value*.\n        \"\"\"\n        self.value = value\n\n    return __init__\n\n\ndef _make_operator(name, func):\n    \"\"\"\n    Create operator method.\n    \"\"\"\n\n    def method(self, other):\n        if not self._is_comparable_to(other):\n            return NotImplemented\n\n        result = func(self.value, other.value)\n        if result is NotImplemented:\n            return NotImplemented\n\n        return result\n\n    method.__name__ = \"__%s__\" % (name,)\n    method.__doc__ = \"Return a %s b.  Computed by attrs.\" % (\n        _operation_names[name],\n    )\n\n    return method\n\n\ndef _is_comparable_to(self, other):\n    \"\"\"\n    Check whether `other` is comparable to `self`.\n    \"\"\"\n    for func in self._requirements:\n        if not func(self, other):\n            return False\n    return True\n\n\ndef _check_same_type(self, other):\n    \"\"\"\n    Return True if *self* and *other* are of the same type, False otherwise.\n    \"\"\"\n    return other.value.__class__ is self.value.__class__\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/_cmp.pyi",
    "content": "from typing import Type\n\nfrom . import _CompareWithType\n\n\ndef cmp_using(\n    eq: Optional[_CompareWithType],\n    lt: Optional[_CompareWithType],\n    le: Optional[_CompareWithType],\n    gt: Optional[_CompareWithType],\n    ge: Optional[_CompareWithType],\n    require_same_type: bool,\n    class_name: str,\n) -> Type: ...\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/_compat.py",
    "content": "from __future__ import absolute_import, division, print_function\n\nimport platform\nimport sys\nimport types\nimport warnings\n\n\nPY2 = sys.version_info[0] == 2\nPYPY = platform.python_implementation() == \"PyPy\"\n\n\nif PYPY or sys.version_info[:2] >= (3, 6):\n    ordered_dict = dict\nelse:\n    from collections import OrderedDict\n\n    ordered_dict = OrderedDict\n\n\nif PY2:\n    from collections import Mapping, Sequence\n\n    from UserDict import IterableUserDict\n\n    # We 'bundle' isclass instead of using inspect as importing inspect is\n    # fairly expensive (order of 10-15 ms for a modern machine in 2016)\n    def isclass(klass):\n        return isinstance(klass, (type, types.ClassType))\n\n    def new_class(name, bases, kwds, exec_body):\n        \"\"\"\n        A minimal stub of types.new_class that we need for make_class.\n        \"\"\"\n        ns = {}\n        exec_body(ns)\n\n        return type(name, bases, ns)\n\n    # TYPE is used in exceptions, repr(int) is different on Python 2 and 3.\n    TYPE = \"type\"\n\n    def iteritems(d):\n        return d.iteritems()\n\n    # Python 2 is bereft of a read-only dict proxy, so we make one!\n    class ReadOnlyDict(IterableUserDict):\n        \"\"\"\n        Best-effort read-only dict wrapper.\n        \"\"\"\n\n        def __setitem__(self, key, val):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise TypeError(\n                \"'mappingproxy' object does not support item assignment\"\n            )\n\n        def update(self, _):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise AttributeError(\n                \"'mappingproxy' object has no attribute 'update'\"\n            )\n\n        def __delitem__(self, _):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise TypeError(\n                \"'mappingproxy' object does not support item deletion\"\n            )\n\n        def clear(self):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise AttributeError(\n                \"'mappingproxy' object has no attribute 'clear'\"\n            )\n\n        def pop(self, key, default=None):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise AttributeError(\n                \"'mappingproxy' object has no attribute 'pop'\"\n            )\n\n        def popitem(self):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise AttributeError(\n                \"'mappingproxy' object has no attribute 'popitem'\"\n            )\n\n        def setdefault(self, key, default=None):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise AttributeError(\n                \"'mappingproxy' object has no attribute 'setdefault'\"\n            )\n\n        def __repr__(self):\n            # Override to be identical to the Python 3 version.\n            return \"mappingproxy(\" + repr(self.data) + \")\"\n\n    def metadata_proxy(d):\n        res = ReadOnlyDict()\n        res.data.update(d)  # We blocked update, so we have to do it like this.\n        return res\n\n    def just_warn(*args, **kw):  # pragma: no cover\n        \"\"\"\n        We only warn on Python 3 because we are not aware of any concrete\n        consequences of not setting the cell on Python 2.\n        \"\"\"\n\n\nelse:  # Python 3 and later.\n    from collections.abc import Mapping, Sequence  # noqa\n\n    def just_warn(*args, **kw):\n        \"\"\"\n        We only warn on Python 3 because we are not aware of any concrete\n        consequences of not setting the cell on Python 2.\n        \"\"\"\n        warnings.warn(\n            \"Running interpreter doesn't sufficiently support code object \"\n            \"introspection.  Some features like bare super() or accessing \"\n            \"__class__ will not work with slotted classes.\",\n            RuntimeWarning,\n            stacklevel=2,\n        )\n\n    def isclass(klass):\n        return isinstance(klass, type)\n\n    TYPE = \"class\"\n\n    def iteritems(d):\n        return d.items()\n\n    new_class = types.new_class\n\n    def metadata_proxy(d):\n        return types.MappingProxyType(dict(d))\n\n\ndef make_set_closure_cell():\n    \"\"\"Return a function of two arguments (cell, value) which sets\n    the value stored in the closure cell `cell` to `value`.\n    \"\"\"\n    # pypy makes this easy. (It also supports the logic below, but\n    # why not do the easy/fast thing?)\n    if PYPY:\n\n        def set_closure_cell(cell, value):\n            cell.__setstate__((value,))\n\n        return set_closure_cell\n\n    # Otherwise gotta do it the hard way.\n\n    # Create a function that will set its first cellvar to `value`.\n    def set_first_cellvar_to(value):\n        x = value\n        return\n\n        # This function will be eliminated as dead code, but\n        # not before its reference to `x` forces `x` to be\n        # represented as a closure cell rather than a local.\n        def force_x_to_be_a_cell():  # pragma: no cover\n            return x\n\n    try:\n        # Extract the code object and make sure our assumptions about\n        # the closure behavior are correct.\n        if PY2:\n            co = set_first_cellvar_to.func_code\n        else:\n            co = set_first_cellvar_to.__code__\n        if co.co_cellvars != (\"x\",) or co.co_freevars != ():\n            raise AssertionError  # pragma: no cover\n\n        # Convert this code object to a code object that sets the\n        # function's first _freevar_ (not cellvar) to the argument.\n        if sys.version_info >= (3, 8):\n            # CPython 3.8+ has an incompatible CodeType signature\n            # (added a posonlyargcount argument) but also added\n            # CodeType.replace() to do this without counting parameters.\n            set_first_freevar_code = co.replace(\n                co_cellvars=co.co_freevars, co_freevars=co.co_cellvars\n            )\n        else:\n            args = [co.co_argcount]\n            if not PY2:\n                args.append(co.co_kwonlyargcount)\n            args.extend(\n                [\n                    co.co_nlocals,\n                    co.co_stacksize,\n                    co.co_flags,\n                    co.co_code,\n                    co.co_consts,\n                    co.co_names,\n                    co.co_varnames,\n                    co.co_filename,\n                    co.co_name,\n                    co.co_firstlineno,\n                    co.co_lnotab,\n                    # These two arguments are reversed:\n                    co.co_cellvars,\n                    co.co_freevars,\n                ]\n            )\n            set_first_freevar_code = types.CodeType(*args)\n\n        def set_closure_cell(cell, value):\n            # Create a function using the set_first_freevar_code,\n            # whose first closure cell is `cell`. Calling it will\n            # change the value of that cell.\n            setter = types.FunctionType(\n                set_first_freevar_code, {}, \"setter\", (), (cell,)\n            )\n            # And call it to set the cell.\n            setter(value)\n\n        # Make sure it works on this interpreter:\n        def make_func_with_cell():\n            x = None\n\n            def func():\n                return x  # pragma: no cover\n\n            return func\n\n        if PY2:\n            cell = make_func_with_cell().func_closure[0]\n        else:\n            cell = make_func_with_cell().__closure__[0]\n        set_closure_cell(cell, 100)\n        if cell.cell_contents != 100:\n            raise AssertionError  # pragma: no cover\n\n    except Exception:\n        return just_warn\n    else:\n        return set_closure_cell\n\n\nset_closure_cell = make_set_closure_cell()\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/_config.py",
    "content": "from __future__ import absolute_import, division, print_function\n\n\n__all__ = [\"set_run_validators\", \"get_run_validators\"]\n\n_run_validators = True\n\n\ndef set_run_validators(run):\n    \"\"\"\n    Set whether or not validators are run.  By default, they are run.\n    \"\"\"\n    if not isinstance(run, bool):\n        raise TypeError(\"'run' must be bool.\")\n    global _run_validators\n    _run_validators = run\n\n\ndef get_run_validators():\n    \"\"\"\n    Return whether or not validators are run.\n    \"\"\"\n    return _run_validators\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/_funcs.py",
    "content": "from __future__ import absolute_import, division, print_function\n\nimport copy\n\nfrom ._compat import iteritems\nfrom ._make import NOTHING, _obj_setattr, fields\nfrom .exceptions import AttrsAttributeNotFoundError\n\n\ndef asdict(\n    inst,\n    recurse=True,\n    filter=None,\n    dict_factory=dict,\n    retain_collection_types=False,\n    value_serializer=None,\n):\n    \"\"\"\n    Return the ``attrs`` attribute values of *inst* as a dict.\n\n    Optionally recurse into other ``attrs``-decorated classes.\n\n    :param inst: Instance of an ``attrs``-decorated class.\n    :param bool recurse: Recurse into classes that are also\n        ``attrs``-decorated.\n    :param callable filter: A callable whose return code determines whether an\n        attribute or element is included (``True``) or dropped (``False``).  Is\n        called with the `attr.Attribute` as the first argument and the\n        value as the second argument.\n    :param callable dict_factory: A callable to produce dictionaries from.  For\n        example, to produce ordered dictionaries instead of normal Python\n        dictionaries, pass in ``collections.OrderedDict``.\n    :param bool retain_collection_types: Do not convert to ``list`` when\n        encountering an attribute whose type is ``tuple`` or ``set``.  Only\n        meaningful if ``recurse`` is ``True``.\n    :param Optional[callable] value_serializer: A hook that is called for every\n        attribute or dict key/value.  It receives the current instance, field\n        and value and must return the (updated) value.  The hook is run *after*\n        the optional *filter* has been applied.\n\n    :rtype: return type of *dict_factory*\n\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class.\n\n    ..  versionadded:: 16.0.0 *dict_factory*\n    ..  versionadded:: 16.1.0 *retain_collection_types*\n    ..  versionadded:: 20.3.0 *value_serializer*\n    \"\"\"\n    attrs = fields(inst.__class__)\n    rv = dict_factory()\n    for a in attrs:\n        v = getattr(inst, a.name)\n        if filter is not None and not filter(a, v):\n            continue\n\n        if value_serializer is not None:\n            v = value_serializer(inst, a, v)\n\n        if recurse is True:\n            if has(v.__class__):\n                rv[a.name] = asdict(\n                    v,\n                    True,\n                    filter,\n                    dict_factory,\n                    retain_collection_types,\n                    value_serializer,\n                )\n            elif isinstance(v, (tuple, list, set, frozenset)):\n                cf = v.__class__ if retain_collection_types is True else list\n                rv[a.name] = cf(\n                    [\n                        _asdict_anything(\n                            i,\n                            filter,\n                            dict_factory,\n                            retain_collection_types,\n                            value_serializer,\n                        )\n                        for i in v\n                    ]\n                )\n            elif isinstance(v, dict):\n                df = dict_factory\n                rv[a.name] = df(\n                    (\n                        _asdict_anything(\n                            kk,\n                            filter,\n                            df,\n                            retain_collection_types,\n                            value_serializer,\n                        ),\n                        _asdict_anything(\n                            vv,\n                            filter,\n                            df,\n                            retain_collection_types,\n                            value_serializer,\n                        ),\n                    )\n                    for kk, vv in iteritems(v)\n                )\n            else:\n                rv[a.name] = v\n        else:\n            rv[a.name] = v\n    return rv\n\n\ndef _asdict_anything(\n    val,\n    filter,\n    dict_factory,\n    retain_collection_types,\n    value_serializer,\n):\n    \"\"\"\n    ``asdict`` only works on attrs instances, this works on anything.\n    \"\"\"\n    if getattr(val.__class__, \"__attrs_attrs__\", None) is not None:\n        # Attrs class.\n        rv = asdict(\n            val,\n            True,\n            filter,\n            dict_factory,\n            retain_collection_types,\n            value_serializer,\n        )\n    elif isinstance(val, (tuple, list, set, frozenset)):\n        cf = val.__class__ if retain_collection_types is True else list\n        rv = cf(\n            [\n                _asdict_anything(\n                    i,\n                    filter,\n                    dict_factory,\n                    retain_collection_types,\n                    value_serializer,\n                )\n                for i in val\n            ]\n        )\n    elif isinstance(val, dict):\n        df = dict_factory\n        rv = df(\n            (\n                _asdict_anything(\n                    kk, filter, df, retain_collection_types, value_serializer\n                ),\n                _asdict_anything(\n                    vv, filter, df, retain_collection_types, value_serializer\n                ),\n            )\n            for kk, vv in iteritems(val)\n        )\n    else:\n        rv = val\n        if value_serializer is not None:\n            rv = value_serializer(None, None, rv)\n\n    return rv\n\n\ndef astuple(\n    inst,\n    recurse=True,\n    filter=None,\n    tuple_factory=tuple,\n    retain_collection_types=False,\n):\n    \"\"\"\n    Return the ``attrs`` attribute values of *inst* as a tuple.\n\n    Optionally recurse into other ``attrs``-decorated classes.\n\n    :param inst: Instance of an ``attrs``-decorated class.\n    :param bool recurse: Recurse into classes that are also\n        ``attrs``-decorated.\n    :param callable filter: A callable whose return code determines whether an\n        attribute or element is included (``True``) or dropped (``False``).  Is\n        called with the `attr.Attribute` as the first argument and the\n        value as the second argument.\n    :param callable tuple_factory: A callable to produce tuples from.  For\n        example, to produce lists instead of tuples.\n    :param bool retain_collection_types: Do not convert to ``list``\n        or ``dict`` when encountering an attribute which type is\n        ``tuple``, ``dict`` or ``set``.  Only meaningful if ``recurse`` is\n        ``True``.\n\n    :rtype: return type of *tuple_factory*\n\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class.\n\n    ..  versionadded:: 16.2.0\n    \"\"\"\n    attrs = fields(inst.__class__)\n    rv = []\n    retain = retain_collection_types  # Very long. :/\n    for a in attrs:\n        v = getattr(inst, a.name)\n        if filter is not None and not filter(a, v):\n            continue\n        if recurse is True:\n            if has(v.__class__):\n                rv.append(\n                    astuple(\n                        v,\n                        recurse=True,\n                        filter=filter,\n                        tuple_factory=tuple_factory,\n                        retain_collection_types=retain,\n                    )\n                )\n            elif isinstance(v, (tuple, list, set, frozenset)):\n                cf = v.__class__ if retain is True else list\n                rv.append(\n                    cf(\n                        [\n                            astuple(\n                                j,\n                                recurse=True,\n                                filter=filter,\n                                tuple_factory=tuple_factory,\n                                retain_collection_types=retain,\n                            )\n                            if has(j.__class__)\n                            else j\n                            for j in v\n                        ]\n                    )\n                )\n            elif isinstance(v, dict):\n                df = v.__class__ if retain is True else dict\n                rv.append(\n                    df(\n                        (\n                            astuple(\n                                kk,\n                                tuple_factory=tuple_factory,\n                                retain_collection_types=retain,\n                            )\n                            if has(kk.__class__)\n                            else kk,\n                            astuple(\n                                vv,\n                                tuple_factory=tuple_factory,\n                                retain_collection_types=retain,\n                            )\n                            if has(vv.__class__)\n                            else vv,\n                        )\n                        for kk, vv in iteritems(v)\n                    )\n                )\n            else:\n                rv.append(v)\n        else:\n            rv.append(v)\n\n    return rv if tuple_factory is list else tuple_factory(rv)\n\n\ndef has(cls):\n    \"\"\"\n    Check whether *cls* is a class with ``attrs`` attributes.\n\n    :param type cls: Class to introspect.\n    :raise TypeError: If *cls* is not a class.\n\n    :rtype: bool\n    \"\"\"\n    return getattr(cls, \"__attrs_attrs__\", None) is not None\n\n\ndef assoc(inst, **changes):\n    \"\"\"\n    Copy *inst* and apply *changes*.\n\n    :param inst: Instance of a class with ``attrs`` attributes.\n    :param changes: Keyword changes in the new copy.\n\n    :return: A copy of inst with *changes* incorporated.\n\n    :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't\n        be found on *cls*.\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class.\n\n    ..  deprecated:: 17.1.0\n        Use `evolve` instead.\n    \"\"\"\n    import warnings\n\n    warnings.warn(\n        \"assoc is deprecated and will be removed after 2018/01.\",\n        DeprecationWarning,\n        stacklevel=2,\n    )\n    new = copy.copy(inst)\n    attrs = fields(inst.__class__)\n    for k, v in iteritems(changes):\n        a = getattr(attrs, k, NOTHING)\n        if a is NOTHING:\n            raise AttrsAttributeNotFoundError(\n                \"{k} is not an attrs attribute on {cl}.\".format(\n                    k=k, cl=new.__class__\n                )\n            )\n        _obj_setattr(new, k, v)\n    return new\n\n\ndef evolve(inst, **changes):\n    \"\"\"\n    Create a new instance, based on *inst* with *changes* applied.\n\n    :param inst: Instance of a class with ``attrs`` attributes.\n    :param changes: Keyword changes in the new copy.\n\n    :return: A copy of inst with *changes* incorporated.\n\n    :raise TypeError: If *attr_name* couldn't be found in the class\n        ``__init__``.\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class.\n\n    ..  versionadded:: 17.1.0\n    \"\"\"\n    cls = inst.__class__\n    attrs = fields(cls)\n    for a in attrs:\n        if not a.init:\n            continue\n        attr_name = a.name  # To deal with private attributes.\n        init_name = attr_name if attr_name[0] != \"_\" else attr_name[1:]\n        if init_name not in changes:\n            changes[init_name] = getattr(inst, attr_name)\n\n    return cls(**changes)\n\n\ndef resolve_types(cls, globalns=None, localns=None, attribs=None):\n    \"\"\"\n    Resolve any strings and forward annotations in type annotations.\n\n    This is only required if you need concrete types in `Attribute`'s *type*\n    field. In other words, you don't need to resolve your types if you only\n    use them for static type checking.\n\n    With no arguments, names will be looked up in the module in which the class\n    was created. If this is not what you want, e.g. if the name only exists\n    inside a method, you may pass *globalns* or *localns* to specify other\n    dictionaries in which to look up these names. See the docs of\n    `typing.get_type_hints` for more details.\n\n    :param type cls: Class to resolve.\n    :param Optional[dict] globalns: Dictionary containing global variables.\n    :param Optional[dict] localns: Dictionary containing local variables.\n    :param Optional[list] attribs: List of attribs for the given class.\n        This is necessary when calling from inside a ``field_transformer``\n        since *cls* is not an ``attrs`` class yet.\n\n    :raise TypeError: If *cls* is not a class.\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class and you didn't pass any attribs.\n    :raise NameError: If types cannot be resolved because of missing variables.\n\n    :returns: *cls* so you can use this function also as a class decorator.\n        Please note that you have to apply it **after** `attr.s`. That means\n        the decorator has to come in the line **before** `attr.s`.\n\n    ..  versionadded:: 20.1.0\n    ..  versionadded:: 21.1.0 *attribs*\n\n    \"\"\"\n    try:\n        # Since calling get_type_hints is expensive we cache whether we've\n        # done it already.\n        cls.__attrs_types_resolved__\n    except AttributeError:\n        import typing\n\n        hints = typing.get_type_hints(cls, globalns=globalns, localns=localns)\n        for field in fields(cls) if attribs is None else attribs:\n            if field.name in hints:\n                # Since fields have been frozen we must work around it.\n                _obj_setattr(field, \"type\", hints[field.name])\n        cls.__attrs_types_resolved__ = True\n\n    # Return the class so you can use it as a decorator too.\n    return cls\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/_make.py",
    "content": "from __future__ import absolute_import, division, print_function\n\nimport copy\nimport inspect\nimport linecache\nimport sys\nimport threading\nimport uuid\nimport warnings\n\nfrom operator import itemgetter\n\nfrom . import _config, setters\nfrom ._compat import (\n    PY2,\n    PYPY,\n    isclass,\n    iteritems,\n    metadata_proxy,\n    new_class,\n    ordered_dict,\n    set_closure_cell,\n)\nfrom .exceptions import (\n    DefaultAlreadySetError,\n    FrozenInstanceError,\n    NotAnAttrsClassError,\n    PythonTooOldError,\n    UnannotatedAttributeError,\n)\n\n\nif not PY2:\n    import typing\n\n\n# This is used at least twice, so cache it here.\n_obj_setattr = object.__setattr__\n_init_converter_pat = \"__attr_converter_%s\"\n_init_factory_pat = \"__attr_factory_{}\"\n_tuple_property_pat = (\n    \"    {attr_name} = _attrs_property(_attrs_itemgetter({index}))\"\n)\n_classvar_prefixes = (\n    \"typing.ClassVar\",\n    \"t.ClassVar\",\n    \"ClassVar\",\n    \"typing_extensions.ClassVar\",\n)\n# we don't use a double-underscore prefix because that triggers\n# name mangling when trying to create a slot for the field\n# (when slots=True)\n_hash_cache_field = \"_attrs_cached_hash\"\n\n_empty_metadata_singleton = metadata_proxy({})\n\n# Unique object for unequivocal getattr() defaults.\n_sentinel = object()\n\n\nclass _Nothing(object):\n    \"\"\"\n    Sentinel class to indicate the lack of a value when ``None`` is ambiguous.\n\n    ``_Nothing`` is a singleton. There is only ever one of it.\n\n    .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False.\n    \"\"\"\n\n    _singleton = None\n\n    def __new__(cls):\n        if _Nothing._singleton is None:\n            _Nothing._singleton = super(_Nothing, cls).__new__(cls)\n        return _Nothing._singleton\n\n    def __repr__(self):\n        return \"NOTHING\"\n\n    def __bool__(self):\n        return False\n\n    def __len__(self):\n        return 0  # __bool__ for Python 2\n\n\nNOTHING = _Nothing()\n\"\"\"\nSentinel to indicate the lack of a value when ``None`` is ambiguous.\n\"\"\"\n\n\nclass _CacheHashWrapper(int):\n    \"\"\"\n    An integer subclass that pickles / copies as None\n\n    This is used for non-slots classes with ``cache_hash=True``, to avoid\n    serializing a potentially (even likely) invalid hash value. Since ``None``\n    is the default value for uncalculated hashes, whenever this is copied,\n    the copy's value for the hash should automatically reset.\n\n    See GH #613 for more details.\n    \"\"\"\n\n    if PY2:\n        # For some reason `type(None)` isn't callable in Python 2, but we don't\n        # actually need a constructor for None objects, we just need any\n        # available function that returns None.\n        def __reduce__(self, _none_constructor=getattr, _args=(0, \"\", None)):\n            return _none_constructor, _args\n\n    else:\n\n        def __reduce__(self, _none_constructor=type(None), _args=()):\n            return _none_constructor, _args\n\n\ndef attrib(\n    default=NOTHING,\n    validator=None,\n    repr=True,\n    cmp=None,\n    hash=None,\n    init=True,\n    metadata=None,\n    type=None,\n    converter=None,\n    factory=None,\n    kw_only=False,\n    eq=None,\n    order=None,\n    on_setattr=None,\n):\n    \"\"\"\n    Create a new attribute on a class.\n\n    ..  warning::\n\n        Does *not* do anything unless the class is also decorated with\n        `attr.s`!\n\n    :param default: A value that is used if an ``attrs``-generated ``__init__``\n        is used and no value is passed while instantiating or the attribute is\n        excluded using ``init=False``.\n\n        If the value is an instance of `Factory`, its callable will be\n        used to construct a new value (useful for mutable data types like lists\n        or dicts).\n\n        If a default is not set (or set manually to `attr.NOTHING`), a value\n        *must* be supplied when instantiating; otherwise a `TypeError`\n        will be raised.\n\n        The default can also be set using decorator notation as shown below.\n\n    :type default: Any value\n\n    :param callable factory: Syntactic sugar for\n        ``default=attr.Factory(factory)``.\n\n    :param validator: `callable` that is called by ``attrs``-generated\n        ``__init__`` methods after the instance has been initialized.  They\n        receive the initialized instance, the `Attribute`, and the\n        passed value.\n\n        The return value is *not* inspected so the validator has to throw an\n        exception itself.\n\n        If a `list` is passed, its items are treated as validators and must\n        all pass.\n\n        Validators can be globally disabled and re-enabled using\n        `get_run_validators`.\n\n        The validator can also be set using decorator notation as shown below.\n\n    :type validator: `callable` or a `list` of `callable`\\\\ s.\n\n    :param repr: Include this attribute in the generated ``__repr__``\n        method. If ``True``, include the attribute; if ``False``, omit it. By\n        default, the built-in ``repr()`` function is used. To override how the\n        attribute value is formatted, pass a ``callable`` that takes a single\n        value and returns a string. Note that the resulting string is used\n        as-is, i.e. it will be used directly *instead* of calling ``repr()``\n        (the default).\n    :type repr: a `bool` or a `callable` to use a custom function.\n\n    :param eq: If ``True`` (default), include this attribute in the\n        generated ``__eq__`` and ``__ne__`` methods that check two instances\n        for equality. To override how the attribute value is compared,\n        pass a ``callable`` that takes a single value and returns the value\n        to be compared.\n    :type eq: a `bool` or a `callable`.\n\n    :param order: If ``True`` (default), include this attributes in the\n        generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods.\n        To override how the attribute value is ordered,\n        pass a ``callable`` that takes a single value and returns the value\n        to be ordered.\n    :type order: a `bool` or a `callable`.\n\n    :param cmp: Setting *cmp* is equivalent to setting *eq* and *order* to the\n        same value. Must not be mixed with *eq* or *order*.\n    :type cmp: a `bool` or a `callable`.\n\n    :param Optional[bool] hash: Include this attribute in the generated\n        ``__hash__`` method.  If ``None`` (default), mirror *eq*'s value.  This\n        is the correct behavior according the Python spec.  Setting this value\n        to anything else than ``None`` is *discouraged*.\n    :param bool init: Include this attribute in the generated ``__init__``\n        method.  It is possible to set this to ``False`` and set a default\n        value.  In that case this attributed is unconditionally initialized\n        with the specified default value or factory.\n    :param callable converter: `callable` that is called by\n        ``attrs``-generated ``__init__`` methods to convert attribute's value\n        to the desired format.  It is given the passed-in value, and the\n        returned value will be used as the new value of the attribute.  The\n        value is converted before being passed to the validator, if any.\n    :param metadata: An arbitrary mapping, to be used by third-party\n        components.  See `extending_metadata`.\n    :param type: The type of the attribute.  In Python 3.6 or greater, the\n        preferred method to specify the type is using a variable annotation\n        (see `PEP 526 <https://www.python.org/dev/peps/pep-0526/>`_).\n        This argument is provided for backward compatibility.\n        Regardless of the approach used, the type will be stored on\n        ``Attribute.type``.\n\n        Please note that ``attrs`` doesn't do anything with this metadata by\n        itself. You can use it as part of your own code or for\n        `static type checking <types>`.\n    :param kw_only: Make this attribute keyword-only (Python 3+)\n        in the generated ``__init__`` (if ``init`` is ``False``, this\n        parameter is ignored).\n    :param on_setattr: Allows to overwrite the *on_setattr* setting from\n        `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used.\n        Set to `attr.setters.NO_OP` to run **no** `setattr` hooks for this\n        attribute -- regardless of the setting in `attr.s`.\n    :type on_setattr: `callable`, or a list of callables, or `None`, or\n        `attr.setters.NO_OP`\n\n    .. versionadded:: 15.2.0 *convert*\n    .. versionadded:: 16.3.0 *metadata*\n    .. versionchanged:: 17.1.0 *validator* can be a ``list`` now.\n    .. versionchanged:: 17.1.0\n       *hash* is ``None`` and therefore mirrors *eq* by default.\n    .. versionadded:: 17.3.0 *type*\n    .. deprecated:: 17.4.0 *convert*\n    .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated\n       *convert* to achieve consistency with other noun-based arguments.\n    .. versionadded:: 18.1.0\n       ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``.\n    .. versionadded:: 18.2.0 *kw_only*\n    .. versionchanged:: 19.2.0 *convert* keyword argument removed.\n    .. versionchanged:: 19.2.0 *repr* also accepts a custom callable.\n    .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.\n    .. versionadded:: 19.2.0 *eq* and *order*\n    .. versionadded:: 20.1.0 *on_setattr*\n    .. versionchanged:: 20.3.0 *kw_only* backported to Python 2\n    .. versionchanged:: 21.1.0\n       *eq*, *order*, and *cmp* also accept a custom callable\n    .. versionchanged:: 21.1.0 *cmp* undeprecated\n    \"\"\"\n    eq, eq_key, order, order_key = _determine_attrib_eq_order(\n        cmp, eq, order, True\n    )\n\n    if hash is not None and hash is not True and hash is not False:\n        raise TypeError(\n            \"Invalid value for hash.  Must be True, False, or None.\"\n        )\n\n    if factory is not None:\n        if default is not NOTHING:\n            raise ValueError(\n                \"The `default` and `factory` arguments are mutually \"\n                \"exclusive.\"\n            )\n        if not callable(factory):\n            raise ValueError(\"The `factory` argument must be a callable.\")\n        default = Factory(factory)\n\n    if metadata is None:\n        metadata = {}\n\n    # Apply syntactic sugar by auto-wrapping.\n    if isinstance(on_setattr, (list, tuple)):\n        on_setattr = setters.pipe(*on_setattr)\n\n    if validator and isinstance(validator, (list, tuple)):\n        validator = and_(*validator)\n\n    if converter and isinstance(converter, (list, tuple)):\n        converter = pipe(*converter)\n\n    return _CountingAttr(\n        default=default,\n        validator=validator,\n        repr=repr,\n        cmp=None,\n        hash=hash,\n        init=init,\n        converter=converter,\n        metadata=metadata,\n        type=type,\n        kw_only=kw_only,\n        eq=eq,\n        eq_key=eq_key,\n        order=order,\n        order_key=order_key,\n        on_setattr=on_setattr,\n    )\n\n\ndef _compile_and_eval(script, globs, locs=None, filename=\"\"):\n    \"\"\"\n    \"Exec\" the script with the given global (globs) and local (locs) variables.\n    \"\"\"\n    bytecode = compile(script, filename, \"exec\")\n    eval(bytecode, globs, locs)\n\n\ndef _make_method(name, script, filename, globs=None):\n    \"\"\"\n    Create the method with the script given and return the method object.\n    \"\"\"\n    locs = {}\n    if globs is None:\n        globs = {}\n\n    _compile_and_eval(script, globs, locs, filename)\n\n    # In order of debuggers like PDB being able to step through the code,\n    # we add a fake linecache entry.\n    linecache.cache[filename] = (\n        len(script),\n        None,\n        script.splitlines(True),\n        filename,\n    )\n\n    return locs[name]\n\n\ndef _make_attr_tuple_class(cls_name, attr_names):\n    \"\"\"\n    Create a tuple subclass to hold `Attribute`s for an `attrs` class.\n\n    The subclass is a bare tuple with properties for names.\n\n    class MyClassAttributes(tuple):\n        __slots__ = ()\n        x = property(itemgetter(0))\n    \"\"\"\n    attr_class_name = \"{}Attributes\".format(cls_name)\n    attr_class_template = [\n        \"class {}(tuple):\".format(attr_class_name),\n        \"    __slots__ = ()\",\n    ]\n    if attr_names:\n        for i, attr_name in enumerate(attr_names):\n            attr_class_template.append(\n                _tuple_property_pat.format(index=i, attr_name=attr_name)\n            )\n    else:\n        attr_class_template.append(\"    pass\")\n    globs = {\"_attrs_itemgetter\": itemgetter, \"_attrs_property\": property}\n    _compile_and_eval(\"\\n\".join(attr_class_template), globs)\n    return globs[attr_class_name]\n\n\n# Tuple class for extracted attributes from a class definition.\n# `base_attrs` is a subset of `attrs`.\n_Attributes = _make_attr_tuple_class(\n    \"_Attributes\",\n    [\n        # all attributes to build dunder methods for\n        \"attrs\",\n        # attributes that have been inherited\n        \"base_attrs\",\n        # map inherited attributes to their originating classes\n        \"base_attrs_map\",\n    ],\n)\n\n\ndef _is_class_var(annot):\n    \"\"\"\n    Check whether *annot* is a typing.ClassVar.\n\n    The string comparison hack is used to avoid evaluating all string\n    annotations which would put attrs-based classes at a performance\n    disadvantage compared to plain old classes.\n    \"\"\"\n    annot = str(annot)\n\n    # Annotation can be quoted.\n    if annot.startswith((\"'\", '\"')) and annot.endswith((\"'\", '\"')):\n        annot = annot[1:-1]\n\n    return annot.startswith(_classvar_prefixes)\n\n\ndef _has_own_attribute(cls, attrib_name):\n    \"\"\"\n    Check whether *cls* defines *attrib_name* (and doesn't just inherit it).\n\n    Requires Python 3.\n    \"\"\"\n    attr = getattr(cls, attrib_name, _sentinel)\n    if attr is _sentinel:\n        return False\n\n    for base_cls in cls.__mro__[1:]:\n        a = getattr(base_cls, attrib_name, None)\n        if attr is a:\n            return False\n\n    return True\n\n\ndef _get_annotations(cls):\n    \"\"\"\n    Get annotations for *cls*.\n    \"\"\"\n    if _has_own_attribute(cls, \"__annotations__\"):\n        return cls.__annotations__\n\n    return {}\n\n\ndef _counter_getter(e):\n    \"\"\"\n    Key function for sorting to avoid re-creating a lambda for every class.\n    \"\"\"\n    return e[1].counter\n\n\ndef _collect_base_attrs(cls, taken_attr_names):\n    \"\"\"\n    Collect attr.ibs from base classes of *cls*, except *taken_attr_names*.\n    \"\"\"\n    base_attrs = []\n    base_attr_map = {}  # A dictionary of base attrs to their classes.\n\n    # Traverse the MRO and collect attributes.\n    for base_cls in reversed(cls.__mro__[1:-1]):\n        for a in getattr(base_cls, \"__attrs_attrs__\", []):\n            if a.inherited or a.name in taken_attr_names:\n                continue\n\n            a = a.evolve(inherited=True)\n            base_attrs.append(a)\n            base_attr_map[a.name] = base_cls\n\n    # For each name, only keep the freshest definition i.e. the furthest at the\n    # back.  base_attr_map is fine because it gets overwritten with every new\n    # instance.\n    filtered = []\n    seen = set()\n    for a in reversed(base_attrs):\n        if a.name in seen:\n            continue\n        filtered.insert(0, a)\n        seen.add(a.name)\n\n    return filtered, base_attr_map\n\n\ndef _collect_base_attrs_broken(cls, taken_attr_names):\n    \"\"\"\n    Collect attr.ibs from base classes of *cls*, except *taken_attr_names*.\n\n    N.B. *taken_attr_names* will be mutated.\n\n    Adhere to the old incorrect behavior.\n\n    Notably it collects from the front and considers inherited attributes which\n    leads to the buggy behavior reported in #428.\n    \"\"\"\n    base_attrs = []\n    base_attr_map = {}  # A dictionary of base attrs to their classes.\n\n    # Traverse the MRO and collect attributes.\n    for base_cls in cls.__mro__[1:-1]:\n        for a in getattr(base_cls, \"__attrs_attrs__\", []):\n            if a.name in taken_attr_names:\n                continue\n\n            a = a.evolve(inherited=True)\n            taken_attr_names.add(a.name)\n            base_attrs.append(a)\n            base_attr_map[a.name] = base_cls\n\n    return base_attrs, base_attr_map\n\n\ndef _transform_attrs(\n    cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer\n):\n    \"\"\"\n    Transform all `_CountingAttr`s on a class into `Attribute`s.\n\n    If *these* is passed, use that and don't look for them on the class.\n\n    *collect_by_mro* is True, collect them in the correct MRO order, otherwise\n    use the old -- incorrect -- order.  See #428.\n\n    Return an `_Attributes`.\n    \"\"\"\n    cd = cls.__dict__\n    anns = _get_annotations(cls)\n\n    if these is not None:\n        ca_list = [(name, ca) for name, ca in iteritems(these)]\n\n        if not isinstance(these, ordered_dict):\n            ca_list.sort(key=_counter_getter)\n    elif auto_attribs is True:\n        ca_names = {\n            name\n            for name, attr in cd.items()\n            if isinstance(attr, _CountingAttr)\n        }\n        ca_list = []\n        annot_names = set()\n        for attr_name, type in anns.items():\n            if _is_class_var(type):\n                continue\n            annot_names.add(attr_name)\n            a = cd.get(attr_name, NOTHING)\n\n            if not isinstance(a, _CountingAttr):\n                if a is NOTHING:\n                    a = attrib()\n                else:\n                    a = attrib(default=a)\n            ca_list.append((attr_name, a))\n\n        unannotated = ca_names - annot_names\n        if len(unannotated) > 0:\n            raise UnannotatedAttributeError(\n                \"The following `attr.ib`s lack a type annotation: \"\n                + \", \".join(\n                    sorted(unannotated, key=lambda n: cd.get(n).counter)\n                )\n                + \".\"\n            )\n    else:\n        ca_list = sorted(\n            (\n                (name, attr)\n                for name, attr in cd.items()\n                if isinstance(attr, _CountingAttr)\n            ),\n            key=lambda e: e[1].counter,\n        )\n\n    own_attrs = [\n        Attribute.from_counting_attr(\n            name=attr_name, ca=ca, type=anns.get(attr_name)\n        )\n        for attr_name, ca in ca_list\n    ]\n\n    if collect_by_mro:\n        base_attrs, base_attr_map = _collect_base_attrs(\n            cls, {a.name for a in own_attrs}\n        )\n    else:\n        base_attrs, base_attr_map = _collect_base_attrs_broken(\n            cls, {a.name for a in own_attrs}\n        )\n\n    attr_names = [a.name for a in base_attrs + own_attrs]\n\n    AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names)\n\n    if kw_only:\n        own_attrs = [a.evolve(kw_only=True) for a in own_attrs]\n        base_attrs = [a.evolve(kw_only=True) for a in base_attrs]\n\n    attrs = AttrsClass(base_attrs + own_attrs)\n\n    # Mandatory vs non-mandatory attr order only matters when they are part of\n    # the __init__ signature and when they aren't kw_only (which are moved to\n    # the end and can be mandatory or non-mandatory in any order, as they will\n    # be specified as keyword args anyway). Check the order of those attrs:\n    had_default = False\n    for a in (a for a in attrs if a.init is not False and a.kw_only is False):\n        if had_default is True and a.default is NOTHING:\n            raise ValueError(\n                \"No mandatory attributes allowed after an attribute with a \"\n                \"default value or factory.  Attribute in question: %r\" % (a,)\n            )\n\n        if had_default is False and a.default is not NOTHING:\n            had_default = True\n\n    if field_transformer is not None:\n        attrs = field_transformer(cls, attrs)\n    return _Attributes((attrs, base_attrs, base_attr_map))\n\n\nif PYPY:\n\n    def _frozen_setattrs(self, name, value):\n        \"\"\"\n        Attached to frozen classes as __setattr__.\n        \"\"\"\n        if isinstance(self, BaseException) and name in (\n            \"__cause__\",\n            \"__context__\",\n        ):\n            BaseException.__setattr__(self, name, value)\n            return\n\n        raise FrozenInstanceError()\n\n\nelse:\n\n    def _frozen_setattrs(self, name, value):\n        \"\"\"\n        Attached to frozen classes as __setattr__.\n        \"\"\"\n        raise FrozenInstanceError()\n\n\ndef _frozen_delattrs(self, name):\n    \"\"\"\n    Attached to frozen classes as __delattr__.\n    \"\"\"\n    raise FrozenInstanceError()\n\n\nclass _ClassBuilder(object):\n    \"\"\"\n    Iteratively build *one* class.\n    \"\"\"\n\n    __slots__ = (\n        \"_attr_names\",\n        \"_attrs\",\n        \"_base_attr_map\",\n        \"_base_names\",\n        \"_cache_hash\",\n        \"_cls\",\n        \"_cls_dict\",\n        \"_delete_attribs\",\n        \"_frozen\",\n        \"_has_pre_init\",\n        \"_has_post_init\",\n        \"_is_exc\",\n        \"_on_setattr\",\n        \"_slots\",\n        \"_weakref_slot\",\n        \"_has_own_setattr\",\n        \"_has_custom_setattr\",\n    )\n\n    def __init__(\n        self,\n        cls,\n        these,\n        slots,\n        frozen,\n        weakref_slot,\n        getstate_setstate,\n        auto_attribs,\n        kw_only,\n        cache_hash,\n        is_exc,\n        collect_by_mro,\n        on_setattr,\n        has_custom_setattr,\n        field_transformer,\n    ):\n        attrs, base_attrs, base_map = _transform_attrs(\n            cls,\n            these,\n            auto_attribs,\n            kw_only,\n            collect_by_mro,\n            field_transformer,\n        )\n\n        self._cls = cls\n        self._cls_dict = dict(cls.__dict__) if slots else {}\n        self._attrs = attrs\n        self._base_names = set(a.name for a in base_attrs)\n        self._base_attr_map = base_map\n        self._attr_names = tuple(a.name for a in attrs)\n        self._slots = slots\n        self._frozen = frozen\n        self._weakref_slot = weakref_slot\n        self._cache_hash = cache_hash\n        self._has_pre_init = bool(getattr(cls, \"__attrs_pre_init__\", False))\n        self._has_post_init = bool(getattr(cls, \"__attrs_post_init__\", False))\n        self._delete_attribs = not bool(these)\n        self._is_exc = is_exc\n        self._on_setattr = on_setattr\n\n        self._has_custom_setattr = has_custom_setattr\n        self._has_own_setattr = False\n\n        self._cls_dict[\"__attrs_attrs__\"] = self._attrs\n\n        if frozen:\n            self._cls_dict[\"__setattr__\"] = _frozen_setattrs\n            self._cls_dict[\"__delattr__\"] = _frozen_delattrs\n\n            self._has_own_setattr = True\n\n        if getstate_setstate:\n            (\n                self._cls_dict[\"__getstate__\"],\n                self._cls_dict[\"__setstate__\"],\n            ) = self._make_getstate_setstate()\n\n    def __repr__(self):\n        return \"<_ClassBuilder(cls={cls})>\".format(cls=self._cls.__name__)\n\n    def build_class(self):\n        \"\"\"\n        Finalize class based on the accumulated configuration.\n\n        Builder cannot be used after calling this method.\n        \"\"\"\n        if self._slots is True:\n            return self._create_slots_class()\n        else:\n            return self._patch_original_class()\n\n    def _patch_original_class(self):\n        \"\"\"\n        Apply accumulated methods and return the class.\n        \"\"\"\n        cls = self._cls\n        base_names = self._base_names\n\n        # Clean class of attribute definitions (`attr.ib()`s).\n        if self._delete_attribs:\n            for name in self._attr_names:\n                if (\n                    name not in base_names\n                    and getattr(cls, name, _sentinel) is not _sentinel\n                ):\n                    try:\n                        delattr(cls, name)\n                    except AttributeError:\n                        # This can happen if a base class defines a class\n                        # variable and we want to set an attribute with the\n                        # same name by using only a type annotation.\n                        pass\n\n        # Attach our dunder methods.\n        for name, value in self._cls_dict.items():\n            setattr(cls, name, value)\n\n        # If we've inherited an attrs __setattr__ and don't write our own,\n        # reset it to object's.\n        if not self._has_own_setattr and getattr(\n            cls, \"__attrs_own_setattr__\", False\n        ):\n            cls.__attrs_own_setattr__ = False\n\n            if not self._has_custom_setattr:\n                cls.__setattr__ = object.__setattr__\n\n        return cls\n\n    def _create_slots_class(self):\n        \"\"\"\n        Build and return a new class with a `__slots__` attribute.\n        \"\"\"\n        cd = {\n            k: v\n            for k, v in iteritems(self._cls_dict)\n            if k not in tuple(self._attr_names) + (\"__dict__\", \"__weakref__\")\n        }\n\n        # If our class doesn't have its own implementation of __setattr__\n        # (either from the user or by us), check the bases, if one of them has\n        # an attrs-made __setattr__, that needs to be reset. We don't walk the\n        # MRO because we only care about our immediate base classes.\n        # XXX: This can be confused by subclassing a slotted attrs class with\n        # XXX: a non-attrs class and subclass the resulting class with an attrs\n        # XXX: class.  See `test_slotted_confused` for details.  For now that's\n        # XXX: OK with us.\n        if not self._has_own_setattr:\n            cd[\"__attrs_own_setattr__\"] = False\n\n            if not self._has_custom_setattr:\n                for base_cls in self._cls.__bases__:\n                    if base_cls.__dict__.get(\"__attrs_own_setattr__\", False):\n                        cd[\"__setattr__\"] = object.__setattr__\n                        break\n\n        # Traverse the MRO to collect existing slots\n        # and check for an existing __weakref__.\n        existing_slots = dict()\n        weakref_inherited = False\n        for base_cls in self._cls.__mro__[1:-1]:\n            if base_cls.__dict__.get(\"__weakref__\", None) is not None:\n                weakref_inherited = True\n            existing_slots.update(\n                {\n                    name: getattr(base_cls, name)\n                    for name in getattr(base_cls, \"__slots__\", [])\n                }\n            )\n\n        base_names = set(self._base_names)\n\n        names = self._attr_names\n        if (\n            self._weakref_slot\n            and \"__weakref__\" not in getattr(self._cls, \"__slots__\", ())\n            and \"__weakref__\" not in names\n            and not weakref_inherited\n        ):\n            names += (\"__weakref__\",)\n\n        # We only add the names of attributes that aren't inherited.\n        # Setting __slots__ to inherited attributes wastes memory.\n        slot_names = [name for name in names if name not in base_names]\n        # There are slots for attributes from current class\n        # that are defined in parent classes.\n        # As their descriptors may be overriden by a child class,\n        # we collect them here and update the class dict\n        reused_slots = {\n            slot: slot_descriptor\n            for slot, slot_descriptor in iteritems(existing_slots)\n            if slot in slot_names\n        }\n        slot_names = [name for name in slot_names if name not in reused_slots]\n        cd.update(reused_slots)\n        if self._cache_hash:\n            slot_names.append(_hash_cache_field)\n        cd[\"__slots__\"] = tuple(slot_names)\n\n        qualname = getattr(self._cls, \"__qualname__\", None)\n        if qualname is not None:\n            cd[\"__qualname__\"] = qualname\n\n        # Create new class based on old class and our methods.\n        cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd)\n\n        # The following is a fix for\n        # https://github.com/python-attrs/attrs/issues/102.  On Python 3,\n        # if a method mentions `__class__` or uses the no-arg super(), the\n        # compiler will bake a reference to the class in the method itself\n        # as `method.__closure__`.  Since we replace the class with a\n        # clone, we rewrite these references so it keeps working.\n        for item in cls.__dict__.values():\n            if isinstance(item, (classmethod, staticmethod)):\n                # Class- and staticmethods hide their functions inside.\n                # These might need to be rewritten as well.\n                closure_cells = getattr(item.__func__, \"__closure__\", None)\n            elif isinstance(item, property):\n                # Workaround for property `super()` shortcut (PY3-only).\n                # There is no universal way for other descriptors.\n                closure_cells = getattr(item.fget, \"__closure__\", None)\n            else:\n                closure_cells = getattr(item, \"__closure__\", None)\n\n            if not closure_cells:  # Catch None or the empty list.\n                continue\n            for cell in closure_cells:\n                try:\n                    match = cell.cell_contents is self._cls\n                except ValueError:  # ValueError: Cell is empty\n                    pass\n                else:\n                    if match:\n                        set_closure_cell(cell, cls)\n\n        return cls\n\n    def add_repr(self, ns):\n        self._cls_dict[\"__repr__\"] = self._add_method_dunders(\n            _make_repr(self._attrs, ns=ns)\n        )\n        return self\n\n    def add_str(self):\n        repr = self._cls_dict.get(\"__repr__\")\n        if repr is None:\n            raise ValueError(\n                \"__str__ can only be generated if a __repr__ exists.\"\n            )\n\n        def __str__(self):\n            return self.__repr__()\n\n        self._cls_dict[\"__str__\"] = self._add_method_dunders(__str__)\n        return self\n\n    def _make_getstate_setstate(self):\n        \"\"\"\n        Create custom __setstate__ and __getstate__ methods.\n        \"\"\"\n        # __weakref__ is not writable.\n        state_attr_names = tuple(\n            an for an in self._attr_names if an != \"__weakref__\"\n        )\n\n        def slots_getstate(self):\n            \"\"\"\n            Automatically created by attrs.\n            \"\"\"\n            return tuple(getattr(self, name) for name in state_attr_names)\n\n        hash_caching_enabled = self._cache_hash\n\n        def slots_setstate(self, state):\n            \"\"\"\n            Automatically created by attrs.\n            \"\"\"\n            __bound_setattr = _obj_setattr.__get__(self, Attribute)\n            for name, value in zip(state_attr_names, state):\n                __bound_setattr(name, value)\n\n            # The hash code cache is not included when the object is\n            # serialized, but it still needs to be initialized to None to\n            # indicate that the first call to __hash__ should be a cache\n            # miss.\n            if hash_caching_enabled:\n                __bound_setattr(_hash_cache_field, None)\n\n        return slots_getstate, slots_setstate\n\n    def make_unhashable(self):\n        self._cls_dict[\"__hash__\"] = None\n        return self\n\n    def add_hash(self):\n        self._cls_dict[\"__hash__\"] = self._add_method_dunders(\n            _make_hash(\n                self._cls,\n                self._attrs,\n                frozen=self._frozen,\n                cache_hash=self._cache_hash,\n            )\n        )\n\n        return self\n\n    def add_init(self):\n        self._cls_dict[\"__init__\"] = self._add_method_dunders(\n            _make_init(\n                self._cls,\n                self._attrs,\n                self._has_pre_init,\n                self._has_post_init,\n                self._frozen,\n                self._slots,\n                self._cache_hash,\n                self._base_attr_map,\n                self._is_exc,\n                self._on_setattr is not None\n                and self._on_setattr is not setters.NO_OP,\n                attrs_init=False,\n            )\n        )\n\n        return self\n\n    def add_attrs_init(self):\n        self._cls_dict[\"__attrs_init__\"] = self._add_method_dunders(\n            _make_init(\n                self._cls,\n                self._attrs,\n                self._has_pre_init,\n                self._has_post_init,\n                self._frozen,\n                self._slots,\n                self._cache_hash,\n                self._base_attr_map,\n                self._is_exc,\n                self._on_setattr is not None\n                and self._on_setattr is not setters.NO_OP,\n                attrs_init=True,\n            )\n        )\n\n        return self\n\n    def add_eq(self):\n        cd = self._cls_dict\n\n        cd[\"__eq__\"] = self._add_method_dunders(\n            _make_eq(self._cls, self._attrs)\n        )\n        cd[\"__ne__\"] = self._add_method_dunders(_make_ne())\n\n        return self\n\n    def add_order(self):\n        cd = self._cls_dict\n\n        cd[\"__lt__\"], cd[\"__le__\"], cd[\"__gt__\"], cd[\"__ge__\"] = (\n            self._add_method_dunders(meth)\n            for meth in _make_order(self._cls, self._attrs)\n        )\n\n        return self\n\n    def add_setattr(self):\n        if self._frozen:\n            return self\n\n        sa_attrs = {}\n        for a in self._attrs:\n            on_setattr = a.on_setattr or self._on_setattr\n            if on_setattr and on_setattr is not setters.NO_OP:\n                sa_attrs[a.name] = a, on_setattr\n\n        if not sa_attrs:\n            return self\n\n        if self._has_custom_setattr:\n            # We need to write a __setattr__ but there already is one!\n            raise ValueError(\n                \"Can't combine custom __setattr__ with on_setattr hooks.\"\n            )\n\n        # docstring comes from _add_method_dunders\n        def __setattr__(self, name, val):\n            try:\n                a, hook = sa_attrs[name]\n            except KeyError:\n                nval = val\n            else:\n                nval = hook(self, a, val)\n\n            _obj_setattr(self, name, nval)\n\n        self._cls_dict[\"__attrs_own_setattr__\"] = True\n        self._cls_dict[\"__setattr__\"] = self._add_method_dunders(__setattr__)\n        self._has_own_setattr = True\n\n        return self\n\n    def _add_method_dunders(self, method):\n        \"\"\"\n        Add __module__ and __qualname__ to a *method* if possible.\n        \"\"\"\n        try:\n            method.__module__ = self._cls.__module__\n        except AttributeError:\n            pass\n\n        try:\n            method.__qualname__ = \".\".join(\n                (self._cls.__qualname__, method.__name__)\n            )\n        except AttributeError:\n            pass\n\n        try:\n            method.__doc__ = \"Method generated by attrs for class %s.\" % (\n                self._cls.__qualname__,\n            )\n        except AttributeError:\n            pass\n\n        return method\n\n\n_CMP_DEPRECATION = (\n    \"The usage of `cmp` is deprecated and will be removed on or after \"\n    \"2021-06-01.  Please use `eq` and `order` instead.\"\n)\n\n\ndef _determine_attrs_eq_order(cmp, eq, order, default_eq):\n    \"\"\"\n    Validate the combination of *cmp*, *eq*, and *order*. Derive the effective\n    values of eq and order.  If *eq* is None, set it to *default_eq*.\n    \"\"\"\n    if cmp is not None and any((eq is not None, order is not None)):\n        raise ValueError(\"Don't mix `cmp` with `eq' and `order`.\")\n\n    # cmp takes precedence due to bw-compatibility.\n    if cmp is not None:\n        return cmp, cmp\n\n    # If left None, equality is set to the specified default and ordering\n    # mirrors equality.\n    if eq is None:\n        eq = default_eq\n\n    if order is None:\n        order = eq\n\n    if eq is False and order is True:\n        raise ValueError(\"`order` can only be True if `eq` is True too.\")\n\n    return eq, order\n\n\ndef _determine_attrib_eq_order(cmp, eq, order, default_eq):\n    \"\"\"\n    Validate the combination of *cmp*, *eq*, and *order*. Derive the effective\n    values of eq and order.  If *eq* is None, set it to *default_eq*.\n    \"\"\"\n    if cmp is not None and any((eq is not None, order is not None)):\n        raise ValueError(\"Don't mix `cmp` with `eq' and `order`.\")\n\n    def decide_callable_or_boolean(value):\n        \"\"\"\n        Decide whether a key function is used.\n        \"\"\"\n        if callable(value):\n            value, key = True, value\n        else:\n            key = None\n        return value, key\n\n    # cmp takes precedence due to bw-compatibility.\n    if cmp is not None:\n        cmp, cmp_key = decide_callable_or_boolean(cmp)\n        return cmp, cmp_key, cmp, cmp_key\n\n    # If left None, equality is set to the specified default and ordering\n    # mirrors equality.\n    if eq is None:\n        eq, eq_key = default_eq, None\n    else:\n        eq, eq_key = decide_callable_or_boolean(eq)\n\n    if order is None:\n        order, order_key = eq, eq_key\n    else:\n        order, order_key = decide_callable_or_boolean(order)\n\n    if eq is False and order is True:\n        raise ValueError(\"`order` can only be True if `eq` is True too.\")\n\n    return eq, eq_key, order, order_key\n\n\ndef _determine_whether_to_implement(\n    cls, flag, auto_detect, dunders, default=True\n):\n    \"\"\"\n    Check whether we should implement a set of methods for *cls*.\n\n    *flag* is the argument passed into @attr.s like 'init', *auto_detect* the\n    same as passed into @attr.s and *dunders* is a tuple of attribute names\n    whose presence signal that the user has implemented it themselves.\n\n    Return *default* if no reason for either for or against is found.\n\n    auto_detect must be False on Python 2.\n    \"\"\"\n    if flag is True or flag is False:\n        return flag\n\n    if flag is None and auto_detect is False:\n        return default\n\n    # Logically, flag is None and auto_detect is True here.\n    for dunder in dunders:\n        if _has_own_attribute(cls, dunder):\n            return False\n\n    return default\n\n\ndef attrs(\n    maybe_cls=None,\n    these=None,\n    repr_ns=None,\n    repr=None,\n    cmp=None,\n    hash=None,\n    init=None,\n    slots=False,\n    frozen=False,\n    weakref_slot=True,\n    str=False,\n    auto_attribs=False,\n    kw_only=False,\n    cache_hash=False,\n    auto_exc=False,\n    eq=None,\n    order=None,\n    auto_detect=False,\n    collect_by_mro=False,\n    getstate_setstate=None,\n    on_setattr=None,\n    field_transformer=None,\n):\n    r\"\"\"\n    A class decorator that adds `dunder\n    <https://wiki.python.org/moin/DunderAlias>`_\\ -methods according to the\n    specified attributes using `attr.ib` or the *these* argument.\n\n    :param these: A dictionary of name to `attr.ib` mappings.  This is\n        useful to avoid the definition of your attributes within the class body\n        because you can't (e.g. if you want to add ``__repr__`` methods to\n        Django models) or don't want to.\n\n        If *these* is not ``None``, ``attrs`` will *not* search the class body\n        for attributes and will *not* remove any attributes from it.\n\n        If *these* is an ordered dict (`dict` on Python 3.6+,\n        `collections.OrderedDict` otherwise), the order is deduced from\n        the order of the attributes inside *these*.  Otherwise the order\n        of the definition of the attributes is used.\n\n    :type these: `dict` of `str` to `attr.ib`\n\n    :param str repr_ns: When using nested classes, there's no way in Python 2\n        to automatically detect that.  Therefore it's possible to set the\n        namespace explicitly for a more meaningful ``repr`` output.\n    :param bool auto_detect: Instead of setting the *init*, *repr*, *eq*,\n        *order*, and *hash* arguments explicitly, assume they are set to\n        ``True`` **unless any** of the involved methods for one of the\n        arguments is implemented in the *current* class (i.e. it is *not*\n        inherited from some base class).\n\n        So for example by implementing ``__eq__`` on a class yourself,\n        ``attrs`` will deduce ``eq=False`` and will create *neither*\n        ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible\n        ``__ne__`` by default, so it *should* be enough to only implement\n        ``__eq__`` in most cases).\n\n        .. warning::\n\n           If you prevent ``attrs`` from creating the ordering methods for you\n           (``order=False``, e.g. by implementing ``__le__``), it becomes\n           *your* responsibility to make sure its ordering is sound. The best\n           way is to use the `functools.total_ordering` decorator.\n\n\n        Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*,\n        *cmp*, or *hash* overrides whatever *auto_detect* would determine.\n\n        *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises\n        a `PythonTooOldError`.\n\n    :param bool repr: Create a ``__repr__`` method with a human readable\n        representation of ``attrs`` attributes..\n    :param bool str: Create a ``__str__`` method that is identical to\n        ``__repr__``.  This is usually not necessary except for\n        `Exception`\\ s.\n    :param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__``\n        and ``__ne__`` methods that check two instances for equality.\n\n        They compare the instances as if they were tuples of their ``attrs``\n        attributes if and only if the types of both classes are *identical*!\n    :param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``,\n        ``__gt__``, and ``__ge__`` methods that behave like *eq* above and\n        allow instances to be ordered. If ``None`` (default) mirror value of\n        *eq*.\n    :param Optional[bool] cmp: Setting *cmp* is equivalent to setting *eq*\n        and *order* to the same value. Must not be mixed with *eq* or *order*.\n    :param Optional[bool] hash: If ``None`` (default), the ``__hash__`` method\n        is generated according how *eq* and *frozen* are set.\n\n        1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you.\n        2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to\n           None, marking it unhashable (which it is).\n        3. If *eq* is False, ``__hash__`` will be left untouched meaning the\n           ``__hash__`` method of the base class will be used (if base class is\n           ``object``, this means it will fall back to id-based hashing.).\n\n        Although not recommended, you can decide for yourself and force\n        ``attrs`` to create one (e.g. if the class is immutable even though you\n        didn't freeze it programmatically) by passing ``True`` or not.  Both of\n        these cases are rather special and should be used carefully.\n\n        See our documentation on `hashing`, Python's documentation on\n        `object.__hash__`, and the `GitHub issue that led to the default \\\n        behavior <https://github.com/python-attrs/attrs/issues/136>`_ for more\n        details.\n    :param bool init: Create a ``__init__`` method that initializes the\n        ``attrs`` attributes. Leading underscores are stripped for the argument\n        name. If a ``__attrs_pre_init__`` method exists on the class, it will\n        be called before the class is initialized. If a ``__attrs_post_init__``\n        method exists on the class, it will be called after the class is fully\n        initialized.\n\n        If ``init`` is ``False``, an ``__attrs_init__`` method will be\n        injected instead. This allows you to define a custom ``__init__``\n        method that can do pre-init work such as ``super().__init__()``,\n        and then call ``__attrs_init__()`` and ``__attrs_post_init__()``.\n    :param bool slots: Create a `slotted class <slotted classes>` that's more\n        memory-efficient. Slotted classes are generally superior to the default\n        dict classes, but have some gotchas you should know about, so we\n        encourage you to read the `glossary entry <slotted classes>`.\n    :param bool frozen: Make instances immutable after initialization.  If\n        someone attempts to modify a frozen instance,\n        `attr.exceptions.FrozenInstanceError` is raised.\n\n        .. note::\n\n            1. This is achieved by installing a custom ``__setattr__`` method\n               on your class, so you can't implement your own.\n\n            2. True immutability is impossible in Python.\n\n            3. This *does* have a minor a runtime performance `impact\n               <how-frozen>` when initializing new instances.  In other words:\n               ``__init__`` is slightly slower with ``frozen=True``.\n\n            4. If a class is frozen, you cannot modify ``self`` in\n               ``__attrs_post_init__`` or a self-written ``__init__``. You can\n               circumvent that limitation by using\n               ``object.__setattr__(self, \"attribute_name\", value)``.\n\n            5. Subclasses of a frozen class are frozen too.\n\n    :param bool weakref_slot: Make instances weak-referenceable.  This has no\n        effect unless ``slots`` is also enabled.\n    :param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated\n        attributes (Python 3.6 and later only) from the class body.\n\n        In this case, you **must** annotate every field.  If ``attrs``\n        encounters a field that is set to an `attr.ib` but lacks a type\n        annotation, an `attr.exceptions.UnannotatedAttributeError` is\n        raised.  Use ``field_name: typing.Any = attr.ib(...)`` if you don't\n        want to set a type.\n\n        If you assign a value to those attributes (e.g. ``x: int = 42``), that\n        value becomes the default value like if it were passed using\n        ``attr.ib(default=42)``.  Passing an instance of `Factory` also\n        works as expected in most cases (see warning below).\n\n        Attributes annotated as `typing.ClassVar`, and attributes that are\n        neither annotated nor set to an `attr.ib` are **ignored**.\n\n        .. warning::\n           For features that use the attribute name to create decorators (e.g.\n           `validators <validators>`), you still *must* assign `attr.ib` to\n           them. Otherwise Python will either not find the name or try to use\n           the default value to call e.g. ``validator`` on it.\n\n           These errors can be quite confusing and probably the most common bug\n           report on our bug tracker.\n\n        .. _`PEP 526`: https://www.python.org/dev/peps/pep-0526/\n    :param bool kw_only: Make all attributes keyword-only (Python 3+)\n        in the generated ``__init__`` (if ``init`` is ``False``, this\n        parameter is ignored).\n    :param bool cache_hash: Ensure that the object's hash code is computed\n        only once and stored on the object.  If this is set to ``True``,\n        hashing must be either explicitly or implicitly enabled for this\n        class.  If the hash code is cached, avoid any reassignments of\n        fields involved in hash code computation or mutations of the objects\n        those fields point to after object creation.  If such changes occur,\n        the behavior of the object's hash code is undefined.\n    :param bool auto_exc: If the class subclasses `BaseException`\n        (which implicitly includes any subclass of any exception), the\n        following happens to behave like a well-behaved Python exceptions\n        class:\n\n        - the values for *eq*, *order*, and *hash* are ignored and the\n          instances compare and hash by the instance's ids (N.B. ``attrs`` will\n          *not* remove existing implementations of ``__hash__`` or the equality\n          methods. It just won't add own ones.),\n        - all attributes that are either passed into ``__init__`` or have a\n          default value are additionally available as a tuple in the ``args``\n          attribute,\n        - the value of *str* is ignored leaving ``__str__`` to base classes.\n    :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs``\n       collects attributes from base classes.  The default behavior is\n       incorrect in certain cases of multiple inheritance.  It should be on by\n       default but is kept off for backward-compatability.\n\n       See issue `#428 <https://github.com/python-attrs/attrs/issues/428>`_ for\n       more details.\n\n    :param Optional[bool] getstate_setstate:\n       .. note::\n          This is usually only interesting for slotted classes and you should\n          probably just set *auto_detect* to `True`.\n\n       If `True`, ``__getstate__`` and\n       ``__setstate__`` are generated and attached to the class. This is\n       necessary for slotted classes to be pickleable. If left `None`, it's\n       `True` by default for slotted classes and ``False`` for dict classes.\n\n       If *auto_detect* is `True`, and *getstate_setstate* is left `None`,\n       and **either** ``__getstate__`` or ``__setstate__`` is detected directly\n       on the class (i.e. not inherited), it is set to `False` (this is usually\n       what you want).\n\n    :param on_setattr: A callable that is run whenever the user attempts to set\n        an attribute (either by assignment like ``i.x = 42`` or by using\n        `setattr` like ``setattr(i, \"x\", 42)``). It receives the same arguments\n        as validators: the instance, the attribute that is being modified, and\n        the new value.\n\n        If no exception is raised, the attribute is set to the return value of\n        the callable.\n\n        If a list of callables is passed, they're automatically wrapped in an\n        `attr.setters.pipe`.\n\n    :param Optional[callable] field_transformer:\n        A function that is called with the original class object and all\n        fields right before ``attrs`` finalizes the class.  You can use\n        this, e.g., to automatically add converters or validators to\n        fields based on their types.  See `transform-fields` for more details.\n\n    .. versionadded:: 16.0.0 *slots*\n    .. versionadded:: 16.1.0 *frozen*\n    .. versionadded:: 16.3.0 *str*\n    .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``.\n    .. versionchanged:: 17.1.0\n       *hash* supports ``None`` as value which is also the default now.\n    .. versionadded:: 17.3.0 *auto_attribs*\n    .. versionchanged:: 18.1.0\n       If *these* is passed, no attributes are deleted from the class body.\n    .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained.\n    .. versionadded:: 18.2.0 *weakref_slot*\n    .. deprecated:: 18.2.0\n       ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a\n       `DeprecationWarning` if the classes compared are subclasses of\n       each other. ``__eq`` and ``__ne__`` never tried to compared subclasses\n       to each other.\n    .. versionchanged:: 19.2.0\n       ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider\n       subclasses comparable anymore.\n    .. versionadded:: 18.2.0 *kw_only*\n    .. versionadded:: 18.2.0 *cache_hash*\n    .. versionadded:: 19.1.0 *auto_exc*\n    .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.\n    .. versionadded:: 19.2.0 *eq* and *order*\n    .. versionadded:: 20.1.0 *auto_detect*\n    .. versionadded:: 20.1.0 *collect_by_mro*\n    .. versionadded:: 20.1.0 *getstate_setstate*\n    .. versionadded:: 20.1.0 *on_setattr*\n    .. versionadded:: 20.3.0 *field_transformer*\n    .. versionchanged:: 21.1.0\n       ``init=False`` injects ``__attrs_init__``\n    .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__``\n    .. versionchanged:: 21.1.0 *cmp* undeprecated\n    \"\"\"\n    if auto_detect and PY2:\n        raise PythonTooOldError(\n            \"auto_detect only works on Python 3 and later.\"\n        )\n\n    eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None)\n    hash_ = hash  # work around the lack of nonlocal\n\n    if isinstance(on_setattr, (list, tuple)):\n        on_setattr = setters.pipe(*on_setattr)\n\n    def wrap(cls):\n\n        if getattr(cls, \"__class__\", None) is None:\n            raise TypeError(\"attrs only works with new-style classes.\")\n\n        is_frozen = frozen or _has_frozen_base_class(cls)\n        is_exc = auto_exc is True and issubclass(cls, BaseException)\n        has_own_setattr = auto_detect and _has_own_attribute(\n            cls, \"__setattr__\"\n        )\n\n        if has_own_setattr and is_frozen:\n            raise ValueError(\"Can't freeze a class with a custom __setattr__.\")\n\n        builder = _ClassBuilder(\n            cls,\n            these,\n            slots,\n            is_frozen,\n            weakref_slot,\n            _determine_whether_to_implement(\n                cls,\n                getstate_setstate,\n                auto_detect,\n                (\"__getstate__\", \"__setstate__\"),\n                default=slots,\n            ),\n            auto_attribs,\n            kw_only,\n            cache_hash,\n            is_exc,\n            collect_by_mro,\n            on_setattr,\n            has_own_setattr,\n            field_transformer,\n        )\n        if _determine_whether_to_implement(\n            cls, repr, auto_detect, (\"__repr__\",)\n        ):\n            builder.add_repr(repr_ns)\n        if str is True:\n            builder.add_str()\n\n        eq = _determine_whether_to_implement(\n            cls, eq_, auto_detect, (\"__eq__\", \"__ne__\")\n        )\n        if not is_exc and eq is True:\n            builder.add_eq()\n        if not is_exc and _determine_whether_to_implement(\n            cls, order_, auto_detect, (\"__lt__\", \"__le__\", \"__gt__\", \"__ge__\")\n        ):\n            builder.add_order()\n\n        builder.add_setattr()\n\n        if (\n            hash_ is None\n            and auto_detect is True\n            and _has_own_attribute(cls, \"__hash__\")\n        ):\n            hash = False\n        else:\n            hash = hash_\n        if hash is not True and hash is not False and hash is not None:\n            # Can't use `hash in` because 1 == True for example.\n            raise TypeError(\n                \"Invalid value for hash.  Must be True, False, or None.\"\n            )\n        elif hash is False or (hash is None and eq is False) or is_exc:\n            # Don't do anything. Should fall back to __object__'s __hash__\n            # which is by id.\n            if cache_hash:\n                raise TypeError(\n                    \"Invalid value for cache_hash.  To use hash caching,\"\n                    \" hashing must be either explicitly or implicitly \"\n                    \"enabled.\"\n                )\n        elif hash is True or (\n            hash is None and eq is True and is_frozen is True\n        ):\n            # Build a __hash__ if told so, or if it's safe.\n            builder.add_hash()\n        else:\n            # Raise TypeError on attempts to hash.\n            if cache_hash:\n                raise TypeError(\n                    \"Invalid value for cache_hash.  To use hash caching,\"\n                    \" hashing must be either explicitly or implicitly \"\n                    \"enabled.\"\n                )\n            builder.make_unhashable()\n\n        if _determine_whether_to_implement(\n            cls, init, auto_detect, (\"__init__\",)\n        ):\n            builder.add_init()\n        else:\n            builder.add_attrs_init()\n            if cache_hash:\n                raise TypeError(\n                    \"Invalid value for cache_hash.  To use hash caching,\"\n                    \" init must be True.\"\n                )\n\n        return builder.build_class()\n\n    # maybe_cls's type depends on the usage of the decorator.  It's a class\n    # if it's used as `@attrs` but ``None`` if used as `@attrs()`.\n    if maybe_cls is None:\n        return wrap\n    else:\n        return wrap(maybe_cls)\n\n\n_attrs = attrs\n\"\"\"\nInternal alias so we can use it in functions that take an argument called\n*attrs*.\n\"\"\"\n\n\nif PY2:\n\n    def _has_frozen_base_class(cls):\n        \"\"\"\n        Check whether *cls* has a frozen ancestor by looking at its\n        __setattr__.\n        \"\"\"\n        return (\n            getattr(cls.__setattr__, \"__module__\", None)\n            == _frozen_setattrs.__module__\n            and cls.__setattr__.__name__ == _frozen_setattrs.__name__\n        )\n\n\nelse:\n\n    def _has_frozen_base_class(cls):\n        \"\"\"\n        Check whether *cls* has a frozen ancestor by looking at its\n        __setattr__.\n        \"\"\"\n        return cls.__setattr__ == _frozen_setattrs\n\n\ndef _generate_unique_filename(cls, func_name):\n    \"\"\"\n    Create a \"filename\" suitable for a function being generated.\n    \"\"\"\n    unique_id = uuid.uuid4()\n    extra = \"\"\n    count = 1\n\n    while True:\n        unique_filename = \"<attrs generated {0} {1}.{2}{3}>\".format(\n            func_name,\n            cls.__module__,\n            getattr(cls, \"__qualname__\", cls.__name__),\n            extra,\n        )\n        # To handle concurrency we essentially \"reserve\" our spot in\n        # the linecache with a dummy line.  The caller can then\n        # set this value correctly.\n        cache_line = (1, None, (str(unique_id),), unique_filename)\n        if (\n            linecache.cache.setdefault(unique_filename, cache_line)\n            == cache_line\n        ):\n            return unique_filename\n\n        # Looks like this spot is taken. Try again.\n        count += 1\n        extra = \"-{0}\".format(count)\n\n\ndef _make_hash(cls, attrs, frozen, cache_hash):\n    attrs = tuple(\n        a for a in attrs if a.hash is True or (a.hash is None and a.eq is True)\n    )\n\n    tab = \"        \"\n\n    unique_filename = _generate_unique_filename(cls, \"hash\")\n    type_hash = hash(unique_filename)\n\n    hash_def = \"def __hash__(self\"\n    hash_func = \"hash((\"\n    closing_braces = \"))\"\n    if not cache_hash:\n        hash_def += \"):\"\n    else:\n        if not PY2:\n            hash_def += \", *\"\n\n        hash_def += (\n            \", _cache_wrapper=\"\n            + \"__import__('attr._make')._make._CacheHashWrapper):\"\n        )\n        hash_func = \"_cache_wrapper(\" + hash_func\n        closing_braces += \")\"\n\n    method_lines = [hash_def]\n\n    def append_hash_computation_lines(prefix, indent):\n        \"\"\"\n        Generate the code for actually computing the hash code.\n        Below this will either be returned directly or used to compute\n        a value which is then cached, depending on the value of cache_hash\n        \"\"\"\n\n        method_lines.extend(\n            [\n                indent + prefix + hash_func,\n                indent + \"        %d,\" % (type_hash,),\n            ]\n        )\n\n        for a in attrs:\n            method_lines.append(indent + \"        self.%s,\" % a.name)\n\n        method_lines.append(indent + \"    \" + closing_braces)\n\n    if cache_hash:\n        method_lines.append(tab + \"if self.%s is None:\" % _hash_cache_field)\n        if frozen:\n            append_hash_computation_lines(\n                \"object.__setattr__(self, '%s', \" % _hash_cache_field, tab * 2\n            )\n            method_lines.append(tab * 2 + \")\")  # close __setattr__\n        else:\n            append_hash_computation_lines(\n                \"self.%s = \" % _hash_cache_field, tab * 2\n            )\n        method_lines.append(tab + \"return self.%s\" % _hash_cache_field)\n    else:\n        append_hash_computation_lines(\"return \", tab)\n\n    script = \"\\n\".join(method_lines)\n    return _make_method(\"__hash__\", script, unique_filename)\n\n\ndef _add_hash(cls, attrs):\n    \"\"\"\n    Add a hash method to *cls*.\n    \"\"\"\n    cls.__hash__ = _make_hash(cls, attrs, frozen=False, cache_hash=False)\n    return cls\n\n\ndef _make_ne():\n    \"\"\"\n    Create __ne__ method.\n    \"\"\"\n\n    def __ne__(self, other):\n        \"\"\"\n        Check equality and either forward a NotImplemented or\n        return the result negated.\n        \"\"\"\n        result = self.__eq__(other)\n        if result is NotImplemented:\n            return NotImplemented\n\n        return not result\n\n    return __ne__\n\n\ndef _make_eq(cls, attrs):\n    \"\"\"\n    Create __eq__ method for *cls* with *attrs*.\n    \"\"\"\n    attrs = [a for a in attrs if a.eq]\n\n    unique_filename = _generate_unique_filename(cls, \"eq\")\n    lines = [\n        \"def __eq__(self, other):\",\n        \"    if other.__class__ is not self.__class__:\",\n        \"        return NotImplemented\",\n    ]\n\n    # We can't just do a big self.x = other.x and... clause due to\n    # irregularities like nan == nan is false but (nan,) == (nan,) is true.\n    globs = {}\n    if attrs:\n        lines.append(\"    return  (\")\n        others = [\"    ) == (\"]\n        for a in attrs:\n            if a.eq_key:\n                cmp_name = \"_%s_key\" % (a.name,)\n                # Add the key function to the global namespace\n                # of the evaluated function.\n                globs[cmp_name] = a.eq_key\n                lines.append(\n                    \"        %s(self.%s),\"\n                    % (\n                        cmp_name,\n                        a.name,\n                    )\n                )\n                others.append(\n                    \"        %s(other.%s),\"\n                    % (\n                        cmp_name,\n                        a.name,\n                    )\n                )\n            else:\n                lines.append(\"        self.%s,\" % (a.name,))\n                others.append(\"        other.%s,\" % (a.name,))\n\n        lines += others + [\"    )\"]\n    else:\n        lines.append(\"    return True\")\n\n    script = \"\\n\".join(lines)\n\n    return _make_method(\"__eq__\", script, unique_filename, globs)\n\n\ndef _make_order(cls, attrs):\n    \"\"\"\n    Create ordering methods for *cls* with *attrs*.\n    \"\"\"\n    attrs = [a for a in attrs if a.order]\n\n    def attrs_to_tuple(obj):\n        \"\"\"\n        Save us some typing.\n        \"\"\"\n        return tuple(\n            key(value) if key else value\n            for value, key in (\n                (getattr(obj, a.name), a.order_key) for a in attrs\n            )\n        )\n\n    def __lt__(self, other):\n        \"\"\"\n        Automatically created by attrs.\n        \"\"\"\n        if other.__class__ is self.__class__:\n            return attrs_to_tuple(self) < attrs_to_tuple(other)\n\n        return NotImplemented\n\n    def __le__(self, other):\n        \"\"\"\n        Automatically created by attrs.\n        \"\"\"\n        if other.__class__ is self.__class__:\n            return attrs_to_tuple(self) <= attrs_to_tuple(other)\n\n        return NotImplemented\n\n    def __gt__(self, other):\n        \"\"\"\n        Automatically created by attrs.\n        \"\"\"\n        if other.__class__ is self.__class__:\n            return attrs_to_tuple(self) > attrs_to_tuple(other)\n\n        return NotImplemented\n\n    def __ge__(self, other):\n        \"\"\"\n        Automatically created by attrs.\n        \"\"\"\n        if other.__class__ is self.__class__:\n            return attrs_to_tuple(self) >= attrs_to_tuple(other)\n\n        return NotImplemented\n\n    return __lt__, __le__, __gt__, __ge__\n\n\ndef _add_eq(cls, attrs=None):\n    \"\"\"\n    Add equality methods to *cls* with *attrs*.\n    \"\"\"\n    if attrs is None:\n        attrs = cls.__attrs_attrs__\n\n    cls.__eq__ = _make_eq(cls, attrs)\n    cls.__ne__ = _make_ne()\n\n    return cls\n\n\n_already_repring = threading.local()\n\n\ndef _make_repr(attrs, ns):\n    \"\"\"\n    Make a repr method that includes relevant *attrs*, adding *ns* to the full\n    name.\n    \"\"\"\n\n    # Figure out which attributes to include, and which function to use to\n    # format them. The a.repr value can be either bool or a custom callable.\n    attr_names_with_reprs = tuple(\n        (a.name, repr if a.repr is True else a.repr)\n        for a in attrs\n        if a.repr is not False\n    )\n\n    def __repr__(self):\n        \"\"\"\n        Automatically created by attrs.\n        \"\"\"\n        try:\n            working_set = _already_repring.working_set\n        except AttributeError:\n            working_set = set()\n            _already_repring.working_set = working_set\n\n        if id(self) in working_set:\n            return \"...\"\n        real_cls = self.__class__\n        if ns is None:\n            qualname = getattr(real_cls, \"__qualname__\", None)\n            if qualname is not None:\n                class_name = qualname.rsplit(\">.\", 1)[-1]\n            else:\n                class_name = real_cls.__name__\n        else:\n            class_name = ns + \".\" + real_cls.__name__\n\n        # Since 'self' remains on the stack (i.e.: strongly referenced) for the\n        # duration of this call, it's safe to depend on id(...) stability, and\n        # not need to track the instance and therefore worry about properties\n        # like weakref- or hash-ability.\n        working_set.add(id(self))\n        try:\n            result = [class_name, \"(\"]\n            first = True\n            for name, attr_repr in attr_names_with_reprs:\n                if first:\n                    first = False\n                else:\n                    result.append(\", \")\n                result.extend(\n                    (name, \"=\", attr_repr(getattr(self, name, NOTHING)))\n                )\n            return \"\".join(result) + \")\"\n        finally:\n            working_set.remove(id(self))\n\n    return __repr__\n\n\ndef _add_repr(cls, ns=None, attrs=None):\n    \"\"\"\n    Add a repr method to *cls*.\n    \"\"\"\n    if attrs is None:\n        attrs = cls.__attrs_attrs__\n\n    cls.__repr__ = _make_repr(attrs, ns)\n    return cls\n\n\ndef fields(cls):\n    \"\"\"\n    Return the tuple of ``attrs`` attributes for a class.\n\n    The tuple also allows accessing the fields by their names (see below for\n    examples).\n\n    :param type cls: Class to introspect.\n\n    :raise TypeError: If *cls* is not a class.\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class.\n\n    :rtype: tuple (with name accessors) of `attr.Attribute`\n\n    ..  versionchanged:: 16.2.0 Returned tuple allows accessing the fields\n        by name.\n    \"\"\"\n    if not isclass(cls):\n        raise TypeError(\"Passed object must be a class.\")\n    attrs = getattr(cls, \"__attrs_attrs__\", None)\n    if attrs is None:\n        raise NotAnAttrsClassError(\n            \"{cls!r} is not an attrs-decorated class.\".format(cls=cls)\n        )\n    return attrs\n\n\ndef fields_dict(cls):\n    \"\"\"\n    Return an ordered dictionary of ``attrs`` attributes for a class, whose\n    keys are the attribute names.\n\n    :param type cls: Class to introspect.\n\n    :raise TypeError: If *cls* is not a class.\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class.\n\n    :rtype: an ordered dict where keys are attribute names and values are\n        `attr.Attribute`\\\\ s. This will be a `dict` if it's\n        naturally ordered like on Python 3.6+ or an\n        :class:`~collections.OrderedDict` otherwise.\n\n    .. versionadded:: 18.1.0\n    \"\"\"\n    if not isclass(cls):\n        raise TypeError(\"Passed object must be a class.\")\n    attrs = getattr(cls, \"__attrs_attrs__\", None)\n    if attrs is None:\n        raise NotAnAttrsClassError(\n            \"{cls!r} is not an attrs-decorated class.\".format(cls=cls)\n        )\n    return ordered_dict(((a.name, a) for a in attrs))\n\n\ndef validate(inst):\n    \"\"\"\n    Validate all attributes on *inst* that have a validator.\n\n    Leaves all exceptions through.\n\n    :param inst: Instance of a class with ``attrs`` attributes.\n    \"\"\"\n    if _config._run_validators is False:\n        return\n\n    for a in fields(inst.__class__):\n        v = a.validator\n        if v is not None:\n            v(inst, a, getattr(inst, a.name))\n\n\ndef _is_slot_cls(cls):\n    return \"__slots__\" in cls.__dict__\n\n\ndef _is_slot_attr(a_name, base_attr_map):\n    \"\"\"\n    Check if the attribute name comes from a slot class.\n    \"\"\"\n    return a_name in base_attr_map and _is_slot_cls(base_attr_map[a_name])\n\n\ndef _make_init(\n    cls,\n    attrs,\n    pre_init,\n    post_init,\n    frozen,\n    slots,\n    cache_hash,\n    base_attr_map,\n    is_exc,\n    has_global_on_setattr,\n    attrs_init,\n):\n    if frozen and has_global_on_setattr:\n        raise ValueError(\"Frozen classes can't use on_setattr.\")\n\n    needs_cached_setattr = cache_hash or frozen\n    filtered_attrs = []\n    attr_dict = {}\n    for a in attrs:\n        if not a.init and a.default is NOTHING:\n            continue\n\n        filtered_attrs.append(a)\n        attr_dict[a.name] = a\n\n        if a.on_setattr is not None:\n            if frozen is True:\n                raise ValueError(\"Frozen classes can't use on_setattr.\")\n\n            needs_cached_setattr = True\n        elif (\n            has_global_on_setattr and a.on_setattr is not setters.NO_OP\n        ) or _is_slot_attr(a.name, base_attr_map):\n            needs_cached_setattr = True\n\n    unique_filename = _generate_unique_filename(cls, \"init\")\n\n    script, globs, annotations = _attrs_to_init_script(\n        filtered_attrs,\n        frozen,\n        slots,\n        pre_init,\n        post_init,\n        cache_hash,\n        base_attr_map,\n        is_exc,\n        needs_cached_setattr,\n        has_global_on_setattr,\n        attrs_init,\n    )\n    if cls.__module__ in sys.modules:\n        # This makes typing.get_type_hints(CLS.__init__) resolve string types.\n        globs.update(sys.modules[cls.__module__].__dict__)\n\n    globs.update({\"NOTHING\": NOTHING, \"attr_dict\": attr_dict})\n\n    if needs_cached_setattr:\n        # Save the lookup overhead in __init__ if we need to circumvent\n        # setattr hooks.\n        globs[\"_cached_setattr\"] = _obj_setattr\n\n    init = _make_method(\n        \"__attrs_init__\" if attrs_init else \"__init__\",\n        script,\n        unique_filename,\n        globs,\n    )\n    init.__annotations__ = annotations\n\n    return init\n\n\ndef _setattr(attr_name, value_var, has_on_setattr):\n    \"\"\"\n    Use the cached object.setattr to set *attr_name* to *value_var*.\n    \"\"\"\n    return \"_setattr('%s', %s)\" % (attr_name, value_var)\n\n\ndef _setattr_with_converter(attr_name, value_var, has_on_setattr):\n    \"\"\"\n    Use the cached object.setattr to set *attr_name* to *value_var*, but run\n    its converter first.\n    \"\"\"\n    return \"_setattr('%s', %s(%s))\" % (\n        attr_name,\n        _init_converter_pat % (attr_name,),\n        value_var,\n    )\n\n\ndef _assign(attr_name, value, has_on_setattr):\n    \"\"\"\n    Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise\n    relegate to _setattr.\n    \"\"\"\n    if has_on_setattr:\n        return _setattr(attr_name, value, True)\n\n    return \"self.%s = %s\" % (attr_name, value)\n\n\ndef _assign_with_converter(attr_name, value_var, has_on_setattr):\n    \"\"\"\n    Unless *attr_name* has an on_setattr hook, use normal assignment after\n    conversion. Otherwise relegate to _setattr_with_converter.\n    \"\"\"\n    if has_on_setattr:\n        return _setattr_with_converter(attr_name, value_var, True)\n\n    return \"self.%s = %s(%s)\" % (\n        attr_name,\n        _init_converter_pat % (attr_name,),\n        value_var,\n    )\n\n\nif PY2:\n\n    def _unpack_kw_only_py2(attr_name, default=None):\n        \"\"\"\n        Unpack *attr_name* from _kw_only dict.\n        \"\"\"\n        if default is not None:\n            arg_default = \", %s\" % default\n        else:\n            arg_default = \"\"\n        return \"%s = _kw_only.pop('%s'%s)\" % (\n            attr_name,\n            attr_name,\n            arg_default,\n        )\n\n    def _unpack_kw_only_lines_py2(kw_only_args):\n        \"\"\"\n        Unpack all *kw_only_args* from _kw_only dict and handle errors.\n\n        Given a list of strings \"{attr_name}\" and \"{attr_name}={default}\"\n        generates list of lines of code that pop attrs from _kw_only dict and\n        raise TypeError similar to builtin if required attr is missing or\n        extra key is passed.\n\n        >>> print(\"\\n\".join(_unpack_kw_only_lines_py2([\"a\", \"b=42\"])))\n        try:\n            a = _kw_only.pop('a')\n            b = _kw_only.pop('b', 42)\n        except KeyError as _key_error:\n            raise TypeError(\n                ...\n        if _kw_only:\n            raise TypeError(\n                ...\n        \"\"\"\n        lines = [\"try:\"]\n        lines.extend(\n            \"    \" + _unpack_kw_only_py2(*arg.split(\"=\"))\n            for arg in kw_only_args\n        )\n        lines += \"\"\"\\\nexcept KeyError as _key_error:\n    raise TypeError(\n        '__init__() missing required keyword-only argument: %s' % _key_error\n    )\nif _kw_only:\n    raise TypeError(\n        '__init__() got an unexpected keyword argument %r'\n        % next(iter(_kw_only))\n    )\n\"\"\".split(\n            \"\\n\"\n        )\n        return lines\n\n\ndef _attrs_to_init_script(\n    attrs,\n    frozen,\n    slots,\n    pre_init,\n    post_init,\n    cache_hash,\n    base_attr_map,\n    is_exc,\n    needs_cached_setattr,\n    has_global_on_setattr,\n    attrs_init,\n):\n    \"\"\"\n    Return a script of an initializer for *attrs* and a dict of globals.\n\n    The globals are expected by the generated script.\n\n    If *frozen* is True, we cannot set the attributes directly so we use\n    a cached ``object.__setattr__``.\n    \"\"\"\n    lines = []\n    if pre_init:\n        lines.append(\"self.__attrs_pre_init__()\")\n\n    if needs_cached_setattr:\n        lines.append(\n            # Circumvent the __setattr__ descriptor to save one lookup per\n            # assignment.\n            # Note _setattr will be used again below if cache_hash is True\n            \"_setattr = _cached_setattr.__get__(self, self.__class__)\"\n        )\n\n    if frozen is True:\n        if slots is True:\n            fmt_setter = _setattr\n            fmt_setter_with_converter = _setattr_with_converter\n        else:\n            # Dict frozen classes assign directly to __dict__.\n            # But only if the attribute doesn't come from an ancestor slot\n            # class.\n            # Note _inst_dict will be used again below if cache_hash is True\n            lines.append(\"_inst_dict = self.__dict__\")\n\n            def fmt_setter(attr_name, value_var, has_on_setattr):\n                if _is_slot_attr(attr_name, base_attr_map):\n                    return _setattr(attr_name, value_var, has_on_setattr)\n\n                return \"_inst_dict['%s'] = %s\" % (attr_name, value_var)\n\n            def fmt_setter_with_converter(\n                attr_name, value_var, has_on_setattr\n            ):\n                if has_on_setattr or _is_slot_attr(attr_name, base_attr_map):\n                    return _setattr_with_converter(\n                        attr_name, value_var, has_on_setattr\n                    )\n\n                return \"_inst_dict['%s'] = %s(%s)\" % (\n                    attr_name,\n                    _init_converter_pat % (attr_name,),\n                    value_var,\n                )\n\n    else:\n        # Not frozen.\n        fmt_setter = _assign\n        fmt_setter_with_converter = _assign_with_converter\n\n    args = []\n    kw_only_args = []\n    attrs_to_validate = []\n\n    # This is a dictionary of names to validator and converter callables.\n    # Injecting this into __init__ globals lets us avoid lookups.\n    names_for_globals = {}\n    annotations = {\"return\": None}\n\n    for a in attrs:\n        if a.validator:\n            attrs_to_validate.append(a)\n\n        attr_name = a.name\n        has_on_setattr = a.on_setattr is not None or (\n            a.on_setattr is not setters.NO_OP and has_global_on_setattr\n        )\n        arg_name = a.name.lstrip(\"_\")\n\n        has_factory = isinstance(a.default, Factory)\n        if has_factory and a.default.takes_self:\n            maybe_self = \"self\"\n        else:\n            maybe_self = \"\"\n\n        if a.init is False:\n            if has_factory:\n                init_factory_name = _init_factory_pat.format(a.name)\n                if a.converter is not None:\n                    lines.append(\n                        fmt_setter_with_converter(\n                            attr_name,\n                            init_factory_name + \"(%s)\" % (maybe_self,),\n                            has_on_setattr,\n                        )\n                    )\n                    conv_name = _init_converter_pat % (a.name,)\n                    names_for_globals[conv_name] = a.converter\n                else:\n                    lines.append(\n                        fmt_setter(\n                            attr_name,\n                            init_factory_name + \"(%s)\" % (maybe_self,),\n                            has_on_setattr,\n                        )\n                    )\n                names_for_globals[init_factory_name] = a.default.factory\n            else:\n                if a.converter is not None:\n                    lines.append(\n                        fmt_setter_with_converter(\n                            attr_name,\n                            \"attr_dict['%s'].default\" % (attr_name,),\n                            has_on_setattr,\n                        )\n                    )\n                    conv_name = _init_converter_pat % (a.name,)\n                    names_for_globals[conv_name] = a.converter\n                else:\n                    lines.append(\n                        fmt_setter(\n                            attr_name,\n                            \"attr_dict['%s'].default\" % (attr_name,),\n                            has_on_setattr,\n                        )\n                    )\n        elif a.default is not NOTHING and not has_factory:\n            arg = \"%s=attr_dict['%s'].default\" % (arg_name, attr_name)\n            if a.kw_only:\n                kw_only_args.append(arg)\n            else:\n                args.append(arg)\n\n            if a.converter is not None:\n                lines.append(\n                    fmt_setter_with_converter(\n                        attr_name, arg_name, has_on_setattr\n                    )\n                )\n                names_for_globals[\n                    _init_converter_pat % (a.name,)\n                ] = a.converter\n            else:\n                lines.append(fmt_setter(attr_name, arg_name, has_on_setattr))\n\n        elif has_factory:\n            arg = \"%s=NOTHING\" % (arg_name,)\n            if a.kw_only:\n                kw_only_args.append(arg)\n            else:\n                args.append(arg)\n            lines.append(\"if %s is not NOTHING:\" % (arg_name,))\n\n            init_factory_name = _init_factory_pat.format(a.name)\n            if a.converter is not None:\n                lines.append(\n                    \"    \"\n                    + fmt_setter_with_converter(\n                        attr_name, arg_name, has_on_setattr\n                    )\n                )\n                lines.append(\"else:\")\n                lines.append(\n                    \"    \"\n                    + fmt_setter_with_converter(\n                        attr_name,\n                        init_factory_name + \"(\" + maybe_self + \")\",\n                        has_on_setattr,\n                    )\n                )\n                names_for_globals[\n                    _init_converter_pat % (a.name,)\n                ] = a.converter\n            else:\n                lines.append(\n                    \"    \" + fmt_setter(attr_name, arg_name, has_on_setattr)\n                )\n                lines.append(\"else:\")\n                lines.append(\n                    \"    \"\n                    + fmt_setter(\n                        attr_name,\n                        init_factory_name + \"(\" + maybe_self + \")\",\n                        has_on_setattr,\n                    )\n                )\n            names_for_globals[init_factory_name] = a.default.factory\n        else:\n            if a.kw_only:\n                kw_only_args.append(arg_name)\n            else:\n                args.append(arg_name)\n\n            if a.converter is not None:\n                lines.append(\n                    fmt_setter_with_converter(\n                        attr_name, arg_name, has_on_setattr\n                    )\n                )\n                names_for_globals[\n                    _init_converter_pat % (a.name,)\n                ] = a.converter\n            else:\n                lines.append(fmt_setter(attr_name, arg_name, has_on_setattr))\n\n        if a.init is True:\n            if a.type is not None and a.converter is None:\n                annotations[arg_name] = a.type\n            elif a.converter is not None and not PY2:\n                # Try to get the type from the converter.\n                sig = None\n                try:\n                    sig = inspect.signature(a.converter)\n                except (ValueError, TypeError):  # inspect failed\n                    pass\n                if sig:\n                    sig_params = list(sig.parameters.values())\n                    if (\n                        sig_params\n                        and sig_params[0].annotation\n                        is not inspect.Parameter.empty\n                    ):\n                        annotations[arg_name] = sig_params[0].annotation\n\n    if attrs_to_validate:  # we can skip this if there are no validators.\n        names_for_globals[\"_config\"] = _config\n        lines.append(\"if _config._run_validators is True:\")\n        for a in attrs_to_validate:\n            val_name = \"__attr_validator_\" + a.name\n            attr_name = \"__attr_\" + a.name\n            lines.append(\n                \"    %s(self, %s, self.%s)\" % (val_name, attr_name, a.name)\n            )\n            names_for_globals[val_name] = a.validator\n            names_for_globals[attr_name] = a\n\n    if post_init:\n        lines.append(\"self.__attrs_post_init__()\")\n\n    # because this is set only after __attrs_post_init is called, a crash\n    # will result if post-init tries to access the hash code.  This seemed\n    # preferable to setting this beforehand, in which case alteration to\n    # field values during post-init combined with post-init accessing the\n    # hash code would result in silent bugs.\n    if cache_hash:\n        if frozen:\n            if slots:\n                # if frozen and slots, then _setattr defined above\n                init_hash_cache = \"_setattr('%s', %s)\"\n            else:\n                # if frozen and not slots, then _inst_dict defined above\n                init_hash_cache = \"_inst_dict['%s'] = %s\"\n        else:\n            init_hash_cache = \"self.%s = %s\"\n        lines.append(init_hash_cache % (_hash_cache_field, \"None\"))\n\n    # For exceptions we rely on BaseException.__init__ for proper\n    # initialization.\n    if is_exc:\n        vals = \",\".join(\"self.\" + a.name for a in attrs if a.init)\n\n        lines.append(\"BaseException.__init__(self, %s)\" % (vals,))\n\n    args = \", \".join(args)\n    if kw_only_args:\n        if PY2:\n            lines = _unpack_kw_only_lines_py2(kw_only_args) + lines\n\n            args += \"%s**_kw_only\" % (\", \" if args else \"\",)  # leading comma\n        else:\n            args += \"%s*, %s\" % (\n                \", \" if args else \"\",  # leading comma\n                \", \".join(kw_only_args),  # kw_only args\n            )\n    return (\n        \"\"\"\\\ndef {init_name}(self, {args}):\n    {lines}\n\"\"\".format(\n            init_name=(\"__attrs_init__\" if attrs_init else \"__init__\"),\n            args=args,\n            lines=\"\\n    \".join(lines) if lines else \"pass\",\n        ),\n        names_for_globals,\n        annotations,\n    )\n\n\nclass Attribute(object):\n    \"\"\"\n    *Read-only* representation of an attribute.\n\n    Instances of this class are frequently used for introspection purposes\n    like:\n\n    - `fields` returns a tuple of them.\n    - Validators get them passed as the first argument.\n    - The *field transformer* hook receives a list of them.\n\n    :attribute name: The name of the attribute.\n    :attribute inherited: Whether or not that attribute has been inherited from\n        a base class.\n\n    Plus *all* arguments of `attr.ib` (except for ``factory``\n    which is only syntactic sugar for ``default=Factory(...)``.\n\n    .. versionadded:: 20.1.0 *inherited*\n    .. versionadded:: 20.1.0 *on_setattr*\n    .. versionchanged:: 20.2.0 *inherited* is not taken into account for\n        equality checks and hashing anymore.\n    .. versionadded:: 21.1.0 *eq_key* and *order_key*\n\n    For the full version history of the fields, see `attr.ib`.\n    \"\"\"\n\n    __slots__ = (\n        \"name\",\n        \"default\",\n        \"validator\",\n        \"repr\",\n        \"eq\",\n        \"eq_key\",\n        \"order\",\n        \"order_key\",\n        \"hash\",\n        \"init\",\n        \"metadata\",\n        \"type\",\n        \"converter\",\n        \"kw_only\",\n        \"inherited\",\n        \"on_setattr\",\n    )\n\n    def __init__(\n        self,\n        name,\n        default,\n        validator,\n        repr,\n        cmp,  # XXX: unused, remove along with other cmp code.\n        hash,\n        init,\n        inherited,\n        metadata=None,\n        type=None,\n        converter=None,\n        kw_only=False,\n        eq=None,\n        eq_key=None,\n        order=None,\n        order_key=None,\n        on_setattr=None,\n    ):\n        eq, eq_key, order, order_key = _determine_attrib_eq_order(\n            cmp, eq_key or eq, order_key or order, True\n        )\n\n        # Cache this descriptor here to speed things up later.\n        bound_setattr = _obj_setattr.__get__(self, Attribute)\n\n        # Despite the big red warning, people *do* instantiate `Attribute`\n        # themselves.\n        bound_setattr(\"name\", name)\n        bound_setattr(\"default\", default)\n        bound_setattr(\"validator\", validator)\n        bound_setattr(\"repr\", repr)\n        bound_setattr(\"eq\", eq)\n        bound_setattr(\"eq_key\", eq_key)\n        bound_setattr(\"order\", order)\n        bound_setattr(\"order_key\", order_key)\n        bound_setattr(\"hash\", hash)\n        bound_setattr(\"init\", init)\n        bound_setattr(\"converter\", converter)\n        bound_setattr(\n            \"metadata\",\n            (\n                metadata_proxy(metadata)\n                if metadata\n                else _empty_metadata_singleton\n            ),\n        )\n        bound_setattr(\"type\", type)\n        bound_setattr(\"kw_only\", kw_only)\n        bound_setattr(\"inherited\", inherited)\n        bound_setattr(\"on_setattr\", on_setattr)\n\n    def __setattr__(self, name, value):\n        raise FrozenInstanceError()\n\n    @classmethod\n    def from_counting_attr(cls, name, ca, type=None):\n        # type holds the annotated value. deal with conflicts:\n        if type is None:\n            type = ca.type\n        elif ca.type is not None:\n            raise ValueError(\n                \"Type annotation and type argument cannot both be present\"\n            )\n        inst_dict = {\n            k: getattr(ca, k)\n            for k in Attribute.__slots__\n            if k\n            not in (\n                \"name\",\n                \"validator\",\n                \"default\",\n                \"type\",\n                \"inherited\",\n            )  # exclude methods and deprecated alias\n        }\n        return cls(\n            name=name,\n            validator=ca._validator,\n            default=ca._default,\n            type=type,\n            cmp=None,\n            inherited=False,\n            **inst_dict\n        )\n\n    @property\n    def cmp(self):\n        \"\"\"\n        Simulate the presence of a cmp attribute and warn.\n        \"\"\"\n        warnings.warn(_CMP_DEPRECATION, DeprecationWarning, stacklevel=2)\n\n        return self.eq and self.order\n\n    # Don't use attr.evolve since fields(Attribute) doesn't work\n    def evolve(self, **changes):\n        \"\"\"\n        Copy *self* and apply *changes*.\n\n        This works similarly to `attr.evolve` but that function does not work\n        with ``Attribute``.\n\n        It is mainly meant to be used for `transform-fields`.\n\n        .. versionadded:: 20.3.0\n        \"\"\"\n        new = copy.copy(self)\n\n        new._setattrs(changes.items())\n\n        return new\n\n    # Don't use _add_pickle since fields(Attribute) doesn't work\n    def __getstate__(self):\n        \"\"\"\n        Play nice with pickle.\n        \"\"\"\n        return tuple(\n            getattr(self, name) if name != \"metadata\" else dict(self.metadata)\n            for name in self.__slots__\n        )\n\n    def __setstate__(self, state):\n        \"\"\"\n        Play nice with pickle.\n        \"\"\"\n        self._setattrs(zip(self.__slots__, state))\n\n    def _setattrs(self, name_values_pairs):\n        bound_setattr = _obj_setattr.__get__(self, Attribute)\n        for name, value in name_values_pairs:\n            if name != \"metadata\":\n                bound_setattr(name, value)\n            else:\n                bound_setattr(\n                    name,\n                    metadata_proxy(value)\n                    if value\n                    else _empty_metadata_singleton,\n                )\n\n\n_a = [\n    Attribute(\n        name=name,\n        default=NOTHING,\n        validator=None,\n        repr=True,\n        cmp=None,\n        eq=True,\n        order=False,\n        hash=(name != \"metadata\"),\n        init=True,\n        inherited=False,\n    )\n    for name in Attribute.__slots__\n]\n\nAttribute = _add_hash(\n    _add_eq(\n        _add_repr(Attribute, attrs=_a),\n        attrs=[a for a in _a if a.name != \"inherited\"],\n    ),\n    attrs=[a for a in _a if a.hash and a.name != \"inherited\"],\n)\n\n\nclass _CountingAttr(object):\n    \"\"\"\n    Intermediate representation of attributes that uses a counter to preserve\n    the order in which the attributes have been defined.\n\n    *Internal* data structure of the attrs library.  Running into is most\n    likely the result of a bug like a forgotten `@attr.s` decorator.\n    \"\"\"\n\n    __slots__ = (\n        \"counter\",\n        \"_default\",\n        \"repr\",\n        \"eq\",\n        \"eq_key\",\n        \"order\",\n        \"order_key\",\n        \"hash\",\n        \"init\",\n        \"metadata\",\n        \"_validator\",\n        \"converter\",\n        \"type\",\n        \"kw_only\",\n        \"on_setattr\",\n    )\n    __attrs_attrs__ = tuple(\n        Attribute(\n            name=name,\n            default=NOTHING,\n            validator=None,\n            repr=True,\n            cmp=None,\n            hash=True,\n            init=True,\n            kw_only=False,\n            eq=True,\n            eq_key=None,\n            order=False,\n            order_key=None,\n            inherited=False,\n            on_setattr=None,\n        )\n        for name in (\n            \"counter\",\n            \"_default\",\n            \"repr\",\n            \"eq\",\n            \"order\",\n            \"hash\",\n            \"init\",\n            \"on_setattr\",\n        )\n    ) + (\n        Attribute(\n            name=\"metadata\",\n            default=None,\n            validator=None,\n            repr=True,\n            cmp=None,\n            hash=False,\n            init=True,\n            kw_only=False,\n            eq=True,\n            eq_key=None,\n            order=False,\n            order_key=None,\n            inherited=False,\n            on_setattr=None,\n        ),\n    )\n    cls_counter = 0\n\n    def __init__(\n        self,\n        default,\n        validator,\n        repr,\n        cmp,\n        hash,\n        init,\n        converter,\n        metadata,\n        type,\n        kw_only,\n        eq,\n        eq_key,\n        order,\n        order_key,\n        on_setattr,\n    ):\n        _CountingAttr.cls_counter += 1\n        self.counter = _CountingAttr.cls_counter\n        self._default = default\n        self._validator = validator\n        self.converter = converter\n        self.repr = repr\n        self.eq = eq\n        self.eq_key = eq_key\n        self.order = order\n        self.order_key = order_key\n        self.hash = hash\n        self.init = init\n        self.metadata = metadata\n        self.type = type\n        self.kw_only = kw_only\n        self.on_setattr = on_setattr\n\n    def validator(self, meth):\n        \"\"\"\n        Decorator that adds *meth* to the list of validators.\n\n        Returns *meth* unchanged.\n\n        .. versionadded:: 17.1.0\n        \"\"\"\n        if self._validator is None:\n            self._validator = meth\n        else:\n            self._validator = and_(self._validator, meth)\n        return meth\n\n    def default(self, meth):\n        \"\"\"\n        Decorator that allows to set the default for an attribute.\n\n        Returns *meth* unchanged.\n\n        :raises DefaultAlreadySetError: If default has been set before.\n\n        .. versionadded:: 17.1.0\n        \"\"\"\n        if self._default is not NOTHING:\n            raise DefaultAlreadySetError()\n\n        self._default = Factory(meth, takes_self=True)\n\n        return meth\n\n\n_CountingAttr = _add_eq(_add_repr(_CountingAttr))\n\n\nclass Factory(object):\n    \"\"\"\n    Stores a factory callable.\n\n    If passed as the default value to `attr.ib`, the factory is used to\n    generate a new value.\n\n    :param callable factory: A callable that takes either none or exactly one\n        mandatory positional argument depending on *takes_self*.\n    :param bool takes_self: Pass the partially initialized instance that is\n        being initialized as a positional argument.\n\n    .. versionadded:: 17.1.0  *takes_self*\n    \"\"\"\n\n    __slots__ = (\"factory\", \"takes_self\")\n\n    def __init__(self, factory, takes_self=False):\n        \"\"\"\n        `Factory` is part of the default machinery so if we want a default\n        value here, we have to implement it ourselves.\n        \"\"\"\n        self.factory = factory\n        self.takes_self = takes_self\n\n    def __getstate__(self):\n        \"\"\"\n        Play nice with pickle.\n        \"\"\"\n        return tuple(getattr(self, name) for name in self.__slots__)\n\n    def __setstate__(self, state):\n        \"\"\"\n        Play nice with pickle.\n        \"\"\"\n        for name, value in zip(self.__slots__, state):\n            setattr(self, name, value)\n\n\n_f = [\n    Attribute(\n        name=name,\n        default=NOTHING,\n        validator=None,\n        repr=True,\n        cmp=None,\n        eq=True,\n        order=False,\n        hash=True,\n        init=True,\n        inherited=False,\n    )\n    for name in Factory.__slots__\n]\n\nFactory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f)\n\n\ndef make_class(name, attrs, bases=(object,), **attributes_arguments):\n    \"\"\"\n    A quick way to create a new class called *name* with *attrs*.\n\n    :param str name: The name for the new class.\n\n    :param attrs: A list of names or a dictionary of mappings of names to\n        attributes.\n\n        If *attrs* is a list or an ordered dict (`dict` on Python 3.6+,\n        `collections.OrderedDict` otherwise), the order is deduced from\n        the order of the names or attributes inside *attrs*.  Otherwise the\n        order of the definition of the attributes is used.\n    :type attrs: `list` or `dict`\n\n    :param tuple bases: Classes that the new class will subclass.\n\n    :param attributes_arguments: Passed unmodified to `attr.s`.\n\n    :return: A new class with *attrs*.\n    :rtype: type\n\n    .. versionadded:: 17.1.0 *bases*\n    .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained.\n    \"\"\"\n    if isinstance(attrs, dict):\n        cls_dict = attrs\n    elif isinstance(attrs, (list, tuple)):\n        cls_dict = dict((a, attrib()) for a in attrs)\n    else:\n        raise TypeError(\"attrs argument must be a dict or a list.\")\n\n    pre_init = cls_dict.pop(\"__attrs_pre_init__\", None)\n    post_init = cls_dict.pop(\"__attrs_post_init__\", None)\n    user_init = cls_dict.pop(\"__init__\", None)\n\n    body = {}\n    if pre_init is not None:\n        body[\"__attrs_pre_init__\"] = pre_init\n    if post_init is not None:\n        body[\"__attrs_post_init__\"] = post_init\n    if user_init is not None:\n        body[\"__init__\"] = user_init\n\n    type_ = new_class(name, bases, {}, lambda ns: ns.update(body))\n\n    # For pickling to work, the __module__ variable needs to be set to the\n    # frame where the class is created.  Bypass this step in environments where\n    # sys._getframe is not defined (Jython for example) or sys._getframe is not\n    # defined for arguments greater than 0 (IronPython).\n    try:\n        type_.__module__ = sys._getframe(1).f_globals.get(\n            \"__name__\", \"__main__\"\n        )\n    except (AttributeError, ValueError):\n        pass\n\n    # We do it here for proper warnings with meaningful stacklevel.\n    cmp = attributes_arguments.pop(\"cmp\", None)\n    (\n        attributes_arguments[\"eq\"],\n        attributes_arguments[\"order\"],\n    ) = _determine_attrs_eq_order(\n        cmp,\n        attributes_arguments.get(\"eq\"),\n        attributes_arguments.get(\"order\"),\n        True,\n    )\n\n    return _attrs(these=cls_dict, **attributes_arguments)(type_)\n\n\n# These are required by within this module so we define them here and merely\n# import into .validators / .converters.\n\n\n@attrs(slots=True, hash=True)\nclass _AndValidator(object):\n    \"\"\"\n    Compose many validators to a single one.\n    \"\"\"\n\n    _validators = attrib()\n\n    def __call__(self, inst, attr, value):\n        for v in self._validators:\n            v(inst, attr, value)\n\n\ndef and_(*validators):\n    \"\"\"\n    A validator that composes multiple validators into one.\n\n    When called on a value, it runs all wrapped validators.\n\n    :param callables validators: Arbitrary number of validators.\n\n    .. versionadded:: 17.1.0\n    \"\"\"\n    vals = []\n    for validator in validators:\n        vals.extend(\n            validator._validators\n            if isinstance(validator, _AndValidator)\n            else [validator]\n        )\n\n    return _AndValidator(tuple(vals))\n\n\ndef pipe(*converters):\n    \"\"\"\n    A converter that composes multiple converters into one.\n\n    When called on a value, it runs all wrapped converters, returning the\n    *last* value.\n\n    Type annotations will be inferred from the wrapped converters', if\n    they have any.\n\n    :param callables converters: Arbitrary number of converters.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n\n    def pipe_converter(val):\n        for converter in converters:\n            val = converter(val)\n\n        return val\n\n    if not PY2:\n        if not converters:\n            # If the converter list is empty, pipe_converter is the identity.\n            A = typing.TypeVar(\"A\")\n            pipe_converter.__annotations__ = {\"val\": A, \"return\": A}\n        else:\n            # Get parameter type.\n            sig = None\n            try:\n                sig = inspect.signature(converters[0])\n            except (ValueError, TypeError):  # inspect failed\n                pass\n            if sig:\n                params = list(sig.parameters.values())\n                if (\n                    params\n                    and params[0].annotation is not inspect.Parameter.empty\n                ):\n                    pipe_converter.__annotations__[\"val\"] = params[\n                        0\n                    ].annotation\n            # Get return type.\n            sig = None\n            try:\n                sig = inspect.signature(converters[-1])\n            except (ValueError, TypeError):  # inspect failed\n                pass\n            if sig and sig.return_annotation is not inspect.Signature().empty:\n                pipe_converter.__annotations__[\n                    \"return\"\n                ] = sig.return_annotation\n\n    return pipe_converter\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/_next_gen.py",
    "content": "\"\"\"\nThese are Python 3.6+-only and keyword-only APIs that call `attr.s` and\n`attr.ib` with different default values.\n\"\"\"\n\nfrom functools import partial\n\nfrom attr.exceptions import UnannotatedAttributeError\n\nfrom . import setters\nfrom ._make import NOTHING, _frozen_setattrs, attrib, attrs\n\n\ndef define(\n    maybe_cls=None,\n    *,\n    these=None,\n    repr=None,\n    hash=None,\n    init=None,\n    slots=True,\n    frozen=False,\n    weakref_slot=True,\n    str=False,\n    auto_attribs=None,\n    kw_only=False,\n    cache_hash=False,\n    auto_exc=True,\n    eq=None,\n    order=False,\n    auto_detect=True,\n    getstate_setstate=None,\n    on_setattr=None,\n    field_transformer=None,\n):\n    r\"\"\"\n    The only behavioral differences are the handling of the *auto_attribs*\n    option:\n\n    :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves\n       exactly like `attr.s`. If left `None`, `attr.s` will try to guess:\n\n       1. If any attributes are annotated and no unannotated `attr.ib`\\ s\n          are found, it assumes *auto_attribs=True*.\n       2. Otherwise it assumes *auto_attribs=False* and tries to collect\n          `attr.ib`\\ s.\n\n    and that mutable classes (``frozen=False``) validate on ``__setattr__``.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n\n    def do_it(cls, auto_attribs):\n        return attrs(\n            maybe_cls=cls,\n            these=these,\n            repr=repr,\n            hash=hash,\n            init=init,\n            slots=slots,\n            frozen=frozen,\n            weakref_slot=weakref_slot,\n            str=str,\n            auto_attribs=auto_attribs,\n            kw_only=kw_only,\n            cache_hash=cache_hash,\n            auto_exc=auto_exc,\n            eq=eq,\n            order=order,\n            auto_detect=auto_detect,\n            collect_by_mro=True,\n            getstate_setstate=getstate_setstate,\n            on_setattr=on_setattr,\n            field_transformer=field_transformer,\n        )\n\n    def wrap(cls):\n        \"\"\"\n        Making this a wrapper ensures this code runs during class creation.\n\n        We also ensure that frozen-ness of classes is inherited.\n        \"\"\"\n        nonlocal frozen, on_setattr\n\n        had_on_setattr = on_setattr not in (None, setters.NO_OP)\n\n        # By default, mutable classes validate on setattr.\n        if frozen is False and on_setattr is None:\n            on_setattr = setters.validate\n\n        # However, if we subclass a frozen class, we inherit the immutability\n        # and disable on_setattr.\n        for base_cls in cls.__bases__:\n            if base_cls.__setattr__ is _frozen_setattrs:\n                if had_on_setattr:\n                    raise ValueError(\n                        \"Frozen classes can't use on_setattr \"\n                        \"(frozen-ness was inherited).\"\n                    )\n\n                on_setattr = setters.NO_OP\n                break\n\n        if auto_attribs is not None:\n            return do_it(cls, auto_attribs)\n\n        try:\n            return do_it(cls, True)\n        except UnannotatedAttributeError:\n            return do_it(cls, False)\n\n    # maybe_cls's type depends on the usage of the decorator.  It's a class\n    # if it's used as `@attrs` but ``None`` if used as `@attrs()`.\n    if maybe_cls is None:\n        return wrap\n    else:\n        return wrap(maybe_cls)\n\n\nmutable = define\nfrozen = partial(define, frozen=True, on_setattr=None)\n\n\ndef field(\n    *,\n    default=NOTHING,\n    validator=None,\n    repr=True,\n    hash=None,\n    init=True,\n    metadata=None,\n    converter=None,\n    factory=None,\n    kw_only=False,\n    eq=None,\n    order=None,\n    on_setattr=None,\n):\n    \"\"\"\n    Identical to `attr.ib`, except keyword-only and with some arguments\n    removed.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n    return attrib(\n        default=default,\n        validator=validator,\n        repr=repr,\n        hash=hash,\n        init=init,\n        metadata=metadata,\n        converter=converter,\n        factory=factory,\n        kw_only=kw_only,\n        eq=eq,\n        order=order,\n        on_setattr=on_setattr,\n    )\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/_version_info.py",
    "content": "from __future__ import absolute_import, division, print_function\n\nfrom functools import total_ordering\n\nfrom ._funcs import astuple\nfrom ._make import attrib, attrs\n\n\n@total_ordering\n@attrs(eq=False, order=False, slots=True, frozen=True)\nclass VersionInfo(object):\n    \"\"\"\n    A version object that can be compared to tuple of length 1--4:\n\n    >>> attr.VersionInfo(19, 1, 0, \"final\")  <= (19, 2)\n    True\n    >>> attr.VersionInfo(19, 1, 0, \"final\") < (19, 1, 1)\n    True\n    >>> vi = attr.VersionInfo(19, 2, 0, \"final\")\n    >>> vi < (19, 1, 1)\n    False\n    >>> vi < (19,)\n    False\n    >>> vi == (19, 2,)\n    True\n    >>> vi == (19, 2, 1)\n    False\n\n    .. versionadded:: 19.2\n    \"\"\"\n\n    year = attrib(type=int)\n    minor = attrib(type=int)\n    micro = attrib(type=int)\n    releaselevel = attrib(type=str)\n\n    @classmethod\n    def _from_version_string(cls, s):\n        \"\"\"\n        Parse *s* and return a _VersionInfo.\n        \"\"\"\n        v = s.split(\".\")\n        if len(v) == 3:\n            v.append(\"final\")\n\n        return cls(\n            year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3]\n        )\n\n    def _ensure_tuple(self, other):\n        \"\"\"\n        Ensure *other* is a tuple of a valid length.\n\n        Returns a possibly transformed *other* and ourselves as a tuple of\n        the same length as *other*.\n        \"\"\"\n\n        if self.__class__ is other.__class__:\n            other = astuple(other)\n\n        if not isinstance(other, tuple):\n            raise NotImplementedError\n\n        if not (1 <= len(other) <= 4):\n            raise NotImplementedError\n\n        return astuple(self)[: len(other)], other\n\n    def __eq__(self, other):\n        try:\n            us, them = self._ensure_tuple(other)\n        except NotImplementedError:\n            return NotImplemented\n\n        return us == them\n\n    def __lt__(self, other):\n        try:\n            us, them = self._ensure_tuple(other)\n        except NotImplementedError:\n            return NotImplemented\n\n        # Since alphabetically \"dev0\" < \"final\" < \"post1\" < \"post2\", we don't\n        # have to do anything special with releaselevel for now.\n        return us < them\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/_version_info.pyi",
    "content": "class VersionInfo:\n    @property\n    def year(self) -> int: ...\n    @property\n    def minor(self) -> int: ...\n    @property\n    def micro(self) -> int: ...\n    @property\n    def releaselevel(self) -> str: ...\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/converters.py",
    "content": "\"\"\"\nCommonly useful converters.\n\"\"\"\n\nfrom __future__ import absolute_import, division, print_function\n\nfrom ._compat import PY2\nfrom ._make import NOTHING, Factory, pipe\n\n\nif not PY2:\n    import inspect\n    import typing\n\n\n__all__ = [\n    \"pipe\",\n    \"optional\",\n    \"default_if_none\",\n]\n\n\ndef optional(converter):\n    \"\"\"\n    A converter that allows an attribute to be optional. An optional attribute\n    is one which can be set to ``None``.\n\n    Type annotations will be inferred from the wrapped converter's, if it\n    has any.\n\n    :param callable converter: the converter that is used for non-``None``\n        values.\n\n    .. versionadded:: 17.1.0\n    \"\"\"\n\n    def optional_converter(val):\n        if val is None:\n            return None\n        return converter(val)\n\n    if not PY2:\n        sig = None\n        try:\n            sig = inspect.signature(converter)\n        except (ValueError, TypeError):  # inspect failed\n            pass\n        if sig:\n            params = list(sig.parameters.values())\n            if params and params[0].annotation is not inspect.Parameter.empty:\n                optional_converter.__annotations__[\"val\"] = typing.Optional[\n                    params[0].annotation\n                ]\n            if sig.return_annotation is not inspect.Signature.empty:\n                optional_converter.__annotations__[\"return\"] = typing.Optional[\n                    sig.return_annotation\n                ]\n\n    return optional_converter\n\n\ndef default_if_none(default=NOTHING, factory=None):\n    \"\"\"\n    A converter that allows to replace ``None`` values by *default* or the\n    result of *factory*.\n\n    :param default: Value to be used if ``None`` is passed. Passing an instance\n       of `attr.Factory` is supported, however the ``takes_self`` option\n       is *not*.\n    :param callable factory: A callable that takes no parameters whose result\n       is used if ``None`` is passed.\n\n    :raises TypeError: If **neither** *default* or *factory* is passed.\n    :raises TypeError: If **both** *default* and *factory* are passed.\n    :raises ValueError: If an instance of `attr.Factory` is passed with\n       ``takes_self=True``.\n\n    .. versionadded:: 18.2.0\n    \"\"\"\n    if default is NOTHING and factory is None:\n        raise TypeError(\"Must pass either `default` or `factory`.\")\n\n    if default is not NOTHING and factory is not None:\n        raise TypeError(\n            \"Must pass either `default` or `factory` but not both.\"\n        )\n\n    if factory is not None:\n        default = Factory(factory)\n\n    if isinstance(default, Factory):\n        if default.takes_self:\n            raise ValueError(\n                \"`takes_self` is not supported by default_if_none.\"\n            )\n\n        def default_if_none_converter(val):\n            if val is not None:\n                return val\n\n            return default.factory()\n\n    else:\n\n        def default_if_none_converter(val):\n            if val is not None:\n                return val\n\n            return default\n\n    return default_if_none_converter\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/converters.pyi",
    "content": "from typing import Callable, Optional, TypeVar, overload\n\nfrom . import _ConverterType\n\n\n_T = TypeVar(\"_T\")\n\ndef pipe(*validators: _ConverterType) -> _ConverterType: ...\ndef optional(converter: _ConverterType) -> _ConverterType: ...\n@overload\ndef default_if_none(default: _T) -> _ConverterType: ...\n@overload\ndef default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ...\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/exceptions.py",
    "content": "from __future__ import absolute_import, division, print_function\n\n\nclass FrozenError(AttributeError):\n    \"\"\"\n    A frozen/immutable instance or attribute have been attempted to be\n    modified.\n\n    It mirrors the behavior of ``namedtuples`` by using the same error message\n    and subclassing `AttributeError`.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n\n    msg = \"can't set attribute\"\n    args = [msg]\n\n\nclass FrozenInstanceError(FrozenError):\n    \"\"\"\n    A frozen instance has been attempted to be modified.\n\n    .. versionadded:: 16.1.0\n    \"\"\"\n\n\nclass FrozenAttributeError(FrozenError):\n    \"\"\"\n    A frozen attribute has been attempted to be modified.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n\n\nclass AttrsAttributeNotFoundError(ValueError):\n    \"\"\"\n    An ``attrs`` function couldn't find an attribute that the user asked for.\n\n    .. versionadded:: 16.2.0\n    \"\"\"\n\n\nclass NotAnAttrsClassError(ValueError):\n    \"\"\"\n    A non-``attrs`` class has been passed into an ``attrs`` function.\n\n    .. versionadded:: 16.2.0\n    \"\"\"\n\n\nclass DefaultAlreadySetError(RuntimeError):\n    \"\"\"\n    A default has been set using ``attr.ib()`` and is attempted to be reset\n    using the decorator.\n\n    .. versionadded:: 17.1.0\n    \"\"\"\n\n\nclass UnannotatedAttributeError(RuntimeError):\n    \"\"\"\n    A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type\n    annotation.\n\n    .. versionadded:: 17.3.0\n    \"\"\"\n\n\nclass PythonTooOldError(RuntimeError):\n    \"\"\"\n    It was attempted to use an ``attrs`` feature that requires a newer Python\n    version.\n\n    .. versionadded:: 18.2.0\n    \"\"\"\n\n\nclass NotCallableError(TypeError):\n    \"\"\"\n    A ``attr.ib()`` requiring a callable has been set with a value\n    that is not callable.\n\n    .. versionadded:: 19.2.0\n    \"\"\"\n\n    def __init__(self, msg, value):\n        super(TypeError, self).__init__(msg, value)\n        self.msg = msg\n        self.value = value\n\n    def __str__(self):\n        return str(self.msg)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/exceptions.pyi",
    "content": "from typing import Any\n\n\nclass FrozenError(AttributeError):\n    msg: str = ...\n\nclass FrozenInstanceError(FrozenError): ...\nclass FrozenAttributeError(FrozenError): ...\nclass AttrsAttributeNotFoundError(ValueError): ...\nclass NotAnAttrsClassError(ValueError): ...\nclass DefaultAlreadySetError(RuntimeError): ...\nclass UnannotatedAttributeError(RuntimeError): ...\nclass PythonTooOldError(RuntimeError): ...\n\nclass NotCallableError(TypeError):\n    msg: str = ...\n    value: Any = ...\n    def __init__(self, msg: str, value: Any) -> None: ...\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/filters.py",
    "content": "\"\"\"\nCommonly useful filters for `attr.asdict`.\n\"\"\"\n\nfrom __future__ import absolute_import, division, print_function\n\nfrom ._compat import isclass\nfrom ._make import Attribute\n\n\ndef _split_what(what):\n    \"\"\"\n    Returns a tuple of `frozenset`s of classes and attributes.\n    \"\"\"\n    return (\n        frozenset(cls for cls in what if isclass(cls)),\n        frozenset(cls for cls in what if isinstance(cls, Attribute)),\n    )\n\n\ndef include(*what):\n    \"\"\"\n    Whitelist *what*.\n\n    :param what: What to whitelist.\n    :type what: `list` of `type` or `attr.Attribute`\\\\ s\n\n    :rtype: `callable`\n    \"\"\"\n    cls, attrs = _split_what(what)\n\n    def include_(attribute, value):\n        return value.__class__ in cls or attribute in attrs\n\n    return include_\n\n\ndef exclude(*what):\n    \"\"\"\n    Blacklist *what*.\n\n    :param what: What to blacklist.\n    :type what: `list` of classes or `attr.Attribute`\\\\ s.\n\n    :rtype: `callable`\n    \"\"\"\n    cls, attrs = _split_what(what)\n\n    def exclude_(attribute, value):\n        return value.__class__ not in cls and attribute not in attrs\n\n    return exclude_\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/filters.pyi",
    "content": "from typing import Any, Union\n\nfrom . import Attribute, _FilterType\n\n\ndef include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ...\ndef exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ...\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/py.typed",
    "content": ""
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/setters.py",
    "content": "\"\"\"\nCommonly used hooks for on_setattr.\n\"\"\"\n\nfrom __future__ import absolute_import, division, print_function\n\nfrom . import _config\nfrom .exceptions import FrozenAttributeError\n\n\ndef pipe(*setters):\n    \"\"\"\n    Run all *setters* and return the return value of the last one.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n\n    def wrapped_pipe(instance, attrib, new_value):\n        rv = new_value\n\n        for setter in setters:\n            rv = setter(instance, attrib, rv)\n\n        return rv\n\n    return wrapped_pipe\n\n\ndef frozen(_, __, ___):\n    \"\"\"\n    Prevent an attribute to be modified.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n    raise FrozenAttributeError()\n\n\ndef validate(instance, attrib, new_value):\n    \"\"\"\n    Run *attrib*'s validator on *new_value* if it has one.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n    if _config._run_validators is False:\n        return new_value\n\n    v = attrib.validator\n    if not v:\n        return new_value\n\n    v(instance, attrib, new_value)\n\n    return new_value\n\n\ndef convert(instance, attrib, new_value):\n    \"\"\"\n    Run *attrib*'s converter -- if it has one --  on *new_value* and return the\n    result.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n    c = attrib.converter\n    if c:\n        return c(new_value)\n\n    return new_value\n\n\nNO_OP = object()\n\"\"\"\nSentinel for disabling class-wide *on_setattr* hooks for certain attributes.\n\nDoes not work in `pipe` or within lists.\n\n.. versionadded:: 20.1.0\n\"\"\"\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/setters.pyi",
    "content": "from typing import Any, NewType, NoReturn, TypeVar, cast\n\nfrom . import Attribute, _OnSetAttrType\n\n\n_T = TypeVar(\"_T\")\n\ndef frozen(\n    instance: Any, attribute: Attribute[Any], new_value: Any\n) -> NoReturn: ...\ndef pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ...\ndef validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ...\n\n# convert is allowed to return Any, because they can be chained using pipe.\ndef convert(\n    instance: Any, attribute: Attribute[Any], new_value: Any\n) -> Any: ...\n\n_NoOpType = NewType(\"_NoOpType\", object)\nNO_OP: _NoOpType\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/validators.py",
    "content": "\"\"\"\nCommonly useful validators.\n\"\"\"\n\nfrom __future__ import absolute_import, division, print_function\n\nimport re\n\nfrom ._make import _AndValidator, and_, attrib, attrs\nfrom .exceptions import NotCallableError\n\n\n__all__ = [\n    \"and_\",\n    \"deep_iterable\",\n    \"deep_mapping\",\n    \"in_\",\n    \"instance_of\",\n    \"is_callable\",\n    \"matches_re\",\n    \"optional\",\n    \"provides\",\n]\n\n\n@attrs(repr=False, slots=True, hash=True)\nclass _InstanceOfValidator(object):\n    type = attrib()\n\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if not isinstance(value, self.type):\n            raise TypeError(\n                \"'{name}' must be {type!r} (got {value!r} that is a \"\n                \"{actual!r}).\".format(\n                    name=attr.name,\n                    type=self.type,\n                    actual=value.__class__,\n                    value=value,\n                ),\n                attr,\n                self.type,\n                value,\n            )\n\n    def __repr__(self):\n        return \"<instance_of validator for type {type!r}>\".format(\n            type=self.type\n        )\n\n\ndef instance_of(type):\n    \"\"\"\n    A validator that raises a `TypeError` if the initializer is called\n    with a wrong type for this particular attribute (checks are performed using\n    `isinstance` therefore it's also valid to pass a tuple of types).\n\n    :param type: The type to check for.\n    :type type: type or tuple of types\n\n    :raises TypeError: With a human readable error message, the attribute\n        (of type `attr.Attribute`), the expected type, and the value it\n        got.\n    \"\"\"\n    return _InstanceOfValidator(type)\n\n\n@attrs(repr=False, frozen=True, slots=True)\nclass _MatchesReValidator(object):\n    regex = attrib()\n    flags = attrib()\n    match_func = attrib()\n\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if not self.match_func(value):\n            raise ValueError(\n                \"'{name}' must match regex {regex!r}\"\n                \" ({value!r} doesn't)\".format(\n                    name=attr.name, regex=self.regex.pattern, value=value\n                ),\n                attr,\n                self.regex,\n                value,\n            )\n\n    def __repr__(self):\n        return \"<matches_re validator for pattern {regex!r}>\".format(\n            regex=self.regex\n        )\n\n\ndef matches_re(regex, flags=0, func=None):\n    r\"\"\"\n    A validator that raises `ValueError` if the initializer is called\n    with a string that doesn't match *regex*.\n\n    :param str regex: a regex string to match against\n    :param int flags: flags that will be passed to the underlying re function\n        (default 0)\n    :param callable func: which underlying `re` function to call (options\n        are `re.fullmatch`, `re.search`, `re.match`, default\n        is ``None`` which means either `re.fullmatch` or an emulation of\n        it on Python 2). For performance reasons, they won't be used directly\n        but on a pre-`re.compile`\\ ed pattern.\n\n    .. versionadded:: 19.2.0\n    \"\"\"\n    fullmatch = getattr(re, \"fullmatch\", None)\n    valid_funcs = (fullmatch, None, re.search, re.match)\n    if func not in valid_funcs:\n        raise ValueError(\n            \"'func' must be one of %s.\"\n            % (\n                \", \".join(\n                    sorted(\n                        e and e.__name__ or \"None\" for e in set(valid_funcs)\n                    )\n                ),\n            )\n        )\n\n    pattern = re.compile(regex, flags)\n    if func is re.match:\n        match_func = pattern.match\n    elif func is re.search:\n        match_func = pattern.search\n    else:\n        if fullmatch:\n            match_func = pattern.fullmatch\n        else:\n            pattern = re.compile(r\"(?:{})\\Z\".format(regex), flags)\n            match_func = pattern.match\n\n    return _MatchesReValidator(pattern, flags, match_func)\n\n\n@attrs(repr=False, slots=True, hash=True)\nclass _ProvidesValidator(object):\n    interface = attrib()\n\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if not self.interface.providedBy(value):\n            raise TypeError(\n                \"'{name}' must provide {interface!r} which {value!r} \"\n                \"doesn't.\".format(\n                    name=attr.name, interface=self.interface, value=value\n                ),\n                attr,\n                self.interface,\n                value,\n            )\n\n    def __repr__(self):\n        return \"<provides validator for interface {interface!r}>\".format(\n            interface=self.interface\n        )\n\n\ndef provides(interface):\n    \"\"\"\n    A validator that raises a `TypeError` if the initializer is called\n    with an object that does not provide the requested *interface* (checks are\n    performed using ``interface.providedBy(value)`` (see `zope.interface\n    <https://zopeinterface.readthedocs.io/en/latest/>`_).\n\n    :param interface: The interface to check for.\n    :type interface: ``zope.interface.Interface``\n\n    :raises TypeError: With a human readable error message, the attribute\n        (of type `attr.Attribute`), the expected interface, and the\n        value it got.\n    \"\"\"\n    return _ProvidesValidator(interface)\n\n\n@attrs(repr=False, slots=True, hash=True)\nclass _OptionalValidator(object):\n    validator = attrib()\n\n    def __call__(self, inst, attr, value):\n        if value is None:\n            return\n\n        self.validator(inst, attr, value)\n\n    def __repr__(self):\n        return \"<optional validator for {what} or None>\".format(\n            what=repr(self.validator)\n        )\n\n\ndef optional(validator):\n    \"\"\"\n    A validator that makes an attribute optional.  An optional attribute is one\n    which can be set to ``None`` in addition to satisfying the requirements of\n    the sub-validator.\n\n    :param validator: A validator (or a list of validators) that is used for\n        non-``None`` values.\n    :type validator: callable or `list` of callables.\n\n    .. versionadded:: 15.1.0\n    .. versionchanged:: 17.1.0 *validator* can be a list of validators.\n    \"\"\"\n    if isinstance(validator, list):\n        return _OptionalValidator(_AndValidator(validator))\n    return _OptionalValidator(validator)\n\n\n@attrs(repr=False, slots=True, hash=True)\nclass _InValidator(object):\n    options = attrib()\n\n    def __call__(self, inst, attr, value):\n        try:\n            in_options = value in self.options\n        except TypeError:  # e.g. `1 in \"abc\"`\n            in_options = False\n\n        if not in_options:\n            raise ValueError(\n                \"'{name}' must be in {options!r} (got {value!r})\".format(\n                    name=attr.name, options=self.options, value=value\n                )\n            )\n\n    def __repr__(self):\n        return \"<in_ validator with options {options!r}>\".format(\n            options=self.options\n        )\n\n\ndef in_(options):\n    \"\"\"\n    A validator that raises a `ValueError` if the initializer is called\n    with a value that does not belong in the options provided.  The check is\n    performed using ``value in options``.\n\n    :param options: Allowed options.\n    :type options: list, tuple, `enum.Enum`, ...\n\n    :raises ValueError: With a human readable error message, the attribute (of\n       type `attr.Attribute`), the expected options, and the value it\n       got.\n\n    .. versionadded:: 17.1.0\n    \"\"\"\n    return _InValidator(options)\n\n\n@attrs(repr=False, slots=False, hash=True)\nclass _IsCallableValidator(object):\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if not callable(value):\n            message = (\n                \"'{name}' must be callable \"\n                \"(got {value!r} that is a {actual!r}).\"\n            )\n            raise NotCallableError(\n                msg=message.format(\n                    name=attr.name, value=value, actual=value.__class__\n                ),\n                value=value,\n            )\n\n    def __repr__(self):\n        return \"<is_callable validator>\"\n\n\ndef is_callable():\n    \"\"\"\n    A validator that raises a `attr.exceptions.NotCallableError` if the\n    initializer is called with a value for this particular attribute\n    that is not callable.\n\n    .. versionadded:: 19.1.0\n\n    :raises `attr.exceptions.NotCallableError`: With a human readable error\n        message containing the attribute (`attr.Attribute`) name,\n        and the value it got.\n    \"\"\"\n    return _IsCallableValidator()\n\n\n@attrs(repr=False, slots=True, hash=True)\nclass _DeepIterable(object):\n    member_validator = attrib(validator=is_callable())\n    iterable_validator = attrib(\n        default=None, validator=optional(is_callable())\n    )\n\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if self.iterable_validator is not None:\n            self.iterable_validator(inst, attr, value)\n\n        for member in value:\n            self.member_validator(inst, attr, member)\n\n    def __repr__(self):\n        iterable_identifier = (\n            \"\"\n            if self.iterable_validator is None\n            else \" {iterable!r}\".format(iterable=self.iterable_validator)\n        )\n        return (\n            \"<deep_iterable validator for{iterable_identifier}\"\n            \" iterables of {member!r}>\"\n        ).format(\n            iterable_identifier=iterable_identifier,\n            member=self.member_validator,\n        )\n\n\ndef deep_iterable(member_validator, iterable_validator=None):\n    \"\"\"\n    A validator that performs deep validation of an iterable.\n\n    :param member_validator: Validator to apply to iterable members\n    :param iterable_validator: Validator to apply to iterable itself\n        (optional)\n\n    .. versionadded:: 19.1.0\n\n    :raises TypeError: if any sub-validators fail\n    \"\"\"\n    return _DeepIterable(member_validator, iterable_validator)\n\n\n@attrs(repr=False, slots=True, hash=True)\nclass _DeepMapping(object):\n    key_validator = attrib(validator=is_callable())\n    value_validator = attrib(validator=is_callable())\n    mapping_validator = attrib(default=None, validator=optional(is_callable()))\n\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if self.mapping_validator is not None:\n            self.mapping_validator(inst, attr, value)\n\n        for key in value:\n            self.key_validator(inst, attr, key)\n            self.value_validator(inst, attr, value[key])\n\n    def __repr__(self):\n        return (\n            \"<deep_mapping validator for objects mapping {key!r} to {value!r}>\"\n        ).format(key=self.key_validator, value=self.value_validator)\n\n\ndef deep_mapping(key_validator, value_validator, mapping_validator=None):\n    \"\"\"\n    A validator that performs deep validation of a dictionary.\n\n    :param key_validator: Validator to apply to dictionary keys\n    :param value_validator: Validator to apply to dictionary values\n    :param mapping_validator: Validator to apply to top-level mapping\n        attribute (optional)\n\n    .. versionadded:: 19.1.0\n\n    :raises TypeError: if any sub-validators fail\n    \"\"\"\n    return _DeepMapping(key_validator, value_validator, mapping_validator)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/attr/validators.pyi",
    "content": "from typing import (\n    Any,\n    AnyStr,\n    Callable,\n    Container,\n    Iterable,\n    List,\n    Mapping,\n    Match,\n    Optional,\n    Tuple,\n    Type,\n    TypeVar,\n    Union,\n    overload,\n)\n\nfrom . import _ValidatorType\n\n\n_T = TypeVar(\"_T\")\n_T1 = TypeVar(\"_T1\")\n_T2 = TypeVar(\"_T2\")\n_T3 = TypeVar(\"_T3\")\n_I = TypeVar(\"_I\", bound=Iterable)\n_K = TypeVar(\"_K\")\n_V = TypeVar(\"_V\")\n_M = TypeVar(\"_M\", bound=Mapping)\n\n# To be more precise on instance_of use some overloads.\n# If there are more than 3 items in the tuple then we fall back to Any\n@overload\ndef instance_of(type: Type[_T]) -> _ValidatorType[_T]: ...\n@overload\ndef instance_of(type: Tuple[Type[_T]]) -> _ValidatorType[_T]: ...\n@overload\ndef instance_of(\n    type: Tuple[Type[_T1], Type[_T2]]\n) -> _ValidatorType[Union[_T1, _T2]]: ...\n@overload\ndef instance_of(\n    type: Tuple[Type[_T1], Type[_T2], Type[_T3]]\n) -> _ValidatorType[Union[_T1, _T2, _T3]]: ...\n@overload\ndef instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ...\ndef provides(interface: Any) -> _ValidatorType[Any]: ...\ndef optional(\n    validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]]\n) -> _ValidatorType[Optional[_T]]: ...\ndef in_(options: Container[_T]) -> _ValidatorType[_T]: ...\ndef and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...\ndef matches_re(\n    regex: AnyStr,\n    flags: int = ...,\n    func: Optional[\n        Callable[[AnyStr, AnyStr, int], Optional[Match[AnyStr]]]\n    ] = ...,\n) -> _ValidatorType[AnyStr]: ...\ndef deep_iterable(\n    member_validator: _ValidatorType[_T],\n    iterable_validator: Optional[_ValidatorType[_I]] = ...,\n) -> _ValidatorType[_I]: ...\ndef deep_mapping(\n    key_validator: _ValidatorType[_K],\n    value_validator: _ValidatorType[_V],\n    mapping_validator: Optional[_ValidatorType[_M]] = ...,\n) -> _ValidatorType[_M]: ...\ndef is_callable() -> _ValidatorType[_T]: ...\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/__init__.py",
    "content": "\"\"\"\nPython HTTP library with thread-safe connection pooling, file post support, user friendly, and more\n\"\"\"\nfrom __future__ import absolute_import\n\n# Set default logging handler to avoid \"No handler found\" warnings.\nimport logging\nimport warnings\nfrom logging import NullHandler\n\nfrom . import exceptions\nfrom ._version import __version__\nfrom .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url\nfrom .filepost import encode_multipart_formdata\nfrom .poolmanager import PoolManager, ProxyManager, proxy_from_url\nfrom .response import HTTPResponse\nfrom .util.request import make_headers\nfrom .util.retry import Retry\nfrom .util.timeout import Timeout\nfrom .util.url import get_host\n\n__author__ = \"Andrey Petrov (andrey.petrov@shazow.net)\"\n__license__ = \"MIT\"\n__version__ = __version__\n\n__all__ = (\n    \"HTTPConnectionPool\",\n    \"HTTPSConnectionPool\",\n    \"PoolManager\",\n    \"ProxyManager\",\n    \"HTTPResponse\",\n    \"Retry\",\n    \"Timeout\",\n    \"add_stderr_logger\",\n    \"connection_from_url\",\n    \"disable_warnings\",\n    \"encode_multipart_formdata\",\n    \"get_host\",\n    \"make_headers\",\n    \"proxy_from_url\",\n)\n\nlogging.getLogger(__name__).addHandler(NullHandler())\n\n\ndef add_stderr_logger(level=logging.DEBUG):\n    \"\"\"\n    Helper for quickly adding a StreamHandler to the logger. Useful for\n    debugging.\n\n    Returns the handler after adding it.\n    \"\"\"\n    # This method needs to be in this __init__.py to get the __name__ correct\n    # even if urllib3 is vendored within another package.\n    logger = logging.getLogger(__name__)\n    handler = logging.StreamHandler()\n    handler.setFormatter(logging.Formatter(\"%(asctime)s %(levelname)s %(message)s\"))\n    logger.addHandler(handler)\n    logger.setLevel(level)\n    logger.debug(\"Added a stderr logging handler to logger: %s\", __name__)\n    return handler\n\n\n# ... Clean up.\ndel NullHandler\n\n\n# All warning filters *must* be appended unless you're really certain that they\n# shouldn't be: otherwise, it's very hard for users to use most Python\n# mechanisms to silence them.\n# SecurityWarning's always go off by default.\nwarnings.simplefilter(\"always\", exceptions.SecurityWarning, append=True)\n# SubjectAltNameWarning's should go off once per host\nwarnings.simplefilter(\"default\", exceptions.SubjectAltNameWarning, append=True)\n# InsecurePlatformWarning's don't vary between requests, so we keep it default.\nwarnings.simplefilter(\"default\", exceptions.InsecurePlatformWarning, append=True)\n# SNIMissingWarnings should go off only once.\nwarnings.simplefilter(\"default\", exceptions.SNIMissingWarning, append=True)\n\n\ndef disable_warnings(category=exceptions.HTTPWarning):\n    \"\"\"\n    Helper for quickly disabling all urllib3 warnings.\n    \"\"\"\n    warnings.simplefilter(\"ignore\", category)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/_collections.py",
    "content": "from __future__ import absolute_import\n\ntry:\n    from collections.abc import Mapping, MutableMapping\nexcept ImportError:\n    from collections import Mapping, MutableMapping\ntry:\n    from threading import RLock\nexcept ImportError:  # Platform-specific: No threads available\n\n    class RLock:\n        def __enter__(self):\n            pass\n\n        def __exit__(self, exc_type, exc_value, traceback):\n            pass\n\n\nfrom collections import OrderedDict\n\nfrom .exceptions import InvalidHeader\nfrom .packages import six\nfrom .packages.six import iterkeys, itervalues\n\n__all__ = [\"RecentlyUsedContainer\", \"HTTPHeaderDict\"]\n\n\n_Null = object()\n\n\nclass RecentlyUsedContainer(MutableMapping):\n    \"\"\"\n    Provides a thread-safe dict-like container which maintains up to\n    ``maxsize`` keys while throwing away the least-recently-used keys beyond\n    ``maxsize``.\n\n    :param maxsize:\n        Maximum number of recent elements to retain.\n\n    :param dispose_func:\n        Every time an item is evicted from the container,\n        ``dispose_func(value)`` is called.  Callback which will get called\n    \"\"\"\n\n    ContainerCls = OrderedDict\n\n    def __init__(self, maxsize=10, dispose_func=None):\n        self._maxsize = maxsize\n        self.dispose_func = dispose_func\n\n        self._container = self.ContainerCls()\n        self.lock = RLock()\n\n    def __getitem__(self, key):\n        # Re-insert the item, moving it to the end of the eviction line.\n        with self.lock:\n            item = self._container.pop(key)\n            self._container[key] = item\n            return item\n\n    def __setitem__(self, key, value):\n        evicted_value = _Null\n        with self.lock:\n            # Possibly evict the existing value of 'key'\n            evicted_value = self._container.get(key, _Null)\n            self._container[key] = value\n\n            # If we didn't evict an existing value, we might have to evict the\n            # least recently used item from the beginning of the container.\n            if len(self._container) > self._maxsize:\n                _key, evicted_value = self._container.popitem(last=False)\n\n        if self.dispose_func and evicted_value is not _Null:\n            self.dispose_func(evicted_value)\n\n    def __delitem__(self, key):\n        with self.lock:\n            value = self._container.pop(key)\n\n        if self.dispose_func:\n            self.dispose_func(value)\n\n    def __len__(self):\n        with self.lock:\n            return len(self._container)\n\n    def __iter__(self):\n        raise NotImplementedError(\n            \"Iteration over this class is unlikely to be threadsafe.\"\n        )\n\n    def clear(self):\n        with self.lock:\n            # Copy pointers to all values, then wipe the mapping\n            values = list(itervalues(self._container))\n            self._container.clear()\n\n        if self.dispose_func:\n            for value in values:\n                self.dispose_func(value)\n\n    def keys(self):\n        with self.lock:\n            return list(iterkeys(self._container))\n\n\nclass HTTPHeaderDict(MutableMapping):\n    \"\"\"\n    :param headers:\n        An iterable of field-value pairs. Must not contain multiple field names\n        when compared case-insensitively.\n\n    :param kwargs:\n        Additional field-value pairs to pass in to ``dict.update``.\n\n    A ``dict`` like container for storing HTTP Headers.\n\n    Field names are stored and compared case-insensitively in compliance with\n    RFC 7230. Iteration provides the first case-sensitive key seen for each\n    case-insensitive pair.\n\n    Using ``__setitem__`` syntax overwrites fields that compare equal\n    case-insensitively in order to maintain ``dict``'s api. For fields that\n    compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add``\n    in a loop.\n\n    If multiple fields that are equal case-insensitively are passed to the\n    constructor or ``.update``, the behavior is undefined and some will be\n    lost.\n\n    >>> headers = HTTPHeaderDict()\n    >>> headers.add('Set-Cookie', 'foo=bar')\n    >>> headers.add('set-cookie', 'baz=quxx')\n    >>> headers['content-length'] = '7'\n    >>> headers['SET-cookie']\n    'foo=bar, baz=quxx'\n    >>> headers['Content-Length']\n    '7'\n    \"\"\"\n\n    def __init__(self, headers=None, **kwargs):\n        super(HTTPHeaderDict, self).__init__()\n        self._container = OrderedDict()\n        if headers is not None:\n            if isinstance(headers, HTTPHeaderDict):\n                self._copy_from(headers)\n            else:\n                self.extend(headers)\n        if kwargs:\n            self.extend(kwargs)\n\n    def __setitem__(self, key, val):\n        self._container[key.lower()] = [key, val]\n        return self._container[key.lower()]\n\n    def __getitem__(self, key):\n        val = self._container[key.lower()]\n        return \", \".join(val[1:])\n\n    def __delitem__(self, key):\n        del self._container[key.lower()]\n\n    def __contains__(self, key):\n        return key.lower() in self._container\n\n    def __eq__(self, other):\n        if not isinstance(other, Mapping) and not hasattr(other, \"keys\"):\n            return False\n        if not isinstance(other, type(self)):\n            other = type(self)(other)\n        return dict((k.lower(), v) for k, v in self.itermerged()) == dict(\n            (k.lower(), v) for k, v in other.itermerged()\n        )\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    if six.PY2:  # Python 2\n        iterkeys = MutableMapping.iterkeys\n        itervalues = MutableMapping.itervalues\n\n    __marker = object()\n\n    def __len__(self):\n        return len(self._container)\n\n    def __iter__(self):\n        # Only provide the originally cased names\n        for vals in self._container.values():\n            yield vals[0]\n\n    def pop(self, key, default=__marker):\n        \"\"\"D.pop(k[,d]) -> v, remove specified key and return the corresponding value.\n        If key is not found, d is returned if given, otherwise KeyError is raised.\n        \"\"\"\n        # Using the MutableMapping function directly fails due to the private marker.\n        # Using ordinary dict.pop would expose the internal structures.\n        # So let's reinvent the wheel.\n        try:\n            value = self[key]\n        except KeyError:\n            if default is self.__marker:\n                raise\n            return default\n        else:\n            del self[key]\n            return value\n\n    def discard(self, key):\n        try:\n            del self[key]\n        except KeyError:\n            pass\n\n    def add(self, key, val):\n        \"\"\"Adds a (name, value) pair, doesn't overwrite the value if it already\n        exists.\n\n        >>> headers = HTTPHeaderDict(foo='bar')\n        >>> headers.add('Foo', 'baz')\n        >>> headers['foo']\n        'bar, baz'\n        \"\"\"\n        key_lower = key.lower()\n        new_vals = [key, val]\n        # Keep the common case aka no item present as fast as possible\n        vals = self._container.setdefault(key_lower, new_vals)\n        if new_vals is not vals:\n            vals.append(val)\n\n    def extend(self, *args, **kwargs):\n        \"\"\"Generic import function for any type of header-like object.\n        Adapted version of MutableMapping.update in order to insert items\n        with self.add instead of self.__setitem__\n        \"\"\"\n        if len(args) > 1:\n            raise TypeError(\n                \"extend() takes at most 1 positional \"\n                \"arguments ({0} given)\".format(len(args))\n            )\n        other = args[0] if len(args) >= 1 else ()\n\n        if isinstance(other, HTTPHeaderDict):\n            for key, val in other.iteritems():\n                self.add(key, val)\n        elif isinstance(other, Mapping):\n            for key in other:\n                self.add(key, other[key])\n        elif hasattr(other, \"keys\"):\n            for key in other.keys():\n                self.add(key, other[key])\n        else:\n            for key, value in other:\n                self.add(key, value)\n\n        for key, value in kwargs.items():\n            self.add(key, value)\n\n    def getlist(self, key, default=__marker):\n        \"\"\"Returns a list of all the values for the named field. Returns an\n        empty list if the key doesn't exist.\"\"\"\n        try:\n            vals = self._container[key.lower()]\n        except KeyError:\n            if default is self.__marker:\n                return []\n            return default\n        else:\n            return vals[1:]\n\n    # Backwards compatibility for httplib\n    getheaders = getlist\n    getallmatchingheaders = getlist\n    iget = getlist\n\n    # Backwards compatibility for http.cookiejar\n    get_all = getlist\n\n    def __repr__(self):\n        return \"%s(%s)\" % (type(self).__name__, dict(self.itermerged()))\n\n    def _copy_from(self, other):\n        for key in other:\n            val = other.getlist(key)\n            if isinstance(val, list):\n                # Don't need to convert tuples\n                val = list(val)\n            self._container[key.lower()] = [key] + val\n\n    def copy(self):\n        clone = type(self)()\n        clone._copy_from(self)\n        return clone\n\n    def iteritems(self):\n        \"\"\"Iterate over all header lines, including duplicate ones.\"\"\"\n        for key in self:\n            vals = self._container[key.lower()]\n            for val in vals[1:]:\n                yield vals[0], val\n\n    def itermerged(self):\n        \"\"\"Iterate over all headers, merging duplicate ones together.\"\"\"\n        for key in self:\n            val = self._container[key.lower()]\n            yield val[0], \", \".join(val[1:])\n\n    def items(self):\n        return list(self.iteritems())\n\n    @classmethod\n    def from_httplib(cls, message):  # Python 2\n        \"\"\"Read headers from a Python 2 httplib message object.\"\"\"\n        # python2.7 does not expose a proper API for exporting multiheaders\n        # efficiently. This function re-reads raw lines from the message\n        # object and extracts the multiheaders properly.\n        obs_fold_continued_leaders = (\" \", \"\\t\")\n        headers = []\n\n        for line in message.headers:\n            if line.startswith(obs_fold_continued_leaders):\n                if not headers:\n                    # We received a header line that starts with OWS as described\n                    # in RFC-7230 S3.2.4. This indicates a multiline header, but\n                    # there exists no previous header to which we can attach it.\n                    raise InvalidHeader(\n                        \"Header continuation with no previous header: %s\" % line\n                    )\n                else:\n                    key, value = headers[-1]\n                    headers[-1] = (key, value + \" \" + line.strip())\n                    continue\n\n            key, value = line.split(\":\", 1)\n            headers.append((key, value.strip()))\n\n        return cls(headers)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/_version.py",
    "content": "# This file is protected via CODEOWNERS\n__version__ = \"1.26.6\"\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/connection.py",
    "content": "from __future__ import absolute_import\n\nimport datetime\nimport logging\nimport os\nimport re\nimport socket\nimport warnings\nfrom socket import error as SocketError\nfrom socket import timeout as SocketTimeout\n\nfrom .packages import six\nfrom .packages.six.moves.http_client import HTTPConnection as _HTTPConnection\nfrom .packages.six.moves.http_client import HTTPException  # noqa: F401\nfrom .util.proxy import create_proxy_ssl_context\n\ntry:  # Compiled with SSL?\n    import ssl\n\n    BaseSSLError = ssl.SSLError\nexcept (ImportError, AttributeError):  # Platform-specific: No SSL.\n    ssl = None\n\n    class BaseSSLError(BaseException):\n        pass\n\n\ntry:\n    # Python 3: not a no-op, we're adding this to the namespace so it can be imported.\n    ConnectionError = ConnectionError\nexcept NameError:\n    # Python 2\n    class ConnectionError(Exception):\n        pass\n\n\ntry:  # Python 3:\n    # Not a no-op, we're adding this to the namespace so it can be imported.\n    BrokenPipeError = BrokenPipeError\nexcept NameError:  # Python 2:\n\n    class BrokenPipeError(Exception):\n        pass\n\n\nfrom ._collections import HTTPHeaderDict  # noqa (historical, removed in v2)\nfrom ._version import __version__\nfrom .exceptions import (\n    ConnectTimeoutError,\n    NewConnectionError,\n    SubjectAltNameWarning,\n    SystemTimeWarning,\n)\nfrom .packages.ssl_match_hostname import CertificateError, match_hostname\nfrom .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection\nfrom .util.ssl_ import (\n    assert_fingerprint,\n    create_urllib3_context,\n    resolve_cert_reqs,\n    resolve_ssl_version,\n    ssl_wrap_socket,\n)\n\nlog = logging.getLogger(__name__)\n\nport_by_scheme = {\"http\": 80, \"https\": 443}\n\n# When it comes time to update this value as a part of regular maintenance\n# (ie test_recent_date is failing) update it to ~6 months before the current date.\nRECENT_DATE = datetime.date(2020, 7, 1)\n\n_CONTAINS_CONTROL_CHAR_RE = re.compile(r\"[^-!#$%&'*+.^_`|~0-9a-zA-Z]\")\n\n\nclass HTTPConnection(_HTTPConnection, object):\n    \"\"\"\n    Based on :class:`http.client.HTTPConnection` but provides an extra constructor\n    backwards-compatibility layer between older and newer Pythons.\n\n    Additional keyword parameters are used to configure attributes of the connection.\n    Accepted parameters include:\n\n    - ``strict``: See the documentation on :class:`urllib3.connectionpool.HTTPConnectionPool`\n    - ``source_address``: Set the source address for the current connection.\n    - ``socket_options``: Set specific options on the underlying socket. If not specified, then\n      defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling\n      Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy.\n\n      For example, if you wish to enable TCP Keep Alive in addition to the defaults,\n      you might pass:\n\n      .. code-block:: python\n\n         HTTPConnection.default_socket_options + [\n             (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),\n         ]\n\n      Or you may want to disable the defaults by passing an empty list (e.g., ``[]``).\n    \"\"\"\n\n    default_port = port_by_scheme[\"http\"]\n\n    #: Disable Nagle's algorithm by default.\n    #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]``\n    default_socket_options = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]\n\n    #: Whether this connection verifies the host's certificate.\n    is_verified = False\n\n    def __init__(self, *args, **kw):\n        if not six.PY2:\n            kw.pop(\"strict\", None)\n\n        # Pre-set source_address.\n        self.source_address = kw.get(\"source_address\")\n\n        #: The socket options provided by the user. If no options are\n        #: provided, we use the default options.\n        self.socket_options = kw.pop(\"socket_options\", self.default_socket_options)\n\n        # Proxy options provided by the user.\n        self.proxy = kw.pop(\"proxy\", None)\n        self.proxy_config = kw.pop(\"proxy_config\", None)\n\n        _HTTPConnection.__init__(self, *args, **kw)\n\n    @property\n    def host(self):\n        \"\"\"\n        Getter method to remove any trailing dots that indicate the hostname is an FQDN.\n\n        In general, SSL certificates don't include the trailing dot indicating a\n        fully-qualified domain name, and thus, they don't validate properly when\n        checked against a domain name that includes the dot. In addition, some\n        servers may not expect to receive the trailing dot when provided.\n\n        However, the hostname with trailing dot is critical to DNS resolution; doing a\n        lookup with the trailing dot will properly only resolve the appropriate FQDN,\n        whereas a lookup without a trailing dot will search the system's search domain\n        list. Thus, it's important to keep the original host around for use only in\n        those cases where it's appropriate (i.e., when doing DNS lookup to establish the\n        actual TCP connection across which we're going to send HTTP requests).\n        \"\"\"\n        return self._dns_host.rstrip(\".\")\n\n    @host.setter\n    def host(self, value):\n        \"\"\"\n        Setter for the `host` property.\n\n        We assume that only urllib3 uses the _dns_host attribute; httplib itself\n        only uses `host`, and it seems reasonable that other libraries follow suit.\n        \"\"\"\n        self._dns_host = value\n\n    def _new_conn(self):\n        \"\"\"Establish a socket connection and set nodelay settings on it.\n\n        :return: New socket connection.\n        \"\"\"\n        extra_kw = {}\n        if self.source_address:\n            extra_kw[\"source_address\"] = self.source_address\n\n        if self.socket_options:\n            extra_kw[\"socket_options\"] = self.socket_options\n\n        try:\n            conn = connection.create_connection(\n                (self._dns_host, self.port), self.timeout, **extra_kw\n            )\n\n        except SocketTimeout:\n            raise ConnectTimeoutError(\n                self,\n                \"Connection to %s timed out. (connect timeout=%s)\"\n                % (self.host, self.timeout),\n            )\n\n        except SocketError as e:\n            raise NewConnectionError(\n                self, \"Failed to establish a new connection: %s\" % e\n            )\n\n        return conn\n\n    def _is_using_tunnel(self):\n        # Google App Engine's httplib does not define _tunnel_host\n        return getattr(self, \"_tunnel_host\", None)\n\n    def _prepare_conn(self, conn):\n        self.sock = conn\n        if self._is_using_tunnel():\n            # TODO: Fix tunnel so it doesn't depend on self.sock state.\n            self._tunnel()\n            # Mark this connection as not reusable\n            self.auto_open = 0\n\n    def connect(self):\n        conn = self._new_conn()\n        self._prepare_conn(conn)\n\n    def putrequest(self, method, url, *args, **kwargs):\n        \"\"\" \"\"\"\n        # Empty docstring because the indentation of CPython's implementation\n        # is broken but we don't want this method in our documentation.\n        match = _CONTAINS_CONTROL_CHAR_RE.search(method)\n        if match:\n            raise ValueError(\n                \"Method cannot contain non-token characters %r (found at least %r)\"\n                % (method, match.group())\n            )\n\n        return _HTTPConnection.putrequest(self, method, url, *args, **kwargs)\n\n    def putheader(self, header, *values):\n        \"\"\" \"\"\"\n        if not any(isinstance(v, str) and v == SKIP_HEADER for v in values):\n            _HTTPConnection.putheader(self, header, *values)\n        elif six.ensure_str(header.lower()) not in SKIPPABLE_HEADERS:\n            raise ValueError(\n                \"urllib3.util.SKIP_HEADER only supports '%s'\"\n                % (\"', '\".join(map(str.title, sorted(SKIPPABLE_HEADERS))),)\n            )\n\n    def request(self, method, url, body=None, headers=None):\n        if headers is None:\n            headers = {}\n        else:\n            # Avoid modifying the headers passed into .request()\n            headers = headers.copy()\n        if \"user-agent\" not in (six.ensure_str(k.lower()) for k in headers):\n            headers[\"User-Agent\"] = _get_default_user_agent()\n        super(HTTPConnection, self).request(method, url, body=body, headers=headers)\n\n    def request_chunked(self, method, url, body=None, headers=None):\n        \"\"\"\n        Alternative to the common request method, which sends the\n        body with chunked encoding and not as one block\n        \"\"\"\n        headers = headers or {}\n        header_keys = set([six.ensure_str(k.lower()) for k in headers])\n        skip_accept_encoding = \"accept-encoding\" in header_keys\n        skip_host = \"host\" in header_keys\n        self.putrequest(\n            method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host\n        )\n        if \"user-agent\" not in header_keys:\n            self.putheader(\"User-Agent\", _get_default_user_agent())\n        for header, value in headers.items():\n            self.putheader(header, value)\n        if \"transfer-encoding\" not in header_keys:\n            self.putheader(\"Transfer-Encoding\", \"chunked\")\n        self.endheaders()\n\n        if body is not None:\n            stringish_types = six.string_types + (bytes,)\n            if isinstance(body, stringish_types):\n                body = (body,)\n            for chunk in body:\n                if not chunk:\n                    continue\n                if not isinstance(chunk, bytes):\n                    chunk = chunk.encode(\"utf8\")\n                len_str = hex(len(chunk))[2:]\n                to_send = bytearray(len_str.encode())\n                to_send += b\"\\r\\n\"\n                to_send += chunk\n                to_send += b\"\\r\\n\"\n                self.send(to_send)\n\n        # After the if clause, to always have a closed body\n        self.send(b\"0\\r\\n\\r\\n\")\n\n\nclass HTTPSConnection(HTTPConnection):\n    \"\"\"\n    Many of the parameters to this constructor are passed to the underlying SSL\n    socket by means of :py:func:`urllib3.util.ssl_wrap_socket`.\n    \"\"\"\n\n    default_port = port_by_scheme[\"https\"]\n\n    cert_reqs = None\n    ca_certs = None\n    ca_cert_dir = None\n    ca_cert_data = None\n    ssl_version = None\n    assert_fingerprint = None\n    tls_in_tls_required = False\n\n    def __init__(\n        self,\n        host,\n        port=None,\n        key_file=None,\n        cert_file=None,\n        key_password=None,\n        strict=None,\n        timeout=socket._GLOBAL_DEFAULT_TIMEOUT,\n        ssl_context=None,\n        server_hostname=None,\n        **kw\n    ):\n\n        HTTPConnection.__init__(self, host, port, strict=strict, timeout=timeout, **kw)\n\n        self.key_file = key_file\n        self.cert_file = cert_file\n        self.key_password = key_password\n        self.ssl_context = ssl_context\n        self.server_hostname = server_hostname\n\n        # Required property for Google AppEngine 1.9.0 which otherwise causes\n        # HTTPS requests to go out as HTTP. (See Issue #356)\n        self._protocol = \"https\"\n\n    def set_cert(\n        self,\n        key_file=None,\n        cert_file=None,\n        cert_reqs=None,\n        key_password=None,\n        ca_certs=None,\n        assert_hostname=None,\n        assert_fingerprint=None,\n        ca_cert_dir=None,\n        ca_cert_data=None,\n    ):\n        \"\"\"\n        This method should only be called once, before the connection is used.\n        \"\"\"\n        # If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also\n        # have an SSLContext object in which case we'll use its verify_mode.\n        if cert_reqs is None:\n            if self.ssl_context is not None:\n                cert_reqs = self.ssl_context.verify_mode\n            else:\n                cert_reqs = resolve_cert_reqs(None)\n\n        self.key_file = key_file\n        self.cert_file = cert_file\n        self.cert_reqs = cert_reqs\n        self.key_password = key_password\n        self.assert_hostname = assert_hostname\n        self.assert_fingerprint = assert_fingerprint\n        self.ca_certs = ca_certs and os.path.expanduser(ca_certs)\n        self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir)\n        self.ca_cert_data = ca_cert_data\n\n    def connect(self):\n        # Add certificate verification\n        conn = self._new_conn()\n        hostname = self.host\n        tls_in_tls = False\n\n        if self._is_using_tunnel():\n            if self.tls_in_tls_required:\n                conn = self._connect_tls_proxy(hostname, conn)\n                tls_in_tls = True\n\n            self.sock = conn\n\n            # Calls self._set_hostport(), so self.host is\n            # self._tunnel_host below.\n            self._tunnel()\n            # Mark this connection as not reusable\n            self.auto_open = 0\n\n            # Override the host with the one we're requesting data from.\n            hostname = self._tunnel_host\n\n        server_hostname = hostname\n        if self.server_hostname is not None:\n            server_hostname = self.server_hostname\n\n        is_time_off = datetime.date.today() < RECENT_DATE\n        if is_time_off:\n            warnings.warn(\n                (\n                    \"System time is way off (before {0}). This will probably \"\n                    \"lead to SSL verification errors\"\n                ).format(RECENT_DATE),\n                SystemTimeWarning,\n            )\n\n        # Wrap socket using verification with the root certs in\n        # trusted_root_certs\n        default_ssl_context = False\n        if self.ssl_context is None:\n            default_ssl_context = True\n            self.ssl_context = create_urllib3_context(\n                ssl_version=resolve_ssl_version(self.ssl_version),\n                cert_reqs=resolve_cert_reqs(self.cert_reqs),\n            )\n\n        context = self.ssl_context\n        context.verify_mode = resolve_cert_reqs(self.cert_reqs)\n\n        # Try to load OS default certs if none are given.\n        # Works well on Windows (requires Python3.4+)\n        if (\n            not self.ca_certs\n            and not self.ca_cert_dir\n            and not self.ca_cert_data\n            and default_ssl_context\n            and hasattr(context, \"load_default_certs\")\n        ):\n            context.load_default_certs()\n\n        self.sock = ssl_wrap_socket(\n            sock=conn,\n            keyfile=self.key_file,\n            certfile=self.cert_file,\n            key_password=self.key_password,\n            ca_certs=self.ca_certs,\n            ca_cert_dir=self.ca_cert_dir,\n            ca_cert_data=self.ca_cert_data,\n            server_hostname=server_hostname,\n            ssl_context=context,\n            tls_in_tls=tls_in_tls,\n        )\n\n        # If we're using all defaults and the connection\n        # is TLSv1 or TLSv1.1 we throw a DeprecationWarning\n        # for the host.\n        if (\n            default_ssl_context\n            and self.ssl_version is None\n            and hasattr(self.sock, \"version\")\n            and self.sock.version() in {\"TLSv1\", \"TLSv1.1\"}\n        ):\n            warnings.warn(\n                \"Negotiating TLSv1/TLSv1.1 by default is deprecated \"\n                \"and will be disabled in urllib3 v2.0.0. Connecting to \"\n                \"'%s' with '%s' can be enabled by explicitly opting-in \"\n                \"with 'ssl_version'\" % (self.host, self.sock.version()),\n                DeprecationWarning,\n            )\n\n        if self.assert_fingerprint:\n            assert_fingerprint(\n                self.sock.getpeercert(binary_form=True), self.assert_fingerprint\n            )\n        elif (\n            context.verify_mode != ssl.CERT_NONE\n            and not getattr(context, \"check_hostname\", False)\n            and self.assert_hostname is not False\n        ):\n            # While urllib3 attempts to always turn off hostname matching from\n            # the TLS library, this cannot always be done. So we check whether\n            # the TLS Library still thinks it's matching hostnames.\n            cert = self.sock.getpeercert()\n            if not cert.get(\"subjectAltName\", ()):\n                warnings.warn(\n                    (\n                        \"Certificate for {0} has no `subjectAltName`, falling back to check for a \"\n                        \"`commonName` for now. This feature is being removed by major browsers and \"\n                        \"deprecated by RFC 2818. (See https://github.com/urllib3/urllib3/issues/497 \"\n                        \"for details.)\".format(hostname)\n                    ),\n                    SubjectAltNameWarning,\n                )\n            _match_hostname(cert, self.assert_hostname or server_hostname)\n\n        self.is_verified = (\n            context.verify_mode == ssl.CERT_REQUIRED\n            or self.assert_fingerprint is not None\n        )\n\n    def _connect_tls_proxy(self, hostname, conn):\n        \"\"\"\n        Establish a TLS connection to the proxy using the provided SSL context.\n        \"\"\"\n        proxy_config = self.proxy_config\n        ssl_context = proxy_config.ssl_context\n        if ssl_context:\n            # If the user provided a proxy context, we assume CA and client\n            # certificates have already been set\n            return ssl_wrap_socket(\n                sock=conn,\n                server_hostname=hostname,\n                ssl_context=ssl_context,\n            )\n\n        ssl_context = create_proxy_ssl_context(\n            self.ssl_version,\n            self.cert_reqs,\n            self.ca_certs,\n            self.ca_cert_dir,\n            self.ca_cert_data,\n        )\n        # By default urllib3's SSLContext disables `check_hostname` and uses\n        # a custom check. For proxies we're good with relying on the default\n        # verification.\n        ssl_context.check_hostname = True\n\n        # If no cert was provided, use only the default options for server\n        # certificate validation\n        return ssl_wrap_socket(\n            sock=conn,\n            ca_certs=self.ca_certs,\n            ca_cert_dir=self.ca_cert_dir,\n            ca_cert_data=self.ca_cert_data,\n            server_hostname=hostname,\n            ssl_context=ssl_context,\n        )\n\n\ndef _match_hostname(cert, asserted_hostname):\n    try:\n        match_hostname(cert, asserted_hostname)\n    except CertificateError as e:\n        log.warning(\n            \"Certificate did not match expected hostname: %s. Certificate: %s\",\n            asserted_hostname,\n            cert,\n        )\n        # Add cert to exception and reraise so client code can inspect\n        # the cert when catching the exception, if they want to\n        e._peer_cert = cert\n        raise\n\n\ndef _get_default_user_agent():\n    return \"python-urllib3/%s\" % __version__\n\n\nclass DummyConnection(object):\n    \"\"\"Used to detect a failed ConnectionCls import.\"\"\"\n\n    pass\n\n\nif not ssl:\n    HTTPSConnection = DummyConnection  # noqa: F811\n\n\nVerifiedHTTPSConnection = HTTPSConnection\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/connectionpool.py",
    "content": "from __future__ import absolute_import\n\nimport errno\nimport logging\nimport socket\nimport sys\nimport warnings\nfrom socket import error as SocketError\nfrom socket import timeout as SocketTimeout\n\nfrom .connection import (\n    BaseSSLError,\n    BrokenPipeError,\n    DummyConnection,\n    HTTPConnection,\n    HTTPException,\n    HTTPSConnection,\n    VerifiedHTTPSConnection,\n    port_by_scheme,\n)\nfrom .exceptions import (\n    ClosedPoolError,\n    EmptyPoolError,\n    HeaderParsingError,\n    HostChangedError,\n    InsecureRequestWarning,\n    LocationValueError,\n    MaxRetryError,\n    NewConnectionError,\n    ProtocolError,\n    ProxyError,\n    ReadTimeoutError,\n    SSLError,\n    TimeoutError,\n)\nfrom .packages import six\nfrom .packages.six.moves import queue\nfrom .packages.ssl_match_hostname import CertificateError\nfrom .request import RequestMethods\nfrom .response import HTTPResponse\nfrom .util.connection import is_connection_dropped\nfrom .util.proxy import connection_requires_http_tunnel\nfrom .util.queue import LifoQueue\nfrom .util.request import set_file_position\nfrom .util.response import assert_header_parsing\nfrom .util.retry import Retry\nfrom .util.timeout import Timeout\nfrom .util.url import Url, _encode_target\nfrom .util.url import _normalize_host as normalize_host\nfrom .util.url import get_host, parse_url\n\nxrange = six.moves.xrange\n\nlog = logging.getLogger(__name__)\n\n_Default = object()\n\n\n# Pool objects\nclass ConnectionPool(object):\n    \"\"\"\n    Base class for all connection pools, such as\n    :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`.\n\n    .. note::\n       ConnectionPool.urlopen() does not normalize or percent-encode target URIs\n       which is useful if your target server doesn't support percent-encoded\n       target URIs.\n    \"\"\"\n\n    scheme = None\n    QueueCls = LifoQueue\n\n    def __init__(self, host, port=None):\n        if not host:\n            raise LocationValueError(\"No host specified.\")\n\n        self.host = _normalize_host(host, scheme=self.scheme)\n        self._proxy_host = host.lower()\n        self.port = port\n\n    def __str__(self):\n        return \"%s(host=%r, port=%r)\" % (type(self).__name__, self.host, self.port)\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n        # Return False to re-raise any potential exceptions\n        return False\n\n    def close(self):\n        \"\"\"\n        Close all pooled connections and disable the pool.\n        \"\"\"\n        pass\n\n\n# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252\n_blocking_errnos = {errno.EAGAIN, errno.EWOULDBLOCK}\n\n\nclass HTTPConnectionPool(ConnectionPool, RequestMethods):\n    \"\"\"\n    Thread-safe connection pool for one host.\n\n    :param host:\n        Host used for this HTTP Connection (e.g. \"localhost\"), passed into\n        :class:`http.client.HTTPConnection`.\n\n    :param port:\n        Port used for this HTTP Connection (None is equivalent to 80), passed\n        into :class:`http.client.HTTPConnection`.\n\n    :param strict:\n        Causes BadStatusLine to be raised if the status line can't be parsed\n        as a valid HTTP/1.0 or 1.1 status line, passed into\n        :class:`http.client.HTTPConnection`.\n\n        .. note::\n           Only works in Python 2. This parameter is ignored in Python 3.\n\n    :param timeout:\n        Socket timeout in seconds for each individual connection. This can\n        be a float or integer, which sets the timeout for the HTTP request,\n        or an instance of :class:`urllib3.util.Timeout` which gives you more\n        fine-grained control over request timeouts. After the constructor has\n        been parsed, this is always a `urllib3.util.Timeout` object.\n\n    :param maxsize:\n        Number of connections to save that can be reused. More than 1 is useful\n        in multithreaded situations. If ``block`` is set to False, more\n        connections will be created but they will not be saved once they've\n        been used.\n\n    :param block:\n        If set to True, no more than ``maxsize`` connections will be used at\n        a time. When no free connections are available, the call will block\n        until a connection has been released. This is a useful side effect for\n        particular multithreaded situations where one does not want to use more\n        than maxsize connections per host to prevent flooding.\n\n    :param headers:\n        Headers to include with all requests, unless other headers are given\n        explicitly.\n\n    :param retries:\n        Retry configuration to use by default with requests in this pool.\n\n    :param _proxy:\n        Parsed proxy URL, should not be used directly, instead, see\n        :class:`urllib3.ProxyManager`\n\n    :param _proxy_headers:\n        A dictionary with proxy headers, should not be used directly,\n        instead, see :class:`urllib3.ProxyManager`\n\n    :param \\\\**conn_kw:\n        Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`,\n        :class:`urllib3.connection.HTTPSConnection` instances.\n    \"\"\"\n\n    scheme = \"http\"\n    ConnectionCls = HTTPConnection\n    ResponseCls = HTTPResponse\n\n    def __init__(\n        self,\n        host,\n        port=None,\n        strict=False,\n        timeout=Timeout.DEFAULT_TIMEOUT,\n        maxsize=1,\n        block=False,\n        headers=None,\n        retries=None,\n        _proxy=None,\n        _proxy_headers=None,\n        _proxy_config=None,\n        **conn_kw\n    ):\n        ConnectionPool.__init__(self, host, port)\n        RequestMethods.__init__(self, headers)\n\n        self.strict = strict\n\n        if not isinstance(timeout, Timeout):\n            timeout = Timeout.from_float(timeout)\n\n        if retries is None:\n            retries = Retry.DEFAULT\n\n        self.timeout = timeout\n        self.retries = retries\n\n        self.pool = self.QueueCls(maxsize)\n        self.block = block\n\n        self.proxy = _proxy\n        self.proxy_headers = _proxy_headers or {}\n        self.proxy_config = _proxy_config\n\n        # Fill the queue up so that doing get() on it will block properly\n        for _ in xrange(maxsize):\n            self.pool.put(None)\n\n        # These are mostly for testing and debugging purposes.\n        self.num_connections = 0\n        self.num_requests = 0\n        self.conn_kw = conn_kw\n\n        if self.proxy:\n            # Enable Nagle's algorithm for proxies, to avoid packet fragmentation.\n            # We cannot know if the user has added default socket options, so we cannot replace the\n            # list.\n            self.conn_kw.setdefault(\"socket_options\", [])\n\n            self.conn_kw[\"proxy\"] = self.proxy\n            self.conn_kw[\"proxy_config\"] = self.proxy_config\n\n    def _new_conn(self):\n        \"\"\"\n        Return a fresh :class:`HTTPConnection`.\n        \"\"\"\n        self.num_connections += 1\n        log.debug(\n            \"Starting new HTTP connection (%d): %s:%s\",\n            self.num_connections,\n            self.host,\n            self.port or \"80\",\n        )\n\n        conn = self.ConnectionCls(\n            host=self.host,\n            port=self.port,\n            timeout=self.timeout.connect_timeout,\n            strict=self.strict,\n            **self.conn_kw\n        )\n        return conn\n\n    def _get_conn(self, timeout=None):\n        \"\"\"\n        Get a connection. Will return a pooled connection if one is available.\n\n        If no connections are available and :prop:`.block` is ``False``, then a\n        fresh connection is returned.\n\n        :param timeout:\n            Seconds to wait before giving up and raising\n            :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and\n            :prop:`.block` is ``True``.\n        \"\"\"\n        conn = None\n        try:\n            conn = self.pool.get(block=self.block, timeout=timeout)\n\n        except AttributeError:  # self.pool is None\n            raise ClosedPoolError(self, \"Pool is closed.\")\n\n        except queue.Empty:\n            if self.block:\n                raise EmptyPoolError(\n                    self,\n                    \"Pool reached maximum size and no more connections are allowed.\",\n                )\n            pass  # Oh well, we'll create a new connection then\n\n        # If this is a persistent connection, check if it got disconnected\n        if conn and is_connection_dropped(conn):\n            log.debug(\"Resetting dropped connection: %s\", self.host)\n            conn.close()\n            if getattr(conn, \"auto_open\", 1) == 0:\n                # This is a proxied connection that has been mutated by\n                # http.client._tunnel() and cannot be reused (since it would\n                # attempt to bypass the proxy)\n                conn = None\n\n        return conn or self._new_conn()\n\n    def _put_conn(self, conn):\n        \"\"\"\n        Put a connection back into the pool.\n\n        :param conn:\n            Connection object for the current host and port as returned by\n            :meth:`._new_conn` or :meth:`._get_conn`.\n\n        If the pool is already full, the connection is closed and discarded\n        because we exceeded maxsize. If connections are discarded frequently,\n        then maxsize should be increased.\n\n        If the pool is closed, then the connection will be closed and discarded.\n        \"\"\"\n        try:\n            self.pool.put(conn, block=False)\n            return  # Everything is dandy, done.\n        except AttributeError:\n            # self.pool is None.\n            pass\n        except queue.Full:\n            # This should never happen if self.block == True\n            log.warning(\"Connection pool is full, discarding connection: %s\", self.host)\n\n        # Connection never got put back into the pool, close it.\n        if conn:\n            conn.close()\n\n    def _validate_conn(self, conn):\n        \"\"\"\n        Called right before a request is made, after the socket is created.\n        \"\"\"\n        pass\n\n    def _prepare_proxy(self, conn):\n        # Nothing to do for HTTP connections.\n        pass\n\n    def _get_timeout(self, timeout):\n        \"\"\"Helper that always returns a :class:`urllib3.util.Timeout`\"\"\"\n        if timeout is _Default:\n            return self.timeout.clone()\n\n        if isinstance(timeout, Timeout):\n            return timeout.clone()\n        else:\n            # User passed us an int/float. This is for backwards compatibility,\n            # can be removed later\n            return Timeout.from_float(timeout)\n\n    def _raise_timeout(self, err, url, timeout_value):\n        \"\"\"Is the error actually a timeout? Will raise a ReadTimeout or pass\"\"\"\n\n        if isinstance(err, SocketTimeout):\n            raise ReadTimeoutError(\n                self, url, \"Read timed out. (read timeout=%s)\" % timeout_value\n            )\n\n        # See the above comment about EAGAIN in Python 3. In Python 2 we have\n        # to specifically catch it and throw the timeout error\n        if hasattr(err, \"errno\") and err.errno in _blocking_errnos:\n            raise ReadTimeoutError(\n                self, url, \"Read timed out. (read timeout=%s)\" % timeout_value\n            )\n\n        # Catch possible read timeouts thrown as SSL errors. If not the\n        # case, rethrow the original. We need to do this because of:\n        # http://bugs.python.org/issue10272\n        if \"timed out\" in str(err) or \"did not complete (read)\" in str(\n            err\n        ):  # Python < 2.7.4\n            raise ReadTimeoutError(\n                self, url, \"Read timed out. (read timeout=%s)\" % timeout_value\n            )\n\n    def _make_request(\n        self, conn, method, url, timeout=_Default, chunked=False, **httplib_request_kw\n    ):\n        \"\"\"\n        Perform a request on a given urllib connection object taken from our\n        pool.\n\n        :param conn:\n            a connection from one of our connection pools\n\n        :param timeout:\n            Socket timeout in seconds for the request. This can be a\n            float or integer, which will set the same timeout value for\n            the socket connect and the socket read, or an instance of\n            :class:`urllib3.util.Timeout`, which gives you more fine-grained\n            control over your timeouts.\n        \"\"\"\n        self.num_requests += 1\n\n        timeout_obj = self._get_timeout(timeout)\n        timeout_obj.start_connect()\n        conn.timeout = timeout_obj.connect_timeout\n\n        # Trigger any extra validation we need to do.\n        try:\n            self._validate_conn(conn)\n        except (SocketTimeout, BaseSSLError) as e:\n            # Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout.\n            self._raise_timeout(err=e, url=url, timeout_value=conn.timeout)\n            raise\n\n        # conn.request() calls http.client.*.request, not the method in\n        # urllib3.request. It also calls makefile (recv) on the socket.\n        try:\n            if chunked:\n                conn.request_chunked(method, url, **httplib_request_kw)\n            else:\n                conn.request(method, url, **httplib_request_kw)\n\n        # We are swallowing BrokenPipeError (errno.EPIPE) since the server is\n        # legitimately able to close the connection after sending a valid response.\n        # With this behaviour, the received response is still readable.\n        except BrokenPipeError:\n            # Python 3\n            pass\n        except IOError as e:\n            # Python 2 and macOS/Linux\n            # EPIPE and ESHUTDOWN are BrokenPipeError on Python 2, and EPROTOTYPE is needed on macOS\n            # https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/\n            if e.errno not in {\n                errno.EPIPE,\n                errno.ESHUTDOWN,\n                errno.EPROTOTYPE,\n            }:\n                raise\n\n        # Reset the timeout for the recv() on the socket\n        read_timeout = timeout_obj.read_timeout\n\n        # App Engine doesn't have a sock attr\n        if getattr(conn, \"sock\", None):\n            # In Python 3 socket.py will catch EAGAIN and return None when you\n            # try and read into the file pointer created by http.client, which\n            # instead raises a BadStatusLine exception. Instead of catching\n            # the exception and assuming all BadStatusLine exceptions are read\n            # timeouts, check for a zero timeout before making the request.\n            if read_timeout == 0:\n                raise ReadTimeoutError(\n                    self, url, \"Read timed out. (read timeout=%s)\" % read_timeout\n                )\n            if read_timeout is Timeout.DEFAULT_TIMEOUT:\n                conn.sock.settimeout(socket.getdefaulttimeout())\n            else:  # None or a value\n                conn.sock.settimeout(read_timeout)\n\n        # Receive the response from the server\n        try:\n            try:\n                # Python 2.7, use buffering of HTTP responses\n                httplib_response = conn.getresponse(buffering=True)\n            except TypeError:\n                # Python 3\n                try:\n                    httplib_response = conn.getresponse()\n                except BaseException as e:\n                    # Remove the TypeError from the exception chain in\n                    # Python 3 (including for exceptions like SystemExit).\n                    # Otherwise it looks like a bug in the code.\n                    six.raise_from(e, None)\n        except (SocketTimeout, BaseSSLError, SocketError) as e:\n            self._raise_timeout(err=e, url=url, timeout_value=read_timeout)\n            raise\n\n        # AppEngine doesn't have a version attr.\n        http_version = getattr(conn, \"_http_vsn_str\", \"HTTP/?\")\n        log.debug(\n            '%s://%s:%s \"%s %s %s\" %s %s',\n            self.scheme,\n            self.host,\n            self.port,\n            method,\n            url,\n            http_version,\n            httplib_response.status,\n            httplib_response.length,\n        )\n\n        try:\n            assert_header_parsing(httplib_response.msg)\n        except (HeaderParsingError, TypeError) as hpe:  # Platform-specific: Python 3\n            log.warning(\n                \"Failed to parse headers (url=%s): %s\",\n                self._absolute_url(url),\n                hpe,\n                exc_info=True,\n            )\n\n        return httplib_response\n\n    def _absolute_url(self, path):\n        return Url(scheme=self.scheme, host=self.host, port=self.port, path=path).url\n\n    def close(self):\n        \"\"\"\n        Close all pooled connections and disable the pool.\n        \"\"\"\n        if self.pool is None:\n            return\n        # Disable access to the pool\n        old_pool, self.pool = self.pool, None\n\n        try:\n            while True:\n                conn = old_pool.get(block=False)\n                if conn:\n                    conn.close()\n\n        except queue.Empty:\n            pass  # Done.\n\n    def is_same_host(self, url):\n        \"\"\"\n        Check if the given ``url`` is a member of the same host as this\n        connection pool.\n        \"\"\"\n        if url.startswith(\"/\"):\n            return True\n\n        # TODO: Add optional support for socket.gethostbyname checking.\n        scheme, host, port = get_host(url)\n        if host is not None:\n            host = _normalize_host(host, scheme=scheme)\n\n        # Use explicit default port for comparison when none is given\n        if self.port and not port:\n            port = port_by_scheme.get(scheme)\n        elif not self.port and port == port_by_scheme.get(scheme):\n            port = None\n\n        return (scheme, host, port) == (self.scheme, self.host, self.port)\n\n    def urlopen(\n        self,\n        method,\n        url,\n        body=None,\n        headers=None,\n        retries=None,\n        redirect=True,\n        assert_same_host=True,\n        timeout=_Default,\n        pool_timeout=None,\n        release_conn=None,\n        chunked=False,\n        body_pos=None,\n        **response_kw\n    ):\n        \"\"\"\n        Get a connection from the pool and perform an HTTP request. This is the\n        lowest level call for making a request, so you'll need to specify all\n        the raw details.\n\n        .. note::\n\n           More commonly, it's appropriate to use a convenience method provided\n           by :class:`.RequestMethods`, such as :meth:`request`.\n\n        .. note::\n\n           `release_conn` will only behave as expected if\n           `preload_content=False` because we want to make\n           `preload_content=False` the default behaviour someday soon without\n           breaking backwards compatibility.\n\n        :param method:\n            HTTP request method (such as GET, POST, PUT, etc.)\n\n        :param url:\n            The URL to perform the request on.\n\n        :param body:\n            Data to send in the request body, either :class:`str`, :class:`bytes`,\n            an iterable of :class:`str`/:class:`bytes`, or a file-like object.\n\n        :param headers:\n            Dictionary of custom headers to send, such as User-Agent,\n            If-None-Match, etc. If None, pool headers are used. If provided,\n            these headers completely replace any pool-specific headers.\n\n        :param retries:\n            Configure the number of retries to allow before raising a\n            :class:`~urllib3.exceptions.MaxRetryError` exception.\n\n            Pass ``None`` to retry until you receive a response. Pass a\n            :class:`~urllib3.util.retry.Retry` object for fine-grained control\n            over different types of retries.\n            Pass an integer number to retry connection errors that many times,\n            but no other types of errors. Pass zero to never retry.\n\n            If ``False``, then retries are disabled and any exception is raised\n            immediately. Also, instead of raising a MaxRetryError on redirects,\n            the redirect response will be returned.\n\n        :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int.\n\n        :param redirect:\n            If True, automatically handle redirects (status codes 301, 302,\n            303, 307, 308). Each redirect counts as a retry. Disabling retries\n            will disable redirect, too.\n\n        :param assert_same_host:\n            If ``True``, will make sure that the host of the pool requests is\n            consistent else will raise HostChangedError. When ``False``, you can\n            use the pool on an HTTP proxy and request foreign hosts.\n\n        :param timeout:\n            If specified, overrides the default timeout for this one\n            request. It may be a float (in seconds) or an instance of\n            :class:`urllib3.util.Timeout`.\n\n        :param pool_timeout:\n            If set and the pool is set to block=True, then this method will\n            block for ``pool_timeout`` seconds and raise EmptyPoolError if no\n            connection is available within the time period.\n\n        :param release_conn:\n            If False, then the urlopen call will not release the connection\n            back into the pool once a response is received (but will release if\n            you read the entire contents of the response such as when\n            `preload_content=True`). This is useful if you're not preloading\n            the response's content immediately. You will need to call\n            ``r.release_conn()`` on the response ``r`` to return the connection\n            back into the pool. If None, it takes the value of\n            ``response_kw.get('preload_content', True)``.\n\n        :param chunked:\n            If True, urllib3 will send the body using chunked transfer\n            encoding. Otherwise, urllib3 will send the body using the standard\n            content-length form. Defaults to False.\n\n        :param int body_pos:\n            Position to seek to in file-like body in the event of a retry or\n            redirect. Typically this won't need to be set because urllib3 will\n            auto-populate the value when needed.\n\n        :param \\\\**response_kw:\n            Additional parameters are passed to\n            :meth:`urllib3.response.HTTPResponse.from_httplib`\n        \"\"\"\n\n        parsed_url = parse_url(url)\n        destination_scheme = parsed_url.scheme\n\n        if headers is None:\n            headers = self.headers\n\n        if not isinstance(retries, Retry):\n            retries = Retry.from_int(retries, redirect=redirect, default=self.retries)\n\n        if release_conn is None:\n            release_conn = response_kw.get(\"preload_content\", True)\n\n        # Check host\n        if assert_same_host and not self.is_same_host(url):\n            raise HostChangedError(self, url, retries)\n\n        # Ensure that the URL we're connecting to is properly encoded\n        if url.startswith(\"/\"):\n            url = six.ensure_str(_encode_target(url))\n        else:\n            url = six.ensure_str(parsed_url.url)\n\n        conn = None\n\n        # Track whether `conn` needs to be released before\n        # returning/raising/recursing. Update this variable if necessary, and\n        # leave `release_conn` constant throughout the function. That way, if\n        # the function recurses, the original value of `release_conn` will be\n        # passed down into the recursive call, and its value will be respected.\n        #\n        # See issue #651 [1] for details.\n        #\n        # [1] <https://github.com/urllib3/urllib3/issues/651>\n        release_this_conn = release_conn\n\n        http_tunnel_required = connection_requires_http_tunnel(\n            self.proxy, self.proxy_config, destination_scheme\n        )\n\n        # Merge the proxy headers. Only done when not using HTTP CONNECT. We\n        # have to copy the headers dict so we can safely change it without those\n        # changes being reflected in anyone else's copy.\n        if not http_tunnel_required:\n            headers = headers.copy()\n            headers.update(self.proxy_headers)\n\n        # Must keep the exception bound to a separate variable or else Python 3\n        # complains about UnboundLocalError.\n        err = None\n\n        # Keep track of whether we cleanly exited the except block. This\n        # ensures we do proper cleanup in finally.\n        clean_exit = False\n\n        # Rewind body position, if needed. Record current position\n        # for future rewinds in the event of a redirect/retry.\n        body_pos = set_file_position(body, body_pos)\n\n        try:\n            # Request a connection from the queue.\n            timeout_obj = self._get_timeout(timeout)\n            conn = self._get_conn(timeout=pool_timeout)\n\n            conn.timeout = timeout_obj.connect_timeout\n\n            is_new_proxy_conn = self.proxy is not None and not getattr(\n                conn, \"sock\", None\n            )\n            if is_new_proxy_conn and http_tunnel_required:\n                self._prepare_proxy(conn)\n\n            # Make the request on the httplib connection object.\n            httplib_response = self._make_request(\n                conn,\n                method,\n                url,\n                timeout=timeout_obj,\n                body=body,\n                headers=headers,\n                chunked=chunked,\n            )\n\n            # If we're going to release the connection in ``finally:``, then\n            # the response doesn't need to know about the connection. Otherwise\n            # it will also try to release it and we'll have a double-release\n            # mess.\n            response_conn = conn if not release_conn else None\n\n            # Pass method to Response for length checking\n            response_kw[\"request_method\"] = method\n\n            # Import httplib's response into our own wrapper object\n            response = self.ResponseCls.from_httplib(\n                httplib_response,\n                pool=self,\n                connection=response_conn,\n                retries=retries,\n                **response_kw\n            )\n\n            # Everything went great!\n            clean_exit = True\n\n        except EmptyPoolError:\n            # Didn't get a connection from the pool, no need to clean up\n            clean_exit = True\n            release_this_conn = False\n            raise\n\n        except (\n            TimeoutError,\n            HTTPException,\n            SocketError,\n            ProtocolError,\n            BaseSSLError,\n            SSLError,\n            CertificateError,\n        ) as e:\n            # Discard the connection for these exceptions. It will be\n            # replaced during the next _get_conn() call.\n            clean_exit = False\n            if isinstance(e, (BaseSSLError, CertificateError)):\n                e = SSLError(e)\n            elif isinstance(e, (SocketError, NewConnectionError)) and self.proxy:\n                e = ProxyError(\"Cannot connect to proxy.\", e)\n            elif isinstance(e, (SocketError, HTTPException)):\n                e = ProtocolError(\"Connection aborted.\", e)\n\n            retries = retries.increment(\n                method, url, error=e, _pool=self, _stacktrace=sys.exc_info()[2]\n            )\n            retries.sleep()\n\n            # Keep track of the error for the retry warning.\n            err = e\n\n        finally:\n            if not clean_exit:\n                # We hit some kind of exception, handled or otherwise. We need\n                # to throw the connection away unless explicitly told not to.\n                # Close the connection, set the variable to None, and make sure\n                # we put the None back in the pool to avoid leaking it.\n                conn = conn and conn.close()\n                release_this_conn = True\n\n            if release_this_conn:\n                # Put the connection back to be reused. If the connection is\n                # expired then it will be None, which will get replaced with a\n                # fresh connection during _get_conn.\n                self._put_conn(conn)\n\n        if not conn:\n            # Try again\n            log.warning(\n                \"Retrying (%r) after connection broken by '%r': %s\", retries, err, url\n            )\n            return self.urlopen(\n                method,\n                url,\n                body,\n                headers,\n                retries,\n                redirect,\n                assert_same_host,\n                timeout=timeout,\n                pool_timeout=pool_timeout,\n                release_conn=release_conn,\n                chunked=chunked,\n                body_pos=body_pos,\n                **response_kw\n            )\n\n        # Handle redirect?\n        redirect_location = redirect and response.get_redirect_location()\n        if redirect_location:\n            if response.status == 303:\n                method = \"GET\"\n\n            try:\n                retries = retries.increment(method, url, response=response, _pool=self)\n            except MaxRetryError:\n                if retries.raise_on_redirect:\n                    response.drain_conn()\n                    raise\n                return response\n\n            response.drain_conn()\n            retries.sleep_for_retry(response)\n            log.debug(\"Redirecting %s -> %s\", url, redirect_location)\n            return self.urlopen(\n                method,\n                redirect_location,\n                body,\n                headers,\n                retries=retries,\n                redirect=redirect,\n                assert_same_host=assert_same_host,\n                timeout=timeout,\n                pool_timeout=pool_timeout,\n                release_conn=release_conn,\n                chunked=chunked,\n                body_pos=body_pos,\n                **response_kw\n            )\n\n        # Check if we should retry the HTTP response.\n        has_retry_after = bool(response.getheader(\"Retry-After\"))\n        if retries.is_retry(method, response.status, has_retry_after):\n            try:\n                retries = retries.increment(method, url, response=response, _pool=self)\n            except MaxRetryError:\n                if retries.raise_on_status:\n                    response.drain_conn()\n                    raise\n                return response\n\n            response.drain_conn()\n            retries.sleep(response)\n            log.debug(\"Retry: %s\", url)\n            return self.urlopen(\n                method,\n                url,\n                body,\n                headers,\n                retries=retries,\n                redirect=redirect,\n                assert_same_host=assert_same_host,\n                timeout=timeout,\n                pool_timeout=pool_timeout,\n                release_conn=release_conn,\n                chunked=chunked,\n                body_pos=body_pos,\n                **response_kw\n            )\n\n        return response\n\n\nclass HTTPSConnectionPool(HTTPConnectionPool):\n    \"\"\"\n    Same as :class:`.HTTPConnectionPool`, but HTTPS.\n\n    :class:`.HTTPSConnection` uses one of ``assert_fingerprint``,\n    ``assert_hostname`` and ``host`` in this order to verify connections.\n    If ``assert_hostname`` is False, no verification is done.\n\n    The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``,\n    ``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl`\n    is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade\n    the connection socket into an SSL socket.\n    \"\"\"\n\n    scheme = \"https\"\n    ConnectionCls = HTTPSConnection\n\n    def __init__(\n        self,\n        host,\n        port=None,\n        strict=False,\n        timeout=Timeout.DEFAULT_TIMEOUT,\n        maxsize=1,\n        block=False,\n        headers=None,\n        retries=None,\n        _proxy=None,\n        _proxy_headers=None,\n        key_file=None,\n        cert_file=None,\n        cert_reqs=None,\n        key_password=None,\n        ca_certs=None,\n        ssl_version=None,\n        assert_hostname=None,\n        assert_fingerprint=None,\n        ca_cert_dir=None,\n        **conn_kw\n    ):\n\n        HTTPConnectionPool.__init__(\n            self,\n            host,\n            port,\n            strict,\n            timeout,\n            maxsize,\n            block,\n            headers,\n            retries,\n            _proxy,\n            _proxy_headers,\n            **conn_kw\n        )\n\n        self.key_file = key_file\n        self.cert_file = cert_file\n        self.cert_reqs = cert_reqs\n        self.key_password = key_password\n        self.ca_certs = ca_certs\n        self.ca_cert_dir = ca_cert_dir\n        self.ssl_version = ssl_version\n        self.assert_hostname = assert_hostname\n        self.assert_fingerprint = assert_fingerprint\n\n    def _prepare_conn(self, conn):\n        \"\"\"\n        Prepare the ``connection`` for :meth:`urllib3.util.ssl_wrap_socket`\n        and establish the tunnel if proxy is used.\n        \"\"\"\n\n        if isinstance(conn, VerifiedHTTPSConnection):\n            conn.set_cert(\n                key_file=self.key_file,\n                key_password=self.key_password,\n                cert_file=self.cert_file,\n                cert_reqs=self.cert_reqs,\n                ca_certs=self.ca_certs,\n                ca_cert_dir=self.ca_cert_dir,\n                assert_hostname=self.assert_hostname,\n                assert_fingerprint=self.assert_fingerprint,\n            )\n            conn.ssl_version = self.ssl_version\n        return conn\n\n    def _prepare_proxy(self, conn):\n        \"\"\"\n        Establishes a tunnel connection through HTTP CONNECT.\n\n        Tunnel connection is established early because otherwise httplib would\n        improperly set Host: header to proxy's IP:port.\n        \"\"\"\n\n        conn.set_tunnel(self._proxy_host, self.port, self.proxy_headers)\n\n        if self.proxy.scheme == \"https\":\n            conn.tls_in_tls_required = True\n\n        conn.connect()\n\n    def _new_conn(self):\n        \"\"\"\n        Return a fresh :class:`http.client.HTTPSConnection`.\n        \"\"\"\n        self.num_connections += 1\n        log.debug(\n            \"Starting new HTTPS connection (%d): %s:%s\",\n            self.num_connections,\n            self.host,\n            self.port or \"443\",\n        )\n\n        if not self.ConnectionCls or self.ConnectionCls is DummyConnection:\n            raise SSLError(\n                \"Can't connect to HTTPS URL because the SSL module is not available.\"\n            )\n\n        actual_host = self.host\n        actual_port = self.port\n        if self.proxy is not None:\n            actual_host = self.proxy.host\n            actual_port = self.proxy.port\n\n        conn = self.ConnectionCls(\n            host=actual_host,\n            port=actual_port,\n            timeout=self.timeout.connect_timeout,\n            strict=self.strict,\n            cert_file=self.cert_file,\n            key_file=self.key_file,\n            key_password=self.key_password,\n            **self.conn_kw\n        )\n\n        return self._prepare_conn(conn)\n\n    def _validate_conn(self, conn):\n        \"\"\"\n        Called right before a request is made, after the socket is created.\n        \"\"\"\n        super(HTTPSConnectionPool, self)._validate_conn(conn)\n\n        # Force connect early to allow us to validate the connection.\n        if not getattr(conn, \"sock\", None):  # AppEngine might not have  `.sock`\n            conn.connect()\n\n        if not conn.is_verified:\n            warnings.warn(\n                (\n                    \"Unverified HTTPS request is being made to host '%s'. \"\n                    \"Adding certificate verification is strongly advised. See: \"\n                    \"https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html\"\n                    \"#ssl-warnings\" % conn.host\n                ),\n                InsecureRequestWarning,\n            )\n\n\ndef connection_from_url(url, **kw):\n    \"\"\"\n    Given a url, return an :class:`.ConnectionPool` instance of its host.\n\n    This is a shortcut for not having to parse out the scheme, host, and port\n    of the url before creating an :class:`.ConnectionPool` instance.\n\n    :param url:\n        Absolute URL string that must include the scheme. Port is optional.\n\n    :param \\\\**kw:\n        Passes additional parameters to the constructor of the appropriate\n        :class:`.ConnectionPool`. Useful for specifying things like\n        timeout, maxsize, headers, etc.\n\n    Example::\n\n        >>> conn = connection_from_url('http://google.com/')\n        >>> r = conn.request('GET', '/')\n    \"\"\"\n    scheme, host, port = get_host(url)\n    port = port or port_by_scheme.get(scheme, 80)\n    if scheme == \"https\":\n        return HTTPSConnectionPool(host, port=port, **kw)\n    else:\n        return HTTPConnectionPool(host, port=port, **kw)\n\n\ndef _normalize_host(host, scheme):\n    \"\"\"\n    Normalize hosts for comparisons and use with sockets.\n    \"\"\"\n\n    host = normalize_host(host, scheme)\n\n    # httplib doesn't like it when we include brackets in IPv6 addresses\n    # Specifically, if we include brackets but also pass the port then\n    # httplib crazily doubles up the square brackets on the Host header.\n    # Instead, we need to make sure we never pass ``None`` as the port.\n    # However, for backward compatibility reasons we can't actually\n    # *assert* that.  See http://bugs.python.org/issue28539\n    if host.startswith(\"[\") and host.endswith(\"]\"):\n        host = host[1:-1]\n    return host\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/contrib/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/contrib/_appengine_environ.py",
    "content": "\"\"\"\nThis module provides means to detect the App Engine environment.\n\"\"\"\n\nimport os\n\n\ndef is_appengine():\n    return is_local_appengine() or is_prod_appengine()\n\n\ndef is_appengine_sandbox():\n    \"\"\"Reports if the app is running in the first generation sandbox.\n\n    The second generation runtimes are technically still in a sandbox, but it\n    is much less restrictive, so generally you shouldn't need to check for it.\n    see https://cloud.google.com/appengine/docs/standard/runtimes\n    \"\"\"\n    return is_appengine() and os.environ[\"APPENGINE_RUNTIME\"] == \"python27\"\n\n\ndef is_local_appengine():\n    return \"APPENGINE_RUNTIME\" in os.environ and os.environ.get(\n        \"SERVER_SOFTWARE\", \"\"\n    ).startswith(\"Development/\")\n\n\ndef is_prod_appengine():\n    return \"APPENGINE_RUNTIME\" in os.environ and os.environ.get(\n        \"SERVER_SOFTWARE\", \"\"\n    ).startswith(\"Google App Engine/\")\n\n\ndef is_prod_appengine_mvms():\n    \"\"\"Deprecated.\"\"\"\n    return False\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/contrib/_securetransport/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/contrib/_securetransport/bindings.py",
    "content": "\"\"\"\nThis module uses ctypes to bind a whole bunch of functions and constants from\nSecureTransport. The goal here is to provide the low-level API to\nSecureTransport. These are essentially the C-level functions and constants, and\nthey're pretty gross to work with.\n\nThis code is a bastardised version of the code found in Will Bond's oscrypto\nlibrary. An enormous debt is owed to him for blazing this trail for us. For\nthat reason, this code should be considered to be covered both by urllib3's\nlicense and by oscrypto's:\n\n    Copyright (c) 2015-2016 Will Bond <will@wbond.net>\n\n    Permission is hereby granted, free of charge, to any person obtaining a\n    copy of this software and associated documentation files (the \"Software\"),\n    to deal in the Software without restriction, including without limitation\n    the rights to use, copy, modify, merge, publish, distribute, sublicense,\n    and/or sell copies of the Software, and to permit persons to whom the\n    Software is furnished to do so, subject to the following conditions:\n\n    The above copyright notice and this permission notice shall be included in\n    all copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n    DEALINGS IN THE SOFTWARE.\n\"\"\"\nfrom __future__ import absolute_import\n\nimport platform\nfrom ctypes import (\n    CDLL,\n    CFUNCTYPE,\n    POINTER,\n    c_bool,\n    c_byte,\n    c_char_p,\n    c_int32,\n    c_long,\n    c_size_t,\n    c_uint32,\n    c_ulong,\n    c_void_p,\n)\nfrom ctypes.util import find_library\n\nfrom urllib3.packages.six import raise_from\n\nif platform.system() != \"Darwin\":\n    raise ImportError(\"Only macOS is supported\")\n\nversion = platform.mac_ver()[0]\nversion_info = tuple(map(int, version.split(\".\")))\nif version_info < (10, 8):\n    raise OSError(\n        \"Only OS X 10.8 and newer are supported, not %s.%s\"\n        % (version_info[0], version_info[1])\n    )\n\n\ndef load_cdll(name, macos10_16_path):\n    \"\"\"Loads a CDLL by name, falling back to known path on 10.16+\"\"\"\n    try:\n        # Big Sur is technically 11 but we use 10.16 due to the Big Sur\n        # beta being labeled as 10.16.\n        if version_info >= (10, 16):\n            path = macos10_16_path\n        else:\n            path = find_library(name)\n        if not path:\n            raise OSError  # Caught and reraised as 'ImportError'\n        return CDLL(path, use_errno=True)\n    except OSError:\n        raise_from(ImportError(\"The library %s failed to load\" % name), None)\n\n\nSecurity = load_cdll(\n    \"Security\", \"/System/Library/Frameworks/Security.framework/Security\"\n)\nCoreFoundation = load_cdll(\n    \"CoreFoundation\",\n    \"/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation\",\n)\n\n\nBoolean = c_bool\nCFIndex = c_long\nCFStringEncoding = c_uint32\nCFData = c_void_p\nCFString = c_void_p\nCFArray = c_void_p\nCFMutableArray = c_void_p\nCFDictionary = c_void_p\nCFError = c_void_p\nCFType = c_void_p\nCFTypeID = c_ulong\n\nCFTypeRef = POINTER(CFType)\nCFAllocatorRef = c_void_p\n\nOSStatus = c_int32\n\nCFDataRef = POINTER(CFData)\nCFStringRef = POINTER(CFString)\nCFArrayRef = POINTER(CFArray)\nCFMutableArrayRef = POINTER(CFMutableArray)\nCFDictionaryRef = POINTER(CFDictionary)\nCFArrayCallBacks = c_void_p\nCFDictionaryKeyCallBacks = c_void_p\nCFDictionaryValueCallBacks = c_void_p\n\nSecCertificateRef = POINTER(c_void_p)\nSecExternalFormat = c_uint32\nSecExternalItemType = c_uint32\nSecIdentityRef = POINTER(c_void_p)\nSecItemImportExportFlags = c_uint32\nSecItemImportExportKeyParameters = c_void_p\nSecKeychainRef = POINTER(c_void_p)\nSSLProtocol = c_uint32\nSSLCipherSuite = c_uint32\nSSLContextRef = POINTER(c_void_p)\nSecTrustRef = POINTER(c_void_p)\nSSLConnectionRef = c_uint32\nSecTrustResultType = c_uint32\nSecTrustOptionFlags = c_uint32\nSSLProtocolSide = c_uint32\nSSLConnectionType = c_uint32\nSSLSessionOption = c_uint32\n\n\ntry:\n    Security.SecItemImport.argtypes = [\n        CFDataRef,\n        CFStringRef,\n        POINTER(SecExternalFormat),\n        POINTER(SecExternalItemType),\n        SecItemImportExportFlags,\n        POINTER(SecItemImportExportKeyParameters),\n        SecKeychainRef,\n        POINTER(CFArrayRef),\n    ]\n    Security.SecItemImport.restype = OSStatus\n\n    Security.SecCertificateGetTypeID.argtypes = []\n    Security.SecCertificateGetTypeID.restype = CFTypeID\n\n    Security.SecIdentityGetTypeID.argtypes = []\n    Security.SecIdentityGetTypeID.restype = CFTypeID\n\n    Security.SecKeyGetTypeID.argtypes = []\n    Security.SecKeyGetTypeID.restype = CFTypeID\n\n    Security.SecCertificateCreateWithData.argtypes = [CFAllocatorRef, CFDataRef]\n    Security.SecCertificateCreateWithData.restype = SecCertificateRef\n\n    Security.SecCertificateCopyData.argtypes = [SecCertificateRef]\n    Security.SecCertificateCopyData.restype = CFDataRef\n\n    Security.SecCopyErrorMessageString.argtypes = [OSStatus, c_void_p]\n    Security.SecCopyErrorMessageString.restype = CFStringRef\n\n    Security.SecIdentityCreateWithCertificate.argtypes = [\n        CFTypeRef,\n        SecCertificateRef,\n        POINTER(SecIdentityRef),\n    ]\n    Security.SecIdentityCreateWithCertificate.restype = OSStatus\n\n    Security.SecKeychainCreate.argtypes = [\n        c_char_p,\n        c_uint32,\n        c_void_p,\n        Boolean,\n        c_void_p,\n        POINTER(SecKeychainRef),\n    ]\n    Security.SecKeychainCreate.restype = OSStatus\n\n    Security.SecKeychainDelete.argtypes = [SecKeychainRef]\n    Security.SecKeychainDelete.restype = OSStatus\n\n    Security.SecPKCS12Import.argtypes = [\n        CFDataRef,\n        CFDictionaryRef,\n        POINTER(CFArrayRef),\n    ]\n    Security.SecPKCS12Import.restype = OSStatus\n\n    SSLReadFunc = CFUNCTYPE(OSStatus, SSLConnectionRef, c_void_p, POINTER(c_size_t))\n    SSLWriteFunc = CFUNCTYPE(\n        OSStatus, SSLConnectionRef, POINTER(c_byte), POINTER(c_size_t)\n    )\n\n    Security.SSLSetIOFuncs.argtypes = [SSLContextRef, SSLReadFunc, SSLWriteFunc]\n    Security.SSLSetIOFuncs.restype = OSStatus\n\n    Security.SSLSetPeerID.argtypes = [SSLContextRef, c_char_p, c_size_t]\n    Security.SSLSetPeerID.restype = OSStatus\n\n    Security.SSLSetCertificate.argtypes = [SSLContextRef, CFArrayRef]\n    Security.SSLSetCertificate.restype = OSStatus\n\n    Security.SSLSetCertificateAuthorities.argtypes = [SSLContextRef, CFTypeRef, Boolean]\n    Security.SSLSetCertificateAuthorities.restype = OSStatus\n\n    Security.SSLSetConnection.argtypes = [SSLContextRef, SSLConnectionRef]\n    Security.SSLSetConnection.restype = OSStatus\n\n    Security.SSLSetPeerDomainName.argtypes = [SSLContextRef, c_char_p, c_size_t]\n    Security.SSLSetPeerDomainName.restype = OSStatus\n\n    Security.SSLHandshake.argtypes = [SSLContextRef]\n    Security.SSLHandshake.restype = OSStatus\n\n    Security.SSLRead.argtypes = [SSLContextRef, c_char_p, c_size_t, POINTER(c_size_t)]\n    Security.SSLRead.restype = OSStatus\n\n    Security.SSLWrite.argtypes = [SSLContextRef, c_char_p, c_size_t, POINTER(c_size_t)]\n    Security.SSLWrite.restype = OSStatus\n\n    Security.SSLClose.argtypes = [SSLContextRef]\n    Security.SSLClose.restype = OSStatus\n\n    Security.SSLGetNumberSupportedCiphers.argtypes = [SSLContextRef, POINTER(c_size_t)]\n    Security.SSLGetNumberSupportedCiphers.restype = OSStatus\n\n    Security.SSLGetSupportedCiphers.argtypes = [\n        SSLContextRef,\n        POINTER(SSLCipherSuite),\n        POINTER(c_size_t),\n    ]\n    Security.SSLGetSupportedCiphers.restype = OSStatus\n\n    Security.SSLSetEnabledCiphers.argtypes = [\n        SSLContextRef,\n        POINTER(SSLCipherSuite),\n        c_size_t,\n    ]\n    Security.SSLSetEnabledCiphers.restype = OSStatus\n\n    Security.SSLGetNumberEnabledCiphers.argtype = [SSLContextRef, POINTER(c_size_t)]\n    Security.SSLGetNumberEnabledCiphers.restype = OSStatus\n\n    Security.SSLGetEnabledCiphers.argtypes = [\n        SSLContextRef,\n        POINTER(SSLCipherSuite),\n        POINTER(c_size_t),\n    ]\n    Security.SSLGetEnabledCiphers.restype = OSStatus\n\n    Security.SSLGetNegotiatedCipher.argtypes = [SSLContextRef, POINTER(SSLCipherSuite)]\n    Security.SSLGetNegotiatedCipher.restype = OSStatus\n\n    Security.SSLGetNegotiatedProtocolVersion.argtypes = [\n        SSLContextRef,\n        POINTER(SSLProtocol),\n    ]\n    Security.SSLGetNegotiatedProtocolVersion.restype = OSStatus\n\n    Security.SSLCopyPeerTrust.argtypes = [SSLContextRef, POINTER(SecTrustRef)]\n    Security.SSLCopyPeerTrust.restype = OSStatus\n\n    Security.SecTrustSetAnchorCertificates.argtypes = [SecTrustRef, CFArrayRef]\n    Security.SecTrustSetAnchorCertificates.restype = OSStatus\n\n    Security.SecTrustSetAnchorCertificatesOnly.argstypes = [SecTrustRef, Boolean]\n    Security.SecTrustSetAnchorCertificatesOnly.restype = OSStatus\n\n    Security.SecTrustEvaluate.argtypes = [SecTrustRef, POINTER(SecTrustResultType)]\n    Security.SecTrustEvaluate.restype = OSStatus\n\n    Security.SecTrustGetCertificateCount.argtypes = [SecTrustRef]\n    Security.SecTrustGetCertificateCount.restype = CFIndex\n\n    Security.SecTrustGetCertificateAtIndex.argtypes = [SecTrustRef, CFIndex]\n    Security.SecTrustGetCertificateAtIndex.restype = SecCertificateRef\n\n    Security.SSLCreateContext.argtypes = [\n        CFAllocatorRef,\n        SSLProtocolSide,\n        SSLConnectionType,\n    ]\n    Security.SSLCreateContext.restype = SSLContextRef\n\n    Security.SSLSetSessionOption.argtypes = [SSLContextRef, SSLSessionOption, Boolean]\n    Security.SSLSetSessionOption.restype = OSStatus\n\n    Security.SSLSetProtocolVersionMin.argtypes = [SSLContextRef, SSLProtocol]\n    Security.SSLSetProtocolVersionMin.restype = OSStatus\n\n    Security.SSLSetProtocolVersionMax.argtypes = [SSLContextRef, SSLProtocol]\n    Security.SSLSetProtocolVersionMax.restype = OSStatus\n\n    try:\n        Security.SSLSetALPNProtocols.argtypes = [SSLContextRef, CFArrayRef]\n        Security.SSLSetALPNProtocols.restype = OSStatus\n    except AttributeError:\n        # Supported only in 10.12+\n        pass\n\n    Security.SecCopyErrorMessageString.argtypes = [OSStatus, c_void_p]\n    Security.SecCopyErrorMessageString.restype = CFStringRef\n\n    Security.SSLReadFunc = SSLReadFunc\n    Security.SSLWriteFunc = SSLWriteFunc\n    Security.SSLContextRef = SSLContextRef\n    Security.SSLProtocol = SSLProtocol\n    Security.SSLCipherSuite = SSLCipherSuite\n    Security.SecIdentityRef = SecIdentityRef\n    Security.SecKeychainRef = SecKeychainRef\n    Security.SecTrustRef = SecTrustRef\n    Security.SecTrustResultType = SecTrustResultType\n    Security.SecExternalFormat = SecExternalFormat\n    Security.OSStatus = OSStatus\n\n    Security.kSecImportExportPassphrase = CFStringRef.in_dll(\n        Security, \"kSecImportExportPassphrase\"\n    )\n    Security.kSecImportItemIdentity = CFStringRef.in_dll(\n        Security, \"kSecImportItemIdentity\"\n    )\n\n    # CoreFoundation time!\n    CoreFoundation.CFRetain.argtypes = [CFTypeRef]\n    CoreFoundation.CFRetain.restype = CFTypeRef\n\n    CoreFoundation.CFRelease.argtypes = [CFTypeRef]\n    CoreFoundation.CFRelease.restype = None\n\n    CoreFoundation.CFGetTypeID.argtypes = [CFTypeRef]\n    CoreFoundation.CFGetTypeID.restype = CFTypeID\n\n    CoreFoundation.CFStringCreateWithCString.argtypes = [\n        CFAllocatorRef,\n        c_char_p,\n        CFStringEncoding,\n    ]\n    CoreFoundation.CFStringCreateWithCString.restype = CFStringRef\n\n    CoreFoundation.CFStringGetCStringPtr.argtypes = [CFStringRef, CFStringEncoding]\n    CoreFoundation.CFStringGetCStringPtr.restype = c_char_p\n\n    CoreFoundation.CFStringGetCString.argtypes = [\n        CFStringRef,\n        c_char_p,\n        CFIndex,\n        CFStringEncoding,\n    ]\n    CoreFoundation.CFStringGetCString.restype = c_bool\n\n    CoreFoundation.CFDataCreate.argtypes = [CFAllocatorRef, c_char_p, CFIndex]\n    CoreFoundation.CFDataCreate.restype = CFDataRef\n\n    CoreFoundation.CFDataGetLength.argtypes = [CFDataRef]\n    CoreFoundation.CFDataGetLength.restype = CFIndex\n\n    CoreFoundation.CFDataGetBytePtr.argtypes = [CFDataRef]\n    CoreFoundation.CFDataGetBytePtr.restype = c_void_p\n\n    CoreFoundation.CFDictionaryCreate.argtypes = [\n        CFAllocatorRef,\n        POINTER(CFTypeRef),\n        POINTER(CFTypeRef),\n        CFIndex,\n        CFDictionaryKeyCallBacks,\n        CFDictionaryValueCallBacks,\n    ]\n    CoreFoundation.CFDictionaryCreate.restype = CFDictionaryRef\n\n    CoreFoundation.CFDictionaryGetValue.argtypes = [CFDictionaryRef, CFTypeRef]\n    CoreFoundation.CFDictionaryGetValue.restype = CFTypeRef\n\n    CoreFoundation.CFArrayCreate.argtypes = [\n        CFAllocatorRef,\n        POINTER(CFTypeRef),\n        CFIndex,\n        CFArrayCallBacks,\n    ]\n    CoreFoundation.CFArrayCreate.restype = CFArrayRef\n\n    CoreFoundation.CFArrayCreateMutable.argtypes = [\n        CFAllocatorRef,\n        CFIndex,\n        CFArrayCallBacks,\n    ]\n    CoreFoundation.CFArrayCreateMutable.restype = CFMutableArrayRef\n\n    CoreFoundation.CFArrayAppendValue.argtypes = [CFMutableArrayRef, c_void_p]\n    CoreFoundation.CFArrayAppendValue.restype = None\n\n    CoreFoundation.CFArrayGetCount.argtypes = [CFArrayRef]\n    CoreFoundation.CFArrayGetCount.restype = CFIndex\n\n    CoreFoundation.CFArrayGetValueAtIndex.argtypes = [CFArrayRef, CFIndex]\n    CoreFoundation.CFArrayGetValueAtIndex.restype = c_void_p\n\n    CoreFoundation.kCFAllocatorDefault = CFAllocatorRef.in_dll(\n        CoreFoundation, \"kCFAllocatorDefault\"\n    )\n    CoreFoundation.kCFTypeArrayCallBacks = c_void_p.in_dll(\n        CoreFoundation, \"kCFTypeArrayCallBacks\"\n    )\n    CoreFoundation.kCFTypeDictionaryKeyCallBacks = c_void_p.in_dll(\n        CoreFoundation, \"kCFTypeDictionaryKeyCallBacks\"\n    )\n    CoreFoundation.kCFTypeDictionaryValueCallBacks = c_void_p.in_dll(\n        CoreFoundation, \"kCFTypeDictionaryValueCallBacks\"\n    )\n\n    CoreFoundation.CFTypeRef = CFTypeRef\n    CoreFoundation.CFArrayRef = CFArrayRef\n    CoreFoundation.CFStringRef = CFStringRef\n    CoreFoundation.CFDictionaryRef = CFDictionaryRef\n\nexcept (AttributeError):\n    raise ImportError(\"Error initializing ctypes\")\n\n\nclass CFConst(object):\n    \"\"\"\n    A class object that acts as essentially a namespace for CoreFoundation\n    constants.\n    \"\"\"\n\n    kCFStringEncodingUTF8 = CFStringEncoding(0x08000100)\n\n\nclass SecurityConst(object):\n    \"\"\"\n    A class object that acts as essentially a namespace for Security constants.\n    \"\"\"\n\n    kSSLSessionOptionBreakOnServerAuth = 0\n\n    kSSLProtocol2 = 1\n    kSSLProtocol3 = 2\n    kTLSProtocol1 = 4\n    kTLSProtocol11 = 7\n    kTLSProtocol12 = 8\n    # SecureTransport does not support TLS 1.3 even if there's a constant for it\n    kTLSProtocol13 = 10\n    kTLSProtocolMaxSupported = 999\n\n    kSSLClientSide = 1\n    kSSLStreamType = 0\n\n    kSecFormatPEMSequence = 10\n\n    kSecTrustResultInvalid = 0\n    kSecTrustResultProceed = 1\n    # This gap is present on purpose: this was kSecTrustResultConfirm, which\n    # is deprecated.\n    kSecTrustResultDeny = 3\n    kSecTrustResultUnspecified = 4\n    kSecTrustResultRecoverableTrustFailure = 5\n    kSecTrustResultFatalTrustFailure = 6\n    kSecTrustResultOtherError = 7\n\n    errSSLProtocol = -9800\n    errSSLWouldBlock = -9803\n    errSSLClosedGraceful = -9805\n    errSSLClosedNoNotify = -9816\n    errSSLClosedAbort = -9806\n\n    errSSLXCertChainInvalid = -9807\n    errSSLCrypto = -9809\n    errSSLInternal = -9810\n    errSSLCertExpired = -9814\n    errSSLCertNotYetValid = -9815\n    errSSLUnknownRootCert = -9812\n    errSSLNoRootCert = -9813\n    errSSLHostNameMismatch = -9843\n    errSSLPeerHandshakeFail = -9824\n    errSSLPeerUserCancelled = -9839\n    errSSLWeakPeerEphemeralDHKey = -9850\n    errSSLServerAuthCompleted = -9841\n    errSSLRecordOverflow = -9847\n\n    errSecVerifyFailed = -67808\n    errSecNoTrustSettings = -25263\n    errSecItemNotFound = -25300\n    errSecInvalidTrustSettings = -25262\n\n    # Cipher suites. We only pick the ones our default cipher string allows.\n    # Source: https://developer.apple.com/documentation/security/1550981-ssl_cipher_suite_values\n    TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xC02C\n    TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 0xC030\n    TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 0xC02B\n    TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xC02F\n    TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 0xCCA9\n    TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 0xCCA8\n    TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 = 0x009F\n    TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 = 0x009E\n    TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = 0xC024\n    TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 = 0xC028\n    TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA = 0xC00A\n    TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA = 0xC014\n    TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 = 0x006B\n    TLS_DHE_RSA_WITH_AES_256_CBC_SHA = 0x0039\n    TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 = 0xC023\n    TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 = 0xC027\n    TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA = 0xC009\n    TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA = 0xC013\n    TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 = 0x0067\n    TLS_DHE_RSA_WITH_AES_128_CBC_SHA = 0x0033\n    TLS_RSA_WITH_AES_256_GCM_SHA384 = 0x009D\n    TLS_RSA_WITH_AES_128_GCM_SHA256 = 0x009C\n    TLS_RSA_WITH_AES_256_CBC_SHA256 = 0x003D\n    TLS_RSA_WITH_AES_128_CBC_SHA256 = 0x003C\n    TLS_RSA_WITH_AES_256_CBC_SHA = 0x0035\n    TLS_RSA_WITH_AES_128_CBC_SHA = 0x002F\n    TLS_AES_128_GCM_SHA256 = 0x1301\n    TLS_AES_256_GCM_SHA384 = 0x1302\n    TLS_AES_128_CCM_8_SHA256 = 0x1305\n    TLS_AES_128_CCM_SHA256 = 0x1304\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/contrib/_securetransport/low_level.py",
    "content": "\"\"\"\nLow-level helpers for the SecureTransport bindings.\n\nThese are Python functions that are not directly related to the high-level APIs\nbut are necessary to get them to work. They include a whole bunch of low-level\nCoreFoundation messing about and memory management. The concerns in this module\nare almost entirely about trying to avoid memory leaks and providing\nappropriate and useful assistance to the higher-level code.\n\"\"\"\nimport base64\nimport ctypes\nimport itertools\nimport os\nimport re\nimport ssl\nimport struct\nimport tempfile\n\nfrom .bindings import CFConst, CoreFoundation, Security\n\n# This regular expression is used to grab PEM data out of a PEM bundle.\n_PEM_CERTS_RE = re.compile(\n    b\"-----BEGIN CERTIFICATE-----\\n(.*?)\\n-----END CERTIFICATE-----\", re.DOTALL\n)\n\n\ndef _cf_data_from_bytes(bytestring):\n    \"\"\"\n    Given a bytestring, create a CFData object from it. This CFData object must\n    be CFReleased by the caller.\n    \"\"\"\n    return CoreFoundation.CFDataCreate(\n        CoreFoundation.kCFAllocatorDefault, bytestring, len(bytestring)\n    )\n\n\ndef _cf_dictionary_from_tuples(tuples):\n    \"\"\"\n    Given a list of Python tuples, create an associated CFDictionary.\n    \"\"\"\n    dictionary_size = len(tuples)\n\n    # We need to get the dictionary keys and values out in the same order.\n    keys = (t[0] for t in tuples)\n    values = (t[1] for t in tuples)\n    cf_keys = (CoreFoundation.CFTypeRef * dictionary_size)(*keys)\n    cf_values = (CoreFoundation.CFTypeRef * dictionary_size)(*values)\n\n    return CoreFoundation.CFDictionaryCreate(\n        CoreFoundation.kCFAllocatorDefault,\n        cf_keys,\n        cf_values,\n        dictionary_size,\n        CoreFoundation.kCFTypeDictionaryKeyCallBacks,\n        CoreFoundation.kCFTypeDictionaryValueCallBacks,\n    )\n\n\ndef _cfstr(py_bstr):\n    \"\"\"\n    Given a Python binary data, create a CFString.\n    The string must be CFReleased by the caller.\n    \"\"\"\n    c_str = ctypes.c_char_p(py_bstr)\n    cf_str = CoreFoundation.CFStringCreateWithCString(\n        CoreFoundation.kCFAllocatorDefault,\n        c_str,\n        CFConst.kCFStringEncodingUTF8,\n    )\n    return cf_str\n\n\ndef _create_cfstring_array(lst):\n    \"\"\"\n    Given a list of Python binary data, create an associated CFMutableArray.\n    The array must be CFReleased by the caller.\n\n    Raises an ssl.SSLError on failure.\n    \"\"\"\n    cf_arr = None\n    try:\n        cf_arr = CoreFoundation.CFArrayCreateMutable(\n            CoreFoundation.kCFAllocatorDefault,\n            0,\n            ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks),\n        )\n        if not cf_arr:\n            raise MemoryError(\"Unable to allocate memory!\")\n        for item in lst:\n            cf_str = _cfstr(item)\n            if not cf_str:\n                raise MemoryError(\"Unable to allocate memory!\")\n            try:\n                CoreFoundation.CFArrayAppendValue(cf_arr, cf_str)\n            finally:\n                CoreFoundation.CFRelease(cf_str)\n    except BaseException as e:\n        if cf_arr:\n            CoreFoundation.CFRelease(cf_arr)\n        raise ssl.SSLError(\"Unable to allocate array: %s\" % (e,))\n    return cf_arr\n\n\ndef _cf_string_to_unicode(value):\n    \"\"\"\n    Creates a Unicode string from a CFString object. Used entirely for error\n    reporting.\n\n    Yes, it annoys me quite a lot that this function is this complex.\n    \"\"\"\n    value_as_void_p = ctypes.cast(value, ctypes.POINTER(ctypes.c_void_p))\n\n    string = CoreFoundation.CFStringGetCStringPtr(\n        value_as_void_p, CFConst.kCFStringEncodingUTF8\n    )\n    if string is None:\n        buffer = ctypes.create_string_buffer(1024)\n        result = CoreFoundation.CFStringGetCString(\n            value_as_void_p, buffer, 1024, CFConst.kCFStringEncodingUTF8\n        )\n        if not result:\n            raise OSError(\"Error copying C string from CFStringRef\")\n        string = buffer.value\n    if string is not None:\n        string = string.decode(\"utf-8\")\n    return string\n\n\ndef _assert_no_error(error, exception_class=None):\n    \"\"\"\n    Checks the return code and throws an exception if there is an error to\n    report\n    \"\"\"\n    if error == 0:\n        return\n\n    cf_error_string = Security.SecCopyErrorMessageString(error, None)\n    output = _cf_string_to_unicode(cf_error_string)\n    CoreFoundation.CFRelease(cf_error_string)\n\n    if output is None or output == u\"\":\n        output = u\"OSStatus %s\" % error\n\n    if exception_class is None:\n        exception_class = ssl.SSLError\n\n    raise exception_class(output)\n\n\ndef _cert_array_from_pem(pem_bundle):\n    \"\"\"\n    Given a bundle of certs in PEM format, turns them into a CFArray of certs\n    that can be used to validate a cert chain.\n    \"\"\"\n    # Normalize the PEM bundle's line endings.\n    pem_bundle = pem_bundle.replace(b\"\\r\\n\", b\"\\n\")\n\n    der_certs = [\n        base64.b64decode(match.group(1)) for match in _PEM_CERTS_RE.finditer(pem_bundle)\n    ]\n    if not der_certs:\n        raise ssl.SSLError(\"No root certificates specified\")\n\n    cert_array = CoreFoundation.CFArrayCreateMutable(\n        CoreFoundation.kCFAllocatorDefault,\n        0,\n        ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks),\n    )\n    if not cert_array:\n        raise ssl.SSLError(\"Unable to allocate memory!\")\n\n    try:\n        for der_bytes in der_certs:\n            certdata = _cf_data_from_bytes(der_bytes)\n            if not certdata:\n                raise ssl.SSLError(\"Unable to allocate memory!\")\n            cert = Security.SecCertificateCreateWithData(\n                CoreFoundation.kCFAllocatorDefault, certdata\n            )\n            CoreFoundation.CFRelease(certdata)\n            if not cert:\n                raise ssl.SSLError(\"Unable to build cert object!\")\n\n            CoreFoundation.CFArrayAppendValue(cert_array, cert)\n            CoreFoundation.CFRelease(cert)\n    except Exception:\n        # We need to free the array before the exception bubbles further.\n        # We only want to do that if an error occurs: otherwise, the caller\n        # should free.\n        CoreFoundation.CFRelease(cert_array)\n\n    return cert_array\n\n\ndef _is_cert(item):\n    \"\"\"\n    Returns True if a given CFTypeRef is a certificate.\n    \"\"\"\n    expected = Security.SecCertificateGetTypeID()\n    return CoreFoundation.CFGetTypeID(item) == expected\n\n\ndef _is_identity(item):\n    \"\"\"\n    Returns True if a given CFTypeRef is an identity.\n    \"\"\"\n    expected = Security.SecIdentityGetTypeID()\n    return CoreFoundation.CFGetTypeID(item) == expected\n\n\ndef _temporary_keychain():\n    \"\"\"\n    This function creates a temporary Mac keychain that we can use to work with\n    credentials. This keychain uses a one-time password and a temporary file to\n    store the data. We expect to have one keychain per socket. The returned\n    SecKeychainRef must be freed by the caller, including calling\n    SecKeychainDelete.\n\n    Returns a tuple of the SecKeychainRef and the path to the temporary\n    directory that contains it.\n    \"\"\"\n    # Unfortunately, SecKeychainCreate requires a path to a keychain. This\n    # means we cannot use mkstemp to use a generic temporary file. Instead,\n    # we're going to create a temporary directory and a filename to use there.\n    # This filename will be 8 random bytes expanded into base64. We also need\n    # some random bytes to password-protect the keychain we're creating, so we\n    # ask for 40 random bytes.\n    random_bytes = os.urandom(40)\n    filename = base64.b16encode(random_bytes[:8]).decode(\"utf-8\")\n    password = base64.b16encode(random_bytes[8:])  # Must be valid UTF-8\n    tempdirectory = tempfile.mkdtemp()\n\n    keychain_path = os.path.join(tempdirectory, filename).encode(\"utf-8\")\n\n    # We now want to create the keychain itself.\n    keychain = Security.SecKeychainRef()\n    status = Security.SecKeychainCreate(\n        keychain_path, len(password), password, False, None, ctypes.byref(keychain)\n    )\n    _assert_no_error(status)\n\n    # Having created the keychain, we want to pass it off to the caller.\n    return keychain, tempdirectory\n\n\ndef _load_items_from_file(keychain, path):\n    \"\"\"\n    Given a single file, loads all the trust objects from it into arrays and\n    the keychain.\n    Returns a tuple of lists: the first list is a list of identities, the\n    second a list of certs.\n    \"\"\"\n    certificates = []\n    identities = []\n    result_array = None\n\n    with open(path, \"rb\") as f:\n        raw_filedata = f.read()\n\n    try:\n        filedata = CoreFoundation.CFDataCreate(\n            CoreFoundation.kCFAllocatorDefault, raw_filedata, len(raw_filedata)\n        )\n        result_array = CoreFoundation.CFArrayRef()\n        result = Security.SecItemImport(\n            filedata,  # cert data\n            None,  # Filename, leaving it out for now\n            None,  # What the type of the file is, we don't care\n            None,  # what's in the file, we don't care\n            0,  # import flags\n            None,  # key params, can include passphrase in the future\n            keychain,  # The keychain to insert into\n            ctypes.byref(result_array),  # Results\n        )\n        _assert_no_error(result)\n\n        # A CFArray is not very useful to us as an intermediary\n        # representation, so we are going to extract the objects we want\n        # and then free the array. We don't need to keep hold of keys: the\n        # keychain already has them!\n        result_count = CoreFoundation.CFArrayGetCount(result_array)\n        for index in range(result_count):\n            item = CoreFoundation.CFArrayGetValueAtIndex(result_array, index)\n            item = ctypes.cast(item, CoreFoundation.CFTypeRef)\n\n            if _is_cert(item):\n                CoreFoundation.CFRetain(item)\n                certificates.append(item)\n            elif _is_identity(item):\n                CoreFoundation.CFRetain(item)\n                identities.append(item)\n    finally:\n        if result_array:\n            CoreFoundation.CFRelease(result_array)\n\n        CoreFoundation.CFRelease(filedata)\n\n    return (identities, certificates)\n\n\ndef _load_client_cert_chain(keychain, *paths):\n    \"\"\"\n    Load certificates and maybe keys from a number of files. Has the end goal\n    of returning a CFArray containing one SecIdentityRef, and then zero or more\n    SecCertificateRef objects, suitable for use as a client certificate trust\n    chain.\n    \"\"\"\n    # Ok, the strategy.\n    #\n    # This relies on knowing that macOS will not give you a SecIdentityRef\n    # unless you have imported a key into a keychain. This is a somewhat\n    # artificial limitation of macOS (for example, it doesn't necessarily\n    # affect iOS), but there is nothing inside Security.framework that lets you\n    # get a SecIdentityRef without having a key in a keychain.\n    #\n    # So the policy here is we take all the files and iterate them in order.\n    # Each one will use SecItemImport to have one or more objects loaded from\n    # it. We will also point at a keychain that macOS can use to work with the\n    # private key.\n    #\n    # Once we have all the objects, we'll check what we actually have. If we\n    # already have a SecIdentityRef in hand, fab: we'll use that. Otherwise,\n    # we'll take the first certificate (which we assume to be our leaf) and\n    # ask the keychain to give us a SecIdentityRef with that cert's associated\n    # key.\n    #\n    # We'll then return a CFArray containing the trust chain: one\n    # SecIdentityRef and then zero-or-more SecCertificateRef objects. The\n    # responsibility for freeing this CFArray will be with the caller. This\n    # CFArray must remain alive for the entire connection, so in practice it\n    # will be stored with a single SSLSocket, along with the reference to the\n    # keychain.\n    certificates = []\n    identities = []\n\n    # Filter out bad paths.\n    paths = (path for path in paths if path)\n\n    try:\n        for file_path in paths:\n            new_identities, new_certs = _load_items_from_file(keychain, file_path)\n            identities.extend(new_identities)\n            certificates.extend(new_certs)\n\n        # Ok, we have everything. The question is: do we have an identity? If\n        # not, we want to grab one from the first cert we have.\n        if not identities:\n            new_identity = Security.SecIdentityRef()\n            status = Security.SecIdentityCreateWithCertificate(\n                keychain, certificates[0], ctypes.byref(new_identity)\n            )\n            _assert_no_error(status)\n            identities.append(new_identity)\n\n            # We now want to release the original certificate, as we no longer\n            # need it.\n            CoreFoundation.CFRelease(certificates.pop(0))\n\n        # We now need to build a new CFArray that holds the trust chain.\n        trust_chain = CoreFoundation.CFArrayCreateMutable(\n            CoreFoundation.kCFAllocatorDefault,\n            0,\n            ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks),\n        )\n        for item in itertools.chain(identities, certificates):\n            # ArrayAppendValue does a CFRetain on the item. That's fine,\n            # because the finally block will release our other refs to them.\n            CoreFoundation.CFArrayAppendValue(trust_chain, item)\n\n        return trust_chain\n    finally:\n        for obj in itertools.chain(identities, certificates):\n            CoreFoundation.CFRelease(obj)\n\n\nTLS_PROTOCOL_VERSIONS = {\n    \"SSLv2\": (0, 2),\n    \"SSLv3\": (3, 0),\n    \"TLSv1\": (3, 1),\n    \"TLSv1.1\": (3, 2),\n    \"TLSv1.2\": (3, 3),\n}\n\n\ndef _build_tls_unknown_ca_alert(version):\n    \"\"\"\n    Builds a TLS alert record for an unknown CA.\n    \"\"\"\n    ver_maj, ver_min = TLS_PROTOCOL_VERSIONS[version]\n    severity_fatal = 0x02\n    description_unknown_ca = 0x30\n    msg = struct.pack(\">BB\", severity_fatal, description_unknown_ca)\n    msg_len = len(msg)\n    record_type_alert = 0x15\n    record = struct.pack(\">BBBH\", record_type_alert, ver_maj, ver_min, msg_len) + msg\n    return record\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/contrib/appengine.py",
    "content": "\"\"\"\nThis module provides a pool manager that uses Google App Engine's\n`URLFetch Service <https://cloud.google.com/appengine/docs/python/urlfetch>`_.\n\nExample usage::\n\n    from urllib3 import PoolManager\n    from urllib3.contrib.appengine import AppEngineManager, is_appengine_sandbox\n\n    if is_appengine_sandbox():\n        # AppEngineManager uses AppEngine's URLFetch API behind the scenes\n        http = AppEngineManager()\n    else:\n        # PoolManager uses a socket-level API behind the scenes\n        http = PoolManager()\n\n    r = http.request('GET', 'https://google.com/')\n\nThere are `limitations <https://cloud.google.com/appengine/docs/python/\\\nurlfetch/#Python_Quotas_and_limits>`_ to the URLFetch service and it may not be\nthe best choice for your application. There are three options for using\nurllib3 on Google App Engine:\n\n1. You can use :class:`AppEngineManager` with URLFetch. URLFetch is\n   cost-effective in many circumstances as long as your usage is within the\n   limitations.\n2. You can use a normal :class:`~urllib3.PoolManager` by enabling sockets.\n   Sockets also have `limitations and restrictions\n   <https://cloud.google.com/appengine/docs/python/sockets/\\\n   #limitations-and-restrictions>`_ and have a lower free quota than URLFetch.\n   To use sockets, be sure to specify the following in your ``app.yaml``::\n\n        env_variables:\n            GAE_USE_SOCKETS_HTTPLIB : 'true'\n\n3. If you are using `App Engine Flexible\n<https://cloud.google.com/appengine/docs/flexible/>`_, you can use the standard\n:class:`PoolManager` without any configuration or special environment variables.\n\"\"\"\n\nfrom __future__ import absolute_import\n\nimport io\nimport logging\nimport warnings\n\nfrom ..exceptions import (\n    HTTPError,\n    HTTPWarning,\n    MaxRetryError,\n    ProtocolError,\n    SSLError,\n    TimeoutError,\n)\nfrom ..packages.six.moves.urllib.parse import urljoin\nfrom ..request import RequestMethods\nfrom ..response import HTTPResponse\nfrom ..util.retry import Retry\nfrom ..util.timeout import Timeout\nfrom . import _appengine_environ\n\ntry:\n    from google.appengine.api import urlfetch\nexcept ImportError:\n    urlfetch = None\n\n\nlog = logging.getLogger(__name__)\n\n\nclass AppEnginePlatformWarning(HTTPWarning):\n    pass\n\n\nclass AppEnginePlatformError(HTTPError):\n    pass\n\n\nclass AppEngineManager(RequestMethods):\n    \"\"\"\n    Connection manager for Google App Engine sandbox applications.\n\n    This manager uses the URLFetch service directly instead of using the\n    emulated httplib, and is subject to URLFetch limitations as described in\n    the App Engine documentation `here\n    <https://cloud.google.com/appengine/docs/python/urlfetch>`_.\n\n    Notably it will raise an :class:`AppEnginePlatformError` if:\n        * URLFetch is not available.\n        * If you attempt to use this on App Engine Flexible, as full socket\n          support is available.\n        * If a request size is more than 10 megabytes.\n        * If a response size is more than 32 megabytes.\n        * If you use an unsupported request method such as OPTIONS.\n\n    Beyond those cases, it will raise normal urllib3 errors.\n    \"\"\"\n\n    def __init__(\n        self,\n        headers=None,\n        retries=None,\n        validate_certificate=True,\n        urlfetch_retries=True,\n    ):\n        if not urlfetch:\n            raise AppEnginePlatformError(\n                \"URLFetch is not available in this environment.\"\n            )\n\n        warnings.warn(\n            \"urllib3 is using URLFetch on Google App Engine sandbox instead \"\n            \"of sockets. To use sockets directly instead of URLFetch see \"\n            \"https://urllib3.readthedocs.io/en/1.26.x/reference/urllib3.contrib.html.\",\n            AppEnginePlatformWarning,\n        )\n\n        RequestMethods.__init__(self, headers)\n        self.validate_certificate = validate_certificate\n        self.urlfetch_retries = urlfetch_retries\n\n        self.retries = retries or Retry.DEFAULT\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        # Return False to re-raise any potential exceptions\n        return False\n\n    def urlopen(\n        self,\n        method,\n        url,\n        body=None,\n        headers=None,\n        retries=None,\n        redirect=True,\n        timeout=Timeout.DEFAULT_TIMEOUT,\n        **response_kw\n    ):\n\n        retries = self._get_retries(retries, redirect)\n\n        try:\n            follow_redirects = redirect and retries.redirect != 0 and retries.total\n            response = urlfetch.fetch(\n                url,\n                payload=body,\n                method=method,\n                headers=headers or {},\n                allow_truncated=False,\n                follow_redirects=self.urlfetch_retries and follow_redirects,\n                deadline=self._get_absolute_timeout(timeout),\n                validate_certificate=self.validate_certificate,\n            )\n        except urlfetch.DeadlineExceededError as e:\n            raise TimeoutError(self, e)\n\n        except urlfetch.InvalidURLError as e:\n            if \"too large\" in str(e):\n                raise AppEnginePlatformError(\n                    \"URLFetch request too large, URLFetch only \"\n                    \"supports requests up to 10mb in size.\",\n                    e,\n                )\n            raise ProtocolError(e)\n\n        except urlfetch.DownloadError as e:\n            if \"Too many redirects\" in str(e):\n                raise MaxRetryError(self, url, reason=e)\n            raise ProtocolError(e)\n\n        except urlfetch.ResponseTooLargeError as e:\n            raise AppEnginePlatformError(\n                \"URLFetch response too large, URLFetch only supports\"\n                \"responses up to 32mb in size.\",\n                e,\n            )\n\n        except urlfetch.SSLCertificateError as e:\n            raise SSLError(e)\n\n        except urlfetch.InvalidMethodError as e:\n            raise AppEnginePlatformError(\n                \"URLFetch does not support method: %s\" % method, e\n            )\n\n        http_response = self._urlfetch_response_to_http_response(\n            response, retries=retries, **response_kw\n        )\n\n        # Handle redirect?\n        redirect_location = redirect and http_response.get_redirect_location()\n        if redirect_location:\n            # Check for redirect response\n            if self.urlfetch_retries and retries.raise_on_redirect:\n                raise MaxRetryError(self, url, \"too many redirects\")\n            else:\n                if http_response.status == 303:\n                    method = \"GET\"\n\n                try:\n                    retries = retries.increment(\n                        method, url, response=http_response, _pool=self\n                    )\n                except MaxRetryError:\n                    if retries.raise_on_redirect:\n                        raise MaxRetryError(self, url, \"too many redirects\")\n                    return http_response\n\n                retries.sleep_for_retry(http_response)\n                log.debug(\"Redirecting %s -> %s\", url, redirect_location)\n                redirect_url = urljoin(url, redirect_location)\n                return self.urlopen(\n                    method,\n                    redirect_url,\n                    body,\n                    headers,\n                    retries=retries,\n                    redirect=redirect,\n                    timeout=timeout,\n                    **response_kw\n                )\n\n        # Check if we should retry the HTTP response.\n        has_retry_after = bool(http_response.getheader(\"Retry-After\"))\n        if retries.is_retry(method, http_response.status, has_retry_after):\n            retries = retries.increment(method, url, response=http_response, _pool=self)\n            log.debug(\"Retry: %s\", url)\n            retries.sleep(http_response)\n            return self.urlopen(\n                method,\n                url,\n                body=body,\n                headers=headers,\n                retries=retries,\n                redirect=redirect,\n                timeout=timeout,\n                **response_kw\n            )\n\n        return http_response\n\n    def _urlfetch_response_to_http_response(self, urlfetch_resp, **response_kw):\n\n        if is_prod_appengine():\n            # Production GAE handles deflate encoding automatically, but does\n            # not remove the encoding header.\n            content_encoding = urlfetch_resp.headers.get(\"content-encoding\")\n\n            if content_encoding == \"deflate\":\n                del urlfetch_resp.headers[\"content-encoding\"]\n\n        transfer_encoding = urlfetch_resp.headers.get(\"transfer-encoding\")\n        # We have a full response's content,\n        # so let's make sure we don't report ourselves as chunked data.\n        if transfer_encoding == \"chunked\":\n            encodings = transfer_encoding.split(\",\")\n            encodings.remove(\"chunked\")\n            urlfetch_resp.headers[\"transfer-encoding\"] = \",\".join(encodings)\n\n        original_response = HTTPResponse(\n            # In order for decoding to work, we must present the content as\n            # a file-like object.\n            body=io.BytesIO(urlfetch_resp.content),\n            msg=urlfetch_resp.header_msg,\n            headers=urlfetch_resp.headers,\n            status=urlfetch_resp.status_code,\n            **response_kw\n        )\n\n        return HTTPResponse(\n            body=io.BytesIO(urlfetch_resp.content),\n            headers=urlfetch_resp.headers,\n            status=urlfetch_resp.status_code,\n            original_response=original_response,\n            **response_kw\n        )\n\n    def _get_absolute_timeout(self, timeout):\n        if timeout is Timeout.DEFAULT_TIMEOUT:\n            return None  # Defer to URLFetch's default.\n        if isinstance(timeout, Timeout):\n            if timeout._read is not None or timeout._connect is not None:\n                warnings.warn(\n                    \"URLFetch does not support granular timeout settings, \"\n                    \"reverting to total or default URLFetch timeout.\",\n                    AppEnginePlatformWarning,\n                )\n            return timeout.total\n        return timeout\n\n    def _get_retries(self, retries, redirect):\n        if not isinstance(retries, Retry):\n            retries = Retry.from_int(retries, redirect=redirect, default=self.retries)\n\n        if retries.connect or retries.read or retries.redirect:\n            warnings.warn(\n                \"URLFetch only supports total retries and does not \"\n                \"recognize connect, read, or redirect retry parameters.\",\n                AppEnginePlatformWarning,\n            )\n\n        return retries\n\n\n# Alias methods from _appengine_environ to maintain public API interface.\n\nis_appengine = _appengine_environ.is_appengine\nis_appengine_sandbox = _appengine_environ.is_appengine_sandbox\nis_local_appengine = _appengine_environ.is_local_appengine\nis_prod_appengine = _appengine_environ.is_prod_appengine\nis_prod_appengine_mvms = _appengine_environ.is_prod_appengine_mvms\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/contrib/ntlmpool.py",
    "content": "\"\"\"\nNTLM authenticating pool, contributed by erikcederstran\n\nIssue #10, see: http://code.google.com/p/urllib3/issues/detail?id=10\n\"\"\"\nfrom __future__ import absolute_import\n\nimport warnings\nfrom logging import getLogger\n\nfrom ntlm import ntlm\n\nfrom .. import HTTPSConnectionPool\nfrom ..packages.six.moves.http_client import HTTPSConnection\n\nwarnings.warn(\n    \"The 'urllib3.contrib.ntlmpool' module is deprecated and will be removed \"\n    \"in urllib3 v2.0 release, urllib3 is not able to support it properly due \"\n    \"to reasons listed in issue: https://github.com/urllib3/urllib3/issues/2282. \"\n    \"If you are a user of this module please comment in the mentioned issue.\",\n    DeprecationWarning,\n)\n\nlog = getLogger(__name__)\n\n\nclass NTLMConnectionPool(HTTPSConnectionPool):\n    \"\"\"\n    Implements an NTLM authentication version of an urllib3 connection pool\n    \"\"\"\n\n    scheme = \"https\"\n\n    def __init__(self, user, pw, authurl, *args, **kwargs):\n        \"\"\"\n        authurl is a random URL on the server that is protected by NTLM.\n        user is the Windows user, probably in the DOMAIN\\\\username format.\n        pw is the password for the user.\n        \"\"\"\n        super(NTLMConnectionPool, self).__init__(*args, **kwargs)\n        self.authurl = authurl\n        self.rawuser = user\n        user_parts = user.split(\"\\\\\", 1)\n        self.domain = user_parts[0].upper()\n        self.user = user_parts[1]\n        self.pw = pw\n\n    def _new_conn(self):\n        # Performs the NTLM handshake that secures the connection. The socket\n        # must be kept open while requests are performed.\n        self.num_connections += 1\n        log.debug(\n            \"Starting NTLM HTTPS connection no. %d: https://%s%s\",\n            self.num_connections,\n            self.host,\n            self.authurl,\n        )\n\n        headers = {\"Connection\": \"Keep-Alive\"}\n        req_header = \"Authorization\"\n        resp_header = \"www-authenticate\"\n\n        conn = HTTPSConnection(host=self.host, port=self.port)\n\n        # Send negotiation message\n        headers[req_header] = \"NTLM %s\" % ntlm.create_NTLM_NEGOTIATE_MESSAGE(\n            self.rawuser\n        )\n        log.debug(\"Request headers: %s\", headers)\n        conn.request(\"GET\", self.authurl, None, headers)\n        res = conn.getresponse()\n        reshdr = dict(res.getheaders())\n        log.debug(\"Response status: %s %s\", res.status, res.reason)\n        log.debug(\"Response headers: %s\", reshdr)\n        log.debug(\"Response data: %s [...]\", res.read(100))\n\n        # Remove the reference to the socket, so that it can not be closed by\n        # the response object (we want to keep the socket open)\n        res.fp = None\n\n        # Server should respond with a challenge message\n        auth_header_values = reshdr[resp_header].split(\", \")\n        auth_header_value = None\n        for s in auth_header_values:\n            if s[:5] == \"NTLM \":\n                auth_header_value = s[5:]\n        if auth_header_value is None:\n            raise Exception(\n                \"Unexpected %s response header: %s\" % (resp_header, reshdr[resp_header])\n            )\n\n        # Send authentication message\n        ServerChallenge, NegotiateFlags = ntlm.parse_NTLM_CHALLENGE_MESSAGE(\n            auth_header_value\n        )\n        auth_msg = ntlm.create_NTLM_AUTHENTICATE_MESSAGE(\n            ServerChallenge, self.user, self.domain, self.pw, NegotiateFlags\n        )\n        headers[req_header] = \"NTLM %s\" % auth_msg\n        log.debug(\"Request headers: %s\", headers)\n        conn.request(\"GET\", self.authurl, None, headers)\n        res = conn.getresponse()\n        log.debug(\"Response status: %s %s\", res.status, res.reason)\n        log.debug(\"Response headers: %s\", dict(res.getheaders()))\n        log.debug(\"Response data: %s [...]\", res.read()[:100])\n        if res.status != 200:\n            if res.status == 401:\n                raise Exception(\"Server rejected request: wrong username or password\")\n            raise Exception(\"Wrong server response: %s %s\" % (res.status, res.reason))\n\n        res.fp = None\n        log.debug(\"Connection established\")\n        return conn\n\n    def urlopen(\n        self,\n        method,\n        url,\n        body=None,\n        headers=None,\n        retries=3,\n        redirect=True,\n        assert_same_host=True,\n    ):\n        if headers is None:\n            headers = {}\n        headers[\"Connection\"] = \"Keep-Alive\"\n        return super(NTLMConnectionPool, self).urlopen(\n            method, url, body, headers, retries, redirect, assert_same_host\n        )\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/contrib/pyopenssl.py",
    "content": "\"\"\"\nTLS with SNI_-support for Python 2. Follow these instructions if you would\nlike to verify TLS certificates in Python 2. Note, the default libraries do\n*not* do certificate checking; you need to do additional work to validate\ncertificates yourself.\n\nThis needs the following packages installed:\n\n* `pyOpenSSL`_ (tested with 16.0.0)\n* `cryptography`_ (minimum 1.3.4, from pyopenssl)\n* `idna`_ (minimum 2.0, from cryptography)\n\nHowever, pyopenssl depends on cryptography, which depends on idna, so while we\nuse all three directly here we end up having relatively few packages required.\n\nYou can install them with the following command:\n\n.. code-block:: bash\n\n    $ python -m pip install pyopenssl cryptography idna\n\nTo activate certificate checking, call\n:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code\nbefore you begin making HTTP requests. This can be done in a ``sitecustomize``\nmodule, or at any other time before your application begins using ``urllib3``,\nlike this:\n\n.. code-block:: python\n\n    try:\n        import urllib3.contrib.pyopenssl\n        urllib3.contrib.pyopenssl.inject_into_urllib3()\n    except ImportError:\n        pass\n\nNow you can use :mod:`urllib3` as you normally would, and it will support SNI\nwhen the required modules are installed.\n\nActivating this module also has the positive side effect of disabling SSL/TLS\ncompression in Python 2 (see `CRIME attack`_).\n\n.. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication\n.. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit)\n.. _pyopenssl: https://www.pyopenssl.org\n.. _cryptography: https://cryptography.io\n.. _idna: https://github.com/kjd/idna\n\"\"\"\nfrom __future__ import absolute_import\n\nimport OpenSSL.SSL\nfrom cryptography import x509\nfrom cryptography.hazmat.backends.openssl import backend as openssl_backend\nfrom cryptography.hazmat.backends.openssl.x509 import _Certificate\n\ntry:\n    from cryptography.x509 import UnsupportedExtension\nexcept ImportError:\n    # UnsupportedExtension is gone in cryptography >= 2.1.0\n    class UnsupportedExtension(Exception):\n        pass\n\n\nfrom io import BytesIO\nfrom socket import error as SocketError\nfrom socket import timeout\n\ntry:  # Platform-specific: Python 2\n    from socket import _fileobject\nexcept ImportError:  # Platform-specific: Python 3\n    _fileobject = None\n    from ..packages.backports.makefile import backport_makefile\n\nimport logging\nimport ssl\nimport sys\n\nfrom .. import util\nfrom ..packages import six\nfrom ..util.ssl_ import PROTOCOL_TLS_CLIENT\n\n__all__ = [\"inject_into_urllib3\", \"extract_from_urllib3\"]\n\n# SNI always works.\nHAS_SNI = True\n\n# Map from urllib3 to PyOpenSSL compatible parameter-values.\n_openssl_versions = {\n    util.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD,\n    PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD,\n    ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,\n}\n\nif hasattr(ssl, \"PROTOCOL_SSLv3\") and hasattr(OpenSSL.SSL, \"SSLv3_METHOD\"):\n    _openssl_versions[ssl.PROTOCOL_SSLv3] = OpenSSL.SSL.SSLv3_METHOD\n\nif hasattr(ssl, \"PROTOCOL_TLSv1_1\") and hasattr(OpenSSL.SSL, \"TLSv1_1_METHOD\"):\n    _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD\n\nif hasattr(ssl, \"PROTOCOL_TLSv1_2\") and hasattr(OpenSSL.SSL, \"TLSv1_2_METHOD\"):\n    _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD\n\n\n_stdlib_to_openssl_verify = {\n    ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE,\n    ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER,\n    ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER\n    + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT,\n}\n_openssl_to_stdlib_verify = dict((v, k) for k, v in _stdlib_to_openssl_verify.items())\n\n# OpenSSL will only write 16K at a time\nSSL_WRITE_BLOCKSIZE = 16384\n\norig_util_HAS_SNI = util.HAS_SNI\norig_util_SSLContext = util.ssl_.SSLContext\n\n\nlog = logging.getLogger(__name__)\n\n\ndef inject_into_urllib3():\n    \"Monkey-patch urllib3 with PyOpenSSL-backed SSL-support.\"\n\n    _validate_dependencies_met()\n\n    util.SSLContext = PyOpenSSLContext\n    util.ssl_.SSLContext = PyOpenSSLContext\n    util.HAS_SNI = HAS_SNI\n    util.ssl_.HAS_SNI = HAS_SNI\n    util.IS_PYOPENSSL = True\n    util.ssl_.IS_PYOPENSSL = True\n\n\ndef extract_from_urllib3():\n    \"Undo monkey-patching by :func:`inject_into_urllib3`.\"\n\n    util.SSLContext = orig_util_SSLContext\n    util.ssl_.SSLContext = orig_util_SSLContext\n    util.HAS_SNI = orig_util_HAS_SNI\n    util.ssl_.HAS_SNI = orig_util_HAS_SNI\n    util.IS_PYOPENSSL = False\n    util.ssl_.IS_PYOPENSSL = False\n\n\ndef _validate_dependencies_met():\n    \"\"\"\n    Verifies that PyOpenSSL's package-level dependencies have been met.\n    Throws `ImportError` if they are not met.\n    \"\"\"\n    # Method added in `cryptography==1.1`; not available in older versions\n    from cryptography.x509.extensions import Extensions\n\n    if getattr(Extensions, \"get_extension_for_class\", None) is None:\n        raise ImportError(\n            \"'cryptography' module missing required functionality.  \"\n            \"Try upgrading to v1.3.4 or newer.\"\n        )\n\n    # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509\n    # attribute is only present on those versions.\n    from OpenSSL.crypto import X509\n\n    x509 = X509()\n    if getattr(x509, \"_x509\", None) is None:\n        raise ImportError(\n            \"'pyOpenSSL' module missing required functionality. \"\n            \"Try upgrading to v0.14 or newer.\"\n        )\n\n\ndef _dnsname_to_stdlib(name):\n    \"\"\"\n    Converts a dNSName SubjectAlternativeName field to the form used by the\n    standard library on the given Python version.\n\n    Cryptography produces a dNSName as a unicode string that was idna-decoded\n    from ASCII bytes. We need to idna-encode that string to get it back, and\n    then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib\n    uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8).\n\n    If the name cannot be idna-encoded then we return None signalling that\n    the name given should be skipped.\n    \"\"\"\n\n    def idna_encode(name):\n        \"\"\"\n        Borrowed wholesale from the Python Cryptography Project. It turns out\n        that we can't just safely call `idna.encode`: it can explode for\n        wildcard names. This avoids that problem.\n        \"\"\"\n        import idna\n\n        try:\n            for prefix in [u\"*.\", u\".\"]:\n                if name.startswith(prefix):\n                    name = name[len(prefix) :]\n                    return prefix.encode(\"ascii\") + idna.encode(name)\n            return idna.encode(name)\n        except idna.core.IDNAError:\n            return None\n\n    # Don't send IPv6 addresses through the IDNA encoder.\n    if \":\" in name:\n        return name\n\n    name = idna_encode(name)\n    if name is None:\n        return None\n    elif sys.version_info >= (3, 0):\n        name = name.decode(\"utf-8\")\n    return name\n\n\ndef get_subj_alt_name(peer_cert):\n    \"\"\"\n    Given an PyOpenSSL certificate, provides all the subject alternative names.\n    \"\"\"\n    # Pass the cert to cryptography, which has much better APIs for this.\n    if hasattr(peer_cert, \"to_cryptography\"):\n        cert = peer_cert.to_cryptography()\n    else:\n        # This is technically using private APIs, but should work across all\n        # relevant versions before PyOpenSSL got a proper API for this.\n        cert = _Certificate(openssl_backend, peer_cert._x509)\n\n    # We want to find the SAN extension. Ask Cryptography to locate it (it's\n    # faster than looping in Python)\n    try:\n        ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value\n    except x509.ExtensionNotFound:\n        # No such extension, return the empty list.\n        return []\n    except (\n        x509.DuplicateExtension,\n        UnsupportedExtension,\n        x509.UnsupportedGeneralNameType,\n        UnicodeError,\n    ) as e:\n        # A problem has been found with the quality of the certificate. Assume\n        # no SAN field is present.\n        log.warning(\n            \"A problem was encountered with the certificate that prevented \"\n            \"urllib3 from finding the SubjectAlternativeName field. This can \"\n            \"affect certificate validation. The error was %s\",\n            e,\n        )\n        return []\n\n    # We want to return dNSName and iPAddress fields. We need to cast the IPs\n    # back to strings because the match_hostname function wants them as\n    # strings.\n    # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8\n    # decoded. This is pretty frustrating, but that's what the standard library\n    # does with certificates, and so we need to attempt to do the same.\n    # We also want to skip over names which cannot be idna encoded.\n    names = [\n        (\"DNS\", name)\n        for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName))\n        if name is not None\n    ]\n    names.extend(\n        (\"IP Address\", str(name)) for name in ext.get_values_for_type(x509.IPAddress)\n    )\n\n    return names\n\n\nclass WrappedSocket(object):\n    \"\"\"API-compatibility wrapper for Python OpenSSL's Connection-class.\n\n    Note: _makefile_refs, _drop() and _reuse() are needed for the garbage\n    collector of pypy.\n    \"\"\"\n\n    def __init__(self, connection, socket, suppress_ragged_eofs=True):\n        self.connection = connection\n        self.socket = socket\n        self.suppress_ragged_eofs = suppress_ragged_eofs\n        self._makefile_refs = 0\n        self._closed = False\n\n    def fileno(self):\n        return self.socket.fileno()\n\n    # Copy-pasted from Python 3.5 source code\n    def _decref_socketios(self):\n        if self._makefile_refs > 0:\n            self._makefile_refs -= 1\n        if self._closed:\n            self.close()\n\n    def recv(self, *args, **kwargs):\n        try:\n            data = self.connection.recv(*args, **kwargs)\n        except OpenSSL.SSL.SysCallError as e:\n            if self.suppress_ragged_eofs and e.args == (-1, \"Unexpected EOF\"):\n                return b\"\"\n            else:\n                raise SocketError(str(e))\n        except OpenSSL.SSL.ZeroReturnError:\n            if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:\n                return b\"\"\n            else:\n                raise\n        except OpenSSL.SSL.WantReadError:\n            if not util.wait_for_read(self.socket, self.socket.gettimeout()):\n                raise timeout(\"The read operation timed out\")\n            else:\n                return self.recv(*args, **kwargs)\n\n        # TLS 1.3 post-handshake authentication\n        except OpenSSL.SSL.Error as e:\n            raise ssl.SSLError(\"read error: %r\" % e)\n        else:\n            return data\n\n    def recv_into(self, *args, **kwargs):\n        try:\n            return self.connection.recv_into(*args, **kwargs)\n        except OpenSSL.SSL.SysCallError as e:\n            if self.suppress_ragged_eofs and e.args == (-1, \"Unexpected EOF\"):\n                return 0\n            else:\n                raise SocketError(str(e))\n        except OpenSSL.SSL.ZeroReturnError:\n            if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:\n                return 0\n            else:\n                raise\n        except OpenSSL.SSL.WantReadError:\n            if not util.wait_for_read(self.socket, self.socket.gettimeout()):\n                raise timeout(\"The read operation timed out\")\n            else:\n                return self.recv_into(*args, **kwargs)\n\n        # TLS 1.3 post-handshake authentication\n        except OpenSSL.SSL.Error as e:\n            raise ssl.SSLError(\"read error: %r\" % e)\n\n    def settimeout(self, timeout):\n        return self.socket.settimeout(timeout)\n\n    def _send_until_done(self, data):\n        while True:\n            try:\n                return self.connection.send(data)\n            except OpenSSL.SSL.WantWriteError:\n                if not util.wait_for_write(self.socket, self.socket.gettimeout()):\n                    raise timeout()\n                continue\n            except OpenSSL.SSL.SysCallError as e:\n                raise SocketError(str(e))\n\n    def sendall(self, data):\n        total_sent = 0\n        while total_sent < len(data):\n            sent = self._send_until_done(\n                data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE]\n            )\n            total_sent += sent\n\n    def shutdown(self):\n        # FIXME rethrow compatible exceptions should we ever use this\n        self.connection.shutdown()\n\n    def close(self):\n        if self._makefile_refs < 1:\n            try:\n                self._closed = True\n                return self.connection.close()\n            except OpenSSL.SSL.Error:\n                return\n        else:\n            self._makefile_refs -= 1\n\n    def getpeercert(self, binary_form=False):\n        x509 = self.connection.get_peer_certificate()\n\n        if not x509:\n            return x509\n\n        if binary_form:\n            return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509)\n\n        return {\n            \"subject\": (((\"commonName\", x509.get_subject().CN),),),\n            \"subjectAltName\": get_subj_alt_name(x509),\n        }\n\n    def version(self):\n        return self.connection.get_protocol_version_name()\n\n    def _reuse(self):\n        self._makefile_refs += 1\n\n    def _drop(self):\n        if self._makefile_refs < 1:\n            self.close()\n        else:\n            self._makefile_refs -= 1\n\n\nif _fileobject:  # Platform-specific: Python 2\n\n    def makefile(self, mode, bufsize=-1):\n        self._makefile_refs += 1\n        return _fileobject(self, mode, bufsize, close=True)\n\n\nelse:  # Platform-specific: Python 3\n    makefile = backport_makefile\n\nWrappedSocket.makefile = makefile\n\n\nclass PyOpenSSLContext(object):\n    \"\"\"\n    I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible\n    for translating the interface of the standard library ``SSLContext`` object\n    to calls into PyOpenSSL.\n    \"\"\"\n\n    def __init__(self, protocol):\n        self.protocol = _openssl_versions[protocol]\n        self._ctx = OpenSSL.SSL.Context(self.protocol)\n        self._options = 0\n        self.check_hostname = False\n\n    @property\n    def options(self):\n        return self._options\n\n    @options.setter\n    def options(self, value):\n        self._options = value\n        self._ctx.set_options(value)\n\n    @property\n    def verify_mode(self):\n        return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()]\n\n    @verify_mode.setter\n    def verify_mode(self, value):\n        self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback)\n\n    def set_default_verify_paths(self):\n        self._ctx.set_default_verify_paths()\n\n    def set_ciphers(self, ciphers):\n        if isinstance(ciphers, six.text_type):\n            ciphers = ciphers.encode(\"utf-8\")\n        self._ctx.set_cipher_list(ciphers)\n\n    def load_verify_locations(self, cafile=None, capath=None, cadata=None):\n        if cafile is not None:\n            cafile = cafile.encode(\"utf-8\")\n        if capath is not None:\n            capath = capath.encode(\"utf-8\")\n        try:\n            self._ctx.load_verify_locations(cafile, capath)\n            if cadata is not None:\n                self._ctx.load_verify_locations(BytesIO(cadata))\n        except OpenSSL.SSL.Error as e:\n            raise ssl.SSLError(\"unable to load trusted certificates: %r\" % e)\n\n    def load_cert_chain(self, certfile, keyfile=None, password=None):\n        self._ctx.use_certificate_chain_file(certfile)\n        if password is not None:\n            if not isinstance(password, six.binary_type):\n                password = password.encode(\"utf-8\")\n            self._ctx.set_passwd_cb(lambda *_: password)\n        self._ctx.use_privatekey_file(keyfile or certfile)\n\n    def set_alpn_protocols(self, protocols):\n        protocols = [six.ensure_binary(p) for p in protocols]\n        return self._ctx.set_alpn_protos(protocols)\n\n    def wrap_socket(\n        self,\n        sock,\n        server_side=False,\n        do_handshake_on_connect=True,\n        suppress_ragged_eofs=True,\n        server_hostname=None,\n    ):\n        cnx = OpenSSL.SSL.Connection(self._ctx, sock)\n\n        if isinstance(server_hostname, six.text_type):  # Platform-specific: Python 3\n            server_hostname = server_hostname.encode(\"utf-8\")\n\n        if server_hostname is not None:\n            cnx.set_tlsext_host_name(server_hostname)\n\n        cnx.set_connect_state()\n\n        while True:\n            try:\n                cnx.do_handshake()\n            except OpenSSL.SSL.WantReadError:\n                if not util.wait_for_read(sock, sock.gettimeout()):\n                    raise timeout(\"select timed out\")\n                continue\n            except OpenSSL.SSL.Error as e:\n                raise ssl.SSLError(\"bad handshake: %r\" % e)\n            break\n\n        return WrappedSocket(cnx, sock)\n\n\ndef _verify_callback(cnx, x509, err_no, err_depth, return_code):\n    return err_no == 0\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/contrib/securetransport.py",
    "content": "\"\"\"\nSecureTranport support for urllib3 via ctypes.\n\nThis makes platform-native TLS available to urllib3 users on macOS without the\nuse of a compiler. This is an important feature because the Python Package\nIndex is moving to become a TLSv1.2-or-higher server, and the default OpenSSL\nthat ships with macOS is not capable of doing TLSv1.2. The only way to resolve\nthis is to give macOS users an alternative solution to the problem, and that\nsolution is to use SecureTransport.\n\nWe use ctypes here because this solution must not require a compiler. That's\nbecause pip is not allowed to require a compiler either.\n\nThis is not intended to be a seriously long-term solution to this problem.\nThe hope is that PEP 543 will eventually solve this issue for us, at which\npoint we can retire this contrib module. But in the short term, we need to\nsolve the impending tire fire that is Python on Mac without this kind of\ncontrib module. So...here we are.\n\nTo use this module, simply import and inject it::\n\n    import urllib3.contrib.securetransport\n    urllib3.contrib.securetransport.inject_into_urllib3()\n\nHappy TLSing!\n\nThis code is a bastardised version of the code found in Will Bond's oscrypto\nlibrary. An enormous debt is owed to him for blazing this trail for us. For\nthat reason, this code should be considered to be covered both by urllib3's\nlicense and by oscrypto's:\n\n.. code-block::\n\n    Copyright (c) 2015-2016 Will Bond <will@wbond.net>\n\n    Permission is hereby granted, free of charge, to any person obtaining a\n    copy of this software and associated documentation files (the \"Software\"),\n    to deal in the Software without restriction, including without limitation\n    the rights to use, copy, modify, merge, publish, distribute, sublicense,\n    and/or sell copies of the Software, and to permit persons to whom the\n    Software is furnished to do so, subject to the following conditions:\n\n    The above copyright notice and this permission notice shall be included in\n    all copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n    DEALINGS IN THE SOFTWARE.\n\"\"\"\nfrom __future__ import absolute_import\n\nimport contextlib\nimport ctypes\nimport errno\nimport os.path\nimport shutil\nimport socket\nimport ssl\nimport struct\nimport threading\nimport weakref\n\nimport six\n\nfrom .. import util\nfrom ..util.ssl_ import PROTOCOL_TLS_CLIENT\nfrom ._securetransport.bindings import CoreFoundation, Security, SecurityConst\nfrom ._securetransport.low_level import (\n    _assert_no_error,\n    _build_tls_unknown_ca_alert,\n    _cert_array_from_pem,\n    _create_cfstring_array,\n    _load_client_cert_chain,\n    _temporary_keychain,\n)\n\ntry:  # Platform-specific: Python 2\n    from socket import _fileobject\nexcept ImportError:  # Platform-specific: Python 3\n    _fileobject = None\n    from ..packages.backports.makefile import backport_makefile\n\n__all__ = [\"inject_into_urllib3\", \"extract_from_urllib3\"]\n\n# SNI always works\nHAS_SNI = True\n\norig_util_HAS_SNI = util.HAS_SNI\norig_util_SSLContext = util.ssl_.SSLContext\n\n# This dictionary is used by the read callback to obtain a handle to the\n# calling wrapped socket. This is a pretty silly approach, but for now it'll\n# do. I feel like I should be able to smuggle a handle to the wrapped socket\n# directly in the SSLConnectionRef, but for now this approach will work I\n# guess.\n#\n# We need to lock around this structure for inserts, but we don't do it for\n# reads/writes in the callbacks. The reasoning here goes as follows:\n#\n#    1. It is not possible to call into the callbacks before the dictionary is\n#       populated, so once in the callback the id must be in the dictionary.\n#    2. The callbacks don't mutate the dictionary, they only read from it, and\n#       so cannot conflict with any of the insertions.\n#\n# This is good: if we had to lock in the callbacks we'd drastically slow down\n# the performance of this code.\n_connection_refs = weakref.WeakValueDictionary()\n_connection_ref_lock = threading.Lock()\n\n# Limit writes to 16kB. This is OpenSSL's limit, but we'll cargo-cult it over\n# for no better reason than we need *a* limit, and this one is right there.\nSSL_WRITE_BLOCKSIZE = 16384\n\n# This is our equivalent of util.ssl_.DEFAULT_CIPHERS, but expanded out to\n# individual cipher suites. We need to do this because this is how\n# SecureTransport wants them.\nCIPHER_SUITES = [\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,\n    SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n    SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,\n    SecurityConst.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,\n    SecurityConst.TLS_DHE_RSA_WITH_AES_256_GCM_SHA384,\n    SecurityConst.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,\n    SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,\n    SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,\n    SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,\n    SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,\n    SecurityConst.TLS_DHE_RSA_WITH_AES_256_CBC_SHA256,\n    SecurityConst.TLS_DHE_RSA_WITH_AES_256_CBC_SHA,\n    SecurityConst.TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,\n    SecurityConst.TLS_DHE_RSA_WITH_AES_128_CBC_SHA,\n    SecurityConst.TLS_AES_256_GCM_SHA384,\n    SecurityConst.TLS_AES_128_GCM_SHA256,\n    SecurityConst.TLS_RSA_WITH_AES_256_GCM_SHA384,\n    SecurityConst.TLS_RSA_WITH_AES_128_GCM_SHA256,\n    SecurityConst.TLS_AES_128_CCM_8_SHA256,\n    SecurityConst.TLS_AES_128_CCM_SHA256,\n    SecurityConst.TLS_RSA_WITH_AES_256_CBC_SHA256,\n    SecurityConst.TLS_RSA_WITH_AES_128_CBC_SHA256,\n    SecurityConst.TLS_RSA_WITH_AES_256_CBC_SHA,\n    SecurityConst.TLS_RSA_WITH_AES_128_CBC_SHA,\n]\n\n# Basically this is simple: for PROTOCOL_SSLv23 we turn it into a low of\n# TLSv1 and a high of TLSv1.2. For everything else, we pin to that version.\n# TLSv1 to 1.2 are supported on macOS 10.8+\n_protocol_to_min_max = {\n    util.PROTOCOL_TLS: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocol12),\n    PROTOCOL_TLS_CLIENT: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocol12),\n}\n\nif hasattr(ssl, \"PROTOCOL_SSLv2\"):\n    _protocol_to_min_max[ssl.PROTOCOL_SSLv2] = (\n        SecurityConst.kSSLProtocol2,\n        SecurityConst.kSSLProtocol2,\n    )\nif hasattr(ssl, \"PROTOCOL_SSLv3\"):\n    _protocol_to_min_max[ssl.PROTOCOL_SSLv3] = (\n        SecurityConst.kSSLProtocol3,\n        SecurityConst.kSSLProtocol3,\n    )\nif hasattr(ssl, \"PROTOCOL_TLSv1\"):\n    _protocol_to_min_max[ssl.PROTOCOL_TLSv1] = (\n        SecurityConst.kTLSProtocol1,\n        SecurityConst.kTLSProtocol1,\n    )\nif hasattr(ssl, \"PROTOCOL_TLSv1_1\"):\n    _protocol_to_min_max[ssl.PROTOCOL_TLSv1_1] = (\n        SecurityConst.kTLSProtocol11,\n        SecurityConst.kTLSProtocol11,\n    )\nif hasattr(ssl, \"PROTOCOL_TLSv1_2\"):\n    _protocol_to_min_max[ssl.PROTOCOL_TLSv1_2] = (\n        SecurityConst.kTLSProtocol12,\n        SecurityConst.kTLSProtocol12,\n    )\n\n\ndef inject_into_urllib3():\n    \"\"\"\n    Monkey-patch urllib3 with SecureTransport-backed SSL-support.\n    \"\"\"\n    util.SSLContext = SecureTransportContext\n    util.ssl_.SSLContext = SecureTransportContext\n    util.HAS_SNI = HAS_SNI\n    util.ssl_.HAS_SNI = HAS_SNI\n    util.IS_SECURETRANSPORT = True\n    util.ssl_.IS_SECURETRANSPORT = True\n\n\ndef extract_from_urllib3():\n    \"\"\"\n    Undo monkey-patching by :func:`inject_into_urllib3`.\n    \"\"\"\n    util.SSLContext = orig_util_SSLContext\n    util.ssl_.SSLContext = orig_util_SSLContext\n    util.HAS_SNI = orig_util_HAS_SNI\n    util.ssl_.HAS_SNI = orig_util_HAS_SNI\n    util.IS_SECURETRANSPORT = False\n    util.ssl_.IS_SECURETRANSPORT = False\n\n\ndef _read_callback(connection_id, data_buffer, data_length_pointer):\n    \"\"\"\n    SecureTransport read callback. This is called by ST to request that data\n    be returned from the socket.\n    \"\"\"\n    wrapped_socket = None\n    try:\n        wrapped_socket = _connection_refs.get(connection_id)\n        if wrapped_socket is None:\n            return SecurityConst.errSSLInternal\n        base_socket = wrapped_socket.socket\n\n        requested_length = data_length_pointer[0]\n\n        timeout = wrapped_socket.gettimeout()\n        error = None\n        read_count = 0\n\n        try:\n            while read_count < requested_length:\n                if timeout is None or timeout >= 0:\n                    if not util.wait_for_read(base_socket, timeout):\n                        raise socket.error(errno.EAGAIN, \"timed out\")\n\n                remaining = requested_length - read_count\n                buffer = (ctypes.c_char * remaining).from_address(\n                    data_buffer + read_count\n                )\n                chunk_size = base_socket.recv_into(buffer, remaining)\n                read_count += chunk_size\n                if not chunk_size:\n                    if not read_count:\n                        return SecurityConst.errSSLClosedGraceful\n                    break\n        except (socket.error) as e:\n            error = e.errno\n\n            if error is not None and error != errno.EAGAIN:\n                data_length_pointer[0] = read_count\n                if error == errno.ECONNRESET or error == errno.EPIPE:\n                    return SecurityConst.errSSLClosedAbort\n                raise\n\n        data_length_pointer[0] = read_count\n\n        if read_count != requested_length:\n            return SecurityConst.errSSLWouldBlock\n\n        return 0\n    except Exception as e:\n        if wrapped_socket is not None:\n            wrapped_socket._exception = e\n        return SecurityConst.errSSLInternal\n\n\ndef _write_callback(connection_id, data_buffer, data_length_pointer):\n    \"\"\"\n    SecureTransport write callback. This is called by ST to request that data\n    actually be sent on the network.\n    \"\"\"\n    wrapped_socket = None\n    try:\n        wrapped_socket = _connection_refs.get(connection_id)\n        if wrapped_socket is None:\n            return SecurityConst.errSSLInternal\n        base_socket = wrapped_socket.socket\n\n        bytes_to_write = data_length_pointer[0]\n        data = ctypes.string_at(data_buffer, bytes_to_write)\n\n        timeout = wrapped_socket.gettimeout()\n        error = None\n        sent = 0\n\n        try:\n            while sent < bytes_to_write:\n                if timeout is None or timeout >= 0:\n                    if not util.wait_for_write(base_socket, timeout):\n                        raise socket.error(errno.EAGAIN, \"timed out\")\n                chunk_sent = base_socket.send(data)\n                sent += chunk_sent\n\n                # This has some needless copying here, but I'm not sure there's\n                # much value in optimising this data path.\n                data = data[chunk_sent:]\n        except (socket.error) as e:\n            error = e.errno\n\n            if error is not None and error != errno.EAGAIN:\n                data_length_pointer[0] = sent\n                if error == errno.ECONNRESET or error == errno.EPIPE:\n                    return SecurityConst.errSSLClosedAbort\n                raise\n\n        data_length_pointer[0] = sent\n\n        if sent != bytes_to_write:\n            return SecurityConst.errSSLWouldBlock\n\n        return 0\n    except Exception as e:\n        if wrapped_socket is not None:\n            wrapped_socket._exception = e\n        return SecurityConst.errSSLInternal\n\n\n# We need to keep these two objects references alive: if they get GC'd while\n# in use then SecureTransport could attempt to call a function that is in freed\n# memory. That would be...uh...bad. Yeah, that's the word. Bad.\n_read_callback_pointer = Security.SSLReadFunc(_read_callback)\n_write_callback_pointer = Security.SSLWriteFunc(_write_callback)\n\n\nclass WrappedSocket(object):\n    \"\"\"\n    API-compatibility wrapper for Python's OpenSSL wrapped socket object.\n\n    Note: _makefile_refs, _drop(), and _reuse() are needed for the garbage\n    collector of PyPy.\n    \"\"\"\n\n    def __init__(self, socket):\n        self.socket = socket\n        self.context = None\n        self._makefile_refs = 0\n        self._closed = False\n        self._exception = None\n        self._keychain = None\n        self._keychain_dir = None\n        self._client_cert_chain = None\n\n        # We save off the previously-configured timeout and then set it to\n        # zero. This is done because we use select and friends to handle the\n        # timeouts, but if we leave the timeout set on the lower socket then\n        # Python will \"kindly\" call select on that socket again for us. Avoid\n        # that by forcing the timeout to zero.\n        self._timeout = self.socket.gettimeout()\n        self.socket.settimeout(0)\n\n    @contextlib.contextmanager\n    def _raise_on_error(self):\n        \"\"\"\n        A context manager that can be used to wrap calls that do I/O from\n        SecureTransport. If any of the I/O callbacks hit an exception, this\n        context manager will correctly propagate the exception after the fact.\n        This avoids silently swallowing those exceptions.\n\n        It also correctly forces the socket closed.\n        \"\"\"\n        self._exception = None\n\n        # We explicitly don't catch around this yield because in the unlikely\n        # event that an exception was hit in the block we don't want to swallow\n        # it.\n        yield\n        if self._exception is not None:\n            exception, self._exception = self._exception, None\n            self.close()\n            raise exception\n\n    def _set_ciphers(self):\n        \"\"\"\n        Sets up the allowed ciphers. By default this matches the set in\n        util.ssl_.DEFAULT_CIPHERS, at least as supported by macOS. This is done\n        custom and doesn't allow changing at this time, mostly because parsing\n        OpenSSL cipher strings is going to be a freaking nightmare.\n        \"\"\"\n        ciphers = (Security.SSLCipherSuite * len(CIPHER_SUITES))(*CIPHER_SUITES)\n        result = Security.SSLSetEnabledCiphers(\n            self.context, ciphers, len(CIPHER_SUITES)\n        )\n        _assert_no_error(result)\n\n    def _set_alpn_protocols(self, protocols):\n        \"\"\"\n        Sets up the ALPN protocols on the context.\n        \"\"\"\n        if not protocols:\n            return\n        protocols_arr = _create_cfstring_array(protocols)\n        try:\n            result = Security.SSLSetALPNProtocols(self.context, protocols_arr)\n            _assert_no_error(result)\n        finally:\n            CoreFoundation.CFRelease(protocols_arr)\n\n    def _custom_validate(self, verify, trust_bundle):\n        \"\"\"\n        Called when we have set custom validation. We do this in two cases:\n        first, when cert validation is entirely disabled; and second, when\n        using a custom trust DB.\n        Raises an SSLError if the connection is not trusted.\n        \"\"\"\n        # If we disabled cert validation, just say: cool.\n        if not verify:\n            return\n\n        successes = (\n            SecurityConst.kSecTrustResultUnspecified,\n            SecurityConst.kSecTrustResultProceed,\n        )\n        try:\n            trust_result = self._evaluate_trust(trust_bundle)\n            if trust_result in successes:\n                return\n            reason = \"error code: %d\" % (trust_result,)\n        except Exception as e:\n            # Do not trust on error\n            reason = \"exception: %r\" % (e,)\n\n        # SecureTransport does not send an alert nor shuts down the connection.\n        rec = _build_tls_unknown_ca_alert(self.version())\n        self.socket.sendall(rec)\n        # close the connection immediately\n        # l_onoff = 1, activate linger\n        # l_linger = 0, linger for 0 seoncds\n        opts = struct.pack(\"ii\", 1, 0)\n        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, opts)\n        self.close()\n        raise ssl.SSLError(\"certificate verify failed, %s\" % reason)\n\n    def _evaluate_trust(self, trust_bundle):\n        # We want data in memory, so load it up.\n        if os.path.isfile(trust_bundle):\n            with open(trust_bundle, \"rb\") as f:\n                trust_bundle = f.read()\n\n        cert_array = None\n        trust = Security.SecTrustRef()\n\n        try:\n            # Get a CFArray that contains the certs we want.\n            cert_array = _cert_array_from_pem(trust_bundle)\n\n            # Ok, now the hard part. We want to get the SecTrustRef that ST has\n            # created for this connection, shove our CAs into it, tell ST to\n            # ignore everything else it knows, and then ask if it can build a\n            # chain. This is a buuuunch of code.\n            result = Security.SSLCopyPeerTrust(self.context, ctypes.byref(trust))\n            _assert_no_error(result)\n            if not trust:\n                raise ssl.SSLError(\"Failed to copy trust reference\")\n\n            result = Security.SecTrustSetAnchorCertificates(trust, cert_array)\n            _assert_no_error(result)\n\n            result = Security.SecTrustSetAnchorCertificatesOnly(trust, True)\n            _assert_no_error(result)\n\n            trust_result = Security.SecTrustResultType()\n            result = Security.SecTrustEvaluate(trust, ctypes.byref(trust_result))\n            _assert_no_error(result)\n        finally:\n            if trust:\n                CoreFoundation.CFRelease(trust)\n\n            if cert_array is not None:\n                CoreFoundation.CFRelease(cert_array)\n\n        return trust_result.value\n\n    def handshake(\n        self,\n        server_hostname,\n        verify,\n        trust_bundle,\n        min_version,\n        max_version,\n        client_cert,\n        client_key,\n        client_key_passphrase,\n        alpn_protocols,\n    ):\n        \"\"\"\n        Actually performs the TLS handshake. This is run automatically by\n        wrapped socket, and shouldn't be needed in user code.\n        \"\"\"\n        # First, we do the initial bits of connection setup. We need to create\n        # a context, set its I/O funcs, and set the connection reference.\n        self.context = Security.SSLCreateContext(\n            None, SecurityConst.kSSLClientSide, SecurityConst.kSSLStreamType\n        )\n        result = Security.SSLSetIOFuncs(\n            self.context, _read_callback_pointer, _write_callback_pointer\n        )\n        _assert_no_error(result)\n\n        # Here we need to compute the handle to use. We do this by taking the\n        # id of self modulo 2**31 - 1. If this is already in the dictionary, we\n        # just keep incrementing by one until we find a free space.\n        with _connection_ref_lock:\n            handle = id(self) % 2147483647\n            while handle in _connection_refs:\n                handle = (handle + 1) % 2147483647\n            _connection_refs[handle] = self\n\n        result = Security.SSLSetConnection(self.context, handle)\n        _assert_no_error(result)\n\n        # If we have a server hostname, we should set that too.\n        if server_hostname:\n            if not isinstance(server_hostname, bytes):\n                server_hostname = server_hostname.encode(\"utf-8\")\n\n            result = Security.SSLSetPeerDomainName(\n                self.context, server_hostname, len(server_hostname)\n            )\n            _assert_no_error(result)\n\n        # Setup the ciphers.\n        self._set_ciphers()\n\n        # Setup the ALPN protocols.\n        self._set_alpn_protocols(alpn_protocols)\n\n        # Set the minimum and maximum TLS versions.\n        result = Security.SSLSetProtocolVersionMin(self.context, min_version)\n        _assert_no_error(result)\n\n        result = Security.SSLSetProtocolVersionMax(self.context, max_version)\n        _assert_no_error(result)\n\n        # If there's a trust DB, we need to use it. We do that by telling\n        # SecureTransport to break on server auth. We also do that if we don't\n        # want to validate the certs at all: we just won't actually do any\n        # authing in that case.\n        if not verify or trust_bundle is not None:\n            result = Security.SSLSetSessionOption(\n                self.context, SecurityConst.kSSLSessionOptionBreakOnServerAuth, True\n            )\n            _assert_no_error(result)\n\n        # If there's a client cert, we need to use it.\n        if client_cert:\n            self._keychain, self._keychain_dir = _temporary_keychain()\n            self._client_cert_chain = _load_client_cert_chain(\n                self._keychain, client_cert, client_key\n            )\n            result = Security.SSLSetCertificate(self.context, self._client_cert_chain)\n            _assert_no_error(result)\n\n        while True:\n            with self._raise_on_error():\n                result = Security.SSLHandshake(self.context)\n\n                if result == SecurityConst.errSSLWouldBlock:\n                    raise socket.timeout(\"handshake timed out\")\n                elif result == SecurityConst.errSSLServerAuthCompleted:\n                    self._custom_validate(verify, trust_bundle)\n                    continue\n                else:\n                    _assert_no_error(result)\n                    break\n\n    def fileno(self):\n        return self.socket.fileno()\n\n    # Copy-pasted from Python 3.5 source code\n    def _decref_socketios(self):\n        if self._makefile_refs > 0:\n            self._makefile_refs -= 1\n        if self._closed:\n            self.close()\n\n    def recv(self, bufsiz):\n        buffer = ctypes.create_string_buffer(bufsiz)\n        bytes_read = self.recv_into(buffer, bufsiz)\n        data = buffer[:bytes_read]\n        return data\n\n    def recv_into(self, buffer, nbytes=None):\n        # Read short on EOF.\n        if self._closed:\n            return 0\n\n        if nbytes is None:\n            nbytes = len(buffer)\n\n        buffer = (ctypes.c_char * nbytes).from_buffer(buffer)\n        processed_bytes = ctypes.c_size_t(0)\n\n        with self._raise_on_error():\n            result = Security.SSLRead(\n                self.context, buffer, nbytes, ctypes.byref(processed_bytes)\n            )\n\n        # There are some result codes that we want to treat as \"not always\n        # errors\". Specifically, those are errSSLWouldBlock,\n        # errSSLClosedGraceful, and errSSLClosedNoNotify.\n        if result == SecurityConst.errSSLWouldBlock:\n            # If we didn't process any bytes, then this was just a time out.\n            # However, we can get errSSLWouldBlock in situations when we *did*\n            # read some data, and in those cases we should just read \"short\"\n            # and return.\n            if processed_bytes.value == 0:\n                # Timed out, no data read.\n                raise socket.timeout(\"recv timed out\")\n        elif result in (\n            SecurityConst.errSSLClosedGraceful,\n            SecurityConst.errSSLClosedNoNotify,\n        ):\n            # The remote peer has closed this connection. We should do so as\n            # well. Note that we don't actually return here because in\n            # principle this could actually be fired along with return data.\n            # It's unlikely though.\n            self.close()\n        else:\n            _assert_no_error(result)\n\n        # Ok, we read and probably succeeded. We should return whatever data\n        # was actually read.\n        return processed_bytes.value\n\n    def settimeout(self, timeout):\n        self._timeout = timeout\n\n    def gettimeout(self):\n        return self._timeout\n\n    def send(self, data):\n        processed_bytes = ctypes.c_size_t(0)\n\n        with self._raise_on_error():\n            result = Security.SSLWrite(\n                self.context, data, len(data), ctypes.byref(processed_bytes)\n            )\n\n        if result == SecurityConst.errSSLWouldBlock and processed_bytes.value == 0:\n            # Timed out\n            raise socket.timeout(\"send timed out\")\n        else:\n            _assert_no_error(result)\n\n        # We sent, and probably succeeded. Tell them how much we sent.\n        return processed_bytes.value\n\n    def sendall(self, data):\n        total_sent = 0\n        while total_sent < len(data):\n            sent = self.send(data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE])\n            total_sent += sent\n\n    def shutdown(self):\n        with self._raise_on_error():\n            Security.SSLClose(self.context)\n\n    def close(self):\n        # TODO: should I do clean shutdown here? Do I have to?\n        if self._makefile_refs < 1:\n            self._closed = True\n            if self.context:\n                CoreFoundation.CFRelease(self.context)\n                self.context = None\n            if self._client_cert_chain:\n                CoreFoundation.CFRelease(self._client_cert_chain)\n                self._client_cert_chain = None\n            if self._keychain:\n                Security.SecKeychainDelete(self._keychain)\n                CoreFoundation.CFRelease(self._keychain)\n                shutil.rmtree(self._keychain_dir)\n                self._keychain = self._keychain_dir = None\n            return self.socket.close()\n        else:\n            self._makefile_refs -= 1\n\n    def getpeercert(self, binary_form=False):\n        # Urgh, annoying.\n        #\n        # Here's how we do this:\n        #\n        # 1. Call SSLCopyPeerTrust to get hold of the trust object for this\n        #    connection.\n        # 2. Call SecTrustGetCertificateAtIndex for index 0 to get the leaf.\n        # 3. To get the CN, call SecCertificateCopyCommonName and process that\n        #    string so that it's of the appropriate type.\n        # 4. To get the SAN, we need to do something a bit more complex:\n        #    a. Call SecCertificateCopyValues to get the data, requesting\n        #       kSecOIDSubjectAltName.\n        #    b. Mess about with this dictionary to try to get the SANs out.\n        #\n        # This is gross. Really gross. It's going to be a few hundred LoC extra\n        # just to repeat something that SecureTransport can *already do*. So my\n        # operating assumption at this time is that what we want to do is\n        # instead to just flag to urllib3 that it shouldn't do its own hostname\n        # validation when using SecureTransport.\n        if not binary_form:\n            raise ValueError(\"SecureTransport only supports dumping binary certs\")\n        trust = Security.SecTrustRef()\n        certdata = None\n        der_bytes = None\n\n        try:\n            # Grab the trust store.\n            result = Security.SSLCopyPeerTrust(self.context, ctypes.byref(trust))\n            _assert_no_error(result)\n            if not trust:\n                # Probably we haven't done the handshake yet. No biggie.\n                return None\n\n            cert_count = Security.SecTrustGetCertificateCount(trust)\n            if not cert_count:\n                # Also a case that might happen if we haven't handshaked.\n                # Handshook? Handshaken?\n                return None\n\n            leaf = Security.SecTrustGetCertificateAtIndex(trust, 0)\n            assert leaf\n\n            # Ok, now we want the DER bytes.\n            certdata = Security.SecCertificateCopyData(leaf)\n            assert certdata\n\n            data_length = CoreFoundation.CFDataGetLength(certdata)\n            data_buffer = CoreFoundation.CFDataGetBytePtr(certdata)\n            der_bytes = ctypes.string_at(data_buffer, data_length)\n        finally:\n            if certdata:\n                CoreFoundation.CFRelease(certdata)\n            if trust:\n                CoreFoundation.CFRelease(trust)\n\n        return der_bytes\n\n    def version(self):\n        protocol = Security.SSLProtocol()\n        result = Security.SSLGetNegotiatedProtocolVersion(\n            self.context, ctypes.byref(protocol)\n        )\n        _assert_no_error(result)\n        if protocol.value == SecurityConst.kTLSProtocol13:\n            raise ssl.SSLError(\"SecureTransport does not support TLS 1.3\")\n        elif protocol.value == SecurityConst.kTLSProtocol12:\n            return \"TLSv1.2\"\n        elif protocol.value == SecurityConst.kTLSProtocol11:\n            return \"TLSv1.1\"\n        elif protocol.value == SecurityConst.kTLSProtocol1:\n            return \"TLSv1\"\n        elif protocol.value == SecurityConst.kSSLProtocol3:\n            return \"SSLv3\"\n        elif protocol.value == SecurityConst.kSSLProtocol2:\n            return \"SSLv2\"\n        else:\n            raise ssl.SSLError(\"Unknown TLS version: %r\" % protocol)\n\n    def _reuse(self):\n        self._makefile_refs += 1\n\n    def _drop(self):\n        if self._makefile_refs < 1:\n            self.close()\n        else:\n            self._makefile_refs -= 1\n\n\nif _fileobject:  # Platform-specific: Python 2\n\n    def makefile(self, mode, bufsize=-1):\n        self._makefile_refs += 1\n        return _fileobject(self, mode, bufsize, close=True)\n\n\nelse:  # Platform-specific: Python 3\n\n    def makefile(self, mode=\"r\", buffering=None, *args, **kwargs):\n        # We disable buffering with SecureTransport because it conflicts with\n        # the buffering that ST does internally (see issue #1153 for more).\n        buffering = 0\n        return backport_makefile(self, mode, buffering, *args, **kwargs)\n\n\nWrappedSocket.makefile = makefile\n\n\nclass SecureTransportContext(object):\n    \"\"\"\n    I am a wrapper class for the SecureTransport library, to translate the\n    interface of the standard library ``SSLContext`` object to calls into\n    SecureTransport.\n    \"\"\"\n\n    def __init__(self, protocol):\n        self._min_version, self._max_version = _protocol_to_min_max[protocol]\n        self._options = 0\n        self._verify = False\n        self._trust_bundle = None\n        self._client_cert = None\n        self._client_key = None\n        self._client_key_passphrase = None\n        self._alpn_protocols = None\n\n    @property\n    def check_hostname(self):\n        \"\"\"\n        SecureTransport cannot have its hostname checking disabled. For more,\n        see the comment on getpeercert() in this file.\n        \"\"\"\n        return True\n\n    @check_hostname.setter\n    def check_hostname(self, value):\n        \"\"\"\n        SecureTransport cannot have its hostname checking disabled. For more,\n        see the comment on getpeercert() in this file.\n        \"\"\"\n        pass\n\n    @property\n    def options(self):\n        # TODO: Well, crap.\n        #\n        # So this is the bit of the code that is the most likely to cause us\n        # trouble. Essentially we need to enumerate all of the SSL options that\n        # users might want to use and try to see if we can sensibly translate\n        # them, or whether we should just ignore them.\n        return self._options\n\n    @options.setter\n    def options(self, value):\n        # TODO: Update in line with above.\n        self._options = value\n\n    @property\n    def verify_mode(self):\n        return ssl.CERT_REQUIRED if self._verify else ssl.CERT_NONE\n\n    @verify_mode.setter\n    def verify_mode(self, value):\n        self._verify = True if value == ssl.CERT_REQUIRED else False\n\n    def set_default_verify_paths(self):\n        # So, this has to do something a bit weird. Specifically, what it does\n        # is nothing.\n        #\n        # This means that, if we had previously had load_verify_locations\n        # called, this does not undo that. We need to do that because it turns\n        # out that the rest of the urllib3 code will attempt to load the\n        # default verify paths if it hasn't been told about any paths, even if\n        # the context itself was sometime earlier. We resolve that by just\n        # ignoring it.\n        pass\n\n    def load_default_certs(self):\n        return self.set_default_verify_paths()\n\n    def set_ciphers(self, ciphers):\n        # For now, we just require the default cipher string.\n        if ciphers != util.ssl_.DEFAULT_CIPHERS:\n            raise ValueError(\"SecureTransport doesn't support custom cipher strings\")\n\n    def load_verify_locations(self, cafile=None, capath=None, cadata=None):\n        # OK, we only really support cadata and cafile.\n        if capath is not None:\n            raise ValueError(\"SecureTransport does not support cert directories\")\n\n        # Raise if cafile does not exist.\n        if cafile is not None:\n            with open(cafile):\n                pass\n\n        self._trust_bundle = cafile or cadata\n\n    def load_cert_chain(self, certfile, keyfile=None, password=None):\n        self._client_cert = certfile\n        self._client_key = keyfile\n        self._client_cert_passphrase = password\n\n    def set_alpn_protocols(self, protocols):\n        \"\"\"\n        Sets the ALPN protocols that will later be set on the context.\n\n        Raises a NotImplementedError if ALPN is not supported.\n        \"\"\"\n        if not hasattr(Security, \"SSLSetALPNProtocols\"):\n            raise NotImplementedError(\n                \"SecureTransport supports ALPN only in macOS 10.12+\"\n            )\n        self._alpn_protocols = [six.ensure_binary(p) for p in protocols]\n\n    def wrap_socket(\n        self,\n        sock,\n        server_side=False,\n        do_handshake_on_connect=True,\n        suppress_ragged_eofs=True,\n        server_hostname=None,\n    ):\n        # So, what do we do here? Firstly, we assert some properties. This is a\n        # stripped down shim, so there is some functionality we don't support.\n        # See PEP 543 for the real deal.\n        assert not server_side\n        assert do_handshake_on_connect\n        assert suppress_ragged_eofs\n\n        # Ok, we're good to go. Now we want to create the wrapped socket object\n        # and store it in the appropriate place.\n        wrapped_socket = WrappedSocket(sock)\n\n        # Now we can handshake\n        wrapped_socket.handshake(\n            server_hostname,\n            self._verify,\n            self._trust_bundle,\n            self._min_version,\n            self._max_version,\n            self._client_cert,\n            self._client_key,\n            self._client_key_passphrase,\n            self._alpn_protocols,\n        )\n        return wrapped_socket\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/contrib/socks.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nThis module contains provisional support for SOCKS proxies from within\nurllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and\nSOCKS5. To enable its functionality, either install PySocks or install this\nmodule with the ``socks`` extra.\n\nThe SOCKS implementation supports the full range of urllib3 features. It also\nsupports the following SOCKS features:\n\n- SOCKS4A (``proxy_url='socks4a://...``)\n- SOCKS4 (``proxy_url='socks4://...``)\n- SOCKS5 with remote DNS (``proxy_url='socks5h://...``)\n- SOCKS5 with local DNS (``proxy_url='socks5://...``)\n- Usernames and passwords for the SOCKS proxy\n\n.. note::\n   It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in\n   your ``proxy_url`` to ensure that DNS resolution is done from the remote\n   server instead of client-side when connecting to a domain name.\n\nSOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5\nsupports IPv4, IPv6, and domain names.\n\nWhen connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url``\nwill be sent as the ``userid`` section of the SOCKS request:\n\n.. code-block:: python\n\n    proxy_url=\"socks4a://<userid>@proxy-host\"\n\nWhen connecting to a SOCKS5 proxy the ``username`` and ``password`` portion\nof the ``proxy_url`` will be sent as the username/password to authenticate\nwith the proxy:\n\n.. code-block:: python\n\n    proxy_url=\"socks5h://<username>:<password>@proxy-host\"\n\n\"\"\"\nfrom __future__ import absolute_import\n\ntry:\n    import socks\nexcept ImportError:\n    import warnings\n\n    from ..exceptions import DependencyWarning\n\n    warnings.warn(\n        (\n            \"SOCKS support in urllib3 requires the installation of optional \"\n            \"dependencies: specifically, PySocks.  For more information, see \"\n            \"https://urllib3.readthedocs.io/en/1.26.x/contrib.html#socks-proxies\"\n        ),\n        DependencyWarning,\n    )\n    raise\n\nfrom socket import error as SocketError\nfrom socket import timeout as SocketTimeout\n\nfrom ..connection import HTTPConnection, HTTPSConnection\nfrom ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool\nfrom ..exceptions import ConnectTimeoutError, NewConnectionError\nfrom ..poolmanager import PoolManager\nfrom ..util.url import parse_url\n\ntry:\n    import ssl\nexcept ImportError:\n    ssl = None\n\n\nclass SOCKSConnection(HTTPConnection):\n    \"\"\"\n    A plain-text HTTP connection that connects via a SOCKS proxy.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        self._socks_options = kwargs.pop(\"_socks_options\")\n        super(SOCKSConnection, self).__init__(*args, **kwargs)\n\n    def _new_conn(self):\n        \"\"\"\n        Establish a new connection via the SOCKS proxy.\n        \"\"\"\n        extra_kw = {}\n        if self.source_address:\n            extra_kw[\"source_address\"] = self.source_address\n\n        if self.socket_options:\n            extra_kw[\"socket_options\"] = self.socket_options\n\n        try:\n            conn = socks.create_connection(\n                (self.host, self.port),\n                proxy_type=self._socks_options[\"socks_version\"],\n                proxy_addr=self._socks_options[\"proxy_host\"],\n                proxy_port=self._socks_options[\"proxy_port\"],\n                proxy_username=self._socks_options[\"username\"],\n                proxy_password=self._socks_options[\"password\"],\n                proxy_rdns=self._socks_options[\"rdns\"],\n                timeout=self.timeout,\n                **extra_kw\n            )\n\n        except SocketTimeout:\n            raise ConnectTimeoutError(\n                self,\n                \"Connection to %s timed out. (connect timeout=%s)\"\n                % (self.host, self.timeout),\n            )\n\n        except socks.ProxyError as e:\n            # This is fragile as hell, but it seems to be the only way to raise\n            # useful errors here.\n            if e.socket_err:\n                error = e.socket_err\n                if isinstance(error, SocketTimeout):\n                    raise ConnectTimeoutError(\n                        self,\n                        \"Connection to %s timed out. (connect timeout=%s)\"\n                        % (self.host, self.timeout),\n                    )\n                else:\n                    raise NewConnectionError(\n                        self, \"Failed to establish a new connection: %s\" % error\n                    )\n            else:\n                raise NewConnectionError(\n                    self, \"Failed to establish a new connection: %s\" % e\n                )\n\n        except SocketError as e:  # Defensive: PySocks should catch all these.\n            raise NewConnectionError(\n                self, \"Failed to establish a new connection: %s\" % e\n            )\n\n        return conn\n\n\n# We don't need to duplicate the Verified/Unverified distinction from\n# urllib3/connection.py here because the HTTPSConnection will already have been\n# correctly set to either the Verified or Unverified form by that module. This\n# means the SOCKSHTTPSConnection will automatically be the correct type.\nclass SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection):\n    pass\n\n\nclass SOCKSHTTPConnectionPool(HTTPConnectionPool):\n    ConnectionCls = SOCKSConnection\n\n\nclass SOCKSHTTPSConnectionPool(HTTPSConnectionPool):\n    ConnectionCls = SOCKSHTTPSConnection\n\n\nclass SOCKSProxyManager(PoolManager):\n    \"\"\"\n    A version of the urllib3 ProxyManager that routes connections via the\n    defined SOCKS proxy.\n    \"\"\"\n\n    pool_classes_by_scheme = {\n        \"http\": SOCKSHTTPConnectionPool,\n        \"https\": SOCKSHTTPSConnectionPool,\n    }\n\n    def __init__(\n        self,\n        proxy_url,\n        username=None,\n        password=None,\n        num_pools=10,\n        headers=None,\n        **connection_pool_kw\n    ):\n        parsed = parse_url(proxy_url)\n\n        if username is None and password is None and parsed.auth is not None:\n            split = parsed.auth.split(\":\")\n            if len(split) == 2:\n                username, password = split\n        if parsed.scheme == \"socks5\":\n            socks_version = socks.PROXY_TYPE_SOCKS5\n            rdns = False\n        elif parsed.scheme == \"socks5h\":\n            socks_version = socks.PROXY_TYPE_SOCKS5\n            rdns = True\n        elif parsed.scheme == \"socks4\":\n            socks_version = socks.PROXY_TYPE_SOCKS4\n            rdns = False\n        elif parsed.scheme == \"socks4a\":\n            socks_version = socks.PROXY_TYPE_SOCKS4\n            rdns = True\n        else:\n            raise ValueError(\"Unable to determine SOCKS version from %s\" % proxy_url)\n\n        self.proxy_url = proxy_url\n\n        socks_options = {\n            \"socks_version\": socks_version,\n            \"proxy_host\": parsed.host,\n            \"proxy_port\": parsed.port,\n            \"username\": username,\n            \"password\": password,\n            \"rdns\": rdns,\n        }\n        connection_pool_kw[\"_socks_options\"] = socks_options\n\n        super(SOCKSProxyManager, self).__init__(\n            num_pools, headers, **connection_pool_kw\n        )\n\n        self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/exceptions.py",
    "content": "from __future__ import absolute_import\n\nfrom .packages.six.moves.http_client import IncompleteRead as httplib_IncompleteRead\n\n# Base Exceptions\n\n\nclass HTTPError(Exception):\n    \"\"\"Base exception used by this module.\"\"\"\n\n    pass\n\n\nclass HTTPWarning(Warning):\n    \"\"\"Base warning used by this module.\"\"\"\n\n    pass\n\n\nclass PoolError(HTTPError):\n    \"\"\"Base exception for errors caused within a pool.\"\"\"\n\n    def __init__(self, pool, message):\n        self.pool = pool\n        HTTPError.__init__(self, \"%s: %s\" % (pool, message))\n\n    def __reduce__(self):\n        # For pickling purposes.\n        return self.__class__, (None, None)\n\n\nclass RequestError(PoolError):\n    \"\"\"Base exception for PoolErrors that have associated URLs.\"\"\"\n\n    def __init__(self, pool, url, message):\n        self.url = url\n        PoolError.__init__(self, pool, message)\n\n    def __reduce__(self):\n        # For pickling purposes.\n        return self.__class__, (None, self.url, None)\n\n\nclass SSLError(HTTPError):\n    \"\"\"Raised when SSL certificate fails in an HTTPS connection.\"\"\"\n\n    pass\n\n\nclass ProxyError(HTTPError):\n    \"\"\"Raised when the connection to a proxy fails.\"\"\"\n\n    def __init__(self, message, error, *args):\n        super(ProxyError, self).__init__(message, error, *args)\n        self.original_error = error\n\n\nclass DecodeError(HTTPError):\n    \"\"\"Raised when automatic decoding based on Content-Type fails.\"\"\"\n\n    pass\n\n\nclass ProtocolError(HTTPError):\n    \"\"\"Raised when something unexpected happens mid-request/response.\"\"\"\n\n    pass\n\n\n#: Renamed to ProtocolError but aliased for backwards compatibility.\nConnectionError = ProtocolError\n\n\n# Leaf Exceptions\n\n\nclass MaxRetryError(RequestError):\n    \"\"\"Raised when the maximum number of retries is exceeded.\n\n    :param pool: The connection pool\n    :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool`\n    :param string url: The requested Url\n    :param exceptions.Exception reason: The underlying error\n\n    \"\"\"\n\n    def __init__(self, pool, url, reason=None):\n        self.reason = reason\n\n        message = \"Max retries exceeded with url: %s (Caused by %r)\" % (url, reason)\n\n        RequestError.__init__(self, pool, url, message)\n\n\nclass HostChangedError(RequestError):\n    \"\"\"Raised when an existing pool gets a request for a foreign host.\"\"\"\n\n    def __init__(self, pool, url, retries=3):\n        message = \"Tried to open a foreign host with url: %s\" % url\n        RequestError.__init__(self, pool, url, message)\n        self.retries = retries\n\n\nclass TimeoutStateError(HTTPError):\n    \"\"\"Raised when passing an invalid state to a timeout\"\"\"\n\n    pass\n\n\nclass TimeoutError(HTTPError):\n    \"\"\"Raised when a socket timeout error occurs.\n\n    Catching this error will catch both :exc:`ReadTimeoutErrors\n    <ReadTimeoutError>` and :exc:`ConnectTimeoutErrors <ConnectTimeoutError>`.\n    \"\"\"\n\n    pass\n\n\nclass ReadTimeoutError(TimeoutError, RequestError):\n    \"\"\"Raised when a socket timeout occurs while receiving data from a server\"\"\"\n\n    pass\n\n\n# This timeout error does not have a URL attached and needs to inherit from the\n# base HTTPError\nclass ConnectTimeoutError(TimeoutError):\n    \"\"\"Raised when a socket timeout occurs while connecting to a server\"\"\"\n\n    pass\n\n\nclass NewConnectionError(ConnectTimeoutError, PoolError):\n    \"\"\"Raised when we fail to establish a new connection. Usually ECONNREFUSED.\"\"\"\n\n    pass\n\n\nclass EmptyPoolError(PoolError):\n    \"\"\"Raised when a pool runs out of connections and no more are allowed.\"\"\"\n\n    pass\n\n\nclass ClosedPoolError(PoolError):\n    \"\"\"Raised when a request enters a pool after the pool has been closed.\"\"\"\n\n    pass\n\n\nclass LocationValueError(ValueError, HTTPError):\n    \"\"\"Raised when there is something wrong with a given URL input.\"\"\"\n\n    pass\n\n\nclass LocationParseError(LocationValueError):\n    \"\"\"Raised when get_host or similar fails to parse the URL input.\"\"\"\n\n    def __init__(self, location):\n        message = \"Failed to parse: %s\" % location\n        HTTPError.__init__(self, message)\n\n        self.location = location\n\n\nclass URLSchemeUnknown(LocationValueError):\n    \"\"\"Raised when a URL input has an unsupported scheme.\"\"\"\n\n    def __init__(self, scheme):\n        message = \"Not supported URL scheme %s\" % scheme\n        super(URLSchemeUnknown, self).__init__(message)\n\n        self.scheme = scheme\n\n\nclass ResponseError(HTTPError):\n    \"\"\"Used as a container for an error reason supplied in a MaxRetryError.\"\"\"\n\n    GENERIC_ERROR = \"too many error responses\"\n    SPECIFIC_ERROR = \"too many {status_code} error responses\"\n\n\nclass SecurityWarning(HTTPWarning):\n    \"\"\"Warned when performing security reducing actions\"\"\"\n\n    pass\n\n\nclass SubjectAltNameWarning(SecurityWarning):\n    \"\"\"Warned when connecting to a host with a certificate missing a SAN.\"\"\"\n\n    pass\n\n\nclass InsecureRequestWarning(SecurityWarning):\n    \"\"\"Warned when making an unverified HTTPS request.\"\"\"\n\n    pass\n\n\nclass SystemTimeWarning(SecurityWarning):\n    \"\"\"Warned when system time is suspected to be wrong\"\"\"\n\n    pass\n\n\nclass InsecurePlatformWarning(SecurityWarning):\n    \"\"\"Warned when certain TLS/SSL configuration is not available on a platform.\"\"\"\n\n    pass\n\n\nclass SNIMissingWarning(HTTPWarning):\n    \"\"\"Warned when making a HTTPS request without SNI available.\"\"\"\n\n    pass\n\n\nclass DependencyWarning(HTTPWarning):\n    \"\"\"\n    Warned when an attempt is made to import a module with missing optional\n    dependencies.\n    \"\"\"\n\n    pass\n\n\nclass ResponseNotChunked(ProtocolError, ValueError):\n    \"\"\"Response needs to be chunked in order to read it as chunks.\"\"\"\n\n    pass\n\n\nclass BodyNotHttplibCompatible(HTTPError):\n    \"\"\"\n    Body should be :class:`http.client.HTTPResponse` like\n    (have an fp attribute which returns raw chunks) for read_chunked().\n    \"\"\"\n\n    pass\n\n\nclass IncompleteRead(HTTPError, httplib_IncompleteRead):\n    \"\"\"\n    Response length doesn't match expected Content-Length\n\n    Subclass of :class:`http.client.IncompleteRead` to allow int value\n    for ``partial`` to avoid creating large objects on streamed reads.\n    \"\"\"\n\n    def __init__(self, partial, expected):\n        super(IncompleteRead, self).__init__(partial, expected)\n\n    def __repr__(self):\n        return \"IncompleteRead(%i bytes read, %i more expected)\" % (\n            self.partial,\n            self.expected,\n        )\n\n\nclass InvalidChunkLength(HTTPError, httplib_IncompleteRead):\n    \"\"\"Invalid chunk length in a chunked response.\"\"\"\n\n    def __init__(self, response, length):\n        super(InvalidChunkLength, self).__init__(\n            response.tell(), response.length_remaining\n        )\n        self.response = response\n        self.length = length\n\n    def __repr__(self):\n        return \"InvalidChunkLength(got length %r, %i bytes read)\" % (\n            self.length,\n            self.partial,\n        )\n\n\nclass InvalidHeader(HTTPError):\n    \"\"\"The header provided was somehow invalid.\"\"\"\n\n    pass\n\n\nclass ProxySchemeUnknown(AssertionError, URLSchemeUnknown):\n    \"\"\"ProxyManager does not support the supplied scheme\"\"\"\n\n    # TODO(t-8ch): Stop inheriting from AssertionError in v2.0.\n\n    def __init__(self, scheme):\n        # 'localhost' is here because our URL parser parses\n        # localhost:8080 -> scheme=localhost, remove if we fix this.\n        if scheme == \"localhost\":\n            scheme = None\n        if scheme is None:\n            message = \"Proxy URL had no scheme, should start with http:// or https://\"\n        else:\n            message = (\n                \"Proxy URL had unsupported scheme %s, should use http:// or https://\"\n                % scheme\n            )\n        super(ProxySchemeUnknown, self).__init__(message)\n\n\nclass ProxySchemeUnsupported(ValueError):\n    \"\"\"Fetching HTTPS resources through HTTPS proxies is unsupported\"\"\"\n\n    pass\n\n\nclass HeaderParsingError(HTTPError):\n    \"\"\"Raised by assert_header_parsing, but we convert it to a log.warning statement.\"\"\"\n\n    def __init__(self, defects, unparsed_data):\n        message = \"%s, unparsed data: %r\" % (defects or \"Unknown\", unparsed_data)\n        super(HeaderParsingError, self).__init__(message)\n\n\nclass UnrewindableBodyError(HTTPError):\n    \"\"\"urllib3 encountered an error when trying to rewind a body\"\"\"\n\n    pass\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/fields.py",
    "content": "from __future__ import absolute_import\n\nimport email.utils\nimport mimetypes\nimport re\n\nfrom .packages import six\n\n\ndef guess_content_type(filename, default=\"application/octet-stream\"):\n    \"\"\"\n    Guess the \"Content-Type\" of a file.\n\n    :param filename:\n        The filename to guess the \"Content-Type\" of using :mod:`mimetypes`.\n    :param default:\n        If no \"Content-Type\" can be guessed, default to `default`.\n    \"\"\"\n    if filename:\n        return mimetypes.guess_type(filename)[0] or default\n    return default\n\n\ndef format_header_param_rfc2231(name, value):\n    \"\"\"\n    Helper function to format and quote a single header parameter using the\n    strategy defined in RFC 2231.\n\n    Particularly useful for header parameters which might contain\n    non-ASCII values, like file names. This follows\n    `RFC 2388 Section 4.4 <https://tools.ietf.org/html/rfc2388#section-4.4>`_.\n\n    :param name:\n        The name of the parameter, a string expected to be ASCII only.\n    :param value:\n        The value of the parameter, provided as ``bytes`` or `str``.\n    :ret:\n        An RFC-2231-formatted unicode string.\n    \"\"\"\n    if isinstance(value, six.binary_type):\n        value = value.decode(\"utf-8\")\n\n    if not any(ch in value for ch in '\"\\\\\\r\\n'):\n        result = u'%s=\"%s\"' % (name, value)\n        try:\n            result.encode(\"ascii\")\n        except (UnicodeEncodeError, UnicodeDecodeError):\n            pass\n        else:\n            return result\n\n    if six.PY2:  # Python 2:\n        value = value.encode(\"utf-8\")\n\n    # encode_rfc2231 accepts an encoded string and returns an ascii-encoded\n    # string in Python 2 but accepts and returns unicode strings in Python 3\n    value = email.utils.encode_rfc2231(value, \"utf-8\")\n    value = \"%s*=%s\" % (name, value)\n\n    if six.PY2:  # Python 2:\n        value = value.decode(\"utf-8\")\n\n    return value\n\n\n_HTML5_REPLACEMENTS = {\n    u\"\\u0022\": u\"%22\",\n    # Replace \"\\\" with \"\\\\\".\n    u\"\\u005C\": u\"\\u005C\\u005C\",\n}\n\n# All control characters from 0x00 to 0x1F *except* 0x1B.\n_HTML5_REPLACEMENTS.update(\n    {\n        six.unichr(cc): u\"%{:02X}\".format(cc)\n        for cc in range(0x00, 0x1F + 1)\n        if cc not in (0x1B,)\n    }\n)\n\n\ndef _replace_multiple(value, needles_and_replacements):\n    def replacer(match):\n        return needles_and_replacements[match.group(0)]\n\n    pattern = re.compile(\n        r\"|\".join([re.escape(needle) for needle in needles_and_replacements.keys()])\n    )\n\n    result = pattern.sub(replacer, value)\n\n    return result\n\n\ndef format_header_param_html5(name, value):\n    \"\"\"\n    Helper function to format and quote a single header parameter using the\n    HTML5 strategy.\n\n    Particularly useful for header parameters which might contain\n    non-ASCII values, like file names. This follows the `HTML5 Working Draft\n    Section 4.10.22.7`_ and matches the behavior of curl and modern browsers.\n\n    .. _HTML5 Working Draft Section 4.10.22.7:\n        https://w3c.github.io/html/sec-forms.html#multipart-form-data\n\n    :param name:\n        The name of the parameter, a string expected to be ASCII only.\n    :param value:\n        The value of the parameter, provided as ``bytes`` or `str``.\n    :ret:\n        A unicode string, stripped of troublesome characters.\n    \"\"\"\n    if isinstance(value, six.binary_type):\n        value = value.decode(\"utf-8\")\n\n    value = _replace_multiple(value, _HTML5_REPLACEMENTS)\n\n    return u'%s=\"%s\"' % (name, value)\n\n\n# For backwards-compatibility.\nformat_header_param = format_header_param_html5\n\n\nclass RequestField(object):\n    \"\"\"\n    A data container for request body parameters.\n\n    :param name:\n        The name of this request field. Must be unicode.\n    :param data:\n        The data/value body.\n    :param filename:\n        An optional filename of the request field. Must be unicode.\n    :param headers:\n        An optional dict-like object of headers to initially use for the field.\n    :param header_formatter:\n        An optional callable that is used to encode and format the headers. By\n        default, this is :func:`format_header_param_html5`.\n    \"\"\"\n\n    def __init__(\n        self,\n        name,\n        data,\n        filename=None,\n        headers=None,\n        header_formatter=format_header_param_html5,\n    ):\n        self._name = name\n        self._filename = filename\n        self.data = data\n        self.headers = {}\n        if headers:\n            self.headers = dict(headers)\n        self.header_formatter = header_formatter\n\n    @classmethod\n    def from_tuples(cls, fieldname, value, header_formatter=format_header_param_html5):\n        \"\"\"\n        A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters.\n\n        Supports constructing :class:`~urllib3.fields.RequestField` from\n        parameter of key/value strings AND key/filetuple. A filetuple is a\n        (filename, data, MIME type) tuple where the MIME type is optional.\n        For example::\n\n            'foo': 'bar',\n            'fakefile': ('foofile.txt', 'contents of foofile'),\n            'realfile': ('barfile.txt', open('realfile').read()),\n            'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'),\n            'nonamefile': 'contents of nonamefile field',\n\n        Field names and filenames must be unicode.\n        \"\"\"\n        if isinstance(value, tuple):\n            if len(value) == 3:\n                filename, data, content_type = value\n            else:\n                filename, data = value\n                content_type = guess_content_type(filename)\n        else:\n            filename = None\n            content_type = None\n            data = value\n\n        request_param = cls(\n            fieldname, data, filename=filename, header_formatter=header_formatter\n        )\n        request_param.make_multipart(content_type=content_type)\n\n        return request_param\n\n    def _render_part(self, name, value):\n        \"\"\"\n        Overridable helper function to format a single header parameter. By\n        default, this calls ``self.header_formatter``.\n\n        :param name:\n            The name of the parameter, a string expected to be ASCII only.\n        :param value:\n            The value of the parameter, provided as a unicode string.\n        \"\"\"\n\n        return self.header_formatter(name, value)\n\n    def _render_parts(self, header_parts):\n        \"\"\"\n        Helper function to format and quote a single header.\n\n        Useful for single headers that are composed of multiple items. E.g.,\n        'Content-Disposition' fields.\n\n        :param header_parts:\n            A sequence of (k, v) tuples or a :class:`dict` of (k, v) to format\n            as `k1=\"v1\"; k2=\"v2\"; ...`.\n        \"\"\"\n        parts = []\n        iterable = header_parts\n        if isinstance(header_parts, dict):\n            iterable = header_parts.items()\n\n        for name, value in iterable:\n            if value is not None:\n                parts.append(self._render_part(name, value))\n\n        return u\"; \".join(parts)\n\n    def render_headers(self):\n        \"\"\"\n        Renders the headers for this request field.\n        \"\"\"\n        lines = []\n\n        sort_keys = [\"Content-Disposition\", \"Content-Type\", \"Content-Location\"]\n        for sort_key in sort_keys:\n            if self.headers.get(sort_key, False):\n                lines.append(u\"%s: %s\" % (sort_key, self.headers[sort_key]))\n\n        for header_name, header_value in self.headers.items():\n            if header_name not in sort_keys:\n                if header_value:\n                    lines.append(u\"%s: %s\" % (header_name, header_value))\n\n        lines.append(u\"\\r\\n\")\n        return u\"\\r\\n\".join(lines)\n\n    def make_multipart(\n        self, content_disposition=None, content_type=None, content_location=None\n    ):\n        \"\"\"\n        Makes this request field into a multipart request field.\n\n        This method overrides \"Content-Disposition\", \"Content-Type\" and\n        \"Content-Location\" headers to the request parameter.\n\n        :param content_type:\n            The 'Content-Type' of the request body.\n        :param content_location:\n            The 'Content-Location' of the request body.\n\n        \"\"\"\n        self.headers[\"Content-Disposition\"] = content_disposition or u\"form-data\"\n        self.headers[\"Content-Disposition\"] += u\"; \".join(\n            [\n                u\"\",\n                self._render_parts(\n                    ((u\"name\", self._name), (u\"filename\", self._filename))\n                ),\n            ]\n        )\n        self.headers[\"Content-Type\"] = content_type\n        self.headers[\"Content-Location\"] = content_location\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/filepost.py",
    "content": "from __future__ import absolute_import\n\nimport binascii\nimport codecs\nimport os\nfrom io import BytesIO\n\nfrom .fields import RequestField\nfrom .packages import six\nfrom .packages.six import b\n\nwriter = codecs.lookup(\"utf-8\")[3]\n\n\ndef choose_boundary():\n    \"\"\"\n    Our embarrassingly-simple replacement for mimetools.choose_boundary.\n    \"\"\"\n    boundary = binascii.hexlify(os.urandom(16))\n    if not six.PY2:\n        boundary = boundary.decode(\"ascii\")\n    return boundary\n\n\ndef iter_field_objects(fields):\n    \"\"\"\n    Iterate over fields.\n\n    Supports list of (k, v) tuples and dicts, and lists of\n    :class:`~urllib3.fields.RequestField`.\n\n    \"\"\"\n    if isinstance(fields, dict):\n        i = six.iteritems(fields)\n    else:\n        i = iter(fields)\n\n    for field in i:\n        if isinstance(field, RequestField):\n            yield field\n        else:\n            yield RequestField.from_tuples(*field)\n\n\ndef iter_fields(fields):\n    \"\"\"\n    .. deprecated:: 1.6\n\n    Iterate over fields.\n\n    The addition of :class:`~urllib3.fields.RequestField` makes this function\n    obsolete. Instead, use :func:`iter_field_objects`, which returns\n    :class:`~urllib3.fields.RequestField` objects.\n\n    Supports list of (k, v) tuples and dicts.\n    \"\"\"\n    if isinstance(fields, dict):\n        return ((k, v) for k, v in six.iteritems(fields))\n\n    return ((k, v) for k, v in fields)\n\n\ndef encode_multipart_formdata(fields, boundary=None):\n    \"\"\"\n    Encode a dictionary of ``fields`` using the multipart/form-data MIME format.\n\n    :param fields:\n        Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`).\n\n    :param boundary:\n        If not specified, then a random boundary will be generated using\n        :func:`urllib3.filepost.choose_boundary`.\n    \"\"\"\n    body = BytesIO()\n    if boundary is None:\n        boundary = choose_boundary()\n\n    for field in iter_field_objects(fields):\n        body.write(b(\"--%s\\r\\n\" % (boundary)))\n\n        writer(body).write(field.render_headers())\n        data = field.data\n\n        if isinstance(data, int):\n            data = str(data)  # Backwards compatibility\n\n        if isinstance(data, six.text_type):\n            writer(body).write(data)\n        else:\n            body.write(data)\n\n        body.write(b\"\\r\\n\")\n\n    body.write(b(\"--%s--\\r\\n\" % (boundary)))\n\n    content_type = str(\"multipart/form-data; boundary=%s\" % boundary)\n\n    return body.getvalue(), content_type\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/packages/__init__.py",
    "content": "from __future__ import absolute_import\n\nfrom . import ssl_match_hostname\n\n__all__ = (\"ssl_match_hostname\",)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/packages/backports/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/packages/backports/makefile.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nbackports.makefile\n~~~~~~~~~~~~~~~~~~\n\nBackports the Python 3 ``socket.makefile`` method for use with anything that\nwants to create a \"fake\" socket object.\n\"\"\"\nimport io\nfrom socket import SocketIO\n\n\ndef backport_makefile(\n    self, mode=\"r\", buffering=None, encoding=None, errors=None, newline=None\n):\n    \"\"\"\n    Backport of ``socket.makefile`` from Python 3.5.\n    \"\"\"\n    if not set(mode) <= {\"r\", \"w\", \"b\"}:\n        raise ValueError(\"invalid mode %r (only r, w, b allowed)\" % (mode,))\n    writing = \"w\" in mode\n    reading = \"r\" in mode or not writing\n    assert reading or writing\n    binary = \"b\" in mode\n    rawmode = \"\"\n    if reading:\n        rawmode += \"r\"\n    if writing:\n        rawmode += \"w\"\n    raw = SocketIO(self, rawmode)\n    self._makefile_refs += 1\n    if buffering is None:\n        buffering = -1\n    if buffering < 0:\n        buffering = io.DEFAULT_BUFFER_SIZE\n    if buffering == 0:\n        if not binary:\n            raise ValueError(\"unbuffered streams must be binary\")\n        return raw\n    if reading and writing:\n        buffer = io.BufferedRWPair(raw, raw, buffering)\n    elif reading:\n        buffer = io.BufferedReader(raw, buffering)\n    else:\n        assert writing\n        buffer = io.BufferedWriter(raw, buffering)\n    if binary:\n        return buffer\n    text = io.TextIOWrapper(buffer, encoding, errors, newline)\n    text.mode = mode\n    return text\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/packages/six.py",
    "content": "# Copyright (c) 2010-2020 Benjamin Peterson\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\"\"\"Utilities for writing code that runs on Python 2 and 3\"\"\"\n\nfrom __future__ import absolute_import\n\nimport functools\nimport itertools\nimport operator\nimport sys\nimport types\n\n__author__ = \"Benjamin Peterson <benjamin@python.org>\"\n__version__ = \"1.16.0\"\n\n\n# Useful for very coarse version differentiation.\nPY2 = sys.version_info[0] == 2\nPY3 = sys.version_info[0] == 3\nPY34 = sys.version_info[0:2] >= (3, 4)\n\nif PY3:\n    string_types = (str,)\n    integer_types = (int,)\n    class_types = (type,)\n    text_type = str\n    binary_type = bytes\n\n    MAXSIZE = sys.maxsize\nelse:\n    string_types = (basestring,)\n    integer_types = (int, long)\n    class_types = (type, types.ClassType)\n    text_type = unicode\n    binary_type = str\n\n    if sys.platform.startswith(\"java\"):\n        # Jython always uses 32 bits.\n        MAXSIZE = int((1 << 31) - 1)\n    else:\n        # It's possible to have sizeof(long) != sizeof(Py_ssize_t).\n        class X(object):\n            def __len__(self):\n                return 1 << 31\n\n        try:\n            len(X())\n        except OverflowError:\n            # 32-bit\n            MAXSIZE = int((1 << 31) - 1)\n        else:\n            # 64-bit\n            MAXSIZE = int((1 << 63) - 1)\n        del X\n\nif PY34:\n    from importlib.util import spec_from_loader\nelse:\n    spec_from_loader = None\n\n\ndef _add_doc(func, doc):\n    \"\"\"Add documentation to a function.\"\"\"\n    func.__doc__ = doc\n\n\ndef _import_module(name):\n    \"\"\"Import module, returning the module after the last dot.\"\"\"\n    __import__(name)\n    return sys.modules[name]\n\n\nclass _LazyDescr(object):\n    def __init__(self, name):\n        self.name = name\n\n    def __get__(self, obj, tp):\n        result = self._resolve()\n        setattr(obj, self.name, result)  # Invokes __set__.\n        try:\n            # This is a bit ugly, but it avoids running this again by\n            # removing this descriptor.\n            delattr(obj.__class__, self.name)\n        except AttributeError:\n            pass\n        return result\n\n\nclass MovedModule(_LazyDescr):\n    def __init__(self, name, old, new=None):\n        super(MovedModule, self).__init__(name)\n        if PY3:\n            if new is None:\n                new = name\n            self.mod = new\n        else:\n            self.mod = old\n\n    def _resolve(self):\n        return _import_module(self.mod)\n\n    def __getattr__(self, attr):\n        _module = self._resolve()\n        value = getattr(_module, attr)\n        setattr(self, attr, value)\n        return value\n\n\nclass _LazyModule(types.ModuleType):\n    def __init__(self, name):\n        super(_LazyModule, self).__init__(name)\n        self.__doc__ = self.__class__.__doc__\n\n    def __dir__(self):\n        attrs = [\"__doc__\", \"__name__\"]\n        attrs += [attr.name for attr in self._moved_attributes]\n        return attrs\n\n    # Subclasses should override this\n    _moved_attributes = []\n\n\nclass MovedAttribute(_LazyDescr):\n    def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):\n        super(MovedAttribute, self).__init__(name)\n        if PY3:\n            if new_mod is None:\n                new_mod = name\n            self.mod = new_mod\n            if new_attr is None:\n                if old_attr is None:\n                    new_attr = name\n                else:\n                    new_attr = old_attr\n            self.attr = new_attr\n        else:\n            self.mod = old_mod\n            if old_attr is None:\n                old_attr = name\n            self.attr = old_attr\n\n    def _resolve(self):\n        module = _import_module(self.mod)\n        return getattr(module, self.attr)\n\n\nclass _SixMetaPathImporter(object):\n\n    \"\"\"\n    A meta path importer to import six.moves and its submodules.\n\n    This class implements a PEP302 finder and loader. It should be compatible\n    with Python 2.5 and all existing versions of Python3\n    \"\"\"\n\n    def __init__(self, six_module_name):\n        self.name = six_module_name\n        self.known_modules = {}\n\n    def _add_module(self, mod, *fullnames):\n        for fullname in fullnames:\n            self.known_modules[self.name + \".\" + fullname] = mod\n\n    def _get_module(self, fullname):\n        return self.known_modules[self.name + \".\" + fullname]\n\n    def find_module(self, fullname, path=None):\n        if fullname in self.known_modules:\n            return self\n        return None\n\n    def find_spec(self, fullname, path, target=None):\n        if fullname in self.known_modules:\n            return spec_from_loader(fullname, self)\n        return None\n\n    def __get_module(self, fullname):\n        try:\n            return self.known_modules[fullname]\n        except KeyError:\n            raise ImportError(\"This loader does not know module \" + fullname)\n\n    def load_module(self, fullname):\n        try:\n            # in case of a reload\n            return sys.modules[fullname]\n        except KeyError:\n            pass\n        mod = self.__get_module(fullname)\n        if isinstance(mod, MovedModule):\n            mod = mod._resolve()\n        else:\n            mod.__loader__ = self\n        sys.modules[fullname] = mod\n        return mod\n\n    def is_package(self, fullname):\n        \"\"\"\n        Return true, if the named module is a package.\n\n        We need this method to get correct spec objects with\n        Python 3.4 (see PEP451)\n        \"\"\"\n        return hasattr(self.__get_module(fullname), \"__path__\")\n\n    def get_code(self, fullname):\n        \"\"\"Return None\n\n        Required, if is_package is implemented\"\"\"\n        self.__get_module(fullname)  # eventually raises ImportError\n        return None\n\n    get_source = get_code  # same as get_code\n\n    def create_module(self, spec):\n        return self.load_module(spec.name)\n\n    def exec_module(self, module):\n        pass\n\n\n_importer = _SixMetaPathImporter(__name__)\n\n\nclass _MovedItems(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects\"\"\"\n\n    __path__ = []  # mark as package\n\n\n_moved_attributes = [\n    MovedAttribute(\"cStringIO\", \"cStringIO\", \"io\", \"StringIO\"),\n    MovedAttribute(\"filter\", \"itertools\", \"builtins\", \"ifilter\", \"filter\"),\n    MovedAttribute(\n        \"filterfalse\", \"itertools\", \"itertools\", \"ifilterfalse\", \"filterfalse\"\n    ),\n    MovedAttribute(\"input\", \"__builtin__\", \"builtins\", \"raw_input\", \"input\"),\n    MovedAttribute(\"intern\", \"__builtin__\", \"sys\"),\n    MovedAttribute(\"map\", \"itertools\", \"builtins\", \"imap\", \"map\"),\n    MovedAttribute(\"getcwd\", \"os\", \"os\", \"getcwdu\", \"getcwd\"),\n    MovedAttribute(\"getcwdb\", \"os\", \"os\", \"getcwd\", \"getcwdb\"),\n    MovedAttribute(\"getoutput\", \"commands\", \"subprocess\"),\n    MovedAttribute(\"range\", \"__builtin__\", \"builtins\", \"xrange\", \"range\"),\n    MovedAttribute(\n        \"reload_module\", \"__builtin__\", \"importlib\" if PY34 else \"imp\", \"reload\"\n    ),\n    MovedAttribute(\"reduce\", \"__builtin__\", \"functools\"),\n    MovedAttribute(\"shlex_quote\", \"pipes\", \"shlex\", \"quote\"),\n    MovedAttribute(\"StringIO\", \"StringIO\", \"io\"),\n    MovedAttribute(\"UserDict\", \"UserDict\", \"collections\"),\n    MovedAttribute(\"UserList\", \"UserList\", \"collections\"),\n    MovedAttribute(\"UserString\", \"UserString\", \"collections\"),\n    MovedAttribute(\"xrange\", \"__builtin__\", \"builtins\", \"xrange\", \"range\"),\n    MovedAttribute(\"zip\", \"itertools\", \"builtins\", \"izip\", \"zip\"),\n    MovedAttribute(\n        \"zip_longest\", \"itertools\", \"itertools\", \"izip_longest\", \"zip_longest\"\n    ),\n    MovedModule(\"builtins\", \"__builtin__\"),\n    MovedModule(\"configparser\", \"ConfigParser\"),\n    MovedModule(\n        \"collections_abc\",\n        \"collections\",\n        \"collections.abc\" if sys.version_info >= (3, 3) else \"collections\",\n    ),\n    MovedModule(\"copyreg\", \"copy_reg\"),\n    MovedModule(\"dbm_gnu\", \"gdbm\", \"dbm.gnu\"),\n    MovedModule(\"dbm_ndbm\", \"dbm\", \"dbm.ndbm\"),\n    MovedModule(\n        \"_dummy_thread\",\n        \"dummy_thread\",\n        \"_dummy_thread\" if sys.version_info < (3, 9) else \"_thread\",\n    ),\n    MovedModule(\"http_cookiejar\", \"cookielib\", \"http.cookiejar\"),\n    MovedModule(\"http_cookies\", \"Cookie\", \"http.cookies\"),\n    MovedModule(\"html_entities\", \"htmlentitydefs\", \"html.entities\"),\n    MovedModule(\"html_parser\", \"HTMLParser\", \"html.parser\"),\n    MovedModule(\"http_client\", \"httplib\", \"http.client\"),\n    MovedModule(\"email_mime_base\", \"email.MIMEBase\", \"email.mime.base\"),\n    MovedModule(\"email_mime_image\", \"email.MIMEImage\", \"email.mime.image\"),\n    MovedModule(\"email_mime_multipart\", \"email.MIMEMultipart\", \"email.mime.multipart\"),\n    MovedModule(\n        \"email_mime_nonmultipart\", \"email.MIMENonMultipart\", \"email.mime.nonmultipart\"\n    ),\n    MovedModule(\"email_mime_text\", \"email.MIMEText\", \"email.mime.text\"),\n    MovedModule(\"BaseHTTPServer\", \"BaseHTTPServer\", \"http.server\"),\n    MovedModule(\"CGIHTTPServer\", \"CGIHTTPServer\", \"http.server\"),\n    MovedModule(\"SimpleHTTPServer\", \"SimpleHTTPServer\", \"http.server\"),\n    MovedModule(\"cPickle\", \"cPickle\", \"pickle\"),\n    MovedModule(\"queue\", \"Queue\"),\n    MovedModule(\"reprlib\", \"repr\"),\n    MovedModule(\"socketserver\", \"SocketServer\"),\n    MovedModule(\"_thread\", \"thread\", \"_thread\"),\n    MovedModule(\"tkinter\", \"Tkinter\"),\n    MovedModule(\"tkinter_dialog\", \"Dialog\", \"tkinter.dialog\"),\n    MovedModule(\"tkinter_filedialog\", \"FileDialog\", \"tkinter.filedialog\"),\n    MovedModule(\"tkinter_scrolledtext\", \"ScrolledText\", \"tkinter.scrolledtext\"),\n    MovedModule(\"tkinter_simpledialog\", \"SimpleDialog\", \"tkinter.simpledialog\"),\n    MovedModule(\"tkinter_tix\", \"Tix\", \"tkinter.tix\"),\n    MovedModule(\"tkinter_ttk\", \"ttk\", \"tkinter.ttk\"),\n    MovedModule(\"tkinter_constants\", \"Tkconstants\", \"tkinter.constants\"),\n    MovedModule(\"tkinter_dnd\", \"Tkdnd\", \"tkinter.dnd\"),\n    MovedModule(\"tkinter_colorchooser\", \"tkColorChooser\", \"tkinter.colorchooser\"),\n    MovedModule(\"tkinter_commondialog\", \"tkCommonDialog\", \"tkinter.commondialog\"),\n    MovedModule(\"tkinter_tkfiledialog\", \"tkFileDialog\", \"tkinter.filedialog\"),\n    MovedModule(\"tkinter_font\", \"tkFont\", \"tkinter.font\"),\n    MovedModule(\"tkinter_messagebox\", \"tkMessageBox\", \"tkinter.messagebox\"),\n    MovedModule(\"tkinter_tksimpledialog\", \"tkSimpleDialog\", \"tkinter.simpledialog\"),\n    MovedModule(\"urllib_parse\", __name__ + \".moves.urllib_parse\", \"urllib.parse\"),\n    MovedModule(\"urllib_error\", __name__ + \".moves.urllib_error\", \"urllib.error\"),\n    MovedModule(\"urllib\", __name__ + \".moves.urllib\", __name__ + \".moves.urllib\"),\n    MovedModule(\"urllib_robotparser\", \"robotparser\", \"urllib.robotparser\"),\n    MovedModule(\"xmlrpc_client\", \"xmlrpclib\", \"xmlrpc.client\"),\n    MovedModule(\"xmlrpc_server\", \"SimpleXMLRPCServer\", \"xmlrpc.server\"),\n]\n# Add windows specific modules.\nif sys.platform == \"win32\":\n    _moved_attributes += [\n        MovedModule(\"winreg\", \"_winreg\"),\n    ]\n\nfor attr in _moved_attributes:\n    setattr(_MovedItems, attr.name, attr)\n    if isinstance(attr, MovedModule):\n        _importer._add_module(attr, \"moves.\" + attr.name)\ndel attr\n\n_MovedItems._moved_attributes = _moved_attributes\n\nmoves = _MovedItems(__name__ + \".moves\")\n_importer._add_module(moves, \"moves\")\n\n\nclass Module_six_moves_urllib_parse(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_parse\"\"\"\n\n\n_urllib_parse_moved_attributes = [\n    MovedAttribute(\"ParseResult\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"SplitResult\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"parse_qs\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"parse_qsl\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urldefrag\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urljoin\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlparse\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlsplit\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlunparse\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlunsplit\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"quote\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"quote_plus\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"unquote\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"unquote_plus\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\n        \"unquote_to_bytes\", \"urllib\", \"urllib.parse\", \"unquote\", \"unquote_to_bytes\"\n    ),\n    MovedAttribute(\"urlencode\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splitquery\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splittag\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splituser\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splitvalue\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"uses_fragment\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_netloc\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_params\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_query\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_relative\", \"urlparse\", \"urllib.parse\"),\n]\nfor attr in _urllib_parse_moved_attributes:\n    setattr(Module_six_moves_urllib_parse, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes\n\n_importer._add_module(\n    Module_six_moves_urllib_parse(__name__ + \".moves.urllib_parse\"),\n    \"moves.urllib_parse\",\n    \"moves.urllib.parse\",\n)\n\n\nclass Module_six_moves_urllib_error(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_error\"\"\"\n\n\n_urllib_error_moved_attributes = [\n    MovedAttribute(\"URLError\", \"urllib2\", \"urllib.error\"),\n    MovedAttribute(\"HTTPError\", \"urllib2\", \"urllib.error\"),\n    MovedAttribute(\"ContentTooShortError\", \"urllib\", \"urllib.error\"),\n]\nfor attr in _urllib_error_moved_attributes:\n    setattr(Module_six_moves_urllib_error, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes\n\n_importer._add_module(\n    Module_six_moves_urllib_error(__name__ + \".moves.urllib.error\"),\n    \"moves.urllib_error\",\n    \"moves.urllib.error\",\n)\n\n\nclass Module_six_moves_urllib_request(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_request\"\"\"\n\n\n_urllib_request_moved_attributes = [\n    MovedAttribute(\"urlopen\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"install_opener\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"build_opener\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"pathname2url\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"url2pathname\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"getproxies\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"Request\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"OpenerDirector\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPDefaultErrorHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPRedirectHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPCookieProcessor\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"ProxyHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"BaseHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPPasswordMgr\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPPasswordMgrWithDefaultRealm\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"AbstractBasicAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPBasicAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"ProxyBasicAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"AbstractDigestAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPDigestAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"ProxyDigestAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPSHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"FileHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"FTPHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"CacheFTPHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"UnknownHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPErrorProcessor\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"urlretrieve\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"urlcleanup\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"URLopener\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"FancyURLopener\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"proxy_bypass\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"parse_http_list\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"parse_keqv_list\", \"urllib2\", \"urllib.request\"),\n]\nfor attr in _urllib_request_moved_attributes:\n    setattr(Module_six_moves_urllib_request, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes\n\n_importer._add_module(\n    Module_six_moves_urllib_request(__name__ + \".moves.urllib.request\"),\n    \"moves.urllib_request\",\n    \"moves.urllib.request\",\n)\n\n\nclass Module_six_moves_urllib_response(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_response\"\"\"\n\n\n_urllib_response_moved_attributes = [\n    MovedAttribute(\"addbase\", \"urllib\", \"urllib.response\"),\n    MovedAttribute(\"addclosehook\", \"urllib\", \"urllib.response\"),\n    MovedAttribute(\"addinfo\", \"urllib\", \"urllib.response\"),\n    MovedAttribute(\"addinfourl\", \"urllib\", \"urllib.response\"),\n]\nfor attr in _urllib_response_moved_attributes:\n    setattr(Module_six_moves_urllib_response, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes\n\n_importer._add_module(\n    Module_six_moves_urllib_response(__name__ + \".moves.urllib.response\"),\n    \"moves.urllib_response\",\n    \"moves.urllib.response\",\n)\n\n\nclass Module_six_moves_urllib_robotparser(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_robotparser\"\"\"\n\n\n_urllib_robotparser_moved_attributes = [\n    MovedAttribute(\"RobotFileParser\", \"robotparser\", \"urllib.robotparser\"),\n]\nfor attr in _urllib_robotparser_moved_attributes:\n    setattr(Module_six_moves_urllib_robotparser, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_robotparser._moved_attributes = (\n    _urllib_robotparser_moved_attributes\n)\n\n_importer._add_module(\n    Module_six_moves_urllib_robotparser(__name__ + \".moves.urllib.robotparser\"),\n    \"moves.urllib_robotparser\",\n    \"moves.urllib.robotparser\",\n)\n\n\nclass Module_six_moves_urllib(types.ModuleType):\n\n    \"\"\"Create a six.moves.urllib namespace that resembles the Python 3 namespace\"\"\"\n\n    __path__ = []  # mark as package\n    parse = _importer._get_module(\"moves.urllib_parse\")\n    error = _importer._get_module(\"moves.urllib_error\")\n    request = _importer._get_module(\"moves.urllib_request\")\n    response = _importer._get_module(\"moves.urllib_response\")\n    robotparser = _importer._get_module(\"moves.urllib_robotparser\")\n\n    def __dir__(self):\n        return [\"parse\", \"error\", \"request\", \"response\", \"robotparser\"]\n\n\n_importer._add_module(\n    Module_six_moves_urllib(__name__ + \".moves.urllib\"), \"moves.urllib\"\n)\n\n\ndef add_move(move):\n    \"\"\"Add an item to six.moves.\"\"\"\n    setattr(_MovedItems, move.name, move)\n\n\ndef remove_move(name):\n    \"\"\"Remove item from six.moves.\"\"\"\n    try:\n        delattr(_MovedItems, name)\n    except AttributeError:\n        try:\n            del moves.__dict__[name]\n        except KeyError:\n            raise AttributeError(\"no such move, %r\" % (name,))\n\n\nif PY3:\n    _meth_func = \"__func__\"\n    _meth_self = \"__self__\"\n\n    _func_closure = \"__closure__\"\n    _func_code = \"__code__\"\n    _func_defaults = \"__defaults__\"\n    _func_globals = \"__globals__\"\nelse:\n    _meth_func = \"im_func\"\n    _meth_self = \"im_self\"\n\n    _func_closure = \"func_closure\"\n    _func_code = \"func_code\"\n    _func_defaults = \"func_defaults\"\n    _func_globals = \"func_globals\"\n\n\ntry:\n    advance_iterator = next\nexcept NameError:\n\n    def advance_iterator(it):\n        return it.next()\n\n\nnext = advance_iterator\n\n\ntry:\n    callable = callable\nexcept NameError:\n\n    def callable(obj):\n        return any(\"__call__\" in klass.__dict__ for klass in type(obj).__mro__)\n\n\nif PY3:\n\n    def get_unbound_function(unbound):\n        return unbound\n\n    create_bound_method = types.MethodType\n\n    def create_unbound_method(func, cls):\n        return func\n\n    Iterator = object\nelse:\n\n    def get_unbound_function(unbound):\n        return unbound.im_func\n\n    def create_bound_method(func, obj):\n        return types.MethodType(func, obj, obj.__class__)\n\n    def create_unbound_method(func, cls):\n        return types.MethodType(func, None, cls)\n\n    class Iterator(object):\n        def next(self):\n            return type(self).__next__(self)\n\n    callable = callable\n_add_doc(\n    get_unbound_function, \"\"\"Get the function out of a possibly unbound function\"\"\"\n)\n\n\nget_method_function = operator.attrgetter(_meth_func)\nget_method_self = operator.attrgetter(_meth_self)\nget_function_closure = operator.attrgetter(_func_closure)\nget_function_code = operator.attrgetter(_func_code)\nget_function_defaults = operator.attrgetter(_func_defaults)\nget_function_globals = operator.attrgetter(_func_globals)\n\n\nif PY3:\n\n    def iterkeys(d, **kw):\n        return iter(d.keys(**kw))\n\n    def itervalues(d, **kw):\n        return iter(d.values(**kw))\n\n    def iteritems(d, **kw):\n        return iter(d.items(**kw))\n\n    def iterlists(d, **kw):\n        return iter(d.lists(**kw))\n\n    viewkeys = operator.methodcaller(\"keys\")\n\n    viewvalues = operator.methodcaller(\"values\")\n\n    viewitems = operator.methodcaller(\"items\")\nelse:\n\n    def iterkeys(d, **kw):\n        return d.iterkeys(**kw)\n\n    def itervalues(d, **kw):\n        return d.itervalues(**kw)\n\n    def iteritems(d, **kw):\n        return d.iteritems(**kw)\n\n    def iterlists(d, **kw):\n        return d.iterlists(**kw)\n\n    viewkeys = operator.methodcaller(\"viewkeys\")\n\n    viewvalues = operator.methodcaller(\"viewvalues\")\n\n    viewitems = operator.methodcaller(\"viewitems\")\n\n_add_doc(iterkeys, \"Return an iterator over the keys of a dictionary.\")\n_add_doc(itervalues, \"Return an iterator over the values of a dictionary.\")\n_add_doc(iteritems, \"Return an iterator over the (key, value) pairs of a dictionary.\")\n_add_doc(\n    iterlists, \"Return an iterator over the (key, [values]) pairs of a dictionary.\"\n)\n\n\nif PY3:\n\n    def b(s):\n        return s.encode(\"latin-1\")\n\n    def u(s):\n        return s\n\n    unichr = chr\n    import struct\n\n    int2byte = struct.Struct(\">B\").pack\n    del struct\n    byte2int = operator.itemgetter(0)\n    indexbytes = operator.getitem\n    iterbytes = iter\n    import io\n\n    StringIO = io.StringIO\n    BytesIO = io.BytesIO\n    del io\n    _assertCountEqual = \"assertCountEqual\"\n    if sys.version_info[1] <= 1:\n        _assertRaisesRegex = \"assertRaisesRegexp\"\n        _assertRegex = \"assertRegexpMatches\"\n        _assertNotRegex = \"assertNotRegexpMatches\"\n    else:\n        _assertRaisesRegex = \"assertRaisesRegex\"\n        _assertRegex = \"assertRegex\"\n        _assertNotRegex = \"assertNotRegex\"\nelse:\n\n    def b(s):\n        return s\n\n    # Workaround for standalone backslash\n\n    def u(s):\n        return unicode(s.replace(r\"\\\\\", r\"\\\\\\\\\"), \"unicode_escape\")\n\n    unichr = unichr\n    int2byte = chr\n\n    def byte2int(bs):\n        return ord(bs[0])\n\n    def indexbytes(buf, i):\n        return ord(buf[i])\n\n    iterbytes = functools.partial(itertools.imap, ord)\n    import StringIO\n\n    StringIO = BytesIO = StringIO.StringIO\n    _assertCountEqual = \"assertItemsEqual\"\n    _assertRaisesRegex = \"assertRaisesRegexp\"\n    _assertRegex = \"assertRegexpMatches\"\n    _assertNotRegex = \"assertNotRegexpMatches\"\n_add_doc(b, \"\"\"Byte literal\"\"\")\n_add_doc(u, \"\"\"Text literal\"\"\")\n\n\ndef assertCountEqual(self, *args, **kwargs):\n    return getattr(self, _assertCountEqual)(*args, **kwargs)\n\n\ndef assertRaisesRegex(self, *args, **kwargs):\n    return getattr(self, _assertRaisesRegex)(*args, **kwargs)\n\n\ndef assertRegex(self, *args, **kwargs):\n    return getattr(self, _assertRegex)(*args, **kwargs)\n\n\ndef assertNotRegex(self, *args, **kwargs):\n    return getattr(self, _assertNotRegex)(*args, **kwargs)\n\n\nif PY3:\n    exec_ = getattr(moves.builtins, \"exec\")\n\n    def reraise(tp, value, tb=None):\n        try:\n            if value is None:\n                value = tp()\n            if value.__traceback__ is not tb:\n                raise value.with_traceback(tb)\n            raise value\n        finally:\n            value = None\n            tb = None\n\n\nelse:\n\n    def exec_(_code_, _globs_=None, _locs_=None):\n        \"\"\"Execute code in a namespace.\"\"\"\n        if _globs_ is None:\n            frame = sys._getframe(1)\n            _globs_ = frame.f_globals\n            if _locs_ is None:\n                _locs_ = frame.f_locals\n            del frame\n        elif _locs_ is None:\n            _locs_ = _globs_\n        exec (\"\"\"exec _code_ in _globs_, _locs_\"\"\")\n\n    exec_(\n        \"\"\"def reraise(tp, value, tb=None):\n    try:\n        raise tp, value, tb\n    finally:\n        tb = None\n\"\"\"\n    )\n\n\nif sys.version_info[:2] > (3,):\n    exec_(\n        \"\"\"def raise_from(value, from_value):\n    try:\n        raise value from from_value\n    finally:\n        value = None\n\"\"\"\n    )\nelse:\n\n    def raise_from(value, from_value):\n        raise value\n\n\nprint_ = getattr(moves.builtins, \"print\", None)\nif print_ is None:\n\n    def print_(*args, **kwargs):\n        \"\"\"The new-style print function for Python 2.4 and 2.5.\"\"\"\n        fp = kwargs.pop(\"file\", sys.stdout)\n        if fp is None:\n            return\n\n        def write(data):\n            if not isinstance(data, basestring):\n                data = str(data)\n            # If the file has an encoding, encode unicode with it.\n            if (\n                isinstance(fp, file)\n                and isinstance(data, unicode)\n                and fp.encoding is not None\n            ):\n                errors = getattr(fp, \"errors\", None)\n                if errors is None:\n                    errors = \"strict\"\n                data = data.encode(fp.encoding, errors)\n            fp.write(data)\n\n        want_unicode = False\n        sep = kwargs.pop(\"sep\", None)\n        if sep is not None:\n            if isinstance(sep, unicode):\n                want_unicode = True\n            elif not isinstance(sep, str):\n                raise TypeError(\"sep must be None or a string\")\n        end = kwargs.pop(\"end\", None)\n        if end is not None:\n            if isinstance(end, unicode):\n                want_unicode = True\n            elif not isinstance(end, str):\n                raise TypeError(\"end must be None or a string\")\n        if kwargs:\n            raise TypeError(\"invalid keyword arguments to print()\")\n        if not want_unicode:\n            for arg in args:\n                if isinstance(arg, unicode):\n                    want_unicode = True\n                    break\n        if want_unicode:\n            newline = unicode(\"\\n\")\n            space = unicode(\" \")\n        else:\n            newline = \"\\n\"\n            space = \" \"\n        if sep is None:\n            sep = space\n        if end is None:\n            end = newline\n        for i, arg in enumerate(args):\n            if i:\n                write(sep)\n            write(arg)\n        write(end)\n\n\nif sys.version_info[:2] < (3, 3):\n    _print = print_\n\n    def print_(*args, **kwargs):\n        fp = kwargs.get(\"file\", sys.stdout)\n        flush = kwargs.pop(\"flush\", False)\n        _print(*args, **kwargs)\n        if flush and fp is not None:\n            fp.flush()\n\n\n_add_doc(reraise, \"\"\"Reraise an exception.\"\"\")\n\nif sys.version_info[0:2] < (3, 4):\n    # This does exactly the same what the :func:`py3:functools.update_wrapper`\n    # function does on Python versions after 3.2. It sets the ``__wrapped__``\n    # attribute on ``wrapper`` object and it doesn't raise an error if any of\n    # the attributes mentioned in ``assigned`` and ``updated`` are missing on\n    # ``wrapped`` object.\n    def _update_wrapper(\n        wrapper,\n        wrapped,\n        assigned=functools.WRAPPER_ASSIGNMENTS,\n        updated=functools.WRAPPER_UPDATES,\n    ):\n        for attr in assigned:\n            try:\n                value = getattr(wrapped, attr)\n            except AttributeError:\n                continue\n            else:\n                setattr(wrapper, attr, value)\n        for attr in updated:\n            getattr(wrapper, attr).update(getattr(wrapped, attr, {}))\n        wrapper.__wrapped__ = wrapped\n        return wrapper\n\n    _update_wrapper.__doc__ = functools.update_wrapper.__doc__\n\n    def wraps(\n        wrapped,\n        assigned=functools.WRAPPER_ASSIGNMENTS,\n        updated=functools.WRAPPER_UPDATES,\n    ):\n        return functools.partial(\n            _update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated\n        )\n\n    wraps.__doc__ = functools.wraps.__doc__\n\nelse:\n    wraps = functools.wraps\n\n\ndef with_metaclass(meta, *bases):\n    \"\"\"Create a base class with a metaclass.\"\"\"\n    # This requires a bit of explanation: the basic idea is to make a dummy\n    # metaclass for one level of class instantiation that replaces itself with\n    # the actual metaclass.\n    class metaclass(type):\n        def __new__(cls, name, this_bases, d):\n            if sys.version_info[:2] >= (3, 7):\n                # This version introduced PEP 560 that requires a bit\n                # of extra care (we mimic what is done by __build_class__).\n                resolved_bases = types.resolve_bases(bases)\n                if resolved_bases is not bases:\n                    d[\"__orig_bases__\"] = bases\n            else:\n                resolved_bases = bases\n            return meta(name, resolved_bases, d)\n\n        @classmethod\n        def __prepare__(cls, name, this_bases):\n            return meta.__prepare__(name, bases)\n\n    return type.__new__(metaclass, \"temporary_class\", (), {})\n\n\ndef add_metaclass(metaclass):\n    \"\"\"Class decorator for creating a class with a metaclass.\"\"\"\n\n    def wrapper(cls):\n        orig_vars = cls.__dict__.copy()\n        slots = orig_vars.get(\"__slots__\")\n        if slots is not None:\n            if isinstance(slots, str):\n                slots = [slots]\n            for slots_var in slots:\n                orig_vars.pop(slots_var)\n        orig_vars.pop(\"__dict__\", None)\n        orig_vars.pop(\"__weakref__\", None)\n        if hasattr(cls, \"__qualname__\"):\n            orig_vars[\"__qualname__\"] = cls.__qualname__\n        return metaclass(cls.__name__, cls.__bases__, orig_vars)\n\n    return wrapper\n\n\ndef ensure_binary(s, encoding=\"utf-8\", errors=\"strict\"):\n    \"\"\"Coerce **s** to six.binary_type.\n\n    For Python 2:\n      - `unicode` -> encoded to `str`\n      - `str` -> `str`\n\n    For Python 3:\n      - `str` -> encoded to `bytes`\n      - `bytes` -> `bytes`\n    \"\"\"\n    if isinstance(s, binary_type):\n        return s\n    if isinstance(s, text_type):\n        return s.encode(encoding, errors)\n    raise TypeError(\"not expecting type '%s'\" % type(s))\n\n\ndef ensure_str(s, encoding=\"utf-8\", errors=\"strict\"):\n    \"\"\"Coerce *s* to `str`.\n\n    For Python 2:\n      - `unicode` -> encoded to `str`\n      - `str` -> `str`\n\n    For Python 3:\n      - `str` -> `str`\n      - `bytes` -> decoded to `str`\n    \"\"\"\n    # Optimization: Fast return for the common case.\n    if type(s) is str:\n        return s\n    if PY2 and isinstance(s, text_type):\n        return s.encode(encoding, errors)\n    elif PY3 and isinstance(s, binary_type):\n        return s.decode(encoding, errors)\n    elif not isinstance(s, (text_type, binary_type)):\n        raise TypeError(\"not expecting type '%s'\" % type(s))\n    return s\n\n\ndef ensure_text(s, encoding=\"utf-8\", errors=\"strict\"):\n    \"\"\"Coerce *s* to six.text_type.\n\n    For Python 2:\n      - `unicode` -> `unicode`\n      - `str` -> `unicode`\n\n    For Python 3:\n      - `str` -> `str`\n      - `bytes` -> decoded to `str`\n    \"\"\"\n    if isinstance(s, binary_type):\n        return s.decode(encoding, errors)\n    elif isinstance(s, text_type):\n        return s\n    else:\n        raise TypeError(\"not expecting type '%s'\" % type(s))\n\n\ndef python_2_unicode_compatible(klass):\n    \"\"\"\n    A class decorator that defines __unicode__ and __str__ methods under Python 2.\n    Under Python 3 it does nothing.\n\n    To support Python 2 and 3 with a single code base, define a __str__ method\n    returning text and apply this decorator to the class.\n    \"\"\"\n    if PY2:\n        if \"__str__\" not in klass.__dict__:\n            raise ValueError(\n                \"@python_2_unicode_compatible cannot be applied \"\n                \"to %s because it doesn't define __str__().\" % klass.__name__\n            )\n        klass.__unicode__ = klass.__str__\n        klass.__str__ = lambda self: self.__unicode__().encode(\"utf-8\")\n    return klass\n\n\n# Complete the moves implementation.\n# This code is at the end of this module to speed up module loading.\n# Turn this module into a package.\n__path__ = []  # required for PEP 302 and PEP 451\n__package__ = __name__  # see PEP 366 @ReservedAssignment\nif globals().get(\"__spec__\") is not None:\n    __spec__.submodule_search_locations = []  # PEP 451 @UndefinedVariable\n# Remove other six meta path importers, since they cause problems. This can\n# happen if six is removed from sys.modules and then reloaded. (Setuptools does\n# this for some reason.)\nif sys.meta_path:\n    for i, importer in enumerate(sys.meta_path):\n        # Here's some real nastiness: Another \"instance\" of the six module might\n        # be floating around. Therefore, we can't use isinstance() to check for\n        # the six meta path importer, since the other six instance will have\n        # inserted an importer with different class.\n        if (\n            type(importer).__name__ == \"_SixMetaPathImporter\"\n            and importer.name == __name__\n        ):\n            del sys.meta_path[i]\n            break\n    del i, importer\n# Finally, add the importer to the meta path import hook.\nsys.meta_path.append(_importer)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/packages/ssl_match_hostname/__init__.py",
    "content": "import sys\n\ntry:\n    # Our match_hostname function is the same as 3.10's, so we only want to\n    # import the match_hostname function if it's at least that good.\n    # We also fallback on Python 3.10+ because our code doesn't emit\n    # deprecation warnings and is the same as Python 3.10 otherwise.\n    if sys.version_info < (3, 5) or sys.version_info >= (3, 10):\n        raise ImportError(\"Fallback to vendored code\")\n\n    from ssl import CertificateError, match_hostname\nexcept ImportError:\n    try:\n        # Backport of the function from a pypi module\n        from backports.ssl_match_hostname import (  # type: ignore\n            CertificateError,\n            match_hostname,\n        )\n    except ImportError:\n        # Our vendored copy\n        from ._implementation import CertificateError, match_hostname  # type: ignore\n\n# Not needed, but documenting what we provide.\n__all__ = (\"CertificateError\", \"match_hostname\")\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/packages/ssl_match_hostname/_implementation.py",
    "content": "\"\"\"The match_hostname() function from Python 3.3.3, essential when using SSL.\"\"\"\n\n# Note: This file is under the PSF license as the code comes from the python\n# stdlib.   http://docs.python.org/3/license.html\n\nimport re\nimport sys\n\n# ipaddress has been backported to 2.6+ in pypi.  If it is installed on the\n# system, use it to handle IPAddress ServerAltnames (this was added in\n# python-3.5) otherwise only do DNS matching.  This allows\n# backports.ssl_match_hostname to continue to be used in Python 2.7.\ntry:\n    import ipaddress\nexcept ImportError:\n    ipaddress = None\n\n__version__ = \"3.5.0.1\"\n\n\nclass CertificateError(ValueError):\n    pass\n\n\ndef _dnsname_match(dn, hostname, max_wildcards=1):\n    \"\"\"Matching according to RFC 6125, section 6.4.3\n\n    http://tools.ietf.org/html/rfc6125#section-6.4.3\n    \"\"\"\n    pats = []\n    if not dn:\n        return False\n\n    # Ported from python3-syntax:\n    # leftmost, *remainder = dn.split(r'.')\n    parts = dn.split(r\".\")\n    leftmost = parts[0]\n    remainder = parts[1:]\n\n    wildcards = leftmost.count(\"*\")\n    if wildcards > max_wildcards:\n        # Issue #17980: avoid denials of service by refusing more\n        # than one wildcard per fragment.  A survey of established\n        # policy among SSL implementations showed it to be a\n        # reasonable choice.\n        raise CertificateError(\n            \"too many wildcards in certificate DNS name: \" + repr(dn)\n        )\n\n    # speed up common case w/o wildcards\n    if not wildcards:\n        return dn.lower() == hostname.lower()\n\n    # RFC 6125, section 6.4.3, subitem 1.\n    # The client SHOULD NOT attempt to match a presented identifier in which\n    # the wildcard character comprises a label other than the left-most label.\n    if leftmost == \"*\":\n        # When '*' is a fragment by itself, it matches a non-empty dotless\n        # fragment.\n        pats.append(\"[^.]+\")\n    elif leftmost.startswith(\"xn--\") or hostname.startswith(\"xn--\"):\n        # RFC 6125, section 6.4.3, subitem 3.\n        # The client SHOULD NOT attempt to match a presented identifier\n        # where the wildcard character is embedded within an A-label or\n        # U-label of an internationalized domain name.\n        pats.append(re.escape(leftmost))\n    else:\n        # Otherwise, '*' matches any dotless string, e.g. www*\n        pats.append(re.escape(leftmost).replace(r\"\\*\", \"[^.]*\"))\n\n    # add the remaining fragments, ignore any wildcards\n    for frag in remainder:\n        pats.append(re.escape(frag))\n\n    pat = re.compile(r\"\\A\" + r\"\\.\".join(pats) + r\"\\Z\", re.IGNORECASE)\n    return pat.match(hostname)\n\n\ndef _to_unicode(obj):\n    if isinstance(obj, str) and sys.version_info < (3,):\n        obj = unicode(obj, encoding=\"ascii\", errors=\"strict\")\n    return obj\n\n\ndef _ipaddress_match(ipname, host_ip):\n    \"\"\"Exact matching of IP addresses.\n\n    RFC 6125 explicitly doesn't define an algorithm for this\n    (section 1.7.2 - \"Out of Scope\").\n    \"\"\"\n    # OpenSSL may add a trailing newline to a subjectAltName's IP address\n    # Divergence from upstream: ipaddress can't handle byte str\n    ip = ipaddress.ip_address(_to_unicode(ipname).rstrip())\n    return ip == host_ip\n\n\ndef match_hostname(cert, hostname):\n    \"\"\"Verify that *cert* (in decoded format as returned by\n    SSLSocket.getpeercert()) matches the *hostname*.  RFC 2818 and RFC 6125\n    rules are followed, but IP addresses are not accepted for *hostname*.\n\n    CertificateError is raised on failure. On success, the function\n    returns nothing.\n    \"\"\"\n    if not cert:\n        raise ValueError(\n            \"empty or no certificate, match_hostname needs a \"\n            \"SSL socket or SSL context with either \"\n            \"CERT_OPTIONAL or CERT_REQUIRED\"\n        )\n    try:\n        # Divergence from upstream: ipaddress can't handle byte str\n        host_ip = ipaddress.ip_address(_to_unicode(hostname))\n    except ValueError:\n        # Not an IP address (common case)\n        host_ip = None\n    except UnicodeError:\n        # Divergence from upstream: Have to deal with ipaddress not taking\n        # byte strings.  addresses should be all ascii, so we consider it not\n        # an ipaddress in this case\n        host_ip = None\n    except AttributeError:\n        # Divergence from upstream: Make ipaddress library optional\n        if ipaddress is None:\n            host_ip = None\n        else:\n            raise\n    dnsnames = []\n    san = cert.get(\"subjectAltName\", ())\n    for key, value in san:\n        if key == \"DNS\":\n            if host_ip is None and _dnsname_match(value, hostname):\n                return\n            dnsnames.append(value)\n        elif key == \"IP Address\":\n            if host_ip is not None and _ipaddress_match(value, host_ip):\n                return\n            dnsnames.append(value)\n    if not dnsnames:\n        # The subject is only checked when there is no dNSName entry\n        # in subjectAltName\n        for sub in cert.get(\"subject\", ()):\n            for key, value in sub:\n                # XXX according to RFC 2818, the most specific Common Name\n                # must be used.\n                if key == \"commonName\":\n                    if _dnsname_match(value, hostname):\n                        return\n                    dnsnames.append(value)\n    if len(dnsnames) > 1:\n        raise CertificateError(\n            \"hostname %r \"\n            \"doesn't match either of %s\" % (hostname, \", \".join(map(repr, dnsnames)))\n        )\n    elif len(dnsnames) == 1:\n        raise CertificateError(\"hostname %r doesn't match %r\" % (hostname, dnsnames[0]))\n    else:\n        raise CertificateError(\n            \"no appropriate commonName or subjectAltName fields were found\"\n        )\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/poolmanager.py",
    "content": "from __future__ import absolute_import\n\nimport collections\nimport functools\nimport logging\n\nfrom ._collections import RecentlyUsedContainer\nfrom .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme\nfrom .exceptions import (\n    LocationValueError,\n    MaxRetryError,\n    ProxySchemeUnknown,\n    ProxySchemeUnsupported,\n    URLSchemeUnknown,\n)\nfrom .packages import six\nfrom .packages.six.moves.urllib.parse import urljoin\nfrom .request import RequestMethods\nfrom .util.proxy import connection_requires_http_tunnel\nfrom .util.retry import Retry\nfrom .util.url import parse_url\n\n__all__ = [\"PoolManager\", \"ProxyManager\", \"proxy_from_url\"]\n\n\nlog = logging.getLogger(__name__)\n\nSSL_KEYWORDS = (\n    \"key_file\",\n    \"cert_file\",\n    \"cert_reqs\",\n    \"ca_certs\",\n    \"ssl_version\",\n    \"ca_cert_dir\",\n    \"ssl_context\",\n    \"key_password\",\n)\n\n# All known keyword arguments that could be provided to the pool manager, its\n# pools, or the underlying connections. This is used to construct a pool key.\n_key_fields = (\n    \"key_scheme\",  # str\n    \"key_host\",  # str\n    \"key_port\",  # int\n    \"key_timeout\",  # int or float or Timeout\n    \"key_retries\",  # int or Retry\n    \"key_strict\",  # bool\n    \"key_block\",  # bool\n    \"key_source_address\",  # str\n    \"key_key_file\",  # str\n    \"key_key_password\",  # str\n    \"key_cert_file\",  # str\n    \"key_cert_reqs\",  # str\n    \"key_ca_certs\",  # str\n    \"key_ssl_version\",  # str\n    \"key_ca_cert_dir\",  # str\n    \"key_ssl_context\",  # instance of ssl.SSLContext or urllib3.util.ssl_.SSLContext\n    \"key_maxsize\",  # int\n    \"key_headers\",  # dict\n    \"key__proxy\",  # parsed proxy url\n    \"key__proxy_headers\",  # dict\n    \"key__proxy_config\",  # class\n    \"key_socket_options\",  # list of (level (int), optname (int), value (int or str)) tuples\n    \"key__socks_options\",  # dict\n    \"key_assert_hostname\",  # bool or string\n    \"key_assert_fingerprint\",  # str\n    \"key_server_hostname\",  # str\n)\n\n#: The namedtuple class used to construct keys for the connection pool.\n#: All custom key schemes should include the fields in this key at a minimum.\nPoolKey = collections.namedtuple(\"PoolKey\", _key_fields)\n\n_proxy_config_fields = (\"ssl_context\", \"use_forwarding_for_https\")\nProxyConfig = collections.namedtuple(\"ProxyConfig\", _proxy_config_fields)\n\n\ndef _default_key_normalizer(key_class, request_context):\n    \"\"\"\n    Create a pool key out of a request context dictionary.\n\n    According to RFC 3986, both the scheme and host are case-insensitive.\n    Therefore, this function normalizes both before constructing the pool\n    key for an HTTPS request. If you wish to change this behaviour, provide\n    alternate callables to ``key_fn_by_scheme``.\n\n    :param key_class:\n        The class to use when constructing the key. This should be a namedtuple\n        with the ``scheme`` and ``host`` keys at a minimum.\n    :type  key_class: namedtuple\n    :param request_context:\n        A dictionary-like object that contain the context for a request.\n    :type  request_context: dict\n\n    :return: A namedtuple that can be used as a connection pool key.\n    :rtype:  PoolKey\n    \"\"\"\n    # Since we mutate the dictionary, make a copy first\n    context = request_context.copy()\n    context[\"scheme\"] = context[\"scheme\"].lower()\n    context[\"host\"] = context[\"host\"].lower()\n\n    # These are both dictionaries and need to be transformed into frozensets\n    for key in (\"headers\", \"_proxy_headers\", \"_socks_options\"):\n        if key in context and context[key] is not None:\n            context[key] = frozenset(context[key].items())\n\n    # The socket_options key may be a list and needs to be transformed into a\n    # tuple.\n    socket_opts = context.get(\"socket_options\")\n    if socket_opts is not None:\n        context[\"socket_options\"] = tuple(socket_opts)\n\n    # Map the kwargs to the names in the namedtuple - this is necessary since\n    # namedtuples can't have fields starting with '_'.\n    for key in list(context.keys()):\n        context[\"key_\" + key] = context.pop(key)\n\n    # Default to ``None`` for keys missing from the context\n    for field in key_class._fields:\n        if field not in context:\n            context[field] = None\n\n    return key_class(**context)\n\n\n#: A dictionary that maps a scheme to a callable that creates a pool key.\n#: This can be used to alter the way pool keys are constructed, if desired.\n#: Each PoolManager makes a copy of this dictionary so they can be configured\n#: globally here, or individually on the instance.\nkey_fn_by_scheme = {\n    \"http\": functools.partial(_default_key_normalizer, PoolKey),\n    \"https\": functools.partial(_default_key_normalizer, PoolKey),\n}\n\npool_classes_by_scheme = {\"http\": HTTPConnectionPool, \"https\": HTTPSConnectionPool}\n\n\nclass PoolManager(RequestMethods):\n    \"\"\"\n    Allows for arbitrary requests while transparently keeping track of\n    necessary connection pools for you.\n\n    :param num_pools:\n        Number of connection pools to cache before discarding the least\n        recently used pool.\n\n    :param headers:\n        Headers to include with all requests, unless other headers are given\n        explicitly.\n\n    :param \\\\**connection_pool_kw:\n        Additional parameters are used to create fresh\n        :class:`urllib3.connectionpool.ConnectionPool` instances.\n\n    Example::\n\n        >>> manager = PoolManager(num_pools=2)\n        >>> r = manager.request('GET', 'http://google.com/')\n        >>> r = manager.request('GET', 'http://google.com/mail')\n        >>> r = manager.request('GET', 'http://yahoo.com/')\n        >>> len(manager.pools)\n        2\n\n    \"\"\"\n\n    proxy = None\n    proxy_config = None\n\n    def __init__(self, num_pools=10, headers=None, **connection_pool_kw):\n        RequestMethods.__init__(self, headers)\n        self.connection_pool_kw = connection_pool_kw\n        self.pools = RecentlyUsedContainer(num_pools, dispose_func=lambda p: p.close())\n\n        # Locally set the pool classes and keys so other PoolManagers can\n        # override them.\n        self.pool_classes_by_scheme = pool_classes_by_scheme\n        self.key_fn_by_scheme = key_fn_by_scheme.copy()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.clear()\n        # Return False to re-raise any potential exceptions\n        return False\n\n    def _new_pool(self, scheme, host, port, request_context=None):\n        \"\"\"\n        Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and\n        any additional pool keyword arguments.\n\n        If ``request_context`` is provided, it is provided as keyword arguments\n        to the pool class used. This method is used to actually create the\n        connection pools handed out by :meth:`connection_from_url` and\n        companion methods. It is intended to be overridden for customization.\n        \"\"\"\n        pool_cls = self.pool_classes_by_scheme[scheme]\n        if request_context is None:\n            request_context = self.connection_pool_kw.copy()\n\n        # Although the context has everything necessary to create the pool,\n        # this function has historically only used the scheme, host, and port\n        # in the positional args. When an API change is acceptable these can\n        # be removed.\n        for key in (\"scheme\", \"host\", \"port\"):\n            request_context.pop(key, None)\n\n        if scheme == \"http\":\n            for kw in SSL_KEYWORDS:\n                request_context.pop(kw, None)\n\n        return pool_cls(host, port, **request_context)\n\n    def clear(self):\n        \"\"\"\n        Empty our store of pools and direct them all to close.\n\n        This will not affect in-flight connections, but they will not be\n        re-used after completion.\n        \"\"\"\n        self.pools.clear()\n\n    def connection_from_host(self, host, port=None, scheme=\"http\", pool_kwargs=None):\n        \"\"\"\n        Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme.\n\n        If ``port`` isn't given, it will be derived from the ``scheme`` using\n        ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is\n        provided, it is merged with the instance's ``connection_pool_kw``\n        variable and used to create the new connection pool, if one is\n        needed.\n        \"\"\"\n\n        if not host:\n            raise LocationValueError(\"No host specified.\")\n\n        request_context = self._merge_pool_kwargs(pool_kwargs)\n        request_context[\"scheme\"] = scheme or \"http\"\n        if not port:\n            port = port_by_scheme.get(request_context[\"scheme\"].lower(), 80)\n        request_context[\"port\"] = port\n        request_context[\"host\"] = host\n\n        return self.connection_from_context(request_context)\n\n    def connection_from_context(self, request_context):\n        \"\"\"\n        Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context.\n\n        ``request_context`` must at least contain the ``scheme`` key and its\n        value must be a key in ``key_fn_by_scheme`` instance variable.\n        \"\"\"\n        scheme = request_context[\"scheme\"].lower()\n        pool_key_constructor = self.key_fn_by_scheme.get(scheme)\n        if not pool_key_constructor:\n            raise URLSchemeUnknown(scheme)\n        pool_key = pool_key_constructor(request_context)\n\n        return self.connection_from_pool_key(pool_key, request_context=request_context)\n\n    def connection_from_pool_key(self, pool_key, request_context=None):\n        \"\"\"\n        Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key.\n\n        ``pool_key`` should be a namedtuple that only contains immutable\n        objects. At a minimum it must have the ``scheme``, ``host``, and\n        ``port`` fields.\n        \"\"\"\n        with self.pools.lock:\n            # If the scheme, host, or port doesn't match existing open\n            # connections, open a new ConnectionPool.\n            pool = self.pools.get(pool_key)\n            if pool:\n                return pool\n\n            # Make a fresh ConnectionPool of the desired type\n            scheme = request_context[\"scheme\"]\n            host = request_context[\"host\"]\n            port = request_context[\"port\"]\n            pool = self._new_pool(scheme, host, port, request_context=request_context)\n            self.pools[pool_key] = pool\n\n        return pool\n\n    def connection_from_url(self, url, pool_kwargs=None):\n        \"\"\"\n        Similar to :func:`urllib3.connectionpool.connection_from_url`.\n\n        If ``pool_kwargs`` is not provided and a new pool needs to be\n        constructed, ``self.connection_pool_kw`` is used to initialize\n        the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs``\n        is provided, it is used instead. Note that if a new pool does not\n        need to be created for the request, the provided ``pool_kwargs`` are\n        not used.\n        \"\"\"\n        u = parse_url(url)\n        return self.connection_from_host(\n            u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs\n        )\n\n    def _merge_pool_kwargs(self, override):\n        \"\"\"\n        Merge a dictionary of override values for self.connection_pool_kw.\n\n        This does not modify self.connection_pool_kw and returns a new dict.\n        Any keys in the override dictionary with a value of ``None`` are\n        removed from the merged dictionary.\n        \"\"\"\n        base_pool_kwargs = self.connection_pool_kw.copy()\n        if override:\n            for key, value in override.items():\n                if value is None:\n                    try:\n                        del base_pool_kwargs[key]\n                    except KeyError:\n                        pass\n                else:\n                    base_pool_kwargs[key] = value\n        return base_pool_kwargs\n\n    def _proxy_requires_url_absolute_form(self, parsed_url):\n        \"\"\"\n        Indicates if the proxy requires the complete destination URL in the\n        request.  Normally this is only needed when not using an HTTP CONNECT\n        tunnel.\n        \"\"\"\n        if self.proxy is None:\n            return False\n\n        return not connection_requires_http_tunnel(\n            self.proxy, self.proxy_config, parsed_url.scheme\n        )\n\n    def _validate_proxy_scheme_url_selection(self, url_scheme):\n        \"\"\"\n        Validates that were not attempting to do TLS in TLS connections on\n        Python2 or with unsupported SSL implementations.\n        \"\"\"\n        if self.proxy is None or url_scheme != \"https\":\n            return\n\n        if self.proxy.scheme != \"https\":\n            return\n\n        if six.PY2 and not self.proxy_config.use_forwarding_for_https:\n            raise ProxySchemeUnsupported(\n                \"Contacting HTTPS destinations through HTTPS proxies \"\n                \"'via CONNECT tunnels' is not supported in Python 2\"\n            )\n\n    def urlopen(self, method, url, redirect=True, **kw):\n        \"\"\"\n        Same as :meth:`urllib3.HTTPConnectionPool.urlopen`\n        with custom cross-host redirect logic and only sends the request-uri\n        portion of the ``url``.\n\n        The given ``url`` parameter must be absolute, such that an appropriate\n        :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it.\n        \"\"\"\n        u = parse_url(url)\n        self._validate_proxy_scheme_url_selection(u.scheme)\n\n        conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)\n\n        kw[\"assert_same_host\"] = False\n        kw[\"redirect\"] = False\n\n        if \"headers\" not in kw:\n            kw[\"headers\"] = self.headers.copy()\n\n        if self._proxy_requires_url_absolute_form(u):\n            response = conn.urlopen(method, url, **kw)\n        else:\n            response = conn.urlopen(method, u.request_uri, **kw)\n\n        redirect_location = redirect and response.get_redirect_location()\n        if not redirect_location:\n            return response\n\n        # Support relative URLs for redirecting.\n        redirect_location = urljoin(url, redirect_location)\n\n        # RFC 7231, Section 6.4.4\n        if response.status == 303:\n            method = \"GET\"\n\n        retries = kw.get(\"retries\")\n        if not isinstance(retries, Retry):\n            retries = Retry.from_int(retries, redirect=redirect)\n\n        # Strip headers marked as unsafe to forward to the redirected location.\n        # Check remove_headers_on_redirect to avoid a potential network call within\n        # conn.is_same_host() which may use socket.gethostbyname() in the future.\n        if retries.remove_headers_on_redirect and not conn.is_same_host(\n            redirect_location\n        ):\n            headers = list(six.iterkeys(kw[\"headers\"]))\n            for header in headers:\n                if header.lower() in retries.remove_headers_on_redirect:\n                    kw[\"headers\"].pop(header, None)\n\n        try:\n            retries = retries.increment(method, url, response=response, _pool=conn)\n        except MaxRetryError:\n            if retries.raise_on_redirect:\n                response.drain_conn()\n                raise\n            return response\n\n        kw[\"retries\"] = retries\n        kw[\"redirect\"] = redirect\n\n        log.info(\"Redirecting %s -> %s\", url, redirect_location)\n\n        response.drain_conn()\n        return self.urlopen(method, redirect_location, **kw)\n\n\nclass ProxyManager(PoolManager):\n    \"\"\"\n    Behaves just like :class:`PoolManager`, but sends all requests through\n    the defined proxy, using the CONNECT method for HTTPS URLs.\n\n    :param proxy_url:\n        The URL of the proxy to be used.\n\n    :param proxy_headers:\n        A dictionary containing headers that will be sent to the proxy. In case\n        of HTTP they are being sent with each request, while in the\n        HTTPS/CONNECT case they are sent only once. Could be used for proxy\n        authentication.\n\n    :param proxy_ssl_context:\n        The proxy SSL context is used to establish the TLS connection to the\n        proxy when using HTTPS proxies.\n\n    :param use_forwarding_for_https:\n        (Defaults to False) If set to True will forward requests to the HTTPS\n        proxy to be made on behalf of the client instead of creating a TLS\n        tunnel via the CONNECT method. **Enabling this flag means that request\n        and response headers and content will be visible from the HTTPS proxy**\n        whereas tunneling keeps request and response headers and content\n        private.  IP address, target hostname, SNI, and port are always visible\n        to an HTTPS proxy even when this flag is disabled.\n\n    Example:\n        >>> proxy = urllib3.ProxyManager('http://localhost:3128/')\n        >>> r1 = proxy.request('GET', 'http://google.com/')\n        >>> r2 = proxy.request('GET', 'http://httpbin.org/')\n        >>> len(proxy.pools)\n        1\n        >>> r3 = proxy.request('GET', 'https://httpbin.org/')\n        >>> r4 = proxy.request('GET', 'https://twitter.com/')\n        >>> len(proxy.pools)\n        3\n\n    \"\"\"\n\n    def __init__(\n        self,\n        proxy_url,\n        num_pools=10,\n        headers=None,\n        proxy_headers=None,\n        proxy_ssl_context=None,\n        use_forwarding_for_https=False,\n        **connection_pool_kw\n    ):\n\n        if isinstance(proxy_url, HTTPConnectionPool):\n            proxy_url = \"%s://%s:%i\" % (\n                proxy_url.scheme,\n                proxy_url.host,\n                proxy_url.port,\n            )\n        proxy = parse_url(proxy_url)\n\n        if proxy.scheme not in (\"http\", \"https\"):\n            raise ProxySchemeUnknown(proxy.scheme)\n\n        if not proxy.port:\n            port = port_by_scheme.get(proxy.scheme, 80)\n            proxy = proxy._replace(port=port)\n\n        self.proxy = proxy\n        self.proxy_headers = proxy_headers or {}\n        self.proxy_ssl_context = proxy_ssl_context\n        self.proxy_config = ProxyConfig(proxy_ssl_context, use_forwarding_for_https)\n\n        connection_pool_kw[\"_proxy\"] = self.proxy\n        connection_pool_kw[\"_proxy_headers\"] = self.proxy_headers\n        connection_pool_kw[\"_proxy_config\"] = self.proxy_config\n\n        super(ProxyManager, self).__init__(num_pools, headers, **connection_pool_kw)\n\n    def connection_from_host(self, host, port=None, scheme=\"http\", pool_kwargs=None):\n        if scheme == \"https\":\n            return super(ProxyManager, self).connection_from_host(\n                host, port, scheme, pool_kwargs=pool_kwargs\n            )\n\n        return super(ProxyManager, self).connection_from_host(\n            self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs\n        )\n\n    def _set_proxy_headers(self, url, headers=None):\n        \"\"\"\n        Sets headers needed by proxies: specifically, the Accept and Host\n        headers. Only sets headers not provided by the user.\n        \"\"\"\n        headers_ = {\"Accept\": \"*/*\"}\n\n        netloc = parse_url(url).netloc\n        if netloc:\n            headers_[\"Host\"] = netloc\n\n        if headers:\n            headers_.update(headers)\n        return headers_\n\n    def urlopen(self, method, url, redirect=True, **kw):\n        \"Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute.\"\n        u = parse_url(url)\n        if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme):\n            # For connections using HTTP CONNECT, httplib sets the necessary\n            # headers on the CONNECT to the proxy. If we're not using CONNECT,\n            # we'll definitely need to set 'Host' at the very least.\n            headers = kw.get(\"headers\", self.headers)\n            kw[\"headers\"] = self._set_proxy_headers(url, headers)\n\n        return super(ProxyManager, self).urlopen(method, url, redirect=redirect, **kw)\n\n\ndef proxy_from_url(url, **kw):\n    return ProxyManager(proxy_url=url, **kw)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/request.py",
    "content": "from __future__ import absolute_import\n\nfrom .filepost import encode_multipart_formdata\nfrom .packages.six.moves.urllib.parse import urlencode\n\n__all__ = [\"RequestMethods\"]\n\n\nclass RequestMethods(object):\n    \"\"\"\n    Convenience mixin for classes who implement a :meth:`urlopen` method, such\n    as :class:`urllib3.HTTPConnectionPool` and\n    :class:`urllib3.PoolManager`.\n\n    Provides behavior for making common types of HTTP request methods and\n    decides which type of request field encoding to use.\n\n    Specifically,\n\n    :meth:`.request_encode_url` is for sending requests whose fields are\n    encoded in the URL (such as GET, HEAD, DELETE).\n\n    :meth:`.request_encode_body` is for sending requests whose fields are\n    encoded in the *body* of the request using multipart or www-form-urlencoded\n    (such as for POST, PUT, PATCH).\n\n    :meth:`.request` is for making any kind of request, it will look up the\n    appropriate encoding format and use one of the above two methods to make\n    the request.\n\n    Initializer parameters:\n\n    :param headers:\n        Headers to include with all requests, unless other headers are given\n        explicitly.\n    \"\"\"\n\n    _encode_url_methods = {\"DELETE\", \"GET\", \"HEAD\", \"OPTIONS\"}\n\n    def __init__(self, headers=None):\n        self.headers = headers or {}\n\n    def urlopen(\n        self,\n        method,\n        url,\n        body=None,\n        headers=None,\n        encode_multipart=True,\n        multipart_boundary=None,\n        **kw\n    ):  # Abstract\n        raise NotImplementedError(\n            \"Classes extending RequestMethods must implement \"\n            \"their own ``urlopen`` method.\"\n        )\n\n    def request(self, method, url, fields=None, headers=None, **urlopen_kw):\n        \"\"\"\n        Make a request using :meth:`urlopen` with the appropriate encoding of\n        ``fields`` based on the ``method`` used.\n\n        This is a convenience method that requires the least amount of manual\n        effort. It can be used in most situations, while still having the\n        option to drop down to more specific methods when necessary, such as\n        :meth:`request_encode_url`, :meth:`request_encode_body`,\n        or even the lowest level :meth:`urlopen`.\n        \"\"\"\n        method = method.upper()\n\n        urlopen_kw[\"request_url\"] = url\n\n        if method in self._encode_url_methods:\n            return self.request_encode_url(\n                method, url, fields=fields, headers=headers, **urlopen_kw\n            )\n        else:\n            return self.request_encode_body(\n                method, url, fields=fields, headers=headers, **urlopen_kw\n            )\n\n    def request_encode_url(self, method, url, fields=None, headers=None, **urlopen_kw):\n        \"\"\"\n        Make a request using :meth:`urlopen` with the ``fields`` encoded in\n        the url. This is useful for request methods like GET, HEAD, DELETE, etc.\n        \"\"\"\n        if headers is None:\n            headers = self.headers\n\n        extra_kw = {\"headers\": headers}\n        extra_kw.update(urlopen_kw)\n\n        if fields:\n            url += \"?\" + urlencode(fields)\n\n        return self.urlopen(method, url, **extra_kw)\n\n    def request_encode_body(\n        self,\n        method,\n        url,\n        fields=None,\n        headers=None,\n        encode_multipart=True,\n        multipart_boundary=None,\n        **urlopen_kw\n    ):\n        \"\"\"\n        Make a request using :meth:`urlopen` with the ``fields`` encoded in\n        the body. This is useful for request methods like POST, PUT, PATCH, etc.\n\n        When ``encode_multipart=True`` (default), then\n        :func:`urllib3.encode_multipart_formdata` is used to encode\n        the payload with the appropriate content type. Otherwise\n        :func:`urllib.parse.urlencode` is used with the\n        'application/x-www-form-urlencoded' content type.\n\n        Multipart encoding must be used when posting files, and it's reasonably\n        safe to use it in other times too. However, it may break request\n        signing, such as with OAuth.\n\n        Supports an optional ``fields`` parameter of key/value strings AND\n        key/filetuple. A filetuple is a (filename, data, MIME type) tuple where\n        the MIME type is optional. For example::\n\n            fields = {\n                'foo': 'bar',\n                'fakefile': ('foofile.txt', 'contents of foofile'),\n                'realfile': ('barfile.txt', open('realfile').read()),\n                'typedfile': ('bazfile.bin', open('bazfile').read(),\n                              'image/jpeg'),\n                'nonamefile': 'contents of nonamefile field',\n            }\n\n        When uploading a file, providing a filename (the first parameter of the\n        tuple) is optional but recommended to best mimic behavior of browsers.\n\n        Note that if ``headers`` are supplied, the 'Content-Type' header will\n        be overwritten because it depends on the dynamic random boundary string\n        which is used to compose the body of the request. The random boundary\n        string can be explicitly set with the ``multipart_boundary`` parameter.\n        \"\"\"\n        if headers is None:\n            headers = self.headers\n\n        extra_kw = {\"headers\": {}}\n\n        if fields:\n            if \"body\" in urlopen_kw:\n                raise TypeError(\n                    \"request got values for both 'fields' and 'body', can only specify one.\"\n                )\n\n            if encode_multipart:\n                body, content_type = encode_multipart_formdata(\n                    fields, boundary=multipart_boundary\n                )\n            else:\n                body, content_type = (\n                    urlencode(fields),\n                    \"application/x-www-form-urlencoded\",\n                )\n\n            extra_kw[\"body\"] = body\n            extra_kw[\"headers\"] = {\"Content-Type\": content_type}\n\n        extra_kw[\"headers\"].update(headers)\n        extra_kw.update(urlopen_kw)\n\n        return self.urlopen(method, url, **extra_kw)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/response.py",
    "content": "from __future__ import absolute_import\n\nimport io\nimport logging\nimport zlib\nfrom contextlib import contextmanager\nfrom socket import error as SocketError\nfrom socket import timeout as SocketTimeout\n\ntry:\n    import brotli\nexcept ImportError:\n    brotli = None\n\nfrom ._collections import HTTPHeaderDict\nfrom .connection import BaseSSLError, HTTPException\nfrom .exceptions import (\n    BodyNotHttplibCompatible,\n    DecodeError,\n    HTTPError,\n    IncompleteRead,\n    InvalidChunkLength,\n    InvalidHeader,\n    ProtocolError,\n    ReadTimeoutError,\n    ResponseNotChunked,\n    SSLError,\n)\nfrom .packages import six\nfrom .util.response import is_fp_closed, is_response_to_head\n\nlog = logging.getLogger(__name__)\n\n\nclass DeflateDecoder(object):\n    def __init__(self):\n        self._first_try = True\n        self._data = b\"\"\n        self._obj = zlib.decompressobj()\n\n    def __getattr__(self, name):\n        return getattr(self._obj, name)\n\n    def decompress(self, data):\n        if not data:\n            return data\n\n        if not self._first_try:\n            return self._obj.decompress(data)\n\n        self._data += data\n        try:\n            decompressed = self._obj.decompress(data)\n            if decompressed:\n                self._first_try = False\n                self._data = None\n            return decompressed\n        except zlib.error:\n            self._first_try = False\n            self._obj = zlib.decompressobj(-zlib.MAX_WBITS)\n            try:\n                return self.decompress(self._data)\n            finally:\n                self._data = None\n\n\nclass GzipDecoderState(object):\n\n    FIRST_MEMBER = 0\n    OTHER_MEMBERS = 1\n    SWALLOW_DATA = 2\n\n\nclass GzipDecoder(object):\n    def __init__(self):\n        self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)\n        self._state = GzipDecoderState.FIRST_MEMBER\n\n    def __getattr__(self, name):\n        return getattr(self._obj, name)\n\n    def decompress(self, data):\n        ret = bytearray()\n        if self._state == GzipDecoderState.SWALLOW_DATA or not data:\n            return bytes(ret)\n        while True:\n            try:\n                ret += self._obj.decompress(data)\n            except zlib.error:\n                previous_state = self._state\n                # Ignore data after the first error\n                self._state = GzipDecoderState.SWALLOW_DATA\n                if previous_state == GzipDecoderState.OTHER_MEMBERS:\n                    # Allow trailing garbage acceptable in other gzip clients\n                    return bytes(ret)\n                raise\n            data = self._obj.unused_data\n            if not data:\n                return bytes(ret)\n            self._state = GzipDecoderState.OTHER_MEMBERS\n            self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)\n\n\nif brotli is not None:\n\n    class BrotliDecoder(object):\n        # Supports both 'brotlipy' and 'Brotli' packages\n        # since they share an import name. The top branches\n        # are for 'brotlipy' and bottom branches for 'Brotli'\n        def __init__(self):\n            self._obj = brotli.Decompressor()\n            if hasattr(self._obj, \"decompress\"):\n                self.decompress = self._obj.decompress\n            else:\n                self.decompress = self._obj.process\n\n        def flush(self):\n            if hasattr(self._obj, \"flush\"):\n                return self._obj.flush()\n            return b\"\"\n\n\nclass MultiDecoder(object):\n    \"\"\"\n    From RFC7231:\n        If one or more encodings have been applied to a representation, the\n        sender that applied the encodings MUST generate a Content-Encoding\n        header field that lists the content codings in the order in which\n        they were applied.\n    \"\"\"\n\n    def __init__(self, modes):\n        self._decoders = [_get_decoder(m.strip()) for m in modes.split(\",\")]\n\n    def flush(self):\n        return self._decoders[0].flush()\n\n    def decompress(self, data):\n        for d in reversed(self._decoders):\n            data = d.decompress(data)\n        return data\n\n\ndef _get_decoder(mode):\n    if \",\" in mode:\n        return MultiDecoder(mode)\n\n    if mode == \"gzip\":\n        return GzipDecoder()\n\n    if brotli is not None and mode == \"br\":\n        return BrotliDecoder()\n\n    return DeflateDecoder()\n\n\nclass HTTPResponse(io.IOBase):\n    \"\"\"\n    HTTP Response container.\n\n    Backwards-compatible with :class:`http.client.HTTPResponse` but the response ``body`` is\n    loaded and decoded on-demand when the ``data`` property is accessed.  This\n    class is also compatible with the Python standard library's :mod:`io`\n    module, and can hence be treated as a readable object in the context of that\n    framework.\n\n    Extra parameters for behaviour not present in :class:`http.client.HTTPResponse`:\n\n    :param preload_content:\n        If True, the response's body will be preloaded during construction.\n\n    :param decode_content:\n        If True, will attempt to decode the body based on the\n        'content-encoding' header.\n\n    :param original_response:\n        When this HTTPResponse wrapper is generated from an :class:`http.client.HTTPResponse`\n        object, it's convenient to include the original for debug purposes. It's\n        otherwise unused.\n\n    :param retries:\n        The retries contains the last :class:`~urllib3.util.retry.Retry` that\n        was used during the request.\n\n    :param enforce_content_length:\n        Enforce content length checking. Body returned by server must match\n        value of Content-Length header, if present. Otherwise, raise error.\n    \"\"\"\n\n    CONTENT_DECODERS = [\"gzip\", \"deflate\"]\n    if brotli is not None:\n        CONTENT_DECODERS += [\"br\"]\n    REDIRECT_STATUSES = [301, 302, 303, 307, 308]\n\n    def __init__(\n        self,\n        body=\"\",\n        headers=None,\n        status=0,\n        version=0,\n        reason=None,\n        strict=0,\n        preload_content=True,\n        decode_content=True,\n        original_response=None,\n        pool=None,\n        connection=None,\n        msg=None,\n        retries=None,\n        enforce_content_length=False,\n        request_method=None,\n        request_url=None,\n        auto_close=True,\n    ):\n\n        if isinstance(headers, HTTPHeaderDict):\n            self.headers = headers\n        else:\n            self.headers = HTTPHeaderDict(headers)\n        self.status = status\n        self.version = version\n        self.reason = reason\n        self.strict = strict\n        self.decode_content = decode_content\n        self.retries = retries\n        self.enforce_content_length = enforce_content_length\n        self.auto_close = auto_close\n\n        self._decoder = None\n        self._body = None\n        self._fp = None\n        self._original_response = original_response\n        self._fp_bytes_read = 0\n        self.msg = msg\n        self._request_url = request_url\n\n        if body and isinstance(body, (six.string_types, bytes)):\n            self._body = body\n\n        self._pool = pool\n        self._connection = connection\n\n        if hasattr(body, \"read\"):\n            self._fp = body\n\n        # Are we using the chunked-style of transfer encoding?\n        self.chunked = False\n        self.chunk_left = None\n        tr_enc = self.headers.get(\"transfer-encoding\", \"\").lower()\n        # Don't incur the penalty of creating a list and then discarding it\n        encodings = (enc.strip() for enc in tr_enc.split(\",\"))\n        if \"chunked\" in encodings:\n            self.chunked = True\n\n        # Determine length of response\n        self.length_remaining = self._init_length(request_method)\n\n        # If requested, preload the body.\n        if preload_content and not self._body:\n            self._body = self.read(decode_content=decode_content)\n\n    def get_redirect_location(self):\n        \"\"\"\n        Should we redirect and where to?\n\n        :returns: Truthy redirect location string if we got a redirect status\n            code and valid location. ``None`` if redirect status and no\n            location. ``False`` if not a redirect status code.\n        \"\"\"\n        if self.status in self.REDIRECT_STATUSES:\n            return self.headers.get(\"location\")\n\n        return False\n\n    def release_conn(self):\n        if not self._pool or not self._connection:\n            return\n\n        self._pool._put_conn(self._connection)\n        self._connection = None\n\n    def drain_conn(self):\n        \"\"\"\n        Read and discard any remaining HTTP response data in the response connection.\n\n        Unread data in the HTTPResponse connection blocks the connection from being released back to the pool.\n        \"\"\"\n        try:\n            self.read()\n        except (HTTPError, SocketError, BaseSSLError, HTTPException):\n            pass\n\n    @property\n    def data(self):\n        # For backwards-compat with earlier urllib3 0.4 and earlier.\n        if self._body:\n            return self._body\n\n        if self._fp:\n            return self.read(cache_content=True)\n\n    @property\n    def connection(self):\n        return self._connection\n\n    def isclosed(self):\n        return is_fp_closed(self._fp)\n\n    def tell(self):\n        \"\"\"\n        Obtain the number of bytes pulled over the wire so far. May differ from\n        the amount of content returned by :meth:``urllib3.response.HTTPResponse.read``\n        if bytes are encoded on the wire (e.g, compressed).\n        \"\"\"\n        return self._fp_bytes_read\n\n    def _init_length(self, request_method):\n        \"\"\"\n        Set initial length value for Response content if available.\n        \"\"\"\n        length = self.headers.get(\"content-length\")\n\n        if length is not None:\n            if self.chunked:\n                # This Response will fail with an IncompleteRead if it can't be\n                # received as chunked. This method falls back to attempt reading\n                # the response before raising an exception.\n                log.warning(\n                    \"Received response with both Content-Length and \"\n                    \"Transfer-Encoding set. This is expressly forbidden \"\n                    \"by RFC 7230 sec 3.3.2. Ignoring Content-Length and \"\n                    \"attempting to process response as Transfer-Encoding: \"\n                    \"chunked.\"\n                )\n                return None\n\n            try:\n                # RFC 7230 section 3.3.2 specifies multiple content lengths can\n                # be sent in a single Content-Length header\n                # (e.g. Content-Length: 42, 42). This line ensures the values\n                # are all valid ints and that as long as the `set` length is 1,\n                # all values are the same. Otherwise, the header is invalid.\n                lengths = set([int(val) for val in length.split(\",\")])\n                if len(lengths) > 1:\n                    raise InvalidHeader(\n                        \"Content-Length contained multiple \"\n                        \"unmatching values (%s)\" % length\n                    )\n                length = lengths.pop()\n            except ValueError:\n                length = None\n            else:\n                if length < 0:\n                    length = None\n\n        # Convert status to int for comparison\n        # In some cases, httplib returns a status of \"_UNKNOWN\"\n        try:\n            status = int(self.status)\n        except ValueError:\n            status = 0\n\n        # Check for responses that shouldn't include a body\n        if status in (204, 304) or 100 <= status < 200 or request_method == \"HEAD\":\n            length = 0\n\n        return length\n\n    def _init_decoder(self):\n        \"\"\"\n        Set-up the _decoder attribute if necessary.\n        \"\"\"\n        # Note: content-encoding value should be case-insensitive, per RFC 7230\n        # Section 3.2\n        content_encoding = self.headers.get(\"content-encoding\", \"\").lower()\n        if self._decoder is None:\n            if content_encoding in self.CONTENT_DECODERS:\n                self._decoder = _get_decoder(content_encoding)\n            elif \",\" in content_encoding:\n                encodings = [\n                    e.strip()\n                    for e in content_encoding.split(\",\")\n                    if e.strip() in self.CONTENT_DECODERS\n                ]\n                if len(encodings):\n                    self._decoder = _get_decoder(content_encoding)\n\n    DECODER_ERROR_CLASSES = (IOError, zlib.error)\n    if brotli is not None:\n        DECODER_ERROR_CLASSES += (brotli.error,)\n\n    def _decode(self, data, decode_content, flush_decoder):\n        \"\"\"\n        Decode the data passed in and potentially flush the decoder.\n        \"\"\"\n        if not decode_content:\n            return data\n\n        try:\n            if self._decoder:\n                data = self._decoder.decompress(data)\n        except self.DECODER_ERROR_CLASSES as e:\n            content_encoding = self.headers.get(\"content-encoding\", \"\").lower()\n            raise DecodeError(\n                \"Received response with content-encoding: %s, but \"\n                \"failed to decode it.\" % content_encoding,\n                e,\n            )\n        if flush_decoder:\n            data += self._flush_decoder()\n\n        return data\n\n    def _flush_decoder(self):\n        \"\"\"\n        Flushes the decoder. Should only be called if the decoder is actually\n        being used.\n        \"\"\"\n        if self._decoder:\n            buf = self._decoder.decompress(b\"\")\n            return buf + self._decoder.flush()\n\n        return b\"\"\n\n    @contextmanager\n    def _error_catcher(self):\n        \"\"\"\n        Catch low-level python exceptions, instead re-raising urllib3\n        variants, so that low-level exceptions are not leaked in the\n        high-level api.\n\n        On exit, release the connection back to the pool.\n        \"\"\"\n        clean_exit = False\n\n        try:\n            try:\n                yield\n\n            except SocketTimeout:\n                # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but\n                # there is yet no clean way to get at it from this context.\n                raise ReadTimeoutError(self._pool, None, \"Read timed out.\")\n\n            except BaseSSLError as e:\n                # FIXME: Is there a better way to differentiate between SSLErrors?\n                if \"read operation timed out\" not in str(e):\n                    # SSL errors related to framing/MAC get wrapped and reraised here\n                    raise SSLError(e)\n\n                raise ReadTimeoutError(self._pool, None, \"Read timed out.\")\n\n            except (HTTPException, SocketError) as e:\n                # This includes IncompleteRead.\n                raise ProtocolError(\"Connection broken: %r\" % e, e)\n\n            # If no exception is thrown, we should avoid cleaning up\n            # unnecessarily.\n            clean_exit = True\n        finally:\n            # If we didn't terminate cleanly, we need to throw away our\n            # connection.\n            if not clean_exit:\n                # The response may not be closed but we're not going to use it\n                # anymore so close it now to ensure that the connection is\n                # released back to the pool.\n                if self._original_response:\n                    self._original_response.close()\n\n                # Closing the response may not actually be sufficient to close\n                # everything, so if we have a hold of the connection close that\n                # too.\n                if self._connection:\n                    self._connection.close()\n\n            # If we hold the original response but it's closed now, we should\n            # return the connection back to the pool.\n            if self._original_response and self._original_response.isclosed():\n                self.release_conn()\n\n    def read(self, amt=None, decode_content=None, cache_content=False):\n        \"\"\"\n        Similar to :meth:`http.client.HTTPResponse.read`, but with two additional\n        parameters: ``decode_content`` and ``cache_content``.\n\n        :param amt:\n            How much of the content to read. If specified, caching is skipped\n            because it doesn't make sense to cache partial content as the full\n            response.\n\n        :param decode_content:\n            If True, will attempt to decode the body based on the\n            'content-encoding' header.\n\n        :param cache_content:\n            If True, will save the returned data such that the same result is\n            returned despite of the state of the underlying file object. This\n            is useful if you want the ``.data`` property to continue working\n            after having ``.read()`` the file object. (Overridden if ``amt`` is\n            set.)\n        \"\"\"\n        self._init_decoder()\n        if decode_content is None:\n            decode_content = self.decode_content\n\n        if self._fp is None:\n            return\n\n        flush_decoder = False\n        fp_closed = getattr(self._fp, \"closed\", False)\n\n        with self._error_catcher():\n            if amt is None:\n                # cStringIO doesn't like amt=None\n                data = self._fp.read() if not fp_closed else b\"\"\n                flush_decoder = True\n            else:\n                cache_content = False\n                data = self._fp.read(amt) if not fp_closed else b\"\"\n                if (\n                    amt != 0 and not data\n                ):  # Platform-specific: Buggy versions of Python.\n                    # Close the connection when no data is returned\n                    #\n                    # This is redundant to what httplib/http.client _should_\n                    # already do.  However, versions of python released before\n                    # December 15, 2012 (http://bugs.python.org/issue16298) do\n                    # not properly close the connection in all cases. There is\n                    # no harm in redundantly calling close.\n                    self._fp.close()\n                    flush_decoder = True\n                    if self.enforce_content_length and self.length_remaining not in (\n                        0,\n                        None,\n                    ):\n                        # This is an edge case that httplib failed to cover due\n                        # to concerns of backward compatibility. We're\n                        # addressing it here to make sure IncompleteRead is\n                        # raised during streaming, so all calls with incorrect\n                        # Content-Length are caught.\n                        raise IncompleteRead(self._fp_bytes_read, self.length_remaining)\n\n        if data:\n            self._fp_bytes_read += len(data)\n            if self.length_remaining is not None:\n                self.length_remaining -= len(data)\n\n            data = self._decode(data, decode_content, flush_decoder)\n\n            if cache_content:\n                self._body = data\n\n        return data\n\n    def stream(self, amt=2 ** 16, decode_content=None):\n        \"\"\"\n        A generator wrapper for the read() method. A call will block until\n        ``amt`` bytes have been read from the connection or until the\n        connection is closed.\n\n        :param amt:\n            How much of the content to read. The generator will return up to\n            much data per iteration, but may return less. This is particularly\n            likely when using compressed data. However, the empty string will\n            never be returned.\n\n        :param decode_content:\n            If True, will attempt to decode the body based on the\n            'content-encoding' header.\n        \"\"\"\n        if self.chunked and self.supports_chunked_reads():\n            for line in self.read_chunked(amt, decode_content=decode_content):\n                yield line\n        else:\n            while not is_fp_closed(self._fp):\n                data = self.read(amt=amt, decode_content=decode_content)\n\n                if data:\n                    yield data\n\n    @classmethod\n    def from_httplib(ResponseCls, r, **response_kw):\n        \"\"\"\n        Given an :class:`http.client.HTTPResponse` instance ``r``, return a\n        corresponding :class:`urllib3.response.HTTPResponse` object.\n\n        Remaining parameters are passed to the HTTPResponse constructor, along\n        with ``original_response=r``.\n        \"\"\"\n        headers = r.msg\n\n        if not isinstance(headers, HTTPHeaderDict):\n            if six.PY2:\n                # Python 2.7\n                headers = HTTPHeaderDict.from_httplib(headers)\n            else:\n                headers = HTTPHeaderDict(headers.items())\n\n        # HTTPResponse objects in Python 3 don't have a .strict attribute\n        strict = getattr(r, \"strict\", 0)\n        resp = ResponseCls(\n            body=r,\n            headers=headers,\n            status=r.status,\n            version=r.version,\n            reason=r.reason,\n            strict=strict,\n            original_response=r,\n            **response_kw\n        )\n        return resp\n\n    # Backwards-compatibility methods for http.client.HTTPResponse\n    def getheaders(self):\n        return self.headers\n\n    def getheader(self, name, default=None):\n        return self.headers.get(name, default)\n\n    # Backwards compatibility for http.cookiejar\n    def info(self):\n        return self.headers\n\n    # Overrides from io.IOBase\n    def close(self):\n        if not self.closed:\n            self._fp.close()\n\n        if self._connection:\n            self._connection.close()\n\n        if not self.auto_close:\n            io.IOBase.close(self)\n\n    @property\n    def closed(self):\n        if not self.auto_close:\n            return io.IOBase.closed.__get__(self)\n        elif self._fp is None:\n            return True\n        elif hasattr(self._fp, \"isclosed\"):\n            return self._fp.isclosed()\n        elif hasattr(self._fp, \"closed\"):\n            return self._fp.closed\n        else:\n            return True\n\n    def fileno(self):\n        if self._fp is None:\n            raise IOError(\"HTTPResponse has no file to get a fileno from\")\n        elif hasattr(self._fp, \"fileno\"):\n            return self._fp.fileno()\n        else:\n            raise IOError(\n                \"The file-like object this HTTPResponse is wrapped \"\n                \"around has no file descriptor\"\n            )\n\n    def flush(self):\n        if (\n            self._fp is not None\n            and hasattr(self._fp, \"flush\")\n            and not getattr(self._fp, \"closed\", False)\n        ):\n            return self._fp.flush()\n\n    def readable(self):\n        # This method is required for `io` module compatibility.\n        return True\n\n    def readinto(self, b):\n        # This method is required for `io` module compatibility.\n        temp = self.read(len(b))\n        if len(temp) == 0:\n            return 0\n        else:\n            b[: len(temp)] = temp\n            return len(temp)\n\n    def supports_chunked_reads(self):\n        \"\"\"\n        Checks if the underlying file-like object looks like a\n        :class:`http.client.HTTPResponse` object. We do this by testing for\n        the fp attribute. If it is present we assume it returns raw chunks as\n        processed by read_chunked().\n        \"\"\"\n        return hasattr(self._fp, \"fp\")\n\n    def _update_chunk_length(self):\n        # First, we'll figure out length of a chunk and then\n        # we'll try to read it from socket.\n        if self.chunk_left is not None:\n            return\n        line = self._fp.fp.readline()\n        line = line.split(b\";\", 1)[0]\n        try:\n            self.chunk_left = int(line, 16)\n        except ValueError:\n            # Invalid chunked protocol response, abort.\n            self.close()\n            raise InvalidChunkLength(self, line)\n\n    def _handle_chunk(self, amt):\n        returned_chunk = None\n        if amt is None:\n            chunk = self._fp._safe_read(self.chunk_left)\n            returned_chunk = chunk\n            self._fp._safe_read(2)  # Toss the CRLF at the end of the chunk.\n            self.chunk_left = None\n        elif amt < self.chunk_left:\n            value = self._fp._safe_read(amt)\n            self.chunk_left = self.chunk_left - amt\n            returned_chunk = value\n        elif amt == self.chunk_left:\n            value = self._fp._safe_read(amt)\n            self._fp._safe_read(2)  # Toss the CRLF at the end of the chunk.\n            self.chunk_left = None\n            returned_chunk = value\n        else:  # amt > self.chunk_left\n            returned_chunk = self._fp._safe_read(self.chunk_left)\n            self._fp._safe_read(2)  # Toss the CRLF at the end of the chunk.\n            self.chunk_left = None\n        return returned_chunk\n\n    def read_chunked(self, amt=None, decode_content=None):\n        \"\"\"\n        Similar to :meth:`HTTPResponse.read`, but with an additional\n        parameter: ``decode_content``.\n\n        :param amt:\n            How much of the content to read. If specified, caching is skipped\n            because it doesn't make sense to cache partial content as the full\n            response.\n\n        :param decode_content:\n            If True, will attempt to decode the body based on the\n            'content-encoding' header.\n        \"\"\"\n        self._init_decoder()\n        # FIXME: Rewrite this method and make it a class with a better structured logic.\n        if not self.chunked:\n            raise ResponseNotChunked(\n                \"Response is not chunked. \"\n                \"Header 'transfer-encoding: chunked' is missing.\"\n            )\n        if not self.supports_chunked_reads():\n            raise BodyNotHttplibCompatible(\n                \"Body should be http.client.HTTPResponse like. \"\n                \"It should have have an fp attribute which returns raw chunks.\"\n            )\n\n        with self._error_catcher():\n            # Don't bother reading the body of a HEAD request.\n            if self._original_response and is_response_to_head(self._original_response):\n                self._original_response.close()\n                return\n\n            # If a response is already read and closed\n            # then return immediately.\n            if self._fp.fp is None:\n                return\n\n            while True:\n                self._update_chunk_length()\n                if self.chunk_left == 0:\n                    break\n                chunk = self._handle_chunk(amt)\n                decoded = self._decode(\n                    chunk, decode_content=decode_content, flush_decoder=False\n                )\n                if decoded:\n                    yield decoded\n\n            if decode_content:\n                # On CPython and PyPy, we should never need to flush the\n                # decoder. However, on Jython we *might* need to, so\n                # lets defensively do it anyway.\n                decoded = self._flush_decoder()\n                if decoded:  # Platform-specific: Jython.\n                    yield decoded\n\n            # Chunk content ends with \\r\\n: discard it.\n            while True:\n                line = self._fp.fp.readline()\n                if not line:\n                    # Some sites may not end with '\\r\\n'.\n                    break\n                if line == b\"\\r\\n\":\n                    break\n\n            # We read everything; close the \"file\".\n            if self._original_response:\n                self._original_response.close()\n\n    def geturl(self):\n        \"\"\"\n        Returns the URL that was the source of this response.\n        If the request that generated this response redirected, this method\n        will return the final redirect location.\n        \"\"\"\n        if self.retries is not None and len(self.retries.history):\n            return self.retries.history[-1].redirect_location\n        else:\n            return self._request_url\n\n    def __iter__(self):\n        buffer = []\n        for chunk in self.stream(decode_content=True):\n            if b\"\\n\" in chunk:\n                chunk = chunk.split(b\"\\n\")\n                yield b\"\".join(buffer) + chunk[0] + b\"\\n\"\n                for x in chunk[1:-1]:\n                    yield x + b\"\\n\"\n                if chunk[-1]:\n                    buffer = [chunk[-1]]\n                else:\n                    buffer = []\n            else:\n                buffer.append(chunk)\n        if buffer:\n            yield b\"\".join(buffer)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/util/__init__.py",
    "content": "from __future__ import absolute_import\n\n# For backwards compatibility, provide imports that used to be here.\nfrom .connection import is_connection_dropped\nfrom .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers\nfrom .response import is_fp_closed\nfrom .retry import Retry\nfrom .ssl_ import (\n    ALPN_PROTOCOLS,\n    HAS_SNI,\n    IS_PYOPENSSL,\n    IS_SECURETRANSPORT,\n    PROTOCOL_TLS,\n    SSLContext,\n    assert_fingerprint,\n    resolve_cert_reqs,\n    resolve_ssl_version,\n    ssl_wrap_socket,\n)\nfrom .timeout import Timeout, current_time\nfrom .url import Url, get_host, parse_url, split_first\nfrom .wait import wait_for_read, wait_for_write\n\n__all__ = (\n    \"HAS_SNI\",\n    \"IS_PYOPENSSL\",\n    \"IS_SECURETRANSPORT\",\n    \"SSLContext\",\n    \"PROTOCOL_TLS\",\n    \"ALPN_PROTOCOLS\",\n    \"Retry\",\n    \"Timeout\",\n    \"Url\",\n    \"assert_fingerprint\",\n    \"current_time\",\n    \"is_connection_dropped\",\n    \"is_fp_closed\",\n    \"get_host\",\n    \"parse_url\",\n    \"make_headers\",\n    \"resolve_cert_reqs\",\n    \"resolve_ssl_version\",\n    \"split_first\",\n    \"ssl_wrap_socket\",\n    \"wait_for_read\",\n    \"wait_for_write\",\n    \"SKIP_HEADER\",\n    \"SKIPPABLE_HEADERS\",\n)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/util/connection.py",
    "content": "from __future__ import absolute_import\n\nimport socket\n\nfrom urllib3.exceptions import LocationParseError\n\nfrom ..contrib import _appengine_environ\nfrom ..packages import six\nfrom .wait import NoWayToWaitForSocketError, wait_for_read\n\n\ndef is_connection_dropped(conn):  # Platform-specific\n    \"\"\"\n    Returns True if the connection is dropped and should be closed.\n\n    :param conn:\n        :class:`http.client.HTTPConnection` object.\n\n    Note: For platforms like AppEngine, this will always return ``False`` to\n    let the platform handle connection recycling transparently for us.\n    \"\"\"\n    sock = getattr(conn, \"sock\", False)\n    if sock is False:  # Platform-specific: AppEngine\n        return False\n    if sock is None:  # Connection already closed (such as by httplib).\n        return True\n    try:\n        # Returns True if readable, which here means it's been dropped\n        return wait_for_read(sock, timeout=0.0)\n    except NoWayToWaitForSocketError:  # Platform-specific: AppEngine\n        return False\n\n\n# This function is copied from socket.py in the Python 2.7 standard\n# library test suite. Added to its signature is only `socket_options`.\n# One additional modification is that we avoid binding to IPv6 servers\n# discovered in DNS if the system doesn't have IPv6 functionality.\ndef create_connection(\n    address,\n    timeout=socket._GLOBAL_DEFAULT_TIMEOUT,\n    source_address=None,\n    socket_options=None,\n):\n    \"\"\"Connect to *address* and return the socket object.\n\n    Convenience function.  Connect to *address* (a 2-tuple ``(host,\n    port)``) and return the socket object.  Passing the optional\n    *timeout* parameter will set the timeout on the socket instance\n    before attempting to connect.  If no *timeout* is supplied, the\n    global default timeout setting returned by :func:`socket.getdefaulttimeout`\n    is used.  If *source_address* is set it must be a tuple of (host, port)\n    for the socket to bind as a source address before making the connection.\n    An host of '' or port 0 tells the OS to use the default.\n    \"\"\"\n\n    host, port = address\n    if host.startswith(\"[\"):\n        host = host.strip(\"[]\")\n    err = None\n\n    # Using the value from allowed_gai_family() in the context of getaddrinfo lets\n    # us select whether to work with IPv4 DNS records, IPv6 records, or both.\n    # The original create_connection function always returns all records.\n    family = allowed_gai_family()\n\n    try:\n        host.encode(\"idna\")\n    except UnicodeError:\n        return six.raise_from(\n            LocationParseError(u\"'%s', label empty or too long\" % host), None\n        )\n\n    for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):\n        af, socktype, proto, canonname, sa = res\n        sock = None\n        try:\n            sock = socket.socket(af, socktype, proto)\n\n            # If provided, set socket level options before connecting.\n            _set_socket_options(sock, socket_options)\n\n            if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:\n                sock.settimeout(timeout)\n            if source_address:\n                sock.bind(source_address)\n            sock.connect(sa)\n            return sock\n\n        except socket.error as e:\n            err = e\n            if sock is not None:\n                sock.close()\n                sock = None\n\n    if err is not None:\n        raise err\n\n    raise socket.error(\"getaddrinfo returns an empty list\")\n\n\ndef _set_socket_options(sock, options):\n    if options is None:\n        return\n\n    for opt in options:\n        sock.setsockopt(*opt)\n\n\ndef allowed_gai_family():\n    \"\"\"This function is designed to work in the context of\n    getaddrinfo, where family=socket.AF_UNSPEC is the default and\n    will perform a DNS search for both IPv6 and IPv4 records.\"\"\"\n\n    family = socket.AF_INET\n    if HAS_IPV6:\n        family = socket.AF_UNSPEC\n    return family\n\n\ndef _has_ipv6(host):\n    \"\"\"Returns True if the system can bind an IPv6 address.\"\"\"\n    sock = None\n    has_ipv6 = False\n\n    # App Engine doesn't support IPV6 sockets and actually has a quota on the\n    # number of sockets that can be used, so just early out here instead of\n    # creating a socket needlessly.\n    # See https://github.com/urllib3/urllib3/issues/1446\n    if _appengine_environ.is_appengine_sandbox():\n        return False\n\n    if socket.has_ipv6:\n        # has_ipv6 returns true if cPython was compiled with IPv6 support.\n        # It does not tell us if the system has IPv6 support enabled. To\n        # determine that we must bind to an IPv6 address.\n        # https://github.com/urllib3/urllib3/pull/611\n        # https://bugs.python.org/issue658327\n        try:\n            sock = socket.socket(socket.AF_INET6)\n            sock.bind((host, 0))\n            has_ipv6 = True\n        except Exception:\n            pass\n\n    if sock:\n        sock.close()\n    return has_ipv6\n\n\nHAS_IPV6 = _has_ipv6(\"::1\")\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/util/proxy.py",
    "content": "from .ssl_ import create_urllib3_context, resolve_cert_reqs, resolve_ssl_version\n\n\ndef connection_requires_http_tunnel(\n    proxy_url=None, proxy_config=None, destination_scheme=None\n):\n    \"\"\"\n    Returns True if the connection requires an HTTP CONNECT through the proxy.\n\n    :param URL proxy_url:\n        URL of the proxy.\n    :param ProxyConfig proxy_config:\n        Proxy configuration from poolmanager.py\n    :param str destination_scheme:\n        The scheme of the destination. (i.e https, http, etc)\n    \"\"\"\n    # If we're not using a proxy, no way to use a tunnel.\n    if proxy_url is None:\n        return False\n\n    # HTTP destinations never require tunneling, we always forward.\n    if destination_scheme == \"http\":\n        return False\n\n    # Support for forwarding with HTTPS proxies and HTTPS destinations.\n    if (\n        proxy_url.scheme == \"https\"\n        and proxy_config\n        and proxy_config.use_forwarding_for_https\n    ):\n        return False\n\n    # Otherwise always use a tunnel.\n    return True\n\n\ndef create_proxy_ssl_context(\n    ssl_version, cert_reqs, ca_certs=None, ca_cert_dir=None, ca_cert_data=None\n):\n    \"\"\"\n    Generates a default proxy ssl context if one hasn't been provided by the\n    user.\n    \"\"\"\n    ssl_context = create_urllib3_context(\n        ssl_version=resolve_ssl_version(ssl_version),\n        cert_reqs=resolve_cert_reqs(cert_reqs),\n    )\n    if (\n        not ca_certs\n        and not ca_cert_dir\n        and not ca_cert_data\n        and hasattr(ssl_context, \"load_default_certs\")\n    ):\n        ssl_context.load_default_certs()\n\n    return ssl_context\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/util/queue.py",
    "content": "import collections\n\nfrom ..packages import six\nfrom ..packages.six.moves import queue\n\nif six.PY2:\n    # Queue is imported for side effects on MS Windows. See issue #229.\n    import Queue as _unused_module_Queue  # noqa: F401\n\n\nclass LifoQueue(queue.Queue):\n    def _init(self, _):\n        self.queue = collections.deque()\n\n    def _qsize(self, len=len):\n        return len(self.queue)\n\n    def _put(self, item):\n        self.queue.append(item)\n\n    def _get(self):\n        return self.queue.pop()\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/util/request.py",
    "content": "from __future__ import absolute_import\n\nfrom base64 import b64encode\n\nfrom ..exceptions import UnrewindableBodyError\nfrom ..packages.six import b, integer_types\n\n# Pass as a value within ``headers`` to skip\n# emitting some HTTP headers that are added automatically.\n# The only headers that are supported are ``Accept-Encoding``,\n# ``Host``, and ``User-Agent``.\nSKIP_HEADER = \"@@@SKIP_HEADER@@@\"\nSKIPPABLE_HEADERS = frozenset([\"accept-encoding\", \"host\", \"user-agent\"])\n\nACCEPT_ENCODING = \"gzip,deflate\"\ntry:\n    import brotli as _unused_module_brotli  # noqa: F401\nexcept ImportError:\n    pass\nelse:\n    ACCEPT_ENCODING += \",br\"\n\n_FAILEDTELL = object()\n\n\ndef make_headers(\n    keep_alive=None,\n    accept_encoding=None,\n    user_agent=None,\n    basic_auth=None,\n    proxy_basic_auth=None,\n    disable_cache=None,\n):\n    \"\"\"\n    Shortcuts for generating request headers.\n\n    :param keep_alive:\n        If ``True``, adds 'connection: keep-alive' header.\n\n    :param accept_encoding:\n        Can be a boolean, list, or string.\n        ``True`` translates to 'gzip,deflate'.\n        List will get joined by comma.\n        String will be used as provided.\n\n    :param user_agent:\n        String representing the user-agent you want, such as\n        \"python-urllib3/0.6\"\n\n    :param basic_auth:\n        Colon-separated username:password string for 'authorization: basic ...'\n        auth header.\n\n    :param proxy_basic_auth:\n        Colon-separated username:password string for 'proxy-authorization: basic ...'\n        auth header.\n\n    :param disable_cache:\n        If ``True``, adds 'cache-control: no-cache' header.\n\n    Example::\n\n        >>> make_headers(keep_alive=True, user_agent=\"Batman/1.0\")\n        {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'}\n        >>> make_headers(accept_encoding=True)\n        {'accept-encoding': 'gzip,deflate'}\n    \"\"\"\n    headers = {}\n    if accept_encoding:\n        if isinstance(accept_encoding, str):\n            pass\n        elif isinstance(accept_encoding, list):\n            accept_encoding = \",\".join(accept_encoding)\n        else:\n            accept_encoding = ACCEPT_ENCODING\n        headers[\"accept-encoding\"] = accept_encoding\n\n    if user_agent:\n        headers[\"user-agent\"] = user_agent\n\n    if keep_alive:\n        headers[\"connection\"] = \"keep-alive\"\n\n    if basic_auth:\n        headers[\"authorization\"] = \"Basic \" + b64encode(b(basic_auth)).decode(\"utf-8\")\n\n    if proxy_basic_auth:\n        headers[\"proxy-authorization\"] = \"Basic \" + b64encode(\n            b(proxy_basic_auth)\n        ).decode(\"utf-8\")\n\n    if disable_cache:\n        headers[\"cache-control\"] = \"no-cache\"\n\n    return headers\n\n\ndef set_file_position(body, pos):\n    \"\"\"\n    If a position is provided, move file to that point.\n    Otherwise, we'll attempt to record a position for future use.\n    \"\"\"\n    if pos is not None:\n        rewind_body(body, pos)\n    elif getattr(body, \"tell\", None) is not None:\n        try:\n            pos = body.tell()\n        except (IOError, OSError):\n            # This differentiates from None, allowing us to catch\n            # a failed `tell()` later when trying to rewind the body.\n            pos = _FAILEDTELL\n\n    return pos\n\n\ndef rewind_body(body, body_pos):\n    \"\"\"\n    Attempt to rewind body to a certain position.\n    Primarily used for request redirects and retries.\n\n    :param body:\n        File-like object that supports seek.\n\n    :param int pos:\n        Position to seek to in file.\n    \"\"\"\n    body_seek = getattr(body, \"seek\", None)\n    if body_seek is not None and isinstance(body_pos, integer_types):\n        try:\n            body_seek(body_pos)\n        except (IOError, OSError):\n            raise UnrewindableBodyError(\n                \"An error occurred when rewinding request body for redirect/retry.\"\n            )\n    elif body_pos is _FAILEDTELL:\n        raise UnrewindableBodyError(\n            \"Unable to record file position for rewinding \"\n            \"request body during a redirect/retry.\"\n        )\n    else:\n        raise ValueError(\n            \"body_pos must be of type integer, instead it was %s.\" % type(body_pos)\n        )\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/util/response.py",
    "content": "from __future__ import absolute_import\n\nfrom email.errors import MultipartInvariantViolationDefect, StartBoundaryNotFoundDefect\n\nfrom ..exceptions import HeaderParsingError\nfrom ..packages.six.moves import http_client as httplib\n\n\ndef is_fp_closed(obj):\n    \"\"\"\n    Checks whether a given file-like object is closed.\n\n    :param obj:\n        The file-like object to check.\n    \"\"\"\n\n    try:\n        # Check `isclosed()` first, in case Python3 doesn't set `closed`.\n        # GH Issue #928\n        return obj.isclosed()\n    except AttributeError:\n        pass\n\n    try:\n        # Check via the official file-like-object way.\n        return obj.closed\n    except AttributeError:\n        pass\n\n    try:\n        # Check if the object is a container for another file-like object that\n        # gets released on exhaustion (e.g. HTTPResponse).\n        return obj.fp is None\n    except AttributeError:\n        pass\n\n    raise ValueError(\"Unable to determine whether fp is closed.\")\n\n\ndef assert_header_parsing(headers):\n    \"\"\"\n    Asserts whether all headers have been successfully parsed.\n    Extracts encountered errors from the result of parsing headers.\n\n    Only works on Python 3.\n\n    :param http.client.HTTPMessage headers: Headers to verify.\n\n    :raises urllib3.exceptions.HeaderParsingError:\n        If parsing errors are found.\n    \"\"\"\n\n    # This will fail silently if we pass in the wrong kind of parameter.\n    # To make debugging easier add an explicit check.\n    if not isinstance(headers, httplib.HTTPMessage):\n        raise TypeError(\"expected httplib.Message, got {0}.\".format(type(headers)))\n\n    defects = getattr(headers, \"defects\", None)\n    get_payload = getattr(headers, \"get_payload\", None)\n\n    unparsed_data = None\n    if get_payload:\n        # get_payload is actually email.message.Message.get_payload;\n        # we're only interested in the result if it's not a multipart message\n        if not headers.is_multipart():\n            payload = get_payload()\n\n            if isinstance(payload, (bytes, str)):\n                unparsed_data = payload\n    if defects:\n        # httplib is assuming a response body is available\n        # when parsing headers even when httplib only sends\n        # header data to parse_headers() This results in\n        # defects on multipart responses in particular.\n        # See: https://github.com/urllib3/urllib3/issues/800\n\n        # So we ignore the following defects:\n        # - StartBoundaryNotFoundDefect:\n        #     The claimed start boundary was never found.\n        # - MultipartInvariantViolationDefect:\n        #     A message claimed to be a multipart but no subparts were found.\n        defects = [\n            defect\n            for defect in defects\n            if not isinstance(\n                defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect)\n            )\n        ]\n\n    if defects or unparsed_data:\n        raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data)\n\n\ndef is_response_to_head(response):\n    \"\"\"\n    Checks whether the request of a response has been a HEAD-request.\n    Handles the quirks of AppEngine.\n\n    :param http.client.HTTPResponse response:\n        Response to check if the originating request\n        used 'HEAD' as a method.\n    \"\"\"\n    # FIXME: Can we do this somehow without accessing private httplib _method?\n    method = response._method\n    if isinstance(method, int):  # Platform-specific: Appengine\n        return method == 3\n    return method.upper() == \"HEAD\"\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/util/retry.py",
    "content": "from __future__ import absolute_import\n\nimport email\nimport logging\nimport re\nimport time\nimport warnings\nfrom collections import namedtuple\nfrom itertools import takewhile\n\nfrom ..exceptions import (\n    ConnectTimeoutError,\n    InvalidHeader,\n    MaxRetryError,\n    ProtocolError,\n    ProxyError,\n    ReadTimeoutError,\n    ResponseError,\n)\nfrom ..packages import six\n\nlog = logging.getLogger(__name__)\n\n\n# Data structure for representing the metadata of requests that result in a retry.\nRequestHistory = namedtuple(\n    \"RequestHistory\", [\"method\", \"url\", \"error\", \"status\", \"redirect_location\"]\n)\n\n\n# TODO: In v2 we can remove this sentinel and metaclass with deprecated options.\n_Default = object()\n\n\nclass _RetryMeta(type):\n    @property\n    def DEFAULT_METHOD_WHITELIST(cls):\n        warnings.warn(\n            \"Using 'Retry.DEFAULT_METHOD_WHITELIST' is deprecated and \"\n            \"will be removed in v2.0. Use 'Retry.DEFAULT_ALLOWED_METHODS' instead\",\n            DeprecationWarning,\n        )\n        return cls.DEFAULT_ALLOWED_METHODS\n\n    @DEFAULT_METHOD_WHITELIST.setter\n    def DEFAULT_METHOD_WHITELIST(cls, value):\n        warnings.warn(\n            \"Using 'Retry.DEFAULT_METHOD_WHITELIST' is deprecated and \"\n            \"will be removed in v2.0. Use 'Retry.DEFAULT_ALLOWED_METHODS' instead\",\n            DeprecationWarning,\n        )\n        cls.DEFAULT_ALLOWED_METHODS = value\n\n    @property\n    def DEFAULT_REDIRECT_HEADERS_BLACKLIST(cls):\n        warnings.warn(\n            \"Using 'Retry.DEFAULT_REDIRECT_HEADERS_BLACKLIST' is deprecated and \"\n            \"will be removed in v2.0. Use 'Retry.DEFAULT_REMOVE_HEADERS_ON_REDIRECT' instead\",\n            DeprecationWarning,\n        )\n        return cls.DEFAULT_REMOVE_HEADERS_ON_REDIRECT\n\n    @DEFAULT_REDIRECT_HEADERS_BLACKLIST.setter\n    def DEFAULT_REDIRECT_HEADERS_BLACKLIST(cls, value):\n        warnings.warn(\n            \"Using 'Retry.DEFAULT_REDIRECT_HEADERS_BLACKLIST' is deprecated and \"\n            \"will be removed in v2.0. Use 'Retry.DEFAULT_REMOVE_HEADERS_ON_REDIRECT' instead\",\n            DeprecationWarning,\n        )\n        cls.DEFAULT_REMOVE_HEADERS_ON_REDIRECT = value\n\n\n@six.add_metaclass(_RetryMeta)\nclass Retry(object):\n    \"\"\"Retry configuration.\n\n    Each retry attempt will create a new Retry object with updated values, so\n    they can be safely reused.\n\n    Retries can be defined as a default for a pool::\n\n        retries = Retry(connect=5, read=2, redirect=5)\n        http = PoolManager(retries=retries)\n        response = http.request('GET', 'http://example.com/')\n\n    Or per-request (which overrides the default for the pool)::\n\n        response = http.request('GET', 'http://example.com/', retries=Retry(10))\n\n    Retries can be disabled by passing ``False``::\n\n        response = http.request('GET', 'http://example.com/', retries=False)\n\n    Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless\n    retries are disabled, in which case the causing exception will be raised.\n\n    :param int total:\n        Total number of retries to allow. Takes precedence over other counts.\n\n        Set to ``None`` to remove this constraint and fall back on other\n        counts.\n\n        Set to ``0`` to fail on the first retry.\n\n        Set to ``False`` to disable and imply ``raise_on_redirect=False``.\n\n    :param int connect:\n        How many connection-related errors to retry on.\n\n        These are errors raised before the request is sent to the remote server,\n        which we assume has not triggered the server to process the request.\n\n        Set to ``0`` to fail on the first retry of this type.\n\n    :param int read:\n        How many times to retry on read errors.\n\n        These errors are raised after the request was sent to the server, so the\n        request may have side-effects.\n\n        Set to ``0`` to fail on the first retry of this type.\n\n    :param int redirect:\n        How many redirects to perform. Limit this to avoid infinite redirect\n        loops.\n\n        A redirect is a HTTP response with a status code 301, 302, 303, 307 or\n        308.\n\n        Set to ``0`` to fail on the first retry of this type.\n\n        Set to ``False`` to disable and imply ``raise_on_redirect=False``.\n\n    :param int status:\n        How many times to retry on bad status codes.\n\n        These are retries made on responses, where status code matches\n        ``status_forcelist``.\n\n        Set to ``0`` to fail on the first retry of this type.\n\n    :param int other:\n        How many times to retry on other errors.\n\n        Other errors are errors that are not connect, read, redirect or status errors.\n        These errors might be raised after the request was sent to the server, so the\n        request might have side-effects.\n\n        Set to ``0`` to fail on the first retry of this type.\n\n        If ``total`` is not set, it's a good idea to set this to 0 to account\n        for unexpected edge cases and avoid infinite retry loops.\n\n    :param iterable allowed_methods:\n        Set of uppercased HTTP method verbs that we should retry on.\n\n        By default, we only retry on methods which are considered to be\n        idempotent (multiple requests with the same parameters end with the\n        same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`.\n\n        Set to a ``False`` value to retry on any verb.\n\n        .. warning::\n\n            Previously this parameter was named ``method_whitelist``, that\n            usage is deprecated in v1.26.0 and will be removed in v2.0.\n\n    :param iterable status_forcelist:\n        A set of integer HTTP status codes that we should force a retry on.\n        A retry is initiated if the request method is in ``allowed_methods``\n        and the response status code is in ``status_forcelist``.\n\n        By default, this is disabled with ``None``.\n\n    :param float backoff_factor:\n        A backoff factor to apply between attempts after the second try\n        (most errors are resolved immediately by a second try without a\n        delay). urllib3 will sleep for::\n\n            {backoff factor} * (2 ** ({number of total retries} - 1))\n\n        seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep\n        for [0.0s, 0.2s, 0.4s, ...] between retries. It will never be longer\n        than :attr:`Retry.BACKOFF_MAX`.\n\n        By default, backoff is disabled (set to 0).\n\n    :param bool raise_on_redirect: Whether, if the number of redirects is\n        exhausted, to raise a MaxRetryError, or to return a response with a\n        response code in the 3xx range.\n\n    :param bool raise_on_status: Similar meaning to ``raise_on_redirect``:\n        whether we should raise an exception, or return a response,\n        if status falls in ``status_forcelist`` range and retries have\n        been exhausted.\n\n    :param tuple history: The history of the request encountered during\n        each call to :meth:`~Retry.increment`. The list is in the order\n        the requests occurred. Each list item is of class :class:`RequestHistory`.\n\n    :param bool respect_retry_after_header:\n        Whether to respect Retry-After header on status codes defined as\n        :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not.\n\n    :param iterable remove_headers_on_redirect:\n        Sequence of headers to remove from the request when a response\n        indicating a redirect is returned before firing off the redirected\n        request.\n    \"\"\"\n\n    #: Default methods to be used for ``allowed_methods``\n    DEFAULT_ALLOWED_METHODS = frozenset(\n        [\"HEAD\", \"GET\", \"PUT\", \"DELETE\", \"OPTIONS\", \"TRACE\"]\n    )\n\n    #: Default status codes to be used for ``status_forcelist``\n    RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503])\n\n    #: Default headers to be used for ``remove_headers_on_redirect``\n    DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset([\"Authorization\"])\n\n    #: Maximum backoff time.\n    BACKOFF_MAX = 120\n\n    def __init__(\n        self,\n        total=10,\n        connect=None,\n        read=None,\n        redirect=None,\n        status=None,\n        other=None,\n        allowed_methods=_Default,\n        status_forcelist=None,\n        backoff_factor=0,\n        raise_on_redirect=True,\n        raise_on_status=True,\n        history=None,\n        respect_retry_after_header=True,\n        remove_headers_on_redirect=_Default,\n        # TODO: Deprecated, remove in v2.0\n        method_whitelist=_Default,\n    ):\n\n        if method_whitelist is not _Default:\n            if allowed_methods is not _Default:\n                raise ValueError(\n                    \"Using both 'allowed_methods' and \"\n                    \"'method_whitelist' together is not allowed. \"\n                    \"Instead only use 'allowed_methods'\"\n                )\n            warnings.warn(\n                \"Using 'method_whitelist' with Retry is deprecated and \"\n                \"will be removed in v2.0. Use 'allowed_methods' instead\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n            allowed_methods = method_whitelist\n        if allowed_methods is _Default:\n            allowed_methods = self.DEFAULT_ALLOWED_METHODS\n        if remove_headers_on_redirect is _Default:\n            remove_headers_on_redirect = self.DEFAULT_REMOVE_HEADERS_ON_REDIRECT\n\n        self.total = total\n        self.connect = connect\n        self.read = read\n        self.status = status\n        self.other = other\n\n        if redirect is False or total is False:\n            redirect = 0\n            raise_on_redirect = False\n\n        self.redirect = redirect\n        self.status_forcelist = status_forcelist or set()\n        self.allowed_methods = allowed_methods\n        self.backoff_factor = backoff_factor\n        self.raise_on_redirect = raise_on_redirect\n        self.raise_on_status = raise_on_status\n        self.history = history or tuple()\n        self.respect_retry_after_header = respect_retry_after_header\n        self.remove_headers_on_redirect = frozenset(\n            [h.lower() for h in remove_headers_on_redirect]\n        )\n\n    def new(self, **kw):\n        params = dict(\n            total=self.total,\n            connect=self.connect,\n            read=self.read,\n            redirect=self.redirect,\n            status=self.status,\n            other=self.other,\n            status_forcelist=self.status_forcelist,\n            backoff_factor=self.backoff_factor,\n            raise_on_redirect=self.raise_on_redirect,\n            raise_on_status=self.raise_on_status,\n            history=self.history,\n            remove_headers_on_redirect=self.remove_headers_on_redirect,\n            respect_retry_after_header=self.respect_retry_after_header,\n        )\n\n        # TODO: If already given in **kw we use what's given to us\n        # If not given we need to figure out what to pass. We decide\n        # based on whether our class has the 'method_whitelist' property\n        # and if so we pass the deprecated 'method_whitelist' otherwise\n        # we use 'allowed_methods'. Remove in v2.0\n        if \"method_whitelist\" not in kw and \"allowed_methods\" not in kw:\n            if \"method_whitelist\" in self.__dict__:\n                warnings.warn(\n                    \"Using 'method_whitelist' with Retry is deprecated and \"\n                    \"will be removed in v2.0. Use 'allowed_methods' instead\",\n                    DeprecationWarning,\n                )\n                params[\"method_whitelist\"] = self.allowed_methods\n            else:\n                params[\"allowed_methods\"] = self.allowed_methods\n\n        params.update(kw)\n        return type(self)(**params)\n\n    @classmethod\n    def from_int(cls, retries, redirect=True, default=None):\n        \"\"\"Backwards-compatibility for the old retries format.\"\"\"\n        if retries is None:\n            retries = default if default is not None else cls.DEFAULT\n\n        if isinstance(retries, Retry):\n            return retries\n\n        redirect = bool(redirect) and None\n        new_retries = cls(retries, redirect=redirect)\n        log.debug(\"Converted retries value: %r -> %r\", retries, new_retries)\n        return new_retries\n\n    def get_backoff_time(self):\n        \"\"\"Formula for computing the current backoff\n\n        :rtype: float\n        \"\"\"\n        # We want to consider only the last consecutive errors sequence (Ignore redirects).\n        consecutive_errors_len = len(\n            list(\n                takewhile(lambda x: x.redirect_location is None, reversed(self.history))\n            )\n        )\n        if consecutive_errors_len <= 1:\n            return 0\n\n        backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1))\n        return min(self.BACKOFF_MAX, backoff_value)\n\n    def parse_retry_after(self, retry_after):\n        # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4\n        if re.match(r\"^\\s*[0-9]+\\s*$\", retry_after):\n            seconds = int(retry_after)\n        else:\n            retry_date_tuple = email.utils.parsedate_tz(retry_after)\n            if retry_date_tuple is None:\n                raise InvalidHeader(\"Invalid Retry-After header: %s\" % retry_after)\n            if retry_date_tuple[9] is None:  # Python 2\n                # Assume UTC if no timezone was specified\n                # On Python2.7, parsedate_tz returns None for a timezone offset\n                # instead of 0 if no timezone is given, where mktime_tz treats\n                # a None timezone offset as local time.\n                retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:]\n\n            retry_date = email.utils.mktime_tz(retry_date_tuple)\n            seconds = retry_date - time.time()\n\n        if seconds < 0:\n            seconds = 0\n\n        return seconds\n\n    def get_retry_after(self, response):\n        \"\"\"Get the value of Retry-After in seconds.\"\"\"\n\n        retry_after = response.getheader(\"Retry-After\")\n\n        if retry_after is None:\n            return None\n\n        return self.parse_retry_after(retry_after)\n\n    def sleep_for_retry(self, response=None):\n        retry_after = self.get_retry_after(response)\n        if retry_after:\n            time.sleep(retry_after)\n            return True\n\n        return False\n\n    def _sleep_backoff(self):\n        backoff = self.get_backoff_time()\n        if backoff <= 0:\n            return\n        time.sleep(backoff)\n\n    def sleep(self, response=None):\n        \"\"\"Sleep between retry attempts.\n\n        This method will respect a server's ``Retry-After`` response header\n        and sleep the duration of the time requested. If that is not present, it\n        will use an exponential backoff. By default, the backoff factor is 0 and\n        this method will return immediately.\n        \"\"\"\n\n        if self.respect_retry_after_header and response:\n            slept = self.sleep_for_retry(response)\n            if slept:\n                return\n\n        self._sleep_backoff()\n\n    def _is_connection_error(self, err):\n        \"\"\"Errors when we're fairly sure that the server did not receive the\n        request, so it should be safe to retry.\n        \"\"\"\n        if isinstance(err, ProxyError):\n            err = err.original_error\n        return isinstance(err, ConnectTimeoutError)\n\n    def _is_read_error(self, err):\n        \"\"\"Errors that occur after the request has been started, so we should\n        assume that the server began processing it.\n        \"\"\"\n        return isinstance(err, (ReadTimeoutError, ProtocolError))\n\n    def _is_method_retryable(self, method):\n        \"\"\"Checks if a given HTTP method should be retried upon, depending if\n        it is included in the allowed_methods\n        \"\"\"\n        # TODO: For now favor if the Retry implementation sets its own method_whitelist\n        # property outside of our constructor to avoid breaking custom implementations.\n        if \"method_whitelist\" in self.__dict__:\n            warnings.warn(\n                \"Using 'method_whitelist' with Retry is deprecated and \"\n                \"will be removed in v2.0. Use 'allowed_methods' instead\",\n                DeprecationWarning,\n            )\n            allowed_methods = self.method_whitelist\n        else:\n            allowed_methods = self.allowed_methods\n\n        if allowed_methods and method.upper() not in allowed_methods:\n            return False\n        return True\n\n    def is_retry(self, method, status_code, has_retry_after=False):\n        \"\"\"Is this method/status code retryable? (Based on allowlists and control\n        variables such as the number of total retries to allow, whether to\n        respect the Retry-After header, whether this header is present, and\n        whether the returned status code is on the list of status codes to\n        be retried upon on the presence of the aforementioned header)\n        \"\"\"\n        if not self._is_method_retryable(method):\n            return False\n\n        if self.status_forcelist and status_code in self.status_forcelist:\n            return True\n\n        return (\n            self.total\n            and self.respect_retry_after_header\n            and has_retry_after\n            and (status_code in self.RETRY_AFTER_STATUS_CODES)\n        )\n\n    def is_exhausted(self):\n        \"\"\"Are we out of retries?\"\"\"\n        retry_counts = (\n            self.total,\n            self.connect,\n            self.read,\n            self.redirect,\n            self.status,\n            self.other,\n        )\n        retry_counts = list(filter(None, retry_counts))\n        if not retry_counts:\n            return False\n\n        return min(retry_counts) < 0\n\n    def increment(\n        self,\n        method=None,\n        url=None,\n        response=None,\n        error=None,\n        _pool=None,\n        _stacktrace=None,\n    ):\n        \"\"\"Return a new Retry object with incremented retry counters.\n\n        :param response: A response object, or None, if the server did not\n            return a response.\n        :type response: :class:`~urllib3.response.HTTPResponse`\n        :param Exception error: An error encountered during the request, or\n            None if the response was received successfully.\n\n        :return: A new ``Retry`` object.\n        \"\"\"\n        if self.total is False and error:\n            # Disabled, indicate to re-raise the error.\n            raise six.reraise(type(error), error, _stacktrace)\n\n        total = self.total\n        if total is not None:\n            total -= 1\n\n        connect = self.connect\n        read = self.read\n        redirect = self.redirect\n        status_count = self.status\n        other = self.other\n        cause = \"unknown\"\n        status = None\n        redirect_location = None\n\n        if error and self._is_connection_error(error):\n            # Connect retry?\n            if connect is False:\n                raise six.reraise(type(error), error, _stacktrace)\n            elif connect is not None:\n                connect -= 1\n\n        elif error and self._is_read_error(error):\n            # Read retry?\n            if read is False or not self._is_method_retryable(method):\n                raise six.reraise(type(error), error, _stacktrace)\n            elif read is not None:\n                read -= 1\n\n        elif error:\n            # Other retry?\n            if other is not None:\n                other -= 1\n\n        elif response and response.get_redirect_location():\n            # Redirect retry?\n            if redirect is not None:\n                redirect -= 1\n            cause = \"too many redirects\"\n            redirect_location = response.get_redirect_location()\n            status = response.status\n\n        else:\n            # Incrementing because of a server error like a 500 in\n            # status_forcelist and the given method is in the allowed_methods\n            cause = ResponseError.GENERIC_ERROR\n            if response and response.status:\n                if status_count is not None:\n                    status_count -= 1\n                cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status)\n                status = response.status\n\n        history = self.history + (\n            RequestHistory(method, url, error, status, redirect_location),\n        )\n\n        new_retry = self.new(\n            total=total,\n            connect=connect,\n            read=read,\n            redirect=redirect,\n            status=status_count,\n            other=other,\n            history=history,\n        )\n\n        if new_retry.is_exhausted():\n            raise MaxRetryError(_pool, url, error or ResponseError(cause))\n\n        log.debug(\"Incremented Retry for (url='%s'): %r\", url, new_retry)\n\n        return new_retry\n\n    def __repr__(self):\n        return (\n            \"{cls.__name__}(total={self.total}, connect={self.connect}, \"\n            \"read={self.read}, redirect={self.redirect}, status={self.status})\"\n        ).format(cls=type(self), self=self)\n\n    def __getattr__(self, item):\n        if item == \"method_whitelist\":\n            # TODO: Remove this deprecated alias in v2.0\n            warnings.warn(\n                \"Using 'method_whitelist' with Retry is deprecated and \"\n                \"will be removed in v2.0. Use 'allowed_methods' instead\",\n                DeprecationWarning,\n            )\n            return self.allowed_methods\n        try:\n            return getattr(super(Retry, self), item)\n        except AttributeError:\n            return getattr(Retry, item)\n\n\n# For backwards compatibility (equivalent to pre-v1.9):\nRetry.DEFAULT = Retry(3)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/util/ssl_.py",
    "content": "from __future__ import absolute_import\n\nimport hmac\nimport os\nimport sys\nimport warnings\nfrom binascii import hexlify, unhexlify\nfrom hashlib import md5, sha1, sha256\n\nfrom ..exceptions import (\n    InsecurePlatformWarning,\n    ProxySchemeUnsupported,\n    SNIMissingWarning,\n    SSLError,\n)\nfrom ..packages import six\nfrom .url import BRACELESS_IPV6_ADDRZ_RE, IPV4_RE\n\nSSLContext = None\nSSLTransport = None\nHAS_SNI = False\nIS_PYOPENSSL = False\nIS_SECURETRANSPORT = False\nALPN_PROTOCOLS = [\"http/1.1\"]\n\n# Maps the length of a digest to a possible hash function producing this digest\nHASHFUNC_MAP = {32: md5, 40: sha1, 64: sha256}\n\n\ndef _const_compare_digest_backport(a, b):\n    \"\"\"\n    Compare two digests of equal length in constant time.\n\n    The digests must be of type str/bytes.\n    Returns True if the digests match, and False otherwise.\n    \"\"\"\n    result = abs(len(a) - len(b))\n    for left, right in zip(bytearray(a), bytearray(b)):\n        result |= left ^ right\n    return result == 0\n\n\n_const_compare_digest = getattr(hmac, \"compare_digest\", _const_compare_digest_backport)\n\ntry:  # Test for SSL features\n    import ssl\n    from ssl import CERT_REQUIRED, wrap_socket\nexcept ImportError:\n    pass\n\ntry:\n    from ssl import HAS_SNI  # Has SNI?\nexcept ImportError:\n    pass\n\ntry:\n    from .ssltransport import SSLTransport\nexcept ImportError:\n    pass\n\n\ntry:  # Platform-specific: Python 3.6\n    from ssl import PROTOCOL_TLS\n\n    PROTOCOL_SSLv23 = PROTOCOL_TLS\nexcept ImportError:\n    try:\n        from ssl import PROTOCOL_SSLv23 as PROTOCOL_TLS\n\n        PROTOCOL_SSLv23 = PROTOCOL_TLS\n    except ImportError:\n        PROTOCOL_SSLv23 = PROTOCOL_TLS = 2\n\ntry:\n    from ssl import PROTOCOL_TLS_CLIENT\nexcept ImportError:\n    PROTOCOL_TLS_CLIENT = PROTOCOL_TLS\n\n\ntry:\n    from ssl import OP_NO_COMPRESSION, OP_NO_SSLv2, OP_NO_SSLv3\nexcept ImportError:\n    OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000\n    OP_NO_COMPRESSION = 0x20000\n\n\ntry:  # OP_NO_TICKET was added in Python 3.6\n    from ssl import OP_NO_TICKET\nexcept ImportError:\n    OP_NO_TICKET = 0x4000\n\n\n# A secure default.\n# Sources for more information on TLS ciphers:\n#\n# - https://wiki.mozilla.org/Security/Server_Side_TLS\n# - https://www.ssllabs.com/projects/best-practices/index.html\n# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/\n#\n# The general intent is:\n# - prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE),\n# - prefer ECDHE over DHE for better performance,\n# - prefer any AES-GCM and ChaCha20 over any AES-CBC for better performance and\n#   security,\n# - prefer AES-GCM over ChaCha20 because hardware-accelerated AES is common,\n# - disable NULL authentication, MD5 MACs, DSS, and other\n#   insecure ciphers for security reasons.\n# - NOTE: TLS 1.3 cipher suites are managed through a different interface\n#   not exposed by CPython (yet!) and are enabled by default if they're available.\nDEFAULT_CIPHERS = \":\".join(\n    [\n        \"ECDHE+AESGCM\",\n        \"ECDHE+CHACHA20\",\n        \"DHE+AESGCM\",\n        \"DHE+CHACHA20\",\n        \"ECDH+AESGCM\",\n        \"DH+AESGCM\",\n        \"ECDH+AES\",\n        \"DH+AES\",\n        \"RSA+AESGCM\",\n        \"RSA+AES\",\n        \"!aNULL\",\n        \"!eNULL\",\n        \"!MD5\",\n        \"!DSS\",\n    ]\n)\n\ntry:\n    from ssl import SSLContext  # Modern SSL?\nexcept ImportError:\n\n    class SSLContext(object):  # Platform-specific: Python 2\n        def __init__(self, protocol_version):\n            self.protocol = protocol_version\n            # Use default values from a real SSLContext\n            self.check_hostname = False\n            self.verify_mode = ssl.CERT_NONE\n            self.ca_certs = None\n            self.options = 0\n            self.certfile = None\n            self.keyfile = None\n            self.ciphers = None\n\n        def load_cert_chain(self, certfile, keyfile):\n            self.certfile = certfile\n            self.keyfile = keyfile\n\n        def load_verify_locations(self, cafile=None, capath=None, cadata=None):\n            self.ca_certs = cafile\n\n            if capath is not None:\n                raise SSLError(\"CA directories not supported in older Pythons\")\n\n            if cadata is not None:\n                raise SSLError(\"CA data not supported in older Pythons\")\n\n        def set_ciphers(self, cipher_suite):\n            self.ciphers = cipher_suite\n\n        def wrap_socket(self, socket, server_hostname=None, server_side=False):\n            warnings.warn(\n                \"A true SSLContext object is not available. This prevents \"\n                \"urllib3 from configuring SSL appropriately and may cause \"\n                \"certain SSL connections to fail. You can upgrade to a newer \"\n                \"version of Python to solve this. For more information, see \"\n                \"https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html\"\n                \"#ssl-warnings\",\n                InsecurePlatformWarning,\n            )\n            kwargs = {\n                \"keyfile\": self.keyfile,\n                \"certfile\": self.certfile,\n                \"ca_certs\": self.ca_certs,\n                \"cert_reqs\": self.verify_mode,\n                \"ssl_version\": self.protocol,\n                \"server_side\": server_side,\n            }\n            return wrap_socket(socket, ciphers=self.ciphers, **kwargs)\n\n\ndef assert_fingerprint(cert, fingerprint):\n    \"\"\"\n    Checks if given fingerprint matches the supplied certificate.\n\n    :param cert:\n        Certificate as bytes object.\n    :param fingerprint:\n        Fingerprint as string of hexdigits, can be interspersed by colons.\n    \"\"\"\n\n    fingerprint = fingerprint.replace(\":\", \"\").lower()\n    digest_length = len(fingerprint)\n    hashfunc = HASHFUNC_MAP.get(digest_length)\n    if not hashfunc:\n        raise SSLError(\"Fingerprint of invalid length: {0}\".format(fingerprint))\n\n    # We need encode() here for py32; works on py2 and p33.\n    fingerprint_bytes = unhexlify(fingerprint.encode())\n\n    cert_digest = hashfunc(cert).digest()\n\n    if not _const_compare_digest(cert_digest, fingerprint_bytes):\n        raise SSLError(\n            'Fingerprints did not match. Expected \"{0}\", got \"{1}\".'.format(\n                fingerprint, hexlify(cert_digest)\n            )\n        )\n\n\ndef resolve_cert_reqs(candidate):\n    \"\"\"\n    Resolves the argument to a numeric constant, which can be passed to\n    the wrap_socket function/method from the ssl module.\n    Defaults to :data:`ssl.CERT_REQUIRED`.\n    If given a string it is assumed to be the name of the constant in the\n    :mod:`ssl` module or its abbreviation.\n    (So you can specify `REQUIRED` instead of `CERT_REQUIRED`.\n    If it's neither `None` nor a string we assume it is already the numeric\n    constant which can directly be passed to wrap_socket.\n    \"\"\"\n    if candidate is None:\n        return CERT_REQUIRED\n\n    if isinstance(candidate, str):\n        res = getattr(ssl, candidate, None)\n        if res is None:\n            res = getattr(ssl, \"CERT_\" + candidate)\n        return res\n\n    return candidate\n\n\ndef resolve_ssl_version(candidate):\n    \"\"\"\n    like resolve_cert_reqs\n    \"\"\"\n    if candidate is None:\n        return PROTOCOL_TLS\n\n    if isinstance(candidate, str):\n        res = getattr(ssl, candidate, None)\n        if res is None:\n            res = getattr(ssl, \"PROTOCOL_\" + candidate)\n        return res\n\n    return candidate\n\n\ndef create_urllib3_context(\n    ssl_version=None, cert_reqs=None, options=None, ciphers=None\n):\n    \"\"\"All arguments have the same meaning as ``ssl_wrap_socket``.\n\n    By default, this function does a lot of the same work that\n    ``ssl.create_default_context`` does on Python 3.4+. It:\n\n    - Disables SSLv2, SSLv3, and compression\n    - Sets a restricted set of server ciphers\n\n    If you wish to enable SSLv3, you can do::\n\n        from urllib3.util import ssl_\n        context = ssl_.create_urllib3_context()\n        context.options &= ~ssl_.OP_NO_SSLv3\n\n    You can do the same to enable compression (substituting ``COMPRESSION``\n    for ``SSLv3`` in the last line above).\n\n    :param ssl_version:\n        The desired protocol version to use. This will default to\n        PROTOCOL_SSLv23 which will negotiate the highest protocol that both\n        the server and your installation of OpenSSL support.\n    :param cert_reqs:\n        Whether to require the certificate verification. This defaults to\n        ``ssl.CERT_REQUIRED``.\n    :param options:\n        Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``,\n        ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``.\n    :param ciphers:\n        Which cipher suites to allow the server to select.\n    :returns:\n        Constructed SSLContext object with specified options\n    :rtype: SSLContext\n    \"\"\"\n    # PROTOCOL_TLS is deprecated in Python 3.10\n    if not ssl_version or ssl_version == PROTOCOL_TLS:\n        ssl_version = PROTOCOL_TLS_CLIENT\n\n    context = SSLContext(ssl_version)\n\n    context.set_ciphers(ciphers or DEFAULT_CIPHERS)\n\n    # Setting the default here, as we may have no ssl module on import\n    cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs\n\n    if options is None:\n        options = 0\n        # SSLv2 is easily broken and is considered harmful and dangerous\n        options |= OP_NO_SSLv2\n        # SSLv3 has several problems and is now dangerous\n        options |= OP_NO_SSLv3\n        # Disable compression to prevent CRIME attacks for OpenSSL 1.0+\n        # (issue #309)\n        options |= OP_NO_COMPRESSION\n        # TLSv1.2 only. Unless set explicitly, do not request tickets.\n        # This may save some bandwidth on wire, and although the ticket is encrypted,\n        # there is a risk associated with it being on wire,\n        # if the server is not rotating its ticketing keys properly.\n        options |= OP_NO_TICKET\n\n    context.options |= options\n\n    # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is\n    # necessary for conditional client cert authentication with TLS 1.3.\n    # The attribute is None for OpenSSL <= 1.1.0 or does not exist in older\n    # versions of Python.  We only enable on Python 3.7.4+ or if certificate\n    # verification is enabled to work around Python issue #37428\n    # See: https://bugs.python.org/issue37428\n    if (cert_reqs == ssl.CERT_REQUIRED or sys.version_info >= (3, 7, 4)) and getattr(\n        context, \"post_handshake_auth\", None\n    ) is not None:\n        context.post_handshake_auth = True\n\n    def disable_check_hostname():\n        if (\n            getattr(context, \"check_hostname\", None) is not None\n        ):  # Platform-specific: Python 3.2\n            # We do our own verification, including fingerprints and alternative\n            # hostnames. So disable it here\n            context.check_hostname = False\n\n    # The order of the below lines setting verify_mode and check_hostname\n    # matter due to safe-guards SSLContext has to prevent an SSLContext with\n    # check_hostname=True, verify_mode=NONE/OPTIONAL. This is made even more\n    # complex because we don't know whether PROTOCOL_TLS_CLIENT will be used\n    # or not so we don't know the initial state of the freshly created SSLContext.\n    if cert_reqs == ssl.CERT_REQUIRED:\n        context.verify_mode = cert_reqs\n        disable_check_hostname()\n    else:\n        disable_check_hostname()\n        context.verify_mode = cert_reqs\n\n    # Enable logging of TLS session keys via defacto standard environment variable\n    # 'SSLKEYLOGFILE', if the feature is available (Python 3.8+). Skip empty values.\n    if hasattr(context, \"keylog_filename\"):\n        sslkeylogfile = os.environ.get(\"SSLKEYLOGFILE\")\n        if sslkeylogfile:\n            context.keylog_filename = sslkeylogfile\n\n    return context\n\n\ndef ssl_wrap_socket(\n    sock,\n    keyfile=None,\n    certfile=None,\n    cert_reqs=None,\n    ca_certs=None,\n    server_hostname=None,\n    ssl_version=None,\n    ciphers=None,\n    ssl_context=None,\n    ca_cert_dir=None,\n    key_password=None,\n    ca_cert_data=None,\n    tls_in_tls=False,\n):\n    \"\"\"\n    All arguments except for server_hostname, ssl_context, and ca_cert_dir have\n    the same meaning as they do when using :func:`ssl.wrap_socket`.\n\n    :param server_hostname:\n        When SNI is supported, the expected hostname of the certificate\n    :param ssl_context:\n        A pre-made :class:`SSLContext` object. If none is provided, one will\n        be created using :func:`create_urllib3_context`.\n    :param ciphers:\n        A string of ciphers we wish the client to support.\n    :param ca_cert_dir:\n        A directory containing CA certificates in multiple separate files, as\n        supported by OpenSSL's -CApath flag or the capath argument to\n        SSLContext.load_verify_locations().\n    :param key_password:\n        Optional password if the keyfile is encrypted.\n    :param ca_cert_data:\n        Optional string containing CA certificates in PEM format suitable for\n        passing as the cadata parameter to SSLContext.load_verify_locations()\n    :param tls_in_tls:\n        Use SSLTransport to wrap the existing socket.\n    \"\"\"\n    context = ssl_context\n    if context is None:\n        # Note: This branch of code and all the variables in it are no longer\n        # used by urllib3 itself. We should consider deprecating and removing\n        # this code.\n        context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers)\n\n    if ca_certs or ca_cert_dir or ca_cert_data:\n        try:\n            context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data)\n        except (IOError, OSError) as e:\n            raise SSLError(e)\n\n    elif ssl_context is None and hasattr(context, \"load_default_certs\"):\n        # try to load OS default certs; works well on Windows (require Python3.4+)\n        context.load_default_certs()\n\n    # Attempt to detect if we get the goofy behavior of the\n    # keyfile being encrypted and OpenSSL asking for the\n    # passphrase via the terminal and instead error out.\n    if keyfile and key_password is None and _is_key_file_encrypted(keyfile):\n        raise SSLError(\"Client private key is encrypted, password is required\")\n\n    if certfile:\n        if key_password is None:\n            context.load_cert_chain(certfile, keyfile)\n        else:\n            context.load_cert_chain(certfile, keyfile, key_password)\n\n    try:\n        if hasattr(context, \"set_alpn_protocols\"):\n            context.set_alpn_protocols(ALPN_PROTOCOLS)\n    except NotImplementedError:  # Defensive: in CI, we always have set_alpn_protocols\n        pass\n\n    # If we detect server_hostname is an IP address then the SNI\n    # extension should not be used according to RFC3546 Section 3.1\n    use_sni_hostname = server_hostname and not is_ipaddress(server_hostname)\n    # SecureTransport uses server_hostname in certificate verification.\n    send_sni = (use_sni_hostname and HAS_SNI) or (\n        IS_SECURETRANSPORT and server_hostname\n    )\n    # Do not warn the user if server_hostname is an invalid SNI hostname.\n    if not HAS_SNI and use_sni_hostname:\n        warnings.warn(\n            \"An HTTPS request has been made, but the SNI (Server Name \"\n            \"Indication) extension to TLS is not available on this platform. \"\n            \"This may cause the server to present an incorrect TLS \"\n            \"certificate, which can cause validation failures. You can upgrade to \"\n            \"a newer version of Python to solve this. For more information, see \"\n            \"https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html\"\n            \"#ssl-warnings\",\n            SNIMissingWarning,\n        )\n\n    if send_sni:\n        ssl_sock = _ssl_wrap_socket_impl(\n            sock, context, tls_in_tls, server_hostname=server_hostname\n        )\n    else:\n        ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls)\n    return ssl_sock\n\n\ndef is_ipaddress(hostname):\n    \"\"\"Detects whether the hostname given is an IPv4 or IPv6 address.\n    Also detects IPv6 addresses with Zone IDs.\n\n    :param str hostname: Hostname to examine.\n    :return: True if the hostname is an IP address, False otherwise.\n    \"\"\"\n    if not six.PY2 and isinstance(hostname, bytes):\n        # IDN A-label bytes are ASCII compatible.\n        hostname = hostname.decode(\"ascii\")\n    return bool(IPV4_RE.match(hostname) or BRACELESS_IPV6_ADDRZ_RE.match(hostname))\n\n\ndef _is_key_file_encrypted(key_file):\n    \"\"\"Detects if a key file is encrypted or not.\"\"\"\n    with open(key_file, \"r\") as f:\n        for line in f:\n            # Look for Proc-Type: 4,ENCRYPTED\n            if \"ENCRYPTED\" in line:\n                return True\n\n    return False\n\n\ndef _ssl_wrap_socket_impl(sock, ssl_context, tls_in_tls, server_hostname=None):\n    if tls_in_tls:\n        if not SSLTransport:\n            # Import error, ssl is not available.\n            raise ProxySchemeUnsupported(\n                \"TLS in TLS requires support for the 'ssl' module\"\n            )\n\n        SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context)\n        return SSLTransport(sock, ssl_context, server_hostname)\n\n    if server_hostname:\n        return ssl_context.wrap_socket(sock, server_hostname=server_hostname)\n    else:\n        return ssl_context.wrap_socket(sock)\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/util/ssltransport.py",
    "content": "import io\nimport socket\nimport ssl\n\nfrom urllib3.exceptions import ProxySchemeUnsupported\nfrom urllib3.packages import six\n\nSSL_BLOCKSIZE = 16384\n\n\nclass SSLTransport:\n    \"\"\"\n    The SSLTransport wraps an existing socket and establishes an SSL connection.\n\n    Contrary to Python's implementation of SSLSocket, it allows you to chain\n    multiple TLS connections together. It's particularly useful if you need to\n    implement TLS within TLS.\n\n    The class supports most of the socket API operations.\n    \"\"\"\n\n    @staticmethod\n    def _validate_ssl_context_for_tls_in_tls(ssl_context):\n        \"\"\"\n        Raises a ProxySchemeUnsupported if the provided ssl_context can't be used\n        for TLS in TLS.\n\n        The only requirement is that the ssl_context provides the 'wrap_bio'\n        methods.\n        \"\"\"\n\n        if not hasattr(ssl_context, \"wrap_bio\"):\n            if six.PY2:\n                raise ProxySchemeUnsupported(\n                    \"TLS in TLS requires SSLContext.wrap_bio() which isn't \"\n                    \"supported on Python 2\"\n                )\n            else:\n                raise ProxySchemeUnsupported(\n                    \"TLS in TLS requires SSLContext.wrap_bio() which isn't \"\n                    \"available on non-native SSLContext\"\n                )\n\n    def __init__(\n        self, socket, ssl_context, server_hostname=None, suppress_ragged_eofs=True\n    ):\n        \"\"\"\n        Create an SSLTransport around socket using the provided ssl_context.\n        \"\"\"\n        self.incoming = ssl.MemoryBIO()\n        self.outgoing = ssl.MemoryBIO()\n\n        self.suppress_ragged_eofs = suppress_ragged_eofs\n        self.socket = socket\n\n        self.sslobj = ssl_context.wrap_bio(\n            self.incoming, self.outgoing, server_hostname=server_hostname\n        )\n\n        # Perform initial handshake.\n        self._ssl_io_loop(self.sslobj.do_handshake)\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, *_):\n        self.close()\n\n    def fileno(self):\n        return self.socket.fileno()\n\n    def read(self, len=1024, buffer=None):\n        return self._wrap_ssl_read(len, buffer)\n\n    def recv(self, len=1024, flags=0):\n        if flags != 0:\n            raise ValueError(\"non-zero flags not allowed in calls to recv\")\n        return self._wrap_ssl_read(len)\n\n    def recv_into(self, buffer, nbytes=None, flags=0):\n        if flags != 0:\n            raise ValueError(\"non-zero flags not allowed in calls to recv_into\")\n        if buffer and (nbytes is None):\n            nbytes = len(buffer)\n        elif nbytes is None:\n            nbytes = 1024\n        return self.read(nbytes, buffer)\n\n    def sendall(self, data, flags=0):\n        if flags != 0:\n            raise ValueError(\"non-zero flags not allowed in calls to sendall\")\n        count = 0\n        with memoryview(data) as view, view.cast(\"B\") as byte_view:\n            amount = len(byte_view)\n            while count < amount:\n                v = self.send(byte_view[count:])\n                count += v\n\n    def send(self, data, flags=0):\n        if flags != 0:\n            raise ValueError(\"non-zero flags not allowed in calls to send\")\n        response = self._ssl_io_loop(self.sslobj.write, data)\n        return response\n\n    def makefile(\n        self, mode=\"r\", buffering=None, encoding=None, errors=None, newline=None\n    ):\n        \"\"\"\n        Python's httpclient uses makefile and buffered io when reading HTTP\n        messages and we need to support it.\n\n        This is unfortunately a copy and paste of socket.py makefile with small\n        changes to point to the socket directly.\n        \"\"\"\n        if not set(mode) <= {\"r\", \"w\", \"b\"}:\n            raise ValueError(\"invalid mode %r (only r, w, b allowed)\" % (mode,))\n\n        writing = \"w\" in mode\n        reading = \"r\" in mode or not writing\n        assert reading or writing\n        binary = \"b\" in mode\n        rawmode = \"\"\n        if reading:\n            rawmode += \"r\"\n        if writing:\n            rawmode += \"w\"\n        raw = socket.SocketIO(self, rawmode)\n        self.socket._io_refs += 1\n        if buffering is None:\n            buffering = -1\n        if buffering < 0:\n            buffering = io.DEFAULT_BUFFER_SIZE\n        if buffering == 0:\n            if not binary:\n                raise ValueError(\"unbuffered streams must be binary\")\n            return raw\n        if reading and writing:\n            buffer = io.BufferedRWPair(raw, raw, buffering)\n        elif reading:\n            buffer = io.BufferedReader(raw, buffering)\n        else:\n            assert writing\n            buffer = io.BufferedWriter(raw, buffering)\n        if binary:\n            return buffer\n        text = io.TextIOWrapper(buffer, encoding, errors, newline)\n        text.mode = mode\n        return text\n\n    def unwrap(self):\n        self._ssl_io_loop(self.sslobj.unwrap)\n\n    def close(self):\n        self.socket.close()\n\n    def getpeercert(self, binary_form=False):\n        return self.sslobj.getpeercert(binary_form)\n\n    def version(self):\n        return self.sslobj.version()\n\n    def cipher(self):\n        return self.sslobj.cipher()\n\n    def selected_alpn_protocol(self):\n        return self.sslobj.selected_alpn_protocol()\n\n    def selected_npn_protocol(self):\n        return self.sslobj.selected_npn_protocol()\n\n    def shared_ciphers(self):\n        return self.sslobj.shared_ciphers()\n\n    def compression(self):\n        return self.sslobj.compression()\n\n    def settimeout(self, value):\n        self.socket.settimeout(value)\n\n    def gettimeout(self):\n        return self.socket.gettimeout()\n\n    def _decref_socketios(self):\n        self.socket._decref_socketios()\n\n    def _wrap_ssl_read(self, len, buffer=None):\n        try:\n            return self._ssl_io_loop(self.sslobj.read, len, buffer)\n        except ssl.SSLError as e:\n            if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs:\n                return 0  # eof, return 0.\n            else:\n                raise\n\n    def _ssl_io_loop(self, func, *args):\n        \"\"\"Performs an I/O loop between incoming/outgoing and the socket.\"\"\"\n        should_loop = True\n        ret = None\n\n        while should_loop:\n            errno = None\n            try:\n                ret = func(*args)\n            except ssl.SSLError as e:\n                if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE):\n                    # WANT_READ, and WANT_WRITE are expected, others are not.\n                    raise e\n                errno = e.errno\n\n            buf = self.outgoing.read()\n            self.socket.sendall(buf)\n\n            if errno is None:\n                should_loop = False\n            elif errno == ssl.SSL_ERROR_WANT_READ:\n                buf = self.socket.recv(SSL_BLOCKSIZE)\n                if buf:\n                    self.incoming.write(buf)\n                else:\n                    self.incoming.write_eof()\n        return ret\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/util/timeout.py",
    "content": "from __future__ import absolute_import\n\nimport time\n\n# The default socket timeout, used by httplib to indicate that no timeout was\n# specified by the user\nfrom socket import _GLOBAL_DEFAULT_TIMEOUT\n\nfrom ..exceptions import TimeoutStateError\n\n# A sentinel value to indicate that no timeout was specified by the user in\n# urllib3\n_Default = object()\n\n\n# Use time.monotonic if available.\ncurrent_time = getattr(time, \"monotonic\", time.time)\n\n\nclass Timeout(object):\n    \"\"\"Timeout configuration.\n\n    Timeouts can be defined as a default for a pool:\n\n    .. code-block:: python\n\n       timeout = Timeout(connect=2.0, read=7.0)\n       http = PoolManager(timeout=timeout)\n       response = http.request('GET', 'http://example.com/')\n\n    Or per-request (which overrides the default for the pool):\n\n    .. code-block:: python\n\n       response = http.request('GET', 'http://example.com/', timeout=Timeout(10))\n\n    Timeouts can be disabled by setting all the parameters to ``None``:\n\n    .. code-block:: python\n\n       no_timeout = Timeout(connect=None, read=None)\n       response = http.request('GET', 'http://example.com/, timeout=no_timeout)\n\n\n    :param total:\n        This combines the connect and read timeouts into one; the read timeout\n        will be set to the time leftover from the connect attempt. In the\n        event that both a connect timeout and a total are specified, or a read\n        timeout and a total are specified, the shorter timeout will be applied.\n\n        Defaults to None.\n\n    :type total: int, float, or None\n\n    :param connect:\n        The maximum amount of time (in seconds) to wait for a connection\n        attempt to a server to succeed. Omitting the parameter will default the\n        connect timeout to the system default, probably `the global default\n        timeout in socket.py\n        <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.\n        None will set an infinite timeout for connection attempts.\n\n    :type connect: int, float, or None\n\n    :param read:\n        The maximum amount of time (in seconds) to wait between consecutive\n        read operations for a response from the server. Omitting the parameter\n        will default the read timeout to the system default, probably `the\n        global default timeout in socket.py\n        <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.\n        None will set an infinite timeout.\n\n    :type read: int, float, or None\n\n    .. note::\n\n        Many factors can affect the total amount of time for urllib3 to return\n        an HTTP response.\n\n        For example, Python's DNS resolver does not obey the timeout specified\n        on the socket. Other factors that can affect total request time include\n        high CPU load, high swap, the program running at a low priority level,\n        or other behaviors.\n\n        In addition, the read and total timeouts only measure the time between\n        read operations on the socket connecting the client and the server,\n        not the total amount of time for the request to return a complete\n        response. For most requests, the timeout is raised because the server\n        has not sent the first byte in the specified time. This is not always\n        the case; if a server streams one byte every fifteen seconds, a timeout\n        of 20 seconds will not trigger, even though the request will take\n        several minutes to complete.\n\n        If your goal is to cut off any request after a set amount of wall clock\n        time, consider having a second \"watcher\" thread to cut off a slow\n        request.\n    \"\"\"\n\n    #: A sentinel object representing the default timeout value\n    DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT\n\n    def __init__(self, total=None, connect=_Default, read=_Default):\n        self._connect = self._validate_timeout(connect, \"connect\")\n        self._read = self._validate_timeout(read, \"read\")\n        self.total = self._validate_timeout(total, \"total\")\n        self._start_connect = None\n\n    def __repr__(self):\n        return \"%s(connect=%r, read=%r, total=%r)\" % (\n            type(self).__name__,\n            self._connect,\n            self._read,\n            self.total,\n        )\n\n    # __str__ provided for backwards compatibility\n    __str__ = __repr__\n\n    @classmethod\n    def _validate_timeout(cls, value, name):\n        \"\"\"Check that a timeout attribute is valid.\n\n        :param value: The timeout value to validate\n        :param name: The name of the timeout attribute to validate. This is\n            used to specify in error messages.\n        :return: The validated and casted version of the given value.\n        :raises ValueError: If it is a numeric value less than or equal to\n            zero, or the type is not an integer, float, or None.\n        \"\"\"\n        if value is _Default:\n            return cls.DEFAULT_TIMEOUT\n\n        if value is None or value is cls.DEFAULT_TIMEOUT:\n            return value\n\n        if isinstance(value, bool):\n            raise ValueError(\n                \"Timeout cannot be a boolean value. It must \"\n                \"be an int, float or None.\"\n            )\n        try:\n            float(value)\n        except (TypeError, ValueError):\n            raise ValueError(\n                \"Timeout value %s was %s, but it must be an \"\n                \"int, float or None.\" % (name, value)\n            )\n\n        try:\n            if value <= 0:\n                raise ValueError(\n                    \"Attempted to set %s timeout to %s, but the \"\n                    \"timeout cannot be set to a value less \"\n                    \"than or equal to 0.\" % (name, value)\n                )\n        except TypeError:\n            # Python 3\n            raise ValueError(\n                \"Timeout value %s was %s, but it must be an \"\n                \"int, float or None.\" % (name, value)\n            )\n\n        return value\n\n    @classmethod\n    def from_float(cls, timeout):\n        \"\"\"Create a new Timeout from a legacy timeout value.\n\n        The timeout value used by httplib.py sets the same timeout on the\n        connect(), and recv() socket requests. This creates a :class:`Timeout`\n        object that sets the individual timeouts to the ``timeout`` value\n        passed to this function.\n\n        :param timeout: The legacy timeout value.\n        :type timeout: integer, float, sentinel default object, or None\n        :return: Timeout object\n        :rtype: :class:`Timeout`\n        \"\"\"\n        return Timeout(read=timeout, connect=timeout)\n\n    def clone(self):\n        \"\"\"Create a copy of the timeout object\n\n        Timeout properties are stored per-pool but each request needs a fresh\n        Timeout object to ensure each one has its own start/stop configured.\n\n        :return: a copy of the timeout object\n        :rtype: :class:`Timeout`\n        \"\"\"\n        # We can't use copy.deepcopy because that will also create a new object\n        # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to\n        # detect the user default.\n        return Timeout(connect=self._connect, read=self._read, total=self.total)\n\n    def start_connect(self):\n        \"\"\"Start the timeout clock, used during a connect() attempt\n\n        :raises urllib3.exceptions.TimeoutStateError: if you attempt\n            to start a timer that has been started already.\n        \"\"\"\n        if self._start_connect is not None:\n            raise TimeoutStateError(\"Timeout timer has already been started.\")\n        self._start_connect = current_time()\n        return self._start_connect\n\n    def get_connect_duration(self):\n        \"\"\"Gets the time elapsed since the call to :meth:`start_connect`.\n\n        :return: Elapsed time in seconds.\n        :rtype: float\n        :raises urllib3.exceptions.TimeoutStateError: if you attempt\n            to get duration for a timer that hasn't been started.\n        \"\"\"\n        if self._start_connect is None:\n            raise TimeoutStateError(\n                \"Can't get connect duration for timer that has not started.\"\n            )\n        return current_time() - self._start_connect\n\n    @property\n    def connect_timeout(self):\n        \"\"\"Get the value to use when setting a connection timeout.\n\n        This will be a positive float or integer, the value None\n        (never timeout), or the default system timeout.\n\n        :return: Connect timeout.\n        :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None\n        \"\"\"\n        if self.total is None:\n            return self._connect\n\n        if self._connect is None or self._connect is self.DEFAULT_TIMEOUT:\n            return self.total\n\n        return min(self._connect, self.total)\n\n    @property\n    def read_timeout(self):\n        \"\"\"Get the value for the read timeout.\n\n        This assumes some time has elapsed in the connection timeout and\n        computes the read timeout appropriately.\n\n        If self.total is set, the read timeout is dependent on the amount of\n        time taken by the connect timeout. If the connection time has not been\n        established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be\n        raised.\n\n        :return: Value to use for the read timeout.\n        :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None\n        :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect`\n            has not yet been called on this object.\n        \"\"\"\n        if (\n            self.total is not None\n            and self.total is not self.DEFAULT_TIMEOUT\n            and self._read is not None\n            and self._read is not self.DEFAULT_TIMEOUT\n        ):\n            # In case the connect timeout has not yet been established.\n            if self._start_connect is None:\n                return self._read\n            return max(0, min(self.total - self.get_connect_duration(), self._read))\n        elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT:\n            return max(0, self.total - self.get_connect_duration())\n        else:\n            return self._read\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/util/url.py",
    "content": "from __future__ import absolute_import\n\nimport re\nfrom collections import namedtuple\n\nfrom ..exceptions import LocationParseError\nfrom ..packages import six\n\nurl_attrs = [\"scheme\", \"auth\", \"host\", \"port\", \"path\", \"query\", \"fragment\"]\n\n# We only want to normalize urls with an HTTP(S) scheme.\n# urllib3 infers URLs without a scheme (None) to be http.\nNORMALIZABLE_SCHEMES = (\"http\", \"https\", None)\n\n# Almost all of these patterns were derived from the\n# 'rfc3986' module: https://github.com/python-hyper/rfc3986\nPERCENT_RE = re.compile(r\"%[a-fA-F0-9]{2}\")\nSCHEME_RE = re.compile(r\"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)\")\nURI_RE = re.compile(\n    r\"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?\"\n    r\"(?://([^\\\\/?#]*))?\"\n    r\"([^?#]*)\"\n    r\"(?:\\?([^#]*))?\"\n    r\"(?:#(.*))?$\",\n    re.UNICODE | re.DOTALL,\n)\n\nIPV4_PAT = r\"(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\"\nHEX_PAT = \"[0-9A-Fa-f]{1,4}\"\nLS32_PAT = \"(?:{hex}:{hex}|{ipv4})\".format(hex=HEX_PAT, ipv4=IPV4_PAT)\n_subs = {\"hex\": HEX_PAT, \"ls32\": LS32_PAT}\n_variations = [\n    #                            6( h16 \":\" ) ls32\n    \"(?:%(hex)s:){6}%(ls32)s\",\n    #                       \"::\" 5( h16 \":\" ) ls32\n    \"::(?:%(hex)s:){5}%(ls32)s\",\n    # [               h16 ] \"::\" 4( h16 \":\" ) ls32\n    \"(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s\",\n    # [ *1( h16 \":\" ) h16 ] \"::\" 3( h16 \":\" ) ls32\n    \"(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s\",\n    # [ *2( h16 \":\" ) h16 ] \"::\" 2( h16 \":\" ) ls32\n    \"(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s\",\n    # [ *3( h16 \":\" ) h16 ] \"::\"    h16 \":\"   ls32\n    \"(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s\",\n    # [ *4( h16 \":\" ) h16 ] \"::\"              ls32\n    \"(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s\",\n    # [ *5( h16 \":\" ) h16 ] \"::\"              h16\n    \"(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s\",\n    # [ *6( h16 \":\" ) h16 ] \"::\"\n    \"(?:(?:%(hex)s:){0,6}%(hex)s)?::\",\n]\n\nUNRESERVED_PAT = r\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._!\\-~\"\nIPV6_PAT = \"(?:\" + \"|\".join([x % _subs for x in _variations]) + \")\"\nZONE_ID_PAT = \"(?:%25|%)(?:[\" + UNRESERVED_PAT + \"]|%[a-fA-F0-9]{2})+\"\nIPV6_ADDRZ_PAT = r\"\\[\" + IPV6_PAT + r\"(?:\" + ZONE_ID_PAT + r\")?\\]\"\nREG_NAME_PAT = r\"(?:[^\\[\\]%:/?#]|%[a-fA-F0-9]{2})*\"\nTARGET_RE = re.compile(r\"^(/[^?#]*)(?:\\?([^#]*))?(?:#.*)?$\")\n\nIPV4_RE = re.compile(\"^\" + IPV4_PAT + \"$\")\nIPV6_RE = re.compile(\"^\" + IPV6_PAT + \"$\")\nIPV6_ADDRZ_RE = re.compile(\"^\" + IPV6_ADDRZ_PAT + \"$\")\nBRACELESS_IPV6_ADDRZ_RE = re.compile(\"^\" + IPV6_ADDRZ_PAT[2:-2] + \"$\")\nZONE_ID_RE = re.compile(\"(\" + ZONE_ID_PAT + r\")\\]$\")\n\n_HOST_PORT_PAT = (\"^(%s|%s|%s)(?::([0-9]{0,5}))?$\") % (\n    REG_NAME_PAT,\n    IPV4_PAT,\n    IPV6_ADDRZ_PAT,\n)\n_HOST_PORT_RE = re.compile(_HOST_PORT_PAT, re.UNICODE | re.DOTALL)\n\nUNRESERVED_CHARS = set(\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-~\"\n)\nSUB_DELIM_CHARS = set(\"!$&'()*+,;=\")\nUSERINFO_CHARS = UNRESERVED_CHARS | SUB_DELIM_CHARS | {\":\"}\nPATH_CHARS = USERINFO_CHARS | {\"@\", \"/\"}\nQUERY_CHARS = FRAGMENT_CHARS = PATH_CHARS | {\"?\"}\n\n\nclass Url(namedtuple(\"Url\", url_attrs)):\n    \"\"\"\n    Data structure for representing an HTTP URL. Used as a return value for\n    :func:`parse_url`. Both the scheme and host are normalized as they are\n    both case-insensitive according to RFC 3986.\n    \"\"\"\n\n    __slots__ = ()\n\n    def __new__(\n        cls,\n        scheme=None,\n        auth=None,\n        host=None,\n        port=None,\n        path=None,\n        query=None,\n        fragment=None,\n    ):\n        if path and not path.startswith(\"/\"):\n            path = \"/\" + path\n        if scheme is not None:\n            scheme = scheme.lower()\n        return super(Url, cls).__new__(\n            cls, scheme, auth, host, port, path, query, fragment\n        )\n\n    @property\n    def hostname(self):\n        \"\"\"For backwards-compatibility with urlparse. We're nice like that.\"\"\"\n        return self.host\n\n    @property\n    def request_uri(self):\n        \"\"\"Absolute path including the query string.\"\"\"\n        uri = self.path or \"/\"\n\n        if self.query is not None:\n            uri += \"?\" + self.query\n\n        return uri\n\n    @property\n    def netloc(self):\n        \"\"\"Network location including host and port\"\"\"\n        if self.port:\n            return \"%s:%d\" % (self.host, self.port)\n        return self.host\n\n    @property\n    def url(self):\n        \"\"\"\n        Convert self into a url\n\n        This function should more or less round-trip with :func:`.parse_url`. The\n        returned url may not be exactly the same as the url inputted to\n        :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls\n        with a blank port will have : removed).\n\n        Example: ::\n\n            >>> U = parse_url('http://google.com/mail/')\n            >>> U.url\n            'http://google.com/mail/'\n            >>> Url('http', 'username:password', 'host.com', 80,\n            ... '/path', 'query', 'fragment').url\n            'http://username:password@host.com:80/path?query#fragment'\n        \"\"\"\n        scheme, auth, host, port, path, query, fragment = self\n        url = u\"\"\n\n        # We use \"is not None\" we want things to happen with empty strings (or 0 port)\n        if scheme is not None:\n            url += scheme + u\"://\"\n        if auth is not None:\n            url += auth + u\"@\"\n        if host is not None:\n            url += host\n        if port is not None:\n            url += u\":\" + str(port)\n        if path is not None:\n            url += path\n        if query is not None:\n            url += u\"?\" + query\n        if fragment is not None:\n            url += u\"#\" + fragment\n\n        return url\n\n    def __str__(self):\n        return self.url\n\n\ndef split_first(s, delims):\n    \"\"\"\n    .. deprecated:: 1.25\n\n    Given a string and an iterable of delimiters, split on the first found\n    delimiter. Return two split parts and the matched delimiter.\n\n    If not found, then the first part is the full input string.\n\n    Example::\n\n        >>> split_first('foo/bar?baz', '?/=')\n        ('foo', 'bar?baz', '/')\n        >>> split_first('foo/bar?baz', '123')\n        ('foo/bar?baz', '', None)\n\n    Scales linearly with number of delims. Not ideal for large number of delims.\n    \"\"\"\n    min_idx = None\n    min_delim = None\n    for d in delims:\n        idx = s.find(d)\n        if idx < 0:\n            continue\n\n        if min_idx is None or idx < min_idx:\n            min_idx = idx\n            min_delim = d\n\n    if min_idx is None or min_idx < 0:\n        return s, \"\", None\n\n    return s[:min_idx], s[min_idx + 1 :], min_delim\n\n\ndef _encode_invalid_chars(component, allowed_chars, encoding=\"utf-8\"):\n    \"\"\"Percent-encodes a URI component without reapplying\n    onto an already percent-encoded component.\n    \"\"\"\n    if component is None:\n        return component\n\n    component = six.ensure_text(component)\n\n    # Normalize existing percent-encoded bytes.\n    # Try to see if the component we're encoding is already percent-encoded\n    # so we can skip all '%' characters but still encode all others.\n    component, percent_encodings = PERCENT_RE.subn(\n        lambda match: match.group(0).upper(), component\n    )\n\n    uri_bytes = component.encode(\"utf-8\", \"surrogatepass\")\n    is_percent_encoded = percent_encodings == uri_bytes.count(b\"%\")\n    encoded_component = bytearray()\n\n    for i in range(0, len(uri_bytes)):\n        # Will return a single character bytestring on both Python 2 & 3\n        byte = uri_bytes[i : i + 1]\n        byte_ord = ord(byte)\n        if (is_percent_encoded and byte == b\"%\") or (\n            byte_ord < 128 and byte.decode() in allowed_chars\n        ):\n            encoded_component += byte\n            continue\n        encoded_component.extend(b\"%\" + (hex(byte_ord)[2:].encode().zfill(2).upper()))\n\n    return encoded_component.decode(encoding)\n\n\ndef _remove_path_dot_segments(path):\n    # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code\n    segments = path.split(\"/\")  # Turn the path into a list of segments\n    output = []  # Initialize the variable to use to store output\n\n    for segment in segments:\n        # '.' is the current directory, so ignore it, it is superfluous\n        if segment == \".\":\n            continue\n        # Anything other than '..', should be appended to the output\n        elif segment != \"..\":\n            output.append(segment)\n        # In this case segment == '..', if we can, we should pop the last\n        # element\n        elif output:\n            output.pop()\n\n    # If the path starts with '/' and the output is empty or the first string\n    # is non-empty\n    if path.startswith(\"/\") and (not output or output[0]):\n        output.insert(0, \"\")\n\n    # If the path starts with '/.' or '/..' ensure we add one more empty\n    # string to add a trailing '/'\n    if path.endswith((\"/.\", \"/..\")):\n        output.append(\"\")\n\n    return \"/\".join(output)\n\n\ndef _normalize_host(host, scheme):\n    if host:\n        if isinstance(host, six.binary_type):\n            host = six.ensure_str(host)\n\n        if scheme in NORMALIZABLE_SCHEMES:\n            is_ipv6 = IPV6_ADDRZ_RE.match(host)\n            if is_ipv6:\n                match = ZONE_ID_RE.search(host)\n                if match:\n                    start, end = match.span(1)\n                    zone_id = host[start:end]\n\n                    if zone_id.startswith(\"%25\") and zone_id != \"%25\":\n                        zone_id = zone_id[3:]\n                    else:\n                        zone_id = zone_id[1:]\n                    zone_id = \"%\" + _encode_invalid_chars(zone_id, UNRESERVED_CHARS)\n                    return host[:start].lower() + zone_id + host[end:]\n                else:\n                    return host.lower()\n            elif not IPV4_RE.match(host):\n                return six.ensure_str(\n                    b\".\".join([_idna_encode(label) for label in host.split(\".\")])\n                )\n    return host\n\n\ndef _idna_encode(name):\n    if name and any([ord(x) > 128 for x in name]):\n        try:\n            import idna\n        except ImportError:\n            six.raise_from(\n                LocationParseError(\"Unable to parse URL without the 'idna' module\"),\n                None,\n            )\n        try:\n            return idna.encode(name.lower(), strict=True, std3_rules=True)\n        except idna.IDNAError:\n            six.raise_from(\n                LocationParseError(u\"Name '%s' is not a valid IDNA label\" % name), None\n            )\n    return name.lower().encode(\"ascii\")\n\n\ndef _encode_target(target):\n    \"\"\"Percent-encodes a request target so that there are no invalid characters\"\"\"\n    path, query = TARGET_RE.match(target).groups()\n    target = _encode_invalid_chars(path, PATH_CHARS)\n    query = _encode_invalid_chars(query, QUERY_CHARS)\n    if query is not None:\n        target += \"?\" + query\n    return target\n\n\ndef parse_url(url):\n    \"\"\"\n    Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is\n    performed to parse incomplete urls. Fields not provided will be None.\n    This parser is RFC 3986 compliant.\n\n    The parser logic and helper functions are based heavily on\n    work done in the ``rfc3986`` module.\n\n    :param str url: URL to parse into a :class:`.Url` namedtuple.\n\n    Partly backwards-compatible with :mod:`urlparse`.\n\n    Example::\n\n        >>> parse_url('http://google.com/mail/')\n        Url(scheme='http', host='google.com', port=None, path='/mail/', ...)\n        >>> parse_url('google.com:80')\n        Url(scheme=None, host='google.com', port=80, path=None, ...)\n        >>> parse_url('/foo?bar')\n        Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...)\n    \"\"\"\n    if not url:\n        # Empty\n        return Url()\n\n    source_url = url\n    if not SCHEME_RE.search(url):\n        url = \"//\" + url\n\n    try:\n        scheme, authority, path, query, fragment = URI_RE.match(url).groups()\n        normalize_uri = scheme is None or scheme.lower() in NORMALIZABLE_SCHEMES\n\n        if scheme:\n            scheme = scheme.lower()\n\n        if authority:\n            auth, _, host_port = authority.rpartition(\"@\")\n            auth = auth or None\n            host, port = _HOST_PORT_RE.match(host_port).groups()\n            if auth and normalize_uri:\n                auth = _encode_invalid_chars(auth, USERINFO_CHARS)\n            if port == \"\":\n                port = None\n        else:\n            auth, host, port = None, None, None\n\n        if port is not None:\n            port = int(port)\n            if not (0 <= port <= 65535):\n                raise LocationParseError(url)\n\n        host = _normalize_host(host, scheme)\n\n        if normalize_uri and path:\n            path = _remove_path_dot_segments(path)\n            path = _encode_invalid_chars(path, PATH_CHARS)\n        if normalize_uri and query:\n            query = _encode_invalid_chars(query, QUERY_CHARS)\n        if normalize_uri and fragment:\n            fragment = _encode_invalid_chars(fragment, FRAGMENT_CHARS)\n\n    except (ValueError, AttributeError):\n        return six.raise_from(LocationParseError(source_url), None)\n\n    # For the sake of backwards compatibility we put empty\n    # string values for path if there are any defined values\n    # beyond the path in the URL.\n    # TODO: Remove this when we break backwards compatibility.\n    if not path:\n        if query is not None or fragment is not None:\n            path = \"\"\n        else:\n            path = None\n\n    # Ensure that each part of the URL is a `str` for\n    # backwards compatibility.\n    if isinstance(url, six.text_type):\n        ensure_func = six.ensure_text\n    else:\n        ensure_func = six.ensure_str\n\n    def ensure_type(x):\n        return x if x is None else ensure_func(x)\n\n    return Url(\n        scheme=ensure_type(scheme),\n        auth=ensure_type(auth),\n        host=ensure_type(host),\n        port=port,\n        path=ensure_type(path),\n        query=ensure_type(query),\n        fragment=ensure_type(fragment),\n    )\n\n\ndef get_host(url):\n    \"\"\"\n    Deprecated. Use :func:`parse_url` instead.\n    \"\"\"\n    p = parse_url(url)\n    return p.scheme or \"http\", p.hostname, p.port\n"
  },
  {
    "path": "openpype/hosts/fusion/vendor/urllib3/util/wait.py",
    "content": "import errno\nimport select\nimport sys\nfrom functools import partial\n\ntry:\n    from time import monotonic\nexcept ImportError:\n    from time import time as monotonic\n\n__all__ = [\"NoWayToWaitForSocketError\", \"wait_for_read\", \"wait_for_write\"]\n\n\nclass NoWayToWaitForSocketError(Exception):\n    pass\n\n\n# How should we wait on sockets?\n#\n# There are two types of APIs you can use for waiting on sockets: the fancy\n# modern stateful APIs like epoll/kqueue, and the older stateless APIs like\n# select/poll. The stateful APIs are more efficient when you have a lots of\n# sockets to keep track of, because you can set them up once and then use them\n# lots of times. But we only ever want to wait on a single socket at a time\n# and don't want to keep track of state, so the stateless APIs are actually\n# more efficient. So we want to use select() or poll().\n#\n# Now, how do we choose between select() and poll()? On traditional Unixes,\n# select() has a strange calling convention that makes it slow, or fail\n# altogether, for high-numbered file descriptors. The point of poll() is to fix\n# that, so on Unixes, we prefer poll().\n#\n# On Windows, there is no poll() (or at least Python doesn't provide a wrapper\n# for it), but that's OK, because on Windows, select() doesn't have this\n# strange calling convention; plain select() works fine.\n#\n# So: on Windows we use select(), and everywhere else we use poll(). We also\n# fall back to select() in case poll() is somehow broken or missing.\n\nif sys.version_info >= (3, 5):\n    # Modern Python, that retries syscalls by default\n    def _retry_on_intr(fn, timeout):\n        return fn(timeout)\n\n\nelse:\n    # Old and broken Pythons.\n    def _retry_on_intr(fn, timeout):\n        if timeout is None:\n            deadline = float(\"inf\")\n        else:\n            deadline = monotonic() + timeout\n\n        while True:\n            try:\n                return fn(timeout)\n            # OSError for 3 <= pyver < 3.5, select.error for pyver <= 2.7\n            except (OSError, select.error) as e:\n                # 'e.args[0]' incantation works for both OSError and select.error\n                if e.args[0] != errno.EINTR:\n                    raise\n                else:\n                    timeout = deadline - monotonic()\n                    if timeout < 0:\n                        timeout = 0\n                    if timeout == float(\"inf\"):\n                        timeout = None\n                    continue\n\n\ndef select_wait_for_socket(sock, read=False, write=False, timeout=None):\n    if not read and not write:\n        raise RuntimeError(\"must specify at least one of read=True, write=True\")\n    rcheck = []\n    wcheck = []\n    if read:\n        rcheck.append(sock)\n    if write:\n        wcheck.append(sock)\n    # When doing a non-blocking connect, most systems signal success by\n    # marking the socket writable. Windows, though, signals success by marked\n    # it as \"exceptional\". We paper over the difference by checking the write\n    # sockets for both conditions. (The stdlib selectors module does the same\n    # thing.)\n    fn = partial(select.select, rcheck, wcheck, wcheck)\n    rready, wready, xready = _retry_on_intr(fn, timeout)\n    return bool(rready or wready or xready)\n\n\ndef poll_wait_for_socket(sock, read=False, write=False, timeout=None):\n    if not read and not write:\n        raise RuntimeError(\"must specify at least one of read=True, write=True\")\n    mask = 0\n    if read:\n        mask |= select.POLLIN\n    if write:\n        mask |= select.POLLOUT\n    poll_obj = select.poll()\n    poll_obj.register(sock, mask)\n\n    # For some reason, poll() takes timeout in milliseconds\n    def do_poll(t):\n        if t is not None:\n            t *= 1000\n        return poll_obj.poll(t)\n\n    return bool(_retry_on_intr(do_poll, timeout))\n\n\ndef null_wait_for_socket(*args, **kwargs):\n    raise NoWayToWaitForSocketError(\"no select-equivalent available\")\n\n\ndef _have_working_poll():\n    # Apparently some systems have a select.poll that fails as soon as you try\n    # to use it, either due to strange configuration or broken monkeypatching\n    # from libraries like eventlet/greenlet.\n    try:\n        poll_obj = select.poll()\n        _retry_on_intr(poll_obj.poll, 0)\n    except (AttributeError, OSError):\n        return False\n    else:\n        return True\n\n\ndef wait_for_socket(*args, **kwargs):\n    # We delay choosing which implementation to use until the first time we're\n    # called. We could do it at import time, but then we might make the wrong\n    # decision if someone goes wild with monkeypatching select.poll after\n    # we're imported.\n    global wait_for_socket\n    if _have_working_poll():\n        wait_for_socket = poll_wait_for_socket\n    elif hasattr(select, \"select\"):\n        wait_for_socket = select_wait_for_socket\n    else:  # Platform-specific: Appengine.\n        wait_for_socket = null_wait_for_socket\n    return wait_for_socket(*args, **kwargs)\n\n\ndef wait_for_read(sock, timeout=None):\n    \"\"\"Waits for reading to be available on a given socket.\n    Returns True if the socket is readable, or False if the timeout expired.\n    \"\"\"\n    return wait_for_socket(sock, read=True, timeout=timeout)\n\n\ndef wait_for_write(sock, timeout=None):\n    \"\"\"Waits for writing to be available on a given socket.\n    Returns True if the socket is readable, or False if the timeout expired.\n    \"\"\"\n    return wait_for_socket(sock, write=True, timeout=timeout)\n"
  },
  {
    "path": "openpype/hosts/harmony/__init__.py",
    "content": "from .addon import (\n    HARMONY_HOST_DIR,\n    HarmonyAddon,\n)\n\n\n__all__ = (\n    \"HARMONY_HOST_DIR\",\n    \"HarmonyAddon\",\n)\n"
  },
  {
    "path": "openpype/hosts/harmony/addon.py",
    "content": "import os\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nHARMONY_HOST_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass HarmonyAddon(OpenPypeModule, IHostAddon):\n    name = \"harmony\"\n    host_name = \"harmony\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        \"\"\"Modify environments to contain all required for implementation.\"\"\"\n        openharmony_path = os.path.join(\n            HARMONY_HOST_DIR, \"vendor\", \"OpenHarmony\"\n        )\n        # TODO check if is already set? What to do if is already set?\n        env[\"LIB_OPENHARMONY_PATH\"] = openharmony_path\n\n    def get_workfile_extensions(self):\n        return [\".zip\"]\n"
  },
  {
    "path": "openpype/hosts/harmony/api/README.md",
    "content": "# Harmony Integration\n\n## Setup\n\nThe easiest way to setup for using Toon Boom Harmony is to use the built-in launch:\n\n```\npython -c \"import openpype.hosts.harmony.api as harmony;harmony.launch(\"path/to/harmony/executable\")\"\n```\n\nCommunication with Harmony happens with a server/client relationship where the server is in the Python process and the client is in the Harmony process. Messages between Python and Harmony are required to be dictionaries, which are serialized to strings:\n```\n+------------+\n|            |\n|   Python   |\n|   Process  |\n|            |\n| +--------+ |\n| |        | |\n| |  Main  | |\n| | Thread | |\n| |        | |\n| +----^---+ |\n|     ||     |\n|     ||     |\n| +---v----+ |     +---------+\n| |        | |     |         |\n| | Server +-------> Harmony |\n| | Thread <-------+ Process |\n| |        | |     |         |\n| +--------+ |     +---------+\n+------------+\n```\n\nServer/client now uses stricter protocol to handle communication. This is necessary because of precise control over data passed between server/client. Each message is prepended with 6 bytes:\n```\n| A | H | 0x00 | 0x00 | 0x00 | 0x00 | ...\n\n```\nFirst two bytes are *magic* bytes stands for **A**valon **H**armony. Next four bytes hold length of the message `...` encoded as 32bit unsigned integer. This way we know how many bytes to read from the socket and if we need more or we need to parse multiple messages.\n\n\n## Usage\n\nThe integration creates an `Openpype` menu entry where all related tools are located.\n\n**NOTE: Menu creation can be temperamental. The best way is to launch Harmony and do nothing else until Harmony is fully launched.**\n\n### Work files\n\nBecause Harmony projects are directories, this integration uses `.zip` as work file extension. Internally the project directories are stored under `[User]/.avalon/harmony`. Whenever the user saves the `.xstage` file, the integration zips up the project directory and moves it to the Avalon project path. Zipping and moving happens in the background.\n\n### Show Workfiles on launch\n\nYou can show the Workfiles app when Harmony launches by setting environment variable `AVALON_HARMONY_WORKFILES_ON_LAUNCH=1`.\n\n## Developing\n\n### Low level messaging\nTo send from Python to Harmony you can use the exposed method:\n```python\nimport openpype.hosts.harmony.api as harmony\nfrom uuid import uuid4\n\n\nfunc = \"\"\"function %s_hello(person)\n{\n  return (\"Hello \" + person + \"!\");\n}\n%s_hello\n\"\"\" % (uuid4(), uuid4())\nprint(harmony.send({\"function\": func, \"args\": [\"Python\"]})[\"result\"])\n```\n**NOTE:** Its important to declare the function at the end of the function string. You can have multiple functions within your function string, but the function declared at the end is what gets executed.\n\nTo send a function with multiple arguments its best to declare the arguments within the function:\n```python\nimport openpype.hosts.harmony.api as harmony\nfrom uuid import uuid4\n\nsignature = str(uuid4()).replace(\"-\", \"_\")\nfunc = \"\"\"function %s_hello(args)\n{\n  var greeting = args[0];\n  var person = args[1];\n  return (greeting + \" \" + person + \"!\");\n}\n%s_hello\n\"\"\" % (signature, signature)\nprint(harmony.send({\"function\": func, \"args\": [\"Hello\", \"Python\"]})[\"result\"])\n```\n\n### Caution\n\nWhen naming your functions be aware that they are executed in global scope. They can potentially clash with Harmony own function and object names.\nFor example `func` is already existing Harmony object. When you call your function `func` it will overwrite in global scope the one from Harmony, causing\nerratic behavior of Harmony. Openpype is prefixing those function names with [UUID4](https://docs.python.org/3/library/uuid.html) making chance of such clash minimal.\nSee above examples how that works. This will result in function named `38dfcef0_a6d7_4064_8069_51fe99ab276e_hello()`.\nYou can find list of Harmony object and function in Harmony documentation.\n\n### Higher level (recommended)\n\nInstead of sending functions directly to Harmony, it is more efficient and safe to just add your code to `js/PypeHarmony.js` or utilize `{\"script\": \"...\"}` method.\n\n#### Extending PypeHarmony.js\n\nAdd your function to `PypeHarmony.js`. For example:\n\n```javascript\nPypeHarmony.myAwesomeFunction = function() {\n  someCoolStuff();\n};\n```\nThen you can call that javascript code from your Python like:\n\n```Python\nimport openpype.hosts.harmony.api as harmony\n\nharmony.send({\"function\": \"PypeHarmony.myAwesomeFunction\"});\n\n```\n\n#### Using Script method\n\nYou can also pass whole scripts into harmony and call their functions later as needed.\n\nFor example, you have bunch of javascript files:\n\n```javascript\n/* Master.js */\n\nvar Master = {\n  Foo = {};\n  Boo = {};\n};\n\n/* FileA.js */\nvar Foo = function() {};\n\nFoo.prototype.A = function() {\n  someAStuff();\n}\n\n// This will construct object Foo and add it to Master namespace.\nMaster.Foo = new Foo();\n\n/* FileB.js */\nvar Boo = function() {};\n\nBoo.prototype.B = function() {\n  someBStuff();\n}\n\n// This will construct object Boo and add it to Master namespace.\nMaster.Boo = new Boo();\n```\n\nNow in python, just read all those files and send them to Harmony.\n\n```python\nfrom pathlib import Path\nimport openpype.hosts.harmony.api as harmony\n\npath_to_js = Path('/path/to/my/js')\nscript_to_send = \"\"\n\nfor file in path_to_js.iterdir():\n  if file.suffix == \".js\":\n    script_to_send += file.read_text()\n\nharmony.send({\"script\": script_to_send})\n\n# and use your code in Harmony\nharmony.send({\"function\": \"Master.Boo.B\"})\n\n```\n\n### Scene Save\nInstead of sending a request to Harmony with `scene.saveAll` please use:\n```python\nimport openpype.hosts.harmony.api as harmony\nharmony.save_scene()\n```\n\n<details>\n  <summary>Click to expand for details on scene save.</summary>\n\n  Because Openpype tools does not deal well with folders for a single entity like a Harmony scene, this integration has implemented to use zip files to encapsulate the Harmony scene folders. Saving scene in Harmony via menu or CTRL+S will not result in producing zip file, only saving it from Workfiles will. This is because\n  zipping process can take some time in which we cannot block user from saving again. If xstage file is changed during zipping process it will produce corrupted zip\n  archive.\n</details>\n\n### Plugin Examples\nThese plugins were made with the [polly config](https://github.com/mindbender-studio/config).\n\n#### Creator Plugin\n```python\nimport openpype.hosts.harmony.api as harmony\nfrom uuid import uuid4\n\n\nclass CreateComposite(harmony.Creator):\n    \"\"\"Composite node for publish.\"\"\"\n\n    name = \"compositeDefault\"\n    label = \"Composite\"\n    family = \"mindbender.template\"\n\n    def __init__(self, *args, **kwargs):\n        super(CreateComposite, self).__init__(*args, **kwargs)\n```\n\nThe creator plugin can be configured to use other node types. For example here is a write node creator:\n```python\nimport openpype.hosts.harmony.api as harmony\n\n\nclass CreateRender(harmony.Creator):\n    \"\"\"Composite node for publishing renders.\"\"\"\n\n    name = \"writeDefault\"\n    label = \"Write\"\n    family = \"mindbender.imagesequence\"\n    node_type = \"WRITE\"\n\n    def __init__(self, *args, **kwargs):\n        super(CreateRender, self).__init__(*args, **kwargs)\n\n    def setup_node(self, node):\n        signature = str(uuid4()).replace(\"-\", \"_\")\n        func = \"\"\"function %s_func(args)\n        {\n            node.setTextAttr(args[0], \"DRAWING_TYPE\", 1, \"PNG4\");\n        }\n        %s_func\n        \"\"\" % (signature, signature)\n        harmony.send(\n            {\"function\": func, \"args\": [node]}\n        )\n```\n\n#### Collector Plugin\n```python\nimport pyblish.api\nimport openpype.hosts.harmony.api as harmony\n\n\nclass CollectInstances(pyblish.api.ContextPlugin):\n    \"\"\"Gather instances by nodes metadata.\n\n    This collector takes into account assets that are associated with\n    a composite node and marked with a unique identifier;\n\n    Identifier:\n        id (str): \"pyblish.avalon.instance\"\n    \"\"\"\n\n    label = \"Instances\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"harmony\"]\n\n    def process(self, context):\n        nodes = harmony.send(\n            {\"function\": \"node.getNodes\", \"args\": [[\"COMPOSITE\"]]}\n        )[\"result\"]\n\n        for node in nodes:\n            data = harmony.read(node)\n\n            # Skip non-tagged nodes.\n            if not data:\n                continue\n\n            # Skip containers.\n            if \"container\" in data[\"id\"]:\n                continue\n\n            instance = context.create_instance(node.split(\"/\")[-1])\n            instance.append(node)\n            instance.data.update(data)\n\n            # Produce diagnostic message for any graphical\n            # user interface interested in visualising it.\n            self.log.info(\"Found: \\\"%s\\\" \" % instance.data[\"name\"])\n```\n\n#### Extractor Plugin\n```python\nimport os\n\nimport pyblish.api\nimport openpype.hosts.harmony.api as harmony\n\nimport clique\n\n\nclass ExtractImage(pyblish.api.InstancePlugin):\n    \"\"\"Produce a flattened image file from instance.\n    This plug-in only takes into account the nodes connected to the composite.\n    \"\"\"\n    label = \"Extract Image Sequence\"\n    order = pyblish.api.ExtractorOrder\n    hosts = [\"harmony\"]\n    families = [\"mindbender.imagesequence\"]\n\n    def process(self, instance):\n        project_path = harmony.send(\n            {\"function\": \"scene.currentProjectPath\"}\n        )[\"result\"]\n\n        # Store reference for integration\n        if \"files\" not in instance.data:\n            instance.data[\"files\"] = list()\n\n        # Store display source node for later.\n        display_node = \"Top/Display\"\n        signature = str(uuid4()).replace(\"-\", \"_\")\n        func = \"\"\"function %s_func(display_node)\n        {\n            var source_node = null;\n            if (node.isLinked(display_node, 0))\n            {\n                source_node = node.srcNode(display_node, 0);\n                node.unlink(display_node, 0);\n            }\n            return source_node\n        }\n        %s_func\n        \"\"\" % (signature, signature)\n        display_source_node = harmony.send(\n            {\"function\": func, \"args\": [display_node]}\n        )[\"result\"]\n\n        # Perform extraction\n        path = os.path.join(\n            os.path.normpath(\n                project_path\n            ).replace(\"\\\\\", \"/\"),\n            instance.data[\"name\"]\n        )\n        if not os.path.exists(path):\n            os.makedirs(path)\n\n        render_func = \"\"\"function frameReady(frame, celImage)\n        {{\n          var path = \"{path}/{filename}\" + frame + \".png\";\n          celImage.imageFileAs(path, \"\", \"PNG4\");\n        }}\n        function %s_func(composite_node)\n        {{\n            node.link(composite_node, 0, \"{display_node}\", 0);\n            render.frameReady.connect(frameReady);\n            render.setRenderDisplay(\"{display_node}\");\n            render.renderSceneAll();\n            render.frameReady.disconnect(frameReady);\n        }}\n        %s_func\n        \"\"\" % (signature, signature)\n        restore_func = \"\"\"function %s_func(args)\n        {\n            var display_node = args[0];\n            var display_source_node = args[1];\n            if (node.isLinked(display_node, 0))\n            {\n                node.unlink(display_node, 0);\n            }\n            node.link(display_source_node, 0, display_node, 0);\n        }\n        %s_func\n        \"\"\" % (signature, signature)\n\n        with harmony.maintained_selection():\n            self.log.info(\"Extracting %s\" % str(list(instance)))\n\n            harmony.send(\n                {\n                    \"function\": render_func.format(\n                        path=path.replace(\"\\\\\", \"/\"),\n                        filename=os.path.basename(path),\n                        display_node=display_node\n                    ),\n                    \"args\": [instance[0]]\n                }\n            )\n\n            # Restore display.\n            if display_source_node:\n                harmony.send(\n                    {\n                        \"function\": restore_func,\n                        \"args\": [display_node, display_source_node]\n                    }\n                )\n\n        files = os.listdir(path)\n        collections, remainder = clique.assemble(files, minimum_items=1)\n        assert not remainder, (\n            \"There shouldn't have been a remainder for '%s': \"\n            \"%s\" % (instance[0], remainder)\n        )\n        assert len(collections) == 1, (\n            \"There should only be one image sequence in {}. Found: {}\".format(\n                path, len(collections)\n            )\n        )\n\n        data = {\n            \"subset\": collections[0].head,\n            \"isSeries\": True,\n            \"stagingDir\": path,\n            \"files\": list(collections[0]),\n        }\n        instance.data.update(data)\n\n        self.log.info(\"Extracted {instance} to {path}\".format(**locals()))\n```\n\n#### Loader Plugin\n```python\nimport os\n\nimport openpype.hosts.harmony.api as harmony\n\nsignature = str(uuid4()).replace(\"-\", \"_\")\ncopy_files = \"\"\"function copyFile(srcFilename, dstFilename)\n{\n    var srcFile = new PermanentFile(srcFilename);\n    var dstFile = new PermanentFile(dstFilename);\n    srcFile.copy(dstFile);\n}\n\"\"\"\n\nimport_files = \"\"\"function %s_import_files()\n{\n  var PNGTransparencyMode = 0;  // Premultiplied with Black\n  var TGATransparencyMode = 0;  // Premultiplied with Black\n  var SGITransparencyMode = 0;  // Premultiplied with Black\n  var LayeredPSDTransparencyMode = 1;  // Straight\n  var FlatPSDTransparencyMode = 2;  // Premultiplied with White\n\n  function getUniqueColumnName( column_prefix )\n  {\n      var suffix = 0;\n      // finds if unique name for a column\n      var column_name = column_prefix;\n      while(suffix < 2000)\n      {\n          if(!column.type(column_name))\n          break;\n\n          suffix = suffix + 1;\n          column_name = column_prefix + \"_\" + suffix;\n      }\n      return column_name;\n  }\n\n  function import_files(args)\n  {\n      var root = args[0];\n      var files = args[1];\n      var name = args[2];\n      var start_frame = args[3];\n\n      var vectorFormat = null;\n      var extension = null;\n      var filename = files[0];\n\n      var pos = filename.lastIndexOf(\".\");\n      if( pos < 0 )\n          return null;\n\n      extension = filename.substr(pos+1).toLowerCase();\n\n      if(extension == \"jpeg\")\n          extension = \"jpg\";\n      if(extension == \"tvg\")\n      {\n          vectorFormat = \"TVG\"\n          extension =\"SCAN\"; // element.add() will use this.\n      }\n\n      var elemId = element.add(\n          name,\n          \"BW\",\n          scene.numberOfUnitsZ(),\n          extension.toUpperCase(),\n          vectorFormat\n      );\n      if (elemId == -1)\n      {\n          // hum, unknown file type most likely -- let's skip it.\n          return null; // no read to add.\n      }\n\n      var uniqueColumnName = getUniqueColumnName(name);\n      column.add(uniqueColumnName , \"DRAWING\");\n      column.setElementIdOfDrawing(uniqueColumnName, elemId);\n\n      var read = node.add(root, name, \"READ\", 0, 0, 0);\n      var transparencyAttr = node.getAttr(\n          read, frame.current(), \"READ_TRANSPARENCY\"\n      );\n      var opacityAttr = node.getAttr(read, frame.current(), \"OPACITY\");\n      transparencyAttr.setValue(true);\n      opacityAttr.setValue(true);\n\n      var alignmentAttr = node.getAttr(read, frame.current(), \"ALIGNMENT_RULE\");\n      alignmentAttr.setValue(\"ASIS\");\n\n      var transparencyModeAttr = node.getAttr(\n          read, frame.current(), \"applyMatteToColor\"\n      );\n      if (extension == \"png\")\n          transparencyModeAttr.setValue(PNGTransparencyMode);\n      if (extension == \"tga\")\n          transparencyModeAttr.setValue(TGATransparencyMode);\n      if (extension == \"sgi\")\n          transparencyModeAttr.setValue(SGITransparencyMode);\n      if (extension == \"psd\")\n          transparencyModeAttr.setValue(FlatPSDTransparencyMode);\n\n      node.linkAttr(read, \"DRAWING.ELEMENT\", uniqueColumnName);\n\n      // Create a drawing for each file.\n      for( var i =0; i <= files.length - 1; ++i)\n      {\n          timing = start_frame + i\n          // Create a drawing drawing, 'true' indicate that the file exists.\n          Drawing.create(elemId, timing, true);\n          // Get the actual path, in tmp folder.\n          var drawingFilePath = Drawing.filename(elemId, timing.toString());\n          copyFile( files[i], drawingFilePath );\n\n          column.setEntry(uniqueColumnName, 1, timing, timing.toString());\n      }\n      return read;\n  }\n  import_files();\n}\n%s_import_files\n\"\"\" % (signature, signature)\n\nreplace_files = \"\"\"function %s_replace_files(args)\n{\n    var files = args[0];\n    var _node = args[1];\n    var start_frame = args[2];\n\n    var _column = node.linkedColumn(_node, \"DRAWING.ELEMENT\");\n\n    // Delete existing drawings.\n    var timings = column.getDrawingTimings(_column);\n    for( var i =0; i <= timings.length - 1; ++i)\n    {\n        column.deleteDrawingAt(_column, parseInt(timings[i]));\n    }\n\n    // Create new drawings.\n    for( var i =0; i <= files.length - 1; ++i)\n    {\n        timing = start_frame + i\n        // Create a drawing drawing, 'true' indicate that the file exists.\n        Drawing.create(node.getElementId(_node), timing, true);\n        // Get the actual path, in tmp folder.\n        var drawingFilePath = Drawing.filename(\n            node.getElementId(_node), timing.toString()\n        );\n        copyFile( files[i], drawingFilePath );\n\n        column.setEntry(_column, 1, timing, timing.toString());\n    }\n}\n%s_replace_files\n\"\"\" % (signature, signature)\n\n\nclass ImageSequenceLoader(load.LoaderPlugin):\n    \"\"\"Load images\n    Stores the imported asset in a container named after the asset.\n    \"\"\"\n    families = [\"mindbender.imagesequence\"]\n    representations = [\"*\"]\n\n    def load(self, context, name=None, namespace=None, data=None):\n        files = []\n        for f in context[\"version\"][\"data\"][\"files\"]:\n            files.append(\n                os.path.join(\n                    context[\"version\"][\"data\"][\"stagingDir\"], f\n                ).replace(\"\\\\\", \"/\")\n            )\n\n        read_node = harmony.send(\n            {\n                \"function\": copy_files + import_files,\n                \"args\": [\"Top\", files, context[\"version\"][\"data\"][\"subset\"], 1]\n            }\n        )[\"result\"]\n\n        self[:] = [read_node]\n\n        return harmony.containerise(\n            name,\n            namespace,\n            read_node,\n            context,\n            self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n        node = container.pop(\"node\")\n\n        project_name = get_current_project_name()\n        version = get_version_by_id(project_name, representation[\"parent\"])\n        files = []\n        for f in version[\"data\"][\"files\"]:\n            files.append(\n                os.path.join(\n                    version[\"data\"][\"stagingDir\"], f\n                ).replace(\"\\\\\", \"/\")\n            )\n\n        harmony.send(\n            {\n                \"function\": copy_files + replace_files,\n                \"args\": [files, node, 1]\n            }\n        )\n\n        harmony.imprint(\n            node, {\"representation\": str(representation[\"_id\"])}\n        )\n\n    def remove(self, container):\n        node = container.pop(\"node\")\n        signature = str(uuid4()).replace(\"-\", \"_\")\n        func = \"\"\"function %s_deleteNode(_node)\n        {\n            node.deleteNode(_node, true, true);\n        }\n        %_deleteNode\n        \"\"\" % (signature, signature)\n        harmony.send(\n            {\"function\": func, \"args\": [node]}\n        )\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n```\n\n## Resources\n- https://github.com/diegogarciahuerta/tk-harmony\n- https://github.com/cfourney/OpenHarmony\n- [Toon Boom Discord](https://discord.gg/syAjy4H)\n- [Toon Boom TD](https://discord.gg/yAjyQtZ)\n"
  },
  {
    "path": "openpype/hosts/harmony/api/TB_sceneOpened.js",
    "content": "/* global QTcpSocket, QByteArray, QDataStream, QTimer, QTextCodec, QIODevice, QApplication, include */\n/* global QTcpSocket, QByteArray, QDataStream, QTimer, QTextCodec, QIODevice, QApplication, include */\n/*\nAvalon Harmony Integration - Client\n-----------------------------------\n\nThis script implements client communication with Avalon server to bridge\ngap between Python and QtScript.\n\n*/\n/* jshint proto: true */\nvar LD_OPENHARMONY_PATH = System.getenv('LIB_OPENHARMONY_PATH');\nLD_OPENHARMONY_PATH = LD_OPENHARMONY_PATH + '/openHarmony.js';\nLD_OPENHARMONY_PATH = LD_OPENHARMONY_PATH.replace(/\\\\/g, \"/\");\ninclude(LD_OPENHARMONY_PATH);\n//this.__proto__['$'] = $;\n\nfunction Client() {\n    var self = this;\n    /** socket */\n    self.socket = new QTcpSocket(this);\n    /** receiving data buffer */\n    self.received = '';\n    self.messageId = 1;\n    self.buffer = new QByteArray();\n    self.waitingForData = 0;\n\n\n    /**\n     * pack number into bytes.\n     * @function\n     * @param   {int} num 32 bit integer\n     * @return  {string}\n     */\n    self.pack = function(num) {\n        var ascii='';\n        for (var i = 3; i >= 0; i--) {\n          var hex = ((num >> (8 * i)) & 255).toString(16);\n          if (hex.length < 2){\n            ascii += \"0\";\n          }\n          ascii += hex;\n        }\n        return ascii;\n    };\n\n    /**\n     * unpack number from string.\n     * @function\n     * @param   {string} numString bytes to unpack\n     * @return  {int} 32bit unsigned integer.\n     */\n    self.unpack = function(numString) {\n        var result=0;\n        for (var i = 3; i >= 0; i--) {\n            result += numString.charCodeAt(3 - i) << (8 * i);\n        }\n        return result;\n    };\n\n    /**\n     * prettify json for easier debugging\n     * @function\n     * @param   {object} json json to process\n     * @return  {string} prettified json string\n     */\n    self.prettifyJson = function(json) {\n        var jsonString = JSON.stringify(json);\n        return JSON.stringify(JSON.parse(jsonString), null, 2);\n    };\n\n    /**\n     * Log message in debug level.\n     * @function\n     * @param {string} data - message\n     */\n    self.logDebug = function(data) {\n        var message = typeof(data.message) != 'undefined' ? data.message : data;\n        MessageLog.trace('(DEBUG): ' + message.toString());\n    };\n\n    /**\n     * Log message in info level.\n     * @function\n     * @param {string} data - message\n     */\n    self.logInfo = function(data) {\n        var message = typeof(data.message) != 'undefined' ? data.message : data;\n        MessageLog.trace('(DEBUG): ' + message.toString());\n    };\n\n    /**\n     * Log message in warning level.\n     * @function\n     * @param {string} data - message\n     */\n    self.logWarning = function(data) {\n        var message = typeof(data.message) != 'undefined' ? data.message : data;\n        MessageLog.trace('(INFO): ' + message.toString());\n    };\n\n    /**\n     * Log message in error level.\n     * @function\n     * @param {string} data - message\n     */\n    self.logError = function(data) {\n        var message = typeof(data.message) != 'undefined' ? data.message : data;\n        MessageLog.trace('(ERROR): ' +message.toString());\n    };\n\n    /**\n     * Show message in Harmony GUI as popup window.\n     * @function\n     * @param {string} msg - message\n     */\n    self.showMessage = function(msg) {\n        MessageBox.information(msg);\n    };\n\n    /**\n     * Implement missing setTimeout() in Harmony.\n     * This calls once given function after some interval in milliseconds.\n     * @function\n     * @param {function} fc       function to call.\n     * @param {int}      interval interval in milliseconds.\n     * @param {boolean}  single   behave as setTimeout or setInterval.\n     */\n    self.setTimeout = function(fc, interval, single) {\n        var timer = new QTimer();\n        if (!single) {\n            timer.singleShot = true; // in-case if setTimout and false in-case of setInterval\n        } else {\n            timer.singleShot = single;\n        }\n        timer.interval = interval; // set the time in milliseconds\n        timer.singleShot = true; // in-case if setTimout and false in-case of setInterval\n        timer.timeout.connect(this, function(){\n            fc.call();\n        });\n        timer.start();\n    };\n\n    /**\n     * Process received request. This will eval received function and produce\n     * results.\n     * @function\n     * @param  {object} request - received request JSON\n     * @return {object} result of evaled function.\n     */\n    self.processRequest = function(request) {\n        var mid = request.message_id;\n        if (typeof request.reply !== 'undefined') {\n            self.logDebug('['+ mid +'] *** received reply.');\n            return;\n        }\n        self.logDebug('['+ mid +'] - Processing: ' + self.prettifyJson(request));\n        var result = null;\n\n        if (typeof request.script !== 'undefined') {\n            self.logDebug('[' + mid + '] Injecting script.');\n            try {\n                eval.call(null, request.script);\n            } catch (error) {\n                self.logError(error);\n            }\n        } else if (typeof request[\"function\"] !== 'undefined') {\n            try {\n                var _func = eval.call(null, request[\"function\"]);\n\n                if (request.args == null) {\n                    result = _func();\n                } else {\n                    result = _func(request.args);\n                }\n            } catch (error) {\n                result = 'Error processing request.\\n' +\n                         'Request:\\n' +\n                         self.prettifyJson(request) + '\\n' +\n                         'Error:\\n' + error;\n            }\n        } else {\n            self.logError('Command type not implemented.');\n        }\n\n        return result;\n    };\n\n    /**\n     * This gets called when socket received new data.\n     * @function\n     */\n    self.onReadyRead = function() {\n        var currentSize = self.buffer.size();\n        self.logDebug('--- Receiving data ( '+ currentSize + ' )...');\n        var newData = self.socket.readAll();\n        var newSize = newData.size();\n        self.buffer.append(newData);\n        self.logDebug('  - got ' + newSize + ' bytes of new data.');\n        self.processBuffer();\n    };\n\n    /**\n     * Process data received in buffer.\n     * This detects messages by looking for header and message length.\n     * @function\n     */\n    self.processBuffer = function() {\n        var length = self.waitingForData;\n        if (self.waitingForData == 0) {\n            // read header from the buffer and remove it\n            var header_data = self.buffer.mid(0, 6);\n            self.buffer = self.buffer.remove(0, 6);\n\n            // convert header to string\n            var header = '';\n            for (var i = 0; i < header_data.size(); ++i) {\n                // data in QByteArray come out as signed bytes.\n                var unsigned = header_data.at(i) & 0xff;\n                header = header.concat(String.fromCharCode(unsigned));\n            }\n\n            // skip 'AH' and read only length, unpack it to integer\n            header = header.substr(2);\n            length = self.unpack(header);\n        }\n\n        var data = self.buffer.mid(0, length);\n        self.logDebug('--- Expected: ' + length + ' | Got: ' + data.size());\n        if (length > data.size()) {\n            // we didn't received whole message.\n            self.waitingForData = length;\n            self.logDebug('... waiting for more data (' + length + ') ...');\n            return;\n        }\n        self.waitingForData = 0;\n        self.buffer.remove(0, length);\n\n        for (var j = 0; j < data.size(); ++j) {\n            self.received = self.received.concat(String.fromCharCode(data.at(j)));\n        }\n\n        // self.logDebug('--- Received: ' + self.received);\n        var to_parse = self.received;\n        var request = JSON.parse(to_parse);\n        var mid = request.message_id;\n        // self.logDebug('[' + mid + '] - Request: ' + '\\n' + JSON.stringify(request));\n        self.logDebug('[' + mid + '] Received.');\n\n        request.result = self.processRequest(request);\n        self.logDebug('[' + mid + '] Processing done.');\n        self.received = '';\n\n        if (request.reply !== true) {\n            request.reply = true;\n            self.logDebug('[' + mid + '] Replying.');\n            self._send(JSON.stringify(request));\n        }\n\n        if ((length < data.size()) || (length < self.buffer.size())) {\n            // we've received more data.\n            self.logDebug('--- Got more data to process ...');\n            self.processBuffer();\n        }\n    };\n\n    /**\n     * Run when Harmony connects to server.\n     * @function\n     */\n    self.onConnected = function() {\n        self.logDebug('Connected to server ...');\n        self.lock = false;\n        self.socket.readyRead.connect(self.onReadyRead);\n        var app = QCoreApplication.instance();\n\n        app.avalonClient.send(\n            {\n                'module': 'openpype.lib',\n                'method': 'emit_event',\n                'args': ['application.launched']\n            }, false);\n    };\n\n    self._send = function(message) {\n      /** Harmony 21.1 doesn't have QDataStream anymore.\n\n      This means we aren't able to write bytes into QByteArray so we had\n      modify how content length is sent do the server.\n      Content length is sent as string of 8 char convertible into integer\n      (instead of 0x00000001[4 bytes] > \"000000001\"[8 bytes]) */\n      var codec_name = new QByteArray().append(\"UTF-8\");\n\n      var codec = QTextCodec.codecForName(codec_name);\n      var msg = codec.fromUnicode(message);\n      var l = msg.size();\n      var header = new QByteArray().append('AH').append(self.pack(l));\n      var coded = msg.prepend(header);\n\n      self.socket.write(coded);\n      self.logDebug('Sent.');\n    };\n\n    self.waitForLock = function() {\n        if (self._lock === false) {\n            self.logDebug('>>> Unlocking ...');\n            return;\n        } else {\n            self.logDebug('Setting timer.');\n            self.setTimeout(self.waitForLock, 300);\n        }\n    };\n\n    /**\n     * Send request to server.\n     * @param {object} request - json encoded request.\n     */\n    self.send = function(request) {\n        request.message_id = self.messageId;\n        if (typeof request.reply == 'undefined') {\n            self.logDebug(\"[\" + self.messageId + \"] sending:\\n\" + self.prettifyJson(request));\n        }\n        self._send(JSON.stringify(request));\n    };\n\n    /**\n     * Executed on disconnection.\n     */\n    self.onDisconnected = function() {\n        self.socket.close();\n    };\n\n    /**\n     * Disconnect from server.\n     */\n    self.disconnect = function() {\n        self.socket.close();\n    };\n\n    self.socket.connected.connect(self.onConnected);\n    self.socket.disconnected.connect(self.onDisconnected);\n}\n\n/**\n * Entry point, creating Avalon Client.\n */\nfunction start() {\n    var self = this;\n    /** hostname or ip of server - should be localhost */\n    var host = '127.0.0.1';\n    /** port of the server */\n    var port = parseInt(System.getenv('AVALON_HARMONY_PORT'));\n\n    // Attach the client to the QApplication to preserve.\n    var app = QCoreApplication.instance();\n\n    if (app.avalonClient == null) {\n        app.avalonClient = new Client();\n        app.avalonClient.socket.connectToHost(host, port);\n    }\n    var mainWindow = null;\n    var widgets = QApplication.topLevelWidgets();\n    for (var i = 0 ; i < widgets.length; i++) {\n      if (widgets[i] instanceof QMainWindow){\n          mainWindow = widgets[i];\n      }\n    }\n    var menuBar = mainWindow.menuBar();\n    var actions = menuBar.actions();\n    app.avalonMenu = null;\n\n    for (var i = 0 ; i < actions.length; i++) {\n        label = System.getenv('AVALON_LABEL');\n        if (actions[i].text == label) {\n            app.avalonMenu = true;\n        }\n    }\n\n    var menu = null;\n    if (app.avalonMenu == null) {\n        menu = menuBar.addMenu(System.getenv('AVALON_LABEL'));\n    }\n    // menu = menuBar.addMenu('Avalon');\n\n    /**\n     * Show creator\n     */\n    self.onCreator = function() {\n        app.avalonClient.send({\n            'module': 'openpype.hosts.harmony.api.lib',\n            'method': 'show',\n            'args': ['creator']\n        }, false);\n    };\n\n    var action = menu.addAction('Create...');\n    action.triggered.connect(self.onCreator);\n\n\n    /**\n     * Show Workfiles\n     */\n    self.onWorkfiles = function() {\n        app.avalonClient.send({\n            'module': 'openpype.hosts.harmony.api.lib',\n            'method': 'show',\n            'args': ['workfiles']\n        }, false);\n    };\n    if (app.avalonMenu == null) {\n        action = menu.addAction('Workfiles...');\n        action.triggered.connect(self.onWorkfiles);\n    }\n\n    /**\n     * Show Loader\n     */\n    self.onLoad = function() {\n        app.avalonClient.send({\n            'module': 'openpype.hosts.harmony.api.lib',\n            'method': 'show',\n            'args': ['loader']\n        }, false);\n    };\n    // add Loader item to menu\n    if (app.avalonMenu == null) {\n        action = menu.addAction('Load...');\n        action.triggered.connect(self.onLoad);\n    }\n\n    /**\n     * Show Publisher\n     */\n    self.onPublish = function() {\n        app.avalonClient.send({\n            'module': 'openpype.hosts.harmony.api.lib',\n            'method': 'show',\n            'args': ['publish']\n        }, false);\n    };\n    // add Publisher item to menu\n    if (app.avalonMenu == null) {\n        action = menu.addAction('Publish...');\n        action.triggered.connect(self.onPublish);\n    }\n\n    /**\n     * Show Scene Manager\n     */\n    self.onManage = function() {\n        app.avalonClient.send({\n            'module': 'openpype.hosts.harmony.api.lib',\n            'method': 'show',\n            'args': ['sceneinventory']\n        }, false);\n    };\n    // add Scene Manager item to menu\n    if (app.avalonMenu == null) {\n        action = menu.addAction('Manage...');\n        action.triggered.connect(self.onManage);\n    }\n\n    /**\n      * Show Subset Manager\n      */\n    self.onSubsetManage = function() {\n        app.avalonClient.send({\n            'module': 'openpype.hosts.harmony.api.lib',\n            'method': 'show',\n            'args': ['subsetmanager']\n        }, false);\n    };\n    // add Subset Manager item to menu\n    if (app.avalonMenu == null) {\n        action = menu.addAction('Subset Manager...');\n        action.triggered.connect(self.onSubsetManage);\n    }\n\n     /**\n      * Set scene settings from DB to the scene\n      */\n    self.onSetSceneSettings = function() {\n          app.avalonClient.send(\n            {\n              \"module\": \"openpype.hosts.harmony.api\",\n              \"method\": \"ensure_scene_settings\",\n              \"args\": []\n            },\n            false\n          );\n    };\n    // add Set Scene Settings\n    if (app.avalonMenu == null) {\n        action = menu.addAction('Set Scene Settings...');\n        action.triggered.connect(self.onSetSceneSettings);\n    }\n\n    /**\n      * Show Experimental dialog\n      */\n    self.onExperimentalTools = function() {\n        app.avalonClient.send({\n            'module': 'openpype.hosts.harmony.api.lib',\n            'method': 'show',\n            'args': ['experimental_tools']\n        }, false);\n    };\n    // add Subset Manager item to menu\n    if (app.avalonMenu == null) {\n        action = menu.addAction('Experimental Tools...');\n        action.triggered.connect(self.onExperimentalTools);\n    }\n\n    // FIXME(antirotor): We need to disable `on_file_changed` now as is wreak\n    // havoc when \"Save\" is called multiple times and zipping didn't finished yet\n    /*\n\n    // Watch scene file for changes.\n    app.onFileChanged = function(path)\n    {\n      var app = QCoreApplication.instance();\n      if (app.avalonOnFileChanged){\n        app.avalonClient.send(\n          {\n            'module': 'avalon.harmony.lib',\n            'method': 'on_file_changed',\n            'args': [path]\n          },\n          false\n        );\n      }\n\n      app.watcher.addPath(path);\n    };\n\n\n\tapp.watcher = new QFileSystemWatcher();\n\tscene_path = scene.currentProjectPath() +\"/\" + scene.currentVersionName() + \".xstage\";\n\tapp.watcher.addPath(scenePath);\n\tapp.watcher.fileChanged.connect(app.onFileChanged);\n  app.avalonOnFileChanged = true;\n  */\n    app.onFileChanged = function(path) {\n        // empty stub\n        return path;\n    };\n}\n\nfunction ensureSceneSettings() {\n  var app = QCoreApplication.instance();\n  app.avalonClient.send(\n    {\n      \"module\": \"openpype.hosts.harmony.api\",\n      \"method\": \"ensure_scene_settings\",\n      \"args\": []\n    },\n    false\n  );\n}\n\nfunction TB_sceneOpened()\n{\n  start();\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/api/__init__.py",
    "content": "\"\"\"Public API\n\nAnything that isn't defined here is INTERNAL and unreliable for external use.\n\n\"\"\"\nfrom .pipeline import (\n    ls,\n    install,\n    list_instances,\n    remove_instance,\n    select_instance,\n    containerise,\n    set_scene_settings,\n    get_asset_settings,\n    ensure_scene_settings,\n    check_inventory,\n    application_launch,\n    export_template,\n    on_pyblish_instance_toggled,\n    inject_avalon_js,\n)\n\nfrom .lib import (\n    launch,\n    maintained_selection,\n    imprint,\n    read,\n    send,\n    maintained_nodes_state,\n    save_scene,\n    save_scene_as,\n    remove,\n    delete_node,\n    find_node_by_name,\n    signature,\n    select_nodes,\n    get_scene_data\n)\n\nfrom .workio import (\n    open_file,\n    save_file,\n    current_file,\n    has_unsaved_changes,\n    file_extensions,\n    work_root\n)\n\n__all__ = [\n    # pipeline\n    \"ls\",\n    \"install\",\n    \"list_instances\",\n    \"remove_instance\",\n    \"select_instance\",\n    \"containerise\",\n    \"set_scene_settings\",\n    \"get_asset_settings\",\n    \"ensure_scene_settings\",\n    \"check_inventory\",\n    \"application_launch\",\n    \"export_template\",\n    \"on_pyblish_instance_toggled\",\n    \"inject_avalon_js\",\n\n    # lib\n    \"launch\",\n    \"maintained_selection\",\n    \"imprint\",\n    \"read\",\n    \"send\",\n    \"maintained_nodes_state\",\n    \"save_scene\",\n    \"save_scene_as\",\n    \"remove\",\n    \"delete_node\",\n    \"find_node_by_name\",\n    \"signature\",\n    \"select_nodes\",\n    \"get_scene_data\",\n\n    # Workfiles API\n    \"open_file\",\n    \"save_file\",\n    \"current_file\",\n    \"has_unsaved_changes\",\n    \"file_extensions\",\n    \"work_root\",\n]\n\n"
  },
  {
    "path": "openpype/hosts/harmony/api/js/.eslintrc.json",
    "content": "{\n    \"env\": {\n        \"browser\": true\n    },\n    \"extends\": \"eslint:recommended\",\n    \"parserOptions\": {\n        \"ecmaVersion\": 3\n    },\n    \"rules\": {\n        \"indent\": [\n            \"error\",\n            4\n        ],\n        \"linebreak-style\": [\n            \"error\",\n            \"unix\"\n        ],\n        \"quotes\": [\n            \"error\",\n            \"single\"\n        ],\n        \"semi\": [\n            \"error\",\n            \"always\"\n        ]\n    },\n    \"globals\": {\n      \"Action\": \"readonly\",\n      \"Backdrop\": \"readonly\",\n      \"Button\": \"readonly\",\n      \"Cel\": \"readonly\",\n      \"Cel3d\": \"readonly\",\n      \"CheckBox\": \"readonly\",\n      \"ColorRGBA\": \"readonly\",\n      \"ComboBox\": \"readonly\",\n      \"DateEdit\": \"readonly\",\n      \"DateEditEnum\": \"readonly\",\n      \"Dialog\": \"readonly\",\n      \"Dir\": \"readonly\",\n      \"DirSpec\": \"readonly\",\n      \"Drawing\": \"readonly\",\n      \"DrawingToolParams\": \"readonly\",\n      \"DrawingTools\": \"readonly\",\n      \"EnvelopeCreator\": \"readonly\",\n      \"ExportVideoDlg\": \"readonly\",\n      \"File\": \"readonly\",\n      \"FileAccess\": \"readonly\",\n      \"FileDialog\": \"readonly\",\n      \"GroupBox\": \"readonly\",\n      \"ImportDrawingDlg\": \"readonly\",\n      \"Input\": \"readonly\",\n      \"KeyModifiers\": \"readonly\",\n      \"Label\": \"readonly\",\n      \"LayoutExports\": \"readonly\",\n      \"LayoutExportsParams\": \"readonly\",\n      \"LineEdit\": \"readonly\",\n      \"Matrix4x4\": \"readonly\",\n      \"MessageBox\": \"readonly\",\n      \"MessageLog\": \"readonly\",\n      \"Model3d\": \"readonly\",\n      \"MovieImport\": \"readonly\",\n      \"NumberEdit\": \"readonly\",\n      \"PaletteManager\": \"readonly\",\n      \"PaletteObjectManager\": \"readonly\",\n      \"PermanentFile\": \"readonly\",\n      \"Point2d\": \"readonly\",\n      \"Point3d\": \"readonly\",\n      \"Process\": \"readonly\",\n      \"Process2\": \"readonly\",\n      \"Quaternion\": \"readonly\",\n      \"QuicktimeExporter\": \"readonly\",\n      \"RadioButton\": \"readonly\",\n      \"RemoteCmd\": \"readonly\",\n      \"Scene\": \"readonly\",\n      \"Settings\": \"readonly\",\n      \"Slider\": \"readonly\",\n      \"SpinBox\": \"readonly\",\n      \"SubnodeData\": \"readonly\",\n      \"System\": \"readonly\",\n      \"TemporaryFile\": \"readonly\",\n      \"TextEdit\": \"readonly\",\n      \"TimeEdit\": \"readonly\",\n      \"Timeline\": \"readonly\",\n      \"ToolProperties\": \"readonly\",\n      \"UiLoader\": \"readonly\",\n      \"Vector2d\": \"readonly\",\n      \"Vector3d\": \"readonly\",\n      \"WebCCExporter\": \"readonly\",\n      \"Workspaces\": \"readonly\",\n      \"__scriptManager__\": \"readonly\",\n      \"__temporaryFileContext__\": \"readonly\",\n      \"about\": \"readonly\",\n      \"column\": \"readonly\",\n      \"compositionOrder\": \"readonly\",\n      \"copyPaste\": \"readonly\",\n      \"deformation\": \"readonly\",\n      \"drawingExport\": \"readonly\",\n      \"element\": \"readonly\",\n      \"exporter\": \"readonly\",\n      \"fileMapper\": \"readonly\",\n      \"frame\": \"readonly\",\n      \"func\": \"readonly\",\n      \"library\": \"readonly\",\n      \"node\": \"readonly\",\n      \"preferences\": \"readonly\",\n      \"render\": \"readonly\",\n      \"scene\": \"readonly\",\n      \"selection\": \"readonly\",\n      \"sound\": \"readonly\",\n      \"specialFolders\": \"readonly\",\n      \"translator\": \"readonly\",\n      \"view\": \"readonly\",\n      \"waypoint\": \"readonly\",\n      \"xsheet\": \"readonly\",\n      \"QCoreApplication\": \"readonly\"\n    }\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/api/js/AvalonHarmony.js",
    "content": "// ***************************************************************************\n// *                        Avalon Harmony Host                                *\n// ***************************************************************************\n\n\n/**\n * @namespace\n * @classdesc AvalonHarmony encapsulate all Avalon related functions.\n */\nvar AvalonHarmony = {};\n\n\n/**\n * Get scene metadata from Harmony.\n * @function\n * @return {object} Scene metadata.\n */\nAvalonHarmony.getSceneData = function() {\n    var metadata = scene.metadata('avalon');\n    if (metadata){\n        return JSON.parse(metadata.value);\n    }else {\n        return {};\n    }\n};\n\n\n/**\n * Set scene metadata to Harmony.\n * @function\n * @param {object} metadata Object containing metadata.\n */\nAvalonHarmony.setSceneData = function(metadata) {\n    scene.setMetadata({\n        'name'       : 'avalon',\n        'type'       : 'string',\n        'creator'    : 'Avalon',\n        'version'    : '1.0',\n        'value'      : JSON.stringify(metadata)\n    });\n};\n\n\n/**\n * Get selected nodes in Harmony.\n * @function\n * @return {array} Selected nodes paths.\n */\nAvalonHarmony.getSelectedNodes = function () {\n    var selectionLength = selection.numberOfNodesSelected();\n    var selectedNodes = [];\n    for (var i = 0 ; i < selectionLength; i++) {\n        selectedNodes.push(selection.selectedNode(i));\n    }\n    return selectedNodes;\n};\n\n\n/**\n * Set selection of nodes.\n * @function\n * @param {array} nodes Arrya containing node paths to add to selection.\n */\nAvalonHarmony.selectNodes = function(nodes) {\n    selection.clearSelection();\n    for (var i = 0 ; i < nodes.length; i++) {\n        selection.addNodeToSelection(nodes[i]);\n    }\n};\n\n\n/**\n * Is node enabled?\n * @function\n * @param {string} node Node path.\n * @return {boolean} state\n */\nAvalonHarmony.isEnabled = function(node) {\n    return node.getEnable(node);\n};\n\n\n/**\n * Are nodes enabled?\n * @function\n * @param {array} nodes Array of node paths.\n * @return {array} array of boolean states.\n */\nAvalonHarmony.areEnabled = function(nodes) {\n    var states = [];\n    for (var i = 0 ; i < nodes.length; i++) {\n        states.push(node.getEnable(nodes[i]));\n    }\n    return states;\n};\n\n\n/**\n * Set state on nodes.\n * @function\n * @param {array} args Array of nodes array and states array.\n */\nAvalonHarmony.setState = function(args) {\n    var nodes = args[0];\n    var states = args[1];\n    // length of both arrays must be equal.\n    if (nodes.length !== states.length) {\n        return false;\n    }\n    for (var i = 0 ; i < nodes.length; i++) {\n        node.setEnable(nodes[i], states[i]);\n    }\n    return true;\n};\n\n\n/**\n * Disable specified nodes.\n * @function\n * @param {array} nodes Array of nodes.\n */\nAvalonHarmony.disableNodes = function(nodes) {\n    for (var i = 0 ; i < nodes.length; i++)\n    {\n        node.setEnable(nodes[i], false);\n    }\n};\n\n\n/**\n * Save scene in Harmony.\n * @function\n * @return {string} Scene path.\n */\nAvalonHarmony.saveScene = function() {\n    var app = QCoreApplication.instance();\n    app.avalon_on_file_changed = false;\n    scene.saveAll();\n    return (\n        scene.currentProjectPath() + '/' +\n          scene.currentVersionName() + '.xstage'\n    );\n};\n\n\n/**\n * Enable Harmony file-watcher.\n * @function\n */\nAvalonHarmony.enableFileWather = function() {\n    var app = QCoreApplication.instance();\n    app.avalon_on_file_changed = true;\n};\n\n\n/**\n * Add path to file-watcher.\n * @function\n * @param {string} path Path to watch.\n */\nAvalonHarmony.addPathToWatcher = function(path) {\n    var app = QCoreApplication.instance();\n    app.watcher.addPath(path);\n};\n\n\n/**\n * Setup node for Creator.\n * @function\n * @param {string} node Node path.\n */\nAvalonHarmony.setupNodeForCreator = function(node) {\n    node.setTextAttr(node, 'COMPOSITE_MODE', 1, 'Pass Through');\n};\n\n\n/**\n * Get node names for specified node type.\n * @function\n * @param {string} nodeType Node type.\n * @return {array} Node names.\n */\nAvalonHarmony.getNodesNamesByType = function(nodeType) {\n    var nodes = node.getNodes(nodeType);\n    var nodeNames = [];\n    for (var i = 0; i < nodes.length; ++i) {\n        nodeNames.push(node.getName(nodes[i]));\n    }\n    return nodeNames;\n};\n\n\n/**\n * Create container node in Harmony.\n * @function\n * @param {array} args Arguments, see example.\n * @return {string} Resulting node.\n *\n * @example\n * // arguments are in following order:\n * var args = [\n *  nodeName,\n *  nodeType,\n *  selection\n * ];\n */\nAvalonHarmony.createContainer = function(args) {\n    var resultNode = node.add('Top', args[0], args[1], 0, 0, 0);\n    if (args.length > 2) {\n        node.link(args[2], 0, resultNode, 0, false, true);\n        node.setCoord(resultNode,\n            node.coordX(args[2]),\n            node.coordY(args[2]) + 70);\n    }\n    return resultNode;\n};\n\n\n/**\n * Delete node.\n * @function\n * @param {string} node Node path.\n */\nAvalonHarmony.deleteNode = function(_node) {\n    node.deleteNode(_node, true, true);\n};"
  },
  {
    "path": "openpype/hosts/harmony/api/lib.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Utility functions used for Avalon - Harmony integration.\"\"\"\nimport subprocess\nimport threading\nimport os\nimport random\nimport zipfile\nimport sys\nimport filecmp\nimport shutil\nimport logging\nimport contextlib\nimport json\nimport signal\nimport time\nfrom uuid import uuid4\nfrom qtpy import QtWidgets, QtCore, QtGui\nimport collections\n\nfrom .server import Server\n\nfrom openpype.tools.stdout_broker.app import StdOutBroker\nfrom openpype.tools.utils import host_tools\nfrom openpype import style\nfrom openpype.lib.applications import get_non_python_host_kwargs\n\n# Setup logging.\nlog = logging.getLogger(__name__)\nlog.setLevel(logging.DEBUG)\n\n\nclass ProcessContext:\n    server = None\n    pid = None\n    process = None\n    application_path = None\n    callback_queue = collections.deque()\n    workfile_path = None\n    port = None\n    stdout_broker = None\n    workfile_tool = None\n\n    @classmethod\n    def execute_in_main_thread(cls, func_to_call_from_main_thread):\n        cls.callback_queue.append(func_to_call_from_main_thread)\n\n    @classmethod\n    def main_thread_listen(cls):\n        if cls.callback_queue:\n            callback = cls.callback_queue.popleft()\n            callback()\n        if cls.process is not None and cls.process.poll() is not None:\n            log.info(\"Server is not running, closing\")\n            ProcessContext.stdout_broker.stop()\n            QtWidgets.QApplication.quit()\n\n\ndef signature(postfix=\"func\") -> str:\n    \"\"\"Return random ECMA6 compatible function name.\n\n    Args:\n        postfix (str): name to append to random string.\n    Returns:\n        str: random function name.\n\n    \"\"\"\n    return \"f{}_{}\".format(str(uuid4()).replace(\"-\", \"_\"), postfix)\n\n\nclass _ZipFile(zipfile.ZipFile):\n    \"\"\"Extended check for windows invalid characters.\"\"\"\n\n    # this is extending default zipfile table for few invalid characters\n    # that can come from Mac\n    _windows_illegal_characters = \":<>|\\\"?*\\r\\n\\x00\"\n    _windows_illegal_name_trans_table = str.maketrans(\n        _windows_illegal_characters,\n        \"_\" * len(_windows_illegal_characters)\n    )\n\n\ndef main(*subprocess_args):\n    # coloring in StdOutBroker\n    os.environ[\"OPENPYPE_LOG_NO_COLORS\"] = \"False\"\n    app = QtWidgets.QApplication([])\n    app.setQuitOnLastWindowClosed(False)\n    icon = QtGui.QIcon(style.get_app_icon_path())\n    app.setWindowIcon(icon)\n\n    ProcessContext.stdout_broker = StdOutBroker('harmony')\n    ProcessContext.stdout_broker.start()\n    launch(*subprocess_args)\n\n    loop_timer = QtCore.QTimer()\n    loop_timer.setInterval(20)\n\n    loop_timer.timeout.connect(ProcessContext.main_thread_listen)\n    loop_timer.start()\n\n    sys.exit(app.exec_())\n\n\ndef setup_startup_scripts():\n    \"\"\"Manages installation of avalon's TB_sceneOpened.js for Harmony launch.\n\n    If a studio already has defined \"TOONBOOM_GLOBAL_SCRIPT_LOCATION\", copies\n    the TB_sceneOpened.js to that location if the file is different.\n    Otherwise, will set the env var to point to the avalon/harmony folder.\n\n    Admins should be aware that this will overwrite TB_sceneOpened in the\n    \"TOONBOOM_GLOBAL_SCRIPT_LOCATION\", and that if they want to have additional\n    logic, they will need to one of the following:\n        * Create a Harmony package to manage startup logic\n        * Use TB_sceneOpenedUI.js instead to manage startup logic\n        * Add their startup logic to avalon/harmony/TB_sceneOpened.js\n    \"\"\"\n    avalon_dcc_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)),\n                                  \"api\")\n    startup_js = \"TB_sceneOpened.js\"\n\n    if os.getenv(\"TOONBOOM_GLOBAL_SCRIPT_LOCATION\"):\n\n        avalon_harmony_startup = os.path.join(avalon_dcc_dir, startup_js)\n\n        env_harmony_startup = os.path.join(\n            os.getenv(\"TOONBOOM_GLOBAL_SCRIPT_LOCATION\"), startup_js)\n\n        if not filecmp.cmp(avalon_harmony_startup, env_harmony_startup):\n            try:\n                shutil.copy(avalon_harmony_startup, env_harmony_startup)\n            except Exception as e:\n                log.error(e)\n                log.warning(\n                    \"Failed to copy {0} to {1}! \"\n                    \"Defaulting to Avalon TOONBOOM_GLOBAL_SCRIPT_LOCATION.\"\n                    .format(avalon_harmony_startup, env_harmony_startup))\n\n                os.environ[\"TOONBOOM_GLOBAL_SCRIPT_LOCATION\"] = avalon_dcc_dir\n    else:\n        os.environ[\"TOONBOOM_GLOBAL_SCRIPT_LOCATION\"] = avalon_dcc_dir\n\n\ndef check_libs():\n    \"\"\"Check if `OpenHarmony`_ is available.\n\n    Avalon expects either path in `LIB_OPENHARMONY_PATH` or `openHarmony.js`\n    present in `TOONBOOM_GLOBAL_SCRIPT_LOCATION`.\n\n    Throws:\n        RuntimeError: If openHarmony is not found.\n\n    .. _OpenHarmony:\n        https://github.com/cfourney/OpenHarmony\n\n    \"\"\"\n    if not os.getenv(\"LIB_OPENHARMONY_PATH\"):\n\n        if os.getenv(\"TOONBOOM_GLOBAL_SCRIPT_LOCATION\"):\n            if os.path.exists(\n                os.path.join(\n                    os.getenv(\"TOONBOOM_GLOBAL_SCRIPT_LOCATION\"),\n                    \"openHarmony.js\")):\n\n                os.environ[\"LIB_OPENHARMONY_PATH\"] = \\\n                    os.getenv(\"TOONBOOM_GLOBAL_SCRIPT_LOCATION\")\n                return\n\n        else:\n            log.error((\"Cannot find OpenHarmony library. \"\n                       \"Please set path to it in LIB_OPENHARMONY_PATH \"\n                       \"environment variable.\"))\n            raise RuntimeError(\"Missing OpenHarmony library.\")\n\n\ndef launch(application_path, *args):\n    \"\"\"Set Harmony for launch.\n\n    Launches Harmony and the server, then starts listening on the main thread\n    for callbacks from the server. This is to have Qt applications run in the\n    main thread.\n\n    Args:\n        application_path (str): Path to Harmony.\n\n    \"\"\"\n    from openpype.pipeline import install_host\n    from openpype.hosts.harmony import api as harmony\n\n    install_host(harmony)\n\n    ProcessContext.port = random.randrange(49152, 65535)\n    os.environ[\"AVALON_HARMONY_PORT\"] = str(ProcessContext.port)\n    ProcessContext.application_path = application_path\n\n    # Launch Harmony.\n    setup_startup_scripts()\n    check_libs()\n\n    if not os.environ.get(\"AVALON_HARMONY_WORKFILES_ON_LAUNCH\", False):\n        open_empty_workfile()\n        return\n\n    ProcessContext.workfile_tool = host_tools.get_tool_by_name(\"workfiles\")\n    host_tools.show_workfiles(save=False)\n    ProcessContext.execute_in_main_thread(check_workfiles_tool)\n\n\ndef check_workfiles_tool():\n    if ProcessContext.workfile_tool.isVisible():\n        ProcessContext.execute_in_main_thread(check_workfiles_tool)\n    elif not ProcessContext.workfile_path:\n        open_empty_workfile()\n\n\ndef open_empty_workfile():\n    zip_file = os.path.join(os.path.dirname(__file__), \"temp.zip\")\n    temp_path = get_local_harmony_path(zip_file)\n    if os.path.exists(temp_path):\n        log.info(f\"removing existing {temp_path}\")\n        try:\n            shutil.rmtree(temp_path)\n        except Exception as e:\n            log.critical(f\"cannot clear {temp_path}\")\n            raise Exception(f\"cannot clear {temp_path}\") from e\n\n    launch_zip_file(zip_file)\n\n\ndef get_local_harmony_path(filepath):\n    \"\"\"From the provided path get the equivalent local Harmony path.\"\"\"\n    basename = os.path.splitext(os.path.basename(filepath))[0]\n    harmony_path = os.path.join(os.path.expanduser(\"~\"), \".avalon\", \"harmony\")\n    return os.path.join(harmony_path, basename)\n\n\ndef launch_zip_file(filepath):\n    \"\"\"Launch a Harmony application instance with the provided zip file.\n\n    Args:\n        filepath (str): Path to file.\n    \"\"\"\n    print(f\"Localizing {filepath}\")\n\n    temp_path = get_local_harmony_path(filepath)\n    scene_name = os.path.basename(temp_path)\n    if os.path.exists(os.path.join(temp_path, scene_name)):\n        # unzipped with duplicated scene_name\n        temp_path = os.path.join(temp_path, scene_name)\n\n    scene_path = os.path.join(\n        temp_path, scene_name + \".xstage\"\n    )\n\n    unzip = False\n    if os.path.exists(scene_path):\n        # Check remote scene is newer than local.\n        if os.path.getmtime(scene_path) < os.path.getmtime(filepath):\n            try:\n                shutil.rmtree(temp_path)\n            except Exception as e:\n                log.error(e)\n                raise Exception(\"Cannot delete working folder\") from e\n            unzip = True\n    else:\n        unzip = True\n\n    if unzip:\n        with _ZipFile(filepath, \"r\") as zip_ref:\n            zip_ref.extractall(temp_path)\n\n        if os.path.exists(os.path.join(temp_path, scene_name)):\n            # unzipped with duplicated scene_name\n            temp_path = os.path.join(temp_path, scene_name)\n\n    # Close existing scene.\n    if ProcessContext.pid:\n        os.kill(ProcessContext.pid, signal.SIGTERM)\n\n    # Stop server.\n    if ProcessContext.server:\n        ProcessContext.server.stop()\n\n    # Launch Avalon server.\n    ProcessContext.server = Server(ProcessContext.port)\n    ProcessContext.server.start()\n    # thread = threading.Thread(target=self.server.start)\n    # thread.daemon = True\n    # thread.start()\n\n    # Save workfile path for later.\n    ProcessContext.workfile_path = filepath\n\n    # find any xstage files is directory, prefer the one with the same name\n    # as directory (plus extension)\n    xstage_files = []\n    for _, _, files in os.walk(temp_path):\n        for file in files:\n            if os.path.splitext(file)[1] == \".xstage\":\n                xstage_files.append(file)\n\n    if not os.path.basename(\"temp.zip\"):\n        if not xstage_files:\n            ProcessContext.server.stop()\n            print(\"no xstage file was found\")\n            return\n\n    # try to use first available\n    scene_path = os.path.join(\n        temp_path, xstage_files[0]\n    )\n\n    # prefer the one named as zip file\n    zip_based_name = \"{}.xstage\".format(\n        os.path.splitext(os.path.basename(filepath))[0])\n\n    if zip_based_name in xstage_files:\n        scene_path = os.path.join(\n            temp_path, zip_based_name\n        )\n\n    if not os.path.exists(scene_path):\n        print(\"error: cannot determine scene file {}\".format(scene_path))\n        ProcessContext.server.stop()\n        return\n\n    print(\"Launching {}\".format(scene_path))\n    kwargs = get_non_python_host_kwargs({}, False)\n    process = subprocess.Popen(\n        [ProcessContext.application_path, scene_path],\n        **kwargs\n    )\n    ProcessContext.pid = process.pid\n    ProcessContext.process = process\n    ProcessContext.stdout_broker.host_connected()\n\n\ndef on_file_changed(path, threaded=True):\n    \"\"\"Threaded zipping and move of the project directory.\n\n    This method is called when the `.xstage` file is changed.\n    \"\"\"\n    log.debug(\"File changed: \" + path)\n\n    if ProcessContext.workfile_path is None:\n        return\n\n    if threaded:\n        thread = threading.Thread(\n            target=zip_and_move,\n            args=(os.path.dirname(path), ProcessContext.workfile_path)\n        )\n        thread.start()\n    else:\n        zip_and_move(os.path.dirname(path), ProcessContext.workfile_path)\n\n\ndef zip_and_move(source, destination):\n    \"\"\"Zip a directory and move to `destination`.\n\n    Args:\n        source (str): Directory to zip and move to destination.\n        destination (str): Destination file path to zip file.\n\n    \"\"\"\n    os.chdir(os.path.dirname(source))\n    shutil.make_archive(os.path.basename(source), \"zip\", source)\n    with _ZipFile(os.path.basename(source) + \".zip\") as zr:\n        if zr.testzip() is not None:\n            raise Exception(\"File archive is corrupted.\")\n    shutil.move(os.path.basename(source) + \".zip\", destination)\n    log.debug(f\"Saved '{source}' to '{destination}'\")\n\n\ndef show(tool_name):\n    \"\"\"Call show on \"module_name\".\n\n    This allows to make a QApplication ahead of time and always \"exec_\" to\n    prevent crashing.\n\n    Args:\n        module_name (str): Name of module to call \"show\" on.\n\n    \"\"\"\n    # Requests often get doubled up when showing tools, so we wait a second for\n    # requests to be received properly.\n    time.sleep(1)\n\n    kwargs = {}\n    if tool_name == \"loader\":\n        kwargs[\"use_context\"] = True\n\n    ProcessContext.execute_in_main_thread(\n        lambda: host_tools.show_tool_by_name(tool_name, **kwargs)\n    )\n\n    # Required return statement.\n    return \"nothing\"\n\n\ndef get_scene_data():\n    try:\n        return send(\n            {\n                \"function\": \"AvalonHarmony.getSceneData\"\n            })[\"result\"]\n    except json.decoder.JSONDecodeError:\n        # Means no scene metadata has been made before.\n        return {}\n    except KeyError:\n        # Means no existing scene metadata has been made.\n        return {}\n\n\ndef set_scene_data(data):\n    \"\"\"Write scene data to metadata.\n\n    Args:\n        data (dict): Data to write.\n\n    \"\"\"\n    # Write scene data.\n    send(\n        {\n            \"function\": \"AvalonHarmony.setSceneData\",\n            \"args\": data\n        })\n\n\ndef read(node_id):\n    \"\"\"Read object metadata in to a dictionary.\n\n    Args:\n        node_id (str): Path to node or id of object.\n\n    Returns:\n        dict\n    \"\"\"\n    scene_data = get_scene_data()\n    if node_id in scene_data:\n        return scene_data[node_id]\n\n    return {}\n\n\ndef remove(node_id):\n    \"\"\"\n        Remove node data from scene metadata.\n\n        Args:\n            node_id (str): full name (eg. 'Top/renderAnimation')\n    \"\"\"\n    data = get_scene_data()\n    del data[node_id]\n    set_scene_data(data)\n\n\ndef delete_node(node):\n    \"\"\" Physically delete node from scene. \"\"\"\n    send(\n        {\n            \"function\": \"AvalonHarmony.deleteNode\",\n            \"args\": node\n        }\n    )\n\n\ndef imprint(node_id, data, remove=False):\n    \"\"\"Write `data` to the `node` as json.\n\n    Arguments:\n        node_id (str): Path to node or id of object.\n        data (dict): Dictionary of key/value pairs.\n        remove (bool): Removes the data from the scene.\n\n    Example:\n        >>> from openpype.hosts.harmony.api import lib\n        >>> node = \"Top/Display\"\n        >>> data = {\"str\": \"something\", \"int\": 1, \"float\": 0.32, \"bool\": True}\n        >>> lib.imprint(layer, data)\n    \"\"\"\n    scene_data = get_scene_data()\n\n    if remove and (node_id in scene_data):\n        scene_data.pop(node_id, None)\n    else:\n        if node_id in scene_data:\n            scene_data[node_id].update(data)\n        else:\n            scene_data[node_id] = data\n\n    set_scene_data(scene_data)\n\n\n@contextlib.contextmanager\ndef maintained_selection():\n    \"\"\"Maintain selection during context.\"\"\"\n\n    selected_nodes = send(\n        {\n            \"function\": \"AvalonHarmony.getSelectedNodes\"\n        })[\"result\"]\n\n    try:\n        yield selected_nodes\n    finally:\n        selected_nodes = send(\n            {\n                \"function\": \"AvalonHarmony.selectNodes\",\n                \"args\": selected_nodes\n            }\n        )\n\n\ndef send(request):\n    \"\"\"Public method for sending requests to Harmony.\"\"\"\n    return ProcessContext.server.send(request)\n\n\ndef select_nodes(nodes):\n    \"\"\" Selects nodes in Node View \"\"\"\n    _ = send(\n        {\n            \"function\": \"AvalonHarmony.selectNodes\",\n            \"args\": nodes\n        }\n    )\n\n\n@contextlib.contextmanager\ndef maintained_nodes_state(nodes):\n    \"\"\"Maintain nodes states during context.\"\"\"\n    # Collect current state.\n    states = send(\n        {\n            \"function\": \"AvalonHarmony.areEnabled\", \"args\": nodes\n        })[\"result\"]\n\n    # Disable all nodes.\n    send(\n        {\n            \"function\": \"AvalonHarmony.disableNodes\", \"args\": nodes\n        })\n\n    try:\n        yield\n    finally:\n        send(\n            {\n                \"function\": \"AvalonHarmony.setState\",\n                \"args\": [nodes, states]\n            })\n\n\ndef save_scene():\n    \"\"\"Save the Harmony scene safely.\n\n    The built-in (to Avalon) background zip and moving of the Harmony scene\n    folder, interfers with server/client communication by sending two requests\n    at the same time. This only happens when sending \"scene.saveAll()\". This\n    method prevents this double request and safely saves the scene.\n\n    \"\"\"\n    # Need to turn off the background watcher else the communication with\n    # the server gets spammed with two requests at the same time.\n    scene_path = send(\n        {\"function\": \"AvalonHarmony.saveScene\"})[\"result\"]\n\n    # Manually update the remote file.\n    on_file_changed(scene_path, threaded=False)\n\n    # Re-enable the background watcher.\n    send({\"function\": \"AvalonHarmony.enableFileWather\"})\n\n\ndef save_scene_as(filepath):\n    \"\"\"Save Harmony scene as `filepath`.\"\"\"\n    scene_dir = os.path.dirname(filepath)\n    destination = os.path.join(\n        os.path.dirname(ProcessContext.workfile_path),\n        os.path.splitext(os.path.basename(filepath))[0] + \".zip\"\n    )\n\n    if os.path.exists(scene_dir):\n        try:\n            shutil.rmtree(scene_dir)\n        except Exception as e:\n            log.error(f\"Cannot remove {scene_dir}\")\n            raise Exception(f\"Cannot remove {scene_dir}\") from e\n\n    send(\n        {\"function\": \"scene.saveAs\", \"args\": [scene_dir]}\n    )[\"result\"]\n\n    zip_and_move(scene_dir, destination)\n\n    ProcessContext.workfile_path = destination\n\n    send(\n        {\"function\": \"AvalonHarmony.addPathToWatcher\", \"args\": filepath}\n    )\n\n\ndef find_node_by_name(name, node_type):\n    \"\"\"Find node by its name.\n\n    Args:\n        name (str): Name of the Node. (without part before '/')\n        node_type (str): Type of the Node.\n            'READ' - for loaded data with Loaders (background)\n            'GROUP' - for loaded data with Loaders (templates)\n            'WRITE' - render nodes\n\n    Returns:\n        str: FQ Node name.\n\n    \"\"\"\n    nodes = send(\n        {\"function\": \"node.getNodes\", \"args\": [[node_type]]}\n    )[\"result\"]\n    for node in nodes:\n        node_name = node.split(\"/\")[-1]\n        if name == node_name:\n            return node\n\n    return None\n"
  },
  {
    "path": "openpype/hosts/harmony/api/pipeline.py",
    "content": "import os\nfrom pathlib import Path\nimport logging\n\nimport pyblish.api\n\nfrom openpype.lib import register_event_callback\nfrom openpype.pipeline import (\n    register_loader_plugin_path,\n    register_creator_plugin_path,\n    deregister_loader_plugin_path,\n    deregister_creator_plugin_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.pipeline.load import get_outdated_containers\nfrom openpype.pipeline.context_tools import get_current_project_asset\n\nfrom openpype.hosts.harmony import HARMONY_HOST_DIR\nimport openpype.hosts.harmony.api as harmony\n\n\nlog = logging.getLogger(\"openpype.hosts.harmony\")\n\nPLUGINS_DIR = os.path.join(HARMONY_HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\nINVENTORY_PATH = os.path.join(PLUGINS_DIR, \"inventory\")\n\n\ndef set_scene_settings(settings):\n    \"\"\"Set correct scene settings in Harmony.\n\n    Args:\n        settings (dict): Scene settings.\n\n    Returns:\n        dict: Dictionary of settings to set.\n\n    \"\"\"\n    harmony.send(\n        {\"function\": \"PypeHarmony.setSceneSettings\", \"args\": settings})\n\n\ndef get_asset_settings():\n    \"\"\"Get settings on current asset from database.\n\n    Returns:\n        dict: Scene data.\n\n    \"\"\"\n\n    asset_doc = get_current_project_asset()\n    asset_data = asset_doc[\"data\"]\n    fps = asset_data.get(\"fps\")\n    frame_start = asset_data.get(\"frameStart\")\n    frame_end = asset_data.get(\"frameEnd\")\n    handle_start = asset_data.get(\"handleStart\")\n    handle_end = asset_data.get(\"handleEnd\")\n    resolution_width = asset_data.get(\"resolutionWidth\")\n    resolution_height = asset_data.get(\"resolutionHeight\")\n    entity_type = asset_data.get(\"entityType\")\n\n    scene_data = {\n        \"fps\": fps,\n        \"frameStart\": frame_start,\n        \"frameEnd\": frame_end,\n        \"handleStart\": handle_start,\n        \"handleEnd\": handle_end,\n        \"resolutionWidth\": resolution_width,\n        \"resolutionHeight\": resolution_height,\n        \"entityType\": entity_type\n    }\n\n    return scene_data\n\n\ndef ensure_scene_settings():\n    \"\"\"Validate if Harmony scene has valid settings.\"\"\"\n    settings = get_asset_settings()\n\n    invalid_settings = []\n    valid_settings = {}\n    for key, value in settings.items():\n        if value is None:\n            invalid_settings.append(key)\n        else:\n            valid_settings[key] = value\n\n    # Warn about missing attributes.\n    if invalid_settings:\n        msg = \"Missing attributes:\"\n        for item in invalid_settings:\n            msg += f\"\\n{item}\"\n\n        harmony.send(\n            {\"function\": \"PypeHarmony.message\", \"args\": msg})\n\n    set_scene_settings(valid_settings)\n\n\ndef check_inventory():\n    \"\"\"Check is scene contains outdated containers.\n\n    If it does it will colorize outdated nodes and display warning message\n    in Harmony.\n    \"\"\"\n\n    outdated_containers = get_outdated_containers()\n    if not outdated_containers:\n        return\n\n    # Colour nodes.\n    outdated_nodes = []\n    for container in outdated_containers:\n        if container[\"loader\"] == \"ImageSequenceLoader\":\n            outdated_nodes.append(\n                harmony.find_node_by_name(container[\"name\"], \"READ\")\n            )\n    harmony.send({\"function\": \"PypeHarmony.setColor\", \"args\": outdated_nodes})\n\n    # Warn about outdated containers.\n    msg = \"There are outdated containers in the scene.\"\n    harmony.send({\"function\": \"PypeHarmony.message\", \"args\": msg})\n\n\ndef application_launch(event):\n    \"\"\"Event that is executed after Harmony is launched.\"\"\"\n    # fills OPENPYPE_HARMONY_JS\n    pype_harmony_path = Path(__file__).parent.parent / \"js\" / \"PypeHarmony.js\"\n    pype_harmony_js = pype_harmony_path.read_text()\n\n    # go through js/creators, loaders and publish folders and load all scripts\n    script = \"\"\n    for item in [\"creators\", \"loaders\", \"publish\"]:\n        dir_to_scan = Path(__file__).parent.parent / \"js\" / item\n        for child in dir_to_scan.iterdir():\n            script += child.read_text()\n\n    # send scripts to Harmony\n    harmony.send({\"script\": pype_harmony_js})\n    harmony.send({\"script\": script})\n    inject_avalon_js()\n\n    # ensure_scene_settings()\n    check_inventory()\n\n\ndef export_template(backdrops, nodes, filepath):\n    \"\"\"Export Template to file.\n\n    Args:\n        backdrops (list): List of backdrops to export.\n        nodes (list): List of nodes to export.\n        filepath (str): Path where to save Template.\n\n    \"\"\"\n    harmony.send({\n        \"function\": \"PypeHarmony.exportTemplate\",\n        \"args\": [\n            backdrops,\n            nodes,\n            os.path.basename(filepath),\n            os.path.dirname(filepath)\n        ]\n    })\n\n\ndef install():\n    \"\"\"Install Pype as host config.\"\"\"\n    print(\"Installing Pype config ...\")\n\n    pyblish.api.register_host(\"harmony\")\n    pyblish.api.register_plugin_path(PUBLISH_PATH)\n    register_loader_plugin_path(LOAD_PATH)\n    register_creator_plugin_path(CREATE_PATH)\n    log.info(PUBLISH_PATH)\n\n    # Register callbacks.\n    pyblish.api.register_callback(\n        \"instanceToggled\", on_pyblish_instance_toggled\n    )\n\n    register_event_callback(\"application.launched\", application_launch)\n\n\ndef uninstall():\n    pyblish.api.deregister_plugin_path(PUBLISH_PATH)\n    deregister_loader_plugin_path(LOAD_PATH)\n    deregister_creator_plugin_path(CREATE_PATH)\n\n\ndef on_pyblish_instance_toggled(instance, old_value, new_value):\n    \"\"\"Toggle node enabling on instance toggles.\"\"\"\n    node = None\n    if instance.data.get(\"setMembers\"):\n        node = instance.data[\"setMembers\"][0]\n\n    if node:\n        harmony.send(\n            {\n                \"function\": \"PypeHarmony.toggleInstance\",\n                \"args\": [node, new_value]\n            }\n        )\n\n\ndef inject_avalon_js():\n    \"\"\"Inject AvalonHarmony.js into Harmony.\"\"\"\n    avalon_harmony_js = Path(__file__).parent.joinpath(\"js/AvalonHarmony.js\")\n    script = avalon_harmony_js.read_text()\n    # send AvalonHarmony.js to Harmony\n    harmony.send({\"script\": script})\n\n\ndef ls():\n    \"\"\"Yields containers from Harmony scene.\n\n    This is the host-equivalent of api.ls(), but instead of listing\n    assets on disk, it lists assets already loaded in Harmony; once loaded\n    they are called 'containers'.\n\n    Yields:\n        dict: container\n    \"\"\"\n    objects = harmony.get_scene_data() or {}\n    for _, data in objects.items():\n        # Skip non-tagged objects.\n        if not data:\n            continue\n\n        # Filter to only containers.\n        if \"container\" not in data.get(\"id\"):\n            continue\n\n        if not data.get(\"objectName\"):  # backward compatibility\n            data[\"objectName\"] = data[\"name\"]\n        yield data\n\n\ndef list_instances(remove_orphaned=True):\n    \"\"\"\n        List all created instances from current workfile which\n        will be published.\n\n        Pulls from File > File Info\n\n        For SubsetManager, by default it check if instance has matching node\n        in the scene, if not, instance gets deleted from metadata.\n\n        Returns:\n            (list) of dictionaries matching instances format\n    \"\"\"\n    objects = harmony.get_scene_data() or {}\n    instances = []\n    for key, data in objects.items():\n        # Skip non-tagged objects.\n        if not data:\n            continue\n\n        # Filter out containers.\n        if \"container\" in data.get(\"id\"):\n            continue\n\n        data['uuid'] = key\n\n        if remove_orphaned:\n            node_name = key.split(\"/\")[-1]\n            located_node = harmony.find_node_by_name(node_name, 'WRITE')\n            if not located_node:\n                print(\"Removing orphaned instance {}\".format(key))\n                harmony.remove(key)\n                continue\n\n        instances.append(data)\n\n    return instances\n\n\ndef remove_instance(instance):\n    \"\"\"\n        Remove instance from current workfile metadata and from scene!\n\n        Updates metadata of current file in File > File Info and removes\n        icon highlight on group layer.\n\n        For SubsetManager\n\n        Args:\n            instance (dict): instance representation from subsetmanager model\n    \"\"\"\n    node = instance.get(\"uuid\")\n    harmony.remove(node)\n    harmony.delete_node(node)\n\n\ndef select_instance(instance):\n    \"\"\"\n        Select instance in Node View\n\n        Args:\n            instance (dict): instance representation from subsetmanager model\n    \"\"\"\n    harmony.select_nodes([instance.get(\"uuid\")])\n\n\ndef containerise(name,\n                 namespace,\n                 node,\n                 context,\n                 loader=None,\n                 suffix=None,\n                 nodes=None):\n    \"\"\"Imprint node with metadata.\n\n    Containerisation enables a tracking of version, author and origin\n    for loaded assets.\n\n    Arguments:\n        name (str): Name of resulting assembly.\n        namespace (str): Namespace under which to host container.\n        node (str): Node to containerise.\n        context (dict): Asset information.\n        loader (str, optional): Name of loader used to produce this container.\n        suffix (str, optional): Suffix of container, defaults to `_CON`.\n\n    Returns:\n        container (str): Path of container assembly.\n    \"\"\"\n    if not nodes:\n        nodes = []\n\n    data = {\n        \"schema\": \"openpype:container-2.0\",\n        \"id\": AVALON_CONTAINER_ID,\n        \"name\": name,\n        \"namespace\": namespace,\n        \"loader\": str(loader),\n        \"representation\": str(context[\"representation\"][\"_id\"]),\n        \"nodes\": nodes\n    }\n\n    harmony.imprint(node, data)\n\n    return node\n"
  },
  {
    "path": "openpype/hosts/harmony/api/plugin.py",
    "content": "from openpype.pipeline import LegacyCreator\nimport openpype.hosts.harmony.api as harmony\n\n\nclass Creator(LegacyCreator):\n    \"\"\"Creator plugin to create instances in Harmony.\n\n    By default a Composite node is created to support any number of nodes in\n    an instance, but any node type is supported.\n    If the selection is used, the selected nodes will be connected to the\n    created node.\n    \"\"\"\n\n    defaults = [\"Main\"]\n    node_type = \"COMPOSITE\"\n\n    def setup_node(self, node):\n        \"\"\"Prepare node as container.\n\n        Args:\n            node (str): Path to node.\n        \"\"\"\n        harmony.send(\n            {\n                \"function\": \"AvalonHarmony.setupNodeForCreator\",\n                \"args\": node\n            }\n        )\n\n    def process(self):\n        \"\"\"Plugin entry point.\"\"\"\n        existing_node_names = harmony.send(\n            {\n                \"function\": \"AvalonHarmony.getNodesNamesByType\",\n                \"args\": self.node_type\n            })[\"result\"]\n\n        # Dont allow instances with the same name.\n        msg = \"Instance with name \\\"{}\\\" already exists.\".format(self.name)\n        for name in existing_node_names:\n            if self.name.lower() == name.lower():\n                harmony.send(\n                    {\n                        \"function\": \"AvalonHarmony.message\", \"args\": msg\n                    }\n                )\n                return False\n\n        with harmony.maintained_selection() as selection:\n            node = None\n\n            if (self.options or {}).get(\"useSelection\") and selection:\n                node = harmony.send(\n                    {\n                        \"function\": \"AvalonHarmony.createContainer\",\n                        \"args\": [self.name, self.node_type, selection[-1]]\n                    }\n                )[\"result\"]\n            else:\n                node = harmony.send(\n                    {\n                        \"function\": \"AvalonHarmony.createContainer\",\n                        \"args\": [self.name, self.node_type]\n                    }\n                )[\"result\"]\n\n            harmony.imprint(node, self.data)\n            self.setup_node(node)\n\n        return node\n"
  },
  {
    "path": "openpype/hosts/harmony/api/server.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Server-side implementation of Toon Boon Harmony communication.\"\"\"\nimport socket\nimport logging\nimport json\nimport traceback\nimport importlib\nimport functools\nimport time\nimport struct\nfrom datetime import datetime\nimport threading\nfrom . import lib\n\n\nclass Server(threading.Thread):\n    \"\"\"Class for communication with Toon Boon Harmony.\n\n    Attributes:\n        connection (Socket): connection holding object.\n        received (str): received data buffer.any(iterable)\n        port (int): port number.\n        message_id (int): index of last message going out.\n        queue (dict): dictionary holding queue of incoming messages.\n\n    \"\"\"\n\n    def __init__(self, port):\n        \"\"\"Constructor.\"\"\"\n        super(Server, self).__init__()\n        self.daemon = True\n        self.connection = None\n        self.received = \"\"\n        self.port = port\n        self.message_id = 1\n\n        # Setup logging.\n        self.log = logging.getLogger(__name__)\n        self.log.setLevel(logging.DEBUG)\n\n        # Create a TCP/IP socket\n        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n\n        # Bind the socket to the port\n        server_address = (\"127.0.0.1\", port)\n        self.log.debug(\n            f\"[{self.timestamp()}] Starting up on \"\n            f\"{server_address[0]}:{server_address[1]}\")\n        self.socket.bind(server_address)\n\n        # Listen for incoming connections\n        self.socket.listen(1)\n        self.queue = {}\n\n    def process_request(self, request):\n        \"\"\"Process incoming request.\n\n        Args:\n            request (dict): {\n                \"module\": (str),  # Module of method.\n                \"method\" (str),  # Name of method in module.\n                \"args\" (list),  # Arguments to pass to method.\n                \"kwargs\" (dict),  # Keyword arguments to pass to method.\n                \"reply\" (bool),  # Optional wait for method completion.\n            }\n        \"\"\"\n        pretty = self._pretty(request)\n        self.log.debug(\n            f\"[{self.timestamp()}] Processing request:\\n{pretty}\")\n\n        try:\n            module = importlib.import_module(request[\"module\"])\n            method = getattr(module, request[\"method\"])\n\n            args = request.get(\"args\", [])\n            kwargs = request.get(\"kwargs\", {})\n            partial_method = functools.partial(method, *args, **kwargs)\n\n            lib.ProcessContext.execute_in_main_thread(partial_method)\n        except Exception:\n            self.log.error(traceback.format_exc())\n\n    def receive(self):\n        \"\"\"Receives data from `self.connection`.\n\n        When the data is a json serializable string, a reply is sent then\n        processing of the request.\n        \"\"\"\n        current_time = time.time()\n        while True:\n            self.log.info(\"wait ttt\")\n            # Receive the data in small chunks and retransmit it\n            request = None\n            try:\n                header = self.connection.recv(10)\n            except OSError:\n                # could happen on MacOS\n                self.log.info(\"\")\n                break\n\n            if len(header) == 0:\n                # null data received, socket is closing.\n                self.log.info(f\"[{self.timestamp()}] Connection closing.\")\n                break\n\n            if header[0:2] != b\"AH\":\n                self.log.error(\"INVALID HEADER\")\n            content_length_str = header[2:].decode()\n\n            length = int(content_length_str, 16)\n            data = self.connection.recv(length)\n            while (len(data) < length):\n                # we didn't received everything in first try, lets wait for\n                # all data.\n                self.log.info(\"loop\")\n                time.sleep(0.1)\n                if self.connection is None:\n                    self.log.error(f\"[{self.timestamp()}] \"\n                                   \"Connection is broken\")\n                    break\n                if time.time() > current_time + 30:\n                    self.log.error(f\"[{self.timestamp()}] Connection timeout.\")\n                    break\n\n                data += self.connection.recv(length - len(data))\n            self.log.debug(\"data:: {} {}\".format(data, type(data)))\n            self.received += data.decode(\"utf-8\")\n            pretty = self._pretty(self.received)\n            self.log.debug(\n                f\"[{self.timestamp()}] Received:\\n{pretty}\")\n\n            try:\n                request = json.loads(self.received)\n            except json.decoder.JSONDecodeError as e:\n                self.log.error(f\"[{self.timestamp()}] \"\n                               f\"Invalid message received.\\n{e}\",\n                               exc_info=True)\n\n            self.received = \"\"\n            if request is None:\n                continue\n\n            if \"message_id\" in request.keys():\n                message_id = request[\"message_id\"]\n                self.message_id = message_id + 1\n                self.log.debug(f\"--- storing request as {message_id}\")\n                self.queue[message_id] = request\n            if \"reply\" not in request.keys():\n                request[\"reply\"] = True\n                self.send(request)\n                self.process_request(request)\n\n                if \"message_id\" in request.keys():\n                    try:\n                        self.log.debug(f\"[{self.timestamp()}] \"\n                                       f\"Removing from the queue {message_id}\")\n                        del self.queue[message_id]\n                    except IndexError:\n                        self.log.debug(f\"[{self.timestamp()}] \"\n                                       f\"{message_id} is no longer in queue\")\n            else:\n                self.log.debug(f\"[{self.timestamp()}] \"\n                               \"received data was just a reply.\")\n\n    def run(self):\n        \"\"\"Entry method for server.\n\n        Waits for a connection on `self.port` before going into listen mode.\n        \"\"\"\n        # Wait for a connection\n        timestamp = datetime.now().strftime(\"%H:%M:%S.%f\")\n        self.log.debug(f\"[{timestamp}] Waiting for a connection.\")\n        self.connection, client_address = self.socket.accept()\n\n        timestamp = datetime.now().strftime(\"%H:%M:%S.%f\")\n        self.log.debug(f\"[{timestamp}] Connection from: {client_address}\")\n\n        self.receive()\n\n    def stop(self):\n        \"\"\"Shutdown socket server gracefully.\"\"\"\n        timestamp = datetime.now().strftime(\"%H:%M:%S.%f\")\n        self.log.debug(f\"[{timestamp}] Shutting down server.\")\n        if self.connection is None:\n            self.log.debug(\"Connect to shutdown.\")\n            socket.socket(\n                socket.AF_INET, socket.SOCK_STREAM\n            ).connect((\"localhost\", self.port))\n\n        self.connection.close()\n        self.connection = None\n\n        self.socket.close()\n\n    def _send(self, message):\n        \"\"\"Send a message to Harmony.\n\n        Args:\n            message (str): Data to send to Harmony.\n        \"\"\"\n        # Wait for a connection.\n        while not self.connection:\n            pass\n\n        timestamp = datetime.now().strftime(\"%H:%M:%S.%f\")\n        encoded = message.encode(\"utf-8\")\n        coded_message = b\"AH\" + struct.pack('>I', len(encoded)) + encoded\n        pretty = self._pretty(coded_message)\n        self.log.debug(\n            f\"[{timestamp}] Sending [{self.message_id}]:\\n{pretty}\")\n        self.log.debug(f\"--- Message length: {len(encoded)}\")\n        self.connection.sendall(coded_message)\n        self.message_id += 1\n\n    def send(self, request):\n        \"\"\"Send a request in dictionary to Harmony.\n\n        Waits for a reply from Harmony.\n\n        Args:\n            request (dict): Data to send to Harmony.\n        \"\"\"\n        request[\"message_id\"] = self.message_id\n        self._send(json.dumps(request))\n        if request.get(\"reply\"):\n            timestamp = datetime.now().strftime(\"%H:%M:%S.%f\")\n            self.log.debug(\n                f\"[{timestamp}] sent reply, not waiting for anything.\")\n            return None\n        result = None\n        current_time = time.time()\n        try_index = 1\n        while True:\n            time.sleep(0.1)\n            if time.time() > current_time + 30:\n                timestamp = datetime.now().strftime(\"%H:%M:%S.%f\")\n                self.log.error((f\"[{timestamp}][{self.message_id}] \"\n                                \"No reply from Harmony in 30s. \"\n                                f\"Retrying {try_index}\"))\n                try_index += 1\n                current_time = time.time()\n            if try_index > 30:\n                break\n            try:\n                result = self.queue[request[\"message_id\"]]\n                timestamp = datetime.now().strftime(\"%H:%M:%S.%f\")\n                self.log.debug((f\"[{timestamp}] Got request \"\n                                f\"id {self.message_id}, \"\n                                \"removing from queue\"))\n                del self.queue[request[\"message_id\"]]\n                break\n            except KeyError:\n                # response not in received queue yey\n                pass\n            try:\n                result = json.loads(self.received)\n                break\n            except json.decoder.JSONDecodeError:\n                pass\n\n        self.received = \"\"\n\n        return result\n\n    def _pretty(self, message) -> str:\n        # result = pformat(message, indent=2)\n        # return result.replace(\"\\\\n\", \"\\n\")\n        return \"{}{}\".format(4 * \" \", message)\n\n    def timestamp(self):\n        \"\"\"Return current timestamp as a string.\n\n        Returns:\n            str: current timestamp.\n\n        \"\"\"\n        return datetime.now().strftime(\"%H:%M:%S.%f\")\n"
  },
  {
    "path": "openpype/hosts/harmony/api/workio.py",
    "content": "\"\"\"Host API required Work Files tool\"\"\"\nimport os\nimport shutil\n\nfrom .lib import (\n    ProcessContext,\n    get_local_harmony_path,\n    zip_and_move,\n    launch_zip_file\n)\n\n# used to lock saving until previous save is done.\nsave_disabled = False\n\n\ndef file_extensions():\n    return [\".zip\"]\n\n\ndef has_unsaved_changes():\n    if ProcessContext.server:\n        return ProcessContext.server.send(\n            {\"function\": \"scene.isDirty\"})[\"result\"]\n\n    return False\n\n\ndef save_file(filepath):\n    global save_disabled\n    if save_disabled:\n        return ProcessContext.server.send(\n            {\n                \"function\": \"show_message\",\n                \"args\": \"Saving in progress, please wait until it finishes.\"\n            })[\"result\"]\n\n    save_disabled = True\n    temp_path = get_local_harmony_path(filepath)\n\n    if ProcessContext.server:\n        if os.path.exists(temp_path):\n            try:\n                shutil.rmtree(temp_path)\n            except Exception as e:\n                raise Exception(f\"cannot delete {temp_path}\") from e\n\n        ProcessContext.server.send(\n            {\"function\": \"scene.saveAs\", \"args\": [temp_path]}\n        )[\"result\"]\n\n        zip_and_move(temp_path, filepath)\n\n        ProcessContext.workfile_path = filepath\n\n        scene_path = os.path.join(\n            temp_path, os.path.basename(temp_path) + \".xstage\"\n        )\n        ProcessContext.server.send(\n            {\"function\": \"AvalonHarmony.addPathToWatcher\", \"args\": scene_path}\n        )\n    else:\n        os.environ[\"HARMONY_NEW_WORKFILE_PATH\"] = filepath.replace(\"\\\\\", \"/\")\n\n    save_disabled = False\n\n\ndef open_file(filepath):\n    launch_zip_file(filepath)\n\n\ndef current_file():\n    \"\"\"Returning None to make Workfiles app look at first file extension.\"\"\"\n    return None\n\n\ndef work_root(session):\n    return os.path.normpath(session[\"AVALON_WORKDIR\"]).replace(\"\\\\\", \"/\")\n"
  },
  {
    "path": "openpype/hosts/harmony/js/.eslintrc.json",
    "content": "{\n    \"env\": {\n        \"browser\": true\n    },\n    \"extends\": \"eslint:recommended\",\n    \"parserOptions\": {\n        \"ecmaVersion\": 3\n    },\n    \"rules\": {\n        \"indent\": [\n            \"error\",\n            4\n        ],\n        \"linebreak-style\": [\n            \"error\",\n            \"unix\"\n        ],\n        \"quotes\": [\n            \"error\",\n            \"single\"\n        ],\n        \"semi\": [\n            \"error\",\n            \"always\"\n        ]\n    },\n    \"globals\": {\n      \"$\": \"readonly\",\n      \"Action\": \"readonly\",\n      \"Backdrop\": \"readonly\",\n      \"Button\": \"readonly\",\n      \"Cel\": \"readonly\",\n      \"Cel3d\": \"readonly\",\n      \"CheckBox\": \"readonly\",\n      \"ColorRGBA\": \"readonly\",\n      \"ComboBox\": \"readonly\",\n      \"DateEdit\": \"readonly\",\n      \"DateEditEnum\": \"readonly\",\n      \"Dialog\": \"readonly\",\n      \"Dir\": \"readonly\",\n      \"DirSpec\": \"readonly\",\n      \"Drawing\": \"readonly\",\n      \"DrawingToolParams\": \"readonly\",\n      \"DrawingTools\": \"readonly\",\n      \"EnvelopeCreator\": \"readonly\",\n      \"ExportVideoDlg\": \"readonly\",\n      \"File\": \"readonly\",\n      \"FileAccess\": \"readonly\",\n      \"FileDialog\": \"readonly\",\n      \"GroupBox\": \"readonly\",\n      \"ImportDrawingDlg\": \"readonly\",\n      \"Input\": \"readonly\",\n      \"KeyModifiers\": \"readonly\",\n      \"Label\": \"readonly\",\n      \"LayoutExports\": \"readonly\",\n      \"LayoutExportsParams\": \"readonly\",\n      \"LineEdit\": \"readonly\",\n      \"Matrix4x4\": \"readonly\",\n      \"MessageBox\": \"readonly\",\n      \"MessageLog\": \"readonly\",\n      \"Model3d\": \"readonly\",\n      \"MovieImport\": \"readonly\",\n      \"NumberEdit\": \"readonly\",\n      \"PaletteManager\": \"readonly\",\n      \"PaletteObjectManager\": \"readonly\",\n      \"PermanentFile\": \"readonly\",\n      \"Point2d\": \"readonly\",\n      \"Point3d\": \"readonly\",\n      \"Process\": \"readonly\",\n      \"Process2\": \"readonly\",\n      \"Quaternion\": \"readonly\",\n      \"QuicktimeExporter\": \"readonly\",\n      \"RadioButton\": \"readonly\",\n      \"RemoteCmd\": \"readonly\",\n      \"Scene\": \"readonly\",\n      \"Settings\": \"readonly\",\n      \"Slider\": \"readonly\",\n      \"SpinBox\": \"readonly\",\n      \"SubnodeData\": \"readonly\",\n      \"System\": \"readonly\",\n      \"TemporaryFile\": \"readonly\",\n      \"TextEdit\": \"readonly\",\n      \"TimeEdit\": \"readonly\",\n      \"Timeline\": \"readonly\",\n      \"ToolProperties\": \"readonly\",\n      \"UiLoader\": \"readonly\",\n      \"Vector2d\": \"readonly\",\n      \"Vector3d\": \"readonly\",\n      \"WebCCExporter\": \"readonly\",\n      \"Workspaces\": \"readonly\",\n      \"__scriptManager__\": \"readonly\",\n      \"__temporaryFileContext__\": \"readonly\",\n      \"about\": \"readonly\",\n      \"column\": \"readonly\",\n      \"compositionOrder\": \"readonly\",\n      \"copyPaste\": \"readonly\",\n      \"deformation\": \"readonly\",\n      \"drawingExport\": \"readonly\",\n      \"element\": \"readonly\",\n      \"exporter\": \"readonly\",\n      \"fileMapper\": \"readonly\",\n      \"frame\": \"readonly\",\n      \"func\": \"readonly\",\n      \"library\": \"readonly\",\n      \"node\": \"readonly\",\n      \"preferences\": \"readonly\",\n      \"render\": \"readonly\",\n      \"scene\": \"readonly\",\n      \"selection\": \"readonly\",\n      \"sound\": \"readonly\",\n      \"specialFolders\": \"readonly\",\n      \"translator\": \"readonly\",\n      \"view\": \"readonly\",\n      \"waypoint\": \"readonly\",\n      \"xsheet\": \"readonly\"\n    }\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/js/PypeHarmony.js",
    "content": "/* global include */\n// ***************************************************************************\n// *                        Pype Harmony Host                                *\n// ***************************************************************************\n\nvar LD_OPENHARMONY_PATH = System.getenv('LIB_OPENHARMONY_PATH');\nLD_OPENHARMONY_PATH = LD_OPENHARMONY_PATH + '/openHarmony.js';\nLD_OPENHARMONY_PATH = LD_OPENHARMONY_PATH.replace(/\\\\/g, \"/\");\n\n\n\n/**\n * @namespace\n * @classdesc PypeHarmony encapsulate all Pype related functions.\n * @property  {Object}  loaders   Namespace for Loaders JS code.\n * @property  {Object}  Creators  Namespace for Creators JS code.\n * @property  {Object}  Publish   Namespace for Publish plugins JS code.\n */\nvar PypeHarmony = {\n    Loaders: {},\n    Creators: {},\n    Publish: {}\n};\n\n\n/**\n * Show message in Harmony.\n * @function\n * @param {string} message  Argument containing message.\n */\nPypeHarmony.message = function(message) {\n    MessageBox.information(message);\n};\n\n\n/**\n * Set scene setting based on shot/asset settngs.\n * @function\n * @param {obj} settings  Scene settings.\n */\nPypeHarmony.setSceneSettings = function(settings) {\n    if (settings.fps) {\n        scene.setFrameRate(settings.fps);\n    }\n\n    if (settings.frameStart && settings.frameEnd) {\n        var duration = settings.frameEnd - settings.frameStart + 1;\n\n        if (frame.numberOf() > duration) {\n            frame.remove(duration, frame.numberOf() - duration);\n        }\n\n        if (frame.numberOf() < duration) {\n            frame.insert(duration, duration - frame.numberOf());\n        }\n\n        scene.setStartFrame(1);\n        scene.setStopFrame(duration);\n    }\n    if (settings.resolutionWidth && settings.resolutionHeight) {\n        scene.setDefaultResolution(\n            settings.resolutionWidth, settings.resolutionHeight, 41.112\n        );\n    }\n};\n\n\n/**\n * Get scene settings.\n * @function\n * @return {array} Scene settings.\n */\nPypeHarmony.getSceneSettings = function() {\n    return [\n        about.getApplicationPath(),\n        scene.currentProjectPath(),\n        scene.currentScene(),\n        scene.getFrameRate(),\n        scene.getStartFrame(),\n        scene.getStopFrame(),\n        sound.getSoundtrackAll().path(),\n        scene.defaultResolutionX(),\n        scene.defaultResolutionY(),\n        scene.defaultResolutionFOV()\n    ];\n};\n\n\n/**\n * Set color of nodes.\n * @function\n * @param {array} nodes List of nodes.\n * @param {array} rgba  array of RGBA components of color.\n */\nPypeHarmony.setColor = function(nodes, rgba) {\n    for (var i =0; i <= nodes.length - 1; ++i) {\n        var color = PypeHarmony.color(rgba);\n        node.setColor(nodes[i], color);\n    }\n};\n\n\n/**\n * Extract Template into file.\n * @function\n * @param {array} args  Arguments for template extraction.\n *\n * @example\n * // arguments are in this order:\n * var args = [backdrops, nodes, templateFilename, templateDir];\n *\n */\nPypeHarmony.exportTemplate = function(args) {\n    var tempNode = node.add('Top', 'temp_note', 'NOTE', 0, 0, 0);\n    var templateGroup = node.createGroup(tempNode, 'temp_group');\n    node.deleteNode( templateGroup + '/temp_note' );\n\n    selection.clearSelection();\n    for (var f = 0; f < args[1].length; f++) {\n        selection.addNodeToSelection(args[1][f]);\n    }\n\n    Action.perform('copy()', 'Node View');\n\n    selection.clearSelection();\n    selection.addNodeToSelection(templateGroup);\n    Action.perform('onActionEnterGroup()', 'Node View');\n    Action.perform('paste()', 'Node View');\n\n    // Recreate backdrops in group.\n    for (var i = 0; i < args[0].length; i++) {\n        MessageLog.trace(args[0][i]);\n        Backdrop.addBackdrop(templateGroup, args[0][i]);\n    }\n\n    Action.perform('selectAll()', 'Node View' );\n    copyPaste.createTemplateFromSelection(args[2], args[3]);\n\n    // Unfocus the group in Node view, delete all nodes and backdrops\n    // created during the process.\n    Action.perform('onActionUpToParent()', 'Node View');\n    node.deleteNode(templateGroup, true, true);\n};\n\n\n/**\n * Toggle instance in Harmony.\n * @function\n * @param {array} args  Instance name and value.\n */\nPypeHarmony.toggleInstance = function(args) {\n    node.setEnable(args[0], args[1]);\n};\n\n\n/**\n * Delete node in Harmony.\n * @function\n * @param {string} _node  Node name.\n */\nPypeHarmony.deleteNode = function(_node) {\n    node.deleteNode(_node, true, true);\n};\n\n\n/**\n * Copy file.\n * @function\n * @param {string}  src Source file name.\n * @param {string}  dst Destination file name.\n */\nPypeHarmony.copyFile = function(src, dst) {\n    var srcFile = new PermanentFile(src);\n    var dstFile = new PermanentFile(dst);\n    srcFile.copy(dstFile);\n};\n\n\n/**\n * create RGBA color from array.\n * @function\n * @param   {array}     rgba array of rgba values.\n * @return  {ColorRGBA} ColorRGBA Harmony class.\n */\nPypeHarmony.color = function(rgba) {\n    return new ColorRGBA(rgba[0], rgba[1], rgba[2], rgba[3]);\n};\n\n\n/**\n * get all dependencies for given node.\n * @function\n * @param   {string}  _node node path.\n * @return  {array}   List of dependent nodes.\n */\nPypeHarmony.getDependencies = function(_node) {\n    var target_node = _node;\n    var numInput = node.numberOfInputPorts(target_node);\n    var dependencies = [];\n    for (var i = 0 ; i < numInput; i++) {\n        dependencies.push(node.srcNode(target_node, i));\n    }\n    return dependencies;\n};\n\n\n/**\n * return version of running Harmony instance.\n * @function\n * @return  {array} [major_version, minor_version]\n */\nPypeHarmony.getVersion = function() {\n    return [\n        about.getMajorVersion(),\n        about.getMinorVersion()\n    ];\n};\n"
  },
  {
    "path": "openpype/hosts/harmony/js/README.md",
    "content": "## Pype - ToonBoom Harmony integration\n\n### Development\n\n#### Setting up ESLint as linter for javascript code\n\nYou nee [node.js](https://nodejs.org/en/) installed. All you need to do then\nis to run:\n\n```sh\nnpm install\n```\nin **js** directory. This will install eslint and all requirements locally.\n\nIn [Atom](https://atom.io/) it is enough to install [linter-eslint](https://atom.io/packages/lintecr-eslint) and set global *npm* prefix in its settings.\n"
  },
  {
    "path": "openpype/hosts/harmony/js/creators/CreateRender.js",
    "content": "/* global PypeHarmony:writable, include */\n// ***************************************************************************\n// *                             CreateRender                                *\n// ***************************************************************************\n\n\n// check if PypeHarmony is defined and if not, load it.\nif (typeof PypeHarmony === 'undefined') {\n    var OPENPYPE_HARMONY_JS = System.getenv('OPENPYPE_HARMONY_JS') + '/PypeHarmony.js';\n    include(OPENPYPE_HARMONY_JS.replace(/\\\\/g, \"/\"));\n}\n\n\n/**\n * @namespace\n * @classdesc Code creating render containers in Harmony.\n */\nvar CreateRender = function() {};\n\n\n/**\n * Create render instance.\n * @function\n * @param {array} args Arguments for instance.\n */\nCreateRender.prototype.create = function(args) {\n    node.setTextAttr(args[0], 'DRAWING_TYPE', 1, 'PNG4');\n    node.setTextAttr(args[0], 'DRAWING_NAME', 1, args[1]);\n    node.setTextAttr(args[0], 'MOVIE_PATH', 1, args[1]);\n};\n\n// add self to Pype Loaders\nPypeHarmony.Creators.CreateRender = new CreateRender();\n"
  },
  {
    "path": "openpype/hosts/harmony/js/loaders/ImageSequenceLoader.js",
    "content": "/* global PypeHarmony:writable, include */\n// ***************************************************************************\n// *                        ImageSequenceLoader                              *\n// ***************************************************************************\n\n// check if PypeHarmony is defined and if not, load it.\nif (typeof PypeHarmony === 'undefined') {\n    var OPENPYPE_HARMONY_JS = System.getenv('OPENPYPE_HARMONY_JS') + '/PypeHarmony.js';\n    include(OPENPYPE_HARMONY_JS.replace(/\\\\/g, \"/\"));\n}\n\nif (typeof $ === 'undefined'){\n    $ = this.__proto__['$'];\n}\n\n/**\n * @namespace\n * @classdesc Image Sequence loader JS code.\n */\nvar ImageSequenceLoader = function() {\n    this.PNGTransparencyMode = 0; // Premultiplied with Black\n    this.TGATransparencyMode = 0; // Premultiplied with Black\n    this.SGITransparencyMode = 0; // Premultiplied with Black\n    this.LayeredPSDTransparencyMode = 1; // Straight\n    this.FlatPSDTransparencyMode = 2; // Premultiplied with White\n};\n\n\nImageSequenceLoader.getCurrentGroup = function () {\n    var doc = $.scn;\n    var nodeView = '';\n    for (var i = 0; i < 200; i++) {\n        nodeView = 'View' + (i);\n        if (view.type(nodeView) == 'Node View') {\n            break;\n        }\n    }\n\n    if (!nodeView) {\n        $.alert('You must have a Node View open!',\n            'No Node View is currently open!\\n' +\n            'Open a Node View and Try Again.',\n            'OK!');\n        return;\n    }\n\n    var currentGroup;\n    if (!nodeView) {\n        currentGroup = doc.root;\n    } else {\n        currentGroup = doc.$node(view.group(nodeView));\n    }\n\n    return currentGroup.path;\n};\n\n\n/**\n * Get unique column name.\n * @function\n * @param  {string}  columnPrefix Column name.\n * @return {string}  Unique column name.\n */\nImageSequenceLoader.getUniqueColumnName = function(columnPrefix) {\n    var suffix = 0;\n    // finds if unique name for a column\n    var columnName = columnPrefix;\n    while (suffix < 2000) {\n        if (!column.type(columnName)) {\n            break;\n        }\n\n        suffix = suffix + 1;\n        columnName = columnPrefix + '_' + suffix;\n    }\n    return columnName;\n};\n\n\n/**\n * Import file sequences into Harmony.\n * @function\n * @param  {object}  args  Arguments for import, see Example.\n * @return {string}  Read node name\n *\n * @example\n * // Arguments are in following order:\n * var args = [\n *    files, // Files in file sequences.\n *    asset, // Asset name.\n *    subset, // Subset name.\n *    startFrame, // Sequence starting frame.\n *    groupId // Unique group ID (uuid4).\n * ];\n */\nImageSequenceLoader.prototype.importFiles = function(args) {\n    MessageLog.trace(\"ImageSequence:: \" + typeof PypeHarmony);\n    MessageLog.trace(\"ImageSequence $:: \" + typeof $);\n    MessageLog.trace(\"ImageSequence OH:: \" + typeof PypeHarmony.OpenHarmony);\n    var PNGTransparencyMode = 0; // Premultiplied with Black\n    var TGATransparencyMode = 0; // Premultiplied with Black\n    var SGITransparencyMode = 0; // Premultiplied with Black\n    var LayeredPSDTransparencyMode = 1; // Straight\n    var FlatPSDTransparencyMode = 2; // Premultiplied with White\n\n    var doc = $.scn;\n    var files = args[0];\n    var asset = args[1];\n    var subset = args[2];\n    var startFrame = args[3];\n    var groupId = args[4];\n    var vectorFormat = null;\n    var extension = null;\n    var filename = files[0];\n    var pos = filename.lastIndexOf('.');\n    if (pos < 0) {\n        return null;\n    }\n\n    // Get the current group\n    var currentGroup = doc.$node(ImageSequenceLoader.getCurrentGroup());\n\n    // Get a unique iterative name for the container read node\n    var num = 0;\n    var name = '';\n    do {\n        name = asset + '_' + (num++) + '_' + subset;\n    } while (currentGroup.getNodeByName(name) != null);\n\n    extension = filename.substr(pos+1).toLowerCase();\n    if (extension == 'jpeg') {\n        extension = 'jpg';\n    }\n\n    if (extension == 'tvg') {\n        vectorFormat = 'TVG';\n        extension ='SCAN'; // element.add() will use this.\n    }\n\n    var elemId = element.add(\n        name,\n        'BW',\n        scene.numberOfUnitsZ(),\n        extension.toUpperCase(),\n        vectorFormat\n    );\n\n    if (elemId == -1) {\n        // hum, unknown file type most likely -- let's skip it.\n        return null; // no read to add.\n    }\n\n    var uniqueColumnName = ImageSequenceLoader.getUniqueColumnName(name);\n    column.add(uniqueColumnName, 'DRAWING');\n    column.setElementIdOfDrawing(uniqueColumnName, elemId);\n    var read = node.add(currentGroup, name, 'READ', 0, 0, 0);\n    var transparencyAttr = node.getAttr(\n        read, frame.current(), 'READ_TRANSPARENCY'\n    );\n    var opacityAttr = node.getAttr(read, frame.current(), 'OPACITY');\n    transparencyAttr.setValue(true);\n    opacityAttr.setValue(true);\n    var alignmentAttr = node.getAttr(read, frame.current(), 'ALIGNMENT_RULE');\n    alignmentAttr.setValue('ASIS');\n    var transparencyModeAttr = node.getAttr(\n        read, frame.current(), 'applyMatteToColor'\n    );\n    if (extension === 'png') {\n        transparencyModeAttr.setValue(PNGTransparencyMode);\n    }\n    if (extension === 'tga') {\n        transparencyModeAttr.setValue(TGATransparencyMode);\n    }\n    if (extension === 'sgi') {\n        transparencyModeAttr.setValue(SGITransparencyMode);\n    }\n    if (extension === 'psd') {\n        transparencyModeAttr.setValue(FlatPSDTransparencyMode);\n    }\n    if (extension === 'jpg') {\n        transparencyModeAttr.setValue(LayeredPSDTransparencyMode);\n    }\n\n    var drawingFilePath;\n    var timing;\n    node.linkAttr(read, 'DRAWING.ELEMENT', uniqueColumnName);\n    if (files.length === 1) {\n        // Create a drawing drawing, 'true' indicate that the file exists.\n        Drawing.create(elemId, 1, true);\n        // Get the actual path, in tmp folder.\n        drawingFilePath = Drawing.filename(elemId, '1');\n        PypeHarmony.copyFile(files[0], drawingFilePath);\n        // Expose the image for the entire frame range.\n        for (var i =0; i <= frame.numberOf() - 1; ++i) {\n            timing = startFrame + i;\n            column.setEntry(uniqueColumnName, 1, timing, '1');\n        }\n    } else {\n        // Create a drawing for each file.\n        for (var j =0; j <= files.length - 1; ++j) {\n            timing = startFrame + j;\n            // Create a drawing drawing, 'true' indicate that the file exists.\n            Drawing.create(elemId, timing, true);\n            // Get the actual path, in tmp folder.\n            drawingFilePath = Drawing.filename(elemId, timing.toString());\n            PypeHarmony.copyFile(files[j], drawingFilePath);\n            column.setEntry(uniqueColumnName, 1, timing, timing.toString());\n        }\n    }\n    var greenColor = new ColorRGBA(0, 255, 0, 255);\n    node.setColor(read, greenColor);\n\n    // Add uuid to attribute of the container read node\n    node.createDynamicAttr(read, 'STRING', 'uuid', 'uuid', false);\n    node.setTextAttr(read, 'uuid', 1.0, groupId);\n    return read;\n};\n\n\n/**\n * Replace files sequences in Harmony.\n * @function\n * @param  {object}  args  Arguments for import, see Example.\n * @return {string}  Read node name\n *\n * @example\n * // Arguments are in following order:\n * var args = [\n *    files, // Files in file sequences\n *    name, // Node name\n *    startFrame // Sequence starting frame\n * ];\n */\nImageSequenceLoader.prototype.replaceFiles = function(args) {\n    var files = args[0];\n    MessageLog.trace(files);\n    MessageLog.trace(files.length);\n    var _node = args[1];\n    var startFrame = args[2];\n    var _column = node.linkedColumn(_node, 'DRAWING.ELEMENT');\n    var elemId = column.getElementIdOfDrawing(_column);\n    // Delete existing drawings.\n    var timings = column.getDrawingTimings(_column);\n    for ( var i =0; i <= timings.length - 1; ++i) {\n        column.deleteDrawingAt(_column, parseInt(timings[i]));\n    }\n    var filename = files[0];\n    var pos = filename.lastIndexOf('.');\n    if (pos < 0) {\n        return null;\n    }\n    var extension = filename.substr(pos+1).toLowerCase();\n    if (extension === 'jpeg') {\n        extension = 'jpg';\n    }\n\n    var transparencyModeAttr = node.getAttr(\n        _node, frame.current(), 'applyMatteToColor'\n    );\n    if (extension === 'png') {\n        transparencyModeAttr.setValue(this.PNGTransparencyMode);\n    }\n    if (extension === 'tga') {\n        transparencyModeAttr.setValue(this.TGATransparencyMode);\n    }\n    if (extension === 'sgi') {\n        transparencyModeAttr.setValue(this.SGITransparencyMode);\n    }\n    if (extension == 'psd') {\n        transparencyModeAttr.setValue(this.FlatPSDTransparencyMode);\n    }\n    if (extension === 'jpg') {\n        transparencyModeAttr.setValue(this.LayeredPSDTransparencyMode);\n    }\n\n    var drawingFilePath;\n    var timing;\n    if (files.length == 1) {\n        // Create a drawing drawing, 'true' indicate that the file exists.\n        Drawing.create(elemId, 1, true);\n        // Get the actual path, in tmp folder.\n        drawingFilePath = Drawing.filename(elemId, '1');\n        PypeHarmony.copyFile(files[0], drawingFilePath);\n        MessageLog.trace(files[0]);\n        MessageLog.trace(drawingFilePath);\n        // Expose the image for the entire frame range.\n        for (var k =0; k <= frame.numberOf() - 1; ++k) {\n            timing = startFrame + k;\n            column.setEntry(_column, 1, timing, '1');\n        }\n    } else {\n        // Create a drawing for each file.\n        for (var l =0; l <= files.length - 1; ++l) {\n            timing = startFrame + l;\n            // Create a drawing drawing, 'true' indicate that the file exists.\n            Drawing.create(elemId, timing, true);\n            // Get the actual path, in tmp folder.\n            drawingFilePath = Drawing.filename(elemId, timing.toString());\n            PypeHarmony.copyFile( files[l], drawingFilePath );\n            column.setEntry(_column, 1, timing, timing.toString());\n        }\n    }\n    var greenColor = new ColorRGBA(0, 255, 0, 255);\n    node.setColor(_node, greenColor);\n};\n\n// add self to Pype Loaders\nPypeHarmony.Loaders.ImageSequenceLoader = new ImageSequenceLoader();\n"
  },
  {
    "path": "openpype/hosts/harmony/js/loaders/TemplateLoader.js",
    "content": "/* global PypeHarmony:writable, include */\n// ***************************************************************************\n// *                        TemplateLoader                                   *\n// ***************************************************************************\n\n\n// check if PypeHarmony is defined and if not, load it.\nif (typeof PypeHarmony === 'undefined') {\n    var OPENPYPE_HARMONY_JS = System.getenv('OPENPYPE_HARMONY_JS') + '/PypeHarmony.js';\n    include(OPENPYPE_HARMONY_JS.replace(/\\\\/g, \"/\"));\n}\n\nif (typeof $ === 'undefined'){\n    $ = this.__proto__['$'];\n}\n/**\n * @namespace\n * @classdesc Image Sequence loader JS code.\n */\nvar TemplateLoader = function() {};\n\n\n/**\n * Load template as container.\n * @function\n * @param {array} args Arguments, see example.\n * @return {string} Name of container.\n *\n * @example\n * // arguments are in following order:\n * var args = [\n *  templatePath, // Path to tpl file.\n *  assetName,    // Asset name.\n *  subsetName,   // Subset name.\n *  groupId       // unique ID (uuid4)\n * ];\n */\nTemplateLoader.prototype.loadContainer = function(args) {\n    var doc = $.scn;\n    var templatePath = args[0];\n    var assetName = args[1];\n    var subset = args[2];\n    var groupId = args[3];\n\n    // Get the current group\n    var nodeViewWidget = $.app.getWidgetByName('Node View');\n    if (!nodeViewWidget) {\n        $.alert('You must have a Node View open!', 'No Node View!', 'OK!');\n        return;\n    }\n\n    nodeViewWidget.setFocus();\n    var currentGroup;\n    var nodeView = view.currentView();\n    if (!nodeView) {\n        currentGroup = doc.root;\n    } else {\n        currentGroup = doc.$node(view.group(nodeView));\n    }\n\n    // Get a unique iterative name for the container group\n    var num = 0;\n    var containerGroupName = '';\n    do {\n        containerGroupName = assetName + '_' + (num++) + '_' + subset;\n    } while (currentGroup.getNodeByName(containerGroupName) != null);\n\n    // import the template\n    var tplNodes = currentGroup.importTemplate(templatePath);\n    MessageLog.trace(tplNodes);\n    // Create the container group\n    var groupNode = currentGroup.addGroup(\n        containerGroupName, false, false, tplNodes);\n\n    // Add uuid to attribute of the container group\n    node.createDynamicAttr(groupNode, 'STRING', 'uuid', 'uuid', false);\n    node.setTextAttr(groupNode, 'uuid', 1.0, groupId);\n\n    return String(groupNode);\n};\n\n\n/**\n * Replace existing node container.\n * @function\n * @param  {string}  dstNodePath Harmony path to destination Node.\n * @param  {string}  srcNodePath Harmony path to source Node.\n * @param  {string}  renameSrc   ...\n * @param  {boolean} cloneSrc    ...\n * @return {boolean}             Success\n * @todo   This is work in progress.\n */\nTemplateLoader.prototype.replaceNode = function(\n    dstNodePath, srcNodePath, renameSrc, cloneSrc) {\n    var doc = $.scn;\n    var srcNode = doc.$node(srcNodePath);\n    var dstNode = doc.$node(dstNodePath);\n    // var dstNodeName = dstNode.name;\n    var replacementNode = srcNode;\n    // var dstGroup = dstNode.group;\n    $.beginUndo();\n    if (cloneSrc) {\n        replacementNode = doc.$node(\n            $.nodeTools.copy_paste_node(\n                srcNodePath, dstNode.name + '_CLONE', dstNode.group.path));\n    } else {\n        if (replacementNode.group.path != srcNode.group.path) {\n            replacementNode.moveToGroup(dstNode);\n        }\n    }\n    var inLinks = dstNode.getInLinks();\n    var link, inNode, inPort, outPort, outNode, success;\n    for (var l in inLinks) {\n        if (Object.prototype.hasOwnProperty.call(inLinks, l)) {\n            link = inLinks[l];\n            inPort = Number(link.inPort);\n            outPort = Number(link.outPort);\n            outNode = link.outNode;\n            success = replacementNode.linkInNode(outNode, inPort, outPort);\n            if (success) {\n                $.log('Successfully connected ' + outNode + ' : ' +\n            outPort + ' -> ' + replacementNode + ' : ' + inPort);\n            } else {\n                $.alert('Failed to connect ' + outNode + ' : ' +\n            outPort + ' -> ' + replacementNode + ' : ' + inPort);\n            }\n        }\n    }\n\n    var outLinks = dstNode.getOutLinks();\n    for (l in outLinks) {\n        if (Object.prototype.hasOwnProperty.call(outLinks, l)) {\n            link = outLinks[l];\n            inPort = Number(link.inPort);\n            outPort = Number(link.outPort);\n            inNode = link.inNode;\n            // first we must disconnect the port from the node being\n            // replaced to this links inNode port\n            inNode.unlinkInPort(inPort);\n            success = replacementNode.linkOutNode(inNode, outPort, inPort);\n            if (success) {\n                $.log('Successfully connected ' + inNode + ' : ' +\n              inPort + ' <- ' + replacementNode + ' : ' + outPort);\n            } else {\n                if (inNode.type == 'MultiLayerWrite') {\n                    $.log('Attempting standard api to connect the nodes...');\n                    success = node.link(\n                        replacementNode, outPort, inNode,\n                        inPort, node.numberOfInputPorts(inNode) + 1);\n                    if (success) {\n                        $.log('Successfully connected ' + inNode + ' : ' +\n                inPort + ' <- ' + replacementNode + ' : ' + outPort);\n                    }\n                }\n            }\n            if (!success) {\n                $.alert('Failed to connect ' + inNode + ' : ' +\n            inPort + ' <- ' + replacementNode + ' : ' + outPort);\n                return false;\n            }\n        }\n    }\n};\n\n\nTemplateLoader.prototype.askForColumnsUpdate = function() {\n    // Ask user if they want to also update columns and\n    // linked attributes here\n    return ($.confirm(\n        'Would you like to update in place and reconnect all \\n' +\n      'ins/outs, attributes, and columns?',\n        'Update & Replace?\\n' +\n      'If you choose No, the version will only be loaded.',\n        'Yes',\n        'No'));\n};\n\n// add self to Pype Loaders\nPypeHarmony.Loaders.TemplateLoader = new TemplateLoader();\n"
  },
  {
    "path": "openpype/hosts/harmony/js/publish/CollectCurrentFile.js",
    "content": "/* global PypeHarmony:writable, include */\n// ***************************************************************************\n// *                        CollectCurrentFile                               *\n// ***************************************************************************\n\n\n// check if PypeHarmony is defined and if not, load it.\nif (typeof PypeHarmony === 'undefined') {\n    var OPENPYPE_HARMONY_JS = System.getenv('OPENPYPE_HARMONY_JS') + '/PypeHarmony.js';\n    include(OPENPYPE_HARMONY_JS.replace(/\\\\/g, \"/\"));\n}\n\n\n/**\n * @namespace\n * @classdesc Collect Current file\n */\nvar CollectCurrentFile = function() {};\n\nCollectCurrentFile.prototype.collect = function() {\n    return (\n        scene.currentProjectPath() + '/' +\n            scene.currentVersionName() + '.xstage'\n    );\n};\n\n// add self to Pype Loaders\nPypeHarmony.Publish.CollectCurrentFile = new CollectCurrentFile();\n"
  },
  {
    "path": "openpype/hosts/harmony/js/publish/CollectFarmRender.js",
    "content": "/* global PypeHarmony:writable, include */\n// ***************************************************************************\n// *                        CollectFarmRender                                *\n// ***************************************************************************\n\n\n// check if PypeHarmony is defined and if not, load it.\nif (typeof PypeHarmony === 'undefined') {\n    var OPENPYPE_HARMONY_JS = System.getenv('OPENPYPE_HARMONY_JS') + '/PypeHarmony.js';\n    include(OPENPYPE_HARMONY_JS.replace(/\\\\/g, \"/\"));\n}\n\n\n/**\n * @namespace\n * @classdesc Image Sequence loader JS code.\n */\nvar CollectFarmRender = function() {};\n\n\n/**\n * Get information important for render output.\n * @function\n * @param node {String} node name.\n * @return {array} array of render info.\n *\n * @example\n *\n * var ret = [\n *    file_prefix, // like foo/bar-\n *    type, // PNG4, ...\n *    leading_zeros, // 3 - for 0001\n *    start // start frame\n * ]\n */\nCollectFarmRender.prototype.getRenderNodeSettings = function(n) {\n    // this will return\n    var output = [\n        node.getTextAttr(\n            n, frame.current(), 'DRAWING_NAME'),\n        node.getTextAttr(\n            n, frame.current(), 'DRAWING_TYPE'),\n        node.getTextAttr(\n            n, frame.current(), 'LEADING_ZEROS'),\n        node.getTextAttr(n, frame.current(), 'START'),\n        node.getEnable(n)\n    ];\n\n    return output;\n};\n\n// add self to Pype Loaders\nPypeHarmony.Publish.CollectFarmRender = new CollectFarmRender();\n"
  },
  {
    "path": "openpype/hosts/harmony/js/publish/CollectPalettes.js",
    "content": "/* global PypeHarmony:writable, include */\n// ***************************************************************************\n// *                        CollectPalettes                                  *\n// ***************************************************************************\n\n\n// check if PypeHarmony is defined and if not, load it.\nif (typeof PypeHarmony === 'undefined') {\n    var OPENPYPE_HARMONY_JS = System.getenv('OPENPYPE_HARMONY_JS') + '/PypeHarmony.js';\n    include(OPENPYPE_HARMONY_JS.replace(/\\\\/g, \"/\"));\n}\n\n\n/**\n * @namespace\n * @classdesc Image Sequence loader JS code.\n */\nvar CollectPalettes = function() {};\n\nCollectPalettes.prototype.getPalettes = function() {\n    var palette_list = PaletteObjectManager.getScenePaletteList();\n\n    var palettes = {};\n    for(var i=0; i < palette_list.numPalettes; ++i) {\n        var palette = palette_list.getPaletteByIndex(i);\n        palettes[palette.getName()] = palette.id;\n    }\n\n    return palettes;\n};\n\n// add self to Pype Loaders\nPypeHarmony.Publish.CollectPalettes = new CollectPalettes();\n"
  },
  {
    "path": "openpype/hosts/harmony/js/publish/ExtractPalette.js",
    "content": "/* global PypeHarmony:writable, include */\n// ***************************************************************************\n// *                           ExtractPalette                                *\n// ***************************************************************************\n\n\n// check if PypeHarmony is defined and if not, load it.\nif (typeof PypeHarmony === 'undefined') {\n    var OPENPYPE_HARMONY_JS = System.getenv('OPENPYPE_HARMONY_JS') + '/PypeHarmony.js';\n    include(OPENPYPE_HARMONY_JS.replace(/\\\\/g, \"/\"));\n}\n\n/**\n * @namespace\n * @classdesc Code for extracting palettes.\n */\nvar ExtractPalette = function() {};\n\n\n/**\n * Get palette from Harmony.\n * @function\n * @param   {string} paletteId ID of palette to get.\n * @return  {array}  [paletteName, palettePath]\n */\nExtractPalette.prototype.getPalette = function(paletteId) {\n    var palette_list = PaletteObjectManager.getScenePaletteList();\n    var palette = palette_list.getPaletteById(paletteId);\n    var palette_name = palette.getName();\n    return [\n        palette_name,\n        (palette.getPath() + '/' + palette.getName() + '.plt')\n    ];  \n};\n\n// add self to Pype Loaders\nPypeHarmony.Publish.ExtractPalette = new ExtractPalette();\n"
  },
  {
    "path": "openpype/hosts/harmony/js/publish/ExtractTemplate.js",
    "content": "/* global PypeHarmony:writable, include */\n// ***************************************************************************\n// *                           ExtractTemplate                               *\n// ***************************************************************************\n\n\n// check if PypeHarmony is defined and if not, load it.\nif (typeof PypeHarmony === 'undefined') {\n    var OPENPYPE_HARMONY_JS = System.getenv('OPENPYPE_HARMONY_JS') + '/PypeHarmony.js';\n    include(OPENPYPE_HARMONY_JS.replace(/\\\\/g, \"/\"));\n}\n\n\n/**\n * @namespace\n * @classdesc Code for extracting palettes.\n */\nvar ExtractTemplate = function() {};\n\n\n/**\n * Get backdrops for given node.\n * @function\n * @param   {string} probeNode Node path to probe for backdrops.\n * @return  {array} list of backdrops.\n */\nExtractTemplate.prototype.getBackdropsByNode = function(probeNode) {\n    var backdrops = Backdrop.backdrops('Top');\n    var valid_backdrops = [];\n    for(var i=0; i<backdrops.length; i++)\n    {\n        var position = backdrops[i].position;\n\n        var x_valid = false;\n        var node_x = node.coordX(probeNode);\n        if (position.x < node_x && node_x < (position.x + position.w)){\n            x_valid = true;\n        }\n\n        var y_valid = false;\n        var node_y = node.coordY(probeNode);\n        if (position.y < node_y && node_y < (position.y + position.h)){\n            y_valid = true;\n        }\n\n        if (x_valid && y_valid){\n            valid_backdrops.push(backdrops[i]);\n        }\n    }\n    return valid_backdrops;\n};\n\n// add self to Pype Loaders\nPypeHarmony.Publish.ExtractTemplate = new ExtractTemplate();\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/harmony/plugins/create/create_farm_render.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Create Composite node for render on farm.\"\"\"\nimport openpype.hosts.harmony.api as harmony\nfrom openpype.hosts.harmony.api import plugin\n\n\nclass CreateFarmRender(plugin.Creator):\n    \"\"\"Composite node for publishing renders.\"\"\"\n\n    name = \"renderDefault\"\n    label = \"Render on Farm\"\n    family = \"renderFarm\"\n    node_type = \"WRITE\"\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"Constructor.\"\"\"\n        super(CreateFarmRender, self).__init__(*args, **kwargs)\n\n    def setup_node(self, node):\n        \"\"\"Set render node.\"\"\"\n        path = \"render/{0}/{0}.\".format(node.split(\"/\")[-1])\n        harmony.send(\n            {\n                \"function\": f\"PypeHarmony.Creators.CreateRender.create\",\n                \"args\": [node, path]\n            })\n        harmony.send(\n            {\n                \"function\": f\"PypeHarmony.color\",\n                \"args\": [[0.9, 0.75, 0.3, 1.0]]\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/create/create_render.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Create render node.\"\"\"\nimport openpype.hosts.harmony.api as harmony\nfrom openpype.hosts.harmony.api import plugin\n\n\nclass CreateRender(plugin.Creator):\n    \"\"\"Composite node for publishing renders.\"\"\"\n\n    name = \"renderDefault\"\n    label = \"Render\"\n    family = \"render\"\n    node_type = \"WRITE\"\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"Constructor.\"\"\"\n        super(CreateRender, self).__init__(*args, **kwargs)\n\n    def setup_node(self, node):\n        \"\"\"Set render node.\"\"\"\n        self_name = self.__class__.__name__\n        path = \"render/{0}/{0}.\".format(node.split(\"/\")[-1])\n        harmony.send(\n            {\n                \"function\": f\"PypeHarmony.Creators.{self_name}.create\",\n                \"args\": [node, path]\n            })\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/create/create_template.py",
    "content": "from openpype.hosts.harmony.api import plugin\n\n\nclass CreateTemplate(plugin.Creator):\n    \"\"\"Composite node for publishing to templates.\"\"\"\n\n    name = \"templateDefault\"\n    label = \"Template\"\n    family = \"harmony.template\"\n\n    def __init__(self, *args, **kwargs):\n        super(CreateTemplate, self).__init__(*args, **kwargs)\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/load/load_audio.py",
    "content": "from openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nimport openpype.hosts.harmony.api as harmony\n\nsig = harmony.signature()\nfunc = \"\"\"\nfunction getUniqueColumnName( column_prefix )\n{\n    var suffix = 0;\n    // finds if unique name for a column\n    var column_name = column_prefix;\n    while(suffix < 2000)\n    {\n        if(!column.type(column_name))\n        break;\n\n        suffix = suffix + 1;\n        column_name = column_prefix + \"_\" + suffix;\n    }\n    return column_name;\n}\n\nfunction %s(args)\n{\n    var uniqueColumnName = getUniqueColumnName(args[0]);\n    column.add(uniqueColumnName , \"SOUND\");\n    column.importSound(uniqueColumnName, 1, args[1]);\n}\n%s\n\"\"\" % (sig, sig)\n\n\nclass ImportAudioLoader(load.LoaderPlugin):\n    \"\"\"Import audio.\"\"\"\n\n    families = [\"shot\", \"audio\"]\n    representations = [\"wav\"]\n    label = \"Import Audio\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        wav_file = get_representation_path(context[\"representation\"])\n        harmony.send(\n            {\"function\": func, \"args\": [context[\"subset\"][\"name\"], wav_file]}\n        )\n\n        subset_name = context[\"subset\"][\"name\"]\n\n        return harmony.containerise(\n            subset_name,\n            namespace,\n            subset_name,\n            context,\n            self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n        pass\n\n    def remove(self, container):\n        pass\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/load/load_background.py",
    "content": "import os\nimport json\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.pipeline.context_tools import is_representation_from_latest\nimport openpype.hosts.harmony.api as harmony\n\n\ncopy_files = \"\"\"function copyFile(srcFilename, dstFilename)\n{\n    var srcFile = new PermanentFile(srcFilename);\n    var dstFile = new PermanentFile(dstFilename);\n    srcFile.copy(dstFile);\n}\n\"\"\"\n\nimport_files = \"\"\"var PNGTransparencyMode = 1; //Premultiplied with Black\nvar TGATransparencyMode = 0; //Premultiplied with Black\nvar SGITransparencyMode = 0; //Premultiplied with Black\nvar LayeredPSDTransparencyMode = 1; //Straight\nvar FlatPSDTransparencyMode = 2; //Premultiplied with White\n\nfunction getUniqueColumnName( column_prefix )\n{\n    var suffix = 0;\n    // finds if unique name for a column\n    var column_name = column_prefix;\n    while(suffix < 2000)\n    {\n        if(!column.type(column_name))\n        break;\n\n        suffix = suffix + 1;\n        column_name = column_prefix + \"_\" + suffix;\n    }\n    return column_name;\n}\n\nfunction import_files(args)\n{\n    var root = args[0];\n    var files = args[1];\n    var name = args[2];\n    var start_frame = args[3];\n\n    var vectorFormat = null;\n    var extension = null;\n    var filename = files[0];\n\n    var pos = filename.lastIndexOf(\".\");\n    if( pos < 0 )\n        return null;\n\n    extension = filename.substr(pos+1).toLowerCase();\n\n    if(extension == \"jpeg\")\n        extension = \"jpg\";\n    if(extension == \"tvg\")\n    {\n        vectorFormat = \"TVG\"\n        extension =\"SCAN\"; // element.add() will use this.\n    }\n\n    var elemId = element.add(\n        name,\n        \"BW\",\n        scene.numberOfUnitsZ(),\n        extension.toUpperCase(),\n        vectorFormat\n    );\n    if (elemId == -1)\n    {\n        // hum, unknown file type most likely -- let's skip it.\n        return null; // no read to add.\n    }\n\n    var uniqueColumnName = getUniqueColumnName(name);\n    column.add(uniqueColumnName , \"DRAWING\");\n    column.setElementIdOfDrawing(uniqueColumnName, elemId);\n\n    var read = node.add(root, name, \"READ\", 0, 0, 0);\n    var transparencyAttr = node.getAttr(\n        read, frame.current(), \"READ_TRANSPARENCY\"\n    );\n    var opacityAttr = node.getAttr(read, frame.current(), \"OPACITY\");\n    transparencyAttr.setValue(true);\n    opacityAttr.setValue(true);\n\n    var alignmentAttr = node.getAttr(read, frame.current(), \"ALIGNMENT_RULE\");\n    alignmentAttr.setValue(\"ASIS\");\n\n    var transparencyModeAttr = node.getAttr(\n        read, frame.current(), \"applyMatteToColor\"\n    );\n    if (extension == \"png\")\n        transparencyModeAttr.setValue(PNGTransparencyMode);\n    if (extension == \"tga\")\n        transparencyModeAttr.setValue(TGATransparencyMode);\n    if (extension == \"sgi\")\n        transparencyModeAttr.setValue(SGITransparencyMode);\n    if (extension == \"psd\")\n        transparencyModeAttr.setValue(FlatPSDTransparencyMode);\n    if (extension == \"jpg\")\n        transparencyModeAttr.setValue(LayeredPSDTransparencyMode);\n\n    node.linkAttr(read, \"DRAWING.ELEMENT\", uniqueColumnName);\n\n    if (files.length == 1)\n    {\n        // Create a drawing drawing, 'true' indicate that the file exists.\n        Drawing.create(elemId, 1, true);\n        // Get the actual path, in tmp folder.\n        var drawingFilePath = Drawing.filename(elemId, \"1\");\n        copyFile(files[0], drawingFilePath);\n        // Expose the image for the entire frame range.\n        for( var i =0; i <= frame.numberOf() - 1; ++i)\n        {\n            timing = start_frame + i\n            column.setEntry(uniqueColumnName, 1, timing, \"1\");\n        }\n    } else {\n        // Create a drawing for each file.\n        for( var i =0; i <= files.length - 1; ++i)\n        {\n            timing = start_frame + i\n            // Create a drawing drawing, 'true' indicate that the file exists.\n            Drawing.create(elemId, timing, true);\n            // Get the actual path, in tmp folder.\n            var drawingFilePath = Drawing.filename(elemId, timing.toString());\n            copyFile( files[i], drawingFilePath );\n\n            column.setEntry(uniqueColumnName, 1, timing, timing.toString());\n        }\n    }\n\n    var green_color = new ColorRGBA(0, 255, 0, 255);\n    node.setColor(read, green_color);\n\n    return read;\n}\nimport_files\n\"\"\"\n\nreplace_files = \"\"\"var PNGTransparencyMode = 1; //Premultiplied with Black\nvar TGATransparencyMode = 0; //Premultiplied with Black\nvar SGITransparencyMode = 0; //Premultiplied with Black\nvar LayeredPSDTransparencyMode = 1; //Straight\nvar FlatPSDTransparencyMode = 2; //Premultiplied with White\n\nfunction replace_files(args)\n{\n    var files = args[0];\n    MessageLog.trace(files);\n    MessageLog.trace(files.length);\n    var _node = args[1];\n    var start_frame = args[2];\n\n    var _column = node.linkedColumn(_node, \"DRAWING.ELEMENT\");\n    var elemId = column.getElementIdOfDrawing(_column);\n\n    // Delete existing drawings.\n    var timings = column.getDrawingTimings(_column);\n    for( var i =0; i <= timings.length - 1; ++i)\n    {\n        column.deleteDrawingAt(_column, parseInt(timings[i]));\n    }\n\n\n    var filename = files[0];\n    var pos = filename.lastIndexOf(\".\");\n    if( pos < 0 )\n        return null;\n    var extension = filename.substr(pos+1).toLowerCase();\n\n    if(extension == \"jpeg\")\n        extension = \"jpg\";\n\n    var transparencyModeAttr = node.getAttr(\n        _node, frame.current(), \"applyMatteToColor\"\n    );\n    if (extension == \"png\")\n        transparencyModeAttr.setValue(PNGTransparencyMode);\n    if (extension == \"tga\")\n        transparencyModeAttr.setValue(TGATransparencyMode);\n    if (extension == \"sgi\")\n        transparencyModeAttr.setValue(SGITransparencyMode);\n    if (extension == \"psd\")\n        transparencyModeAttr.setValue(FlatPSDTransparencyMode);\n    if (extension == \"jpg\")\n        transparencyModeAttr.setValue(LayeredPSDTransparencyMode);\n\n    if (files.length == 1)\n    {\n        // Create a drawing drawing, 'true' indicate that the file exists.\n        Drawing.create(elemId, 1, true);\n        // Get the actual path, in tmp folder.\n        var drawingFilePath = Drawing.filename(elemId, \"1\");\n        copyFile(files[0], drawingFilePath);\n        MessageLog.trace(files[0]);\n        MessageLog.trace(drawingFilePath);\n        // Expose the image for the entire frame range.\n        for( var i =0; i <= frame.numberOf() - 1; ++i)\n        {\n            timing = start_frame + i\n            column.setEntry(_column, 1, timing, \"1\");\n        }\n    } else {\n        // Create a drawing for each file.\n        for( var i =0; i <= files.length - 1; ++i)\n        {\n            timing = start_frame + i\n            // Create a drawing drawing, 'true' indicate that the file exists.\n            Drawing.create(elemId, timing, true);\n            // Get the actual path, in tmp folder.\n            var drawingFilePath = Drawing.filename(elemId, timing.toString());\n            copyFile( files[i], drawingFilePath );\n\n            column.setEntry(_column, 1, timing, timing.toString());\n        }\n    }\n\n    var green_color = new ColorRGBA(0, 255, 0, 255);\n    node.setColor(_node, green_color);\n}\nreplace_files\n\"\"\"\n\n\nclass BackgroundLoader(load.LoaderPlugin):\n    \"\"\"Load images\n    Stores the imported asset in a container named after the asset.\n    \"\"\"\n    families = [\"background\"]\n    representations = [\"json\"]\n\n    def load(self, context, name=None, namespace=None, data=None):\n\n        path = self.filepath_from_context(context)\n        with open(path) as json_file:\n            data = json.load(json_file)\n\n        layers = list()\n\n        for child in data['children']:\n            if child.get(\"filename\"):\n                layers.append(child[\"filename\"])\n            else:\n                for layer in child['children']:\n                    if layer.get(\"filename\"):\n                        layers.append(layer[\"filename\"])\n\n        bg_folder = os.path.dirname(path)\n\n        subset_name = context[\"subset\"][\"name\"]\n        # read_node_name += \"_{}\".format(uuid.uuid4())\n        container_nodes = []\n\n        for layer in sorted(layers):\n            file_to_import = [\n                os.path.join(bg_folder, layer).replace(\"\\\\\", \"/\")\n            ]\n\n            read_node = harmony.send(\n                {\n                    \"function\": copy_files + import_files,\n                    \"args\": [\"Top\", file_to_import, layer, 1]\n                }\n            )[\"result\"]\n            container_nodes.append(read_node)\n\n        return harmony.containerise(\n            subset_name,\n            namespace,\n            subset_name,\n            context,\n            self.__class__.__name__,\n            nodes=container_nodes\n        )\n\n    def update(self, container, representation):\n        path = get_representation_path(representation)\n        with open(path) as json_file:\n            data = json.load(json_file)\n\n        layers = list()\n\n        for child in data['children']:\n            if child.get(\"filename\"):\n                print(child[\"filename\"])\n                layers.append(child[\"filename\"])\n            else:\n                for layer in child['children']:\n                    if layer.get(\"filename\"):\n                        print(layer[\"filename\"])\n                        layers.append(layer[\"filename\"])\n\n        bg_folder = os.path.dirname(path)\n\n        print(container)\n\n        is_latest = is_representation_from_latest(representation)\n        for layer in sorted(layers):\n            file_to_import = [\n                os.path.join(bg_folder, layer).replace(\"\\\\\", \"/\")\n            ]\n            print(20 * \"#\")\n            print(f\"FILE TO REPLACE: {file_to_import}\")\n            print(f\"LAYER: {layer}\")\n            node = harmony.find_node_by_name(layer, \"READ\")\n            print(f\"{node}\")\n\n            if node in container['nodes']:\n                harmony.send(\n                    {\n                        \"function\": copy_files + replace_files,\n                        \"args\": [file_to_import, node, 1]\n                    }\n                )\n            else:\n                read_node = harmony.send(\n                    {\n                        \"function\": copy_files + import_files,\n                        \"args\": [\"Top\", file_to_import, layer, 1]\n                    }\n                )[\"result\"]\n                container['nodes'].append(read_node)\n\n            # Colour node.\n            sig = harmony.signature(\"set_color\")\n            func = \"\"\"function %s(args){\n                for( var i =0; i <= args[0].length - 1; ++i)\n                {\n                    var red_color = new ColorRGBA(255, 0, 0, 255);\n                    var green_color = new ColorRGBA(0, 255, 0, 255);\n                    if (args[1] == \"red\"){\n                        node.setColor(args[0], red_color);\n                    }\n                    if (args[1] == \"green\"){\n                        node.setColor(args[0], green_color);\n                    }\n                }\n            }\n            %s\n            \"\"\" % (sig, sig)\n            if is_latest:\n                harmony.send({\"function\": func, \"args\": [node, \"green\"]})\n            else:\n                harmony.send({\"function\": func, \"args\": [node, \"red\"]})\n\n        harmony.imprint(\n            container['name'], {\"representation\": str(representation[\"_id\"]),\n                                \"nodes\": container['nodes']}\n        )\n\n    def remove(self, container):\n        for node in container.get(\"nodes\"):\n\n            func = \"\"\"function deleteNode(_node)\n            {\n                node.deleteNode(_node, true, true);\n            }\n            deleteNode\n            \"\"\"\n            harmony.send(\n                {\"function\": func, \"args\": [node]}\n            )\n            harmony.imprint(container['name'], {}, remove=True)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/load/load_imagesequence.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Loader for image sequences.\"\"\"\nimport os\nimport uuid\nfrom pathlib import Path\n\nimport clique\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.pipeline.context_tools import is_representation_from_latest\nimport openpype.hosts.harmony.api as harmony\n\n\nclass ImageSequenceLoader(load.LoaderPlugin):\n    \"\"\"Load image sequences.\n\n    Stores the imported asset in a container named after the asset.\n    \"\"\"\n\n    families = [\"shot\", \"render\", \"image\", \"plate\", \"reference\", \"review\"]\n    representations = [\"*\"]\n    extensions = {\"jpeg\", \"png\", \"jpg\"}\n\n    def load(self, context, name=None, namespace=None, data=None):\n        \"\"\"Plugin entry point.\n\n        Args:\n            context (:class:`pyblish.api.Context`): Context.\n            name (str, optional): Container name.\n            namespace (str, optional): Container namespace.\n            data (dict, optional): Additional data passed into loader.\n\n        \"\"\"\n        fname = Path(self.filepath_from_context(context))\n        self_name = self.__class__.__name__\n        collections, remainder = clique.assemble(\n            os.listdir(fname.parent.as_posix())\n        )\n        files = []\n        if collections:\n            for f in list(collections[0]):\n                files.append(fname.parent.joinpath(f).as_posix())\n        else:\n            files.append(fname.parent.joinpath(remainder[0]).as_posix())\n\n        asset = context[\"asset\"][\"name\"]\n        subset = context[\"subset\"][\"name\"]\n\n        group_id = str(uuid.uuid4())\n        read_node = harmony.send(\n            {\n                \"function\": f\"PypeHarmony.Loaders.{self_name}.importFiles\",  # noqa: E501\n                \"args\": [\n                    files,\n                    asset,\n                    subset,\n                    1,\n                    group_id\n                ]\n            }\n        )[\"result\"]\n\n        return harmony.containerise(\n            f\"{asset}_{subset}\",\n            namespace,\n            read_node,\n            context,\n            self_name,\n            nodes=[read_node]\n        )\n\n    def update(self, container, representation):\n        \"\"\"Update loaded containers.\n\n        Args:\n            container (dict): Container data.\n            representation (dict): Representation data.\n\n        \"\"\"\n        self_name = self.__class__.__name__\n        node = container.get(\"nodes\").pop()\n\n        path = get_representation_path(representation)\n        collections, remainder = clique.assemble(\n            os.listdir(os.path.dirname(path))\n        )\n        files = []\n        if collections:\n            for f in list(collections[0]):\n                files.append(\n                    os.path.join(\n                        os.path.dirname(path), f\n                    ).replace(\"\\\\\", \"/\")\n                )\n        else:\n            files.append(\n                os.path.join(\n                    os.path.dirname(path), remainder[0]\n                ).replace(\"\\\\\", \"/\")\n            )\n\n        harmony.send(\n            {\n                \"function\": f\"PypeHarmony.Loaders.{self_name}.replaceFiles\",\n                \"args\": [files, node, 1]\n            }\n        )\n\n        # Colour node.\n        if is_representation_from_latest(representation):\n            harmony.send(\n                {\n                    \"function\": \"PypeHarmony.setColor\",\n                    \"args\": [node, [0, 255, 0, 255]]\n                })\n        else:\n            harmony.send(\n                {\n                    \"function\": \"PypeHarmony.setColor\",\n                    \"args\": [node, [255, 0, 0, 255]]\n                })\n\n        harmony.imprint(\n            node, {\"representation\": str(representation[\"_id\"])}\n        )\n\n    def remove(self, container):\n        \"\"\"Remove loaded container.\n\n        Args:\n            container (dict): Container data.\n\n        \"\"\"\n        node = container.get(\"nodes\").pop()\n        harmony.send(\n            {\"function\": \"PypeHarmony.deleteNode\", \"args\": [node]}\n        )\n        harmony.imprint(node, {}, remove=True)\n\n    def switch(self, container, representation):\n        \"\"\"Switch loaded representations.\"\"\"\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/load/load_palette.py",
    "content": "import os\nimport shutil\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nimport openpype.hosts.harmony.api as harmony\n\n\nclass ImportPaletteLoader(load.LoaderPlugin):\n    \"\"\"Import palettes.\"\"\"\n\n    families = [\"palette\", \"harmony.palette\"]\n    representations = [\"plt\"]\n    label = \"Import Palette\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        name = self.load_palette(context[\"representation\"])\n\n        return harmony.containerise(\n            name,\n            namespace,\n            name,\n            context,\n            self.__class__.__name__\n        )\n\n    def load_palette(self, representation):\n        subset_name = representation[\"context\"][\"subset\"]\n        name = subset_name.replace(\"palette\", \"\")\n\n        # Overwrite palette on disk.\n        scene_path = harmony.send(\n            {\"function\": \"scene.currentProjectPath\"}\n        )[\"result\"]\n        src = get_representation_path(representation)\n        dst = os.path.join(\n            scene_path,\n            \"palette-library\",\n            \"{}.plt\".format(name)\n        )\n        shutil.copy(src, dst)\n\n        harmony.save_scene()\n\n        msg = \"Updated {}.\".format(subset_name)\n        msg += \" You need to reload the scene to see the changes.\\n\"\n        msg += \"Please save workfile when ready and use Workfiles \"\n        msg += \"to reopen it.\"\n\n        harmony.send(\n            {\n                \"function\": \"PypeHarmony.message\",\n                \"args\": msg\n            })\n        return name\n\n    def remove(self, container):\n        harmony.remove(container[\"name\"])\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n        self.remove(container)\n        name = self.load_palette(representation)\n\n        container[\"representation\"] = str(representation[\"_id\"])\n        container[\"name\"] = name\n        harmony.imprint(name, container)\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/load/load_template.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Load template.\"\"\"\nimport tempfile\nimport zipfile\nimport os\nimport shutil\nimport uuid\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.pipeline.context_tools import is_representation_from_latest\nimport openpype.hosts.harmony.api as harmony\n\n\nclass TemplateLoader(load.LoaderPlugin):\n    \"\"\"Load Harmony template as container.\n\n    .. todo::\n\n        This must be implemented properly.\n\n    \"\"\"\n\n    families = [\"template\", \"workfile\"]\n    representations = [\"*\"]\n    label = \"Load Template\"\n    icon = \"gift\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        \"\"\"Plugin entry point.\n\n        Args:\n            context (:class:`pyblish.api.Context`): Context.\n            name (str, optional): Container name.\n            namespace (str, optional): Container namespace.\n            data (dict, optional): Additional data passed into loader.\n\n        \"\"\"\n        # Load template.\n        self_name = self.__class__.__name__\n        temp_dir = tempfile.mkdtemp()\n        zip_file = get_representation_path(context[\"representation\"])\n        template_path = os.path.join(temp_dir, \"temp.tpl\")\n        with zipfile.ZipFile(zip_file, \"r\") as zip_ref:\n            zip_ref.extractall(template_path)\n\n        group_id = \"{}\".format(uuid.uuid4())\n\n        container_group = harmony.send(\n            {\n                \"function\": f\"PypeHarmony.Loaders.{self_name}.loadContainer\",\n                \"args\": [template_path,\n                         context[\"asset\"][\"name\"],\n                         context[\"subset\"][\"name\"],\n                         group_id]\n            }\n        )[\"result\"]\n\n        # Cleanup the temp directory\n        shutil.rmtree(temp_dir)\n\n        # We must validate the group_node\n        return harmony.containerise(\n            name,\n            namespace,\n            container_group,\n            context,\n            self_name\n        )\n\n    def update(self, container, representation):\n        \"\"\"Update loaded containers.\n\n        Args:\n            container (dict): Container data.\n            representation (dict): Representation data.\n\n        \"\"\"\n        node_name = container[\"name\"]\n        node = harmony.find_node_by_name(node_name, \"GROUP\")\n        self_name = self.__class__.__name__\n\n        if is_representation_from_latest(representation):\n            self._set_green(node)\n        else:\n            self._set_red(node)\n\n        update_and_replace = harmony.send(\n            {\n                \"function\": f\"PypeHarmony.Loaders.{self_name}.\"\n                            \"askForColumnsUpdate\",\n                \"args\": []\n            }\n        )[\"result\"]\n\n        if update_and_replace:\n            # FIXME: This won't work, need to implement it.\n            harmony.send(\n                {\n                    \"function\": f\"PypeHarmony.Loaders.{self_name}.\"\n                                \"replaceNode\",\n                    \"args\": []\n                }\n            )\n        else:\n            self.load(\n                container[\"context\"], container[\"name\"],\n                None, container[\"data\"])\n\n        harmony.imprint(\n            node, {\"representation\": str(representation[\"_id\"])}\n        )\n\n    def remove(self, container):\n        \"\"\"Remove container.\n\n        Args:\n            container (dict): container definition.\n\n        \"\"\"\n        node = harmony.find_node_by_name(container[\"name\"], \"GROUP\")\n        harmony.send(\n            {\"function\": \"PypeHarmony.deleteNode\", \"args\": [node]}\n        )\n\n    def switch(self, container, representation):\n        \"\"\"Switch representation containers.\"\"\"\n        self.update(container, representation)\n\n    def _set_green(self, node):\n        \"\"\"Set node color to green `rgba(0, 255, 0, 255)`.\"\"\"\n        harmony.send(\n            {\n                \"function\": \"PypeHarmony.setColor\",\n                \"args\": [node, [0, 255, 0, 255]]\n            })\n\n    def _set_red(self, node):\n        \"\"\"Set node color to red `rgba(255, 0, 0, 255)`.\"\"\"\n        harmony.send(\n            {\n                \"function\": \"PypeHarmony.setColor\",\n                \"args\": [node, [255, 0, 0, 255]]\n            })\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/load/load_template_workfile.py",
    "content": "import tempfile\nimport zipfile\nimport os\nimport shutil\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nimport openpype.hosts.harmony.api as harmony\n\n\nclass ImportTemplateLoader(load.LoaderPlugin):\n    \"\"\"Import templates.\"\"\"\n\n    families = [\"harmony.template\", \"workfile\"]\n    representations = [\"*\"]\n    label = \"Import Template\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        # Import template.\n        temp_dir = tempfile.mkdtemp()\n        zip_file = get_representation_path(context[\"representation\"])\n        template_path = os.path.join(temp_dir, \"temp.tpl\")\n        with zipfile.ZipFile(zip_file, \"r\") as zip_ref:\n            zip_ref.extractall(template_path)\n\n        sig = harmony.signature(\"paste\")\n        func = \"\"\"function %s(args)\n        {\n            var template_path = args[0];\n            var drag_object = copyPaste.pasteTemplateIntoGroup(\n                template_path, \"Top\", 1\n            );\n        }\n        %s\n        \"\"\" % (sig, sig)\n\n        harmony.send({\"function\": func, \"args\": [template_path]})\n\n        shutil.rmtree(temp_dir)\n\n        subset_name = context[\"subset\"][\"name\"]\n\n        return harmony.containerise(\n            subset_name,\n            namespace,\n            subset_name,\n            context,\n            self.__class__.__name__\n        )\n\n        def update(self, container, representation):\n            pass\n\n        def remove(self, container):\n            pass\n\n\nclass ImportWorkfileLoader(ImportTemplateLoader):\n    \"\"\"Import workfiles.\"\"\"\n\n    families = [\"workfile\"]\n    representations = [\"zip\"]\n    label = \"Import Workfile\"\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/collect_audio.py",
    "content": "import os\nimport pyblish.api\n\nimport pyblish.api\n\nclass CollectAudio(pyblish.api.InstancePlugin):\n    \"\"\"\n        Collect relative path for audio file to instance.\n\n        Harmony api `getSoundtrackAll` returns useless path to temp folder,\n        for render on farm we look into 'audio' folder and select first file.\n\n        Correct path needs to be calculated in `submit_harmony_deadline.py`\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.499\n    label = \"Collect Audio\"\n    hosts = [\"harmony\"]\n    families = [\"render.farm\"]\n\n    def process(self, instance):\n        full_file_name = None\n        audio_dir = os.path.join(\n            os.path.dirname(instance.context.data.get(\"currentFile\")), 'audio')\n        if os.path.isdir(audio_dir):\n            for full_file_name in os.listdir(audio_dir):\n                file_name, file_ext = os.path.splitext(full_file_name)\n\n                if file_ext not in ['.wav', '.mp3', '.aiff']:\n                    self.log.error(\"Unsupported file {}.{}\".format(file_name,\n                                                                   file_ext))\n                    full_file_name = None\n\n        if full_file_name:\n            audio_file_path = os.path.join('audio', full_file_name)\n            self.log.debug(\"audio_file_path {}\".format(audio_file_path))\n            instance.data[\"audioFile\"] = audio_file_path\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/collect_current_file.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect information about current file.\"\"\"\nimport os\n\nimport pyblish.api\nimport openpype.hosts.harmony.api as harmony\n\n\nclass CollectCurrentFile(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current working file into context.\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.5\n    label = \"Current File\"\n    hosts = [\"harmony\"]\n\n    def process(self, context):\n        \"\"\"Inject the current working file.\"\"\"\n        self_name = self.__class__.__name__\n\n        current_file = harmony.send(\n            {\"function\": f\"PypeHarmony.Publish.{self_name}.collect\"})[\"result\"]\n        context.data[\"currentFile\"] = os.path.normpath(current_file)\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/collect_farm_render.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect data to render from scene.\"\"\"\nfrom pathlib import Path\n\nimport attr\n\nfrom openpype.lib import get_formatted_current_time\nfrom openpype.pipeline import publish\nfrom openpype.pipeline.publish import RenderInstance\nimport openpype.hosts.harmony.api as harmony\n\n\n@attr.s\nclass HarmonyRenderInstance(RenderInstance):\n    outputType = attr.ib(default=\"Image\")\n    outputFormat = attr.ib(default=\"PNG4\")\n    outputStartFrame = attr.ib(default=1)\n    leadingZeros = attr.ib(default=3)\n\n\nclass CollectFarmRender(publish.AbstractCollectRender):\n    \"\"\"Gather all publishable renders.\"\"\"\n\n    # https://docs.toonboom.com/help/harmony-17/premium/reference/node/output/write-node-image-formats.html\n    ext_mapping = {\n        \"tvg\": [\"TVG\"],\n        \"tga\": [\"TGA\", \"TGA4\", \"TGA3\", \"TGA1\"],\n        \"sgi\": [\"SGI\", \"SGI4\", \"SGA3\", \"SGA1\", \"SGIDP\", \"SGIDP4\", \"SGIDP3\"],\n        \"psd\": [\"PSD\", \"PSD1\", \"PSD3\", \"PSD4\", \"PSDDP\", \"PSDDP1\", \"PSDDP3\",\n                \"PSDDP4\"],\n        \"yuv\": [\"YUV\"],\n        \"pal\": [\"PAL\"],\n        \"scan\": [\"SCAN\"],\n        \"png\": [\"PNG\", \"PNG4\", \"PNGDP\", \"PNGDP3\", \"PNGDP4\"],\n        \"jpg\": [\"JPG\"],\n        \"bmp\": [\"BMP\", \"BMP4\"],\n        \"opt\": [\"OPT\", \"OPT1\", \"OPT3\", \"OPT4\"],\n        \"var\": [\"VAR\"],\n        \"tif\": [\"TIF\"],\n        \"dpx\": [\"DPX\", \"DPX3_8\", \"DPX3_10\", \"DPX3_12\", \"DPX3_16\",\n                \"DPX3_10_INVERTED_CHANNELS\", \"DPX3_12_INVERTED_CHANNELS\",\n                \"DPX3_16_INVERTED_CHANNELS\"],\n        \"exr\": [\"EXR\"],\n        \"pdf\": [\"PDF\"],\n        \"dtext\": [\"DTEX\"]\n    }\n\n    def get_expected_files(self, render_instance):\n        \"\"\"Get list of expected files to be rendered from Harmony.\n\n        This returns full path with file name determined by Write node\n        settings.\n        \"\"\"\n        start = render_instance.frameStart - render_instance.handleStart\n        end = render_instance.frameEnd + render_instance.handleEnd\n        node = render_instance.setMembers[0]\n        self_name = self.__class__.__name__\n        # 0 - filename / 1 - type / 2 - zeros / 3 - start\n        info = harmony.send(\n            {\n                \"function\": f\"PypeHarmony.Publish.{self_name}.\"\n                            \"getRenderNodeSettings\",\n                \"args\": node\n            })[\"result\"]\n\n        ext = None\n        for k, v in self.ext_mapping.items():\n            if info[1] in v:\n                ext = k\n\n        if not ext:\n            raise AssertionError(\n                f\"Cannot determine file extension for {info[1]}\")\n\n        path = Path(render_instance.source).parent\n        # is sequence start node on write node offsetting whole sequence?\n        expected_files = []\n\n        # '-' in name is important for Harmony17\n        for frame in range(start, end + 1):\n            expected_files.append(\n                path / \"{}-{}.{}\".format(\n                    render_instance.subset,\n                    str(frame).rjust(int(info[2]) + 1, \"0\"),\n                    ext\n                )\n            )\n        self.log.debug(\"expected_files::{}\".format(expected_files))\n        return expected_files\n\n    def get_instances(self, context):\n        \"\"\"Get instances per Write node in `renderFarm` family.\"\"\"\n        version = None\n        if self.sync_workfile_version:\n            version = context.data[\"version\"]\n\n        instances = []\n\n        self_name = self.__class__.__name__\n\n        asset_name = context.data[\"asset\"]\n\n        for node in context.data[\"allNodes\"]:\n            data = harmony.read(node)\n\n            # Skip non-tagged nodes.\n            if not data:\n                continue\n\n            # Skip containers.\n            if \"container\" in data[\"id\"]:\n                continue\n\n            if data[\"family\"] != \"renderFarm\":\n                continue\n\n            # 0 - filename / 1 - type / 2 - zeros / 3 - start / 4 - enabled\n            info = harmony.send(\n                {\n                    \"function\": f\"PypeHarmony.Publish.{self_name}.\"\n                                \"getRenderNodeSettings\",\n                    \"args\": node\n                })[\"result\"]\n\n            # TODO: handle pixel aspect and frame step\n            # TODO: set Deadline stuff (pools, priority, etc. by presets)\n            # because of using 'renderFarm' as a family, replace 'Farm' with\n            # capitalized task name - issue of avalon-core Creator app\n            subset_name = node.split(\"/\")[1]\n            task_name = context.data[\"anatomyData\"][\"task\"][\n                \"name\"].capitalize()\n            replace_str = \"\"\n            if task_name.lower() not in subset_name.lower():\n                replace_str = task_name\n            subset_name = subset_name.replace(\n                'Farm',\n                replace_str)\n\n            render_instance = HarmonyRenderInstance(\n                version=version,\n                time=get_formatted_current_time(),\n                source=context.data[\"currentFile\"],\n                label=node.split(\"/\")[1],\n                subset=subset_name,\n                asset=asset_name,\n                task=task_name,\n                attachTo=False,\n                setMembers=[node],\n                publish=info[4],\n                renderer=None,\n                priority=50,\n                name=node.split(\"/\")[1],\n\n                family=\"render.farm\",\n                families=[\"render.farm\"],\n                farm=True,\n\n                resolutionWidth=context.data[\"resolutionWidth\"],\n                resolutionHeight=context.data[\"resolutionHeight\"],\n                pixelAspect=1.0,\n                multipartExr=False,\n                tileRendering=False,\n                tilesX=0,\n                tilesY=0,\n                convertToScanline=False,\n\n                # time settings\n                frameStart=context.data[\"frameStart\"],\n                frameEnd=context.data[\"frameEnd\"],\n                handleStart=context.data[\"handleStart\"],  # from DB\n                handleEnd=context.data[\"handleEnd\"],      # from DB\n                frameStep=1,\n                outputType=\"Image\",\n                outputFormat=info[1],\n                outputStartFrame=info[3],\n                leadingZeros=info[2],\n                ignoreFrameHandleCheck=True\n\n            )\n            render_instance.context = context\n            self.log.debug(render_instance)\n            instances.append(render_instance)\n\n        return instances\n\n    def add_additional_data(self, instance):\n        instance[\"FOV\"] = self._context.data[\"FOV\"]\n\n        return instance\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/collect_instances.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect instances in Harmony.\"\"\"\nimport json\n\nimport pyblish.api\nimport openpype.hosts.harmony.api as harmony\n\n\nclass CollectInstances(pyblish.api.ContextPlugin):\n    \"\"\"Gather instances by nodes metadata.\n\n    This collector takes into account assets that are associated with\n    a composite node and marked with a unique identifier.\n\n    Identifier:\n        id (str): \"pyblish.avalon.instance\"\n    \"\"\"\n\n    label = \"Instances\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"harmony\"]\n    families_mapping = {\n        \"render\": [\"review\", \"ftrack\"],\n        \"harmony.template\": [],\n        \"palette\": [\"palette\", \"ftrack\"]\n    }\n\n    pair_media = True\n\n    def process(self, context):\n        \"\"\"Plugin entry point.\n\n        Args:\n            context (:class:`pyblish.api.Context`): Context data.\n\n        \"\"\"\n        nodes = harmony.send(\n            {\"function\": \"node.subNodes\", \"args\": [\"Top\"]}\n        )[\"result\"]\n\n        for node in nodes:\n            data = harmony.read(node)\n\n            # Skip non-tagged nodes.\n            if not data:\n                continue\n\n            # Skip containers.\n            if \"container\" in data[\"id\"]:\n                continue\n\n            # skip render farm family as it is collected separately\n            if data[\"family\"] == \"renderFarm\":\n                continue\n\n            instance = context.create_instance(node.split(\"/\")[-1])\n            instance.data.update(data)\n            instance.data[\"setMembers\"] = [node]\n            instance.data[\"publish\"] = harmony.send(\n                {\"function\": \"node.getEnable\", \"args\": [node]}\n            )[\"result\"]\n            instance.data[\"families\"] = self.families_mapping[data[\"family\"]]\n\n            # If set in plugin, pair the scene Version in ftrack with\n            # thumbnails and review media.\n            if (self.pair_media and instance.data[\"family\"] == \"scene\"):\n                context.data[\"scene_instance\"] = instance\n\n            # Produce diagnostic message for any graphical\n            # user interface interested in visualising it.\n            self.log.info(\n                \"Found: \\\"{0}\\\": \\n{1}\".format(\n                    instance.data[\"name\"], json.dumps(instance.data, indent=4)\n                )\n            )\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/collect_palettes.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect palettes from Harmony.\"\"\"\nimport json\nimport re\n\nimport pyblish.api\nimport openpype.hosts.harmony.api as harmony\n\n\nclass CollectPalettes(pyblish.api.ContextPlugin):\n    \"\"\"Gather palettes from scene when publishing templates.\"\"\"\n\n    label = \"Palettes\"\n    order = pyblish.api.CollectorOrder + 0.003\n    hosts = [\"harmony\"]\n\n    # list of regexes for task names where collecting should happen\n    allowed_tasks = []\n\n    def process(self, context):\n        \"\"\"Collector entry point.\"\"\"\n        self_name = self.__class__.__name__\n        palettes = harmony.send(\n            {\n                \"function\": f\"PypeHarmony.Publish.{self_name}.getPalettes\",\n            })[\"result\"]\n\n        # skip collecting if not in allowed task\n        if self.allowed_tasks:\n            task_name = context.data[\"anatomyData\"][\"task\"][\"name\"].lower()\n            if (not any([re.search(pattern, task_name)\n                         for pattern in self.allowed_tasks])):\n                return\n        asset_name = context.data[\"asset\"]\n\n        for name, id in palettes.items():\n            instance = context.create_instance(name)\n            instance.data.update({\n                \"id\": id,\n                \"family\": \"harmony.palette\",\n                'families': [],\n                \"asset\": asset_name,\n                \"subset\": \"{}{}\".format(\"palette\", name)\n            })\n            self.log.info(\n                \"Created instance:\\n\" + json.dumps(\n                    instance.data, sort_keys=True, indent=4\n                )\n            )\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/collect_scene.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect scene data.\"\"\"\nimport os\n\nimport pyblish.api\nimport openpype.hosts.harmony.api as harmony\n\n\nclass CollectScene(pyblish.api.ContextPlugin):\n    \"\"\"Collect basic scene information.\"\"\"\n\n    label = \"Scene Data\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"harmony\"]\n\n    def process(self, context):\n        \"\"\"Plugin entry point.\"\"\"\n        result = harmony.send(\n            {\n                f\"function\": \"PypeHarmony.getSceneSettings\",\n                \"args\": []}\n        )[\"result\"]\n\n        context.data[\"applicationPath\"] = result[0]\n        context.data[\"scenePath\"] = os.path.join(\n            result[1], result[2] + \".xstage\")\n        context.data[\"frameRate\"] = result[3]\n        context.data[\"frameStartHandle\"] = result[4]\n        context.data[\"frameEndHandle\"] = result[5]\n        context.data[\"audioPath\"] = result[6]\n        context.data[\"resolutionWidth\"] = result[7]\n        context.data[\"resolutionHeight\"] = result[8]\n        context.data[\"FOV\"] = result[9]\n\n        # harmony always starts from 1. frame\n        # 1001 - 10010 >> 1 - 10\n        # frameStart, frameEnd already collected by global plugin\n        offset = context.data[\"frameStart\"] - 1\n        frame_start = context.data[\"frameStart\"] - offset\n        frames_count = context.data[\"frameEnd\"] - \\\n            context.data[\"frameStart\"] + 1\n\n        # increase by handleStart - real frame range\n        # frameStart != frameStartHandle with handle presence\n        context.data[\"frameStart\"] = int(frame_start) + \\\n            context.data[\"handleStart\"]\n        context.data[\"frameEnd\"] = int(frames_count) + \\\n            context.data[\"frameStart\"] - 1\n\n        all_nodes = harmony.send(\n            {\"function\": \"node.subNodes\", \"args\": [\"Top\"]}\n        )[\"result\"]\n\n        context.data[\"allNodes\"] = all_nodes\n\n        # collect all write nodes to be able disable them in Deadline\n        all_write_nodes = harmony.send(\n            {\"function\": \"node.getNodes\", \"args\": [\"WRITE\"]}\n        )[\"result\"]\n\n        context.data[\"all_write_nodes\"] = all_write_nodes\n\n        result = harmony.send(\n            {\n                f\"function\": \"PypeHarmony.getVersion\",\n                \"args\": []}\n        )[\"result\"]\n        context.data[\"harmonyVersion\"] = \"{}.{}\".format(result[0], result[1])\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/collect_workfile.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect current workfile from Harmony.\"\"\"\nimport os\nimport pyblish.api\n\nfrom openpype.pipeline.create import get_subset_name\n\n\nclass CollectWorkfile(pyblish.api.ContextPlugin):\n    \"\"\"Collect current script for publish.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.1\n    label = \"Collect Workfile\"\n    hosts = [\"harmony\"]\n\n    def process(self, context):\n        \"\"\"Plugin entry point.\"\"\"\n        family = \"workfile\"\n        basename = os.path.basename(context.data[\"currentFile\"])\n        subset = get_subset_name(\n            family,\n            \"\",\n            context.data[\"anatomyData\"][\"task\"][\"name\"],\n            context.data[\"assetEntity\"],\n            context.data[\"anatomyData\"][\"project\"][\"name\"],\n            host_name=context.data[\"hostName\"],\n            project_settings=context.data[\"project_settings\"]\n        )\n\n        # Create instance\n        instance = context.create_instance(subset)\n        instance.data.update({\n            \"subset\": subset,\n            \"label\": basename,\n            \"name\": basename,\n            \"family\": family,\n            \"families\": [family],\n            \"representations\": [],\n            \"asset\": context.data[\"asset\"]\n        })\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/extract_palette.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract palette from Harmony.\"\"\"\nimport os\nimport csv\n\nfrom PIL import Image, ImageDraw, ImageFont\n\nimport openpype.hosts.harmony.api as harmony\nfrom openpype.pipeline import publish\n\n\nclass ExtractPalette(publish.Extractor):\n    \"\"\"Extract palette.\"\"\"\n\n    label = \"Extract Palette\"\n    hosts = [\"harmony\"]\n    families = [\"harmony.palette\"]\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        self_name = self.__class__.__name__\n        result = harmony.send(\n            {\n                \"function\": f\"PypeHarmony.Publish.{self_name}.getPalette\",\n                \"args\": instance.data[\"id\"]\n            })[\"result\"]\n\n        if not isinstance(result, list):\n            self.log.error(f\"Invalid reply: {result}\")\n            raise AssertionError(\"Invalid reply from server.\")\n        palette_name = result[0]\n        palette_file = result[1]\n        self.log.info(f\"Got palette named {palette_name} \"\n                      f\"and file {palette_file}.\")\n\n        tmp_thumb_path = os.path.join(os.path.dirname(palette_file),\n                                      os.path.basename(palette_file)\n                                      .split(\".plt\")[0] + \"_swatches.png\"\n                                      )\n        self.log.info(f\"Temporary thumbnail path {tmp_thumb_path}\")\n\n        palette_version = str(instance.data.get(\"version\")).zfill(3)\n\n        self.log.info(f\"Palette version {palette_version}\")\n\n        if not instance.data.get(\"representations\"):\n            instance.data[\"representations\"] = []\n\n        try:\n            thumbnail_path = self.create_palette_thumbnail(palette_name,\n                                                           palette_version,\n                                                           palette_file,\n                                                           tmp_thumb_path)\n        except OSError as e:\n            # FIXME: this happens on Mac where PIL cannot access fonts\n            # for some reason.\n            self.log.warning(\"Thumbnail generation failed\")\n            self.log.warning(e)\n        except ValueError:\n            self.log.error(\"Unsupported palette type for thumbnail.\")\n\n        else:\n            thumbnail = {\n                \"name\": \"thumbnail\",\n                \"ext\": \"png\",\n                \"files\": os.path.basename(thumbnail_path),\n                \"stagingDir\": os.path.dirname(thumbnail_path),\n                \"tags\": [\"thumbnail\"]\n            }\n\n            instance.data[\"representations\"].append(thumbnail)\n\n        representation = {\n            \"name\": \"plt\",\n            \"ext\": \"plt\",\n            \"files\": os.path.basename(palette_file),\n            \"stagingDir\": os.path.dirname(palette_file)\n        }\n\n        instance.data[\"representations\"].append(representation)\n\n    def create_palette_thumbnail(self,\n                                 palette_name,\n                                 palette_version,\n                                 palette_path,\n                                 dst_path):\n        \"\"\"Create thumbnail for palette file.\n\n        Args:\n            palette_name (str): Name of palette.\n            palette_version (str): Version of palette.\n            palette_path (str): Path to palette file.\n            dst_path (str): Thumbnail path.\n\n        Returns:\n            str: Thumbnail path.\n\n        \"\"\"\n        colors = {}\n\n        with open(palette_path, newline='') as plt:\n            plt_parser = csv.reader(plt, delimiter=\" \")\n            for i, line in enumerate(plt_parser):\n                if i == 0:\n                    continue\n                while (\"\" in line):\n                    line.remove(\"\")\n                # self.log.debug(line)\n                if line[0] not in [\"Solid\"]:\n                    raise ValueError(\"Unsupported palette type.\")\n                color_name = line[1].strip('\"')\n                colors[color_name] = {\"type\": line[0],\n                                      \"uuid\": line[2],\n                                      \"rgba\": (int(line[3]),\n                                               int(line[4]),\n                                               int(line[5]),\n                                               int(line[6])),\n                                      }\n            plt.close()\n\n        img_pad_top = 80\n        label_pad_name = 30\n        label_pad_rgb = 580\n        swatch_pad_left = 300\n        swatch_pad_top = 10\n        swatch_w = 120\n        swatch_h = 50\n\n        image_w = 800\n        image_h = (img_pad_top +\n                   (len(colors.keys()) *\n                    swatch_h) +\n                   (swatch_pad_top *\n                    len(colors.keys()))\n                   )\n\n        img = Image.new(\"RGBA\", (image_w, image_h), \"white\")\n\n        # For bg of colors with alpha, create checkerboard image\n        checkers = Image.new(\"RGB\", (swatch_w, swatch_h))\n        pixels = checkers.load()\n\n        # Make pixels white where (row+col) is odd\n        for i in range(swatch_w):\n            for j in range(swatch_h):\n                if (i + j) % 2:\n                    pixels[i, j] = (255, 255, 255)\n\n        draw = ImageDraw.Draw(img)\n        # TODO: This needs to be font included with Pype because\n        # arial is not available on other platforms then Windows.\n        title_font = ImageFont.truetype(\"arial.ttf\", 28)\n        label_font = ImageFont.truetype(\"arial.ttf\", 20)\n\n        draw.text((label_pad_name, 20),\n                  \"{} (v{})\".format(palette_name, palette_version),\n                  \"black\",\n                  font=title_font)\n\n        for i, name in enumerate(colors):\n            rgba = colors[name][\"rgba\"]\n            # @TODO: Fix this so alpha colors are displayed with checkboard\n            # if not rgba[3] == \"255\":\n            #     img.paste(checkers,\n            #               (swatch_pad_left,\n            #                img_pad_top + swatch_pad_top + (i * swatch_h))\n            #               )\n            #\n            #     half_y = (img_pad_top + swatch_pad_top + (i * swatch_h))/2\n            #\n            #     draw.rectangle((\n            #         swatch_pad_left,  # upper LX\n            #         img_pad_top + swatch_pad_top + (i * swatch_h), # upper LY\n            #         swatch_pad_left + (swatch_w * 2),  # lower RX\n            #         half_y),  # lower RY\n            #         fill=rgba[:-1], outline=(0, 0, 0), width=2)\n            #     draw.rectangle((\n            #         swatch_pad_left,  # upper LX\n            #         half_y,  # upper LY\n            #         swatch_pad_left + (swatch_w * 2),  # lower RX\n            #         img_pad_top + swatch_h + (i * swatch_h)),  # lower RY\n            #         fill=rgba, outline=(0, 0, 0), width=2)\n            # else:\n\n            draw.rectangle((\n                swatch_pad_left,  # upper left x\n                img_pad_top + swatch_pad_top + (i * swatch_h),  # upper left y\n                swatch_pad_left + (swatch_w * 2),  # lower right x\n                img_pad_top + swatch_h + (i * swatch_h)),  # lower right y\n                fill=rgba, outline=(0, 0, 0), width=2)\n\n            draw.text((label_pad_name, img_pad_top + (i * swatch_h) + swatch_pad_top + (swatch_h / 4)),  # noqa: E501\n                      name,\n                      \"black\",\n                      font=label_font)\n\n            draw.text((label_pad_rgb, img_pad_top + (i * swatch_h) + swatch_pad_top + (swatch_h / 4)),  # noqa: E501\n                      str(rgba),\n                      \"black\",\n                      font=label_font)\n\n        draw = ImageDraw.Draw(img)\n\n        img.save(dst_path)\n        return dst_path\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/extract_render.py",
    "content": "import os\nimport tempfile\nimport subprocess\n\nimport pyblish.api\nimport openpype.hosts.harmony.api as harmony\nimport openpype.lib\n\nimport clique\n\n\nclass ExtractRender(pyblish.api.InstancePlugin):\n    \"\"\"Produce a flattened image file from instance.\n    This plug-in only takes into account the nodes connected to the composite.\n    \"\"\"\n\n    label = \"Extract Render\"\n    order = pyblish.api.ExtractorOrder\n    hosts = [\"harmony\"]\n    families = [\"render\"]\n\n    def process(self, instance):\n        # Collect scene data.\n\n        application_path = instance.context.data.get(\"applicationPath\")\n        scene_path = instance.context.data.get(\"scenePath\")\n        frame_rate = instance.context.data.get(\"frameRate\")\n        # real value from timeline\n        frame_start = instance.context.data.get(\"frameStartHandle\")\n        frame_end = instance.context.data.get(\"frameEndHandle\")\n        audio_path = instance.context.data.get(\"audioPath\")\n\n        if audio_path and os.path.exists(audio_path):\n            self.log.info(f\"Using audio from {audio_path}\")\n            instance.data[\"audio\"] = [{\"filename\": audio_path}]\n\n        instance.data[\"fps\"] = frame_rate\n\n        # Set output path to temp folder.\n        path = tempfile.mkdtemp()\n        sig = harmony.signature()\n        func = \"\"\"function %s(args)\n        {\n            node.setTextAttr(args[0], \"DRAWING_NAME\", 1, args[1]);\n        }\n        %s\n        \"\"\" % (sig, sig)\n        harmony.send(\n            {\n                \"function\": func,\n                \"args\": [instance.data[\"setMembers\"][0],\n                         path + \"/\" + instance.data[\"name\"]]\n            }\n        )\n        harmony.save_scene()\n\n        # Execute rendering. Ignoring error cause Harmony returns error code\n        # always.\n\n        args = [application_path, \"-batch\",\n                \"-frames\", str(frame_start), str(frame_end),\n                scene_path]\n        self.log.info(f\"running: {' '.join(args)}\")\n        proc = subprocess.Popen(\n            args,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n            stdin=subprocess.PIPE\n        )\n        output, error = proc.communicate()\n        self.log.info(\"Click on the line below to see more details.\")\n        self.log.info(output.decode(\"utf-8\"))\n\n        # Collect rendered files.\n        self.log.debug(f\"collecting from: {path}\")\n        files = os.listdir(path)\n        assert files, (\n            \"No rendered files found, render failed.\"\n        )\n        self.log.debug(f\"files there: {files}\")\n        collections, remainder = clique.assemble(files, minimum_items=1)\n        assert not remainder, (\n            \"There should not be a remainder for {0}: {1}\".format(\n                instance.data[\"setMembers\"][0], remainder\n            )\n        )\n        self.log.debug(collections)\n        if len(collections) > 1:\n            for col in collections:\n                if len(list(col)) > 1:\n                    collection = col\n        else:\n            collection = collections[0]\n\n        # Generate thumbnail.\n        thumbnail_path = os.path.join(path, \"thumbnail.png\")\n        args = openpype.lib.get_ffmpeg_tool_args(\n            \"ffmpeg\",\n            \"-y\",\n            \"-i\", os.path.join(path, list(collections[0])[0]),\n            \"-vf\", \"scale=300:-1\",\n            \"-vframes\", \"1\",\n            thumbnail_path\n        )\n        process = subprocess.Popen(\n            args,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n            stdin=subprocess.PIPE\n        )\n\n        output = process.communicate()[0]\n\n        if process.returncode != 0:\n            raise ValueError(output.decode(\"utf-8\", errors=\"backslashreplace\"))\n\n        self.log.debug(output.decode(\"utf-8\", errors=\"backslashreplace\"))\n\n        # Generate representations.\n        extension = collection.tail[1:]\n        representation = {\n            \"name\": extension,\n            \"ext\": extension,\n            \"files\": list(collection),\n            \"stagingDir\": path,\n            \"tags\": [\"review\"],\n            \"fps\": frame_rate\n        }\n\n        thumbnail = {\n            \"name\": \"thumbnail\",\n            \"ext\": \"png\",\n            \"files\": os.path.basename(thumbnail_path),\n            \"stagingDir\": path,\n            \"tags\": [\"thumbnail\"]\n        }\n        instance.data[\"representations\"] = [representation, thumbnail]\n\n        if audio_path and os.path.exists(audio_path):\n            instance.data[\"audio\"] = [{\"filename\": audio_path}]\n\n        # Required for extract_review plugin (L222 onwards).\n        instance.data[\"frameStart\"] = frame_start\n        instance.data[\"frameEnd\"] = frame_end\n        instance.data[\"fps\"] = frame_rate\n\n        self.log.info(f\"Extracted {instance} to {path}\")\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/extract_save_scene.py",
    "content": "import pyblish.api\nimport openpype.hosts.harmony.api as harmony\n\n\nclass ExtractSaveScene(pyblish.api.ContextPlugin):\n    \"\"\"Save scene for extraction.\"\"\"\n\n    label = \"Extract Save Scene\"\n    order = pyblish.api.ExtractorOrder - 0.49\n    hosts = [\"harmony\"]\n\n    def process(self, context):\n        harmony.save_scene()\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/extract_template.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract template.\"\"\"\nimport os\nimport shutil\n\nfrom openpype.pipeline import publish\nimport openpype.hosts.harmony.api as harmony\n\n\nclass ExtractTemplate(publish.Extractor):\n    \"\"\"Extract the connected nodes to the composite instance.\"\"\"\n\n    label = \"Extract Template\"\n    hosts = [\"harmony\"]\n    families = [\"harmony.template\"]\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        staging_dir = self.staging_dir(instance)\n        filepath = os.path.join(staging_dir, f\"{instance.name}.tpl\")\n\n        self.log.info(f\"Outputting template to {staging_dir}\")\n\n        dependencies = []\n        self.get_dependencies(instance.data[\"setMembers\"][0], dependencies)\n\n        # Get backdrops.\n        backdrops = {}\n        for dependency in dependencies:\n            for backdrop in self.get_backdrops(dependency):\n                backdrops[backdrop[\"title\"][\"text\"]] = backdrop\n        unique_backdrops = [backdrops[x] for x in set(backdrops.keys())]\n        if not unique_backdrops:\n            self.log.error((\"No backdrops detected for template. \"\n                            \"Please move template instance node onto \"\n                            \"some backdrop and try again.\"))\n            raise AssertionError(\"No backdrop detected\")\n        # Get non-connected nodes within backdrops.\n        all_nodes = instance.context.data.get(\"allNodes\")\n        for node in [x for x in all_nodes if x not in dependencies]:\n            within_unique_backdrops = bool(\n                [x for x in self.get_backdrops(node) if x in unique_backdrops]\n            )\n            if within_unique_backdrops:\n                dependencies.append(node)\n\n        # Make sure we dont export the instance node.\n        if instance.data[\"setMembers\"][0] in dependencies:\n            dependencies.remove(instance.data[\"setMembers\"][0])\n\n        # Export template.\n        harmony.export_template(\n            unique_backdrops, dependencies, filepath\n        )\n\n        # Prep representation.\n        os.chdir(staging_dir)\n        shutil.make_archive(\n            f\"{instance.name}\",\n            \"zip\",\n            os.path.join(staging_dir, f\"{instance.name}.tpl\")\n        )\n\n        representation = {\n            \"name\": \"tpl\",\n            \"ext\": \"zip\",\n            \"files\": f\"{instance.name}.zip\",\n            \"stagingDir\": staging_dir\n        }\n\n        self.log.info(instance.data.get(\"representations\"))\n        if instance.data.get(\"representations\"):\n            instance.data[\"representations\"].extend([representation])\n        else:\n            instance.data[\"representations\"] = [representation]\n\n        instance.data[\"version_name\"] = \"{}_{}\".format(\n            instance.data[\"subset\"], instance.context.data[\"task\"])\n\n    def get_backdrops(self, node: str) -> list:\n        \"\"\"Get backdrops for the node.\n\n        Args:\n            node (str): Node path.\n\n        Returns:\n            list: list of Backdrops.\n\n        \"\"\"\n        self_name = self.__class__.__name__\n        return harmony.send({\n            \"function\": f\"PypeHarmony.Publish.{self_name}.getBackdropsByNode\",\n            \"args\": node})[\"result\"]\n\n    def get_dependencies(\n            self, node: str, dependencies: list = None) -> list:\n        \"\"\"Get node dependencies.\n\n        This will return recursive dependency list of given node.\n\n        Args:\n            node (str): Path to the node.\n            dependencies (list, optional): existing dependency list.\n\n        Returns:\n            list: List of dependent nodes.\n\n        \"\"\"\n        current_dependencies = harmony.send(\n            {\n                \"function\": \"PypeHarmony.getDependencies\",\n                \"args\": node}\n        )[\"result\"]\n\n        for dependency in current_dependencies:\n            if not dependency:\n                continue\n\n            if dependency in dependencies:\n                continue\n\n            dependencies.append(dependency)\n\n            self.get_dependencies(dependency, dependencies)\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/extract_workfile.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract work file.\"\"\"\nimport os\nimport shutil\nfrom zipfile import ZipFile\n\nfrom openpype.pipeline import publish\n\n\nclass ExtractWorkfile(publish.Extractor):\n    \"\"\"Extract and zip complete workfile folder into zip.\"\"\"\n\n    label = \"Extract Workfile\"\n    hosts = [\"harmony\"]\n    families = [\"workfile\"]\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        staging_dir = self.staging_dir(instance)\n        filepath = os.path.join(staging_dir, \"{}.tpl\".format(instance.name))\n        src = os.path.dirname(instance.context.data[\"currentFile\"])\n        self.log.info(\"Copying to {}\".format(filepath))\n        shutil.copytree(src, filepath)\n\n        # Prep representation.\n        os.chdir(staging_dir)\n        shutil.make_archive(\n            f\"{instance.name}\",\n            \"zip\",\n            os.path.join(staging_dir, f\"{instance.name}.tpl\")\n        )\n        # Check if archive is ok\n        with ZipFile(os.path.basename(f\"{instance.name}.zip\")) as zr:\n            if zr.testzip() is not None:\n                raise Exception(\"File archive is corrupted.\")\n\n        representation = {\n            \"name\": \"tpl\",\n            \"ext\": \"zip\",\n            \"files\": f\"{instance.name}.zip\",\n            \"stagingDir\": staging_dir\n        }\n        instance.data[\"representations\"] = [representation]\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/help/validate_audio.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Missing audio file</title>\n<description>\n## Cannot locate linked audio file\n\nAudio file at {audio_url} cannot be found.\n\n### How to repair?\n\nCopy audio file to the highlighted location or remove audio link in the workfile.\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/help/validate_instances.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Subset context</title>\n<description>\n## Invalid subset context\n\nAsset name found '{found}' in subsets, expected '{expected}'.\n\n### How to repair?\n\nYou can fix this with `Repair` button on the right. This will use '{expected}' asset name and overwrite '{found}' asset name in scene metadata.\n\nAfter that restart `Publish` with a `Reload button`.\n\nIf this is unwanted, close workfile and open again, that way different asset value would be used for context information.\n</description>\n<detail>\n### __Detailed Info__ (optional)\n\nThis might happen if you are reuse old workfile and open it in different context.\n(Eg. you created subset \"renderCompositingDefault\" from asset \"Robot' in \"your_project_Robot_compositing.aep\", now you opened this workfile in a context \"Sloth\" but existing subset for \"Robot\" asset stayed in the workfile.)\n</detail>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/help/validate_scene_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Scene setting</title>\n<description>\n## Invalid scene setting found\n\nOne of the settings in a scene doesn't match to asset settings in database.\n\n{invalid_setting_str}\n\n### How to repair?\n\nChange values for {invalid_keys_str} in the scene OR change them in the asset database if they are wrong there.\n</description>\n<detail>\n### __Detailed Info__ (optional)\n\nThis error is shown when for example resolution in the scene doesn't match to resolution set on the asset in the database.\nEither value in the database or in the scene is wrong.\n</detail>\n</error>\n<error id=\"file_not_found\">\n<title>Scene file doesn't exist</title>\n<description>\n## Scene file doesn't exist\n\nCollected scene {scene_url} doesn't exist.\n\n### How to repair?\n\nRe-save file, start publish from the beginning again.\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/increment_workfile.py",
    "content": "import os\n\nimport pyblish.api\nfrom openpype.pipeline.publish import get_errored_plugins_from_context\nfrom openpype.lib import version_up\nimport openpype.hosts.harmony.api as harmony\n\n\nclass IncrementWorkfile(pyblish.api.InstancePlugin):\n    \"\"\"Increment the current workfile.\n\n    Saves the current scene with an increased version number.\n    \"\"\"\n\n    label = \"Increment Workfile\"\n    order = pyblish.api.IntegratorOrder + 9.0\n    hosts = [\"harmony\"]\n    families = [\"workfile\"]\n    optional = True\n\n    def process(self, instance):\n        errored_plugins = get_errored_plugins_from_context(instance.context)\n        if errored_plugins:\n            raise RuntimeError(\n                \"Skipping incrementing current file because publishing failed.\"\n            )\n\n        scene_dir = version_up(\n            os.path.dirname(instance.context.data[\"currentFile\"])\n        )\n        scene_path = os.path.join(\n            scene_dir, os.path.basename(scene_dir) + \".xstage\"\n        )\n\n        harmony.save_scene_as(scene_path)\n\n        self.log.info(\"Incremented workfile to: {}\".format(scene_path))\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/validate_audio.py",
    "content": "import os\n\nimport pyblish.api\n\nimport openpype.hosts.harmony.api as harmony\n\nfrom openpype.pipeline import PublishXmlValidationError\n\n\nclass ValidateAudio(pyblish.api.InstancePlugin):\n    \"\"\"Ensures that there is an audio file in the scene.\n\n    If you are sure that you want to send render without audio, you can\n    disable this validator before clicking on \"publish\"\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Audio\"\n    families = [\"render\"]\n    hosts = [\"harmony\"]\n    optional = True\n\n    def process(self, instance):\n        node = None\n        if instance.data.get(\"setMembers\"):\n            node = instance.data[\"setMembers\"][0]\n\n        if not node:\n            return\n        # Collect scene data.\n        func = \"\"\"function func(write_node)\n        {\n            return [\n                sound.getSoundtrackAll().path()\n            ]\n        }\n        func\n        \"\"\"\n        result = harmony.send(\n            {\"function\": func, \"args\": [node]}\n        )[\"result\"]\n\n        audio_path = result[0]\n\n        msg = \"You are missing audio file:\\n{}\".format(audio_path)\n\n        formatting_data = {\n            \"audio_url\": audio_path\n        }\n        if not os.path.isfile(audio_path):\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/validate_instances.py",
    "content": "import pyblish.api\n\nimport openpype.hosts.harmony.api as harmony\nfrom openpype.pipeline import get_current_asset_name\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n)\n\n\nclass ValidateInstanceRepair(pyblish.api.Action):\n    \"\"\"Repair the instance.\"\"\"\n\n    label = \"Repair\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n\n        # Get the errored instances\n        failed = []\n        for result in context.data[\"results\"]:\n            if (result[\"error\"] is not None and result[\"instance\"] is not None\n                    and result[\"instance\"] not in failed):\n                failed.append(result[\"instance\"])\n\n        # Apply pyblish.logic to get the instances for the plug-in\n        instances = pyblish.api.instances_by_plugin(failed, plugin)\n\n        for instance in instances:\n            data = harmony.read(instance.data[\"setMembers\"][0])\n            data[\"asset\"] = get_current_asset_name()\n            harmony.imprint(instance.data[\"setMembers\"][0], data)\n\n\nclass ValidateInstance(pyblish.api.InstancePlugin):\n    \"\"\"Validate the instance asset is the current asset.\"\"\"\n\n    label = \"Validate Instance\"\n    hosts = [\"harmony\"]\n    actions = [ValidateInstanceRepair]\n    order = ValidateContentsOrder\n\n    def process(self, instance):\n        instance_asset = instance.data[\"asset\"]\n        current_asset = get_current_asset_name()\n        msg = (\n            \"Instance asset is not the same as current asset:\"\n            f\"\\nInstance: {instance_asset}\\nCurrent: {current_asset}\"\n        )\n\n        formatting_data = {\n            \"found\": instance_asset,\n            \"expected\": current_asset\n        }\n        if instance_asset != current_asset:\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n"
  },
  {
    "path": "openpype/hosts/harmony/plugins/publish/validate_scene_settings.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validate scene settings.\"\"\"\nimport os\nimport json\nimport re\n\nimport pyblish.api\n\nimport openpype.hosts.harmony.api as harmony\nfrom openpype.pipeline import PublishXmlValidationError\n\n\nclass ValidateSceneSettingsRepair(pyblish.api.Action):\n    \"\"\"Repair the instance.\"\"\"\n\n    label = \"Repair\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n        \"\"\"Repair action entry point.\"\"\"\n        expected = harmony.get_asset_settings()\n        asset_settings = _update_frames(dict.copy(expected))\n        asset_settings[\"frameStart\"] = 1\n        asset_settings[\"frameEnd\"] = asset_settings[\"frameEnd\"] + \\\n            asset_settings[\"handleEnd\"]\n        harmony.set_scene_settings(asset_settings)\n        if not os.path.exists(context.data[\"scenePath\"]):\n            self.log.info(\"correcting scene name\")\n            scene_dir = os.path.dirname(context.data[\"currentFile\"])\n            scene_path = os.path.join(\n                scene_dir, os.path.basename(scene_dir) + \".xstage\"\n            )\n            harmony.save_scene_as(scene_path)\n\n\nclass ValidateSceneSettings(pyblish.api.InstancePlugin):\n    \"\"\"Ensure the scene settings are in sync with database.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Scene Settings\"\n    families = [\"workfile\"]\n    hosts = [\"harmony\"]\n    actions = [ValidateSceneSettingsRepair]\n    optional = True\n\n    # skip frameEnd check if asset contains any of:\n    frame_check_filter = [\"_ch_\", \"_pr_\", \"_intd_\", \"_extd_\"]  # regex\n\n    # skip resolution check if Task name matches any of regex patterns\n    skip_resolution_check = [\"render\", \"Render\"]  # regex\n\n    # skip frameStart, frameEnd check if Task name matches any of regex patt.\n    skip_timelines_check = []  # regex\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n\n        # TODO 'get_asset_settings' could expect asset document as argument\n        #   which is available on 'context.data[\"assetEntity\"]'\n        #   - the same approach can be used in 'ValidateSceneSettingsRepair'\n        expected_settings = harmony.get_asset_settings()\n        self.log.info(\"scene settings from DB:{}\".format(expected_settings))\n        expected_settings.pop(\"entityType\")  # not useful for the validation\n\n        expected_settings = _update_frames(dict.copy(expected_settings))\n        expected_settings[\"frameEndHandle\"] = expected_settings[\"frameEnd\"] +\\\n            expected_settings[\"handleEnd\"]\n\n        task_name = instance.context.data[\"task\"]\n\n        if (any(re.search(pattern, task_name)\n                for pattern in self.skip_resolution_check)):\n            self.log.info(\"Skipping resolution check because of \"\n                          \"task name and pattern {}\".format(\n                              self.skip_resolution_check))\n            expected_settings.pop(\"resolutionWidth\")\n            expected_settings.pop(\"resolutionHeight\")\n\n        if (any(re.search(pattern, os.getenv('AVALON_TASK'))\n                for pattern in self.skip_timelines_check)):\n            self.log.info(\"Skipping frames check because of \"\n                          \"task name and pattern {}\".format(\n                              self.skip_timelines_check))\n            expected_settings.pop('frameStart', None)\n            expected_settings.pop('frameEnd', None)\n            expected_settings.pop('frameStartHandle', None)\n            expected_settings.pop('frameEndHandle', None)\n\n        asset_name = instance.context.data['anatomyData']['asset']\n        if any(re.search(pattern, asset_name)\n                for pattern in self.frame_check_filter):\n            self.log.info(\"Skipping frames check because of \"\n                          \"task name and pattern {}\".format(\n                              self.frame_check_filter))\n            expected_settings.pop('frameStart', None)\n            expected_settings.pop('frameEnd', None)\n            expected_settings.pop('frameStartHandle', None)\n            expected_settings.pop('frameEndHandle', None)\n\n        # handle case where ftrack uses only two decimal places\n        # 23.976023976023978 vs. 23.98\n        fps = instance.context.data.get(\"frameRate\")\n        if isinstance(instance.context.data.get(\"frameRate\"), float):\n            fps = float(\n                \"{:.2f}\".format(instance.context.data.get(\"frameRate\")))\n\n        self.log.debug(\"filtered settings: {}\".format(expected_settings))\n\n        current_settings = {\n            \"fps\": fps,\n            \"frameStart\": instance.context.data[\"frameStart\"],\n            \"frameEnd\": instance.context.data[\"frameEnd\"],\n            \"handleStart\": instance.context.data.get(\"handleStart\"),\n            \"handleEnd\": instance.context.data.get(\"handleEnd\"),\n            \"frameStartHandle\": instance.context.data.get(\"frameStartHandle\"),\n            \"frameEndHandle\": instance.context.data.get(\"frameEndHandle\"),\n            \"resolutionWidth\": instance.context.data.get(\"resolutionWidth\"),\n            \"resolutionHeight\": instance.context.data.get(\"resolutionHeight\"),\n        }\n        self.log.debug(\"current scene settings {}\".format(current_settings))\n\n        invalid_settings = []\n        invalid_keys = set()\n        for key, value in expected_settings.items():\n            if value != current_settings[key]:\n                invalid_settings.append(\n                    \"{} expected: {}  found: {}\".format(key, value,\n                                                        current_settings[key]))\n                invalid_keys.add(key)\n\n        if ((expected_settings[\"handleStart\"]\n            or expected_settings[\"handleEnd\"])\n           and invalid_settings):\n            msg = \"Handles included in calculation. Remove handles in DB \" +\\\n                  \"or extend frame range in timeline.\"\n            invalid_settings[-1][\"reason\"] = msg\n\n        msg = \"Found invalid settings:\\n{}\".format(\n            json.dumps(invalid_settings, sort_keys=True, indent=4)\n        )\n\n        if invalid_settings:\n            invalid_keys_str = \",\".join(invalid_keys)\n            break_str = \"<br/>\"\n            invalid_setting_str = \"<b>Found invalid settings:</b><br/>{}\".\\\n                format(break_str.join(invalid_settings))\n\n            formatting_data = {\n                \"invalid_setting_str\": invalid_setting_str,\n                \"invalid_keys_str\": invalid_keys_str\n            }\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n\n        scene_url = instance.context.data.get(\"scenePath\")\n        if not os.path.exists(scene_url):\n            msg = \"Scene file {} not found (saved under wrong name)\".format(\n                scene_url\n            )\n            formatting_data = {\n                \"scene_url\": scene_url\n            }\n            raise PublishXmlValidationError(self, msg, key=\"file_not_found\",\n                                            formatting_data=formatting_data)\n\n\ndef _update_frames(expected_settings):\n    \"\"\"\n        Calculate proper frame range including handles set in DB.\n\n        Harmony requires rendering from 1, so frame range is always moved\n        to 1.\n    Args:\n        expected_settings (dict): pulled from DB\n\n    Returns:\n        modified expected_setting (dict)\n    \"\"\"\n    frames_count = expected_settings[\"frameEnd\"] - \\\n        expected_settings[\"frameStart\"] + 1\n\n    expected_settings[\"frameStart\"] = 1.0 + expected_settings[\"handleStart\"]\n    expected_settings[\"frameEnd\"] = \\\n        expected_settings[\"frameStart\"] + frames_count - 1\n    return expected_settings\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/.eslintrc.json",
    "content": "{\n    \"env\": {\n        \"browser\": true\n    },\n    \"extends\": \"eslint:recommended\",\n    \"ignorePatterns\": [\"**/*.js\"]\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/.gitattributes",
    "content": "$.html merge=ours \n$.oAttribute.html merge=ours\n$.oBackdrop.html merge=ours\n$.oBox.html merge=ours\n$.oColor.html merge=ours\n$.oColorValue.html merge=ours\n$.oColumn.html merge=ours\n$.oDialog.html merge=ours\n$.oDialog.Progress.html merge=ours\n$.oDrawing.html merge=ours\n$.oDrawingColumn.html merge=ours\n$.oDrawingNode.html merge=ours\n$.oElement.html merge=ours\n$.oFile.html merge=ours\n$.oFolder.html merge=ours\n$.oFrame.html merge=ours\n$.oGroupNode.html merge=ours\n$.oList.html merge=ours\n$.oNetwork.html merge=ours\n$.oNode.html merge=ours\n$.oNodeLink.html merge=ours\n$.oPalette.html merge=ours\n$.oPathPoint.html merge=ours\n$.oPegNode.html merge=ours\n$.oPoint.html merge=ours\n$.oScene.html merge=ours\n$.oThread.html merge=ours\n$.oTimeline.html merge=ours\n$.oTimelineLayer.html merge=ours\n$.oUtils.html merge=ours\n$.index.html merge=ours\n$.global.html merge=ours\n$.oDatabase.html merge=ours\n$.oProgressDialog.html merge=ours\n$.oProcess.html merge=ours\nNodeTypes.html merge=ours"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/.gitignore",
    "content": "node_modules/*\n\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/Install.bat",
    "content": "@echo off\r\nSETLOCAL ENABLEDELAYEDEXPANSION \r\nSET dlPath=%~dp0\r\nset harmonyPrefsDir=%appdata%\\Toon Boom Animation\r\n\r\nSETX LIB_OPENHARMONY_PATH %dlPath%\r\n\r\necho -------------------------------------------------------------------\r\necho -- Starting install of openHarmony open source scripting library --\r\necho -------------------------------------------------------------------\r\necho OpenHarmony will be installed to the folder :\r\necho %dlpath%\r\necho Do not delete the contents of this folder.\r\n\r\nREM Check Harmony Versions and make a list\r\nfor /d %%D in (\"%harmonyPrefsDir%\\*Harmony*\") do (\r\n  set harmonyVersionDir=%%~fD\r\n  for /d %%V in (\"!harmonyVersionDir!\\*-layouts*\") do (\r\n    set \"folderName=%%~nD\"\r\n    set \"versionName=%%~nV\"\r\n    set \"harmonyFolder=!folderName:~-7!\"\r\n    set \"harmonyVersions=!versionName:~0,2!\"\r\n    echo Found Toonboom Harmony !harmonyFolder! !harmonyVersions! - installing openHarmony for this version.\r\n    set \"installDir=!harmonyPrefsDir!\\Toon Boom Harmony !harmonyFolder!\\!harmonyVersions!00-scripts\\\"\r\n\r\n    if not \"!installDir!\" == \"!dlPath!\" (\r\n      REM creating a \"openHarmony.js\" file in script folders\r\n      if not exist \"!installDir!\" mkdir \"!installDir!\"\r\n\r\n      cd !installDir!\r\n      \r\n      set \"script=include(System.getenv('LIB_OPENHARMONY_PATH')+'openHarmony.js');\" \r\n      echo !script!> openHarmony.js\r\n    )\r\n    echo ---- done. ----\r\n  )\r\n)\r\n\r\necho - Install Complete -\r\npause"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/LICENSE",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/README.md",
    "content": "# OpenHarmony - The Toonboom Harmony Open Source DOM Library\n\n## Why did we make this library ?\n\nEver tried to make a simple script for toonboom Harmony, then got stumped by the numerous amount of steps required to execute the simplest action? Or bored of coding the same helper routines again and again for every studio you work for?\n\nToonboom Harmony is a very powerful software, with hundreds of functions and tools, and it unlocks a great amount of possibilities for animation studios around the globe. And... being the produce of the hard work of a small team forced to prioritise, it can also be a bit rustic at times!\n\nWe are users at heart, animators and riggers, who just want to interact with the software as simply as possible. Simplicity is at the heart of the design of openHarmony. But we also are developers, and we made the library for people like us who can't resist tweaking the software and bend it in all possible ways, and are looking for powerful functions to help them do it.\n\nThis library's aim is to create a more direct way to interact with Toonboom through scripts, by providing a more intuitive way to access its elements, and help with the cumbersome and repetitive tasks as well as help unlock untapped potential in its many available systems. So we can go from having to do things like this:\n\n```javascript\n  // adding a Drawing to the scene with the official API\n  var myNodeName = \"Drawing\";\n  var myColumnName = myNodeName;\n  var myNode = node.add(\"Top\", myNodeName, \"READ\",0,0,0);\n  var myColumn = column.add(myColumnName, \"DRAWING\", \"BOTTOM\");\n  var myElement = element.add (myNodeName, \"COLOR\", 12, \"SCAN\", \"TVG\");\n  column.setElementIdOfDrawing(myColumnName, myElement);\n  node.linkAttr (myNode, \"DRAWING.ELEMENT\", myColumnName);\n  drawing.create (myElement, \"1\", false, false);\n  column.setEntry (myColumnName, 0, 1, \"1\");\n```\n\nto simply writing :\n\n```javascript\n  // with openHarmony\n  var myNode = $.scene.root.addDrawingNode(\"Drawing\");\n  myNode.element.addDrawing(1);\n```\n\nLess time spent coding, more time spent having ideas!\n\n-----\n## Do I need any knowledge of toonboom scripting to use openHarmony?\n\nOpenHarmony aims to be self contained and to reimplement all the basic functions of the Harmony API. So, while it might help to have prior experience to understand what goes on under the hood, knowledge of the official API is not required.\n\nHowever, should you reach the limits of what openHarmony can offer at this time, you can always access the official API at any moment. Maybe you can submit a request and the missing parts will be added eventually, or you can even delve into the code and add the necessary functions yourself if you feel like it!\n\nYou can access a list of all the functions, how to use them, as well as examples, from the online documentation:\n\n[https://cfourney.github.io/OpenHarmony/$.html](https://cfourney.github.io/OpenHarmony/$.html)\n\nTo help you get started, here is a full example using the library to make and animate a small car, covering most of the basic features.\n\n[https://github.com/cfourney/OpenHarmony/blob/master/examples/openHarmonyExample.js](https://github.com/cfourney/OpenHarmony/blob/master/examples/openHarmonyExample.js)\n\n-----\n## The OpenHarmony Document Object Model or DOM\n\nOpenHarmony is based around the four principles of Object Oriented Programming: *Abstraction*, *Encapsulation*, *Inheritance*, *Polymorphism*.\n\nThis means every element of the Harmony scene has a corresponding abstraction existing in the code as a class. We have oNode, oScene, oColumn, etc. Unlike in the official API, each class is designed to create objects that are instances of these classes and encapsulate them and all their actions. It means no more storing the path of nodes, column abstract names and element ids to interact with them; if you can create or call it, you can access all of its functionalities. Nodes are declined as DrawingNodes and PegNodes, which inherint from the Node Class, and so on.\n\nThe openHarmony library doesn't merely provide *access* to the elements of a Toonboom Harmony file, it *models* them and their relationship to each others.\n\n<img src=\"https://raw.githubusercontent.com/cfourney/OpenHarmony/master/oH_DOM.jpg\" alt=\"The Document ObjectModel\" width=\"1600\">\n\nThe *Document Object Model* is a way to organise the elements of the Toonboom scene by highlighting the way they interact with each other. The Scene object has a root group, which contains Nodes, which have Attributes which can be linked to Columns which contain Frames, etc. This way it's always easy to find and access the content you are looking for. The attribute system has also been streamlined and you can now set values of node properties with a simple attribution synthax.\n\nWe implemented a global access to all elements and functions through the standard **dot notation** for the hierarchy, for ease of use, and clarity of code.\n\nFunctions and methods also make extensive use of **optional parameters** so no more need to fill in all arguments when calling functions when the default behavior is all that's needed.\n\nOn the other hand, the \"o\" naming scheme allows us to retain full access to the official API at all times. This means you can use it only when it really makes your life better.\n\n-----\n## Adopting openHarmony for your project\n\nThis library is made available under the [Mozilla Public license 2.0](https://www.mozilla.org/en-US/MPL/2.0/).\n\nOpenHarmony can be downloaded from [this repository](https://github.com/cfourney/OpenHarmony/releases/) directly. In order to make use of its functions, it needs to be unzipped next to the scripts you will be writing.\n\nAll you have to do is call :\n```javascript\ninclude(\"openHarmony.js\");\n```\nat the beginning of your script.\n\nYou can ask your users to download their copy of the library and store it alongside, or bundle it as you wish as long as you include the license file provided on this repository.\n\nThe entire library is documented at the address :\n\nhttps://cfourney.github.io/OpenHarmony/$.html\n\nThis include a list of all the available functions as well as examples and references (such as the list of all available node attributes).\n\nAs time goes by, more functions will be added and the documentation will also get richer as more examples get created.\n\n-----\n## Installation\n\n#### simple install:\n- download the zip from [the releases page](https://github.com/cfourney/OpenHarmony/releases/),\n- unzip the contents to [your scripts folder](https://docs.toonboom.com/help/harmony-17/advanced/scripting/import-script.html).\n\n#### advanced install (for developers):\n- clone the repository to the location of your choice\n\n -- or --\n\n- download the zip from [the releases page](https://github.com/cfourney/OpenHarmony/releases/)\n- unzip the contents where you want to store the library,\n\n -- then --\n\n- run `install.bat`.\n\nThis last step will tell Harmony where to look to load the library, by setting the environment variable `LIB_OPENHARMONY_PATH` to the current folder.\n\nIt will then create a `openHarmony.js` file into the user scripts folder which calls the files from the folder from the `LIB_OPENHARMONY_PATH` variable, so that scripts can make direct use of it without having to worry about where openHarmony is stored.\n\n##### Troubleshooting:\n- to test if the library is correctly installed, open the `Script Editor` window and type:\n```javascript\ninclude (\"openHarmony.js\");\n$.alert(\"hello world\");\n```\nRun the script, and if there is an error (for ex `MAX_REENTRENCY `), check that the file `openHarmony.js` exists in the script folder, and contains only the line:\n```javascript\ninclude(System.getenv('LIB_OPENHARMONY_PATH')+'openHarmony.js');\n```\nCheck that the environment variable `LIB_OPENHARMONY_PATH` is set correctly to the remote folder.\n\n-----\n## How to add openHarmony to vscode intellisense for autocompletion\n\nAlthough not fully supported, you can get most of the autocompletion features to work by adding the following lines to a `jsconfig.json` file placed at the root of your working folder.\nThe paths need to be relative which means the openHarmony source code must be placed directly in your developping environment.\n\nFor example, if your working folder contains the openHarmony source in a folder called `OpenHarmony` and your working scripts in a folder called `myScripts`, place the `jsconfig.json` file at the root of the folder and add these lines to the file:\n\n```javascript\n{\n  include : [\n    \"OpenHarmony/*\",\n    \"OpenHarmony/openHarmony/*\",\n    \"myScripts/*\",\n    \"*\"\n  ]\n}\n```\n\n[More information on vs code and jsconfig.json.](https://code.visualstudio.com/docs/nodejs/working-with-javascript)\n\n-----\n## Let's get technical. I can code, and want to contribute, where do I start?\n\nReading and understanding the existing code, or at least the structure of the lib, is a great start, but not a requirement. You can simply start adding your classes to the $ object that is the root of the harmony lib, and start implementing. However, try to follow these guidelines as they are the underlying principles that make the library consistent:\n\n  * There is a $ global object, which contains all the class declarations, and can be passed from one context to another to access the functions.\n\n  * Each class is an abstract representation of a core concept of Harmony, so naming and consistency (within the lib) is essential. But we are not bound by the structure or naming of Harmony if we find a better way, for example to make nomenclatures more consistent between the scripting interface and the UI.\n\n  * Each class defines a bunch of class properties with getter/setters for the values that are directly related to an entity of the scene. If you're thinking of making a getter function that doesn't require arguments, use a getter setter instead!\n\n  * Each class also defines methods which can be called on the class instances to affect its contents, or its children's contents. For example, you'd go to the scene class to add the things that live in the scene, such as elements, columns and palettes. You wouldn't go to the column class or palette class to add one, because then what are you adding it *to*?\n\n  * We use encapsulation over having to pass a function arguments every time we can. Instead of adding a node to the scene, and having to pass a group as argument, adding a node is done directly by calling a method of the parent group. This way the parent/child relationship is always clear and the arguments list kept to a minimum.\n\n  * The goal is to make the most useful set of functions we can. Instead of making a large function that does a lot, consider extracting the small useful subroutines you need in your function into the existing classes directly.\n\n  * Each method argument besides the core one (for example, for adding nodes, we have to specify the type of the new node we create) must have a default fallback to make the argument optional.\n\n  * Don't use globals ever, but maybe use a class property if you need an enum for example.\n\n  * Don't use the official API namespace, any function that exists in the official API must remain accessible otherwise things will break. Prefix your class names with \"o\" to avoid this and to signify this function is part of openHarmony.\n\n  * We use the official API as little as we can in the code, so that if the implementation changes, we can easily fix it in a minimal amount of places. Wrap it, then use the wrapper. (ex: oScene.name)\n\n  * Users of the lib should almost never have to use \"new\" to create instances of their classes. Create accessors/factories that will do that for them. For example, $.scn creates and return a oScene instance, and $.scn.nodes returns new oNodes instances, but users don't have to create them themselves, so it's like they were always there, contained within. It also lets you create different subclasses for one factory. For example, $.scn.$node(\"Top/myNode\") will either return a oNode, oDrawingNode, oPegNode or oGroupNode object depending on the node type of the node represented by the object.\n  Exceptions are small useful value containing objects that don't belong to the Harmony hierarchy like oPoint, oBox, oColorValue, etc.\n\n  * It's a JS Library, so use camelCase naming and try to follow the google style guide for JS :\n  https://google.github.io/styleguide/jsguide.html\n\n  * Document your new functions using the JSDocs synthax : https://devdocs.io/jsdoc/howto-es2015-classes\n\n  * Make a branch, create a merge request when you're done, and we'll add the new stuff you added to the lib :)\n\n\n-----\n## Credits\n\nThis library was created by Mathieu Chaptel and Chris Fourney.\n\nIf you're using openHarmony, and are noticing things that you would like to see in the library, please feel free to contribute to the code directly, or send us feedback through Github. This project will only be as good as people working together can make it, and we need every piece of code and feedback we can get, and would love to hear from you!\n\n-----\n## Community\n\nJoin the discord community for help with the library and to contribute:\nhttps://discord.gg/kgT38MG\n\n-----\n## Acknowledgements\n  * [Yu Ueda](https://github.com/yueda1984) for his help to understand Harmony coordinate systems\n  * [Dash](https://github.com/35743) for his help to debug, test and develop the Pie Menus widgets\n  * [All the contributors](https://github.com/cfourney/OpenHarmony/graphs/contributors) for their precious help."
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/build_doc.bat",
    "content": "jsdoc -c ./documentation.json -t ../node_modules/jaguarjs-jsdoc\r\npause"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/documentation.json",
    "content": "{\n    \"plugins\": [],\n    \"recurseDepth\": 10,\n    \"source\": {\n        \"include\": [\".\"],\n        \"includePattern\": \".+\\\\.js(doc|x)?$\",\n        \"exclude\": [ \"./openHarmony_tools.js\" ]\n    },\n    \"sourceType\": \"module\",\n    \"tags\": {\n        \"allowUnknownTags\": true,\n        \"dictionaries\": [\"jsdoc\",\"closure\"]\n    },\n    \"templates\": {\n        \"cleverLinks\": false,\n        \"monospaceLinks\": false\n    },\n    \"opts\": {\n        \"encoding\": \"utf8\",               \n        \"destination\": \"./docs/\",          \n        \"recurse\": true\n    }\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/install.sh",
    "content": "#!/bin/bash\n\nset dlPath=pwd\nset harmonyPrefsDir=~/Library/Preferences/Toon Boom Animation/\n\necho -------------------------------------------------------------------\necho -- Starting install of openHarmony open source scripting library --\necho -------------------------------------------------------------------\necho OpenHarmony will be installed to the folder :\necho $dlpath\necho Do not delete the contents of this folder.\n\nREM Check Harmony Versions and make a list\nfor /d %%D in (\"%harmonyPrefsDir%\\*Harmony*\") do (\n  set harmonyVersionDir=%%~fD\n  for /d %%V in (\"!harmonyVersionDir!\\*-layouts*\") do (\n    set \"folderName=%%~nD\"\n    set \"versionName=%%~nV\"\n    set \"harmonyFolder=!folderName:~-7!\"\n    set \"harmonyVersions=!versionName:~0,2!\"\n    echo Found Toonboom Harmony !harmonyFolder! !harmonyVersions! - installing openHarmony for this version.\n    set \"installDir=!harmonyPrefsDir!\\Toon Boom Harmony !harmonyFolder!\\!harmonyVersions!00-scripts\\\"\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_actions.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//\n// This document was outputted by this function.\n//\n//\n// printOutDoc();\n//\n// function printOutDoc(){\n// \tvar doc = \"\";\n// \tvar responders = Action.getResponderList()\n// \tfor (var i in responders){\n// \t\tvar docString = [\"\\n\\n/\\**\\n * Actions available in the \"+responders[i]+\" responder.\", \" @name actions#\"+responders[i]];\n// \t\tvar actions = Action.getActionList(responders[i]);\n// \t\tfor (var j in actions){\n// \t\t\tdocString.push(\" @property {QAction} \"+actions[j]);\n// \t\t}\n// \t\tdocString.push (\"/\");\n// \t\tdoc += docString.join(\"\\n *\");\n// \t}\n//\n// \tMessageLog.trace(doc);\n// }\n\n\n/**\n * Actions are used by the Harmony interface to represent something the user can ask the software to do. This is a list of all the available names and responders available.\n * @class actions\n * @hideconstructor \n * @namespace\n * @example\n * // To check whether an action is available, call the synthax:\n * Action.validate (<actionName>, <responder>);\n * \n * // To launch an action, call the synthax:\n * Action.perform (<actionName>, <responder>, parameters);\n */\n\n/**\n * Actions available in the ExportGifResponder responder.\n * @name actions#ExportGifResponder\n * @property {QAction} onActionExportGif()\n */\n\n/**\n * Actions available in the ExposureFillResponder responder.\n * @name actions#ExposureFillResponder\n * @property {QAction} onActionExposureFillUsingRenderChange()\n */\n\n/**\n * Actions available in the GameSkinResponder responder.\n * @name actions#GameSkinResponder\n */\n\n/**\n * Actions available in the GuideToolResponder responder.\n * @name actions#GuideToolResponder\n */\n\n/**\n * Actions available in the MatteGeneratorResponder responder.\n * @name actions#MatteGeneratorResponder\n * @property {QAction} onActionNull()\n * @property {QAction} onActionNullAddHint()\n * @property {QAction} onActionGenerateMatte()\n * @property {QAction} onActionGenerateHints()\n * @property {QAction} onActionMakeMatteVerticesAnimatable()\n * @property {QAction} onActionMergeInnerOuterContours()\n * @property {QAction} onActionRemoveUnusedMatteDisplacements()\n * @property {QAction} onActionShowOptionsInCameraView()\n * @property {QAction} onActionRemoveSelectedHints()\n * @property {QAction} onActionRemoveSelectedVertices()\n * @property {QAction} onActionRemoveSelectedVertexAnimation()\n * @property {QAction} onActionEnableNormalMode()\n * @property {QAction} onActionEnableSetupMode()\n * @property {QAction} onActionEnableAddVertexMode()\n * @property {QAction} onActionEnableAddHintMode()\n * @property {QAction} onActionToggleShowOuterContour()\n * @property {QAction} onActionToggleShowInnerContour()\n * @property {QAction} onActionToggleShowPointId()\n * @property {QAction} onActionToggleShowHints()\n * @property {QAction} onActionToggleShowOverlayAnnotation()\n * @property {QAction} onActionToggleShowInflate()\n * @property {QAction} onActionAlignMatteHandles()\n * @property {QAction} onActionShortenMatteHandles()\n * @property {QAction} onActionExpandOuterContour()\n * @property {QAction} onActionExpandInnerContour()\n * @property {QAction} onActionReduceOuterContour()\n * @property {QAction} onActionReduceInnerContour()\n * @property {QAction} onActionToggleAutoKey()\n * @property {QAction} onActionToggleFixAdjacentKeyframes()\n * @property {QAction} onActionReloadView()\n * @property {QAction} onActionDefineResourceFolder()\n * @property {QAction} onActionExportResourceFolder()\n * @property {QAction} onActionResetResourceFolder()\n */\n\n/**\n * Actions available in the ModuleLibraryIconView responder.\n * @name actions#ModuleLibraryIconView\n */\n\n/**\n * Actions available in the ModuleLibraryListView responder.\n * @name actions#ModuleLibraryListView\n */\n\n/**\n * Actions available in the ModuleLibraryTemplatesResponder responder.\n * @name actions#ModuleLibraryTemplatesResponder\n */\n\n/**\n * Actions available in the Node View responder.\n * @name actions#Node View\n * @property {QAction} onActionResetView()\n * @property {QAction} onActionTag()\n * @property {QAction} onActionUntag()\n * @property {QAction} onActionUntagAll()\n * @property {QAction} onActionUntagAllOthers()\n * @property {QAction} onActionZoomIn()\n * @property {QAction} onActionZoomOut()\n * @property {QAction} onActionResetZoom()\n * @property {QAction} onActionResetPan()\n * @property {QAction} onActionShowAllModules()\n * @property {QAction} onActionPasteSpecial()\n * @property {QAction} onActionPasteSpecialAgain()\n * @property {QAction} onActionCloneElement()\n * @property {QAction} onActionCloneElement_DrawingsOnly()\n * @property {QAction} onActionCopyQualifiedName()\n * @property {QAction} onActionDuplicateElement()\n * @property {QAction} onActionSelEnable()\n * @property {QAction} onActionEnableAll()\n * @property {QAction} onActionSelDisable()\n * @property {QAction} onActionDisableAllUnselected()\n * @property {QAction} onActionRecomputeAll()\n * @property {QAction} onActionRecomputeSelected()\n * @property {QAction} onActionSelCreateGroup()\n * @property {QAction} onActionSelCreateGroupWithComposite()\n * @property {QAction} onActionSelMoveToParentGroup()\n * @property {QAction} onActionSelMergeInto()\n * @property {QAction} onActionClearPublishedAttributes()\n * @property {QAction} onActionPrintNetwork()\n * @property {QAction} onActionUpToParent()\n * @property {QAction} onActionShowHideWorldView()\n * @property {QAction} onActionMoveWorldNE()\n * @property {QAction} onActionMoveWorldNW()\n * @property {QAction} onActionMoveWorldSE()\n * @property {QAction} onActionMoveWorldSW()\n * @property {QAction} onActionEnterGroup()\n * @property {QAction} onActionCreateGroup()\n * @property {QAction} onActionCreatePeg()\n * @property {QAction} onActionCreateParentPeg()\n * @property {QAction} onActionCreateDisplay()\n * @property {QAction} onActionCreateRead()\n * @property {QAction} onActionCreateComposite()\n * @property {QAction} onActionCreateBackdrop()\n * @property {QAction} onActionToggleDefinePublishMode()\n * @property {QAction} onActionNavigateGroup()\n * @property {QAction} onActionGotoPortAbove()\n * @property {QAction} onActionGotoPortUnder()\n * @property {QAction} onActionGotoPortLeft()\n * @property {QAction} onActionGotoPortRight()\n * @property {QAction} onActionToggleDefineAttributeInfo()\n * @property {QAction} onActionCreateFavorite(QString)\n * @property {QAction} onActionCreateModule(QString)\n * @property {QAction} onActionCableLine()\n * @property {QAction} onActionCableStraight()\n * @property {QAction} onActionCableBezier()\n * @property {QAction} onActionRecenter()\n * @property {QAction} onActionFocusOnSelectionNV()\n * @property {QAction} onActionFocusOnParentNodeNV()\n * @property {QAction} onActionFocusOnChildNodeNV()\n * @property {QAction} onActionToggleSelectedThumbNail()\n * @property {QAction} onActionShowAllThumbNail()\n * @property {QAction} onActionHideAllThumbNails()\n * @property {QAction} onActionShowSelectedThumbNail()\n * @property {QAction} onActionHideSelectedThumbNail()\n * @property {QAction} onActionNaviSelectChild()\n * @property {QAction} onActionNaviSelectChilds()\n * @property {QAction} onActionNaviSelectParent()\n * @property {QAction} onActionNaviSelectPreviousBrother()\n * @property {QAction} onActionNaviSelectNextBrother()\n * @property {QAction} onActionNaviSelectInnerChildren()\n * @property {QAction} onActionNaviSelectParentWithEffects()\n * @property {QAction} onActionNaviSelectChildWithEffects()\n * @property {QAction} onActionSelectLinkedLayers()\n * @property {QAction} onActionSetDrawingAsSubLayer()\n * @property {QAction} onActionUnlinkSubLayer()\n * @property {QAction} onActionAddSubLayer()\n * @property {QAction} onActionRenameWaypoint()\n * @property {QAction} onActionCreateWaypointFromContextMenu()\n * @property {QAction} onActionCreateWaypointFromShortcut()\n */\n\n/**\n * Actions available in the ParticleCoreGuiResponder responder.\n * @name actions#ParticleCoreGuiResponder\n * @property {QAction} onActionInsertParticleTemplate(QString)\n * @property {QAction} onActionToggleShowAsDots()\n */\n\n/**\n * Actions available in the PluginHelpViewResponder responder.\n * @name actions#PluginHelpViewResponder\n * @property {QAction} onActionShowShortcuts()\n */\n\n/**\n * Actions available in the Script responder.\n * @name actions#Script\n */\n\n/**\n * Actions available in the ScriptManagerResponder responder.\n * @name actions#ScriptManagerResponder\n */\n\n/**\n * Actions available in the ScriptViewResponder responder.\n * @name actions#ScriptViewResponder\n * @property {QAction} onActionNewScriptFile()\n * @property {QAction} onActionImportScript()\n * @property {QAction} onActionDeleteScript()\n * @property {QAction} onActionRefreshScripts()\n * @property {QAction} onActionRun()\n * @property {QAction} onActionDebug()\n * @property {QAction} onActionStopExecution()\n * @property {QAction} onActionOpenHelp()\n * @property {QAction} onActionSetTarget()\n * @property {QAction} onActionSetExternalEditor()\n * @property {QAction} onActionCallExternalEditor()\n */\n\n/**\n * Actions available in the ShiftAndTraceToolResponder responder.\n * @name actions#ShiftAndTraceToolResponder\n * @property {QAction} onActionEnableShiftAndTrace()\n * @property {QAction} onActionSelectShiftAndTraceTool()\n * @property {QAction} onActionToggleShowManipulator()\n * @property {QAction} onActionToggleShowPegs()\n * @property {QAction} onActionToggleShowOutline()\n * @property {QAction} onActionSetPegPosition(int)\n * @property {QAction} onActionShiftAndTraceToolRotateOverride()\n * @property {QAction} onActionShiftAndTraceToolScaleOverride()\n * @property {QAction} onActionShiftAndTraceToolResetCurrentPosition()\n * @property {QAction} onActionShiftAndTraceToolResetAllPositions()\n * @property {QAction} onActionResetCurrentShiftPosition()\n * @property {QAction} onActionResetAllShiftPositions()\n * @property {QAction} onActionShowCrossHair()\n * @property {QAction} onActionAddCrossHairMode()\n * @property {QAction} onActionRemoveCrossHair()\n */\n\n/**\n * Actions available in the artLayerResponder responder.\n * @name actions#artLayerResponder\n * @property {QAction} onActionPreviewModeToggle()\n * @property {QAction} onActionOverlayArtSelected()\n * @property {QAction} onActionLineArtSelected()\n * @property {QAction} onActionColorArtSelected()\n * @property {QAction} onActionUnderlayArtSelected()\n * @property {QAction} onActionToggleLineColorArt()\n * @property {QAction} onActionToggleOverlayUnderlayArt()\n */\n\n/**\n * Actions available in the brushSettingsResponder responder.\n * @name actions#brushSettingsResponder\n */\n\n/**\n * Actions available in the cameraView responder.\n * @name actions#cameraView\n * @property {QAction} onActionRenameDrawing()\n * @property {QAction} onActionRenameDrawingWithPrefix()\n * @property {QAction} onActionDeleteDrawings()\n * @property {QAction} onActionToggleShowSymbolPivot()\n * @property {QAction} onActionShowGrid()\n * @property {QAction} onActionNormalGrid()\n * @property {QAction} onAction12FieldGrid()\n * @property {QAction} onAction16FieldGrid()\n * @property {QAction} onActionWorldGrid()\n * @property {QAction} onActionGridUnderlay()\n * @property {QAction} onActionGridOverlay()\n * @property {QAction} onActionFieldGridBox()\n * @property {QAction} onActionHideLineTexture()\n * @property {QAction} onActionAutoLightTable()\n * @property {QAction} onActionGetRightToModifyDrawings()\n * @property {QAction} onActionReleaseRightToModifyDrawings()\n * @property {QAction} onActionChooseSelectToolOverride()\n * @property {QAction} onActionChooseContourEditorToolOverride()\n * @property {QAction} onActionChooseCenterlineEditorToolOverride()\n * @property {QAction} onActionChooseDeformToolOverride()\n * @property {QAction} onActionChoosePerspectiveToolOverride()\n * @property {QAction} onActionChooseCutterToolOverride()\n * @property {QAction} onActionChooseMorphToolOverride()\n * @property {QAction} onActionChooseBrushToolOverride()\n * @property {QAction} onActionChooseRepositionAllDrawingsToolOverride()\n * @property {QAction} onActionChooseEraserToolOverride()\n * @property {QAction} onActionChooseRepaintBrushToolOverride()\n * @property {QAction} onActionChoosePencilToolOverride()\n * @property {QAction} onActionChooseLineToolOverride()\n * @property {QAction} onActionChoosePolylineToolOverride()\n * @property {QAction} onActionChooseRectangleToolOverride()\n * @property {QAction} onActionChooseEllipseToolOverride()\n * @property {QAction} onActionChoosePaintToolOverride()\n * @property {QAction} onActionChooseInkToolOverride()\n * @property {QAction} onActionChoosePaintUnpaintedToolOverride()\n * @property {QAction} onActionChooseRepaintToolOverride()\n * @property {QAction} onActionChooseStrokeToolOverride()\n * @property {QAction} onActionChooseCloseGapToolOverride()\n * @property {QAction} onActionChooseUnpaintToolOverride()\n * @property {QAction} onActionChooseDropperToolOverride()\n * @property {QAction} onActionChooseEditTransformToolOverride()\n * @property {QAction} onActionChooseGrabberToolOverride()\n * @property {QAction} onActionChooseZoomToolOverride()\n * @property {QAction} onActionChooseRotateToolOverride()\n * @property {QAction} onActionChooseThirdPersonNavigation3dToolOverride()\n * @property {QAction} onActionChooseFirstPersonNavigation3dToolOverride()\n * @property {QAction} onActionChooseShiftAndTraceToolOverride()\n * @property {QAction} onActionChooseNoToolOverride()\n * @property {QAction} onActionChooseResizePenStyleToolOverride()\n * @property {QAction} onActionZoomIn()\n * @property {QAction} onActionZoomOut()\n * @property {QAction} onActionRotateCW()\n * @property {QAction} onActionRotateCCW()\n * @property {QAction} onActionToggleQuickCloseUp()\n * @property {QAction} onActionResetZoom()\n * @property {QAction} onActionResetRotation()\n * @property {QAction} onActionResetPan()\n * @property {QAction} onActionResetView()\n * @property {QAction} onActionRecenter()\n * @property {QAction} onActionMorphSwitchKeyDrawing()\n * @property {QAction} onActionShowPaletteManager()\n * @property {QAction} onActionShowColorEditor()\n * @property {QAction} onActionShowColorPicker()\n * @property {QAction} onActionShowColorModel()\n * @property {QAction} onActionShowThumbnailPanel()\n * @property {QAction} onActionPlayByFrame()\n * @property {QAction} onActionPreviousDrawing()\n * @property {QAction} onActionNextDrawing()\n * @property {QAction} onActionPreviousColumn()\n * @property {QAction} onActionNextColumn()\n * @property {QAction} onActionCreateEmptyDrawing()\n * @property {QAction} onActionDuplicateDrawing()\n * @property {QAction} onActionShowScanInfo()\n * @property {QAction} onActionSetThumbnailSize(int)\n * @property {QAction} onActionSelectedElementSwapToNextDrawing()\n * @property {QAction} onActionSelectedElementSwapToPrevDrawing()\n * @property {QAction} onActionToggleShiftAndTracePegView()\n * @property {QAction} onActionToggleShiftAndTraceManipulator()\n * @property {QAction} onActionShowMorphingInspector()\n * @property {QAction} onActionRemoveFromDrawingList()\n * @property {QAction} onActionResetDrawingPosition()\n * @property {QAction} onActionToggleDrawingOnPeg()\n * @property {QAction} onActionToggleDrawingOnPeg(VL_DrawingListWidget*)\n * @property {QAction} onActionToggleDrawingVisibility()\n * @property {QAction} onActionToggleDrawingVisibility(VL_DrawingListWidget*)\n * @property {QAction} onActionMoveDrawingUp()\n * @property {QAction} onActionMoveDrawingDown()\n * @property {QAction} onActionReturnToNormalMode()\n * @property {QAction} onActionLinkSelectedDrawings()\n * @property {QAction} onActionMainGotoNextFrame()\n * @property {QAction} onActionMainGotoPreviousFrame()\n * @property {QAction} onActionMainGotoFirstFrame()\n * @property {QAction} onActionMainGotoLastFrame()\n * @property {QAction} onActionRenameDrawing()\n * @property {QAction} onActionRenameDrawingWithPrefix()\n * @property {QAction} onActionNaviSelectChild()\n * @property {QAction} onActionNaviSelectChilds()\n * @property {QAction} onActionInsertControlPoint()\n * @property {QAction} onActionSelectControlPoint()\n * @property {QAction} onActionNaviSelectNextBrother()\n * @property {QAction} onActionNaviSelectParent()\n * @property {QAction} onActionNaviSelectPreviousBrother()\n * @property {QAction} onActionNaviSelectParentWithEffects()\n * @property {QAction} onActionNaviSelectChildWithEffects()\n * @property {QAction} onActionAutoLightTable()\n * @property {QAction} onActionSetSmallFilesResolution()\n * @property {QAction} onActionInvalidateCanvas()\n * @property {QAction} onActionChooseSpSelectToolOverride()\n * @property {QAction} onActionChooseSpTranslateToolOverride()\n * @property {QAction} onActionChooseSpRotateToolOverride()\n * @property {QAction} onActionChooseSpScaleToolOverride()\n * @property {QAction} onActionChooseSpSkewToolOverride()\n * @property {QAction} onActionChooseSpMaintainSizeToolOverride()\n * @property {QAction} onActionChooseSpTransformToolOverride()\n * @property {QAction} onActionChooseSpInverseKinematicsToolOverride()\n * @property {QAction} onActionChooseSpOffsetZToolOverride()\n * @property {QAction} onActionChooseSpSplineOffsetToolOverride()\n * @property {QAction} onActionChooseSpSmoothEditingToolOverride()\n * @property {QAction} onActionUnlockAll()\n * @property {QAction} onActionUnlock()\n * @property {QAction} onActionLockAll()\n * @property {QAction} onActionLockAllOthers()\n * @property {QAction} onActionLock()\n * @property {QAction} onActionTag()\n * @property {QAction} onActionUntag()\n * @property {QAction} onActionToggleCameraCone()\n * @property {QAction} onActionToggleCameraMask()\n * @property {QAction} onActionTogglePreventFromDrawing()\n * @property {QAction} onActionFocusOnSelectionCV()\n * @property {QAction} onActionTogglePlayback()\n * @property {QAction} onActionOnionOnSelection()\n * @property {QAction} onActionOnionOffSelection()\n * @property {QAction} onActionOnionOffAllOther()\n * @property {QAction} onActionOnionOnAll()\n * @property {QAction} onActionOnionOffAll()\n * @property {QAction} onActionOpenGLView()\n * @property {QAction} onActionRenderView()\n * @property {QAction} onActionMatteView()\n * @property {QAction} onActionDepthView()\n * @property {QAction} onActionNodeCacheEnable()\n * @property {QAction} onActionNodeCacheQuality()\n * @property {QAction} onActionNodeCacheHide()\n * @property {QAction} onActionNodeCacheDisable()\n * @property {QAction} onActionMorphSwitchKeyDrawing()\n * @property {QAction} onActionRender()\n * @property {QAction} onActionAutoRender()\n * @property {QAction} onActionEnterSymbol()\n * @property {QAction} onActionLeaveSymbol()\n */\n\n/**\n * Actions available in the colorOperationsResponder responder.\n * @name actions#colorOperationsResponder\n * @property {QAction} onActionRepaintColorInDrawing()\n */\n\n/**\n * Actions available in the coordControlView responder.\n * @name actions#coordControlView\n */\n\n/**\n * Actions available in the drawingSelectionResponder responder.\n * @name actions#drawingSelectionResponder\n */\n\n/**\n * Actions available in the drawingView responder.\n * @name actions#drawingView\n * @property {QAction} onActionRenameDrawing()\n * @property {QAction} onActionRenameDrawingWithPrefix()\n * @property {QAction} onActionDeleteDrawings()\n * @property {QAction} onActionToggleShowSymbolPivot()\n * @property {QAction} onActionShowGrid()\n * @property {QAction} onActionNormalGrid()\n * @property {QAction} onAction12FieldGrid()\n * @property {QAction} onAction16FieldGrid()\n * @property {QAction} onActionWorldGrid()\n * @property {QAction} onActionGridUnderlay()\n * @property {QAction} onActionGridOverlay()\n * @property {QAction} onActionFieldGridBox()\n * @property {QAction} onActionHideLineTexture()\n * @property {QAction} onActionAutoLightTable()\n * @property {QAction} onActionGetRightToModifyDrawings()\n * @property {QAction} onActionReleaseRightToModifyDrawings()\n * @property {QAction} onActionChooseSelectToolOverride()\n * @property {QAction} onActionChooseContourEditorToolOverride()\n * @property {QAction} onActionChooseCenterlineEditorToolOverride()\n * @property {QAction} onActionChooseDeformToolOverride()\n * @property {QAction} onActionChoosePerspectiveToolOverride()\n * @property {QAction} onActionChooseCutterToolOverride()\n * @property {QAction} onActionChooseMorphToolOverride()\n * @property {QAction} onActionChooseBrushToolOverride()\n * @property {QAction} onActionChooseRepositionAllDrawingsToolOverride()\n * @property {QAction} onActionChooseEraserToolOverride()\n * @property {QAction} onActionChooseRepaintBrushToolOverride()\n * @property {QAction} onActionChoosePencilToolOverride()\n * @property {QAction} onActionChooseLineToolOverride()\n * @property {QAction} onActionChoosePolylineToolOverride()\n * @property {QAction} onActionChooseRectangleToolOverride()\n * @property {QAction} onActionChooseEllipseToolOverride()\n * @property {QAction} onActionChoosePaintToolOverride()\n * @property {QAction} onActionChooseInkToolOverride()\n * @property {QAction} onActionChoosePaintUnpaintedToolOverride()\n * @property {QAction} onActionChooseRepaintToolOverride()\n * @property {QAction} onActionChooseStrokeToolOverride()\n * @property {QAction} onActionChooseCloseGapToolOverride()\n * @property {QAction} onActionChooseUnpaintToolOverride()\n * @property {QAction} onActionChooseDropperToolOverride()\n * @property {QAction} onActionChooseEditTransformToolOverride()\n * @property {QAction} onActionChooseGrabberToolOverride()\n * @property {QAction} onActionChooseZoomToolOverride()\n * @property {QAction} onActionChooseRotateToolOverride()\n * @property {QAction} onActionChooseThirdPersonNavigation3dToolOverride()\n * @property {QAction} onActionChooseFirstPersonNavigation3dToolOverride()\n * @property {QAction} onActionChooseShiftAndTraceToolOverride()\n * @property {QAction} onActionChooseNoToolOverride()\n * @property {QAction} onActionChooseResizePenStyleToolOverride()\n * @property {QAction} onActionZoomIn()\n * @property {QAction} onActionZoomOut()\n * @property {QAction} onActionRotateCW()\n * @property {QAction} onActionRotateCCW()\n * @property {QAction} onActionToggleQuickCloseUp()\n * @property {QAction} onActionResetZoom()\n * @property {QAction} onActionResetRotation()\n * @property {QAction} onActionResetPan()\n * @property {QAction} onActionResetView()\n * @property {QAction} onActionRecenter()\n * @property {QAction} onActionMorphSwitchKeyDrawing()\n * @property {QAction} onActionShowPaletteManager()\n * @property {QAction} onActionShowColorEditor()\n * @property {QAction} onActionShowColorPicker()\n * @property {QAction} onActionShowColorModel()\n * @property {QAction} onActionShowThumbnailPanel()\n * @property {QAction} onActionPlayByFrame()\n * @property {QAction} onActionPreviousDrawing()\n * @property {QAction} onActionNextDrawing()\n * @property {QAction} onActionPreviousColumn()\n * @property {QAction} onActionNextColumn()\n * @property {QAction} onActionCreateEmptyDrawing()\n * @property {QAction} onActionDuplicateDrawing()\n * @property {QAction} onActionShowScanInfo()\n * @property {QAction} onActionSetThumbnailSize(int)\n * @property {QAction} onActionSelectedElementSwapToNextDrawing()\n * @property {QAction} onActionSelectedElementSwapToPrevDrawing()\n * @property {QAction} onActionToggleShiftAndTracePegView()\n * @property {QAction} onActionToggleShiftAndTraceManipulator()\n * @property {QAction} onActionShowMorphingInspector()\n * @property {QAction} onActionRemoveFromDrawingList()\n * @property {QAction} onActionResetDrawingPosition()\n * @property {QAction} onActionToggleDrawingOnPeg()\n * @property {QAction} onActionToggleDrawingOnPeg(VL_DrawingListWidget*)\n * @property {QAction} onActionToggleDrawingVisibility()\n * @property {QAction} onActionToggleDrawingVisibility(VL_DrawingListWidget*)\n * @property {QAction} onActionMoveDrawingUp()\n * @property {QAction} onActionMoveDrawingDown()\n * @property {QAction} onActionReturnToNormalMode()\n * @property {QAction} onActionLinkSelectedDrawings()\n */\n\n/**\n * Actions available in the exportCoreResponder responder.\n * @name actions#exportCoreResponder\n * @property {QAction} onActionGenerateLayoutImage()\n */\n\n/**\n * Actions available in the graph3dresponder responder.\n * @name actions#graph3dresponder\n * @property {QAction} onActionShowSubnodeShape()\n * @property {QAction} onActionHideSubnodeShape()\n * @property {QAction} onActionEnableSubnode()\n * @property {QAction} onActionDisableSubnode()\n * @property {QAction} onActionCreateSubNodeTransformation()\n * @property {QAction} onActionAddSubTransformationFilter()\n * @property {QAction} onActionSelectParent()\n * @property {QAction} onActionSelectChild()\n * @property {QAction} onActionSelectNextSibling()\n * @property {QAction} onActionSelectPreviousSibling()\n * @property {QAction} onActionDumpSceneGraphInformation()\n */\n\n/**\n * Actions available in the ikResponder responder.\n * @name actions#ikResponder\n * @property {QAction} onActionSetIKNail()\n * @property {QAction} onActionSetIKHoldOrientation()\n * @property {QAction} onActionSetIKHoldX()\n * @property {QAction} onActionSetIKHoldY()\n * @property {QAction} onActionSetIKMinAngle()\n * @property {QAction} onActionSetIKMaxAngle()\n * @property {QAction} onActionRemoveAllConstraints()\n */\n\n/**\n * Actions available in the libraryView responder.\n * @name actions#libraryView\n */\n\n/**\n * Actions available in the logView responder.\n * @name actions#logView\n */\n\n/**\n * Actions available in the miniPegModuleResponder responder.\n * @name actions#miniPegModuleResponder\n * @property {QAction} onActionResetDeform()\n * @property {QAction} onActionCopyRestingPositionToCurrentPosition()\n * @property {QAction} onActionConvertEllipseToShape()\n * @property {QAction} onActionSelectRigTool()\n * @property {QAction} onActionInsertDeformationAbove()\n * @property {QAction} onActionInsertDeformationUnder()\n * @property {QAction} onActionToggleEnableDeformation()\n * @property {QAction} onActionToggleShowAllManipulators()\n * @property {QAction} onActionToggleShowAllROI()\n * @property {QAction} onActionToggleShowSimpleManipulators()\n * @property {QAction} onActionConvertSelectionToCurve()\n * @property {QAction} onActionStraightenSelection()\n * @property {QAction} onActionShowSelectedDeformers()\n * @property {QAction} onActionShowDeformer(QString)\n * @property {QAction} onActionHideDeformer(QString)\n * @property {QAction} onActionCreateKinematicOutput()\n * @property {QAction} onActionConvertDeformedDrawingsToDrawings()\n * @property {QAction} onActionConvertDeformedDrawingsAndCreateDeformation()\n * @property {QAction} onActionUnsetLocalFlag()\n * @property {QAction} onActionCopyCurvePositionToOffset()\n * @property {QAction} onActionAddDeformationModuleByName(QString)\n * @property {QAction} onActionCreateNewDeformationChain()\n * @property {QAction} onActionRenameTransformation()\n * @property {QAction} onActionSetTransformation()\n * @property {QAction} onActionSetMasterElementModule()\n * @property {QAction} onActionToggleShowManipulator()\n */\n\n/**\n * Actions available in the moduleLibraryView responder.\n * @name actions#moduleLibraryView\n * @property {QAction} onActionReceiveFocus()\n * @property {QAction} onActionNewCategory()\n * @property {QAction} onActionRenameCategory()\n * @property {QAction} onActionRemoveCategory()\n * @property {QAction} onActionRemoveUserModule()\n * @property {QAction} onActionRefresh()\n */\n\n/**\n * Actions available in the moduleResponder responder.\n * @name actions#moduleResponder\n * @property {QAction} onActionAddModuleByName(QString)\n * @property {QAction} onActionAddModule(int,int)\n */\n\n/**\n * Actions available in the onionSkinResponder responder.\n * @name actions#onionSkinResponder\n * @property {QAction} onActionOnionSkinToggle()\n * @property {QAction} onActionOnionSkinToggleCenterline()\n * @property {QAction} onActionOnionSkinToggleFramesToDrawingsMode()\n * @property {QAction} onActionOnionSkinNoPrevDrawings()\n * @property {QAction} onActionOnionSkin1PrevDrawing()\n * @property {QAction} onActionOnionSkin2PrevDrawings()\n * @property {QAction} onActionOnionSkin3PrevDrawings()\n * @property {QAction} onActionOnionSkinNoNextDrawings()\n * @property {QAction} onActionOnionSkin1NextDrawing()\n * @property {QAction} onActionOnionSkin2NextDrawings()\n * @property {QAction} onActionOnionSkin3NextDrawings()\n * @property {QAction} onActionSetMarksOnionSkinInBetween()\n * @property {QAction} onActionSetMarksOnionSkinInBreakdown()\n * @property {QAction} onActionSetMarksOnionSkinInKey()\n * @property {QAction} onActionSetDrawingEnhancedOnionSkin()\n * @property {QAction} onActionOnionSkinReduceNextDrawing()\n * @property {QAction} onActionOnionSkinAddNextDrawing()\n * @property {QAction} onActionOnionSkinReducePrevDrawing()\n * @property {QAction} onActionOnionSkinAddPrevDrawing()\n * @property {QAction} onActionToggleOnionSkinForCustomMarkedType(QString)\n * @property {QAction} onActionOnionSkinSelectedLayerOnly()\n * @property {QAction} onActionOnionSkinRenderStyle(int)\n * @property {QAction} onActionOnionSkinDrawingMode(int)\n * @property {QAction} onActionOnionSkinToggleBaseMode()\n * @property {QAction} onActionOnionSkinToggleAdvancedMode()\n * @property {QAction} onActionOnionSkinToggleColorWash()\n * @property {QAction} onActionOnionSkinLinkSliders()\n * @property {QAction} onActionOnionSkinToggleAdvancedNext(int)\n * @property {QAction} onActionOnionSkinToggleAdvancedPrev(int)\n * @property {QAction} onActionOnionSkinAdvancedNextSliderChanged(int,int)\n * @property {QAction} onActionOnionSkinAdvancedPrevSliderChanged(int,int)\n * @property {QAction} onActionMaxOpacitySliderChanged(int)\n */\n\n/**\n * Actions available in the onionSkinView responder.\n * @name actions#onionSkinView\n */\n\n/**\n * Actions available in the opacityPanel responder.\n * @name actions#opacityPanel\n * @property {QAction} onActionNewOpacityTexture()\n * @property {QAction} onActionDeleteOpacityTexture()\n * @property {QAction} onActionRenameOpacityTexture()\n * @property {QAction} onActionCurToPrefPalette()\n * @property {QAction} onActionPrefToCurPalette()\n */\n\n/**\n * Actions available in the paletteView responder.\n * @name actions#paletteView\n */\n\n/**\n * Actions available in the pencilPanel responder.\n * @name actions#pencilPanel\n * @property {QAction} onActionNewPencilTemplate()\n * @property {QAction} onActionDeletePencilTemplate()\n * @property {QAction} onActionRenamePencilTemplate()\n * @property {QAction} onActionShowSmallThumbnail()\n * @property {QAction} onActionShowLargeThumbnail()\n * @property {QAction} onActionShowStroke()\n */\n\n/**\n * Actions available in the scene responder.\n * @name actions#scene\n * @property {QAction} onActionHideSelection()\n * @property {QAction} onActionShowHidden()\n * @property {QAction} onActionRehideSelection()\n * @property {QAction} onActionInsertPositionKeyframe()\n * @property {QAction} onActionInsertKeyframe()\n * @property {QAction} onActionSetKeyFrames()\n * @property {QAction} onActionInsertControlPointAtFrame()\n * @property {QAction} onActionSetConstant()\n * @property {QAction} onActionSetNonConstant()\n * @property {QAction} onActionToggleContinuity()\n * @property {QAction} onActionToggleLockInTime()\n * @property {QAction} onActionResetTransformation()\n * @property {QAction} onActionResetAll()\n * @property {QAction} onActionResetAllExceptZ()\n * @property {QAction} onActionSelectPrevObject()\n * @property {QAction} onActionSelectNextObject()\n * @property {QAction} onActionToggleNoFBDragging()\n * @property {QAction} onActionToggleAutoApply()\n * @property {QAction} onActionToggleAutoLock()\n * @property {QAction} onActionToggleAutoLockPalettes()\n * @property {QAction} onActionToggleAutoLockPaletteLists()\n * @property {QAction} onActionToggleEnableWrite()\n * @property {QAction} onActionShowHideManager()\n * @property {QAction} onActionToggleControl()\n * @property {QAction} onActionHideAllControls()\n * @property {QAction} onActionPreviousDrawing()\n * @property {QAction} onActionNextDrawing()\n * @property {QAction} onActionPreviousColumn()\n * @property {QAction} onActionNextColumn()\n * @property {QAction} onActionShowSubNode(bool)\n * @property {QAction} onActionGotoDrawing1()\n * @property {QAction} onActionGotoDrawing2()\n * @property {QAction} onActionGotoDrawing3()\n * @property {QAction} onActionGotoDrawing4()\n * @property {QAction} onActionGotoDrawing5()\n * @property {QAction} onActionGotoDrawing6()\n * @property {QAction} onActionGotoDrawing7()\n * @property {QAction} onActionGotoDrawing8()\n * @property {QAction} onActionGotoDrawing9()\n * @property {QAction} onActionGotoDrawing10()\n * @property {QAction} onActionToggleVelocityEditor()\n * @property {QAction} onActionCreateScene()\n * @property {QAction} onActionChooseSelectToolInNormalMode()\n * @property {QAction} onActionChooseSelectToolInColorMode()\n * @property {QAction} onActionChoosePaintToolInPaintMode()\n * @property {QAction} onActionChooseInkTool()\n * @property {QAction} onActionChoosePaintToolInRepaintMode()\n * @property {QAction} onActionChoosePaintToolInUnpaintMode()\n * @property {QAction} onActionChoosePaintToolInPaintUnpaintedMode()\n * @property {QAction} onActionChooseBrushToolInBrushMode()\n * @property {QAction} onActionChooseBrushToolInRepaintBrushMode()\n * @property {QAction} onActionToggleDrawBehindMode()\n * @property {QAction} onActionWhatsThis()\n * @property {QAction} onActionEditProperties()\n */\n\n/**\n * Actions available in the sceneUI responder.\n * @name actions#sceneUI\n * @property {QAction} onActionSaveLayouts()\n * @property {QAction} onActionSaveWorkspaceAs()\n * @property {QAction} onActionShowLayoutManager()\n * @property {QAction} onActionFullscreen()\n * @property {QAction} onActionRaiseArea(QString,bool)\n * @property {QAction} onActionRaiseArea(QString)\n * @property {QAction} onActionSetLayout(QString,int)\n * @property {QAction} onActionLockScene()\n * @property {QAction} onActionLockSceneVersion()\n * @property {QAction} onActionPaintModePaletteManager()\n * @property {QAction} onActionPaintModeLogView()\n * @property {QAction} onActionPaintModeModelView()\n * @property {QAction} onActionPaintModeToolPropertiesView()\n * @property {QAction} onActionUndo()\n * @property {QAction} onActionUndo(int)\n * @property {QAction} onActionRedo()\n * @property {QAction} onActionRedo(int)\n * @property {QAction} onActionShowCurrentDrawingOnTop()\n * @property {QAction} onActionShowWelcomeScreen()\n * @property {QAction} onActionShowWelcomeScreenQuit()\n * @property {QAction} onActionSaveLayoutInScene()\n * @property {QAction} onActionNewView(int)\n * @property {QAction} onActionNewView(QString)\n * @property {QAction} onActionNewViewChecked(QString)\n * @property {QAction} onActionToggleRenderer()\n * @property {QAction} onActionToggleBBoxHighlighting()\n * @property {QAction} onActionToggleShowLockedDrawingsInOutline()\n * @property {QAction} onActionCancelSoftRender()\n * @property {QAction} onActionCheckFiles()\n * @property {QAction} onActionCleanPaletteLists()\n * @property {QAction} onActionDeleteVersions()\n * @property {QAction} onActionMacroManager()\n * @property {QAction} onActionRestoreDefaultLayout()\n * @property {QAction} onActionExit()\n * @property {QAction} onActionExitDelayed()\n * @property {QAction} onActionAddVectorDrawing()\n * @property {QAction} onActionAddSound()\n * @property {QAction} onActionAddPeg()\n * @property {QAction} onActionToggleShowMergeSelectionDialog()\n * @property {QAction} onActionToggleShowScanDialog()\n * @property {QAction} onActionSetPreviewResolution(int)\n * @property {QAction} onActionSetTempoMarker()\n * @property {QAction} onActionSingleFlip()\n * @property {QAction} onActionNewScene()\n * @property {QAction} onActionNewSceneDelayed()\n * @property {QAction} onActionOpen()\n * @property {QAction} onActionOpenDelayed()\n * @property {QAction} onActionOpenScene()\n * @property {QAction} onActionOpenSceneDelayed()\n * @property {QAction} onActionOpenScene(QString)\n * @property {QAction} onActionSaveEverything()\n * @property {QAction} onActionSaveEverythingIncludingSceneMachineFrames()\n * @property {QAction} onActionSaveAsScene()\n * @property {QAction} onActionSaveVersion()\n * @property {QAction} onActionSaveDialog()\n * @property {QAction} onActionImportDrawings()\n * @property {QAction} onActionImport3dmodels()\n * @property {QAction} onActionImportTimings()\n * @property {QAction} onActionScanDrawings()\n * @property {QAction} onActionImportLocalLibrary()\n * @property {QAction} onActionImportSound()\n * @property {QAction} onActionMmxImport()\n * @property {QAction} onActionFlashExport()\n * @property {QAction} onActionFLVExport()\n * @property {QAction} onActionMmxExport()\n * @property {QAction} onActionSoundtrackExport()\n * @property {QAction} onActionComposite()\n * @property {QAction} onActionCompositeBatchOnly()\n * @property {QAction} onActionSaveOpenGLFrames()\n * @property {QAction} onActionToggleFlipForCustomMarkedType(QString)\n * @property {QAction} onActionCreateFullImport()\n * @property {QAction} onActionPerformFullImport()\n * @property {QAction} onActionPerformPartialImport()\n * @property {QAction} onActionPerformPartialUpdate()\n * @property {QAction} onActionToggleToolBar(QString)\n * @property {QAction} onActionSetDefaultDisplay(QString)\n * @property {QAction} onActionCloseScene()\n * @property {QAction} onActionCloseSceneDelayed()\n * @property {QAction} onActionCloseThenReopen()\n * @property {QAction} onActionOpenDrawings()\n * @property {QAction} onActionOpenDrawingsDelayed()\n * @property {QAction} onActionOpenDrawingsModify()\n * @property {QAction} onActionOpenElements()\n * @property {QAction} onActionOpenElementsDelayed()\n * @property {QAction} onActionClearRecentSceneList()\n * @property {QAction} onActionOpenBackgroundFile()\n * @property {QAction} onActionUnloadBackground()\n * @property {QAction} onActionScaleBackgroundUp()\n * @property {QAction} onActionScaleBackgroundDown()\n * @property {QAction} onActionResetBackgroundPosition()\n * @property {QAction} onActionSetDrawingMarksFlipKey()\n * @property {QAction} onActionSetDrawingMarksFlipBreakdown()\n * @property {QAction} onActionSetDrawingMarksFlipInBetween()\n * @property {QAction} onActionChooseSelectTool()\n * @property {QAction} onActionChooseContourEditorTool()\n * @property {QAction} onActionChooseCenterlineEditorTool()\n * @property {QAction} onActionChooseDeformTool()\n * @property {QAction} onActionChoosePerspectiveTool()\n * @property {QAction} onActionChooseEnvelopeTool()\n * @property {QAction} onActionChooseCutterTool()\n * @property {QAction} onActionChooseMorphTool()\n * @property {QAction} onActionChoosePivotTool()\n * @property {QAction} onActionChooseBrushTool()\n * @property {QAction} onActionChooseRepositionAllDrawingsTool()\n * @property {QAction} onActionChooseEraserTool()\n * @property {QAction} onActionChooseRepaintBrushTool()\n * @property {QAction} onActionChoosePencilTool()\n * @property {QAction} onActionChoosePencilEditorTool()\n * @property {QAction} onActionChooseLineTool()\n * @property {QAction} onActionChoosePolylineTool()\n * @property {QAction} onActionChooseRectangleTool()\n * @property {QAction} onActionChooseEllipseTool()\n * @property {QAction} onActionChoosePaintTool()\n * @property {QAction} onActionChooseInkTool()\n * @property {QAction} onActionChoosePaintUnpaintedTool()\n * @property {QAction} onActionChooseRepaintTool()\n * @property {QAction} onActionChooseStampTool()\n * @property {QAction} onActionChooseStrokeTool()\n * @property {QAction} onActionChooseCloseGapTool()\n * @property {QAction} onActionChooseUnpaintTool()\n * @property {QAction} onActionChooseDropperTool()\n * @property {QAction} onActionChooseEditTransformTool()\n * @property {QAction} onActionChooseGrabberTool()\n * @property {QAction} onActionChooseZoomTool()\n * @property {QAction} onActionChooseRotateTool()\n * @property {QAction} onActionChooseThirdPersonNavigation3dTool()\n * @property {QAction} onActionChooseFirstPersonNavigation3dTool()\n * @property {QAction} onActionChooseShiftAndTraceTool()\n * @property {QAction} onActionChooseNoTool()\n * @property {QAction} onActionChooseResizePenStyleTool()\n * @property {QAction} onActionChooseSpSelectTool()\n * @property {QAction} onActionChooseSpTranslateTool()\n * @property {QAction} onActionChooseSpRotateTool()\n * @property {QAction} onActionChooseSpScaleTool()\n * @property {QAction} onActionChooseSpSkewTool()\n * @property {QAction} onActionChooseSpMaintainSizeTool()\n * @property {QAction} onActionChooseSpTransformTool()\n * @property {QAction} onActionChooseSpInverseKinematicsTool()\n * @property {QAction} onActionChooseSpOffsetZTool()\n * @property {QAction} onActionChooseSpSplineOffsetTool()\n * @property {QAction} onActionChooseSpSmoothEditingTool()\n * @property {QAction} onActionChooseMoveBackgroundTool()\n * @property {QAction} onActionChooseTextTool()\n * @property {QAction} onActionActivatePreset(int)\n * @property {QAction} onActionToggleKeyframeMode()\n * @property {QAction} onActionAnimatedKeyframeMode()\n * @property {QAction} onActionAnimatedOnRangeKeyframeMode()\n * @property {QAction} onActionStaticKeyframeMode()\n * @property {QAction} onActionSetKeyframeMode()\n * @property {QAction} onActionSetAllKeyframesMode()\n * @property {QAction} onActionSetConstantSegMode()\n * @property {QAction} onActionMainPlay()\n * @property {QAction} onActionMainPlayFw()\n * @property {QAction} onActionMainPlayBw()\n * @property {QAction} onActionMainPlayPreviewFw()\n * @property {QAction} onActionMainPlayPreviewSwf()\n * @property {QAction} onActionMainStopPlaying()\n * @property {QAction} onActionMainToggleLoopPlay()\n * @property {QAction} onActionMainToggleEnableCacheForPlay()\n * @property {QAction} onActionMainToggleEnableSoundForPlay()\n * @property {QAction} onActionMainToggleEnableSoundScrubbing()\n * @property {QAction} onActionChooseSpRepositionTool()\n * @property {QAction} onActionMainSetPlaybackStartFrame()\n * @property {QAction} onActionMainSetPlaybackStopFrame()\n * @property {QAction} onActionMainGotoFrame()\n * @property {QAction} onActionMainSetPlaybackSpeed()\n * @property {QAction} onActionMainGotoFirstFrame()\n * @property {QAction} onActionMainGotoPreviousFrame()\n * @property {QAction} onActionMainGotoLastFrame()\n * @property {QAction} onActionMainGotoNextFrame()\n * @property {QAction} onActionToggleSideViewPlayback()\n * @property {QAction} onActionToggleTopViewPlayback()\n * @property {QAction} onActionTogglePersViewPlayback()\n * @property {QAction} onActionReshapeMultipleKeyframes()\n * @property {QAction} onActionJogForward()\n * @property {QAction} onActionJogBackward()\n * @property {QAction} onActionShuttleForward()\n * @property {QAction} onActionShuttleBackward()\n * @property {QAction} onActionShuttleReset()\n * @property {QAction} onActionConformationImport()\n * @property {QAction} onActionShowPreferenceDialog()\n * @property {QAction} onActionShowShortcutsDialog()\n * @property {QAction} onActionManageLocalCaches()\n * @property {QAction} onActionReadChangedDrawings()\n * @property {QAction} onActionReadChangedDrawingsNoWarning()\n * @property {QAction} onActionPaletteOperations()\n * @property {QAction} onActionToggleDebugMode()\n * @property {QAction} onActionHelp()\n * @property {QAction} onActionHtmlHelp(QString)\n * @property {QAction} onActionOpenBook(QString)\n * @property {QAction} onActionAbout()\n * @property {QAction} onActionCEIP()\n * @property {QAction} onActionShowLicense()\n * @property {QAction} onActionShowReadme()\n * @property {QAction} onActionOpenURL(QString,int)\n * @property {QAction} onActionOpenURL(QString)\n * @property {QAction} onActionTerminate()\n * @property {QAction} onActionToggleGoogleAnalytics()\n */\n\n/**\n * Actions available in the scriptResponder responder.\n * @name actions#scriptResponder\n * @property {QAction} onActionExecuteScript(QString)\n * @property {QAction} onActionExecuteScriptWithValidator(QString,AC_ActionInfo*)\n * @property {QAction} onActionActivateTool(int)\n * @property {QAction} onActionActivateToolByName(QString)\n */\n\n/**\n * Actions available in the selectionResponder responder.\n * @name actions#selectionResponder\n */\n\n/**\n * Actions available in the sessionResponder responder.\n * @name actions#sessionResponder\n * @property {QAction} onActionFixSymbolCompositeAndDisplay()\n */\n\n/**\n * Actions available in the timelineView responder.\n * @name actions#timelineView\n * @property {QAction} onActionPropagateLayerSelection()\n */\n\n/**\n * Actions available in the toolProperties responder.\n * @name actions#toolProperties\n * @property {QAction} onActionNewBrush()\n * @property {QAction} onActionDeleteBrush()\n * @property {QAction} onActionRenameBrush()\n * @property {QAction} onActionEditBrush()\n * @property {QAction} onActionImportBrushes()\n * @property {QAction} onActionExportBrushes()\n * @property {QAction} onActionShowSmallThumbnail()\n * @property {QAction} onActionShowLargeThumbnail()\n * @property {QAction} onActionShowStroke()\n */\n\n/**\n * Actions available in the toolPropertiesView responder.\n * @name actions#toolPropertiesView\n */\n\n/**\n * Actions available in the xsheetView responder.\n * @name actions#xsheetView\n * @property {QAction} onActionPrintXsheet()\n * @property {QAction} onActionResetCellsSize()\n * @property {QAction} onActionZoomIn()\n * @property {QAction} onActionZoomOut()\n * @property {QAction} onActionZoomExtents()\n * @property {QAction} onActionResetZoom()\n * @property {QAction} onActionResetPan()\n * @property {QAction} onActionResetView()\n * @property {QAction} onActionShowUnhideObjectsEditor()\n * @property {QAction} onActionUnhideAllColumns()\n * @property {QAction} onActionXsheetHoldValueMenu(int)\n * @property {QAction} onActionSendToFunctionView()\n * @property {QAction} onActionToggleShowGrouping()\n * @property {QAction} onActionToggleMinimalHeaders()\n * @property {QAction} onActionDisplayShowDlg()\n * @property {QAction} onActionToggleInsertMode()\n * @property {QAction} onActionToggleGesturalDrag()\n * @property {QAction} onActionToggleThumbnails()\n * @property {QAction} onActionToggleIsShowDrawingCols()\n * @property {QAction} onActionToggleIsShowFunctionCols()\n * @property {QAction} onActionToggleIsShowPath3dCols()\n * @property {QAction} onActionToggleIsShow3dRotationCols()\n * @property {QAction} onActionToggleIsShowSoundCols()\n * @property {QAction} onActionToggleSidePanel()\n * @property {QAction} onActionXsheetHeldFramesLine(int)\n * @property {QAction} onActionXsheetEmptyCellsX(int)\n * @property {QAction} onActionXsheetLabelsFrames(int)\n * @property {QAction} onActionSelectedElementSwapToNextDrawing()\n * @property {QAction} onActionSelectedElementSwapToPrevDrawing()\n * @property {QAction} onActionToggleSelection()\n * @property {QAction} onActionTag()\n * @property {QAction} onActionUntag()\n * @property {QAction} onActionUntagAllOthers()\n * @property {QAction} onActionTagPublic()\n * @property {QAction} onActionUntagPublic()\n * @property {QAction} onActionUntagPublicAllOthers()\n */"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_application.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oApp class            //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * The constructor for the $.oApp class\n * @classdesc\n * The $.oApp class provides access to the Harmony application and its widgets.\n * @constructor\n */\n$.oApp = function(){\n}\n\n\n/**\n * The Harmony version number\n * @name $.oApp#version\n * @type {int}\n * @readonly\n */\nObject.defineProperty($.oApp.prototype, 'version', {\n  get : function(){\n    return parseInt(about.getVersionInfoStr().split(\"version\").pop().split(\".\")[0], 10);\n  }\n});\n\n\n/**\n * The software flavour: Premium, Advanced, Essential\n * @name $.oApp#flavour\n * @type {string}\n * @readonly\n */\nObject.defineProperty($.oApp.prototype, 'flavour', {\n  get : function(){\n    return about.getFlavorString();\n  }\n});\n\n\n/**\n * The Harmony Main Window.\n * @name $.oApp#mainWindow\n * @type {QWidget}\n * @readonly\n */\nObject.defineProperty($.oApp.prototype, 'mainWindow', {\n  get : function(){\n    var windows = QApplication.topLevelWidgets();\n    for ( var i in windows) {\n      if (windows[i] instanceof QMainWindow && !windows[i].parentWidget()) return windows[i];\n    }\n    return false\n  }\n});\n\n\n/**\n * The Harmony UI Toolbars.\n * @name $.oApp#toolbars\n * @type {QToolbar}\n * @readonly\n */\nObject.defineProperty($.oApp.prototype, 'toolbars', {\n  get : function(){\n    var widgets = QApplication.allWidgets();\n    var _toolbars = widgets.filter(function(x){return x instanceof QToolBar})\n\n    return _toolbars\n  }\n});\n\n\n\n/**\n * The Position of the mouse cursor in the toonboom window coordinates.\n * @name $.oApp#mousePosition\n * @type {$.oPoint}\n * @readonly\n */\nObject.defineProperty($.oApp.prototype, 'mousePosition', {\n  get : function(){\n    var _position = this.$.app.mainWindow.mapFromGlobal(QCursor.pos());\n    return new this.$.oPoint(_position.x(), _position.y(), 0);\n  }\n});\n\n\n/**\n * The Position of the mouse cursor in the screen coordinates.\n * @name $.oApp#globalMousePosition\n * @type {$.oPoint}\n * @readonly\n */\nObject.defineProperty($.oApp.prototype, 'globalMousePosition', {\n  get : function(){\n    var _position = QCursor.pos();\n    return new this.$.oPoint(_position.x(), _position.y(), 0);\n  }\n});\n\n\n/**\n * Access the tools available in the application\n * @name $.oApp#tools\n * @type {$.oTool[]}\n * @readonly\n * @example\n * // Access the list of currently existing tools by using the $.app object\n * var tools = $.app.tools;\n *\n * // output the list of tools names and ids\n * for (var i in tools){\n *   log(i+\" \"+tools[i].name)\n * }\n *\n * // To get a tool by name, use the $.app.getToolByName() function\n * var brushTool = $.app.getToolByName(\"Brush\");\n * log (brushTool.name+\" \"+brushTool.id)            // Output: Brush 9\n *\n * // it's also possible to activate a tool in several ways:\n * $.app.currentTool = 9;         // using the tool \"id\"\n * $.app.currentTool = brushTool  // by passing a oTool object\n * $.app.currentTool = \"Brush\"    // using the tool name\n *\n * brushTool.activate()           // by using the activate function of the oTool class\n */\nObject.defineProperty($.oApp.prototype, 'tools', {\n  get: function(){\n    if (typeof this._toolsObject === 'undefined'){\n      this._toolsObject = [];\n      var _currentTool = this.currentTool;\n      var i = 0;\n      Tools.setToolSettings({currentTool:{id:i}})\n      while(Tools.getToolSettings().currentTool.name){\n        var tool = Tools.getToolSettings().currentTool;\n        this._toolsObject.push(new this.$.oTool(tool.id,tool.name));\n        i++;\n        Tools.setToolSettings({currentTool:{id:i}});\n      }\n      this.currentTool = _currentTool;\n    }\n    return this._toolsObject;\n  }\n})\n\n\n/**\n * The Position of the mouse cursor in the screen coordinates.\n * @name $.oApp#currentTool\n * @type {$.oTool}\n */\nObject.defineProperty($.oApp.prototype, 'currentTool', {\n  get : function(){\n    var _tool = Tools.getToolSettings().currentTool.id;\n    return _tool;\n  },\n  set : function(tool){\n    if (tool instanceof this.$.oTool) {\n      tool.activate();\n      return\n    }\n    if (typeof tool == \"string\"){\n      try{\n        this.getToolByName(tool).activate();\n        return\n      }catch(err){\n        this.$.debug(\"'\"+ tool + \"' is not a valid tool name. Valid: \"+this.tools.map(function(x){return x.name}).join(\", \"))\n      }\n    }\n    if (typeof tool == \"number\"){\n      this.tools[tool].activate();\n      return\n    }\n  }\n});\n\n\n/**\n * Gets access to a widget from the Harmony Interface.\n * @param   {string}   name              The name of the widget to look for.\n * @param   {string}   [parentName]      The name of the parent widget to look into, in case of duplicates.\n * @return  {QWidget}   The widget if found, or null if it doesn't exist.\n */\n$.oApp.prototype.getWidgetByName = function(name, parentName){\n  var widgets = QApplication.allWidgets();\n  for( var i in widgets){\n    if (widgets[i].objectName == name){\n      if (typeof parentName !== 'undefined' && (widgets[i].parentWidget().objectName != parentName)) continue;\n      return widgets[i];\n    }\n  }\n  return null;\n}\n\n\n/**\n * Access the Harmony Preferences\n * @name $.oApp#preferences\n * @example\n * // To access the preferences of Harmony, grab the preference object in the $.oApp class:\n * var prefs = $.app.preferences;\n *\n * // It's then possible to access all available preferences of the software:\n * for (var i in prefs){\n *   log (i+\" \"+prefs[i]);\n * }\n *\n * // accessing the preference value can be done directly by using the dot notation:\n * prefs.USE_OVERLAY_UNDERLAY_ART = true;\n * log (prefs.USE_OVERLAY_UNDERLAY_ART);\n *\n * //the details objects of the preferences object allows access to more information about each preference\n * var details = prefs.details\n * log(details.USE_OVERLAY_UNDERLAY_ART.category+\" \"+details.USE_OVERLAY_UNDERLAY_ART.id+\" \"+details.USE_OVERLAY_UNDERLAY_ART.type);\n *\n * for (var i in details){\n *   log(i+\" \"+JSON.stringify(details[i]))       // each object inside detail is a complete oPreference instance\n * }\n *\n * // the preference object also holds a categories array with the list of all categories\n * log (prefs.categories)\n */\nObject.defineProperty($.oApp.prototype, 'preferences', {\n  get: function(){\n    if (typeof this._prefsObject === 'undefined'){\n      var _prefsObject = {};\n      _categories = [];\n      _details = {};\n\n      Object.defineProperty(_prefsObject, \"categories\", {\n        enumerable:false,\n        value:_categories\n      })\n\n      Object.defineProperty(_prefsObject, \"details\", {\n        enumerable:false,\n        value:_details\n      })\n\n      var prefFile = (new oFile(specialFolders.resource+\"/prefs.xml\")).parseAsXml().children[0].children;\n\n      var userPrefFile = new oFile(specialFolders.userConfig + \"/Harmony Premium-pref.xml\")\n      // Harmony Pref file is called differently on the database userConfig\n      if (!userPrefFile.exists) userPrefFile = new oFile(specialFolders.userConfig + \"/Harmony-pref.xml\")\n\n      if (userPrefFile.exists){\n        var userPref = {objectName: \"category\", id: \"user\", children:userPrefFile.parseAsXml().children[0].children};\n        prefFile.push(userPref);\n      }\n\n      for (var i in prefFile){\n        if (prefFile[i].objectName != \"category\" || prefFile[i].id == \"Storyboard\") continue;\n        var category = prefFile[i].id;\n        if (_categories.indexOf(category) == -1) _categories.push(category);\n\n        var preferences = prefFile[i].children;\n\n        // create a oPreference instance for each found preference and add a getter setter to the $.oApp._prefsObject\n        for (var j in preferences){\n\n          // evaluate condition for conditional preferences. For now only support Harmony Premium prefs\n          if (preferences[j].objectName == \"if\"){\n            var condition = preferences[j].condition;\n            var regex = /(not essentials|not sboard|not paint)/\n            if (regex.exec(condition)) preferences = preferences.concat(preferences[j].children)\n            continue;\n          }\n          var type = preferences[j].objectName;\n          var keyword = preferences[j].id;\n          var description = preferences[j].shortDesc;\n          var descriptionText = preferences[j].longDesc;\n          if (type == \"color\"){\n            if (typeof preferences[j].alpha === 'undefined') preferences[j].alpha = 255;\n            var value = new ColorRGBA(preferences[j].red, preferences[j].green, preferences[j].blue, preferences[j].alpha)\n          }else{\n            var value = preferences[j].value;\n          }\n          // var docString = (category+\" \"+keyword+\" \"+type+\" \"+description)\n          //\n          var pref = this.$.oPreference.createPreference(category, keyword, type, value, description, descriptionText, _prefsObject);\n          _details[pref.keyword] = pref;\n        }\n      }\n\n      this._prefsObject = _prefsObject;\n    }\n\n    return this._prefsObject;\n  }\n})\n\n\n\n/**\n * The list of stencils available in the Harmony UI.\n * @name $.oApp#stencils\n * @type {$.oStencil[]}\n * @example\n * // Access the stencils list through the $.app object.\n * var stencils = $.app.stencils\n *\n * // list all the properties of stencils\n * for (var i in stencils){\n *   log(\" ---- \"+stencils[i].type+\" \"+stencils[i].name+\" ---- \")\n *   for(var j in stencils[i]){\n *     log (j);\n *   }\n * }\n */\nObject.defineProperty($.oApp.prototype, 'stencils', {\n  get: function(){\n    if (typeof this._stencilsObject === 'undefined'){\n      // parse stencil xml file penstyles.xml to get stencils info\n      var stencilsFile = (new oFile(specialFolders.userConfig+\"/penstyles.xml\")).read();\n      var penRegex = /<pen>([\\S\\s]*?)<\\/pen>/igm\n      var stencils = [];\n      var stencilXml;\n      while(stencilXml = penRegex.exec(stencilsFile)){\n        var stencilObject = this.$.oStencil.getFromXml(stencilXml[1]);\n        stencils.push(stencilObject);\n      }\n      this._stencilsObject = stencils;\n    }\n    return this._stencilsObject;\n  }\n})\n\n\n\n/**\n * The currently selected stencil. Always returns the pencil tool current stencil.\n * @name $.oApp#currentStencil\n * @type {$.oStencil}\n */\nObject.defineProperty($.oApp.prototype, 'currentStencil', {\n  get: function(){\n    return this.stencils[PaletteManager.getCurrentPenstyleIndex()];\n  },\n  set: function(stencil){\n    if (stencil instanceof this.$.oStencil) var stencil = stencil.name\n    this.$.debug(\"Setting current pen: \"+ stencil)\n    PenstyleManager.setCurrentPenstyleByName(stencil);\n  }\n})\n\n\n\n// $.oApp Class Methods\n\n/**\n * get a tool by its name\n * @return {$.oTool}   a oTool object representing the tool, or null if not found.\n */\n$.oApp.prototype.getToolByName = function(toolName){\n  var _tools  = this.tools;\n  for (var i in _tools){\n    if (_tools[i].name.toLowerCase() == toolName.toLowerCase()) return _tools[i];\n  }\n  return null;\n}\n\n\n/**\n * returns the list of stencils usable by the specified tool\n * @param {$.oTool}     tool      the tool object we want valid stencils for\n * @return {$.oStencil[]}    the list of stencils compatible with the specified tool\n */\n$.oApp.prototype.getValidStencils = function (tool){\n  if (typeof tool === 'undefined') var tool = this.currentTool;\n  return tool.stencils;\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oToolbar class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * The $.oToolbar constructor.\n * @name        $.oToolbar\n * @constructor\n * @classdesc   A toolbar that can contain any type of widgets.\n * @param       {string}     name              The name of the toolbar to create.\n * @param       {QWidget[]} [widgets]          The list of widgets to add to the toolbar.\n * @param       {QWidget}   [parent]           The parent widget to add the toolbar to.\n * @param       {bool}      [show]             Whether to show the toolbar instantly after creation.\n */\n$.oToolbar = function( name, widgets, parent, show ){\n  if (typeof parent === 'undefined') var parent = $.app.mainWindow;\n  if (typeof widgets === 'undefined') var widgets = [];\n  if (typeof show === 'undefined') var show = true;\n\n  this.name = name;\n  this._widgets = widgets;\n  this._parent = parent;\n\n  if (show) this.show();\n}\n\n\n/**\n * Shows the oToolbar.\n * @name    $.oToolbar#show\n */\n$.oToolbar.prototype.show = function(){\n  if (this.$.batchMode) {\n    this.$.debug(\"$.oToolbar not supported in batch mode\", this.$.DEBUG_LEVEL.ERROR)\n    return;\n  }\n\n  var _parent = this._parent;\n  var _toolbar = new QToolbar();\n  _toolbar.objectName = this.name;\n\n  for (var i in this.widgets){\n    _toolbar.addWidget(this.widgets[i]);\n  }\n\n  _parent.addToolbar(_toolbar);\n  this.toolbar = _toolbar;\n\n  return this.toolbar;\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_attribute.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library v0.01\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney...\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the MIT license.\n//   https://opensource.org/licenses/mit\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oAttribute class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * The constructor for the $.oAttribute class.\n * @classdesc\n * The $.oAttribute class holds the smart version of the parameter you can find in layer property.<br>\n * It is used internally to get and set values and link a oColumn to a parameter in order to animate it. (Users should never have to instantiate this class) <br>\n * For a list of attributes existing in each node type and their type, as well as examples of the values they can hold, refer to :<br>\n * {@link NodeType}.\n * @constructor\n * @param   {$.oNode}                  oNodeObject                The oNodeObject that the attribute is associated to.\n * @param   {attr}                     attributeObject            The internal harmony Attribute Object.\n * @param   {$.oAttribute}             parentAttribute            The parent attribute of the subattribute.\n *\n * @property {$.oNode}                 node                       The oNode this attribute belongs to.\n * @property {attr}                    attributeObject            The internal harmony Attribute Object.\n * @property {string}                  keyword                    The keyword describing this attribute. (always in lower case)\n * @property {string}                  shortKeyword               The full keyword describing this attribute, including parent attributes separated with a \".\" (always in lower case)\n * @property {$.oAttribute}            parentAttribute            The parent oAttribute object\n * @property {$.oAttribute[]}          subAttributes              The subattributes of this attribute.\n * @example\n * // oAttribute objects can be grabbed from the node .attributes object with dot notation, by calling the attribute keyword in lowercase.\n *\n * var myNode = $.scn.getSelectedNodes()[0];          // grab the first selected node\n * var Xattribute = myNode.attributes.position.x;     // gets the position.x attribute of the node if it has it (for example, PEG nodes have it)\n *\n * var Xcolumn = Xattribute.column;                   // retrieve the linked column to the element (The object that holds the animation)\n *\n * Xattribute.setValue(5, 5);                         // sets the value to 5 at frame 5\n *\n * // attribute values can also be set directly on the node when not animated:\n * myNode.position.x = 5;\n *\n */\n$.oAttribute = function( oNodeObject, attributeObject, parentAttribute ){\n  this._type = \"attribute\";\n\n  this.node = oNodeObject;\n  this.attributeObject = attributeObject;\n\n  this._shortKeyword = attributeObject.keyword();\n\n  if( attributeObject.fullKeyword ){\n    this._keyword = attributeObject.fullKeyword();\n  }else{\n    this._keyword = (parentAttribute?(parentAttribute._keyword+\".\"):\"\") + this._shortKeyword;\n  }\n\n  this.parentAttribute = parentAttribute; // only for subAttributes\n\n  // recursively add all subattributes as properties on the object\n  this.createSubAttributes(attributeObject);\n}\n\n\n/**\n * Private function to create subAttributes in an oAttribute object at initialisation.\n * @private\n * @return  {void}   Nothing returned.\n */\n$.oAttribute.prototype.createSubAttributes = function (attributeObject){\n  var _subAttributes = [];\n\n  // if harmony version supports getSubAttributes\n  var _subAttributesList = [];\n  if (attributeObject.getSubAttributes){\n    _subAttributesList = attributeObject.getSubAttributes();\n  }else{\n    var sub_attrs = node.getAttrList( this.node.path, 1, this._keyword );\n\n    if( sub_attrs && sub_attrs.length>0 ){\n      _subAttributesList = sub_attrs;\n    }\n  }\n\n  for (var i in _subAttributesList){\n    var _subAttribute = new this.$.oAttribute( this.node, _subAttributesList[i], this );\n    var _keyword = _subAttribute.shortKeyword;\n\n    // creating a property on the attribute object with the subattribute name to access it\n    this[_keyword] = _subAttribute;\n    _subAttributes.push(_subAttribute)\n  }\n\n  // subAttributes is made available as an array for more formal access\n  this.subAttributes = _subAttributes;\n}\n\n\n/**\n * Private function to add utility to subattributes on older versions of Harmony.\n * @private\n * @deprecated\n * @return  {void}   Nothing returned.\n */\n$.oAttribute.prototype.getSubAttributes_oldVersion = function (){\n  var sub_attrs = [];\n\n  switch( this.type ){\n      case \"POSITION_3D\" :\n        //hard coded subAttr handler for POSITION_3D in older versions of Harmony.\n        sub_attrs = [ 'SEPARATE', 'X', 'Y', 'Z'];\n        break\n      case \"ROTATION_3D\" :\n        sub_attrs = [ 'SEPARATE', 'ANGLEX', 'ANGLEY', 'ANGLEZ', \"QUATERNIONPATH\" ];\n        break\n      case \"SCALE_3D\" :\n        sub_attrs = [ 'SEPARATE', 'IN_FIELDS', 'XY', 'X', 'Y', 'Z' ];\n        break\n      case \"DRAWING\" :\n        sub_attrs = [ 'ELEMENT', 'ELEMENT_MODE', 'CUSTOM_NAME'];\n        break\n      case \"ELEMENT\" :\n        sub_attrs = [ 'LAYER' ]\n        break\n      case \"CUSTOM_NAME\" :\n        sub_attrs = [ 'NAME', 'TIMING', 'EXTENSION', 'FIELD_CHART' ]\n      default:\n        break\n  }\n\n  var _node = this.node.path;\n  var _keyword = this._keyword;\n\n  sub_attrs = sub_attrs.map(function(x){return node.getAttr( _node, 1, _keyword+\".\"+x )})\n\n  return sub_attrs;\n}\n\n\n/**\n * The display name of the attribute\n * @name $.oAttribute#name\n * @type {string}\n */\nObject.defineProperty($.oAttribute.prototype, 'name', {\n  get: function(){\n    return this.attributeObject.name();\n  }\n})\n\n/**\n * The full keyword of the attribute.\n * @name $.oAttribute#keyword\n * @type {string}\n */\nObject.defineProperty($.oAttribute.prototype, 'keyword', {\n    get : function(){\n        // formatting the keyword for our purposes\n        // hard coding a fix for 3DPath attribute name which starts with a number\n        var _keyword = this._keyword.toLowerCase();\n        if (_keyword == \"3dpath\") _keyword = \"path3d\";\n        return _keyword;\n    }\n});\n\n\n/**\n * The part of the attribute's keyword that is after the \".\" for subAttributes.\n * @name $.oAttribute#shortKeyword\n * @type {string}\n */\nObject.defineProperty($.oAttribute.prototype, 'shortKeyword', {\n    get : function(){\n        // formatting the keyword for our purposes\n        // hard coding a fix for 3DPath attribute name which starts with a number\n        var _keyword = this._shortKeyword.toLowerCase();\n        if (_keyword == \"3dpath\") _keyword = \"path3d\";\n        return _keyword;\n    }\n});\n\n\n/**\n * The type of the attribute.\n * @name $.oAttribute#type\n * @type {string}\n */\nObject.defineProperty($.oAttribute.prototype, 'type', {\n    get : function(){\n        return this.attributeObject.typeName();\n    }\n});\n\n/**\n * The column attached to the attribute.\n * @name $.oAttribute#column\n * @type {$.oColumn}\n * @example\n// link a new column to an attribute by setting this value:\nvar myColumn = $.scn.addColumn(\"BEZIER\");\nmyNode.attributes.position.x.column = myColumn; // values contained in \"myColumn\" now define the animation of our peg's x position\n\n// to automatically create a column and link it to the attribute, use:\nmyNode.attributes.position.x.addColumn(); // if the column exist already, it will just be returned.\n\n// to unlink a column, just set it to null/undefined:\nmyNode.attributes.position.x.column = null; // values are no longer animated.\n */\nObject.defineProperty($.oAttribute.prototype, 'column', {\n  get : function(){\n    var _column = node.linkedColumn ( this.node.path, this._keyword );\n    if( _column && _column.length ){\n      return this.node.scene.$column( _column, this );\n    }else{\n      return null;\n    }\n  },\n\n  set : function(columnObject){\n    // unlink if provided with null value or empty string\n    if (!columnObject){\n      node.unlinkAttr(this.node.path, this._keyword);\n    }else{\n      node.linkAttr(this.node.path, this._keyword, columnObject.uniqueName);\n      columnObject.attributeObject = this;\n      // TODO: transfer current value of attribute to a first key on the column if column is empty\n    }\n  }\n});\n\n\n /**\n * The frames array holding the values of the animation. Starts at 1, as array indexes correspond to frame numbers.\n * @name $.oAttribute#frames\n * @type {$.oFrame[]}\n */\nObject.defineProperty($.oAttribute.prototype, 'frames', {\n    get : function(){\n         var _column = this.column\n         if (_column != null){\n            return _column.frames;\n         }else{\n          //Need a method to get frames of non-column values. Local Values.\n            return [ new this.$.oFrame( 1, this, false ) ];\n         }\n    },\n\n    set : function(){\n      throw \"Not implemented.\"\n    }\n});\n\n\n/**\n * An array of only the keyframes (frames with a set value) of the animation.\n * @name $.oAttribute#keyframes\n * @type {$.oFrame[]}\n */\n// MCNote: I would prefer if this could remain getKeyFrames()\nObject.defineProperty($.oAttribute.prototype, 'keyframes', {\n    get : function(){\n      var col     = this.column;\n      var frames  = this.frames;\n\n      if( !col ){\n        return frames[1];\n      }\n\n      return this.column.keyframes;\n    },\n\n    set : function(){\n      throw \"Not implemented.\"\n    }\n});\n\n/**\n * WIP.\n * @name $.oAttribute#useSeparate\n * @type {bool}\n * @private\n */\n//CF Note: Not sure if this should be a general attribute, or a subattribute.\nObject.defineProperty($.oAttribute.prototype, \"useSeparate\", {\n    get : function(){\n        // TODO\n        throw new Error(\"not yet implemented\");\n    },\n\n    set : function( _value ){\n        // TODO: when swapping from one to the other, copy key values and link new columns if missing\n        throw new Error(\"not yet implemented\");\n    }\n});\n\n\n/**\n * Returns the default value of the attribute for most keywords\n * @name $.oAttribute#defaultValue\n * @type {bool}\n * @todo switch the implementation to types?\n * @example\n * // to reset an attribute to its default value:\n * // (mostly used for position/angle/skew parameters of pegs and drawing nodes)\n * var myAttribute = $.scn.nodes[0].attributes.position.x;\n *\n * myAttribute.setValue(myAttribute.defaultValue);\n */\nObject.defineProperty($.oAttribute.prototype, \"defaultValue\", {\n    get : function(){\n        // TODO: we could use this to reset bones/deformers to their rest states\n        var _keyword = this._keyword;\n\n        switch (_keyword){\n            case \"OFFSET.X\" :\n            case \"OFFSET.Y\" :\n            case \"OFFSET.Z\" :\n\n            case \"POSITION.X\" :\n            case \"POSITION.Y\" :\n            case \"POSITION.Z\" :\n\n            case \"PIVOT.X\":\n            case \"PIVOT.Y\":\n            case \"PIVOT.Z\":\n\n            case \"ROTATION.ANGLEX\":\n            case \"ROTATION.ANGLEY\":\n            case \"ROTATION.ANGLEZ\":\n\n            case \"ANGLE\":\n            case \"SKEW\":\n\n            case \"SPLINE_OFFSET.X\":\n            case \"SPLINE_OFFSET.Y\":\n            case \"SPLINE_OFFSET.Z\":\n\n                return 0;\n\n            case \"SCALE.X\" :\n            case \"SCALE.Y\" :\n            case \"SCALE.Z\" :\n                return 1;\n\n            case \"OPACITY\" :\n                return 100;\n\n            case \"COLOR\" :\n                return new this.$.oColorValue();\n\n            case \"OFFSET.3DPATH\":\n                // pseudo oPathPoint\n                // CFNote: is this supposed to be an object?\n                // this is a fake object value that can be easily checked with a \"==\" operator.\n                // oPathPoint will be converted to string for checking, and have the same format.\n                // I made this to check if the value is default but I guess it's not ideal for assigning a default value, so maybe we should change it.\n                return \"{x:0, y:0, z:0}\";\n\n            default:\n                return null; // for attributes that don't have a default value, we return null\n        }\n    }\n});\n\n\n// $.oAttribute Class methods\n\n/**\n * Provides the keyframes of the attribute.\n * @return {$.oFrame[]}   The filtered keyframes.\n */\n$.oAttribute.prototype.getKeyframes = function(){\n    var _frames = this.frames;\n    _frames = _frames.filter(function(x){return x.isKeyframe});\n    return _frames;\n}\n\n\n/**\n * Provides the keyframes of the attribute.\n * @return {$.oFrame[]}   The filtered keyframes.\n * @deprecated For case consistency, keyframe will never have a capital F\n */\n$.oAttribute.prototype.getKeyFrames = function(){\n    this.$.debug(\"oAttribute.getKeyFrames is deprecated. Use oAttribute.getKeyframes instead.\", this.$.DEBUG_LEVEL.ERROR);\n    var _frames = this.frames;\n    _frames = _frames.filter(function(x){return x.isKeyframe});\n    return _frames;\n}\n\n\n/**\n * Recursively get all the columns linked to the attribute and its subattributes\n * @return {$.oColumn[]}    the list of columns linked to the subattributes\n */\n$.oAttribute.prototype.getLinkedColumns = function(){\n  var _columns = [];\n  var _subAttributes = this.subAttributes;\n  var _ownColumn = this.column;\n  if (_ownColumn != null) _columns.push(_ownColumn);\n\n  for (var i=0; i<_subAttributes.length; i++) {\n    _columns = _columns.concat(_subAttributes[i].getLinkedColumns());\n  }\n\n  return _columns;\n}\n\n\n/**\n * Recursively sets an attribute to the same value as another. Both must have the same keyword.\n * @param {bool}    [duplicateColumns=false]      In the case that the attribute has a column, whether to duplicate the column before linking\n * @private\n */\n$.oAttribute.prototype.setToAttributeValue = function(attributeToCopy, duplicateColumns){\n  if (typeof duplicateColumns === 'undefined') var duplicateColumns = false;\n\n  if (this.keyword !== attributeToCopy.keyword) return;\n  var _subAttributes = this.subAttributes;\n\n  var _column = attributeToCopy.column;\n  if (_column == null) {\n    var value = attributeToCopy.getValue();\n    this.setValue(value);\n  }else{\n    if (duplicateColumns) var _column = _column.duplicate(this);\n    this.column = _column;\n  }\n\n  var _subAttributesToCopy = attributeToCopy.subAttributes;\n  for (var i=0; i<_subAttributes.length; i++){\n    _subAttributes[i].setToAttributeValue(_subAttributesToCopy[i], duplicateColumns);\n  }\n}\n\n\n//CFNote: Is it worth having a getValueType?\n/**\n * Gets the value of the attribute at the given frame.\n * @param   {int}        frame                 The frame at which to set the value, if not set, assumes 1\n *\n * @return {object}      The value of the attribute in the native format of that attribute (contextual to the attribute).\n */\n$.oAttribute.prototype.getValue = function (frame) {\n    if (typeof frame === 'undefined') var frame = 1;\n    this.$.debug('getting value of frame :'+frame+' of attribute: '+this._keyword+' of node '+this.node+' - type '+this.type, this.$.DEBUG_LEVEL.LOG)\n\n    var _attr = this.attributeObject;\n    var _type = this.type;\n    var _value;\n    var _column = this.column;\n\n    // handling conversion of all return types into our own types\n    switch (_type){\n        case 'BOOL':\n            _value = _attr.boolValueAt(frame)\n            break;\n\n        case 'INT':\n            _value = _attr.intValueAt(frame)\n            break;\n\n        case 'DOUBLE':\n        case 'DOUBLEVB':\n            _value = _attr.doubleValueAt(frame)\n            break;\n\n        case 'STRING':\n            _value = _attr.textValueAt(frame)\n            break;\n\n        case 'COLOR':\n            _value = new this.$.oColorValue(_attr.colorValueAt(frame))\n            break;\n\n        case 'POSITION_2D':\n            _value = _attr.pos2dValueAt(frame)\n            _value = new this.$.oPoint(_value.x, _value.y)\n            break;\n\n        case 'POSITION_3D':\n            _value = _attr.pos3dValueAt(frame)\n            _value = new this.$.oPoint(_value.x, _value.y, _value.z)\n            break;\n\n        case 'SCALE_3D':\n            _value = _attr.pos3dValueAt(frame)\n            _value = new this.$.oPoint(_value.x, _value.y, _value.z)\n            break;\n\n        case 'PATH_3D':\n            _attr = this.parentAttribute.attributeObject;\n            var _frame = _column?(new this.$.oFrame(frame, _column)):(new this.$.oFrame(frame, _attr));\n            if(_column && _frame.isKeyframe){\n                _value = new this.$.oPathPoint(_column, _frame);\n            }else{\n                _value = _attr.pos3dValueAt(frame);\n            }\n            break;\n\n        /*case 'DRAWING':\n            // override with returning an oElement object\n            this.$.debug( \"DRAWING: \" + this.keyword , this.$.DEBUG_LEVEL.LOG);\n\n            value = _column.element;\n            break;*/\n\n        case 'ELEMENT':\n            // an element always has a column, so we'll fetch it from there\n            _value = column.getEntry(_column.uniqueName, 1, frame);\n\n            // Convert to an instance of oDrawing, with a safety in case of psd import\n            _drawing = _column.element.getDrawingByName(_value);\n            if (_drawing) _value = _drawing;\n            break;\n\n        // TODO: How does QUATERNION_PATH work? subcolumns I imagine\n        // TODO: How to get types SCALE_3D, ROTATION_3D, DRAWING, GENERIC_ENUM? -> maybe we don't need to, they don't have intrinsic values\n\n        default:\n            // enums, etc\n            _value = _attr.textValueAt(frame);\n\n            // in case of subattributes, create a fake string that can have properties so we can create getter setters on it for its subattrs\n            if ( _attr.hasSubAttributes && _attr.hasSubAttributes() ){\n                _value = { value:_value };\n                _value.toString = function(){ return this.value };\n            }else{\n              var sub_attrs = node.getAttrList( this.node.path, 1, this._keyword );\n              if( sub_attrs && sub_attrs.length>0 ){\n                _value = { value:_value };\n                _value.toString = function(){ return this.value };\n              }\n            }\n    }\n\n    return _value;\n}\n\n\n/**\n * Sets the value of the attribute at the given frame.\n * @param   {string}     value        The value to set on the attribute.\n * @param   {int}        [frame=1]    The frame at which to set the value, if not set, assumes 1\n */\n$.oAttribute.prototype.setValue = function (value, frame) {\n    var _attr = this.attributeObject;\n    var _column = this.column;\n    var _type = this.type;\n    var _animate = false;\n\n    if (!frame){\n      // we don't animate\n      var frame = 1;\n    }else if (!_column){\n      // generate a new column to be able to animate\n      _column = this.addColumn();\n    }\n\n    if( _column ){\n      _animate = true;\n    }\n\n    try{\n      this.$.debug(\"setting attr \"+this._keyword+\" (type : \"+this.type+\") on node \"+this.node+\" to value \"+JSON.stringify(value)+\" at frame \"+frame, this.$.DEBUG_LEVEL.LOG)\n    }catch(err){\n      this.$.debug(\"setting attr \"+this._keyword+\" at frame \"+frame, this.$.DEBUG_LEVEL.LOG)\n    };\n\n    switch(_type){\n        // TODO: sanitize input\n        case \"COLOR\" :\n            // doesn't work for burnin because it has color.Red, color.green etc and not .r .g ...\n            value = (value instanceof this.$.oColorValue)?value: new this.$.oColorValue(value);\n            value = ColorRGBA(value.r, value.g, value.b, value.a);\n            _animate ? _attr.setValueAt(value, frame) : _attr.setValue(value);\n            break;\n\n        case \"GENERIC_ENUM\" :\n            node.setTextAttr(this.node.path, this._keyword, frame, value);\n            break;\n\n        case \"PATH_3D\" :\n          // check if frame is tied to a column or an attribute\n          var _frame = _column?(new this.$.oFrame(frame, this.column)):(new this.$.oFrame(frame, _attr));\n          if (_column){\n            if (!_frame.isKeyframe) _frame.isKeyframe = true;\n            var _point = new this.$.oPathPoint (this.column, _frame);\n            _point.set(value);\n          }else{\n            // TODO: create keyframe?\n            this.parentAttribute.setValue(value);\n          }\n          break;\n\n        case \"POSITION_2D\":\n            value = Point2d(value.x, value.y);\n            _animate ? _attr.setValueAt(value, frame) : _attr.setValue(value);\n            break;\n\n        case \"POSITION_3D\":\n            value = Point3d(value.x, value.y, value.z);\n            _animate ? _attr.setValueAt(value, frame) : _attr.setValue(value);\n            break;\n\n        case \"ELEMENT\" :\n            _column = this.column;\n            value = (value instanceof this.$.oDrawing) ? value.name : value;\n            column.setEntry(_column.uniqueName, 1, frame, value+\"\");\n            break;\n\n        case \"QUATERNIONPATH\" :\n            // set quaternion paths as textattr until a better way is found\n\n        default :\n            try{\n              _animate ? _attr.setValueAt( value, frame ) : _attr.setValue( value );\n            }catch(err){\n              this.$.debug(\"error setting attr \"+this._keyword+\" value \"+value+\": \"+err, this.$.DEBUG_LEVEL.DEBUG);\n              this.$.debug(\"setting text attr \"+this._keyword+\" value \"+value+\" as textAttr \", this.$.DEBUG_LEVEL.ERROR);\n              node.setTextAttr( this.node.path, this._keyword, frame, value );\n            }\n    }\n}\n\n\n/**\n * Adds a column with a default name, based on the attribute type.\n * If a column already exists, it returns it.\n * @returns {$.oColumn} the created column\n */\n$.oAttribute.prototype.addColumn = function(){\n  var _column = this.column;\n  if (_column) return _column;\n\n  if (this.hasSubAttributes){\n    throw new Error(\"Can't create columns for attribute \"+this.keyword+\", column must be created for its subattributes.\");\n  }\n\n  var _type = this.type;\n  var _columnType = \"\";\n  var _columnName = this.node.name+\": \"+this.name.replace(/\\s/g, \"_\");\n\n  switch(_type){\n    case 'INT':\n    case 'DOUBLE':\n    case 'DOUBLEVB':\n      _columnType = \"BEZIER\";\n      break;\n\n    case \"QUATERNIONPATH\" :\n      _columnName = \"QUARTERNION\";\n      break;\n\n      case \"PATH_3D\" :\n      _columnName = \"3DPATH\";\n      break;\n\n    case \"ELEMENT\" :\n      _columnType = \"DRAWING\";\n      _columnName = this.node.name;\n      break;\n\n    default :\n      throw new Error(\"Can't create columns for attribute \"+this.keyword+\", not supported by attribute type '\"+_type+\"'\");\n  }\n\n  var _column = this.$.scn.addColumn(_columnType, _columnName);\n  this.column = _column;\n\n  if (!this.column) {\n    _column.remove();\n    throw new Error(\"Can't create columns for attribute \"+this.keyword+\", animation not supported.\");\n  }\n\n  return this.column;\n}\n\n\n/**\n * Gets the value of the attribute at the given frame.\n * @param   {int}        frame                 The frame at which to set the value, if not set, assumes 1\n * @deprecated use oAttribute.getValue(frame) instead (see: function names as verbs)\n * @return {object}      The value of the attribute in the native format of that attribute (contextual to the attribute).\n */\n$.oAttribute.prototype.value = function(frame){\n  return this.getValue( frame );\n}\n\n\n/**\n * Represents an oAttribute object in string form\n * @private\n * @returns {string}\n */\n$.oAttribute.prototype.toString = function(){\n  return \"[object $.oAttribute '\"+this.keyword+(this.subAttributes.length?\"' subAttributes: \"+this.subAttributes.map(function(x){return x.shortKeyword}):\"\")+\"]\";\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_backdrop.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oBackdrop class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for  the $.oBackdrop class.\n * @constructor\n * @classdesc  The $.oBackdrop Class represents a backdrop in the node view, and allows users to add, remove and modify existing Backdrops. Accessing these functions is done through the oGroupNode class.\n * @param   {string}                 groupPath                   The path to the object in which this backdrop is placed.\n * @param   {backdropObject}         backdropObject              The harmony-internal backdrop object associated with this oBackdrop.\n * @example\n * function createColoredBackdrop(){\n *  // This script will prompt for a color and create a backdrop around the selection\n *  $.beginUndo()\n *\n *  var doc = $.scn; // grab the scene\n *  var nodes = doc.selectedNodes; // grab the selection\n *\n *  if(!nodes) return    // exit the function if no nodes are selected\n *\n *  var color = pickColor(); // prompt for color\n *\n *  var group = doc.root // get the group to add the backdrop to\n *  var backdrop = group.addBackdropToNodes(nodes, \"BackDrop\", \"\", color)\n *\n *  $.endUndo();\n *\n *  // function to get the color chosen by the user\n *  function pickColor(){\n *    var d = new QColorDialog;\n *    d.exec();\n *    var color = d.selectedColor();\n *    return new $.oColorValue({r:color.red(), g:color.green(), b:color.blue(), a:color.alpha()})\n *  }\n * }\n */\n$.oBackdrop = function( groupPath, backdropObject ){\n  this.group = ( groupPath instanceof this.$.oGroupNode )? groupPath.path: groupPath;\n\tthis.backdropObject = backdropObject;\n}\n\n\n/**\n * The index of this backdrop in the current group.\n * @name $.oBackdrop#index\n * @type {int}\n */\nObject.defineProperty($.oBackdrop.prototype, 'index', {\n    get : function(){\n      var _groupBackdrops = Backdrop.backdrops(this.group).map(function(x){return x.title.text})\n\t\t  return _groupBackdrops.indexOf(this.title)\n    }\n})\n\n\n/**\n * The title of the backdrop.\n * @name $.oBackdrop#title\n * @type {string}\n */\nObject.defineProperty($.oBackdrop.prototype, 'title', {\n  get : function(){\n    var _title = this.backdropObject.title.text;\n    return _title;\n  },\n\n  set : function(newTitle){\n    var _backdrops = Backdrop.backdrops(this.group);\n\n    // incrementing to prevent two backdrops to have the same title\n    var names = _backdrops.map(function(x){return x.title.text})\n    var count = 0;\n    var title = newTitle\n\n    while (names.indexOf(title) != -1){\n      count++;\n      title = newTitle+\"_\"+count;\n    }\n    newTitle = title;\n\n    var _index = this.index;\n\n    _backdrops[_index].title.text = newTitle;\n\n    this.backdropObject = _backdrops[_index];\n    Backdrop.setBackdrops(this.group, _backdrops);\n  }\n})\n\n\n/**\n * The body text of the backdrop.\n * @name $.oBackdrop#body\n * @type {string}\n */\nObject.defineProperty($.oBackdrop.prototype, 'body', {\n    get : function(){\n         var _title = this.backdropObject.description.text;\n         return _title;\n    },\n\n    set : function(newBody){\n        var _backdrops = Backdrop.backdrops(this.group);\n\n\t\tvar _index = this.index;\n        _backdrops[_index].description.text = newBody;\n\n\t\tthis.backdropObject = _backdrops[_index];\n        Backdrop.setBackdrops(this.group, _backdrops);\n    }\n})\n\n\n/**\n * The title font of the backdrop in form { family:\"familyName\", \"size\":int, \"color\": oColorValue }\n * @name $.oBackdrop#titleFont\n * @type {object}\n */\nObject.defineProperty($.oBackdrop.prototype, 'titleFont', {\n    get : function(){\n         var _font = {family : this.backdropObject.title.font,\n                      size : this.backdropObject.title.size,\n                      color : ( new oColorValue() ).parseColorFromInt(this.backdropObject.title.color)}\n         return _font;\n    },\n\n    set : function(newFont){\n        var _backdrops = Backdrop.backdrops(this.group);\n\t\tvar _index = this.index;\n\n        _backdrops[_index].title.font = newFont.family;\n        _backdrops[_index].title.size = newFont.size;\n        _backdrops[_index].title.color = newFont.color.toInt();\n\n        this.backdropObject = _backdrops[_index];\n        Backdrop.setBackdrops(this.group, _backdrops);\n    }\n})\n\n\n/**\n * The body font of the backdrop in form { family:\"familyName\", \"size\":int, \"color\": oColorValue }\n * @name $.oBackdrop#bodyFont\n * @type {object}\n */\nObject.defineProperty($.oBackdrop.prototype, 'bodyFont', {\n    get : function(){\n         var _font = {family : this.backdropObject.description.font,\n                      size : this.backdropObject.description.size,\n                      color : ( new oColorValue() ).parseColorFromInt(this.backdropObject.description.color)}\n         return _font;\n    },\n\n    set : function(newFont){\n        var _backdrops = Backdrop.backdrops(this.group);\n\t\tvar _index = this.index;\n\n        _backdrops[_index].title.font = newFont.family;\n        _backdrops[_index].title.size = newFont.size;\n        _backdrops[_index].title.color = newFont.color.toInt();\n\n\t\tthis.backdropObject = _backdrops[_index];\n        Backdrop.setBackdrops(this.group, _backdrops);\n    }\n})\n\n\n/**\n * The nodes contained within this backdrop\n * @name $.oBackdrop#parent\n * @type {$.oNode[]}\n * @readonly\n */\n Object.defineProperty($.oBackdrop.prototype, 'parent', {\n  get : function(){\n    if (!this.hasOwnProperty(\"_parent\")){\n      this._parent = this.$.scn.getNodeByPath(this.group);\n    }\n    return this._parent\n  }\n})\n\n\n/**\n * The nodes contained within this backdrop\n * @name $.oBackdrop#nodes\n * @type {$.oNode[]}\n * @readonly\n */\nObject.defineProperty($.oBackdrop.prototype, 'nodes', {\n  get : function(){\n    var _nodes = this.parent.nodes;\n    var _bounds = this.bounds;\n    _nodes = _nodes.filter(function(x){\n      return _bounds.contains(x.bounds);\n    })\n\n    return _nodes;\n  }\n})\n\n/**\n * The position of the backdrop on the horizontal axis.\n * @name $.oBackdrop#x\n * @type {float}\n */\nObject.defineProperty($.oBackdrop.prototype, 'x', {\n  get : function(){\n    var _x = this.backdropObject.position.x;\n    return _x;\n  },\n\n  set : function(newX){\n    var _backdrops = Backdrop.backdrops(this.group);\n    var _index = this.index;\n\n    _backdrops[_index].position.x = newX;\n\n    this.backdropObject = _backdrops[_index];\n        Backdrop.setBackdrops(this.group, _backdrops);\n    }\n})\n\n\n/**\n * The position of the backdrop on the vertical axis.\n * @name $.oBackdrop#y\n * @type {float}\n */\nObject.defineProperty($.oBackdrop.prototype, 'y', {\n  get : function(){\n    var _y = this.backdropObject.position.y;\n    return _y;\n  },\n\n  set : function(newY){\n    var _backdrops = Backdrop.backdrops(this.group);\n    var _index = this.index;\n\n    _backdrops[_index].position.y = newY;\n\n    this.backdropObject = _backdrops[_index];\n    Backdrop.setBackdrops(this.group, _backdrops);\n  }\n})\n\n\n/**\n * The width of the backdrop.\n * @name $.oBackdrop#width\n * @type {float}\n */\nObject.defineProperty($.oBackdrop.prototype, 'width', {\n  get : function(){\n    var _width = this.backdropObject.position.w;\n    return _width;\n  },\n\n  set : function(newWidth){\n    var _backdrops = Backdrop.backdrops(this.group);\n    var _index = this.index;\n\n    _backdrops[_index].position.w = newWidth;\n\n    this.backdropObject = _backdrops[_index];\n    Backdrop.setBackdrops(this.group, _backdrops);\n  }\n})\n\n\n/**\n * The height of the backdrop.\n * @name $.oBackdrop#height\n * @memberof $.oBackdrop#\n * @type {float}\n */\nObject.defineProperty($.oBackdrop.prototype, 'height', {\n  get : function(){\n    var _height = this.backdropObject.position.h;\n    return _height;\n  },\n\n  set : function(newHeight){\n    var _backdrops = Backdrop.backdrops(this.group);\n    var _index = this.index;\n\n    _backdrops[_index].position.h = newHeight;\n\n    this.backdropObject = _backdrops[_index];\n    Backdrop.setBackdrops(this.group, _backdrops);\n  }\n})\n\n\n/**\n * The position of the backdrop.\n * @name $.oBackdrop#position\n * @type {oPoint}\n */\nObject.defineProperty($.oBackdrop.prototype, 'position', {\n  get : function(){\n    var _position = new oPoint(this.x, this.y, this.index)\n    return _position;\n  },\n\n  set : function(newPos){\n    var _backdrops = Backdrop.backdrops(this.group);\n    var _index = this.index;\n\n    _backdrops[_index].position.x = newPos.x;\n    _backdrops[_index].position.y = newPos.y;\n\n    this.backdropObject = _backdrops[_index];\n    Backdrop.setBackdrops(this.group, _backdrops);\n  }\n})\n\n\n/**\n * The bounds of the backdrop.\n * @name $.oBackdrop#bounds\n * @type {oBox}\n */\nObject.defineProperty($.oBackdrop.prototype, 'bounds', {\n  get : function(){\n    var _box = new oBox(this.x, this.y, this.width+this.x, this.height+this.y)\n    return _box;\n  },\n\n  set : function(newBounds){\n    var _backdrops = Backdrop.backdrops(this.group);\n    var _index = this.index;\n\n    _backdrops[_index].position.x = newBounds.top;\n    _backdrops[_index].position.y = newBounds.left;\n    _backdrops[_index].position.w = newBounds.width;\n    _backdrops[_index].position.h = newBounds.height;\n\n    this.backdropObject = _backdrops[_index];\n    Backdrop.setBackdrops(this.group, _backdrops);\n  }\n})\n\n\n/**\n * The color of the backdrop.\n * @name $.oBackdrop#color\n * @type {oColorValue}\n */\nObject.defineProperty($.oBackdrop.prototype, 'color', {\n  get : function(){\n    var _color = this.backdropObject.color;\n    // TODO: get the rgba values from the int\n    return _color;\n  },\n\n  set : function(newOColorValue){\n    var _color = new oColorValue(newOColorValue);\n    var _index = this.index;\n\n    var _backdrops = Backdrop.backdrops(this.group);\n    _backdrops[_index].color = _color.toInt();\n\n    this.backdropObject = _backdrops[_index];\n    Backdrop.setBackdrops(this.group, _backdrops);\n  }\n})"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_color.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oColorValue class       //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * This class holds a color value. It can be used to set color attributes to a specific value and to convert colors between different formats such as hex strings, RGBA decompositions, as well as HSL values.\n * @constructor\n * @classdesc  Constructor for the $.oColorValue Class.\n * @param   {string/object}            colorValue            Hex string value, or object in form {rgba}\n *\n * @property {int}                    r                      The int value of the red component.\n * @property {int}                    g                      The int value of the green component.\n * @property {int}                    b                      The int value of the blue component.\n * @property {int}                    a                      The int value of the alpha component.\n * @example\n * // initialise the class to start setting up attributes and making conversions by creating a new instance\n *\n * var myColor = new $.oColorValue(\"#336600ff\");\n * $.log(myColor.r+\" \"+mycolor.g+\" \"+myColor.b+\" \"+myColor+a) // you can then access each component of the color\n *\n * var myBackdrop = $.scn.root.addBackdrop(\"Backdrop\")\n * var myBackdrop.color = myColor                             // can be used to set the color of a backdrop\n *\n */\n$.oColorValue = function( colorValue ){\n    if (typeof colorValue === 'undefined') var colorValue = \"#000000ff\";\n\n    this.r = 0;\n    this.g = 0;\n    this.b = 0;\n    this.a = 255;\n\n    //Special case in which RGBA values are defined directly.\n    switch( arguments.length ){\n      case 4:\n        this.a = ( (typeof arguments[3]) == \"number\" ) ? arguments[3] : 0;\n      case 3:\n        this.r = ( (typeof arguments[0]) == \"number\" ) ? arguments[0] : 0;\n        this.g = ( (typeof arguments[1]) == \"number\" ) ? arguments[1] : 0;\n        this.b = ( (typeof arguments[2]) == \"number\" ) ? arguments[2] : 0;\n        return;\n      default:\n    }\n\n    if (typeof colorValue === 'string'){\n      this.fromColorString(colorValue);\n    }else{\n      if (typeof colorValue.r === 'undefined') colorValue.r = 0;\n      if (typeof colorValue.g === 'undefined') colorValue.g = 0;\n      if (typeof colorValue.b === 'undefined') colorValue.b = 0;\n      if (typeof colorValue.a === 'undefined') colorValue.a = 255;\n\n      this.r = colorValue.r;\n      this.g = colorValue.g;\n      this.b = colorValue.b;\n      this.a = colorValue.a;\n    }\n}\n\n\n/**\n * Creates an int from the color value, as used for backdrop colors.\n * @return: {string}       ALPHA<<24  RED<<16  GREEN<<8  BLUE\n */\n$.oColorValue.prototype.toInt = function (){\n     return ((this.a & 0xff) << 24) | ((this.r & 0xff) << 16) | ((this.g & 0xff) << 8) | (this.b & 0xff);\n}\n\n\n/**\n * The colour value represented as a string.\n * @return: {string}       RGBA components in a string in format #RRGGBBAA\n */\n$.oColorValue.prototype.toString = function (){\n    var _hex = \"#\";\n\n    var r = (\"00\"+this.r.toString(16)).slice(-2);\n    var g = (\"00\"+this.g.toString(16)).slice(-2);\n    var b = (\"00\"+this.b.toString(16)).slice(-2);\n    var a = (\"00\"+this.a.toString(16)).slice(-2);\n\n    _hex += r + g + b + a;\n\n    return _hex;\n}\n\n/**\n * The colour value represented as a string.\n * @return: {string}       RGBA components in a string in format #RRGGBBAA\n */\n$.oColorValue.prototype.toHex = function (){\n  return this.toString();\n}\n\n/**\n * Ingest a hex string in form #RRGGBBAA to define the colour.\n * @param   {string}    hexString                The colour in form #RRGGBBAA\n */\n$.oColorValue.prototype.fromColorString = function (hexString){\n    hexString = hexString.replace(\"#\",\"\");\n    if (hexString.length == 6) hexString += \"ff\";\n    if (hexString.length != 8) throw new Error(\"incorrect color string format\");\n\n    this.$.debug( \"HEX : \" + hexString, this.$.DEBUG_LEVEL.LOG);\n\n    this.r = parseInt(hexString.slice(0,2), 16);\n    this.g = parseInt(hexString.slice(2,4), 16);\n    this.b = parseInt(hexString.slice(4,6), 16);\n    this.a = parseInt(hexString.slice(6,8), 16);\n}\n\n\n/**\n * Uses a color integer (used in backdrops) and parses the INT; applies the RGBA components of the INT to the oColorValue\n * @param   { int }    colorInt                      24 bit-shifted integer containing RGBA values\n */\n$.oColorValue.prototype.parseColorFromInt = function(colorInt){\n\tthis.r = colorInt >> 16 & 0xFF;\n\tthis.g = colorInt >> 8 & 0xFF;\n\tthis.b = colorInt & 0xFF;\n  this.a = colorInt >> 24 & 0xFF;\n}\n\n\n/**\n * Gets the color's HUE value.\n * @name $.oColorValue#h\n * @type {float}\n */\nObject.defineProperty($.oColorValue.prototype, 'h', {\n    get : function(){\n        var r = this.r;\n        var g = this.g;\n        var b = this.b;\n\n        var cmin = Math.min(r,g,b);\n        var cmax = Math.max(r,g,b);\n        var delta = cmax - cmin;\n        var h = 0;\n        var s = 0;\n        var l = 0;\n\n        if (delta == 0){\n          h = 0.0;\n        // Red is max\n        }else if (cmax == r){\n          h = ((g - b) / delta) % 6.0;\n        // Green is max\n        }else if (cmax == g){\n          h = (b - r) / delta + 2.0;\n        // Blue is max\n        }else{\n          h = (r - g) / delta + 4.0;\n        }\n\n        h = Math.round(h * 60.0);\n\n        //WRAP IN 360.\n        if (h < 0){\n            h += 360.0;\n        }\n\n        // // Calculate lightness\n        // l = (cmax + cmin) / 2.0;\n\n        // // Calculate saturation\n        // s = delta == 0 ? 0 : delta / (1.0 - Math.abs(2.0 * l - 1.0));\n\n        // s = Math.min( Math.abs(s)*100.0, 100.0 );\n        // l = (Math.abs(l)/255.0)*100.0;\n\n        return h;\n    },\n\n    set : function( new_h ){\n      var h = Math.min( new_h, 360.0 );\n      var s = Math.min( this.s, 100.0 )/100.0;\n      var l = Math.min( this.l, 100.0 )/100.0;\n\n      var c = (1.0 - Math.abs(2.0 * l - 1.0)) * s;\n      var x = c * (1 - Math.abs((h / 60.0) % 2.0 - 1.0));\n      var m = l - c/2.0;\n      var r = 0.0;\n      var g = 0.0;\n      var b = 0.0;\n\n      if (0.0 <= h && h < 60.0) {\n        r = c; g = x; b = 0;\n      } else if (60.0 <= h && h < 120.0) {\n        r = x; g = c; b = 0;\n      } else if (120.0 <= h && h < 180.0) {\n        r = 0; g = c; b = x;\n      } else if (180.0 <= h && h < 240.0) {\n        r = 0; g = x; b = c;\n      } else if (240.0 <= h && h < 300.0) {\n        r = x; g = 0; b = c;\n      } else if (300.0 <= h && h < 360.0) {\n        r = c; g = 0; b = x;\n      }\n\n      this.r = (r + m) * 255.0;\n      this.g = (g + m) * 255.0;\n      this.b = (b + m) * 255.0;\n    }\n});\n\n/**\n * Gets the color's SATURATION value.\n * @name $.oColorValue#s\n * @type {float}\n */\nObject.defineProperty($.oColorValue.prototype, 's', {\n    get : function(){\n        var r = this.r;\n        var g = this.g;\n        var b = this.b;\n\n        var cmin = Math.min(r,g,b);\n        var cmax = Math.max(r,g,b);\n        var delta = cmax - cmin;\n        var s = 0;\n        var l = 0;\n\n        // Calculate lightness\n        l = (cmax + cmin) / 2.0;\n        s = delta == 0 ? 0 : delta / (1.0 - Math.abs(2.0 * l - 1.0));\n\n        // Calculate saturation\n        s = Math.min( Math.abs(s)*100.0, 100.0 );\n\n        return s;\n    },\n\n    set : function( new_s ){\n      var h = Math.min( this.h, 360.0 );\n      var s = Math.min( new_s, 100.0 )/100.0;\n      var l = Math.min( this.l, 100.0 )/100.0;\n\n      var c = (1.0 - Math.abs(2.0 * l - 1.0)) * s;\n      var x = c * (1 - Math.abs((h / 60.0) % 2.0 - 1.0));\n      var m = l - c/2.0;\n      var r = 0.0;\n      var g = 0.0;\n      var b = 0.0;\n\n      if (0.0 <= h && h < 60.0) {\n        r = c; g = x; b = 0;\n      } else if (60.0 <= h && h < 120.0) {\n        r = x; g = c; b = 0;\n      } else if (120.0 <= h && h < 180.0) {\n        r = 0; g = c; b = x;\n      } else if (180.0 <= h && h < 240.0) {\n        r = 0; g = x; b = c;\n      } else if (240.0 <= h && h < 300.0) {\n        r = x; g = 0; b = c;\n      } else if (300.0 <= h && h < 360.0) {\n        r = c; g = 0; b = x;\n      }\n\n      this.r = (r + m) * 255.0;\n      this.g = (g + m) * 255.0;\n      this.b = (b + m) * 255.0;\n    }\n});\n\n/**\n * Gets the color's LIGHTNESS value.\n * @name $.oColorValue#l\n * @type {float}\n */\nObject.defineProperty($.oColorValue.prototype, 'l', {\n    get : function(){\n        var r = this.r;\n        var g = this.g;\n        var b = this.b;\n\n        var cmin = Math.min(r,g,b);\n        var cmax = Math.max(r,g,b);\n        var delta = cmax - cmin;\n        var s = 0;\n        var l = 0;\n\n\n        // Calculate lightness\n        l = (cmax + cmin) / 2.0;\n        l = (Math.abs(l)/255.0)*100.0;\n        return l;\n    },\n\n    set : function( new_l ){\n      var h = Math.min( this.h, 360.0 );\n      var s = Math.min( this.s, 100.0 )/100.0;\n      var l = Math.min( new_l, 100.0 )/100.0;\n\n      var c = (1.0 - Math.abs(2.0 * l - 1.0)) * s;\n      var x = c * (1 - Math.abs((h / 60.0) % 2.0 - 1.0));\n      var m = l - c/2.0;\n      var r = 0.0;\n      var g = 0.0;\n      var b = 0.0;\n\n      if (0.0 <= h && h < 60.0) {\n        r = c; g = x; b = 0;\n      } else if (60.0 <= h && h < 120.0) {\n        r = x; g = c; b = 0;\n      } else if (120.0 <= h && h < 180.0) {\n        r = 0; g = c; b = x;\n      } else if (180.0 <= h && h < 240.0) {\n        r = 0; g = x; b = c;\n      } else if (240.0 <= h && h < 300.0) {\n        r = x; g = 0; b = c;\n      } else if (300.0 <= h && h < 360.0) {\n        r = c; g = 0; b = x;\n      }\n\n      this.r = (r + m) * 255.0;\n      this.g = (g + m) * 255.0;\n      this.b = (b + m) * 255.0;\n    }\n});\n\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//           $.oColor class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n// oPalette constructor\n\n/**\n * The base class for the $.oColor.\n * @constructor\n * @classdesc  $.oColor Base Class\n * @param   {$.oPalette}             oPaletteObject             The palette to which the color belongs.\n * @param   {int}                    attributeObject            The index of the color in the palette.\n *\n * @property {$.oPalette}            palette                    The palette to which the color belongs.\n */\n$.oColor = function( oPaletteObject, index ){\n  // We don't use id in the constructor as multiple colors with the same id can exist in the same palette.\n  this._type = \"color\";\n\n  this.palette = oPaletteObject;\n  this._index = index;\n}\n\n// $.oColor Object Properties\n\n/**\n * The Harmony color object.\n * @name $.oColor#colorObject\n * @type {BaseColor}\n */\nObject.defineProperty($.oColor.prototype, 'colorObject', {\n    get : function(){\n        return this.palette.paletteObject.getColorByIndex(this._index);\n    }\n});\n\n\n\n/**\n * The name of the color.\n * @name $.oColor#name\n * @type {string}\n */\nObject.defineProperty($.oColor.prototype, 'name', {\n    get : function(){\n        var _color = this.colorObject;\n        return _color.name;\n    },\n\n    set : function(newName){\n        var _color = this.colorObject;\n        _color.setName(newName);\n    }\n});\n\n\n/**\n * The id of the color.\n * @name $.oColor#id\n * @type {string}\n */\nObject.defineProperty($.oColor.prototype, 'id', {\n    get : function(){\n        var _color = this.colorObject;\n        return _color.id\n    },\n\n    set : function(newId){\n        // TODO: figure out a way to change id? Create a new color with specific id in the palette?\n        throw new Error(\"setting oColor.id Not yet implemented\");\n    }\n});\n\n\n/**\n * The index of the color.\n * @name $.oColor#index\n * @type {int}\n */\nObject.defineProperty($.oColor.prototype, 'index', {\n    get : function(){\n        return this._index;\n    },\n\n    set : function(newIndex){\n        var _color = this.palette.paletteObject.moveColor(this._index, newIndex);\n        this._index = newIndex;\n    }\n});\n\n\n/**\n * The type of the color.\n * @name $.oColor#type\n * @type {int}\n */\nObject.defineProperty($.oColor.prototype, 'type', {\n    set : function(){\n      throw new Error(\"setting oColor.type Not yet implemented.\");\n    },\n\n    get : function(){\n        var _color = this.colorObject;\n        if (_color.isTexture) return \"texture\";\n\n        switch (_color.colorType) {\n            case PaletteObjectManager.Constants.ColorType.SOLID_COLOR:\n                return \"solid\";\n            case PaletteObjectManager.Constants.ColorType.LINEAR_GRADIENT :\n                return \"gradient\";\n            case PaletteObjectManager.Constants.ColorType.RADIAL_GRADIENT:\n                return \"radial gradient\";\n            default:\n        }\n    }\n});\n\n\n/**\n * Whether the color is selected.\n * @name $.oColor#selected\n * @type {bool}\n */\nObject.defineProperty($.oColor.prototype, 'selected', {\n    get : function(){\n        var _currentId = PaletteManager.getCurrentColorId()\n        var _colors = this.palette.colors;\n        var _ids = _colors.map(function(x){return x.id})\n        return this._index == _ids.indexOf(_currentId);\n    },\n\n    set : function(isSelected){\n        // TODO: find a way to work with index as more than one color can have the same id, also, can there be no selected color when removing selection?\n        if (isSelected){\n            var _id = this.id;\n            PaletteManager.setCurrentColorById(_id);\n        }\n    }\n});\n\n\n/**\n * Takes a string or array of strings for gradients and filename for textures. Instead of passing rgba objects, it accepts \"#rrggbbaa\" hex strings for convenience.<br>set gradients, provide an object with keys from 0 to 1 for the position of each color.<br>(ex: {0: new $.oColorValue(\"000000ff\"), 1:new $.oColorValue(\"ffffffff\")}).\n * @name $.oColor#value\n * @type {$.oColorValue}\n */\nObject.defineProperty($.oColor.prototype, 'value', {\n  get : function(){\n    var _color = this.colorObject;\n\n    switch(this.type){\n      case \"solid\":\n        return new this.$.oColorValue(_color.colorData);\n      case \"texture\":\n        return this.palette.path.parent.path + this.palette.name+\"_textures/\" + this.id + \".tga\";\n      case \"gradient\":\n      case \"radial gradient\":\n        var _gradientArray = _color.colorData;\n        var _value = {};\n        for (var i in _gradientArray){\n          var _data = _gradientArray[i];\n          _value[_gradientArray[i].t] = new this.$.oColorValue(_data.r, _data.g, _data.b, _data.a);\n        }\n        return _value;\n      default:\n    }\n  },\n\n  set : function(newValue){\n    var _color = this.colorObject;\n\n    switch(this.type){\n      case \"solid\":\n        _value = new $.oColorValue(newValue);\n        _color.setColorData(_value);\n        break;\n      case \"texture\":\n        // TODO: need to copy the file into the folder first?\n        _color.setTextureFile(newValue);\n        break;\n      case \"gradient\":\n      case \"radial gradient\":\n        var _value = [];\n        var _gradient = newValue;\n        for (var i  in _gradient){\n          var _color = _gradient[i];\n          var _tack = {r:_color.r, g:_color.g, b:_color.b, a:_color.a, t:parseFloat(i, 10)}\n          _value.push(_tack);\n        }\n        _color.setColorData(_value);\n        break;\n      default:\n    };\n  }\n});\n\n\n// Methods\n\n/**\n * Moves the palette to another Palette Object (CFNote: perhaps have it push to paletteObject, instead of being done at the color level)\n * @param   {$.oPalette}         oPaletteObject              The paletteObject to move this color into.\n * @param   {int}                index                       Need clarification from mchap\n *\n * @return: {$.oColor}           The new resulting $.oColor object.\n */\n$.oColor.prototype.moveToPalette = function (oPaletteObject, index){\n    if (typeof index === 'undefined') var index = oPaletteObject.paletteObject.nColors;\n    var _duplicate = this.copyToPalette(oPaletteObject, index)\n    this.remove()\n\n    return _duplicate;\n}\n\n\n/**\n * Copies the palette to another Palette Object (CFNote: perhaps have it push to paletteObject, instead of being done at the color level)\n * @param   {$.oPalette}         oPaletteObject              The paletteObject to move this color into.\n * @param   {int}                index                       Need clarification from mchap\n *\n * @return: {$.oColor}           The new resulting $.oColor object.\n */\n$.oColor.prototype.copyToPalette = function (oPaletteObject, index){\n    var _color = this.colorObject;\n\n    oPaletteObject.paletteObject.cloneColor(_color);\n    var _colors = oPaletteObject.colors;\n    var _duplicate = _colors.pop();\n\n    if (typeof index !== 'undefined')  _duplicate.index = index;\n\n    return _duplicate;\n}\n\n\n/**\n * Removes the color from the palette it belongs to.\n */\n$.oColor.prototype.remove = function (){\n    // TODO: find a way to work with index as more than one color can have the same id\n    this.palette.paletteObject.removeColor(this.id);\n}\n\n\n/**\n * Static helper function to convert from {r:int, g:int, b:int, a:int} to a hex string in format #FFFFFFFF <br>\n *          Consider moving this to a helper function.\n * @param   { obj }       rgbaObject                       RGB object\n * @static\n * @return: { string }    Hex color string in format #FFFFFFFF.\n */\n$.oColor.prototype.rgbaToHex = function (rgbaObject){\n    var _hex = \"#\";\n    _hex += rvbObject.r.toString(16)\n    _hex += rvbObject.g.toString(16)\n    _hex += rvbObject.b.toString(16)\n    _hex += rvbObject.a.toString(16)\n\n    return _hex;\n}\n\n\n/**\n *  Static helper function to convert from hex string in format #FFFFFFFF to {r:int, g:int, b:int, a:int} <br>\n *          Consider moving this to a helper function.\n * @param   { string }    hexString                       RGB object\n * @static\n * @return: { obj }    The hex object returned { r:int, g:int, b:int, a:int }\n */\n$.oColor.prototype.hexToRgba = function (hexString){\n    var _rgba = {};\n    //Needs a better fail state.\n\n    _rgba.r = parseInt(hexString.slice(1,3), 16)\n    _rgba.g = parseInt(hexString.slice(3,5), 16)\n    _rgba.b = parseInt(hexString.slice(5,7), 16)\n    _rgba.a = parseInt(hexString.slice(7,9), 16)\n\n    return _rgba;\n}\n\n\n\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_column.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oColumn class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oColumn class.\n * @classdesc  Columns are the objects that hold all the animation information of an attribute. Any animated value in Harmony is so thanks to a column linked to the attribute representing the node parameter. Columns can be added from the scene class, or are directly created when giving a non 1 value when setting an attribute.\n * @constructor\n * @param   {string}                   uniqueName                  The unique name of the column.\n * @param   {$.oAttribute}             oAttributeObject            The oAttribute thats connected to the column.\n *\n * @property {string}                  uniqueName                  The unique name of the column.\n * @property {$.oAttribute}            attributeObject             The attribute object that the column is attached to.\n * @example\n * // You can get the entirety of the columns in the scene by calling:\n * var doc = $.scn;\n * var allColumns = doc.columns;\n *\n * // However, to get a specific column, you can retrieve it from its linked attribute:\n *\n * var myAttribute = doc.nodes[0].attributes.position.x\n * var mycolumn = myAttribute.column;\n *\n * // once you have the column, you can do things like remove duplicates keys to simplify an animation;\n * myColumn.removeDuplicateKeys();\n *\n * // you can extract all the keys to be able to iterate over it:\n * var keyFrames = myColumn.getKeyFrames();\n *\n * for (var i in keyFrames){\n *   $.log (keyFrames[i].frameNumber);\n * }\n *\n * // you can also link a given column to more than one attribute so they share the same animated values:\n *\n * doc.nodes[0].attributes.position.y.column = myColumn;  // now position.x and position.y will share the same animation on the node.\n */\n$.oColumn = function( uniqueName, oAttributeObject ){\n  var instance = this.$.getInstanceFromCache.call(this, uniqueName);\n  if (instance) return instance;\n\n  this._type = \"column\";\n\n  this.uniqueName = uniqueName;\n  this.attributeObject = oAttributeObject;\n\n  this._cacheFrames = [];\n\n  //Helper cache for subsequent actions.\n  try{\n    // fails when the column has no attribute\n    if( !this.$.cache_columnToNodeAttribute ){ this.$.cache_columnToNodeAttribute = {}; }\n    this.$.cache_columnToNodeAttribute[this.uniqueName] = { \"node\":oAttributeObject.node, \"attribute\": this.attributeObject, \"date\": (new Date()).getTime() };\n  }catch(err){}\n}\n\n\n// $.oColumn Object Properties\n/**\n * The name of the column.\n * @name $.oColumn#name\n * @type {string}\n */\nObject.defineProperty( $.oColumn.prototype, 'name', {\n    get : function(){\n         return column.getDisplayName(this.uniqueName);\n    },\n\n    set : function(newName){\n        var _success = column.rename(this.uniqueName, newName)\n        if (_success){\n          this.uniqueName = newName;\n        }else{\n          throw new Error(\"Failed to rename column \"+this.uniqueName+\" to \"+newName+\".\")\n        }\n    }\n});\n\n\n/**\n * The type of the column. There are nine column types: drawing (DRAWING), sound (SOUND), 3D Path (3DPATH), Bezier Curve (BEZIER), Ease Curve (EASE), Expression (EXPR), Timing (TIMING) for timing columns, Quaternion path (QUATERNIONPATH) for 3D rotation and Annotation (ANNOTATION) for annotation columns.\n * @name $.oColumn#type\n * @readonly\n * @type {string}\n */\nObject.defineProperty( $.oColumn.prototype, 'type', {\n    get : function(){\n        return column.type(this.uniqueName)\n    }\n});\n\n\n/**\n * Whether the column is selected.\n * @name $.oColumn#selected\n * @type {bool}\n */\nObject.defineProperty($.oColumn.prototype, 'selected', {\n    get : function(){\n        var sel_num = selection.numberOfColumnsSelected();\n        for( var n=0;n<sel_num;n++ ){\n          var col = selection.selectedColumn( n );\n          if( col == this.uniqueName ){\n            return true;\n          }\n        }\n\n        return false;\n    },\n    set : function(){\n      selection.addColumnToSelection(this.uniqueName);\n    }\n});\n\n\n/**\n * An array of the oFrame objects provided by the column.\n * @name $.oColumn#frames\n * @type {$.oFrame[]}\n * @readonly\n */\nObject.defineProperty($.oColumn.prototype, 'frames', {\n    get : function(){\n        while( this._cacheFrames.length < frame.numberOf()+1 ){\n          this._cacheFrames.push( new this.$.oFrame( this._cacheFrames.length, this ) );\n        }\n\n        return this._cacheFrames;\n    }\n});\n\n\n/**\n * An array of the keyframes provided by the column.\n * @name $.oColumn#keyframes\n * @readonly\n * @type {$.oFrame[]}\n */\nObject.defineProperty($.oColumn.prototype, 'keyframes', {\n    get : function(){\n      return this.getKeyframes();\n    }\n});\n\n\n/**\n * Provides the available subcolumns, based on the type of the column.\n * @name $.oColumn#subColumns\n * @readonly\n * @type {object}\n */\nObject.defineProperty($.oColumn.prototype, 'subColumns', {\n    get : function(){\n      //CF Note: Not sure of this use.\n      //MC > allows to loop through subcolumns if they exist\n        if (this.type == \"3DPATH\"){\n            return { x : 1,\n                     y : 2,\n                     z : 3,\n                     velocity : 4}\n        }\n        return { a : 1 };\n    }\n});\n\n/**\n * The type of easing used by the column\n * @name $.oColumn#subColumns\n * @readonly\n * @type {object}\n */\nObject.defineProperty($.oColumn.prototype, 'easeType', {\n    get : function(){\n        switch(this.type){\n            case \"BEZIER\":\n                return \"BEZIER\";\n            case \"3DPATH\":\n                return column.velocityType( this.uniqueName );\n            default:\n                return null;\n        }\n    }\n})\n\n\n/**\n * An object with three int values : start, end and step, representing the value of the stepped section parameter (interpolation with non linear \"step\" parameter).\n * @name $.oColumn#stepSection\n * @type {object}\n */\nObject.defineProperty($.oColumn.prototype, 'stepSection', {\n  get : function(){\n    var _columnName = this.uniqueName;\n    var _section = {\n      start: func.holdStartFrame (_columnName),\n      end : func.holdStopFrame (_columnName),\n      step : func.holdStep (_columnName)\n    }\n    return _section;\n  },\n\n  set : function(newSection){\n    var _columnName = this.uniqueName;\n    func.setHoldStartFrame (_columnName, newSection.start)\n    func.setHoldStopFrame (_columnName, newSection.end)\n    func.setHoldStep (_columnName, newSection.step)\n  }\n});\n\n\n// $.oColumn Class methods\n\n/**\n * Deletes the column from the scene. The column must be unlinked from any attribute first.\n */\n$.oColumn.prototype.remove = function(){\n  column.removeUnlinkedFunctionColumn(this.name);\n  if (this.type) throw new Error(\"Couldn't remove column \"+this.name+\", unlink it from any attribute first.\")\n}\n\n\n/**\n * Extends the exposure of the drawing's keyframes given the provided arguments.\n * @deprecated Use oDrawingColumn.extendExposures instead.\n * @param   {$.oFrame[]}  exposures            The exposures to extend. If UNDEFINED, extends all keyframes.\n * @param   {int}         amount               The amount to extend.\n * @param   {bool}        replace              Setting this to false will insert frames as opposed to overwrite existing ones.\n */\n$.oColumn.prototype.extendExposures = function( exposures, amount, replace){\n    if (this.type != \"DRAWING\") return false;\n    // if amount is undefined, extend function below will automatically fill empty frames\n\n    if (typeof exposures === 'undefined') var exposures = this.attributeObject.getKeyframes();\n\n    for (var i in exposures) {\n        if (!exposures[i].isBlank) exposures[i].extend(amount, replace);\n    }\n\n}\n\n\n\n/**\n * Removes concurrent/duplicate keys from drawing layers.\n */\n$.oColumn.prototype.removeDuplicateKeys = function(){\n    var _keys = this.getKeyframes();\n\n    var _pointsToRemove = [];\n    var _pointC;\n\n    // check the extremities\n    var _pointA = _keys[0].value;\n    var _pointB = _keys[1].value;\n    if (JSON.stringify(_pointA) == JSON.stringify(_pointB)) _pointsToRemove.push(_keys[0].frameNumber);\n\n    for (var k=1; k<_keys.length-2; k++){\n        _pointA = _keys[k-1].value;\n        _pointB = _keys[k].value;\n        _pointC = _keys[k+1].value;\n\n        MessageLog.trace(this.attributeObject.keyword+\" pointA: \"+JSON.stringify(_pointA)+\" pointB: \"+JSON.stringify(_pointB)+\" pointC: \"+JSON.stringify(_pointC));\n\n        if (JSON.stringify(_pointA) == JSON.stringify(_pointB) && JSON.stringify(_pointB) == JSON.stringify(_pointC)){\n            _pointsToRemove.push(_keys[k].frameNumber);\n        }\n    }\n\n    _pointA = _keys[_keys.length-2].value;\n    _pointB = _keys[_keys.length-1].value;\n    if (JSON.stringify(_pointC) == JSON.stringify(_pointB)) _pointsToRemove.push(_keys[_keys.length-1].frameNumber);\n\n    var _frames = this.frames;\n\n    for (var i in _pointsToRemove){\n        _frames[_pointsToRemove[i]].isKeyframe = false;\n    }\n\n}\n\n\n/**\n * Duplicates a column. Because of the way Harmony works, specifying an attribute the column will be connected to ensures higher value fidelity between the original and the copy.\n * @param {$.oAttribute}     [newAttribute]         An attribute to link the column to upon duplication.\n *\n * @return {$.oColumn}                The column generated.\n */\n$.oColumn.prototype.duplicate = function(newAttribute) {\n  var _duplicateColumn = this.$.scene.addColumn(this.type, this.name);\n\n  // linking to an attribute if one is provided\n  if (typeof newAttribute !== 'undefined'){\n    newAttribute.column = _duplicateColumn;\n    _duplicateColumn.attributeObject = newAttribute;\n  }\n\n  var _duplicatedFrames = _duplicateColumn.frames;\n  var _keyframes = this.keyframes;\n\n  // we set the ease twice to avoid incompatibilities between ease parameters and yet unchanged points\n  for (var i in _keyframes){\n    var _duplicateFrame = _duplicatedFrames[_keyframes[i].frameNumber];\n    _duplicateFrame.value = _keyframes[i].value;\n  }\n\n  for (var i in _keyframes){\n    var _duplicateFrame = _duplicatedFrames[_keyframes[i].frameNumber];\n    _duplicateFrame.ease = _keyframes[i].ease;\n  }\n\n  for (var i in _keyframes){\n    var _duplicateFrame = _duplicatedFrames[_keyframes[i].frameNumber];\n    _duplicateFrame.ease = _keyframes[i].ease;\n  }\n\n  _duplicateColumn.stepSection = this.stepSection;\n\n  return _duplicateColumn;\n}\n\n\n/**\n * Filters out only the keyframes from the frames array.\n *\n * @return {$.oFrame[]}    Provides the array of frames from the column.\n */\n$.oColumn.prototype.getKeyframes = function(){\n  var _frames = this.frames;\n\n  var _ease = this.easeType;\n  if( _ease == \"BEZIER\" || _ease == \"EASE\" ){\n    var _keyFrames = [];\n    var _columnName = this.uniqueName;\n    var _points = func.numberOfPoints(_columnName);\n\n    for (var i = 0; i<_points; i++) {\n      var _frameNumber = func.pointX( _columnName, i )\n      _keyFrames.push( _frames[_frameNumber] );\n    }\n\n    return _keyFrames;\n  }\n\n  _frames = _frames.filter(function(x){return x.isKeyframe});\n  return _frames;\n}\n\n\n/**\n * Filters out only the keyframes from the frames array.\n * @deprecated For case consistency, keyframe will never have a capital F\n * @return {$.oFrame[]}    Provides the array of frames from the column.\n */\n$.oColumn.prototype.getKeyFrames = function(){\n  this.$.debug(\"oColumn.getKeyFrames is deprecated. Use oColumn.getKeyframes instead.\", this.$.DEBUG_LEVEL.ERROR);\n  return this.keyframes;\n}\n\n\n/**\n * Gets the value of the column at the given frame.\n * @param {int}  [frame=1]       The frame at which to get the value\n * @return  {various}            The value of the column, can be different types depending on column type.\n */\n$.oColumn.prototype.getValue = function(frame){\n  if (typeof frame === 'undefined') var frame = 1;\n\n  // this.$.log(\"Getting value of frame \"+this.frameNumber+\" of column \"+this.column.name)\n  if (this.attributeObject){\n    return this.attributeObject.getValue(frame);\n  }else{\n    this.$.debug(\"getting unlinked column \"+this.name+\" value at frame \"+frame, this.$.DEBUG_LEVEL.ERROR);\n    this.$.debug(\"warning : getting a value from a column without attribute destroys value fidelity\", this.$.DEBUG_LEVEL.ERROR);\n\n    if (this.type == \"3DPATH\") {\n      var _frame = new this.$.oFrame(frame, this, this.subColumns);\n      return new this.$.oPathPoint(this, _frame);\n    }\n\n    return column.getEntry (this.uniqueName, 1, frame);\n  }\n}\n\n\n/**\n * Sets the value of the column at the given frame.\n * @param {various}   newValue        The new value to set the column to\n * @param {int}       [frame=1]       The frame at which to get the value\n */\n$.oColumn.prototype.setValue = function(newValue, frame){\n  if (typeof frame === 'undefined') var frame = 1;\n\n  if (this.attributeObject){\n    this.attributeObject.setValue( newValue, frame);\n  }else{\n    this.$.debug(\"setting unlinked column \"+this.name+\" value to \"+newValue+\" at frame \"+frame, this.$.DEBUG_LEVEL.ERROR);\n    this.$.debug(\"warning : setting a value on a column without attribute destroys value fidelity\", this.$.DEBUG_LEVEL.ERROR);\n\n    if (this.type == \"3DPATH\") {\n      column.setEntry (this.uniqueName, 1, frame, newValue.x);\n      column.setEntry (this.uniqueName, 2, frame, newValue.y);\n      column.setEntry (this.uniqueName, 3, frame, newValue.z);\n      column.setEntry (this.uniqueName, 4, frame, newValue.velocity);\n    }else{\n      column.setEntry (this.uniqueName, 1, frame, newValue.toString());\n    }\n  }\n}\n\n\n\n/**\n * Retrieves the nodes index in the timeline provided.\n * @param   {oTimeline}   [timeline]     Optional: the timeline object to search the column Layer. (by default, grabs the current timeline)\n *\n * @return  {int}    The index within that timeline.\n */\n$.oColumn.prototype.getTimelineLayer = function(timeline){\n  if (typeof timeline === 'undefined') var timeline = this.$.scene.getTimeline();\n\n  var _columnNames = timeline.allLayers.map(function(x){return x.column?x.column.uniqueName:null});\n  return timeline.allLayers[_columnNames.indexOf(this.uniqueName)];\n}\n\n\n/**\n * @private\n */\n$.oColumn.prototype.toString = function(){\n  return \"[object $.oColumn '\"+this.name+\"']\"\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//      $.oDrawingColumn class      //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * the $.oDrawingColumn constructor. Only called internally by the factory function [scene.getColumnByName()]{@link $.oScene#getColumnByName};\n * @constructor\n * @classdesc  oDrawingColumn is a special case of column which can be linked to an [oElement]{@link $.oElement}. This type of column is used to display drawings and always is visible in the Xsheet window.\n * @augments   $.oColumn\n * @param   {string}                   uniqueName                  The unique name of the column.\n * @param   {$.oAttribute}             oAttributeObject            The oAttribute thats connected to the column.\n *\n * @property {string}                  uniqueName                  The unique name of the column.\n * @property {$.oAttribute}            attributeObject             The attribute object that the column is attached to.\n */\n$.oDrawingColumn = function( uniqueName, oAttributeObject ) {\n  // $.oDrawingColumn can only represent a column of type 'DRAWING'\n    if (column.type(uniqueName) != 'DRAWING') throw new Error(\"'uniqueName' parameter must point to a 'DRAWING' type node\");\n    //MessageBox.information(\"getting an instance of $.oDrawingColumn for column : \"+uniqueName)\n    var instance = $.oColumn.call(this, uniqueName, oAttributeObject);\n    if (instance) return instance;\n}\n\n\n// extends $.oColumn and can use its methods\n$.oDrawingColumn.prototype = Object.create($.oColumn.prototype);\n$.oDrawingColumn.prototype.constructor = $.oColumn;\n\n\n/**\n * Retrieve and set the drawing element attached to the column.\n * @name $.oDrawingColumn#element\n * @type {$.oElement}\n */\nObject.defineProperty($.oDrawingColumn.prototype, 'element', {\n    get : function(){\n        return new this.$.oElement(column.getElementIdOfDrawing( this.uniqueName), this);\n    },\n\n    set : function(oElementObject){\n        column.setElementIdOfDrawing( this.uniqueName, oElementObject.id );\n        oElementObject.column = this;\n    }\n})\n\n\n/**\n * Extends the exposure of the drawing's keyframes by the specified amount.\n * @param   {$.oFrame[]}  [exposures]            The exposures to extend. If not specified, extends all keyframes.\n * @param   {int}         [amount]               The number of frames to add to each exposure. If not specified, will extend frame up to the next one.\n * @param   {bool}        [replace=false]        Setting this to false will insert frames as opposed to overwrite existing ones.(currently unsupported))\n */\n$.oDrawingColumn.prototype.extendExposures = function( exposures, amount, replace){\n    // if amount is undefined, extend function below will automatically fill empty frames\n    if (typeof exposures === 'undefined') var exposures = this.getKeyframes();\n\n    this.$.debug(\"extendingExposures \"+exposures.map(function(x){return x.frameNumber})+\" by \"+amount, this.$.DEBUG_LEVEL.DEBUG)\n\n    // can't extend blank exposures, so we remove them from the list to extend\n    exposures = exposures.filter(function(x){return !x.isBlank})\n\n    for (var i in exposures) {\n      exposures[i].extend(amount, replace);\n    }\n}\n\n\n/**\n * Duplicates a Drawing column.\n * @param {bool}          [duplicateElement=true]     Whether to also duplicate the element. Default is true.\n * @param {$.oAttribute}  [newAttribute]              Whether to link the new column to an attribute at this point.\n *\n * @return {$.oColumn}    The created column.\n */\n$.oDrawingColumn.prototype.duplicate = function(newAttribute, duplicateElement) {\n  // duplicate element?\n  if (typeof duplicateElement === 'undefined') var duplicateElement = true;\n  var _duplicateElement = duplicateElement?this.element.duplicate():this.element;\n\n  var _duplicateColumn = this.$.scene.addColumn(this.type, this.name, _duplicateElement);\n\n  // linking to an attribute if one is provided\n  if (typeof newAttribute !== 'undefined'){\n    newAttribute.column = _duplicateColumn;\n    _duplicateColumn.attributeObject = newAttribute;\n  }\n\n  var _frames = this.frames;\n  for (var i in _frames){\n    var _duplicateFrame = _duplicateColumn.frames[i];\n    _duplicateFrame.value = _frames[i].value;\n    if (_frames[i].isKeyframe) _duplicateFrame.isKeyframe = true;\n  }\n\n  return _duplicateColumn;\n}\n\n\n/**\n * Renames the column's exposed drawings according to the frame they are first displayed at.\n * @param   {string}  [prefix]            a prefix to add to all names.\n * @param   {string}  [suffix]            a suffix to add to all names.\n */\n$.oDrawingColumn.prototype.renameAllByFrame = function(prefix, suffix){\n  if (typeof prefix === 'undefined') var prefix = \"\";\n  if (typeof suffix === 'undefined') var suffix = \"\";\n\n  // get exposed drawings\n  var _displayedDrawings = this.getExposedDrawings();\n  this.$.debug(\"Column \"+this.name+\" has drawings : \"+_displayedDrawings.map(function(x){return x.value}), this.$.DEBUG_LEVEL.LOG);\n\n  // remove duplicates\n  var _seen = [];\n  for (var i=0; i<_displayedDrawings.length; i++){\n    var _drawing = _displayedDrawings[i].value;\n\n    if (_seen.indexOf(_drawing.name) == -1){\n      _seen.push(_drawing.name);\n    }else{\n      _displayedDrawings.splice(i,1);\n      i--;\n    }\n  }\n\n  // rename\n  for (var i in _displayedDrawings){\n    var _frameNum = _displayedDrawings[i].frameNumber;\n    var _drawing = _displayedDrawings[i].value;\n    this.$.debug(\"renaming drawing \"+_drawing+\" of column \"+this.name+\" to \"+prefix+_frameNum+suffix, this.$.DEBUG_LEVEL.LOG);\n    _drawing.name = prefix+_frameNum+suffix;\n  }\n}\n\n\n/**\n * Removes unused drawings from the column.\n * @param   {$.oFrame[]}  exposures            The exposures to extend. If UNDEFINED, extends all keyframes.\n */\n$.oDrawingColumn.prototype.removeUnexposedDrawings = function(){\n  var _element = this.element;\n  var _displayedDrawings = this.getExposedDrawings().map(function(x){return x.value.name;});\n  var _element = this.element;\n  var _drawings = _element.drawings;\n\n  for (var i=_drawings.length-1; i>=0; i--){\n    this.$.debug(\"removing drawing \"+_drawings[i].name+\" of column \"+this.name+\"? \"+(_displayedDrawings.indexOf(_drawings[i].name) == -1), this.$.DEBUG_LEVEL.LOG);\n    if (_displayedDrawings.indexOf(_drawings[i].name) == -1) _drawings[i].remove();\n  }\n}\n\n$.oDrawingColumn.prototype.getExposedDrawings = function (){\n  return this.keyframes.filter(function(x){return x.value != null});\n}\n\n\n/**\n * @private\n */\n $.oDrawingColumn.prototype.toString = function(){\n  return \"<$.oDrawingColumn '\"+this.name+\"'>\";\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_database.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oDataBase class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oDataBase.\n * @classdesc\n * A class to access the contents of the Harmony database from a scene.\n * @constructor\n */\n$.oDatabase = function(){\n}\n\n\n/**\n * Function to query the database using the dbu utility.\n * @private\n */\n$.oDatabase.prototype.query = function(args){\n\tvar dbbin = specialFolders.bin+\"/dbu\";\n\tvar p = new $.oProcess(dbbin, args);\n\tvar result = p.execute();\n\n\tresult = result.split(\"Name:\").join(\"\").split(\"\\r\\n\");\n\n\treturn result;\n}\n\n/**\n * Lists the environments existing on the local database\n * @return {string[]}  The list of names of environments\n */\n$.oDatabase.prototype.getEnvironments = function(){\n  var dbFile = new this.$.oFile(\"/USA_DB/envs/env.db\");\n  if (!dbFile.exists){\n    this.$.debug(\"Can't access Harmony Database at address : /USA_DB/envs/env.db\", this.$.DEBUG_LEVEL.ERROR);\n    return null;\n  }\n  var dbqueryArgs = [\"-l\", \"-sh\", \"Name\", dbFile.path];\n\tvar envs = this.query(dbqueryArgs);\n\treturn envs;\n}\n\n\n/**\n * Lists the jobs in the given environment in the local database\n * @param {string}  [environment]    The name of the environment to return the jobs from. Returns the jobs from the current environment by default. \n * @return {string[]}  The list of job names in the environment.\n */\n$.oDatabase.prototype.getJobs = function(environment){\n  if (typeof environment === 'undefined' && this.$.scene.online) {\n    var environment = this.$.scene.environment;\n  }else{\n    return null;\n  }\n\n  var dbFile = new this.$.oFile(\"/USA_DB/online_jobs/jobs.db\");\n  if (!dbFile.exists){\n    this.$.debug(\"Can't access Harmony Database at address : /USA_DB/online_jobs/jobs.db\", this.$.DEBUG_LEVEL.ERROR);\n    return null;\n  }\n\tvar dbqueryArgs = [\"-l\", \"-sh\", \"Name\", \"-search\", \"Env == \"+environment, dbFile.path];\n\tvar jobs = this.query(dbqueryArgs);\n\treturn jobs;\n}\n\n\n/**\n * Lists the scenes in the given environment in the local database\n * @param {string}  [job]    The name of the jobs to return the scenes from. Returns the scenes from the current job by default. \n * @return {string[]}  The list of scene names in the job.\n */\n$.oDatabase.prototype.getScenes = function(job){\n  if (typeof job === 'undefined' && this.$.scene.online){\n    var job = this.$.scene.job;\n  }else{\n    return null;\n  }\n\n  var dbFile = new this.$.oFile(\"/USA_DB/db_jobs/\"+job+\"/scene.db\");\n  if (!dbFile.exists){\n    this.$.debug(\"Can't access Harmony Database at address : /USA_DB/db_jobs/\"+job+\"/scene.db\", this.$.DEBUG_LEVEL.ERROR);\n    return null;\n  }\n  var dbqueryArgs = [\"-l\", \"-sh\", \"Name\", dbFile.path];\n\tvar scenes = this.query(dbqueryArgs);\n\treturn scenes;\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_dialog.js",
    "content": "\"use strict\"\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oDialog class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The base class for the $.oDialog.\n * @classdesc\n * $.oDialog Base Class -- helper class for showing GUI content.\n * @constructor\n */\n$.oDialog = function( ){\n}\n\n\n/**\n * Prompts with a confirmation dialog (yes/no choice).\n * @name    $.oDialog#confirm\n * @function\n * @param   {string}           [labelText]                    The label/internal text of the dialog.\n * @param   {string}           [title]                        The title of the confirmation dialog.\n * @param   {string}           [okButtonText]                 The text on the OK button of the dialog.\n * @param   {string}           [cancelButtonText]             The text on the CANCEL button of the dialog.\n *\n * @return  {bool}       Result of the confirmation dialog.\n */\n\n$.oDialog.prototype.confirm = function( labelText, title, okButtonText, cancelButtonText ){\n  if (this.$.batchMode) {\n    this.$.debug(\"$.oDialog.confirm not supported in batch mode\", this.$.DEBUG_LEVEL.WARNING)\n    return;\n  }\n\n  if (typeof labelText === 'undefined')        var labelText = false;\n  if (typeof title === 'undefined')            var title = \"Confirmation\";\n  if (typeof okButtonText === 'undefined')     var okButtonText = \"OK\";\n  if (typeof cancelButtonText === 'undefined') var cancelButtonText = \"Cancel\";\n\n  var d = new Dialog();\n      d.title            = title;\n      d.okButtonText     = okButtonText;\n      d.cancelButtonText = cancelButtonText;\n\n  if( labelText ){\n    var label = new Label;\n    label.text = labelText;\n  }\n\n  d.add( label );\n\n  if ( !d.exec() ){\n    return false;\n  }\n\n  return true;\n}\n\n\n/**\n * Prompts with an alert dialog (informational).\n * @param   {string}           [labelText]                    The label/internal text of the dialog.\n * @param   {string}           [title]                        The title of the confirmation dialog.\n * @param   {string}           [okButtonText]                 The text on the OK button of the dialog.\n *\n */\n$.oDialog.prototype.alert = function( labelText, title, okButtonText ){\n  if (this.$.batchMode) {\n    this.$.debug(\"$.oDialog.alert not supported in batch mode\", this.$.DEBUG_LEVEL.WARNING)\n    return;\n  }\n\n  if (typeof labelText === 'undefined') var labelText = \"Alert!\";\n  if (typeof title === 'undefined') var title = \"Alert\";\n  if (typeof okButtonText === 'undefined') var okButtonText = \"OK\";\n\n  this.$.debug(labelText, this.$.DEBUG_LEVEL.LOG)\n\n  var d = new QMessageBox( false, title, labelText, QMessageBox.Ok );\n  d.setWindowTitle( title );\n\n  d.buttons()[0].text = okButtonText;\n\n  if( labelText ){\n    d.text = labelText;\n  }\n\n  if ( !d.exec() ){\n    return;\n  }\n}\n\n\n/**\n * Prompts with an alert dialog with a text box which can be selected (informational).\n * @param   {string}           [labelText]                    The label/internal text of the dialog.\n * @param   {string}           [title]                        The title of the confirmation dialog.\n * @param   {string}           [okButtonText=\"OK\"]            The text on the OK button of the dialog.\n * @param   {bool}             [htmlSupport=false]\n */\n$.oDialog.prototype.alertBox = function( labelText, title, okButtonText, htmlSupport){\n  if (this.$.batchMode) {\n    this.$.debug(\"$.oDialog.alert not supported in batch mode\", this.$.DEBUG_LEVEL.WARNING)\n    return;\n  }\n\n  if (typeof labelText === 'undefined') var labelText = \"\";\n  if (typeof title === 'undefined') var title = \"\";\n  if (typeof okButtonText === 'undefined') var okButtonText = \"OK\";\n  if (typeof htmlSupport === 'undefined') var htmlSupport = false;\n\n  this.$.debug(labelText, this.$.DEBUG_LEVEL.LOG)\n\n  var d = new QDialog();\n\n  if (htmlSupport){\n    var label = new QTextEdit(labelText + \"\");\n  }else{\n    var label = new QPlainTextEdit(labelText + \"\");\n  }\n  label.readOnly = true;\n\n  var button = new QPushButton(okButtonText);\n\n  var layout = new QVBoxLayout(d);\n  layout.addWidget(label, 1, Qt.Justify);\n  layout.addWidget(button, 0, Qt.AlignHCenter);\n\n  d.setWindowTitle( title );\n  button.clicked.connect(d.accept);\n\n  d.exec();\n}\n\n\n/**\n * Prompts with an toast alert. This is a small message that can't be clicked and only stays on the screen for the duration specified.\n * @param   {string}         labelText          The label/internal text of the dialog.\n * @param   {$.oPoint}       [position]         The position on the screen where the toast will appear (by default, slightly under the middle of the screen).\n * @param   {float}          [duration=2000]    The duration of the display (in milliseconds).\n * @param   {$.oColorValue}  [color=\"#000000\"]  The color of the background (a 50% alpha value will be applied).\n */\n$.oDialog.prototype.toast = function(labelText, position, duration, color){\n  if (this.$.batchMode) {\n    this.$.debug(\"$.oDialog.alert not supported in batch mode\", this.$.DEBUG_LEVEL.WARNING);\n    return;\n  }\n\n  if (typeof duration === 'undefined') var duration = 2000;\n  if (typeof color === 'undefined') var color = new $.oColorValue(0,0,0);\n  if (typeof position === 'undefined'){\n    var center = QApplication.desktop().screen().rect.center();\n    var position = new $.oPoint(center.x(), center.y()+UiLoader.dpiScale(150))\n  }\n\n  var toast = new QWidget()\n  var flags = new Qt.WindowFlags(Qt.Popup|Qt.FramelessWindowHint|Qt.WA_TransparentForMouseEvents);\n  toast.setWindowFlags(flags);\n  toast.setAttribute(Qt.WA_TranslucentBackground);\n  toast.setAttribute(Qt.WA_DeleteOnClose);\n\n  var styleSheet = \"QWidget {\" +\n  \"background-color: rgba(\"+color.r+\", \"+color.g+\", \"+color.b+\", 50%); \" +\n  \"color: white; \" +\n  \"border-radius: \"+UiLoader.dpiScale(10)+\"px; \" +\n  \"padding: \"+UiLoader.dpiScale(10)+\"px; \" +\n  \"font-family: Arial; \" +\n  \"font-size: \"+UiLoader.dpiScale(12)+\"pt;}\"\n\n  toast.setStyleSheet(styleSheet);\n\n  var layout = new QHBoxLayout(toast);\n  layout.addWidget(new QLabel(labelText), 0, Qt.AlignHCenter);\n\n  var timer = new QTimer()\n  timer.singleShot = true;\n  timer.timeout.connect(this, function(){\n    toast.close();\n  })\n\n  toast.show();\n\n  toast.move(position.x-toast.width/2, position.y);\n\n  timer.start(duration);\n}\n\n\n/**\n * Prompts for a user input.\n * @param   {string}           [labelText]                    The label/internal text of the dialog.\n * @param   {string}           [title]                        The title of the confirmation dialog.\n * @param   {string}           [prefilledText]                The text to display in the input area.\n *\n */\n$.oDialog.prototype.prompt = function( labelText, title, prefilledText){\n  if (typeof labelText === 'undefined') var labelText = \"enter value :\";\n  if (typeof title === 'undefined') var title = \"Prompt\";\n  if (typeof prefilledText === 'undefined') var prefilledText = \"\";\n  return Input.getText(labelText, prefilledText, title);\n}\n\n\n/**\n * Prompts with a file selector window\n * @param   {string}           [text=\"Select a file:\"]       The title of the confirmation dialog.\n * @param   {string}           [filter=\"*\"]                  The filter for the file type and/or file name that can be selected. Accepts wildcard character \"*\".\n * @param   {string}           [getExisting=true]            Whether to select an existing file or a save location\n * @param   {string}           [acceptMultiple=false]        Whether or not selecting more than one file is ok. Is ignored if getExisting is falses.\n * @param   {string}           [startDirectory]              The directory showed at the opening of the dialog.\n *\n * @return  {string[]}         The list of selected Files, 'undefined' if the dialog is cancelled\n */\n$.oDialog.prototype.browseForFile = function( text, filter, getExisting, acceptMultiple, startDirectory){\n  if (this.$.batchMode) {\n    this.$.debug(\"$.oDialog.browseForFile not supported in batch mode\", this.$.DEBUG_LEVEL.WARNING)\n    return;\n  }\n\n  if (typeof title === 'undefined') var title = \"Select a file:\";\n  if (typeof filter === 'undefined') var filter = \"*\"\n  if (typeof getExisting === 'undefined') var getExisting = true;\n  if (typeof acceptMultiple === 'undefined') var acceptMultiple = false;\n\n\n  if (getExisting){\n    if (acceptMultiple){\n      var _files = QFileDialog.getOpenFileNames(0, text, startDirectory, filter);\n    }else{\n      var _files = QFileDialog.getOpenFileName(0, text, startDirectory, filter);\n    }\n  }else{\n    var _files = QFileDialog.getSaveFileName(0, text, startDirectory, filter);\n  }\n\n  for (var i in _files){\n    _files[i] = _files[i].replace(/\\\\/g, \"/\");\n  }\n\n  this.$.debug(_files);\n  return _files;\n}\n\n\n/**\n * Prompts with a browse for folder dialog (informational).\n * @param   {string}           [text]                        The title of the confirmation dialog.\n * @param   {string}           [startDirectory]              The directory showed at the opening of the dialog.\n *\n * @return  {string}           The path of the selected folder, 'undefined' if the dialog is cancelled\n */\n$.oDialog.prototype.browseForFolder = function(text, startDirectory){\n  if (this.$.batchMode) {\n    this.$.debug(\"$.oDialog.browseForFolder not supported in batch mode\", this.$.DEBUG_LEVEL.WARNING)\n    return;\n  }\n\n  if (typeof title === 'undefined') var title = \"Select a folder:\";\n\n  var _folder = QFileDialog.getExistingDirectory(0, text, startDirectory);\n  _folder = _folder.split(\"\\\\\").join(\"/\");\n  // this.$.alert(_folder)\n  return _folder;\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//     $.oProgressDialog class      //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The $.oProgressDialog constructor.\n * @name        $.oProgressDialog\n * @constructor\n * @classdesc   An simple progress dialog to display the progress of a task.\n * To react to the user clicking the cancel button, connect a function to $.oProgressDialog.canceled() signal.\n * When $.batchmode is true, the progress will be outputted as a \"Progress : value/range\" string to the Harmony stdout.\n * @param       {string}              [labelText]                The text displayed above the progress bar.\n * @param       {string}              [range=100]                The maximum value that represents a full progress bar.\n * @param       {string}              [title]                    The title of the dialog\n * @param       {bool}                [show=false]               Whether to immediately show the dialog.\n *\n * @property    {bool}                wasCanceled                Whether the progress bar was cancelled.\n * @property    {$.oSignal}           canceled                   A Signal emitted when the dialog is canceled. Can be connected to a callback.\n */\n$.oProgressDialog = function( labelText, range, title, show ){\n  if (typeof title === 'undefined') var title = \"Progress\";\n  if (typeof range === 'undefined') var range = 100;\n  if (typeof labelText === 'undefined') var labelText = \"\";\n\n  this._value = 0;\n  this._range = range;\n  this._title = title;\n  this._labelText = labelText;\n\n  this.canceled = new this.$.oSignal();\n  this.wasCanceled = false;\n\n  if (!this.$.batchMode) {\n    this.progress = new QProgressDialog();\n    this.progress.title = this._title;\n    this.progress.setLabelText( this._labelText );\n    this.progress.setRange( 0, this._range );\n    this.progress.setWindowFlags(Qt.Popup|Qt.WindowStaysOnTopHint)\n\n    this.progress[\"canceled()\"].connect( this, function(){this.wasCanceled = true; this.canceled.emit()} );\n\n    if (show) this.show();\n  }\n}\n\n\n// legacy compatibility\n$.oDialog.Progress = $.oProgressDialog;\n\n\n/**\n * The text displayed by the window.\n * @name $.oProgressDialog#label\n * @type {string}\n */\nObject.defineProperty( $.oProgressDialog.prototype, 'label', {\n  get: function(){\n    return this._labelText;\n  },\n  set: function( val ){\n    this._labelText = val;\n    if (!this.$.batchMode) this.progress.setLabelText( val );\n  }\n});\n\n\n/**\n * The maximum value that can be displayed by the progress dialog (equivalent to \"finished\")\n * @name $.oProgressDialog#range\n * @type {int}\n */\nObject.defineProperty( $.oProgressDialog.prototype, 'range', {\n    get: function(){\n      return this._range;\n    },\n    set: function( val ){\n      this._range = val;\n      if (!this.$.batchMode) this.progress.setRange( 0, val );\n    }\n});\n\n\n/**\n * The current value of the progress bar. Setting this to the value of 'range' will close the dialog.\n * @name $.oProgressDialog#value\n * @type {int}\n */\nObject.defineProperty( $.oProgressDialog.prototype, 'value', {\n    get: function(){\n      return this._value;\n    },\n    set: function( val ){\n      if (val > this.range) val = this.range;\n      this._value = val;\n      if (this.$.batchMode) {\n        this.$.log(\"Progress : \"+val+\"/\"+this._range)\n      }else {\n        this.progress.value = val;\n      }\n\n      // update the widget appearance\n      QCoreApplication.processEvents();\n    }\n});\n\n\n/**\n * Whether the Progress Dialog was cancelled by the user.\n * @name $.oProgressDialog#cancelled\n * @deprecated use $.oProgressDialog.wasCanceled to get the cancel status, or connect a function to the \"canceled\" signal.\n */\nObject.defineProperty( $.oProgressDialog.prototype, 'cancelled', {\n  get: function(){\n    return this.wasCanceled;\n  }\n});\n\n\n// oProgressDialog Class Methods\n\n/**\n * Shows the dialog.\n */\n$.oProgressDialog.prototype.show = function(){\n  if (this.$.batchMode) {\n    this.$.debug(\"$.oProgressDialog not supported in batch mode\", this.$.DEBUG_LEVEL.ERROR)\n    return;\n  }\n\n  this.progress.show();\n}\n\n/**\n * Closes the dialog.\n */\n$.oProgressDialog.prototype.close = function(){\n  this.value = this.range;\n  this.$.log(\"Progress : \"+this.value+\"/\"+this._range)\n\n  if (this.$.batchMode) {\n    this.$.debug(\"$.oProgressDialog not supported in batch mode\", this.$.DEBUG_LEVEL.ERROR)\n    return;\n  }\n\n  this.canceled.blocked = true;\n  this.progress.close();\n  this.canceled.blocked = false;\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oPieMenu class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The $.oPieMenu constructor.\n * @name        $.oPieMenu\n * @constructor\n * @classdesc   A type of menu with nested levels that appear around the mouse\n * @param       {string}              name                    The name for this pie Menu.\n * @param       {QWidget[]}           [widgets]               The widgets to display in the menu.\n * @param       {bool}                [show=false]            Whether to immediately show the dialog.\n * @param       {float}               [minAngle]              The low limit of the range of angles used by the menu, in multiples of PI (0 : left, 0.5 : top, 1 : right, -0.5 : bottom)\n * @param       {float}               [maxAngle]              The high limit of the  range of angles used by the menu, in multiples of PI (0 : left, 0.5 : top, 1 : right, -0.5 : bottom)\n * @param       {float}               [radius]                The radius of the menu.\n * @param       {$.oPoint}            [position]              The central position of the menu.\n *\n * @property    {string}              name                    The name for this pie Menu.\n * @property    {QWidget[]}           widgets                 The widgets to display in the menu.\n * @property    {float}               minAngle                The low limit of the range of angles used by the menu, in multiples of PI (0 : left, 0.5 : top, 1 : right, -0.5 : bottom)\n * @property    {float}               maxAngle                The high limit of the  range of angles used by the menu, in multiples of PI (0 : left, 0.5 : top, 1 : right, -0.5 : bottom)\n * @property    {float}               radius                  The radius of the menu.\n * @property    {$.oPoint}            position                The central position of the menu or button position for imbricated menus.\n * @property    {QWidget}             menuWidget              The central position of the menu or button position for imbricated menus.\n * @property    {QColor}              sliceColor              The color of the slices. Can set to any fill type accepted by QBrush\n * @property    {QColor}              backgroundColor         The background of the menu. Can set to any fill type accepted by QBrush\n * @property    {QColor}              linesColor              The color of the lines.\n * @example\n// This example function creates a menu full of generated push buttons with callbacks, but any type of widget can be added.\n// Normally it doesn't make sense to create buttons this way, and they will be created one by one to cater to specific needs,\n// such as launching Harmony actions, or scripts, etc. Assign this function to a shortcut by creating a Harmony Package for it.\n\nfunction openMenu(){\n  MessageLog.clearLog()\n\n  // we create a list of tool widgets for our submenu\n  var toolSubMenuWidgets = [\n    new $.oToolButton(\"select\"),\n    new $.oToolButton(\"brush\"),\n    new $.oToolButton(\"pencil\"),\n    new $.oToolButton(\"eraser\"),\n  ];\n  // we initialise our submenu\n  var toolSubMenu = new $.oPieSubMenu(\"tools\", toolSubMenuWidgets);\n\n  // we create a list of tool widgets for our submenu\n  // (check out the scripts from http://raindropmoment.com and http://www.cartoonflow.com, they are great!)\n  var ScriptSubMenuWidgets = [\n    new $.oScriptButton(specialFolders.userScripts + \"/CF_CopyPastePivots_1.0.1.js\", \"CF_CopyPastePivots\" ),\n    new $.oScriptButton(specialFolders.userScripts + \"/ANM_Paste_In_Place.js\", \"ANM_Paste_In_Place\"),\n    new $.oScriptButton(specialFolders.userScripts + \"/ANM_Set_Layer_Pivots_At_Center_Of_Drawings.js\", \"ANM_Set_Layer_Pivots_At_Center_Of_Drawings\"),\n    new $.oScriptButton(specialFolders.userScripts + \"/DEF_Copy_Deformation_Values_To_Resting.js\", \"DEF_Copy_Deformation_Values_To_Resting\"),\n  ];\n  var scriptsSubMenu = new $.oPieSubMenu(\"scripts\", ScriptSubMenuWidgets);\n\n  // we create a list of color widgets for our submenu\n  var colorSubMenuWidgets = []\n  var currentPalette = $.scn.selectedPalette\n  var colors = currentPalette.colors\n  for (var i in colors){\n    colorSubMenuWidgets.push(new $.oColorButton(currentPalette.name, colors[i].name));\n  }\n  var colorSubMenu = new $.oPieSubMenu(\"colors\", colorSubMenuWidgets);\n\n  onionSkinSlider = new QSlider(Qt.Horizontal)\n  onionSkinSlider.minimum = 0;\n\tonionSkinSlider.maximum = 256;\n  onionSkinSlider.valueChanged.connect(function(value){\n    preferences.setDouble(\"DRAWING_ONIONSKIN_MAX_OPACITY\",\n      value/256.0);\n    view.refreshViews();\n  })\n\n  // widgets that will appear in the main menu\n  var mainWidgets = [\n    onionSkinSlider,\n    toolSubMenu,\n    colorSubMenu,\n    scriptsSubMenu\n  ]\n\n  // we initialise our main menu. The numerical values are for the minimum and maximum angle of the\n  // circle in multiples of Pi. Going clockwise, 0 is right, 1 is left, -0.5 is the bottom from the right,\n  // and 1.5 is the bottom from the left side. 0.5 is the top of the circle.\n  var menu = new $.oPieMenu(\"menu\", mainWidgets, false, -0.2, 1.2);\n\n  // configurating the look of it\n  // var backgroundGradient = new QRadialGradient (menu.center, menu.maxRadius);\n  // var menuBg = menu.backgroundColor\n  // backgroundGradient.setColorAt(1, new QColor(menuBg.red(), menuBg.green(), menuBg.blue(), 255));\n  // backgroundGradient.setColorAt(0, menuBg);\n\n  // var sliceGradient = new QRadialGradient (menu.center, menu.maxRadius);\n  // var menuColor = menu.sliceColor\n  // sliceGradient.setColorAt(1, new QColor(menuColor.red(), menuColor.green(), menuColor.blue(), 20));\n  // sliceGradient.setColorAt(0, menuColor);\n\n  // menu.backgroundColor = backgroundGradient\n  // menu.sliceColor = sliceGradient\n\n  // we show it!\n  menu.show();\n}*/\n$.oPieMenu = function( name, widgets, show, minAngle, maxAngle, radius, position, parent){\n  this.name = name;\n  this.widgets = widgets;\n\n  if (typeof minAngle === 'undefined') var minAngle = 0;\n  if (typeof maxAngle === 'undefined') var maxAngle = 1;\n  if (typeof radius === 'undefined') var radius = this.getMenuRadius();\n  if (typeof position === 'undefined') var position = this.$.app.globalMousePosition;\n  if (typeof show === 'undefined') var show = false;\n  if (typeof parent === 'undefined') var parent = this.$.app.mainWindow;\n  this._parent = parent;\n\n  // close all previously opened piemenu widgets\n  if (!$._piemenu) $._piemenu = []\n  while ($._piemenu.length){\n    var pie = $._piemenu.pop();\n    if (pie){\n      // a menu was already open, we close it\n      pie.closeMenu()\n    }\n  }\n\n  QWidget.call(this, parent)\n  this.objectName = \"pieMenu_\" + name;\n  $._piemenu.push(this)\n\n  this.radius = radius;\n  this.minAngle = minAngle;\n  this.maxAngle = maxAngle;\n  this.globalCenter = position;\n\n  // how wide outside the icons is the slice drawn\n  this._circleMargin = 30;\n\n  // set these values before calling show() to customize the menu appearance\n  this.sliceColor = new QColor(0, 200, 255, 200);\n  this.backgroundColor = new QColor(40, 40, 40, 180);\n  this.linesColor = new QColor(0,0,0,0);\n\n  // create main button\n  this.button = this.buildButton()\n\n  // add buildWidget call before show(),\n  // for some reason show() is not in QWidget.prototype ?\n  this.qWidgetShow = this.show\n  this.show = function(){\n    this.buildWidget()\n  }\n\n  this.focusPolicy = Qt.StrongFocus;\n  this.focusOutEvent = function(){\n    this.deactivate()\n  }\n\n  var menu = this;\n  this.button.clicked.connect(function(){return menu.deactivate()})\n\n  if (show) this.show();\n}\n$.oPieMenu.prototype = Object.create(QWidget.prototype);\n\n\n/**\n * function run when the menu button is clicked\n */\n$.oPieMenu.prototype.deactivate = function(){\n  this.closeMenu()\n}\n\n/**\n * Closes the menu and all its subWidgets\n * @private\n */\n$.oPieMenu.prototype.closeMenu = function(){\n  for (var i in this.widgets){\n    this.widgets[i].close()\n  }\n  this.close();\n}\n\n/**\n * The top left point of the entire widget\n * @name $.oPieMenu#anchor\n * @type {$.oPoint}\n */\nObject.defineProperty($.oPieMenu.prototype, \"anchor\", {\n  get: function(){\n    var point = this.globalCenter.add(-this.center.x, -this.center.y);\n    return point;\n  }\n})\n\n\n/**\n * The center of the entire widget\n * @name $.oPieMenu#center\n * @type {$.oPoint}\n */\nObject.defineProperty($.oPieMenu.prototype, \"center\", {\n  get: function(){\n    return new this.$.oPoint(this.widgetSize/2, this.widgetSize/2)\n  }\n})\n\n\n/**\n * The min radius of the pie background\n * @name $.oPieMenu#minRadius\n * @type {int}\n */\nObject.defineProperty($.oPieMenu.prototype, \"minRadius\", {\n  get: function(){\n    return this._circleMargin;\n  }\n})\n\n\n/**\n * The max radius of the pie background\n * @name $.oPieMenu#maxRadius\n * @type {int}\n */\nObject.defineProperty($.oPieMenu.prototype, \"maxRadius\", {\n  get: function(){\n    return this.radius + this._circleMargin;\n  }\n})\n\n/**\n * The widget size of the pie background (it's a square so it's both the width and the height.)\n * @name $.oPieMenu#widgetSize\n * @type {int}\n */\n Object.defineProperty($.oPieMenu.prototype, \"widgetSize\", {\n  get: function(){\n    return this.maxRadius*4;\n  }\n})\n\n\n/**\n * Builds the menu's main button.\n * @returns {$.oPieButton}\n */\n$.oPieMenu.prototype.buildButton = function(){\n  // add main button in constructor because it needs to exist before show()\n  var icon = specialFolders.resource + \"/icons/brushpreset/defaultpresetellipse/ellipse03.svg\"\n  button = new this.$.oPieButton(icon, \"\", this);\n  button.objectName = this.name+\"_button\";\n  button.parentMenu = this;\n\n  return button;\n}\n\n/**\n * Build and show the pie menu and its widgets.\n * @private\n */\n$.oPieMenu.prototype.buildWidget = function(){\n  // match the widget geometry with the main window/parent\n  var anchor = this.anchor\n  this.move(anchor.x, anchor.y);\n  this.minimumHeight = this.maximumHeight = this.widgetSize;\n  this.minimumWidth = this.maximumWidth = this.widgetSize;\n\n  var flags = new Qt.WindowFlags(Qt.Popup|Qt.FramelessWindowHint|Qt.WA_TransparentForMouseEvents);\n  this.setWindowFlags(flags);\n  this.setAttribute(Qt.WA_TranslucentBackground);\n  this.setAttribute(Qt.WA_DeleteOnClose);\n\n  // draw background pie slice\n  this.slice = this.drawSlice();\n  this.qWidgetShow()\n  // arrange widgets into half a circle around the center\n  var center = this.center;\n\n  for (var i=0; i < this.widgets.length; i++){\n    var widget = this.widgets[i];\n    widget.pieIndex = i;\n    widget.setParent(this);\n\n    var itemPosition = this.getItemPosition(i);\n    var widgetPosition = new this.$.oPoint(center.x + itemPosition.x, center.y + itemPosition.y);\n\n    widget.show();\n    widget.move(widgetPosition.x - widget.width/2, widgetPosition.y - widget.height/2);\n  }\n\n  this.button.show();\n  this.button.move(center.x - (this.button.width/2), center.y - (this.button.height/2));\n}\n\n\n/**\n * draws a background transparent slice and set up the mouse tracking.\n * @param {int}   [minRadius]      specify a minimum radius for the slice\n * @private\n */\n$.oPieMenu.prototype.drawSlice = function(){\n  var index = 0;\n\n  // get the slice and background geometry\n  var center = this.center;\n  var angleSlice = this.getItemAngleRange(index);\n  var slicePath = this.getSlicePath(center, angleSlice[0], angleSlice[1], this.minRadius, this.maxRadius);\n  var contactPath = this.getSlicePath(center, this.minAngle, this.maxAngle, this.minRadius, this.maxRadius);\n\n  // create a widget to paint into\n  var sliceWidget = new QWidget(this);\n  sliceWidget.objectName = \"slice\";\n  // make widget background invisible\n  sliceWidget.setStyleSheet(\"background-color: rgba(0, 0, 0, 0.5%);\");\n  var flags = new Qt.WindowFlags(Qt.FramelessWindowHint);\n  sliceWidget.setWindowFlags(flags)\n  sliceWidget.minimumHeight = this.height;\n  sliceWidget.minimumWidth = this.width;\n  sliceWidget.lower();\n\n  var sliceWidth = angleSlice[1]-angleSlice[0];\n\n  // painting the slice on sliceWidget.update()\n  var sliceColor = this.sliceColor;\n  var backgroundColor = this.backgroundColor;\n  var linesColor = this.linesColor;\n\n  sliceWidget.paintEvent = function(){\n    var painter = new QPainter();\n    painter.save();\n    painter.begin(sliceWidget);\n\n    // draw background\n    painter.setRenderHint(QPainter.Antialiasing);\n    painter.setPen(new QPen(linesColor));\n    painter.setBrush(new QBrush(backgroundColor));\n\n    painter.drawPath(contactPath);\n\n    // draw slice and rotate around widget center\n    painter.translate(center.x, center.y);\n    painter.rotate(sliceWidth*index*(-180));\n    painter.translate(-center.x, -center.y);\n    painter.setPen(new QPen(linesColor));\n    painter.setBrush(new QBrush(sliceColor));\n    painter.drawPath(slicePath);\n    painter.end();\n    painter.restore();\n  }\n\n  //set up automatic following of the mouse\n  sliceWidget.mouseTracking = true;\n\n  var pieMenu = this;\n  var currentDistance = false;\n  sliceWidget.mouseMoveEvent = function(mousePos){\n    // work out the index based on relative position to the center\n    var position = new pieMenu.$.oPoint(mousePos.x(), mousePos.y());\n    var angle = -position.add(-center.x, -center.y).polarCoordinates.angle/Math.PI;\n    if (angle < (-0.5)) angle += 2; // our coordinates system uses continuous angle values with cutoff at the bottom (1.5/-0.5)\n    var currentIndex = pieMenu.getIndexAtAngle(angle);\n    var distance = position.distance(center);\n\n    // on distance value change, if the distance is greater than the maxRadius, activate the widget\n    var indexChanged = (index != currentIndex)\n    var indexWithinRange = (currentIndex >= 0 && currentIndex < pieMenu.widgets.length)\n    var distanceWithinRange = (distance > pieMenu.minRadius && distance < pieMenu.maxRadius)\n    var distanceChanged = (distanceWithinRange != currentDistance)\n\n    // react to distance/angle change when the mouse moves on the pieMenu\n    if (indexWithinRange){\n      var indexWidget = pieMenu.widgets[currentIndex];\n\n      if (indexChanged && distance < pieMenu.maxRadius){\n        index = currentIndex;\n        sliceWidget.update();\n        indexWidget.setFocus(true);\n      }\n\n      if (distanceChanged){\n        currentDistance = distanceWithinRange;\n        if (distance > pieMenu.maxRadius){\n          // activate the button\n          if (indexWidget.activate) indexWidget.activate();\n        }else if (distance < pieMenu.minRadius){\n          // cursor reentered the widget: close the subMenu\n          if (indexWidget.deactivate) indexWidget.deactivate();\n        }\n        if (distance < pieMenu.minRadius){\n          if (pieMenu.deactivate) pieMenu.deactivate();\n        }\n      }\n    }\n  }\n\n  return sliceWidget;\n}\n\n\n/**\n * Generate a pie slice path to draw based on parameters\n * @param {$.oPoint}    center      the center of the slice\n * @param {float}       minAngle    a value between -0.5 and 1.5 for the lowest angle value for the pie slice\n * @param {float}       maxAngle    a value between -0.5 and 1.5 for the highest angle value for the pie slice\n * @param {float}       minRadius   the smallest circle radius\n * @param {float}       maxRadius   the largest circle radius\n * @private\n */\n$.oPieMenu.prototype.getSlicePath = function(center, minAngle, maxAngle, minRadius, maxRadius){\n  // work out the geometry\n  var smallArcBoundingBox = new QRectF(center.x-minRadius, center.y-minRadius, minRadius*2, minRadius*2);\n  var smallArcStart = new this.$.oPoint();\n  smallArcStart.polarCoordinates = {radius: minRadius, angle:minAngle*(-Math.PI)}\n  smallArcStart.translate(center.x, center.y);\n  var smallArcAngleStart = minAngle*180;\n  var smallArcSweep = (maxAngle-minAngle)*180; // convert values from 0-2 (radiant angles in multiples of pi) to degrees\n\n  var bigArcBoundingBox = new QRectF(center.x-maxRadius, center.y-maxRadius, maxRadius*2, maxRadius*2);\n  var bigArcAngleStart = maxAngle*180;\n  var bigArcSweep = -smallArcSweep;\n\n  // we draw the slice path\n  var slicePath = new QPainterPath;\n  slicePath.moveTo(new QPointF(smallArcStart.x, smallArcStart.y));\n  slicePath.arcTo(smallArcBoundingBox, smallArcAngleStart, smallArcSweep);\n  slicePath.arcTo(bigArcBoundingBox, bigArcAngleStart, bigArcSweep);\n\n  return slicePath;\n}\n\n\n/**\n * Get the angle range for the item pie slice based on index.\n * @private\n * @param {int}     index         the index of the widget\n * @return {float[]}\n */\n$.oPieMenu.prototype.getItemAngleRange = function(index){\n  var length = this.widgets.length;\n  var angleStart = this.minAngle+(index/length)*(this.maxAngle-this.minAngle);\n  var angleEnd = this.minAngle+((index+1)/length)*(this.maxAngle-this.minAngle);\n\n  return [angleStart, angleEnd];\n}\n\n/**\n * Get the angle for the item widget based on index.\n * @private\n * @param {int}     index         the index of the widget\n * @return {float}\n */\n$.oPieMenu.prototype.getItemAngle = function(index){\n  var angleRange = this.getItemAngleRange(index, this.minAngle, this.maxAngle);\n  var angle = (angleRange[1] - angleRange[0])/2+angleRange[0]\n\n  return angle;\n}\n\n\n/**\n * Get the widget index for the angle value.\n * @private\n * @param {float}     angle         the index of the widget\n * @return {float}\n */\n$.oPieMenu.prototype.getIndexAtAngle = function(angle){\n  var angleRange = (this.maxAngle-this.minAngle)/this.widgets.length\n  return Math.floor((angle-this.minAngle)/angleRange);\n}\n\n\n/**\n * Get the position from the center for the item based on index.\n * @private\n * @param {int}     index         the index of the widget\n * @return {$.oPoint}\n */\n$.oPieMenu.prototype.getItemPosition = function(index){\n  // we add pi to the angle because of the inverted Y axis of widgets coordinates\n  var pi = Math.PI;\n  var angle = this.getItemAngle(index, this.minAngle, this.maxAngle)*(-pi);\n  var _point = new this.$.oPoint();\n  _point.polarCoordinates = {radius:this.radius, angle:angle}\n\n  return _point;\n}\n\n\n/**\n * Get a pie menu radius setting for a given amount of items.\n * @private\n * @return {float}\n */\n$.oPieMenu.prototype.getMenuRadius = function(){\n  var itemsNumber = this.widgets.length\n  var _maxRadius = UiLoader.dpiScale(200);\n  var _minRadius = UiLoader.dpiScale(30);\n  var _speed = 10; // the higher the value, the slower the progression\n\n  // hyperbolic tangent function to determine the radius\n  var exp = Math.exp(2*itemsNumber/_speed);\n  var _radius = ((exp-1)/(exp+1))*_maxRadius+_minRadius;\n\n  return _radius;\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//       $.oPieSubMenu class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The $.oPieSubMenu constructor.\n * @name        $.oPieSubMenu\n * @constructor\n * @classdesc   A menu with more options that opens/closes when the user clicks on the button.\n * @param       {string}              name                     The name for this pie Menu.\n * @param       {QWidget[]}           [widgets]                The widgets to display in the menu.\n *\n * @property    {string}              name                     The name for this pie Menu.\n * @property    {string}              widgets                  The widgets to display in the menu.\n * @property    {string}              menu                     The oPieMenu Object containing the widgets for the submenu\n * @property    {string}              itemAngle                a set angle for each items instead of spreading them across the entire circle\n * @property    {string}              extraRadius              using a set radius between each submenu levels\n * @property    {$.oPieMenu}          parentMenu               the parent menu for this subMenu. Set during initialisation of the menu.\n */\n$.oPieSubMenu = function(name, widgets) {\n  this.menuIcon = specialFolders.resource + \"/icons/toolbar/menu.svg\";\n  this.closeIcon = specialFolders.resource + \"/icons/toolbar/collapseopen.png\";\n\n  // min/max angle and radius will be set from parent during buildWidget()\n  this.$.oPieMenu.call(this, name, widgets, false);\n\n  // change these settings before calling show() to modify the look of the pieSubMenu\n  this.itemAngle = 0.06;\n  this.extraRadius = UiLoader.dpiScale(80);\n  this.parentMenu = undefined;\n\n  this.focusOutEvent = function(){} // delete focusOutEvent response from submenu\n}\n$.oPieSubMenu.prototype = Object.create($.oPieMenu.prototype)\n\n\n/**\n * function called when main button is clicked\n */\n$.oPieSubMenu.prototype.deactivate = function(){\n  this.toggleMenu()\n}\n\n/**\n * The top left point of the entire widget\n * @name $.oPieSubMenu#anchor\n * @type {$.oPoint}\n */\nObject.defineProperty($.oPieSubMenu.prototype, \"anchor\", {\n  get: function(){\n    var center = this.parentMenu.globalCenter;\n    return center.add(-this.widgetSize/2, -this.widgetSize/2);\n  }\n})\n\n\n/**\n * The min radius of the pie background\n * @name $.oPieSubMenu#minRadius\n * @type {int}\n */\nObject.defineProperty($.oPieSubMenu.prototype, \"minRadius\", {\n  get: function(){\n    return this.parentMenu.maxRadius;\n  }\n})\n\n\n/**\n * The max radius of the pie background\n * @name $.oPieSubMenu#maxRadius\n * @type {int}\n */\nObject.defineProperty($.oPieSubMenu.prototype, \"maxRadius\", {\n  get: function(){\n    return this.minRadius + this.extraRadius;\n  }\n})\n\n\n/**\n * activate the menu button when activate() is called on the menu\n * @private\n */\n$.oPieSubMenu.prototype.activate = function(){\n  this.showMenu(true);\n  this.setFocus(true)\n}\n\n\n/**\n * In order for pieSubMenus to behave like other pie widgets, we reimplement\n * move() so that it only moves the button, and the slice will remain aligned with\n * the parent.\n * @param  {int}      x     The x coordinate for the button relative to the piewidget\n * @param  {int}      y     The x coordinate for the button relative to the piewidget\n * @private\n */\n$.oPieSubMenu.prototype.move = function(x, y){\n  // move the actual widget to its anchor, but move the button instead\n  QWidget.prototype.move.call(this, this.anchor.x, this.anchor.y);\n\n  // calculate the actual position for the button as if it was a child of the pieMenu\n  // whereas it uses global coordinates\n  var buttonPos = new this.$.oPoint(x, y)\n  var parentAnchor = this.parentMenu.anchor;\n  var anchorDiff = parentAnchor.add(-this.anchor.x, -this.anchor.y)\n  var localPos = buttonPos.add(anchorDiff.x, anchorDiff.y)\n\n  // move() is used by the pieMenu with half the widget size to center the button, so we have to cancel it out\n  this.button.move(localPos.x+this.widgetSize/2-this.button.width/2, localPos.y+this.widgetSize/2-this.button.height/2 );\n}\n\n\n/**\n * sets a parent and assigns it to this.parentMenu.\n * using the normal setParent from QPushButton creates a weird bug\n * where calling parent() returns a QWidget and not a $.oPieButton\n * @private\n */\n$.oPieSubMenu.prototype.setParent = function(parent){\n  $.oPieMenu.prototype.setParent.call(this, parent);\n  this.parentMenu = parent;\n}\n\n\n/**\n * build the main button for the menu\n * @private\n * @returns {$.oPieButton}\n */\n$.oPieSubMenu.prototype.buildButton = function(){\n  // add main button in constructor because it needs to exist before show()\n  var button = new this.$.oPieButton(this.menuIcon, this.name, this);\n  button.objectName = this.name+\"_button\";\n\n  return button;\n}\n\n\n/**\n * Shows or hides the menu itself (not the button)\n * @param {*} visibility\n */\n$.oPieSubMenu.prototype.showMenu = function(visibility){\n  this.slice.visible = visibility;\n  for (var i in this.widgets){\n    this.widgets[i].visible = visibility;\n  }\n  var icon = visibility?this.closeIcon:this.menuIcon;\n  UiLoader.setSvgIcon(this.button, icon);\n}\n\n\n/**\n * toggles the display of the menu\n */\n$.oPieSubMenu.prototype.toggleMenu = function(){\n  this.showMenu(!this.slice.visible);\n}\n\n/**\n * Function to initialise the widgets for the submenu\n * @private\n */\n$.oPieSubMenu.prototype.buildWidget = function(){\n  if (!this.parentMenu){\n    throw new Error(\"must set parent first before calling $.oPieMenu.buildWidget()\")\n  }\n  parentWidget = this.parentMenu;\n\n  // submenu widgets calculate their range from to go on both sides of the button, at a fixed angle\n  // (in order to keep the span of submenu options centered around the menu button)\n  var widgetNum = this.widgets.length/2;\n  var angle = parentWidget.getItemAngle(this.pieIndex);\n\n  // create the submenu on top of the main menu\n  this.radius = parentWidget.radius+this.extraRadius;\n  this.minAngle = angle-widgetNum*this.itemAngle;\n  this.maxAngle = angle+widgetNum*this.itemAngle;\n\n  $.oPieMenu.prototype.buildWidget.call(this);\n\n  this.showMenu(false)\n}\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oPieButton class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for $.oPieButton\n * @constructor\n * @classdesc This subclass of QPushButton provides an easy way to create a button for a PieMenu.<br>\n *\n * This class is a subclass of QPushButton and all the methods from that class are available to modify this button.\n * @param {string}   iconFile               The icon file for the button\n * @param {string}   text                   A text to display next to the icon\n * @param {QWidget}  parent                 The parent QWidget for the button. Automatically set during initialisation of the menu.\n *\n */\n $.oPieButton = function(iconFile, text, parent) {\n  // if icon isnt provided\n  if (typeof parent === 'undefined') var parent = $.app.mainWindow\n  if (typeof text === 'undefined') var text = \"\"\n  if (typeof iconFile === 'undefined') var iconFile = specialFolders.resource+\"/icons/script/qtgeneric.svg\"\n\n  QPushButton.call(this, text, parent);\n\n  this.minimumHeight = 24;\n  this.minimumWidth = 24;\n\n  // set during addition to the pie Menu\n  this.pieIndex = undefined;\n\n  UiLoader.setSvgIcon(this, iconFile)\n  this.setIconSize(new QSize(this.minimumWidth, this.minimumHeight));\n  this.cursor = new QCursor(Qt.PointingHandCursor);\n\n  var styleSheet = \"QPushButton{ background-color: rgba(0, 0, 0, 1%); }\" +\n  \"QPushButton:hover{ background-color: rgba(0, 200, 255, 80%); }\"+\n  \"QToolTip{ background-color: rgba(0, 255, 255, 100%); }\"\n  this.setStyleSheet(styleSheet);\n\n  var button = this;\n  this.clicked.connect(function(){button.activate()})\n}\n$.oPieButton.prototype = Object.create(QPushButton.prototype);\n\n\n/**\n * Closes the parent menu of the button and all its subWidgets.\n */\n$.oPieButton.prototype.closeMenu = function(){\n  var menu = this.parentMenu;\n  while (menu && menu.parentMenu){\n    menu = menu.parentMenu;\n  }\n  menu.closeMenu()\n}\n\n/**\n * Reimplement this function in order to activate the button and also close the menu.\n */\n$.oPieButton.prototype.activate = function(){\n  // reimplement to change the behavior when the button is activated.\n  // by default, will just close the menu.\n  this.closeMenu();\n}\n\n\n/**\n * sets a parent and assigns it to this.parentMenu.\n * using the normal setParent from QPushButton creates a weird bug\n * where calling parent() returns a QWidget and not a $.oPieButton\n * @private\n */\n$.oPieButton.prototype.setParent = function(parent){\n  QPushButton.prototype.setParent.call(this, parent);\n  this.parentMenu = parent;\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//      $.oToolButton class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for $.oToolButton\n * @name          $.oToolButton\n * @constructor\n * @classdescription This subclass of QPushButton provides an easy way to create a button for a tool.\n * This class is a subclass of QPushButton and all the methods from that class are available to modify this button.\n * @param {string}   toolName               The path to the script file that will be launched\n * @param {string}   scriptFunction           The function name to launch from the script\n * @param {QWidget}  parent                   The parent QWidget for the button. Automatically set during initialisation of the menu.\n *\n */\n $.oToolButton = function(toolName, iconFile, parent) {\n  this.toolName = toolName;\n\n  if (typeof iconFile === \"undefined\"){\n    // find an icon for the function in the script-icons folder\n    var scriptIconsFolder = new this.$.oFolder(specialFolders.resource+\"/icons/drawingtool\");\n    var iconFiles = scriptIconsFolder.getFiles(toolName.replace(\" \", \"\").toLowerCase() + \".*\");\n\n    if (iconFiles.length > 0){\n      var iconFile = iconFiles[0].path;\n    }else{\n      // choose default toonboom \"missing icon\" script icon\n      // currently svg icons seem unsupported?\n      var iconFile = specialFolders.resource+\"/icons/script/qtgeneric.svg\";\n    }\n  }\n  this.$.oPieButton.call(this, iconFile, parent);\n\n  this.toolTip = this.toolName;\n}\n$.oToolButton.prototype = Object.create($.oPieButton.prototype);\n\n\n$.oToolButton.prototype.activate = function(){\n  this.$.app.currentTool = this.toolName;\n  this.closeMenu()\n}\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//      $.oActionButton class       //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for $.oActionButton\n * @name          $.oActionButton\n * @constructor\n * @classdescription This subclass of QPushButton provides an easy way to create a button for a tool.\n * This class is a subclass of QPushButton and all the methods from that class are available to modify this button.\n * @param {string}   actionName               The action string that will be executed with Action.perform\n * @param {string}   responder                The responder for the action\n * @param {string}   text                     A text for the button display.\n * @param {string}   iconFile                 An icon path for the button.\n * @param {QWidget}  parent                   The parent QWidget for the button. Automatically set during initialisation of the menu.\n */\n $.oActionButton = function(actionName, responder, text, iconFile, parent) {\n  this.action = actionName;\n  this.responder = responder;\n\n  if (typeof text === 'undefined') var text = \"action\";\n\n  if (typeof iconFile === 'undefined') var iconFile = specialFolders.resource+\"/icons/old/exec.png\";\n\n  this.$.oPieButton.call(this, iconFile, text, parent);\n  this.toolTip = this.toolName;\n}\n$.oActionButton.prototype = Object.create($.oPieButton.prototype);\n\n\n$.oActionButton.prototype.activate = function(){\n  if (this.responder){\n    // log(\"Validating : \"+ this.actionName + \" ? \"+ Action.validate(this.actionName, this.responder).enabled)\n    if (Action.validate(this.action, this.responder).enabled){\n      Action.perform(this.action, this.responder);\n    }\n  }else{\n    if (Action.Validate(this.action).enabled){\n      Action.perform(this.action);\n    }\n  }\n  view.refreshViews();\n  this.closeMenu()\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//      $.oColorButton class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for $.oColorButton\n * @name          $.oColorButton\n * @constructor\n * @classdescription This subclass of QPushButton provides an easy way to create a button to choose a color from a palette.\n * This class is a subclass of QPushButton and all the methods from that class are available to modify this button.\n * @param {string}   paletteName              The name of the palette that contains the color\n * @param {string}   colorName                The name of the color (if more than one is present, will pick the first match)\n * @param {bool}     showName                 Whether to display the name of the color on the button\n * @param {QWidget}  parent                   The parent QWidget for the button. Automatically set during initialisation of the menu.\n *\n */\n $.oColorButton = function(paletteName, colorName, showName, parent) {\n  this.paletteName = paletteName;\n  this.colorName = colorName;\n\n  if (typeof showName === \"undefined\") var showName = false;\n\n  this.$.oPieButton.call(this, \"\", showName?colorName:\"\", parent);\n\n  var palette = this.$.scn.getPaletteByName(paletteName);\n  var color = palette.getColorByName(colorName);\n  var colorValue = color.value\n\n  var iconMap = new QPixmap(this.minimumHeight,this.minimumHeight)\n  iconMap.fill(new QColor(colorValue.r, colorValue.g, colorValue.b, colorValue.a))\n  var icon = new QIcon(iconMap);\n\n  this.icon = icon;\n\n  this.toolTip = this.paletteName + \": \" + this.colorName;\n}\n$.oColorButton.prototype = Object.create($.oPieButton.prototype);\n\n\n$.oColorButton.prototype.activate = function(){\n  var palette = this.$.scn.getPaletteByName(this.paletteName);\n  var color = palette.getColorByName(this.colorName);\n\n  this.$.scn.currentPalette = palette;\n  palette.currentColor = color;\n  this.closeMenu()\n}\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//      $.oScriptButton class       //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for $.oScriptButton\n * @name          $.oScriptButton\n * @constructor\n * @classdescription This subclass of QPushButton provides an easy way to create a button for a widget that will launch a function from another script file.<br>\n * The buttons created this way automatically load the icon named after the script if it finds one named like the function in a script-icons folder next to the script file.<br>\n * It will also automatically set the callback to lanch the function from the script.<br>\n * This class is a subclass of QPushButton and all the methods from that class are available to modify this button.\n * @param {string}   scriptFile               The path to the script file that will be launched\n * @param {string}   scriptFunction           The function name to launch from the script\n * @param {QWidget}  parent                   The parent QWidget for the button. Automatically set during initialisation of the menu.\n */\n$.oScriptButton = function(scriptFile, scriptFunction, parent) {\n  this.scriptFile = scriptFile;\n  this.scriptFunction = scriptFunction;\n\n  // find an icon for the function in the script-icons folder\n  var scriptFile = new this.$.oFile(scriptFile)\n  var scriptIconsFolder = new this.$.oFolder(scriptFile.folder.path+\"/script-icons\");\n  var iconFiles = scriptIconsFolder.getFiles(scriptFunction+\".*\");\n  if (iconFiles.length > 0){\n    var iconFile = iconFiles[0].path;\n  }else{\n    // choose default toonboom \"missing icon\" script icon\n    // currently svg icons seem unsupported?\n    var iconFile = specialFolders.resource+\"/icons/script/qtgeneric.svg\";\n  }\n\n  this.$.oPieButton.call(this, iconFile, \"\", parent);\n\n  this.toolTip = this.scriptFunction;\n}\n$.oScriptButton.prototype = Object.create($.oPieButton.prototype);\n\n$.oScriptButton.prototype.activate = function(){\n  include(this.scriptFile);\n  eval(this.scriptFunction)();\n  this.closeMenu()\n}\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//       $.oPrefButton class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for $.oPrefButton\n * @name          $.oPrefButton\n * @constructor\n * @classdescription This subclass of QPushButton provides an easy way to create a button to change a boolean preference.\n * This class is a subclass of QPushButton and all the methods from that class are available to modify this button.\n * @param {string}   preferenceString         The name of the preference to show/change.\n * @param {string}   text                     A text for the button display.\n * @param {string}   iconFile                 An icon path for the button.\n * @param {QWidget}  parent                   The parent QWidget for the button. Automatically set during initialisation of the menu.\n */\n$.oPrefButton = function(preferenceString, text, iconFile, parent) {\n  this.preferenceString = preferenceString;\n\n  if (typeof iconFile === 'undefined') var iconFile = specialFolders.resource+\"/icons/toolproperties/settings.svg\";\n  this.checkable = true;\n  this.checked = preferences.getBool(preferenceString, true);\n\n  $.oPieButton.call(this, iconFile, text, parent);\n\n  this.toolTip = this.preferenceString;\n}\n$.oPrefButton.prototype = Object.create($.oPieButton.prototype);\n\n\n$.oPrefButton.prototype.activate = function(){\n  var value = preferences.getBool(this.preferenceString, true);\n  this.checked != value;\n  preferences.setBool(this.preferenceString, value);\n  this.closeMenu()\n}\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//      $.oStencilButton class      //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n// not currently working\n$.oStencilButton = function(stencilName, parent) {\n  this.stencilName = stencilName;\n\n  var iconFile = specialFolders.resource+\"/icons/brushpreset/default.svg\";\n\n  $.oPieButton.call(this, iconFile, stencilName, parent);\n\n  this.toolTip = stencilName;\n}\n$.oStencilButton.prototype = Object.create($.oPieButton.prototype);\n\n$.oStencilButton.prototype.activate = function(){\n  this.$.app.currentStencil = this.stencilName;\n\n  this.closeMenu()\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_drawing.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oDrawing class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The $.oDrawing constructor.\n * @constructor\n * @classdesc The $.oDrawing Class represents a single drawing from an element.\n * @param   {int}                    name                       The name of the drawing.\n * @param   {$.oElement}             oElementObject             The element object associated to the element.\n *\n * @property {int}                   name                       The name of the drawing.\n * @property {$.oElement}            element                    The element object associated to the element.\n */\n$.oDrawing = function (name, oElementObject) {\n  this._type = \"drawing\";\n  this._name = name;\n  this.element = oElementObject;\n\n  this._key = Drawing.Key({\n    elementId: oElementObject.id,\n    exposure: name\n  });\n\n  //log(JSON.stringify(this._key))\n\n  this._overlay = new this.$.oArtLayer(3, this);\n  this._lineArt = new this.$.oArtLayer(2, this);\n  this._colorArt = new this.$.oArtLayer(1, this);\n  this._underlay = new this.$.oArtLayer(0, this);\n  this._artLayers = [this._underlay, this._colorArt, this._lineArt, this._overlay];\n}\n\n\n/**\n * The different types of lines ends.\n * @name $.oDrawing#LINE_END_TYPE\n * @enum\n */\n$.oDrawing.LINE_END_TYPE = {\n  ROUND: 1,\n  FLAT: 2,\n  BEVEL: 3\n};\n\n\n/**\n * The reference to the art layers to use with oDrawing.setAsActiveDrawing()\n * @name $.oDrawing#ART_LAYER\n * @enum\n */\n$.oDrawing.ART_LAYER = {\n  OVERLAY: 8,\n  LINEART: 4,\n  COLORART: 2,\n  UNDERLAY: 1\n};\n\n\n/**\n * The name of the drawing.\n * @name $.oDrawing#name\n * @type {string}\n */\nObject.defineProperty($.oDrawing.prototype, 'name', {\n  get: function () {\n    return this._name;\n  },\n\n  set: function (newName) {\n    if (this._name == newName) return;\n\n    var _column = this.element.column.uniqueName;\n    // this ripples recursively\n\n    if (Drawing.isExists(this.element.id, newName)) this.element.getDrawingByName(newName).name = newName + \"_1\";\n    column.renameDrawing(_column, this._name, newName);\n    this._name = newName;\n  }\n})\n\n\n/**\n * The internal Id used to identify drawings.\n * @name $.oDrawing#id\n * @readonly\n * @type {int}\n */\nObject.defineProperty($.oDrawing.prototype, 'id', {\n  get: function () {\n    return this._key.drawingId;\n  }\n})\n\n\n/**\n * The folder path of the drawing on the filesystem.\n * @name $.oDrawing#path\n * @readonly\n * @type {string}\n */\nObject.defineProperty($.oDrawing.prototype, 'path', {\n  get: function () {\n    return fileMapper.toNativePath(Drawing.filename(this.element.id, this.name))\n  }\n})\n\n\n/**\n * The drawing pivot of the drawing.\n * @name $.oDrawing#pivot\n * @type {$.oPoint}\n */\nObject.defineProperty($.oDrawing.prototype, 'pivot', {\n  get: function () {\n    if (this.$.batchMode){\n      throw new Error(\"oDrawing.pivot is not available in batch mode.\")\n    }\n\n    var _pivot = Drawing.getPivot({ \"drawing\": this._key });\n    return new this.$.oPoint(_pivot.x, _pivot.y, 0);\n  },\n\n  set: function (newPivot) {\n    var _pivot = { x: newPivot.x, y: newPivot.y };\n    Drawing.setPivot({ drawing: this._key, pivot: _pivot });\n  }\n})\n\n\n/**\n * The color Ids present on the drawing.\n * @name $.oDrawing#usedColorIds\n * @type {string[]}\n */\nObject.defineProperty($.oDrawing.prototype, 'usedColorIds', {\n  get: function () {\n    var _colorIds = DrawingTools.getDrawingUsedColors(this._key);\n    return _colorIds;\n  }\n})\n\n\n/**\n * The bounding box of the drawing, in drawing space coordinates. (null if the drawing is empty.)\n * @name $.oDrawing#boundingBox\n * @readonly\n * @type {$.oBox}\n */\nObject.defineProperty($.oDrawing.prototype, 'boundingBox', {\n  get: function () {\n    if (this.$.batchMode){\n      throw new Error(\"oDrawing.boudingBox is not available in batch mode.\")\n    }\n\n    var _box = new this.$.oBox()\n    for (var i in this.artLayers) {\n      var _layerBox = this.artLayers[i].boundingBox\n      if (_layerBox) _box.include(_layerBox)\n    }\n\n    return _box\n  }\n})\n\n\n/**\n * Access the underlay art layer's content through this object.\n * @name $.oDrawing#underlay\n * @readonly\n * @type {$.oArtLayer}\n */\nObject.defineProperty($.oDrawing.prototype, 'underlay', {\n  get: function () {\n    return this._underlay;\n  }\n})\n\n\n/**\n * Access the color art layer's content through this object.\n * @name $.oDrawing#colorArt\n * @readonly\n * @type {$.oArtLayer}\n */\nObject.defineProperty($.oDrawing.prototype, 'colorArt', {\n  get: function () {\n    return this._colorArt;\n  }\n})\n\n\n/**\n * Access the line art layer's content through this object.\n * @name $.oDrawing#lineArt\n * @readonly\n * @type {$.oArtLayer}\n */\nObject.defineProperty($.oDrawing.prototype, 'lineArt', {\n  get: function () {\n    return this._lineArt;\n  }\n})\n\n\n/**\n * Access the overlay art layer's content through this object.\n * @name $.oDrawing#overlay\n * @readonly\n * @type {$.oArtLayer}\n */\nObject.defineProperty($.oDrawing.prototype, 'overlay', {\n  get: function () {\n    return this._overlay;\n  }\n})\n\n\n/**\n * The list of artLayers of this drawing.\n * @name $.oDrawing#artLayers\n * @readonly\n * @type {$.oArtLayer[]}\n */\nObject.defineProperty($.oDrawing.prototype, 'artLayers', {\n  get: function () {\n    return this._artLayers;\n  }\n})\n\n\n\n/**\n * the shapes contained amongst all artLayers of this drawing.\n * @name $.oDrawing#shapes\n * @readonly\n * @type {$.oShape[]}\n */\nObject.defineProperty($.oDrawing.prototype, 'shapes', {\n  get: function () {\n    var _shapes = [];\n    for (var i in this.artLayers) {\n      _shapes = _shapes.concat(this.artLayers[i].shapes);\n    }\n\n    return _shapes;\n  }\n})\n\n\n/**\n * the strokes contained amongst all artLayers of this drawing.\n * @name $.oDrawing#strokes\n * @readonly\n * @type {$.oStroke[]}\n */\nObject.defineProperty($.oDrawing.prototype, 'strokes', {\n  get: function () {\n    var _strokes = [];\n    for (var i in this.artLayers) {\n      _strokes = _strokes.concat(this.artLayers[i].strokes);\n    }\n\n    return _strokes;\n  }\n})\n\n\n/**\n * The contours contained amongst all the shapes of the artLayer.\n * @name $.oDrawing#contours\n * @type {$.oContour[]}\n */\n Object.defineProperty($.oDrawing.prototype, 'contours', {\n  get: function () {\n    var _contours = []\n\n    for (var i in this.artLayers) {\n      _contours = _contours.concat(this.artLayers[i].contours)\n    }\n\n    return _contours\n  }\n})\n\n\n\n/**\n * the currently active art layer of this drawing.\n * @name $.oDrawing#activeArtLayer\n * @type {$.oArtLayer}\n */\nObject.defineProperty($.oDrawing.prototype, 'activeArtLayer', {\n  get: function () {\n    var settings = Tools.getToolSettings();\n    if (!settings.currentDrawing) return null;\n\n    return this.artLayers[settings.activeArt]\n  },\n  set: function (newArtLayer) {\n    var layers = this.$.oDrawing.ART_LAYER\n    var index = layers[newArtLayer.name.toUpperCase()]\n    this.setAsActiveDrawing(index);\n  }\n})\n\n\n/**\n * the selected shapes on this drawing\n * @name $.oDrawing#selectedShapes\n * @type {$.oShape}\n */\nObject.defineProperty($.oDrawing.prototype, 'selectedShapes', {\n  get: function () {\n    var _selectedShapes = [];\n    for (var i in this.artLayers) {\n      _selectedShapes = _selectedShapes.concat(this.artLayers[i].selectedShapes);\n    }\n\n    return _selectedShapes;\n  }\n})\n\n\n/**\n * the selected shapes on this drawing\n * @name $.oDrawing#selectedStrokes\n * @type {$.oShape}\n */\nObject.defineProperty($.oDrawing.prototype, 'selectedStrokes', {\n  get: function () {\n    var _selectedStrokes = [];\n    for (var i in this.artLayers) {\n      _selectedStrokes = _selectedStrokes.concat(this.artLayers[i].selectedStrokes);\n    }\n\n    return _selectedStrokes;\n  }\n})\n\n\n/**\n * the selected shapes on this drawing\n * @name $.oDrawing#selectedContours\n * @type {$.oShape}\n */\nObject.defineProperty($.oDrawing.prototype, 'selectedContours', {\n  get: function () {\n    var _selectedContours = [];\n    for (var i in this.artLayers) {\n      _selectedContours = _selectedContours.concat(this.artLayers[i].selectedContours);\n    }\n\n    return _selectedContours;\n  }\n})\n\n\n/**\n * all the data from this drawing. For internal use.\n * @name $.oDrawing#drawingData\n * @type {Object}\n * @readonly\n * @private\n */\nObject.defineProperty($.oDrawing.prototype, 'drawingData', {\n  get: function () {\n    var _data = Drawing.query.getData({drawing: this._key});\n    if (!_data) throw new Error(\"Data unavailable for drawing \"+this.name)\n    return _data;\n  }\n})\n\n\n\n\n// $.oDrawing Class methods\n\n/**\n * Import a given file into an existing drawing.\n * @param   {$.oFile} file                  The path to the file\n * @param   {bool}    [convertToTvg=false]  Whether to convert the bitmap to the tvg format (this doesn't vectorise the drawing)\n *\n * @return { $.oFile }   the oFile object pointing to the drawing file after being it has been imported into the element folder.\n */\n$.oDrawing.prototype.importBitmap = function (file, convertToTvg) {\n  var _path = new this.$.oFile(this.path);\n  if (!(file instanceof this.$.oFile)) file = new this.$.oFile(file);\n  if (!file.exists) throw new Error (\"Can't import bitmap \"+file.path+\", file doesn't exist\");\n\n  if (convertToTvg && file.extension.toLowerCase() != \"tvg\"){\n    // use utransform binary to perform conversion\n    var _bin = specialFolders.bin + \"/utransform\";\n\n    var tempFolder = this.$.scn.tempFolder;\n\n    var _convertedFilePath = tempFolder.path + \"/\" + file.name + \".tvg\";\n    var _convertProcess = new this.$.oProcess(_bin, [\"-outformat\", \"TVG\", \"-debug\", \"-scale\", \"1\", \"-bboxtvgincrease\",\"0\" , \"-outfile\", _convertedFilePath, file.path]);\n    log(_convertProcess.execute())\n\n    var convertedFile = new this.$.oFile(_convertedFilePath);\n    if (!convertedFile.exists) throw new Error (\"Converting \" + file.path + \" to TVG has failed.\");\n\n    file = convertedFile;\n  }\n\n  return file.copy(_path.folder, _path.name, true);\n}\n\n\n/**\n * @returns {int[]}  The frame numbers at which this drawing appears.\n */\n$.oDrawing.prototype.getVisibleFrames = function () {\n  var _element = this.element;\n  var _column = _element.column;\n\n  if (!_column) {\n    this.$.debug(\"Column missing: can't get visible frames for  drawing \" + this.name + \" of element \" + _element.name, this.$.DEBUG_LEVEL.ERROR);\n    return null;\n  }\n\n  var _frames = [];\n  var _keys = _column.keyframes;\n  for (var i in _keys) {\n    if (_keys[i].value == this.name) _frames.push(_keys[i].frameNumber);\n  }\n\n  return _frames;\n}\n\n\n/**\n * Remove the drawing from the element.\n */\n$.oDrawing.prototype.remove = function () {\n  var _element = this.element;\n  var _column = _element.column;\n\n  if (!_column) {\n    throw new Error (\"Column missing: impossible to delete drawing \" + this.name + \" of element \" + _element.name);\n  }\n\n  var _frames = _column.frames;\n  var _lastFrame = _frames.pop();\n\n  var _thisDrawing = this;\n\n  // we have to expose the drawing on the column to delete it. Exposing at the last frame...\n  this.$.debug(\"deleting drawing \" + _thisDrawing + \" from element \" + _element.name, this.$.DEBUG_LEVEL.LOG);\n  var _lastDrawing = _lastFrame.value;\n  var _keyFrame = _lastFrame.isKeyFrame;\n  _lastFrame.value = _thisDrawing;\n\n  column.deleteDrawingAt(_column.uniqueName, _lastFrame.frameNumber);\n\n  // resetting the last frame\n  _lastFrame.value = _lastDrawing;\n  _lastFrame.isKeyFrame = _keyFrame;\n}\n\n\n\n/**\n * refresh the preview of the drawing.\n */\n$.oDrawing.prototype.refreshPreview = function () {\n  if (this.element.format == \"TVG\") return;\n\n  var _path = new this.$.oFile(this.path);\n  var _elementFolder = _path.folder;\n  var _previewFiles = _elementFolder.getFiles(_path.name + \"-*.tga\");\n\n  for (var i in _previewFiles) {\n    _previewFiles[i].remove();\n  }\n}\n\n\n/**\n* Change the currently active drawing. Can specify an art Layer\n* Doesn't work in batch mode.\n* @param {oDrawing.ART_LAYER}   [artLayer]      activate the given art layer\n* @return {bool}   success of setting the drawing as current\n*/\n$.oDrawing.prototype.setAsActiveDrawing = function (artLayer) {\n  if (this.$.batchMode) {\n    this.$.debug(\"Setting as active drawing not available in batch mode\", this.$.DEBUG_LEVEL.ERROR);\n    return false;\n  }\n\n  var _column = this.element.column;\n  if (!_column) {\n    this.$.debug(\"Column missing: impossible to set as active drawing \" + this.name + \" of element \" + _element.name, this.$.DEBUG_LEVEL.ERROR);\n    return false;\n  }\n\n  var _frame = this.getVisibleFrames();\n  if (_frame.length == 0) {\n    this.$.debug(\"Drawing not exposed: impossible to set as active drawing \" + this.name + \" of element \" + _element.name, this.$.DEBUG_LEVEL.ERROR);\n    return false;\n  }\n\n  DrawingTools.setCurrentDrawingFromColumnName(_column.uniqueName, _frame[0]);\n\n  if (artLayer) DrawingTools.setCurrentArt(artLayer);\n\n  return true;\n}\n\n\n/**\n * Duplicates the drawing to the given frame, and renames the drawing with the given name.\n * @param {int}      [frame]     the frame at which to create the drawing. By default, the current frame.\n * @param {string}   [newName]   A new name for the drawing. By default, the name will be the number of the frame.\n * @returns {$.oDrawing}   the newly created drawing\n */\n$.oDrawing.prototype.duplicate = function(frame, newName){\n  var _element = this.element\n  if (typeof frame ==='undefined') var frame = this.$.scn.currentFrame;\n  if (typeof newName === 'undefined') var newName = frame;\n  var newDrawing = _element.addDrawing(frame, newName, this.path)\n  return newDrawing;\n}\n\n/**\n * Replaces a color Id present on the drawing by another.\n * @param {string} currentId\n * @param {string} newId\n */\n$.oDrawing.prototype.replaceColorId = function (currentId, newId){\n  DrawingTools.recolorDrawing( this._key, [{from:currentId, to:newId}]);\n}\n\n\n/**\n * Copies the contents of the Drawing into the clipboard\n * @param {oDrawing.ART_LAYER} [artLayer]    Specify to only copy the contents of the specified artLayer\n */\n$.oDrawing.prototype.copyContents = function (artLayer) {\n\n  var _current = this.setAsActiveDrawing(artLayer);\n  if (!_current) {\n    this.$.debug(\"Impossible to copy contents of drawing \" + this.name + \" of element \" + _element.name + \", the drawing cannot be set as active.\", this.DEBUG_LEVEL.ERROR);\n    return;\n  }\n  ToolProperties.setApplyAllArts(!artLayer);\n  Action.perform(\"deselect()\", \"cameraView\");\n  Action.perform(\"onActionChooseSelectTool()\");\n  Action.perform(\"selectAll()\", \"cameraView\");\n\n  if (Action.validate(\"copy()\", \"cameraView\").enabled) Action.perform(\"copy()\", \"cameraView\");\n}\n\n\n/**\n * Pastes the contents of the clipboard into the Drawing\n * @param {oDrawing.ART_LAYER} [artLayer]    Specify to only paste the contents onto the specified artLayer\n */\n$.oDrawing.prototype.pasteContents = function (artLayer) {\n\n  var _current = this.setAsActiveDrawing(artLayer);\n  if (!_current) {\n    this.$.debug(\"Impossible to copy contents of drawing \" + this.name + \" of element \" + _element.name + \", the drawing cannot be set as active.\", this.DEBUG_LEVEL.ERROR);\n    return;\n  }\n  ToolProperties.setApplyAllArts(!artLayer);\n  Action.perform(\"deselect()\", \"cameraView\");\n  Action.perform(\"onActionChooseSelectTool()\");\n  if (Action.validate(\"paste()\", \"cameraView\").enabled) Action.perform(\"paste()\", \"cameraView\");\n}\n\n\n/**\n* Converts the line ends of the Drawing object to the defined type.\n* Doesn't work in batch mode. This function modifies the selection.\n*\n* @param {oDrawing.LINE_END_TYPE}     endType        the type of line ends to set.\n* @param {oDrawing.ART_LAYER}        [artLayer]      only apply to provided art Layer.\n*/\n$.oDrawing.prototype.setLineEnds = function (endType, artLayer) {\n  if (this.$.batchMode) {\n    this.$.debug(\"setting line ends not available in batch mode\", this.DEBUG_LEVEL.ERROR);\n    return;\n  }\n\n  var _current = this.setAsActiveDrawing(artLayer);\n  if (!_current) {\n    this.$.debug(\"Impossible to change line ends on drawing \" + this.name + \" of element \" + _element.name + \", the drawing cannot be set as active.\", this.DEBUG_LEVEL.ERROR);\n    return;\n  }\n\n  // apply to all arts only if art layer not specified\n  ToolProperties.setApplyAllArts(!artLayer);\n  Action.perform(\"deselect()\", \"cameraView\");\n  Action.perform(\"onActionChooseSelectTool()\");\n  Action.perform(\"selectAll()\", \"cameraView\");\n\n  var widget = $.getHarmonyUIWidget(\"pencilShape\", \"frameBrushParameters\");\n  if (widget) {\n    widget.onChangeTipStart(endType);\n    widget.onChangeTipEnd(endType);\n    widget.onChangeJoin(endType);\n  }\n  Action.perform(\"deselect()\", \"cameraView\");\n}\n\n\n/**\n* Converts the Drawing object to a string of the drawing name.\n* @return: { string }                 The name of the drawing.\n*/\n$.oDrawing.prototype.toString = function () {\n  return this.name;\n}\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oArtLayer class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oArtLayer class.\n * @constructor\n * @classdesc  $.oArtLayer represents art layers, as described by the artlayer toolbar. Access the drawing contents of the layers through this class.\n * @param   {int}                    index                      The artLayerIndex (0: underlay, 1: line art, 2: color art, 3:overlay).\n * @param   {$.oDrawing}             oDrawingObject             The oDrawing this layer belongs to.\n */\n$.oArtLayer = function (index, oDrawingObject) {\n  this._layerIndex = index;\n  this._drawing = oDrawingObject;\n  //log(this._drawing._key)\n  this._key = { \"drawing\": this._drawing._key, \"art\": index }\n}\n\n\n/**\n * The name of the artLayer (lineArt, colorArt, etc)\n * @name $.oArtLayer#name\n * @type {string}\n */\nObject.defineProperty($.oArtLayer.prototype, 'name', {\n  get: function(){\n    var names = [\"underlay\", \"colorArt\", \"lineArt\", \"overlay\"];\n    return names[this._layerIndex];\n  }\n})\n\n\n/**\n * The shapes contained on the artLayer.\n * @name $.oArtLayer#shapes\n * @type {$.oShape[]}\n */\nObject.defineProperty($.oArtLayer.prototype, 'shapes', {\n  get: function () {\n    if (!this.hasOwnProperty(\"_shapes\")){\n      var _shapesNum = Drawing.query.getNumberOfLayers(this._key);\n      var _shapes = [];\n      for (var i = 0; i < _shapesNum; i++) {\n        _shapes.push(this.getShapeByIndex(i));\n      }\n      this._shapes = _shapes;\n    }\n    return this._shapes;\n  }\n})\n\n\n/**\n * The strokes contained amongst all the shapes of the artLayer.\n * @name $.oArtLayer#strokes\n * @type {$.oStroke[]}\n */\nObject.defineProperty($.oArtLayer.prototype, 'strokes', {\n  get: function () {\n    var _strokes = [];\n\n    var _shapes = this.shapes;\n    for (var i in _shapes) {\n      _strokes = _strokes.concat(_shapes[i].strokes);\n    }\n\n    return _strokes;\n  }\n})\n\n\n/**\n * The contours contained amongst all the shapes of the artLayer.\n * @name $.oArtLayer#contours\n * @type {$.oContour[]}\n */\nObject.defineProperty($.oArtLayer.prototype, 'contours', {\n  get: function () {\n    var _contours = [];\n\n    var _shapes = this.shapes;\n    for (var i in _shapes) {\n      _contours = _contours.concat(_shapes[i].contours);\n    }\n\n    return _contours;\n  }\n})\n\n\n/**\n * The bounds of the layer, in drawing space coordinates. (null if the drawing is empty.)\n * @name $.oArtLayer#boundingBox\n * @type {$.oBox}\n */\nObject.defineProperty($.oArtLayer.prototype, 'boundingBox', {\n  get: function () {\n    var _box = Drawing.query.getBox(this._key);\n    if (_box.empty) return null;\n\n    var _boundingBox = new $.oBox(_box.x0, _box.y0, _box.x1, _box.y1);\n    return _boundingBox;\n  }\n})\n\n\n/**\n * the currently selected shapes on the ArtLayer.\n * @name $.oArtLayer#selectedShapes\n * @type {$.oShape[]}\n */\nObject.defineProperty($.oArtLayer.prototype, 'selectedShapes', {\n  get: function () {\n    var _shapes = Drawing.selection.get(this._key).selectedLayers;\n    var _artLayer = this;\n    return _shapes.map(function (x) { return _artLayer.getShapeByIndex(x) });\n  }\n})\n\n\n\n/**\n * the currently selected strokes on the ArtLayer.\n * @name $.oArtLayer#selectedStrokes\n * @type {$.oStroke[]}\n */\nObject.defineProperty($.oArtLayer.prototype, 'selectedStrokes', {\n  get: function () {\n    var _shapes = this.selectedShapes;\n    var _strokes = [];\n\n    for (var i in _shapes) {\n      _strokes = _strokes.concat(_shapes[i].strokes);\n    }\n\n    return _strokes;\n  }\n})\n\n\n/**\n * the currently selected contours on the ArtLayer.\n * @name $.oArtLayer#selectedContours\n * @type {$.oContour[]}\n */\nObject.defineProperty($.oArtLayer.prototype, 'selectedContours', {\n  get: function () {\n    var _shapes = this.selectedShapes;\n    var _contours = [];\n\n    for (var i in _shapes) {\n      _contours = _contours.concat(_shapes[i].contours);\n    }\n\n    return _contours;\n  }\n})\n\n\n\n/**\n * all the data from this artLayer. For internal use.\n * @name $.oArtLayer#drawingData\n * @type {$.oStroke[]}\n * @readonly\n * @private\n */\nObject.defineProperty($.oArtLayer.prototype, 'drawingData', {\n  get: function () {\n    var _data = this._drawing.drawingData\n    for (var i in _data.arts){\n      if (_data.arts[i].art == this._layerIndex) {\n        return _data.arts[i];\n      }\n    }\n\n    // in case of empty layerArt, return a default object\n    return {art:this._layerIndex, artName:this.name, layers:[]};\n  }\n})\n\n\n/**\n * Draws a circle on the artLayer.\n * @param {$.oPoint}       center         The center of the circle\n * @param {float}          radius         The radius of the circle\n * @param {$.oLineStyle}   [lineStyle]    Provide a $.oLineStyle object to specify how the line will look\n * @param {object}         [fillStyle=null]    The fill information to fill the circle with.\n * @returns {$.oShape}  the created shape containing the circle.\n*/\n$.oArtLayer.prototype.drawCircle = function(center, radius, lineStyle, fillStyle){\n  if (typeof fillStyle === 'undefined') var fillStyle = null;\n\n  var arg = {\n    x: center.x,\n    y: center.y,\n    radius: radius\n  };\n  var _path = Drawing.geometry.createCircle(arg);\n\n  return this.drawShape(_path, lineStyle, fillStyle);\n}\n\n/**\n * Draws the given path on the artLayer.\n * @param {$.oVertex[]}    path         an array of $.oVertex objects that describe a path.\n * @param {$.oLineStyle}   [lineStyle]  the line style to draw with. (By default, will use the current stencil selection)\n * @param {$.oFillStyle}   [fillStyle]  the fill information for the path. (By default, will use the current palette selection)\n * @param {bool}   [polygon]            Whether bezier handles should be created for the points in the path (ignores \"onCurve\" properties of oVertex from path)\n * @param {bool}   [createUnderneath]   Whether the new shape will appear on top or underneath the contents of the layer. (not working yet)\n */\n$.oArtLayer.prototype.drawShape = function(path, lineStyle, fillStyle, polygon, createUnderneath){\n  if (typeof fillStyle === 'undefined') var fillStyle = new this.$.oFillStyle();\n  if (typeof lineStyle === 'undefined') var lineStyle = new this.$.oLineStyle();\n  if (typeof polygon === 'undefined') var polygon = false;\n  if (typeof createUnderneath === 'undefined') var createUnderneath = false;\n\n  var index = this.shapes.length;\n\n  var _lineStyle = {};\n\n  if (lineStyle){\n    _lineStyle.pencilColorId = lineStyle.colorId;\n    _lineStyle.thickness = {\n      \"minThickness\": lineStyle.minThickness,\n      \"maxThickness\": lineStyle.maxThickness,\n      \"thicknessPath\": 0\n    };\n  }\n\n  if (fillStyle) _lineStyle.shaderLeft = 0;\n  if (polygon) _lineStyle.polygon = true;\n  _lineStyle.under = createUnderneath;\n  _lineStyle.stroke = !!lineStyle;\n\n  var strokeDesciption = _lineStyle;\n  strokeDesciption.path = path;\n  strokeDesciption.closed = !!fillStyle;\n\n  var shapeDescription = {}\n  if (fillStyle) shapeDescription.shaders = [{ colorId : fillStyle.colorId }]\n  shapeDescription.strokes = [strokeDesciption]\n  if (lineStyle) shapeDescription.thicknessPaths = [lineStyle.stencil.thicknessPath]\n\n  var config = {\n    label: \"draw shape\",\n    drawing: this._key.drawing,\n    art: this._key.art,\n    layers: [shapeDescription]\n  };\n\n\n  var layers = DrawingTools.createLayers(config);\n\n  var newShape = this.getShapeByIndex(index);\n  this._shapes.push(newShape);\n  return newShape;\n};\n\n\n/**\n * Draws the given path on the artLayer.\n * @param {$.oVertex[]}    path          an array of $.oVertex objects that describe a path.\n * @param {$.oLineStyle}   lineStyle     the line style to draw with.\n * @returns {$.oShape} the shape containing the added stroke.\n */\n$.oArtLayer.prototype.drawStroke = function(path, lineStyle){\n  return this.drawShape(path, lineStyle, null);\n};\n\n\n/**\n * Draws the given path on the artLayer as a contour.\n * @param {$.oVertex[]}    path          an array of $.oVertex objects that describe a path.\n * @param {$.oFillStyle}   fillStyle     the fill style to draw with.\n * @returns {$.oShape} the shape newly created from the path.\n */\n$.oArtLayer.prototype.drawContour = function(path, fillStyle){\n  return this.drawShape(path, null, fillStyle);\n};\n\n\n/**\n * Draws a rectangle on the artLayer.\n * @param {float}        x          the x coordinate of the top left corner.\n * @param {float}        y          the y coordinate of the top left corner.\n * @param {float}        width      the width of the rectangle.\n * @param {float}        height     the height of the rectangle.\n * @param {$.oLineStyle} lineStyle  a line style to use for the rectangle stroke.\n * @param {$.oFillStyle} fillStyle  a fill style to use for the rectangle fill.\n * @returns {$.oShape} the shape containing the added stroke.\n */\n$.oArtLayer.prototype.drawRectangle = function(x, y, width, height, lineStyle, fillStyle){\n  if (typeof fillStyle === 'undefined') var fillStyle = null;\n\n  var path = [\n    {x:x,y:y,onCurve:true},\n    {x:x+width,y:y,onCurve:true},\n    {x:x+width,y:y-height,onCurve:true},\n    {x:x,y:y-height,onCurve:true},\n    {x:x,y:y,onCurve:true}\n  ];\n\n  return this.drawShape(path, lineStyle, fillStyle);\n}\n\n\n\n/**\n * Draws a line on the artLayer\n * @param {$.oPoint}     startPoint\n * @param {$.oPoint}     endPoint\n * @param {$.oLineStyle} lineStyle\n * @returns {$.oShape} the shape containing the added line.\n */\n$.oArtLayer.prototype.drawLine = function(startPoint, endPoint, lineStyle){\n  var path = [{x:startPoint.x,y:startPoint.y,onCurve:true},{x:endPoint.x,y:endPoint.y,onCurve:true}];\n\n  return this.drawShape(path, lineStyle, null);\n}\n\n\n/**\n * Removes the contents of the art layer.\n */\n$.oArtLayer.prototype.clear = function(){\n  var _shapes = this.shapes;\n  this.$.debug(_shapes, this.$.DEBUG_LEVEL.DEBUG);\n  for (var i=_shapes.length - 1; i>=0; i--){\n    _shapes[i].remove();\n  }\n}\n\n\n/**\n * get a shape from the artLayer by its index\n * @param {int} index\n *\n * @return {$.oShape}\n */\n$.oArtLayer.prototype.getShapeByIndex = function (index) {\n  return new this.$.oShape(index, this);\n}\n\n\n/**\n * @private\n */\n$.oArtLayer.prototype.toString = function(){\n  return \"Object $.oArtLayer [\"+this.name+\"]\";\n}\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//       $.oLineStyle class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oLineStyle class.\n * @constructor\n * @classdesc\n * The $.oLineStyle class describes a lineStyle used to describe the appearance of strokes and perform drawing operations. <br>\n * Initializing a $.oLineStyle without any parameters attempts to get the current pencil thickness settings and color.\n * @param {string}     colorId             the color Id to paint the line with.\n * @param {$.oStencil} stencil             the stencil object representing the thickness keys\n */\n$.oLineStyle = function (colorId, stencil) {\n  if (typeof minThickness === 'undefined') var minThickness = PenstyleManager.getCurrentPenstyleMinimumSize();\n  if (typeof maxThickness === 'undefined') {\n    var maxThickness = PenstyleManager.getCurrentPenstyleMaximumSize();\n    if (!maxThickness && !minThickness) maxThickness = 1;\n  }\n  if (typeof stencil === 'undefined') {\n    var stencil = new $.oStencil(\"\", \"pencil\", {maxThickness:maxThickness, minThickness:minThickness, keys:[]});\n  }\n\n  if (typeof colorId === 'undefined'){\n    var _palette = this.$.scn.selectedPalette;\n    if (_palette) {\n      var _color = this.$.scn.selectedPalette.currentColor;\n      if (_color) {\n        var colorId = _color.id;\n      } else{\n        var colorId = \"0000000000000003\";\n      }\n    }\n  }\n\n  this.colorId = colorId;\n  this.stencil = stencil;\n\n  // this.$.debug(colorId+\" \"+minThickness+\" \"+maxThickness+\" \"+stencil, this.$.DEBUG_LEVEL.DEBUG)\n}\n\n\n/**\n * The minimum thickness of the line using this lineStyle\n * @name $.oLineStyle#minThickness\n * @type {float}\n */\nObject.defineProperty($.oLineStyle.prototype, \"minThickness\", {\n  get: function(){\n    return this.stencil.minThickness;\n  },\n\n  set: function(newMinThickness){\n    this.stencil.minThickness = newMinThickness;\n  }\n})\n\n\n/**\n * The minimum thickness of the line using this lineStyle\n * @name $.oLineStyle#maxThickness\n * @type {float}\n */\nObject.defineProperty($.oLineStyle.prototype, \"maxThickness\", {\n  get: function(){\n    return this.stencil.maxThickness;\n  },\n\n  set: function(newMaxThickness){\n    this.stencil.maxThickness = newMaxThickness;\n  }\n})\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oShape class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oShape class. These types of objects are not supported for harmony versions < 16\n * @constructor\n * @classdesc  $.oShape represents shapes drawn on the art layer. Strokes, colors, line styles, can be accessed through this class.<br>Warning, Toonboom stores strokes by index, so stroke objects may become obsolete when modifying the contents of the drawing.\n * @param   {int}                    index                      The index of the shape on the artLayer\n * @param   {$.oArtLayer}            oArtLayerObject            The oArtLayer this layer belongs to.\n *\n * @property {int}          index       the index of the shape in the parent artLayer\n * @property {$.oArtLayer}  artLayer    the art layer that contains this shape\n */\n$.oShape = function (index, oArtLayerObject) {\n  this.index = index;\n  this.artLayer = oArtLayerObject;\n}\n\n\n/**\n * the toonboom key object identifying this shape.\n * @name $.oShape#_key\n * @type {object}\n * @private\n * @readonly\n */\nObject.defineProperty($.oShape.prototype, '_key', {\n  get: function () {\n    var _key = this.artLayer._key;\n    return { drawing: _key.drawing, art: _key.art, layers: [this.index] };\n  }\n})\n\n\n/**\n * The underlying data describing the shape.\n * @name $.oShape#_data\n * @type {$.oShape[]}\n * @readonly\n * @private\n */\nObject.defineProperty($.oShape.prototype, '_data', {\n  get: function () {\n    return this.artLayer.drawingData.layers[this.index];\n  }\n})\n\n\n/**\n * The strokes making up the shape.\n * @name $.oShape#strokes\n * @type {$.oShape[]}\n * @readonly\n */\nObject.defineProperty($.oShape.prototype, 'strokes', {\n  get: function () {\n    if (!this.hasOwnProperty(\"_strokes\")) {\n      var _data = this._data;\n\n      if (!_data.hasOwnProperty(\"strokes\")) return [];\n\n      var _shape = this;\n      var _strokes = _data.strokes.map(function (x, idx) { return new _shape.$.oStroke(idx, x, _shape) })\n      this._strokes = _strokes;\n    }\n    return this._strokes;\n  }\n})\n\n\n/**\n * The contours (invisible strokes that can delimit colored areas) making up the shape.\n * @name $.oShape#contours\n * @type {$.oContour[]}\n * @readonly\n */\n Object.defineProperty($.oShape.prototype, 'contours', {\n  get: function () {\n    if (!this.hasOwnProperty(\"_contours\")) {\n      var _data = this._data\n\n      if (!_data.hasOwnProperty(\"contours\")) return [];\n\n      var _shape = this;\n      var _contours = _data.contours.map(function (x, idx) { return new this.$.oContour(idx, x, _shape) })\n      this._contours = _contours;\n    }\n    return this._contours;\n  }\n})\n\n\n/**\n * The fills styles contained in the shape\n * @name $.oShape#fills\n * @type {$.oFillStyle[]}\n * @readonly\n */\nObject.defineProperty($.oShape.prototype, 'fills', {\n  get: function () {\n    if (!this.hasOwnProperty(\"_fills\")) {\n      var _data = this._data\n\n      if (!_data.hasOwnProperty(\"contours\")) return [];\n\n      var _fills = _data.contours.map(function (x) { return new this.$.oFillStyle(x.colorId, x.matrix) })\n      this._fills = _fills;\n    }\n    return this._fills;\n  }\n})\n\n/**\n * The stencils used by the shape.\n * @name $.oShape#stencils\n * @type {$.oStencil[]}\n * @readonly\n */\nObject.defineProperty($.oShape.prototype, 'stencils', {\n  get: function () {\n    if (!this.hasOwnProperty(\"_stencils\")) {\n      var _data = this._data;\n      var _shape = this;\n      var _stencils = _data.thicknessPaths.map(function (x) { return new _shape.$.oStencil(\"\", \"pencil\", x) })\n      this._stencils = _stencils;\n    }\n    return this._stencils;\n  }\n})\n\n\n/**\n * The bounding box of the shape.\n * @name $.oShape#bounds\n * @type {$.oBox}\n * @readonly\n */\nObject.defineProperty($.oShape.prototype, 'bounds', {\n  get: function () {\n    var _bounds = new this.$.oBox();\n    var _contours = this.contours;\n    var _strokes = this.strokes;\n\n    for (var i in _contours){\n      _bounds.include(_contours[i].bounds);\n    }\n\n    for (var i in _strokes){\n      _bounds.include(_strokes[i].bounds);\n    }\n\n    return _bounds;\n  }\n})\n\n/**\n * The x coordinate of the shape.\n * @name $.oShape#x\n * @type {float}\n * @readonly\n */\nObject.defineProperty($.oShape.prototype, 'x', {\n  get: function () {\n    return this.bounds.left;\n  }\n})\n\n\n/**\n * The x coordinate of the shape.\n * @name $.oShape#x\n * @type {float}\n * @readonly\n */\nObject.defineProperty($.oShape.prototype, 'y', {\n  get: function () {\n    return this.bounds.top;\n  }\n})\n\n\n/**\n * The width of the shape.\n * @name $.oShape#width\n * @type {float}\n * @readonly\n */\nObject.defineProperty($.oShape.prototype, 'width', {\n  get: function () {\n    return this.bounds.width;\n  }\n})\n\n\n/**\n * The height coordinate of the shape.\n * @name $.oShape#height\n * @type {float}\n * @readonly\n */\nObject.defineProperty($.oShape.prototype, 'height', {\n  get: function () {\n    return this.bounds.height;\n  }\n})\n\n\n/**\n * Retrieve and set the selected status of each shape.\n * @name $.oShape#selected\n * @type {bool}\n */\nObject.defineProperty($.oShape.prototype, 'selected', {\n  get: function () {\n    var _selection = this.artLayer._selectedShapes;\n    var _indices = _selection.map(function (x) { return x.index });\n    return (_indices.indexOf(this.index) != -1)\n  },\n  set: function (newSelectedState) {\n    var _key = this.artLayer._key;\n\n    var currentSelection = Drawing.selection.get(_key);\n    var config = {drawing:_key.drawing, art:_key.art};\n\n    if (newSelectedState){\n      // adding elements to selection\n      config.selectedLayers = currentSelection.selectedLayers.concat([this.index]);\n      config.selectedStrokes = currentSelection.selectedStrokes;\n    }else{\n      config.selectedLayers = currentSelection.selectedLayers;\n      config.selectedStrokes = currentSelection.selectedStrokes;\n\n      // remove current element from selection before setting again\n      for (var i=config.selectedLayers.length-1; i>=0; i--){\n        if (config.selectedLayers[i] == this.index) config.selectedLayers.splice(i, 1);\n      }\n      for (var i=config.selectedStrokes.length-1; i>=0; i--){\n        if (config.selectedStrokes[i].layer == this.index) config.selectedStrokes.splice(i, 1);\n      }\n    }\n\n    Drawing.selection.set(config);\n  }\n})\n\n\n/**\n * Deletes the shape from its artlayer.\n * Updates the index of all other oShapes on the artLayer in order to\n * keep tracking all of them without having to query the drawing again.\n */\n$.oShape.prototype.remove = function(){\n  DrawingTools.deleteLayers(this._key);\n\n  // update shapes list for this artLayer\n  var shapes = this.artLayer.shapes\n  for (var i in shapes){\n    if (i > this.index){\n      shapes[i].index--;\n    }\n  }\n  shapes.splice(this.index, 1);\n}\n\n\n/**\n * Deletes the shape from its artlayer.\n * Warning : Because shapes are referenced by index, deleting a shape\n * that isn't at the end of the list of shapes from this layer\n * might render other shape objects from this layer obsolete.\n * Get them again with artlayer.shapes.\n * @deprecated use oShape.remove instead\n */\n$.oShape.prototype.deleteShape = function(){\n  this.remove();\n}\n\n\n/**\n * Gets a stroke from this shape by its index\n * @param {int} index\n *\n * @returns {$.oStroke}\n */\n$.oShape.prototype.getStrokeByIndex = function (index) {\n  return this.strokes[index];\n}\n\n\n$.oShape.prototype.toString = function (){\n  return \"<oShape index:\"+this.index+\", layer:\"+this.artLayer.name+\", drawing:'\"+this.artLayer._drawing.name+\"'>\"\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//       $.oFillStyle class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oFillStyle class.\n * @constructor\n * @classdesc\n * The $.oFillStyle class describes a fillStyle used to describe the appearance of filled in color areas and perform drawing operations. <br>\n * Initializing a $.oFillStyle without any parameters attempts to get the current color id.\n * @param {string}     colorId             the color Id to paint the line with.\n * @param {object}     fillMatrix\n */\n$.oFillStyle = function (colorId, fillMatrix) {\n  if (typeof fillMatrix === 'undefined') var fillMatrix = {\n    \"ox\": 1,\n    \"oy\": 1,\n    \"xx\": 1,\n    \"xy\": 0,\n    \"yx\": 0,\n    \"yy\": 1\n  }\n\n  if (typeof colorId === 'undefined'){\n    var _palette = this.$.scn.selectedPalette;\n    if (_palette) {\n      var _color = this.$.scn.selectedPalette.currentColor;\n      if (_color) {\n        var colorId = _color.id;\n      } else{\n        var colorId = \"0000000000000003\";\n      }\n    }\n  }\n\n  this.colorId = colorId;\n  this.fillMatrix = fillMatrix;\n\n  this.$.log(\"new fill created: \" + colorId + \" \" + JSON.stringify(this.fillMatrix))\n}\n\n\n$.oFillStyle.prototype.toString = function(){\n  return \"<oFillStyle colorId:\"+this.colorId+\", matrix:\"+JSON.stringify(this.fillMatrix)+\">\";\n}\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oStroke class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oStroke class. These types of objects are not supported for harmony versions < 16\n * @constructor\n * @classdesc  The $.oStroke class models the strokes that make up the shapes visible on the Drawings.\n * @param {int}       index             The index of the stroke in the shape.\n * @param {object}    strokeObject      The stroke object descriptor that contains the info for the stroke\n * @param {oShape}    oShapeObject      The parent oShape\n *\n * @property {int}          index       the index of the stroke in the parent shape\n * @property {$.oShape}     shape       the shape that contains this stroke\n * @property {$.oArtLayer}  artLayer    the art layer that contains this stroke\n */\n$.oStroke = function (index, strokeObject, oShapeObject) {\n  this.index = index;\n  this.shape = oShapeObject;\n  this.artLayer = oShapeObject.artLayer;\n  this._data = strokeObject;\n}\n\n\n/**\n * The $.oVertex (including bezier handles) making up the complete path of the stroke.\n * @name $.oStroke#path\n * @type {$.oVertex[]}\n * @readonly\n */\nObject.defineProperty($.oStroke.prototype, \"path\", {\n  get: function () {\n    // path vertices get cached\n    if (!this.hasOwnProperty(\"_path\")){\n      var _stroke = this;\n      var _path = this._data.path.map(function(point, index){\n        return new _stroke.$.oVertex(_stroke, point.x, point.y, point.onCurve, index);\n      })\n\n      this._path = _path;\n    }\n    return this._path;\n  }\n})\n\n\n/**\n * The oVertex that are on the stroke (Bezier handles excluded.)\n * The first is repeated at the last position when the stroke is closed.\n * @name $.oStroke#points\n * @type {$.oVertex[]}\n * @readonly\n */\nObject.defineProperty($.oStroke.prototype, \"points\", {\n  get: function () {\n    return this.path.filter(function(x){return x.onCurve});\n  }\n})\n\n\n/**\n * The segments making up the stroke. Each segment is a slice of the path, starting and stopping with oVertex present on the curve, and includes the bezier handles oVertex.\n * @name $.oStroke#segments\n * @type {$.oVertex[][]}\n * @readonly\n */\nObject.defineProperty($.oStroke.prototype, \"segments\", {\n  get: function () {\n    var _points = this.points;\n    var _path = this.path;\n    var _segments = [];\n\n    for (var i=0; i<_points.length-1; i++){\n      var _indexStart = _points[i].index;\n      var _indexStop = _points[i+1].index;\n      var _segment = _path.slice(_indexStart, _indexStop+1);\n      _segments.push(_segment);\n    }\n\n    return _segments;\n  }\n})\n\n\n/**\n * The index of the stroke in the shape\n * @name $.oStroke#index\n * @type {int}\n */\nObject.defineProperty($.oStroke.prototype, \"index\", {\n  get: function () {\n    this.$.debug(\"stroke object : \"+JSON.stringify(this._stroke, null, \"  \"), this.$.DEBUG_LEVEL.DEBUG);\n    return this._data.strokeIndex;\n  }\n})\n\n\n/**\n * The style of the stroke. null if the stroke is invisible\n * @name $.oStroke#style\n * @type {$.oLineStyle}\n */\nObject.defineProperty($.oStroke.prototype, \"style\", {\n  get: function () {\n    if (this._data.invisible){\n      return null;\n    }\n    var _colorId = this._data.pencilColorId;\n    var _stencil = this.shape.stencils[this._data.thickness];\n\n    return new this.$.oLineStyle(_colorId, _stencil);\n  }\n})\n\n\n/**\n * whether the stroke is a closed shape.\n * @name $.oStroke#closed\n * @type {bool}\n */\nObject.defineProperty($.oStroke.prototype, \"closed\", {\n  get: function () {\n    var _path = this.path;\n    $.log(_path)\n    $.log(_path[_path.length-1].strokePosition)\n    return _path[_path.length-1].strokePosition == 0;\n  }\n})\n\n\n/**\n * The bounding box of the stroke.\n * @name $.oStroke#bounds\n * @type {$.oBox}\n * @readonly\n */\n Object.defineProperty($.oStroke.prototype, 'bounds', {\n  get: function () {\n    var _bounds = new this.$.oBox();\n    // since Harmony doesn't allow natively to calculate the bounding box of a string,\n    // we convert the bezier into a series of points and calculate the box from it\n    var points = Drawing.geometry.discretize({precision: 1, path : this.path});\n    for (var j in points){\n      var point = points [j]\n      var pointBox = new this.$.oBox(point.x, point.y, point.x, point.y);\n      _bounds.include(pointBox);\n    }\n    return _bounds;\n  }\n})\n\n\n/**\n * The intersections on this stroke. Each intersection is an object with stroke ($.oStroke), point($.oPoint), strokePoint(float) and ownPoint(float)\n * One of these objects describes an intersection by giving the stroke it intersects with, the coordinates of the intersection and two values which represent the place on the stroke at which the point is placed, with a value between 0 (start) and 1(end)\n * @param  {$.oStroke}   [stroke]       Specify a stroke to find intersections specific to it. If no stroke is specified,\n * @return {Object[]}\n * @example\n// get the selected strokes on the active drawing\nvar sel = $.scn.activeDrawing.selectedStrokes;\n\nfor (var i in sel){\n  // get intersections with all other elements of the drawing\n\tvar intersections = sel[i].getIntersections();\n\n  for (var j in intersections){\n    log(\"intersection : \" + j);\n    log(\"point : \" + intersections[j].point);                    // the point coordinates\n    log(\"strokes index : \" + intersections[j].stroke.index);     // the index of the intersecting strokes in their own shape\n    log(\"own point : \" + intersections[j].ownPoint);             // how far the intersection is on the stroke itself\n    log(\"stroke point : \" + intersections[j].strokePoint);       // how far the intersection is on the intersecting stroke\n  }\n}\n */\n$.oStroke.prototype.getIntersections = function (stroke){\n  if (typeof stroke !== 'undefined'){\n    // get intersection with provided stroke only\n    var _key = { \"path0\": [{ path: this.path }], \"path0\": [{ path: stroke.path }] };\n    var intersections = Drawing.query.getIntersections(_key)[0];\n  }else{\n    // get all intersections on the stroke\n    var _drawingKey = this.artLayer._key;\n    var _key = { \"drawing\": _drawingKey.drawing, \"art\": _drawingKey.art, \"paths\": [{ path: this.path }] };\n    var intersections = Drawing.query.getIntersections(_key)[0];\n  }\n\n  var result = [];\n  for (var i in intersections) {\n    var _shape = this.artLayer.getShapeByIndex(intersections[i].layer);\n    var _stroke = _shape.getStrokeByIndex(intersections[i].strokeIndex);\n\n    for (var j in intersections[i].intersections){\n      var points = intersections[i].intersections[j];\n\n      var point = new this.$.oVertex(this, points.x0, points.y0, true);\n      var intersection = { stroke: _stroke, point: point, ownPoint: points.t0, strokePoint: points.t1 };\n\n      result.push(intersection);\n    }\n  }\n\n  return result;\n}\n\n\n\n/**\n * Adds points on the stroke without moving them, at the distance specified (0=start vertice, 1=end vertice)\n * @param   {float[]}       pointsToAdd     an array of float value between 0 and the number of current points on the curve\n * @returns {$.oVertex[]}   the points that were created (if points already existed, they will be returned)\n * @example\n// get the selected stroke and create points where it intersects with the other two strokes\nvar sel = $.scn.activeDrawing.selectedStrokes[0];\n\nvar intersections = sel.getIntersections();\n\n// get the two intersections\nvar intersection1 = intersections[0];\nvar intersection2 = intersections[1];\n\n// add the points at the intersections on the intersecting strokes\nintersection1.stroke.addPoints([intersection1.strokePoint]);\nintersection2.stroke.addPoints([intersection2.strokePoint]);\n\n// add the points on the stroke\nsel.addPoints([intersection1.ownPoint, intersection2.ownPoint]);\n*/\n$.oStroke.prototype.addPoints = function (pointsToAdd) {\n  // calculate the points that will be created\n  var points = Drawing.geometry.insertPoints({path:this._data.path, params : pointsToAdd});\n\n  // find the newly added points amongst the returned values\n  for (var i in this.path){\n    var pathPoint = this.path[i];\n\n    // if point is found in path, it's not newly created\n    for (var j = points.length-1; j >=0; j--){\n      var point = points[j];\n      if (point.x == pathPoint.x && point.y == pathPoint.y) {\n        points.splice(j, 1);\n        break\n      }\n    }\n  }\n\n  // actually add the points\n  var config = this.artLayer._key;\n  config.label = \"addPoint\";\n  config.strokes = [{layer:this.shape.index, strokeIndex:this.index, insertPoints: pointsToAdd }];\n\n  DrawingTools.modifyStrokes(config);\n  this.updateDefinition();\n\n  var newPoints = [];\n  // find the points for the coordinates from the new path\n  for (var i in points){\n    var point = points[i];\n\n    for (var j in this.path){\n      var pathPoint = this.path[j];\n      if (point.x == pathPoint.x && point.y == pathPoint.y) newPoints.push(pathPoint);\n    }\n  }\n\n  if (newPoints.length != pointsToAdd.length) throw new Error (\"some points in \" + pointsToAdd + \" were not created.\");\n  return newPoints;\n}\n\n\n/**\n * fetch the stroke information again to update it after modifications.\n * @returns {object} the data definition of the stroke, for internal use.\n */\n$.oStroke.prototype.updateDefinition = function(){\n  var _key = this.artLayer._key;\n  var strokes = Drawing.query.getStrokes(_key);\n  this._data = strokes.layers[this.shape.index].strokes[this.index];\n\n  // remove cache for path\n  delete this._path;\n\n  return this._data;\n}\n\n\n/**\n * Gets the closest position of the point on the stroke (float value) from a point with x and y coordinates.\n * @param {oPoint}  point\n * @return {float}  the strokePosition of the point on the stroke (@see $.oVertex#strokePosition)\n */\n$.oStroke.prototype.getPointPosition = function(point){\n  var arg = {\n    path : this.path,\n    points: [{x:point.x, y:point.y}]\n  }\n  var strokePoint = Drawing.geometry.getClosestPoint(arg)[0].closestPoint;\n  if (!strokePoint) return 0; // the only time this fails is when the point is the origin of the stroke\n\n  return strokePoint.t;\n}\n\n\n/**\n * Get the coordinates of the point on the stroke from its strokePosition (@see $.oVertex#strokePosition).\n * Only works until a distance of 600 drawing vector units.\n * @param {float}  position\n * @return {$.oPoint} an oPoint object containing the coordinates.\n */\n$.oStroke.prototype.getPointCoordinates = function(position){\n  var arg = {\n    path : this.path,\n    params : [ position ]\n  };\n  var point = Drawing.geometry.evaluate(arg)[0];\n\n  return new $.oPoint(point.x, point.y);\n}\n\n\n/**\n * projects a point onto a stroke and returns the closest point belonging to the stroke.\n * Only works until a distance of 600 drawing vector units.\n * @param {$.oPoint} point\n * @returns {$.oPoint}\n */\n$.oStroke.prototype.getClosestPoint = function (point){\n  var arg = {\n    path : this.path,\n    points: [{x:point.x, y:point.y}]\n  };\n\n  // returns an array of length 1 with an object containing\n  // the original query and a \"closestPoint\" key that contains the information.\n  var _result = Drawing.geometry.getClosestPoint(arg)[0];\n\n  return new $.oPoint(_result.closestPoint.x, _result.closestPoint.y);\n}\n\n\n/**\n * projects a point onto a stroke and returns the distance between the point and the stroke.\n * Only works until a distance of 600 drawing vector units.\n * @param {$.oPoint} point\n * @returns {float}\n */\n$.oStroke.prototype.getPointDistance = function (point){\n  var arg = {\n    path : this.path,\n    points: [{x:point.x, y:point.y}]\n  };\n\n  // returns an array of length 1 with an object containing\n  // the original query and a \"closestPoint\" key that contains the information.\n  var _result = Drawing.geometry.getClosestPoint(arg)[0].closestPoint;\n\n  return _result.distance;\n}\n\n\n/**\n * @private\n */\n$.oStroke.prototype.toString = function(){\n  return \"<oStroke: path:\"+this.path+\">\"\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oContour class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oContour class. These types of objects are not supported for harmony versions < 16\n * @constructor\n * @classdesc  The $.oContour class models the strokes that make up the shapes visible on the Drawings.<br>\n * $.oContour is a subclass of $.oSroke and shares its properties, but represents a stroke with a fill.\n * @extends $.oStroke\n * @param {int}       index             The index of the contour in the shape.\n * @param {object}    contourObject     The stroke object descriptor that contains the info for the stroke\n * @param {oShape}    oShapeObject      The parent oShape\n *\n * @property {int}          index       the index of the stroke in the parent shape\n * @property {$.oShape}     shape       the shape that contains this stroke\n * @property {$.oArtLayer}  artLayer    the art layer that contains this stroke\n */\n$.oContour = function (index, contourObject, oShapeObject) {\n  this.$.oStroke.call(this, index, contourObject, oShapeObject)\n}\n$.oContour.prototype = Object.create($.oStroke.prototype)\n\n\n/**\n * The information about the fill of this contour\n * @name $.oContour#fill\n * @type {$.oFillStyle}\n */\nObject.defineProperty($.oContour.prototype, \"fill\", {\n  get: function () {\n    var _data = this._data;\n    return new this.$.oFillStyle(_data.colorId, _data.matrix);\n  }\n})\n\n\n/**\n * The bounding box of the contour.\n * @name $.oContour#bounds\n * @type {$.oBox}\n * @readonly\n */\n Object.defineProperty($.oContour.prototype, 'bounds', {\n  get: function () {\n    var _data = this._data;\n    var _box = _data.box;\n    var _bounds = new this.$.oBox(_box.x0,_box.y0, _box.x1, _box.y1);\n    return _bounds;\n  }\n})\n\n/**\n * @private\n */\n$.oContour.prototype.toString = function(){\n  return \"<oContour path:\"+this.path+\", fill:\"+fill+\">\"\n}\n\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oVertex class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oVertex class\n * @constructor\n * @classdesc\n * The $.oVertex class represents a single control point on a stroke. This class is used to get the index of the point in the stroke path sequence, as well as its position as a float along the stroke's length.\n * The onCurve property describes whether this control point is a bezier handle or a point on the curve.\n *\n * @param {$.oStroke} stroke   the stroke that this vertex belongs to\n * @param {float}     x        the x coordinate of the vertex, in drawing space\n * @param {float}     y        the y coordinate of the vertex, in drawing space\n * @param {bool}      onCurve  whether the point is a bezier handle or situated on the curve\n * @param {int}       index    the index of the point on the stroke\n *\n * @property {$.oStroke} stroke    the stroke that this vertex belongs to\n * @property {float}     x         the x coordinate of the vertex, in drawing space\n * @property {float}     y         the y coordinate of the vertex, in drawing space\n * @property {bool}      onCurve   whether the point is a bezier handle or situated on the curve\n * @property {int}       index     the index of the point on the stroke\n */\n$.oVertex = function(stroke, x, y, onCurve, index){\n  if (typeof onCurve === 'undefined') var onCurve = false;\n  if (typeof index === 'undefined') var index = stroke.getPointPosition({x:x, y:y});\n\n  this.x = x;\n  this.y = y;\n  this.onCurve = onCurve;\n  this.stroke = stroke;\n  this.index = index\n}\n\n\n/**\n * The position of the point on the curve, from 0 to the maximum number of points\n * @name $.oVertex#strokePosition\n * @type {float}\n * @readonly\n */\nObject.defineProperty($.oVertex.prototype, 'strokePosition', {\n  get: function(){\n    var _position = this.stroke.getPointPosition(this);\n    return _position;\n  }\n})\n\n\n/**\n * The position of the point on the drawing, as an oPoint\n * @name $.oVertex#position\n * @type {oPoint}\n * @readonly\n */\nObject.defineProperty($.oVertex.prototype, 'position', {\n  get: function(){\n    var _position = new this.$.oPoint(this.x, this.y, 0);\n    return _position;\n  }\n})\n\n\n/**\n * The angle of the curve going through this vertex, compared to the x axis, counterclockwise.\n * (In degrees, or null if the stroke is open ended on the right.)\n * @name $.oVertex#angleRight\n * @type {float}\n * @readonly\n */\nObject.defineProperty($.oVertex.prototype, 'angleRight', {\n  get: function(){\n    var _index = this.index+1;\n    var _path = this.stroke.path;\n\n    // get the next point by looping around if the stroke is closed\n    if (_index >= _path.length){\n      if (this.stroke.closed){\n        var _nextPoint = _path[1];\n      }else{\n        return null;\n      }\n    }else{\n      var _nextPoint = _path[_index];\n    }\n\n    var vector = this.$.oVector.fromPoints(this, _nextPoint);\n    var angle = vector.degreesAngle;\n    // if (angle < 0) angle += 360 //ensuring only positive values\n    return angle\n  }\n})\n\n\n/**\n * The angle of the line or bezier handle on the left of this vertex, compared to the x axis, counterclockwise.\n * (In degrees, or null if the stroke is open ended on the left.)\n * @name $.oVertex#angleLeft\n * @type {float}\n * @readonly\n */\nObject.defineProperty($.oVertex.prototype, 'angleLeft', {\n  get: function(){\n    var _index = this.index-1;\n    var _path = this.stroke.path;\n\n    // get the next point by looping around if the stroke is closed\n    if (_index < 0){\n      if (this.stroke.closed){\n        var _nextPoint = _path[_path.length-2]; //first and last points are the same when the stroke is closed\n      }else{\n        return null;\n      }\n    }else{\n      var _nextPoint = _path[_index];\n    }\n\n    var vector = this.$.oVector.fromPoints(_nextPoint, this);\n    var angle = vector.degreesAngle;\n    // if (angle < 0) angle += 360 //ensuring only positive values\n    return angle\n  }\n})\n\n\n/**\n * @private\n */\n$.oVertex.prototype.toString = function(){\n return \"oVertex : { index:\"+this.index+\", x: \"+this.x+\", y: \"+this.y+\", onCurve: \"+this.onCurve+\", strokePosition: \"+this.strokePosition+\" }\"\n}\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oStencil class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oStencil class.\n * @constructor\n * @classdesc  The $.oStencil class allows access to some of the settings, name and type of the stencils available in the Harmony UI. <br>\n * Harmony stencils can have the following types: \"pencil\", \"penciltemplate\", \"brush\", \"texture\", \"bitmapbrush\" and \"bitmaperaser\". Each type is only available to specific tools. <br>\n * Access the main size information of the brush with the mainBrushShape property.\n * @param   {string}   xmlDescription        the part of the penstyles.xml file between <pen> tags that describe a stencils.\n * @property {string}  name                  the display name of the stencil\n * @property {string}  type                  the type of stencil\n * @property {Object}  thicknessPathObject   the description of the shape of the stencil\n */\n$.oStencil = function (name, type, thicknessPathObject) {\n  this.name = name;\n  this.type = type;\n  this.thicknessPathObject = thicknessPathObject;\n  // log(\"thicknessPath: \" + JSON.stringify(this.thicknessPathObject))\n}\n\n\n/**\n * The minimum thickness of the line using this stencil\n * @name $.oStencil#minThickness\n * @type {float}\n */\nObject.defineProperty($.oStencil.prototype, \"minThickness\", {\n  get: function(){\n    return this.thicknessPathObject.minThickness;\n  },\n  set: function(newMinThickness){\n    this.thicknessPathObject.minThickness = newMinThickness;\n    // TODO: also change in thicknessPath.keys\n  }\n})\n\n\n/**\n * The maximum thickness of the line using this stencil\n * @name $.oStencil#maxThickness\n * @type {float}\n */\nObject.defineProperty($.oStencil.prototype, \"maxThickness\", {\n  get: function(){\n    return this.thicknessPathObject.maxThickness;\n  },\n  set: function(newMaxThickness){\n    this.thicknessPathObject.maxThickness = newMaxThickness;\n    // TODO: also change in thicknessPath.keys\n  }\n})\n\n\n/**\n * Parses the xml string of the stencil xml description to create an object with all the information from it.\n * @private\n */\n$.oStencil.getFromXml = function (xmlString) {\n  var object = this.prototype.$.oStencil.getSettingsFromXml(xmlString)\n\n  var maxThickness = object.mainBrushShape.sizeRange.maxValue\n  var minThickness = object.mainBrushShape.sizeRange.minPercentage * maxThickness\n\n  var thicknessPathObject = {\n    maxThickness:maxThickness,\n    minThickness:minThickness,\n    keys: [\n      {t:0},\n      {t:1}\n    ]\n  }\n\n  var _stencil = new this.$.oStencil(object.name, object.style, thicknessPathObject)\n  for (var i in object) {\n    try{\n      // attempt to set values from the object\n      _stencil[i] = _settings[i];\n    }catch(err){\n      this.$.log(err)\n    }\n  }\n  return _stencil;\n}\n\n\n/**\n * Parses the xml string of the stencil xml description to create an object with all the information from it.\n * @private\n */\n$.oStencil.getSettingsFromXml = function (xmlString) {\n  var object = {};\n  var objectRE = /<(\\w+)>([\\S\\s]*?)<\\/\\1>/igm\n  var match;\n  var string = xmlString + \"\";\n  while (match = objectRE.exec(xmlString)) {\n    object[match[1]] = this.prototype.$.oStencil.getSettingsFromXml(match[2]);\n    // remove the match from the string to parse the rest as properties\n    string = string.replace(match[0], \"\");\n  }\n\n  var propsRE = /<(\\w+) value=\"([\\S\\s]*?)\"\\/>/igm\n  var match;\n  while (match = propsRE.exec(string)) {\n    // try to convert the value to int, float or bool\n    var value = match[2];\n    var intValue = parseInt(value, 10);\n    var floatValue = parseFloat(value);\n    if (value == \"true\" || value == \"false\") {\n      value = !!value;\n    } else if (!isNaN(floatValue)) {\n      if (intValue == floatValue) {\n        value = intValue;\n      } else {\n        value = floatValue;\n      }\n    }\n\n    object[match[1]] = match[2];\n  }\n\n  return object;\n}\n\n$.oStencil.prototype.toString = function (){\n  return \"$.oStencil: '\" + this.name + \"'\"\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_element.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library v0.01\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney...\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the MIT license.\n//   https://opensource.org/licenses/mit\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oElement class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The base class for the $.oElement.<br> Elements hold the drawings displayed by a \"READ\" Node or Drawing Node. They can be used to create new drawings, rename them, etc.\n * @constructor\n * @classdesc  $.oElement Class\n * @param   {int}                   id                          The element ID.\n * @param   {$.oColumn}             oColumnObject               The column object associated to the element.\n *\n * @property {int}                  id                          The element ID.\n * @property {$.oColumn}            oColumnObject               The column object associated to the element.\n */\n$.oElement = function( id, oColumnObject){\n  this._type = \"element\";\n\n  this.id = id;\n  this.column = oColumnObject;\n}\n\n// $.oElement Object Properties\n\n/**\n * The name of the element.\n * @name $.oElement#name\n * @type {string}\n */\nObject.defineProperty($.oElement.prototype, 'name', {\n    get : function(){\n         return element.getNameById(this.id)\n    },\n\n    set : function(newName){\n         element.renameById(this.id, newName);\n    }\n})\n\n\n/**\n * The folder path of the element on the filesystem.\n * @name $.oElement#path\n * @type {string}\n */\nObject.defineProperty($.oElement.prototype, 'path', {\n    get : function(){\n         return fileMapper.toNativePath(element.completeFolder(this.id))\n    }\n})\n\n\n/**\n * The drawings available in the element.\n * @name $.oElement#drawings\n * @type {$.oDrawing[]}\n */\nObject.defineProperty($.oElement.prototype, 'drawings', {\n  get : function(){\n    var _drawingsNumber = Drawing.numberOf(this.id);\n    var _drawings = [];\n    for (var i=0; i<_drawingsNumber; i++){\n      _drawings.push( new this.$.oDrawing(Drawing.name(this.id, i), this) );\n    }\n    return _drawings;\n  }\n})\n\n\n/**\n * The file format of the element.\n * @name $.oElement#format\n * @type {string}\n */\nObject.defineProperty($.oElement.prototype, 'format', {\n  get : function(){\n    var _type = element.pixmapFormat(this.id);\n    if (element.vectorType(this.id)) _type = \"TVG\";\n    return _type;\n  }\n})\n\n\n/**\n * The palettes linked to this element.\n * @name $.oElement#palettes\n * @type {$.oPalette[]}\n */\nObject.defineProperty($.oElement.prototype, 'palettes', {\n  get: function(){\n    var _paletteList = PaletteObjectManager.getPaletteListByElementId(this.id);\n    var _palettes = [];\n    for (var i=0; i<_paletteList.numPalettes; i++){\n      _palettes.push( new this.$.oPalette( _paletteList.getPaletteByIndex(i), _paletteList ) );\n    }\n\n    return _palettes;\n  }\n})\n\n\n// $.oElement Class methods\n\n/**\n * Adds a drawing to the element. Provide a filename to import an external file as a drawing.\n * @param   {int}        [atFrame=1]            The frame at which to add the drawing on the $.oDrawingColumn. Values < 1 create no exposure.\n * @param   {name}       [name]                 The name of the drawing to add.\n * @param   {string}     [filename]             Optionally, a path for a drawing file to use for this drawing. Can pass an oFile object as well.\n * @param   {bool}       [convertToTvg=false]   If the filename isn't a tvg file, specify if you want it converted (this doesn't vectorize the drawing).\n *\n * @return {$.oDrawing}      The added drawing\n */\n$.oElement.prototype.addDrawing = function( atFrame, name, filename, convertToTvg ){\n  if (typeof atFrame === 'undefined') var atFrame = 1;\n  if (typeof filename === 'undefined') var filename = null;\n  var nameByFrame = this.$.app.preferences.XSHEET_NAME_BY_FRAME;\n  if (typeof name === 'undefined') var name = nameByFrame?atFrame:1;\n  var name = name +\"\"; // convert name to string\n\n  // ensure a new drawing is always created by incrementing depending on preference\n  var _drawingNames = this.drawings.map(function(x){return x.name}); // index of existing names\n  var _nameFormat = /(.*?)_(\\d+)$/\n  while (_drawingNames.indexOf(name) != -1){\n    if (nameByFrame || isNaN(name)){\n      var nameGroups = name.match(_nameFormat);\n      if (nameGroups){\n        // increment the part after the underscore\n        name = nameGroups[1] + \"_\" + (parseInt(nameGroups[2])+1);\n      }else{\n        name += \"_1\";\n      }\n    }else{\n      name = parseInt(name, 10);\n      if (isNaN(name)) name = 0;\n      name = name + 1 + \"\"; // increment and convert back to string\n    }\n  }\n\n  if (!(filename instanceof this.$.oFile)) filename = new this.$.oFile(filename);\n  var _fileExists = filename.exists;\n  Drawing.create (this.id, name, _fileExists, true);\n\n  var _drawing = new this.$.oDrawing( name, this );\n\n  if (_fileExists) _drawing.importBitmap(filename, convertToTvg);\n\n  // place drawing on the column at the provided frame\n  if (this.column != null || this.column != undefined && atFrame >= 1){\n    column.setEntry(this.column.uniqueName, 1, atFrame, name);\n  }\n\n  return _drawing;\n}\n\n\n/**\n * Gets a drawing object by the name.\n * @param   {string}  name  The name of the drawing to get.\n *\n * @return  {$.oDrawing}      The drawing found by the search\n */\n$.oElement.prototype.getDrawingByName = function ( name ){\n  var _drawings = this.drawings;\n  for (var i in _drawings){\n    if (_drawings[i].name == name) return _drawings[i];\n  }\n  return null;\n}\n\n\n/**\n * Link a provided palette to an element as an Element palette.\n * @param   {$.oPalette}    oPaletteObject              The oPalette object to link\n * @param   {int}           [listIndex]              The index in the element palette list at which to add the newly linked palette\n * @return  {$.oPalette}    The linked element palette.\n */\n$.oElement.prototype.linkPalette = function ( oPaletteObject , listIndex){\n  var _paletteList = PaletteObjectManager.getPaletteListByElementId(this.id);\n  if (typeof listIndex === 'undefined') var listIndex = _paletteList.numPalettes;\n\n  var _palettePath = oPaletteObject.path.path.replace(\".plt\", \"\");\n\n  var _palette = new this.$.oPalette(_paletteList.insertPalette (_palettePath, listIndex), _paletteList);\n  return _palette;\n}\n\n\n/**\n * If the palette passed as a parameter is linked to this element, it will be unlinked, and moved to the scene palette list.\n * @param {$.oPalette} oPaletteObject\n * @return {bool} the success of the unlinking process.\n */\n$.oElement.prototype.unlinkPalette = function (oPaletteObject) {\n  var _palettes = this.palettes;\n  var _ids = _palettes.map(function(x){return x.id});\n  var _paletteId = oPaletteObject.id;\n  var _paletteIndex = _ids.indexOf(_paletteId);\n\n  if (_paletteIndex == -1) return; // palette already isn't linked\n\n  var _palette = _palettes[_paletteIndex];\n  try{\n    _palette.remove(false);\n    return true;\n  }catch(err){\n    this.$.debug(\"Failed to unlink palette \"+_palette.name+\" from element \"+this.name);\n    return false;\n  }\n}\n\n\n\n/**\n * Duplicate an element.\n * @param   {string}     [name]              The new name for the duplicated element.\n * @return  {$.oElement}      The duplicate element\n */\n$.oElement.prototype.duplicate = function(name){\n  if (typeof name === 'undefined') var name = this.name;\n\n  var _fieldGuide = element.fieldChart(this.id);\n  var _scanType = element.scanType(this.id);\n\n  var _duplicateElement = this.$.scene.addElement(name, this.format, _fieldGuide, _scanType);\n\n  var _drawings = this.drawings;\n  var _elementFolder = new this.$.oFolder(_duplicateElement.path);\n\n  for (var i in _drawings){\n    var _drawingFile = new this.$.oFile(_drawings[i].path);\n    try{\n      var duplicateDrawing = _duplicateElement.addDrawing(0, _drawings[i].name, _drawingFile);\n      _drawingFile.copy(_elementFolder, duplicateDrawing.name, true);\n    }catch(err){\n      this.debug(\"could not copy drawing file \"+drawingFile.name+\" into element \"+_duplicateElement.name, this.DEBUG_LEVEL.ERROR);\n    }\n  }\n  return _duplicateElement;\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_file.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oFolder class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The $.oFolder helper class -- providing utilities for folder manipulation and access.\n * @constructor\n * @classdesc  $.oFolder Base Class\n * @param       {string}              path                      The path to the folder.\n *\n * @property    {string}             path                      The path to the folder.\n */\n$.oFolder = function(path){\n    this._type = \"folder\";\n    this._path = fileMapper.toNativePath(path).split(\"\\\\\").join(\"/\");\n\n    // fix lowercase drive letter\n    var path_components = this._path.split(\"/\");\n    if (path_components[0] && about.isWindowsArch()){\n      // local path that starts with a drive letter\n      path_components[0] = path_components[0].toUpperCase()\n      this._path = path_components.join(\"/\");\n    }\n}\n\n\n/**\n * The path of the folder.  Setting a path doesn't move the file, only changes where the file object is pointing.\n * @name $.oFolder#path\n * @type {string}\n */\nObject.defineProperty($.oFolder.prototype, 'path', {\n    get: function(){\n      return this._path;\n    },\n    set: function( newPath ){\n      this._path = fileMapper.toNativePath( newPath ).split(\"\\\\\").join(\"/\");\n    }\n});\n\n\n/**\n * The path of the file encoded as a toonboom relative path.\n * @name $.oFile#toonboomPath\n * @readonly\n * @type {string}\n */\nObject.defineProperty( $.oFolder.prototype, 'toonboomPath', {\n  get: function(){\n    var _path = this._path;\n    if (!this.$.scene.online) return _path;\n    if (_path.slice(0,2) != (\"//\")) return _path;\n\n    var _pathComponents = _path.replace(\"//\", \"\").split(\"/\");\n    var _drive = (_pathComponents[1]==\"usadata000\")?_pathComponents[1]:_pathComponents[1].toUpperCase();\n    var _path = _pathComponents.slice(2);\n\n    return [\"\",_drive].concat(_path).join(\"/\");\n  }\n});\n\n\n/**\n * The name of the folder.\n * @name $.oFolder#name\n * @type {string}\n */\nObject.defineProperty($.oFolder.prototype, 'name', {\n    get: function(){\n        var _name = this.path.split(\"/\");\n        _name = _name.pop();\n        return _name;\n    },\n    set: function(newName){\n      this.rename(newName)\n    }\n});\n\n\n/**\n * The parent folder.\n * @name $.oFolder#folder\n * @type {$.oFolder}\n */\nObject.defineProperty($.oFolder.prototype, 'folder', {\n    get: function(){\n        var _folder = this.path.slice(0,this.path.lastIndexOf(\"/\", this.path.length-2));\n        return new this.$.oFolder(_folder);\n    }\n});\n\n\n/**\n * The parent folder.\n * @name $.oFolder#exists\n * @type {string}\n */\nObject.defineProperty($.oFolder.prototype, 'exists', {\n    get: function(){\n        var dir = new QDir;\n        dir.setPath(this.path)\n        return dir.exists();\n    }\n});\n\n\n/**\n * The files in the folder.\n * @name $.oFolder#files\n * @type {$.oFile[]}\n * @deprecated use oFolder.getFiles() instead to specify filter\n */\nObject.defineProperty($.oFolder.prototype, 'files', {\n    get: function(){\n      var dir = new QDir;\n      dir.setPath(this.path);\n      dir.setFilter( QDir.Files );\n\n      if (!dir.exists) throw new Error(\"can't get files from folder \"+this.path+\" because it doesn't exist\");\n\n      return dir.entryList().map(function(x){return new this.$.oFile(dir.path()+\"/\"+x)});\n    }\n});\n\n\n/**\n * The folders within this folder.\n * @name $.oFolder#folders\n * @type {$.oFile[]}\n * @deprecated oFolder.folder is the containing parent folder, it can't also mean the children folders\n */\nObject.defineProperty($.oFolder.prototype, 'folders', {\n    get: function(){\n      var _dir = new QDir;\n      _dir.setPath(this.path);\n      if (!_dir.exists) throw new Error(\"can't get files from folder \"+this.path+\" because it doesn't exist\");\n      _dir.setFilter(QDir.Dirs);\n      var _folders = _dir.entryList();\n\n      for (var i = _folders.length-1; i>=0; i--){\n          if (_folders[i] == \".\" || _folders[i] == \"..\") _folders.splice(i,1);\n      }\n\n      return _folders.map(function(x){return new this.$.oFolder( _dir.path() + \"/\" + x )});\n    }\n});\n\n\n/**\n * The content within the folder -- both folders and files.\n * @name $.oFolder#content\n * @type {$.oFile/$.oFolder[] }\n */\nObject.defineProperty($.oFolder.prototype, 'content', {\n    get: function(){\n      var content = this.files;\n          content = content.concat( this.folders );\n      return content;\n    }\n});\n\n\n/**\n * Lists the file names contained inside the folder.\n * @param   {string}   [filter]               Filter wildcards for the content of the folder.\n *\n * @returns {string[]}   The names of the files contained in the folder that match the filter.\n */\n$.oFolder.prototype.listFiles = function(filter){\n    if (typeof filter === 'undefined') var filter = \"*\";\n\n    var _dir = new QDir;\n    _dir.setPath(this.path);\n    if (!_dir.exists) throw new Error(\"can't get files from folder \"+this.path+\" because it doesn't exist\");\n    _dir.setNameFilters([filter]);\n    _dir.setFilter( QDir.Files);\n    var _files = _dir.entryList();\n\n    return _files;\n}\n\n\n/**\n * get the files from the folder\n * @param   {string}   [filter]     Filter wildcards for the content of the folder.\n *\n * @returns {$.oFile[]}   A list of files contained in the folder as oFile objects.\n */\n$.oFolder.prototype.getFiles = function( filter ){\n    if (typeof filter === 'undefined') var filter = \"*\";\n    // returns the list of $.oFile in a directory that match a filter\n\n    var _path = this.path;\n\n    var _files = [];\n    var _file_list = this.listFiles(filter);\n    for( var i in _file_list){\n      _files.push( new this.$.oFile( _path+'/'+_file_list[i] ) );\n    }\n\n    return _files;\n}\n\n\n/**\n * lists the folder names contained inside the folder.\n * @param   {string}   [filter=\"*.*\"]    Filter wildcards for the content of the folder.\n *\n * @returns {string[]}  The names of the files contained in the folder that match the filter.\n */\n$.oFolder.prototype.listFolders = function(filter){\n\n    if (typeof filter === 'undefined') var filter = \"*\";\n\n    var _dir = new QDir;\n    _dir.setPath(this.path);\n\n    if (!_dir.exists){\n      this.$.debug(\"can't get files from folder \"+this.path+\" because it doesn't exist\", this.$.DEBUG_LEVEL.ERROR);\n      return [];\n    }\n\n    _dir.setNameFilters([filter]);\n    _dir.setFilter(QDir.Dirs); //QDir.NoDotAndDotDot not supported?\n    var _folders = _dir.entryList();\n\n    _folders = _folders.filter(function(x){return x!= \".\" && x!= \"..\"})\n\n    return _folders;\n}\n\n\n/**\n * gets the folders inside the oFolder\n * @param   {string}   [filter]              Filter wildcards for the content of the folder.\n *\n * @returns {$.oFolder[]}      A list of folders contained in the folder, as oFolder objects.\n */\n$.oFolder.prototype.getFolders = function( filter ){\n    if (typeof filter === 'undefined') var filter = \"*\";\n    // returns the list of $.oFile in a directory that match a filter\n\n    var _path = this.path;\n\n    var _folders = [];\n    var _folders_list = this.listFolders(filter);\n    for( var i in _folders_list){\n      _folders.push( new this.$.oFolder(_path+'/'+_folders_list[i]));\n    }\n\n    return _folders;\n}\n\n\n /**\n * Creates the folder, if it doesn't already exist.\n * @returns { bool }      The existence of the newly created folder.\n */\n$.oFolder.prototype.create = function(){\n  if( this.exists ){\n    this.$.debug(\"folder \"+this.path+\" already exists and will not be created\", this.$.DEBUG_LEVEL.WARNING)\n    return true;\n  }\n\n  var dir = new QDir(this.path);\n\n  dir.mkpath(this.path);\n  if (!this.exists) throw new Error (\"folder \" + this.path + \" could not be created.\")\n}\n\n\n/**\n * Copy the folder and its contents to another path.\n * @param   {string}   folderPath          The path to an existing folder in which to copy this folder. (Can provide an oFolder)\n * @param   {string}   [copyName]          Optionally, a name for the folder copy, if different from the original\n * @param   {bool}     [overwrite=false]   Whether to overwrite the files that are already present at the copy location.\n * @returns {$.oFolder} the oFolder describing the newly created copy.\n */\n$.oFolder.prototype.copy = function( folderPath, copyName, overwrite ){\n  // TODO: it should propagate errors from the recursive copy and throw them before ending?\n  if (typeof overwrite === 'undefined') var overwrite = false;\n  if (typeof copyName === 'undefined' || !copyName) var copyName = this.name;\n  if (!(folderPath instanceof this.$.oFolder)) folderPath = new $.oFolder(folderPath);\n  if (this.name == copyName && folderPath == this.folder.path) copyName += \"_copy\";\n\n  if (!folderPath.exists) throw new Error(\"Target folder \" + folderPath +\" doesn't exist. Can't copy folder \"+this.path)\n\n  var nextFolder = new $.oFolder(folderPath.path + \"/\" + copyName);\n  nextFolder.create();\n  var files = this.getFiles();\n  for (var i in files){\n    var _file = files[i];\n    var targetFile = new $.oFile(nextFolder.path + \"/\" + _file.fullName);\n\n    // deal with overwriting\n    if (targetFile.exists && !overwrite){\n      this.$.debug(\"File \" + targetFile + \" already exists, skipping copy of \"+ _file, this.$.DEBUG_LEVEL.ERROR);\n      continue;\n    }\n\n    _file.copy(nextFolder, undefined, overwrite);\n  }\n  var folders = this.getFolders();\n  for (var i in folders){\n    folders[i].copy(nextFolder, undefined, overwrite);\n  }\n\n  return nextFolder;\n}\n\n\n/**\n * Move this folder to the specified path.\n * @param   {string}   destFolderPath           The new complete path of the folder after the move\n * @param   {bool}     [overwrite=false]        Whether to overwrite the target.\n *\n * @return { bool }                            The result of the move.\n * @todo implement with Robocopy\n */\n$.oFolder.prototype.move = function( destFolderPath, overwrite ){\n    if (typeof overwrite === 'undefined') var overwrite = false;\n\n    if (destFolderPath instanceof this.$.oFolder) destFolderPath = destFolderPath.path;\n\n    var dir = new Dir;\n    dir.path = destFolderPath;\n\n    if (dir.exists && !overwrite)\n        throw new Error(\"destination file \"+dir.path+\" exists and will not be overwritten. Can't move folder.\");\n\n    var path = fileMapper.toNativePath(this.path);\n    var destPath = fileMapper.toNativePath(dir.path+\"/\");\n\n    var destDir = new Dir;\n    try {\n        destDir.rename( path, destPath );\n        this._path = destPath;\n\n        return true;\n    }catch (err){\n        throw new Error (\"Couldn't move folder \"+this.path+\" to new address \"+destPath + \": \" + err);\n    }\n}\n\n\n/**\n * Move this folder to a different parent folder, while retaining its content and base name.\n * @param   {string}   destFolderPath           The path of the destination to copy the folder into.\n * @param   {bool}     [overwrite=false]        Whether to overwrite the target. Default is false.\n *\n * @return: { bool }                            The result of the move.\n */\n$.oFolder.prototype.moveToFolder = function( destFolderPath, overwrite ){\n  destFolderPath = (destFolderPath instanceof this.$.oFolder)?destFolderPath:new this.$.oFolder(destFolderPath)\n\n  var folder = destFolderPath.path;\n  var name = this.name;\n\n  this.move(folder+\"/\"+name, overwrite);\n}\n\n\n/**\n * Renames the folder\n * @param {string} newName\n */\n$.oFolder.prototype.rename = function(newName){\n  var destFolderPath = this.folder.path+\"/\"+newName\n  if ((new this.$.oFolder(destFolderPath)).exists) throw new Error(\"Can't rename folder \"+this.path + \" to \"+newName+\", a folder already exists at this location\")\n\n  this.move(destFolderPath)\n}\n\n\n/**\n * Deletes the folder.\n * @param   {bool}    removeContents            Whether to check if the folder contains files before deleting.\n */\n$.oFolder.prototype.remove = function (removeContents){\n  if (typeof removeContents === 'undefined') var removeContents = false;\n\n  if (this.listFiles.length > 0 && this.listFolders.length > 0 && !removeContents) throw new Error(\"Can't remove folder \"+this.path+\", it is not empty.\")\n  var _folder = new Dir(this.path);\n  _folder.rmdirs();\n}\n\n\n/**\n * Get the sub folder or file by name.\n * @param   {string}   name                     The sub name of a folder or file within a directory.\n * @return: {$.oFolder/$.oFile}                 The resulting oFile or oFolder.\n */\n$.oFolder.prototype.get = function( destName ){\n  var new_path = this.path + \"/\" + destName;\n  var new_folder = new $.oFolder( new_path );\n  if( new_folder.exists ){\n    return new_folder;\n  }\n\n  var new_file = new $.oFile( new_path );\n  if( new_file.exists ){\n    return new_file;\n  }\n\n  return false;\n}\n\n\n /**\n * Used in converting the folder to a string value, provides the string-path.\n * @return  {string}   The folder path's as a string.\n */\n$.oFolder.prototype.toString = function(){\n    return this.path;\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//           $.oFile class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The $.oFile helper class -- providing utilities for file manipulation and access.\n * @constructor\n * @classdesc  $.oFile Base Class\n * @param      {string}              path                     The path to the file.\n *\n * @property    {string}             path                     The path to the file.\n */\n$.oFile = function(path){\n  this._type = \"file\";\n  this._path = fileMapper.toNativePath(path).split('\\\\').join('/');\n\n  // fix lowercase drive letter\n  var path_components = this._path.split(\"/\");\n  if (path_components[0] && about.isWindowsArch()){\n    // local path that starts with a drive letter\n    path_components[0] = path_components[0].toUpperCase()\n    this._path = path_components.join(\"/\");\n  }\n}\n\n\n/**\n * The name of the file with extension.\n * @name $.oFile#fullName\n * @type {string}\n */\nObject.defineProperty($.oFile.prototype, 'fullName', {\n    get: function(){\n        var _name = this.path.slice( this.path.lastIndexOf(\"/\")+1 );\n        return _name;\n    }\n});\n\n\n/**\n * The name of the file without extension.\n * @name $.oFile#name\n * @type {string}\n */\nObject.defineProperty($.oFile.prototype, 'name', {\n    get: function(){\n      var _fullName = this.fullName;\n      if (_fullName.indexOf(\".\") == -1) return _fullName;\n\n      var _name = _fullName.slice(0, _fullName.lastIndexOf(\".\"));\n      return _name;\n    },\n    set: function(newName){\n      this.rename(newName)\n    }\n});\n\n\n/**\n * The extension of the file.\n * @name $.oFile#extension\n * @type {string}\n */\nObject.defineProperty($.oFile.prototype, 'extension', {\n    get: function(){\n      var _fullName = this.fullName;\n      if (_fullName.indexOf(\".\") == -1) return \"\";\n\n      var _extension = _fullName.slice(_fullName.lastIndexOf(\".\")+1);\n      return _extension;\n    }\n});\n\n\n/**\n * The folder containing the file.\n * @name $.oFile#folder\n * @type {$.oFolder}\n */\nObject.defineProperty($.oFile.prototype, 'folder', {\n    get: function(){\n        var _folder = this.path.slice(0,this.path.lastIndexOf(\"/\"));\n        return new this.$.oFolder(_folder);\n    }\n});\n\n\n/**\n * Whether the file exists already.\n * @name $.oFile#exists\n * @type {bool}\n */\nObject.defineProperty($.oFile.prototype, 'exists', {\n    get: function(){\n        var _file = new File( this.path );\n        return _file.exists;\n    }\n})\n\n\n/**\n * The path of the file. Setting a path doesn't move the file, only changes where the file object is pointing.\n * @name $.oFile#path\n * @type {string}\n */\nObject.defineProperty( $.oFile.prototype, 'path', {\n  get: function(){\n    return this._path;\n  },\n\n  set: function( newPath ){\n    this._path = fileMapper.toNativePath( newPath ).split(\"\\\\\").join(\"/\");\n  }\n});\n\n\n/**\n * The path of the file encoded as a toonboom relative path.\n * @name $.oFile#toonboomPath\n * @readonly\n * @type {string}\n */\nObject.defineProperty( $.oFile.prototype, 'toonboomPath', {\n  get: function(){\n    var _path = this._path;\n    if (!this.$.scene.online) return _path;\n    if (_path.slice(0,2) != (\"//\")) return _path;\n\n    var _pathComponents = _path.replace(\"//\", \"\").split(\"/\");\n    var _drive = (_pathComponents[1]==\"usadata000\")?_pathComponents[1]:_pathComponents[1].toUpperCase();\n    var _path = _pathComponents.slice(2);\n\n    return [\"\",_drive].concat(_path).join(\"/\");\n  }\n});\n\n\n//Todo, Size, Date Created, Date Modified\n\n\n/**\n * Reads the content of the file.\n *\n * @return: { string }                      The contents of the file.\n */\n$.oFile.prototype.read = function() {\n  var file = new File(this.path);\n\n  try {\n    if (file.exists) {\n      file.open(FileAccess.ReadOnly);\n      var string = file.read();\n      file.close();\n      return string;\n    }\n  } catch (err) {\n    this.$.debug(err, this.DEBUG_LEVEL.ERROR)\n    return null\n  }\n}\n\n\n/**\n * Writes to the file.\n * @param   {string}   content               Content to write to the file.\n * @param   {bool}     [append=false]        Whether to append to the file.\n */\n$.oFile.prototype.write = function(content, append){\n    if (typeof append === 'undefined') var append = false\n\n    var file = new File(this.path);\n    try {\n        if (append){\n            file.open(FileAccess.Append);\n        }else{\n            file.open(FileAccess.WriteOnly);\n        }\n        file.write(content);\n        file.close();\n        return true\n    } catch (err) {return false;}\n}\n\n\n/**\n * Moves the file to the specified path.\n * @param   {string}   folder                  destination folder for the file.\n * @param   {bool}     [overwrite=false]       Whether to overwrite the file.\n *\n * @return: { bool }                           The result of the move.\n */\n$.oFile.prototype.move = function( newPath, overwrite ){\n  if (typeof overwrite === 'undefined') var overwrite = false;\n\n  if(newPath instanceof this.$.oFile) newPath = newPath.path;\n\n  var _file = new PermanentFile(this.path);\n  var _dest = new PermanentFile(newPath);\n  // this.$.alert(\"moving \"+_file.path()+\" to \"+_dest.path()+\" exists?\"+_dest.exists())\n\n  if (_dest.exists()){\n    if (!overwrite){\n      this.$.debug(\"destination file \"+newPath+\" exists and will not be overwritten. Can't move file.\", this.$.DEBUG_LEVEL.ERROR);\n      return false;\n    }else{\n      _dest.remove()\n    }\n  }\n\n  var success = _file.move(_dest);\n  // this.$.alert(success)\n  if (success) {\n    this.path = _dest.path()\n    return this;\n  }\n  return false;\n}\n\n\n /**\n * Moves the file to the folder.\n * @param   {string}   folder                  destination folder for the file.\n * @param   {bool}     [overwrite=false]       Whether to overwrite the file.\n *\n * @return: { bool }                           The result of the move.\n */\n$.oFile.prototype.moveToFolder = function( folder, overwrite ){\n  if (folder instanceof this.$.oFolder) folder = folder.path;\n  var _fileName = this.fullName;\n\n  return this.move(folder+\"/\"+_fileName, overwrite)\n}\n\n\n /**\n * Renames the file.\n * @param   {string}   newName                 the new name for the file, without the extension.\n * @param   {bool}     [overwrite=false]       Whether to replace a file of the same name if it exists in the folder.\n *\n * @return: { bool }                           The result of the renaming.\n */\n$.oFile.prototype.rename = function( newName, overwrite){\n  if (newName == this.name) return true;\n  if (this.extension != \"\") newName += \".\"+this.extension;\n  return this.move(this.folder.path+\"/\"+newName, overwrite);\n}\n\n\n\n/**\n * Copies the file to the folder.\n * @param   {string}   [folder]                Content to write to the file.\n * @param   {string}   [copyName]              Name of the copied file without the extension. If not specified, the copy will keep its name unless another file is present in which case it will be called \"_copy\"\n * @param   {bool}     [overwrite=false]       Whether to overwrite the file.\n *\n * @return: { bool }                           The result of the copy.\n */\n$.oFile.prototype.copy = function( destfolder, copyName, overwrite){\n    if (typeof overwrite === 'undefined') var overwrite = false;\n    if (typeof copyName === 'undefined') var copyName = this.name;\n    if (typeof destfolder === 'undefined') var destfolder = this.folder.path;\n\n    var _fileName = this.fullName;\n    if(destfolder instanceof this.$.oFolder) destfolder = destfolder.path;\n\n    // remove extension from name in case user added it to the param\n    copyName.replace (\".\"+this.extension, \"\");\n    if (this.name == copyName && destfolder == this.folder.path) copyName += \"_copy\";\n\n    var _fileName = copyName+((this.extension.length>0)?\".\"+this.extension:\"\");\n\n    var _file = new PermanentFile(this.path);\n    var _dest = new PermanentFile(destfolder+\"/\"+_fileName);\n\n    if (_dest.exists() && !overwrite){\n        throw new Error(\"Destination file \"+destfolder+\"/\"+_fileName+\" exists and will not be overwritten. Can't copy file.\", this.DEBUG_LEVEL.ERROR);\n    }\n\n    this.$.debug(\"copying \"+_file.path()+\" to \"+_dest.path(), this.$.DEBUG_LEVEL.LOG)\n\n    var success = _file.copy(_dest);\n    if (!success) throw new Error (\"Copy of file \"+_file.path()+\" to location \"+_dest.path()+\" has failed.\", this.$.DEBUG_LEVEL.ERROR)\n\n    return new this.$.oFile(_dest.path());\n}\n\n\n/**\n * Removes the file.\n * @return: { bool }                           The result of the removal.\n */\n$.oFile.prototype.remove = function(){\n    var _file = new PermanentFile(this.path)\n    if (_file.exists()) return _file.remove()\n}\n\n\n\n/**\n * Parses the file as a XML and returns an object containing the values.\n * @example\n * // parses the xml file as an object with imbricated hierarchy.\n * // each xml node is represented by a simple object with a \"children\" property containing the children nodes,\n * // and a objectName property representing the name of the node.\n * // If the node has attributes, those are set as properties on the object. All values are set as strings.\n *\n * // example: parsing the shortcuts file\n *\n * var shortcutsFile = (new $.oFile(specialFolders.userConfig+\"/shortcuts.xml\")).parseAsXml();\n *\n * // The returned object will always be a simple document object with a single \"children\" property containing the document nodes.\n *\n * var shortcuts = shortcuts.children[0].children     // children[0] is the \"shortcuts\" parent node, we want the nodes contained within\n *\n * for (var i in shortcuts){\n *   log (shortcuts[i].id)\n * }\n */\n$.oFile.prototype.parseAsXml = function(){\n  if (this.extension.toLowerCase() != \"xml\") return\n\n  // build an object model representation of the contents of the XML by parsing it character by character\n  var xml = this.read();\n  var xmlDocument = new this.$.oXml(xml);\n  return xmlDocument;\n}\n\n\n /**\n * Used in converting the file to a string value, provides the string-path.\n * @return  {string}   The file path's as a string.\n */\n$.oFile.prototype.toString = function(){\n    return this.path;\n}\n\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//           $.oXml class           //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oXml class.\n * @classdesc\n * The $.oXml class can be used to create an object from a xml string. It will contain a \"children\" property which is an array that holds all the children node from the main document.\n * @constructor\n * @param {string}     xmlString           the string to parse for xml content\n * @param {string}     objectName          \"xmlDocument\" for the top node, otherwise, the string description of the xml node (ex: <objectName> <property = \"value\"/> </objectName>)\n * @property {string}  objectName\n * @property {$.oXml[]}  children\n */\n$.oXml = function (xmlString, objectName){\n  if (typeof objectName === 'undefined') var objectName = \"xmlDocument\";\n  this.objectName = objectName;\n  this.children = [];\n\n  var string = xmlString+\"\";\n\n  // matches children xml nodes, multiline or single line, and provides one group for the objectName and one for the insides to parse again.\n  var objectRE = /<(\\w+)[ >?]([\\S\\s]+?\\/\\1|[^<]+?\\/)>/igm\n  var match;\n  while (match = objectRE.exec(xmlString)){\n    this.children.push(new this.$.oXml(match[2], match[1]));\n    // remove the match from the string to parse the rest as properties\n    string = string.replace(match[0], \"\");\n  }\n\n  // matches a line with name=\"property\"\n  var propertyRE = /(\\w+)=\"([^\\=\\<\\>]+?)\"/igm\n  var match;\n  while (match = propertyRE.exec(string)){\n    // set the property on the object\n    this[match[1]] = match[2];\n  }\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_frame.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oFrame class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oFrame.\n * @constructor\n * @classdesc  Frames describe the frames of a oColumn, and allow to access the value, ease settings, as well as frameNumber.\n * @param   {int}                    frameNumber             The frame to which this references.\n * @param   {oColumn}                oColumnObject           The column to which this frame references.\n * @param   {int}                    subColumns              The subcolumn index.\n *\n * @property {int}                     frameNumber           The frame to which this references.\n * @property {oColumn}                 column                The oColumnObject to which this frame references.\n * @property {oAttribute}              attributeObject       The oAttributeObject to which this frame references.\n * @property {int}                     subColumns            The subcolumn index.\n * @example\n * // to access the frames of a column, simply call oColumn.frames:\n * var myColumn = $.scn.columns[O]      // access the first column of the list of columns present in the scene\n *\n * var frames = myColumn.frames;\n *\n * // then you can iterate over them to check their properties:\n *\n * for (var i in frames){\n *   $.log(frames[i].isKeyframe);\n *   $.log(frames[i].continuity);\n * }\n *\n * // you can get and set the value of the frame\n *\n * frames[1].value = 5;   // frame array values and frameNumbers are matched, so this sets the value of frame 1\n */\n$.oFrame = function( frameNumber, oColumnObject, subColumns ){\n  this._type = \"frame\";\n\n  this.frameNumber = frameNumber;\n\n  if( oColumnObject instanceof $.oAttribute ){  //Direct access to an attribute, when not keyable. We still provide a frame access for consistency.  > MCNote ?????\n    this.column = false;\n    this.attributeObject = oColumnObject;\n  }else if( oColumnObject instanceof $.oColumn ){\n    this.column = oColumnObject;\n\n    if (this.column && typeof subColumns === 'undefined'){\n      var subColumns = this.column.subColumns;\n    }else{\n      var subColumns = { a : 1 };\n    }\n\n    this.attributeObject = this.column.attributeObject;\n  }\n\n  this.subColumns = subColumns;\n}\n\n\n// $.oFrame Object Properties\n/**\n * The value of the frame. Contextual to the attribute type.\n * @name $.oFrame#value\n * @type {object}\n * @todo Include setting values on column that don't have attributes linked?\n */\nObject.defineProperty($.oFrame.prototype, 'value', {\n  get : function(){\n    if (this.attributeObject){\n      this.$.debug(\"getting value of frame \"+this.frameNumber+\" through attribute object : \"+this.attributeObject.keyword, this.$.DEBUG_LEVEL.LOG);\n      return this.attributeObject.getValue(this.frameNumber);\n    }else{\n      this.$.debug(\"getting value of frame \"+this.frameNumber+\" through column object : \"+this.column.name, this.$.DEBUG_LEVEL.LOG);\n      return this.column.getValue(this.frameNumber);\n    }\n    /*\n      // this.$.log(\"Getting value of frame \"+this.frameNumber+\" of column \"+this.column.name)\n    if (this.attributeObject){\n      return this.attributeObject.getValue(this.frameNumber);\n    }else{\n      this.$.debug(\"getting unlinked column \"+this.name+\" value at frame \"+this.frameNumber, this.$.DEBUG_LEVEL.ERROR);\n      this.$.debug(\"warning : getting a value from a column without attribute destroys value fidelity\", this.$.DEBUG_LEVEL.ERROR);\n      if (this.\n      return column.getEntry (this.name, 1, this.frameNumber);\n    }*/\n  },\n\n  set : function(newValue){\n    if (this.attributeObject){\n      return this.attributeObject.setValue(newValue, this.frameNumber);\n    }else{\n      return this.column.setValue(newValue, this.frameNumber);\n    }\n\n    /*// this.$.log(\"Setting frame \"+this.frameNumber+\" of column \"+this.column.name+\" to value: \"+newValue)\n    if (this.attributeObject){\n      this.attributeObject.setValue( newValue, this.frameNumber );\n    }else{\n      this.$.debug(\"setting unlinked column \"+this.name+\" value to \"+newValue+\" at frame \"+this.frameNumber, this.$.DEBUG_LEVEL.ERROR);\n      this.$.debug(\"warning : setting a value on a column without attribute destroys value fidelity\", this.$.DEBUG_LEVEL.ERROR);\n\n      var _subColumns = this.subColumns;\n      for (var i in _subColumns){\n        column.setEntry (this.name, _subColumns[i], this.frameNumber, newValue);\n      }\n    }*/\n  }\n});\n\n\n/**\n * Whether the frame is a keyframe.\n * @name $.oFrame#isKeyframe\n * @type {bool}\n */\nObject.defineProperty($.oFrame.prototype, 'isKeyframe', {\n    get : function(){\n      if( !this.column ) return true;\n      if( this.frameNumber == 0 ) return false;  // frames array start at 0 but first index is not a real frame\n\n      var _column = this.column.uniqueName;\n      if (this.column.type == 'DRAWING' || this.column.type == 'TIMING'){\n        if( column.getTimesheetEntry){\n          return !column.getTimesheetEntry(_column, 1, this.frameNumber).heldFrame;\n        }else{\n          return false;   //No valid way to check for keys on a drawing without getTimesheetEntry\n        }\n      }else if (['BEZIER', '3DPATH', 'EASE', 'QUATERNION'].indexOf(this.column.type) != -1){\n        return column.isKeyFrame(_column, 1, this.frameNumber);\n      }\n      return false;\n    },\n\n    set : function(keyframe){\n      this.$.log(\"setting keyframe for frame \"+this.frameNumber);\n      var col = this.column;\n      if( !col ) return;\n\n      var _column = col.uniqueName;\n\n      if( col.type == \"DRAWING\" ){\n        if (keyframe){\n          column.addKeyDrawingExposureAt( _column, this.frameNumber );\n        }else{\n          column.removeKeyDrawingExposureAt( _column, this.frameNumber );\n        }\n      }else{\n        if (keyframe){\n          //Sanity check, in certain situations, the setKeyframe resets to 0 (specifically if there is no pre-existing key elsewhere.)\n          //This will check the value prior to the key, set the key, and enforce the value after.\n\n          //var val = 0.0;\n          // try{\n          var val = this.value;\n          // }catch(err){}\n          //this.$.log(\"setting keyframe for frame \"+this.frameNumber);\n          column.setKeyFrame( _column, this.frameNumber );\n\n          // try{\n          //var post_val = this.value;\n          //if (val != post_val) {\n            this.value = val;\n          //}\n          // }catch(err){}\n        }else{\n          column.clearKeyFrame( _column, this.frameNumber );\n        }\n      }\n    }\n});\n\n\n/**\n * Whether the frame is a keyframe.\n * @name $.oFrame#isKeyFrame\n * @deprecated For case consistency, keyframe will never have a capital F\n * @type {bool}\n */\nObject.defineProperty($.oFrame.prototype, 'isKeyFrame', {\n    get : function(){\n      return this.isKeyframe;\n    },\n\n    set : function(keyframe){\n      this.$.debug(\"oFrame.isKeyFrame is deprecated. Use oFrame.isKeyframe instead.\", this.$.DEBUG_LEVEL.ERROR);\n      this.isKeyframe = keyframe;\n    }\n});\n\n\n/**\n * Whether the frame is a keyframe.\n * @name $.oFrame#isKey\n * @type {bool}\n */\nObject.defineProperty($.oFrame.prototype, 'isKey', {\n    get : function(){\n      return this.isKeyframe;\n    },\n\n    set : function(keyFrame){\n      this.isKeyframe = keyFrame;\n    }\n});\n\n\n/**\n * The duration of the keyframe exposure of the frame.\n * @name $.oFrame#duration\n * @type {int}\n */\nObject.defineProperty($.oFrame.prototype, 'duration', {\n    get : function(){\n        var _startFrame = this.startFrame;\n        var _sceneLength = frame.numberOf()\n\n        if( !this.column ){\n          return _sceneLength;\n        }\n\n        // walk up the frames of the scene to the next keyFrame to determine duration\n        var _frames = this.column.frames\n        for (var i=this.frameNumber+1; i<_sceneLength; i++){\n            if (_frames[i].isKeyframe) return _frames[i].frameNumber - _startFrame;\n        }\n        return _sceneLength - _startFrame;\n    },\n\n    set : function( val ){\n      throw \"Not implemented.\";\n    }\n});\n\n\n/**\n * Identifies if the frame is blank/empty.\n * @name $.oFrame#isBlank\n * @type {int}\n */\nObject.defineProperty($.oFrame.prototype, 'isBlank', {\n    get : function(){\n      var col = this.column;\n      if( !col ){\n        return false;\n      }\n\n      if ( col.type != \"DRAWING\") return false;\n\n      if( !column.getTimesheetEntry ){\n        return (this.value == \"\");\n      }\n\n      return column.getTimesheetEntry( col.uniqueName, 1, this.frameNumber ).emptyCell;\n    },\n\n    set : function( val ){\n      throw \"Not implemented.\";\n    }\n});\n\n\n/**\n * Identifies the starting frame of the exposed drawing.\n * @name $.oFrame#startFrame\n * @type {int}\n * @readonly\n */\nObject.defineProperty($.oFrame.prototype, 'startFrame', {\n    get : function(){\n      if( !this.column ){\n        return 1;\n      }\n\n      if (this.isKeyframe) return this.frameNumber;\n      if (this.isBlank) return -1;\n\n      var _frames = this.column.frames;\n      for (var i=this.frameNumber-1; i>=1; i--){\n          if (_frames[i].isKeyframe) return _frames[i].frameNumber;\n      }\n      return -1;\n    }\n});\n\n\n/**\n * Returns the drawing types used in the drawing column. K = key drawings, I = inbetween, B = breakdown\n * @name $.oFrame#marker\n * @type {string}\n */\nObject.defineProperty($.oFrame.prototype, 'marker', {\n    get : function(){\n        if( !this.column ){\n          return \"\";\n        }\n\n        var _column = this.column;\n        if (_column.type != \"DRAWING\") return \"\";\n        return column.getDrawingType(_column.uniqueName, this.frameNumber);\n    },\n\n    set: function( marker ){\n        if( !this.column ){\n          return;\n        }\n\n        var _column = this.column;\n        if (_column.type != \"DRAWING\") throw \"can't set 'marker' property on columns that are not 'DRAWING' type\"\n        column.setDrawingType( _column.uniqueName, this.frameNumber, marker );\n    }\n});\n\n\n/**\n * Find the index of this frame in the corresponding columns keyframes. -1 if unavailable.\n * @name $.oFrame#keyframeIndex\n * @type {int}\n */\nObject.defineProperty($.oFrame.prototype, 'keyframeIndex', {\n    get : function(){\n        var _kf = this.column.getKeyframes().map(function(x){return x.frameNumber});\n        var _kfIndex = _kf.indexOf(this.frameNumber);\n        return _kfIndex;\n    }\n});\n\n\n/**\n * Find the the nearest keyframe to this, on the left. Returns itself if it is a key.\n * @name $.oFrame#keyframeLeft\n * @type {oFrame}\n */\nObject.defineProperty($.oFrame.prototype, 'keyframeLeft', {\n    get : function(){\n      return (new this.$.oFrame(this.startFrame, this.column));\n    }\n});\n\n\n/**\n * Find the the nearest keyframe to this, on the right.\n * @name $.oFrame#keyframeRight\n * @type {oFrame}\n */\nObject.defineProperty($.oFrame.prototype, 'keyframeRight', {\n    get : function(){\n      return (new this.$.oFrame(this.startFrame+this.duration, this.column));\n    }\n});\n\n\n/**\n * Access the velocity value of a keyframe from a 3DPATH column.\n * @name $.oFrame#velocity\n * @type {oFrame}\n */\nObject.defineProperty($.oFrame.prototype, 'velocity', {\n  get : function(){\n    if (!this.column) return null;\n    if (this.column.type != \"3DPATH\") return null;\n    var _columnName = this.column.uniqueName;\n    if (!this.isKeyframe) return column.getEntry(_columnName, 4, this.frameNumber);\n\n    var index = this.keyframeIndex;\n\n    var _y = func.pointY(_columnName, index);\n    return _y;\n  },\n\n  set : function(newVelocity){\n    var _curVelocity = this.velocity;\n    throw new Error(\"setting oFrame.velocity is not yet implemented\")\n  }\n});\n\n\n/**\n * Gets a general ease object for the frame, which can be used to set frames to the same ease values. ease Objects contain the following properties:\n * x : frame number\n * y : position of the value of the column or velocity for 3dpath\n * easeIn : a $.oPoint object representing the left handle for bezier columns, or a {point, ease} object for ease columns.\n * easeOut : a $.oPoint object representing the left handle for bezier columns, or a {point, ease} object for ease columns.\n * continuity : the type of bezier used by the point.\n * constant : whether the frame is interpolated or a held value.\n * @name $.oFrame#ease\n * @type {oPoint/object}\n */\nObject.defineProperty($.oFrame.prototype, 'ease', {\n  get : function(){\n    var _column = this.column;\n    if (!_column) return null;\n    if ( !this.isKeyFrame ) return null;\n    if ( this.isBlank ) return null;\n\n    var _columnName = _column.uniqueName;\n    var _index = this.keyframeIndex;\n\n    var ease = {\n      x : func.pointX(_columnName, _index),\n      y : func.pointY(_columnName, _index),\n      constant : func.pointConstSeg(_columnName, _index),\n      continuity : func.pointContinuity(_columnName, _index)\n    }\n\n    if( _column.easeType == \"BEZIER\" ){\n      ease.easeIn = new this.$.oPoint(func.pointHandleLeftX(_columnName, _index), func.pointHandleLeftY(_columnName, _index),0);\n      ease.easeOut = new this.$.oPoint(func.pointHandleRightX(_columnName, _index), func.pointHandleRightY(_columnName, _index),0);\n    }\n\n    if( _column.easeType == \"EASE\" ){\n      ease.easeIn = {point:func.pointEaseIn(_columnName, _index), angle:func.angleEaseIn(_columnName, _index)};\n      ease.easeOut = {point:func.pointEaseOut(_columnName, _index), angle:func.angleEaseOut(_columnName, _index)};\n    }\n\n    return ease;\n  },\n\n  set : function(newEase){\n    if ( !this.isKeyFrame ) throw new Error(\"can't set ease on a non keyframe\");;\n    if (this.isBlank) throw new Error(\"can't set ease on an empty frame\");\n\n    var _column = this.column;\n    if (!_column) throw new Error (\"Can't set ease on a frame without a column\");\n    var _columnName = _column.uniqueName;\n\n    if( _column.easeType == \"BEZIER\" ){\n      if (!newEase.hasOwnProperty(\"x\")) throw new Error(\"Incorrect ease type for a BEZIER column\");\n      func.setBezierPoint (_columnName, newEase.x, newEase.y, newEase.easeIn.x, newEase.easeIn.y, newEase.easeOut.x, newEase.easeOut.y, newEase.constant, newEase.continuity)\n    }\n\n    if (_column.easeType == \"EASE\" ){\n      if (!newEase.hasOwnProperty(\"point\")) throw new Error(\"Incorrect ease type for a EASE column\");\n      func.setEasePoint(_columnName, newEase.x, newEase.y, newEase.easeIn.point, newEase.easeIn.angle, newEase.easeOut.point, newEase.easeOut.angle, newEase.constant, newEase.continuity)\n    }\n  }\n});\n\n\n/**\n * Gets the ease parameter of the segment, easing into this frame.\n * @name $.oFrame#easeIn\n * @type {oPoint/object}\n */\nObject.defineProperty($.oFrame.prototype, 'easeIn', {\n  get : function(){\n    return this.ease.easeIn;\n  },\n\n  set : function(newEaseIn){\n    var _ease = this.ease;\n    _ease.easeIn = newEaseIn;\n\n    this.ease = ease;\n  }\n});\n\n\n/**\n * Gets the ease parameter of the segment, easing out of this frame.\n * @name $.oFrame#easeOut\n * @type {oPoint/object}\n */\nObject.defineProperty($.oFrame.prototype, 'easeOut', {\n  get : function(){\n    return this.ease.easeOut;\n  },\n\n  set : function(newEaseOut){\n    var _ease = this.ease;\n    _ease.easeOut = newEaseOut;\n\n    this.ease = _ease;\n  }\n});\n\n\n/**\n * Determines the frame's continuity setting. Can take the values \"CORNER\", (two independent bezier handles on each side), \"SMOOTH\"(handles are aligned) or \"STRAIGHT\" (no handles and in straight lines).\n * @name $.oFrame#continuity\n * @type {string}\n */\nObject.defineProperty($.oFrame.prototype, 'continuity', {\n  get : function(){\n    var _frame = this.keyframeLeft;    //Works on the left keyframe, in the event that this is not a keyframe itself.\n\n    return _frame.ease.continuity;\n  },\n\n  set : function( newContinuity ){\n    var _frame = this.keyframeLeft;    //Works on the left keyframe, in the event that this is not a keyframe itself.\n    var _ease = _frame.ease;\n    _ease.continuity = newContinuity;\n\n    _frame.ease = ease;\n  }\n});\n\n\n/**\n * Whether the frame is tweened or constant. Uses nearest keyframe if this frame isnt.\n * @name $.oFrame#constant\n * @type {string}\n */\nObject.defineProperty($.oFrame.prototype, 'constant', {\n  get : function(){\n    var _frame = this.keyframeLeft;    //Works on the left keyframe, in the event that this is not a keyframe itself.\n\n    return _frame.ease.constant;\n  },\n\n  set : function( newConstant ){\n    if( this.column ){\n      var _frame = this.keyframeLeft;    //Works on the left keyframe, in the event that this is not a keyframe itself.\n\n      var _ease = _frame.ease;\n      _ease.constant = newConstant;\n      _frame.ease = _ease;\n    }\n  }\n});\n\n\n/**\n * Identifies or sets whether there is a tween. Inverse of constant.\n * @name $.oFrame#tween\n * @type {string}\n */\nObject.defineProperty($.oFrame.prototype, 'tween', {\n  get : function(){\n    return !this.constant;\n  },\n  set : function( new_tween ){\n    this.constant = !new_tween;\n  }\n});\n\n\n\n// oFrame Class Methods\n\n\n/**\n * Extends the frames value to the specified duration, replaces in the event that replace is specified.\n * @param   {int}        duration              The duration to extend it to; if no duration specified, extends to the next available keyframe.\n * @param   {bool}       replace               Setting this to false will insert frames as opposed to overwrite existing ones.\n */\n$.oFrame.prototype.extend = function( duration, replace ){\n    if (typeof replace === 'undefined') var replace = true;\n    // setting this to false will insert frames as opposed to overwrite existing ones\n\n    if( !this.column ){\n      return;\n    }\n\n    var _frames = this.column.frames;\n\n    if (typeof duration === 'undefined'){\n        // extend to next non blank keyframe if not set\n        var duration = 0;\n        var curFrameEnd = this.startFrame + this.duration;\n        var sceneLength = this.$.scene.length;\n\n        // find next non blank keyframe\n        while ((curFrameEnd + duration) <= sceneLength && _frames[curFrameEnd + duration].isBlank){\n            duration ++;\n        }\n    }\n\n    var _value = this.value;\n    var startExtending = this.startFrame+this.duration;\n\n    for (var i = 0; i<duration; i++){\n        if (!replace){\n            // TODO : push all other frames back\n        }\n        _frames[startExtending+i].value = _value;\n    }\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_list.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library v0.01\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney...\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the MIT license.\n//   https://opensource.org/licenses/mit\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n/** \n * Constructor to generate an $.oList type object. \n * @constructor\n * @classdesc \n * The base class for the $.oList.<br>\n * Provides a list of values similar to an array, but with simpler filtering and sorting functions provided.<br>\n * It can have any starting index and so can implement lists with a first index of 1 like the $.oColumn.frames returned value.   \n * @param   {object[]}                 initArray             An array to initialize the list.\n * @param   {int}                      [startIndex=0]        The first index exposed in the list.\n * @param   {int}                      [length=0]            The length of the list -- the max between this value and the initial array's length is used.\n * @param   {function}                 [getFunction=null]    The function used to initialize the list when accessing an uninitiated element in the list.<br>In form <i>function( listItem, index ){ return value; }</i>\n * @param   {function}                 [setFunction=null]    The function run when setting an entry in the list.<br>In form <i>function( listItem, index, value ){ return resolvedValue; }</i> -- must return a resolved value. \n * @param   {function}                 [sizeFunction=null]   The function run when resizing the list.<br>In form <i>function( listItem, length ){ }</i>\n */\n$.oList = function( initArray, startIndex, length, getFunction, setFunction, sizeFunction ){\n  if(typeof initArray == 'undefined') var initArray = [];\n  if(typeof startIndex == 'undefined') var startIndex = 0;\n  if(typeof getFunction == 'undefined') var getFunction = false;\n  if(typeof setFunction == 'undefined') var setFunction = false;\n  if(typeof sizeFunction == 'undefined') var sizeFunction = false; \n  if(typeof length == 'undefined') var length = 0;\n  \n  //Extend the cache if the content has been provided initially.\n  //Must be not enumerable. . .\n  // this._initArray      = initArray;\n  // this._cache          = [];\n  \n  // this._getFunction    = getFunction;\n  // this._setFunction    = setFunction;  \n  // this._sizeFunction   = sizeFunction;\n  \n  // this.startIndex    = startIndex;\n  // this._length       = Math.max( initArray.length, startIndex+length );\n  // this.currentIndex  = startIndex;\n  \n  Object.defineProperty( this, '_initArray', {\n    enumerable : false, writable : true,\n    value: initArray\n  });\n\n  Object.defineProperty( this, '_cache', {\n    enumerable : false, writable : true,\n    value: []\n  });\n\n  Object.defineProperty( this, '_getFunction', {\n    enumerable : false, writable : true, configurable: false,\n    value: getFunction\n  });\n\n  Object.defineProperty( this, '_setFunction', {\n    enumerable : false, writable : true, configurable: false,\n    value: setFunction\n  });\n\n  Object.defineProperty( this, '_sizeFunction', {\n    enumerable : false, writable : true, configurable: false,\n    value: sizeFunction\n  });\n\n  Object.defineProperty( this, 'currentIndex', {\n    enumerable : false, writable : true, configurable: false,\n    value: startIndex\n  });\n\n  Object.defineProperty( this, '_startIndex', {\n    enumerable : false, writable : true, configurable: false,\n    value: startIndex\n  });\n\n  Object.defineProperty( this, '_length', {\n    enumerable : false, writable : true, configurable: false,\n    value: Math.max( initArray.length, startIndex+length )\n  });\n  \n  this.createGettersSetters();\n}\n\n\nObject.defineProperty( $.oList.prototype, '_type', {\n  enumerable : false, writable : false, configurable: false,\n  value: 'dynList'\n});\n\n\n/**\n * The next item in the list, undefined if reaching the end of the list.\n * @name $.oList#createGettersSetters\n * @private\n */\nObject.defineProperty($.oList.prototype, 'createGettersSetters', {\n  enumerable : false,\n  value: function(){\n    { \n      //Dynamic getter/setters.\n      var func_get  =  function( listItem, index ){\n                        if( index >= listItem._cache._length ) return null;\n                        if( listItem._cache[index].cacheAvailable ){                  \n                         return listItem._cache[index].value;                     \n                        }                    \n                        if( listItem._getFunction ){                     \n                          listItem._cache[index].cacheAvailable = true;             \n                          listItem._cache[index].value          = listItem._getFunction( listItem, index );          \n                          return listItem._cache[ index ].value;                    \n                        }\n                        return null;\n                      };\n      \n      //Either set the cache function directly, or run the setFunction to get a value and set it.\n      var func_set  =  function( listItem, index, value ){\n                        if( index >= listItem._cache._length ){\n                          if( listItem._sizeFunction ){\n                            listItem.length = index+1;\n                          }else{\n                            throw new ReferenceError( 'Index of out of range: '+index+ ' out of ' + listItem._cache._length )\n                          }\n                        }\n                          \n                        if( listItem._setFunction ){\n                          listItem._cache[index].cacheAvailable = true;\n                          try{\n                            listItem._cache[index].value          = listItem._setFunction( listItem, index, value );\n                          }catch(err){}\n                        }else{\n                          listItem._cache[index].cacheAvailable = true;\n                          listItem._cache[index].value          = value;\n                        }\n                       };\n    \n      var setup_length           = Math.max( this._length, this._cache.length ); \n      if( this._cache.length < setup_length ){\n        this._cache                = this._cache.concat( new Array( setup_length-this._cache.length ) );\n      }\n      \n      for( var n=0;n<this._length;n++ ){\n        if( !this._cache[n] ){\n          var cacheAvail = n<this._initArray.length;\n          this._cache[n] = { \n                        \"cacheAvailable\": cacheAvail,\n                        \"enumerable\"    : false,\n                        \"value\"         : cacheAvail ? this._initArray[n] : null,\n                      };\n        }\n        \n        var currentEnumerable = n>=this.startIndex && n< this.length;\n        \n        if( currentEnumerable && !this._cache[n].enumerable ){\n          Object.defineProperty( this, n, {\n            enumerable : true,\n            configurable: true,\n            set : eval( 'val = function(val){ return func_set( this, '+n+', val ); }' ),\n            get : eval( 'val = function(){ return func_get( this, '+n+'); }' )\n          });\n          this._cache[n].enumerable = true;\n        }else if( !currentEnumerable && this._cache[n].enumerable ){\n          Object.defineProperty( this, n, {\n            enumerable : false,\n            configurable: true,\n            value : null\n          });\n          this._cache[n].enumerable = false;\n        }\n      }\n    }\n  }\n});\n\n\n/**\n * The startIndex of the list.\n * @name $.oList#startIndex\n * @type {int}\n */\nObject.defineProperty( $.oList.prototype, 'startIndex', {\n  enumerable : false,\n  get: function(){\n    return this._startIndex;\n  },\n  \n  set: function( val ){\n    this._startIndex = val;\n    this.currentIndex = Math.max( this.currentIndex, val );\n    \n    this.createGettersSetters();\n  }\n});\n\n\n/**\n * The length of the list.\n * @name $.oList#length\n * @function\n * @return {int}   The length of the list, considering the startIndex.\n */\nObject.defineProperty($.oList.prototype, 'length', {\n  enumerable : false,\n  get: function(){\n    return this._length;\n  },\n  \n  set: function( val ){\n    //Reset the size as needed.\n    \n    var new_val = val+this.startIndex;\n    if( new_val != this._length ){\n      this._length = new_val;\n      this.createGettersSetters();\n    }\n     \n    this._sizeFunction( this, this._length );\n  }\n});\n\n\n/**\n * The first item in the list, resets the iterator to the first entry.\n * @name $.oList#first\n * @function\n * @return {object}   The first item in the list.\n */\nObject.defineProperty($.oList.prototype, 'first', {\n  enumerable : false,\n  value: function(){\n    this.currentIndex = this.startIndex;\n    return this[ this.startIndex ];\n  }\n});\n\n/**\n * The next item in the list, undefined if reaching the end of the list.\n * @name $.oList#next\n * @function\n * @return {object}     Grabs the next item using the property $.oList.currentIndex, and increase the iterator\n * @example\n * var myList = new $.oList([1,2,3], 1)\n *\n * var item = myList.first();  // 1\n *\n * while( item != undefined ){\n *   $.log(item)               // traces the whole array one item at a time : 1,2,3   \n *   item = myList.next();   \n * }\n */\nObject.defineProperty($.oList.prototype, 'next', {\n  enumerable : false,\n  value: function(){\n    this.currentIndex++;\n    \n    if( this.currentIndex >= this.length ){\n      return;\n    }\n    if( !this.hasOwnProperty( this.currentIndex) ) return;  // we return undefined so we can check correctly in the case of list of boolean values\n\n    return this[ this.currentIndex ];\n  }\n});\n\n\n/**\n * The index of the last valid element of the list\n * @name $.oList#lastIndex\n * @type {int}\n */\nObject.defineProperty($.oList.prototype, 'lastIndex', {\n  enumerable : false,\n  get: function(){\n    return this.length - 1;\n  }\n});\n\n\n/**\n * Similar to Array.push. Adds the value given as parameter to the end of the oList\n * @name $.oList#push\n * @function\n * @param   {various}     newElement                    The value to add at the end of the oList\n *\n * @return  {int}   Returns the new length of the oList.\n */\nObject.defineProperty($.oList.prototype, 'push', {\n  enumerable : false,\n  value : function( newElement ){\n    var origLength = this.length;\n    this.length    = origLength+1;\n    \n    this[ origLength ] = newElement;\n    return origLength+1;\n  }\n});\n\n/**\n * Similar to Array.pop. Removes the last value from the array and returns it. It is then removed from the array.\n * @name $.oList#pop\n * @function\n * @return  {int}   The item popped from the back of the array.\n */\nObject.defineProperty($.oList.prototype, 'pop', {\n  enumerable : false,\n  value : function( ){\n\n    var origLength = this.length;\n    if( !this.hasOwnProperty( origLength-1 ) ){\n      return;\n    }\n    \n    var cache = this._cache.pop();\n  \n    this.length    = origLength-1;\n    \n    return cache.value;\n  }\n});\n\n\n/**\n * Returns an oList object containing only the elements that passed the provided filter function.\n * @name $.oList#filterByFunction\n * @function\n * @param   {function}     func                    A function that is used to filter, returns true if it is to be kept in the list.\n *\n * @return  {$.oList}   The list represented as an array, filtered given the function.\n */\nObject.defineProperty($.oList.prototype, 'filterByFunction', {\n  enumerable : false,\n  value : function( func ){\n    var _results = [];\n    for (var i in this){\n      if ( func(this[i]) ){\n        _results.push( this[i] );\n      }\n    }\n\n    return new this.$.oList( _results );\n  }\n});\n\n\n/**\n * Returns an oList object containing only the elements that have the same property value as provided.\n * @name $.oList#filterByProperty\n * @function\n * @param   {string}    property                    The property to find.\n * @param   {string}    search                      The value to search for in the property.\n *\n * @return  {$.oList}   The list represented as an array, filtered given its properties.\n * @example\n * var doc = $.s // grab the scene object\n * var nodeList = new $.oList(doc.nodes, 1) // get a list of all the nodes, with a first index of 1\n * \n * $.log(nodeList) // outputs the list of all the node paths\n * \n * var readNodes = nodeList.filterByProperty(\"type\", \"READ\") // get a new list of only the nodes of type 'READ'\n * \n * $.log(readNodes.extractProperty(\"name\"))  // prints the names of the result\n *\n */\nObject.defineProperty($.oList.prototype, 'filterByProperty', {\n  enumerable : false,\n  value : function(property, search){\n    var _results = []\n    var _lastIndex = this.lastIndex;\n    for (var i=this.startIndex; i < _lastIndex; i++){\n      // this.$.log(i+\" \"+(property in this[i])+\" \"+(this[i][property] == search)+_lastIndex)\n      if ((property in this[i]) && (this[i][property] == search)) _results.push(this[i])\n    }\n    // this.$.log(_results)\n    return new this.$.oList(_results)\n  }\n});\n\n\n/**\n * Returns an oList object containing only the values of the specified property.\n * @name $.oList#extractProperty\n * @function\n * @param   {string}     property                    The property to find.\n *\n * @return  {$.oList}   The newly created oList object containing the property values.\n */\nObject.defineProperty($.oList.prototype, 'extractProperty', {\n  enumerable : false,\n  value : function(property){\n    var _results = []\n    var _lastIndex = this.lastIndex;\n    for (var i=this.startIndex; i < _lastIndex; i++){\n      _results.push(this[i][property])\n    }\n    return new this.$.oList(_results)\n  }\n});\n\n\n/**\n * Returns an oList object sorted according to the values of the specified property.\n * @name $.oList#sortByProperty\n * @function\n * @param   {string}    property                    The property to find.\n * @param   {bool}      [ascending=true]            Whether the sort is ascending/descending.\n *\n * @return  {$.oList}   The sorted $oList.\n */\nObject.defineProperty($.oList.prototype, 'sortByProperty', {\n  enumerable : false,\n  value : function( property, ascending ){\n    if (typeof ascending === 'undefined') var ascending = true;\n\n    var _array = this.toArray();\n    if (ascending){\n      var results = _array.sort(function (a,b){return a[property] - b[property]});\n    }else{\n      var results = _array.sort(function (a,b){return b[property] - a[property]});\n    }\n\n    // Sort in place or return a copy?\n    return new this.$.oList( results, this.startIndex );\n  }\n});\n\n\n/**\n * Returns an oList object sorted according to the sorting function provided.\n * @name $.oList#sortByFunction\n * @function\n * @param   {function}   func                    A function that is used to sort, in form function (a,b){return a - b}. (A positive a-b value will put the element b before a)\n *\n * @return  {$.oList}   The sorted $oList.\n */\nObject.defineProperty($.oList.prototype, 'sortByFunction', {\n  enumerable : false,\n  value : function( func ){\n    var _array = this.toArray();\n    var results = _array.sort( func );\n\n    // Sort in place or return a copy?\n    return new this.$.oList( results, this.startIndex );\n  }\n});\n\n\n// Methods must be declared as unenumerable properties this way\n/**\n * Converts the oList to an array\n * @name $.oList#toArray\n * @function\n * @return  {object[]}   The list represented as an array.\n */\nObject.defineProperty($.oList.prototype, 'toArray', {\n  enumerable : false,\n  value : function(){\n    var _array = [];\n    for (var i=0; i<this.startIndex+this.length; i++){\n      if( i<this.startIndex ){\n        _array.push( null );\n      }else{\n        _array.push( this[i] );\n      }\n    }\n    return _array;\n  }\n});\n\n/**\n * outputs the list to a string for easy logging\n * @name $.oList#toString\n * @function \n * @type {string}\n */\nObject.defineProperty($.oList.prototype, 'toString', {\n  enumerable : false,\n  value: function(){\n    return this.toArray().join(\",\");\n  }\n});\n\n\n\n\n//Needs all filtering, limiting. mapping, pop,  concat, join, etc\n//Speed up by finessing the way it extends and tracks the enumerable properties."
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_math.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oPoint class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n\n/**\n * The $.oPoint helper class - representing a 3D point.\n * @constructor\n * @classdesc  $.oPoint Base Class\n * @param     {float}              x                              Horizontal coordinate\n * @param     {float}              y                              Vertical coordinate\n * @param     {float}             [z]                             Depth Coordinate\n *\n * @property     {float}           x                              Horizontal coordinate\n * @property     {float}           y                              Vertical coordinate\n * @property     {float}           z                              Depth Coordinate\n */\n$.oPoint = function(x, y, z){\n    if (typeof z === 'undefined') var z = 0;\n\n    this._type = \"point\";\n\n    this.x = x;\n    this.y = y;\n    this.z = z;\n}\n\n/**\n * @name $.oPoint#polarCoordinates\n * an object containing {angle (float), radius (float)} values that represents polar coordinates (angle in radians) for the point's x and y value (z not yet supported)\n */\nObject.defineProperty( $.oPoint.prototype, 'polarCoordinates', {\n  get: function(){\n    var _angle = Math.atan2(this.y, this.x)\n    var _radius = Math.sqrt(this.x*this.x+this.y*this.y)\n    return {angle: _angle, radius: _radius};\n  },\n\n  set: function(polarPoint){\n    var _angle = polarPoint.angle;\n    var _radius = polarPoint.radius;\n    var _x = Math.cos(_angle)*_radius;\n    var _y = Math.sin(_angle)*_radius;\n    this.x = _x;\n    this.y = _y;\n  }\n});\n\n\n/**\n * Translate the point by the provided values.\n * @param   {int}       x                  the x value to move the point by.\n * @param   {int}       y                  the y value to move the point by.\n * @param   {int}       z                  the z value to move the point by.\n *\n * @returns { $.oPoint }                   Returns self (for inline addition).\n */\n$.oPoint.prototype.translate = function( x, y, z){\n  if (typeof x === 'undefined') var x = 0;\n  if (typeof y === 'undefined') var y = 0;\n  if (typeof z === 'undefined') var z = 0;\n\n  this.x += x;\n  this.y += y;\n  this.z += z;\n\n  return this;\n}\n\n/**\n * Translate the point by the provided values.\n * @param   {int}       x                  the x value to move the point by.\n * @param   {int}       y                  the y value to move the point by.\n * @param   {int}       z                  the z value to move the point by.\n *\n * @return: { $.oPoint }                   Returns self (for inline addition).\n */\n $.oPoint.prototype.add = function( x, y, z ){\n  if (typeof x === 'undefined') var x = 0;\n  if (typeof y === 'undefined') var y = 0;\n  if (typeof z === 'undefined') var z = 0;\n\n  var x = this.x + x;\n  var y = this.y + y;\n  var z = this.z + z;\n\n  return new this.$.oPoint(x, y, z);\n}\n\n/**\n * The distance between two points.\n * @param {$.oPoint}     point            the other point to calculate the distance from.\n * @returns {float}\n */\n$.oPoint.prototype.distance = function ( point ){\n  var distanceX = point.x-this.x;\n  var distanceY = point.y-this.y;\n  var distanceZ = point.z-this.z;\n\n  return Math.sqrt(distanceX*distanceX + distanceY*distanceY + distanceZ*distanceZ)\n}\n\n/**\n * Adds the point to the coordinates of the current oPoint.\n * @param   {$.oPoint}       add_pt                The point to add to this point.\n * @returns { $.oPoint }                           Returns itself (for inline addition).\n */\n$.oPoint.prototype.pointAdd = function( add_pt ){\n  this.x += add_pt.x;\n  this.y += add_pt.y;\n  this.z += add_pt.z;\n\n  return this;\n}\n\n/**\n * Adds the point to the coordinates of the current oPoint and returns a new oPoint with the result.\n * @param {$.oPoint}   oPoint                The point to add to this point.\n * @returns {$.oPoint}\n */\n$.oPoint.prototype.addPoint = function( point ){\n  var x = this.x + point.x;\n  var y = this.y + point.y;\n  var z = this.z + point.z;\n\n  return new this.$.oPoint(x, y, z);\n}\n\n\n/**\n * Subtracts the point to the coordinates of the current oPoint.\n * @param   {$.oPoint}       sub_pt                The point to subtract to this point.\n * @returns { $.oPoint }                           Returns itself (for inline addition).\n */\n$.oPoint.prototype.pointSubtract = function( sub_pt ){\n  this.x -= sub_pt.x;\n  this.y -= sub_pt.y;\n  this.z -= sub_pt.z;\n\n  return this;\n}\n\n\n/**\n * Subtracts the point to the coordinates of the current oPoint and returns a new oPoint with the result.\n * @param {$.oPoint}   point                The point to subtract to this point.\n * @returns {$.oPoint} a new independent oPoint.\n */\n$.oPoint.prototype.subtractPoint = function( point ){\n  var x = this.x - point.x;\n  var y = this.y - point.y;\n  var z = this.z - point.z;\n\n  return new this.$.oPoint(x, y, z);\n}\n\n/**\n * Multiply all coordinates by this value.\n * @param   {float}       float_val                Multiply all coordinates by this value.\n *\n * @returns { $.oPoint }                           Returns itself (for inline addition).\n */\n$.oPoint.prototype.multiply = function( float_val ){\n  this.x *= float_val;\n  this.y *= float_val;\n  this.z *= float_val;\n\n  return this;\n}\n\n/**\n * Divides all coordinates by this value.\n * @param   {float}       float_val                Divide all coordinates by this value.\n *\n * @returns { $.oPoint }                           Returns itself (for inline addition).\n */\n$.oPoint.prototype.divide = function( float_val ){\n  this.x /= float_val;\n  this.y /= float_val;\n  this.z /= float_val;\n\n  return this;\n}\n\n/**\n * Find average of provided points.\n * @param   {$.oPoint[]}       point_array         The array of points to get the average.\n *\n * @returns { $.oPoint }                           Returns the $.oPoint average of provided points.\n */\n$.oPoint.prototype.pointAverage = function( point_array ){\n  var _avg = new this.$.oPoint( 0.0, 0.0, 0.0 );\n  for (var x=0; x<point_array.length; x++) {\n    _avg.pointAdd(point_array[x]);\n  }\n  _avg.divide(point_array.length);\n\n  return _avg;\n}\n\n\n/**\n * Converts a Drawing point coordinate into a scene coordinate, as used by pegs (since drawings are 2D, z is untouched)\n * @returns {$.oPoint}\n */\n$.oPoint.prototype.convertToSceneCoordinates = function () {\n  return new this.$.oPoint(this.x/this.$.scene.fieldVectorResolutionX, this.y/this.$.scene.fieldVectorResolutionY, this.z);\n}\n\n\n/**\n * Converts a scene coordinate point into a Drawing space coordinate, as used by Drawing tools and $.oShape (since drawings are 2D, z is untouched)\n * @returns {$.oPoint}\n */\n$.oPoint.prototype.convertToDrawingSpace = function () {\n  return new this.$.oPoint(this.x * this.$.scene.fieldVectorResolutionX, this.y * this.$.scene.fieldVectorResolutionY, this.z);\n}\n\n\n/**\n * Uses the scene settings to convert this as a worldspace point into an OpenGL point, used in underlying transformation operations in Harmony.\n * OpenGL units have a square aspect ratio and go from -1 to 1 vertically in the camera field.\n * @returns nothing\n */\n$.oPoint.prototype.convertToOpenGL = function(){\n\n  var qpt = scene.toOGL( new Point3d( this.x, this.y, this.z ) );\n\n  this.x = qpt.x;\n  this.y = qpt.y;\n  this.z = qpt.z;\n\n}\n\n\n/**\n * Uses the scene settings to convert this as an OpenGL point into a Harmony worldspace point.\n * @returns nothing\n */\n$.oPoint.prototype.convertToWorldspace = function(){\n\n  var qpt = scene.fromOGL( new Point3d( this.x, this.y, this.z ) );\n\n  this.x = qpt.x;\n  this.y = qpt.y;\n  this.z = qpt.z;\n\n}\n\n\n/**\n * Linearly Interpolate between this (0.0) and the provided point (1.0)\n * @param   {$.oPoint}       point                The target point at 100%\n * @param   {double}       perc                 0-1.0 value to linearly interp\n *\n * @return: { $.oPoint }                          The interpolated value.\n */\n$.oPoint.prototype.lerp = function( point, perc ){\n  var delta = new this.$.oPoint( point.x, point.y, point.z );\n\n  delta = delta.pointSubtract( this );\n  delta.multiply( perc );\n  delta.pointAdd( this );\n\n  return delta;\n}\n\n\n$.oPoint.prototype.toString = function(){\n  return this._type+\": {x:\"+this.x+\", y:\"+this.y+\", z:\"+this.z+\"}\";\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//            $.oBox class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n\n/**\n * The $.oBox helper class - representing a 2D box.\n * @constructor\n * @classdesc  $.oBox Base Class\n * @param      {float}       left                             left horizontal bound\n * @param      {float}       top                              top vertical bound\n * @param      {float}       right                            right horizontal bound\n * @param      {float}       bottom                           bottom vertical bound\n *\n * @property      {float}       left                             left horizontal bound\n * @property      {float}       top                              top vertical bound\n * @property      {float}       right                            right horizontal bound\n * @property      {float}       bottom                           bottom vertical bound\n */\n$.oBox = function( left, top, right, bottom ){\n  this._type = \"box\";\n\n  if (typeof top === 'undefined') var top = Infinity\n  if (typeof left === 'undefined') var left = Infinity\n  if (typeof right === 'undefined') var right = -Infinity\n  if (typeof bottom === 'undefined') var bottom = -Infinity\n\n  this.top = top;\n  this.left = left;\n  this.right = right;\n  this.bottom = bottom;\n}\n\n\n/**\n * The width of the box.\n * @name $.oBox#width\n * @type {float}\n */\nObject.defineProperty($.oBox.prototype, 'width', {\n  get : function(){\n    return this.right - this.left + 1; //Inclusive size.\n  }\n})\n\n\n/**\n * The height of the box.\n * @name $.oBox#height\n * @type {float}\n */\nObject.defineProperty($.oBox.prototype, 'height', {\n  get : function(){\n    return this.bottom - this.top;\n  }\n})\n\n\n/**\n * The center of the box.\n * @name $.oBox#center\n * @type {$.oPoint}\n */\nObject.defineProperty($.oBox.prototype, 'center', {\n  get : function(){\n    return new this.$.oPoint(this.left+this.width/2, this.top+this.height/2);\n  }\n})\n\n\n/**\n * Adds the input box to the bounds of the current $.oBox.\n * @param   {$.oBox}       box                The $.oBox to include.\n */\n$.oBox.prototype.include = function(box){\n  if (box.left < this.left) this.left = box.left;\n  if (box.top < this.top) this.top = box.top;\n  if (box.right > this.right) this.right = box.right;\n  if (box.bottom > this.bottom) this.bottom = box.bottom;\n}\n\n\n/**\n * Checks whether the box contains another $.oBox.\n * @param   {$.oBox}       box                The $.oBox to check for.\n * @param   {bool}         [partial=false]    whether to accept partially contained boxes.\n */\n$.oBox.prototype.contains = function(box, partial){\n  if (typeof partial === 'undefined') var partial = false;\n\n  var fitLeft = (box.left >= this.left);\n  var fitTop = (box.top >= this.top);\n  var fitRight =(box.right <= this.right);\n  var fitBottom = (box.bottom <= this.bottom);\n\n  if (partial){\n    return (fitLeft || fitRight) && (fitTop || fitBottom);\n  }else{\n    return fitLeft && fitRight && fitTop && fitBottom;\n  }\n\n}\n\n/**\n * Adds the bounds of the nodes to the current $.oBox.\n * @param   {oNode[]}       oNodeArray                An array of nodes to include in the box.\n */\n$.oBox.prototype.includeNodes = function(oNodeArray){\n  // convert to array if only one node is passed\n  if (!Array.isArray(oNodeArray)) oNodeArray = [oNodeArray];\n\n  for (var i in oNodeArray){\n     var _node = oNodeArray[i];\n     var _nodeBox = _node.bounds;\n     this.include(_nodeBox);\n  }\n}\n\n/**\n * @private\n */\n$.oBox.prototype.toString = function(){\n  return \"{top:\"+this.top+\", right:\"+this.right+\", bottom:\"+this.bottom+\", left:\"+this.left+\"}\"\n}\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oMatrix class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * The $.oMatrix constructor.\n * @constructor\n * @classdesc The $.oMatrix is a subclass of the native Matrix4x4 object from Harmony. It has the same methods and properties plus the ones listed here.\n * @param {Matrix4x4} matrixObject a matrix object to initialize the instance from\n */\n$.oMatrix = function(matrixObject){\n  Matrix4x4.constructor.call(this);\n  if (matrixObject){\n    log(matrixObject)\n    this.m00 = matrixObject.m00;\n    this.m01 = matrixObject.m01;\n    this.m02 = matrixObject.m02;\n    this.m03 = matrixObject.m03;\n    this.m10 = matrixObject.m10;\n    this.m11 = matrixObject.m11;\n    this.m12 = matrixObject.m12;\n    this.m13 = matrixObject.m13;\n    this.m20 = matrixObject.m20;\n    this.m21 = matrixObject.m21;\n    this.m22 = matrixObject.m22;\n    this.m23 = matrixObject.m23;\n    this.m30 = matrixObject.m30;\n    this.m31 = matrixObject.m31;\n    this.m32 = matrixObject.m32;\n    this.m33 = matrixObject.m33;\n  }\n}\n$.oMatrix.prototype = Object.create(Matrix4x4.prototype)\n\n\n/**\n * A 2D array that contains the values from the matrix, rows by rows.\n * @name $.oMatrix#values\n * @type {Array}\n */\nObject.defineProperty($.oMatrix.prototype, \"values\", {\n  get:function(){\n    return [\n      [this.m00, this.m01, this.m02, this.m03],\n      [this.m10, this.m11, this.m12, this.m13],\n      [this.m20, this.m21, this.m22, this.m23],\n      [this.m30, this.m31, this.m32, this.m33],\n    ]\n  }\n})\n\n\n/**\n * @private\n */\n$.oMatrix.prototype.toString = function(){\n  return \"< $.oMatrix object : \\n\"+this.values.join(\"\\n\")+\">\";\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oVector class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The $.oVector constructor.\n * @constructor\n * @classdesc The $.oVector is a replacement for the Vector3d objects of Harmony.\n * @param {float} x a x coordinate for this vector.\n * @param {float} y a y coordinate for this vector.\n * @param {float} [z=0] a z coordinate for this vector. If omitted, will be set to 0 and vector will be 2D.\n */\n$.oVector = function(x, y, z){\n  if (typeof z === \"undefined\" || isNaN(z)) var z = 0;\n\n  // since Vector3d doesn't have a prototype, we need to cheat to subclass it.\n  this._vector = new Vector3d(x, y, z);\n}\n\n\n/**\n * The X Coordinate of the vector.\n * @name $.oVector#x\n * @type {float}\n */\nObject.defineProperty($.oVector.prototype, \"x\", {\n  get: function(){\n    return this._vector.x;\n  },\n  set: function(newX){\n    this._vector.x = newX;\n  }\n})\n\n\n/**\n * The Y Coordinate of the vector.\n * @name $.oVector#y\n * @type {float}\n */\nObject.defineProperty($.oVector.prototype, \"y\", {\n  get: function(){\n    return this._vector.y;\n  },\n  set: function(newY){\n    this._vector.y = newY;\n  }\n})\n\n\n/**\n * The Z Coordinate of the vector.\n * @name $.oVector#z\n * @type {float}\n */\nObject.defineProperty($.oVector.prototype, \"z\", {\n  get: function(){\n    return this._vector.z;\n  },\n  set: function(newX){\n    this._vector.z = newX;\n  }\n})\n\n\n/**\n * The length of the vector.\n * @name $.oVector#length\n * @type {float}\n * @readonly\n */\nObject.defineProperty($.oVector.prototype, \"length\", {\n  get: function(){\n    return this._vector.length();\n  }\n})\n\n\n/**\n * @static\n * A function of the oVector class (not oVector objects) that gives a vector from two points.\n */\n$.oVector.fromPoints = function(pointA, pointB){\n  return new $.oVector(pointB.x-pointA.x, pointB.y-pointA.y, pointB.z-pointA.z);\n}\n\n\n/**\n * Adds another vector to this one.\n * @param {$.oVector} vector2\n * @returns {$.oVector} returns itself.\n */\n$.oVector.prototype.add = function (vector2){\n  this.x += vector2.x;\n  this.y += vector2.y;\n  this.z += vector2.z;\n\n  return this;\n}\n\n\n/**\n * Multiply this vector coordinates by a number (scalar multiplication)\n * @param {float} num\n * @returns {$.oVector} returns itself\n */\n$.oVector.prototype.multiply = function(num){\n  this.x = num*this.x;\n  this.y = num*this.y;\n  this.z = num*this.z;\n\n  return this;\n}\n\n\n/**\n * The dot product of the two vectors\n * @param {$.oVector} vector2 a vector object.\n * @returns {float} the resultant vector from the dot product of the two vectors.\n */\n$.oVector.prototype.dot = function(vector2){\n  var _dot = this._vector.dot(new Vector3d(vector2.x, vector2.y, vector2.z));\n  return _dot;\n}\n\n/**\n * The cross product of the two vectors\n * @param {$.oVector} vector2 a vector object.\n * @returns {$.oVector} the resultant vector from the dot product of the two vectors.\n */\n$.oVector.prototype.cross = function(vector2){\n  var _cross = this._vector.cross(new Vector3d(vector2.x, vector2.y, vector2.z));\n  return new this.$.oVector(_cross.x, _cross.y, _cross.z);\n}\n\n/**\n * The projected vectors resulting from the operation\n * @param {$.oVector} vector2 a vector object.\n * @returns {$.oVector} the resultant vector from the projection of the current vector.\n */\n$.oVector.prototype.project = function(vector2){\n  var _projection = this._vector.project(new Vector3d(vector2.x, vector2.y, vector2.z));\n  return new this.$.oVector(_projection.x, _projection.y, _projection.z);\n}\n\n/**\n * Normalize the vector.\n * @returns {$.oVector} returns itself after normalization.\n */\n$.oVector.prototype.normalize = function(){\n  this._vector.normalize();\n  return this;\n}\n\n\n/**\n * The angle of this vector in radians.\n * @name $.oVector#angle\n * @type {float}\n * @readonly\n */\nObject.defineProperty($.oVector.prototype, \"angle\", {\n  get: function(){\n    return Math.atan2(this.y, this.x);\n  }\n})\n\n\n/**\n * The angle of this vector in degrees.\n * @name $.oVector#degreesAngle\n * @type {float}\n * @readonly\n */\nObject.defineProperty($.oVector.prototype, \"degreesAngle\", {\n  get: function(){\n    return this.angle * (180 / Math.PI);\n  }\n})\n\n\n/**\n * @private\n */\n$.oVector.prototype.toString = function(){\n  return \"<$.oVector [\"+this.x+\", \"+this.y+\", \"+this.z+\"]>\";\n}\n\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_metadata.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library v0.01\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney...\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the MIT license.\n//   https://opensource.org/licenses/mit\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oMetadata class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oMetadata class.\n * @classdesc  Provides access to getting/setting metadata as an object interface.<br>Given a node as a source, will use provide the metadata associated to that node, \n * otherwise provides metadata for the scene.\n * @param   {$.oNode}                 source            A node as the source of the metadata-- otherwise provides the scene metadata.\n * @todo Need to extend this to allow node metadata.\n * @constructor\n * @example\n * var metadata = $.scene.getMetadata();\n * metadata.create( \"mySceneMetadataName\", {\"ref\":\"thisReferenceValue\"} );\n * metadata[\"mySceneMetadataName\"]; //Provides: {\"ref\":\"thisReferenceValue\"}\n */\n$.oMetadata = function( source ){\n  this._type             = \"metadata\";\n  if( !source ){ source = 'scene'; }\n  this.source = source;\n  \n  this._metadatas = {};\n  \n  this.refresh();\n}\n\n\n/**\n * Refreshes the preferences by re-reading the preference file and ingesting their values appropriately. They are then available as properties of this class.<br>\n * <b>Note, any new preferences will not be available as properties until Harmony saves the preference file at exit. In order to reference new preferences, use the get function.\n * @name $.oMetadata#refresh\n * @function\n */\n$.oMetadata.prototype.refresh = function(){\n  \n  //----------------------------\n  //GETTER/SETTERS\n  var set_val = function( meta, name, val ){\n    var metadata = meta._metadatas[ name ];\n    \n    var valtype = false;\n    var jsonify = false;\n    switch( typeof val ){\n      case 'string':\n        valtype = 'string';\n        break;\n      case 'number':\n        if( val%1.0==0.0 ){\n          valtype = 'int';\n        }else{\n          valtype = 'double';\n        }\n        break\n      case 'boolean':\n      case 'undefined':\n      case 'null':\n        valtype = 'bool';\n        break\n      case 'object':\n      default:\n        valtype = 'string';\n        jsonify = true;\n        break\n    }\n    \n    if(jsonify){\n      val = 'json('+JSON.stringify( val )+')';\n    }\n    \n    if( meta.source == \"scene\" ){\n      var type = false;\n      scene.setMetadata( {\n                           \"name\"       : name,\n                           \"type\"       : valtype,\n                           \"creator\"    : \"OpenHarmony\",\n                           \"version\"    : \"1.0\",\n                           \"value\"      : val\n                         }\n                       );\n    }else{\n      var metaAttr = this.source.attributes[\"meta\"];\n      if( metaAttr ){\n        metaAttr[ name ] = val;\n      }\n    }\n    \n    meta.refresh();\n  }\n  \n  var get_val = function( meta, name ){\n    return meta._metadatas[name].value;\n  }\n  \n  //Definition of properties.\n  var getterSetter_create = function( targ, id, type, value ){\n  \n    if( type == \"string\" ){\n      if( value.slice( 0, 5 ) == \"json(\" ){\n          var obj = value.slice( 5, value.length-1 );\n          value = JSON.parse( obj );\n      }\n    }\n    targ._metadatas[ id ] = { \"value\": value, \"type\":type };\n   \n    //Create a getter/setter for it!\n    Object.defineProperty( targ, id, {\n      enumerable : true,\n      configurable: true,\n      set : eval( 'val = function(val){ set_val( targ, \"'+id+'\", val ); }' ),\n      get : eval( 'val = function(){ return get_val( targ, \"'+id+'\"); }' )\n    });\n  }\n  \n  \n  //Clear this objects previous getter/setters to make room for new ones.\n  if( this._metadatas ){\n    for( n in this._metadatas ){ //Remove them if they've disappeared.\n        Object.defineProperty( this, n, {\n          enumerable : false,\n          configurable: true,\n          set : function(){},\n          get : function(){}\n        });\n    }\n  }\n  this._metadatas = {};\n\n  if( this.source == \"scene\" ){\n    var metadatas = scene.metadatas();\n    \n    for( var n=0;n<metadatas.length;n++ ){\n      var metadata = metadatas[n];\n      getterSetter_create( this, metadata.name.toLowerCase(), metadata.type, metadata.value );\n    }\n  }else{\n    //createDynamicAttr (String node, String type, String attrName, String displayName, bool linkable)\n    //var alist = node.getAttrList( this.source.name, 1, 'meta' );\n    var metaAttr = this.source.attributes[\"meta\"];\n    if( metaAttr ){\n      var subAttrs = metaAttr.subAttributes;\n      if( subAttrs.length>0 ){\n        for( var i=0;i<subAttrs.length;i++ ){\n          getterSetter_create( this, subAttrs[i].shortKeyword.toLowerCase(), subAttrs[i].type, subAttrs[i].getValue(1) );\n        }\n      }\n    }\n    \n  }\n}\n\n\n/**\n * Creates a new metadata based on name and value.<br>The metadata is created on the source to which this metadata object references.\n * @name $.oMetadata#create\n * @param   {string}                 name            The name of the new metadata to create.\n * @param   {object}                 val             The value of the new metadata created.\n */\n$.oMetadata.prototype.create = function( name, val ){\n  var name = name.toLowerCase();\n\n  if( this[ name ] ){\n    throw ReferenceError( \"Metadata already exists by name: \" + name );\n  }\n  \n  var valtype = false;\n  var jsonify = false;\n  switch( typeof val ){\n    case 'string':\n      valtype = 'string';\n      break;\n    case 'number':\n      if( val%1.0==0.0 ){\n        valtype = 'int';\n      }else{\n        valtype = 'double';\n      }\n      break\n    case 'boolean':\n    case 'undefined':\n    case 'null':\n      valtype = 'bool';\n      break\n    case 'object':\n    default:\n      valtype = 'string';\n      jsonify = true;\n      break\n  }\n  \n  if(jsonify){\n    val = 'json('+JSON.stringify( val )+')';\n  }\n  \n  if( this.source == \"scene\" ){\n    scene.setMetadata( {\n                         \"name\"       : name,\n                         \"type\"       : valtype,\n                         \"creator\"    : \"OpenHarmony\",\n                         \"version\"    : \"1.0\",\n                         \"value\"      : val\n                       }\n                     );\n  }else{\n    var attr = this.source.createAttribute( \"meta.\"+name, valtype, valtype, false );\n    if( attr ){ \n      attr.setValue( val, 1 ); \n    }\n  }\n  this.refresh();\n}\n\n/**\n * Removes a new metadata based on name and value.<br>The metadata is removed from the source to which this metadata object references.\n * @name $.oMetadata#remove\n * @param   {string}                 name            The name of the metadata to remove.\n */\n$.oMetadata.prototype.remove = function( name ){\n  var name = name.toLowerCase();\n  if( !this.hasOwnProperty( name ) ){ return true; }\n  \n  var res = false;\n  if( this.source == \"scene\" ){\n    if( !scene.removeMetadata ){\n      res = scene.removeMetadata( scene.metadata(name), this._metadatas[ name ].type );\n    }else{\n      throw ReferenceError( \"This is supposed to exist, but doesn't seem to be available.\" );\n    }\n  }else{\n    res = this.source.removeAttribute( \"meta.\"+name );\n  }\n  \n  this.refresh();\n  return res;\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_misc.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library v0.01\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney...\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the MIT license.\n//   https://opensource.org/licenses/mit\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n//TODO : view.currentToolManager integration.\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//           $.oUtils class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The $.oUtils helper class -- providing generic utilities. Doesn't need instantiation.\n * @classdesc  $.oUtils utility Class\n */\n$.oUtils = function(){\n    this._type = \"utils\";\n}\n\n/**\n * Finds the longest common substring between two strings.\n * @param   {string}   str1\n * @param   {string}   str2\n * @returns {string} the found string\n */\n$.oUtils.longestCommonSubstring = function( str1, str2 ){\n\tif (!str1 || !str2)\n\t\treturn {\n\t\t\tlength: 0,\n\t\t\tsequence: \"\",\n\t\t\toffset: 0\n\t\t};\n\n\tvar sequence = \"\",\n\t\tstr1Length = str1.length,\n\t\tstr2Length = str2.length,\n\t\tnum = new Array(str1Length),\n\t\tmaxlen = 0,\n\t\tlastSubsBegin = 0;\n\n\tfor (var i = 0; i < str1Length; i++) {\n\t\tvar subArray = new Array(str2Length);\n\t\tfor (var j = 0; j < str2Length; j++)\n\t\t\tsubArray[j] = 0;\n\t\tnum[i] = subArray;\n\t}\n\tvar subsBegin = null;\n\tfor (var i = 0; i < str1Length; i++){\n\t\tfor (var j = 0; j < str2Length; j++){\n\t\t\tif (str1[i] !== str2[j]){\n\t\t\t\tnum[i][j] = 0;\n\t\t\t}else{\n\t\t\t\tif ((i === 0) || (j === 0)){\n\t\t\t\t\tnum[i][j] = 1;\n\t\t\t\t}else{\n\t\t\t\t\tnum[i][j] = 1 + num[i - 1][j - 1];\n        }\n\t\t\t\tif (num[i][j] > maxlen){\n\t\t\t\t\tmaxlen = num[i][j];\n\t\t\t\t\tsubsBegin = i - num[i][j] + 1;\n\t\t\t\t\tif (lastSubsBegin === subsBegin){//if the current LCS is the same as the last time this block ran\n\t\t\t\t\t\tsequence += str1[i];\n\t\t\t\t\t}else{\n            //this block resets the string builder if a different LCS is found\n\t\t\t\t\t\tlastSubsBegin = subsBegin;\n\t\t\t\t\t\tsequence= \"\"; //clear it\n\t\t\t\t\t\tsequence += str1.substr(lastSubsBegin, (i + 1) - lastSubsBegin);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn {\n\t\tlength: maxlen,\n\t\tsequence: sequence,\n\t\toffset: subsBegin\n\t};\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_network.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library v0.01\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney...\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the MIT license.\n//   https://opensource.org/licenses/mit\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oNetwork methods        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * Network helper for HTTP methods.<br>Available under <b>$.network</b>\n * @constructor\n * @classdesc  Network Helper Class\n * @param   {dom}                  $         The connection back to the DOM.\n *\n */\n$.oNetwork = function( ){\n    //Expect a path for CURL.\n    var avail_paths = [ \n                        \"c:\\\\Windows\\\\System32\\\\curl.exe\"\n                     ];\n    if( !about.isWindowsArch() ){\n      avail_paths = [ \n                      \"/usr/bin/curl\",\n                      \"/usr/local/bin/curl\"\n                    ];\n    }\n    \n    var curl_path = false;\n    for( var n=0;n<avail_paths.length;n++ ){\n      if( ( new File(avail_paths[n]) ).exists ){\n        curl_path = avail_paths[n];\n        break;\n      }\n    }\n    \n    this.useCurl = true;\n    this.curlPath = curl_path;\n}\n\n\n/**\n *  Connects to HTTP and gets the text response from a web site/API.<br><b>Note, Harmony has issues with HTTPS, useCurl=true prevents this</b>\n * @param   {string}       address                    The address for the web query.\n * @param   {function}     callback_func              Providing a callback function prevents blocking, and will respond on this function. The callback function is in form func( results ){}\n * @param   {bool}         use_json                   In the event of a JSON api, this will return an object converted from the returned JSON.\n *  \n * @return: {string/object}       The resulting object/string from the query -- otherwise a bool as false when an error occurred..\n */\n$.oNetwork.prototype.webQuery = function ( address, callback_func, use_json ){\n  if (typeof callback_func === 'undefined') var callback_func = false;\n  if (typeof use_json === 'undefined') var use_json = false;\n  \n  if( this.useCurl && this.curlPath ){\n    try{\n      var cmdline = [ \"-L\", address ];\n      var p = new QProcess();\n      if( !callback_func ){\n        p.start( this.curlPath, cmdline );\n        p.waitForFinished( 10000 );\n        \n        try{\n          var readOut = ( new QTextStream( p.readAllStandardOutput() ) ).readAll();\n          if( use_json ){\n            readOut = JSON.parse( readOut );\n          }\n          return readOut;\n          \n        }catch(err){\n          this.$.debug( err + \" (\"+err.lineNumber+\")\", this.$.DEBUG_LEVEL[\"ERROR\"] );\n          return false;\n        }\n      }else{\n        p.start( this.curlPath, cmdline );\n        \n        var callback = function( status ){\n          var readOut = ( new QTextStream( p.readAllStandardOutput() ) ).readAll();\n          if( use_json ){\n            readOut = JSON.parse( readOut );\n          }\n        \n          callback_func( readOut );\n        }\n        p[\"finished(int)\"].connect( this, callback );\n        \n        return true;\n      }\n    }catch( err ){\n      this.$.debug( err + \" (\"+err.lineNumber+\")\", this.$.DEBUG_LEVEL[\"ERROR\"] );\n      return false;\n    }\n  }else{\n    \n    System.println( callback );\n    \n    var data            = new QByteArray( \"\" );\n    var qurl            = new QUrl( address );\n    var request         = new QNetworkRequest( qurl );\n    var header          = new QByteArray(\"text/xml;charset=ISO-8859-1\");\n    var accessManager   = new QNetworkAccessManager();\n    \n    request.setHeader( QNetworkRequest.ContentTypeHeader,  header );\n    request.setHeader( QNetworkRequest.ServerHeader, \"application/json\" );\n    request.setHeader( QNetworkRequest.ContentLengthHeader, data.size() );\n    request.setAttribute( QNetworkRequest.CacheLoadControlAttribute, QNetworkRequest.AlwaysNetwork );\n    request.setAttribute( QNetworkRequest.FollowRedirectsAttribute, true );\n    \n    if( callback_func ){\n      replyRecd = function( reply ){\n        try{ \n          var statusCode = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute );\n          var reasonCode = reply.attribute( QNetworkRequest.HttpReasonPhraseAttribute );\n          \n          if( !statusCode ){\n            callback_func( false );\n            return;\n          }\n          \n          if( statusCode == 301 ){\n            callback_func( false );\n            return;\n          }\n\n          var stream = new QTextStream( reply );\n          var result = stream.readAll(); \n            \n          if( use_json ){\n            try{\n              result = JSON.parse( result );\n            }catch(err){\n              this.$.debug( err + \" (\"+err.lineNumber+\")\", this.$.DEBUG_LEVEL[\"ERROR\"] );\n              callback_func( false );\n            }\n          }\n          \n          callback_func( result );\n        }catch(err){\n          this.$.debug( err + \" (\"+err.lineNumber+\")\", this.$.DEBUG_LEVEL[\"ERROR\"] );\n          callback_func( false );\n        }\n      }\n      \n      error = function( error ){\n        switch( \"\"+error ){\n          case \"1\":\n            // MessageBox.information( \"Connection Refused\" );\n            break;\n            \n          case \"2\":\n            // MessageBox.information( \"Remote Host Closed\" );\n            break;\n            \n          case \"3\":\n            // MessageBox.information( \"Host Not Found\" );\n            break;\n            \n          case \"4\":\n            // MessageBox.information( \"Timeout Error\" );\n            break;\n            \n          case \"5\":\n            // MessageBox.information( \"Operation Cancelled\" );\n            break;\n            \n          case \"6\":\n            // MessageBox.information( \"SSL Handshake Failed\" );\n            break;\n        }\n        \n        callback( false );\n      }\n    \n    \n      accessManager[\"finished(QNetworkReply*)\"].connect( this, replyRecd );\n      var send_reply = accessManager.get( request );\n      send_reply[\"error(QNetworkReply::NetworkError)\"].connect( this, error );\n      \n      return true;\n    }else{\n      System.println( \"STARTING\" );\n      var wait    = new QEventLoop();\n      var timeout = new QTimer();\n      \n          timeout[\"timeout\"].connect( this, wait[\"quit\"] );\n      accessManager[\"finished(QNetworkReply*)\"].connect( this, wait[\"quit\"] );\n      \n      var send_reply = accessManager.get( request );\n      timeout.start( 10000 );\n      wait.exec();\n      timeout.stop();\n      \n      try{ \n        var statusCode = send_reply.attribute( QNetworkRequest.HttpStatusCodeAttribute );\n        var reasonCode = send_reply.attribute( QNetworkRequest.HttpReasonPhraseAttribute );\n        \n        if( !statusCode ){\n          return false;\n        }\n        \n        if( statusCode == 301 ){\n          return false;\n        }\n\n        var stream = new QTextStream( send_reply );\n        var result = stream.readAll(); \n          \n        if( use_json ){\n          try{\n            result = JSON.parse( result );\n          }catch(err){\n            this.$.debug( err + \" (\"+err.lineNumber+\")\", this.$.DEBUG_LEVEL[\"ERROR\"] );\n          }\n        }\n        \n        return( result );\n      }catch(err){\n        System.println( err );\n        this.$.debug( err + \" (\"+err.lineNumber+\")\", this.$.DEBUG_LEVEL[\"ERROR\"] );\n      }\n      \n      return( false );\n    }\n  }\n}\n\n\n/**\n *  Downloads a file from the internet at the given address<br><b>Note, only implemented with useCurl=true.</b>\n * @param   {string}       address                    The address for the file to be downloaded.\n * @param   {function}     path                       The local file path to save the download.\n * @param   {bool}         replace                    Replace the file if it exists.\n *  \n * @return: {string/object}       The resulting object/string from the query -- otherwise a bool as false when an error occurred..\n */\n$.oNetwork.prototype.downloadSingle = function ( address, path, replace ){\n  if (typeof replace === 'undefined') var replace = false;\n  \n  try{\n    if( this.useCurl && this.curlPath ){            \n      var file = new this.$.oFile( path );\n      if( file.exists ){\n        if( replace ){\n          file.remove();\n        }else{\n          this.$.debug( \"File already exists- unable to replace: \" + path, this.$.DEBUG_LEVEL[\"ERROR\"] );\n          return false;\n        }\n      }\n      \n      var cmdline = [ \"-L\", \"-o\", path, address ];\n      \n      var p = new QProcess();\n      p.start( this.curlPath, cmdline );  \n      p.waitForFinished( 10000 );\n      \n      var file = new this.$.oFile( path );\n      return file.exists;\n      \n    }else{\n      this.$.debug( \"Downloads without curl are not implemented.\", this.$.DEBUG_LEVEL[\"ERROR\"] );\n      return false;\n    }\n  }catch( err ){\n    this.$.debug( err + \" (\"+err.lineNumber+\")\", this.$.DEBUG_LEVEL[\"ERROR\"] );\n    return false;\n  }\n}\n\n\n/**\n *  Threads multiple downloads at a time [10 concurrent].  Downloads a from the internet at the given addresses<br><b>Note, only implemented with useCurl=true.</b>\n * @param   {object[]}     instructions               The instructions for download, in format [ { \"path\": localPathOnDisk, \"url\":\"DownloadPath\" } ]\n * @param   {bool}         replace                    Replace the file if it exists.\n *  \n * @return: {bool[]}       The results of the download, for each file in the instruction bool[]\n */\n$.oNetwork.prototype.downloadMulti = function ( address_path, replace ){\n  if (typeof replace === 'undefined') var replace = false;\n  \n  var progress = new QProgressDialog();\n  progress.setLabelText( \"Downloading files...\" );\n  progress.show();\n  progress.setRange( 0, address_path.length );\n  \n  var complete_process = function( val ){ \n  }\n  \n  var dload_cnt = 0;\n  try{\n    if( this.useCurl && this.curlPath ){\n      var in_proc = [];\n      var skipped = [];\n      for( var x=0;x<address_path.length;x++ ){\n        var add_grp = address_path[x];\n        \n        skipped.push( false );\n        try{\n          var url  = add_grp.url;\n          var path = add_grp.path;\n          \n          while( in_proc.length >= 10 ){  //Allow 10 concurrent processes.\n            var procs = [];\n            for( var n=0;n<in_proc.length;n++ ){     //Cull the finished processes.\n              QCoreApplication.processEvents();\n              if( parseInt( \"\"+ in_proc[n].state() ) > 0 ){\n                procs.push( in_proc[n] );\n              }else{\n                dload_cnt++;\n                progress.setValue( dload_cnt );\n              }\n            } \n            in_proc = procs;\n          }\n          \n          var file = new this.$.oFile( path );\n          if( file.exists ){\n            if( replace ){\n              file.remove();\n            }else{\n              this.$.debug( \"File already exists- unable to replace: \" + path, this.$.DEBUG_LEVEL[\"ERROR\"] );\n              skipped[x] == true;\n              continue;\n            }\n          }\n          \n          var cmdline = [ \"-L\", \"-o\", path, url ];\n          var p = new QProcess();\n          p[\"finished(int)\"].connect( this, complete_process );\n          p.start( this.curlPath, cmdline );  \n          in_proc.push( p );\n          \n          progress.setLabelText( \"Downloading file: \"+path );\n          \n          QCoreApplication.processEvents();\n        }catch(err){\n          this.$.debug( err + \" : \" + err.lineNumber + \" : \" + err.fileName, this.$.DEBUG_LEVEL[\"ERROR\"] );\n        }\n      }\n      \n      while( in_proc.length > 0 ){  //Allow 5 concurrent processes.\n        var procs = [];\n        for( var n=0;n<in_proc.length;n++ ){     //Cull the finished processes.\n          QCoreApplication.processEvents();\n          if( parseInt( \"\"+ in_proc[n].state() ) > 0 ){\n            procs.push( in_proc[n] );\n          }else{\n            dload_cnt++;\n            progress.setValue( dload_cnt );\n          }\n          \n          progress.setLabelText( \"Downloading \"+in_proc.length+\" File(s)\" );\n        } \n        \n        in_proc = procs;\n      }\n\n      progress.accept();\n      \n      var file_results = [];\n      for( var x=0;x<address_path.length;x++ ){\n        file_results.push( false );\n        if( skipped[x] ){\n          continue;\n        }\n        \n        var add_grp = address_path[x];\n        var file = new this.$.oFile( add_grp.path );\n        if( file.exists ){\n          file_results[x] = true;\n        }\n      }\n      \n      return file_results;\n    }else{\n      this.$.debug( \"Downloads without curl are not implemented.\", this.$.DEBUG_LEVEL[\"ERROR\"] );\n      return false;\n    }\n  }catch( err ){\n    this.$.debug( err + \" (\"+err.lineNumber+\")\", this.$.DEBUG_LEVEL[\"ERROR\"] );\n    return false;\n  }\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_node.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\ninclude(specialFolders.resource+\"/scripts/TB_orderNetworkUp.js\"  );\ninclude(specialFolders.userScripts+\"/TB_orderNetworkUp.js\");       // for older versions of harmony\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//           $.oNode class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n//TODO: Smart pathing, network movement, better duplication handling\n//TODO: Metadata, settings, aspect, camera peg, view.\n//TODO: group's multi-in-ports, multi-out-port modules\n\n/**\n * Constructor for $.oNode class\n * @classdesc\n * The oNode class represents a node in the Harmony scene. <br>\n * It holds the value of its position in the node view, and functions to link to other nodes, as well as set the attributes of the node.<br><br>\n * It uses a cache system, so a node for a given path will only be created once. <br>\n * If the nodes change path through other means than the openHarmony functions during the execution of the script, use oNode.invalidateCache() to create new nodes again.<br><br>\n * This constructor should not be invoqued by users, who should use $.scene.getNodeByPath() or $.scene.root.getNodeByName() instead.\n * @constructor\n * @param   {string}         path                          Path to the node in the network.\n * @param   {$.oScene}         [oSceneObject]                  Access to the oScene object of the DOM.\n * @see NodeType\n * @example\n * // To grab a node object from the scene, it's possible to create a new node object by calling the constructor:\n * var myNode = new $.oNode(\"Top/Drawing\", $.scn)\n *\n * // However, most nodes will be grabbed directly from the scene object.\n * var doc = $.scn\n * var nodes = doc.nodes;                   // grabs the list of all the nodes in the scene\n *\n * // It's possible to grab a single node from the path in the scene\n * var myNode = doc.getNodeByPath(\"Top/Drawing\")\n * var myNode = doc.$node(\"Top/Drawing\")    // short synthax but same function\n *\n * // depending on the type of node, oNode objects returned by these functions can actually be an instance the subclasses\n * // oDrawingNode, oGroupNode, oPegNode...\n *\n * $.log(myNode instanceof $.oNode)           // true\n * $.log(myNode instanceof $.oDrawingNode)  // true\n *\n * // These other subclasses of nodes have other methods that are only shared by nodes of a certain type.\n *\n * // Not documented in this class, oNode objects have attributes which correspond to the values visible in the Layer Properties window.\n * // The attributes values can be accessed and set by using the dot notation on the oNode object:\n *\n * myNode.can_animate = false;\n * myNode.position.separate = true;\n * myNode.position.x = 10;\n *\n * // To access the oAttribute objects in the node, call the oNode.attributes object that contains them\n *\n * var attributes = myNode.attributes;\n */\n$.oNode = function( path, oSceneObject ){\n  var instance = this.$.getInstanceFromCache.call(this, path);\n  if (instance) return instance;\n\n  this._path = path;\n  this.type  = node.type(this.path);\n  this.scene = (typeof oSceneObject === 'undefined')?this.$.scene:oSceneObject;\n\n  this._type = 'node';\n\n  this.refreshAttributes();\n}\n\n/**\n * Initialize the attribute cache.\n * @private\n */\n$.oNode.prototype.attributesBuildCache = function (){\n  //Cache time can be used at later times, to check for auto-rebuild of caches. Not yet implemented.\n  this._cacheTime = (new Date()).getTime();\n\n  var _attributesList = node.getAttrList( this.path, 1 );\n  var _attributes = {};\n\n  for (var i in _attributesList){\n\n      var _attribute = new this.$.oAttribute(this, _attributesList[i]);\n      var _keyword = _attribute.keyword;\n\n      _attributes[_keyword] = _attribute;\n  }\n\n  this._attributes_cached = _attributes;\n}\n\n\n/**\n * Private function to create attributes setters and getters as properties of the node\n * @private\n */\n$.oNode.prototype.setAttrGetterSetter = function (attr, context){\n    if (typeof context === 'undefined') context = this;\n    // this.$.debug(\"Setting getter setters for attribute: \"+attr.keyword+\" of node: \"+this.name, this.$.DEBUG_LEVEL.DEBUG)\n\n    var _keyword = attr.shortKeyword;\n\n    Object.defineProperty( context, _keyword, {\n        enumerable : true,\n        configurable : true,\n        get : function(){\n            // MessageLog.trace(\"getting attribute \"+attr.keyword+\". animated: \"+(attr.column != null))\n            var _subAttrs = attr.subAttributes;\n            if (_subAttrs.length == 0){\n                // if attribute has animation, return the frames\n                if (attr.column != null) return attr.frames;\n                // otherwise return the value\n                var _value =  attr.getValue();\n            }else{\n                // if there are subattributes, create getter setters for each on the returned object\n                // this means every result of attr.getValue must be an object.\n                // For attributes that have a string return value, attr.getValue() actually returns a fake string object\n                // which is an object with a value property and a toString() method returning the value.\n                var _value = (attr.column != null)?new this.$.oList(attr.frames, 1):attr.getValue();\n                for (var i in _subAttrs){\n                    this.setAttrGetterSetter( _subAttrs[i], _value );\n                }\n            }\n            return _value;\n        },\n\n        set : function(newValue){\n            // this.$.debug(\"setting attribute through getter setter \"+attr.keyword+\" to value: \"+newValue, this.$.DEBUG_LEVEL.DEBUG)\n            // if attribute has animation, passed value must be a frame object\n            var _subAttrs = attr.subAttributes;\n\n            // setting the attribute directly if no subattributes are present, or if value is a color (exception)\n            if (_subAttrs.length == 0 || attr.type == \"COLOR\"){\n                if (attr.column != null) {\n                    if (!newValue.hasOwnProperty(\"frameNumber\")) {\n                        // fallback to set frame 1\n                        newValue = {value:newValue, frameNumber:1};\n                    }\n                    attr.setValue(newValue.value, newValue.frameNumber)\n                }else{\n                    return attr.setValue(newValue)\n                }\n            }else{\n                var _frame = undefined;\n                var _value = newValue;\n                // dealing with value being an object with frameNumber for animated values\n                if (attr.column != null) {\n                    if (!(newValue instanceof oFrame)) {\n                        // fallback to set frame 1\n                        newValue = {value:newValue, frameNumber:1};\n                    }\n\n                    _frame = newValue.frameNumber;\n                    _value = newValue.value;\n                }\n\n                // setting non animated attribute value\n                for (var i in _subAttrs){\n                    // set each subAttr individually based on corresponding values in the provided object\n                    var _keyword = _subAttrs[i].shortKeyword;\n                    if (_value.hasOwnProperty(_keyword)) _subAttrs[i].setValue(_value[_keyword], _frame);\n                }\n            }\n        }\n    });\n};\n\n\n/**\n * The derived path to the node.\n * @deprecated use oNode.path instead\n * @name $.oNode#fullPath\n * @readonly\n * @type {string}\n */\nObject.defineProperty($.oNode.prototype, 'fullPath', {\n    get : function( ){\n      return this._path;\n    }\n});\n\n\n/**\n * The path of the node (includes all groups from 'Top' separated by forward slashes).\n * To change the path of a node, use oNode.moveToGroup()\n * @name $.oNode#path\n * @type {string}\n * @readonly\n */\nObject.defineProperty($.oNode.prototype, 'path', {\n    get : function( ){\n      return this._path;\n    }\n});\n\n\n/**\n * The type of the node.\n * @name $.oNode#type\n * @readonly\n * @type {string}\n */\nObject.defineProperty( $.oNode.prototype, 'type', {\n    get : function( ){\n      return node.type( this.path );\n    }\n});\n\n\n/**\n * Is the node a group?\n * @name $.oNode#isGroup\n * @readonly\n * @deprecated check if the node is an instance of oGroupNode instead\n * @type {bool}\n */\nObject.defineProperty($.oNode.prototype, 'isGroup', {\n    get : function( ){\n      if( this.root ){\n        //in a sense, its a group.\n        return true;\n      }\n\n      return node.isGroup( this.path );\n    }\n});\n\n\n/**\n * The $.oNode objects contained in this group. This is deprecated and was moved to oGroupNode\n * @DEPRECATED Use oGroupNode.children instead.\n * @name $.oNode#children\n * @readonly\n * @type {$.oNode[]}\n */\nObject.defineProperty($.oNode.prototype, 'children', {\n    get : function( ){\n      if( !this.isGroup ){ return []; }\n\n      var _children = [];\n      var _subnodes = node.subNodes( this.path );\n      for( var n=0; n<_subnodes.length; n++ ){\n        _children.push( this.scene.getNodeByPath( _subnodes[n] ) );\n      }\n\n      return _children;\n    },\n\n    set : function( arr_children ){\n      //Consider a way to have this group adopt the children, move content here?\n      //this may be a bit tough to extend.\n    }\n});\n\n\n/**\n * Does the node exist?\n * @name $.oNode#exists\n * @type {bool}\n * @readonly\n */\nObject.defineProperty($.oNode.prototype, 'exists', {\n    get : function(){\n      if( this.type ){\n        return true;\n      }else{\n        return false;\n      }\n    }\n});\n\n\n/**\n * Is the node selected?\n * @name $.oNode#selected\n * @type {bool}\n */\nObject.defineProperty($.oNode.prototype, 'selected', {\n    get : function(){\n      for( var n=0;n<selection.numberOfNodesSelected;n++ ){\n          if( selection.selectedNode(n) == this.path ){\n            return true;\n          }\n      }\n\n      return false;\n    },\n\n    //Add it to the selection.\n    set : function( bool_exist ){\n      if( bool_exist ){\n        selection.addNodeToSelection( this.path );\n      }else{\n        selection.removeNodeFromSelection( this.path );\n      }\n    }\n\n});\n\n\n/**\n * The node's name.\n * @name $.oNode#name\n * @type {string}\n */\nObject.defineProperty($.oNode.prototype, 'name', {\n  get : function(){\n     return node.getName(this.path);\n  },\n\n  set : function(newName){\n    var _parent = node.parentNode(this.path);\n\n    // create a node with the chosen name to get the safe name generated by Harmony\n    var testName = node.add(_parent, newName, \"\", 0,0,0).split(\"/\").pop()\n    node.deleteNode(_parent + \"/\" + testName)\n\n    // do the renaming and update the path\n    node.rename(this.path, testName);\n    this._path = _parent+'/'+testName;\n\n    this.refreshAttributes();\n  }\n});\n\n\n/**\n * The group containing the node.\n * @name $.oNode#group\n * @readonly\n * @type {oGroupNode}\n */\nObject.defineProperty($.oNode.prototype, 'group', {\n    get : function(){\n         return this.scene.getNodeByPath( node.parentNode(this.path) )\n    }\n});\n\n\n/**\n * The $.oNode object for the parent in which this node exists.\n * @name $.oNode#parent\n * @readonly\n * @type {$.oNode}\n */\nObject.defineProperty( $.oNode.prototype, 'parent', {\n    get : function(){\n      if( this.root ){ return false; }\n\n      return this.scene.getNodeByPath( node.parentNode( this.path ) );\n    }\n});\n\n\n/**\n * Is the node enabled?\n * @name $.oNode#enabled\n * @type {bool}\n */\nObject.defineProperty($.oNode.prototype, 'enabled', {\n    get : function(){\n         return node.getEnable(this.path)\n    },\n\n    set : function(enabled){\n         node.setEnable(this.path, enabled)\n    }\n});\n\n\n/**\n * Is the node locked?\n * @name $.oNode#locked\n * @type {bool}\n */\nObject.defineProperty($.oNode.prototype, 'locked', {\n    get : function(){\n         return node.getLocked(this.path)\n    },\n\n    set : function(locked){\n         node.setLocked(this.path, locked)\n    }\n});\n\n\n/**\n * Is the node the root?\n * @name $.oNode#isRoot\n * @readonly\n * @type {bool}\n */\nObject.defineProperty($.oNode.prototype, 'isRoot', {\n    get : function(){\n         return this.path == \"Top\"\n    }\n});\n\n\n\n/**\n * The list of backdrops which contain this node.\n * @name $.oNode#containingBackdrops\n * @readonly\n * @type {$.oBackdrop[]}\n */\n Object.defineProperty($.oNode.prototype, 'containingBackdrops', {\n  get : function(){\n    var _backdrops = this.parent.backdrops;\n    var _path = this.path;\n    return _backdrops.filter(function(x){\n      var _nodePaths = x.nodes.map(function(x){return x.path});\n      return _nodePaths.indexOf(_path) != -1;\n    })\n  }\n});\n\n\n/**\n * The position of the node.\n * @name $.oNode#nodePosition\n * @type {oPoint}\n */\nObject.defineProperty($.oNode.prototype, 'nodePosition', {\n    get : function(){\n      var _z = 0.0;\n      try{ _z = node.coordZ(this.path); } catch( err ){this.$.debug(\"setting coordZ not implemented in Harmony versions before 17.\", this.$.DEBUG_LEVEL.ERROR)}\n      return new this.$.oPoint(node.coordX(this.path), node.coordY(this.path), _z );\n    },\n\n    set : function(newPosition){\n        node.setCoord(this.path, newPosition.x, newPosition.y, newPosition.y);\n    }\n});\n\n\n/**\n * The horizontal position of the node in the node view.\n * @name $.oNode#x\n * @type {float}\n */\nObject.defineProperty($.oNode.prototype, 'x', {\n    get : function(){\n         return node.coordX(this.path)\n    },\n\n    set : function(x){\n        var _pos = this.nodePosition;\n        node.setCoord(this.path, x, _pos.y)\n    }\n});\n\n\n/**\n * The vertical position of the node in the node view.\n * @name $.oNode#y\n * @type {float}\n */\nObject.defineProperty($.oNode.prototype, 'y', {\n    get : function(){\n         return node.coordY(this.path)\n    },\n\n    set : function(y){\n        var _pos = this.nodePosition;\n        node.setCoord(this.path, _pos.x, y)\n    }\n});\n\n\n/**\n * The depth position of the node in the node view.\n * @name $.oNode#z\n * @type {float}\n */\nObject.defineProperty($.oNode.prototype, 'z', {\n    get : function(){\n        var _z = 0.0;\n        try{ _z = node.coordZ(this.path); } catch( err ){ this.$.debug(\"setting coordZ not implemented in Harmony versions before 17.\", this.$.DEBUG_LEVEL.ERROR)}\n\n        return _z;\n    },\n\n    set : function(z){\n        var _pos = this.nodePosition;\n        node.setCoord( this.path, _pos.x, _pos.y, z );\n    }\n});\n\n\n/**\n * The width of the node in the node view.\n * @name $.oNode#width\n * @readonly\n * @type {float}\n */\nObject.defineProperty($.oNode.prototype, 'width', {\n    get : function(){\n         return node.width(this.path)\n    }\n});\n\n\n\n/**\n * The height of the node in the node view.\n * @name $.oNode#height\n * @readonly\n * @type {float}\n */\nObject.defineProperty($.oNode.prototype, 'height', {\n    get : function(){\n         return node.height(this.path)\n    }\n});\n\n\n\n/**\n * The list of oNodeLinks objects describing the connections to the inport of this node, in order of inport.\n * @name $.oNode#inLinks\n * @readonly\n * @deprecated returns $.oNodeLink instances but $.oLink is preferred. Use oNode.getInLinks() instead.\n * @type {$.oNodeLink[]}\n */\nObject.defineProperty($.oNode.prototype, 'inLinks', {\n    get : function(){\n        var nodeRef = this;\n        var newList = new this.$.oList( [], 0, node.numberOfInputPorts(this.path),\n                                           function( listItem, index ){ return new this.$.oNodeLink( false, false, nodeRef, index, false ); },\n                                           function(){ throw new ReferenceError(\"Unable to set inLinks\"); },\n                                           false\n                                         );\n        return newList;\n    }\n});\n\n\n/**\n * The list of nodes connected to the inport of this node, in order of inport.\n * @name $.oNode#inNodes\n * @readonly\n * @type {$.oNode[]}\n * @deprecated returns $.oNodeLink instances but $.oLink is preferred. Use oNode.linkedInNodes instead.\n*/\nObject.defineProperty($.oNode.prototype, 'inNodes', {\n    get : function(){\n        var _inNodes = [];\n        var _inPorts = this.inPorts;\n        // TODO: ignore/traverse groups\n        for (var i = 0; i < _inPorts; i++){\n            var _node = this.getLinkedInNode(i);\n            if (_node != null) _inNodes.push(_node)\n        }\n        return _inNodes;\n    }\n});\n\n\n/**\n * The number of link ports on top of the node, connected or not.\n * @name $.oNode#inPorts\n * @readonly\n * @type {int}\n*/\nObject.defineProperty($.oNode.prototype, 'inPorts', {\n  get : function(){\n    return node.numberOfInputPorts(this.path);\n  }\n});\n\n\n/**\n * The list of nodes connected to the outports of this node\n * @name $.oNode#outNodes\n * @readonly\n * @type {$.oNode[][]}\n * @deprecated  returns $.oNodeLink instances but $.oLink is preferred. Use oNode.linkedOutNodes instead.\n*/\nObject.defineProperty($.oNode.prototype, 'outNodes', {\n    get : function(){\n        var _outNodes = [];\n        var _outPorts = this.outPorts;\n\n        for (var i = 0; i < _outPorts; i++){\n            var _outLinks = [];\n            var _outLinksNumber = this.getOutLinksNumber(i);\n            for (var j = 0; j < _outLinksNumber; j++){\n                var _node = this.getLinkedOutNode(i, j);\n\n                if (_node != null) _outLinks.push(_node);\n            }\n\n            //Always return the list of links for consistency.\n            _outNodes.push(_outLinks);\n        }\n        return _outNodes;\n    }\n});\n\n\n/**\n * The number of link ports at the bottom of the node, connected or not.\n * @name $.oNode#outPorts\n * @readonly\n * @type {int}\n*/\nObject.defineProperty($.oNode.prototype, 'outPorts', {\n  get : function(){\n    return node.numberOfOutputPorts(this.path);\n  }\n});\n\n\n/**\n * The list of oNodeLinks objects describing the connections to the outports of this node, in order of outport.\n * @name $.oNode#outLinks\n * @readonly\n * @type {$.oNodeLink[]}\n * @deprecated  returns $.oNodeLink instances but $.oLink is preferred. Use oNode.getOutLinks instead.\n */\nObject.defineProperty($.oNode.prototype, 'outLinks', {\n    get : function(){\n        var nodeRef = this;\n\n        var lookup_list = [];\n        for (var i = 0; i < node.numberOfOutputPorts(this.path); i++){\n          if( node.numberOfOutputLinks(this.path, i) > 0 ){\n            for (var j = 0; j < node.numberOfOutputLinks(this.path, i); j++){\n              lookup_list.push( [i,j] );\n            }\n          }else{\n            lookup_list.push( [i,0] );\n          }\n        }\n\n        var newList = new this.$.oList( [], 0, lookup_list.length,\n                                           function( listItem, index ){ return new this.$.oNodeLink( nodeRef, lookup_list[index][0], false, false, lookup_list[index][1] ); },\n                                           function(){ throw new ReferenceError(\"Unable to set inLinks\"); },\n                                           false\n                                         );\n        return newList;\n    }\n});\n\n\n/**\n * The list of nodes connected to the inport of this node, as a flat list, in order of inport.\n * @name $.oNode#linkedOutNodes\n * @readonly\n * @type {$.oNode[]}\n */\nObject.defineProperty($.oNode.prototype, 'linkedOutNodes', {\n  get: function(){\n    var _outNodes = this.getOutLinks().map(function(x){return x.inNode});\n    return _outNodes;\n  }\n})\n\n\n/**\n * The list of nodes connected to the inport of this node, as a flat list, in order of inport.\n * @name $.oNode#linkedInNodes\n * @readonly\n * @type {$.oNode[]}\n */\nObject.defineProperty($.oNode.prototype, 'linkedInNodes', {\n  get: function(){\n    var _inNodes = this.getInLinks().map(function(x){return x.outNode});\n    return _inNodes\n  }\n})\n\n\n/**\n * The list of nodes connected to the inport of this node, in order of inport. Similar to oNode.inNodes\n * @name $.oNode#ins\n * @readonly\n * @type {$.oNode[]}\n * @deprecated alias for deprecated oNode.inNodes property\n*/\nObject.defineProperty($.oNode.prototype, 'ins', {\n    get : function(){\n      return this.inNodes;\n    }\n});\n\n\n/**\n * The list of nodes connected to the outport of this node, in order of outport and links. Similar to oNode.outNodes\n * @name $.oNode#outs\n * @readonly\n * @type {$.oNode[][]}\n * @deprecated alias for deprecated oNode.outNodes property\n*/\nObject.defineProperty($.oNode.prototype, 'outs', {\n    get : function(){\n      return this.outNodes;\n    }\n});\n\n\n/**\n * An object containing all attributes of this node.\n * @name $.oNode#attributes\n * @readonly\n * @type {oAttribute}\n * @example\n * // You can get access to the actual oAttribute object for a node parameter by using the dot notation:\n *\n * var myNode = $.scn.$node(\"Top/Drawing\")\n * var drawingAttribute = myNode.attributes.drawing.element\n *\n * // from there, it's possible to set/get the value of the attribute, get the column, the attribute keyword etc.\n *\n * drawingAttribute.setValue (\"1\", 5);           // creating an exposure of drawing 1 at frame 5\n * var drawingColumn = drawingAttribute.column;  // grabbing the column linked to the attribute that holds all the animation\n * $.log(drawingAttribute.keyword);              // \"DRAWING.ELEMENT\"\n *\n * // for a more direct way to access an attribute, it's possible to also call:\n *\n * var drawingAttribute = myNode.getAttributeByName(\"DRAWING.ELEMENT\");\n*/\nObject.defineProperty($.oNode.prototype, 'attributes', {\n  get : function(){\n      return this._attributes_cached;\n  }\n});\n\n\n/**\n * The bounds of the node rectangle in the node view.\n * @name $.oNode#bounds\n * @readonly\n * @type {oBox}\n*/\nObject.defineProperty( $.oNode.prototype, 'bounds', {\n  get : function(){\n    return new this.$.oBox(this.x, this.y, this.x+this.width, this.y+this.height);\n  }\n});\n\n\n/**\n * The transformation matrix of the node at the currentFrame.\n * @name $.oNode#matrix\n * @readonly\n * @type {oMatrix}\n*/\nObject.defineProperty( $.oNode.prototype, 'matrix', {\n  get : function(){\n    return this.getMatrixAtFrame(this.scene.currentFrame);\n  }\n});\n\n\n/**\n * The list of all columns linked across all the attributes of this node.\n * @name $.oNode#linkedColumns\n * @readonly\n * @type {oColumn[]}\n*/\nObject.defineProperty($.oNode.prototype, 'linkedColumns', {\n  get : function(){\n    var _attributes = this.attributes;\n    var _columns = [];\n\n    for (var i in _attributes){\n      _columns = _columns.concat(_attributes[i].getLinkedColumns());\n    }\n    return _columns;\n  }\n})\n\n\n\n/**\n * Whether the node can create new in-ports.\n * @name $.oNode#canCreateInPorts\n * @readonly\n * @type {bool}\n*/\nObject.defineProperty($.oNode.prototype, 'canCreateInPorts', {\n  get : function(){\n    return [\"COMPOSITE\",\n            \"GROUP\",\n            \"MultiLayerWrite\",\n            \"TransformGate\",\n            \"TransformationSwitch\",\n            \"DeformationCompositeModule\",\n            \"MATTE_COMPOSITE\",\n            \"COMPOSITE_GENERIC\",\n            \"ParticleBkerComposite\",\n            \"ParticleSystemComposite\",\n            \"ParticleRegionComposite\",\n            \"PointConstraintMulti\",\n            \"MULTIPORT_OUT\"]\n            .indexOf(this.type) != -1;\n  }\n})\n\n\n/**\n * Whether the node can create new out-ports.\n * @name $.oNode#canCreateOutPorts\n * @readonly\n * @type {bool}\n*/\nObject.defineProperty($.oNode.prototype, 'canCreateOutPorts', {\n  get : function(){\n    return [\"GROUP\",\n            \"MULTIPORT_IN\"]\n            .indexOf(this.type) != -1;\n  }\n})\n\n\n/**\n * Returns the number of links connected to an in-port\n * @param   {int}      inPort      the number of the port to get links from.\n */\n$.oNode.prototype.getInLinksNumber = function(inPort){\n  if (this.inPorts < inPort) return null;\n  return node.isLinked(this.path, inPort)?1:0;\n}\n\n\n/**\n * Returns the oLink object representing the connection of a specific inPort\n * @param   {int}      inPort      the number of the port to get links from.\n * @return  {$.oLink}  the oLink Object representing the link connected to the inport\n */\n$.oNode.prototype.getInLink = function(inPort){\n  if (this.inPorts < inPort) return null;\n  var _info = node.srcNodeInfo(this.path, inPort);\n  // this.$.log(this.path+\" \"+inPort+\" \"+JSON.stringify(_info))\n\n  if (!_info) return null;\n\n  var _inNode = this.scene.getNodeByPath(_info.node);\n  var _inLink = new this.$.oLink(_inNode, this, _info.port, inPort, _info.link, true);\n\n  // this.$.log(\"inLink: \"+_inLink)\n  return _inLink;\n}\n\n\n/**\n * Returns all the valid oLink objects describing the links that are connected into this node.\n * @return {$.oLink[]}  An array of $.oLink objects.\n */\n$.oNode.prototype.getInLinks = function(){\n  var _inPorts = this.inPorts;\n  var _inLinks = [];\n\n  for (var i = 0; i<_inPorts; i++){\n    var _link = this.getInLink(i);\n    if (_link != null) _inLinks.push(_link);\n  }\n\n  return _inLinks;\n}\n\n\n/**\n * Returns a free unconnected in-port\n * @param  {bool}  [createNew=true]  Whether to allow creation of new ports\n * @return {int} the port number that isn't connected\n */\n$.oNode.prototype.getFreeInPort = function(createNew){\n  if (typeof createNew === 'undefined') var createNew = true;\n\n  var _inPorts = this.inPorts;\n\n  for (var i=0; i<_inPorts; i++){\n    if (this.getInLinksNumber(i) == 0) return i;\n  }\n  if (_inPorts == 0 && this.canCreateInPorts) return 0;\n  if (createNew && this.canCreateInPorts) return _inPorts;\n  this.$.debug(\"can't get free inPort for node \"+this.path, this.$.DEBUG_LEVEL.ERROR);\n  return null\n}\n\n\n/**\n * Links this node's inport to the given module, at the inport and outport indices.\n * @param   {$.oNode}   nodeToLink             The node to link this one's inport to.\n * @param   {int}       [ownPort]              This node's inport to connect.\n * @param   {int}       [destPort]             The target node's outport to connect.\n * @param   {bool}      [createPorts]          Whether to create new ports on the nodes.\n *\n * @return  {bool}    The result of the link, if successful.\n */\n$.oNode.prototype.linkInNode = function( nodeToLink, ownPort, destPort, createPorts){\n  if (!(nodeToLink instanceof this.$.oNode)) throw new Error(\"Incorrect type for argument 'nodeToLink'. Must provide an $.oNode.\")\n\n  var _link = (new this.$.oLink(nodeToLink, this, destPort, ownPort)).getValidLink(createPorts, createPorts);\n  if (_link == null) return;\n  this.$.debug(\"linking \"+_link, this.$.DEBUG_LEVEL.LOG);\n\n  return _link.connect();\n};\n\n\n/**\n * Searches for and unlinks the $.oNode object from this node's inNodes.\n * @param   {$.oNode}   oNodeObject            The node to link this one's inport to.\n * @return  {bool}    The result of the unlink.\n */\n$.oNode.prototype.unlinkInNode = function( oNodeObject ){\n\n  var _node = oNodeObject.path;\n\n  var _links = this.getInLinks();\n\n  for (var i in _links){\n    if (_links[i].outNode.path == _node) return _links[i].disconnect();\n  }\n\n  throw new Error (oNodeObject.name + \" is not linked to node \" + this.name + \", can't unlink.\");\n};\n\n\n/**\n * Unlinks a specific port from this node's inport.\n * @param   {int}       inPort                 The inport to disconnect.\n *\n * @return  {bool}    The result of the unlink, if successful.\n */\n$.oNode.prototype.unlinkInPort = function( inPort ){\n  // Default values for optional parameters\n  if (typeof inPort === 'undefined') inPort = 0;\n\n  return node.unlink( this.path, inPort );\n};\n\n\n/**\n * Returns the node connected to a specific in-port\n * @param   {int}        inPort      the number of the port to get the linked Node from.\n * @return  {$.oNode}                The node connected to this in-port\n */\n$.oNode.prototype.getLinkedInNode = function(inPort){\n  if (this.inPorts < inPort) return null;\n  return this.scene.getNodeByPath(node.srcNode(this.path, inPort));\n}\n\n\n/**\n * Returns the number of links connected to an outPort\n * @param   {int}      outPort      the number of the port to get links from.\n * @return  {int}    the number of links\n */\n$.oNode.prototype.getOutLinksNumber = function(outPort){\n  if (this.outPorts < outPort) return null;\n  return node.numberOfOutputLinks(this.path, outPort);\n}\n\n\n/**\n * Returns the $.oLink object representing the connection of a specific outPort / link\n * @param   {int}      outPort      the number of the port to get the link from.\n * @param   {int}      [outLink]    the index of the link.\n * @return {$.oLink}   The link object describing the connection\n */\n$.oNode.prototype.getOutLink = function(outPort, outLink){\n  if (typeof outLink === 'undefined') var outLink = 0;\n\n  if (this.outPorts < outPort) return null;\n  if (this.getOutLinksNumber(outPort) < outLink) return null;\n\n  var _info = node.dstNodeInfo(this.path, outPort, outLink);\n  if (!_info) return null;\n\n  var _outNode = this.scene.getNodeByPath(_info.node);\n  var _outLink = new this.$.oLink(this, _outNode, outPort, _info.port, outLink, true);\n\n  return _outLink;\n}\n\n\n/**\n * Returns all the valid oLink objects describing the links that are coming out of this node.\n * @return {$.oLink[]}  An array of $.oLink objects.\n */\n$.oNode.prototype.getOutLinks = function(){\n  var _outPorts = this.outPorts;\n  var _links = [];\n\n  for (var i = 0; i<_outPorts; i++){\n    var _outLinks = this.getOutLinksNumber(i);\n    for (var j = 0; j<_outLinks; j++){\n      var _link = this.getOutLink(i, j);\n      if (_link != null) _links.push(_link);\n    }\n  }\n\n  return _links;\n}\n\n\n/**\n * Returns a free unconnected out-port\n * @param  {bool}  [createNew=true]  Whether to allow creation of new ports\n * @return {int} the port number that isn't connected\n */\n$.oNode.prototype.getFreeOutPort = function(createNew){\n  if (typeof createNew === 'undefined') var createNew = false;\n\n  var _outPorts = this.outPorts;\n  for (var i=0; i<_outPorts; i++){\n    if (this.getOutLinksNumber(i) == 0) return i;\n  }\n\n  if (_outPorts == 0 && this.canCreateOutPorts) return 0;\n\n  if (createNew && this.canCreateOutPorts) return _outPorts;\n\n  return _outPorts-1; // if no empty outPort can be found, return the last one\n}\n\n\n/**\n * Links this node's out-port to the given module, at the inport and outport indices.\n * @param   {$.oNode} nodeToLink             The node to link this one's outport to.\n * @param   {int}     [ownPort]              This node's outport to connect.\n * @param   {int}     [destPort]             The target node's inport to connect.\n * @param   {bool}    [createPorts]          Whether to create new ports on the nodes.\n *\n * @return  {bool}    The result of the link, if successful.\n */\n$.oNode.prototype.linkOutNode = function(nodeToLink, ownPort, destPort, createPorts){\n  if (!(nodeToLink instanceof this.$.oNode)) throw new Error(\"Incorrect type for argument 'nodeToLink'. Must provide an $.oNode.\")\n\n  var _link = (new this.$.oLink(this, nodeToLink, ownPort, destPort)).getValidLink(createPorts, createPorts)\n  if (_link == null) return;\n  this.$.debug(\"linking \"+_link, this.$.DEBUG_LEVEL.LOG);\n\n  return _link.connect();\n}\n\n\n/**\n * Links this node's out-port to the given module, at the inport and outport indices.\n * @param   {$.oNode}   oNodeObject            The node to unlink from this node's outports.\n *\n * @return  {bool}    The result of the link, if successful.\n */\n$.oNode.prototype.unlinkOutNode = function( oNodeObject ){\n  var _node = oNodeObject.path;\n\n  var _links = this.getOutLinks();\n\n  for (var i in _links){\n    if (_links[i].inNode.path == _node) return _links[i].disconnect();\n  }\n\n  throw new Error (oNodeObject.name + \" is not linked to node \" + this.name + \", can't unlink.\");\n};\n\n\n/**\n * Returns the node connected to a specific outPort\n * @param   {int}      outPort      the number of the port to get the node from.\n * @param   {int}      [outLink=0]  the index of the link.\n * @return  {$.oNode}   The node connected to this outPort and outLink\n */\n$.oNode.prototype.getLinkedOutNode = function(outPort, outLink){\n  if (typeof outLink == 'undefined') var outLink = 0;\n  if (this.outPorts < outPort || this.getOutLinksNumber(outPort) < outLink) return null;\n  return this.scene.getNodeByPath(node.dstNode(this.path, outPort, outLink));\n}\n\n\n/**\n * Unlinks a specific port/link from this node's output.\n * @param   {int}     outPort                 The outPort to disconnect.\n * @param   {int}     outLink                 The outLink to disconnect.\n *\n * @return  {bool}    The result of the unlink, if successful.\n */\n$.oNode.prototype.unlinkOutPort = function( outPort, outLink ){\n    // Default values for optional parameters\n    if (typeof outLink === 'undefined') outLink = 0;\n\n    try{\n      var dstNodeInfo = node.dstNodeInfo(this.path, outPort, outLink);\n      if (dstNodeInfo) node.unlink(dstNodeInfo.node, dstNodeInfo.port);\n      return true;\n    }catch(err){\n      this.$.debug(\"couldn't unlink port \"+outPort+\" of node \"+this.path, this.$.DEBUG_LEVEL.ERROR)\n      return false;\n    }\n};\n\n\n/**\n * Inserts the $.oNodeObject provided as an innode to this node, placing it between any existing nodes if the link already exists.\n * @param   {int}     inPort                 This node's inport to connect.\n * @param   {$.oNode} oNodeObject            The node to link this one's outport to.\n * @param   {int}     inPortTarget           The target node's inPort to connect.\n * @param   {int}     outPortTarget          The target node's outPort to connect.\n *\n * @return  {bool}    The result of the link, if successful.\n */\n$.oNode.prototype.insertInNode = function( inPort, oNodeObject, inPortTarget, outPortTarget ){\n    var _node = oNodeObject.path;\n\n    //QScriptValue\n    if( this.ins[inPort] ){\n      //INSERT BETWEEN.\n      var node_linkinfo = node.srcNodeInfo( this.path, inPort );\n      node.link( node_linkinfo.node, node_linkinfo.port, _node, inPortTarget, true, true );\n      node.unlink( this.path, inPort );\n      return node.link( oNodeObject.path, outPortTarget, this.path, inPort, true, true );\n    }\n\n    return this.linkInNode( oNodeObject, inPort, outPortTarget );\n};\n\n\n/**\n * Moves the node into the specified group. This doesn't create any composite or links to the multiport nodes. The node will be unlinked.\n * @param   {oGroupNode}   group      the group node to move the node into.\n */\n$.oNode.prototype.moveToGroup = function(group){\n  var _name = this.name;\n  if (group instanceof oGroupNode) group = group.path;\n\n  if (this.group != group){\n    this.$.beginUndo(\"oH_moveNodeToGroup_\"+_name)\n\n    var _groupNodes = node.subNodes(group);\n\n    node.moveToGroup(this.path, group);\n    this._path = group+\"/\"+_name;\n\n    // detect creation of a composite and remove it\n    var _newNodes = node.subNodes(group)\n    if (_newNodes.length > _groupNodes.length+1){\n      for (var i in _newNodes){\n        if (_groupNodes.indexOf(_newNodes[i]) == -1 && _newNodes[i] != this.path) {\n           var _comp = this.scene.getNodeByPath(_newNodes[i]);\n           if (_comp && _comp.type == \"COMPOSITE\") _comp.remove();\n           break;\n        }\n      }\n    }\n\n    // remove automated links\n    var _inPorts = this.inPorts;\n    for (var i=0; i<_inPorts; i++){\n      this.unlinkInPort(i);\n    }\n\n    var _outPorts = this.outPorts;\n    for (var i=0; i<_outPorts; i++){\n      var _outLinks = this.getOutLinksNumber(i);\n      for (var j=_outLinks-1; j>=0; j--){\n        this.unlinkOutPort(i, j);\n      }\n    }\n\n    this.refreshAttributes();\n\n    this.$.endUndo();\n  }\n}\n\n\n/**\n * Get the transformation matrix for the node at the given frame\n * @param {int} frameNumber\n * @returns {oMatrix}  the matrix object\n */\n$.oNode.prototype.getMatrixAtFrame = function (frameNumber){\n  return new this.$.oMatrix(node.getMatrix(this.path, frameNumber));\n}\n\n\n/**\n * Retrieves the node layer in the timeline provided.\n * @param   {oTimeline}   [timeline]     Optional: the timeline object to search the column Layer. (by default, grabs the current timeline)\n *\n * @return  {int}    The index within that timeline.\n */\n $.oNode.prototype.getTimelineLayer = function(timeline){\n  if (typeof timeline === 'undefined') var timeline = this.$.scene.currentTimeline;\n\n  var _nodeLayers = timeline.layers.map(function(x){return x.node.path});\n  if (_nodeLayers.indexOf(this.path)<timeline.layers.length && _nodeLayers.indexOf(this.path)>0){\n    return timeline.layers[_nodeLayers.indexOf(this.path)];\n  }\n  return null\n}\n\n\n/**\n * Retrieves the node index in the timeline provided.\n * @param   {oTimeline}   [timeline]     Optional: the timeline object to search the column Layer. (by default, grabs the current timeline)\n *\n * @return  {int}    The index within that timeline.\n */\n$.oNode.prototype.timelineIndex = function(timeline){\n  if (typeof timeline === 'undefined') var timeline = this.$.scene.currentTimeline;\n\n  var _nodes = timeline.compositionLayersList;\n  return _nodes.indexOf(this.path);\n}\n\n\n/**\n * obtains the nodes contained in the group, allows recursive search. This method is deprecated and was moved to oGroupNode\n * @DEPRECATED\n * @param   {bool}   recurse           Whether to recurse internally for nodes within children groups.\n *\n * @return  {$.oNode[]}    The subbnodes contained in the group.\n */\n$.oNode.prototype.subNodes = function(recurse){\n    if (typeof recurse === 'undefined') recurse = false;\n    var _nodes = node.subNodes(this.path);\n    var _subNodes = [];\n    for (var _node in _nodes){\n        var _oNodeObject = new this.$.oNode( _nodes[_node] );\n        _subNodes.push(_oNodeObject);\n        if (recurse && node.isGroup(_nodes[_node])) _subNodes = _subNodes.concat(_$.oNodeObject.subNodes(recurse));\n    }\n\n    return _subNodes;\n};\n\n\n /**\n * Place a node above one or more nodes with an offset.\n * @param   {$.oNode[]}     oNodeArray                The array of nodes to center this above.\n * @param   {float}         xOffset                   The horizontal offset to apply after centering.\n * @param   {float}         yOffset                   The vertical offset to apply after centering.\n *\n * @return  {oPoint}   The resulting position of the node.\n */\n$.oNode.prototype.centerAbove = function( oNodeArray, xOffset, yOffset ){\n  if (!oNodeArray) throw new Error (\"An array of nodes to center node '\"+this.name+\"' above must be provided.\")\n\n  // Defaults for optional parameters\n    if (typeof xOffset === 'undefined') var xOffset = 0;\n    if (typeof yOffset === 'undefined') var yOffset = -30;\n\n    // Works with nodes and nodes array\n    if (oNodeArray instanceof this.$.oNode) oNodeArray = [oNodeArray];\n    if (oNodeArray.filter(function(x){return !x}).length) throw new Error (\"Can't center node '\"+ this.name+ \"' above nodes \"+ oNodeArray + \", invalid nodes found.\")\n\n    var _box = new this.$.oBox();\n    _box.includeNodes( oNodeArray );\n\n    this.x = _box.center.x - this.width/2 + xOffset;\n    this.y = _box.top - this.height + yOffset;\n\n    return new this.$.oPoint(this.x, this.y, this.z);\n};\n\n\n /**\n * Place a node below one or more nodes with an offset.\n * @param   {$.oNode[]} oNodeArray           The array of nodes to center this below.\n * @param   {float}     xOffset              The horizontal offset to apply after centering.\n * @param   {float}     yOffset              The vertical offset to apply after centering.\n *\n * @return  {oPoint}   The resulting position of the node.\n */\n$.oNode.prototype.centerBelow = function( oNodeArray, xOffset, yOffset){\n    if (!oNodeArray) throw new Error (\"An array of nodes to center node '\"+this.name+\"' below must be provided.\")\n\n    // Defaults for optional parameters\n    if (typeof xOffset === 'undefined') var xOffset = 0;\n    if (typeof yOffset === 'undefined') var yOffset = 30;\n\n    // Works with nodes and nodes array\n    if (oNodeArray instanceof this.$.oNode) oNodeArray = [oNodeArray];\n    if (oNodeArray.filter(function(x){return !x}).length) throw new Error (\"Can't center node '\"+ this.name+ \"' below nodes \"+ oNodeArray + \", invalid nodes found.\")\n\n    var _box = new this.$.oBox();\n    _box.includeNodes(oNodeArray);\n\n    this.x = _box.center.x - this.width/2 + xOffset;\n    this.y = _box.bottom + yOffset;\n\n    return new this.$.oPoint(this.x, this.y, this.z);\n}\n\n\n /**\n * Place at center of one or more nodes with an offset.\n * @param   {$.oNode[]} oNodeArray           The array of nodes to center this below.\n * @param   {float}     xOffset              The horizontal offset to apply after centering.\n * @param   {float}     yOffset              The vertical offset to apply after centering.\n *\n * @return  {oPoint}   The resulting position of the node.\n */\n$.oNode.prototype.placeAtCenter = function( oNodeArray, xOffset, yOffset ){\n    // Defaults for optional parameters\n    if (typeof xOffset === 'undefined') var xOffset = 0;\n    if (typeof yOffset === 'undefined') var yOffset = 0;\n\n    // Works with nodes and nodes array\n    if (typeof oNodeArray === 'oNode') oNodeArray = [oNodeArray];\n\n    var _box = new this.$.oBox();\n    _box.includeNodes(oNodeArray);\n\n    this.x = _box.center.x - this.width/2 + xOffset;\n    this.y = _box.center.y - this.height/2 + yOffset;\n\n    return new this.$.oPoint(this.x, this.y, this.z);\n}\n\n\n /**\n * Create a clone of the node.\n * @param   {string}    newName              The new name for the cloned module.\n * @param   {oPoint}    newPosition          The new position for the cloned module.\n */\n$.oNode.prototype.clone = function( newName, newPosition ){\n  // Defaults for optional parameters\n  if (typeof newPosition === 'undefined') var newPosition = this.nodePosition;\n  if (typeof newName === 'undefined') var newName = this.name+\"_clone\";\n\n  this.$.beginUndo(\"oH_cloneNode_\"+this.name);\n\n  var _clonedNode = this.group.addNode(this.type, newName, newPosition);\n  var _attributes = this.attributes;\n\n  for (var i in _attributes){\n    var _clonedAttribute = _clonedNode.getAttributeByName(_attributes[i].keyword);\n    _clonedAttribute.setToAttributeValue(_attributes[i]);\n  }\n\n  var palettes = this.palettes\n  for (var i in palettes){\n    _clonedNode.linkPalette(palettes[i])\n  }\n\n  this.$.endUndo();\n\n  return _clonedNode;\n};\n\n\n /**\n * Duplicates a node by creating an independent copy.\n * @param   {string}    [newName]              The new name for the duplicated node.\n * @param   {oPoint}    [newPosition]          The new position for the duplicated node.\n */\n$.oNode.prototype.duplicate = function(newName, newPosition){\n  if (typeof newPosition === 'undefined') var newPosition = this.nodePosition;\n  if (typeof newName === 'undefined') var newName = this.name+\"_duplicate\";\n\n  this.$.beginUndo(\"oH_cloneNode_\"+this.name);\n\n  var _duplicateNode = this.group.addNode(this.type, newName, newPosition);\n  var _attributes = this.attributes;\n\n  for (var i in _attributes){\n    var _duplicateAttribute = _duplicateNode.getAttributeByName(_attributes[i].keyword);\n    _duplicateAttribute.setToAttributeValue(_attributes[i], true);\n  }\n\n  var palettes = this.palettes\n  for (var i in palettes){\n    _duplicateNode.linkPalette(palettes[i])\n  }\n\n  this.$.endUndo();\n\n  return _duplicateNode;\n};\n\n\n /**\n * Removes the node from the scene.\n * @param   {bool}    deleteColumns              Should the columns of drawings be deleted as well?\n * @param   {bool}    deleteElements             Should the elements of drawings be deleted as well?\n *\n * @return  {void}\n */\n$.oNode.prototype.remove = function( deleteColumns, deleteElements ){\n  if (typeof deleteFrames === 'undefined') var deleteColumns = true;\n  if (typeof deleteElements === 'undefined') var deleteElements = true;\n\n  this.$.beginUndo(\"oH_deleteNode_\"+this.name)\n  // restore links for special types;\n  if (this.type == \"PEG\"){\n    var inNodes = this.inNodes; //Pegs can only have one inNode but we'll implement the general case for other types\n    var outNodes = this.outNodes;\n    for (var i in inNodes){\n      for (var j in outNodes){\n        for( var k in outNodes[j] ){\n          inNodes[i].linkOutNode(outNodes[j][k]);\n        }\n      }\n    }\n  }\n\n  node.deleteNode(this.path, deleteColumns, deleteElements);\n  this.$.endUndo();\n}\n\n\n /**\n * Provides a matching attribute based on provided keyword name. Keyword can include \".\" to get subattributes.\n * @param   {string}    keyword                    The attribute keyword to search.\n * @return  {oAttribute}   The matched attribute object, given the keyword.\n */\n$.oNode.prototype.getAttributeByName = function( keyword ){\n  keyword = keyword.toLowerCase();\n  keyword = keyword.split(\".\");\n\n  // we go through the keywords, trying to access an attribute corresponding to the name\n  var _attribute = this.attributes;\n  for (var i in keyword){\n    var _keyword = keyword[i];\n\n    // applying conversion to the name 3dpath\n    if (_keyword == \"3dpath\") _keyword = \"path3d\";\n\n    if (!(_keyword in _attribute)) return null;\n\n    _attribute = _attribute[_keyword];\n  }\n\n  if (_attribute instanceof this.$.oAttribute) return _attribute;\n  return null;\n}\n\n\n /**\n * Used in converting the node to a string value, provides the string-path.\n * @return  {string}   The node path's as a string.\n */\n$.oNode.prototype.toString = function(){\n    return this.path;\n}\n\n\n /**\n * Provides a matching attribute based on the column name provided. Assumes only one match at the moment.\n * @param   {string}       columnName                    The column name to search.\n * @return  {oAttribute}   The matched attribute object, given the column name.\n */\n$.oNode.prototype.getAttributeByColumnName = function( columnName ){\n  // var attribs = [];\n\n  //Initially check for cache.\n  var cdate = (new Date()).getTime();\n  if( this.$.cache_columnToNodeAttribute[columnName] ){\n    if( ( cdate - this.$.cache_columnToNodeAttribute[columnName].date ) < 5000 ){\n      //Cache is in form : { \"node\":oAttributeObject.node, \"attribute\": this, \"date\": (new Date()).getTime() }\n      // attribs.push( this.$.cache_columnToNodeAttribute[columnName].attribute );\n      return this.$.cache_columnToNodeAttribute[columnName].attribute;\n    }\n  }\n\n  var _attributes = this.attributes;\n\n  for( var n in _attributes){\n    var t_attrib = _attributes[n];\n    if( t_attrib.subAttributes.length>0 ){\n      //Also check subattributes.\n      for( var t=0; t<t_attrib.subAttributes.length; t++ ){\n        var t_attr = t_attrib.subAttributes[t];\n        if( t_attr.column && t_attr.column.uniqueName == columnName) return t_attr;\n      }\n    }\n\n    if( t_attrib.column && t_attrib.column.uniqueName == columnName) return t_attrib;\n  }\n  // return attribs;\n}\n\n\n /**\n * Provides a column->attribute lookup table for timeline building.\n * @private\n * @return  {object}   The column_name->attribute object LUT.  {colName: { \"node\":oNode, \"column\":oColumn } }\n */\n$.oNode.prototype.getAttributesColumnCache = function( obj_lut ){\n  if (typeof obj_lut === 'undefined') obj_lut = {};\n\n  for( var n in this.attributes ){\n    var t_attrib = this.attributes[n];\n    if( t_attrib.subAttributes.length>0 ){\n      //Also check subattributes.\n      for( var t=0;t<t_attrib.subAttributes.length;t++ ){\n        var t_attr = t_attrib.subAttributes[t];\n        if( t_attr.column ){\n          obj_lut[ t_attr.column.uniqueName ] = { \"node\":this, \"attribute\":t_attr };\n        }\n      }\n    }\n\n    if( t_attrib.column ){\n      obj_lut[ t_attr.column.uniqueName ] = { \"node\":this, \"attribute\":t_attr };\n    }\n  }\n\n  return obj_lut;\n}\n\n\n/**\n * Creates an $.oNodeLink and connects this node to the target via this nodes outport.\n * @param   {oNode}         nodeToLink                          The target node as an in node.\n * @param   {int}           ownPort                             The out port on this node to connect to.\n * @param   {int}           destPort                            The in port on the inNode to connect to.\n *\n * @return {$.oNodeLink}    the resulting created link.\n * @example\n *  var peg1     = $.scene.getNodeByPath( \"Top/Peg1\" );\n *  var peg2     = $.scene.getNodeByPath( \"Top/Group/Peg2\" );\n *  var newLink  = peg1.addOutLink( peg2, 0, 0 );\n */\n$.oNode.prototype.addOutLink = function( nodeToLink, ownPort, destPort ){\n  if (typeof ownPort == 'undefined') var ownPort = 0;\n  if (typeof destPort == 'undefined') var destPort = 0;\n\n  var newLink = new this.$.oNodeLink( this, ownPort, nodeToLink, destPort );\n  newLink.apply();\n\n  return newLink;\n};\n\n/**\n * Creates a new dynamic attribute in the node.\n * @param   {string}   attrName                   The attribute name to create.\n * @param   {string}   [type=\"string\"]            The type of the attribute [\"string\", \"bool\", \"double\", \"int\"]\n * @param   {string}   [displayName=attrName]     The visible attribute name to the GUI user.\n * @param   {bool}     [linkable=false]           Whether the attribute can be linked to a column.\n *\n * @return  {$.oAttribute}     The resulting attribute created.\n */\n$.oNode.prototype.createAttribute = function( attrName, type, displayName, linkable ){\n  if( !attrName ){ return false; }\n  attrName = attrName.toLowerCase();\n\n  if (typeof type === 'undefined') type = 'string';\n  if (typeof displayName === 'undefined') displayName = attrName;\n  if (typeof linkable === 'undefined') linkable = false;\n\n  var res = node.createDynamicAttr( this.path, type.toUpperCase(), attrName, displayName, linkable );\n  if( !res ){\n    return false;\n  }\n\n  this.refreshAttributes();\n\n  var res_split = attrName.split(\".\");\n  if( res_split.length>0 ){\n    //Its a sub attribute created.\n    try{\n      var sub_attr = this.attributes[ res_split[0] ];\n      for( x = 1; x<res_split.length;x++ ){\n        sub_attr = sub_attr[ res_split[x] ];\n      }\n      return sub_attr;\n\n    }catch( err ){\n      return false;\n    }\n  }\n\n  var res = this.attributes[ attrName ];\n  return this.attributes[ attrName ];\n}\n\n\n/**\n * Removes an existing dynamic attribute in the node.\n * @param   {string}   attrName                   The attribute name to remove.\n *\n * @return  {bool}     The result of the removal.\n */\n$.oNode.prototype.removeAttribute = function( attrName ){\n  attrName = attrName.toLowerCase();\n  return node.removeDynamicAttr( this.path, attrName );\n}\n\n\n/**\n * Refreshes/rebuilds the attributes and getter/setters.\n * @param   {$.oNode}   oNodeObject            The node to link this one's inport to.\n * @return  {bool}    The result of the unlink.\n */\n$.oNode.prototype.refreshAttributes = function( ){\n    // generate properties from node attributes to allow for dot notation access\n    this.attributesBuildCache();\n\n    // for each attribute, create a getter setter as a property of the node object\n    // that handles the animated/not animated duality\n    var _attributes = this.attributes\n    for (var i in _attributes){\n      var _attr = _attributes[i];\n      this.setAttrGetterSetter(_attr);\n    }\n}\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oPegNode class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * Constructor for the $.oPegNode class\n * @classdesc\n * $.oPegNode is a subclass of $.oNode and implements the same methods and properties as $.oNode. <br>\n * It represents peg nodes in the scene.\n * @constructor\n * @augments   $.oNode\n * @classdesc  Peg Module Class\n * @param   {string}         path                          Path to the node in the network.\n * @param   {oScene}         oSceneObject                  Access to the oScene object of the DOM.\n */\n$.oPegNode = function( path, oSceneObject ) {\n    if (node.type(path) != 'PEG') throw \"'path' parameter must point to a 'PEG' type node\";\n    var instance = this.$.oNode.call( this, path, oSceneObject );\n    if (instance) return instance;\n\n    this._type = 'pegNode';\n}\n$.oPegNode.prototype = Object.create( $.oNode.prototype );\n$.oPegNode.prototype.constructor = $.oPegNode;\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oDrawingNode class      //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * Constructor for the $.oDrawingNode class\n * @classdesc\n * $.oDrawingNode is a subclass of $.oNode and implements the same methods and properties as $.oNode. <br>\n * It represents 'read' nodes or Drawing nodes in the scene.\n * @constructor\n * @augments   $.oNode\n * @param   {string}           path                          Path to the node in the network.\n * @param   {$.oScene}         oSceneObject                  Access to the oScene object of the DOM.\n * @example\n * // Drawing Nodes are more than a node, as they do not work without an associated Drawing column and element.\n * // adding a drawing node will automatically create a column and an element, unless they are provided as arguments.\n * // Creating an element makes importing a drawing file possible.\n *\n * var doc = $.scn;\n *\n * var drawingName = \"myDrawing\";\n * var myElement = doc.addElement(drawingName, \"TVG\");                      // add an element that holds TVG(Toonboom Vector Drawing) files\n * var myDrawingColumn = doc.addColumn(\"DRAWING\", drawingName, myElement);  // create a column and link the element created to it\n *\n * var sceneRoot = doc.root;                                                // grab the scene root group\n *\n * // Creating the Drawing node and linking the previously created element and column\n * var myDrawingNode = sceneRoot.addDrawingNode(drawingName, new $.oPoint(), myDrawingColumn, myElement);\n *\n * // This also works:\n *\n * var myOtherNode = sceneRoot.addDrawingNode(\"Drawing2\");\n */\n$.oDrawingNode = function(path, oSceneObject) {\n    // $.oDrawingNode can only represent a node of type 'READ'\n    if (node.type(path) != 'READ') throw \"'path' parameter must point to a 'READ' type node\";\n    var instance = this.$.oNode.call(this, path, oSceneObject);\n    if (instance) return instance;\n\n    this._type = 'drawingNode';\n}\n$.oDrawingNode.prototype = Object.create($.oNode.prototype);\n$.oDrawingNode.prototype.constructor = $.oDrawingNode;\n\n\n/**\n * The element that holds the drawings displayed by the node.\n * @name $.oDrawingNode#element\n * @type {$.oElement}\n */\nObject.defineProperty($.oDrawingNode.prototype, \"element\", {\n  get : function(){\n    var _column = this.attributes.drawing.element.column;\n    return ( new this.$.oElement( node.getElementId(this.path), _column ) );\n  },\n\n  set : function( oElementObject ){\n    var _column = this.attributes.drawing.element.column;\n    column.setElementIdOfDrawing( _column.uniqueName, oElementObject.id );\n  }\n});\n\n\n/**\n * The column that holds the drawings displayed by the node.\n * @name $.oDrawingNode.timingColumn\n * @type {$.oDrawingColumn}\n */\nObject.defineProperty($.oDrawingNode.prototype, \"timingColumn\", {\n  get : function(){\n    var _column = this.attributes.drawing.element.column;\n    return _column;\n  },\n\n  set : function (oColumnObject){\n    var _attribute = this.attributes.drawing.element;\n    _attribute.column = oColumnObject;\n  }\n});\n\n\n/**\n * An array of the colorIds contained within the drawings displayed by the node.\n * @name $.oDrawingNode#usedColorIds\n * @type {int[]}\n */\nObject.defineProperty($.oDrawingNode.prototype, \"usedColorIds\", {\n  get : function(){\n    // this.$.log(\"used colors in node : \"+this.name)\n    var _drawings = this.element.drawings;\n    var _colors = [];\n\n    for (var i in _drawings){\n      var _drawingColors = _drawings[i].usedColorIds;\n      for (var c in _drawingColors){\n        if (_colors.indexOf(_drawingColors[c]) == -1) _colors.push(_drawingColors[c]);\n      }\n    }\n\n    return _colors;\n  }\n});\n\n\n/**\n * An array of the colors contained within the drawings displayed by the node, found in the palettes.\n * @name $.oDrawingNode#usedColors\n * @type {$.oColor[]}\n */\nObject.defineProperty($.oDrawingNode.prototype, \"usedColors\", {\n  get : function(){\n    // get unique Color Ids\n    var _ids = this.usedColorIds;\n\n    // look in both element and scene palettes\n    var _palettes = this.palettes.concat(this.$.scn.palettes);\n\n    // build a palette/id list to speedup massive palettes/palette lists\n    var _colorIds = {}\n    for (var i in _palettes){\n      var _palette = _palettes[i];\n      var _colors = _palette.colors;\n      _colorIds[_palette.name] = {};\n      for (var j in _colors){\n        _colorIds[_palette.name][_colors[j].id] = _colors[j];\n      }\n    }\n\n    // for each id on the drawing, identify the corresponding color\n    var _usedColors = _ids.map(function(id){\n      for (var paletteName in _colorIds){\n        if (_colorIds[paletteName][id]) return _colorIds[paletteName][id];\n      }\n      throw new Error(\"Missing color found for id: \"+id+\". Color doesn't belong to any palette in the scene or element.\");\n    })\n\n    return _usedColors;\n  }\n})\n\n\n/**\n * The drawing.element keyframes.\n * @name $.oDrawingNode#timings\n * @type {$.oFrames[]}\n * @example\n * // The timings hold the keyframes that display the drawings across time.\n *\n * var timings = $.scn.$node(\"Top/Drawing\").timings;\n * for (var i in timings){\n *   $.log( timings.frameNumber+\" : \"+timings.value);      // outputs the frame and the value of each keyframe\n * }\n *\n * // timings are keyframe objects, so they are dynamic.\n * timings[2].value = \"5\";                                 // sets the displayed image of the second key to the drawing named \"5\"\n *\n * // to set a new value to a frame that wasn't a keyframe before, it's possible to use the attribute keyword like so:\n *\n * var myNode = $.scn.$node(\"Top/Drawing\");\n * myNode.drawing.element = {frameNumber: 5, value: \"10\"}             // setting the value of the frame 5\n * myNode.drawing.element = {frameNumber: 6, value: timings[1].value} // setting the value to the same as one of the timings\n */\nObject.defineProperty($.oDrawingNode.prototype, \"timings\", {\n    get : function(){\n        return this.attributes.drawing.element.getKeyframes();\n    }\n})\n\n\n/**\n * The element palettes linked to the node.\n * @name $.oDrawingNode#palettes\n * @type {$.oPalette[]}\n */\nObject.defineProperty($.oDrawingNode.prototype, \"palettes\", {\n  get : function(){\n    var _element = this.element;\n    return _element.palettes;\n  }\n})\n\n\n// Class Methods\n\n/**\n * Gets the drawing name at the given frame.\n * @param {int} frameNumber\n * @return {$.oDrawing}\n */\n$.oDrawingNode.prototype.getDrawingAtFrame = function(frameNumber){\n  if (typeof frame === \"undefined\") var frame = this.$.scene.currentFrame;\n\n  var _attribute = this.attributes.drawing.element\n  return _attribute.getValue(frameNumber);\n}\n\n\n /**\n * Gets the list of palettes containing colors used by a drawing node. This only gets palettes with the first occurrence of the colors.\n * @return  {$.oPalette[]}   The palettes that contain the color IDs used by the drawings of the node.\n */\n$.oDrawingNode.prototype.getUsedPalettes = function(){\n  var _palettes = {};\n  var _usedPalettes = [];\n\n  var _usedColors = this.usedColors;\n  // build an object of palettes under ids as keys to remove duplicates\n  for (var i in _usedColors){\n    var _palette = _usedColors[i].palette;\n    _palettes[_palette.id] = _palette;\n  }\n  for (var i in _palettes){\n    _usedPalettes.push(_palettes[i]);\n  }\n\n  return _usedPalettes;\n}\n\n\n/**\n * Displays all the drawings from the node's element onto the timeline\n * @param {int} [framesPerDrawing=1]   The number of frames each drawing will be shown for\n */\n$.oDrawingNode.prototype.exposeAllDrawings = function(framesPerDrawing){\n  if (typeof framesPerDrawing === 'undefined') var framesPerDrawing = 1;\n\n  var _drawings = this.element.drawings;\n  var frameNumber = 1;\n  for (var i=0; i < _drawings.length; i++){\n    //log(\"showing drawing \"+_drawings[i].name+\" at frame \"+i)\n    this.showDrawingAtFrame(_drawings[i], frameNumber);\n    frameNumber+=framesPerDrawing;\n  }\n\n  var _column = this.attributes.drawing.element.column;\n  var _exposures = _column.getKeyframes();\n  _column.extendExposures(_exposures, framesPerDrawing-1);\n}\n\n\n/**\n * Displays the given drawing at the given frame\n * @param {$.oDrawing} drawing\n * @param {int} frameNum\n */\n$.oDrawingNode.prototype.showDrawingAtFrame = function(drawing, frameNum){\n  var _column = this.attributes.drawing.element.column;\n  _column.setValue(drawing.name, frameNum);\n}\n\n\n /**\n * Links a palette to a drawing node as Element Palette.\n * @param {$.oPalette}     oPaletteObject      the palette to link to the node\n * @param {int}            [index]             The index of the list at which the palette should appear once linked\n *\n * @return  {$.oPalette}   The linked element Palette.\n */\n$.oDrawingNode.prototype.linkPalette = function(oPaletteObject, index){\n  return this.element.linkPalette(oPaletteObject, index);\n}\n\n\n /**\n * Unlinks an Element Palette from a drawing node.\n * @param {$.oPalette}     oPaletteObject      the palette to unlink from the node\n *\n * @return {bool}          The success of the unlink operation.\n */\n$.oDrawingNode.prototype.unlinkPalette = function(oPaletteObject){\n  return this.element.unlinkPalette(oPaletteObject);\n}\n\n\n\n\n /**\n * Duplicates a node by creating an independent copy.\n * @param   {string}    [newName]              The new name for the duplicated node.\n * @param   {oPoint}    [newPosition]          The new position for the duplicated node.\n * @param   {bool}      [duplicateElement]     Whether to also duplicate the element.\n */\n$.oDrawingNode.prototype.duplicate = function(newName, newPosition, duplicateElement){\n  if (typeof newPosition === 'undefined') var newPosition = this.nodePosition;\n  if (typeof newName === 'undefined') var newName = this.name+\"_1\";\n  if (typeof duplicateElement === 'undefined') var duplicateElement = true;\n\n  var _duplicateElement = duplicateElement?this.element.duplicate(this.name):this.element;\n\n  var _duplicateNode = this.group.addDrawingNode(newName, newPosition, _duplicateElement);\n  var _attributes = this.attributes;\n\n  for (var i in _attributes){\n    var _duplicateAttribute = _duplicateNode.getAttributeByName(_attributes[i].keyword);\n    _duplicateAttribute.setToAttributeValue(_attributes[i], true);\n  }\n\n  var _duplicateAttribute = _duplicateNode.getAttributeByName(_attributes[i].keyword);\n  _duplicateAttribute.setToAttributeValue(_attributes[i], true);\n\n  return _duplicateNode;\n};\n\n\n /**\n * Updates the imported drawings in the node.\n * @param {$.oFile}   sourcePath        the oFile object pointing to the source to update from\n * @param {string}    [drawingName]       the drawing to import the updated bitmap into\n * @todo implement a memory of the source through metadata\n */\n$.oDrawingNode.prototype.update = function(sourcePath, drawingName){\n  if (!this.element) return; // no element means nothing to update, import instead.\n  if (typeof drawingName === 'undefined') var drawingName = this.element.drawings[0].name;\n\n  var _drawing = this.element.getDrawingByName(drawingName);\n\n  _drawing.importBitmap(sourcePath);\n  _drawing.refreshPreview();\n}\n\n\n /**\n * Extracts the position information on a drawing node, and applies it to a new peg instead.\n * @return  {$.oPegNode}   The created peg.\n */\n$.oDrawingNode.prototype.extractPeg = function(){\n    var _drawingNode = this;\n    var _peg = this.group.addNode(\"PEG\", this.name+\"-P\");\n    var _columns = _drawingNode.linkedColumns;\n\n    _peg.position.separate = _drawingNode.offset.separate;\n    _peg.scale.separate = _drawingNode.scale.separate;\n\n    // link each column that can be to the peg instead and reset the drawing node\n    for (var i in _columns){\n        var _attribute = _columns[i].attributeObject;\n        var _keyword = _attribute._keyword;\n\n        var _nodeAttribute = _drawingNode.getAttributeByName(_keyword);\n\n        if (_keyword.indexOf(\"OFFSET\") != -1) _keyword = _keyword.replace(\"OFFSET\", \"POSITION\");\n\n        var _pegAttribute = _peg.getAttributeByName(_keyword);\n\n        if (_pegAttribute !== null){\n            _pegAttribute.column = _columns[i];\n            _nodeAttribute.column = null;\n            _drawingNode[_keyword] = _attribute.defaultValue;\n        }\n    }\n\n    _drawingNode.offset.separate = false; // doesn't work?\n    _drawingNode.can_animate = false;\n\n    _peg.centerAbove(_drawingNode, -1, -30)\n    _drawingNode.linkInNode(_peg)\n\n    return _peg;\n}\n\n\n /**\n * Gets the contour curves of the drawing, as a concave hull.\n * @param   {int}          [count]                          The number of points on the contour curve to derive.\n * @param   {int}          [frame]                          The frame to derive the contours.\n *\n * @return  {oPoint[][]}   The contour curves.\n */\n$.oDrawingNode.prototype.getContourCurves = function( count, frame ){\n\n  if (typeof frame === 'undefined') var frame = this.scene.currentFrame;\n  if (typeof count === 'undefined') var count = 3;\n\n  var res = EnvelopeCreator().getDrawingBezierPath( this.path,\n                           frame,      //FRAME\n                           2.5,        //DISCRETIZER\n                           0,          //K\n                           count,      //DESIRED POINT COUNT\n                           0,          //BLUR\n                           0,          //EXPAND\n                           false,      //SINGLELINE\n                           true,       //USE MIN POINTS,\n                           0,          //ADDITIONAL BISSECTING\n\n                           false\n                        );\n  if( res.success ){\n    var _curves = res.results.map(function(x){return [\n                                                      new this.$.oPoint( x[0][0], x[0][1], 0.0 ),\n                                                      new this.$.oPoint( x[1][0], x[1][1], 0.0 ),\n                                                      new this.$.oPoint( x[2][0], x[2][1], 0.0 ),\n                                                      new this.$.oPoint( x[3][0], x[3][1], 0.0 )\n                                                    ]; } );\n    return _curves;\n  }\n\n  return [];\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//   $.oTransformSwitchNode class   //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * Constructor for the $.oTransformSwitchNode class\n * @classdesc\n * $.oTransformSwitchNode is a subclass of $.oNode and implements the same methods and properties as $.oNode. <br>\n * It represents transform switches in the scene.\n * @constructor\n * @augments   $.oNode\n * @param   {string}         path            Path to the node in the network.\n * @param   {oScene}         oSceneObject    Access to the oScene object of the DOM.\n * @property {$.oTransformNamesObject} names An array-like object with static indices (starting at 0) for each transformation name, which can be retrieved/set directly.\n * @example\n * // Assuming the existence of a Deformation group applied to a 'Drawing' node at the root of the scene\n * var myNode = $.scn.getNodeByPath(\"Top/Deformation-Drawing/Transformation-Switch\");\n *\n * myNode.names[0] = \"B\";                              // setting the value for the first transform drawing name to \"B\"\n *\n * var drawingNames = [\"A\", \"B\", \"C\"]                  // example of iterating over the existing names to set/retrieve them\n * for (var i in myNode.names){\n *   $.log(i+\": \"+myNode.names[i]);\n *   $.log(myNode.names[i] = drawingNames[i]);\n * }\n *\n * $.log(\"length: \" + myNode.names.length)             // the number of names\n * $.log(\"names: \" + myNode.names)                     // prints the list of names\n * $.log(\"indexOf 'B': \" + myNode.names.indexOf(\"B\"))  // can use methods from Array\n */\n$.oTransformSwitchNode = function( path, oSceneObject ) {\n  if (node.type(path) != 'TransformationSwitch') throw \"'path' parameter (\"+path+\") must point to a 'TransformationSwitch' type node. Got: \"+node.type(path);\n  var instance = this.$.oNode.call( this, path, oSceneObject );\n  if (instance) return instance;\n\n  this._type = 'transformSwitchNode';\n  this.names = new this.$.oTransformNamesObject(this);\n}\n$.oTransformSwitchNode.prototype = Object.create( $.oNode.prototype );\n$.oTransformSwitchNode.prototype.constructor = $.oTransformSwitchNode;\n\n\n/**\n * Constructor for the $.oTransformNamesObject class\n * @classdesc\n * $.oTransformNamesObject is an array like object with static length that exposes getter setters for\n * each transformation name used by the oTransformSwitchNode. It can use the same methods as any array.\n * @constructor\n * @param {$.oTransformSwitchNode} instance the transform Node instance using this object\n * @property {int} length the number of valid elements in the object.\n */\n$.oTransformNamesObject = function(transformSwitchNode){\n  Object.defineProperty(this, \"transformSwitchNode\", {\n    enumerable:false,\n    get: function(){\n      return transformSwitchNode;\n    },\n  })\n\n  this.refresh();\n}\n$.oTransformNamesObject.prototype = Object.create(Array.prototype);\n\n\n/**\n * creates a $.oTransformSwitch.names property with an index for each name to get/set the name value\n * @private\n */\nObject.defineProperty($.oTransformNamesObject.prototype, \"createGetterSetter\", {\n  enumerable:false,\n  value: function(index){\n    var attrName = \"transformation_\" + (index+1);\n    var transformNode = this.transformSwitchNode;\n\n    Object.defineProperty(this, index, {\n      enumerable:true,\n      configurable:true,\n      get: function(){\n        return transformNode.transformationnames[attrName];\n      },\n      set: function(newName){\n        newName = newName+\"\"; // convert to string\n        this.$.debug(\"setting \"+attrName+\" to drawing \"+newName+\" on \"+transformNode.path, this.$.DEBUG_LEVEL.DEBUG)\n        if (newName instanceof this.$.oDrawing) newName = newName.name;\n        transformNode.transformationnames[attrName] = newName;\n      }\n    })\n  }\n})\n\n\n/**\n * The length of the array of names on the oTransformSwitchNode node. Corresponds to the transformationnames.size subAttribute.\n * @name $.oTransformNamesObject#length\n * @type {int}\n */\n Object.defineProperty($.oTransformNamesObject.prototype, \"length\", {\n  enumerable:false,\n  get: function(){\n    return this.transformSwitchNode.transformationnames.size;\n  },\n})\n\n\n/**\n * A string representation of the names list\n * @private\n */\nObject.defineProperty($.oTransformNamesObject.prototype, \"toString\", {\n  enumerable:false,\n  value: function(){\n    return this.join(\",\");\n  }\n})\n\n\n/**\n * @private\n */\nObject.defineProperty($.oTransformNamesObject.prototype, \"refresh\", {\n  enumerable:false,\n  value:function(){\n    for (var i in this){\n      delete this[i];\n    }\n    for (var i=0; i<this.length; i++){\n      this.createGetterSetter(i);\n    }\n  }\n})\n\n\n/**\n * @private\n */\n$.oTransformSwitchNode.prototype.refreshNames = function(){\n  this.refreshAttributes();\n  this.names.refresh();\n}\n\n\n/**\n * Links this node's inport to the given module, at the inport and outport indices.\n * Refreshes attributes to update for the changes of connected transformation.\n * @param   {$.oNode}   nodeToLink             The node to link this one's inport to.\n * @param   {int}       [ownPort]              This node's inport to connect.\n * @param   {int}       [destPort]             The target node's outport to connect.\n * @param   {bool}      [createPorts]          Whether to create new ports on the nodes.\n *\n * @return  {bool}    The result of the link, if successful.\n */\n$.oTransformSwitchNode.prototype.linkInNode = function(nodeToLink, ownPort, destPort, createPorts){\n  this.$.oNode.prototype.linkInNode.apply(this, arguments);\n  this.refreshNames()\n}\n\n/**\n * Searches for and unlinks the $.oNode object from this node's inNodes.\n * @param   {$.oNode}   oNodeObject            The node to link this one's inport to.\n * @return  {bool}    The result of the unlink. Refreshes attributes to update for the changes of connected transformation.\n */\n$.oTransformSwitchNode.prototype.unlinkInNode = function( oNodeObject ){\n  this.$.oNode.prototype.unlinkInNode.apply(this, arguments);\n  this.refreshNames()\n}\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//    $.oColorOverrideNode class    //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * Constructor for the $.oColorOverrideNode class\n * @classdesc\n * $.oColorOverrideNode is a subclass of $.oNode and implements the same methods and properties as $.oNode. <br>\n * It represents color overrides in the scene.\n * @constructor\n * @augments   $.oNode\n * @param   {string}         path                          Path to the node in the network.\n * @param   {oScene}         oSceneObject                  Access to the oScene object of the DOM.\n */\n $.oColorOverrideNode = function(path, oSceneObject) {\n  // $.oDrawingNode can only represent a node of type 'READ'\n  if (node.type(path) != 'COLOR_OVERRIDE_TVG') throw \"'path' parameter must point to a 'COLOR_OVERRIDE_TVG' type node\";\n  var instance = this.$.oNode.call(this, path, oSceneObject);\n  if (instance) return instance;\n\n  this._type = 'colorOverrideNode';\n  this._coObject = node.getColorOverride(path)\n}\n$.oColorOverrideNode.prototype = Object.create($.oNode.prototype);\n$.oColorOverrideNode.prototype.constructor = $.oColorOverrideNode;\n\n\n/**\n * The list of palette overrides in this color override node\n * @name $.oColorOverrideNode#palettes\n * @type {$.oPalette[]}\n * @readonly\n */\nObject.defineProperty($.oColorOverrideNode.prototype, \"palettes\", {\n  get: function(){\n    this.$.debug(\"getting palettes\", this.$.DEBUG_LEVEL.LOG)\n    if (!this._palettes){\n      this._palettes = [];\n\n      var _numPalettes = this._coObject.getNumPalettes();\n      for (var i=0; i<_numPalettes; i++){\n        var _palettePath = this._coObject.palettePath(i) + \".plt\";\n        var _palette = this.$.scn.getPaletteByPath(_palettePath);\n        if (_palette) this._palettes.push(_palette);\n      }\n    }\n\n    return this._palettes;\n  }\n})\n\n/**\n * Add a new palette to the palette list (for now, only supports scene palettes)\n * @param {$.oPalette} palette\n*/\n$.oColorOverrideNode.prototype.addPalette = function(palette){\n  var _palettes = this.palettes // init palettes cache to add to it\n\n  this._coObject.addPalette(palette.path.path);\n  this._palettes.push(palette);\n}\n\n/**\n * Removes a palette to the palette list (for now, only supports scene palettes)\n * @param {$.oPalette} palette\n */\n$.oColorOverrideNode.prototype.removePalette = function(palette){\n  this._coObject.removePalette(palette.path.path);\n}\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oGroupNode class       //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * Constructor for the $.oGroupNode class\n * @classdesc\n * $.oGroupNode is a subclass of $.oNode and implements the same methods and properties as $.oNode. <br>\n * It represents groups in the scene. From this class, it's possible to add nodes, and backdrops, import files and templates into the group.\n * @constructor\n * @augments   $.oNode\n * @param   {string}         path                          Path to the node in the network.\n * @param   {oScene}         oSceneObject                  Access to the oScene object of the DOM.\n * @example\n * // to add a new node, grab the group it'll be created in first\n * var doc = $.scn\n * var sceneRoot = doc.root;                                              // grab the scene root group\n *\n * var myGroup = sceneRoot.addGrop(\"myGroup\", false, false);              // create a group in the scene root, with a peg and composite but no nodes\n * var MPO = myGroup.multiportOut;                                        // grab the multiport in of the group\n *\n * var myNode = myGroup.addDrawingNode(\"myDrawingNode\");                  // add a drawing node inside the group\n * myNode.linkOutNode(MPO);                                               // link the newly created node to the multiport\n * myNode.centerAbove(MPO);\n *\n * var sceneComposite = doc.$node(\"Top/Composite\");                       // grab the scene composite node\n * myGroup.linkOutNode(sceneComposite);                                   // link the group to it\n *\n * myGroup.centerAbove(sceneComposite);\n */\n$.oGroupNode = function(path, oSceneObject) {\n    // $.oDrawingNode can only represent a node of type 'READ'\n    if (node.type(path) != 'GROUP') throw \"'path' parameter must point to a 'GROUP' type node\";\n    var instance = this.$.oNode.call(this, path, oSceneObject);\n    if (instance) return instance;\n\n    this._type = 'groupNode';\n}\n$.oGroupNode.prototype = Object.create($.oNode.prototype);\n$.oGroupNode.prototype.constructor = $.oGroupNode;\n\n/**\n * The multiport in node of the group. If one doesn't exist, it will be created.\n * @name $.oGroupNode#multiportIn\n * @readonly\n * @type {$.oNode}\n */\nObject.defineProperty($.oGroupNode.prototype, \"multiportIn\", {\n    get : function(){\n        if (this.isRoot) return null\n        var _MPI = this.scene.getNodeByPath(node.getGroupInputModule(this.path, \"Multiport-In\", 0,-100,0),this.scene)\n        return (_MPI)\n    }\n})\n\n\n/**\n * The multiport out node of the group. If one doesn't exist, it will be created.\n * @name $.oGroupNode#multiportOut\n * @readonly\n * @type {$.oNode}\n */\nObject.defineProperty($.oGroupNode.prototype, \"multiportOut\", {\n    get : function(){\n        if (this.isRoot) return null\n        var _MPO = this.scene.getNodeByPath(node.getGroupOutputModule(this.path, \"Multiport-Out\", 0, 100,0),this.scene)\n        return (_MPO)\n    }\n});\n\n /**\n * All the nodes contained within the group, one level deep.\n * @name $.oGroupNode#nodes\n * @readonly\n * @type {$.oNode[]}\n */\nObject.defineProperty($.oGroupNode.prototype, \"nodes\", {\n  get : function() {\n    var _path = this.path;\n    var _nodes = node.subNodes(_path);\n\n    var self = this;\n    return _nodes.map(function(x){return self.scene.getNodeByPath(x)});\n  }\n});\n\n\n\n /**\n * All the backdrops contained within the group.\n * @name $.oGroupNode#backdrops\n * @readonly\n * @type {$.oBackdrop[]}\n */\nObject.defineProperty($.oGroupNode.prototype, \"backdrops\", {\n  get : function() {\n    var _path = this.path;\n    var _backdropObjects = Backdrop.backdrops(this.path);\n    var _backdrops = _backdropObjects.map(function(x){return new this.$.oBackdrop(_path, x)});\n\n    return _backdrops;\n  }\n});\n\n\n /**\n * Returns a node from within a group based on its name.\n * @param   {string}      name           The name of the node.\n *\n * @return  {$.oNode}     The node, or null if can't be found.\n */\n$.oGroupNode.prototype.getNodeByName = function(name){\n  var _path = this.path+\"/\"+name;\n\n  return this.scene.getNodeByPath(_path);\n}\n\n\n /**\n * Returns all the nodes of a certain type in the group.\n * Pass a value to recurse to look into the groups as well.\n * @param   {string}        typeName      The type of the nodes.\n * @param   {bool}          recurse       Whether to look inside the groups.\n *\n * @return  {$.oNode[]}     The nodes found.\n */\n$.oGroupNode.prototype.getNodesByType = function(typeName, recurse){\n  if (typeof recurse === 'undefined') var recurse = false;\n  return this.subNodes(recurse).filter(function(x){return x.type == typeName});\n}\n\n\n /**\n * Returns a child node in a group based on a search.\n * @param   {string}      name           The name of the node.\n *\n * @return  {$.oNode}     The node, or null if can't be found.\n */\n$.oGroupNode.prototype.$node = function(name){\n  return this.getNodeByName(name);\n}\n\n\n /**\n * Gets all the nodes contained within the group.\n * @param   {bool}    [recurse=false]             Whether to recurse the groups within the groups.\n *\n * @return  {$.oNode[]}   The nodes in the group\n */\n$.oGroupNode.prototype.subNodes = function(recurse){\n    if (typeof recurse === 'undefined') recurse = false;\n\n    var _nodes = node.subNodes(this.path);\n    var _subNodes = [];\n\n    for (var i in _nodes){\n        var _oNodeObject = this.scene.getNodeByPath(_nodes[i]);\n        _subNodes.push(_oNodeObject);\n        if (recurse && node.isGroup(_nodes[i])) _subNodes = _subNodes.concat(_oNodeObject.subNodes(recurse));\n    }\n\n    return _subNodes;\n}\n\n\n /**\n * Gets all children of the group.\n * @param   {bool}    [recurse=false]             Whether to recurse the groups within the groups.\n *\n * @return  {$.oNode[]}   The nodes in the group\n */\n$.oGroupNode.prototype.children = function(recurse){\n  return this.subNodes(recurse);\n}\n\n\n\n /**\n * Creates an in-port on top of a group\n * @param   {int}         portNum            The port number where a port will be added\n * @type    {string}\n *\n * @return  {int}   The number of the created port in case the port specified was not correct (for example larger than the current number of ports + 1)\n */\n$.oGroupNode.prototype.addInPort = function(portNum, type){\n  var _inPorts = this.inPorts;\n\n  if (typeof portNum === 'undefined') var portNum = _inPorts;\n  if (portNum > _inPorts) portNum = _inPorts;\n\n  var _type = (type==\"transform\")?\"READ\":\"none\"\n  var _dummyNode = this.addNode(_type, \"dummy_add_port_node\");\n  var _MPI = this.multiportIn;\n  _dummyNode.linkInNode(_MPI, 0, portNum, true);\n  _dummyNode.unlinkInNode(_MPI);\n  _dummyNode.remove();\n\n  return portNum;\n}\n\n\n /**\n * Creates an out-port at the bottom of a group. For some reason groups can have many unconnected in-ports but only one unconnected out-port.\n * @param   {int}         [portNum]            The port number where a port will be added\n * @type    {string}\n *\n * @return  {int}   The number of the created port in case the port specified was not correct (for example larger than the current number of ports + 1)\n */\n$.oGroupNode.prototype.addOutPort = function(portNum, type){\n  var _outPorts = this.outPorts;\n\n  if (typeof portNum === 'undefined') var portNum = _outPorts;\n  if (portNum > _outPorts) portNum = _outPorts;\n\n  var _type = (type==\"transform\")?\"PEG\":\"none\"\n  var _dummyNode = this.addNode(_type, \"dummy_add_port_node\");\n  var _MPO = this.multiportOut;\n\n  _dummyNode.linkOutNode(_MPO, 0, portNum, true);\n  _dummyNode.unlinkOutNode(_MPO);\n  _dummyNode.remove();\n\n  return portNum;\n}\n\n /**\n * Gets all children of the group.\n * @param   {bool}    [recurse=false]             Whether to recurse the groups within the groups.\n *\n * @return  {$.oNode[]}   The nodes in the group\n */\n$.oGroupNode.prototype.children = function(recurse){\n  return this.subNodes(recurse);\n}\n\n\n\n /**\n * Sorts out the node view inside the group\n * @param   {bool}    [recurse=false]             Whether to recurse the groups within the groups.\n */\n$.oGroupNode.prototype.orderNodeView = function(recurse){\n    if (typeof recurse === 'undefined') var recurse = false;\n\n    TB_orderNetworkUpBatchFromList( node.subNodes(this.path) );\n\n    if (!this.isRoot){\n        var _MPO = this.multiportOut;\n        var _MPI = this.multiportIn;\n\n        _MPI.x = _MPO.x\n    }\n\n    if (recurse){\n        var _subNodes = this.subNodes().filter(function(x){return x.type == \"GROUP\"});\n        for (var i in _subNodes){\n            _subNodes[i].orderNodeView(recurse);\n        }\n    }\n}\n\n\n/**\n * Adds a node to the group.\n * @param   {string}        type                   The type-name of the node to add.\n * @param   {string}        [name=type]            The name of the newly created node.\n * @param   {$.oPoint}      [nodePosition={0,0,0}] The position for the node to be placed in the network.\n *\n * @return {$.oNode}   The created node, or bool as false.\n * @example\n * // to add a node, simply call addNode on the group you want the node to be added to.\n * var sceneRoot = $.scn.root; // grab the scene root group (\"Top\")\n *\n * var peg = sceneRoot.addNode(\"PEG\", \"MyNewlyCreatedPeg\");           // adding a peg\n *\n * // Now we'll also create a drawing node to connect under the peg\n * var sceneComposite = $.scn.getNodeByPath(\"Top/Composite\");         // can also use $.scn.$node(\"Top/Composite\") for shorter synthax\n *\n * var drawingNode = sceneRoot.addDrawingNode(\"myNewDrawingNode\");\n * drawingNode.linkOutNode(sceneComposite);\n * drawingNode.can_animate = false                // setting some attributes on the newly created Node\n *\n * peg.linkOutNode(drawingNode);\n *\n * //through all this we didn't specify nodePosition parameters so we'll sort everything at once\n *\n * sceneRoot.orderNodeView();\n *\n * // we can also do:\n *\n * peg.centerAbove(drawingNode);\n *\n */\n$.oGroupNode.prototype.addNode = function( type, name, nodePosition ){\n  // Defaults for optional parameters\n  if (typeof nodePosition === 'undefined') var nodePosition = new this.$.oPoint(0,0,0);\n  if (typeof name === 'undefined') var name = type[0]+type.slice(1).toLowerCase();\n  if (typeof name !== 'string') name = name+\"\";\n\n  var _group = this.path;\n\n  // create node and return result (this sanitizes/increments the name, so we only create the oNode with the returned value)\n  var _path = node.add(_group, name, type, nodePosition.x, nodePosition.y, nodePosition.z);\n  _node = this.scene.getNodeByPath(_path);\n\n  return _node;\n}\n\n\n/**\n * Adds a drawing layer to the group, with a drawing column and element linked. Possible to specify the column and element to use.\n * @param   {string}     name                     The name of the newly created node.\n * @param   {$.oPoint}   [nodePosition={0,0,0}]   The position for the node to be placed in the network.\n * @param   {$.object}   [element]                The element to attach to the column.\n * @param   {object}     [drawingColumn]          The column to attach to the drawing module.\n\n * @return {$.oNode}     The created node, or bool as false.\n */\n\n$.oGroupNode.prototype.addDrawingNode = function( name, nodePosition, oElementObject, drawingColumn){\n  // add drawing column and element if not passed as parameters\n  this.$.beginUndo(\"oH_addDrawingNode_\"+name);\n\n  // Defaults for optional parameters\n  if (typeof nodePosition === 'undefined') var nodePosition = new this.$.oPoint(0,0,0);\n  if (typeof name === 'undefined') var name = type[0]+type.slice(1).toLowerCase();\n\n  // creating the node first to get the \"safe name\" returned by harmony\n  var _node = this.addNode(\"READ\", name, nodePosition);\n\n  if (typeof oElementObject === 'undefined') var oElementObject = this.scene.addElement(_node.name);\n  if (typeof drawingColumn === 'undefined'){\n    // first look for a column in the element\n    if (!oElementObject.column) {\n      var drawingColumn = this.scene.addColumn(\"DRAWING\", _node.name, oElementObject);\n    }else{\n      var drawingColumn = oElementObject.column;\n    }\n  }\n\n  // setup the node\n  // setup animate mode/separate based on preferences?\n  _node.attributes.drawing.element.column = drawingColumn;\n\n  this.$.endUndo();\n\n  return _node;\n}\n\n\n/**\n * Adds a new group to the group, and optionally move the specified nodes into it.\n * @param   {string}     name                           The name of the newly created group.\n * @param   {$.oPoint}   [addComposite=false]           Whether to add a composite.\n * @param   {bool}       [addPeg=false]                 Whether to add a peg.\n * @param   {$.oNode[]}  [includeNodes]                 The nodes to add to the group.\n * @param   {$.oPoint}   [nodePosition={0,0,0}]         The position for the node to be placed in the network.\n\n * @return {$.oGroupNode}   The created node, or bool as false.\n */\n$.oGroupNode.prototype.addGroup = function( name, addComposite, addPeg, includeNodes, nodePosition ){\n    // Defaults for optional parameters\n    if (typeof addPeg === 'undefined') var addPeg = false;\n    if (typeof addComposite === 'undefined') var addComposite = false;\n    if (typeof includeNodes === 'undefined') var includeNodes = [];\n\n    this.$.beginUndo(\"oH_addGroup_\"+name);\n\n    var nodeBox = new this.$.oBox();\n    includeNodes = includeNodes.filter(function(x){return !!x}) // filter out all invalid types\n    if (includeNodes.length > 0) nodeBox.includeNodes(includeNodes);\n\n    if (typeof nodePosition === 'undefined') var nodePosition = includeNodes.length?nodeBox.center:new this.$.oPoint(0,0,0);\n\n    var _group = this.addNode( \"GROUP\", name, nodePosition );\n\n    var _MPI = _group.multiportIn;\n    var _MPO = _group.multiportOut;\n\n    if (addComposite){\n      var _composite = _group.addNode(\"COMPOSITE\", name+\"_Composite\");\n      _composite.composite_mode = \"Pass Through\"; // get preference?\n      _composite.linkOutNode(_MPO);\n      _composite.centerAbove(_MPO);\n    }\n\n    if (addPeg){\n      var _peg = _group.addNode(\"PEG\", name+\"-P\");\n      _peg.linkInNode(_MPI);\n      _peg.centerBelow(_MPI);\n    }\n\n    // moves nodes into the created group and recreates their hierarchy and links\n    if (includeNodes.length > 0){\n      includeNodes = includeNodes.sort(function(a, b){return a.timelineIndex()>=b.timelineIndex()?1:-1})\n\n      var _links = this.scene.getNodesLinks(includeNodes);\n\n      for (var i in includeNodes){\n        includeNodes[i].moveToGroup(_group);\n      }\n\n      for (var i in _links){\n        _links[i].connect();\n      }\n\n      // link all unconnected nodes to the peg/MPI and comp/MPO\n      var _topNode = _peg?_peg:_MPI;\n      var _bottomNode = _composite?_composite:_MPO;\n\n      for (var i in includeNodes){\n        for (var j=0; j < includeNodes[i].inPorts; j++){\n          if (includeNodes[i].getInLinksNumber(j) == 0) includeNodes[i].linkInNode(_topNode);\n        }\n\n        for (var j=0; j < includeNodes[i].outPorts; j++){\n          if (includeNodes[i].getOutLinksNumber(j) == 0) includeNodes[i].linkOutNode(_bottomNode,0,0);\n        }\n      }\n\n      //shifting MPI/MPO/peg/comp out of the way of included nodes\n      if (_peg){\n        _peg.centerAbove(includeNodes);\n        includeNodes.push(_peg);\n      }\n\n      if (_composite){\n        _composite.centerBelow(includeNodes);\n        includeNodes.push(_composite);\n      }\n\n      _MPI.centerAbove(includeNodes);\n      _MPO.centerBelow(includeNodes);\n    }\n\n    this.$.endUndo();\n    return _group;\n}\n\n\n/**\n * Imports the specified template into the scene.\n * @param   {string}           tplPath                                        The path of the TPL file to import.\n * @param   {$.oNode[]}        [destinationNodes=false]                       The nodes affected by the template.\n * @param   {bool}             [extendScene=true]                             Whether to extend the exposures of the content imported.\n * @param   {$.oPoint}         [nodePosition={0,0,0}]                         The position to offset imported new nodes.\n * @param   {object}           [pasteOptions]                                 An object containing paste options as per Harmony's standard paste options.\n *\n * @return {$.oNode[]}         The resulting pasted nodes.\n */\n$.oGroupNode.prototype.importTemplate = function( tplPath, destinationNodes, extendScene, nodePosition, pasteOptions ){\n  if (typeof nodePosition === 'undefined') var nodePosition = new oPoint(0,0,0);\n  if (typeof destinationNodes === 'undefined' || destinationNodes.length == 0) var destinationNodes = false;\n  if (typeof extendScene === 'undefined') var extendScene = true;\n\n  if (typeof pasteOptions === 'undefined') var pasteOptions = copyPaste.getCurrentPasteOptions();\n  pasteOptions.extendScene = extendScene;\n\n  this.$.beginUndo(\"oH_importTemplate\");\n\n  var _group = this.path;\n\n  if(tplPath instanceof this.$.oFolder) tplPath = tplPath.path;\n\n  this.$.debug(\"importing template : \"+tplPath, this.$.DEBUG_LEVEL.LOG);\n\n  var _copyOptions = copyPaste.getCurrentCreateOptions();\n  var _tpl = copyPaste.copyFromTemplate(tplPath, 0, 999, _copyOptions); // any way to get the length of a template before importing it?\n\n  if (destinationNodes){\n    // TODO: deal with import options to specify frames\n    copyPaste.paste(_tpl, destinationNodes.map(function(x){return x.path}), 0, 999, pasteOptions);\n    var _nodes = destinationNodes;\n  }else{\n    var oldBackdrops = this.backdrops;\n    copyPaste.pasteNewNodes(_tpl, _group, pasteOptions);\n    var _scene = this.scene;\n    var _nodes = selection.selectedNodes().map(function(x){return _scene.$node(x)});\n    for (var i in _nodes){\n      // only move the root nodes\n      if (_nodes[i].parent.path != this.path) continue\n\n      _nodes[i].x += nodePosition.x;\n      _nodes[i].y += nodePosition.y;\n    }\n\n    // move backdrops present in the template\n    var backdrops = this.backdrops.slice(oldBackdrops.length);\n    for (var i in backdrops){\n      backdrops[i].x += nodePosition.x;\n      backdrops[i].y += nodePosition.y;\n    }\n    \n    // move waypoints in the top level of the template\n    for (var i in _nodes) {\n      var nodePorts = _nodes[i].outPorts;\n      for (var p = 0; p < nodePorts; p++) {\n        var theseWP = waypoint.childWaypoints(_nodes[i], p);\n        if (theseWP.length > 0) {\n          for (var w in theseWP) {\n            var x = waypoint.coordX(theseWP[w]);\n            var y = waypoint.coordY(theseWP[w]);\n            x += nodePosition.x;\n            y += nodePosition.y;\n            waypoint.setCoord(theseWP[w],x,y);\n          }\n        }\n      }\n    }\n    \n  }\n\n  this.$.endUndo();\n  return _nodes;\n}\n\n\n/**\n * Adds a backdrop to a group in a specific position.\n * @param   {string}           [title=\"Backdrop\"]                The title of the backdrop.\n * @param   {string}           [body=\"\"]                         The body text of the backdrop.\n * @param   {$.oColorValue}    [color=\"#323232ff\"]               The oColorValue of the node.\n * @param   {float}            [x=0]                             The X position of the backdrop, an offset value if nodes are specified.\n * @param   {float}            [y=0]                             The Y position of the backdrop, an offset value if nodes are specified.\n * @param   {float}            [width=30]                        The Width of the backdrop, a padding value if nodes are specified.\n * @param   {float}            [height=30]                       The Height of the backdrop, a padding value if nodes are specified.\n *\n * @return {$.oBackdrop}       The created backdrop.\n */\n$.oGroupNode.prototype.addBackdrop = function(title, body, color, x, y, width, height ){\n  if (typeof color === 'undefined') var color = new this.$.oColorValue(\"#323232ff\");\n  if (typeof body === 'undefined') var body = \"\";\n\n  if (typeof x === 'undefined') var x = 0;\n  if (typeof y === 'undefined') var y = 0;\n  if (typeof width === 'undefined') var width = 30;\n  if (typeof height === 'undefined') var height = 30;\n\n  var position = {\"x\":x, \"y\":y, \"w\":width, \"h\":height};\n\n  var groupPath = this.path;\n\n  if(!(color instanceof this.$.oColorValue)) color = new this.$.oColorValue(color);\n\n\n  // incrementing title so that two backdrops can't have the same title\n  if (typeof title === 'undefined') var title = \"Backdrop\";\n\n  var _groupBackdrops = Backdrop.backdrops(groupPath);\n  var names = _groupBackdrops.map(function(x){return x.title.text})\n  var count = 0;\n  var newTitle = title;\n\n  while (names.indexOf(newTitle) != -1){\n    count++;\n    newTitle = title+\"_\"+count;\n  }\n  title = newTitle;\n\n\n  var _backdrop = {\n  \"position\"    : position,\n  \"title\"       : {\"text\":title, \"color\":4278190080, \"size\":12, \"font\":\"Arial\"},\n  \"description\" : {\"text\":body, \"color\":4278190080, \"size\":12, \"font\":\"Arial\"},\n  \"color\"       : color.toInt()\n  }\n\n  Backdrop.addBackdrop(groupPath, _backdrop)\n  return new this.$.oBackdrop(groupPath, _backdrop)\n};\n\n\n/**\n * Adds a backdrop to a group around specified nodes\n * @param   {$.oNode[]}        nodes                             The nodes that the backdrop encompasses.\n * @param   {string}           [title=\"Backdrop\"]                The title of the backdrop.\n * @param   {string}           [body=\"\"]                         The body text of the backdrop.\n * @param   {$.oColorValue}    [color=#323232ff]                 The oColorValue of the node.\n * @param   {float}            [x=0]                             The X position of the backdrop, an offset value if nodes are specified.\n * @param   {float}            [y=0]                             The Y position of the backdrop, an offset value if nodes are specified.\n * @param   {float}            [width=20]                        The Width of the backdrop, a padding value if nodes are specified.\n * @param   {float}            [height=20]                       The Height of the backdrop, a padding value if nodes are specified.\n *\n * @return {$.oBackdrop}       The created backdrop.\n * @example\n * function createColoredBackdrop(){\n *  // This script will prompt for a color and create a backdrop around the selection\n *  $.beginUndo()\n *\n *  var doc = $.scn; // grab the scene\n *  var nodes = doc.getSelectedNodes(); // grab the selection\n *\n *  if(!nodes) return    // exit the function if no nodes are selected\n *\n *  var color = pickColor(); // prompt for color\n *\n *  var group = nodes[0].group // get the group to add the backdrop to\n *  var backdrop = group.addBackdropToNodes(nodes, \"BackDrop\", \"\", color)\n *\n *  $.endUndo();\n *\n *  // function to get the color chosen by the user\n *  function pickColor(){\n *    var d = new QColorDialog;\n *    d.exec();\n *    var color = d.selectedColor();\n *    return new $.oColorValue({r:color.red(), g:color.green(), b:color.blue(), a:color.alpha()})\n *  }\n * }\n */\n$.oGroupNode.prototype.addBackdropToNodes = function( nodes, title, body, color, x, y, width, height ){\n  if (typeof color === 'undefined') var color = new this.$.oColorValue(\"#323232ff\");\n  if (typeof body === 'undefined') var body = \"\";\n  if (typeof x === 'undefined') var x = 0;\n  if (typeof y === 'undefined') var y = 0;\n  if (typeof width === 'undefined') var width = 20;\n  if (typeof height === 'undefined') var height = 20;\n\n\n  // get default size from node bounds\n  if (typeof nodes === 'undefined') var nodes = [];\n\n  if (nodes.length > 0) {\n    var _nodeBox = new this.$.oBox();\n    _nodeBox.includeNodes(nodes);\n\n    x = _nodeBox.left - x - width;\n    y = _nodeBox.top - y - height;\n    width = _nodeBox.width  + width*2;\n    height = _nodeBox.height + height*2;\n  }\n\n  var _backdrop = this.addBackdrop(title, body, color, x, y, width, height)\n\n  return _backdrop;\n};\n\n\n/**\n * Imports a PSD into the group.\n * This function is not available when running as harmony in batch mode.\n * @param   {string}         path                          The PSD file to import.\n * @param   {bool}           [separateLayers=true]         Separate the layers of the PSD.\n * @param   {bool}           [addPeg=true]                 Whether to add a peg.\n * @param   {bool}           [addComposite=true]           Whether to add a composite.\n * @param   {string}         [alignment=\"ASIS\"]            Alignment type.\n * @param   {$.oPoint}       [nodePosition={0,0,0}]        The position for the node to be placed in the node view.\n *\n * @return {$.oNode[]}     The nodes being created as part of the PSD import.\n * @example\n * // This example browses for a PSD file then import it in the root of the scene, then connects it to the main composite.\n *\n * function importCustomPSD(){\n *   $.beginUndo(\"importCustomPSD\");\n *   var psd = $.dialog.browseForFile(\"get PSD\", \"*.psd\");       // prompt for a PSD file\n *\n *   if (!psd) return;                                           // dialog was cancelled, exit the function\n *\n *   var doc = $.scn;                                            // get the scene object\n *   var sceneRoot = doc.root                                    // grab the scene root group\n *   var psdNodes = sceneRoot.importPSD(psd);                    // import the psd with default settings\n *   var psdComp = psdNodes.pop()                                // get the composite node at the end of the psdNodes array\n *   var sceneComp = doc.$node(\"Top/Composite\")                  // get the scene main composite\n *   psdComp.linkOutNode(sceneComp);                             // ... and link the two.\n *   sceneRoot.orderNodeView();                                  // orders the node view inside the group\n *   $.endUndo();\n * }\n */\n$.oGroupNode.prototype.importPSD = function( path, separateLayers, addPeg, addComposite, alignment, nodePosition){\n  if (typeof alignment === 'undefined') var alignment = \"ASIS\" // create an enum for alignments?\n  if (typeof addComposite === 'undefined') var addComposite = true;\n  if (typeof addPeg === 'undefined') var addPeg = true;\n  if (typeof separateLayers === 'undefined') var separateLayers = true;\n  if (typeof nodePosition === 'undefined') var nodePosition = new this.$.oPoint(0,0,0);\n\n  if (this.$.batchMode){\n    this.$.debug(\"Error: can't import PSD file \"+_psdFile.path+\" in batch mode.\", this.$.DEBUG_LEVEL.ERROR);\n    return null\n  }\n\n  var _psdFile = (path instanceof this.$.oFile)?path:new this.$.oFile( path );\n  if (!_psdFile.exists){\n    this.$.debug(\"Error: can't import PSD file \"+_psdFile.path+\" because it doesn't exist\", this.$.DEBUG_LEVEL.ERROR);\n    return null;\n  }\n\n  this.$.beginUndo(\"oH_importPSD_\"+_psdFile.name);\n\n  var _elementName = _psdFile.name;\n\n  var _xSpacing = 45;\n  var _ySpacing = 30;\n\n  var _element = this.scene.addElement(_elementName, \"PSD\");\n\n  // save scene otherwise PSD is copied correctly into the element\n  // but the TGA for each layer are not generated\n  // TODO: how to go around this to avoid saving?\n  scene.saveAll();\n  var _drawing = _element.addDrawing(1);\n\n  if (addPeg) var _peg = this.addNode(\"PEG\", _elementName+\"-P\", nodePosition);\n  if (addComposite) var _comp = this.addNode(\"COMPOSITE\", _elementName+\"-Composite\", nodePosition);\n\n  // Import the PSD in the element\n  CELIO.pasteImageFile({ src : _psdFile.path, dst : { elementId : _element.id, exposure : _drawing.name}});\n  var _layers = CELIO.getLayerInformation(_psdFile.path);\n  var _info = CELIO.getInformation(_psdFile.path);\n\n  // create the nodes for each layer\n  var _nodes = [];\n  if (separateLayers){\n\n    var _scale = _info.height/scene.defaultResolutionY();\n    var _x = nodePosition.x - _layers.length/2*_xSpacing;\n    var _y = nodePosition.y - _layers.length/2*_ySpacing;\n\n    for (var i in _layers){\n      // generate nodes and set them to show the element for each layer\n      var _layer = _layers[i];\n      var _layerName = _layer.layerName.split(\" \").join(\"_\");\n      var _nodePosition = new this.$.oPoint(_x+=_xSpacing, _y +=_ySpacing, 0);\n\n      // get/build the group\n      var _group = this;\n      var _groupPathComponents = _layer.layerPathComponents;\n      var _destinationPath = this.path;\n      var _groupPeg = _peg;\n      var _groupComp = _comp;\n\n      // recursively creating groups if they are missing\n      for (var i in _groupPathComponents){\n        var _destinationPath = _destinationPath + \"/\" + _groupPathComponents[i];\n        var _nextGroup = this.$.scene.getNodeByPath(_destinationPath);\n\n        if (!_nextGroup){\n          _nextGroup = _group.addGroup(_groupPathComponents[i], true, true, [], _nodePosition);\n          if (_groupPeg) _nextGroup.linkInNode(_groupPeg);\n          if (_groupComp) _nextGroup.linkOutNode(_groupComp, 0, 0);\n        }\n        // store the peg/comp for next iteration or layer node\n        _group = _nextGroup;\n        _groupPeg = _group.multiportIn.linkedOutNodes[0];\n        _groupComp = _group.multiportOut.linkedInNodes[0];\n      }\n\n      var _column = this.scene.addColumn(\"DRAWING\", _layerName, _element);\n      var _node = _group.addDrawingNode(_layerName, _nodePosition, _element, _column);\n\n      _node.enabled = _layers[i].visible;\n      _node.can_animate = false; // use general pref?\n      _node.apply_matte_to_color = \"Straight\";\n      _node.alignment_rule = alignment;\n      _node.scale.x = _scale;\n      _node.scale.y = _scale;\n\n      _column.setValue(_layer.layer != \"\"?\"1:\"+_layer.layer:1, 1);\n      _column.extendExposures();\n\n      if (_groupPeg) _node.linkInNode(_groupPeg);\n      if (_groupComp) _node.linkOutNode(_groupComp, 0, 0);\n\n      _nodes.push(_node);\n    }\n  }else{\n    this.$.endUndo();\n    throw new Error(\"importing PSD as a flattened layer not yet implemented\");\n  }\n\n  if (addPeg){\n    _peg.centerAbove(_nodes, 0, -_ySpacing )\n    _nodes.unshift(_peg)\n  }\n\n  if (addComposite){\n    _comp.centerBelow(_nodes, 0, _ySpacing )\n    _nodes.push(_comp)\n  }\n  // TODO how to display only one node with the whole file\n  this.$.endUndo()\n\n  return _nodes\n}\n\n\n/**\n * Updates a PSD previously imported into the group\n * @param   {string}       path                          The updated psd file to import.\n * @param   {bool}         [separateLayers=true]         Separate the layers of the PSD.\n *\n * @return  {$.oNode[]}    The nodes that have been updated/created\n */\n$.oGroupNode.prototype.updatePSD = function( path, separateLayers ){\n  if (typeof separateLayers === 'undefined') var separateLayers = true;\n\n  var _psdFile = (path instanceof this.$.oFile)?path:new this.$.oFile(path);\n  if (!_psdFile.exists){\n    this.$.debug(\"Error: can't import PSD file \"+_psdFile.path+\" for update because it doesn't exist\", this.$.DEBUG_LEVEL.ERROR);\n    return null;\n  }\n\n  this.$.beginUndo(\"oH_updatePSD_\"+_psdFile.name)\n\n  // get info from the PSD\n  var _info = CELIO.getInformation(_psdFile.path);\n  var _layers = CELIO.getLayerInformation(_psdFile.path);\n  var _scale = _info.height/scene.defaultResolutionY();\n\n  // use layer information to find nodes from precedent export\n  if (separateLayers){\n    var _nodes = this.subNodes(true).filter(function(x){return x.type == \"READ\"});\n    var _nodeNames = _nodes.map(function(x){return x.name});\n\n    var _psdNodes = [];\n    var _missingLayers = [];\n    var _PSDelement = \"\";\n    var _positions = new Array(_layers.length);\n    var _scale = _info.height/scene.defaultResolutionY();\n\n    // for each layer find the node by looking at the column name\n    for (var i in _layers){\n      var _layer = _layers[i];\n      var _layerName = _layers[i].layerName.split(\" \").join(\"_\");\n      var _found = false;\n\n      // find the node\n      for (var j in _nodes){\n        if (_nodes[j].element.format != \"PSD\") continue;\n\n        var _drawingColumn = _nodes[j].attributes.drawing.element.column;\n\n        // update the node if found\n        if (_drawingColumn.name == _layer.layerName){\n          _psdNodes.push(_nodes[j]);\n          _found = true;\n\n           // update scale in case PSDfile size changed\n          _nodes[j].scale.x = _scale;\n          _nodes[j].scale.y = _scale;\n\n          _positions[_layer.position] = _nodes[j];\n\n          // store the element\n          _PSDelement = _nodes[j].element\n\n          break;\n        }\n        // if not found, add to the list of layers to import\n        _found = false;\n      }\n\n      if (!_found) _missingLayers.push(_layer);\n    }\n\n\n    if (_psdNodes.length == 0){\n      // PSD was never imported, use import instead?\n      this.$.debug(\"can't find a PSD element to update\", this.$.DEBUG_LEVEL.ERROR);\n      this.$.endUndo();\n      return null;\n    }\n\n    // pasting updated PSD into element\n    CELIO.pasteImageFile({ src : _psdFile.path, dst : { elementId : _PSDelement.id, exposure : \"1\"}})\n\n    for (var i in _missingLayers){\n      // find previous import Settings re: group/alignment etc\n      var _layer = _missingLayers[i];\n      var _layerName = _layer.layerName.split(\" \").join(\"_\");\n\n      var _layerIndex = _layer.position;\n      var _nodePosition = new this.$.oPoint(0,0,0);\n      var _group = _psdNodes[0].group;\n      var _alignment = _psdNodes[0].alignment_rule;\n      var _scale = _psdNodes[0].scale.x;\n      var _peg = _psdNodes[0].inNodes[0];\n      var _comp = _psdNodes[0].outNodes[0];\n      var _scale = _info.height/scene.defaultResolutionY()\n      var _port;\n\n      //TODO: set into right group according to PSD organisation\n      // looking for the existing node below and get the comp port from it\n      for (var j = _layerIndex-1; j>=0; j--){\n        if (_positions[j] != undefined) break;\n      }\n      var _nodeBelow = _positions[j];\n\n      var _compNodes = _comp.inNodes;\n\n      for (var j=0; j<_compNodes.length; j++){\n        if (_nodeBelow.path == _compNodes[j].path){\n          _port = j+1;\n          _nodePosition = _compNodes[j].nodePosition;\n          _nodePosition.x -= 35;\n          _nodePosition.y -= 25;\n        }\n      }\n\n      // generate nodes and set them to show the element for each layer\n      var _node = this.addDrawingNode(_layerName, _nodePosition, _PSDelement);\n\n      _node.enabled = _layer.visible;\n      _node.can_animate = false; // use general pref?\n      _node.apply_matte_to_color = \"Straight\";\n      _node.alignment_rule = _alignment;\n      _node.scale.x = _scale;\n      _node.scale.y = _scale;\n\n      _node.attributes.drawing.element.setValue(_layer.layer != \"\"?\"1:\"+_layer.layer:1, 1);\n      _node.attributes.drawing.element.column.extendExposures();\n\n      // find composite/peg to connect to based on other layers\n\n      //if (addPeg) _node.linkInNode(_peg)\n      if (_port) _node.linkOutNode(_comp, 0, _port)\n\n      _nodes.push(_node);\n    }\n    this.$.endUndo();\n    return nodes;\n  } else{\n      this.$.endUndo();\n      throw new Error(\"updating a PSD imported as a flattened layer not yet implemented\");\n  }\n}\n\n\n/**\n * Import a generic image format (PNG, JPG, TGA etc) as a read node.\n * @param {string} path The image file to import.\n * @param {string} [alignment=\"ASIS\"] Alignment type.\n * @param {$.oPoint} [nodePosition={0,0,0}] The position for the node to be placed in the node view.\n *\n * @return  {$.oNode}    The node for the imported image\n */\n$.oGroupNode.prototype.importImage = function( path, alignment, nodePosition, convertToTvg){\n  if (typeof alignment === 'undefined') var alignment = \"ASIS\"; // create an enum for alignments?\n  if (typeof nodePosition === 'undefined') var nodePosition = new this.$.oPoint(0,0,0);\n\n  var _imageFile = (path instanceof this.$.oFile)?path:new this.$.oFile( path );\n  var _elementName = _imageFile.name;\n\n  var _elementType = convertToTvg?\"TVG\":_imageFile.extension.toUpperCase();\n  var _element = this.scene.addElement(_elementName, _elementType);\n  var _column = this.scene.addColumn(\"DRAWING\", _elementName, _element);\n  _element.column = _column;\n\n  if (_imageFile.exists) {\n    var _drawing = _element.addDrawing(1, 1, _imageFile.path, convertToTvg);\n  }else{\n    this.$.debug(\"Image file to import \"+_imageFile.path+\" could not be found.\", this.$.DEBUG_LEVEL.ERROR);\n  }\n\n  var _imageNode = this.addDrawingNode(_elementName, nodePosition, _element);\n\n  _imageNode.can_animate = false; // use general pref?\n  _imageNode.apply_matte_to_color = \"Straight\";\n  _imageNode.alignment_rule = alignment;\n\n  var _scale = CELIO.getInformation(_imageFile.path).height/this.scene.defaultResolutionY;\n  _imageNode.scale.x = _scale;\n  _imageNode.scale.y = _scale;\n\n  _imageNode.attributes.drawing.element.setValue(_drawing.name, 1);\n  _imageNode.attributes.drawing.element.column.extendExposures();\n\n  // TODO how to display only one node with the whole file\n  return _imageNode;\n}\n\n\n/**\n * imports an image as a tvg drawing.\n * @param {$.oFile} path                         the image file to import\n * @param {string} [alignment=\"ASIS\"]            the alignment mode for the imported image\n * @param {$.oPoint} [nodePosition={0,0,0}]      the position for the created node.\n */\n$.oGroupNode.prototype.importImageAsTVG = function(path, alignment, nodePosition){\n  if (!(path instanceof this.$.oFile)) path = new this.$.oFile(path);\n\n  var _imageNode = this.importImage(_convertedFilePath, alignment, nodePosition, true);\n  _imageNode.name = path.name;\n\n  return _imageNode;\n}\n\n\n/**\n * imports an image sequence as a node into the current group.\n * @param {$.oFile[]} imagePaths           a list of paths to the images to import (can pass a list of strings or $.oFile)\n * @param {number}    [exposureLength=1]   the number of frames each drawing should be exposed at. If set to 0/false, each drawing will use the numbering suffix of the file to set its frame.\n * @param {boolean}   [convertToTvg=false] whether to convert the files to tvg during import\n * @param {string}    [alignment=\"ASIS\"]   the alignment to apply to the node\n * @param {$.oPoint}  [nodePosition]       the position of the node in the nodeview\n *\n * @returns {$.oDrawingNode} the created node\n */\n$.oGroupNode.prototype.importImageSequence = function(imagePaths, exposureLength, convertToTvg, alignment, nodePosition, extendScene) {\n  if (typeof exposureLength === 'undefined') var exposureLength = 1;\n  if (typeof alignment === 'undefined') var alignment = \"ASIS\"; // create an enum for alignments?\n  if (typeof nodePosition === 'undefined') var nodePosition = new this.$.oPoint(0,0,0);\n\n  if (typeof extendScene === 'undefined') var extendScene = false;\n\n  // match anything but capture trailing numbers and separates punctuation preceding it\n  var numberingRe = /(.*?)([\\W_]+)?(\\d*)$/i;\n\n  // sanitize imagePaths\n  imagePaths = imagePaths.map(function(x){\n    if (x instanceof this.$.oFile){\n      return x;\n    } else {\n      return new this.$.oFile(x);\n    }\n  })\n\n  var images = [];\n\n  if (!exposureLength) {\n  // figure out scene length based on exposure and extend the scene if needed\n    var sceneLength = 0;\n    var image = {frame:0, path:\"\"};\n\n    for (var i in imagePaths){\n      var imagePath = imagePaths[i];\n      if (!(imagePath instanceof this.$.oFile)) imagePath = new this.$.oFile(imagePath);\n      var nameGroups = imagePath.name.match(numberingRe);\n\n      if (nameGroups[3]){\n        // use trailing number as frame number\n        var frameNumber = parseInt(nameGroups[3], 10);\n        if (frameNumber > sceneLength) sceneLength = frameNumber;\n\n        images.push({frame: frameNumber, path:imagePath});\n      }\n    }\n  } else {\n    // simply create a list of numbers based on exposure\n    images = imagePaths.map(function(x, index){\n      var frameNumber = index * exposureLength + 1;\n      return ({frame:frameNumber, path:x});\n    })\n    var sceneLength = images[images.length-1].frame + exposureLength - 1;\n  }\n\n  if (extendScene){\n    if (this.scene.length < sceneLength) this.scene.length = sceneLength;\n  }\n\n  // create a node to hold the image sequence\n  var firstImage = imagePaths[0];\n  var name = firstImage.name.match(numberingRe)[1]; // match anything before trailing digits\n  var drawingNode = this.importImage(firstImage, alignment, nodePosition, convertToTvg);\n  drawingNode.name = name;\n\n  for (var i in images){\n    var image = images[i];\n    drawingNode.element.addDrawing(image.frame, image.frame, image.path, convertToTvg);\n  }\n\n  drawingNode.timingColumn.extendExposures();\n\n  return drawingNode;\n}\n\n/**\n * Imports a QT into the group\n * @param   {string}         path                          The palette file to import.\n * @param   {bool}           [importSound=true]            Whether to import the sound\n * @param   {bool}           [extendScene=true]            Whether to extend the scene to the duration of the QT.\n * @param   {string}         [alignment=\"ASIS\"]            Alignment type.\n * @param   {$.oPoint}       [nodePosition]                The position for the node to be placed in the network.\n *\n * @return {$.oNode}        The imported Quicktime Node.\n */\n$.oGroupNode.prototype.importQT = function( path, importSound, extendScene, alignment, nodePosition){\n  if (typeof alignment === 'undefined') var alignment = \"ASIS\";\n  if (typeof extendScene === 'undefined') var extendScene = true;\n  if (typeof importSound === 'undefined') var importSound = true;\n  if (typeof nodePosition === 'undefined') var nodePosition = new this.$.oPoint(0,0,0);\n\n  var _QTFile = (path instanceof this.$.oFile)?path:new this.$.oFile(path);\n  if (!_QTFile.exists){\n    throw new Error (\"Import Quicktime failed: file \"+_QTFile.path+\" doesn't exist\");\n  }\n\n  var _movieName = _QTFile.name;\n  this.$.beginUndo(\"oH_importQT_\"+_movieName);\n\n  var _element = this.scene.addElement(_movieName, \"PNG\");\n  var _elementName = _element.name;\n\n  var _movieNode = this.addDrawingNode(_movieName, nodePosition, _element);\n  var _column = _movieNode.attributes.drawing.element.column;\n  _element.column = _column;\n\n  // setup the node\n  _movieNode.can_animate = false;\n  _movieNode.alignment_rule = alignment;\n\n  // create the temp folder\n  var _tempFolder = new this.$.oFolder(this.$.scn.tempFolder.path + \"/movImport/\" + _element.id);\n  _tempFolder.create();\n\n  var _tempFolderPath = _tempFolder.path;\n  var _audioPath = _tempFolder.path + \"/\" + _movieName + \".wav\";\n\n  // progressDialog will display an infinite loading bar as we don't have precise feedback\n  var progressDialog = new this.$.oProgressDialog(\"Importing video...\", 0, \"Import Movie\", true);\n\n  // setup import\n  MovieImport.setMovieFilename(_QTFile.path);\n  MovieImport.setImageFolder(_tempFolder);\n  MovieImport.setImagePrefix(_movieName);\n  if (importSound) MovieImport.setAudioFile(_audioPath);\n  this.$.log(\"converting movie file to pngs...\");\n  MovieImport.doImport();\n  this.$.log(\"conversion finished\");\n\n  progressDialog.range = 100;\n  progressDialog.value = 80;\n\n  var _movielength = MovieImport.numberOfImages();\n\n  if (extendScene && this.scene.length < _movielength) this.scene.length = _movielength;\n\n  // create a drawing for each frame\n  for (var i=1; i<=_movielength; i++) {\n    _drawingPath = _tempFolder + \"/\" + _movieName + \"-\" + i + \".png\";\n    _element.addDrawing(i, i, _drawingPath);\n  }\n\n  progressDialog.value = 95;\n\n  // creating an audio column for the sound\n  if (importSound && MovieImport.isAudioFileCreated() ){\n    var _soundName = _elementName + \"_sound\";\n    var _soundColumn = this.scene.addColumn(\"SOUND\", _soundName);\n    column.importSound( _soundColumn.name, 1, _audioPath);\n  }\n\n  progressDialog.value = 100;\n\n  this.$.endUndo();\n  return _movieNode;\n}\n\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_nodeAttributes.js",
    "content": "var docstringStart = \"/**\"\nvar docstringEnd = \"*/\"\n/*\n// values outputted by following script:\nfunction traceAll(){\n  var globalMessage = [docstringStart+\"\\n * Attributes associated to Node types\\n * @class NodeTypes \\n \"+docstringEnd];\n  var nodes = selection.selectedNodes()\n\n  for (var i in nodes){\n    var message = [\n      \"Attributes present in the node : \" + node.getName(nodes[i]),\n      \"@name  NodeTypes#\"+ node.type(nodes[i])\n    ]\n    message = message.concat(node.getAttrList( nodes[i], 1).map(function(x){return traceAttributes(x, nodes[i]));\n    message = (\"\\n \"+docstringStart+\"\\n * \")+message.join(\"\\n * \")+(\"\\n \"+docstringEnd+\"\\n\");\n    globalMessage.push(message);\n  }\n\n  MessageLog.trace(globalMessage.join(\"\\n\"));\n}\n\nfunction traceAttributes(attribute, theNode){\n  var message = [formatAttribute(attribute, theNode)];\n  if (attribute.hasSubAttributes()){\n    var subattributes = attribute.getSubAttributes();\n    for (var i in subattributes){\n      message = message.concat(traceAttributes(subattributes[i], theNode));\n    }\n  }\n  return message\n}\n\nfunction formatAttribute(attr, theNode){\n  var keyword = attr.fullKeyword();\n  var type = attr.typeName().toLowerCase();\n  var name = attr.name();\n  var defaultValue = node.getTextAttr(theNode, 1, keyword).split(\" \").join(\"_\").replace(\".0000\", \"\");\n  if (defaultValue == \"N\") defaultValue = \"false\";\n  if (defaultValue == \"Y\") defaultValue = \"true\";\n  var message = \"@property {\"+type+\"}  \"+keyword.toLowerCase()+((defaultValue)?\"=\"+defaultValue:\"\")+\"   - \"+name+\".\"\n  return message;\n}\n*/\n\n/**\n * Attributes associated to Node types.<br>These are the types to specify when creating a node, and the corresponding usual node name when creating directly through Harmony's interface. The attributes displayed here can be set and manipulated by calling the displayed names.\n * @class NodeTypes\n * @hideconstructor\n * @namespace\n * @example\n * // This is how to use this page:\n *\n * var myNode = $.scn.root.addNode(\"READ\");        // This is the node type as specified for each node under the default display name.\n * $.log(myNode.type)                              // This is how to find out the type\n *\n * myNode.drawing.element = \"1\"                    // Sets the drawing.element attribute to display drawing \"1\"\n *\n * myNode.drawing.element = {frameNumber: 5, \"2\"}  // If the attribute can be animated, pass a {frameNumber, value} object to set a specific frame;\n *\n * myNode.attributes.drawing.element.setValue (\"2\", 5)   // also possible to set the attribute directly.\n *\n * // refer to the node type on this page to find out what properties can be set with what synthax for each Node Type.\n */\n\n/**\n * Attributes present in the node of type: 'MasterController'\n * @name  NodeTypes#MasterController\n * @property {string}  [specs_editor=\"<specs>\n  <ports>\n    <in type=\"IMAGE\"/>\n    <out type=\"IMAGE\"/>\n  </ports>\n  <attributes>\n  </attributes>\n</specs>\n\"]   - Specifications.\n * @property {file_editor}  script_editor   - .\n * @property {file_editor}  init_script   - .\n * @property {file_editor}  cleanup_script   - .\n * @property {file_editor}  ui_script   - .\n * @property {string}  ui_data   - .\n * @property {file_library}  files   - .\n * @property {generic_enum}  [show_controls_mode=\"Normal\"]   - Show Controls Mode.\n */\n\n\n/**\n * Attributes present in the node of type: 'ShapeCurve'\n * @name  NodeTypes#Shape-Curve\n * @property {bool}  flattenz=true   - Flatten Z.\n * @property {position_3d}  position1   - Position 1.\n * @property {bool}  position1.separate=On   - Separate.\n * @property {double}  position1.x=0   - Pos x.\n * @property {double}  position1.y=0   - Pos y.\n * @property {double}  position1.z=0   - Pos z.\n * @property {path_3d}  position1.3dpath   - Path.\n * @property {position_3d}  position2   - Left Handle Offset.\n * @property {bool}  position2.separate=On   - Separate.\n * @property {double}  position2.x=0   - Pos x.\n * @property {double}  position2.y=-1   - Pos y.\n * @property {double}  position2.z=0   - Pos z.\n * @property {path_3d}  position2.3dpath   - Path.\n * @property {position_3d}  position3   - Right Handle Offset.\n * @property {bool}  position3.separate=On   - Separate.\n * @property {double}  position3.x=0   - Pos x.\n * @property {double}  position3.y=1   - Pos y.\n * @property {double}  position3.z=0   - Pos z.\n * @property {path_3d}  position3.3dpath   - Path.\n * @property {bool}  closeshape=false   - Close Shape Contour.\n */\n\n\n/**\n * Attributes present in the node of type: 'SubNodeAnimation'\n * @name  NodeTypes#Subnode-Animation\n */\n\n\n/**\n * Attributes present in the node of type: 'BoneModule'\n * @name  NodeTypes#Stick\n * @property {generic_enum}  [influencetype=\"Infinite\"]   - Influence Type.\n * @property {double}  influencefade=0.5000   - Influence Fade Radius.\n * @property {bool}  symmetric=true   - Symmetric Ellipse of Influence.\n * @property {double}  transversalradius=1   - Transversal Influence Radius Left.\n * @property {double}  transversalradiusright=1   - Transversal Influence Radius Right.\n * @property {double}  longitudinalradiusbegin=1   - Longitudinal Influence Radius Begin.\n * @property {double}  longitudinalradius=1   - Longitudinal Influence Radius End.\n * @property {double}  restlength=3   - Rest Length.\n * @property {double}  length=3   - Length.\n */\n\n\n/**\n * Attributes present in the node of type: 'CurveModule'\n * @name  NodeTypes#Curve\n * @property {bool}  localreferential=true   - Apply Parent Transformation.\n * @property {generic_enum}  [influencetype=\"Infinite\"]   - Influence Type.\n * @property {double}  influencefade=0.5000   - Influence Fade Radius.\n * @property {bool}  symmetric=true   - Symmetric Ellipse of Influence.\n * @property {double}  transversalradius=1   - Transversal Influence Radius Left.\n * @property {double}  transversalradiusright=1   - Transversal Influence Radius Right.\n * @property {double}  longitudinalradiusbegin=1   - Longitudinal Influence Radius Begin.\n * @property {double}  longitudinalradius=1   - Longitudinal Influence Radius End.\n * @property {bool}  closepath=false   - Close Contour.\n * @property {double}  restlength0=2   - Rest Length 0.\n * @property {double}  restingorientation0=0   - Resting Orientation 0.\n * @property {position_2d}  restingoffset   - Resting Offset.\n * @property {bool}  restingoffset.separate=On   - Separate.\n * @property {double}  restingoffset.x=6   - Pos x.\n * @property {double}  restingoffset.y=0   - Pos y.\n * @property {double}  restlength1=2   - Rest Length 1.\n * @property {double}  restingorientation1=0   - Resting Orientation 1.\n * @property {double}  length0=2   - Length 0.\n * @property {double}  orientation0=0   - Orientation 0.\n * @property {position_2d}  offset   - Offset.\n * @property {bool}  offset.separate=On   - Separate.\n * @property {double}  offset.x=6   - Pos x.\n * @property {double}  offset.y=0   - Pos y.\n * @property {point_2d}  offset.2dpoint   - Point.\n * @property {double}  length1=2   - Length 1.\n * @property {double}  orientation1=0   - Orientation 1.\n */\n\n\n/**\n * Attributes present in the node of type: 'BendyBoneModule'\n * @name  NodeTypes#Bone\n * @property {generic_enum}  [influencetype=\"Infinite\"]   - Influence Type.\n * @property {double}  influencefade=0.5000   - Influence Fade Radius.\n * @property {bool}  symmetric=true   - Symmetric Ellipse of Influence.\n * @property {double}  transversalradius=1   - Transversal Influence Radius Left.\n * @property {double}  transversalradiusright=1   - Transversal Influence Radius Right.\n * @property {double}  longitudinalradiusbegin=1   - Longitudinal Influence Radius Begin.\n * @property {double}  longitudinalradius=1   - Longitudinal Influence Radius End.\n * @property {position_2d}  restoffset   - Rest Offset.\n * @property {bool}  restoffset.separate=On   - Separate.\n * @property {double}  restoffset.x=0   - Pos x.\n * @property {double}  restoffset.y=0   - Pos y.\n * @property {double}  restorientation=0   - Rest Orientation.\n * @property {double}  restradius=0.5000   - Rest Radius.\n * @property {double}  restbias=0.4500   - Rest Bias.\n * @property {double}  restlength=3   - Rest Length.\n * @property {position_2d}  offset   - Offset.\n * @property {bool}  offset.separate=On   - Separate.\n * @property {double}  offset.x=0   - Pos x.\n * @property {double}  offset.y=0   - Pos y.\n * @property {point_2d}  offset.2dpoint   - Point.\n * @property {double}  orientation=0   - Orientation.\n * @property {double}  radius=0.5000   - Radius.\n * @property {double}  bias=0.4500   - Bias.\n * @property {double}  length=3   - Length.\n */\n\n\n/**\n * Attributes present in the node of type: 'ShapeRender'\n * @name  NodeTypes#Shape-Render\n * @property {generic_enum}  [edgetype=\"Hard Edge\"]   - Edge Type.\n * @property {double}  scale=25   - Scale.\n * @property {double}  discretizationscale=5   - Discretization Scale.\n * @property {double}  preblur=0   - Pre-Blur.\n * @property {double}  postblur=0   - Post-Blur.\n */\n\n\n/**\n * Attributes present in the node of type: 'NormalFloat'\n * @name  NodeTypes#Normal-Map-Converter\n * @property {generic_enum}  [conversiontype=\"Genarts\"]   - Conversion Type.\n * @property {double}  offset=0   - Offset.\n * @property {double}  length=1   - Length.\n * @property {bool}  invertred=false   - Invert Red.\n * @property {bool}  invertgreen=false   - Invert Green.\n * @property {bool}  invertblue=false   - Invert Blue.\n */\n\n\n/**\n * Attributes present in the node of type: 'GameBoneModule'\n * @name  NodeTypes#Game-Bone\n * @property {position_2d}  restoffset   - Rest Offset.\n * @property {bool}  restoffset.separate=On   - Separate.\n * @property {double}  restoffset.x=0   - Pos x.\n * @property {double}  restoffset.y=0   - Pos y.\n * @property {double}  restorientation=0   - Rest Orientation.\n * @property {double}  restradius=0.5000   - Rest Radius.\n * @property {double}  restbias=0.4500   - Rest Bias.\n * @property {double}  restlength=3   - Rest Length.\n * @property {position_2d}  offset   - Offset.\n * @property {bool}  offset.separate=On   - Separate.\n * @property {double}  offset.x=0   - Pos x.\n * @property {double}  offset.y=0   - Pos y.\n * @property {point_2d}  offset.2dpoint   - Point.\n * @property {double}  orientation=0   - Orientation.\n * @property {double}  radius=0.5000   - Radius.\n * @property {double}  bias=0.4500   - Bias.\n * @property {double}  length=3   - Length.\n */\n\n\n/**\n * Attributes present in the node of type: 'OffsetModule'\n * @name  NodeTypes#Offset\n * @property {bool}  localreferential=true   - Apply Parent Transformation.\n * @property {position_2d}  restingoffset   - Resting Offset.\n * @property {bool}  restingoffset.separate=On   - Separate.\n * @property {double}  restingoffset.x=1   - Pos x.\n * @property {double}  restingoffset.y=0   - Pos y.\n * @property {double}  restingorientation=0   - Resting Orientation.\n * @property {position_2d}  offset   - Offset.\n * @property {bool}  offset.separate=On   - Separate.\n * @property {double}  offset.x=1   - Pos x.\n * @property {double}  offset.y=0   - Pos y.\n * @property {point_2d}  offset.2dpoint   - Point.\n * @property {double}  orientation=0   - Orientation.\n */\n\n\n/**\n * Attributes present in the node of type: 'ArticulationModule'\n * @name  NodeTypes#Articulation\n * @property {generic_enum}  [influencetype=\"Infinite\"]   - Influence Type.\n * @property {double}  influencefade=0.5000   - Influence Fade Radius.\n * @property {bool}  symmetric=true   - Symmetric Ellipse of Influence.\n * @property {double}  transversalradius=1   - Transversal Influence Radius Left.\n * @property {double}  transversalradiusright=1   - Transversal Influence Radius Right.\n * @property {double}  longitudinalradiusbegin=1   - Longitudinal Influence Radius Begin.\n * @property {double}  longitudinalradius=1   - Longitudinal Influence Radius End.\n * @property {double}  restradius=0.5000   - Rest Radius.\n * @property {double}  restingorientation=0   - Resting Orientation.\n * @property {double}  restbias=0.4500   - Rest Bias.\n * @property {double}  radius=0.5000   - Radius.\n * @property {double}  orientation=0   - Orientation.\n * @property {double}  bias=0.4500   - Bias.\n */\n\n\n/**\n * Attributes present in the node of type: 'TransformGate'\n * @name  NodeTypes#Transformation-Gate\n * @property {double}  active=100   - ACTIVE.\n * @property {int}  target_gate=1   - LOCAL TARGET GATE.\n * @property {int}  default_gate=0   - DEFAULT GATE.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleBaker'\n * @name  NodeTypes#Particle-Baker\n * @property {int}  maxnumparticles=10000   - Maximum Number of Particles.\n * @property {generic_enum}  [simulationquality=\"Normal\"]   - Simulation Quality.\n * @property {int}  seed=0   - Seed.\n * @property {int}  transientframes=0   - Number of Pre-roll Frames.\n * @property {bool}  moveage=false   - Age Particles.\n * @property {bool}  moveposition=true   - Move Position.\n * @property {bool}  moveangle=false   - Move Angle.\n * @property {bool}  roundage=false   - Round Particle Age.\n */\n\n\n/**\n * Attributes present in the node of type: 'KinematicOutputModule'\n * @name  NodeTypes#Kinematic-Output\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleVisualizer'\n * @name  NodeTypes#Particle-Visualizer\n * @property {bool}  forcedots=false   - Force to Render as Dots.\n * @property {generic_enum}  [sortingstrategy=\"Back to Front\"]   - Rendering Order.\n * @property {bool}  fixalpha=true   - Fix Output Alpha.\n * @property {bool}  useviewscaling=true   - Scale Particle System Using Parent Peg.\n * @property {double}  globalsize=1   - Global Scaling Factor.\n */\n\n\n/**\n * Attributes present in the node of type: 'FreeFormDeformation'\n * @name  NodeTypes#Free-Form-Deformer\n * @property {generic_enum}  [deformationquality=\"Very High\"]   - Deformation Quality.\n */\n\n\n/**\n * Attributes present in the node of type: 'ComputeNormals'\n * @name  NodeTypes#Normal-Map\n * @property {string}  objectlist   - Volume Creation.\n * @property {bool}  depthinblue=false   - Output Elevation in Blue Channel.\n * @property {double}  blurscale=1   - Bevel Multiplier.\n * @property {bool}  clipblurredwithgeometry=true   - Clip Blurred Image with Geometry.\n * @property {double}  elevationscale=1   - Elevation Multiplier.\n * @property {double}  elevationsmoothness=1   - Elevation Smoothness Multiplier.\n * @property {bool}  generatenormals=true   - Generate Normals.\n * @property {generic_enum}  [normalquality=\"Low\"]   - Normal Map Quality.\n * @property {bool}  usetruckfactor=false   - Consider Truck Factor.\n * @property {string}  colorinformation   - Colour Information.\n */\n\n\n/**\n * Attributes present in the node of type: 'PointConstraint3'\n * @name  NodeTypes#Three-Points-Constraints\n * @property {double}  active=100   - Active.\n * @property {generic_enum}  [flattentype=\"Allow 3D Transform\"]   - Flatten Type.\n * @property {generic_enum}  [transformtype=\"Translate\"]   - Transform Type.\n * @property {generic_enum}  [primaryport=\"Right\"]   - Primary Port.\n */\n\n\n/**\n * Attributes present in the node of type: 'GROUP'\n * @name  NodeTypes#Group\n * @property {string}  [editor=\"<editor dockable=\"true\" title=\"Script\" winPreferred=\"640x460\" linuxPreferred=\"640x480\">\n  <tab title=\"Editor\" expand=\"true\">\n    <attr name=\"EDITOR\"/>\n  </tab>\n  <tab title=\"Options\">\n    <attr name=\"TARGET_COMPOSITE\"/>\n    <attr name=\"TIMELINE_MODULE\"/>\n  </tab>\n  <tab title=\"Extern Attributes\">\n    <!-- <attr name=\"GroupA/GroupB/Module:Attr1/SubAttr\"/> -->\n    <!-- <attr module=\"GroupA/GroupB/Module\" name=\"Attr1/SubAttr\"/> -->\n  </tab>\n</editor>\n\n\"]   - .\n * @property {string}  target_composite   - Target Composite.\n * @property {string}  timeline_module   - Substitute Node in Timeline.\n * @property {bool}  mask=false   - Mask Flag.\n * @property {bool}  publish_to_parents=true   - Publish to Parent Groups.\n */\n\n\n/**\n * Attributes present in the node of type: 'ComputeWorld'\n * @name  NodeTypes#Surface-Map\n * @property {string}  objectlist   - Volume Creation.\n * @property {double}  blurscale=1   - Bevel Multiplier.\n * @property {bool}  clipblurredwithgeometry=true   - Clip Blurred Image with Geometry.\n * @property {double}  elevationscale=1   - Elevation Multiplier.\n * @property {double}  elevationsmoothness=1   - Elevation Smoothness Multiplier.\n * @property {bool}  usetruckfactor=false   - Consider Truck Factor.\n * @property {string}  colorinformation   - Colour Information.\n */\n\n\n/**\n * Attributes present in the node of type: 'FOCUS_SET'\n * @name  NodeTypes#Focus\n * @property {bool}  mirror=true   - Mirror.\n * @property {double}  ratio=2   - Mirror Front/Back Ratio.\n * @property {simple_bezier}  radius=(Curve)   - Radius.\n * @property {generic_enum}  [quality=\"High\"]   - Quality.\n */\n\n\n/**\n * Attributes present in the node of type: 'SCRIPT_MODULE'\n * @name  NodeTypes#ScriptModule\n * @property {string}  [specs_editor=\"<specs>\n  <ports>\n    <in type=\"IMAGE\"/>\n    <out type=\"IMAGE\"/>\n  </ports>\n  <attributes>\n  </attributes>\n</specs>\n\"]   - Specifications.\n * @property {file_editor}  script_editor   - .\n * @property {file_editor}  init_script   - .\n * @property {file_editor}  cleanup_script   - .\n * @property {file_editor}  ui_script   - .\n * @property {string}  ui_data   - .\n * @property {file_library}  files   - .\n */\n\n\n/**\n * Attributes present in the node of type: 'StaticConstraint'\n * @name  NodeTypes#Static-Transformation\n * @property {push_button}  bakeattr   - Bake Immediate Parent's Transformation.\n * @property {push_button}  bakeattr_all   - Bake All Incoming Transformations.\n * @property {bool}  active=false   - Active.\n * @property {position_3d}  translate   - Translate.\n * @property {bool}  translate.separate=On   - Separate.\n * @property {double}  translate.x=0   - Pos x.\n * @property {double}  translate.y=0   - Pos y.\n * @property {double}  translate.z=0   - Pos z.\n * @property {scale_3d}  scale   - Skew.\n * @property {bool}  scale.separate=On   - Separate.\n * @property {bool}  scale.in_fields=Off   - In fields.\n * @property {doublevb}  scale.xy=1   - Scale.\n * @property {doublevb}  scale.x=1   - Scale x.\n * @property {doublevb}  scale.y=1   - Scale y.\n * @property {doublevb}  scale.z=1   - Scale z.\n * @property {rotation_3d}  rotate   - Rotate.\n * @property {bool}  rotate.separate=On   - Separate.\n * @property {doublevb}  rotate.anglex=0   - Angle_x.\n * @property {doublevb}  rotate.angley=0   - Angle_y.\n * @property {doublevb}  rotate.anglez=0   - Angle_z.\n * @property {double}  skewx=0   - Skew X.\n * @property {double}  skewy=0   - Skew Y.\n * @property {double}  skewz=0   - Skew Z.\n * @property {bool}  inverted=false   - Invert Transformation.\n */\n\n\n/**\n * Attributes present in the node of type: 'GLCacheLock'\n * @name  NodeTypes#OpenGL-Cache-Lock\n * @property {bool}  composite_3d=false   - 3D.\n */\n\n\n/**\n * Attributes present in the node of type: 'PLUGIN'\n * @name  NodeTypes#Brightness-Contrast\n * @property {double}  b=0   - Brightness.\n * @property {double}  c=0   - Contrast.\n */\n\n\n/**\n * Attributes present in the node of type: 'PLUGIN'\n * @name  NodeTypes#Sparkle\n * @property {double}  angle=0   - Start angle.\n * @property {double}  scale=1   - Scale.\n * @property {double}  factor=0.7500   - Factor.\n * @property {double}  density=50   - Density.\n * @property {int}  n_points=8   - Number of Points.\n * @property {double}  prob_app=100   - Probability of Appearing.\n * @property {double}  point_noise=0   - Point Noise.\n * @property {double}  center_noise=0   - Center Noise.\n * @property {double}  angle_noise=0   - Angle Noise.\n * @property {int}  seed=0   - Random Seed.\n * @property {bool}  use_drawing_color=true   - Use Drawing Colours.\n * @property {bool}  flatten_sparkles_of_same_color=true   - Flatten Sparkles of Same Colour.\n * @property {color}  sparkle_color=80ffffff   - Sparkles' Colour.\n * @property {int}  sparkle_color.red=255   - Red.\n * @property {int}  sparkle_color.green=255   - Green.\n * @property {int}  sparkle_color.blue=255   - Blue.\n * @property {int}  sparkle_color.alpha=128   - Alpha.\n * @property {generic_enum}  [sparkle_color.preferred_ui=\"Separate\"]   - Preferred Editor.\n */\n\n\n/**\n * Attributes present in the node of type: 'WeightedDeform'\n * @name  NodeTypes#Weighted-Deform\n * @property {double}  blend=0   - Pre-Blend Amount.\n * @property {double}  postblend=0   - Post-Blend Amount.\n * @property {generic_enum}  [deformationquality=\"Very High\"]   - Deformation Quality.\n */\n\n\n/**\n * Attributes present in the node of type: 'BurnIn'\n * @name  NodeTypes#Burn-In\n * @property {bool}  drawbackgroundbox=false   - Add background box.\n * @property {color}  textcolor=ffffffff   - Text colour.\n * @property {int}  textcolor.red=255   - Red.\n * @property {int}  textcolor.green=255   - Green.\n * @property {int}  textcolor.blue=255   - Blue.\n * @property {int}  textcolor.alpha=255   - Alpha.\n * @property {generic_enum}  [textcolor.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {color}  backgroundcolor=ff000000   - Background colour.\n * @property {int}  backgroundcolor.red=0   - Red.\n * @property {int}  backgroundcolor.green=0   - Green.\n * @property {int}  backgroundcolor.blue=0   - Blue.\n * @property {int}  backgroundcolor.alpha=255   - Alpha.\n * @property {generic_enum}  [backgroundcolor.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {int}  size=36   - Size.\n * @property {int}  frameoffset=0   - Frame Offset.\n * @property {bool}  drawframenumber=true   - Frame Number.\n * @property {bool}  drawtimecode=true   - Time code.\n * @property {string}  [printinfo=\"Environment %e Job %j Scene %s\"]   - Scene Name.\n * @property {generic_enum}  [alignment=\"Left\"]   - Alignment.\n * @property {int}  font=Arial   - Font.\n */\n\n\n/**\n * Attributes present in the node of type: 'TransformLimit'\n * @name  NodeTypes#Transformation-Limit\n * @property {double}  active=100   - Active.\n * @property {generic_enum}  [switchtype=\"Active Value\"]   - Switch Effects.\n * @property {double}  tx=100   - Translate X.\n * @property {double}  ty=100   - Translate Y.\n * @property {double}  tz=100   - Translate Z.\n * @property {double}  rot=100   - Rotate.\n * @property {double}  skew=100   - Skew.\n * @property {double}  sx=100   - Scale X.\n * @property {double}  sy=100   - Scale Y.\n * @property {bool}  allowflip=true   - Allow Flipping.\n * @property {bool}  uniformscale=false   - Uniform Scale.\n * @property {double}  pignore=0   - Ignore Parents.\n * @property {string}  parentname   - Parent's Name.\n * @property {generic_enum}  [flattentype=\"Allow 3D Translate\"]   - Flatten Type.\n * @property {generic_enum}  [skewtype=\"Skew Optimized for Rotation\"]   - Skew Type.\n * @property {generic_enum}  [flipaxis=\"Allow Flip on X-Axis\"]   - Flip Axis.\n * @property {position_2d}  pos   - Control Position.\n * @property {bool}  pos.separate=On   - Separate.\n * @property {double}  pos.x=0   - Pos x.\n * @property {double}  pos.y=0   - Pos y.\n * @property {point_2d}  pos.2dpoint   - Point.\n */\n\n\n/**\n * Attributes present in the node of type: 'SCRIPT_MODULE'\n * @name  NodeTypes#Maya-Batch-Render\n * @property {string}  [specs_editor=\"\n<specs>\n  <ports>\n    <in type=\"IMAGE\"/>\n    <out type=\"IMAGE\"/>\n  </ports>\n  <attributes>\n    <attr type=\"string\" name=\"renderer\" value=\"\" tooltip=\"If this attribute is not set, then the MayaBatchRender node will use the default renderer specified in the Maya file. If this attribute is set, then it forces the use of a specific renderer other than the default. The following renderers are currently supported: 'renderMan' (renderMan version 22.0 or higher), 'renderManRIS' or 'RIS' (renderMan version 21.x or earlier), 'renderManReyes' or 'reyes' (renderMan version 20.x or earlier), 'arnold', 'mentalRay', 'mayaSoftware' or 'maya'. Note that those values are case insensitive.\"/>\n  </attributes>\n</specs>\n\"]   - Specifications.\n * @property {file_editor}  script_editor   - .\n * @property {file_editor}  init_script   - .\n * @property {file_editor}  cleanup_script   - .\n * @property {file_editor}  ui_script   - .\n * @property {string}  ui_data   - .\n * @property {file_library}  files   - .\n * @property {string}  renderer   - renderer.\n */\n\n\n/**\n * Attributes present in the node of type: 'FIELD_CHART'\n * @name  NodeTypes#Field-Chart\n * @property {bool}  enable_3d=false   - Enable 3D.\n * @property {bool}  face_camera=false   - Face Camera.\n * @property {generic_enum}  [camera_alignment=\"None\"]   - Camera Alignment.\n * @property {position_3d}  offset   - Position.\n * @property {bool}  offset.separate=On   - Separate.\n * @property {double}  offset.x=0   - Pos x.\n * @property {double}  offset.y=0   - Pos y.\n * @property {double}  offset.z=0   - Pos z.\n * @property {path_3d}  offset.3dpath   - Path.\n * @property {scale_3d}  scale   - Scale.\n * @property {bool}  scale.separate=On   - Separate.\n * @property {bool}  scale.in_fields=Off   - In fields.\n * @property {doublevb}  scale.xy=1   - Scale.\n * @property {doublevb}  scale.x=1   - Scale x.\n * @property {doublevb}  scale.y=1   - Scale y.\n * @property {doublevb}  scale.z=1   - Scale z.\n * @property {rotation_3d}  rotation   - Rotation.\n * @property {bool}  rotation.separate=Off   - Separate.\n * @property {doublevb}  rotation.anglex=0   - Angle_x.\n * @property {doublevb}  rotation.angley=0   - Angle_y.\n * @property {doublevb}  rotation.anglez=0   - Angle_z.\n * @property {quaternion_path}  rotation.quaternionpath   - Quaternion.\n * @property {alias}  angle=0   - Angle.\n * @property {double}  skew=0   - Skew.\n * @property {position_3d}  pivot   - Pivot.\n * @property {bool}  pivot.separate=On   - Separate.\n * @property {double}  pivot.x=0   - Pos x.\n * @property {double}  pivot.y=0   - Pos y.\n * @property {double}  pivot.z=0   - Pos z.\n * @property {position_3d}  spline_offset   - Spline Offset.\n * @property {bool}  spline_offset.separate=On   - Separate.\n * @property {double}  spline_offset.x=0   - Pos x.\n * @property {double}  spline_offset.y=0   - Pos y.\n * @property {double}  spline_offset.z=0   - Pos z.\n * @property {bool}  ignore_parent_peg_scaling=false   - Ignore Parent Scaling.\n * @property {bool}  disable_field_rendering=false   - Disable Field Rendering.\n * @property {int}  depth=0   - Depth.\n * @property {bool}  enable_min_max_angle=false   - Enable Min/Max Angle.\n * @property {double}  min_angle=-360   - Min Angle.\n * @property {double}  max_angle=360   - Max Angle.\n * @property {bool}  nail_for_children=false   - Nail for Children.\n * @property {bool}  ik_hold_orientation=false   - Hold Orientation in IK.\n * @property {bool}  ik_hold_x=false   - Hold X in IK.\n * @property {bool}  ik_hold_y=false   - Hold Y in IK.\n * @property {bool}  ik_excluded=false   - Is Excluded from IK.\n * @property {bool}  ik_can_rotate=true   - Can Rotate during IK.\n * @property {bool}  ik_can_translate_x=false   - Can Translate in X during IK.\n * @property {bool}  ik_can_translate_y=false   - Can Translate in Y during IK.\n * @property {double}  ik_bone_x=0.2000   - X Direction of Bone.\n * @property {double}  ik_bone_y=0   - Y Direction of Bone.\n * @property {double}  ik_stiffness=1   - Stiffness of Bone.\n * @property {drawing}  drawing   - Drawing.\n * @property {bool}  drawing.element_mode=On   - Element Mode.\n * @property {element}  drawing.element=unknown   - Element.\n * @property {string}  drawing.element.layer   - Layer.\n * @property {custom_name}  drawing.custom_name   - Custom Name.\n * @property {string}  drawing.custom_name.name   - Local Name.\n * @property {timing}  drawing.custom_name.timing   - Timing.\n * @property {string}  drawing.custom_name.extension=tga   - Extension.\n * @property {double}  drawing.custom_name.field_chart=12   - FieldChart.\n * @property {bool}  read_overlay=true   - Overlay Art Enabled.\n * @property {bool}  read_line_art=true   - Line Art Enabled.\n * @property {bool}  read_color_art=true   - Colour Art Enabled.\n * @property {bool}  read_underlay=true   - Underlay Art Enabled.\n * @property {generic_enum}  [overlay_art_drawing_mode=\"Vector\"]   - Overlay Art Type.\n * @property {generic_enum}  [line_art_drawing_mode=\"Vector\"]   - Line Art Type.\n * @property {generic_enum}  [color_art_drawing_mode=\"Vector\"]   - Colour Art Type.\n * @property {generic_enum}  [underlay_art_drawing_mode=\"Vector\"]   - Underlay Art Type.\n * @property {bool}  pencil_line_deformation_preserve_thickness=false   - Preserve Line Thickness.\n * @property {generic_enum}  [pencil_line_deformation_quality=\"Low\"]   - Pencil Lines Quality.\n * @property {int}  pencil_line_deformation_smooth=1   - Pencil Lines Smoothing.\n * @property {double}  pencil_line_deformation_fit_error=3   - Fit Error.\n * @property {bool}  read_color=true   - Colour.\n * @property {bool}  read_transparency=true   - Transparency.\n * @property {generic_enum}  [color_transformation=\"Linear\"]   - Colour Space.\n * @property {string}  color_space   - Colour Space.\n * @property {generic_enum}  [apply_matte_to_color=\"Premultiplied with Black\"]   - Transparency Type.\n * @property {bool}  enable_line_texture=true   - Enable Line Texture.\n * @property {generic_enum}  [antialiasing_quality=\"Medium\"]   - Antialiasing Quality.\n * @property {double}  antialiasing_exponent=1   - Antialiasing Exponent.\n * @property {double}  opacity=100   - Opacity.\n * @property {generic_enum}  [texture_filter=\"Nearest (Filtered)\"]   - Texture Filter.\n * @property {bool}  adjust_pencil_thickness=false   - Adjust Pencil Lines Thickness.\n * @property {bool}  normal_line_art_thickness=true   - Normal Thickness.\n * @property {generic_enum}  [zoom_independent_line_art_thickness=\"Scale Independent\"]   - Scale Independent.\n * @property {double}  mult_line_art_thickness=1   - Proportional.\n * @property {double}  add_line_art_thickness=0   - Constant.\n * @property {double}  min_line_art_thickness=0   - Minimum.\n * @property {double}  max_line_art_thickness=0   - Maximum.\n * @property {generic_enum}  [use_drawing_pivot=\"Apply Embedded Pivot on Drawing Layer\"]   - Use Embedded Pivots.\n * @property {bool}  flip_hor=false   - Flip Horizontal.\n * @property {bool}  flip_vert=false   - Flip Vertical.\n * @property {bool}  turn_before_alignment=false   - Turn Before Alignment.\n * @property {bool}  no_clipping=false   - No Clipping.\n * @property {int}  x_clip_factor=0   - Clipping Factor (x).\n * @property {int}  y_clip_factor=0   - Clipping Factor (y).\n * @property {generic_enum}  [alignment_rule=\"Center First Page\"]   - Alignment Rule.\n * @property {double}  morphing_velo=0   - Morphing Velocity.\n * @property {bool}  can_animate=true   - Animate Using Animation Tools.\n * @property {bool}  tile_horizontal=false   - Tile Horizontally.\n * @property {bool}  tile_vertical=false   - Tile Vertically.\n * @property {generic_enum}  [size=\"12\"]   - Size.\n * @property {bool}  opaque=false   - Opaque.\n */\n\n\n/**\n * Attributes present in the node of type: 'READ'\n * @name  NodeTypes#Drawing\n * @property {bool}  enable_3d=false   - Enable 3D.\n * @property {bool}  face_camera=false   - Face Camera.\n * @property {generic_enum}  [camera_alignment=\"None\"]   - Camera Alignment.\n * @property {position_3d}  offset   - Position.\n * @property {bool}  offset.separate=On   - Separate.\n * @property {double}  offset.x=0   - Pos x.\n * @property {double}  offset.y=0   - Pos y.\n * @property {double}  offset.z=0   - Pos z.\n * @property {path_3d}  offset.3dpath   - Path.\n * @property {scale_3d}  scale   - Scale.\n * @property {bool}  scale.separate=On   - Separate.\n * @property {bool}  scale.in_fields=Off   - In fields.\n * @property {doublevb}  scale.xy=1   - Scale.\n * @property {doublevb}  scale.x=1   - Scale x.\n * @property {doublevb}  scale.y=1   - Scale y.\n * @property {doublevb}  scale.z=1   - Scale z.\n * @property {rotation_3d}  rotation   - Rotation.\n * @property {bool}  rotation.separate=Off   - Separate.\n * @property {doublevb}  rotation.anglex=0   - Angle_x.\n * @property {doublevb}  rotation.angley=0   - Angle_y.\n * @property {doublevb}  rotation.anglez=0   - Angle_z.\n * @property {quaternion_path}  rotation.quaternionpath   - Quaternion.\n * @property {alias}  angle=0   - Angle.\n * @property {double}  skew=0   - Skew.\n * @property {position_3d}  pivot   - Pivot.\n * @property {bool}  pivot.separate=On   - Separate.\n * @property {double}  pivot.x=0   - Pos x.\n * @property {double}  pivot.y=0   - Pos y.\n * @property {double}  pivot.z=0   - Pos z.\n * @property {position_3d}  spline_offset   - Spline Offset.\n * @property {bool}  spline_offset.separate=On   - Separate.\n * @property {double}  spline_offset.x=0   - Pos x.\n * @property {double}  spline_offset.y=0   - Pos y.\n * @property {double}  spline_offset.z=0   - Pos z.\n * @property {bool}  ignore_parent_peg_scaling=false   - Ignore Parent Scaling.\n * @property {bool}  disable_field_rendering=false   - Disable Field Rendering.\n * @property {int}  depth=0   - Depth.\n * @property {bool}  enable_min_max_angle=false   - Enable Min/Max Angle.\n * @property {double}  min_angle=-360   - Min Angle.\n * @property {double}  max_angle=360   - Max Angle.\n * @property {bool}  nail_for_children=false   - Nail for Children.\n * @property {bool}  ik_hold_orientation=false   - Hold Orientation in IK.\n * @property {bool}  ik_hold_x=false   - Hold X in IK.\n * @property {bool}  ik_hold_y=false   - Hold Y in IK.\n * @property {bool}  ik_excluded=false   - Is Excluded from IK.\n * @property {bool}  ik_can_rotate=true   - Can Rotate during IK.\n * @property {bool}  ik_can_translate_x=false   - Can Translate in X during IK.\n * @property {bool}  ik_can_translate_y=false   - Can Translate in Y during IK.\n * @property {double}  ik_bone_x=0.2000   - X Direction of Bone.\n * @property {double}  ik_bone_y=0   - Y Direction of Bone.\n * @property {double}  ik_stiffness=1   - Stiffness of Bone.\n * @property {drawing}  drawing=C:/Users/mathieuc/Documents/NewScene/elements/Drawing/Drawing-1.tvg   - Drawing.\n * @property {bool}  drawing.element_mode=On   - Element Mode.\n * @property {element}  drawing.element=unknown   - Element.\n * @property {string}  drawing.element.layer   - Layer.\n * @property {custom_name}  drawing.custom_name   - Custom Name.\n * @property {string}  drawing.custom_name.name   - Local Name.\n * @property {timing}  drawing.custom_name.timing   - Timing.\n * @property {string}  drawing.custom_name.extension=tga   - Extension.\n * @property {double}  drawing.custom_name.field_chart=12   - FieldChart.\n * @property {bool}  read_overlay=true   - Overlay Art Enabled.\n * @property {bool}  read_line_art=true   - Line Art Enabled.\n * @property {bool}  read_color_art=true   - Colour Art Enabled.\n * @property {bool}  read_underlay=true   - Underlay Art Enabled.\n * @property {generic_enum}  [overlay_art_drawing_mode=\"Vector\"]   - Overlay Art Type.\n * @property {generic_enum}  [line_art_drawing_mode=\"Vector\"]   - Line Art Type.\n * @property {generic_enum}  [color_art_drawing_mode=\"Vector\"]   - Colour Art Type.\n * @property {generic_enum}  [underlay_art_drawing_mode=\"Vector\"]   - Underlay Art Type.\n * @property {bool}  pencil_line_deformation_preserve_thickness=false   - Preserve Line Thickness.\n * @property {generic_enum}  [pencil_line_deformation_quality=\"Low\"]   - Pencil Lines Quality.\n * @property {int}  pencil_line_deformation_smooth=1   - Pencil Lines Smoothing.\n * @property {double}  pencil_line_deformation_fit_error=3   - Fit Error.\n * @property {bool}  read_color=true   - Colour.\n * @property {bool}  read_transparency=true   - Transparency.\n * @property {generic_enum}  [color_transformation=\"Linear\"]   - Colour Space.\n * @property {string}  color_space   - Colour Space.\n * @property {generic_enum}  [apply_matte_to_color=\"Premultiplied with Black\"]   - Transparency Type.\n * @property {bool}  enable_line_texture=true   - Enable Line Texture.\n * @property {generic_enum}  [antialiasing_quality=\"High\"]   - Antialiasing Quality.\n * @property {double}  antialiasing_exponent=1   - Antialiasing Exponent.\n * @property {double}  opacity=100   - Opacity.\n * @property {generic_enum}  [texture_filter=\"Nearest (Filtered)\"]   - Texture Filter.\n * @property {bool}  adjust_pencil_thickness=false   - Adjust Pencil Lines Thickness.\n * @property {bool}  normal_line_art_thickness=true   - Normal Thickness.\n * @property {generic_enum}  [zoom_independent_line_art_thickness=\"Scale Independent\"]   - Scale Independent.\n * @property {double}  mult_line_art_thickness=1   - Proportional.\n * @property {double}  add_line_art_thickness=0   - Constant.\n * @property {double}  min_line_art_thickness=0   - Minimum.\n * @property {double}  max_line_art_thickness=0   - Maximum.\n * @property {generic_enum}  [use_drawing_pivot=\"Apply Embedded Pivot on Drawing Layer\"]   - Use Embedded Pivots.\n * @property {bool}  flip_hor=false   - Flip Horizontal.\n * @property {bool}  flip_vert=false   - Flip Vertical.\n * @property {bool}  turn_before_alignment=false   - Turn Before Alignment.\n * @property {bool}  no_clipping=false   - No Clipping.\n * @property {int}  x_clip_factor=0   - Clipping Factor (x).\n * @property {int}  y_clip_factor=0   - Clipping Factor (y).\n * @property {generic_enum}  [alignment_rule=\"Center First Page\"]   - Alignment Rule.\n * @property {double}  morphing_velo=0   - Morphing Velocity.\n * @property {bool}  can_animate=false   - Animate Using Animation Tools.\n * @property {bool}  tile_horizontal=false   - Tile Horizontally.\n * @property {bool}  tile_vertical=false   - Tile Vertically.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'Shake'\n * @name  NodeTypes#Shake\n * @property {double}  frequency=0.3000   - Frequency.\n * @property {int}  octaves=2   - Octaves.\n * @property {double}  multiplier=0.5000   - Multiplier.\n * @property {double}  positionx=1   - Position X.\n * @property {double}  positiony=1.3300   - Position Y.\n * @property {double}  positionz=0.1000   - Position Z.\n * @property {double}  rotationx=0   - Rotation X.\n * @property {double}  rotationy=0   - Rotation Y.\n * @property {double}  rotationz=1   - Rotation Z.\n * @property {double}  pivotx=0   - Pivot X.\n * @property {double}  pivoty=0   - Pivot Y.\n * @property {double}  pivotz=0   - Pivot Z.\n * @property {int}  steps=1   - Steps.\n * @property {int}  seed=0   - Random Seed.\n */\n\n\n/**\n * Attributes present in the node of type: 'QUADMAP'\n * @name  NodeTypes#Quadmap\n * @property {position_2d}  src_point_1   - Source Point 1.\n * @property {bool}  src_point_1.separate=On   - Separate.\n * @property {double}  src_point_1.x=-12   - Pos x.\n * @property {double}  src_point_1.y=12   - Pos y.\n * @property {point_2d}  src_point_1.2dpoint   - Point.\n * @property {position_2d}  src_point_2   - Source Point 2.\n * @property {bool}  src_point_2.separate=On   - Separate.\n * @property {double}  src_point_2.x=12   - Pos x.\n * @property {double}  src_point_2.y=12   - Pos y.\n * @property {point_2d}  src_point_2.2dpoint   - Point.\n * @property {position_2d}  src_point_3   - Source Point 3.\n * @property {bool}  src_point_3.separate=On   - Separate.\n * @property {double}  src_point_3.x=-12   - Pos x.\n * @property {double}  src_point_3.y=-12   - Pos y.\n * @property {point_2d}  src_point_3.2dpoint   - Point.\n * @property {position_2d}  src_point_4   - Source Point 4.\n * @property {bool}  src_point_4.separate=On   - Separate.\n * @property {double}  src_point_4.x=12   - Pos x.\n * @property {double}  src_point_4.y=-12   - Pos y.\n * @property {point_2d}  src_point_4.2dpoint   - Point.\n * @property {position_2d}  point_1   - Destination Point 1.\n * @property {bool}  point_1.separate=On   - Separate.\n * @property {double}  point_1.x=-12   - Pos x.\n * @property {double}  point_1.y=12   - Pos y.\n * @property {point_2d}  point_1.2dpoint   - Point.\n * @property {position_2d}  point_2   - Destination Point 2.\n * @property {bool}  point_2.separate=On   - Separate.\n * @property {double}  point_2.x=12   - Pos x.\n * @property {double}  point_2.y=12   - Pos y.\n * @property {point_2d}  point_2.2dpoint   - Point.\n * @property {position_2d}  point_3   - Destination Point 3.\n * @property {bool}  point_3.separate=On   - Separate.\n * @property {double}  point_3.x=-12   - Pos x.\n * @property {double}  point_3.y=-12   - Pos y.\n * @property {point_2d}  point_3.2dpoint   - Point.\n * @property {position_2d}  point_4   - Destination Point 4.\n * @property {bool}  point_4.separate=On   - Separate.\n * @property {double}  point_4.x=12   - Pos x.\n * @property {double}  point_4.y=-12   - Pos y.\n * @property {point_2d}  point_4.2dpoint   - Point.\n * @property {position_2d}  pivot   - Pivot.\n * @property {bool}  pivot.separate=On   - Separate.\n * @property {double}  pivot.x=0   - Pos x.\n * @property {double}  pivot.y=0   - Pos y.\n */\n\n\n/**\n * Attributes present in the node of type: 'RADIALBLUR-PLUGIN'\n * @name  NodeTypes#Blur-Radial-Zoom\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {bool}  truck_factor=true   - Truck Factor.\n * @property {bool}  bidirectional=true   - Bidirectional.\n * @property {generic_enum}  [precision=\"Medium 8\"]   - Precision.\n * @property {double}  blurriness=0   - Blurriness.\n * @property {double}  fall_off=0   - Fall Off.\n * @property {double}  spiral=0   - Custom.\n * @property {position_2d}  focus   - Focus.\n * @property {bool}  focus.separate=On   - Separate.\n * @property {double}  focus.x=0   - Pos x.\n * @property {double}  focus.y=0   - Pos y.\n * @property {point_2d}  focus.2dpoint   - Point.\n * @property {generic_enum}  [smoothness=\"Quadratic\"]   - Variation.\n * @property {double}  quality=1   - Quality.\n * @property {bool}  legacy=false   - Legacy.\n */\n\n\n/**\n * Attributes present in the node of type: 'CAMERA'\n * @name  NodeTypes#Camera\n * @property {position_3d}  offset   - Offset.\n * @property {bool}  offset.separate=On   - Separate.\n * @property {double}  offset.x=0   - Pos x.\n * @property {double}  offset.y=0   - Pos y.\n * @property {double}  offset.z=12   - Pos z.\n * @property {position_2d}  pivot   - Pivot.\n * @property {bool}  pivot.separate=On   - Separate.\n * @property {double}  pivot.x=0   - Pos x.\n * @property {double}  pivot.y=0   - Pos y.\n * @property {doublevb}  angle=0   - Angle.\n * @property {bool}  override_scene_fov=false   - Override Scene Fov.\n * @property {doublevb}  fov=41.1121   - FOV.\n * @property {double}  near_plane=0.1000   - Near Plane.\n * @property {double}  far_plane=1000   - Far Plane.\n */\n\n\n/**\n * Attributes present in the node of type: 'COMPOSITE'\n * @name  NodeTypes#Composite\n * @property {generic_enum}  [composite_mode=\"Pass Through\"]   - Mode.\n * @property {bool}  flatten_output=false   - Flatten Output.\n * @property {bool}  flatten_vector=false   - Vector Flatten Output.\n * @property {bool}  composite_2d=false   - 2D.\n * @property {bool}  composite_3d=false   - 3D.\n * @property {generic_enum}  [output_z=\"Leftmost\"]   - Output Z.\n * @property {int}  output_z_input_port=1   - Port For Output Z.\n * @property {bool}  apply_focus=true   - Apply Focus.\n * @property {double}  multiplier=1   - Focus Multiplier.\n * @property {string}  tvg_palette=compositedPalette   - Palette Name.\n * @property {bool}  merge_vector=false   - Flatten.\n */\n\n\n/**\n * Attributes present in the node of type: 'CastShadow'\n * @name  NodeTypes#Cast-Shadow\n * @property {generic_enum}  [lighttype=\"Point\"]   - Light Type.\n * @property {generic_enum}  [shadetype=\"Smooth\"]   - Shading Type.\n * @property {double}  anglebias=0   - Angle Bias.\n * @property {double}  shadowdarkness=100   - Shadow Darkness.\n * @property {double}  shadowlength=0   - Shadow Length.\n * @property {double}  shadowbias=0   - Shadow Bias.\n * @property {int}  samplecount=1   - Sample Count.\n * @property {double}  samplesize=0.5000   - Sample Size.\n * @property {int}  softcount=1   - Soft Shadow Sample Count.\n * @property {double}  softsize=1   - Soft Shadow Sample Size.\n * @property {double}  softweight=1   - Soft Shadow Weight.\n * @property {double}  postblur=0   - Final Blur.\n * @property {double}  postblurthreshold=0   - Final Blur Threshold.\n * @property {double}  shadownoise=0   - Shadow Noise.\n * @property {int}  expandsource=0   - Expand Render Source.\n * @property {double}  shadowgamma=1   - Shadow Gamma.\n * @property {int}  antialiasbias=1   - Anti-Alias Bias.\n * @property {int}  antialiasblend=1   - Anti-Alias Blend.\n * @property {int}  antialiasblur=1   - Anti-Alias Blur.\n * @property {double}  exponent=2   - Abruptness.\n * @property {color}  color=ff000000   - Light Colour.\n * @property {int}  color.red=0   - Red.\n * @property {int}  color.green=0   - Green.\n * @property {int}  color.blue=0   - Blue.\n * @property {int}  color.alpha=255   - Alpha.\n * @property {generic_enum}  [color.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  useimagecolor=false   - Use Image Colour.\n * @property {double}  imagecolorweight=50   - Image Colour Intensity.\n * @property {bool}  usetruckfactor=true   - Consider Truck Factor.\n * @property {bool}  polarmethod=false   - Use Polar Method.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'PRECOMP'\n * @name  NodeTypes#Pre-render-Cache\n * @property {generic_enum}  [composite_mode=\"As Bitmap\"]   - Mode.\n * @property {bool}  flatten_output=true   - Flatten Output.\n * @property {bool}  flatten_vector=false   - Vector Flatten Output.\n * @property {bool}  composite_2d=false   - 2D.\n * @property {bool}  composite_3d=false   - 3D.\n * @property {generic_enum}  [output_z=\"Leftmost\"]   - Output Z.\n * @property {int}  output_z_input_port=1   - Port For Output Z.\n * @property {bool}  apply_focus=true   - Apply Focus.\n * @property {double}  multiplier=1   - Focus Multiplier.\n * @property {string}  tvg_palette=compositedPalette   - Palette Name.\n * @property {bool}  merge_vector=false   - Flatten.\n * @property {bool}  prerender_current_frame=false   - Current Frame.\n * @property {bool}  prerender_selected_frames=false   - Selected Frames.\n * @property {int}  prerender_frames_from=1   - From.\n * @property {int}  prerender_frames_to=1   - To.\n * @property {push_button}  render_prerender_cache   - Render.\n * @property {push_button}  clear_prerender_cache   - Clear.\n */\n\n\n/**\n * Attributes present in the node of type: 'MATTE_COMPOSITE'\n * @name  NodeTypes#Matte-Composite\n */\n\n\n/**\n * Attributes present in the node of type: 'TransformationSwitch'\n * @name  NodeTypes#Transformation-Switch\n * @property {drawing}  drawing   - Drawing.\n * @property {bool}  drawing.element_mode=On   - Element Mode.\n * @property {element}  drawing.element=unknown   - Element.\n * @property {string}  drawing.element.layer   - Layer.\n * @property {custom_name}  drawing.custom_name   - Custom Name.\n * @property {string}  drawing.custom_name.name   - Local Name.\n * @property {timing}  drawing.custom_name.timing   - Timing.\n * @property {string}  drawing.custom_name.extension=tga   - Extension.\n * @property {double}  drawing.custom_name.field_chart=12   - FieldChart.\n * @property {array_string}  transformationnames=2;;   - Transformation Names.\n * @property {int}  transformationnames.size=2   - Size.\n * @property {string}  transformationnames.transformation_1   - Transformation 1.\n * @property {string}  transformationnames.transformation_2   - Transformation 2.\n */\n\n\n/**\n * Attributes present in the node of type: 'MOTION_BLUR'\n * @name  NodeTypes#Motion-Blur-Legacy\n * @property {double}  nb_frames_trail=10   - Number of Frames in the Trail.\n * @property {double}  samples=200   - Number of Samples.\n * @property {double}  falloff=2   - Fall-off Rate.\n * @property {double}  intensity=1   - Intensity.\n * @property {bool}  mirror=false   - Use Mirror on Edges.\n */\n\n\n/**\n * Attributes present in the node of type: 'BLUR_VARIABLE'\n * @name  NodeTypes#Blur-Variable\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {double}  black_radius=0   - Black radius.\n * @property {double}  white_radius=0   - White radius.\n * @property {generic_enum}  [quality=\"High\"]   - Quality.\n * @property {bool}  keep_inside_source_image=false   - Keep inside source image.\n */\n\n\n/**\n * Attributes present in the node of type: 'LAYER_SELECTOR'\n * @name  NodeTypes#Layer-Selector\n * @property {bool}  flatten=false   - Flatten.\n * @property {bool}  apply_to_matte_ports=false   - Apply to Matte Ports on Input Effects.\n * @property {generic_enum}  [antialiasing_quality=\"Ignore\"]   - Antialiasing Quality.\n * @property {double}  antialiasing_exponent=1   - Antialiasing Exponent.\n * @property {bool}  read_overlay=false   - Read Overlay.\n * @property {bool}  read_lineart=true   - Read LineArt.\n * @property {bool}  read_colourart=true   - Read ColourArt.\n * @property {bool}  read_underlay=false   - Read Underlay.\n */\n\n\n/**\n * Attributes present in the node of type: 'MOTIONBLUR-PLUGIN'\n * @name  NodeTypes#Motion-Blur\n * @property {double}  nb_frames_trail=1   - Duration (Number of Frames).\n * @property {int}  samples=30   - Samples per Frame.\n * @property {double}  falloff=0   - Fall-off Rate.\n * @property {bool}  use_camera_transformation=false   - Use Camera Motion.\n * @property {bool}  preroll_motion=true   - Preroll Motion.\n */\n\n\n/**\n * Attributes present in the node of type: 'COLOR_CARD'\n * @name  NodeTypes#Colour-Card\n * @property {int}  depth=0   - Depth.\n * @property {double}  offset_z=-12   - Offset Z.\n * @property {color}  color=ffffffff   - Color.\n * @property {int}  color.red=255   - Red.\n * @property {int}  color.green=255   - Green.\n * @property {int}  color.blue=255   - Blue.\n * @property {int}  color.alpha=255   - Alpha.\n * @property {generic_enum}  [color.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'TransformLoop'\n * @name  NodeTypes#Transform-Loop\n * @property {bool}  autorange=true   - Automatic Range Detection.\n * @property {int}  rangestart=1   -     Start.\n * @property {int}  rangeend=1   -     End.\n * @property {generic_enum}  [looptype=\"Repeat\"]   - Loop Type.\n */\n\n\n/**\n * Attributes present in the node of type: 'COLOR_MASK'\n * @name  NodeTypes#Channel-Selector\n * @property {bool}  red=true   - Red.\n * @property {bool}  green=true   - Green.\n * @property {bool}  blue=true   - Blue.\n * @property {bool}  alpha=true   - Alpha.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'SCALE'\n * @name  NodeTypes#Scale-Output\n * @property {bool}  by_value=true   - Custom Resolution.\n * @property {string}  resolution_name   - Resolution Name.\n * @property {int}  res_x=720   - Width.\n * @property {int}  res_y=540   - Height.\n */\n\n\n/**\n * Attributes present in the node of type: 'COLOR_LEVELS'\n * @name  NodeTypes#Colour-Levels\n * @property {levels-channel-params-attribute}  rgb   - RGB.\n * @property {double}  rgb.input_black=0   - Input Black.\n * @property {double}  rgb.input_white=1   - Input White.\n * @property {double}  rgb.gamma=1   - Gamma.\n * @property {double}  rgb.output_black=0   - Output Black.\n * @property {double}  rgb.output_white=1   - Output White.\n * @property {levels-channel-params-attribute}  red   - Red.\n * @property {double}  red.input_black=0   - Input Black.\n * @property {double}  red.input_white=1   - Input White.\n * @property {double}  red.gamma=1   - Gamma.\n * @property {double}  red.output_black=0   - Output Black.\n * @property {double}  red.output_white=1   - Output White.\n * @property {levels-channel-params-attribute}  green   - Green.\n * @property {double}  green.input_black=0   - Input Black.\n * @property {double}  green.input_white=1   - Input White.\n * @property {double}  green.gamma=1   - Gamma.\n * @property {double}  green.output_black=0   - Output Black.\n * @property {double}  green.output_white=1   - Output White.\n * @property {levels-channel-params-attribute}  blue   - Blue.\n * @property {double}  blue.input_black=0   - Input Black.\n * @property {double}  blue.input_white=1   - Input White.\n * @property {double}  blue.gamma=1   - Gamma.\n * @property {double}  blue.output_black=0   - Output Black.\n * @property {double}  blue.output_white=1   - Output White.\n * @property {levels-channel-params-attribute}  alpha   - Alpha.\n * @property {double}  alpha.input_black=0   - Input Black.\n * @property {double}  alpha.input_white=1   - Input White.\n * @property {double}  alpha.gamma=1   - Gamma.\n * @property {double}  alpha.output_black=0   - Output Black.\n * @property {double}  alpha.output_white=1   - Output White.\n */\n\n\n/**\n * Attributes present in the node of type: 'COLOR_FADE'\n * @name  NodeTypes#Colour-Fade\n * @property {double}  fadefactor=100   - Fade.\n * @property {generic_enum}  [colorspace=\"RGB\"]   - Colour Interpolation.\n * @property {generic_enum}  [hueinterpolation=\"Linear\"]   - Hue Interpolation.\n */\n\n\n/**\n * Attributes present in the node of type: 'Gamma'\n * @name  NodeTypes#Gamma\n * @property {double}  rgb_gamma=1   - RGB Gamma.\n * @property {double}  red_gamma=1   - Red Gamma.\n * @property {double}  green_gamma=1   - Green Gamma.\n * @property {double}  blue_gamma=1   - Blue Gamma.\n * @property {double}  alpha_gamma=1   - Alpha Gamma.\n */\n\n\n/**\n * Attributes present in the node of type: 'DITHER'\n * @name  NodeTypes#Dither\n * @property {double}  magnitude=1   - Magnitude.\n * @property {bool}  correlate=false   - Correlate.\n * @property {bool}  random=true   - Random.\n */\n\n\n/**\n * Attributes present in the node of type: 'GRAIN'\n * @name  NodeTypes#Grain\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {double}  noise=0.3000   - Noise.\n * @property {double}  smooth=0   - Smooth.\n * @property {bool}  random=true   - Random At Each Frame.\n * @property {int}  seed=0   - Seed Value.\n */\n\n\n/**\n * Attributes present in the node of type: 'ShapeLine'\n * @name  NodeTypes#Shape-Line\n * @property {bool}  flattenz=true   - Flatten Z.\n * @property {position_3d}  position1   - Position 1.\n * @property {bool}  position1.separate=On   - Separate.\n * @property {double}  position1.x=0   - Pos x.\n * @property {double}  position1.y=0   - Pos y.\n * @property {double}  position1.z=0   - Pos z.\n * @property {path_3d}  position1.3dpath   - Path.\n * @property {bool}  closeshape=false   - Close Shape Contour.\n */\n\n\n/**\n * Attributes present in the node of type: 'COMPOSITE_GENERIC'\n * @name  NodeTypes#Composite-Generic\n * @property {generic_enum}  [color_operation=\"Apply With Alpha\"]   - Colour Operation.\n * @property {double}  intensity_color_red=1   - Intensity Red.\n * @property {double}  intensity_color_blue=1   - Intensity Blue.\n * @property {double}  intensity_color_green=1   - Intensity Green.\n * @property {double}  opacity=100   - Opacity.\n * @property {generic_enum}  [alpha_operation=\"Apply\"]   - Alpha Operation.\n * @property {generic_enum}  [output_z=\"Leftmost\"]   - Output Z.\n * @property {int}  output_z_input_port=1   - Port For Output Z.\n */\n\n\n/**\n * Attributes present in the node of type: 'COLOR_ART'\n * @name  NodeTypes#Colour-Art\n * @property {bool}  flatten=false   - Flatten.\n * @property {bool}  apply_to_matte_ports=false   - Apply to Matte Ports on Input Effects.\n * @property {generic_enum}  [antialiasing_quality=\"Ignore\"]   - Antialiasing Quality.\n * @property {double}  antialiasing_exponent=1   - Antialiasing Exponent.\n */\n\n\n/**\n * Attributes present in the node of type: 'UNDERLAY'\n * @name  NodeTypes#Underlay-Layer\n * @property {bool}  flatten=false   - Flatten.\n * @property {bool}  apply_to_matte_ports=false   - Apply to Matte Ports on Input Effects.\n * @property {generic_enum}  [antialiasing_quality=\"Ignore\"]   - Antialiasing Quality.\n * @property {double}  antialiasing_exponent=1   - Antialiasing Exponent.\n */\n\n\n/**\n * Attributes present in the node of type: 'CUTTER'\n * @name  NodeTypes#Cutter\n * @property {bool}  inverted=false   - Inverted.\n */\n\n\n/**\n * Attributes present in the node of type: 'FOCUS_APPLY'\n * @name  NodeTypes#Focus-Multiplier\n * @property {double}  multiplier=1   - Multiplier.\n */\n\n\n/**\n * Attributes present in the node of type: 'PEG_APPLY3'\n * @name  NodeTypes#Apply-Image-Transformation\n */\n\n\n/**\n * Attributes present in the node of type: 'PEG_APPLY3_V2'\n * @name  NodeTypes#Apply-Peg-Transformation\n */\n\n\n/**\n * Attributes present in the node of type: 'LINE_ART'\n * @name  NodeTypes#Line-Art\n * @property {bool}  flatten=false   - Flatten.\n * @property {bool}  apply_to_matte_ports=false   - Apply to Matte Ports on Input Effects.\n * @property {generic_enum}  [antialiasing_quality=\"Ignore\"]   - Antialiasing Quality.\n * @property {double}  antialiasing_exponent=1   - Antialiasing Exponent.\n */\n\n\n/**\n * Attributes present in the node of type: 'OVERLAY'\n * @name  NodeTypes#Overlay-Layer\n * @property {bool}  flatten=false   - Flatten.\n * @property {bool}  apply_to_matte_ports=false   - Apply to Matte Ports on Input Effects.\n * @property {generic_enum}  [antialiasing_quality=\"Ignore\"]   - Antialiasing Quality.\n * @property {double}  antialiasing_exponent=1   - Antialiasing Exponent.\n */\n\n\n/**\n * Attributes present in the node of type: 'AutoPatchModule'\n * @name  NodeTypes#Auto-Patch\n */\n\n\n/**\n * Attributes present in the node of type: 'SubNodeAnimationFilter'\n * @name  NodeTypes#3D-Kinematic-Output\n * @property {string}  sub_node_name   - Subnode Name.\n */\n\n\n/**\n * Attributes present in the node of type: 'BOXBLUR-PLUGIN'\n * @name  NodeTypes#Blur-Box\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {bool}  truck_factor=true   - Truck Factor.\n * @property {bool}  bidirectional=true   - Bidirectional.\n * @property {generic_enum}  [precision=\"Medium 8\"]   - Precision.\n * @property {bool}  repeat_edge_pixels=false   - Repeat Edge Pixels.\n * @property {bool}  directional=false   - Directional.\n * @property {double}  angle=0   - Angle.\n * @property {int}  iterations=1   - Number of Iterations.\n * @property {double}  radius=0   - Radius.\n * @property {double}  width=0   - Width.\n * @property {double}  length=0   - Length.\n * @property {double}  fall_off=0   - Fall Off.\n */\n\n\n/**\n * Attributes present in the node of type: 'RGB_DIFFERENCE_KEYER'\n * @name  NodeTypes#RGB-Difference-Keyer\n * @property {color}  color=ff000000   - Colour.\n * @property {int}  color.red=0   - Red.\n * @property {int}  color.green=0   - Green.\n * @property {int}  color.blue=0   - Blue.\n * @property {int}  color.alpha=255   - Alpha.\n * @property {generic_enum}  [color.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  pre_multiplied_alpha=true   - Pre-multiplied Alpha.\n * @property {double}  key_difference_weighting=50   - Key Weight.\n * @property {double}  edge_key_threshold=0   - Background Bias.\n * @property {double}  key_primary_spill_reduction=0   - Spill Reduction.\n * @property {double}  key_low_intensity_scaled_thresh=100   - Low Intensity Threshold.\n * @property {bool}  blend_input_alpha=true   - Blend Input Alpha.\n * @property {bool}  despill_matte=true   - Despill Matte.\n * @property {color}  spill_color=ff000000   - Spill Replace Colour.\n * @property {int}  spill_color.red=0   - Red.\n * @property {int}  spill_color.green=0   - Green.\n * @property {int}  spill_color.blue=0   - Blue.\n * @property {int}  spill_color.alpha=255   - Alpha.\n * @property {generic_enum}  [spill_color.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {double}  matte_clip_black=0   - Clip Black.\n * @property {double}  matte_clip_white=100   - Clip White.\n * @property {bool}  invert_matte=false   - Invert Output Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'CHANNEL_SWAP'\n * @name  NodeTypes#Channel-Swap\n * @property {generic_enum}  [redchannelselection=\"Red\"]   - Red Channel From.\n * @property {generic_enum}  [greenchannelselection=\"Green\"]   - Green Channel From.\n * @property {generic_enum}  [bluechannelselection=\"Blue\"]   - Blue Channel From.\n * @property {generic_enum}  [alphachannelselection=\"Alpha\"]   - Alpha Channel From.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'FADE'\n * @name  NodeTypes#Transparency\n * @property {double}  transparency=50   - Transparency.\n */\n\n\n/**\n * Attributes present in the node of type: 'OGLBYPASS'\n * @name  NodeTypes#OpenGL-Bypass\n */\n\n\n/**\n * Attributes present in the node of type: 'NOTE'\n * @name  NodeTypes#Note\n * @property {string}  text   - Text.\n */\n\n\n/**\n * Attributes present in the node of type: 'OpenGLPreview'\n * @name  NodeTypes#RenderPreview\n * @property {generic_enum}  [refreshstrategy=\"Current Frame Only\"]   - Render.\n * @property {generic_enum}  [scaling=\"Use Render Preview Setting\"]   - Preview Resolution.\n * @property {generic_enum}  [renderstrategy=\"Use Previously Rendered\"]   - Outdated Images Mode.\n * @property {push_button}  computeallimages   - Render Frames.\n */\n\n\n/**\n * Attributes present in the node of type: 'TbdColorSelector'\n * @name  NodeTypes#Colour-Selector\n * @property {string}  selectedcolors   - Selected Colours.\n * @property {bool}  applytomatte=false   - Apply to Matte Ports on Input Effects.\n */\n\n\n/**\n * Attributes present in the node of type: 'COLOR_OVERRIDE_TVG'\n * @name  NodeTypes#Colour-Override\n */\n\n\n/**\n * Attributes present in the node of type: 'FLATTEN'\n * @name  NodeTypes#Flatten\n */\n\n\n/**\n * Attributes present in the node of type: 'DISPLAY'\n * @name  NodeTypes#Display\n */\n\n\n/**\n * Attributes present in the node of type: 'DynamicSpring'\n * @name  NodeTypes#Dynamic-Spring\n * @property {double}  active=100   - Active.\n * @property {bool}  matchexposures=false   - Match Animation on Active Attribute.\n * @property {double}  tensionx=7   - Tension X.\n * @property {double}  inertiax=80   - Inertia X.\n * @property {double}  tensiony=7   - Tension Y.\n * @property {double}  inertiay=80   - Inertia Y.\n * @property {double}  tensionz=7   - Tension Z.\n * @property {double}  inertiaz=80   - Inertia Z.\n * @property {double}  tensionscale=7   - Tension Scale.\n * @property {double}  inertiascale=80   - Inertia Scale.\n * @property {double}  tensionskew=7   - Tension Skew.\n * @property {double}  inertiaskew=80   - Inertia Skew.\n * @property {double}  tensionrotate=7   - Tension Rotate.\n * @property {double}  inertiarotate=80   - Inertia Rotate.\n * @property {double}  pignore=0   - Ignore Parents.\n * @property {string}  parentname   - Parent's Name.\n */\n\n\n/**\n * Attributes present in the node of type: 'WRITE'\n * @name  NodeTypes#Write\n * @property {generic_enum}  [export_to_movie=\"Output Drawings\"]   - Export to movie.\n * @property {string}  drawing_name=frames/final-   - Drawing name.\n * @property {string}  movie_path=frames/output   - Movie path.\n * @property {string}  movie_format=com.toonboom.quicktime.legacy   - Movie format setting.\n * @property {string}  movie_audio   - Movie audio settings.\n * @property {string}  movie_video   - Movie video settings.\n * @property {string}  movie_videoaudio   - Movie video and audio settings.\n * @property {int}  leading_zeros=3   - Leading zeros.\n * @property {int}  start=1   - Start.\n * @property {string}  drawing_type=TGA   - Drawing type.\n * @property {enable}  [enabling=\"Always Enabled\"]   - Enabling.\n * @property {generic_enum}  [enabling.filter=\"Always Enabled\"]   - Filter.\n * @property {string}  enabling.filter_name   - Filter name.\n * @property {int}  enabling.filter_res_x=720   - X resolution.\n * @property {int}  enabling.filter_res_y=540   - Y resolution.\n * @property {bool}  script_movie=false   - Script Movie.\n * @property {string}  [script_editor=\"// Following code will be called at the end of Write Node rendering\n// operations to create a movie file from rendered images.\n\n// ...\"]   - Movie Generation Script.\n * @property {string}  color_space   - Colour Space.\n * @property {generic_enum}  [composite_partitioning=\"Off\"]   - Composite Partitioning.\n * @property {double}  z_partition_range=1   - Z Partition Range.\n * @property {bool}  clean_up_partition_folders=true   - Clean up partition folders.\n */\n\n\n/**\n * Attributes present in the node of type: 'MultiLayerWrite'\n * @name  NodeTypes#Multi-Layer-Write\n * @property {generic_enum}  [export_to_movie=\"Output Drawings\"]   - Export to movie.\n * @property {string}  drawing_name=frames/final-   - Drawing name.\n * @property {string}  movie_path=frames/output   - Movie path.\n * @property {string}  movie_format=com.toonboom.quicktime.legacy   - Movie format setting.\n * @property {string}  movie_audio   - Movie audio settings.\n * @property {string}  movie_video   - Movie video settings.\n * @property {string}  movie_videoaudio   - Movie video and audio settings.\n * @property {int}  leading_zeros=3   - Leading zeros.\n * @property {int}  start=1   - Start.\n * @property {string}  drawing_type=PSD   - Drawing type.\n * @property {enable}  [enabling=\"Always Enabled\"]   - Enabling.\n * @property {generic_enum}  [enabling.filter=\"Always Enabled\"]   - Filter.\n * @property {string}  enabling.filter_name   - Filter name.\n * @property {int}  enabling.filter_res_x=720   - X resolution.\n * @property {int}  enabling.filter_res_y=540   - Y resolution.\n * @property {bool}  script_movie=false   - Script Movie.\n * @property {string}  [script_editor=\"// Following code will be called at the end of Write Node rendering\n// operations to create a movie file from rendered images.\n\n// ...\"]   - Movie Generation Script.\n * @property {string}  color_space   - Colour Space.\n * @property {generic_enum}  [composite_partitioning=\"Off\"]   - Composite Partitioning.\n * @property {double}  z_partition_range=1   - Z Partition Range.\n * @property {bool}  clean_up_partition_folders=true   - Clean up partition folders.\n * @property {string}  input_names   - Input Names.\n */\n\n\n/**\n * Attributes present in the node of type: 'BLUR_DIRECTIONAL'\n * @name  NodeTypes#Blur-Directional\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {bool}  truck_factor=true   - Truck Factor.\n * @property {double}  fallof_rate=0   - Falloff Rate.\n * @property {double}  angle=0   - Angle.\n * @property {double}  radius=0   - Radius.\n * @property {generic_enum}  [direction_of_trail=\"Angle\"]   - Direction of trail.\n * @property {bool}  ignore_alpha=false   - Ignore Alpha.\n * @property {bool}  extra_final_blur=true   - Extra Final Blur.\n */\n\n\n/**\n * Attributes present in the node of type: 'TONE'\n * @name  NodeTypes#Tone\n * @property {bool}  truck_factor=true   - Truck Factor.\n * @property {generic_enum}  [blur_type=\"Radial\"]   - Blur Type.\n * @property {double}  radius=2   - Radius.\n * @property {double}  directional_angle=0   - Directional Angle.\n * @property {double}  directional_falloff_rate=1   - Directional Falloff Rate.\n * @property {bool}  use_matte_color=false   - Use Matte Colour.\n * @property {bool}  invert_matte=false   - Invert Matte.\n * @property {color}  color=649c9c9c   - Color.\n * @property {int}  color.red=-100   - Red.\n * @property {int}  color.green=-100   - Green.\n * @property {int}  color.blue=-100   - Blue.\n * @property {int}  color.alpha=100   - Alpha.\n * @property {generic_enum}  [color.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  multiplicative=false   - Multiplicative.\n * @property {double}  colour_gain=1   - Intensity.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'GAUSSIANBLUR-PLUGIN'\n * @name  NodeTypes#Blur-Gaussian\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {bool}  truck_factor=true   - Truck Factor.\n * @property {bool}  bidirectional=true   - Bidirectional.\n * @property {generic_enum}  [precision=\"Medium 8\"]   - Precision.\n * @property {bool}  repeat_edge_pixels=false   - Repeat Edge Pixels.\n * @property {bool}  directional=false   - Directional.\n * @property {double}  angle=0   - Angle.\n * @property {int}  iterations=1   - Number of Iterations.\n * @property {double}  blurriness=0   - Blurriness.\n * @property {double}  vertical=0   - Vertical Blurriness.\n * @property {double}  horizontal=0   - Horizontal Blurriness.\n */\n\n\n/**\n * Attributes present in the node of type: 'HIGHLIGHT'\n * @name  NodeTypes#Highlight\n * @property {bool}  truck_factor=true   - Truck Factor.\n * @property {generic_enum}  [blur_type=\"Radial\"]   - Blur Type.\n * @property {double}  radius=2   - Radius.\n * @property {double}  directional_angle=0   - Directional Angle.\n * @property {double}  directional_falloff_rate=1   - Directional Falloff Rate.\n * @property {bool}  use_matte_color=false   - Use Matte Colour.\n * @property {bool}  invert_matte=false   - Invert Matte.\n * @property {color}  color=64646464   - Color.\n * @property {int}  color.red=100   - Red.\n * @property {int}  color.green=100   - Green.\n * @property {int}  color.blue=100   - Blue.\n * @property {int}  color.alpha=100   - Alpha.\n * @property {generic_enum}  [color.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  multiplicative=false   - Multiplicative.\n * @property {double}  colour_gain=1   - Intensity.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'MATTE_BLUR'\n * @name  NodeTypes#Matte-Blur\n * @property {bool}  truck_factor=true   - Truck Factor.\n * @property {generic_enum}  [blur_type=\"Radial\"]   - Blur Type.\n * @property {double}  radius=0   - Radius.\n * @property {double}  directional_angle=0   - Directional Angle.\n * @property {double}  directional_falloff_rate=1   - Directional Falloff Rate.\n * @property {bool}  use_matte_color=false   - Use Matte Colour.\n * @property {bool}  invert_matte=false   - Invert Matte.\n * @property {color}  color=ffffffff   - Color.\n * @property {int}  color.red=255   - Red.\n * @property {int}  color.green=255   - Green.\n * @property {int}  color.blue=255   - Blue.\n * @property {int}  color.alpha=255   - Alpha.\n * @property {generic_enum}  [color.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  multiplicative=false   - Multiplicative.\n * @property {double}  colour_gain=1   - Intensity.\n */\n\n\n/**\n * Attributes present in the node of type: 'PEG'\n * @name  NodeTypes#Peg\n * @property {bool}  enable_3d=false   - Enable 3D.\n * @property {bool}  face_camera=false   - Face Camera.\n * @property {generic_enum}  [camera_alignment=\"None\"]   - Camera Alignment.\n * @property {position_3d}  position   - Position.\n * @property {bool}  position.separate=On   - Separate.\n * @property {double}  position.x=0   - Pos x.\n * @property {double}  position.y=0   - Pos y.\n * @property {double}  position.z=0   - Pos z.\n * @property {path_3d}  position.3dpath   - Path.\n * @property {scale_3d}  scale   - Scale.\n * @property {bool}  scale.separate=On   - Separate.\n * @property {bool}  scale.in_fields=Off   - In fields.\n * @property {doublevb}  scale.xy=1   - Scale.\n * @property {doublevb}  scale.x=1   - Scale x.\n * @property {doublevb}  scale.y=1   - Scale y.\n * @property {doublevb}  scale.z=1   - Scale z.\n * @property {rotation_3d}  rotation   - Rotation.\n * @property {bool}  rotation.separate=Off   - Separate.\n * @property {doublevb}  rotation.anglex=0   - Angle_x.\n * @property {doublevb}  rotation.angley=0   - Angle_y.\n * @property {doublevb}  rotation.anglez=0   - Angle_z.\n * @property {quaternion_path}  rotation.quaternionpath   - Quaternion.\n * @property {alias}  angle=0   - Angle.\n * @property {double}  skew=0   - Skew.\n * @property {position_3d}  pivot   - Pivot.\n * @property {bool}  pivot.separate=On   - Separate.\n * @property {double}  pivot.x=0   - Pos x.\n * @property {double}  pivot.y=0   - Pos y.\n * @property {double}  pivot.z=0   - Pos z.\n * @property {position_3d}  spline_offset   - Spline Offset.\n * @property {bool}  spline_offset.separate=On   - Separate.\n * @property {double}  spline_offset.x=0   - Pos x.\n * @property {double}  spline_offset.y=0   - Pos y.\n * @property {double}  spline_offset.z=0   - Pos z.\n * @property {bool}  ignore_parent_peg_scaling=false   - Ignore Parent Scaling.\n * @property {bool}  disable_field_rendering=false   - Disable Field Rendering.\n * @property {int}  depth=0   - Depth.\n * @property {bool}  enable_min_max_angle=false   - Enable Min/Max Angle.\n * @property {double}  min_angle=-360   - Min Angle.\n * @property {double}  max_angle=360   - Max Angle.\n * @property {bool}  nail_for_children=false   - Nail for Children.\n * @property {bool}  ik_hold_orientation=false   - Hold Orientation in IK.\n * @property {bool}  ik_hold_x=false   - Hold X in IK.\n * @property {bool}  ik_hold_y=false   - Hold Y in IK.\n * @property {bool}  ik_excluded=false   - Is Excluded from IK.\n * @property {bool}  ik_can_rotate=true   - Can Rotate during IK.\n * @property {bool}  ik_can_translate_x=false   - Can Translate in X during IK.\n * @property {bool}  ik_can_translate_y=false   - Can Translate in Y during IK.\n * @property {double}  ik_bone_x=0.2000   - X Direction of Bone.\n * @property {double}  ik_bone_y=0   - Y Direction of Bone.\n * @property {double}  ik_stiffness=1   - Stiffness of Bone.\n * @property {bool}  group_at_network_building=false   - Group at Network Building.\n * @property {bool}  add_composite_to_group=true   - Add Composite to Group.\n */\n\n\n/**\n * Attributes present in the node of type: 'GLOW'\n * @name  NodeTypes#Glow\n * @property {bool}  truck_factor=true   - Truck Factor.\n * @property {generic_enum}  [blur_type=\"Radial\"]   - Blur Type.\n * @property {double}  radius=0   - Radius.\n * @property {double}  directional_angle=0   - Directional Angle.\n * @property {double}  directional_falloff_rate=1   - Directional Falloff Rate.\n * @property {bool}  use_matte_color=false   - Use Source Colour.\n * @property {bool}  invert_matte=false   - Invert Matte.\n * @property {color}  color=ff646464   - Color.\n * @property {int}  color.red=100   - Red.\n * @property {int}  color.green=100   - Green.\n * @property {int}  color.blue=100   - Blue.\n * @property {int}  color.alpha=255   - Alpha.\n * @property {generic_enum}  [color.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  multiplicative=false   - Multiplicative.\n * @property {double}  colour_gain=1   - Intensity.\n */\n\n\n/**\n * Attributes present in the node of type: 'BLEND_MODE_MODULE'\n * @name  NodeTypes#Blending\n * @property {generic_enum}  [blend_mode=\"Normal\"]   - Blend Mode.\n * @property {generic_enum}  [flash_blend_mode=\"Normal\"]   - SWF Blend Mode.\n */\n\n\n/**\n * Attributes present in the node of type: 'BLUR_RADIAL'\n * @name  NodeTypes#Blur\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {bool}  truck_factor=true   - Truck Factor.\n * @property {double}  radius=0   - Radius.\n * @property {generic_enum}  [quality=\"High\"]   - Quality.\n */\n\n\n/**\n * Attributes present in the node of type: 'ImageSwitch'\n * @name  NodeTypes#Image-Switch\n * @property {int}  port_index=0   - Port Index.\n */\n\n\n/**\n * Attributes present in the node of type: 'ORTHOLOCK'\n * @name  NodeTypes#OrthoLock\n * @property {generic_enum}  [rotation_axis=\"X and Y Axes\"]   - Rotation Axis.\n * @property {double}  max_angle=0   - Max Angle.\n */\n\n\n/**\n * Attributes present in the node of type: 'CHROMA_KEYING'\n * @name  NodeTypes#Chroma-Keying\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {color}  color=ffffffff   - Color.\n * @property {int}  color.red=255   - Red.\n * @property {int}  color.green=255   - Green.\n * @property {int}  color.blue=255   - Blue.\n * @property {int}  color.alpha=255   - Alpha.\n * @property {generic_enum}  [color.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {double}  chroma_key_minimum=0   - Black Point.\n * @property {double}  chroma_key_maximum=255   - White Point.\n * @property {double}  chroma_key_filter_intensity=1   - Blur Passes.\n * @property {double}  chroma_key_sampling=2   - Adjacent Pixels per Pass.\n * @property {bool}  applyfinalthreshold=true   - Threshold Matte.\n * @property {double}  finalthreshold=10   - Threshold.\n * @property {bool}  cutimage=true   - Cut Colour.\n */\n\n\n/**\n * Attributes present in the node of type: 'Quake'\n * @name  NodeTypes#Quake\n * @property {int}  hold=1   - Hold Time.\n * @property {bool}  interpolate=false   - Interpolate.\n * @property {double}  moveamplitude=1   - Move Amplitude.\n * @property {bool}  applyx=true   - Apply on X.\n * @property {bool}  applyy=true   - Apply on Y.\n * @property {bool}  applyz=true   - Apply on Z.\n * @property {double}  rotationamplitude=0   - Rotation Amplitude.\n * @property {int}  seed=0   - Random Seed.\n */\n\n\n/**\n * Attributes present in the node of type: 'GRADIENT-PLUGIN'\n * @name  NodeTypes#Gradient\n * @property {int}  depth=0   - Depth.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {generic_enum}  [type=\"Linear\"]   - Gradient Type.\n * @property {position_2d}  0   - Position 0.\n * @property {bool}  0.separate=On   - Separate.\n * @property {double}  0.x=0   - Pos x.\n * @property {double}  0.y=12   - Pos y.\n * @property {point_2d}  0.2dpoint   - Point.\n * @property {position_2d}  1   - Position 1.\n * @property {bool}  1.separate=On   - Separate.\n * @property {double}  1.x=0   - Pos x.\n * @property {double}  1.y=-12   - Pos y.\n * @property {point_2d}  1.2dpoint   - Point.\n * @property {color}  color0=ff000000   - Colour 0.\n * @property {int}  color0.red=0   - Red.\n * @property {int}  color0.green=0   - Green.\n * @property {int}  color0.blue=0   - Blue.\n * @property {int}  color0.alpha=255   - Alpha.\n * @property {generic_enum}  [color0.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {color}  color1=ffffffff   - Colour 1.\n * @property {int}  color1.red=255   - Red.\n * @property {int}  color1.green=255   - Green.\n * @property {int}  color1.blue=255   - Blue.\n * @property {int}  color1.alpha=255   - Alpha.\n * @property {generic_enum}  [color1.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {double}  offset_z=0   - Offset Z.\n */\n\n\n/**\n * Attributes present in the node of type: 'BRIGHTNESS_CONTRAST'\n * @name  NodeTypes#BrightnessContrast\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {bool}  brightcontrast_legacy_brightness=false   - Legacy Brightness.\n * @property {double}  brightcontrast_brightness_adjustment=0   - Brightness.\n * @property {double}  brightcontrast_contrast_adjustment=0   - Contrast.\n * @property {bool}  brightcontrast_legacy_contrast=false   - Legacy Contrast.\n */\n\n\n/**\n * Attributes present in the node of type: 'Switch'\n * @name  NodeTypes#Constraint-Switch\n * @property {double}  active=100   - ACTIVE.\n * @property {int}  gatenum=0   - TARGET GATE.\n * @property {position_2d}  uioffsetpos   - GUI OFFSET.\n * @property {bool}  uioffsetpos.separate=On   - Separate.\n * @property {double}  uioffsetpos.x=0   - Pos x.\n * @property {double}  uioffsetpos.y=0   - Pos y.\n * @property {double}  uiscale=1   - GUI SCALE.\n */\n\n\n/**\n * Attributes present in the node of type: 'Grid'\n * @name  NodeTypes#Grid\n * @property {int}  size=12   - Size.\n * @property {double}  aspect=1.3333   - Aspect.\n * @property {bool}  showtext=true   - Display Text.\n * @property {color}  gridcolor=ff000000   - Color.\n * @property {int}  gridcolor.red=0   - Red.\n * @property {int}  gridcolor.green=0   - Green.\n * @property {int}  gridcolor.blue=0   - Blue.\n * @property {int}  gridcolor.alpha=255   - Alpha.\n * @property {generic_enum}  [gridcolor.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {color}  bgcolor=ffffffff   - Color.\n * @property {int}  bgcolor.red=255   - Red.\n * @property {int}  bgcolor.green=255   - Green.\n * @property {int}  bgcolor.blue=255   - Blue.\n * @property {int}  bgcolor.alpha=255   - Alpha.\n * @property {generic_enum}  [bgcolor.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  opaque=false   - Fill.\n * @property {bool}  fitvertical=true   - Fit Vertical.\n */\n\n\n/**\n * Attributes present in the node of type: 'Turbulence'\n * @name  NodeTypes#Turbulence\n * @property {generic_enum}  [fractal_type=\"Fractional Brownian\"]   - Fractal Type.\n * @property {generic_enum}  [noise_type=\"Perlin\"]   - Noise Type.\n * @property {locked}  frequency   - Frequency.\n * @property {bool}  frequency.separate=Off   - Separate.\n * @property {doublevb}  frequency.xyfrequency=1   - Frequency xy.\n * @property {doublevb}  frequency.xfrequency=0   - Frequency x.\n * @property {doublevb}  frequency.yfrequency=0   - Frequency y.\n * @property {locked}  amount   - Amount.\n * @property {bool}  amount.separate=Off   - Separate.\n * @property {doublevb}  amount.xyamount=0   - Amount xy.\n * @property {doublevb}  amount.xamount=0   - Amount x.\n * @property {doublevb}  amount.yamount=0   - Amount y.\n * @property {locked}  offset   - Offset.\n * @property {bool}  offset.separate=Off   - Separate.\n * @property {doublevb}  offset.xyoffset=0   - Offset xy.\n * @property {doublevb}  offset.xoffset=0   - Offset x.\n * @property {doublevb}  offset.yoffset=0   - Offset y.\n * @property {locked}  seed   - Seed.\n * @property {bool}  seed.separate=On   - Separate.\n * @property {doublevb}  seed.xyseed=0   - Seed xy.\n * @property {doublevb}  seed.xseed=10   - Seed x.\n * @property {doublevb}  seed.yseed=0   - Seed y.\n * @property {double}  evolution=0   - Evolution.\n * @property {double}  evolution_frequency=0   - Evolution Frequency.\n * @property {double}  gain=0.6500   - Gain.\n * @property {double}  lacunarity=2   - Sub Scaling.\n * @property {double}  octaves=1   - Complexity.\n * @property {bool}  pinning=false   - Pinning.\n * @property {generic_enum}  [deformationquality=\"Medium\"]   - Deformation Quality.\n */\n\n\n/**\n * Attributes present in the node of type: 'PointConstraintMulti'\n * @name  NodeTypes#Multi-Points-Constraint\n * @property {double}  active=100   - Active.\n * @property {generic_enum}  [flattentype=\"Allow 3D Transform\"]   - Flatten Type.\n * @property {bool}  convexhull=false   - Ignore Internal Points.\n * @property {bool}  allowpersp=false   - Allow Perspective Transform.\n */\n\n\n/**\n * Attributes present in the node of type: 'VISIBILITY'\n * @name  NodeTypes#Visibility\n * @property {bool}  oglrender=true   - Display in OpenGL View.\n * @property {bool}  softrender=true   - Soft Render.\n */\n\n\n/**\n * Attributes present in the node of type: 'REFRACT'\n * @name  NodeTypes#Refract\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {double}  intensity=10   - Intensity.\n * @property {double}  height=0   - Height.\n */\n\n\n/**\n * Attributes present in the node of type: 'MULTIPORT_OUT'\n * @name  NodeTypes#Multi-Port-Out\n */\n\n\n/**\n * Attributes present in the node of type: 'REMOVE_TRANSPARENCY'\n * @name  NodeTypes#Remove-Transparency\n * @property {double}  threshold=50   - Threshold.\n * @property {bool}  remove_color_transparency=true   - Remove Colour Transparency.\n * @property {bool}  remove_alpha_transparency=true   - Remove Alpha Transparency.\n */\n\n\n/**\n * Attributes present in the node of type: 'Bloom'\n * @name  NodeTypes#Bloom\n * @property {double}  luminancethresholdthresh=75   - Threshold.\n * @property {bool}  luminancethresholdsoften=true   - Soften Colours.\n * @property {double}  luminancethresholdgamma=1.5000   - Gamma Correction.\n * @property {bool}  luminancethresholdgrey=false   - Output Greyscale Matte.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {bool}  truck_factor=true   - Truck Factor.\n * @property {double}  radius=4   - Radius.\n * @property {generic_enum}  [quality=\"High\"]   - Quality.\n * @property {bool}  composite_src_image=true   - Composite with Source Image.\n * @property {generic_enum}  [blend_mode=\"Screen\"]   - Blend Mode.\n */\n\n\n/**\n * Attributes present in the node of type: 'MULTIPORT_IN'\n * @name  NodeTypes#Multi-Port-In\n */\n\n\n/**\n * Attributes present in the node of type: 'MedianFilter'\n * @name  NodeTypes#Median\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {double}  radius=0   - Radius.\n * @property {int}  bitdepth=256   - Colour Depth.\n */\n\n\n/**\n * Attributes present in the node of type: 'ToneShader'\n * @name  NodeTypes#Tone-Shader\n * @property {generic_enum}  [lighttype=\"Directional\"]   - Light Type.\n * @property {double}  floodangle=90   - Cone Angle.\n * @property {double}  floodsharpness=0   - Diffusion.\n * @property {double}  floodradius=2000   - Falloff.\n * @property {double}  pointelevation=200   - Light Source Elevation.\n * @property {double}  anglethreshold=90   - Surface Reflectivity.\n * @property {generic_enum}  [shadetype=\"Smooth\"]   - Shading Type.\n * @property {double}  bias=0.1000   - Bias.\n * @property {double}  exponent=2   - Abruptness.\n * @property {color}  lightcolor=ff646464   - Light Colour.\n * @property {int}  lightcolor.red=100   - Red.\n * @property {int}  lightcolor.green=100   - Green.\n * @property {int}  lightcolor.blue=100   - Blue.\n * @property {int}  lightcolor.alpha=255   - Alpha.\n * @property {generic_enum}  [lightcolor.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  flatten=true   - Flatten Fx.\n * @property {bool}  useimagecolor=false   - Use image Colour.\n * @property {double}  imagecolorweight=50   - Image Colour Intensity.\n * @property {bool}  adjustlevel=false   - Adjust Light Intensity.\n * @property {double}  adjustedlevel=75   - Intensity.\n * @property {double}  scale=1   - Multiplier.\n * @property {bool}  usesurface=false   - Use Surface Lighting.\n * @property {double}  speculartransparency=0   - Specular Transparency.\n * @property {double}  specularity=100   - Specular Strength.\n * @property {double}  distancefalloff=0   - Distance Falloff.\n */\n\n\n/**\n * Attributes present in the node of type: 'DeformationCompositeModule'\n * @name  NodeTypes#Deformation-Composite\n * @property {bool}  outputmatrixonly=false   - Output Kinematic Only.\n * @property {bool}  outputselectedonly=false   - Output Selected Port Only.\n * @property {generic_enum}  [outputkinematicchainselector=\"Rightmost\"]   - Output Kinematic Chain.\n * @property {int}  outputkinematicchain=1   - Output Kinematic Chain Selection.\n */\n\n\n/**\n * Attributes present in the node of type: 'AnimatedMatteGenerator'\n * @name  NodeTypes#Animated-Matte-Generator\n * @property {double}  snapradius=15   - Drag-to-Snap Distance.\n * @property {bool}  snapoutlinesonly=false   - Snap to Outlines Only.\n * @property {generic_enum}  [outputtype=\"Feathered\"]   - Type.\n * @property {double}  outputinterpolation=0   - Interpolation Factor.\n * @property {color}  insidecolor=ffffffff   - Inside Colour.\n * @property {int}  insidecolor.red=255   - Red.\n * @property {int}  insidecolor.green=255   - Green.\n * @property {int}  insidecolor.blue=255   - Blue.\n * @property {int}  insidecolor.alpha=255   - Alpha.\n * @property {generic_enum}  [insidecolor.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {color}  outsidecolor=ffffffff   - Outside Colour.\n * @property {int}  outsidecolor.red=255   - Red.\n * @property {int}  outsidecolor.green=255   - Green.\n * @property {int}  outsidecolor.blue=255   - Blue.\n * @property {int}  outsidecolor.alpha=255   - Alpha.\n * @property {generic_enum}  [outsidecolor.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {generic_enum}  [interpolationmode=\"Distance\"]   - Interpolation Mode.\n * @property {generic_enum}  [colorinterpolation=\"Constant\"]   - Colour Interpolation.\n * @property {generic_enum}  [alphamapping=\"Linear\"]   - Alpha Mapping.\n * @property {int}  colorlutdomain=100   - Colour LUT Domain.\n * @property {int}  alphalutdomain=100   - Alpha LUT Domain.\n * @property {double}  colorgamma=1   - Colour Gamma.\n * @property {double}  alphagamma=1   - Alpha Gamma.\n * @property {double}  colorlut=0   - Colour LUT.\n * @property {double}  alphalut=0   - Alpha LUT.\n * @property {bool}  snapunderlay=false   - Underlay Art.\n * @property {bool}  snapcolor=true   - Colour Art.\n * @property {bool}  snapline=true   - Line Art.\n * @property {bool}  snapoverlay=false   - Overlay Art.\n * @property {bool}  usehints=true   - Enable Hints.\n * @property {double}  snapradiusdraghint=20   - Drag-to-Snap Distance.\n * @property {double}  snapradiusgeneratehint=95   - Generated Matte Snap Distance.\n * @property {double}  mindistancepregeneratehints=75   - Minimum Distance Between Generated Hints.\n * @property {bool}  snaphintunderlay=false   - Underlay Art.\n * @property {bool}  snaphintcolor=true   - Colour Art.\n * @property {bool}  snaphintline=false   - Line Art.\n * @property {bool}  snaphintoverlay=false   - Overlay Art.\n * @property {bool}  overlayisnote=true   - Use Overlay Layer as Note.\n * @property {string}  contourcentres   - Contour Centres.\n */\n\n\n/**\n * Attributes present in the node of type: 'COLOR2BW'\n * @name  NodeTypes#Greyscale\n * @property {double}  percent=100   - Percent.\n * @property {bool}  matte_output=false   - Matte Output.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'CONTRAST'\n * @name  NodeTypes#Contrast\n * @property {double}  mid_point=0.5000   - Mid Point.\n * @property {double}  dark_pixel_adjustement=1   - Dark Pixel Adjustment.\n * @property {double}  bright_pixel_adjustement=1   - Bright Pixel Adjustment.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'COLOR_CURVES'\n * @name  NodeTypes#Colour-Curves\n * @property {color-correction-control-point-attribute}  control_point_red_0   - Control point.\n * @property {position_2d}  control_point_red_0.control   - Control Point.\n * @property {bool}  control_point_red_0.control.separate=Off   - Separate.\n * @property {double}  control_point_red_0.control.x=0   - Pos x.\n * @property {double}  control_point_red_0.control.y=0   - Pos y.\n * @property {point_2d}  control_point_red_0.control.2dpoint   - Point.\n * @property {position_2d}  control_point_red_0.left_handle   - Left Handle.\n * @property {bool}  control_point_red_0.left_handle.separate=Off   - Separate.\n * @property {double}  control_point_red_0.left_handle.x=-0.1000   - Pos x.\n * @property {double}  control_point_red_0.left_handle.y=-0.1000   - Pos y.\n * @property {point_2d}  control_point_red_0.left_handle.2dpoint   - Point.\n * @property {position_2d}  control_point_red_0.right_handle   - Right Handle.\n * @property {bool}  control_point_red_0.right_handle.separate=Off   - Separate.\n * @property {double}  control_point_red_0.right_handle.x=0.1000   - Pos x.\n * @property {double}  control_point_red_0.right_handle.y=0.1000   - Pos y.\n * @property {point_2d}  control_point_red_0.right_handle.2dpoint   - Point.\n * @property {color-correction-control-point-attribute}  control_point_red_1   - Control point.\n * @property {position_2d}  control_point_red_1.control   - Control Point.\n * @property {bool}  control_point_red_1.control.separate=Off   - Separate.\n * @property {double}  control_point_red_1.control.x=1   - Pos x.\n * @property {double}  control_point_red_1.control.y=1   - Pos y.\n * @property {point_2d}  control_point_red_1.control.2dpoint   - Point.\n * @property {position_2d}  control_point_red_1.left_handle   - Left Handle.\n * @property {bool}  control_point_red_1.left_handle.separate=Off   - Separate.\n * @property {double}  control_point_red_1.left_handle.x=-0.1000   - Pos x.\n * @property {double}  control_point_red_1.left_handle.y=-0.1000   - Pos y.\n * @property {point_2d}  control_point_red_1.left_handle.2dpoint   - Point.\n * @property {position_2d}  control_point_red_1.right_handle   - Right Handle.\n * @property {bool}  control_point_red_1.right_handle.separate=Off   - Separate.\n * @property {double}  control_point_red_1.right_handle.x=0.1000   - Pos x.\n * @property {double}  control_point_red_1.right_handle.y=0.1000   - Pos y.\n * @property {point_2d}  control_point_red_1.right_handle.2dpoint   - Point.\n * @property {color-correction-control-point-attribute}  control_point_green_0   - Control point.\n * @property {position_2d}  control_point_green_0.control   - Control Point.\n * @property {bool}  control_point_green_0.control.separate=Off   - Separate.\n * @property {double}  control_point_green_0.control.x=0   - Pos x.\n * @property {double}  control_point_green_0.control.y=0   - Pos y.\n * @property {point_2d}  control_point_green_0.control.2dpoint   - Point.\n * @property {position_2d}  control_point_green_0.left_handle   - Left Handle.\n * @property {bool}  control_point_green_0.left_handle.separate=Off   - Separate.\n * @property {double}  control_point_green_0.left_handle.x=-0.1000   - Pos x.\n * @property {double}  control_point_green_0.left_handle.y=-0.1000   - Pos y.\n * @property {point_2d}  control_point_green_0.left_handle.2dpoint   - Point.\n * @property {position_2d}  control_point_green_0.right_handle   - Right Handle.\n * @property {bool}  control_point_green_0.right_handle.separate=Off   - Separate.\n * @property {double}  control_point_green_0.right_handle.x=0.1000   - Pos x.\n * @property {double}  control_point_green_0.right_handle.y=0.1000   - Pos y.\n * @property {point_2d}  control_point_green_0.right_handle.2dpoint   - Point.\n * @property {color-correction-control-point-attribute}  control_point_green_1   - Control point.\n * @property {position_2d}  control_point_green_1.control   - Control Point.\n * @property {bool}  control_point_green_1.control.separate=Off   - Separate.\n * @property {double}  control_point_green_1.control.x=1   - Pos x.\n * @property {double}  control_point_green_1.control.y=1   - Pos y.\n * @property {point_2d}  control_point_green_1.control.2dpoint   - Point.\n * @property {position_2d}  control_point_green_1.left_handle   - Left Handle.\n * @property {bool}  control_point_green_1.left_handle.separate=Off   - Separate.\n * @property {double}  control_point_green_1.left_handle.x=-0.1000   - Pos x.\n * @property {double}  control_point_green_1.left_handle.y=-0.1000   - Pos y.\n * @property {point_2d}  control_point_green_1.left_handle.2dpoint   - Point.\n * @property {position_2d}  control_point_green_1.right_handle   - Right Handle.\n * @property {bool}  control_point_green_1.right_handle.separate=Off   - Separate.\n * @property {double}  control_point_green_1.right_handle.x=0.1000   - Pos x.\n * @property {double}  control_point_green_1.right_handle.y=0.1000   - Pos y.\n * @property {point_2d}  control_point_green_1.right_handle.2dpoint   - Point.\n * @property {color-correction-control-point-attribute}  control_point_blue_0   - Control point.\n * @property {position_2d}  control_point_blue_0.control   - Control Point.\n * @property {bool}  control_point_blue_0.control.separate=Off   - Separate.\n * @property {double}  control_point_blue_0.control.x=0   - Pos x.\n * @property {double}  control_point_blue_0.control.y=0   - Pos y.\n * @property {point_2d}  control_point_blue_0.control.2dpoint   - Point.\n * @property {position_2d}  control_point_blue_0.left_handle   - Left Handle.\n * @property {bool}  control_point_blue_0.left_handle.separate=Off   - Separate.\n * @property {double}  control_point_blue_0.left_handle.x=-0.1000   - Pos x.\n * @property {double}  control_point_blue_0.left_handle.y=-0.1000   - Pos y.\n * @property {point_2d}  control_point_blue_0.left_handle.2dpoint   - Point.\n * @property {position_2d}  control_point_blue_0.right_handle   - Right Handle.\n * @property {bool}  control_point_blue_0.right_handle.separate=Off   - Separate.\n * @property {double}  control_point_blue_0.right_handle.x=0.1000   - Pos x.\n * @property {double}  control_point_blue_0.right_handle.y=0.1000   - Pos y.\n * @property {point_2d}  control_point_blue_0.right_handle.2dpoint   - Point.\n * @property {color-correction-control-point-attribute}  control_point_blue_1   - Control point.\n * @property {position_2d}  control_point_blue_1.control   - Control Point.\n * @property {bool}  control_point_blue_1.control.separate=Off   - Separate.\n * @property {double}  control_point_blue_1.control.x=1   - Pos x.\n * @property {double}  control_point_blue_1.control.y=1   - Pos y.\n * @property {point_2d}  control_point_blue_1.control.2dpoint   - Point.\n * @property {position_2d}  control_point_blue_1.left_handle   - Left Handle.\n * @property {bool}  control_point_blue_1.left_handle.separate=Off   - Separate.\n * @property {double}  control_point_blue_1.left_handle.x=-0.1000   - Pos x.\n * @property {double}  control_point_blue_1.left_handle.y=-0.1000   - Pos y.\n * @property {point_2d}  control_point_blue_1.left_handle.2dpoint   - Point.\n * @property {position_2d}  control_point_blue_1.right_handle   - Right Handle.\n * @property {bool}  control_point_blue_1.right_handle.separate=Off   - Separate.\n * @property {double}  control_point_blue_1.right_handle.x=0.1000   - Pos x.\n * @property {double}  control_point_blue_1.right_handle.y=0.1000   - Pos y.\n * @property {point_2d}  control_point_blue_1.right_handle.2dpoint   - Point.\n * @property {color-correction-control-point-attribute}  control_point_rgb_0   - Control point.\n * @property {position_2d}  control_point_rgb_0.control   - Control Point.\n * @property {bool}  control_point_rgb_0.control.separate=Off   - Separate.\n * @property {double}  control_point_rgb_0.control.x=0   - Pos x.\n * @property {double}  control_point_rgb_0.control.y=0   - Pos y.\n * @property {point_2d}  control_point_rgb_0.control.2dpoint   - Point.\n * @property {position_2d}  control_point_rgb_0.left_handle   - Left Handle.\n * @property {bool}  control_point_rgb_0.left_handle.separate=Off   - Separate.\n * @property {double}  control_point_rgb_0.left_handle.x=-0.1000   - Pos x.\n * @property {double}  control_point_rgb_0.left_handle.y=-0.1000   - Pos y.\n * @property {point_2d}  control_point_rgb_0.left_handle.2dpoint   - Point.\n * @property {position_2d}  control_point_rgb_0.right_handle   - Right Handle.\n * @property {bool}  control_point_rgb_0.right_handle.separate=Off   - Separate.\n * @property {double}  control_point_rgb_0.right_handle.x=0.1000   - Pos x.\n * @property {double}  control_point_rgb_0.right_handle.y=0.1000   - Pos y.\n * @property {point_2d}  control_point_rgb_0.right_handle.2dpoint   - Point.\n * @property {color-correction-control-point-attribute}  control_point_rgb_1   - Control point.\n * @property {position_2d}  control_point_rgb_1.control   - Control Point.\n * @property {bool}  control_point_rgb_1.control.separate=Off   - Separate.\n * @property {double}  control_point_rgb_1.control.x=1   - Pos x.\n * @property {double}  control_point_rgb_1.control.y=1   - Pos y.\n * @property {point_2d}  control_point_rgb_1.control.2dpoint   - Point.\n * @property {position_2d}  control_point_rgb_1.left_handle   - Left Handle.\n * @property {bool}  control_point_rgb_1.left_handle.separate=Off   - Separate.\n * @property {double}  control_point_rgb_1.left_handle.x=-0.1000   - Pos x.\n * @property {double}  control_point_rgb_1.left_handle.y=-0.1000   - Pos y.\n * @property {point_2d}  control_point_rgb_1.left_handle.2dpoint   - Point.\n * @property {position_2d}  control_point_rgb_1.right_handle   - Right Handle.\n * @property {bool}  control_point_rgb_1.right_handle.separate=Off   - Separate.\n * @property {double}  control_point_rgb_1.right_handle.x=0.1000   - Pos x.\n * @property {double}  control_point_rgb_1.right_handle.y=0.1000   - Pos y.\n * @property {point_2d}  control_point_rgb_1.right_handle.2dpoint   - Point.\n * @property {color-correction-control-point-attribute}  control_point_alpha_0   - Control point.\n * @property {position_2d}  control_point_alpha_0.control   - Control Point.\n * @property {bool}  control_point_alpha_0.control.separate=Off   - Separate.\n * @property {double}  control_point_alpha_0.control.x=0   - Pos x.\n * @property {double}  control_point_alpha_0.control.y=0   - Pos y.\n * @property {point_2d}  control_point_alpha_0.control.2dpoint   - Point.\n * @property {position_2d}  control_point_alpha_0.left_handle   - Left Handle.\n * @property {bool}  control_point_alpha_0.left_handle.separate=Off   - Separate.\n * @property {double}  control_point_alpha_0.left_handle.x=-0.1000   - Pos x.\n * @property {double}  control_point_alpha_0.left_handle.y=-0.1000   - Pos y.\n * @property {point_2d}  control_point_alpha_0.left_handle.2dpoint   - Point.\n * @property {position_2d}  control_point_alpha_0.right_handle   - Right Handle.\n * @property {bool}  control_point_alpha_0.right_handle.separate=Off   - Separate.\n * @property {double}  control_point_alpha_0.right_handle.x=0.1000   - Pos x.\n * @property {double}  control_point_alpha_0.right_handle.y=0.1000   - Pos y.\n * @property {point_2d}  control_point_alpha_0.right_handle.2dpoint   - Point.\n * @property {color-correction-control-point-attribute}  control_point_alpha_1   - Control point.\n * @property {position_2d}  control_point_alpha_1.control   - Control Point.\n * @property {bool}  control_point_alpha_1.control.separate=Off   - Separate.\n * @property {double}  control_point_alpha_1.control.x=1   - Pos x.\n * @property {double}  control_point_alpha_1.control.y=1   - Pos y.\n * @property {point_2d}  control_point_alpha_1.control.2dpoint   - Point.\n * @property {position_2d}  control_point_alpha_1.left_handle   - Left Handle.\n * @property {bool}  control_point_alpha_1.left_handle.separate=Off   - Separate.\n * @property {double}  control_point_alpha_1.left_handle.x=-0.1000   - Pos x.\n * @property {double}  control_point_alpha_1.left_handle.y=-0.1000   - Pos y.\n * @property {point_2d}  control_point_alpha_1.left_handle.2dpoint   - Point.\n * @property {position_2d}  control_point_alpha_1.right_handle   - Right Handle.\n * @property {bool}  control_point_alpha_1.right_handle.separate=Off   - Separate.\n * @property {double}  control_point_alpha_1.right_handle.x=0.1000   - Pos x.\n * @property {double}  control_point_alpha_1.right_handle.y=0.1000   - Pos y.\n * @property {point_2d}  control_point_alpha_1.right_handle.2dpoint   - Point.\n */\n\n\n/**\n * Attributes present in the node of type: 'HUE_SATURATION'\n * @name  NodeTypes#Hue-Saturation\n * @property {hue_range}  masterrangecolor   - Master.\n * @property {double}  masterrangecolor.hue_shift=0   - Hue.\n * @property {double}  masterrangecolor.saturation=0   - Saturation.\n * @property {double}  masterrangecolor.lightness=0   - Lightness.\n * @property {hue_range}  redrangecolor   - Reds.\n * @property {double}  redrangecolor.hue_shift=0   - Hue.\n * @property {double}  redrangecolor.saturation=0   - Saturation.\n * @property {double}  redrangecolor.lightness=0   - Lightness.\n * @property {double}  redrangecolor.low_range=345   - LowRange.\n * @property {double}  redrangecolor.high_range=15   - HighRange.\n * @property {double}  redrangecolor.low_falloff=30   - LowFalloff.\n * @property {double}  redrangecolor.high_falloff=30   - HighFalloff.\n * @property {hue_range}  greenrangecolor   - Greens.\n * @property {double}  greenrangecolor.hue_shift=0   - Hue.\n * @property {double}  greenrangecolor.saturation=0   - Saturation.\n * @property {double}  greenrangecolor.lightness=0   - Lightness.\n * @property {double}  greenrangecolor.low_range=105   - LowRange.\n * @property {double}  greenrangecolor.high_range=135   - HighRange.\n * @property {double}  greenrangecolor.low_falloff=30   - LowFalloff.\n * @property {double}  greenrangecolor.high_falloff=30   - HighFalloff.\n * @property {hue_range}  bluerangecolor   - Blues.\n * @property {double}  bluerangecolor.hue_shift=0   - Hue.\n * @property {double}  bluerangecolor.saturation=0   - Saturation.\n * @property {double}  bluerangecolor.lightness=0   - Lightness.\n * @property {double}  bluerangecolor.low_range=225   - LowRange.\n * @property {double}  bluerangecolor.high_range=255   - HighRange.\n * @property {double}  bluerangecolor.low_falloff=30   - LowFalloff.\n * @property {double}  bluerangecolor.high_falloff=30   - HighFalloff.\n * @property {hue_range}  cyanrangecolor   - Cyans.\n * @property {double}  cyanrangecolor.hue_shift=0   - Hue.\n * @property {double}  cyanrangecolor.saturation=0   - Saturation.\n * @property {double}  cyanrangecolor.lightness=0   - Lightness.\n * @property {double}  cyanrangecolor.low_range=165   - LowRange.\n * @property {double}  cyanrangecolor.high_range=195   - HighRange.\n * @property {double}  cyanrangecolor.low_falloff=30   - LowFalloff.\n * @property {double}  cyanrangecolor.high_falloff=30   - HighFalloff.\n * @property {hue_range}  magentarangecolor   - Magentas.\n * @property {double}  magentarangecolor.hue_shift=0   - Hue.\n * @property {double}  magentarangecolor.saturation=0   - Saturation.\n * @property {double}  magentarangecolor.lightness=0   - Lightness.\n * @property {double}  magentarangecolor.low_range=285   - LowRange.\n * @property {double}  magentarangecolor.high_range=315   - HighRange.\n * @property {double}  magentarangecolor.low_falloff=30   - LowFalloff.\n * @property {double}  magentarangecolor.high_falloff=30   - HighFalloff.\n * @property {hue_range}  yellowrangecolor   - Yellows.\n * @property {double}  yellowrangecolor.hue_shift=0   - Hue.\n * @property {double}  yellowrangecolor.saturation=0   - Saturation.\n * @property {double}  yellowrangecolor.lightness=0   - Lightness.\n * @property {double}  yellowrangecolor.low_range=45   - LowRange.\n * @property {double}  yellowrangecolor.high_range=75   - HighRange.\n * @property {double}  yellowrangecolor.low_falloff=30   - LowFalloff.\n * @property {double}  yellowrangecolor.high_falloff=30   - HighFalloff.\n * @property {bool}  colorizeenable=false   - Colorize.\n * @property {hsl}  colorizehsl   - Colorize HSL.\n * @property {double}  colorizehsl.hue=0   - Hue.\n * @property {double}  colorizehsl.saturation=25   - Saturation.\n * @property {double}  colorizehsl.lightness=0   - Lightness.\n * @property {push_button}  resetred   - Reset Range.\n * @property {push_button}  resetgreen   - Reset Range.\n * @property {push_button}  resetblue   - Reset Range.\n * @property {push_button}  resetcyan   - Reset Range.\n * @property {push_button}  resetmagenta   - Reset Range.\n * @property {push_button}  resetyellow   - Reset Range.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'NEGATE'\n * @name  NodeTypes#Negate\n * @property {bool}  color=true   - Negate Colour.\n * @property {bool}  color_alpha=false   - Negate Alpha.\n * @property {bool}  color_clamp_to_alpha=true   - Negate Colour Clamp to Alpha.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'TurbulentNoise'\n * @name  NodeTypes#TurbulentNoise\n * @property {int}  depth=0   - Depth.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {generic_enum}  [fractal_type=\"Fractional Brownian\"]   - Fractal Type.\n * @property {generic_enum}  [noise_type=\"Perlin\"]   - Noise Type.\n * @property {locked}  frequency   - Frequency.\n * @property {bool}  frequency.separate=Off   - Separate.\n * @property {doublevb}  frequency.xyfrequency=0   - Frequency xy.\n * @property {doublevb}  frequency.xfrequency=0   - Frequency x.\n * @property {doublevb}  frequency.yfrequency=0   - Frequency y.\n * @property {locked}  offset   - Offset.\n * @property {bool}  offset.separate=Off   - Separate.\n * @property {doublevb}  offset.xyoffset=0   - Offset xy.\n * @property {doublevb}  offset.xoffset=0   - Offset x.\n * @property {doublevb}  offset.yoffset=0   - Offset y.\n * @property {double}  evolution=0   - Evolution.\n * @property {double}  evolution_frequency=0   - Evolution Frequency.\n * @property {double}  gain=0.6500   - Gain.\n * @property {double}  lacunarity=2   - Sub Scaling.\n * @property {double}  octaves=1   - Complexity.\n */\n\n\n/**\n * Attributes present in the node of type: 'BezierMesh'\n * @name  NodeTypes#Mesh-Warp\n * @property {array_position_2d}  mesh   - Mesh.\n * @property {int}  mesh.size=105   - Size.\n * @property {position_2d}  mesh.meshpoint0x0   - MeshPoint0x0.\n * @property {bool}  mesh.meshpoint0x0.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x0.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint0x0.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x0.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x1   - MeshPoint0x1.\n * @property {bool}  mesh.meshpoint0x1.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x1.x=-10   - Pos x.\n * @property {double}  mesh.meshpoint0x1.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x1.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x2   - MeshPoint0x2.\n * @property {bool}  mesh.meshpoint0x2.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x2.x=-8   - Pos x.\n * @property {double}  mesh.meshpoint0x2.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x2.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x3   - MeshPoint0x3.\n * @property {bool}  mesh.meshpoint0x3.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x3.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint0x3.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x3.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x4   - MeshPoint0x4.\n * @property {bool}  mesh.meshpoint0x4.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x4.x=-4   - Pos x.\n * @property {double}  mesh.meshpoint0x4.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x4.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x5   - MeshPoint0x5.\n * @property {bool}  mesh.meshpoint0x5.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x5.x=-2   - Pos x.\n * @property {double}  mesh.meshpoint0x5.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x5.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x6   - MeshPoint0x6.\n * @property {bool}  mesh.meshpoint0x6.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x6.x=0   - Pos x.\n * @property {double}  mesh.meshpoint0x6.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x6.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x7   - MeshPoint0x7.\n * @property {bool}  mesh.meshpoint0x7.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x7.x=2   - Pos x.\n * @property {double}  mesh.meshpoint0x7.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x7.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x8   - MeshPoint0x8.\n * @property {bool}  mesh.meshpoint0x8.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x8.x=4   - Pos x.\n * @property {double}  mesh.meshpoint0x8.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x8.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x9   - MeshPoint0x9.\n * @property {bool}  mesh.meshpoint0x9.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x9.x=6   - Pos x.\n * @property {double}  mesh.meshpoint0x9.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x9.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x10   - MeshPoint0x10.\n * @property {bool}  mesh.meshpoint0x10.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x10.x=8   - Pos x.\n * @property {double}  mesh.meshpoint0x10.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x10.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x11   - MeshPoint0x11.\n * @property {bool}  mesh.meshpoint0x11.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x11.x=10   - Pos x.\n * @property {double}  mesh.meshpoint0x11.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x11.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x12   - MeshPoint0x12.\n * @property {bool}  mesh.meshpoint0x12.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x12.x=12   - Pos x.\n * @property {double}  mesh.meshpoint0x12.y=-12   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x12.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x13   - MeshPoint0x13.\n * @property {bool}  mesh.meshpoint0x13.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x13.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint0x13.y=-10   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x13.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x14   - MeshPoint0x14.\n * @property {bool}  mesh.meshpoint0x14.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x14.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint0x14.y=-10   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x14.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x15   - MeshPoint0x15.\n * @property {bool}  mesh.meshpoint0x15.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x15.x=0   - Pos x.\n * @property {double}  mesh.meshpoint0x15.y=-10   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x15.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x16   - MeshPoint0x16.\n * @property {bool}  mesh.meshpoint0x16.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x16.x=6   - Pos x.\n * @property {double}  mesh.meshpoint0x16.y=-10   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x16.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x17   - MeshPoint0x17.\n * @property {bool}  mesh.meshpoint0x17.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x17.x=12   - Pos x.\n * @property {double}  mesh.meshpoint0x17.y=-10   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x17.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x18   - MeshPoint0x18.\n * @property {bool}  mesh.meshpoint0x18.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x18.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint0x18.y=-8   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x18.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x19   - MeshPoint0x19.\n * @property {bool}  mesh.meshpoint0x19.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x19.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint0x19.y=-8   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x19.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x20   - MeshPoint0x20.\n * @property {bool}  mesh.meshpoint0x20.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x20.x=0   - Pos x.\n * @property {double}  mesh.meshpoint0x20.y=-8   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x20.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x21   - MeshPoint0x21.\n * @property {bool}  mesh.meshpoint0x21.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x21.x=6   - Pos x.\n * @property {double}  mesh.meshpoint0x21.y=-8   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x21.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x22   - MeshPoint0x22.\n * @property {bool}  mesh.meshpoint0x22.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x22.x=12   - Pos x.\n * @property {double}  mesh.meshpoint0x22.y=-8   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x22.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x23   - MeshPoint0x23.\n * @property {bool}  mesh.meshpoint0x23.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x23.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint0x23.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x23.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x24   - MeshPoint0x24.\n * @property {bool}  mesh.meshpoint0x24.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x24.x=-10   - Pos x.\n * @property {double}  mesh.meshpoint0x24.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x24.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x25   - MeshPoint0x25.\n * @property {bool}  mesh.meshpoint0x25.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x25.x=-8   - Pos x.\n * @property {double}  mesh.meshpoint0x25.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x25.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x26   - MeshPoint0x26.\n * @property {bool}  mesh.meshpoint0x26.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x26.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint0x26.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x26.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x27   - MeshPoint0x27.\n * @property {bool}  mesh.meshpoint0x27.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x27.x=-4   - Pos x.\n * @property {double}  mesh.meshpoint0x27.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x27.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x28   - MeshPoint0x28.\n * @property {bool}  mesh.meshpoint0x28.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x28.x=-2   - Pos x.\n * @property {double}  mesh.meshpoint0x28.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x28.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x29   - MeshPoint0x29.\n * @property {bool}  mesh.meshpoint0x29.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x29.x=0   - Pos x.\n * @property {double}  mesh.meshpoint0x29.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x29.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint0x30   - MeshPoint0x30.\n * @property {bool}  mesh.meshpoint0x30.separate=On   - Separate.\n * @property {double}  mesh.meshpoint0x30.x=2   - Pos x.\n * @property {double}  mesh.meshpoint0x30.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint0x30.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint1x0   - MeshPoint1x0.\n * @property {bool}  mesh.meshpoint1x0.separate=On   - Separate.\n * @property {double}  mesh.meshpoint1x0.x=4   - Pos x.\n * @property {double}  mesh.meshpoint1x0.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint1x0.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint1x1   - MeshPoint1x1.\n * @property {bool}  mesh.meshpoint1x1.separate=On   - Separate.\n * @property {double}  mesh.meshpoint1x1.x=6   - Pos x.\n * @property {double}  mesh.meshpoint1x1.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint1x1.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint1x2   - MeshPoint1x2.\n * @property {bool}  mesh.meshpoint1x2.separate=On   - Separate.\n * @property {double}  mesh.meshpoint1x2.x=8   - Pos x.\n * @property {double}  mesh.meshpoint1x2.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint1x2.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint1x3   - MeshPoint1x3.\n * @property {bool}  mesh.meshpoint1x3.separate=On   - Separate.\n * @property {double}  mesh.meshpoint1x3.x=10   - Pos x.\n * @property {double}  mesh.meshpoint1x3.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint1x3.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint1x4   - MeshPoint1x4.\n * @property {bool}  mesh.meshpoint1x4.separate=On   - Separate.\n * @property {double}  mesh.meshpoint1x4.x=12   - Pos x.\n * @property {double}  mesh.meshpoint1x4.y=-6   - Pos y.\n * @property {point_2d}  mesh.meshpoint1x4.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint1x5   - MeshPoint1x5.\n * @property {bool}  mesh.meshpoint1x5.separate=On   - Separate.\n * @property {double}  mesh.meshpoint1x5.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint1x5.y=-4   - Pos y.\n * @property {point_2d}  mesh.meshpoint1x5.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint1x6   - MeshPoint1x6.\n * @property {bool}  mesh.meshpoint1x6.separate=On   - Separate.\n * @property {double}  mesh.meshpoint1x6.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint1x6.y=-4   - Pos y.\n * @property {point_2d}  mesh.meshpoint1x6.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint1x7   - MeshPoint1x7.\n * @property {bool}  mesh.meshpoint1x7.separate=On   - Separate.\n * @property {double}  mesh.meshpoint1x7.x=0   - Pos x.\n * @property {double}  mesh.meshpoint1x7.y=-4   - Pos y.\n * @property {point_2d}  mesh.meshpoint1x7.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint1x8   - MeshPoint1x8.\n * @property {bool}  mesh.meshpoint1x8.separate=On   - Separate.\n * @property {double}  mesh.meshpoint1x8.x=6   - Pos x.\n * @property {double}  mesh.meshpoint1x8.y=-4   - Pos y.\n * @property {point_2d}  mesh.meshpoint1x8.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint1x9   - MeshPoint1x9.\n * @property {bool}  mesh.meshpoint1x9.separate=On   - Separate.\n * @property {double}  mesh.meshpoint1x9.x=12   - Pos x.\n * @property {double}  mesh.meshpoint1x9.y=-4   - Pos y.\n * @property {point_2d}  mesh.meshpoint1x9.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint1x10   - MeshPoint1x10.\n * @property {bool}  mesh.meshpoint1x10.separate=On   - Separate.\n * @property {double}  mesh.meshpoint1x10.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint1x10.y=-2   - Pos y.\n * @property {point_2d}  mesh.meshpoint1x10.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint2x0   - MeshPoint2x0.\n * @property {bool}  mesh.meshpoint2x0.separate=On   - Separate.\n * @property {double}  mesh.meshpoint2x0.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint2x0.y=-2   - Pos y.\n * @property {point_2d}  mesh.meshpoint2x0.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint2x1   - MeshPoint2x1.\n * @property {bool}  mesh.meshpoint2x1.separate=On   - Separate.\n * @property {double}  mesh.meshpoint2x1.x=0   - Pos x.\n * @property {double}  mesh.meshpoint2x1.y=-2   - Pos y.\n * @property {point_2d}  mesh.meshpoint2x1.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint2x2   - MeshPoint2x2.\n * @property {bool}  mesh.meshpoint2x2.separate=On   - Separate.\n * @property {double}  mesh.meshpoint2x2.x=6   - Pos x.\n * @property {double}  mesh.meshpoint2x2.y=-2   - Pos y.\n * @property {point_2d}  mesh.meshpoint2x2.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint2x3   - MeshPoint2x3.\n * @property {bool}  mesh.meshpoint2x3.separate=On   - Separate.\n * @property {double}  mesh.meshpoint2x3.x=12   - Pos x.\n * @property {double}  mesh.meshpoint2x3.y=-2   - Pos y.\n * @property {point_2d}  mesh.meshpoint2x3.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint2x4   - MeshPoint2x4.\n * @property {bool}  mesh.meshpoint2x4.separate=On   - Separate.\n * @property {double}  mesh.meshpoint2x4.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint2x4.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint2x4.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint2x5   - MeshPoint2x5.\n * @property {bool}  mesh.meshpoint2x5.separate=On   - Separate.\n * @property {double}  mesh.meshpoint2x5.x=-10   - Pos x.\n * @property {double}  mesh.meshpoint2x5.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint2x5.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint2x6   - MeshPoint2x6.\n * @property {bool}  mesh.meshpoint2x6.separate=On   - Separate.\n * @property {double}  mesh.meshpoint2x6.x=-8   - Pos x.\n * @property {double}  mesh.meshpoint2x6.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint2x6.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint2x7   - MeshPoint2x7.\n * @property {bool}  mesh.meshpoint2x7.separate=On   - Separate.\n * @property {double}  mesh.meshpoint2x7.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint2x7.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint2x7.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint2x8   - MeshPoint2x8.\n * @property {bool}  mesh.meshpoint2x8.separate=On   - Separate.\n * @property {double}  mesh.meshpoint2x8.x=-4   - Pos x.\n * @property {double}  mesh.meshpoint2x8.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint2x8.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint2x9   - MeshPoint2x9.\n * @property {bool}  mesh.meshpoint2x9.separate=On   - Separate.\n * @property {double}  mesh.meshpoint2x9.x=-2   - Pos x.\n * @property {double}  mesh.meshpoint2x9.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint2x9.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint2x10   - MeshPoint2x10.\n * @property {bool}  mesh.meshpoint2x10.separate=On   - Separate.\n * @property {double}  mesh.meshpoint2x10.x=0   - Pos x.\n * @property {double}  mesh.meshpoint2x10.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint2x10.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x0   - MeshPoint3x0.\n * @property {bool}  mesh.meshpoint3x0.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x0.x=2   - Pos x.\n * @property {double}  mesh.meshpoint3x0.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x0.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x1   - MeshPoint3x1.\n * @property {bool}  mesh.meshpoint3x1.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x1.x=4   - Pos x.\n * @property {double}  mesh.meshpoint3x1.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x1.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x2   - MeshPoint3x2.\n * @property {bool}  mesh.meshpoint3x2.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x2.x=6   - Pos x.\n * @property {double}  mesh.meshpoint3x2.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x2.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x3   - MeshPoint3x3.\n * @property {bool}  mesh.meshpoint3x3.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x3.x=8   - Pos x.\n * @property {double}  mesh.meshpoint3x3.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x3.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x4   - MeshPoint3x4.\n * @property {bool}  mesh.meshpoint3x4.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x4.x=10   - Pos x.\n * @property {double}  mesh.meshpoint3x4.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x4.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x5   - MeshPoint3x5.\n * @property {bool}  mesh.meshpoint3x5.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x5.x=12   - Pos x.\n * @property {double}  mesh.meshpoint3x5.y=0   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x5.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x6   - MeshPoint3x6.\n * @property {bool}  mesh.meshpoint3x6.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x6.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint3x6.y=2   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x6.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x7   - MeshPoint3x7.\n * @property {bool}  mesh.meshpoint3x7.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x7.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint3x7.y=2   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x7.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x8   - MeshPoint3x8.\n * @property {bool}  mesh.meshpoint3x8.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x8.x=0   - Pos x.\n * @property {double}  mesh.meshpoint3x8.y=2   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x8.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x9   - MeshPoint3x9.\n * @property {bool}  mesh.meshpoint3x9.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x9.x=6   - Pos x.\n * @property {double}  mesh.meshpoint3x9.y=2   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x9.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x10   - MeshPoint3x10.\n * @property {bool}  mesh.meshpoint3x10.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x10.x=12   - Pos x.\n * @property {double}  mesh.meshpoint3x10.y=2   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x10.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x11   - MeshPoint3x11.\n * @property {bool}  mesh.meshpoint3x11.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x11.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint3x11.y=4   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x11.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x12   - MeshPoint3x12.\n * @property {bool}  mesh.meshpoint3x12.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x12.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint3x12.y=4   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x12.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x13   - MeshPoint3x13.\n * @property {bool}  mesh.meshpoint3x13.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x13.x=0   - Pos x.\n * @property {double}  mesh.meshpoint3x13.y=4   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x13.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x14   - MeshPoint3x14.\n * @property {bool}  mesh.meshpoint3x14.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x14.x=6   - Pos x.\n * @property {double}  mesh.meshpoint3x14.y=4   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x14.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x15   - MeshPoint3x15.\n * @property {bool}  mesh.meshpoint3x15.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x15.x=12   - Pos x.\n * @property {double}  mesh.meshpoint3x15.y=4   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x15.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x16   - MeshPoint3x16.\n * @property {bool}  mesh.meshpoint3x16.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x16.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint3x16.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x16.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x17   - MeshPoint3x17.\n * @property {bool}  mesh.meshpoint3x17.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x17.x=-10   - Pos x.\n * @property {double}  mesh.meshpoint3x17.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x17.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x18   - MeshPoint3x18.\n * @property {bool}  mesh.meshpoint3x18.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x18.x=-8   - Pos x.\n * @property {double}  mesh.meshpoint3x18.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x18.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x19   - MeshPoint3x19.\n * @property {bool}  mesh.meshpoint3x19.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x19.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint3x19.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x19.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x20   - MeshPoint3x20.\n * @property {bool}  mesh.meshpoint3x20.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x20.x=-4   - Pos x.\n * @property {double}  mesh.meshpoint3x20.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x20.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x21   - MeshPoint3x21.\n * @property {bool}  mesh.meshpoint3x21.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x21.x=-2   - Pos x.\n * @property {double}  mesh.meshpoint3x21.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x21.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x22   - MeshPoint3x22.\n * @property {bool}  mesh.meshpoint3x22.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x22.x=0   - Pos x.\n * @property {double}  mesh.meshpoint3x22.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x22.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x23   - MeshPoint3x23.\n * @property {bool}  mesh.meshpoint3x23.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x23.x=2   - Pos x.\n * @property {double}  mesh.meshpoint3x23.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x23.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x24   - MeshPoint3x24.\n * @property {bool}  mesh.meshpoint3x24.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x24.x=4   - Pos x.\n * @property {double}  mesh.meshpoint3x24.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x24.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x25   - MeshPoint3x25.\n * @property {bool}  mesh.meshpoint3x25.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x25.x=6   - Pos x.\n * @property {double}  mesh.meshpoint3x25.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x25.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x26   - MeshPoint3x26.\n * @property {bool}  mesh.meshpoint3x26.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x26.x=8   - Pos x.\n * @property {double}  mesh.meshpoint3x26.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x26.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x27   - MeshPoint3x27.\n * @property {bool}  mesh.meshpoint3x27.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x27.x=10   - Pos x.\n * @property {double}  mesh.meshpoint3x27.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x27.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x28   - MeshPoint3x28.\n * @property {bool}  mesh.meshpoint3x28.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x28.x=12   - Pos x.\n * @property {double}  mesh.meshpoint3x28.y=6   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x28.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x29   - MeshPoint3x29.\n * @property {bool}  mesh.meshpoint3x29.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x29.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint3x29.y=8   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x29.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint3x30   - MeshPoint3x30.\n * @property {bool}  mesh.meshpoint3x30.separate=On   - Separate.\n * @property {double}  mesh.meshpoint3x30.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint3x30.y=8   - Pos y.\n * @property {point_2d}  mesh.meshpoint3x30.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint4x0   - MeshPoint4x0.\n * @property {bool}  mesh.meshpoint4x0.separate=On   - Separate.\n * @property {double}  mesh.meshpoint4x0.x=0   - Pos x.\n * @property {double}  mesh.meshpoint4x0.y=8   - Pos y.\n * @property {point_2d}  mesh.meshpoint4x0.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint4x1   - MeshPoint4x1.\n * @property {bool}  mesh.meshpoint4x1.separate=On   - Separate.\n * @property {double}  mesh.meshpoint4x1.x=6   - Pos x.\n * @property {double}  mesh.meshpoint4x1.y=8   - Pos y.\n * @property {point_2d}  mesh.meshpoint4x1.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint4x2   - MeshPoint4x2.\n * @property {bool}  mesh.meshpoint4x2.separate=On   - Separate.\n * @property {double}  mesh.meshpoint4x2.x=12   - Pos x.\n * @property {double}  mesh.meshpoint4x2.y=8   - Pos y.\n * @property {point_2d}  mesh.meshpoint4x2.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint4x3   - MeshPoint4x3.\n * @property {bool}  mesh.meshpoint4x3.separate=On   - Separate.\n * @property {double}  mesh.meshpoint4x3.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint4x3.y=10   - Pos y.\n * @property {point_2d}  mesh.meshpoint4x3.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint4x4   - MeshPoint4x4.\n * @property {bool}  mesh.meshpoint4x4.separate=On   - Separate.\n * @property {double}  mesh.meshpoint4x4.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint4x4.y=10   - Pos y.\n * @property {point_2d}  mesh.meshpoint4x4.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint4x5   - MeshPoint4x5.\n * @property {bool}  mesh.meshpoint4x5.separate=On   - Separate.\n * @property {double}  mesh.meshpoint4x5.x=0   - Pos x.\n * @property {double}  mesh.meshpoint4x5.y=10   - Pos y.\n * @property {point_2d}  mesh.meshpoint4x5.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint4x6   - MeshPoint4x6.\n * @property {bool}  mesh.meshpoint4x6.separate=On   - Separate.\n * @property {double}  mesh.meshpoint4x6.x=6   - Pos x.\n * @property {double}  mesh.meshpoint4x6.y=10   - Pos y.\n * @property {point_2d}  mesh.meshpoint4x6.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint4x7   - MeshPoint4x7.\n * @property {bool}  mesh.meshpoint4x7.separate=On   - Separate.\n * @property {double}  mesh.meshpoint4x7.x=12   - Pos x.\n * @property {double}  mesh.meshpoint4x7.y=10   - Pos y.\n * @property {point_2d}  mesh.meshpoint4x7.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint4x8   - MeshPoint4x8.\n * @property {bool}  mesh.meshpoint4x8.separate=On   - Separate.\n * @property {double}  mesh.meshpoint4x8.x=-12   - Pos x.\n * @property {double}  mesh.meshpoint4x8.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint4x8.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint4x9   - MeshPoint4x9.\n * @property {bool}  mesh.meshpoint4x9.separate=On   - Separate.\n * @property {double}  mesh.meshpoint4x9.x=-10   - Pos x.\n * @property {double}  mesh.meshpoint4x9.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint4x9.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint4x10   - MeshPoint4x10.\n * @property {bool}  mesh.meshpoint4x10.separate=On   - Separate.\n * @property {double}  mesh.meshpoint4x10.x=-8   - Pos x.\n * @property {double}  mesh.meshpoint4x10.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint4x10.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint5x0   - MeshPoint5x0.\n * @property {bool}  mesh.meshpoint5x0.separate=On   - Separate.\n * @property {double}  mesh.meshpoint5x0.x=-6   - Pos x.\n * @property {double}  mesh.meshpoint5x0.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint5x0.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint5x1   - MeshPoint5x1.\n * @property {bool}  mesh.meshpoint5x1.separate=On   - Separate.\n * @property {double}  mesh.meshpoint5x1.x=-4   - Pos x.\n * @property {double}  mesh.meshpoint5x1.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint5x1.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint5x2   - MeshPoint5x2.\n * @property {bool}  mesh.meshpoint5x2.separate=On   - Separate.\n * @property {double}  mesh.meshpoint5x2.x=-2   - Pos x.\n * @property {double}  mesh.meshpoint5x2.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint5x2.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint5x3   - MeshPoint5x3.\n * @property {bool}  mesh.meshpoint5x3.separate=On   - Separate.\n * @property {double}  mesh.meshpoint5x3.x=0   - Pos x.\n * @property {double}  mesh.meshpoint5x3.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint5x3.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint5x4   - MeshPoint5x4.\n * @property {bool}  mesh.meshpoint5x4.separate=On   - Separate.\n * @property {double}  mesh.meshpoint5x4.x=2   - Pos x.\n * @property {double}  mesh.meshpoint5x4.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint5x4.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint5x5   - MeshPoint5x5.\n * @property {bool}  mesh.meshpoint5x5.separate=On   - Separate.\n * @property {double}  mesh.meshpoint5x5.x=4   - Pos x.\n * @property {double}  mesh.meshpoint5x5.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint5x5.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint5x6   - MeshPoint5x6.\n * @property {bool}  mesh.meshpoint5x6.separate=On   - Separate.\n * @property {double}  mesh.meshpoint5x6.x=6   - Pos x.\n * @property {double}  mesh.meshpoint5x6.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint5x6.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint5x7   - MeshPoint5x7.\n * @property {bool}  mesh.meshpoint5x7.separate=On   - Separate.\n * @property {double}  mesh.meshpoint5x7.x=8   - Pos x.\n * @property {double}  mesh.meshpoint5x7.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint5x7.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint5x8   - MeshPoint5x8.\n * @property {bool}  mesh.meshpoint5x8.separate=On   - Separate.\n * @property {double}  mesh.meshpoint5x8.x=10   - Pos x.\n * @property {double}  mesh.meshpoint5x8.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint5x8.2dpoint   - Point.\n * @property {position_2d}  mesh.meshpoint5x9   - MeshPoint5x9.\n * @property {bool}  mesh.meshpoint5x9.separate=On   - Separate.\n * @property {double}  mesh.meshpoint5x9.x=12   - Pos x.\n * @property {double}  mesh.meshpoint5x9.y=12   - Pos y.\n * @property {point_2d}  mesh.meshpoint5x9.2dpoint   - Point.\n * @property {int}  mesh.rows=4   - Rows.\n * @property {int}  mesh.columns=4   - Columns.\n * @property {position_2d}  origin   - Origin.\n * @property {bool}  origin.separate=On   - Separate.\n * @property {double}  origin.x=0   - Pos x.\n * @property {double}  origin.y=0   - Pos y.\n * @property {point_2d}  origin.2dpoint   - Point.\n * @property {double}  width=12   - Width.\n * @property {double}  height=12   - Height.\n * @property {generic_enum}  [deformationquality=\"Very High\"]   - Deformation Quality.\n */\n\n\n/**\n * Attributes present in the node of type: 'EXTERNAL'\n * @name  NodeTypes#External\n * @property {string}  program_name   - External Program.\n * @property {string}  program_input   - Program First Input File ($IN1).\n * @property {string}  program_input2   - Program Second Input File ($IN2).\n * @property {string}  program_output   - Program Output File ($OUT).\n * @property {string}  extension=TGA   - Extension ($EXT).\n * @property {double}  program_num_param=0   - Numerical Parameter 1 ($NUM).\n * @property {bool}  program_uniqueid=true   - Generate Unique FileNames.\n * @property {double}  program_num_param_2=0   - Numerical Parameter 2 ($NUM2).\n * @property {double}  program_num_param_3=0   - Numerical Parameter 3 ($NUM3).\n * @property {double}  program_num_param_4=0   - Numerical Parameter 4 ($NUM4).\n * @property {double}  program_num_param_5=0   - Numerical Parameter 5 ($NUM5).\n */\n\n\n/**\n * Attributes present in the node of type: 'PointConstraint2'\n * @name  NodeTypes#Two-Points-Constraint\n * @property {double}  active=100   - Active.\n * @property {double}  volumemod=75   - Volume Modifier.\n * @property {double}  volumemax=200   - Volume Max.\n * @property {double}  volumemin=25   - Volume Min.\n * @property {double}  stretchmax=0   - Distance Max.\n * @property {double}  stretchmin=0   - Distance Min.\n * @property {double}  skewcontrol=0   - Skew Modifier.\n * @property {double}  smooth=0   - Smoothing.\n * @property {double}  balance=0   - Point Balance.\n * @property {generic_enum}  [flattentype=\"Allow 3D Transform\"]   - Flatten Type.\n * @property {generic_enum}  [transformtype=\"Translate\"]   - Transform Type.\n * @property {generic_enum}  [primaryport=\"Right\"]   - Primary Port.\n */\n\n\n/**\n * Attributes present in the node of type: 'COLOR_SCALE'\n * @name  NodeTypes#Colour-Scale\n * @property {double}  red=1   - Red.\n * @property {double}  green=1   - Green.\n * @property {double}  blue=1   - Blue.\n * @property {double}  alpha=1   - Alpha.\n * @property {double}  hue=1   - Hue.\n * @property {double}  hue_offset=0   - Hue Offset.\n * @property {double}  saturation=1   - Saturation.\n * @property {double}  value=1   - Value.\n * @property {bool}  clampneg=false   - Clamp Negative.\n * @property {bool}  pre_multiplied_alpha=false   - Pre-multiplied Alpha.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'LightPosition'\n * @name  NodeTypes#Light-Position\n * @property {position_3d}  position0   - Position.\n * @property {bool}  position0.separate=On   - Separate.\n * @property {double}  position0.x=0   - Pos x.\n * @property {double}  position0.y=0   - Pos y.\n * @property {double}  position0.z=0   - Pos z.\n * @property {path_3d}  position0.3dpath   - Path.\n * @property {position_3d}  position1   - Look at.\n * @property {bool}  position1.separate=On   - Separate.\n * @property {double}  position1.x=1   - Pos x.\n * @property {double}  position1.y=0   - Pos y.\n * @property {double}  position1.z=0   - Pos z.\n * @property {path_3d}  position1.3dpath   - Path.\n */\n\n\n/**\n * Attributes present in the node of type: 'SurfaceNormal'\n * @name  NodeTypes#Surface-Normal\n * @property {generic_enum}  [normalquality=\"Low\"]   - Surface Normal Quality.\n */\n\n\n/**\n * Attributes present in the node of type: 'FilterBanding'\n * @name  NodeTypes#Colour-Banding\n * @property {double}  threshold1=20   - Threshold 1.\n * @property {color}  colour1=ffffffff   - Colour 1.\n * @property {int}  colour1.red=255   - Red.\n * @property {int}  colour1.green=255   - Green.\n * @property {int}  colour1.blue=255   - Blue.\n * @property {int}  colour1.alpha=255   - Alpha.\n * @property {generic_enum}  [colour1.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {double}  blur1=0   - Blur 1.\n * @property {double}  threshold2=40   - Threshold 2.\n * @property {color}  colour2=ffffffff   - Colour 2.\n * @property {int}  colour2.red=255   - Red.\n * @property {int}  colour2.green=255   - Green.\n * @property {int}  colour2.blue=255   - Blue.\n * @property {int}  colour2.alpha=255   - Alpha.\n * @property {generic_enum}  [colour2.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {double}  blur2=0   - Blur 2.\n * @property {double}  threshold3=20   - Threshold 3.\n * @property {color}  colour3=ffffffff   - Colour 3.\n * @property {int}  colour3.red=255   - Red.\n * @property {int}  colour3.green=255   - Green.\n * @property {int}  colour3.blue=255   - Blue.\n * @property {int}  colour3.alpha=255   - Alpha.\n * @property {generic_enum}  [colour3.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {double}  blur3=0   - Blur 3.\n * @property {double}  threshold4=20   - Threshold 4.\n * @property {color}  colour4=ffffffff   - Colour 4.\n * @property {int}  colour4.red=255   - Red.\n * @property {int}  colour4.green=255   - Green.\n * @property {int}  colour4.blue=255   - Blue.\n * @property {int}  colour4.alpha=255   - Alpha.\n * @property {generic_enum}  [colour4.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {double}  blur4=0   - Blur 4.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'MATTE_RESIZE'\n * @name  NodeTypes#Matte-Resize\n * @property {double}  radius=0   - Radius.\n */\n\n\n/**\n * Attributes present in the node of type: 'IncreaseOpacity'\n * @name  NodeTypes#Increase-Opacity\n * @property {double}  factor=1.8000   - Factor.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'FLICKER_BLUR'\n * @name  NodeTypes#Anti-Flicker\n * @property {double}  radius=0   - Radius.\n */\n\n\n/**\n * Attributes present in the node of type: 'ObjectDefinition'\n * @name  NodeTypes#Volume-Object\n * @property {int}  objectid=1   - ID.\n * @property {bool}  cutvolumecues=false   - Cut Volume Cues with Geometry.\n * @property {bool}  usegeometry=true   - Use Drawing to Create Volume.\n * @property {double}  geometryintensity=50   - Elevation Baseline.\n * @property {double}  elevationmin=0   - Elevation Min.\n * @property {double}  elevationmax=1   - Elevation Max.\n * @property {double}  smoothness=1   - Elevation Smoothness.\n * @property {bool}  invertmask=false   - Invert Height Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'LuminanceThreshold'\n * @name  NodeTypes#Luminance-Threshold\n * @property {double}  luminancethresholdthresh=75   - Threshold.\n * @property {bool}  luminancethresholdsoften=true   - Soften Colours.\n * @property {double}  luminancethresholdgamma=1.5000   - Gamma Correction.\n * @property {bool}  luminancethresholdgrey=false   - Output Greyscale Matte.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'PIXELATE'\n * @name  NodeTypes#Pixelate\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n * @property {double}  factor=0.0125   - Factor.\n */\n\n\n/**\n * Attributes present in the node of type: 'LightShader'\n * @name  NodeTypes#Light-Shader\n * @property {generic_enum}  [lighttype=\"Directional\"]   - Light Type.\n * @property {double}  floodangle=90   - Cone Angle.\n * @property {double}  floodsharpness=0   - Diffusion.\n * @property {double}  floodradius=2000   - Falloff.\n * @property {double}  pointelevation=200   - Light Source Elevation.\n * @property {double}  anglethreshold=90   - Surface Reflectivity.\n * @property {generic_enum}  [shadetype=\"Smooth\"]   - Shading Type.\n * @property {double}  bias=0.1000   - Bias.\n * @property {double}  exponent=2   - Abruptness.\n * @property {color}  lightcolor=ffc8c8c8   - Light Colour.\n * @property {int}  lightcolor.red=200   - Red.\n * @property {int}  lightcolor.green=200   - Green.\n * @property {int}  lightcolor.blue=200   - Blue.\n * @property {int}  lightcolor.alpha=255   - Alpha.\n * @property {generic_enum}  [lightcolor.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  flatten=true   - Flatten Fx.\n * @property {bool}  useimagecolor=false   - Use image Colour.\n * @property {double}  imagecolorweight=50   - Image Colour Intensity.\n * @property {bool}  adjustlevel=false   - Adjust Light Intensity.\n * @property {double}  adjustedlevel=75   - Intensity.\n * @property {double}  scale=1   - Multiplier.\n * @property {bool}  usesurface=false   - Use Surface Lighting.\n * @property {double}  speculartransparency=100   - Specular Transparency.\n * @property {double}  specularity=100   - Specular Strength.\n * @property {double}  distancefalloff=0   - Distance Falloff.\n */\n\n\n/**\n * Attributes present in the node of type: 'DeformationRootModule'\n * @name  NodeTypes#Deformation-Root\n * @property {generic_enum}  [deformationquality=\"Very High\"]   - Quality.\n */\n\n\n/**\n * Attributes present in the node of type: 'DeformationSwitchModule'\n * @name  NodeTypes#Deformation-Switch\n * @property {generic_enum}  [vectorquality=\"Very High\"]   - Vector Quality.\n * @property {double}  fadeexponent=3   - Influence Fade Exponent.\n * @property {bool}  fadeinside=false   - Fade Inside Zones.\n * @property {int}  enabledeformation=1   - Enable Deformation.\n * @property {int}  chainselectionreference=1   - Kinematic Chain Selection Reference.\n */\n\n\n/**\n * Attributes present in the node of type: 'AutoMuscleModule'\n * @name  NodeTypes#Auto-Muscle\n * @property {bool}  enableleft=true   - Muscle Left.\n * @property {double}  leftstart=-3   - Left Start.\n * @property {double}  leftspan=1   - Left Span.\n * @property {double}  leftamplitude=0.2500   - Left Amplitude.\n * @property {bool}  enableright=true   - Muscle Right.\n * @property {bool}  symmetric=false   - Same as Left.\n * @property {double}  rightstart=-3   - Right Start.\n * @property {double}  rightspan=1   - Right Span.\n * @property {double}  rightamplitude=0.2500   - Right Amplitude.\n */\n\n\n/**\n * Attributes present in the node of type: 'AutoFoldModule'\n * @name  NodeTypes#Deformation-AutoFold\n * @property {int}  enable=1   - Enable AutoFold.\n * @property {double}  length=12   - Length.\n */\n\n\n/**\n * Attributes present in the node of type: 'DeformationScaleModule'\n * @name  NodeTypes#Deformation-Scale\n * @property {bool}  enableleft=true   - Scale Left.\n * @property {double}  leftfadein=0   - Left Fade In.\n * @property {double}  leftfadeout=0   - Left Fade Out.\n * @property {double}  leftstart=0   - Left Start.\n * @property {double}  leftspan=1   - Left Span.\n * @property {double}  leftscale0=1   - Left Start Scale.\n * @property {double}  leftscale1=1   - Left End Scale.\n * @property {double}  lefthandleposition0=25   - Left Start Handle Position.\n * @property {double}  lefthandleposition1=25   - Left End Handle Position.\n * @property {double}  lefthandlescale0=1   - Left Start Handle Scale.\n * @property {double}  lefthandlescale1=1   - Left End Handle Scale.\n * @property {bool}  enableright=true   - Scale Right.\n * @property {bool}  symmetric=false   - Same as Left.\n * @property {double}  rightfadein=0   - Right Fade In.\n * @property {double}  rightfadeout=0   - Right Fade Out.\n * @property {double}  rightstart=0   - Right Start.\n * @property {double}  rightspan=1   - Right Span.\n * @property {double}  rightscale0=1   - Right Start Scale.\n * @property {double}  rightscale1=1   - Right End Scale.\n * @property {double}  righthandleposition0=25   - Right Start Handle Position.\n * @property {double}  righthandleposition1=25   - Right End Handle Position.\n * @property {double}  righthandlescale0=1   - Right Start Handle Scale.\n * @property {double}  righthandlescale1=1   - Right End Handle Scale.\n */\n\n\n/**\n * Attributes present in the node of type: 'DeformationWaveModule'\n * @name  NodeTypes#Deformation-Wave\n * @property {bool}  enableleft=true   - Wave Left.\n * @property {double}  leftstart=0   - Left Start.\n * @property {double}  leftspan=10   - Left Span.\n * @property {double}  leftoffsett=0   - Left Offset Deformer.\n * @property {double}  leftamplitude=1   - Left Amplitude.\n * @property {double}  leftoffset=1   - Left Offset Scaling.\n * @property {double}  leftperiod=1   - Left Period.\n * @property {bool}  enableright=true   - Wave Right.\n * @property {bool}  symmetric=false   - Same as Left.\n * @property {double}  rightstart=0   - Right Start.\n * @property {double}  rightspan=10   - Right Span.\n * @property {double}  rightoffsett=0   - Right Offset Deformer.\n * @property {double}  rightamplitude=1   - Right Amplitude.\n * @property {double}  rightoffset=1   - Right Offset Scaling.\n * @property {double}  rightperiod=1   - Right Period.\n */\n\n\n/**\n * Attributes present in the node of type: 'FoldModule'\n * @name  NodeTypes#Deformation-Fold\n * @property {int}  enable=1   - Enable Fold.\n * @property {double}  t=1   - Where.\n * @property {double}  tbefore=1   - Span Before.\n * @property {double}  tafter=1   - Span After.\n * @property {double}  angle=0   - Orientation.\n * @property {double}  length=12   - Length.\n */\n\n\n/**\n * Attributes present in the node of type: 'DeformationUniformScaleModule'\n * @name  NodeTypes#Deformation-Uniform-Scale\n * @property {double}  scale=1   - Scale.\n */\n\n\n/**\n * Attributes present in the node of type: 'DEPTHBLUR'\n * @name  NodeTypes#Z-Buffer-Smoothing\n * @property {double}  histogram_range=80   - Histogram Range.\n * @property {int}  kernel_size=5   - Kernel Size.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleExplosion'\n * @name  NodeTypes#Explosion\n * @property {int}  trigger=1   - Trigger.\n * @property {double}  explosionx=0   - X.\n * @property {double}  explosiony=0   - Y.\n * @property {double}  explosionz=0   - Z.\n * @property {double}  explosionradius=3   - Radius.\n * @property {double}  explosionsigma=1   - Sigma.\n * @property {double}  explosionmagnitude=5   - Magnitude.\n * @property {double}  explosionepsilon=0.0010   - Epsilon.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleImageEmitter'\n * @name  NodeTypes#Image-Fracture\n * @property {int}  trigger=0   - Trigger.\n * @property {double}  ageatbirth=0   - Age at Birth.\n * @property {double}  ageatbirthstd=0   - Age at Birth Standard Deviation.\n * @property {double}  mass=1   - Particles Mass.\n * @property {generic_enum}  [typechoosingstrategy=\"Sequentially Assign Type Number\"]   - Type Generation Strategy.\n * @property {int}  particletype0=1   - Particle Type 0.\n * @property {int}  particletype1=1   - Particle Type 1.\n * @property {double}  particlesize=1   - Size over Age.\n * @property {bool}  overridevelocity=false   - Align Initial Velocity.\n * @property {generic_enum}  [blend_mode=\"Normal\"]   - Blend Mode.\n * @property {double}  blendintensity=100   - Blend Intensity.\n * @property {generic_enum}  [colouringstrategy=\"Use Drawing Colour\"]   - Colouring Strategy.\n * @property {color}  particlecolour=ffffffff   - Colour.\n * @property {int}  particlecolour.red=255   - Red.\n * @property {int}  particlecolour.green=255   - Green.\n * @property {int}  particlecolour.blue=255   - Blue.\n * @property {int}  particlecolour.alpha=255   - Alpha.\n * @property {generic_enum}  [particlecolour.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  alignwithdirection=true   - Align with Direction.\n * @property {bool}  userotation=false   - Use Rotation of Particle.\n * @property {bool}  directionalscale=false   - Directional Scale.\n * @property {double}  directionalscalefactor=1   - Directional Scale Exponent Factor.\n * @property {bool}  keepvolume=true   - Keep Volume.\n * @property {generic_enum}  [blur=\"No Blur\"]   - Blur.\n * @property {double}  blurintensity=1   - Blur Intensity.\n * @property {double}  blurfallof=0.5000   - Falloff Rate.\n * @property {bool}  flipwithdirectionx=false   - Flip X Axis to Match Direction.\n * @property {bool}  flipwithdirectiony=false   - Flip Y Axis to Match Direction.\n * @property {generic_enum}  [alignwithdirectionaxis=\"Positive X\"]   - Axis to Align.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleBounce'\n * @name  NodeTypes#Bounce\n * @property {int}  trigger=1   - Trigger.\n * @property {double}  friction=0.5000   - Friction.\n * @property {double}  resilience=0.5000   - Resilience.\n * @property {double}  cutoff=0.5000   - Cutoff Speed.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleMove'\n * @name  NodeTypes#Move-Particles\n * @property {int}  trigger=1   - Trigger.\n * @property {bool}  moveage=false   - Age Particles.\n * @property {bool}  moveposition=true   - Move Position.\n * @property {bool}  moveangle=true   - Move Angle.\n * @property {bool}  followeachother=false   - Make Particles Follow each Other.\n * @property {double}  followintensity=1   - Follow Intensity.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleKill'\n * @name  NodeTypes#Kill\n * @property {int}  trigger=1   - Trigger.\n * @property {bool}  handlenaturaldeth=true   - Use Maximum Lifespan.\n * @property {bool}  killyounger=false   - Kill Younger.\n * @property {int}  killyoungerthan=-1   - Kill Younger than.\n * @property {bool}  killolder=true   - Kill Older.\n * @property {int}  killolderthan=100   - Kill Older than.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleOrbit'\n * @name  NodeTypes#Orbit\n * @property {int}  trigger=1   - Trigger.\n * @property {generic_enum}  [strategy=\"Around Point\"]   - Orbit Type.\n * @property {double}  magnitude=1   - Magnitude.\n * @property {double}  v0x=0   - Point X.\n * @property {double}  v0y=0   - Point Y.\n * @property {double}  v0z=0   - Point Z.\n * @property {double}  v1x=0   - Direction X.\n * @property {double}  v1y=0   - Direction Y.\n * @property {double}  v1z=1   - Direction Z.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleGravity'\n * @name  NodeTypes#Gravity\n * @property {int}  trigger=1   - Trigger.\n * @property {bool}  applygravity=true   - Apply Gravity.\n * @property {double}  directionx=0   - X Direction.\n * @property {double}  directiony=-1   - Y Direction.\n * @property {double}  directionz=0   - Z Direction.\n * @property {bool}  relativegravity=false   - Apply Gravity between Particles (Relative Gravity).\n * @property {double}  relativemagnitude=1   - Relative Gravity Magnitude.\n * @property {double}  relativegravityepsilon=0.0010   - Relative Gravity Epsilon.\n * @property {double}  relativegravitymaxradius=2   - Relative Gravity Maximum Distance.\n */\n\n\n/**\n * Attributes present in the node of type: 'Particle3dRegion'\n * @name  NodeTypes#3D-Region\n * @property {generic_enum}  [shapetype=\"Sphere\"]   - Type.\n * @property {double}  sizex=6   - Width.\n * @property {double}  sizey=6   - Height.\n * @property {double}  sizez=6   - Depth.\n * @property {double}  outerradius=6   - Max.\n * @property {double}  innerradius=0   - Min.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleSink'\n * @name  NodeTypes#Sink\n * @property {int}  trigger=1   - Trigger.\n * @property {bool}  ifinside=false   - Invert.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleRandom'\n * @name  NodeTypes#Random-Parameter\n * @property {int}  trigger=1   - Trigger.\n * @property {generic_enum}  [parametertorandomize=\"Speed\"]   - Parameter.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleVortex'\n * @name  NodeTypes#Vortex\n * @property {int}  trigger=1   - Trigger.\n * @property {double}  vortexx=0   - X Direction.\n * @property {double}  vortexy=12   - Y Direction.\n * @property {double}  vortexz=0   - Z Direction.\n * @property {double}  vortexradius=4   - Radius.\n * @property {double}  vortexexponent=1   - Exponent (1=cone).\n * @property {double}  vortexupspeed=0.0050   - Up Acceleration.\n * @property {double}  vortexinspeed=0.0050   - In Acceleration.\n * @property {double}  vortexaroundspeed=0.0050   - Around Acceleration.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleWindFriction'\n * @name  NodeTypes#Wind-Friction\n * @property {int}  trigger=1   - Trigger.\n * @property {double}  windfrictionx=0   - Friction/Wind X.\n * @property {double}  windfrictiony=0   - Friction/Wind Y.\n * @property {double}  windfrictionz=0   - Friction/Wind Z.\n * @property {double}  windfrictionminspeed=0   - Min Speed.\n * @property {double}  windfrictionmaxspeed=10   - Max Speed.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleRotationVelocity'\n * @name  NodeTypes#Rotation-Velocity\n * @property {int}  trigger=1   - Trigger.\n * @property {double}  w0=0   - Minimum.\n * @property {double}  w1=5   - Maximum.\n * @property {generic_enum}  [axisstrategy=\"Constant Axis\"]   - Axis Type.\n * @property {double}  v0x=0   - Axis0 X.\n * @property {double}  v0y=0   - Axis0 Y.\n * @property {double}  v0z=1   - Axis0 Z.\n * @property {double}  v1x=0   - Axis1 X.\n * @property {double}  v1y=1   - Axis1 Y.\n * @property {double}  v1z=0   - Axis1 Z.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleRepulse'\n * @name  NodeTypes#Repulse\n * @property {int}  trigger=1   - Trigger.\n * @property {double}  magnitude=1   - Magnitude.\n * @property {double}  lookahead=1   - Look Ahead.\n * @property {double}  epsilon=0.0010   - Epsilon.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleJavascript'\n * @name  NodeTypes#Scripted-Action[Beta]\n * @property {int}  trigger=1   - Trigger.\n * @property {file_editor}  particle_action_script   - .\n * @property {file_library}  files   - .\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleVelocity'\n * @name  NodeTypes#Velocity\n * @property {int}  trigger=1   - Trigger.\n * @property {generic_enum}  [velocitytype=\"Constant Speed\"]   - Velocity Type.\n * @property {double}  v0x=1   - X.\n * @property {double}  v0y=0   - Y.\n * @property {double}  v0z=0   - Z.\n * @property {double}  minspeed=0.5000   - Minimum.\n * @property {double}  maxspeed=0.5000   - Maximum.\n * @property {double}  theta0=0   - Minimum Angle (degrees).\n * @property {double}  theta1=30   - Maximum Angle (degrees).\n * @property {bool}  bilateral=false   - Bilateral.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleSize'\n * @name  NodeTypes#Size\n * @property {int}  trigger=1   - Trigger.\n * @property {generic_enum}  [sizestrategy=\"Constant Size\"]   - Size Type.\n * @property {double}  particlesize=1   - Size.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticlePlanarRegion'\n * @name  NodeTypes#Planar-Region\n * @property {generic_enum}  [shapetype=\"Rectangle\"]   - Shape Type.\n * @property {double}  sizex=12   - Width.\n * @property {double}  sizey=12   - Height.\n * @property {double}  x1=0   - X.\n * @property {double}  y1=0   - Y.\n * @property {double}  x2=0   - X.\n * @property {double}  y2=0   - Y.\n * @property {double}  x3=1   - X.\n * @property {double}  y3=1   - Y.\n * @property {double}  minradius=0   - Minimum.\n * @property {double}  maxradius=6   - Maximum.\n * @property {bool}  mirrornegativeframes=false   - Mirror Negative Frames.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleBkerComposite'\n * @name  NodeTypes#Baker-Composite\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleSprite'\n * @name  NodeTypes#Sprite-Emitter\n * @property {int}  trigger=1   - Trigger.\n * @property {double}  ageatbirth=0   - Age at Birth.\n * @property {double}  ageatbirthstd=0   - Age at Birth Standard Deviation.\n * @property {double}  mass=1   - Particles Mass.\n * @property {generic_enum}  [typechoosingstrategy=\"Sequentially Assign Type Number\"]   - Type Generation Strategy.\n * @property {int}  particletype0=1   - Particle Type 0.\n * @property {int}  particletype1=1   - Particle Type 1.\n * @property {double}  particlesize=1   - Size over Age.\n * @property {bool}  overridevelocity=false   - Align Initial Velocity.\n * @property {generic_enum}  [blend_mode=\"Normal\"]   - Blend Mode.\n * @property {double}  blendintensity=100   - Blend Intensity.\n * @property {generic_enum}  [colouringstrategy=\"Use Drawing Colour\"]   - Colouring Strategy.\n * @property {color}  particlecolour=ffffffff   - Colour.\n * @property {int}  particlecolour.red=255   - Red.\n * @property {int}  particlecolour.green=255   - Green.\n * @property {int}  particlecolour.blue=255   - Blue.\n * @property {int}  particlecolour.alpha=255   - Alpha.\n * @property {generic_enum}  [particlecolour.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  alignwithdirection=true   - Align with Direction.\n * @property {bool}  userotation=false   - Use Rotation of Particle.\n * @property {bool}  directionalscale=false   - Directional Scale.\n * @property {double}  directionalscalefactor=1   - Directional Scale Exponent Factor.\n * @property {bool}  keepvolume=true   - Keep Volume.\n * @property {generic_enum}  [blur=\"No Blur\"]   - Blur.\n * @property {double}  blurintensity=1   - Blur Intensity.\n * @property {double}  blurfallof=0.5000   - Falloff Rate.\n * @property {bool}  flipwithdirectionx=false   - Flip X Axis to Match Direction.\n * @property {bool}  flipwithdirectiony=false   - Flip Y Axis to Match Direction.\n * @property {generic_enum}  [alignwithdirectionaxis=\"Positive X\"]   - Axis to Align.\n * @property {generic_enum}  [renderingstrategy=\"Use Particle Type\"]   - Rendering Strategy.\n * @property {generic_enum}  [cycletype=\"No Cycle\"]   - Cycling.\n * @property {int}  cyclesize=5   - Number of Drawings in Cycle.\n * @property {int}  numberofparticles=100   - Number of Particles.\n * @property {double}  probabilityofgeneratingparticles=100   - Probability of Generating Any Particles.\n * @property {int}  indexselector=0   - Selector.\n * @property {double}  multisize=1   - Region Size for Baked Particle Input.\n * @property {bool}  copyvelocity=false   - Copy Particle Velocity for Baked Particle Input.\n * @property {double}  mininitialangle=0   - Minimum Initial Rotation.\n * @property {double}  maxinitialangle=0   - Maximum Initial Rotation.\n * @property {bool}  copyage=false   - Add Particle Age for Baked Particle Input.\n * @property {bool}  applyprobabilityforeachparticle=true   - Apply Probability for Each Particle.\n * @property {double}  sourcetimespan=0   - Source Sampling Duration.\n * @property {double}  sourcesamplesperframe=16   - Source Samples per Frame.\n * @property {int}  seed=0   - Streak Seed.\n * @property {double}  streaksize=0   - Streak Size.\n * @property {double}  sourcetimeoffset=0   - Source Sampling Time Offset.\n * @property {bool}  setmaxlifespan=false   - Set Maximum Lifespan.\n * @property {double}  maxlifespan=30   - Maximum Lifespan.\n * @property {double}  maxlifespansigma=0   - Maximum Lifespan Sigma.\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleRegionComposite'\n * @name  NodeTypes#Particle-Region-Composite\n */\n\n\n/**\n * Attributes present in the node of type: 'ParticleSystemComposite'\n * @name  NodeTypes#Particle-System-Composite\n */\n\n\n/**\n * Attributes present in the node of type: 'LensFlare'\n * @name  NodeTypes#LensFlare\n * @property {generic_enum}  [blend_mode=\"Normal\"]   - Blend Mode.\n * @property {generic_enum}  [flash_blend_mode=\"Normal\"]   - SWF Blend Mode.\n * @property {bool}  usergba=false   - Blend Mode: Normal/Screen.\n * @property {bool}  brightenable=true   - On/Off.\n * @property {double}  brightness=100   - Intensity.\n * @property {color}  brightcolor=ffffffff   - Color.\n * @property {int}  brightcolor.red=255   - Red.\n * @property {int}  brightcolor.green=255   - Green.\n * @property {int}  brightcolor.blue=255   - Blue.\n * @property {int}  brightcolor.alpha=255   - Alpha.\n * @property {generic_enum}  [brightcolor.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {double}  positionx=6   - PositionX.\n * @property {double}  positiony=6   - PositionY.\n * @property {double}  positionz=0   - PositionZ.\n * @property {generic_enum}  [flareconfig=\"Type 1\"]   - Flare Type.\n * @property {bool}  enable1=true   - Enable/Disable.\n * @property {double}  size1=0.7500   - Size.\n * @property {double}  position1=0   - Position.\n * @property {int}  drawing1=1   - Drawing.\n * @property {double}  blur1=0   - Blur Intensity.\n * @property {bool}  enable2=true   - Enable/Disable.\n * @property {double}  size2=0.8000   - Size.\n * @property {double}  position2=2   - Position.\n * @property {int}  drawing2=2   - Drawing.\n * @property {double}  blur2=0   - Blur Intensity.\n * @property {bool}  enable3=true   - Enable/Disable.\n * @property {double}  size3=1.2000   - Size.\n * @property {double}  position3=-0.2000   - Position.\n * @property {int}  drawing3=3   - Drawing.\n * @property {double}  blur3=0   - Blur Intensity.\n * @property {bool}  enable4=true   - Enable/Disable.\n * @property {double}  size4=0.6500   - Size.\n * @property {double}  position4=0.7500   - Position.\n * @property {int}  drawing4=4   - Drawing.\n * @property {double}  blur4=0   - Blur Intensity.\n * @property {bool}  enable5=true   - Enable/Disable.\n * @property {double}  size5=1   - Size.\n * @property {double}  position5=2   - Position.\n * @property {int}  drawing5=5   - Drawing.\n * @property {double}  blur5=0   - Blur Intensity.\n * @property {bool}  enable6=true   - Enable/Disable.\n * @property {double}  size6=1   - Size.\n * @property {double}  position6=0   - Position.\n * @property {int}  drawing6=6   - Drawing.\n * @property {double}  blur6=0   - Blur Intensity.\n * @property {bool}  enable7=true   - Enable/Disable.\n * @property {double}  size7=0.8000   - Size.\n * @property {double}  position7=2   - Position.\n * @property {int}  drawing7=7   - Drawing.\n * @property {double}  blur7=0   - Blur Intensity.\n * @property {bool}  enable8=true   - Enable/Disable.\n * @property {double}  size8=0.5500   - Size.\n * @property {double}  position8=-0.3000   - Position.\n * @property {int}  drawing8=8   - Drawing.\n * @property {double}  blur8=0   - Blur Intensity.\n * @property {bool}  enable9=true   - Enable/Disable.\n * @property {double}  size9=0.6500   - Size.\n * @property {double}  position9=1.2500   - Position.\n * @property {int}  drawing9=9   - Drawing.\n * @property {double}  blur9=0   - Blur Intensity.\n * @property {bool}  enable10=true   - Enable/Disable.\n * @property {double}  size10=0.5500   - Size.\n * @property {double}  position10=2.3000   - Position.\n * @property {int}  drawing10=10   - Drawing.\n * @property {double}  blur10=0   - Blur Intensity.\n */\n\n\n/**\n * Attributes present in the node of type: 'CROP'\n * @name  NodeTypes#Crop\n * @property {int}  res_x=1920   - X Resolution.\n * @property {int}  res_y=1080   - Y Resolution.\n * @property {double}  offset_x=0   - X Offset.\n * @property {double}  offset_y=0   - Y Offset.\n * @property {bool}  draw_frame=false   - Draw Frame.\n * @property {color}  frame_color=ffffffff   - Frame Color.\n * @property {int}  frame_color.red=255   - Red.\n * @property {int}  frame_color.green=255   - Green.\n * @property {int}  frame_color.blue=255   - Blue.\n * @property {int}  frame_color.alpha=255   - Alpha.\n * @property {generic_enum}  [frame_color.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {enable}  [enabling=\"Always Enabled\"]   - Enabling.\n * @property {generic_enum}  [enabling.filter=\"Always Enabled\"]   - Filter.\n * @property {string}  enabling.filter_name   - Filter name.\n * @property {int}  enabling.filter_res_x=720   - X resolution.\n * @property {int}  enabling.filter_res_y=540   - Y resolution.\n */\n\n\n/**\n * Attributes present in the node of type: 'GLUE'\n * @name  NodeTypes#Glue\n * @property {bool}  invert_matte_port=true   - Invert Matte.\n * @property {double}  bias=0.5000   - Bias.\n * @property {double}  tension=1   - Tension.\n * @property {generic_enum}  [type=\"Curve\"]   - Type.\n * @property {bool}  use_z=true   - Use Z for Composition Order.\n * @property {bool}  a_over_b=true   - A Over B.\n * @property {bool}  spread_a=false   - Spread A.\n */\n\n\n/**\n * Attributes present in the node of type: 'DeformTransformOut'\n * @name  NodeTypes#Point-Kinematic-Output\n * @property {generic_enum}  [sample=\"One-Point Sampling\"]   - Sampling Type.\n * @property {position_2d}  pivot1   - Main Position Tracker.\n * @property {bool}  pivot1.separate=Off   - Separate.\n * @property {double}  pivot1.x=0   - Pos x.\n * @property {double}  pivot1.y=0   - Pos y.\n * @property {point_2d}  pivot1.2dpoint   - Point.\n * @property {position_2d}  pivot2   - Tracker 2.\n * @property {bool}  pivot2.separate=Off   - Separate.\n * @property {double}  pivot2.x=1   - Pos x.\n * @property {double}  pivot2.y=0   - Pos y.\n * @property {point_2d}  pivot2.2dpoint   - Point.\n * @property {position_2d}  pivot3   - Tracker 3.\n * @property {bool}  pivot3.separate=Off   - Separate.\n * @property {double}  pivot3.x=0   - Pos x.\n * @property {double}  pivot3.y=1   - Pos y.\n * @property {point_2d}  pivot3.2dpoint   - Point.\n * @property {double}  volume=1   - Volume Modifier.\n */\n\n\n/**\n * Attributes present in the node of type: 'AmbientOcclusion'\n * @name  NodeTypes#Ambient-Occlusion\n * @property {double}  darkness=200   - Darkness.\n * @property {double}  zrange=1   - Shadow Max Depth Range.\n * @property {double}  bias=0   - Shadow Bias.\n * @property {double}  exponent=1   - Abruptness.\n * @property {double}  gamma=2   - Shadow Gamma.\n * @property {double}  blur=10   - Affected Range.\n * @property {double}  postblur=0   - Post-Blur.\n * @property {double}  postblurthreshold=0.1000   - Post-Blur Depth Threshold.\n * @property {bool}  useimagecolor=false   - Use Image Colour.\n * @property {color}  color=ff000000   - Light Colour.\n * @property {int}  color.red=0   - Red.\n * @property {int}  color.green=0   - Green.\n * @property {int}  color.blue=0   - Blue.\n * @property {int}  color.alpha=255   - Alpha.\n * @property {generic_enum}  [color.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {double}  imagecolorweight=50   - Image Colour Intensity.\n * @property {bool}  invert_matte_port=false   - Invert Matte.\n */\n\n\n/**\n * Attributes present in the node of type: 'SHADOW'\n * @name  NodeTypes#Shadow\n * @property {bool}  truck_factor=true   - Truck Factor.\n * @property {generic_enum}  [blur_type=\"Radial\"]   - Blur Type.\n * @property {double}  radius=2   - Radius.\n * @property {double}  directional_angle=0   - Directional Angle.\n * @property {double}  directional_falloff_rate=1   - Directional Falloff Rate.\n * @property {bool}  use_matte_color=false   - Use Source Colour.\n * @property {bool}  invert_matte=false   - Invert Matte.\n * @property {color}  color=649c9c9c   - Color.\n * @property {int}  color.red=-100   - Red.\n * @property {int}  color.green=-100   - Green.\n * @property {int}  color.blue=-100   - Blue.\n * @property {int}  color.alpha=100   - Alpha.\n * @property {generic_enum}  [color.preferred_ui=\"Separate\"]   - Preferred Editor.\n * @property {bool}  multiplicative=false   - Multiplicative.\n * @property {double}  colour_gain=1   - Intensity.\n */\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_nodeLink.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library v0.01\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney...\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the MIT license.\n//   https://opensource.org/licenses/mit\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oNodeLink class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The base class for the oTimeline.\n * @constructor\n * @classdesc  oTimeline Base Class\n * @param   {oNode}                   outNode                   The source oNode of the link.\n * @param   {int}                     outPort                   The outport of the outNode that is connecting the link.\n * @param   {oNode}                   inNode                    The destination oNode of the link.\n * @param   {int}                     inPort                    The inport of the inNode that is connecting the link.\n *\n * @property   {bool}                    autoDisconnect         Whether to auto-disconnect links if they already exist. Defaults to true.\n * @example\n *  //Connect two pegs together.\n *  var peg1     = $.scene.getNodeByPath( \"Top/Peg1\" );\n *  var peg2     = $.scene.getNodeByPath( \"Top/Peg2\" );\n *\n *  //Create a new $.oNodeLink -- We'll connect two pegs with this new nodeLink.\n *  var link = new $.oNodeLink( peg1,     //Out Node\n *                              0,        //Out Port\n *                              peg2,     //In Node\n *                              0 );      //In Port\n *\n *  //The node link doesn't exist yet, but lets apply it.\n *  link.apply();\n *  //This connection between peg1 and peg2 now exists.\n *\n *  //We can also get the outlinks for the entire node and all of its outputs.\n *  var outLinks = peg2.outLinks;\n *\n *  //Lets connect peg3 to the chain with this existing outLink. This will use an existing link if its already there, or create a new one if none exists.\n *  var peg3     = $.scene.getNodeByPath( \"Top/Peg3\" );\n *  outLinks[0].linkIn( peg3, 0 );\n *\n *  //Uh oh! We need to connect a peg between 1 and 2.\n *  var peg4     = $.scene.getNodeByPath( \"Top/Peg4\" );\n *\n *  //The link we already created above can have a node inserted between it easily.\n *  link.insertNode(  peg4, 0, 0 ); //Peg to insert, in port, out port.\n *\n *  //Oh no! Peg 5 is in a group. Well, it still works!\n *  var peg5     = $.scene.getNodeByPath( \"Top/Group/Peg5\" );\n *  var newLink  = peg1.addOutLink( peg5 );\n */\n$.oNodeLink = function( outNode, outPort, inNode, inPort, outlink ){\n\n    //Public properties.\n    this.autoDisconnect = true;\n    this.path           = false;\n\n    //Private properties.\n    this._outNode = outNode;\n    this._outPort = outPort;\n    this._outLink = outlink;\n\n    this._realOutNode = outNode;\n    this._realOutPort = 0;\n\n    this._inNode  = inNode;\n    this._inPort  = inPort;\n\n    this._stopUpdates        = false;\n    this._newInNode     = null;\n    this._newInPort     = null;\n    this._newOutNode    = null;\n    this._newOutPort    = null;\n    this._exists        = false;\n    this._validated     = false;\n\n    //Assume validation when providing all details. This is done to speed up subsequent lookups.\n    if( outNode &&\n        inNode  &&\n        typeof outPort === 'number' &&\n        typeof inPort  === 'number' &&\n        typeof outlink === 'number'\n      ){\n\n      //Skip validation in the event that we've beengiven all details -- this is to just speed up list generation.\n      return;\n    }\n\n    this.validate();\n}\n\n\n/**\n * Whether the nodeLink exists in the provided state.\n * @name $.oNodeLink#exists\n * @type {bool}\n * @example\n *   //Connect two pegs together.\n *  var peg1     = $.scene.getNodeByPath( \"Top/Peg1\" );\n *  var peg2     = $.scene.getNodeByPath( \"Top/NodeDoesntExist\" );\n *  var link = new $.oNodeLink( peg1,     //Out Node\n *                              0,        //Out Port\n *                              peg2,     //In Node\n *                              0 );      //In Port\n *  link.exists == false;   //FALSE, This link doesnt exist in this context, because the node doesnt exist.\n */\nObject.defineProperty($.oNodeLink.prototype, 'exists', {\n    get : function(){\n      if( !this._validated ){\n        this.validate();\n      }\n      return this._exists;\n    }\n});\n\n\n/**\n * The outnode of this $.oNodeLink. The outNode that is outputting the connection for this link on its outPort and outLink.\n * @name $.oNodeLink#outNode\n * @type {$.oNode}\n */\nObject.defineProperty($.oNodeLink.prototype, 'outNode', {\n    get : function(){\n      return this._outNode;\n\n    },\n    set : function( val ){\n      this._validated = false;\n      this._newOutNode = val;\n\n      if( this.stopUpdates ){\n        return;\n      }\n\n      this.apply(); // do we really want to apply every time we set?\n    }\n});\n\n\n/**\n * The inNode of this $.oNodeLink. The inNode that is accepting this link on its inport.\n * @name $.oNodeLink#inNode\n * @type {$.oNode}\n */\nObject.defineProperty($.oNodeLink.prototype, 'inNode', {\n    get : function(){\n      return this._inNode;\n    },\n\n    set : function( val ){\n      //PATH FIND UP TO THE INNODE.\n      this._validated = false;\n      this._newInNode = val;\n\n      if( this.stopUpdates ){\n        return;\n      }\n\n      this.apply();  // do we really want to apply every time we set?\n    }\n});\n\n\n/**\n * The outport of this $.oNodeLink. The port that the outNode connected to for this link.\n * @name $.oNodeLink#outPort\n * @type {int}\n */\nObject.defineProperty($.oNodeLink.prototype, 'outPort', {\n    get : function(){\n      return this._outPort;\n\n    },\n\n    set : function( val ){\n      this._validated  = false;\n      this._newOutPort = val;\n\n      if( this.stopUpdates ){\n        return;\n      }\n\n      this.apply();  // do we really want to apply every time we set?\n    }\n});\n\n\n/**\n * The outLink of this $.oNodeLink. The link index that the outNode connected to for this link.\n * @name $.oNodeLink#outLink\n * @type {int}\n */\nObject.defineProperty($.oNodeLink.prototype, 'outLink', {\n    get : function(){\n      return this._outLink;\n    }\n});\n\n\n/**\n * The inPort of this $.oNodeLink.\n * @name $.oNodeLink#inPort\n * @type {oNode[]}\n */\nObject.defineProperty($.oNodeLink.prototype, 'inPort', {\n    get : function(){\n      return this._inPort;\n    },\n    set : function( val ){\n      this._validated = false;\n      this._newInPort = val;\n\n      if( this.stopUpdates ){\n        return;\n      }\n\n      this.apply();  // do we really want to apply every time we set?\n    }\n});\n\n\n/**\n * When enabled, changes to the link will no longer update -- the changes will then apply when no longer stopped.\n * @name $.oNodeLink#stopUpdates\n * @private\n * @type {bool}\n */\nObject.defineProperty($.oNodeLink.prototype, 'stopUpdates', {\n    get : function(){\n      return this._stopUpdates;\n    },\n    set : function( val ){\n      this._stopUpdates = val;\n\n      if( !val ){\n        this.apply();\n      }\n    }\n});\n\n\n/**\n * Dereferences up a node's chain, in order to find the exact node its actually attached to.\n * @private\n * @param   {oNode}                   onode                   The node to dereference the groups for.\n * @param   {int}                     port                    The port to dereference.\n * @param   {object[]}                path                    The array path to pass along recursively.<br>[ { \"node\": src_node, \"port\":srcNodeInfo.port, \"link\":srcNodeInfo.link } ]\n * @private\n * @return {object}                   Object in form { \"node\":oNode, \"port\":int, \"link\": int }\n */\n$.oNodeLink.prototype.findInputPath = function( onode, port, path ) {\n  var srcNodeInfo = node.srcNodeInfo( onode.path, port );\n  if( !srcNodeInfo ){\n    return path;\n  }\n\n  var src_node = this.$.scene.getNodeByPath( srcNodeInfo.node );\n  if( !src_node ){\n    return path;\n  }\n\n  if( src_node.type == \"MULTIPORT_IN\" ){\n    //Continue to dereference until we find something other than a group/multiport in.\n    var ret = { \"node\": src_node, \"port\":srcNodeInfo.port, \"link\":srcNodeInfo.link };\n    path.push( ret );\n\n    var src_node = src_node.group;\n\n    var ret = { \"node\": src_node, \"port\":srcNodeInfo.port, \"link\":srcNodeInfo.link };\n    path.push( ret );\n  }else if( src_node.type == \"GROUP\" ){\n    //Continue to dereference until we find something other than a group/multiport out.\n    var ret = { \"node\": src_node, \"port\":srcNodeInfo.port, \"link\":srcNodeInfo.link };\n    path.push( ret );\n\n    var src_node =  src_node.multiportOut;\n\n    var ret = { \"node\": src_node, \"port\":srcNodeInfo.port, \"link\":srcNodeInfo.link };\n    path.push( ret );\n  }else{\n    var ret = { \"node\": src_node, \"port\":srcNodeInfo.port, \"link\":srcNodeInfo.link };\n    path.push( ret );\n    return path;\n  }\n\n  return this.findInputPath( src_node, srcNodeInfo.port, path );\n}\n\n\n/**\n * Changes both the in-node and in-port at once.\n * @param   {oNode}                   onode                   The node to link on the input.\n * @param   {int}                     port                    The port to link on the input.\n * @example\n *  //Connect two pegs together.\n *  var peg1     = $.scene.getNodeByPath( \"Top/Peg1\" );\n *  var peg2     = $.scene.getNodeByPath( \"Top/Peg2\" );\n *\n *  var outLinks  = peg1.outLinks;\n *  outLinks[0].linkIn( peg2, 0 ); //Links the input of peg2, port 0 -- to this link, connecting its outNode [peg1] and outPort [0] and outLink [arbitrary].\n */\n$.oNodeLink.prototype.linkIn = function( onode, port ) {\n  this._validated = false;\n  var stopUpdates_val = this.stopUpdates;\n  this.stopUpdates = true;\n\n  this.inNode = onode;\n  this.inPort = port;\n\n  this.stopUpdates = stopUpdates_val;\n}\n\n\n/**\n * Changes both the out-node and out-port at once.\n * @param   {oNode}                   onode                   The node to link on the output.\n * @param   {int}                     port                    The port to link on the output.\n * @example\n *  //Connect two pegs together.\n *  var peg1     = $.scene.getNodeByPath( \"Top/Peg1\" );\n *  var peg2     = $.scene.getNodeByPath( \"Top/Peg2\" );\n *\n *  var inLinks  = peg1.inLinks;\n *  inLinks[0].linkOut( peg2, 0 ); //Links the output of peg2, port 0 -- to this link, connecting its inNode [peg1] and inPort [0].\n */\n$.oNodeLink.prototype.linkOut = function( onode, port ) {\n  this._validated = false;\n\n  var stopUpdates_val = this.stopUpdates;\n  this.stopUpdates = true;\n\n  this.outNode = onode;\n  this.outPort = port;\n\n  this.stopUpdates = stopUpdates_val;\n}\n\n\n/**\n * Insert a node in the middle of the link chain.\n * @param   {oNode}                   nodeToInsert            The node to link on the output.\n * @param   {int}                     inPort                  The port to link on the output.\n * @param   {int}                     outPort                 The port to link on the output.\n * @example\n *  //Connect two pegs together.\n *  var peg1     = $.scene.getNodeByPath( \"Top/Peg1\" );\n *  var peg2     = $.scene.getNodeByPath( \"Top/Peg2\" );\n *\n *  //Create a new $.oNodeLink -- We'll connect two pegs with this new nodeLink.\n *  var link = new $.oNodeLink( peg1,     //Out Node\n *                              0,        //Out Port\n *                              peg2,     //In Node\n *                              0 );      //In Port\n *\n *  //The link we already created above can have a node inserted between it easily.\n *  var peg4     = $.scene.getNodeByPath( \"Top/Peg4\" );\n *  link.insertNode(  peg4, 0, 0 ); //Peg to insert, in port, out port.\n */\n$.oNodeLink.prototype.insertNode = function( nodeToInsert, inPort, outPort ) {\n  this.stopUpdates = true;\n\n  var inNode = this.inNode;\n  var inPort = this.inPort;\n\n  this.inNode = nodeToInsert;\n  this.inport = inPort;\n\n  this.stopUpdates = false;\n\n  var new_link = new this.$.oNodeLink( nodeToInsert, outPort, inNode, inPort, 0 );\n  new_link.apply( true );\n}\n\n/**\n * Apply the links as needed after unfreezing the oNodeLink\n * @param   {bool}                   force                   Forcefully reconnect/disconnect the note given the current settings of this nodelink.\n * @example\n *  //Connect two pegs together.\n *  var peg1     = $.scene.getNodeByPath( \"Top/Peg1\" );\n *  var peg2     = $.scene.getNodeByPath( \"Top/Peg2\" );\n *\n *  //Create a new $.oNodeLink -- We'll connect two pegs with this new nodeLink.\n *  var link = new $.oNodeLink( peg1,     //Out Node\n *                              0,        //Out Port\n *                              peg2,     //In Node\n *                              0 );      //In Port\n *\n *  //The node link doesn't exist yet, but lets apply it.\n *  link.apply();\n */\n$.oNodeLink.prototype.apply = function( force ) {\n  this._stopUpdates = false;\n  this._validated = false; // ? Shouldn't we use this to bypass application if it's already been validated?\n\n  var disconnect_in = false;\n  var disconnect_out = false;\n  var inports_removed  = {};\n  var outports_removed = {};\n\n  if( force || !this._exists ){    //Apply this.\n    this._newInNode     = this._newInNode ? this._newInNode : this._inNode;\n    this._newOutNode    = this._newOutNode ? this._newOutNode : this._outNode;\n    this._newOutPort    = ( this._newOutPort === null ) ? this._outPort : this._newOutPort;\n    this._newInPort     = ( this._newInPort  === null ) ? this._inPort  : this._newInPort;\n\n    var force = true;\n\n    disconnect_in = true;\n    disconnect_out = true;\n  }else{\n\n    //Force a reconnect -- track content as needed.\n    //Check and validate in ports.\n    var target_port = this._inPort;\n    if( this._newInPort !== null ){\n      if( this._newInPort != this._inPort ){\n        target_port = this._newInPort;\n        disconnect_in = true;\n      }\n    }\n\n    var old_inPortCount = false;    //Used to track if the inport count has changed upon its removal.\n    if( this._newInNode !== null ){\n      if( this._newInNode ){\n        if( !this._inNode || ( this._inNode.path != this._newInNode.path ) ){\n          disconnect_in = true;\n        }\n      }else if( this._inNode ){\n        disconnect_in = true;\n      }\n    }\n\n    //Check and validate out ports.\n    if( this._newOutPort !== null ){\n      if( ( this._newOutPort !== this._outPort ) ){\n        disconnect_out = true;\n      }\n    }\n\n    if( this._newOutNode !== null ){\n      if( this._newOutNode ){\n        if( !this._outNode || ( this._outNode.path != this._newOutNode.path ) ){\n          disconnect_out = true;\n        }\n      }else if( this._outNode ){\n        disconnect_out = true;\n      }\n    }\n  }\n\n  if( !disconnect_in && !disconnect_out ){\n    //Nothing happened.\n    // System.println( \"NOTHING TO DO\" );\n    return;\n  }\n\n  if( this._newInNode ){\n    // if( this._newInNode.inNodes.length > target_port ){\n    if( this._newInNode.inPorts > target_port ){\n      // if( this._newInNode.inNodes[ target_port ] ){\n      if( node.isLinked(this._newInNode.path, target_port) ){\n        //-- Theres already a connection here-- lets remove it.\n        if( this.autoDisconnect ){\n          node.unlink (this._newInNode.path, target_port)\n          // this._newInNode.unlinkInPort( target_port );\n          inports_removed[ this._newInNode.path ] = target_port;\n        }else{\n          throw \"Unable to link \"+this._outNode+\" to \"+this._newInNode+\", port \"+target_port+\" is already occupied.\";\n        }\n      }\n    }\n  }\n\n  //We'll work with the new values -- pretend any new connection is a new one.\n  this._newInNode  = this._newInNode ? this._newInNode : this._inNode;\n  this._newOutNode = this._newOutNode ? this._newOutNode : this._outNode;\n  this._newOutPort = ( this._newOutPort === null ) ? this._outPort : this._newOutPort;\n  this._newInPort  = ( this._newInPort === null ) ? this._inPort : this._newInPort;\n\n\n  if( !this._newInNode || !this._newOutNode ){\n    //Nothing to attach.\n    this._inNode  = this._newInNode;\n    this._inPort  = this._newInPort;\n    this._outNode = this._newOutNode;\n    this._outPort = this._newOutPort;\n\n    return;\n  }\n\n  if( !this._newInNode.exists || !this._newOutNode.exists ){\n    this._inNode  = this._newInNode;\n    this._inPort  = this._newInPort;\n    this._outNode = this._newOutNode;\n    this._outPort = this._newOutPort;\n\n    return;\n  }\n\n\n  //Kill and rebuild the current connection - but first, calculate existing port indices so they can be reconnected contextually.\n  // var newInPortCount   = this._newInNode ? this._newInNode.inNodes.length : 0;\n  var newInPortCount   = this._newInNode ? this._newInNode.inPorts : 0;\n  // var newOutPortCount  = this._newOutNode ? this._newOutNode.outNodes.length : 0;\n  var newOutPortCount  = this._newOutNode ? this._newOutNode.outPorts : 0;\n\n  //Unlink it anyway! Dont worry, we'll reattach that after.\n  if( this._inNode ){\n    // this._inNode.unlinkInPort( this._inPort );\n    node.unlink (this._inNode.path, this._inPort)\n    if( this._outNode ) outports_removed[ this._outNode.path ] = this._outPort;\n    inports_removed[ this._inNode.path ] = this._inPort;\n  }\n\n  //Cant connect without a valid port.\n  if( ( this._newOutPort === null ) || ( this._newOutPort === false ) ){\n    this._inNode  = this._newInNode;\n    this._inPort  = this._newInPort;\n    this._outNode = this._newOutNode;\n    this._outPort = this._newOutPort;\n\n    return;\n  }\n  if( ( this._newInPort === null ) || ( this._newInPort === false ) ){\n    this._inNode  = this._newInNode;\n    this._inPort  = this._newInPort;\n    this._outNode = this._newOutNode;\n    this._outPort = this._newOutPort;\n\n    return;\n  }\n\n  //Check to see if any of the port values have changed.\n  var newInPortCount_result   = this._newInNode ? this._newInNode.inNodes.length : 0;\n  var newOutPortCount_result  = this._newOutNode ? this._newOutNode.outNodes.length : 0;\n\n  if( newOutPortCount_result != newOutPortCount ){\n    //Outport might have changed. React appropriately.\n    if( this._newOutNode.path in outports_removed ){\n      if( this._newOutPort > outports_removed[ this._newOutNode.path ] ){\n        this._newOutPort-=1;\n      }\n    }\n  }\n\n  if( newInPortCount_result != newInPortCount ){\n    //Outport might have changed. React appropriately.\n    if( this._newInNode.path in inports_removed ){\n      if( this._newInPort > inports_removed[ this._newInNode.path ] ){\n        this._newInPort-=1;\n      }\n    }\n  }\n\n  var new_inGroup  = this._newInNode.group;\n  var new_outGroup = this._newOutNode.group;\n  if( new_inGroup.path == new_outGroup.path ){\n    //Simple direct connection within the same group.\n    node.link( _newOutNode.path, this._newInPort, this._newInNode.path, this._newOutPort);\n    //this._newOutNode.linkOutNode( this._newInNode, this._newInPort, this._newOutPort ); MCNote: use the API so we can replace stuff into it later\n\n  }else{\n    //Look for an access route.\n\n    var common_path = [];\n    var split_in  = new_inGroup.path.split( \"/\" );\n    var split_out = new_outGroup.path.split( \"/\" );\n\n    //Find the common top path.\n    for( var n=0;n<Math.min(split_in.length, split_out.length);n++ ){\n      if( split_in[n] != split_out[n] ) break;\n\n      common_path.push( split_out[n] );\n    }\n\n    //The common path is the place we need to attach; find a common link.\n    //Work forward from in, backwards from out.\n\n    var common_path = common_path.join( \"/\" );\n\n    //Outward Path finding.\n    // var outward_path = find_outward( this._newOutNode, this._newOutPort, common_path, [] );\n    var outward_search = new this.$.oNodeLink(this._newOutNode, this._newOutPort, new this.$.oNode(common_path), 0)\n    var outward_path = outward_search.findOutwardPath();\n\n    var common_from = outward_path[outward_path.length-1].from;\n    var common_port = outward_path[outward_path.length-1].fromport;\n\n    var targ_path = this._newInNode.path;\n    targ_path_split = targ_path.split( \"/\" );\n\n    // Find forward from the common junction.\n    var inward_search = new this.$.oNodeLink(common_from, common_port, this._newInNode, this._newInPort)\n    var inward_path = inward_search.findInwardPath( true );\n\n    var cleanPath = [];\n    for( var n = 0; n < outward_path.length; n++ ){\n      var t_path = outward_path[n];\n      if( !t_path.exists && t_path.to ){\n        // t_path.from.linkOutNode( t_path.to, t_path.fromport, t_path.toport, true );\n        node.link(t_path.from.path, t_path.fromport, t_path.to.path, t_path.toport, true , true);\n        // System.println( \"RESULT OUT: \" + t_path.from + \" : \" + t_path.fromport + \" -- \" + t_path.to + \" : \" + t_path.toport );\n      }\n    }\n\n    for( var n = inward_path.length-1; n >= 0; n-- ){\n      var t_path = inward_path[n];\n      if( !t_path.exists ){\n        // t_path.from.linkOutNode( t_path.to, t_path.fromport, t_path.toport, t_path.createPort );\n        node.link(t_path.from.path, t_path.fromport, t_path.to.path, t_path.toport, t_path.createPort, t_path.createPort);\n        // System.println( \"RESULT IN: \" + t_path.from + \" : \" + t_path.fromport + \" -- \" + t_path.to + \" : \" + t_path.toport + \"  \" + t_path.createPort );\n      }\n    }\n  }\n\n  this._inNode  = this._newInNode;\n  this._inPort  = this._newInPort;\n  this._outNode = this._newOutNode;\n  this._outPort = this._newOutPort;\n\n  this._newInNode     = null;\n  this._newInPort     = null;\n  this._newOutNode    = null;\n  this._newOutPort    = null;\n\n  if( !this.validate() ){\n    throw ReferenceError( \"Failed to connect the targets appropriately.\" );\n  }\n}\n\n\n/**\n * findInwardPath. Used internally when applying link.\n * finds the sequence of groups to go into to find the node to connect from a higher level\n * @private\n * @return {path}      an array of path node objects : { \"end\" : bool,\n *                                                       \"exists\" : bool,\n *                                                       \"from\" : oNode,\n *                                                       \"fromport\" : int,\n *                                                       \"to\" : oNode,\n *                                                       \"toport\" : int,\n *                                                       \"createPort\" : bool\n *                                                      }\n */\n$.oNodeLink.prototype.findInwardPath = function( createPort ){\n  var from_node = this._outNode;\n  var from_port = this._outPort;\n  var targ_node = this._inNode;\n  var targ_port = this._inPort;\n  var path = [];\n\n  var length_parent = from_node.group.path.split(\"/\").length;\n  var targ_grp = targ_path_split.slice( 0, length_parent+1 ).join(\"/\");\n\n  if( targ_grp == targ_path ){\n    //Should it create the port?\n\n    path.push( { \"end\": true, \"exists\":false, \"from\":from_node, \"fromport\":from_port, \"to\":targ_node, \"toport\":targ_port, \"createPort\":createPort } );\n    return path;\n  }\n\n  //Find a common link from this target to the next.\n  var grp = this.$.scene.getNodeByPath( targ_grp );\n  var mport = grp.multiportIn;\n  // var followPort = mport.outNodes.length;\n  var followPort = mport.outPorts;\n\n  //Find if the outnodes of from, at the given outNode, connects to the multiportOut already.\n  try{\n    var found_existing = false;\n    var createPortForward = true;\n    // if( from_node.outNodes.length>from_port ){\n    if( from_node.outPorts > from_port){\n      // var ops = from_node.outNodes[from_port];\n      var ops = from_node.getOutLinksNumber(from_port);\n      // for( var n=0; n<ops.length; n++ ){\n      for( var n=0; n<ops; n++ ){\n        // if( ops[n].path == targ_grp ){\n        if( node.linkedNode(from_node.path, from_port, n) == targ_grp){\n          //Dont add it as a new connection, add it as an existing one.\n          var info = node.dstNodeInfo( from_node.path, from_port, n );\n          if( info ){\n            found_existing = true;\n            followPort = info.port;\n            createPortForward = false;\n            break;\n          }\n       }\n      }\n      if(!found_existing){\n        // var grprIns = grp.inNodes;\n        var grprIns = grp.inPorts;\n        // var mpOuts  = mport.outNodes;\n        var mpOuts = mport.outPorts;\n\n        //It can either be acceptable, if the connection is not connected at grpin and not connected at mpout,\n        //or if grpin is not connected, and mpout is connected where we want it to be.\n        var targ_internal = targ_path_split.slice( 0, length_parent+2 ).join(\"/\");\n        for( var n=0; n < grprIns; n++ ){\n          var grprLinks = grp.getInLinksNumber(n);\n          var mpLinks = mport.getOutLinksNumber(n);\n\n          // if( !grprIns[n] ){\n          if( grprLinks == 0 ){\n            // if( mpOuts[n].length == 0 ){\n            if( mpLinks == 0 ){\n              //Its not being used.\n              followPort = n;\n              createPortForward = false;\n\n              break;\n            // }else if( mpOuts[n].length == 1 ){\n            }else if( mpLinks == 1 ){\n              // Its being used, check if its just passing through to another group.\n              // if( mpOuts[n][0].path == targ_internal ){\n              if( node.linkedNode(mport.path, n, 0) == grp.multiportOut.path ){\n                followPort = n;\n                createPortForward = false;\n                break;\n              }\n            }\n          }\n        }\n      }\n    }\n\n    path.push( { \"end\" : false, \"exists\":found_existing, \"from\":from_node, \"fromport\":from_port, \"to\":grp, \"toport\":followPort, \"createPort\":createPort } );\n    // path = find_inward( mport, followPort, targ_node, targ_port, path, createPortForward );\n    var checkLink = new this.$.oNodeLink(mport, followPort, targ_node, targ_port)\n    path = path.concat(checkLink.find_inward( createPortForward ));\n\n  }catch(err){\n    this.$.debug( \"ERR: \" + err.message + \"  \" + err.lineNumber + \" : \" + err.fileName, this.$.DEBUG_LEVEL.ERROR );\n  }\n\n  return path;\n}\n\n\n/**\n * findOutwardPath. Used internally when applying link.\n * finds the sequence of links to go to the highest level of group needed to connect\n * @private\n * @return {path}      an array of path node objects : { \"end\" : bool,\n *                                                       \"exists\" : bool,\n *                                                       \"from\" : oNode,\n *                                                       \"fromport\" : int,\n *                                                       \"to\" : oNode,\n *                                                       \"toport\" : int,\n *                                                       \"createPort\" : bool\n *                                                      }\n */\n$.oNodeLink.prototype.findOutwardPath = function(){\n  var from_node = this._outNode;\n  var port = this._outPort;\n  var targ = this._inNode;\n\n  // if( from_node.group.path != targ ){\n  if( from_node.group.path != targ.path ){\n    //Attach to a group one higher.\n    //multiportIn\n    var grp   = from_node.group;\n    var mport = grp.multiportOut;\n    var followPort = mport.inPorts;\n    //Find if the outnodes of from, at the given outNode, connects to the multiportOut already.\n    try{\n      var found_existing = false;\n      // if( from_node.outNodes.length>port ){\n      if( from_node.outPorts > port ){\n        for( var n = 0; n < from_node.outPorts; n++ ){\n          // if( from_node.outNodes[port][n].path == mport.path ){\n          if( node.dstNode(from_node.path, port, n) == mport.path ){\n            //Dont add it as a new connection, add it as an existing one.\n            var info = node.dstNodeInfo( from_node.path, port, n );\n            if( info ){\n              found_existing = true;\n              followPort = info.port;\n              break;\n            }\n          }\n        }\n      }\n\n      path.push( { \"end\" : false, \"exists\":found_existing, \"from\":from_node, \"fromport\":port, \"to\":mport, \"toport\":followPort  } );\n      // path = find_outward( grp, followPort, targ, path );\n      var checkLink = new this.$.oNodeLink(grp, followPort, this._inNode)\n      path = path.concat( checkLink.findOutwardPath());\n    }catch(err){\n      this.$.debug( \"ERR: \" + err.message + \"  \" + err.lineNumber + \" : \" + err.fileName , this.$.DEBUG_LEVEL.ERROR);\n    }\n\n  }else{\n    path.push( { \"end\": true, \"exists\":true, \"from\":from_node, \"fromport\":port, \"to\":false, \"toport\":false  } );\n  }\n\n  return path;\n}\n\n\n/**\n * Validates the details of a given connection. Used internally when details change.\n * @private\n * @return {bool}      Whether the connection is a valid connection that exists currently in the node system.\n */\n$.oNodeLink.prototype.validate = function ( ) {\n    //Initialize the connection and get the information.\n    //First check to see if the path is valid.\n    this._exists    = false;\n    this._validated = true;\n\n    var inportProvided  = !(!this._inPort && this._inPort!== 0);\n    var outportProvided = !(!this._outPort && this._outPort!== 0);\n\n    if( !inportProvided && !outportProvided ){\n      //inport is the safest to determine contextually.\n      //If either has 1 input.\n      if( this._inNode && this._inNode.inNodes.length == 1 ){\n        this._inPort = 0;\n        inportProvided = true;\n      }\n    }\n\n    if( !this._outNode && !this._inNode ){\n      //Unable to comply, need at least the nodes.\n      this._exists = false;\n      return false;\n    }else if( !this._outNode ){\n      //No outnode. Just look for one above it given the inport.\n      //Lets derive up the chain.\n      if( inportProvided ){\n        this.validateUpwards( this._inPort );\n\n        if( !this.path || this.path.length==0 ){\n          return false;\n        }\n\n        this._outNode     = this.path[ this.path.length-1 ].node;\n        this._outPort     = this.path[ this.path.length-1 ].port;\n        this._outLink     = this.path[ this.path.length-1 ].link;\n\n        this._realOutNode = this.path[ 0 ].node;\n        this._realOutPort = this.path[ 0 ].port;\n        this._realOutLink = this.path[ 0 ].link;\n\n        this._exists = true;\n        return true;\n      }\n    }else if( !this._inNode ){\n      //There can be multiple links. This is very difficult and only possible if theres only a singular path, we'll have to derive them all downwards.\n      //This is just hopeful thinking that there is only one valid path.\n\n      this._outLink = this._outLink ? this._outLink : 0;\n\n      var huntInNode = function( currentNode, port, link ){\n        try{\n          // var on = currentNode.outNodes[port];\n          var numOutLinks = currentNode.getOutLinksNumber(port);\n\n          // if( on.length != 1 ){\n          if( numOutLinks != 1 ){\n            return false;\n          }\n\n          var dstNodeInfo = node.dstNodeInfo( currentNode.path, port, link );\n          if( !dstNodeInfo ){\n            return false;\n          }\n\n          var outNode = this.$.scene.getNodeByPath(node.dstNode( currentNode.path, port, 0 ))\n\n          // if( on[0].type == \"MULTIPORT_OUT\" ){\n          if( outNode.type == \"MULTIPORT_OUT\" ){\n            return huntInNode( currentNode.grp, dstNodeInfo.port );\n          // }else if( on[0].type == \"GROUP\" ){\n          }else if( outNode.type == \"GROUP\" ){\n            return huntInNode( outNode.multiportIn, dstNodeInfo.port, dstNodeInfo.link );\n          }else{\n            // var ret = { \"node\": on[0], \"port\":dstNodeInfo.port };\n            var ret = { \"node\": outNode, \"port\":dstNodeInfo.port };\n            return ret;\n          }\n        }catch(err){\n          this.$.debug( err , this.$.DEBUG_LEVEL.ERROR);\n          return false;\n        }\n      }\n\n      //Find the in node recursively.\n      var res = huntInNode( this._outNode, this._outPort, this._outLink );\n      if( !res ){\n        this._exists = false;\n        return false;\n      }\n\n      if( inportProvided ){\n        if( res.port != this._inPort ){\n          this._exists = false;\n          return false;\n        }\n      }\n\n      this._inNode = res.node;\n      this._inPort = res.port;\n      inportProvided = true;\n    }\n\n    if( !this._outNode || !this._inNode ){\n        this._exists = false;\n        return false;\n    }\n\n    if( !inportProvided && !outportProvided ){\n      //Still no ports provided.\n      //Just simply assume the 0 port on the input.\n      this._inPort = 0;\n      inportProvided = true;\n    }\n\n    if( !inportProvided ){\n      //Derive upwards for each input, if its valid, keep it.\n      var inNodes = this._inNode.inNodes;\n      for( var n=0;n<inNodes.length;n++ ){\n        if( this.validateUpwards( n, outportProvided ) ){\n          this._inPort = n;\n          return true;\n        }\n      }\n      return false;\n    }\n\n    return this.validateUpwards( this._inPort, outportProvided );\n}\n\n\n/**\n * Validates the a node upwards until it hits the target. Given an inport argument to scan.\n * @static\n * @private\n * @param   {int}                   inport                   The inport to scan.\n * @param   {bool}                  outportProvided          Was an outport provided.\n * @return {bool}      Whether the connection is a valid connection that exists currently in the node system.\n */\n$.oNodeLink.prototype.validateUpwards = function( inport, outportProvided ) {\n  //IN THE EVENT OUTNODE WASN'T PROVIDED.\n  this.path = this.findInputPath( this._inNode, inport, [] );\n  if( !this.path || this.path.length == 0 ){\n    return false;\n  }\n\n  var valid = false;\n  for( var n=0;n<this.path.length;n++ ){\n    if( this.path[n].node.path == this._outNode.path ){\n\n      if( !outportProvided ){\n        valid = this.path[n];\n        break;\n      }\n\n      if( this.path[n].port == this._outPort ){\n        valid = this.path[n];\n        break;\n      }\n    }\n  }\n\n  if( !valid ){\n    return false;\n  }\n\n  this._exists = true;\n  this._outLink = valid.link;\n  this._realOutNode = this.path[ 0 ].node;\n  this._realOutPort = this.path[ 0 ].port;\n  this._realOutLink = this.path[ 0 ].link;\n\n  return true;\n}\n\n\n\n/**\n * Converts the node link to a string.\n */\n$.oNodeLink.prototype.toString = function( ) {\n  return '{\"inNode\":\"'+this.inNode+'\", \"inPort\":\"'+this.inPort+'\", \"outNode\":\"'+this.outNode+'\", \"outPort\":\"'+this.outPort+'\", \"outLink\":\"'+this.outLink+'\" }';\n}\n\n\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//           $.oLink class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * Constructor for $.oLink class\n * @classdesc\n * The $.oLink class models a connection between two nodes.<br>\n * A $.oLink object is always describing just one connection between two nodes in the same group. For distant nodes in separate groups, use $.oLinkPath.\n * @constructor\n * @param   {$.oNode}        outNode                         The node from which the link is coming out.\n * @param   {$.oNode}        inNode                          The node into which the link is connected.\n * @param   {oScene}         [outPortNum]                    The out-port of the outNode used by this link.\n * @param   {oScene}         [inPortNum]                     The in-port of the inNode used by this link.\n * @param   {oScene}         [outLinkNum]                    The link index coming out of the out-port.\n * @param   {bool}           [isValid=false]                 Bypass checks and assume this link is connected.\n * @example\n * // find out if two nodes are linked, and through which ports\n * var doc = $.scn;\n * var myNode = doc.root.$node(\"Drawing\");\n * var sceneComp = doc.root.$node(\"Composite\");\n *\n * var myLink = new $.oLink(myNode, sceneComp);\n *\n * log(myLink.linked+\" \"+myLink.inPort+\" \"+myLink.outPort+\" \"+myLink.outLink); // trace the details of the connection.\n *\n * // activate/deactivate connections simply:\n * myLink.connect();\n * log (myLink.linked)  // true\n *\n * myLink.disconnect();\n * log (myLink.linked)  // false\n *\n * // it is also possible to set the linked status directly on the linked property:\n * myLink.linked = true;\n *\n * // however, changing the ports of the link object don't physically change the connection\n *\n * myLink.inPort = 2    // the connection didn't change, the link object simply represents now a different connection possible.\n * log (myLink.linked)  // false\n *\n * myLink.connect()     // this will connect the nodes once more, with different ports. A new connection is created.\n */\n$.oLink = function(outNode, inNode, outPortNum, inPortNum, outLinkNum, isValid){\n  this._outNode = outNode;\n  this._inNode = inNode;\n  this._outPort = (typeof outPortNum !== 'undefined')? outPortNum:undefined;\n  this._outLink = (typeof outLinkNum !== 'undefined')? outLinkNum:undefined;\n  this._inPort = (typeof inPortNum !== 'undefined')? inPortNum:undefined;\n  this._linked = (typeof isValid !== 'undefined')? isValid:false;\n}\n\n\n/**\n * The node that the link is coming out of. Changing this value doesn't reconnect the link, just changes the connection described by the link object.\n * @name $.oLink#outNode\n * @type {$.oNode}\n */Object.defineProperty($.oLink.prototype, 'outNode', {\n  get : function(){\n    return this._outNode;\n  },\n\n  set : function(newOutNode){\n    this._outNode = newOutNode;\n    this._linked = false;\n  }\n});\n\n\n/**\n * The node that the link is connected into. Changing this value doesn't reconnect the link, just changes the connection described by the link object.\n * @name $.oLink#inNode\n * @type {$.oNode}\n */\nObject.defineProperty($.oLink.prototype, 'inNode', {\n  get : function(){\n    return this._inNode;\n  },\n\n  set: function(newInNode){\n    this._inNode = newInNode;\n    this._linked = false;\n  }\n});\n\n\n/**\n * The in-port used by the link. Changing this value doesn't reconnect the link, just changes the connection described by the link object.\n * <br>In the event this value wasn't known by the link object but the link is actually connected, the correct value will be found.\n * @name $.oLink#inPort\n * @type {int}\n */\nObject.defineProperty($.oLink.prototype, 'inPort', {\n  get : function(){\n    if (this.linked) return this._inPort;  // cached value was correct\n\n    var _found = this.findPorts();\n    if (_found) return this._inPort;\n\n    // nodes are not connected\n    return null;\n  },\n\n  set : function(newInPort){\n    this._inPort = newInPort;\n    this._linked = false;\n  }\n});\n\n\n/**\n * The out-port used by the link. Changing this value doesn't reconnect the link, just changes the connection described by the link object.\n * <br>In the event this value wasn't known by the link object but the link is actually connected, the correct value will be found.\n * @name $.oLink#outPort\n * @type {int}\n */\nObject.defineProperty($.oLink.prototype, 'outPort', {\n  get : function(){\n    if (this.linked) return this._outPort;  // cached value was correct\n\n    var _found = this.findPorts();\n    if (_found) return this._outPort;\n\n    // nodes are not connected\n    return null;\n  },\n\n  set : function(newOutPort){\n    this._outPort = newOutPort;\n    this._linked = false;\n  }\n});\n\n\n/**\n * The index of the link coming out of the out-port.\n * <br>In the event this value wasn't known by the link object but the link is actually connected, the correct value will be found.\n * @name $.oLink#outLink\n * @readonly\n * @type {int}\n */\nObject.defineProperty($.oLink.prototype, 'outLink', {\n  get : function(){\n    if (this.linked) return this._outLink;\n\n    var _found = this.findPorts();\n    if (_found) return this._outLink;\n\n    // nodes are not connected\n    return null;\n  }\n});\n\n\n/**\n * Get and set the linked status of a link\n * @name $.oLink#linked\n * @type {bool}\n */\nObject.defineProperty($.oLink.prototype, 'linked', {\n  get : function(){\n    if (this._linked) return this._linked;\n\n    // first check if node object refers to two valid nodes\n    if (this.outNode === undefined || this.inNode === undefined){\n      this.$.debug(\"checking 'linked' for invalid link: \"+this.outNode+\">\"+this.inNode, this.$.DEBUG_LEVEL.ERROR)\n      return false;\n    }\n\n    // if ports/links unknown, get a valid link we can check\n    if (this._outPort === undefined || this._inPort === undefined || this._outLink === undefined){\n      if (!this.findPorts()){\n        return false;\n      }\n    }\n\n    // if ports/links are specified, we check the if the nodes connected to each port correspond with the link values\n    var _linkedOutNode = this.outNode.getLinkedOutNode(this._outPort, this._outLink);\n    var _linkedInNode = this.inNode.getLinkedInNode(this._inPort);\n\n    if (_linkedOutNode == null || _linkedInNode == null) return false;\n\n    var validOutLink = (_linkedOutNode.path == this.inNode.path);\n    var validInLink = (_linkedInNode.path == this.outNode.path);\n\n    if (validOutLink && validInLink){\n      this._linked = true;\n      return true;\n    }\n    return false;\n  },\n\n  set : function(newLinkedStatus){\n    if (newLinkedStatus){\n      this.connect();\n    }else{\n      this.disconnect();\n    }\n  }\n});\n\n\n/**\n * Compares the start and end nodes groups to see if the path traverses several groups or not.\n * @name $.oLink#isMultiLevel\n * @readonly\n * @type {bool}\n */\nObject.defineProperty($.oLink.prototype, 'isMultiLevel', {\n  get : function(){\n    //this.$.debug(\"isMultiLevel? \"+this.outNode +\" \"+this.inNode, this.$.DEBUG_LEVEL.LOG);\n    if (!this.outNode || !this.outNode.group || !this.inNode || !this.inNode.group) return false;\n    return this.outNode.group.path != this.inNode.group.path;\n  }\n});\n\n\n/**\n * Compares the start and end nodes groups to see if the path traverses several groups or not.\n * @name $.oLink#isMultiLevel\n * @readonly\n * @type {bool}\n */\nObject.defineProperty($.oLink.prototype, 'waypoints', {\n  get : function(){\n    if (!this.linked) return []\n    var _waypoints = waypoint.getAllWaypointsAbove (this.inNode, this.inPort)\n    return _waypoints;\n  }\n});\n\n\n/**\n * Get a link that can be connected by working out ports that can be used. If a link already exists, it will be returned.\n * @return {$.oLink} A separate $.oLink object that can be connected. Null if none could be constructed.\n */\n$.oLink.prototype.getValidLink = function(createOutPorts, createInPorts){\n  if (typeof createOutPorts === 'undefined') var createOutPorts = false;\n  if (typeof createInPorts === 'undefined') var createInPorts = true;\n  var start = this.outNode;\n  var end = this.inNode;\n  var outPort = this._outPort;\n  var inPort = this._inPort;\n\n  if (!start || !end) {\n    $.debug(\"A valid link can't be found: node missing in link \"+this.toString(), this.$.DEBUG_LEVEL.ERROR)\n    return null;\n  }\n\n  if (this.isMultiLevel) return null;\n\n  var _link = new this.$.oLink(start, end, outPort, inPort);\n  _link.findPorts();\n\n  // if can't be found, choose a new non existent link\n  if (!_link.linked){\n    if (typeof outPort === 'undefined' || outPort === undefined){\n      _link._outPort = start.getFreeOutPort(createOutPorts);\n      // if (_link._outPort == null) _link._outPort = 0; // just use a current port and add a link\n    }\n\n    _link._outLink = start.getOutLinksNumber(_link._outPort);\n\n    if (typeof inPort === 'undefined' || inPort === undefined){\n      _link._inPort = end.getFreeInPort(createInPorts);\n      if (_link._inPort == null){\n        this.$.debug(\"can't create link because the node \"+end+\" can't create a free inPort\", this.$.DEBUG_LEVEL.ERROR);\n        return null; // can't create a valid link.\n      }\n\n    }else{\n      _link._inPort = inPort;\n\n      if (end.getInLinksNumber(inPort)!= 0 && !end.canCreateInPorts){\n        this.$.debug(\"can't create link because the requested port \"+_link._inPort+\" of node \"+end+\" isn't free\", this.$.DEBUG_LEVEL.ERROR);\n        return null;\n      }\n    }\n  }\n\n  return _link;\n}\n\n\n/**\n * Attempts to connect a link. Will guess the ports if not provided.\n * @return {bool}\n */\n$.oLink.prototype.connect = function(){\n  if (this._linked){\n    return true;\n  }\n\n  // do we want to just always get a valid link here or do we want it to fail if not set properly?\n  if (!this.findPorts()){\n    var _validLink = this.getValidLink(this.outNode.canCreateInPorts, this.inNode.canCreateInPorts);\n    if (!_validLink) return false;\n    this.inPort = _validLink.inPort;\n    this.outPort = _validLink.outPort;\n    this.outLink = _validLink.outLink;\n  };\n\n  if (this.inNode.getInLinksNumber(this._inPort) > 0 && !this.inNode.canCreateInPorts) return false; // can't connect if the in-port is already connected\n\n  var createOutPorts = (this.outNode.outPorts <= this._outPort && this.outNode.canCreateOutPorts);\n  var createInPorts = ((this.inNode.inPorts <= this._inPort || this.inNode.getInLinksNumber(this._inPort)>0) && this.inNode.canCreateInPorts);\n\n  if (this._outNode.type == \"GROUP\" && createOutPorts) this._outNode.addOutPort(this._outPort);\n  if (this._inNode.type == \"GROUP\" && createInPorts) this._inNode.addInPort(this._inPort);\n\n  try{\n    this.$.debug(\"linking nodes \"+this._outNode+\" to \"+this._inNode+\" through outPort: \"+this._outPort+\", inPort: \"+this._inPort+\" and create ports: \"+createOutPorts+\" \"+createInPorts, this.$.DEBUG_LEVEL.LOG);\n\n    var success = node.link(this._outNode, this._outPort, this._inNode, this._inPort, createOutPorts, createInPorts);\n    this._linked = success;\n\n    if (!success) throw new Error();\n    return success;\n\n  }catch(err){\n    this.$.debug(\"linking nodes \"+this._outNode+\" to \"+this._inNode+\" through outPort: \"+this._outPort+\", inPort: \"+this._inPort+\", create outports: \"+createOutPorts+\", create inports:\"+createInPorts, this.$.DEBUG_LEVEL.ERROR);\n    this.$.debug(\"Error linking nodes: \" +err, this.$.DEBUG_LEVEL.ERROR);\n    return false;\n  }\n}\n\n\n/**\n * Disconnects a link.\n * @return {bool} Whether disconnecting was successful;\n */\n$.oLink.prototype.disconnect = function(){\n  if (!this._linked) return true;\n\n  if (!this.findPorts()) return false;\n\n  node.unlink(this._inNode, this._inPort);\n  this._linked = false;\n  return true;\n}\n\n\n/**\n * Finds ports missing or undefined ports in the link object if it is linked, and update the object accordingly. <br>\n * This will not update ports if the link isn't connected. Use getValidLink to get a connectable unconnected link.\n * @private\n * @return {bool} Whether finding ports was successful.\n */\n$.oLink.prototype.findPorts = function(){\n  // Unless some ports are specified, this will always find the first link and stop there. Provide more info in case of multiple links\n\n  if (!this.outNode|| !this.inNode) {\n    this.$.debug(\"calling 'findPorts' for invalid link: \"+this.outNode+\" > \"+this.inNode, this.$.DEBUG_LEVEL.ERROR);\n    return false;\n  }\n\n  if (this._inPort !== undefined && this._outPort!== undefined && this._outLink!== undefined) return true; // ports already are valid, even if link might not be linked\n\n  var _inNodePath = this.inNode.path;\n  var _outNodePath = this.outNode.path;\n\n  // Try to find outPort based on inPort\n  // most likely to be missing is outLink, and this is the quickest way to find it.\n  if (this._inPort != undefined){\n    var _nodeInfo = node.srcNodeInfo(_inNodePath, this._inPort);\n    if (_nodeInfo && _nodeInfo.node == _outNodePath && (this._outPort == undefined || this._outPort == _nodeInfo.port)){\n      this._outPort = _nodeInfo.port;\n      this._outLink = _nodeInfo.link;\n      this._linked = true;\n\n      // this.$.log(\"found ports through provided inPort: \"+ this._inPort)\n      return true;\n    }\n  }\n\n  // Try to find ports based on outLink/outPort\n  if (this._outPort !== undefined && this._outLink !== undefined){\n    var _nodeInfo = node.dstNodeInfo(_outNodePath, this._outPort, this._outLink);\n    if (_nodeInfo && _nodeInfo.node == _inNodePath){\n      this._inPort = _nodeInfo.port;\n      this._linked = true;\n\n      // this.$.log(\"found ports through provided outPort/outLink: \"+this._outPort+\" \"+this._outLink)\n      return true;\n    }\n  }\n\n  // Find the ports if we are missing all of them, looking at in-ports to avoid messing with outLinks\n  var _inPorts = this.inNode.inPorts;\n  for (var i = 0; i<_inPorts; i++){\n    var _nodeInfo = node.srcNodeInfo(_inNodePath, i);\n    if (_nodeInfo && _nodeInfo.node == _outNodePath){\n      if (this._outPort !== undefined && this._outPort !== _nodeInfo.port) continue;\n\n      this._inPort = i;\n      this._outPort = _nodeInfo.port;\n      this._outLink = _nodeInfo.link;\n\n      // this.$.log(\"found ports through iterations\")\n      this._linked = true;\n\n      return true;\n    }\n  }\n\n  // The nodes are not linked\n  this._linked = false;\n  return false;\n}\n\n\n/**\n * Connects the given node in the middle of the link. The link must be connected.\n * @param {$.oNode} oNode          The node to insert in the link\n * @param {int} [nodeInPort = 0]   The inPort to use on the inserted node\n * @param {int} [nodeOutPort = 0]  The outPort to use on the inserted node\n * @param {int} [nodeOutLink = 0]  The outLink to use on the inserted node\n * @return {$.oLink[]}   an Array of two oLink objects that describe the new connections.\n * @example\n * include(\"openHarmony.js\")\n * doc = $.scn\n * var node1 = doc.$node(\"Top/Drawing\")\n * var node2 = doc.$node(\"Top/Composite\")\n * var node3 = doc.$node(\"Top/Transparency\")\n *\n * var link = new $.oLink(node1, node2)\n * link.insertNode(node3) // insert the Transparency node between the Drawing and Composite\n */\n$.oLink.prototype.insertNode = function(oNode, nodeInPort, nodeOutPort, nodeOutLink){\n  if (!this.linked) return    // can't insert a node if the link isn't connected\n\n  this.$.beginUndo(\"oh_insertNode\")\n\n  var _inNode = this.inNode\n  var _outNode = this.outNode\n  var _inPort = this.inPort\n  var _outPort = this.outPort\n  var _outLink = this.outLink\n\n  var _topLink = new this.$.oLink(_outNode, oNode, _outPort, nodeInPort, _outLink)\n  var _lowerLink = new this.$.oLink(oNode, _inNode, nodeOutPort, _inPort, nodeOutLink)\n\n  this.linked = false;\n  var success = (_topLink.connect() && _lowerLink.connect());\n\n  this.$.endUndo()\n\n  if (success) {\n    return [_topLink, _lowerLink]\n  } else{\n    // we restore the links to default state and return false\n    this.$.debug(\"failed to insert node \"+oNode+\" into link \"+this)\n    this.$.undo()\n    return false\n  }\n}\n\n/**\n * Converts the node link to a string.\n * @private\n */\n$.oLink.prototype.toString = function( ) {\n  return ('link: {\"'+this._outNode+'\" ['+this._outPort+', '+this._outLink+'] -> \"'+this._inNode+'\" ['+this._inPort+']} linked:'+this._linked);\n  // return '{outNode:'+this.outNode+' inNode:'+this.inNode+' }';\n}\n\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oLinkPath class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * Constructor for $.oLinkPath class\n * @classdesc\n * The $.oLinkPath class allows to figure out paths as a series of links between distant nodes.<br>\n * It can either look for existing paths and check that two distant nodes are connected or create new ones that can then be connected.\n * @constructor\n * @param   {$.oNode}        startNode                       The first node from which the link is coming out.\n * @param   {$.oNode}        endNode                         The last node into which the link is connected.\n * @param   {oScene}         [outPortNum]                    The out-port of the startNode.\n * @param   {oScene}         [inPortNum]                     The in-port of the endNode.\n * @param   {oScene}         [outLinkNum]                    The link index coming out of the out-port of the startNode.\n * @see NodeType\n */\n$.oLinkPath = function( startNode, endNode, outPort, inPort, outLink){\n  this.startNode = startNode;\n  this.endNode = endNode;\n  this.outPort = (typeof outPort !== 'undefined')? outPort:undefined;\n  this.inPort = (typeof inPort !== 'undefined')? inPort:undefined;\n  this.outLink = (typeof outLink !== 'undefined')? outLink:undefined;\n}\n\n\n/**\n * Compares the start and end nodes groups to see if the path traverses several groups or not.\n * @name $.oLinkPath#isMultiLevel\n * @readonly\n * @type {bool}\n */\nObject.defineProperty($.oLinkPath.prototype, 'isMultiLevel', {\n  get : function(){\n    //this.$.log(this.startNode+\" \"+this.endNode)\n    return this.startNode.group.path != this.endNode.group.path;\n  }\n});\n\n\n/**\n * Identifies the group in which the two nodes will connect if they are at different levels of depth.\n * @name $.oLinkPath#lowestCommonGroup\n * @readonly\n * @type {$.oGroupNode}\n */\nObject.defineProperty($.oLinkPath.prototype, 'lowestCommonGroup', {\n  get : function(){\n    var startPath = this.startNode.group.path.split(\"/\");\n    var endPath = this.endNode.group.path.split(\"/\");\n\n    var commonPath = [];\n    for (var i=0; i<startPath.length; i++){\n      if (startPath[i] != endPath[i]) break;\n      commonPath.push(startPath[i]);\n    }\n\n    return this.$.scene.getNodeByPath(commonPath.join(\"/\"));\n  }\n});\n\n\n/**\n * Finds an existing path if one exists between two distant nodes.\n *\n * @return {$.oLink[]} The list of successive $.oLink objects describing the path. Returns null if no such path could be found.\n */\n$.oLinkPath.prototype.findExistingPath = function(){\n  // looking for the startNode from the endNode going up since the hierarchy is usually simpler this direction\n  // if inPort is provided, we assume it's correct or a search filter, otherwise look up all inLinks\n  var _searchPorts = (this.inPort !== undefined)?[this.inPort]:Array.apply(null, new Array(this.endNode.inPorts)).map(function (x, i) {return i;});\n\n  for (var i = 0; i<_searchPorts.length; i++){\n    var _inLink = this.endNode.getInLink(_searchPorts[i]);\n    if (_inLink == null) continue; // if no node is connected, this branch is a dead end;\n\n    var _connectedInNode = _inLink.outNode;\n\n    // start looking again from the corresponding node\n    if(_connectedInNode.path == this.startNode.path){\n      // check that we have the valid outPort/outLink if provided as search filter\n      if (this.outPort !== undefined && _inLink.outPort != this.outPort) return null;\n      if (this.outLink !== undefined && _inLink.outLink != this.outLink) return null;\n\n      return [_inLink]; // we found the node\n    }else if (_connectedInNode.type == \"MULTIPORT_IN\"){\n      _connectedInNode = _connectedInNode.group;\n    }else if (_connectedInNode.type == \"GROUP\"){\n      _connectedInNode = _connectedInNode.multiportOut;\n    }else{\n      // stop looking? any other nodes we want to traverse? Composites?\n      continue;\n    }\n\n    var _searchLink = new this.$.oLinkPath(this.startNode, _connectedInNode, this.outPort, _inLink.outPort, this.outLink);\n    var _path = _searchLink.findExistingPath();\n\n    if (_path == null) return null; // this branch is a dead end\n\n    _path.push(_inLink);\n    return _path;\n  }\n\n  // we couldn't find a path\n  return null\n}\n\n\n/**\n * Gets a link object from two nodes that can be successfully connected. Provide port numbers if there are specific requirements to match. If a link already exists, it will be returned.\n * @param  {$.oNode}         start          The node from which the link originates.\n * @param  {$.oNode}         end            The node at which the link ends.\n * @param  {int}             [outPort]      A preferred out-port for the link to use.\n * @param  {int}             [inPort]       A preferred in-port for the link to use.\n *\n * @return {$.oLink} the valid $.oLink object.  Returns null if no such link could be created (for example if the node's in-port is already linked)\n */\n$.oLinkPath.prototype.getValidLink = function(start, end, outPort, inPort){\n  var _link = new $.oLink(start, end, outPort, inPort)\n  return _link.getValidLink();\n}\n\n\n/**\n * Finds a valid path between two distant nodes, even if one doesn't currently exist.\n *\n * @return {$.oLink[]}     The list of links needed for the path. Some can already be connected.\n */\n$.oLinkPath.prototype.findNewPath = function(){\n  // look for the lowest common group we will have to reach first\n  subLinks = [];\n  var commonGroup = this.lowestCommonGroup;\n\n  //get links out of the start group until the common group\n  var _startPath = [];\n  var _node = this.startNode;\n  var _preferedOutPort = this.outPort;\n\n  while (_node.group.path != commonGroup.path){\n    var _linkOutNode = _node;\n    var _linkInNode = _node.group.multiportOut;\n\n    // look for an existing link to reuse\n    var _link = this.getValidLink(_linkOutNode, _linkInNode, _preferedOutPort);\n    _startPath.push(_link);\n\n    // prepare for next step\n    _node = _node.group\n    _preferedOutPort = _link.inPort;\n  }\n  var startGroup = _node;\n\n\n  // get links out of the end group until the common group\n  var _endPath = []\n  _node = this.endNode;\n  var _preferedInPort = this.inPort;\n\n  while (_node.group.path != commonGroup.path){\n    var _linkOutNode = _node.group.multiportIn;\n    var _linkInNode = _node;\n\n    // look for an existing link to reuse\n    var _link = this.getValidLink(_linkOutNode, _linkInNode, undefined, _preferedInPort);\n    _endPath.unshift(_link);\n\n    // prepare for next step\n    _node = _node.group;\n    _preferedInPort = _link.OutPort;\n  }\n\n  var endGroup = _node;\n\n  var _link = this.getValidLink(startGroup, endGroup, _preferedOutPort, _preferedInPort);\n  _startPath.push(_link)\n\n  var _path = _startPath.concat(_endPath)\n  this.$.log(_path.join(\"\\n\"))\n\n  return _path\n}\n\n\n/**\n * Connects all the unconnected links between two distant nodes\n * @return {$.oLink[]} return the list of links present in the created path\n */\n\n$.oLinkPath.prototype.connectPath = function(){\n  var newPath = this.findNewPath();\n\n  for (var i in newPath){\n    newPath[i].connect();\n  }\n\n  return newPath;\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_palette.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library v0.01\n//\n//\n//         Developped by Mathieu Chaptel, Chris Fourney...\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is garanteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the MIT license.\n//   https://opensource.org/licenses/mit\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oPalette class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n//\n/**\n * $.oPalette constructor.\n * @constructor\n * @classdesc  $.oPalette Base Class\n * @param   {palette}                 paletteObject             The Harmony palette object.\n * @param   {paletteList}             paletteListObject         The Harmony paletteListObject object.\n *\n * @property   {palette}                 paletteObject          The Harmony palette object.\n * @property   {oSceneObject}            scene                  The DOM Scene object.\n */\n$.oPalette = function (paletteObject, paletteListObject) {\n  this._type = \"palette\";\n\n  this.paletteObject = paletteObject;\n  this._paletteList = paletteListObject;\n  this.scene = this.$.scn;\n}\n\n\n// Class properties\n$.oPalette.location = {\n  \"environment\": PaletteObjectManager.Constants.Location.ENVIRONMENT,\n  \"job\": PaletteObjectManager.Constants.Location.JOB,\n  \"scene\": PaletteObjectManager.Constants.Location.SCENE,\n  \"element\": PaletteObjectManager.Constants.Location.ELEMENT,\n  \"external\": PaletteObjectManager.Constants.Location.EXTERNAL\n}\n\n\n// $.oPalette Object Properties\n/**\n * The palette ID.\n * @name $.oPalette#id\n * @type {string}\n */\nObject.defineProperty($.oPalette.prototype, 'id', {\n  get: function () {\n    return this.paletteObject.id;\n  }\n})\n\n\n/**\n * The palette name.\n * @name $.oPalette#name\n * @type {string}\n */\nObject.defineProperty($.oPalette.prototype, 'name', {\n  get: function () {\n    return this.paletteObject.getName();\n  },\n\n  set: function (newName) {\n    // Rename palette file then unlink and relink the palette\n    this.$.debug(\"renaming palette \" + this.name + \" to \" + newName, this.$.DEBUG_LEVEL.LOG)\n    var _paletteFile = this.path;\n    var _newPath = _paletteFile.folder + \"/\" + newName;\n    try{\n      _paletteFile.move(_newPath + \".plt\", true);\n    }catch(err) {\n      throw new Error (\"couldn't rename palette \" + this.path + \" to \" + newName + \": \"+ err)\n    }\n\n    var _list = this._paletteList;\n    var _name = this.name;\n    _list.removePaletteById(this.id);\n\n    var _paletteObject = _list.insertPalette(_newPath.replace(\".plt\", \"\"), this.index);\n    this.paletteObject = _paletteObject;\n  }\n})\n\n\n/**\n * The palette index in the palette list.\n * @name $.oPalette#index\n * @type {int}\n */\nObject.defineProperty($.oPalette.prototype, 'index', {\n  get: function () {\n    var _list = this._paletteList;\n    var _n = _list.numPalettes;\n    for (var i = 0; i < _n; i++) {\n      var _paletteId = _list.getPaletteByIndex(i).id;\n      if (_paletteId == this.id) return i;\n    }\n  },\n\n  set: function (newIndex) {\n    var _list = this._paletteList;\n    var _path = this.path.path.replace(\".plt\", \"\");\n    _list.removePaletteById(this.id);\n    _list.insertPalette(_path, newIndex);\n  }\n})\n\n\n/**\n * The element containing the palette if stored in element folder.\n * @name $.oPalette#element\n * @type {$.oElement}\n * @readonly\n */\nObject.defineProperty($.oPalette.prototype, 'element', {\n  get: function () {\n    var _storage = this.paletteStorage;\n    var _paletteObject = this._paletteObject;\n    if (_storage != \"element\") return null;\n    return new this.$.oElement(_paletteObject.elementId);\n  }\n})\n\n\n/**\n * The palette path on disk.\n * @name $.oPalette#path\n * @type {$.oFile}\n * @readonly\n */\nObject.defineProperty($.oPalette.prototype, 'path', {\n  get: function () {\n    var _path = this.paletteObject.getPath();\n    return new this.$.oFile(_path + \"/\" + this.name + \".plt\");\n  }\n})\n\n\n/**\n * The storage place for the palette (environment, scene, job, element or external)\n * @name $.oPalette#paletteStorage\n * @type {$.oFile}\n */\nObject.defineProperty($.oPalette.prototype, 'paletteStorage', {\n  get: function () {\n    var _location = this.$.oPalette.location;\n    var _storage = {\n      environment: fileMapper.toNativePath(PaletteObjectManager.Locator.folderForLocation(_location.environment, 1)),\n      job: fileMapper.toNativePath(PaletteObjectManager.Locator.folderForLocation(_location.job, 1)),\n      scene: fileMapper.toNativePath(PaletteObjectManager.Locator.folderForLocation(_location.scene, 1))\n    }\n\n    var _path = this.path.folder.path;\n\n    if (_path.indexOf(\"/elements\") != -1) {\n      // find out which element?\n      return \"element\";\n    }\n    for (var i in _storage) {\n      if (_storage[i].split(\"\\\\\").join(\"/\") == _path) return i;\n    }\n\n    return \"external\";\n  }\n})\n\n\n/**\n * Whether the palette is selected.\n * @name $.oPalette#selected\n * @type {bool}\n */\nObject.defineProperty($.oPalette.prototype, 'selected', {\n  get: function () {\n    var _currentId = PaletteManager.getCurrentPaletteId()\n    return this.id == _currentId;\n  },\n\n  set: function (isSelected) {\n    // TODO: find a way to work with index as more than one color can have the same id, also, can there be no selected color when removing selection?\n    if (isSelected) {\n      var _id = this.id;\n      PaletteManager.setCurrentPaletteById(_id);\n    }\n  }\n})\n\n\n/**\n * The oColor objects contained in the palette.\n * @name $.oPalette#colors\n * @type {oColor[]}\n */\nObject.defineProperty($.oPalette.prototype, 'colors', {\n  get: function () {\n    var _palette = this.paletteObject\n    var _colors = []\n    for (var i = 0; i < _palette.nColors; i++) {\n      _colors.push(new this.$.oColor(this, i))\n    }\n    return _colors\n  }\n})\n\n\n/**\n * The color currently active in the palette view. 'null' if no color is currently selected\n * @name $.oPalette#currentColor\n * @type {oColor}\n */\nObject.defineProperty($.oPalette.prototype, 'currentColor', {\n  get: function () {\n    var id = PaletteManager.getCurrentColorId()\n    return this.getColorById(id)\n  },\n  set: function (newColor) {\n    var id = newColor.id\n    if (!this.getColorById(id)) return\n    PaletteManager.setCurrentColorById(id)\n  }\n})\n\n\n// $.oPalette Class methods\n\n/**\n * Adds a solid color to the palette\n * @param {string}        name        the display name for the newly created color\n * @param {$.oColorValue} colorValue  a $.oColorValue object describing the color\n */\n$.oPalette.prototype.addColor = function (name, colorValue) {\n  var colorData = {r : colorValue.r, g: colorValue.g, b: colorValue.b, a : colorValue.a };\n  this.paletteObject.createNewSolidColor(name, colorData);\n\n  return this.colors.slice(-1)[0];\n}\n\n/**\n * Adds a texture swatch to the palette\n * @param {string} name\n * @param {string} texturePath\n * @param {bool} tiled  Wether the texture will be tiled or not\n */\n$.oPalette.prototype.addTexture = function (name, texturePath, tiled) {\n  if (typeof texturePath === this.$.oFile) texturePath = texturePath.path;\n  this.paletteObject.createNewTexture(name, texturePath, tiled);\n\n  return this.colors.slice(-1)[0];\n}\n\n\n/**\n * Adds a gradient to the palette, using the passed values\n * @param {string} name\n * @param {object} colorValues an object with keys between 0 and 1 containing a colorValue for each \"tack\". ex: {0: new $.oColorValue(\"000000ff\"), 1:new $.oColorValue(\"ffffffff\")}\n * @param {bool} radial\n */\n$.oPalette.prototype.addGradient = function (name, colorValues, radial) {\n  if (typeof radial === 'undefined') var radial = false;\n\n  var types = PaletteObjectManager.Constants.ColorType;\n  var type = radial?types.RADIAL_GRADIENT:types.LINEAR_GRADIENT;\n  var gradient = []\n  for (var i in colorValues){\n    var color = colorValues[i];\n    var tack = {t:parseFloat(i, 10), r:color.r, g:color.g, b:color.b, a:color.a};\n    gradient.push[tack];\n  }\n\n  this.paletteObject.createNewColor(type, name, gradient);\n  return this.colors.slice(-1)[0];\n}\n\n\n/**\n * Gets a oColor object based on id. 'null' if the color is not found in this palette\n * @param   {string}     id                          the color id as found in toonboom palette file.\n *\n * @return: {oColor}     the found oColor object.\n */\n$.oPalette.prototype.getColorById = function (id) {\n  var _colors = this.colors;\n  for (var i in _colors){\n    if (_colors[i].id == id) return _colors[i];\n  }\n  return null;\n}\n\n\n/**\n * Gets a oColor object based on name. Warning: more than one color can have the same name in a palette, the first one found will be returned.\n * @param   {string}     name           the color name for the color in the palette.\n *\n * @return: {oColor}     the found oColor object.\n */\n $.oPalette.prototype.getColorByName = function (name) {\n  var _colors = this.colors;\n  var _names = _colors.map(function (x) { return x.name })\n  var _colorIndex = _names.indexOf(name)\n  if (_colorIndex != -1) return _colors[_colorIndex]\n  return null;\n}\n\n\n/**\n *  Removes the palette file from the filesystem and palette list.\n * @param   {bool}       removeFile                 Whether the palette file should be removed on the filesystem.\n *\n * @return: {bool}       The success-result of the removal.\n */\n$.oPalette.prototype.remove = function (removeFile) {\n  if (typeof removeFile === 'undefined') var removeFile = false;\n\n  var success = false;\n\n  if (removeFile) {\n    try {\n      if (this.$.batchMode) {\n        this.path.remove();\n        success = this._paletteList.removePaletteById(this.id);\n      } else {\n        success = PaletteObjectManager.removePaletteReferencesAndDeleteOnDisk(this.id)\n      }\n    } catch (err) {\n      success = false;\n    }\n  } else {\n    success = this._paletteList.removePaletteById(this.id);\n  }\n\n  //Todo: should actually check for its removal.\n  return success;\n}\n\n\n$.oPalette.prototype.toString = function(){\n  return this.path.path || this.name;\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_path.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library v0.01\n//\n//\n//         Developped by Mathieu Chaptel, Chris Fourney...\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is garanteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the MIT license.\n//   https://opensource.org/licenses/mit\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//        $.oPathPoint class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oPathPoint class.\n * @constructor\n * @classdesc  The $.oPathPoint Class represents a point on a column of type 3DPath. This class is used to access information about the curve that this point belongs to.\n * @param   {oColumn}                  oColumnObject         The 3DPath column that contains this information\n * @param   {oFrame}                   oFrameObject          The frame on which the point is placed\n *\n * @property {oColumn}                 column                The column this point belongs to\n * @property {oFrame}                  frame                 The frame on which the point is placed.\n */\n$.oPathPoint = function(oColumnObject, oFrameObject){\n    this.column = oColumnObject;\n    this.frame = oFrameObject;\n}\n\n\n/**\n * The keyframe index of the frame/key at this point.\n * @name $.oPathPoint#pointIndex\n * @type {int}\n */\nObject.defineProperty($.oPathPoint.prototype, 'pointIndex', {\n    get : function(){\n         return this.frame.keyframeIndex;\n    }\n})\n\n\n/**\n * The X value of the path element.\n * @name $.oPathPoint#x\n * @type {float}\n */\nObject.defineProperty($.oPathPoint.prototype, 'x', {\n    get : function(){\n         var _column = this.column.uniqueName;\n         var _index = this.pointIndex;\n         var _x = func.pointXPath3d(_column, _index);\n\n         return _x;\n    },\n\n    set : function(newX){\n        var _column = this.column.uniqueName;\n        var _index = this.pointIndex;\n\n        func.setPointPath3d (_column, _index, newX, this.y, this.z, this.tension, this.continuity, this.bias)\n    }\n})\n\n\n/**\n * The Y value of the path element.\n * @name $.oPathPoint#y\n * @type {float}\n */\nObject.defineProperty($.oPathPoint.prototype, 'y', {\n    get : function(){\n         var _column = this.column.uniqueName;\n         var _index = this.pointIndex;\n         var _y = func.pointYPath3d (_column, _index);\n\n         return _y;\n    },\n\n    set : function(newY){\n        var _column = this.column.uniqueName;\n        var _index = this.pointIndex;\n\n        func.setPointPath3d (_column, _index, this.x, newY, this.z, this.tension, this.continuity, this.bias)\n    }\n})\n\n\n/**\n * The Z value of the path element.\n * @name $.oPathPoint#z\n * @type {float}\n */\nObject.defineProperty($.oPathPoint.prototype, 'z', {\n    get : function(){\n         var _column = this.column.uniqueName;\n         var _index = this.pointIndex;\n         var _z = func.pointZPath3d (_column, _index);\n\n         return _z;\n    },\n\n    set : function(newZ){\n        var _column = this.column.uniqueName;\n        var _index = this.pointIndex;\n\n        func.setPointPath3d (_column, _index, this.x, this.y, newZ, this.tension, this.continuity, this.bias)\n    }\n})\n\n\n/**\n * The tension at the current keyframe point.\n * @name $.oPathPoint#tension\n * @type {float}\n */\nObject.defineProperty($.oPathPoint.prototype, 'tension', {\n    get : function(){\n         var _column = this.column.uniqueName;\n         var _index = this.pointIndex;\n         return func.pointTensionPath3d (_column, _index);\n    },\n\n    set : function(newTension){\n        var _column = this.column.uniqueName;\n        var _index = this.pointIndex;\n\n        func.setPointPath3d (_column, _index, this.x, this.y, this.z, newTension, this.continuity, this.bias)\n    }\n})\n\n\n/**\n * The continuity at the current keyframe point.\n * @name $.oPathPoint#continuity\n * @type {float}\n */\nObject.defineProperty($.oPathPoint.prototype, 'continuity', {\n    get : function(){\n         var _column = this.column.uniqueName;\n         var _index = this.pointIndex;\n         return func.pointContinuityPath3d (_column, _index);\n    },\n\n    set : function(newContinuity){\n        var _column = this.column.uniqueName;\n        var _index = this.pointIndex;\n        func.setPointPath3d (_column, _index, this.x, this.y, this.z, this.tension, newContinuity, this.bias)\n    }\n\n})\n\n\n/**\n * The bias at the current keyframe point.\n * @name $.oPathPoint#bias\n * @type {float}\n */\nObject.defineProperty($.oPathPoint.prototype, 'bias', {\n    get : function(){\n         var _column = this.column.uniqueName;\n         var _index = this.pointIndex;\n         return func.pointBiasPath3d (_column, _index);\n    },\n\n    set : function(newBias){\n        var _column = this.column.uniqueName;\n        var _index = this.pointIndex;\n        var _point = this.point;\n        func.setPointPath3d (_column, _index, this.x, this.y, this.z, this.tension, this.continuity, newBias)\n    }\n\n})\n\n\n/**\n * The bezier lock at the current keyframe point.\n * @name $.oPathPoint#lock\n * @type {float}\n */\nObject.defineProperty($.oPathPoint.prototype, 'lock', {\n    get : function(){\n         var _column = this.column.uniqueName;\n         var _index = this.pointIndex;\n         return func.pointLockedAtFrame (_column, _index);\n    },\n\n    set : function(newLockedFrame){\n        var _column = this.column.uniqueName;\n        var _index = this.pointIndex;\n\n        throw new Error(\"$.oPathPoint.lock (set) - not yet implemented\")\n    }\n})\n\n\n/**\n * The velocity at the current keyframe point.\n * @name $.oPathPoint#velocity\n * @type {float}\n */\nObject.defineProperty($.oPathPoint.prototype, 'velocity', {\n    get : function(){\n         var _column = this.column.uniqueName;\n         return column.getEntry(this.column.uniqueName, 4, this.frame.frameNumber)\n    },\n\n    set : function(newVelocity){\n        var _column = this.column.uniqueName;\n        return column.setEntry(this.column.uniqueName, 4, this.frame.frameNumber, newVelocity)\n    }\n})\n\n\n// $.oPathPoint class methods\n\n/**\n * Matches this path point to the provided one.\n * @param   {$.oPathPoint}    pseudoPathPoint                The path point object to match this to.\n */\n$.oPathPoint.prototype.set = function( pseudoPathPoint ){\n    // Set a point by providing all values in an object corresponding to a dumb $.oPathPoint object with static values for each property;\n    var _point = pseudoPathPoint;\n\n    // default values for missing values in pseudoPathPoint\n    var _x = (_point.x != undefined)?_point.x:0.0;\n    var _y = (_point.y != undefined)?_point.y:0.0;\n    var _z = (_point.z != undefined)?_point.z:0.0;\n    var _tension = (_point.tension != undefined)?_point.tension:0.0;\n    var _continuity = (_point.continuity != undefined)?_point.continuity:0.0;\n    var _bias = (_point.bias != undefined)?_point.bias:0.0;\n\n    var _column = this.column.uniqueName;\n    var _index = this.pointIndex;\n\n    func.setPointPath3d (_column, _index, _x, _y, _z, _tension, _continuity, _bias);\n}\n\n/**\n * Converts the pathpoint to a string.\n * @return {string}    The pathpoint represented as a string.\n */\n$.oPathPoint.prototype.toString = function(){\n    return \"{x:\"+this.x+\", y:\"+this.y+\", z:\"+this.z+\"}\"\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_preferencedoc.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developped by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is garanteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//\n// This document was outputted by this function.\n//\n//\n// include(\"openHarmony.js\")\n//\n// printOutDoc();\n//\n// function printOutDoc(){\n// \tvar doc = \"\";\n// \tvar prefs = $.app.preferences\n// \tvar categories = prefs.categories\n// \tfor (var i in categories){\n// \t\tvar docString = [\"\\n\\n/\\**\\n * Preferences in the \"+categories[i]+\" category.\", \" @name preferences#\"+categories[i]];\n// \t\tcategoryPrefs = prefs.details\n// \t\tfor (var j in categoryPrefs){\n// \t\t\tvar pref = categoryPrefs[j]\n// \t\t\tif (pref.category != categories[i])continue;\n// \t\t\tif (pref.descriptionText == undefined) pref.descriptionText = \"\"\n// \t\t\tif (pref.description == undefined) pref.description = pref.descriptionText\n// \t\t\tdocString.push(\" @property {\"+pref.type+\"} \"+pref.keyword+\"=\"+pref.value+\" \"+pref.description);\n// \t\t}\n// \t\tdocString.push (\"/\");\n// \t\tdoc += docString.join(\"\\n *\");\n// \t}\n//\n// \tMessageLog.trace(doc);\n// }\n\n\n/**\n * The preferences of Harmony can be accessed with the following keywords.\n * @class preferences\n * @hideconstructor \n * @namespace\n * @example\n * // To access the preferences of Harmony, grab the preference object in the $.oApp class:\n * var prefs = $.app.preferences;\n * \n * // It's then possible to access all available preferences of the software:\n * for (var i in prefs){\n *   log (i+\" \"+prefs[i]);\n * }\n * \n * // accessing the preference value can be done directly by using the dot notation:\n * prefs.USE_OVERLAY_UNDERLAY_ART = true;\n * log (prefs.USE_OVERLAY_UNDERLAY_ART);\n * \n * //the details objects of the preferences object allows access to more information about each preference\n * var details = prefs.details\n * log(details.USE_OVERLAY_UNDERLAY_ART.category+\" \"+details.USE_OVERLAY_UNDERLAY_ART.id+\" \"+details.USE_OVERLAY_UNDERLAY_ART.type);\n * \n * for (var i in details){\n *   log(i+\" \"+JSON.stringify(details[i]))       // each object inside detail is a complete oPreference instance\n * }\n * \n * // the preference object also holds a categories array with the list of all categories\n * log (prefs.categories)\n */\n\n/**\n * Preferences in the Internal category.\n * @name preferences#Internal\n * @property {string} PREFERENCE_SET=Cutout Animation \n * @property {TUPencilDeformationQualityItem} DEFAULT_PENCIL_DEFORMATION_QUALITY=Low Default Pencil Line Deformation Quality\n * @property {bool} DEFAULT_PRESERVE_LINE_THICKNESS=false \n * @property {int} TV_DEFAULT_VIEW=3 \n * @property {double} DBL_STEP_2DPOSITION_X=0.1 Internal\n * @property {double} DBL_STEP_2DPOSITION_Y=0.1 Internal\n * @property {double} DBL_STEP_3DPOSITION_X=0.1 Internal\n * @property {double} DBL_STEP_3DPOSITION_Y=0.1 Internal\n * @property {double} DBL_STEP_3DPOSITION_Z=0.1 Internal\n * @property {double} DBL_STEP_CUSTOMNAME_FIELD_CHART=1 Internal\n * @property {double} DBL_STEP_APPLYFOCUS_MULTIPLIER=0.1 Internal\n * @property {double} DBL_STEP_COLORCARD_OFFSET_Z=0.2 Internal\n * @property {double} DBL_STEP_COLORSCALE_RED=0.1 Internal\n * @property {double} DBL_STEP_COLORSCALE_GREEN=0.1 Internal\n * @property {double} DBL_STEP_COLORSCALE_BLUE=0.1 Internal\n * @property {double} DBL_STEP_COLORSCALE_ALPHA=0.1 Internal\n * @property {double} DBL_STEP_COLORSCALE_HUE=0.1 Internal\n * @property {double} DBL_STEP_COLORSCALE_SATURATION=0.1 Internal\n * @property {double} DBL_STEP_COLORSCALE_VALUE=0.1 Internal\n * @property {double} DBL_STEP_COLORSCREEN_RED_MIN=0.1 Internal\n * @property {double} DBL_STEP_COLORSCREEN_RED_MAX=0.1 Internal\n * @property {double} DBL_STEP_COLORSCREEN_GREEN_MIN=0.1 Internal\n * @property {double} DBL_STEP_COLORSCREEN_GREEN_MAX=0.1 Internal\n * @property {double} DBL_STEP_COLORSCREEN_BLUE_MIN=0.1 Internal\n * @property {double} DBL_STEP_COLORSCREEN_BLUE_MAX=0.1 Internal\n * @property {double} DBL_STEP_COLORSCREEN_MATCH=0.1 Internal\n * @property {double} DBL_STEP_COLORTOBW_PERCENT=1 Internal\n * @property {double} DBL_STEP_COMPOSITE3D_MULTIPLIER=0.1 Internal\n * @property {double} DBL_STEP_CONTRAST_MIDPOINT=0.1 Internal\n * @property {double} DBL_STEP_CONTRAST_PIXEL_ADJUST=0.1 Internal\n * @property {double} DBL_STEP_CONTRAST_DARK_ADJUST=0.1 Internal\n * @property {double} DBL_STEP_BRIGHTNESSCONTRAST_BRIGHTNESS_ADJUST=1 Internal\n * @property {double} DBL_STEP_BRIGHTNESSCONTRAST_CONTRAST_ADJUST=1 Internal\n * @property {double} DBL_STEP_CROP_OFFSET_X=0.1 Internal\n * @property {double} DBL_STEP_CROP_OFFSET_Y=0.1 Internal\n * @property {double} DBL_STEP_DIRBLUR_RADIUS=0.1 Internal\n * @property {double} DBL_STEP_DIRBLUR_ANGLE=1 Internal\n * @property {double} DBL_STEP_DIRBLUR_FALLOFF_RATE=0.1 Internal\n * @property {double} DBL_STEP_DITHER_MAGNITUDE=0.1 Internal\n * @property {double} DBL_STEP_EXTERNAL_NUM_PARAM=1 Internal\n * @property {double} DBL_STEP_FADE_TRANSPARENCY=1 Internal\n * @property {double} DBL_STEP_GRADIENT_OFFSET_Z=0.2 Internal\n * @property {double} DBL_STEP_GRAIN_NOISE=0.01 Internal\n * @property {double} DBL_STEP_GRAIN_SMOOTH=0.01 Internal\n * @property {double} DBL_STEP_MATTEBLUR_RADIUS=0.1 Internal\n * @property {double} DBL_STEP_MATTEBLUR_ANGLE=1 Internal\n * @property {double} DBL_STEP_MATTEBLUR_FALLOFF_RATE=0.1 Internal\n * @property {double} DBL_STEP_PIXELATE_FACTOR=0.001 Internal\n * @property {double} DBL_STEP_RADIALBLUR_RADIUS=0.1 Internal\n * @property {double} DBL_STEP_RAWELEMENT_AA_EXPONENT=1 Internal\n * @property {double} DBL_STEP_ELEMENT_MODULE_LINE_SMOOTHING_ERROR=1 Internal\n * @property {double} DBL_STEP_REFRACT_M=1 Internal\n * @property {double} DBL_STEP_REFRACT_B=1 Internal\n * @property {double} DBL_STEP_REFRACT_N=0.1 Internal\n * @property {double} DBL_STEP_REMTRANSPARENCY_THRESHOLD=1 Internal\n * @property {double} DBL_STEP_RESIZEMATTE_RADIUS=0.1 Internal\n * @property {double} DBL_STEP_VARBLUR_BLACK_RADIUS=0.1 Internal\n * @property {double} DBL_STEP_VARBLUR_WHITE_RADIUS=0.1 Internal\n * @property {double} DBL_STEP_SCALEATTR_X=0.01 Internal\n * @property {double} DBL_STEP_SCALEATTR_Y=0.01 Internal\n * @property {double} DBL_STEP_SCALEATTR_Z=0.01 Internal\n * @property {double} DBL_STEP_SCALEATTR_XY=0.01 Internal\n * @property {double} DBL_STEP_CAMERA_ANGLE=1 Internal\n * @property {double} DBL_STEP_CAMERA_FOV=0.5 Internal\n * @property {double} DBL_STEP_CAMERA_NEAR_PLANE=1 Internal\n * @property {double} DBL_STEP_CAMERA_FAR_PLANE=1 Internal\n * @property {double} DBL_STEP_ANGLE=1 Internal\n * @property {double} DBL_STEP_ANGLE_SKEW=1 Internal\n * @property {double} DBL_STEP_SET_FOCUS=0.1 Internal\n * @property {double} DBL_STEP_SCALE_LINE_THICKNESS=0.01 \n * @property {double} DBL_STEP_LINE_THICKNESS=0.1 \n * @property {double} DBL_STEP_GLUE_BIAS=0.1 Internal\n * @property {double} DBL_STEP_GLUE_TENSION=0.1 Internal\n * @property {double} DBL_STEP_NB_FRAMES_TAIL=1 Internal\n * @property {double} DBL_STEP_NB_SAMPLES=5 Internal\n * @property {double} DBL_STEP_FALLOFF=0.1 Internal\n * @property {double} DBL_STEP_BLURRINESS=0.1 Internal\n * @property {double} DBL_STEP_BLUR_VARIANCE=0.1 Internal\n * @property {double} DBL_STEP_RADIAL_BLUR_QUALITY=0.01 Internal\n * @property {double} DBL_STEP_INTERPOLATION=0.1 Internal\n * @property {double} STEP_TURBULENCE_FREQUENCY=0.1 Internal\n * @property {double} STEP_TURBULENCE_SEED=0.1 Internal\n * @property {double} STEP_TURBULENCE_EVOLUTION=0.1 Internal\n * @property {double} STEP_TURBULENCE_EVOLUTION_FREQUENCY=0.1 Internal\n * @property {double} STEP_TURBULENCE_GAIN=0.01 Internal\n * @property {double} STEP_TURBULENCE_LACUNARITY=0.01 Internal\n * @property {double} DBL_STEP_GAMMA_ADJUST=0.01 Internal\n * @property {double} DBL_STEP_COLOR_GAMMA=0.1 Internal\n * @property {double} DBL_STEP_LENGTH=0.1 Internal\n * @property {double} DBL_STEP_ASPECT_RATIO=0.1 Internal\n * @property {double} Z_PARTITION_RANGE=0.1 Internal\n * @property {string} FILE_SYSTEM_SAVED_PATH= Internal\n * @property {string} SCENE_LEVEL_SAVED_PATH= Internal\n * @property {string} NEW_SCENE_SAVED_PATH= Internal\n * @property {string} RECENT_SCENES_LIST= Internal\n * @property {double} DBL_STEP_CONSTRAINT_ACTIVE=1 Internal\n * @property {double} DBL_STEP_DYN_TENSION=1 Internal\n * @property {double} DBL_STEP_DYN_INERTIA=1 Internal\n * @property {double} DBL_STEP_CONSTRAINT_RATIO=1 Internal\n * @property {double} DBL_STEP_CONSTRAINT_RATIOFLOAT=0.1 Internal\n * @property {bool} DEBUG_VERIFY_DRAWING_FILES_EXIST=false Test that drawing files exist\n */\n\n/**\n * Preferences in the General category.\n * @name preferences#General\n * @property {bool} ACCEPT_UNICODE_NAME=false Accept unicode name\n * @property {shortcut} OPEN_KEY=Ctrl+O Open\n * @property {shortcut} OPEN_ELEMENTS_KEY=Ctrl+E Open Elements\n * @property {shortcut} SAVE_ALL_KEY=Ctrl+S Save All\n * @property {shortcut} HELP_KEY=F1 Help\n * @property {shortcut} DELETE_KEY=Del Delete\n * @property {shortcut} DESELECT_ALL_KEY=Esc Deselect All\n * @property {shortcut} SELECT_ALL_KEY=Ctrl+A Select All\n * @property {bool} SAVE_SCENE=true Save Scene\n * @property {bool} SAVE_PALETTE_LISTS=true Save Palette Lists\n * @property {bool} SAVE_PALETTES=true Save Palettes\n * @property {bool} SAVE_DRAWINGS=true Save Drawings\n * @property {bool} AUTO_SAVE_LAYOUT=true Automatically Save Workspace\n * @property {bool} STAGE_AUTOSAVE_PROJECT_ENABLED=false Auto Save Scene\n * @property {bool} STAGE_AUTOSAVE_ASK_BEFORE=false Ask Before Auto Saving\n * @property {double} STAGE_AUTOSAVE_PROJECT_INTERVAL_MINUTES=10 Auto Save Interval\n * @property {double} STAGE_AUTOSAVE_PROJECT_INACTIVITY_DELAY_SECONDS=1 Auto Save Inactivity Interval\n * @property {bool} undefined=false Automatically save the scan drawing each time.\n * @property {double} DEFAULT_SCALE_FACTOR=1 Scale value for imported 3d models.\n * @property {bool} TB_PREVENT_OVERWRITE_OF_UNKNOWN_PLUGINS=false Prevent save if plugin nodes are unrecognized.\n * @property {bool} COMPRESS_KEY_FRAMES_ON_SAVE=true Compress key frames on save\n * @property {bool} FOCUS_ON_MOUSE_ENTER=true Focus On Mouse Enter\n * @property {bool} SHORTCUT_ZOOM_ON_MOUSE=false Shortcut Zooms On Mouse\n * @property {bool} AUTO_RENDER=true Automatic Render\n * @property {bool} INVERSE_CLOSE_PREVIOUS_EDITORS=false Close Previous Editors\n * @property {bool} SHOW_CONTROL_POINTS_ON_SELECTED_LAYERS=false Enables the automatic display of control points on selected layers\n * @property {bool} USE_OVERLAY_UNDERLAY_ART=true Enables the use of overlay and underlay arts\n * @property {bool} TIMELINE_REDUCE_INDENTATION=true Reduce the indentation of the timeline.\n * @property {bool} TIMELINE_SHOW_SCENE_MARKER=true Display the end of scene marker.\n * @property {bool} ADVANCED_DISPLAY_IN_VIEWS=true Shows the display selector in views\n * @property {bool} Z_ORDER_COMPATIBILITY_WITH_7_3=false Enables the z-order compatibility with version 7.3 or older.\n * @property {bool} RESTORE_SELECTED_DRAWING_ON_UNDO=false Restore the previously edited drawing when undoing commands\n * @property {bool} DISPLAY_ONLY_DIFFS_IN_MERGE_EDITOR=false Display only element differences in the merge editor.\n * @property {bool} CUSTOMIZE_GROUP_FUNCTIONALITY=false Customize the group node functionality.\n * @property {bool} PEG_ENFORCE_MINMAX_ANGLE=false Rotation Angle Enforced by Peg\n * @property {bool} PLAY_ENABLE_ANALOG_SCRUBBING=false Enable Analog Sound Scrubbing\n * @property {int} SOUND_ANALOG_JOG_SPEED=5 Analog Jog Speed\n * @property {int} SOUND_ANALOG_JOG_DAMPENING=10 Analog Jog Dampening\n * @property {bool} AUTO_APPLY=true Enable Auto Apply\n * @property {bool} AUTO_LOCK=true Enable Automatic Locking of Drawings\n * @property {bool} AUTO_LOCK_PALETTES=false Enable Automatic Getting the Rights to Modify of Palettes.\n * @property {bool} AUTO_LOCK_PALETTE_LISTS=true Enable Automatic Getting the Rights to Modify of Palette Lists.\n * @property {bool} PEG_DEFAULT_SEPARATE_POSITION=true Default Separate Position for Pegs\n * @property {bool} PEG_DEFAULT_SEPARATE_SCALE=true Default Separate Scale for Pegs\n * @property {bool} PEG_DEFAULT_BEZIER=true Default Bezier\n * @property {bool} PEG_DEFAULT_BEZIER_VELOCITY=true Default Bezier Velocity\n * @property {bool} READ_DEFAULT_SEPARATE_POSITION=true Default Separate Position for Elements\n * @property {bool} READ_DEFAULT_SEPARATE_SCALE=true Default Separate Scale for Elements\n * @property {bool} READ_USE_DRAWING_PIVOT=false Read use drawing pivot\n * @property {bool} RENDERER_CONSERVATIVETEXTUREMANAGEMENT=true Conservative Texture Management\n * @property {bool} RENDERER_SMOOTHTEXTURES=false Smooth Textures\n * @property {bool} CUSTOMIZE_RENDER_SCRIPTS=true Give access to full script customization interface.\n * @property {bool} OPENGL_ALTERNATE_CLEAR_STENCIL=false Alternate Clear Stencil\n * @property {int} OPENGL_MAX_TVG_TEXTURE_SIZE=1024 Maximum Texture Size for TVG\n * @property {int} OPENGL_MAXIMUM_MOVIE_TRACK_TEXTURE_SIZE=512 Maximum OpenGL Movie Track Preview Size\n * @property {bool} DISPLAY_CARD_COORDINATES=true Display Scalar or Cardinal Coordinates\n * @property {bool} DRAWING_CYCLE=true Cycle\n * @property {bool} EDITORS_ALWAYS_ON_TOP=true Editors always on top\n * @property {bool} DETACHED_HAVE_MENU_BAR=true Detached views have a menu bar\n * @property {bool} SNAP_KEYFRAME=false Snap Keyframe\n * @property {int} LEVELS_OF_UNDO=50 Levels of Undo\n * @property {int} RENDERER_TEXTUREREDUCTIONSIZE=2048 Texture Reduction Size\n * @property {double} MIN_FOV=1 Minimum FOV\n * @property {double} MAX_FOV=179 Maximum FOV\n * @property {double} FIELD_CHART_X=12 Field Chart X\n * @property {double} FIELD_CHART_Y=12 Field Chart Y\n * @property {double} FIELD_CHART_Z=12 Field Chart Z\n * @property {color} CURRENT_VIEW_BORDER_COLOR=#ff0000ff Current View Border Colour\n * @property {color} CURRENT_VIEW_BORDER_TEMPLATE_EDITION_COLOR=#00ff00ff Current View Border in Template Edit\n * @property {bool} SHOW_WELCOME_SCREEN_ON_STARTUP=true Show Welcome Screen on Startup\n * @property {bool} SHOW_WELCOME_IMAGE_ON_STARTUP=false Show Welcome Image on Startup\n * @property {bool} CREATE_BUNDLES=false On Mac OS X, determine if the new scene created are bundle.\n * @property {string} UNIX_HTML_BROWSER= HTML Browser for Unix\n * @property {bool} VECT_SHOW_STROKES=false Show the strokes\n * @property {color} EXPORTVIDDLG_BACKGROUND_COLOR=#ffffffff Export Video Bg Colour\n * @property {color} TIMECODE_BGCOLOR=#000000ff \n * @property {color} TIMECODE_COLOR=#ffffffff \n * @property {font} TIMECODE_FONT=arial \n * @property {int} TIMECODE_HEIGHT=10 \n * @property {position} TIMECODE_POSITION=BOTTOM_CENTER \n * @property {bool} SCRIPT_BUSY_WARNING=false Display script in progress warning\n * @property {int} VECTOR_LAYER_DEFAULT_RESOLUTION_FACTOR=100 Default vector layer resolution factor used when creating a new project.\n * @property {int} BITMAP_LAYER_DEFAULT_RESOLUTION_FACTOR=100 Default bitmap layer resolution factor used when creating a new project.\n * @property {int} BITMAP_LAYER_DEFAULT_CANVAS_WIDTH_FACTOR=200 Default bitmap layer canvas width factor used when creating a new project.\n * @property {int} BITMAP_LAYER_DEFAULT_CANVAS_HEIGHT_FACTOR=200 Default bitmap layer canvas height factor used when creating a new project.\n * @property {string} WEBCC_URL= Web Control Center URL\n * @property {bool} WEBCC_SSL_SELF_SIGNED=false Accept Web Control Center's self-signed certificate\n * @property {bool} ENGLISH_CONTEXT_SENSITIVE_HELP=false Enabling this displays all context sensitive help in english.\n * @property {bool} CAMERA_VIEW_DISABLE_RENDER_PREVIEW=false Disable the render preview buttons in the camera view status bar.\n * @property {bool} ENABLE_LOG_IO=false Used to enable the internal logging mechanism, to track file creation/removal.\n * @property {bool} ENABLE_MIDDLE_BUTTON_PANS_VIEW=false Used to enable the middle mouse button to pan Camera and Drawing views.\n * @property {TUDefaultColorEditorItem} DEFAULT_COLOR_EDITOR=separate Default Colour Editor\n * @property {int} SCR_EVENT_PROCESSING_INTERVAL=-1 Set an interval above zero to allow event processing while scripts are running.\n */\n\n/**\n * Preferences in the Sceneplanning category.\n * @name preferences#Sceneplanning\n * @property {shortcut} SP_TOGGLE_SNAP_KEYFRAME=X Toggle Snap Keyframe\n * @property {bool} ELEMENT_CAN_BE_ANIMATED_DEFAULT_VALUE=false Default value of the can be animated flag on element node\n * @property {bool} SP_BBOX_HIGHLIGHTING=false Bounding Box Selection\n * @property {bool} SP_SHOW_LOCKED_DRAWINGS_IN_OUTLINE=false Show Locked Drawing in Outline\n * @property {bool} SP_CONSTANT_SEGMENT=true Constant Segments\n * @property {bool} SP_ENABLE_WASH_BACKGROUND=false Enable Wash Background in Sceneplanning\n * @property {bool} SP_ENABLE_WASH_BACKGROUND_DRAWING_MODE=false Enable Wash Background in Sceneplanning Drawing Mode\n * @property {double} SP_WASH_PREVIEW_PERCENTAGE=0.2 Dirty Preview: wash % (between 0.0-1.0)\n * @property {bool} SP_ENABLE_PREVIEW_WASH=false Enable Wash Background in Preview Mode\n * @property {bool} SP_USE_PIVOT_OFFSET_FOR_CONTROL_POINTS_SPLINE=true Use the current pivot for offset on the control points spline\n * @property {int} SP_THUMBNAIL_SIZE=64 Thumbnail Size\n * @property {bool} NV_ALTERNATE_WIDE_CABLE_APPREARANCE=true Two Colour Cables for Pass Through Connections\n * @property {int} SP_IK_NB_OF_ITERATIONS=20 Maximum number of iterations for IK\n * @property {bool} SP_IK_HIERARCHY_STOP_AT_FIRST_INTERSECTION=true Stop at first intersection for IK\n * @property {bool} SP_IK_HIERARCHY_ALL_CHAIN=false Advanced manipulation mode for IK\n * @property {int} SP_SMALL_FILES_RESOLUTION=1024 Resolution for small pixmaps files\n * @property {bool} SP_OVERRIDE_SMALL_FILES=true Decide if the resolution of the small bitmap files are overriden by the preference value.\n * @property {int} SP_WASH_BACKGROUND_PERCENTAGE=70 Wash Background Percentage\n * @property {double} SP_TV_SAFETY=0.1 Safe Area\n * @property {double} SP_ROTATION_CONTROL_SIZE=0.5 Rotation Control Size\n * @property {int} SP_TRANSFORM_TOOL_SIZE=200 Transform tools size in pixels\n * @property {int} SP_PIVOTS_AND_CONTROL_POINTS_SIZE=12 Pivots and control points size\n * @property {double} SP_PIVOT_SIZE_1=0.01 Pivot Size 1\n * @property {double} SP_PIVOT_SIZE_2=0.025 Pivot Size 2\n * @property {double} SP_PIVOT_SIZE_3=0.0025 Pivot Size 3\n * @property {double} SP_TENSION=0 Control Point Tension\n * @property {double} SP_CONTINUITY=0 Control Point Continuity\n * @property {double} SP_BIAS=0 Control Point Bias\n * @property {double} SP_SIDE_TOP_VIEW_DEFAULT_ZOOM=0.5 Top/Side View Default Zoom\n * @property {double} SP_IK_MAXIMUM_ERROR=0.01 Maximum error for IK\n * @property {double} SP_IK_OUT_OF_REACH_RETRY_INCREMENT=0.05 Retry increment for IK when it's out of reach.\n * @property {double} SP_TRANSLATE_SCALE_FACTOR=1 Sceneplanning nudge increment.\n * @property {color} SP_BORDER_COLOR=#262626ff Border Colour\n * @property {color} SP_CAMERA_COLOR=#242424ff Camera Colour\n * @property {color} SP_AXIS_COLOR=#ffffffff Axis Colour\n * @property {color} SP_SPLINE_COLOR=#ff7f00ff Spline Colour\n * @property {color} SP_CURRENT_FRAME_COLOR=#00ff00ff Current Colour\n * @property {color} SP_CONTROL_POINT_COLOR=#ffff00ff Control Point Colour\n * @property {color} SP_KEYFRAME_COLOR=#ff0000ff Keyframe Colour\n * @property {color} SP_PIVOT_DARK_COLOR=#335faaff Dark Pivot Colour\n * @property {color} SP_PIVOT_LIGHT_COLOR=#c1d8ffff Light Pivot Colour\n * @property {color} SP_PEG_PIVOT_DARK_COLOR=#aa5f33ff Dark Peg Pivot Colour\n * @property {color} SP_PEG_PIVOT_LIGHT_COLOR=#ffd8c1ff Light Peg Pivot Colour\n * @property {color} SP_ROTATION_CONTROL_COLOR_1=#ff0000ff Rotation Control Colour 1\n * @property {color} SP_ROTATION_CONTROL_COLOR_2=#00ff00ff Rotation Control Colour 2\n * @property {color} SP_SCALE_CONTROL_COLOR=#ff0000ff Scale Control Colour\n * @property {color} SP_SKEW_CONTROL_COLOR=#ff0000ff Skew Control Colour\n * @property {color} SP_IK_CONTROL_COLOR=#ff0000ff IK Control Colour\n * @property {color} SP_IK_MIN_CONTROL_COLOR=#00ff00ff IK Min Control Colour\n * @property {color} SP_IK_MAX_CONTROL_COLOR=#ff0000ff IK Max Control Colour\n * @property {CX_FullAnimationModeIds} SP_ANIMATION_MODE=KEYFRAME_MODE Default Sceneplanning Mode\n * @property {TUFrameViewDefaultZoomItem} SP_FRAME_VIEW_DEFAULT_ZOOM=Fit to View Default Camera View Default Zoom\n * @property {bool} SP_TRANSFORM_TOOL_CREATE_ALL_KEYFRAMES=true Transform Tool create keyframe on all functions\n * @property {bool} SP_TRANSFORM_TOOL_FORCE_KEY_FRAME_AT_FRAME_ONE=true Transform Tool create keyframe at frame one\n * @property {bool} CAMERA_PASTE_FORCE_KEY_FRAME=true Pasting creates start/end keyframes\n * @property {bool} DRAWING_TOOL_BAR_FLAT=false Drawing Tools tool bar will appear flat and customizable.\n * @property {bool} DARK_STYLE_SHEET=true Use a dark look.\n * @property {bool} TB_USE_TOUCH_INTERFACE=false Enable the gestural touch interface for the OpenGL views.\n * @property {bool} TB_TOUCH_INVERT_SCROLL=false Invert the scroll direction.\n * @property {double} TB_TOUCH_SPEED=2 Touch Sensitivity.\n * @property {bool} USE_QT_WINTAB=true Use Qt built in Wintab API\n */\n\n/**\n * Preferences in the Timeline category.\n * @name preferences#Timeline\n * @property {bool} TIMELINE_SHOW_SOUND=true Show Sound Layers\n * @property {bool} TIMELINE_SHOW_SOUND_WAVEFORMS=true Show Sound Waveforms\n * @property {bool} TIMELINE_SHOW_GROUP=true Show Group Layers\n * @property {bool} TIMELINE_SHOW_EFFECT=true Show Effect Layers\n * @property {bool} TIMELINE_SHOW_COMPOSITE=true Show Composite Layers\n * @property {bool} TIMELINE_PASTE_FORCE_KEY_FRAME=true Pasting creates start/end keyframes\n * @property {color} TIMELINE_MARK_KEY_DRAWING_COLOR=#ff0000ff Colour of the key drawing marker.\n * @property {color} TIMELINE_MARK_BREAK_DRAWING_COLOR=#0091ffff Colour of the break drawing marker.\n */\n\n/**\n * Preferences in the Node View category.\n * @name preferences#Node View\n * @property {bool} NV_WORLD_VIEW_STARTING_STATE=true World View Starting State\n * @property {color} NV_BACKGROUND_COLOR=#787878ff Background Colour\n * @property {int} NV_DEFAULT_THUMBNAIL_RESOLUTION=64 Default thumbnail resolution\n * @property {color} NV_DEFAULT_THUMBNAIL_BACKGROUND_COLOR=#ffffffff Default thumbnail background color\n * @property {color} NV_PROXY_PORT_LIGHT_COLOR=#cacacaff Proxy Port Light Colour\n * @property {color} NV_PROXY_PORT_DARK_COLOR=#5e5e5eff Proxy Port Dark Colour\n * @property {color} NV_MODULE_LIGHT_COLOR=#4678baff Node Light Colour\n * @property {color} NV_MODULE_DARK_COLOR=#3a4a87ff Node Dark Colour\n * @property {color} NV_MODULE_SHADOW_COLOR=#000000ff Node Shadow Colour\n * @property {color} NV_MODULE_EDITOR_LIGHT_COLOR=#ffea4cff Node Editor Button Light Colour\n * @property {color} NV_MODULE_EDITOR_DARK_COLOR=#a0911eff Node Editor Button Dark Colour\n * @property {color} NV_MODULE_DISPLAY_LIGHT_COLOR=#f0f9f8ff Node Display Light Colour\n * @property {color} NV_MODULE_DISPLAY_DARK_COLOR=#bac6c8ff Node Display Dark Colour\n * @property {color} NV_MODULE_GROUP_LIGHT_COLOR=#c3e0f8ff Group Light Colour\n * @property {color} NV_MODULE_GROUP_DARK_COLOR=#768896ff Group Dark Colour\n * @property {color} NV_MODULE_MOVE_LIGHT_COLOR=#8bbe55ff Move Nodes Light Colour\n * @property {color} NV_MODULE_MOVE_DARK_COLOR=#576a36ff Move Nodes Dark Colour\n * @property {color} NV_MODULE_IO_LIGHT_COLOR=#7399c3ff I/O Nodes Light Colour\n * @property {color} NV_MODULE_IO_DARK_COLOR=#304262ff I/O Nodes Dark Colour\n * @property {color} NV_MODULE_NO_FLATTEN_LIGHT_COLOR=#7292e2ff Composite Nodes Light Colour When Not Flattening the Output\n * @property {color} NV_MODULE_NO_FLATTEN_DARK_COLOR=#313c66ff Composite Nodes Dark Colour When Not Flattening the Output\n * @property {color} NV_MODULE_CAMERA_LIGHT_COLOR=#9dd3c9ff Camera Nodes Light Colour\n * @property {color} NV_MODULE_CAMERA_DARK_COLOR=#719798ff Camera Nodes Dark Colour\n * @property {color} NV_PORT_MATRIX_LIGHT_COLOR=#7fc820ff Peg Port Light Colour\n * @property {color} NV_PORT_MATRIX_DARK_COLOR=#465f2aff Peg Port Dark Colour\n * @property {color} NV_PORT_IMAGE_LIGHT_COLOR=#3dadddff Image Port Light Colour\n * @property {color} NV_WIDE_CABLE_INNER_COLOR=#286eb4ff Wide Cable Inner Line\n * @property {color} NV_PORT_IMAGE_DARK_COLOR=#31445aff Image Port Dark Colour\n * @property {color} NV_PORT_KEEP_IMAGE_LIGHT_COLOR=#e6d72eff Flagged Nodes Image Port Light Colour\n * @property {color} NV_PORT_KEEP_IMAGE_DARK_COLOR=#967929ff Flagged Nodes Image Port Dark Colour\n * @property {color} NV_Z_PORT_LIGHT_COLOR=#4be6e6ff Output Z Input Port Light Colour\n * @property {color} NV_Z_PORT_DARK_COLOR=#2d8282ff Output Z Input Port Dark Colour\n * @property {color} NV_MODULE_GROUP_EFFECT_LIGHT_COLOR=#ffbe22ff Node Group Effect Light Colour\n * @property {color} NV_MODULE_GROUP_EFFECT_DARK_COLOR=#b78818ff Node Group Effect Dark Colour\n * @property {double} NV_MAGNIFIER_SCALE_FACTOR=5 Zoom Factor\n * @property {double} NV_MAGNIFIER_WIDTH_MULTIPLE=6 Zoom Factor\n * @property {double} NV_MAGNIFIER_ASPECT_RATIO=1.5 Aspect Ratio\n * @property {double} NV_MAGNIFIER_OPACITY=80 Opacity\n * @property {TUDirectionFlagsItem} NV_PORT_IN_ORDERING=RIGHT_TO_LEFT Port Input Ordering\n * @property {TUDirectionFlagsItem} NV_PORT_OUT_ORDERING=RIGHT_TO_LEFT Port Output Ordering\n * @property {TUCableTypeItem} NV_CABLE_TYPE=BEZIER Cable Type\n * @property {TUCornerTypeItem} NV_WORLD_VIEW_STARTING_CORNER=SE World View Starting Corner\n * @property {double} NV_ANTIALIASING_EXPONENT=1 Value of Antialiasing Exponent\n * @property {TUAntialiasQualityItem} NV_ANTIALIASING_QUALITY=HIGH Setting of Antialiasing Quality\n * @property {TUAlignmentRuleItem} NV_ALIGNMENT_RULE=CENTER_FIRST_PAGE Alignment Rule\n * @property {TUUseDrawingPivotMethodItem} NV_EMBEDDED_PIVOT=APPLY_ON_READ_TRANSFORM Embedded Pivot\n * @property {TUPremultiplyItem} NV_TRANSPARENCY_TYPE=N Transparency Type\n * @property {bool} NV_ENABLE_EXTERNAL_READ=false Enables the NV_EXTERNAL_READ_THRESHOLD preference\n * @property {bool} NV_READ_COLOR=true Read Colour\n * @property {bool} NV_READ_TRANSPARENCY=true Read Transparency\n * @property {double} NV_EXTERNAL_READ_THRESHOLD=1.5 Size threshold of input image to use external read node compute.\n * @property {double} NV_COLOR_CARD_Z_OFFSET=-12 The Z offset default value of colour card.\n * @property {bool} NV_PAN_ON_MIDDLE_MOUSE_BUTTON=true Pan on Middle Mouse\n * @property {bool} NV_DOUBLE_CLICK_OPENS_EDITOR=false Double Mouse Click opens Editor\n * @property {TUDrawingModeItem} NV_OVERLAY_VECTORBITMAP=Vector Sets Overlay Art to either Bitmap or Vector\n * @property {TUDrawingModeItem} NV_LINEART_VECTORBITMAP=Vector Sets Line Art to either Bitmap or Vector\n * @property {TUDrawingModeItem} NV_COLOURART_VECTORBITMAP=Vector Sets Colour Art to either Bitmap or Vector\n * @property {TUDrawingModeItem} NV_UNDERLAY_VECTORBITMAP=Vector Sets Underlay to either Bitmap or Vector\n * @property {bool} COMPOSITE_DEFAULT_PASS_THROUGH=true Default Pass Through Composite\n */\n\n/**\n * Preferences in the Exposure Sheet category.\n * @name preferences#Exposure Sheet\n * @property {int} XSHEET_DEFAULT_COLUMN_WIDTH=100 Default Column Width\n * @property {int} XSHEET_MIN_ZOOM=5 Xsheet Minimum Zoom Level\n * @property {int} XSHEET_MAX_ZOOM=16 Xsheet Maximum Zoom Level\n * @property {bool} XSHEET_NAME_BY_FRAME=false When enabled, new drawings will be named based on the current frame position, otherwise drawings will be named based on creation order.\n * @property {bool} XSHEET_APPLYNEXT_LEFTRIGHT=false Edit Columns Left to Right\n * @property {bool} XSHEET_APPLYNEXT_RIGHTLEFT=true Edit Columns Right to Left\n * @property {bool} XSHEET_SHOW_SELECTION=false Show Selection\n * @property {bool} XSHEET_SHOW_DRAWING_COLS=true Show Drawing Columns\n * @property {bool} XSHEET_SHOW_FUNCTION_COLS=true Show Function Columns\n * @property {bool} XSHEET_SHOW_3DPATH_COLS=true Show 3D Path Columns\n * @property {bool} XSHEET_SHOW_3DROTATION_COLS=true Show 3D Rotation Columns\n * @property {bool} XSHEET_SHOW_SOUND_COLS=true Show Sound Columns\n * @property {bool} XSHEET_SHOW_ANNOTATION_COLS=true Show Annotation Columns\n * @property {bool} XSHEET_ANNOTATION_FRAME_MARKER=false Show Frame Marker in Annotation Columns\n * @property {double} XS_ANNOTATION_HARDNESS=10 Annotation Column Antialiasing Value\n * @property {color} XSHEET_BACKGROUND_COLOR_DARK=#171717ff Background Colour\n * @property {color} XSHEET_BACKGROUND_COLOR=#cbcbcbff Background Colour\n * @property {color} XSHEET_CURRENT_FRAME_COLOR=#5d5d5dff Current Frame Colour\n * @property {color} XSHEET_FRAME_BEAT_COLOR=#0000faff Frame per beat Colour\n * @property {color} XSHEET_BEAT_BAR_COLOR=#fa0000ff Beat Per bar Colour\n * @property {color} XSHEET_CURRENT_DRAWING_COLOR=#8a0000ff Current Drawing Colour\n * @property {color} XSHEET_LIGHT_TABLE_COLOR=#f2d5d1ff Light Table Colour\n * @property {color} XSHEET_ONION_SKIN_COLOR=#b6d7f7ff Onion Skin Colour\n * @property {int} XSHEET_DEFAULT_HOLD_VALUE=1 Xsheet Default Hold Value\n * @property {bool} XSHEET_GESTURAL_DRAG_ENABLED=true Xsheet Gestural Drag\n * @property {bool} XSHEET_CENTRE_ON_CURRENT_FRAME=false Xsheet Centre on Current Frame\n * @property {color} XSHEET_STOP_MOTION_KEYFRAME_COLOR=#8c0000ff Xsheet Stop-Motion Keyframe Colour\n * @property {color} DRAWING_TV_COLOR=#ffffffff Drawing Column Colour\n * @property {color} TIMING_TV_COLOR=#f5dcb9ff Timing Column Colour\n * @property {color} 3D_PATH_TV_COLOR=#dededeff 3D Path Column Colour\n * @property {color} BEZIER_TV_COLOR=#c7c6bbff Bezier Column Colour\n * @property {color} VELOBASED_TV_COLOR=#f5fff0ff Velobased Column Colour\n * @property {color} EASE_TV_COLOR=#d5cdc3ff Ease Column Colour\n * @property {color} EXPR_TV_COLOR=#c4d1dfff Expression Column Colour\n * @property {color} SOUND_TV_COLOR=#8a8a8aff Sound Column Colour\n * @property {color} ANNOTATION_TV_COLOR=#ffffffff Annotation Column Colour\n * @property {TUXSheetAddColumnsItem} XSHEET_ADD_COLUMNS=BOTTOM Default Xsheet insertion option\n * @property {int} XSHEET_LINE_HOLD=3 Threshold exposure for hiding the holding line.\n */\n\n/**\n * Preferences in the Color Management category.\n * @name preferences#Color Management\n * @property {bool} COLOR_ENABLE_INTERACTIVE_COLOR_RECOVERY=true Enable Interactive Colour Recovery\n * @property {bool} COLOR_ENABLE_COLOR_RECOVERY=true Enable Colour Recovery\n * @property {bool} IS_SWATCH_MODE=false Use swatch mode to display the palettes\n * @property {bool} IS_BITMAP_SWATCH_MODE=true Use swatch mode to display the bitmap palettes\n * @property {bool} SB_IS_HSV_MODE=true Colour view sliders displayed in HSV mode.\n * @property {bool} SYNC_VECTOR_WITH_BITMAP_COLOUR=false Attempt to maintain sync between the selected vector and bitmap colour\n * @property {bool} COLOR_USE_ELEMENT_PALETTE_LIST=false Use element palette lists.\n * @property {color} COLOR_REPLACEMENT_COLOR=#ff0000ff Replacement Colour\n */\n\n/**\n * Preferences in the Drawing Mode category.\n * @name preferences#Drawing Mode\n * @property {bool} DRAWING_CREATE_EXTEND_EXPOSURE=true When a new drawing is created, automatically extend the exposure of previous drawings\n * @property {shortcut} DRAWING_COLOR_DROPPER_TOOL_KEY=Alt+D Dropper Tool\n * @property {shortcut} DRAWING_ZOOM_TOOL_KEY=Alt+Z Zoom Tool\n * @property {TUSideTypeItem} DRAWING_VIEW_THUMBNAILS_LOCATION=LEFT Thumbnail View location\n * @property {bool} DRAWING_VIEW_DO_NOT_ZOOM_WHEN_RESIZE=true The Drawing View Does not Zoom When Resized\n * @property {bool} DRAWING_GRID_ON_BY_DEFAULT=false Grid On By Default\n * @property {bool} DRAWING_LIGHTTABLE_ENABLE_SHADE=true Light Table: Enable Shade\n * @property {bool} DRAWING_LIGHTTABLE_ENABLE_SHADE_FRAME_VIEW=true Shade other drawing in camera view\n * @property {bool} DRAWING_ENHANCED_ONION_SKIN=false Shows Auto Light Table on Every Drawing Visible in the Onion Skin\n * @property {bool} DRAWING_AUTOSAVE_PENSTYLE=true Auto Save Pencil Styles\n * @property {bool} DRAWING_USE_ROTATION_LEVER=false Use Rotation Lever Handle for Select Tool and Transform Tool.\n * @property {TUOnionSkinRenderModeItem} DRAWING_ONIONSKIN_RENDER_MODE=SHADE Onion Skin Render Mode (Normal, Shade or Outline) \n * @property {TUOnionSkinDrawingModeItem} DRAWING_ONIONSKIN_DRAWING_MODE=byFrames Onion Skin Drawing Mode (byFrames or byDrawing) \n * @property {bool} DRAWING_SELECT_TOOL_IS_LASSO=true Select Tool Is Lasso\n * @property {bool} TOOL_BOUNDING_BOX_MOVABLE=false Select Tool Bounding Box is Movable\n * @property {bool} DRAWING_SYNCHRONIZE_ERASER=false Synchronize Eraser\n * @property {bool} DRAWING_DEFAULT_COLOR_PICKER_IS_MULTIWHEEL=false Default Colour Picker interface is Multiwheel\n * @property {bool} DRAWING_MOUSEMOVE_INTERPOLATION=true Interpolates the input points from the tablet (or the mouse) to generate a smooth curve.\n * @property {DT_StabilizerMode} DRAWING_STABILIZER_MODE=NoStabilizer \n * @property {DT_StabilizerMode} DRAWING_STABILIZER_ACTIVE_MODE=NoStabilizer \n * @property {bool} DRAWING_BRUSH_SIZE_CURSOR_ON=false Display real brush cursor\n * @property {bool} DRAWING_NEW_COLOR=false Create the new colour in the palette\n * @property {bool} DRAWING_TOOL_MODE_OVERRIDE=true Tool overrides may also change internal tool mode\n * @property {bool} DRAWING_SHOW_CURRENT_DRAWING_ON_TOP=false Show current drawing on top.\n * @property {bool} DRAWING_STICKY_EYEDROPPER=false Sticky Eyedropper\n * @property {int} DRAWING_CLOSE_GAP_VALUE=0 Auto Gap Closing on Startup\n * @property {bool} DRAWING_CLOSE_GAP_VALUE_IN_PIXEL=true Auto Gap Closing in Pixel Unit.\n * @property {double} DRAWING_MORPHING_QUALITY=0.2 Morphing quality\n * @property {int} DRAWING_PENCIL_TO_BRUSH_CANVAS_SIZE=4096 Pencil to Brush vectorization canvas size.\n * @property {double} DRAWING_ONIONSKIN_MINIMUM_WASH_PERCENT=0.4 Onion Skin : Minimum Wash Value (0.0-1.0)\n * @property {double} DRAWING_ONIONSKIN_MAXIMUM_WASH_PERCENT=0.8 Onion Skin : Maximum Wash Value (0.0-1.0)\n * @property {double} DRAWING_ONIONSKIN_MIN_OPACITY=0.2 This preference controls the minimum % of opacity for the onion skin.\n * @property {double} DRAWING_ONIONSKIN_MAX_OPACITY=0.8 This preference controls the maximum % of opacity for the onion skin.\n * @property {double} DRAWING_LIGHTTABLE_OPACITY=0.5 This preference controls the % of opacity for the light table.\n * @property {double} DRAWING_LIGHTTABLE_WASH=0.2 Light Table: wash % (between 0.0-1.0)\n * @property {double} DRAWING_PENCIL_LINES_OPACITY=100 Pencil Lines Opacity: (between 0 and 100)\n * @property {color} DRAWING_BACKLITE_COLOR=#3e4c7dff Backlight Colour\n * @property {double} DRAWING_HIGHLIGHT_COLOR_ALPHA=1 Colour Highlight Mode Opacity\n * @property {color} DRAWING_BACKGROUND_DARK=#e1e1e1ff Background Colour\n * @property {color} DRAWING_BACKGROUND=#ffffffff Background Colour\n * @property {color} BRUSH_PREVIEW_BACKGROUND_DARK=#e1e1e1ff Brush Preview Background Colour\n * @property {color} BRUSH_PREVIEW_BACKGROUND=#ffffffff Brush Preview Background Colour\n * @property {color} DRAWING_GRID_COLOR=#8c8c8cff Grid Colour\n * @property {color} DRAWING_ONION_SKIN_COLOR_AFTER=#377837ff Onion Skin: Colour After\n * @property {color} DRAWING_ONION_SKIN_COLOR_BEFORE=#ff0000ff Onion Skin: Colour Before\n * @property {color} DRAWING_ONION_SKIN_COLOR_PREVELEMENT1=#bc7d00ff Onion Skin: Colour Previous Element\n * @property {color} DRAWING_ONION_SKIN_COLOR_PREVELEMENT2=#e19600ff Onion Skin: Colour Previous 2nd Element\n * @property {color} DRAWING_ONION_SKIN_COLOR_PREVELEMENT3=#f8c800ff Onion Skin: Colour Previous 3rd Element\n * @property {color} DRAWING_ONION_SKIN_COLOR_PREVELEMENT4=#ffea00ff Onion Skin: Colour Previous 4th Element\n * @property {color} DRAWING_ONION_SKIN_COLOR_NEXTELEMENT1=#0000ffff Onion Skin: Colour Next Element\n * @property {color} DRAWING_ONION_SKIN_COLOR_NEXTELEMENT2=#3b58ffff Onion Skin: Colour Next 2nd Element\n * @property {color} DRAWING_ONION_SKIN_COLOR_NEXTELEMENT3=#87afffff Onion Skin: Colour Next 3rd Element\n * @property {color} DRAWING_ONION_SKIN_COLOR_NEXTELEMENT4=#c1d7ffff Onion Skin: Colour Next 4th Element\n * @property {bool} DRAWING_ADJUST_PIXEL_RESOLUTION_TO_CAMERA_FOR_NEW_DRAWINGS=false Pixel Density Proportional to Camera\n * @property {color} PALETTE_MANAGER_COLOR=#ffffffff Palette Manager Background Colour\n * @property {color} PALETTE_MANAGER_COLOR_DARK=#171717ff Palette Manager Background Colour\n * @property {color} CAMERA_VIEW_DRAWING_TOOL_LABEL_COLOR=#f2f2c8ff Camera View Label Colour\n * @property {color} DRAWING_NEW_COLOR_DEFAULT_VALUE=#464646ff Drawing Mode: New Default Colour\n * @property {TUDrawingToolItem} DRAWING_INITIAL_TOOL=Close Gap Initial Drawing Tool\n * @property {TUDrawingToolItem} PAINT_MODE_DRAWING_INITIAL_TOOL=PAINT Initial Drawing Tool\n * @property {color} DRAWING_SMOOTH_BRUSH_COLOR=#ffff00ff Overlay brush colour used with smoothing tool.\n * @property {bool} DT_SELECT_TOOL_SNAP_TO_GRID=true Select tool can snap to grid.\n * @property {bool} DT_CONTOUR_EDITOR_SNAP_AND_ALIGN=true Contour Editor tool can snap and align to boxes.\n * @property {bool} DT_CONTOUR_EDITOR_TOOL_SNAP_TO_GRID=true Contour Editor tool can snap to grid.\n * @property {bool} DT_SHAPE_TOOL_SNAP_TO_GRID=true Shape tools can snap to grid.\n * @property {bool} DT_PIVOT_TOOL_SNAP_TO_GRID=true Pivot tool can snap to grid.\n * @property {bool} DRAWING_ENABLE_PAPER_ZOOM=false Paper zoom flag.\n * @property {double} DRAWING_PAPER_ZOOM_PIXELS_PER_INCH=72 Paper zoom number of pixels per inch (ppi).\n * @property {int} DRAWING_PAPER_ZOOM_MAX_ZOOM=4 Paper zoom maximum zoom level.\n * @property {double} DRAWING_PAPER_ZOOM_MAGNIFY_ZOOM=2 Magnifier zoom factor.\n * @property {int} DRAWING_PAPER_ZOOM_MAGNIFY_WIDTH=150 Magnifier zoom window width.\n * @property {int} DRAWING_PAPER_ZOOM_MAGNIFY_HEIGHT=150 Magnifier zoom window height.\n * @property {int} DRAWING_PAPER_ZOOM_MAGNIFY_OFFSETX=-90 Magnifier zoom window horizontal offset.\n * @property {int} DRAWING_PAPER_ZOOM_MAGNIFY_OFFSETY=90 Magnifier zoom window vertical offset.\n * @property {double} QUICK_ZOOM_MAGNIFY_ZOOM=4 Quick Close Up zoom factor.\n * @property {DT_PencilTipModeItem} DRAWING_CUTTER_TIP_MODE=BevelTip \n * @property {DT_PencilTipModeItem} DRAWING_CONTOUR_EDITOR_TIP_MODE=RoundTip \n * @property {DT_PencilTipModeItem} DRAWING_ERASER_TIP_MODE=BevelTip \n * @property {DT_PencilTipModeItem} DRAWING_INK_TIP_MODE=BevelTip \n */\n\n/**\n * Preferences in the Highlighting category.\n * @name preferences#Highlighting\n * @property {double} NODE_HIGHLIGHT_INTENSITY=0.3 Node Intensity\n * @property {double} CURRENT_FRAME_HIGHLIGHT_INTENSITY=0.3 Current Frame Intensity\n * @property {double} ALL_FRAMES_HIGHLIGHT_INTENSITY=0.3 All Frames Intensity\n * @property {double} FRAME_RANGE_HIGHLIGHT_INTENSITY=0.3 Frame Range Intensity\n * @property {double} SPLINE_POSITION_HIGHLIGHT_INTENSITY=0.3 Spline Position Intensity\n * @property {color} NODE_HIGHLIGHT_COLOR=#0000ffff Node Colour\n * @property {color} ELEMENT_HIGHLIGHT_COLOR=#ff00ffff Element Colour\n * @property {double} ELEMENT_HIGHLIGHT_INTENSITY=0.3 Element Intensity\n * @property {color} CURRENT_FRAME_HIGHLIGHT_COLOR=#ffff00ff Current Frame Colour\n * @property {color} FRAME_RANGE_HIGHLIGHT_COLOR=#00ff00ff Range Frame Colour\n * @property {color} ALL_FRAMES_HIGHLIGHT_COLOR=#ff0000ff All Frames Colour\n */\n\n/**\n * Preferences in the Element Manager category.\n * @name preferences#Element Manager\n * @property {int} SCAN_TYPE=2 Scan Type\n * @property {string} PIXMAP_FORMAT=SCAN Pixmap Format\n * @property {bool} ADVANCED_ELEMENT_TRADITIONAL=false Advanced Element Properties\n * @property {bool} ADVANCED_ELEMENT_BASIC_ANIMATE=false More advanced element property than essentials\n * @property {bool} ADVANCED_ELEMENT_AUTO_RENAME=true Automatically rename an element when renaming the node/layer referencing that element\n */\n\n/**\n * Preferences in the Function Editor category.\n * @name preferences#Function Editor\n * @property {bool} FE_GRID_ON=true Show Grid\n * @property {int} FE_LOAD_LIMIT=50 Number of functions displayed in the canvas.\n * @property {bool} FE_3DPATH_CONST_Z_DEFAULT=false 3D Path Constant Z Default\n * @property {color} FE_BG_COLOR=#787878ff Background Colour\n * @property {color} FE_GRID_COLOR=#888888ff Grid Colour\n * @property {color} FE_BG_CURVE_COLOR=#b4b4b4ff Background Curve Colour\n * @property {color} FE_BG_X_CURVE_COLOR=#ff0000ff Background X Curve Colour\n * @property {color} FE_BG_Y_CURVE_COLOR=#00ff00ff Background Y Curve Colour\n * @property {color} FE_BG_Z_CURVE_COLOR=#0000ffff Background Z Curve Colour\n * @property {color} FE_BG_VELO_CURVE_COLOR=#b4b4b4ff Background velocity Curve Colour\n * @property {color} FE_FG_CURVE_COLOR=#000000ff Foreground Curve\n */\n\n/**\n * Preferences in the Library category.\n * @name preferences#Library\n * @property {bool} LIBRARY_AUTO_GENERATE_THUMBNAILS=true Auto Generate Thumbnails\n * @property {bool} LIBRARY_PASTE_CREATE_NEW_DRAWING=false Create new drawings\n * @property {TUPaletteOperationItem} LIBRARY_PASTE_PALETTE=USE_COPY Template palette operation preferences\n */\n\n/**\n * Preferences in the Playback View category.\n * @name preferences#Playback View\n * @property {bool} PLAY_ENABLE_SOUND=false Enable sound\n * @property {bool} PLAY_ENABLE_SCRUBBING=false Enable sound scrubbing\n * @property {bool} PLAY_ENABLE_PREROLL=false Enable preroll playback\n * @property {int} PLAY_PREROLL_FRAMES=24 Set number of preroll frames\n * @property {bool} PLAY_ENABLE_LOOP=false Enable loop playback\n */\n\n/**\n * Preferences in the Scanning category.\n * @name preferences#Scanning\n * @property {int} SCANNING_PEGGING=2 Pegging mode of the scanner\n * @property {int} SCANNING_PEGSIDE=0 Side of the peg\n * @property {int} SCANNING_DPI=300 Scanning dpi value\n * @property {double} SCANNING_GAMMA=1.05 Gamma value\n * @property {int} SCANNING_THRESHOLD=100 The threshold value from 1 to 255\n * @property {int} SCANNING_WHITEPOINT=190 The white point value from 1 to 255\n * @property {int} SCANNING_BLACKPOINT=70 The black point value from 1 to 255\n * @property {int} SCANNING_SCANNER=0 \n * @property {int} SCANNING_PAPERSIZE=1 Paper size of the scanned object\n * @property {bool} SCANNING_OPTREG=true The optical registration value\n * @property {bool} SCANNING_FLIPDRAWING=false Do we flip the drawing\n * @property {bool} SCANNING_SCANANDADVANCE=false Advance to the next frame\n * @property {bool} SCANNING_OVERDRAWING=false Do we overwrite drawing?\n * @property {int} SCANNING_SCANDEPTH=0 Scan depth\n * @property {string} SCANNING_SCANNER_NAME= Scanner name\n */\n\n/**\n * Preferences in the Render category.\n * @name preferences#Render\n * @property {bool} SOFT_RENDER_ENABLE_BLUR=true Enable blur.\n * @property {bool} SOFT_RENDER_ENABLE_LINE_TEXTURE=true Enable line texture.\n * @property {bool} SOFT_RENDER_ENABLE_FOCUS=true Enable focus.\n * @property {int} SOFT_RENDER_IMAGE_MEMORY_PERCENT=25 Cache size for intermediate preview images (percentage of system RAM)\n * @property {string} SOFT_RENDER_CACHE_PATH= Cache path and size on disk for intermediate preview images\n * @property {bool} SOFT_RENDER_ENABLE_DISK_CACHE=false Enable the cache on disk for intermediate preview images\n * @property {int} SOFT_RENDER_THREADS=0 Number of rendering threads for all type of soft rendering. A value of zero automatically sets the number of threads based on the number of CPU cores. \n * @property {bool} SOFT_RENDER_GPU=true Enable the GPU\n * @property {bool} SOFT_RENDER_LOAD_OPENCL=true Enable the GPU (requires a restart of the application)\n * @property {bool} RENDER_ENABLE_TONE_AND_HIGHLIGHT=true Disabling this will produce a drawing performance improvement in the OpenGL views.\n * @property {bool} RENDER_ENABLE_CUTTER=true Disabling cutters and mask effects will produce a drawing performance improvement in the openGL views.\n * @property {bool} RENDER_ENABLE_TEXTURES_AND_GRADIENTS_IN_DRAWINGS=true Disabling this will produce a drawing performance improvement in the OpenGL views.\n * @property {bool} RENDER_ENABLE_TEXTURES_IN_PENCIL_LINES=true Disabling this will produce a drawing performance improvement in the OpenGL views.\n * @property {bool} RENDER_ENABLE_COLOUR_OVERRIDE=true Disabling this will produce a drawing performance improvement in the OpenGL views.\n * @property {bool} RENDER_ENABLE_VARIABLE_LINE_THICKNESS=false Disabling this will produce a drawing performance improvement in the OpenGL views.\n * @property {bool} RENDER_ENABLE_GLUE_MODULE=true \n * @property {bool} OPENGL_ENABLE_PLAYBACK_CACHE=true Enable image caching for opengl playback\n * @property {int} OPENGL_PLAYBACK_CACHE_SIZE_MB=2000 Cache size for playback images in Mb\n * @property {bool} OPENGL_ENABLE_CACHELOCK_NODE=true Enable rendering caching of GL cache lock nodes\n * @property {int} OPENGL_LOCK_NODE_CACHE_SIZE_MB=128 Cache size for OpenGL lock node caching in Mb\n * @property {bool} OPENGL_ENABLE_COMPOSITE_NODE_CACHE=true Enable rendering caching of non-passthrough composite nodes\n * @property {int} OPENGL_COMPOSITE_NODE_CACHE_SIZE_MB=256 Cache size for OpenGL composite node caching in Mb\n * @property {bool} OPENGL_ENABLE_CAMERAVIEW_CACHE=false Enable rendering caching of camera view\n * @property {bool} OPENGL_ENABLE_PERSISTENT_CACHE_NODE=true Enable rendering caching of persistent cache nodes\n * @property {int} OPENGL_RIG_CACHE_SIZE_MB=2048 Maximum size used by the OpenGL Node Cache in RAM\n * @property {int} OPENGL_RIG_CACHE_DISK_SIZE_MB=10240 Maximum size used by the OpenGL Node Cache on disk\n * @property {bool} OPENGL_RIG_CACHE_WAL_MODE=false WAL journal mode used by the OpenGL Node Cache on disk\n * @property {bool} OPENGL_SUPPORT_TRIPLE_BUFFER=true Support triple buffering\n * @property {bool} OPENGL_SUPPORT_DESKTOP_EFFECTS=false Support desktop effects\n * @property {bool} OPTIMIZED_DRAWING_LOADER__USE_OPTIMIZED=false Use optimized drawings for OpenGL rendering\n * @property {double} OPTIMIZED_DRAWING_LOADER__DISCRETIZATION_SCALE=0.25 Discretization scale used for calculation of optimized drawing.\n * @property {int} OPTIMIZED_DRAWING_LOADER__QUALITY_FACTOR=1 Quality factor used for calculation of optimized drawing.\n * @property {double} DEFORMATION_DRAWING_DISCRETIZATION_SCALE=0.25 Discretization scale used for calculation of deformed drawing.\n * @property {int} DEFORMATION_DRAWING_CACHE_SIZE=200 Number of deformed drawings to save in cache.\n * @property {bool} OPENGL_ENABLE_FSAA=true Enable Full Scene Anti-Aliasing\n * @property {int} OPENGL_SUPERSAMPLING_FSAA=4 Number of samples used for Full Scene Anti-Aliasing\n * @property {bool} OPENGL_GENERATE_MIPMAP=true Generate anti-aliased mipmap textures.\n * @property {bool} BITMAP_PREMULTIPLY_ALPHA=true Premultiply alpha to colour channels for Bitmap Layer.\n * @property {bool} USE_PBUFFER_FOR_PICKING=true Use PBuffer for Picking\n * @property {bool} PDF_SUPPORT_CMYK=true Support CMYK in Import\n * @property {bool} PDF_SUPPORT_SEPARATE_LAYERS=true Support Separate Layers in Import\n * @property {double} IK_MIN_MAX_ANGLE_CONSTRAINT_WEIGHT=0.0005 Weight of minmax angle constraint\n * @property {bool} OPENSG_RENDER_FIRST=false Render OpenSG elements first during composition.\n * @property {bool} FBX_TRIANGULATE_IMPORT=true Triangulate Fbx mesh during import.\n */\n\n/**\n * Preferences in the ToolProperties category.\n * @name preferences#ToolProperties\n * @property {int} TP_BRUSH_PENSTYLELIST_DISPLAY_MODE=2 Brush Tool Properties Pen Style List Default View Mode.\n * @property {int} TP_SHAPE_PENSTYLELIST_DISPLAY_MODE=0 (Rectangle/Ellipse/Line) Tool Properties Pen Style List Default View Mode.\n * @property {int} TP_POLYLINE_PENSTYLELIST_DISPLAY_MODE=0 Polyline Tool Properties Pen Style List Default View Mode.\n */\n\n/**\n * Preferences in the Backdrops category.\n * @name preferences#Backdrops\n * @property {color} BACKDROP_INNER_STROKE_COLOR=#373737ff \n * @property {color} BACKDROP_RESIZE_COLOR=#373737ff \n * @property {int} BACKDROP_SELECTED_TRANSPARENCY=220 \n * @property {int} BACKDROP_UNSELECTED_TRANSPARENCY=170 \n * @property {int} BACKDROP_DEFAULT_TITLE_SIZE=14 \n * @property {int} BACKDROP_DEFAULT_DESCRIPTION_SIZE=14 \n * @property {int} BACKDROP_COLOR_LIST_SIZE=17 \n * @property {color} BACKDROP_COLOR_0=#9a0707ff \n * @property {color} BACKDROP_COLOR_1=#c11717ff \n * @property {color} BACKDROP_COLOR_2=#843a16ff \n * @property {color} BACKDROP_COLOR_3=#e16b14ff \n * @property {color} BACKDROP_COLOR_4=#dcaa32ff \n * @property {color} BACKDROP_COLOR_5=#81c615ff \n * @property {color} BACKDROP_COLOR_6=#7b8d03ff \n * @property {color} BACKDROP_COLOR_7=#077f04ff \n * @property {color} BACKDROP_COLOR_8=#084c18ff \n * @property {color} BACKDROP_COLOR_9=#0d6b58ff \n * @property {color} BACKDROP_COLOR_10=#023cbeff \n * @property {color} BACKDROP_COLOR_11=#460fe3ff \n * @property {color} BACKDROP_COLOR_12=#6c0e9cff \n * @property {color} BACKDROP_COLOR_13=#a521a3ff \n * @property {color} BACKDROP_COLOR_14=#e30fa0ff \n * @property {color} BACKDROP_COLOR_15=#e30f69ff \n * @property {color} BACKDROP_COLOR_16=#323232ff \n */\n\n/**\n * Preferences in the user category.\n * @name preferences#user\n * @property {bool} ADVANCED_PALETTELIST=false \n * @property {string} AMG_VIEW_RESOURCE_FOLDER= \n * @property {bool} ANIMATE_WAS_NEVER_RUN=false \n * @property {bool} COLORVIEW_SHOW_PALETTELIST=true \n * @property {int} COLOR_SELECTION_STARTINGBUTTON=0 \n * @property {double} DBL_MEDIAN_MODULE_STEP_RADIUS=0.1 \n * @property {double} DBL_MEDIAN_MODULE_STEP_RADIUS_MAX=2160 \n * @property {double} DBL_MEDIAN_MODULE_STEP_RADIUS_MIN=0 \n * @property {double} DBL_SHAKE_ANGLE_PARAMETER=0.1 \n * @property {double} DBL_SHAKE_NORMAL_PARAMETER=0.01 \n * @property {double} DBL_SHAKE_POSITION_PARAMETER=0.1 \n * @property {double} DBL_STEP_MATTEBLUR_COLOUR_GAIN=0.1 \n * @property {double} DBL_STEP_MATTEBLUR_COLOUR_GAIN_MAX=1.79769e+308 \n * @property {double} DBL_STEP_MATTEBLUR_COLOUR_GAIN_MIN=0 \n * @property {bool} DRAWING_CLOSE_GAP_ON=false \n * @property {string} DRAWING_PRESSURE_CURVE= \n * @property {bool} DRAWING_STABILIZER_CATCH_UP=true \n * @property {bool} DRAWING_STABILIZER_SHOW_STRING=true \n * @property {double} DRAWING_STABILIZER_SMOOTHING=0 \n * @property {bool} DRAWING_STABILIZER_WITH_ERASER=false \n * @property {int} DeformationConvertDrawingsTextureSize=1024 \n * @property {color} DeformationDeformedControlHandle=#19592aff \n * @property {color} DeformationDeformedHandle=#00ff00ff \n * @property {color} DeformationDeformedSelectedChild=#dcff00ff \n * @property {color} DeformationDeformedSelectedSkeleton=#ffffffff \n * @property {color} DeformationDeformedSkeleton=#00ff00ff \n * @property {color} DeformationModuleDarkColor=#576a36ff \n * @property {color} DeformationModuleLightColor=#25a919ff \n * @property {color} DeformationRestingControlHandle=#951f39ff \n * @property {color} DeformationRestingHandle=#ff0000ff \n * @property {color} DeformationRestingSelectedChild=#ff7f00ff \n * @property {color} DeformationRestingSelectedSkeleton=#ffffffff \n * @property {color} DeformationRestingSkeleton=#ff0000ff \n * @property {double} DeformationScalingFieldSize=2 \n * @property {double} DeformationScalingPixelSize=64 \n * @property {bool} DeformationScalingUsePixelSize=false \n * @property {string} EXPORTMMX_AUDIOCONFIG= \n * @property {int} EXPORTMMX_CUSTOMRESX=0 \n * @property {string} EXPORTMMX_DISPLAY=Display \n * @property {bool} EXPORTMMX_EXPORTALL=false \n * @property {string} EXPORTMMX_LASTSCENE_NAME=\n * @property {string} EXPORTMMX_OUTPUTFILE=\n * @property {string} EXPORTMMX_OUTPUTFORMAT=mov \n * @property {int} EXPORTMMX_RANGESTART=1 \n * @property {int} EXPORTMMX_RANGESTOP=318 \n * @property {int} EXPORTMMX_RESOLUTION=2 \n * @property {string} EXPORTMMX_VIDEOAUDIOCONFIG=Enable Sound(true)Enable Video(true)QT(000000000000000000000000000003BE7365616E000000010000000600000000000001AF76696465000000010000001000000000000000227370746C000000010000000000000000726C652000000000002000000300000000207470726C000000010000000000000000000002000000000000000018000000246472617400000001000000000000000000000000000000000000000000000000000000156D70736F00000001000000000000000000000000186D66726100000001000000000000000000000000000000187073667200000001000000000000000000000000000000156266726100000001000000000000000000000000166D70657300000001000000000000000000000000002868617264000000010000000000000000000000000000000000000000000000000000000000000016656E647300000001000000000000000000000000001663666C67000000010000000000000000004400000018636D66720000000100000000000000006170706C00000014636C757400000001000000000000000000000014636465630000000100000000000000000000001C766572730000000100000000000000000003001C000100000000001574726E6300000001000000000000000000000001066973697A00000001000000090000000000000018697764740000000100000000000000000000000000000018696867740000000100000000000000000000000000000018707764740000000100000000000000000000000000000018706867740000000100000000000000000000000000000034636C617000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000001C706173700000000100000000000000000000000000000000000000187363616D000000010000000000000000000000000000001564696E74000000010000000000000000000000001575656E66000000010000000000000000000000008C736F756E0000000100000005000000000000001873736374000000010000000000000000736F777400000018737372740000000100000000000000005622000000000016737373730000000100000000000000000010000000167373636300000001000000000000000000010000001C76657273000000010000000000000000000300140001000000000015656E76690000000100000000000000000100000015656E736F000000010000000000000000010000003F7361766500000001000000020000000000000015666173740000000100000000000000000100000016737374790000000100000000000000000001) \n * @property {string} EXPORTMMX_VIDEOCONFIG= \n * @property {bool} EXPORT_IMAGE_ALLFRAMES=false \n * @property {int} EXPORT_IMAGE_EXPORTFROM=1 \n * @property {int} EXPORT_IMAGE_EXPORTTO=47 \n * @property {string} EXPORT_IMAGE_LAST_SCENE_NAME=\n * @property {bool} EXPORT_IMAGE_PREVIEW=false \n * @property {bool} GUIDE_ALIGN_ENABLED=true \n * @property {bool} GUIDE_ERASER_ENABLED=false \n * @property {bool} GUIDE_FULL_DISPLAY_ENABLED=true \n * @property {int} GUIDE_GRID_DENSITY=10 \n * @property {bool} GUIDE_GRID_ENABLED=false \n * @property {bool} GUIDE_LOCK_ENABLED=true \n * @property {bool} IMPORTIMGDLG_IMAGE_ADDTOLAYER=false \n * @property {int} IMPORTIMGDLG_IMAGE_ALIGNMENT=4 \n * @property {bool} IMPORTIMGDLG_IMAGE_BITMAP_ART=true \n * @property {int} IMPORTIMGDLG_IMAGE_BITMAP_LAYER_ALIGNMENT=6 \n * @property {int} IMPORTIMGDLG_IMAGE_BITMAP_LAYER_TRANSPARENCY=2 \n * @property {bool} IMPORTIMGDLG_IMAGE_ELEMENTONFILENAME=false \n * @property {string} IMPORTIMGDLG_IMAGE_LASTIMPORT=\n * @property {string} IMPORTIMGDLG_IMAGE_LAYERNAME= \n * @property {bool} IMPORTIMGDLG_IMAGE_NEWLAYER=true \n * @property {string} IMPORTIMGDLG_IMAGE_NEWLAYERNAME=\n * @property {bool} IMPORTIMGDLG_IMAGE_ONEELEMENT=true \n * @property {int} IMPORTIMGDLG_IMAGE_TRANSPARENCY=2 \n * @property {string} IMPORTIMGDLG_IMAGE_VECTORIZATION=Grey \n * @property {bool} IMPORTIMGDLG_IMAGE_VECTORIZE=false \n * @property {double} LENSFLARE_ANGLE_STEP=1 \n * @property {double} LENSFLARE_BLUR_STEP=0.1 \n * @property {double} LENSFLARE_INTENSITY_STEP=1 \n * @property {double} LENSFLARE_POSITION_STEP=0.01 \n * @property {double} LENSFLARE_SIZE_STEP=0.1 \n * @property {double} LENSFLARE_SPACING_STEP=0.1 \n * @property {bool} NAVIGATE_USING_NODE_VIEW_PARENTING=false \n * @property {bool} PALETTE_BROWSER_RECOLOR_SELECTED=false \n * @property {double} PARTICLE_ANGLE_STEP=1 \n * @property {double} PARTICLE_FIELD_STEP=0.05 \n * @property {double} PARTICLE_MAGNITUDE_STEP=0.01 \n * @property {double} PARTICLE_UNIT_STEP=1 \n * @property {int} PENSTYLE_BITMAP_BRUSH_LIST_SEL=-1 \n * @property {int} PENSTYLE_BITMAP_ERASER_LIST_SEL=-1 \n * @property {int} PENSTYLE_BRUSH_LIST_SEL=0 \n * @property {int} PENSTYLE_ERASER_LIST_SEL=-1 \n * @property {int} PENSTYLE_PENCIL_LIST_SEL=0 \n * @property {int} PENSTYLE_STAMP_LIST_SEL=-1 \n * @property {int} PENSTYLE_TEXTURE_QUALITY=100 \n * @property {bool} PLAYBACK_IN_PERSPECTIVE_VIEW_ENABLED=false \n * @property {bool} PLAYBACK_IN_SIDE_VIEW_ENABLED=false \n * @property {bool} PLAYBACK_IN_TOP_VIEW_ENABLED=false \n * @property {bool} PLAYBACK_IN_XSHEET_VIEW_ENABLED=false \n * @property {color} ParticleNV_actionColor=#b547ffff \n * @property {color} ParticleNV_bakerColor=#e2d06eff \n * @property {color} ParticleNV_regionColor=#09e1a3ff \n * @property {color} ParticleNV_regionCompositeColor=#99c87dff \n * @property {color} ParticleNV_shapeColor=#00c8c8ff \n * @property {color} ParticleNV_systemCompositeColor=#4b78f4ff \n * @property {color} ParticleNV_visualizerColor=#9a18ffff \n * @property {bool} ParticleShowParticlesAsDotsInOpenGL=false \n * @property {int} RENDER_TEXTUREMEMORY=1591 \n * @property {int} RIG_CACHE_DEFAULT_RENDER_POLICY=2 \n * @property {int} RIG_CACHE_RESOLUTION_LEVEL=9 \n * @property {int} ShiftAndTracePegPosition=0 \n * @property {bool} ShiftAndTraceShowCrossHair=true \n * @property {bool} ShiftAndTraceShowManipulator=true \n * @property {bool} ShiftAndTraceShowOutline=false \n * @property {bool} ShiftAndTraceShowPegs=true \n * @property {string} TB_EXTERNAL_SCRIPT_PACKAGES_FOLDER=\n * @property {string} TEMPLATE_LIBRARY_PATH0= \n * @property {int} TEMPLATE_LIBRARY_PATH_NB=1 \n * @property {int} TIMELINE_EXTRATRACK_CELL_WIDTH_NO_HDPI=8 \n * @property {int} TIMELINE_TRACK_CELL_WIDTH_NO_HDPI=8 \n * @property {bool} TL_LAYERCONNECTION_VISIBLE=false \n * @property {bool} TL_LAYERDATAVIEW_VISIBLE=false \n * @property {bool} TOOL_APPLY_TO_ALL_LAYERS=true \n * @property {bool} TOOL_AUTO_FILL_ELLIPSE=false \n * @property {bool} TOOL_AUTO_FILL_PEN=false \n * @property {bool} TOOL_AUTO_FILL_POLYLINE=false \n * @property {bool} TOOL_AUTO_FILL_RECT=false \n * @property {bool} TOOL_AUTO_FILL_STROKE=false \n * @property {bool} TOOL_LINE_MODE_STROKE=false \n * @property {int} TOOL_MERGE_WITH_TOP_LAYER_MODE=0 \n * @property {bool} TOOL_SNAP_MODE_STROKE=false \n * @property {double} TOOL_STROKE_SMOOTH_VALUE=3 \n * @property {bool} TOOL_TRIM_EXTRA_PEN=false \n * @property {bool} TOOL_TRIM_EXTRA_POLYLINE=false \n * @property {bool} TOOL_TRIM_EXTRA_STROKE=false \n * @property {bool} TOOL_TRIM_MATCH_PEN=false \n * @property {bool} TOOL_TRIM_MATCH_POLYLINE=false \n * @property {bool} TOOL_TRIM_MATCH_STROKE=false \n * @property {int} TP_PENCIL_PANEL_PENSTYLELIST_DISPLAY_MODE=2 \n * @property {int} TP_PEN_PENSTYLELIST_DISPLAY_MODE=2 \n * @property {bool} WM_SHOW_WS_WEB_MESSAGE=true \n * @property {bool} deformationCreatePosedDeformation=true \n * @property {int} particleImageEmitterHorizontalPreviewResolution=360 \n */"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_preferences.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developped by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is garanteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//      $.oPreferences class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.preferences class.\n * @classdesc  Provides access to getting/setting preferenes as an object interface.<br>All preferences that have been written to the file are accessible as properties of this class.<br>\n * Alternatively, new preferences can be retrieved with the .get function.\n * @constructor\n * @example\n * var pref = $.getPreferences();\n * pref.create( \"MyNewPreferenceName\", \"MyPreferenceValue\" );\n * pref[\"MyNewPreferenceName\"];     // Provides: MyPreferenceValue\n * pref.get(\"MyNewPreferenceName\"); // Provides: MyPreferenceValue\n */\n$.oPreferences = function( ){\n  this._type             = \"preferences\";\n  this._addedPreferences = []\n\n  this.refresh();\n}\n\n\n/**\n * Refreshes the preferences by re-reading the preference file and ingesting their values appropriately. They are then available as properties of this class.<br>\n * <b>Note, any new preferences will not be available as properties until Harmony saves the preference file at exit. In order to reference new preferences, use the get function.\n * @name $.oPreferences#refresh\n * @function\n */\n$.oPreferences.prototype.refresh = function(){\n  var fl = specialFolders.userConfig + \"/Harmony Premium-pref.xml\";\n  var nfl = new this.$.oFile( fl );\n  if( !nfl.exists ){\n    System.println( \"Unable to find preference file: \" + fl );\n    this.$.debug( \"Unable to find preference file: \" + fl, this.$.DEBUG_LEVEL.ERROR );\n    return;\n  }\n\n  var xmlDom = new QDomDocument();\n      xmlDom.setContent( nfl.read() );\n\n  if( !xmlDom ){\n    return;\n  }\n\n  var prefXML = xmlDom.elementsByTagName( \"preferences\" );\n  if( prefXML.length() == 0 ){\n    this.$.debug( \"Unable to find preferences in file: \" + fl, this.$.DEBUG_LEVEL.ERROR );\n    return;\n  }\n\n  var XMLpreferences = prefXML.at(0);\n\n  //Clear this objects previous getter/setters to make room for new ones.\n  if( this._preferences ){\n    for( n in this._preferences ){ //Remove them if they've disappeared.\n        Object.defineProperty( this, n, {\n          enumerable : false,\n          configurable: true,\n          set : function(){},\n          get : function(){}\n        });\n    }\n  }\n  this._preferences = {};\n\n  if( !XMLpreferences.hasChildNodes() ){\n    this.$.debug( \"Unable to find preferences in file: \" + fl, this.$.DEBUG_LEVEL.ERROR );\n    return;\n  }\n\n  //THE DEFAULT SETTER\n  var set_val = function( pref, name, val ){\n    var prefObj = pref._preferences[name];\n\n    //Check against types, unable to set types differently.\n    switch( typeof val ){\n      case 'string':\n        if( prefObj[\"type\"] != \"string\" ){\n          throw ReferenceError( \"Harmony does not support preference type-changes. Preference must remain \" + prefObj[\"type\"] );\n        }\n        preferences.setString( name, val );\n        break;\n      case 'number':\n        if( prefObj[\"type\"] == \"int\" ){\n          val = Math.floor( val );\n          preferences.setInt( name, val );\n        }else if( prefObj[\"type\"] == \"double\" ){\n          //This is fine.\n          preferences.setDouble( name, val );\n        }else{\n          throw ReferenceError( \"Harmony does not support preference type-changes. Preference must remain \" + prefObj[\"type\"] );\n        }\n        break\n      case 'boolean':\n      case 'undefined':\n      case 'null':\n        if( prefObj[\"type\"] != \"bool\" ){\n          throw ReferenceError( \"Harmony does not support preference type-changes. Preference must remain \" + prefObj[\"type\"] );\n        }\n        preferences.setBool( name, val ? true:false );\n        break\n      case 'object':\n      default:\n        var set = false;\n        try{\n          if( val.r && val.g && val.b && val.a ){\n            if( prefObj[\"type\"] != \"color\" ){\n              throw ReferenceError( \"Harmony does not support preference type-changes. Preference must remain \" + prefObj[\"type\"] );\n            }\n\n            value = preferences.setColor( name, new ColorRGBA( val.r, val.g, val.b, val.a ) );\n            set = true;\n          }\n        }catch(err){\n        }\n\n        if(!set){\n          if( prefObj[\"type\"] != \"string\" ){\n            throw ReferenceError( \"Harmony does not support preference type-changes. Preference must remain \" + prefObj[\"type\"] );\n          }\n          var json_val = 'json('+JSON.stringify( val )+')';\n          preferences.setString( name, json_val );\n        }\n        break\n    }\n\n    {\n      pref._preferences[name].value = val;\n    }\n  }\n\n  //THE DEFAULT GETTER\n  var get_val = function( pref, name ){\n    return pref._preferences[name].value;\n  }\n\n\n  var getterSetter_create = function( targ, id, type ){\n      switch( type ){\n      case 'color':\n        var tempVal = preferences.getColor( id, new ColorRGBA () );\n        value = new $.oColorValue( tempVal.r, tempVal.g, tempVal.b, tempVal.a );\n        break;\n      case 'int':\n        value = preferences.getInt( id, 0 );\n        break\n      case 'double':\n        value = preferences.getDouble( id, 0.0 );\n        break\n      case 'bool':\n        value = preferences.getBool( id, false );\n        break\n      case 'string':\n        value = preferences.getString( id, \"unknown\" );\n        if( value.slice( 0, 5 ) == \"json(\" ){\n          var obj = value.slice( 5, value.length-1 );\n          value = JSON.parse( obj );\n        }\n        break\n      default:\n        break;\n    }\n    if( value === null ) return;\n\n    targ._preferences[ id ] = { \"value\": value, \"type\":type };\n\n    //Create a getter/setter for it!\n    Object.defineProperty( targ, id, {\n      enumerable : true,\n      configurable: true,\n      set : eval( 'val = function(val){ set_val( targ, \"'+id+'\", val ); }' ),\n      get : eval( 'val = function(){ return get_val( targ, \"'+id+'\"); }' )\n    });\n  }\n\n\n  //Get all the children preferences.\n  var childNodes = XMLpreferences.childNodes();\n  for( var cn=0;cn<childNodes.length();cn++ ){\n    try{\n      var thisChild = childNodes.at( cn );\n      if (thisChild.isElement()){\n        var e = thisChild.toElement();\n        var type = e.tagName();\n        var id   = e.attribute( \"id\", \"null\" );\n\n        if( id == \"null\" ){ continue; }\n\n        var value = null;\n\n        getterSetter_create( this, id, type );\n      }\n    }catch( err ){\n      //Internal error, skip it.\n      System.println( err );\n    }\n  }\n\n  for( var n=0;n<this._addedPreferences.length;n++ ){\n    var pref = this._addedPreferences[n];\n    var id   = pref[\"name\"];\n    var type = pref[\"type\"];\n\n    getterSetter_create( this, id, type );\n  }\n\n}\n\n\n/**\n * Creates a new preferences based on name and value.<br><b>Note- A new preference isn't actively written into the Harmony's preference file until created and the application closed. Use preference.get for newly created preferences.</b>\n * @name $.oPreferences#create\n * @param   {string}                 name            The name of the new preference to create.\n * @param   {object}                 val             The value of the new preference created.\n */\n$.oPreferences.prototype.create = function( name, val ){\n  if( this[ name ] ){\n    throw ReferenceError( \"Preference already exists by name: \" + name );\n  }\n\n  var type = '';\n  //Check against types, unable to set types differently.\n  switch( typeof val ){\n    case 'string':\n      type = 'string';\n      preferences.setString( name, val );\n      break;\n    case 'number':\n      type = 'double';\n      preferences.setDouble( name, val );\n      break\n    case 'boolean':\n    case 'undefined':\n    case 'null':\n      type = 'bool';\n      preferences.setBool( name, val ? true:false );\n      break\n    case 'object':\n    default:\n      var set = false;\n      try{\n        if( val.r && val.g && val.b && val.a ){\n          type = 'color';\n          value = preferences.setColor( name, new ColorRGBA( val.r, val.g, val.b, val.a ) );\n          set = true;\n        }\n      }catch(err){\n      }\n\n      if(!set){\n        type = 'string';\n        var json_val = 'json('+JSON.stringify( val )+')';\n        preferences.setString( name, json_val );\n      }\n      break\n  }\n\n  this._addedPreferences.push( {\"type\":type, \"name\":name } );\n  this.refresh();\n}\n\n\n/**\n * Retrieves a preference and attempts to identify its type automatically.<br>This is generally useful for accessing newly created preferences that have not been written to disk.\n * @name $.oPreferences#get\n * @param   {string}                 name            The name of the preference to retrieve.\n * @example\n * var pref = $.getPreferences();\n * pref.create( \"MyNewPreferenceName\", \"MyPreferenceValue\" );\n * //This new preference won't be available in the file until Harmony closes.\n * //So if preferences are reinstantiated, it won't be readily available -- but it can still be retrieved with get.\n *\n * var pref2 = $.getPreferences();\n * pref[\"MyNewPreferenceName\"];     // Provides: undefined -- its not in the Harmony preference file.\n * pref.get(\"MyNewPreferenceName\"); // Provides: MyPreferenceValue, its still available\n */\n$.oPreferences.prototype.get = function( name ){\n  if( this[name] ){\n    return this[name];\n  }\n\n  var testTime   = (new Date()).getTime();\n  var doubleExist = preferences.getDouble( name, testTime );\n  if( doubleExist!= testTime ){\n    this._addedPreferences.push( {\"type\":'double', \"name\":name } );\n    this.refresh();\n\n    return doubleExist;\n  }\n\n  var intExist = preferences.getInt( name, testTime );\n  if( intExist!= testTime ){\n    this._addedPreferences.push( {\"type\":'int', \"name\":name } );\n    this.refresh();\n\n    return intExist;\n  }\n\n\n  var colorExist = preferences.getColor( name, new ColorRGBA(1,2,3,4) );\n  if( !( (colorExist.r==1) && (colorExist.g==2) && (colorExist.b==3) && (colorExist.a==4) ) ){\n    this._addedPreferences.push( {\"type\":'color', \"name\":name } );\n    this.refresh();\n\n    return colorExist;\n  }\n\n  var stringExist = preferences.getString( name, \"doesntExist\" );\n  if( stringExist != \"doesntExist\" ){\n    this._addedPreferences.push( {\"type\":'color', \"name\":name } );\n    this.refresh();\n\n    return this[name];\n  }\n\n  return preferences.getBool( name, false );\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//       $.oPreference class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * The constructor for the oPreference Class.\n * @classdesc\n * The oPreference class wraps a single preference item.\n * @constructor\n * @param {string} category             The category of the preference\n * @param {string} keyword              The keyword used by the preference\n * @param {string} type                 The type of value held by the preference\n * @param {string} description          A short string of description\n * @param {string} descriptionText      The complete tooltip text for the preference\n * @example\n * // To access the preferences of Harmony, grab the preference object in the $.oApp class:\n * var prefs = $.app.preferences;\n *\n * // It's then possible to access all available preferences of the software:\n * for (var i in prefs){\n *   log (i+\" \"+prefs[i]);\n * }\n *\n * // accessing the preference value can be done directly by using the dot notation:\n * prefs.USE_OVERLAY_UNDERLAY_ART = true;\n * log (prefs.USE_OVERLAY_UNDERLAY_ART);\n *\n * //the details objects of the preferences object allows access to more information about each preference\n * var details = prefs.details\n * log(details.USE_OVERLAY_UNDERLAY_ART.category+\" \"+details.USE_OVERLAY_UNDERLAY_ART.id+\" \"+details.USE_OVERLAY_UNDERLAY_ART.type);\n *\n * for (var i in details){\n *   log(i+\" \"+JSON.stringify(details[i]))       // each object inside detail is a complete oPreference instance\n * }\n *\n * // the preference object also holds a categories array with the list of all categories\n * log (prefs.categories)\n */\n$.oPreference = function(category, keyword, type, value, description, descriptionText){\n  this.category = category;\n  this.keyword = keyword;\n  this.type = type;\n  this.description = description;\n  this.descriptionText = descriptionText;\n  this.defaultValue = value;\n}\n\n\n/**\n * get and set a preference value\n * @name $.oPreference#value\n */\nObject.defineProperty ($.oPreference.prototype, 'value', {\n  get: function(){\n    try{\n      switch(this.type){\n        case \"bool\":\n          var _value = preferences.getBool(this.keyword, this.defaultValue);\n          break\n        case \"int\":\n          var _value = preferences.getInt(this.keyword, this.defaultValue);\n          break;\n        case \"double\":\n          var _value = preferences.getDouble(this.keyword, this.defaultValue);\n          break;\n        case \"color\":\n          var _value = preferences.getColor(this.keyword, this.defaultValue);\n          _value = new this.$.oColorValue(_value.r, _value.g, _value.b, _value.a)\n          break;\n        default:\n          var _value = preferences.getString(this.keyword, this.defaultValue);\n      }\n    }catch(err){\n      this.$.debug(err, this.$.DEBUG_LEVEL.ERROR)\n    }\n    this.$.debug(\"Getting value of Preference \"+this.keyword+\" : \"+_value, this.$.DEBUG_LEVEL.LOG)\n    return _value;\n  },\n\n  set : function(newValue){\n    switch(this.type){\n      case \"bool\":\n        preferences.setBool(this.keyword, newValue);\n        break\n      case \"int\":\n        preferences.setInt(this.keyword, newValue);\n        break;\n      case \"double\":\n        preferences.setDouble(this.keyword, newValue);\n        break;\n      case \"color\":\n        if (typeof newValue == String) newValue = (new oColorValue()).fromColorString(newValue);\n        preferences.setColor(this.keyword, new ColorRGBA(newValue.r, newValue.g, newValue.b, newValue.a));\n        break;\n      default:\n        preferences.setString(this.keyword, newValue);\n    }\n    this.$.debug(\"Preference \"+this.keyword+\" was set to : \"+newValue, this.$.DEBUG_LEVEL.LOG)\n  }\n})\n\n\n/**\n * Creates getter setters on a simple object for the preference described by the params\n * @private\n * @param {string} category             The category of the preference\n * @param {string} keyword              The keyword used by the preference\n * @param {string} type                 The type of value held by the preference\n * @param {string} description          A short string of description\n * @param {string} descriptionText      The complete tooltip text for the preference\n * @param {Object} prefObject           The preference object that will receive the getter setter property (usually $.oApp._prefObject)\n */\n$.oPreference.createPreference = function(category, keyword, type, value, description, descriptionText, prefObject){\n  if (!prefObject.details.hasOwnProperty(keyword)){\n    var pref = new $.oPreference(category, keyword, type, value, description, descriptionText);\n    Object.defineProperty(prefObject, keyword,{\n      enumerable: true,\n      get : function(){\n        return pref.value;\n      },\n      set : function(newValue){\n        pref.value = newValue;\n      }\n    })\n  }else{\n    var pref = prefObject.details[keyword]\n  }\n\n  return pref;\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_scene.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developped by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is garanteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oScene class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n//TODO: Metadata, settings, aspect, camera peg, view.\n/**\n * The constructor for $.oScene.\n * @classdesc\n * The base Class to access all the contents of the scene, and add elements. <br>This is the main class to do exporting operations as well as column/element/palette creation.\n * @constructor\n * @example\n * // Access to the direct dom object. Available and automatically instantiated as $.getScene, $.scene, $.scn, $.s\n * var doc = $.getScene ;\n * var doc = $.scn ;\n * ver doc = $.s ;         // all these are equivalents\n *\n * // To grab the scene from a QWidget Dialog callback, store the $ object in a local variable to access all the fonctions from the library.\n * function myCallBackFunction(){\n *   var this.$ = $;\n *\n *   var doc = this.$.scn;\n * }\n *\n *\n */\n$.oScene = function( ){\n    // $.oScene.nodes property is a class property shared by all instances, so it can be passed by reference and always contain all nodes in the scene\n\n    //var _topNode = new this.$.oNode(\"Top\");\n    //this.__proto__.nodes = _topNode.subNodes(true);\n\n  this._type = \"scene\";\n}\n\n\n//-------------------------------------------------------------------------------------\n//--- $.oScene Objects Properties\n//-------------------------------------------------------------------------------------\n//-------------------------------------------------------------------------------------\n\n\n/**\n * The folder that contains this scene.\n * @name $.oScene#path\n * @type {$.oFolder}\n * @readonly\n */\nObject.defineProperty($.oScene.prototype, 'path', {\n  get : function(){\n    return new this.$.oFolder( scene.currentProjectPathRemapped() );\n  }\n});\n\n/**\n * The stage file of the scene.\n * @name $.oScene#stage\n * @type {$.oFile}\n * @readonly\n */\nObject.defineProperty($.oScene.prototype, 'stage', {\n  get : function(){\n    if (this.online) return this.path + \"/stage/\" + this.name + \".stage\";\n    return this.path + \"/\" + this.version + \".xstage\";\n  }\n});\n\n/**\n * The folder that contains this scene.\n * @name $.oScene#paletteFolder\n * @type {$.oFolder}\n * @readonly\n */\nObject.defineProperty($.oScene.prototype, 'paletteFolder', {\n  get : function(){\n    return new this.$.oFolder( this.path+\"/palette-library\" );\n  }\n});\n\n\n/**\n * The temporary folder where files are created before being saved.\n * If the folder doesn't exist yet, it will be created.\n * @name $.oScene#tempFolder\n * @type {$.oFolder}\n * @readonly\n */\nObject.defineProperty($.oScene.prototype, 'tempFolder', {\n  get : function(){\n    if (!this.hasOwnProperty(\"_tempFolder\")){\n      this._tempFolder = new this.$.oFolder(scene.tempProjectPathRemapped());\n      if (!this._tempFolder.exists) this._tempFolder.create()\n    }\n    return this._tempFolder;\n  }\n});\n\n/**\n * The name of the scene.\n * @name $.oScene#name\n * @readonly\n * @type {string}\n */\nObject.defineProperty($.oScene.prototype, 'name', {\n  get : function(){\n    return scene.currentScene();\n  }\n});\n\n\n/**\n * Wether the scene is hosted on a Toonboom database.\n * @name $.oScene#online\n * @readonly\n * @type {bool}\n */\nObject.defineProperty($.oScene.prototype, 'online', {\n  get : function(){\n    return about.isDatabaseMode()\n  }\n});\n\n/**\n * The name of the scene.\n * @name $.oScene#environnement\n * @readonly\n * @type {string}\n */\nObject.defineProperty($.oScene.prototype, 'environnement', {\n  get : function(){\n    if (!this.online) return null;\n    return scene.currentEnvironment();\n  }\n});\n\n\n/**\n * The name of the scene.\n * @name $.oScene#job\n * @readonly\n * @type {string}\n */\nObject.defineProperty($.oScene.prototype, 'job', {\n  get : function(){\n    if (!this.online) return null;\n    return scene.currentJob();\n  }\n});\n\n\n/**\n * The name of the scene.\n * @name $.oScene#version\n * @readonly\n * @type {string}\n */\nObject.defineProperty($.oScene.prototype, 'version', {\n  get : function(){\n    return scene.currentVersionName();\n  }\n});\n\n\n/**\n * The sceneName file of the scene.\n * @Deprecated\n * @readonly\n * @name $.oScene#sceneName\n * @type {string}\n */\nObject.defineProperty($.oScene.prototype, 'sceneName', {\n  get : function(){\n    return this.name;\n  }\n});\n\n\n\n/**\n * The startframe to the playback of the scene.\n * @name $.oScene#startPreview\n * @type {int}\n */\nObject.defineProperty($.oScene.prototype, 'startPreview', {\n  get : function(){\n    return scene.getStartFrame();\n  },\n  set : function(val){\n    scene.setStartFrame( val );\n  }\n});\n\n/**\n * The stopFrame to the playback of the scene.\n * @name $.oScene#stopPreview\n * @type {int}\n */\nObject.defineProperty($.oScene.prototype, 'stopPreview', {\n  get : function(){\n    return scene.getStopFrame()+1;\n  },\n  set : function(val){\n    scene.setStopFrame( val-1 );\n  }\n});\n\n/**\n * The frame rate of the scene.\n * @name $.oScene#framerate\n * @type {float}\n */\nObject.defineProperty($.oScene.prototype, 'framerate', {\n  get : function(){\n    return scene.getFrameRate();\n  },\n  set : function(val){\n    return scene.setFrameRate( val );\n  }\n});\n\n\n/**\n * The Field unit aspect ratio as a coefficient (width/height).\n * @name $.oScene#unitsAspectRatio\n * @type {double}\n */\n Object.defineProperty($.oScene.prototype, 'unitsAspectRatio', {\n  get : function(){\n    return this.aspectRatioX/this.aspectRatioY;\n  }\n});\n\n\n/**\n * The horizontal aspect ratio of Field units.\n * @name $.oScene#aspectRatioX\n * @type {double}\n */\nObject.defineProperty($.oScene.prototype, 'aspectRatioX', {\n  get : function(){\n    return scene.unitsAspectRatioX();\n  },\n  set : function(val){\n    scene.setUnitsAspectRatio( val, this.aspectRatioY );\n  }\n});\n\n/**\n * The vertical aspect ratio of Field units.\n * @name $.oScene#aspectRatioY\n * @type {double}\n */\nObject.defineProperty($.oScene.prototype, 'aspectRatioY', {\n    get : function(){\n        return scene.unitsAspectRatioY();\n    },\n    set : function(val){\n        scene.setUnitsAspectRatio( this.aspectRatioY, val );\n    }\n});\n\n/**\n * The horizontal Field unit count.\n * @name $.oScene#unitsX\n * @type {double}\n */\nObject.defineProperty($.oScene.prototype, 'unitsX', {\n    get : function(){\n        return scene.numberOfUnitsX();\n    },\n    set : function(val){\n        scene.setNumberOfUnits( val, this.unitsY, this.unitsZ );\n    }\n});\n\n/**\n * The vertical Field unit count.\n * @name $.oScene#unitsY\n * @type {double}\n */\nObject.defineProperty($.oScene.prototype, 'unitsY', {\n    get : function(){\n        return scene.numberOfUnitsY();\n    },\n    set : function(val){\n        scene.setNumberOfUnits( this.unitsX, val, this.unitsZ );\n    }\n});\n\n/**\n * The depth Field unit count.\n * @name $.oScene#unitsZ\n * @type {double}\n */\nObject.defineProperty($.oScene.prototype, 'unitsZ', {\n    get : function(){\n        return scene.numberOfUnitsZ();\n    },\n    set : function(val){\n        scene.setNumberOfUnits( this.unitsX, this.unitsY, val );\n    }\n});\n\n\n/**\n * The center coordinates of the scene.\n * @name $.oScene#center\n * @type {$.oPoint}\n */\nObject.defineProperty($.oScene.prototype, 'center', {\n    get : function(){\n        return new this.$.oPoint( scene.coordAtCenterX(), scene.coordAtCenterY(), 0.0 );\n    },\n    set : function( val ){\n        scene.setCoordAtCenter( val.x, val.y );\n    }\n});\n\n\n/**\n * The amount of drawing units represented by 1 field on the horizontal axis.\n * @name $.oScene#fieldVectorResolutionX\n * @type {double}\n * @readonly\n */\nObject.defineProperty($.oScene.prototype, 'fieldVectorResolutionX', {\n  get : function(){\n    var yUnit = this.fieldVectorResolutionY;\n    var unit = yUnit * this.unitsAspectRatio;\n    return unit\n  }\n});\n\n\n/**\n * The amount of drawing units represented by 1 field on the vertical axis.\n * @name $.oScene#fieldVectorResolutionY\n * @type {double}\n * @readonly\n */\nObject.defineProperty($.oScene.prototype, 'fieldVectorResolutionY', {\n  get : function(){\n    var verticalResolution = 1875 // the amount of drawing units for the max vertical field value\n    var unit = verticalResolution/12; // the vertical number of units on drawings is always 12 regardless of $.scn.unitsY\n    return unit\n  }\n});\n\n\n/**\n * The horizontal resolution in pixels (for rendering).\n * @name $.oScene#resolutionX\n * @readonly\n * @type {int}\n */\nObject.defineProperty($.oScene.prototype, 'resolutionX', {\n    get : function(){\n        return scene.currentResolutionX();\n    }\n});\n\n/**\n * The vertical resolution in pixels (for rendering).\n * @name $.oScene#resolutionY\n * @type {int}\n */\nObject.defineProperty($.oScene.prototype, 'resolutionY', {\n    get : function(){\n        return scene.currentResolutionY();\n    }\n});\n\n/**\n * The default horizontal resolution in pixels.\n * @name $.oScene#defaultResolutionX\n * @type {int}\n */\nObject.defineProperty($.oScene.prototype, 'defaultResolutionX', {\n    get : function(){\n        return scene.defaultResolutionX();\n    },\n    set : function(val){\n        scene.setDefaultResolution( val, this.defaultResolutionY, this.fov );\n    }\n});\n\n/**\n * The default vertical resolution in pixels.\n * @name $.oScene#defaultResolutionY\n * @type {int}\n */\nObject.defineProperty($.oScene.prototype, 'defaultResolutionY', {\n    get : function(){\n        return scene.defaultResolutionY();\n    },\n    set : function(val){\n        scene.setDefaultResolution( this.defaultResolutionX, val, this.fov );\n    }\n});\n\n/**\n * The field of view of the scene.\n * @name $.oScene#fov\n * @type {double}\n */\nObject.defineProperty($.oScene.prototype, 'fov', {\n    get : function(){\n        return scene.defaultResolutionFOV();\n    },\n    set : function(val){\n        scene.setDefaultResolution( this.defaultResolutionX, this.defaultResolutionY, val );\n    }\n});\n\n\n/**\n * The default Display of the scene.\n * @name $.oScene#defaultDisplay\n * @type {oNode}\n */\nObject.defineProperty($.oScene.prototype, 'defaultDisplay', {\n  get : function(){\n    return this.getNodeByPath(scene.getDefaultDisplay());\n  },\n\n  set : function(newDisplay){\n    node.setAsGlobalDisplay(newDisplay.path);\n  }\n});\n\n\n/**\n * Whether the scene contains unsaved changes.\n * @name $.oScene#unsaved\n * @readonly\n * @type {bool}\n */\nObject.defineProperty($.oScene.prototype, 'unsaved', {\n    get : function(){\n        return scene.isDirty();\n    }\n});\n\n\n/**\n * The root group of the scene.\n * @name $.oScene#root\n * @type {$.oGroupNode}\n * @readonly\n */\nObject.defineProperty($.oScene.prototype, 'root', {\n    get : function(){\n        var _topNode = this.getNodeByPath( \"Top\" );\n        return _topNode\n    }\n});\n\n\n/**\n * Contains the list of all the nodes present in the scene.\n * @name $.oScene#nodes\n * @readonly\n * @type {$.oNode[]}\n */\nObject.defineProperty($.oScene.prototype, 'nodes', {\n    get : function(){\n        var _topNode = this.root;\n        return _topNode.subNodes( true );\n    }\n});\n\n\n/**\n * Contains the list of columns present in the scene.\n * @name $.oScene#columns\n * @readonly\n * @type {$.oColumn[]}\n * @todo add attribute finding to get complete column objects\n */\nObject.defineProperty($.oScene.prototype, 'columns', {\n    get : function(){\n        var _columns = [];\n        for (var i=0; i<column.numberOf(); i++){\n            _columns.push( this.$column(column.getName(i)) );\n        }\n        return _columns;\n    }\n});\n\n\n/**\n * Contains the list of scene palettes present in the scene.\n * @name $.oScene#palettes\n * @readonly\n * @type {$.oPalette[]}\n */\nObject.defineProperty($.oScene.prototype, 'palettes', {\n  get : function(){\n    var _paletteList = PaletteObjectManager.getScenePaletteList();\n\n    var _palettes = [];\n    for (var i=0; i<_paletteList.numPalettes; i++){\n        _palettes.push( new this.$.oPalette( _paletteList.getPaletteByIndex(i), _paletteList ) );\n    }\n    return _palettes;\n  }\n});\n\n\n/**\n * Contains the list of elements present in the scene. Element ids can appear more than once if they are used by more than one Drawing column\n * @name $.oScene#elements\n * @readonly\n * @type {$.oElement[]}\n */\nObject.defineProperty($.oScene.prototype, 'elements', {\n  get : function(){\n    var _elements = [];\n    var _columns = this.columns;\n    var _ids = [];\n    for (var i in _columns){\n      if (_columns.type != \"DRAWING\") continue;\n      var _element = _columns[i].element\n      _elements.push(_element);\n      if (_ids.indexOf(_element.id) == -1) _ids.push (_element.id);\n    }\n\n    // adding the elements not linked to columns\n    var _elementNum = element.numberOf();\n    for (var i = 0; i<_elementNum; i++){\n      var _id = element.id(i);\n      if (_ids.indexOf(_id) == -1) {\n        _elements.push(new this.$.oElement(_id));\n        _ids.push (_id)\n      }\n    }\n    return _elements;\n  }\n});\n\n\n\n/**\n * The length of the scene.\n * @name $.oScene#length\n * @type {int}\n */\nObject.defineProperty($.oScene.prototype, 'length', {\n    get : function(){\n        return frame.numberOf()\n    },\n\n    set : function (newLength){\n        var _length = frame.numberOf();\n        var _toAdd = newLength-_length;\n        if (_toAdd>0){\n            frame.insert(_length-1, _toAdd)\n        }else{\n            frame.remove(_length-1, _toAdd)\n        }\n    }\n});\n\n\n/**\n * The current frame of the scene.\n * @name $.oScene#currentFrame\n * @type {int}\n */\nObject.defineProperty($.oScene.prototype, 'currentFrame', {\n    get : function(){\n        return frame.current();\n    },\n\n    set : function( frm ){\n        return frame.setCurrent( frm );\n    }\n});\n\n\n/**\n * Retrieve and change the selection of nodes.\n * @name $.oScene#selectedNodes\n * @type {$.oNode[]}\n */\nObject.defineProperty($.oScene.prototype, 'selectedNodes', {\n  get : function(){\n    return this.getSelectedNodes();\n  },\n\n  set : function(nodesToSelect){\n    selection.clearSelection ();\n    for (var i in nodesToSelect){\n      selection.addNodeToSelection(nodesToSelect[i].path);\n    };\n  }\n});\n\n\n/**\n * Retrieve and change the selected frames. This is an array with two values, one for the start and one for the end of the selection (not included).\n * @name $.oScene#selectedFrames\n * @type {int[]}\n */\nObject.defineProperty($.oScene.prototype, 'selectedFrames', {\n  get : function(){\n    if (selection.isSelectionRange()){\n      var _selectedFrames = [selection.startFrame(), selection.startFrame()+selection.numberOfFrames()];\n    }else{\n      var _selectedFrames = [this.currentFrame, this.currentFrame+1];\n    }\n\n    return _selectedFrames;\n  },\n\n  set : function(frameRange){\n    selection.setSelectionFrameRange(frameRange[0], frameRange[1]-frameRange[0]);\n  }\n});\n\n\n/**\n * Retrieve and set the selected palette from the scene palette list.\n * @name $.oScene#selectedPalette\n * @type {$.oPalette}\n */\nObject.defineProperty($.oScene.prototype, \"selectedPalette\", {\n  get: function(){\n    var _paletteList = PaletteObjectManager.getScenePaletteList()\n    var _id = PaletteManager.getCurrentPaletteId()\n    if (_id == \"\") return null;\n    var _palette = new this.$.oPalette(_paletteList.getPaletteById(_id), _paletteList);\n    return _palette;\n  },\n\n  set: function(newSelection){\n    var _id = newSelection.id;\n    PaletteManager.setCurrentPaletteById(_id);\n  }\n})\n\n\n/**\n * The selected strokes on the currently active Drawing\n * @name $.oScene#selectedShapes\n * @type {$.oStroke[]}\n */\nObject.defineProperty($.oScene.prototype, \"selectedShapes\", {\n  get : function(){\n    var _currentDrawing = this.activeDrawing;\n    var _shapes = _currentDrawing.selectedShapes;\n\n    return _shapes;\n  }\n})\n\n\n/**\n * The selected strokes on the currently active Drawing\n * @name $.oScene#selectedStrokes\n * @type {$.oStroke[]}\n */\nObject.defineProperty($.oScene.prototype, \"selectedStrokes\", {\n  get : function(){\n    var _currentDrawing = this.activeDrawing;\n    var _strokes = _currentDrawing.selectedStrokes;\n\n    return _strokes;\n  }\n})\n\n\n/**\n * The selected strokes on the currently active Drawing\n * @name $.oScene#selectedContours\n * @type {$.oContour[]}\n */\nObject.defineProperty($.oScene.prototype, \"selectedContours\", {\n  get : function(){\n    var _currentDrawing = this.activeDrawing;\n    var _strokes = _currentDrawing.selectedContours;\n\n    return _strokes;\n  }\n})\n\n\n/**\n * The currently active drawing in the harmony UI.\n * @name $.oScene#activeDrawing\n * @type {$.oDrawing}\n */\nObject.defineProperty($.oScene.prototype, 'activeDrawing', {\n  get : function(){\n    var _curDrawing = Tools.getToolSettings().currentDrawing;\n    if (!_curDrawing) return null;\n\n    var _element = this.selectedNodes[0].element;\n    var _drawings = _element.drawings;\n    for (var i in _drawings){\n      if (_drawings[i].id == _curDrawing.drawingId) return _drawings[i];\n    }\n\n    return null\n  },\n\n  set : function( newCurrentDrawing ){\n    newCurrentDrawing.setAsActiveDrawing();\n  }\n});\n\n\n/**\n * The current timeline using the default Display.\n * @name $.oScene#currentTimeline\n * @type {$.oTimeline}\n * @readonly\n */\nObject.defineProperty($.oScene.prototype, 'currentTimeline', {\n  get : function(){\n    if (!this.hasOwnProperty(\"_timeline\")){\n      this._timeline = this.getTimeline();\n    }\n    return this._timeline;\n  }\n});\n\n\n\n\n//-------------------------------------------------------------------------------------\n//--- $.oScene Objects Methods\n//-------------------------------------------------------------------------------------\n//-------------------------------------------------------------------------------------\n\n\n/**\n * Gets a node by the path.\n * @param   {string}   fullPath         The path of the node in question.\n *\n * @return {$.oNode}                    The node found given the query.\n */\n$.oScene.prototype.getNodeByPath = function(fullPath){\n    var _type = node.type(fullPath);\n    if (_type == \"\") return null;\n\n    var _node;\n    switch(_type){\n      case \"READ\" :\n        _node = new this.$.oDrawingNode( fullPath, this );\n        break;\n      case \"PEG\" :\n        _node = new this.$.oPegNode( fullPath, this );\n        break;\n      case \"COLOR_OVERRIDE_TVG\" :\n        _node = new this.$.oColorOverrideNode( fullPath, this );\n        break;\n      case \"TransformationSwitch\" :\n        _node = new this.$.oTransformSwitchNode( fullPath, this );\n        break;\n      case \"GROUP\" :\n        _node = new this.$.oGroupNode( fullPath, this );\n        break;\n      default:\n        _node = new this.$.oNode( fullPath, this );\n    }\n\n    return _node;\n}\n\n /**\n * Returns the nodes of a certain type in the entire scene.\n * @param   {string}      typeName       The name of the node.\n *\n * @return  {$.oNode[]}     The nodes found.\n */\n$.oScene.prototype.getNodesByType = function(typeName){\n  return this.root.getNodesByType(typeName, true);\n}\n\n/**\n * Gets a column by the name.\n * @param  {string}             uniqueName               The unique name of the column as a string.\n * @param  {$.oAttribute}       oAttributeObject         The oAttributeObject owning the column.\n * @todo   cache and find attribute if it is missing\n *\n * @return {$.oColumn}                    The column found given the query.\n */\n$.oScene.prototype.getColumnByName = function( uniqueName, oAttributeObject ){\n    var _type = column.type(uniqueName);\n\n    switch (_type) {\n        case \"\" :\n            return null;\n        case \"DRAWING\" :\n            return new this.$.oDrawingColumn(uniqueName, oAttributeObject);\n        default :\n            return new this.$.oColumn(uniqueName, oAttributeObject);\n    }\n}\n\n\n/**\n * Gets an element by Id.\n * @param  {string}        id                         The unique name of the column as a string.\n * @param  {$.oColumn}     [oColumnObject]            The oColumn object linked to the element in case of duplicate.\n *\n * @return {$.oElement}                               The element found given the query. In case of an element linked to several column, only the first one will be returned, unless the column is specified\n */\n$.oScene.prototype.getElementById = function( id, oColumnObject ){\n  if (element.getNameById(id) == \"\") return null;\n\n  var _sceneElements = this.elements.filter(function(x){return x.id == id});\n  if (typeof oColumnObject !== 'undefined') _sceneElements = _sceneElements.filter(function(x){return x.column.uniqueName == oColumnObject.uniqueName});\n\n  if (_sceneElements.length > 0) return _sceneElements[0];\n  return null;\n}\n\n\n/**\n * Gets the selected Nodes.\n * @param  {bool}   recurse            Whether to recurse into groups.\n *\n * @return {$.oNode[]}                 The selected nodes.\n */\n$.oScene.prototype.getSelectedNodes = function( recurse, sortResult ){\n    if (typeof recurse === 'undefined') var recurse = false;\n    if (typeof sort_result === 'undefined') var sortResult = false;     //Avoid sorting, save time, if unnecessary and used internally.\n\n    var _selection = selection.selectedNodes();\n\n    var _selectedNodes = [];\n    for (var i = 0; i<_selection.length; i++){\n\n        var _oNodeObject = this.$node(_selection[i])\n\n        _selectedNodes.push(_oNodeObject)\n        if (recurse && node.isGroup(_selection[i])){\n            _selectedNodes = _selectedNodes.concat(_oNodeObject.subNodes(recurse))\n        }\n    }\n\n    // sorting by timeline index\n    if( sortResult ){\n      var _timeline = this.getTimeline();\n      _selectedNodes = _selectedNodes.sort(function(a, b){return a.timelineIndex(_timeline)-b.timelineIndex(_timeline)})\n    }\n\n    return _selectedNodes;\n}\n\n\n/**\n * Searches for a node based on the query.\n * @param   {string}   query            The query for finding the node[s].\n *\n * @return {$.oNode[]}                 The node[s] found given the query.\n */\n$.oScene.prototype.nodeSearch = function( query, sort_result ){\n  if (typeof sort_result    === 'undefined') var sort_result = true;     //Avoid sorting, save time, if unnecessary and used internally.\n\n  //-----------------------------------\n  //Breakdown with regexp as needed, find the query details.\n  //-----------------------------------\n\n  // NAME, NODE, WILDCARDS, ATTRIBUTE VALUE MATCHING, SELECTION/OPTIONS, COLOURS\n\n  //----------------------------------------------\n  // -- PATH/WILDCARD#TYPE[ATTRIBUTE:VALUE,ATTRIBUTE:VALUE][OPTION:VALUE,OPTION:VALUE]\n  // ^(.*?)(\\#.*?)?(\\[.*\\])?(\\(.*\\))?$\n\n  //ALLOW USAGE OF AN INPUT LIST, LIST OF NAMES, OR OBJECTS,\n\n  //--------------------------------------------------\n  //-- EASY RETURNS FOR FAST OVERLOADS.\n\n  //* -- OVERRIDE FOR ALL NODES\n\n  if( query == \"*\" ){\n    return this.nodes;\n\n  //(SELECTED) SELECTED -- OVERRIDE FOR ALL SELECTED NODES\n  }else if( query == \"(SELECTED)\" || query == \"SELECTED\" ){\n\n    return this.getSelectedNodes( true, sort_result );\n\n  //(NOT SELECTED) !SELECTED NOT SELECTED -- OVERRIDE FOR ALL SELECTED NODES\n\n  }else if( query == \"(NOT SELECTED)\" ||\n            query == \"NOT SELECTED\"   ||\n            query == \"(! SELECTED)\"   ||\n            query == \"! SELECTED\"     ||\n            query == \"(UNSELECTED)\"   ||\n            query == \"UNSELECTED\"\n          ){\n\n    var nodes_returned = [];\n\n    var sel_list = {};\n    for( var p=0;p<selection.numberOfNodesSelected();p++ ){\n      sel_list[ selection.selectedNode(p) ] = true;\n    }\n\n    var all_nodes = this.nodes;\n    for( var x=0;x<all_nodes.length;x++ ){\n      if( !sel_list[ all_nodes[x].path ] ){\n        var node_ret = this.getNodeByPath( all_nodes[x].path );\n        if( node_ret && node_ret.exists ){\n          nodes_returned.push( node_ret );\n        }\n      }\n    }\n\n    if( sort_result ){\n      var _timeline = this.getTimeline();\n      nodes_returned = nodes_returned.sort(function(a, b){return a.timelineIndex(_timeline)-b.timelineIndex(_timeline)})\n    }\n    return nodes_returned;\n  }\n\n\n  //FULL QUERIES.\n  var regexp = /^(.*?)(\\#.*?)?(\\[.*\\])?(\\(.*\\))?$/;\n  var query_match = query.match( regexp );\n\n  this.$.debug( \"QUERYING: \" + query, this.$.DEBUG_LEVEL.LOG );\n  this.$.debug( \"QUERYING: \" + query_match.length, this.$.DEBUG_LEVEL.LOG );\n\n  var nodes_returned = [];\n\n  if( query_match && query_match.length > 1 && query_match[1] && query_match[1].length > 0 ){\n    //CONSIDER A LIST, COMMA SEPARATION, AND ESCAPED COMMAS.\n    var query_list = [];\n    var last_str   = '';\n    var split_list = query_match[1].split( \",\" );\n\n    for( var n=0; n<split_list.length; n++ ){\n      var split_val = split_list[n];\n      if( split_val.slice( split_val.length-1, split_val.length ) == \"\\\\\" ){\n        last_str += split_val + \",\";\n\n      }else{\n        query_list.push( last_str + split_val );\n        last_str = \"\";\n      }\n    }\n\n    if( last_str.length>0 ){\n      query_list.push( last_str );\n    }\n\n    this.$.debug( \"GETTING NODE LIST FROM QUERY\", this.$.DEBUG_LEVEL.LOG );\n    //NOW DEAL WITH WILDCARDS\n\n    var added_nodes = {}; //Add the full path to a list when adding/querying existing. Prevent duplicate attempts.\n    var all_nodes = false;\n    for( var x=0; x<query_list.length; x++ ){\n      if( (query_list[x].indexOf(\"*\")>=0) || (query_list[x].indexOf(\"?\")>=0) ){\n        //THERE ARE WILDCARDS.\n        this.$.debug( \"WILDCARD NODE QUERY: \"+query_list[x], this.$.DEBUG_LEVEL.LOG );\n        //Make a wildcard search for the nodes.\n\n        if( all_nodes === false ){\n          all_nodes = this.nodes;\n        }\n\n        //Run the Wildcard regexp against the available nodes.\n        var regexp = query_list[x];\n            regexp = regexp.split( \"?\" ).join( \".\" );\n            regexp = regexp.split( \"*\" ).join( \".*?\" );\n            regexp = '^'+regexp+'$';\n\n        this.$.debug( \"WILDCARD QUERY REGEXP: \"+regexp, this.$.DEBUG_LEVEL.LOG );\n\n        var regexp_filter = RegExp( regexp, 'gi' );\n        for( var n=0;n<all_nodes.length;n++ ){\n          if( !added_nodes[all_nodes[n].path] ){\n            this.$.debug( \"WILDCARD NODE TEST: \"+all_nodes[n].path, this.$.DEBUG_LEVEL.LOG );\n            if( regexp_filter.test( all_nodes[n].path ) ){\n              this.$.debug( \"WILDCARD NODE TESTED SUCCESS: \"+all_nodes[n].path, this.$.DEBUG_LEVEL.LOG );\n\n              var node_ret = all_nodes[n]; //this.getNodeByPath( all_nodes[n].path ); //new this.$.oNode( this.$, all_nodes[n].path );\n              if( node_ret && node_ret.exists ){\n                this.$.debug( \"WILDCARD NODE MATCH: \"+all_nodes[n].path+\"\\n\", this.$.DEBUG_LEVEL.LOG );\n                nodes_returned.push( node_ret );\n              }\n              added_nodes[ all_nodes[n].path ] = true;\n            }\n          }\n        }\n\n      }else if( query_list[x].length >=3 && query_list[x]==\"re:\" ){\n        //THERE ARE WILDCARDS.\n        this.$.debug( \"REGEXP NODE QUERY: \"+query_list[x], this.$.DEBUG_LEVEL.LOG );\n        //Make a wildcard search for the nodes.\n\n        if( all_nodes === false ){\n          all_nodes = this.nodes;\n        }\n\n        //Run the Wildcard regexp against the available nodes.\n        var regexp = query_list[x];\n        this.$.debug( \"REGEXP QUERY REGEXP: \"+regexp, this.$.DEBUG_LEVEL.LOG );\n\n        var regexp_filter = RegExp( regexp, 'gi' );\n        for( var n=0;n<all_nodes.length;n++ ){\n          if( !added_nodes[all_nodes[n].path] ){\n            this.$.debug( \"REGEXP NODE TEST: \"+all_nodes[n].path, this.$.DEBUG_LEVEL.LOG );\n            if( regexp_filter.test( all_nodes[n].path ) ){\n              this.$.debug( \"REGEXP NODE TESTED SUCCESS: \"+all_nodes[n].path, this.$.DEBUG_LEVEL.LOG );\n\n              var node_ret = all_nodes[n]; //new this.$.oNode( this.$, all_nodes[n].path );\n              if( node_ret && node_ret.exists ){\n                this.$.debug( \"REGEXP NODE MATCH: \"+all_nodes[n].path+\"\\n\", this.$.DEBUG_LEVEL.LOG );\n                nodes_returned.push( node_ret );\n              }\n              added_nodes[ all_nodes[n].path ] = true;\n            }\n          }\n        }\n      }else{\n        //ITS JUST THE EXACT NODE.\n        this.$.debug( \"EXACT NODE QUERY: \"+query_list[x], this.$.DEBUG_LEVEL.LOG );\n\n        var node_ret = this.getNodeByPath( query_list[x] ); //new this.$.oNode( this.$, query_list[x] );\n        if( !added_nodes[ query_list[x] ] ){\n          if( node_ret && node_ret.exists ){\n            this.$.debug( \"EXACT NODE MATCH: \"+query_list[x]+\"\\n\", this.$.DEBUG_LEVEL.LOG );\n            nodes_returned.push( node_ret );\n          }\n          added_nodes[ query_list[x] ] = true;\n        }\n      }\n    }\n  }else{\n    nodes_returned = this.nodes;\n  }\n\n  this.$.debug( \"FILTER CODE\", this.$.DEBUG_LEVEL.LOG );\n\n  //-----------------------------------------------------\n  //IT HAS SOME SORT OF FILTER ASSOCIATED WITH THE QUERY.\n  if( query_match.length > 2 ){\n    var filtered_nodes = nodes_returned;\n    for( var n=2;n<query_match.length;n++ ){\n      //RUN THE FITERS.\n\n      if( !query_match[n] ){\n        continue;\n      }\n\n      if( query_match[n].slice(0, 1) == \"#\" ){         //TYPE\n        this.$.debug( \"TYPE FILTER INIT: \" + query_match[n], this.$.DEBUG_LEVEL.LOG );\n\n        var res_nodes = [];\n        var match_val = query_match[n].slice(1,query_match[n].length).toUpperCase();\n        for( var x=0;x<filtered_nodes.length;x++ ){\n          if( filtered_nodes[x].type == match_val ){\n            res_nodes.push( filtered_nodes[x] );\n          }\n        }\n\n        filtered_nodes = res_nodes;\n      }else if( query_match[n].slice(0, 1) == \"[\" ){   //ATTRIBUTES\n        var split_attrs = query_match[n].toUpperCase().slice( 1, query_match[n].length-1 ).split(\",\");\n        for( var t=0;t<split_attrs.length;t++ ){\n          var res_nodes = [];\n\n          var split_attrs = split_attrs[t].split( \":\" );\n          if( split_attrs.length==1 ){\n            //It simply just must have this attribute.\n            //res_nodes.push( filtered_nodes[0] );\n\n          }else{\n            //You must compare values of the attribute -- currently only supports string, but should also use float/int comparisons and logic.\n\n\n          }\n\n          //filtered_nodes = res_nodes;\n        }\n      }else if( query_match[n].slice(0, 1) == \"(\" ){   //OPTIONS\n        //SELECTED, ECT.\n\n        var split_options = query_match[n].toUpperCase().slice( 1, query_match[n].length-1 ).split(\",\");\n\n        for( var t=0;t<split_options.length;t++ ){\n          var res_nodes = [];\n\n          //THE OPTION FOR SELECTED NODES ONLY, COMPARE IT AGAINST THE LIST.\n          if( split_options[t] == \"SELECTED\" ){\n            this.$.debug( \"TYPE FILTER SELECTION ONLY\", this.$.DEBUG_LEVEL.LOG );\n\n            //GET THE SELECTION LIST.\n            var sel_list = {};\n            for( var p=0;p<selection.numberOfNodesSelected();p++ ){\n              sel_list[ selection.selectedNode(p) ] = true;\n              this.$.debug( selection.selectedNode(p), this.$.DEBUG_LEVEL.LOG );\n            }\n\n            for( var x=0;x<filtered_nodes.length;x++ ){\n              if( sel_list[ filtered_nodes[x].path ] ){\n                res_nodes.push( filtered_nodes[x] );\n              }\n            }\n          }\n\n          //--- NOTSELECTED DESELECTED !SELECTED  NOT SELECTED\n          //THE OPTION FOR SELECTED NODES ONLY, COMPARE IT AGAINST THE LIST.\n          else if( split_options[t] == \"NOT SELECTED\" || split_options[t] == \"NOTSELECTED\" || split_options[t] == \"DESELECTED\" || split_options[t] == \"!SELECTED\" ){\n            this.$.debug( \"TYPE FILTER SELECTION ONLY\", this.$.DEBUG_LEVEL.LOG );\n\n            //GET THE SELECTION LIST.\n            var sel_list = {};\n            for( var p=0;p<selection.numberOfNodesSelected();p++ ){\n              sel_list[ selection.selectedNode(p) ] = true;\n            }\n\n            for( var x=0;x<filtered_nodes.length;x++ ){\n              if( !sel_list[ filtered_nodes[x].path ] ){\n                res_nodes.push( filtered_nodes[x] );\n              }\n            }\n          }\n\n          filtered_nodes = res_nodes;\n        }\n      }\n    }\n\n    nodes_returned = filtered_nodes;\n  }\n\n  if( sort_result ){\n    var _timeline = this.getTimeline();\n    nodes_returned = nodes_returned.sort(function(a, b){return a.timelineIndex(_timeline)-b.timelineIndex(_timeline)})\n  }\n  return nodes_returned;\n}\n\n\n/**\n * Adds a node to the scene.\n * @Deprecated         use AddNode directly in the destination group by calling it on the oGroupNode\n * @param   {string}   type            The type-name of the node to add.\n * @param   {string}   name            The name of the newly created node.\n * @param   {string}   group           The groupname to add the node.\n * @param   {$.oPoint} nodePosition    The position for the node to be placed in the network.\n *\n * @return {$.oNode}   The created node\n */\n$.oScene.prototype.addNode = function( type, name, group, nodePosition ){\n  var _group = (group instanceof this.$.oGroupNode)?group:this.$node(group);\n\n  if (_group != null && _group instanceof this.$.oGroupNode){\n    this.$.log(\"oScene.addNode is deprecated. Use oGroupNode.addNode instead\")\n    var _node = _group.addNode(type, name, nodePosition)\n    return _node;\n  }else{\n    if (group == undefined) throw new Error (\"Group path not specified for adding node. Use oGroupNode.addNode() instead.\")\n    throw new Error (group+\" is an invalid group to add the Node to.\")\n  }\n}\n\n\n/**\n * Adds a column to the scene.\n * @param   {string}   type                           The type of the column.\n * @param   {string}   name                           The name of the column.\n * @param   {$.oElement}   oElementObject             For Drawing column, the element that will be represented by the column.\n *\n * @return {$.oColumn}  The created column\n */\n\n$.oScene.prototype.addColumn = function( type, name, oElementObject ){\n    // Defaults for optional parameters\n    if (!type) throw new Error (\"Must provide a type when creating a new column.\");\n\n    if (typeof name === 'undefined'){\n      if( column.generateAnonymousName ){\n        var name = column.generateAnonymousName();\n      }else{\n        var name = \"ATV\"+(new Date()).getTime();\n      }\n    }\n\n    var _increment = 1;\n    var _columnName = name;\n\n    // increment name if a column with that name already exists\n    while (column.type(_columnName) != \"\"){\n        _columnName = name+\"_\"+_increment;\n        _increment++;\n    }\n\n    this.$.debug( \"CREATING THE COLUMN: \" + name, this.$.DEBUG_LEVEL.LOG );\n    column.add(_columnName, type);\n\n    if (column.type(_columnName)!= type) throw new Error (\"Couldn't create column with name '\"+name+\"' and type \"+type)\n\n    var _column = new this.$.oColumn( _columnName );\n\n    if (type == \"DRAWING\" && typeof oElementObject !== 'undefined'){\n        oElementObject.column = this;// TODO: fix: this doesn't seem to actually work for some reason?\n        this.$.debug( \"set element \"+oElementObject.id+\" to column \"+_column.uniqueName, this.$.DEBUG_LEVEL.LOG );\n        column.setElementIdOfDrawing(_column.uniqueName, oElementObject.id);\n    }\n\n    return _column;\n}\n\n\n/**\n * Adds an element to the scene.\n * @param   {string}     name                    The name of the element\n * @param   {string}     [imageFormat=\"TVG\"]     The image format in capital letters (ex: \"TVG\", \"PNG\"...)\n * @param   {int}        [fieldGuide=12]         The field guide .\n * @param   {string}     [scanType=\"COLOR\"]      can have the values \"COLOR\", \"GRAY_SCALE\" or \"BW\".\n *\n * @return {$.oElement}  The created element\n */\n$.oScene.prototype.addElement = function(name, imageFormat, fieldGuide, scanType){\n    // Defaults for optional parameters\n    if (typeof scanType === 'undefined') var scanType = \"COLOR\";\n    if (typeof fieldGuide === 'undefined') var fieldGuide = 12;\n    if (typeof imageFormat === 'undefined') var imageFormat = \"TVG\";\n\n    var _fileFormat = (imageFormat == \"TVG\")?\"SCAN\":imageFormat;\n    var _vectorFormat = (imageFormat == \"TVG\")?imageFormat:\"None\";\n\n    // sanitize input to graciously handle forbidden characters\n    name = name.replace(/[^A-Za-z\\d_\\-]/g, \"_\").replace(/ /g, \"_\");\n\n    var _id = element.add(name, scanType, fieldGuide, _fileFormat, _vectorFormat);\n    if (_id <0) throw new Error(\"Couldn't create an element with settings {name:'\"+name+\"', imageFormat:\"+ imageFormat+\", fieldGuide:\"+fieldGuide+\", scanType:\"+scanType+\"}\")\n\n    var _element = new this.$.oElement( _id )\n\n    return _element;\n}\n\n\n/**\n * Adds a drawing layer to the scene, with a drawing column and element linked. Possible to specify the column and element to use.\n * @Deprecated Use oGroupNode.addDrawingNode instead\n * @param   {string}     name            The name of the newly created node.\n * @param   {string}     group           The group in which the node is added.\n * @param   {$.oPoint}   nodePosition    The position for the node to be placed in the network.\n * @param   {$.object}   element         The element to attach to the column.\n * @param   {object}     drawingColumn   The column to attach to the drawing module.\n * @param   {object}     options         The creation options, nothing available at this point.\n\n * @return {$.oDrawingNode}     The created node.\n */\n$.oScene.prototype.addDrawingNode = function( name, group, nodePosition, oElementObject, drawingColumn, options ){\n  var _group = (group instanceof this.$.oGroupNode)?group:this.$node(group);\n\n  if (_group != null && _group instanceof this.$.oGroupNode){\n    this.$.log(\"oScene.addDrawingNode is deprecated. Use oGroupNode.addDrawingNode instead\")\n    var _node = _group.addNode(name, nodePosition, oElementObject, drawingColumn, options )\n    return _node;\n  }else{\n    throw new Error (group+\" is an invalid group to add the Drawing Node to.\")\n  }\n}\n\n\n/**\n * Adds a group to the scene.\n * @Deprecated Use oGroupNode.addGroup instead\n * @param   {string}     name                   The name of the newly created group.\n * @param   {string}     includeNodes           The nodes to add to the group.\n * @param   {$.oPoint}   addComposite           Whether to add a composite.\n * @param   {bool}       addPeg                 Whether to add a peg.\n * @param   {string}     group                  The group in which the node is added.\n * @param   {$.oPoint}   nodePosition           The position for the node to be placed in the network.\n\n * @return {$.oGroupNode}   The created node.\n */\n$.oScene.prototype.addGroup = function( name, includeNodes, addComposite, addPeg, group, nodePosition ){\n  var _group = (group instanceof this.$.oGroupNode)?group:this.$node(group);\n\n  if (_group != null && _group instanceof this.$.oGroupNode){\n    this.$.log(\"oScene.addGroup is deprecated. Use oGroupNode.addGroup instead\")\n    var _node = _group.addGroup(name, includeNodes, addComposite, addPeg, nodePosition )\n    return _node;\n  }else{\n    throw new Error (group+\" is an invalid group to add the Group Node to.\")\n  }\n}\n\n\n\n/**\n * Grabs the timeline object for a specific display.\n * @param   {string}        [display]                The display node to build the timeline for.\n * @return {$.oTimeline}    The timelne object given the display.\n */\n$.oScene.prototype.getTimeline = function(display){\n    return new this.$.oTimeline( display, this );\n}\n\n\n/**\n * Gets a scene palette by the name.\n * @param   {string}   name            The palette name to query and find.\n *\n * @return  {$.oPalette}                 The oPalette found given the query.\n */\n$.oScene.prototype.getPaletteByName = function(name){\n  var _palettes = this.palettes;\n  for (var i in _palettes){\n    if (_palettes[i].name == name) return _palettes[i];\n  }\n  return null;\n}\n\n/**\n * Gets a scene palette by the path of the plt file.\n * @param   {string}   path              The palette path to find.\n * @return  {$.oPalette}                 The oPalette or null if not found.\n */\n$.oScene.prototype.getPaletteByPath = function(path){\n  var _palettes = this.palettes;\n  for (var i in _palettes){\n    if (_palettes[i].path.path == path) return _palettes[i];\n  }\n  return null;\n}\n\n\n/**\n * Grabs the selected palette.\n * @return {$.oPalette}   oPalette with provided name.\n * @deprecated\n */\n$.oScene.prototype.getSelectedPalette = function(){\n    var _paletteList = PaletteManager.getScenePaletteList();\n    var _id = PaletteManager.getCurrentPaletteId()\n    var _palette = new this.$.oPalette(_paletteList.getPaletteById(_id), _paletteList);\n    return _palette;\n}\n\n\n/**\n * Add a palette object to the scene palette list and into the specified location.\n * @param   {string}         name                          The name for the palette.\n * @param   {string}         index                         Index at which to insert the palette.\n * @param   {string}         paletteStorage                Storage type: environment, job, scene, element, external.\n * @param   {$.oElement}     storeInElement                The element to store the palette in. If paletteStorage is set to \"external\", provide a destination folder for the palette here.\n *\n * @return {$.oPalette}   newly created oPalette with provided name.\n */\n$.oScene.prototype.addPalette = function(name, insertAtIndex, paletteStorage, storeInElement){\n  if (typeof paletteStorage === 'undefined') var paletteStorage = \"scene\";\n  if (typeof insertAtIndex === 'undefined') var insertAtIndex = 0;\n\n  var _list = PaletteObjectManager.getScenePaletteList();\n\n  if (typeof storeInElement === 'undefined'){\n    if (paletteStorage == \"external\") throw new Error(\"Element parameter should point to storage path if palette destination is External\")\n    if (paletteStorage == \"element\") throw new Error(\"Element parameter cannot be omitted if palette destination is Element\")\n    var storeInElement = 1;\n  }\n\n  var _destination = $.oPalette.location[paletteStorage]\n  if (paletteStorage == \"element\") var storeInElement = storeInElement.id;\n\n  this.$.log(paletteStorage+\" \"+_destination)\n\n  if (paletteStorage == \"external\") var _palette = new this.$.oPalette(_list.createPalette(storeInElement+\"/\"+name, insertAtIndex), _list);\n\n  // can fail if database lock wasn't released\n  var _palette = new this.$.oPalette(_list.createPaletteAtLocation(_destination, storeInElement, name, insertAtIndex), _list);\n  log(\"created palette : \"+_palette.path)\n  return _palette;\n}\n\n\n\n/**\n * Imports a palette to the scene palette list and into the specified storage location.\n * @param   {string}      path               The palette file to import.\n * @param   {string}      name               The name for the palette.\n * @param   {string}      index              Index at which to insert the palette.\n * @param   {string}      paletteStorage     Storage type: environment, job, scene, element, external.\n * @param   {$.oElement}  storeInElement     The element to store the palette in. If paletteStorage is set to \"external\", provide a destination folder for the palette here.\n *\n * @return {$.oPalette}   oPalette with provided name.\n */\n$.oScene.prototype.importPalette = function(filename, name, index, paletteStorage, storeInElement){\n  var _paletteFile = new this.$.oFile(filename);\n  if (!_paletteFile.exists){\n    throw new Error (\"Cannot import palette from file \"+filename+\" because it doesn't exist\", this.$.DEBUG_LEVEL.ERROR);\n  }\n\n  if (typeof name === 'undefined') var name = _paletteFile.name;\n\n  var _list = PaletteObjectManager.getScenePaletteList();\n  if  (typeof index === 'undefined') var index = _list.numPalettes;\n  if  (typeof paletteStorage === 'undefined') var paletteStorage = \"scene\";\n\n  if (typeof storeInElement === 'undefined'){\n    if (paletteStorage == \"external\") throw new Error(\"Element parameter should point to storage path if palette destination is External\")\n    if (paletteStorage == \"element\") throw new Error(\"Element parameter cannot be omitted if palette destination is Element\")\n    var storeInElement = 1;\n  }\n\n  var _location = this.$.oPalette.location;\n  switch (paletteStorage){\n    case 'environment' :\n      var paletteFolder = fileMapper.toNativePath(PaletteObjectManager.Locator.folderForLocation(_location.environment,1));\n      break;\n\n    case 'job' :\n      var paletteFolder = fileMapper.toNativePath(PaletteObjectManager.Locator.folderForLocation(_location.job,1))\n      break;\n\n    case 'scene' :\n      var paletteFolder = fileMapper.toNativePath(PaletteObjectManager.Locator.folderForLocation(_location.scene,1))\n      break;\n\n    case 'element' :\n      var paletteFolder = fileMapper.toNativePath(PaletteObjectManager.Locator.folderForLocation(_location.element, storeInElement.id))\n      break;\n  }\n\n  var paletteFolder = new this.$.oFolder(paletteFolder);\n\n  if (!paletteFolder.exists){\n    try{\n      paletteFolder.create();\n    }catch(err){\n      throw new Error (\"Couldn't create missing palette folder \" + paletteFolder +\": \" + err);\n    }\n  }\n\n  if (_paletteFile.folder.path != paletteFolder.path) {\n    _paletteFile = _paletteFile.copy(paletteFolder.path, name, true);\n  }\n\n  var _palette = _list.insertPalette(_paletteFile.toonboomPath.replace(\".plt\", \"\"), index);\n\n  _newPalette = new this.$.oPalette(_palette, _list);\n\n  return _newPalette;\n}\n\n\n/**\n * Creates a single palette containing all the individual colors used by an ensemble of nodes.\n * @param   {$.oNode[]}    nodes               The nodes to look at.\n * @param   {string}       [paletteName]       A custom name for the created palette.\n * @param   {string}       [colorName]         A custom name to give to the gathered colors.\n *\n * @return       {$.oLink[]}      An array of unique links existing between the nodes.\n */\n$.oScene.prototype.createPaletteFromNodes = function(nodes, paletteName, colorName){\n  if (typeof paletteName === 'undefined') var paletteName = this.name;\n  if (typeof colorName ==='undefined') var colorName = false;\n\n  // get unique Color Ids\n  var _usedColors = {};\n  for (var i in nodes){\n    _colors = nodes[i].usedColors;\n    for (var j in _colors){\n      _usedColors[_colors[j].id] = _colors[j];\n    }\n  }\n  _usedColors = Object.keys(_usedColors).map(function(x){return _usedColors[x]});\n\n  // create single palette\n  var _newPalette = this.addPalette(paletteName);\n  _newPalette.colors[0].remove();\n\n  for (var i=0; i<_usedColors.length; i++){\n    var _colorCopy = _usedColors[i].copyToPalette(_newPalette);\n    if (colorName) _colorCopy.name = colorName+\" \"+(i+1);\n  }\n\n  return _newPalette;\n}\n\n\n/**\n * Returns all the links existing between an ensemble of nodes.\n * @param   {$.oNode[]}      nodes                         The nodes to look at.\n *\n * @return  {$.oLink[]}      An array of unique links existing between the nodes.\n */\n$.oScene.prototype.getNodesLinks = function (nodes){\n  var _links = [];\n  var _linkStrings = [];\n  // var _nodePaths = nodes.map(function(x){return x.path});\n  var _nodePaths = {};\n\n  for (var i in nodes){\n    _nodePaths[nodes[i].path] = nodes[i];\n  }\n\n  for (var i in nodes){\n    var _inLinks = nodes[i].getInLinks();\n    var _outLinks = nodes[i].getOutLinks();\n\n    // add link only once and replace the node object for one from the passed arguments\n    for (var j in _inLinks){\n      var _link = _inLinks[j];\n      var _path = _link.outNode.path;\n      if (_nodePaths.hasOwnProperty(_path) && _linkStrings.indexOf(_link.toString()) == -1){\n        _link.inNode = nodes[i];\n        _link.outNode = _nodePaths[_path];\n        _links.push(_link);\n        _linkStrings.push(_link.toString());\n      }\n    }\n\n    // add link only once and replace the inNode object for one from the passed arguments\n    for (var j in _outLinks){\n      var _link = _outLinks[j];\n      var _path = _link.inNode.path;\n      if (_nodePaths.hasOwnProperty(_link.inNode.path) && _linkStrings.indexOf(_link.toString()) == -1){\n        _link.outNode = nodes[i];\n        _link.inNode = _nodePaths[_link.inNode.path];\n        _links.push(_link);\n        _linkStrings.push(_link.toString());\n      }\n    }\n  }\n\n  return _links;\n}\n\n\n/**\n * Merges Drawing nodes into a single node.\n * @param   {$.oNode[]}      nodes                         The Drawing nodes to merge.\n * @param   {string}         resultName                    The Node name for the resulting node of the merged content.\n * @param   {bool}           deleteMerged                  Whether the original nodes be deleted.\n *\n * @return {$.oNode}        The resulting drawing node from the merge.\n */\n$.oScene.prototype.mergeNodes = function (nodes, resultName, deleteMerged){\n    // TODO: is there a way to do this without Action.perform?\n    // pass a oNode object as argument for destination node instead of name/group?\n\n    if (typeof resultName === 'undefined') var resultName = nodes[0].name+\"_merged\"\n    if (typeof group === 'undefined') var group = nodes[0].group;\n    if (typeof deleteMerged === 'undefined') var deleteMerged = true;\n\n    // only merge READ nodes so we filter out other nodes from parameters\n    nodes = nodes.filter(function(x){return x.type == \"READ\"})\n\n    var _timeline = this.getTimeline()\n    nodes = nodes.sort(function(a, b){return a.timelineIndex(_timeline) - b.timelineIndex(_timeline)})\n\n    // create a new destination node for the merged result\n    var _mergedNode = this.addDrawingNode(resultName);\n\n    // connect the node to the scene base composite, TODO: handle better placement\n    // also TODO: check that the composite is connected to the display currently active\n    // also TODO: disable pegs that affect the nodes but that we don't want to merge\n    var _composite = this.nodes.filter(function(x){return x.type == \"COMPOSITE\"})[0]\n\n    _mergedNode.linkOutNode(_composite);\n\n    // get  the individual keys of all nodes\n    var _keys = []\n    for (var i in nodes){\n        var _timings = nodes[i].timings;\n        var _frameNumbers = _keys.map(function (x){return x.frameNumber})\n        for (var j in _timings){\n            if (_frameNumbers.indexOf(_timings[j].frameNumber) == -1) _keys.push(_timings[j])\n        }\n    }\n\n\n    // sort frame objects by frameNumber\n    _keys = _keys.sort(function(a, b){return a.frameNumber - b.frameNumber})\n\n    // create an empty drawing for each exposure of the nodes to be merged\n    for (var i in _keys){\n        var _frame = _keys[i].frameNumber\n        _mergedNode.element.addDrawing(_frame)\n\n        // copy paste the content of each of the nodes onto the mergedNode\n        // code inspired by Bake_Parent_to_Drawings v1.2 from Yu Ueda (raindropmoment.com)\n        frame.setCurrent( _frame );\n\n        Action.perform(\"onActionChooseSelectTool()\", \"cameraView\");\n        for (var j=nodes.length-1; j>=0; j--){\n            //if (nodes[j].attributes.drawing.element.frames[_frame].isBlank) continue;\n\n            DrawingTools.setCurrentDrawingFromNodeName( nodes[j].path, _frame );\n            Action.perform(\"selectAll()\", \"cameraView\");\n\n            // select all and check. If empty, operation ends for the current frame\n            if (Action.validate(\"copy()\", \"cameraView\").enabled){\n                Action.perform(\"copy()\", \"cameraView\");\n                DrawingTools.setCurrentDrawingFromNodeName( _mergedNode.path, _frame );\n                Action.perform(\"paste()\", \"cameraView\");\n            }\n        }\n    }\n\n    _mergedNode.attributes.drawing.element.column.extendExposures();\n    _mergedNode.placeAtCenter(nodes)\n\n    // connect to the same composite as the first node, at the same place\n    // delete nodes that were merged if parameter is specified\n    if (deleteMerged){\n        for (var i in nodes){\n            nodes[i].remove();\n        }\n    }\n    return _mergedNode;\n}\n\n\n/**\n * export a template from the specified nodes.\n * @param   {$.oNodes[]}  nodes                             The list of nodes included in the template.\n * @param   {bool}        [exportPath]                      The path of the TPL file to export.\n * @param   {string}      [exportPalettesMode='usedOnly']   can have the values : \"usedOnly\", \"all\", \"createPalette\"\n * @param   {string}      [renameUsedColors=]               if creating a palette, optionally set here the name for the colors (they will have a number added to each)\n * @param   {copyOptions} [copyOptions]                     An object containing paste options as per Harmony's standard paste options.\n *\n * @return {bool}         The success of the export.\n * @todo turn exportPalettesMode into an enum?\n * @example\n * // how to export a clean palette with no extra drawings and everything renamed by frame, and only the necessary colors gathered in one palette:\n *\n * $.beginUndo();\n *\n * var doc = $.scn;\n * var nodes = doc.getSelectedNodes();\n *\n * for (var i in nodes){\n *   if (nodes[i].type != \"READ\") continue;\n *\n *   var myColumn = nodes[i].element.column;      // we grab the column directly from the element of the node\n *   myColumn.removeUnexposedDrawings();          // remove extra unused drawings\n *   myColumn.renameAllByFrame();                 // rename all drawings by frame\n * }\n *\n * doc.exportTemplate(nodes, \"C:/templateExample.tpl\", \"createPalette\"); // \"createPalette\" value will create one palette for all colors\n *\n * $.endUndo();\n */\n$.oScene.prototype.exportTemplate = function(nodes, exportPath, exportPalettesMode, renameUsedColors, copyOptions){\n  if (typeof exportPalettesMode === 'undefined') var exportPalettesMode = \"usedOnly\";\n  if (typeof copyOptions === 'undefined') var copyOptions = copyPaste.getCurrentCreateOptions();\n  if (typeof renameUsedColors === 'undefined') var renameUsedColors = false;\n\n  if (!Array.isArray(nodes)) nodes = [nodes];\n\n  // add nodes included in groups as they'll get automatically exported\n  var _allNodes = nodes;\n  for (var i in nodes){\n    if (nodes[i].type == \"GROUP\") _allNodes = _allNodes.concat(nodes[i].subNodes(true));\n  }\n\n  var _readNodes = _allNodes.filter(function (x){return x.type == \"READ\";});\n\n  var _templateFolder = new this.$.oFolder(exportPath);\n  while (_templateFolder.exists) _templateFolder = new this.$.oFolder(_templateFolder.path.replace(\".tpl\", \"_1.tpl\"));\n\n  var _name = _templateFolder.name.replace(\".tpl\", \"\");\n  var _folder = _templateFolder.folder.path;\n\n  // create the palette with only the colors contained in the layers\n  if (_readNodes.length > 0){\n    if(exportPalettesMode == \"usedOnly\"){\n      var _usedPalettes = [];\n      var _usedPalettePaths = [];\n      for (var i in _readNodes){\n        var _palettes = _readNodes[i].getUsedPalettes();\n        for (var j in _palettes){\n          if (_usedPalettePaths.indexOf(_palettes[j].path.path) == -1){\n            _usedPalettes.push(_palettes[j]);\n            _usedPalettePaths.push(_palettes[j].path.path);\n          }\n        }\n      }\n      this.$.debug(\"found palettes : \"+_usedPalettes.map(function(x){return x.name}), this.$.DEBUG_LEVEL.LOG);\n    }\n\n    if (exportPalettesMode == \"createPalette\"){\n      var templatePalette = this.createPaletteFromNodes(_readNodes, _name, renameUsedColors);\n      var _usedPalettes = [templatePalette];\n    }\n  }\n\n\n  this.selectedNodes = _allNodes;\n  this.selectedFrames = [this.startPreview, this.stopPreview];\n\n  this.$.debug(\"exporting selection :\"+this.selectedFrames+\"\\n\\n\"+this.selectedNodes.join(\"\\n\")+\"\\n\\n to folder : \"+_folder+\"/\"+_name, this.$.DEBUG_LEVEL.LOG)\n\n  try{\n    var success = copyPaste.createTemplateFromSelection (_name, _folder);\n    if (success == \"\") throw new Error(\"export failed\")\n  }catch(error){\n    this.$.debug(\"Export of template \"+_name+\" failed. Error: \"+error, this.$.DEBUG_LEVEL.ERROR);\n    return false;\n  }\n\n  this.$.debug(\"export of template \"+_name+\" finished, cleaning palettes\", this.$.DEBUG_LEVEL.LOG);\n\n  if (_readNodes.length > 0 && exportPalettesMode != \"all\"){\n    // deleting the extra palettes from the exported template\n    var _paletteFolder = new this.$.oFolder(_templateFolder.path+\"/palette-library\");\n    var _paletteFiles = _paletteFolder.getFiles();\n    var _paletteNames = _usedPalettes.map(function(x){return x.name});\n\n    for (var i in _paletteFiles){\n      var _paletteName = _paletteFiles[i].name;\n      if (_paletteNames.indexOf(_paletteName) == -1) _paletteFiles[i].remove();\n    }\n\n    // building the template palette list\n    var _listFile = [\"ToonBoomAnimationInc PaletteList 1\"];\n\n    if (exportPalettesMode == \"createPalette\"){\n      _listFile.push(\"palette-library/\"+_name+' LINK \"'+_paletteFolder+\"/\"+_name+'.plt\"');\n    }else if (exportPalettesMode == \"usedOnly\"){\n      for (var i in _usedPalettes){\n        this.$.debug(\"palette \"+_usedPalettes[i].name+\" to be included in template\", this.$.DEBUG_LEVEL.LOG);\n        _listFile.push(\"palette-library/\"+_usedPalettes[i].name+' LINK \"'+_paletteFolder+\"/\"+_usedPalettes[i].name+'.plt\"');\n      }\n    }\n\n    var _paletteListFile = new this.$.oFile(_templateFolder.path+\"/PALETTE_LIST\");\n    try{\n      _paletteListFile.write(_listFile.join(\"\\n\"));\n    }catch(err){\n      this.$.debug(err, this.$.DEBUG_LEVEL.ERROR)\n    }\n\n    // remove the palette created for the template\n    if (exportPalettesMode == \"createPalette\"){\n      var _paletteFile = _paletteFolder.getFiles()[0];\n      if (_paletteFile){\n        _paletteFile.rename(_name);\n        if (templatePalette) templatePalette.remove(true);\n      }\n    }\n  }\n\n  selection.clearSelection();\n  return true;\n}\n\n\n/**\n * Imports the specified template into the scene.\n * @deprecated\n * @param   {string}           tplPath                                        The path of the TPL file to import.\n * @param   {string}           [group]                                        The path of the existing target group to which the TPL is imported.\n * @param   {$.oNode[]}        [destinationNodes]                             The nodes affected by the template.\n * @param   {bool}             [extendScene]                                  Whether to extend the exposures of the content imported.\n * @param   {$.oPoint}         [nodePosition]                                 The position to offset imported new nodes.\n * @param   {object}           [pasteOptions]                                 An object containing paste options as per Harmony's standard paste options.\n *\n * @return {$.oNode[]}         The resulting pasted nodes.\n */\n$.oScene.prototype.importTemplate = function( tplPath, group, destinationNodes, extendScene, nodePosition, pasteOptions ){\n  if (typeof group === 'undefined') var group = this.root;\n  var _group = (group instanceof this.$.oGroupNode)?group:this.$node(group);\n\n  if (_group != null && _group instanceof this.$.oGroupNode){\n    this.$.log(\"oScene.importTemplate is deprecated. Use oGroupNode.importTemplate instead\")\n    var _node = _group.addNode(tplPath, destinationNodes, extendScene, nodePosition, pasteOptions )\n    return _nodes;\n  }else{\n    throw new Error (group+\" is an invalid group to import the template into.\")\n  }\n}\n\n\n/**\n * Exports a png of the selected node/frame. if no node is given, all layers will be visible.\n * @param {$.oFile}  path                        The path in which to save the image. Image will be outputted as PNG.\n * @param {$.oNode}  [includedNodes]             The nodes to include in the rendering. If no node is specified, all layers will be visible.\n * @param {int}      [exportFrame]               The frame at which to create the image. By default, the timeline current Frame.\n * @param {bool}     [exportCameraFrame=false]   Whether to export the camera frames\n * @param {bool}     [exportBackground=false]    Whether to add a white background.\n * @param {float}    [frameScale=1]              A factor by which to scale the frame. ex: 1.05 will add a 10% margin (5% on both sides)\n */\n$.oScene.prototype.exportLayoutImage = function (path, includedNodes, exportFrame, exportCameraFrame, exportBackground, frameScale, format){\n  if (typeof includedNodes === 'undefined') var includedNodes = [];\n  if (typeof exportCameraFrame === 'undefined') var exportCameraFrame = false;\n  if (typeof exportBackground === 'undefined') var exportBackground = false;\n  if (typeof frameScale === 'undefined') var frameScale = 1;\n  if (typeof frame === 'undefined') var frame = 1;\n  if (typeof format === 'undefined') var format = \"PNG4\";\n  if (typeof path != this.$.oFile) path = new $.oFile(path);\n\n  var exporter = new LayoutExport();\n  var params = new LayoutExportParams();\n  params.renderStaticCameraAtSceneRes = true;\n  params.fileFormat = format;\n  params.borderScale = frameScale;\n  params.exportCameraFrame = exportCameraFrame;\n  params.exportAllCameraFrame = false;\n  params.filePattern = path.name;\n  params.fileDirectory = path.folder;\n  params.whiteBackground = exportBackground;\n\n  includedNodes = includedNodes.filter(function(x){return [\"CAMERA\", \"READ\", \"COLOR_CARD\", \"GRADIENT\"].indexOf(x.type) != -1 && x.enabled })\n  var _timeline = new this.$.oTimeline();\n  includedNodes = includedNodes.sort(function (a, b){return b.timelineIndex(_timeline) - a.timelineIndex(_timeline)})\n\n  if (includedNodes.length == 0) {\n    params.node = this.root;\n    params.frame = exportFrame;\n    params.layoutname = this.name;\n    exporter.addRender(params);\n    if (!exporter.save(params)) throw new Error(\"failed to export layer \"+oNode.name+\" at location \"+path);\n  }else{\n    for (var i in includedNodes){\n      var includedNode = includedNodes[i];\n      params.whiteBackground = (i==0 && exportBackground);\n      params.node = includedNode.path;\n      params.frame = exportFrame;\n      params.layoutname = includedNode.name;\n      params.exportCameraFrame = ((i == includedNodes.length-1) && exportCameraFrame);\n      exporter.addRender(params);\n      if (!exporter.save(params)) throw new Error(\"failed to export layer \"+oNode.name+\" at location \"+path);\n    }\n  }\n\n  exporter.flush();\n\n  return path;\n}\n\n\n/**\n * Export the scene as a single PSD file, with layers described by the layerDescription array. This function is not supported in batch mode.\n * @param {$.oFile}  path\n * @param {float}    margin                    a factor by which to increase the rendering area. for example, 1.05 creates a 10% margin. (5% on each side)\n * @param {Object[]} layersDescription          must be an array of objects {layer: $.oNode, frame: int} which describe all the images to export. By default, will include all visible layers of the timeline.\n */\n$.oScene.prototype.exportPSD = function (path, margin, layersDescription){\n  if (typeof margin === 'undefined') var margin = 1;\n  if (typeof layersDescription === 'undefined') {\n    // export the current frame for each drawing layer present in the default timeline.\n    var _allNodes = this.nodes.filter(function(x){return [\"READ\", \"COLOR_CARD\", \"GRADIENT\"].indexOf(x.type) != -1 && x.enabled })\n    var _timeline = new this.$.oTimeline();\n    _allNodes = _allNodes.sort(function (a, b){return b.timelineIndex(_timeline) - a.timelineIndex(_timeline)})\n    var _scene = this;\n    var layersDescription = _allNodes.map(function(x){return ({layer: x, frame: _scene.currentFrame})})\n  }\n  if (typeof path != this.$.oFile) path = new $.oFile(path)\n  var tempPath = new $.oFile(path.folder+\"/\"+path.name+\"~\")\n\n  var errors = [];\n\n  // setting up render\n  var exporter = new LayoutExport();\n  var params = new LayoutExportParams();\n  params.renderStaticCameraAtSceneRes = true;\n  params.fileFormat = \"PSD4\";\n  params.borderScale = margin;\n  params.exportCameraFrame = false;\n  params.exportAllCameraFrame = false;\n  params.filePattern = tempPath.name;\n  params.fileDirectory = tempPath.folder;\n  params.whiteBackground = false;\n\n  // export layers\n  for (var i in layersDescription){\n    var _frame = layersDescription[i].frame;\n    var _layer = layersDescription[i].layer;\n\n    params.node = _layer.path;\n    params.frame = _frame;\n    params.layoutname = _layer.name;\n    params.exportCameraFrame = (i == layersDescription.length-1);\n    exporter.addRender(params);\n\n    if (!exporter.save(params)) errors.push(params.layoutname);\n  }\n\n  if (errors.length > 0) throw new Error(\"errors during export of file \"+path+\" with layers \"+errors)\n\n  // write file\n  exporter.flush();\n\n  if (path.exists) path.remove();\n  log(tempPath.exist+\" \"+tempPath);\n  tempPath.rename(path.name+\".psd\");\n}\n\n\n/**\n * Imports a PSD to the scene.\n * @Deprecated use oGroupNode.importPSD instead\n * @param   {string}         path                          The palette file to import.\n * @param   {string}         [group]                       The path of the existing group to import the PSD into.\n * @param   {$.oPoint}       [nodePosition]                The position for the node to be placed in the network.\n * @param   {bool}           [separateLayers]              Separate the layers of the PSD.\n * @param   {bool}           [addPeg]                      Whether to add a peg.\n * @param   {bool}           [addComposite]                Whether to add a composite.\n * @param   {string}         [alignment]                   Alignment type.\n *\n * @return {$.oNode[]}     The nodes being created as part of the PSD import.\n */\n$.oScene.prototype.importPSD = function( path, group, nodePosition, separateLayers, addPeg, addComposite, alignment ){\n  if (typeof group === 'undefined') var group = this.root;\n  var _group = (group instanceof this.$.oGroupNode)?group:this.$node(group);\n\n  if (_group != null && _group instanceof this.$.oGroupNode){\n    this.$.log(\"oScene.importPSD is deprecated. Use oGroupNode.importPSD instead\")\n    var _node = _group.importPSD(path, separateLayers, addPeg, addComposite, alignment, nodePosition)\n    return _node;\n  }else{\n    throw new Error (group+\" is an invalid group to import a PSD file to.\")\n  }\n}\n\n\n/**\n * Updates a previously imported PSD by matching layer names.\n * @deprecated\n * @param   {string}       path                          The PSD file to update.\n * @param   {bool}         [separateLayers]              Whether the PSD was imported as separate layers.\n *\n * @returns {$.oNode[]}    The nodes affected by the update\n */\n$.oScene.prototype.updatePSD = function( path, group, separateLayers ){\n  if (typeof group === 'undefined') var group = this.root;\n  var _group = (group instanceof this.$.oGroupNode)?group:this.$node(group);\n\n  if (_group != null && _group instanceof this.$.oGroupNode){\n    this.$.log(\"oScene.updatePSD is deprecated. Use oGroupNode.updatePSD instead\")\n    var _node = _group.updatePSD(path, separateLayers)\n    return _node;\n  }else{\n    throw new Error (group+\" is an invalid group to update a PSD file in.\")\n  }\n}\n\n\n/**\n * Imports a sound into the scene\n * @param   {string}         path                          The sound file to import.\n * @param   {string}         layerName                     The name to give the layer created.\n *\n * @return {$.oNode}        The imported sound column.\n */\n $.oScene.prototype.importSound = function(path, layerName){\n   var _audioFile = new this.$.oFile(path);\n   if (typeof layerName === 'undefined') var layerName = _audioFile.name;\n\n   // creating an audio column for the sound\n    var _soundColumn = this.addColumn(\"SOUND\", layerName);\n    column.importSound( _soundColumn.name, 1, path);\n\n    return _soundColumn;\n }\n\n\n /**\n * Exports a QT of the scene\n * @param   {string}         path                          The path to export the quicktime file to.\n * @param   {string}         display                       The name of the display to use to export.\n * @param   {double}         scale                         The scale of the export compared to the scene resolution.\n * @param   {bool}           exportSound                   Whether to include the sound in the export.\n * @param   {bool}           exportPreviewArea             Whether to only export the preview area of the timeline.\n *\n * @return {bool}        The success of the export\n */\n$.oScene.prototype.exportQT = function( path, display, scale, exportSound, exportPreviewArea){\n  if (typeof display === 'undefined') var display = node.getName(node.getNodes([\"DISPLAY\"])[0]);\n  if (typeof exportSound === 'undefined') var exportSound = true;\n  if (typeof exportPreviewArea === 'undefined') var exportPreviewArea = false;\n  if (typeof scale === 'undefined') var scale = 1;\n\n  if (display instanceof oNode) display = display.name;\n\n  var _startFrame = exportPreviewArea?scene.getStartFrame():1;\n  var _stopFrame = exportPreviewArea?scene.getStopFrame():this.length-1;\n  var _resX = this.defaultResolutionX*scale\n  var _resY= this.defaultResolutionY*scale\n  return exporter.exportToQuicktime (\"\", _startFrame, _stopFrame, exportSound, _resX, _resY, path, display, true, 1);\n}\n\n\n/**\n * Imports a QT into the scene\n * @Deprecated\n * @param   {string}         path                          The quicktime file to import.\n * @param   {string}         group                         The group to import the QT into.\n * @param   {$.oPoint}       nodePosition                  The position for the node to be placed in the network.\n * @param   {bool}           extendScene                   Whether to extend the scene to the duration of the QT.\n * @param   {string}         alignment                     Alignment type.\n *\n * @return {$.oNode}        The imported Quicktime Node.\n */\n$.oScene.prototype.importQT = function( path, group, importSound, nodePosition, extendScene, alignment ){\n  if (typeof group === 'undefined') var group = this.root;\n  var _group = (group instanceof this.$.oGroupNode)?group:this.$node(group);\n\n  if (_group != null && _group instanceof this.$.oGroupNode){\n    this.$.log(\"oScene.importQT is deprecated. Use oGroupNode.importQTs instead\")\n    var _node = _group.importQT(path, importSound, extendScene, alignment, nodePosition)\n    return _node;\n  }else{\n    throw new Error (group+\" is an invalid group to import a QT file to.\")\n  }\n}\n\n\n/**\n * Adds a backdrop to a group in a specific position.\n * @Deprecated\n * @param   {string}           groupPath                         The group in which this backdrop is created.\n * @param   {string}           title                             The title of the backdrop.\n * @param   {string}           body                              The body text of the backdrop.\n * @param   {$.oColorValue}    color                             The oColorValue of the node.\n * @param   {float}            x                                 The X position of the backdrop, an offset value if nodes are specified.\n * @param   {float}            y                                 The Y position of the backdrop, an offset value if nodes are specified.\n * @param   {float}            width                             The Width of the backdrop, a padding value if nodes are specified.\n * @param   {float}            height                            The Height of the backdrop, a padding value if nodes are specified.\n *\n * @return {$.oBackdrop}       The created backdrop.\n */\n$.oScene.prototype.addBackdrop = function( groupPath, title, body, color, x, y, width, height ){\n  if (typeof group === 'undefined') var group = this.root;\n  var _group = (group instanceof this.$.oGroupNode)?group:this.$node(group);\n\n  if (_group != null && _group instanceof this.$.oGroupNode){\n    this.$.log(\"oScene.addBackdrop is deprecated. Use oGroupNode.addBackdrop instead\")\n    var _backdrop = _group.addBackdrop(title, body, color, x, y, width, height)\n    return _backdrop;\n  }else{\n    throw new Error (groupPath+\" is an invalid group to add the BackDrop to.\")\n  }\n};\n\n\n/**\n * Adds a backdrop to a group around specified nodes\n * @Deprecated\n * @param   {string}           groupPath                         The group in which this backdrop is created.\n * @param   {$.oNode[]}        nodes                             The nodes that the backdrop encompasses.\n * @param   {string}           title                             The title of the backdrop.\n * @param   {string}           body                              The body text of the backdrop.\n * @param   {$.oColorValue}    color                             The oColorValue of the node.\n * @param   {float}            x                                 The X position of the backdrop, an offset value if nodes are specified.\n * @param   {float}            y                                 The Y position of the backdrop, an offset value if nodes are specified.\n * @param   {float}            width                             The Width of the backdrop, a padding value if nodes are specified.\n * @param   {float}            height                            The Height of the backdrop, a padding value if nodes are specified.\n *\n * @return {$.oBackdrop}       The created backdrop.\n */\n$.oScene.prototype.addBackdropToNodes = function( groupPath, nodes, title, body, color, x, y, width, height ){\n  if (typeof group === 'undefined') var group = this.root;\n  var _group = (group instanceof this.$.oGroupNode)?group:this.$node(group);\n\n  if (_group != null && _group instanceof this.$.oGroupNode) {\n    this.$.log(\"oScene.addBackdropToNodes is deprecated. Use oGroupNode.addBackdropToNodes instead\")\n    var _backdrop = _group.addBackdropToNodes(nodes, title, body, color, x, y, width, height)\n    return _backdrop;\n  }else{\n    throw new Error (groupPath+\" is an invalid group to add the BackDrop to.\")\n  }\n};\n\n\n/**\n * Saves the scene.\n */\n$.oScene.prototype.save = function( ){\n  scene.saveAll();\n}\n\n\n/**\n * Saves the scene in a different location (only available on offline scenes).\n * @param {string} newPath    the new location for the scene (must be a folder path and not a .xstage)\n */\n$.oScene.prototype.saveAs = function(newPath){\n  if (this.online) {\n    this.$.debug(\"Can't use saveAs() in database mode.\", this.$.DEBUG_LEVEL.ERROR);\n    return;\n  }\n\n  if (newPath instanceof this.$.oFile) newPath = newPath.path;\n  return scene.saveAs(newPath);\n}\n\n\n/**\n * Saves the scene as new version.\n * @param {string}      newVersionName      The name for the new version\n * @param {bool}        markAsDefault       Wether to make this new version the default version that will be opened from the database.\n */\n$.oScene.prototype.saveNewVersion = function(newVersionName, markAsDefault){\n  if (typeof markAsDefault === 'undefined') var markAsDefault = true;\n\n  return scene.saveAsNewVersion (newVersionName, markAsDefault);\n}\n\n\n/**\n * Renders the write nodes of the scene. This action saves the scene.\n * @param {bool}   [renderInBackground=true]    Whether to do the render on the main thread and block script execution\n * @param {int}    [startFrame=1]               The first frame to render\n * @param {int}    [endFrame=oScene.length]     The end of the render (non included)\n * @param {int}    [resX]                       The horizontal resolution of the render. Uses the scene resolution by default.\n * @param {int}    [resY]                       The vertical resolution of the render. Uses the scene resolution by default.\n * @param {string} [preRenderScript]            The path to the script to execute on the scene before doing the render\n * @param {string} [postRenderScript]           The path to the script to execute on the scene after the render is finished\n * @return {$.oProcess} In case of using renderInBackground, will return the oProcess object doing the render\n */\n$.oScene.prototype.renderWriteNodes = function(renderInBackground, startFrame, endFrame, resX, resY, preRenderScript, postRenderScript){\n  if (typeof renderInBackground === 'undefined') var renderInBackground = true;\n  if (typeof startFrame === 'undefined') var startFrame = 1;\n  if (typeof endFrame === 'undefined') var endFrame = this.length+1;\n  if (typeof resX === 'undefined') var resX = this.resolutionX;\n  if (typeof resY === 'undefined') var resY = this.resolutionY;\n\n  this.save();\n  var harmonyBin = specialFolders.bin+\"/HarmonyPremium.exe\";\n\n  var args = [\"-batch\", \"-frames\", startFrame, endFrame, \"-res\", resX, resY, this.fov];\n\n  if (typeof preRenderScript !== 'undefined'){\n    args.push(\"-preRenderScript\");\n    args.push(preRenderScript);\n  }\n\n  if (typeof postRenderScript !== 'undefined'){\n    args.push(\"-postRenderScript\");\n    args.push(postRenderScript);\n  }\n\n  if (this.online){\n    args.push(\"-env\");\n    args.push(this.environnement);\n    args.push(\"-job\");\n    args.push(this.job);\n    args.push(\"-scene\");\n    args.push(this.name);\n  }else{\n    args.push(this.stage);\n  }\n\n  var p = new this.$.oProcess(harmonyBin, args);\n  p.readChannel = \"All\";\n\n  this.$.log(\"Starting render of scene \"+this.name);\n  if (renderInBackground){\n    var length = endFrame - startFrame + 1;\n\n    var progressDialogue = new this.$.oProgressDialog(\"Rendering : \",length,\"Render Write Nodes\", true);\n\n    var cancelRender = function(){\n      p.kill();\n      this.$.alert(\"Render was canceled.\")\n    }\n\n    var renderProgress = function(message){\n      // reporting progress to log window\n      var progressRegex = /Rendered Frame ([0-9]+)/igm;\n      var matches = [];\n      while (match = progressRegex.exec(message)) {\n        matches.push(match[1]);\n      }\n      if (matches.length!=0){\n        var progress = parseInt(matches.pop(), 10) - startFrame;\n        progressDialogue.label = \"Rendering Frame: \" + progress + \"/\" + length;\n        progressDialogue.value = progress;\n        var percentage = Math.round(progress/length * 100);\n        this.$.log(\"render : \" + percentage + \"% complete\");\n      }\n    }\n\n    var renderFinished = function(exitCode){\n      if (exitCode == 0){\n        // render success\n        progressDialogue.label = \"Rendering Finished\"\n        progressDialogue.value = length;\n        this.$.log(exitCode + \" : render finished\");\n      }else{\n        this.$.log(exitCode + \" : render cancelled\");\n      }\n    }\n\n    progressDialogue.canceled.connect(this, cancelRender);\n    p.readyRead.connect(this, renderProgress);\n    p.finished.connect(this, renderFinished);\n    p.launchAndRead();\n    return p;\n  }else{\n    var readout  = p.execute();\n    this.$.log(\"render finished\");\n    return readout;\n  }\n}\n\n/**\n * Closes the scene.\n * @param   {bool}            [exit]                                       Whether it should exit after closing.\n */\n$.oScene.prototype.close = function( exit ){\n  if (typeof nodePosition === 'undefined') exit = false;\n\n  if( exit ){\n    scene.closeSceneAndExit();\n  }else{\n    scene.closeScene();\n  }\n}\n\n/**\n * Gets the current camera matrix.\n *\n * @return {Matrix4x4}          The matrix of the camera.\n */\n$.oScene.prototype.getCameraMatrix = function( ){\n    return scene.getCameraMatrix();\n}\n\n/**\n * Gets the current projection matrix.\n *\n * @return {Matrix4x4}          The projection matrix of the camera/scene.\n */\n$.oScene.prototype.getProjectionMatrix = function( ){\n  var fov = this.fov;\n  var f   = scene.toOGL( new Point3d( 0.0, 0.0, this.unitsZ ) ).z;\n  var n   = 0.00001;\n\n  //Standard pprojection matrix derivation.\n  var S = 1.0 / Math.tan( ( fov/2.0 ) * ( $.pi/180.0 ) );\n  var projectionMatrix = [  S,          0.0,                  0.0,     0.0,\n                            0.0,          S,                  0.0,     0.0,\n                            0.0,        0.0,       -1.0*(f/(f-n)),    -1.0,\n                            0.0,        0.0,   -1.0*((f*n)/(f-n)),     0.0\n                         ];\n\n  var newMatrix = new Matrix4x4();\n  for( var r=0;r<4;r++ ){\n    for( var c=0;c<4;c++ ){\n      newMatrix[\"m\"+r+\"\"+c] = projectionMatrix[ (c*4.0)+r ];\n    }\n  }\n  return newMatrix;\n}\n\n\n/**\n * Gets the current scene's metadata.\n *\n * @see $.oMetadata\n * @return {$.oMetadata}          The metadata of the scene.\n */\n$.oScene.prototype.getMetadata = function( ){\n  return new this.$.oMetadata( );\n}\n\n\n// Short Notations\n\n/**\n * Gets a node by the path.\n * @param   {string}   fullPath         The path of the node in question.\n *\n * @return  {$.oNode}                     The node found given the query.\n */\n$.oScene.prototype.$node = function( fullPath ){\n    return this.getNodeByPath( fullPath );\n}\n\n/**\n * Gets a column by the name.\n * @param  {string}             uniqueName               The unique name of the column as a string.\n * @param  {$.oAttribute}       oAttributeObject         The oAttribute object the column is linked to.\n *\n * @return {$.oColumn}          The node found given the query.\n */\n$.oScene.prototype.$column = function( uniqueName, oAttributeObject ){\n    return this.getColumnByName( uniqueName, oAttributeObject );\n}\n\n\n/**\n * Gets a palette by its name.\n * @param   {string}   name            The name of the palette.\n *\n * @return  {$.oPalette}               The node found given the query.\n */\n$.oScene.prototype.$palette = function( name ){\n    return this.getPaletteByName( name );\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_threading.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developped by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is garanteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oThread class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The base class for the $.oThread -- WIP, NOT TRULY THREADED AS THE EVENT MANAGER DOESNT ALLOW FOR THREADS YET.\n * @constructor\n * @classdesc  $.oThread Base Class\n * @param   {function}                 kernel                The kernel that is iterating.\n * @param   {object[]}                 list                  The list of elements to iterate upon.\n * @param   {int}                      [threadCount]         The amount of threads to initiate. Default: 5\n * @param   {bool}                     [start]               Whether to start on instantiation, or to wait until prompted. Default: false\n * @param   {int}                      [timeout]             Timeout in MS\n * @param   {bool}                     [reserveThread]       Whether to reserve a thread for this to process while blocking.\n *\n * @property {int}                     threadCount           The amount of threads to initiate.\n * @property {QTimer[]}                threads               The underlying QTimers that behave as threads.\n * @property {object[]}                results_thread        The results from the kernel, should match indices of provided list.\n * @property {string[]}                error_thread          The errors from the kernel, in the event there are code errors.\n * @property {bool[]}                  complete_thread       The completion (note: not success) state of the thread. Success state would be the result.\n * @property {bool}                    started               The start state of all threads.\n * @property {int}                     timeout               MS timeout for blocking processes.\n */\n$.oThread = function( kernel, list, threadCount, start, timeout, reserveThread ){\n  if (typeof threadCount === 'undefined') var threadCount = \"2\";\n  if (typeof start === 'undefined') var start = false;\n  if (typeof reserveThread === 'undefined') reserveThread = true;\n\n  threadCount = Math.min( threadCount, list.length );\n\n  this.list               = list;\n  this.threadCount        = threadCount;\n  this.threads            = [];\n  this.started_thread     = [];\n  this.results_thread     = [];\n  this.error_thread       = [];\n  this.complete_thread    = [];\n\n  this.started     = false;\n\n  this.startAtInstantiation = start;\n  this.threads_available    = false;\n  this.reserveThread        = reserveThread;\n  this.reservedThread       = false;\n\n  this.timeout     = 1000.0 * 60.0;\n  if ( timeout ) this.timeout = timeout;\n\n  //Instantiate the results.\n  for( var n=0;n<list.length;n++ ){\n    this.results_thread.push( false );\n    this.complete_thread.push( false );\n    this.error_thread.push( false );\n  }\n\n  var context = {\n                  \"kernel\"   : kernel,\n                  \"list\"     : list,\n                  \"results\"  : this.results_thread,\n                  \"complete\" : this.complete_thread,\n                  \"error\"    : this.error_thread\n                };\n\n  this.kernel = function( thread, from, to ){\n    var local_context = context;\n    for( var n=from;n<to;n++ ){\n      try{\n        var result = local_context[\"kernel\"]( local_context[\"list\"][n] );\n        local_context[\"results\"][n]   = result;\n        local_context[\"complete\"][n]  = true;\n        local_context[\"error\"][n]     = true;\n      }catch( err ){\n        System.println( err + \" (\" +err.lineNumber+ \" \" + err.fileName + \")\" );\n        local_context[\"results\"][n]   = false;\n        local_context[\"complete\"][n]  = true;\n        local_context[\"error\"][n]     = ( err + \" (\" +err.lineNumber+ \" \" + err.fileName + \")\" );\n      }\n    }\n  };\n\n}\n\n/**\n * The completion state of all the threads.\n * @name $.oThread#complete\n * @type {bool}\n */\nObject.defineProperty($.oThread.prototype, 'complete', {\n    get : function(){\n        if( !this.started ){\n          System.println( \"Not yet started\" );\n          return false;\n        }\n\n        for( var n=0;n<this.complete_thread.length;n++ ){\n          if( !this.complete_thread[n] ){\n            return false;\n          }\n        }\n\n        return true;\n    }\n});\n\n/**\n * The indices that have completed results.\n * @name $.oThread#completedIndices\n * @type {int[]}\n */\nObject.defineProperty($.oThread.prototype, 'completedIndices', {\n    get : function(){\n        var indices = [];\n        for( var n=0;n<this.complete_thread.length;n++ ){\n          if( this.complete_thread[n] ){\n            indices.push( n );\n          }\n        }\n\n        return indices;\n    }\n});\n\n/**\n * The errors, if any, in form { \"index\" : int, \"error\" : string }\n * @name $.oThread#errorsWithIndex\n * @type {object[]}\n */\nObject.defineProperty($.oThread.prototype, 'errorsWithIndex', {\n    get : function(){\n        var errors = [];\n        for( var n=0;n<this.error_thread.length;n++ ){\n          if( this.error_thread[n] ){\n            errors.push( { \"index\" : n, \"error\":this.error_thread[n] } );\n          }\n        }\n\n      return errors;\n    }\n});\n\n/**\n * The results, if any, in form { \"index\" : int, \"results\" : object }\n * @name $.oThread#resultsWithIndex\n * @type {object[]}\n */\nObject.defineProperty($.oThread.prototype, 'resultsWithIndex', {\n    get : function(){\n        var results = [];\n        for( var n=0;n<this.results_thread.length;n++ ){\n          if( this.results_thread[n] ){\n            results.push( { \"index\" : n, \"results\":this.results_thread[n] } );\n          }\n        }\n\n      return results;\n    }\n});\n\n/**\n * The errors, matching index of input list.\n * @name $.oThread#errors\n * @type {string[]}\n */\nObject.defineProperty($.oThread.prototype, 'errors', {\n    get : function(){\n      return this.error_thread;\n    }\n});\n\n/**\n * The errors, matching index of input list.\n * @name $.oThread#results\n * @type {object[]}\n */\nObject.defineProperty($.oThread.prototype, 'results', {\n    get : function(){\n      return this.results_thread;\n    }\n});\n\n\n/**\n * Start the thread and block if necessary.\n * @param   {bool}         block                    Whether the process should block and wait for completion.\n */\n$.oThread.prototype.start = function( block ){\n  if (typeof block === 'undefined') block = true;\n\n  if( !this.threads_available ){\n    if( !this.prepareThreads() ){\n      return;\n    }\n  }\n\n  for( var n=0;n<this.threads.length;n++ ){\n    // System.println( \"THREAD STARTING: \" + n );\n    if( this.started_thread[ n ] ){\n      continue;\n    }\n\n    this.threads[n].start( 0 );\n    QCoreApplication.processEvents();\n\n    this.started_thread[ n ] = true;\n  }\n\n  this.started = true;\n\n  if( block ){\n    this.wait();\n  }\n}\n\n\n/**\n * If threads are not yet prepared, this will prepare them.\n * @param   {bool}         [block]                    Whether the process should block and wait for completion.\n */\n$.oThread.prototype.prepareThreads = function( start ){\n  if (start) this.startAtInstantiation = start;\n\n  if( this.threads_available ){\n    return false;\n  }\n\n  try{\n    for( var thread_num=0;thread_num<this.threadCount;thread_num++ ){\n      this.started_thread.push( false );\n\n      var from_val  = Math.floor( ( thread_num / this.threadCount ) * this.list.length );\n      var to_val    = Math.floor( ( (thread_num+1) / this.threadCount ) * this.list.length );\n\n      if( this.reserveThread && thread_num == this.threadCount-1 ){\n        this.reservedThread = eval( 'kernel = function(){ this.kernel('+thread_num+', '+from_val+','+to_val+') }'  );\n        continue;\n      }\n\n      this.threads.push( new QTimer() );\n      this.threads[thread_num].singleShot = true;\n      this.threads[thread_num][\"timeout\"].connect( this, eval( 'kernel = function(){ this.kernel('+thread_num+', '+from_val+','+to_val+') }'  ) );\n\n      if( this.startAtInstantiation ){\n        this.threads[thread_num].start(0);\n        QCoreApplication.processEvents();\n        this.started = true;\n        this.started_thread[ thread_num ] = true;\n      }\n    }\n  }catch(err){\n    System.println( err + \" (\" +err.lineNumber+ \" \" + err.fileName + \")\" );\n  }\n\n  this.threads_available    = true;\n  return true;\n}\n\n\n/**\n * If started, will block until completion or timeout.\n * @param   {int}         block_time                    The MS time to block.\n */\n$.oThread.prototype.wait = function( block_time ){\n    if ( block_time ) this.timeout = block_time;\n\n    if( this.reserveThread && this.reservedThread ){\n      this.reservedThread();\n    }\n\n    if( !this.started ){\n      return;\n    }\n\n    var start_time = (new Date()).getTime();\n    var curr_time  = (new Date()).getTime();\n\n    var completed  = false;\n    while( (curr_time - start_time) < this.timeout ){\n      QCoreApplication.processEvents();\n      if( this.complete ){\n        completed = true;\n        break;\n      }\n      curr_time  = (new Date()).getTime();\n    }\n}\n\n/**\n * If started, will block until completion or timeout.\n */\n$.oThread.prototype.runSingleThreaded = function( ){\n  this.started = true;\n  for( var n=0;n<this.list.length;n++ ){\n    this.kernel( 0, n, n+1 );\n  }\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oProcess class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for $.oProcess.\n * @name        $.oProcess\n * @classdesc\n * Process class that allows user to launch executables outside harmony and get feedback from them.\n * @constructor\n * @param    {string}    bin          The path to the binary executable that will be launched.\n * @param    {string[]}  queryArgs    A string array of the different arguments given to the command.\n *\n * @property {$.oSignal} readyRead    A $.oSignal that can be connected to a callback, emitted every time new messages are outputted by the oProcess. Signature: readyRead(stdout (string))\n * @property {$.oSignal} finished     A $.oSignal that can be connected to a callback, emitted when the oProcess has finished. Signature: finished(returnCode(int), stdout(string))\n * @property {QProcess}  process      the QProcess object wrapped by the $.oProcess object.\n * @property {string}    bin          The path to the binary executable that will be launched.\n * @property {string[]}  queryArgs    A string array of the different arguments given to the command.\n * @property {string}    log          The full log of all the messages outputted over the course of the process lifetime.\n */\n$.oProcess = function(bin, queryArgs){\n  this.readyRead = new this.$.oSignal()\n  this.finished = new this.$.oSignal()\n  this.bin = bin;\n  this.queryArgs = queryArgs;\n  this.process = new QProcess();\n  this.readChannel = \"All\";\n  this.log = \"\";\n}\n\n\n/**\n * Which channel will the process read from. Set before launching the process. can take the values \"All\", \"Output\" and \"Error\".\n * @name $.oProcess#readChannel\n * @type {string}\n */\nObject.defineProperty($.oProcess.prototype, 'readChannel', {\n  get : function(){\n    var merged = (this.process.processChannelMode() == QProcess.MergedChannels);\n    if (merged) return \"All\";\n    if (this.process.readChannel == QProcess.StandardOutput) return \"Output\";\n    if (this.process.readChannel == QProcess.StandardError) return \"Error\";\n  },\n\n  set : function(channel){\n    if (channel == \"All\") {\n      this.process.setProcessChannelMode(QProcess.MergedChannels);\n      this.process.readChannel = QProcess.StandardOutput;\n    }else if (channel == \"Output\"){\n      this.process.setProcessChannelMode(QProcess.SeparateChannels);\n      this.process.readChannel = QProcess.StandardOutput;\n    }else if (channel == \"Error\"){\n      this.process.setProcessChannelMode(QProcess.SeparateChannels);\n      this.process.readChannel = QProcess.StandardError;\n    }\n  }\n});\n\n\n/**\n * kills the process instantly (useful for hanging processes, etc).\n */\n$.oProcess.prototype.kill = function(){\n  if (!this.process) return;\n  this.process.kill()\n}\n\n/**\n * Attempts to terminate the process execution by asking it to close itself.\n */\n$.oProcess.prototype.terminate = function(){\n  if (!this.process) return;\n  this.process.terminate()\n}\n\n/**\n * Execute a process and read the result as a string.\n * @param {function} [readCallback]         User can provide a function to execute when new info can be read. This function's first argument will contain the available output from the process.\n * @param {function} [finishedCallback]     User can provide a function to execute when new process has finished\n * @example\n * // This example from the openHarmony oScene.renderWriteNodes() function code\n * // uses the oProcess class to launch an async process and print its progress\n * // to the MessageLog.\n *\n * // declaring the binary called by the process\n * var harmonyBin = specialFolders.bin+\"/HarmonyPremium\";\n *\n * // building the list of arguments based on user provided input\n * var args = [\"-batch\", \"-frames\", startFrame, endFrame, \"-res\", resX, resY];\n *\n * // different arguments depending on wether the scene is stored on the database or offline\n * if (this.online){\n *   args.push(\"-env\");\n *   args.push(this.environnement);\n *   args.push(\"-job\");\n *   args.push(this.job);\n *   args.push(\"-scene\");\n *   args.push(this.name);\n * }else{\n *   args.push(this.stage);\n * }\n *\n * // Create the process with the arguments above\n * var p = new this.$.oProcess(harmonyBin, args);\n * p.readChannel = \"All\"; // specifying which channel of the process we will listen to: here we listen to both stdout and error.\n *\n * // creating an async process\n * if (renderInBackground){\n *   var length = endFrame - startFrame;\n *\n *   // Creating a function to respond to new readable information on the output channel.\n *   // This function takes a \"message\" argument which will contain the returned output of the process.\n *\n *   var progressDialogue = new this.$.oProgressDialog(\"Rendering : \",length,\"Render Write Nodes\", true);\n *   var self = this;\n *\n *   var renderProgress = function(message){\n *     // parsing the message to find a Rendered frame number.\n *     var progressRegex = /Rendered Frame ([0-9]+)/igm;\n *     var matches = [];\n *     while (match = progressRegex.exec(message)) {\n *       matches.push(match[1]);\n *     }\n *     if (matches.length!=0){\n *       // if a number is found, we compare it to the total frames in the render to deduce a completion percentage.\n *       var progress = parseInt(matches.pop(),10)\n *       progressDialogue.label = \"Rendering Frame: \"+progress+\"/\"+length\n *       progressDialogue.value = progress;\n *       var percentage = Math.round(progress/length*100);\n *       self.$.log(\"render : \"+percentage+\"% complete\");\n *     }\n *   }\n *\n *   // Creating a function that will trigger when process exits.\n *   // This function can take an \"exit code\" argument that will tell if the process terminated without problem.\n *\n *   var renderFinished = function(exitCode){\n *     // here we simply output that the render completed successfully.\n *     progressDialogue.label = \"Rendering Finished\"\n       progressDialogue.value = length;\n *     self.$.log(exitCode+\" : render finished\");\n *   }\n *\n *   // launching the process in async mode by providing true as first argument, and then the functions created above.\n *\n *   p.launchAndRead(renderProgress, renderFinished);\n *   this.$.log(\"Starting render of scene \"+this.name);\n\n * }else{\n *\n *   // if we don't want to use an async process and prefer to freeze the execution while waiting, we can simply call:\n *   var readout  = p.execute();\n * }\n *\n * // we return the output of the process in case we didn't use async.\n * return readout\n *\n */\n$.oProcess.prototype.launchAndRead = function(readCallback, finishedCallback){\n  if (typeof timeOut === 'undefined') var timeOut = -1;\n\n  var bin = this.bin.split(\"/\");\n\tvar app = bin.pop();\n\tvar directory = bin.join(\"\\\\\");\n\n  var p = this.process;\n\tp.setWorkingDirectory(directory);\n\n  this.$.debug(\"Executing Process with arguments : \"+this.bin+\" \"+this.queryArgs.join(\" \"), this.$.DEBUG_LEVEL.LOG);\n\n  // start process and attach functions to \"readyRead\" and \"finished\" signals\n  function onRead(){\n    var stdout = this.read();\n    this.readyRead.emit(stdout);\n  }\n\n  function onFinished(returnCode){\n    var stdout = this.read(); // reading any extra messages issued since last read() call to add to log\n    this.finished.emit(returnCode, this.log);\n  }\n\n  p.readyRead.connect(this, onRead);\n  p[\"finished(int)\"].connect(this, onFinished);\n\n  if (typeof readCallback !== 'undefined') this.readyRead.connect(readCallback);\n  if (typeof finishedCallback !== 'undefined') this.finished.connect(onFinished);\n\n  p.start(app, this.queryArgs);\n}\n\n\n/**\n * read the output of a process.\n * @return {string}   The lines as returned by the process since the last \"read\" instruction\n */\n$.oProcess.prototype.read = function (){\n  var p = this.process;\n  if (p.readChannel == QProcess.StandardOutput){\n    var readOut = p.readAllStandardOutput();\n  }else {\n    var readOut = p.readAllStandardError();\n  }\n\n  var output = new QTextStream(readOut).readAll();\n  while(output.slice(-1)== \"\\n\" || output.slice(-1)== \"\\r\"){\n    output = output.slice (0, -1);\n  }\n\n  this.log += output;\n\n  return output;\n}\n\n\n/**\n * Execute a process and waits for the end of the execution.\n * @return {string}   The lines as returned by the process.\n */\n$.oProcess.prototype.execute = function(){\n  this.$.debug(\"Executing Process with arguments : \"+this.bin+\" \"+this.queryArgs.join(\" \"), this.$.DEBUG_LEVEL.LOG);\n\n  var p = this.process;\n\tp.start( this.bin, this.queryArgs );\n\tp.waitForFinished(-1);\n  var result = this.read();\n  return result;\n}\n\n\n/**\n * Execute a process as a separate application, which doesn't block the script execution and stops the script from interacting with it further.\n */\n$.oProcess.prototype.launchAndDetach = function(){\n  QProcess.startDetached(this.bin, this.queryArgs);\n}\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oSignal class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for $.oSignal.\n * @name        $.oSignal\n * @classdesc\n * A Qt like custom signal that can be defined, connected and emitted.\n * As this signal is not actually threaded, the connected callbacks will be executed\n * directly when the signal is emited, and the rest of the code will execute after.\n * @constructor\n */\n$.oSignal = function(type){\n  // this.emitType = type;\n  this.connexions = [];\n  this.blocked = false;\n}\n\n\n/**\n * Register the calling object and the slot.\n * @param {object} context\n * @param {function} slot\n */\n$.oSignal.prototype.connect = function (context, slot){\n  // support slot.connect(callback) synthax\n  if (typeof slot === 'undefined'){\n    var slot = context;\n    var context = null;\n  }\n  this.connexions.push ({context: context, slot:slot});\n}\n\n\n/**\n * Remove a connection registered with this Signal.\n * @param {function} [slot] the function to disconnect from the signal. If not specified, all connexions will be removed.\n */\n$.oSignal.prototype.disconnect = function(slot){\n  if (typeof slot === \"undefined\"){\n    this.connexions = [];\n    return\n  }\n\n  for (var i in this.connexions){\n    if (this.connexions[i].slot == slot){\n      this.connexions.splice(i, 1);\n    }\n  }\n}\n\n\n/**\n * Call the slot function using the provided context and and any arguments.\n */\n$.oSignal.prototype.emit = function () {\n  if (this.blocked) return;\n\n  // if (!(value instanceof this.type)){ // can't make it work for primitives, might try to fix later?\n  //   throw new error (\"Signal can't emit type \"+ (typeof value) + \". Must be : \" + this.type)\n  // }\n\n  var args = [];\n  for (var i=0; i<arguments.length; i++){\n    args.push(arguments[i]);\n  }\n\n  this.$.debug(\"emiting signal with \"+ args, this.$.DEBUG_LEVEL.LOG);\n\n  for (var i in this.connexions){\n    var context = this.connexions[i].context;\n    var slot = this.connexions[i].slot;\n\n    // support connecting signals to each other\n    if (slot instanceof this.$.oSignal){\n      slot.emit.apply(context, args)\n    }else{\n      slot.apply(context, args);\n    }\n  }\n}\n\n\n$.oSignal.prototype.toString = function(){\n  return \"Signal\";\n}\n\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_timeline.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library v0.01\n//\n//\n//         Developped by Mathieu Chaptel, Chris Fourney...\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is garanteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the MIT license.\n//   https://opensource.org/licenses/mit\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oLayer class          //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * Constructor for $.oLayer class\n * @classdesc\n * The $.oLayer class represents a single line in the timeline.\n * @constructor\n * @param   {oTimeline}                oTimelineObject       The timeline associated to this layer.\n * @param   {int}                      layerIndex            The index of the layer on the timeline (all layers included, node and columns).\n *\n * @property {int}                     index                 The index of the layer on the timeline.\n * @property {oTimeline}               timeline              The timeline associated to this layer.\n * @property {oNode}                   node                  The node associated to the layer.\n */\n$.oLayer = function( oTimelineObject, layerIndex){\n  this.timeline = oTimelineObject;\n  this.index = layerIndex;\n}\n\n\n/**\n * The node associated to the layer.\n * @name $.oLayer#node\n * @type {$.oNode}\n */\nObject.defineProperty($.oLayer.prototype, \"node\", {\n  get: function(){\n    if (this.$.batchMode){\n      _node = this.timeline.nodes[this.index];\n    } else {\n      _node = this.$.scn.getNodeByPath(Timeline.layerToNode(this.index));\n    }\n    return _node\n  }\n})\n\n\n/**\n * the parent layer for this layer in the timeline. Returns the root group if layer is top level.\n * @name $.oLayer#parent\n * @type {$.oLayer}\n */\nObject.defineProperty($.oLayer.prototype, \"parent\", {\n  get: function(){\n    var _parentIndex = Timeline.parentNodeIndex(this.index);\n    if (_parentIndex == -1) return $.scn.root;\n    var _parent = this.timeline.allLayers[_parentIndex];\n\n    return _parent;\n  }\n})\n\n\n/**\n * wether or not the layer is selected.\n * @name $.oLayer#selected\n * @type {bool}\n * @readonly\n */\n Object.defineProperty($.oLayer.prototype, \"selected\", {\n  get: function(){\n    var selectionLength = Timeline.numLayerSel\n    for (var i=0; i<selectionLength; i++){\n      if (Timeline.selToLayer(i) == this.index) return true;\n    }\n    return false;\n  },\n  set:function(){\n    throw new Error (\"unnamed layers selection cannot be set.\")\n  }\n})\n\n\n/**\n * The name of this layer/node.\n * @name $.oLayer#name\n * @type {string}\n * @readonly\n */\n Object.defineProperty($.oLayer.prototype, \"name\", {\n  get: function(){\n    return \"unnamed layer\";\n  }\n})\n\n\n/**\n * @private\n */\n$.oLayer.prototype.toString = function(){\n  return \"<$.oLayer '\"+this.name+\"'>\";\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//       $.oNodeLayer class         //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * Constructor for $.oNodeLayer class\n * @classdesc\n * The $.oNodeLayer class represents a timeline layer corresponding to a node from the scene.\n * @constructor\n * @extends $.oLayer\n * @param   {oTimeline}                oTimelineObject       The timeline associated to this layer.\n * @param   {int}                      layerIndex            The index of the layer on the timeline.\n *\n * @property {int}                     index                 The index of the layer on the timeline.\n * @property {oTimeline}               timeline              The timeline associated to this layer.\n * @property {oNode}                   node                  The node associated to the layer.\n */\n$.oNodeLayer = function( oTimelineObject, layerIndex){\n  this.$.oLayer.apply(this, [oTimelineObject, layerIndex]);\n}\n$.oNodeLayer.prototype = Object.create($.oLayer.prototype);\n\n\n/**\n * The name of this layer/node.\n * @name $.oNodeLayer#name\n * @type {string}\n */\nObject.defineProperty($.oNodeLayer.prototype, \"name\", {\n  get: function(){\n    return this.node.name;\n  },\n  set: function(newName){\n    this.node.name = newName;\n  }\n})\n\n\n/**\n * The layer index when ignoring subLayers.\n * @name $.oNodeLayer#layerIndex\n * @type {int}\n*/\nObject.defineProperty($.oNodeLayer.prototype, \"layerIndex\", {\n  get: function(){\n    var _layers = this.timeline.layers.map(function(x){return x.node.path});\n    return _layers.indexOf(this.node.path);\n  }\n})\n\n\n/**\n * wether or not the layer is selected.\n * @name $.oNodeLayer#selected\n * @type {bool}\n */\nObject.defineProperty($.oNodeLayer.prototype, \"selected\", {\n  get: function(){\n    if ($.batchMode) return this.node.selected;\n\n    var selectionLength = Timeline.numLayerSel\n    for (var i=0; i<selectionLength; i++){\n      if (Timeline.selToLayer(i) == this.index) return true;\n    }\n    return false;\n  },\n  set: function(selected){\n    this.node.selected = selected;\n  }\n})\n\n\n/**\n * The column layers associated with this node.\n * @name $.oNodeLayer#subLayers\n * @type {$.oColumnLayer[]}\n*/\nObject.defineProperty($.oNodeLayer.prototype, \"subLayers\", {\n  get: function(){\n    var _node = this.node;\n    var _nodeLayerType = this.$.oNodeLayer;\n    return this.timeline.allLayers.filter(function (x){return x.node.path == _node.path && !(x instanceof _nodeLayerType)});\n  }\n})\n\n\n\n/**\n * @private\n */\n$.oNodeLayer.prototype.toString = function(){\n  return \"<$.oNodeLayer '\"+this.name+\"'>\";\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//      $.oDrawingLayer class       //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n/**\n * Constructor for $.oDrawingLayer class\n * @classdesc\n * The $.oDrawingLayer class represents a timeline layer corresponding to a 'READ' node (or Drawing in Toonboom UI) from the scene.\n * @constructor\n * @extends $.oNodeLayer\n * @param   {oTimeline}                oTimelineObject       The timeline associated to this layer.\n * @param   {int}                      layerIndex            The index of the layer on the timeline.\n *\n * @property {int}                     index                 The index of the layer on the timeline.\n * @property {oTimeline}               timeline              The timeline associated to this layer.\n * @property {oNode}                   node                  The node associated to the layer.\n */\n$.oDrawingLayer = function( oTimelineObject, layerIndex){\n  this.$.oNodeLayer.apply(this, [oTimelineObject, layerIndex]);\n}\n$.oDrawingLayer.prototype = Object.create($.oNodeLayer.prototype);\n\n\n/**\n * The oFrame objects that hold the drawings for this layer.\n * @name oDrawingLayer#drawingColumn\n * @type {oFrame[]}\n */\n Object.defineProperty($.oDrawingLayer.prototype, \"drawingColumn\", {\n  get: function(){\n    return this.node.attributes.drawing.elements.column;\n  }\n})\n\n\n/**\n * The oFrame objects that hold the drawings for this layer.\n * @name oDrawingLayer#exposures\n * @type {oFrame[]}\n */\nObject.defineProperty($.oDrawingLayer.prototype, \"exposures\", {\n  get: function(){\n    return this.drawingColumn.frames;\n  }\n})\n\n\n/**\n * @private\n */\n $.oDrawingLayer.prototype.toString = function(){\n  return \"<$.oDrawingLayer '\"+this.name+\"'>\";\n}\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//       $.oColumnLayer class       //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n/**\n * Constructor for $.oColumnLayer class\n * @classdesc\n * The $.oColumnLayer class represents a timeline layer corresponding to the animated values of a column linked to a node.\n * @constructor\n * @extends $.oLayer\n * @param   {oTimeline}                oTimelineObject       The timeline associated to this layer.\n * @param   {int}                      layerIndex            The index of the layer on the timeline.\n *\n * @property {int}                     index                 The index of the layer on the timeline.\n * @property {oTimeline}               timeline              The timeline associated to this layer.\n * @property {oNode}                   node                  The node associated to the layer.\n */\n$.oColumnLayer = function( oTimelineObject, layerIndex){\n  this.$.oLayer.apply(this, [oTimelineObject, layerIndex]);\n}\n$.oColumnLayer.prototype = Object.create($.oLayer.prototype);\n\n\n/**\n * The name of this layer.\n * (corresponding to the display name of the column, not the name displayed in timeline, not exposed by the Toonboom API).\n * @name $.oColumnLayer#name\n * @type {string}\n */\nObject.defineProperty($.oColumnLayer.prototype, \"name\", {\n  get: function(){\n    return this.column.name;\n  }\n})\n\n\n\n/**\n * the node attribute associated with this layer. Only available if the attribute has a column.\n * @name $.oColumnLayer#attribute\n * @type {$.oColumn}\n */\nObject.defineProperty($.oColumnLayer.prototype, \"attribute\", {\n  get: function(){\n    if (!this._attribute){\n      this._attribute = this.column.attributeObject;\n    }\n    return this._attribute\n  }\n})\n\n\n\n/**\n * the node associated with this layer\n * @name $.oColumnLayer#column\n * @type {$.oColumn}\n */\nObject.defineProperty($.oColumnLayer.prototype, \"column\", {\n  get: function(){\n    if (!this._column){\n      var _name = Timeline.layerToColumn(this.index);\n      var _attribute = this.node.getAttributeByColumnName(_name);\n      this._column = _attribute.column;\n    }\n    return this._column;\n  }\n})\n\n\n/**\n * The layer representing the node to which this column is linked\n */\nObject.defineProperty($.oColumnLayer.prototype, \"nodeLayer\", {\n  get: function(){\n    var _node = this.node;\n    var _nodeLayerType = this.$.oNodeLayer;\n    this.timeline.allLayers.filter(function (x){return x.node == _node && x instanceof _nodeLayerType})[0];\n  }\n})\n\n\n\n/**\n * @private\n */\n $.oColumnLayer.prototype.toString = function(){\n  return \"<$.oColumnLayer '\"+this.name+\"'>\";\n}\n\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//         $.oTimeline class        //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The $.oTimeline constructor.\n * @constructor\n * @classdesc  The $.oTimeline class represents a timeline corresponding to a specific display.\n * @param   {string}      [display]  The display node's path. By default, the defaultDisplay of the scene.\n *\n * @property {string}     display    The display node's path.\n */\n$.oTimeline = function(display){\n  if (typeof display === 'undefined') var display = this.$.scn.defaultDisplay;\n  if (display instanceof this.$.oNode) display = display.path;\n\n  this.display = display;\n}\n\n\n/**\n * Gets the list of node layers in timeline.\n * @name $.oTimeline#layers\n * @type {$.oLayer[]}\n */\nObject.defineProperty($.oTimeline.prototype, 'layers', {\n  get : function(){\n    var nodeLayer = this.$.oNodeLayer;\n    return this.allLayers.filter(function (x){return x instanceof nodeLayer})\n  }\n});\n\n\n/**\n * Gets the list of all layers in timeline, nodes and columns. In batchmode, will only return the nodes, not the sublayers.\n * @name $.oTimeline#allLayers\n * @type {$.oLayer[]}\n */\nObject.defineProperty($.oTimeline.prototype, 'allLayers', {\n  get : function(){\n    if (!this._layers){\n      var _layers = [];\n\n      if (!$.batchMode){\n        for( var i=0; i < Timeline.numLayers; i++ ){\n          if (Timeline.layerIsNode(i)){\n            var _layer = new this.$.oNodeLayer(this, i);\n            if (_layer.node.type == \"READ\") var _layer = new this.$.oDrawingLayer(this, i);\n          }else if (Timeline.layerIsColumn(i)) {\n            var _layer = new this.$.oColumnLayer(this, i);\n          }else{\n            var _layer = new this.$.oLayer(this, i);\n          }\n          _layers.push(_layer);\n        }\n      } else {\n        var _tl = this;\n        var _layers = this.nodes.map(function(x, index){\n          if (x.type == \"READ\") return new _tl.$.oDrawingLayer(_tl, index);\n          return new _tl.$.oNodeLayer(_tl, index)\n        })\n      }\n\n      this._layers = _layers;\n    }\n    return this._layers;\n  }\n});\n\n\n/**\n * Gets the list of selected layers as oTimelineLayer objects.\n * @name $.oTimeline#selectedLayers\n * @type {oTimelineLayer[]}\n */\nObject.defineProperty($.oTimeline.prototype, 'selectedLayers', {\n  get : function(){\n    return this.allLayers.filter(function(x){return x.selected});\n  }\n});\n\n\n\n\n/**\n * The node layers in the scene, based on the timeline's order given a specific display.\n * @name $.oTimeline#compositionLayers\n * @type {oNode[]}\n * @deprecated use oTimeline.nodes instead if you want the nodes\n */\nObject.defineProperty($.oTimeline.prototype, 'compositionLayers', {\n  get : function(){\n    return this.nodes;\n  }\n});\n\n\n/**\n * The nodes present in the timeline.\n * @name $.oTimeline#nodes\n * @type {oNode[]}\n */\nObject.defineProperty($.oTimeline.prototype, 'nodes', {\n  get : function(){\n    var _timeline = this.compositionLayersList;\n    var _scene = this.$.scene;\n\n    _timeline = _timeline.map( function(x){return _scene.getNodeByPath(x)} );\n\n    return _timeline;\n  }\n});\n\n\n/**\n * Gets the paths of the nodes displayed in the timeline.\n * @name $.oTimeline#nodesList\n * @type {string[]}\n * @deprecated only returns node path strings, use oTimeline.layers insteads\n */\nObject.defineProperty($.oTimeline.prototype, 'nodesList', {\n  get : function(){\n    return this.compositionLayersList;\n  }\n});\n\n\n/**\n * Gets the paths of the layers in order, given the specific display's timeline.\n * @name $.oTimeline#compositionLayersList\n * @type {string[]}\n * @deprecated only returns node path strings\n */\nObject.defineProperty($.oTimeline.prototype, 'compositionLayersList', {\n  get : function(){\n    var _composition = this.composition;\n    var _timeline = _composition.map(function(x){return x.node})\n\n    return _timeline;\n  }\n});\n\n\n/**\n * gets the composition for this timeline (array of native toonboom api 'compositionItems' objects)\n * @deprecated exposes native harmony api objects\n */\nObject.defineProperty($.oTimeline.prototype, \"composition\", {\n  get: function(){\n    return compositionOrder.buildCompositionOrderForDisplay(this.display);\n  }\n})\n\n\n\n/**\n * Refreshes the oTimeline's cached listing- in the event it changes in the runtime of the script.\n * @deprecated oTimeline.composition is now always refreshed when accessed.\n */\n$.oTimeline.prototype.refresh = function( ){\n  if (!node.type(this.display)) {\n      this.composition = compositionOrder.buildDefaultCompositionOrder();\n  }else{\n      this.composition = compositionOrder.buildCompositionOrderForDisplay(this.display);\n  }\n}\n\n\n/**\n * Build column to oNode/Attribute lookup cache. Makes the layer generation faster if using oTimeline.layers, oTimeline.selectedLayers\n * @deprecated\n */\n$.oTimeline.prototype.buildLayerCache = function( forced ){\n  if (typeof forced === 'undefined') forced = false;\n\n  var cdate   = (new Date).getTime();\n  var rebuild = forced;\n  if( !this.$.cache_columnToNodeAttribute_date ){\n    rebuild = true;\n  }else if( !rebuild ){\n    if( ( cdate - this.$.cache_columnToNodeAttribute_date ) > 1000*10 ){\n      rebuild = true;\n    }\n  }\n\n  if(rebuild){\n    var nodeLayers = this.compositionLayers;\n\n    if( this.$.cache_nodeAttribute ){\n      this.$.cache_columnToNodeAttribute = {};\n    }\n\n    for( var n=0;n<nodeLayers.length;n++ ){\n      this.$.cache_columnToNodeAttribute    = nodeLayers[n].getAttributesColumnCache( this.$.cache_columnToNodeAttribute );\n    }\n    this.$.cache_columnToNodeAttribute_date = cdate;\n  }\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_tool.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developped by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is garanteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $.oTool class           //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * The constructor for the $.oTool class\n * @constructor\n * @classdec\n * The $.oTool Class describes a single tool available. It can be activated through this class.\n * @param {int}   id     The id of the tool\n * @param {name}  name   The name of the tool\n * @property {int}   id     The id of the tool\n * @property {name}  name   The name of the tool\n * @example\n * // Access the list of currently existing tools by using the $.app object\n * var tools = $.app.tools;\n * \n * // output the list of tools names and ids\n * for (var i in tools){\n *   log(i+\" \"+tools[i].name)\n * }\n * \n * // To get a tool by name, use the $.app.getToolByName() function\n * var brushTool = $.app.getToolByName(\"Brush\");\n * log (brushTool.name+\" \"+brushTool.id)            // Output: Brush 9\n * \n * // it's also possible to activate a tool in several ways:\n * $.app.currentTool = 9;         // using the tool \"id\"\n * $.app.currentTool = brushTool  // by passing a oTool object\n * $.app.currentTool = \"Brush\"    // using the tool name\n * \n * brushTool.activate()           // by using the activate function of the oTool class\n */\n$.oTool = function(id, name){\n  this.id = id;\n  this.name = name;\n}\n\n\n/**\n * The list of stencils this tool can use. Not currently supported by custom tools.\n * @name $.oTool#stencils\n * @type {$.oStencil[]}\n */\nObject.defineProperty($.oTool, \"stencils\", {\n  get: function(){\n    // an object describing what tool can use what stencils\n    var _stencilTypes = {\n      pencil:[\"Ellipse\", \"Line\", \"Pencil\", \"Polyline\", \"Rectangle\"],\n      penciltemplate:[\"Ellipse\", \"Line\", \"Pencil\", \"Polyline\", \"Rectangle\"],\n      brush:[\"Brush\"],\n      texture:[\"Brush\"],\n      bitmapbrush:[\"Brush\"],\n      bitmaperaser:[\"Eraser\"]\n    }\n\n    var _stencils = this.$.app.stencils;\n    var stencilsTypeList = [];\n    var stencilsList = [];\n\n    for (var i in _stencilTypes){\n      if (_stencilTypes[i].indexOf(this.name) != -1) stencilsTypeList.push(i);\n    }\n\n    for (var i in _stencils){\n      if (stencilsTypeList.indexOf(_stencils[i].type) != -1) stencilsList.push(_stencils[i]);\n    }\n\n    return stencilsList;\n  }\n})\n\n/**\n * Activates the tool.\n */\n$.oTool.prototype.activate = function(){\n  Tools.setToolSettings({currentTool:{id:this.id}});\n}\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_toolInstall.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>OpenHarmonyToolInstaller</class>\n <widget class=\"QWidget\" name=\"OpenHarmonyToolInstaller\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>547</width>\n    <height>642</height>\n   </rect>\n  </property>\n  <property name=\"windowTitle\">\n   <string>OpenHarmony Tool Installer</string>\n  </property>\n  <layout class=\"QHBoxLayout\" name=\"horizontalLayout\">\n   <item>\n    <widget class=\"QGroupBox\" name=\"toolListGroupBox\">\n     <property name=\"sizePolicy\">\n      <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n       <horstretch>3</horstretch>\n       <verstretch>0</verstretch>\n      </sizepolicy>\n     </property>\n     <property name=\"title\">\n      <string>Available Tools</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\n      <item>\n       <widget class=\"QComboBox\" name=\"branchCombo\"/>\n      </item>\n      <item>\n       <widget class=\"QListWidget\" name=\"installList\"/>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QGroupBox\" name=\"detailGroupbox\">\n     <property name=\"sizePolicy\">\n      <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n       <horstretch>4</horstretch>\n       <verstretch>0</verstretch>\n      </sizepolicy>\n     </property>\n     <property name=\"title\">\n      <string>Details</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout_4\">\n      <item>\n       <widget class=\"QTextEdit\" name=\"detailArea\">\n        <property name=\"sizePolicy\">\n         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Expanding\">\n          <horstretch>0</horstretch>\n          <verstretch>0</verstretch>\n         </sizepolicy>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"installLabel\">\n        <property name=\"text\">\n         <string/>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QPushButton\" name=\"installButton\">\n        <property name=\"text\">\n         <string>INSTALL/REMOVE</string>\n        </property>\n       </widget>\n      </item>\n     </layout>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library\n//\n//\n//         Developed by Mathieu Chaptel, Chris Fourney\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the Mozilla Public license 2.0.\n//   https://www.mozilla.org/en-US/MPL/2.0/\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//          $ (DOM) class           //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n\n/**\n * All the classes can be accessed from it, and it can be passed to a different context.\n * @namespace\n * @classdesc The $ global object that holds all the functions of openHarmony.\n * @property {int}     debug_level               The debug level of the DOM.\n * @property {bool}    batchMode                 Deactivate all ui and incompatible functions to ensure scripts run in batch.\n * @property {string}  file                      The openHarmony base file - THIS!\n *\n * @property {$.oScene}    getScene                  The harmony scene.\n * @property {$.oScene}    scene                     The harmony scene.\n * @property {$.oScene}    scn                       The harmony scene.\n * @property {$.oScene}    s                         The harmony scene.\n * @property {$.oApp}      getApplication            The Harmony Application Object.\n * @property {$.oApp}      application               The Harmony Application Object.\n * @property {$.oApp}      app                       The Harmony Application Object.\n * @property {$.oNetwork}  network                   Access point for all the functions of the $.oNetwork class\n * @property {$.oUtils}    utils                     Access point for all the functions of the $.oUtils class\n * @property {$.oDialog}   dialog                    Access point for all the functions of the $.oDialog class\n * @property {Object}      global                    The global scope.\n *\n * @example\n * // To access the functions, first call the $ object. It is made available after loading openHarmony like so:\n *\n * include (\"openHarmony.js\");\n *\n * var doc = $.scn;                    // grabbing the scene document\n * $.log(\"hello\");                     // prints out a message to the MessageLog.\n * var myPoint = new $.oPoint(0,0,0);  // create a new class instance from an openHarmony class.\n *\n * // function members of the $ objects get published to the global scope, which means $ can be omitted\n *\n * log(\"hello\");\n * var myPoint = new oPoint(0,0,0);    // This is all valid\n * var doc = scn;                      // \"scn\" isn't a function so this one isn't\n *\n */\n$ = {\n  debug_level : 0,\n\n /**\n * Enum to set the debug level of debug statements.\n * @name    $#DEBUG_LEVEL\n * @enum\n */\n  DEBUG_LEVEL : {\n                 'ERROR'   : 0,\n                 'WARNING' : 1,\n                 'LOG'     : 2\n                },\n  file      : __file__,\n  directory : false,\n  pi        : 3.14159265359\n};\n\n\n/**\n * The openHarmony main Install directory\n * @name $#directory\n * @type {string}\n */\nObject.defineProperty( $, \"directory\", {\n  get : function(){\n    var currentFile = __file__\n    return currentFile.split(\"\\\\\").join(\"/\").split( \"/\" ).slice(0, -1).join('/');\n  }\n});\n\n\n/**\n * Whether Harmony is run with the interface or simply from command line\n */\nObject.defineProperty( $, \"batchMode\", {\n  get: function(){\n    // use a cache to avoid pulling the widgets every time\n    if (!this.hasOwnProperty(\"_batchMode\")){\n      this._batchMode = true;\n\n      // batchmode is false if there are any widgets visible in the application\n      var _widgets = QApplication.topLevelWidgets();\n      for (var i in _widgets){\n        if (_widgets[i].visible) this._batchMode = false;\n      }\n    }\n    return this._batchMode\n  }\n})\n\n/**\n * Function to load openHarmony files from the %installdir%/openHarmony/ folder.\n * @name $#loadOpenHarmonyFiles\n * @private\n */\nvar _ohDirectory = $.directory+\"/openHarmony/\";\nvar _dir = new QDir(_ohDirectory);\n_dir.setNameFilters([\"openHarmony*.js\"]);\n_dir.setFilter( QDir.Files);\nvar _files = _dir.entryList();\n\nfor (var i in _files){\n  include( _ohDirectory + \"/\" + _files[i]);\n}\n\n\n\n\n/**\n * The standard debug that uses logic and level to write to the messagelog. Everything should just call this to write internally to a log in OpenHarmony.\n * @function\n * @name    $#debug\n * @param   {obj}   obj            Description.\n * @param   {int}   level          The debug level of the incoming message to log.\n */\n$.debug = function( obj, level ){\n  if( level > this.debug_level ) return;\n\n  try{\n    if (typeof obj !== 'object') throw new Error();\n    this.log(JSON.stringify(obj));\n  }catch(err){\n    this.log(obj);\n  }\n}\n\n\n/**\n * Log the string to the MessageLog.\n * @function\n * @name    $#log\n * @param {string}  str            Text to log.\n */\n$.log = function( str ){\n  MessageLog.trace( str );\n  System.println( str );\n}\n\n\n/**\n * Log the object and its contents.\n * @function\n * @name    $#logObj\n * @param   {object}   object            The object to log.\n * @param   {int}      debugLevel        The debug level.\n */\n$.logObj = function( object ){\n  for (var i in object){\n    try {\n      if (typeof object[i] === \"function\") continue;\n      $.log(i+' : '+object[i])\n      if (typeof object[i] == \"Object\"){\n        $.log(' -> ')\n        $.logObj(object[i])\n        $.log(' ----- ')\n      }\n    }catch(error){}\n  }\n}\n\n\n//---- App  --------------\n$.app = new $.oApp();\n$.application = $.app;\n$.getApplication = $.app;\n\n\n//---- Scene  --------------\n$.s     = new $.oScene();\n$.scn   = $.s;\n$.scene = $.s;\n$.getScene = $.s;\n\n\n/**\n * Prompts with a confirmation dialog (yes/no choice).\n * @function\n * @name    $#confirm\n * @param   {string}           [labelText]                    The label/internal text of the dialog.\n * @param   {string}           [title]                        The title of the confirmation dialog.\n * @param   {string}           [okButtonText]                 The text on the OK button of the dialog.\n * @param   {string}           [cancelButtonText]             The text on the CANCEL button of the dialog.\n *\n * @return  {bool}       Result of the confirmation dialog.\n */\n$.confirm = function(){ return $.dialog.confirm.apply( $.dialog, arguments ) };\n\n\n/**\n * Prompts with an alert dialog (informational).\n * @function\n * @name    $#alert\n * @param   {string}           [labelText]                    The label/internal text of the dialog.\n * @param   {string}           [title]                        The title of the confirmation dialog.\n * @param   {string}           [okButtonText]                 The text on the OK button of the dialog.\n */\n$.alert = function(){ return $.dialog.alert.apply( $.dialog, arguments ) };\n\n\n\n/**\n * Prompts with an alert dialog with a text box which can be selected (informational).\n * @function\n * @name    $#alertBox\n * @param   {string}           [labelText]                    The label/internal text of the dialog.\n * @param   {string}           [title]                        The title of the confirmation dialog.\n * @param   {string}           [okButtonText]                 The text on the OK button of the dialog.\n */\n$.alertBox = function(){ return $.dialog.alertBox.apply( $.dialog, arguments ) };\n\n\n\n/**\n * Prompts with an toast alert. This is a small message that can't be clicked and only stays on the screen for the duration specified.\n * @function\n * @name    $#toast\n * @param   {string}         labelText          The label/internal text of the dialog.\n * @param   {$.oPoint}       [position]         The position on the screen where the toast will appear (by default, slightly under the middle of the screen).\n * @param   {float}          [duration=2000]    The duration of the display (in milliseconds).\n * @param   {$.oColorValue}  [color=\"#000000\"]  The color of the background (a 50% alpha value will be applied).\n */\n$.toast = function(){ return $.dialog.toast.apply( $.dialog, arguments ) };\n\n\n\n/**\n * Prompts for a user input.\n * @function\n * @name    $#prompt\n * @param   {string}           [labelText]                    The label/internal text of the dialog.\n * @param   {string}           [title]                        The title of the confirmation dialog.\n * @param   {string}           [prefilledText]                The text to display in the input area.\n */\n$.prompt = function(){ return $.dialog.prompt.apply( $.dialog, arguments ) };\n\n\n/**\n * Prompts with a file selector window\n * @function\n * @name $#browseForFile\n * @param {string} [text=\"Select a file:\"] The title of the file select dialog.\n * @param {string} [filter=\"*\"]            The filter for the file type and/or file name that can be selected. Accepts wildcard character \"*\".\n * @param {string} [getExisting=true]      Whether to select an existing file or a save location\n * @param {string} [acceptMultiple=false]  Whether or not selecting more than one file is ok. Is ignored if getExisting is false.\n * @param {string} [startDirectory]        The directory showed at the opening of the dialog.\n *\n * @return  {string[]}         The list of selected Files, 'undefined' if the dialog is cancelled\n */\n$.browseForFile = function(){ return $.dialog.browseForFile.apply( $.dialog, arguments ) };\n\n\n/**\n * Prompts with a folder selector window.\n * @function\n * @name $#browseForFolder\n * @param {string} [text]                The title of the confirmation dialog.\n * @param {string} [startDirectory]      The directory showed at the opening of the dialog.\n *\n * @return  {string}         The path of the selected folder, 'undefined' if the dialog is cancelled\n */\n$.browseForFolder = function(){ return $.dialog.browseForFolder.apply( $.dialog, arguments ) };\n\n\n/**\n * Gets access to a widget from the Harmony Interface.\n * @function\n * @name    $#getHarmonyUIWidget\n * @param   {string}   name              The name of the widget to look for.\n * @param   {string}   [parentName]      The name of the parent widget to look into, in case of duplicates.\n */\n$.getHarmonyUIWidget = function(){ return $.app.getWidgetByName.apply( $.app, arguments ) }\n\n\n//---- Cache Helpers ------\n$.cache_columnToNodeAttribute = {};\n$.cache_columnToNodeAttribute_date = (new Date()).getTime();\n$.cache_oNode = {};\n\n\n//------------------------------------------------\n//-- Undo operations\n\n/**\n * Starts the tracking of the undo accumulation, all subsequent actions are done in a single undo operation.<br>Close the undo accum with $.endUndo().\n * If this function is called multiple time, only the first time will count.\n * (this prevents small functions wrapped in their own undo block to interfere with global script undo)\n * @param   {string}           undoName        The name of the operation that is being done in the undo accum.\n * @name $#beginUndo\n * @function\n * @see $.endUndo\n */\n$.beginUndo = function( undoName ){\n  if ($.batchMode) return\n  if (typeof undoName === 'undefined') var undoName = ''+((new Date()).getTime());\n  if (!$.hasOwnProperty(\"undoStackSize\")) $.undoStackSize = 0;\n  if ($.undoStackSize == 0) scene.beginUndoRedoAccum( undoName );\n  $.undoStackSize++;\n}\n\n/**\n * Cancels the tracking of the undo accumulation, everything between this and the start of the accumulation is undone.\n * @name $#cancelUndo\n * @function\n */\n$.cancelUndo = function( ){\n  scene.cancelUndoRedoAccum( );\n}\n\n/**\n * Stops the tracking of the undo accumulation, everything between this and the start of the accumulation behaves as a single undo operation.\n * If beginUndo function is called multiple time, each call must be matched with this function.\n * (this prevents small functions wrapped in their own undo block to interfere with global script undo)\n * @name $#endUndo\n * @function\n * @see $.beginUndo\n */\n$.endUndo = function( ){\n  if ($.batchMode) return\n\n  if (!$.hasOwnProperty(\"undoStackSize\")) $.undoStackSize = 1;\n  $.undoStackSize--;\n  if ($.undoStackSize == 0)   scene.endUndoRedoAccum();\n}\n\n/**\n * \tUndoes the last n operations. If n is not specified, it will be 1\n * @name $#undo\n * @function\n * @param   {int}           dist                                    The amount of operations to undo.\n */\n$.undo = function( dist ){\n  if (typeof dist === 'undefined'){ var dist = 1; }\n  scene.undo( dist );\n}\n\n/**\n * \tRedoes the last n operations. If n is not specified, it will be 1\n * @name $#redo\n * @function\n * @param   {int}           dist                                    The amount of operations to undo.\n */\n$.redo = function( dist ){\n  if (typeof dist === 'undefined'){ var dist = 1; }\n  scene.redo( dist );\n}\n\n\n/**\n * \tGets the preferences from the Harmony stage.\n * @name $#getPreferences\n * @function\n */\n$.getPreferences = function( ){\n  return new $.oPreferences();\n}\n\n//---- Attach Helpers ------\n$.network     = new $.oNetwork();\n$.utils       = $.oUtils;\n$.dialog      = new $.oDialog();\n$.global      = this;\n\n\n//---- Self caching -----\n\n/**\n * change this value to allow self caching across openHarmony when initialising objects.\n * @name $#useCache\n * @type {bool}\n */\n$.useCache = false;\n\n\n/**\n * function to call in constructors of classes so that instances of this class\n * are cached and unique based on constructor arguments.\n * @returns a cached class instance or null if no cached instance exists.\n */\n$.getInstanceFromCache = function(){\n  if (!this.__proto__.hasOwnProperty(\"__cache__\")) {\n    this.__proto__.__cache__ = {};\n  }\n  var _cache = this.__proto__.__cache__;\n\n  if (!this.$.useCache) return;\n\n  var key = [];\n  for (var i=0; i<arguments.length; i++){\n    try{\n      key.push(arguments[i]+\"\")\n    }catch(err){} // ignore arguments that can't be converted to string\n  }\n\n  if (_cache.hasOwnProperty(key)) {\n    this.$.log(\"instance returned from cache for \"+key, this.$.DEBUG_LEVEL.ERROR)\n    return _cache[key];\n  }\n  this.$.log(\"creating new instance for \"+key, this.$.DEBUG_LEVEL.ERROR)\n  _cache[key] = this;\n\n  this.constructor.invalidateCache = function(){\n    delete this.prototype.__cache__;\n  }\n  return\n}\n\n\n/**\n * invalidate all cache for classes that are self caching.\n * Will be run at each include('openHarmony.js') statement.\n */\n$.clearOpenHarmonyCache = function(){\n  // clear cache at openHarmony loading.\n  for (var classItem in this.$){\n    var ohClass = this.$[classItem]\n    if (typeof ohClass === \"function\" && ohClass.prototype.hasOwnProperty('__cache__')){\n      ohClass.invalidateCache();\n    }\n  }\n}\n$.clearOpenHarmonyCache();\n\n\n//---- Instantiate Class $ DOM Access ------\nfunction addDOMAccess( target, item ){\n  Object.defineProperty( target, '$', {\n    configurable: false,\n    enumerable: false,\n    value: item\n  });\n}\n\n//Add the context as a local member of the classes.\nfor( var classItem in $ ){\n  if( ( typeof $[classItem] ) == \"function\" ){\n    try{\n      addDOMAccess( $[classItem].prototype, $ );\n    }catch(err){\n      $.debug( \"Error extending DOM access to : \" + classItem + \": \"+err, $.DEBUG_LEVEL.ERROR );\n    }\n\n    //Also extend it to the global object.\n    this[classItem] = $[classItem];\n  }\n}\n\n\n// Add global access to $ object\nthis.__proto__.$ = $"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony_install.js",
    "content": "function openHarmony_install_main(){\n  try{\n    var d = new Dialog();\n    d.title = \"Install/Update OpenHarmony\";\n    d.cancelButtonText = \"Cancel\";\n    \n    //Standard install paths.\n    var install_base = specialFolders.userScripts;\n    var library_base = install_base;\n    \n    var libdir = new Dir(library_base);\n    var label = new Label;\n        label.text = \"Install openHarmony libraries?\\n\";\n    \n    d.add( label );\n    \n    if ( !d.exec() ){\n      return;\n    }\n    \n    if( !libdir.exists ){\n      MessageBox.information( \"Failed to create folders for openHarmony - please check permissions.\" );\n    }\n    \n    var apic = api_call( \"https://api.github.com/repos/cfourney/OpenHarmony/branches\" );\n    if( !apic ){\n      MessageBox.information( \"API Error - Failed to get available branches.\" );\n      return;\n    }\n    \n    var branch_names = apic.map(function(x, index){return index+\": \"+x.name.toUpperCase()})\n    \n    var res = Input.getItem( \"Which Branch\", branch_names );\n    if( !res ){ return; }\n\n    var branch_data = false;\n    var fnd = false;\n    for( var x=0;x<apic.length;x++ ){\n      var fname = (x+1)+\": \"+apic[x].name.toUpperCase();\n      if( fname == res ){\n        \n        fnd = apic[x][\"commit\"][\"sha\"];\n        \n        branch_data = apic[x].name.toUpperCase();        \n        branch_data = branch_data + \"\\n\" + fnd;\n        branch_data = branch_data + \"\\n\" + new Date();\n        break;\n      }\n    }\n    \n    if( !fnd ){\n      MessageBox.information( \"Error - This shouldn't have happened.\" );\n      return;\n    }\n    \n    var contents_url = \"https://api.github.com/repos/cfourney/OpenHarmony/contents?ref=\"+fnd;\n    \n    var apic = api_call( contents_url );\n    if( !apic ){\n      MessageBox.information( \"API Error - Failed to get available branch content.\" );\n      return;\n    }\n    \n    if( apic && apic.length ){\n      //Download the files.\n      var progress = new QProgressDialog();\n      progress.setLabelText( \"Downloading files...\" );\n      progress.show();\n      progress.setRange( 0, 100 );\n      \n      var file_download_listing = recurse_files( apic, [] );\n      progress.setRange( 0, file_download_listing.length );\n\n      \n      \n      var oh_install = specialFolders.userScripts + \"/\" + \"openHarmony_install\";\n      var libdir = new Dir( oh_install );\n      if( !libdir.exists ) libdir.mkdirs();\n\n      if( libdir.exists ){\n        var oh_install_file = oh_install + \"/\" + \"OpenHarmony\";\n        \n        var file = new File( oh_install_file );\n        try {\n          var content = [ fnd ];\n          for( var n=0;n<file_download_listing.length;n++ ){\n            var path    = file_download_listing[n][1];\n            var local_path = library_base+'/'+path;\n            \n            content.push( local_path );\n          }\n          \n          file.open(FileAccess.WriteOnly);\n          file.write( content.join(\"\\n\") );\n          file.close();\n        } catch (err) {}\n      }\n      \n      \n      for( var n=0;n<file_download_listing.length;n++ ){\n        progress.setValue( n );\n        QCoreApplication.processEvents();\n        \n        var raw_url = file_download_listing[n][0];\n        var path    = file_download_listing[n][1];\n        \n        var local_path = library_base+'/'+path;\n        \n        if( path.slice( 0, path.indexOf(\"/\") ) == \"tools\" ){\n          //Dont include the tools directory, these will be installed as needed.\n          continue;\n        }\n\n        if( path.slice( 0, path.indexOf(\"/\") ) == \"docs\" ){\n          //Dont include the docs directory, these will be installed as needed.\n          continue;\n        }\n\n        \n        var local_dir  = local_path.slice( 0, local_path.lastIndexOf(\"/\") );\n        \n        var libdir = new Dir(local_dir);\n        if( !libdir.exists ){\n          libdir.mkdirs();\n        }\n        \n        if( !libdir.exists ){\n          MessageBox.information( \"Failed to create openHarmony directory: \" + local_dir );\n          progress.accept();\n          return;\n        }\n        \n        //Override path of active scripts to ensure they're accessible to the user.\n        //----------------------------------------------------------------------------\n        var downloaded = download( raw_url, local_path );\n        if( !downloaded ){\n          MessageBox.information( \"Failed to create openHarmony file: \" + local_path );\n          progress.accept();\n          return;\n        }\n      }\n      \n      progress.accept();\n      \n      if( (new File( local_dir + \"/\" + \"openHarmony.js\" )).exists ){\n        MessageBox.information( \"OpenHarmony successfully installed.\" );\n        preferences.setString( 'openHarmonyInclude', ( local_dir + \"/\" + \"openHarmony.js\" ) );\n        preferences.setString( 'openHarmonyPath', ( local_dir ) );\n        \n        try {\n          var install_path = local_dir + \"/\" + \"INSTALL\";\n          var file = new File(install_path);\n          file.open(FileAccess.Append);\n          file.write( branch_data );\n          file.close();\n        }catch (err){\n        }\n      }else{\n        MessageBox.information( \"Unable to find OpenHarmony in install path: \" + (local_dir + \"/\" + \"openHarmony.js\") );\n      }\n    }\n  }catch( err ){\n    MessageBox.information( \"Failed to install: \" + err + \" (\"+ err.lineNumber +\")\" );\n  }\n  \n  \n  function recurse_files( contents, arr_files ){\n    for( var n=0;n<contents.length;n++ ){\n      var tfl = contents[n];\n      \n      if( contents[n].type == \"file\" ){\n        arr_files.push( [tfl[\"download_url\"], tfl[\"path\"]] );\n        \n      }else if( contents[n].type == \"dir\" ){\n        //Get more contents.\n        QCoreApplication.processEvents();\n        var apic = api_call( tfl[\"url\"] );\n        if( apic ){\n          arr_files = recurse_files( apic, arr_files );\n        }else{\n          return false;\n        }\n      }\n    }\n    \n    return arr_files;\n  }\n\n\n  function api_call( address ){\n    try{\n    \n      var avail_paths = [ \n                          \"c:\\\\Windows\\\\System32\\\\curl.exe\"\n                        ];\n      if( !about.isWindowsArch() ){\n        avail_paths = [ \n                        \"/usr/bin/curl\",\n                        \"/usr/local/bin/curl\"\n                      ];\n      }\n      \n      var curl_path = false;\n      for( var n=0;n<avail_paths.length;n++ ){\n        if( ( new File(avail_paths[n]) ).exists ){\n          curl_path = avail_paths[n];\n          break;\n        }\n      }\n\n      if (!curl_path){\n        MessageBox.information(\"curl is required and is not found on this machine. Install curl to continue.\\n\\n Download available at : \"+(about.isWindowsArch?\"https://curl.haxx.se/windows/\":\"https://curl.haxx.se/download.html\"))\n        return false;\n      }\n\n      var cmdline = [ \"-L\", address ];\n      \n      var p = new QProcess();\n      // p.setWorkingDirectory( pathToTemplate ); \n      p.start( curl_path, cmdline );  \n      p.waitForFinished( 10000 );\n      \n      try{\n        var readOut = JSON.parse((new QTextStream(p.readAllStandardOutput())).readAll());\n        return readOut;\n      }catch(err){\n        return false;\n      }    \n    }catch( err ){\n      System.println( err + \" (\"+address.lineNumber+\")\" );\n      MessageLog.trace( err + \" (\"+address.lineNumber+\")\" );\n    }\n  }\n\n\n  function download( address, target ){\n    try{\n    \n      var avail_paths = [ \n                          \"c:\\\\Windows\\\\System32\\\\curl.exe\"\n                       ];\n      if( !about.isWindowsArch() ){\n        avail_paths = [ \n                        \"/usr/bin/curl\",\n                        \"/usr/local/bin/curl\"\n                      ];\n      }\n      \n      var curl_path = false;\n      for( var n=0;n<avail_paths.length;n++ ){\n        if( ( new File(avail_paths[n]) ).exists ){\n          curl_path = avail_paths[n];\n          break;\n        }\n      }\n      \n      if( !curl_path ){\n        MessageBox.information( \"Unable to find application for web hook.\" );\n        return false;\n      }\n      \n      var file = new File( target );\n      if( file.exists ){\n        file.remove();\n      }\n      \n      var cmdline = [ \"-L\", \"-o\", target, address ];\n      \n      var p = new QProcess();\n      p.start( curl_path, cmdline );  \n      p.waitForFinished( 10000 );\n      \n      var file = new File( target );\n      return file.exists;\n      \n    }catch( err ){\n      System.println( err + \"(\"+err.lineNumber+\")\" );\n      MessageLog.trace( err + \" (\"+address.lineNumber+\")\" );\n    }\n  }  \n}\n\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/openHarmony_tools.js",
    "content": "//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n//\n//                            openHarmony Library v0.01\n//\n//\n//         Developed by Mathieu Chaptel, ...\n//\n//\n//   This library is an open source implementation of a Document Object Model\n//   for Toonboom Harmony. It also implements patterns similar to JQuery\n//   for traversing this DOM.\n//\n//   Its intended purpose is to simplify and streamline toonboom scripting to\n//   empower users and be easy on newcomers, with default parameters values,\n//   and by hiding the heavy lifting required by the official API.\n//\n//   This library is provided as is and is a work in progress. As such, not every\n//   function has been implemented or is guaranteed to work. Feel free to contribute\n//   improvements to its official github. If you do make sure you follow the provided\n//   template and naming conventions and document your new methods properly.\n//\n//   This library doesn't overwrite any of the objects and classes of the official\n//   Toonboom API which must remains available.\n//\n//   This library is made available under the MIT license.\n//   https://opensource.org/licenses/mit\n//\n//   The repository for this library is available at the address:\n//   https://github.com/cfourney/OpenHarmony/\n//\n//\n//   For any requests feel free to contact m.chaptel@gmail.com\n//\n//\n//\n//\n//////////////////////////////////////////////////////////////////////////////////////\n//////////////////////////////////////////////////////////////////////////////////////\n\n\n//////////////////////////////////////\n//////////////////////////////////////\n//                                  //\n//                                  //\n//             TOOLS                //\n//                                  //\n//                                  //\n//////////////////////////////////////\n//////////////////////////////////////\n\n//-- Standalone Tool for openHarmony tool installation and management.\n//--------------------------------------------------------------------\n\nfunction oh_load(){\n  try{\n    var oh_incl = preferences.getString( 'openHarmonyInclude', false );\n    if( !this[\"$\"] ){  \n      include( oh_incl );\n    }\n    if( !this[\"$\"] ){  \n      MessageBox.warning( \"Unable to load the openHarmony library. Is it installed?\" );\n    }\n  }catch(err){\n    System.println( err + \" : \" + err.lineNumber + \" \" + err.fileName );\n  }\n}\n\nfunction openHarmony_toolInstaller(){\n  oh_load();  \n    var tool_installer_gui = function(){\n      try{\n        \n        this.$ = $;\n        var oh_path = preferences.getString( 'openHarmonyPath', false );\n        this.loaded = false;\n        this.ui_path = oh_path + \"/openHarmony/openHarmony_toolInstall.ui\";\n        \n        var branch_list = \"https://api.github.com/repos/cfourney/OpenHarmony/branches\";\n        \n        if( !(new $.oFile(this.ui_path).exists) ){\n          return;\n        }\n        \n        this.ui = UiLoader.load( this.ui_path );\n        \n        this.loaded = true;\n        \n        this.branchList    = this.ui.toolListGroupBox.branchCombo;\n        this.installList   = this.ui.toolListGroupBox.installList;\n        this.installButton = this.ui.detailGroupbox.installButton;\n        this.installLabel  = this.ui.detailGroupbox.installLabel;\n        this.detailText    = this.ui.detailGroupbox.detailArea;\n        \n        this.branchList.clear();\n        this.branchList.addItems( [ \"LOADING. . .\" ] );\n        \n        this.detailText.readOnly = true;\n        \n        var context = this;\n        \n        this.installButton.setEnabled( false ); \n        \n        //INSTALL DETAILS\n        var oh_install        = specialFolders.userScripts + \"/\" + \"openHarmony_install\";\n        var oh_install_folder = ( new $.oFolder(oh_install) );\n        if( !oh_install_folder.exists ){\n          //CREATE IT.\n          oh_install_folder.create();\n        }\n        \n        this.ref_cache = {};\n        \n        this.install_type = 'install';\n         \n        //----------------------------------------------\n        //-- CHECK TO SEE IF THE FILE IS ALREADY INSTALLED.\n        this.getInstalledStatus = function( item ){\n          with( context.$.global ){\n            try{\n              var $    = context.$;\n              var m    = context;\n              \n              //A file is written here if it is installed.\n              var install_detail_script = oh_install + \"/\" + item[\"name\"];\n              var install_file = new $.oFile( install_detail_script );\n              \n              if( install_file.exists ){\n                //--The file exists. It might be an remove if same version, or an upgrade.\n                var read_file = install_file.read().split(\"\\n\");\n                if( read_file[0] == item[\"sha\"] ){\n                 return 'remove';\n                }else{\n                 return 'update';\n                }\n              }else{\n                return 'install';\n              } \n            }catch(err){\n              $.debug( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n              return 'install';\n            }\n          }\n        }\n        \n        //----------------------------------------------\n        //-- UPDATE THE INSTALLED STATUS OF THE SELECTED PLUGIN\n        this.updateInstalledStatus = function( item ){\n          with( context.$.global ){\n            try{\n              var $    = context.$;\n              var m    = context;\n              \n              var install_status = m.getInstalledStatus( item );\n              \n              switch( install_status ){\n                case 'install':\n                  m.install_type = install_status;\n                  m.installButton.text = \"INSTALL\";\n                  m.installButton.setEnabled( true );\n                  break;\n                  \n                case 'update':\n                  m.install_type = install_status;\n                  m.installButton.text = \"UPDATE\";\n                  m.installButton.setEnabled( true );\n                  break;\n                  \n                case 'remove':\n                  m.install_type = install_status;\n                  m.installButton.text = \"REMOVE\";\n                  m.installButton.setEnabled( true );\n                  break;\n                  \n                default:\n                  m.install_type = \"install\";\n                  m.installButton.text = \"INSTALL/UPDATE\";\n                  m.installButton.setEnabled( false );\n                  break;\n                  \n              }\n\n            }catch(err){\n              $.debug( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n            }\n          }\n        }\n        \n        this.readme_cache  = {};\n        this.install_cache = {};\n        this.content_cache = {};\n        \n        //----------------------------------------------\n        //-- RETRIEVE THE SELECTED ITEM\n        this.getItem = function(){\n          with( context.$.global ){\n            try{\n              var $    = context.$;\n              var m    = context;\n              \n              var indx = m.installList.currentRow;\n              if( indx >= m.availableItems.length ){\n                return false;\n              }\n              var item = m.availableItems[ indx ];\n              return item;\n            }catch(err){\n              $.debug( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n            }\n          }\n        }\n        \n        \n        //----------------------------------------------\n        //-- GET THE FILE CONTENTS IN A DIRECTORY ON GIT\n        this.recurse_files = function( contents, arr_files ){\n          with( context.$.global ){\n            try{\n              var $    = context.$;\n              var m    = context;\n              for( var n=0;n<contents.length;n++ ){\n                var tfl = contents[n];\n                \n                if( contents[n].type == \"file\" ){\n                  arr_files.push( [tfl[\"download_url\"], tfl[\"path\"]] );\n                  \n                }else if( contents[n].type == \"dir\" ){\n                  //Get more contents.\n                  QCoreApplication.processEvents();\n                  var apic = new api_call( tfl[\"url\"] );\n                  if( apic ){\n                    arr_files = m.recurse_files( apic, arr_files );\n                  }\n                }\n              }\n              \n              return arr_files;\n            }catch(err){\n              $.debug( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n            }\n          }\n        }\n        \n        //----------------------------------------------\n        //-- STANDARD DIALO CONFIRMATION\n        this.confirmDialog = function( d_title, d_str, ok_text, cancel_text ){\n            if (typeof ok_text === 'undefined') var ok_text = \"Okay\";\n            if (typeof cancel_text === 'undefined') var cancel_text = \"Cancel\";            \n          \n            var d = new Dialog();\n            d.title = d_title;\n            \n            //Standard install paths.\n            var install_base = specialFolders.userScripts;\n            var library_base = install_base + '/openHarmony';\n            \n            var libdir = new Dir(library_base);\n            var label = new Label;\n            label.text = d_str;\n            d.okButtonText     = ok_text;\n            d.cancelButtonText = cancel_text;\n            d.add( label );\n            \n            if ( !d.exec() ){\n              return false;\n            }\n            \n          return true;\n        }\n        \n        //----------------------------------------------\n        //-- FILES ARE DOWNLOADED\n        this.downloadFiles = function( file_download_listing, overwrite ){\n          with( context.$.global ){\n            try{\n              var $    = context.$;\n              var m    = context;\n              \n              var item = m.getItem();\n              if( !item ){ return; }\n              \n              var install_type = m.install_cache[ item[\"url\"] ];\n              \n              // $.debug( file_download_listing, $.DEBUG_LEVEL[\"ERROR\"] );\n              \n              var path_download = [];\n              var install_base  = specialFolders.userScripts;\n              \n              var ignore_files = {  \n                                    \"README\"  : true, \n                                    \"INSTALL\" : true\n                                 };\n                    \n              // $.debug( item, $.DEBUG_LEVEL[\"ERROR\"] );\n              \n              var script_files         = [];\n              var install_instructions = [];\n              var install_files        = [];\n              for( var n=0; n<file_download_listing.length; n++ ){\n                var tfl = file_download_listing[n];\n                \n                var url  = tfl[0];\n                var file = tfl[1];\n                \n                try{\n                  var local_path = file.slice( item[\"path\"].length+1 );\n                  if( ignore_files[ local_path ] ){\n                    continue; //Skipped file.\n                  }\n                  \n                  if( local_path.split(\"/\").length > 0 && local_path.toUpperCase().indexOf(\".JS\")>0 ){\n                    script_files.push( local_path );\n                  }\n                  \n                  var lpth = install_base + \"/\" + local_path;\n                  install_files.push( lpth );\n                  \n                  var lfl = new $.oFile( lpth );\n                  if( lfl.exists && !overwrite ){\n                    //Confirm deletion?\n                    if( !confirmDialog( \"Overwrite File\", \"Overwrite \" + lpth, \"Overwrite\", \"Cancel\" ) ){ continue; }\n                  }\n                  \n                  install_instructions.push( { \"url\": url, \"path\": lpth } );\n                }catch(err){\n                  continue;\n                }\n              }\n              \n              var downloaded = $.network.downloadMulti( install_instructions, true );\n              \n              var all_success = true;\n              for( var x=0;x<downloaded.length;x++ ){\n                if( !downloaded[x] ){\n                  all_success = false;\n                }\n              }\n              \n              if( !all_success ){\n                MessageBox.information( \"Failed to download \" + item[\"name\"] +\", try again later.\" );\n                return;\n              }\n              \n              var str = '    No Script Files Available.';\n              if( script_files.length>0 ){\n                var str_limited = [];\n                for( var t=0;t<( Math.min(script_files.length,4) );t++ ){\n                  str_limited.push( \"    \" + script_files[t] );\n                }              \n                if( script_files.length>4 ){\n                  str_limited.push( \"         \" + \"And More!\" );\n                }\n                str = str_limited.join( \"\\n\" );\n              }\n              \n              m.installButton.text = \"INSTALLED!\";\n              m.installButton.setEnabled( false );\n              MessageBox.information( \"Installed \" + item[\"name\"] + \"!\\n\\nThe installed scripts include:\\n\" + str );\n              \n              //TODO: Create the install script with details.\n              var install_detail_script = oh_install + \"/\" + item[\"name\"];\n              var install_details_text = [];\n              \n              var install_fl = new $.oFile( install_detail_script );\n              install_fl.write( item[\"sha\"] + \"\\n\" + install_files.join( \"\\n\" ) );\n              \n              m.get_tools();\n            }catch(err){\n              $.debug( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n            }\n          }\n        }\n        \n        this.basePath = '';\n        \n        this.removeAction = function( ev ){\n          with( context.$.global ){\n            try{\n              var $    = context.$;\n              var m    = context;\n              \n              var item = m.getItem();\n              if( !item ){\n                $.debug( \"Failed to install - no item seems to be selected.\", $.DEBUG_LEVEL[\"ERROR\"] );\n                return;\n              }\n              \n              var install_detail_script = oh_install + \"/\" + item[\"name\"];\n              var install_file = new $.oFile( install_detail_script );\n              \n              if( install_file.exists ){\n                //--The file exists. It might be an remove if same version, or an upgrade.\n                var read_file = install_file.read().split(\"\\n\");\n                \n                for( var n=1;n<read_file.length;n++ ){\n                  var fl = read_file[n];\n                  \n                  var flobj = new $.oFile( fl );\n                  if( flobj.exists ){\n                    $.debug( \"Removing file: \" + fl, $.DEBUG_LEVEL[\"ERROR\"] );\n                    flobj.remove();\n                  }\n                }\n                \n                if( install_file.exists ){\n                  $.debug( \"Removing file: \" + install_detail_script, $.DEBUG_LEVEL[\"ERROR\"] );\n                  install_file.remove();\n                }\n                \n                m.get_tools();\n                MessageBox.information( \"Removed the \" + item[\"name\"] + \" plugin.\" );\n                \n              }else{\n                MessageBox.information( \"Unable to find the installed plugin.\" );\n                \n              }\n            }catch(err){\n              $.debug( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n            }\n          }\n        }\n        \n        //----------------------------------------------\n        //-- THE INSTALL BUTTON WAS CLICK.\n        this.installAction = function( ){\n          with( context.$.global ){\n            try{\n              var $    = context.$;\n              var m    = context;\n              \n              var item = m.getItem();\n              if( !item ){\n                $.debug( \"Failed to install - no item seems to be selected.\", $.DEBUG_LEVEL[\"ERROR\"] );\n                return;\n              }\n                       \n              if( m.install_type == \"remove\" ){\n                //Iterate the files and remove them.\n                m.removeAction();\n                return;\n              }\n                       \n              if( !m.content_cache[ item[\"url\"] ] ){\n                $.debug( \"Failed to install - cache contents should be available already.\", $.DEBUG_LEVEL[\"ERROR\"] );\n                return;\n              }\n              \n              var files = m.recurse_files( m.content_cache[ item[\"url\"] ], [] );\n              $.debug( \"FILES TO INSTALL: \"+files.length, $.DEBUG_LEVEL[\"LOG\"] );\n\n              m.downloadFiles( files, false );\n              \n            }catch(err){\n              $.debug( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n            }\n          }\n        }\n        this.installButton[\"clicked\"].connect( this, this.installAction );\n        \n        //----------------------------------------------\n        //-- GET THE README DETAILS TO SHOW TO THE USER\n        this.getReadme = function( results ){\n          with( context.$.global ){\n            try{\n              var $    = context.$;\n              var m    = context;\n            \n              var item = m.getItem();\n              if( !item ){ return; }\n            \n              m.install_cache[ item[\"url\"] ] = \"script\";\n            \n              if( !results ){\n                m.detailText.setText( \"Failed to load description.\" );\n                return;\n              }\n            \n              m.content_cache[ item[\"url\"] ] = results;\n              m.installLabel.text = 'SCRIPT';\n              \n              //Standardize the types.\n              var install_types = {\n                                    \"script\" : \"SCRIPT\"\n                                  };\n              \n              var set_readme = false;\n              for( var n=0;n<results.length;n++ ){\n                //--- \n                var item = results[ n ];\n                if( item[\"name\"].toUpperCase() == \"README\" ){\n                  //README WAS FOUND\n                  var download_item = item[\"download_url\"]; //download_item\n                  var query = $.network.webQuery( download_item, false, false );\n                  if( query ){\n                    m.detailText.setHtml( query );\n                    m.readme_cache[ item[\"url\"] ] = query;\n                    set_readme = true;\n                  }\n                }else if( item[\"name\"].toUpperCase() == \"INSTALL\" ){\n                  //INSTALL WAS FOUND\n                  var download_item = item[\"download_url\"];\n                  var query = $.network.webQuery( download_item, false, false );\n                  if( query ){\n                    //INSTALL TYPES ARE script, package, etc.\n                    \n                    if( install_types[ m.install_cache[ item[\"url\"] ] ] ){\n                      m.installLabel.text = install_types[ m.install_cache[ item[\"url\"] ] ];\n                    }else{\n                      m.installLabel.text = \"SCRIPT\";\n                    }\n                    \n                    m.install_cache[ item[\"url\"] ] = query.toLowerCase();\n                  }\n                }\n              }\n                                   \n              if( !install_types[ m.install_cache[ item[\"url\"] ] ] ){\n                m.install_cache[ item[\"url\"] ] = \"script\";\n              }\n              \n              if( !set_readme ){\n                m.detailText.setText( \"No README available.\" );\n              }\n            }catch(err){\n              $.debug( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n            }\n          }\n        }\n        \n        //----------------------------------------------\n        //-- TOOL IS SELECTED, QUERY OTHER DETAILS\n        this.select_tool = function( item, item ){\n          with( context.$.global ){\n            try{\n              var $    = context.$;\n              var m    = context;\n              \n              m.detailText.setText( \"\" );\n              m.installButton.setEnabled( false );\n              m.installButton.text = \"INSTALL/REMOVE\";\n              \n              var item = m.getItem();\n              if( !item ){\n                return;\n              }\n              \n              m.detailText.setText( \"Loading. . .\" );\n              m.updateInstalledStatus( item );\n              \n              if( !m.readme_cache[ item[\"url\"] ] ){\n                var query = $.network.webQuery( item[\"url\"], this.getReadme, true );\n              }else{\n                m.detailText.setHtml( m.readme_cache[ item[\"url\"] ] );\n              }\n            }catch(err){\n              $.debug( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n            }\n          }\n        }\n        this.installList[\"itemSelectionChanged()\"].connect( this, this.select_tool );\n        \n        \n        //----------------------------------------------\n        //-- SHOW THE TOOLS\n        this.tool_dir_cache = {};\n        this.show_tools = function( results ){\n          with( context.$.global ){\n            try{\n              var $    = context.$;\n              var m    = context;\n              \n              m.availableItems = [];\n              m.installList.clear();\n              \n              var c_sha = m.branches[ m.branchList.currentIndex ];\n              m.ref_cache[ c_sha ] = results;\n              \n              if(! results){\n                //NO OPTIONS\n                m.installList.addItem( \"None Available\" );\n                m.detailText.setHtml( \"\" );\n                return;\n              }\n              \n              var tool_dir = false;\n              //FIND THE TOOLS DIRECTORY IN THE DEPLOYMENT.\n              for( var n=0;n<results.length;n++ ){\n                if( results[n].name.toUpperCase() == \"TOOLS\" && results[n].type == \"dir\" ){\n                  tool_dir = results[n];\n                }\n              }\n              \n              if(!tool_dir){\n                m.installList.addItem( \"None Available\" );\n                m.detailText.setHtml( \"\" );\n                return;\n              }\n              \n              if( m.tool_dir_cache[ tool_dir[\"url\"] ] ){\n                query = m.tool_dir_cache[ tool_dir[\"url\"] ];\n              }else{              \n                var query = $.network.webQuery( tool_dir[\"url\"], false, true );\n                if( !query ){\n                  m.installList.addItem( \"None Available\" );\n                  return;\n                }\n                m.tool_dir_cache[ tool_dir[\"url\"] ] = query;\n              }\n              \n              //List and color the tool listing.\n              for( var n=0;n<query.length; n++ ){\n                m.installList.addItem( query[n][\"name\"] );\n                var stat = m.getInstalledStatus( query[n] );\n                \n                switch( stat ){\n                  case \"remove\":\n                    var t_item = m.installList.item( n );\n                    t_item.setForeground( new QBrush( new QColor( new QColor( 0, 125, 0, 255 ) ) ) );\n                    break;\n                    \n                  case \"update\":\n                    var t_item = m.installList.item( n );\n                    t_item.setForeground( new QBrush( new QColor( new QColor( 125, 0, 0, 255 ) ) ) );\n                    break;\n                }\n\n                m.availableItems.push( query[n] );\n              }\n            }catch( err ){\n              $.debug( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n            }\n          }\n        }\n        \n        \n        //----------------------------------------------\n        //-- GET THE TOOLS\n        this.get_tools = function( ev ){\n          with( context.$.global ){\n            try{\n              var $    = context.$;\n              var m    = context;\n              \n              var c_sha = m.branches[ m.branchList.currentIndex ];\n              \n              m.installList.clear();\n              m.installList.addItem( \"Loading. . .\" );\n              m.detailText.setHtml( \"\" );\n              \n              if( !c_sha ){\n                m.installList.addItem( \"\" );\n                return;\n              }\n              \n              if( !m.ref_cache[ c_sha ] ){\n                var contents_url = \"https://api.github.com/repos/cfourney/OpenHarmony/contents?ref=\"+c_sha;             \n                var query = $.network.webQuery( contents_url, this.show_tools, true );\n              }else{\n                m.show_tools( m.ref_cache[ c_sha ] );\n              }\n              \n            }catch( err ){\n              $.debug( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n            }\n          }\n        }\n        \n        this.branchList[\"currentIndexChanged(int)\"].connect( this, this.get_tools );\n        \n        \n        //----------------------------------------------\n        //-- GET THE BRANCHES AND DISPLAY THEM IN THE PULLDOWN\n        this.get_branches = function( results ){\n          with( context.$.global ){\n            try{\n              \n              var $    = context.$;\n              var m    = context;\n              \n              m.branches = [];\n              m.branchList.clear();\n              m.detailText.setHtml( \"\" );\n              if( results.length == 0 ){\n                this.branchList.addItems( [ \"NO BRANCHES\" ] )\n              }else{\n                for( var n=0;n<results.length;n++ ){\n                  m.branches.push( results[n][\"commit\"][\"sha\"] );\n                  m.branchList.insertItem(n, results[n][\"name\"].toUpperCase(), results[n][\"commit\"][\"sha\"] );\n                  \n                }\n              }\n              \n            }catch( err ){\n              $.debug( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n            }\n          context.get_tools();\n          }\n        }\n        \n        var query = $.network.webQuery( branch_list, this.get_branches, true );\n        this.ui.show();\n\n      }catch(err){\n        $.debug( err + \" (\"+err.lineNumber+\")\", $.DEBUG_LEVEL[\"ERROR\"] );\n      }\n    };\n    \n  var tool_instaler = new tool_installer_gui();\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/reference/Reference_view_currentToolManager().txt",
    "content": "Dump of content from: view.currentToolManager() \n\ndeleteLater()\nselectionChanged()\nselectionCleared()\ntoolChanged()\nonActionAdjustLineTextureOpacity()\nonActionAdjustLineTextureOpacityValidate(AC_ActionInfo*)\nonActionRemoveDirt()\nonActionRemoveDirtValidate(AC_ActionInfo*)\nonActionRemoveHair()\nonActionRemoveHairValidate(AC_ActionInfo*)\nonActionCloseGap()\nonActionCloseGapValidate(AC_ActionInfo*)\nonActionGenerateAutoMatte()\nonActionGenerateAutoMatteValidate(AC_ActionInfo*)\nonActionFlatten()\nonActionFlattenValidate(AC_ActionInfo*)\nonActionFlattenAndRemoveInvisibleLines()\nonActionFlattenAndRemoveInvisibleLinesValidate(AC_ActionInfo*)\nonActionRemoveInvisibleLines()\nonActionRemoveInvisibleLinesValidate(AC_ActionInfo*)\nonActionOptimize()\nonActionOptimizeValidate(AC_ActionInfo*)\nonActionOptimizeTextures()\nonActionOptimizeTexturesValidate(AC_ActionInfo*)\nonActionReduceTextures()\nonActionReduceTexturesValidate(AC_ActionInfo*)\nonActionSmooth()\nonActionSmoothValidate(AC_ActionInfo*)\nonActionSmoothFast()\nonActionSmoothFastValidate(AC_ActionInfo*)\nonActionPumpThickness()\nonActionPumpThicknessValidate(AC_ActionInfo*)\nonActionDeflateThickness()\nonActionDeflateThicknessValidate(AC_ActionInfo*)\nonActionReverseThickness()\nonActionReverseThicknessValidate(AC_ActionInfo*)\nonActionAddStrokeAttribute()\nonActionAddStrokeAttributeValidate(AC_ActionInfo*)\nonActionRemoveStrokeAttribute()\nonActionRemoveStrokeAttributeValidate(AC_ActionInfo*)\nonActionPencilToBrush()\nonActionPencilToBrushValidate(AC_ActionInfo*)\nonActionExtractCenterLine()\nonActionExtractCenterLineValidate(AC_ActionInfo*)\nonActionStrokeToPencil()\nonActionStrokeToPencilValidate(AC_ActionInfo*)\nonActionBreakTextSelection()\nonActionBreakTextSelectionValidate(AC_ActionInfo*)\nonActionLineArtToColorArt()\nonActionLineArtToColorArt(AC_ActionInfo*)\nonActionLineArtToColorArtValidate(AC_ActionInfo*)\nonActionCreateBreakingTriangles()\nonActionCreateBreakingTrianglesValidate(AC_ActionInfo*)\nonActionConvertToVectorLayer()\nonActionConvertToVectorLayerValidate(AC_ActionInfo*)\nonActionConvertToBitmapLayer()\nonActionConvertToBitmapLayerValidate(AC_ActionInfo*)\nonActionChangeVectorLayerResolution()\nonActionChangeVectorLayerResolutionValidate(AC_ActionInfo*)\nonActionChangeBitmapLayerResolution()\nonActionChangeBitmapLayerResolutionValidate(AC_ActionInfo*)\nonActionBringToFront()\nonActionBringToFrontValidate(AC_ActionInfo*)\nonActionBringForward()\nonActionBringForwardValidate(AC_ActionInfo*)\nonActionSendBackward()\nonActionSendBackwardValidate(AC_ActionInfo*)\nonActionSendToBack()\nonActionSendToBackValidate(AC_ActionInfo*)\nonActionDrawingFlipHorizontal()\nonActionDrawingFlipHorizontalValidate(AC_ActionInfo*)\nonActionDrawingFlipVertical()\nonActionDrawingFlipVerticalValidate(AC_ActionInfo*)\nonActionRotate90CW()\nonActionRotate90CWValidate(AC_ActionInfo*)\nonActionRotate90CCW()\nonActionRotate90CCWValidate(AC_ActionInfo*)\nonActionRotate180()\nonActionRotate180Validate(AC_ActionInfo*)\nonActionGroup()\nonActionGroupValidate(AC_ActionInfo*)\nonActionUngroup()\nonActionUngroupValidate(AC_ActionInfo*)\ncut()\ncutValidate(AC_ActionInfo*)\ncopy()\ncopyValidate(AC_ActionInfo*)\npaste()\npasteValidate(AC_ActionInfo*)\ndeleteSelection()\ndeleteSelectionValidate(AC_ActionInfo*)\ndeleteNonSelection()\ndeleteNonSelectionValidate(AC_ActionInfo*)\nselectAll()\nselectAllItems()\nselectAllValidate(AC_ActionInfo*)\ninvertSelection()\ninvertSelectionValidate(AC_ActionInfo*)\ndeselect()\ndeselectValidate(AC_ActionInfo*)\ndeselectAll()\ndeselectAllValidate(AC_ActionInfo*)\ndeselectDrawings()\ndeselectDrawingsValidate(AC_ActionInfo*)\nonActionSelectAllCurrentColor()\nonActionSelectAllCurrentColorValidate(AC_ActionInfo*)\nonActionShowStrokes()\nonActionShowStrokesValidate(AC_ActionInfo*)\nonActionShowStrokesWithColourWash()\nonActionShowStrokesWithColourWashValidate(AC_ActionInfo*)\nonActionBackLight()\nonActionBackLightValidate(AC_ActionInfo*)\nonActionPermanentColorHighlight()\nonActionPermanentColorHighlightValidate(AC_ActionInfo*)\nOnLineToColor()\nonActionShowOtherDrawingThumbnail()\nonActionShowOtherDrawingThumbnailValidate(AC_ActionInfo*)\nonActionMergeToTopLayer()\nonActionMergeToTopLayerValidate(AC_ActionInfo*)\nonActionNoMergeToTopLayer()\nonActionNoMergeToTopLayerValidate(AC_ActionInfo*)\nonActionAutoFlatten()\nonActionAutoFlattenValidate(AC_ActionInfo*)\nonActionDrawTopLayerCycle()\nonActionDrawTopLayerCycleValidate(AC_ActionInfo*)\nonActionSetAutoGapDisabled()\nonActionSetAutoGapDisabledValidate(AC_ActionInfo*)\nonActionSetCloseSmallGap()\nonActionSetCloseSmallGapValidate(AC_ActionInfo*)\nonActionSetCloseMediumGap()\nonActionSetCloseMediumGapValidate(AC_ActionInfo*)\nonActionSetCloseLargeGap()\nonActionSetCloseLargeGapValidate(AC_ActionInfo*)\nonActionSetPaintModePaint()\nonActionSetPaintModePaintValidate(AC_ActionInfo*)\nonActionSetPaintModePaintUnpainted()\nonActionSetPaintModePaintUnpaintedValidate(AC_ActionInfo*)\nonActionSetPaintModeRepaint()\nonActionSetPaintModeRepaintValidate(AC_ActionInfo*)\nonActionSetPaintModeUnpaint()\nonActionSetPaintModeUnpaintValidate(AC_ActionInfo*)\nonActionTogglePermanentSelection()\nonActionTogglePermanentSelectionValidate(AC_ActionInfo*)\nonActionToggleApplyToolToAllLayers()\nonActionToggleApplyToolToAllLayersValidate(AC_ActionInfo*)\nonActionToggleApplyToAllDrawings()\nonActionToggleApplyToAllDrawingsValidate(AC_ActionInfo*)\nonActionSetApplyToAllDrawings()\nonActionSetApplyToAllDrawingsValidate(AC_ActionInfo*)\nonActionUnsetApplyToAllDrawings()\nonActionUnsetApplyToAllDrawingsValidate(AC_ActionInfo*)\nonActionToggleSnapToContour()\nonActionToggleSnapToContourValidate(AC_ActionInfo*)\nonActionToggleSnapToBoundingBoxes()\nonActionToggleSnapToBoundingBoxesValidate(AC_ActionInfo*)\nonActionToggleSnapToGrid()\nonActionToggleSnapToGridValidate(AC_ActionInfo*)\nonActionDuplicateDrawings()\nonActionCreateAndReplaceCurrentDrawing()\nonActionTogglePaintOverTexture()\nonActionTogglePaintOverTextureValidate(AC_ActionInfo*)\nonActionToggleCreateColorArtOnBrush()\nonActionToggleCreateColorArtOnBrushValidate(AC_ActionInfo*)\nonActionContourEditorShowAllControls()\nonActionContourEditorShowAllControlsValidate(AC_ActionInfo*)\nonActionDeleteSelection()\nonActionDeleteSelectionValidate(AC_ActionInfo*)\nonActionDeleteSelectionAllDrawings()\nonActionDeleteSelectionAllDrawingsValidate(AC_ActionInfo*)\nonActionDeleteNonSelection()\nonActionDeleteNonSelectionValidate(AC_ActionInfo*)\nonActionDeleteNonSelectionAllDrawings()\nonActionDeleteNonSelectionAllDrawingsValidate(AC_ActionInfo*)\nonActionUnpaintSelection()\nonActionUnpaintSelectionValidate(AC_ActionInfo*)\nonActionUnpaintSelectionAllDrawings()\nonActionUnpaintSelectionAllDrawingsValidate(AC_ActionInfo*)\nonActionUnpaintOutsideSelection()\nonActionUnpaintOutsideSelectionValidate(AC_ActionInfo*)\nonActionUnpaintOutsideSelectionAllDrawings()\nonActionUnpaintOutsideSelectionAllDrawingsValidate(AC_ActionInfo*)\nonActionRepaintSelection()\nonActionRepaintSelectionValidate(AC_ActionInfo*)\nonActionRepaintSelectionAllDrawings()\nonActionRepaintSelectionAllDrawingsValidate(AC_ActionInfo*)\nonActionRepaintOutsideSelection()\nonActionRepaintOutsideSelectionValidate(AC_ActionInfo*)\nonActionRepaintOutsideSelectionAllDrawings()\nonActionRepaintOutsideSelectionAllDrawingsValidate(AC_ActionInfo*)\nonActionAutoFillInsideRegions()\nonActionAutoFillInsideRegionsValidate(AC_ActionInfo*)\nonActionToggleCurrentColorStickyness()\nonActionToggleCurrentColorStickynessValidate(AC_ActionInfo*)\nonActionRespectStickyColor()\nonActionRespectStickyColorValidate(AC_ActionInfo*)\nonActionCreateDrawingFromDrawingSelection()\nonActionCreateDrawingFromDrawingSelectionValidate(AC_ActionInfo*)\nonActionCreateSymbolFromSelection()\nonActionCreateSymbolFromSelectionValidate(AC_ActionInfo*)\nonActionDistributeToLayers()\nonActionDistributeToLayersValidate(AC_ActionInfo*)\nonActionMergeLineStyle()\nonActionMergeLineStyleValidate(AC_ActionInfo*)\nonActionJoinPencilLine()\nonActionJoinPencilLineValidate(AC_ActionInfo*)\nonActionSplitPencilLine()\nonActionSplitPencilLineValidate(AC_ActionInfo*)\nonActionToggleLineBuildingMode()\nonActionToggleLineBuildingModeValidate(AC_ActionInfo*)\nonActionToggleMagnifier()\nonActionToggleMagnifierValidate(AC_ActionInfo*)\nonActionIncrementBrushSize()\nonActionIncrementBrushSizeValidate(AC_ActionInfo*)\nonActionDecrementBrushSize()\nonActionDecrementBrushSizeValidate(AC_ActionInfo*)\nOnRecenterOnPoint(Math::Point2d)\nOnEditDeleteUnselected()\nOnUpdateEditDeleteUnselected(GUT_CmdUI&)\nOnRepaintBrushTool()\nOnUpdateRepaintBrushTool(GUT_CmdUI&)\nOnUpdateCommandThatNeedDrawing(GUT_CmdUI&)\nUpdatePickingBuffer()\nscriptMouseDown(double,double,double,bool)\nscriptMouseMove(double,double,double,bool)\nscriptMouseUp(double,double,double,bool)\nscriptCaptureMouseEvents(bool,bool)\nscriptGetCapturedMouseEventCount()\nscriptGetCapturedMouseEvent(int)\nonActionPasteSpecial()\nonActionPasteSpecialValidate(AC_ActionInfo*)\nonActionPasteSpecialAgain()\nonActionPasteSpecialAgainValidate(AC_ActionInfo*)\nonActionFlipVertical()\nonActionFlipVerticalValidate(AC_ActionInfo*)\nonActionFlipHorizontal()\nonActionFlipHorizontalValidate(AC_ActionInfo*)\nonActionFlipScaleX()\nonActionFlipScaleXValidate(AC_ActionInfo*)\nonActionFlipScaleY()\nonActionFlipScaleYValidate(AC_ActionInfo*)\nonActionShowHideTransformControls()\nonActionShowHideTransformControlsValidate(AC_ActionInfo*)\nonActionCycleThroughPastePresets()\nonActionSetPastePreset(QString)"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/tbpackage.json",
    "content": "{\n \"name\": \"OpenHarmony\",\n \"website\": \"https://cfourney.github.io/OpenHarmony/\",\n \"social\": \"\",\n \"repository\": \"https://github.com/cfourney/OpenHarmony/\",\n \"extensions\": [\n  {\n   \"name\": \"openHarmony\",\n   \"version\": \"0.8.0\",\n   \"compatibility\": \"Harmony Premium 15\",\n   \"description\": \"<p>The openHarmony library enables people to create scripts for Harmony using less code and a simpler synthax.</p><p>The complete documentation is available at this address:<a href='https://cfourney.github.io/OpenHarmony/'>https://cfourney.github.io/OpenHarmony/</a></p><p>Install this library to be able to use the scripts that require it.</p>\",\n   \"repository\": \"https://github.com/cfourney/OpenHarmony/\",\n   \"isPackage\": false,\n   \"files\": [\n    \"openHarmony.js\",\n    \"openHarmony/\"\n   ],\n   \"keywords\": [\n    \"openHarmony\",\n    \"library\"\n   ],\n   \"author\": \"OpenHarmony\",\n   \"license\": \"MPL-2.0\",\n   \"website\": \"https://github.com/cfourney/OpenHarmony/\",\n   \"localFiles\": \"\"\n  },\n  {\n   \"name\": \"oH Anim tools - Smartkey\",\n   \"version\": \"1.0.0\",\n   \"compatibility\": \"Harmony Premium 15\",\n   \"description\": \"This script creates a key frame on the current timeline layer, and set the stop motion or interpolated mode of the key according to the surrounding keyframes. A Keyframe placed on an interpolation will remain interpolated, and a key placed between stop motion keyframes will also be set to stop motion.\\n<p>This script uses the <b>OpenHarmony</b> library. Install it first to be able to use it.</p>\\n\\n<p>Assign this script to a shortcut with the script <b>ScriptShortcuts</b>.</p>\",\n   \"repository\": \"https://github.com/cfourney/OpenHarmony/\",\n   \"isPackage\": false,\n   \"files\": [\n    \"tools/OpenHarmony_basic/openHarmony_anim_tools.js\"\n   ],\n   \"keywords\": [\n    \"openHarmony\",\n    \"animation\"\n   ],\n   \"author\": \"Chris F\",\n   \"license\": \"MPL-v2.0\",\n   \"website\": \"https://github.com/cfourney/OpenHarmony/\",\n   \"localFiles\": \"\"\n  },\n  {\n   \"name\": \"oH Rigging tools\",\n   \"version\": \"1.0.0\",\n   \"compatibility\": \"Harmony Premium 15\",\n   \"description\": \"<b>OpenHarmony Rigging Tools</b>\\n<p>Those scripts require the <b>openHarmony</b> lib to work. Install it first for the scripts to work.</p>\\n<p><u>Add Centered Weighted Peg</u><br>Adds a peg with a pivot at the center of the selected drawing.</p>\\n\\n<p><u>Place Pivot with Click</u><br>Place the pivot with a simple click.</p>\\n\\n<p><u>Clean Unused Palettes</u><br>\\nFinds and removes all unnecessary palettes files from the filesystem. Doesn't support Element Palettes yet!</p>\\n\\n<p><u>Create Backdrop on Selection</u><br>Set up backdrops easily on the selection with this script.</p>\\n\",\n   \"repository\": \"https://github.com/cfourney/OpenHarmony/\",\n   \"isPackage\": false,\n   \"files\": [\n    \"tools/OpenHarmony_basic/openHarmony_rigging_tools.js\",\n    \"tools/OpenHarmony_basic/openHarmony_basic_backdropPicker.ui\"\n   ],\n   \"keywords\": [\n    \"openHarmony\",\n    \"palettes\"\n   ],\n   \"author\": \"Chris F\",\n   \"license\": \"MPL-v2.0\",\n   \"website\": \"https://github.com/cfourney/OpenHarmony/\",\n   \"localFiles\": \"\"\n  }\n ]\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/tools/OpenHarmony_basic/INSTALL",
    "content": "scripts"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/tools/OpenHarmony_basic/README",
    "content": "Basic Helper Scripts, used for rigging, animation and compositing."
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/tools/OpenHarmony_basic/openHarmony_anim_tools.js",
    "content": "/**\n *  Load the Open Harmony Library as needed.\n */\ntry{\n  var oh_incl = preferences.getString( 'openHarmonyIncludeDebug', false );\n  if( oh_incl==\"false\" ) oh_incl = preferences.getString( 'openHarmonyInclude', false );\n  if( oh_incl==\"false\" ) oh_incl = \"openHarmony.js\";\n  include(oh_incl);\n}catch(err){\n  MessageBox.information (\"OpenHarmony is not installed. Get it here: \\nhttps://github.com/cfourney/OpenHarmony\")\n}\n\n\n/**\n *  Smart key button. Only adds a key if a column already exists. Maintains sections that are tweens and other sections that are stop-motion/holds.\n */\nfunction oh_anim_smartKey(){\n  System.println(\"\");\n  \n  scene.beginUndoRedoAccum( \"oh_anim_smartKey\" );\n  \n  var timeline = $.scene.getTimeline();\n  var layers   = timeline.selectedLayers;\n  \n  //--------------------------------------------------\n  //--- The key function, used \n    \n  var smart_key_item = function( attr ){\n    var cfrm  = $.scene.currentFrame; \n    \n    var frame = attr.frames[ cfrm ];\n\n    \n    if( attr.node.type == \"READ\" ){\n      \n      if( attr.column.type == \"DRAWING\" ){\n        frame.isKeyFrame = true;\n        return;\n      }\n      \n      //DONT KEY THE OFFSETS IF ITS NOT ANIMATEABLE.\n      var check_pos = {\n                        \"offset.x\"        : true,\n                        \"offset.y\"        : true,\n                        \"offset.z\"        : true,\n                        \"skew\"            : true,\n                        \"scale.x\"         : true,\n                        \"scale.y\"         : true,\n                        \"scale.z\"         : true,\n                        \"rotation.anglez\" : true\n                      }\n      if( !attr.node[\"can_animate\"] ){\n        return;\n      }\n    }\n    \n    if( !frame.isKeyFrame ){\n      var lk    = frame.keyframeLeft;\n      frame.isKeyFrame = true;\n      \n      if(!lk){\n        //Consider this as static.\n        frame.constant   = true;\n      }else{\n        if( lk.constant ){\n          frame.constant   = true;\n          // lk.constant      = true;   //UNNECESSARY, DEFAULT APPRAOCH TO isKeyFrame = true;\n        }else{\n          var rk    = frame.keyframeRight;\n          \n          if( rk ){\n            //Something is to the right, keep the tween.\n            frame.constant   = false;\n            // lk.constant      = false;   //UNNECESSARY, DEFAULT APPRAOCH TO isKeyFrame = true;\n          }else{\n            frame.constant   = true;\n            lk.constant      = true;\n          }\n        }\n      }\n    }\n  }\n  \n  \n  if( layers.length == 0 ){\n    var layers = timeline.compositionLayers;\n    \n    var items = [];\n    for( var n=0;n<layers.length;n++ ){\n      var tlayer = layers[n];\n      for( var attrname in tlayer.attributes ){\n        var tattr = tlayer.attributes[ attrname ];\n        if( tattr.column ){\n          smart_key_item( tattr );\n        }else{\n          for( var tt=0;tt<tattr.subAttributes.length;tt++ ){\n            var subattr = tattr.subAttributes[tt];\n            if( subattr.column ){\n              smart_key_item( subattr );\n            }\n          }\n        }\n      }\n    }\n  }else{\n    //Selected layers are sufficient.\n    for( var n=0;n<layers.length;n++ ){\n      var tlayer = layers[n];\n      if( tlayer.attribute ){\n        if( tlayer.attribute.column ){  \n          smart_key_item( tlayer.attribute );\n        }\n      }\n    }\n  }\n\n  System.println( \"END\" );\n  scene.endUndoRedoAccum( );\n}"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/tools/OpenHarmony_basic/openHarmony_basic_backdropPicker.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>Dialog</class>\n <widget class=\"QDialog\" name=\"Dialog\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>471</width>\n    <height>223</height>\n   </rect>\n  </property>\n  <property name=\"windowTitle\">\n   <string>Backdrop Details</string>\n  </property>\n  <property name=\"modal\">\n   <bool>true</bool>\n  </property>\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\n   <item>\n    <layout class=\"QHBoxLayout\" name=\"nameBox\">\n     <item>\n      <widget class=\"QLabel\" name=\"backdropNameLabel\">\n       <property name=\"minimumSize\">\n        <size>\n         <width>75</width>\n         <height>0</height>\n        </size>\n       </property>\n       <property name=\"text\">\n        <string>Name</string>\n       </property>\n      </widget>\n     </item>\n     <item>\n      <widget class=\"QLineEdit\" name=\"backdropName\"/>\n     </item>\n    </layout>\n   </item>\n   <item>\n    <layout class=\"QHBoxLayout\" name=\"textBox\">\n     <item>\n      <widget class=\"QLabel\" name=\"backdropTextLabel\">\n       <property name=\"minimumSize\">\n        <size>\n         <width>75</width>\n         <height>0</height>\n        </size>\n       </property>\n       <property name=\"text\">\n        <string>Text</string>\n       </property>\n       <property name=\"alignment\">\n        <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>\n       </property>\n      </widget>\n     </item>\n     <item>\n      <widget class=\"QPlainTextEdit\" name=\"backdropText\"/>\n     </item>\n    </layout>\n   </item>\n   <item>\n    <layout class=\"QHBoxLayout\" name=\"colorBox\">\n     <item>\n      <widget class=\"QLabel\" name=\"backdropColorLabel\">\n       <property name=\"minimumSize\">\n        <size>\n         <width>74</width>\n         <height>0</height>\n        </size>\n       </property>\n       <property name=\"text\">\n        <string>Color</string>\n       </property>\n      </widget>\n     </item>\n     <item>\n      <widget class=\"QLabel\" name=\"backdropColor\">\n       <property name=\"sizePolicy\">\n        <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n         <horstretch>1</horstretch>\n         <verstretch>0</verstretch>\n        </sizepolicy>\n       </property>\n       <property name=\"text\">\n        <string/>\n       </property>\n      </widget>\n     </item>\n     <item>\n      <widget class=\"QPushButton\" name=\"backdropColorButton\">\n       <property name=\"text\">\n        <string>Change Color</string>\n       </property>\n      </widget>\n     </item>\n    </layout>\n   </item>\n   <item>\n    <widget class=\"QDialogButtonBox\" name=\"buttonBox\">\n     <property name=\"orientation\">\n      <enum>Qt::Horizontal</enum>\n     </property>\n     <property name=\"standardButtons\">\n      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>\n     </property>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections>\n  <connection>\n   <sender>buttonBox</sender>\n   <signal>accepted()</signal>\n   <receiver>Dialog</receiver>\n   <slot>accept()</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>248</x>\n     <y>254</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>157</x>\n     <y>274</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>buttonBox</sender>\n   <signal>rejected()</signal>\n   <receiver>Dialog</receiver>\n   <slot>reject()</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>316</x>\n     <y>260</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>286</x>\n     <y>274</y>\n    </hint>\n   </hints>\n  </connection>\n </connections>\n</ui>\n"
  },
  {
    "path": "openpype/hosts/harmony/vendor/OpenHarmony/tools/OpenHarmony_basic/openHarmony_rigging_tools.js",
    "content": "/**\n *  Load the Open Harmony Library as needed.\n */\n\ntry{\n  var oh_incl = preferences.getString( 'openHarmonyIncludeDebug', false );\n  if( oh_incl==\"false\" ) oh_incl = preferences.getString( 'openHarmonyInclude', false );\n  if( oh_incl==\"false\" ) oh_incl = \"openHarmony.js\";\n  include(oh_incl);\n}catch(err){\n  MessageBox.information (\"OpenHarmony is not installed. Get it here: \\nhttps://github.com/cfourney/OpenHarmony\")\n}\n\n/**\n *  Finds and removes all unnecessary asset files from the filesystem.\n */\nfunction oh_rigging_removeUnnecesaryPaletteFiles(){\n  var palette_list = $.scene.palettes;\n  var registered_palette_files = {};\n  \n  //Find the path of all registered palettes. Add it to a group for easy lookup.\n  for( var n=0;n<palette_list.length;n++ ){\n    //-- Find the files for the palettes.\n    var t_pal = palette_list[n];\n    registered_palette_files[ t_pal.path ] = t_pal;\n    \n  }\n  \n  //The scene path of this file.\n  var scene_path = $.scene.path;\n  \n  //Look the all files in the plugin folder that are left over for some reason.\n  \n  var unreferenced_palettes = [];\n  \n  //Find the palette_library \n  var palette_folder = scene_path.get( \"palette-library\" );\n  if( !palette_folder ){\n    $.dialog.alert( \"Palette Details\", \"Unable to find the palette-library.\" );\n    return;\n  }\n  \n  //Find all palette files within the palette folder (*.plt)\n  var fls = palette_folder.files;\n  for( var n=0;n<fls.length;n++ ){\n    var t_fl = fls[n];\n    if( t_fl.extension.toUpperCase() == \"PLT\" ){\n      if( !registered_palette_files[ t_fl.path ] ){\n        unreferenced_palettes.push( t_fl );\n        \n      }\n    }\n  }\n  \n  if( unreferenced_palettes.length == 0 ){\n    $.dialog.alert( \"Palette Details\", \"No unnecessary palettes to remove.\" );\n    return;\n  }\n  \n  //Confirm the action\n  var labelText = 'Remove ' + unreferenced_palettes.length + ' unnecessary palette(s)?\\n';\n      for( var n=0;n<Math.min( unreferenced_palettes.length, 3 ); n++ ){\n        labelText += '      '+unreferenced_palettes[n].fullName + '\\n';\n      }\n      if( unreferenced_palettes.length>3 ){\n        labelText += '\\n      and '+(unreferenced_palettes.length-3)+' more . . .';\n      }\n      \n  var confirmation = $.dialog.confirm( \"Remove Palettes\", labelText );\n  \n  if( confirmation ){\n    //Delete all palettes from disk.\n    var prog = new $.dialog.Progress( \"Removing Palettes\", unreferenced_palettes.length, true );\n      \n    for( var n=0;n<unreferenced_palettes.length; n++ ){\n      var t_pal_fl = unreferenced_palettes[n];\n      \n      prog.text = \"Removing: \" + t_pal_fl.fullName;\n      prog.value = n;\n      \n      t_pal_fl.remove();\n    }\n    \n    prog.close();\n  }\n}\n\n/**\n *  Adds a peg with a pivot at the center of the selected drawings module(s).\n */\nfunction oh_rigging_addCenterWeightedPeg(){\n  scene.beginUndoRedoAccum( \"oh_rigging_addCenterWeightedPeg\" );\n  \n  //Work only on SELECTED READ modules.\n  var nodes = $.scene.nodeSearch( \"#READ(SELECTED)\" );\n  \n  for( var n=0;n<nodes.length;n++ ){\n    \n    //Get a curve representation of the drawing.\n    var curves = nodes[n].getContourCurves( 250 );\n    var avg = new oPoint( 0.0, 0.0, 0.0 );\n    var cnt = 0;\n    \n    //Get the average of the curve end points.\n    for( var x=0;x<curves.length;x++ ){\n      avg.pointAdd( curves[x][0] );\n      cnt+=1;\n\n      if( x==curves.length-1 ){\n        avg.pointAdd( curves[x][3] );\n        cnt+=1;\n      }\n    }\n    \n    //The average of all the points.\n    avg.divide( cnt );\n    \n    //Get the in node of the drawing.\n    var innode  = nodes[n].ins[0];\n    \n    //Create the node, increment if the name already exists.\n    var res = $.scene.addNode( \"PEG\", nodes[n].name+\"-P\", nodes[n].parent, new oPoint(0.0, 0.0, 0.0), true );\n    res.pivot = avg;\n    \n    res.centerAbove( [nodes[n]], 0.0, -50.0 );\n    if( innode ){ \n      res.y = ((innode.y - nodes[n].y)*0.25) + nodes[n].y;\n    }\n    \n    //Insert it in, add it between the existing modules if necessary.\n    nodes[n].insertInNode( 0, res, 0, 0 );\n  }\n  \n  scene.endUndoRedoAccum( );\n}\n\n\n/**\n *  Adds a backdrop with specified color, to selected nodes in the node-view.\n */\nfunction oh_rigging_addBackdropToSelected(){\n  scene.beginUndoRedoAccum( \"oh_rigging_addBackdropToSelected\" );\n\n  //The path to the UI should be here:\n  var oh_colorPicker = specialFolders.userScripts + \"/openHarmony_basic_backdropPicker.ui\";\n  var ui_file = new $.oFile( oh_colorPicker );\n\n  if( !ui_file.exists ){\n    MessageBox.information( \"Unable to find UI file for the command -- please ensure it was installed properly.\" );\n    return;\n  }\n\n  var backdropGUI = function(){\n    this.gui = UiLoader.load( oh_colorPicker );\n    \n    this.gui_labelColor  = this.gui.backdropColor;\n    this.gui_buttonColor = this.gui.backdropColorButton;\n    this.gui_name        = this.gui.backdropName;\n    this.gui_text        = this.gui.backdropText;\n    \n    //When the change color button is clicked.\n    this.changeColor = function( ev ){\n      try{\n        var colorDialog = new QColorDialog( new QColor( this.color.r, this.color.g, this.color.b ) );\n        var color_res = colorDialog.exec();\n        \n        if( !color_res ){\n          return;\n        }\n        \n        var qcol = colorDialog.currentColor;\n        this.color.r = qcol.red(); this.color.g = qcol.green(); this.color.b = qcol.blue(); this.color.alpha = qcol.alpha();\n        \n        var hex_color = this.color.toString().slice( 0, 7 );\n          \n        this.gui_labelColor.setStyleSheet(\"QLabel { background-color : \"+hex_color+\"; color : blue; opacity:0}\");\n        this.gui_labelColor.update();\n      }catch(err){\n        System.println( err + \" (\"+err.fileName+\" \"+err.lineNumber+\")\" );\n      }\n    }\n    \n    this.gui_buttonColor[\"clicked\"].connect( this, this.changeColor );\n    \n    //When the dialog comes into view, select the input area immediately.\n    var gui_focuser  = this.gui;\n    var text_focuser = this.gui_name;\n    this.focusInput = function( ev ){\n    \n      try{\n        gui_focuser.activateWindow();\n        gui_focuser.raise();\n        gui_focuser.setFocus();\n        \n        text_focuser.setFocus();\n        text_focuser.selectAll();\n      }catch( err ){\n        System.println( err );\n      }\n    }\n    \n    //WHEN LAUNCHING THE DIALOG, INITIALIZE IT.\n    this.exec = function( name, text, color ){\n      this.color = new $.oColorValue( Math.random()*256|0, Math.random()*256|0, Math.random()*256|0 );\n      var hex_color = this.color.toString().slice( 0, 7 );\n      \n      this.gui_name.text = name;\n      this.gui_text.text = text;\n      \n      this.gui_labelColor.setStyleSheet(\"QLabel { background-color : \"+hex_color+\"; color : blue; opacity:0}\");\n      \n      //QTimer::singleShot(0, line, SLOT(setFocus()));\n      var focusTimer = new QTimer();\n          focusTimer.singleShot = true;\n          focusTimer[\"timeout\"].connect( this, this.focusInput );\n          focusTimer.start( 50 );\n      \n      var result = this.gui.exec();\n      \n      if( !result ){\n        return false;\n      }\n      \n      \n      return { \n                \"name\"  : this.gui_name.text, \n                \"text\"  : this.gui_text.plainText,\n                \"color\" : this.color\n              };\n    }\n  }\n\n  var color_selector = new backdropGUI();\n  \n  var equivalent = {\n                      \"HAND\"  : \"ARM\",\n                      \"FOOT\"  : \"LEG\",\n                      \n                      \"EYE\"     : \"FEATURES\",\n                      \"EYES\"    : \"FEATURES\",\n                      \"NOSE\"    : \"FEATURES\",\n                      \"MOUTH\"   : \"FEATURES\",\n                      \"PUPIL\"   : \"FEATURES\",\n                      \n                      \"EYE\"     : \"HEAD\",\n                      \"EYES\"    : \"HEAD\",\n                      \"NOSE\"    : \"HEAD\",\n                      \"MOUTH\"   : \"HEAD\",\n                      \"PUPIL\"   : \"HEAD\",\n                      \"EAR\"     : \"HEAD\",\n                      \"HAIR\"    : \"HEAD\",\n                      \"BROW\"    : \"HEAD\",\n                      \"EYEBROW\" : \"HEAD\",\n                    };\n  \n  //Separate selections into common groups.\n  var group_items = {};\n  \n  var nodes = $.scene.nodeSearch( \"(SELECTED)\" );\n  \n  //Iterate the nodes, and separate them into common groups.\n  for( var n=0;n<nodes.length;n++ ){\n    var tnode = nodes[n];\n    if( !group_items[tnode.group] ){\n      group_items[tnode.group] = [];\n    }\n    \n    group_items[tnode.group].push( tnode );\n  }\n  \n  //Now, with each group in mind, lets provide an interface to colour, ect.\n  for( var grp in group_items ){\n    var grp_items = group_items[ grp ];\n    var laststring = false;\n    var common_substrings = {};\n    \n    var numNodes = grp_items.length;\n    \n    // Find the longest common substring. This accumulates the common substrings, and makes a 'voting' system that will subsequently name\n    // the backdrop based on the common result (as 'voted').\n    for (i = 0; i < numNodes; ++i){\n      var node = grp_items[i];\n      \n      var bnm = node.name.toUpperCase();\n          \n      if( bnm.slice( bnm.length-2 ).toUpperCase() == (\"-P\") ){\n        bnm = bnm.slice( 0, bnm.length-2 );\n      }\n          \n      if( laststring ){\n        var lcs = $.utils.longestCommonSubstring( bnm, laststring );\n        \n        if( lcs.length>2 ){\n          var clean_lcs = lcs.sequence;\n          \n          if( clean_lcs.slice( clean_lcs.length-1 ) == (\"_\") ){\n            clean_lcs = clean_lcs.slice( 0, clean_lcs.length-1 );\n          }\n            \n          if( clean_lcs.slice( 0,1 ) == (\"_\") ){\n            clean_lcs = clean_lcs.slice( 1 );\n          }\n          \n          if(!common_substrings[clean_lcs]){\n            common_substrings[clean_lcs] = 0;\n          }\n          \n          common_substrings[clean_lcs]++;\n        }\n      }\n      \n      laststring = bnm;\n    }\n\n    var names = [];\n    for( var n in common_substrings ){\n      names.push( n );\n    }\n    \n    //Now compare cleaned LCS and accumulate them as votes as well.\n    for( var n=0;n<names.length;n++ ){\n      for( var x=n+1;x<names.length;x++ ){\n        var lcs = $.utils.longestCommonSubstring(names[n], names[x]);\n        if( lcs.length>2 ){\n          var clean_lcs = lcs.sequence;\n          \n          if( clean_lcs.slice( clean_lcs.length-1 ) == (\"_\") ){\n            clean_lcs = clean_lcs.slice( 0, clean_lcs.length-1 );\n          } \n          if( clean_lcs.slice( 0,1 ) == (\"_\") ){\n            clean_lcs = clean_lcs.slice( 1 );\n          }\n          \n          if(!common_substrings[clean_lcs]){\n            common_substrings[clean_lcs] = 0;\n          }\n          common_substrings[clean_lcs]++;\n          \n          if( equivalent[clean_lcs] ){\n            var clean_lcs2 = equivalent[clean_lcs];\n            if(!common_substrings[clean_lcs2]){\n              common_substrings[clean_lcs2] = 0;\n            }\n            common_substrings[clean_lcs2]++;\n          }\n        }\n      }\n    }\n    \n    //Find the highest voted LCS.\n    var highest    = 0;\n    var common_name = 'Backdrop';\n    for( var n in common_substrings ){\n      if( common_substrings[n] > highest ){\n        if( n.toUpperCase()==\"DRAWING\" ){\n          continue;\n        }\n        \n        highest    = common_substrings[n];\n        common_name = n;\n      }\n    }\n    \n    var res = color_selector.exec( common_name, \"\", \"\" );\n    if( !res ){ //A cancel option was selected.\n      continue;\n    }\n    \n    //Add that beautiful backdrop.\n    $.scene.addBackdropToNodes( grp, grp_items, res.name, res.text, res.color, 0, 0, 35, 35 );\n  }\n  \n  scene.endUndoRedoAccum( );\n}\n\n\n/**\n *  Sets the peg's pivot based on a clicked position in the interface.\n */\nfunction oh_rigging_setSelectedPegPivotWithClick(){\n  var nodes = $.scene.nodeSearch( \"#PEG(SELECTED)\" );\n  \n  if( nodes.length == 0 ){\n    $.dialog.alert( \"No peg selected.\" );\n    return;\n  }\n  \n  Action.perform( \"onActionChoosePencilTool()\" );\n  var context = this;\n  \n    var setPiv = function( res ){\n      var $    = context.$;\n      var m    = context;\n          \n      $.beginUndo();\n      try{\n        for( var n=0;n<nodes.length;n++ ){\n          nodes[n].pivot = new $.oPoint( res[0][0], res[0][1], 0.0 );\n        }\n      }catch( err ){\n        System.println( err + \" \" + err.lineNumber + \" \" + err.fileName );\n      }\n      Action.perform( \"onActionChooseSpTransformTool()\" );\n      $.endUndo();\n    }\n  \n    //We're using the EnvelopeCreator interface for a click interface. This may have been removed in newer versions of Harmony.\n    var en  = new EnvelopeCreator();\n    en.drawPathOverlay( setPiv, 1 );\n  \n}"
  },
  {
    "path": "openpype/hosts/hiero/__init__.py",
    "content": "from .addon import (\n    HIERO_ROOT_DIR,\n    HieroAddon,\n)\n\n\n__all__ = (\n    \"HIERO_ROOT_DIR\",\n    \"HieroAddon\",\n)\n"
  },
  {
    "path": "openpype/hosts/hiero/addon.py",
    "content": "import os\nimport platform\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nHIERO_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass HieroAddon(OpenPypeModule, IHostAddon):\n    name = \"hiero\"\n    host_name = \"hiero\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        # Add requirements to HIERO_PLUGIN_PATH\n        new_hiero_paths = [\n            os.path.join(HIERO_ROOT_DIR, \"api\", \"startup\")\n        ]\n        old_hiero_path = env.get(\"HIERO_PLUGIN_PATH\") or \"\"\n        for path in old_hiero_path.split(os.pathsep):\n            if not path:\n                continue\n\n            norm_path = os.path.normpath(path)\n            if norm_path not in new_hiero_paths:\n                new_hiero_paths.append(norm_path)\n\n        env[\"HIERO_PLUGIN_PATH\"] = os.pathsep.join(new_hiero_paths)\n        # Remove auto screen scale factor for Qt\n        # - let Hiero decide it's value\n        env.pop(\"QT_AUTO_SCREEN_SCALE_FACTOR\", None)\n        # Remove tkinter library paths if are set\n        env.pop(\"TK_LIBRARY\", None)\n        env.pop(\"TCL_LIBRARY\", None)\n\n        # Add vendor to PYTHONPATH\n        python_path = env[\"PYTHONPATH\"]\n        python_path_parts = []\n        if python_path:\n            python_path_parts = python_path.split(os.pathsep)\n        vendor_path = os.path.join(HIERO_ROOT_DIR, \"vendor\")\n        python_path_parts.insert(0, vendor_path)\n        env[\"PYTHONPATH\"] = os.pathsep.join(python_path_parts)\n\n        # Set default values if are not already set via settings\n        defaults = {\n            \"LOGLEVEL\": \"DEBUG\"\n        }\n        for key, value in defaults.items():\n            if not env.get(key):\n                env[key] = value\n\n        # Try to add QuickTime to PATH\n        quick_time_path = \"C:/Program Files (x86)/QuickTime/QTSystem\"\n        if platform.system() == \"windows\" and os.path.exists(quick_time_path):\n            path_value = env.get(\"PATH\") or \"\"\n            path_paths = [\n                path\n                for path in path_value.split(os.pathsep)\n                if path\n            ]\n            path_paths.append(quick_time_path)\n            env[\"PATH\"] = os.pathsep.join(path_paths)\n\n    def get_workfile_extensions(self):\n        return [\".hrox\"]\n"
  },
  {
    "path": "openpype/hosts/hiero/api/__init__.py",
    "content": "from .workio import (\n    open_file,\n    save_file,\n    current_file,\n    has_unsaved_changes,\n    file_extensions,\n    work_root\n)\n\nfrom .pipeline import (\n    launch_workfiles_app,\n    ls,\n    install,\n    uninstall,\n    reload_config,\n    containerise,\n    publish,\n    maintained_selection,\n    parse_container,\n    update_container,\n    reset_selection\n)\n\nfrom .constants import (\n    OPENPYPE_TAG_NAME,\n    DEFAULT_SEQUENCE_NAME,\n    DEFAULT_BIN_NAME\n)\n\nfrom .lib import (\n    flatten,\n    get_track_items,\n    get_current_project,\n    get_current_sequence,\n    get_timeline_selection,\n    get_current_track,\n    get_track_item_tags,\n    get_track_openpype_tag,\n    set_track_openpype_tag,\n    get_track_openpype_data,\n    get_track_item_pype_tag,\n    set_track_item_pype_tag,\n    get_track_item_pype_data,\n    get_trackitem_openpype_tag,\n    set_trackitem_openpype_tag,\n    get_trackitem_openpype_data,\n    set_publish_attribute,\n    get_publish_attribute,\n    imprint,\n    get_selected_track_items,\n    set_selected_track_items,\n    create_nuke_workfile_clips,\n    create_bin,\n    apply_colorspace_project,\n    apply_colorspace_clips,\n    is_overlapping,\n    get_sequence_pattern_and_padding\n)\n\nfrom .plugin import (\n    CreatorWidget,\n    Creator,\n    PublishClip,\n    SequenceLoader,\n    ClipLoader\n)\n\n__all__ = [\n    # avalon pipeline module\n    \"launch_workfiles_app\",\n    \"ls\",\n    \"install\",\n    \"uninstall\",\n    \"reload_config\",\n    \"containerise\",\n    \"publish\",\n    \"maintained_selection\",\n    \"parse_container\",\n    \"update_container\",\n    \"reset_selection\",\n\n    # Workfiles API\n    \"open_file\",\n    \"save_file\",\n    \"current_file\",\n    \"has_unsaved_changes\",\n    \"file_extensions\",\n    \"work_root\",\n\n    # Constants\n    \"OPENPYPE_TAG_NAME\",\n    \"DEFAULT_SEQUENCE_NAME\",\n    \"DEFAULT_BIN_NAME\",\n\n    # Lib functions\n    \"flatten\",\n    \"get_track_items\",\n    \"get_current_project\",\n    \"get_current_sequence\",\n    \"get_timeline_selection\",\n    \"get_current_track\",\n    \"get_track_item_tags\",\n    \"get_track_openpype_tag\",\n    \"set_track_openpype_tag\",\n    \"get_track_openpype_data\",\n    \"get_trackitem_openpype_tag\",\n    \"set_trackitem_openpype_tag\",\n    \"get_trackitem_openpype_data\",\n    \"set_publish_attribute\",\n    \"get_publish_attribute\",\n    \"imprint\",\n    \"get_selected_track_items\",\n    \"set_selected_track_items\",\n    \"create_nuke_workfile_clips\",\n    \"create_bin\",\n    \"is_overlapping\",\n    \"apply_colorspace_project\",\n    \"apply_colorspace_clips\",\n    \"get_sequence_pattern_and_padding\",\n    # deprecated\n    \"get_track_item_pype_tag\",\n    \"set_track_item_pype_tag\",\n    \"get_track_item_pype_data\",\n\n    # plugins\n    \"CreatorWidget\",\n    \"Creator\",\n    \"PublishClip\",\n    \"SequenceLoader\",\n    \"ClipLoader\"\n]\n"
  },
  {
    "path": "openpype/hosts/hiero/api/constants.py",
    "content": "OPENPYPE_TAG_NAME = \"openpypeData\"\nDEFAULT_SEQUENCE_NAME = \"openpypeSequence\"\nDEFAULT_BIN_NAME = \"openpypeBin\"\n"
  },
  {
    "path": "openpype/hosts/hiero/api/events.py",
    "content": "import os\nimport hiero.core.events\nfrom openpype.lib import Logger, register_event_callback\nfrom .lib import (\n    sync_avalon_data_to_workfile,\n    launch_workfiles_app,\n    before_project_save,\n    apply_colorspace_project\n)\nfrom .tags import add_tags_to_workfile\nfrom .menu import update_menu_task_label\n\nlog = Logger.get_logger(__name__)\n\n\ndef startupCompleted(event):\n    log.info(\"startup competed event...\")\n    return\n\n\ndef shutDown(event):\n    log.info(\"shut down event...\")\n    return\n\n\ndef beforeNewProjectCreated(event):\n    log.info(\"before new project created event...\")\n    return\n\n\ndef afterNewProjectCreated(event):\n    log.info(\"after new project created event...\")\n    # sync avalon data to project properties\n    sync_avalon_data_to_workfile()\n\n    # add tags from preset\n    add_tags_to_workfile()\n\n    # Workfiles.\n    if int(os.environ.get(\"WORKFILES_STARTUP\", \"0\")):\n        hiero.core.events.sendEvent(\"kStartWorkfiles\", None)\n        # reset workfiles startup not to open any more in session\n        os.environ[\"WORKFILES_STARTUP\"] = \"0\"\n\n    apply_colorspace_project()\n\n\ndef beforeProjectLoad(event):\n    log.info(\"before project load event...\")\n    return\n\n\ndef afterProjectLoad(event):\n    log.info(\"after project load event...\")\n    # sync avalon data to project properties\n    sync_avalon_data_to_workfile()\n\n    # add tags from preset\n    add_tags_to_workfile()\n\n\ndef beforeProjectClosed(event):\n    log.info(\"before project closed event...\")\n    return\n\n\ndef afterProjectClosed(event):\n    log.info(\"after project closed event...\")\n    return\n\n\ndef beforeProjectSaved(event):\n    log.info(\"before project saved event...\")\n    return\n\n\ndef afterProjectSaved(event):\n    log.info(\"after project saved event...\")\n    return\n\n\ndef register_hiero_events():\n    log.info(\n        \"Registering events for: kBeforeNewProjectCreated, \"\n        \"kAfterNewProjectCreated, kBeforeProjectLoad, kAfterProjectLoad, \"\n        \"kBeforeProjectSave, kAfterProjectSave, kBeforeProjectClose, \"\n        \"kAfterProjectClose, kShutdown, kStartup, kSelectionChanged\"\n    )\n\n    # hiero.core.events.registerInterest(\n    #     \"kBeforeNewProjectCreated\", beforeNewProjectCreated)\n    hiero.core.events.registerInterest(\n        \"kAfterNewProjectCreated\", afterNewProjectCreated)\n\n    # hiero.core.events.registerInterest(\n    #     \"kBeforeProjectLoad\", beforeProjectLoad)\n    hiero.core.events.registerInterest(\n        \"kAfterProjectLoad\", afterProjectLoad)\n\n    hiero.core.events.registerInterest(\n        \"kBeforeProjectSave\", before_project_save)\n    # hiero.core.events.registerInterest(\n    #     \"kAfterProjectSave\", afterProjectSaved)\n    #\n    # hiero.core.events.registerInterest(\n    #     \"kBeforeProjectClose\", beforeProjectClosed)\n    # hiero.core.events.registerInterest(\n    #     \"kAfterProjectClose\", afterProjectClosed)\n    #\n    # hiero.core.events.registerInterest(\"kShutdown\", shutDown)\n    # hiero.core.events.registerInterest(\"kStartup\", startupCompleted)\n\n    # INFO: was disabled because it was slowing down timeline operations\n    # hiero.core.events.registerInterest(\n    #     (\"kSelectionChanged\", \"kTimeline\"), selection_changed_timeline)\n\n    # workfiles\n    try:\n        hiero.core.events.registerEventType(\"kStartWorkfiles\")\n        hiero.core.events.registerInterest(\n            \"kStartWorkfiles\", launch_workfiles_app)\n    except RuntimeError:\n        pass\n\ndef register_events():\n    \"\"\"\n    Adding all callbacks.\n    \"\"\"\n\n    # if task changed then change notext of hiero\n    register_event_callback(\"taskChanged\", update_menu_task_label)\n    log.info(\"Installed event callback for 'taskChanged'..\")\n"
  },
  {
    "path": "openpype/hosts/hiero/api/launchforhiero.py",
    "content": "import logging\n\nfrom scriptsmenu import scriptsmenu\nfrom qtpy import QtWidgets\n\n\nlog = logging.getLogger(__name__)\n\n\ndef _hiero_main_window():\n    \"\"\"Return Hiero's main window\"\"\"\n    for obj in QtWidgets.QApplication.topLevelWidgets():\n        if (obj.inherits('QMainWindow') and\n                obj.metaObject().className() == 'Foundry::UI::DockMainWindow'):\n            return obj\n    raise RuntimeError('Could not find HieroWindow instance')\n\n\ndef _hiero_main_menubar():\n    \"\"\"Retrieve the main menubar of the Hiero window\"\"\"\n    hiero_window = _hiero_main_window()\n    menubar = [i for i in hiero_window.children() if isinstance(\n        i,\n        QtWidgets.QMenuBar\n    )]\n\n    assert len(menubar) == 1, \"Error, could not find menu bar!\"\n    return menubar[0]\n\n\ndef find_scripts_menu(title, parent):\n    \"\"\"\n    Check if the menu exists with the given title in the parent\n\n    Args:\n        title (str): the title name of the scripts menu\n\n        parent (QtWidgets.QMenuBar): the menubar to check\n\n    Returns:\n        QtWidgets.QMenu or None\n\n    \"\"\"\n\n    menu = None\n    search = [i for i in parent.children() if\n              isinstance(i, scriptsmenu.ScriptsMenu)\n              and i.title() == title]\n    if search:\n        assert len(search) < 2, (\"Multiple instances of menu '{}' \"\n                                 \"in menu bar\".format(title))\n        menu = search[0]\n\n    return menu\n\n\ndef main(title=\"Scripts\", parent=None, objectName=None):\n    \"\"\"Build the main scripts menu in Hiero\n\n    Args:\n        title (str): name of the menu in the application\n\n        parent (QtWidgets.QtMenuBar): the parent object for the menu\n\n        objectName (str): custom objectName for scripts menu\n\n    Returns:\n        scriptsmenu.ScriptsMenu instance\n\n    \"\"\"\n    hieromainbar = parent or _hiero_main_menubar()\n    try:\n        # check menu already exists\n        menu = find_scripts_menu(title, hieromainbar)\n        if not menu:\n            log.info(\"Attempting to build menu ...\")\n            object_name = objectName or title.lower()\n            menu = scriptsmenu.ScriptsMenu(title=title,\n                                           parent=hieromainbar,\n                                           objectName=object_name)\n    except Exception as e:\n        log.error(e)\n        return\n\n    return menu\n"
  },
  {
    "path": "openpype/hosts/hiero/api/lib.py",
    "content": "\"\"\"\nHost specific functions where host api is connected\n\"\"\"\n\nfrom copy import deepcopy\nimport os\nimport re\nimport platform\nimport functools\nimport warnings\nimport json\nimport ast\nimport secrets\nimport shutil\nimport hiero\n\nfrom qtpy import QtWidgets, QtCore\ntry:\n    from PySide import QtXml\nexcept ImportError:\n    from PySide2 import QtXml\n\nfrom openpype.client import get_project\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import Anatomy, get_current_project_name\nfrom openpype.pipeline.load import filter_containers\nfrom openpype.lib import Logger\nfrom . import tags\nfrom .constants import (\n    OPENPYPE_TAG_NAME,\n    DEFAULT_SEQUENCE_NAME,\n    DEFAULT_BIN_NAME\n)\nfrom openpype.pipeline.colorspace import (\n    get_imageio_config\n)\n\n\nclass _CTX:\n    has_been_setup = False\n    has_menu = False\n    parent_gui = None\n\n\nclass DeprecatedWarning(DeprecationWarning):\n    pass\n\n\ndef deprecated(new_destination):\n    \"\"\"Mark functions as deprecated.\n\n    It will result in a warning being emitted when the function is used.\n    \"\"\"\n\n    func = None\n    if callable(new_destination):\n        func = new_destination\n        new_destination = None\n\n    def _decorator(decorated_func):\n        if new_destination is None:\n            warning_message = (\n                \" Please check content of deprecated function to figure out\"\n                \" possible replacement.\"\n            )\n        else:\n            warning_message = \" Please replace your usage with '{}'.\".format(\n                new_destination\n            )\n\n        @functools.wraps(decorated_func)\n        def wrapper(*args, **kwargs):\n            warnings.simplefilter(\"always\", DeprecatedWarning)\n            warnings.warn(\n                (\n                    \"Call to deprecated function '{}'\"\n                    \"\\nFunction was moved or removed.{}\"\n                ).format(decorated_func.__name__, warning_message),\n                category=DeprecatedWarning,\n                stacklevel=4\n            )\n            return decorated_func(*args, **kwargs)\n        return wrapper\n\n    if func is None:\n        return _decorator\n    return _decorator(func)\n\n\nlog = Logger.get_logger(__name__)\n\n\ndef flatten(list_):\n    for item_ in list_:\n        if isinstance(item_, (list, tuple)):\n            for sub_item in flatten(item_):\n                yield sub_item\n        else:\n            yield item_\n\n\ndef get_current_project(remove_untitled=False):\n    projects = hiero.core.projects()\n    if not remove_untitled:\n        return projects[0]\n\n    # if remove_untitled\n    for proj in projects:\n        if \"Untitled\" in proj.name():\n            proj.close()\n        else:\n            return proj\n\n\ndef get_current_sequence(name=None, new=False):\n    \"\"\"\n    Get current sequence in context of active project.\n\n    Args:\n        name (str)[optional]: name of sequence we want to return\n        new (bool)[optional]: if we want to create new one\n\n    Returns:\n        hiero.core.Sequence: the sequence object\n    \"\"\"\n    sequence = None\n    project = get_current_project()\n    root_bin = project.clipsBin()\n\n    if new:\n        # create new\n        name = name or DEFAULT_SEQUENCE_NAME\n        sequence = hiero.core.Sequence(name)\n        root_bin.addItem(hiero.core.BinItem(sequence))\n    elif name:\n        # look for sequence by name\n        sequences = project.sequences()\n        for _sequence in sequences:\n            if _sequence.name() == name:\n                sequence = _sequence\n        if not sequence:\n            # if nothing found create new with input name\n            sequence = get_current_sequence(name, True)\n    else:\n        # if name is none and new is False then return current open sequence\n        sequence = hiero.ui.activeSequence()\n\n    return sequence\n\n\ndef get_timeline_selection():\n    active_sequence = hiero.ui.activeSequence()\n    timeline_editor = hiero.ui.getTimelineEditor(active_sequence)\n    return list(timeline_editor.selection())\n\n\ndef get_current_track(sequence, name, audio=False):\n    \"\"\"\n    Get current track in context of active project.\n\n    Creates new if none is found.\n\n    Args:\n        sequence (hiero.core.Sequence): hiero sequene object\n        name (str): name of track we want to return\n        audio (bool)[optional]: switch to AudioTrack\n\n    Returns:\n        hiero.core.Track: the track object\n    \"\"\"\n    tracks = sequence.videoTracks()\n\n    if audio:\n        tracks = sequence.audioTracks()\n\n    # get track by name\n    track = None\n    for _track in tracks:\n        if _track.name() == name:\n            track = _track\n\n    if not track:\n        if not audio:\n            track = hiero.core.VideoTrack(name)\n        else:\n            track = hiero.core.AudioTrack(name)\n\n        sequence.addTrack(track)\n\n    return track\n\n\ndef get_track_items(\n        selection=False,\n        sequence_name=None,\n        track_item_name=None,\n        track_name=None,\n        track_type=None,\n        check_enabled=True,\n        check_locked=True,\n        check_tagged=False):\n    \"\"\"Get all available current timeline track items.\n\n    Attribute:\n        selection (list)[optional]: list of selected track items\n        sequence_name (str)[optional]: return only clips from input sequence\n        track_item_name (str)[optional]: return only item with input name\n        track_name (str)[optional]: return only items from track name\n        track_type (str)[optional]: return only items of given type\n                                    (`audio` or `video`) default is `video`\n        check_enabled (bool)[optional]: ignore disabled if True\n        check_locked (bool)[optional]: ignore locked if True\n\n    Return:\n        list or hiero.core.TrackItem: list of track items or single track item\n    \"\"\"\n    track_type = track_type or \"video\"\n    selection = selection or []\n    return_list = []\n\n    # get selected track items or all in active sequence\n    if selection:\n        try:\n            for track_item in selection:\n                log.info(\"___ track_item: {}\".format(track_item))\n                # make sure only trackitems are selected\n                if not isinstance(track_item, hiero.core.TrackItem):\n                    continue\n\n                if _validate_all_atrributes(\n                    track_item,\n                    track_item_name,\n                    track_name,\n                    track_type,\n                    check_enabled,\n                    check_tagged\n                ):\n                    log.info(\"___ valid trackitem: {}\".format(track_item))\n                    return_list.append(track_item)\n        except AttributeError:\n            pass\n\n    # collect all available active sequence track items\n    if not return_list:\n        sequence = get_current_sequence(name=sequence_name)\n\n        tracks = []\n        if sequence is not None:\n            # get all available tracks from sequence\n            tracks = list(sequence.audioTracks())\n            tracks += list(sequence.videoTracks())\n\n        # loop all tracks\n        for track in tracks:\n            if check_locked and track.isLocked():\n                continue\n            if check_enabled and not track.isEnabled():\n                continue\n            # and all items in track\n            for track_item in track.items():\n                # make sure no subtrackitem is also track items\n                if not isinstance(track_item, hiero.core.TrackItem):\n                    continue\n\n                if _validate_all_atrributes(\n                    track_item,\n                    track_item_name,\n                    track_name,\n                    track_type,\n                    check_enabled,\n                    check_tagged\n                ):\n                    return_list.append(track_item)\n\n    return return_list\n\n\ndef _validate_all_atrributes(\n    track_item,\n    track_item_name,\n    track_name,\n    track_type,\n    check_enabled,\n    check_tagged\n):\n    def _validate_correct_name_track_item():\n        if track_item_name and track_item_name in track_item.name():\n            return True\n        elif not track_item_name:\n            return True\n\n    def _validate_tagged_track_item():\n        if check_tagged and track_item.tags():\n            return True\n        elif not check_tagged:\n            return True\n\n    def _validate_enabled_track_item():\n        if check_enabled and track_item.isEnabled():\n            return True\n        elif not check_enabled:\n            return True\n\n    def _validate_parent_track_item():\n        if track_name and track_name in track_item.parent().name():\n            # filter only items fitting input track name\n            return True\n        elif not track_name:\n            # or add all if no track_name was defined\n            return True\n\n    def _validate_type_track_item():\n        if track_type == \"video\" and isinstance(\n                track_item.parent(), hiero.core.VideoTrack):\n            # only video track items are allowed\n            return True\n        elif track_type == \"audio\" and isinstance(\n                track_item.parent(), hiero.core.AudioTrack):\n            # only audio track items are allowed\n            return True\n\n    # check if track item is enabled\n    return all([\n        _validate_enabled_track_item(),\n        _validate_type_track_item(),\n        _validate_tagged_track_item(),\n        _validate_parent_track_item(),\n        _validate_correct_name_track_item()\n    ])\n\n\ndef get_track_item_tags(track_item):\n    \"\"\"\n    Get track item tags excluded openpype tag\n\n    Attributes:\n        trackItem (hiero.core.TrackItem): hiero object\n\n    Returns:\n        hiero.core.Tag: hierarchy, orig clip attributes\n    \"\"\"\n    returning_tag_data = []\n    # get all tags from track item\n    _tags = track_item.tags()\n    if not _tags:\n        return []\n\n    # collect all tags which are not openpype tag\n    returning_tag_data.extend(\n        tag for tag in _tags\n        if tag.name() != OPENPYPE_TAG_NAME\n    )\n\n    return returning_tag_data\n\n\ndef _get_tag_unique_hash():\n    # sourcery skip: avoid-builtin-shadow\n    return secrets.token_hex(nbytes=4)\n\n\ndef set_track_openpype_tag(track, data=None):\n    \"\"\"\n    Set openpype track tag to input track object.\n\n    Attributes:\n        track (hiero.core.VideoTrack): hiero object\n\n    Returns:\n        hiero.core.Tag\n    \"\"\"\n    data = data or {}\n\n    # basic Tag's attribute\n    tag_data = {\n        \"editable\": \"0\",\n        \"note\": \"OpenPype data container\",\n        \"icon\": \"openpype_icon.png\",\n        \"metadata\": dict(data.items())\n    }\n    # get available pype tag if any\n    _tag = get_track_openpype_tag(track)\n\n    if _tag:\n        # it not tag then create one\n        tag = tags.update_tag(_tag, tag_data)\n    else:\n        # if pype tag available then update with input data\n        tag = tags.create_tag(\n            \"{}_{}\".format(\n                OPENPYPE_TAG_NAME,\n                _get_tag_unique_hash()\n            ),\n            tag_data\n        )\n        # add it to the input track item\n        track.addTag(tag)\n\n    return tag\n\n\ndef get_track_openpype_tag(track):\n    \"\"\"\n    Get pype track item tag created by creator or loader plugin.\n\n    Attributes:\n        trackItem (hiero.core.TrackItem): hiero object\n\n    Returns:\n        hiero.core.Tag: hierarchy, orig clip attributes\n    \"\"\"\n    # get all tags from track item\n    _tags = track.tags()\n    if not _tags:\n        return None\n    for tag in _tags:\n        # return only correct tag defined by global name\n        if OPENPYPE_TAG_NAME in tag.name():\n            return tag\n\n\ndef get_track_openpype_data(track, container_name=None):\n    \"\"\"\n    Get track's openpype tag data.\n\n    Attributes:\n        trackItem (hiero.core.VideoTrack): hiero object\n\n    Returns:\n        dict: data found on pype tag\n    \"\"\"\n    return_data = {}\n    # get pype data tag from track item\n    tag = get_track_openpype_tag(track)\n\n    if not tag:\n        return None\n\n    # get tag metadata attribute\n    tag_data = deepcopy(dict(tag.metadata()))\n\n    for obj_name, obj_data in tag_data.items():\n        obj_name = obj_name.replace(\"tag.\", \"\")\n\n        if obj_name in [\"applieswhole\", \"note\", \"label\"]:\n            continue\n        return_data[obj_name] = json.loads(obj_data)\n\n    return (\n        return_data[container_name]\n        if container_name\n        else return_data\n    )\n\n\n@deprecated(\"openpype.hosts.hiero.api.lib.get_trackitem_openpype_tag\")\ndef get_track_item_pype_tag(track_item):\n    # backward compatibility alias\n    return get_trackitem_openpype_tag(track_item)\n\n\n@deprecated(\"openpype.hosts.hiero.api.lib.set_trackitem_openpype_tag\")\ndef set_track_item_pype_tag(track_item, data=None):\n    # backward compatibility alias\n    return set_trackitem_openpype_tag(track_item, data)\n\n\n@deprecated(\"openpype.hosts.hiero.api.lib.get_trackitem_openpype_data\")\ndef get_track_item_pype_data(track_item):\n    # backward compatibility alias\n    return get_trackitem_openpype_data(track_item)\n\n\ndef get_trackitem_openpype_tag(track_item):\n    \"\"\"\n    Get pype track item tag created by creator or loader plugin.\n\n    Attributes:\n        trackItem (hiero.core.TrackItem): hiero object\n\n    Returns:\n        hiero.core.Tag: hierarchy, orig clip attributes\n    \"\"\"\n    # get all tags from track item\n    _tags = track_item.tags()\n    if not _tags:\n        return None\n    for tag in _tags:\n        # return only correct tag defined by global name\n        if OPENPYPE_TAG_NAME in tag.name():\n            return tag\n\n\ndef set_trackitem_openpype_tag(track_item, data=None):\n    \"\"\"\n    Set openpype track tag to input track object.\n\n    Attributes:\n        track (hiero.core.VideoTrack): hiero object\n\n    Returns:\n        hiero.core.Tag\n    \"\"\"\n    data = data or {}\n\n    # basic Tag's attribute\n    tag_data = {\n        \"editable\": \"0\",\n        \"note\": \"OpenPype data container\",\n        \"icon\": \"openpype_icon.png\",\n        \"metadata\": dict(data.items())\n    }\n    # get available pype tag if any\n    _tag = get_trackitem_openpype_tag(track_item)\n    if _tag:\n        # it not tag then create one\n        tag = tags.update_tag(_tag, tag_data)\n    else:\n        # if pype tag available then update with input data\n        tag = tags.create_tag(\n            \"{}_{}\".format(\n                OPENPYPE_TAG_NAME,\n                _get_tag_unique_hash()\n            ),\n            tag_data\n        )\n        # add it to the input track item\n        track_item.addTag(tag)\n\n    return tag\n\n\ndef get_trackitem_openpype_data(track_item):\n    \"\"\"\n    Get track item's pype tag data.\n\n    Attributes:\n        trackItem (hiero.core.TrackItem): hiero object\n\n    Returns:\n        dict: data found on pype tag\n    \"\"\"\n    data = {}\n    # get pype data tag from track item\n    tag = get_trackitem_openpype_tag(track_item)\n\n    if not tag:\n        return None\n\n    # get tag metadata attribute\n    tag_data = deepcopy(dict(tag.metadata()))\n    # convert tag metadata to normal keys names and values to correct types\n    for k, v in tag_data.items():\n        key = k.replace(\"tag.\", \"\")\n\n        try:\n            # capture exceptions which are related to strings only\n            if re.match(r\"^[\\d]+$\", v):\n                value = int(v)\n            elif re.match(r\"^True$\", v):\n                value = True\n            elif re.match(r\"^False$\", v):\n                value = False\n            elif re.match(r\"^None$\", v):\n                value = None\n            elif re.match(r\"^[\\w\\d_]+$\", v):\n                value = v\n            else:\n                value = ast.literal_eval(v)\n        except (ValueError, SyntaxError) as msg:\n            log.warning(msg)\n            value = v\n\n        data[key] = value\n\n    return data\n\n\ndef imprint(track_item, data=None):\n    \"\"\"\n    Adding `Avalon data` into a hiero track item tag.\n\n    Also including publish attribute into tag.\n\n    Arguments:\n        track_item (hiero.core.TrackItem): hiero track item object\n        data (dict): Any data which needst to be imprinted\n\n    Examples:\n        data = {\n            'asset': 'sq020sh0280',\n            'family': 'render',\n            'subset': 'subsetMain'\n        }\n    \"\"\"\n    data = data or {}\n\n    tag = set_trackitem_openpype_tag(track_item, data)\n\n    # add publish attribute\n    set_publish_attribute(tag, True)\n\n\ndef set_publish_attribute(tag, value):\n    \"\"\" Set Publish attribute in input Tag object\n\n    Attribute:\n        tag (hiero.core.Tag): a tag object\n        value (bool): True or False\n    \"\"\"\n    tag_data = tag.metadata()\n    # set data to the publish attribute\n    tag_data.setValue(\"tag.publish\", str(value))\n\n\ndef get_publish_attribute(tag):\n    \"\"\" Get Publish attribute from input Tag object\n\n    Attribute:\n        tag (hiero.core.Tag): a tag object\n        value (bool): True or False\n    \"\"\"\n    tag_data = tag.metadata()\n    # get data to the publish attribute\n    value = tag_data.value(\"tag.publish\")\n    # return value converted to bool value. Atring is stored in tag.\n    return ast.literal_eval(value)\n\n\ndef sync_avalon_data_to_workfile():\n    # import session to get project dir\n    project_name = get_current_project_name()\n\n    anatomy = Anatomy(project_name)\n    work_template = anatomy.templates[\"work\"][\"path\"]\n    work_root = anatomy.root_value_for_template(work_template)\n    active_project_root = (\n        os.path.join(work_root, project_name)\n    ).replace(\"\\\\\", \"/\")\n    # getting project\n    project = get_current_project()\n\n    if \"Tag Presets\" in project.name():\n        return\n\n    log.debug(\"Synchronizing Pype metadata to project: {}\".format(\n        project.name()))\n\n    # set project root with backward compatibility\n    try:\n        project.setProjectDirectory(active_project_root)\n    except Exception:\n        # old way of setting it\n        project.setProjectRoot(active_project_root)\n\n    # get project data from avalon db\n    project_doc = get_project(project_name)\n    project_data = project_doc[\"data\"]\n\n    log.debug(\"project_data: {}\".format(project_data))\n\n    # get format and fps property from avalon db on project\n    width = project_data[\"resolutionWidth\"]\n    height = project_data[\"resolutionHeight\"]\n    pixel_aspect = project_data[\"pixelAspect\"]\n    fps = project_data['fps']\n    format_name = project_data['code']\n\n    # create new format in hiero project\n    format = hiero.core.Format(width, height, pixel_aspect, format_name)\n    project.setOutputFormat(format)\n\n    # set fps to hiero project\n    project.setFramerate(fps)\n\n    # TODO: add auto colorspace set from project drop\n    log.info(\"Project property has been synchronised with Avalon db\")\n\n\ndef launch_workfiles_app(event):\n    \"\"\"\n    Event for launching workfiles after hiero start\n\n    Args:\n        event (obj): required but unused\n    \"\"\"\n    from . import launch_workfiles_app\n    launch_workfiles_app()\n\n\ndef setup(console=False, port=None, menu=True):\n    \"\"\"Setup integration\n\n    Registers Pyblish for Hiero plug-ins and appends an item to the File-menu\n\n    Arguments:\n        console (bool): Display console with GUI\n        port (int, optional): Port from which to start looking for an\n            available port to connect with Pyblish QML, default\n            provided by Pyblish Integration.\n        menu (bool, optional): Display file menu in Hiero.\n    \"\"\"\n\n    if _CTX.has_been_setup:\n        teardown()\n\n    add_submission()\n\n    if menu:\n        add_to_filemenu()\n        _CTX.has_menu = True\n\n    _CTX.has_been_setup = True\n    log.debug(\"pyblish: Loaded successfully.\")\n\n\ndef teardown():\n    \"\"\"Remove integration\"\"\"\n    if not _CTX.has_been_setup:\n        return\n\n    if _CTX.has_menu:\n        remove_from_filemenu()\n        _CTX.has_menu = False\n\n    _CTX.has_been_setup = False\n    log.debug(\"pyblish: Integration torn down successfully\")\n\n\ndef remove_from_filemenu():\n    raise NotImplementedError(\"Implement me please.\")\n\n\ndef add_to_filemenu():\n    PublishAction()\n\n\nclass PyblishSubmission(hiero.exporters.FnSubmission.Submission):\n\n    def __init__(self):\n        hiero.exporters.FnSubmission.Submission.__init__(self)\n\n    def addToQueue(self):\n        from . import publish\n        # Add submission to Hiero module for retrieval in plugins.\n        hiero.submission = self\n        publish(hiero.ui.mainWindow())\n\n\ndef add_submission():\n    registry = hiero.core.taskRegistry\n    registry.addSubmission(\"Pyblish\", PyblishSubmission)\n\n\nclass PublishAction(QtWidgets.QAction):\n    \"\"\"\n    Action with is showing as menu item\n    \"\"\"\n\n    def __init__(self):\n        QtWidgets.QAction.__init__(self, \"Publish\", None)\n        self.triggered.connect(self.publish)\n\n        for interest in [\"kShowContextMenu/kTimeline\",\n                         \"kShowContextMenukBin\",\n                         \"kShowContextMenu/kSpreadsheet\"]:\n            hiero.core.events.registerInterest(interest, self.eventHandler)\n\n        self.setShortcut(\"Ctrl+Alt+P\")\n\n    def publish(self):\n        from . import publish\n        # Removing \"submission\" attribute from hiero module, to prevent tasks\n        # from getting picked up when not using the \"Export\" dialog.\n        if hasattr(hiero, \"submission\"):\n            del hiero.submission\n        publish(hiero.ui.mainWindow())\n\n    def eventHandler(self, event):\n        # Add the Menu to the right-click menu\n        event.menu.addAction(self)\n\n\n# def CreateNukeWorkfile(nodes=None,\n#                        nodes_effects=None,\n#                        to_timeline=False,\n#                        **kwargs):\n#     ''' Creating nuke workfile with particular version with given nodes\n#     Also it is creating timeline track items as precomps.\n#\n#     Arguments:\n#         nodes(list of dict): each key in dict is knob order is important\n#         to_timeline(type): will build trackItem with metadata\n#\n#     Returns:\n#         bool: True if done\n#\n#     Raises:\n#         Exception: with traceback\n#\n#     '''\n#     import hiero.core\n#     from openpype.hosts.nuke.api.lib import (\n#         BuildWorkfile,\n#         imprint\n#     )\n#\n#     # check if the file exists if does then Raise \"File exists!\"\n#     if os.path.exists(filepath):\n#         raise FileExistsError(\"File already exists: `{}`\".format(filepath))\n#\n#     # if no representations matching then\n#     #   Raise \"no representations to be build\"\n#     if len(representations) == 0:\n#         raise AttributeError(\"Missing list of `representations`\")\n#\n#     # check nodes input\n#     if len(nodes) == 0:\n#         log.warning(\"Missing list of `nodes`\")\n#\n#     # create temp nk file\n#     nuke_script = hiero.core.nuke.ScriptWriter()\n#\n#     # create root node and save all metadata\n#     root_node = hiero.core.nuke.RootNode()\n#\n#     anatomy = Anatomy(get_current_project_name())\n#     work_template = anatomy.templates[\"work\"][\"path\"]\n#     root_path = anatomy.root_value_for_template(work_template)\n#\n#     nuke_script.addNode(root_node)\n#\n#     script_builder = BuildWorkfile(\n#         root_node=root_node,\n#         root_path=root_path,\n#         nodes=nuke_script.getNodes(),\n#         **kwargs\n#     )\n\n\ndef create_nuke_workfile_clips(nuke_workfiles, seq=None):\n    '''\n    nuke_workfiles is list of dictionaries like:\n    [{\n        'path': 'P:/Jakub_testy_pipeline/test_v01.nk',\n        'name': 'test',\n        'handleStart': 15, # added asymetrically to handles\n        'handleEnd': 10, # added asymetrically to handles\n        \"clipIn\": 16,\n        \"frameStart\": 991,\n        \"frameEnd\": 1023,\n        'task': 'Comp-tracking',\n        'work_dir': 'VFX_PR',\n        'shot': '00010'\n    }]\n    '''\n\n    proj = hiero.core.projects()[-1]\n    root = proj.clipsBin()\n\n    if not seq:\n        seq = hiero.core.Sequence('NewSequences')\n        root.addItem(hiero.core.BinItem(seq))\n    # todo will need to define this better\n    # track = seq[1]  # lazy example to get a destination#  track\n    clips_lst = []\n    for nk in nuke_workfiles:\n        task_path = '/'.join([nk['work_dir'], nk['shot'], nk['task']])\n        bin = create_bin(task_path, proj)\n\n        if nk['task'] not in seq.videoTracks():\n            track = hiero.core.VideoTrack(nk['task'])\n            seq.addTrack(track)\n        else:\n            track = seq.tracks(nk['task'])\n\n        # create clip media\n        media = hiero.core.MediaSource(nk['path'])\n        media_in = int(media.startTime() or 0)\n        media_duration = int(media.duration() or 0)\n\n        handle_start = nk.get(\"handleStart\")\n        handle_end = nk.get(\"handleEnd\")\n\n        if media_in:\n            source_in = media_in + handle_start\n        else:\n            source_in = nk[\"frameStart\"] + handle_start\n\n        if media_duration:\n            source_out = (media_in + media_duration - 1) - handle_end\n        else:\n            source_out = nk[\"frameEnd\"] - handle_end\n\n        source = hiero.core.Clip(media)\n\n        name = os.path.basename(os.path.splitext(nk['path'])[0])\n        split_name = split_by_client_version(name)[0] or name\n\n        # add to bin as clip item\n        items_in_bin = [b.name() for b in bin.items()]\n        if split_name not in items_in_bin:\n            binItem = hiero.core.BinItem(source)\n            bin.addItem(binItem)\n\n        new_source = [\n            item for item in bin.items() if split_name in item.name()\n        ][0].items()[0].item()\n\n        # add to track as clip item\n        trackItem = hiero.core.TrackItem(\n            split_name, hiero.core.TrackItem.kVideo)\n        trackItem.setSource(new_source)\n        trackItem.setSourceIn(source_in)\n        trackItem.setSourceOut(source_out)\n        trackItem.setTimelineIn(nk[\"clipIn\"])\n        trackItem.setTimelineOut(nk[\"clipIn\"] + (source_out - source_in))\n        track.addTrackItem(trackItem)\n        clips_lst.append(trackItem)\n\n    return clips_lst\n\n\ndef create_bin(path=None, project=None):\n    '''\n    Create bin in project.\n    If the path is \"bin1/bin2/bin3\" it will create whole depth\n    and return `bin3`\n\n    '''\n    # get the first loaded project\n    project = project or get_current_project()\n\n    path = path or DEFAULT_BIN_NAME\n\n    path = path.replace(\"\\\\\", \"/\").split(\"/\")\n\n    root_bin = project.clipsBin()\n\n    done_bin_lst = []\n    for i, b in enumerate(path):\n        if i == 0 and len(path) > 1:\n            if b in [bin.name() for bin in root_bin.bins()]:\n                bin = [bin for bin in root_bin.bins() if b in bin.name()][0]\n                done_bin_lst.append(bin)\n            else:\n                create_bin = hiero.core.Bin(b)\n                root_bin.addItem(create_bin)\n                done_bin_lst.append(create_bin)\n\n        elif i >= 1 and i < len(path) - 1:\n            if b in [bin.name() for bin in done_bin_lst[i - 1].bins()]:\n                bin = [\n                    bin for bin in done_bin_lst[i - 1].bins()\n                    if b in bin.name()\n                ][0]\n                done_bin_lst.append(bin)\n            else:\n                create_bin = hiero.core.Bin(b)\n                done_bin_lst[i - 1].addItem(create_bin)\n                done_bin_lst.append(create_bin)\n\n        elif i == len(path) - 1:\n            if b in [bin.name() for bin in done_bin_lst[i - 1].bins()]:\n                bin = [\n                    bin for bin in done_bin_lst[i - 1].bins()\n                    if b in bin.name()\n                ][0]\n                done_bin_lst.append(bin)\n            else:\n                create_bin = hiero.core.Bin(b)\n                done_bin_lst[i - 1].addItem(create_bin)\n                done_bin_lst.append(create_bin)\n\n    return done_bin_lst[-1]\n\n\ndef split_by_client_version(string):\n    regex = r\"[/_.]v\\d+\"\n    try:\n        matches = re.findall(regex, string, re.IGNORECASE)\n        return string.split(matches[0])\n    except Exception as error:\n        log.error(error)\n        return None\n\n\ndef get_selected_track_items(sequence=None):\n    _sequence = sequence or get_current_sequence()\n\n    # Getting selection\n    timeline_editor = hiero.ui.getTimelineEditor(_sequence)\n    return timeline_editor.selection()\n\n\ndef set_selected_track_items(track_items_list, sequence=None):\n    _sequence = sequence or get_current_sequence()\n\n    # make sure only trackItems are in list selection\n    only_track_items = [\n        i for i in track_items_list\n        if isinstance(i, hiero.core.TrackItem)]\n\n    # Getting selection\n    timeline_editor = hiero.ui.getTimelineEditor(_sequence)\n    return timeline_editor.setSelection(only_track_items)\n\n\ndef _read_doc_from_path(path):\n    # reading QtXml.QDomDocument from HROX path\n    hrox_file = QtCore.QFile(path)\n    if not hrox_file.open(QtCore.QFile.ReadOnly):\n        raise RuntimeError(\"Failed to open file for reading\")\n    doc = QtXml.QDomDocument()\n    doc.setContent(hrox_file)\n    hrox_file.close()\n    return doc\n\n\ndef _write_doc_to_path(doc, path):\n    # write QtXml.QDomDocument to path as HROX\n    hrox_file = QtCore.QFile(path)\n    if not hrox_file.open(QtCore.QFile.WriteOnly):\n        raise RuntimeError(\"Failed to open file for writing\")\n    stream = QtCore.QTextStream(hrox_file)\n    doc.save(stream, 1)\n    hrox_file.close()\n\n\ndef _set_hrox_project_knobs(doc, **knobs):\n    # set attributes to Project Tag\n    proj_elem = doc.documentElement().firstChildElement(\"Project\")\n    for k, v in knobs.items():\n        if \"ocioconfigpath\" in k:\n            paths_to_format = v[platform.system().lower()]\n            for _path in paths_to_format:\n                v = _path.format(**os.environ)\n                if not os.path.exists(v):\n                    continue\n        log.debug(\"Project colorspace knob `{}` was set to `{}`\".format(k, v))\n        if isinstance(v, dict):\n            continue\n        proj_elem.setAttribute(str(k), v)\n\n\ndef apply_colorspace_project():\n    \"\"\"Apply colorspaces from settings.\n\n    Due to not being able to set the project settings through the Python API,\n    we need to do use some dubious code to find the widgets and set them. It is\n    possible to set the project settings without traversing through the widgets\n    but it involves reading the hrox files from disk with XML, so no in-memory\n    support. See https://community.foundry.com/discuss/topic/137771/change-a-project-s-default-color-transform-with-python  # noqa\n    for more details.\n    \"\"\"\n    # get presets for hiero\n    project_name = get_current_project_name()\n    imageio = get_project_settings(project_name)[\"hiero\"][\"imageio\"]\n    presets = imageio.get(\"workfile\")\n\n    # Open Project Settings UI.\n    for act in hiero.ui.registeredActions():\n        if act.objectName() == \"foundry.project.settings\":\n            act.trigger()\n\n    # Find widgets from their sibling label.\n    labels = {\n        \"Working Space:\": \"workingSpace\",\n        \"Viewer:\": \"viewerLut\",\n        \"Thumbnails:\": \"thumbnailLut\",\n        \"Monitor Out:\": \"monitorOutLut\",\n        \"8 Bit Files:\": \"eightBitLut\",\n        \"16 Bit Files:\": \"sixteenBitLut\",\n        \"Log Files:\": \"logLut\",\n        \"Floating Point Files:\": \"floatLut\"\n    }\n    widgets = {x: None for x in labels.values()}\n\n    def _recursive_children(widget, labels, widgets):\n        children = widget.children()\n        for count, child in enumerate(children):\n            if isinstance(child, QtWidgets.QLabel):\n                if child.text() in labels.keys():\n                    widgets[labels[child.text()]] = children[count + 1]\n            _recursive_children(child, labels, widgets)\n\n    app = QtWidgets.QApplication.instance()\n    title = \"Project Settings\"\n    for widget in app.topLevelWidgets():\n        if isinstance(widget, QtWidgets.QMainWindow):\n            if widget.windowTitle() != title:\n                continue\n            _recursive_children(widget, labels, widgets)\n            widget.close()\n\n    msg = \"Setting value \\\"{}\\\" is not a valid option for \\\"{}\\\"\"\n    for key, widget in widgets.items():\n        options = [widget.itemText(i) for i in range(widget.count())]\n        setting_value = presets[key]\n        assert setting_value in options, msg.format(setting_value, key)\n        widget.setCurrentText(presets[key])\n\n    # This code block is for setting up project colorspaces for files on disk.\n    # Due to not having Python API access to set the project settings, the\n    # Foundry recommended way is to modify the hrox files on disk with XML. See\n    # this forum thread for more details;\n    # https://community.foundry.com/discuss/topic/137771/change-a-project-s-default-color-transform-with-python  # noqa\n    '''\n    # backward compatibility layer\n    # TODO: remove this after some time\n    config_data = get_imageio_config(\n        project_name=get_current_project_name(),\n        host_name=\"hiero\"\n    )\n\n    if config_data:\n        presets.update({\n            \"ocioConfigName\": \"custom\"\n        })\n\n    # get path the the active projects\n    project = get_current_project()\n    current_file = project.path()\n\n    msg = \"The project needs to be saved to disk to apply colorspace settings.\"\n    assert current_file, msg\n\n    # save the workfile as subversion \"comment:_colorspaceChange\"\n    split_current_file = os.path.splitext(current_file)\n    copy_current_file = current_file\n\n    if \"_colorspaceChange\" not in current_file:\n        copy_current_file = (\n            split_current_file[0]\n            + \"_colorspaceChange\"\n            + split_current_file[1]\n        )\n\n    try:\n        # duplicate the file so the changes are applied only to the copy\n        shutil.copyfile(current_file, copy_current_file)\n    except shutil.Error:\n        # in case the file already exists and it want to copy to the\n        # same filewe need to do this trick\n        # TEMP file name change\n        copy_current_file_tmp = copy_current_file + \"_tmp\"\n        # create TEMP file\n        shutil.copyfile(current_file, copy_current_file_tmp)\n        # remove original file\n        os.remove(current_file)\n        # copy TEMP back to original name\n        shutil.copyfile(copy_current_file_tmp, copy_current_file)\n        # remove the TEMP file as we dont need it\n        os.remove(copy_current_file_tmp)\n\n    # use the code from below for changing xml hrox Attributes\n    presets.update({\"name\": os.path.basename(copy_current_file)})\n\n    # read HROX in as QDomSocument\n    doc = _read_doc_from_path(copy_current_file)\n\n    # apply project colorspace properties\n    _set_hrox_project_knobs(doc, **presets)\n\n    # write QDomSocument back as HROX\n    _write_doc_to_path(doc, copy_current_file)\n\n    # open the file as current project\n    hiero.core.openProject(copy_current_file)\n    '''\n\n\ndef apply_colorspace_clips():\n    project_name = get_current_project_name()\n    project = get_current_project(remove_untitled=True)\n    clips = project.clips()\n\n    # get presets for hiero\n    imageio = get_project_settings(project_name)[\"hiero\"][\"imageio\"]\n\n    presets = imageio.get(\"regexInputs\", {}).get(\"inputs\", {})\n    for clip in clips:\n        clip_media_source_path = clip.mediaSource().firstpath()\n        clip_name = clip.name()\n        clip_colorspace = clip.sourceMediaColourTransform()\n\n        if \"default\" in clip_colorspace:\n            continue\n\n        # check if any colorspace presets for read is matching\n        preset_clrsp = None\n        for k in presets:\n            if not bool(re.search(k[\"regex\"], clip_media_source_path)):\n                continue\n            preset_clrsp = k[\"colorspace\"]\n\n        if preset_clrsp:\n            log.debug(\"Changing clip.path: {}\".format(clip_media_source_path))\n            log.info(\"Changing clip `{}` colorspace {} to {}\".format(\n                clip_name, clip_colorspace, preset_clrsp))\n            # set the found preset to the clip\n            clip.setSourceMediaColourTransform(preset_clrsp)\n\n    # save project after all is changed\n    project.save()\n\n\ndef is_overlapping(ti_test, ti_original, strict=False):\n    covering_exp = (\n        (ti_test.timelineIn() <= ti_original.timelineIn())\n        and (ti_test.timelineOut() >= ti_original.timelineOut())\n    )\n\n    if strict:\n        return covering_exp\n\n    inside_exp = (\n        (ti_test.timelineIn() >= ti_original.timelineIn())\n        and (ti_test.timelineOut() <= ti_original.timelineOut())\n    )\n    overlaying_right_exp = (\n        (ti_test.timelineIn() < ti_original.timelineOut())\n        and (ti_test.timelineOut() >= ti_original.timelineOut())\n    )\n    overlaying_left_exp = (\n        (ti_test.timelineOut() > ti_original.timelineIn())\n        and (ti_test.timelineIn() <= ti_original.timelineIn())\n    )\n\n    return any((\n        covering_exp,\n        inside_exp,\n        overlaying_right_exp,\n        overlaying_left_exp\n    ))\n\n\ndef get_sequence_pattern_and_padding(file):\n    \"\"\" Return sequence pattern and padding from file\n\n    Attributes:\n        file (string): basename form path\n\n    Example:\n        Can find file.0001.ext, file.%02d.ext, file.####.ext\n\n    Return:\n        string: any matching sequence pattern\n        int: padding of sequnce numbering\n    \"\"\"\n    foundall = re.findall(\n        r\"(#+)|(%\\d+d)|(?<=[^a-zA-Z0-9])(\\d+)(?=\\.\\w+$)\", file)\n    if not foundall:\n        return None, None\n    found = sorted(list(set(foundall[0])))[-1]\n\n    padding = int(\n        re.findall(r\"\\d+\", found)[-1]) if \"%\" in found else len(found)\n    return found, padding\n\n\ndef sync_clip_name_to_data_asset(track_items_list):\n    # loop through all selected clips\n    for track_item in track_items_list:\n        # ignore if parent track is locked or disabled\n        if track_item.parent().isLocked():\n            continue\n        if not track_item.parent().isEnabled():\n            continue\n        # ignore if the track item is disabled\n        if not track_item.isEnabled():\n            continue\n\n        # get name and data\n        ti_name = track_item.name()\n        data = get_trackitem_openpype_data(track_item)\n\n        # ignore if no data on the clip or not publish instance\n        if not data:\n            continue\n        if data.get(\"id\") != \"pyblish.avalon.instance\":\n            continue\n\n        # fix data if wrong name\n        if data[\"asset\"] != ti_name:\n            data[\"asset\"] = ti_name\n            # remove the original tag\n            tag = get_trackitem_openpype_tag(track_item)\n            track_item.removeTag(tag)\n            # create new tag with updated data\n            set_trackitem_openpype_tag(track_item, data)\n            print(\"asset was changed in clip: {}\".format(ti_name))\n\n\ndef set_track_color(track_item, color):\n    track_item.source().binItem().setColor(color)\n\n\ndef check_inventory_versions(track_items=None):\n    \"\"\"\n    Actual version color identifier of Loaded containers\n\n    Check all track items and filter only\n    Loader nodes for its version. It will get all versions from database\n    and check if the node is having actual version. If not then it will color\n    it to red.\n    \"\"\"\n    from . import parse_container\n\n    track_items = track_items or get_track_items()\n    # presets\n    clip_color_last = \"green\"\n    clip_color = \"red\"\n\n    containers = []\n    # Find all containers and collect it's node and representation ids\n    for track_item in track_items:\n        container = parse_container(track_item)\n        if container:\n            containers.append(container)\n\n    # Skip if nothing was found\n    if not containers:\n        return\n\n    project_name = get_current_project_name()\n    filter_result = filter_containers(containers, project_name)\n    for container in filter_result.latest:\n        set_track_color(container[\"_item\"], clip_color_last)\n\n    for container in filter_result.outdated:\n        set_track_color(container[\"_item\"], clip_color)\n\n\ndef selection_changed_timeline(event):\n    \"\"\"Callback on timeline to check if asset in data is the same as clip name.\n\n    Args:\n        event (hiero.core.Event): timeline event\n    \"\"\"\n    timeline_editor = event.sender\n    selection = timeline_editor.selection()\n\n    track_items = get_track_items(\n        selection=selection,\n        track_type=\"video\",\n        check_enabled=True,\n        check_locked=True,\n        check_tagged=True\n    )\n\n    # run checking function\n    sync_clip_name_to_data_asset(track_items)\n\n\ndef before_project_save(event):\n    track_items = get_track_items(\n        track_type=\"video\",\n        check_enabled=True,\n        check_locked=True,\n        check_tagged=True\n    )\n\n    # run checking function\n    sync_clip_name_to_data_asset(track_items)\n\n    # also mark old versions of loaded containers\n    check_inventory_versions(track_items)\n\n\ndef get_main_window():\n    \"\"\"Acquire Nuke's main window\"\"\"\n    if _CTX.parent_gui is None:\n        top_widgets = QtWidgets.QApplication.topLevelWidgets()\n        name = \"Foundry::UI::DockMainWindow\"\n        main_window = next(widget for widget in top_widgets if\n                           widget.inherits(\"QMainWindow\") and\n                           widget.metaObject().className() == name)\n        _CTX.parent_gui = main_window\n    return _CTX.parent_gui\n"
  },
  {
    "path": "openpype/hosts/hiero/api/menu.py",
    "content": "import os\nimport sys\n\nimport hiero.core\nfrom hiero.ui import findMenuAction\n\nfrom qtpy import QtGui\n\nfrom openpype.lib import Logger\nfrom openpype.tools.utils import host_tools\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import (\n    get_current_project_name,\n    get_current_asset_name,\n    get_current_task_name\n)\n\nfrom . import tags\n\nlog = Logger.get_logger(__name__)\n\nself = sys.modules[__name__]\nself._change_context_menu = None\n\n\ndef get_context_label():\n    return \"{}, {}\".format(\n        get_current_asset_name(),\n        get_current_task_name()\n    )\n\n\ndef update_menu_task_label():\n    \"\"\"Update the task label in Avalon menu to current session\"\"\"\n\n    object_name = self._change_context_menu\n    found_menu = findMenuAction(object_name)\n\n    if not found_menu:\n        log.warning(\"Can't find menuItem: {}\".format(object_name))\n        return\n\n    label = get_context_label()\n\n    menu = found_menu.menu()\n    self._change_context_menu = label\n    menu.setTitle(label)\n\n\ndef menu_install():\n    \"\"\"\n    Installing menu into Hiero\n\n    \"\"\"\n\n    from . import (\n        publish, launch_workfiles_app, reload_config,\n        apply_colorspace_project, apply_colorspace_clips\n    )\n    from .lib import get_main_window\n\n    main_window = get_main_window()\n\n    # here is the best place to add menu\n\n    menu_name = os.environ['AVALON_LABEL']\n\n    context_label = get_context_label()\n\n    self._change_context_menu = context_label\n\n    try:\n        check_made_menu = findMenuAction(menu_name)\n    except Exception:\n        check_made_menu = None\n\n    if not check_made_menu:\n        # Grab Hiero's MenuBar\n        menu = hiero.ui.menuBar().addMenu(menu_name)\n    else:\n        menu = check_made_menu.menu()\n\n    context_label_action = menu.addAction(context_label)\n    context_label_action.setEnabled(False)\n\n    menu.addSeparator()\n\n    workfiles_action = menu.addAction(\"Work Files...\")\n    workfiles_action.setIcon(QtGui.QIcon(\"icons:Position.png\"))\n    workfiles_action.triggered.connect(launch_workfiles_app)\n\n    default_tags_action = menu.addAction(\"Create Default Tags\")\n    default_tags_action.setIcon(QtGui.QIcon(\"icons:Position.png\"))\n    default_tags_action.triggered.connect(tags.add_tags_to_workfile)\n\n    menu.addSeparator()\n\n    creator_action = menu.addAction(\"Create...\")\n    creator_action.setIcon(QtGui.QIcon(\"icons:CopyRectangle.png\"))\n    creator_action.triggered.connect(\n        lambda: host_tools.show_creator(parent=main_window)\n    )\n\n    publish_action = menu.addAction(\"Publish...\")\n    publish_action.setIcon(QtGui.QIcon(\"icons:Output.png\"))\n    publish_action.triggered.connect(\n        lambda *args: publish(hiero.ui.mainWindow())\n    )\n\n    loader_action = menu.addAction(\"Load...\")\n    loader_action.setIcon(QtGui.QIcon(\"icons:CopyRectangle.png\"))\n    loader_action.triggered.connect(\n        lambda: host_tools.show_loader(parent=main_window)\n    )\n\n    sceneinventory_action = menu.addAction(\"Manage...\")\n    sceneinventory_action.setIcon(QtGui.QIcon(\"icons:CopyRectangle.png\"))\n    sceneinventory_action.triggered.connect(\n        lambda: host_tools.show_scene_inventory(parent=main_window)\n    )\n\n    library_action = menu.addAction(\"Library...\")\n    library_action.setIcon(QtGui.QIcon(\"icons:CopyRectangle.png\"))\n    library_action.triggered.connect(\n        lambda: host_tools.show_library_loader(parent=main_window)\n    )\n\n    if os.getenv(\"OPENPYPE_DEVELOP\"):\n        menu.addSeparator()\n        reload_action = menu.addAction(\"Reload pipeline\")\n        reload_action.setIcon(QtGui.QIcon(\"icons:ColorAdd.png\"))\n        reload_action.triggered.connect(reload_config)\n\n    menu.addSeparator()\n    apply_colorspace_p_action = menu.addAction(\"Apply Colorspace Project\")\n    apply_colorspace_p_action.setIcon(QtGui.QIcon(\"icons:ColorAdd.png\"))\n    apply_colorspace_p_action.triggered.connect(apply_colorspace_project)\n\n    apply_colorspace_c_action = menu.addAction(\"Apply Colorspace Clips\")\n    apply_colorspace_c_action.setIcon(QtGui.QIcon(\"icons:ColorAdd.png\"))\n    apply_colorspace_c_action.triggered.connect(apply_colorspace_clips)\n\n    menu.addSeparator()\n\n    exeprimental_action = menu.addAction(\"Experimental tools...\")\n    exeprimental_action.triggered.connect(\n        lambda: host_tools.show_experimental_tools_dialog(parent=main_window)\n    )\n\n\ndef add_scripts_menu():\n    try:\n        from . import launchforhiero\n    except ImportError:\n\n        log.warning(\n            \"Skipping studio.menu install, because \"\n            \"'scriptsmenu' module seems unavailable.\"\n        )\n        return\n\n    # load configuration of custom menu\n    project_settings = get_project_settings(get_current_project_name())\n    config = project_settings[\"hiero\"][\"scriptsmenu\"][\"definition\"]\n    _menu = project_settings[\"hiero\"][\"scriptsmenu\"][\"name\"]\n\n    if not config:\n        log.warning(\"Skipping studio menu, no definition found.\")\n        return\n\n    # run the launcher for Hiero menu\n    studio_menu = launchforhiero.main(title=_menu.title())\n\n    # apply configuration\n    studio_menu.build_from_configuration(studio_menu, config)\n"
  },
  {
    "path": "openpype/hosts/hiero/api/otio/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/hiero/api/otio/hiero_export.py",
    "content": "\"\"\" compatibility OpenTimelineIO 0.12.0 and newer\n\"\"\"\n\nimport os\nimport re\nimport ast\nimport opentimelineio as otio\nfrom . import utils\nimport hiero.core\nimport hiero.ui\n\n\nTRACK_TYPE_MAP = {\n    hiero.core.VideoTrack: otio.schema.TrackKind.Video,\n    hiero.core.AudioTrack: otio.schema.TrackKind.Audio\n}\nMARKER_COLOR_MAP = {\n    \"magenta\": otio.schema.MarkerColor.MAGENTA,\n    \"red\": otio.schema.MarkerColor.RED,\n    \"yellow\": otio.schema.MarkerColor.YELLOW,\n    \"green\": otio.schema.MarkerColor.GREEN,\n    \"cyan\": otio.schema.MarkerColor.CYAN,\n    \"blue\": otio.schema.MarkerColor.BLUE,\n}\n\n\nclass CTX:\n    project_fps = None\n    timeline = None\n    include_tags = True\n\n\ndef flatten(list_):\n    for item_ in list_:\n        if isinstance(item_, (list, tuple)):\n            for sub_item in flatten(item_):\n                yield sub_item\n        else:\n            yield item_\n\n\ndef create_otio_rational_time(frame, fps):\n    return otio.opentime.RationalTime(\n        float(frame),\n        float(fps)\n    )\n\n\ndef create_otio_time_range(start_frame, frame_duration, fps):\n    return otio.opentime.TimeRange(\n        start_time=create_otio_rational_time(start_frame, fps),\n        duration=create_otio_rational_time(frame_duration, fps)\n    )\n\n\ndef _get_metadata(item):\n    if hasattr(item, 'metadata'):\n        return {key: value for key, value in dict(item.metadata()).items()}\n    return {}\n\n\ndef create_time_effects(otio_clip, track_item):\n    # get all subtrack items\n    subTrackItems = flatten(track_item.parent().subTrackItems())\n    speed = track_item.playbackSpeed()\n\n    otio_effect = None\n    # retime on track item\n    if speed != 1.:\n        # make effect\n        otio_effect = otio.schema.LinearTimeWarp()\n        otio_effect.name = \"Speed\"\n        otio_effect.time_scalar = speed\n\n    # freeze frame effect\n    if speed == 0.:\n        otio_effect = otio.schema.FreezeFrame()\n        otio_effect.name = \"FreezeFrame\"\n\n    if otio_effect:\n        # add otio effect to clip effects\n        otio_clip.effects.append(otio_effect)\n\n    # loop through and get all Timewarps\n    for effect in subTrackItems:\n        if ((track_item not in effect.linkedItems())\n                and (len(effect.linkedItems()) > 0)):\n            continue\n        # avoid all effect which are not TimeWarp and disabled\n        if \"TimeWarp\" not in effect.name():\n            continue\n\n        if not effect.isEnabled():\n            continue\n\n        node = effect.node()\n        name = node[\"name\"].value()\n\n        # solve effect class as effect name\n        _name = effect.name()\n        if \"_\" in _name:\n            effect_name = re.sub(r\"(?:_)[_0-9]+\", \"\", _name)  # more numbers\n        else:\n            effect_name = re.sub(r\"\\d+\", \"\", _name)  # one number\n\n        metadata = {}\n        # add knob to metadata\n        for knob in [\"lookup\", \"length\"]:\n            value = node[knob].value()\n            animated = node[knob].isAnimated()\n            if animated:\n                value = [\n                    ((node[knob].getValueAt(i)) - i)\n                    for i in range(\n                        track_item.timelineIn(), track_item.timelineOut() + 1)\n                ]\n\n            metadata[knob] = value\n\n        # make effect\n        otio_effect = otio.schema.TimeEffect()\n        otio_effect.name = name\n        otio_effect.effect_name = effect_name\n        otio_effect.metadata.update(metadata)\n\n        # add otio effect to clip effects\n        otio_clip.effects.append(otio_effect)\n\n\ndef create_otio_reference(clip):\n    metadata = _get_metadata(clip)\n    media_source = clip.mediaSource()\n\n    # get file info for path and start frame\n    file_info = media_source.fileinfos().pop()\n    frame_start = file_info.startFrame()\n    path = file_info.filename()\n\n    # get padding and other file infos\n    padding = media_source.filenamePadding()\n    file_head = media_source.filenameHead()\n    is_sequence = not media_source.singleFile()\n    frame_duration = media_source.duration()\n    fps = utils.get_rate(clip) or CTX.project_fps\n    extension = os.path.splitext(path)[-1]\n\n    if is_sequence:\n        metadata.update({\n            \"isSequence\": True,\n            \"padding\": padding\n        })\n\n    # add resolution metadata\n    metadata.update({\n        \"openpype.source.colourtransform\": clip.sourceMediaColourTransform(),\n        \"openpype.source.width\": int(media_source.width()),\n        \"openpype.source.height\": int(media_source.height()),\n        \"openpype.source.pixelAspect\": float(media_source.pixelAspect())\n    })\n\n    otio_ex_ref_item = None\n\n    if is_sequence:\n        # if it is file sequence try to create `ImageSequenceReference`\n        # the OTIO might not be compatible so return nothing and do it old way\n        try:\n            dirname = os.path.dirname(path)\n            otio_ex_ref_item = otio.schema.ImageSequenceReference(\n                target_url_base=dirname + os.sep,\n                name_prefix=file_head,\n                name_suffix=extension,\n                start_frame=frame_start,\n                frame_zero_padding=padding,\n                rate=fps,\n                available_range=create_otio_time_range(\n                    frame_start,\n                    frame_duration,\n                    fps\n                )\n            )\n        except AttributeError:\n            pass\n\n    if not otio_ex_ref_item:\n        reformat_path = utils.get_reformated_path(path, padded=False)\n        # in case old OTIO or video file create `ExternalReference`\n        otio_ex_ref_item = otio.schema.ExternalReference(\n            target_url=reformat_path,\n            available_range=create_otio_time_range(\n                frame_start,\n                frame_duration,\n                fps\n            )\n        )\n\n    # add metadata to otio item\n    add_otio_metadata(otio_ex_ref_item, media_source, **metadata)\n\n    return otio_ex_ref_item\n\n\ndef get_marker_color(tag):\n    icon = tag.icon()\n    pat = r'icons:Tag(?P<color>\\w+)\\.\\w+'\n\n    res = re.search(pat, icon)\n    if res:\n        color = res.groupdict().get('color')\n        if color.lower() in MARKER_COLOR_MAP:\n            return MARKER_COLOR_MAP[color.lower()]\n\n    return otio.schema.MarkerColor.RED\n\n\ndef create_otio_markers(otio_item, item):\n    for tag in item.tags():\n        if not tag.visible():\n            continue\n\n        if tag.name() == 'Copy':\n            # Hiero adds this tag to a lot of clips\n            continue\n\n        frame_rate = utils.get_rate(item) or CTX.project_fps\n\n        marked_range = otio.opentime.TimeRange(\n            start_time=otio.opentime.RationalTime(\n                tag.inTime(),\n                frame_rate\n            ),\n            duration=otio.opentime.RationalTime(\n                int(tag.metadata().dict().get('tag.length', '0')),\n                frame_rate\n            )\n        )\n        # add tag metadata but remove \"tag.\" string\n        metadata = {}\n\n        for key, value in tag.metadata().dict().items():\n            _key = key.replace(\"tag.\", \"\")\n\n            try:\n                # capture exceptions which are related to strings only\n                _value = ast.literal_eval(value)\n            except (ValueError, SyntaxError):\n                _value = value\n\n            metadata.update({_key: _value})\n\n        # Store the source item for future import assignment\n        metadata['hiero_source_type'] = item.__class__.__name__\n\n        marker = otio.schema.Marker(\n            name=tag.name(),\n            color=get_marker_color(tag),\n            marked_range=marked_range,\n            metadata=metadata\n        )\n\n        otio_item.markers.append(marker)\n\n\ndef create_otio_clip(track_item):\n    clip = track_item.source()\n    speed = track_item.playbackSpeed()\n    # flip if speed is in minus\n    source_in = track_item.sourceIn() if speed > 0 else track_item.sourceOut()\n\n    duration = int(track_item.duration())\n\n    fps = utils.get_rate(track_item) or CTX.project_fps\n    name = track_item.name()\n\n    media_reference = create_otio_reference(clip)\n    source_range = create_otio_time_range(\n        int(source_in),\n        int(duration),\n        fps\n    )\n\n    otio_clip = otio.schema.Clip(\n        name=name,\n        source_range=source_range,\n        media_reference=media_reference\n    )\n\n    # Add tags as markers\n    if CTX.include_tags:\n        create_otio_markers(otio_clip, track_item)\n        create_otio_markers(otio_clip, track_item.source())\n\n    # only if video\n    if not clip.mediaSource().hasAudio():\n        # Add effects to clips\n        create_time_effects(otio_clip, track_item)\n\n    return otio_clip\n\n\ndef create_otio_gap(gap_start, clip_start, tl_start_frame, fps):\n    return otio.schema.Gap(\n        source_range=create_otio_time_range(\n            gap_start,\n            (clip_start - tl_start_frame) - gap_start,\n            fps\n        )\n    )\n\n\ndef _create_otio_timeline():\n    project = CTX.timeline.project()\n    metadata = _get_metadata(CTX.timeline)\n\n    metadata.update({\n        \"openpype.timeline.width\": int(CTX.timeline.format().width()),\n        \"openpype.timeline.height\": int(CTX.timeline.format().height()),\n        \"openpype.timeline.pixelAspect\": int(CTX.timeline.format().pixelAspect()),  # noqa\n        \"openpype.project.useOCIOEnvironmentOverride\": project.useOCIOEnvironmentOverride(),  # noqa\n        \"openpype.project.lutSetting16Bit\": project.lutSetting16Bit(),\n        \"openpype.project.lutSetting8Bit\": project.lutSetting8Bit(),\n        \"openpype.project.lutSettingFloat\": project.lutSettingFloat(),\n        \"openpype.project.lutSettingLog\": project.lutSettingLog(),\n        \"openpype.project.lutSettingViewer\": project.lutSettingViewer(),\n        \"openpype.project.lutSettingWorkingSpace\": project.lutSettingWorkingSpace(),  # noqa\n        \"openpype.project.lutUseOCIOForExport\": project.lutUseOCIOForExport(),\n        \"openpype.project.ocioConfigName\": project.ocioConfigName(),\n        \"openpype.project.ocioConfigPath\": project.ocioConfigPath()\n    })\n\n    start_time = create_otio_rational_time(\n        CTX.timeline.timecodeStart(), CTX.project_fps)\n\n    return otio.schema.Timeline(\n        name=CTX.timeline.name(),\n        global_start_time=start_time,\n        metadata=metadata\n    )\n\n\ndef create_otio_track(track_type, track_name):\n    return otio.schema.Track(\n        name=track_name,\n        kind=TRACK_TYPE_MAP[track_type]\n    )\n\n\ndef add_otio_gap(track_item, otio_track, prev_out):\n    gap_length = track_item.timelineIn() - prev_out\n    if prev_out != 0:\n        gap_length -= 1\n\n    gap = otio.opentime.TimeRange(\n        duration=otio.opentime.RationalTime(\n            gap_length,\n            CTX.project_fps\n        )\n    )\n    otio_gap = otio.schema.Gap(source_range=gap)\n    otio_track.append(otio_gap)\n\n\ndef add_otio_metadata(otio_item, media_source, **kwargs):\n    metadata = _get_metadata(media_source)\n\n    # add additional metadata from kwargs\n    if kwargs:\n        metadata.update(kwargs)\n\n    # add metadata to otio item metadata\n    for key, value in metadata.items():\n        otio_item.metadata.update({key: value})\n\n\ndef create_otio_timeline():\n\n    def set_prev_item(itemindex, track_item):\n        # Add Gap if needed\n        if itemindex == 0:\n            # if it is first track item at track then add\n            # it to previous item\n            return track_item\n\n        else:\n            # get previous item\n            return track_item.parent().items()[itemindex - 1]\n\n    # get current timeline\n    CTX.timeline = hiero.ui.activeSequence()\n    CTX.project_fps = CTX.timeline.framerate().toFloat()\n\n    # convert timeline to otio\n    otio_timeline = _create_otio_timeline()\n\n    # loop all defined track types\n    for track in CTX.timeline.items():\n        # skip if track is disabled\n        if not track.isEnabled():\n            continue\n\n        # convert track to otio\n        otio_track = create_otio_track(\n            type(track), track.name())\n\n        for itemindex, track_item in enumerate(track):\n            # Add Gap if needed\n            if itemindex == 0:\n                # if it is first track item at track then add\n                # it to previous item\n                prev_item = track_item\n\n            else:\n                # get previous item\n                prev_item = track_item.parent().items()[itemindex - 1]\n\n            # calculate clip frame range difference from each other\n            clip_diff = track_item.timelineIn() - prev_item.timelineOut()\n\n            # add gap if first track item is not starting\n            # at first timeline frame\n            if itemindex == 0 and track_item.timelineIn() > 0:\n                add_otio_gap(track_item, otio_track, 0)\n\n            # or add gap if following track items are having\n            # frame range differences from each other\n            elif itemindex and clip_diff != 1:\n                add_otio_gap(track_item, otio_track, prev_item.timelineOut())\n\n            # create otio clip and add it to track\n            otio_clip = create_otio_clip(track_item)\n            otio_track.append(otio_clip)\n\n        # Add tags as markers\n        if CTX.include_tags:\n            create_otio_markers(otio_track, track)\n\n        # add track to otio timeline\n        otio_timeline.tracks.append(otio_track)\n\n    return otio_timeline\n\n\ndef write_to_file(otio_timeline, path):\n    otio.adapters.write_to_file(otio_timeline, path)\n"
  },
  {
    "path": "openpype/hosts/hiero/api/otio/hiero_import.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n__author__ = \"Daniel Flehner Heen\"\n__credits__ = [\"Jakub Jezek\", \"Daniel Flehner Heen\"]\n\n\nimport os\nimport hiero.core\nimport hiero.ui\n\nimport PySide2.QtWidgets as qw\n\ntry:\n    from urllib import unquote\n\nexcept ImportError:\n    from urllib.parse import unquote  # lint:ok\n\nimport opentimelineio as otio\n\n_otio_old = False\n\n\ndef inform(messages):\n    if isinstance(messages, type('')):\n        messages = [messages]\n\n    qw.QMessageBox.information(\n        hiero.ui.mainWindow(),\n        'OTIO Import',\n        '\\n'.join(messages),\n        qw.QMessageBox.StandardButton.Ok\n    )\n\n\ndef get_transition_type(otio_item, otio_track):\n    _in, _out = otio_track.neighbors_of(otio_item)\n\n    if isinstance(_in, otio.schema.Gap):\n        _in = None\n\n    if isinstance(_out, otio.schema.Gap):\n        _out = None\n\n    if _in and _out:\n        return 'dissolve'\n\n    elif _in and not _out:\n        return 'fade_out'\n\n    elif not _in and _out:\n        return 'fade_in'\n\n    else:\n        return 'unknown'\n\n\ndef find_trackitem(otio_clip, hiero_track):\n    for item in hiero_track.items():\n        if item.timelineIn() == otio_clip.range_in_parent().start_time.value:\n            if item.name() == otio_clip.name:\n                return item\n\n    return None\n\n\ndef get_neighboring_trackitems(otio_item, otio_track, hiero_track):\n    _in, _out = otio_track.neighbors_of(otio_item)\n    trackitem_in = None\n    trackitem_out = None\n\n    if _in:\n        trackitem_in = find_trackitem(_in, hiero_track)\n\n    if _out:\n        trackitem_out = find_trackitem(_out, hiero_track)\n\n    return trackitem_in, trackitem_out\n\n\ndef apply_transition(otio_track, otio_item, track):\n    warning = None\n\n    # Figure out type of transition\n    transition_type = get_transition_type(otio_item, otio_track)\n\n    # Figure out track kind for getattr below\n    kind = ''\n    if isinstance(track, hiero.core.AudioTrack):\n        kind = 'Audio'\n\n    # Gather TrackItems involved in trasition\n    item_in, item_out = get_neighboring_trackitems(\n        otio_item,\n        otio_track,\n        track\n    )\n\n    # Create transition object\n    if transition_type == 'dissolve':\n        transition_func = getattr(\n            hiero.core.Transition,\n            'create{kind}DissolveTransition'.format(kind=kind)\n        )\n\n        try:\n            transition = transition_func(\n                item_in,\n                item_out,\n                otio_item.in_offset.value,\n                otio_item.out_offset.value\n            )\n\n        # Catch error raised if transition is bigger than TrackItem source\n        except RuntimeError as e:\n            transition = None\n            warning = (\n                \"Unable to apply transition \\\"{t.name}\\\": {e} \"\n                \"Ignoring the transition.\").format(t=otio_item, e=str(e))\n\n    elif transition_type == 'fade_in':\n        transition_func = getattr(\n            hiero.core.Transition,\n            'create{kind}FadeInTransition'.format(kind=kind)\n        )\n\n        # Warn user if part of fade is outside of clip\n        if otio_item.in_offset.value:\n            warning = \\\n                'Fist half of transition \"{t.name}\" is outside of clip and ' \\\n                'not valid in Hiero. Only applied second half.' \\\n                .format(t=otio_item)\n\n        transition = transition_func(\n            item_out,\n            otio_item.out_offset.value\n        )\n\n    elif transition_type == 'fade_out':\n        transition_func = getattr(\n            hiero.core.Transition,\n            'create{kind}FadeOutTransition'.format(kind=kind)\n        )\n        transition = transition_func(\n            item_in,\n            otio_item.in_offset.value\n        )\n\n        # Warn user if part of fade is outside of clip\n        if otio_item.out_offset.value:\n            warning = \\\n                'Second half of transition \"{t.name}\" is outside of clip ' \\\n                'and not valid in Hiero. Only applied first half.' \\\n                .format(t=otio_item)\n\n    else:\n        # Unknown transition\n        return\n\n    # Apply transition to track\n    if transition:\n        track.addTransition(transition)\n\n    # Inform user about missing or adjusted transitions\n    return warning\n\n\ndef prep_url(url_in):\n    url = unquote(url_in)\n\n    if url.startswith('file://localhost/'):\n        return url\n\n    url = 'file://localhost{sep}{url}'.format(\n        sep=url.startswith(os.sep) and '' or os.sep,\n        url=url.startswith(os.sep) and url[1:] or url\n    )\n\n    return url\n\n\ndef create_offline_mediasource(otio_clip, path=None):\n    global _otio_old\n\n    hiero_rate = hiero.core.TimeBase(\n        otio_clip.source_range.start_time.rate\n    )\n\n    try:\n        legal_media_refs = (\n            otio.schema.ExternalReference,\n            otio.schema.ImageSequenceReference\n        )\n    except AttributeError:\n        _otio_old = True\n        legal_media_refs = (\n            otio.schema.ExternalReference\n        )\n\n    if isinstance(otio_clip.media_reference, legal_media_refs):\n        source_range = otio_clip.available_range()\n\n    else:\n        source_range = otio_clip.source_range\n\n    if path is None:\n        path = otio_clip.name\n\n    media = hiero.core.MediaSource.createOfflineVideoMediaSource(\n        prep_url(path),\n        source_range.start_time.value,\n        source_range.duration.value,\n        hiero_rate,\n        source_range.start_time.value\n    )\n\n    return media\n\n\ndef load_otio(otio_file, project=None, sequence=None):\n    otio_timeline = otio.adapters.read_from_file(otio_file)\n    build_sequence(otio_timeline, project=project, sequence=sequence)\n\n\nmarker_color_map = {\n    \"PINK\": \"Magenta\",\n    \"RED\": \"Red\",\n    \"ORANGE\": \"Yellow\",\n    \"YELLOW\": \"Yellow\",\n    \"GREEN\": \"Green\",\n    \"CYAN\": \"Cyan\",\n    \"BLUE\": \"Blue\",\n    \"PURPLE\": \"Magenta\",\n    \"MAGENTA\": \"Magenta\",\n    \"BLACK\": \"Blue\",\n    \"WHITE\": \"Green\"\n}\n\n\ndef get_tag(tagname, tagsbin):\n    for tag in tagsbin.items():\n        if tag.name() == tagname:\n            return tag\n\n        if isinstance(tag, hiero.core.Bin):\n            tag = get_tag(tagname, tag)\n\n            if tag is not None:\n                return tag\n\n    return None\n\n\ndef add_metadata(metadata, hiero_item):\n    for key, value in metadata.get('Hiero', dict()).items():\n        if key == 'source_type':\n            # Only used internally to reassign tag to correct Hiero item\n            continue\n\n        if isinstance(value, dict):\n            add_metadata(value, hiero_item)\n            continue\n\n        if value is not None:\n            if not key.startswith('tag.'):\n                key = 'tag.' + key\n\n            hiero_item.metadata().setValue(key, str(value))\n\n\ndef add_markers(otio_item, hiero_item, tagsbin):\n    if isinstance(otio_item, (otio.schema.Stack, otio.schema.Clip)):\n        markers = otio_item.markers\n\n    elif isinstance(otio_item, otio.schema.Timeline):\n        markers = otio_item.tracks.markers\n\n    else:\n        markers = []\n\n    for marker in markers:\n        meta = marker.metadata.get('Hiero', dict())\n        if 'source_type' in meta:\n            if hiero_item.__class__.__name__ != meta.get('source_type'):\n                continue\n\n        marker_color = marker.color\n\n        _tag = get_tag(marker.name, tagsbin)\n        if _tag is None:\n            _tag = get_tag(marker_color_map[marker_color], tagsbin)\n\n        if _tag is None:\n            _tag = hiero.core.Tag(marker_color_map[marker.color])\n\n        start = marker.marked_range.start_time.value\n        end = (\n            marker.marked_range.start_time.value +\n            marker.marked_range.duration.value\n        )\n\n        if hasattr(hiero_item, 'addTagToRange'):\n            tag = hiero_item.addTagToRange(_tag, start, end)\n\n        else:\n            tag = hiero_item.addTag(_tag)\n\n        tag.setName(marker.name or marker_color_map[marker_color])\n        # tag.setNote(meta.get('tag.note', ''))\n\n        # Add metadata\n        add_metadata(marker.metadata, tag)\n\n\ndef create_track(otio_track, tracknum, track_kind):\n    if track_kind is None and hasattr(otio_track, 'kind'):\n        track_kind = otio_track.kind\n\n    # Create a Track\n    if track_kind == otio.schema.TrackKind.Video:\n        track = hiero.core.VideoTrack(\n            otio_track.name or 'Video{n}'.format(n=tracknum)\n        )\n\n    else:\n        track = hiero.core.AudioTrack(\n            otio_track.name or 'Audio{n}'.format(n=tracknum)\n        )\n\n    return track\n\n\ndef create_clip(otio_clip, tagsbin, sequencebin):\n    # Create MediaSource\n    url = None\n    media = None\n    otio_media = otio_clip.media_reference\n\n    if isinstance(otio_media, otio.schema.ExternalReference):\n        url = prep_url(otio_media.target_url)\n        media = hiero.core.MediaSource(url)\n\n    elif not _otio_old:\n        if isinstance(otio_media, otio.schema.ImageSequenceReference):\n            url = prep_url(otio_media.abstract_target_url('#'))\n            media = hiero.core.MediaSource(url)\n\n    if media is None or media.isOffline():\n        media = create_offline_mediasource(otio_clip, url)\n\n    # Reuse previous clip if possible\n    clip = None\n    for item in sequencebin.clips():\n        if item.activeItem().mediaSource() == media:\n            clip = item.activeItem()\n            break\n\n    if not clip:\n        # Create new Clip\n        clip = hiero.core.Clip(media)\n\n        # Add Clip to a Bin\n        sequencebin.addItem(hiero.core.BinItem(clip))\n\n    # Add markers\n    add_markers(otio_clip, clip, tagsbin)\n\n    return clip\n\n\ndef create_trackitem(playhead, track, otio_clip, clip):\n    source_range = otio_clip.source_range\n\n    trackitem = track.createTrackItem(otio_clip.name)\n    trackitem.setPlaybackSpeed(source_range.start_time.rate)\n    trackitem.setSource(clip)\n\n    time_scalar = 1.\n\n    # Check for speed effects and adjust playback speed accordingly\n    for effect in otio_clip.effects:\n        if isinstance(effect, otio.schema.LinearTimeWarp):\n            time_scalar = effect.time_scalar\n            # Only reverse effect can be applied here\n            if abs(time_scalar) == 1.:\n                trackitem.setPlaybackSpeed(\n                    trackitem.playbackSpeed() * time_scalar)\n\n        elif isinstance(effect, otio.schema.FreezeFrame):\n            # For freeze frame, playback speed must be set after range\n            time_scalar = 0.\n\n    # If reverse playback speed swap source in and out\n    if trackitem.playbackSpeed() < 0:\n        source_out = source_range.start_time.value\n        source_in = source_range.end_time_inclusive().value\n\n        timeline_in = playhead + source_out\n        timeline_out = (\n            timeline_in +\n            source_range.duration.value\n        ) - 1\n    else:\n        # Normal playback speed\n        source_in = source_range.start_time.value\n        source_out = source_range.end_time_inclusive().value\n\n        timeline_in = playhead\n        timeline_out = (\n            timeline_in +\n            source_range.duration.value\n        ) - 1\n\n    # Set source and timeline in/out points\n    trackitem.setTimes(\n        timeline_in,\n        timeline_out,\n        source_in,\n        source_out\n\n    )\n\n    # Apply playback speed for freeze frames\n    if abs(time_scalar) != 1.:\n        trackitem.setPlaybackSpeed(trackitem.playbackSpeed() * time_scalar)\n\n    # Link audio to video when possible\n    if isinstance(track, hiero.core.AudioTrack):\n        for other in track.parent().trackItemsAt(playhead):\n            if other.source() == clip:\n                trackitem.link(other)\n\n    return trackitem\n\n\ndef build_sequence(\n        otio_timeline, project=None, sequence=None, track_kind=None):\n    if project is None:\n        if sequence:\n            project = sequence.project()\n\n        else:\n            # Per version 12.1v2 there is no way of getting active project\n            project = hiero.core.projects(hiero.core.Project.kUserProjects)[-1]\n\n    projectbin = project.clipsBin()\n\n    if not sequence:\n        # Create a Sequence\n        sequence = hiero.core.Sequence(otio_timeline.name or 'OTIOSequence')\n\n        # Set sequence settings from otio timeline if available\n        if (\n            hasattr(otio_timeline, 'global_start_time')\n            and otio_timeline.global_start_time\n        ):\n            start_time = otio_timeline.global_start_time\n            sequence.setFramerate(start_time.rate)\n            sequence.setTimecodeStart(start_time.value)\n\n        # Create a Bin to hold clips\n        projectbin.addItem(hiero.core.BinItem(sequence))\n\n        sequencebin = hiero.core.Bin(sequence.name())\n        projectbin.addItem(sequencebin)\n\n    else:\n        sequencebin = projectbin\n\n    # Get tagsBin\n    tagsbin = hiero.core.project(\"Tag Presets\").tagsBin()\n\n    # Add timeline markers\n    add_markers(otio_timeline, sequence, tagsbin)\n\n    if isinstance(otio_timeline, otio.schema.Timeline):\n        tracks = otio_timeline.tracks\n\n    else:\n        tracks = [otio_timeline]\n\n    for tracknum, otio_track in enumerate(tracks):\n        playhead = 0\n        _transitions = []\n\n        # Add track to sequence\n        track = create_track(otio_track, tracknum, track_kind)\n        sequence.addTrack(track)\n\n        # iterate over items in track\n        for _itemnum, otio_clip in enumerate(otio_track):\n            if isinstance(otio_clip, (otio.schema.Track, otio.schema.Stack)):\n                inform('Nested sequences/tracks are created separately.')\n\n                # Add gap where the nested sequence would have been\n                playhead += otio_clip.source_range.duration.value\n\n                # Process nested sequence\n                build_sequence(\n                    otio_clip,\n                    project=project,\n                    track_kind=otio_track.kind\n                )\n\n            elif isinstance(otio_clip, otio.schema.Clip):\n                # Create a Clip\n                clip = create_clip(otio_clip, tagsbin, sequencebin)\n\n                # Create TrackItem\n                trackitem = create_trackitem(\n                    playhead,\n                    track,\n                    otio_clip,\n                    clip\n                )\n\n                # Add markers\n                add_markers(otio_clip, trackitem, tagsbin)\n\n                # Add trackitem to track\n                track.addTrackItem(trackitem)\n\n                # Update playhead\n                playhead = trackitem.timelineOut() + 1\n\n            elif isinstance(otio_clip, otio.schema.Transition):\n                # Store transitions for when all clips in the track are created\n                _transitions.append((otio_track, otio_clip))\n\n            elif isinstance(otio_clip, otio.schema.Gap):\n                # Hiero has no fillers, slugs or blanks at the moment\n                playhead += otio_clip.source_range.duration.value\n\n        # Apply transitions we stored earlier now that all clips are present\n        warnings = []\n        for otio_track, otio_item in _transitions:\n            # Catch warnings form transitions in case\n            # of unsupported transitions\n            warning = apply_transition(otio_track, otio_item, track)\n            if warning:\n                warnings.append(warning)\n\n        if warnings:\n            inform(warnings)\n"
  },
  {
    "path": "openpype/hosts/hiero/api/otio/utils.py",
    "content": "import re\nimport opentimelineio as otio\n\n\ndef timecode_to_frames(timecode, framerate):\n    rt = otio.opentime.from_timecode(timecode, 24)\n    return int(otio.opentime.to_frames(rt))\n\n\ndef frames_to_timecode(frames, framerate):\n    rt = otio.opentime.from_frames(frames, framerate)\n    return otio.opentime.to_timecode(rt)\n\n\ndef frames_to_secons(frames, framerate):\n    rt = otio.opentime.from_frames(frames, framerate)\n    return otio.opentime.to_seconds(rt)\n\n\ndef get_reformated_path(path, padded=True):\n    \"\"\"\n    Return fixed python expression path\n\n    Args:\n        path (str): path url or simple file name\n\n    Returns:\n        type: string with reformated path\n\n    Example:\n        get_reformated_path(\"plate.[0001-1008].exr\") > plate.%04d.exr\n\n    \"\"\"\n    if \"%\" in path:\n        padding_pattern = r\"(\\d+)\"\n        padding = int(re.findall(padding_pattern, path).pop())\n        num_pattern = r\"(%\\d+d)\"\n        if padded:\n            path = re.sub(num_pattern, \"%0{}d\".format(padding), path)\n        else:\n            path = re.sub(num_pattern, \"%d\", path)\n    return path\n\n\ndef get_padding_from_path(path):\n    \"\"\"\n    Return padding number from DaVinci Resolve sequence path style\n\n    Args:\n        path (str): path url or simple file name\n\n    Returns:\n        int: padding number\n\n    Example:\n        get_padding_from_path(\"plate.[0001-1008].exr\") > 4\n\n    \"\"\"\n    padding_pattern = \"(\\\\d+)(?=-)\"\n    if \"[\" in path:\n        return len(re.findall(padding_pattern, path).pop())\n\n    return None\n\n\ndef get_rate(item):\n    if not hasattr(item, 'framerate'):\n        return None\n\n    num, den = item.framerate().toRational()\n\n    try:\n        rate = float(num) / float(den)\n    except ZeroDivisionError:\n        return None\n\n    if rate.is_integer():\n        return rate\n\n    return round(rate, 4)\n"
  },
  {
    "path": "openpype/hosts/hiero/api/pipeline.py",
    "content": "\"\"\"\nBasic avalon integration\n\"\"\"\nfrom copy import deepcopy\nimport os\nimport contextlib\nfrom collections import OrderedDict\n\nfrom pyblish import api as pyblish\nfrom openpype.lib import Logger\nfrom openpype.pipeline import (\n    schema,\n    register_creator_plugin_path,\n    register_loader_plugin_path,\n    deregister_creator_plugin_path,\n    deregister_loader_plugin_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.tools.utils import host_tools\nfrom . import lib, menu, events\nimport hiero\n\nlog = Logger.get_logger(__name__)\n\n# plugin paths\nAPI_DIR = os.path.dirname(os.path.abspath(__file__))\nHOST_DIR = os.path.dirname(API_DIR)\nPLUGINS_DIR = os.path.join(HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\").replace(\"\\\\\", \"/\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\").replace(\"\\\\\", \"/\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\").replace(\"\\\\\", \"/\")\n\nAVALON_CONTAINERS = \":AVALON_CONTAINERS\"\n\n\ndef install():\n    \"\"\"Installing Hiero integration.\"\"\"\n\n    # adding all events\n    events.register_events()\n\n    log.info(\"Registering Hiero plug-ins..\")\n    pyblish.register_host(\"hiero\")\n    pyblish.register_plugin_path(PUBLISH_PATH)\n    register_loader_plugin_path(LOAD_PATH)\n    register_creator_plugin_path(CREATE_PATH)\n\n    # register callback for switching publishable\n    pyblish.register_callback(\"instanceToggled\", on_pyblish_instance_toggled)\n\n    # install menu\n    menu.menu_install()\n    menu.add_scripts_menu()\n\n    # register hiero events\n    events.register_hiero_events()\n\n\ndef uninstall():\n    \"\"\"\n    Uninstalling Hiero integration for avalon\n\n    \"\"\"\n    log.info(\"Deregistering Hiero plug-ins..\")\n    pyblish.deregister_host(\"hiero\")\n    pyblish.deregister_plugin_path(PUBLISH_PATH)\n    deregister_loader_plugin_path(LOAD_PATH)\n    deregister_creator_plugin_path(CREATE_PATH)\n\n    # register callback for switching publishable\n    pyblish.deregister_callback(\"instanceToggled\", on_pyblish_instance_toggled)\n\n\ndef containerise(track_item,\n                 name,\n                 namespace,\n                 context,\n                 loader=None,\n                 data=None):\n    \"\"\"Bundle Hiero's object into an assembly and imprint it with metadata\n\n    Containerisation enables a tracking of version, author and origin\n    for loaded assets.\n\n    Arguments:\n        track_item (hiero.core.TrackItem): object to imprint as container\n        name (str): Name of resulting assembly\n        namespace (str): Namespace under which to host container\n        context (dict): Asset information\n        loader (str, optional): Name of node used to produce this container.\n\n    Returns:\n        track_item (hiero.core.TrackItem): containerised object\n\n    \"\"\"\n\n    data_imprint = OrderedDict({\n        \"schema\": \"openpype:container-2.0\",\n        \"id\": AVALON_CONTAINER_ID,\n        \"name\": str(name),\n        \"namespace\": str(namespace),\n        \"loader\": str(loader),\n        \"representation\": str(context[\"representation\"][\"_id\"]),\n    })\n\n    if data:\n        for k, v in data.items():\n            data_imprint.update({k: v})\n\n    log.debug(\"_ data_imprint: {}\".format(data_imprint))\n    lib.set_trackitem_openpype_tag(track_item, data_imprint)\n\n    return track_item\n\n\ndef ls():\n    \"\"\"List available containers.\n\n    This function is used by the Container Manager in Nuke. You'll\n    need to implement a for-loop that then *yields* one Container at\n    a time.\n\n    See the `container.json` schema for details on how it should look,\n    and the Maya equivalent, which is in `avalon.maya.pipeline`\n    \"\"\"\n\n    # get all track items from current timeline\n    all_items = lib.get_track_items()\n\n    # append all video tracks\n    for track in lib.get_current_sequence():\n        if type(track) != hiero.core.VideoTrack:\n            continue\n        all_items.append(track)\n\n    for item in all_items:\n        container_data = parse_container(item)\n\n        if isinstance(container_data, list):\n            for _c in container_data:\n                yield _c\n        elif container_data:\n            yield container_data\n\n\ndef parse_container(item, validate=True):\n    \"\"\"Return container data from track_item's pype tag.\n\n    Args:\n        item (hiero.core.TrackItem or hiero.core.VideoTrack):\n            A containerised track item.\n        validate (bool)[optional]: validating with avalon scheme\n\n    Returns:\n        dict: The container schema data for input containerized track item.\n\n    \"\"\"\n    def data_to_container(item, data):\n        if (\n            not data\n            or data.get(\"id\") != \"pyblish.avalon.container\"\n        ):\n            return\n\n        if validate and data and data.get(\"schema\"):\n            schema.validate(data)\n\n        if not isinstance(data, dict):\n            return\n\n        # If not all required data return the empty container\n        required = ['schema', 'id', 'name',\n                    'namespace', 'loader', 'representation']\n\n        if any(key not in data for key in required):\n            return\n\n        container = {key: data[key] for key in required}\n\n        container[\"objectName\"] = item.name()\n\n        # Store reference to the node object\n        container[\"_item\"] = item\n\n        return container\n\n    # convert tag metadata to normal keys names\n    if type(item) == hiero.core.VideoTrack:\n        return_list = []\n        _data = lib.get_track_openpype_data(item)\n\n        if not _data:\n            return\n        # convert the data to list and validate them\n        for _, obj_data in _data.items():\n            container = data_to_container(item, obj_data)\n            return_list.append(container)\n        return return_list\n    else:\n        _data = lib.get_trackitem_openpype_data(item)\n        return data_to_container(item, _data)\n\n\ndef _update_container_data(container, data):\n    for key in container:\n        try:\n            container[key] = data[key]\n        except KeyError:\n            pass\n    return container\n\n\ndef update_container(item, data=None):\n    \"\"\"Update container data to input track_item or track's\n    openpype tag.\n\n    Args:\n        item (hiero.core.TrackItem or hiero.core.VideoTrack):\n            A containerised track item.\n        data (dict)[optional]: dictionery with data to be updated\n\n    Returns:\n        bool: True if container was updated correctly\n\n    \"\"\"\n\n    data = data or {}\n    data = deepcopy(data)\n\n    if type(item) == hiero.core.VideoTrack:\n        # form object data for test\n        object_name = data[\"objectName\"]\n\n        # get all available containers\n        containers = lib.get_track_openpype_data(item)\n        container = lib.get_track_openpype_data(item, object_name)\n\n        containers = deepcopy(containers)\n        container = deepcopy(container)\n\n        # update data in container\n        updated_container = _update_container_data(container, data)\n        # merge updated container back to containers\n        containers.update({object_name: updated_container})\n\n        return bool(lib.set_track_openpype_tag(item, containers))\n    else:\n        container = lib.get_trackitem_openpype_data(item)\n        updated_container = _update_container_data(container, data)\n\n        log.info(\"Updating container: `{}`\".format(item.name()))\n        return bool(lib.set_trackitem_openpype_tag(item, updated_container))\n\n\ndef launch_workfiles_app(*args):\n    ''' Wrapping function for workfiles launcher '''\n    from .lib import get_main_window\n\n    main_window = get_main_window()\n    # show workfile gui\n    host_tools.show_workfiles(parent=main_window)\n\n\ndef publish(parent):\n    \"\"\"Shorthand to publish from within host\"\"\"\n    return host_tools.show_publish(parent)\n\n\n@contextlib.contextmanager\ndef maintained_selection():\n    \"\"\"Maintain selection during context\n\n    Example:\n        >>> with maintained_selection():\n        ...     for track_item in track_items:\n        ...         < do some stuff >\n    \"\"\"\n    from .lib import (\n        set_selected_track_items,\n        get_selected_track_items\n    )\n    previous_selection = get_selected_track_items()\n    reset_selection()\n    try:\n        # do the operation\n        yield\n    finally:\n        reset_selection()\n        set_selected_track_items(previous_selection)\n\n\ndef reset_selection():\n    \"\"\"Deselect all selected nodes\n    \"\"\"\n    from .lib import set_selected_track_items\n    set_selected_track_items([])\n\n\ndef reload_config():\n    \"\"\"Attempt to reload pipeline at run-time.\n\n    CAUTION: This is primarily for development and debugging purposes.\n\n    \"\"\"\n    import importlib\n\n    for module in (\n        \"openpype.hosts.hiero.lib\",\n        \"openpype.hosts.hiero.menu\",\n        \"openpype.hosts.hiero.tags\"\n    ):\n        log.info(\"Reloading module: {}...\".format(module))\n        try:\n            module = importlib.import_module(module)\n            import imp\n            imp.reload(module)\n        except Exception as e:\n            log.warning(\"Cannot reload module: {}\".format(e))\n            importlib.reload(module)\n\n\ndef on_pyblish_instance_toggled(instance, old_value, new_value):\n    \"\"\"Toggle node passthrough states on instance toggles.\"\"\"\n\n    log.info(\"instance toggle: {}, old_value: {}, new_value:{} \".format(\n        instance, old_value, new_value))\n\n    from openpype.hosts.hiero.api import (\n        get_trackitem_openpype_tag,\n        set_publish_attribute\n    )\n\n    # Whether instances should be passthrough based on new value\n    track_item = instance.data[\"item\"]\n    tag = get_trackitem_openpype_tag(track_item)\n    set_publish_attribute(tag, new_value)\n"
  },
  {
    "path": "openpype/hosts/hiero/api/plugin.py",
    "content": "import os\nfrom pprint import pformat\nimport re\nfrom copy import deepcopy\n\nimport hiero\n\nfrom qtpy import QtWidgets, QtCore\nimport qargparse\n\nfrom openpype.settings import get_current_project_settings\nfrom openpype.lib import Logger\nfrom openpype.pipeline import LoaderPlugin, LegacyCreator\nfrom openpype.pipeline.load import get_representation_path_from_context\nfrom . import lib\n\nlog = Logger.get_logger(__name__)\n\n\ndef load_stylesheet():\n    path = os.path.join(os.path.dirname(__file__), \"style.css\")\n    if not os.path.exists(path):\n        log.warning(\"Unable to load stylesheet, file not found in resources\")\n        return \"\"\n\n    with open(path, \"r\") as file_stream:\n        stylesheet = file_stream.read()\n    return stylesheet\n\n\nclass CreatorWidget(QtWidgets.QDialog):\n\n    # output items\n    items = {}\n\n    def __init__(self, name, info, ui_inputs, parent=None):\n        super(CreatorWidget, self).__init__(parent)\n\n        self.setObjectName(name)\n\n        self.setWindowFlags(\n            QtCore.Qt.Window\n            | QtCore.Qt.CustomizeWindowHint\n            | QtCore.Qt.WindowTitleHint\n            | QtCore.Qt.WindowCloseButtonHint\n            | QtCore.Qt.WindowStaysOnTopHint\n        )\n        self.setWindowTitle(name or \"Pype Creator Input\")\n        self.resize(500, 700)\n\n        # Where inputs and labels are set\n        self.content_widget = [QtWidgets.QWidget(self)]\n        top_layout = QtWidgets.QFormLayout(self.content_widget[0])\n        top_layout.setObjectName(\"ContentLayout\")\n        top_layout.addWidget(Spacer(5, self))\n\n        # first add widget tag line\n        top_layout.addWidget(QtWidgets.QLabel(info))\n\n        # main dynamic layout\n        self.scroll_area = QtWidgets.QScrollArea(self, widgetResizable=True)\n        self.scroll_area.setVerticalScrollBarPolicy(\n            QtCore.Qt.ScrollBarAsNeeded)\n        self.scroll_area.setVerticalScrollBarPolicy(\n            QtCore.Qt.ScrollBarAlwaysOn)\n        self.scroll_area.setHorizontalScrollBarPolicy(\n            QtCore.Qt.ScrollBarAlwaysOff)\n        self.scroll_area.setWidgetResizable(True)\n\n        self.content_widget.append(self.scroll_area)\n\n        scroll_widget = QtWidgets.QWidget(self)\n        in_scroll_area = QtWidgets.QVBoxLayout(scroll_widget)\n        self.content_layout = [in_scroll_area]\n\n        # add preset data into input widget layout\n        self.items = self.populate_widgets(ui_inputs)\n        self.scroll_area.setWidget(scroll_widget)\n\n        # Confirmation buttons\n        btns_widget = QtWidgets.QWidget(self)\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\")\n        btns_layout.addWidget(cancel_btn)\n\n        ok_btn = QtWidgets.QPushButton(\"Ok\")\n        btns_layout.addWidget(ok_btn)\n\n        # Main layout of the dialog\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(10, 10, 10, 10)\n        main_layout.setSpacing(0)\n\n        # adding content widget\n        for w in self.content_widget:\n            main_layout.addWidget(w)\n\n        main_layout.addWidget(btns_widget)\n\n        ok_btn.clicked.connect(self._on_ok_clicked)\n        cancel_btn.clicked.connect(self._on_cancel_clicked)\n\n        stylesheet = load_stylesheet()\n        self.setStyleSheet(stylesheet)\n\n    def _on_ok_clicked(self):\n        self.result = self.value(self.items)\n        self.close()\n\n    def _on_cancel_clicked(self):\n        self.result = None\n        self.close()\n\n    def value(self, data, new_data=None):\n        new_data = new_data or dict()\n        for k, v in data.items():\n            new_data[k] = {\n                \"target\": None,\n                \"value\": None\n            }\n            if v[\"type\"] == \"dict\":\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = self.value(v[\"value\"])\n            if v[\"type\"] == \"section\":\n                new_data.pop(k)\n                new_data = self.value(v[\"value\"], new_data)\n            elif getattr(v[\"value\"], \"currentText\", None):\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = v[\"value\"].currentText()\n            elif getattr(v[\"value\"], \"isChecked\", None):\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = v[\"value\"].isChecked()\n            elif getattr(v[\"value\"], \"value\", None):\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = v[\"value\"].value()\n            elif getattr(v[\"value\"], \"text\", None):\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = v[\"value\"].text()\n\n        return new_data\n\n    def camel_case_split(self, text):\n        matches = re.finditer(\n            '.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', text)\n        return \" \".join([str(m.group(0)).capitalize() for m in matches])\n\n    def create_row(self, layout, type, text, **kwargs):\n        value_keys = [\"setText\", \"setCheckState\", \"setValue\", \"setChecked\"]\n\n        # get type attribute from qwidgets\n        attr = getattr(QtWidgets, type)\n\n        # convert label text to normal capitalized text with spaces\n        label_text = self.camel_case_split(text)\n\n        # assign the new text to label widget\n        label = QtWidgets.QLabel(label_text)\n        label.setObjectName(\"LineLabel\")\n\n        # create attribute name text strip of spaces\n        attr_name = text.replace(\" \", \"\")\n\n        # create attribute and assign default values\n        setattr(\n            self,\n            attr_name,\n            attr(parent=self))\n\n        # assign the created attribute to variable\n        item = getattr(self, attr_name)\n\n        # set attributes to item which are not values\n        for func, val in kwargs.items():\n            if func in value_keys:\n                continue\n\n            if getattr(item, func):\n                log.debug(\"Setting {} to {}\".format(func, val))\n                func_attr = getattr(item, func)\n                if isinstance(val, tuple):\n                    func_attr(*val)\n                else:\n                    func_attr(val)\n\n        # set values to item\n        for value_item in value_keys:\n            if value_item not in kwargs:\n                continue\n            if getattr(item, value_item):\n                getattr(item, value_item)(kwargs[value_item])\n\n        # add to layout\n        layout.addRow(label, item)\n\n        return item\n\n    def populate_widgets(self, data, content_layout=None):\n        \"\"\"\n        Populate widget from input dict.\n\n        Each plugin has its own set of widget rows defined in dictionary\n        each row values should have following keys: `type`, `target`,\n        `label`, `order`, `value` and optionally also `toolTip`.\n\n        Args:\n            data (dict): widget rows or organized groups defined\n                         by types `dict` or `section`\n            content_layout (QtWidgets.QFormLayout)[optional]: used when nesting\n\n        Returns:\n            dict: redefined data dict updated with created widgets\n\n        \"\"\"\n\n        content_layout = content_layout or self.content_layout[-1]\n        # fix order of process by defined order value\n        ordered_keys = list(data.keys())\n        for k, v in data.items():\n            try:\n                # try removing a key from index which should\n                # be filled with new\n                ordered_keys.pop(v[\"order\"])\n            except IndexError:\n                pass\n            # add key into correct order\n            ordered_keys.insert(v[\"order\"], k)\n\n        # process ordered\n        for k in ordered_keys:\n            v = data[k]\n            tool_tip = v.get(\"toolTip\", \"\")\n            if v[\"type\"] == \"dict\":\n                # adding spacer between sections\n                self.content_layout.append(QtWidgets.QWidget(self))\n                content_layout.addWidget(self.content_layout[-1])\n                self.content_layout[-1].setObjectName(\"sectionHeadline\")\n\n                headline = QtWidgets.QVBoxLayout(self.content_layout[-1])\n                headline.addWidget(Spacer(20, self))\n                headline.addWidget(QtWidgets.QLabel(v[\"label\"]))\n\n                # adding nested layout with label\n                self.content_layout.append(QtWidgets.QWidget(self))\n                self.content_layout[-1].setObjectName(\"sectionContent\")\n\n                nested_content_layout = QtWidgets.QFormLayout(\n                    self.content_layout[-1])\n                nested_content_layout.setObjectName(\"NestedContentLayout\")\n                content_layout.addWidget(self.content_layout[-1])\n\n                # add nested key as label\n                data[k][\"value\"] = self.populate_widgets(\n                    v[\"value\"], nested_content_layout)\n\n            if v[\"type\"] == \"section\":\n                # adding spacer between sections\n                self.content_layout.append(QtWidgets.QWidget(self))\n                content_layout.addWidget(self.content_layout[-1])\n                self.content_layout[-1].setObjectName(\"sectionHeadline\")\n\n                headline = QtWidgets.QVBoxLayout(self.content_layout[-1])\n                headline.addWidget(Spacer(20, self))\n                headline.addWidget(QtWidgets.QLabel(v[\"label\"]))\n\n                # adding nested layout with label\n                self.content_layout.append(QtWidgets.QWidget(self))\n                self.content_layout[-1].setObjectName(\"sectionContent\")\n\n                nested_content_layout = QtWidgets.QFormLayout(\n                    self.content_layout[-1])\n                nested_content_layout.setObjectName(\"NestedContentLayout\")\n                content_layout.addWidget(self.content_layout[-1])\n\n                # add nested key as label\n                data[k][\"value\"] = self.populate_widgets(\n                    v[\"value\"], nested_content_layout)\n\n            elif v[\"type\"] == \"QLineEdit\":\n                data[k][\"value\"] = self.create_row(\n                    content_layout, \"QLineEdit\", v[\"label\"],\n                    setText=v[\"value\"], setToolTip=tool_tip)\n            elif v[\"type\"] == \"QComboBox\":\n                data[k][\"value\"] = self.create_row(\n                    content_layout, \"QComboBox\", v[\"label\"],\n                    addItems=v[\"value\"], setToolTip=tool_tip)\n            elif v[\"type\"] == \"QCheckBox\":\n                data[k][\"value\"] = self.create_row(\n                    content_layout, \"QCheckBox\", v[\"label\"],\n                    setChecked=v[\"value\"], setToolTip=tool_tip)\n            elif v[\"type\"] == \"QSpinBox\":\n                data[k][\"value\"] = self.create_row(\n                    content_layout, \"QSpinBox\", v[\"label\"],\n                    setValue=v[\"value\"],\n                    setDisplayIntegerBase=10000,\n                    setRange=(0, 99999), setMinimum=0,\n                    setMaximum=100000, setToolTip=tool_tip)\n\n        return data\n\n\nclass Spacer(QtWidgets.QWidget):\n    def __init__(self, height, *args, **kwargs):\n        super(self.__class__, self).__init__(*args, **kwargs)\n\n        self.setFixedHeight(height)\n\n        real_spacer = QtWidgets.QWidget(self)\n        real_spacer.setObjectName(\"Spacer\")\n        real_spacer.setFixedHeight(height)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(real_spacer)\n\n        self.setLayout(layout)\n\n\nclass SequenceLoader(LoaderPlugin):\n    \"\"\"A basic SequenceLoader for Resolve\n\n    This will implement the basic behavior for a loader to inherit from that\n    will containerize the reference and will implement the `remove` and\n    `update` logic.\n\n    \"\"\"\n\n    options = [\n        qargparse.Boolean(\n            \"handles\",\n            label=\"Include handles\",\n            default=0,\n            help=\"Load with handles or without?\"\n        ),\n        qargparse.Choice(\n            \"load_to\",\n            label=\"Where to load clips\",\n            items=[\n                \"Current timeline\",\n                \"New timeline\"\n            ],\n            default=\"Current timeline\",\n            help=\"Where do you want clips to be loaded?\"\n        ),\n        qargparse.Choice(\n            \"load_how\",\n            label=\"How to load clips\",\n            items=[\n                \"Original timing\",\n                \"Sequentially in order\"\n            ],\n            default=\"Original timing\",\n            help=\"Would you like to place it at original timing?\"\n        )\n    ]\n\n    def load(\n        self,\n        context,\n        name=None,\n        namespace=None,\n        options=None\n    ):\n        pass\n\n    def update(self, container, representation):\n        \"\"\"Update an existing `container`\n        \"\"\"\n        pass\n\n    def remove(self, container):\n        \"\"\"Remove an existing `container`\n        \"\"\"\n        pass\n\n\nclass ClipLoader:\n\n    active_bin = None\n    data = dict()\n\n    def __init__(self, cls, context, path, **options):\n        \"\"\" Initialize object\n\n        Arguments:\n            cls (avalon.api.Loader): plugin object\n            context (dict): loader plugin context\n            options (dict)[optional]: possible keys:\n                projectBinPath: \"path/to/binItem\"\n\n        \"\"\"\n        self.__dict__.update(cls.__dict__)\n        self.context = context\n        self.active_project = lib.get_current_project()\n        self.fname = path\n\n        # try to get value from options or evaluate key value for `handles`\n        self.with_handles = options.get(\"handles\") or bool(\n            options.get(\"handles\") is True)\n        # try to get value from options or evaluate key value for `load_how`\n        self.sequencial_load = options.get(\"sequentially\") or bool(\n            \"Sequentially in order\" in options.get(\"load_how\", \"\"))\n        # try to get value from options or evaluate key value for `load_to`\n        self.new_sequence = options.get(\"newSequence\") or bool(\n            \"New timeline\" in options.get(\"load_to\", \"\"))\n        self.clip_name_template = options.get(\n            \"clipNameTemplate\") or \"{asset}_{subset}_{representation}\"\n        assert self._populate_data(), str(\n            \"Cannot Load selected data, look into database \"\n            \"or call your supervisor\")\n\n        # inject asset data to representation dict\n        self._get_asset_data()\n        log.info(\"__init__ self.data: `{}`\".format(pformat(self.data)))\n        log.info(\"__init__ options: `{}`\".format(pformat(options)))\n\n        # add active components to class\n        if self.new_sequence:\n            if options.get(\"sequence\"):\n                # if multiselection is set then use options sequence\n                self.active_sequence = options[\"sequence\"]\n            else:\n                # create new sequence\n                self.active_sequence = lib.get_current_sequence(new=True)\n                self.active_sequence.setFramerate(\n                    hiero.core.TimeBase.fromString(\n                        str(self.data[\"assetData\"][\"fps\"])))\n        else:\n            self.active_sequence = lib.get_current_sequence()\n\n        if options.get(\"track\"):\n            # if multiselection is set then use options track\n            self.active_track = options[\"track\"]\n        else:\n            self.active_track = lib.get_current_track(\n                self.active_sequence, self.data[\"track_name\"])\n\n    def _populate_data(self):\n        \"\"\" Gets context and convert it to self.data\n        data structure:\n            {\n                \"name\": \"assetName_subsetName_representationName\"\n                \"path\": \"path/to/file/created/by/get_repr..\",\n                \"binPath\": \"projectBinPath\",\n            }\n        \"\"\"\n        # create name\n        repr = self.context[\"representation\"]\n        repr_cntx = repr[\"context\"]\n        asset = str(repr_cntx[\"asset\"])\n        subset = str(repr_cntx[\"subset\"])\n        representation = str(repr_cntx[\"representation\"])\n        self.data[\"clip_name\"] = self.clip_name_template.format(**repr_cntx)\n        self.data[\"track_name\"] = \"_\".join([subset, representation])\n        self.data[\"versionData\"] = self.context[\"version\"][\"data\"]\n        # gets file path\n        file = get_representation_path_from_context(self.context)\n        if not file:\n            repr_id = repr[\"_id\"]\n            log.warning(\n                \"Representation id `{}` is failing to load\".format(repr_id))\n            return None\n        self.data[\"path\"] = file.replace(\"\\\\\", \"/\")\n\n        # convert to hashed path\n        if repr_cntx.get(\"frame\"):\n            self._fix_path_hashes()\n\n        # solve project bin structure path\n        hierarchy = str(\"/\".join((\n            \"Loader\",\n            repr_cntx[\"hierarchy\"].replace(\"\\\\\", \"/\"),\n            asset\n        )))\n\n        self.data[\"binPath\"] = hierarchy\n\n        return True\n\n    def _fix_path_hashes(self):\n        \"\"\" Convert file path where it is needed padding with hashes\n        \"\"\"\n        file = self.data[\"path\"]\n        if \"#\" not in file:\n            frame = self.context[\"representation\"][\"context\"].get(\"frame\")\n            padding = len(frame)\n            file = file.replace(frame, \"#\" * padding)\n        self.data[\"path\"] = file\n\n    def _get_asset_data(self):\n        \"\"\" Get all available asset data\n\n        joint `data` key with asset.data dict into the representation\n\n        \"\"\"\n\n        asset_doc = self.context[\"asset\"]\n        self.data[\"assetData\"] = asset_doc[\"data\"]\n\n    def _make_track_item(self, source_bin_item, audio=False):\n        \"\"\" Create track item with \"\"\"\n\n        clip = source_bin_item.activeItem()\n\n        # add to track as clip item\n        if not audio:\n            track_item = hiero.core.TrackItem(\n                self.data[\"clip_name\"], hiero.core.TrackItem.kVideo)\n        else:\n            track_item = hiero.core.TrackItem(\n                self.data[\"clip_name\"], hiero.core.TrackItem.kAudio)\n\n        track_item.setSource(clip)\n        track_item.setSourceIn(self.handle_start)\n        track_item.setTimelineIn(self.timeline_in)\n        track_item.setSourceOut((self.media_duration) - self.handle_end)\n        track_item.setTimelineOut(self.timeline_out)\n        track_item.setPlaybackSpeed(1)\n        self.active_track.addTrackItem(track_item)\n\n        return track_item\n\n    def load(self):\n        # create project bin for the media to be imported into\n        self.active_bin = lib.create_bin(self.data[\"binPath\"])\n\n        # create mediaItem in active project bin\n        # create clip media\n        self.media = hiero.core.MediaSource(self.data[\"path\"])\n        self.media_duration = int(self.media.duration())\n\n        # get handles\n        self.handle_start = self.data[\"versionData\"].get(\"handleStart\")\n        self.handle_end = self.data[\"versionData\"].get(\"handleEnd\")\n        if self.handle_start is None:\n            self.handle_start = self.data[\"assetData\"][\"handleStart\"]\n        if self.handle_end is None:\n            self.handle_end = self.data[\"assetData\"][\"handleEnd\"]\n\n        self.handle_start = int(self.handle_start)\n        self.handle_end = int(self.handle_end)\n\n        if self.sequencial_load:\n            last_track_item = lib.get_track_items(\n                sequence_name=self.active_sequence.name(),\n                track_name=self.active_track.name()\n            )\n            if len(last_track_item) == 0:\n                last_timeline_out = 0\n            else:\n                last_track_item = last_track_item[-1]\n                last_timeline_out = int(last_track_item.timelineOut()) + 1\n            self.timeline_in = last_timeline_out\n            self.timeline_out = last_timeline_out + int(\n                self.data[\"assetData\"][\"clipOut\"]\n                - self.data[\"assetData\"][\"clipIn\"])\n        else:\n            self.timeline_in = int(self.data[\"assetData\"][\"clipIn\"])\n            self.timeline_out = int(self.data[\"assetData\"][\"clipOut\"])\n\n        log.debug(\"__ self.timeline_in: {}\".format(self.timeline_in))\n        log.debug(\"__ self.timeline_out: {}\".format(self.timeline_out))\n\n        # check if slate is included\n        slate_on = \"slate\" in self.context[\"version\"][\"data\"][\"families\"]\n        log.debug(\"__ slate_on: {}\".format(slate_on))\n\n        # if slate is on then remove the slate frame from beginning\n        if slate_on:\n            self.media_duration -= 1\n            self.handle_start += 1\n\n        # create Clip from Media\n        clip = hiero.core.Clip(self.media)\n        clip.setName(self.data[\"clip_name\"])\n\n        # add Clip to bin if not there yet\n        if self.data[\"clip_name\"] not in [\n                b.name() for b in self.active_bin.items()]:\n            bin_item = hiero.core.BinItem(clip)\n            self.active_bin.addItem(bin_item)\n\n        # just make sure the clip is created\n        # there were some cases were hiero was not creating it\n        source_bin_item = None\n        for item in self.active_bin.items():\n            if self.data[\"clip_name\"] == item.name():\n                source_bin_item = item\n        if not source_bin_item:\n            log.warning(\"Problem with created Source clip: `{}`\".format(\n                self.data[\"clip_name\"]))\n\n        # include handles\n        if self.with_handles:\n            self.timeline_in -= self.handle_start\n            self.timeline_out += self.handle_end\n            self.handle_start = 0\n            self.handle_end = 0\n\n        # make track item from source in bin as item\n        track_item = self._make_track_item(source_bin_item)\n\n        log.info(\"Loading clips: `{}`\".format(self.data[\"clip_name\"]))\n        return track_item\n\n\nclass Creator(LegacyCreator):\n    \"\"\"Creator class wrapper\n    \"\"\"\n    clip_color = \"Purple\"\n    rename_index = None\n\n    def __init__(self, *args, **kwargs):\n        super(Creator, self).__init__(*args, **kwargs)\n        import openpype.hosts.hiero.api as phiero\n        self.presets = get_current_project_settings()[\n            \"hiero\"][\"create\"].get(self.__class__.__name__, {})\n\n        # adding basic current context resolve objects\n        self.project = phiero.get_current_project()\n        self.sequence = phiero.get_current_sequence()\n\n        if (self.options or {}).get(\"useSelection\"):\n            timeline_selection = phiero.get_timeline_selection()\n            self.selected = phiero.get_track_items(\n                selection=timeline_selection\n            )\n        else:\n            self.selected = phiero.get_track_items()\n\n        self.widget = CreatorWidget\n\n\nclass PublishClip:\n    \"\"\"\n    Convert a track item to publishable instance\n\n    Args:\n        track_item (hiero.core.TrackItem): hiero track item object\n        kwargs (optional): additional data needed for rename=True (presets)\n\n    Returns:\n        hiero.core.TrackItem: hiero track item object with pype tag\n    \"\"\"\n    vertical_clip_match = {}\n    tag_data = {}\n    types = {\n        \"shot\": \"shot\",\n        \"folder\": \"folder\",\n        \"episode\": \"episode\",\n        \"sequence\": \"sequence\",\n        \"track\": \"sequence\",\n    }\n\n    # parents search pattern\n    parents_search_pattern = r\"\\{([a-z]*?)\\}\"\n\n    # default templates for non-ui use\n    rename_default = False\n    hierarchy_default = \"{_folder_}/{_sequence_}/{_track_}\"\n    clip_name_default = \"shot_{_trackIndex_:0>3}_{_clipIndex_:0>4}\"\n    subset_name_default = \"<track_name>\"\n    review_track_default = \"< none >\"\n    subset_family_default = \"plate\"\n    count_from_default = 10\n    count_steps_default = 10\n    vertical_sync_default = False\n    driving_layer_default = \"\"\n\n    def __init__(self, cls, track_item, **kwargs):\n        # populate input cls attribute onto self.[attr]\n        self.__dict__.update(cls.__dict__)\n\n        # get main parent objects\n        self.track_item = track_item\n        sequence_name = lib.get_current_sequence().name()\n        self.sequence_name = str(sequence_name).replace(\" \", \"_\")\n\n        # track item (clip) main attributes\n        self.ti_name = track_item.name()\n        self.ti_index = int(track_item.eventNumber())\n\n        # get track name and index\n        track_name = track_item.parent().name()\n        self.track_name = str(track_name).replace(\" \", \"_\")\n        self.track_index = int(track_item.parent().trackIndex())\n\n        # adding tag.family into tag\n        if kwargs.get(\"avalon\"):\n            self.tag_data.update(kwargs[\"avalon\"])\n\n        # add publish attribute to tag data\n        self.tag_data.update({\"publish\": True})\n\n        # adding ui inputs if any\n        self.ui_inputs = kwargs.get(\"ui_inputs\", {})\n\n        # populate default data before we get other attributes\n        self._populate_track_item_default_data()\n\n        # use all populated default data to create all important attributes\n        self._populate_attributes()\n\n        # create parents with correct types\n        self._create_parents()\n\n    def convert(self):\n        # solve track item data and add them to tag data\n        tag_hierarchy_data = self._convert_to_tag_data()\n\n        self.tag_data.update(tag_hierarchy_data)\n\n        # if track name is in review track name and also if driving track name\n        # is not in review track name: skip tag creation\n        if (self.track_name in self.review_layer) and (\n                self.driving_layer not in self.review_layer):\n            return\n\n        # deal with clip name\n        new_name = self.tag_data.pop(\"newClipName\")\n\n        if self.rename:\n            # rename track item\n            self.track_item.setName(new_name)\n            self.tag_data[\"asset_name\"] = new_name\n        else:\n            self.tag_data[\"asset_name\"] = self.ti_name\n            self.tag_data[\"hierarchyData\"][\"shot\"] = self.ti_name\n\n        # AYON unique identifier\n        folder_path = \"/{}/{}\".format(\n            tag_hierarchy_data[\"hierarchy\"],\n            self.tag_data[\"asset_name\"]\n        )\n        self.tag_data[\"folderPath\"] = folder_path\n        if self.tag_data[\"heroTrack\"] and self.review_layer:\n            self.tag_data.update({\"reviewTrack\": self.review_layer})\n        else:\n            self.tag_data.update({\"reviewTrack\": None})\n\n        # TODO: remove debug print\n        log.debug(\"___ self.tag_data: {}\".format(\n            pformat(self.tag_data)\n        ))\n\n        # create pype tag on track_item and add data\n        lib.imprint(self.track_item, self.tag_data)\n\n        return self.track_item\n\n    def _populate_track_item_default_data(self):\n        \"\"\" Populate default formatting data from track item. \"\"\"\n\n        self.track_item_default_data = {\n            \"_folder_\": \"shots\",\n            \"_sequence_\": self.sequence_name,\n            \"_track_\": self.track_name,\n            \"_clip_\": self.ti_name,\n            \"_trackIndex_\": self.track_index,\n            \"_clipIndex_\": self.ti_index\n        }\n\n    def _populate_attributes(self):\n        \"\"\" Populate main object attributes. \"\"\"\n        # track item frame range and parent track name for vertical sync check\n        self.clip_in = int(self.track_item.timelineIn())\n        self.clip_out = int(self.track_item.timelineOut())\n\n        # define ui inputs if non gui mode was used\n        self.shot_num = self.ti_index\n        log.debug(\n            \"____ self.shot_num: {}\".format(self.shot_num))\n\n        # ui_inputs data or default values if gui was not used\n        self.rename = self.ui_inputs.get(\n            \"clipRename\", {}).get(\"value\") or self.rename_default\n        self.clip_name = self.ui_inputs.get(\n            \"clipName\", {}).get(\"value\") or self.clip_name_default\n        self.hierarchy = self.ui_inputs.get(\n            \"hierarchy\", {}).get(\"value\") or self.hierarchy_default\n        self.hierarchy_data = self.ui_inputs.get(\n            \"hierarchyData\", {}).get(\"value\") or \\\n            self.track_item_default_data.copy()\n        self.count_from = self.ui_inputs.get(\n            \"countFrom\", {}).get(\"value\") or self.count_from_default\n        self.count_steps = self.ui_inputs.get(\n            \"countSteps\", {}).get(\"value\") or self.count_steps_default\n        self.subset_name = self.ui_inputs.get(\n            \"subsetName\", {}).get(\"value\") or self.subset_name_default\n        self.subset_family = self.ui_inputs.get(\n            \"subsetFamily\", {}).get(\"value\") or self.subset_family_default\n        self.vertical_sync = self.ui_inputs.get(\n            \"vSyncOn\", {}).get(\"value\") or self.vertical_sync_default\n        self.driving_layer = self.ui_inputs.get(\n            \"vSyncTrack\", {}).get(\"value\") or self.driving_layer_default\n        self.review_track = self.ui_inputs.get(\n            \"reviewTrack\", {}).get(\"value\") or self.review_track_default\n        self.audio = self.ui_inputs.get(\n            \"audio\", {}).get(\"value\") or False\n\n        # build subset name from layer name\n        if self.subset_name == \"<track_name>\":\n            self.subset_name = self.track_name\n\n        # create subset for publishing\n        self.subset = self.subset_family + self.subset_name.capitalize()\n\n    def _replace_hash_to_expression(self, name, text):\n        \"\"\" Replace hash with number in correct padding. \"\"\"\n        _spl = text.split(\"#\")\n        _len = (len(_spl) - 1)\n        _repl = \"{{{0}:0>{1}}}\".format(name, _len)\n        return text.replace((\"#\" * _len), _repl)\n\n\n    def _convert_to_tag_data(self):\n        \"\"\" Convert internal data to tag data.\n\n        Populating the tag data into internal variable self.tag_data\n        \"\"\"\n        # define vertical sync attributes\n        hero_track = True\n        self.review_layer = \"\"\n        if self.vertical_sync:\n            # check if track name is not in driving layer\n            if self.track_name not in self.driving_layer:\n                # if it is not then define vertical sync as None\n                hero_track = False\n\n        # increasing steps by index of rename iteration\n        self.count_steps *= self.rename_index\n\n        hierarchy_formatting_data = {}\n        hierarchy_data = deepcopy(self.hierarchy_data)\n        _data = self.track_item_default_data.copy()\n        if self.ui_inputs:\n            # adding tag metadata from ui\n            for _k, _v in self.ui_inputs.items():\n                if _v[\"target\"] == \"tag\":\n                    self.tag_data[_k] = _v[\"value\"]\n\n            # driving layer is set as positive match\n            if hero_track or self.vertical_sync:\n                # mark review layer\n                if self.review_track and (\n                        self.review_track not in self.review_track_default):\n                    # if review layer is defined and not the same as default\n                    self.review_layer = self.review_track\n                # shot num calculate\n                if self.rename_index == 0:\n                    self.shot_num = self.count_from\n                else:\n                    self.shot_num = self.count_from + self.count_steps\n\n            # clip name sequence number\n            _data.update({\"shot\": self.shot_num})\n\n            # solve # in test to pythonic expression\n            for _k, _v in hierarchy_data.items():\n                if \"#\" not in _v[\"value\"]:\n                    continue\n                hierarchy_data[\n                    _k][\"value\"] = self._replace_hash_to_expression(\n                        _k, _v[\"value\"])\n\n            # fill up pythonic expresisons in hierarchy data\n            for k, _v in hierarchy_data.items():\n                hierarchy_formatting_data[k] = _v[\"value\"].format(**_data)\n        else:\n            # if no gui mode then just pass default data\n            hierarchy_formatting_data = hierarchy_data\n\n        tag_hierarchy_data = self._solve_tag_hierarchy_data(\n            hierarchy_formatting_data\n        )\n\n        tag_hierarchy_data.update({\"heroTrack\": True})\n        if hero_track and self.vertical_sync:\n            self.vertical_clip_match.update({\n                (self.clip_in, self.clip_out): tag_hierarchy_data\n            })\n\n        if not hero_track and self.vertical_sync:\n            # driving layer is set as negative match\n            for (_in, _out), hero_data in self.vertical_clip_match.items():\n                hero_data.update({\"heroTrack\": False})\n                if _in == self.clip_in and _out == self.clip_out:\n                    data_subset = hero_data[\"subset\"]\n                    # add track index in case duplicity of names in hero data\n                    if self.subset in data_subset:\n                        hero_data[\"subset\"] = self.subset + str(\n                            self.track_index)\n                    # in case track name and subset name is the same then add\n                    if self.subset_name == self.track_name:\n                        hero_data[\"subset\"] = self.subset\n                    # assign data to return hierarchy data to tag\n                    tag_hierarchy_data = hero_data\n\n        # add data to return data dict\n        return tag_hierarchy_data\n\n    def _solve_tag_hierarchy_data(self, hierarchy_formatting_data):\n        \"\"\" Solve tag data from hierarchy data and templates. \"\"\"\n        # fill up clip name and hierarchy keys\n        hierarchy_filled = self.hierarchy.format(**hierarchy_formatting_data)\n        clip_name_filled = self.clip_name.format(**hierarchy_formatting_data)\n\n        # remove shot from hierarchy data: is not needed anymore\n        hierarchy_formatting_data.pop(\"shot\")\n\n        return {\n            \"newClipName\": clip_name_filled,\n            \"hierarchy\": hierarchy_filled,\n            \"parents\": self.parents,\n            \"hierarchyData\": hierarchy_formatting_data,\n            \"subset\": self.subset,\n            \"family\": self.subset_family,\n            \"families\": [self.data[\"family\"]]\n        }\n\n    def _convert_to_entity(self, type, template):\n        \"\"\" Converting input key to key with type. \"\"\"\n        # convert to entity type\n        entity_type = self.types.get(type, None)\n\n        assert entity_type, \"Missing entity type for `{}`\".format(\n            type\n        )\n\n        # first collect formatting data to use for formatting template\n        formatting_data = {}\n        for _k, _v in self.hierarchy_data.items():\n            value = _v[\"value\"].format(\n                **self.track_item_default_data)\n            formatting_data[_k] = value\n\n        return {\n            \"entity_type\": entity_type,\n            \"entity_name\": template.format(\n                **formatting_data\n            )\n        }\n\n    def _create_parents(self):\n        \"\"\" Create parents and return it in list. \"\"\"\n        self.parents = []\n\n        pattern = re.compile(self.parents_search_pattern)\n\n        par_split = [(pattern.findall(t).pop(), t)\n                     for t in self.hierarchy.split(\"/\")]\n\n        for type, template in par_split:\n            parent = self._convert_to_entity(type, template)\n            self.parents.append(parent)\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/HieroPlayer/PlayerPresets.hrox",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE hieroXML>\n<hieroXML version=\"8\" revision=\"Unknown\" release=\"dev\" name=\"HieroPlayer\" hash=\"44cd31b0-04b5-4659-4be0-f0f1a3929f82\">\n <Project editable=\"1\" timedisplayformat=\"0\" samplerate=\"0/0\" sixteenBitLut=\"\" guid=\"{756dbdc7-b87f-4506-8465-1ce4055e7f8c}\" logLut=\"\" floatLut=\"\" buildTrackName=\"VFX\" nukeUseOCIO=\"0\" framerate=\"0/0\" redVideoDecodeMode=\"0\" starttimecode=\"0\" monitorLut=\"\" name=\"Tag Presets\" thumbnailLut=\"\" viewerLut=\"\" eightBitLut=\"\">\n  <items>\n   <RootBinProjectItem editable=\"1\" guid=\"{186e9601-0000-0000-186e-960100000000}\" name=\"Tags\">\n    <items>\n      <BinProjectItem editable=\"1\" guid=\"{06fe5252-d5d0-4f65-804a-55d8d10ed9b7}\" name=\"Status\">\n        <items>\n          <TagClassProjectItem editable=\"1\" guid=\"{821fc015-a2c7-44b4-ad7e-0623022e9657}\" name=\"Ready To Start\">\n            <TagClass editable=\"0\" objName=\"tag\" guid=\"217676a2-0470-4eb4-bef4-fa45ed1981fa\" visible=\"1\" icon=\"icons:status/TagReadyToStart.png\" name=\"Ready To Start\">\n              <sets>\n                <Set title=\"\" domainroot=\"tag\">\n                  <values>\n                    <StringValue default=\"1\" value=\"Ready To Start\" name=\"tag.label\"/>\n                  </values>\n                </Set>\n              </sets>\n            </TagClass>\n          </TagClassProjectItem>\n          <TagClassProjectItem editable=\"1\" guid=\"{41a29485-d6e2-48c1-98cb-338be50b4fd6}\" name=\"In Progress\">\n            <TagClass editable=\"0\" objName=\"tag\" guid=\"07ffe597-0d4b-4103-bc5b-c751364d076c\" visible=\"1\" icon=\"icons:status/TagInProgress.png\" name=\"In Progress\">\n              <sets>\n                <Set title=\"\" domainroot=\"tag\">\n                  <values>\n                    <StringValue default=\"1\" value=\"In Progress\" name=\"tag.label\"/>\n                  </values>\n                </Set>\n              </sets>\n            </TagClass>\n          </TagClassProjectItem>\n          <TagClassProjectItem editable=\"1\" guid=\"{e26e36fc-b6d8-49a5-8c7b-dd2cc4d49118}\" name=\"Awaiting Approval\">\n            <TagClass editable=\"0\" objName=\"tag\" guid=\"6ed2d474-a910-431e-a5ee-0a4750911a15\" visible=\"1\" icon=\"icons:status/TagAwaitingApproval.png\" name=\"Awaiting Approval\">\n              <sets>\n                <Set title=\"\" domainroot=\"tag\">\n                  <values>\n                    <StringValue default=\"1\" value=\"Awaiting Approval\" name=\"tag.label\"/>\n                  </values>\n                </Set>\n              </sets>\n            </TagClass>\n          </TagClassProjectItem>\n          <TagClassProjectItem editable=\"1\" guid=\"{9899bf01-0000-0000-9899-bf0100000000}\" name=\"Approved\">\n            <TagClass editable=\"0\" objName=\"tag\" guid=\"9640cfe3-53b0-481d-9d77-2ede61d1ea11\" visible=\"1\" icon=\"icons:status/TagApproved.png\" name=\"Approved\">\n              <sets>\n                <Set title=\"\" domainroot=\"tag\">\n                  <values>\n                    <StringValue default=\"1\" value=\"Approved\" name=\"tag.label\"/>\n                  </values>\n                </Set>\n              </sets>\n            </TagClass>\n          </TagClassProjectItem>\n          <TagClassProjectItem editable=\"1\" guid=\"{7894bf01-0000-0000-7894-bf0100000000}\" name=\"Unapproved\">\n            <TagClass editable=\"0\" objName=\"tag\" guid=\"2aa67ad4-4b83-48f3-af72-84ce994722a6\" visible=\"1\" icon=\"icons:status/TagUnapproved.png\" name=\"Unapproved\">\n              <sets>\n                <Set title=\"\" domainroot=\"tag\">\n                  <values>\n                    <StringValue default=\"1\" value=\"Unapproved\" name=\"tag.label\"/>\n                  </values>\n                </Set>\n              </sets>\n            </TagClass>\n          </TagClassProjectItem>\n          <TagClassProjectItem editable=\"1\" guid=\"{c70be73e-d320-4f42-afca-5a7bb10854bd}\" name=\"Final\">\n            <TagClass editable=\"0\" objName=\"tag\" guid=\"0d336316-9190-4792-b4cf-7a16d3b07f78\" visible=\"1\" icon=\"icons:status/TagFinal.png\" name=\"Final\">\n              <sets>\n                <Set title=\"\" domainroot=\"tag\">\n                  <values>\n                    <StringValue default=\"1\" value=\"Final\" name=\"tag.label\"/>\n                  </values>\n                </Set>\n              </sets>\n            </TagClass>\n          </TagClassProjectItem>\n          <TagClassProjectItem editable=\"1\" guid=\"{a19dd121-9ca5-4f5b-be99-e82dda08cbc3}\" name=\"Omitted\">\n            <TagClass editable=\"0\" objName=\"tag\" guid=\"1d40bf8d-42ca-4df7-83a3-020f69fcc8df\" visible=\"1\" icon=\"icons:status/TagOmitted.png\" name=\"Omitted\">\n              <sets>\n                <Set title=\"\" domainroot=\"tag\">\n                  <values>\n                    <StringValue default=\"1\" value=\"Omitted\" name=\"tag.label\"/>\n                  </values>\n                </Set>\n              </sets>\n            </TagClass>\n          </TagClassProjectItem>\n          <TagClassProjectItem editable=\"1\" guid=\"{619c2968-cf48-4549-ac64-f81180adbc1c}\" name=\"On Hold\">\n            <TagClass editable=\"0\" objName=\"tag\" guid=\"c64e5eba-466e-43de-bd73-262815008859\" visible=\"1\" icon=\"icons:status/TagOnHold.png\" name=\"On Hold\">\n              <sets>\n                <Set title=\"\" domainroot=\"tag\">\n                  <values>\n                    <StringValue default=\"1\" value=\"On Hold\" name=\"tag.label\"/>\n                  </values>\n                </Set>\n              </sets>\n            </TagClass>\n          </TagClassProjectItem>\n          <TagClassProjectItem editable=\"1\" guid=\"{0adc90d1-8477-41b3-a489-274f5a50cdde}\" name=\"Blocked\">\n            <TagClass editable=\"0\" objName=\"tag\" guid=\"ffefb61a-b9e7-481d-b16d-a78b4bee4f8e\" visible=\"1\" icon=\"icons:status/TagBlocked.png\" name=\"Blocked\">\n              <sets>\n                <Set title=\"\" domainroot=\"tag\">\n                  <values>\n                    <StringValue default=\"1\" value=\"Blocked\" name=\"tag.label\"/>\n                  </values>\n                </Set>\n              </sets>\n            </TagClass>\n          </TagClassProjectItem>\n        </items>\n        <BinViewType>2</BinViewType>\n        <BinViewZoom>70</BinViewZoom>\n      </BinProjectItem>\n     <BinProjectItem editable=\"1\" guid=\"{e889bf01-0000-0000-e889-bf0100000000}\" name=\"Review\">\n      <items>\n       <TagClassProjectItem editable=\"1\" guid=\"{9d3c2215-6dd8-43bf-a53f-2949c77e7ee9}\" name=\"Note\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"4767ef41-e6dc-435c-8e21-4f6f86bfcabb\" visible=\"1\" icon=\"icons:TagNote.png\" name=\"Note\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Note\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{550327bf-f862-4872-ab73-b87bd0ad82d9}\" name=\"Comment\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"f83ca32f-0619-4ccc-bd84-ef774c0e5df2\" visible=\"1\" icon=\"icons:TagComment.png\" name=\"Comment\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Comment\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n      </items>\n      <BinViewType>2</BinViewType>\n      <BinViewZoom>70</BinViewZoom>\n     </BinProjectItem>\n     <BinProjectItem editable=\"1\" guid=\"{e5c428a4-2ffe-4367-a880-15c26988e269}\" name=\"Markers\">\n      <items>\n       <TagClassProjectItem editable=\"1\" guid=\"{1849bf01-0000-0000-1849-bf0100000000}\" name=\"Green\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"4f91d465-a0bf-4c50-95ac-58bf55306f50\" visible=\"1\" icon=\"icons:TagGreen.png\" name=\"Green\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Green\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{e8a5bf01-0000-0000-e8a5-bf0100000000}\" name=\"Red\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"68982d77-6a45-4dc8-a3bb-5f445f6b281f\" visible=\"1\" icon=\"icons:TagRed.png\" name=\"Red\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Red\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{b85ebf01-0000-0000-b85e-bf0100000000}\" name=\"Magenta\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"dc3de087-df20-4542-9f7e-0dd1f5cde434\" visible=\"1\" icon=\"icons:TagMagenta.png\" name=\"Magenta\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Magenta\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{9859bf01-0000-0000-9859-bf0100000000}\" name=\"Cyan\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"db4d84ac-67cf-41a9-8075-ebc3a347c18b\" visible=\"1\" icon=\"icons:TagCyan.png\" name=\"Cyan\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Cyan\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{7854bf01-0000-0000-7854-bf0100000000}\" name=\"Yellow\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"a1f82277-1a6b-4303-a95d-258ff5a77f33\" visible=\"1\" icon=\"icons:TagYellow.png\" name=\"Yellow\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Yellow\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{b85ebf01-0000-0000-b85e-bf0200000000}\" name=\"Blue\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"d02a1da2-f309-4a1d-a491-453e895fe980\" visible=\"1\" icon=\"icons:TagBlue.png\" name=\"Blue\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Blue\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n      </items>\n      <BinViewType>2</BinViewType>\n      <BinViewZoom>70</BinViewZoom>\n     </BinProjectItem>\n     <BinProjectItem editable=\"1\" guid=\"{52162d8d-ed2d-48ce-9f3a-e556cae13d3f}\" name=\"Apps\">\n      <items>\n       <BinProjectItem editable=\"1\" guid=\"{8767ad13-2762-4859-ab89-dba2c444d9f2}\" name=\"The Foundry\">\n        <items>\n         <TagClassProjectItem editable=\"1\" guid=\"{1f514008-5243-4982-b520-eb522b37a6d3}\" name=\"Nuke\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"a6d95def-65fa-4758-9732-bcb4cc462c98\" visible=\"1\" icon=\"icons:TagNuke.png\" name=\"Nuke\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Nuke\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{dfb01ec6-ed4f-4695-ada5-2c2635fb3acc}\" name=\"Hiero\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"4ded0a25-4367-4aea-9576-89512fba846b\" visible=\"1\" icon=\"icons:TagHiero.png\" name=\"Hiero\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Hiero\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{ea7bfafa-83f1-43d6-adb3-05525928ed67}\" name=\"HieroPlayer\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"0abac1b9-20b3-48a4-886d-34c1279f6630\" visible=\"1\" icon=\"icons:TagHieroPlayer.png\" name=\"HieroPlayer\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"HieroPlayer\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{8080e60f-5e7c-4c1d-b4e5-acf4b92a03d8}\" name=\"Mari\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"9000af8b-8aaa-41ad-9a33-f24cad75997f\" visible=\"1\" icon=\"icons:TagMari.png\" name=\"Mari\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Mari\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{51234a8a-cc1a-4cd7-a471-ac5ebea66cee}\" name=\"Katana\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"cff05542-fb1f-4f33-9ad1-9f2783a813f6\" visible=\"1\" icon=\"icons:TagKatana.png\" name=\"Katana\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Katana\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{c7e6362b-3f2b-4dc8-ac0e-29c9742713b8}\" name=\"Modo\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"29237c37-0d5b-4ec6-bf84-86b4570f3615\" visible=\"1\" icon=\"icons:TagModo.png\" name=\"Modo\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Modo\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{fcdf4cb6-07de-41a6-afac-431d77627dd1}\" name=\"Ocula\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"75f0afad-7f25-4b70-980f-45d4d485da93\" visible=\"1\" icon=\"icons:TagOcula.png\" name=\"Ocula\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Ocula\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{02d76f70-8ceb-4c5a-b7e7-2fc4b4a2bcb8}\" name=\"Furnace\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"7cde78d0-8d20-462a-9b29-6f6262db7d19\" visible=\"1\" icon=\"icons:TagFurnace.png\" name=\"Furnace\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Furnace\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{0befcdac-1b3b-4d98-8c06-a10b26ff540e}\" name=\"Keylight\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"d5056bde-95a7-4a9d-b746-e16b64ceba69\" visible=\"1\" icon=\"icons:TagKeylight.png\" name=\"Keylight\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Keylight\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{1bb1507c-be35-4493-8dbc-35113592cdef}\" name=\"RollingShutter\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"5f7afa92-8ffa-48e4-909d-096a5d4c1778\" visible=\"1\" icon=\"icons:TagRollingShutter.png\" name=\"RollingShutter\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"RollingShutter\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{ff1028d0-3f74-4f3c-96f2-a4578db32191}\" name=\"Kronos\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"c9c35b16-054b-47ad-b430-2b70f45fa7e2\" visible=\"1\" icon=\"icons:TagKronos.png\" name=\"Kronos\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Kronos\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{260c07d2-6728-44d9-94bc-433a0ead7b02}\" name=\"CameraTracker\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"2aaf96c5-7e8e-40b4-867a-50a1504aa7ca\" visible=\"1\" icon=\"icons:TagCameraTracker.png\" name=\"CameraTracker\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"CameraTracker\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n        </items>\n        <BinViewType>2</BinViewType>\n        <BinViewZoom>70</BinViewZoom>\n       </BinProjectItem>\n      </items>\n      <BinViewType>2</BinViewType>\n      <BinViewZoom>70</BinViewZoom>\n     </BinProjectItem>\n     <BinProjectItem editable=\"1\" guid=\"{f22acfb0-f13b-4c05-9b3f-56657cf341a1}\" name=\"Shots\">\n      <items>\n       <TagClassProjectItem editable=\"1\" guid=\"{7861c001-0000-0000-7861-c00100000000}\" name=\"Blue Screen\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"9e2d6ccd-ff26-4e13-af4d-ec3aafe86c43\" visible=\"1\" icon=\"icons:TagBlueScreen.png\" name=\"Blue Screen\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Blue Screen\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{b866c001-0000-0000-b866-c00100000000}\" name=\"Green Screen\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"5ef1a3e5-cfa2-4b9e-827d-55046974c74f\" visible=\"1\" icon=\"icons:TagGreenScreen.png\" name=\"Green Screen\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Green Screen\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{b85ebf01-0000-0000-b85e-bf0300000000}\" name=\"Reference\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"d02a1da2-f309-4a1d-a491-453e895fe981\" visible=\"1\" icon=\"icons:Reference.png\" name=\"Reference\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Reference\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{5837c001-0000-0000-5837-c00100000000}\" name=\"Foreground\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"6e7b1408-7acb-47c3-b2f9-e554d64b9240\" visible=\"1\" icon=\"icons:TagForeground.png\" name=\"Foreground\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Foreground\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{983cc001-0000-0000-983c-c00100000000}\" name=\"Middle Ground\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"1f576520-cac4-4104-986e-553c74093f4e\" visible=\"1\" icon=\"icons:TagMiddleGround.png\" name=\"Middle Ground\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Middle Ground\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{d841c001-0000-0000-d841-c00100000000}\" name=\"Background\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"a106608b-9e05-4e84-af97-84615f90a2d3\" visible=\"1\" icon=\"icons:TagBackground.png\" name=\"Background\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Background\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n      </items>\n      <BinViewType>2</BinViewType>\n      <BinViewZoom>70</BinViewZoom>\n     </BinProjectItem>\n     <BinProjectItem editable=\"1\" guid=\"{e8eebf01-0000-0000-e8ee-bf0100000000}\" name=\"Views\">\n      <items>\n       <TagClassProjectItem editable=\"1\" guid=\"{68719601-0000-0000-6871-960100000000}\" name=\"Left\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"6f31becd-2cf4-4272-9f2d-cc1f932df88f\" visible=\"1\" icon=\"icons:TagViewLeft.png\" name=\"Left\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Left\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{88769601-0000-0000-8876-960100000000}\" name=\"Right\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"59c12c72-a67e-4427-b1bc-57ffdf670114\" visible=\"1\" icon=\"icons:TagViewRight.png\" name=\"Right\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Right\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n       <TagClassProjectItem editable=\"1\" guid=\"{c888bf01-0000-0000-c888-bf0100000000}\" name=\"Centre\">\n        <TagClass editable=\"0\" objName=\"tag\" guid=\"d3006675-82d6-4f35-bdb3-97df23d721ec\" visible=\"1\" icon=\"icons:TagViewCentre.png\" name=\"Centre\">\n         <sets>\n          <Set title=\"\" domainroot=\"tag\">\n           <values>\n            <StringValue default=\"1\" value=\"Centre\" name=\"tag.label\"/>\n           </values>\n          </Set>\n         </sets>\n        </TagClass>\n       </TagClassProjectItem>\n      </items>\n      <BinViewType>2</BinViewType>\n      <BinViewZoom>70</BinViewZoom>\n     </BinProjectItem>\n     <BinProjectItem editable=\"1\" guid=\"{74ebcb34-609c-405a-b666-624d3e91dbc7}\" name=\"Flags\">\n      <items>\n       <BinProjectItem editable=\"1\" guid=\"{33a344d5-7877-4e38-8e41-fffaf69ff5ad}\" name=\"S. America\">\n        <items>\n         <TagClassProjectItem editable=\"1\" guid=\"{299f9661-3732-4983-a72a-7374dbfb0a8b}\" name=\"Argentina\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"7270d962-2fa0-4c43-8a34-2fc443cb69ee\" visible=\"1\" icon=\"icons:flags/TagArgentina.png\" name=\"Argentina\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Argentina\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{f0f0e2a2-b466-4c02-aa4b-0a5de63f88b3}\" name=\"Brazil\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"5d0c767a-c93c-480d-8357-99bf7775ad85\" visible=\"1\" icon=\"icons:flags/TagBrazil.png\" name=\"Brazil\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Brazil\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{2868d074-c88b-4377-ac15-22c3d46cf51c}\" name=\"Colombia\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"22a860a1-0363-41b5-b8eb-2869c0744e62\" visible=\"1\" icon=\"icons:flags/TagColombia.png\" name=\"Colombia\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Colombia\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n        </items>\n        <BinViewType>2</BinViewType>\n        <BinViewZoom>70</BinViewZoom>\n       </BinProjectItem>\n       <BinProjectItem editable=\"1\" guid=\"{3a41945a-d1d7-4c49-877b-cedcd5ee3823}\" name=\"N. America\">\n        <items>\n         <TagClassProjectItem editable=\"1\" guid=\"{6acd2100-a632-4cbc-8405-8178b553248e}\" name=\"Canada\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"749d15ed-fb67-49b0-bf3b-577642bda402\" visible=\"1\" icon=\"icons:flags/TagCanada.png\" name=\"Canada\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Canada\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{e4c93ca1-faf7-4643-abc5-5bcbdde54572}\" name=\"Mexico\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"ce9fb9a1-8086-45c3-8548-e65946162fed\" visible=\"1\" icon=\"icons:flags/TagMexico.png\" name=\"Mexico\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Mexico\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{608baf8a-714f-4559-a2f0-c52e9b2fc7e3}\" name=\"USA\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"258442b7-fad2-4f17-9a7a-6f8ea2cd66eb\" visible=\"1\" icon=\"icons:flags/TagUSA.png\" name=\"USA\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"USA\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n        </items>\n        <BinViewType>2</BinViewType>\n        <BinViewZoom>70</BinViewZoom>\n       </BinProjectItem>\n       <BinProjectItem editable=\"1\" guid=\"{7fcab974-d1e5-4aef-a01c-ec107c1ac23a}\" name=\"Europe\">\n        <items>\n         <TagClassProjectItem editable=\"1\" guid=\"{ee0f384d-1a16-4d83-973c-11604aa2696c}\" name=\"Austria\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"bb724581-9409-4363-a4fa-f41615168984\" visible=\"1\" icon=\"icons:flags/TagAustria.png\" name=\"Austria\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Austria\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{58dbaa69-af09-4110-b5bc-6ecaed9c45d5}\" name=\"Belgium\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"2dca8546-f494-4f61-9309-65af74529740\" visible=\"1\" icon=\"icons:flags/TagBelgium.png\" name=\"Belgium\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Belgium\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{89d74048-9b93-4993-b975-e2fdcb514863}\" name=\"Czech Republic\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"9a090db8-a25c-45a9-a7ed-62c0101dae32\" visible=\"1\" icon=\"icons:flags/TagCzech-Republic.png\" name=\"Czech Republic\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Czech Republic\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{71857b8a-cd55-4108-aaec-ab7e6fa4c6c7}\" name=\"Denmark\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"3b498546-5a77-44af-bf40-fef39eca14c8\" visible=\"1\" icon=\"icons:flags/TagDenmark.png\" name=\"Denmark\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Denmark\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{0cef1ec0-487a-4101-ade3-a690794b8e3c}\" name=\"England\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"9f63839c-dfa4-4896-b806-7397c3efcdb2\" visible=\"1\" icon=\"icons:flags/TagEngland.png\" name=\"England\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"England\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{ebc22169-7f68-419e-9093-ab72bc3a153f}\" name=\"Finland\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"11ea12ca-caf2-4ce4-8365-97f7e170ca35\" visible=\"1\" icon=\"icons:flags/TagFinland.png\" name=\"Finland\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Finland\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{d19f126b-7c4c-4409-9b97-f262b075763a}\" name=\"France\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"ff7f00ee-18e1-45e5-ba37-8ad59acafd24\" visible=\"1\" icon=\"icons:flags/TagFrance.png\" name=\"France\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"France\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{6fab8051-4907-4783-9e74-3968e9be582d}\" name=\"Germany\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"19c173f0-5e2a-436d-b45e-7772d8d2cbd5\" visible=\"1\" icon=\"icons:flags/TagGermany.png\" name=\"Germany\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Germany\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{f5cf7f41-6597-4e31-b2b1-21384c95fe44}\" name=\"Greece\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"2cfa41a6-689e-47e0-a79d-109ea4188505\" visible=\"1\" icon=\"icons:flags/TagGreece.png\" name=\"Greece\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Greece\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{de646fed-0871-4d1c-806f-744e0a6638c3}\" name=\"Ireland\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"3d18efb1-5c01-44ca-98d7-4a27f4575f10\" visible=\"1\" icon=\"icons:flags/TagIreland.png\" name=\"Ireland\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Ireland\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{a91bc6e8-2d7f-4e51-8097-f6a63171f0c0}\" name=\"Israel\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"9fc199cf-c4a3-4401-8c43-f1d885f9f542\" visible=\"1\" icon=\"icons:flags/TagIsrael.png\" name=\"Israel\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Israel\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{d8093dc3-1a6e-4f9c-a287-eba05d23470f}\" name=\"Italy\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"58d8483a-f5c3-4337-9b4f-1a431b4a1979\" visible=\"1\" icon=\"icons:flags/TagItaly.png\" name=\"Italy\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Italy\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{41868fec-faa6-4d8d-bd6c-c204db9a65ec}\" name=\"Iceland\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"209f21c0-08d5-4132-b68c-9a466f39fb90\" visible=\"1\" icon=\"icons:flags/TagIceland.png\" name=\"Iceland\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Iceland\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{b853b1ea-a4a4-4545-8cab-e0bd9f9e5b7c}\" name=\"Netherlands\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"a9565124-c78a-4297-a9bc-4f6ee9ef1572\" visible=\"1\" icon=\"icons:flags/TagNetherlands.png\" name=\"Netherlands\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Netherlands\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{5978414f-b8d9-4a61-bc0a-d5c503519921}\" name=\"Norway\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"8e1d2ac6-59ca-457b-b718-e0a8b4c5eb41\" visible=\"1\" icon=\"icons:flags/TagNorway.png\" name=\"Norway\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Norway\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{2cf9ab6b-9e49-49f3-8308-8ca229b2ab6c}\" name=\"Poland\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"c1e06c95-f364-4f12-aeb6-d2d514d22bb9\" visible=\"1\" icon=\"icons:flags/TagPoland.png\" name=\"Poland\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Poland\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{beca25b7-1f07-4507-9623-8283a0646342}\" name=\"Portugal\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"a675017c-d159-44cd-b517-98659d45629e\" visible=\"1\" icon=\"icons:flags/TagPortugal.png\" name=\"Portugal\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Portugal\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{16affedd-a529-4415-b6b7-98ddd1ee585a}\" name=\"Russia\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"882baee0-6bfc-4477-be4f-b366c039abd0\" visible=\"1\" icon=\"icons:flags/TagRussia.png\" name=\"Russia\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Russia\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{c0d4fa38-6185-4bcf-b3c7-91dc81732dd0}\" name=\"Switzerland\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"df04f5ed-a65e-4d04-98b9-1906ad0834ae\" visible=\"1\" icon=\"icons:flags/TagSwitzerland.png\" name=\"Switzerland\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Switzerland\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{e953fdd0-c682-474f-9672-1e3732d83ab2}\" name=\"Spain\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"43b5d01e-dc7f-4d5b-8173-c13e51617060\" visible=\"1\" icon=\"icons:flags/TagSpain.png\" name=\"Spain\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Spain\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{6b8a27d6-65f0-4826-9eb5-4760b4d6b302}\" name=\"Sweden\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"ed0ca8fa-923a-451d-8abf-d16b61d5843f\" visible=\"1\" icon=\"icons:flags/TagSweden.png\" name=\"Sweden\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Sweden\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{f4afacd6-669b-4d1b-90a8-c717d092293d}\" name=\"Scotland\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"5cde5ade-6d70-4a6f-9eb8-348f0c7c7cb1\" visible=\"1\" icon=\"icons:flags/TagScotland.png\" name=\"Scotland\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Scotland\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{f404f9b0-be6e-4c89-af24-5f7d5cb7662d}\" name=\"UK\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"fd78e675-4da5-481d-b2c8-d72f7b1dda5a\" visible=\"1\" icon=\"icons:flags/TagUK.png\" name=\"UK\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"UK\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{254cd741-75e0-4d26-a9ac-aea3fad5592f}\" name=\"Wales\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"6b715954-64e8-41f1-b2d1-6df8c8398be0\" visible=\"1\" icon=\"icons:flags/TagWales.png\" name=\"Wales\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Wales\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n        </items>\n        <BinViewType>2</BinViewType>\n        <BinViewZoom>50</BinViewZoom>\n       </BinProjectItem>\n       <BinProjectItem editable=\"1\" guid=\"{cc94cc59-8625-4dbb-b392-3340f21e708a}\" name=\"Asia\">\n        <items>\n         <TagClassProjectItem editable=\"1\" guid=\"{737014a9-0827-412e-b74b-70ed50078623}\" name=\"China\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"1eaea40f-2d14-4438-aba5-99fdef84961a\" visible=\"1\" icon=\"icons:flags/TagChina.png\" name=\"China\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"China\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{8cf7a69a-606d-4140-8a6b-080a8937cf5e}\" name=\"Hong Kong\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"a864e1ba-7bfe-42cf-a7e9-41196c4126b1\" visible=\"1\" icon=\"icons:flags/TagHong-Kong.png\" name=\"Hong Kong\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Hong Kong\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{755a8411-ec0f-40a3-9284-9e990d5b637a}\" name=\"India\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"3e532d7d-b1be-4fc0-a56c-33a7355a85ae\" visible=\"1\" icon=\"icons:flags/TagIndia.png\" name=\"India\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"India\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{16c5dfc9-8db5-4e8a-a5ee-fc641379aae0}\" name=\"Japan\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"7bfd594d-ee5f-4b74-b33b-c49995c90752\" visible=\"1\" icon=\"icons:flags/TagJapan.png\" name=\"Japan\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Japan\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{f75a4de0-48f8-475e-b877-c569859015b0}\" name=\"Singapore\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"d431daaa-fa73-475c-8cc6-cb19c91e8f83\" visible=\"1\" icon=\"icons:flags/TagSingapore.png\" name=\"Singapore\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Singapore\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{449fc1b0-d66b-475b-833e-8177e7ed1c50}\" name=\"South Korea\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"bec90bdb-5794-426b-8754-f70371b02953\" visible=\"1\" icon=\"icons:flags/TagSouth-Korea.png\" name=\"South Korea\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"South Korea\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{08d37974-cb85-4dd3-8748-59ecb5bb3837}\" name=\"Thailand\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"0fe04ade-bd45-4ce9-9f3c-fc78968bccf1\" visible=\"1\" icon=\"icons:flags/TagThailand.png\" name=\"Thailand\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Thailand\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{ed6a8608-76e5-4888-af64-239ff8f7011b}\" name=\"UAE\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"d4de844b-018b-4197-9e9b-a0ce14ba8c23\" visible=\"1\" icon=\"icons:flags/TagUAE.png\" name=\"UAE\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"UAE\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n        </items>\n        <BinViewType>2</BinViewType>\n        <BinViewZoom>70</BinViewZoom>\n       </BinProjectItem>\n       <BinProjectItem editable=\"1\" guid=\"{008da0af-776a-40d3-ab3b-f72ced61bfd9}\" name=\"Africa\">\n        <items>\n         <TagClassProjectItem editable=\"1\" guid=\"{22c5c3c4-c97a-4134-9655-c75689ef9ac7}\" name=\"Egypt\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"0d5ee434-29ce-4a0c-9b2b-fef218001d61\" visible=\"1\" icon=\"icons:flags/TagEgypt.png\" name=\"Egypt\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Egypt\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{c0c2f347-7078-4e90-bd08-7dab52b518b4}\" name=\"South Africa\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"17c0b967-e031-4484-aa94-6bbc8dca1058\" visible=\"1\" icon=\"icons:flags/TagSouth-Africa.png\" name=\"South Africa\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"South Africa\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n        </items>\n        <BinViewType>2</BinViewType>\n        <BinViewZoom>70</BinViewZoom>\n       </BinProjectItem>\n       <BinProjectItem editable=\"1\" guid=\"{47af494c-10fa-4f9c-ad99-2ffe3ebec1bd}\" name=\"Australasia\">\n        <items>\n         <TagClassProjectItem editable=\"1\" guid=\"{34958ffd-3298-4c06-a2db-dbde6f30d4e6}\" name=\"Australia\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"a4cbb4f6-95e1-4f5c-913f-ab8eb7db4931\" visible=\"1\" icon=\"icons:flags/TagAustralia.png\" name=\"Australia\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Australia\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{f844a7c3-2207-4e9d-875b-3aa72476aa67}\" name=\"New Zealand\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"aeb22b63-8819-4fea-8ecb-93a798eacacd\" visible=\"1\" icon=\"icons:flags/TagNew-Zealand.png\" name=\"New Zealand\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"New Zealand\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n        </items>\n        <BinViewType>2</BinViewType>\n        <BinViewZoom>70</BinViewZoom>\n       </BinProjectItem>\n      </items>\n      <BinViewType>2</BinViewType>\n      <BinViewZoom>70</BinViewZoom>\n     </BinProjectItem>\n     <BinProjectItem editable=\"1\" guid=\"{033ca6e3-ef3f-4924-9c78-06e71ae65e00}\" name=\"Other\">\n      <items>\n       <BinProjectItem editable=\"1\" guid=\"{a88a4705-2843-4877-a540-9a401889fcd3}\" name=\"Lens\">\n        <items>\n         <TagClassProjectItem editable=\"1\" guid=\"{1847c001-0000-0000-1847-c00100000000}\" name=\"f/1.4\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"2e14f447-b151-4deb-825e-87f4ea056107\" visible=\"1\" icon=\"icons:Tagf1-4.png\" name=\"f/1.4\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"f/1.4\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{d417fc48-9f11-4028-917a-98ba8ef8018e}\" name=\"f/2.8\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"b8d98c3f-fb6f-460d-8d70-a9bac9603076\" visible=\"1\" icon=\"icons:Tagf2-8.png\" name=\"f/2.8\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"f/2.8\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{584dc001-0000-0000-584d-c00100000000}\" name=\"f/4\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"5e24dde6-ee12-4bd8-95c0-62939595d5e8\" visible=\"1\" icon=\"icons:Tagf4.png\" name=\"f/4\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"f/4\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{9852c001-0000-0000-9852-c00100000000}\" name=\"f/11\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"cadec7d8-7ec1-4d74-a46a-9da77d7bfd4d\" visible=\"1\" icon=\"icons:Tagf11.png\" name=\"f/11\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"f/11\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{6857c001-0000-0000-6857-c00100000000}\" name=\"f/22\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"7a355313-1180-451e-9270-77cfde1e2fb3\" visible=\"1\" icon=\"icons:Tagf22.png\" name=\"f/22\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"f/22\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n        </items>\n        <BinViewType>2</BinViewType>\n        <BinViewZoom>70</BinViewZoom>\n       </BinProjectItem>\n       <BinProjectItem editable=\"1\" guid=\"{b1dec61a-0909-42bd-9925-fb8859cec494}\" name=\"Weather\">\n        <items>\n         <TagClassProjectItem editable=\"1\" guid=\"{63ea7999-1e6f-48a5-ab77-45f3037e552a}\" name=\"Cloudy\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"5006b758-6c0c-4526-93cd-1ad68aed000d\" visible=\"1\" icon=\"icons:TagCloudy.png\" name=\"Cloudy\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Cloudy\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{c5ea8145-b30f-431b-811c-7ac3bfae40c9}\" name=\"Rain\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"9a1bc7d6-4ded-4b18-9896-b15768f1065c\" visible=\"1\" icon=\"icons:TagRain.png\" name=\"Rain\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Rain\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{a0e5c455-5684-4f04-bc74-d31110860ccb}\" name=\"Snow\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"55aa4cf7-9fc5-471d-8f7e-abc85d5559e2\" visible=\"1\" icon=\"icons:TagSnow.png\" name=\"Snow\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Snow\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n         <TagClassProjectItem editable=\"1\" guid=\"{50c323d3-b7e3-431f-a1bf-c4720b26aaee}\" name=\"Sunny\">\n          <TagClass editable=\"0\" objName=\"tag\" guid=\"94869ef8-6ae8-4fb1-8d34-bec05fe677c0\" visible=\"1\" icon=\"icons:TagSunny.png\" name=\"Sunny\">\n           <sets>\n            <Set title=\"\" domainroot=\"tag\">\n             <values>\n              <StringValue default=\"1\" value=\"Sunny\" name=\"tag.label\"/>\n             </values>\n            </Set>\n           </sets>\n          </TagClass>\n         </TagClassProjectItem>\n        </items>\n        <BinViewType>2</BinViewType>\n        <BinViewZoom>70</BinViewZoom>\n       </BinProjectItem>\n      </items>\n      <BinViewType>2</BinViewType>\n      <BinViewZoom>70</BinViewZoom>\n     </BinProjectItem>\n    </items>\n    <BinViewType>2</BinViewType>\n    <BinViewZoom>70</BinViewZoom>\n    <AllowedItems>17</AllowedItems>\n   </RootBinProjectItem>\n   <RootBinProjectItem editable=\"1\" guid=\"{581d4603-0000-0000-581d-460300000000}\" name=\"Sequences\">\n    <BinViewType>126935040</BinViewType>\n    <BinViewZoom>70</BinViewZoom>\n    <AllowedItems>-1</AllowedItems>\n   </RootBinProjectItem>\n  </items>\n  <BinViewType>2</BinViewType>\n  <BinViewZoom>70</BinViewZoom>\n  <AllowedItems>2</AllowedItems>\n  <RootBinProjectItem objName=\"sequencesBin\" guid=\"{581d4603-0000-0000-581d-460300000000}\" link=\"internal\"/>\n  <RootBinProjectItem objName=\"tagsBin\" guid=\"{186e9601-0000-0000-186e-960100000000}\" link=\"internal\"/>\n  <MediaFormatValue default=\"0\" objName=\"outputformat\" value=\"1,[ 0, 0, 0, 0],[ 0, 0, 0, 0],\" name=\"\"/>\n </Project>\n <UIState/>\n</hieroXML>\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/Startup/SpreadsheetExport.py",
    "content": "# This action adds itself to the Spreadsheet View context menu allowing the contents of the Spreadsheet be exported as a CSV file.\n# Usage: Right-click in Spreadsheet > \"Export as .CSV\"\n# Note: This only prints the text data that is visible in the active Spreadsheet View.\n# If you've filtered text, only the visible text will be printed to the CSV file\n# Usage: Copy to ~/.hiero/Python/StartupUI\nimport hiero.core.events\nimport hiero.ui\nimport os, csv\ntry:\n    from PySide.QtGui import *\n    from PySide.QtCore import *\nexcept:\n    from PySide2.QtGui import *\n    from PySide2.QtWidgets import *\n    from PySide2.QtCore import *\n\n\n### Magic Widget Finding Methods - This stuff crawls all the PySide widgets, looking for an answer\ndef findWidget(w):\n    global foundryWidgets\n    if \"Foundry\" in w.metaObject().className():\n        foundryWidgets += [w]\n\n    for c in w.children():\n        findWidget(c)\n    return foundryWidgets\n\n\ndef getFoundryWidgetsWithClassName(filter=None):\n    global foundryWidgets\n    foundryWidgets = []\n    widgets = []\n    app = QApplication.instance()\n    for w in app.topLevelWidgets():\n        findWidget(w)\n\n    filteredWidgets = foundryWidgets\n    if filter:\n        filteredWidgets = []\n        for widget in foundryWidgets:\n            if filter in widget.metaObject().className():\n                filteredWidgets += [widget]\n    return filteredWidgets\n\n\n# When right click, get the Sequence Name\ndef activeSpreadsheetTreeView():\n    \"\"\"\n  Does some PySide widget Magic to detect the Active Spreadsheet TreeView.\n  \"\"\"\n    spreadsheetViews = getFoundryWidgetsWithClassName(\n        filter=\"SpreadsheetTreeView\")\n    for spreadSheet in spreadsheetViews:\n        if spreadSheet.hasFocus():\n            activeSpreadSheet = spreadSheet\n            return activeSpreadSheet\n    return None\n\n\n#### Adds \"Export .CSV\" action to the Spreadsheet Context menu ####\nclass SpreadsheetExportCSVAction(QAction):\n    def __init__(self):\n        QAction.__init__(self, \"Export as .CSV\", None)\n        self.triggered.connect(self.exportCSVFromActiveSpreadsheetView)\n        hiero.core.events.registerInterest(\"kShowContextMenu/kSpreadsheet\",\n                                           self.eventHandler)\n        self.setIcon(QIcon(\"icons:FBGridView.png\"))\n\n    def eventHandler(self, event):\n        # Insert the action to the Export CSV menu\n        event.menu.addAction(self)\n\n    #### The guts!.. Writes a CSV file from a Sequence Object ####\n    def exportCSVFromActiveSpreadsheetView(self):\n\n        # Get the active QTreeView from the active Spreadsheet\n        spreadsheetTreeView = activeSpreadsheetTreeView()\n\n        if not spreadsheetTreeView:\n            return \"Unable to detect the active TreeView.\"\n        seq = hiero.ui.activeView().sequence()\n        if not seq:\n            print(\"Unable to detect the active Sequence from the activeView.\")\n            return\n\n        # The data model of the QTreeView\n        model = spreadsheetTreeView.model()\n\n        csvSavePath = os.path.join(QDir.homePath(), \"Desktop\",\n                                   seq.name() + \".csv\")\n        savePath, filter = QFileDialog.getSaveFileName(\n            None,\n            caption=\"Export Spreadsheet to .CSV as...\",\n            dir=csvSavePath,\n            filter=\"*.csv\")\n        print(\"Saving To: {}\".format(savePath))\n\n        # Saving was cancelled...\n        if len(savePath) == 0:\n            return\n\n        # Get the Visible Header Columns from the QTreeView\n\n        #csvHeader = [\"Event\", \"Status\", \"Shot Name\", \"Reel\",  \"Track\", \"Speed\", \"Src In\", \"Src Out\",\"Src Duration\", \"Dst In\", \"Dst Out\", \"Dst Duration\", \"Clip\", \"Clip Media\"]\n\n        # Get a CSV writer object\n        f = open(savePath, \"w\")\n        csvWriter = csv.writer(\n            f, delimiter=',', quotechar=\"|\", quoting=csv.QUOTE_MINIMAL)\n\n        # This is a list of the Column titles\n        csvHeader = []\n\n        for col in range(0, model.columnCount()):\n            if not spreadsheetTreeView.isColumnHidden(col):\n                csvHeader += [model.headerData(col, Qt.Horizontal)]\n\n        # Write the Header row to the CSV file\n        csvWriter.writerow(csvHeader)\n\n        # Go through each row/column and print\n        for row in range(model.rowCount()):\n            row_data = []\n            for col in range(model.columnCount()):\n                if not spreadsheetTreeView.isColumnHidden(col):\n                    row_data.append(\n                        model.index(row, col, QModelIndex()).data(\n                            Qt.DisplayRole))\n\n            # Write row to CSV file...\n            csvWriter.writerow(row_data)\n\n        f.close()\n        # Conveniently show the CSV file in the native file browser...\n        QDesktopServices.openUrl(\n            QUrl('file:///%s' % (os.path.dirname(savePath))))\n\n\n# Add the action...\ncsvActions = SpreadsheetExportCSVAction()\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/Startup/Startup.py",
    "content": "import traceback\n\n# activate hiero from pype\nfrom openpype.pipeline import install_host\nimport openpype.hosts.hiero.api as phiero\ninstall_host(phiero)\n\ntry:\n    __import__(\"openpype.hosts.hiero.api\")\n    __import__(\"pyblish\")\n\nexcept ImportError as e:\n    print(traceback.format_exc())\n    print(\"pyblish: Could not load integration: %s \" % e)\n\nelse:\n    # Setup integration\n    import openpype.hosts.hiero.api as phiero\n    phiero.lib.setup()\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/OTIOExportTask.py",
    "content": "# -*- coding: utf-8 -*-\n\n__author__ = \"Daniel Flehner Heen\"\n__credits__ = [\"Jakub Jezek\", \"Daniel Flehner Heen\"]\n\nimport os\nimport hiero.core\nfrom hiero.core import util\n\nimport opentimelineio as otio\nfrom openpype.hosts.hiero.api.otio import hiero_export\n\nclass OTIOExportTask(hiero.core.TaskBase):\n\n    def __init__(self, initDict):\n        \"\"\"Initialize\"\"\"\n        hiero.core.TaskBase.__init__(self, initDict)\n        self.otio_timeline = None\n\n    def name(self):\n        return str(type(self))\n\n    def startTask(self):\n        self.otio_timeline = hiero_export.create_otio_timeline()\n\n    def taskStep(self):\n        return False\n\n    def finishTask(self):\n        try:\n            exportPath = self.resolvedExportPath()\n\n            # Check file extension\n            if not exportPath.lower().endswith(\".otio\"):\n                exportPath += \".otio\"\n\n            # check export root exists\n            dirname = os.path.dirname(exportPath)\n            util.filesystem.makeDirs(dirname)\n\n            # write otio file\n            hiero_export.write_to_file(self.otio_timeline, exportPath)\n\n        # Catch all exceptions and log error\n        except Exception as e:\n            self.setError(\"failed to write file {f}\\n{e}\".format(\n                f=exportPath,\n                e=e)\n            )\n\n        hiero.core.TaskBase.finishTask(self)\n\n    def forcedAbort(self):\n        pass\n\n\nclass OTIOExportPreset(hiero.core.TaskPresetBase):\n    def __init__(self, name, properties):\n        \"\"\"Initialise presets to default values\"\"\"\n        hiero.core.TaskPresetBase.__init__(self, OTIOExportTask, name)\n\n        self.properties()[\"includeTags\"] = hiero_export.include_tags = True\n        self.properties().update(properties)\n\n    def supportedItems(self):\n        return hiero.core.TaskPresetBase.kSequence\n\n    def addCustomResolveEntries(self, resolver):\n        resolver.addResolver(\n            \"{ext}\",\n            \"Extension of the file to be output\",\n            lambda keyword, task: \"otio\"\n        )\n\n    def supportsAudio(self):\n        return True\n\n\nhiero.core.taskRegistry.registerTask(OTIOExportPreset, OTIOExportTask)\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/OTIOExportUI.py",
    "content": "# -*- coding: utf-8 -*-\n\n__author__ = \"Daniel Flehner Heen\"\n__credits__ = [\"Jakub Jezek\", \"Daniel Flehner Heen\"]\n\nimport hiero.ui\nfrom .OTIOExportTask import (\n    OTIOExportTask,\n    OTIOExportPreset\n)\n\ntry:\n    # Hiero >= 11.x\n    from PySide2 import QtCore\n    from PySide2.QtWidgets import QCheckBox\n    from hiero.ui.FnTaskUIFormLayout import TaskUIFormLayout as FormLayout\n\nexcept ImportError:\n    # Hiero <= 10.x\n    from PySide import QtCore  # lint:ok\n    from PySide.QtGui import QCheckBox, QFormLayout  # lint:ok\n\n    FormLayout = QFormLayout  # lint:ok\n\nfrom openpype.hosts.hiero.api.otio import hiero_export\n\nclass OTIOExportUI(hiero.ui.TaskUIBase):\n    def __init__(self, preset):\n        \"\"\"Initialize\"\"\"\n        hiero.ui.TaskUIBase.__init__(\n            self,\n            OTIOExportTask,\n            preset,\n            \"OTIO Exporter\"\n        )\n\n    def includeMarkersCheckboxChanged(self, state):\n        # Slot to handle change of checkbox state\n        hiero_export.include_tags = state == QtCore.Qt.Checked\n\n    def populateUI(self, widget, exportTemplate):\n        layout = widget.layout()\n        formLayout = FormLayout()\n\n        # Hiero ~= 10.0v4\n        if layout is None:\n            layout = formLayout\n            widget.setLayout(layout)\n\n        else:\n            layout.addLayout(formLayout)\n\n        # Checkboxes for whether the OTIO should contain markers or not\n        self.includeMarkersCheckbox = QCheckBox()\n        self.includeMarkersCheckbox.setToolTip(\n            \"Enable to include Tags as markers in the exported OTIO file.\"\n        )\n        self.includeMarkersCheckbox.setCheckState(QtCore.Qt.Unchecked)\n\n        if self._preset.properties()[\"includeTags\"]:\n            self.includeMarkersCheckbox.setCheckState(QtCore.Qt.Checked)\n\n        self.includeMarkersCheckbox.stateChanged.connect(\n            self.includeMarkersCheckboxChanged\n        )\n\n        # Add Checkbox to layout\n        formLayout.addRow(\"Include Tags:\", self.includeMarkersCheckbox)\n\n\nhiero.ui.taskUIRegistry.registerTaskUI(\n    OTIOExportPreset,\n    OTIOExportUI\n)\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/__init__.py",
    "content": "from .OTIOExportTask import OTIOExportTask\nfrom .OTIOExportUI import OTIOExportUI\n\n__all__ = [\n    \"OTIOExportTask\",\n    \"OTIOExportUI\"\n]\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/Startup/project_helpers.py",
    "content": "try:\n    from PySide.QtGui import *\n    from PySide.QtCore import *\nexcept:\n    from PySide2.QtGui import *\n    from PySide2.QtWidgets import *\n    from PySide2.QtCore import *\n\nfrom hiero.core.util import uniquify, version_get, version_set\nimport hiero.core\nimport hiero.ui\nimport nuke\n\n# A globally variable for storing the current Project\ngTrackedActiveProject = None\n\n# This selection handler will track changes in items selected/deselected in the Bin/Timeline/Spreadsheet Views\n\n\ndef __trackActiveProjectHandler(event):\n    global gTrackedActiveProject\n    selection = event.sender.selection()\n    binSelection = selection\n    if len(binSelection) > 0 and hasattr(binSelection[0], \"project\"):\n        proj = binSelection[0].project()\n\n        # We only store this if its a valid, active User Project\n        if proj in hiero.core.projects(hiero.core.Project.kUserProjects):\n            gTrackedActiveProject = proj\n\n\nhiero.core.events.registerInterest(\n    \"kSelectionChanged/kBin\", __trackActiveProjectHandler)\nhiero.core.events.registerInterest(\n    \"kSelectionChanged/kTimeline\", __trackActiveProjectHandler)\nhiero.core.events.registerInterest(\n    \"kSelectionChanged/Spreadsheet\", __trackActiveProjectHandler)\n\n\ndef activeProject():\n    \"\"\"hiero.ui.activeProject() -> returns the current Project\n\n    Note: There is not technically a notion of a \"active\" Project in Hiero/NukeStudio, as it is a multi-project App.\n    This method determines what is \"active\" by going down the following rules...\n\n    # 1 - If the current Viewer (hiero.ui.currentViewer) contains a Clip or Sequence, this item is assumed to give the active Project\n    # 2 - If nothing is currently in the Viewer, look to the active View, determine project from active selection\n    # 3 - If no current selection can be determined, fall back to a globally tracked last selection from trackActiveProjectHandler\n    # 4 - If all those rules fail, fall back to the last project in the list of hiero.core.projects()\n\n    @return: hiero.core.Project\"\"\"\n    global gTrackedActiveProject\n    activeProject = None\n\n    # Case 1 : Look for what the current Viewr tells us - this might not be what we want, and relies on hiero.ui.currentViewer() being robust.\n    cv = hiero.ui.currentViewer().player().sequence()\n    if hasattr(cv, \"project\"):\n        activeProject = cv.project()\n    else:\n        # Case 2: We can't determine a project from the current Viewer, so try seeing what's selected in the activeView\n        # Note that currently, if you run activeProject from the Script Editor, the activeView is always None, so this will rarely get used!\n        activeView = hiero.ui.activeView()\n        if activeView:\n            # We can determine an active View.. see what's being worked with\n            selection = activeView.selection()\n\n            # Handle the case where nothing is selected in the active view\n            if len(selection) == 0:\n                # It's possible that there is no selection in a Timeline/Spreadsheet, but these views have \"sequence\" method, so try that...\n                if isinstance(hiero.ui.activeView(), (hiero.ui.TimelineEditor, hiero.ui.SpreadsheetView)):\n                    activeSequence = activeView.sequence()\n                    if hasattr(currentItem, \"project\"):\n                        activeProject = activeSequence.project()\n\n            # The active view has a selection... assume that the first item in the selection has the active Project\n            else:\n                currentItem = selection[0]\n                if hasattr(currentItem, \"project\"):\n                    activeProject = currentItem.project()\n\n    # Finally, Cases 3 and 4...\n    if not activeProject:\n        activeProjects = hiero.core.projects(hiero.core.Project.kUserProjects)\n        if gTrackedActiveProject in activeProjects:\n            activeProject = gTrackedActiveProject\n        else:\n            activeProject = activeProjects[-1]\n\n    return activeProject\n\n# Method to get all recent projects\n\n\ndef recentProjects():\n    \"\"\"hiero.core.recentProjects() -> Returns a list of paths to recently opened projects\n\n    Hiero stores up to 5 recent projects in uistate.ini with the [recentFile]/# key.\n\n    @return: list of paths to .hrox Projects\"\"\"\n\n    appSettings = hiero.core.ApplicationSettings()\n    recentProjects = []\n    for i in range(0, 5):\n        proj = appSettings.value('recentFile/%i' % i)\n        if len(proj) > 0:\n            recentProjects.append(proj)\n    return recentProjects\n\n# Method to get recent project by index\n\n\ndef recentProject(k=0):\n    \"\"\"hiero.core.recentProject(k) -> Returns the recent project path, specified by integer k (0-4)\n\n    @param: k (optional, default = 0) - an integer from 0-4, relating to the index of recent projects.\n\n    @return: hiero.core.Project\"\"\"\n\n    appSettings = hiero.core.ApplicationSettings()\n    proj = appSettings.value('recentFile/%i' % int(k), None)\n    return proj\n\n# Method to get open project by index\n\n\ndef openRecentProject(k=0):\n    \"\"\"hiero.core.openRecentProject(k) -> Opens the most the recent project as listed in the Open Recent list.\n\n    @param: k (optional, default = 0) - an integer from 0-4, relating to the index of recent projects.\n    @return: hiero.core.Project\"\"\"\n\n    appSettings = hiero.core.ApplicationSettings()\n    proj = appSettings.value('recentFile/%i' % int(k), None)\n    proj = hiero.core.openProject(proj)\n    return proj\n\n\n# Duck punch these methods into the relevant ui/core namespaces\nhiero.ui.activeProject = activeProject\nhiero.core.recentProjects = recentProjects\nhiero.core.recentProject = recentProject\nhiero.core.openRecentProject = openRecentProject\n\n\n# Method to Save a new Version of the activeHrox Project\nclass SaveAllProjects(QAction):\n\n    def __init__(self):\n        QAction.__init__(self, \"Save All Projects\", None)\n        self.triggered.connect(self.projectSaveAll)\n        hiero.core.events.registerInterest(\n            \"kShowContextMenu/kBin\", self.eventHandler)\n\n    def projectSaveAll(self):\n        allProjects = hiero.core.projects()\n        for proj in allProjects:\n            try:\n                proj.save()\n                print(\"Saved Project: {} to: {} \".format(\n                    proj.name(), proj.path()\n                ))\n            except:\n                print((\n                    \"Unable to save Project: {} to: {}. \"\n                    \"Check file permissions.\").format(\n                        proj.name(), proj.path()))\n\n    def eventHandler(self, event):\n        event.menu.addAction(self)\n\n# For projects with v# in the path name, saves out a new Project with v#+1\n\n\nclass SaveNewProjectVersion(QAction):\n\n    def __init__(self):\n        QAction.__init__(self, \"Save New Version...\", None)\n        self.triggered.connect(self.saveNewVersion)\n        hiero.core.events.registerInterest(\n            \"kShowContextMenu/kBin\", self.eventHandler)\n        self.selectedProjects = []\n\n    def saveNewVersion(self):\n        if len(self.selectedProjects) > 0:\n            projects = self.selectedProjects\n        else:\n            projects = [hiero.ui.activeProject()]\n\n        if len(projects) < 1:\n            return\n\n        for proj in projects:\n            oldName = proj.name()\n            path = proj.path()\n            v = None\n            prefix = None\n            try:\n                (prefix, v) = version_get(path, \"v\")\n            except ValueError as msg:\n                print(msg)\n\n            if (prefix is not None) and (v is not None):\n                v = int(v)\n                newPath = version_set(path, prefix, v, v + 1)\n                try:\n                    proj.saveAs(newPath)\n                    print(\"Saved new project version: {} to: {} \".format(\n                        oldName, newPath))\n                except:\n                    print((\n                        \"Unable to save Project: {}. Check file permissions.\"\n                    ).format(oldName))\n            else:\n                newPath = path.replace(\".hrox\", \"_v01.hrox\")\n                answer = nuke.ask(\n                    \"%s does not contain a version number.\\nDo you want to save as %s?\" % (proj, newPath))\n                if answer:\n                    try:\n                        proj.saveAs(newPath)\n                        print(\"Saved new project version: {} to: {} \".format(\n                            oldName, newPath))\n                    except:\n                        print((\n                            \"Unable to save Project: {}. Check file \"\n                            \"permissions.\").format(oldName))\n\n    def eventHandler(self, event):\n        self.selectedProjects = []\n        if hasattr(event.sender, \"selection\") and event.sender.selection() is not None and len(event.sender.selection()) != 0:\n            selection = event.sender.selection()\n            self.selectedProjects = uniquify(\n                [item.project() for item in selection])\n            event.menu.addAction(self)\n\n\n# Instantiate the actions\nsaveAllAct = SaveAllProjects()\nsaveNewAct = SaveNewProjectVersion()\n\nfileMenu = hiero.ui.findMenuAction(\"foundry.menu.file\")\nimportAct = hiero.ui.findMenuAction(\"foundry.project.importFiles\")\nhiero.ui.insertMenuAction(saveNewAct, fileMenu.menu(),\n                          before=\"Import File(s)...\")\nhiero.ui.insertMenuAction(saveAllAct, fileMenu.menu(),\n                          before=\"Import File(s)...\")\nfileMenu.menu().insertSeparator(importAct)\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/Startup/selection_tracker.py",
    "content": "\"\"\"Puts the selection project into \"hiero.selection\"\"\"\n\nimport hiero\n\n\ndef selectionChanged(event):\n    hiero.selection = event.sender.selection()\n\nhiero.core.events.registerInterest(\"kSelectionChanged\", selectionChanged)\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/Startup/setFrameRate.py",
    "content": "# setFrameRate - adds a Right-click menu to the Project Bin view, allowing multiple BinItems (Clips/Sequences) to have their frame rates set.\n# Install in: ~/.hiero/Python/StartupUI\n# Requires 1.5v1 or later\n\nimport hiero.core\nimport hiero.ui\ntry:\n  from PySide.QtGui import *\n  from PySide.QtCore import *\nexcept:\n  from PySide2.QtGui import *\n  from PySide2.QtCore import *\n  from PySide2.QtWidgets import *\n\n# Dialog for setting a Custom frame rate.\nclass SetFrameRateDialog(QDialog):\n\n  def __init__(self,itemSelection=None,parent=None):\n    super(SetFrameRateDialog, self).__init__(parent)\n    self.setWindowTitle(\"Set Custom Frame Rate\")\n    self.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Fixed )\n    layout = QFormLayout()\n    self._itemSelection = itemSelection\n\n    self._frameRateField = QLineEdit()\n    self._frameRateField.setToolTip(\"Enter custom frame rate here.\")\n    self._frameRateField.setValidator(QDoubleValidator(1, 99, 3, self))\n    self._frameRateField.textChanged.connect(self._textChanged)\n    layout.addRow(\"Enter fps: \",self._frameRateField)\n\n    # Standard buttons for Add/Cancel\n    self._buttonbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)\n    self._buttonbox.accepted.connect(self.accept)\n    self._buttonbox.rejected.connect(self.reject)\n    self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(False)\n    layout.addRow(\"\",self._buttonbox)\n    self.setLayout(layout)\n\n  def _updateOkButtonState(self):\n    # Cancel is always an option but only enable Ok if there is some text.\n    currentFramerate = float(self.currentFramerateString())\n    enableOk = False\n    enableOk = ((currentFramerate > 0.0) and (currentFramerate <= 250.0))\n    print(\"enabledOk\", enableOk)\n    self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(enableOk)\n\n  def _textChanged(self, newText):\n    self._updateOkButtonState()\n\n  # Returns the current frame rate as a string\n  def currentFramerateString(self):\n    return str(self._frameRateField.text())\n\n  # Presents the Dialog and sets the Frame rate from a selection\n  def showDialogAndSetFrameRateFromSelection(self):\n\n    if self._itemSelection is not None:\n      if self.exec_():\n        # For the Undo loop...\n\n        # Construct an TimeBase object for setting the Frame Rate (fps)\n        fps = hiero.core.TimeBase().fromString(self.currentFramerateString())\n\n\n        # Set the frame rate for the selected BinItmes\n        for item in self._itemSelection:\n          item.setFramerate(fps)\n    return\n\n# This is just a convenience method for returning QActions with a title, triggered method and icon.\ndef makeAction(title, method, icon = None):\n  action = QAction(title,None)\n  action.setIcon(QIcon(icon))\n\n  # We do this magic, so that the title string from the action is used to set the frame rate!\n  def methodWrapper():\n    method(title)\n\n  action.triggered.connect( methodWrapper )\n  return action\n\n# Menu which adds a Set Frame Rate Menu to Project Bin view\nclass SetFrameRateMenu:\n\n  def __init__(self):\n      self._frameRateMenu = None\n      self._frameRatesDialog = None\n\n\n      # ant: Could use hiero.core.defaultFrameRates() here but messes up with string matching because we seem to mix decimal points\n      self.frameRates = [\"8\",\"12\",\"12.50\",\"15\",\"23.98\",\"24\",\"25\",\"29.97\",\"30\",\"48\",\"50\",\"59.94\",\"60\"]\n      hiero.core.events.registerInterest(\"kShowContextMenu/kBin\", self.binViewEventHandler)\n\n      self.menuActions = []\n\n  def createFrameRateMenus(self,selection):\n    selectedClipFPS  = [str(bi.activeItem().framerate()) for bi in selection if (isinstance(bi,hiero.core.BinItem) and hasattr(bi,\"activeItem\"))]\n    selectedClipFPS = hiero.core.util.uniquify(selectedClipFPS)\n    sameFrameRate = len(selectedClipFPS)==1\n    self.menuActions = []\n    for fps in self.frameRates:\n      if fps in selectedClipFPS:\n        if sameFrameRate:\n          self.menuActions+=[makeAction(fps,self.setFrameRateFromMenuSelection, icon=\"icons:Ticked.png\")]\n        else:\n          self.menuActions+=[makeAction(fps,self.setFrameRateFromMenuSelection, icon=\"icons:remove active.png\")]\n      else:\n        self.menuActions+=[makeAction(fps,self.setFrameRateFromMenuSelection, icon=None)]\n\n    # Now add Custom... menu\n    self.menuActions += [makeAction(\n      \"Custom...\", self.setFrameRateFromMenuSelection, icon=None)\n      ]\n\n    frameRateMenu = QMenu(\"Set Frame Rate\")\n    for a in self.menuActions:\n      frameRateMenu.addAction(a)\n\n    return frameRateMenu\n\n  def setFrameRateFromMenuSelection(self, menuSelectionFPS):\n\n    selectedBinItems  = [bi.activeItem() for bi in self._selection if (isinstance(bi,hiero.core.BinItem) and hasattr(bi,\"activeItem\"))]\n    currentProject = selectedBinItems[0].project()\n\n    with currentProject.beginUndo(\"Set Frame Rate\"):\n      if menuSelectionFPS == \"Custom...\":\n        self._frameRatesDialog = SetFrameRateDialog(itemSelection = selectedBinItems )\n        self._frameRatesDialog.showDialogAndSetFrameRateFromSelection()\n\n      else:\n        for b in selectedBinItems:\n          b.setFramerate(hiero.core.TimeBase().fromString(menuSelectionFPS))\n\n    return\n\n  # This handles events from the Project Bin View\n  def binViewEventHandler(self,event):\n    if not hasattr(event.sender, \"selection\"):\n      # Something has gone wrong, we should only be here if raised\n      # by the Bin view which gives a selection.\n      return\n\n    # Reset the selection to None...\n    self._selection = None\n    s = event.sender.selection()\n\n    # Return if there's no Selection. We won't add the Menu.\n    if s == None:\n      return\n    # Filter the selection to BinItems\n    self._selection = [item for item in s if isinstance(item, hiero.core.BinItem)]\n    if len(self._selection)==0:\n      return\n    # Creating the menu based on items selected, to highlight which frame rates are contained\n\n    self._frameRateMenu = self.createFrameRateMenus(self._selection)\n\n    # Insert the Set Frame Rate Button before the Set Media Colour Transform Action\n    for action in event.menu.actions():\n      if str(action.text()) == \"Set Media Colour Transform\":\n        event.menu.insertMenu(action, self._frameRateMenu)\n        break\n\n# Instantiate the Menu to get it to register itself.\nSetFrameRateMenu = SetFrameRateMenu()"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/StartupUI/PimpMySpreadsheet.py",
    "content": "# PimpMySpreadsheet 1.0, Antony Nasce, 23/05/13.\n# Adds custom spreadsheet columns and right-click menu for setting the Shot Status, and Artist Shot Assignment.\n# gStatusTags is a global dictionary of key(status)-value(icon) pairs, which can be overridden with custom icons if required\n# Requires Hiero 1.7v2 or later.\n# Install Instructions: Copy to ~/.hiero/Python/StartupUI\n\nimport hiero.core\nimport hiero.ui\n\ntry:\n    from PySide.QtGui import *\n    from PySide.QtCore import *\nexcept:\n    from PySide2.QtGui import *\n    from PySide2.QtWidgets import *\n    from PySide2.QtCore import *\n\n# Set to True, if you wat \"Set Status\" right-click menu, False if not\nkAddStatusMenu = True\n\n# Set to True, if you wat \"Assign Artist\" right-click menu, False if not\nkAssignArtistMenu = True\n\n# Global list of Artist Name Dictionaries\n# Note: Override this to add different names, icons, department, IDs.\ngArtistList = [{\n    \"artistName\": \"John Smith\",\n    \"artistIcon\": \"icons:TagActor.png\",\n    \"artistDepartment\": \"3D\",\n    \"artistID\": 0\n}, {\n    \"artistName\": \"Savlvador Dali\",\n    \"artistIcon\": \"icons:TagActor.png\",\n    \"artistDepartment\": \"Roto\",\n    \"artistID\": 1\n}, {\n    \"artistName\": \"Leonardo Da Vinci\",\n    \"artistIcon\": \"icons:TagActor.png\",\n    \"artistDepartment\": \"Paint\",\n    \"artistID\": 2\n}, {\n    \"artistName\": \"Claude Monet\",\n    \"artistIcon\": \"icons:TagActor.png\",\n    \"artistDepartment\": \"Comp\",\n    \"artistID\": 3\n}, {\n    \"artistName\": \"Pablo Picasso\",\n    \"artistIcon\": \"icons:TagActor.png\",\n    \"artistDepartment\": \"Animation\",\n    \"artistID\": 4\n}]\n\n# Global Dictionary of Status Tags.\n# Note: This can be overwritten if you want to add a new status cellType or custom icon\n# Override the gStatusTags dictionary by adding your own \"Status\":\"Icon.png\" key-value pairs.\n# Add new custom keys like so: gStatusTags[\"For Client\"] = \"forClient.png\"\ngStatusTags = {\n    \"Approved\": \"icons:status/TagApproved.png\",\n    \"Unapproved\": \"icons:status/TagUnapproved.png\",\n    \"Ready To Start\": \"icons:status/TagReadyToStart.png\",\n    \"Blocked\": \"icons:status/TagBlocked.png\",\n    \"On Hold\": \"icons:status/TagOnHold.png\",\n    \"In Progress\": \"icons:status/TagInProgress.png\",\n    \"Awaiting Approval\": \"icons:status/TagAwaitingApproval.png\",\n    \"Omitted\": \"icons:status/TagOmitted.png\",\n    \"Final\": \"icons:status/TagFinal.png\"\n}\n\n\n# The Custom Spreadsheet Columns\nclass CustomSpreadsheetColumns(QObject):\n    \"\"\"\n    A class defining custom columns for Hiero's spreadsheet view. This has a similar, but\n    slightly simplified, interface to the QAbstractItemModel and QItemDelegate classes.\n  \"\"\"\n    global gStatusTags\n    global gArtistList\n\n    # Ideally, we'd set this list on a Per Item basis, but this is expensive for a large mixed selection\n    standardColourSpaces = [\n        \"linear\", \"sRGB\", \"rec709\", \"Cineon\", \"Gamma1.8\", \"Gamma2.2\",\n        \"Panalog\", \"REDLog\", \"ViperLog\"\n    ]\n    arriColourSpaces = [\n        \"Video - Rec709\", \"LogC - Camera Native\", \"Video - P3\", \"ACES\",\n        \"LogC - Film\", \"LogC - Wide Gamut\"\n    ]\n    r3dColourSpaces = [\n        \"Linear\", \"Rec709\", \"REDspace\", \"REDlog\", \"PDlog685\", \"PDlog985\",\n        \"CustomPDlog\", \"REDgamma\", \"SRGB\", \"REDlogFilm\", \"REDgamma2\",\n        \"REDgamma3\"\n    ]\n    gColourSpaces = standardColourSpaces + arriColourSpaces + r3dColourSpaces\n\n    currentView = hiero.ui.activeView()\n\n    # This is the list of Columns available\n    gCustomColumnList = [\n        {\n            \"name\": \"Tags\",\n            \"cellType\": \"readonly\"\n        },\n        {\n            \"name\": \"Colourspace\",\n            \"cellType\": \"dropdown\"\n        },\n        {\n            \"name\": \"Notes\",\n            \"cellType\": \"readonly\"\n        },\n        {\n            \"name\": \"FileType\",\n            \"cellType\": \"readonly\"\n        },\n        {\n            \"name\": \"Shot Status\",\n            \"cellType\": \"dropdown\"\n        },\n        {\n            \"name\": \"Thumbnail\",\n            \"cellType\": \"readonly\"\n        },\n        {\n            \"name\": \"MediaType\",\n            \"cellType\": \"readonly\"\n        },\n        {\n            \"name\": \"Width\",\n            \"cellType\": \"readonly\"\n        },\n        {\n            \"name\": \"Height\",\n            \"cellType\": \"readonly\"\n        },\n        {\n            \"name\": \"Pixel Aspect\",\n            \"cellType\": \"readonly\"\n        },\n        {\n            \"name\": \"Artist\",\n            \"cellType\": \"dropdown\"\n        },\n        {\n            \"name\": \"Department\",\n            \"cellType\": \"readonly\"\n        },\n    ]\n\n    def numColumns(self):\n        \"\"\"\n      Return the number of custom columns in the spreadsheet view\n    \"\"\"\n        return len(self.gCustomColumnList)\n\n    def columnName(self, column):\n        \"\"\"\n      Return the name of a custom column\n    \"\"\"\n        return self.gCustomColumnList[column][\"name\"]\n\n    def getTagsString(self, item):\n        \"\"\"\n      Convenience method for returning all the Notes in a Tag as a string\n    \"\"\"\n        tagNames = []\n        tags = item.tags()\n        for tag in tags:\n            tagNames += [tag.name()]\n        tagNameString = ','.join(tagNames)\n        return tagNameString\n\n    def getNotes(self, item):\n        \"\"\"\n      Convenience method for returning all the Notes in a Tag as a string\n    \"\"\"\n        notes = \"\"\n        tags = item.tags()\n        for tag in tags:\n            note = tag.note()\n            if len(note) > 0:\n                notes += tag.note() + ', '\n        return notes[:-2]\n\n    def getData(self, row, column, item):\n        \"\"\"\n      Return the data in a cell\n    \"\"\"\n        currentColumn = self.gCustomColumnList[column]\n        if currentColumn[\"name\"] == \"Tags\":\n            return self.getTagsString(item)\n\n        if currentColumn[\"name\"] == \"Colourspace\":\n            try:\n                colTransform = item.sourceMediaColourTransform()\n            except:\n                colTransform = \"--\"\n            return colTransform\n\n        if currentColumn[\"name\"] == \"Notes\":\n            try:\n                note = self.getNotes(item)\n            except:\n                note = \"\"\n            return note\n\n        if currentColumn[\"name\"] == \"FileType\":\n            fileType = \"--\"\n            M = item.source().mediaSource().metadata()\n            if M.hasKey(\"foundry.source.type\"):\n                fileType = M.value(\"foundry.source.type\")\n            elif M.hasKey(\"media.input.filereader\"):\n                fileType = M.value(\"media.input.filereader\")\n            return fileType\n\n        if currentColumn[\"name\"] == \"Shot Status\":\n            status = item.status()\n            if not status:\n                status = \"--\"\n            return str(status)\n\n        if currentColumn[\"name\"] == \"MediaType\":\n            M = item.mediaType()\n            return str(M).split(\"MediaType\")[-1].replace(\".k\", \"\")\n\n        if currentColumn[\"name\"] == \"Thumbnail\":\n            return str(item.eventNumber())\n\n        if currentColumn[\"name\"] == \"Width\":\n            return str(item.source().format().width())\n\n        if currentColumn[\"name\"] == \"Height\":\n            return str(item.source().format().height())\n\n        if currentColumn[\"name\"] == \"Pixel Aspect\":\n            return str(item.source().format().pixelAspect())\n\n        if currentColumn[\"name\"] == \"Artist\":\n            if item.artist():\n                name = item.artist()[\"artistName\"]\n                return name\n            else:\n                return \"--\"\n\n        if currentColumn[\"name\"] == \"Department\":\n            if item.artist():\n                dep = item.artist()[\"artistDepartment\"]\n                return dep\n            else:\n                return \"--\"\n\n        return \"\"\n\n    def setData(self, row, column, item, data):\n        \"\"\"\n      Set the data in a cell - unused in this example\n    \"\"\"\n\n        return None\n\n    def getTooltip(self, row, column, item):\n        \"\"\"\n      Return the tooltip for a cell\n    \"\"\"\n        currentColumn = self.gCustomColumnList[column]\n        if currentColumn[\"name\"] == \"Tags\":\n            return str([item.name() for item in item.tags()])\n\n        if currentColumn[\"name\"] == \"Notes\":\n            return str(self.getNotes(item))\n        return \"\"\n\n    def getFont(self, row, column, item):\n        \"\"\"\n      Return the tooltip for a cell\n    \"\"\"\n        return None\n\n    def getBackground(self, row, column, item):\n        \"\"\"\n      Return the background colour for a cell\n    \"\"\"\n        if not item.source().mediaSource().isMediaPresent():\n            return QColor(80, 20, 20)\n        return None\n\n    def getForeground(self, row, column, item):\n        \"\"\"\n      Return the text colour for a cell\n    \"\"\"\n        #if column == 1:\n        #  return QColor(255, 64, 64)\n        return None\n\n    def getIcon(self, row, column, item):\n        \"\"\"\n      Return the icon for a cell\n    \"\"\"\n        currentColumn = self.gCustomColumnList[column]\n        if currentColumn[\"name\"] == \"Colourspace\":\n            return QIcon(\"icons:LUT.png\")\n\n        if currentColumn[\"name\"] == \"Shot Status\":\n            status = item.status()\n            if status:\n                return QIcon(gStatusTags[status])\n\n        if currentColumn[\"name\"] == \"MediaType\":\n            mediaType = item.mediaType()\n            if mediaType == hiero.core.TrackItem.kVideo:\n                return QIcon(\"icons:VideoOnly.png\")\n            elif mediaType == hiero.core.TrackItem.kAudio:\n                return QIcon(\"icons:AudioOnly.png\")\n\n        if currentColumn[\"name\"] == \"Artist\":\n            try:\n                return QIcon(item.artist()[\"artistIcon\"])\n            except:\n                return None\n        return None\n\n    def getSizeHint(self, row, column, item):\n        \"\"\"\n      Return the size hint for a cell\n    \"\"\"\n        currentColumnName = self.gCustomColumnList[column][\"name\"]\n\n        if currentColumnName == \"Thumbnail\":\n            return QSize(90, 50)\n\n        return QSize(50, 50)\n\n    def paintCell(self, row, column, item, painter, option):\n        \"\"\"\n      Paint a custom cell. Return True if the cell was painted, or False to continue\n      with the default cell painting.\n    \"\"\"\n        currentColumn = self.gCustomColumnList[column]\n        if currentColumn[\"name\"] == \"Tags\":\n            if option.state & QStyle.State_Selected:\n                painter.fillRect(option.rect, option.palette.highlight())\n            iconSize = 20\n            r = QRect(option.rect.x(),\n                      option.rect.y() + (option.rect.height() - iconSize) / 2,\n                      iconSize, iconSize)\n            tags = item.tags()\n            if len(tags) > 0:\n                painter.save()\n                painter.setClipRect(option.rect)\n                for tag in item.tags():\n                    M = tag.metadata()\n                    if not (M.hasKey(\"tag.status\")\n                            or M.hasKey(\"tag.artistID\")):\n                        QIcon(tag.icon()).paint(painter, r, Qt.AlignLeft)\n                        r.translate(r.width() + 2, 0)\n                painter.restore()\n                return True\n\n        if currentColumn[\"name\"] == \"Thumbnail\":\n            imageView = None\n            pen = QPen()\n            r = QRect(option.rect.x() + 2, (option.rect.y() +\n                                            (option.rect.height() - 46) / 2),\n                      85, 46)\n            if not item.source().mediaSource().isMediaPresent():\n                imageView = QImage(\"icons:Offline.png\")\n                pen.setColor(QColor(Qt.red))\n\n            if item.mediaType() == hiero.core.TrackItem.MediaType.kAudio:\n                imageView = QImage(\"icons:AudioOnly.png\")\n                #pen.setColor(QColor(Qt.green))\n                painter.fillRect(r, QColor(45, 59, 45))\n\n            if option.state & QStyle.State_Selected:\n                painter.fillRect(option.rect, option.palette.highlight())\n\n            tags = item.tags()\n            painter.save()\n            painter.setClipRect(option.rect)\n\n            if not imageView:\n                try:\n                    imageView = item.thumbnail(item.sourceIn())\n                    pen.setColor(QColor(20, 20, 20))\n                # If we're here, we probably have a TC error, no thumbnail, so get it from the source Clip...\n                except:\n                    pen.setColor(QColor(Qt.red))\n\n            if not imageView:\n                try:\n                    imageView = item.source().thumbnail()\n                    pen.setColor(QColor(Qt.yellow))\n                except:\n                    imageView = QImage(\"icons:Offline.png\")\n                    pen.setColor(QColor(Qt.red))\n\n            QIcon(QPixmap.fromImage(imageView)).paint(painter, r,\n                                                      Qt.AlignCenter)\n            painter.setPen(pen)\n            painter.drawRoundedRect(r, 1, 1)\n            painter.restore()\n            return True\n\n        return False\n\n    def createEditor(self, row, column, item, view):\n        \"\"\"\n      Create an editing widget for a custom cell\n    \"\"\"\n        self.currentView = view\n\n        currentColumn = self.gCustomColumnList[column]\n        if currentColumn[\"cellType\"] == \"readonly\":\n            cle = QLabel()\n            cle.setEnabled(False)\n            cle.setVisible(False)\n            return cle\n\n        if currentColumn[\"name\"] == \"Colourspace\":\n            cb = QComboBox()\n            for colourspace in self.gColourSpaces:\n                cb.addItem(colourspace)\n            cb.currentIndexChanged.connect(self.colourspaceChanged)\n            return cb\n\n        if currentColumn[\"name\"] == \"Shot Status\":\n            cb = QComboBox()\n            cb.addItem(\"\")\n            for key in gStatusTags.keys():\n                cb.addItem(QIcon(gStatusTags[key]), key)\n            cb.addItem(\"--\")\n            cb.currentIndexChanged.connect(self.statusChanged)\n\n            return cb\n\n        if currentColumn[\"name\"] == \"Artist\":\n            cb = QComboBox()\n            cb.addItem(\"\")\n            for artist in gArtistList:\n                cb.addItem(artist[\"artistName\"])\n            cb.addItem(\"--\")\n            cb.currentIndexChanged.connect(self.artistNameChanged)\n            return cb\n        return None\n\n    def setModelData(self, row, column, item, editor):\n        return False\n\n    def dropMimeData(self, row, column, item, data, items):\n        \"\"\"\n      Handle a drag and drop operation - adds a Dragged Tag to the shot\n    \"\"\"\n        for thing in items:\n            if isinstance(thing, hiero.core.Tag):\n                item.addTag(thing)\n        return None\n\n    def colourspaceChanged(self, index):\n        \"\"\"\n      This method is called when Colourspace widget changes index.\n    \"\"\"\n        index = self.sender().currentIndex()\n        colourspace = self.gColourSpaces[index]\n        selection = self.currentView.selection()\n        project = selection[0].project()\n        with project.beginUndo(\"Set Colourspace\"):\n            items = [\n                item for item in selection\n                if (item.mediaType() == hiero.core.TrackItem.MediaType.kVideo)\n            ]\n            for trackItem in items:\n                trackItem.setSourceMediaColourTransform(colourspace)\n\n    def statusChanged(self, arg):\n        \"\"\"\n      This method is called when Shot Status widget changes index.\n    \"\"\"\n        view = hiero.ui.activeView()\n        selection = view.selection()\n        status = self.sender().currentText()\n        project = selection[0].project()\n        with project.beginUndo(\"Set Status\"):\n            # A string of \"--\" characters denotes clear the status\n            if status != \"--\":\n                for trackItem in selection:\n                    trackItem.setStatus(status)\n            else:\n                for trackItem in selection:\n                    tTags = trackItem.tags()\n                    for tag in tTags:\n                        if tag.metadata().hasKey(\"tag.status\"):\n                            trackItem.removeTag(tag)\n                            break\n\n    def artistNameChanged(self, arg):\n        \"\"\"\n      This method is called when Artist widget changes index.\n    \"\"\"\n        view = hiero.ui.activeView()\n        selection = view.selection()\n        name = self.sender().currentText()\n        project = selection[0].project()\n        with project.beginUndo(\"Assign Artist\"):\n            # A string of \"--\" denotes clear the assignee...\n            if name != \"--\":\n                for trackItem in selection:\n                    trackItem.setArtistByName(name)\n            else:\n                for trackItem in selection:\n                    tTags = trackItem.tags()\n                    for tag in tTags:\n                        if tag.metadata().hasKey(\"tag.artistID\"):\n                            trackItem.removeTag(tag)\n                            break\n\n\ndef _getArtistFromID(self, artistID):\n    \"\"\" getArtistFromID -> returns an artist dictionary, by their given ID\"\"\"\n    global gArtistList\n    artist = [\n        element for element in gArtistList\n        if element[\"artistID\"] == int(artistID)\n    ]\n    if not artist:\n        return None\n    return artist[0]\n\n\ndef _getArtistFromName(self, artistName):\n    \"\"\" getArtistFromID -> returns an artist dictionary, by their given ID \"\"\"\n    global gArtistList\n    artist = [\n        element for element in gArtistList\n        if element[\"artistName\"] == artistName\n    ]\n    if not artist:\n        return None\n    return artist[0]\n\n\ndef _artist(self):\n    \"\"\"_artist -> Returns the artist dictionary assigned to this shot\"\"\"\n    artist = None\n    tags = self.tags()\n    for tag in tags:\n        if tag.metadata().hasKey(\"tag.artistID\"):\n            artistID = tag.metadata().value(\"tag.artistID\")\n            artist = self.getArtistFromID(artistID)\n    return artist\n\n\ndef _updateArtistTag(self, artistDict):\n    # A shot will only have one artist assigned. Check if one exists and set accordingly\n\n    artistTag = None\n    tags = self.tags()\n    for tag in tags:\n        if tag.metadata().hasKey(\"tag.artistID\"):\n            artistTag = tag\n            break\n\n    if not artistTag:\n        artistTag = hiero.core.Tag(\"Artist\")\n        artistTag.setIcon(artistDict[\"artistIcon\"])\n        artistTag.metadata().setValue(\"tag.artistID\",\n                                      str(artistDict[\"artistID\"]))\n        artistTag.metadata().setValue(\"tag.artistName\",\n                                      str(artistDict[\"artistName\"]))\n        artistTag.metadata().setValue(\"tag.artistDepartment\",\n                                      str(artistDict[\"artistDepartment\"]))\n        self.sequence().editFinished()\n        self.addTag(artistTag)\n        self.sequence().editFinished()\n        return\n\n    artistTag.setIcon(artistDict[\"artistIcon\"])\n    artistTag.metadata().setValue(\"tag.artistID\", str(artistDict[\"artistID\"]))\n    artistTag.metadata().setValue(\"tag.artistName\",\n                                  str(artistDict[\"artistName\"]))\n    artistTag.metadata().setValue(\"tag.artistDepartment\",\n                                  str(artistDict[\"artistDepartment\"]))\n    self.sequence().editFinished()\n    return\n\n\ndef _setArtistByName(self, artistName):\n    \"\"\" setArtistByName(artistName) -> sets the artist tag on a TrackItem by a given artistName string\"\"\"\n    global gArtistList\n\n    artist = self.getArtistFromName(artistName)\n    if not artist:\n        print((\n            \"Artist name: {} was not found in \"\n            \"the gArtistList.\").format(artistName))\n        return\n\n    # Do the update.\n    self.updateArtistTag(artist)\n\n\ndef _setArtistByID(self, artistID):\n    \"\"\" setArtistByID(artistID) -> sets the artist tag on a TrackItem by a given artistID integer\"\"\"\n    global gArtistList\n\n    artist = self.getArtistFromID(artistID)\n    if not artist:\n        print(\"Artist name: {} was not found in the gArtistList.\".format(\n            artistID))\n        return\n\n    # Do the update.\n    self.updateArtistTag(artist)\n\n\n# Inject status getter and setter methods into hiero.core.TrackItem\nhiero.core.TrackItem.artist = _artist\nhiero.core.TrackItem.setArtistByName = _setArtistByName\nhiero.core.TrackItem.setArtistByID = _setArtistByID\nhiero.core.TrackItem.getArtistFromName = _getArtistFromName\nhiero.core.TrackItem.getArtistFromID = _getArtistFromID\nhiero.core.TrackItem.updateArtistTag = _updateArtistTag\n\n\ndef _status(self):\n    \"\"\"status -> Returns the Shot status. None if no Status is set.\"\"\"\n\n    status = None\n    tags = self.tags()\n    for tag in tags:\n        if tag.metadata().hasKey(\"tag.status\"):\n            status = tag.metadata().value(\"tag.status\")\n    return status\n\n\ndef _setStatus(self, status):\n    \"\"\"setShotStatus(status) -> Method to set the Status of a Shot.\n  Adds a special kind of status Tag to a TrackItem\n  Example: myTrackItem.setStatus(\"Final\")\n\n  @param status - a string, corresponding to the Status name\n  \"\"\"\n    global gStatusTags\n\n    # Get a valid Tag object from the Global list of statuses\n    if not status in gStatusTags.keys():\n        print(\"Status requested was not a valid Status string.\")\n        return\n\n    # A shot should only have one status. Check if one exists and set accordingly\n    statusTag = None\n    tags = self.tags()\n    for tag in tags:\n        if tag.metadata().hasKey(\"tag.status\"):\n            statusTag = tag\n            break\n\n    if not statusTag:\n        statusTag = hiero.core.Tag(\"Status\")\n        statusTag.setIcon(gStatusTags[status])\n        statusTag.metadata().setValue(\"tag.status\", status)\n        self.addTag(statusTag)\n\n    statusTag.setIcon(gStatusTags[status])\n    statusTag.metadata().setValue(\"tag.status\", status)\n\n    self.sequence().editFinished()\n    return\n\n\n# Inject status getter and setter methods into hiero.core.TrackItem\nhiero.core.TrackItem.setStatus = _setStatus\nhiero.core.TrackItem.status = _status\n\n\n# This is a convenience method for returning QActions with a triggered method based on the title string\ndef titleStringTriggeredAction(title, method, icon=None):\n    action = QAction(title, None)\n    action.setIcon(QIcon(icon))\n\n    # We do this magic, so that the title string from the action is used to set the status\n    def methodWrapper():\n        method(title)\n\n    action.triggered.connect(methodWrapper)\n    return action\n\n\n# Menu which adds a Set Status Menu to Timeline and Spreadsheet Views\nclass SetStatusMenu(QMenu):\n    def __init__(self):\n        QMenu.__init__(self, \"Set Status\", None)\n\n        global gStatusTags\n        self.statuses = gStatusTags\n        self._statusActions = self.createStatusMenuActions()\n\n        # Add the Actions to the Menu.\n        for act in self.menuActions:\n            self.addAction(act)\n\n        hiero.core.events.registerInterest(\"kShowContextMenu/kTimeline\",\n                                           self.eventHandler)\n        hiero.core.events.registerInterest(\"kShowContextMenu/kSpreadsheet\",\n                                           self.eventHandler)\n\n    def createStatusMenuActions(self):\n        self.menuActions = []\n        for status in self.statuses:\n            self.menuActions += [\n                titleStringTriggeredAction(\n                    status,\n                    self.setStatusFromMenuSelection,\n                    icon=gStatusTags[status])\n            ]\n\n    def setStatusFromMenuSelection(self, menuSelectionStatus):\n        selectedShots = [\n            item for item in self._selection\n            if (isinstance(item, hiero.core.TrackItem))\n        ]\n        selectedTracks = [\n            item for item in self._selection\n            if (isinstance(item, (hiero.core.VideoTrack,\n                                  hiero.core.AudioTrack)))\n        ]\n\n        # If we have a Track Header Selection, no shots could be selected, so create shotSelection list\n        if len(selectedTracks) >= 1:\n            for track in selectedTracks:\n                selectedShots += [\n                    item for item in track.items()\n                    if (isinstance(item, hiero.core.TrackItem))\n                ]\n\n        # It's possible no shots exist on the Track, in which case nothing is required\n        if len(selectedShots) == 0:\n            return\n\n        currentProject = selectedShots[0].project()\n\n        with currentProject.beginUndo(\"Set Status\"):\n            # Shots selected\n            for shot in selectedShots:\n                shot.setStatus(menuSelectionStatus)\n\n    # This handles events from the Project Bin View\n    def eventHandler(self, event):\n        if not hasattr(event.sender, \"selection\"):\n            # Something has gone wrong, we should only be here if raised\n            # by the Timeline/Spreadsheet view which gives a selection.\n            return\n\n        # Set the current selection\n        self._selection = event.sender.selection()\n\n        # Return if there's no Selection. We won't add the Menu.\n        if len(self._selection) == 0:\n            return\n\n        event.menu.addMenu(self)\n\n\n# Menu which adds a Set Status Menu to Timeline and Spreadsheet Views\nclass AssignArtistMenu(QMenu):\n    def __init__(self):\n        QMenu.__init__(self, \"Assign Artist\", None)\n\n        global gArtistList\n        self.artists = gArtistList\n        self._artistsActions = self.createAssignArtistMenuActions()\n\n        # Add the Actions to the Menu.\n        for act in self.menuActions:\n            self.addAction(act)\n\n        hiero.core.events.registerInterest(\"kShowContextMenu/kTimeline\",\n                                           self.eventHandler)\n        hiero.core.events.registerInterest(\"kShowContextMenu/kSpreadsheet\",\n                                           self.eventHandler)\n\n    def createAssignArtistMenuActions(self):\n        self.menuActions = []\n        for artist in self.artists:\n            self.menuActions += [\n                titleStringTriggeredAction(\n                    artist[\"artistName\"],\n                    self.setArtistFromMenuSelection,\n                    icon=artist[\"artistIcon\"])\n            ]\n\n    def setArtistFromMenuSelection(self, menuSelectionArtist):\n        selectedShots = [\n            item for item in self._selection\n            if (isinstance(item, hiero.core.TrackItem))\n        ]\n        selectedTracks = [\n            item for item in self._selection\n            if (isinstance(item, (hiero.core.VideoTrack,\n                                  hiero.core.AudioTrack)))\n        ]\n\n        # If we have a Track Header Selection, no shots could be selected, so create shotSelection list\n        if len(selectedTracks) >= 1:\n            for track in selectedTracks:\n                selectedShots += [\n                    item for item in track.items()\n                    if (isinstance(item, hiero.core.TrackItem))\n                ]\n\n        # It's possible no shots exist on the Track, in which case nothing is required\n        if len(selectedShots) == 0:\n            return\n\n        currentProject = selectedShots[0].project()\n\n        with currentProject.beginUndo(\"Assign Artist\"):\n            # Shots selected\n            for shot in selectedShots:\n                shot.setArtistByName(menuSelectionArtist)\n\n    # This handles events from the Project Bin View\n    def eventHandler(self, event):\n        if not hasattr(event.sender, \"selection\"):\n            # Something has gone wrong, we should only be here if raised\n            # by the Timeline/Spreadsheet view which gives a selection.\n            return\n\n        # Set the current selection\n        self._selection = event.sender.selection()\n\n        # Return if there's no Selection. We won't add the Menu.\n        if len(self._selection) == 0:\n            return\n\n        event.menu.addMenu(self)\n\n\n# Add the \"Set Status\" context menu to Timeline and Spreadsheet\nif kAddStatusMenu:\n    setStatusMenu = SetStatusMenu()\n\nif kAssignArtistMenu:\n    assignArtistMenu = AssignArtistMenu()\n\n# Register our custom columns\nhiero.ui.customColumn = CustomSpreadsheetColumns()\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/StartupUI/Purge.py",
    "content": "# Purge Unused Clips - Removes any unused Clips from a Project\n# Usage: Copy to ~/.hiero/Python/StartupUI\n# Demonstrates the use of hiero.core.find_items module.\n# Usage: Right-click on an item in the Bin View > \"Purge Unused Clips\"\n# Result: Any Clips not used in a Sequence in the active project will be removed\n# Requires Hiero 1.5v1 or later.\n# Version 1.1\n\nimport hiero\nimport hiero.core.find_items\ntry:\n    from PySide.QtGui import *\n    from PySide.QtCore import *\nexcept:\n    from PySide2.QtGui import *\n    from PySide2.QtWidgets import *\n    from PySide2.QtCore import *\n\n\nclass PurgeUnusedAction(QAction):\n    def __init__(self):\n        QAction.__init__(self, \"Purge Unused Clips\", None)\n        self.triggered.connect(self.PurgeUnused)\n        hiero.core.events.registerInterest(\"kShowContextMenu/kBin\",\n                                           self.eventHandler)\n        self.setIcon(QIcon(\"icons:TagDelete.png\"))\n\n    # Method to return whether a Bin is empty...\n    def binIsEmpty(self, b):\n        numBinItems = 0\n        bItems = b.items()\n        empty = False\n\n        if len(bItems) == 0:\n            empty = True\n            return empty\n        else:\n            for b in bItems:\n                if isinstance(b, hiero.core.BinItem) or isinstance(\n                        b, hiero.core.Bin):\n                    numBinItems += 1\n            if numBinItems == 0:\n                empty = True\n\n        return empty\n\n    def PurgeUnused(self):\n\n        #Get selected items\n        item = self.selectedItem\n        proj = item.project()\n\n        # Build a list of Projects\n        SEQS = hiero.core.findItems(proj, \"Sequences\")\n\n        # Build a list of Clips\n        CLIPSTOREMOVE = hiero.core.findItems(proj, \"Clips\")\n\n        if len(SEQS) == 0:\n            # Present Dialog Asking if User wants to remove Clips\n            msgBox = QMessageBox()\n            msgBox.setText(\"Purge Unused Clips\")\n            msgBox.setInformativeText(\n                \"You have no Sequences in this Project. Do you want to remove all Clips (%i) from Project: %s?\"\n                % (len(CLIPSTOREMOVE), proj.name()))\n            msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)\n            msgBox.setDefaultButton(QMessageBox.Ok)\n            ret = msgBox.exec_()\n            if ret == QMessageBox.Cancel:\n                print(\"Not purging anything.\")\n            elif ret == QMessageBox.Ok:\n                with proj.beginUndo(\"Purge Unused Clips\"):\n                    BINS = []\n                    for clip in CLIPSTOREMOVE:\n                        BI = clip.binItem()\n                        B = BI.parentBin()\n                        BINS += [B]\n                        print(\"Removing: {}\".format(BI))\n                        try:\n                            B.removeItem(BI)\n                        except:\n                            print(\"Unable to remove: {}\".format(BI))\n            return\n\n        # For each sequence, iterate through each track Item, see if the Clip is in the CLIPS list.\n        # Remaining items in CLIPS will be removed\n\n        for seq in SEQS:\n\n            #Loop through selected and make folders\n            for track in seq:\n                for trackitem in track:\n\n                    if trackitem.source() in CLIPSTOREMOVE:\n                        CLIPSTOREMOVE.remove(trackitem.source())\n\n        # Present Dialog Asking if User wants to remove Clips\n        msgBox = QMessageBox()\n        msgBox.setText(\"Purge Unused Clips\")\n        msgBox.setInformativeText(\"Remove %i unused Clips from Project %s?\" %\n                                  (len(CLIPSTOREMOVE), proj.name()))\n        msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)\n        msgBox.setDefaultButton(QMessageBox.Ok)\n        ret = msgBox.exec_()\n\n        if ret == QMessageBox.Cancel:\n            print(\"Cancel\")\n            return\n        elif ret == QMessageBox.Ok:\n            BINS = []\n            with proj.beginUndo(\"Purge Unused Clips\"):\n                # Delete the rest of the Clips\n                for clip in CLIPSTOREMOVE:\n                    BI = clip.binItem()\n                    B = BI.parentBin()\n                    BINS += [B]\n                    print(\"Removing: {}\".format(BI))\n                    try:\n                        B.removeItem(BI)\n                    except:\n                        print(\"Unable to remove: {}\".format(BI))\n\n    def eventHandler(self, event):\n        if not hasattr(event.sender, \"selection\"):\n            # Something has gone wrong, we shouldn't only be here if raised\n            # by the Bin view which will give a selection.\n            return\n\n        self.selectedItem = None\n        s = event.sender.selection()\n\n        if len(s) >= 1:\n            self.selectedItem = s[0]\n            title = \"Purge Unused Clips\"\n            self.setText(title)\n            event.menu.addAction(self)\n\n        return\n\n\n# Instantiate the action to get it to register itself.\nPurgeUnusedAction = PurgeUnusedAction()\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/StartupUI/nukeStyleKeyboardShortcuts.py",
    "content": "# nukeStyleKeyboardShortcuts, v1, 30/07/2012, Ant Nasce.\n# A few Nuke-Style File menu shortcuts for those whose muscle memory has set in...\n# Usage: Copy this file to ~/.hiero/Python/StartupUI/\n\nimport hiero.ui\ntry:\n    from PySide.QtGui import *\n    from PySide.QtCore import *\nexcept:\n    from PySide2.QtGui import *\n    from PySide2.QtWidgets import *\n    from PySide2.QtCore import *\n\n#----------------------------------------------\na = hiero.ui.findMenuAction('Import File(s)...')\n# Note: You probably best to make this 'Ctrl+R' - currently conflicts with \"Red\" in the Viewer!\na.setShortcut(QKeySequence(\"R\"))\n#----------------------------------------------\na = hiero.ui.findMenuAction('Import Folder(s)...')\na.setShortcut(QKeySequence('Shift+R'))\n#----------------------------------------------\na = hiero.ui.findMenuAction(\"Import EDL/XML/AAF...\")\na.setShortcut(QKeySequence('Ctrl+Shift+O'))\n#----------------------------------------------\na = hiero.ui.findMenuAction(\"Metadata View\")\na.setShortcut(QKeySequence(\"I\"))\n#----------------------------------------------\na = hiero.ui.findMenuAction(\"Edit Settings\")\na.setShortcut(QKeySequence(\"S\"))\n#----------------------------------------------\na = hiero.ui.findMenuAction(\"Monitor Output\")\nif a:\n    a.setShortcut(QKeySequence('Ctrl+U'))\n#----------------------------------------------\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py",
    "content": "# MIT License\n#\n# Copyright (c) 2018 Daniel Flehner Heen (Storm Studios)\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport os\nimport sys\nimport hiero.core\nimport hiero.ui\n\ntry:\n    from urllib import unquote\n\nexcept ImportError:\n    from urllib.parse import unquote  # lint:ok\n\nimport opentimelineio as otio\n\n\ndef get_transition_type(otio_item, otio_track):\n    _in, _out = otio_track.neighbors_of(otio_item)\n\n    if isinstance(_in, otio.schema.Gap):\n        _in = None\n\n    if isinstance(_out, otio.schema.Gap):\n        _out = None\n\n    if _in and _out:\n        return \"dissolve\"\n\n    elif _in and not _out:\n        return \"fade_out\"\n\n    elif not _in and _out:\n        return \"fade_in\"\n\n    else:\n        return \"unknown\"\n\n\ndef find_trackitem(name, hiero_track):\n    for item in hiero_track.items():\n        if item.name() == name:\n            return item\n\n    return None\n\n\ndef get_neighboring_trackitems(otio_item, otio_track, hiero_track):\n    _in, _out = otio_track.neighbors_of(otio_item)\n    trackitem_in = None\n    trackitem_out = None\n\n    if _in:\n        trackitem_in = find_trackitem(_in.name, hiero_track)\n\n    if _out:\n        trackitem_out = find_trackitem(_out.name, hiero_track)\n\n    return trackitem_in, trackitem_out\n\n\ndef apply_transition(otio_track, otio_item, track):\n    # Figure out type of transition\n    transition_type = get_transition_type(otio_item, otio_track)\n\n    # Figure out track kind for getattr below\n    if isinstance(track, hiero.core.VideoTrack):\n        kind = \"\"\n\n    else:\n        kind = \"Audio\"\n\n    try:\n        # Gather TrackItems involved in trasition\n        item_in, item_out = get_neighboring_trackitems(\n            otio_item,\n            otio_track,\n            track\n        )\n\n        # Create transition object\n        if transition_type == \"dissolve\":\n            transition_func = getattr(\n                hiero.core.Transition,\n                'create{kind}DissolveTransition'.format(kind=kind)\n            )\n\n            transition = transition_func(\n                item_in,\n                item_out,\n                otio_item.in_offset.value,\n                otio_item.out_offset.value\n            )\n\n        elif transition_type == \"fade_in\":\n            transition_func = getattr(\n                hiero.core.Transition,\n                'create{kind}FadeInTransition'.format(kind=kind)\n            )\n            transition = transition_func(\n                item_out,\n                otio_item.out_offset.value\n            )\n\n        elif transition_type == \"fade_out\":\n            transition_func = getattr(\n                hiero.core.Transition,\n                'create{kind}FadeOutTransition'.format(kind=kind)\n            )\n            transition = transition_func(\n                item_in,\n                otio_item.in_offset.value\n            )\n\n        else:\n            # Unknown transition\n            return\n\n        # Apply transition to track\n        track.addTransition(transition)\n\n    except Exception, e:\n        sys.stderr.write(\n            'Unable to apply transition \"{t}\": \"{e}\"\\n'.format(\n                t=otio_item,\n                e=e\n            )\n        )\n\n\ndef prep_url(url_in):\n    url = unquote(url_in)\n\n    if url.startswith(\"file://localhost/\"):\n        return url.replace(\"file://localhost/\", \"\")\n\n    url = '{url}'.format(\n        sep=url.startswith(os.sep) and \"\" or os.sep,\n        url=url.startswith(os.sep) and url[1:] or url\n    )\n\n    return url\n\n\ndef create_offline_mediasource(otio_clip, path=None):\n    hiero_rate = hiero.core.TimeBase(\n        otio_clip.source_range.start_time.rate\n    )\n\n    if isinstance(otio_clip.media_reference, otio.schema.ExternalReference):\n        source_range = otio_clip.available_range()\n\n    else:\n        source_range = otio_clip.source_range\n\n    if path is None:\n        path = otio_clip.name\n\n    media = hiero.core.MediaSource.createOfflineVideoMediaSource(\n        prep_url(path),\n        source_range.start_time.value,\n        source_range.duration.value,\n        hiero_rate,\n        source_range.start_time.value\n    )\n\n    return media\n\n\ndef load_otio(otio_file):\n    otio_timeline = otio.adapters.read_from_file(otio_file)\n    build_sequence(otio_timeline)\n\n\nmarker_color_map = {\n    \"PINK\": \"Magenta\",\n    \"RED\": \"Red\",\n    \"ORANGE\": \"Yellow\",\n    \"YELLOW\": \"Yellow\",\n    \"GREEN\": \"Green\",\n    \"CYAN\": \"Cyan\",\n    \"BLUE\": \"Blue\",\n    \"PURPLE\": \"Magenta\",\n    \"MAGENTA\": \"Magenta\",\n    \"BLACK\": \"Blue\",\n    \"WHITE\": \"Green\",\n    \"MINT\": \"Cyan\"\n}\n\n\ndef get_tag(tagname, tagsbin):\n    for tag in tagsbin.items():\n        if tag.name() == tagname:\n            return tag\n\n        if isinstance(tag, hiero.core.Bin):\n            tag = get_tag(tagname, tag)\n\n            if tag is not None:\n                return tag\n\n    return None\n\n\ndef add_metadata(metadata, hiero_item):\n    for key, value in metadata.items():\n        if isinstance(value, dict):\n            add_metadata(value, hiero_item)\n            continue\n\n        if value is not None:\n            if not key.startswith(\"tag.\"):\n                key = \"tag.\" + key\n\n            hiero_item.metadata().setValue(key, str(value))\n\n\ndef add_markers(otio_item, hiero_item, tagsbin):\n    if isinstance(otio_item, (otio.schema.Stack, otio.schema.Clip)):\n        markers = otio_item.markers\n\n    elif isinstance(otio_item, otio.schema.Timeline):\n        markers = otio_item.tracks.markers\n\n    else:\n        markers = []\n\n    for marker in markers:\n        marker_color = marker.color\n\n        _tag = get_tag(marker.name, tagsbin)\n        if _tag is None:\n            _tag = get_tag(marker_color_map[marker_color], tagsbin)\n\n        if _tag is None:\n            _tag = hiero.core.Tag(marker_color_map[marker.color])\n\n        start = marker.marked_range.start_time.value\n        end = (\n            marker.marked_range.start_time.value +\n            marker.marked_range.duration.value\n        )\n\n        tag = hiero_item.addTag(_tag)\n        tag.setName(marker.name or marker_color_map[marker_color])\n\n        # Add metadata\n        add_metadata(marker.metadata, tag)\n\n\ndef create_track(otio_track, tracknum, track_kind):\n    # Add track kind when dealing with nested stacks\n    if isinstance(otio_track, otio.schema.Stack):\n        otio_track.kind = track_kind\n\n    # Create a Track\n    if otio_track.kind == otio.schema.TrackKind.Video:\n        track = hiero.core.VideoTrack(\n            otio_track.name or 'Video{n}'.format(n=tracknum)\n        )\n\n    else:\n        track = hiero.core.AudioTrack(\n            otio_track.name or 'Audio{n}'.format(n=tracknum)\n        )\n\n    return track\n\n\ndef create_clip(otio_clip):\n    # Create MediaSource\n    otio_media = otio_clip.media_reference\n    if isinstance(otio_media, otio.schema.ExternalReference):\n        url = prep_url(otio_media.target_url)\n        media = hiero.core.MediaSource(url)\n        if media.isOffline():\n            media = create_offline_mediasource(otio_clip, url)\n\n    else:\n        media = create_offline_mediasource(otio_clip)\n\n    # Create Clip\n    clip = hiero.core.Clip(media)\n\n    return clip\n\n\ndef create_trackitem(playhead, track, otio_clip, clip, tagsbin):\n    source_range = otio_clip.source_range\n\n    trackitem = track.createTrackItem(otio_clip.name)\n    trackitem.setPlaybackSpeed(source_range.start_time.rate)\n    trackitem.setSource(clip)\n\n    # Check for speed effects and adjust playback speed accordingly\n    for effect in otio_clip.effects:\n        if isinstance(effect, otio.schema.LinearTimeWarp):\n            trackitem.setPlaybackSpeed(\n                trackitem.playbackSpeed() *\n                effect.time_scalar\n            )\n\n    # If reverse playback speed swap source in and out\n    if trackitem.playbackSpeed() < 0:\n        source_out = source_range.start_time.value\n        source_in = (\n            source_range.start_time.value +\n            source_range.duration.value\n        ) - 1\n        timeline_in = playhead + source_out\n        timeline_out = (\n            timeline_in +\n            source_range.duration.value\n        ) - 1\n    else:\n        # Normal playback speed\n        source_in = source_range.start_time.value\n        source_out = (\n            source_range.start_time.value +\n            source_range.duration.value\n        ) - 1\n        timeline_in = playhead\n        timeline_out = (\n            timeline_in +\n            source_range.duration.value\n        ) - 1\n\n    # Set source and timeline in/out points\n    trackitem.setSourceIn(source_in)\n    trackitem.setSourceOut(source_out)\n    trackitem.setTimelineIn(timeline_in)\n    trackitem.setTimelineOut(timeline_out)\n\n    # Add markers\n    add_markers(otio_clip, trackitem, tagsbin)\n\n    return trackitem\n\n\ndef build_sequence(\n        otio_timeline, project=None, sequence=None, track_kind=None):\n\n    if project is None:\n        if sequence:\n            project = sequence.project()\n\n        else:\n            # Per version 12.1v2 there is no way of getting active project\n            project = hiero.core.projects(hiero.core.Project.kUserProjects)[-1]\n\n    projectbin = project.clipsBin()\n\n    if not sequence:\n        # Create a Sequence\n        sequence = hiero.core.Sequence(otio_timeline.name or \"OTIOSequence\")\n\n        # Set sequence settings from otio timeline if available\n        if hasattr(otio_timeline, \"global_start_time\"):\n            if otio_timeline.global_start_time:\n                start_time = otio_timeline.global_start_time\n                sequence.setFramerate(start_time.rate)\n                sequence.setTimecodeStart(start_time.value)\n\n        # Create a Bin to hold clips\n        projectbin.addItem(hiero.core.BinItem(sequence))\n\n        sequencebin = hiero.core.Bin(sequence.name())\n        projectbin.addItem(sequencebin)\n\n    else:\n        sequencebin = projectbin\n\n    # Get tagsBin\n    tagsbin = hiero.core.project(\"Tag Presets\").tagsBin()\n\n    # Add timeline markers\n    add_markers(otio_timeline, sequence, tagsbin)\n\n    if isinstance(otio_timeline, otio.schema.Timeline):\n        tracks = otio_timeline.tracks\n\n    else:\n        tracks = [otio_timeline]\n\n    for tracknum, otio_track in enumerate(tracks):\n        playhead = 0\n        _transitions = []\n\n        # Add track to sequence\n        track = create_track(otio_track, tracknum, track_kind)\n        sequence.addTrack(track)\n\n        # iterate over items in track\n        for itemnum, otio_clip in enumerate(otio_track):\n            if isinstance(otio_clip, otio.schema.Stack):\n                bar = hiero.ui.mainWindow().statusBar()\n                bar.showMessage(\n                    \"Nested sequences are created separately.\",\n                    timeout=3000\n                )\n                build_sequence(otio_clip, project, otio_track.kind)\n\n            elif isinstance(otio_clip, otio.schema.Clip):\n                # Create a Clip\n                clip = create_clip(otio_clip)\n\n                # Add Clip to a Bin\n                sequencebin.addItem(hiero.core.BinItem(clip))\n\n                # Create TrackItem\n                trackitem = create_trackitem(\n                    playhead,\n                    track,\n                    otio_clip,\n                    clip,\n                    tagsbin\n                )\n\n                # Add trackitem to track\n                track.addTrackItem(trackitem)\n\n                # Update playhead\n                playhead = trackitem.timelineOut() + 1\n\n            elif isinstance(otio_clip, otio.schema.Transition):\n                # Store transitions for when all clips in the track are created\n                _transitions.append((otio_track, otio_clip))\n\n            elif isinstance(otio_clip, otio.schema.Gap):\n                # Hiero has no fillers, slugs or blanks at the moment\n                playhead += otio_clip.source_range.duration.value\n\n        # Apply transitions we stored earlier now that all clips are present\n        for otio_track, otio_item in _transitions:\n            apply_transition(otio_track, otio_item, track)\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/StartupUI/otioimporter/__init__.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n__author__ = \"Daniel Flehner Heen\"\n__credits__ = [\"Jakub Jezek\", \"Daniel Flehner Heen\"]\n\nimport hiero.ui\nimport hiero.core\n\nimport PySide2.QtWidgets as qw\n\nfrom openpype.hosts.hiero.api.otio.hiero_import import load_otio\n\n\nclass OTIOProjectSelect(qw.QDialog):\n\n    def __init__(self, projects, *args, **kwargs):\n        super(OTIOProjectSelect, self).__init__(*args, **kwargs)\n        self.setWindowTitle(\"Please select active project\")\n        self.layout = qw.QVBoxLayout()\n\n        self.label = qw.QLabel(\n            \"Unable to determine which project to import sequence to.\\n\"\n            \"Please select one.\"\n        )\n        self.layout.addWidget(self.label)\n\n        self.projects = qw.QComboBox()\n        self.projects.addItems(map(lambda p: p.name(), projects))\n        self.layout.addWidget(self.projects)\n\n        QBtn = qw.QDialogButtonBox.Ok | qw.QDialogButtonBox.Cancel\n        self.buttonBox = qw.QDialogButtonBox(QBtn)\n        self.buttonBox.accepted.connect(self.accept)\n        self.buttonBox.rejected.connect(self.reject)\n\n        self.layout.addWidget(self.buttonBox)\n        self.setLayout(self.layout)\n\n\ndef get_sequence(view):\n    sequence = None\n    if isinstance(view, hiero.ui.TimelineEditor):\n        sequence = view.sequence()\n\n    elif isinstance(view, hiero.ui.BinView):\n        for item in view.selection():\n            if not hasattr(item, \"acitveItem\"):\n                continue\n\n            if isinstance(item.activeItem(), hiero.core.Sequence):\n                sequence = item.activeItem()\n\n    return sequence\n\n\ndef OTIO_menu_action(event):\n    # Menu actions\n    otio_import_action = hiero.ui.createMenuAction(\n        \"Import OTIO...\",\n        open_otio_file,\n        icon=None\n    )\n\n    otio_add_track_action = hiero.ui.createMenuAction(\n        \"New Track(s) from OTIO...\",\n        open_otio_file,\n        icon=None\n    )\n    otio_add_track_action.setEnabled(False)\n\n    hiero.ui.registerAction(otio_import_action)\n    hiero.ui.registerAction(otio_add_track_action)\n\n    view = hiero.ui.currentContextMenuView()\n\n    if view:\n        sequence = get_sequence(view)\n        if sequence:\n            otio_add_track_action.setEnabled(True)\n\n    for action in event.menu.actions():\n        if action.text() == \"Import\":\n            action.menu().addAction(otio_import_action)\n            action.menu().addAction(otio_add_track_action)\n\n        elif action.text() == \"New Track\":\n            action.menu().addAction(otio_add_track_action)\n\n\ndef open_otio_file():\n    files = hiero.ui.openFileBrowser(\n        caption=\"Please select an OTIO file of choice\",\n        pattern=\"*.otio\",\n        requiredExtension=\".otio\"\n    )\n\n    selection = None\n    sequence = None\n\n    view = hiero.ui.currentContextMenuView()\n    if view:\n        sequence = get_sequence(view)\n        selection = view.selection()\n\n    if sequence:\n        project = sequence.project()\n\n    elif selection:\n        project = selection[0].project()\n\n    elif len(hiero.core.projects()) > 1:\n        dialog = OTIOProjectSelect(hiero.core.projects())\n        if dialog.exec_():\n            project = hiero.core.projects()[dialog.projects.currentIndex()]\n\n        else:\n            bar = hiero.ui.mainWindow().statusBar()\n            bar.showMessage(\n                \"OTIO Import aborted by user\",\n                timeout=3000\n            )\n            return\n\n    else:\n        project = hiero.core.projects()[-1]\n\n    for otio_file in files:\n        load_otio(otio_file, project, sequence)\n\n\n# HieroPlayer is quite limited and can't create transitions etc.\nif not hiero.core.isHieroPlayer():\n    hiero.core.events.registerInterest(\n        \"kShowContextMenu/kBin\",\n        OTIO_menu_action\n    )\n    hiero.core.events.registerInterest(\n        \"kShowContextMenu/kTimeline\",\n        OTIO_menu_action\n    )\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/Python/StartupUI/setPosterFrame.py",
    "content": "import hiero.core\nimport hiero.ui\ntry:\n    from PySide.QtGui import *\n    from PySide.QtCore import *\nexcept:\n    from PySide2.QtGui import *\n    from PySide2.QtWidgets import *\n    from PySide2.QtCore import *\n\n\ndef setPosterFrame(posterFrame=.5):\n    \"\"\"\n    Update the poster frame of the given clipItmes\n    posterFrame = .5 uses the centre frame, a value of 0 uses the first frame, a value of 1 uses the last frame\n    \"\"\"\n    view = hiero.ui.activeView()\n\n    selectedBinItems = view.selection()\n    selectedClipItems = [(item.activeItem()\n                          if hasattr(item, \"activeItem\") else item)\n                         for item in selectedBinItems]\n\n    for clip in selectedClipItems:\n        centreFrame = int(clip.duration() * posterFrame)\n        clip.setPosterFrame(centreFrame)\n\n\nclass SetPosterFrameAction(QAction):\n    def __init__(self):\n        QAction.__init__(self, \"Set Poster Frame (centre)\", None)\n        self._selection = None\n\n        self.triggered.connect(lambda: setPosterFrame(.5))\n        hiero.core.events.registerInterest(\"kShowContextMenu/kBin\",\n                                           self.eventHandler)\n\n    def eventHandler(self, event):\n        view = event.sender\n        # Add the Menu to the right-click menu\n        event.menu.addAction(self)\n\n\n# The act of initialising the action adds it to the right-click menu...\nSetPosterFrameAction()\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/TaskPresets/10.5/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml",
    "content": "<root presetname=\"pipeline\" tasktype=\"hiero.exporters.FnShotProcessor.ShotProcessor\">\n    <startFrameIndex valuetype=\"int\">991</startFrameIndex>\n    <exportRoot valuetype=\"str\">//10.11.0.184/171001_ftrack/tgbvfx/editorial/hiero/workspace/</exportRoot>\n    <versionIndex valuetype=\"int\">1</versionIndex>\n    <cutUseHandles valuetype=\"bool\">True</cutUseHandles>\n    <versionPadding valuetype=\"int\">3</versionPadding>\n    <exportTemplate valuetype=\"list\">\n        <SequenceItem valuetype=\"tuple\">\n            <SequenceItem valuetype=\"str\">{shot}/editorial_raw.%04d.{fileext}</SequenceItem>\n            <SequenceItem valuetype=\"hiero.exporters.FnSymLinkExporter.SymLinkPreset\">\n                <root presetname=\"hiero.exporters.FnSymLinkExporter.SymLinkExporter\" tasktype=\"hiero.exporters.FnSymLinkExporter.SymLinkExporter\">\n                    <colourspace valuetype=\"str\">default</colourspace>\n                    <file_type valuetype=\"unicode\">exr</file_type>\n                    <readAllLinesForExport valuetype=\"bool\">False</readAllLinesForExport>\n                    <channels valuetype=\"str\">all</channels>\n                    <includeAudio valuetype=\"bool\">False</includeAudio>\n                    <burninDataEnabled valuetype=\"bool\">False</burninDataEnabled>\n                    <useSingleSocket valuetype=\"bool\">False</useSingleSocket>\n                    <additionalNodesEnabled valuetype=\"bool\">False</additionalNodesEnabled>\n                    <deleteAudio valuetype=\"bool\">True</deleteAudio>\n                    <additionalNodesData valuetype=\"list\" />\n                    <dpx valuetype=\"dict\">\n                        <datatype valuetype=\"str\">8 bit</datatype>\n                        <transfer valuetype=\"str\">(auto detect)</transfer>\n                        <bigEndian valuetype=\"bool\">True</bigEndian>\n                        <fill valuetype=\"bool\">False</fill>\n                    </dpx>\n                    <includeEffects valuetype=\"bool\">False</includeEffects>\n                    <burninData valuetype=\"dict\">\n                        <burnIn_bottomRight valuetype=\"NoneType\">None</burnIn_bottomRight>\n                        <burnIn_topLeft valuetype=\"NoneType\">None</burnIn_topLeft>\n                        <burnIn_topMiddle valuetype=\"NoneType\">None</burnIn_topMiddle>\n                        <burnIn_padding valuetype=\"NoneType\">None</burnIn_padding>\n                        <burnIn_topRight valuetype=\"NoneType\">None</burnIn_topRight>\n                        <burnIn_textSize valuetype=\"NoneType\">None</burnIn_textSize>\n                        <burnIn_bottomLeft valuetype=\"NoneType\">None</burnIn_bottomLeft>\n                        <burnIn_bottomMiddle valuetype=\"NoneType\">None</burnIn_bottomMiddle>\n                        <burnIn_font valuetype=\"NoneType\">None</burnIn_font>\n                    </burninData>\n                    <exr valuetype=\"dict\">\n                        <compression valuetype=\"str\">Zip (16 scanline)</compression>\n                        <datatype valuetype=\"str\">32 bit float</datatype>\n                        <noprefix valuetype=\"bool\">False</noprefix>\n                        <write_full_layer_names valuetype=\"bool\">False</write_full_layer_names>\n                        <standard_layer_name_format valuetype=\"bool\">False</standard_layer_name_format>\n                        <interleave valuetype=\"str\">channels, layers and views</interleave>\n                        <dw_compression_level valuetype=\"float\">45.0</dw_compression_level>\n                        <truncateChannelNames valuetype=\"bool\">False</truncateChannelNames>\n                        <metadata valuetype=\"str\">all metadata</metadata>\n                    </exr>\n                    <writeNodeName valuetype=\"str\">Write_{ext}</writeNodeName>\n                    <reformat valuetype=\"dict\">\n                        <filter valuetype=\"str\">Cubic</filter>\n                        <to_type valuetype=\"str\">None</to_type>\n                        <scale valuetype=\"float\">1.0</scale>\n                        <center valuetype=\"bool\">True</center>\n                        <resize valuetype=\"str\">width</resize>\n                    </reformat>\n                    <keepNukeScript valuetype=\"bool\">False</keepNukeScript>\n                    <method valuetype=\"str\">Blend</method>\n                </root>\n            </SequenceItem>\n        </SequenceItem>\n        <SequenceItem valuetype=\"tuple\">\n            <SequenceItem valuetype=\"str\">{shot}/editorial.%04d.{ext}</SequenceItem>\n            <SequenceItem valuetype=\"hiero.exporters.FnTranscodeExporter.TranscodePreset\">\n                <root presetname=\"hiero.exporters.FnTranscodeExporter.TranscodeExporter\" tasktype=\"hiero.exporters.FnTranscodeExporter.TranscodeExporter\">\n                    <colourspace valuetype=\"str\">default</colourspace>\n                    <file_type valuetype=\"unicode\">exr</file_type>\n                    <readAllLinesForExport valuetype=\"bool\">False</readAllLinesForExport>\n                    <channels valuetype=\"str\">all</channels>\n                    <includeAudio valuetype=\"bool\">False</includeAudio>\n                    <burninDataEnabled valuetype=\"bool\">False</burninDataEnabled>\n                    <useSingleSocket valuetype=\"bool\">False</useSingleSocket>\n                    <additionalNodesEnabled valuetype=\"bool\">False</additionalNodesEnabled>\n                    <deleteAudio valuetype=\"bool\">True</deleteAudio>\n                    <additionalNodesData valuetype=\"list\" />\n                    <dpx valuetype=\"dict\">\n                        <datatype valuetype=\"str\">8 bit</datatype>\n                        <transfer valuetype=\"str\">(auto detect)</transfer>\n                        <bigEndian valuetype=\"bool\">True</bigEndian>\n                        <fill valuetype=\"bool\">False</fill>\n                    </dpx>\n                    <includeEffects valuetype=\"bool\">True</includeEffects>\n                    <burninData valuetype=\"dict\">\n                        <burnIn_bottomRight valuetype=\"NoneType\">None</burnIn_bottomRight>\n                        <burnIn_topLeft valuetype=\"NoneType\">None</burnIn_topLeft>\n                        <burnIn_topMiddle valuetype=\"NoneType\">None</burnIn_topMiddle>\n                        <burnIn_padding valuetype=\"NoneType\">None</burnIn_padding>\n                        <burnIn_topRight valuetype=\"NoneType\">None</burnIn_topRight>\n                        <burnIn_textSize valuetype=\"NoneType\">None</burnIn_textSize>\n                        <burnIn_bottomLeft valuetype=\"NoneType\">None</burnIn_bottomLeft>\n                        <burnIn_bottomMiddle valuetype=\"NoneType\">None</burnIn_bottomMiddle>\n                        <burnIn_font valuetype=\"NoneType\">None</burnIn_font>\n                    </burninData>\n                    <exr valuetype=\"dict\">\n                        <compression valuetype=\"str\">Zip (16 scanline)</compression>\n                        <datatype valuetype=\"str\">16 bit half</datatype>\n                        <noprefix valuetype=\"bool\">False</noprefix>\n                        <write_full_layer_names valuetype=\"bool\">False</write_full_layer_names>\n                        <standard_layer_name_format valuetype=\"bool\">False</standard_layer_name_format>\n                        <interleave valuetype=\"str\">channels, layers and views</interleave>\n                        <dw_compression_level valuetype=\"float\">45.0</dw_compression_level>\n                        <truncateChannelNames valuetype=\"bool\">False</truncateChannelNames>\n                        <metadata valuetype=\"str\">all metadata</metadata>\n                    </exr>\n                    <writeNodeName valuetype=\"str\">Write_{ext}</writeNodeName>\n                    <reformat valuetype=\"dict\">\n                        <filter valuetype=\"str\">Cubic</filter>\n                        <to_type valuetype=\"str\">To Sequence Resolution</to_type>\n                        <scale valuetype=\"float\">1.0</scale>\n                        <center valuetype=\"bool\">True</center>\n                        <resize valuetype=\"str\">width</resize>\n                    </reformat>\n                    <keepNukeScript valuetype=\"bool\">False</keepNukeScript>\n                    <method valuetype=\"str\">Blend</method>\n                </root>\n            </SequenceItem>\n        </SequenceItem>\n        <SequenceItem valuetype=\"tuple\">\n            <SequenceItem valuetype=\"str\">{shot}/editorial.nk</SequenceItem>\n            <SequenceItem valuetype=\"hiero.exporters.FnNukeShotExporter.NukeShotPreset\">\n                <root presetname=\"hiero.exporters.FnNukeShotExporter.NukeShotExporter\" tasktype=\"hiero.exporters.FnNukeShotExporter.NukeShotExporter\">\n                    <postProcessScript valuetype=\"bool\">True</postProcessScript>\n                    <colourspace valuetype=\"str\">default</colourspace>\n                    <file_type valuetype=\"unicode\">mov</file_type>\n                    <annotationsPreCompPaths valuetype=\"list\" />\n                    <channels valuetype=\"str\">rgb</channels>\n                    <includeAudio valuetype=\"bool\">False</includeAudio>\n                    <readPaths valuetype=\"list\" />\n                    <connectTracks valuetype=\"bool\">False</connectTracks>\n                    <useSingleSocket valuetype=\"bool\">False</useSingleSocket>\n                    <collateSequence valuetype=\"bool\">False</collateSequence>\n                    <additionalNodesData valuetype=\"list\" />\n                    <collateShotNames valuetype=\"bool\">True</collateShotNames>\n                    <includeEffects valuetype=\"bool\">True</includeEffects>\n                    <writePaths valuetype=\"list\">\n                        <SequenceItem valuetype=\"str\">{shot}/editorial_raw.%04d.{fileext}</SequenceItem>\n                    </writePaths>\n                    <reformat valuetype=\"dict\">\n                        <filter valuetype=\"str\">Cubic</filter>\n                        <to_type valuetype=\"str\">None</to_type>\n                        <scale valuetype=\"float\">1.0</scale>\n                        <center valuetype=\"bool\">True</center>\n                        <resize valuetype=\"str\">width</resize>\n                    </reformat>\n                    <keepNukeScript valuetype=\"bool\">False</keepNukeScript>\n                    <method valuetype=\"str\">Blend</method>\n                    <includeAnnotations valuetype=\"bool\">False</includeAnnotations>\n                    <enable valuetype=\"bool\">True</enable>\n                    <showAnnotations valuetype=\"bool\">True</showAnnotations>\n                    <mov valuetype=\"dict\">\n                        <b_frames valuetype=\"int\">0</b_frames>\n                        <bitrate_tolerance valuetype=\"int\">40000000</bitrate_tolerance>\n                        <gop_size valuetype=\"int\">12</gop_size>\n                        <quality_max valuetype=\"int\">31</quality_max>\n                        <quality_min valuetype=\"int\">2</quality_min>\n                        <codec valuetype=\"str\">avc1&#x09;H.264</codec>\n                        <ycbcr_matrix_type valuetype=\"str\">Auto</ycbcr_matrix_type>\n                        <encoder valuetype=\"str\">mov32</encoder>\n                        <bitrate valuetype=\"int\">20000</bitrate>\n                    </mov>\n                    <readAllLinesForExport valuetype=\"bool\">False</readAllLinesForExport>\n                    <deleteAudio valuetype=\"bool\">True</deleteAudio>\n                    <collateCustomStart valuetype=\"bool\">True</collateCustomStart>\n                    <burninDataEnabled valuetype=\"bool\">False</burninDataEnabled>\n                    <additionalNodesEnabled valuetype=\"bool\">False</additionalNodesEnabled>\n                    <timelineWriteNode valuetype=\"str\">{shot}/editorial_raw.%04d.{fileext}</timelineWriteNode>\n                    <burninData valuetype=\"dict\">\n                        <burnIn_bottomRight valuetype=\"NoneType\">None</burnIn_bottomRight>\n                        <burnIn_topLeft valuetype=\"NoneType\">None</burnIn_topLeft>\n                        <burnIn_topMiddle valuetype=\"NoneType\">None</burnIn_topMiddle>\n                        <burnIn_padding valuetype=\"NoneType\">None</burnIn_padding>\n                        <burnIn_topRight valuetype=\"NoneType\">None</burnIn_topRight>\n                        <burnIn_bottomMiddle valuetype=\"NoneType\">None</burnIn_bottomMiddle>\n                        <burnIn_bottomLeft valuetype=\"NoneType\">None</burnIn_bottomLeft>\n                        <burnIn_textSize valuetype=\"NoneType\">None</burnIn_textSize>\n                        <burnIn_font valuetype=\"NoneType\">None</burnIn_font>\n                    </burninData>\n                    <dpx valuetype=\"dict\">\n                        <datatype valuetype=\"str\">8 bit</datatype>\n                        <transfer valuetype=\"str\">(auto detect)</transfer>\n                        <bigEndian valuetype=\"bool\">True</bigEndian>\n                        <fill valuetype=\"bool\">False</fill>\n                    </dpx>\n                    <writeNodeName valuetype=\"str\">Write_{ext}</writeNodeName>\n                    <collateTracks valuetype=\"bool\">False</collateTracks>\n                </root>\n            </SequenceItem>\n        </SequenceItem>\n    </exportTemplate>\n    <excludeTags valuetype=\"list\" />\n    <includeTags valuetype=\"list\" />\n    <includeRetimes valuetype=\"bool\">False</includeRetimes>\n    <startFrameSource valuetype=\"str\">Custom</startFrameSource>\n    <cutLength valuetype=\"bool\">True</cutLength>\n    <cutHandles valuetype=\"int\">10</cutHandles>\n</root>\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/TaskPresets/11.1/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml",
    "content": "<root presetname=\"pipeline\" tasktype=\"hiero.exporters.FnShotProcessor.ShotProcessor\">\n    <startFrameIndex valuetype=\"int\">991</startFrameIndex>\n    <exportRoot valuetype=\"str\">//10.11.0.184/171001_ftrack/tgbvfx/editorial/hiero/workspace/</exportRoot>\n    <versionIndex valuetype=\"int\">1</versionIndex>\n    <cutUseHandles valuetype=\"bool\">True</cutUseHandles>\n    <versionPadding valuetype=\"int\">3</versionPadding>\n    <exportTemplate valuetype=\"list\">\n        <SequenceItem valuetype=\"tuple\">\n            <SequenceItem valuetype=\"str\">{shot}/editorial_raw.%04d.{fileext}</SequenceItem>\n            <SequenceItem valuetype=\"hiero.exporters.FnSymLinkExporter.SymLinkPreset\">\n                <root presetname=\"hiero.exporters.FnSymLinkExporter.SymLinkExporter\" tasktype=\"hiero.exporters.FnSymLinkExporter.SymLinkExporter\">\n                    <colourspace valuetype=\"str\">default</colourspace>\n                    <file_type valuetype=\"unicode\">exr</file_type>\n                    <readAllLinesForExport valuetype=\"bool\">False</readAllLinesForExport>\n                    <channels valuetype=\"str\">all</channels>\n                    <includeAudio valuetype=\"bool\">False</includeAudio>\n                    <burninDataEnabled valuetype=\"bool\">False</burninDataEnabled>\n                    <useSingleSocket valuetype=\"bool\">False</useSingleSocket>\n                    <additionalNodesEnabled valuetype=\"bool\">False</additionalNodesEnabled>\n                    <deleteAudio valuetype=\"bool\">True</deleteAudio>\n                    <additionalNodesData valuetype=\"list\" />\n                    <dpx valuetype=\"dict\">\n                        <datatype valuetype=\"str\">8 bit</datatype>\n                        <transfer valuetype=\"str\">(auto detect)</transfer>\n                        <bigEndian valuetype=\"bool\">True</bigEndian>\n                        <fill valuetype=\"bool\">False</fill>\n                    </dpx>\n                    <includeEffects valuetype=\"bool\">False</includeEffects>\n                    <burninData valuetype=\"dict\">\n                        <burnIn_bottomRight valuetype=\"NoneType\">None</burnIn_bottomRight>\n                        <burnIn_topLeft valuetype=\"NoneType\">None</burnIn_topLeft>\n                        <burnIn_topMiddle valuetype=\"NoneType\">None</burnIn_topMiddle>\n                        <burnIn_padding valuetype=\"NoneType\">None</burnIn_padding>\n                        <burnIn_topRight valuetype=\"NoneType\">None</burnIn_topRight>\n                        <burnIn_textSize valuetype=\"NoneType\">None</burnIn_textSize>\n                        <burnIn_bottomLeft valuetype=\"NoneType\">None</burnIn_bottomLeft>\n                        <burnIn_bottomMiddle valuetype=\"NoneType\">None</burnIn_bottomMiddle>\n                        <burnIn_font valuetype=\"NoneType\">None</burnIn_font>\n                    </burninData>\n                    <exr valuetype=\"dict\">\n                        <compression valuetype=\"str\">Zip (16 scanline)</compression>\n                        <datatype valuetype=\"str\">32 bit float</datatype>\n                        <noprefix valuetype=\"bool\">False</noprefix>\n                        <write_full_layer_names valuetype=\"bool\">False</write_full_layer_names>\n                        <standard_layer_name_format valuetype=\"bool\">False</standard_layer_name_format>\n                        <interleave valuetype=\"str\">channels, layers and views</interleave>\n                        <dw_compression_level valuetype=\"float\">45.0</dw_compression_level>\n                        <truncateChannelNames valuetype=\"bool\">False</truncateChannelNames>\n                        <metadata valuetype=\"str\">all metadata</metadata>\n                    </exr>\n                    <writeNodeName valuetype=\"str\">Write_{ext}</writeNodeName>\n                    <reformat valuetype=\"dict\">\n                        <filter valuetype=\"str\">Cubic</filter>\n                        <to_type valuetype=\"str\">None</to_type>\n                        <scale valuetype=\"float\">1.0</scale>\n                        <center valuetype=\"bool\">True</center>\n                        <resize valuetype=\"str\">width</resize>\n                    </reformat>\n                    <keepNukeScript valuetype=\"bool\">False</keepNukeScript>\n                    <method valuetype=\"str\">Blend</method>\n                </root>\n            </SequenceItem>\n        </SequenceItem>\n        <SequenceItem valuetype=\"tuple\">\n            <SequenceItem valuetype=\"str\">{shot}/editorial.%04d.{ext}</SequenceItem>\n            <SequenceItem valuetype=\"hiero.exporters.FnTranscodeExporter.TranscodePreset\">\n                <root presetname=\"hiero.exporters.FnTranscodeExporter.TranscodeExporter\" tasktype=\"hiero.exporters.FnTranscodeExporter.TranscodeExporter\">\n                    <colourspace valuetype=\"str\">default</colourspace>\n                    <file_type valuetype=\"unicode\">exr</file_type>\n                    <readAllLinesForExport valuetype=\"bool\">False</readAllLinesForExport>\n                    <channels valuetype=\"str\">all</channels>\n                    <includeAudio valuetype=\"bool\">False</includeAudio>\n                    <burninDataEnabled valuetype=\"bool\">False</burninDataEnabled>\n                    <useSingleSocket valuetype=\"bool\">False</useSingleSocket>\n                    <additionalNodesEnabled valuetype=\"bool\">False</additionalNodesEnabled>\n                    <deleteAudio valuetype=\"bool\">True</deleteAudio>\n                    <additionalNodesData valuetype=\"list\" />\n                    <dpx valuetype=\"dict\">\n                        <datatype valuetype=\"str\">8 bit</datatype>\n                        <transfer valuetype=\"str\">(auto detect)</transfer>\n                        <bigEndian valuetype=\"bool\">True</bigEndian>\n                        <fill valuetype=\"bool\">False</fill>\n                    </dpx>\n                    <includeEffects valuetype=\"bool\">True</includeEffects>\n                    <burninData valuetype=\"dict\">\n                        <burnIn_bottomRight valuetype=\"NoneType\">None</burnIn_bottomRight>\n                        <burnIn_topLeft valuetype=\"NoneType\">None</burnIn_topLeft>\n                        <burnIn_topMiddle valuetype=\"NoneType\">None</burnIn_topMiddle>\n                        <burnIn_padding valuetype=\"NoneType\">None</burnIn_padding>\n                        <burnIn_topRight valuetype=\"NoneType\">None</burnIn_topRight>\n                        <burnIn_textSize valuetype=\"NoneType\">None</burnIn_textSize>\n                        <burnIn_bottomLeft valuetype=\"NoneType\">None</burnIn_bottomLeft>\n                        <burnIn_bottomMiddle valuetype=\"NoneType\">None</burnIn_bottomMiddle>\n                        <burnIn_font valuetype=\"NoneType\">None</burnIn_font>\n                    </burninData>\n                    <exr valuetype=\"dict\">\n                        <compression valuetype=\"str\">Zip (16 scanline)</compression>\n                        <datatype valuetype=\"str\">16 bit half</datatype>\n                        <noprefix valuetype=\"bool\">False</noprefix>\n                        <write_full_layer_names valuetype=\"bool\">False</write_full_layer_names>\n                        <standard_layer_name_format valuetype=\"bool\">False</standard_layer_name_format>\n                        <interleave valuetype=\"str\">channels, layers and views</interleave>\n                        <dw_compression_level valuetype=\"float\">45.0</dw_compression_level>\n                        <truncateChannelNames valuetype=\"bool\">False</truncateChannelNames>\n                        <metadata valuetype=\"str\">all metadata</metadata>\n                    </exr>\n                    <writeNodeName valuetype=\"str\">Write_{ext}</writeNodeName>\n                    <reformat valuetype=\"dict\">\n                        <filter valuetype=\"str\">Cubic</filter>\n                        <to_type valuetype=\"str\">To Sequence Resolution</to_type>\n                        <scale valuetype=\"float\">1.0</scale>\n                        <center valuetype=\"bool\">True</center>\n                        <resize valuetype=\"str\">width</resize>\n                    </reformat>\n                    <keepNukeScript valuetype=\"bool\">False</keepNukeScript>\n                    <method valuetype=\"str\">Blend</method>\n                </root>\n            </SequenceItem>\n        </SequenceItem>\n        <SequenceItem valuetype=\"tuple\">\n            <SequenceItem valuetype=\"str\">{shot}/editorial.nk</SequenceItem>\n            <SequenceItem valuetype=\"hiero.exporters.FnNukeShotExporter.NukeShotPreset\">\n                <root presetname=\"hiero.exporters.FnNukeShotExporter.NukeShotExporter\" tasktype=\"hiero.exporters.FnNukeShotExporter.NukeShotExporter\">\n                    <postProcessScript valuetype=\"bool\">True</postProcessScript>\n                    <colourspace valuetype=\"str\">default</colourspace>\n                    <file_type valuetype=\"unicode\">mov</file_type>\n                    <annotationsPreCompPaths valuetype=\"list\" />\n                    <channels valuetype=\"str\">rgb</channels>\n                    <includeAudio valuetype=\"bool\">False</includeAudio>\n                    <readPaths valuetype=\"list\" />\n                    <connectTracks valuetype=\"bool\">False</connectTracks>\n                    <useSingleSocket valuetype=\"bool\">False</useSingleSocket>\n                    <collateSequence valuetype=\"bool\">False</collateSequence>\n                    <additionalNodesData valuetype=\"list\" />\n                    <collateShotNames valuetype=\"bool\">True</collateShotNames>\n                    <includeEffects valuetype=\"bool\">True</includeEffects>\n                    <writePaths valuetype=\"list\">\n                        <SequenceItem valuetype=\"str\">{shot}/editorial_raw.%04d.{fileext}</SequenceItem>\n                    </writePaths>\n                    <reformat valuetype=\"dict\">\n                        <filter valuetype=\"str\">Cubic</filter>\n                        <to_type valuetype=\"str\">None</to_type>\n                        <scale valuetype=\"float\">1.0</scale>\n                        <center valuetype=\"bool\">True</center>\n                        <resize valuetype=\"str\">width</resize>\n                    </reformat>\n                    <keepNukeScript valuetype=\"bool\">False</keepNukeScript>\n                    <method valuetype=\"str\">Blend</method>\n                    <includeAnnotations valuetype=\"bool\">False</includeAnnotations>\n                    <enable valuetype=\"bool\">True</enable>\n                    <showAnnotations valuetype=\"bool\">True</showAnnotations>\n                    <mov valuetype=\"dict\">\n                        <b_frames valuetype=\"int\">0</b_frames>\n                        <bitrate_tolerance valuetype=\"int\">40000000</bitrate_tolerance>\n                        <gop_size valuetype=\"int\">12</gop_size>\n                        <quality_max valuetype=\"int\">31</quality_max>\n                        <quality_min valuetype=\"int\">2</quality_min>\n                        <codec valuetype=\"str\">avc1&#x09;H.264</codec>\n                        <ycbcr_matrix_type valuetype=\"str\">Auto</ycbcr_matrix_type>\n                        <encoder valuetype=\"str\">mov32</encoder>\n                        <bitrate valuetype=\"int\">20000</bitrate>\n                    </mov>\n                    <readAllLinesForExport valuetype=\"bool\">False</readAllLinesForExport>\n                    <deleteAudio valuetype=\"bool\">True</deleteAudio>\n                    <collateCustomStart valuetype=\"bool\">True</collateCustomStart>\n                    <burninDataEnabled valuetype=\"bool\">False</burninDataEnabled>\n                    <additionalNodesEnabled valuetype=\"bool\">False</additionalNodesEnabled>\n                    <timelineWriteNode valuetype=\"str\">{shot}/editorial_raw.%04d.{fileext}</timelineWriteNode>\n                    <burninData valuetype=\"dict\">\n                        <burnIn_bottomRight valuetype=\"NoneType\">None</burnIn_bottomRight>\n                        <burnIn_topLeft valuetype=\"NoneType\">None</burnIn_topLeft>\n                        <burnIn_topMiddle valuetype=\"NoneType\">None</burnIn_topMiddle>\n                        <burnIn_padding valuetype=\"NoneType\">None</burnIn_padding>\n                        <burnIn_topRight valuetype=\"NoneType\">None</burnIn_topRight>\n                        <burnIn_bottomMiddle valuetype=\"NoneType\">None</burnIn_bottomMiddle>\n                        <burnIn_bottomLeft valuetype=\"NoneType\">None</burnIn_bottomLeft>\n                        <burnIn_textSize valuetype=\"NoneType\">None</burnIn_textSize>\n                        <burnIn_font valuetype=\"NoneType\">None</burnIn_font>\n                    </burninData>\n                    <dpx valuetype=\"dict\">\n                        <datatype valuetype=\"str\">8 bit</datatype>\n                        <transfer valuetype=\"str\">(auto detect)</transfer>\n                        <bigEndian valuetype=\"bool\">True</bigEndian>\n                        <fill valuetype=\"bool\">False</fill>\n                    </dpx>\n                    <writeNodeName valuetype=\"str\">Write_{ext}</writeNodeName>\n                    <collateTracks valuetype=\"bool\">False</collateTracks>\n                </root>\n            </SequenceItem>\n        </SequenceItem>\n    </exportTemplate>\n    <excludeTags valuetype=\"list\" />\n    <includeTags valuetype=\"list\" />\n    <includeRetimes valuetype=\"bool\">False</includeRetimes>\n    <startFrameSource valuetype=\"str\">Custom</startFrameSource>\n    <cutLength valuetype=\"bool\">True</cutLength>\n    <cutHandles valuetype=\"int\">10</cutHandles>\n</root>\n"
  },
  {
    "path": "openpype/hosts/hiero/api/startup/TaskPresets/11.2/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml",
    "content": "<root presetname=\"pipeline\" tasktype=\"hiero.exporters.FnShotProcessor.ShotProcessor\">\n    <startFrameIndex valuetype=\"int\">991</startFrameIndex>\n    <exportRoot valuetype=\"str\">//10.11.0.184/171001_ftrack/tgbvfx/editorial/hiero/workspace/</exportRoot>\n    <versionIndex valuetype=\"int\">1</versionIndex>\n    <cutUseHandles valuetype=\"bool\">True</cutUseHandles>\n    <versionPadding valuetype=\"int\">3</versionPadding>\n    <exportTemplate valuetype=\"list\">\n        <SequenceItem valuetype=\"tuple\">\n            <SequenceItem valuetype=\"str\">{shot}/editorial_raw.%04d.{fileext}</SequenceItem>\n            <SequenceItem valuetype=\"hiero.exporters.FnSymLinkExporter.SymLinkPreset\">\n                <root presetname=\"hiero.exporters.FnSymLinkExporter.SymLinkExporter\" tasktype=\"hiero.exporters.FnSymLinkExporter.SymLinkExporter\">\n                    <colourspace valuetype=\"str\">default</colourspace>\n                    <file_type valuetype=\"unicode\">exr</file_type>\n                    <readAllLinesForExport valuetype=\"bool\">False</readAllLinesForExport>\n                    <channels valuetype=\"str\">all</channels>\n                    <includeAudio valuetype=\"bool\">False</includeAudio>\n                    <burninDataEnabled valuetype=\"bool\">False</burninDataEnabled>\n                    <useSingleSocket valuetype=\"bool\">False</useSingleSocket>\n                    <additionalNodesEnabled valuetype=\"bool\">False</additionalNodesEnabled>\n                    <deleteAudio valuetype=\"bool\">True</deleteAudio>\n                    <additionalNodesData valuetype=\"list\" />\n                    <dpx valuetype=\"dict\">\n                        <datatype valuetype=\"str\">8 bit</datatype>\n                        <transfer valuetype=\"str\">(auto detect)</transfer>\n                        <bigEndian valuetype=\"bool\">True</bigEndian>\n                        <fill valuetype=\"bool\">False</fill>\n                    </dpx>\n                    <includeEffects valuetype=\"bool\">False</includeEffects>\n                    <burninData valuetype=\"dict\">\n                        <burnIn_bottomRight valuetype=\"NoneType\">None</burnIn_bottomRight>\n                        <burnIn_topLeft valuetype=\"NoneType\">None</burnIn_topLeft>\n                        <burnIn_topMiddle valuetype=\"NoneType\">None</burnIn_topMiddle>\n                        <burnIn_padding valuetype=\"NoneType\">None</burnIn_padding>\n                        <burnIn_topRight valuetype=\"NoneType\">None</burnIn_topRight>\n                        <burnIn_textSize valuetype=\"NoneType\">None</burnIn_textSize>\n                        <burnIn_bottomLeft valuetype=\"NoneType\">None</burnIn_bottomLeft>\n                        <burnIn_bottomMiddle valuetype=\"NoneType\">None</burnIn_bottomMiddle>\n                        <burnIn_font valuetype=\"NoneType\">None</burnIn_font>\n                    </burninData>\n                    <exr valuetype=\"dict\">\n                        <compression valuetype=\"str\">Zip (16 scanline)</compression>\n                        <datatype valuetype=\"str\">32 bit float</datatype>\n                        <noprefix valuetype=\"bool\">False</noprefix>\n                        <write_full_layer_names valuetype=\"bool\">False</write_full_layer_names>\n                        <standard_layer_name_format valuetype=\"bool\">False</standard_layer_name_format>\n                        <interleave valuetype=\"str\">channels, layers and views</interleave>\n                        <dw_compression_level valuetype=\"float\">45.0</dw_compression_level>\n                        <truncateChannelNames valuetype=\"bool\">False</truncateChannelNames>\n                        <metadata valuetype=\"str\">all metadata</metadata>\n                    </exr>\n                    <writeNodeName valuetype=\"str\">Write_{ext}</writeNodeName>\n                    <reformat valuetype=\"dict\">\n                        <filter valuetype=\"str\">Cubic</filter>\n                        <to_type valuetype=\"str\">None</to_type>\n                        <scale valuetype=\"float\">1.0</scale>\n                        <center valuetype=\"bool\">True</center>\n                        <resize valuetype=\"str\">width</resize>\n                    </reformat>\n                    <keepNukeScript valuetype=\"bool\">False</keepNukeScript>\n                    <method valuetype=\"str\">Blend</method>\n                </root>\n            </SequenceItem>\n        </SequenceItem>\n        <SequenceItem valuetype=\"tuple\">\n            <SequenceItem valuetype=\"str\">{shot}/editorial.%04d.{ext}</SequenceItem>\n            <SequenceItem valuetype=\"hiero.exporters.FnTranscodeExporter.TranscodePreset\">\n                <root presetname=\"hiero.exporters.FnTranscodeExporter.TranscodeExporter\" tasktype=\"hiero.exporters.FnTranscodeExporter.TranscodeExporter\">\n                    <colourspace valuetype=\"str\">default</colourspace>\n                    <file_type valuetype=\"unicode\">exr</file_type>\n                    <readAllLinesForExport valuetype=\"bool\">False</readAllLinesForExport>\n                    <channels valuetype=\"str\">all</channels>\n                    <includeAudio valuetype=\"bool\">False</includeAudio>\n                    <burninDataEnabled valuetype=\"bool\">False</burninDataEnabled>\n                    <useSingleSocket valuetype=\"bool\">False</useSingleSocket>\n                    <additionalNodesEnabled valuetype=\"bool\">False</additionalNodesEnabled>\n                    <deleteAudio valuetype=\"bool\">True</deleteAudio>\n                    <additionalNodesData valuetype=\"list\" />\n                    <dpx valuetype=\"dict\">\n                        <datatype valuetype=\"str\">8 bit</datatype>\n                        <transfer valuetype=\"str\">(auto detect)</transfer>\n                        <bigEndian valuetype=\"bool\">True</bigEndian>\n                        <fill valuetype=\"bool\">False</fill>\n                    </dpx>\n                    <includeEffects valuetype=\"bool\">True</includeEffects>\n                    <burninData valuetype=\"dict\">\n                        <burnIn_bottomRight valuetype=\"NoneType\">None</burnIn_bottomRight>\n                        <burnIn_topLeft valuetype=\"NoneType\">None</burnIn_topLeft>\n                        <burnIn_topMiddle valuetype=\"NoneType\">None</burnIn_topMiddle>\n                        <burnIn_padding valuetype=\"NoneType\">None</burnIn_padding>\n                        <burnIn_topRight valuetype=\"NoneType\">None</burnIn_topRight>\n                        <burnIn_textSize valuetype=\"NoneType\">None</burnIn_textSize>\n                        <burnIn_bottomLeft valuetype=\"NoneType\">None</burnIn_bottomLeft>\n                        <burnIn_bottomMiddle valuetype=\"NoneType\">None</burnIn_bottomMiddle>\n                        <burnIn_font valuetype=\"NoneType\">None</burnIn_font>\n                    </burninData>\n                    <exr valuetype=\"dict\">\n                        <compression valuetype=\"str\">Zip (16 scanline)</compression>\n                        <datatype valuetype=\"str\">16 bit half</datatype>\n                        <noprefix valuetype=\"bool\">False</noprefix>\n                        <write_full_layer_names valuetype=\"bool\">False</write_full_layer_names>\n                        <standard_layer_name_format valuetype=\"bool\">False</standard_layer_name_format>\n                        <interleave valuetype=\"str\">channels, layers and views</interleave>\n                        <dw_compression_level valuetype=\"float\">45.0</dw_compression_level>\n                        <truncateChannelNames valuetype=\"bool\">False</truncateChannelNames>\n                        <metadata valuetype=\"str\">all metadata</metadata>\n                    </exr>\n                    <writeNodeName valuetype=\"str\">Write_{ext}</writeNodeName>\n                    <reformat valuetype=\"dict\">\n                        <filter valuetype=\"str\">Cubic</filter>\n                        <to_type valuetype=\"str\">To Sequence Resolution</to_type>\n                        <scale valuetype=\"float\">1.0</scale>\n                        <center valuetype=\"bool\">True</center>\n                        <resize valuetype=\"str\">width</resize>\n                    </reformat>\n                    <keepNukeScript valuetype=\"bool\">False</keepNukeScript>\n                    <method valuetype=\"str\">Blend</method>\n                </root>\n            </SequenceItem>\n        </SequenceItem>\n        <SequenceItem valuetype=\"tuple\">\n            <SequenceItem valuetype=\"str\">{shot}/editorial.nk</SequenceItem>\n            <SequenceItem valuetype=\"hiero.exporters.FnNukeShotExporter.NukeShotPreset\">\n                <root presetname=\"hiero.exporters.FnNukeShotExporter.NukeShotExporter\" tasktype=\"hiero.exporters.FnNukeShotExporter.NukeShotExporter\">\n                    <postProcessScript valuetype=\"bool\">True</postProcessScript>\n                    <colourspace valuetype=\"str\">default</colourspace>\n                    <file_type valuetype=\"unicode\">mov</file_type>\n                    <annotationsPreCompPaths valuetype=\"list\" />\n                    <channels valuetype=\"str\">rgb</channels>\n                    <includeAudio valuetype=\"bool\">False</includeAudio>\n                    <readPaths valuetype=\"list\" />\n                    <connectTracks valuetype=\"bool\">False</connectTracks>\n                    <useSingleSocket valuetype=\"bool\">False</useSingleSocket>\n                    <collateSequence valuetype=\"bool\">False</collateSequence>\n                    <additionalNodesData valuetype=\"list\" />\n                    <collateShotNames valuetype=\"bool\">True</collateShotNames>\n                    <includeEffects valuetype=\"bool\">True</includeEffects>\n                    <writePaths valuetype=\"list\">\n                        <SequenceItem valuetype=\"str\">{shot}/editorial_raw.%04d.{fileext}</SequenceItem>\n                    </writePaths>\n                    <reformat valuetype=\"dict\">\n                        <filter valuetype=\"str\">Cubic</filter>\n                        <to_type valuetype=\"str\">None</to_type>\n                        <scale valuetype=\"float\">1.0</scale>\n                        <center valuetype=\"bool\">True</center>\n                        <resize valuetype=\"str\">width</resize>\n                    </reformat>\n                    <keepNukeScript valuetype=\"bool\">False</keepNukeScript>\n                    <method valuetype=\"str\">Blend</method>\n                    <includeAnnotations valuetype=\"bool\">False</includeAnnotations>\n                    <enable valuetype=\"bool\">True</enable>\n                    <showAnnotations valuetype=\"bool\">True</showAnnotations>\n                    <mov valuetype=\"dict\">\n                        <b_frames valuetype=\"int\">0</b_frames>\n                        <bitrate_tolerance valuetype=\"int\">40000000</bitrate_tolerance>\n                        <gop_size valuetype=\"int\">12</gop_size>\n                        <quality_max valuetype=\"int\">31</quality_max>\n                        <quality_min valuetype=\"int\">2</quality_min>\n                        <codec valuetype=\"str\">avc1&#x09;H.264</codec>\n                        <ycbcr_matrix_type valuetype=\"str\">Auto</ycbcr_matrix_type>\n                        <encoder valuetype=\"str\">mov32</encoder>\n                        <bitrate valuetype=\"int\">20000</bitrate>\n                    </mov>\n                    <readAllLinesForExport valuetype=\"bool\">False</readAllLinesForExport>\n                    <deleteAudio valuetype=\"bool\">True</deleteAudio>\n                    <collateCustomStart valuetype=\"bool\">True</collateCustomStart>\n                    <burninDataEnabled valuetype=\"bool\">False</burninDataEnabled>\n                    <additionalNodesEnabled valuetype=\"bool\">False</additionalNodesEnabled>\n                    <timelineWriteNode valuetype=\"str\">{shot}/editorial_raw.%04d.{fileext}</timelineWriteNode>\n                    <burninData valuetype=\"dict\">\n                        <burnIn_bottomRight valuetype=\"NoneType\">None</burnIn_bottomRight>\n                        <burnIn_topLeft valuetype=\"NoneType\">None</burnIn_topLeft>\n                        <burnIn_topMiddle valuetype=\"NoneType\">None</burnIn_topMiddle>\n                        <burnIn_padding valuetype=\"NoneType\">None</burnIn_padding>\n                        <burnIn_topRight valuetype=\"NoneType\">None</burnIn_topRight>\n                        <burnIn_bottomMiddle valuetype=\"NoneType\">None</burnIn_bottomMiddle>\n                        <burnIn_bottomLeft valuetype=\"NoneType\">None</burnIn_bottomLeft>\n                        <burnIn_textSize valuetype=\"NoneType\">None</burnIn_textSize>\n                        <burnIn_font valuetype=\"NoneType\">None</burnIn_font>\n                    </burninData>\n                    <dpx valuetype=\"dict\">\n                        <datatype valuetype=\"str\">8 bit</datatype>\n                        <transfer valuetype=\"str\">(auto detect)</transfer>\n                        <bigEndian valuetype=\"bool\">True</bigEndian>\n                        <fill valuetype=\"bool\">False</fill>\n                    </dpx>\n                    <writeNodeName valuetype=\"str\">Write_{ext}</writeNodeName>\n                    <collateTracks valuetype=\"bool\">False</collateTracks>\n                </root>\n            </SequenceItem>\n        </SequenceItem>\n    </exportTemplate>\n    <excludeTags valuetype=\"list\" />\n    <includeTags valuetype=\"list\" />\n    <includeRetimes valuetype=\"bool\">False</includeRetimes>\n    <startFrameSource valuetype=\"str\">Custom</startFrameSource>\n    <cutLength valuetype=\"bool\">True</cutLength>\n    <cutHandles valuetype=\"int\">10</cutHandles>\n</root>\n"
  },
  {
    "path": "openpype/hosts/hiero/api/style.css",
    "content": "QWidget {\n  font-size: 13px;\n}\n\nQSpinBox {\n  padding: 2;\n  max-width: 8em;\n}\n\nQLineEdit {\n  padding: 2;\n  min-width: 15em;\n}\n\nQVBoxLayout {\n  min-width: 15em;\n  background-color: #201f1f;\n}\n\nQComboBox {\n  min-width: 8em;\n}\n\n#sectionContent {\n  background-color: #2E2D2D;\n}"
  },
  {
    "path": "openpype/hosts/hiero/api/tags.py",
    "content": "import json\nimport re\nimport os\nimport hiero\n\nfrom openpype.client import get_project, get_assets\nfrom openpype.lib import Logger\nfrom openpype.pipeline import get_current_project_name\n\nlog = Logger.get_logger(__name__)\n\n\ndef tag_data():\n    return {\n        \"[Lenses]\": {\n            \"Set lense here\": {\n                \"editable\": \"1\",\n                \"note\": \"Adjust parameters of your lense and then drop to clip. Remember! You can always overwrite on clip\",  # noqa\n                \"icon\": \"lense.png\",\n                \"metadata\": {\n                    \"focalLengthMm\": 57\n\n                }\n            }\n        },\n        # \"NukeScript\": {\n        #     \"editable\": \"1\",\n        #     \"note\": \"Collecting track items to Nuke scripts.\",\n        #     \"icon\": \"icons:TagNuke.png\",\n        #     \"metadata\": {\n        #         \"family\": \"nukescript\",\n        #         \"subset\": \"main\"\n        #     }\n        # },\n        \"Comment\": {\n            \"editable\": \"1\",\n            \"note\": \"Comment on a shot.\",\n            \"icon\": \"icons:TagComment.png\",\n            \"metadata\": {\n                \"family\": \"comment\",\n                \"subset\": \"main\"\n            }\n        },\n        \"FrameMain\": {\n            \"editable\": \"1\",\n            \"note\": \"Publishing a frame subset.\",\n            \"icon\": \"z_layer_main.png\",\n            \"metadata\": {\n                \"family\": \"frame\",\n                \"subset\": \"main\",\n                \"format\": \"png\"\n            }\n        }\n    }\n\n\ndef create_tag(key, data):\n    \"\"\"\n    Creating Tag object.\n\n    Args:\n        key (str): name of tag\n        data (dict): parameters of tag\n\n    Returns:\n        object: Tag object\n    \"\"\"\n    tag = hiero.core.Tag(str(key))\n    return update_tag(tag, data)\n\n\ndef update_tag(tag, data):\n    \"\"\"\n    Fixing Tag object.\n\n    Args:\n        tag (obj): Tag object\n        data (dict): parameters of tag\n    \"\"\"\n    # set icon if any available in input data\n    if data.get(\"icon\"):\n        tag.setIcon(str(data[\"icon\"]))\n\n    # get metadata of tag\n    mtd = tag.metadata()\n    # get metadata key from data\n    data_mtd = data.get(\"metadata\", {})\n\n    # set all data metadata to tag metadata\n    for _k, _v in data_mtd.items():\n        value = str(_v)\n        if type(_v) == dict:\n            value = json.dumps(_v)\n\n        # set the value\n        mtd.setValue(\n            \"tag.{}\".format(str(_k)),\n            value\n        )\n\n    # set note description of tag\n    tag.setNote(str(data[\"note\"]))\n    return tag\n\n\ndef add_tags_to_workfile():\n    \"\"\"\n    Will create default tags from presets.\n    \"\"\"\n    from .lib import get_current_project\n\n    def add_tag_to_bin(root_bin, name, data):\n        # for Tags to be created in root level Bin\n        # at first check if any of input data tag is not already created\n        done_tag = next((t for t in root_bin.items()\n                        if str(name) in t.name()), None)\n\n        if not done_tag:\n            # create Tag\n            tag = create_tag(name, data)\n            tag.setName(str(name))\n\n            log.debug(\"__ creating tag: {}\".format(tag))\n            # adding Tag to Root Bin\n            root_bin.addItem(tag)\n        else:\n            # update only non hierarchy tags\n            update_tag(done_tag, data)\n            done_tag.setName(str(name))\n            log.debug(\"__ updating tag: {}\".format(done_tag))\n\n    # get project and root bin object\n    project = get_current_project()\n    root_bin = project.tagsBin()\n\n    if \"Tag Presets\" in project.name():\n        return\n\n    log.debug(\"Setting default tags on project: {}\".format(project.name()))\n\n    # get hiero tags.json\n    nks_pres_tags = tag_data()\n\n    # Get project task types.\n    project_name = get_current_project_name()\n    project_doc = get_project(project_name)\n    tasks = project_doc[\"config\"][\"tasks\"]\n    nks_pres_tags[\"[Tasks]\"] = {}\n    log.debug(\"__ tasks: {}\".format(tasks))\n    for task_type in tasks.keys():\n        nks_pres_tags[\"[Tasks]\"][task_type.lower()] = {\n            \"editable\": \"1\",\n            \"note\": task_type,\n            \"icon\": \"icons:TagGood.png\",\n            \"metadata\": {\n                \"family\": \"task\",\n                \"type\": task_type\n            }\n        }\n\n    # Get project assets. Currently Ftrack specific to differentiate between\n    # asset builds and shots.\n    if int(os.getenv(\"TAG_ASSETBUILD_STARTUP\", 0)) == 1:\n        nks_pres_tags[\"[AssetBuilds]\"] = {}\n        for asset in get_assets(\n            project_name, fields=[\"name\", \"data.entityType\"]\n        ):\n            if asset[\"data\"][\"entityType\"] == \"AssetBuild\":\n                nks_pres_tags[\"[AssetBuilds]\"][asset[\"name\"]] = {\n                    \"editable\": \"1\",\n                    \"note\": \"\",\n                    \"icon\": {\n                        \"path\": \"icons:TagActor.png\"\n                    },\n                    \"metadata\": {\n                        \"family\": \"assetbuild\"\n                    }\n                }\n\n    # loop through tag data dict and create deep tag structure\n    for _k, _val in nks_pres_tags.items():\n        # check if key is not decorated with [] so it is defined as bin\n        bin_find = None\n        pattern = re.compile(r\"\\[(.*)\\]\")\n        _bin_finds = pattern.findall(_k)\n        # if there is available any then pop it to string\n        if _bin_finds:\n            bin_find = _bin_finds.pop()\n\n        # if bin was found then create or update\n        if bin_find:\n            root_add = False\n            # first check if in root lever is not already created bins\n            bins = [b for b in root_bin.items()\n                    if b.name() in str(bin_find)]\n\n            if bins:\n                bin = bins.pop()\n            else:\n                root_add = True\n                # create Bin object for processing\n                bin = hiero.core.Bin(str(bin_find))\n\n            # update or create tags in the bin\n            for __k, __v in _val.items():\n                add_tag_to_bin(bin, __k, __v)\n\n            # finally add the Bin object to the root level Bin\n            if root_add:\n                # adding Tag to Root Bin\n                root_bin.addItem(bin)\n        else:\n            add_tag_to_bin(root_bin, _k, _val)\n\n    log.info(\"Default Tags were set...\")\n"
  },
  {
    "path": "openpype/hosts/hiero/api/workio.py",
    "content": "import os\nimport hiero\n\nfrom openpype.lib import Logger\n\nlog = Logger.get_logger(__name__)\n\n\ndef file_extensions():\n    return [\".hrox\"]\n\n\ndef has_unsaved_changes():\n    # There are no methods for querying unsaved changes to a project, so\n    # enforcing to always save.\n    # but we could at least check if a current open script has a path\n    project = hiero.core.projects()[-1]\n    if project.path():\n        return True\n    else:\n        return False\n\n\ndef save_file(filepath):\n    file = os.path.basename(filepath)\n    project = hiero.core.projects()[-1]\n\n    if project:\n        log.info(\"Saving project: `{}` as '{}'\".format(project.name(), file))\n        project.saveAs(filepath)\n    else:\n        log.info(\"Creating new project...\")\n        project = hiero.core.newProject()\n        project.saveAs(filepath)\n\n\ndef open_file(filepath):\n    \"\"\"Manually fire the kBeforeProjectLoad event in order to work around a bug in Hiero.\n    The Foundry has logged this bug as:\n      Bug 40413 - Python API - kBeforeProjectLoad event type is not triggered\n      when calling hiero.core.openProject() (only triggered through UI)\n    It exists in all versions of Hiero through (at least) v1.9v1b12.\n\n    Once this bug is fixed, a version check will need to be added here in order to\n    prevent accidentally firing this event twice. The following commented-out code\n    is just an example, and will need to be updated when the bug is fixed to catch the\n    correct versions.\"\"\"\n    # if (hiero.core.env['VersionMajor'] < 1 or\n    #     hiero.core.env['VersionMajor'] == 1 and hiero.core.env['VersionMinor'] < 10:\n    hiero.core.events.sendEvent(\"kBeforeProjectLoad\", None)\n\n    project = hiero.core.projects()[-1]\n\n    # open project file\n    hiero.core.openProject(filepath.replace(os.path.sep, \"/\"))\n\n    # close previous project\n    project.close()\n\n\n\n    return True\n\n\ndef current_file():\n    current_file = hiero.core.projects()[-1].path()\n    if not current_file:\n        return None\n    return os.path.normpath(current_file)\n\n\ndef work_root(session):\n    return os.path.normpath(session[\"AVALON_WORKDIR\"]).replace(\"\\\\\", \"/\")\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/create/create_shot_clip.py",
    "content": "from copy import deepcopy\nimport openpype.hosts.hiero.api as phiero\n# from openpype.hosts.hiero.api import plugin, lib\n# reload(lib)\n# reload(plugin)\n# reload(phiero)\n\n\nclass CreateShotClip(phiero.Creator):\n    \"\"\"Publishable clip\"\"\"\n\n    label = \"Create Publishable Clip\"\n    family = \"clip\"\n    icon = \"film\"\n    defaults = [\"Main\"]\n\n    gui_tracks = [track.name()\n                  for track in phiero.get_current_sequence().videoTracks()]\n    gui_name = \"Pype publish attributes creator\"\n    gui_info = \"Define sequential rename and fill hierarchy data.\"\n    gui_inputs = {\n        \"renameHierarchy\": {\n            \"type\": \"section\",\n            \"label\": \"Shot Hierarchy And Rename Settings\",\n            \"target\": \"ui\",\n            \"order\": 0,\n            \"value\": {\n                \"hierarchy\": {\n                    \"value\": \"{folder}/{sequence}\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"Shot Parent Hierarchy\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Parents folder for shot root folder, Template filled with `Hierarchy Data` section\",  # noqa\n                    \"order\": 0},\n                \"clipRename\": {\n                    \"value\": False,\n                    \"type\": \"QCheckBox\",\n                    \"label\": \"Rename clips\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"Renaming selected clips on fly\",  # noqa\n                    \"order\": 1},\n                \"clipName\": {\n                    \"value\": \"{sequence}{shot}\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"Clip Name Template\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"template for creating shot namespaused for renaming (use rename: on)\",  # noqa\n                    \"order\": 2},\n                \"countFrom\": {\n                    \"value\": 10,\n                    \"type\": \"QSpinBox\",\n                    \"label\": \"Count sequence from\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"Set when the sequence number stafrom\",  # noqa\n                    \"order\": 3},\n                \"countSteps\": {\n                    \"value\": 10,\n                    \"type\": \"QSpinBox\",\n                    \"label\": \"Stepping number\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"What number is adding every new step\",  # noqa\n                    \"order\": 4},\n            }\n        },\n        \"hierarchyData\": {\n            \"type\": \"dict\",\n            \"label\": \"Shot Template Keywords\",\n            \"target\": \"tag\",\n            \"order\": 1,\n            \"value\": {\n                \"folder\": {\n                    \"value\": \"shots\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"{folder}\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Name of folder used for root of generated shots.\\nUsable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                    \"order\": 0},\n                \"episode\": {\n                    \"value\": \"ep01\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"{episode}\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Name of episode.\\nUsable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                    \"order\": 1},\n                \"sequence\": {\n                    \"value\": \"sq01\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"{sequence}\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Name of sequence of shots.\\nUsable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                    \"order\": 2},\n                \"track\": {\n                    \"value\": \"{_track_}\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"{track}\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Name of sequence of shots.\\nUsable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                    \"order\": 3},\n                \"shot\": {\n                    \"value\": \"sh###\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"{shot}\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Name of shot. `#` is converted to paded number. \\nAlso could be used with usable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                    \"order\": 4}\n            }\n        },\n        \"verticalSync\": {\n            \"type\": \"section\",\n            \"label\": \"Vertical Synchronization Of Attributes\",\n            \"target\": \"ui\",\n            \"order\": 2,\n            \"value\": {\n                \"vSyncOn\": {\n                    \"value\": True,\n                    \"type\": \"QCheckBox\",\n                    \"label\": \"Enable Vertical Sync\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"Switch on if you want clips above each other to share its attributes\",  # noqa\n                    \"order\": 0},\n                \"vSyncTrack\": {\n                    \"value\": gui_tracks,  # noqa\n                    \"type\": \"QComboBox\",\n                    \"label\": \"Hero track\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"Select driving track name which should be hero for all others\",  # noqa\n                    \"order\": 1}\n            }\n        },\n        \"publishSettings\": {\n            \"type\": \"section\",\n            \"label\": \"Publish Settings\",\n            \"target\": \"ui\",\n            \"order\": 3,\n            \"value\": {\n                \"subsetName\": {\n                    \"value\": [\"<track_name>\", \"main\", \"bg\", \"fg\", \"bg\",\n                              \"animatic\"],\n                    \"type\": \"QComboBox\",\n                    \"label\": \"Subset Name\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"chose subset name pattern, if <track_name> is selected, name of track layer will be used\",  # noqa\n                    \"order\": 0},\n                \"subsetFamily\": {\n                    \"value\": [\"plate\", \"take\"],\n                    \"type\": \"QComboBox\",\n                    \"label\": \"Subset Family\",\n                    \"target\": \"ui\", \"toolTip\": \"What use of this subset is for\",  # noqa\n                    \"order\": 1},\n                \"reviewTrack\": {\n                    \"value\": [\"< none >\"] + gui_tracks,\n                    \"type\": \"QComboBox\",\n                    \"label\": \"Use Review Track\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"Generate preview videos on fly, if `< none >` is defined nothing will be generated.\",  # noqa\n                    \"order\": 2},\n                \"audio\": {\n                    \"value\": False,\n                    \"type\": \"QCheckBox\",\n                    \"label\": \"Include audio\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Process subsets with corresponding audio\",  # noqa\n                    \"order\": 3},\n                \"sourceResolution\": {\n                    \"value\": False,\n                    \"type\": \"QCheckBox\",\n                    \"label\": \"Source resolution\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Is resloution taken from timeline or source?\",  # noqa\n                    \"order\": 4},\n            }\n        },\n        \"frameRangeAttr\": {\n            \"type\": \"section\",\n            \"label\": \"Shot Attributes\",\n            \"target\": \"ui\",\n            \"order\": 4,\n            \"value\": {\n                \"workfileFrameStart\": {\n                    \"value\": 1001,\n                    \"type\": \"QSpinBox\",\n                    \"label\": \"Workfiles Start Frame\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Set workfile starting frame number\",  # noqa\n                    \"order\": 0\n                },\n                \"handleStart\": {\n                    \"value\": 0,\n                    \"type\": \"QSpinBox\",\n                    \"label\": \"Handle Start\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Handle at start of clip\",  # noqa\n                    \"order\": 1\n                },\n                \"handleEnd\": {\n                    \"value\": 0,\n                    \"type\": \"QSpinBox\",\n                    \"label\": \"Handle End\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Handle at end of clip\",  # noqa\n                    \"order\": 2\n                }\n            }\n        }\n    }\n\n    presets = None\n\n    def process(self):\n        # Creator copy of object attributes that are modified during `process`\n        presets = deepcopy(self.presets)\n        gui_inputs = deepcopy(self.gui_inputs)\n\n        # get key pares from presets and match it on ui inputs\n        for k, v in gui_inputs.items():\n            if v[\"type\"] in (\"dict\", \"section\"):\n                # nested dictionary (only one level allowed\n                # for sections and dict)\n                for _k, _v in v[\"value\"].items():\n                    if presets.get(_k):\n                        gui_inputs[k][\n                            \"value\"][_k][\"value\"] = presets[_k]\n            if presets.get(k):\n                gui_inputs[k][\"value\"] = presets[k]\n\n        # open widget for plugins inputs\n        widget = self.widget(self.gui_name, self.gui_info, gui_inputs)\n        widget.exec_()\n\n        if len(self.selected) < 1:\n            return\n\n        if not widget.result:\n            print(\"Operation aborted\")\n            return\n\n        self.rename_add = 0\n\n        # get ui output for track name for vertical sync\n        v_sync_track = widget.result[\"vSyncTrack\"][\"value\"]\n\n        # sort selected trackItems by\n        sorted_selected_track_items = list()\n        unsorted_selected_track_items = list()\n        for _ti in self.selected:\n            if _ti.parent().name() in v_sync_track:\n                sorted_selected_track_items.append(_ti)\n            else:\n                unsorted_selected_track_items.append(_ti)\n\n        sorted_selected_track_items.extend(unsorted_selected_track_items)\n\n        kwargs = {\n            \"ui_inputs\": widget.result,\n            \"avalon\": self.data\n        }\n\n        for i, track_item in enumerate(sorted_selected_track_items):\n            self.rename_index = i\n\n            # convert track item to timeline media pool item\n            phiero.PublishClip(self, track_item, **kwargs).convert()\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/load/load_clip.py",
    "content": "from openpype.client import (\n    get_version_by_id,\n    get_last_version_by_subset_id\n)\nfrom openpype.pipeline import (\n    get_representation_path,\n    get_current_project_name,\n)\nfrom openpype.lib.transcoding import (\n    VIDEO_EXTENSIONS,\n    IMAGE_EXTENSIONS\n)\nimport openpype.hosts.hiero.api as phiero\n\n\nclass LoadClip(phiero.SequenceLoader):\n    \"\"\"Load a subset to timeline as clip\n\n    Place clip to timeline on its asset origin timings collected\n    during conforming to project\n    \"\"\"\n\n    families = [\"render2d\", \"source\", \"plate\", \"render\", \"review\"]\n    representations = [\"*\"]\n    extensions = set(\n        ext.lstrip(\".\") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)\n    )\n\n    label = \"Load as clip\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    # for loader multiselection\n    sequence = None\n    track = None\n\n    # presets\n    clip_color_last = \"green\"\n    clip_color = \"red\"\n\n    clip_name_template = \"{asset}_{subset}_{representation}\"\n\n    @classmethod\n    def apply_settings(cls, project_settings, system_settings):\n        plugin_type_settings = (\n            project_settings\n            .get(\"hiero\", {})\n            .get(\"load\", {})\n        )\n\n        if not plugin_type_settings:\n            return\n\n        plugin_name = cls.__name__\n\n        plugin_settings = None\n        # Look for plugin settings in host specific settings\n        if plugin_name in plugin_type_settings:\n            plugin_settings = plugin_type_settings[plugin_name]\n\n        if not plugin_settings:\n            return\n\n        print(\">>> We have preset for {}\".format(plugin_name))\n        for option, value in plugin_settings.items():\n            if option == \"enabled\" and value is False:\n                print(\"  - is disabled by preset\")\n            elif option == \"representations\":\n                continue\n            else:\n                print(\"  - setting `{}`: `{}`\".format(option, value))\n            setattr(cls, option, value)\n\n\n    def load(self, context, name, namespace, options):\n        # add clip name template to options\n        options.update({\n            \"clipNameTemplate\": self.clip_name_template\n        })\n        # in case loader uses multiselection\n        if self.track and self.sequence:\n            options.update({\n                \"sequence\": self.sequence,\n                \"track\": self.track,\n                \"clipNameTemplate\": self.clip_name_template\n            })\n\n        # load clip to timeline and get main variables\n        path = self.filepath_from_context(context)\n        track_item = phiero.ClipLoader(self, context, path, **options).load()\n        namespace = namespace or track_item.name()\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        version_name = version.get(\"name\", None)\n        colorspace = version_data.get(\"colorspace\", None)\n        object_name = self.clip_name_template.format(\n            **context[\"representation\"][\"context\"])\n\n        # set colorspace\n        if colorspace:\n            track_item.source().setSourceMediaColourTransform(colorspace)\n\n        # add additional metadata from the version to imprint Avalon knob\n        add_keys = [\n            \"frameStart\", \"frameEnd\", \"source\", \"author\",\n            \"fps\", \"handleStart\", \"handleEnd\"\n        ]\n\n        # move all version data keys to tag data\n        data_imprint = {}\n        for key in add_keys:\n            data_imprint.update({\n                key: version_data.get(key, str(None))\n            })\n\n        # add variables related to version context\n        data_imprint.update({\n            \"version\": version_name,\n            \"colorspace\": colorspace,\n            \"objectName\": object_name\n        })\n\n        # update color of clip regarding the version order\n        self.set_item_color(track_item, version)\n\n        # deal with multiselection\n        self.multiselection(track_item)\n\n        self.log.info(\"Loader done: `{}`\".format(name))\n\n        return phiero.containerise(\n            track_item,\n            name, namespace, context,\n            self.__class__.__name__,\n            data_imprint)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n        \"\"\" Updating previously loaded clips\n        \"\"\"\n\n        # load clip to timeline and get main variables\n        name = container['name']\n        namespace = container['namespace']\n        track_item = phiero.get_track_items(\n            track_item_name=namespace).pop()\n\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n\n        version_data = version_doc.get(\"data\", {})\n        version_name = version_doc.get(\"name\", None)\n        colorspace = version_data.get(\"colorspace\", None)\n        object_name = \"{}_{}\".format(name, namespace)\n        file = get_representation_path(representation).replace(\"\\\\\", \"/\")\n        clip = track_item.source()\n\n        # reconnect media to new path\n        clip.reconnectMedia(file)\n\n        # set colorspace\n        if colorspace:\n            clip.setSourceMediaColourTransform(colorspace)\n\n        # add additional metadata from the version to imprint Avalon knob\n        add_keys = [\n            \"frameStart\", \"frameEnd\", \"source\", \"author\",\n            \"fps\", \"handleStart\", \"handleEnd\"\n        ]\n\n        # move all version data keys to tag data\n        data_imprint = {}\n        for key in add_keys:\n            data_imprint.update({\n                key: version_data.get(key, str(None))\n            })\n\n        # add variables related to version context\n        data_imprint.update({\n            \"representation\": str(representation[\"_id\"]),\n            \"version\": version_name,\n            \"colorspace\": colorspace,\n            \"objectName\": object_name\n        })\n\n        # update color of clip regarding the version order\n        self.set_item_color(track_item, version_doc)\n\n        return phiero.update_container(track_item, data_imprint)\n\n    def remove(self, container):\n        \"\"\" Removing previously loaded clips\n        \"\"\"\n        # load clip to timeline and get main variables\n        namespace = container['namespace']\n        track_item = phiero.get_track_items(\n            track_item_name=namespace).pop()\n        track = track_item.parent()\n\n        # remove track item from track\n        track.removeItem(track_item)\n\n    @classmethod\n    def multiselection(cls, track_item):\n        if not cls.track:\n            cls.track = track_item.parent()\n            cls.sequence = cls.track.parent()\n\n    @classmethod\n    def set_item_color(cls, track_item, version_doc):\n        project_name = get_current_project_name()\n        last_version_doc = get_last_version_by_subset_id(\n            project_name, version_doc[\"parent\"], fields=[\"_id\"]\n        )\n        clip = track_item.source()\n        # set clip colour\n        if version_doc[\"_id\"] == last_version_doc[\"_id\"]:\n            clip.binItem().setColor(cls.clip_color_last)\n        else:\n            clip.binItem().setColor(cls.clip_color)\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/load/load_effects.py",
    "content": "import json\nfrom collections import OrderedDict\nimport six\n\nfrom openpype.client import (\n    get_version_by_id\n)\n\nfrom openpype.pipeline import (\n    AVALON_CONTAINER_ID,\n    load,\n    get_representation_path,\n    get_current_project_name\n)\nfrom openpype.hosts.hiero import api as phiero\nfrom openpype.lib import Logger\n\n\nclass LoadEffects(load.LoaderPlugin):\n    \"\"\"Loading colorspace soft effect exported from nukestudio\"\"\"\n\n    families = [\"effect\"]\n    representations = [\"*\"]\n    extension = {\"json\"}\n\n    label = \"Load Effects\"\n    order = 0\n    icon = \"cc\"\n    color = \"white\"\n\n    log = Logger.get_logger(__name__)\n\n    def load(self, context, name, namespace, data):\n        \"\"\"\n        Loading function to get the soft effects to particular read node\n\n        Arguments:\n            context (dict): context of version\n            name (str): name of the version\n            namespace (str): asset name\n            data (dict): compulsory attribute > not used\n\n        Returns:\n            nuke node: containerised nuke node object\n        \"\"\"\n        active_sequence = phiero.get_current_sequence()\n        active_track = phiero.get_current_track(\n            active_sequence, \"Loaded_{}\".format(name))\n\n        # get main variables\n        namespace = namespace or context[\"asset\"][\"name\"]\n        object_name = \"{}_{}\".format(name, namespace)\n        clip_in = context[\"asset\"][\"data\"][\"clipIn\"]\n        clip_out = context[\"asset\"][\"data\"][\"clipOut\"]\n\n        data_imprint = {\n            \"objectName\": object_name,\n            \"children_names\": []\n        }\n\n        # getting file path\n        file = self.filepath_from_context(context)\n        file = file.replace(\"\\\\\", \"/\")\n\n        if self._shared_loading(\n            file,\n            active_track,\n            clip_in,\n            clip_out,\n            data_imprint\n        ):\n            self.containerise(\n                active_track,\n                name=name,\n                namespace=namespace,\n                object_name=object_name,\n                context=context,\n                loader=self.__class__.__name__,\n                data=data_imprint)\n\n    def _shared_loading(\n        self,\n        file,\n        active_track,\n        clip_in,\n        clip_out,\n        data_imprint,\n        update=False\n    ):\n        # getting data from json file with unicode conversion\n        with open(file, \"r\") as f:\n            json_f = {self.byteify(key): self.byteify(value)\n                      for key, value in json.load(f).items()}\n\n        # get correct order of nodes by positions on track and subtrack\n        nodes_order = self.reorder_nodes(json_f)\n\n        used_subtracks = {\n            stitem.name(): stitem\n            for stitem in phiero.flatten(active_track.subTrackItems())\n        }\n\n        loaded = False\n        for index_order, (ef_name, ef_val) in enumerate(nodes_order.items()):\n            new_name = \"{}_loaded\".format(ef_name)\n            if new_name not in used_subtracks:\n                effect_track_item = active_track.createEffect(\n                    effectType=ef_val[\"class\"],\n                    timelineIn=clip_in,\n                    timelineOut=clip_out,\n                    subTrackIndex=index_order\n\n                )\n                effect_track_item.setName(new_name)\n            else:\n                effect_track_item = used_subtracks[new_name]\n\n            node = effect_track_item.node()\n            for knob_name, knob_value in ef_val[\"node\"].items():\n                if (\n                    not knob_value\n                    or knob_name == \"name\"\n                ):\n                    continue\n\n                try:\n                    # assume list means animation\n                    # except 4 values could be RGBA or vector\n                    if isinstance(knob_value, list) and len(knob_value) > 4:\n                        node[knob_name].setAnimated()\n                        for i, value in enumerate(knob_value):\n                            if isinstance(value, list):\n                                # list can have vector animation\n                                for ci, cv in enumerate(value):\n                                    node[knob_name].setValueAt(\n                                        cv,\n                                        (clip_in + i),\n                                        ci\n                                    )\n                            else:\n                                # list is single values\n                                node[knob_name].setValueAt(\n                                    value,\n                                    (clip_in + i)\n                                )\n                    else:\n                        node[knob_name].setValue(knob_value)\n                except NameError:\n                    self.log.warning(\"Knob: {} cannot be set\".format(\n                        knob_name))\n\n            # register all loaded children\n            data_imprint[\"children_names\"].append(new_name)\n\n            # make sure containerisation will happen\n            loaded = True\n\n        return loaded\n\n    def update(self, container, representation):\n        \"\"\" Updating previously loaded effects\n        \"\"\"\n        active_track = container[\"_item\"]\n        file = get_representation_path(representation).replace(\"\\\\\", \"/\")\n\n        # get main variables\n        name = container['name']\n        namespace = container['namespace']\n\n        # get timeline in out data\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n        version_data = version_doc[\"data\"]\n        clip_in = version_data[\"clipIn\"]\n        clip_out = version_data[\"clipOut\"]\n\n        object_name = \"{}_{}\".format(name, namespace)\n\n        # Disable previously created nodes\n        used_subtracks = {\n            stitem.name(): stitem\n            for stitem in phiero.flatten(active_track.subTrackItems())\n        }\n        container = phiero.get_track_openpype_data(\n            active_track, object_name\n        )\n\n        loaded_subtrack_items = container[\"children_names\"]\n        for loaded_stitem in loaded_subtrack_items:\n            if loaded_stitem not in used_subtracks:\n                continue\n            item_to_remove = used_subtracks.pop(loaded_stitem)\n            # TODO: find a way to erase nodes\n            self.log.debug(\n                \"This node needs to be removed: {}\".format(item_to_remove))\n\n        data_imprint = {\n            \"objectName\": object_name,\n            \"name\": name,\n            \"representation\": str(representation[\"_id\"]),\n            \"children_names\": []\n        }\n\n        if self._shared_loading(\n            file,\n            active_track,\n            clip_in,\n            clip_out,\n            data_imprint,\n            update=True\n        ):\n            return phiero.update_container(active_track, data_imprint)\n\n    def reorder_nodes(self, data):\n        new_order = OrderedDict()\n        trackNums = [v[\"trackIndex\"] for k, v in data.items()\n                     if isinstance(v, dict)]\n        subTrackNums = [v[\"subTrackIndex\"] for k, v in data.items()\n                        if isinstance(v, dict)]\n\n        for trackIndex in range(\n                min(trackNums), max(trackNums) + 1):\n            for subTrackIndex in range(\n                    min(subTrackNums), max(subTrackNums) + 1):\n                item = self.get_item(data, trackIndex, subTrackIndex)\n                if item is not {}:\n                    new_order.update(item)\n        return new_order\n\n    def get_item(self, data, trackIndex, subTrackIndex):\n        return {key: val for key, val in data.items()\n                if isinstance(val, dict)\n                if subTrackIndex == val[\"subTrackIndex\"]\n                if trackIndex == val[\"trackIndex\"]}\n\n    def byteify(self, input):\n        \"\"\"\n        Converts unicode strings to strings\n        It goes through all dictionary\n\n        Arguments:\n            input (dict/str): input\n\n        Returns:\n            dict: with fixed values and keys\n\n        \"\"\"\n\n        if isinstance(input, dict):\n            return {self.byteify(key): self.byteify(value)\n                    for key, value in input.items()}\n        elif isinstance(input, list):\n            return [self.byteify(element) for element in input]\n        elif isinstance(input, six.text_type):\n            return str(input)\n        else:\n            return input\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        pass\n\n    def containerise(\n        self,\n        track,\n        name,\n        namespace,\n        object_name,\n        context,\n        loader=None,\n        data=None\n    ):\n        \"\"\"Bundle Hiero's object into an assembly and imprint it with metadata\n\n        Containerisation enables a tracking of version, author and origin\n        for loaded assets.\n\n        Arguments:\n            track (hiero.core.VideoTrack): object to imprint as container\n            name (str): Name of resulting assembly\n            namespace (str): Namespace under which to host container\n            object_name (str): name of container\n            context (dict): Asset information\n            loader (str, optional): Name of node used to produce this\n                                    container.\n\n        Returns:\n            track_item (hiero.core.TrackItem): containerised object\n\n        \"\"\"\n\n        data_imprint = {\n            object_name: {\n                \"schema\": \"openpype:container-2.0\",\n                \"id\": AVALON_CONTAINER_ID,\n                \"name\": str(name),\n                \"namespace\": str(namespace),\n                \"loader\": str(loader),\n                \"representation\": str(context[\"representation\"][\"_id\"]),\n            }\n        }\n\n        if data:\n            for k, v in data.items():\n                data_imprint[object_name].update({k: v})\n\n        self.log.debug(\"_ data_imprint: {}\".format(data_imprint))\n        phiero.set_track_openpype_tag(track, data_imprint)\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/publish/collect_clip_effects.py",
    "content": "import re\nimport pyblish.api\n\n\nclass CollectClipEffects(pyblish.api.InstancePlugin):\n    \"\"\"Collect soft effects instances.\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.078\n    label = \"Collect Clip Effects Instances\"\n    families = [\"clip\"]\n\n    effect_categories = []\n\n    def process(self, instance):\n        family = \"effect\"\n        effects = {}\n        review = instance.data.get(\"review\")\n        review_track_index = instance.context.data.get(\"reviewTrackIndex\")\n        item = instance.data[\"item\"]\n\n        if \"audio\" in instance.data[\"family\"]:\n            return\n\n        # frame range\n        self.handle_start = instance.data[\"handleStart\"]\n        self.handle_end = instance.data[\"handleEnd\"]\n        self.clip_in = int(item.timelineIn())\n        self.clip_out = int(item.timelineOut())\n        self.clip_in_h = self.clip_in - self.handle_start\n        self.clip_out_h = self.clip_out + self.handle_end\n\n        track_item = instance.data[\"item\"]\n        track = track_item.parent()\n        track_index = track.trackIndex()\n        tracks_effect_items = instance.context.data.get(\"tracksEffectItems\")\n        clip_effect_items = instance.data.get(\"clipEffectItems\")\n\n        # add clips effects to track's:\n        if clip_effect_items:\n            tracks_effect_items[track_index] = clip_effect_items\n\n        # process all effects and divide them to instance\n        for _track_index, sub_track_items in tracks_effect_items.items():\n            # skip if track index is the same as review track index\n            if review and review_track_index == _track_index:\n                continue\n            for sitem in sub_track_items:\n                # make sure this subtrack item is relative of track item\n                if ((track_item not in sitem.linkedItems())\n                        and (len(sitem.linkedItems()) > 0)):\n                    continue\n\n                if not (track_index <= _track_index):\n                    continue\n\n                effect = self.add_effect(_track_index, sitem)\n                if effect:\n                    effects.update(effect)\n\n        # skip any without effects\n        if not effects:\n            return\n\n        subset = instance.data.get(\"subset\")\n        effects.update({\"assignTo\": subset})\n\n        subset_split = re.findall(r'[A-Z][^A-Z]*', subset)\n\n        if len(subset_split) > 0:\n            root_name = subset.replace(subset_split[0], \"\")\n            subset_split.insert(0, root_name.capitalize())\n\n        subset_split.insert(0, \"effect\")\n\n        # Need to convert to dict for AYON settings. This isinstance check can\n        # be removed in the future when OpenPype is no longer.\n        effect_categories = self.effect_categories\n        if isinstance(self.effect_categories, list):\n            effect_categories = {\n                x[\"name\"]: x[\"effect_classes\"] for x in self.effect_categories\n            }\n\n        category_by_effect = {\"\": \"\"}\n        for key, values in effect_categories.items():\n            for cls in values:\n                category_by_effect[cls] = key\n\n        effects_categorized = {k: {} for k in effect_categories.keys()}\n        effects_categorized[\"\"] = {}\n        for key, value in effects.items():\n            if key == \"assignTo\":\n                continue\n\n            # Some classes can have a number in them. Like Text2.\n            found_cls = \"\"\n            for cls in category_by_effect.keys():\n                if cls in value[\"class\"]:\n                    found_cls = cls\n\n            effects_categorized[category_by_effect[found_cls]][key] = value\n\n        categories = list(effects_categorized.keys())\n        for category in categories:\n            if not effects_categorized[category]:\n                effects_categorized.pop(category)\n                continue\n\n            effects_categorized[category][\"assignTo\"] = effects[\"assignTo\"]\n\n        for category, effects in effects_categorized.items():\n            name = \"\".join(subset_split)\n            name += category.capitalize()\n\n            # create new instance and inherit data\n            data = {}\n            for key, value in instance.data.items():\n                if \"clipEffectItems\" in key:\n                    continue\n                data[key] = value\n\n            # change names\n            data[\"subset\"] = name\n            data[\"family\"] = family\n            data[\"families\"] = [family]\n            data[\"name\"] = data[\"subset\"] + \"_\" + data[\"asset\"]\n            data[\"label\"] = \"{} - {}\".format(\n                data['asset'], data[\"subset\"]\n            )\n            data[\"effects\"] = effects\n\n            # create new instance\n            _instance = instance.context.create_instance(**data)\n            self.log.info(\"Created instance `{}`\".format(_instance))\n            self.log.debug(\"instance.data `{}`\".format(_instance.data))\n\n    def test_overlap(self, effect_t_in, effect_t_out):\n        covering_exp = bool(\n            (effect_t_in <= self.clip_in)\n            and (effect_t_out >= self.clip_out)\n        )\n        overlaying_right_exp = bool(\n            (effect_t_in < self.clip_out)\n            and (effect_t_out >= self.clip_out)\n        )\n        overlaying_left_exp = bool(\n            (effect_t_out > self.clip_in)\n            and (effect_t_in <= self.clip_in)\n        )\n\n        return any((\n            covering_exp,\n            overlaying_right_exp,\n            overlaying_left_exp\n        ))\n\n    def add_effect(self, track_index, sitem):\n        track = sitem.parentTrack().name()\n        # node serialization\n        node = sitem.node()\n        node_serialized = self.node_serialization(node)\n        node_name = sitem.name()\n        node_class = node.Class()\n\n        # collect timelineIn/Out\n        effect_t_in = int(sitem.timelineIn())\n        effect_t_out = int(sitem.timelineOut())\n\n        if not self.test_overlap(effect_t_in, effect_t_out):\n            return\n\n        self.log.debug(\"node_name: `{}`\".format(node_name))\n        self.log.debug(\"node_class: `{}`\".format(node_class))\n\n        return {node_name: {\n            \"class\": node_class,\n            \"timelineIn\": effect_t_in,\n            \"timelineOut\": effect_t_out,\n            \"subTrackIndex\": sitem.subTrackIndex(),\n            \"trackIndex\": track_index,\n            \"track\": track,\n            \"node\": node_serialized\n        }}\n\n    def node_serialization(self, node):\n        node_serialized = {}\n\n        # adding ignoring knob keys\n        _ignoring_keys = ['invert_mask', 'help', 'mask',\n                          'xpos', 'ypos', 'layer', 'process_mask', 'channel',\n                          'channels', 'maskChannelMask', 'maskChannelInput',\n                          'note_font', 'note_font_size', 'unpremult',\n                          'postage_stamp_frame', 'maskChannel', 'export_cc',\n                          'select_cccid', 'mix', 'version', 'matrix']\n\n        # loop through all knobs and collect not ignored\n        # and any with any value\n        for knob in node.knobs().keys():\n            # skip nodes in ignore keys\n            if knob in _ignoring_keys:\n                continue\n\n            # get animation if node is animated\n            if node[knob].isAnimated():\n                # grab animation including handles\n                knob_anim = [node[knob].getValueAt(i)\n                             for i in range(\n                             self.clip_in_h, self.clip_out_h + 1)]\n\n                node_serialized[knob] = knob_anim\n            else:\n                node_serialized[knob] = node[knob].value()\n\n        return node_serialized\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py",
    "content": "from pprint import pformat\nimport re\nimport ast\nimport json\n\nimport pyblish.api\n\nfrom openpype.client import get_asset_name_identifier\n\n\nclass CollectFrameTagInstances(pyblish.api.ContextPlugin):\n    \"\"\"Collect frames from tags.\n\n    Tag is expected to have metadata:\n    {\n        \"family\": \"frame\"\n        \"subset\": \"main\"\n    }\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = \"Collect Frames\"\n    hosts = [\"hiero\"]\n\n    def process(self, context):\n        self._context = context\n\n        # collect all sequence tags\n        subset_data = self._create_frame_subset_data_sequence(context)\n\n        self.log.debug(\"__ subset_data: {}\".format(\n            pformat(subset_data)\n        ))\n\n        # create instances\n        self._create_instances(subset_data)\n\n    def _get_tag_data(self, tag):\n        data = {}\n\n        # get tag metadata attribute\n        tag_data = tag.metadata()\n\n        # convert tag metadata to normal keys names and values to correct types\n        for k, v in dict(tag_data).items():\n            key = k.replace(\"tag.\", \"\")\n\n            try:\n                # capture exceptions which are related to strings only\n                if re.match(r\"^[\\d]+$\", v):\n                    value = int(v)\n                elif re.match(r\"^True$\", v):\n                    value = True\n                elif re.match(r\"^False$\", v):\n                    value = False\n                elif re.match(r\"^None$\", v):\n                    value = None\n                elif re.match(r\"^[\\w\\d_]+$\", v):\n                    value = v\n                else:\n                    value = ast.literal_eval(v)\n            except (ValueError, SyntaxError):\n                value = v\n\n            data[key] = value\n\n        return data\n\n    def _create_frame_subset_data_sequence(self, context):\n\n        sequence_tags = []\n        sequence = context.data[\"activeTimeline\"]\n\n        # get all publishable sequence frames\n        publish_frames = range(int(sequence.duration() + 1))\n\n        self.log.debug(\"__ publish_frames: {}\".format(\n            pformat(publish_frames)\n        ))\n\n        # get all sequence tags\n        for tag in sequence.tags():\n            tag_data = self._get_tag_data(tag)\n            self.log.debug(\"__ tag_data: {}\".format(\n                pformat(tag_data)\n            ))\n            if not tag_data:\n                continue\n\n            if \"family\" not in tag_data:\n                continue\n\n            if tag_data[\"family\"] != \"frame\":\n                continue\n\n            sequence_tags.append(tag_data)\n\n        self.log.debug(\"__ sequence_tags: {}\".format(\n            pformat(sequence_tags)\n        ))\n\n        # first collect all available subset tag frames\n        subset_data = {}\n        context_asset_doc = context.data[\"assetEntity\"]\n        context_asset_name = get_asset_name_identifier(context_asset_doc)\n\n        for tag_data in sequence_tags:\n            frame = int(tag_data[\"start\"])\n\n            if frame not in publish_frames:\n                continue\n\n            subset = tag_data[\"subset\"]\n\n            if subset in subset_data:\n                # update existing subset key\n                subset_data[subset][\"frames\"].append(frame)\n            else:\n                # create new subset key\n                subset_data[subset] = {\n                    \"frames\": [frame],\n                    \"format\": tag_data[\"format\"],\n                    \"asset\": context_asset_name\n                }\n        return subset_data\n\n    def _create_instances(self, subset_data):\n        # create instance per subset\n        for subset_name, subset_data in subset_data.items():\n            name = \"frame\" + subset_name.title()\n            data = {\n                \"name\": name,\n                \"label\": \"{} {}\".format(name, subset_data[\"frames\"]),\n                \"family\": \"image\",\n                \"families\": [\"frame\"],\n                \"asset\": subset_data[\"asset\"],\n                \"subset\": name,\n                \"format\": subset_data[\"format\"],\n                \"frames\": subset_data[\"frames\"]\n            }\n            self._context.create_instance(**data)\n\n            self.log.info(\n                \"Created instance: {}\".format(\n                    json.dumps(data, sort_keys=True, indent=4)\n                )\n            )\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/publish/collect_tag_tasks.py",
    "content": "from pyblish import api\n\n\nclass CollectClipTagTasks(api.InstancePlugin):\n    \"\"\"Collect Tags from selected track items.\"\"\"\n\n    order = api.CollectorOrder - 0.077\n    label = \"Collect Tag Tasks\"\n    hosts = [\"hiero\"]\n    families = [\"shot\"]\n\n    def process(self, instance):\n        # gets tags\n        tags = instance.data[\"tags\"]\n\n        tasks = {}\n        for tag in tags:\n            t_metadata = dict(tag.metadata())\n            t_family = t_metadata.get(\"tag.family\", \"\")\n\n            # gets only task family tags and collect labels\n            if \"task\" in t_family:\n                t_task_name = t_metadata.get(\"tag.label\", \"\")\n                t_task_type = t_metadata.get(\"tag.type\", \"\")\n                tasks[t_task_name] = {\"type\": t_task_type}\n\n        instance.data[\"tasks\"] = tasks\n\n        self.log.info(\"Collected Tasks from Tags: `{}`\".format(\n            instance.data[\"tasks\"]))\n        return\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/publish/extract_clip_effects.py",
    "content": "# from openpype import plugins\nimport os\nimport json\nimport pyblish.api\n\nfrom openpype.pipeline import publish\n\n\nclass ExtractClipEffects(publish.Extractor):\n    \"\"\"Extract clip effects instances.\"\"\"\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Export Clip Effects\"\n    families = [\"effect\"]\n\n    def process(self, instance):\n        item = instance.data[\"item\"]\n        effects = instance.data.get(\"effects\")\n\n        # skip any without effects\n        if not effects:\n            return\n\n        subset = instance.data.get(\"subset\")\n        family = instance.data[\"family\"]\n\n        self.log.debug(\"creating staging dir\")\n        staging_dir = self.staging_dir(instance)\n\n        transfers = list()\n        if \"transfers\" not in instance.data:\n            instance.data[\"transfers\"] = list()\n\n        ext = \"json\"\n        file = subset + \".\" + ext\n\n        # when instance is created during collection part\n        resources_dir = instance.data[\"resourcesDir\"]\n\n        # change paths in effects to files\n        for k, effect in effects.items():\n            if \"assignTo\" in k:\n                continue\n            trn = self.copy_linked_files(effect, resources_dir)\n            if trn:\n                transfers.append((trn[0], trn[1]))\n\n        instance.data[\"transfers\"].extend(transfers)\n        self.log.debug(\"_ transfers: `{}`\".format(\n            instance.data[\"transfers\"]))\n\n        # create representations\n        instance.data[\"representations\"] = list()\n\n        transfer_data = [\n            \"handleStart\", \"handleEnd\",\n            \"sourceStart\", \"sourceStartH\", \"sourceEnd\", \"sourceEndH\",\n            \"frameStart\", \"frameEnd\",\n            \"clipIn\", \"clipOut\", \"clipInH\", \"clipOutH\",\n            \"asset\", \"version\"\n        ]\n\n        # pass data to version\n        version_data = dict()\n        version_data.update({k: instance.data[k] for k in transfer_data})\n\n        # add to data of representation\n        version_data.update({\n            \"colorspace\": item.sourceMediaColourTransform(),\n            \"colorspaceScript\": instance.context.data[\"colorspace\"],\n            \"families\": [family, \"plate\"],\n            \"subset\": subset,\n            \"fps\": instance.context.data[\"fps\"]\n        })\n        instance.data[\"versionData\"] = version_data\n\n        representation = {\n            'files': file,\n            'stagingDir': staging_dir,\n            'name': family + ext.title(),\n            'ext': ext\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"_ representations: `{}`\".format(\n            instance.data[\"representations\"]))\n\n        self.log.debug(\"_ version_data: `{}`\".format(\n            instance.data[\"versionData\"]))\n\n        with open(os.path.join(staging_dir, file), \"w\") as outfile:\n            outfile.write(json.dumps(effects, indent=4, sort_keys=True))\n\n    def copy_linked_files(self, effect, dst_dir):\n        for k, v in effect[\"node\"].items():\n            if k in \"file\" and v != '':\n                base_name = os.path.basename(v)\n                dst = os.path.join(dst_dir, base_name).replace(\"\\\\\", \"/\")\n\n                # add it to the json\n                effect[\"node\"][k] = dst\n                return (v, dst)\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/publish/extract_frames.py",
    "content": "import os\nimport pyblish.api\n\nfrom openpype.lib import (\n    get_oiio_tool_args,\n    run_subprocess,\n)\nfrom openpype.pipeline import publish\n\n\nclass ExtractFrames(publish.Extractor):\n    \"\"\"Extracts frames\"\"\"\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract Frames\"\n    hosts = [\"hiero\"]\n    families = [\"frame\"]\n    movie_extensions = [\"mov\", \"mp4\"]\n\n    def process(self, instance):\n        oiio_tool_args = get_oiio_tool_args(\"oiiotool\")\n        staging_dir = self.staging_dir(instance)\n        output_template = os.path.join(staging_dir, instance.data[\"name\"])\n        sequence = instance.context.data[\"activeTimeline\"]\n\n        files = []\n        for frame in instance.data[\"frames\"]:\n            track_item = sequence.trackItemAt(frame)\n            media_source = track_item.source().mediaSource()\n            input_path = media_source.fileinfos()[0].filename()\n            input_frame = (\n                track_item.mapTimelineToSource(frame) +\n                track_item.source().mediaSource().startTime()\n            )\n            output_ext = instance.data[\"format\"]\n            output_path = output_template\n            output_path += \".{:04d}.{}\".format(int(frame), output_ext)\n\n            args = list(oiio_tool_args)\n\n            ext = os.path.splitext(input_path)[1][1:]\n            if ext in self.movie_extensions:\n                args.extend([\"--subimage\", str(int(input_frame))])\n            else:\n                args.extend([\"--frames\", str(int(input_frame))])\n\n            if ext == \"exr\":\n                args.extend([\"--powc\", \"0.45,0.45,0.45,1.0\"])\n\n            args.extend([input_path, \"-o\", output_path])\n            output = run_subprocess(args)\n\n            failed_output = \"oiiotool produced no output.\"\n            if failed_output in output:\n                raise ValueError(\n                    \"oiiotool processing failed. Args: {}\".format(args)\n                )\n\n            files.append(output_path)\n\n            # Feedback to user because \"oiiotool\" can make the publishing\n            # appear unresponsive.\n            self.log.info(\n                \"Processed {} of {} frames\".format(\n                    instance.data[\"frames\"].index(frame) + 1,\n                    len(instance.data[\"frames\"])\n                )\n            )\n\n        if len(files) == 1:\n            instance.data[\"representations\"] = [\n                {\n                    \"name\": output_ext,\n                    \"ext\": output_ext,\n                    \"files\": os.path.basename(files[0]),\n                    \"stagingDir\": staging_dir\n                }\n            ]\n        else:\n            instance.data[\"representations\"] = [\n                {\n                    \"name\": output_ext,\n                    \"ext\": output_ext,\n                    \"files\": [os.path.basename(x) for x in files],\n                    \"stagingDir\": staging_dir\n                }\n            ]\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/publish/extract_thumbnail.py",
    "content": "import os\nimport pyblish.api\n\nfrom openpype.pipeline import publish\n\n\nclass ExtractThumnail(publish.Extractor):\n    \"\"\"\n    Extractor for track item's tumnails\n    \"\"\"\n\n    label = \"Extract Thumnail\"\n    order = pyblish.api.ExtractorOrder\n    families = [\"plate\", \"take\"]\n    hosts = [\"hiero\"]\n\n    def process(self, instance):\n        # create representation data\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        staging_dir = self.staging_dir(instance)\n\n        self.create_thumbnail(staging_dir, instance)\n\n    def create_thumbnail(self, staging_dir, instance):\n        track_item = instance.data[\"item\"]\n        track_item_name = track_item.name()\n\n        # frames\n        duration = track_item.sourceDuration()\n        frame_start = track_item.sourceIn()\n        self.log.debug(\n            \"__ frame_start: `{}`, duration: `{}`\".format(\n                frame_start, duration))\n\n        # get thumbnail frame from the middle\n        thumb_frame = int(frame_start + (duration / 2))\n\n        thumb_file = \"{}thumbnail{}{}\".format(\n            track_item_name, thumb_frame, \".png\")\n        thumb_path = os.path.join(staging_dir, thumb_file)\n\n        thumbnail = track_item.thumbnail(thumb_frame, \"colour\").save(\n            thumb_path,\n            format='png'\n        )\n        self.log.debug(\n            \"__ thumb_path: `{}`, frame: `{}`\".format(thumbnail, thumb_frame))\n\n        self.log.info(\"Thumnail was generated to: {}\".format(thumb_path))\n        thumb_representation = {\n            'files': thumb_file,\n            'stagingDir': staging_dir,\n            'name': \"thumbnail\",\n            'thumbnail': True,\n            'ext': \"png\"\n        }\n        instance.data[\"representations\"].append(\n            thumb_representation)\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/publish/integrate_version_up_workfile.py",
    "content": "from pyblish import api\n\nfrom openpype.lib import version_up\n\n\nclass IntegrateVersionUpWorkfile(api.ContextPlugin):\n    \"\"\"Save as new workfile version\"\"\"\n\n    order = api.IntegratorOrder + 10.1\n    label = \"Version-up Workfile\"\n    hosts = [\"hiero\"]\n\n    optional = True\n    active = True\n\n    def process(self, context):\n        project = context.data[\"activeProject\"]\n        path = context.data.get(\"currentFile\")\n        new_path = version_up(path)\n\n        if project:\n            project.saveAs(new_path)\n\n        self.log.info(\"Project workfile was versioned up\")\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/publish/precollect_instances.py",
    "content": "import pyblish\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline.editorial import is_overlapping_otio_ranges\n\nfrom openpype.hosts.hiero import api as phiero\nfrom openpype.hosts.hiero.api.otio import hiero_export\n\nimport hiero\n# # developer reload modules\nfrom pprint import pformat\n\n\nclass PrecollectInstances(pyblish.api.ContextPlugin):\n    \"\"\"Collect all Track items selection.\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.49\n    label = \"Precollect Instances\"\n    hosts = [\"hiero\"]\n\n    audio_track_items = []\n\n    def process(self, context):\n        self.otio_timeline = context.data[\"otioTimeline\"]\n        timeline_selection = phiero.get_timeline_selection()\n        selected_timeline_items = phiero.get_track_items(\n            selection=timeline_selection,\n            check_tagged=True,\n            check_enabled=True\n        )\n\n        # only return enabled track items\n        if not selected_timeline_items:\n            selected_timeline_items = phiero.get_track_items(\n                check_enabled=True, check_tagged=True)\n\n        self.log.info(\n            \"Processing enabled track items: {}\".format(\n                selected_timeline_items))\n\n        # add all tracks subtreck effect items to context\n        all_tracks = hiero.ui.activeSequence().videoTracks()\n        tracks_effect_items = self.collect_sub_track_items(all_tracks)\n        context.data[\"tracksEffectItems\"] = tracks_effect_items\n\n        # process all sellected timeline track items\n        for track_item in selected_timeline_items:\n            data = {}\n            clip_name = track_item.name()\n            source_clip = track_item.source()\n            self.log.debug(\"clip_name: {}\".format(clip_name))\n\n            # get openpype tag data\n            tag_data = phiero.get_trackitem_openpype_data(track_item)\n            self.log.debug(\"__ tag_data: {}\".format(pformat(tag_data)))\n\n            if not tag_data:\n                continue\n\n            if tag_data.get(\"id\") != \"pyblish.avalon.instance\":\n                continue\n\n            # get clips subtracks and anotations\n            annotations = self.clip_annotations(source_clip)\n            subtracks = self.clip_subtrack(track_item)\n            self.log.debug(\"Annotations: {}\".format(annotations))\n            self.log.debug(\">> Subtracks: {}\".format(subtracks))\n\n            # solve handles length\n            tag_data[\"handleStart\"] = min(\n                tag_data[\"handleStart\"], int(track_item.handleInLength()))\n            tag_data[\"handleEnd\"] = min(\n                tag_data[\"handleEnd\"], int(track_item.handleOutLength()))\n\n            # add audio to families\n            with_audio = False\n            if tag_data.pop(\"audio\"):\n                with_audio = True\n\n            # add tag data to instance data\n            data.update({\n                k: v for k, v in tag_data.items()\n                if k not in (\"id\", \"applieswhole\", \"label\")\n            })\n\n            asset, asset_name = self._get_asset_data(tag_data)\n\n            subset = tag_data[\"subset\"]\n\n            # insert family into families\n            families = [str(f) for f in tag_data[\"families\"]]\n\n            # form label\n            label = \"{} -\".format(asset)\n            if asset_name != clip_name:\n                label += \" ({})\".format(clip_name)\n            label += \" {}\".format(subset)\n\n            data.update({\n                \"name\": \"{}_{}\".format(asset, subset),\n                \"label\": label,\n                \"asset\": asset,\n                \"asset_name\": asset_name,\n                \"item\": track_item,\n                \"families\": families,\n                \"publish\": tag_data[\"publish\"],\n                \"fps\": context.data[\"fps\"],\n\n                # clip's effect\n                \"clipEffectItems\": subtracks,\n                \"clipAnnotations\": annotations,\n\n                # add all additional tags\n                \"tags\": phiero.get_track_item_tags(track_item),\n                \"newAssetPublishing\": True\n            })\n\n            # otio clip data\n            otio_data = self.get_otio_clip_instance_data(track_item) or {}\n            self.log.debug(\"__ otio_data: {}\".format(pformat(otio_data)))\n            data.update(otio_data)\n            self.log.debug(\"__ data: {}\".format(pformat(data)))\n\n            # add resolution\n            self.get_resolution_to_data(data, context)\n\n            # create instance\n            instance = context.create_instance(**data)\n\n            # add colorspace data\n            instance.data.update({\n                \"versionData\": {\n                    \"colorspace\": track_item.sourceMediaColourTransform(),\n                }\n            })\n\n            # create shot instance for shot attributes create/update\n            self.create_shot_instance(context, **data)\n\n            self.log.info(\"Creating instance: {}\".format(instance))\n            self.log.info(\n                \"_ instance.data: {}\".format(pformat(instance.data)))\n\n            if not with_audio:\n                continue\n\n            # create audio subset instance\n            self.create_audio_instance(context, **data)\n\n            # add audioReview attribute to plate instance data\n            # if reviewTrack is on\n            if tag_data.get(\"reviewTrack\") is not None:\n                instance.data[\"reviewAudio\"] = True\n\n    def get_resolution_to_data(self, data, context):\n        assert data.get(\"otioClip\"), \"Missing `otioClip` data\"\n\n        # solve source resolution option\n        if data.get(\"sourceResolution\", None):\n            otio_clip_metadata = data[\n                \"otioClip\"].media_reference.metadata\n            data.update({\n                \"resolutionWidth\": otio_clip_metadata[\n                        \"openpype.source.width\"],\n                \"resolutionHeight\": otio_clip_metadata[\n                    \"openpype.source.height\"],\n                \"pixelAspect\": otio_clip_metadata[\n                    \"openpype.source.pixelAspect\"]\n            })\n        else:\n            otio_tl_metadata = context.data[\"otioTimeline\"].metadata\n            data.update({\n                \"resolutionWidth\": otio_tl_metadata[\"openpype.timeline.width\"],\n                \"resolutionHeight\": otio_tl_metadata[\n                    \"openpype.timeline.height\"],\n                \"pixelAspect\": otio_tl_metadata[\n                    \"openpype.timeline.pixelAspect\"]\n            })\n\n    def create_shot_instance(self, context, **data):\n        subset = \"shotMain\"\n        master_layer = data.get(\"heroTrack\")\n        hierarchy_data = data.get(\"hierarchyData\")\n        item = data.get(\"item\")\n        clip_name = item.name()\n\n        if not master_layer:\n            return\n\n        if not hierarchy_data:\n            return\n\n        asset = data[\"asset\"]\n        asset_name = data[\"asset_name\"]\n\n        # insert family into families\n        family = \"shot\"\n\n        # form label\n        label = \"{} -\".format(asset)\n        if asset_name != clip_name:\n            label += \" ({}) \".format(clip_name)\n        label += \" {}\".format(subset)\n\n        data.update({\n            \"name\": \"{}_{}\".format(asset, subset),\n            \"label\": label,\n            \"subset\": subset,\n            \"family\": family,\n            \"families\": []\n        })\n\n        instance = context.create_instance(**data)\n        self.log.info(\"Creating instance: {}\".format(instance))\n        self.log.debug(\n            \"_ instance.data: {}\".format(pformat(instance.data)))\n\n    def _get_asset_data(self, data):\n        folder_path = data.pop(\"folderPath\", None)\n\n        if data.get(\"asset_name\"):\n            asset_name = data[\"asset_name\"]\n        else:\n            asset_name = data[\"asset\"]\n\n        # backward compatibility for clip tags\n        # which are missing folderPath key\n        # TODO remove this in future versions\n        if not folder_path:\n            hierarchy_path = data[\"hierarchy\"]\n            folder_path = \"/{}/{}\".format(\n                hierarchy_path,\n                asset_name\n            )\n\n        if AYON_SERVER_ENABLED:\n            asset = folder_path\n        else:\n            asset = asset_name\n\n        return asset, asset_name\n\n    def create_audio_instance(self, context, **data):\n        subset = \"audioMain\"\n        master_layer = data.get(\"heroTrack\")\n\n        if not master_layer:\n            return\n\n        asset = data.get(\"asset\")\n        item = data.get(\"item\")\n        clip_name = item.name()\n\n        # test if any audio clips\n        if not self.test_any_audio(item):\n            return\n\n        asset = data[\"asset\"]\n        asset_name = data[\"asset_name\"]\n\n        # insert family into families\n        family = \"audio\"\n\n        # form label\n        label = \"{} -\".format(asset)\n        if asset_name != clip_name:\n            label += \" ({}) \".format(clip_name)\n        label += \" {}\".format(subset)\n\n        data.update({\n            \"name\": \"{}_{}\".format(asset, subset),\n            \"label\": label,\n            \"subset\": subset,\n            \"family\": family,\n            \"families\": [\"clip\"]\n        })\n        # remove review track attr if any\n        data.pop(\"reviewTrack\")\n\n        # create instance\n        instance = context.create_instance(**data)\n        self.log.info(\"Creating instance: {}\".format(instance))\n        self.log.debug(\n            \"_ instance.data: {}\".format(pformat(instance.data)))\n\n    def test_any_audio(self, track_item):\n        # collect all audio tracks to class variable\n        if not self.audio_track_items:\n            for otio_clip in self.otio_timeline.each_clip():\n                if otio_clip.parent().kind != \"Audio\":\n                    continue\n                self.audio_track_items.append(otio_clip)\n\n        # get track item timeline range\n        timeline_range = self.create_otio_time_range_from_timeline_item_data(\n            track_item)\n\n        # loop through audio track items and search for overlapping clip\n        for otio_audio in self.audio_track_items:\n            parent_range = otio_audio.range_in_parent()\n\n            # if any overaling clip found then return True\n            if is_overlapping_otio_ranges(\n                    parent_range, timeline_range, strict=False):\n                return True\n\n    def get_otio_clip_instance_data(self, track_item):\n        \"\"\"\n        Return otio objects for timeline, track and clip\n\n        Args:\n            timeline_item_data (dict): timeline_item_data from list returned by\n                                    resolve.get_current_timeline_items()\n            otio_timeline (otio.schema.Timeline): otio object\n\n        Returns:\n            dict: otio clip object\n\n        \"\"\"\n        ti_track_name = track_item.parent().name()\n        timeline_range = self.create_otio_time_range_from_timeline_item_data(\n            track_item)\n        for otio_clip in self.otio_timeline.each_clip():\n            track_name = otio_clip.parent().name\n            parent_range = otio_clip.range_in_parent()\n            if ti_track_name != track_name:\n                continue\n            if otio_clip.name != track_item.name():\n                continue\n            self.log.debug(\"__ parent_range: {}\".format(parent_range))\n            self.log.debug(\"__ timeline_range: {}\".format(timeline_range))\n            if is_overlapping_otio_ranges(\n                    parent_range, timeline_range, strict=True):\n\n                # add pypedata marker to otio_clip metadata\n                for marker in otio_clip.markers:\n                    if phiero.OPENPYPE_TAG_NAME in marker.name:\n                        otio_clip.metadata.update(marker.metadata)\n                return {\"otioClip\": otio_clip}\n\n        return None\n\n    @staticmethod\n    def create_otio_time_range_from_timeline_item_data(track_item):\n        timeline = phiero.get_current_sequence()\n        frame_start = int(track_item.timelineIn())\n        frame_duration = int(track_item.duration())\n        fps = timeline.framerate().toFloat()\n\n        return hiero_export.create_otio_time_range(\n            frame_start, frame_duration, fps)\n\n    def collect_sub_track_items(self, tracks):\n        \"\"\"\n        Returns dictionary with track index as key and list of subtracks\n        \"\"\"\n        # collect all subtrack items\n        sub_track_items = {}\n        for track in tracks:\n            items = track.items()\n\n            effet_items = track.subTrackItems()\n\n            # skip if no clips on track > need track with effect only\n            if not effet_items:\n                continue\n\n            # skip all disabled tracks\n            if not track.isEnabled():\n                continue\n\n            track_index = track.trackIndex()\n            _sub_track_items = phiero.flatten(effet_items)\n\n            _sub_track_items = list(_sub_track_items)\n            # continue only if any subtrack items are collected\n            if not _sub_track_items:\n                continue\n\n            enabled_sti = []\n            # loop all found subtrack items and check if they are enabled\n            for _sti in _sub_track_items:\n                # checking if not enabled\n                if not _sti.isEnabled():\n                    continue\n                if isinstance(_sti, hiero.core.Annotation):\n                    continue\n                # collect the subtrack item\n                enabled_sti.append(_sti)\n\n            # continue only if any subtrack items are collected\n            if not enabled_sti:\n                continue\n\n            # add collection of subtrackitems to dict\n            sub_track_items[track_index] = enabled_sti\n\n        return sub_track_items\n\n    @staticmethod\n    def clip_annotations(clip):\n        \"\"\"\n        Returns list of Clip's hiero.core.Annotation\n        \"\"\"\n        annotations = []\n        subTrackItems = phiero.flatten(clip.subTrackItems())\n        annotations += [item for item in subTrackItems if isinstance(\n            item, hiero.core.Annotation)]\n        return annotations\n\n    @staticmethod\n    def clip_subtrack(clip):\n        \"\"\"\n        Returns list of Clip's hiero.core.SubTrackItem\n        \"\"\"\n        subtracks = []\n        subTrackItems = phiero.flatten(clip.parent().subTrackItems())\n        for item in subTrackItems:\n            if \"TimeWarp\" in item.name():\n                continue\n            # avoid all anotation\n            if isinstance(item, hiero.core.Annotation):\n                continue\n            # # avoid all not anaibled\n            if not item.isEnabled():\n                continue\n            subtracks.append(item)\n        return subtracks\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/publish/precollect_workfile.py",
    "content": "import os\nimport tempfile\nfrom pprint import pformat\n\nimport pyblish.api\nfrom qtpy.QtGui import QPixmap\n\nimport hiero.ui\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.hosts.hiero.api.otio import hiero_export\n\n\nclass PrecollectWorkfile(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current working file into context\"\"\"\n\n    label = \"Precollect Workfile\"\n    order = pyblish.api.CollectorOrder - 0.491\n\n    def process(self, context):\n        asset = context.data[\"asset\"]\n        asset_name = asset\n        if AYON_SERVER_ENABLED:\n            asset_name = asset_name.split(\"/\")[-1]\n\n        active_timeline = hiero.ui.activeSequence()\n        project = active_timeline.project()\n        fps = active_timeline.framerate().toFloat()\n\n        # adding otio timeline to context\n        otio_timeline = hiero_export.create_otio_timeline()\n\n        # get workfile thumbnail paths\n        tmp_staging = tempfile.mkdtemp(prefix=\"pyblish_tmp_\")\n        thumbnail_name = \"workfile_thumbnail.png\"\n        thumbnail_path = os.path.join(tmp_staging, thumbnail_name)\n\n        # search for all windows with name of actual sequence\n        _windows = [w for w in hiero.ui.windowManager().windows()\n                    if active_timeline.name() in w.windowTitle()]\n\n        # export window to thumb path\n        QPixmap.grabWidget(_windows[-1]).save(thumbnail_path, 'png')\n\n        # thumbnail\n        thumb_representation = {\n            'files': thumbnail_name,\n            'stagingDir': tmp_staging,\n            'name': \"thumbnail\",\n            'thumbnail': True,\n            'ext': \"png\"\n        }\n\n        # get workfile paths\n        current_file = project.path()\n        staging_dir, base_name = os.path.split(current_file)\n\n        # creating workfile representation\n        workfile_representation = {\n            'name': 'hrox',\n            'ext': 'hrox',\n            'files': base_name,\n            \"stagingDir\": staging_dir,\n        }\n        family = \"workfile\"\n        instance_data = {\n            \"label\": \"{} - {}Main\".format(\n                asset, family),\n            \"name\": \"{}_{}\".format(asset_name, family),\n            \"asset\": context.data[\"asset\"],\n            # TODO use 'get_subset_name'\n            \"subset\": \"{}{}Main\".format(asset_name, family.capitalize()),\n            \"item\": project,\n            \"family\": family,\n            \"families\": [],\n            \"representations\": [workfile_representation, thumb_representation]\n        }\n\n        # create instance with workfile\n        instance = context.create_instance(**instance_data)\n\n        # update context with main project attributes\n        context_data = {\n            \"activeProject\": project,\n            \"activeTimeline\": active_timeline,\n            \"otioTimeline\": otio_timeline,\n            \"currentFile\": current_file,\n            \"colorspace\": self.get_colorspace(project),\n            \"fps\": fps\n        }\n        self.log.debug(\"__ context_data: {}\".format(pformat(context_data)))\n        context.data.update(context_data)\n\n        self.log.info(\"Creating instance: {}\".format(instance))\n        self.log.debug(\"__ instance.data: {}\".format(pformat(instance.data)))\n        self.log.debug(\"__ context_data: {}\".format(pformat(context_data)))\n\n    def get_colorspace(self, project):\n        # get workfile's colorspace properties\n        return {\n            \"useOCIOEnvironmentOverride\": project.useOCIOEnvironmentOverride(),\n            \"lutSetting16Bit\": project.lutSetting16Bit(),\n            \"lutSetting8Bit\": project.lutSetting8Bit(),\n            \"lutSettingFloat\": project.lutSettingFloat(),\n            \"lutSettingLog\": project.lutSettingLog(),\n            \"lutSettingViewer\": project.lutSettingViewer(),\n            \"lutSettingWorkingSpace\": project.lutSettingWorkingSpace(),\n            \"lutUseOCIOForExport\": project.lutUseOCIOForExport(),\n            \"ocioConfigName\": project.ocioConfigName(),\n            \"ocioConfigPath\": project.ocioConfigPath()\n        }\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py",
    "content": "from pyblish import api\n\nfrom openpype.client import get_assets, get_asset_name_identifier\n\n\nclass CollectAssetBuilds(api.ContextPlugin):\n    \"\"\"Collect asset from tags.\n\n    Tag is expected to have name of the asset and metadata:\n        {\n            \"family\": \"assetbuild\"\n        }\n    \"\"\"\n\n    # Run just after CollectClip\n    order = api.CollectorOrder + 0.02\n    label = \"Collect AssetBuilds\"\n    hosts = [\"hiero\"]\n\n    def process(self, context):\n        project_name = context.data[\"projectName\"]\n        asset_builds = {}\n        for asset_doc in get_assets(project_name):\n            if asset_doc[\"data\"].get(\"entityType\") != \"AssetBuild\":\n                continue\n\n            asset_name = get_asset_name_identifier(asset_doc)\n            self.log.debug(\"Found \\\"{}\\\" in database.\".format(asset_doc))\n            asset_builds[asset_name] = asset_doc\n\n        for instance in context:\n            if instance.data[\"family\"] != \"clip\":\n                continue\n\n            # Exclude non-tagged instances.\n            tagged = False\n            asset_names = []\n\n            for tag in instance.data[\"tags\"]:\n                t_metadata = dict(tag.metadata())\n                t_family = t_metadata.get(\"tag.family\", \"\")\n\n                if t_family.lower() == \"assetbuild\":\n                    asset_names.append(tag[\"name\"])\n                    tagged = True\n\n            if not tagged:\n                self.log.debug(\n                    \"Skipping \\\"{}\\\" because its not tagged with \"\n                    \"\\\"assetbuild\\\"\".format(instance)\n                )\n                continue\n\n            # Collect asset builds.\n            data = {\"assetbuilds\": []}\n            for name in asset_names:\n                data[\"assetbuilds\"].append(asset_builds[name])\n            self.log.debug(\n                \"Found asset builds: {}\".format(data[\"assetbuilds\"])\n            )\n\n            instance.data.update(data)\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/publish_old_workflow/collect_tag_comments.py",
    "content": "from pyblish import api\n\n\nclass CollectClipTagComments(api.InstancePlugin):\n    \"\"\"Collect comments from tags on selected track items and their sources.\"\"\"\n\n    order = api.CollectorOrder + 0.013\n    label = \"Collect Comments\"\n    hosts = [\"hiero\"]\n    families = [\"clip\"]\n\n    def process(self, instance):\n        # Collect comments.\n        instance.data[\"comments\"] = []\n\n        # Exclude non-tagged instances.\n        for tag in instance.data[\"tags\"]:\n            if tag[\"name\"].lower() == \"comment\":\n                instance.data[\"comments\"].append(\n                    tag[\"metadata\"][\"tag.note\"]\n                )\n\n        # Find tags on the source clip.\n        tags = instance.data[\"item\"].source().tags()\n        for tag in tags:\n            if tag.name().lower() == \"comment\":\n                instance.data[\"comments\"].append(\n                    tag.metadata().dict()[\"tag.note\"]\n                )\n\n        # Update label with comments counter.\n        instance.data[\"label\"] = \"{} - comments:{}\".format(\n            instance.data[\"label\"],\n            len(instance.data[\"comments\"])\n        )\n"
  },
  {
    "path": "openpype/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py",
    "content": "from pyblish import api\nimport hiero\nimport math\nfrom openpype.hosts.hiero.api.otio.hiero_export import create_otio_time_range\n\nclass PrecollectRetime(api.InstancePlugin):\n    \"\"\"Calculate Retiming of selected track items.\"\"\"\n\n    order = api.CollectorOrder - 0.578\n    label = \"Precollect Retime\"\n    hosts = [\"hiero\"]\n    families = ['retime_']\n\n    def process(self, instance):\n        if not instance.data.get(\"versionData\"):\n            instance.data[\"versionData\"] = {}\n\n        # get basic variables\n        otio_clip = instance.data[\"otioClip\"]\n\n        source_range = otio_clip.source_range\n        oc_source_fps = source_range.start_time.rate\n        oc_source_in = source_range.start_time.value\n\n        handle_start = instance.data[\"handleStart\"]\n        handle_end = instance.data[\"handleEnd\"]\n        frame_start = instance.data[\"frameStart\"]\n\n        track_item = instance.data[\"item\"]\n\n        # define basic clip frame range variables\n        timeline_in = int(track_item.timelineIn())\n        timeline_out = int(track_item.timelineOut())\n        source_in = int(track_item.sourceIn())\n        source_out = int(track_item.sourceOut())\n        speed = track_item.playbackSpeed()\n\n        # calculate available material before retime\n        available_in = int(track_item.handleInLength() * speed)\n        available_out = int(track_item.handleOutLength() * speed)\n\n        self.log.debug((\n            \"_BEFORE: \\n timeline_in: `{0}`,\\n timeline_out: `{1}`, \\n \"\n            \"source_in: `{2}`,\\n source_out: `{3}`,\\n speed: `{4}`,\\n \"\n            \"handle_start: `{5}`,\\n handle_end: `{6}`\").format(\n                timeline_in,\n                timeline_out,\n                source_in,\n                source_out,\n                speed,\n                handle_start,\n                handle_end\n        ))\n\n        # loop within subtrack items\n        time_warp_nodes = []\n        source_in_change = 0\n        source_out_change = 0\n        for s_track_item in track_item.linkedItems():\n            if isinstance(s_track_item, hiero.core.EffectTrackItem) \\\n                    and \"TimeWarp\" in s_track_item.node().Class():\n\n                # adding timewarp attribute to instance\n                time_warp_nodes = []\n\n                # ignore item if not enabled\n                if s_track_item.isEnabled():\n                    node = s_track_item.node()\n                    name = node[\"name\"].value()\n                    look_up = node[\"lookup\"].value()\n                    animated = node[\"lookup\"].isAnimated()\n                    if animated:\n                        look_up = [\n                            ((node[\"lookup\"].getValueAt(i)) - i)\n                            for i in range(\n                                (timeline_in - handle_start),\n                                (timeline_out + handle_end) + 1)\n                        ]\n                        # calculate difference\n                        diff_in = (node[\"lookup\"].getValueAt(\n                            timeline_in)) - timeline_in\n                        diff_out = (node[\"lookup\"].getValueAt(\n                            timeline_out)) - timeline_out\n\n                        # calculate source\n                        source_in_change += diff_in\n                        source_out_change += diff_out\n\n                        # calculate speed\n                        speed_in = (node[\"lookup\"].getValueAt(timeline_in) / (\n                            float(timeline_in) * .01)) * .01\n                        speed_out = (node[\"lookup\"].getValueAt(timeline_out) / (\n                            float(timeline_out) * .01)) * .01\n\n                        # calculate handles\n                        handle_start = int(\n                            math.ceil(\n                                (handle_start * speed_in * 1000) / 1000.0)\n                        )\n\n                        handle_end = int(\n                            math.ceil(\n                                (handle_end * speed_out * 1000) / 1000.0)\n                        )\n                        self.log.debug(\n                            (\"diff_in, diff_out\", diff_in, diff_out))\n                        self.log.debug(\n                            (\"source_in_change, source_out_change\",\n                             source_in_change, source_out_change))\n\n                    time_warp_nodes.append({\n                        \"Class\": \"TimeWarp\",\n                        \"name\": name,\n                        \"lookup\": look_up\n                    })\n\n        self.log.debug(\n            \"timewarp source in changes: in {}, out {}\".format(\n                source_in_change, source_out_change))\n\n        # recalculate handles by the speed\n        handle_start *= speed\n        handle_end *= speed\n        self.log.debug(\"speed: handle_start: '{0}', handle_end: '{1}'\".format(\n            handle_start, handle_end))\n\n        # recalculate source with timewarp and by the speed\n        source_in += int(source_in_change)\n        source_out += int(source_out_change * speed)\n\n        source_in_h = int(source_in - math.ceil(\n            (handle_start * 1000) / 1000.0))\n        source_out_h = int(source_out + math.ceil(\n            (handle_end * 1000) / 1000.0))\n\n        self.log.debug(\n            \"retimed: source_in_h: '{0}', source_out_h: '{1}'\".format(\n                source_in_h, source_out_h))\n\n        # add all data to Instance\n        instance.data[\"handleStart\"] = handle_start\n        instance.data[\"handleEnd\"] = handle_end\n        instance.data[\"sourceIn\"] = source_in\n        instance.data[\"sourceOut\"] = source_out\n        instance.data[\"sourceInH\"] = source_in_h\n        instance.data[\"sourceOutH\"] = source_out_h\n        instance.data[\"speed\"] = speed\n\n        source_handle_start = source_in_h - source_in\n        # frame_start = instance.data[\"frameStart\"] + source_handle_start\n        duration = source_out_h - source_in_h\n        frame_end = int(frame_start + duration - (handle_start + handle_end))\n\n        instance.data[\"versionData\"].update({\n            \"retime\": True,\n            \"speed\": speed,\n            \"timewarps\": time_warp_nodes,\n            \"frameStart\": frame_start,\n            \"frameEnd\": frame_end,\n            \"handleStart\": abs(source_handle_start),\n            \"handleEnd\": source_out_h - source_out\n        })\n        self.log.debug(\"versionData: {}\".format(instance.data[\"versionData\"]))\n        self.log.debug(\"sourceIn: {}\".format(instance.data[\"sourceIn\"]))\n        self.log.debug(\"sourceOut: {}\".format(instance.data[\"sourceOut\"]))\n        self.log.debug(\"speed: {}\".format(instance.data[\"speed\"]))\n\n        # change otio clip data\n        instance.data[\"otioClip\"].source_range = create_otio_time_range(\n            oc_source_in, (source_out - source_in + 1), oc_source_fps)\n        self.log.debug(\"otioClip: {}\".format(instance.data[\"otioClip\"]))\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/__init__.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n# Copyright 2007 Google Inc. All Rights Reserved.\n\n__version__ = '3.20.1'\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/any_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/any.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x19google/protobuf/any.proto\\x12\\x0fgoogle.protobuf\\\"&\\n\\x03\\x41ny\\x12\\x10\\n\\x08type_url\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x0c\\x42v\\n\\x13\\x63om.google.protobufB\\x08\\x41nyProtoP\\x01Z,google.golang.org/protobuf/types/known/anypb\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.any_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\010AnyProtoP\\001Z,google.golang.org/protobuf/types/known/anypb\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _ANY._serialized_start=46\n  _ANY._serialized_end=84\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/api_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/api.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2\nfrom google.protobuf import type_pb2 as google_dot_protobuf_dot_type__pb2\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x19google/protobuf/api.proto\\x12\\x0fgoogle.protobuf\\x1a$google/protobuf/source_context.proto\\x1a\\x1agoogle/protobuf/type.proto\\\"\\x81\\x02\\n\\x03\\x41pi\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12(\\n\\x07methods\\x18\\x02 \\x03(\\x0b\\x32\\x17.google.protobuf.Method\\x12(\\n\\x07options\\x18\\x03 \\x03(\\x0b\\x32\\x17.google.protobuf.Option\\x12\\x0f\\n\\x07version\\x18\\x04 \\x01(\\t\\x12\\x36\\n\\x0esource_context\\x18\\x05 \\x01(\\x0b\\x32\\x1e.google.protobuf.SourceContext\\x12&\\n\\x06mixins\\x18\\x06 \\x03(\\x0b\\x32\\x16.google.protobuf.Mixin\\x12\\'\\n\\x06syntax\\x18\\x07 \\x01(\\x0e\\x32\\x17.google.protobuf.Syntax\\\"\\xd5\\x01\\n\\x06Method\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x18\\n\\x10request_type_url\\x18\\x02 \\x01(\\t\\x12\\x19\\n\\x11request_streaming\\x18\\x03 \\x01(\\x08\\x12\\x19\\n\\x11response_type_url\\x18\\x04 \\x01(\\t\\x12\\x1a\\n\\x12response_streaming\\x18\\x05 \\x01(\\x08\\x12(\\n\\x07options\\x18\\x06 \\x03(\\x0b\\x32\\x17.google.protobuf.Option\\x12\\'\\n\\x06syntax\\x18\\x07 \\x01(\\x0e\\x32\\x17.google.protobuf.Syntax\\\"#\\n\\x05Mixin\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0c\\n\\x04root\\x18\\x02 \\x01(\\tBv\\n\\x13\\x63om.google.protobufB\\x08\\x41piProtoP\\x01Z,google.golang.org/protobuf/types/known/apipb\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.api_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\010ApiProtoP\\001Z,google.golang.org/protobuf/types/known/apipb\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _API._serialized_start=113\n  _API._serialized_end=370\n  _METHOD._serialized_start=373\n  _METHOD._serialized_end=586\n  _MIXIN._serialized_start=588\n  _MIXIN._serialized_end=623\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/compiler/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/compiler/plugin_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/compiler/plugin.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n%google/protobuf/compiler/plugin.proto\\x12\\x18google.protobuf.compiler\\x1a google/protobuf/descriptor.proto\\\"F\\n\\x07Version\\x12\\r\\n\\x05major\\x18\\x01 \\x01(\\x05\\x12\\r\\n\\x05minor\\x18\\x02 \\x01(\\x05\\x12\\r\\n\\x05patch\\x18\\x03 \\x01(\\x05\\x12\\x0e\\n\\x06suffix\\x18\\x04 \\x01(\\t\\\"\\xba\\x01\\n\\x14\\x43odeGeneratorRequest\\x12\\x18\\n\\x10\\x66ile_to_generate\\x18\\x01 \\x03(\\t\\x12\\x11\\n\\tparameter\\x18\\x02 \\x01(\\t\\x12\\x38\\n\\nproto_file\\x18\\x0f \\x03(\\x0b\\x32$.google.protobuf.FileDescriptorProto\\x12;\\n\\x10\\x63ompiler_version\\x18\\x03 \\x01(\\x0b\\x32!.google.protobuf.compiler.Version\\\"\\xc1\\x02\\n\\x15\\x43odeGeneratorResponse\\x12\\r\\n\\x05\\x65rror\\x18\\x01 \\x01(\\t\\x12\\x1a\\n\\x12supported_features\\x18\\x02 \\x01(\\x04\\x12\\x42\\n\\x04\\x66ile\\x18\\x0f \\x03(\\x0b\\x32\\x34.google.protobuf.compiler.CodeGeneratorResponse.File\\x1a\\x7f\\n\\x04\\x46ile\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x17\\n\\x0finsertion_point\\x18\\x02 \\x01(\\t\\x12\\x0f\\n\\x07\\x63ontent\\x18\\x0f \\x01(\\t\\x12?\\n\\x13generated_code_info\\x18\\x10 \\x01(\\x0b\\x32\\\".google.protobuf.GeneratedCodeInfo\\\"8\\n\\x07\\x46\\x65\\x61ture\\x12\\x10\\n\\x0c\\x46\\x45\\x41TURE_NONE\\x10\\x00\\x12\\x1b\\n\\x17\\x46\\x45\\x41TURE_PROTO3_OPTIONAL\\x10\\x01\\x42W\\n\\x1c\\x63om.google.protobuf.compilerB\\x0cPluginProtosZ)google.golang.org/protobuf/types/pluginpb')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.compiler.plugin_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\034com.google.protobuf.compilerB\\014PluginProtosZ)google.golang.org/protobuf/types/pluginpb'\n  _VERSION._serialized_start=101\n  _VERSION._serialized_end=171\n  _CODEGENERATORREQUEST._serialized_start=174\n  _CODEGENERATORREQUEST._serialized_end=360\n  _CODEGENERATORRESPONSE._serialized_start=363\n  _CODEGENERATORRESPONSE._serialized_end=684\n  _CODEGENERATORRESPONSE_FILE._serialized_start=499\n  _CODEGENERATORRESPONSE_FILE._serialized_end=626\n  _CODEGENERATORRESPONSE_FEATURE._serialized_start=628\n  _CODEGENERATORRESPONSE_FEATURE._serialized_end=684\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/descriptor.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Descriptors essentially contain exactly the information found in a .proto\nfile, in types that make this information accessible in Python.\n\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\nimport threading\nimport warnings\n\nfrom google.protobuf.internal import api_implementation\n\n_USE_C_DESCRIPTORS = False\nif api_implementation.Type() == 'cpp':\n  # Used by MakeDescriptor in cpp mode\n  import binascii\n  import os\n  from google.protobuf.pyext import _message\n  _USE_C_DESCRIPTORS = True\n\n\nclass Error(Exception):\n  \"\"\"Base error for this module.\"\"\"\n\n\nclass TypeTransformationError(Error):\n  \"\"\"Error transforming between python proto type and corresponding C++ type.\"\"\"\n\n\nif _USE_C_DESCRIPTORS:\n  # This metaclass allows to override the behavior of code like\n  #     isinstance(my_descriptor, FieldDescriptor)\n  # and make it return True when the descriptor is an instance of the extension\n  # type written in C++.\n  class DescriptorMetaclass(type):\n    def __instancecheck__(cls, obj):\n      if super(DescriptorMetaclass, cls).__instancecheck__(obj):\n        return True\n      if isinstance(obj, cls._C_DESCRIPTOR_CLASS):\n        return True\n      return False\nelse:\n  # The standard metaclass; nothing changes.\n  DescriptorMetaclass = type\n\n\nclass _Lock(object):\n  \"\"\"Wrapper class of threading.Lock(), which is allowed by 'with'.\"\"\"\n\n  def __new__(cls):\n    self = object.__new__(cls)\n    self._lock = threading.Lock()  # pylint: disable=protected-access\n    return self\n\n  def __enter__(self):\n    self._lock.acquire()\n\n  def __exit__(self, exc_type, exc_value, exc_tb):\n    self._lock.release()\n\n\n_lock = threading.Lock()\n\n\ndef _Deprecated(name):\n  if _Deprecated.count > 0:\n    _Deprecated.count -= 1\n    warnings.warn(\n        'Call to deprecated create function %s(). Note: Create unlinked '\n        'descriptors is going to go away. Please use get/find descriptors from '\n        'generated code or query the descriptor_pool.'\n        % name,\n        category=DeprecationWarning, stacklevel=3)\n\n\n# Deprecated warnings will print 100 times at most which should be enough for\n# users to notice and do not cause timeout.\n_Deprecated.count = 100\n\n\n_internal_create_key = object()\n\n\nclass DescriptorBase(metaclass=DescriptorMetaclass):\n\n  \"\"\"Descriptors base class.\n\n  This class is the base of all descriptor classes. It provides common options\n  related functionality.\n\n  Attributes:\n    has_options:  True if the descriptor has non-default options.  Usually it\n        is not necessary to read this -- just call GetOptions() which will\n        happily return the default instance.  However, it's sometimes useful\n        for efficiency, and also useful inside the protobuf implementation to\n        avoid some bootstrapping issues.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    # The class, or tuple of classes, that are considered as \"virtual\n    # subclasses\" of this descriptor class.\n    _C_DESCRIPTOR_CLASS = ()\n\n  def __init__(self, options, serialized_options, options_class_name):\n    \"\"\"Initialize the descriptor given its options message and the name of the\n    class of the options message. The name of the class is required in case\n    the options message is None and has to be created.\n    \"\"\"\n    self._options = options\n    self._options_class_name = options_class_name\n    self._serialized_options = serialized_options\n\n    # Does this descriptor have non-default options?\n    self.has_options = (options is not None) or (serialized_options is not None)\n\n  def _SetOptions(self, options, options_class_name):\n    \"\"\"Sets the descriptor's options\n\n    This function is used in generated proto2 files to update descriptor\n    options. It must not be used outside proto2.\n    \"\"\"\n    self._options = options\n    self._options_class_name = options_class_name\n\n    # Does this descriptor have non-default options?\n    self.has_options = options is not None\n\n  def GetOptions(self):\n    \"\"\"Retrieves descriptor options.\n\n    This method returns the options set or creates the default options for the\n    descriptor.\n    \"\"\"\n    if self._options:\n      return self._options\n\n    from google.protobuf import descriptor_pb2\n    try:\n      options_class = getattr(descriptor_pb2,\n                              self._options_class_name)\n    except AttributeError:\n      raise RuntimeError('Unknown options class name %s!' %\n                         (self._options_class_name))\n\n    with _lock:\n      if self._serialized_options is None:\n        self._options = options_class()\n      else:\n        self._options = _ParseOptions(options_class(),\n                                      self._serialized_options)\n\n      return self._options\n\n\nclass _NestedDescriptorBase(DescriptorBase):\n  \"\"\"Common class for descriptors that can be nested.\"\"\"\n\n  def __init__(self, options, options_class_name, name, full_name,\n               file, containing_type, serialized_start=None,\n               serialized_end=None, serialized_options=None):\n    \"\"\"Constructor.\n\n    Args:\n      options: Protocol message options or None\n        to use default message options.\n      options_class_name (str): The class name of the above options.\n      name (str): Name of this protocol message type.\n      full_name (str): Fully-qualified name of this protocol message type,\n        which will include protocol \"package\" name and the name of any\n        enclosing types.\n      file (FileDescriptor): Reference to file info.\n      containing_type: if provided, this is a nested descriptor, with this\n        descriptor as parent, otherwise None.\n      serialized_start: The start index (inclusive) in block in the\n        file.serialized_pb that describes this descriptor.\n      serialized_end: The end index (exclusive) in block in the\n        file.serialized_pb that describes this descriptor.\n      serialized_options: Protocol message serialized options or None.\n    \"\"\"\n    super(_NestedDescriptorBase, self).__init__(\n        options, serialized_options, options_class_name)\n\n    self.name = name\n    # TODO(falk): Add function to calculate full_name instead of having it in\n    #             memory?\n    self.full_name = full_name\n    self.file = file\n    self.containing_type = containing_type\n\n    self._serialized_start = serialized_start\n    self._serialized_end = serialized_end\n\n  def CopyToProto(self, proto):\n    \"\"\"Copies this to the matching proto in descriptor_pb2.\n\n    Args:\n      proto: An empty proto instance from descriptor_pb2.\n\n    Raises:\n      Error: If self couldn't be serialized, due to to few constructor\n        arguments.\n    \"\"\"\n    if (self.file is not None and\n        self._serialized_start is not None and\n        self._serialized_end is not None):\n      proto.ParseFromString(self.file.serialized_pb[\n          self._serialized_start:self._serialized_end])\n    else:\n      raise Error('Descriptor does not contain serialization.')\n\n\nclass Descriptor(_NestedDescriptorBase):\n\n  \"\"\"Descriptor for a protocol message type.\n\n  Attributes:\n      name (str): Name of this protocol message type.\n      full_name (str): Fully-qualified name of this protocol message type,\n          which will include protocol \"package\" name and the name of any\n          enclosing types.\n      containing_type (Descriptor): Reference to the descriptor of the type\n          containing us, or None if this is top-level.\n      fields (list[FieldDescriptor]): Field descriptors for all fields in\n          this type.\n      fields_by_number (dict(int, FieldDescriptor)): Same\n          :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed\n          by \"number\" attribute in each FieldDescriptor.\n      fields_by_name (dict(str, FieldDescriptor)): Same\n          :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed by\n          \"name\" attribute in each :class:`FieldDescriptor`.\n      nested_types (list[Descriptor]): Descriptor references\n          for all protocol message types nested within this one.\n      nested_types_by_name (dict(str, Descriptor)): Same Descriptor\n          objects as in :attr:`nested_types`, but indexed by \"name\" attribute\n          in each Descriptor.\n      enum_types (list[EnumDescriptor]): :class:`EnumDescriptor` references\n          for all enums contained within this type.\n      enum_types_by_name (dict(str, EnumDescriptor)): Same\n          :class:`EnumDescriptor` objects as in :attr:`enum_types`, but\n          indexed by \"name\" attribute in each EnumDescriptor.\n      enum_values_by_name (dict(str, EnumValueDescriptor)): Dict mapping\n          from enum value name to :class:`EnumValueDescriptor` for that value.\n      extensions (list[FieldDescriptor]): All extensions defined directly\n          within this message type (NOT within a nested type).\n      extensions_by_name (dict(str, FieldDescriptor)): Same FieldDescriptor\n          objects as :attr:`extensions`, but indexed by \"name\" attribute of each\n          FieldDescriptor.\n      is_extendable (bool):  Does this type define any extension ranges?\n      oneofs (list[OneofDescriptor]): The list of descriptors for oneof fields\n          in this message.\n      oneofs_by_name (dict(str, OneofDescriptor)): Same objects as in\n          :attr:`oneofs`, but indexed by \"name\" attribute.\n      file (FileDescriptor): Reference to file descriptor.\n\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.Descriptor\n\n    def __new__(\n        cls,\n        name=None,\n        full_name=None,\n        filename=None,\n        containing_type=None,\n        fields=None,\n        nested_types=None,\n        enum_types=None,\n        extensions=None,\n        options=None,\n        serialized_options=None,\n        is_extendable=True,\n        extension_ranges=None,\n        oneofs=None,\n        file=None,  # pylint: disable=redefined-builtin\n        serialized_start=None,\n        serialized_end=None,\n        syntax=None,\n        create_key=None):\n      _message.Message._CheckCalledFromGeneratedFile()\n      return _message.default_pool.FindMessageTypeByName(full_name)\n\n  # NOTE(tmarek): The file argument redefining a builtin is nothing we can\n  # fix right now since we don't know how many clients already rely on the\n  # name of the argument.\n  def __init__(self, name, full_name, filename, containing_type, fields,\n               nested_types, enum_types, extensions, options=None,\n               serialized_options=None,\n               is_extendable=True, extension_ranges=None, oneofs=None,\n               file=None, serialized_start=None, serialized_end=None,  # pylint: disable=redefined-builtin\n               syntax=None, create_key=None):\n    \"\"\"Arguments to __init__() are as described in the description\n    of Descriptor fields above.\n\n    Note that filename is an obsolete argument, that is not used anymore.\n    Please use file.name to access this as an attribute.\n    \"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('Descriptor')\n\n    super(Descriptor, self).__init__(\n        options, 'MessageOptions', name, full_name, file,\n        containing_type, serialized_start=serialized_start,\n        serialized_end=serialized_end, serialized_options=serialized_options)\n\n    # We have fields in addition to fields_by_name and fields_by_number,\n    # so that:\n    #   1. Clients can index fields by \"order in which they're listed.\"\n    #   2. Clients can easily iterate over all fields with the terse\n    #      syntax: for f in descriptor.fields: ...\n    self.fields = fields\n    for field in self.fields:\n      field.containing_type = self\n    self.fields_by_number = dict((f.number, f) for f in fields)\n    self.fields_by_name = dict((f.name, f) for f in fields)\n    self._fields_by_camelcase_name = None\n\n    self.nested_types = nested_types\n    for nested_type in nested_types:\n      nested_type.containing_type = self\n    self.nested_types_by_name = dict((t.name, t) for t in nested_types)\n\n    self.enum_types = enum_types\n    for enum_type in self.enum_types:\n      enum_type.containing_type = self\n    self.enum_types_by_name = dict((t.name, t) for t in enum_types)\n    self.enum_values_by_name = dict(\n        (v.name, v) for t in enum_types for v in t.values)\n\n    self.extensions = extensions\n    for extension in self.extensions:\n      extension.extension_scope = self\n    self.extensions_by_name = dict((f.name, f) for f in extensions)\n    self.is_extendable = is_extendable\n    self.extension_ranges = extension_ranges\n    self.oneofs = oneofs if oneofs is not None else []\n    self.oneofs_by_name = dict((o.name, o) for o in self.oneofs)\n    for oneof in self.oneofs:\n      oneof.containing_type = self\n    self.syntax = syntax or \"proto2\"\n\n  @property\n  def fields_by_camelcase_name(self):\n    \"\"\"Same FieldDescriptor objects as in :attr:`fields`, but indexed by\n    :attr:`FieldDescriptor.camelcase_name`.\n    \"\"\"\n    if self._fields_by_camelcase_name is None:\n      self._fields_by_camelcase_name = dict(\n          (f.camelcase_name, f) for f in self.fields)\n    return self._fields_by_camelcase_name\n\n  def EnumValueName(self, enum, value):\n    \"\"\"Returns the string name of an enum value.\n\n    This is just a small helper method to simplify a common operation.\n\n    Args:\n      enum: string name of the Enum.\n      value: int, value of the enum.\n\n    Returns:\n      string name of the enum value.\n\n    Raises:\n      KeyError if either the Enum doesn't exist or the value is not a valid\n        value for the enum.\n    \"\"\"\n    return self.enum_types_by_name[enum].values_by_number[value].name\n\n  def CopyToProto(self, proto):\n    \"\"\"Copies this to a descriptor_pb2.DescriptorProto.\n\n    Args:\n      proto: An empty descriptor_pb2.DescriptorProto.\n    \"\"\"\n    # This function is overridden to give a better doc comment.\n    super(Descriptor, self).CopyToProto(proto)\n\n\n# TODO(robinson): We should have aggressive checking here,\n# for example:\n#   * If you specify a repeated field, you should not be allowed\n#     to specify a default value.\n#   * [Other examples here as needed].\n#\n# TODO(robinson): for this and other *Descriptor classes, we\n# might also want to lock things down aggressively (e.g.,\n# prevent clients from setting the attributes).  Having\n# stronger invariants here in general will reduce the number\n# of runtime checks we must do in reflection.py...\nclass FieldDescriptor(DescriptorBase):\n\n  \"\"\"Descriptor for a single field in a .proto file.\n\n  Attributes:\n    name (str): Name of this field, exactly as it appears in .proto.\n    full_name (str): Name of this field, including containing scope.  This is\n      particularly relevant for extensions.\n    index (int): Dense, 0-indexed index giving the order that this\n      field textually appears within its message in the .proto file.\n    number (int): Tag number declared for this field in the .proto file.\n\n    type (int): (One of the TYPE_* constants below) Declared type.\n    cpp_type (int): (One of the CPPTYPE_* constants below) C++ type used to\n      represent this field.\n\n    label (int): (One of the LABEL_* constants below) Tells whether this\n      field is optional, required, or repeated.\n    has_default_value (bool): True if this field has a default value defined,\n      otherwise false.\n    default_value (Varies): Default value of this field.  Only\n      meaningful for non-repeated scalar fields.  Repeated fields\n      should always set this to [], and non-repeated composite\n      fields should always set this to None.\n\n    containing_type (Descriptor): Descriptor of the protocol message\n      type that contains this field.  Set by the Descriptor constructor\n      if we're passed into one.\n      Somewhat confusingly, for extension fields, this is the\n      descriptor of the EXTENDED message, not the descriptor\n      of the message containing this field.  (See is_extension and\n      extension_scope below).\n    message_type (Descriptor): If a composite field, a descriptor\n      of the message type contained in this field.  Otherwise, this is None.\n    enum_type (EnumDescriptor): If this field contains an enum, a\n      descriptor of that enum.  Otherwise, this is None.\n\n    is_extension: True iff this describes an extension field.\n    extension_scope (Descriptor): Only meaningful if is_extension is True.\n      Gives the message that immediately contains this extension field.\n      Will be None iff we're a top-level (file-level) extension field.\n\n    options (descriptor_pb2.FieldOptions): Protocol message field options or\n      None to use default field options.\n\n    containing_oneof (OneofDescriptor): If the field is a member of a oneof\n      union, contains its descriptor. Otherwise, None.\n\n    file (FileDescriptor): Reference to file descriptor.\n  \"\"\"\n\n  # Must be consistent with C++ FieldDescriptor::Type enum in\n  # descriptor.h.\n  #\n  # TODO(robinson): Find a way to eliminate this repetition.\n  TYPE_DOUBLE         = 1\n  TYPE_FLOAT          = 2\n  TYPE_INT64          = 3\n  TYPE_UINT64         = 4\n  TYPE_INT32          = 5\n  TYPE_FIXED64        = 6\n  TYPE_FIXED32        = 7\n  TYPE_BOOL           = 8\n  TYPE_STRING         = 9\n  TYPE_GROUP          = 10\n  TYPE_MESSAGE        = 11\n  TYPE_BYTES          = 12\n  TYPE_UINT32         = 13\n  TYPE_ENUM           = 14\n  TYPE_SFIXED32       = 15\n  TYPE_SFIXED64       = 16\n  TYPE_SINT32         = 17\n  TYPE_SINT64         = 18\n  MAX_TYPE            = 18\n\n  # Must be consistent with C++ FieldDescriptor::CppType enum in\n  # descriptor.h.\n  #\n  # TODO(robinson): Find a way to eliminate this repetition.\n  CPPTYPE_INT32       = 1\n  CPPTYPE_INT64       = 2\n  CPPTYPE_UINT32      = 3\n  CPPTYPE_UINT64      = 4\n  CPPTYPE_DOUBLE      = 5\n  CPPTYPE_FLOAT       = 6\n  CPPTYPE_BOOL        = 7\n  CPPTYPE_ENUM        = 8\n  CPPTYPE_STRING      = 9\n  CPPTYPE_MESSAGE     = 10\n  MAX_CPPTYPE         = 10\n\n  _PYTHON_TO_CPP_PROTO_TYPE_MAP = {\n      TYPE_DOUBLE: CPPTYPE_DOUBLE,\n      TYPE_FLOAT: CPPTYPE_FLOAT,\n      TYPE_ENUM: CPPTYPE_ENUM,\n      TYPE_INT64: CPPTYPE_INT64,\n      TYPE_SINT64: CPPTYPE_INT64,\n      TYPE_SFIXED64: CPPTYPE_INT64,\n      TYPE_UINT64: CPPTYPE_UINT64,\n      TYPE_FIXED64: CPPTYPE_UINT64,\n      TYPE_INT32: CPPTYPE_INT32,\n      TYPE_SFIXED32: CPPTYPE_INT32,\n      TYPE_SINT32: CPPTYPE_INT32,\n      TYPE_UINT32: CPPTYPE_UINT32,\n      TYPE_FIXED32: CPPTYPE_UINT32,\n      TYPE_BYTES: CPPTYPE_STRING,\n      TYPE_STRING: CPPTYPE_STRING,\n      TYPE_BOOL: CPPTYPE_BOOL,\n      TYPE_MESSAGE: CPPTYPE_MESSAGE,\n      TYPE_GROUP: CPPTYPE_MESSAGE\n      }\n\n  # Must be consistent with C++ FieldDescriptor::Label enum in\n  # descriptor.h.\n  #\n  # TODO(robinson): Find a way to eliminate this repetition.\n  LABEL_OPTIONAL      = 1\n  LABEL_REQUIRED      = 2\n  LABEL_REPEATED      = 3\n  MAX_LABEL           = 3\n\n  # Must be consistent with C++ constants kMaxNumber, kFirstReservedNumber,\n  # and kLastReservedNumber in descriptor.h\n  MAX_FIELD_NUMBER = (1 << 29) - 1\n  FIRST_RESERVED_FIELD_NUMBER = 19000\n  LAST_RESERVED_FIELD_NUMBER = 19999\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.FieldDescriptor\n\n    def __new__(cls, name, full_name, index, number, type, cpp_type, label,\n                default_value, message_type, enum_type, containing_type,\n                is_extension, extension_scope, options=None,\n                serialized_options=None,\n                has_default_value=True, containing_oneof=None, json_name=None,\n                file=None, create_key=None):  # pylint: disable=redefined-builtin\n      _message.Message._CheckCalledFromGeneratedFile()\n      if is_extension:\n        return _message.default_pool.FindExtensionByName(full_name)\n      else:\n        return _message.default_pool.FindFieldByName(full_name)\n\n  def __init__(self, name, full_name, index, number, type, cpp_type, label,\n               default_value, message_type, enum_type, containing_type,\n               is_extension, extension_scope, options=None,\n               serialized_options=None,\n               has_default_value=True, containing_oneof=None, json_name=None,\n               file=None, create_key=None):  # pylint: disable=redefined-builtin\n    \"\"\"The arguments are as described in the description of FieldDescriptor\n    attributes above.\n\n    Note that containing_type may be None, and may be set later if necessary\n    (to deal with circular references between message types, for example).\n    Likewise for extension_scope.\n    \"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('FieldDescriptor')\n\n    super(FieldDescriptor, self).__init__(\n        options, serialized_options, 'FieldOptions')\n    self.name = name\n    self.full_name = full_name\n    self.file = file\n    self._camelcase_name = None\n    if json_name is None:\n      self.json_name = _ToJsonName(name)\n    else:\n      self.json_name = json_name\n    self.index = index\n    self.number = number\n    self.type = type\n    self.cpp_type = cpp_type\n    self.label = label\n    self.has_default_value = has_default_value\n    self.default_value = default_value\n    self.containing_type = containing_type\n    self.message_type = message_type\n    self.enum_type = enum_type\n    self.is_extension = is_extension\n    self.extension_scope = extension_scope\n    self.containing_oneof = containing_oneof\n    if api_implementation.Type() == 'cpp':\n      if is_extension:\n        self._cdescriptor = _message.default_pool.FindExtensionByName(full_name)\n      else:\n        self._cdescriptor = _message.default_pool.FindFieldByName(full_name)\n    else:\n      self._cdescriptor = None\n\n  @property\n  def camelcase_name(self):\n    \"\"\"Camelcase name of this field.\n\n    Returns:\n      str: the name in CamelCase.\n    \"\"\"\n    if self._camelcase_name is None:\n      self._camelcase_name = _ToCamelCase(self.name)\n    return self._camelcase_name\n\n  @property\n  def has_presence(self):\n    \"\"\"Whether the field distinguishes between unpopulated and default values.\n\n    Raises:\n      RuntimeError: singular field that is not linked with message nor file.\n    \"\"\"\n    if self.label == FieldDescriptor.LABEL_REPEATED:\n      return False\n    if (self.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE or\n        self.containing_oneof):\n      return True\n    if hasattr(self.file, 'syntax'):\n      return self.file.syntax == 'proto2'\n    if hasattr(self.message_type, 'syntax'):\n      return self.message_type.syntax == 'proto2'\n    raise RuntimeError(\n        'has_presence is not ready to use because field %s is not'\n        ' linked with message type nor file' % self.full_name)\n\n  @staticmethod\n  def ProtoTypeToCppProtoType(proto_type):\n    \"\"\"Converts from a Python proto type to a C++ Proto Type.\n\n    The Python ProtocolBuffer classes specify both the 'Python' datatype and the\n    'C++' datatype - and they're not the same. This helper method should\n    translate from one to another.\n\n    Args:\n      proto_type: the Python proto type (descriptor.FieldDescriptor.TYPE_*)\n    Returns:\n      int: descriptor.FieldDescriptor.CPPTYPE_*, the C++ type.\n    Raises:\n      TypeTransformationError: when the Python proto type isn't known.\n    \"\"\"\n    try:\n      return FieldDescriptor._PYTHON_TO_CPP_PROTO_TYPE_MAP[proto_type]\n    except KeyError:\n      raise TypeTransformationError('Unknown proto_type: %s' % proto_type)\n\n\nclass EnumDescriptor(_NestedDescriptorBase):\n\n  \"\"\"Descriptor for an enum defined in a .proto file.\n\n  Attributes:\n    name (str): Name of the enum type.\n    full_name (str): Full name of the type, including package name\n      and any enclosing type(s).\n\n    values (list[EnumValueDescriptor]): List of the values\n      in this enum.\n    values_by_name (dict(str, EnumValueDescriptor)): Same as :attr:`values`,\n      but indexed by the \"name\" field of each EnumValueDescriptor.\n    values_by_number (dict(int, EnumValueDescriptor)): Same as :attr:`values`,\n      but indexed by the \"number\" field of each EnumValueDescriptor.\n    containing_type (Descriptor): Descriptor of the immediate containing\n      type of this enum, or None if this is an enum defined at the\n      top level in a .proto file.  Set by Descriptor's constructor\n      if we're passed into one.\n    file (FileDescriptor): Reference to file descriptor.\n    options (descriptor_pb2.EnumOptions): Enum options message or\n      None to use default enum options.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.EnumDescriptor\n\n    def __new__(cls, name, full_name, filename, values,\n                containing_type=None, options=None,\n                serialized_options=None, file=None,  # pylint: disable=redefined-builtin\n                serialized_start=None, serialized_end=None, create_key=None):\n      _message.Message._CheckCalledFromGeneratedFile()\n      return _message.default_pool.FindEnumTypeByName(full_name)\n\n  def __init__(self, name, full_name, filename, values,\n               containing_type=None, options=None,\n               serialized_options=None, file=None,  # pylint: disable=redefined-builtin\n               serialized_start=None, serialized_end=None, create_key=None):\n    \"\"\"Arguments are as described in the attribute description above.\n\n    Note that filename is an obsolete argument, that is not used anymore.\n    Please use file.name to access this as an attribute.\n    \"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('EnumDescriptor')\n\n    super(EnumDescriptor, self).__init__(\n        options, 'EnumOptions', name, full_name, file,\n        containing_type, serialized_start=serialized_start,\n        serialized_end=serialized_end, serialized_options=serialized_options)\n\n    self.values = values\n    for value in self.values:\n      value.type = self\n    self.values_by_name = dict((v.name, v) for v in values)\n    # Values are reversed to ensure that the first alias is retained.\n    self.values_by_number = dict((v.number, v) for v in reversed(values))\n\n  def CopyToProto(self, proto):\n    \"\"\"Copies this to a descriptor_pb2.EnumDescriptorProto.\n\n    Args:\n      proto (descriptor_pb2.EnumDescriptorProto): An empty descriptor proto.\n    \"\"\"\n    # This function is overridden to give a better doc comment.\n    super(EnumDescriptor, self).CopyToProto(proto)\n\n\nclass EnumValueDescriptor(DescriptorBase):\n\n  \"\"\"Descriptor for a single value within an enum.\n\n  Attributes:\n    name (str): Name of this value.\n    index (int): Dense, 0-indexed index giving the order that this\n      value appears textually within its enum in the .proto file.\n    number (int): Actual number assigned to this enum value.\n    type (EnumDescriptor): :class:`EnumDescriptor` to which this value\n      belongs.  Set by :class:`EnumDescriptor`'s constructor if we're\n      passed into one.\n    options (descriptor_pb2.EnumValueOptions): Enum value options message or\n      None to use default enum value options options.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.EnumValueDescriptor\n\n    def __new__(cls, name, index, number,\n                type=None,  # pylint: disable=redefined-builtin\n                options=None, serialized_options=None, create_key=None):\n      _message.Message._CheckCalledFromGeneratedFile()\n      # There is no way we can build a complete EnumValueDescriptor with the\n      # given parameters (the name of the Enum is not known, for example).\n      # Fortunately generated files just pass it to the EnumDescriptor()\n      # constructor, which will ignore it, so returning None is good enough.\n      return None\n\n  def __init__(self, name, index, number,\n               type=None,  # pylint: disable=redefined-builtin\n               options=None, serialized_options=None, create_key=None):\n    \"\"\"Arguments are as described in the attribute description above.\"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('EnumValueDescriptor')\n\n    super(EnumValueDescriptor, self).__init__(\n        options, serialized_options, 'EnumValueOptions')\n    self.name = name\n    self.index = index\n    self.number = number\n    self.type = type\n\n\nclass OneofDescriptor(DescriptorBase):\n  \"\"\"Descriptor for a oneof field.\n\n  Attributes:\n    name (str): Name of the oneof field.\n    full_name (str): Full name of the oneof field, including package name.\n    index (int): 0-based index giving the order of the oneof field inside\n      its containing type.\n    containing_type (Descriptor): :class:`Descriptor` of the protocol message\n      type that contains this field.  Set by the :class:`Descriptor` constructor\n      if we're passed into one.\n    fields (list[FieldDescriptor]): The list of field descriptors this\n      oneof can contain.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.OneofDescriptor\n\n    def __new__(\n        cls, name, full_name, index, containing_type, fields, options=None,\n        serialized_options=None, create_key=None):\n      _message.Message._CheckCalledFromGeneratedFile()\n      return _message.default_pool.FindOneofByName(full_name)\n\n  def __init__(\n      self, name, full_name, index, containing_type, fields, options=None,\n      serialized_options=None, create_key=None):\n    \"\"\"Arguments are as described in the attribute description above.\"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('OneofDescriptor')\n\n    super(OneofDescriptor, self).__init__(\n        options, serialized_options, 'OneofOptions')\n    self.name = name\n    self.full_name = full_name\n    self.index = index\n    self.containing_type = containing_type\n    self.fields = fields\n\n\nclass ServiceDescriptor(_NestedDescriptorBase):\n\n  \"\"\"Descriptor for a service.\n\n  Attributes:\n    name (str): Name of the service.\n    full_name (str): Full name of the service, including package name.\n    index (int): 0-indexed index giving the order that this services\n      definition appears within the .proto file.\n    methods (list[MethodDescriptor]): List of methods provided by this\n      service.\n    methods_by_name (dict(str, MethodDescriptor)): Same\n      :class:`MethodDescriptor` objects as in :attr:`methods_by_name`, but\n      indexed by \"name\" attribute in each :class:`MethodDescriptor`.\n    options (descriptor_pb2.ServiceOptions): Service options message or\n      None to use default service options.\n    file (FileDescriptor): Reference to file info.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.ServiceDescriptor\n\n    def __new__(\n        cls,\n        name=None,\n        full_name=None,\n        index=None,\n        methods=None,\n        options=None,\n        serialized_options=None,\n        file=None,  # pylint: disable=redefined-builtin\n        serialized_start=None,\n        serialized_end=None,\n        create_key=None):\n      _message.Message._CheckCalledFromGeneratedFile()  # pylint: disable=protected-access\n      return _message.default_pool.FindServiceByName(full_name)\n\n  def __init__(self, name, full_name, index, methods, options=None,\n               serialized_options=None, file=None,  # pylint: disable=redefined-builtin\n               serialized_start=None, serialized_end=None, create_key=None):\n    if create_key is not _internal_create_key:\n      _Deprecated('ServiceDescriptor')\n\n    super(ServiceDescriptor, self).__init__(\n        options, 'ServiceOptions', name, full_name, file,\n        None, serialized_start=serialized_start,\n        serialized_end=serialized_end, serialized_options=serialized_options)\n    self.index = index\n    self.methods = methods\n    self.methods_by_name = dict((m.name, m) for m in methods)\n    # Set the containing service for each method in this service.\n    for method in self.methods:\n      method.containing_service = self\n\n  def FindMethodByName(self, name):\n    \"\"\"Searches for the specified method, and returns its descriptor.\n\n    Args:\n      name (str): Name of the method.\n    Returns:\n      MethodDescriptor or None: the descriptor for the requested method, if\n      found.\n    \"\"\"\n    return self.methods_by_name.get(name, None)\n\n  def CopyToProto(self, proto):\n    \"\"\"Copies this to a descriptor_pb2.ServiceDescriptorProto.\n\n    Args:\n      proto (descriptor_pb2.ServiceDescriptorProto): An empty descriptor proto.\n    \"\"\"\n    # This function is overridden to give a better doc comment.\n    super(ServiceDescriptor, self).CopyToProto(proto)\n\n\nclass MethodDescriptor(DescriptorBase):\n\n  \"\"\"Descriptor for a method in a service.\n\n  Attributes:\n    name (str): Name of the method within the service.\n    full_name (str): Full name of method.\n    index (int): 0-indexed index of the method inside the service.\n    containing_service (ServiceDescriptor): The service that contains this\n      method.\n    input_type (Descriptor): The descriptor of the message that this method\n      accepts.\n    output_type (Descriptor): The descriptor of the message that this method\n      returns.\n    client_streaming (bool): Whether this method uses client streaming.\n    server_streaming (bool): Whether this method uses server streaming.\n    options (descriptor_pb2.MethodOptions or None): Method options message, or\n      None to use default method options.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.MethodDescriptor\n\n    def __new__(cls,\n                name,\n                full_name,\n                index,\n                containing_service,\n                input_type,\n                output_type,\n                client_streaming=False,\n                server_streaming=False,\n                options=None,\n                serialized_options=None,\n                create_key=None):\n      _message.Message._CheckCalledFromGeneratedFile()  # pylint: disable=protected-access\n      return _message.default_pool.FindMethodByName(full_name)\n\n  def __init__(self,\n               name,\n               full_name,\n               index,\n               containing_service,\n               input_type,\n               output_type,\n               client_streaming=False,\n               server_streaming=False,\n               options=None,\n               serialized_options=None,\n               create_key=None):\n    \"\"\"The arguments are as described in the description of MethodDescriptor\n    attributes above.\n\n    Note that containing_service may be None, and may be set later if necessary.\n    \"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('MethodDescriptor')\n\n    super(MethodDescriptor, self).__init__(\n        options, serialized_options, 'MethodOptions')\n    self.name = name\n    self.full_name = full_name\n    self.index = index\n    self.containing_service = containing_service\n    self.input_type = input_type\n    self.output_type = output_type\n    self.client_streaming = client_streaming\n    self.server_streaming = server_streaming\n\n  def CopyToProto(self, proto):\n    \"\"\"Copies this to a descriptor_pb2.MethodDescriptorProto.\n\n    Args:\n      proto (descriptor_pb2.MethodDescriptorProto): An empty descriptor proto.\n\n    Raises:\n      Error: If self couldn't be serialized, due to too few constructor\n        arguments.\n    \"\"\"\n    if self.containing_service is not None:\n      from google.protobuf import descriptor_pb2\n      service_proto = descriptor_pb2.ServiceDescriptorProto()\n      self.containing_service.CopyToProto(service_proto)\n      proto.CopyFrom(service_proto.method[self.index])\n    else:\n      raise Error('Descriptor does not contain a service.')\n\n\nclass FileDescriptor(DescriptorBase):\n  \"\"\"Descriptor for a file. Mimics the descriptor_pb2.FileDescriptorProto.\n\n  Note that :attr:`enum_types_by_name`, :attr:`extensions_by_name`, and\n  :attr:`dependencies` fields are only set by the\n  :py:mod:`google.protobuf.message_factory` module, and not by the generated\n  proto code.\n\n  Attributes:\n    name (str): Name of file, relative to root of source tree.\n    package (str): Name of the package\n    syntax (str): string indicating syntax of the file (can be \"proto2\" or\n      \"proto3\")\n    serialized_pb (bytes): Byte string of serialized\n      :class:`descriptor_pb2.FileDescriptorProto`.\n    dependencies (list[FileDescriptor]): List of other :class:`FileDescriptor`\n      objects this :class:`FileDescriptor` depends on.\n    public_dependencies (list[FileDescriptor]): A subset of\n      :attr:`dependencies`, which were declared as \"public\".\n    message_types_by_name (dict(str, Descriptor)): Mapping from message names\n      to their :class:`Descriptor`.\n    enum_types_by_name (dict(str, EnumDescriptor)): Mapping from enum names to\n      their :class:`EnumDescriptor`.\n    extensions_by_name (dict(str, FieldDescriptor)): Mapping from extension\n      names declared at file scope to their :class:`FieldDescriptor`.\n    services_by_name (dict(str, ServiceDescriptor)): Mapping from services'\n      names to their :class:`ServiceDescriptor`.\n    pool (DescriptorPool): The pool this descriptor belongs to.  When not\n      passed to the constructor, the global default pool is used.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.FileDescriptor\n\n    def __new__(cls, name, package, options=None,\n                serialized_options=None, serialized_pb=None,\n                dependencies=None, public_dependencies=None,\n                syntax=None, pool=None, create_key=None):\n      # FileDescriptor() is called from various places, not only from generated\n      # files, to register dynamic proto files and messages.\n      # pylint: disable=g-explicit-bool-comparison\n      if serialized_pb == b'':\n        # Cpp generated code must be linked in if serialized_pb is ''\n        try:\n          return _message.default_pool.FindFileByName(name)\n        except KeyError:\n          raise RuntimeError('Please link in cpp generated lib for %s' % (name))\n      elif serialized_pb:\n        return _message.default_pool.AddSerializedFile(serialized_pb)\n      else:\n        return super(FileDescriptor, cls).__new__(cls)\n\n  def __init__(self, name, package, options=None,\n               serialized_options=None, serialized_pb=None,\n               dependencies=None, public_dependencies=None,\n               syntax=None, pool=None, create_key=None):\n    \"\"\"Constructor.\"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('FileDescriptor')\n\n    super(FileDescriptor, self).__init__(\n        options, serialized_options, 'FileOptions')\n\n    if pool is None:\n      from google.protobuf import descriptor_pool\n      pool = descriptor_pool.Default()\n    self.pool = pool\n    self.message_types_by_name = {}\n    self.name = name\n    self.package = package\n    self.syntax = syntax or \"proto2\"\n    self.serialized_pb = serialized_pb\n\n    self.enum_types_by_name = {}\n    self.extensions_by_name = {}\n    self.services_by_name = {}\n    self.dependencies = (dependencies or [])\n    self.public_dependencies = (public_dependencies or [])\n\n  def CopyToProto(self, proto):\n    \"\"\"Copies this to a descriptor_pb2.FileDescriptorProto.\n\n    Args:\n      proto: An empty descriptor_pb2.FileDescriptorProto.\n    \"\"\"\n    proto.ParseFromString(self.serialized_pb)\n\n\ndef _ParseOptions(message, string):\n  \"\"\"Parses serialized options.\n\n  This helper function is used to parse serialized options in generated\n  proto2 files. It must not be used outside proto2.\n  \"\"\"\n  message.ParseFromString(string)\n  return message\n\n\ndef _ToCamelCase(name):\n  \"\"\"Converts name to camel-case and returns it.\"\"\"\n  capitalize_next = False\n  result = []\n\n  for c in name:\n    if c == '_':\n      if result:\n        capitalize_next = True\n    elif capitalize_next:\n      result.append(c.upper())\n      capitalize_next = False\n    else:\n      result += c\n\n  # Lower-case the first letter.\n  if result and result[0].isupper():\n    result[0] = result[0].lower()\n  return ''.join(result)\n\n\ndef _OptionsOrNone(descriptor_proto):\n  \"\"\"Returns the value of the field `options`, or None if it is not set.\"\"\"\n  if descriptor_proto.HasField('options'):\n    return descriptor_proto.options\n  else:\n    return None\n\n\ndef _ToJsonName(name):\n  \"\"\"Converts name to Json name and returns it.\"\"\"\n  capitalize_next = False\n  result = []\n\n  for c in name:\n    if c == '_':\n      capitalize_next = True\n    elif capitalize_next:\n      result.append(c.upper())\n      capitalize_next = False\n    else:\n      result += c\n\n  return ''.join(result)\n\n\ndef MakeDescriptor(desc_proto, package='', build_file_if_cpp=True,\n                   syntax=None):\n  \"\"\"Make a protobuf Descriptor given a DescriptorProto protobuf.\n\n  Handles nested descriptors. Note that this is limited to the scope of defining\n  a message inside of another message. Composite fields can currently only be\n  resolved if the message is defined in the same scope as the field.\n\n  Args:\n    desc_proto: The descriptor_pb2.DescriptorProto protobuf message.\n    package: Optional package name for the new message Descriptor (string).\n    build_file_if_cpp: Update the C++ descriptor pool if api matches.\n                       Set to False on recursion, so no duplicates are created.\n    syntax: The syntax/semantics that should be used.  Set to \"proto3\" to get\n            proto3 field presence semantics.\n  Returns:\n    A Descriptor for protobuf messages.\n  \"\"\"\n  if api_implementation.Type() == 'cpp' and build_file_if_cpp:\n    # The C++ implementation requires all descriptors to be backed by the same\n    # definition in the C++ descriptor pool. To do this, we build a\n    # FileDescriptorProto with the same definition as this descriptor and build\n    # it into the pool.\n    from google.protobuf import descriptor_pb2\n    file_descriptor_proto = descriptor_pb2.FileDescriptorProto()\n    file_descriptor_proto.message_type.add().MergeFrom(desc_proto)\n\n    # Generate a random name for this proto file to prevent conflicts with any\n    # imported ones. We need to specify a file name so the descriptor pool\n    # accepts our FileDescriptorProto, but it is not important what that file\n    # name is actually set to.\n    proto_name = binascii.hexlify(os.urandom(16)).decode('ascii')\n\n    if package:\n      file_descriptor_proto.name = os.path.join(package.replace('.', '/'),\n                                                proto_name + '.proto')\n      file_descriptor_proto.package = package\n    else:\n      file_descriptor_proto.name = proto_name + '.proto'\n\n    _message.default_pool.Add(file_descriptor_proto)\n    result = _message.default_pool.FindFileByName(file_descriptor_proto.name)\n\n    if _USE_C_DESCRIPTORS:\n      return result.message_types_by_name[desc_proto.name]\n\n  full_message_name = [desc_proto.name]\n  if package: full_message_name.insert(0, package)\n\n  # Create Descriptors for enum types\n  enum_types = {}\n  for enum_proto in desc_proto.enum_type:\n    full_name = '.'.join(full_message_name + [enum_proto.name])\n    enum_desc = EnumDescriptor(\n        enum_proto.name, full_name, None, [\n            EnumValueDescriptor(enum_val.name, ii, enum_val.number,\n                                create_key=_internal_create_key)\n            for ii, enum_val in enumerate(enum_proto.value)],\n        create_key=_internal_create_key)\n    enum_types[full_name] = enum_desc\n\n  # Create Descriptors for nested types\n  nested_types = {}\n  for nested_proto in desc_proto.nested_type:\n    full_name = '.'.join(full_message_name + [nested_proto.name])\n    # Nested types are just those defined inside of the message, not all types\n    # used by fields in the message, so no loops are possible here.\n    nested_desc = MakeDescriptor(nested_proto,\n                                 package='.'.join(full_message_name),\n                                 build_file_if_cpp=False,\n                                 syntax=syntax)\n    nested_types[full_name] = nested_desc\n\n  fields = []\n  for field_proto in desc_proto.field:\n    full_name = '.'.join(full_message_name + [field_proto.name])\n    enum_desc = None\n    nested_desc = None\n    if field_proto.json_name:\n      json_name = field_proto.json_name\n    else:\n      json_name = None\n    if field_proto.HasField('type_name'):\n      type_name = field_proto.type_name\n      full_type_name = '.'.join(full_message_name +\n                                [type_name[type_name.rfind('.')+1:]])\n      if full_type_name in nested_types:\n        nested_desc = nested_types[full_type_name]\n      elif full_type_name in enum_types:\n        enum_desc = enum_types[full_type_name]\n      # Else type_name references a non-local type, which isn't implemented\n    field = FieldDescriptor(\n        field_proto.name, full_name, field_proto.number - 1,\n        field_proto.number, field_proto.type,\n        FieldDescriptor.ProtoTypeToCppProtoType(field_proto.type),\n        field_proto.label, None, nested_desc, enum_desc, None, False, None,\n        options=_OptionsOrNone(field_proto), has_default_value=False,\n        json_name=json_name, create_key=_internal_create_key)\n    fields.append(field)\n\n  desc_name = '.'.join(full_message_name)\n  return Descriptor(desc_proto.name, desc_name, None, None, fields,\n                    list(nested_types.values()), list(enum_types.values()), [],\n                    options=_OptionsOrNone(desc_proto),\n                    create_key=_internal_create_key)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/descriptor_database.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Provides a container for DescriptorProtos.\"\"\"\n\n__author__ = 'matthewtoia@google.com (Matt Toia)'\n\nimport warnings\n\n\nclass Error(Exception):\n  pass\n\n\nclass DescriptorDatabaseConflictingDefinitionError(Error):\n  \"\"\"Raised when a proto is added with the same name & different descriptor.\"\"\"\n\n\nclass DescriptorDatabase(object):\n  \"\"\"A container accepting FileDescriptorProtos and maps DescriptorProtos.\"\"\"\n\n  def __init__(self):\n    self._file_desc_protos_by_file = {}\n    self._file_desc_protos_by_symbol = {}\n\n  def Add(self, file_desc_proto):\n    \"\"\"Adds the FileDescriptorProto and its types to this database.\n\n    Args:\n      file_desc_proto: The FileDescriptorProto to add.\n    Raises:\n      DescriptorDatabaseConflictingDefinitionError: if an attempt is made to\n        add a proto with the same name but different definition than an\n        existing proto in the database.\n    \"\"\"\n    proto_name = file_desc_proto.name\n    if proto_name not in self._file_desc_protos_by_file:\n      self._file_desc_protos_by_file[proto_name] = file_desc_proto\n    elif self._file_desc_protos_by_file[proto_name] != file_desc_proto:\n      raise DescriptorDatabaseConflictingDefinitionError(\n          '%s already added, but with different descriptor.' % proto_name)\n    else:\n      return\n\n    # Add all the top-level descriptors to the index.\n    package = file_desc_proto.package\n    for message in file_desc_proto.message_type:\n      for name in _ExtractSymbols(message, package):\n        self._AddSymbol(name, file_desc_proto)\n    for enum in file_desc_proto.enum_type:\n      self._AddSymbol(('.'.join((package, enum.name))), file_desc_proto)\n      for enum_value in enum.value:\n        self._file_desc_protos_by_symbol[\n            '.'.join((package, enum_value.name))] = file_desc_proto\n    for extension in file_desc_proto.extension:\n      self._AddSymbol(('.'.join((package, extension.name))), file_desc_proto)\n    for service in file_desc_proto.service:\n      self._AddSymbol(('.'.join((package, service.name))), file_desc_proto)\n\n  def FindFileByName(self, name):\n    \"\"\"Finds the file descriptor proto by file name.\n\n    Typically the file name is a relative path ending to a .proto file. The\n    proto with the given name will have to have been added to this database\n    using the Add method or else an error will be raised.\n\n    Args:\n      name: The file name to find.\n\n    Returns:\n      The file descriptor proto matching the name.\n\n    Raises:\n      KeyError if no file by the given name was added.\n    \"\"\"\n\n    return self._file_desc_protos_by_file[name]\n\n  def FindFileContainingSymbol(self, symbol):\n    \"\"\"Finds the file descriptor proto containing the specified symbol.\n\n    The symbol should be a fully qualified name including the file descriptor's\n    package and any containing messages. Some examples:\n\n    'some.package.name.Message'\n    'some.package.name.Message.NestedEnum'\n    'some.package.name.Message.some_field'\n\n    The file descriptor proto containing the specified symbol must be added to\n    this database using the Add method or else an error will be raised.\n\n    Args:\n      symbol: The fully qualified symbol name.\n\n    Returns:\n      The file descriptor proto containing the symbol.\n\n    Raises:\n      KeyError if no file contains the specified symbol.\n    \"\"\"\n    try:\n      return self._file_desc_protos_by_symbol[symbol]\n    except KeyError:\n      # Fields, enum values, and nested extensions are not in\n      # _file_desc_protos_by_symbol. Try to find the top level\n      # descriptor. Non-existent nested symbol under a valid top level\n      # descriptor can also be found. The behavior is the same with\n      # protobuf C++.\n      top_level, _, _ = symbol.rpartition('.')\n      try:\n        return self._file_desc_protos_by_symbol[top_level]\n      except KeyError:\n        # Raise the original symbol as a KeyError for better diagnostics.\n        raise KeyError(symbol)\n\n  def FindFileContainingExtension(self, extendee_name, extension_number):\n    # TODO(jieluo): implement this API.\n    return None\n\n  def FindAllExtensionNumbers(self, extendee_name):\n    # TODO(jieluo): implement this API.\n    return []\n\n  def _AddSymbol(self, name, file_desc_proto):\n    if name in self._file_desc_protos_by_symbol:\n      warn_msg = ('Conflict register for file \"' + file_desc_proto.name +\n                  '\": ' + name +\n                  ' is already defined in file \"' +\n                  self._file_desc_protos_by_symbol[name].name + '\"')\n      warnings.warn(warn_msg, RuntimeWarning)\n    self._file_desc_protos_by_symbol[name] = file_desc_proto\n\n\ndef _ExtractSymbols(desc_proto, package):\n  \"\"\"Pulls out all the symbols from a descriptor proto.\n\n  Args:\n    desc_proto: The proto to extract symbols from.\n    package: The package containing the descriptor type.\n\n  Yields:\n    The fully qualified name found in the descriptor.\n  \"\"\"\n  message_name = package + '.' + desc_proto.name if package else desc_proto.name\n  yield message_name\n  for nested_type in desc_proto.nested_type:\n    for symbol in _ExtractSymbols(nested_type, message_name):\n      yield symbol\n  for enum_type in desc_proto.enum_type:\n    yield '.'.join((message_name, enum_type.name))\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/descriptor_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/descriptor.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nif _descriptor._USE_C_DESCRIPTORS == False:\n  DESCRIPTOR = _descriptor.FileDescriptor(\n    name='google/protobuf/descriptor.proto',\n    package='google.protobuf',\n    syntax='proto2',\n    serialized_options=None,\n    create_key=_descriptor._internal_create_key,\n    serialized_pb=b'\\n google/protobuf/descriptor.proto\\x12\\x0fgoogle.protobuf\\\"G\\n\\x11\\x46ileDescriptorSet\\x12\\x32\\n\\x04\\x66ile\\x18\\x01 \\x03(\\x0b\\x32$.google.protobuf.FileDescriptorProto\\\"\\xdb\\x03\\n\\x13\\x46ileDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0f\\n\\x07package\\x18\\x02 \\x01(\\t\\x12\\x12\\n\\ndependency\\x18\\x03 \\x03(\\t\\x12\\x19\\n\\x11public_dependency\\x18\\n \\x03(\\x05\\x12\\x17\\n\\x0fweak_dependency\\x18\\x0b \\x03(\\x05\\x12\\x36\\n\\x0cmessage_type\\x18\\x04 \\x03(\\x0b\\x32 .google.protobuf.DescriptorProto\\x12\\x37\\n\\tenum_type\\x18\\x05 \\x03(\\x0b\\x32$.google.protobuf.EnumDescriptorProto\\x12\\x38\\n\\x07service\\x18\\x06 \\x03(\\x0b\\x32\\'.google.protobuf.ServiceDescriptorProto\\x12\\x38\\n\\textension\\x18\\x07 \\x03(\\x0b\\x32%.google.protobuf.FieldDescriptorProto\\x12-\\n\\x07options\\x18\\x08 \\x01(\\x0b\\x32\\x1c.google.protobuf.FileOptions\\x12\\x39\\n\\x10source_code_info\\x18\\t \\x01(\\x0b\\x32\\x1f.google.protobuf.SourceCodeInfo\\x12\\x0e\\n\\x06syntax\\x18\\x0c \\x01(\\t\\\"\\xa9\\x05\\n\\x0f\\x44\\x65scriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x34\\n\\x05\\x66ield\\x18\\x02 \\x03(\\x0b\\x32%.google.protobuf.FieldDescriptorProto\\x12\\x38\\n\\textension\\x18\\x06 \\x03(\\x0b\\x32%.google.protobuf.FieldDescriptorProto\\x12\\x35\\n\\x0bnested_type\\x18\\x03 \\x03(\\x0b\\x32 .google.protobuf.DescriptorProto\\x12\\x37\\n\\tenum_type\\x18\\x04 \\x03(\\x0b\\x32$.google.protobuf.EnumDescriptorProto\\x12H\\n\\x0f\\x65xtension_range\\x18\\x05 \\x03(\\x0b\\x32/.google.protobuf.DescriptorProto.ExtensionRange\\x12\\x39\\n\\noneof_decl\\x18\\x08 \\x03(\\x0b\\x32%.google.protobuf.OneofDescriptorProto\\x12\\x30\\n\\x07options\\x18\\x07 \\x01(\\x0b\\x32\\x1f.google.protobuf.MessageOptions\\x12\\x46\\n\\x0ereserved_range\\x18\\t \\x03(\\x0b\\x32..google.protobuf.DescriptorProto.ReservedRange\\x12\\x15\\n\\rreserved_name\\x18\\n \\x03(\\t\\x1a\\x65\\n\\x0e\\x45xtensionRange\\x12\\r\\n\\x05start\\x18\\x01 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x02 \\x01(\\x05\\x12\\x37\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32&.google.protobuf.ExtensionRangeOptions\\x1a+\\n\\rReservedRange\\x12\\r\\n\\x05start\\x18\\x01 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x02 \\x01(\\x05\\\"g\\n\\x15\\x45xtensionRangeOptions\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\xd5\\x05\\n\\x14\\x46ieldDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0e\\n\\x06number\\x18\\x03 \\x01(\\x05\\x12:\\n\\x05label\\x18\\x04 \\x01(\\x0e\\x32+.google.protobuf.FieldDescriptorProto.Label\\x12\\x38\\n\\x04type\\x18\\x05 \\x01(\\x0e\\x32*.google.protobuf.FieldDescriptorProto.Type\\x12\\x11\\n\\ttype_name\\x18\\x06 \\x01(\\t\\x12\\x10\\n\\x08\\x65xtendee\\x18\\x02 \\x01(\\t\\x12\\x15\\n\\rdefault_value\\x18\\x07 \\x01(\\t\\x12\\x13\\n\\x0boneof_index\\x18\\t \\x01(\\x05\\x12\\x11\\n\\tjson_name\\x18\\n \\x01(\\t\\x12.\\n\\x07options\\x18\\x08 \\x01(\\x0b\\x32\\x1d.google.protobuf.FieldOptions\\x12\\x17\\n\\x0fproto3_optional\\x18\\x11 \\x01(\\x08\\\"\\xb6\\x02\\n\\x04Type\\x12\\x0f\\n\\x0bTYPE_DOUBLE\\x10\\x01\\x12\\x0e\\n\\nTYPE_FLOAT\\x10\\x02\\x12\\x0e\\n\\nTYPE_INT64\\x10\\x03\\x12\\x0f\\n\\x0bTYPE_UINT64\\x10\\x04\\x12\\x0e\\n\\nTYPE_INT32\\x10\\x05\\x12\\x10\\n\\x0cTYPE_FIXED64\\x10\\x06\\x12\\x10\\n\\x0cTYPE_FIXED32\\x10\\x07\\x12\\r\\n\\tTYPE_BOOL\\x10\\x08\\x12\\x0f\\n\\x0bTYPE_STRING\\x10\\t\\x12\\x0e\\n\\nTYPE_GROUP\\x10\\n\\x12\\x10\\n\\x0cTYPE_MESSAGE\\x10\\x0b\\x12\\x0e\\n\\nTYPE_BYTES\\x10\\x0c\\x12\\x0f\\n\\x0bTYPE_UINT32\\x10\\r\\x12\\r\\n\\tTYPE_ENUM\\x10\\x0e\\x12\\x11\\n\\rTYPE_SFIXED32\\x10\\x0f\\x12\\x11\\n\\rTYPE_SFIXED64\\x10\\x10\\x12\\x0f\\n\\x0bTYPE_SINT32\\x10\\x11\\x12\\x0f\\n\\x0bTYPE_SINT64\\x10\\x12\\\"C\\n\\x05Label\\x12\\x12\\n\\x0eLABEL_OPTIONAL\\x10\\x01\\x12\\x12\\n\\x0eLABEL_REQUIRED\\x10\\x02\\x12\\x12\\n\\x0eLABEL_REPEATED\\x10\\x03\\\"T\\n\\x14OneofDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12.\\n\\x07options\\x18\\x02 \\x01(\\x0b\\x32\\x1d.google.protobuf.OneofOptions\\\"\\xa4\\x02\\n\\x13\\x45numDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x38\\n\\x05value\\x18\\x02 \\x03(\\x0b\\x32).google.protobuf.EnumValueDescriptorProto\\x12-\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32\\x1c.google.protobuf.EnumOptions\\x12N\\n\\x0ereserved_range\\x18\\x04 \\x03(\\x0b\\x32\\x36.google.protobuf.EnumDescriptorProto.EnumReservedRange\\x12\\x15\\n\\rreserved_name\\x18\\x05 \\x03(\\t\\x1a/\\n\\x11\\x45numReservedRange\\x12\\r\\n\\x05start\\x18\\x01 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x02 \\x01(\\x05\\\"l\\n\\x18\\x45numValueDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0e\\n\\x06number\\x18\\x02 \\x01(\\x05\\x12\\x32\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32!.google.protobuf.EnumValueOptions\\\"\\x90\\x01\\n\\x16ServiceDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x36\\n\\x06method\\x18\\x02 \\x03(\\x0b\\x32&.google.protobuf.MethodDescriptorProto\\x12\\x30\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32\\x1f.google.protobuf.ServiceOptions\\\"\\xc1\\x01\\n\\x15MethodDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x12\\n\\ninput_type\\x18\\x02 \\x01(\\t\\x12\\x13\\n\\x0boutput_type\\x18\\x03 \\x01(\\t\\x12/\\n\\x07options\\x18\\x04 \\x01(\\x0b\\x32\\x1e.google.protobuf.MethodOptions\\x12\\x1f\\n\\x10\\x63lient_streaming\\x18\\x05 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x1f\\n\\x10server_streaming\\x18\\x06 \\x01(\\x08:\\x05\\x66\\x61lse\\\"\\xa5\\x06\\n\\x0b\\x46ileOptions\\x12\\x14\\n\\x0cjava_package\\x18\\x01 \\x01(\\t\\x12\\x1c\\n\\x14java_outer_classname\\x18\\x08 \\x01(\\t\\x12\\\"\\n\\x13java_multiple_files\\x18\\n \\x01(\\x08:\\x05\\x66\\x61lse\\x12)\\n\\x1djava_generate_equals_and_hash\\x18\\x14 \\x01(\\x08\\x42\\x02\\x18\\x01\\x12%\\n\\x16java_string_check_utf8\\x18\\x1b \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x46\\n\\x0coptimize_for\\x18\\t \\x01(\\x0e\\x32).google.protobuf.FileOptions.OptimizeMode:\\x05SPEED\\x12\\x12\\n\\ngo_package\\x18\\x0b \\x01(\\t\\x12\\\"\\n\\x13\\x63\\x63_generic_services\\x18\\x10 \\x01(\\x08:\\x05\\x66\\x61lse\\x12$\\n\\x15java_generic_services\\x18\\x11 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\\"\\n\\x13py_generic_services\\x18\\x12 \\x01(\\x08:\\x05\\x66\\x61lse\\x12#\\n\\x14php_generic_services\\x18* \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x19\\n\\ndeprecated\\x18\\x17 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x1e\\n\\x10\\x63\\x63_enable_arenas\\x18\\x1f \\x01(\\x08:\\x04true\\x12\\x19\\n\\x11objc_class_prefix\\x18$ \\x01(\\t\\x12\\x18\\n\\x10\\x63sharp_namespace\\x18% \\x01(\\t\\x12\\x14\\n\\x0cswift_prefix\\x18\\' \\x01(\\t\\x12\\x18\\n\\x10php_class_prefix\\x18( \\x01(\\t\\x12\\x15\\n\\rphp_namespace\\x18) \\x01(\\t\\x12\\x1e\\n\\x16php_metadata_namespace\\x18, \\x01(\\t\\x12\\x14\\n\\x0cruby_package\\x18- \\x01(\\t\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption\\\":\\n\\x0cOptimizeMode\\x12\\t\\n\\x05SPEED\\x10\\x01\\x12\\r\\n\\tCODE_SIZE\\x10\\x02\\x12\\x10\\n\\x0cLITE_RUNTIME\\x10\\x03*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08&\\x10\\'\\\"\\x84\\x02\\n\\x0eMessageOptions\\x12&\\n\\x17message_set_wire_format\\x18\\x01 \\x01(\\x08:\\x05\\x66\\x61lse\\x12.\\n\\x1fno_standard_descriptor_accessor\\x18\\x02 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x19\\n\\ndeprecated\\x18\\x03 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x11\\n\\tmap_entry\\x18\\x07 \\x01(\\x08\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08\\x04\\x10\\x05J\\x04\\x08\\x05\\x10\\x06J\\x04\\x08\\x06\\x10\\x07J\\x04\\x08\\x08\\x10\\tJ\\x04\\x08\\t\\x10\\n\\\"\\xbe\\x03\\n\\x0c\\x46ieldOptions\\x12:\\n\\x05\\x63type\\x18\\x01 \\x01(\\x0e\\x32#.google.protobuf.FieldOptions.CType:\\x06STRING\\x12\\x0e\\n\\x06packed\\x18\\x02 \\x01(\\x08\\x12?\\n\\x06jstype\\x18\\x06 \\x01(\\x0e\\x32$.google.protobuf.FieldOptions.JSType:\\tJS_NORMAL\\x12\\x13\\n\\x04lazy\\x18\\x05 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x1e\\n\\x0funverified_lazy\\x18\\x0f \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x19\\n\\ndeprecated\\x18\\x03 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x13\\n\\x04weak\\x18\\n \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption\\\"/\\n\\x05\\x43Type\\x12\\n\\n\\x06STRING\\x10\\x00\\x12\\x08\\n\\x04\\x43ORD\\x10\\x01\\x12\\x10\\n\\x0cSTRING_PIECE\\x10\\x02\\\"5\\n\\x06JSType\\x12\\r\\n\\tJS_NORMAL\\x10\\x00\\x12\\r\\n\\tJS_STRING\\x10\\x01\\x12\\r\\n\\tJS_NUMBER\\x10\\x02*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08\\x04\\x10\\x05\\\"^\\n\\x0cOneofOptions\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\x93\\x01\\n\\x0b\\x45numOptions\\x12\\x13\\n\\x0b\\x61llow_alias\\x18\\x02 \\x01(\\x08\\x12\\x19\\n\\ndeprecated\\x18\\x03 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08\\x05\\x10\\x06\\\"}\\n\\x10\\x45numValueOptions\\x12\\x19\\n\\ndeprecated\\x18\\x01 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"{\\n\\x0eServiceOptions\\x12\\x19\\n\\ndeprecated\\x18! \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\xad\\x02\\n\\rMethodOptions\\x12\\x19\\n\\ndeprecated\\x18! \\x01(\\x08:\\x05\\x66\\x61lse\\x12_\\n\\x11idempotency_level\\x18\\\" \\x01(\\x0e\\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\\x13IDEMPOTENCY_UNKNOWN\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption\\\"P\\n\\x10IdempotencyLevel\\x12\\x17\\n\\x13IDEMPOTENCY_UNKNOWN\\x10\\x00\\x12\\x13\\n\\x0fNO_SIDE_EFFECTS\\x10\\x01\\x12\\x0e\\n\\nIDEMPOTENT\\x10\\x02*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\x9e\\x02\\n\\x13UninterpretedOption\\x12;\\n\\x04name\\x18\\x02 \\x03(\\x0b\\x32-.google.protobuf.UninterpretedOption.NamePart\\x12\\x18\\n\\x10identifier_value\\x18\\x03 \\x01(\\t\\x12\\x1a\\n\\x12positive_int_value\\x18\\x04 \\x01(\\x04\\x12\\x1a\\n\\x12negative_int_value\\x18\\x05 \\x01(\\x03\\x12\\x14\\n\\x0c\\x64ouble_value\\x18\\x06 \\x01(\\x01\\x12\\x14\\n\\x0cstring_value\\x18\\x07 \\x01(\\x0c\\x12\\x17\\n\\x0f\\x61ggregate_value\\x18\\x08 \\x01(\\t\\x1a\\x33\\n\\x08NamePart\\x12\\x11\\n\\tname_part\\x18\\x01 \\x02(\\t\\x12\\x14\\n\\x0cis_extension\\x18\\x02 \\x02(\\x08\\\"\\xd5\\x01\\n\\x0eSourceCodeInfo\\x12:\\n\\x08location\\x18\\x01 \\x03(\\x0b\\x32(.google.protobuf.SourceCodeInfo.Location\\x1a\\x86\\x01\\n\\x08Location\\x12\\x10\\n\\x04path\\x18\\x01 \\x03(\\x05\\x42\\x02\\x10\\x01\\x12\\x10\\n\\x04span\\x18\\x02 \\x03(\\x05\\x42\\x02\\x10\\x01\\x12\\x18\\n\\x10leading_comments\\x18\\x03 \\x01(\\t\\x12\\x19\\n\\x11trailing_comments\\x18\\x04 \\x01(\\t\\x12!\\n\\x19leading_detached_comments\\x18\\x06 \\x03(\\t\\\"\\xa7\\x01\\n\\x11GeneratedCodeInfo\\x12\\x41\\n\\nannotation\\x18\\x01 \\x03(\\x0b\\x32-.google.protobuf.GeneratedCodeInfo.Annotation\\x1aO\\n\\nAnnotation\\x12\\x10\\n\\x04path\\x18\\x01 \\x03(\\x05\\x42\\x02\\x10\\x01\\x12\\x13\\n\\x0bsource_file\\x18\\x02 \\x01(\\t\\x12\\r\\n\\x05\\x62\\x65gin\\x18\\x03 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x04 \\x01(\\x05\\x42~\\n\\x13\\x63om.google.protobufB\\x10\\x44\\x65scriptorProtosH\\x01Z-google.golang.org/protobuf/types/descriptorpb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1aGoogle.Protobuf.Reflection'\n  )\nelse:\n  DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n google/protobuf/descriptor.proto\\x12\\x0fgoogle.protobuf\\\"G\\n\\x11\\x46ileDescriptorSet\\x12\\x32\\n\\x04\\x66ile\\x18\\x01 \\x03(\\x0b\\x32$.google.protobuf.FileDescriptorProto\\\"\\xdb\\x03\\n\\x13\\x46ileDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0f\\n\\x07package\\x18\\x02 \\x01(\\t\\x12\\x12\\n\\ndependency\\x18\\x03 \\x03(\\t\\x12\\x19\\n\\x11public_dependency\\x18\\n \\x03(\\x05\\x12\\x17\\n\\x0fweak_dependency\\x18\\x0b \\x03(\\x05\\x12\\x36\\n\\x0cmessage_type\\x18\\x04 \\x03(\\x0b\\x32 .google.protobuf.DescriptorProto\\x12\\x37\\n\\tenum_type\\x18\\x05 \\x03(\\x0b\\x32$.google.protobuf.EnumDescriptorProto\\x12\\x38\\n\\x07service\\x18\\x06 \\x03(\\x0b\\x32\\'.google.protobuf.ServiceDescriptorProto\\x12\\x38\\n\\textension\\x18\\x07 \\x03(\\x0b\\x32%.google.protobuf.FieldDescriptorProto\\x12-\\n\\x07options\\x18\\x08 \\x01(\\x0b\\x32\\x1c.google.protobuf.FileOptions\\x12\\x39\\n\\x10source_code_info\\x18\\t \\x01(\\x0b\\x32\\x1f.google.protobuf.SourceCodeInfo\\x12\\x0e\\n\\x06syntax\\x18\\x0c \\x01(\\t\\\"\\xa9\\x05\\n\\x0f\\x44\\x65scriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x34\\n\\x05\\x66ield\\x18\\x02 \\x03(\\x0b\\x32%.google.protobuf.FieldDescriptorProto\\x12\\x38\\n\\textension\\x18\\x06 \\x03(\\x0b\\x32%.google.protobuf.FieldDescriptorProto\\x12\\x35\\n\\x0bnested_type\\x18\\x03 \\x03(\\x0b\\x32 .google.protobuf.DescriptorProto\\x12\\x37\\n\\tenum_type\\x18\\x04 \\x03(\\x0b\\x32$.google.protobuf.EnumDescriptorProto\\x12H\\n\\x0f\\x65xtension_range\\x18\\x05 \\x03(\\x0b\\x32/.google.protobuf.DescriptorProto.ExtensionRange\\x12\\x39\\n\\noneof_decl\\x18\\x08 \\x03(\\x0b\\x32%.google.protobuf.OneofDescriptorProto\\x12\\x30\\n\\x07options\\x18\\x07 \\x01(\\x0b\\x32\\x1f.google.protobuf.MessageOptions\\x12\\x46\\n\\x0ereserved_range\\x18\\t \\x03(\\x0b\\x32..google.protobuf.DescriptorProto.ReservedRange\\x12\\x15\\n\\rreserved_name\\x18\\n \\x03(\\t\\x1a\\x65\\n\\x0e\\x45xtensionRange\\x12\\r\\n\\x05start\\x18\\x01 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x02 \\x01(\\x05\\x12\\x37\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32&.google.protobuf.ExtensionRangeOptions\\x1a+\\n\\rReservedRange\\x12\\r\\n\\x05start\\x18\\x01 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x02 \\x01(\\x05\\\"g\\n\\x15\\x45xtensionRangeOptions\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\xd5\\x05\\n\\x14\\x46ieldDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0e\\n\\x06number\\x18\\x03 \\x01(\\x05\\x12:\\n\\x05label\\x18\\x04 \\x01(\\x0e\\x32+.google.protobuf.FieldDescriptorProto.Label\\x12\\x38\\n\\x04type\\x18\\x05 \\x01(\\x0e\\x32*.google.protobuf.FieldDescriptorProto.Type\\x12\\x11\\n\\ttype_name\\x18\\x06 \\x01(\\t\\x12\\x10\\n\\x08\\x65xtendee\\x18\\x02 \\x01(\\t\\x12\\x15\\n\\rdefault_value\\x18\\x07 \\x01(\\t\\x12\\x13\\n\\x0boneof_index\\x18\\t \\x01(\\x05\\x12\\x11\\n\\tjson_name\\x18\\n \\x01(\\t\\x12.\\n\\x07options\\x18\\x08 \\x01(\\x0b\\x32\\x1d.google.protobuf.FieldOptions\\x12\\x17\\n\\x0fproto3_optional\\x18\\x11 \\x01(\\x08\\\"\\xb6\\x02\\n\\x04Type\\x12\\x0f\\n\\x0bTYPE_DOUBLE\\x10\\x01\\x12\\x0e\\n\\nTYPE_FLOAT\\x10\\x02\\x12\\x0e\\n\\nTYPE_INT64\\x10\\x03\\x12\\x0f\\n\\x0bTYPE_UINT64\\x10\\x04\\x12\\x0e\\n\\nTYPE_INT32\\x10\\x05\\x12\\x10\\n\\x0cTYPE_FIXED64\\x10\\x06\\x12\\x10\\n\\x0cTYPE_FIXED32\\x10\\x07\\x12\\r\\n\\tTYPE_BOOL\\x10\\x08\\x12\\x0f\\n\\x0bTYPE_STRING\\x10\\t\\x12\\x0e\\n\\nTYPE_GROUP\\x10\\n\\x12\\x10\\n\\x0cTYPE_MESSAGE\\x10\\x0b\\x12\\x0e\\n\\nTYPE_BYTES\\x10\\x0c\\x12\\x0f\\n\\x0bTYPE_UINT32\\x10\\r\\x12\\r\\n\\tTYPE_ENUM\\x10\\x0e\\x12\\x11\\n\\rTYPE_SFIXED32\\x10\\x0f\\x12\\x11\\n\\rTYPE_SFIXED64\\x10\\x10\\x12\\x0f\\n\\x0bTYPE_SINT32\\x10\\x11\\x12\\x0f\\n\\x0bTYPE_SINT64\\x10\\x12\\\"C\\n\\x05Label\\x12\\x12\\n\\x0eLABEL_OPTIONAL\\x10\\x01\\x12\\x12\\n\\x0eLABEL_REQUIRED\\x10\\x02\\x12\\x12\\n\\x0eLABEL_REPEATED\\x10\\x03\\\"T\\n\\x14OneofDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12.\\n\\x07options\\x18\\x02 \\x01(\\x0b\\x32\\x1d.google.protobuf.OneofOptions\\\"\\xa4\\x02\\n\\x13\\x45numDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x38\\n\\x05value\\x18\\x02 \\x03(\\x0b\\x32).google.protobuf.EnumValueDescriptorProto\\x12-\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32\\x1c.google.protobuf.EnumOptions\\x12N\\n\\x0ereserved_range\\x18\\x04 \\x03(\\x0b\\x32\\x36.google.protobuf.EnumDescriptorProto.EnumReservedRange\\x12\\x15\\n\\rreserved_name\\x18\\x05 \\x03(\\t\\x1a/\\n\\x11\\x45numReservedRange\\x12\\r\\n\\x05start\\x18\\x01 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x02 \\x01(\\x05\\\"l\\n\\x18\\x45numValueDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0e\\n\\x06number\\x18\\x02 \\x01(\\x05\\x12\\x32\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32!.google.protobuf.EnumValueOptions\\\"\\x90\\x01\\n\\x16ServiceDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x36\\n\\x06method\\x18\\x02 \\x03(\\x0b\\x32&.google.protobuf.MethodDescriptorProto\\x12\\x30\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32\\x1f.google.protobuf.ServiceOptions\\\"\\xc1\\x01\\n\\x15MethodDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x12\\n\\ninput_type\\x18\\x02 \\x01(\\t\\x12\\x13\\n\\x0boutput_type\\x18\\x03 \\x01(\\t\\x12/\\n\\x07options\\x18\\x04 \\x01(\\x0b\\x32\\x1e.google.protobuf.MethodOptions\\x12\\x1f\\n\\x10\\x63lient_streaming\\x18\\x05 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x1f\\n\\x10server_streaming\\x18\\x06 \\x01(\\x08:\\x05\\x66\\x61lse\\\"\\xa5\\x06\\n\\x0b\\x46ileOptions\\x12\\x14\\n\\x0cjava_package\\x18\\x01 \\x01(\\t\\x12\\x1c\\n\\x14java_outer_classname\\x18\\x08 \\x01(\\t\\x12\\\"\\n\\x13java_multiple_files\\x18\\n \\x01(\\x08:\\x05\\x66\\x61lse\\x12)\\n\\x1djava_generate_equals_and_hash\\x18\\x14 \\x01(\\x08\\x42\\x02\\x18\\x01\\x12%\\n\\x16java_string_check_utf8\\x18\\x1b \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x46\\n\\x0coptimize_for\\x18\\t \\x01(\\x0e\\x32).google.protobuf.FileOptions.OptimizeMode:\\x05SPEED\\x12\\x12\\n\\ngo_package\\x18\\x0b \\x01(\\t\\x12\\\"\\n\\x13\\x63\\x63_generic_services\\x18\\x10 \\x01(\\x08:\\x05\\x66\\x61lse\\x12$\\n\\x15java_generic_services\\x18\\x11 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\\"\\n\\x13py_generic_services\\x18\\x12 \\x01(\\x08:\\x05\\x66\\x61lse\\x12#\\n\\x14php_generic_services\\x18* \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x19\\n\\ndeprecated\\x18\\x17 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x1e\\n\\x10\\x63\\x63_enable_arenas\\x18\\x1f \\x01(\\x08:\\x04true\\x12\\x19\\n\\x11objc_class_prefix\\x18$ \\x01(\\t\\x12\\x18\\n\\x10\\x63sharp_namespace\\x18% \\x01(\\t\\x12\\x14\\n\\x0cswift_prefix\\x18\\' \\x01(\\t\\x12\\x18\\n\\x10php_class_prefix\\x18( \\x01(\\t\\x12\\x15\\n\\rphp_namespace\\x18) \\x01(\\t\\x12\\x1e\\n\\x16php_metadata_namespace\\x18, \\x01(\\t\\x12\\x14\\n\\x0cruby_package\\x18- \\x01(\\t\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption\\\":\\n\\x0cOptimizeMode\\x12\\t\\n\\x05SPEED\\x10\\x01\\x12\\r\\n\\tCODE_SIZE\\x10\\x02\\x12\\x10\\n\\x0cLITE_RUNTIME\\x10\\x03*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08&\\x10\\'\\\"\\x84\\x02\\n\\x0eMessageOptions\\x12&\\n\\x17message_set_wire_format\\x18\\x01 \\x01(\\x08:\\x05\\x66\\x61lse\\x12.\\n\\x1fno_standard_descriptor_accessor\\x18\\x02 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x19\\n\\ndeprecated\\x18\\x03 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x11\\n\\tmap_entry\\x18\\x07 \\x01(\\x08\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08\\x04\\x10\\x05J\\x04\\x08\\x05\\x10\\x06J\\x04\\x08\\x06\\x10\\x07J\\x04\\x08\\x08\\x10\\tJ\\x04\\x08\\t\\x10\\n\\\"\\xbe\\x03\\n\\x0c\\x46ieldOptions\\x12:\\n\\x05\\x63type\\x18\\x01 \\x01(\\x0e\\x32#.google.protobuf.FieldOptions.CType:\\x06STRING\\x12\\x0e\\n\\x06packed\\x18\\x02 \\x01(\\x08\\x12?\\n\\x06jstype\\x18\\x06 \\x01(\\x0e\\x32$.google.protobuf.FieldOptions.JSType:\\tJS_NORMAL\\x12\\x13\\n\\x04lazy\\x18\\x05 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x1e\\n\\x0funverified_lazy\\x18\\x0f \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x19\\n\\ndeprecated\\x18\\x03 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x13\\n\\x04weak\\x18\\n \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption\\\"/\\n\\x05\\x43Type\\x12\\n\\n\\x06STRING\\x10\\x00\\x12\\x08\\n\\x04\\x43ORD\\x10\\x01\\x12\\x10\\n\\x0cSTRING_PIECE\\x10\\x02\\\"5\\n\\x06JSType\\x12\\r\\n\\tJS_NORMAL\\x10\\x00\\x12\\r\\n\\tJS_STRING\\x10\\x01\\x12\\r\\n\\tJS_NUMBER\\x10\\x02*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08\\x04\\x10\\x05\\\"^\\n\\x0cOneofOptions\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\x93\\x01\\n\\x0b\\x45numOptions\\x12\\x13\\n\\x0b\\x61llow_alias\\x18\\x02 \\x01(\\x08\\x12\\x19\\n\\ndeprecated\\x18\\x03 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08\\x05\\x10\\x06\\\"}\\n\\x10\\x45numValueOptions\\x12\\x19\\n\\ndeprecated\\x18\\x01 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"{\\n\\x0eServiceOptions\\x12\\x19\\n\\ndeprecated\\x18! \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\xad\\x02\\n\\rMethodOptions\\x12\\x19\\n\\ndeprecated\\x18! \\x01(\\x08:\\x05\\x66\\x61lse\\x12_\\n\\x11idempotency_level\\x18\\\" \\x01(\\x0e\\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\\x13IDEMPOTENCY_UNKNOWN\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption\\\"P\\n\\x10IdempotencyLevel\\x12\\x17\\n\\x13IDEMPOTENCY_UNKNOWN\\x10\\x00\\x12\\x13\\n\\x0fNO_SIDE_EFFECTS\\x10\\x01\\x12\\x0e\\n\\nIDEMPOTENT\\x10\\x02*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\x9e\\x02\\n\\x13UninterpretedOption\\x12;\\n\\x04name\\x18\\x02 \\x03(\\x0b\\x32-.google.protobuf.UninterpretedOption.NamePart\\x12\\x18\\n\\x10identifier_value\\x18\\x03 \\x01(\\t\\x12\\x1a\\n\\x12positive_int_value\\x18\\x04 \\x01(\\x04\\x12\\x1a\\n\\x12negative_int_value\\x18\\x05 \\x01(\\x03\\x12\\x14\\n\\x0c\\x64ouble_value\\x18\\x06 \\x01(\\x01\\x12\\x14\\n\\x0cstring_value\\x18\\x07 \\x01(\\x0c\\x12\\x17\\n\\x0f\\x61ggregate_value\\x18\\x08 \\x01(\\t\\x1a\\x33\\n\\x08NamePart\\x12\\x11\\n\\tname_part\\x18\\x01 \\x02(\\t\\x12\\x14\\n\\x0cis_extension\\x18\\x02 \\x02(\\x08\\\"\\xd5\\x01\\n\\x0eSourceCodeInfo\\x12:\\n\\x08location\\x18\\x01 \\x03(\\x0b\\x32(.google.protobuf.SourceCodeInfo.Location\\x1a\\x86\\x01\\n\\x08Location\\x12\\x10\\n\\x04path\\x18\\x01 \\x03(\\x05\\x42\\x02\\x10\\x01\\x12\\x10\\n\\x04span\\x18\\x02 \\x03(\\x05\\x42\\x02\\x10\\x01\\x12\\x18\\n\\x10leading_comments\\x18\\x03 \\x01(\\t\\x12\\x19\\n\\x11trailing_comments\\x18\\x04 \\x01(\\t\\x12!\\n\\x19leading_detached_comments\\x18\\x06 \\x03(\\t\\\"\\xa7\\x01\\n\\x11GeneratedCodeInfo\\x12\\x41\\n\\nannotation\\x18\\x01 \\x03(\\x0b\\x32-.google.protobuf.GeneratedCodeInfo.Annotation\\x1aO\\n\\nAnnotation\\x12\\x10\\n\\x04path\\x18\\x01 \\x03(\\x05\\x42\\x02\\x10\\x01\\x12\\x13\\n\\x0bsource_file\\x18\\x02 \\x01(\\t\\x12\\r\\n\\x05\\x62\\x65gin\\x18\\x03 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x04 \\x01(\\x05\\x42~\\n\\x13\\x63om.google.protobufB\\x10\\x44\\x65scriptorProtosH\\x01Z-google.golang.org/protobuf/types/descriptorpb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1aGoogle.Protobuf.Reflection')\n\nif _descriptor._USE_C_DESCRIPTORS == False:\n  _FIELDDESCRIPTORPROTO_TYPE = _descriptor.EnumDescriptor(\n    name='Type',\n    full_name='google.protobuf.FieldDescriptorProto.Type',\n    filename=None,\n    file=DESCRIPTOR,\n    create_key=_descriptor._internal_create_key,\n    values=[\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_DOUBLE', index=0, number=1,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_FLOAT', index=1, number=2,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_INT64', index=2, number=3,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_UINT64', index=3, number=4,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_INT32', index=4, number=5,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_FIXED64', index=5, number=6,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_FIXED32', index=6, number=7,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_BOOL', index=7, number=8,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_STRING', index=8, number=9,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_GROUP', index=9, number=10,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_MESSAGE', index=10, number=11,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_BYTES', index=11, number=12,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_UINT32', index=12, number=13,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_ENUM', index=13, number=14,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_SFIXED32', index=14, number=15,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_SFIXED64', index=15, number=16,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_SINT32', index=16, number=17,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_SINT64', index=17, number=18,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n    ],\n    containing_type=None,\n    serialized_options=None,\n  )\n  _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_TYPE)\n\n  _FIELDDESCRIPTORPROTO_LABEL = _descriptor.EnumDescriptor(\n    name='Label',\n    full_name='google.protobuf.FieldDescriptorProto.Label',\n    filename=None,\n    file=DESCRIPTOR,\n    create_key=_descriptor._internal_create_key,\n    values=[\n      _descriptor.EnumValueDescriptor(\n        name='LABEL_OPTIONAL', index=0, number=1,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='LABEL_REQUIRED', index=1, number=2,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='LABEL_REPEATED', index=2, number=3,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n    ],\n    containing_type=None,\n    serialized_options=None,\n  )\n  _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_LABEL)\n\n  _FILEOPTIONS_OPTIMIZEMODE = _descriptor.EnumDescriptor(\n    name='OptimizeMode',\n    full_name='google.protobuf.FileOptions.OptimizeMode',\n    filename=None,\n    file=DESCRIPTOR,\n    create_key=_descriptor._internal_create_key,\n    values=[\n      _descriptor.EnumValueDescriptor(\n        name='SPEED', index=0, number=1,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='CODE_SIZE', index=1, number=2,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='LITE_RUNTIME', index=2, number=3,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n    ],\n    containing_type=None,\n    serialized_options=None,\n  )\n  _sym_db.RegisterEnumDescriptor(_FILEOPTIONS_OPTIMIZEMODE)\n\n  _FIELDOPTIONS_CTYPE = _descriptor.EnumDescriptor(\n    name='CType',\n    full_name='google.protobuf.FieldOptions.CType',\n    filename=None,\n    file=DESCRIPTOR,\n    create_key=_descriptor._internal_create_key,\n    values=[\n      _descriptor.EnumValueDescriptor(\n        name='STRING', index=0, number=0,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='CORD', index=1, number=1,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='STRING_PIECE', index=2, number=2,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n    ],\n    containing_type=None,\n    serialized_options=None,\n  )\n  _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_CTYPE)\n\n  _FIELDOPTIONS_JSTYPE = _descriptor.EnumDescriptor(\n    name='JSType',\n    full_name='google.protobuf.FieldOptions.JSType',\n    filename=None,\n    file=DESCRIPTOR,\n    create_key=_descriptor._internal_create_key,\n    values=[\n      _descriptor.EnumValueDescriptor(\n        name='JS_NORMAL', index=0, number=0,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='JS_STRING', index=1, number=1,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='JS_NUMBER', index=2, number=2,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n    ],\n    containing_type=None,\n    serialized_options=None,\n  )\n  _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_JSTYPE)\n\n  _METHODOPTIONS_IDEMPOTENCYLEVEL = _descriptor.EnumDescriptor(\n    name='IdempotencyLevel',\n    full_name='google.protobuf.MethodOptions.IdempotencyLevel',\n    filename=None,\n    file=DESCRIPTOR,\n    create_key=_descriptor._internal_create_key,\n    values=[\n      _descriptor.EnumValueDescriptor(\n        name='IDEMPOTENCY_UNKNOWN', index=0, number=0,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='NO_SIDE_EFFECTS', index=1, number=1,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='IDEMPOTENT', index=2, number=2,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n    ],\n    containing_type=None,\n    serialized_options=None,\n  )\n  _sym_db.RegisterEnumDescriptor(_METHODOPTIONS_IDEMPOTENCYLEVEL)\n\n\n  _FILEDESCRIPTORSET = _descriptor.Descriptor(\n    name='FileDescriptorSet',\n    full_name='google.protobuf.FileDescriptorSet',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='file', full_name='google.protobuf.FileDescriptorSet.file', index=0,\n        number=1, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _FILEDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='FileDescriptorProto',\n    full_name='google.protobuf.FileDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.FileDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='package', full_name='google.protobuf.FileDescriptorProto.package', index=1,\n        number=2, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='dependency', full_name='google.protobuf.FileDescriptorProto.dependency', index=2,\n        number=3, type=9, cpp_type=9, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='public_dependency', full_name='google.protobuf.FileDescriptorProto.public_dependency', index=3,\n        number=10, type=5, cpp_type=1, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='weak_dependency', full_name='google.protobuf.FileDescriptorProto.weak_dependency', index=4,\n        number=11, type=5, cpp_type=1, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='message_type', full_name='google.protobuf.FileDescriptorProto.message_type', index=5,\n        number=4, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='enum_type', full_name='google.protobuf.FileDescriptorProto.enum_type', index=6,\n        number=5, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='service', full_name='google.protobuf.FileDescriptorProto.service', index=7,\n        number=6, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='extension', full_name='google.protobuf.FileDescriptorProto.extension', index=8,\n        number=7, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.FileDescriptorProto.options', index=9,\n        number=8, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='source_code_info', full_name='google.protobuf.FileDescriptorProto.source_code_info', index=10,\n        number=9, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='syntax', full_name='google.protobuf.FileDescriptorProto.syntax', index=11,\n        number=12, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _DESCRIPTORPROTO_EXTENSIONRANGE = _descriptor.Descriptor(\n    name='ExtensionRange',\n    full_name='google.protobuf.DescriptorProto.ExtensionRange',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='start', full_name='google.protobuf.DescriptorProto.ExtensionRange.start', index=0,\n        number=1, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='end', full_name='google.protobuf.DescriptorProto.ExtensionRange.end', index=1,\n        number=2, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.DescriptorProto.ExtensionRange.options', index=2,\n        number=3, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _DESCRIPTORPROTO_RESERVEDRANGE = _descriptor.Descriptor(\n    name='ReservedRange',\n    full_name='google.protobuf.DescriptorProto.ReservedRange',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='start', full_name='google.protobuf.DescriptorProto.ReservedRange.start', index=0,\n        number=1, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='end', full_name='google.protobuf.DescriptorProto.ReservedRange.end', index=1,\n        number=2, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _DESCRIPTORPROTO = _descriptor.Descriptor(\n    name='DescriptorProto',\n    full_name='google.protobuf.DescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.DescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='field', full_name='google.protobuf.DescriptorProto.field', index=1,\n        number=2, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='extension', full_name='google.protobuf.DescriptorProto.extension', index=2,\n        number=6, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='nested_type', full_name='google.protobuf.DescriptorProto.nested_type', index=3,\n        number=3, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='enum_type', full_name='google.protobuf.DescriptorProto.enum_type', index=4,\n        number=4, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='extension_range', full_name='google.protobuf.DescriptorProto.extension_range', index=5,\n        number=5, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='oneof_decl', full_name='google.protobuf.DescriptorProto.oneof_decl', index=6,\n        number=8, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.DescriptorProto.options', index=7,\n        number=7, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='reserved_range', full_name='google.protobuf.DescriptorProto.reserved_range', index=8,\n        number=9, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='reserved_name', full_name='google.protobuf.DescriptorProto.reserved_name', index=9,\n        number=10, type=9, cpp_type=9, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[_DESCRIPTORPROTO_EXTENSIONRANGE, _DESCRIPTORPROTO_RESERVEDRANGE, ],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _EXTENSIONRANGEOPTIONS = _descriptor.Descriptor(\n    name='ExtensionRangeOptions',\n    full_name='google.protobuf.ExtensionRangeOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.ExtensionRangeOptions.uninterpreted_option', index=0,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _FIELDDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='FieldDescriptorProto',\n    full_name='google.protobuf.FieldDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.FieldDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='number', full_name='google.protobuf.FieldDescriptorProto.number', index=1,\n        number=3, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='label', full_name='google.protobuf.FieldDescriptorProto.label', index=2,\n        number=4, type=14, cpp_type=8, label=1,\n        has_default_value=False, default_value=1,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='type', full_name='google.protobuf.FieldDescriptorProto.type', index=3,\n        number=5, type=14, cpp_type=8, label=1,\n        has_default_value=False, default_value=1,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='type_name', full_name='google.protobuf.FieldDescriptorProto.type_name', index=4,\n        number=6, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='extendee', full_name='google.protobuf.FieldDescriptorProto.extendee', index=5,\n        number=2, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='default_value', full_name='google.protobuf.FieldDescriptorProto.default_value', index=6,\n        number=7, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='oneof_index', full_name='google.protobuf.FieldDescriptorProto.oneof_index', index=7,\n        number=9, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='json_name', full_name='google.protobuf.FieldDescriptorProto.json_name', index=8,\n        number=10, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.FieldDescriptorProto.options', index=9,\n        number=8, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='proto3_optional', full_name='google.protobuf.FieldDescriptorProto.proto3_optional', index=10,\n        number=17, type=8, cpp_type=7, label=1,\n        has_default_value=False, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n      _FIELDDESCRIPTORPROTO_TYPE,\n      _FIELDDESCRIPTORPROTO_LABEL,\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _ONEOFDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='OneofDescriptorProto',\n    full_name='google.protobuf.OneofDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.OneofDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.OneofDescriptorProto.options', index=1,\n        number=2, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE = _descriptor.Descriptor(\n    name='EnumReservedRange',\n    full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='start', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.start', index=0,\n        number=1, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='end', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.end', index=1,\n        number=2, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _ENUMDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='EnumDescriptorProto',\n    full_name='google.protobuf.EnumDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.EnumDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='value', full_name='google.protobuf.EnumDescriptorProto.value', index=1,\n        number=2, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.EnumDescriptorProto.options', index=2,\n        number=3, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='reserved_range', full_name='google.protobuf.EnumDescriptorProto.reserved_range', index=3,\n        number=4, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='reserved_name', full_name='google.protobuf.EnumDescriptorProto.reserved_name', index=4,\n        number=5, type=9, cpp_type=9, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[_ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE, ],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _ENUMVALUEDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='EnumValueDescriptorProto',\n    full_name='google.protobuf.EnumValueDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.EnumValueDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='number', full_name='google.protobuf.EnumValueDescriptorProto.number', index=1,\n        number=2, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.EnumValueDescriptorProto.options', index=2,\n        number=3, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _SERVICEDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='ServiceDescriptorProto',\n    full_name='google.protobuf.ServiceDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.ServiceDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='method', full_name='google.protobuf.ServiceDescriptorProto.method', index=1,\n        number=2, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.ServiceDescriptorProto.options', index=2,\n        number=3, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _METHODDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='MethodDescriptorProto',\n    full_name='google.protobuf.MethodDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.MethodDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='input_type', full_name='google.protobuf.MethodDescriptorProto.input_type', index=1,\n        number=2, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='output_type', full_name='google.protobuf.MethodDescriptorProto.output_type', index=2,\n        number=3, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.MethodDescriptorProto.options', index=3,\n        number=4, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='client_streaming', full_name='google.protobuf.MethodDescriptorProto.client_streaming', index=4,\n        number=5, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='server_streaming', full_name='google.protobuf.MethodDescriptorProto.server_streaming', index=5,\n        number=6, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _FILEOPTIONS = _descriptor.Descriptor(\n    name='FileOptions',\n    full_name='google.protobuf.FileOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='java_package', full_name='google.protobuf.FileOptions.java_package', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='java_outer_classname', full_name='google.protobuf.FileOptions.java_outer_classname', index=1,\n        number=8, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='java_multiple_files', full_name='google.protobuf.FileOptions.java_multiple_files', index=2,\n        number=10, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='java_generate_equals_and_hash', full_name='google.protobuf.FileOptions.java_generate_equals_and_hash', index=3,\n        number=20, type=8, cpp_type=7, label=1,\n        has_default_value=False, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='java_string_check_utf8', full_name='google.protobuf.FileOptions.java_string_check_utf8', index=4,\n        number=27, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='optimize_for', full_name='google.protobuf.FileOptions.optimize_for', index=5,\n        number=9, type=14, cpp_type=8, label=1,\n        has_default_value=True, default_value=1,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='go_package', full_name='google.protobuf.FileOptions.go_package', index=6,\n        number=11, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='cc_generic_services', full_name='google.protobuf.FileOptions.cc_generic_services', index=7,\n        number=16, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='java_generic_services', full_name='google.protobuf.FileOptions.java_generic_services', index=8,\n        number=17, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='py_generic_services', full_name='google.protobuf.FileOptions.py_generic_services', index=9,\n        number=18, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='php_generic_services', full_name='google.protobuf.FileOptions.php_generic_services', index=10,\n        number=42, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.FileOptions.deprecated', index=11,\n        number=23, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='cc_enable_arenas', full_name='google.protobuf.FileOptions.cc_enable_arenas', index=12,\n        number=31, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=True,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='objc_class_prefix', full_name='google.protobuf.FileOptions.objc_class_prefix', index=13,\n        number=36, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='csharp_namespace', full_name='google.protobuf.FileOptions.csharp_namespace', index=14,\n        number=37, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='swift_prefix', full_name='google.protobuf.FileOptions.swift_prefix', index=15,\n        number=39, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='php_class_prefix', full_name='google.protobuf.FileOptions.php_class_prefix', index=16,\n        number=40, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='php_namespace', full_name='google.protobuf.FileOptions.php_namespace', index=17,\n        number=41, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='php_metadata_namespace', full_name='google.protobuf.FileOptions.php_metadata_namespace', index=18,\n        number=44, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='ruby_package', full_name='google.protobuf.FileOptions.ruby_package', index=19,\n        number=45, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.FileOptions.uninterpreted_option', index=20,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n      _FILEOPTIONS_OPTIMIZEMODE,\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _MESSAGEOPTIONS = _descriptor.Descriptor(\n    name='MessageOptions',\n    full_name='google.protobuf.MessageOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='message_set_wire_format', full_name='google.protobuf.MessageOptions.message_set_wire_format', index=0,\n        number=1, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='no_standard_descriptor_accessor', full_name='google.protobuf.MessageOptions.no_standard_descriptor_accessor', index=1,\n        number=2, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.MessageOptions.deprecated', index=2,\n        number=3, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='map_entry', full_name='google.protobuf.MessageOptions.map_entry', index=3,\n        number=7, type=8, cpp_type=7, label=1,\n        has_default_value=False, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.MessageOptions.uninterpreted_option', index=4,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _FIELDOPTIONS = _descriptor.Descriptor(\n    name='FieldOptions',\n    full_name='google.protobuf.FieldOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='ctype', full_name='google.protobuf.FieldOptions.ctype', index=0,\n        number=1, type=14, cpp_type=8, label=1,\n        has_default_value=True, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='packed', full_name='google.protobuf.FieldOptions.packed', index=1,\n        number=2, type=8, cpp_type=7, label=1,\n        has_default_value=False, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='jstype', full_name='google.protobuf.FieldOptions.jstype', index=2,\n        number=6, type=14, cpp_type=8, label=1,\n        has_default_value=True, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='lazy', full_name='google.protobuf.FieldOptions.lazy', index=3,\n        number=5, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='unverified_lazy', full_name='google.protobuf.FieldOptions.unverified_lazy', index=4,\n        number=15, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.FieldOptions.deprecated', index=5,\n        number=3, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='weak', full_name='google.protobuf.FieldOptions.weak', index=6,\n        number=10, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.FieldOptions.uninterpreted_option', index=7,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n      _FIELDOPTIONS_CTYPE,\n      _FIELDOPTIONS_JSTYPE,\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _ONEOFOPTIONS = _descriptor.Descriptor(\n    name='OneofOptions',\n    full_name='google.protobuf.OneofOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.OneofOptions.uninterpreted_option', index=0,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _ENUMOPTIONS = _descriptor.Descriptor(\n    name='EnumOptions',\n    full_name='google.protobuf.EnumOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='allow_alias', full_name='google.protobuf.EnumOptions.allow_alias', index=0,\n        number=2, type=8, cpp_type=7, label=1,\n        has_default_value=False, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.EnumOptions.deprecated', index=1,\n        number=3, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.EnumOptions.uninterpreted_option', index=2,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _ENUMVALUEOPTIONS = _descriptor.Descriptor(\n    name='EnumValueOptions',\n    full_name='google.protobuf.EnumValueOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.EnumValueOptions.deprecated', index=0,\n        number=1, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.EnumValueOptions.uninterpreted_option', index=1,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _SERVICEOPTIONS = _descriptor.Descriptor(\n    name='ServiceOptions',\n    full_name='google.protobuf.ServiceOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.ServiceOptions.deprecated', index=0,\n        number=33, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.ServiceOptions.uninterpreted_option', index=1,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _METHODOPTIONS = _descriptor.Descriptor(\n    name='MethodOptions',\n    full_name='google.protobuf.MethodOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.MethodOptions.deprecated', index=0,\n        number=33, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='idempotency_level', full_name='google.protobuf.MethodOptions.idempotency_level', index=1,\n        number=34, type=14, cpp_type=8, label=1,\n        has_default_value=True, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.MethodOptions.uninterpreted_option', index=2,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n      _METHODOPTIONS_IDEMPOTENCYLEVEL,\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _UNINTERPRETEDOPTION_NAMEPART = _descriptor.Descriptor(\n    name='NamePart',\n    full_name='google.protobuf.UninterpretedOption.NamePart',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name_part', full_name='google.protobuf.UninterpretedOption.NamePart.name_part', index=0,\n        number=1, type=9, cpp_type=9, label=2,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='is_extension', full_name='google.protobuf.UninterpretedOption.NamePart.is_extension', index=1,\n        number=2, type=8, cpp_type=7, label=2,\n        has_default_value=False, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _UNINTERPRETEDOPTION = _descriptor.Descriptor(\n    name='UninterpretedOption',\n    full_name='google.protobuf.UninterpretedOption',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.UninterpretedOption.name', index=0,\n        number=2, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='identifier_value', full_name='google.protobuf.UninterpretedOption.identifier_value', index=1,\n        number=3, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='positive_int_value', full_name='google.protobuf.UninterpretedOption.positive_int_value', index=2,\n        number=4, type=4, cpp_type=4, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='negative_int_value', full_name='google.protobuf.UninterpretedOption.negative_int_value', index=3,\n        number=5, type=3, cpp_type=2, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='double_value', full_name='google.protobuf.UninterpretedOption.double_value', index=4,\n        number=6, type=1, cpp_type=5, label=1,\n        has_default_value=False, default_value=float(0),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='string_value', full_name='google.protobuf.UninterpretedOption.string_value', index=5,\n        number=7, type=12, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\",\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='aggregate_value', full_name='google.protobuf.UninterpretedOption.aggregate_value', index=6,\n        number=8, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[_UNINTERPRETEDOPTION_NAMEPART, ],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _SOURCECODEINFO_LOCATION = _descriptor.Descriptor(\n    name='Location',\n    full_name='google.protobuf.SourceCodeInfo.Location',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='path', full_name='google.protobuf.SourceCodeInfo.Location.path', index=0,\n        number=1, type=5, cpp_type=1, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='span', full_name='google.protobuf.SourceCodeInfo.Location.span', index=1,\n        number=2, type=5, cpp_type=1, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='leading_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_comments', index=2,\n        number=3, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='trailing_comments', full_name='google.protobuf.SourceCodeInfo.Location.trailing_comments', index=3,\n        number=4, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='leading_detached_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_detached_comments', index=4,\n        number=6, type=9, cpp_type=9, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _SOURCECODEINFO = _descriptor.Descriptor(\n    name='SourceCodeInfo',\n    full_name='google.protobuf.SourceCodeInfo',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='location', full_name='google.protobuf.SourceCodeInfo.location', index=0,\n        number=1, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[_SOURCECODEINFO_LOCATION, ],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _GENERATEDCODEINFO_ANNOTATION = _descriptor.Descriptor(\n    name='Annotation',\n    full_name='google.protobuf.GeneratedCodeInfo.Annotation',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='path', full_name='google.protobuf.GeneratedCodeInfo.Annotation.path', index=0,\n        number=1, type=5, cpp_type=1, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='source_file', full_name='google.protobuf.GeneratedCodeInfo.Annotation.source_file', index=1,\n        number=2, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='begin', full_name='google.protobuf.GeneratedCodeInfo.Annotation.begin', index=2,\n        number=3, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='end', full_name='google.protobuf.GeneratedCodeInfo.Annotation.end', index=3,\n        number=4, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _GENERATEDCODEINFO = _descriptor.Descriptor(\n    name='GeneratedCodeInfo',\n    full_name='google.protobuf.GeneratedCodeInfo',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='annotation', full_name='google.protobuf.GeneratedCodeInfo.annotation', index=0,\n        number=1, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[_GENERATEDCODEINFO_ANNOTATION, ],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _FILEDESCRIPTORSET.fields_by_name['file'].message_type = _FILEDESCRIPTORPROTO\n  _FILEDESCRIPTORPROTO.fields_by_name['message_type'].message_type = _DESCRIPTORPROTO\n  _FILEDESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO\n  _FILEDESCRIPTORPROTO.fields_by_name['service'].message_type = _SERVICEDESCRIPTORPROTO\n  _FILEDESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO\n  _FILEDESCRIPTORPROTO.fields_by_name['options'].message_type = _FILEOPTIONS\n  _FILEDESCRIPTORPROTO.fields_by_name['source_code_info'].message_type = _SOURCECODEINFO\n  _DESCRIPTORPROTO_EXTENSIONRANGE.fields_by_name['options'].message_type = _EXTENSIONRANGEOPTIONS\n  _DESCRIPTORPROTO_EXTENSIONRANGE.containing_type = _DESCRIPTORPROTO\n  _DESCRIPTORPROTO_RESERVEDRANGE.containing_type = _DESCRIPTORPROTO\n  _DESCRIPTORPROTO.fields_by_name['field'].message_type = _FIELDDESCRIPTORPROTO\n  _DESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO\n  _DESCRIPTORPROTO.fields_by_name['nested_type'].message_type = _DESCRIPTORPROTO\n  _DESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO\n  _DESCRIPTORPROTO.fields_by_name['extension_range'].message_type = _DESCRIPTORPROTO_EXTENSIONRANGE\n  _DESCRIPTORPROTO.fields_by_name['oneof_decl'].message_type = _ONEOFDESCRIPTORPROTO\n  _DESCRIPTORPROTO.fields_by_name['options'].message_type = _MESSAGEOPTIONS\n  _DESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _DESCRIPTORPROTO_RESERVEDRANGE\n  _EXTENSIONRANGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _FIELDDESCRIPTORPROTO.fields_by_name['label'].enum_type = _FIELDDESCRIPTORPROTO_LABEL\n  _FIELDDESCRIPTORPROTO.fields_by_name['type'].enum_type = _FIELDDESCRIPTORPROTO_TYPE\n  _FIELDDESCRIPTORPROTO.fields_by_name['options'].message_type = _FIELDOPTIONS\n  _FIELDDESCRIPTORPROTO_TYPE.containing_type = _FIELDDESCRIPTORPROTO\n  _FIELDDESCRIPTORPROTO_LABEL.containing_type = _FIELDDESCRIPTORPROTO\n  _ONEOFDESCRIPTORPROTO.fields_by_name['options'].message_type = _ONEOFOPTIONS\n  _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE.containing_type = _ENUMDESCRIPTORPROTO\n  _ENUMDESCRIPTORPROTO.fields_by_name['value'].message_type = _ENUMVALUEDESCRIPTORPROTO\n  _ENUMDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMOPTIONS\n  _ENUMDESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE\n  _ENUMVALUEDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMVALUEOPTIONS\n  _SERVICEDESCRIPTORPROTO.fields_by_name['method'].message_type = _METHODDESCRIPTORPROTO\n  _SERVICEDESCRIPTORPROTO.fields_by_name['options'].message_type = _SERVICEOPTIONS\n  _METHODDESCRIPTORPROTO.fields_by_name['options'].message_type = _METHODOPTIONS\n  _FILEOPTIONS.fields_by_name['optimize_for'].enum_type = _FILEOPTIONS_OPTIMIZEMODE\n  _FILEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _FILEOPTIONS_OPTIMIZEMODE.containing_type = _FILEOPTIONS\n  _MESSAGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _FIELDOPTIONS.fields_by_name['ctype'].enum_type = _FIELDOPTIONS_CTYPE\n  _FIELDOPTIONS.fields_by_name['jstype'].enum_type = _FIELDOPTIONS_JSTYPE\n  _FIELDOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _FIELDOPTIONS_CTYPE.containing_type = _FIELDOPTIONS\n  _FIELDOPTIONS_JSTYPE.containing_type = _FIELDOPTIONS\n  _ONEOFOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _ENUMOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _ENUMVALUEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _SERVICEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _METHODOPTIONS.fields_by_name['idempotency_level'].enum_type = _METHODOPTIONS_IDEMPOTENCYLEVEL\n  _METHODOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _METHODOPTIONS_IDEMPOTENCYLEVEL.containing_type = _METHODOPTIONS\n  _UNINTERPRETEDOPTION_NAMEPART.containing_type = _UNINTERPRETEDOPTION\n  _UNINTERPRETEDOPTION.fields_by_name['name'].message_type = _UNINTERPRETEDOPTION_NAMEPART\n  _SOURCECODEINFO_LOCATION.containing_type = _SOURCECODEINFO\n  _SOURCECODEINFO.fields_by_name['location'].message_type = _SOURCECODEINFO_LOCATION\n  _GENERATEDCODEINFO_ANNOTATION.containing_type = _GENERATEDCODEINFO\n  _GENERATEDCODEINFO.fields_by_name['annotation'].message_type = _GENERATEDCODEINFO_ANNOTATION\n  DESCRIPTOR.message_types_by_name['FileDescriptorSet'] = _FILEDESCRIPTORSET\n  DESCRIPTOR.message_types_by_name['FileDescriptorProto'] = _FILEDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['DescriptorProto'] = _DESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['ExtensionRangeOptions'] = _EXTENSIONRANGEOPTIONS\n  DESCRIPTOR.message_types_by_name['FieldDescriptorProto'] = _FIELDDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['OneofDescriptorProto'] = _ONEOFDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['EnumDescriptorProto'] = _ENUMDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['EnumValueDescriptorProto'] = _ENUMVALUEDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['ServiceDescriptorProto'] = _SERVICEDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['MethodDescriptorProto'] = _METHODDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['FileOptions'] = _FILEOPTIONS\n  DESCRIPTOR.message_types_by_name['MessageOptions'] = _MESSAGEOPTIONS\n  DESCRIPTOR.message_types_by_name['FieldOptions'] = _FIELDOPTIONS\n  DESCRIPTOR.message_types_by_name['OneofOptions'] = _ONEOFOPTIONS\n  DESCRIPTOR.message_types_by_name['EnumOptions'] = _ENUMOPTIONS\n  DESCRIPTOR.message_types_by_name['EnumValueOptions'] = _ENUMVALUEOPTIONS\n  DESCRIPTOR.message_types_by_name['ServiceOptions'] = _SERVICEOPTIONS\n  DESCRIPTOR.message_types_by_name['MethodOptions'] = _METHODOPTIONS\n  DESCRIPTOR.message_types_by_name['UninterpretedOption'] = _UNINTERPRETEDOPTION\n  DESCRIPTOR.message_types_by_name['SourceCodeInfo'] = _SOURCECODEINFO\n  DESCRIPTOR.message_types_by_name['GeneratedCodeInfo'] = _GENERATEDCODEINFO\n  _sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\nelse:\n  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.descriptor_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  _FILEDESCRIPTORSET._serialized_start=53\n  _FILEDESCRIPTORSET._serialized_end=124\n  _FILEDESCRIPTORPROTO._serialized_start=127\n  _FILEDESCRIPTORPROTO._serialized_end=602\n  _DESCRIPTORPROTO._serialized_start=605\n  _DESCRIPTORPROTO._serialized_end=1286\n  _DESCRIPTORPROTO_EXTENSIONRANGE._serialized_start=1140\n  _DESCRIPTORPROTO_EXTENSIONRANGE._serialized_end=1241\n  _DESCRIPTORPROTO_RESERVEDRANGE._serialized_start=1243\n  _DESCRIPTORPROTO_RESERVEDRANGE._serialized_end=1286\n  _EXTENSIONRANGEOPTIONS._serialized_start=1288\n  _EXTENSIONRANGEOPTIONS._serialized_end=1391\n  _FIELDDESCRIPTORPROTO._serialized_start=1394\n  _FIELDDESCRIPTORPROTO._serialized_end=2119\n  _FIELDDESCRIPTORPROTO_TYPE._serialized_start=1740\n  _FIELDDESCRIPTORPROTO_TYPE._serialized_end=2050\n  _FIELDDESCRIPTORPROTO_LABEL._serialized_start=2052\n  _FIELDDESCRIPTORPROTO_LABEL._serialized_end=2119\n  _ONEOFDESCRIPTORPROTO._serialized_start=2121\n  _ONEOFDESCRIPTORPROTO._serialized_end=2205\n  _ENUMDESCRIPTORPROTO._serialized_start=2208\n  _ENUMDESCRIPTORPROTO._serialized_end=2500\n  _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._serialized_start=2453\n  _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._serialized_end=2500\n  _ENUMVALUEDESCRIPTORPROTO._serialized_start=2502\n  _ENUMVALUEDESCRIPTORPROTO._serialized_end=2610\n  _SERVICEDESCRIPTORPROTO._serialized_start=2613\n  _SERVICEDESCRIPTORPROTO._serialized_end=2757\n  _METHODDESCRIPTORPROTO._serialized_start=2760\n  _METHODDESCRIPTORPROTO._serialized_end=2953\n  _FILEOPTIONS._serialized_start=2956\n  _FILEOPTIONS._serialized_end=3761\n  _FILEOPTIONS_OPTIMIZEMODE._serialized_start=3686\n  _FILEOPTIONS_OPTIMIZEMODE._serialized_end=3744\n  _MESSAGEOPTIONS._serialized_start=3764\n  _MESSAGEOPTIONS._serialized_end=4024\n  _FIELDOPTIONS._serialized_start=4027\n  _FIELDOPTIONS._serialized_end=4473\n  _FIELDOPTIONS_CTYPE._serialized_start=4354\n  _FIELDOPTIONS_CTYPE._serialized_end=4401\n  _FIELDOPTIONS_JSTYPE._serialized_start=4403\n  _FIELDOPTIONS_JSTYPE._serialized_end=4456\n  _ONEOFOPTIONS._serialized_start=4475\n  _ONEOFOPTIONS._serialized_end=4569\n  _ENUMOPTIONS._serialized_start=4572\n  _ENUMOPTIONS._serialized_end=4719\n  _ENUMVALUEOPTIONS._serialized_start=4721\n  _ENUMVALUEOPTIONS._serialized_end=4846\n  _SERVICEOPTIONS._serialized_start=4848\n  _SERVICEOPTIONS._serialized_end=4971\n  _METHODOPTIONS._serialized_start=4974\n  _METHODOPTIONS._serialized_end=5275\n  _METHODOPTIONS_IDEMPOTENCYLEVEL._serialized_start=5184\n  _METHODOPTIONS_IDEMPOTENCYLEVEL._serialized_end=5264\n  _UNINTERPRETEDOPTION._serialized_start=5278\n  _UNINTERPRETEDOPTION._serialized_end=5564\n  _UNINTERPRETEDOPTION_NAMEPART._serialized_start=5513\n  _UNINTERPRETEDOPTION_NAMEPART._serialized_end=5564\n  _SOURCECODEINFO._serialized_start=5567\n  _SOURCECODEINFO._serialized_end=5780\n  _SOURCECODEINFO_LOCATION._serialized_start=5646\n  _SOURCECODEINFO_LOCATION._serialized_end=5780\n  _GENERATEDCODEINFO._serialized_start=5783\n  _GENERATEDCODEINFO._serialized_end=5950\n  _GENERATEDCODEINFO_ANNOTATION._serialized_start=5871\n  _GENERATEDCODEINFO_ANNOTATION._serialized_end=5950\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/descriptor_pool.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Provides DescriptorPool to use as a container for proto2 descriptors.\n\nThe DescriptorPool is used in conjection with a DescriptorDatabase to maintain\na collection of protocol buffer descriptors for use when dynamically creating\nmessage types at runtime.\n\nFor most applications protocol buffers should be used via modules generated by\nthe protocol buffer compiler tool. This should only be used when the type of\nprotocol buffers used in an application or library cannot be predetermined.\n\nBelow is a straightforward example on how to use this class::\n\n  pool = DescriptorPool()\n  file_descriptor_protos = [ ... ]\n  for file_descriptor_proto in file_descriptor_protos:\n    pool.Add(file_descriptor_proto)\n  my_message_descriptor = pool.FindMessageTypeByName('some.package.MessageType')\n\nThe message descriptor can be used in conjunction with the message_factory\nmodule in order to create a protocol buffer class that can be encoded and\ndecoded.\n\nIf you want to get a Python class for the specified proto, use the\nhelper functions inside google.protobuf.message_factory\ndirectly instead of this class.\n\"\"\"\n\n__author__ = 'matthewtoia@google.com (Matt Toia)'\n\nimport collections\nimport warnings\n\nfrom google.protobuf import descriptor\nfrom google.protobuf import descriptor_database\nfrom google.protobuf import text_encoding\n\n\n_USE_C_DESCRIPTORS = descriptor._USE_C_DESCRIPTORS  # pylint: disable=protected-access\n\n\ndef _Deprecated(func):\n  \"\"\"Mark functions as deprecated.\"\"\"\n\n  def NewFunc(*args, **kwargs):\n    warnings.warn(\n        'Call to deprecated function %s(). Note: Do add unlinked descriptors '\n        'to descriptor_pool is wrong. Use Add() or AddSerializedFile() '\n        'instead.' % func.__name__,\n        category=DeprecationWarning)\n    return func(*args, **kwargs)\n  NewFunc.__name__ = func.__name__\n  NewFunc.__doc__ = func.__doc__\n  NewFunc.__dict__.update(func.__dict__)\n  return NewFunc\n\n\ndef _NormalizeFullyQualifiedName(name):\n  \"\"\"Remove leading period from fully-qualified type name.\n\n  Due to b/13860351 in descriptor_database.py, types in the root namespace are\n  generated with a leading period. This function removes that prefix.\n\n  Args:\n    name (str): The fully-qualified symbol name.\n\n  Returns:\n    str: The normalized fully-qualified symbol name.\n  \"\"\"\n  return name.lstrip('.')\n\n\ndef _OptionsOrNone(descriptor_proto):\n  \"\"\"Returns the value of the field `options`, or None if it is not set.\"\"\"\n  if descriptor_proto.HasField('options'):\n    return descriptor_proto.options\n  else:\n    return None\n\n\ndef _IsMessageSetExtension(field):\n  return (field.is_extension and\n          field.containing_type.has_options and\n          field.containing_type.GetOptions().message_set_wire_format and\n          field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and\n          field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL)\n\n\nclass DescriptorPool(object):\n  \"\"\"A collection of protobufs dynamically constructed by descriptor protos.\"\"\"\n\n  if _USE_C_DESCRIPTORS:\n\n    def __new__(cls, descriptor_db=None):\n      # pylint: disable=protected-access\n      return descriptor._message.DescriptorPool(descriptor_db)\n\n  def __init__(self, descriptor_db=None):\n    \"\"\"Initializes a Pool of proto buffs.\n\n    The descriptor_db argument to the constructor is provided to allow\n    specialized file descriptor proto lookup code to be triggered on demand. An\n    example would be an implementation which will read and compile a file\n    specified in a call to FindFileByName() and not require the call to Add()\n    at all. Results from this database will be cached internally here as well.\n\n    Args:\n      descriptor_db: A secondary source of file descriptors.\n    \"\"\"\n\n    self._internal_db = descriptor_database.DescriptorDatabase()\n    self._descriptor_db = descriptor_db\n    self._descriptors = {}\n    self._enum_descriptors = {}\n    self._service_descriptors = {}\n    self._file_descriptors = {}\n    self._toplevel_extensions = {}\n    # TODO(jieluo): Remove _file_desc_by_toplevel_extension after\n    # maybe year 2020 for compatibility issue (with 3.4.1 only).\n    self._file_desc_by_toplevel_extension = {}\n    self._top_enum_values = {}\n    # We store extensions in two two-level mappings: The first key is the\n    # descriptor of the message being extended, the second key is the extension\n    # full name or its tag number.\n    self._extensions_by_name = collections.defaultdict(dict)\n    self._extensions_by_number = collections.defaultdict(dict)\n\n  def _CheckConflictRegister(self, desc, desc_name, file_name):\n    \"\"\"Check if the descriptor name conflicts with another of the same name.\n\n    Args:\n      desc: Descriptor of a message, enum, service, extension or enum value.\n      desc_name (str): the full name of desc.\n      file_name (str): The file name of descriptor.\n    \"\"\"\n    for register, descriptor_type in [\n        (self._descriptors, descriptor.Descriptor),\n        (self._enum_descriptors, descriptor.EnumDescriptor),\n        (self._service_descriptors, descriptor.ServiceDescriptor),\n        (self._toplevel_extensions, descriptor.FieldDescriptor),\n        (self._top_enum_values, descriptor.EnumValueDescriptor)]:\n      if desc_name in register:\n        old_desc = register[desc_name]\n        if isinstance(old_desc, descriptor.EnumValueDescriptor):\n          old_file = old_desc.type.file.name\n        else:\n          old_file = old_desc.file.name\n\n        if not isinstance(desc, descriptor_type) or (\n            old_file != file_name):\n          error_msg = ('Conflict register for file \"' + file_name +\n                       '\": ' + desc_name +\n                       ' is already defined in file \"' +\n                       old_file + '\". Please fix the conflict by adding '\n                       'package name on the proto file, or use different '\n                       'name for the duplication.')\n          if isinstance(desc, descriptor.EnumValueDescriptor):\n            error_msg += ('\\nNote: enum values appear as '\n                          'siblings of the enum type instead of '\n                          'children of it.')\n\n          raise TypeError(error_msg)\n\n        return\n\n  def Add(self, file_desc_proto):\n    \"\"\"Adds the FileDescriptorProto and its types to this pool.\n\n    Args:\n      file_desc_proto (FileDescriptorProto): The file descriptor to add.\n    \"\"\"\n\n    self._internal_db.Add(file_desc_proto)\n\n  def AddSerializedFile(self, serialized_file_desc_proto):\n    \"\"\"Adds the FileDescriptorProto and its types to this pool.\n\n    Args:\n      serialized_file_desc_proto (bytes): A bytes string, serialization of the\n        :class:`FileDescriptorProto` to add.\n\n    Returns:\n      FileDescriptor: Descriptor for the added file.\n    \"\"\"\n\n    # pylint: disable=g-import-not-at-top\n    from google.protobuf import descriptor_pb2\n    file_desc_proto = descriptor_pb2.FileDescriptorProto.FromString(\n        serialized_file_desc_proto)\n    file_desc = self._ConvertFileProtoToFileDescriptor(file_desc_proto)\n    file_desc.serialized_pb = serialized_file_desc_proto\n    return file_desc\n\n  # Add Descriptor to descriptor pool is dreprecated. Please use Add()\n  # or AddSerializedFile() to add a FileDescriptorProto instead.\n  @_Deprecated\n  def AddDescriptor(self, desc):\n    self._AddDescriptor(desc)\n\n  # Never call this method. It is for internal usage only.\n  def _AddDescriptor(self, desc):\n    \"\"\"Adds a Descriptor to the pool, non-recursively.\n\n    If the Descriptor contains nested messages or enums, the caller must\n    explicitly register them. This method also registers the FileDescriptor\n    associated with the message.\n\n    Args:\n      desc: A Descriptor.\n    \"\"\"\n    if not isinstance(desc, descriptor.Descriptor):\n      raise TypeError('Expected instance of descriptor.Descriptor.')\n\n    self._CheckConflictRegister(desc, desc.full_name, desc.file.name)\n\n    self._descriptors[desc.full_name] = desc\n    self._AddFileDescriptor(desc.file)\n\n  # Add EnumDescriptor to descriptor pool is dreprecated. Please use Add()\n  # or AddSerializedFile() to add a FileDescriptorProto instead.\n  @_Deprecated\n  def AddEnumDescriptor(self, enum_desc):\n    self._AddEnumDescriptor(enum_desc)\n\n  # Never call this method. It is for internal usage only.\n  def _AddEnumDescriptor(self, enum_desc):\n    \"\"\"Adds an EnumDescriptor to the pool.\n\n    This method also registers the FileDescriptor associated with the enum.\n\n    Args:\n      enum_desc: An EnumDescriptor.\n    \"\"\"\n\n    if not isinstance(enum_desc, descriptor.EnumDescriptor):\n      raise TypeError('Expected instance of descriptor.EnumDescriptor.')\n\n    file_name = enum_desc.file.name\n    self._CheckConflictRegister(enum_desc, enum_desc.full_name, file_name)\n    self._enum_descriptors[enum_desc.full_name] = enum_desc\n\n    # Top enum values need to be indexed.\n    # Count the number of dots to see whether the enum is toplevel or nested\n    # in a message. We cannot use enum_desc.containing_type at this stage.\n    if enum_desc.file.package:\n      top_level = (enum_desc.full_name.count('.')\n                   - enum_desc.file.package.count('.') == 1)\n    else:\n      top_level = enum_desc.full_name.count('.') == 0\n    if top_level:\n      file_name = enum_desc.file.name\n      package = enum_desc.file.package\n      for enum_value in enum_desc.values:\n        full_name = _NormalizeFullyQualifiedName(\n            '.'.join((package, enum_value.name)))\n        self._CheckConflictRegister(enum_value, full_name, file_name)\n        self._top_enum_values[full_name] = enum_value\n    self._AddFileDescriptor(enum_desc.file)\n\n  # Add ServiceDescriptor to descriptor pool is dreprecated. Please use Add()\n  # or AddSerializedFile() to add a FileDescriptorProto instead.\n  @_Deprecated\n  def AddServiceDescriptor(self, service_desc):\n    self._AddServiceDescriptor(service_desc)\n\n  # Never call this method. It is for internal usage only.\n  def _AddServiceDescriptor(self, service_desc):\n    \"\"\"Adds a ServiceDescriptor to the pool.\n\n    Args:\n      service_desc: A ServiceDescriptor.\n    \"\"\"\n\n    if not isinstance(service_desc, descriptor.ServiceDescriptor):\n      raise TypeError('Expected instance of descriptor.ServiceDescriptor.')\n\n    self._CheckConflictRegister(service_desc, service_desc.full_name,\n                                service_desc.file.name)\n    self._service_descriptors[service_desc.full_name] = service_desc\n\n  # Add ExtensionDescriptor to descriptor pool is dreprecated. Please use Add()\n  # or AddSerializedFile() to add a FileDescriptorProto instead.\n  @_Deprecated\n  def AddExtensionDescriptor(self, extension):\n    self._AddExtensionDescriptor(extension)\n\n  # Never call this method. It is for internal usage only.\n  def _AddExtensionDescriptor(self, extension):\n    \"\"\"Adds a FieldDescriptor describing an extension to the pool.\n\n    Args:\n      extension: A FieldDescriptor.\n\n    Raises:\n      AssertionError: when another extension with the same number extends the\n        same message.\n      TypeError: when the specified extension is not a\n        descriptor.FieldDescriptor.\n    \"\"\"\n    if not (isinstance(extension, descriptor.FieldDescriptor) and\n            extension.is_extension):\n      raise TypeError('Expected an extension descriptor.')\n\n    if extension.extension_scope is None:\n      self._toplevel_extensions[extension.full_name] = extension\n\n    try:\n      existing_desc = self._extensions_by_number[\n          extension.containing_type][extension.number]\n    except KeyError:\n      pass\n    else:\n      if extension is not existing_desc:\n        raise AssertionError(\n            'Extensions \"%s\" and \"%s\" both try to extend message type \"%s\" '\n            'with field number %d.' %\n            (extension.full_name, existing_desc.full_name,\n             extension.containing_type.full_name, extension.number))\n\n    self._extensions_by_number[extension.containing_type][\n        extension.number] = extension\n    self._extensions_by_name[extension.containing_type][\n        extension.full_name] = extension\n\n    # Also register MessageSet extensions with the type name.\n    if _IsMessageSetExtension(extension):\n      self._extensions_by_name[extension.containing_type][\n          extension.message_type.full_name] = extension\n\n  @_Deprecated\n  def AddFileDescriptor(self, file_desc):\n    self._InternalAddFileDescriptor(file_desc)\n\n  # Never call this method. It is for internal usage only.\n  def _InternalAddFileDescriptor(self, file_desc):\n    \"\"\"Adds a FileDescriptor to the pool, non-recursively.\n\n    If the FileDescriptor contains messages or enums, the caller must explicitly\n    register them.\n\n    Args:\n      file_desc: A FileDescriptor.\n    \"\"\"\n\n    self._AddFileDescriptor(file_desc)\n    # TODO(jieluo): This is a temporary solution for FieldDescriptor.file.\n    # FieldDescriptor.file is added in code gen. Remove this solution after\n    # maybe 2020 for compatibility reason (with 3.4.1 only).\n    for extension in file_desc.extensions_by_name.values():\n      self._file_desc_by_toplevel_extension[\n          extension.full_name] = file_desc\n\n  def _AddFileDescriptor(self, file_desc):\n    \"\"\"Adds a FileDescriptor to the pool, non-recursively.\n\n    If the FileDescriptor contains messages or enums, the caller must explicitly\n    register them.\n\n    Args:\n      file_desc: A FileDescriptor.\n    \"\"\"\n\n    if not isinstance(file_desc, descriptor.FileDescriptor):\n      raise TypeError('Expected instance of descriptor.FileDescriptor.')\n    self._file_descriptors[file_desc.name] = file_desc\n\n  def FindFileByName(self, file_name):\n    \"\"\"Gets a FileDescriptor by file name.\n\n    Args:\n      file_name (str): The path to the file to get a descriptor for.\n\n    Returns:\n      FileDescriptor: The descriptor for the named file.\n\n    Raises:\n      KeyError: if the file cannot be found in the pool.\n    \"\"\"\n\n    try:\n      return self._file_descriptors[file_name]\n    except KeyError:\n      pass\n\n    try:\n      file_proto = self._internal_db.FindFileByName(file_name)\n    except KeyError as error:\n      if self._descriptor_db:\n        file_proto = self._descriptor_db.FindFileByName(file_name)\n      else:\n        raise error\n    if not file_proto:\n      raise KeyError('Cannot find a file named %s' % file_name)\n    return self._ConvertFileProtoToFileDescriptor(file_proto)\n\n  def FindFileContainingSymbol(self, symbol):\n    \"\"\"Gets the FileDescriptor for the file containing the specified symbol.\n\n    Args:\n      symbol (str): The name of the symbol to search for.\n\n    Returns:\n      FileDescriptor: Descriptor for the file that contains the specified\n      symbol.\n\n    Raises:\n      KeyError: if the file cannot be found in the pool.\n    \"\"\"\n\n    symbol = _NormalizeFullyQualifiedName(symbol)\n    try:\n      return self._InternalFindFileContainingSymbol(symbol)\n    except KeyError:\n      pass\n\n    try:\n      # Try fallback database. Build and find again if possible.\n      self._FindFileContainingSymbolInDb(symbol)\n      return self._InternalFindFileContainingSymbol(symbol)\n    except KeyError:\n      raise KeyError('Cannot find a file containing %s' % symbol)\n\n  def _InternalFindFileContainingSymbol(self, symbol):\n    \"\"\"Gets the already built FileDescriptor containing the specified symbol.\n\n    Args:\n      symbol (str): The name of the symbol to search for.\n\n    Returns:\n      FileDescriptor: Descriptor for the file that contains the specified\n      symbol.\n\n    Raises:\n      KeyError: if the file cannot be found in the pool.\n    \"\"\"\n    try:\n      return self._descriptors[symbol].file\n    except KeyError:\n      pass\n\n    try:\n      return self._enum_descriptors[symbol].file\n    except KeyError:\n      pass\n\n    try:\n      return self._service_descriptors[symbol].file\n    except KeyError:\n      pass\n\n    try:\n      return self._top_enum_values[symbol].type.file\n    except KeyError:\n      pass\n\n    try:\n      return self._file_desc_by_toplevel_extension[symbol]\n    except KeyError:\n      pass\n\n    # Try fields, enum values and nested extensions inside a message.\n    top_name, _, sub_name = symbol.rpartition('.')\n    try:\n      message = self.FindMessageTypeByName(top_name)\n      assert (sub_name in message.extensions_by_name or\n              sub_name in message.fields_by_name or\n              sub_name in message.enum_values_by_name)\n      return message.file\n    except (KeyError, AssertionError):\n      raise KeyError('Cannot find a file containing %s' % symbol)\n\n  def FindMessageTypeByName(self, full_name):\n    \"\"\"Loads the named descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the descriptor to load.\n\n    Returns:\n      Descriptor: The descriptor for the named type.\n\n    Raises:\n      KeyError: if the message cannot be found in the pool.\n    \"\"\"\n\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    if full_name not in self._descriptors:\n      self._FindFileContainingSymbolInDb(full_name)\n    return self._descriptors[full_name]\n\n  def FindEnumTypeByName(self, full_name):\n    \"\"\"Loads the named enum descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the enum descriptor to load.\n\n    Returns:\n      EnumDescriptor: The enum descriptor for the named type.\n\n    Raises:\n      KeyError: if the enum cannot be found in the pool.\n    \"\"\"\n\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    if full_name not in self._enum_descriptors:\n      self._FindFileContainingSymbolInDb(full_name)\n    return self._enum_descriptors[full_name]\n\n  def FindFieldByName(self, full_name):\n    \"\"\"Loads the named field descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the field descriptor to load.\n\n    Returns:\n      FieldDescriptor: The field descriptor for the named field.\n\n    Raises:\n      KeyError: if the field cannot be found in the pool.\n    \"\"\"\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    message_name, _, field_name = full_name.rpartition('.')\n    message_descriptor = self.FindMessageTypeByName(message_name)\n    return message_descriptor.fields_by_name[field_name]\n\n  def FindOneofByName(self, full_name):\n    \"\"\"Loads the named oneof descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the oneof descriptor to load.\n\n    Returns:\n      OneofDescriptor: The oneof descriptor for the named oneof.\n\n    Raises:\n      KeyError: if the oneof cannot be found in the pool.\n    \"\"\"\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    message_name, _, oneof_name = full_name.rpartition('.')\n    message_descriptor = self.FindMessageTypeByName(message_name)\n    return message_descriptor.oneofs_by_name[oneof_name]\n\n  def FindExtensionByName(self, full_name):\n    \"\"\"Loads the named extension descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the extension descriptor to load.\n\n    Returns:\n      FieldDescriptor: The field descriptor for the named extension.\n\n    Raises:\n      KeyError: if the extension cannot be found in the pool.\n    \"\"\"\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    try:\n      # The proto compiler does not give any link between the FileDescriptor\n      # and top-level extensions unless the FileDescriptorProto is added to\n      # the DescriptorDatabase, but this can impact memory usage.\n      # So we registered these extensions by name explicitly.\n      return self._toplevel_extensions[full_name]\n    except KeyError:\n      pass\n    message_name, _, extension_name = full_name.rpartition('.')\n    try:\n      # Most extensions are nested inside a message.\n      scope = self.FindMessageTypeByName(message_name)\n    except KeyError:\n      # Some extensions are defined at file scope.\n      scope = self._FindFileContainingSymbolInDb(full_name)\n    return scope.extensions_by_name[extension_name]\n\n  def FindExtensionByNumber(self, message_descriptor, number):\n    \"\"\"Gets the extension of the specified message with the specified number.\n\n    Extensions have to be registered to this pool by calling :func:`Add` or\n    :func:`AddExtensionDescriptor`.\n\n    Args:\n      message_descriptor (Descriptor): descriptor of the extended message.\n      number (int): Number of the extension field.\n\n    Returns:\n      FieldDescriptor: The descriptor for the extension.\n\n    Raises:\n      KeyError: when no extension with the given number is known for the\n        specified message.\n    \"\"\"\n    try:\n      return self._extensions_by_number[message_descriptor][number]\n    except KeyError:\n      self._TryLoadExtensionFromDB(message_descriptor, number)\n      return self._extensions_by_number[message_descriptor][number]\n\n  def FindAllExtensions(self, message_descriptor):\n    \"\"\"Gets all the known extensions of a given message.\n\n    Extensions have to be registered to this pool by build related\n    :func:`Add` or :func:`AddExtensionDescriptor`.\n\n    Args:\n      message_descriptor (Descriptor): Descriptor of the extended message.\n\n    Returns:\n      list[FieldDescriptor]: Field descriptors describing the extensions.\n    \"\"\"\n    # Fallback to descriptor db if FindAllExtensionNumbers is provided.\n    if self._descriptor_db and hasattr(\n        self._descriptor_db, 'FindAllExtensionNumbers'):\n      full_name = message_descriptor.full_name\n      all_numbers = self._descriptor_db.FindAllExtensionNumbers(full_name)\n      for number in all_numbers:\n        if number in self._extensions_by_number[message_descriptor]:\n          continue\n        self._TryLoadExtensionFromDB(message_descriptor, number)\n\n    return list(self._extensions_by_number[message_descriptor].values())\n\n  def _TryLoadExtensionFromDB(self, message_descriptor, number):\n    \"\"\"Try to Load extensions from descriptor db.\n\n    Args:\n      message_descriptor: descriptor of the extended message.\n      number: the extension number that needs to be loaded.\n    \"\"\"\n    if not self._descriptor_db:\n      return\n    # Only supported when FindFileContainingExtension is provided.\n    if not hasattr(\n        self._descriptor_db, 'FindFileContainingExtension'):\n      return\n\n    full_name = message_descriptor.full_name\n    file_proto = self._descriptor_db.FindFileContainingExtension(\n        full_name, number)\n\n    if file_proto is None:\n      return\n\n    try:\n      self._ConvertFileProtoToFileDescriptor(file_proto)\n    except:\n      warn_msg = ('Unable to load proto file %s for extension number %d.' %\n                  (file_proto.name, number))\n      warnings.warn(warn_msg, RuntimeWarning)\n\n  def FindServiceByName(self, full_name):\n    \"\"\"Loads the named service descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the service descriptor to load.\n\n    Returns:\n      ServiceDescriptor: The service descriptor for the named service.\n\n    Raises:\n      KeyError: if the service cannot be found in the pool.\n    \"\"\"\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    if full_name not in self._service_descriptors:\n      self._FindFileContainingSymbolInDb(full_name)\n    return self._service_descriptors[full_name]\n\n  def FindMethodByName(self, full_name):\n    \"\"\"Loads the named service method descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the method descriptor to load.\n\n    Returns:\n      MethodDescriptor: The method descriptor for the service method.\n\n    Raises:\n      KeyError: if the method cannot be found in the pool.\n    \"\"\"\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    service_name, _, method_name = full_name.rpartition('.')\n    service_descriptor = self.FindServiceByName(service_name)\n    return service_descriptor.methods_by_name[method_name]\n\n  def _FindFileContainingSymbolInDb(self, symbol):\n    \"\"\"Finds the file in descriptor DB containing the specified symbol.\n\n    Args:\n      symbol (str): The name of the symbol to search for.\n\n    Returns:\n      FileDescriptor: The file that contains the specified symbol.\n\n    Raises:\n      KeyError: if the file cannot be found in the descriptor database.\n    \"\"\"\n    try:\n      file_proto = self._internal_db.FindFileContainingSymbol(symbol)\n    except KeyError as error:\n      if self._descriptor_db:\n        file_proto = self._descriptor_db.FindFileContainingSymbol(symbol)\n      else:\n        raise error\n    if not file_proto:\n      raise KeyError('Cannot find a file containing %s' % symbol)\n    return self._ConvertFileProtoToFileDescriptor(file_proto)\n\n  def _ConvertFileProtoToFileDescriptor(self, file_proto):\n    \"\"\"Creates a FileDescriptor from a proto or returns a cached copy.\n\n    This method also has the side effect of loading all the symbols found in\n    the file into the appropriate dictionaries in the pool.\n\n    Args:\n      file_proto: The proto to convert.\n\n    Returns:\n      A FileDescriptor matching the passed in proto.\n    \"\"\"\n    if file_proto.name not in self._file_descriptors:\n      built_deps = list(self._GetDeps(file_proto.dependency))\n      direct_deps = [self.FindFileByName(n) for n in file_proto.dependency]\n      public_deps = [direct_deps[i] for i in file_proto.public_dependency]\n\n      file_descriptor = descriptor.FileDescriptor(\n          pool=self,\n          name=file_proto.name,\n          package=file_proto.package,\n          syntax=file_proto.syntax,\n          options=_OptionsOrNone(file_proto),\n          serialized_pb=file_proto.SerializeToString(),\n          dependencies=direct_deps,\n          public_dependencies=public_deps,\n          # pylint: disable=protected-access\n          create_key=descriptor._internal_create_key)\n      scope = {}\n\n      # This loop extracts all the message and enum types from all the\n      # dependencies of the file_proto. This is necessary to create the\n      # scope of available message types when defining the passed in\n      # file proto.\n      for dependency in built_deps:\n        scope.update(self._ExtractSymbols(\n            dependency.message_types_by_name.values()))\n        scope.update((_PrefixWithDot(enum.full_name), enum)\n                     for enum in dependency.enum_types_by_name.values())\n\n      for message_type in file_proto.message_type:\n        message_desc = self._ConvertMessageDescriptor(\n            message_type, file_proto.package, file_descriptor, scope,\n            file_proto.syntax)\n        file_descriptor.message_types_by_name[message_desc.name] = (\n            message_desc)\n\n      for enum_type in file_proto.enum_type:\n        file_descriptor.enum_types_by_name[enum_type.name] = (\n            self._ConvertEnumDescriptor(enum_type, file_proto.package,\n                                        file_descriptor, None, scope, True))\n\n      for index, extension_proto in enumerate(file_proto.extension):\n        extension_desc = self._MakeFieldDescriptor(\n            extension_proto, file_proto.package, index, file_descriptor,\n            is_extension=True)\n        extension_desc.containing_type = self._GetTypeFromScope(\n            file_descriptor.package, extension_proto.extendee, scope)\n        self._SetFieldType(extension_proto, extension_desc,\n                           file_descriptor.package, scope)\n        file_descriptor.extensions_by_name[extension_desc.name] = (\n            extension_desc)\n        self._file_desc_by_toplevel_extension[extension_desc.full_name] = (\n            file_descriptor)\n\n      for desc_proto in file_proto.message_type:\n        self._SetAllFieldTypes(file_proto.package, desc_proto, scope)\n\n      if file_proto.package:\n        desc_proto_prefix = _PrefixWithDot(file_proto.package)\n      else:\n        desc_proto_prefix = ''\n\n      for desc_proto in file_proto.message_type:\n        desc = self._GetTypeFromScope(\n            desc_proto_prefix, desc_proto.name, scope)\n        file_descriptor.message_types_by_name[desc_proto.name] = desc\n\n      for index, service_proto in enumerate(file_proto.service):\n        file_descriptor.services_by_name[service_proto.name] = (\n            self._MakeServiceDescriptor(service_proto, index, scope,\n                                        file_proto.package, file_descriptor))\n\n      self._file_descriptors[file_proto.name] = file_descriptor\n\n    # Add extensions to the pool\n    file_desc = self._file_descriptors[file_proto.name]\n    for extension in file_desc.extensions_by_name.values():\n      self._AddExtensionDescriptor(extension)\n    for message_type in file_desc.message_types_by_name.values():\n      for extension in message_type.extensions:\n        self._AddExtensionDescriptor(extension)\n\n    return file_desc\n\n  def _ConvertMessageDescriptor(self, desc_proto, package=None, file_desc=None,\n                                scope=None, syntax=None):\n    \"\"\"Adds the proto to the pool in the specified package.\n\n    Args:\n      desc_proto: The descriptor_pb2.DescriptorProto protobuf message.\n      package: The package the proto should be located in.\n      file_desc: The file containing this message.\n      scope: Dict mapping short and full symbols to message and enum types.\n      syntax: string indicating syntax of the file (\"proto2\" or \"proto3\")\n\n    Returns:\n      The added descriptor.\n    \"\"\"\n\n    if package:\n      desc_name = '.'.join((package, desc_proto.name))\n    else:\n      desc_name = desc_proto.name\n\n    if file_desc is None:\n      file_name = None\n    else:\n      file_name = file_desc.name\n\n    if scope is None:\n      scope = {}\n\n    nested = [\n        self._ConvertMessageDescriptor(\n            nested, desc_name, file_desc, scope, syntax)\n        for nested in desc_proto.nested_type]\n    enums = [\n        self._ConvertEnumDescriptor(enum, desc_name, file_desc, None,\n                                    scope, False)\n        for enum in desc_proto.enum_type]\n    fields = [self._MakeFieldDescriptor(field, desc_name, index, file_desc)\n              for index, field in enumerate(desc_proto.field)]\n    extensions = [\n        self._MakeFieldDescriptor(extension, desc_name, index, file_desc,\n                                  is_extension=True)\n        for index, extension in enumerate(desc_proto.extension)]\n    oneofs = [\n        # pylint: disable=g-complex-comprehension\n        descriptor.OneofDescriptor(\n            desc.name,\n            '.'.join((desc_name, desc.name)),\n            index,\n            None,\n            [],\n            _OptionsOrNone(desc),\n            # pylint: disable=protected-access\n            create_key=descriptor._internal_create_key)\n        for index, desc in enumerate(desc_proto.oneof_decl)\n    ]\n    extension_ranges = [(r.start, r.end) for r in desc_proto.extension_range]\n    if extension_ranges:\n      is_extendable = True\n    else:\n      is_extendable = False\n    desc = descriptor.Descriptor(\n        name=desc_proto.name,\n        full_name=desc_name,\n        filename=file_name,\n        containing_type=None,\n        fields=fields,\n        oneofs=oneofs,\n        nested_types=nested,\n        enum_types=enums,\n        extensions=extensions,\n        options=_OptionsOrNone(desc_proto),\n        is_extendable=is_extendable,\n        extension_ranges=extension_ranges,\n        file=file_desc,\n        serialized_start=None,\n        serialized_end=None,\n        syntax=syntax,\n        # pylint: disable=protected-access\n        create_key=descriptor._internal_create_key)\n    for nested in desc.nested_types:\n      nested.containing_type = desc\n    for enum in desc.enum_types:\n      enum.containing_type = desc\n    for field_index, field_desc in enumerate(desc_proto.field):\n      if field_desc.HasField('oneof_index'):\n        oneof_index = field_desc.oneof_index\n        oneofs[oneof_index].fields.append(fields[field_index])\n        fields[field_index].containing_oneof = oneofs[oneof_index]\n\n    scope[_PrefixWithDot(desc_name)] = desc\n    self._CheckConflictRegister(desc, desc.full_name, desc.file.name)\n    self._descriptors[desc_name] = desc\n    return desc\n\n  def _ConvertEnumDescriptor(self, enum_proto, package=None, file_desc=None,\n                             containing_type=None, scope=None, top_level=False):\n    \"\"\"Make a protobuf EnumDescriptor given an EnumDescriptorProto protobuf.\n\n    Args:\n      enum_proto: The descriptor_pb2.EnumDescriptorProto protobuf message.\n      package: Optional package name for the new message EnumDescriptor.\n      file_desc: The file containing the enum descriptor.\n      containing_type: The type containing this enum.\n      scope: Scope containing available types.\n      top_level: If True, the enum is a top level symbol. If False, the enum\n          is defined inside a message.\n\n    Returns:\n      The added descriptor\n    \"\"\"\n\n    if package:\n      enum_name = '.'.join((package, enum_proto.name))\n    else:\n      enum_name = enum_proto.name\n\n    if file_desc is None:\n      file_name = None\n    else:\n      file_name = file_desc.name\n\n    values = [self._MakeEnumValueDescriptor(value, index)\n              for index, value in enumerate(enum_proto.value)]\n    desc = descriptor.EnumDescriptor(name=enum_proto.name,\n                                     full_name=enum_name,\n                                     filename=file_name,\n                                     file=file_desc,\n                                     values=values,\n                                     containing_type=containing_type,\n                                     options=_OptionsOrNone(enum_proto),\n                                     # pylint: disable=protected-access\n                                     create_key=descriptor._internal_create_key)\n    scope['.%s' % enum_name] = desc\n    self._CheckConflictRegister(desc, desc.full_name, desc.file.name)\n    self._enum_descriptors[enum_name] = desc\n\n    # Add top level enum values.\n    if top_level:\n      for value in values:\n        full_name = _NormalizeFullyQualifiedName(\n            '.'.join((package, value.name)))\n        self._CheckConflictRegister(value, full_name, file_name)\n        self._top_enum_values[full_name] = value\n\n    return desc\n\n  def _MakeFieldDescriptor(self, field_proto, message_name, index,\n                           file_desc, is_extension=False):\n    \"\"\"Creates a field descriptor from a FieldDescriptorProto.\n\n    For message and enum type fields, this method will do a look up\n    in the pool for the appropriate descriptor for that type. If it\n    is unavailable, it will fall back to the _source function to\n    create it. If this type is still unavailable, construction will\n    fail.\n\n    Args:\n      field_proto: The proto describing the field.\n      message_name: The name of the containing message.\n      index: Index of the field\n      file_desc: The file containing the field descriptor.\n      is_extension: Indication that this field is for an extension.\n\n    Returns:\n      An initialized FieldDescriptor object\n    \"\"\"\n\n    if message_name:\n      full_name = '.'.join((message_name, field_proto.name))\n    else:\n      full_name = field_proto.name\n\n    if field_proto.json_name:\n      json_name = field_proto.json_name\n    else:\n      json_name = None\n\n    return descriptor.FieldDescriptor(\n        name=field_proto.name,\n        full_name=full_name,\n        index=index,\n        number=field_proto.number,\n        type=field_proto.type,\n        cpp_type=None,\n        message_type=None,\n        enum_type=None,\n        containing_type=None,\n        label=field_proto.label,\n        has_default_value=False,\n        default_value=None,\n        is_extension=is_extension,\n        extension_scope=None,\n        options=_OptionsOrNone(field_proto),\n        json_name=json_name,\n        file=file_desc,\n        # pylint: disable=protected-access\n        create_key=descriptor._internal_create_key)\n\n  def _SetAllFieldTypes(self, package, desc_proto, scope):\n    \"\"\"Sets all the descriptor's fields's types.\n\n    This method also sets the containing types on any extensions.\n\n    Args:\n      package: The current package of desc_proto.\n      desc_proto: The message descriptor to update.\n      scope: Enclosing scope of available types.\n    \"\"\"\n\n    package = _PrefixWithDot(package)\n\n    main_desc = self._GetTypeFromScope(package, desc_proto.name, scope)\n\n    if package == '.':\n      nested_package = _PrefixWithDot(desc_proto.name)\n    else:\n      nested_package = '.'.join([package, desc_proto.name])\n\n    for field_proto, field_desc in zip(desc_proto.field, main_desc.fields):\n      self._SetFieldType(field_proto, field_desc, nested_package, scope)\n\n    for extension_proto, extension_desc in (\n        zip(desc_proto.extension, main_desc.extensions)):\n      extension_desc.containing_type = self._GetTypeFromScope(\n          nested_package, extension_proto.extendee, scope)\n      self._SetFieldType(extension_proto, extension_desc, nested_package, scope)\n\n    for nested_type in desc_proto.nested_type:\n      self._SetAllFieldTypes(nested_package, nested_type, scope)\n\n  def _SetFieldType(self, field_proto, field_desc, package, scope):\n    \"\"\"Sets the field's type, cpp_type, message_type and enum_type.\n\n    Args:\n      field_proto: Data about the field in proto format.\n      field_desc: The descriptor to modify.\n      package: The package the field's container is in.\n      scope: Enclosing scope of available types.\n    \"\"\"\n    if field_proto.type_name:\n      desc = self._GetTypeFromScope(package, field_proto.type_name, scope)\n    else:\n      desc = None\n\n    if not field_proto.HasField('type'):\n      if isinstance(desc, descriptor.Descriptor):\n        field_proto.type = descriptor.FieldDescriptor.TYPE_MESSAGE\n      else:\n        field_proto.type = descriptor.FieldDescriptor.TYPE_ENUM\n\n    field_desc.cpp_type = descriptor.FieldDescriptor.ProtoTypeToCppProtoType(\n        field_proto.type)\n\n    if (field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE\n        or field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP):\n      field_desc.message_type = desc\n\n    if field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM:\n      field_desc.enum_type = desc\n\n    if field_proto.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n      field_desc.has_default_value = False\n      field_desc.default_value = []\n    elif field_proto.HasField('default_value'):\n      field_desc.has_default_value = True\n      if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or\n          field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT):\n        field_desc.default_value = float(field_proto.default_value)\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING:\n        field_desc.default_value = field_proto.default_value\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL:\n        field_desc.default_value = field_proto.default_value.lower() == 'true'\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM:\n        field_desc.default_value = field_desc.enum_type.values_by_name[\n            field_proto.default_value].number\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES:\n        field_desc.default_value = text_encoding.CUnescape(\n            field_proto.default_value)\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE:\n        field_desc.default_value = None\n      else:\n        # All other types are of the \"int\" type.\n        field_desc.default_value = int(field_proto.default_value)\n    else:\n      field_desc.has_default_value = False\n      if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or\n          field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT):\n        field_desc.default_value = 0.0\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING:\n        field_desc.default_value = u''\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL:\n        field_desc.default_value = False\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM:\n        field_desc.default_value = field_desc.enum_type.values[0].number\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES:\n        field_desc.default_value = b''\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE:\n        field_desc.default_value = None\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP:\n        field_desc.default_value = None\n      else:\n        # All other types are of the \"int\" type.\n        field_desc.default_value = 0\n\n    field_desc.type = field_proto.type\n\n  def _MakeEnumValueDescriptor(self, value_proto, index):\n    \"\"\"Creates a enum value descriptor object from a enum value proto.\n\n    Args:\n      value_proto: The proto describing the enum value.\n      index: The index of the enum value.\n\n    Returns:\n      An initialized EnumValueDescriptor object.\n    \"\"\"\n\n    return descriptor.EnumValueDescriptor(\n        name=value_proto.name,\n        index=index,\n        number=value_proto.number,\n        options=_OptionsOrNone(value_proto),\n        type=None,\n        # pylint: disable=protected-access\n        create_key=descriptor._internal_create_key)\n\n  def _MakeServiceDescriptor(self, service_proto, service_index, scope,\n                             package, file_desc):\n    \"\"\"Make a protobuf ServiceDescriptor given a ServiceDescriptorProto.\n\n    Args:\n      service_proto: The descriptor_pb2.ServiceDescriptorProto protobuf message.\n      service_index: The index of the service in the File.\n      scope: Dict mapping short and full symbols to message and enum types.\n      package: Optional package name for the new message EnumDescriptor.\n      file_desc: The file containing the service descriptor.\n\n    Returns:\n      The added descriptor.\n    \"\"\"\n\n    if package:\n      service_name = '.'.join((package, service_proto.name))\n    else:\n      service_name = service_proto.name\n\n    methods = [self._MakeMethodDescriptor(method_proto, service_name, package,\n                                          scope, index)\n               for index, method_proto in enumerate(service_proto.method)]\n    desc = descriptor.ServiceDescriptor(\n        name=service_proto.name,\n        full_name=service_name,\n        index=service_index,\n        methods=methods,\n        options=_OptionsOrNone(service_proto),\n        file=file_desc,\n        # pylint: disable=protected-access\n        create_key=descriptor._internal_create_key)\n    self._CheckConflictRegister(desc, desc.full_name, desc.file.name)\n    self._service_descriptors[service_name] = desc\n    return desc\n\n  def _MakeMethodDescriptor(self, method_proto, service_name, package, scope,\n                            index):\n    \"\"\"Creates a method descriptor from a MethodDescriptorProto.\n\n    Args:\n      method_proto: The proto describing the method.\n      service_name: The name of the containing service.\n      package: Optional package name to look up for types.\n      scope: Scope containing available types.\n      index: Index of the method in the service.\n\n    Returns:\n      An initialized MethodDescriptor object.\n    \"\"\"\n    full_name = '.'.join((service_name, method_proto.name))\n    input_type = self._GetTypeFromScope(\n        package, method_proto.input_type, scope)\n    output_type = self._GetTypeFromScope(\n        package, method_proto.output_type, scope)\n    return descriptor.MethodDescriptor(\n        name=method_proto.name,\n        full_name=full_name,\n        index=index,\n        containing_service=None,\n        input_type=input_type,\n        output_type=output_type,\n        client_streaming=method_proto.client_streaming,\n        server_streaming=method_proto.server_streaming,\n        options=_OptionsOrNone(method_proto),\n        # pylint: disable=protected-access\n        create_key=descriptor._internal_create_key)\n\n  def _ExtractSymbols(self, descriptors):\n    \"\"\"Pulls out all the symbols from descriptor protos.\n\n    Args:\n      descriptors: The messages to extract descriptors from.\n    Yields:\n      A two element tuple of the type name and descriptor object.\n    \"\"\"\n\n    for desc in descriptors:\n      yield (_PrefixWithDot(desc.full_name), desc)\n      for symbol in self._ExtractSymbols(desc.nested_types):\n        yield symbol\n      for enum in desc.enum_types:\n        yield (_PrefixWithDot(enum.full_name), enum)\n\n  def _GetDeps(self, dependencies, visited=None):\n    \"\"\"Recursively finds dependencies for file protos.\n\n    Args:\n      dependencies: The names of the files being depended on.\n      visited: The names of files already found.\n\n    Yields:\n      Each direct and indirect dependency.\n    \"\"\"\n\n    visited = visited or set()\n    for dependency in dependencies:\n      if dependency not in visited:\n        visited.add(dependency)\n        dep_desc = self.FindFileByName(dependency)\n        yield dep_desc\n        public_files = [d.name for d in dep_desc.public_dependencies]\n        yield from self._GetDeps(public_files, visited)\n\n  def _GetTypeFromScope(self, package, type_name, scope):\n    \"\"\"Finds a given type name in the current scope.\n\n    Args:\n      package: The package the proto should be located in.\n      type_name: The name of the type to be found in the scope.\n      scope: Dict mapping short and full symbols to message and enum types.\n\n    Returns:\n      The descriptor for the requested type.\n    \"\"\"\n    if type_name not in scope:\n      components = _PrefixWithDot(package).split('.')\n      while components:\n        possible_match = '.'.join(components + [type_name])\n        if possible_match in scope:\n          type_name = possible_match\n          break\n        else:\n          components.pop(-1)\n    return scope[type_name]\n\n\ndef _PrefixWithDot(name):\n  return name if name.startswith('.') else '.%s' % name\n\n\nif _USE_C_DESCRIPTORS:\n  # TODO(amauryfa): This pool could be constructed from Python code, when we\n  # support a flag like 'use_cpp_generated_pool=True'.\n  # pylint: disable=protected-access\n  _DEFAULT = descriptor._message.default_pool\nelse:\n  _DEFAULT = DescriptorPool()\n\n\ndef Default():\n  return _DEFAULT\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/duration_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/duration.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x1egoogle/protobuf/duration.proto\\x12\\x0fgoogle.protobuf\\\"*\\n\\x08\\x44uration\\x12\\x0f\\n\\x07seconds\\x18\\x01 \\x01(\\x03\\x12\\r\\n\\x05nanos\\x18\\x02 \\x01(\\x05\\x42\\x83\\x01\\n\\x13\\x63om.google.protobufB\\rDurationProtoP\\x01Z1google.golang.org/protobuf/types/known/durationpb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.duration_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\rDurationProtoP\\001Z1google.golang.org/protobuf/types/known/durationpb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _DURATION._serialized_start=51\n  _DURATION._serialized_end=93\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/empty_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/empty.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x1bgoogle/protobuf/empty.proto\\x12\\x0fgoogle.protobuf\\\"\\x07\\n\\x05\\x45mptyB}\\n\\x13\\x63om.google.protobufB\\nEmptyProtoP\\x01Z.google.golang.org/protobuf/types/known/emptypb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.empty_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\nEmptyProtoP\\001Z.google.golang.org/protobuf/types/known/emptypb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _EMPTY._serialized_start=48\n  _EMPTY._serialized_end=55\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/field_mask_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/field_mask.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n google/protobuf/field_mask.proto\\x12\\x0fgoogle.protobuf\\\"\\x1a\\n\\tFieldMask\\x12\\r\\n\\x05paths\\x18\\x01 \\x03(\\tB\\x85\\x01\\n\\x13\\x63om.google.protobufB\\x0e\\x46ieldMaskProtoP\\x01Z2google.golang.org/protobuf/types/known/fieldmaskpb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.field_mask_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\016FieldMaskProtoP\\001Z2google.golang.org/protobuf/types/known/fieldmaskpb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _FIELDMASK._serialized_start=53\n  _FIELDMASK._serialized_end=79\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/_parameterized.py",
    "content": "#! /usr/bin/env python\n#\n# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Adds support for parameterized tests to Python's unittest TestCase class.\n\nA parameterized test is a method in a test case that is invoked with different\nargument tuples.\n\nA simple example:\n\n  class AdditionExample(parameterized.TestCase):\n    @parameterized.parameters(\n       (1, 2, 3),\n       (4, 5, 9),\n       (1, 1, 3))\n    def testAddition(self, op1, op2, result):\n      self.assertEqual(result, op1 + op2)\n\n\nEach invocation is a separate test case and properly isolated just\nlike a normal test method, with its own setUp/tearDown cycle. In the\nexample above, there are three separate testcases, one of which will\nfail due to an assertion error (1 + 1 != 3).\n\nParameters for individual test cases can be tuples (with positional parameters)\nor dictionaries (with named parameters):\n\n  class AdditionExample(parameterized.TestCase):\n    @parameterized.parameters(\n       {'op1': 1, 'op2': 2, 'result': 3},\n       {'op1': 4, 'op2': 5, 'result': 9},\n    )\n    def testAddition(self, op1, op2, result):\n      self.assertEqual(result, op1 + op2)\n\nIf a parameterized test fails, the error message will show the\noriginal test name (which is modified internally) and the arguments\nfor the specific invocation, which are part of the string returned by\nthe shortDescription() method on test cases.\n\nThe id method of the test, used internally by the unittest framework,\nis also modified to show the arguments. To make sure that test names\nstay the same across several invocations, object representations like\n\n  >>> class Foo(object):\n  ...  pass\n  >>> repr(Foo())\n  '<__main__.Foo object at 0x23d8610>'\n\nare turned into '<__main__.Foo>'. For even more descriptive names,\nespecially in test logs, you can use the named_parameters decorator. In\nthis case, only tuples are supported, and the first parameters has to\nbe a string (or an object that returns an apt name when converted via\nstr()):\n\n  class NamedExample(parameterized.TestCase):\n    @parameterized.named_parameters(\n       ('Normal', 'aa', 'aaa', True),\n       ('EmptyPrefix', '', 'abc', True),\n       ('BothEmpty', '', '', True))\n    def testStartsWith(self, prefix, string, result):\n      self.assertEqual(result, strings.startswith(prefix))\n\nNamed tests also have the benefit that they can be run individually\nfrom the command line:\n\n  $ testmodule.py NamedExample.testStartsWithNormal\n  .\n  --------------------------------------------------------------------\n  Ran 1 test in 0.000s\n\n  OK\n\nParameterized Classes\n=====================\nIf invocation arguments are shared across test methods in a single\nTestCase class, instead of decorating all test methods\nindividually, the class itself can be decorated:\n\n  @parameterized.parameters(\n    (1, 2, 3)\n    (4, 5, 9))\n  class ArithmeticTest(parameterized.TestCase):\n    def testAdd(self, arg1, arg2, result):\n      self.assertEqual(arg1 + arg2, result)\n\n    def testSubtract(self, arg2, arg2, result):\n      self.assertEqual(result - arg1, arg2)\n\nInputs from Iterables\n=====================\nIf parameters should be shared across several test cases, or are dynamically\ncreated from other sources, a single non-tuple iterable can be passed into\nthe decorator. This iterable will be used to obtain the test cases:\n\n  class AdditionExample(parameterized.TestCase):\n    @parameterized.parameters(\n      c.op1, c.op2, c.result for c in testcases\n    )\n    def testAddition(self, op1, op2, result):\n      self.assertEqual(result, op1 + op2)\n\n\nSingle-Argument Test Methods\n============================\nIf a test method takes only one argument, the single argument does not need to\nbe wrapped into a tuple:\n\n  class NegativeNumberExample(parameterized.TestCase):\n    @parameterized.parameters(\n       -1, -3, -4, -5\n    )\n    def testIsNegative(self, arg):\n      self.assertTrue(IsNegative(arg))\n\"\"\"\n\n__author__ = 'tmarek@google.com (Torsten Marek)'\n\nimport functools\nimport re\nimport types\nimport unittest\nimport uuid\n\ntry:\n  # Since python 3\n  import collections.abc as collections_abc\nexcept ImportError:\n  # Won't work after python 3.8\n  import collections as collections_abc\n\nADDR_RE = re.compile(r'\\<([a-zA-Z0-9_\\-\\.]+) object at 0x[a-fA-F0-9]+\\>')\n_SEPARATOR = uuid.uuid1().hex\n_FIRST_ARG = object()\n_ARGUMENT_REPR = object()\n\n\ndef _CleanRepr(obj):\n  return ADDR_RE.sub(r'<\\1>', repr(obj))\n\n\n# Helper function formerly from the unittest module, removed from it in\n# Python 2.7.\ndef _StrClass(cls):\n  return '%s.%s' % (cls.__module__, cls.__name__)\n\n\ndef _NonStringIterable(obj):\n  return (isinstance(obj, collections_abc.Iterable) and\n          not isinstance(obj, str))\n\n\ndef _FormatParameterList(testcase_params):\n  if isinstance(testcase_params, collections_abc.Mapping):\n    return ', '.join('%s=%s' % (argname, _CleanRepr(value))\n                     for argname, value in testcase_params.items())\n  elif _NonStringIterable(testcase_params):\n    return ', '.join(map(_CleanRepr, testcase_params))\n  else:\n    return _FormatParameterList((testcase_params,))\n\n\nclass _ParameterizedTestIter(object):\n  \"\"\"Callable and iterable class for producing new test cases.\"\"\"\n\n  def __init__(self, test_method, testcases, naming_type):\n    \"\"\"Returns concrete test functions for a test and a list of parameters.\n\n    The naming_type is used to determine the name of the concrete\n    functions as reported by the unittest framework. If naming_type is\n    _FIRST_ARG, the testcases must be tuples, and the first element must\n    have a string representation that is a valid Python identifier.\n\n    Args:\n      test_method: The decorated test method.\n      testcases: (list of tuple/dict) A list of parameter\n                 tuples/dicts for individual test invocations.\n      naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR.\n    \"\"\"\n    self._test_method = test_method\n    self.testcases = testcases\n    self._naming_type = naming_type\n\n  def __call__(self, *args, **kwargs):\n    raise RuntimeError('You appear to be running a parameterized test case '\n                       'without having inherited from parameterized.'\n                       'TestCase. This is bad because none of '\n                       'your test cases are actually being run.')\n\n  def __iter__(self):\n    test_method = self._test_method\n    naming_type = self._naming_type\n\n    def MakeBoundParamTest(testcase_params):\n      @functools.wraps(test_method)\n      def BoundParamTest(self):\n        if isinstance(testcase_params, collections_abc.Mapping):\n          test_method(self, **testcase_params)\n        elif _NonStringIterable(testcase_params):\n          test_method(self, *testcase_params)\n        else:\n          test_method(self, testcase_params)\n\n      if naming_type is _FIRST_ARG:\n        # Signal the metaclass that the name of the test function is unique\n        # and descriptive.\n        BoundParamTest.__x_use_name__ = True\n        BoundParamTest.__name__ += str(testcase_params[0])\n        testcase_params = testcase_params[1:]\n      elif naming_type is _ARGUMENT_REPR:\n        # __x_extra_id__ is used to pass naming information to the __new__\n        # method of TestGeneratorMetaclass.\n        # The metaclass will make sure to create a unique, but nondescriptive\n        # name for this test.\n        BoundParamTest.__x_extra_id__ = '(%s)' % (\n            _FormatParameterList(testcase_params),)\n      else:\n        raise RuntimeError('%s is not a valid naming type.' % (naming_type,))\n\n      BoundParamTest.__doc__ = '%s(%s)' % (\n          BoundParamTest.__name__, _FormatParameterList(testcase_params))\n      if test_method.__doc__:\n        BoundParamTest.__doc__ += '\\n%s' % (test_method.__doc__,)\n      return BoundParamTest\n    return (MakeBoundParamTest(c) for c in self.testcases)\n\n\ndef _IsSingletonList(testcases):\n  \"\"\"True iff testcases contains only a single non-tuple element.\"\"\"\n  return len(testcases) == 1 and not isinstance(testcases[0], tuple)\n\n\ndef _ModifyClass(class_object, testcases, naming_type):\n  assert not getattr(class_object, '_id_suffix', None), (\n      'Cannot add parameters to %s,'\n      ' which already has parameterized methods.' % (class_object,))\n  class_object._id_suffix = id_suffix = {}\n  # We change the size of __dict__ while we iterate over it,\n  # which Python 3.x will complain about, so use copy().\n  for name, obj in class_object.__dict__.copy().items():\n    if (name.startswith(unittest.TestLoader.testMethodPrefix)\n        and isinstance(obj, types.FunctionType)):\n      delattr(class_object, name)\n      methods = {}\n      _UpdateClassDictForParamTestCase(\n          methods, id_suffix, name,\n          _ParameterizedTestIter(obj, testcases, naming_type))\n      for name, meth in methods.items():\n        setattr(class_object, name, meth)\n\n\ndef _ParameterDecorator(naming_type, testcases):\n  \"\"\"Implementation of the parameterization decorators.\n\n  Args:\n    naming_type: The naming type.\n    testcases: Testcase parameters.\n\n  Returns:\n    A function for modifying the decorated object.\n  \"\"\"\n  def _Apply(obj):\n    if isinstance(obj, type):\n      _ModifyClass(\n          obj,\n          list(testcases) if not isinstance(testcases, collections_abc.Sequence)\n          else testcases,\n          naming_type)\n      return obj\n    else:\n      return _ParameterizedTestIter(obj, testcases, naming_type)\n\n  if _IsSingletonList(testcases):\n    assert _NonStringIterable(testcases[0]), (\n        'Single parameter argument must be a non-string iterable')\n    testcases = testcases[0]\n\n  return _Apply\n\n\ndef parameters(*testcases):  # pylint: disable=invalid-name\n  \"\"\"A decorator for creating parameterized tests.\n\n  See the module docstring for a usage example.\n  Args:\n    *testcases: Parameters for the decorated method, either a single\n                iterable, or a list of tuples/dicts/objects (for tests\n                with only one argument).\n\n  Returns:\n     A test generator to be handled by TestGeneratorMetaclass.\n  \"\"\"\n  return _ParameterDecorator(_ARGUMENT_REPR, testcases)\n\n\ndef named_parameters(*testcases):  # pylint: disable=invalid-name\n  \"\"\"A decorator for creating parameterized tests.\n\n  See the module docstring for a usage example. The first element of\n  each parameter tuple should be a string and will be appended to the\n  name of the test method.\n\n  Args:\n    *testcases: Parameters for the decorated method, either a single\n                iterable, or a list of tuples.\n\n  Returns:\n     A test generator to be handled by TestGeneratorMetaclass.\n  \"\"\"\n  return _ParameterDecorator(_FIRST_ARG, testcases)\n\n\nclass TestGeneratorMetaclass(type):\n  \"\"\"Metaclass for test cases with test generators.\n\n  A test generator is an iterable in a testcase that produces callables. These\n  callables must be single-argument methods. These methods are injected into\n  the class namespace and the original iterable is removed. If the name of the\n  iterable conforms to the test pattern, the injected methods will be picked\n  up as tests by the unittest framework.\n\n  In general, it is supposed to be used in conjunction with the\n  parameters decorator.\n  \"\"\"\n\n  def __new__(mcs, class_name, bases, dct):\n    dct['_id_suffix'] = id_suffix = {}\n    for name, obj in dct.copy().items():\n      if (name.startswith(unittest.TestLoader.testMethodPrefix) and\n          _NonStringIterable(obj)):\n        iterator = iter(obj)\n        dct.pop(name)\n        _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator)\n\n    return type.__new__(mcs, class_name, bases, dct)\n\n\ndef _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator):\n  \"\"\"Adds individual test cases to a dictionary.\n\n  Args:\n    dct: The target dictionary.\n    id_suffix: The dictionary for mapping names to test IDs.\n    name: The original name of the test case.\n    iterator: The iterator generating the individual test cases.\n  \"\"\"\n  for idx, func in enumerate(iterator):\n    assert callable(func), 'Test generators must yield callables, got %r' % (\n        func,)\n    if getattr(func, '__x_use_name__', False):\n      new_name = func.__name__\n    else:\n      new_name = '%s%s%d' % (name, _SEPARATOR, idx)\n    assert new_name not in dct, (\n        'Name of parameterized test case \"%s\" not unique' % (new_name,))\n    dct[new_name] = func\n    id_suffix[new_name] = getattr(func, '__x_extra_id__', '')\n\n\nclass TestCase(unittest.TestCase, metaclass=TestGeneratorMetaclass):\n  \"\"\"Base class for test cases using the parameters decorator.\"\"\"\n\n  def _OriginalName(self):\n    return self._testMethodName.split(_SEPARATOR)[0]\n\n  def __str__(self):\n    return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__))\n\n  def id(self):  # pylint: disable=invalid-name\n    \"\"\"Returns the descriptive ID of the test.\n\n    This is used internally by the unittesting framework to get a name\n    for the test to be used in reports.\n\n    Returns:\n      The test id.\n    \"\"\"\n    return '%s.%s%s' % (_StrClass(self.__class__),\n                        self._OriginalName(),\n                        self._id_suffix.get(self._testMethodName, ''))\n\n\ndef CoopTestCase(other_base_class):\n  \"\"\"Returns a new base class with a cooperative metaclass base.\n\n  This enables the TestCase to be used in combination\n  with other base classes that have custom metaclasses, such as\n  mox.MoxTestBase.\n\n  Only works with metaclasses that do not override type.__new__.\n\n  Example:\n\n    import google3\n    import mox\n\n    from google3.testing.pybase import parameterized\n\n    class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)):\n      ...\n\n  Args:\n    other_base_class: (class) A test case base class.\n\n  Returns:\n    A new class object.\n  \"\"\"\n  metaclass = type(\n      'CoopMetaclass',\n      (other_base_class.__metaclass__,\n       TestGeneratorMetaclass), {})\n  return metaclass(\n      'CoopTestCase',\n      (other_base_class, TestCase), {})\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/api_implementation.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Determine which implementation of the protobuf API is used in this process.\n\"\"\"\n\nimport os\nimport sys\nimport warnings\n\ntry:\n  # pylint: disable=g-import-not-at-top\n  from google.protobuf.internal import _api_implementation\n  # The compile-time constants in the _api_implementation module can be used to\n  # switch to a certain implementation of the Python API at build time.\n  _api_version = _api_implementation.api_version\nexcept ImportError:\n  _api_version = -1  # Unspecified by compiler flags.\n\nif _api_version == 1:\n  raise ValueError('api_version=1 is no longer supported.')\n\n\n_default_implementation_type = ('cpp' if _api_version > 0 else 'python')\n\n\n# This environment variable can be used to switch to a certain implementation\n# of the Python API, overriding the compile-time constants in the\n# _api_implementation module. Right now only 'python' and 'cpp' are valid\n# values. Any other value will be ignored.\n_implementation_type = os.getenv('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION',\n                                 _default_implementation_type)\n\nif _implementation_type != 'python':\n  _implementation_type = 'cpp'\n\nif 'PyPy' in sys.version and _implementation_type == 'cpp':\n  warnings.warn('PyPy does not work yet with cpp protocol buffers. '\n                'Falling back to the python implementation.')\n  _implementation_type = 'python'\n\n\n# Detect if serialization should be deterministic by default\ntry:\n  # The presence of this module in a build allows the proto implementation to\n  # be upgraded merely via build deps.\n  #\n  # NOTE: Merely importing this automatically enables deterministic proto\n  # serialization for C++ code, but we still need to export it as a boolean so\n  # that we can do the same for `_implementation_type == 'python'`.\n  #\n  # NOTE2: It is possible for C++ code to enable deterministic serialization by\n  # default _without_ affecting Python code, if the C++ implementation is not in\n  # use by this module.  That is intended behavior, so we don't actually expose\n  # this boolean outside of this module.\n  #\n  # pylint: disable=g-import-not-at-top,unused-import\n  from google.protobuf import enable_deterministic_proto_serialization\n  _python_deterministic_proto_serialization = True\nexcept ImportError:\n  _python_deterministic_proto_serialization = False\n\n\n# Usage of this function is discouraged. Clients shouldn't care which\n# implementation of the API is in use. Note that there is no guarantee\n# that differences between APIs will be maintained.\n# Please don't use this function if possible.\ndef Type():\n  return _implementation_type\n\n\ndef _SetType(implementation_type):\n  \"\"\"Never use! Only for protobuf benchmark.\"\"\"\n  global _implementation_type\n  _implementation_type = implementation_type\n\n\n# See comment on 'Type' above.\ndef Version():\n  return 2\n\n\n# For internal use only\ndef IsPythonDefaultSerializationDeterministic():\n  return _python_deterministic_proto_serialization\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/builder.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Builds descriptors, message classes and services for generated _pb2.py.\n\nThis file is only called in python generated _pb2.py files. It builds\ndescriptors, message classes and services that users can directly use\nin generated code.\n\"\"\"\n\n__author__ = 'jieluo@google.com (Jie Luo)'\n\nfrom google.protobuf.internal import enum_type_wrapper\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\n\n_sym_db = _symbol_database.Default()\n\n\ndef BuildMessageAndEnumDescriptors(file_des, module):\n  \"\"\"Builds message and enum descriptors.\n\n  Args:\n    file_des: FileDescriptor of the .proto file\n    module: Generated _pb2 module\n  \"\"\"\n\n  def BuildNestedDescriptors(msg_des, prefix):\n    for (name, nested_msg) in msg_des.nested_types_by_name.items():\n      module_name = prefix + name.upper()\n      module[module_name] = nested_msg\n      BuildNestedDescriptors(nested_msg, module_name + '_')\n    for enum_des in msg_des.enum_types:\n      module[prefix + enum_des.name.upper()] = enum_des\n\n  for (name, msg_des) in file_des.message_types_by_name.items():\n    module_name = '_' + name.upper()\n    module[module_name] = msg_des\n    BuildNestedDescriptors(msg_des, module_name + '_')\n\n\ndef BuildTopDescriptorsAndMessages(file_des, module_name, module):\n  \"\"\"Builds top level descriptors and message classes.\n\n  Args:\n    file_des: FileDescriptor of the .proto file\n    module_name: str, the name of generated _pb2 module\n    module: Generated _pb2 module\n  \"\"\"\n\n  def BuildMessage(msg_des):\n    create_dict = {}\n    for (name, nested_msg) in msg_des.nested_types_by_name.items():\n      create_dict[name] = BuildMessage(nested_msg)\n    create_dict['DESCRIPTOR'] = msg_des\n    create_dict['__module__'] = module_name\n    message_class = _reflection.GeneratedProtocolMessageType(\n        msg_des.name, (_message.Message,), create_dict)\n    _sym_db.RegisterMessage(message_class)\n    return message_class\n\n  # top level enums\n  for (name, enum_des) in file_des.enum_types_by_name.items():\n    module['_' + name.upper()] = enum_des\n    module[name] = enum_type_wrapper.EnumTypeWrapper(enum_des)\n    for enum_value in enum_des.values:\n      module[enum_value.name] = enum_value.number\n\n  # top level extensions\n  for (name, extension_des) in file_des.extensions_by_name.items():\n    module[name.upper() + '_FIELD_NUMBER'] = extension_des.number\n    module[name] = extension_des\n\n  # services\n  for (name, service) in file_des.services_by_name.items():\n    module['_' + name.upper()] = service\n\n  # Build messages.\n  for (name, msg_des) in file_des.message_types_by_name.items():\n    module[name] = BuildMessage(msg_des)\n\n\ndef BuildServices(file_des, module_name, module):\n  \"\"\"Builds services classes and services stub class.\n\n  Args:\n    file_des: FileDescriptor of the .proto file\n    module_name: str, the name of generated _pb2 module\n    module: Generated _pb2 module\n  \"\"\"\n  # pylint: disable=g-import-not-at-top\n  from google.protobuf import service as _service\n  from google.protobuf import service_reflection\n  # pylint: enable=g-import-not-at-top\n  for (name, service) in file_des.services_by_name.items():\n    module[name] = service_reflection.GeneratedServiceType(\n        name, (_service.Service,),\n        dict(DESCRIPTOR=service, __module__=module_name))\n    stub_name = name + '_Stub'\n    module[stub_name] = service_reflection.GeneratedServiceStubType(\n        stub_name, (module[name],),\n        dict(DESCRIPTOR=service, __module__=module_name))\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/containers.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Contains container classes to represent different protocol buffer types.\n\nThis file defines container classes which represent categories of protocol\nbuffer field types which need extra maintenance. Currently these categories\nare:\n\n-   Repeated scalar fields - These are all repeated fields which aren't\n    composite (e.g. they are of simple types like int32, string, etc).\n-   Repeated composite fields - Repeated fields which are composite. This\n    includes groups and nested messages.\n\"\"\"\n\nimport collections.abc\nimport copy\nimport pickle\nfrom typing import (\n    Any,\n    Iterable,\n    Iterator,\n    List,\n    MutableMapping,\n    MutableSequence,\n    NoReturn,\n    Optional,\n    Sequence,\n    TypeVar,\n    Union,\n    overload,\n)\n\n\n_T = TypeVar('_T')\n_K = TypeVar('_K')\n_V = TypeVar('_V')\n\n\nclass BaseContainer(Sequence[_T]):\n  \"\"\"Base container class.\"\"\"\n\n  # Minimizes memory usage and disallows assignment to other attributes.\n  __slots__ = ['_message_listener', '_values']\n\n  def __init__(self, message_listener: Any) -> None:\n    \"\"\"\n    Args:\n      message_listener: A MessageListener implementation.\n        The RepeatedScalarFieldContainer will call this object's\n        Modified() method when it is modified.\n    \"\"\"\n    self._message_listener = message_listener\n    self._values = []\n\n  @overload\n  def __getitem__(self, key: int) -> _T:\n    ...\n\n  @overload\n  def __getitem__(self, key: slice) -> List[_T]:\n    ...\n\n  def __getitem__(self, key):\n    \"\"\"Retrieves item by the specified key.\"\"\"\n    return self._values[key]\n\n  def __len__(self) -> int:\n    \"\"\"Returns the number of elements in the container.\"\"\"\n    return len(self._values)\n\n  def __ne__(self, other: Any) -> bool:\n    \"\"\"Checks if another instance isn't equal to this one.\"\"\"\n    # The concrete classes should define __eq__.\n    return not self == other\n\n  __hash__ = None\n\n  def __repr__(self) -> str:\n    return repr(self._values)\n\n  def sort(self, *args, **kwargs) -> None:\n    # Continue to support the old sort_function keyword argument.\n    # This is expected to be a rare occurrence, so use LBYL to avoid\n    # the overhead of actually catching KeyError.\n    if 'sort_function' in kwargs:\n      kwargs['cmp'] = kwargs.pop('sort_function')\n    self._values.sort(*args, **kwargs)\n\n  def reverse(self) -> None:\n    self._values.reverse()\n\n\n# TODO(slebedev): Remove this. BaseContainer does *not* conform to\n# MutableSequence, only its subclasses do.\ncollections.abc.MutableSequence.register(BaseContainer)\n\n\nclass RepeatedScalarFieldContainer(BaseContainer[_T], MutableSequence[_T]):\n  \"\"\"Simple, type-checked, list-like container for holding repeated scalars.\"\"\"\n\n  # Disallows assignment to other attributes.\n  __slots__ = ['_type_checker']\n\n  def __init__(\n      self,\n      message_listener: Any,\n      type_checker: Any,\n  ) -> None:\n    \"\"\"Args:\n\n      message_listener: A MessageListener implementation. The\n      RepeatedScalarFieldContainer will call this object's Modified() method\n      when it is modified.\n      type_checker: A type_checkers.ValueChecker instance to run on elements\n      inserted into this container.\n    \"\"\"\n    super().__init__(message_listener)\n    self._type_checker = type_checker\n\n  def append(self, value: _T) -> None:\n    \"\"\"Appends an item to the list. Similar to list.append().\"\"\"\n    self._values.append(self._type_checker.CheckValue(value))\n    if not self._message_listener.dirty:\n      self._message_listener.Modified()\n\n  def insert(self, key: int, value: _T) -> None:\n    \"\"\"Inserts the item at the specified position. Similar to list.insert().\"\"\"\n    self._values.insert(key, self._type_checker.CheckValue(value))\n    if not self._message_listener.dirty:\n      self._message_listener.Modified()\n\n  def extend(self, elem_seq: Iterable[_T]) -> None:\n    \"\"\"Extends by appending the given iterable. Similar to list.extend().\"\"\"\n    if elem_seq is None:\n      return\n    try:\n      elem_seq_iter = iter(elem_seq)\n    except TypeError:\n      if not elem_seq:\n        # silently ignore falsy inputs :-/.\n        # TODO(ptucker): Deprecate this behavior. b/18413862\n        return\n      raise\n\n    new_values = [self._type_checker.CheckValue(elem) for elem in elem_seq_iter]\n    if new_values:\n      self._values.extend(new_values)\n    self._message_listener.Modified()\n\n  def MergeFrom(\n      self,\n      other: Union['RepeatedScalarFieldContainer[_T]', Iterable[_T]],\n  ) -> None:\n    \"\"\"Appends the contents of another repeated field of the same type to this\n    one. We do not check the types of the individual fields.\n    \"\"\"\n    self._values.extend(other)\n    self._message_listener.Modified()\n\n  def remove(self, elem: _T):\n    \"\"\"Removes an item from the list. Similar to list.remove().\"\"\"\n    self._values.remove(elem)\n    self._message_listener.Modified()\n\n  def pop(self, key: Optional[int] = -1) -> _T:\n    \"\"\"Removes and returns an item at a given index. Similar to list.pop().\"\"\"\n    value = self._values[key]\n    self.__delitem__(key)\n    return value\n\n  @overload\n  def __setitem__(self, key: int, value: _T) -> None:\n    ...\n\n  @overload\n  def __setitem__(self, key: slice, value: Iterable[_T]) -> None:\n    ...\n\n  def __setitem__(self, key, value) -> None:\n    \"\"\"Sets the item on the specified position.\"\"\"\n    if isinstance(key, slice):\n      if key.step is not None:\n        raise ValueError('Extended slices not supported')\n      self._values[key] = map(self._type_checker.CheckValue, value)\n      self._message_listener.Modified()\n    else:\n      self._values[key] = self._type_checker.CheckValue(value)\n      self._message_listener.Modified()\n\n  def __delitem__(self, key: Union[int, slice]) -> None:\n    \"\"\"Deletes the item at the specified position.\"\"\"\n    del self._values[key]\n    self._message_listener.Modified()\n\n  def __eq__(self, other: Any) -> bool:\n    \"\"\"Compares the current instance with another one.\"\"\"\n    if self is other:\n      return True\n    # Special case for the same type which should be common and fast.\n    if isinstance(other, self.__class__):\n      return other._values == self._values\n    # We are presumably comparing against some other sequence type.\n    return other == self._values\n\n  def __deepcopy__(\n      self,\n      unused_memo: Any = None,\n  ) -> 'RepeatedScalarFieldContainer[_T]':\n    clone = RepeatedScalarFieldContainer(\n        copy.deepcopy(self._message_listener), self._type_checker)\n    clone.MergeFrom(self)\n    return clone\n\n  def __reduce__(self, **kwargs) -> NoReturn:\n    raise pickle.PickleError(\n        \"Can't pickle repeated scalar fields, convert to list first\")\n\n\n# TODO(slebedev): Constrain T to be a subtype of Message.\nclass RepeatedCompositeFieldContainer(BaseContainer[_T], MutableSequence[_T]):\n  \"\"\"Simple, list-like container for holding repeated composite fields.\"\"\"\n\n  # Disallows assignment to other attributes.\n  __slots__ = ['_message_descriptor']\n\n  def __init__(self, message_listener: Any, message_descriptor: Any) -> None:\n    \"\"\"\n    Note that we pass in a descriptor instead of the generated directly,\n    since at the time we construct a _RepeatedCompositeFieldContainer we\n    haven't yet necessarily initialized the type that will be contained in the\n    container.\n\n    Args:\n      message_listener: A MessageListener implementation.\n        The RepeatedCompositeFieldContainer will call this object's\n        Modified() method when it is modified.\n      message_descriptor: A Descriptor instance describing the protocol type\n        that should be present in this container.  We'll use the\n        _concrete_class field of this descriptor when the client calls add().\n    \"\"\"\n    super().__init__(message_listener)\n    self._message_descriptor = message_descriptor\n\n  def add(self, **kwargs: Any) -> _T:\n    \"\"\"Adds a new element at the end of the list and returns it. Keyword\n    arguments may be used to initialize the element.\n    \"\"\"\n    new_element = self._message_descriptor._concrete_class(**kwargs)\n    new_element._SetListener(self._message_listener)\n    self._values.append(new_element)\n    if not self._message_listener.dirty:\n      self._message_listener.Modified()\n    return new_element\n\n  def append(self, value: _T) -> None:\n    \"\"\"Appends one element by copying the message.\"\"\"\n    new_element = self._message_descriptor._concrete_class()\n    new_element._SetListener(self._message_listener)\n    new_element.CopyFrom(value)\n    self._values.append(new_element)\n    if not self._message_listener.dirty:\n      self._message_listener.Modified()\n\n  def insert(self, key: int, value: _T) -> None:\n    \"\"\"Inserts the item at the specified position by copying.\"\"\"\n    new_element = self._message_descriptor._concrete_class()\n    new_element._SetListener(self._message_listener)\n    new_element.CopyFrom(value)\n    self._values.insert(key, new_element)\n    if not self._message_listener.dirty:\n      self._message_listener.Modified()\n\n  def extend(self, elem_seq: Iterable[_T]) -> None:\n    \"\"\"Extends by appending the given sequence of elements of the same type\n\n    as this one, copying each individual message.\n    \"\"\"\n    message_class = self._message_descriptor._concrete_class\n    listener = self._message_listener\n    values = self._values\n    for message in elem_seq:\n      new_element = message_class()\n      new_element._SetListener(listener)\n      new_element.MergeFrom(message)\n      values.append(new_element)\n    listener.Modified()\n\n  def MergeFrom(\n      self,\n      other: Union['RepeatedCompositeFieldContainer[_T]', Iterable[_T]],\n  ) -> None:\n    \"\"\"Appends the contents of another repeated field of the same type to this\n    one, copying each individual message.\n    \"\"\"\n    self.extend(other)\n\n  def remove(self, elem: _T) -> None:\n    \"\"\"Removes an item from the list. Similar to list.remove().\"\"\"\n    self._values.remove(elem)\n    self._message_listener.Modified()\n\n  def pop(self, key: Optional[int] = -1) -> _T:\n    \"\"\"Removes and returns an item at a given index. Similar to list.pop().\"\"\"\n    value = self._values[key]\n    self.__delitem__(key)\n    return value\n\n  @overload\n  def __setitem__(self, key: int, value: _T) -> None:\n    ...\n\n  @overload\n  def __setitem__(self, key: slice, value: Iterable[_T]) -> None:\n    ...\n\n  def __setitem__(self, key, value):\n    # This method is implemented to make RepeatedCompositeFieldContainer\n    # structurally compatible with typing.MutableSequence. It is\n    # otherwise unsupported and will always raise an error.\n    raise TypeError(\n        f'{self.__class__.__name__} object does not support item assignment')\n\n  def __delitem__(self, key: Union[int, slice]) -> None:\n    \"\"\"Deletes the item at the specified position.\"\"\"\n    del self._values[key]\n    self._message_listener.Modified()\n\n  def __eq__(self, other: Any) -> bool:\n    \"\"\"Compares the current instance with another one.\"\"\"\n    if self is other:\n      return True\n    if not isinstance(other, self.__class__):\n      raise TypeError('Can only compare repeated composite fields against '\n                      'other repeated composite fields.')\n    return self._values == other._values\n\n\nclass ScalarMap(MutableMapping[_K, _V]):\n  \"\"\"Simple, type-checked, dict-like container for holding repeated scalars.\"\"\"\n\n  # Disallows assignment to other attributes.\n  __slots__ = ['_key_checker', '_value_checker', '_values', '_message_listener',\n               '_entry_descriptor']\n\n  def __init__(\n      self,\n      message_listener: Any,\n      key_checker: Any,\n      value_checker: Any,\n      entry_descriptor: Any,\n  ) -> None:\n    \"\"\"\n    Args:\n      message_listener: A MessageListener implementation.\n        The ScalarMap will call this object's Modified() method when it\n        is modified.\n      key_checker: A type_checkers.ValueChecker instance to run on keys\n        inserted into this container.\n      value_checker: A type_checkers.ValueChecker instance to run on values\n        inserted into this container.\n      entry_descriptor: The MessageDescriptor of a map entry: key and value.\n    \"\"\"\n    self._message_listener = message_listener\n    self._key_checker = key_checker\n    self._value_checker = value_checker\n    self._entry_descriptor = entry_descriptor\n    self._values = {}\n\n  def __getitem__(self, key: _K) -> _V:\n    try:\n      return self._values[key]\n    except KeyError:\n      key = self._key_checker.CheckValue(key)\n      val = self._value_checker.DefaultValue()\n      self._values[key] = val\n      return val\n\n  def __contains__(self, item: _K) -> bool:\n    # We check the key's type to match the strong-typing flavor of the API.\n    # Also this makes it easier to match the behavior of the C++ implementation.\n    self._key_checker.CheckValue(item)\n    return item in self._values\n\n  @overload\n  def get(self, key: _K) -> Optional[_V]:\n    ...\n\n  @overload\n  def get(self, key: _K, default: _T) -> Union[_V, _T]:\n    ...\n\n  # We need to override this explicitly, because our defaultdict-like behavior\n  # will make the default implementation (from our base class) always insert\n  # the key.\n  def get(self, key, default=None):\n    if key in self:\n      return self[key]\n    else:\n      return default\n\n  def __setitem__(self, key: _K, value: _V) -> _T:\n    checked_key = self._key_checker.CheckValue(key)\n    checked_value = self._value_checker.CheckValue(value)\n    self._values[checked_key] = checked_value\n    self._message_listener.Modified()\n\n  def __delitem__(self, key: _K) -> None:\n    del self._values[key]\n    self._message_listener.Modified()\n\n  def __len__(self) -> int:\n    return len(self._values)\n\n  def __iter__(self) -> Iterator[_K]:\n    return iter(self._values)\n\n  def __repr__(self) -> str:\n    return repr(self._values)\n\n  def MergeFrom(self, other: 'ScalarMap[_K, _V]') -> None:\n    self._values.update(other._values)\n    self._message_listener.Modified()\n\n  def InvalidateIterators(self) -> None:\n    # It appears that the only way to reliably invalidate iterators to\n    # self._values is to ensure that its size changes.\n    original = self._values\n    self._values = original.copy()\n    original[None] = None\n\n  # This is defined in the abstract base, but we can do it much more cheaply.\n  def clear(self) -> None:\n    self._values.clear()\n    self._message_listener.Modified()\n\n  def GetEntryClass(self) -> Any:\n    return self._entry_descriptor._concrete_class\n\n\nclass MessageMap(MutableMapping[_K, _V]):\n  \"\"\"Simple, type-checked, dict-like container for with submessage values.\"\"\"\n\n  # Disallows assignment to other attributes.\n  __slots__ = ['_key_checker', '_values', '_message_listener',\n               '_message_descriptor', '_entry_descriptor']\n\n  def __init__(\n      self,\n      message_listener: Any,\n      message_descriptor: Any,\n      key_checker: Any,\n      entry_descriptor: Any,\n  ) -> None:\n    \"\"\"\n    Args:\n      message_listener: A MessageListener implementation.\n        The ScalarMap will call this object's Modified() method when it\n        is modified.\n      key_checker: A type_checkers.ValueChecker instance to run on keys\n        inserted into this container.\n      value_checker: A type_checkers.ValueChecker instance to run on values\n        inserted into this container.\n      entry_descriptor: The MessageDescriptor of a map entry: key and value.\n    \"\"\"\n    self._message_listener = message_listener\n    self._message_descriptor = message_descriptor\n    self._key_checker = key_checker\n    self._entry_descriptor = entry_descriptor\n    self._values = {}\n\n  def __getitem__(self, key: _K) -> _V:\n    key = self._key_checker.CheckValue(key)\n    try:\n      return self._values[key]\n    except KeyError:\n      new_element = self._message_descriptor._concrete_class()\n      new_element._SetListener(self._message_listener)\n      self._values[key] = new_element\n      self._message_listener.Modified()\n      return new_element\n\n  def get_or_create(self, key: _K) -> _V:\n    \"\"\"get_or_create() is an alias for getitem (ie. map[key]).\n\n    Args:\n      key: The key to get or create in the map.\n\n    This is useful in cases where you want to be explicit that the call is\n    mutating the map.  This can avoid lint errors for statements like this\n    that otherwise would appear to be pointless statements:\n\n      msg.my_map[key]\n    \"\"\"\n    return self[key]\n\n  @overload\n  def get(self, key: _K) -> Optional[_V]:\n    ...\n\n  @overload\n  def get(self, key: _K, default: _T) -> Union[_V, _T]:\n    ...\n\n  # We need to override this explicitly, because our defaultdict-like behavior\n  # will make the default implementation (from our base class) always insert\n  # the key.\n  def get(self, key, default=None):\n    if key in self:\n      return self[key]\n    else:\n      return default\n\n  def __contains__(self, item: _K) -> bool:\n    item = self._key_checker.CheckValue(item)\n    return item in self._values\n\n  def __setitem__(self, key: _K, value: _V) -> NoReturn:\n    raise ValueError('May not set values directly, call my_map[key].foo = 5')\n\n  def __delitem__(self, key: _K) -> None:\n    key = self._key_checker.CheckValue(key)\n    del self._values[key]\n    self._message_listener.Modified()\n\n  def __len__(self) -> int:\n    return len(self._values)\n\n  def __iter__(self) -> Iterator[_K]:\n    return iter(self._values)\n\n  def __repr__(self) -> str:\n    return repr(self._values)\n\n  def MergeFrom(self, other: 'MessageMap[_K, _V]') -> None:\n    # pylint: disable=protected-access\n    for key in other._values:\n      # According to documentation: \"When parsing from the wire or when merging,\n      # if there are duplicate map keys the last key seen is used\".\n      if key in self:\n        del self[key]\n      self[key].CopyFrom(other[key])\n    # self._message_listener.Modified() not required here, because\n    # mutations to submessages already propagate.\n\n  def InvalidateIterators(self) -> None:\n    # It appears that the only way to reliably invalidate iterators to\n    # self._values is to ensure that its size changes.\n    original = self._values\n    self._values = original.copy()\n    original[None] = None\n\n  # This is defined in the abstract base, but we can do it much more cheaply.\n  def clear(self) -> None:\n    self._values.clear()\n    self._message_listener.Modified()\n\n  def GetEntryClass(self) -> Any:\n    return self._entry_descriptor._concrete_class\n\n\nclass _UnknownField:\n  \"\"\"A parsed unknown field.\"\"\"\n\n  # Disallows assignment to other attributes.\n  __slots__ = ['_field_number', '_wire_type', '_data']\n\n  def __init__(self, field_number, wire_type, data):\n    self._field_number = field_number\n    self._wire_type = wire_type\n    self._data = data\n    return\n\n  def __lt__(self, other):\n    # pylint: disable=protected-access\n    return self._field_number < other._field_number\n\n  def __eq__(self, other):\n    if self is other:\n      return True\n    # pylint: disable=protected-access\n    return (self._field_number == other._field_number and\n            self._wire_type == other._wire_type and\n            self._data == other._data)\n\n\nclass UnknownFieldRef:  # pylint: disable=missing-class-docstring\n\n  def __init__(self, parent, index):\n    self._parent = parent\n    self._index = index\n\n  def _check_valid(self):\n    if not self._parent:\n      raise ValueError('UnknownField does not exist. '\n                       'The parent message might be cleared.')\n    if self._index >= len(self._parent):\n      raise ValueError('UnknownField does not exist. '\n                       'The parent message might be cleared.')\n\n  @property\n  def field_number(self):\n    self._check_valid()\n    # pylint: disable=protected-access\n    return self._parent._internal_get(self._index)._field_number\n\n  @property\n  def wire_type(self):\n    self._check_valid()\n    # pylint: disable=protected-access\n    return self._parent._internal_get(self._index)._wire_type\n\n  @property\n  def data(self):\n    self._check_valid()\n    # pylint: disable=protected-access\n    return self._parent._internal_get(self._index)._data\n\n\nclass UnknownFieldSet:\n  \"\"\"UnknownField container\"\"\"\n\n  # Disallows assignment to other attributes.\n  __slots__ = ['_values']\n\n  def __init__(self):\n    self._values = []\n\n  def __getitem__(self, index):\n    if self._values is None:\n      raise ValueError('UnknownFields does not exist. '\n                       'The parent message might be cleared.')\n    size = len(self._values)\n    if index < 0:\n      index += size\n    if index < 0 or index >= size:\n      raise IndexError('index %d out of range'.index)\n\n    return UnknownFieldRef(self, index)\n\n  def _internal_get(self, index):\n    return self._values[index]\n\n  def __len__(self):\n    if self._values is None:\n      raise ValueError('UnknownFields does not exist. '\n                       'The parent message might be cleared.')\n    return len(self._values)\n\n  def _add(self, field_number, wire_type, data):\n    unknown_field = _UnknownField(field_number, wire_type, data)\n    self._values.append(unknown_field)\n    return unknown_field\n\n  def __iter__(self):\n    for i in range(len(self)):\n      yield UnknownFieldRef(self, i)\n\n  def _extend(self, other):\n    if other is None:\n      return\n    # pylint: disable=protected-access\n    self._values.extend(other._values)\n\n  def __eq__(self, other):\n    if self is other:\n      return True\n    # Sort unknown fields because their order shouldn't\n    # affect equality test.\n    values = list(self._values)\n    if other is None:\n      return not values\n    values.sort()\n    # pylint: disable=protected-access\n    other_values = sorted(other._values)\n    return values == other_values\n\n  def _clear(self):\n    for value in self._values:\n      # pylint: disable=protected-access\n      if isinstance(value._data, UnknownFieldSet):\n        value._data._clear()  # pylint: disable=protected-access\n    self._values = None\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/decoder.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Code for decoding protocol buffer primitives.\n\nThis code is very similar to encoder.py -- read the docs for that module first.\n\nA \"decoder\" is a function with the signature:\n  Decode(buffer, pos, end, message, field_dict)\nThe arguments are:\n  buffer:     The string containing the encoded message.\n  pos:        The current position in the string.\n  end:        The position in the string where the current message ends.  May be\n              less than len(buffer) if we're reading a sub-message.\n  message:    The message object into which we're parsing.\n  field_dict: message._fields (avoids a hashtable lookup).\nThe decoder reads the field and stores it into field_dict, returning the new\nbuffer position.  A decoder for a repeated field may proactively decode all of\nthe elements of that field, if they appear consecutively.\n\nNote that decoders may throw any of the following:\n  IndexError:  Indicates a truncated message.\n  struct.error:  Unpacking of a fixed-width field failed.\n  message.DecodeError:  Other errors.\n\nDecoders are expected to raise an exception if they are called with pos > end.\nThis allows callers to be lax about bounds checking:  it's fineto read past\n\"end\" as long as you are sure that someone else will notice and throw an\nexception later on.\n\nSomething up the call stack is expected to catch IndexError and struct.error\nand convert them to message.DecodeError.\n\nDecoders are constructed using decoder constructors with the signature:\n  MakeDecoder(field_number, is_repeated, is_packed, key, new_default)\nThe arguments are:\n  field_number:  The field number of the field we want to decode.\n  is_repeated:   Is the field a repeated field? (bool)\n  is_packed:     Is the field a packed field? (bool)\n  key:           The key to use when looking up the field within field_dict.\n                 (This is actually the FieldDescriptor but nothing in this\n                 file should depend on that.)\n  new_default:   A function which takes a message object as a parameter and\n                 returns a new instance of the default value for this field.\n                 (This is called for repeated fields and sub-messages, when an\n                 instance does not already exist.)\n\nAs with encoders, we define a decoder constructor for every type of field.\nThen, for every field of every message class we construct an actual decoder.\nThat decoder goes into a dict indexed by tag, so when we decode a message\nwe repeatedly read a tag, look up the corresponding decoder, and invoke it.\n\"\"\"\n\n__author__ = 'kenton@google.com (Kenton Varda)'\n\nimport math\nimport struct\n\nfrom google.protobuf.internal import containers\nfrom google.protobuf.internal import encoder\nfrom google.protobuf.internal import wire_format\nfrom google.protobuf import message\n\n\n# This is not for optimization, but rather to avoid conflicts with local\n# variables named \"message\".\n_DecodeError = message.DecodeError\n\n\ndef _VarintDecoder(mask, result_type):\n  \"\"\"Return an encoder for a basic varint value (does not include tag).\n\n  Decoded values will be bitwise-anded with the given mask before being\n  returned, e.g. to limit them to 32 bits.  The returned decoder does not\n  take the usual \"end\" parameter -- the caller is expected to do bounds checking\n  after the fact (often the caller can defer such checking until later).  The\n  decoder returns a (value, new_pos) pair.\n  \"\"\"\n\n  def DecodeVarint(buffer, pos):\n    result = 0\n    shift = 0\n    while 1:\n      b = buffer[pos]\n      result |= ((b & 0x7f) << shift)\n      pos += 1\n      if not (b & 0x80):\n        result &= mask\n        result = result_type(result)\n        return (result, pos)\n      shift += 7\n      if shift >= 64:\n        raise _DecodeError('Too many bytes when decoding varint.')\n  return DecodeVarint\n\n\ndef _SignedVarintDecoder(bits, result_type):\n  \"\"\"Like _VarintDecoder() but decodes signed values.\"\"\"\n\n  signbit = 1 << (bits - 1)\n  mask = (1 << bits) - 1\n\n  def DecodeVarint(buffer, pos):\n    result = 0\n    shift = 0\n    while 1:\n      b = buffer[pos]\n      result |= ((b & 0x7f) << shift)\n      pos += 1\n      if not (b & 0x80):\n        result &= mask\n        result = (result ^ signbit) - signbit\n        result = result_type(result)\n        return (result, pos)\n      shift += 7\n      if shift >= 64:\n        raise _DecodeError('Too many bytes when decoding varint.')\n  return DecodeVarint\n\n# All 32-bit and 64-bit values are represented as int.\n_DecodeVarint = _VarintDecoder((1 << 64) - 1, int)\n_DecodeSignedVarint = _SignedVarintDecoder(64, int)\n\n# Use these versions for values which must be limited to 32 bits.\n_DecodeVarint32 = _VarintDecoder((1 << 32) - 1, int)\n_DecodeSignedVarint32 = _SignedVarintDecoder(32, int)\n\n\ndef ReadTag(buffer, pos):\n  \"\"\"Read a tag from the memoryview, and return a (tag_bytes, new_pos) tuple.\n\n  We return the raw bytes of the tag rather than decoding them.  The raw\n  bytes can then be used to look up the proper decoder.  This effectively allows\n  us to trade some work that would be done in pure-python (decoding a varint)\n  for work that is done in C (searching for a byte string in a hash table).\n  In a low-level language it would be much cheaper to decode the varint and\n  use that, but not in Python.\n\n  Args:\n    buffer: memoryview object of the encoded bytes\n    pos: int of the current position to start from\n\n  Returns:\n    Tuple[bytes, int] of the tag data and new position.\n  \"\"\"\n  start = pos\n  while buffer[pos] & 0x80:\n    pos += 1\n  pos += 1\n\n  tag_bytes = buffer[start:pos].tobytes()\n  return tag_bytes, pos\n\n\n# --------------------------------------------------------------------\n\n\ndef _SimpleDecoder(wire_type, decode_value):\n  \"\"\"Return a constructor for a decoder for fields of a particular type.\n\n  Args:\n      wire_type:  The field's wire type.\n      decode_value:  A function which decodes an individual value, e.g.\n        _DecodeVarint()\n  \"\"\"\n\n  def SpecificDecoder(field_number, is_repeated, is_packed, key, new_default,\n                      clear_if_default=False):\n    if is_packed:\n      local_DecodeVarint = _DecodeVarint\n      def DecodePackedField(buffer, pos, end, message, field_dict):\n        value = field_dict.get(key)\n        if value is None:\n          value = field_dict.setdefault(key, new_default(message))\n        (endpoint, pos) = local_DecodeVarint(buffer, pos)\n        endpoint += pos\n        if endpoint > end:\n          raise _DecodeError('Truncated message.')\n        while pos < endpoint:\n          (element, pos) = decode_value(buffer, pos)\n          value.append(element)\n        if pos > endpoint:\n          del value[-1]   # Discard corrupt value.\n          raise _DecodeError('Packed element was truncated.')\n        return pos\n      return DecodePackedField\n    elif is_repeated:\n      tag_bytes = encoder.TagBytes(field_number, wire_type)\n      tag_len = len(tag_bytes)\n      def DecodeRepeatedField(buffer, pos, end, message, field_dict):\n        value = field_dict.get(key)\n        if value is None:\n          value = field_dict.setdefault(key, new_default(message))\n        while 1:\n          (element, new_pos) = decode_value(buffer, pos)\n          value.append(element)\n          # Predict that the next tag is another copy of the same repeated\n          # field.\n          pos = new_pos + tag_len\n          if buffer[new_pos:pos] != tag_bytes or new_pos >= end:\n            # Prediction failed.  Return.\n            if new_pos > end:\n              raise _DecodeError('Truncated message.')\n            return new_pos\n      return DecodeRepeatedField\n    else:\n      def DecodeField(buffer, pos, end, message, field_dict):\n        (new_value, pos) = decode_value(buffer, pos)\n        if pos > end:\n          raise _DecodeError('Truncated message.')\n        if clear_if_default and not new_value:\n          field_dict.pop(key, None)\n        else:\n          field_dict[key] = new_value\n        return pos\n      return DecodeField\n\n  return SpecificDecoder\n\n\ndef _ModifiedDecoder(wire_type, decode_value, modify_value):\n  \"\"\"Like SimpleDecoder but additionally invokes modify_value on every value\n  before storing it.  Usually modify_value is ZigZagDecode.\n  \"\"\"\n\n  # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but\n  # not enough to make a significant difference.\n\n  def InnerDecode(buffer, pos):\n    (result, new_pos) = decode_value(buffer, pos)\n    return (modify_value(result), new_pos)\n  return _SimpleDecoder(wire_type, InnerDecode)\n\n\ndef _StructPackDecoder(wire_type, format):\n  \"\"\"Return a constructor for a decoder for a fixed-width field.\n\n  Args:\n      wire_type:  The field's wire type.\n      format:  The format string to pass to struct.unpack().\n  \"\"\"\n\n  value_size = struct.calcsize(format)\n  local_unpack = struct.unpack\n\n  # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but\n  # not enough to make a significant difference.\n\n  # Note that we expect someone up-stack to catch struct.error and convert\n  # it to _DecodeError -- this way we don't have to set up exception-\n  # handling blocks every time we parse one value.\n\n  def InnerDecode(buffer, pos):\n    new_pos = pos + value_size\n    result = local_unpack(format, buffer[pos:new_pos])[0]\n    return (result, new_pos)\n  return _SimpleDecoder(wire_type, InnerDecode)\n\n\ndef _FloatDecoder():\n  \"\"\"Returns a decoder for a float field.\n\n  This code works around a bug in struct.unpack for non-finite 32-bit\n  floating-point values.\n  \"\"\"\n\n  local_unpack = struct.unpack\n\n  def InnerDecode(buffer, pos):\n    \"\"\"Decode serialized float to a float and new position.\n\n    Args:\n      buffer: memoryview of the serialized bytes\n      pos: int, position in the memory view to start at.\n\n    Returns:\n      Tuple[float, int] of the deserialized float value and new position\n      in the serialized data.\n    \"\"\"\n    # We expect a 32-bit value in little-endian byte order.  Bit 1 is the sign\n    # bit, bits 2-9 represent the exponent, and bits 10-32 are the significand.\n    new_pos = pos + 4\n    float_bytes = buffer[pos:new_pos].tobytes()\n\n    # If this value has all its exponent bits set, then it's non-finite.\n    # In Python 2.4, struct.unpack will convert it to a finite 64-bit value.\n    # To avoid that, we parse it specially.\n    if (float_bytes[3:4] in b'\\x7F\\xFF' and float_bytes[2:3] >= b'\\x80'):\n      # If at least one significand bit is set...\n      if float_bytes[0:3] != b'\\x00\\x00\\x80':\n        return (math.nan, new_pos)\n      # If sign bit is set...\n      if float_bytes[3:4] == b'\\xFF':\n        return (-math.inf, new_pos)\n      return (math.inf, new_pos)\n\n    # Note that we expect someone up-stack to catch struct.error and convert\n    # it to _DecodeError -- this way we don't have to set up exception-\n    # handling blocks every time we parse one value.\n    result = local_unpack('<f', float_bytes)[0]\n    return (result, new_pos)\n  return _SimpleDecoder(wire_format.WIRETYPE_FIXED32, InnerDecode)\n\n\ndef _DoubleDecoder():\n  \"\"\"Returns a decoder for a double field.\n\n  This code works around a bug in struct.unpack for not-a-number.\n  \"\"\"\n\n  local_unpack = struct.unpack\n\n  def InnerDecode(buffer, pos):\n    \"\"\"Decode serialized double to a double and new position.\n\n    Args:\n      buffer: memoryview of the serialized bytes.\n      pos: int, position in the memory view to start at.\n\n    Returns:\n      Tuple[float, int] of the decoded double value and new position\n      in the serialized data.\n    \"\"\"\n    # We expect a 64-bit value in little-endian byte order.  Bit 1 is the sign\n    # bit, bits 2-12 represent the exponent, and bits 13-64 are the significand.\n    new_pos = pos + 8\n    double_bytes = buffer[pos:new_pos].tobytes()\n\n    # If this value has all its exponent bits set and at least one significand\n    # bit set, it's not a number.  In Python 2.4, struct.unpack will treat it\n    # as inf or -inf.  To avoid that, we treat it specially.\n    if ((double_bytes[7:8] in b'\\x7F\\xFF')\n        and (double_bytes[6:7] >= b'\\xF0')\n        and (double_bytes[0:7] != b'\\x00\\x00\\x00\\x00\\x00\\x00\\xF0')):\n      return (math.nan, new_pos)\n\n    # Note that we expect someone up-stack to catch struct.error and convert\n    # it to _DecodeError -- this way we don't have to set up exception-\n    # handling blocks every time we parse one value.\n    result = local_unpack('<d', double_bytes)[0]\n    return (result, new_pos)\n  return _SimpleDecoder(wire_format.WIRETYPE_FIXED64, InnerDecode)\n\n\ndef EnumDecoder(field_number, is_repeated, is_packed, key, new_default,\n                clear_if_default=False):\n  \"\"\"Returns a decoder for enum field.\"\"\"\n  enum_type = key.enum_type\n  if is_packed:\n    local_DecodeVarint = _DecodeVarint\n    def DecodePackedField(buffer, pos, end, message, field_dict):\n      \"\"\"Decode serialized packed enum to its value and a new position.\n\n      Args:\n        buffer: memoryview of the serialized bytes.\n        pos: int, position in the memory view to start at.\n        end: int, end position of serialized data\n        message: Message object to store unknown fields in\n        field_dict: Map[Descriptor, Any] to store decoded values in.\n\n      Returns:\n        int, new position in serialized data.\n      \"\"\"\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      (endpoint, pos) = local_DecodeVarint(buffer, pos)\n      endpoint += pos\n      if endpoint > end:\n        raise _DecodeError('Truncated message.')\n      while pos < endpoint:\n        value_start_pos = pos\n        (element, pos) = _DecodeSignedVarint32(buffer, pos)\n        # pylint: disable=protected-access\n        if element in enum_type.values_by_number:\n          value.append(element)\n        else:\n          if not message._unknown_fields:\n            message._unknown_fields = []\n          tag_bytes = encoder.TagBytes(field_number,\n                                       wire_format.WIRETYPE_VARINT)\n\n          message._unknown_fields.append(\n              (tag_bytes, buffer[value_start_pos:pos].tobytes()))\n          if message._unknown_field_set is None:\n            message._unknown_field_set = containers.UnknownFieldSet()\n          message._unknown_field_set._add(\n              field_number, wire_format.WIRETYPE_VARINT, element)\n          # pylint: enable=protected-access\n      if pos > endpoint:\n        if element in enum_type.values_by_number:\n          del value[-1]   # Discard corrupt value.\n        else:\n          del message._unknown_fields[-1]\n          # pylint: disable=protected-access\n          del message._unknown_field_set._values[-1]\n          # pylint: enable=protected-access\n        raise _DecodeError('Packed element was truncated.')\n      return pos\n    return DecodePackedField\n  elif is_repeated:\n    tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_VARINT)\n    tag_len = len(tag_bytes)\n    def DecodeRepeatedField(buffer, pos, end, message, field_dict):\n      \"\"\"Decode serialized repeated enum to its value and a new position.\n\n      Args:\n        buffer: memoryview of the serialized bytes.\n        pos: int, position in the memory view to start at.\n        end: int, end position of serialized data\n        message: Message object to store unknown fields in\n        field_dict: Map[Descriptor, Any] to store decoded values in.\n\n      Returns:\n        int, new position in serialized data.\n      \"\"\"\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      while 1:\n        (element, new_pos) = _DecodeSignedVarint32(buffer, pos)\n        # pylint: disable=protected-access\n        if element in enum_type.values_by_number:\n          value.append(element)\n        else:\n          if not message._unknown_fields:\n            message._unknown_fields = []\n          message._unknown_fields.append(\n              (tag_bytes, buffer[pos:new_pos].tobytes()))\n          if message._unknown_field_set is None:\n            message._unknown_field_set = containers.UnknownFieldSet()\n          message._unknown_field_set._add(\n              field_number, wire_format.WIRETYPE_VARINT, element)\n        # pylint: enable=protected-access\n        # Predict that the next tag is another copy of the same repeated\n        # field.\n        pos = new_pos + tag_len\n        if buffer[new_pos:pos] != tag_bytes or new_pos >= end:\n          # Prediction failed.  Return.\n          if new_pos > end:\n            raise _DecodeError('Truncated message.')\n          return new_pos\n    return DecodeRepeatedField\n  else:\n    def DecodeField(buffer, pos, end, message, field_dict):\n      \"\"\"Decode serialized repeated enum to its value and a new position.\n\n      Args:\n        buffer: memoryview of the serialized bytes.\n        pos: int, position in the memory view to start at.\n        end: int, end position of serialized data\n        message: Message object to store unknown fields in\n        field_dict: Map[Descriptor, Any] to store decoded values in.\n\n      Returns:\n        int, new position in serialized data.\n      \"\"\"\n      value_start_pos = pos\n      (enum_value, pos) = _DecodeSignedVarint32(buffer, pos)\n      if pos > end:\n        raise _DecodeError('Truncated message.')\n      if clear_if_default and not enum_value:\n        field_dict.pop(key, None)\n        return pos\n      # pylint: disable=protected-access\n      if enum_value in enum_type.values_by_number:\n        field_dict[key] = enum_value\n      else:\n        if not message._unknown_fields:\n          message._unknown_fields = []\n        tag_bytes = encoder.TagBytes(field_number,\n                                     wire_format.WIRETYPE_VARINT)\n        message._unknown_fields.append(\n            (tag_bytes, buffer[value_start_pos:pos].tobytes()))\n        if message._unknown_field_set is None:\n          message._unknown_field_set = containers.UnknownFieldSet()\n        message._unknown_field_set._add(\n            field_number, wire_format.WIRETYPE_VARINT, enum_value)\n        # pylint: enable=protected-access\n      return pos\n    return DecodeField\n\n\n# --------------------------------------------------------------------\n\n\nInt32Decoder = _SimpleDecoder(\n    wire_format.WIRETYPE_VARINT, _DecodeSignedVarint32)\n\nInt64Decoder = _SimpleDecoder(\n    wire_format.WIRETYPE_VARINT, _DecodeSignedVarint)\n\nUInt32Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint32)\nUInt64Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint)\n\nSInt32Decoder = _ModifiedDecoder(\n    wire_format.WIRETYPE_VARINT, _DecodeVarint32, wire_format.ZigZagDecode)\nSInt64Decoder = _ModifiedDecoder(\n    wire_format.WIRETYPE_VARINT, _DecodeVarint, wire_format.ZigZagDecode)\n\n# Note that Python conveniently guarantees that when using the '<' prefix on\n# formats, they will also have the same size across all platforms (as opposed\n# to without the prefix, where their sizes depend on the C compiler's basic\n# type sizes).\nFixed32Decoder  = _StructPackDecoder(wire_format.WIRETYPE_FIXED32, '<I')\nFixed64Decoder  = _StructPackDecoder(wire_format.WIRETYPE_FIXED64, '<Q')\nSFixed32Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED32, '<i')\nSFixed64Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED64, '<q')\nFloatDecoder = _FloatDecoder()\nDoubleDecoder = _DoubleDecoder()\n\nBoolDecoder = _ModifiedDecoder(\n    wire_format.WIRETYPE_VARINT, _DecodeVarint, bool)\n\n\ndef StringDecoder(field_number, is_repeated, is_packed, key, new_default,\n                  clear_if_default=False):\n  \"\"\"Returns a decoder for a string field.\"\"\"\n\n  local_DecodeVarint = _DecodeVarint\n\n  def _ConvertToUnicode(memview):\n    \"\"\"Convert byte to unicode.\"\"\"\n    byte_str = memview.tobytes()\n    try:\n      value = str(byte_str, 'utf-8')\n    except UnicodeDecodeError as e:\n      # add more information to the error message and re-raise it.\n      e.reason = '%s in field: %s' % (e, key.full_name)\n      raise\n\n    return value\n\n  assert not is_packed\n  if is_repeated:\n    tag_bytes = encoder.TagBytes(field_number,\n                                 wire_format.WIRETYPE_LENGTH_DELIMITED)\n    tag_len = len(tag_bytes)\n    def DecodeRepeatedField(buffer, pos, end, message, field_dict):\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      while 1:\n        (size, pos) = local_DecodeVarint(buffer, pos)\n        new_pos = pos + size\n        if new_pos > end:\n          raise _DecodeError('Truncated string.')\n        value.append(_ConvertToUnicode(buffer[pos:new_pos]))\n        # Predict that the next tag is another copy of the same repeated field.\n        pos = new_pos + tag_len\n        if buffer[new_pos:pos] != tag_bytes or new_pos == end:\n          # Prediction failed.  Return.\n          return new_pos\n    return DecodeRepeatedField\n  else:\n    def DecodeField(buffer, pos, end, message, field_dict):\n      (size, pos) = local_DecodeVarint(buffer, pos)\n      new_pos = pos + size\n      if new_pos > end:\n        raise _DecodeError('Truncated string.')\n      if clear_if_default and not size:\n        field_dict.pop(key, None)\n      else:\n        field_dict[key] = _ConvertToUnicode(buffer[pos:new_pos])\n      return new_pos\n    return DecodeField\n\n\ndef BytesDecoder(field_number, is_repeated, is_packed, key, new_default,\n                 clear_if_default=False):\n  \"\"\"Returns a decoder for a bytes field.\"\"\"\n\n  local_DecodeVarint = _DecodeVarint\n\n  assert not is_packed\n  if is_repeated:\n    tag_bytes = encoder.TagBytes(field_number,\n                                 wire_format.WIRETYPE_LENGTH_DELIMITED)\n    tag_len = len(tag_bytes)\n    def DecodeRepeatedField(buffer, pos, end, message, field_dict):\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      while 1:\n        (size, pos) = local_DecodeVarint(buffer, pos)\n        new_pos = pos + size\n        if new_pos > end:\n          raise _DecodeError('Truncated string.')\n        value.append(buffer[pos:new_pos].tobytes())\n        # Predict that the next tag is another copy of the same repeated field.\n        pos = new_pos + tag_len\n        if buffer[new_pos:pos] != tag_bytes or new_pos == end:\n          # Prediction failed.  Return.\n          return new_pos\n    return DecodeRepeatedField\n  else:\n    def DecodeField(buffer, pos, end, message, field_dict):\n      (size, pos) = local_DecodeVarint(buffer, pos)\n      new_pos = pos + size\n      if new_pos > end:\n        raise _DecodeError('Truncated string.')\n      if clear_if_default and not size:\n        field_dict.pop(key, None)\n      else:\n        field_dict[key] = buffer[pos:new_pos].tobytes()\n      return new_pos\n    return DecodeField\n\n\ndef GroupDecoder(field_number, is_repeated, is_packed, key, new_default):\n  \"\"\"Returns a decoder for a group field.\"\"\"\n\n  end_tag_bytes = encoder.TagBytes(field_number,\n                                   wire_format.WIRETYPE_END_GROUP)\n  end_tag_len = len(end_tag_bytes)\n\n  assert not is_packed\n  if is_repeated:\n    tag_bytes = encoder.TagBytes(field_number,\n                                 wire_format.WIRETYPE_START_GROUP)\n    tag_len = len(tag_bytes)\n    def DecodeRepeatedField(buffer, pos, end, message, field_dict):\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      while 1:\n        value = field_dict.get(key)\n        if value is None:\n          value = field_dict.setdefault(key, new_default(message))\n        # Read sub-message.\n        pos = value.add()._InternalParse(buffer, pos, end)\n        # Read end tag.\n        new_pos = pos+end_tag_len\n        if buffer[pos:new_pos] != end_tag_bytes or new_pos > end:\n          raise _DecodeError('Missing group end tag.')\n        # Predict that the next tag is another copy of the same repeated field.\n        pos = new_pos + tag_len\n        if buffer[new_pos:pos] != tag_bytes or new_pos == end:\n          # Prediction failed.  Return.\n          return new_pos\n    return DecodeRepeatedField\n  else:\n    def DecodeField(buffer, pos, end, message, field_dict):\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      # Read sub-message.\n      pos = value._InternalParse(buffer, pos, end)\n      # Read end tag.\n      new_pos = pos+end_tag_len\n      if buffer[pos:new_pos] != end_tag_bytes or new_pos > end:\n        raise _DecodeError('Missing group end tag.')\n      return new_pos\n    return DecodeField\n\n\ndef MessageDecoder(field_number, is_repeated, is_packed, key, new_default):\n  \"\"\"Returns a decoder for a message field.\"\"\"\n\n  local_DecodeVarint = _DecodeVarint\n\n  assert not is_packed\n  if is_repeated:\n    tag_bytes = encoder.TagBytes(field_number,\n                                 wire_format.WIRETYPE_LENGTH_DELIMITED)\n    tag_len = len(tag_bytes)\n    def DecodeRepeatedField(buffer, pos, end, message, field_dict):\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      while 1:\n        # Read length.\n        (size, pos) = local_DecodeVarint(buffer, pos)\n        new_pos = pos + size\n        if new_pos > end:\n          raise _DecodeError('Truncated message.')\n        # Read sub-message.\n        if value.add()._InternalParse(buffer, pos, new_pos) != new_pos:\n          # The only reason _InternalParse would return early is if it\n          # encountered an end-group tag.\n          raise _DecodeError('Unexpected end-group tag.')\n        # Predict that the next tag is another copy of the same repeated field.\n        pos = new_pos + tag_len\n        if buffer[new_pos:pos] != tag_bytes or new_pos == end:\n          # Prediction failed.  Return.\n          return new_pos\n    return DecodeRepeatedField\n  else:\n    def DecodeField(buffer, pos, end, message, field_dict):\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      # Read length.\n      (size, pos) = local_DecodeVarint(buffer, pos)\n      new_pos = pos + size\n      if new_pos > end:\n        raise _DecodeError('Truncated message.')\n      # Read sub-message.\n      if value._InternalParse(buffer, pos, new_pos) != new_pos:\n        # The only reason _InternalParse would return early is if it encountered\n        # an end-group tag.\n        raise _DecodeError('Unexpected end-group tag.')\n      return new_pos\n    return DecodeField\n\n\n# --------------------------------------------------------------------\n\nMESSAGE_SET_ITEM_TAG = encoder.TagBytes(1, wire_format.WIRETYPE_START_GROUP)\n\ndef MessageSetItemDecoder(descriptor):\n  \"\"\"Returns a decoder for a MessageSet item.\n\n  The parameter is the message Descriptor.\n\n  The message set message looks like this:\n    message MessageSet {\n      repeated group Item = 1 {\n        required int32 type_id = 2;\n        required string message = 3;\n      }\n    }\n  \"\"\"\n\n  type_id_tag_bytes = encoder.TagBytes(2, wire_format.WIRETYPE_VARINT)\n  message_tag_bytes = encoder.TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED)\n  item_end_tag_bytes = encoder.TagBytes(1, wire_format.WIRETYPE_END_GROUP)\n\n  local_ReadTag = ReadTag\n  local_DecodeVarint = _DecodeVarint\n  local_SkipField = SkipField\n\n  def DecodeItem(buffer, pos, end, message, field_dict):\n    \"\"\"Decode serialized message set to its value and new position.\n\n    Args:\n      buffer: memoryview of the serialized bytes.\n      pos: int, position in the memory view to start at.\n      end: int, end position of serialized data\n      message: Message object to store unknown fields in\n      field_dict: Map[Descriptor, Any] to store decoded values in.\n\n    Returns:\n      int, new position in serialized data.\n    \"\"\"\n    message_set_item_start = pos\n    type_id = -1\n    message_start = -1\n    message_end = -1\n\n    # Technically, type_id and message can appear in any order, so we need\n    # a little loop here.\n    while 1:\n      (tag_bytes, pos) = local_ReadTag(buffer, pos)\n      if tag_bytes == type_id_tag_bytes:\n        (type_id, pos) = local_DecodeVarint(buffer, pos)\n      elif tag_bytes == message_tag_bytes:\n        (size, message_start) = local_DecodeVarint(buffer, pos)\n        pos = message_end = message_start + size\n      elif tag_bytes == item_end_tag_bytes:\n        break\n      else:\n        pos = SkipField(buffer, pos, end, tag_bytes)\n        if pos == -1:\n          raise _DecodeError('Missing group end tag.')\n\n    if pos > end:\n      raise _DecodeError('Truncated message.')\n\n    if type_id == -1:\n      raise _DecodeError('MessageSet item missing type_id.')\n    if message_start == -1:\n      raise _DecodeError('MessageSet item missing message.')\n\n    extension = message.Extensions._FindExtensionByNumber(type_id)\n    # pylint: disable=protected-access\n    if extension is not None:\n      value = field_dict.get(extension)\n      if value is None:\n        message_type = extension.message_type\n        if not hasattr(message_type, '_concrete_class'):\n          # pylint: disable=protected-access\n          message._FACTORY.GetPrototype(message_type)\n        value = field_dict.setdefault(\n            extension, message_type._concrete_class())\n      if value._InternalParse(buffer, message_start,message_end) != message_end:\n        # The only reason _InternalParse would return early is if it encountered\n        # an end-group tag.\n        raise _DecodeError('Unexpected end-group tag.')\n    else:\n      if not message._unknown_fields:\n        message._unknown_fields = []\n      message._unknown_fields.append(\n          (MESSAGE_SET_ITEM_TAG, buffer[message_set_item_start:pos].tobytes()))\n      if message._unknown_field_set is None:\n        message._unknown_field_set = containers.UnknownFieldSet()\n      message._unknown_field_set._add(\n          type_id,\n          wire_format.WIRETYPE_LENGTH_DELIMITED,\n          buffer[message_start:message_end].tobytes())\n      # pylint: enable=protected-access\n\n    return pos\n\n  return DecodeItem\n\n# --------------------------------------------------------------------\n\ndef MapDecoder(field_descriptor, new_default, is_message_map):\n  \"\"\"Returns a decoder for a map field.\"\"\"\n\n  key = field_descriptor\n  tag_bytes = encoder.TagBytes(field_descriptor.number,\n                               wire_format.WIRETYPE_LENGTH_DELIMITED)\n  tag_len = len(tag_bytes)\n  local_DecodeVarint = _DecodeVarint\n  # Can't read _concrete_class yet; might not be initialized.\n  message_type = field_descriptor.message_type\n\n  def DecodeMap(buffer, pos, end, message, field_dict):\n    submsg = message_type._concrete_class()\n    value = field_dict.get(key)\n    if value is None:\n      value = field_dict.setdefault(key, new_default(message))\n    while 1:\n      # Read length.\n      (size, pos) = local_DecodeVarint(buffer, pos)\n      new_pos = pos + size\n      if new_pos > end:\n        raise _DecodeError('Truncated message.')\n      # Read sub-message.\n      submsg.Clear()\n      if submsg._InternalParse(buffer, pos, new_pos) != new_pos:\n        # The only reason _InternalParse would return early is if it\n        # encountered an end-group tag.\n        raise _DecodeError('Unexpected end-group tag.')\n\n      if is_message_map:\n        value[submsg.key].CopyFrom(submsg.value)\n      else:\n        value[submsg.key] = submsg.value\n\n      # Predict that the next tag is another copy of the same repeated field.\n      pos = new_pos + tag_len\n      if buffer[new_pos:pos] != tag_bytes or new_pos == end:\n        # Prediction failed.  Return.\n        return new_pos\n\n  return DecodeMap\n\n# --------------------------------------------------------------------\n# Optimization is not as heavy here because calls to SkipField() are rare,\n# except for handling end-group tags.\n\ndef _SkipVarint(buffer, pos, end):\n  \"\"\"Skip a varint value.  Returns the new position.\"\"\"\n  # Previously ord(buffer[pos]) raised IndexError when pos is out of range.\n  # With this code, ord(b'') raises TypeError.  Both are handled in\n  # python_message.py to generate a 'Truncated message' error.\n  while ord(buffer[pos:pos+1].tobytes()) & 0x80:\n    pos += 1\n  pos += 1\n  if pos > end:\n    raise _DecodeError('Truncated message.')\n  return pos\n\ndef _SkipFixed64(buffer, pos, end):\n  \"\"\"Skip a fixed64 value.  Returns the new position.\"\"\"\n\n  pos += 8\n  if pos > end:\n    raise _DecodeError('Truncated message.')\n  return pos\n\n\ndef _DecodeFixed64(buffer, pos):\n  \"\"\"Decode a fixed64.\"\"\"\n  new_pos = pos + 8\n  return (struct.unpack('<Q', buffer[pos:new_pos])[0], new_pos)\n\n\ndef _SkipLengthDelimited(buffer, pos, end):\n  \"\"\"Skip a length-delimited value.  Returns the new position.\"\"\"\n\n  (size, pos) = _DecodeVarint(buffer, pos)\n  pos += size\n  if pos > end:\n    raise _DecodeError('Truncated message.')\n  return pos\n\n\ndef _SkipGroup(buffer, pos, end):\n  \"\"\"Skip sub-group.  Returns the new position.\"\"\"\n\n  while 1:\n    (tag_bytes, pos) = ReadTag(buffer, pos)\n    new_pos = SkipField(buffer, pos, end, tag_bytes)\n    if new_pos == -1:\n      return pos\n    pos = new_pos\n\n\ndef _DecodeUnknownFieldSet(buffer, pos, end_pos=None):\n  \"\"\"Decode UnknownFieldSet.  Returns the UnknownFieldSet and new position.\"\"\"\n\n  unknown_field_set = containers.UnknownFieldSet()\n  while end_pos is None or pos < end_pos:\n    (tag_bytes, pos) = ReadTag(buffer, pos)\n    (tag, _) = _DecodeVarint(tag_bytes, 0)\n    field_number, wire_type = wire_format.UnpackTag(tag)\n    if wire_type == wire_format.WIRETYPE_END_GROUP:\n      break\n    (data, pos) = _DecodeUnknownField(buffer, pos, wire_type)\n    # pylint: disable=protected-access\n    unknown_field_set._add(field_number, wire_type, data)\n\n  return (unknown_field_set, pos)\n\n\ndef _DecodeUnknownField(buffer, pos, wire_type):\n  \"\"\"Decode a unknown field.  Returns the UnknownField and new position.\"\"\"\n\n  if wire_type == wire_format.WIRETYPE_VARINT:\n    (data, pos) = _DecodeVarint(buffer, pos)\n  elif wire_type == wire_format.WIRETYPE_FIXED64:\n    (data, pos) = _DecodeFixed64(buffer, pos)\n  elif wire_type == wire_format.WIRETYPE_FIXED32:\n    (data, pos) = _DecodeFixed32(buffer, pos)\n  elif wire_type == wire_format.WIRETYPE_LENGTH_DELIMITED:\n    (size, pos) = _DecodeVarint(buffer, pos)\n    data = buffer[pos:pos+size].tobytes()\n    pos += size\n  elif wire_type == wire_format.WIRETYPE_START_GROUP:\n    (data, pos) = _DecodeUnknownFieldSet(buffer, pos)\n  elif wire_type == wire_format.WIRETYPE_END_GROUP:\n    return (0, -1)\n  else:\n    raise _DecodeError('Wrong wire type in tag.')\n\n  return (data, pos)\n\n\ndef _EndGroup(buffer, pos, end):\n  \"\"\"Skipping an END_GROUP tag returns -1 to tell the parent loop to break.\"\"\"\n\n  return -1\n\n\ndef _SkipFixed32(buffer, pos, end):\n  \"\"\"Skip a fixed32 value.  Returns the new position.\"\"\"\n\n  pos += 4\n  if pos > end:\n    raise _DecodeError('Truncated message.')\n  return pos\n\n\ndef _DecodeFixed32(buffer, pos):\n  \"\"\"Decode a fixed32.\"\"\"\n\n  new_pos = pos + 4\n  return (struct.unpack('<I', buffer[pos:new_pos])[0], new_pos)\n\n\ndef _RaiseInvalidWireType(buffer, pos, end):\n  \"\"\"Skip function for unknown wire types.  Raises an exception.\"\"\"\n\n  raise _DecodeError('Tag had invalid wire type.')\n\ndef _FieldSkipper():\n  \"\"\"Constructs the SkipField function.\"\"\"\n\n  WIRETYPE_TO_SKIPPER = [\n      _SkipVarint,\n      _SkipFixed64,\n      _SkipLengthDelimited,\n      _SkipGroup,\n      _EndGroup,\n      _SkipFixed32,\n      _RaiseInvalidWireType,\n      _RaiseInvalidWireType,\n      ]\n\n  wiretype_mask = wire_format.TAG_TYPE_MASK\n\n  def SkipField(buffer, pos, end, tag_bytes):\n    \"\"\"Skips a field with the specified tag.\n\n    |pos| should point to the byte immediately after the tag.\n\n    Returns:\n        The new position (after the tag value), or -1 if the tag is an end-group\n        tag (in which case the calling loop should break).\n    \"\"\"\n\n    # The wire type is always in the first byte since varints are little-endian.\n    wire_type = ord(tag_bytes[0:1]) & wiretype_mask\n    return WIRETYPE_TO_SKIPPER[wire_type](buffer, pos, end)\n\n  return SkipField\n\nSkipField = _FieldSkipper()\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/encoder.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Code for encoding protocol message primitives.\n\nContains the logic for encoding every logical protocol field type\ninto one of the 5 physical wire types.\n\nThis code is designed to push the Python interpreter's performance to the\nlimits.\n\nThe basic idea is that at startup time, for every field (i.e. every\nFieldDescriptor) we construct two functions:  a \"sizer\" and an \"encoder\".  The\nsizer takes a value of this field's type and computes its byte size.  The\nencoder takes a writer function and a value.  It encodes the value into byte\nstrings and invokes the writer function to write those strings.  Typically the\nwriter function is the write() method of a BytesIO.\n\nWe try to do as much work as possible when constructing the writer and the\nsizer rather than when calling them.  In particular:\n* We copy any needed global functions to local variables, so that we do not need\n  to do costly global table lookups at runtime.\n* Similarly, we try to do any attribute lookups at startup time if possible.\n* Every field's tag is encoded to bytes at startup, since it can't change at\n  runtime.\n* Whatever component of the field size we can compute at startup, we do.\n* We *avoid* sharing code if doing so would make the code slower and not sharing\n  does not burden us too much.  For example, encoders for repeated fields do\n  not just call the encoders for singular fields in a loop because this would\n  add an extra function call overhead for every loop iteration; instead, we\n  manually inline the single-value encoder into the loop.\n* If a Python function lacks a return statement, Python actually generates\n  instructions to pop the result of the last statement off the stack, push\n  None onto the stack, and then return that.  If we really don't care what\n  value is returned, then we can save two instructions by returning the\n  result of the last statement.  It looks funny but it helps.\n* We assume that type and bounds checking has happened at a higher level.\n\"\"\"\n\n__author__ = 'kenton@google.com (Kenton Varda)'\n\nimport struct\n\nfrom google.protobuf.internal import wire_format\n\n\n# This will overflow and thus become IEEE-754 \"infinity\".  We would use\n# \"float('inf')\" but it doesn't work on Windows pre-Python-2.6.\n_POS_INF = 1e10000\n_NEG_INF = -_POS_INF\n\n\ndef _VarintSize(value):\n  \"\"\"Compute the size of a varint value.\"\"\"\n  if value <= 0x7f: return 1\n  if value <= 0x3fff: return 2\n  if value <= 0x1fffff: return 3\n  if value <= 0xfffffff: return 4\n  if value <= 0x7ffffffff: return 5\n  if value <= 0x3ffffffffff: return 6\n  if value <= 0x1ffffffffffff: return 7\n  if value <= 0xffffffffffffff: return 8\n  if value <= 0x7fffffffffffffff: return 9\n  return 10\n\n\ndef _SignedVarintSize(value):\n  \"\"\"Compute the size of a signed varint value.\"\"\"\n  if value < 0: return 10\n  if value <= 0x7f: return 1\n  if value <= 0x3fff: return 2\n  if value <= 0x1fffff: return 3\n  if value <= 0xfffffff: return 4\n  if value <= 0x7ffffffff: return 5\n  if value <= 0x3ffffffffff: return 6\n  if value <= 0x1ffffffffffff: return 7\n  if value <= 0xffffffffffffff: return 8\n  if value <= 0x7fffffffffffffff: return 9\n  return 10\n\n\ndef _TagSize(field_number):\n  \"\"\"Returns the number of bytes required to serialize a tag with this field\n  number.\"\"\"\n  # Just pass in type 0, since the type won't affect the tag+type size.\n  return _VarintSize(wire_format.PackTag(field_number, 0))\n\n\n# --------------------------------------------------------------------\n# In this section we define some generic sizers.  Each of these functions\n# takes parameters specific to a particular field type, e.g. int32 or fixed64.\n# It returns another function which in turn takes parameters specific to a\n# particular field, e.g. the field number and whether it is repeated or packed.\n# Look at the next section to see how these are used.\n\n\ndef _SimpleSizer(compute_value_size):\n  \"\"\"A sizer which uses the function compute_value_size to compute the size of\n  each value.  Typically compute_value_size is _VarintSize.\"\"\"\n\n  def SpecificSizer(field_number, is_repeated, is_packed):\n    tag_size = _TagSize(field_number)\n    if is_packed:\n      local_VarintSize = _VarintSize\n      def PackedFieldSize(value):\n        result = 0\n        for element in value:\n          result += compute_value_size(element)\n        return result + local_VarintSize(result) + tag_size\n      return PackedFieldSize\n    elif is_repeated:\n      def RepeatedFieldSize(value):\n        result = tag_size * len(value)\n        for element in value:\n          result += compute_value_size(element)\n        return result\n      return RepeatedFieldSize\n    else:\n      def FieldSize(value):\n        return tag_size + compute_value_size(value)\n      return FieldSize\n\n  return SpecificSizer\n\n\ndef _ModifiedSizer(compute_value_size, modify_value):\n  \"\"\"Like SimpleSizer, but modify_value is invoked on each value before it is\n  passed to compute_value_size.  modify_value is typically ZigZagEncode.\"\"\"\n\n  def SpecificSizer(field_number, is_repeated, is_packed):\n    tag_size = _TagSize(field_number)\n    if is_packed:\n      local_VarintSize = _VarintSize\n      def PackedFieldSize(value):\n        result = 0\n        for element in value:\n          result += compute_value_size(modify_value(element))\n        return result + local_VarintSize(result) + tag_size\n      return PackedFieldSize\n    elif is_repeated:\n      def RepeatedFieldSize(value):\n        result = tag_size * len(value)\n        for element in value:\n          result += compute_value_size(modify_value(element))\n        return result\n      return RepeatedFieldSize\n    else:\n      def FieldSize(value):\n        return tag_size + compute_value_size(modify_value(value))\n      return FieldSize\n\n  return SpecificSizer\n\n\ndef _FixedSizer(value_size):\n  \"\"\"Like _SimpleSizer except for a fixed-size field.  The input is the size\n  of one value.\"\"\"\n\n  def SpecificSizer(field_number, is_repeated, is_packed):\n    tag_size = _TagSize(field_number)\n    if is_packed:\n      local_VarintSize = _VarintSize\n      def PackedFieldSize(value):\n        result = len(value) * value_size\n        return result + local_VarintSize(result) + tag_size\n      return PackedFieldSize\n    elif is_repeated:\n      element_size = value_size + tag_size\n      def RepeatedFieldSize(value):\n        return len(value) * element_size\n      return RepeatedFieldSize\n    else:\n      field_size = value_size + tag_size\n      def FieldSize(value):\n        return field_size\n      return FieldSize\n\n  return SpecificSizer\n\n\n# ====================================================================\n# Here we declare a sizer constructor for each field type.  Each \"sizer\n# constructor\" is a function that takes (field_number, is_repeated, is_packed)\n# as parameters and returns a sizer, which in turn takes a field value as\n# a parameter and returns its encoded size.\n\n\nInt32Sizer = Int64Sizer = EnumSizer = _SimpleSizer(_SignedVarintSize)\n\nUInt32Sizer = UInt64Sizer = _SimpleSizer(_VarintSize)\n\nSInt32Sizer = SInt64Sizer = _ModifiedSizer(\n    _SignedVarintSize, wire_format.ZigZagEncode)\n\nFixed32Sizer = SFixed32Sizer = FloatSizer  = _FixedSizer(4)\nFixed64Sizer = SFixed64Sizer = DoubleSizer = _FixedSizer(8)\n\nBoolSizer = _FixedSizer(1)\n\n\ndef StringSizer(field_number, is_repeated, is_packed):\n  \"\"\"Returns a sizer for a string field.\"\"\"\n\n  tag_size = _TagSize(field_number)\n  local_VarintSize = _VarintSize\n  local_len = len\n  assert not is_packed\n  if is_repeated:\n    def RepeatedFieldSize(value):\n      result = tag_size * len(value)\n      for element in value:\n        l = local_len(element.encode('utf-8'))\n        result += local_VarintSize(l) + l\n      return result\n    return RepeatedFieldSize\n  else:\n    def FieldSize(value):\n      l = local_len(value.encode('utf-8'))\n      return tag_size + local_VarintSize(l) + l\n    return FieldSize\n\n\ndef BytesSizer(field_number, is_repeated, is_packed):\n  \"\"\"Returns a sizer for a bytes field.\"\"\"\n\n  tag_size = _TagSize(field_number)\n  local_VarintSize = _VarintSize\n  local_len = len\n  assert not is_packed\n  if is_repeated:\n    def RepeatedFieldSize(value):\n      result = tag_size * len(value)\n      for element in value:\n        l = local_len(element)\n        result += local_VarintSize(l) + l\n      return result\n    return RepeatedFieldSize\n  else:\n    def FieldSize(value):\n      l = local_len(value)\n      return tag_size + local_VarintSize(l) + l\n    return FieldSize\n\n\ndef GroupSizer(field_number, is_repeated, is_packed):\n  \"\"\"Returns a sizer for a group field.\"\"\"\n\n  tag_size = _TagSize(field_number) * 2\n  assert not is_packed\n  if is_repeated:\n    def RepeatedFieldSize(value):\n      result = tag_size * len(value)\n      for element in value:\n        result += element.ByteSize()\n      return result\n    return RepeatedFieldSize\n  else:\n    def FieldSize(value):\n      return tag_size + value.ByteSize()\n    return FieldSize\n\n\ndef MessageSizer(field_number, is_repeated, is_packed):\n  \"\"\"Returns a sizer for a message field.\"\"\"\n\n  tag_size = _TagSize(field_number)\n  local_VarintSize = _VarintSize\n  assert not is_packed\n  if is_repeated:\n    def RepeatedFieldSize(value):\n      result = tag_size * len(value)\n      for element in value:\n        l = element.ByteSize()\n        result += local_VarintSize(l) + l\n      return result\n    return RepeatedFieldSize\n  else:\n    def FieldSize(value):\n      l = value.ByteSize()\n      return tag_size + local_VarintSize(l) + l\n    return FieldSize\n\n\n# --------------------------------------------------------------------\n# MessageSet is special: it needs custom logic to compute its size properly.\n\n\ndef MessageSetItemSizer(field_number):\n  \"\"\"Returns a sizer for extensions of MessageSet.\n\n  The message set message looks like this:\n    message MessageSet {\n      repeated group Item = 1 {\n        required int32 type_id = 2;\n        required string message = 3;\n      }\n    }\n  \"\"\"\n  static_size = (_TagSize(1) * 2 + _TagSize(2) + _VarintSize(field_number) +\n                 _TagSize(3))\n  local_VarintSize = _VarintSize\n\n  def FieldSize(value):\n    l = value.ByteSize()\n    return static_size + local_VarintSize(l) + l\n\n  return FieldSize\n\n\n# --------------------------------------------------------------------\n# Map is special: it needs custom logic to compute its size properly.\n\n\ndef MapSizer(field_descriptor, is_message_map):\n  \"\"\"Returns a sizer for a map field.\"\"\"\n\n  # Can't look at field_descriptor.message_type._concrete_class because it may\n  # not have been initialized yet.\n  message_type = field_descriptor.message_type\n  message_sizer = MessageSizer(field_descriptor.number, False, False)\n\n  def FieldSize(map_value):\n    total = 0\n    for key in map_value:\n      value = map_value[key]\n      # It's wasteful to create the messages and throw them away one second\n      # later since we'll do the same for the actual encode.  But there's not an\n      # obvious way to avoid this within the current design without tons of code\n      # duplication. For message map, value.ByteSize() should be called to\n      # update the status.\n      entry_msg = message_type._concrete_class(key=key, value=value)\n      total += message_sizer(entry_msg)\n      if is_message_map:\n        value.ByteSize()\n    return total\n\n  return FieldSize\n\n# ====================================================================\n# Encoders!\n\n\ndef _VarintEncoder():\n  \"\"\"Return an encoder for a basic varint value (does not include tag).\"\"\"\n\n  local_int2byte = struct.Struct('>B').pack\n\n  def EncodeVarint(write, value, unused_deterministic=None):\n    bits = value & 0x7f\n    value >>= 7\n    while value:\n      write(local_int2byte(0x80|bits))\n      bits = value & 0x7f\n      value >>= 7\n    return write(local_int2byte(bits))\n\n  return EncodeVarint\n\n\ndef _SignedVarintEncoder():\n  \"\"\"Return an encoder for a basic signed varint value (does not include\n  tag).\"\"\"\n\n  local_int2byte = struct.Struct('>B').pack\n\n  def EncodeSignedVarint(write, value, unused_deterministic=None):\n    if value < 0:\n      value += (1 << 64)\n    bits = value & 0x7f\n    value >>= 7\n    while value:\n      write(local_int2byte(0x80|bits))\n      bits = value & 0x7f\n      value >>= 7\n    return write(local_int2byte(bits))\n\n  return EncodeSignedVarint\n\n\n_EncodeVarint = _VarintEncoder()\n_EncodeSignedVarint = _SignedVarintEncoder()\n\n\ndef _VarintBytes(value):\n  \"\"\"Encode the given integer as a varint and return the bytes.  This is only\n  called at startup time so it doesn't need to be fast.\"\"\"\n\n  pieces = []\n  _EncodeVarint(pieces.append, value, True)\n  return b\"\".join(pieces)\n\n\ndef TagBytes(field_number, wire_type):\n  \"\"\"Encode the given tag and return the bytes.  Only called at startup.\"\"\"\n\n  return bytes(_VarintBytes(wire_format.PackTag(field_number, wire_type)))\n\n# --------------------------------------------------------------------\n# As with sizers (see above), we have a number of common encoder\n# implementations.\n\n\ndef _SimpleEncoder(wire_type, encode_value, compute_value_size):\n  \"\"\"Return a constructor for an encoder for fields of a particular type.\n\n  Args:\n      wire_type:  The field's wire type, for encoding tags.\n      encode_value:  A function which encodes an individual value, e.g.\n        _EncodeVarint().\n      compute_value_size:  A function which computes the size of an individual\n        value, e.g. _VarintSize().\n  \"\"\"\n\n  def SpecificEncoder(field_number, is_repeated, is_packed):\n    if is_packed:\n      tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n      local_EncodeVarint = _EncodeVarint\n      def EncodePackedField(write, value, deterministic):\n        write(tag_bytes)\n        size = 0\n        for element in value:\n          size += compute_value_size(element)\n        local_EncodeVarint(write, size, deterministic)\n        for element in value:\n          encode_value(write, element, deterministic)\n      return EncodePackedField\n    elif is_repeated:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeRepeatedField(write, value, deterministic):\n        for element in value:\n          write(tag_bytes)\n          encode_value(write, element, deterministic)\n      return EncodeRepeatedField\n    else:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeField(write, value, deterministic):\n        write(tag_bytes)\n        return encode_value(write, value, deterministic)\n      return EncodeField\n\n  return SpecificEncoder\n\n\ndef _ModifiedEncoder(wire_type, encode_value, compute_value_size, modify_value):\n  \"\"\"Like SimpleEncoder but additionally invokes modify_value on every value\n  before passing it to encode_value.  Usually modify_value is ZigZagEncode.\"\"\"\n\n  def SpecificEncoder(field_number, is_repeated, is_packed):\n    if is_packed:\n      tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n      local_EncodeVarint = _EncodeVarint\n      def EncodePackedField(write, value, deterministic):\n        write(tag_bytes)\n        size = 0\n        for element in value:\n          size += compute_value_size(modify_value(element))\n        local_EncodeVarint(write, size, deterministic)\n        for element in value:\n          encode_value(write, modify_value(element), deterministic)\n      return EncodePackedField\n    elif is_repeated:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeRepeatedField(write, value, deterministic):\n        for element in value:\n          write(tag_bytes)\n          encode_value(write, modify_value(element), deterministic)\n      return EncodeRepeatedField\n    else:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeField(write, value, deterministic):\n        write(tag_bytes)\n        return encode_value(write, modify_value(value), deterministic)\n      return EncodeField\n\n  return SpecificEncoder\n\n\ndef _StructPackEncoder(wire_type, format):\n  \"\"\"Return a constructor for an encoder for a fixed-width field.\n\n  Args:\n      wire_type:  The field's wire type, for encoding tags.\n      format:  The format string to pass to struct.pack().\n  \"\"\"\n\n  value_size = struct.calcsize(format)\n\n  def SpecificEncoder(field_number, is_repeated, is_packed):\n    local_struct_pack = struct.pack\n    if is_packed:\n      tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n      local_EncodeVarint = _EncodeVarint\n      def EncodePackedField(write, value, deterministic):\n        write(tag_bytes)\n        local_EncodeVarint(write, len(value) * value_size, deterministic)\n        for element in value:\n          write(local_struct_pack(format, element))\n      return EncodePackedField\n    elif is_repeated:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeRepeatedField(write, value, unused_deterministic=None):\n        for element in value:\n          write(tag_bytes)\n          write(local_struct_pack(format, element))\n      return EncodeRepeatedField\n    else:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeField(write, value, unused_deterministic=None):\n        write(tag_bytes)\n        return write(local_struct_pack(format, value))\n      return EncodeField\n\n  return SpecificEncoder\n\n\ndef _FloatingPointEncoder(wire_type, format):\n  \"\"\"Return a constructor for an encoder for float fields.\n\n  This is like StructPackEncoder, but catches errors that may be due to\n  passing non-finite floating-point values to struct.pack, and makes a\n  second attempt to encode those values.\n\n  Args:\n      wire_type:  The field's wire type, for encoding tags.\n      format:  The format string to pass to struct.pack().\n  \"\"\"\n\n  value_size = struct.calcsize(format)\n  if value_size == 4:\n    def EncodeNonFiniteOrRaise(write, value):\n      # Remember that the serialized form uses little-endian byte order.\n      if value == _POS_INF:\n        write(b'\\x00\\x00\\x80\\x7F')\n      elif value == _NEG_INF:\n        write(b'\\x00\\x00\\x80\\xFF')\n      elif value != value:           # NaN\n        write(b'\\x00\\x00\\xC0\\x7F')\n      else:\n        raise\n  elif value_size == 8:\n    def EncodeNonFiniteOrRaise(write, value):\n      if value == _POS_INF:\n        write(b'\\x00\\x00\\x00\\x00\\x00\\x00\\xF0\\x7F')\n      elif value == _NEG_INF:\n        write(b'\\x00\\x00\\x00\\x00\\x00\\x00\\xF0\\xFF')\n      elif value != value:                         # NaN\n        write(b'\\x00\\x00\\x00\\x00\\x00\\x00\\xF8\\x7F')\n      else:\n        raise\n  else:\n    raise ValueError('Can\\'t encode floating-point values that are '\n                     '%d bytes long (only 4 or 8)' % value_size)\n\n  def SpecificEncoder(field_number, is_repeated, is_packed):\n    local_struct_pack = struct.pack\n    if is_packed:\n      tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n      local_EncodeVarint = _EncodeVarint\n      def EncodePackedField(write, value, deterministic):\n        write(tag_bytes)\n        local_EncodeVarint(write, len(value) * value_size, deterministic)\n        for element in value:\n          # This try/except block is going to be faster than any code that\n          # we could write to check whether element is finite.\n          try:\n            write(local_struct_pack(format, element))\n          except SystemError:\n            EncodeNonFiniteOrRaise(write, element)\n      return EncodePackedField\n    elif is_repeated:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeRepeatedField(write, value, unused_deterministic=None):\n        for element in value:\n          write(tag_bytes)\n          try:\n            write(local_struct_pack(format, element))\n          except SystemError:\n            EncodeNonFiniteOrRaise(write, element)\n      return EncodeRepeatedField\n    else:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeField(write, value, unused_deterministic=None):\n        write(tag_bytes)\n        try:\n          write(local_struct_pack(format, value))\n        except SystemError:\n          EncodeNonFiniteOrRaise(write, value)\n      return EncodeField\n\n  return SpecificEncoder\n\n\n# ====================================================================\n# Here we declare an encoder constructor for each field type.  These work\n# very similarly to sizer constructors, described earlier.\n\n\nInt32Encoder = Int64Encoder = EnumEncoder = _SimpleEncoder(\n    wire_format.WIRETYPE_VARINT, _EncodeSignedVarint, _SignedVarintSize)\n\nUInt32Encoder = UInt64Encoder = _SimpleEncoder(\n    wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize)\n\nSInt32Encoder = SInt64Encoder = _ModifiedEncoder(\n    wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize,\n    wire_format.ZigZagEncode)\n\n# Note that Python conveniently guarantees that when using the '<' prefix on\n# formats, they will also have the same size across all platforms (as opposed\n# to without the prefix, where their sizes depend on the C compiler's basic\n# type sizes).\nFixed32Encoder  = _StructPackEncoder(wire_format.WIRETYPE_FIXED32, '<I')\nFixed64Encoder  = _StructPackEncoder(wire_format.WIRETYPE_FIXED64, '<Q')\nSFixed32Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED32, '<i')\nSFixed64Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED64, '<q')\nFloatEncoder    = _FloatingPointEncoder(wire_format.WIRETYPE_FIXED32, '<f')\nDoubleEncoder   = _FloatingPointEncoder(wire_format.WIRETYPE_FIXED64, '<d')\n\n\ndef BoolEncoder(field_number, is_repeated, is_packed):\n  \"\"\"Returns an encoder for a boolean field.\"\"\"\n\n  false_byte = b'\\x00'\n  true_byte = b'\\x01'\n  if is_packed:\n    tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n    local_EncodeVarint = _EncodeVarint\n    def EncodePackedField(write, value, deterministic):\n      write(tag_bytes)\n      local_EncodeVarint(write, len(value), deterministic)\n      for element in value:\n        if element:\n          write(true_byte)\n        else:\n          write(false_byte)\n    return EncodePackedField\n  elif is_repeated:\n    tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_VARINT)\n    def EncodeRepeatedField(write, value, unused_deterministic=None):\n      for element in value:\n        write(tag_bytes)\n        if element:\n          write(true_byte)\n        else:\n          write(false_byte)\n    return EncodeRepeatedField\n  else:\n    tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_VARINT)\n    def EncodeField(write, value, unused_deterministic=None):\n      write(tag_bytes)\n      if value:\n        return write(true_byte)\n      return write(false_byte)\n    return EncodeField\n\n\ndef StringEncoder(field_number, is_repeated, is_packed):\n  \"\"\"Returns an encoder for a string field.\"\"\"\n\n  tag = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n  local_EncodeVarint = _EncodeVarint\n  local_len = len\n  assert not is_packed\n  if is_repeated:\n    def EncodeRepeatedField(write, value, deterministic):\n      for element in value:\n        encoded = element.encode('utf-8')\n        write(tag)\n        local_EncodeVarint(write, local_len(encoded), deterministic)\n        write(encoded)\n    return EncodeRepeatedField\n  else:\n    def EncodeField(write, value, deterministic):\n      encoded = value.encode('utf-8')\n      write(tag)\n      local_EncodeVarint(write, local_len(encoded), deterministic)\n      return write(encoded)\n    return EncodeField\n\n\ndef BytesEncoder(field_number, is_repeated, is_packed):\n  \"\"\"Returns an encoder for a bytes field.\"\"\"\n\n  tag = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n  local_EncodeVarint = _EncodeVarint\n  local_len = len\n  assert not is_packed\n  if is_repeated:\n    def EncodeRepeatedField(write, value, deterministic):\n      for element in value:\n        write(tag)\n        local_EncodeVarint(write, local_len(element), deterministic)\n        write(element)\n    return EncodeRepeatedField\n  else:\n    def EncodeField(write, value, deterministic):\n      write(tag)\n      local_EncodeVarint(write, local_len(value), deterministic)\n      return write(value)\n    return EncodeField\n\n\ndef GroupEncoder(field_number, is_repeated, is_packed):\n  \"\"\"Returns an encoder for a group field.\"\"\"\n\n  start_tag = TagBytes(field_number, wire_format.WIRETYPE_START_GROUP)\n  end_tag = TagBytes(field_number, wire_format.WIRETYPE_END_GROUP)\n  assert not is_packed\n  if is_repeated:\n    def EncodeRepeatedField(write, value, deterministic):\n      for element in value:\n        write(start_tag)\n        element._InternalSerialize(write, deterministic)\n        write(end_tag)\n    return EncodeRepeatedField\n  else:\n    def EncodeField(write, value, deterministic):\n      write(start_tag)\n      value._InternalSerialize(write, deterministic)\n      return write(end_tag)\n    return EncodeField\n\n\ndef MessageEncoder(field_number, is_repeated, is_packed):\n  \"\"\"Returns an encoder for a message field.\"\"\"\n\n  tag = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n  local_EncodeVarint = _EncodeVarint\n  assert not is_packed\n  if is_repeated:\n    def EncodeRepeatedField(write, value, deterministic):\n      for element in value:\n        write(tag)\n        local_EncodeVarint(write, element.ByteSize(), deterministic)\n        element._InternalSerialize(write, deterministic)\n    return EncodeRepeatedField\n  else:\n    def EncodeField(write, value, deterministic):\n      write(tag)\n      local_EncodeVarint(write, value.ByteSize(), deterministic)\n      return value._InternalSerialize(write, deterministic)\n    return EncodeField\n\n\n# --------------------------------------------------------------------\n# As before, MessageSet is special.\n\n\ndef MessageSetItemEncoder(field_number):\n  \"\"\"Encoder for extensions of MessageSet.\n\n  The message set message looks like this:\n    message MessageSet {\n      repeated group Item = 1 {\n        required int32 type_id = 2;\n        required string message = 3;\n      }\n    }\n  \"\"\"\n  start_bytes = b\"\".join([\n      TagBytes(1, wire_format.WIRETYPE_START_GROUP),\n      TagBytes(2, wire_format.WIRETYPE_VARINT),\n      _VarintBytes(field_number),\n      TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED)])\n  end_bytes = TagBytes(1, wire_format.WIRETYPE_END_GROUP)\n  local_EncodeVarint = _EncodeVarint\n\n  def EncodeField(write, value, deterministic):\n    write(start_bytes)\n    local_EncodeVarint(write, value.ByteSize(), deterministic)\n    value._InternalSerialize(write, deterministic)\n    return write(end_bytes)\n\n  return EncodeField\n\n\n# --------------------------------------------------------------------\n# As before, Map is special.\n\n\ndef MapEncoder(field_descriptor):\n  \"\"\"Encoder for extensions of MessageSet.\n\n  Maps always have a wire format like this:\n    message MapEntry {\n      key_type key = 1;\n      value_type value = 2;\n    }\n    repeated MapEntry map = N;\n  \"\"\"\n  # Can't look at field_descriptor.message_type._concrete_class because it may\n  # not have been initialized yet.\n  message_type = field_descriptor.message_type\n  encode_message = MessageEncoder(field_descriptor.number, False, False)\n\n  def EncodeField(write, value, deterministic):\n    value_keys = sorted(value.keys()) if deterministic else value\n    for key in value_keys:\n      entry_msg = message_type._concrete_class(key=key, value=value[key])\n      encode_message(write, entry_msg, deterministic)\n\n  return EncodeField\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/enum_type_wrapper.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"A simple wrapper around enum types to expose utility functions.\n\nInstances are created as properties with the same name as the enum they wrap\non proto classes.  For usage, see:\n  reflection_test.py\n\"\"\"\n\n__author__ = 'rabsatt@google.com (Kevin Rabsatt)'\n\n\nclass EnumTypeWrapper(object):\n  \"\"\"A utility for finding the names of enum values.\"\"\"\n\n  DESCRIPTOR = None\n\n  # This is a type alias, which mypy typing stubs can type as\n  # a genericized parameter constrained to an int, allowing subclasses\n  # to be typed with more constraint in .pyi stubs\n  # Eg.\n  # def MyGeneratedEnum(Message):\n  #   ValueType = NewType('ValueType', int)\n  #   def Name(self, number: MyGeneratedEnum.ValueType) -> str\n  ValueType = int\n\n  def __init__(self, enum_type):\n    \"\"\"Inits EnumTypeWrapper with an EnumDescriptor.\"\"\"\n    self._enum_type = enum_type\n    self.DESCRIPTOR = enum_type  # pylint: disable=invalid-name\n\n  def Name(self, number):  # pylint: disable=invalid-name\n    \"\"\"Returns a string containing the name of an enum value.\"\"\"\n    try:\n      return self._enum_type.values_by_number[number].name\n    except KeyError:\n      pass  # fall out to break exception chaining\n\n    if not isinstance(number, int):\n      raise TypeError(\n          'Enum value for {} must be an int, but got {} {!r}.'.format(\n              self._enum_type.name, type(number), number))\n    else:\n      # repr here to handle the odd case when you pass in a boolean.\n      raise ValueError('Enum {} has no name defined for value {!r}'.format(\n          self._enum_type.name, number))\n\n  def Value(self, name):  # pylint: disable=invalid-name\n    \"\"\"Returns the value corresponding to the given enum name.\"\"\"\n    try:\n      return self._enum_type.values_by_name[name].number\n    except KeyError:\n      pass  # fall out to break exception chaining\n    raise ValueError('Enum {} has no value defined for name {!r}'.format(\n        self._enum_type.name, name))\n\n  def keys(self):\n    \"\"\"Return a list of the string names in the enum.\n\n    Returns:\n      A list of strs, in the order they were defined in the .proto file.\n    \"\"\"\n\n    return [value_descriptor.name\n            for value_descriptor in self._enum_type.values]\n\n  def values(self):\n    \"\"\"Return a list of the integer values in the enum.\n\n    Returns:\n      A list of ints, in the order they were defined in the .proto file.\n    \"\"\"\n\n    return [value_descriptor.number\n            for value_descriptor in self._enum_type.values]\n\n  def items(self):\n    \"\"\"Return a list of the (name, value) pairs of the enum.\n\n    Returns:\n      A list of (str, int) pairs, in the order they were defined\n      in the .proto file.\n    \"\"\"\n    return [(value_descriptor.name, value_descriptor.number)\n            for value_descriptor in self._enum_type.values]\n\n  def __getattr__(self, name):\n    \"\"\"Returns the value corresponding to the given enum name.\"\"\"\n    try:\n      return super(\n          EnumTypeWrapper,\n          self).__getattribute__('_enum_type').values_by_name[name].number\n    except KeyError:\n      pass  # fall out to break exception chaining\n    raise AttributeError('Enum {} has no value defined for name {!r}'.format(\n        self._enum_type.name, name))\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/extension_dict.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Contains _ExtensionDict class to represent extensions.\n\"\"\"\n\nfrom google.protobuf.internal import type_checkers\nfrom google.protobuf.descriptor import FieldDescriptor\n\n\ndef _VerifyExtensionHandle(message, extension_handle):\n  \"\"\"Verify that the given extension handle is valid.\"\"\"\n\n  if not isinstance(extension_handle, FieldDescriptor):\n    raise KeyError('HasExtension() expects an extension handle, got: %s' %\n                   extension_handle)\n\n  if not extension_handle.is_extension:\n    raise KeyError('\"%s\" is not an extension.' % extension_handle.full_name)\n\n  if not extension_handle.containing_type:\n    raise KeyError('\"%s\" is missing a containing_type.'\n                   % extension_handle.full_name)\n\n  if extension_handle.containing_type is not message.DESCRIPTOR:\n    raise KeyError('Extension \"%s\" extends message type \"%s\", but this '\n                   'message is of type \"%s\".' %\n                   (extension_handle.full_name,\n                    extension_handle.containing_type.full_name,\n                    message.DESCRIPTOR.full_name))\n\n\n# TODO(robinson): Unify error handling of \"unknown extension\" crap.\n# TODO(robinson): Support iteritems()-style iteration over all\n# extensions with the \"has\" bits turned on?\nclass _ExtensionDict(object):\n\n  \"\"\"Dict-like container for Extension fields on proto instances.\n\n  Note that in all cases we expect extension handles to be\n  FieldDescriptors.\n  \"\"\"\n\n  def __init__(self, extended_message):\n    \"\"\"\n    Args:\n      extended_message: Message instance for which we are the Extensions dict.\n    \"\"\"\n    self._extended_message = extended_message\n\n  def __getitem__(self, extension_handle):\n    \"\"\"Returns the current value of the given extension handle.\"\"\"\n\n    _VerifyExtensionHandle(self._extended_message, extension_handle)\n\n    result = self._extended_message._fields.get(extension_handle)\n    if result is not None:\n      return result\n\n    if extension_handle.label == FieldDescriptor.LABEL_REPEATED:\n      result = extension_handle._default_constructor(self._extended_message)\n    elif extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE:\n      message_type = extension_handle.message_type\n      if not hasattr(message_type, '_concrete_class'):\n        # pylint: disable=protected-access\n        self._extended_message._FACTORY.GetPrototype(message_type)\n      assert getattr(extension_handle.message_type, '_concrete_class', None), (\n          'Uninitialized concrete class found for field %r (message type %r)'\n          % (extension_handle.full_name,\n             extension_handle.message_type.full_name))\n      result = extension_handle.message_type._concrete_class()\n      try:\n        result._SetListener(self._extended_message._listener_for_children)\n      except ReferenceError:\n        pass\n    else:\n      # Singular scalar -- just return the default without inserting into the\n      # dict.\n      return extension_handle.default_value\n\n    # Atomically check if another thread has preempted us and, if not, swap\n    # in the new object we just created.  If someone has preempted us, we\n    # take that object and discard ours.\n    # WARNING:  We are relying on setdefault() being atomic.  This is true\n    #   in CPython but we haven't investigated others.  This warning appears\n    #   in several other locations in this file.\n    result = self._extended_message._fields.setdefault(\n        extension_handle, result)\n\n    return result\n\n  def __eq__(self, other):\n    if not isinstance(other, self.__class__):\n      return False\n\n    my_fields = self._extended_message.ListFields()\n    other_fields = other._extended_message.ListFields()\n\n    # Get rid of non-extension fields.\n    my_fields = [field for field in my_fields if field.is_extension]\n    other_fields = [field for field in other_fields if field.is_extension]\n\n    return my_fields == other_fields\n\n  def __ne__(self, other):\n    return not self == other\n\n  def __len__(self):\n    fields = self._extended_message.ListFields()\n    # Get rid of non-extension fields.\n    extension_fields = [field for field in fields if field[0].is_extension]\n    return len(extension_fields)\n\n  def __hash__(self):\n    raise TypeError('unhashable object')\n\n  # Note that this is only meaningful for non-repeated, scalar extension\n  # fields.  Note also that we may have to call _Modified() when we do\n  # successfully set a field this way, to set any necessary \"has\" bits in the\n  # ancestors of the extended message.\n  def __setitem__(self, extension_handle, value):\n    \"\"\"If extension_handle specifies a non-repeated, scalar extension\n    field, sets the value of that field.\n    \"\"\"\n\n    _VerifyExtensionHandle(self._extended_message, extension_handle)\n\n    if (extension_handle.label == FieldDescriptor.LABEL_REPEATED or\n        extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE):\n      raise TypeError(\n          'Cannot assign to extension \"%s\" because it is a repeated or '\n          'composite type.' % extension_handle.full_name)\n\n    # It's slightly wasteful to lookup the type checker each time,\n    # but we expect this to be a vanishingly uncommon case anyway.\n    type_checker = type_checkers.GetTypeChecker(extension_handle)\n    # pylint: disable=protected-access\n    self._extended_message._fields[extension_handle] = (\n        type_checker.CheckValue(value))\n    self._extended_message._Modified()\n\n  def __delitem__(self, extension_handle):\n    self._extended_message.ClearExtension(extension_handle)\n\n  def _FindExtensionByName(self, name):\n    \"\"\"Tries to find a known extension with the specified name.\n\n    Args:\n      name: Extension full name.\n\n    Returns:\n      Extension field descriptor.\n    \"\"\"\n    return self._extended_message._extensions_by_name.get(name, None)\n\n  def _FindExtensionByNumber(self, number):\n    \"\"\"Tries to find a known extension with the field number.\n\n    Args:\n      number: Extension field number.\n\n    Returns:\n      Extension field descriptor.\n    \"\"\"\n    return self._extended_message._extensions_by_number.get(number, None)\n\n  def __iter__(self):\n    # Return a generator over the populated extension fields\n    return (f[0] for f in self._extended_message.ListFields()\n            if f[0].is_extension)\n\n  def __contains__(self, extension_handle):\n    _VerifyExtensionHandle(self._extended_message, extension_handle)\n\n    if extension_handle not in self._extended_message._fields:\n      return False\n\n    if extension_handle.label == FieldDescriptor.LABEL_REPEATED:\n      return bool(self._extended_message._fields.get(extension_handle))\n\n    if extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE:\n      value = self._extended_message._fields.get(extension_handle)\n      # pylint: disable=protected-access\n      return value is not None and value._is_present_in_parent\n\n    return True\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/message_listener.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Defines a listener interface for observing certain\nstate transitions on Message objects.\n\nAlso defines a null implementation of this interface.\n\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\n\nclass MessageListener(object):\n\n  \"\"\"Listens for modifications made to a message.  Meant to be registered via\n  Message._SetListener().\n\n  Attributes:\n    dirty:  If True, then calling Modified() would be a no-op.  This can be\n            used to avoid these calls entirely in the common case.\n  \"\"\"\n\n  def Modified(self):\n    \"\"\"Called every time the message is modified in such a way that the parent\n    message may need to be updated.  This currently means either:\n    (a) The message was modified for the first time, so the parent message\n        should henceforth mark the message as present.\n    (b) The message's cached byte size became dirty -- i.e. the message was\n        modified for the first time after a previous call to ByteSize().\n        Therefore the parent should also mark its byte size as dirty.\n    Note that (a) implies (b), since new objects start out with a client cached\n    size (zero).  However, we document (a) explicitly because it is important.\n\n    Modified() will *only* be called in response to one of these two events --\n    not every time the sub-message is modified.\n\n    Note that if the listener's |dirty| attribute is true, then calling\n    Modified at the moment would be a no-op, so it can be skipped.  Performance-\n    sensitive callers should check this attribute directly before calling since\n    it will be true most of the time.\n    \"\"\"\n\n    raise NotImplementedError\n\n\nclass NullMessageListener(object):\n\n  \"\"\"No-op MessageListener implementation.\"\"\"\n\n  def Modified(self):\n    pass\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/message_set_extensions_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/internal/message_set_extensions.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n5google/protobuf/internal/message_set_extensions.proto\\x12\\x18google.protobuf.internal\\\"\\x1e\\n\\x0eTestMessageSet*\\x08\\x08\\x04\\x10\\xff\\xff\\xff\\xff\\x07:\\x02\\x08\\x01\\\"\\xa5\\x01\\n\\x18TestMessageSetExtension1\\x12\\t\\n\\x01i\\x18\\x0f \\x01(\\x05\\x32~\\n\\x15message_set_extension\\x12(.google.protobuf.internal.TestMessageSet\\x18\\xab\\xff\\xf6. \\x01(\\x0b\\x32\\x32.google.protobuf.internal.TestMessageSetExtension1\\\"\\xa7\\x01\\n\\x18TestMessageSetExtension2\\x12\\x0b\\n\\x03str\\x18\\x19 \\x01(\\t2~\\n\\x15message_set_extension\\x12(.google.protobuf.internal.TestMessageSet\\x18\\xca\\xff\\xf6. \\x01(\\x0b\\x32\\x32.google.protobuf.internal.TestMessageSetExtension2\\\"(\\n\\x18TestMessageSetExtension3\\x12\\x0c\\n\\x04text\\x18# \\x01(\\t:\\x7f\\n\\x16message_set_extension3\\x12(.google.protobuf.internal.TestMessageSet\\x18\\xdf\\xff\\xf6. \\x01(\\x0b\\x32\\x32.google.protobuf.internal.TestMessageSetExtension3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.message_set_extensions_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n  TestMessageSet.RegisterExtension(message_set_extension3)\n  TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION1.extensions_by_name['message_set_extension'])\n  TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION2.extensions_by_name['message_set_extension'])\n\n  DESCRIPTOR._options = None\n  _TESTMESSAGESET._options = None\n  _TESTMESSAGESET._serialized_options = b'\\010\\001'\n  _TESTMESSAGESET._serialized_start=83\n  _TESTMESSAGESET._serialized_end=113\n  _TESTMESSAGESETEXTENSION1._serialized_start=116\n  _TESTMESSAGESETEXTENSION1._serialized_end=281\n  _TESTMESSAGESETEXTENSION2._serialized_start=284\n  _TESTMESSAGESETEXTENSION2._serialized_end=451\n  _TESTMESSAGESETEXTENSION3._serialized_start=453\n  _TESTMESSAGESETEXTENSION3._serialized_end=493\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/missing_enum_values_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/internal/missing_enum_values.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n2google/protobuf/internal/missing_enum_values.proto\\x12\\x1fgoogle.protobuf.python.internal\\\"\\xc1\\x02\\n\\x0eTestEnumValues\\x12X\\n\\x14optional_nested_enum\\x18\\x01 \\x01(\\x0e\\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\\x12X\\n\\x14repeated_nested_enum\\x18\\x02 \\x03(\\x0e\\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\\x12Z\\n\\x12packed_nested_enum\\x18\\x03 \\x03(\\x0e\\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnumB\\x02\\x10\\x01\\\"\\x1f\\n\\nNestedEnum\\x12\\x08\\n\\x04ZERO\\x10\\x00\\x12\\x07\\n\\x03ONE\\x10\\x01\\\"\\xd3\\x02\\n\\x15TestMissingEnumValues\\x12_\\n\\x14optional_nested_enum\\x18\\x01 \\x01(\\x0e\\x32\\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\\x12_\\n\\x14repeated_nested_enum\\x18\\x02 \\x03(\\x0e\\x32\\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\\x12\\x61\\n\\x12packed_nested_enum\\x18\\x03 \\x03(\\x0e\\x32\\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnumB\\x02\\x10\\x01\\\"\\x15\\n\\nNestedEnum\\x12\\x07\\n\\x03TWO\\x10\\x02\\\"\\x1b\\n\\nJustString\\x12\\r\\n\\x05\\x64ummy\\x18\\x01 \\x02(\\t')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.missing_enum_values_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  _TESTENUMVALUES.fields_by_name['packed_nested_enum']._options = None\n  _TESTENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\\020\\001'\n  _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._options = None\n  _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\\020\\001'\n  _TESTENUMVALUES._serialized_start=88\n  _TESTENUMVALUES._serialized_end=409\n  _TESTENUMVALUES_NESTEDENUM._serialized_start=378\n  _TESTENUMVALUES_NESTEDENUM._serialized_end=409\n  _TESTMISSINGENUMVALUES._serialized_start=412\n  _TESTMISSINGENUMVALUES._serialized_end=751\n  _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_start=730\n  _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_end=751\n  _JUSTSTRING._serialized_start=753\n  _JUSTSTRING._serialized_end=780\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/internal/more_extensions_dynamic.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom google.protobuf.internal import more_extensions_pb2 as google_dot_protobuf_dot_internal_dot_more__extensions__pb2\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n6google/protobuf/internal/more_extensions_dynamic.proto\\x12\\x18google.protobuf.internal\\x1a.google/protobuf/internal/more_extensions.proto\\\"\\x1f\\n\\x12\\x44ynamicMessageType\\x12\\t\\n\\x01\\x61\\x18\\x01 \\x01(\\x05:J\\n\\x17\\x64ynamic_int32_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x64 \\x01(\\x05:z\\n\\x19\\x64ynamic_message_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x65 \\x01(\\x0b\\x32,.google.protobuf.internal.DynamicMessageType:\\x83\\x01\\n\\\"repeated_dynamic_message_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x66 \\x03(\\x0b\\x32,.google.protobuf.internal.DynamicMessageType')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_dynamic_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n  google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_int32_extension)\n  google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_message_extension)\n  google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(repeated_dynamic_message_extension)\n\n  DESCRIPTOR._options = None\n  _DYNAMICMESSAGETYPE._serialized_start=132\n  _DYNAMICMESSAGETYPE._serialized_end=163\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/more_extensions_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/internal/more_extensions.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n.google/protobuf/internal/more_extensions.proto\\x12\\x18google.protobuf.internal\\\"\\x99\\x01\\n\\x0fTopLevelMessage\\x12\\x41\\n\\nsubmessage\\x18\\x01 \\x01(\\x0b\\x32).google.protobuf.internal.ExtendedMessageB\\x02(\\x01\\x12\\x43\\n\\x0enested_message\\x18\\x02 \\x01(\\x0b\\x32\\'.google.protobuf.internal.NestedMessageB\\x02(\\x01\\\"R\\n\\rNestedMessage\\x12\\x41\\n\\nsubmessage\\x18\\x01 \\x01(\\x0b\\x32).google.protobuf.internal.ExtendedMessageB\\x02(\\x01\\\"K\\n\\x0f\\x45xtendedMessage\\x12\\x17\\n\\x0eoptional_int32\\x18\\xe9\\x07 \\x01(\\x05\\x12\\x18\\n\\x0frepeated_string\\x18\\xea\\x07 \\x03(\\t*\\x05\\x08\\x01\\x10\\xe8\\x07\\\"-\\n\\x0e\\x46oreignMessage\\x12\\x1b\\n\\x13\\x66oreign_message_int\\x18\\x01 \\x01(\\x05:I\\n\\x16optional_int_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x01 \\x01(\\x05:w\\n\\x1aoptional_message_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x02 \\x01(\\x0b\\x32(.google.protobuf.internal.ForeignMessage:I\\n\\x16repeated_int_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x03 \\x03(\\x05:w\\n\\x1arepeated_message_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x04 \\x03(\\x0b\\x32(.google.protobuf.internal.ForeignMessage')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n  ExtendedMessage.RegisterExtension(optional_int_extension)\n  ExtendedMessage.RegisterExtension(optional_message_extension)\n  ExtendedMessage.RegisterExtension(repeated_int_extension)\n  ExtendedMessage.RegisterExtension(repeated_message_extension)\n\n  DESCRIPTOR._options = None\n  _TOPLEVELMESSAGE.fields_by_name['submessage']._options = None\n  _TOPLEVELMESSAGE.fields_by_name['submessage']._serialized_options = b'(\\001'\n  _TOPLEVELMESSAGE.fields_by_name['nested_message']._options = None\n  _TOPLEVELMESSAGE.fields_by_name['nested_message']._serialized_options = b'(\\001'\n  _NESTEDMESSAGE.fields_by_name['submessage']._options = None\n  _NESTEDMESSAGE.fields_by_name['submessage']._serialized_options = b'(\\001'\n  _TOPLEVELMESSAGE._serialized_start=77\n  _TOPLEVELMESSAGE._serialized_end=230\n  _NESTEDMESSAGE._serialized_start=232\n  _NESTEDMESSAGE._serialized_end=314\n  _EXTENDEDMESSAGE._serialized_start=316\n  _EXTENDEDMESSAGE._serialized_end=391\n  _FOREIGNMESSAGE._serialized_start=393\n  _FOREIGNMESSAGE._serialized_end=438\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/more_messages_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/internal/more_messages.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n,google/protobuf/internal/more_messages.proto\\x12\\x18google.protobuf.internal\\\"h\\n\\x10OutOfOrderFields\\x12\\x17\\n\\x0foptional_sint32\\x18\\x05 \\x01(\\x11\\x12\\x17\\n\\x0foptional_uint32\\x18\\x03 \\x01(\\r\\x12\\x16\\n\\x0eoptional_int32\\x18\\x01 \\x01(\\x05*\\x04\\x08\\x04\\x10\\x05*\\x04\\x08\\x02\\x10\\x03\\\"\\xcd\\x02\\n\\x05\\x63lass\\x12\\x1b\\n\\tint_field\\x18\\x01 \\x01(\\x05R\\x08json_int\\x12\\n\\n\\x02if\\x18\\x02 \\x01(\\x05\\x12(\\n\\x02\\x61s\\x18\\x03 \\x01(\\x0e\\x32\\x1c.google.protobuf.internal.is\\x12\\x30\\n\\nenum_field\\x18\\x04 \\x01(\\x0e\\x32\\x1c.google.protobuf.internal.is\\x12>\\n\\x11nested_enum_field\\x18\\x05 \\x01(\\x0e\\x32#.google.protobuf.internal.class.for\\x12;\\n\\x0enested_message\\x18\\x06 \\x01(\\x0b\\x32#.google.protobuf.internal.class.try\\x1a\\x1c\\n\\x03try\\x12\\r\\n\\x05\\x66ield\\x18\\x01 \\x01(\\x05*\\x06\\x08\\xe7\\x07\\x10\\x90N\\\"\\x1c\\n\\x03\\x66or\\x12\\x0b\\n\\x07\\x64\\x65\\x66\\x61ult\\x10\\x00\\x12\\x08\\n\\x04True\\x10\\x01*\\x06\\x08\\xe7\\x07\\x10\\x90N\\\"?\\n\\x0b\\x45xtendClass20\\n\\x06return\\x12\\x1f.google.protobuf.internal.class\\x18\\xea\\x07 \\x01(\\x05\\\"~\\n\\x0fTestFullKeyword\\x12:\\n\\x06\\x66ield1\\x18\\x01 \\x01(\\x0b\\x32*.google.protobuf.internal.OutOfOrderFields\\x12/\\n\\x06\\x66ield2\\x18\\x02 \\x01(\\x0b\\x32\\x1f.google.protobuf.internal.class\\\"\\xa5\\x0f\\n\\x11LotsNestedMessage\\x1a\\x04\\n\\x02\\x42\\x30\\x1a\\x04\\n\\x02\\x42\\x31\\x1a\\x04\\n\\x02\\x42\\x32\\x1a\\x04\\n\\x02\\x42\\x33\\x1a\\x04\\n\\x02\\x42\\x34\\x1a\\x04\\n\\x02\\x42\\x35\\x1a\\x04\\n\\x02\\x42\\x36\\x1a\\x04\\n\\x02\\x42\\x37\\x1a\\x04\\n\\x02\\x42\\x38\\x1a\\x04\\n\\x02\\x42\\x39\\x1a\\x05\\n\\x03\\x42\\x31\\x30\\x1a\\x05\\n\\x03\\x42\\x31\\x31\\x1a\\x05\\n\\x03\\x42\\x31\\x32\\x1a\\x05\\n\\x03\\x42\\x31\\x33\\x1a\\x05\\n\\x03\\x42\\x31\\x34\\x1a\\x05\\n\\x03\\x42\\x31\\x35\\x1a\\x05\\n\\x03\\x42\\x31\\x36\\x1a\\x05\\n\\x03\\x42\\x31\\x37\\x1a\\x05\\n\\x03\\x42\\x31\\x38\\x1a\\x05\\n\\x03\\x42\\x31\\x39\\x1a\\x05\\n\\x03\\x42\\x32\\x30\\x1a\\x05\\n\\x03\\x42\\x32\\x31\\x1a\\x05\\n\\x03\\x42\\x32\\x32\\x1a\\x05\\n\\x03\\x42\\x32\\x33\\x1a\\x05\\n\\x03\\x42\\x32\\x34\\x1a\\x05\\n\\x03\\x42\\x32\\x35\\x1a\\x05\\n\\x03\\x42\\x32\\x36\\x1a\\x05\\n\\x03\\x42\\x32\\x37\\x1a\\x05\\n\\x03\\x42\\x32\\x38\\x1a\\x05\\n\\x03\\x42\\x32\\x39\\x1a\\x05\\n\\x03\\x42\\x33\\x30\\x1a\\x05\\n\\x03\\x42\\x33\\x31\\x1a\\x05\\n\\x03\\x42\\x33\\x32\\x1a\\x05\\n\\x03\\x42\\x33\\x33\\x1a\\x05\\n\\x03\\x42\\x33\\x34\\x1a\\x05\\n\\x03\\x42\\x33\\x35\\x1a\\x05\\n\\x03\\x42\\x33\\x36\\x1a\\x05\\n\\x03\\x42\\x33\\x37\\x1a\\x05\\n\\x03\\x42\\x33\\x38\\x1a\\x05\\n\\x03\\x42\\x33\\x39\\x1a\\x05\\n\\x03\\x42\\x34\\x30\\x1a\\x05\\n\\x03\\x42\\x34\\x31\\x1a\\x05\\n\\x03\\x42\\x34\\x32\\x1a\\x05\\n\\x03\\x42\\x34\\x33\\x1a\\x05\\n\\x03\\x42\\x34\\x34\\x1a\\x05\\n\\x03\\x42\\x34\\x35\\x1a\\x05\\n\\x03\\x42\\x34\\x36\\x1a\\x05\\n\\x03\\x42\\x34\\x37\\x1a\\x05\\n\\x03\\x42\\x34\\x38\\x1a\\x05\\n\\x03\\x42\\x34\\x39\\x1a\\x05\\n\\x03\\x42\\x35\\x30\\x1a\\x05\\n\\x03\\x42\\x35\\x31\\x1a\\x05\\n\\x03\\x42\\x35\\x32\\x1a\\x05\\n\\x03\\x42\\x35\\x33\\x1a\\x05\\n\\x03\\x42\\x35\\x34\\x1a\\x05\\n\\x03\\x42\\x35\\x35\\x1a\\x05\\n\\x03\\x42\\x35\\x36\\x1a\\x05\\n\\x03\\x42\\x35\\x37\\x1a\\x05\\n\\x03\\x42\\x35\\x38\\x1a\\x05\\n\\x03\\x42\\x35\\x39\\x1a\\x05\\n\\x03\\x42\\x36\\x30\\x1a\\x05\\n\\x03\\x42\\x36\\x31\\x1a\\x05\\n\\x03\\x42\\x36\\x32\\x1a\\x05\\n\\x03\\x42\\x36\\x33\\x1a\\x05\\n\\x03\\x42\\x36\\x34\\x1a\\x05\\n\\x03\\x42\\x36\\x35\\x1a\\x05\\n\\x03\\x42\\x36\\x36\\x1a\\x05\\n\\x03\\x42\\x36\\x37\\x1a\\x05\\n\\x03\\x42\\x36\\x38\\x1a\\x05\\n\\x03\\x42\\x36\\x39\\x1a\\x05\\n\\x03\\x42\\x37\\x30\\x1a\\x05\\n\\x03\\x42\\x37\\x31\\x1a\\x05\\n\\x03\\x42\\x37\\x32\\x1a\\x05\\n\\x03\\x42\\x37\\x33\\x1a\\x05\\n\\x03\\x42\\x37\\x34\\x1a\\x05\\n\\x03\\x42\\x37\\x35\\x1a\\x05\\n\\x03\\x42\\x37\\x36\\x1a\\x05\\n\\x03\\x42\\x37\\x37\\x1a\\x05\\n\\x03\\x42\\x37\\x38\\x1a\\x05\\n\\x03\\x42\\x37\\x39\\x1a\\x05\\n\\x03\\x42\\x38\\x30\\x1a\\x05\\n\\x03\\x42\\x38\\x31\\x1a\\x05\\n\\x03\\x42\\x38\\x32\\x1a\\x05\\n\\x03\\x42\\x38\\x33\\x1a\\x05\\n\\x03\\x42\\x38\\x34\\x1a\\x05\\n\\x03\\x42\\x38\\x35\\x1a\\x05\\n\\x03\\x42\\x38\\x36\\x1a\\x05\\n\\x03\\x42\\x38\\x37\\x1a\\x05\\n\\x03\\x42\\x38\\x38\\x1a\\x05\\n\\x03\\x42\\x38\\x39\\x1a\\x05\\n\\x03\\x42\\x39\\x30\\x1a\\x05\\n\\x03\\x42\\x39\\x31\\x1a\\x05\\n\\x03\\x42\\x39\\x32\\x1a\\x05\\n\\x03\\x42\\x39\\x33\\x1a\\x05\\n\\x03\\x42\\x39\\x34\\x1a\\x05\\n\\x03\\x42\\x39\\x35\\x1a\\x05\\n\\x03\\x42\\x39\\x36\\x1a\\x05\\n\\x03\\x42\\x39\\x37\\x1a\\x05\\n\\x03\\x42\\x39\\x38\\x1a\\x05\\n\\x03\\x42\\x39\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x39\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x30\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x31\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x32\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x33\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x34\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x35\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x36\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x37\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x38\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x39\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x30\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x31\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x32\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x33\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x34\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x35\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x36\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x37\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x38\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x39\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x30\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x31\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x32\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x33\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x34\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x35\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x36\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x37\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x38\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x39\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x30\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x31\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x32\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x33\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x34\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x35\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x36\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x37\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x38\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x39\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x30\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x31\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x32\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x33\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x34\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x35\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x36\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x37\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x38\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x39\\x1a\\x06\\n\\x04\\x42\\x32\\x35\\x30\\x1a\\x06\\n\\x04\\x42\\x32\\x35\\x31\\x1a\\x06\\n\\x04\\x42\\x32\\x35\\x32\\x1a\\x06\\n\\x04\\x42\\x32\\x35\\x33\\x1a\\x06\\n\\x04\\x42\\x32\\x35\\x34\\x1a\\x06\\n\\x04\\x42\\x32\\x35\\x35*\\x1b\\n\\x02is\\x12\\x0b\\n\\x07\\x64\\x65\\x66\\x61ult\\x10\\x00\\x12\\x08\\n\\x04\\x65lse\\x10\\x01:C\\n\\x0foptional_uint64\\x12*.google.protobuf.internal.OutOfOrderFields\\x18\\x04 \\x01(\\x04:B\\n\\x0eoptional_int64\\x12*.google.protobuf.internal.OutOfOrderFields\\x18\\x02 \\x01(\\x03:2\\n\\x08\\x63ontinue\\x12\\x1f.google.protobuf.internal.class\\x18\\xe9\\x07 \\x01(\\x05:2\\n\\x04with\\x12#.google.protobuf.internal.class.try\\x18\\xe9\\x07 \\x01(\\x05')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_messages_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n  OutOfOrderFields.RegisterExtension(optional_uint64)\n  OutOfOrderFields.RegisterExtension(optional_int64)\n  globals()['class'].RegisterExtension(globals()['continue'])\n  getattr(globals()['class'], 'try').RegisterExtension(globals()['with'])\n  globals()['class'].RegisterExtension(_EXTENDCLASS.extensions_by_name['return'])\n\n  DESCRIPTOR._options = None\n  _IS._serialized_start=2669\n  _IS._serialized_end=2696\n  _OUTOFORDERFIELDS._serialized_start=74\n  _OUTOFORDERFIELDS._serialized_end=178\n  _CLASS._serialized_start=181\n  _CLASS._serialized_end=514\n  _CLASS_TRY._serialized_start=448\n  _CLASS_TRY._serialized_end=476\n  _CLASS_FOR._serialized_start=478\n  _CLASS_FOR._serialized_end=506\n  _EXTENDCLASS._serialized_start=516\n  _EXTENDCLASS._serialized_end=579\n  _TESTFULLKEYWORD._serialized_start=581\n  _TESTFULLKEYWORD._serialized_end=707\n  _LOTSNESTEDMESSAGE._serialized_start=710\n  _LOTSNESTEDMESSAGE._serialized_end=2667\n  _LOTSNESTEDMESSAGE_B0._serialized_start=731\n  _LOTSNESTEDMESSAGE_B0._serialized_end=735\n  _LOTSNESTEDMESSAGE_B1._serialized_start=737\n  _LOTSNESTEDMESSAGE_B1._serialized_end=741\n  _LOTSNESTEDMESSAGE_B2._serialized_start=743\n  _LOTSNESTEDMESSAGE_B2._serialized_end=747\n  _LOTSNESTEDMESSAGE_B3._serialized_start=749\n  _LOTSNESTEDMESSAGE_B3._serialized_end=753\n  _LOTSNESTEDMESSAGE_B4._serialized_start=755\n  _LOTSNESTEDMESSAGE_B4._serialized_end=759\n  _LOTSNESTEDMESSAGE_B5._serialized_start=761\n  _LOTSNESTEDMESSAGE_B5._serialized_end=765\n  _LOTSNESTEDMESSAGE_B6._serialized_start=767\n  _LOTSNESTEDMESSAGE_B6._serialized_end=771\n  _LOTSNESTEDMESSAGE_B7._serialized_start=773\n  _LOTSNESTEDMESSAGE_B7._serialized_end=777\n  _LOTSNESTEDMESSAGE_B8._serialized_start=779\n  _LOTSNESTEDMESSAGE_B8._serialized_end=783\n  _LOTSNESTEDMESSAGE_B9._serialized_start=785\n  _LOTSNESTEDMESSAGE_B9._serialized_end=789\n  _LOTSNESTEDMESSAGE_B10._serialized_start=791\n  _LOTSNESTEDMESSAGE_B10._serialized_end=796\n  _LOTSNESTEDMESSAGE_B11._serialized_start=798\n  _LOTSNESTEDMESSAGE_B11._serialized_end=803\n  _LOTSNESTEDMESSAGE_B12._serialized_start=805\n  _LOTSNESTEDMESSAGE_B12._serialized_end=810\n  _LOTSNESTEDMESSAGE_B13._serialized_start=812\n  _LOTSNESTEDMESSAGE_B13._serialized_end=817\n  _LOTSNESTEDMESSAGE_B14._serialized_start=819\n  _LOTSNESTEDMESSAGE_B14._serialized_end=824\n  _LOTSNESTEDMESSAGE_B15._serialized_start=826\n  _LOTSNESTEDMESSAGE_B15._serialized_end=831\n  _LOTSNESTEDMESSAGE_B16._serialized_start=833\n  _LOTSNESTEDMESSAGE_B16._serialized_end=838\n  _LOTSNESTEDMESSAGE_B17._serialized_start=840\n  _LOTSNESTEDMESSAGE_B17._serialized_end=845\n  _LOTSNESTEDMESSAGE_B18._serialized_start=847\n  _LOTSNESTEDMESSAGE_B18._serialized_end=852\n  _LOTSNESTEDMESSAGE_B19._serialized_start=854\n  _LOTSNESTEDMESSAGE_B19._serialized_end=859\n  _LOTSNESTEDMESSAGE_B20._serialized_start=861\n  _LOTSNESTEDMESSAGE_B20._serialized_end=866\n  _LOTSNESTEDMESSAGE_B21._serialized_start=868\n  _LOTSNESTEDMESSAGE_B21._serialized_end=873\n  _LOTSNESTEDMESSAGE_B22._serialized_start=875\n  _LOTSNESTEDMESSAGE_B22._serialized_end=880\n  _LOTSNESTEDMESSAGE_B23._serialized_start=882\n  _LOTSNESTEDMESSAGE_B23._serialized_end=887\n  _LOTSNESTEDMESSAGE_B24._serialized_start=889\n  _LOTSNESTEDMESSAGE_B24._serialized_end=894\n  _LOTSNESTEDMESSAGE_B25._serialized_start=896\n  _LOTSNESTEDMESSAGE_B25._serialized_end=901\n  _LOTSNESTEDMESSAGE_B26._serialized_start=903\n  _LOTSNESTEDMESSAGE_B26._serialized_end=908\n  _LOTSNESTEDMESSAGE_B27._serialized_start=910\n  _LOTSNESTEDMESSAGE_B27._serialized_end=915\n  _LOTSNESTEDMESSAGE_B28._serialized_start=917\n  _LOTSNESTEDMESSAGE_B28._serialized_end=922\n  _LOTSNESTEDMESSAGE_B29._serialized_start=924\n  _LOTSNESTEDMESSAGE_B29._serialized_end=929\n  _LOTSNESTEDMESSAGE_B30._serialized_start=931\n  _LOTSNESTEDMESSAGE_B30._serialized_end=936\n  _LOTSNESTEDMESSAGE_B31._serialized_start=938\n  _LOTSNESTEDMESSAGE_B31._serialized_end=943\n  _LOTSNESTEDMESSAGE_B32._serialized_start=945\n  _LOTSNESTEDMESSAGE_B32._serialized_end=950\n  _LOTSNESTEDMESSAGE_B33._serialized_start=952\n  _LOTSNESTEDMESSAGE_B33._serialized_end=957\n  _LOTSNESTEDMESSAGE_B34._serialized_start=959\n  _LOTSNESTEDMESSAGE_B34._serialized_end=964\n  _LOTSNESTEDMESSAGE_B35._serialized_start=966\n  _LOTSNESTEDMESSAGE_B35._serialized_end=971\n  _LOTSNESTEDMESSAGE_B36._serialized_start=973\n  _LOTSNESTEDMESSAGE_B36._serialized_end=978\n  _LOTSNESTEDMESSAGE_B37._serialized_start=980\n  _LOTSNESTEDMESSAGE_B37._serialized_end=985\n  _LOTSNESTEDMESSAGE_B38._serialized_start=987\n  _LOTSNESTEDMESSAGE_B38._serialized_end=992\n  _LOTSNESTEDMESSAGE_B39._serialized_start=994\n  _LOTSNESTEDMESSAGE_B39._serialized_end=999\n  _LOTSNESTEDMESSAGE_B40._serialized_start=1001\n  _LOTSNESTEDMESSAGE_B40._serialized_end=1006\n  _LOTSNESTEDMESSAGE_B41._serialized_start=1008\n  _LOTSNESTEDMESSAGE_B41._serialized_end=1013\n  _LOTSNESTEDMESSAGE_B42._serialized_start=1015\n  _LOTSNESTEDMESSAGE_B42._serialized_end=1020\n  _LOTSNESTEDMESSAGE_B43._serialized_start=1022\n  _LOTSNESTEDMESSAGE_B43._serialized_end=1027\n  _LOTSNESTEDMESSAGE_B44._serialized_start=1029\n  _LOTSNESTEDMESSAGE_B44._serialized_end=1034\n  _LOTSNESTEDMESSAGE_B45._serialized_start=1036\n  _LOTSNESTEDMESSAGE_B45._serialized_end=1041\n  _LOTSNESTEDMESSAGE_B46._serialized_start=1043\n  _LOTSNESTEDMESSAGE_B46._serialized_end=1048\n  _LOTSNESTEDMESSAGE_B47._serialized_start=1050\n  _LOTSNESTEDMESSAGE_B47._serialized_end=1055\n  _LOTSNESTEDMESSAGE_B48._serialized_start=1057\n  _LOTSNESTEDMESSAGE_B48._serialized_end=1062\n  _LOTSNESTEDMESSAGE_B49._serialized_start=1064\n  _LOTSNESTEDMESSAGE_B49._serialized_end=1069\n  _LOTSNESTEDMESSAGE_B50._serialized_start=1071\n  _LOTSNESTEDMESSAGE_B50._serialized_end=1076\n  _LOTSNESTEDMESSAGE_B51._serialized_start=1078\n  _LOTSNESTEDMESSAGE_B51._serialized_end=1083\n  _LOTSNESTEDMESSAGE_B52._serialized_start=1085\n  _LOTSNESTEDMESSAGE_B52._serialized_end=1090\n  _LOTSNESTEDMESSAGE_B53._serialized_start=1092\n  _LOTSNESTEDMESSAGE_B53._serialized_end=1097\n  _LOTSNESTEDMESSAGE_B54._serialized_start=1099\n  _LOTSNESTEDMESSAGE_B54._serialized_end=1104\n  _LOTSNESTEDMESSAGE_B55._serialized_start=1106\n  _LOTSNESTEDMESSAGE_B55._serialized_end=1111\n  _LOTSNESTEDMESSAGE_B56._serialized_start=1113\n  _LOTSNESTEDMESSAGE_B56._serialized_end=1118\n  _LOTSNESTEDMESSAGE_B57._serialized_start=1120\n  _LOTSNESTEDMESSAGE_B57._serialized_end=1125\n  _LOTSNESTEDMESSAGE_B58._serialized_start=1127\n  _LOTSNESTEDMESSAGE_B58._serialized_end=1132\n  _LOTSNESTEDMESSAGE_B59._serialized_start=1134\n  _LOTSNESTEDMESSAGE_B59._serialized_end=1139\n  _LOTSNESTEDMESSAGE_B60._serialized_start=1141\n  _LOTSNESTEDMESSAGE_B60._serialized_end=1146\n  _LOTSNESTEDMESSAGE_B61._serialized_start=1148\n  _LOTSNESTEDMESSAGE_B61._serialized_end=1153\n  _LOTSNESTEDMESSAGE_B62._serialized_start=1155\n  _LOTSNESTEDMESSAGE_B62._serialized_end=1160\n  _LOTSNESTEDMESSAGE_B63._serialized_start=1162\n  _LOTSNESTEDMESSAGE_B63._serialized_end=1167\n  _LOTSNESTEDMESSAGE_B64._serialized_start=1169\n  _LOTSNESTEDMESSAGE_B64._serialized_end=1174\n  _LOTSNESTEDMESSAGE_B65._serialized_start=1176\n  _LOTSNESTEDMESSAGE_B65._serialized_end=1181\n  _LOTSNESTEDMESSAGE_B66._serialized_start=1183\n  _LOTSNESTEDMESSAGE_B66._serialized_end=1188\n  _LOTSNESTEDMESSAGE_B67._serialized_start=1190\n  _LOTSNESTEDMESSAGE_B67._serialized_end=1195\n  _LOTSNESTEDMESSAGE_B68._serialized_start=1197\n  _LOTSNESTEDMESSAGE_B68._serialized_end=1202\n  _LOTSNESTEDMESSAGE_B69._serialized_start=1204\n  _LOTSNESTEDMESSAGE_B69._serialized_end=1209\n  _LOTSNESTEDMESSAGE_B70._serialized_start=1211\n  _LOTSNESTEDMESSAGE_B70._serialized_end=1216\n  _LOTSNESTEDMESSAGE_B71._serialized_start=1218\n  _LOTSNESTEDMESSAGE_B71._serialized_end=1223\n  _LOTSNESTEDMESSAGE_B72._serialized_start=1225\n  _LOTSNESTEDMESSAGE_B72._serialized_end=1230\n  _LOTSNESTEDMESSAGE_B73._serialized_start=1232\n  _LOTSNESTEDMESSAGE_B73._serialized_end=1237\n  _LOTSNESTEDMESSAGE_B74._serialized_start=1239\n  _LOTSNESTEDMESSAGE_B74._serialized_end=1244\n  _LOTSNESTEDMESSAGE_B75._serialized_start=1246\n  _LOTSNESTEDMESSAGE_B75._serialized_end=1251\n  _LOTSNESTEDMESSAGE_B76._serialized_start=1253\n  _LOTSNESTEDMESSAGE_B76._serialized_end=1258\n  _LOTSNESTEDMESSAGE_B77._serialized_start=1260\n  _LOTSNESTEDMESSAGE_B77._serialized_end=1265\n  _LOTSNESTEDMESSAGE_B78._serialized_start=1267\n  _LOTSNESTEDMESSAGE_B78._serialized_end=1272\n  _LOTSNESTEDMESSAGE_B79._serialized_start=1274\n  _LOTSNESTEDMESSAGE_B79._serialized_end=1279\n  _LOTSNESTEDMESSAGE_B80._serialized_start=1281\n  _LOTSNESTEDMESSAGE_B80._serialized_end=1286\n  _LOTSNESTEDMESSAGE_B81._serialized_start=1288\n  _LOTSNESTEDMESSAGE_B81._serialized_end=1293\n  _LOTSNESTEDMESSAGE_B82._serialized_start=1295\n  _LOTSNESTEDMESSAGE_B82._serialized_end=1300\n  _LOTSNESTEDMESSAGE_B83._serialized_start=1302\n  _LOTSNESTEDMESSAGE_B83._serialized_end=1307\n  _LOTSNESTEDMESSAGE_B84._serialized_start=1309\n  _LOTSNESTEDMESSAGE_B84._serialized_end=1314\n  _LOTSNESTEDMESSAGE_B85._serialized_start=1316\n  _LOTSNESTEDMESSAGE_B85._serialized_end=1321\n  _LOTSNESTEDMESSAGE_B86._serialized_start=1323\n  _LOTSNESTEDMESSAGE_B86._serialized_end=1328\n  _LOTSNESTEDMESSAGE_B87._serialized_start=1330\n  _LOTSNESTEDMESSAGE_B87._serialized_end=1335\n  _LOTSNESTEDMESSAGE_B88._serialized_start=1337\n  _LOTSNESTEDMESSAGE_B88._serialized_end=1342\n  _LOTSNESTEDMESSAGE_B89._serialized_start=1344\n  _LOTSNESTEDMESSAGE_B89._serialized_end=1349\n  _LOTSNESTEDMESSAGE_B90._serialized_start=1351\n  _LOTSNESTEDMESSAGE_B90._serialized_end=1356\n  _LOTSNESTEDMESSAGE_B91._serialized_start=1358\n  _LOTSNESTEDMESSAGE_B91._serialized_end=1363\n  _LOTSNESTEDMESSAGE_B92._serialized_start=1365\n  _LOTSNESTEDMESSAGE_B92._serialized_end=1370\n  _LOTSNESTEDMESSAGE_B93._serialized_start=1372\n  _LOTSNESTEDMESSAGE_B93._serialized_end=1377\n  _LOTSNESTEDMESSAGE_B94._serialized_start=1379\n  _LOTSNESTEDMESSAGE_B94._serialized_end=1384\n  _LOTSNESTEDMESSAGE_B95._serialized_start=1386\n  _LOTSNESTEDMESSAGE_B95._serialized_end=1391\n  _LOTSNESTEDMESSAGE_B96._serialized_start=1393\n  _LOTSNESTEDMESSAGE_B96._serialized_end=1398\n  _LOTSNESTEDMESSAGE_B97._serialized_start=1400\n  _LOTSNESTEDMESSAGE_B97._serialized_end=1405\n  _LOTSNESTEDMESSAGE_B98._serialized_start=1407\n  _LOTSNESTEDMESSAGE_B98._serialized_end=1412\n  _LOTSNESTEDMESSAGE_B99._serialized_start=1414\n  _LOTSNESTEDMESSAGE_B99._serialized_end=1419\n  _LOTSNESTEDMESSAGE_B100._serialized_start=1421\n  _LOTSNESTEDMESSAGE_B100._serialized_end=1427\n  _LOTSNESTEDMESSAGE_B101._serialized_start=1429\n  _LOTSNESTEDMESSAGE_B101._serialized_end=1435\n  _LOTSNESTEDMESSAGE_B102._serialized_start=1437\n  _LOTSNESTEDMESSAGE_B102._serialized_end=1443\n  _LOTSNESTEDMESSAGE_B103._serialized_start=1445\n  _LOTSNESTEDMESSAGE_B103._serialized_end=1451\n  _LOTSNESTEDMESSAGE_B104._serialized_start=1453\n  _LOTSNESTEDMESSAGE_B104._serialized_end=1459\n  _LOTSNESTEDMESSAGE_B105._serialized_start=1461\n  _LOTSNESTEDMESSAGE_B105._serialized_end=1467\n  _LOTSNESTEDMESSAGE_B106._serialized_start=1469\n  _LOTSNESTEDMESSAGE_B106._serialized_end=1475\n  _LOTSNESTEDMESSAGE_B107._serialized_start=1477\n  _LOTSNESTEDMESSAGE_B107._serialized_end=1483\n  _LOTSNESTEDMESSAGE_B108._serialized_start=1485\n  _LOTSNESTEDMESSAGE_B108._serialized_end=1491\n  _LOTSNESTEDMESSAGE_B109._serialized_start=1493\n  _LOTSNESTEDMESSAGE_B109._serialized_end=1499\n  _LOTSNESTEDMESSAGE_B110._serialized_start=1501\n  _LOTSNESTEDMESSAGE_B110._serialized_end=1507\n  _LOTSNESTEDMESSAGE_B111._serialized_start=1509\n  _LOTSNESTEDMESSAGE_B111._serialized_end=1515\n  _LOTSNESTEDMESSAGE_B112._serialized_start=1517\n  _LOTSNESTEDMESSAGE_B112._serialized_end=1523\n  _LOTSNESTEDMESSAGE_B113._serialized_start=1525\n  _LOTSNESTEDMESSAGE_B113._serialized_end=1531\n  _LOTSNESTEDMESSAGE_B114._serialized_start=1533\n  _LOTSNESTEDMESSAGE_B114._serialized_end=1539\n  _LOTSNESTEDMESSAGE_B115._serialized_start=1541\n  _LOTSNESTEDMESSAGE_B115._serialized_end=1547\n  _LOTSNESTEDMESSAGE_B116._serialized_start=1549\n  _LOTSNESTEDMESSAGE_B116._serialized_end=1555\n  _LOTSNESTEDMESSAGE_B117._serialized_start=1557\n  _LOTSNESTEDMESSAGE_B117._serialized_end=1563\n  _LOTSNESTEDMESSAGE_B118._serialized_start=1565\n  _LOTSNESTEDMESSAGE_B118._serialized_end=1571\n  _LOTSNESTEDMESSAGE_B119._serialized_start=1573\n  _LOTSNESTEDMESSAGE_B119._serialized_end=1579\n  _LOTSNESTEDMESSAGE_B120._serialized_start=1581\n  _LOTSNESTEDMESSAGE_B120._serialized_end=1587\n  _LOTSNESTEDMESSAGE_B121._serialized_start=1589\n  _LOTSNESTEDMESSAGE_B121._serialized_end=1595\n  _LOTSNESTEDMESSAGE_B122._serialized_start=1597\n  _LOTSNESTEDMESSAGE_B122._serialized_end=1603\n  _LOTSNESTEDMESSAGE_B123._serialized_start=1605\n  _LOTSNESTEDMESSAGE_B123._serialized_end=1611\n  _LOTSNESTEDMESSAGE_B124._serialized_start=1613\n  _LOTSNESTEDMESSAGE_B124._serialized_end=1619\n  _LOTSNESTEDMESSAGE_B125._serialized_start=1621\n  _LOTSNESTEDMESSAGE_B125._serialized_end=1627\n  _LOTSNESTEDMESSAGE_B126._serialized_start=1629\n  _LOTSNESTEDMESSAGE_B126._serialized_end=1635\n  _LOTSNESTEDMESSAGE_B127._serialized_start=1637\n  _LOTSNESTEDMESSAGE_B127._serialized_end=1643\n  _LOTSNESTEDMESSAGE_B128._serialized_start=1645\n  _LOTSNESTEDMESSAGE_B128._serialized_end=1651\n  _LOTSNESTEDMESSAGE_B129._serialized_start=1653\n  _LOTSNESTEDMESSAGE_B129._serialized_end=1659\n  _LOTSNESTEDMESSAGE_B130._serialized_start=1661\n  _LOTSNESTEDMESSAGE_B130._serialized_end=1667\n  _LOTSNESTEDMESSAGE_B131._serialized_start=1669\n  _LOTSNESTEDMESSAGE_B131._serialized_end=1675\n  _LOTSNESTEDMESSAGE_B132._serialized_start=1677\n  _LOTSNESTEDMESSAGE_B132._serialized_end=1683\n  _LOTSNESTEDMESSAGE_B133._serialized_start=1685\n  _LOTSNESTEDMESSAGE_B133._serialized_end=1691\n  _LOTSNESTEDMESSAGE_B134._serialized_start=1693\n  _LOTSNESTEDMESSAGE_B134._serialized_end=1699\n  _LOTSNESTEDMESSAGE_B135._serialized_start=1701\n  _LOTSNESTEDMESSAGE_B135._serialized_end=1707\n  _LOTSNESTEDMESSAGE_B136._serialized_start=1709\n  _LOTSNESTEDMESSAGE_B136._serialized_end=1715\n  _LOTSNESTEDMESSAGE_B137._serialized_start=1717\n  _LOTSNESTEDMESSAGE_B137._serialized_end=1723\n  _LOTSNESTEDMESSAGE_B138._serialized_start=1725\n  _LOTSNESTEDMESSAGE_B138._serialized_end=1731\n  _LOTSNESTEDMESSAGE_B139._serialized_start=1733\n  _LOTSNESTEDMESSAGE_B139._serialized_end=1739\n  _LOTSNESTEDMESSAGE_B140._serialized_start=1741\n  _LOTSNESTEDMESSAGE_B140._serialized_end=1747\n  _LOTSNESTEDMESSAGE_B141._serialized_start=1749\n  _LOTSNESTEDMESSAGE_B141._serialized_end=1755\n  _LOTSNESTEDMESSAGE_B142._serialized_start=1757\n  _LOTSNESTEDMESSAGE_B142._serialized_end=1763\n  _LOTSNESTEDMESSAGE_B143._serialized_start=1765\n  _LOTSNESTEDMESSAGE_B143._serialized_end=1771\n  _LOTSNESTEDMESSAGE_B144._serialized_start=1773\n  _LOTSNESTEDMESSAGE_B144._serialized_end=1779\n  _LOTSNESTEDMESSAGE_B145._serialized_start=1781\n  _LOTSNESTEDMESSAGE_B145._serialized_end=1787\n  _LOTSNESTEDMESSAGE_B146._serialized_start=1789\n  _LOTSNESTEDMESSAGE_B146._serialized_end=1795\n  _LOTSNESTEDMESSAGE_B147._serialized_start=1797\n  _LOTSNESTEDMESSAGE_B147._serialized_end=1803\n  _LOTSNESTEDMESSAGE_B148._serialized_start=1805\n  _LOTSNESTEDMESSAGE_B148._serialized_end=1811\n  _LOTSNESTEDMESSAGE_B149._serialized_start=1813\n  _LOTSNESTEDMESSAGE_B149._serialized_end=1819\n  _LOTSNESTEDMESSAGE_B150._serialized_start=1821\n  _LOTSNESTEDMESSAGE_B150._serialized_end=1827\n  _LOTSNESTEDMESSAGE_B151._serialized_start=1829\n  _LOTSNESTEDMESSAGE_B151._serialized_end=1835\n  _LOTSNESTEDMESSAGE_B152._serialized_start=1837\n  _LOTSNESTEDMESSAGE_B152._serialized_end=1843\n  _LOTSNESTEDMESSAGE_B153._serialized_start=1845\n  _LOTSNESTEDMESSAGE_B153._serialized_end=1851\n  _LOTSNESTEDMESSAGE_B154._serialized_start=1853\n  _LOTSNESTEDMESSAGE_B154._serialized_end=1859\n  _LOTSNESTEDMESSAGE_B155._serialized_start=1861\n  _LOTSNESTEDMESSAGE_B155._serialized_end=1867\n  _LOTSNESTEDMESSAGE_B156._serialized_start=1869\n  _LOTSNESTEDMESSAGE_B156._serialized_end=1875\n  _LOTSNESTEDMESSAGE_B157._serialized_start=1877\n  _LOTSNESTEDMESSAGE_B157._serialized_end=1883\n  _LOTSNESTEDMESSAGE_B158._serialized_start=1885\n  _LOTSNESTEDMESSAGE_B158._serialized_end=1891\n  _LOTSNESTEDMESSAGE_B159._serialized_start=1893\n  _LOTSNESTEDMESSAGE_B159._serialized_end=1899\n  _LOTSNESTEDMESSAGE_B160._serialized_start=1901\n  _LOTSNESTEDMESSAGE_B160._serialized_end=1907\n  _LOTSNESTEDMESSAGE_B161._serialized_start=1909\n  _LOTSNESTEDMESSAGE_B161._serialized_end=1915\n  _LOTSNESTEDMESSAGE_B162._serialized_start=1917\n  _LOTSNESTEDMESSAGE_B162._serialized_end=1923\n  _LOTSNESTEDMESSAGE_B163._serialized_start=1925\n  _LOTSNESTEDMESSAGE_B163._serialized_end=1931\n  _LOTSNESTEDMESSAGE_B164._serialized_start=1933\n  _LOTSNESTEDMESSAGE_B164._serialized_end=1939\n  _LOTSNESTEDMESSAGE_B165._serialized_start=1941\n  _LOTSNESTEDMESSAGE_B165._serialized_end=1947\n  _LOTSNESTEDMESSAGE_B166._serialized_start=1949\n  _LOTSNESTEDMESSAGE_B166._serialized_end=1955\n  _LOTSNESTEDMESSAGE_B167._serialized_start=1957\n  _LOTSNESTEDMESSAGE_B167._serialized_end=1963\n  _LOTSNESTEDMESSAGE_B168._serialized_start=1965\n  _LOTSNESTEDMESSAGE_B168._serialized_end=1971\n  _LOTSNESTEDMESSAGE_B169._serialized_start=1973\n  _LOTSNESTEDMESSAGE_B169._serialized_end=1979\n  _LOTSNESTEDMESSAGE_B170._serialized_start=1981\n  _LOTSNESTEDMESSAGE_B170._serialized_end=1987\n  _LOTSNESTEDMESSAGE_B171._serialized_start=1989\n  _LOTSNESTEDMESSAGE_B171._serialized_end=1995\n  _LOTSNESTEDMESSAGE_B172._serialized_start=1997\n  _LOTSNESTEDMESSAGE_B172._serialized_end=2003\n  _LOTSNESTEDMESSAGE_B173._serialized_start=2005\n  _LOTSNESTEDMESSAGE_B173._serialized_end=2011\n  _LOTSNESTEDMESSAGE_B174._serialized_start=2013\n  _LOTSNESTEDMESSAGE_B174._serialized_end=2019\n  _LOTSNESTEDMESSAGE_B175._serialized_start=2021\n  _LOTSNESTEDMESSAGE_B175._serialized_end=2027\n  _LOTSNESTEDMESSAGE_B176._serialized_start=2029\n  _LOTSNESTEDMESSAGE_B176._serialized_end=2035\n  _LOTSNESTEDMESSAGE_B177._serialized_start=2037\n  _LOTSNESTEDMESSAGE_B177._serialized_end=2043\n  _LOTSNESTEDMESSAGE_B178._serialized_start=2045\n  _LOTSNESTEDMESSAGE_B178._serialized_end=2051\n  _LOTSNESTEDMESSAGE_B179._serialized_start=2053\n  _LOTSNESTEDMESSAGE_B179._serialized_end=2059\n  _LOTSNESTEDMESSAGE_B180._serialized_start=2061\n  _LOTSNESTEDMESSAGE_B180._serialized_end=2067\n  _LOTSNESTEDMESSAGE_B181._serialized_start=2069\n  _LOTSNESTEDMESSAGE_B181._serialized_end=2075\n  _LOTSNESTEDMESSAGE_B182._serialized_start=2077\n  _LOTSNESTEDMESSAGE_B182._serialized_end=2083\n  _LOTSNESTEDMESSAGE_B183._serialized_start=2085\n  _LOTSNESTEDMESSAGE_B183._serialized_end=2091\n  _LOTSNESTEDMESSAGE_B184._serialized_start=2093\n  _LOTSNESTEDMESSAGE_B184._serialized_end=2099\n  _LOTSNESTEDMESSAGE_B185._serialized_start=2101\n  _LOTSNESTEDMESSAGE_B185._serialized_end=2107\n  _LOTSNESTEDMESSAGE_B186._serialized_start=2109\n  _LOTSNESTEDMESSAGE_B186._serialized_end=2115\n  _LOTSNESTEDMESSAGE_B187._serialized_start=2117\n  _LOTSNESTEDMESSAGE_B187._serialized_end=2123\n  _LOTSNESTEDMESSAGE_B188._serialized_start=2125\n  _LOTSNESTEDMESSAGE_B188._serialized_end=2131\n  _LOTSNESTEDMESSAGE_B189._serialized_start=2133\n  _LOTSNESTEDMESSAGE_B189._serialized_end=2139\n  _LOTSNESTEDMESSAGE_B190._serialized_start=2141\n  _LOTSNESTEDMESSAGE_B190._serialized_end=2147\n  _LOTSNESTEDMESSAGE_B191._serialized_start=2149\n  _LOTSNESTEDMESSAGE_B191._serialized_end=2155\n  _LOTSNESTEDMESSAGE_B192._serialized_start=2157\n  _LOTSNESTEDMESSAGE_B192._serialized_end=2163\n  _LOTSNESTEDMESSAGE_B193._serialized_start=2165\n  _LOTSNESTEDMESSAGE_B193._serialized_end=2171\n  _LOTSNESTEDMESSAGE_B194._serialized_start=2173\n  _LOTSNESTEDMESSAGE_B194._serialized_end=2179\n  _LOTSNESTEDMESSAGE_B195._serialized_start=2181\n  _LOTSNESTEDMESSAGE_B195._serialized_end=2187\n  _LOTSNESTEDMESSAGE_B196._serialized_start=2189\n  _LOTSNESTEDMESSAGE_B196._serialized_end=2195\n  _LOTSNESTEDMESSAGE_B197._serialized_start=2197\n  _LOTSNESTEDMESSAGE_B197._serialized_end=2203\n  _LOTSNESTEDMESSAGE_B198._serialized_start=2205\n  _LOTSNESTEDMESSAGE_B198._serialized_end=2211\n  _LOTSNESTEDMESSAGE_B199._serialized_start=2213\n  _LOTSNESTEDMESSAGE_B199._serialized_end=2219\n  _LOTSNESTEDMESSAGE_B200._serialized_start=2221\n  _LOTSNESTEDMESSAGE_B200._serialized_end=2227\n  _LOTSNESTEDMESSAGE_B201._serialized_start=2229\n  _LOTSNESTEDMESSAGE_B201._serialized_end=2235\n  _LOTSNESTEDMESSAGE_B202._serialized_start=2237\n  _LOTSNESTEDMESSAGE_B202._serialized_end=2243\n  _LOTSNESTEDMESSAGE_B203._serialized_start=2245\n  _LOTSNESTEDMESSAGE_B203._serialized_end=2251\n  _LOTSNESTEDMESSAGE_B204._serialized_start=2253\n  _LOTSNESTEDMESSAGE_B204._serialized_end=2259\n  _LOTSNESTEDMESSAGE_B205._serialized_start=2261\n  _LOTSNESTEDMESSAGE_B205._serialized_end=2267\n  _LOTSNESTEDMESSAGE_B206._serialized_start=2269\n  _LOTSNESTEDMESSAGE_B206._serialized_end=2275\n  _LOTSNESTEDMESSAGE_B207._serialized_start=2277\n  _LOTSNESTEDMESSAGE_B207._serialized_end=2283\n  _LOTSNESTEDMESSAGE_B208._serialized_start=2285\n  _LOTSNESTEDMESSAGE_B208._serialized_end=2291\n  _LOTSNESTEDMESSAGE_B209._serialized_start=2293\n  _LOTSNESTEDMESSAGE_B209._serialized_end=2299\n  _LOTSNESTEDMESSAGE_B210._serialized_start=2301\n  _LOTSNESTEDMESSAGE_B210._serialized_end=2307\n  _LOTSNESTEDMESSAGE_B211._serialized_start=2309\n  _LOTSNESTEDMESSAGE_B211._serialized_end=2315\n  _LOTSNESTEDMESSAGE_B212._serialized_start=2317\n  _LOTSNESTEDMESSAGE_B212._serialized_end=2323\n  _LOTSNESTEDMESSAGE_B213._serialized_start=2325\n  _LOTSNESTEDMESSAGE_B213._serialized_end=2331\n  _LOTSNESTEDMESSAGE_B214._serialized_start=2333\n  _LOTSNESTEDMESSAGE_B214._serialized_end=2339\n  _LOTSNESTEDMESSAGE_B215._serialized_start=2341\n  _LOTSNESTEDMESSAGE_B215._serialized_end=2347\n  _LOTSNESTEDMESSAGE_B216._serialized_start=2349\n  _LOTSNESTEDMESSAGE_B216._serialized_end=2355\n  _LOTSNESTEDMESSAGE_B217._serialized_start=2357\n  _LOTSNESTEDMESSAGE_B217._serialized_end=2363\n  _LOTSNESTEDMESSAGE_B218._serialized_start=2365\n  _LOTSNESTEDMESSAGE_B218._serialized_end=2371\n  _LOTSNESTEDMESSAGE_B219._serialized_start=2373\n  _LOTSNESTEDMESSAGE_B219._serialized_end=2379\n  _LOTSNESTEDMESSAGE_B220._serialized_start=2381\n  _LOTSNESTEDMESSAGE_B220._serialized_end=2387\n  _LOTSNESTEDMESSAGE_B221._serialized_start=2389\n  _LOTSNESTEDMESSAGE_B221._serialized_end=2395\n  _LOTSNESTEDMESSAGE_B222._serialized_start=2397\n  _LOTSNESTEDMESSAGE_B222._serialized_end=2403\n  _LOTSNESTEDMESSAGE_B223._serialized_start=2405\n  _LOTSNESTEDMESSAGE_B223._serialized_end=2411\n  _LOTSNESTEDMESSAGE_B224._serialized_start=2413\n  _LOTSNESTEDMESSAGE_B224._serialized_end=2419\n  _LOTSNESTEDMESSAGE_B225._serialized_start=2421\n  _LOTSNESTEDMESSAGE_B225._serialized_end=2427\n  _LOTSNESTEDMESSAGE_B226._serialized_start=2429\n  _LOTSNESTEDMESSAGE_B226._serialized_end=2435\n  _LOTSNESTEDMESSAGE_B227._serialized_start=2437\n  _LOTSNESTEDMESSAGE_B227._serialized_end=2443\n  _LOTSNESTEDMESSAGE_B228._serialized_start=2445\n  _LOTSNESTEDMESSAGE_B228._serialized_end=2451\n  _LOTSNESTEDMESSAGE_B229._serialized_start=2453\n  _LOTSNESTEDMESSAGE_B229._serialized_end=2459\n  _LOTSNESTEDMESSAGE_B230._serialized_start=2461\n  _LOTSNESTEDMESSAGE_B230._serialized_end=2467\n  _LOTSNESTEDMESSAGE_B231._serialized_start=2469\n  _LOTSNESTEDMESSAGE_B231._serialized_end=2475\n  _LOTSNESTEDMESSAGE_B232._serialized_start=2477\n  _LOTSNESTEDMESSAGE_B232._serialized_end=2483\n  _LOTSNESTEDMESSAGE_B233._serialized_start=2485\n  _LOTSNESTEDMESSAGE_B233._serialized_end=2491\n  _LOTSNESTEDMESSAGE_B234._serialized_start=2493\n  _LOTSNESTEDMESSAGE_B234._serialized_end=2499\n  _LOTSNESTEDMESSAGE_B235._serialized_start=2501\n  _LOTSNESTEDMESSAGE_B235._serialized_end=2507\n  _LOTSNESTEDMESSAGE_B236._serialized_start=2509\n  _LOTSNESTEDMESSAGE_B236._serialized_end=2515\n  _LOTSNESTEDMESSAGE_B237._serialized_start=2517\n  _LOTSNESTEDMESSAGE_B237._serialized_end=2523\n  _LOTSNESTEDMESSAGE_B238._serialized_start=2525\n  _LOTSNESTEDMESSAGE_B238._serialized_end=2531\n  _LOTSNESTEDMESSAGE_B239._serialized_start=2533\n  _LOTSNESTEDMESSAGE_B239._serialized_end=2539\n  _LOTSNESTEDMESSAGE_B240._serialized_start=2541\n  _LOTSNESTEDMESSAGE_B240._serialized_end=2547\n  _LOTSNESTEDMESSAGE_B241._serialized_start=2549\n  _LOTSNESTEDMESSAGE_B241._serialized_end=2555\n  _LOTSNESTEDMESSAGE_B242._serialized_start=2557\n  _LOTSNESTEDMESSAGE_B242._serialized_end=2563\n  _LOTSNESTEDMESSAGE_B243._serialized_start=2565\n  _LOTSNESTEDMESSAGE_B243._serialized_end=2571\n  _LOTSNESTEDMESSAGE_B244._serialized_start=2573\n  _LOTSNESTEDMESSAGE_B244._serialized_end=2579\n  _LOTSNESTEDMESSAGE_B245._serialized_start=2581\n  _LOTSNESTEDMESSAGE_B245._serialized_end=2587\n  _LOTSNESTEDMESSAGE_B246._serialized_start=2589\n  _LOTSNESTEDMESSAGE_B246._serialized_end=2595\n  _LOTSNESTEDMESSAGE_B247._serialized_start=2597\n  _LOTSNESTEDMESSAGE_B247._serialized_end=2603\n  _LOTSNESTEDMESSAGE_B248._serialized_start=2605\n  _LOTSNESTEDMESSAGE_B248._serialized_end=2611\n  _LOTSNESTEDMESSAGE_B249._serialized_start=2613\n  _LOTSNESTEDMESSAGE_B249._serialized_end=2619\n  _LOTSNESTEDMESSAGE_B250._serialized_start=2621\n  _LOTSNESTEDMESSAGE_B250._serialized_end=2627\n  _LOTSNESTEDMESSAGE_B251._serialized_start=2629\n  _LOTSNESTEDMESSAGE_B251._serialized_end=2635\n  _LOTSNESTEDMESSAGE_B252._serialized_start=2637\n  _LOTSNESTEDMESSAGE_B252._serialized_end=2643\n  _LOTSNESTEDMESSAGE_B253._serialized_start=2645\n  _LOTSNESTEDMESSAGE_B253._serialized_end=2651\n  _LOTSNESTEDMESSAGE_B254._serialized_start=2653\n  _LOTSNESTEDMESSAGE_B254._serialized_end=2659\n  _LOTSNESTEDMESSAGE_B255._serialized_start=2661\n  _LOTSNESTEDMESSAGE_B255._serialized_end=2667\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/no_package_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/internal/no_package.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n)google/protobuf/internal/no_package.proto\\\";\\n\\x10NoPackageMessage\\x12\\'\\n\\x0fno_package_enum\\x18\\x01 \\x01(\\x0e\\x32\\x0e.NoPackageEnum*?\\n\\rNoPackageEnum\\x12\\x16\\n\\x12NO_PACKAGE_VALUE_0\\x10\\x00\\x12\\x16\\n\\x12NO_PACKAGE_VALUE_1\\x10\\x01')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.no_package_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  _NOPACKAGEENUM._serialized_start=106\n  _NOPACKAGEENUM._serialized_end=169\n  _NOPACKAGEMESSAGE._serialized_start=45\n  _NOPACKAGEMESSAGE._serialized_end=104\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/python_message.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n# This code is meant to work on Python 2.4 and above only.\n#\n# TODO(robinson): Helpers for verbose, common checks like seeing if a\n# descriptor's cpp_type is CPPTYPE_MESSAGE.\n\n\"\"\"Contains a metaclass and helper functions used to create\nprotocol message classes from Descriptor objects at runtime.\n\nRecall that a metaclass is the \"type\" of a class.\n(A class is to a metaclass what an instance is to a class.)\n\nIn this case, we use the GeneratedProtocolMessageType metaclass\nto inject all the useful functionality into the classes\noutput by the protocol compiler at compile-time.\n\nThe upshot of all this is that the real implementation\ndetails for ALL pure-Python protocol buffers are *here in\nthis file*.\n\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\nfrom io import BytesIO\nimport struct\nimport sys\nimport weakref\n\n# We use \"as\" to avoid name collisions with variables.\nfrom google.protobuf.internal import api_implementation\nfrom google.protobuf.internal import containers\nfrom google.protobuf.internal import decoder\nfrom google.protobuf.internal import encoder\nfrom google.protobuf.internal import enum_type_wrapper\nfrom google.protobuf.internal import extension_dict\nfrom google.protobuf.internal import message_listener as message_listener_mod\nfrom google.protobuf.internal import type_checkers\nfrom google.protobuf.internal import well_known_types\nfrom google.protobuf.internal import wire_format\nfrom google.protobuf import descriptor as descriptor_mod\nfrom google.protobuf import message as message_mod\nfrom google.protobuf import text_format\n\n_FieldDescriptor = descriptor_mod.FieldDescriptor\n_AnyFullTypeName = 'google.protobuf.Any'\n_ExtensionDict = extension_dict._ExtensionDict\n\nclass GeneratedProtocolMessageType(type):\n\n  \"\"\"Metaclass for protocol message classes created at runtime from Descriptors.\n\n  We add implementations for all methods described in the Message class.  We\n  also create properties to allow getting/setting all fields in the protocol\n  message.  Finally, we create slots to prevent users from accidentally\n  \"setting\" nonexistent fields in the protocol message, which then wouldn't get\n  serialized / deserialized properly.\n\n  The protocol compiler currently uses this metaclass to create protocol\n  message classes at runtime.  Clients can also manually create their own\n  classes at runtime, as in this example:\n\n  mydescriptor = Descriptor(.....)\n  factory = symbol_database.Default()\n  factory.pool.AddDescriptor(mydescriptor)\n  MyProtoClass = factory.GetPrototype(mydescriptor)\n  myproto_instance = MyProtoClass()\n  myproto.foo_field = 23\n  ...\n  \"\"\"\n\n  # Must be consistent with the protocol-compiler code in\n  # proto2/compiler/internal/generator.*.\n  _DESCRIPTOR_KEY = 'DESCRIPTOR'\n\n  def __new__(cls, name, bases, dictionary):\n    \"\"\"Custom allocation for runtime-generated class types.\n\n    We override __new__ because this is apparently the only place\n    where we can meaningfully set __slots__ on the class we're creating(?).\n    (The interplay between metaclasses and slots is not very well-documented).\n\n    Args:\n      name: Name of the class (ignored, but required by the\n        metaclass protocol).\n      bases: Base classes of the class we're constructing.\n        (Should be message.Message).  We ignore this field, but\n        it's required by the metaclass protocol\n      dictionary: The class dictionary of the class we're\n        constructing.  dictionary[_DESCRIPTOR_KEY] must contain\n        a Descriptor object describing this protocol message\n        type.\n\n    Returns:\n      Newly-allocated class.\n\n    Raises:\n      RuntimeError: Generated code only work with python cpp extension.\n    \"\"\"\n    descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY]\n\n    if isinstance(descriptor, str):\n      raise RuntimeError('The generated code only work with python cpp '\n                         'extension, but it is using pure python runtime.')\n\n    # If a concrete class already exists for this descriptor, don't try to\n    # create another.  Doing so will break any messages that already exist with\n    # the existing class.\n    #\n    # The C++ implementation appears to have its own internal `PyMessageFactory`\n    # to achieve similar results.\n    #\n    # This most commonly happens in `text_format.py` when using descriptors from\n    # a custom pool; it calls symbol_database.Global().getPrototype() on a\n    # descriptor which already has an existing concrete class.\n    new_class = getattr(descriptor, '_concrete_class', None)\n    if new_class:\n      return new_class\n\n    if descriptor.full_name in well_known_types.WKTBASES:\n      bases += (well_known_types.WKTBASES[descriptor.full_name],)\n    _AddClassAttributesForNestedExtensions(descriptor, dictionary)\n    _AddSlots(descriptor, dictionary)\n\n    superclass = super(GeneratedProtocolMessageType, cls)\n    new_class = superclass.__new__(cls, name, bases, dictionary)\n    return new_class\n\n  def __init__(cls, name, bases, dictionary):\n    \"\"\"Here we perform the majority of our work on the class.\n    We add enum getters, an __init__ method, implementations\n    of all Message methods, and properties for all fields\n    in the protocol type.\n\n    Args:\n      name: Name of the class (ignored, but required by the\n        metaclass protocol).\n      bases: Base classes of the class we're constructing.\n        (Should be message.Message).  We ignore this field, but\n        it's required by the metaclass protocol\n      dictionary: The class dictionary of the class we're\n        constructing.  dictionary[_DESCRIPTOR_KEY] must contain\n        a Descriptor object describing this protocol message\n        type.\n    \"\"\"\n    descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY]\n\n    # If this is an _existing_ class looked up via `_concrete_class` in the\n    # __new__ method above, then we don't need to re-initialize anything.\n    existing_class = getattr(descriptor, '_concrete_class', None)\n    if existing_class:\n      assert existing_class is cls, (\n          'Duplicate `GeneratedProtocolMessageType` created for descriptor %r'\n          % (descriptor.full_name))\n      return\n\n    cls._decoders_by_tag = {}\n    if (descriptor.has_options and\n        descriptor.GetOptions().message_set_wire_format):\n      cls._decoders_by_tag[decoder.MESSAGE_SET_ITEM_TAG] = (\n          decoder.MessageSetItemDecoder(descriptor), None)\n\n    # Attach stuff to each FieldDescriptor for quick lookup later on.\n    for field in descriptor.fields:\n      _AttachFieldHelpers(cls, field)\n\n    descriptor._concrete_class = cls  # pylint: disable=protected-access\n    _AddEnumValues(descriptor, cls)\n    _AddInitMethod(descriptor, cls)\n    _AddPropertiesForFields(descriptor, cls)\n    _AddPropertiesForExtensions(descriptor, cls)\n    _AddStaticMethods(cls)\n    _AddMessageMethods(descriptor, cls)\n    _AddPrivateHelperMethods(descriptor, cls)\n\n    superclass = super(GeneratedProtocolMessageType, cls)\n    superclass.__init__(name, bases, dictionary)\n\n\n# Stateless helpers for GeneratedProtocolMessageType below.\n# Outside clients should not access these directly.\n#\n# I opted not to make any of these methods on the metaclass, to make it more\n# clear that I'm not really using any state there and to keep clients from\n# thinking that they have direct access to these construction helpers.\n\n\ndef _PropertyName(proto_field_name):\n  \"\"\"Returns the name of the public property attribute which\n  clients can use to get and (in some cases) set the value\n  of a protocol message field.\n\n  Args:\n    proto_field_name: The protocol message field name, exactly\n      as it appears (or would appear) in a .proto file.\n  \"\"\"\n  # TODO(robinson): Escape Python keywords (e.g., yield), and test this support.\n  # nnorwitz makes my day by writing:\n  # \"\"\"\n  # FYI.  See the keyword module in the stdlib. This could be as simple as:\n  #\n  # if keyword.iskeyword(proto_field_name):\n  #   return proto_field_name + \"_\"\n  # return proto_field_name\n  # \"\"\"\n  # Kenton says:  The above is a BAD IDEA.  People rely on being able to use\n  #   getattr() and setattr() to reflectively manipulate field values.  If we\n  #   rename the properties, then every such user has to also make sure to apply\n  #   the same transformation.  Note that currently if you name a field \"yield\",\n  #   you can still access it just fine using getattr/setattr -- it's not even\n  #   that cumbersome to do so.\n  # TODO(kenton):  Remove this method entirely if/when everyone agrees with my\n  #   position.\n  return proto_field_name\n\n\ndef _AddSlots(message_descriptor, dictionary):\n  \"\"\"Adds a __slots__ entry to dictionary, containing the names of all valid\n  attributes for this message type.\n\n  Args:\n    message_descriptor: A Descriptor instance describing this message type.\n    dictionary: Class dictionary to which we'll add a '__slots__' entry.\n  \"\"\"\n  dictionary['__slots__'] = ['_cached_byte_size',\n                             '_cached_byte_size_dirty',\n                             '_fields',\n                             '_unknown_fields',\n                             '_unknown_field_set',\n                             '_is_present_in_parent',\n                             '_listener',\n                             '_listener_for_children',\n                             '__weakref__',\n                             '_oneofs']\n\n\ndef _IsMessageSetExtension(field):\n  return (field.is_extension and\n          field.containing_type.has_options and\n          field.containing_type.GetOptions().message_set_wire_format and\n          field.type == _FieldDescriptor.TYPE_MESSAGE and\n          field.label == _FieldDescriptor.LABEL_OPTIONAL)\n\n\ndef _IsMapField(field):\n  return (field.type == _FieldDescriptor.TYPE_MESSAGE and\n          field.message_type.has_options and\n          field.message_type.GetOptions().map_entry)\n\n\ndef _IsMessageMapField(field):\n  value_type = field.message_type.fields_by_name['value']\n  return value_type.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE\n\n\ndef _AttachFieldHelpers(cls, field_descriptor):\n  is_repeated = (field_descriptor.label == _FieldDescriptor.LABEL_REPEATED)\n  is_packable = (is_repeated and\n                 wire_format.IsTypePackable(field_descriptor.type))\n  is_proto3 = field_descriptor.containing_type.syntax == 'proto3'\n  if not is_packable:\n    is_packed = False\n  elif field_descriptor.containing_type.syntax == 'proto2':\n    is_packed = (field_descriptor.has_options and\n                field_descriptor.GetOptions().packed)\n  else:\n    has_packed_false = (field_descriptor.has_options and\n                        field_descriptor.GetOptions().HasField('packed') and\n                        field_descriptor.GetOptions().packed == False)\n    is_packed = not has_packed_false\n  is_map_entry = _IsMapField(field_descriptor)\n\n  if is_map_entry:\n    field_encoder = encoder.MapEncoder(field_descriptor)\n    sizer = encoder.MapSizer(field_descriptor,\n                             _IsMessageMapField(field_descriptor))\n  elif _IsMessageSetExtension(field_descriptor):\n    field_encoder = encoder.MessageSetItemEncoder(field_descriptor.number)\n    sizer = encoder.MessageSetItemSizer(field_descriptor.number)\n  else:\n    field_encoder = type_checkers.TYPE_TO_ENCODER[field_descriptor.type](\n        field_descriptor.number, is_repeated, is_packed)\n    sizer = type_checkers.TYPE_TO_SIZER[field_descriptor.type](\n        field_descriptor.number, is_repeated, is_packed)\n\n  field_descriptor._encoder = field_encoder\n  field_descriptor._sizer = sizer\n  field_descriptor._default_constructor = _DefaultValueConstructorForField(\n      field_descriptor)\n\n  def AddDecoder(wiretype, is_packed):\n    tag_bytes = encoder.TagBytes(field_descriptor.number, wiretype)\n    decode_type = field_descriptor.type\n    if (decode_type == _FieldDescriptor.TYPE_ENUM and\n        type_checkers.SupportsOpenEnums(field_descriptor)):\n      decode_type = _FieldDescriptor.TYPE_INT32\n\n    oneof_descriptor = None\n    clear_if_default = False\n    if field_descriptor.containing_oneof is not None:\n      oneof_descriptor = field_descriptor\n    elif (is_proto3 and not is_repeated and\n          field_descriptor.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE):\n      clear_if_default = True\n\n    if is_map_entry:\n      is_message_map = _IsMessageMapField(field_descriptor)\n\n      field_decoder = decoder.MapDecoder(\n          field_descriptor, _GetInitializeDefaultForMap(field_descriptor),\n          is_message_map)\n    elif decode_type == _FieldDescriptor.TYPE_STRING:\n      field_decoder = decoder.StringDecoder(\n          field_descriptor.number, is_repeated, is_packed,\n          field_descriptor, field_descriptor._default_constructor,\n          clear_if_default)\n    elif field_descriptor.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n      field_decoder = type_checkers.TYPE_TO_DECODER[decode_type](\n          field_descriptor.number, is_repeated, is_packed,\n          field_descriptor, field_descriptor._default_constructor)\n    else:\n      field_decoder = type_checkers.TYPE_TO_DECODER[decode_type](\n          field_descriptor.number, is_repeated, is_packed,\n          # pylint: disable=protected-access\n          field_descriptor, field_descriptor._default_constructor,\n          clear_if_default)\n\n    cls._decoders_by_tag[tag_bytes] = (field_decoder, oneof_descriptor)\n\n  AddDecoder(type_checkers.FIELD_TYPE_TO_WIRE_TYPE[field_descriptor.type],\n             False)\n\n  if is_repeated and wire_format.IsTypePackable(field_descriptor.type):\n    # To support wire compatibility of adding packed = true, add a decoder for\n    # packed values regardless of the field's options.\n    AddDecoder(wire_format.WIRETYPE_LENGTH_DELIMITED, True)\n\n\ndef _AddClassAttributesForNestedExtensions(descriptor, dictionary):\n  extensions = descriptor.extensions_by_name\n  for extension_name, extension_field in extensions.items():\n    assert extension_name not in dictionary\n    dictionary[extension_name] = extension_field\n\n\ndef _AddEnumValues(descriptor, cls):\n  \"\"\"Sets class-level attributes for all enum fields defined in this message.\n\n  Also exporting a class-level object that can name enum values.\n\n  Args:\n    descriptor: Descriptor object for this message type.\n    cls: Class we're constructing for this message type.\n  \"\"\"\n  for enum_type in descriptor.enum_types:\n    setattr(cls, enum_type.name, enum_type_wrapper.EnumTypeWrapper(enum_type))\n    for enum_value in enum_type.values:\n      setattr(cls, enum_value.name, enum_value.number)\n\n\ndef _GetInitializeDefaultForMap(field):\n  if field.label != _FieldDescriptor.LABEL_REPEATED:\n    raise ValueError('map_entry set on non-repeated field %s' % (\n        field.name))\n  fields_by_name = field.message_type.fields_by_name\n  key_checker = type_checkers.GetTypeChecker(fields_by_name['key'])\n\n  value_field = fields_by_name['value']\n  if _IsMessageMapField(field):\n    def MakeMessageMapDefault(message):\n      return containers.MessageMap(\n          message._listener_for_children, value_field.message_type, key_checker,\n          field.message_type)\n    return MakeMessageMapDefault\n  else:\n    value_checker = type_checkers.GetTypeChecker(value_field)\n    def MakePrimitiveMapDefault(message):\n      return containers.ScalarMap(\n          message._listener_for_children, key_checker, value_checker,\n          field.message_type)\n    return MakePrimitiveMapDefault\n\ndef _DefaultValueConstructorForField(field):\n  \"\"\"Returns a function which returns a default value for a field.\n\n  Args:\n    field: FieldDescriptor object for this field.\n\n  The returned function has one argument:\n    message: Message instance containing this field, or a weakref proxy\n      of same.\n\n  That function in turn returns a default value for this field.  The default\n    value may refer back to |message| via a weak reference.\n  \"\"\"\n\n  if _IsMapField(field):\n    return _GetInitializeDefaultForMap(field)\n\n  if field.label == _FieldDescriptor.LABEL_REPEATED:\n    if field.has_default_value and field.default_value != []:\n      raise ValueError('Repeated field default value not empty list: %s' % (\n          field.default_value))\n    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n      # We can't look at _concrete_class yet since it might not have\n      # been set.  (Depends on order in which we initialize the classes).\n      message_type = field.message_type\n      def MakeRepeatedMessageDefault(message):\n        return containers.RepeatedCompositeFieldContainer(\n            message._listener_for_children, field.message_type)\n      return MakeRepeatedMessageDefault\n    else:\n      type_checker = type_checkers.GetTypeChecker(field)\n      def MakeRepeatedScalarDefault(message):\n        return containers.RepeatedScalarFieldContainer(\n            message._listener_for_children, type_checker)\n      return MakeRepeatedScalarDefault\n\n  if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n    # _concrete_class may not yet be initialized.\n    message_type = field.message_type\n    def MakeSubMessageDefault(message):\n      assert getattr(message_type, '_concrete_class', None), (\n          'Uninitialized concrete class found for field %r (message type %r)'\n          % (field.full_name, message_type.full_name))\n      result = message_type._concrete_class()\n      result._SetListener(\n          _OneofListener(message, field)\n          if field.containing_oneof is not None\n          else message._listener_for_children)\n      return result\n    return MakeSubMessageDefault\n\n  def MakeScalarDefault(message):\n    # TODO(protobuf-team): This may be broken since there may not be\n    # default_value.  Combine with has_default_value somehow.\n    return field.default_value\n  return MakeScalarDefault\n\n\ndef _ReraiseTypeErrorWithFieldName(message_name, field_name):\n  \"\"\"Re-raise the currently-handled TypeError with the field name added.\"\"\"\n  exc = sys.exc_info()[1]\n  if len(exc.args) == 1 and type(exc) is TypeError:\n    # simple TypeError; add field name to exception message\n    exc = TypeError('%s for field %s.%s' % (str(exc), message_name, field_name))\n\n  # re-raise possibly-amended exception with original traceback:\n  raise exc.with_traceback(sys.exc_info()[2])\n\n\ndef _AddInitMethod(message_descriptor, cls):\n  \"\"\"Adds an __init__ method to cls.\"\"\"\n\n  def _GetIntegerEnumValue(enum_type, value):\n    \"\"\"Convert a string or integer enum value to an integer.\n\n    If the value is a string, it is converted to the enum value in\n    enum_type with the same name.  If the value is not a string, it's\n    returned as-is.  (No conversion or bounds-checking is done.)\n    \"\"\"\n    if isinstance(value, str):\n      try:\n        return enum_type.values_by_name[value].number\n      except KeyError:\n        raise ValueError('Enum type %s: unknown label \"%s\"' % (\n            enum_type.full_name, value))\n    return value\n\n  def init(self, **kwargs):\n    self._cached_byte_size = 0\n    self._cached_byte_size_dirty = len(kwargs) > 0\n    self._fields = {}\n    # Contains a mapping from oneof field descriptors to the descriptor\n    # of the currently set field in that oneof field.\n    self._oneofs = {}\n\n    # _unknown_fields is () when empty for efficiency, and will be turned into\n    # a list if fields are added.\n    self._unknown_fields = ()\n    # _unknown_field_set is None when empty for efficiency, and will be\n    # turned into UnknownFieldSet struct if fields are added.\n    self._unknown_field_set = None      # pylint: disable=protected-access\n    self._is_present_in_parent = False\n    self._listener = message_listener_mod.NullMessageListener()\n    self._listener_for_children = _Listener(self)\n    for field_name, field_value in kwargs.items():\n      field = _GetFieldByName(message_descriptor, field_name)\n      if field is None:\n        raise TypeError('%s() got an unexpected keyword argument \"%s\"' %\n                        (message_descriptor.name, field_name))\n      if field_value is None:\n        # field=None is the same as no field at all.\n        continue\n      if field.label == _FieldDescriptor.LABEL_REPEATED:\n        copy = field._default_constructor(self)\n        if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:  # Composite\n          if _IsMapField(field):\n            if _IsMessageMapField(field):\n              for key in field_value:\n                copy[key].MergeFrom(field_value[key])\n            else:\n              copy.update(field_value)\n          else:\n            for val in field_value:\n              if isinstance(val, dict):\n                copy.add(**val)\n              else:\n                copy.add().MergeFrom(val)\n        else:  # Scalar\n          if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM:\n            field_value = [_GetIntegerEnumValue(field.enum_type, val)\n                           for val in field_value]\n          copy.extend(field_value)\n        self._fields[field] = copy\n      elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n        copy = field._default_constructor(self)\n        new_val = field_value\n        if isinstance(field_value, dict):\n          new_val = field.message_type._concrete_class(**field_value)\n        try:\n          copy.MergeFrom(new_val)\n        except TypeError:\n          _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name)\n        self._fields[field] = copy\n      else:\n        if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM:\n          field_value = _GetIntegerEnumValue(field.enum_type, field_value)\n        try:\n          setattr(self, field_name, field_value)\n        except TypeError:\n          _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name)\n\n  init.__module__ = None\n  init.__doc__ = None\n  cls.__init__ = init\n\n\ndef _GetFieldByName(message_descriptor, field_name):\n  \"\"\"Returns a field descriptor by field name.\n\n  Args:\n    message_descriptor: A Descriptor describing all fields in message.\n    field_name: The name of the field to retrieve.\n  Returns:\n    The field descriptor associated with the field name.\n  \"\"\"\n  try:\n    return message_descriptor.fields_by_name[field_name]\n  except KeyError:\n    raise ValueError('Protocol message %s has no \"%s\" field.' %\n                     (message_descriptor.name, field_name))\n\n\ndef _AddPropertiesForFields(descriptor, cls):\n  \"\"\"Adds properties for all fields in this protocol message type.\"\"\"\n  for field in descriptor.fields:\n    _AddPropertiesForField(field, cls)\n\n  if descriptor.is_extendable:\n    # _ExtensionDict is just an adaptor with no state so we allocate a new one\n    # every time it is accessed.\n    cls.Extensions = property(lambda self: _ExtensionDict(self))\n\n\ndef _AddPropertiesForField(field, cls):\n  \"\"\"Adds a public property for a protocol message field.\n  Clients can use this property to get and (in the case\n  of non-repeated scalar fields) directly set the value\n  of a protocol message field.\n\n  Args:\n    field: A FieldDescriptor for this field.\n    cls: The class we're constructing.\n  \"\"\"\n  # Catch it if we add other types that we should\n  # handle specially here.\n  assert _FieldDescriptor.MAX_CPPTYPE == 10\n\n  constant_name = field.name.upper() + '_FIELD_NUMBER'\n  setattr(cls, constant_name, field.number)\n\n  if field.label == _FieldDescriptor.LABEL_REPEATED:\n    _AddPropertiesForRepeatedField(field, cls)\n  elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n    _AddPropertiesForNonRepeatedCompositeField(field, cls)\n  else:\n    _AddPropertiesForNonRepeatedScalarField(field, cls)\n\n\nclass _FieldProperty(property):\n  __slots__ = ('DESCRIPTOR',)\n\n  def __init__(self, descriptor, getter, setter, doc):\n    property.__init__(self, getter, setter, doc=doc)\n    self.DESCRIPTOR = descriptor\n\n\ndef _AddPropertiesForRepeatedField(field, cls):\n  \"\"\"Adds a public property for a \"repeated\" protocol message field.  Clients\n  can use this property to get the value of the field, which will be either a\n  RepeatedScalarFieldContainer or RepeatedCompositeFieldContainer (see\n  below).\n\n  Note that when clients add values to these containers, we perform\n  type-checking in the case of repeated scalar fields, and we also set any\n  necessary \"has\" bits as a side-effect.\n\n  Args:\n    field: A FieldDescriptor for this field.\n    cls: The class we're constructing.\n  \"\"\"\n  proto_field_name = field.name\n  property_name = _PropertyName(proto_field_name)\n\n  def getter(self):\n    field_value = self._fields.get(field)\n    if field_value is None:\n      # Construct a new object to represent this field.\n      field_value = field._default_constructor(self)\n\n      # Atomically check if another thread has preempted us and, if not, swap\n      # in the new object we just created.  If someone has preempted us, we\n      # take that object and discard ours.\n      # WARNING:  We are relying on setdefault() being atomic.  This is true\n      #   in CPython but we haven't investigated others.  This warning appears\n      #   in several other locations in this file.\n      field_value = self._fields.setdefault(field, field_value)\n    return field_value\n  getter.__module__ = None\n  getter.__doc__ = 'Getter for %s.' % proto_field_name\n\n  # We define a setter just so we can throw an exception with a more\n  # helpful error message.\n  def setter(self, new_value):\n    raise AttributeError('Assignment not allowed to repeated field '\n                         '\"%s\" in protocol message object.' % proto_field_name)\n\n  doc = 'Magic attribute generated for \"%s\" proto field.' % proto_field_name\n  setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc))\n\n\ndef _AddPropertiesForNonRepeatedScalarField(field, cls):\n  \"\"\"Adds a public property for a nonrepeated, scalar protocol message field.\n  Clients can use this property to get and directly set the value of the field.\n  Note that when the client sets the value of a field by using this property,\n  all necessary \"has\" bits are set as a side-effect, and we also perform\n  type-checking.\n\n  Args:\n    field: A FieldDescriptor for this field.\n    cls: The class we're constructing.\n  \"\"\"\n  proto_field_name = field.name\n  property_name = _PropertyName(proto_field_name)\n  type_checker = type_checkers.GetTypeChecker(field)\n  default_value = field.default_value\n  is_proto3 = field.containing_type.syntax == 'proto3'\n\n  def getter(self):\n    # TODO(protobuf-team): This may be broken since there may not be\n    # default_value.  Combine with has_default_value somehow.\n    return self._fields.get(field, default_value)\n  getter.__module__ = None\n  getter.__doc__ = 'Getter for %s.' % proto_field_name\n\n  clear_when_set_to_default = is_proto3 and not field.containing_oneof\n\n  def field_setter(self, new_value):\n    # pylint: disable=protected-access\n    # Testing the value for truthiness captures all of the proto3 defaults\n    # (0, 0.0, enum 0, and False).\n    try:\n      new_value = type_checker.CheckValue(new_value)\n    except TypeError as e:\n      raise TypeError(\n          'Cannot set %s to %.1024r: %s' % (field.full_name, new_value, e))\n    if clear_when_set_to_default and not new_value:\n      self._fields.pop(field, None)\n    else:\n      self._fields[field] = new_value\n    # Check _cached_byte_size_dirty inline to improve performance, since scalar\n    # setters are called frequently.\n    if not self._cached_byte_size_dirty:\n      self._Modified()\n\n  if field.containing_oneof:\n    def setter(self, new_value):\n      field_setter(self, new_value)\n      self._UpdateOneofState(field)\n  else:\n    setter = field_setter\n\n  setter.__module__ = None\n  setter.__doc__ = 'Setter for %s.' % proto_field_name\n\n  # Add a property to encapsulate the getter/setter.\n  doc = 'Magic attribute generated for \"%s\" proto field.' % proto_field_name\n  setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc))\n\n\ndef _AddPropertiesForNonRepeatedCompositeField(field, cls):\n  \"\"\"Adds a public property for a nonrepeated, composite protocol message field.\n  A composite field is a \"group\" or \"message\" field.\n\n  Clients can use this property to get the value of the field, but cannot\n  assign to the property directly.\n\n  Args:\n    field: A FieldDescriptor for this field.\n    cls: The class we're constructing.\n  \"\"\"\n  # TODO(robinson): Remove duplication with similar method\n  # for non-repeated scalars.\n  proto_field_name = field.name\n  property_name = _PropertyName(proto_field_name)\n\n  def getter(self):\n    field_value = self._fields.get(field)\n    if field_value is None:\n      # Construct a new object to represent this field.\n      field_value = field._default_constructor(self)\n\n      # Atomically check if another thread has preempted us and, if not, swap\n      # in the new object we just created.  If someone has preempted us, we\n      # take that object and discard ours.\n      # WARNING:  We are relying on setdefault() being atomic.  This is true\n      #   in CPython but we haven't investigated others.  This warning appears\n      #   in several other locations in this file.\n      field_value = self._fields.setdefault(field, field_value)\n    return field_value\n  getter.__module__ = None\n  getter.__doc__ = 'Getter for %s.' % proto_field_name\n\n  # We define a setter just so we can throw an exception with a more\n  # helpful error message.\n  def setter(self, new_value):\n    raise AttributeError('Assignment not allowed to composite field '\n                         '\"%s\" in protocol message object.' % proto_field_name)\n\n  # Add a property to encapsulate the getter.\n  doc = 'Magic attribute generated for \"%s\" proto field.' % proto_field_name\n  setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc))\n\n\ndef _AddPropertiesForExtensions(descriptor, cls):\n  \"\"\"Adds properties for all fields in this protocol message type.\"\"\"\n  extensions = descriptor.extensions_by_name\n  for extension_name, extension_field in extensions.items():\n    constant_name = extension_name.upper() + '_FIELD_NUMBER'\n    setattr(cls, constant_name, extension_field.number)\n\n  # TODO(amauryfa): Migrate all users of these attributes to functions like\n  #   pool.FindExtensionByNumber(descriptor).\n  if descriptor.file is not None:\n    # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available.\n    pool = descriptor.file.pool\n    cls._extensions_by_number = pool._extensions_by_number[descriptor]\n    cls._extensions_by_name = pool._extensions_by_name[descriptor]\n\ndef _AddStaticMethods(cls):\n  # TODO(robinson): This probably needs to be thread-safe(?)\n  def RegisterExtension(extension_handle):\n    extension_handle.containing_type = cls.DESCRIPTOR\n    # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available.\n    # pylint: disable=protected-access\n    cls.DESCRIPTOR.file.pool._AddExtensionDescriptor(extension_handle)\n    _AttachFieldHelpers(cls, extension_handle)\n  cls.RegisterExtension = staticmethod(RegisterExtension)\n\n  def FromString(s):\n    message = cls()\n    message.MergeFromString(s)\n    return message\n  cls.FromString = staticmethod(FromString)\n\n\ndef _IsPresent(item):\n  \"\"\"Given a (FieldDescriptor, value) tuple from _fields, return true if the\n  value should be included in the list returned by ListFields().\"\"\"\n\n  if item[0].label == _FieldDescriptor.LABEL_REPEATED:\n    return bool(item[1])\n  elif item[0].cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n    return item[1]._is_present_in_parent\n  else:\n    return True\n\n\ndef _AddListFieldsMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n\n  def ListFields(self):\n    all_fields = [item for item in self._fields.items() if _IsPresent(item)]\n    all_fields.sort(key = lambda item: item[0].number)\n    return all_fields\n\n  cls.ListFields = ListFields\n\n_PROTO3_ERROR_TEMPLATE = \\\n  ('Protocol message %s has no non-repeated submessage field \"%s\" '\n   'nor marked as optional')\n_PROTO2_ERROR_TEMPLATE = 'Protocol message %s has no non-repeated field \"%s\"'\n\ndef _AddHasFieldMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n\n  is_proto3 = (message_descriptor.syntax == \"proto3\")\n  error_msg = _PROTO3_ERROR_TEMPLATE if is_proto3 else _PROTO2_ERROR_TEMPLATE\n\n  hassable_fields = {}\n  for field in message_descriptor.fields:\n    if field.label == _FieldDescriptor.LABEL_REPEATED:\n      continue\n    # For proto3, only submessages and fields inside a oneof have presence.\n    if (is_proto3 and field.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE and\n        not field.containing_oneof):\n      continue\n    hassable_fields[field.name] = field\n\n  # Has methods are supported for oneof descriptors.\n  for oneof in message_descriptor.oneofs:\n    hassable_fields[oneof.name] = oneof\n\n  def HasField(self, field_name):\n    try:\n      field = hassable_fields[field_name]\n    except KeyError:\n      raise ValueError(error_msg % (message_descriptor.full_name, field_name))\n\n    if isinstance(field, descriptor_mod.OneofDescriptor):\n      try:\n        return HasField(self, self._oneofs[field].name)\n      except KeyError:\n        return False\n    else:\n      if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n        value = self._fields.get(field)\n        return value is not None and value._is_present_in_parent\n      else:\n        return field in self._fields\n\n  cls.HasField = HasField\n\n\ndef _AddClearFieldMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def ClearField(self, field_name):\n    try:\n      field = message_descriptor.fields_by_name[field_name]\n    except KeyError:\n      try:\n        field = message_descriptor.oneofs_by_name[field_name]\n        if field in self._oneofs:\n          field = self._oneofs[field]\n        else:\n          return\n      except KeyError:\n        raise ValueError('Protocol message %s has no \"%s\" field.' %\n                         (message_descriptor.name, field_name))\n\n    if field in self._fields:\n      # To match the C++ implementation, we need to invalidate iterators\n      # for map fields when ClearField() happens.\n      if hasattr(self._fields[field], 'InvalidateIterators'):\n        self._fields[field].InvalidateIterators()\n\n      # Note:  If the field is a sub-message, its listener will still point\n      #   at us.  That's fine, because the worst than can happen is that it\n      #   will call _Modified() and invalidate our byte size.  Big deal.\n      del self._fields[field]\n\n      if self._oneofs.get(field.containing_oneof, None) is field:\n        del self._oneofs[field.containing_oneof]\n\n    # Always call _Modified() -- even if nothing was changed, this is\n    # a mutating method, and thus calling it should cause the field to become\n    # present in the parent message.\n    self._Modified()\n\n  cls.ClearField = ClearField\n\n\ndef _AddClearExtensionMethod(cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def ClearExtension(self, extension_handle):\n    extension_dict._VerifyExtensionHandle(self, extension_handle)\n\n    # Similar to ClearField(), above.\n    if extension_handle in self._fields:\n      del self._fields[extension_handle]\n    self._Modified()\n  cls.ClearExtension = ClearExtension\n\n\ndef _AddHasExtensionMethod(cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def HasExtension(self, extension_handle):\n    extension_dict._VerifyExtensionHandle(self, extension_handle)\n    if extension_handle.label == _FieldDescriptor.LABEL_REPEATED:\n      raise KeyError('\"%s\" is repeated.' % extension_handle.full_name)\n\n    if extension_handle.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n      value = self._fields.get(extension_handle)\n      return value is not None and value._is_present_in_parent\n    else:\n      return extension_handle in self._fields\n  cls.HasExtension = HasExtension\n\ndef _InternalUnpackAny(msg):\n  \"\"\"Unpacks Any message and returns the unpacked message.\n\n  This internal method is different from public Any Unpack method which takes\n  the target message as argument. _InternalUnpackAny method does not have\n  target message type and need to find the message type in descriptor pool.\n\n  Args:\n    msg: An Any message to be unpacked.\n\n  Returns:\n    The unpacked message.\n  \"\"\"\n  # TODO(amauryfa): Don't use the factory of generated messages.\n  # To make Any work with custom factories, use the message factory of the\n  # parent message.\n  # pylint: disable=g-import-not-at-top\n  from google.protobuf import symbol_database\n  factory = symbol_database.Default()\n\n  type_url = msg.type_url\n\n  if not type_url:\n    return None\n\n  # TODO(haberman): For now we just strip the hostname.  Better logic will be\n  # required.\n  type_name = type_url.split('/')[-1]\n  descriptor = factory.pool.FindMessageTypeByName(type_name)\n\n  if descriptor is None:\n    return None\n\n  message_class = factory.GetPrototype(descriptor)\n  message = message_class()\n\n  message.ParseFromString(msg.value)\n  return message\n\n\ndef _AddEqualsMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def __eq__(self, other):\n    if (not isinstance(other, message_mod.Message) or\n        other.DESCRIPTOR != self.DESCRIPTOR):\n      return False\n\n    if self is other:\n      return True\n\n    if self.DESCRIPTOR.full_name == _AnyFullTypeName:\n      any_a = _InternalUnpackAny(self)\n      any_b = _InternalUnpackAny(other)\n      if any_a and any_b:\n        return any_a == any_b\n\n    if not self.ListFields() == other.ListFields():\n      return False\n\n    # TODO(jieluo): Fix UnknownFieldSet to consider MessageSet extensions,\n    # then use it for the comparison.\n    unknown_fields = list(self._unknown_fields)\n    unknown_fields.sort()\n    other_unknown_fields = list(other._unknown_fields)\n    other_unknown_fields.sort()\n    return unknown_fields == other_unknown_fields\n\n  cls.__eq__ = __eq__\n\n\ndef _AddStrMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def __str__(self):\n    return text_format.MessageToString(self)\n  cls.__str__ = __str__\n\n\ndef _AddReprMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def __repr__(self):\n    return text_format.MessageToString(self)\n  cls.__repr__ = __repr__\n\n\ndef _AddUnicodeMethod(unused_message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n\n  def __unicode__(self):\n    return text_format.MessageToString(self, as_utf8=True).decode('utf-8')\n  cls.__unicode__ = __unicode__\n\n\ndef _BytesForNonRepeatedElement(value, field_number, field_type):\n  \"\"\"Returns the number of bytes needed to serialize a non-repeated element.\n  The returned byte count includes space for tag information and any\n  other additional space associated with serializing value.\n\n  Args:\n    value: Value we're serializing.\n    field_number: Field number of this value.  (Since the field number\n      is stored as part of a varint-encoded tag, this has an impact\n      on the total bytes required to serialize the value).\n    field_type: The type of the field.  One of the TYPE_* constants\n      within FieldDescriptor.\n  \"\"\"\n  try:\n    fn = type_checkers.TYPE_TO_BYTE_SIZE_FN[field_type]\n    return fn(field_number, value)\n  except KeyError:\n    raise message_mod.EncodeError('Unrecognized field type: %d' % field_type)\n\n\ndef _AddByteSizeMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n\n  def ByteSize(self):\n    if not self._cached_byte_size_dirty:\n      return self._cached_byte_size\n\n    size = 0\n    descriptor = self.DESCRIPTOR\n    if descriptor.GetOptions().map_entry:\n      # Fields of map entry should always be serialized.\n      size = descriptor.fields_by_name['key']._sizer(self.key)\n      size += descriptor.fields_by_name['value']._sizer(self.value)\n    else:\n      for field_descriptor, field_value in self.ListFields():\n        size += field_descriptor._sizer(field_value)\n      for tag_bytes, value_bytes in self._unknown_fields:\n        size += len(tag_bytes) + len(value_bytes)\n\n    self._cached_byte_size = size\n    self._cached_byte_size_dirty = False\n    self._listener_for_children.dirty = False\n    return size\n\n  cls.ByteSize = ByteSize\n\n\ndef _AddSerializeToStringMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n\n  def SerializeToString(self, **kwargs):\n    # Check if the message has all of its required fields set.\n    if not self.IsInitialized():\n      raise message_mod.EncodeError(\n          'Message %s is missing required fields: %s' % (\n          self.DESCRIPTOR.full_name, ','.join(self.FindInitializationErrors())))\n    return self.SerializePartialToString(**kwargs)\n  cls.SerializeToString = SerializeToString\n\n\ndef _AddSerializePartialToStringMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n\n  def SerializePartialToString(self, **kwargs):\n    out = BytesIO()\n    self._InternalSerialize(out.write, **kwargs)\n    return out.getvalue()\n  cls.SerializePartialToString = SerializePartialToString\n\n  def InternalSerialize(self, write_bytes, deterministic=None):\n    if deterministic is None:\n      deterministic = (\n          api_implementation.IsPythonDefaultSerializationDeterministic())\n    else:\n      deterministic = bool(deterministic)\n\n    descriptor = self.DESCRIPTOR\n    if descriptor.GetOptions().map_entry:\n      # Fields of map entry should always be serialized.\n      descriptor.fields_by_name['key']._encoder(\n          write_bytes, self.key, deterministic)\n      descriptor.fields_by_name['value']._encoder(\n          write_bytes, self.value, deterministic)\n    else:\n      for field_descriptor, field_value in self.ListFields():\n        field_descriptor._encoder(write_bytes, field_value, deterministic)\n      for tag_bytes, value_bytes in self._unknown_fields:\n        write_bytes(tag_bytes)\n        write_bytes(value_bytes)\n  cls._InternalSerialize = InternalSerialize\n\n\ndef _AddMergeFromStringMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def MergeFromString(self, serialized):\n    serialized = memoryview(serialized)\n    length = len(serialized)\n    try:\n      if self._InternalParse(serialized, 0, length) != length:\n        # The only reason _InternalParse would return early is if it\n        # encountered an end-group tag.\n        raise message_mod.DecodeError('Unexpected end-group tag.')\n    except (IndexError, TypeError):\n      # Now ord(buf[p:p+1]) == ord('') gets TypeError.\n      raise message_mod.DecodeError('Truncated message.')\n    except struct.error as e:\n      raise message_mod.DecodeError(e)\n    return length   # Return this for legacy reasons.\n  cls.MergeFromString = MergeFromString\n\n  local_ReadTag = decoder.ReadTag\n  local_SkipField = decoder.SkipField\n  decoders_by_tag = cls._decoders_by_tag\n\n  def InternalParse(self, buffer, pos, end):\n    \"\"\"Create a message from serialized bytes.\n\n    Args:\n      self: Message, instance of the proto message object.\n      buffer: memoryview of the serialized data.\n      pos: int, position to start in the serialized data.\n      end: int, end position of the serialized data.\n\n    Returns:\n      Message object.\n    \"\"\"\n    # Guard against internal misuse, since this function is called internally\n    # quite extensively, and its easy to accidentally pass bytes.\n    assert isinstance(buffer, memoryview)\n    self._Modified()\n    field_dict = self._fields\n    # pylint: disable=protected-access\n    unknown_field_set = self._unknown_field_set\n    while pos != end:\n      (tag_bytes, new_pos) = local_ReadTag(buffer, pos)\n      field_decoder, field_desc = decoders_by_tag.get(tag_bytes, (None, None))\n      if field_decoder is None:\n        if not self._unknown_fields:   # pylint: disable=protected-access\n          self._unknown_fields = []    # pylint: disable=protected-access\n        if unknown_field_set is None:\n          # pylint: disable=protected-access\n          self._unknown_field_set = containers.UnknownFieldSet()\n          # pylint: disable=protected-access\n          unknown_field_set = self._unknown_field_set\n        # pylint: disable=protected-access\n        (tag, _) = decoder._DecodeVarint(tag_bytes, 0)\n        field_number, wire_type = wire_format.UnpackTag(tag)\n        if field_number == 0:\n          raise message_mod.DecodeError('Field number 0 is illegal.')\n        # TODO(jieluo): remove old_pos.\n        old_pos = new_pos\n        (data, new_pos) = decoder._DecodeUnknownField(\n            buffer, new_pos, wire_type)  # pylint: disable=protected-access\n        if new_pos == -1:\n          return pos\n        # pylint: disable=protected-access\n        unknown_field_set._add(field_number, wire_type, data)\n        # TODO(jieluo): remove _unknown_fields.\n        new_pos = local_SkipField(buffer, old_pos, end, tag_bytes)\n        if new_pos == -1:\n          return pos\n        self._unknown_fields.append(\n            (tag_bytes, buffer[old_pos:new_pos].tobytes()))\n        pos = new_pos\n      else:\n        pos = field_decoder(buffer, new_pos, end, self, field_dict)\n        if field_desc:\n          self._UpdateOneofState(field_desc)\n    return pos\n  cls._InternalParse = InternalParse\n\n\ndef _AddIsInitializedMethod(message_descriptor, cls):\n  \"\"\"Adds the IsInitialized and FindInitializationError methods to the\n  protocol message class.\"\"\"\n\n  required_fields = [field for field in message_descriptor.fields\n                           if field.label == _FieldDescriptor.LABEL_REQUIRED]\n\n  def IsInitialized(self, errors=None):\n    \"\"\"Checks if all required fields of a message are set.\n\n    Args:\n      errors:  A list which, if provided, will be populated with the field\n               paths of all missing required fields.\n\n    Returns:\n      True iff the specified message has all required fields set.\n    \"\"\"\n\n    # Performance is critical so we avoid HasField() and ListFields().\n\n    for field in required_fields:\n      if (field not in self._fields or\n          (field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE and\n           not self._fields[field]._is_present_in_parent)):\n        if errors is not None:\n          errors.extend(self.FindInitializationErrors())\n        return False\n\n    for field, value in list(self._fields.items()):  # dict can change size!\n      if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n        if field.label == _FieldDescriptor.LABEL_REPEATED:\n          if (field.message_type.has_options and\n              field.message_type.GetOptions().map_entry):\n            continue\n          for element in value:\n            if not element.IsInitialized():\n              if errors is not None:\n                errors.extend(self.FindInitializationErrors())\n              return False\n        elif value._is_present_in_parent and not value.IsInitialized():\n          if errors is not None:\n            errors.extend(self.FindInitializationErrors())\n          return False\n\n    return True\n\n  cls.IsInitialized = IsInitialized\n\n  def FindInitializationErrors(self):\n    \"\"\"Finds required fields which are not initialized.\n\n    Returns:\n      A list of strings.  Each string is a path to an uninitialized field from\n      the top-level message, e.g. \"foo.bar[5].baz\".\n    \"\"\"\n\n    errors = []  # simplify things\n\n    for field in required_fields:\n      if not self.HasField(field.name):\n        errors.append(field.name)\n\n    for field, value in self.ListFields():\n      if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n        if field.is_extension:\n          name = '(%s)' % field.full_name\n        else:\n          name = field.name\n\n        if _IsMapField(field):\n          if _IsMessageMapField(field):\n            for key in value:\n              element = value[key]\n              prefix = '%s[%s].' % (name, key)\n              sub_errors = element.FindInitializationErrors()\n              errors += [prefix + error for error in sub_errors]\n          else:\n            # ScalarMaps can't have any initialization errors.\n            pass\n        elif field.label == _FieldDescriptor.LABEL_REPEATED:\n          for i in range(len(value)):\n            element = value[i]\n            prefix = '%s[%d].' % (name, i)\n            sub_errors = element.FindInitializationErrors()\n            errors += [prefix + error for error in sub_errors]\n        else:\n          prefix = name + '.'\n          sub_errors = value.FindInitializationErrors()\n          errors += [prefix + error for error in sub_errors]\n\n    return errors\n\n  cls.FindInitializationErrors = FindInitializationErrors\n\n\ndef _FullyQualifiedClassName(klass):\n  module = klass.__module__\n  name = getattr(klass, '__qualname__', klass.__name__)\n  if module in (None, 'builtins', '__builtin__'):\n    return name\n  return module + '.' + name\n\n\ndef _AddMergeFromMethod(cls):\n  LABEL_REPEATED = _FieldDescriptor.LABEL_REPEATED\n  CPPTYPE_MESSAGE = _FieldDescriptor.CPPTYPE_MESSAGE\n\n  def MergeFrom(self, msg):\n    if not isinstance(msg, cls):\n      raise TypeError(\n          'Parameter to MergeFrom() must be instance of same class: '\n          'expected %s got %s.' % (_FullyQualifiedClassName(cls),\n                                   _FullyQualifiedClassName(msg.__class__)))\n\n    assert msg is not self\n    self._Modified()\n\n    fields = self._fields\n\n    for field, value in msg._fields.items():\n      if field.label == LABEL_REPEATED:\n        field_value = fields.get(field)\n        if field_value is None:\n          # Construct a new object to represent this field.\n          field_value = field._default_constructor(self)\n          fields[field] = field_value\n        field_value.MergeFrom(value)\n      elif field.cpp_type == CPPTYPE_MESSAGE:\n        if value._is_present_in_parent:\n          field_value = fields.get(field)\n          if field_value is None:\n            # Construct a new object to represent this field.\n            field_value = field._default_constructor(self)\n            fields[field] = field_value\n          field_value.MergeFrom(value)\n      else:\n        self._fields[field] = value\n        if field.containing_oneof:\n          self._UpdateOneofState(field)\n\n    if msg._unknown_fields:\n      if not self._unknown_fields:\n        self._unknown_fields = []\n      self._unknown_fields.extend(msg._unknown_fields)\n      # pylint: disable=protected-access\n      if self._unknown_field_set is None:\n        self._unknown_field_set = containers.UnknownFieldSet()\n      self._unknown_field_set._extend(msg._unknown_field_set)\n\n  cls.MergeFrom = MergeFrom\n\n\ndef _AddWhichOneofMethod(message_descriptor, cls):\n  def WhichOneof(self, oneof_name):\n    \"\"\"Returns the name of the currently set field inside a oneof, or None.\"\"\"\n    try:\n      field = message_descriptor.oneofs_by_name[oneof_name]\n    except KeyError:\n      raise ValueError(\n          'Protocol message has no oneof \"%s\" field.' % oneof_name)\n\n    nested_field = self._oneofs.get(field, None)\n    if nested_field is not None and self.HasField(nested_field.name):\n      return nested_field.name\n    else:\n      return None\n\n  cls.WhichOneof = WhichOneof\n\n\ndef _Clear(self):\n  # Clear fields.\n  self._fields = {}\n  self._unknown_fields = ()\n  # pylint: disable=protected-access\n  if self._unknown_field_set is not None:\n    self._unknown_field_set._clear()\n    self._unknown_field_set = None\n\n  self._oneofs = {}\n  self._Modified()\n\n\ndef _UnknownFields(self):\n  if self._unknown_field_set is None:  # pylint: disable=protected-access\n    # pylint: disable=protected-access\n    self._unknown_field_set = containers.UnknownFieldSet()\n  return self._unknown_field_set    # pylint: disable=protected-access\n\n\ndef _DiscardUnknownFields(self):\n  self._unknown_fields = []\n  self._unknown_field_set = None      # pylint: disable=protected-access\n  for field, value in self.ListFields():\n    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n      if _IsMapField(field):\n        if _IsMessageMapField(field):\n          for key in value:\n            value[key].DiscardUnknownFields()\n      elif field.label == _FieldDescriptor.LABEL_REPEATED:\n        for sub_message in value:\n          sub_message.DiscardUnknownFields()\n      else:\n        value.DiscardUnknownFields()\n\n\ndef _SetListener(self, listener):\n  if listener is None:\n    self._listener = message_listener_mod.NullMessageListener()\n  else:\n    self._listener = listener\n\n\ndef _AddMessageMethods(message_descriptor, cls):\n  \"\"\"Adds implementations of all Message methods to cls.\"\"\"\n  _AddListFieldsMethod(message_descriptor, cls)\n  _AddHasFieldMethod(message_descriptor, cls)\n  _AddClearFieldMethod(message_descriptor, cls)\n  if message_descriptor.is_extendable:\n    _AddClearExtensionMethod(cls)\n    _AddHasExtensionMethod(cls)\n  _AddEqualsMethod(message_descriptor, cls)\n  _AddStrMethod(message_descriptor, cls)\n  _AddReprMethod(message_descriptor, cls)\n  _AddUnicodeMethod(message_descriptor, cls)\n  _AddByteSizeMethod(message_descriptor, cls)\n  _AddSerializeToStringMethod(message_descriptor, cls)\n  _AddSerializePartialToStringMethod(message_descriptor, cls)\n  _AddMergeFromStringMethod(message_descriptor, cls)\n  _AddIsInitializedMethod(message_descriptor, cls)\n  _AddMergeFromMethod(cls)\n  _AddWhichOneofMethod(message_descriptor, cls)\n  # Adds methods which do not depend on cls.\n  cls.Clear = _Clear\n  cls.UnknownFields = _UnknownFields\n  cls.DiscardUnknownFields = _DiscardUnknownFields\n  cls._SetListener = _SetListener\n\n\ndef _AddPrivateHelperMethods(message_descriptor, cls):\n  \"\"\"Adds implementation of private helper methods to cls.\"\"\"\n\n  def Modified(self):\n    \"\"\"Sets the _cached_byte_size_dirty bit to true,\n    and propagates this to our listener iff this was a state change.\n    \"\"\"\n\n    # Note:  Some callers check _cached_byte_size_dirty before calling\n    #   _Modified() as an extra optimization.  So, if this method is ever\n    #   changed such that it does stuff even when _cached_byte_size_dirty is\n    #   already true, the callers need to be updated.\n    if not self._cached_byte_size_dirty:\n      self._cached_byte_size_dirty = True\n      self._listener_for_children.dirty = True\n      self._is_present_in_parent = True\n      self._listener.Modified()\n\n  def _UpdateOneofState(self, field):\n    \"\"\"Sets field as the active field in its containing oneof.\n\n    Will also delete currently active field in the oneof, if it is different\n    from the argument. Does not mark the message as modified.\n    \"\"\"\n    other_field = self._oneofs.setdefault(field.containing_oneof, field)\n    if other_field is not field:\n      del self._fields[other_field]\n      self._oneofs[field.containing_oneof] = field\n\n  cls._Modified = Modified\n  cls.SetInParent = Modified\n  cls._UpdateOneofState = _UpdateOneofState\n\n\nclass _Listener(object):\n\n  \"\"\"MessageListener implementation that a parent message registers with its\n  child message.\n\n  In order to support semantics like:\n\n    foo.bar.baz.qux = 23\n    assert foo.HasField('bar')\n\n  ...child objects must have back references to their parents.\n  This helper class is at the heart of this support.\n  \"\"\"\n\n  def __init__(self, parent_message):\n    \"\"\"Args:\n      parent_message: The message whose _Modified() method we should call when\n        we receive Modified() messages.\n    \"\"\"\n    # This listener establishes a back reference from a child (contained) object\n    # to its parent (containing) object.  We make this a weak reference to avoid\n    # creating cyclic garbage when the client finishes with the 'parent' object\n    # in the tree.\n    if isinstance(parent_message, weakref.ProxyType):\n      self._parent_message_weakref = parent_message\n    else:\n      self._parent_message_weakref = weakref.proxy(parent_message)\n\n    # As an optimization, we also indicate directly on the listener whether\n    # or not the parent message is dirty.  This way we can avoid traversing\n    # up the tree in the common case.\n    self.dirty = False\n\n  def Modified(self):\n    if self.dirty:\n      return\n    try:\n      # Propagate the signal to our parents iff this is the first field set.\n      self._parent_message_weakref._Modified()\n    except ReferenceError:\n      # We can get here if a client has kept a reference to a child object,\n      # and is now setting a field on it, but the child's parent has been\n      # garbage-collected.  This is not an error.\n      pass\n\n\nclass _OneofListener(_Listener):\n  \"\"\"Special listener implementation for setting composite oneof fields.\"\"\"\n\n  def __init__(self, parent_message, field):\n    \"\"\"Args:\n      parent_message: The message whose _Modified() method we should call when\n        we receive Modified() messages.\n      field: The descriptor of the field being set in the parent message.\n    \"\"\"\n    super(_OneofListener, self).__init__(parent_message)\n    self._field = field\n\n  def Modified(self):\n    \"\"\"Also updates the state of the containing oneof in the parent message.\"\"\"\n    try:\n      self._parent_message_weakref._UpdateOneofState(self._field)\n      super(_OneofListener, self).Modified()\n    except ReferenceError:\n      pass\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/type_checkers.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Provides type checking routines.\n\nThis module defines type checking utilities in the forms of dictionaries:\n\nVALUE_CHECKERS: A dictionary of field types and a value validation object.\nTYPE_TO_BYTE_SIZE_FN: A dictionary with field types and a size computing\n  function.\nTYPE_TO_SERIALIZE_METHOD: A dictionary with field types and serialization\n  function.\nFIELD_TYPE_TO_WIRE_TYPE: A dictionary with field typed and their\n  corresponding wire types.\nTYPE_TO_DESERIALIZE_METHOD: A dictionary with field types and deserialization\n  function.\n\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\nimport ctypes\nimport numbers\n\nfrom google.protobuf.internal import decoder\nfrom google.protobuf.internal import encoder\nfrom google.protobuf.internal import wire_format\nfrom google.protobuf import descriptor\n\n_FieldDescriptor = descriptor.FieldDescriptor\n\n\ndef TruncateToFourByteFloat(original):\n  return ctypes.c_float(original).value\n\n\ndef ToShortestFloat(original):\n  \"\"\"Returns the shortest float that has same value in wire.\"\"\"\n  # All 4 byte floats have between 6 and 9 significant digits, so we\n  # start with 6 as the lower bound.\n  # It has to be iterative because use '.9g' directly can not get rid\n  # of the noises for most values. For example if set a float_field=0.9\n  # use '.9g' will print 0.899999976.\n  precision = 6\n  rounded = float('{0:.{1}g}'.format(original, precision))\n  while TruncateToFourByteFloat(rounded) != original:\n    precision += 1\n    rounded = float('{0:.{1}g}'.format(original, precision))\n  return rounded\n\n\ndef SupportsOpenEnums(field_descriptor):\n  return field_descriptor.containing_type.syntax == 'proto3'\n\n\ndef GetTypeChecker(field):\n  \"\"\"Returns a type checker for a message field of the specified types.\n\n  Args:\n    field: FieldDescriptor object for this field.\n\n  Returns:\n    An instance of TypeChecker which can be used to verify the types\n    of values assigned to a field of the specified type.\n  \"\"\"\n  if (field.cpp_type == _FieldDescriptor.CPPTYPE_STRING and\n      field.type == _FieldDescriptor.TYPE_STRING):\n    return UnicodeValueChecker()\n  if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM:\n    if SupportsOpenEnums(field):\n      # When open enums are supported, any int32 can be assigned.\n      return _VALUE_CHECKERS[_FieldDescriptor.CPPTYPE_INT32]\n    else:\n      return EnumValueChecker(field.enum_type)\n  return _VALUE_CHECKERS[field.cpp_type]\n\n\n# None of the typecheckers below make any attempt to guard against people\n# subclassing builtin types and doing weird things.  We're not trying to\n# protect against malicious clients here, just people accidentally shooting\n# themselves in the foot in obvious ways.\nclass TypeChecker(object):\n\n  \"\"\"Type checker used to catch type errors as early as possible\n  when the client is setting scalar fields in protocol messages.\n  \"\"\"\n\n  def __init__(self, *acceptable_types):\n    self._acceptable_types = acceptable_types\n\n  def CheckValue(self, proposed_value):\n    \"\"\"Type check the provided value and return it.\n\n    The returned value might have been normalized to another type.\n    \"\"\"\n    if not isinstance(proposed_value, self._acceptable_types):\n      message = ('%.1024r has type %s, but expected one of: %s' %\n                 (proposed_value, type(proposed_value), self._acceptable_types))\n      raise TypeError(message)\n    return proposed_value\n\n\nclass TypeCheckerWithDefault(TypeChecker):\n\n  def __init__(self, default_value, *acceptable_types):\n    TypeChecker.__init__(self, *acceptable_types)\n    self._default_value = default_value\n\n  def DefaultValue(self):\n    return self._default_value\n\n\nclass BoolValueChecker(object):\n  \"\"\"Type checker used for bool fields.\"\"\"\n\n  def CheckValue(self, proposed_value):\n    if not hasattr(proposed_value, '__index__') or (\n        type(proposed_value).__module__ == 'numpy' and\n        type(proposed_value).__name__ == 'ndarray'):\n      message = ('%.1024r has type %s, but expected one of: %s' %\n                 (proposed_value, type(proposed_value), (bool, int)))\n      raise TypeError(message)\n    return bool(proposed_value)\n\n  def DefaultValue(self):\n    return False\n\n\n# IntValueChecker and its subclasses perform integer type-checks\n# and bounds-checks.\nclass IntValueChecker(object):\n\n  \"\"\"Checker used for integer fields.  Performs type-check and range check.\"\"\"\n\n  def CheckValue(self, proposed_value):\n    if not hasattr(proposed_value, '__index__') or (\n        type(proposed_value).__module__ == 'numpy' and\n        type(proposed_value).__name__ == 'ndarray'):\n      message = ('%.1024r has type %s, but expected one of: %s' %\n                 (proposed_value, type(proposed_value), (int,)))\n      raise TypeError(message)\n\n    if not self._MIN <= int(proposed_value) <= self._MAX:\n      raise ValueError('Value out of range: %d' % proposed_value)\n    # We force all values to int to make alternate implementations where the\n    # distinction is more significant (e.g. the C++ implementation) simpler.\n    proposed_value = int(proposed_value)\n    return proposed_value\n\n  def DefaultValue(self):\n    return 0\n\n\nclass EnumValueChecker(object):\n\n  \"\"\"Checker used for enum fields.  Performs type-check and range check.\"\"\"\n\n  def __init__(self, enum_type):\n    self._enum_type = enum_type\n\n  def CheckValue(self, proposed_value):\n    if not isinstance(proposed_value, numbers.Integral):\n      message = ('%.1024r has type %s, but expected one of: %s' %\n                 (proposed_value, type(proposed_value), (int,)))\n      raise TypeError(message)\n    if int(proposed_value) not in self._enum_type.values_by_number:\n      raise ValueError('Unknown enum value: %d' % proposed_value)\n    return proposed_value\n\n  def DefaultValue(self):\n    return self._enum_type.values[0].number\n\n\nclass UnicodeValueChecker(object):\n\n  \"\"\"Checker used for string fields.\n\n  Always returns a unicode value, even if the input is of type str.\n  \"\"\"\n\n  def CheckValue(self, proposed_value):\n    if not isinstance(proposed_value, (bytes, str)):\n      message = ('%.1024r has type %s, but expected one of: %s' %\n                 (proposed_value, type(proposed_value), (bytes, str)))\n      raise TypeError(message)\n\n    # If the value is of type 'bytes' make sure that it is valid UTF-8 data.\n    if isinstance(proposed_value, bytes):\n      try:\n        proposed_value = proposed_value.decode('utf-8')\n      except UnicodeDecodeError:\n        raise ValueError('%.1024r has type bytes, but isn\\'t valid UTF-8 '\n                         'encoding. Non-UTF-8 strings must be converted to '\n                         'unicode objects before being added.' %\n                         (proposed_value))\n    else:\n      try:\n        proposed_value.encode('utf8')\n      except UnicodeEncodeError:\n        raise ValueError('%.1024r isn\\'t a valid unicode string and '\n                         'can\\'t be encoded in UTF-8.'%\n                         (proposed_value))\n\n    return proposed_value\n\n  def DefaultValue(self):\n    return u\"\"\n\n\nclass Int32ValueChecker(IntValueChecker):\n  # We're sure to use ints instead of longs here since comparison may be more\n  # efficient.\n  _MIN = -2147483648\n  _MAX = 2147483647\n\n\nclass Uint32ValueChecker(IntValueChecker):\n  _MIN = 0\n  _MAX = (1 << 32) - 1\n\n\nclass Int64ValueChecker(IntValueChecker):\n  _MIN = -(1 << 63)\n  _MAX = (1 << 63) - 1\n\n\nclass Uint64ValueChecker(IntValueChecker):\n  _MIN = 0\n  _MAX = (1 << 64) - 1\n\n\n# The max 4 bytes float is about 3.4028234663852886e+38\n_FLOAT_MAX = float.fromhex('0x1.fffffep+127')\n_FLOAT_MIN = -_FLOAT_MAX\n_INF = float('inf')\n_NEG_INF = float('-inf')\n\n\nclass DoubleValueChecker(object):\n  \"\"\"Checker used for double fields.\n\n  Performs type-check and range check.\n  \"\"\"\n\n  def CheckValue(self, proposed_value):\n    \"\"\"Check and convert proposed_value to float.\"\"\"\n    if (not hasattr(proposed_value, '__float__') and\n        not hasattr(proposed_value, '__index__')) or (\n            type(proposed_value).__module__ == 'numpy' and\n            type(proposed_value).__name__ == 'ndarray'):\n      message = ('%.1024r has type %s, but expected one of: int, float' %\n                 (proposed_value, type(proposed_value)))\n      raise TypeError(message)\n    return float(proposed_value)\n\n  def DefaultValue(self):\n    return 0.0\n\n\nclass FloatValueChecker(DoubleValueChecker):\n  \"\"\"Checker used for float fields.\n\n  Performs type-check and range check.\n\n  Values exceeding a 32-bit float will be converted to inf/-inf.\n  \"\"\"\n\n  def CheckValue(self, proposed_value):\n    \"\"\"Check and convert proposed_value to float.\"\"\"\n    converted_value = super().CheckValue(proposed_value)\n    # This inf rounding matches the C++ proto SafeDoubleToFloat logic.\n    if converted_value > _FLOAT_MAX:\n      return _INF\n    if converted_value < _FLOAT_MIN:\n      return _NEG_INF\n\n    return TruncateToFourByteFloat(converted_value)\n\n# Type-checkers for all scalar CPPTYPEs.\n_VALUE_CHECKERS = {\n    _FieldDescriptor.CPPTYPE_INT32: Int32ValueChecker(),\n    _FieldDescriptor.CPPTYPE_INT64: Int64ValueChecker(),\n    _FieldDescriptor.CPPTYPE_UINT32: Uint32ValueChecker(),\n    _FieldDescriptor.CPPTYPE_UINT64: Uint64ValueChecker(),\n    _FieldDescriptor.CPPTYPE_DOUBLE: DoubleValueChecker(),\n    _FieldDescriptor.CPPTYPE_FLOAT: FloatValueChecker(),\n    _FieldDescriptor.CPPTYPE_BOOL: BoolValueChecker(),\n    _FieldDescriptor.CPPTYPE_STRING: TypeCheckerWithDefault(b'', bytes),\n}\n\n\n# Map from field type to a function F, such that F(field_num, value)\n# gives the total byte size for a value of the given type.  This\n# byte size includes tag information and any other additional space\n# associated with serializing \"value\".\nTYPE_TO_BYTE_SIZE_FN = {\n    _FieldDescriptor.TYPE_DOUBLE: wire_format.DoubleByteSize,\n    _FieldDescriptor.TYPE_FLOAT: wire_format.FloatByteSize,\n    _FieldDescriptor.TYPE_INT64: wire_format.Int64ByteSize,\n    _FieldDescriptor.TYPE_UINT64: wire_format.UInt64ByteSize,\n    _FieldDescriptor.TYPE_INT32: wire_format.Int32ByteSize,\n    _FieldDescriptor.TYPE_FIXED64: wire_format.Fixed64ByteSize,\n    _FieldDescriptor.TYPE_FIXED32: wire_format.Fixed32ByteSize,\n    _FieldDescriptor.TYPE_BOOL: wire_format.BoolByteSize,\n    _FieldDescriptor.TYPE_STRING: wire_format.StringByteSize,\n    _FieldDescriptor.TYPE_GROUP: wire_format.GroupByteSize,\n    _FieldDescriptor.TYPE_MESSAGE: wire_format.MessageByteSize,\n    _FieldDescriptor.TYPE_BYTES: wire_format.BytesByteSize,\n    _FieldDescriptor.TYPE_UINT32: wire_format.UInt32ByteSize,\n    _FieldDescriptor.TYPE_ENUM: wire_format.EnumByteSize,\n    _FieldDescriptor.TYPE_SFIXED32: wire_format.SFixed32ByteSize,\n    _FieldDescriptor.TYPE_SFIXED64: wire_format.SFixed64ByteSize,\n    _FieldDescriptor.TYPE_SINT32: wire_format.SInt32ByteSize,\n    _FieldDescriptor.TYPE_SINT64: wire_format.SInt64ByteSize\n    }\n\n\n# Maps from field types to encoder constructors.\nTYPE_TO_ENCODER = {\n    _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleEncoder,\n    _FieldDescriptor.TYPE_FLOAT: encoder.FloatEncoder,\n    _FieldDescriptor.TYPE_INT64: encoder.Int64Encoder,\n    _FieldDescriptor.TYPE_UINT64: encoder.UInt64Encoder,\n    _FieldDescriptor.TYPE_INT32: encoder.Int32Encoder,\n    _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Encoder,\n    _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Encoder,\n    _FieldDescriptor.TYPE_BOOL: encoder.BoolEncoder,\n    _FieldDescriptor.TYPE_STRING: encoder.StringEncoder,\n    _FieldDescriptor.TYPE_GROUP: encoder.GroupEncoder,\n    _FieldDescriptor.TYPE_MESSAGE: encoder.MessageEncoder,\n    _FieldDescriptor.TYPE_BYTES: encoder.BytesEncoder,\n    _FieldDescriptor.TYPE_UINT32: encoder.UInt32Encoder,\n    _FieldDescriptor.TYPE_ENUM: encoder.EnumEncoder,\n    _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Encoder,\n    _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Encoder,\n    _FieldDescriptor.TYPE_SINT32: encoder.SInt32Encoder,\n    _FieldDescriptor.TYPE_SINT64: encoder.SInt64Encoder,\n    }\n\n\n# Maps from field types to sizer constructors.\nTYPE_TO_SIZER = {\n    _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleSizer,\n    _FieldDescriptor.TYPE_FLOAT: encoder.FloatSizer,\n    _FieldDescriptor.TYPE_INT64: encoder.Int64Sizer,\n    _FieldDescriptor.TYPE_UINT64: encoder.UInt64Sizer,\n    _FieldDescriptor.TYPE_INT32: encoder.Int32Sizer,\n    _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Sizer,\n    _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Sizer,\n    _FieldDescriptor.TYPE_BOOL: encoder.BoolSizer,\n    _FieldDescriptor.TYPE_STRING: encoder.StringSizer,\n    _FieldDescriptor.TYPE_GROUP: encoder.GroupSizer,\n    _FieldDescriptor.TYPE_MESSAGE: encoder.MessageSizer,\n    _FieldDescriptor.TYPE_BYTES: encoder.BytesSizer,\n    _FieldDescriptor.TYPE_UINT32: encoder.UInt32Sizer,\n    _FieldDescriptor.TYPE_ENUM: encoder.EnumSizer,\n    _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Sizer,\n    _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Sizer,\n    _FieldDescriptor.TYPE_SINT32: encoder.SInt32Sizer,\n    _FieldDescriptor.TYPE_SINT64: encoder.SInt64Sizer,\n    }\n\n\n# Maps from field type to a decoder constructor.\nTYPE_TO_DECODER = {\n    _FieldDescriptor.TYPE_DOUBLE: decoder.DoubleDecoder,\n    _FieldDescriptor.TYPE_FLOAT: decoder.FloatDecoder,\n    _FieldDescriptor.TYPE_INT64: decoder.Int64Decoder,\n    _FieldDescriptor.TYPE_UINT64: decoder.UInt64Decoder,\n    _FieldDescriptor.TYPE_INT32: decoder.Int32Decoder,\n    _FieldDescriptor.TYPE_FIXED64: decoder.Fixed64Decoder,\n    _FieldDescriptor.TYPE_FIXED32: decoder.Fixed32Decoder,\n    _FieldDescriptor.TYPE_BOOL: decoder.BoolDecoder,\n    _FieldDescriptor.TYPE_STRING: decoder.StringDecoder,\n    _FieldDescriptor.TYPE_GROUP: decoder.GroupDecoder,\n    _FieldDescriptor.TYPE_MESSAGE: decoder.MessageDecoder,\n    _FieldDescriptor.TYPE_BYTES: decoder.BytesDecoder,\n    _FieldDescriptor.TYPE_UINT32: decoder.UInt32Decoder,\n    _FieldDescriptor.TYPE_ENUM: decoder.EnumDecoder,\n    _FieldDescriptor.TYPE_SFIXED32: decoder.SFixed32Decoder,\n    _FieldDescriptor.TYPE_SFIXED64: decoder.SFixed64Decoder,\n    _FieldDescriptor.TYPE_SINT32: decoder.SInt32Decoder,\n    _FieldDescriptor.TYPE_SINT64: decoder.SInt64Decoder,\n    }\n\n# Maps from field type to expected wiretype.\nFIELD_TYPE_TO_WIRE_TYPE = {\n    _FieldDescriptor.TYPE_DOUBLE: wire_format.WIRETYPE_FIXED64,\n    _FieldDescriptor.TYPE_FLOAT: wire_format.WIRETYPE_FIXED32,\n    _FieldDescriptor.TYPE_INT64: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_UINT64: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_INT32: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_FIXED64: wire_format.WIRETYPE_FIXED64,\n    _FieldDescriptor.TYPE_FIXED32: wire_format.WIRETYPE_FIXED32,\n    _FieldDescriptor.TYPE_BOOL: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_STRING:\n      wire_format.WIRETYPE_LENGTH_DELIMITED,\n    _FieldDescriptor.TYPE_GROUP: wire_format.WIRETYPE_START_GROUP,\n    _FieldDescriptor.TYPE_MESSAGE:\n      wire_format.WIRETYPE_LENGTH_DELIMITED,\n    _FieldDescriptor.TYPE_BYTES:\n      wire_format.WIRETYPE_LENGTH_DELIMITED,\n    _FieldDescriptor.TYPE_UINT32: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_ENUM: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_SFIXED32: wire_format.WIRETYPE_FIXED32,\n    _FieldDescriptor.TYPE_SFIXED64: wire_format.WIRETYPE_FIXED64,\n    _FieldDescriptor.TYPE_SINT32: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_SINT64: wire_format.WIRETYPE_VARINT,\n    }\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/well_known_types.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Contains well known classes.\n\nThis files defines well known classes which need extra maintenance including:\n  - Any\n  - Duration\n  - FieldMask\n  - Struct\n  - Timestamp\n\"\"\"\n\n__author__ = 'jieluo@google.com (Jie Luo)'\n\nimport calendar\nimport collections.abc\nimport datetime\n\nfrom google.protobuf.descriptor import FieldDescriptor\n\n_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S'\n_NANOS_PER_SECOND = 1000000000\n_NANOS_PER_MILLISECOND = 1000000\n_NANOS_PER_MICROSECOND = 1000\n_MILLIS_PER_SECOND = 1000\n_MICROS_PER_SECOND = 1000000\n_SECONDS_PER_DAY = 24 * 3600\n_DURATION_SECONDS_MAX = 315576000000\n\n\nclass Any(object):\n  \"\"\"Class for Any Message type.\"\"\"\n\n  __slots__ = ()\n\n  def Pack(self, msg, type_url_prefix='type.googleapis.com/',\n           deterministic=None):\n    \"\"\"Packs the specified message into current Any message.\"\"\"\n    if len(type_url_prefix) < 1 or type_url_prefix[-1] != '/':\n      self.type_url = '%s/%s' % (type_url_prefix, msg.DESCRIPTOR.full_name)\n    else:\n      self.type_url = '%s%s' % (type_url_prefix, msg.DESCRIPTOR.full_name)\n    self.value = msg.SerializeToString(deterministic=deterministic)\n\n  def Unpack(self, msg):\n    \"\"\"Unpacks the current Any message into specified message.\"\"\"\n    descriptor = msg.DESCRIPTOR\n    if not self.Is(descriptor):\n      return False\n    msg.ParseFromString(self.value)\n    return True\n\n  def TypeName(self):\n    \"\"\"Returns the protobuf type name of the inner message.\"\"\"\n    # Only last part is to be used: b/25630112\n    return self.type_url.split('/')[-1]\n\n  def Is(self, descriptor):\n    \"\"\"Checks if this Any represents the given protobuf type.\"\"\"\n    return '/' in self.type_url and self.TypeName() == descriptor.full_name\n\n\n_EPOCH_DATETIME_NAIVE = datetime.datetime.utcfromtimestamp(0)\n_EPOCH_DATETIME_AWARE = datetime.datetime.fromtimestamp(\n    0, tz=datetime.timezone.utc)\n\n\nclass Timestamp(object):\n  \"\"\"Class for Timestamp message type.\"\"\"\n\n  __slots__ = ()\n\n  def ToJsonString(self):\n    \"\"\"Converts Timestamp to RFC 3339 date string format.\n\n    Returns:\n      A string converted from timestamp. The string is always Z-normalized\n      and uses 3, 6 or 9 fractional digits as required to represent the\n      exact time. Example of the return format: '1972-01-01T10:00:20.021Z'\n    \"\"\"\n    nanos = self.nanos % _NANOS_PER_SECOND\n    total_sec = self.seconds + (self.nanos - nanos) // _NANOS_PER_SECOND\n    seconds = total_sec % _SECONDS_PER_DAY\n    days = (total_sec - seconds) // _SECONDS_PER_DAY\n    dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(days, seconds)\n\n    result = dt.isoformat()\n    if (nanos % 1e9) == 0:\n      # If there are 0 fractional digits, the fractional\n      # point '.' should be omitted when serializing.\n      return result + 'Z'\n    if (nanos % 1e6) == 0:\n      # Serialize 3 fractional digits.\n      return result + '.%03dZ' % (nanos / 1e6)\n    if (nanos % 1e3) == 0:\n      # Serialize 6 fractional digits.\n      return result + '.%06dZ' % (nanos / 1e3)\n    # Serialize 9 fractional digits.\n    return result + '.%09dZ' % nanos\n\n  def FromJsonString(self, value):\n    \"\"\"Parse a RFC 3339 date string format to Timestamp.\n\n    Args:\n      value: A date string. Any fractional digits (or none) and any offset are\n          accepted as long as they fit into nano-seconds precision.\n          Example of accepted format: '1972-01-01T10:00:20.021-05:00'\n\n    Raises:\n      ValueError: On parsing problems.\n    \"\"\"\n    if not isinstance(value, str):\n      raise ValueError('Timestamp JSON value not a string: {!r}'.format(value))\n    timezone_offset = value.find('Z')\n    if timezone_offset == -1:\n      timezone_offset = value.find('+')\n    if timezone_offset == -1:\n      timezone_offset = value.rfind('-')\n    if timezone_offset == -1:\n      raise ValueError(\n          'Failed to parse timestamp: missing valid timezone offset.')\n    time_value = value[0:timezone_offset]\n    # Parse datetime and nanos.\n    point_position = time_value.find('.')\n    if point_position == -1:\n      second_value = time_value\n      nano_value = ''\n    else:\n      second_value = time_value[:point_position]\n      nano_value = time_value[point_position + 1:]\n    if 't' in second_value:\n      raise ValueError(\n          'time data \\'{0}\\' does not match format \\'%Y-%m-%dT%H:%M:%S\\', '\n          'lowercase \\'t\\' is not accepted'.format(second_value))\n    date_object = datetime.datetime.strptime(second_value, _TIMESTAMPFOMAT)\n    td = date_object - datetime.datetime(1970, 1, 1)\n    seconds = td.seconds + td.days * _SECONDS_PER_DAY\n    if len(nano_value) > 9:\n      raise ValueError(\n          'Failed to parse Timestamp: nanos {0} more than '\n          '9 fractional digits.'.format(nano_value))\n    if nano_value:\n      nanos = round(float('0.' + nano_value) * 1e9)\n    else:\n      nanos = 0\n    # Parse timezone offsets.\n    if value[timezone_offset] == 'Z':\n      if len(value) != timezone_offset + 1:\n        raise ValueError('Failed to parse timestamp: invalid trailing'\n                         ' data {0}.'.format(value))\n    else:\n      timezone = value[timezone_offset:]\n      pos = timezone.find(':')\n      if pos == -1:\n        raise ValueError(\n            'Invalid timezone offset value: {0}.'.format(timezone))\n      if timezone[0] == '+':\n        seconds -= (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60\n      else:\n        seconds += (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60\n    # Set seconds and nanos\n    self.seconds = int(seconds)\n    self.nanos = int(nanos)\n\n  def GetCurrentTime(self):\n    \"\"\"Get the current UTC into Timestamp.\"\"\"\n    self.FromDatetime(datetime.datetime.utcnow())\n\n  def ToNanoseconds(self):\n    \"\"\"Converts Timestamp to nanoseconds since epoch.\"\"\"\n    return self.seconds * _NANOS_PER_SECOND + self.nanos\n\n  def ToMicroseconds(self):\n    \"\"\"Converts Timestamp to microseconds since epoch.\"\"\"\n    return (self.seconds * _MICROS_PER_SECOND +\n            self.nanos // _NANOS_PER_MICROSECOND)\n\n  def ToMilliseconds(self):\n    \"\"\"Converts Timestamp to milliseconds since epoch.\"\"\"\n    return (self.seconds * _MILLIS_PER_SECOND +\n            self.nanos // _NANOS_PER_MILLISECOND)\n\n  def ToSeconds(self):\n    \"\"\"Converts Timestamp to seconds since epoch.\"\"\"\n    return self.seconds\n\n  def FromNanoseconds(self, nanos):\n    \"\"\"Converts nanoseconds since epoch to Timestamp.\"\"\"\n    self.seconds = nanos // _NANOS_PER_SECOND\n    self.nanos = nanos % _NANOS_PER_SECOND\n\n  def FromMicroseconds(self, micros):\n    \"\"\"Converts microseconds since epoch to Timestamp.\"\"\"\n    self.seconds = micros // _MICROS_PER_SECOND\n    self.nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND\n\n  def FromMilliseconds(self, millis):\n    \"\"\"Converts milliseconds since epoch to Timestamp.\"\"\"\n    self.seconds = millis // _MILLIS_PER_SECOND\n    self.nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND\n\n  def FromSeconds(self, seconds):\n    \"\"\"Converts seconds since epoch to Timestamp.\"\"\"\n    self.seconds = seconds\n    self.nanos = 0\n\n  def ToDatetime(self, tzinfo=None):\n    \"\"\"Converts Timestamp to a datetime.\n\n    Args:\n      tzinfo: A datetime.tzinfo subclass; defaults to None.\n\n    Returns:\n      If tzinfo is None, returns a timezone-naive UTC datetime (with no timezone\n      information, i.e. not aware that it's UTC).\n\n      Otherwise, returns a timezone-aware datetime in the input timezone.\n    \"\"\"\n    delta = datetime.timedelta(\n        seconds=self.seconds,\n        microseconds=_RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND))\n    if tzinfo is None:\n      return _EPOCH_DATETIME_NAIVE + delta\n    else:\n      return _EPOCH_DATETIME_AWARE.astimezone(tzinfo) + delta\n\n  def FromDatetime(self, dt):\n    \"\"\"Converts datetime to Timestamp.\n\n    Args:\n      dt: A datetime. If it's timezone-naive, it's assumed to be in UTC.\n    \"\"\"\n    # Using this guide: http://wiki.python.org/moin/WorkingWithTime\n    # And this conversion guide: http://docs.python.org/library/time.html\n\n    # Turn the date parameter into a tuple (struct_time) that can then be\n    # manipulated into a long value of seconds.  During the conversion from\n    # struct_time to long, the source date in UTC, and so it follows that the\n    # correct transformation is calendar.timegm()\n    self.seconds = calendar.timegm(dt.utctimetuple())\n    self.nanos = dt.microsecond * _NANOS_PER_MICROSECOND\n\n\nclass Duration(object):\n  \"\"\"Class for Duration message type.\"\"\"\n\n  __slots__ = ()\n\n  def ToJsonString(self):\n    \"\"\"Converts Duration to string format.\n\n    Returns:\n      A string converted from self. The string format will contains\n      3, 6, or 9 fractional digits depending on the precision required to\n      represent the exact Duration value. For example: \"1s\", \"1.010s\",\n      \"1.000000100s\", \"-3.100s\"\n    \"\"\"\n    _CheckDurationValid(self.seconds, self.nanos)\n    if self.seconds < 0 or self.nanos < 0:\n      result = '-'\n      seconds = - self.seconds + int((0 - self.nanos) // 1e9)\n      nanos = (0 - self.nanos) % 1e9\n    else:\n      result = ''\n      seconds = self.seconds + int(self.nanos // 1e9)\n      nanos = self.nanos % 1e9\n    result += '%d' % seconds\n    if (nanos % 1e9) == 0:\n      # If there are 0 fractional digits, the fractional\n      # point '.' should be omitted when serializing.\n      return result + 's'\n    if (nanos % 1e6) == 0:\n      # Serialize 3 fractional digits.\n      return result + '.%03ds' % (nanos / 1e6)\n    if (nanos % 1e3) == 0:\n      # Serialize 6 fractional digits.\n      return result + '.%06ds' % (nanos / 1e3)\n    # Serialize 9 fractional digits.\n    return result + '.%09ds' % nanos\n\n  def FromJsonString(self, value):\n    \"\"\"Converts a string to Duration.\n\n    Args:\n      value: A string to be converted. The string must end with 's'. Any\n          fractional digits (or none) are accepted as long as they fit into\n          precision. For example: \"1s\", \"1.01s\", \"1.0000001s\", \"-3.100s\n\n    Raises:\n      ValueError: On parsing problems.\n    \"\"\"\n    if not isinstance(value, str):\n      raise ValueError('Duration JSON value not a string: {!r}'.format(value))\n    if len(value) < 1 or value[-1] != 's':\n      raise ValueError(\n          'Duration must end with letter \"s\": {0}.'.format(value))\n    try:\n      pos = value.find('.')\n      if pos == -1:\n        seconds = int(value[:-1])\n        nanos = 0\n      else:\n        seconds = int(value[:pos])\n        if value[0] == '-':\n          nanos = int(round(float('-0{0}'.format(value[pos: -1])) *1e9))\n        else:\n          nanos = int(round(float('0{0}'.format(value[pos: -1])) *1e9))\n      _CheckDurationValid(seconds, nanos)\n      self.seconds = seconds\n      self.nanos = nanos\n    except ValueError as e:\n      raise ValueError(\n          'Couldn\\'t parse duration: {0} : {1}.'.format(value, e))\n\n  def ToNanoseconds(self):\n    \"\"\"Converts a Duration to nanoseconds.\"\"\"\n    return self.seconds * _NANOS_PER_SECOND + self.nanos\n\n  def ToMicroseconds(self):\n    \"\"\"Converts a Duration to microseconds.\"\"\"\n    micros = _RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND)\n    return self.seconds * _MICROS_PER_SECOND + micros\n\n  def ToMilliseconds(self):\n    \"\"\"Converts a Duration to milliseconds.\"\"\"\n    millis = _RoundTowardZero(self.nanos, _NANOS_PER_MILLISECOND)\n    return self.seconds * _MILLIS_PER_SECOND + millis\n\n  def ToSeconds(self):\n    \"\"\"Converts a Duration to seconds.\"\"\"\n    return self.seconds\n\n  def FromNanoseconds(self, nanos):\n    \"\"\"Converts nanoseconds to Duration.\"\"\"\n    self._NormalizeDuration(nanos // _NANOS_PER_SECOND,\n                            nanos % _NANOS_PER_SECOND)\n\n  def FromMicroseconds(self, micros):\n    \"\"\"Converts microseconds to Duration.\"\"\"\n    self._NormalizeDuration(\n        micros // _MICROS_PER_SECOND,\n        (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND)\n\n  def FromMilliseconds(self, millis):\n    \"\"\"Converts milliseconds to Duration.\"\"\"\n    self._NormalizeDuration(\n        millis // _MILLIS_PER_SECOND,\n        (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND)\n\n  def FromSeconds(self, seconds):\n    \"\"\"Converts seconds to Duration.\"\"\"\n    self.seconds = seconds\n    self.nanos = 0\n\n  def ToTimedelta(self):\n    \"\"\"Converts Duration to timedelta.\"\"\"\n    return datetime.timedelta(\n        seconds=self.seconds, microseconds=_RoundTowardZero(\n            self.nanos, _NANOS_PER_MICROSECOND))\n\n  def FromTimedelta(self, td):\n    \"\"\"Converts timedelta to Duration.\"\"\"\n    self._NormalizeDuration(td.seconds + td.days * _SECONDS_PER_DAY,\n                            td.microseconds * _NANOS_PER_MICROSECOND)\n\n  def _NormalizeDuration(self, seconds, nanos):\n    \"\"\"Set Duration by seconds and nanos.\"\"\"\n    # Force nanos to be negative if the duration is negative.\n    if seconds < 0 and nanos > 0:\n      seconds += 1\n      nanos -= _NANOS_PER_SECOND\n    self.seconds = seconds\n    self.nanos = nanos\n\n\ndef _CheckDurationValid(seconds, nanos):\n  if seconds < -_DURATION_SECONDS_MAX or seconds > _DURATION_SECONDS_MAX:\n    raise ValueError(\n        'Duration is not valid: Seconds {0} must be in range '\n        '[-315576000000, 315576000000].'.format(seconds))\n  if nanos <= -_NANOS_PER_SECOND or nanos >= _NANOS_PER_SECOND:\n    raise ValueError(\n        'Duration is not valid: Nanos {0} must be in range '\n        '[-999999999, 999999999].'.format(nanos))\n  if (nanos < 0 and seconds > 0) or (nanos > 0 and seconds < 0):\n    raise ValueError(\n        'Duration is not valid: Sign mismatch.')\n\n\ndef _RoundTowardZero(value, divider):\n  \"\"\"Truncates the remainder part after division.\"\"\"\n  # For some languages, the sign of the remainder is implementation\n  # dependent if any of the operands is negative. Here we enforce\n  # \"rounded toward zero\" semantics. For example, for (-5) / 2 an\n  # implementation may give -3 as the result with the remainder being\n  # 1. This function ensures we always return -2 (closer to zero).\n  result = value // divider\n  remainder = value % divider\n  if result < 0 and remainder > 0:\n    return result + 1\n  else:\n    return result\n\n\nclass FieldMask(object):\n  \"\"\"Class for FieldMask message type.\"\"\"\n\n  __slots__ = ()\n\n  def ToJsonString(self):\n    \"\"\"Converts FieldMask to string according to proto3 JSON spec.\"\"\"\n    camelcase_paths = []\n    for path in self.paths:\n      camelcase_paths.append(_SnakeCaseToCamelCase(path))\n    return ','.join(camelcase_paths)\n\n  def FromJsonString(self, value):\n    \"\"\"Converts string to FieldMask according to proto3 JSON spec.\"\"\"\n    if not isinstance(value, str):\n      raise ValueError('FieldMask JSON value not a string: {!r}'.format(value))\n    self.Clear()\n    if value:\n      for path in value.split(','):\n        self.paths.append(_CamelCaseToSnakeCase(path))\n\n  def IsValidForDescriptor(self, message_descriptor):\n    \"\"\"Checks whether the FieldMask is valid for Message Descriptor.\"\"\"\n    for path in self.paths:\n      if not _IsValidPath(message_descriptor, path):\n        return False\n    return True\n\n  def AllFieldsFromDescriptor(self, message_descriptor):\n    \"\"\"Gets all direct fields of Message Descriptor to FieldMask.\"\"\"\n    self.Clear()\n    for field in message_descriptor.fields:\n      self.paths.append(field.name)\n\n  def CanonicalFormFromMask(self, mask):\n    \"\"\"Converts a FieldMask to the canonical form.\n\n    Removes paths that are covered by another path. For example,\n    \"foo.bar\" is covered by \"foo\" and will be removed if \"foo\"\n    is also in the FieldMask. Then sorts all paths in alphabetical order.\n\n    Args:\n      mask: The original FieldMask to be converted.\n    \"\"\"\n    tree = _FieldMaskTree(mask)\n    tree.ToFieldMask(self)\n\n  def Union(self, mask1, mask2):\n    \"\"\"Merges mask1 and mask2 into this FieldMask.\"\"\"\n    _CheckFieldMaskMessage(mask1)\n    _CheckFieldMaskMessage(mask2)\n    tree = _FieldMaskTree(mask1)\n    tree.MergeFromFieldMask(mask2)\n    tree.ToFieldMask(self)\n\n  def Intersect(self, mask1, mask2):\n    \"\"\"Intersects mask1 and mask2 into this FieldMask.\"\"\"\n    _CheckFieldMaskMessage(mask1)\n    _CheckFieldMaskMessage(mask2)\n    tree = _FieldMaskTree(mask1)\n    intersection = _FieldMaskTree()\n    for path in mask2.paths:\n      tree.IntersectPath(path, intersection)\n    intersection.ToFieldMask(self)\n\n  def MergeMessage(\n      self, source, destination,\n      replace_message_field=False, replace_repeated_field=False):\n    \"\"\"Merges fields specified in FieldMask from source to destination.\n\n    Args:\n      source: Source message.\n      destination: The destination message to be merged into.\n      replace_message_field: Replace message field if True. Merge message\n          field if False.\n      replace_repeated_field: Replace repeated field if True. Append\n          elements of repeated field if False.\n    \"\"\"\n    tree = _FieldMaskTree(self)\n    tree.MergeMessage(\n        source, destination, replace_message_field, replace_repeated_field)\n\n\ndef _IsValidPath(message_descriptor, path):\n  \"\"\"Checks whether the path is valid for Message Descriptor.\"\"\"\n  parts = path.split('.')\n  last = parts.pop()\n  for name in parts:\n    field = message_descriptor.fields_by_name.get(name)\n    if (field is None or\n        field.label == FieldDescriptor.LABEL_REPEATED or\n        field.type != FieldDescriptor.TYPE_MESSAGE):\n      return False\n    message_descriptor = field.message_type\n  return last in message_descriptor.fields_by_name\n\n\ndef _CheckFieldMaskMessage(message):\n  \"\"\"Raises ValueError if message is not a FieldMask.\"\"\"\n  message_descriptor = message.DESCRIPTOR\n  if (message_descriptor.name != 'FieldMask' or\n      message_descriptor.file.name != 'google/protobuf/field_mask.proto'):\n    raise ValueError('Message {0} is not a FieldMask.'.format(\n        message_descriptor.full_name))\n\n\ndef _SnakeCaseToCamelCase(path_name):\n  \"\"\"Converts a path name from snake_case to camelCase.\"\"\"\n  result = []\n  after_underscore = False\n  for c in path_name:\n    if c.isupper():\n      raise ValueError(\n          'Fail to print FieldMask to Json string: Path name '\n          '{0} must not contain uppercase letters.'.format(path_name))\n    if after_underscore:\n      if c.islower():\n        result.append(c.upper())\n        after_underscore = False\n      else:\n        raise ValueError(\n            'Fail to print FieldMask to Json string: The '\n            'character after a \"_\" must be a lowercase letter '\n            'in path name {0}.'.format(path_name))\n    elif c == '_':\n      after_underscore = True\n    else:\n      result += c\n\n  if after_underscore:\n    raise ValueError('Fail to print FieldMask to Json string: Trailing \"_\" '\n                     'in path name {0}.'.format(path_name))\n  return ''.join(result)\n\n\ndef _CamelCaseToSnakeCase(path_name):\n  \"\"\"Converts a field name from camelCase to snake_case.\"\"\"\n  result = []\n  for c in path_name:\n    if c == '_':\n      raise ValueError('Fail to parse FieldMask: Path name '\n                       '{0} must not contain \"_\"s.'.format(path_name))\n    if c.isupper():\n      result += '_'\n      result += c.lower()\n    else:\n      result += c\n  return ''.join(result)\n\n\nclass _FieldMaskTree(object):\n  \"\"\"Represents a FieldMask in a tree structure.\n\n  For example, given a FieldMask \"foo.bar,foo.baz,bar.baz\",\n  the FieldMaskTree will be:\n      [_root] -+- foo -+- bar\n            |       |\n            |       +- baz\n            |\n            +- bar --- baz\n  In the tree, each leaf node represents a field path.\n  \"\"\"\n\n  __slots__ = ('_root',)\n\n  def __init__(self, field_mask=None):\n    \"\"\"Initializes the tree by FieldMask.\"\"\"\n    self._root = {}\n    if field_mask:\n      self.MergeFromFieldMask(field_mask)\n\n  def MergeFromFieldMask(self, field_mask):\n    \"\"\"Merges a FieldMask to the tree.\"\"\"\n    for path in field_mask.paths:\n      self.AddPath(path)\n\n  def AddPath(self, path):\n    \"\"\"Adds a field path into the tree.\n\n    If the field path to add is a sub-path of an existing field path\n    in the tree (i.e., a leaf node), it means the tree already matches\n    the given path so nothing will be added to the tree. If the path\n    matches an existing non-leaf node in the tree, that non-leaf node\n    will be turned into a leaf node with all its children removed because\n    the path matches all the node's children. Otherwise, a new path will\n    be added.\n\n    Args:\n      path: The field path to add.\n    \"\"\"\n    node = self._root\n    for name in path.split('.'):\n      if name not in node:\n        node[name] = {}\n      elif not node[name]:\n        # Pre-existing empty node implies we already have this entire tree.\n        return\n      node = node[name]\n    # Remove any sub-trees we might have had.\n    node.clear()\n\n  def ToFieldMask(self, field_mask):\n    \"\"\"Converts the tree to a FieldMask.\"\"\"\n    field_mask.Clear()\n    _AddFieldPaths(self._root, '', field_mask)\n\n  def IntersectPath(self, path, intersection):\n    \"\"\"Calculates the intersection part of a field path with this tree.\n\n    Args:\n      path: The field path to calculates.\n      intersection: The out tree to record the intersection part.\n    \"\"\"\n    node = self._root\n    for name in path.split('.'):\n      if name not in node:\n        return\n      elif not node[name]:\n        intersection.AddPath(path)\n        return\n      node = node[name]\n    intersection.AddLeafNodes(path, node)\n\n  def AddLeafNodes(self, prefix, node):\n    \"\"\"Adds leaf nodes begin with prefix to this tree.\"\"\"\n    if not node:\n      self.AddPath(prefix)\n    for name in node:\n      child_path = prefix + '.' + name\n      self.AddLeafNodes(child_path, node[name])\n\n  def MergeMessage(\n      self, source, destination,\n      replace_message, replace_repeated):\n    \"\"\"Merge all fields specified by this tree from source to destination.\"\"\"\n    _MergeMessage(\n        self._root, source, destination, replace_message, replace_repeated)\n\n\ndef _StrConvert(value):\n  \"\"\"Converts value to str if it is not.\"\"\"\n  # This file is imported by c extension and some methods like ClearField\n  # requires string for the field name. py2/py3 has different text\n  # type and may use unicode.\n  if not isinstance(value, str):\n    return value.encode('utf-8')\n  return value\n\n\ndef _MergeMessage(\n    node, source, destination, replace_message, replace_repeated):\n  \"\"\"Merge all fields specified by a sub-tree from source to destination.\"\"\"\n  source_descriptor = source.DESCRIPTOR\n  for name in node:\n    child = node[name]\n    field = source_descriptor.fields_by_name[name]\n    if field is None:\n      raise ValueError('Error: Can\\'t find field {0} in message {1}.'.format(\n          name, source_descriptor.full_name))\n    if child:\n      # Sub-paths are only allowed for singular message fields.\n      if (field.label == FieldDescriptor.LABEL_REPEATED or\n          field.cpp_type != FieldDescriptor.CPPTYPE_MESSAGE):\n        raise ValueError('Error: Field {0} in message {1} is not a singular '\n                         'message field and cannot have sub-fields.'.format(\n                             name, source_descriptor.full_name))\n      if source.HasField(name):\n        _MergeMessage(\n            child, getattr(source, name), getattr(destination, name),\n            replace_message, replace_repeated)\n      continue\n    if field.label == FieldDescriptor.LABEL_REPEATED:\n      if replace_repeated:\n        destination.ClearField(_StrConvert(name))\n      repeated_source = getattr(source, name)\n      repeated_destination = getattr(destination, name)\n      repeated_destination.MergeFrom(repeated_source)\n    else:\n      if field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE:\n        if replace_message:\n          destination.ClearField(_StrConvert(name))\n        if source.HasField(name):\n          getattr(destination, name).MergeFrom(getattr(source, name))\n      else:\n        setattr(destination, name, getattr(source, name))\n\n\ndef _AddFieldPaths(node, prefix, field_mask):\n  \"\"\"Adds the field paths descended from node to field_mask.\"\"\"\n  if not node and prefix:\n    field_mask.paths.append(prefix)\n    return\n  for name in sorted(node):\n    if prefix:\n      child_path = prefix + '.' + name\n    else:\n      child_path = name\n    _AddFieldPaths(node[name], child_path, field_mask)\n\n\ndef _SetStructValue(struct_value, value):\n  if value is None:\n    struct_value.null_value = 0\n  elif isinstance(value, bool):\n    # Note: this check must come before the number check because in Python\n    # True and False are also considered numbers.\n    struct_value.bool_value = value\n  elif isinstance(value, str):\n    struct_value.string_value = value\n  elif isinstance(value, (int, float)):\n    struct_value.number_value = value\n  elif isinstance(value, (dict, Struct)):\n    struct_value.struct_value.Clear()\n    struct_value.struct_value.update(value)\n  elif isinstance(value, (list, ListValue)):\n    struct_value.list_value.Clear()\n    struct_value.list_value.extend(value)\n  else:\n    raise ValueError('Unexpected type')\n\n\ndef _GetStructValue(struct_value):\n  which = struct_value.WhichOneof('kind')\n  if which == 'struct_value':\n    return struct_value.struct_value\n  elif which == 'null_value':\n    return None\n  elif which == 'number_value':\n    return struct_value.number_value\n  elif which == 'string_value':\n    return struct_value.string_value\n  elif which == 'bool_value':\n    return struct_value.bool_value\n  elif which == 'list_value':\n    return struct_value.list_value\n  elif which is None:\n    raise ValueError('Value not set')\n\n\nclass Struct(object):\n  \"\"\"Class for Struct message type.\"\"\"\n\n  __slots__ = ()\n\n  def __getitem__(self, key):\n    return _GetStructValue(self.fields[key])\n\n  def __contains__(self, item):\n    return item in self.fields\n\n  def __setitem__(self, key, value):\n    _SetStructValue(self.fields[key], value)\n\n  def __delitem__(self, key):\n    del self.fields[key]\n\n  def __len__(self):\n    return len(self.fields)\n\n  def __iter__(self):\n    return iter(self.fields)\n\n  def keys(self):  # pylint: disable=invalid-name\n    return self.fields.keys()\n\n  def values(self):  # pylint: disable=invalid-name\n    return [self[key] for key in self]\n\n  def items(self):  # pylint: disable=invalid-name\n    return [(key, self[key]) for key in self]\n\n  def get_or_create_list(self, key):\n    \"\"\"Returns a list for this key, creating if it didn't exist already.\"\"\"\n    if not self.fields[key].HasField('list_value'):\n      # Clear will mark list_value modified which will indeed create a list.\n      self.fields[key].list_value.Clear()\n    return self.fields[key].list_value\n\n  def get_or_create_struct(self, key):\n    \"\"\"Returns a struct for this key, creating if it didn't exist already.\"\"\"\n    if not self.fields[key].HasField('struct_value'):\n      # Clear will mark struct_value modified which will indeed create a struct.\n      self.fields[key].struct_value.Clear()\n    return self.fields[key].struct_value\n\n  def update(self, dictionary):  # pylint: disable=invalid-name\n    for key, value in dictionary.items():\n      _SetStructValue(self.fields[key], value)\n\ncollections.abc.MutableMapping.register(Struct)\n\n\nclass ListValue(object):\n  \"\"\"Class for ListValue message type.\"\"\"\n\n  __slots__ = ()\n\n  def __len__(self):\n    return len(self.values)\n\n  def append(self, value):\n    _SetStructValue(self.values.add(), value)\n\n  def extend(self, elem_seq):\n    for value in elem_seq:\n      self.append(value)\n\n  def __getitem__(self, index):\n    \"\"\"Retrieves item by the specified index.\"\"\"\n    return _GetStructValue(self.values.__getitem__(index))\n\n  def __setitem__(self, index, value):\n    _SetStructValue(self.values.__getitem__(index), value)\n\n  def __delitem__(self, key):\n    del self.values[key]\n\n  def items(self):\n    for i in range(len(self)):\n      yield self[i]\n\n  def add_struct(self):\n    \"\"\"Appends and returns a struct value as the next value in the list.\"\"\"\n    struct_value = self.values.add().struct_value\n    # Clear will mark struct_value modified which will indeed create a struct.\n    struct_value.Clear()\n    return struct_value\n\n  def add_list(self):\n    \"\"\"Appends and returns a list value as the next value in the list.\"\"\"\n    list_value = self.values.add().list_value\n    # Clear will mark list_value modified which will indeed create a list.\n    list_value.Clear()\n    return list_value\n\ncollections.abc.MutableSequence.register(ListValue)\n\n\nWKTBASES = {\n    'google.protobuf.Any': Any,\n    'google.protobuf.Duration': Duration,\n    'google.protobuf.FieldMask': FieldMask,\n    'google.protobuf.ListValue': ListValue,\n    'google.protobuf.Struct': Struct,\n    'google.protobuf.Timestamp': Timestamp,\n}\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/internal/wire_format.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Constants and static functions to support protocol buffer wire format.\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\nimport struct\nfrom google.protobuf import descriptor\nfrom google.protobuf import message\n\n\nTAG_TYPE_BITS = 3  # Number of bits used to hold type info in a proto tag.\nTAG_TYPE_MASK = (1 << TAG_TYPE_BITS) - 1  # 0x7\n\n# These numbers identify the wire type of a protocol buffer value.\n# We use the least-significant TAG_TYPE_BITS bits of the varint-encoded\n# tag-and-type to store one of these WIRETYPE_* constants.\n# These values must match WireType enum in google/protobuf/wire_format.h.\nWIRETYPE_VARINT = 0\nWIRETYPE_FIXED64 = 1\nWIRETYPE_LENGTH_DELIMITED = 2\nWIRETYPE_START_GROUP = 3\nWIRETYPE_END_GROUP = 4\nWIRETYPE_FIXED32 = 5\n_WIRETYPE_MAX = 5\n\n\n# Bounds for various integer types.\nINT32_MAX = int((1 << 31) - 1)\nINT32_MIN = int(-(1 << 31))\nUINT32_MAX = (1 << 32) - 1\n\nINT64_MAX = (1 << 63) - 1\nINT64_MIN = -(1 << 63)\nUINT64_MAX = (1 << 64) - 1\n\n# \"struct\" format strings that will encode/decode the specified formats.\nFORMAT_UINT32_LITTLE_ENDIAN = '<I'\nFORMAT_UINT64_LITTLE_ENDIAN = '<Q'\nFORMAT_FLOAT_LITTLE_ENDIAN = '<f'\nFORMAT_DOUBLE_LITTLE_ENDIAN = '<d'\n\n\n# We'll have to provide alternate implementations of AppendLittleEndian*() on\n# any architectures where these checks fail.\nif struct.calcsize(FORMAT_UINT32_LITTLE_ENDIAN) != 4:\n  raise AssertionError('Format \"I\" is not a 32-bit number.')\nif struct.calcsize(FORMAT_UINT64_LITTLE_ENDIAN) != 8:\n  raise AssertionError('Format \"Q\" is not a 64-bit number.')\n\n\ndef PackTag(field_number, wire_type):\n  \"\"\"Returns an unsigned 32-bit integer that encodes the field number and\n  wire type information in standard protocol message wire format.\n\n  Args:\n    field_number: Expected to be an integer in the range [1, 1 << 29)\n    wire_type: One of the WIRETYPE_* constants.\n  \"\"\"\n  if not 0 <= wire_type <= _WIRETYPE_MAX:\n    raise message.EncodeError('Unknown wire type: %d' % wire_type)\n  return (field_number << TAG_TYPE_BITS) | wire_type\n\n\ndef UnpackTag(tag):\n  \"\"\"The inverse of PackTag().  Given an unsigned 32-bit number,\n  returns a (field_number, wire_type) tuple.\n  \"\"\"\n  return (tag >> TAG_TYPE_BITS), (tag & TAG_TYPE_MASK)\n\n\ndef ZigZagEncode(value):\n  \"\"\"ZigZag Transform:  Encodes signed integers so that they can be\n  effectively used with varint encoding.  See wire_format.h for\n  more details.\n  \"\"\"\n  if value >= 0:\n    return value << 1\n  return (value << 1) ^ (~0)\n\n\ndef ZigZagDecode(value):\n  \"\"\"Inverse of ZigZagEncode().\"\"\"\n  if not value & 0x1:\n    return value >> 1\n  return (value >> 1) ^ (~0)\n\n\n\n# The *ByteSize() functions below return the number of bytes required to\n# serialize \"field number + type\" information and then serialize the value.\n\n\ndef Int32ByteSize(field_number, int32):\n  return Int64ByteSize(field_number, int32)\n\n\ndef Int32ByteSizeNoTag(int32):\n  return _VarUInt64ByteSizeNoTag(0xffffffffffffffff & int32)\n\n\ndef Int64ByteSize(field_number, int64):\n  # Have to convert to uint before calling UInt64ByteSize().\n  return UInt64ByteSize(field_number, 0xffffffffffffffff & int64)\n\n\ndef UInt32ByteSize(field_number, uint32):\n  return UInt64ByteSize(field_number, uint32)\n\n\ndef UInt64ByteSize(field_number, uint64):\n  return TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(uint64)\n\n\ndef SInt32ByteSize(field_number, int32):\n  return UInt32ByteSize(field_number, ZigZagEncode(int32))\n\n\ndef SInt64ByteSize(field_number, int64):\n  return UInt64ByteSize(field_number, ZigZagEncode(int64))\n\n\ndef Fixed32ByteSize(field_number, fixed32):\n  return TagByteSize(field_number) + 4\n\n\ndef Fixed64ByteSize(field_number, fixed64):\n  return TagByteSize(field_number) + 8\n\n\ndef SFixed32ByteSize(field_number, sfixed32):\n  return TagByteSize(field_number) + 4\n\n\ndef SFixed64ByteSize(field_number, sfixed64):\n  return TagByteSize(field_number) + 8\n\n\ndef FloatByteSize(field_number, flt):\n  return TagByteSize(field_number) + 4\n\n\ndef DoubleByteSize(field_number, double):\n  return TagByteSize(field_number) + 8\n\n\ndef BoolByteSize(field_number, b):\n  return TagByteSize(field_number) + 1\n\n\ndef EnumByteSize(field_number, enum):\n  return UInt32ByteSize(field_number, enum)\n\n\ndef StringByteSize(field_number, string):\n  return BytesByteSize(field_number, string.encode('utf-8'))\n\n\ndef BytesByteSize(field_number, b):\n  return (TagByteSize(field_number)\n          + _VarUInt64ByteSizeNoTag(len(b))\n          + len(b))\n\n\ndef GroupByteSize(field_number, message):\n  return (2 * TagByteSize(field_number)  # START and END group.\n          + message.ByteSize())\n\n\ndef MessageByteSize(field_number, message):\n  return (TagByteSize(field_number)\n          + _VarUInt64ByteSizeNoTag(message.ByteSize())\n          + message.ByteSize())\n\n\ndef MessageSetItemByteSize(field_number, msg):\n  # First compute the sizes of the tags.\n  # There are 2 tags for the beginning and ending of the repeated group, that\n  # is field number 1, one with field number 2 (type_id) and one with field\n  # number 3 (message).\n  total_size = (2 * TagByteSize(1) + TagByteSize(2) + TagByteSize(3))\n\n  # Add the number of bytes for type_id.\n  total_size += _VarUInt64ByteSizeNoTag(field_number)\n\n  message_size = msg.ByteSize()\n\n  # The number of bytes for encoding the length of the message.\n  total_size += _VarUInt64ByteSizeNoTag(message_size)\n\n  # The size of the message.\n  total_size += message_size\n  return total_size\n\n\ndef TagByteSize(field_number):\n  \"\"\"Returns the bytes required to serialize a tag with this field number.\"\"\"\n  # Just pass in type 0, since the type won't affect the tag+type size.\n  return _VarUInt64ByteSizeNoTag(PackTag(field_number, 0))\n\n\n# Private helper function for the *ByteSize() functions above.\n\ndef _VarUInt64ByteSizeNoTag(uint64):\n  \"\"\"Returns the number of bytes required to serialize a single varint\n  using boundary value comparisons. (unrolled loop optimization -WPierce)\n  uint64 must be unsigned.\n  \"\"\"\n  if uint64 <= 0x7f: return 1\n  if uint64 <= 0x3fff: return 2\n  if uint64 <= 0x1fffff: return 3\n  if uint64 <= 0xfffffff: return 4\n  if uint64 <= 0x7ffffffff: return 5\n  if uint64 <= 0x3ffffffffff: return 6\n  if uint64 <= 0x1ffffffffffff: return 7\n  if uint64 <= 0xffffffffffffff: return 8\n  if uint64 <= 0x7fffffffffffffff: return 9\n  if uint64 > UINT64_MAX:\n    raise message.EncodeError('Value out of range: %d' % uint64)\n  return 10\n\n\nNON_PACKABLE_TYPES = (\n  descriptor.FieldDescriptor.TYPE_STRING,\n  descriptor.FieldDescriptor.TYPE_GROUP,\n  descriptor.FieldDescriptor.TYPE_MESSAGE,\n  descriptor.FieldDescriptor.TYPE_BYTES\n)\n\n\ndef IsTypePackable(field_type):\n  \"\"\"Return true iff packable = true is valid for fields of this type.\n\n  Args:\n    field_type: a FieldDescriptor::Type value.\n\n  Returns:\n    True iff fields of this type are packable.\n  \"\"\"\n  return field_type not in NON_PACKABLE_TYPES\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/json_format.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Contains routines for printing protocol messages in JSON format.\n\nSimple usage example:\n\n  # Create a proto object and serialize it to a json format string.\n  message = my_proto_pb2.MyMessage(foo='bar')\n  json_string = json_format.MessageToJson(message)\n\n  # Parse a json format string to proto object.\n  message = json_format.Parse(json_string, my_proto_pb2.MyMessage())\n\"\"\"\n\n__author__ = 'jieluo@google.com (Jie Luo)'\n\n\nimport base64\nfrom collections import OrderedDict\nimport json\nimport math\nfrom operator import methodcaller\nimport re\nimport sys\n\nfrom google.protobuf.internal import type_checkers\nfrom google.protobuf import descriptor\nfrom google.protobuf import symbol_database\n\n\n_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S'\n_INT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT32,\n                        descriptor.FieldDescriptor.CPPTYPE_UINT32,\n                        descriptor.FieldDescriptor.CPPTYPE_INT64,\n                        descriptor.FieldDescriptor.CPPTYPE_UINT64])\n_INT64_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT64,\n                          descriptor.FieldDescriptor.CPPTYPE_UINT64])\n_FLOAT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_FLOAT,\n                          descriptor.FieldDescriptor.CPPTYPE_DOUBLE])\n_INFINITY = 'Infinity'\n_NEG_INFINITY = '-Infinity'\n_NAN = 'NaN'\n\n_UNPAIRED_SURROGATE_PATTERN = re.compile(\n    u'[\\ud800-\\udbff](?![\\udc00-\\udfff])|(?<![\\ud800-\\udbff])[\\udc00-\\udfff]')\n\n_VALID_EXTENSION_NAME = re.compile(r'\\[[a-zA-Z0-9\\._]*\\]$')\n\n\nclass Error(Exception):\n  \"\"\"Top-level module error for json_format.\"\"\"\n\n\nclass SerializeToJsonError(Error):\n  \"\"\"Thrown if serialization to JSON fails.\"\"\"\n\n\nclass ParseError(Error):\n  \"\"\"Thrown in case of parsing error.\"\"\"\n\n\ndef MessageToJson(\n    message,\n    including_default_value_fields=False,\n    preserving_proto_field_name=False,\n    indent=2,\n    sort_keys=False,\n    use_integers_for_enums=False,\n    descriptor_pool=None,\n    float_precision=None,\n    ensure_ascii=True):\n  \"\"\"Converts protobuf message to JSON format.\n\n  Args:\n    message: The protocol buffers message instance to serialize.\n    including_default_value_fields: If True, singular primitive fields,\n        repeated fields, and map fields will always be serialized.  If\n        False, only serialize non-empty fields.  Singular message fields\n        and oneof fields are not affected by this option.\n    preserving_proto_field_name: If True, use the original proto field\n        names as defined in the .proto file. If False, convert the field\n        names to lowerCamelCase.\n    indent: The JSON object will be pretty-printed with this indent level.\n        An indent level of 0 or negative will only insert newlines.\n    sort_keys: If True, then the output will be sorted by field names.\n    use_integers_for_enums: If true, print integers instead of enum names.\n    descriptor_pool: A Descriptor Pool for resolving types. If None use the\n        default.\n    float_precision: If set, use this to specify float field valid digits.\n    ensure_ascii: If True, strings with non-ASCII characters are escaped.\n        If False, Unicode strings are returned unchanged.\n\n  Returns:\n    A string containing the JSON formatted protocol buffer message.\n  \"\"\"\n  printer = _Printer(\n      including_default_value_fields,\n      preserving_proto_field_name,\n      use_integers_for_enums,\n      descriptor_pool,\n      float_precision=float_precision)\n  return printer.ToJsonString(message, indent, sort_keys, ensure_ascii)\n\n\ndef MessageToDict(\n    message,\n    including_default_value_fields=False,\n    preserving_proto_field_name=False,\n    use_integers_for_enums=False,\n    descriptor_pool=None,\n    float_precision=None):\n  \"\"\"Converts protobuf message to a dictionary.\n\n  When the dictionary is encoded to JSON, it conforms to proto3 JSON spec.\n\n  Args:\n    message: The protocol buffers message instance to serialize.\n    including_default_value_fields: If True, singular primitive fields,\n        repeated fields, and map fields will always be serialized.  If\n        False, only serialize non-empty fields.  Singular message fields\n        and oneof fields are not affected by this option.\n    preserving_proto_field_name: If True, use the original proto field\n        names as defined in the .proto file. If False, convert the field\n        names to lowerCamelCase.\n    use_integers_for_enums: If true, print integers instead of enum names.\n    descriptor_pool: A Descriptor Pool for resolving types. If None use the\n        default.\n    float_precision: If set, use this to specify float field valid digits.\n\n  Returns:\n    A dict representation of the protocol buffer message.\n  \"\"\"\n  printer = _Printer(\n      including_default_value_fields,\n      preserving_proto_field_name,\n      use_integers_for_enums,\n      descriptor_pool,\n      float_precision=float_precision)\n  # pylint: disable=protected-access\n  return printer._MessageToJsonObject(message)\n\n\ndef _IsMapEntry(field):\n  return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and\n          field.message_type.has_options and\n          field.message_type.GetOptions().map_entry)\n\n\nclass _Printer(object):\n  \"\"\"JSON format printer for protocol message.\"\"\"\n\n  def __init__(\n      self,\n      including_default_value_fields=False,\n      preserving_proto_field_name=False,\n      use_integers_for_enums=False,\n      descriptor_pool=None,\n      float_precision=None):\n    self.including_default_value_fields = including_default_value_fields\n    self.preserving_proto_field_name = preserving_proto_field_name\n    self.use_integers_for_enums = use_integers_for_enums\n    self.descriptor_pool = descriptor_pool\n    if float_precision:\n      self.float_format = '.{}g'.format(float_precision)\n    else:\n      self.float_format = None\n\n  def ToJsonString(self, message, indent, sort_keys, ensure_ascii):\n    js = self._MessageToJsonObject(message)\n    return json.dumps(\n        js, indent=indent, sort_keys=sort_keys, ensure_ascii=ensure_ascii)\n\n  def _MessageToJsonObject(self, message):\n    \"\"\"Converts message to an object according to Proto3 JSON Specification.\"\"\"\n    message_descriptor = message.DESCRIPTOR\n    full_name = message_descriptor.full_name\n    if _IsWrapperMessage(message_descriptor):\n      return self._WrapperMessageToJsonObject(message)\n    if full_name in _WKTJSONMETHODS:\n      return methodcaller(_WKTJSONMETHODS[full_name][0], message)(self)\n    js = {}\n    return self._RegularMessageToJsonObject(message, js)\n\n  def _RegularMessageToJsonObject(self, message, js):\n    \"\"\"Converts normal message according to Proto3 JSON Specification.\"\"\"\n    fields = message.ListFields()\n\n    try:\n      for field, value in fields:\n        if self.preserving_proto_field_name:\n          name = field.name\n        else:\n          name = field.json_name\n        if _IsMapEntry(field):\n          # Convert a map field.\n          v_field = field.message_type.fields_by_name['value']\n          js_map = {}\n          for key in value:\n            if isinstance(key, bool):\n              if key:\n                recorded_key = 'true'\n              else:\n                recorded_key = 'false'\n            else:\n              recorded_key = str(key)\n            js_map[recorded_key] = self._FieldToJsonObject(\n                v_field, value[key])\n          js[name] = js_map\n        elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n          # Convert a repeated field.\n          js[name] = [self._FieldToJsonObject(field, k)\n                      for k in value]\n        elif field.is_extension:\n          name = '[%s]' % field.full_name\n          js[name] = self._FieldToJsonObject(field, value)\n        else:\n          js[name] = self._FieldToJsonObject(field, value)\n\n      # Serialize default value if including_default_value_fields is True.\n      if self.including_default_value_fields:\n        message_descriptor = message.DESCRIPTOR\n        for field in message_descriptor.fields:\n          # Singular message fields and oneof fields will not be affected.\n          if ((field.label != descriptor.FieldDescriptor.LABEL_REPEATED and\n               field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE) or\n              field.containing_oneof):\n            continue\n          if self.preserving_proto_field_name:\n            name = field.name\n          else:\n            name = field.json_name\n          if name in js:\n            # Skip the field which has been serialized already.\n            continue\n          if _IsMapEntry(field):\n            js[name] = {}\n          elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n            js[name] = []\n          else:\n            js[name] = self._FieldToJsonObject(field, field.default_value)\n\n    except ValueError as e:\n      raise SerializeToJsonError(\n          'Failed to serialize {0} field: {1}.'.format(field.name, e))\n\n    return js\n\n  def _FieldToJsonObject(self, field, value):\n    \"\"\"Converts field value according to Proto3 JSON Specification.\"\"\"\n    if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n      return self._MessageToJsonObject(value)\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM:\n      if self.use_integers_for_enums:\n        return value\n      if field.enum_type.full_name == 'google.protobuf.NullValue':\n        return None\n      enum_value = field.enum_type.values_by_number.get(value, None)\n      if enum_value is not None:\n        return enum_value.name\n      else:\n        if field.file.syntax == 'proto3':\n          return value\n        raise SerializeToJsonError('Enum field contains an integer value '\n                                   'which can not mapped to an enum value.')\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING:\n      if field.type == descriptor.FieldDescriptor.TYPE_BYTES:\n        # Use base64 Data encoding for bytes\n        return base64.b64encode(value).decode('utf-8')\n      else:\n        return value\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL:\n      return bool(value)\n    elif field.cpp_type in _INT64_TYPES:\n      return str(value)\n    elif field.cpp_type in _FLOAT_TYPES:\n      if math.isinf(value):\n        if value < 0.0:\n          return _NEG_INFINITY\n        else:\n          return _INFINITY\n      if math.isnan(value):\n        return _NAN\n      if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT:\n        if self.float_format:\n          return float(format(value, self.float_format))\n        else:\n          return type_checkers.ToShortestFloat(value)\n\n    return value\n\n  def _AnyMessageToJsonObject(self, message):\n    \"\"\"Converts Any message according to Proto3 JSON Specification.\"\"\"\n    if not message.ListFields():\n      return {}\n    # Must print @type first, use OrderedDict instead of {}\n    js = OrderedDict()\n    type_url = message.type_url\n    js['@type'] = type_url\n    sub_message = _CreateMessageFromTypeUrl(type_url, self.descriptor_pool)\n    sub_message.ParseFromString(message.value)\n    message_descriptor = sub_message.DESCRIPTOR\n    full_name = message_descriptor.full_name\n    if _IsWrapperMessage(message_descriptor):\n      js['value'] = self._WrapperMessageToJsonObject(sub_message)\n      return js\n    if full_name in _WKTJSONMETHODS:\n      js['value'] = methodcaller(_WKTJSONMETHODS[full_name][0],\n                                 sub_message)(self)\n      return js\n    return self._RegularMessageToJsonObject(sub_message, js)\n\n  def _GenericMessageToJsonObject(self, message):\n    \"\"\"Converts message according to Proto3 JSON Specification.\"\"\"\n    # Duration, Timestamp and FieldMask have ToJsonString method to do the\n    # convert. Users can also call the method directly.\n    return message.ToJsonString()\n\n  def _ValueMessageToJsonObject(self, message):\n    \"\"\"Converts Value message according to Proto3 JSON Specification.\"\"\"\n    which = message.WhichOneof('kind')\n    # If the Value message is not set treat as null_value when serialize\n    # to JSON. The parse back result will be different from original message.\n    if which is None or which == 'null_value':\n      return None\n    if which == 'list_value':\n      return self._ListValueMessageToJsonObject(message.list_value)\n    if which == 'struct_value':\n      value = message.struct_value\n    else:\n      value = getattr(message, which)\n    oneof_descriptor = message.DESCRIPTOR.fields_by_name[which]\n    return self._FieldToJsonObject(oneof_descriptor, value)\n\n  def _ListValueMessageToJsonObject(self, message):\n    \"\"\"Converts ListValue message according to Proto3 JSON Specification.\"\"\"\n    return [self._ValueMessageToJsonObject(value)\n            for value in message.values]\n\n  def _StructMessageToJsonObject(self, message):\n    \"\"\"Converts Struct message according to Proto3 JSON Specification.\"\"\"\n    fields = message.fields\n    ret = {}\n    for key in fields:\n      ret[key] = self._ValueMessageToJsonObject(fields[key])\n    return ret\n\n  def _WrapperMessageToJsonObject(self, message):\n    return self._FieldToJsonObject(\n        message.DESCRIPTOR.fields_by_name['value'], message.value)\n\n\ndef _IsWrapperMessage(message_descriptor):\n  return message_descriptor.file.name == 'google/protobuf/wrappers.proto'\n\n\ndef _DuplicateChecker(js):\n  result = {}\n  for name, value in js:\n    if name in result:\n      raise ParseError('Failed to load JSON: duplicate key {0}.'.format(name))\n    result[name] = value\n  return result\n\n\ndef _CreateMessageFromTypeUrl(type_url, descriptor_pool):\n  \"\"\"Creates a message from a type URL.\"\"\"\n  db = symbol_database.Default()\n  pool = db.pool if descriptor_pool is None else descriptor_pool\n  type_name = type_url.split('/')[-1]\n  try:\n    message_descriptor = pool.FindMessageTypeByName(type_name)\n  except KeyError:\n    raise TypeError(\n        'Can not find message descriptor by type_url: {0}'.format(type_url))\n  message_class = db.GetPrototype(message_descriptor)\n  return message_class()\n\n\ndef Parse(text,\n          message,\n          ignore_unknown_fields=False,\n          descriptor_pool=None,\n          max_recursion_depth=100):\n  \"\"\"Parses a JSON representation of a protocol message into a message.\n\n  Args:\n    text: Message JSON representation.\n    message: A protocol buffer message to merge into.\n    ignore_unknown_fields: If True, do not raise errors for unknown fields.\n    descriptor_pool: A Descriptor Pool for resolving types. If None use the\n      default.\n    max_recursion_depth: max recursion depth of JSON message to be\n      deserialized. JSON messages over this depth will fail to be\n      deserialized. Default value is 100.\n\n  Returns:\n    The same message passed as argument.\n\n  Raises::\n    ParseError: On JSON parsing problems.\n  \"\"\"\n  if not isinstance(text, str):\n    text = text.decode('utf-8')\n  try:\n    js = json.loads(text, object_pairs_hook=_DuplicateChecker)\n  except ValueError as e:\n    raise ParseError('Failed to load JSON: {0}.'.format(str(e)))\n  return ParseDict(js, message, ignore_unknown_fields, descriptor_pool,\n                   max_recursion_depth)\n\n\ndef ParseDict(js_dict,\n              message,\n              ignore_unknown_fields=False,\n              descriptor_pool=None,\n              max_recursion_depth=100):\n  \"\"\"Parses a JSON dictionary representation into a message.\n\n  Args:\n    js_dict: Dict representation of a JSON message.\n    message: A protocol buffer message to merge into.\n    ignore_unknown_fields: If True, do not raise errors for unknown fields.\n    descriptor_pool: A Descriptor Pool for resolving types. If None use the\n      default.\n    max_recursion_depth: max recursion depth of JSON message to be\n      deserialized. JSON messages over this depth will fail to be\n      deserialized. Default value is 100.\n\n  Returns:\n    The same message passed as argument.\n  \"\"\"\n  parser = _Parser(ignore_unknown_fields, descriptor_pool, max_recursion_depth)\n  parser.ConvertMessage(js_dict, message, '')\n  return message\n\n\n_INT_OR_FLOAT = (int, float)\n\n\nclass _Parser(object):\n  \"\"\"JSON format parser for protocol message.\"\"\"\n\n  def __init__(self, ignore_unknown_fields, descriptor_pool,\n               max_recursion_depth):\n    self.ignore_unknown_fields = ignore_unknown_fields\n    self.descriptor_pool = descriptor_pool\n    self.max_recursion_depth = max_recursion_depth\n    self.recursion_depth = 0\n\n  def ConvertMessage(self, value, message, path):\n    \"\"\"Convert a JSON object into a message.\n\n    Args:\n      value: A JSON object.\n      message: A WKT or regular protocol message to record the data.\n      path: parent path to log parse error info.\n\n    Raises:\n      ParseError: In case of convert problems.\n    \"\"\"\n    self.recursion_depth += 1\n    if self.recursion_depth > self.max_recursion_depth:\n      raise ParseError('Message too deep. Max recursion depth is {0}'.format(\n          self.max_recursion_depth))\n    message_descriptor = message.DESCRIPTOR\n    full_name = message_descriptor.full_name\n    if not path:\n      path = message_descriptor.name\n    if _IsWrapperMessage(message_descriptor):\n      self._ConvertWrapperMessage(value, message, path)\n    elif full_name in _WKTJSONMETHODS:\n      methodcaller(_WKTJSONMETHODS[full_name][1], value, message, path)(self)\n    else:\n      self._ConvertFieldValuePair(value, message, path)\n    self.recursion_depth -= 1\n\n  def _ConvertFieldValuePair(self, js, message, path):\n    \"\"\"Convert field value pairs into regular message.\n\n    Args:\n      js: A JSON object to convert the field value pairs.\n      message: A regular protocol message to record the data.\n      path: parent path to log parse error info.\n\n    Raises:\n      ParseError: In case of problems converting.\n    \"\"\"\n    names = []\n    message_descriptor = message.DESCRIPTOR\n    fields_by_json_name = dict((f.json_name, f)\n                               for f in message_descriptor.fields)\n    for name in js:\n      try:\n        field = fields_by_json_name.get(name, None)\n        if not field:\n          field = message_descriptor.fields_by_name.get(name, None)\n        if not field and _VALID_EXTENSION_NAME.match(name):\n          if not message_descriptor.is_extendable:\n            raise ParseError(\n                'Message type {0} does not have extensions at {1}'.format(\n                    message_descriptor.full_name, path))\n          identifier = name[1:-1]  # strip [] brackets\n          # pylint: disable=protected-access\n          field = message.Extensions._FindExtensionByName(identifier)\n          # pylint: enable=protected-access\n          if not field:\n            # Try looking for extension by the message type name, dropping the\n            # field name following the final . separator in full_name.\n            identifier = '.'.join(identifier.split('.')[:-1])\n            # pylint: disable=protected-access\n            field = message.Extensions._FindExtensionByName(identifier)\n            # pylint: enable=protected-access\n        if not field:\n          if self.ignore_unknown_fields:\n            continue\n          raise ParseError(\n              ('Message type \"{0}\" has no field named \"{1}\" at \"{2}\".\\n'\n               ' Available Fields(except extensions): \"{3}\"').format(\n                   message_descriptor.full_name, name, path,\n                   [f.json_name for f in message_descriptor.fields]))\n        if name in names:\n          raise ParseError('Message type \"{0}\" should not have multiple '\n                           '\"{1}\" fields at \"{2}\".'.format(\n                               message.DESCRIPTOR.full_name, name, path))\n        names.append(name)\n        value = js[name]\n        # Check no other oneof field is parsed.\n        if field.containing_oneof is not None and value is not None:\n          oneof_name = field.containing_oneof.name\n          if oneof_name in names:\n            raise ParseError('Message type \"{0}\" should not have multiple '\n                             '\"{1}\" oneof fields at \"{2}\".'.format(\n                                 message.DESCRIPTOR.full_name, oneof_name,\n                                 path))\n          names.append(oneof_name)\n\n        if value is None:\n          if (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE\n              and field.message_type.full_name == 'google.protobuf.Value'):\n            sub_message = getattr(message, field.name)\n            sub_message.null_value = 0\n          elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM\n                and field.enum_type.full_name == 'google.protobuf.NullValue'):\n            setattr(message, field.name, 0)\n          else:\n            message.ClearField(field.name)\n          continue\n\n        # Parse field value.\n        if _IsMapEntry(field):\n          message.ClearField(field.name)\n          self._ConvertMapFieldValue(value, message, field,\n                                     '{0}.{1}'.format(path, name))\n        elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n          message.ClearField(field.name)\n          if not isinstance(value, list):\n            raise ParseError('repeated field {0} must be in [] which is '\n                             '{1} at {2}'.format(name, value, path))\n          if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n            # Repeated message field.\n            for index, item in enumerate(value):\n              sub_message = getattr(message, field.name).add()\n              # None is a null_value in Value.\n              if (item is None and\n                  sub_message.DESCRIPTOR.full_name != 'google.protobuf.Value'):\n                raise ParseError('null is not allowed to be used as an element'\n                                 ' in a repeated field at {0}.{1}[{2}]'.format(\n                                     path, name, index))\n              self.ConvertMessage(item, sub_message,\n                                  '{0}.{1}[{2}]'.format(path, name, index))\n          else:\n            # Repeated scalar field.\n            for index, item in enumerate(value):\n              if item is None:\n                raise ParseError('null is not allowed to be used as an element'\n                                 ' in a repeated field at {0}.{1}[{2}]'.format(\n                                     path, name, index))\n              getattr(message, field.name).append(\n                  _ConvertScalarFieldValue(\n                      item, field, '{0}.{1}[{2}]'.format(path, name, index)))\n        elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n          if field.is_extension:\n            sub_message = message.Extensions[field]\n          else:\n            sub_message = getattr(message, field.name)\n          sub_message.SetInParent()\n          self.ConvertMessage(value, sub_message, '{0}.{1}'.format(path, name))\n        else:\n          if field.is_extension:\n            message.Extensions[field] = _ConvertScalarFieldValue(\n                value, field, '{0}.{1}'.format(path, name))\n          else:\n            setattr(\n                message, field.name,\n                _ConvertScalarFieldValue(value, field,\n                                         '{0}.{1}'.format(path, name)))\n      except ParseError as e:\n        if field and field.containing_oneof is None:\n          raise ParseError('Failed to parse {0} field: {1}.'.format(name, e))\n        else:\n          raise ParseError(str(e))\n      except ValueError as e:\n        raise ParseError('Failed to parse {0} field: {1}.'.format(name, e))\n      except TypeError as e:\n        raise ParseError('Failed to parse {0} field: {1}.'.format(name, e))\n\n  def _ConvertAnyMessage(self, value, message, path):\n    \"\"\"Convert a JSON representation into Any message.\"\"\"\n    if isinstance(value, dict) and not value:\n      return\n    try:\n      type_url = value['@type']\n    except KeyError:\n      raise ParseError(\n          '@type is missing when parsing any message at {0}'.format(path))\n\n    try:\n      sub_message = _CreateMessageFromTypeUrl(type_url, self.descriptor_pool)\n    except TypeError as e:\n      raise ParseError('{0} at {1}'.format(e, path))\n    message_descriptor = sub_message.DESCRIPTOR\n    full_name = message_descriptor.full_name\n    if _IsWrapperMessage(message_descriptor):\n      self._ConvertWrapperMessage(value['value'], sub_message,\n                                  '{0}.value'.format(path))\n    elif full_name in _WKTJSONMETHODS:\n      methodcaller(_WKTJSONMETHODS[full_name][1], value['value'], sub_message,\n                   '{0}.value'.format(path))(\n                       self)\n    else:\n      del value['@type']\n      self._ConvertFieldValuePair(value, sub_message, path)\n      value['@type'] = type_url\n    # Sets Any message\n    message.value = sub_message.SerializeToString()\n    message.type_url = type_url\n\n  def _ConvertGenericMessage(self, value, message, path):\n    \"\"\"Convert a JSON representation into message with FromJsonString.\"\"\"\n    # Duration, Timestamp, FieldMask have a FromJsonString method to do the\n    # conversion. Users can also call the method directly.\n    try:\n      message.FromJsonString(value)\n    except ValueError as e:\n      raise ParseError('{0} at {1}'.format(e, path))\n\n  def _ConvertValueMessage(self, value, message, path):\n    \"\"\"Convert a JSON representation into Value message.\"\"\"\n    if isinstance(value, dict):\n      self._ConvertStructMessage(value, message.struct_value, path)\n    elif isinstance(value, list):\n      self._ConvertListValueMessage(value, message.list_value, path)\n    elif value is None:\n      message.null_value = 0\n    elif isinstance(value, bool):\n      message.bool_value = value\n    elif isinstance(value, str):\n      message.string_value = value\n    elif isinstance(value, _INT_OR_FLOAT):\n      message.number_value = value\n    else:\n      raise ParseError('Value {0} has unexpected type {1} at {2}'.format(\n          value, type(value), path))\n\n  def _ConvertListValueMessage(self, value, message, path):\n    \"\"\"Convert a JSON representation into ListValue message.\"\"\"\n    if not isinstance(value, list):\n      raise ParseError('ListValue must be in [] which is {0} at {1}'.format(\n          value, path))\n    message.ClearField('values')\n    for index, item in enumerate(value):\n      self._ConvertValueMessage(item, message.values.add(),\n                                '{0}[{1}]'.format(path, index))\n\n  def _ConvertStructMessage(self, value, message, path):\n    \"\"\"Convert a JSON representation into Struct message.\"\"\"\n    if not isinstance(value, dict):\n      raise ParseError('Struct must be in a dict which is {0} at {1}'.format(\n          value, path))\n    # Clear will mark the struct as modified so it will be created even if\n    # there are no values.\n    message.Clear()\n    for key in value:\n      self._ConvertValueMessage(value[key], message.fields[key],\n                                '{0}.{1}'.format(path, key))\n    return\n\n  def _ConvertWrapperMessage(self, value, message, path):\n    \"\"\"Convert a JSON representation into Wrapper message.\"\"\"\n    field = message.DESCRIPTOR.fields_by_name['value']\n    setattr(\n        message, 'value',\n        _ConvertScalarFieldValue(value, field, path='{0}.value'.format(path)))\n\n  def _ConvertMapFieldValue(self, value, message, field, path):\n    \"\"\"Convert map field value for a message map field.\n\n    Args:\n      value: A JSON object to convert the map field value.\n      message: A protocol message to record the converted data.\n      field: The descriptor of the map field to be converted.\n      path: parent path to log parse error info.\n\n    Raises:\n      ParseError: In case of convert problems.\n    \"\"\"\n    if not isinstance(value, dict):\n      raise ParseError(\n          'Map field {0} must be in a dict which is {1} at {2}'.format(\n              field.name, value, path))\n    key_field = field.message_type.fields_by_name['key']\n    value_field = field.message_type.fields_by_name['value']\n    for key in value:\n      key_value = _ConvertScalarFieldValue(key, key_field,\n                                           '{0}.key'.format(path), True)\n      if value_field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n        self.ConvertMessage(value[key],\n                            getattr(message, field.name)[key_value],\n                            '{0}[{1}]'.format(path, key_value))\n      else:\n        getattr(message, field.name)[key_value] = _ConvertScalarFieldValue(\n            value[key], value_field, path='{0}[{1}]'.format(path, key_value))\n\n\ndef _ConvertScalarFieldValue(value, field, path, require_str=False):\n  \"\"\"Convert a single scalar field value.\n\n  Args:\n    value: A scalar value to convert the scalar field value.\n    field: The descriptor of the field to convert.\n    path: parent path to log parse error info.\n    require_str: If True, the field value must be a str.\n\n  Returns:\n    The converted scalar field value\n\n  Raises:\n    ParseError: In case of convert problems.\n  \"\"\"\n  try:\n    if field.cpp_type in _INT_TYPES:\n      return _ConvertInteger(value)\n    elif field.cpp_type in _FLOAT_TYPES:\n      return _ConvertFloat(value, field)\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL:\n      return _ConvertBool(value, require_str)\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING:\n      if field.type == descriptor.FieldDescriptor.TYPE_BYTES:\n        if isinstance(value, str):\n          encoded = value.encode('utf-8')\n        else:\n          encoded = value\n        # Add extra padding '='\n        padded_value = encoded + b'=' * (4 - len(encoded) % 4)\n        return base64.urlsafe_b64decode(padded_value)\n      else:\n        # Checking for unpaired surrogates appears to be unreliable,\n        # depending on the specific Python version, so we check manually.\n        if _UNPAIRED_SURROGATE_PATTERN.search(value):\n          raise ParseError('Unpaired surrogate')\n        return value\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM:\n      # Convert an enum value.\n      enum_value = field.enum_type.values_by_name.get(value, None)\n      if enum_value is None:\n        try:\n          number = int(value)\n          enum_value = field.enum_type.values_by_number.get(number, None)\n        except ValueError:\n          raise ParseError('Invalid enum value {0} for enum type {1}'.format(\n              value, field.enum_type.full_name))\n        if enum_value is None:\n          if field.file.syntax == 'proto3':\n            # Proto3 accepts unknown enums.\n            return number\n          raise ParseError('Invalid enum value {0} for enum type {1}'.format(\n              value, field.enum_type.full_name))\n      return enum_value.number\n  except ParseError as e:\n    raise ParseError('{0} at {1}'.format(e, path))\n\n\ndef _ConvertInteger(value):\n  \"\"\"Convert an integer.\n\n  Args:\n    value: A scalar value to convert.\n\n  Returns:\n    The integer value.\n\n  Raises:\n    ParseError: If an integer couldn't be consumed.\n  \"\"\"\n  if isinstance(value, float) and not value.is_integer():\n    raise ParseError('Couldn\\'t parse integer: {0}'.format(value))\n\n  if isinstance(value, str) and value.find(' ') != -1:\n    raise ParseError('Couldn\\'t parse integer: \"{0}\"'.format(value))\n\n  if isinstance(value, bool):\n    raise ParseError('Bool value {0} is not acceptable for '\n                     'integer field'.format(value))\n\n  return int(value)\n\n\ndef _ConvertFloat(value, field):\n  \"\"\"Convert an floating point number.\"\"\"\n  if isinstance(value, float):\n    if math.isnan(value):\n      raise ParseError('Couldn\\'t parse NaN, use quoted \"NaN\" instead')\n    if math.isinf(value):\n      if value > 0:\n        raise ParseError('Couldn\\'t parse Infinity or value too large, '\n                         'use quoted \"Infinity\" instead')\n      else:\n        raise ParseError('Couldn\\'t parse -Infinity or value too small, '\n                         'use quoted \"-Infinity\" instead')\n    if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT:\n      # pylint: disable=protected-access\n      if value > type_checkers._FLOAT_MAX:\n        raise ParseError('Float value too large')\n      # pylint: disable=protected-access\n      if value < type_checkers._FLOAT_MIN:\n        raise ParseError('Float value too small')\n  if value == 'nan':\n    raise ParseError('Couldn\\'t parse float \"nan\", use \"NaN\" instead')\n  try:\n    # Assume Python compatible syntax.\n    return float(value)\n  except ValueError:\n    # Check alternative spellings.\n    if value == _NEG_INFINITY:\n      return float('-inf')\n    elif value == _INFINITY:\n      return float('inf')\n    elif value == _NAN:\n      return float('nan')\n    else:\n      raise ParseError('Couldn\\'t parse float: {0}'.format(value))\n\n\ndef _ConvertBool(value, require_str):\n  \"\"\"Convert a boolean value.\n\n  Args:\n    value: A scalar value to convert.\n    require_str: If True, value must be a str.\n\n  Returns:\n    The bool parsed.\n\n  Raises:\n    ParseError: If a boolean value couldn't be consumed.\n  \"\"\"\n  if require_str:\n    if value == 'true':\n      return True\n    elif value == 'false':\n      return False\n    else:\n      raise ParseError('Expected \"true\" or \"false\", not {0}'.format(value))\n\n  if not isinstance(value, bool):\n    raise ParseError('Expected true or false without quotes')\n  return value\n\n_WKTJSONMETHODS = {\n    'google.protobuf.Any': ['_AnyMessageToJsonObject',\n                            '_ConvertAnyMessage'],\n    'google.protobuf.Duration': ['_GenericMessageToJsonObject',\n                                 '_ConvertGenericMessage'],\n    'google.protobuf.FieldMask': ['_GenericMessageToJsonObject',\n                                  '_ConvertGenericMessage'],\n    'google.protobuf.ListValue': ['_ListValueMessageToJsonObject',\n                                  '_ConvertListValueMessage'],\n    'google.protobuf.Struct': ['_StructMessageToJsonObject',\n                               '_ConvertStructMessage'],\n    'google.protobuf.Timestamp': ['_GenericMessageToJsonObject',\n                                  '_ConvertGenericMessage'],\n    'google.protobuf.Value': ['_ValueMessageToJsonObject',\n                              '_ConvertValueMessage']\n}\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/message.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n# TODO(robinson): We should just make these methods all \"pure-virtual\" and move\n# all implementation out, into reflection.py for now.\n\n\n\"\"\"Contains an abstract base class for protocol messages.\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\nclass Error(Exception):\n  \"\"\"Base error type for this module.\"\"\"\n  pass\n\n\nclass DecodeError(Error):\n  \"\"\"Exception raised when deserializing messages.\"\"\"\n  pass\n\n\nclass EncodeError(Error):\n  \"\"\"Exception raised when serializing messages.\"\"\"\n  pass\n\n\nclass Message(object):\n\n  \"\"\"Abstract base class for protocol messages.\n\n  Protocol message classes are almost always generated by the protocol\n  compiler.  These generated types subclass Message and implement the methods\n  shown below.\n  \"\"\"\n\n  # TODO(robinson): Link to an HTML document here.\n\n  # TODO(robinson): Document that instances of this class will also\n  # have an Extensions attribute with __getitem__ and __setitem__.\n  # Again, not sure how to best convey this.\n\n  # TODO(robinson): Document that the class must also have a static\n  #   RegisterExtension(extension_field) method.\n  #   Not sure how to best express at this point.\n\n  # TODO(robinson): Document these fields and methods.\n\n  __slots__ = []\n\n  #: The :class:`google.protobuf.descriptor.Descriptor` for this message type.\n  DESCRIPTOR = None\n\n  def __deepcopy__(self, memo=None):\n    clone = type(self)()\n    clone.MergeFrom(self)\n    return clone\n\n  def __eq__(self, other_msg):\n    \"\"\"Recursively compares two messages by value and structure.\"\"\"\n    raise NotImplementedError\n\n  def __ne__(self, other_msg):\n    # Can't just say self != other_msg, since that would infinitely recurse. :)\n    return not self == other_msg\n\n  def __hash__(self):\n    raise TypeError('unhashable object')\n\n  def __str__(self):\n    \"\"\"Outputs a human-readable representation of the message.\"\"\"\n    raise NotImplementedError\n\n  def __unicode__(self):\n    \"\"\"Outputs a human-readable representation of the message.\"\"\"\n    raise NotImplementedError\n\n  def MergeFrom(self, other_msg):\n    \"\"\"Merges the contents of the specified message into current message.\n\n    This method merges the contents of the specified message into the current\n    message. Singular fields that are set in the specified message overwrite\n    the corresponding fields in the current message. Repeated fields are\n    appended. Singular sub-messages and groups are recursively merged.\n\n    Args:\n      other_msg (Message): A message to merge into the current message.\n    \"\"\"\n    raise NotImplementedError\n\n  def CopyFrom(self, other_msg):\n    \"\"\"Copies the content of the specified message into the current message.\n\n    The method clears the current message and then merges the specified\n    message using MergeFrom.\n\n    Args:\n      other_msg (Message): A message to copy into the current one.\n    \"\"\"\n    if self is other_msg:\n      return\n    self.Clear()\n    self.MergeFrom(other_msg)\n\n  def Clear(self):\n    \"\"\"Clears all data that was set in the message.\"\"\"\n    raise NotImplementedError\n\n  def SetInParent(self):\n    \"\"\"Mark this as present in the parent.\n\n    This normally happens automatically when you assign a field of a\n    sub-message, but sometimes you want to make the sub-message\n    present while keeping it empty.  If you find yourself using this,\n    you may want to reconsider your design.\n    \"\"\"\n    raise NotImplementedError\n\n  def IsInitialized(self):\n    \"\"\"Checks if the message is initialized.\n\n    Returns:\n      bool: The method returns True if the message is initialized (i.e. all of\n      its required fields are set).\n    \"\"\"\n    raise NotImplementedError\n\n  # TODO(robinson): MergeFromString() should probably return None and be\n  # implemented in terms of a helper that returns the # of bytes read.  Our\n  # deserialization routines would use the helper when recursively\n  # deserializing, but the end user would almost always just want the no-return\n  # MergeFromString().\n\n  def MergeFromString(self, serialized):\n    \"\"\"Merges serialized protocol buffer data into this message.\n\n    When we find a field in `serialized` that is already present\n    in this message:\n\n    -   If it's a \"repeated\" field, we append to the end of our list.\n    -   Else, if it's a scalar, we overwrite our field.\n    -   Else, (it's a nonrepeated composite), we recursively merge\n        into the existing composite.\n\n    Args:\n      serialized (bytes): Any object that allows us to call\n        ``memoryview(serialized)`` to access a string of bytes using the\n        buffer interface.\n\n    Returns:\n      int: The number of bytes read from `serialized`.\n      For non-group messages, this will always be `len(serialized)`,\n      but for messages which are actually groups, this will\n      generally be less than `len(serialized)`, since we must\n      stop when we reach an ``END_GROUP`` tag.  Note that if\n      we *do* stop because of an ``END_GROUP`` tag, the number\n      of bytes returned does not include the bytes\n      for the ``END_GROUP`` tag information.\n\n    Raises:\n      DecodeError: if the input cannot be parsed.\n    \"\"\"\n    # TODO(robinson): Document handling of unknown fields.\n    # TODO(robinson): When we switch to a helper, this will return None.\n    raise NotImplementedError\n\n  def ParseFromString(self, serialized):\n    \"\"\"Parse serialized protocol buffer data into this message.\n\n    Like :func:`MergeFromString()`, except we clear the object first.\n\n    Raises:\n      message.DecodeError if the input cannot be parsed.\n    \"\"\"\n    self.Clear()\n    return self.MergeFromString(serialized)\n\n  def SerializeToString(self, **kwargs):\n    \"\"\"Serializes the protocol message to a binary string.\n\n    Keyword Args:\n      deterministic (bool): If true, requests deterministic serialization\n        of the protobuf, with predictable ordering of map keys.\n\n    Returns:\n      A binary string representation of the message if all of the required\n      fields in the message are set (i.e. the message is initialized).\n\n    Raises:\n      EncodeError: if the message isn't initialized (see :func:`IsInitialized`).\n    \"\"\"\n    raise NotImplementedError\n\n  def SerializePartialToString(self, **kwargs):\n    \"\"\"Serializes the protocol message to a binary string.\n\n    This method is similar to SerializeToString but doesn't check if the\n    message is initialized.\n\n    Keyword Args:\n      deterministic (bool): If true, requests deterministic serialization\n        of the protobuf, with predictable ordering of map keys.\n\n    Returns:\n      bytes: A serialized representation of the partial message.\n    \"\"\"\n    raise NotImplementedError\n\n  # TODO(robinson): Decide whether we like these better\n  # than auto-generated has_foo() and clear_foo() methods\n  # on the instances themselves.  This way is less consistent\n  # with C++, but it makes reflection-type access easier and\n  # reduces the number of magically autogenerated things.\n  #\n  # TODO(robinson): Be sure to document (and test) exactly\n  # which field names are accepted here.  Are we case-sensitive?\n  # What do we do with fields that share names with Python keywords\n  # like 'lambda' and 'yield'?\n  #\n  # nnorwitz says:\n  # \"\"\"\n  # Typically (in python), an underscore is appended to names that are\n  # keywords. So they would become lambda_ or yield_.\n  # \"\"\"\n  def ListFields(self):\n    \"\"\"Returns a list of (FieldDescriptor, value) tuples for present fields.\n\n    A message field is non-empty if HasField() would return true. A singular\n    primitive field is non-empty if HasField() would return true in proto2 or it\n    is non zero in proto3. A repeated field is non-empty if it contains at least\n    one element. The fields are ordered by field number.\n\n    Returns:\n      list[tuple(FieldDescriptor, value)]: field descriptors and values\n      for all fields in the message which are not empty. The values vary by\n      field type.\n    \"\"\"\n    raise NotImplementedError\n\n  def HasField(self, field_name):\n    \"\"\"Checks if a certain field is set for the message.\n\n    For a oneof group, checks if any field inside is set. Note that if the\n    field_name is not defined in the message descriptor, :exc:`ValueError` will\n    be raised.\n\n    Args:\n      field_name (str): The name of the field to check for presence.\n\n    Returns:\n      bool: Whether a value has been set for the named field.\n\n    Raises:\n      ValueError: if the `field_name` is not a member of this message.\n    \"\"\"\n    raise NotImplementedError\n\n  def ClearField(self, field_name):\n    \"\"\"Clears the contents of a given field.\n\n    Inside a oneof group, clears the field set. If the name neither refers to a\n    defined field or oneof group, :exc:`ValueError` is raised.\n\n    Args:\n      field_name (str): The name of the field to check for presence.\n\n    Raises:\n      ValueError: if the `field_name` is not a member of this message.\n    \"\"\"\n    raise NotImplementedError\n\n  def WhichOneof(self, oneof_group):\n    \"\"\"Returns the name of the field that is set inside a oneof group.\n\n    If no field is set, returns None.\n\n    Args:\n      oneof_group (str): the name of the oneof group to check.\n\n    Returns:\n      str or None: The name of the group that is set, or None.\n\n    Raises:\n      ValueError: no group with the given name exists\n    \"\"\"\n    raise NotImplementedError\n\n  def HasExtension(self, extension_handle):\n    \"\"\"Checks if a certain extension is present for this message.\n\n    Extensions are retrieved using the :attr:`Extensions` mapping (if present).\n\n    Args:\n      extension_handle: The handle for the extension to check.\n\n    Returns:\n      bool: Whether the extension is present for this message.\n\n    Raises:\n      KeyError: if the extension is repeated. Similar to repeated fields,\n        there is no separate notion of presence: a \"not present\" repeated\n        extension is an empty list.\n    \"\"\"\n    raise NotImplementedError\n\n  def ClearExtension(self, extension_handle):\n    \"\"\"Clears the contents of a given extension.\n\n    Args:\n      extension_handle: The handle for the extension to clear.\n    \"\"\"\n    raise NotImplementedError\n\n  def UnknownFields(self):\n    \"\"\"Returns the UnknownFieldSet.\n\n    Returns:\n      UnknownFieldSet: The unknown fields stored in this message.\n    \"\"\"\n    raise NotImplementedError\n\n  def DiscardUnknownFields(self):\n    \"\"\"Clears all fields in the :class:`UnknownFieldSet`.\n\n    This operation is recursive for nested message.\n    \"\"\"\n    raise NotImplementedError\n\n  def ByteSize(self):\n    \"\"\"Returns the serialized size of this message.\n\n    Recursively calls ByteSize() on all contained messages.\n\n    Returns:\n      int: The number of bytes required to serialize this message.\n    \"\"\"\n    raise NotImplementedError\n\n  @classmethod\n  def FromString(cls, s):\n    raise NotImplementedError\n\n  @staticmethod\n  def RegisterExtension(extension_handle):\n    raise NotImplementedError\n\n  def _SetListener(self, message_listener):\n    \"\"\"Internal method used by the protocol message implementation.\n    Clients should not call this directly.\n\n    Sets a listener that this message will call on certain state transitions.\n\n    The purpose of this method is to register back-edges from children to\n    parents at runtime, for the purpose of setting \"has\" bits and\n    byte-size-dirty bits in the parent and ancestor objects whenever a child or\n    descendant object is modified.\n\n    If the client wants to disconnect this Message from the object tree, she\n    explicitly sets callback to None.\n\n    If message_listener is None, unregisters any existing listener.  Otherwise,\n    message_listener must implement the MessageListener interface in\n    internal/message_listener.py, and we discard any listener registered\n    via a previous _SetListener() call.\n    \"\"\"\n    raise NotImplementedError\n\n  def __getstate__(self):\n    \"\"\"Support the pickle protocol.\"\"\"\n    return dict(serialized=self.SerializePartialToString())\n\n  def __setstate__(self, state):\n    \"\"\"Support the pickle protocol.\"\"\"\n    self.__init__()\n    serialized = state['serialized']\n    # On Python 3, using encoding='latin1' is required for unpickling\n    # protos pickled by Python 2.\n    if not isinstance(serialized, bytes):\n      serialized = serialized.encode('latin1')\n    self.ParseFromString(serialized)\n\n  def __reduce__(self):\n    message_descriptor = self.DESCRIPTOR\n    if message_descriptor.containing_type is None:\n      return type(self), (), self.__getstate__()\n    # the message type must be nested.\n    # Python does not pickle nested classes; use the symbol_database on the\n    # receiving end.\n    container = message_descriptor\n    return (_InternalConstructMessage, (container.full_name,),\n            self.__getstate__())\n\n\ndef _InternalConstructMessage(full_name):\n  \"\"\"Constructs a nested message.\"\"\"\n  from google.protobuf import symbol_database  # pylint:disable=g-import-not-at-top\n\n  return symbol_database.Default().GetSymbol(full_name)()\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/message_factory.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Provides a factory class for generating dynamic messages.\n\nThe easiest way to use this class is if you have access to the FileDescriptor\nprotos containing the messages you want to create you can just do the following:\n\nmessage_classes = message_factory.GetMessages(iterable_of_file_descriptors)\nmy_proto_instance = message_classes['some.proto.package.MessageName']()\n\"\"\"\n\n__author__ = 'matthewtoia@google.com (Matt Toia)'\n\nfrom google.protobuf.internal import api_implementation\nfrom google.protobuf import descriptor_pool\nfrom google.protobuf import message\n\nif api_implementation.Type() == 'cpp':\n  from google.protobuf.pyext import cpp_message as message_impl\nelse:\n  from google.protobuf.internal import python_message as message_impl\n\n\n# The type of all Message classes.\n_GENERATED_PROTOCOL_MESSAGE_TYPE = message_impl.GeneratedProtocolMessageType\n\n\nclass MessageFactory(object):\n  \"\"\"Factory for creating Proto2 messages from descriptors in a pool.\"\"\"\n\n  def __init__(self, pool=None):\n    \"\"\"Initializes a new factory.\"\"\"\n    self.pool = pool or descriptor_pool.DescriptorPool()\n\n    # local cache of all classes built from protobuf descriptors\n    self._classes = {}\n\n  def GetPrototype(self, descriptor):\n    \"\"\"Obtains a proto2 message class based on the passed in descriptor.\n\n    Passing a descriptor with a fully qualified name matching a previous\n    invocation will cause the same class to be returned.\n\n    Args:\n      descriptor: The descriptor to build from.\n\n    Returns:\n      A class describing the passed in descriptor.\n    \"\"\"\n    if descriptor not in self._classes:\n      result_class = self.CreatePrototype(descriptor)\n      # The assignment to _classes is redundant for the base implementation, but\n      # might avoid confusion in cases where CreatePrototype gets overridden and\n      # does not call the base implementation.\n      self._classes[descriptor] = result_class\n      return result_class\n    return self._classes[descriptor]\n\n  def CreatePrototype(self, descriptor):\n    \"\"\"Builds a proto2 message class based on the passed in descriptor.\n\n    Don't call this function directly, it always creates a new class. Call\n    GetPrototype() instead. This method is meant to be overridden in subblasses\n    to perform additional operations on the newly constructed class.\n\n    Args:\n      descriptor: The descriptor to build from.\n\n    Returns:\n      A class describing the passed in descriptor.\n    \"\"\"\n    descriptor_name = descriptor.name\n    result_class = _GENERATED_PROTOCOL_MESSAGE_TYPE(\n        descriptor_name,\n        (message.Message,),\n        {\n            'DESCRIPTOR': descriptor,\n            # If module not set, it wrongly points to message_factory module.\n            '__module__': None,\n        })\n    result_class._FACTORY = self  # pylint: disable=protected-access\n    # Assign in _classes before doing recursive calls to avoid infinite\n    # recursion.\n    self._classes[descriptor] = result_class\n    for field in descriptor.fields:\n      if field.message_type:\n        self.GetPrototype(field.message_type)\n    for extension in result_class.DESCRIPTOR.extensions:\n      if extension.containing_type not in self._classes:\n        self.GetPrototype(extension.containing_type)\n      extended_class = self._classes[extension.containing_type]\n      extended_class.RegisterExtension(extension)\n    return result_class\n\n  def GetMessages(self, files):\n    \"\"\"Gets all the messages from a specified file.\n\n    This will find and resolve dependencies, failing if the descriptor\n    pool cannot satisfy them.\n\n    Args:\n      files: The file names to extract messages from.\n\n    Returns:\n      A dictionary mapping proto names to the message classes. This will include\n      any dependent messages as well as any messages defined in the same file as\n      a specified message.\n    \"\"\"\n    result = {}\n    for file_name in files:\n      file_desc = self.pool.FindFileByName(file_name)\n      for desc in file_desc.message_types_by_name.values():\n        result[desc.full_name] = self.GetPrototype(desc)\n\n      # While the extension FieldDescriptors are created by the descriptor pool,\n      # the python classes created in the factory need them to be registered\n      # explicitly, which is done below.\n      #\n      # The call to RegisterExtension will specifically check if the\n      # extension was already registered on the object and either\n      # ignore the registration if the original was the same, or raise\n      # an error if they were different.\n\n      for extension in file_desc.extensions_by_name.values():\n        if extension.containing_type not in self._classes:\n          self.GetPrototype(extension.containing_type)\n        extended_class = self._classes[extension.containing_type]\n        extended_class.RegisterExtension(extension)\n    return result\n\n\n_FACTORY = MessageFactory()\n\n\ndef GetMessages(file_protos):\n  \"\"\"Builds a dictionary of all the messages available in a set of files.\n\n  Args:\n    file_protos: Iterable of FileDescriptorProto to build messages out of.\n\n  Returns:\n    A dictionary mapping proto names to the message classes. This will include\n    any dependent messages as well as any messages defined in the same file as\n    a specified message.\n  \"\"\"\n  # The cpp implementation of the protocol buffer library requires to add the\n  # message in topological order of the dependency graph.\n  file_by_name = {file_proto.name: file_proto for file_proto in file_protos}\n  def _AddFile(file_proto):\n    for dependency in file_proto.dependency:\n      if dependency in file_by_name:\n        # Remove from elements to be visited, in order to cut cycles.\n        _AddFile(file_by_name.pop(dependency))\n    _FACTORY.pool.Add(file_proto)\n  while file_by_name:\n    _AddFile(file_by_name.popitem()[1])\n  return _FACTORY.GetMessages([file_proto.name for file_proto in file_protos])\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/proto_builder.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Dynamic Protobuf class creator.\"\"\"\n\nfrom collections import OrderedDict\nimport hashlib\nimport os\n\nfrom google.protobuf import descriptor_pb2\nfrom google.protobuf import descriptor\nfrom google.protobuf import message_factory\n\n\ndef _GetMessageFromFactory(factory, full_name):\n  \"\"\"Get a proto class from the MessageFactory by name.\n\n  Args:\n    factory: a MessageFactory instance.\n    full_name: str, the fully qualified name of the proto type.\n  Returns:\n    A class, for the type identified by full_name.\n  Raises:\n    KeyError, if the proto is not found in the factory's descriptor pool.\n  \"\"\"\n  proto_descriptor = factory.pool.FindMessageTypeByName(full_name)\n  proto_cls = factory.GetPrototype(proto_descriptor)\n  return proto_cls\n\n\ndef MakeSimpleProtoClass(fields, full_name=None, pool=None):\n  \"\"\"Create a Protobuf class whose fields are basic types.\n\n  Note: this doesn't validate field names!\n\n  Args:\n    fields: dict of {name: field_type} mappings for each field in the proto. If\n        this is an OrderedDict the order will be maintained, otherwise the\n        fields will be sorted by name.\n    full_name: optional str, the fully-qualified name of the proto type.\n    pool: optional DescriptorPool instance.\n  Returns:\n    a class, the new protobuf class with a FileDescriptor.\n  \"\"\"\n  factory = message_factory.MessageFactory(pool=pool)\n\n  if full_name is not None:\n    try:\n      proto_cls = _GetMessageFromFactory(factory, full_name)\n      return proto_cls\n    except KeyError:\n      # The factory's DescriptorPool doesn't know about this class yet.\n      pass\n\n  # Get a list of (name, field_type) tuples from the fields dict. If fields was\n  # an OrderedDict we keep the order, but otherwise we sort the field to ensure\n  # consistent ordering.\n  field_items = fields.items()\n  if not isinstance(fields, OrderedDict):\n    field_items = sorted(field_items)\n\n  # Use a consistent file name that is unlikely to conflict with any imported\n  # proto files.\n  fields_hash = hashlib.sha1()\n  for f_name, f_type in field_items:\n    fields_hash.update(f_name.encode('utf-8'))\n    fields_hash.update(str(f_type).encode('utf-8'))\n  proto_file_name = fields_hash.hexdigest() + '.proto'\n\n  # If the proto is anonymous, use the same hash to name it.\n  if full_name is None:\n    full_name = ('net.proto2.python.public.proto_builder.AnonymousProto_' +\n                 fields_hash.hexdigest())\n    try:\n      proto_cls = _GetMessageFromFactory(factory, full_name)\n      return proto_cls\n    except KeyError:\n      # The factory's DescriptorPool doesn't know about this class yet.\n      pass\n\n  # This is the first time we see this proto: add a new descriptor to the pool.\n  factory.pool.Add(\n      _MakeFileDescriptorProto(proto_file_name, full_name, field_items))\n  return _GetMessageFromFactory(factory, full_name)\n\n\ndef _MakeFileDescriptorProto(proto_file_name, full_name, field_items):\n  \"\"\"Populate FileDescriptorProto for MessageFactory's DescriptorPool.\"\"\"\n  package, name = full_name.rsplit('.', 1)\n  file_proto = descriptor_pb2.FileDescriptorProto()\n  file_proto.name = os.path.join(package.replace('.', '/'), proto_file_name)\n  file_proto.package = package\n  desc_proto = file_proto.message_type.add()\n  desc_proto.name = name\n  for f_number, (f_name, f_type) in enumerate(field_items, 1):\n    field_proto = desc_proto.field.add()\n    field_proto.name = f_name\n    # # If the number falls in the reserved range, reassign it to the correct\n    # # number after the range.\n    if f_number >= descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER:\n      f_number += (\n          descriptor.FieldDescriptor.LAST_RESERVED_FIELD_NUMBER -\n          descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER + 1)\n    field_proto.number = f_number\n    field_proto.label = descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL\n    field_proto.type = f_type\n  return file_proto\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/pyext/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/pyext/cpp_message.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Protocol message implementation hooks for C++ implementation.\n\nContains helper functions used to create protocol message classes from\nDescriptor objects at runtime backed by the protocol buffer C++ API.\n\"\"\"\n\n__author__ = 'tibell@google.com (Johan Tibell)'\n\nfrom google.protobuf.pyext import _message\n\n\nclass GeneratedProtocolMessageType(_message.MessageMeta):\n\n  \"\"\"Metaclass for protocol message classes created at runtime from Descriptors.\n\n  The protocol compiler currently uses this metaclass to create protocol\n  message classes at runtime.  Clients can also manually create their own\n  classes at runtime, as in this example:\n\n  mydescriptor = Descriptor(.....)\n  factory = symbol_database.Default()\n  factory.pool.AddDescriptor(mydescriptor)\n  MyProtoClass = factory.GetPrototype(mydescriptor)\n  myproto_instance = MyProtoClass()\n  myproto.foo_field = 23\n  ...\n\n  The above example will not work for nested types. If you wish to include them,\n  use reflection.MakeClass() instead of manually instantiating the class in\n  order to create the appropriate class structure.\n  \"\"\"\n\n  # Must be consistent with the protocol-compiler code in\n  # proto2/compiler/internal/generator.*.\n  _DESCRIPTOR_KEY = 'DESCRIPTOR'\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/pyext/python_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/pyext/python.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\\"google/protobuf/pyext/python.proto\\x12\\x1fgoogle.protobuf.python.internal\\\"\\xbc\\x02\\n\\x0cTestAllTypes\\x12\\\\\\n\\x17repeated_nested_message\\x18\\x01 \\x03(\\x0b\\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\\x12\\\\\\n\\x17optional_nested_message\\x18\\x02 \\x01(\\x0b\\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\\x12\\x16\\n\\x0eoptional_int32\\x18\\x03 \\x01(\\x05\\x1aX\\n\\rNestedMessage\\x12\\n\\n\\x02\\x62\\x62\\x18\\x01 \\x01(\\x05\\x12;\\n\\x02\\x63\\x63\\x18\\x02 \\x01(\\x0b\\x32/.google.protobuf.python.internal.ForeignMessage\\\"&\\n\\x0e\\x46oreignMessage\\x12\\t\\n\\x01\\x63\\x18\\x01 \\x01(\\x05\\x12\\t\\n\\x01\\x64\\x18\\x02 \\x03(\\x05\\\"\\x1d\\n\\x11TestAllExtensions*\\x08\\x08\\x01\\x10\\x80\\x80\\x80\\x80\\x02:\\x9a\\x01\\n!optional_nested_message_extension\\x12\\x32.google.protobuf.python.internal.TestAllExtensions\\x18\\x01 \\x01(\\x0b\\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage:\\x9a\\x01\\n!repeated_nested_message_extension\\x12\\x32.google.protobuf.python.internal.TestAllExtensions\\x18\\x02 \\x03(\\x0b\\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessageB\\x02H\\x01')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.pyext.python_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n  TestAllExtensions.RegisterExtension(optional_nested_message_extension)\n  TestAllExtensions.RegisterExtension(repeated_nested_message_extension)\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'H\\001'\n  _TESTALLTYPES._serialized_start=72\n  _TESTALLTYPES._serialized_end=388\n  _TESTALLTYPES_NESTEDMESSAGE._serialized_start=300\n  _TESTALLTYPES_NESTEDMESSAGE._serialized_end=388\n  _FOREIGNMESSAGE._serialized_start=390\n  _FOREIGNMESSAGE._serialized_end=428\n  _TESTALLEXTENSIONS._serialized_start=430\n  _TESTALLEXTENSIONS._serialized_end=459\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/reflection.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n# This code is meant to work on Python 2.4 and above only.\n\n\"\"\"Contains a metaclass and helper functions used to create\nprotocol message classes from Descriptor objects at runtime.\n\nRecall that a metaclass is the \"type\" of a class.\n(A class is to a metaclass what an instance is to a class.)\n\nIn this case, we use the GeneratedProtocolMessageType metaclass\nto inject all the useful functionality into the classes\noutput by the protocol compiler at compile-time.\n\nThe upshot of all this is that the real implementation\ndetails for ALL pure-Python protocol buffers are *here in\nthis file*.\n\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\n\nfrom google.protobuf import message_factory\nfrom google.protobuf import symbol_database\n\n# The type of all Message classes.\n# Part of the public interface, but normally only used by message factories.\nGeneratedProtocolMessageType = message_factory._GENERATED_PROTOCOL_MESSAGE_TYPE\n\nMESSAGE_CLASS_CACHE = {}\n\n\n# Deprecated. Please NEVER use reflection.ParseMessage().\ndef ParseMessage(descriptor, byte_str):\n  \"\"\"Generate a new Message instance from this Descriptor and a byte string.\n\n  DEPRECATED: ParseMessage is deprecated because it is using MakeClass().\n  Please use MessageFactory.GetPrototype() instead.\n\n  Args:\n    descriptor: Protobuf Descriptor object\n    byte_str: Serialized protocol buffer byte string\n\n  Returns:\n    Newly created protobuf Message object.\n  \"\"\"\n  result_class = MakeClass(descriptor)\n  new_msg = result_class()\n  new_msg.ParseFromString(byte_str)\n  return new_msg\n\n\n# Deprecated. Please NEVER use reflection.MakeClass().\ndef MakeClass(descriptor):\n  \"\"\"Construct a class object for a protobuf described by descriptor.\n\n  DEPRECATED: use MessageFactory.GetPrototype() instead.\n\n  Args:\n    descriptor: A descriptor.Descriptor object describing the protobuf.\n  Returns:\n    The Message class object described by the descriptor.\n  \"\"\"\n  # Original implementation leads to duplicate message classes, which won't play\n  # well with extensions. Message factory info is also missing.\n  # Redirect to message_factory.\n  return symbol_database.Default().GetPrototype(descriptor)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/service.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"DEPRECATED:  Declares the RPC service interfaces.\n\nThis module declares the abstract interfaces underlying proto2 RPC\nservices.  These are intended to be independent of any particular RPC\nimplementation, so that proto2 services can be used on top of a variety\nof implementations.  Starting with version 2.3.0, RPC implementations should\nnot try to build on these, but should instead provide code generator plugins\nwhich generate code specific to the particular RPC implementation.  This way\nthe generated code can be more appropriate for the implementation in use\nand can avoid unnecessary layers of indirection.\n\"\"\"\n\n__author__ = 'petar@google.com (Petar Petrov)'\n\n\nclass RpcException(Exception):\n  \"\"\"Exception raised on failed blocking RPC method call.\"\"\"\n  pass\n\n\nclass Service(object):\n\n  \"\"\"Abstract base interface for protocol-buffer-based RPC services.\n\n  Services themselves are abstract classes (implemented either by servers or as\n  stubs), but they subclass this base interface. The methods of this\n  interface can be used to call the methods of the service without knowing\n  its exact type at compile time (analogous to the Message interface).\n  \"\"\"\n\n  def GetDescriptor():\n    \"\"\"Retrieves this service's descriptor.\"\"\"\n    raise NotImplementedError\n\n  def CallMethod(self, method_descriptor, rpc_controller,\n                 request, done):\n    \"\"\"Calls a method of the service specified by method_descriptor.\n\n    If \"done\" is None then the call is blocking and the response\n    message will be returned directly.  Otherwise the call is asynchronous\n    and \"done\" will later be called with the response value.\n\n    In the blocking case, RpcException will be raised on error.\n\n    Preconditions:\n\n    * method_descriptor.service == GetDescriptor\n    * request is of the exact same classes as returned by\n      GetRequestClass(method).\n    * After the call has started, the request must not be modified.\n    * \"rpc_controller\" is of the correct type for the RPC implementation being\n      used by this Service.  For stubs, the \"correct type\" depends on the\n      RpcChannel which the stub is using.\n\n    Postconditions:\n\n    * \"done\" will be called when the method is complete.  This may be\n      before CallMethod() returns or it may be at some point in the future.\n    * If the RPC failed, the response value passed to \"done\" will be None.\n      Further details about the failure can be found by querying the\n      RpcController.\n    \"\"\"\n    raise NotImplementedError\n\n  def GetRequestClass(self, method_descriptor):\n    \"\"\"Returns the class of the request message for the specified method.\n\n    CallMethod() requires that the request is of a particular subclass of\n    Message. GetRequestClass() gets the default instance of this required\n    type.\n\n    Example:\n      method = service.GetDescriptor().FindMethodByName(\"Foo\")\n      request = stub.GetRequestClass(method)()\n      request.ParseFromString(input)\n      service.CallMethod(method, request, callback)\n    \"\"\"\n    raise NotImplementedError\n\n  def GetResponseClass(self, method_descriptor):\n    \"\"\"Returns the class of the response message for the specified method.\n\n    This method isn't really needed, as the RpcChannel's CallMethod constructs\n    the response protocol message. It's provided anyway in case it is useful\n    for the caller to know the response type in advance.\n    \"\"\"\n    raise NotImplementedError\n\n\nclass RpcController(object):\n\n  \"\"\"An RpcController mediates a single method call.\n\n  The primary purpose of the controller is to provide a way to manipulate\n  settings specific to the RPC implementation and to find out about RPC-level\n  errors. The methods provided by the RpcController interface are intended\n  to be a \"least common denominator\" set of features which we expect all\n  implementations to support.  Specific implementations may provide more\n  advanced features (e.g. deadline propagation).\n  \"\"\"\n\n  # Client-side methods below\n\n  def Reset(self):\n    \"\"\"Resets the RpcController to its initial state.\n\n    After the RpcController has been reset, it may be reused in\n    a new call. Must not be called while an RPC is in progress.\n    \"\"\"\n    raise NotImplementedError\n\n  def Failed(self):\n    \"\"\"Returns true if the call failed.\n\n    After a call has finished, returns true if the call failed.  The possible\n    reasons for failure depend on the RPC implementation.  Failed() must not\n    be called before a call has finished.  If Failed() returns true, the\n    contents of the response message are undefined.\n    \"\"\"\n    raise NotImplementedError\n\n  def ErrorText(self):\n    \"\"\"If Failed is true, returns a human-readable description of the error.\"\"\"\n    raise NotImplementedError\n\n  def StartCancel(self):\n    \"\"\"Initiate cancellation.\n\n    Advises the RPC system that the caller desires that the RPC call be\n    canceled.  The RPC system may cancel it immediately, may wait awhile and\n    then cancel it, or may not even cancel the call at all.  If the call is\n    canceled, the \"done\" callback will still be called and the RpcController\n    will indicate that the call failed at that time.\n    \"\"\"\n    raise NotImplementedError\n\n  # Server-side methods below\n\n  def SetFailed(self, reason):\n    \"\"\"Sets a failure reason.\n\n    Causes Failed() to return true on the client side.  \"reason\" will be\n    incorporated into the message returned by ErrorText().  If you find\n    you need to return machine-readable information about failures, you\n    should incorporate it into your response protocol buffer and should\n    NOT call SetFailed().\n    \"\"\"\n    raise NotImplementedError\n\n  def IsCanceled(self):\n    \"\"\"Checks if the client cancelled the RPC.\n\n    If true, indicates that the client canceled the RPC, so the server may\n    as well give up on replying to it.  The server should still call the\n    final \"done\" callback.\n    \"\"\"\n    raise NotImplementedError\n\n  def NotifyOnCancel(self, callback):\n    \"\"\"Sets a callback to invoke on cancel.\n\n    Asks that the given callback be called when the RPC is canceled.  The\n    callback will always be called exactly once.  If the RPC completes without\n    being canceled, the callback will be called after completion.  If the RPC\n    has already been canceled when NotifyOnCancel() is called, the callback\n    will be called immediately.\n\n    NotifyOnCancel() must be called no more than once per request.\n    \"\"\"\n    raise NotImplementedError\n\n\nclass RpcChannel(object):\n\n  \"\"\"Abstract interface for an RPC channel.\n\n  An RpcChannel represents a communication line to a service which can be used\n  to call that service's methods.  The service may be running on another\n  machine. Normally, you should not use an RpcChannel directly, but instead\n  construct a stub {@link Service} wrapping it.  Example:\n\n  Example:\n    RpcChannel channel = rpcImpl.Channel(\"remotehost.example.com:1234\")\n    RpcController controller = rpcImpl.Controller()\n    MyService service = MyService_Stub(channel)\n    service.MyMethod(controller, request, callback)\n  \"\"\"\n\n  def CallMethod(self, method_descriptor, rpc_controller,\n                 request, response_class, done):\n    \"\"\"Calls the method identified by the descriptor.\n\n    Call the given method of the remote service.  The signature of this\n    procedure looks the same as Service.CallMethod(), but the requirements\n    are less strict in one important way:  the request object doesn't have to\n    be of any specific class as long as its descriptor is method.input_type.\n    \"\"\"\n    raise NotImplementedError\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/service_reflection.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Contains metaclasses used to create protocol service and service stub\nclasses from ServiceDescriptor objects at runtime.\n\nThe GeneratedServiceType and GeneratedServiceStubType metaclasses are used to\ninject all useful functionality into the classes output by the protocol\ncompiler at compile-time.\n\"\"\"\n\n__author__ = 'petar@google.com (Petar Petrov)'\n\n\nclass GeneratedServiceType(type):\n\n  \"\"\"Metaclass for service classes created at runtime from ServiceDescriptors.\n\n  Implementations for all methods described in the Service class are added here\n  by this class. We also create properties to allow getting/setting all fields\n  in the protocol message.\n\n  The protocol compiler currently uses this metaclass to create protocol service\n  classes at runtime. Clients can also manually create their own classes at\n  runtime, as in this example::\n\n    mydescriptor = ServiceDescriptor(.....)\n    class MyProtoService(service.Service):\n      __metaclass__ = GeneratedServiceType\n      DESCRIPTOR = mydescriptor\n    myservice_instance = MyProtoService()\n    # ...\n  \"\"\"\n\n  _DESCRIPTOR_KEY = 'DESCRIPTOR'\n\n  def __init__(cls, name, bases, dictionary):\n    \"\"\"Creates a message service class.\n\n    Args:\n      name: Name of the class (ignored, but required by the metaclass\n        protocol).\n      bases: Base classes of the class being constructed.\n      dictionary: The class dictionary of the class being constructed.\n        dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object\n        describing this protocol service type.\n    \"\"\"\n    # Don't do anything if this class doesn't have a descriptor. This happens\n    # when a service class is subclassed.\n    if GeneratedServiceType._DESCRIPTOR_KEY not in dictionary:\n      return\n\n    descriptor = dictionary[GeneratedServiceType._DESCRIPTOR_KEY]\n    service_builder = _ServiceBuilder(descriptor)\n    service_builder.BuildService(cls)\n    cls.DESCRIPTOR = descriptor\n\n\nclass GeneratedServiceStubType(GeneratedServiceType):\n\n  \"\"\"Metaclass for service stubs created at runtime from ServiceDescriptors.\n\n  This class has similar responsibilities as GeneratedServiceType, except that\n  it creates the service stub classes.\n  \"\"\"\n\n  _DESCRIPTOR_KEY = 'DESCRIPTOR'\n\n  def __init__(cls, name, bases, dictionary):\n    \"\"\"Creates a message service stub class.\n\n    Args:\n      name: Name of the class (ignored, here).\n      bases: Base classes of the class being constructed.\n      dictionary: The class dictionary of the class being constructed.\n        dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object\n        describing this protocol service type.\n    \"\"\"\n    super(GeneratedServiceStubType, cls).__init__(name, bases, dictionary)\n    # Don't do anything if this class doesn't have a descriptor. This happens\n    # when a service stub is subclassed.\n    if GeneratedServiceStubType._DESCRIPTOR_KEY not in dictionary:\n      return\n\n    descriptor = dictionary[GeneratedServiceStubType._DESCRIPTOR_KEY]\n    service_stub_builder = _ServiceStubBuilder(descriptor)\n    service_stub_builder.BuildServiceStub(cls)\n\n\nclass _ServiceBuilder(object):\n\n  \"\"\"This class constructs a protocol service class using a service descriptor.\n\n  Given a service descriptor, this class constructs a class that represents\n  the specified service descriptor. One service builder instance constructs\n  exactly one service class. That means all instances of that class share the\n  same builder.\n  \"\"\"\n\n  def __init__(self, service_descriptor):\n    \"\"\"Initializes an instance of the service class builder.\n\n    Args:\n      service_descriptor: ServiceDescriptor to use when constructing the\n        service class.\n    \"\"\"\n    self.descriptor = service_descriptor\n\n  def BuildService(builder, cls):\n    \"\"\"Constructs the service class.\n\n    Args:\n      cls: The class that will be constructed.\n    \"\"\"\n\n    # CallMethod needs to operate with an instance of the Service class. This\n    # internal wrapper function exists only to be able to pass the service\n    # instance to the method that does the real CallMethod work.\n    # Making sure to use exact argument names from the abstract interface in\n    # service.py to match the type signature\n    def _WrapCallMethod(self, method_descriptor, rpc_controller, request, done):\n      return builder._CallMethod(self, method_descriptor, rpc_controller,\n                                 request, done)\n\n    def _WrapGetRequestClass(self, method_descriptor):\n      return builder._GetRequestClass(method_descriptor)\n\n    def _WrapGetResponseClass(self, method_descriptor):\n      return builder._GetResponseClass(method_descriptor)\n\n    builder.cls = cls\n    cls.CallMethod = _WrapCallMethod\n    cls.GetDescriptor = staticmethod(lambda: builder.descriptor)\n    cls.GetDescriptor.__doc__ = 'Returns the service descriptor.'\n    cls.GetRequestClass = _WrapGetRequestClass\n    cls.GetResponseClass = _WrapGetResponseClass\n    for method in builder.descriptor.methods:\n      setattr(cls, method.name, builder._GenerateNonImplementedMethod(method))\n\n  def _CallMethod(self, srvc, method_descriptor,\n                  rpc_controller, request, callback):\n    \"\"\"Calls the method described by a given method descriptor.\n\n    Args:\n      srvc: Instance of the service for which this method is called.\n      method_descriptor: Descriptor that represent the method to call.\n      rpc_controller: RPC controller to use for this method's execution.\n      request: Request protocol message.\n      callback: A callback to invoke after the method has completed.\n    \"\"\"\n    if method_descriptor.containing_service != self.descriptor:\n      raise RuntimeError(\n          'CallMethod() given method descriptor for wrong service type.')\n    method = getattr(srvc, method_descriptor.name)\n    return method(rpc_controller, request, callback)\n\n  def _GetRequestClass(self, method_descriptor):\n    \"\"\"Returns the class of the request protocol message.\n\n    Args:\n      method_descriptor: Descriptor of the method for which to return the\n        request protocol message class.\n\n    Returns:\n      A class that represents the input protocol message of the specified\n      method.\n    \"\"\"\n    if method_descriptor.containing_service != self.descriptor:\n      raise RuntimeError(\n          'GetRequestClass() given method descriptor for wrong service type.')\n    return method_descriptor.input_type._concrete_class\n\n  def _GetResponseClass(self, method_descriptor):\n    \"\"\"Returns the class of the response protocol message.\n\n    Args:\n      method_descriptor: Descriptor of the method for which to return the\n        response protocol message class.\n\n    Returns:\n      A class that represents the output protocol message of the specified\n      method.\n    \"\"\"\n    if method_descriptor.containing_service != self.descriptor:\n      raise RuntimeError(\n          'GetResponseClass() given method descriptor for wrong service type.')\n    return method_descriptor.output_type._concrete_class\n\n  def _GenerateNonImplementedMethod(self, method):\n    \"\"\"Generates and returns a method that can be set for a service methods.\n\n    Args:\n      method: Descriptor of the service method for which a method is to be\n        generated.\n\n    Returns:\n      A method that can be added to the service class.\n    \"\"\"\n    return lambda inst, rpc_controller, request, callback: (\n        self._NonImplementedMethod(method.name, rpc_controller, callback))\n\n  def _NonImplementedMethod(self, method_name, rpc_controller, callback):\n    \"\"\"The body of all methods in the generated service class.\n\n    Args:\n      method_name: Name of the method being executed.\n      rpc_controller: RPC controller used to execute this method.\n      callback: A callback which will be invoked when the method finishes.\n    \"\"\"\n    rpc_controller.SetFailed('Method %s not implemented.' % method_name)\n    callback(None)\n\n\nclass _ServiceStubBuilder(object):\n\n  \"\"\"Constructs a protocol service stub class using a service descriptor.\n\n  Given a service descriptor, this class constructs a suitable stub class.\n  A stub is just a type-safe wrapper around an RpcChannel which emulates a\n  local implementation of the service.\n\n  One service stub builder instance constructs exactly one class. It means all\n  instances of that class share the same service stub builder.\n  \"\"\"\n\n  def __init__(self, service_descriptor):\n    \"\"\"Initializes an instance of the service stub class builder.\n\n    Args:\n      service_descriptor: ServiceDescriptor to use when constructing the\n        stub class.\n    \"\"\"\n    self.descriptor = service_descriptor\n\n  def BuildServiceStub(self, cls):\n    \"\"\"Constructs the stub class.\n\n    Args:\n      cls: The class that will be constructed.\n    \"\"\"\n\n    def _ServiceStubInit(stub, rpc_channel):\n      stub.rpc_channel = rpc_channel\n    self.cls = cls\n    cls.__init__ = _ServiceStubInit\n    for method in self.descriptor.methods:\n      setattr(cls, method.name, self._GenerateStubMethod(method))\n\n  def _GenerateStubMethod(self, method):\n    return (lambda inst, rpc_controller, request, callback=None:\n        self._StubMethod(inst, method, rpc_controller, request, callback))\n\n  def _StubMethod(self, stub, method_descriptor,\n                  rpc_controller, request, callback):\n    \"\"\"The body of all service methods in the generated stub class.\n\n    Args:\n      stub: Stub instance.\n      method_descriptor: Descriptor of the invoked method.\n      rpc_controller: Rpc controller to execute the method.\n      request: Request protocol message.\n      callback: A callback to execute when the method finishes.\n    Returns:\n      Response message (in case of blocking call).\n    \"\"\"\n    return stub.rpc_channel.CallMethod(\n        method_descriptor, rpc_controller, request,\n        method_descriptor.output_type._concrete_class, callback)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/source_context_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/source_context.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n$google/protobuf/source_context.proto\\x12\\x0fgoogle.protobuf\\\"\\\"\\n\\rSourceContext\\x12\\x11\\n\\tfile_name\\x18\\x01 \\x01(\\tB\\x8a\\x01\\n\\x13\\x63om.google.protobufB\\x12SourceContextProtoP\\x01Z6google.golang.org/protobuf/types/known/sourcecontextpb\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.source_context_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\022SourceContextProtoP\\001Z6google.golang.org/protobuf/types/known/sourcecontextpb\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _SOURCECONTEXT._serialized_start=57\n  _SOURCECONTEXT._serialized_end=91\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/struct_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/struct.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x1cgoogle/protobuf/struct.proto\\x12\\x0fgoogle.protobuf\\\"\\x84\\x01\\n\\x06Struct\\x12\\x33\\n\\x06\\x66ields\\x18\\x01 \\x03(\\x0b\\x32#.google.protobuf.Struct.FieldsEntry\\x1a\\x45\\n\\x0b\\x46ieldsEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12%\\n\\x05value\\x18\\x02 \\x01(\\x0b\\x32\\x16.google.protobuf.Value:\\x02\\x38\\x01\\\"\\xea\\x01\\n\\x05Value\\x12\\x30\\n\\nnull_value\\x18\\x01 \\x01(\\x0e\\x32\\x1a.google.protobuf.NullValueH\\x00\\x12\\x16\\n\\x0cnumber_value\\x18\\x02 \\x01(\\x01H\\x00\\x12\\x16\\n\\x0cstring_value\\x18\\x03 \\x01(\\tH\\x00\\x12\\x14\\n\\nbool_value\\x18\\x04 \\x01(\\x08H\\x00\\x12/\\n\\x0cstruct_value\\x18\\x05 \\x01(\\x0b\\x32\\x17.google.protobuf.StructH\\x00\\x12\\x30\\n\\nlist_value\\x18\\x06 \\x01(\\x0b\\x32\\x1a.google.protobuf.ListValueH\\x00\\x42\\x06\\n\\x04kind\\\"3\\n\\tListValue\\x12&\\n\\x06values\\x18\\x01 \\x03(\\x0b\\x32\\x16.google.protobuf.Value*\\x1b\\n\\tNullValue\\x12\\x0e\\n\\nNULL_VALUE\\x10\\x00\\x42\\x7f\\n\\x13\\x63om.google.protobufB\\x0bStructProtoP\\x01Z/google.golang.org/protobuf/types/known/structpb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.struct_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\013StructProtoP\\001Z/google.golang.org/protobuf/types/known/structpb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _STRUCT_FIELDSENTRY._options = None\n  _STRUCT_FIELDSENTRY._serialized_options = b'8\\001'\n  _NULLVALUE._serialized_start=474\n  _NULLVALUE._serialized_end=501\n  _STRUCT._serialized_start=50\n  _STRUCT._serialized_end=182\n  _STRUCT_FIELDSENTRY._serialized_start=113\n  _STRUCT_FIELDSENTRY._serialized_end=182\n  _VALUE._serialized_start=185\n  _VALUE._serialized_end=419\n  _LISTVALUE._serialized_start=421\n  _LISTVALUE._serialized_end=472\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/symbol_database.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"A database of Python protocol buffer generated symbols.\n\nSymbolDatabase is the MessageFactory for messages generated at compile time,\nand makes it easy to create new instances of a registered type, given only the\ntype's protocol buffer symbol name.\n\nExample usage::\n\n  db = symbol_database.SymbolDatabase()\n\n  # Register symbols of interest, from one or multiple files.\n  db.RegisterFileDescriptor(my_proto_pb2.DESCRIPTOR)\n  db.RegisterMessage(my_proto_pb2.MyMessage)\n  db.RegisterEnumDescriptor(my_proto_pb2.MyEnum.DESCRIPTOR)\n\n  # The database can be used as a MessageFactory, to generate types based on\n  # their name:\n  types = db.GetMessages(['my_proto.proto'])\n  my_message_instance = types['MyMessage']()\n\n  # The database's underlying descriptor pool can be queried, so it's not\n  # necessary to know a type's filename to be able to generate it:\n  filename = db.pool.FindFileContainingSymbol('MyMessage')\n  my_message_instance = db.GetMessages([filename])['MyMessage']()\n\n  # This functionality is also provided directly via a convenience method:\n  my_message_instance = db.GetSymbol('MyMessage')()\n\"\"\"\n\n\nfrom google.protobuf.internal import api_implementation\nfrom google.protobuf import descriptor_pool\nfrom google.protobuf import message_factory\n\n\nclass SymbolDatabase(message_factory.MessageFactory):\n  \"\"\"A database of Python generated symbols.\"\"\"\n\n  def RegisterMessage(self, message):\n    \"\"\"Registers the given message type in the local database.\n\n    Calls to GetSymbol() and GetMessages() will return messages registered here.\n\n    Args:\n      message: A :class:`google.protobuf.message.Message` subclass (or\n        instance); its descriptor will be registered.\n\n    Returns:\n      The provided message.\n    \"\"\"\n\n    desc = message.DESCRIPTOR\n    self._classes[desc] = message\n    self.RegisterMessageDescriptor(desc)\n    return message\n\n  def RegisterMessageDescriptor(self, message_descriptor):\n    \"\"\"Registers the given message descriptor in the local database.\n\n    Args:\n      message_descriptor (Descriptor): the message descriptor to add.\n    \"\"\"\n    if api_implementation.Type() == 'python':\n      # pylint: disable=protected-access\n      self.pool._AddDescriptor(message_descriptor)\n\n  def RegisterEnumDescriptor(self, enum_descriptor):\n    \"\"\"Registers the given enum descriptor in the local database.\n\n    Args:\n      enum_descriptor (EnumDescriptor): The enum descriptor to register.\n\n    Returns:\n      EnumDescriptor: The provided descriptor.\n    \"\"\"\n    if api_implementation.Type() == 'python':\n      # pylint: disable=protected-access\n      self.pool._AddEnumDescriptor(enum_descriptor)\n    return enum_descriptor\n\n  def RegisterServiceDescriptor(self, service_descriptor):\n    \"\"\"Registers the given service descriptor in the local database.\n\n    Args:\n      service_descriptor (ServiceDescriptor): the service descriptor to\n        register.\n    \"\"\"\n    if api_implementation.Type() == 'python':\n      # pylint: disable=protected-access\n      self.pool._AddServiceDescriptor(service_descriptor)\n\n  def RegisterFileDescriptor(self, file_descriptor):\n    \"\"\"Registers the given file descriptor in the local database.\n\n    Args:\n      file_descriptor (FileDescriptor): The file descriptor to register.\n    \"\"\"\n    if api_implementation.Type() == 'python':\n      # pylint: disable=protected-access\n      self.pool._InternalAddFileDescriptor(file_descriptor)\n\n  def GetSymbol(self, symbol):\n    \"\"\"Tries to find a symbol in the local database.\n\n    Currently, this method only returns message.Message instances, however, if\n    may be extended in future to support other symbol types.\n\n    Args:\n      symbol (str): a protocol buffer symbol.\n\n    Returns:\n      A Python class corresponding to the symbol.\n\n    Raises:\n      KeyError: if the symbol could not be found.\n    \"\"\"\n\n    return self._classes[self.pool.FindMessageTypeByName(symbol)]\n\n  def GetMessages(self, files):\n    # TODO(amauryfa): Fix the differences with MessageFactory.\n    \"\"\"Gets all registered messages from a specified file.\n\n    Only messages already created and registered will be returned; (this is the\n    case for imported _pb2 modules)\n    But unlike MessageFactory, this version also returns already defined nested\n    messages, but does not register any message extensions.\n\n    Args:\n      files (list[str]): The file names to extract messages from.\n\n    Returns:\n      A dictionary mapping proto names to the message classes.\n\n    Raises:\n      KeyError: if a file could not be found.\n    \"\"\"\n\n    def _GetAllMessages(desc):\n      \"\"\"Walk a message Descriptor and recursively yields all message names.\"\"\"\n      yield desc\n      for msg_desc in desc.nested_types:\n        for nested_desc in _GetAllMessages(msg_desc):\n          yield nested_desc\n\n    result = {}\n    for file_name in files:\n      file_desc = self.pool.FindFileByName(file_name)\n      for msg_desc in file_desc.message_types_by_name.values():\n        for desc in _GetAllMessages(msg_desc):\n          try:\n            result[desc.full_name] = self._classes[desc]\n          except KeyError:\n            # This descriptor has no registered class, skip it.\n            pass\n    return result\n\n\n_DEFAULT = SymbolDatabase(pool=descriptor_pool.Default())\n\n\ndef Default():\n  \"\"\"Returns the default SymbolDatabase.\"\"\"\n  return _DEFAULT\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/text_encoding.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Encoding related utilities.\"\"\"\nimport re\n\n_cescape_chr_to_symbol_map = {}\n_cescape_chr_to_symbol_map[9] = r'\\t'  # optional escape\n_cescape_chr_to_symbol_map[10] = r'\\n'  # optional escape\n_cescape_chr_to_symbol_map[13] = r'\\r'  # optional escape\n_cescape_chr_to_symbol_map[34] = r'\\\"'  # necessary escape\n_cescape_chr_to_symbol_map[39] = r\"\\'\"  # optional escape\n_cescape_chr_to_symbol_map[92] = r'\\\\'  # necessary escape\n\n# Lookup table for unicode\n_cescape_unicode_to_str = [chr(i) for i in range(0, 256)]\nfor byte, string in _cescape_chr_to_symbol_map.items():\n  _cescape_unicode_to_str[byte] = string\n\n# Lookup table for non-utf8, with necessary escapes at (o >= 127 or o < 32)\n_cescape_byte_to_str = ([r'\\%03o' % i for i in range(0, 32)] +\n                        [chr(i) for i in range(32, 127)] +\n                        [r'\\%03o' % i for i in range(127, 256)])\nfor byte, string in _cescape_chr_to_symbol_map.items():\n  _cescape_byte_to_str[byte] = string\ndel byte, string\n\n\ndef CEscape(text, as_utf8):\n  # type: (...) -> str\n  \"\"\"Escape a bytes string for use in an text protocol buffer.\n\n  Args:\n    text: A byte string to be escaped.\n    as_utf8: Specifies if result may contain non-ASCII characters.\n        In Python 3 this allows unescaped non-ASCII Unicode characters.\n        In Python 2 the return value will be valid UTF-8 rather than only ASCII.\n  Returns:\n    Escaped string (str).\n  \"\"\"\n  # Python's text.encode() 'string_escape' or 'unicode_escape' codecs do not\n  # satisfy our needs; they encodes unprintable characters using two-digit hex\n  # escapes whereas our C++ unescaping function allows hex escapes to be any\n  # length.  So, \"\\0011\".encode('string_escape') ends up being \"\\\\x011\", which\n  # will be decoded in C++ as a single-character string with char code 0x11.\n  text_is_unicode = isinstance(text, str)\n  if as_utf8 and text_is_unicode:\n    # We're already unicode, no processing beyond control char escapes.\n    return text.translate(_cescape_chr_to_symbol_map)\n  ord_ = ord if text_is_unicode else lambda x: x  # bytes iterate as ints.\n  if as_utf8:\n    return ''.join(_cescape_unicode_to_str[ord_(c)] for c in text)\n  return ''.join(_cescape_byte_to_str[ord_(c)] for c in text)\n\n\n_CUNESCAPE_HEX = re.compile(r'(\\\\+)x([0-9a-fA-F])(?![0-9a-fA-F])')\n\n\ndef CUnescape(text):\n  # type: (str) -> bytes\n  \"\"\"Unescape a text string with C-style escape sequences to UTF-8 bytes.\n\n  Args:\n    text: The data to parse in a str.\n  Returns:\n    A byte string.\n  \"\"\"\n\n  def ReplaceHex(m):\n    # Only replace the match if the number of leading back slashes is odd. i.e.\n    # the slash itself is not escaped.\n    if len(m.group(1)) & 1:\n      return m.group(1) + 'x0' + m.group(2)\n    return m.group(0)\n\n  # This is required because the 'string_escape' encoding doesn't\n  # allow single-digit hex escapes (like '\\xf').\n  result = _CUNESCAPE_HEX.sub(ReplaceHex, text)\n\n  return (result.encode('utf-8')  # Make it bytes to allow decode.\n          .decode('unicode_escape')\n          # Make it bytes again to return the proper type.\n          .encode('raw_unicode_escape'))\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/text_format.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Contains routines for printing protocol messages in text format.\n\nSimple usage example::\n\n  # Create a proto object and serialize it to a text proto string.\n  message = my_proto_pb2.MyMessage(foo='bar')\n  text_proto = text_format.MessageToString(message)\n\n  # Parse a text proto string.\n  message = text_format.Parse(text_proto, my_proto_pb2.MyMessage())\n\"\"\"\n\n__author__ = 'kenton@google.com (Kenton Varda)'\n\n# TODO(b/129989314) Import thread contention leads to test failures.\nimport encodings.raw_unicode_escape  # pylint: disable=unused-import\nimport encodings.unicode_escape  # pylint: disable=unused-import\nimport io\nimport math\nimport re\n\nfrom google.protobuf.internal import decoder\nfrom google.protobuf.internal import type_checkers\nfrom google.protobuf import descriptor\nfrom google.protobuf import text_encoding\n\n# pylint: disable=g-import-not-at-top\n__all__ = ['MessageToString', 'Parse', 'PrintMessage', 'PrintField',\n           'PrintFieldValue', 'Merge', 'MessageToBytes']\n\n_INTEGER_CHECKERS = (type_checkers.Uint32ValueChecker(),\n                     type_checkers.Int32ValueChecker(),\n                     type_checkers.Uint64ValueChecker(),\n                     type_checkers.Int64ValueChecker())\n_FLOAT_INFINITY = re.compile('-?inf(?:inity)?f?$', re.IGNORECASE)\n_FLOAT_NAN = re.compile('nanf?$', re.IGNORECASE)\n_QUOTES = frozenset((\"'\", '\"'))\n_ANY_FULL_TYPE_NAME = 'google.protobuf.Any'\n\n\nclass Error(Exception):\n  \"\"\"Top-level module error for text_format.\"\"\"\n\n\nclass ParseError(Error):\n  \"\"\"Thrown in case of text parsing or tokenizing error.\"\"\"\n\n  def __init__(self, message=None, line=None, column=None):\n    if message is not None and line is not None:\n      loc = str(line)\n      if column is not None:\n        loc += ':{0}'.format(column)\n      message = '{0} : {1}'.format(loc, message)\n    if message is not None:\n      super(ParseError, self).__init__(message)\n    else:\n      super(ParseError, self).__init__()\n    self._line = line\n    self._column = column\n\n  def GetLine(self):\n    return self._line\n\n  def GetColumn(self):\n    return self._column\n\n\nclass TextWriter(object):\n\n  def __init__(self, as_utf8):\n    self._writer = io.StringIO()\n\n  def write(self, val):\n    return self._writer.write(val)\n\n  def close(self):\n    return self._writer.close()\n\n  def getvalue(self):\n    return self._writer.getvalue()\n\n\ndef MessageToString(\n    message,\n    as_utf8=False,\n    as_one_line=False,\n    use_short_repeated_primitives=False,\n    pointy_brackets=False,\n    use_index_order=False,\n    float_format=None,\n    double_format=None,\n    use_field_number=False,\n    descriptor_pool=None,\n    indent=0,\n    message_formatter=None,\n    print_unknown_fields=False,\n    force_colon=False):\n  # type: (...) -> str\n  \"\"\"Convert protobuf message to text format.\n\n  Double values can be formatted compactly with 15 digits of\n  precision (which is the most that IEEE 754 \"double\" can guarantee)\n  using double_format='.15g'. To ensure that converting to text and back to a\n  proto will result in an identical value, double_format='.17g' should be used.\n\n  Args:\n    message: The protocol buffers message.\n    as_utf8: Return unescaped Unicode for non-ASCII characters.\n        In Python 3 actual Unicode characters may appear as is in strings.\n        In Python 2 the return value will be valid UTF-8 rather than only ASCII.\n    as_one_line: Don't introduce newlines between fields.\n    use_short_repeated_primitives: Use short repeated format for primitives.\n    pointy_brackets: If True, use angle brackets instead of curly braces for\n      nesting.\n    use_index_order: If True, fields of a proto message will be printed using\n      the order defined in source code instead of the field number, extensions\n      will be printed at the end of the message and their relative order is\n      determined by the extension number. By default, use the field number\n      order.\n    float_format (str): If set, use this to specify float field formatting\n      (per the \"Format Specification Mini-Language\"); otherwise, shortest float\n      that has same value in wire will be printed. Also affect double field\n      if double_format is not set but float_format is set.\n    double_format (str): If set, use this to specify double field formatting\n      (per the \"Format Specification Mini-Language\"); if it is not set but\n      float_format is set, use float_format. Otherwise, use ``str()``\n    use_field_number: If True, print field numbers instead of names.\n    descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types.\n    indent (int): The initial indent level, in terms of spaces, for pretty\n      print.\n    message_formatter (function(message, indent, as_one_line) -> unicode|None):\n      Custom formatter for selected sub-messages (usually based on message\n      type). Use to pretty print parts of the protobuf for easier diffing.\n    print_unknown_fields: If True, unknown fields will be printed.\n    force_colon: If set, a colon will be added after the field name even if the\n      field is a proto message.\n\n  Returns:\n    str: A string of the text formatted protocol buffer message.\n  \"\"\"\n  out = TextWriter(as_utf8)\n  printer = _Printer(\n      out,\n      indent,\n      as_utf8,\n      as_one_line,\n      use_short_repeated_primitives,\n      pointy_brackets,\n      use_index_order,\n      float_format,\n      double_format,\n      use_field_number,\n      descriptor_pool,\n      message_formatter,\n      print_unknown_fields=print_unknown_fields,\n      force_colon=force_colon)\n  printer.PrintMessage(message)\n  result = out.getvalue()\n  out.close()\n  if as_one_line:\n    return result.rstrip()\n  return result\n\n\ndef MessageToBytes(message, **kwargs):\n  # type: (...) -> bytes\n  \"\"\"Convert protobuf message to encoded text format.  See MessageToString.\"\"\"\n  text = MessageToString(message, **kwargs)\n  if isinstance(text, bytes):\n    return text\n  codec = 'utf-8' if kwargs.get('as_utf8') else 'ascii'\n  return text.encode(codec)\n\n\ndef _IsMapEntry(field):\n  return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and\n          field.message_type.has_options and\n          field.message_type.GetOptions().map_entry)\n\n\ndef PrintMessage(message,\n                 out,\n                 indent=0,\n                 as_utf8=False,\n                 as_one_line=False,\n                 use_short_repeated_primitives=False,\n                 pointy_brackets=False,\n                 use_index_order=False,\n                 float_format=None,\n                 double_format=None,\n                 use_field_number=False,\n                 descriptor_pool=None,\n                 message_formatter=None,\n                 print_unknown_fields=False,\n                 force_colon=False):\n  printer = _Printer(\n      out=out, indent=indent, as_utf8=as_utf8,\n      as_one_line=as_one_line,\n      use_short_repeated_primitives=use_short_repeated_primitives,\n      pointy_brackets=pointy_brackets,\n      use_index_order=use_index_order,\n      float_format=float_format,\n      double_format=double_format,\n      use_field_number=use_field_number,\n      descriptor_pool=descriptor_pool,\n      message_formatter=message_formatter,\n      print_unknown_fields=print_unknown_fields,\n      force_colon=force_colon)\n  printer.PrintMessage(message)\n\n\ndef PrintField(field,\n               value,\n               out,\n               indent=0,\n               as_utf8=False,\n               as_one_line=False,\n               use_short_repeated_primitives=False,\n               pointy_brackets=False,\n               use_index_order=False,\n               float_format=None,\n               double_format=None,\n               message_formatter=None,\n               print_unknown_fields=False,\n               force_colon=False):\n  \"\"\"Print a single field name/value pair.\"\"\"\n  printer = _Printer(out, indent, as_utf8, as_one_line,\n                     use_short_repeated_primitives, pointy_brackets,\n                     use_index_order, float_format, double_format,\n                     message_formatter=message_formatter,\n                     print_unknown_fields=print_unknown_fields,\n                     force_colon=force_colon)\n  printer.PrintField(field, value)\n\n\ndef PrintFieldValue(field,\n                    value,\n                    out,\n                    indent=0,\n                    as_utf8=False,\n                    as_one_line=False,\n                    use_short_repeated_primitives=False,\n                    pointy_brackets=False,\n                    use_index_order=False,\n                    float_format=None,\n                    double_format=None,\n                    message_formatter=None,\n                    print_unknown_fields=False,\n                    force_colon=False):\n  \"\"\"Print a single field value (not including name).\"\"\"\n  printer = _Printer(out, indent, as_utf8, as_one_line,\n                     use_short_repeated_primitives, pointy_brackets,\n                     use_index_order, float_format, double_format,\n                     message_formatter=message_formatter,\n                     print_unknown_fields=print_unknown_fields,\n                     force_colon=force_colon)\n  printer.PrintFieldValue(field, value)\n\n\ndef _BuildMessageFromTypeName(type_name, descriptor_pool):\n  \"\"\"Returns a protobuf message instance.\n\n  Args:\n    type_name: Fully-qualified protobuf  message type name string.\n    descriptor_pool: DescriptorPool instance.\n\n  Returns:\n    A Message instance of type matching type_name, or None if the a Descriptor\n    wasn't found matching type_name.\n  \"\"\"\n  # pylint: disable=g-import-not-at-top\n  if descriptor_pool is None:\n    from google.protobuf import descriptor_pool as pool_mod\n    descriptor_pool = pool_mod.Default()\n  from google.protobuf import symbol_database\n  database = symbol_database.Default()\n  try:\n    message_descriptor = descriptor_pool.FindMessageTypeByName(type_name)\n  except KeyError:\n    return None\n  message_type = database.GetPrototype(message_descriptor)\n  return message_type()\n\n\n# These values must match WireType enum in google/protobuf/wire_format.h.\nWIRETYPE_LENGTH_DELIMITED = 2\nWIRETYPE_START_GROUP = 3\n\n\nclass _Printer(object):\n  \"\"\"Text format printer for protocol message.\"\"\"\n\n  def __init__(\n      self,\n      out,\n      indent=0,\n      as_utf8=False,\n      as_one_line=False,\n      use_short_repeated_primitives=False,\n      pointy_brackets=False,\n      use_index_order=False,\n      float_format=None,\n      double_format=None,\n      use_field_number=False,\n      descriptor_pool=None,\n      message_formatter=None,\n      print_unknown_fields=False,\n      force_colon=False):\n    \"\"\"Initialize the Printer.\n\n    Double values can be formatted compactly with 15 digits of precision\n    (which is the most that IEEE 754 \"double\" can guarantee) using\n    double_format='.15g'. To ensure that converting to text and back to a proto\n    will result in an identical value, double_format='.17g' should be used.\n\n    Args:\n      out: To record the text format result.\n      indent: The initial indent level for pretty print.\n      as_utf8: Return unescaped Unicode for non-ASCII characters.\n          In Python 3 actual Unicode characters may appear as is in strings.\n          In Python 2 the return value will be valid UTF-8 rather than ASCII.\n      as_one_line: Don't introduce newlines between fields.\n      use_short_repeated_primitives: Use short repeated format for primitives.\n      pointy_brackets: If True, use angle brackets instead of curly braces for\n        nesting.\n      use_index_order: If True, print fields of a proto message using the order\n        defined in source code instead of the field number. By default, use the\n        field number order.\n      float_format: If set, use this to specify float field formatting\n        (per the \"Format Specification Mini-Language\"); otherwise, shortest\n        float that has same value in wire will be printed. Also affect double\n        field if double_format is not set but float_format is set.\n      double_format: If set, use this to specify double field formatting\n        (per the \"Format Specification Mini-Language\"); if it is not set but\n        float_format is set, use float_format. Otherwise, str() is used.\n      use_field_number: If True, print field numbers instead of names.\n      descriptor_pool: A DescriptorPool used to resolve Any types.\n      message_formatter: A function(message, indent, as_one_line): unicode|None\n        to custom format selected sub-messages (usually based on message type).\n        Use to pretty print parts of the protobuf for easier diffing.\n      print_unknown_fields: If True, unknown fields will be printed.\n      force_colon: If set, a colon will be added after the field name even if\n        the field is a proto message.\n    \"\"\"\n    self.out = out\n    self.indent = indent\n    self.as_utf8 = as_utf8\n    self.as_one_line = as_one_line\n    self.use_short_repeated_primitives = use_short_repeated_primitives\n    self.pointy_brackets = pointy_brackets\n    self.use_index_order = use_index_order\n    self.float_format = float_format\n    if double_format is not None:\n      self.double_format = double_format\n    else:\n      self.double_format = float_format\n    self.use_field_number = use_field_number\n    self.descriptor_pool = descriptor_pool\n    self.message_formatter = message_formatter\n    self.print_unknown_fields = print_unknown_fields\n    self.force_colon = force_colon\n\n  def _TryPrintAsAnyMessage(self, message):\n    \"\"\"Serializes if message is a google.protobuf.Any field.\"\"\"\n    if '/' not in message.type_url:\n      return False\n    packed_message = _BuildMessageFromTypeName(message.TypeName(),\n                                               self.descriptor_pool)\n    if packed_message:\n      packed_message.MergeFromString(message.value)\n      colon = ':' if self.force_colon else ''\n      self.out.write('%s[%s]%s ' % (self.indent * ' ', message.type_url, colon))\n      self._PrintMessageFieldValue(packed_message)\n      self.out.write(' ' if self.as_one_line else '\\n')\n      return True\n    else:\n      return False\n\n  def _TryCustomFormatMessage(self, message):\n    formatted = self.message_formatter(message, self.indent, self.as_one_line)\n    if formatted is None:\n      return False\n\n    out = self.out\n    out.write(' ' * self.indent)\n    out.write(formatted)\n    out.write(' ' if self.as_one_line else '\\n')\n    return True\n\n  def PrintMessage(self, message):\n    \"\"\"Convert protobuf message to text format.\n\n    Args:\n      message: The protocol buffers message.\n    \"\"\"\n    if self.message_formatter and self._TryCustomFormatMessage(message):\n      return\n    if (message.DESCRIPTOR.full_name == _ANY_FULL_TYPE_NAME and\n        self._TryPrintAsAnyMessage(message)):\n      return\n    fields = message.ListFields()\n    if self.use_index_order:\n      fields.sort(\n          key=lambda x: x[0].number if x[0].is_extension else x[0].index)\n    for field, value in fields:\n      if _IsMapEntry(field):\n        for key in sorted(value):\n          # This is slow for maps with submessage entries because it copies the\n          # entire tree.  Unfortunately this would take significant refactoring\n          # of this file to work around.\n          #\n          # TODO(haberman): refactor and optimize if this becomes an issue.\n          entry_submsg = value.GetEntryClass()(key=key, value=value[key])\n          self.PrintField(field, entry_submsg)\n      elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n        if (self.use_short_repeated_primitives\n            and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE\n            and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_STRING):\n          self._PrintShortRepeatedPrimitivesValue(field, value)\n        else:\n          for element in value:\n            self.PrintField(field, element)\n      else:\n        self.PrintField(field, value)\n\n    if self.print_unknown_fields:\n      self._PrintUnknownFields(message.UnknownFields())\n\n  def _PrintUnknownFields(self, unknown_fields):\n    \"\"\"Print unknown fields.\"\"\"\n    out = self.out\n    for field in unknown_fields:\n      out.write(' ' * self.indent)\n      out.write(str(field.field_number))\n      if field.wire_type == WIRETYPE_START_GROUP:\n        if self.as_one_line:\n          out.write(' { ')\n        else:\n          out.write(' {\\n')\n          self.indent += 2\n\n        self._PrintUnknownFields(field.data)\n\n        if self.as_one_line:\n          out.write('} ')\n        else:\n          self.indent -= 2\n          out.write(' ' * self.indent + '}\\n')\n      elif field.wire_type == WIRETYPE_LENGTH_DELIMITED:\n        try:\n          # If this field is parseable as a Message, it is probably\n          # an embedded message.\n          # pylint: disable=protected-access\n          (embedded_unknown_message, pos) = decoder._DecodeUnknownFieldSet(\n              memoryview(field.data), 0, len(field.data))\n        except Exception:    # pylint: disable=broad-except\n          pos = 0\n\n        if pos == len(field.data):\n          if self.as_one_line:\n            out.write(' { ')\n          else:\n            out.write(' {\\n')\n            self.indent += 2\n\n          self._PrintUnknownFields(embedded_unknown_message)\n\n          if self.as_one_line:\n            out.write('} ')\n          else:\n            self.indent -= 2\n            out.write(' ' * self.indent + '}\\n')\n        else:\n          # A string or bytes field. self.as_utf8 may not work.\n          out.write(': \\\"')\n          out.write(text_encoding.CEscape(field.data, False))\n          out.write('\\\" ' if self.as_one_line else '\\\"\\n')\n      else:\n        # varint, fixed32, fixed64\n        out.write(': ')\n        out.write(str(field.data))\n        out.write(' ' if self.as_one_line else '\\n')\n\n  def _PrintFieldName(self, field):\n    \"\"\"Print field name.\"\"\"\n    out = self.out\n    out.write(' ' * self.indent)\n    if self.use_field_number:\n      out.write(str(field.number))\n    else:\n      if field.is_extension:\n        out.write('[')\n        if (field.containing_type.GetOptions().message_set_wire_format and\n            field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and\n            field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL):\n          out.write(field.message_type.full_name)\n        else:\n          out.write(field.full_name)\n        out.write(']')\n      elif field.type == descriptor.FieldDescriptor.TYPE_GROUP:\n        # For groups, use the capitalized name.\n        out.write(field.message_type.name)\n      else:\n          out.write(field.name)\n\n    if (self.force_colon or\n        field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE):\n      # The colon is optional in this case, but our cross-language golden files\n      # don't include it. Here, the colon is only included if force_colon is\n      # set to True\n      out.write(':')\n\n  def PrintField(self, field, value):\n    \"\"\"Print a single field name/value pair.\"\"\"\n    self._PrintFieldName(field)\n    self.out.write(' ')\n    self.PrintFieldValue(field, value)\n    self.out.write(' ' if self.as_one_line else '\\n')\n\n  def _PrintShortRepeatedPrimitivesValue(self, field, value):\n    \"\"\"\"Prints short repeated primitives value.\"\"\"\n    # Note: this is called only when value has at least one element.\n    self._PrintFieldName(field)\n    self.out.write(' [')\n    for i in range(len(value) - 1):\n      self.PrintFieldValue(field, value[i])\n      self.out.write(', ')\n    self.PrintFieldValue(field, value[-1])\n    self.out.write(']')\n    self.out.write(' ' if self.as_one_line else '\\n')\n\n  def _PrintMessageFieldValue(self, value):\n    if self.pointy_brackets:\n      openb = '<'\n      closeb = '>'\n    else:\n      openb = '{'\n      closeb = '}'\n\n    if self.as_one_line:\n      self.out.write('%s ' % openb)\n      self.PrintMessage(value)\n      self.out.write(closeb)\n    else:\n      self.out.write('%s\\n' % openb)\n      self.indent += 2\n      self.PrintMessage(value)\n      self.indent -= 2\n      self.out.write(' ' * self.indent + closeb)\n\n  def PrintFieldValue(self, field, value):\n    \"\"\"Print a single field value (not including name).\n\n    For repeated fields, the value should be a single element.\n\n    Args:\n      field: The descriptor of the field to be printed.\n      value: The value of the field.\n    \"\"\"\n    out = self.out\n    if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n      self._PrintMessageFieldValue(value)\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM:\n      enum_value = field.enum_type.values_by_number.get(value, None)\n      if enum_value is not None:\n        out.write(enum_value.name)\n      else:\n        out.write(str(value))\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING:\n      out.write('\\\"')\n      if isinstance(value, str) and not self.as_utf8:\n        out_value = value.encode('utf-8')\n      else:\n        out_value = value\n      if field.type == descriptor.FieldDescriptor.TYPE_BYTES:\n        # We always need to escape all binary data in TYPE_BYTES fields.\n        out_as_utf8 = False\n      else:\n        out_as_utf8 = self.as_utf8\n      out.write(text_encoding.CEscape(out_value, out_as_utf8))\n      out.write('\\\"')\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL:\n      if value:\n        out.write('true')\n      else:\n        out.write('false')\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT:\n      if self.float_format is not None:\n        out.write('{1:{0}}'.format(self.float_format, value))\n      else:\n        if math.isnan(value):\n          out.write(str(value))\n        else:\n          out.write(str(type_checkers.ToShortestFloat(value)))\n    elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_DOUBLE and\n          self.double_format is not None):\n      out.write('{1:{0}}'.format(self.double_format, value))\n    else:\n      out.write(str(value))\n\n\ndef Parse(text,\n          message,\n          allow_unknown_extension=False,\n          allow_field_number=False,\n          descriptor_pool=None,\n          allow_unknown_field=False):\n  \"\"\"Parses a text representation of a protocol message into a message.\n\n  NOTE: for historical reasons this function does not clear the input\n  message. This is different from what the binary msg.ParseFrom(...) does.\n  If text contains a field already set in message, the value is appended if the\n  field is repeated. Otherwise, an error is raised.\n\n  Example::\n\n    a = MyProto()\n    a.repeated_field.append('test')\n    b = MyProto()\n\n    # Repeated fields are combined\n    text_format.Parse(repr(a), b)\n    text_format.Parse(repr(a), b) # repeated_field contains [\"test\", \"test\"]\n\n    # Non-repeated fields cannot be overwritten\n    a.singular_field = 1\n    b.singular_field = 2\n    text_format.Parse(repr(a), b) # ParseError\n\n    # Binary version:\n    b.ParseFromString(a.SerializeToString()) # repeated_field is now \"test\"\n\n  Caller is responsible for clearing the message as needed.\n\n  Args:\n    text (str): Message text representation.\n    message (Message): A protocol buffer message to merge into.\n    allow_unknown_extension: if True, skip over missing extensions and keep\n      parsing\n    allow_field_number: if True, both field number and field name are allowed.\n    descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types.\n    allow_unknown_field: if True, skip over unknown field and keep\n      parsing. Avoid to use this option if possible. It may hide some\n      errors (e.g. spelling error on field name)\n\n  Returns:\n    Message: The same message passed as argument.\n\n  Raises:\n    ParseError: On text parsing problems.\n  \"\"\"\n  return ParseLines(text.split(b'\\n' if isinstance(text, bytes) else u'\\n'),\n                    message,\n                    allow_unknown_extension,\n                    allow_field_number,\n                    descriptor_pool=descriptor_pool,\n                    allow_unknown_field=allow_unknown_field)\n\n\ndef Merge(text,\n          message,\n          allow_unknown_extension=False,\n          allow_field_number=False,\n          descriptor_pool=None,\n          allow_unknown_field=False):\n  \"\"\"Parses a text representation of a protocol message into a message.\n\n  Like Parse(), but allows repeated values for a non-repeated field, and uses\n  the last one. This means any non-repeated, top-level fields specified in text\n  replace those in the message.\n\n  Args:\n    text (str): Message text representation.\n    message (Message): A protocol buffer message to merge into.\n    allow_unknown_extension: if True, skip over missing extensions and keep\n      parsing\n    allow_field_number: if True, both field number and field name are allowed.\n    descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types.\n    allow_unknown_field: if True, skip over unknown field and keep\n      parsing. Avoid to use this option if possible. It may hide some\n      errors (e.g. spelling error on field name)\n\n  Returns:\n    Message: The same message passed as argument.\n\n  Raises:\n    ParseError: On text parsing problems.\n  \"\"\"\n  return MergeLines(\n      text.split(b'\\n' if isinstance(text, bytes) else u'\\n'),\n      message,\n      allow_unknown_extension,\n      allow_field_number,\n      descriptor_pool=descriptor_pool,\n      allow_unknown_field=allow_unknown_field)\n\n\ndef ParseLines(lines,\n               message,\n               allow_unknown_extension=False,\n               allow_field_number=False,\n               descriptor_pool=None,\n               allow_unknown_field=False):\n  \"\"\"Parses a text representation of a protocol message into a message.\n\n  See Parse() for caveats.\n\n  Args:\n    lines: An iterable of lines of a message's text representation.\n    message: A protocol buffer message to merge into.\n    allow_unknown_extension: if True, skip over missing extensions and keep\n      parsing\n    allow_field_number: if True, both field number and field name are allowed.\n    descriptor_pool: A DescriptorPool used to resolve Any types.\n    allow_unknown_field: if True, skip over unknown field and keep\n      parsing. Avoid to use this option if possible. It may hide some\n      errors (e.g. spelling error on field name)\n\n  Returns:\n    The same message passed as argument.\n\n  Raises:\n    ParseError: On text parsing problems.\n  \"\"\"\n  parser = _Parser(allow_unknown_extension,\n                   allow_field_number,\n                   descriptor_pool=descriptor_pool,\n                   allow_unknown_field=allow_unknown_field)\n  return parser.ParseLines(lines, message)\n\n\ndef MergeLines(lines,\n               message,\n               allow_unknown_extension=False,\n               allow_field_number=False,\n               descriptor_pool=None,\n               allow_unknown_field=False):\n  \"\"\"Parses a text representation of a protocol message into a message.\n\n  See Merge() for more details.\n\n  Args:\n    lines: An iterable of lines of a message's text representation.\n    message: A protocol buffer message to merge into.\n    allow_unknown_extension: if True, skip over missing extensions and keep\n      parsing\n    allow_field_number: if True, both field number and field name are allowed.\n    descriptor_pool: A DescriptorPool used to resolve Any types.\n    allow_unknown_field: if True, skip over unknown field and keep\n      parsing. Avoid to use this option if possible. It may hide some\n      errors (e.g. spelling error on field name)\n\n  Returns:\n    The same message passed as argument.\n\n  Raises:\n    ParseError: On text parsing problems.\n  \"\"\"\n  parser = _Parser(allow_unknown_extension,\n                   allow_field_number,\n                   descriptor_pool=descriptor_pool,\n                   allow_unknown_field=allow_unknown_field)\n  return parser.MergeLines(lines, message)\n\n\nclass _Parser(object):\n  \"\"\"Text format parser for protocol message.\"\"\"\n\n  def __init__(self,\n               allow_unknown_extension=False,\n               allow_field_number=False,\n               descriptor_pool=None,\n               allow_unknown_field=False):\n    self.allow_unknown_extension = allow_unknown_extension\n    self.allow_field_number = allow_field_number\n    self.descriptor_pool = descriptor_pool\n    self.allow_unknown_field = allow_unknown_field\n\n  def ParseLines(self, lines, message):\n    \"\"\"Parses a text representation of a protocol message into a message.\"\"\"\n    self._allow_multiple_scalars = False\n    self._ParseOrMerge(lines, message)\n    return message\n\n  def MergeLines(self, lines, message):\n    \"\"\"Merges a text representation of a protocol message into a message.\"\"\"\n    self._allow_multiple_scalars = True\n    self._ParseOrMerge(lines, message)\n    return message\n\n  def _ParseOrMerge(self, lines, message):\n    \"\"\"Converts a text representation of a protocol message into a message.\n\n    Args:\n      lines: Lines of a message's text representation.\n      message: A protocol buffer message to merge into.\n\n    Raises:\n      ParseError: On text parsing problems.\n    \"\"\"\n    # Tokenize expects native str lines.\n    str_lines = (\n        line if isinstance(line, str) else line.decode('utf-8')\n        for line in lines)\n    tokenizer = Tokenizer(str_lines)\n    while not tokenizer.AtEnd():\n      self._MergeField(tokenizer, message)\n\n  def _MergeField(self, tokenizer, message):\n    \"\"\"Merges a single protocol message field into a message.\n\n    Args:\n      tokenizer: A tokenizer to parse the field name and values.\n      message: A protocol message to record the data.\n\n    Raises:\n      ParseError: In case of text parsing problems.\n    \"\"\"\n    message_descriptor = message.DESCRIPTOR\n    if (message_descriptor.full_name == _ANY_FULL_TYPE_NAME and\n        tokenizer.TryConsume('[')):\n      type_url_prefix, packed_type_name = self._ConsumeAnyTypeUrl(tokenizer)\n      tokenizer.Consume(']')\n      tokenizer.TryConsume(':')\n      if tokenizer.TryConsume('<'):\n        expanded_any_end_token = '>'\n      else:\n        tokenizer.Consume('{')\n        expanded_any_end_token = '}'\n      expanded_any_sub_message = _BuildMessageFromTypeName(packed_type_name,\n                                                           self.descriptor_pool)\n      if not expanded_any_sub_message:\n        raise ParseError('Type %s not found in descriptor pool' %\n                         packed_type_name)\n      while not tokenizer.TryConsume(expanded_any_end_token):\n        if tokenizer.AtEnd():\n          raise tokenizer.ParseErrorPreviousToken('Expected \"%s\".' %\n                                                  (expanded_any_end_token,))\n        self._MergeField(tokenizer, expanded_any_sub_message)\n      deterministic = False\n\n      message.Pack(expanded_any_sub_message,\n                   type_url_prefix=type_url_prefix,\n                   deterministic=deterministic)\n      return\n\n    if tokenizer.TryConsume('['):\n      name = [tokenizer.ConsumeIdentifier()]\n      while tokenizer.TryConsume('.'):\n        name.append(tokenizer.ConsumeIdentifier())\n      name = '.'.join(name)\n\n      if not message_descriptor.is_extendable:\n        raise tokenizer.ParseErrorPreviousToken(\n            'Message type \"%s\" does not have extensions.' %\n            message_descriptor.full_name)\n      # pylint: disable=protected-access\n      field = message.Extensions._FindExtensionByName(name)\n      # pylint: enable=protected-access\n\n\n      if not field:\n        if self.allow_unknown_extension:\n          field = None\n        else:\n          raise tokenizer.ParseErrorPreviousToken(\n              'Extension \"%s\" not registered. '\n              'Did you import the _pb2 module which defines it? '\n              'If you are trying to place the extension in the MessageSet '\n              'field of another message that is in an Any or MessageSet field, '\n              'that message\\'s _pb2 module must be imported as well' % name)\n      elif message_descriptor != field.containing_type:\n        raise tokenizer.ParseErrorPreviousToken(\n            'Extension \"%s\" does not extend message type \"%s\".' %\n            (name, message_descriptor.full_name))\n\n      tokenizer.Consume(']')\n\n    else:\n      name = tokenizer.ConsumeIdentifierOrNumber()\n      if self.allow_field_number and name.isdigit():\n        number = ParseInteger(name, True, True)\n        field = message_descriptor.fields_by_number.get(number, None)\n        if not field and message_descriptor.is_extendable:\n          field = message.Extensions._FindExtensionByNumber(number)\n      else:\n        field = message_descriptor.fields_by_name.get(name, None)\n\n        # Group names are expected to be capitalized as they appear in the\n        # .proto file, which actually matches their type names, not their field\n        # names.\n        if not field:\n          field = message_descriptor.fields_by_name.get(name.lower(), None)\n          if field and field.type != descriptor.FieldDescriptor.TYPE_GROUP:\n            field = None\n\n        if (field and field.type == descriptor.FieldDescriptor.TYPE_GROUP and\n            field.message_type.name != name):\n          field = None\n\n      if not field and not self.allow_unknown_field:\n        raise tokenizer.ParseErrorPreviousToken(\n            'Message type \"%s\" has no field named \"%s\".' %\n            (message_descriptor.full_name, name))\n\n    if field:\n      if not self._allow_multiple_scalars and field.containing_oneof:\n        # Check if there's a different field set in this oneof.\n        # Note that we ignore the case if the same field was set before, and we\n        # apply _allow_multiple_scalars to non-scalar fields as well.\n        which_oneof = message.WhichOneof(field.containing_oneof.name)\n        if which_oneof is not None and which_oneof != field.name:\n          raise tokenizer.ParseErrorPreviousToken(\n              'Field \"%s\" is specified along with field \"%s\", another member '\n              'of oneof \"%s\" for message type \"%s\".' %\n              (field.name, which_oneof, field.containing_oneof.name,\n               message_descriptor.full_name))\n\n      if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n        tokenizer.TryConsume(':')\n        merger = self._MergeMessageField\n      else:\n        tokenizer.Consume(':')\n        merger = self._MergeScalarField\n\n      if (field.label == descriptor.FieldDescriptor.LABEL_REPEATED and\n          tokenizer.TryConsume('[')):\n        # Short repeated format, e.g. \"foo: [1, 2, 3]\"\n        if not tokenizer.TryConsume(']'):\n          while True:\n            merger(tokenizer, message, field)\n            if tokenizer.TryConsume(']'):\n              break\n            tokenizer.Consume(',')\n\n      else:\n        merger(tokenizer, message, field)\n\n    else:  # Proto field is unknown.\n      assert (self.allow_unknown_extension or self.allow_unknown_field)\n      _SkipFieldContents(tokenizer)\n\n    # For historical reasons, fields may optionally be separated by commas or\n    # semicolons.\n    if not tokenizer.TryConsume(','):\n      tokenizer.TryConsume(';')\n\n\n  def _ConsumeAnyTypeUrl(self, tokenizer):\n    \"\"\"Consumes a google.protobuf.Any type URL and returns the type name.\"\"\"\n    # Consume \"type.googleapis.com/\".\n    prefix = [tokenizer.ConsumeIdentifier()]\n    tokenizer.Consume('.')\n    prefix.append(tokenizer.ConsumeIdentifier())\n    tokenizer.Consume('.')\n    prefix.append(tokenizer.ConsumeIdentifier())\n    tokenizer.Consume('/')\n    # Consume the fully-qualified type name.\n    name = [tokenizer.ConsumeIdentifier()]\n    while tokenizer.TryConsume('.'):\n      name.append(tokenizer.ConsumeIdentifier())\n    return '.'.join(prefix), '.'.join(name)\n\n  def _MergeMessageField(self, tokenizer, message, field):\n    \"\"\"Merges a single scalar field into a message.\n\n    Args:\n      tokenizer: A tokenizer to parse the field value.\n      message: The message of which field is a member.\n      field: The descriptor of the field to be merged.\n\n    Raises:\n      ParseError: In case of text parsing problems.\n    \"\"\"\n    is_map_entry = _IsMapEntry(field)\n\n    if tokenizer.TryConsume('<'):\n      end_token = '>'\n    else:\n      tokenizer.Consume('{')\n      end_token = '}'\n\n    if field.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n      if field.is_extension:\n        sub_message = message.Extensions[field].add()\n      elif is_map_entry:\n        sub_message = getattr(message, field.name).GetEntryClass()()\n      else:\n        sub_message = getattr(message, field.name).add()\n    else:\n      if field.is_extension:\n        if (not self._allow_multiple_scalars and\n            message.HasExtension(field)):\n          raise tokenizer.ParseErrorPreviousToken(\n              'Message type \"%s\" should not have multiple \"%s\" extensions.' %\n              (message.DESCRIPTOR.full_name, field.full_name))\n        sub_message = message.Extensions[field]\n      else:\n        # Also apply _allow_multiple_scalars to message field.\n        # TODO(jieluo): Change to _allow_singular_overwrites.\n        if (not self._allow_multiple_scalars and\n            message.HasField(field.name)):\n          raise tokenizer.ParseErrorPreviousToken(\n              'Message type \"%s\" should not have multiple \"%s\" fields.' %\n              (message.DESCRIPTOR.full_name, field.name))\n        sub_message = getattr(message, field.name)\n      sub_message.SetInParent()\n\n    while not tokenizer.TryConsume(end_token):\n      if tokenizer.AtEnd():\n        raise tokenizer.ParseErrorPreviousToken('Expected \"%s\".' % (end_token,))\n      self._MergeField(tokenizer, sub_message)\n\n    if is_map_entry:\n      value_cpptype = field.message_type.fields_by_name['value'].cpp_type\n      if value_cpptype == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n        value = getattr(message, field.name)[sub_message.key]\n        value.CopyFrom(sub_message.value)\n      else:\n        getattr(message, field.name)[sub_message.key] = sub_message.value\n\n  @staticmethod\n  def _IsProto3Syntax(message):\n    message_descriptor = message.DESCRIPTOR\n    return (hasattr(message_descriptor, 'syntax') and\n            message_descriptor.syntax == 'proto3')\n\n  def _MergeScalarField(self, tokenizer, message, field):\n    \"\"\"Merges a single scalar field into a message.\n\n    Args:\n      tokenizer: A tokenizer to parse the field value.\n      message: A protocol message to record the data.\n      field: The descriptor of the field to be merged.\n\n    Raises:\n      ParseError: In case of text parsing problems.\n      RuntimeError: On runtime errors.\n    \"\"\"\n    _ = self.allow_unknown_extension\n    value = None\n\n    if field.type in (descriptor.FieldDescriptor.TYPE_INT32,\n                      descriptor.FieldDescriptor.TYPE_SINT32,\n                      descriptor.FieldDescriptor.TYPE_SFIXED32):\n      value = _ConsumeInt32(tokenizer)\n    elif field.type in (descriptor.FieldDescriptor.TYPE_INT64,\n                        descriptor.FieldDescriptor.TYPE_SINT64,\n                        descriptor.FieldDescriptor.TYPE_SFIXED64):\n      value = _ConsumeInt64(tokenizer)\n    elif field.type in (descriptor.FieldDescriptor.TYPE_UINT32,\n                        descriptor.FieldDescriptor.TYPE_FIXED32):\n      value = _ConsumeUint32(tokenizer)\n    elif field.type in (descriptor.FieldDescriptor.TYPE_UINT64,\n                        descriptor.FieldDescriptor.TYPE_FIXED64):\n      value = _ConsumeUint64(tokenizer)\n    elif field.type in (descriptor.FieldDescriptor.TYPE_FLOAT,\n                        descriptor.FieldDescriptor.TYPE_DOUBLE):\n      value = tokenizer.ConsumeFloat()\n    elif field.type == descriptor.FieldDescriptor.TYPE_BOOL:\n      value = tokenizer.ConsumeBool()\n    elif field.type == descriptor.FieldDescriptor.TYPE_STRING:\n      value = tokenizer.ConsumeString()\n    elif field.type == descriptor.FieldDescriptor.TYPE_BYTES:\n      value = tokenizer.ConsumeByteString()\n    elif field.type == descriptor.FieldDescriptor.TYPE_ENUM:\n      value = tokenizer.ConsumeEnum(field)\n    else:\n      raise RuntimeError('Unknown field type %d' % field.type)\n\n    if field.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n      if field.is_extension:\n        message.Extensions[field].append(value)\n      else:\n        getattr(message, field.name).append(value)\n    else:\n      if field.is_extension:\n        if (not self._allow_multiple_scalars and\n            not self._IsProto3Syntax(message) and\n            message.HasExtension(field)):\n          raise tokenizer.ParseErrorPreviousToken(\n              'Message type \"%s\" should not have multiple \"%s\" extensions.' %\n              (message.DESCRIPTOR.full_name, field.full_name))\n        else:\n          message.Extensions[field] = value\n      else:\n        duplicate_error = False\n        if not self._allow_multiple_scalars:\n          if self._IsProto3Syntax(message):\n            # Proto3 doesn't represent presence so we try best effort to check\n            # multiple scalars by compare to default values.\n            duplicate_error = bool(getattr(message, field.name))\n          else:\n            duplicate_error = message.HasField(field.name)\n\n        if duplicate_error:\n          raise tokenizer.ParseErrorPreviousToken(\n              'Message type \"%s\" should not have multiple \"%s\" fields.' %\n              (message.DESCRIPTOR.full_name, field.name))\n        else:\n          setattr(message, field.name, value)\n\n\ndef _SkipFieldContents(tokenizer):\n  \"\"\"Skips over contents (value or message) of a field.\n\n  Args:\n    tokenizer: A tokenizer to parse the field name and values.\n  \"\"\"\n  # Try to guess the type of this field.\n  # If this field is not a message, there should be a \":\" between the\n  # field name and the field value and also the field value should not\n  # start with \"{\" or \"<\" which indicates the beginning of a message body.\n  # If there is no \":\" or there is a \"{\" or \"<\" after \":\", this field has\n  # to be a message or the input is ill-formed.\n  if tokenizer.TryConsume(':') and not tokenizer.LookingAt(\n      '{') and not tokenizer.LookingAt('<'):\n    _SkipFieldValue(tokenizer)\n  else:\n    _SkipFieldMessage(tokenizer)\n\n\ndef _SkipField(tokenizer):\n  \"\"\"Skips over a complete field (name and value/message).\n\n  Args:\n    tokenizer: A tokenizer to parse the field name and values.\n  \"\"\"\n  if tokenizer.TryConsume('['):\n    # Consume extension name.\n    tokenizer.ConsumeIdentifier()\n    while tokenizer.TryConsume('.'):\n      tokenizer.ConsumeIdentifier()\n    tokenizer.Consume(']')\n  else:\n    tokenizer.ConsumeIdentifierOrNumber()\n\n  _SkipFieldContents(tokenizer)\n\n  # For historical reasons, fields may optionally be separated by commas or\n  # semicolons.\n  if not tokenizer.TryConsume(','):\n    tokenizer.TryConsume(';')\n\n\ndef _SkipFieldMessage(tokenizer):\n  \"\"\"Skips over a field message.\n\n  Args:\n    tokenizer: A tokenizer to parse the field name and values.\n  \"\"\"\n\n  if tokenizer.TryConsume('<'):\n    delimiter = '>'\n  else:\n    tokenizer.Consume('{')\n    delimiter = '}'\n\n  while not tokenizer.LookingAt('>') and not tokenizer.LookingAt('}'):\n    _SkipField(tokenizer)\n\n  tokenizer.Consume(delimiter)\n\n\ndef _SkipFieldValue(tokenizer):\n  \"\"\"Skips over a field value.\n\n  Args:\n    tokenizer: A tokenizer to parse the field name and values.\n\n  Raises:\n    ParseError: In case an invalid field value is found.\n  \"\"\"\n  # String/bytes tokens can come in multiple adjacent string literals.\n  # If we can consume one, consume as many as we can.\n  if tokenizer.TryConsumeByteString():\n    while tokenizer.TryConsumeByteString():\n      pass\n    return\n\n  if (not tokenizer.TryConsumeIdentifier() and\n      not _TryConsumeInt64(tokenizer) and not _TryConsumeUint64(tokenizer) and\n      not tokenizer.TryConsumeFloat()):\n    raise ParseError('Invalid field value: ' + tokenizer.token)\n\n\nclass Tokenizer(object):\n  \"\"\"Protocol buffer text representation tokenizer.\n\n  This class handles the lower level string parsing by splitting it into\n  meaningful tokens.\n\n  It was directly ported from the Java protocol buffer API.\n  \"\"\"\n\n  _WHITESPACE = re.compile(r'\\s+')\n  _COMMENT = re.compile(r'(\\s*#.*$)', re.MULTILINE)\n  _WHITESPACE_OR_COMMENT = re.compile(r'(\\s|(#.*$))+', re.MULTILINE)\n  _TOKEN = re.compile('|'.join([\n      r'[a-zA-Z_][0-9a-zA-Z_+-]*',  # an identifier\n      r'([0-9+-]|(\\.[0-9]))[0-9a-zA-Z_.+-]*',  # a number\n  ] + [  # quoted str for each quote mark\n      # Avoid backtracking! https://stackoverflow.com/a/844267\n      r'{qt}[^{qt}\\n\\\\]*((\\\\.)+[^{qt}\\n\\\\]*)*({qt}|\\\\?$)'.format(qt=mark)\n      for mark in _QUOTES\n  ]))\n\n  _IDENTIFIER = re.compile(r'[^\\d\\W]\\w*')\n  _IDENTIFIER_OR_NUMBER = re.compile(r'\\w+')\n\n  def __init__(self, lines, skip_comments=True):\n    self._position = 0\n    self._line = -1\n    self._column = 0\n    self._token_start = None\n    self.token = ''\n    self._lines = iter(lines)\n    self._current_line = ''\n    self._previous_line = 0\n    self._previous_column = 0\n    self._more_lines = True\n    self._skip_comments = skip_comments\n    self._whitespace_pattern = (skip_comments and self._WHITESPACE_OR_COMMENT\n                                or self._WHITESPACE)\n    self._SkipWhitespace()\n    self.NextToken()\n\n  def LookingAt(self, token):\n    return self.token == token\n\n  def AtEnd(self):\n    \"\"\"Checks the end of the text was reached.\n\n    Returns:\n      True iff the end was reached.\n    \"\"\"\n    return not self.token\n\n  def _PopLine(self):\n    while len(self._current_line) <= self._column:\n      try:\n        self._current_line = next(self._lines)\n      except StopIteration:\n        self._current_line = ''\n        self._more_lines = False\n        return\n      else:\n        self._line += 1\n        self._column = 0\n\n  def _SkipWhitespace(self):\n    while True:\n      self._PopLine()\n      match = self._whitespace_pattern.match(self._current_line, self._column)\n      if not match:\n        break\n      length = len(match.group(0))\n      self._column += length\n\n  def TryConsume(self, token):\n    \"\"\"Tries to consume a given piece of text.\n\n    Args:\n      token: Text to consume.\n\n    Returns:\n      True iff the text was consumed.\n    \"\"\"\n    if self.token == token:\n      self.NextToken()\n      return True\n    return False\n\n  def Consume(self, token):\n    \"\"\"Consumes a piece of text.\n\n    Args:\n      token: Text to consume.\n\n    Raises:\n      ParseError: If the text couldn't be consumed.\n    \"\"\"\n    if not self.TryConsume(token):\n      raise self.ParseError('Expected \"%s\".' % token)\n\n  def ConsumeComment(self):\n    result = self.token\n    if not self._COMMENT.match(result):\n      raise self.ParseError('Expected comment.')\n    self.NextToken()\n    return result\n\n  def ConsumeCommentOrTrailingComment(self):\n    \"\"\"Consumes a comment, returns a 2-tuple (trailing bool, comment str).\"\"\"\n\n    # Tokenizer initializes _previous_line and _previous_column to 0. As the\n    # tokenizer starts, it looks like there is a previous token on the line.\n    just_started = self._line == 0 and self._column == 0\n\n    before_parsing = self._previous_line\n    comment = self.ConsumeComment()\n\n    # A trailing comment is a comment on the same line than the previous token.\n    trailing = (self._previous_line == before_parsing\n                and not just_started)\n\n    return trailing, comment\n\n  def TryConsumeIdentifier(self):\n    try:\n      self.ConsumeIdentifier()\n      return True\n    except ParseError:\n      return False\n\n  def ConsumeIdentifier(self):\n    \"\"\"Consumes protocol message field identifier.\n\n    Returns:\n      Identifier string.\n\n    Raises:\n      ParseError: If an identifier couldn't be consumed.\n    \"\"\"\n    result = self.token\n    if not self._IDENTIFIER.match(result):\n      raise self.ParseError('Expected identifier.')\n    self.NextToken()\n    return result\n\n  def TryConsumeIdentifierOrNumber(self):\n    try:\n      self.ConsumeIdentifierOrNumber()\n      return True\n    except ParseError:\n      return False\n\n  def ConsumeIdentifierOrNumber(self):\n    \"\"\"Consumes protocol message field identifier.\n\n    Returns:\n      Identifier string.\n\n    Raises:\n      ParseError: If an identifier couldn't be consumed.\n    \"\"\"\n    result = self.token\n    if not self._IDENTIFIER_OR_NUMBER.match(result):\n      raise self.ParseError('Expected identifier or number, got %s.' % result)\n    self.NextToken()\n    return result\n\n  def TryConsumeInteger(self):\n    try:\n      self.ConsumeInteger()\n      return True\n    except ParseError:\n      return False\n\n  def ConsumeInteger(self):\n    \"\"\"Consumes an integer number.\n\n    Returns:\n      The integer parsed.\n\n    Raises:\n      ParseError: If an integer couldn't be consumed.\n    \"\"\"\n    try:\n      result = _ParseAbstractInteger(self.token)\n    except ValueError as e:\n      raise self.ParseError(str(e))\n    self.NextToken()\n    return result\n\n  def TryConsumeFloat(self):\n    try:\n      self.ConsumeFloat()\n      return True\n    except ParseError:\n      return False\n\n  def ConsumeFloat(self):\n    \"\"\"Consumes an floating point number.\n\n    Returns:\n      The number parsed.\n\n    Raises:\n      ParseError: If a floating point number couldn't be consumed.\n    \"\"\"\n    try:\n      result = ParseFloat(self.token)\n    except ValueError as e:\n      raise self.ParseError(str(e))\n    self.NextToken()\n    return result\n\n  def ConsumeBool(self):\n    \"\"\"Consumes a boolean value.\n\n    Returns:\n      The bool parsed.\n\n    Raises:\n      ParseError: If a boolean value couldn't be consumed.\n    \"\"\"\n    try:\n      result = ParseBool(self.token)\n    except ValueError as e:\n      raise self.ParseError(str(e))\n    self.NextToken()\n    return result\n\n  def TryConsumeByteString(self):\n    try:\n      self.ConsumeByteString()\n      return True\n    except ParseError:\n      return False\n\n  def ConsumeString(self):\n    \"\"\"Consumes a string value.\n\n    Returns:\n      The string parsed.\n\n    Raises:\n      ParseError: If a string value couldn't be consumed.\n    \"\"\"\n    the_bytes = self.ConsumeByteString()\n    try:\n      return str(the_bytes, 'utf-8')\n    except UnicodeDecodeError as e:\n      raise self._StringParseError(e)\n\n  def ConsumeByteString(self):\n    \"\"\"Consumes a byte array value.\n\n    Returns:\n      The array parsed (as a string).\n\n    Raises:\n      ParseError: If a byte array value couldn't be consumed.\n    \"\"\"\n    the_list = [self._ConsumeSingleByteString()]\n    while self.token and self.token[0] in _QUOTES:\n      the_list.append(self._ConsumeSingleByteString())\n    return b''.join(the_list)\n\n  def _ConsumeSingleByteString(self):\n    \"\"\"Consume one token of a string literal.\n\n    String literals (whether bytes or text) can come in multiple adjacent\n    tokens which are automatically concatenated, like in C or Python.  This\n    method only consumes one token.\n\n    Returns:\n      The token parsed.\n    Raises:\n      ParseError: When the wrong format data is found.\n    \"\"\"\n    text = self.token\n    if len(text) < 1 or text[0] not in _QUOTES:\n      raise self.ParseError('Expected string but found: %r' % (text,))\n\n    if len(text) < 2 or text[-1] != text[0]:\n      raise self.ParseError('String missing ending quote: %r' % (text,))\n\n    try:\n      result = text_encoding.CUnescape(text[1:-1])\n    except ValueError as e:\n      raise self.ParseError(str(e))\n    self.NextToken()\n    return result\n\n  def ConsumeEnum(self, field):\n    try:\n      result = ParseEnum(field, self.token)\n    except ValueError as e:\n      raise self.ParseError(str(e))\n    self.NextToken()\n    return result\n\n  def ParseErrorPreviousToken(self, message):\n    \"\"\"Creates and *returns* a ParseError for the previously read token.\n\n    Args:\n      message: A message to set for the exception.\n\n    Returns:\n      A ParseError instance.\n    \"\"\"\n    return ParseError(message, self._previous_line + 1,\n                      self._previous_column + 1)\n\n  def ParseError(self, message):\n    \"\"\"Creates and *returns* a ParseError for the current token.\"\"\"\n    return ParseError('\\'' + self._current_line + '\\': ' + message,\n                      self._line + 1, self._column + 1)\n\n  def _StringParseError(self, e):\n    return self.ParseError('Couldn\\'t parse string: ' + str(e))\n\n  def NextToken(self):\n    \"\"\"Reads the next meaningful token.\"\"\"\n    self._previous_line = self._line\n    self._previous_column = self._column\n\n    self._column += len(self.token)\n    self._SkipWhitespace()\n\n    if not self._more_lines:\n      self.token = ''\n      return\n\n    match = self._TOKEN.match(self._current_line, self._column)\n    if not match and not self._skip_comments:\n      match = self._COMMENT.match(self._current_line, self._column)\n    if match:\n      token = match.group(0)\n      self.token = token\n    else:\n      self.token = self._current_line[self._column]\n\n# Aliased so it can still be accessed by current visibility violators.\n# TODO(dbarnett): Migrate violators to textformat_tokenizer.\n_Tokenizer = Tokenizer  # pylint: disable=invalid-name\n\n\ndef _ConsumeInt32(tokenizer):\n  \"\"\"Consumes a signed 32bit integer number from tokenizer.\n\n  Args:\n    tokenizer: A tokenizer used to parse the number.\n\n  Returns:\n    The integer parsed.\n\n  Raises:\n    ParseError: If a signed 32bit integer couldn't be consumed.\n  \"\"\"\n  return _ConsumeInteger(tokenizer, is_signed=True, is_long=False)\n\n\ndef _ConsumeUint32(tokenizer):\n  \"\"\"Consumes an unsigned 32bit integer number from tokenizer.\n\n  Args:\n    tokenizer: A tokenizer used to parse the number.\n\n  Returns:\n    The integer parsed.\n\n  Raises:\n    ParseError: If an unsigned 32bit integer couldn't be consumed.\n  \"\"\"\n  return _ConsumeInteger(tokenizer, is_signed=False, is_long=False)\n\n\ndef _TryConsumeInt64(tokenizer):\n  try:\n    _ConsumeInt64(tokenizer)\n    return True\n  except ParseError:\n    return False\n\n\ndef _ConsumeInt64(tokenizer):\n  \"\"\"Consumes a signed 32bit integer number from tokenizer.\n\n  Args:\n    tokenizer: A tokenizer used to parse the number.\n\n  Returns:\n    The integer parsed.\n\n  Raises:\n    ParseError: If a signed 32bit integer couldn't be consumed.\n  \"\"\"\n  return _ConsumeInteger(tokenizer, is_signed=True, is_long=True)\n\n\ndef _TryConsumeUint64(tokenizer):\n  try:\n    _ConsumeUint64(tokenizer)\n    return True\n  except ParseError:\n    return False\n\n\ndef _ConsumeUint64(tokenizer):\n  \"\"\"Consumes an unsigned 64bit integer number from tokenizer.\n\n  Args:\n    tokenizer: A tokenizer used to parse the number.\n\n  Returns:\n    The integer parsed.\n\n  Raises:\n    ParseError: If an unsigned 64bit integer couldn't be consumed.\n  \"\"\"\n  return _ConsumeInteger(tokenizer, is_signed=False, is_long=True)\n\n\ndef _ConsumeInteger(tokenizer, is_signed=False, is_long=False):\n  \"\"\"Consumes an integer number from tokenizer.\n\n  Args:\n    tokenizer: A tokenizer used to parse the number.\n    is_signed: True if a signed integer must be parsed.\n    is_long: True if a long integer must be parsed.\n\n  Returns:\n    The integer parsed.\n\n  Raises:\n    ParseError: If an integer with given characteristics couldn't be consumed.\n  \"\"\"\n  try:\n    result = ParseInteger(tokenizer.token, is_signed=is_signed, is_long=is_long)\n  except ValueError as e:\n    raise tokenizer.ParseError(str(e))\n  tokenizer.NextToken()\n  return result\n\n\ndef ParseInteger(text, is_signed=False, is_long=False):\n  \"\"\"Parses an integer.\n\n  Args:\n    text: The text to parse.\n    is_signed: True if a signed integer must be parsed.\n    is_long: True if a long integer must be parsed.\n\n  Returns:\n    The integer value.\n\n  Raises:\n    ValueError: Thrown Iff the text is not a valid integer.\n  \"\"\"\n  # Do the actual parsing. Exception handling is propagated to caller.\n  result = _ParseAbstractInteger(text)\n\n  # Check if the integer is sane. Exceptions handled by callers.\n  checker = _INTEGER_CHECKERS[2 * int(is_long) + int(is_signed)]\n  checker.CheckValue(result)\n  return result\n\n\ndef _ParseAbstractInteger(text):\n  \"\"\"Parses an integer without checking size/signedness.\n\n  Args:\n    text: The text to parse.\n\n  Returns:\n    The integer value.\n\n  Raises:\n    ValueError: Thrown Iff the text is not a valid integer.\n  \"\"\"\n  # Do the actual parsing. Exception handling is propagated to caller.\n  orig_text = text\n  c_octal_match = re.match(r'(-?)0(\\d+)$', text)\n  if c_octal_match:\n    # Python 3 no longer supports 0755 octal syntax without the 'o', so\n    # we always use the '0o' prefix for multi-digit numbers starting with 0.\n    text = c_octal_match.group(1) + '0o' + c_octal_match.group(2)\n  try:\n    return int(text, 0)\n  except ValueError:\n    raise ValueError('Couldn\\'t parse integer: %s' % orig_text)\n\n\ndef ParseFloat(text):\n  \"\"\"Parse a floating point number.\n\n  Args:\n    text: Text to parse.\n\n  Returns:\n    The number parsed.\n\n  Raises:\n    ValueError: If a floating point number couldn't be parsed.\n  \"\"\"\n  try:\n    # Assume Python compatible syntax.\n    return float(text)\n  except ValueError:\n    # Check alternative spellings.\n    if _FLOAT_INFINITY.match(text):\n      if text[0] == '-':\n        return float('-inf')\n      else:\n        return float('inf')\n    elif _FLOAT_NAN.match(text):\n      return float('nan')\n    else:\n      # assume '1.0f' format\n      try:\n        return float(text.rstrip('f'))\n      except ValueError:\n        raise ValueError('Couldn\\'t parse float: %s' % text)\n\n\ndef ParseBool(text):\n  \"\"\"Parse a boolean value.\n\n  Args:\n    text: Text to parse.\n\n  Returns:\n    Boolean values parsed\n\n  Raises:\n    ValueError: If text is not a valid boolean.\n  \"\"\"\n  if text in ('true', 't', '1', 'True'):\n    return True\n  elif text in ('false', 'f', '0', 'False'):\n    return False\n  else:\n    raise ValueError('Expected \"true\" or \"false\".')\n\n\ndef ParseEnum(field, value):\n  \"\"\"Parse an enum value.\n\n  The value can be specified by a number (the enum value), or by\n  a string literal (the enum name).\n\n  Args:\n    field: Enum field descriptor.\n    value: String value.\n\n  Returns:\n    Enum value number.\n\n  Raises:\n    ValueError: If the enum value could not be parsed.\n  \"\"\"\n  enum_descriptor = field.enum_type\n  try:\n    number = int(value, 0)\n  except ValueError:\n    # Identifier.\n    enum_value = enum_descriptor.values_by_name.get(value, None)\n    if enum_value is None:\n      raise ValueError('Enum type \"%s\" has no value named %s.' %\n                       (enum_descriptor.full_name, value))\n  else:\n    # Numeric value.\n    if hasattr(field.file, 'syntax'):\n      # Attribute is checked for compatibility.\n      if field.file.syntax == 'proto3':\n        # Proto3 accept numeric unknown enums.\n        return number\n    enum_value = enum_descriptor.values_by_number.get(number, None)\n    if enum_value is None:\n      raise ValueError('Enum type \"%s\" has no value with number %d.' %\n                       (enum_descriptor.full_name, number))\n  return enum_value.number\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/timestamp_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/timestamp.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x1fgoogle/protobuf/timestamp.proto\\x12\\x0fgoogle.protobuf\\\"+\\n\\tTimestamp\\x12\\x0f\\n\\x07seconds\\x18\\x01 \\x01(\\x03\\x12\\r\\n\\x05nanos\\x18\\x02 \\x01(\\x05\\x42\\x85\\x01\\n\\x13\\x63om.google.protobufB\\x0eTimestampProtoP\\x01Z2google.golang.org/protobuf/types/known/timestamppb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.timestamp_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\016TimestampProtoP\\001Z2google.golang.org/protobuf/types/known/timestamppb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _TIMESTAMP._serialized_start=52\n  _TIMESTAMP._serialized_end=95\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/type_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/type.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2\nfrom google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x1agoogle/protobuf/type.proto\\x12\\x0fgoogle.protobuf\\x1a\\x19google/protobuf/any.proto\\x1a$google/protobuf/source_context.proto\\\"\\xd7\\x01\\n\\x04Type\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12&\\n\\x06\\x66ields\\x18\\x02 \\x03(\\x0b\\x32\\x16.google.protobuf.Field\\x12\\x0e\\n\\x06oneofs\\x18\\x03 \\x03(\\t\\x12(\\n\\x07options\\x18\\x04 \\x03(\\x0b\\x32\\x17.google.protobuf.Option\\x12\\x36\\n\\x0esource_context\\x18\\x05 \\x01(\\x0b\\x32\\x1e.google.protobuf.SourceContext\\x12\\'\\n\\x06syntax\\x18\\x06 \\x01(\\x0e\\x32\\x17.google.protobuf.Syntax\\\"\\xd5\\x05\\n\\x05\\x46ield\\x12)\\n\\x04kind\\x18\\x01 \\x01(\\x0e\\x32\\x1b.google.protobuf.Field.Kind\\x12\\x37\\n\\x0b\\x63\\x61rdinality\\x18\\x02 \\x01(\\x0e\\x32\\\".google.protobuf.Field.Cardinality\\x12\\x0e\\n\\x06number\\x18\\x03 \\x01(\\x05\\x12\\x0c\\n\\x04name\\x18\\x04 \\x01(\\t\\x12\\x10\\n\\x08type_url\\x18\\x06 \\x01(\\t\\x12\\x13\\n\\x0boneof_index\\x18\\x07 \\x01(\\x05\\x12\\x0e\\n\\x06packed\\x18\\x08 \\x01(\\x08\\x12(\\n\\x07options\\x18\\t \\x03(\\x0b\\x32\\x17.google.protobuf.Option\\x12\\x11\\n\\tjson_name\\x18\\n \\x01(\\t\\x12\\x15\\n\\rdefault_value\\x18\\x0b \\x01(\\t\\\"\\xc8\\x02\\n\\x04Kind\\x12\\x10\\n\\x0cTYPE_UNKNOWN\\x10\\x00\\x12\\x0f\\n\\x0bTYPE_DOUBLE\\x10\\x01\\x12\\x0e\\n\\nTYPE_FLOAT\\x10\\x02\\x12\\x0e\\n\\nTYPE_INT64\\x10\\x03\\x12\\x0f\\n\\x0bTYPE_UINT64\\x10\\x04\\x12\\x0e\\n\\nTYPE_INT32\\x10\\x05\\x12\\x10\\n\\x0cTYPE_FIXED64\\x10\\x06\\x12\\x10\\n\\x0cTYPE_FIXED32\\x10\\x07\\x12\\r\\n\\tTYPE_BOOL\\x10\\x08\\x12\\x0f\\n\\x0bTYPE_STRING\\x10\\t\\x12\\x0e\\n\\nTYPE_GROUP\\x10\\n\\x12\\x10\\n\\x0cTYPE_MESSAGE\\x10\\x0b\\x12\\x0e\\n\\nTYPE_BYTES\\x10\\x0c\\x12\\x0f\\n\\x0bTYPE_UINT32\\x10\\r\\x12\\r\\n\\tTYPE_ENUM\\x10\\x0e\\x12\\x11\\n\\rTYPE_SFIXED32\\x10\\x0f\\x12\\x11\\n\\rTYPE_SFIXED64\\x10\\x10\\x12\\x0f\\n\\x0bTYPE_SINT32\\x10\\x11\\x12\\x0f\\n\\x0bTYPE_SINT64\\x10\\x12\\\"t\\n\\x0b\\x43\\x61rdinality\\x12\\x17\\n\\x13\\x43\\x41RDINALITY_UNKNOWN\\x10\\x00\\x12\\x18\\n\\x14\\x43\\x41RDINALITY_OPTIONAL\\x10\\x01\\x12\\x18\\n\\x14\\x43\\x41RDINALITY_REQUIRED\\x10\\x02\\x12\\x18\\n\\x14\\x43\\x41RDINALITY_REPEATED\\x10\\x03\\\"\\xce\\x01\\n\\x04\\x45num\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12-\\n\\tenumvalue\\x18\\x02 \\x03(\\x0b\\x32\\x1a.google.protobuf.EnumValue\\x12(\\n\\x07options\\x18\\x03 \\x03(\\x0b\\x32\\x17.google.protobuf.Option\\x12\\x36\\n\\x0esource_context\\x18\\x04 \\x01(\\x0b\\x32\\x1e.google.protobuf.SourceContext\\x12\\'\\n\\x06syntax\\x18\\x05 \\x01(\\x0e\\x32\\x17.google.protobuf.Syntax\\\"S\\n\\tEnumValue\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0e\\n\\x06number\\x18\\x02 \\x01(\\x05\\x12(\\n\\x07options\\x18\\x03 \\x03(\\x0b\\x32\\x17.google.protobuf.Option\\\";\\n\\x06Option\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12#\\n\\x05value\\x18\\x02 \\x01(\\x0b\\x32\\x14.google.protobuf.Any*.\\n\\x06Syntax\\x12\\x11\\n\\rSYNTAX_PROTO2\\x10\\x00\\x12\\x11\\n\\rSYNTAX_PROTO3\\x10\\x01\\x42{\\n\\x13\\x63om.google.protobufB\\tTypeProtoP\\x01Z-google.golang.org/protobuf/types/known/typepb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.type_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\tTypeProtoP\\001Z-google.golang.org/protobuf/types/known/typepb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _SYNTAX._serialized_start=1413\n  _SYNTAX._serialized_end=1459\n  _TYPE._serialized_start=113\n  _TYPE._serialized_end=328\n  _FIELD._serialized_start=331\n  _FIELD._serialized_end=1056\n  _FIELD_KIND._serialized_start=610\n  _FIELD_KIND._serialized_end=938\n  _FIELD_CARDINALITY._serialized_start=940\n  _FIELD_CARDINALITY._serialized_end=1056\n  _ENUM._serialized_start=1059\n  _ENUM._serialized_end=1265\n  _ENUMVALUE._serialized_start=1267\n  _ENUMVALUE._serialized_end=1350\n  _OPTION._serialized_start=1352\n  _OPTION._serialized_end=1411\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/util/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/util/json_format_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/util/json_format.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n&google/protobuf/util/json_format.proto\\x12\\x11protobuf_unittest\\\"\\x89\\x01\\n\\x13TestFlagsAndStrings\\x12\\t\\n\\x01\\x41\\x18\\x01 \\x02(\\x05\\x12K\\n\\rrepeatedgroup\\x18\\x02 \\x03(\\n24.protobuf_unittest.TestFlagsAndStrings.RepeatedGroup\\x1a\\x1a\\n\\rRepeatedGroup\\x12\\t\\n\\x01\\x66\\x18\\x03 \\x02(\\t\\\"!\\n\\x14TestBase64ByteArrays\\x12\\t\\n\\x01\\x61\\x18\\x01 \\x02(\\x0c\\\"G\\n\\x12TestJavaScriptJSON\\x12\\t\\n\\x01\\x61\\x18\\x01 \\x01(\\x05\\x12\\r\\n\\x05\\x66inal\\x18\\x02 \\x01(\\x02\\x12\\n\\n\\x02in\\x18\\x03 \\x01(\\t\\x12\\x0b\\n\\x03Var\\x18\\x04 \\x01(\\t\\\"Q\\n\\x18TestJavaScriptOrderJSON1\\x12\\t\\n\\x01\\x64\\x18\\x01 \\x01(\\x05\\x12\\t\\n\\x01\\x63\\x18\\x02 \\x01(\\x05\\x12\\t\\n\\x01x\\x18\\x03 \\x01(\\x08\\x12\\t\\n\\x01\\x62\\x18\\x04 \\x01(\\x05\\x12\\t\\n\\x01\\x61\\x18\\x05 \\x01(\\x05\\\"\\x89\\x01\\n\\x18TestJavaScriptOrderJSON2\\x12\\t\\n\\x01\\x64\\x18\\x01 \\x01(\\x05\\x12\\t\\n\\x01\\x63\\x18\\x02 \\x01(\\x05\\x12\\t\\n\\x01x\\x18\\x03 \\x01(\\x08\\x12\\t\\n\\x01\\x62\\x18\\x04 \\x01(\\x05\\x12\\t\\n\\x01\\x61\\x18\\x05 \\x01(\\x05\\x12\\x36\\n\\x01z\\x18\\x06 \\x03(\\x0b\\x32+.protobuf_unittest.TestJavaScriptOrderJSON1\\\"$\\n\\x0cTestLargeInt\\x12\\t\\n\\x01\\x61\\x18\\x01 \\x02(\\x03\\x12\\t\\n\\x01\\x62\\x18\\x02 \\x02(\\x04\\\"\\xa0\\x01\\n\\x0bTestNumbers\\x12\\x30\\n\\x01\\x61\\x18\\x01 \\x01(\\x0e\\x32%.protobuf_unittest.TestNumbers.MyType\\x12\\t\\n\\x01\\x62\\x18\\x02 \\x01(\\x05\\x12\\t\\n\\x01\\x63\\x18\\x03 \\x01(\\x02\\x12\\t\\n\\x01\\x64\\x18\\x04 \\x01(\\x08\\x12\\t\\n\\x01\\x65\\x18\\x05 \\x01(\\x01\\x12\\t\\n\\x01\\x66\\x18\\x06 \\x01(\\r\\\"(\\n\\x06MyType\\x12\\x06\\n\\x02OK\\x10\\x00\\x12\\x0b\\n\\x07WARNING\\x10\\x01\\x12\\t\\n\\x05\\x45RROR\\x10\\x02\\\"T\\n\\rTestCamelCase\\x12\\x14\\n\\x0cnormal_field\\x18\\x01 \\x01(\\t\\x12\\x15\\n\\rCAPITAL_FIELD\\x18\\x02 \\x01(\\x05\\x12\\x16\\n\\x0e\\x43\\x61melCaseField\\x18\\x03 \\x01(\\x05\\\"|\\n\\x0bTestBoolMap\\x12=\\n\\x08\\x62ool_map\\x18\\x01 \\x03(\\x0b\\x32+.protobuf_unittest.TestBoolMap.BoolMapEntry\\x1a.\\n\\x0c\\x42oolMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x08\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\\"O\\n\\rTestRecursion\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x05\\x12/\\n\\x05\\x63hild\\x18\\x02 \\x01(\\x0b\\x32 .protobuf_unittest.TestRecursion\\\"\\x86\\x01\\n\\rTestStringMap\\x12\\x43\\n\\nstring_map\\x18\\x01 \\x03(\\x0b\\x32/.protobuf_unittest.TestStringMap.StringMapEntry\\x1a\\x30\\n\\x0eStringMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\t:\\x02\\x38\\x01\\\"\\xc4\\x01\\n\\x14TestStringSerializer\\x12\\x15\\n\\rscalar_string\\x18\\x01 \\x01(\\t\\x12\\x17\\n\\x0frepeated_string\\x18\\x02 \\x03(\\t\\x12J\\n\\nstring_map\\x18\\x03 \\x03(\\x0b\\x32\\x36.protobuf_unittest.TestStringSerializer.StringMapEntry\\x1a\\x30\\n\\x0eStringMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\t:\\x02\\x38\\x01\\\"$\\n\\x18TestMessageWithExtension*\\x08\\x08\\x64\\x10\\x80\\x80\\x80\\x80\\x02\\\"z\\n\\rTestExtension\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\t2Z\\n\\x03\\x65xt\\x12+.protobuf_unittest.TestMessageWithExtension\\x18\\x64 \\x01(\\x0b\\x32 .protobuf_unittest.TestExtension\\\"Q\\n\\x14TestDefaultEnumValue\\x12\\x39\\n\\nenum_value\\x18\\x01 \\x01(\\x0e\\x32\\x1c.protobuf_unittest.EnumValue:\\x07\\x44\\x45\\x46\\x41ULT*2\\n\\tEnumValue\\x12\\x0c\\n\\x08PROTOCOL\\x10\\x00\\x12\\n\\n\\x06\\x42UFFER\\x10\\x01\\x12\\x0b\\n\\x07\\x44\\x45\\x46\\x41ULT\\x10\\x02')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n  TestMessageWithExtension.RegisterExtension(_TESTEXTENSION.extensions_by_name['ext'])\n\n  DESCRIPTOR._options = None\n  _TESTBOOLMAP_BOOLMAPENTRY._options = None\n  _TESTBOOLMAP_BOOLMAPENTRY._serialized_options = b'8\\001'\n  _TESTSTRINGMAP_STRINGMAPENTRY._options = None\n  _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\\001'\n  _TESTSTRINGSERIALIZER_STRINGMAPENTRY._options = None\n  _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_options = b'8\\001'\n  _ENUMVALUE._serialized_start=1607\n  _ENUMVALUE._serialized_end=1657\n  _TESTFLAGSANDSTRINGS._serialized_start=62\n  _TESTFLAGSANDSTRINGS._serialized_end=199\n  _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_start=173\n  _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_end=199\n  _TESTBASE64BYTEARRAYS._serialized_start=201\n  _TESTBASE64BYTEARRAYS._serialized_end=234\n  _TESTJAVASCRIPTJSON._serialized_start=236\n  _TESTJAVASCRIPTJSON._serialized_end=307\n  _TESTJAVASCRIPTORDERJSON1._serialized_start=309\n  _TESTJAVASCRIPTORDERJSON1._serialized_end=390\n  _TESTJAVASCRIPTORDERJSON2._serialized_start=393\n  _TESTJAVASCRIPTORDERJSON2._serialized_end=530\n  _TESTLARGEINT._serialized_start=532\n  _TESTLARGEINT._serialized_end=568\n  _TESTNUMBERS._serialized_start=571\n  _TESTNUMBERS._serialized_end=731\n  _TESTNUMBERS_MYTYPE._serialized_start=691\n  _TESTNUMBERS_MYTYPE._serialized_end=731\n  _TESTCAMELCASE._serialized_start=733\n  _TESTCAMELCASE._serialized_end=817\n  _TESTBOOLMAP._serialized_start=819\n  _TESTBOOLMAP._serialized_end=943\n  _TESTBOOLMAP_BOOLMAPENTRY._serialized_start=897\n  _TESTBOOLMAP_BOOLMAPENTRY._serialized_end=943\n  _TESTRECURSION._serialized_start=945\n  _TESTRECURSION._serialized_end=1024\n  _TESTSTRINGMAP._serialized_start=1027\n  _TESTSTRINGMAP._serialized_end=1161\n  _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=1113\n  _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=1161\n  _TESTSTRINGSERIALIZER._serialized_start=1164\n  _TESTSTRINGSERIALIZER._serialized_end=1360\n  _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_start=1113\n  _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_end=1161\n  _TESTMESSAGEWITHEXTENSION._serialized_start=1362\n  _TESTMESSAGEWITHEXTENSION._serialized_end=1398\n  _TESTEXTENSION._serialized_start=1400\n  _TESTEXTENSION._serialized_end=1522\n  _TESTDEFAULTENUMVALUE._serialized_start=1524\n  _TESTDEFAULTENUMVALUE._serialized_end=1605\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/util/json_format_proto3_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/util/json_format_proto3.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2\nfrom google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2\nfrom google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2\nfrom google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2\nfrom google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2\nfrom google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2\nfrom google.protobuf import unittest_pb2 as google_dot_protobuf_dot_unittest__pb2\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n-google/protobuf/util/json_format_proto3.proto\\x12\\x06proto3\\x1a\\x19google/protobuf/any.proto\\x1a\\x1egoogle/protobuf/duration.proto\\x1a google/protobuf/field_mask.proto\\x1a\\x1cgoogle/protobuf/struct.proto\\x1a\\x1fgoogle/protobuf/timestamp.proto\\x1a\\x1egoogle/protobuf/wrappers.proto\\x1a\\x1egoogle/protobuf/unittest.proto\\\"\\x1c\\n\\x0bMessageType\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x05\\\"\\x94\\x05\\n\\x0bTestMessage\\x12\\x12\\n\\nbool_value\\x18\\x01 \\x01(\\x08\\x12\\x13\\n\\x0bint32_value\\x18\\x02 \\x01(\\x05\\x12\\x13\\n\\x0bint64_value\\x18\\x03 \\x01(\\x03\\x12\\x14\\n\\x0cuint32_value\\x18\\x04 \\x01(\\r\\x12\\x14\\n\\x0cuint64_value\\x18\\x05 \\x01(\\x04\\x12\\x13\\n\\x0b\\x66loat_value\\x18\\x06 \\x01(\\x02\\x12\\x14\\n\\x0c\\x64ouble_value\\x18\\x07 \\x01(\\x01\\x12\\x14\\n\\x0cstring_value\\x18\\x08 \\x01(\\t\\x12\\x13\\n\\x0b\\x62ytes_value\\x18\\t \\x01(\\x0c\\x12$\\n\\nenum_value\\x18\\n \\x01(\\x0e\\x32\\x10.proto3.EnumType\\x12*\\n\\rmessage_value\\x18\\x0b \\x01(\\x0b\\x32\\x13.proto3.MessageType\\x12\\x1b\\n\\x13repeated_bool_value\\x18\\x15 \\x03(\\x08\\x12\\x1c\\n\\x14repeated_int32_value\\x18\\x16 \\x03(\\x05\\x12\\x1c\\n\\x14repeated_int64_value\\x18\\x17 \\x03(\\x03\\x12\\x1d\\n\\x15repeated_uint32_value\\x18\\x18 \\x03(\\r\\x12\\x1d\\n\\x15repeated_uint64_value\\x18\\x19 \\x03(\\x04\\x12\\x1c\\n\\x14repeated_float_value\\x18\\x1a \\x03(\\x02\\x12\\x1d\\n\\x15repeated_double_value\\x18\\x1b \\x03(\\x01\\x12\\x1d\\n\\x15repeated_string_value\\x18\\x1c \\x03(\\t\\x12\\x1c\\n\\x14repeated_bytes_value\\x18\\x1d \\x03(\\x0c\\x12-\\n\\x13repeated_enum_value\\x18\\x1e \\x03(\\x0e\\x32\\x10.proto3.EnumType\\x12\\x33\\n\\x16repeated_message_value\\x18\\x1f \\x03(\\x0b\\x32\\x13.proto3.MessageType\\\"\\x8c\\x02\\n\\tTestOneof\\x12\\x1b\\n\\x11oneof_int32_value\\x18\\x01 \\x01(\\x05H\\x00\\x12\\x1c\\n\\x12oneof_string_value\\x18\\x02 \\x01(\\tH\\x00\\x12\\x1b\\n\\x11oneof_bytes_value\\x18\\x03 \\x01(\\x0cH\\x00\\x12,\\n\\x10oneof_enum_value\\x18\\x04 \\x01(\\x0e\\x32\\x10.proto3.EnumTypeH\\x00\\x12\\x32\\n\\x13oneof_message_value\\x18\\x05 \\x01(\\x0b\\x32\\x13.proto3.MessageTypeH\\x00\\x12\\x36\\n\\x10oneof_null_value\\x18\\x06 \\x01(\\x0e\\x32\\x1a.google.protobuf.NullValueH\\x00\\x42\\r\\n\\x0boneof_value\\\"\\xe1\\x04\\n\\x07TestMap\\x12.\\n\\x08\\x62ool_map\\x18\\x01 \\x03(\\x0b\\x32\\x1c.proto3.TestMap.BoolMapEntry\\x12\\x30\\n\\tint32_map\\x18\\x02 \\x03(\\x0b\\x32\\x1d.proto3.TestMap.Int32MapEntry\\x12\\x30\\n\\tint64_map\\x18\\x03 \\x03(\\x0b\\x32\\x1d.proto3.TestMap.Int64MapEntry\\x12\\x32\\n\\nuint32_map\\x18\\x04 \\x03(\\x0b\\x32\\x1e.proto3.TestMap.Uint32MapEntry\\x12\\x32\\n\\nuint64_map\\x18\\x05 \\x03(\\x0b\\x32\\x1e.proto3.TestMap.Uint64MapEntry\\x12\\x32\\n\\nstring_map\\x18\\x06 \\x03(\\x0b\\x32\\x1e.proto3.TestMap.StringMapEntry\\x1a.\\n\\x0c\\x42oolMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x08\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a/\\n\\rInt32MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x05\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a/\\n\\rInt64MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x03\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x30\\n\\x0eUint32MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\r\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x30\\n\\x0eUint64MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x04\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x30\\n\\x0eStringMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\\"\\x85\\x06\\n\\rTestNestedMap\\x12\\x34\\n\\x08\\x62ool_map\\x18\\x01 \\x03(\\x0b\\x32\\\".proto3.TestNestedMap.BoolMapEntry\\x12\\x36\\n\\tint32_map\\x18\\x02 \\x03(\\x0b\\x32#.proto3.TestNestedMap.Int32MapEntry\\x12\\x36\\n\\tint64_map\\x18\\x03 \\x03(\\x0b\\x32#.proto3.TestNestedMap.Int64MapEntry\\x12\\x38\\n\\nuint32_map\\x18\\x04 \\x03(\\x0b\\x32$.proto3.TestNestedMap.Uint32MapEntry\\x12\\x38\\n\\nuint64_map\\x18\\x05 \\x03(\\x0b\\x32$.proto3.TestNestedMap.Uint64MapEntry\\x12\\x38\\n\\nstring_map\\x18\\x06 \\x03(\\x0b\\x32$.proto3.TestNestedMap.StringMapEntry\\x12\\x32\\n\\x07map_map\\x18\\x07 \\x03(\\x0b\\x32!.proto3.TestNestedMap.MapMapEntry\\x1a.\\n\\x0c\\x42oolMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x08\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a/\\n\\rInt32MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x05\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a/\\n\\rInt64MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x03\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x30\\n\\x0eUint32MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\r\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x30\\n\\x0eUint64MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x04\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x30\\n\\x0eStringMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x44\\n\\x0bMapMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12$\\n\\x05value\\x18\\x02 \\x01(\\x0b\\x32\\x15.proto3.TestNestedMap:\\x02\\x38\\x01\\\"{\\n\\rTestStringMap\\x12\\x38\\n\\nstring_map\\x18\\x01 \\x03(\\x0b\\x32$.proto3.TestStringMap.StringMapEntry\\x1a\\x30\\n\\x0eStringMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\t:\\x02\\x38\\x01\\\"\\xee\\x07\\n\\x0bTestWrapper\\x12.\\n\\nbool_value\\x18\\x01 \\x01(\\x0b\\x32\\x1a.google.protobuf.BoolValue\\x12\\x30\\n\\x0bint32_value\\x18\\x02 \\x01(\\x0b\\x32\\x1b.google.protobuf.Int32Value\\x12\\x30\\n\\x0bint64_value\\x18\\x03 \\x01(\\x0b\\x32\\x1b.google.protobuf.Int64Value\\x12\\x32\\n\\x0cuint32_value\\x18\\x04 \\x01(\\x0b\\x32\\x1c.google.protobuf.UInt32Value\\x12\\x32\\n\\x0cuint64_value\\x18\\x05 \\x01(\\x0b\\x32\\x1c.google.protobuf.UInt64Value\\x12\\x30\\n\\x0b\\x66loat_value\\x18\\x06 \\x01(\\x0b\\x32\\x1b.google.protobuf.FloatValue\\x12\\x32\\n\\x0c\\x64ouble_value\\x18\\x07 \\x01(\\x0b\\x32\\x1c.google.protobuf.DoubleValue\\x12\\x32\\n\\x0cstring_value\\x18\\x08 \\x01(\\x0b\\x32\\x1c.google.protobuf.StringValue\\x12\\x30\\n\\x0b\\x62ytes_value\\x18\\t \\x01(\\x0b\\x32\\x1b.google.protobuf.BytesValue\\x12\\x37\\n\\x13repeated_bool_value\\x18\\x0b \\x03(\\x0b\\x32\\x1a.google.protobuf.BoolValue\\x12\\x39\\n\\x14repeated_int32_value\\x18\\x0c \\x03(\\x0b\\x32\\x1b.google.protobuf.Int32Value\\x12\\x39\\n\\x14repeated_int64_value\\x18\\r \\x03(\\x0b\\x32\\x1b.google.protobuf.Int64Value\\x12;\\n\\x15repeated_uint32_value\\x18\\x0e \\x03(\\x0b\\x32\\x1c.google.protobuf.UInt32Value\\x12;\\n\\x15repeated_uint64_value\\x18\\x0f \\x03(\\x0b\\x32\\x1c.google.protobuf.UInt64Value\\x12\\x39\\n\\x14repeated_float_value\\x18\\x10 \\x03(\\x0b\\x32\\x1b.google.protobuf.FloatValue\\x12;\\n\\x15repeated_double_value\\x18\\x11 \\x03(\\x0b\\x32\\x1c.google.protobuf.DoubleValue\\x12;\\n\\x15repeated_string_value\\x18\\x12 \\x03(\\x0b\\x32\\x1c.google.protobuf.StringValue\\x12\\x39\\n\\x14repeated_bytes_value\\x18\\x13 \\x03(\\x0b\\x32\\x1b.google.protobuf.BytesValue\\\"n\\n\\rTestTimestamp\\x12)\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x1a.google.protobuf.Timestamp\\x12\\x32\\n\\x0erepeated_value\\x18\\x02 \\x03(\\x0b\\x32\\x1a.google.protobuf.Timestamp\\\"k\\n\\x0cTestDuration\\x12(\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x19.google.protobuf.Duration\\x12\\x31\\n\\x0erepeated_value\\x18\\x02 \\x03(\\x0b\\x32\\x19.google.protobuf.Duration\\\":\\n\\rTestFieldMask\\x12)\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x1a.google.protobuf.FieldMask\\\"e\\n\\nTestStruct\\x12&\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x17.google.protobuf.Struct\\x12/\\n\\x0erepeated_value\\x18\\x02 \\x03(\\x0b\\x32\\x17.google.protobuf.Struct\\\"\\\\\\n\\x07TestAny\\x12#\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x14.google.protobuf.Any\\x12,\\n\\x0erepeated_value\\x18\\x02 \\x03(\\x0b\\x32\\x14.google.protobuf.Any\\\"b\\n\\tTestValue\\x12%\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x16.google.protobuf.Value\\x12.\\n\\x0erepeated_value\\x18\\x02 \\x03(\\x0b\\x32\\x16.google.protobuf.Value\\\"n\\n\\rTestListValue\\x12)\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x1a.google.protobuf.ListValue\\x12\\x32\\n\\x0erepeated_value\\x18\\x02 \\x03(\\x0b\\x32\\x1a.google.protobuf.ListValue\\\"\\x89\\x01\\n\\rTestBoolValue\\x12\\x12\\n\\nbool_value\\x18\\x01 \\x01(\\x08\\x12\\x34\\n\\x08\\x62ool_map\\x18\\x02 \\x03(\\x0b\\x32\\\".proto3.TestBoolValue.BoolMapEntry\\x1a.\\n\\x0c\\x42oolMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x08\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\\"+\\n\\x12TestCustomJsonName\\x12\\x15\\n\\x05value\\x18\\x01 \\x01(\\x05R\\x06@value\\\"J\\n\\x0eTestExtensions\\x12\\x38\\n\\nextensions\\x18\\x01 \\x01(\\x0b\\x32$.protobuf_unittest.TestAllExtensions\\\"\\x84\\x01\\n\\rTestEnumValue\\x12%\\n\\x0b\\x65num_value1\\x18\\x01 \\x01(\\x0e\\x32\\x10.proto3.EnumType\\x12%\\n\\x0b\\x65num_value2\\x18\\x02 \\x01(\\x0e\\x32\\x10.proto3.EnumType\\x12%\\n\\x0b\\x65num_value3\\x18\\x03 \\x01(\\x0e\\x32\\x10.proto3.EnumType*\\x1c\\n\\x08\\x45numType\\x12\\x07\\n\\x03\\x46OO\\x10\\x00\\x12\\x07\\n\\x03\\x42\\x41R\\x10\\x01\\x42,\\n\\x18\\x63om.google.protobuf.utilB\\x10JsonFormatProto3b\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_proto3_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\030com.google.protobuf.utilB\\020JsonFormatProto3'\n  _TESTMAP_BOOLMAPENTRY._options = None\n  _TESTMAP_BOOLMAPENTRY._serialized_options = b'8\\001'\n  _TESTMAP_INT32MAPENTRY._options = None\n  _TESTMAP_INT32MAPENTRY._serialized_options = b'8\\001'\n  _TESTMAP_INT64MAPENTRY._options = None\n  _TESTMAP_INT64MAPENTRY._serialized_options = b'8\\001'\n  _TESTMAP_UINT32MAPENTRY._options = None\n  _TESTMAP_UINT32MAPENTRY._serialized_options = b'8\\001'\n  _TESTMAP_UINT64MAPENTRY._options = None\n  _TESTMAP_UINT64MAPENTRY._serialized_options = b'8\\001'\n  _TESTMAP_STRINGMAPENTRY._options = None\n  _TESTMAP_STRINGMAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_BOOLMAPENTRY._options = None\n  _TESTNESTEDMAP_BOOLMAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_INT32MAPENTRY._options = None\n  _TESTNESTEDMAP_INT32MAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_INT64MAPENTRY._options = None\n  _TESTNESTEDMAP_INT64MAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_UINT32MAPENTRY._options = None\n  _TESTNESTEDMAP_UINT32MAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_UINT64MAPENTRY._options = None\n  _TESTNESTEDMAP_UINT64MAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_STRINGMAPENTRY._options = None\n  _TESTNESTEDMAP_STRINGMAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_MAPMAPENTRY._options = None\n  _TESTNESTEDMAP_MAPMAPENTRY._serialized_options = b'8\\001'\n  _TESTSTRINGMAP_STRINGMAPENTRY._options = None\n  _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\\001'\n  _TESTBOOLVALUE_BOOLMAPENTRY._options = None\n  _TESTBOOLVALUE_BOOLMAPENTRY._serialized_options = b'8\\001'\n  _ENUMTYPE._serialized_start=4849\n  _ENUMTYPE._serialized_end=4877\n  _MESSAGETYPE._serialized_start=277\n  _MESSAGETYPE._serialized_end=305\n  _TESTMESSAGE._serialized_start=308\n  _TESTMESSAGE._serialized_end=968\n  _TESTONEOF._serialized_start=971\n  _TESTONEOF._serialized_end=1239\n  _TESTMAP._serialized_start=1242\n  _TESTMAP._serialized_end=1851\n  _TESTMAP_BOOLMAPENTRY._serialized_start=1557\n  _TESTMAP_BOOLMAPENTRY._serialized_end=1603\n  _TESTMAP_INT32MAPENTRY._serialized_start=1605\n  _TESTMAP_INT32MAPENTRY._serialized_end=1652\n  _TESTMAP_INT64MAPENTRY._serialized_start=1654\n  _TESTMAP_INT64MAPENTRY._serialized_end=1701\n  _TESTMAP_UINT32MAPENTRY._serialized_start=1703\n  _TESTMAP_UINT32MAPENTRY._serialized_end=1751\n  _TESTMAP_UINT64MAPENTRY._serialized_start=1753\n  _TESTMAP_UINT64MAPENTRY._serialized_end=1801\n  _TESTMAP_STRINGMAPENTRY._serialized_start=1803\n  _TESTMAP_STRINGMAPENTRY._serialized_end=1851\n  _TESTNESTEDMAP._serialized_start=1854\n  _TESTNESTEDMAP._serialized_end=2627\n  _TESTNESTEDMAP_BOOLMAPENTRY._serialized_start=1557\n  _TESTNESTEDMAP_BOOLMAPENTRY._serialized_end=1603\n  _TESTNESTEDMAP_INT32MAPENTRY._serialized_start=1605\n  _TESTNESTEDMAP_INT32MAPENTRY._serialized_end=1652\n  _TESTNESTEDMAP_INT64MAPENTRY._serialized_start=1654\n  _TESTNESTEDMAP_INT64MAPENTRY._serialized_end=1701\n  _TESTNESTEDMAP_UINT32MAPENTRY._serialized_start=1703\n  _TESTNESTEDMAP_UINT32MAPENTRY._serialized_end=1751\n  _TESTNESTEDMAP_UINT64MAPENTRY._serialized_start=1753\n  _TESTNESTEDMAP_UINT64MAPENTRY._serialized_end=1801\n  _TESTNESTEDMAP_STRINGMAPENTRY._serialized_start=1803\n  _TESTNESTEDMAP_STRINGMAPENTRY._serialized_end=1851\n  _TESTNESTEDMAP_MAPMAPENTRY._serialized_start=2559\n  _TESTNESTEDMAP_MAPMAPENTRY._serialized_end=2627\n  _TESTSTRINGMAP._serialized_start=2629\n  _TESTSTRINGMAP._serialized_end=2752\n  _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=2704\n  _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=2752\n  _TESTWRAPPER._serialized_start=2755\n  _TESTWRAPPER._serialized_end=3761\n  _TESTTIMESTAMP._serialized_start=3763\n  _TESTTIMESTAMP._serialized_end=3873\n  _TESTDURATION._serialized_start=3875\n  _TESTDURATION._serialized_end=3982\n  _TESTFIELDMASK._serialized_start=3984\n  _TESTFIELDMASK._serialized_end=4042\n  _TESTSTRUCT._serialized_start=4044\n  _TESTSTRUCT._serialized_end=4145\n  _TESTANY._serialized_start=4147\n  _TESTANY._serialized_end=4239\n  _TESTVALUE._serialized_start=4241\n  _TESTVALUE._serialized_end=4339\n  _TESTLISTVALUE._serialized_start=4341\n  _TESTLISTVALUE._serialized_end=4451\n  _TESTBOOLVALUE._serialized_start=4454\n  _TESTBOOLVALUE._serialized_end=4591\n  _TESTBOOLVALUE_BOOLMAPENTRY._serialized_start=1557\n  _TESTBOOLVALUE_BOOLMAPENTRY._serialized_end=1603\n  _TESTCUSTOMJSONNAME._serialized_start=4593\n  _TESTCUSTOMJSONNAME._serialized_end=4636\n  _TESTEXTENSIONS._serialized_start=4638\n  _TESTEXTENSIONS._serialized_end=4712\n  _TESTENUMVALUE._serialized_start=4715\n  _TESTENUMVALUE._serialized_end=4847\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/hiero/vendor/google/protobuf/wrappers_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/wrappers.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x1egoogle/protobuf/wrappers.proto\\x12\\x0fgoogle.protobuf\\\"\\x1c\\n\\x0b\\x44oubleValue\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x01\\\"\\x1b\\n\\nFloatValue\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x02\\\"\\x1b\\n\\nInt64Value\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x03\\\"\\x1c\\n\\x0bUInt64Value\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x04\\\"\\x1b\\n\\nInt32Value\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x05\\\"\\x1c\\n\\x0bUInt32Value\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\r\\\"\\x1a\\n\\tBoolValue\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x08\\\"\\x1c\\n\\x0bStringValue\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\t\\\"\\x1b\\n\\nBytesValue\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x0c\\x42\\x83\\x01\\n\\x13\\x63om.google.protobufB\\rWrappersProtoP\\x01Z1google.golang.org/protobuf/types/known/wrapperspb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.wrappers_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\rWrappersProtoP\\001Z1google.golang.org/protobuf/types/known/wrapperspb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _DOUBLEVALUE._serialized_start=51\n  _DOUBLEVALUE._serialized_end=79\n  _FLOATVALUE._serialized_start=81\n  _FLOATVALUE._serialized_end=108\n  _INT64VALUE._serialized_start=110\n  _INT64VALUE._serialized_end=137\n  _UINT64VALUE._serialized_start=139\n  _UINT64VALUE._serialized_end=167\n  _INT32VALUE._serialized_start=169\n  _INT32VALUE._serialized_end=196\n  _UINT32VALUE._serialized_start=198\n  _UINT32VALUE._serialized_end=226\n  _BOOLVALUE._serialized_start=228\n  _BOOLVALUE._serialized_end=254\n  _STRINGVALUE._serialized_start=256\n  _STRINGVALUE._serialized_end=284\n  _BYTESVALUE._serialized_start=286\n  _BYTESVALUE._serialized_end=313\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/houdini/__init__.py",
    "content": "from .addon import (\n    HoudiniAddon,\n    HOUDINI_HOST_DIR,\n)\n\n\n__all__ = (\n    \"HoudiniAddon\",\n    \"HOUDINI_HOST_DIR\",\n)\n"
  },
  {
    "path": "openpype/hosts/houdini/addon.py",
    "content": "import os\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nHOUDINI_HOST_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass HoudiniAddon(OpenPypeModule, IHostAddon):\n    name = \"houdini\"\n    host_name = \"houdini\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        # Add requirements to HOUDINI_PATH and HOUDINI_MENU_PATH\n        startup_path = os.path.join(HOUDINI_HOST_DIR, \"startup\")\n        new_houdini_path = [startup_path]\n        new_houdini_menu_path = [startup_path]\n\n        old_houdini_path = env.get(\"HOUDINI_PATH\") or \"\"\n        old_houdini_menu_path = env.get(\"HOUDINI_MENU_PATH\") or \"\"\n\n        for path in old_houdini_path.split(os.pathsep):\n            if not path:\n                continue\n\n            norm_path = os.path.normpath(path)\n            if norm_path not in new_houdini_path:\n                new_houdini_path.append(norm_path)\n\n        for path in old_houdini_menu_path.split(os.pathsep):\n            if not path:\n                continue\n\n            norm_path = os.path.normpath(path)\n            if norm_path not in new_houdini_menu_path:\n                new_houdini_menu_path.append(norm_path)\n\n        # Add ampersand for unknown reason (Maybe is needed in Houdini?)\n        new_houdini_path.append(\"&\")\n        new_houdini_menu_path.append(\"&\")\n\n        env[\"HOUDINI_PATH\"] = os.pathsep.join(new_houdini_path)\n        env[\"HOUDINI_MENU_PATH\"] = os.pathsep.join(new_houdini_menu_path)\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [\n            os.path.join(HOUDINI_HOST_DIR, \"hooks\")\n        ]\n\n    def get_workfile_extensions(self):\n        return [\".hip\", \".hiplc\", \".hipnc\"]\n"
  },
  {
    "path": "openpype/hosts/houdini/api/__init__.py",
    "content": "from .pipeline import (\n    HoudiniHost,\n    ls,\n    containerise\n)\n\nfrom .plugin import (\n    Creator,\n)\n\nfrom .lib import (\n    lsattr,\n    lsattrs,\n    read,\n\n    maintained_selection\n)\n\n\n__all__ = [\n    \"HoudiniHost\",\n\n    \"ls\",\n    \"containerise\",\n\n    \"Creator\",\n\n    # Utility functions\n    \"lsattr\",\n    \"lsattrs\",\n    \"read\",\n\n    \"maintained_selection\"\n]\n"
  },
  {
    "path": "openpype/hosts/houdini/api/action.py",
    "content": "import pyblish.api\nimport hou\n\nfrom openpype.pipeline.publish import get_errored_instances_from_context\n\n\nclass SelectInvalidAction(pyblish.api.Action):\n    \"\"\"Select invalid nodes in Maya when plug-in failed.\n\n    To retrieve the invalid nodes this assumes a static `get_invalid()`\n    method is available on the plugin.\n\n    \"\"\"\n    label = \"Select invalid\"\n    on = \"failed\"  # This action is only available on a failed plug-in\n    icon = \"search\"  # Icon from Awesome Icon\n\n    def process(self, context, plugin):\n\n        errored_instances = get_errored_instances_from_context(context,\n                                                               plugin=plugin)\n\n        # Get the invalid nodes for the plug-ins\n        self.log.info(\"Finding invalid nodes..\")\n        invalid = list()\n        for instance in errored_instances:\n            invalid_nodes = plugin.get_invalid(instance)\n            if invalid_nodes:\n                if isinstance(invalid_nodes, (list, tuple)):\n                    invalid.extend(invalid_nodes)\n                else:\n                    self.log.warning(\"Plug-in returned to be invalid, \"\n                                     \"but has no selectable nodes.\")\n\n        hou.clearAllSelected()\n        if invalid:\n            self.log.info(\"Selecting invalid nodes: {}\".format(\n                \", \".join(node.path() for node in invalid)\n            ))\n            for node in invalid:\n                node.setSelected(True)\n                node.setCurrent(True)\n        else:\n            self.log.info(\"No invalid nodes found.\")\n\n\nclass SelectROPAction(pyblish.api.Action):\n    \"\"\"Select ROP.\n\n    It's used to select the associated ROPs with the errored instances.\n    \"\"\"\n\n    label = \"Select ROP\"\n    on = \"failed\"  # This action is only available on a failed plug-in\n    icon = \"mdi.cursor-default-click\"\n\n    def process(self, context, plugin):\n        errored_instances = get_errored_instances_from_context(context, plugin)\n\n        # Get the invalid nodes for the plug-ins\n        self.log.info(\"Finding ROP nodes..\")\n        rop_nodes = list()\n        for instance in errored_instances:\n            node_path = instance.data.get(\"instance_node\")\n            if not node_path:\n                continue\n\n            node = hou.node(node_path)\n            if not node:\n                continue\n\n            rop_nodes.append(node)\n\n        hou.clearAllSelected()\n        if rop_nodes:\n            self.log.info(\"Selecting ROP nodes: {}\".format(\n                \", \".join(node.path() for node in rop_nodes)\n            ))\n            for node in rop_nodes:\n                node.setSelected(True)\n                node.setCurrent(True)\n        else:\n            self.log.info(\"No ROP nodes found.\")\n"
  },
  {
    "path": "openpype/hosts/houdini/api/colorspace.py",
    "content": "import attr\nimport hou\nfrom openpype.hosts.houdini.api.lib import get_color_management_preferences\nfrom openpype.pipeline.colorspace import get_display_view_colorspace_name\n\n@attr.s\nclass LayerMetadata(object):\n    \"\"\"Data class for Render Layer metadata.\"\"\"\n    frameStart = attr.ib()\n    frameEnd = attr.ib()\n\n\n@attr.s\nclass RenderProduct(object):\n    \"\"\"Getting Colorspace as\n    Specific Render Product Parameter for submitting\n    publish job.\n\n    \"\"\"\n    colorspace = attr.ib()                      # colorspace\n    view = attr.ib()\n    productName = attr.ib(default=None)\n\n\nclass ARenderProduct(object):\n\n    def __init__(self):\n        \"\"\"Constructor.\"\"\"\n        # Initialize\n        self.layer_data = self._get_layer_data()\n        self.layer_data.products = self.get_colorspace_data()\n\n    def _get_layer_data(self):\n        return LayerMetadata(\n            frameStart=int(hou.playbar.frameRange()[0]),\n            frameEnd=int(hou.playbar.frameRange()[1]),\n        )\n\n    def get_colorspace_data(self):\n        \"\"\"To be implemented by renderer class.\n\n        This should return a list of RenderProducts.\n\n        Returns:\n            list: List of RenderProduct\n\n        \"\"\"\n        data = get_color_management_preferences()\n        colorspace_data = [\n            RenderProduct(\n                colorspace=data[\"display\"],\n                view=data[\"view\"],\n                productName=\"\"\n            )\n        ]\n        return colorspace_data\n\n\ndef get_default_display_view_colorspace():\n    \"\"\"Returns the colorspace attribute of the default (display, view) pair.\n\n    It's used for 'ociocolorspace' parm in OpenGL Node.\"\"\"\n\n    prefs = get_color_management_preferences()\n    return get_display_view_colorspace_name(\n        config_path=prefs[\"config\"],\n        display=prefs[\"display\"],\n        view=prefs[\"view\"]\n    )\n"
  },
  {
    "path": "openpype/hosts/houdini/api/creator_node_shelves.py",
    "content": "\"\"\"Library to register OpenPype Creators for Houdini TAB node search menu.\n\nThis can be used to install custom houdini tools for the TAB search\nmenu which will trigger a publish instance to be created interactively.\n\nThe Creators are automatically registered on launch of Houdini through the\nHoudini integration's `host.install()` method.\n\n\"\"\"\nimport contextlib\nimport tempfile\nimport logging\nimport os\n\nfrom openpype.client import get_asset_by_name\nfrom openpype.pipeline import registered_host\nfrom openpype.pipeline.create import CreateContext\nfrom openpype.resources import get_openpype_icon_filepath\n\nimport hou\nimport stateutils\nimport soptoolutils\nimport loptoolutils\nimport cop2toolutils\n\n\nlog = logging.getLogger(__name__)\n\nCATEGORY_GENERIC_TOOL = {\n    hou.sopNodeTypeCategory(): soptoolutils.genericTool,\n    hou.cop2NodeTypeCategory(): cop2toolutils.genericTool,\n    hou.lopNodeTypeCategory(): loptoolutils.genericTool\n}\n\n\nCREATE_SCRIPT = \"\"\"\nfrom openpype.hosts.houdini.api.creator_node_shelves import create_interactive\ncreate_interactive(\"{identifier}\", **kwargs)\n\"\"\"\n\n\ndef create_interactive(creator_identifier, **kwargs):\n    \"\"\"Create a Creator using its identifier interactively.\n\n    This is used by the generated shelf tools as callback when a user selects\n    the creator from the node tab search menu.\n\n    The `kwargs` should be what Houdini passes to the tool create scripts\n    context. For more information see:\n    https://www.sidefx.com/docs/houdini/hom/tool_script.html#arguments\n\n    Args:\n        creator_identifier (str): The creator identifier of the Creator plugin\n            to create.\n\n    Return:\n        list: The created instances.\n\n    \"\"\"\n    host = registered_host()\n    context = CreateContext(host)\n    creator = context.manual_creators.get(creator_identifier)\n    if not creator:\n        raise RuntimeError(\"Invalid creator identifier: {}\".format(\n            creator_identifier)\n        )\n\n    # TODO Use Qt instead\n    result, variant = hou.ui.readInput(\n        \"Define variant name\",\n        buttons=(\"Ok\", \"Cancel\"),\n        initial_contents=creator.get_default_variant(),\n        title=\"Define variant\",\n        help=\"Set the variant for the publish instance\",\n        close_choice=1\n    )\n\n    if result == 1:\n        # User interrupted\n        return\n\n    variant = variant.strip()\n    if not variant:\n        raise RuntimeError(\"Empty variant value entered.\")\n\n    # TODO: Once more elaborate unique create behavior should exist per Creator\n    #   instead of per network editor area then we should move this from here\n    #   to a method on the Creators for which this could be the default\n    #   implementation.\n    pane = stateutils.activePane(kwargs)\n    if isinstance(pane, hou.NetworkEditor):\n        pwd = pane.pwd()\n        subset_name = creator.get_subset_name(\n            variant=variant,\n            task_name=context.get_current_task_name(),\n            asset_doc=get_asset_by_name(\n                project_name=context.get_current_project_name(),\n                asset_name=context.get_current_asset_name()\n            ),\n            project_name=context.get_current_project_name(),\n            host_name=context.host_name\n        )\n\n        tool_fn = CATEGORY_GENERIC_TOOL.get(pwd.childTypeCategory())\n        if tool_fn is not None:\n            out_null = tool_fn(kwargs, \"null\")\n            out_null.setName(\"OUT_{}\".format(subset_name), unique_name=True)\n\n    before = context.instances_by_id.copy()\n\n    # Create the instance\n    context.create(\n        creator_identifier=creator_identifier,\n        variant=variant,\n        pre_create_data={\"use_selection\": True}\n    )\n\n    # For convenience we set the new node as current since that's much more\n    # familiar to the artist when creating a node interactively\n    # TODO Allow to disable auto-select in studio settings or user preferences\n    after = context.instances_by_id\n    new = set(after) - set(before)\n    if new:\n        # Select the new instance\n        for instance_id in new:\n            instance = after[instance_id]\n            node = hou.node(instance.get(\"instance_node\"))\n            node.setCurrent(True)\n\n    return list(new)\n\n\n@contextlib.contextmanager\ndef shelves_change_block():\n    \"\"\"Write shelf changes at the end of the context.\"\"\"\n    hou.shelves.beginChangeBlock()\n    try:\n        yield\n    finally:\n        hou.shelves.endChangeBlock()\n\n\ndef install():\n    \"\"\"Install the Creator plug-ins to show in Houdini's TAB node search menu.\n\n    This function is re-entrant and can be called again to reinstall and\n    update the node definitions. For example during development it can be\n    useful to call it manually:\n        >>> from openpype.hosts.houdini.api.creator_node_shelves import install\n        >>> install()\n\n    Returns:\n        list: List of `hou.Tool` instances\n\n    \"\"\"\n\n    host = registered_host()\n\n    # Store the filepath on the host\n    # TODO: Define a less hacky static shelf path for current houdini session\n    filepath_attr = \"_creator_node_shelf_filepath\"\n    filepath = getattr(host, filepath_attr, None)\n    if filepath is None:\n        f = tempfile.NamedTemporaryFile(prefix=\"houdini_creator_nodes_\",\n                                        suffix=\".shelf\",\n                                        delete=False)\n        f.close()\n        filepath = f.name\n        setattr(host, filepath_attr, filepath)\n    elif os.path.exists(filepath):\n        # Remove any existing shelf file so that we can completey regenerate\n        # and update the tools file if creator identifiers change\n        os.remove(filepath)\n\n    icon = get_openpype_icon_filepath()\n    tab_menu_label = os.environ.get(\"AVALON_LABEL\") or \"AYON\"\n\n    # Create context only to get creator plugins, so we don't reset and only\n    # populate what we need to retrieve the list of creator plugins\n    create_context = CreateContext(host, reset=False)\n    create_context.reset_current_context()\n    create_context._reset_creator_plugins()\n\n    log.debug(\"Writing OpenPype Creator nodes to shelf: {}\".format(filepath))\n    tools = []\n\n    with shelves_change_block():\n        for identifier, creator in create_context.manual_creators.items():\n\n            # Allow the creator plug-in itself to override the categories\n            # for where they are shown with `Creator.get_network_categories()`\n            if not hasattr(creator, \"get_network_categories\"):\n                log.debug(\"Creator {} has no `get_network_categories` method \"\n                          \"and will not be added to TAB search.\")\n                continue\n\n            network_categories = creator.get_network_categories()\n            if not network_categories:\n                continue\n\n            key = \"ayon_create.{}\".format(identifier)\n            log.debug(f\"Registering {key}\")\n            script = CREATE_SCRIPT.format(identifier=identifier)\n            data = {\n                \"script\": script,\n                \"language\": hou.scriptLanguage.Python,\n                \"icon\": icon,\n                \"help\": \"Create Ayon publish instance for {}\".format(\n                    creator.label\n                ),\n                \"help_url\": None,\n                \"network_categories\": network_categories,\n                \"viewer_categories\": [],\n                \"cop_viewer_categories\": [],\n                \"network_op_type\": None,\n                \"viewer_op_type\": None,\n                \"locations\": [tab_menu_label]\n            }\n            label = \"Create {}\".format(creator.label)\n            tool = hou.shelves.tool(key)\n            if tool:\n                tool.setData(**data)\n                tool.setLabel(label)\n            else:\n                tool = hou.shelves.newTool(\n                    file_path=filepath,\n                    name=key,\n                    label=label,\n                    **data\n                )\n\n            tools.append(tool)\n\n    # Ensure the shelf is reloaded\n    hou.shelves.loadFile(filepath)\n\n    return tools\n"
  },
  {
    "path": "openpype/hosts/houdini/api/lib.py",
    "content": "# -*- coding: utf-8 -*-\nimport sys\nimport os\nimport errno\nimport re\nimport uuid\nimport logging\nfrom contextlib import contextmanager\nimport json\n\nimport six\n\nfrom openpype.lib import StringTemplate\nfrom openpype.client import get_project, get_asset_by_name\nfrom openpype.settings import get_current_project_settings\nfrom openpype.pipeline import (\n    Anatomy,\n    get_current_project_name,\n    get_current_asset_name,\n    registered_host,\n    get_current_context,\n    get_current_host_name,\n)\nfrom openpype.pipeline.create import CreateContext\nfrom openpype.pipeline.template_data import get_template_data\nfrom openpype.pipeline.context_tools import get_current_project_asset\nfrom openpype.widgets import popup\nfrom openpype.tools.utils.host_tools import get_tool_by_name\n\nimport hou\n\n\nself = sys.modules[__name__]\nself._parent = None\nlog = logging.getLogger(__name__)\nJSON_PREFIX = \"JSON:::\"\n\n\ndef get_asset_fps(asset_doc=None):\n    \"\"\"Return current asset fps.\"\"\"\n\n    if asset_doc is None:\n        asset_doc = get_current_project_asset(fields=[\"data.fps\"])\n    return asset_doc[\"data\"][\"fps\"]\n\n\ndef set_id(node, unique_id, overwrite=False):\n    exists = node.parm(\"id\")\n    if not exists:\n        imprint(node, {\"id\": unique_id})\n\n    if not exists and overwrite:\n        node.setParm(\"id\", unique_id)\n\n\ndef get_id(node):\n    \"\"\"Get the `cbId` attribute of the given node.\n\n    Args:\n        node (hou.Node): the name of the node to retrieve the attribute from\n\n    Returns:\n        str: cbId attribute of the node.\n\n    \"\"\"\n\n    if node is not None:\n        return node.parm(\"id\")\n\n\ndef generate_ids(nodes, asset_id=None):\n    \"\"\"Returns new unique ids for the given nodes.\n\n    Note: This does not assign the new ids, it only generates the values.\n\n    To assign new ids using this method:\n    >>> nodes = [\"a\", \"b\", \"c\"]\n    >>> for node, id in generate_ids(nodes):\n    >>>     set_id(node, id)\n\n    To also override any existing values (and assign regenerated ids):\n    >>> nodes = [\"a\", \"b\", \"c\"]\n    >>> for node, id in generate_ids(nodes):\n    >>>     set_id(node, id, overwrite=True)\n\n    Args:\n        nodes (list): List of nodes.\n        asset_id (str or bson.ObjectId): The database id for the *asset* to\n            generate for. When None provided the current asset in the\n            active session is used.\n\n    Returns:\n        list: A list of (node, id) tuples.\n\n    \"\"\"\n\n    if asset_id is None:\n        project_name = get_current_project_name()\n        asset_name = get_current_asset_name()\n        # Get the asset ID from the database for the asset of current context\n        asset_doc = get_asset_by_name(project_name, asset_name, fields=[\"_id\"])\n\n        assert asset_doc, \"No current asset found in Session\"\n        asset_id = asset_doc['_id']\n\n    node_ids = []\n    for node in nodes:\n        _, uid = str(uuid.uuid4()).rsplit(\"-\", 1)\n        unique_id = \"{}:{}\".format(asset_id, uid)\n        node_ids.append((node, unique_id))\n\n    return node_ids\n\n\ndef get_id_required_nodes():\n\n    valid_types = [\"geometry\"]\n    nodes = {n for n in hou.node(\"/out\").children() if\n             n.type().name() in valid_types}\n\n    return list(nodes)\n\n\ndef get_output_parameter(node):\n    \"\"\"Return the render output parameter of the given node\n\n    Example:\n        root = hou.node(\"/obj\")\n        my_alembic_node = root.createNode(\"alembic\")\n        get_output_parameter(my_alembic_node)\n        >>> \"filename\"\n\n    Notes:\n        I'm using node.type().name() to get on par with the creators,\n            Because the return value of `node.type().name()` is the\n            same string value used in creators\n            e.g. instance_data.update({\"node_type\": \"alembic\"})\n\n        Rop nodes in different network categories have\n            the same output parameter.\n            So, I took that into consideration as a hint for\n            future development.\n\n    Args:\n        node(hou.Node): node instance\n\n    Returns:\n        hou.Parm\n    \"\"\"\n\n    node_type = node.type().name()\n\n    # Figure out which type of node is being rendered\n    if node_type in {\"alembic\", \"rop_alembic\"}:\n        return node.parm(\"filename\")\n    elif node_type == \"arnold\":\n        if node_type.evalParm(\"ar_ass_export_enable\"):\n            return node.parm(\"ar_ass_file\")\n        return node.parm(\"ar_picture\")\n    elif node_type in {\n        \"geometry\",\n        \"rop_geometry\",\n        \"filmboxfbx\",\n        \"rop_fbx\"\n    }:\n        return node.parm(\"sopoutput\")\n    elif node_type == \"comp\":\n        return node.parm(\"copoutput\")\n    elif node_type in {\"karma\", \"opengl\"}:\n        return node.parm(\"picture\")\n    elif node_type == \"ifd\":  # Mantra\n        if node.evalParm(\"soho_outputmode\"):\n            return node.parm(\"soho_diskfile\")\n        return node.parm(\"vm_picture\")\n    elif node_type == \"Redshift_Proxy_Output\":\n        return node.parm(\"RS_archive_file\")\n    elif node_type == \"Redshift_ROP\":\n        return node.parm(\"RS_outputFileNamePrefix\")\n    elif node_type in {\"usd\", \"usd_rop\", \"usdexport\"}:\n        return node.parm(\"lopoutput\")\n    elif node_type in {\"usdrender\", \"usdrender_rop\"}:\n        return node.parm(\"outputimage\")\n    elif node_type == \"vray_renderer\":\n        return node.parm(\"SettingsOutput_img_file_path\")\n\n    raise TypeError(\"Node type '%s' not supported\" % node_type)\n\n\ndef set_scene_fps(fps):\n    hou.setFps(fps)\n\n\n# Valid FPS\ndef validate_fps():\n    \"\"\"Validate current scene FPS and show pop-up when it is incorrect\n\n    Returns:\n        bool\n\n    \"\"\"\n\n    fps = get_asset_fps()\n    current_fps = hou.fps()  # returns float\n\n    if current_fps != fps:\n\n        # Find main window\n        parent = hou.ui.mainQtWindow()\n        if parent is None:\n            pass\n        else:\n            dialog = popup.PopupUpdateKeys(parent=parent)\n            dialog.setModal(True)\n            dialog.setWindowTitle(\"Houdini scene does not match project FPS\")\n            dialog.setMessage(\"Scene %i FPS does not match project %i FPS\" %\n                              (current_fps, fps))\n            dialog.setButtonText(\"Fix\")\n\n            # on_show is the Fix button clicked callback\n            dialog.on_clicked_state.connect(lambda: set_scene_fps(fps))\n\n            dialog.show()\n\n            return False\n\n    return True\n\n\ndef create_remote_publish_node(force=True):\n    \"\"\"Function to create a remote publish node in /out\n\n    This is a hacked \"Shell\" node that does *nothing* except for triggering\n    `colorbleed.lib.publish_remote()` as pre-render script.\n\n    All default attributes of the Shell node are hidden to the Artist to\n    avoid confusion.\n\n    Additionally some custom attributes are added that can be collected\n    by a Collector to set specific settings for the publish, e.g. whether\n    to separate the jobs per instance or process in one single job.\n\n    \"\"\"\n\n    cmd = \"import colorbleed.lib; colorbleed.lib.publish_remote()\"\n\n    existing = hou.node(\"/out/REMOTE_PUBLISH\")\n    if existing:\n        if force:\n            log.warning(\"Removing existing '/out/REMOTE_PUBLISH' node..\")\n            existing.destroy()\n        else:\n            raise RuntimeError(\"Node already exists /out/REMOTE_PUBLISH. \"\n                               \"Please remove manually or set `force` to \"\n                               \"True.\")\n\n    # Create the shell node\n    out = hou.node(\"/out\")\n    node = out.createNode(\"shell\", node_name=\"REMOTE_PUBLISH\")\n    node.moveToGoodPosition()\n\n    # Set color make it stand out (avalon/pyblish color)\n    node.setColor(hou.Color(0.439, 0.709, 0.933))\n\n    # Set the pre-render script\n    node.setParms({\n        \"prerender\": cmd,\n        \"lprerender\": \"python\"  # command language\n    })\n\n    # Lock the attributes to ensure artists won't easily mess things up.\n    node.parm(\"prerender\").lock(True)\n    node.parm(\"lprerender\").lock(True)\n\n    # Lock up the actual shell command\n    command_parm = node.parm(\"command\")\n    command_parm.set(\"\")\n    command_parm.lock(True)\n    shellexec_parm = node.parm(\"shellexec\")\n    shellexec_parm.set(False)\n    shellexec_parm.lock(True)\n\n    # Get the node's parm template group so we can customize it\n    template = node.parmTemplateGroup()\n\n    # Hide default tabs\n    template.hideFolder(\"Shell\", True)\n    template.hideFolder(\"Scripts\", True)\n\n    # Hide default settings\n    template.hide(\"execute\", True)\n    template.hide(\"renderdialog\", True)\n    template.hide(\"trange\", True)\n    template.hide(\"f\", True)\n    template.hide(\"take\", True)\n\n    # Add custom settings to this node.\n    parm_folder = hou.FolderParmTemplate(\"folder\", \"Submission Settings\")\n\n    # Separate Jobs per Instance\n    parm = hou.ToggleParmTemplate(name=\"separateJobPerInstance\",\n                                  label=\"Separate Job per Instance\",\n                                  default_value=False)\n    parm_folder.addParmTemplate(parm)\n\n    # Add our custom Submission Settings folder\n    template.append(parm_folder)\n\n    # Apply template back to the node\n    node.setParmTemplateGroup(template)\n\n\ndef render_rop(ropnode):\n    \"\"\"Render ROP node utility for Publishing.\n\n    This renders a ROP node with the settings we want during Publishing.\n    \"\"\"\n    # Print verbose when in batch mode without UI\n    verbose = not hou.isUIAvailable()\n\n    # Render\n    try:\n        ropnode.render(verbose=verbose,\n                       # Allow Deadline to capture completion percentage\n                       output_progress=verbose)\n    except hou.Error as exc:\n        # The hou.Error is not inherited from a Python Exception class,\n        # so we explicitly capture the houdini error, otherwise pyblish\n        # will remain hanging.\n        import traceback\n        traceback.print_exc()\n        raise RuntimeError(\"Render failed: {0}\".format(exc))\n\n\ndef imprint(node, data, update=False):\n    \"\"\"Store attributes with value on a node\n\n    Depending on the type of attribute it creates the correct parameter\n    template. Houdini uses a template per type, see the docs for more\n    information.\n\n    http://www.sidefx.com/docs/houdini/hom/hou/ParmTemplate.html\n\n    Because of some update glitch where you cannot overwrite existing\n    ParmTemplates on node using:\n        `setParmTemplates()` and `parmTuplesInFolder()`\n    update is done in another pass.\n\n    Args:\n        node(hou.Node): node object from Houdini\n        data(dict): collection of attributes and their value\n        update (bool, optional): flag if imprint should update\n            already existing data or leave them untouched and only\n            add new.\n\n    Returns:\n        None\n\n    \"\"\"\n    if not data:\n        return\n    if not node:\n        self.log.error(\"Node is not set, calling imprint on invalid data.\")\n        return\n\n    current_parms = {p.name(): p for p in node.spareParms()}\n    update_parm_templates = []\n    new_parm_templates = []\n\n    for key, value in data.items():\n        if value is None:\n            continue\n\n        parm_template = get_template_from_value(key, value)\n\n        if key in current_parms:\n            if node.evalParm(key) == value:\n                continue\n            if not update:\n                log.debug(f\"{key} already exists on {node}\")\n            else:\n                log.debug(f\"replacing {key}\")\n                update_parm_templates.append(parm_template)\n            continue\n\n        new_parm_templates.append(parm_template)\n\n    if not new_parm_templates and not update_parm_templates:\n        return\n\n    parm_group = node.parmTemplateGroup()\n\n    # Add new parm templates\n    if new_parm_templates:\n        parm_folder = parm_group.findFolder(\"Extra\")\n\n        # if folder doesn't exist yet, create one and append to it,\n        # else append to existing one\n        if not parm_folder:\n            parm_folder = hou.FolderParmTemplate(\"folder\", \"Extra\")\n            parm_folder.setParmTemplates(new_parm_templates)\n            parm_group.append(parm_folder)\n        else:\n            # Add to parm template folder instance then replace with updated\n            # one in parm template group\n            for template in new_parm_templates:\n                parm_folder.addParmTemplate(template)\n            parm_group.replace(parm_folder.name(), parm_folder)\n\n    # Update existing parm templates\n    for parm_template in update_parm_templates:\n        parm_group.replace(parm_template.name(), parm_template)\n\n        # When replacing a parm with a parm of the same name it preserves its\n        # value if before the replacement the parm was not at the default,\n        # because it has a value override set. Since we're trying to update the\n        # parm by using the new value as `default` we enforce the parm is at\n        # default state\n        node.parm(parm_template.name()).revertToDefaults()\n\n    node.setParmTemplateGroup(parm_group)\n\n\ndef lsattr(attr, value=None, root=\"/\"):\n    \"\"\"Return nodes that have `attr`\n     When `value` is not None it will only return nodes matching that value\n     for the given attribute.\n     Args:\n         attr (str): Name of the attribute (hou.Parm)\n         value (object, Optional): The value to compare the attribute too.\n            When the default None is provided the value check is skipped.\n        root (str): The root path in Houdini to search in.\n    Returns:\n        list: Matching nodes that have attribute with value.\n    \"\"\"\n    if value is None:\n        # Use allSubChildren() as allNodes() errors on nodes without\n        # permission to enter without a means to continue of querying\n        # the rest\n        nodes = hou.node(root).allSubChildren()\n        return [n for n in nodes if n.parm(attr)]\n    return lsattrs({attr: value})\n\n\ndef lsattrs(attrs, root=\"/\"):\n    \"\"\"Return nodes matching `key` and `value`\n    Arguments:\n        attrs (dict): collection of attribute: value\n        root (str): The root path in Houdini to search in.\n    Example:\n        >> lsattrs({\"id\": \"myId\"})\n        [\"myNode\"]\n        >> lsattr(\"id\")\n        [\"myNode\", \"myOtherNode\"]\n    Returns:\n        list: Matching nodes that have attribute with value.\n    \"\"\"\n\n    matches = set()\n    # Use allSubChildren() as allNodes() errors on nodes without\n    # permission to enter without a means to continue of querying\n    # the rest\n    nodes = hou.node(root).allSubChildren()\n    for node in nodes:\n        for attr in attrs:\n            if not node.parm(attr):\n                continue\n            elif node.evalParm(attr) != attrs[attr]:\n                continue\n            else:\n                matches.add(node)\n\n    return list(matches)\n\n\ndef read(node):\n    \"\"\"Read the container data in to a dict\n\n    Args:\n        node(hou.Node): Houdini node\n\n    Returns:\n        dict\n\n    \"\"\"\n    # `spareParms` returns a tuple of hou.Parm objects\n    data = {}\n    if not node:\n        return data\n    for parameter in node.spareParms():\n        value = parameter.eval()\n        # test if value is json encoded dict\n        if isinstance(value, six.string_types) and \\\n                value.startswith(JSON_PREFIX):\n            try:\n                value = json.loads(value[len(JSON_PREFIX):])\n            except json.JSONDecodeError:\n                # not a json\n                pass\n        data[parameter.name()] = value\n\n    return data\n\n\n@contextmanager\ndef maintained_selection():\n    \"\"\"Maintain selection during context\n    Example:\n        >>> with maintained_selection():\n        ...     # Modify selection\n        ...     node.setSelected(on=False, clear_all_selected=True)\n        >>> # Selection restored\n    \"\"\"\n\n    previous_selection = hou.selectedNodes()\n    try:\n        yield\n    finally:\n        # Clear the selection\n        # todo: does hou.clearAllSelected() do the same?\n        for node in hou.selectedNodes():\n            node.setSelected(on=False)\n\n        if previous_selection:\n            for node in previous_selection:\n                node.setSelected(on=True)\n\n\ndef reset_framerange():\n    \"\"\"Set frame range and FPS to current asset\"\"\"\n\n    # Get asset data\n    project_name = get_current_project_name()\n    asset_name = get_current_asset_name()\n    # Get the asset ID from the database for the asset of current context\n    asset_doc = get_asset_by_name(project_name, asset_name)\n    asset_data = asset_doc[\"data\"]\n\n    # Get FPS\n    fps = get_asset_fps(asset_doc)\n\n    # Get Start and End Frames\n    frame_start = asset_data.get(\"frameStart\")\n    frame_end = asset_data.get(\"frameEnd\")\n\n    if frame_start is None or frame_end is None:\n        log.warning(\"No edit information found for %s\" % asset_name)\n        return\n\n    handle_start = asset_data.get(\"handleStart\", 0)\n    handle_end = asset_data.get(\"handleEnd\", 0)\n\n    frame_start -= int(handle_start)\n    frame_end += int(handle_end)\n\n    # Set frame range and FPS\n    print(\"Setting scene FPS to {}\".format(int(fps)))\n    set_scene_fps(fps)\n    hou.playbar.setFrameRange(frame_start, frame_end)\n    hou.playbar.setPlaybackRange(frame_start, frame_end)\n    hou.setFrame(frame_start)\n\n\ndef get_main_window():\n    \"\"\"Acquire Houdini's main window\"\"\"\n    if self._parent is None:\n        self._parent = hou.ui.mainQtWindow()\n    return self._parent\n\n\ndef get_template_from_value(key, value):\n    if isinstance(value, float):\n        parm = hou.FloatParmTemplate(name=key,\n                                     label=key,\n                                     num_components=1,\n                                     default_value=(value,))\n    elif isinstance(value, bool):\n        parm = hou.ToggleParmTemplate(name=key,\n                                      label=key,\n                                      default_value=value)\n    elif isinstance(value, int):\n        parm = hou.IntParmTemplate(name=key,\n                                   label=key,\n                                   num_components=1,\n                                   default_value=(value,))\n    elif isinstance(value, six.string_types):\n        parm = hou.StringParmTemplate(name=key,\n                                      label=key,\n                                      num_components=1,\n                                      default_value=(value,))\n    elif isinstance(value, (dict, list, tuple)):\n        parm = hou.StringParmTemplate(name=key,\n                                      label=key,\n                                      num_components=1,\n                                      default_value=(\n                                          JSON_PREFIX + json.dumps(value),))\n    else:\n        raise TypeError(\"Unsupported type: %r\" % type(value))\n\n    return parm\n\n\ndef get_frame_data(node, log=None):\n    \"\"\"Get the frame data: `frameStartHandle`, `frameEndHandle`\n    and `byFrameStep`.\n\n    This function uses Houdini node's `trange`, `t1, `t2` and `t3`\n    parameters as the source of truth for the full inclusive frame\n    range to render, as such these are considered as the frame\n    range including the handles.\n\n    The non-inclusive frame start and frame end without handles\n    can be computed by subtracting the handles from the inclusive\n    frame range.\n\n    Args:\n        node (hou.Node): ROP node to retrieve frame range from,\n            the frame range is assumed to be the frame range\n            *including* the start and end handles.\n\n    Returns:\n        dict: frame data for `frameStartHandle`, `frameEndHandle`\n            and `byFrameStep`.\n\n    \"\"\"\n\n    if log is None:\n        log = self.log\n\n    data = {}\n\n    if node.parm(\"trange\") is None:\n        log.debug(\n            \"Node has no 'trange' parameter: {}\".format(node.path())\n        )\n        return data\n\n    if node.evalParm(\"trange\") == 0:\n        data[\"frameStartHandle\"] = hou.intFrame()\n        data[\"frameEndHandle\"] = hou.intFrame()\n        data[\"byFrameStep\"] = 1.0\n\n        log.info(\n            \"Node '{}' has 'Render current frame' set.\\n\"\n            \"Asset Handles are ignored.\\n\"\n            \"frameStart and frameEnd are set to the \"\n            \"current frame.\".format(node.path())\n        )\n    else:\n        data[\"frameStartHandle\"] = int(node.evalParm(\"f1\"))\n        data[\"frameEndHandle\"] = int(node.evalParm(\"f2\"))\n        data[\"byFrameStep\"] = node.evalParm(\"f3\")\n\n    return data\n\n\ndef splitext(name, allowed_multidot_extensions):\n    # type: (str, list) -> tuple\n    \"\"\"Split file name to name and extension.\n\n    Args:\n        name (str): File name to split.\n        allowed_multidot_extensions (list of str): List of allowed multidot\n            extensions.\n\n    Returns:\n        tuple: Name and extension.\n    \"\"\"\n\n    for ext in allowed_multidot_extensions:\n        if name.endswith(ext):\n            return name[:-len(ext)], ext\n\n    return os.path.splitext(name)\n\n\ndef get_top_referenced_parm(parm):\n\n    processed = set()  # disallow infinite loop\n    while True:\n        if parm.path() in processed:\n            raise RuntimeError(\"Parameter references result in cycle.\")\n\n        processed.add(parm.path())\n\n        ref = parm.getReferencedParm()\n        if ref.path() == parm.path():\n            # It returns itself when it doesn't reference\n            # another parameter\n            return ref\n        else:\n            parm = ref\n\n\ndef evalParmNoFrame(node, parm, pad_character=\"#\"):\n\n    parameter = node.parm(parm)\n    assert parameter, \"Parameter does not exist: %s.%s\" % (node, parm)\n\n    # If the parameter has a parameter reference, then get that\n    # parameter instead as otherwise `unexpandedString()` fails.\n    parameter = get_top_referenced_parm(parameter)\n\n    # Substitute out the frame numbering with padded characters\n    try:\n        raw = parameter.unexpandedString()\n    except hou.Error as exc:\n        print(\"Failed: %s\" % parameter)\n        raise RuntimeError(exc)\n\n    def replace(match):\n        padding = 1\n        n = match.group(2)\n        if n and int(n):\n            padding = int(n)\n        return pad_character * padding\n\n    expression = re.sub(r\"(\\$F([0-9]*))\", replace, raw)\n\n    with hou.ScriptEvalContext(parameter):\n        return hou.expandStringAtFrame(expression, 0)\n\n\ndef get_color_management_preferences():\n    \"\"\"Get default OCIO preferences\"\"\"\n    return {\n        \"config\": hou.Color.ocio_configPath(),\n        \"display\": hou.Color.ocio_defaultDisplay(),\n        \"view\": hou.Color.ocio_defaultView()\n    }\n\n\ndef get_obj_node_output(obj_node):\n    \"\"\"Find output node.\n\n    If the node has any output node return the\n    output node with the minimum `outputidx`.\n    When no output is present return the node\n    with the display flag set. If no output node is\n    detected then None is returned.\n\n    Arguments:\n        node (hou.Node): The node to retrieve a single\n            the output node for.\n\n    Returns:\n        Optional[hou.Node]: The child output node.\n\n    \"\"\"\n\n    outputs = obj_node.subnetOutputs()\n    if not outputs:\n        return\n\n    elif len(outputs) == 1:\n        return outputs[0]\n\n    else:\n        return min(outputs,\n                   key=lambda node: node.evalParm('outputidx'))\n\n\ndef get_output_children(output_node, include_sops=True):\n    \"\"\"Recursively return a list of all output nodes\n    contained in this node including this node.\n\n    It works in a similar manner to output_node.allNodes().\n    \"\"\"\n    out_list = [output_node]\n\n    if output_node.childTypeCategory() == hou.objNodeTypeCategory():\n        for child in output_node.children():\n            out_list += get_output_children(child, include_sops=include_sops)\n\n    elif include_sops and \\\n            output_node.childTypeCategory() == hou.sopNodeTypeCategory():\n        out = get_obj_node_output(output_node)\n        if out:\n            out_list += [out]\n\n    return out_list\n\n\ndef get_resolution_from_doc(doc):\n    \"\"\"Get resolution from the given asset document. \"\"\"\n\n    if not doc or \"data\" not in doc:\n        print(\"Entered document is not valid. \\\"{}\\\"\".format(str(doc)))\n        return None\n\n    resolution_width = doc[\"data\"].get(\"resolutionWidth\")\n    resolution_height = doc[\"data\"].get(\"resolutionHeight\")\n\n    # Make sure both width and height are set\n    if resolution_width is None or resolution_height is None:\n        print(\"No resolution information found for \\\"{}\\\"\".format(doc[\"name\"]))\n        return None\n\n    return int(resolution_width), int(resolution_height)\n\n\ndef set_camera_resolution(camera, asset_doc=None):\n    \"\"\"Apply resolution to camera from asset document of the publish\"\"\"\n\n    if not asset_doc:\n        asset_doc = get_current_project_asset()\n\n    resolution = get_resolution_from_doc(asset_doc)\n\n    if resolution:\n        print(\"Setting camera resolution: {} -> {}x{}\".format(\n            camera.name(), resolution[0], resolution[1]\n        ))\n        camera.parm(\"resx\").set(resolution[0])\n        camera.parm(\"resy\").set(resolution[1])\n\n\ndef get_camera_from_container(container):\n    \"\"\"Get camera from container node. \"\"\"\n\n    cameras = container.recursiveGlob(\n        \"*\",\n        filter=hou.nodeTypeFilter.ObjCamera,\n        include_subnets=False\n    )\n\n    assert len(cameras) == 1, \"Camera instance must have only one camera\"\n    return cameras[0]\n\n\ndef get_current_context_template_data_with_asset_data():\n    \"\"\"\n    TODOs:\n        Support both 'assetData' and 'folderData' in future.\n    \"\"\"\n\n    context = get_current_context()\n    project_name = context[\"project_name\"]\n    asset_name = context[\"asset_name\"]\n    task_name = context[\"task_name\"]\n    host_name = get_current_host_name()\n\n    anatomy = Anatomy(project_name)\n    project_doc = get_project(project_name)\n    asset_doc = get_asset_by_name(project_name, asset_name)\n\n    # get context specific vars\n    asset_data = asset_doc[\"data\"]\n\n    # compute `frameStartHandle` and `frameEndHandle`\n    frame_start = asset_data.get(\"frameStart\")\n    frame_end = asset_data.get(\"frameEnd\")\n    handle_start = asset_data.get(\"handleStart\")\n    handle_end = asset_data.get(\"handleEnd\")\n    if frame_start is not None and handle_start is not None:\n        asset_data[\"frameStartHandle\"] = frame_start - handle_start\n\n    if frame_end is not None and handle_end is not None:\n        asset_data[\"frameEndHandle\"] = frame_end + handle_end\n\n    template_data = get_template_data(\n        project_doc, asset_doc, task_name, host_name\n    )\n    template_data[\"root\"] = anatomy.roots\n    template_data[\"assetData\"] = asset_data\n\n    return template_data\n\n\ndef get_context_var_changes():\n    \"\"\"get context var changes.\"\"\"\n\n    houdini_vars_to_update = {}\n\n    project_settings = get_current_project_settings()\n    houdini_vars_settings = \\\n        project_settings[\"houdini\"][\"general\"][\"update_houdini_var_context\"]\n\n    if not houdini_vars_settings[\"enabled\"]:\n        return houdini_vars_to_update\n\n    houdini_vars = houdini_vars_settings[\"houdini_vars\"]\n\n    # No vars specified - nothing to do\n    if not houdini_vars:\n        return houdini_vars_to_update\n\n    # Get Template data\n    template_data = get_current_context_template_data_with_asset_data()\n\n    # Set Houdini Vars\n    for item in houdini_vars:\n        # For consistency reasons we always force all vars to be uppercase\n        # Also remove any leading, and trailing whitespaces.\n        var = item[\"var\"].strip().upper()\n\n        # get and resolve template in value\n        item_value = StringTemplate.format_template(\n            item[\"value\"],\n            template_data\n        )\n\n        if var == \"JOB\" and item_value == \"\":\n            # sync $JOB to $HIP if $JOB is empty\n            item_value = os.environ[\"HIP\"]\n\n        if item[\"is_directory\"]:\n            item_value = item_value.replace(\"\\\\\", \"/\")\n\n        current_value = hou.hscript(\"echo -n `${}`\".format(var))[0]\n\n        if current_value != item_value:\n            houdini_vars_to_update[var] = (\n                current_value, item_value, item[\"is_directory\"]\n            )\n\n    return houdini_vars_to_update\n\n\ndef update_houdini_vars_context():\n    \"\"\"Update asset context variables\"\"\"\n\n    for var, (_old, new, is_directory) in get_context_var_changes().items():\n        if is_directory:\n            try:\n                os.makedirs(new)\n            except OSError as e:\n                if e.errno != errno.EEXIST:\n                    print(\n                        \"Failed to create ${} dir. Maybe due to \"\n                        \"insufficient permissions.\".format(var)\n                    )\n\n        hou.hscript(\"set {}={}\".format(var, new))\n        os.environ[var] = new\n        print(\"Updated ${} to {}\".format(var, new))\n\n\ndef update_houdini_vars_context_dialog():\n    \"\"\"Show pop-up to update asset context variables\"\"\"\n    update_vars = get_context_var_changes()\n    if not update_vars:\n        # Nothing to change\n        print(\"Nothing to change, Houdini vars are already up to date.\")\n        return\n\n    message = \"\\n\".join(\n        \"${}: {} -> {}\".format(var, old or \"None\", new or \"None\")\n        for var, (old, new, _is_directory) in update_vars.items()\n    )\n\n    # TODO: Use better UI!\n    parent = hou.ui.mainQtWindow()\n    dialog = popup.Popup(parent=parent)\n    dialog.setModal(True)\n    dialog.setWindowTitle(\"Houdini scene has outdated asset variables\")\n    dialog.setMessage(message)\n    dialog.setButtonText(\"Fix\")\n\n    # on_show is the Fix button clicked callback\n    dialog.on_clicked.connect(update_houdini_vars_context)\n\n    dialog.show()\n\n\ndef publisher_show_and_publish(comment=None):\n    \"\"\"Open publisher window and trigger publishing action.\n\n    Args:\n        comment (Optional[str]): Comment to set in publisher window.\n    \"\"\"\n\n    main_window = get_main_window()\n    publisher_window = get_tool_by_name(\n        tool_name=\"publisher\",\n        parent=main_window,\n    )\n    publisher_window.show_and_publish(comment)\n\n\ndef find_rop_input_dependencies(input_tuple):\n    \"\"\"Self publish from ROP nodes.\n\n    Arguments:\n        tuple (hou.RopNode.inputDependencies) which can be a nested tuples\n        represents the input dependencies of the ROP node, consisting of ROPs,\n        and the frames that need to be be rendered prior to rendering the ROP.\n\n    Returns:\n        list of the RopNode.path() that can be found inside\n        the input tuple.\n    \"\"\"\n\n    out_list = []\n    if isinstance(input_tuple[0], hou.RopNode):\n        return input_tuple[0].path()\n\n    if isinstance(input_tuple[0], tuple):\n        for item in input_tuple:\n            out_list.append(find_rop_input_dependencies(item))\n\n    return out_list\n\n\ndef self_publish():\n    \"\"\"Self publish from ROP nodes.\n\n    Firstly, it gets the node and its dependencies.\n    Then, it deactivates all other ROPs\n    And finaly, it triggers the publishing action.\n    \"\"\"\n\n    result, comment = hou.ui.readInput(\n        \"Add Publish Comment\",\n        buttons=(\"Publish\", \"Cancel\"),\n        title=\"Publish comment\",\n        close_choice=1\n    )\n\n    if result:\n        return\n\n    current_node = hou.node(\".\")\n    inputs_paths = find_rop_input_dependencies(\n        current_node.inputDependencies()\n    )\n    inputs_paths.append(current_node.path())\n\n    host = registered_host()\n    context = CreateContext(host, reset=True)\n\n    for instance in context.instances:\n        node_path = instance.data.get(\"instance_node\")\n        instance[\"active\"] = node_path and node_path in inputs_paths\n\n    context.save_changes()\n\n    publisher_show_and_publish(comment)\n\n\ndef add_self_publish_button(node):\n    \"\"\"Adds a self publish button to the rop node.\"\"\"\n\n    label = os.environ.get(\"AVALON_LABEL\") or \"AYON\"\n\n    button_parm = hou.ButtonParmTemplate(\n        \"ayon_self_publish\",\n        \"{} Publish\".format(label),\n        script_callback=\"from openpype.hosts.houdini.api.lib import \"\n                        \"self_publish; self_publish()\",\n        script_callback_language=hou.scriptLanguage.Python,\n        join_with_next=True\n    )\n\n    template = node.parmTemplateGroup()\n    template.insertBefore((0,), button_parm)\n    node.setParmTemplateGroup(template)\n"
  },
  {
    "path": "openpype/hosts/houdini/api/pipeline.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Pipeline tools for OpenPype Houdini integration.\"\"\"\nimport os\nimport sys\nimport logging\n\nimport hou  # noqa\n\nfrom openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost\n\nimport pyblish.api\n\nfrom openpype.pipeline import (\n    register_creator_plugin_path,\n    register_loader_plugin_path,\n    register_inventory_action_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.pipeline.load import any_outdated_containers\nfrom openpype.hosts.houdini import HOUDINI_HOST_DIR\nfrom openpype.hosts.houdini.api import lib, shelves, creator_node_shelves\n\nfrom openpype.lib import (\n    register_event_callback,\n    emit_event,\n)\n\n\nlog = logging.getLogger(\"openpype.hosts.houdini\")\n\nAVALON_CONTAINERS = \"/obj/AVALON_CONTAINERS\"\nCONTEXT_CONTAINER = \"/obj/OpenPypeContext\"\nIS_HEADLESS = not hasattr(hou, \"ui\")\n\nPLUGINS_DIR = os.path.join(HOUDINI_HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\nINVENTORY_PATH = os.path.join(PLUGINS_DIR, \"inventory\")\n\n\nclass HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):\n    name = \"houdini\"\n\n    def __init__(self):\n        super(HoudiniHost, self).__init__()\n        self._op_events = {}\n        self._has_been_setup = False\n\n    def install(self):\n        pyblish.api.register_host(\"houdini\")\n        pyblish.api.register_host(\"hython\")\n        pyblish.api.register_host(\"hpython\")\n\n        pyblish.api.register_plugin_path(PUBLISH_PATH)\n        register_loader_plugin_path(LOAD_PATH)\n        register_creator_plugin_path(CREATE_PATH)\n        register_inventory_action_path(INVENTORY_PATH)\n\n        log.info(\"Installing callbacks ... \")\n        # register_event_callback(\"init\", on_init)\n        self._register_callbacks()\n        register_event_callback(\"before.save\", before_save)\n        register_event_callback(\"save\", on_save)\n        register_event_callback(\"open\", on_open)\n        register_event_callback(\"new\", on_new)\n\n        self._has_been_setup = True\n\n        # Set asset settings for the empty scene directly after launch of\n        # Houdini so it initializes into the correct scene FPS,\n        # Frame Range, etc.\n        # TODO: make sure this doesn't trigger when\n        #       opening with last workfile.\n        _set_context_settings()\n\n        if not IS_HEADLESS:\n            import hdefereval  # noqa, hdefereval is only available in ui mode\n            # Defer generation of shelves due to issue on Windows where shelf\n            # initialization during start up delays Houdini UI by minutes\n            # making it extremely slow to launch.\n            hdefereval.executeDeferred(shelves.generate_shelves)\n\n        if not IS_HEADLESS:\n            import hdefereval # noqa, hdefereval is only available in ui mode\n            hdefereval.executeDeferred(creator_node_shelves.install)\n\n    def workfile_has_unsaved_changes(self):\n        return hou.hipFile.hasUnsavedChanges()\n\n    def get_workfile_extensions(self):\n        return [\".hip\", \".hiplc\", \".hipnc\"]\n\n    def save_workfile(self, dst_path=None):\n        # Force forwards slashes to avoid segfault\n        if dst_path:\n            dst_path = dst_path.replace(\"\\\\\", \"/\")\n        hou.hipFile.save(file_name=dst_path,\n                         save_to_recent_files=True)\n        return dst_path\n\n    def open_workfile(self, filepath):\n        # Force forwards slashes to avoid segfault\n        filepath = filepath.replace(\"\\\\\", \"/\")\n\n        hou.hipFile.load(filepath,\n                         suppress_save_prompt=True,\n                         ignore_load_warnings=False)\n\n        return filepath\n\n    def get_current_workfile(self):\n        current_filepath = hou.hipFile.path()\n        if (os.path.basename(current_filepath) == \"untitled.hip\" and\n                not os.path.exists(current_filepath)):\n            # By default a new scene in houdini is saved in the current\n            # working directory as \"untitled.hip\" so we need to capture\n            # that and consider it 'not saved' when it's in that state.\n            return None\n\n        return current_filepath\n\n    def get_containers(self):\n        return ls()\n\n    def _register_callbacks(self):\n        for event in self._op_events.copy().values():\n            if event is None:\n                continue\n\n            try:\n                hou.hipFile.removeEventCallback(event)\n            except RuntimeError as e:\n                log.info(e)\n\n        self._op_events[on_file_event_callback] = hou.hipFile.addEventCallback(\n            on_file_event_callback\n        )\n\n    @staticmethod\n    def create_context_node():\n        \"\"\"Helper for creating context holding node.\n\n        Returns:\n            hou.Node: context node\n\n        \"\"\"\n        obj_network = hou.node(\"/obj\")\n        op_ctx = obj_network.createNode(\"subnet\",\n                                        node_name=\"OpenPypeContext\",\n                                        run_init_scripts=False,\n                                        load_contents=False)\n\n        op_ctx.moveToGoodPosition()\n        op_ctx.setBuiltExplicitly(False)\n        op_ctx.setCreatorState(\"OpenPype\")\n        op_ctx.setComment(\"OpenPype node to hold context metadata\")\n        op_ctx.setColor(hou.Color((0.081, 0.798, 0.810)))\n        op_ctx.setDisplayFlag(False)\n        op_ctx.hide(True)\n        return op_ctx\n\n    def update_context_data(self, data, changes):\n        op_ctx = hou.node(CONTEXT_CONTAINER)\n        if not op_ctx:\n            op_ctx = self.create_context_node()\n\n        lib.imprint(op_ctx, data)\n\n    def get_context_data(self):\n        op_ctx = hou.node(CONTEXT_CONTAINER)\n        if not op_ctx:\n            op_ctx = self.create_context_node()\n        return lib.read(op_ctx)\n\n    def save_file(self, dst_path=None):\n        # Force forwards slashes to avoid segfault\n        dst_path = dst_path.replace(\"\\\\\", \"/\")\n\n        hou.hipFile.save(file_name=dst_path,\n                         save_to_recent_files=True)\n\n\ndef on_file_event_callback(event):\n    if event == hou.hipFileEventType.AfterLoad:\n        emit_event(\"open\")\n    elif event == hou.hipFileEventType.AfterSave:\n        emit_event(\"save\")\n    elif event == hou.hipFileEventType.BeforeSave:\n        emit_event(\"before.save\")\n    elif event == hou.hipFileEventType.AfterClear:\n        emit_event(\"new\")\n\n\ndef containerise(name,\n                 namespace,\n                 nodes,\n                 context,\n                 loader=None,\n                 suffix=\"\"):\n    \"\"\"Bundle `nodes` into a subnet and imprint it with metadata\n\n    Containerisation enables a tracking of version, author and origin\n    for loaded assets.\n\n    Arguments:\n        name (str): Name of resulting assembly\n        namespace (str): Namespace under which to host container\n        nodes (list): Long names of nodes to containerise\n        context (dict): Asset information\n        loader (str, optional): Name of loader used to produce this container.\n        suffix (str, optional): Suffix of container, defaults to `_CON`.\n\n    Returns:\n        container (str): Name of container assembly\n\n    \"\"\"\n\n    # Ensure AVALON_CONTAINERS subnet exists\n    subnet = hou.node(AVALON_CONTAINERS)\n    if subnet is None:\n        obj_network = hou.node(\"/obj\")\n        subnet = obj_network.createNode(\"subnet\",\n                                        node_name=\"AVALON_CONTAINERS\")\n\n    # Create proper container name\n    container_name = \"{}_{}\".format(name, suffix or \"CON\")\n    container = hou.node(\"/obj/{}\".format(name))\n    container.setName(container_name, unique_name=True)\n\n    data = {\n        \"schema\": \"openpype:container-2.0\",\n        \"id\": AVALON_CONTAINER_ID,\n        \"name\": name,\n        \"namespace\": namespace,\n        \"loader\": str(loader),\n        \"representation\": str(context[\"representation\"][\"_id\"]),\n    }\n\n    lib.imprint(container, data)\n\n    # \"Parent\" the container under the container network\n    hou.moveNodesTo([container], subnet)\n\n    subnet.node(container_name).moveToGoodPosition()\n\n    return container\n\n\ndef parse_container(container):\n    \"\"\"Return the container node's full container data.\n\n    Args:\n        container (hou.Node): A container node name.\n\n    Returns:\n        dict: The container schema data for this container node.\n\n    \"\"\"\n    data = lib.read(container)\n\n    # Backwards compatibility pre-schemas for containers\n    data[\"schema\"] = data.get(\"schema\", \"openpype:container-1.0\")\n\n    # Append transient data\n    data[\"objectName\"] = container.path()\n    data[\"node\"] = container\n\n    return data\n\n\ndef ls():\n    containers = []\n    for identifier in (AVALON_CONTAINER_ID,\n                       \"pyblish.mindbender.container\"):\n        containers += lib.lsattr(\"id\", identifier)\n\n    for container in sorted(containers,\n                            # Hou 19+ Python 3 hou.ObjNode are not\n                            # sortable due to not supporting greater\n                            # than comparisons\n                            key=lambda node: node.path()):\n        yield parse_container(container)\n\n\ndef before_save():\n    return lib.validate_fps()\n\n\ndef on_save():\n\n    log.info(\"Running callback on save..\")\n\n    # update houdini vars\n    lib.update_houdini_vars_context_dialog()\n\n    nodes = lib.get_id_required_nodes()\n    for node, new_id in lib.generate_ids(nodes):\n        lib.set_id(node, new_id, overwrite=False)\n\n\ndef _show_outdated_content_popup():\n    # Get main window\n    parent = lib.get_main_window()\n    if parent is None:\n        log.info(\"Skipping outdated content pop-up \"\n                 \"because Houdini window can't be found.\")\n    else:\n        from openpype.widgets import popup\n\n        # Show outdated pop-up\n        def _on_show_inventory():\n            from openpype.tools.utils import host_tools\n            host_tools.show_scene_inventory(parent=parent)\n\n        dialog = popup.Popup(parent=parent)\n        dialog.setWindowTitle(\"Houdini scene has outdated content\")\n        dialog.setMessage(\"There are outdated containers in \"\n                          \"your Houdini scene.\")\n        dialog.on_clicked.connect(_on_show_inventory)\n        dialog.show()\n\n\ndef on_open():\n\n    if not hou.isUIAvailable():\n        log.debug(\"Batch mode detected, ignoring `on_open` callbacks..\")\n        return\n\n    log.info(\"Running callback on open..\")\n\n    # update houdini vars\n    lib.update_houdini_vars_context_dialog()\n\n    # Validate FPS after update_task_from_path to\n    # ensure it is using correct FPS for the asset\n    lib.validate_fps()\n\n    if any_outdated_containers():\n        parent = lib.get_main_window()\n        if parent is None:\n            # When opening Houdini with last workfile on launch the UI hasn't\n            # initialized yet completely when the `on_open` callback triggers.\n            # We defer the dialog popup to wait for the UI to become available.\n            # We assume it will open because `hou.isUIAvailable()` returns True\n            import hdefereval\n            hdefereval.executeDeferred(_show_outdated_content_popup)\n        else:\n            _show_outdated_content_popup()\n\n        log.warning(\"Scene has outdated content.\")\n\n\ndef on_new():\n    \"\"\"Set project resolution and fps when create a new file\"\"\"\n\n    if hou.hipFile.isLoadingHipFile():\n        # This event also triggers when Houdini opens a file due to the\n        # new event being registered to 'afterClear'. As such we can skip\n        # 'new' logic if the user is opening a file anyway\n        log.debug(\"Skipping on new callback due to scene being opened.\")\n        return\n\n    log.info(\"Running callback on new..\")\n    _set_context_settings()\n\n    # It seems that the current frame always gets reset to frame 1 on\n    # new scene. So we enforce current frame to be at the start of the playbar\n    # with execute deferred\n    def _enforce_start_frame():\n        start = hou.playbar.playbackRange()[0]\n        hou.setFrame(start)\n\n    if hou.isUIAvailable():\n        import hdefereval\n        hdefereval.executeDeferred(_enforce_start_frame)\n    else:\n        # Run without execute deferred when no UI is available because\n        # without UI `hdefereval` is not available to import\n        _enforce_start_frame()\n\n\ndef _set_context_settings():\n    \"\"\"Apply the project settings from the project definition\n\n    Settings can be overwritten by an asset if the asset.data contains\n    any information regarding those settings.\n\n    Examples of settings:\n        fps\n        resolution\n        renderer\n\n    Returns:\n        None\n    \"\"\"\n\n    lib.reset_framerange()\n    lib.update_houdini_vars_context()\n"
  },
  {
    "path": "openpype/hosts/houdini/api/plugin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Houdini specific Avalon/Pyblish plugin definitions.\"\"\"\nimport sys\nfrom abc import (\n    ABCMeta\n)\nimport six\nimport hou\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import (\n    CreatorError,\n    LegacyCreator,\n    Creator as NewCreator,\n    CreatedInstance\n)\nfrom openpype.lib import BoolDef\nfrom .lib import imprint, read, lsattr, add_self_publish_button\n\n\nclass OpenPypeCreatorError(CreatorError):\n    pass\n\n\nclass Creator(LegacyCreator):\n    \"\"\"Creator plugin to create instances in Houdini\n\n    To support the wide range of node types for render output (Alembic, VDB,\n    Mantra) the Creator needs a node type to create the correct instance\n\n    By default, if none is given, is `geometry`. An example of accepted node\n    types: geometry, alembic, ifd (mantra)\n\n    Please check the Houdini documentation for more node types.\n\n    Tip: to find the exact node type to create press the `i` left of the node\n    when hovering over a node. The information is visible under the name of\n    the node.\n\n    Deprecated:\n        This creator is deprecated and will be removed in future version.\n\n    \"\"\"\n    defaults = ['Main']\n\n    def __init__(self, *args, **kwargs):\n        super(Creator, self).__init__(*args, **kwargs)\n        self.nodes = []\n\n    def process(self):\n        \"\"\"This is the base functionality to create instances in Houdini\n\n        The selected nodes are stored in self to be used in an override method.\n        This is currently necessary in order to support the multiple output\n        types in Houdini which can only be rendered through their own node.\n\n        Default node type if none is given is `geometry`\n\n        It also makes it easier to apply custom settings per instance type\n\n        Example of override method for Alembic:\n\n            def process(self):\n                instance =  super(CreateEpicNode, self, process()\n                # Set parameters for Alembic node\n                instance.setParms(\n                    {\"sop_path\": \"$HIP/%s.abc\" % self.nodes[0]}\n                )\n\n        Returns:\n            hou.Node\n\n        \"\"\"\n        try:\n            if (self.options or {}).get(\"useSelection\"):\n                self.nodes = hou.selectedNodes()\n\n            # Get the node type and remove it from the data, not needed\n            node_type = self.data.pop(\"node_type\", None)\n            if node_type is None:\n                node_type = \"geometry\"\n\n            # Get out node\n            out = hou.node(\"/out\")\n            instance = out.createNode(node_type, node_name=self.name)\n            instance.moveToGoodPosition()\n\n            imprint(instance, self.data)\n\n            self._process(instance)\n\n        except hou.Error as er:\n            six.reraise(\n                OpenPypeCreatorError,\n                OpenPypeCreatorError(\"Creator error: {}\".format(er)),\n                sys.exc_info()[2])\n\n\nclass HoudiniCreatorBase(object):\n    @staticmethod\n    def cache_subsets(shared_data):\n        \"\"\"Cache instances for Creators to shared data.\n\n        Create `houdini_cached_subsets` key when needed in shared data and\n        fill it with all collected instances from the scene under its\n        respective creator identifiers.\n\n        Create `houdini_cached_legacy_subsets` key for any legacy instances\n        detected in the scene as instances per family.\n\n        Args:\n            Dict[str, Any]: Shared data.\n\n        Return:\n            Dict[str, Any]: Shared data dictionary.\n\n        \"\"\"\n        if shared_data.get(\"houdini_cached_subsets\") is None:\n            cache = dict()\n            cache_legacy = dict()\n\n            for node in lsattr(\"id\", \"pyblish.avalon.instance\"):\n\n                creator_identifier_parm = node.parm(\"creator_identifier\")\n                if creator_identifier_parm:\n                    # creator instance\n                    creator_id = creator_identifier_parm.eval()\n                    cache.setdefault(creator_id, []).append(node)\n\n                else:\n                    # legacy instance\n                    family_parm = node.parm(\"family\")\n                    if not family_parm:\n                        # must be a broken instance\n                        continue\n\n                    family = family_parm.eval()\n                    cache_legacy.setdefault(family, []).append(node)\n\n            shared_data[\"houdini_cached_subsets\"] = cache\n            shared_data[\"houdini_cached_legacy_subsets\"] = cache_legacy\n\n        return shared_data\n\n    @staticmethod\n    def create_instance_node(\n        asset_name, node_name, parent, node_type=\"geometry\"\n    ):\n        # type: (str, str, str) -> hou.Node\n        \"\"\"Create node representing instance.\n\n        Arguments:\n            asset_name (str): Asset name.\n            node_name (str): Name of the new node.\n            parent (str): Name of the parent node.\n            node_type (str, optional): Type of the node.\n\n        Returns:\n            hou.Node: Newly created instance node.\n\n        \"\"\"\n        parent_node = hou.node(parent)\n        instance_node = parent_node.createNode(\n            node_type, node_name=node_name)\n        instance_node.moveToGoodPosition()\n        return instance_node\n\n\n@six.add_metaclass(ABCMeta)\nclass HoudiniCreator(NewCreator, HoudiniCreatorBase):\n    \"\"\"Base class for most of the Houdini creator plugins.\"\"\"\n    selected_nodes = []\n    settings_name = None\n    add_publish_button = False\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        try:\n            self.selected_nodes = []\n\n            if pre_create_data.get(\"use_selection\"):\n                self.selected_nodes = hou.selectedNodes()\n\n            # Get the node type and remove it from the data, not needed\n            node_type = instance_data.pop(\"node_type\", None)\n            if node_type is None:\n                node_type = \"geometry\"\n\n            if AYON_SERVER_ENABLED:\n                asset_name = instance_data[\"folderPath\"]\n            else:\n                asset_name = instance_data[\"asset\"]\n\n            instance_node = self.create_instance_node(\n                asset_name, subset_name, \"/out\", node_type)\n\n            self.customize_node_look(instance_node)\n\n            instance_data[\"instance_node\"] = instance_node.path()\n            instance_data[\"instance_id\"] = instance_node.path()\n            instance = CreatedInstance(\n                self.family,\n                subset_name,\n                instance_data,\n                self)\n            self._add_instance_to_context(instance)\n            self.imprint(instance_node, instance.data_to_store())\n\n            if self.add_publish_button:\n                add_self_publish_button(instance_node)\n\n            return instance\n\n        except hou.Error as er:\n            six.reraise(\n                OpenPypeCreatorError,\n                OpenPypeCreatorError(\"Creator error: {}\".format(er)),\n                sys.exc_info()[2])\n\n    def lock_parameters(self, node, parameters):\n        \"\"\"Lock list of specified parameters on the node.\n\n        Args:\n            node (hou.Node): Houdini node to lock parameters on.\n            parameters (list of str): List of parameter names.\n\n        \"\"\"\n        for name in parameters:\n            try:\n                parm = node.parm(name)\n                parm.lock(True)\n            except AttributeError:\n                self.log.debug(\"missing lock pattern {}\".format(name))\n\n    def collect_instances(self):\n        # cache instances  if missing\n        self.cache_subsets(self.collection_shared_data)\n        for instance in self.collection_shared_data[\n                \"houdini_cached_subsets\"].get(self.identifier, []):\n\n            node_data = read(instance)\n\n            # Node paths are always the full node path since that is unique\n            # Because it's the node's path it's not written into attributes\n            # but explicitly collected\n            node_path = instance.path()\n            node_data[\"instance_id\"] = node_path\n            node_data[\"instance_node\"] = node_path\n\n            created_instance = CreatedInstance.from_existing(\n                node_data, self\n            )\n            self._add_instance_to_context(created_instance)\n\n    def update_instances(self, update_list):\n        for created_inst, changes in update_list:\n            instance_node = hou.node(created_inst.get(\"instance_node\"))\n            new_values = {\n                key: changes[key].new_value\n                for key in changes.changed_keys\n            }\n            # Update parm templates and values\n            self.imprint(\n                instance_node,\n                new_values,\n                update=True\n            )\n\n    def imprint(self, node, values, update=False):\n        # Never store instance node and instance id since that data comes\n        # from the node's path\n        values.pop(\"instance_node\", None)\n        values.pop(\"instance_id\", None)\n        imprint(node, values, update=update)\n\n    def remove_instances(self, instances):\n        \"\"\"Remove specified instance from the scene.\n\n        This is only removing `id` parameter so instance is no longer\n        instance, because it might contain valuable data for artist.\n\n        \"\"\"\n        for instance in instances:\n            instance_node = hou.node(instance.data.get(\"instance_node\"))\n            if instance_node:\n                instance_node.destroy()\n\n            self._remove_instance_from_context(instance)\n\n    def get_pre_create_attr_defs(self):\n        return [\n            BoolDef(\"use_selection\", label=\"Use selection\")\n        ]\n\n    @staticmethod\n    def customize_node_look(\n            node, color=None,\n            shape=\"chevron_down\"):\n        \"\"\"Set custom look for instance nodes.\n\n        Args:\n            node (hou.Node): Node to set look.\n            color (hou.Color, Optional): Color of the node.\n            shape (str, Optional): Shape name of the node.\n\n        Returns:\n            None\n\n        \"\"\"\n        if not color:\n            color = hou.Color((0.616, 0.871, 0.769))\n        node.setUserData('nodeshape', shape)\n        node.setColor(color)\n\n    def get_network_categories(self):\n        \"\"\"Return in which network view type this creator should show.\n\n        The node type categories returned here will be used to define where\n        the creator will show up in the TAB search for nodes in Houdini's\n        Network View.\n\n        This can be overridden in inherited classes to define where that\n        particular Creator should be visible in the TAB search.\n\n        Returns:\n            list: List of houdini node type categories\n\n        \"\"\"\n        return [hou.ropNodeTypeCategory()]\n\n    def apply_settings(self, project_settings):\n        \"\"\"Method called on initialization of plugin to apply settings.\"\"\"\n\n        # Apply General Settings\n        houdini_general_settings = project_settings[\"houdini\"][\"general\"]\n        self.add_publish_button = houdini_general_settings.get(\n            \"add_self_publish_button\", False)\n\n        # Apply Creator Settings\n        settings_name = self.settings_name\n        if settings_name is None:\n            settings_name = self.__class__.__name__\n\n        settings = project_settings[\"houdini\"][\"create\"]\n        settings = settings.get(settings_name)\n        if settings is None:\n            self.log.debug(\n                \"No settings found for {}\".format(self.__class__.__name__)\n            )\n            return\n\n        for key, value in settings.items():\n            setattr(self, key, value)\n"
  },
  {
    "path": "openpype/hosts/houdini/api/shelves.py",
    "content": "import os\nimport re\nimport logging\nimport platform\n\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import get_current_project_name\n\nfrom openpype.lib import StringTemplate\n\nimport hou\n\nfrom .lib import get_current_context_template_data_with_asset_data\n\nlog = logging.getLogger(\"openpype.hosts.houdini.shelves\")\n\n\ndef generate_shelves():\n    \"\"\"This function generates complete shelves from shelf set to tools\n    in Houdini from openpype project settings houdini shelf definition.\n    \"\"\"\n    current_os = platform.system().lower()\n\n    # load configuration of houdini shelves\n    project_name = get_current_project_name()\n    project_settings = get_project_settings(project_name)\n    shelves_configs = project_settings[\"houdini\"][\"shelves\"]\n\n    if not shelves_configs:\n        log.debug(\"No custom shelves found in project settings.\")\n        return\n\n    # Get Template data\n    template_data = get_current_context_template_data_with_asset_data()\n\n    for config in shelves_configs:\n        selected_option = config[\"options\"]\n        shelf_set_config = config[selected_option]\n\n        shelf_set_filepath = shelf_set_config.get('shelf_set_source_path')\n        if shelf_set_filepath:\n            shelf_set_os_filepath = shelf_set_filepath[current_os]\n            if shelf_set_os_filepath:\n                shelf_set_os_filepath = get_path_using_template_data(\n                    shelf_set_os_filepath, template_data\n                )\n                if not os.path.isfile(shelf_set_os_filepath):\n                    log.error(\"Shelf path doesn't exist - \"\n                              \"{}\".format(shelf_set_os_filepath))\n                    continue\n\n                hou.shelves.loadFile(shelf_set_os_filepath)\n                continue\n\n        shelf_set_name = shelf_set_config.get('shelf_set_name')\n        if not shelf_set_name:\n            log.warning(\"No name found in shelf set definition.\")\n            continue\n\n        shelves_definition = shelf_set_config.get('shelf_definition')\n        if not shelves_definition:\n            log.debug(\n                \"No shelf definition found for shelf set named '{}'\".format(\n                    shelf_set_name\n                )\n            )\n            continue\n\n        shelf_set = get_or_create_shelf_set(shelf_set_name)\n        for shelf_definition in shelves_definition:\n            shelf_name = shelf_definition.get('shelf_name')\n            if not shelf_name:\n                log.warning(\"No name found in shelf definition.\")\n                continue\n\n            shelf = get_or_create_shelf(shelf_name)\n\n            if not shelf_definition.get('tools_list'):\n                log.debug(\n                    \"No tool definition found for shelf named {}\".format(\n                        shelf_name\n                    )\n                )\n                continue\n\n            mandatory_attributes = {'label', 'script'}\n            for tool_definition in shelf_definition.get('tools_list'):\n                # We verify that the name and script attributes of the tool\n                # are set\n                if not all(\n                    tool_definition[key] for key in mandatory_attributes\n                ):\n                    log.warning(\n                        \"You need to specify at least the name and the \"\n                        \"script path of the tool.\")\n                    continue\n\n                tool = get_or_create_tool(\n                    tool_definition, shelf, template_data\n                )\n\n                if not tool:\n                    continue\n\n                # Add the tool to the shelf if not already in it\n                if tool not in shelf.tools():\n                    shelf.setTools(list(shelf.tools()) + [tool])\n\n            # Add the shelf in the shelf set if not already in it\n            if shelf not in shelf_set.shelves():\n                shelf_set.setShelves(shelf_set.shelves() + (shelf,))\n\n\ndef get_or_create_shelf_set(shelf_set_label):\n    \"\"\"This function verifies if the shelf set label exists. If not,\n    creates a new shelf set.\n\n    Arguments:\n        shelf_set_label (str): The label of the shelf set\n\n    Returns:\n        hou.ShelfSet: The shelf set existing or the new one\n    \"\"\"\n    all_shelves_sets = hou.shelves.shelfSets().values()\n\n    shelf_set = next((shelf for shelf in all_shelves_sets if\n                      shelf.label() == shelf_set_label), None)\n    if shelf_set:\n        return shelf_set\n\n    shelf_set_name = shelf_set_label.replace(' ', '_').lower()\n    new_shelf_set = hou.shelves.newShelfSet(\n        name=shelf_set_name,\n        label=shelf_set_label\n    )\n    return new_shelf_set\n\n\ndef get_or_create_shelf(shelf_label):\n    \"\"\"This function verifies if the shelf label exists. If not, creates\n    a new shelf.\n\n    Arguments:\n        shelf_label (str): The label of the shelf\n\n    Returns:\n        hou.Shelf: The shelf existing or the new one\n    \"\"\"\n    all_shelves = hou.shelves.shelves().values()\n\n    shelf = next((s for s in all_shelves if s.label() == shelf_label), None)\n    if shelf:\n        return shelf\n\n    shelf_name = shelf_label.replace(' ', '_').lower()\n    new_shelf = hou.shelves.newShelf(\n        name=shelf_name,\n        label=shelf_label\n    )\n    return new_shelf\n\n\ndef get_or_create_tool(tool_definition, shelf, template_data):\n    \"\"\"This function verifies if the tool exists and updates it. If not, creates\n    a new one.\n\n    Arguments:\n        tool_definition (dict): Dict with label, script, icon and help\n        shelf (hou.Shelf): The parent shelf of the tool\n\n    Returns:\n        hou.Tool: The tool updated or the new one\n    \"\"\"\n\n    tool_label = tool_definition.get(\"label\")\n    if not tool_label:\n        log.warning(\"Skipped shelf without label\")\n        return\n\n    script_path = tool_definition[\"script\"]\n    script_path = get_path_using_template_data(script_path, template_data)\n    if not script_path or not os.path.exists(script_path):\n        log.warning(\"This path doesn't exist - {}\".format(script_path))\n        return\n\n    icon_path = tool_definition[\"icon\"]\n    if icon_path:\n        icon_path = get_path_using_template_data(icon_path, template_data)\n        tool_definition[\"icon\"] = icon_path\n\n    existing_tools = shelf.tools()\n    existing_tool = next(\n        (tool for tool in existing_tools if tool.label() == tool_label),\n        None\n    )\n\n    with open(script_path) as stream:\n        script = stream.read()\n\n    tool_definition[\"script\"] = script\n\n    if existing_tool:\n        tool_definition.pop(\"label\", None)\n        existing_tool.setData(**tool_definition)\n        return existing_tool\n\n    tool_name = re.sub(r\"[^\\w\\d]+\", \"_\", tool_label).lower()\n    return hou.shelves.newTool(name=tool_name, **tool_definition)\n\n\ndef get_path_using_template_data(path, template_data):\n    path = StringTemplate.format_template(path, template_data)\n    path = path.replace(\"\\\\\", \"/\")\n\n    return path\n"
  },
  {
    "path": "openpype/hosts/houdini/api/usd.py",
    "content": "\"\"\"Houdini-specific USD Library functions.\"\"\"\n\nimport contextlib\nimport logging\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import style\nfrom openpype.client import get_asset_by_name\nfrom openpype.pipeline import legacy_io\nfrom openpype.tools.utils.assets_widget import SingleSelectAssetsWidget\n\nfrom pxr import Sdf\n\n\nlog = logging.getLogger(__name__)\n\n\nclass SelectAssetDialog(QtWidgets.QWidget):\n    \"\"\"Frameless assets dialog to select asset with double click.\n\n    Args:\n        parm: Parameter where selected asset name is set.\n    \"\"\"\n\n    def __init__(self, parm):\n        self.setWindowTitle(\"Pick Asset\")\n        self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Popup)\n\n        assets_widget = SingleSelectAssetsWidget(legacy_io, parent=self)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.addWidget(assets_widget)\n\n        assets_widget.double_clicked.connect(self._set_parameter)\n        self._assets_widget = assets_widget\n        self._parm = parm\n\n    def _set_parameter(self):\n        name = self._assets_widget.get_selected_asset_name()\n        self._parm.set(name)\n        self.close()\n\n    def _on_show(self):\n        pos = QtGui.QCursor.pos()\n        # Select the current asset if there is any\n        select_id = None\n        name = self._parm.eval()\n        if name:\n            project_name = legacy_io.active_project()\n            db_asset = get_asset_by_name(project_name, name, fields=[\"_id\"])\n            if db_asset:\n                select_id = db_asset[\"_id\"]\n\n        # Set stylesheet\n        self.setStyleSheet(style.load_stylesheet())\n        # Refresh assets (is threaded)\n        self._assets_widget.refresh()\n        # Select asset - must be done after refresh\n        if select_id is not None:\n            self._assets_widget.select_asset(select_id)\n\n        # Show cursor (top right of window) near cursor\n        self.resize(250, 400)\n        self.move(self.mapFromGlobal(pos) - QtCore.QPoint(self.width(), 0))\n\n    def showEvent(self, event):\n        super(SelectAssetDialog, self).showEvent(event)\n        self._on_show()\n\n\ndef pick_asset(node):\n    \"\"\"Show a user interface to select an Asset in the project\n\n    When double clicking an asset it will set the Asset value in the\n    'asset' parameter.\n\n    \"\"\"\n\n    parm = node.parm(\"asset_name\")\n    if not parm:\n        log.error(\"Node has no 'asset' parameter: %s\", node)\n        return\n\n    # Construct a frameless popup so it automatically\n    # closes when clicked outside of it.\n    global tool\n    tool = SelectAssetDialog(parm)\n    tool.show()\n\n\ndef add_usd_output_processor(ropnode, processor):\n    \"\"\"Add USD Output Processor to USD Rop node.\n\n    Args:\n        ropnode (hou.RopNode): The USD Rop node.\n        processor (str): The output processor name. This is the basename of\n            the python file that contains the Houdini USD Output Processor.\n\n    \"\"\"\n\n    import loputils\n\n    loputils.handleOutputProcessorAdd(\n        {\n            \"node\": ropnode,\n            \"parm\": ropnode.parm(\"outputprocessors\"),\n            \"script_value\": processor,\n        }\n    )\n\n\ndef remove_usd_output_processor(ropnode, processor):\n    \"\"\"Removes USD Output Processor from USD Rop node.\n\n    Args:\n        ropnode (hou.RopNode): The USD Rop node.\n        processor (str): The output processor name. This is the basename of\n            the python file that contains the Houdini USD Output Processor.\n\n    \"\"\"\n    import loputils\n\n    parm = ropnode.parm(processor + \"_remove\")\n    if not parm:\n        raise RuntimeError(\n            \"Output Processor %s does not \"\n            \"exist on %s\" % (processor, ropnode.name())\n        )\n\n    loputils.handleOutputProcessorRemove({\"node\": ropnode, \"parm\": parm})\n\n\n@contextlib.contextmanager\ndef outputprocessors(ropnode, processors=tuple(), disable_all_others=True):\n    \"\"\"Context manager to temporarily add Output Processors to USD ROP node.\n\n    Args:\n        ropnode (hou.RopNode): The USD Rop node.\n        processors (tuple or list): The processors to add.\n        disable_all_others (bool, Optional): Whether to disable all\n            output processors currently on the ROP node that are not in the\n            `processors` list passed to this function.\n\n    \"\"\"\n    # TODO: Add support for forcing the correct Order of the processors\n\n    original = []\n    prefix = \"enableoutputprocessor_\"\n    processor_parms = ropnode.globParms(prefix + \"*\")\n    for parm in processor_parms:\n        original.append((parm, parm.eval()))\n\n    if disable_all_others:\n        for parm in processor_parms:\n            parm.set(False)\n\n    added = []\n    for processor in processors:\n\n        parm = ropnode.parm(prefix + processor)\n        if parm:\n            # If processor already exists, just enable it\n            parm.set(True)\n\n        else:\n            # Else add the new processor\n            add_usd_output_processor(ropnode, processor)\n            added.append(processor)\n\n    try:\n        yield\n    finally:\n\n        # Remove newly added processors\n        for processor in added:\n            remove_usd_output_processor(ropnode, processor)\n\n        # Revert to original values\n        for parm, value in original:\n            if parm:\n                parm.set(value)\n\n\ndef get_usd_rop_loppath(node):\n\n    # Get sop path\n    node_type = node.type().name()\n    if node_type == \"usd\":\n        return node.parm(\"loppath\").evalAsNode()\n\n    elif node_type in {\"usd_rop\", \"usdrender_rop\"}:\n        # Inside Solaris e.g. /stage (not in ROP context)\n        # When incoming connection is present it takes it directly\n        inputs = node.inputs()\n        if inputs:\n            return inputs[0]\n        else:\n            return node.parm(\"loppath\").evalAsNode()\n\n\ndef get_layer_save_path(layer):\n    \"\"\"Get custom HoudiniLayerInfo->HoudiniSavePath from SdfLayer.\n\n    Args:\n        layer (pxr.Sdf.Layer): The Layer to retrieve the save pah data from.\n\n    Returns:\n        str or None: Path to save to when data exists.\n\n    \"\"\"\n    hou_layer_info = layer.rootPrims.get(\"HoudiniLayerInfo\")\n    if not hou_layer_info:\n        return\n\n    save_path = hou_layer_info.customData.get(\"HoudiniSavePath\", None)\n    if save_path:\n        # Unfortunately this doesn't actually resolve the full absolute path\n        return layer.ComputeAbsolutePath(save_path)\n\n\ndef get_referenced_layers(layer):\n    \"\"\"Return SdfLayers for all external references of the current layer\n\n    Args:\n        layer (pxr.Sdf.Layer): The Layer to retrieve the save pah data from.\n\n    Returns:\n        list: List of pxr.Sdf.Layer that are external references to this layer\n\n    \"\"\"\n\n    layers = []\n    for layer_id in layer.GetExternalReferences():\n        layer = Sdf.Layer.Find(layer_id)\n        if not layer:\n            # A file may not be in memory and is\n            # referenced from disk. As such it cannot\n            # be found. We will ignore those layers.\n            continue\n\n        layers.append(layer)\n\n    return layers\n\n\ndef iter_layer_recursive(layer):\n    \"\"\"Recursively iterate all 'external' referenced layers\"\"\"\n\n    layers = get_referenced_layers(layer)\n    traversed = set(layers)  # Avoid recursion to itself (if even possible)\n    traverse = list(layers)\n    for layer in traverse:\n\n        # Include children layers (recursion)\n        children_layers = get_referenced_layers(layer)\n        children_layers = [x for x in children_layers if x not in traversed]\n        traverse.extend(children_layers)\n        traversed.update(children_layers)\n\n        yield layer\n\n\ndef get_configured_save_layers(usd_rop):\n\n    lop_node = get_usd_rop_loppath(usd_rop)\n    stage = lop_node.stage(apply_viewport_overrides=False)\n    if not stage:\n        raise RuntimeError(\n            \"No valid USD stage for ROP node: \" \"%s\" % usd_rop.path()\n        )\n\n    root_layer = stage.GetRootLayer()\n\n    save_layers = []\n    for layer in iter_layer_recursive(root_layer):\n        save_path = get_layer_save_path(layer)\n        if save_path is not None:\n            save_layers.append(layer)\n\n    return save_layers\n"
  },
  {
    "path": "openpype/hosts/houdini/hooks/set_paths.py",
    "content": "from openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass SetPath(PreLaunchHook):\n    \"\"\"Set current dir to workdir.\n\n    Hook `GlobalHostDataHook` must be executed before this hook.\n    \"\"\"\n    app_groups = {\"houdini\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        workdir = self.launch_context.env.get(\"AVALON_WORKDIR\", \"\")\n        if not workdir:\n            self.log.warning(\"BUG: Workdir is not filled.\")\n            return\n\n        self.launch_context.kwargs[\"cwd\"] = workdir\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/convert_legacy.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Converter for legacy Houdini subsets.\"\"\"\nfrom openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin\nfrom openpype.hosts.houdini.api.lib import imprint\n\n\nclass HoudiniLegacyConvertor(SubsetConvertorPlugin):\n    \"\"\"Find and convert any legacy subsets in the scene.\n\n    This Converter will find all legacy subsets in the scene and will\n    transform them to the current system. Since the old subsets doesn't\n    retain any information about their original creators, the only mapping\n    we can do is based on their families.\n\n    Its limitation is that you can have multiple creators creating subset\n    of the same family and there is no way to handle it. This code should\n    nevertheless cover all creators that came with OpenPype.\n\n    \"\"\"\n    identifier = \"io.openpype.creators.houdini.legacy\"\n    family_to_id = {\n        \"camera\": \"io.openpype.creators.houdini.camera\",\n        \"ass\": \"io.openpype.creators.houdini.ass\",\n        \"imagesequence\": \"io.openpype.creators.houdini.imagesequence\",\n        \"hda\": \"io.openpype.creators.houdini.hda\",\n        \"pointcache\": \"io.openpype.creators.houdini.pointcache\",\n        \"redshiftproxy\": \"io.openpype.creators.houdini.redshiftproxy\",\n        \"redshift_rop\": \"io.openpype.creators.houdini.redshift_rop\",\n        \"usd\": \"io.openpype.creators.houdini.usd\",\n        \"usdrender\": \"io.openpype.creators.houdini.usdrender\",\n        \"vdbcache\": \"io.openpype.creators.houdini.vdbcache\"\n    }\n\n    def __init__(self, *args, **kwargs):\n        super(HoudiniLegacyConvertor, self).__init__(*args, **kwargs)\n        self.legacy_subsets = {}\n\n    def find_instances(self):\n        \"\"\"Find legacy subsets in the scene.\n\n        Legacy subsets are the ones that doesn't have `creator_identifier`\n        parameter on them.\n\n        This is using cached entries done in\n        :py:meth:`~HoudiniCreatorBase.cache_subsets()`\n\n        \"\"\"\n        self.legacy_subsets = self.collection_shared_data.get(\n            \"houdini_cached_legacy_subsets\")\n        if not self.legacy_subsets:\n            return\n        self.add_convertor_item(\"Found {} incompatible subset{}.\".format(\n            len(self.legacy_subsets), \"s\" if len(self.legacy_subsets) > 1 else \"\")\n        )\n\n    def convert(self):\n        \"\"\"Convert all legacy subsets to current.\n\n        It is enough to add `creator_identifier` and `instance_node`.\n\n        \"\"\"\n        if not self.legacy_subsets:\n            return\n\n        for family, subsets in self.legacy_subsets.items():\n            if family in self.family_to_id:\n                for subset in subsets:\n                    data = {\n                        \"creator_identifier\": self.family_to_id[family],\n                        \"instance_node\": subset.path()\n                    }\n                    if family == \"pointcache\":\n                        data[\"families\"] = [\"abc\"]\n                    self.log.info(\"Converting {} to {}\".format(\n                        subset.path(), self.family_to_id[family]))\n                    imprint(subset, data)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_alembic_camera.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating alembic camera subsets.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.pipeline import CreatedInstance, CreatorError\n\nimport hou\n\n\nclass CreateAlembicCamera(plugin.HoudiniCreator):\n    \"\"\"Single baked camera from Alembic ROP.\"\"\"\n\n    identifier = \"io.openpype.creators.houdini.camera\"\n    label = \"Camera (Abc)\"\n    family = \"camera\"\n    icon = \"camera\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        import hou\n\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"alembic\"})\n\n        instance = super(CreateAlembicCamera, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: CreatedInstance\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n        parms = {\n            \"filename\": hou.text.expandString(\n                \"$HIP/pyblish/{}.abc\".format(subset_name)),\n            \"use_sop_path\": False,\n        }\n\n        if self.selected_nodes:\n            if len(self.selected_nodes) > 1:\n                raise CreatorError(\"More than one item selected.\")\n            path = self.selected_nodes[0].path()\n            # Split the node path into the first root and the remainder\n            # So we can set the root and objects parameters correctly\n            _, root, remainder = path.split(\"/\", 2)\n            parms.update({\"root\": \"/\" + root, \"objects\": remainder})\n\n        instance_node.setParms(parms)\n\n        # Lock the Use Sop Path setting so the\n        # user doesn't accidentally enable it.\n        to_lock = [\"use_sop_path\"]\n        self.lock_parameters(instance_node, to_lock)\n\n        instance_node.parm(\"trange\").set(1)\n\n    def get_network_categories(self):\n        return [\n            hou.ropNodeTypeCategory(),\n            hou.objNodeTypeCategory()\n        ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_arnold_ass.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating Arnold ASS files.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.lib import BoolDef\n\n\nclass CreateArnoldAss(plugin.HoudiniCreator):\n    \"\"\"Arnold .ass Archive\"\"\"\n\n    identifier = \"io.openpype.creators.houdini.ass\"\n    label = \"Arnold ASS\"\n    family = \"ass\"\n    icon = \"magic\"\n\n    # Default extension: `.ass` or `.ass.gz`\n    # however calling HoudiniCreator.create()\n    # will override it by the value in the project settings\n    ext = \".ass\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        import hou\n\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"arnold\"})\n        creator_attributes = instance_data.setdefault(\n            \"creator_attributes\", dict())\n        creator_attributes[\"farm\"] = pre_create_data[\"farm\"]\n\n        instance = super(CreateArnoldAss, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: plugin.CreatedInstance\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n        # Hide Properties Tab on Arnold ROP since that's used\n        # for rendering instead of .ass Archive Export\n        parm_template_group = instance_node.parmTemplateGroup()\n        parm_template_group.hideFolder(\"Properties\", True)\n        instance_node.setParmTemplateGroup(parm_template_group)\n\n        filepath = \"{}{}\".format(\n            hou.text.expandString(\"$HIP/pyblish/\"),\n            \"{}.$F4{}\".format(subset_name, self.ext)\n        )\n        parms = {\n            # Render frame range\n            \"trange\": 1,\n            # Arnold ROP settings\n            \"ar_ass_file\": filepath,\n            \"ar_ass_export_enable\": 1\n        }\n\n        instance_node.setParms(parms)\n\n        # Lock any parameters in this list\n        to_lock = [\"ar_ass_export_enable\", \"family\", \"id\"]\n        self.lock_parameters(instance_node, to_lock)\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\"farm\",\n                    label=\"Submitting to Farm\",\n                    default=False)\n        ]\n\n    def get_pre_create_attr_defs(self):\n        attrs = super().get_pre_create_attr_defs()\n        # Use same attributes as for instance attributes\n        return attrs + self.get_instance_attr_defs()\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_arnold_rop.py",
    "content": "from openpype.hosts.houdini.api import plugin\nfrom openpype.lib import EnumDef, BoolDef\n\n\nclass CreateArnoldRop(plugin.HoudiniCreator):\n    \"\"\"Arnold ROP\"\"\"\n\n    identifier = \"io.openpype.creators.houdini.arnold_rop\"\n    label = \"Arnold ROP\"\n    family = \"arnold_rop\"\n    icon = \"magic\"\n\n    # Default extension\n    ext = \"exr\"\n\n    # Default to split export and render jobs\n    export_job = True\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        import hou\n\n        # Remove the active, we are checking the bypass flag of the nodes\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"arnold\"})\n\n        # Add chunk size attribute\n        instance_data[\"chunkSize\"] = 1\n        # Submit for job publishing\n        instance_data[\"farm\"] = pre_create_data.get(\"farm\")\n\n        instance = super(CreateArnoldRop, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: plugin.CreatedInstance\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n        ext = pre_create_data.get(\"image_format\")\n\n        filepath = \"{renders_dir}{subset_name}/{subset_name}.$F4.{ext}\".format(\n            renders_dir=hou.text.expandString(\"$HIP/pyblish/renders/\"),\n            subset_name=subset_name,\n            ext=ext,\n        )\n        parms = {\n            # Render frame range\n            \"trange\": 1,\n\n            # Arnold ROP settings\n            \"ar_picture\": filepath,\n            \"ar_exr_half_precision\": 1           # half precision\n        }\n\n        if pre_create_data.get(\"export_job\"):\n            ass_filepath = \\\n                \"{export_dir}{subset_name}/{subset_name}.$F4.ass\".format(\n                    export_dir=hou.text.expandString(\"$HIP/pyblish/ass/\"),\n                    subset_name=subset_name,\n                )\n            parms[\"ar_ass_export_enable\"] = 1\n            parms[\"ar_ass_file\"] = ass_filepath\n\n        instance_node.setParms(parms)\n\n        # Lock any parameters in this list\n        to_lock = [\"family\", \"id\"]\n        self.lock_parameters(instance_node, to_lock)\n\n    def get_pre_create_attr_defs(self):\n        attrs = super(CreateArnoldRop, self).get_pre_create_attr_defs()\n\n        image_format_enum = [\n            \"bmp\", \"cin\", \"exr\", \"jpg\", \"pic\", \"pic.gz\", \"png\",\n            \"rad\", \"rat\", \"rta\", \"sgi\", \"tga\", \"tif\",\n        ]\n\n        return attrs + [\n            BoolDef(\"farm\",\n                    label=\"Submitting to Farm\",\n                    default=True),\n            BoolDef(\"export_job\",\n                    label=\"Split export and render jobs\",\n                    default=self.export_job),\n            EnumDef(\"image_format\",\n                    image_format_enum,\n                    default=self.ext,\n                    label=\"Image Format Options\")\n        ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_bgeo.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating pointcache bgeo files.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.pipeline import CreatedInstance, CreatorError\nimport hou\nfrom openpype.lib import EnumDef, BoolDef\n\n\nclass CreateBGEO(plugin.HoudiniCreator):\n    \"\"\"BGEO pointcache creator.\"\"\"\n    identifier = \"io.openpype.creators.houdini.bgeo\"\n    label = \"PointCache (Bgeo)\"\n    family = \"pointcache\"\n    icon = \"gears\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        instance_data.pop(\"active\", None)\n\n        instance_data.update({\"node_type\": \"geometry\"})\n        creator_attributes = instance_data.setdefault(\n            \"creator_attributes\", dict())\n        creator_attributes[\"farm\"] = pre_create_data[\"farm\"]\n\n        instance = super(CreateBGEO, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: CreatedInstance\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n        file_path = \"{}{}\".format(\n            hou.text.expandString(\"$HIP/pyblish/\"),\n            \"{}.$F4.{}\".format(\n                subset_name,\n                pre_create_data.get(\"bgeo_type\") or \"bgeo.sc\")\n        )\n        parms = {\n            \"sopoutput\": file_path\n        }\n\n        instance_node.parm(\"trange\").set(1)\n        if self.selected_nodes:\n            # if selection is on SOP level, use it\n            if isinstance(self.selected_nodes[0], hou.SopNode):\n                parms[\"soppath\"] = self.selected_nodes[0].path()\n            else:\n                # try to find output node with the lowest index\n                outputs = [\n                    child for child in self.selected_nodes[0].children()\n                    if child.type().name() == \"output\"\n                ]\n                if not outputs:\n                    instance_node.setParms(parms)\n                    raise CreatorError((\n                        \"Missing output node in SOP level for the selection. \"\n                        \"Please select correct SOP path in created instance.\"\n                    ))\n                outputs.sort(key=lambda output: output.evalParm(\"outputidx\"))\n                parms[\"soppath\"] = outputs[0].path()\n\n        instance_node.setParms(parms)\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\"farm\",\n                    label=\"Submitting to Farm\",\n                    default=False)\n        ]\n\n    def get_pre_create_attr_defs(self):\n        attrs = super().get_pre_create_attr_defs()\n        bgeo_enum = [\n            {\n                \"value\": \"bgeo\",\n                \"label\": \"uncompressed bgeo (.bgeo)\"\n            },\n            {\n                \"value\": \"bgeosc\",\n                \"label\": \"BLOSC compressed bgeo (.bgeosc)\"\n            },\n            {\n                \"value\": \"bgeo.sc\",\n                \"label\": \"BLOSC compressed bgeo (.bgeo.sc)\"\n            },\n            {\n                \"value\": \"bgeo.gz\",\n                \"label\": \"GZ compressed bgeo (.bgeo.gz)\"\n            },\n            {\n                \"value\": \"bgeo.lzma\",\n                \"label\": \"LZMA compressed bgeo (.bgeo.lzma)\"\n            },\n            {\n                \"value\": \"bgeo.bz2\",\n                \"label\": \"BZip2 compressed bgeo (.bgeo.bz2)\"\n            }\n        ]\n\n        return attrs + [\n            EnumDef(\"bgeo_type\", bgeo_enum, label=\"BGEO Options\"),\n        ] + self.get_instance_attr_defs()\n\n    def get_network_categories(self):\n        return [\n            hou.ropNodeTypeCategory(),\n            hou.sopNodeTypeCategory()\n        ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_composite.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating composite sequences.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.pipeline import CreatedInstance, CreatorError\n\nimport hou\n\n\nclass CreateCompositeSequence(plugin.HoudiniCreator):\n    \"\"\"Composite ROP to Image Sequence\"\"\"\n\n    identifier = \"io.openpype.creators.houdini.imagesequence\"\n    label = \"Composite (Image Sequence)\"\n    family = \"imagesequence\"\n    icon = \"gears\"\n\n    ext = \".exr\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        import hou  # noqa\n\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"comp\"})\n\n        instance = super(CreateCompositeSequence, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: CreatedInstance\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n        filepath = \"{}{}\".format(\n            hou.text.expandString(\"$HIP/pyblish/\"),\n            \"{}.$F4{}\".format(subset_name, self.ext)\n        )\n        parms = {\n            \"trange\": 1,\n            \"copoutput\": filepath\n        }\n\n        if self.selected_nodes:\n            if len(self.selected_nodes) > 1:\n                raise CreatorError(\"More than one item selected.\")\n            path = self.selected_nodes[0].path()\n            parms[\"coppath\"] = path\n\n        instance_node.setParms(parms)\n\n        # Manually set f1 & f2 to $FSTART and $FEND respectively\n        # to match other Houdini nodes default.\n        instance_node.parm(\"f1\").setExpression(\"$FSTART\")\n        instance_node.parm(\"f2\").setExpression(\"$FEND\")\n\n        # Lock any parameters in this list\n        to_lock = [\"prim_to_detail_pattern\"]\n        self.lock_parameters(instance_node, to_lock)\n\n    def get_network_categories(self):\n        return [\n            hou.ropNodeTypeCategory(),\n            hou.cop2NodeTypeCategory()\n        ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_hda.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating publishable Houdini Digital Assets.\"\"\"\nfrom openpype.client import (\n    get_asset_by_name,\n    get_subsets,\n)\nfrom openpype.hosts.houdini.api import plugin\nimport hou\n\n\nclass CreateHDA(plugin.HoudiniCreator):\n    \"\"\"Publish Houdini Digital Asset file.\"\"\"\n\n    identifier = \"io.openpype.creators.houdini.hda\"\n    label = \"Houdini Digital Asset (Hda)\"\n    family = \"hda\"\n    icon = \"gears\"\n    maintain_selection = False\n\n    def _check_existing(self, asset_name, subset_name):\n        # type: (str) -> bool\n        \"\"\"Check if existing subset name versions already exists.\"\"\"\n        # Get all subsets of the current asset\n        project_name = self.project_name\n        asset_doc = get_asset_by_name(\n            project_name, asset_name, fields=[\"_id\"]\n        )\n        subset_docs = get_subsets(\n            project_name, asset_ids=[asset_doc[\"_id\"]], fields=[\"name\"]\n        )\n        existing_subset_names_low = {\n            subset_doc[\"name\"].lower()\n            for subset_doc in subset_docs\n        }\n        return subset_name.lower() in existing_subset_names_low\n\n    def create_instance_node(\n        self, asset_name, node_name, parent, node_type=\"geometry\"\n    ):\n\n        parent_node = hou.node(\"/obj\")\n        if self.selected_nodes:\n            # if we have `use selection` enabled, and we have some\n            # selected nodes ...\n            subnet = parent_node.collapseIntoSubnet(\n                self.selected_nodes,\n                subnet_name=\"{}_subnet\".format(node_name))\n            subnet.moveToGoodPosition()\n            to_hda = subnet\n        else:\n            to_hda = parent_node.createNode(\n                \"subnet\", node_name=\"{}_subnet\".format(node_name))\n        if not to_hda.type().definition():\n            # if node type has not its definition, it is not user\n            # created hda. We test if hda can be created from the node.\n            if not to_hda.canCreateDigitalAsset():\n                raise plugin.OpenPypeCreatorError(\n                    \"cannot create hda from node {}\".format(to_hda))\n\n            hda_node = to_hda.createDigitalAsset(\n                name=node_name,\n                hda_file_name=\"$HIP/{}.hda\".format(node_name)\n            )\n            hda_node.layoutChildren()\n        elif self._check_existing(asset_name, node_name):\n            raise plugin.OpenPypeCreatorError(\n                (\"subset {} is already published with different HDA\"\n                 \"definition.\").format(node_name))\n        else:\n            hda_node = to_hda\n\n        hda_node.setName(node_name)\n        self.customize_node_look(hda_node)\n        return hda_node\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        instance_data.pop(\"active\", None)\n\n        instance = super(CreateHDA, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: plugin.CreatedInstance\n\n        return instance\n\n    def get_network_categories(self):\n        return [\n            hou.objNodeTypeCategory()\n        ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_karma_rop.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin to create Karma ROP.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.pipeline import CreatedInstance\nfrom openpype.lib import BoolDef, EnumDef, NumberDef\n\n\nclass CreateKarmaROP(plugin.HoudiniCreator):\n    \"\"\"Karma ROP\"\"\"\n    identifier = \"io.openpype.creators.houdini.karma_rop\"\n    label = \"Karma ROP\"\n    family = \"karma_rop\"\n    icon = \"magic\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        import hou  # noqa\n\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"karma\"})\n        # Add chunk size attribute\n        instance_data[\"chunkSize\"] = 10\n        # Submit for job publishing\n        instance_data[\"farm\"] = pre_create_data.get(\"farm\")\n\n        instance = super(CreateKarmaROP, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: CreatedInstance\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n        ext = pre_create_data.get(\"image_format\")\n\n        filepath = \"{renders_dir}{subset_name}/{subset_name}.$F4.{ext}\".format(\n            renders_dir=hou.text.expandString(\"$HIP/pyblish/renders/\"),\n            subset_name=subset_name,\n            ext=ext,\n        )\n        checkpoint = \"{cp_dir}{subset_name}.$F4.checkpoint\".format(\n            cp_dir=hou.text.expandString(\"$HIP/pyblish/\"),\n            subset_name=subset_name\n        )\n\n        usd_directory = \"{usd_dir}{subset_name}_$RENDERID\".format(\n            usd_dir=hou.text.expandString(\"$HIP/pyblish/renders/usd_renders/\"),     # noqa\n            subset_name=subset_name\n        )\n\n        parms = {\n            # Render Frame Range\n            \"trange\": 1,\n            # Karma ROP Setting\n            \"picture\": filepath,\n            # Karma Checkpoint Setting\n            \"productName\": checkpoint,\n            # USD Output Directory\n            \"savetodirectory\": usd_directory,\n        }\n\n        res_x = pre_create_data.get(\"res_x\")\n        res_y = pre_create_data.get(\"res_y\")\n\n        if self.selected_nodes:\n            # If camera found in selection\n            # we will use as render camera\n            camera = None\n            for node in self.selected_nodes:\n                if node.type().name() == \"cam\":\n                    camera = node.path()\n                    has_camera = pre_create_data.get(\"cam_res\")\n                    if has_camera:\n                        res_x = node.evalParm(\"resx\")\n                        res_y = node.evalParm(\"resy\")\n\n            if not camera:\n                self.log.warning(\"No render camera found in selection\")\n\n            parms.update({\n                \"camera\": camera or \"\",\n                \"resolutionx\": res_x,\n                \"resolutiony\": res_y,\n            })\n\n        instance_node.setParms(parms)\n\n        # Lock some Avalon attributes\n        to_lock = [\"family\", \"id\"]\n        self.lock_parameters(instance_node, to_lock)\n\n    def get_pre_create_attr_defs(self):\n        attrs = super(CreateKarmaROP, self).get_pre_create_attr_defs()\n\n        image_format_enum = [\n            \"bmp\", \"cin\", \"exr\", \"jpg\", \"pic\", \"pic.gz\", \"png\",\n            \"rad\", \"rat\", \"rta\", \"sgi\", \"tga\", \"tif\",\n        ]\n\n        return attrs + [\n            BoolDef(\"farm\",\n                    label=\"Submitting to Farm\",\n                    default=True),\n            EnumDef(\"image_format\",\n                    image_format_enum,\n                    default=\"exr\",\n                    label=\"Image Format Options\"),\n            NumberDef(\"res_x\",\n                      label=\"width\",\n                      default=1920,\n                      decimals=0),\n            NumberDef(\"res_y\",\n                      label=\"height\",\n                      default=720,\n                      decimals=0),\n            BoolDef(\"cam_res\",\n                    label=\"Camera Resolution\",\n                    default=False)\n        ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_mantra_ifd.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating pointcache alembics.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.pipeline import CreatedInstance\nfrom openpype.lib import BoolDef\n\n\nclass CreateMantraIFD(plugin.HoudiniCreator):\n    \"\"\"Mantra .ifd Archive\"\"\"\n    identifier = \"io.openpype.creators.houdini.mantraifd\"\n    label = \"Mantra IFD\"\n    family = \"mantraifd\"\n    icon = \"gears\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        import hou\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"ifd\"})\n        creator_attributes = instance_data.setdefault(\n            \"creator_attributes\", dict())\n        creator_attributes[\"farm\"] = pre_create_data[\"farm\"]\n        instance = super(CreateMantraIFD, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: CreatedInstance\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n        filepath = \"{}{}\".format(\n            hou.text.expandString(\"$HIP/pyblish/\"),\n            \"{}.$F4.ifd\".format(subset_name))\n        parms = {\n            # Render frame range\n            \"trange\": 1,\n            # Arnold ROP settings\n            \"soho_diskfile\": filepath,\n            \"soho_outputmode\": 1\n        }\n\n        instance_node.setParms(parms)\n\n        # Lock any parameters in this list\n        to_lock = [\"soho_outputmode\", \"family\", \"id\"]\n        self.lock_parameters(instance_node, to_lock)\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\"farm\",\n                    label=\"Submitting to Farm\",\n                    default=False)\n        ]\n\n    def get_pre_create_attr_defs(self):\n        attrs = super().get_pre_create_attr_defs()\n        # Use same attributes as for instance attributes\n        return attrs + self.get_instance_attr_defs()\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_mantra_rop.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin to create Mantra ROP.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.pipeline import CreatedInstance\nfrom openpype.lib import EnumDef, BoolDef\n\n\nclass CreateMantraROP(plugin.HoudiniCreator):\n    \"\"\"Mantra ROP\"\"\"\n    identifier = \"io.openpype.creators.houdini.mantra_rop\"\n    label = \"Mantra ROP\"\n    family = \"mantra_rop\"\n    icon = \"magic\"\n\n    # Default to split export and render jobs\n    export_job = True\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        import hou  # noqa\n\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"ifd\"})\n        # Add chunk size attribute\n        instance_data[\"chunkSize\"] = 10\n        # Submit for job publishing\n        instance_data[\"farm\"] = pre_create_data.get(\"farm\")\n\n        instance = super(CreateMantraROP, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: CreatedInstance\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n        ext = pre_create_data.get(\"image_format\")\n\n        filepath = \"{renders_dir}{subset_name}/{subset_name}.$F4.{ext}\".format(\n            renders_dir=hou.text.expandString(\"$HIP/pyblish/renders/\"),\n            subset_name=subset_name,\n            ext=ext,\n        )\n\n        parms = {\n            # Render Frame Range\n            \"trange\": 1,\n            # Mantra ROP Setting\n            \"vm_picture\": filepath,\n        }\n\n        if pre_create_data.get(\"export_job\"):\n            ifd_filepath = \\\n                \"{export_dir}{subset_name}/{subset_name}.$F4.ifd\".format(\n                    export_dir=hou.text.expandString(\"$HIP/pyblish/ifd/\"),\n                    subset_name=subset_name,\n                )\n            parms[\"soho_outputmode\"] = 1\n            parms[\"soho_diskfile\"] = ifd_filepath\n\n        if self.selected_nodes:\n            # If camera found in selection\n            # we will use as render camera\n            camera = None\n            for node in self.selected_nodes:\n                if node.type().name() == \"cam\":\n                    camera = node.path()\n\n            if not camera:\n                self.log.warning(\"No render camera found in selection\")\n\n            parms.update({\"camera\": camera or \"\"})\n\n        custom_res = pre_create_data.get(\"override_resolution\")\n        if custom_res:\n            parms.update({\"override_camerares\": 1})\n        instance_node.setParms(parms)\n\n        # Lock some Avalon attributes\n        to_lock = [\"family\", \"id\"]\n        self.lock_parameters(instance_node, to_lock)\n\n    def get_pre_create_attr_defs(self):\n        attrs = super(CreateMantraROP, self).get_pre_create_attr_defs()\n\n        image_format_enum = [\n            \"bmp\", \"cin\", \"exr\", \"jpg\", \"pic\", \"pic.gz\", \"png\",\n            \"rad\", \"rat\", \"rta\", \"sgi\", \"tga\", \"tif\",\n        ]\n\n        return attrs + [\n            BoolDef(\"farm\",\n                    label=\"Submitting to Farm\",\n                    default=True),\n            BoolDef(\"export_job\",\n                    label=\"Split export and render jobs\",\n                    default=self.export_job),\n            EnumDef(\"image_format\",\n                    image_format_enum,\n                    default=\"exr\",\n                    label=\"Image Format Options\"),\n            BoolDef(\"override_resolution\",\n                    label=\"Override Camera Resolution\",\n                    tooltip=\"Override the current camera \"\n                            \"resolution, recommended for IPR.\",\n                    default=False)\n        ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_pointcache.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating pointcache alembics.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.lib import BoolDef\n\nimport hou\n\n\n\nclass CreatePointCache(plugin.HoudiniCreator):\n    \"\"\"Alembic ROP to pointcache\"\"\"\n    identifier = \"io.openpype.creators.houdini.pointcache\"\n    label = \"PointCache (Abc)\"\n    family = \"pointcache\"\n    icon = \"gears\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"alembic\"})\n        creator_attributes = instance_data.setdefault(\n            \"creator_attributes\", dict())\n        creator_attributes[\"farm\"] = pre_create_data[\"farm\"]\n\n        instance = super(CreatePointCache, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n        parms = {\n            \"use_sop_path\": True,\n            \"build_from_path\": True,\n            \"path_attrib\": \"path\",\n            \"prim_to_detail_pattern\": \"cbId\",\n            \"format\": 2,\n            \"facesets\": 0,\n            \"filename\": hou.text.expandString(\n                \"$HIP/pyblish/{}.abc\".format(subset_name))\n        }\n\n        if self.selected_nodes:\n            selected_node = self.selected_nodes[0]\n\n            # Although Houdini allows ObjNode path on `sop_path` for the\n            # the ROP node we prefer it set to the SopNode path explicitly\n\n            # Allow sop level paths (e.g. /obj/geo1/box1)\n            if isinstance(selected_node, hou.SopNode):\n                parms[\"sop_path\"] = selected_node.path()\n                self.log.debug(\n                   \"Valid SopNode selection, 'SOP Path' in ROP will be set to '%s'.\"\n                   % selected_node.path()\n                )\n\n            # Allow object level paths to Geometry nodes (e.g. /obj/geo1)\n            # but do not allow other object level nodes types like cameras, etc.\n            elif isinstance(selected_node, hou.ObjNode) and \\\n                    selected_node.type().name() in [\"geo\"]:\n\n                # get the output node with the minimum\n                # 'outputidx' or the node with display flag\n                sop_path = self.get_obj_output(selected_node)\n\n                if sop_path:\n                    parms[\"sop_path\"] = sop_path.path()\n                    self.log.debug(\n                        \"Valid ObjNode selection, 'SOP Path' in ROP will be set to \"\n                        \"the child path '%s'.\"\n                        % sop_path.path()\n                    )\n\n            if not parms.get(\"sop_path\", None):\n                self.log.debug(\n                    \"Selection isn't valid. 'SOP Path' in ROP will be empty.\"\n                )\n        else:\n            self.log.debug(\n                \"No Selection. 'SOP Path' in ROP will be empty.\"\n            )\n\n        instance_node.setParms(parms)\n        instance_node.parm(\"trange\").set(1)\n\n        # Lock any parameters in this list\n        to_lock = [\"prim_to_detail_pattern\"]\n        self.lock_parameters(instance_node, to_lock)\n\n    def get_network_categories(self):\n        return [\n            hou.ropNodeTypeCategory(),\n            hou.sopNodeTypeCategory()\n        ]\n\n    def get_obj_output(self, obj_node):\n        \"\"\"Find output node with the smallest 'outputidx'.\"\"\"\n\n        outputs = obj_node.subnetOutputs()\n\n        # if obj_node is empty\n        if not outputs:\n            return\n\n        # if obj_node has one output child whether its\n        # sop output node or a node with the render flag\n        elif len(outputs) == 1:\n            return outputs[0]\n\n        # if there are more than one, then it have multiple ouput nodes\n        # return the one with the minimum 'outputidx'\n        else:\n            return min(outputs,\n                       key=lambda node: node.evalParm('outputidx'))\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\"farm\",\n                    label=\"Submitting to Farm\",\n                    default=False)\n        ]\n\n    def get_pre_create_attr_defs(self):\n        attrs = super().get_pre_create_attr_defs()\n        # Use same attributes as for instance attributes\n        return attrs + self.get_instance_attr_defs()\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_redshift_proxy.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating Redshift proxies.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nimport hou\nfrom openpype.lib import BoolDef\n\n\nclass CreateRedshiftProxy(plugin.HoudiniCreator):\n    \"\"\"Redshift Proxy\"\"\"\n    identifier = \"io.openpype.creators.houdini.redshiftproxy\"\n    label = \"Redshift Proxy\"\n    family = \"redshiftproxy\"\n    icon = \"magic\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        # Remove the active, we are checking the bypass flag of the nodes\n        instance_data.pop(\"active\", None)\n\n        # Redshift provides a `Redshift_Proxy_Output` node type which shows\n        # a limited set of parameters by default and is set to extract a\n        # Redshift Proxy. However when \"imprinting\" extra parameters needed\n        # for OpenPype it starts showing all its parameters again. It's unclear\n        # why this happens.\n        # TODO: Somehow enforce so that it only shows the original limited\n        #       attributes of the Redshift_Proxy_Output node type\n        instance_data.update({\"node_type\": \"Redshift_Proxy_Output\"})\n        creator_attributes = instance_data.setdefault(\n            \"creator_attributes\", dict())\n        creator_attributes[\"farm\"] = pre_create_data[\"farm\"]\n\n        instance = super(CreateRedshiftProxy, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n        parms = {\n            \"RS_archive_file\": '$HIP/pyblish/{}.$F4.rs'.format(subset_name),\n        }\n\n        if self.selected_nodes:\n            parms[\"RS_archive_sopPath\"] = self.selected_nodes[0].path()\n\n        instance_node.setParms(parms)\n\n        # Lock some Avalon attributes\n        to_lock = [\"family\", \"id\", \"prim_to_detail_pattern\"]\n        self.lock_parameters(instance_node, to_lock)\n\n    def get_network_categories(self):\n        return [\n            hou.ropNodeTypeCategory(),\n            hou.sopNodeTypeCategory()\n        ]\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\"farm\",\n                    label=\"Submitting to Farm\",\n                    default=False)\n        ]\n\n    def get_pre_create_attr_defs(self):\n        attrs = super().get_pre_create_attr_defs()\n        # Use same attributes as for instance attributes\n        return attrs + self.get_instance_attr_defs()\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_redshift_rop.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin to create Redshift ROP.\"\"\"\nimport hou  # noqa\n\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.lib import EnumDef, BoolDef\n\n\nclass CreateRedshiftROP(plugin.HoudiniCreator):\n    \"\"\"Redshift ROP\"\"\"\n\n    identifier = \"io.openpype.creators.houdini.redshift_rop\"\n    label = \"Redshift ROP\"\n    family = \"redshift_rop\"\n    icon = \"magic\"\n    ext = \"exr\"\n\n    # Default to split export and render jobs\n    split_render = True\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"Redshift_ROP\"})\n        # Add chunk size attribute\n        instance_data[\"chunkSize\"] = 10\n        # Submit for job publishing\n        instance_data[\"farm\"] = pre_create_data.get(\"farm\")\n\n        instance = super(CreateRedshiftROP, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n        basename = instance_node.name()\n\n        # Also create the linked Redshift IPR Rop\n        try:\n            ipr_rop = instance_node.parent().createNode(\n                \"Redshift_IPR\", node_name=f\"{basename}_IPR\"\n            )\n        except hou.OperationFailed as e:\n            raise plugin.OpenPypeCreatorError(\n                (\n                    \"Cannot create Redshift node. Is Redshift \"\n                    \"installed and enabled?\"\n                )\n            ) from e\n\n        # Move it to directly under the Redshift ROP\n        ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1))\n\n        # Set the linked rop to the Redshift ROP\n        ipr_rop.parm(\"linked_rop\").set(instance_node.path())\n\n        ext = pre_create_data.get(\"image_format\")\n        filepath = \"{renders_dir}{subset_name}/{subset_name}.{fmt}\".format(\n            renders_dir=hou.text.expandString(\"$HIP/pyblish/renders/\"),\n            subset_name=subset_name,\n            fmt=\"${aov}.$F4.{ext}\".format(aov=\"AOV\", ext=ext)\n        )\n\n        ext_format_index = {\"exr\": 0, \"tif\": 1, \"jpg\": 2, \"png\": 3}\n\n        parms = {\n            # Render frame range\n            \"trange\": 1,\n            # Redshift ROP settings\n            \"RS_outputFileNamePrefix\": filepath,\n            \"RS_outputMultilayerMode\": \"1\",  # no multi-layered exr\n            \"RS_outputBeautyAOVSuffix\": \"beauty\",\n            \"RS_outputFileFormat\": ext_format_index[ext],\n        }\n\n        if self.selected_nodes:\n            # set up the render camera from the selected node\n            camera = None\n            for node in self.selected_nodes:\n                if node.type().name() == \"cam\":\n                    camera = node.path()\n            parms[\"RS_renderCamera\"] = camera or \"\"\n\n        export_dir = hou.text.expandString(\"$HIP/pyblish/rs/\")\n        rs_filepath = f\"{export_dir}{subset_name}/{subset_name}.$F4.rs\"\n        parms[\"RS_archive_file\"] = rs_filepath\n\n        if pre_create_data.get(\"split_render\", self.split_render):\n            parms[\"RS_archive_enable\"] = 1\n\n        instance_node.setParms(parms)\n\n        # Lock some Avalon attributes\n        to_lock = [\"family\", \"id\"]\n        self.lock_parameters(instance_node, to_lock)\n\n    def remove_instances(self, instances):\n        for instance in instances:\n            node = instance.data.get(\"instance_node\")\n\n            ipr_node = hou.node(f\"{node}_IPR\")\n            if ipr_node:\n                ipr_node.destroy()\n\n        return super(CreateRedshiftROP, self).remove_instances(instances)\n\n    def get_pre_create_attr_defs(self):\n        attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs()\n        image_format_enum = [\n            \"exr\", \"tif\", \"jpg\", \"png\",\n        ]\n\n        return attrs + [\n            BoolDef(\"farm\",\n                    label=\"Submitting to Farm\",\n                    default=True),\n            BoolDef(\"split_render\",\n                    label=\"Split export and render jobs\",\n                    default=self.split_render),\n            EnumDef(\"image_format\",\n                    image_format_enum,\n                    default=self.ext,\n                    label=\"Image Format Options\")\n        ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_review.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating openGL reviews.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.lib import EnumDef, BoolDef, NumberDef\n\nimport os\nimport hou\n\n\nclass CreateReview(plugin.HoudiniCreator):\n    \"\"\"Review with OpenGL ROP\"\"\"\n\n    identifier = \"io.openpype.creators.houdini.review\"\n    label = \"Review\"\n    family = \"review\"\n    icon = \"video-camera\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"opengl\"})\n        instance_data[\"imageFormat\"] = pre_create_data.get(\"imageFormat\")\n        instance_data[\"keepImages\"] = pre_create_data.get(\"keepImages\")\n\n        instance = super(CreateReview, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n        frame_range = hou.playbar.frameRange()\n\n        filepath = \"{root}/{subset}/{subset}.$F4.{ext}\".format(\n            root=hou.text.expandString(\"$HIP/pyblish\"),\n            subset=\"`chs(\\\"subset\\\")`\",  # keep dynamic link to subset\n            ext=pre_create_data.get(\"image_format\") or \"png\"\n        )\n\n        parms = {\n            \"picture\": filepath,\n\n            \"trange\": 1,\n\n            # Unlike many other ROP nodes the opengl node does not default\n            # to expression of $FSTART and $FEND so we preserve that behavior\n            # but do set the range to the frame range of the playbar\n            \"f1\": frame_range[0],\n            \"f2\": frame_range[1],\n        }\n\n        override_resolution = pre_create_data.get(\"override_resolution\")\n        if override_resolution:\n            parms.update({\n                \"tres\": override_resolution,\n                \"res1\": pre_create_data.get(\"resx\"),\n                \"res2\": pre_create_data.get(\"resy\"),\n                \"aspect\": pre_create_data.get(\"aspect\"),\n            })\n\n        if self.selected_nodes:\n            # The first camera found in selection we will use as camera\n            # Other node types we set in force objects\n            camera = None\n            force_objects = []\n            for node in self.selected_nodes:\n                path = node.path()\n                if node.type().name() == \"cam\":\n                    if camera:\n                        continue\n                    camera = path\n                else:\n                    force_objects.append(path)\n\n            if not camera:\n                self.log.warning(\"No camera found in selection.\")\n\n            parms.update({\n                \"camera\": camera or \"\",\n                \"scenepath\": \"/obj\",\n                \"forceobjects\": \" \".join(force_objects),\n                \"vobjects\": \"\"  # clear candidate objects from '*' value\n            })\n\n        instance_node.setParms(parms)\n\n        # Set OCIO Colorspace to the default output colorspace\n        #  if there's OCIO\n        if os.getenv(\"OCIO\"):\n            self.set_colorcorrect_to_default_view_space(instance_node)\n\n        to_lock = [\"id\", \"family\"]\n\n        self.lock_parameters(instance_node, to_lock)\n\n    def get_pre_create_attr_defs(self):\n        attrs = super(CreateReview, self).get_pre_create_attr_defs()\n\n        image_format_enum = [\n            \"bmp\", \"cin\", \"exr\", \"jpg\", \"pic\", \"pic.gz\", \"png\",\n            \"rad\", \"rat\", \"rta\", \"sgi\", \"tga\", \"tif\",\n        ]\n\n        return attrs + [\n            BoolDef(\"keepImages\",\n                    label=\"Keep Image Sequences\",\n                    default=False),\n            EnumDef(\"imageFormat\",\n                    image_format_enum,\n                    default=\"png\",\n                    label=\"Image Format Options\"),\n            BoolDef(\"override_resolution\",\n                    label=\"Override resolution\",\n                    tooltip=\"When disabled the resolution set on the camera \"\n                            \"is used instead.\",\n                    default=True),\n            NumberDef(\"resx\",\n                      label=\"Resolution Width\",\n                      default=1280,\n                      minimum=2,\n                      decimals=0),\n            NumberDef(\"resy\",\n                      label=\"Resolution Height\",\n                      default=720,\n                      minimum=2,\n                      decimals=0),\n            NumberDef(\"aspect\",\n                      label=\"Aspect Ratio\",\n                      default=1.0,\n                      minimum=0.0001,\n                      decimals=3)\n        ]\n\n    def set_colorcorrect_to_default_view_space(self,\n                                               instance_node):\n        \"\"\"Set ociocolorspace to the default output space.\"\"\"\n        from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace  # noqa\n\n        # set Color Correction parameter to OpenColorIO\n        instance_node.setParms({\"colorcorrect\": 2})\n\n        # Get default view space for ociocolorspace parm.\n        default_view_space = get_default_display_view_colorspace()\n        instance_node.setParms(\n            {\"ociocolorspace\": default_view_space}\n        )\n\n        self.log.debug(\n            \"'OCIO Colorspace' parm on '{}' has been set to \"\n            \"the default view color space '{}'\"\n            .format(instance_node, default_view_space)\n        )\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_staticmesh.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator for Unreal Static Meshes.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.lib import BoolDef, EnumDef\n\nimport hou\n\n\nclass CreateStaticMesh(plugin.HoudiniCreator):\n    \"\"\"Static Meshes as FBX. \"\"\"\n\n    identifier = \"io.openpype.creators.houdini.staticmesh.fbx\"\n    label = \"Static Mesh (FBX)\"\n    family = \"staticMesh\"\n    icon = \"fa5s.cubes\"\n\n    default_variants = [\"Main\"]\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        instance_data.update({\"node_type\": \"filmboxfbx\"})\n\n        instance = super(CreateStaticMesh, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)\n\n        # get the created rop node\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n        # prepare parms\n        output_path = hou.text.expandString(\n            \"$HIP/pyblish/{}.fbx\".format(subset_name)\n        )\n\n        parms = {\n            \"startnode\": self.get_selection(),\n            \"sopoutput\": output_path,\n            # vertex cache format\n            \"vcformat\": pre_create_data.get(\"vcformat\"),\n            \"convertunits\": pre_create_data.get(\"convertunits\"),\n            # set render range to use frame range start-end frame\n            \"trange\": 1,\n            \"createsubnetroot\": pre_create_data.get(\"createsubnetroot\")\n        }\n\n        # set parms\n        instance_node.setParms(parms)\n\n        # Lock any parameters in this list\n        to_lock = [\"family\", \"id\"]\n        self.lock_parameters(instance_node, to_lock)\n\n    def get_network_categories(self):\n        return [\n            hou.ropNodeTypeCategory(),\n            hou.objNodeTypeCategory(),\n            hou.sopNodeTypeCategory()\n        ]\n\n    def get_pre_create_attr_defs(self):\n        \"\"\"Add settings for users. \"\"\"\n\n        attrs = super(CreateStaticMesh, self).get_pre_create_attr_defs()\n        createsubnetroot = BoolDef(\"createsubnetroot\",\n                                   tooltip=\"Create an extra root for the \"\n                                           \"Export node when it's a \"\n                                           \"subnetwork. This causes the \"\n                                           \"exporting subnetwork node to be \"\n                                           \"represented in the FBX file.\",\n                                   default=False,\n                                   label=\"Create Root for Subnet\")\n        vcformat = EnumDef(\"vcformat\",\n                           items={\n                               0: \"Maya Compatible (MC)\",\n                               1: \"3DS MAX Compatible (PC2)\"\n                           },\n                           default=0,\n                           label=\"Vertex Cache Format\")\n        convert_units = BoolDef(\"convertunits\",\n                                tooltip=\"When on, the FBX is converted\"\n                                        \"from the current Houdini \"\n                                        \"system units to the native \"\n                                        \"FBX unit of centimeters.\",\n                                default=False,\n                                label=\"Convert Units\")\n\n        return attrs + [createsubnetroot, vcformat, convert_units]\n\n    def get_dynamic_data(\n        self, variant, task_name, asset_doc, project_name, host_name, instance\n    ):\n        \"\"\"\n        The default subset name templates for Unreal include {asset} and thus\n        we should pass that along as dynamic data.\n        \"\"\"\n        dynamic_data = super(CreateStaticMesh, self).get_dynamic_data(\n            variant, task_name, asset_doc, project_name, host_name, instance\n        )\n        dynamic_data[\"asset\"] = asset_doc[\"name\"]\n        return dynamic_data\n\n    def get_selection(self):\n        \"\"\"Selection Logic.\n\n        how self.selected_nodes should be processed to get\n        the desirable node from selection.\n\n        Returns:\n            str : node path\n        \"\"\"\n\n        selection = \"\"\n\n        if self.selected_nodes:\n            selected_node = self.selected_nodes[0]\n\n            # Accept sop level nodes (e.g. /obj/geo1/box1)\n            if isinstance(selected_node, hou.SopNode):\n                selection = selected_node.path()\n                self.log.debug(\n                    \"Valid SopNode selection, 'Export' in filmboxfbx\"\n                    \" will be set to '%s'.\", selected_node\n                )\n\n            # Accept object level nodes (e.g. /obj/geo1)\n            elif isinstance(selected_node, hou.ObjNode):\n                selection = selected_node.path()\n                self.log.debug(\n                    \"Valid ObjNode selection, 'Export' in filmboxfbx \"\n                    \"will be set to the child path '%s'.\", selection\n                )\n\n            else:\n                self.log.debug(\n                    \"Selection isn't valid. 'Export' in \"\n                    \"filmboxfbx will be empty.\"\n                )\n        else:\n            self.log.debug(\n                \"No Selection. 'Export' in filmboxfbx will be empty.\"\n            )\n\n        return selection\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_usd.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating USDs.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.pipeline import CreatedInstance\n\nimport hou\n\n\nclass CreateUSD(plugin.HoudiniCreator):\n    \"\"\"Universal Scene Description\"\"\"\n    identifier = \"io.openpype.creators.houdini.usd\"\n    label = \"USD (experimental)\"\n    family = \"usd\"\n    icon = \"gears\"\n    enabled = False\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"usd\"})\n\n        instance = super(CreateUSD, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: CreatedInstance\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n        parms = {\n            \"lopoutput\": \"$HIP/pyblish/{}.usd\".format(subset_name),\n            \"enableoutputprocessor_simplerelativepaths\": False,\n        }\n\n        if self.selected_nodes:\n            parms[\"loppath\"] = self.selected_nodes[0].path()\n\n        instance_node.setParms(parms)\n\n        # Lock any parameters in this list\n        to_lock = [\n            \"fileperframe\",\n            # Lock some Avalon attributes\n            \"family\",\n            \"id\",\n        ]\n        self.lock_parameters(instance_node, to_lock)\n\n    def get_network_categories(self):\n        return [\n            hou.ropNodeTypeCategory(),\n            hou.lopNodeTypeCategory()\n        ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_usdrender.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating USD renders.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.pipeline import CreatedInstance\n\n\nclass CreateUSDRender(plugin.HoudiniCreator):\n    \"\"\"USD Render ROP in /stage\"\"\"\n    identifier = \"io.openpype.creators.houdini.usdrender\"\n    label = \"USD Render (experimental)\"\n    family = \"usdrender\"\n    icon = \"magic\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        import hou  # noqa\n\n        instance_data[\"parent\"] = hou.node(\"/stage\")\n\n        # Remove the active, we are checking the bypass flag of the nodes\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"usdrender\"})\n\n        instance = super(CreateUSDRender, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: CreatedInstance\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n\n        parms = {\n            # Render frame range\n            \"trange\": 1\n        }\n        if self.selected_nodes:\n            parms[\"loppath\"] = self.selected_nodes[0].path()\n        instance_node.setParms(parms)\n\n        # Lock some Avalon attributes\n        to_lock = [\"family\", \"id\"]\n        self.lock_parameters(instance_node, to_lock)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_vbd_cache.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating VDB Caches.\"\"\"\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.pipeline import CreatedInstance\nfrom openpype.lib import BoolDef\n\nimport hou\n\n\nclass CreateVDBCache(plugin.HoudiniCreator):\n    \"\"\"OpenVDB from Geometry ROP\"\"\"\n    identifier = \"io.openpype.creators.houdini.vdbcache\"\n    name = \"vbdcache\"\n    label = \"VDB Cache\"\n    family = \"vdbcache\"\n    icon = \"cloud\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        import hou\n\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"geometry\"})\n        creator_attributes = instance_data.setdefault(\n            \"creator_attributes\", dict())\n        creator_attributes[\"farm\"] = pre_create_data[\"farm\"]\n        instance = super(CreateVDBCache, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: CreatedInstance\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n        file_path = \"{}{}\".format(\n            hou.text.expandString(\"$HIP/pyblish/\"),\n            \"{}.$F4.vdb\".format(subset_name))\n        parms = {\n            \"sopoutput\": file_path,\n            \"initsim\": True,\n            \"trange\": 1\n        }\n\n        if self.selected_nodes:\n            parms[\"soppath\"] = self.get_sop_node_path(self.selected_nodes[0])\n\n        instance_node.setParms(parms)\n\n    def get_network_categories(self):\n        return [\n            hou.ropNodeTypeCategory(),\n            hou.objNodeTypeCategory(),\n            hou.sopNodeTypeCategory()\n        ]\n\n    def get_sop_node_path(self, selected_node):\n        \"\"\"Get Sop Path of the selected node.\n\n        Although Houdini allows ObjNode path on `sop_path` for the\n        the ROP node, we prefer it set to the SopNode path explicitly.\n        \"\"\"\n\n        # Allow sop level paths (e.g. /obj/geo1/box1)\n        if isinstance(selected_node, hou.SopNode):\n            self.log.debug(\n                \"Valid SopNode selection, 'SOP Path' in ROP will\"\n                \" be set to '%s'.\", selected_node.path()\n            )\n            return selected_node.path()\n\n        # Allow object level paths to Geometry nodes (e.g. /obj/geo1)\n        # but do not allow other object level nodes types like cameras, etc.\n        elif isinstance(selected_node, hou.ObjNode) and \\\n                selected_node.type().name() == \"geo\":\n\n            # Try to find output node.\n            sop_node = self.get_obj_output(selected_node)\n            if sop_node:\n                self.log.debug(\n                    \"Valid ObjNode selection, 'SOP Path' in ROP will \"\n                    \"be set to the child path '%s'.\", sop_node.path()\n                )\n                return sop_node.path()\n\n        self.log.debug(\n            \"Selection isn't valid. 'SOP Path' in ROP will be empty.\"\n        )\n        return \"\"\n\n    def get_obj_output(self, obj_node):\n        \"\"\"Try to find output node.\n\n        If any output nodes are present, return the output node with\n          the minimum 'outputidx'\n        If no output nodes are present, return the node with display flag\n        If no nodes are present at all, return None\n        \"\"\"\n\n        outputs = obj_node.subnetOutputs()\n\n        # if obj_node is empty\n        if not outputs:\n            return\n\n        # if obj_node has one output child whether its\n        # sop output node or a node with the render flag\n        elif len(outputs) == 1:\n            return outputs[0]\n\n        # if there are more than one, then it has multiple output nodes\n        # return the one with the minimum 'outputidx'\n        else:\n            return min(outputs,\n                       key=lambda node: node.evalParm('outputidx'))\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\"farm\",\n                    label=\"Submitting to Farm\",\n                    default=False)\n        ]\n\n    def get_pre_create_attr_defs(self):\n        attrs = super().get_pre_create_attr_defs()\n        # Use same attributes as for instance attributes\n        return attrs + self.get_instance_attr_defs()\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_vray_rop.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin to create VRay ROP.\"\"\"\nimport hou\n\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.pipeline import CreatedInstance\nfrom openpype.lib import EnumDef, BoolDef\n\n\nclass CreateVrayROP(plugin.HoudiniCreator):\n    \"\"\"VRay ROP\"\"\"\n\n    identifier = \"io.openpype.creators.houdini.vray_rop\"\n    label = \"VRay ROP\"\n    family = \"vray_rop\"\n    icon = \"magic\"\n    ext = \"exr\"\n\n    # Default to split export and render jobs\n    export_job = True\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        instance_data.pop(\"active\", None)\n        instance_data.update({\"node_type\": \"vray_renderer\"})\n        # Add chunk size attribute\n        instance_data[\"chunkSize\"] = 10\n        # Submit for job publishing\n        instance_data[\"farm\"] = pre_create_data.get(\"farm\")\n\n        instance = super(CreateVrayROP, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)  # type: CreatedInstance\n\n        instance_node = hou.node(instance.get(\"instance_node\"))\n\n        # Add IPR for Vray\n        basename = instance_node.name()\n        try:\n            ipr_rop = instance_node.parent().createNode(\n                \"vray\", node_name=basename + \"_IPR\"\n            )\n        except hou.OperationFailed:\n            raise plugin.OpenPypeCreatorError(\n                \"Cannot create Vray render node. \"\n                \"Make sure Vray installed and enabled!\"\n            )\n\n        ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1))\n        ipr_rop.parm(\"rop\").set(instance_node.path())\n\n        parms = {\n            \"trange\": 1,\n            \"SettingsEXR_bits_per_channel\": \"16\"   # half precision\n        }\n\n        if pre_create_data.get(\"export_job\"):\n            scene_filepath = \\\n                \"{export_dir}{subset_name}/{subset_name}.$F4.vrscene\".format(\n                    export_dir=hou.text.expandString(\"$HIP/pyblish/vrscene/\"),\n                    subset_name=subset_name,\n                )\n            # Setting render_export_mode to \"2\" because that's for\n            # \"Export only\" (\"1\" is for \"Export & Render\")\n            parms[\"render_export_mode\"] = \"2\"\n            parms[\"render_export_filepath\"] = scene_filepath\n\n        if self.selected_nodes:\n            # set up the render camera from the selected node\n            camera = None\n            for node in self.selected_nodes:\n                if node.type().name() == \"cam\":\n                    camera = node.path()\n            parms.update({\n                \"render_camera\": camera or \"\"\n            })\n\n        # Enable render element\n        ext = pre_create_data.get(\"image_format\")\n        instance_data[\"RenderElement\"] = pre_create_data.get(\"render_element_enabled\")         # noqa\n        if pre_create_data.get(\"render_element_enabled\", True):\n            # Vray has its own tag for AOV file output\n            filepath = \"{renders_dir}{subset_name}/{subset_name}.{fmt}\".format(\n                renders_dir=hou.text.expandString(\"$HIP/pyblish/renders/\"),\n                subset_name=subset_name,\n                fmt=\"${aov}.$F4.{ext}\".format(aov=\"AOV\",\n                                              ext=ext)\n            )\n            filepath = \"{}{}\".format(\n                hou.text.expandString(\"$HIP/pyblish/renders/\"),\n                \"{}/{}.${}.$F4.{}\".format(subset_name,\n                                          subset_name,\n                                          \"AOV\",\n                                          ext)\n            )\n            re_rop = instance_node.parent().createNode(\n                \"vray_render_channels\",\n                node_name=basename + \"_render_element\"\n            )\n            # move the render element node next to the vray renderer node\n            re_rop.setPosition(instance_node.position() + hou.Vector2(0, 1))\n            re_path = re_rop.path()\n            parms.update({\n                \"use_render_channels\": 1,\n                \"SettingsOutput_img_file_path\": filepath,\n                \"render_network_render_channels\": re_path\n            })\n\n        else:\n            filepath = \"{renders_dir}{subset_name}/{subset_name}.{fmt}\".format(\n                renders_dir=hou.text.expandString(\"$HIP/pyblish/renders/\"),\n                subset_name=subset_name,\n                fmt=\"$F4.{ext}\".format(ext=ext)\n            )\n            parms.update({\n                \"use_render_channels\": 0,\n                \"SettingsOutput_img_file_path\": filepath\n            })\n\n        custom_res = pre_create_data.get(\"override_resolution\")\n        if custom_res:\n            parms.update({\"override_camerares\": 1})\n\n        instance_node.setParms(parms)\n\n        # lock parameters from AVALON\n        to_lock = [\"family\", \"id\"]\n        self.lock_parameters(instance_node, to_lock)\n\n    def remove_instances(self, instances):\n        for instance in instances:\n            node = instance.data.get(\"instance_node\")\n            # for the extra render node from the plugins\n            # such as vray and redshift\n            ipr_node = hou.node(\"{}{}\".format(node, \"_IPR\"))\n            if ipr_node:\n                ipr_node.destroy()\n            re_node = hou.node(\"{}{}\".format(node,\n                                             \"_render_element\"))\n            if re_node:\n                re_node.destroy()\n\n        return super(CreateVrayROP, self).remove_instances(instances)\n\n    def get_pre_create_attr_defs(self):\n        attrs = super(CreateVrayROP, self).get_pre_create_attr_defs()\n        image_format_enum = [\n            \"bmp\", \"cin\", \"exr\", \"jpg\", \"pic\", \"pic.gz\", \"png\",\n            \"rad\", \"rat\", \"rta\", \"sgi\", \"tga\", \"tif\",\n        ]\n\n        return attrs + [\n            BoolDef(\"farm\",\n                    label=\"Submitting to Farm\",\n                    default=True),\n            BoolDef(\"export_job\",\n                    label=\"Split export and render jobs\",\n                    default=self.export_job),\n            EnumDef(\"image_format\",\n                    image_format_enum,\n                    default=self.ext,\n                    label=\"Image Format Options\"),\n            BoolDef(\"override_resolution\",\n                    label=\"Override Camera Resolution\",\n                    tooltip=\"Override the current camera \"\n                            \"resolution, recommended for IPR.\",\n                    default=False),\n            BoolDef(\"render_element_enabled\",\n                    label=\"Render Element\",\n                    tooltip=\"Create Render Element Node \"\n                            \"if enabled\",\n                    default=False)\n        ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/create/create_workfile.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating workfiles.\"\"\"\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.hosts.houdini.api import plugin\nfrom openpype.hosts.houdini.api.lib import read, imprint\nfrom openpype.hosts.houdini.api.pipeline import CONTEXT_CONTAINER\nfrom openpype.pipeline import CreatedInstance, AutoCreator\nfrom openpype.client import get_asset_by_name\nimport hou\n\n\nclass CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator):\n    \"\"\"Workfile auto-creator.\"\"\"\n    identifier = \"io.openpype.creators.houdini.workfile\"\n    label = \"Workfile\"\n    family = \"workfile\"\n    icon = \"fa5.file\"\n\n    default_variant = \"Main\"\n\n    def create(self):\n        variant = self.default_variant\n        current_instance = next(\n            (\n                instance for instance in self.create_context.instances\n                if instance.creator_identifier == self.identifier\n            ), None)\n\n        project_name = self.project_name\n        asset_name = self.create_context.get_current_asset_name()\n        task_name = self.create_context.get_current_task_name()\n        host_name = self.host_name\n\n        if current_instance is None:\n            current_instance_asset = None\n        elif AYON_SERVER_ENABLED:\n            current_instance_asset = current_instance[\"folderPath\"]\n        else:\n            current_instance_asset = current_instance[\"asset\"]\n\n        if current_instance is None:\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                variant, task_name, asset_doc, project_name, host_name\n            )\n            data = {\n                \"task\": task_name,\n                \"variant\": variant\n            }\n            if AYON_SERVER_ENABLED:\n                data[\"folderPath\"] = asset_name\n            else:\n                data[\"asset\"] = asset_name\n\n            data.update(\n                self.get_dynamic_data(\n                    variant, task_name, asset_doc,\n                    project_name, host_name, current_instance)\n            )\n            self.log.info(\"Auto-creating workfile instance...\")\n            current_instance = CreatedInstance(\n                self.family, subset_name, data, self\n            )\n            self._add_instance_to_context(current_instance)\n        elif (\n            current_instance_asset != asset_name\n            or current_instance[\"task\"] != task_name\n        ):\n            # Update instance context if is not the same\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                variant, task_name, asset_doc, project_name, host_name\n            )\n            if AYON_SERVER_ENABLED:\n                current_instance[\"folderPath\"] = asset_name\n            else:\n                current_instance[\"asset\"] = asset_name\n            current_instance[\"task\"] = task_name\n            current_instance[\"subset\"] = subset_name\n\n        # write workfile information to context container.\n        op_ctx = hou.node(CONTEXT_CONTAINER)\n        if not op_ctx:\n            op_ctx = self.create_context_node()\n\n        workfile_data = {\"workfile\": current_instance.data_to_store()}\n        imprint(op_ctx, workfile_data)\n\n    def collect_instances(self):\n        op_ctx = hou.node(CONTEXT_CONTAINER)\n        instance = read(op_ctx)\n        if not instance:\n            return\n        workfile = instance.get(\"workfile\")\n        if not workfile:\n            return\n        created_instance = CreatedInstance.from_existing(\n            workfile, self\n        )\n        self._add_instance_to_context(created_instance)\n\n    def update_instances(self, update_list):\n        op_ctx = hou.node(CONTEXT_CONTAINER)\n        for created_inst, _changes in update_list:\n            if created_inst[\"creator_identifier\"] == self.identifier:\n                workfile_data = {\"workfile\": created_inst.data_to_store()}\n                imprint(op_ctx, workfile_data, update=True)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py",
    "content": "from openpype.pipeline import InventoryAction\nfrom openpype.hosts.houdini.api.lib import (\n    get_camera_from_container,\n    set_camera_resolution\n)\nfrom openpype.pipeline.context_tools import get_current_project_asset\n\n\nclass SetCameraResolution(InventoryAction):\n\n    label = \"Set Camera Resolution\"\n    icon = \"desktop\"\n    color = \"orange\"\n\n    @staticmethod\n    def is_compatible(container):\n        return (\n            container.get(\"loader\") == \"CameraLoader\"\n        )\n\n    def process(self, containers):\n        asset_doc = get_current_project_asset()\n        for container in containers:\n            node = container[\"node\"]\n            camera = get_camera_from_container(node)\n            set_camera_resolution(camera, asset_doc)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/actions.py",
    "content": "\"\"\"A module containing generic loader actions that will display in the Loader.\n\n\"\"\"\n\nfrom openpype.pipeline import load\n\n\nclass SetFrameRangeLoader(load.LoaderPlugin):\n    \"\"\"Set frame range excluding pre- and post-handles\"\"\"\n\n    families = [\n        \"animation\",\n        \"camera\",\n        \"pointcache\",\n        \"vdbcache\",\n        \"usd\",\n    ]\n    representations = [\"abc\", \"vdb\", \"usd\"]\n\n    label = \"Set frame range\"\n    order = 11\n    icon = \"clock-o\"\n    color = \"white\"\n\n    def load(self, context, name, namespace, data):\n\n        import hou\n\n        version = context[\"version\"]\n        version_data = version.get(\"data\", {})\n\n        start = version_data.get(\"frameStart\", None)\n        end = version_data.get(\"frameEnd\", None)\n\n        if start is None or end is None:\n            print(\n                \"Skipping setting frame range because start or \"\n                \"end frame data is missing..\"\n            )\n            return\n\n        hou.playbar.setFrameRange(start, end)\n        hou.playbar.setPlaybackRange(start, end)\n\n\nclass SetFrameRangeWithHandlesLoader(load.LoaderPlugin):\n    \"\"\"Set frame range including pre- and post-handles\"\"\"\n\n    families = [\n        \"animation\",\n        \"camera\",\n        \"pointcache\",\n        \"vdbcache\",\n        \"usd\",\n    ]\n    representations = [\"abc\", \"vdb\", \"usd\"]\n\n    label = \"Set frame range (with handles)\"\n    order = 12\n    icon = \"clock-o\"\n    color = \"white\"\n\n    def load(self, context, name, namespace, data):\n\n        import hou\n\n        version = context[\"version\"]\n        version_data = version.get(\"data\", {})\n\n        start = version_data.get(\"frameStart\", None)\n        end = version_data.get(\"frameEnd\", None)\n\n        if start is None or end is None:\n            print(\n                \"Skipping setting frame range because start or \"\n                \"end frame data is missing..\"\n            )\n            return\n\n        # Include handles\n        start -= version_data.get(\"handleStart\", 0)\n        end += version_data.get(\"handleEnd\", 0)\n\n        hou.playbar.setFrameRange(start, end)\n        hou.playbar.setPlaybackRange(start, end)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/load_alembic.py",
    "content": "import os\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.houdini.api import pipeline\n\n\nclass AbcLoader(load.LoaderPlugin):\n    \"\"\"Load Alembic\"\"\"\n\n    families = [\"model\", \"animation\", \"pointcache\", \"gpuCache\"]\n    label = \"Load Alembic\"\n    representations = [\"abc\"]\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        import hou\n\n        # Format file name, Houdini only wants forward slashes\n        file_path = self.filepath_from_context(context)\n        file_path = os.path.normpath(file_path)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        # Get the root node\n        obj = hou.node(\"/obj\")\n\n        # Define node name\n        namespace = namespace if namespace else context[\"asset\"][\"name\"]\n        node_name = \"{}_{}\".format(namespace, name) if namespace else name\n\n        # Create a new geo node\n        container = obj.createNode(\"geo\", node_name=node_name)\n\n        # Remove the file node, it only loads static meshes\n        # Houdini 17 has removed the file node from the geo node\n        file_node = container.node(\"file1\")\n        if file_node:\n            file_node.destroy()\n\n        # Create an alembic node (supports animation)\n        alembic = container.createNode(\"alembic\", node_name=node_name)\n        alembic.setParms({\"fileName\": file_path})\n\n        # Add unpack node\n        unpack_name = \"unpack_{}\".format(name)\n        unpack = container.createNode(\"unpack\", node_name=unpack_name)\n        unpack.setInput(0, alembic)\n        unpack.setParms({\"transfer_attributes\": \"path\"})\n\n        # Add normal to points\n        # Order of menu ['point', 'vertex', 'prim', 'detail']\n        normal_name = \"normal_{}\".format(name)\n        normal_node = container.createNode(\"normal\", node_name=normal_name)\n        normal_node.setParms({\"type\": 0})\n\n        normal_node.setInput(0, unpack)\n\n        null = container.createNode(\"null\", node_name=\"OUT\".format(name))\n        null.setInput(0, normal_node)\n\n        # Ensure display flag is on the Alembic input node and not on the OUT\n        # node to optimize \"debug\" displaying in the viewport.\n        alembic.setDisplayFlag(True)\n\n        # Set new position for unpack node else it gets cluttered\n        nodes = [container, alembic, unpack, normal_node, null]\n        for nr, node in enumerate(nodes):\n            node.setPosition([0, (0 - nr)])\n\n        self[:] = nodes\n\n        return pipeline.containerise(\n            node_name,\n            namespace,\n            nodes,\n            context,\n            self.__class__.__name__,\n            suffix=\"\",\n        )\n\n    def update(self, container, representation):\n\n        node = container[\"node\"]\n        try:\n            alembic_node = next(\n                n for n in node.children() if n.type().name() == \"alembic\"\n            )\n        except StopIteration:\n            self.log.error(\"Could not find node of type `alembic`\")\n            return\n\n        # Update the file path\n        file_path = get_representation_path(representation)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        alembic_node.setParms({\"fileName\": file_path})\n\n        # Update attribute\n        node.setParms({\"representation\": str(representation[\"_id\"])})\n\n    def remove(self, container):\n\n        node = container[\"node\"]\n        node.destroy()\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/load_alembic_archive.py",
    "content": "import os\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.houdini.api import pipeline\n\n\nclass AbcArchiveLoader(load.LoaderPlugin):\n    \"\"\"Load Alembic as full geometry network hierarchy \"\"\"\n\n    families = [\"model\", \"animation\", \"pointcache\", \"gpuCache\"]\n    label = \"Load Alembic as Archive\"\n    representations = [\"abc\"]\n    order = -5\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n\n        import hou\n\n        # Format file name, Houdini only wants forward slashes\n        file_path = self.filepath_from_context(context)\n        file_path = os.path.normpath(file_path)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        # Get the root node\n        obj = hou.node(\"/obj\")\n\n        # Define node name\n        namespace = namespace if namespace else context[\"asset\"][\"name\"]\n        node_name = \"{}_{}\".format(namespace, name) if namespace else name\n\n        # Create an Alembic archive node\n        node = obj.createNode(\"alembicarchive\", node_name=node_name)\n        node.moveToGoodPosition()\n\n        # TODO: add FPS of project / asset\n        node.setParms({\"fileName\": file_path,\n                       \"channelRef\": True})\n\n        # Apply some magic\n        node.parm(\"buildHierarchy\").pressButton()\n        node.moveToGoodPosition()\n\n        nodes = [node]\n\n        self[:] = nodes\n\n        return pipeline.containerise(node_name,\n                                     namespace,\n                                     nodes,\n                                     context,\n                                     self.__class__.__name__,\n                                     suffix=\"\")\n\n    def update(self, container, representation):\n\n        node = container[\"node\"]\n\n        # Update the file path\n        file_path = get_representation_path(representation)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        # Update attributes\n        node.setParms({\"fileName\": file_path,\n                       \"representation\": str(representation[\"_id\"])})\n\n        # Rebuild\n        node.parm(\"buildHierarchy\").pressButton()\n\n    def remove(self, container):\n\n        node = container[\"node\"]\n        node.destroy()\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/load_ass.py",
    "content": "import os\nimport re\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.houdini.api import pipeline\n\n\nclass AssLoader(load.LoaderPlugin):\n    \"\"\"Load .ass with Arnold Procedural\"\"\"\n\n    families = [\"ass\"]\n    label = \"Load Arnold Procedural\"\n    representations = [\"ass\"]\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        import hou\n\n        # Get the root node\n        obj = hou.node(\"/obj\")\n\n        # Define node name\n        namespace = namespace if namespace else context[\"asset\"][\"name\"]\n        node_name = \"{}_{}\".format(namespace, name) if namespace else name\n\n        # Create a new geo node\n        procedural = obj.createNode(\"arnold::procedural\", node_name=node_name)\n\n        procedural.setParms(\n            {\n                \"ar_filename\": self.format_path(context[\"representation\"])\n            })\n\n        nodes = [procedural]\n        self[:] = nodes\n\n        return pipeline.containerise(\n            node_name,\n            namespace,\n            nodes,\n            context,\n            self.__class__.__name__,\n            suffix=\"\",\n        )\n\n    def update(self, container, representation):\n        # Update the file path\n        procedural = container[\"node\"]\n        procedural.setParms({\"ar_filename\": self.format_path(representation)})\n\n        # Update attribute\n        procedural.setParms({\"representation\": str(representation[\"_id\"])})\n\n    def remove(self, container):\n        node = container[\"node\"]\n        node.destroy()\n\n    @staticmethod\n    def format_path(representation):\n        \"\"\"Format file path correctly for single ass.* or ass.* sequence.\n\n        Args:\n            representation (dict): representation to be loaded.\n\n        Returns:\n             str: Formatted path to be used by the input node.\n\n        \"\"\"\n        path = get_representation_path(representation)\n        if not os.path.exists(path):\n            raise RuntimeError(\"Path does not exist: {}\".format(path))\n\n        is_sequence = bool(representation[\"context\"].get(\"frame\"))\n        # The path is either a single file or sequence in a folder.\n        if is_sequence:\n            dir_path, file_name = os.path.split(path)\n            path = os.path.join(\n                dir_path,\n                re.sub(r\"(.*)\\.(\\d+)\\.(ass.*)\", \"\\\\1.$F4.\\\\3\", file_name)\n            )\n\n        return os.path.normpath(path).replace(\"\\\\\", \"/\")\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/load_bgeo.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nimport re\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.houdini.api import pipeline\n\n\nclass BgeoLoader(load.LoaderPlugin):\n    \"\"\"Load bgeo files to Houdini.\"\"\"\n\n    label = \"Load bgeo\"\n    families = [\"model\", \"pointcache\", \"bgeo\"]\n    representations = [\n        \"bgeo\", \"bgeosc\", \"bgeogz\",\n        \"bgeo.sc\", \"bgeo.gz\", \"bgeo.lzma\", \"bgeo.bz2\"]\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n\n        import hou\n\n        # Get the root node\n        obj = hou.node(\"/obj\")\n\n        # Define node name\n        namespace = namespace if namespace else context[\"asset\"][\"name\"]\n        node_name = \"{}_{}\".format(namespace, name) if namespace else name\n\n        # Create a new geo node\n        container = obj.createNode(\"geo\", node_name=node_name)\n\n        # Remove the file node, it only loads static meshes\n        # Houdini 17 has removed the file node from the geo node\n        file_node = container.node(\"file1\")\n        if file_node:\n            file_node.destroy()\n\n        # Explicitly create a file node\n        path = self.filepath_from_context(context)\n        file_node = container.createNode(\"file\", node_name=node_name)\n        file_node.setParms(\n            {\"file\": self.format_path(path, context[\"representation\"])})\n\n        # Set display on last node\n        file_node.setDisplayFlag(True)\n\n        nodes = [container, file_node]\n        self[:] = nodes\n\n        return pipeline.containerise(\n            node_name,\n            namespace,\n            nodes,\n            context,\n            self.__class__.__name__,\n            suffix=\"\",\n        )\n\n    @staticmethod\n    def format_path(path, representation):\n        \"\"\"Format file path correctly for single bgeo or bgeo sequence.\"\"\"\n        if not os.path.exists(path):\n            raise RuntimeError(\"Path does not exist: %s\" % path)\n\n        is_sequence = bool(representation[\"context\"].get(\"frame\"))\n        # The path is either a single file or sequence in a folder.\n        if not is_sequence:\n            filename = path\n        else:\n            filename = re.sub(r\"(.*)\\.(\\d+)\\.(bgeo.*)\", \"\\\\1.$F4.\\\\3\", path)\n\n            filename = os.path.join(path, filename)\n\n        filename = os.path.normpath(filename)\n        filename = filename.replace(\"\\\\\", \"/\")\n\n        return filename\n\n    def update(self, container, representation):\n\n        node = container[\"node\"]\n        try:\n            file_node = next(\n                n for n in node.children() if n.type().name() == \"file\"\n            )\n        except StopIteration:\n            self.log.error(\"Could not find node of type `alembic`\")\n            return\n\n        # Update the file path\n        file_path = get_representation_path(representation)\n        file_path = self.format_path(file_path, representation)\n\n        file_node.setParms({\"file\": file_path})\n\n        # Update attribute\n        node.setParms({\"representation\": str(representation[\"_id\"])})\n\n    def remove(self, container):\n\n        node = container[\"node\"]\n        node.destroy()\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/load_camera.py",
    "content": "from openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.houdini.api import pipeline\n\nfrom openpype.hosts.houdini.api.lib import (\n    set_camera_resolution,\n    get_camera_from_container\n)\n\nimport hou\n\n\nARCHIVE_EXPRESSION = ('__import__(\"_alembic_hom_extensions\")'\n                      '.alembicGetCameraDict')\n\n\ndef transfer_non_default_values(src, dest, ignore=None):\n    \"\"\"Copy parm from src to dest.\n\n    Because the Alembic Archive rebuilds the entire node\n    hierarchy on triggering \"Build Hierarchy\" we want to\n    preserve any local tweaks made by the user on the camera\n    for ease of use. That could be a background image, a\n    resolution change or even Redshift camera parameters.\n\n    We try to do so by finding all Parms that exist on both\n    source and destination node, include only those that both\n    are not at their default value, they must be visible,\n    we exclude those that have the special \"alembic archive\"\n    channel expression and ignore certain Parm types.\n\n    \"\"\"\n\n    ignore_types = {\n        hou.parmTemplateType.Toggle,\n        hou.parmTemplateType.Menu,\n        hou.parmTemplateType.Button,\n        hou.parmTemplateType.FolderSet,\n        hou.parmTemplateType.Separator,\n        hou.parmTemplateType.Label,\n    }\n\n    src.updateParmStates()\n\n    for parm in src.allParms():\n\n        if ignore and parm.name() in ignore:\n            continue\n\n        # If destination parm does not exist, ignore..\n        dest_parm = dest.parm(parm.name())\n        if not dest_parm:\n            continue\n\n        # Ignore values that are currently at default\n        if parm.isAtDefault() and dest_parm.isAtDefault():\n            continue\n\n        if not parm.isVisible():\n            # Ignore hidden parameters, assume they\n            # are implementation details\n            continue\n\n        expression = None\n        try:\n            expression = parm.expression()\n        except hou.OperationFailed:\n            # No expression present\n            pass\n\n        if expression is not None and ARCHIVE_EXPRESSION in expression:\n            # Assume it's part of the automated connections that the\n            # Alembic Archive makes on loading of the camera and thus we do\n            # not want to transfer the expression\n            continue\n\n        # Ignore folders, separators, etc.\n        if parm.parmTemplate().type() in ignore_types:\n            continue\n\n        print(\"Preserving attribute: %s\" % parm.name())\n        dest_parm.setFromParm(parm)\n\n\nclass CameraLoader(load.LoaderPlugin):\n    \"\"\"Load camera from an Alembic file\"\"\"\n\n    families = [\"camera\"]\n    label = \"Load Camera (abc)\"\n    representations = [\"abc\"]\n    order = -10\n\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n\n        # Format file name, Houdini only wants forward slashes\n        file_path = self.filepath_from_context(context).replace(\"\\\\\", \"/\")\n\n        # Get the root node\n        obj = hou.node(\"/obj\")\n\n        # Define node name\n        namespace = namespace if namespace else context[\"asset\"][\"name\"]\n        node_name = \"{}_{}\".format(namespace, name) if namespace else name\n\n        # Create a archive node\n        node = self.create_and_connect(obj, \"alembicarchive\", node_name)\n\n        # TODO: add FPS of project / asset\n        node.setParms({\"fileName\": file_path, \"channelRef\": True})\n\n        # Apply some magic\n        node.parm(\"buildHierarchy\").pressButton()\n        node.moveToGoodPosition()\n\n        # Create an alembic xform node\n        nodes = [node]\n\n        camera = get_camera_from_container(node)\n        self._match_maya_render_mask(camera)\n        set_camera_resolution(camera, asset_doc=context[\"asset\"])\n        self[:] = nodes\n\n        return pipeline.containerise(node_name,\n                                     namespace,\n                                     nodes,\n                                     context,\n                                     self.__class__.__name__,\n                                     suffix=\"\")\n\n    def update(self, container, representation):\n\n        node = container[\"node\"]\n\n        # Update the file path\n        file_path = get_representation_path(representation)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        # Update attributes\n        node.setParms({\"fileName\": file_path,\n                       \"representation\": str(representation[\"_id\"])})\n\n        # Store the cam temporarily next to the Alembic Archive\n        # so that we can preserve parm values the user set on it\n        # after build hierarchy was triggered.\n        old_camera = get_camera_from_container(node)\n        temp_camera = old_camera.copyTo(node.parent())\n\n        # Rebuild\n        node.parm(\"buildHierarchy\").pressButton()\n\n        # Apply values to the new camera\n        new_camera = get_camera_from_container(node)\n        transfer_non_default_values(temp_camera,\n                                    new_camera,\n                                    # The hidden uniform scale attribute\n                                    # gets a default connection to\n                                    # \"icon_scale\" just skip that completely\n                                    ignore={\"scale\"})\n\n        self._match_maya_render_mask(new_camera)\n        set_camera_resolution(new_camera)\n\n        temp_camera.destroy()\n\n    def remove(self, container):\n\n        node = container[\"node\"]\n        node.destroy()\n\n    def create_and_connect(self, node, node_type, name=None):\n        \"\"\"Create a node within a node which and connect it to the input\n\n        Args:\n            node(hou.Node): parent of the new node\n            node_type(str) name of the type of node, eg: 'alembic'\n            name(str, Optional): name of the node\n\n        Returns:\n            hou.Node\n\n        \"\"\"\n        if name:\n            new_node = node.createNode(node_type, node_name=name)\n        else:\n            new_node = node.createNode(node_type)\n\n        new_node.moveToGoodPosition()\n        return new_node\n\n    def _match_maya_render_mask(self, camera):\n        \"\"\"Workaround to match Maya render mask in Houdini\"\"\"\n\n        # print(\"Setting match maya render mask \")\n        parm = camera.parm(\"aperture\")\n        expression = parm.expression()\n        expression = expression.replace(\"return \", \"aperture = \")\n        expression += \"\"\"\n# Match maya render mask (logic from Houdini's own FBX importer)\nnode = hou.pwd()\nresx = node.evalParm('resx')\nresy = node.evalParm('resy')\naspect = node.evalParm('aspect')\naperture *= min(1, (resx / resy * aspect) / 1.5)\nreturn aperture\n\"\"\"\n        parm.setExpression(expression, language=hou.exprLanguage.Python)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/load_fbx.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Fbx Loader for houdini. \"\"\"\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.houdini.api import pipeline\n\n\nclass FbxLoader(load.LoaderPlugin):\n    \"\"\"Load fbx files. \"\"\"\n\n    label = \"Load FBX\"\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    order = -10\n\n    families = [\"*\"]\n    representations = [\"*\"]\n    extensions = {\"fbx\"}\n\n    def load(self, context, name=None, namespace=None, data=None):\n\n        # get file path from context\n        file_path = self.filepath_from_context(context)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        # get necessary data\n        namespace, node_name = self.get_node_name(context, name, namespace)\n\n        # create load tree\n        nodes = self.create_load_node_tree(file_path, node_name, name)\n\n        self[:] = nodes\n\n        # Call containerise function which does some automations for you\n        #  like moving created nodes to the AVALON_CONTAINERS subnetwork\n        containerised_nodes = pipeline.containerise(\n            node_name,\n            namespace,\n            nodes,\n            context,\n            self.__class__.__name__,\n            suffix=\"\",\n        )\n\n        return containerised_nodes\n\n    def update(self, container, representation):\n\n        node = container[\"node\"]\n        try:\n            file_node = next(\n                n for n in node.children() if n.type().name() == \"file\"\n            )\n        except StopIteration:\n            self.log.error(\"Could not find node of type `file`\")\n            return\n\n        # Update the file path from representation\n        file_path = get_representation_path(representation)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        file_node.setParms({\"file\": file_path})\n\n        # Update attribute\n        node.setParms({\"representation\": str(representation[\"_id\"])})\n\n    def remove(self, container):\n\n        node = container[\"node\"]\n        node.destroy()\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def get_node_name(self, context, name=None, namespace=None):\n        \"\"\"Define node name.\"\"\"\n\n        if not namespace:\n            namespace = context[\"asset\"][\"name\"]\n\n        if namespace:\n            node_name = \"{}_{}\".format(namespace, name)\n        else:\n            node_name = name\n\n        return namespace, node_name\n\n    def create_load_node_tree(self, file_path, node_name, subset_name):\n        \"\"\"Create Load network.\n\n        you can start building your tree at any obj level.\n        it'll be much easier to build it in the root obj level.\n\n        Afterwards, your tree will be automatically moved to\n        '/obj/AVALON_CONTAINERS' subnetwork.\n        \"\"\"\n        import hou\n\n        # Get the root obj level\n        obj = hou.node(\"/obj\")\n\n        # Create a new obj geo node\n        parent_node = obj.createNode(\"geo\", node_name=node_name)\n\n        # In older houdini,\n        # when reating a new obj geo node, a default file node will be\n        # automatically created.\n        # so, we will delete it if exists.\n        file_node = parent_node.node(\"file1\")\n        if file_node:\n            file_node.destroy()\n\n        # Create a new file node\n        file_node = parent_node.createNode(\"file\", node_name=node_name)\n        file_node.setParms({\"file\": file_path})\n\n        # Create attribute delete\n        attribdelete_name = \"attribdelete_{}\".format(subset_name)\n        attribdelete = parent_node.createNode(\"attribdelete\",\n                                              node_name=attribdelete_name)\n        attribdelete.setParms({\"ptdel\": \"fbx_*\"})\n        attribdelete.setInput(0, file_node)\n\n        # Create a Null node\n        null_name = \"OUT_{}\".format(subset_name)\n        null = parent_node.createNode(\"null\", node_name=null_name)\n        null.setInput(0, attribdelete)\n\n        # Ensure display flag is on the file_node input node and not on the OUT\n        # node to optimize \"debug\" displaying in the viewport.\n        file_node.setDisplayFlag(True)\n\n        # Set new position for children nodes\n        parent_node.layoutChildren()\n\n        # Return all the nodes\n        return [parent_node, file_node, attribdelete, null]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/load_hda.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.houdini.api import pipeline\n\n\nclass HdaLoader(load.LoaderPlugin):\n    \"\"\"Load Houdini Digital Asset file.\"\"\"\n\n    families = [\"hda\"]\n    label = \"Load Hda\"\n    representations = [\"hda\"]\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        import hou\n\n        # Format file name, Houdini only wants forward slashes\n        file_path = self.filepath_from_context(context)\n        file_path = os.path.normpath(file_path)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        # Get the root node\n        obj = hou.node(\"/obj\")\n\n        # Create a unique name\n        counter = 1\n        namespace = namespace or context[\"asset\"][\"name\"]\n        formatted = \"{}_{}\".format(namespace, name) if namespace else name\n        node_name = \"{0}_{1:03d}\".format(formatted, counter)\n\n        hou.hda.installFile(file_path)\n        hda_node = obj.createNode(name, node_name)\n\n        self[:] = [hda_node]\n\n        return pipeline.containerise(\n            node_name,\n            namespace,\n            [hda_node],\n            context,\n            self.__class__.__name__,\n            suffix=\"\",\n        )\n\n    def update(self, container, representation):\n        import hou\n\n        hda_node = container[\"node\"]\n        file_path = get_representation_path(representation)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n        hou.hda.installFile(file_path)\n        defs = hda_node.type().allInstalledDefinitions()\n        def_paths = [d.libraryFilePath() for d in defs]\n        new = def_paths.index(file_path)\n        defs[new].setIsPreferred(True)\n        hda_node.setParms({\n            \"representation\": str(representation[\"_id\"])\n        })\n\n    def remove(self, container):\n        node = container[\"node\"]\n        node.destroy()\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/load_image.py",
    "content": "import os\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.hosts.houdini.api import lib, pipeline\n\nimport hou\n\n\ndef get_image_avalon_container():\n    \"\"\"The COP2 files must be in a COP2 network.\n\n    So we maintain a single entry point within AVALON_CONTAINERS,\n    just for ease of use.\n\n    \"\"\"\n\n    path = pipeline.AVALON_CONTAINERS\n    avalon_container = hou.node(path)\n    if not avalon_container:\n        # Let's create avalon container secretly\n        # but make sure the pipeline still is built the\n        # way we anticipate it was built, asserting it.\n        assert path == \"/obj/AVALON_CONTAINERS\"\n\n        parent = hou.node(\"/obj\")\n        avalon_container = parent.createNode(\n            \"subnet\", node_name=\"AVALON_CONTAINERS\"\n        )\n\n    image_container = hou.node(path + \"/IMAGES\")\n    if not image_container:\n        image_container = avalon_container.createNode(\n            \"cop2net\", node_name=\"IMAGES\"\n        )\n        image_container.moveToGoodPosition()\n\n    return image_container\n\n\nclass ImageLoader(load.LoaderPlugin):\n    \"\"\"Load images into COP2\"\"\"\n\n    families = [\"imagesequence\"]\n    label = \"Load Image (COP2)\"\n    representations = [\"*\"]\n    order = -10\n\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n\n        # Format file name, Houdini only wants forward slashes\n        file_path = self.filepath_from_context(context)\n        file_path = os.path.normpath(file_path)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n        file_path = self._get_file_sequence(file_path)\n\n        # Get the root node\n        parent = get_image_avalon_container()\n\n        # Define node name\n        namespace = namespace if namespace else context[\"asset\"][\"name\"]\n        node_name = \"{}_{}\".format(namespace, name) if namespace else name\n\n        node = parent.createNode(\"file\", node_name=node_name)\n        node.moveToGoodPosition()\n\n        node.setParms({\"filename1\": file_path})\n\n        # Imprint it manually\n        data = {\n            \"schema\": \"openpype:container-2.0\",\n            \"id\": AVALON_CONTAINER_ID,\n            \"name\": node_name,\n            \"namespace\": namespace,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": str(context[\"representation\"][\"_id\"]),\n        }\n\n        # todo: add folder=\"Avalon\"\n        lib.imprint(node, data)\n\n        return node\n\n    def update(self, container, representation):\n\n        node = container[\"node\"]\n\n        # Update the file path\n        file_path = get_representation_path(representation)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n        file_path = self._get_file_sequence(file_path)\n\n        # Update attributes\n        node.setParms(\n            {\n                \"filename1\": file_path,\n                \"representation\": str(representation[\"_id\"]),\n            }\n        )\n\n    def remove(self, container):\n\n        node = container[\"node\"]\n\n        # Let's clean up the IMAGES COP2 network\n        # if it ends up being empty and we deleted\n        # the last file node. Store the parent\n        # before we delete the node.\n        parent = node.parent()\n\n        node.destroy()\n\n        if not parent.children():\n            parent.destroy()\n\n    def _get_file_sequence(self, file_path):\n        root = os.path.dirname(file_path)\n        files = sorted(os.listdir(root))\n\n        first_fname = files[0]\n        prefix, padding, suffix = first_fname.rsplit(\".\", 2)\n        fname = \".\".join([prefix, \"$F{}\".format(len(padding)), suffix])\n        return os.path.join(root, fname).replace(\"\\\\\", \"/\")\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/load_redshift_proxy.py",
    "content": "import os\nimport re\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.houdini.api import pipeline\nfrom openpype.pipeline.load import LoadError\n\nimport hou\n\n\nclass RedshiftProxyLoader(load.LoaderPlugin):\n    \"\"\"Load Redshift Proxy\"\"\"\n\n    families = [\"redshiftproxy\"]\n    label = \"Load Redshift Proxy\"\n    representations = [\"rs\"]\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n\n        # Get the root node\n        obj = hou.node(\"/obj\")\n\n        # Define node name\n        namespace = namespace if namespace else context[\"asset\"][\"name\"]\n        node_name = \"{}_{}\".format(namespace, name) if namespace else name\n\n        # Create a new geo node\n        container = obj.createNode(\"geo\", node_name=node_name)\n\n        # Check whether the Redshift parameters exist - if not, then likely\n        # redshift is not set up or initialized correctly\n        if not container.parm(\"RS_objprop_proxy_enable\"):\n            container.destroy()\n            raise LoadError(\"Unable to initialize geo node with Redshift \"\n                            \"attributes. Make sure you have the Redshift \"\n                            \"plug-in set up correctly for Houdini.\")\n\n        # Enable by default\n        container.setParms({\n            \"RS_objprop_proxy_enable\": True,\n            \"RS_objprop_proxy_file\": self.format_path(\n                self.filepath_from_context(context),\n                context[\"representation\"])\n        })\n\n        # Remove the file node, it only loads static meshes\n        # Houdini 17 has removed the file node from the geo node\n        file_node = container.node(\"file1\")\n        if file_node:\n            file_node.destroy()\n\n        # Add this stub node inside so it previews ok\n        proxy_sop = container.createNode(\"redshift_proxySOP\",\n                                         node_name=node_name)\n        proxy_sop.setDisplayFlag(True)\n\n        nodes = [container, proxy_sop]\n\n        self[:] = nodes\n\n        return pipeline.containerise(\n            node_name,\n            namespace,\n            nodes,\n            context,\n            self.__class__.__name__,\n            suffix=\"\",\n        )\n\n    def update(self, container, representation):\n\n        # Update the file path\n        file_path = get_representation_path(representation)\n\n        node = container[\"node\"]\n        node.setParms({\n            \"RS_objprop_proxy_file\": self.format_path(\n                file_path, representation)\n        })\n\n        # Update attribute\n        node.setParms({\"representation\": str(representation[\"_id\"])})\n\n    def remove(self, container):\n\n        node = container[\"node\"]\n        node.destroy()\n\n    @staticmethod\n    def format_path(path, representation):\n        \"\"\"Format file path correctly for single redshift proxy\n        or redshift proxy sequence.\"\"\"\n        if not os.path.exists(path):\n            raise RuntimeError(\"Path does not exist: %s\" % path)\n\n        is_sequence = bool(representation[\"context\"].get(\"frame\"))\n        # The path is either a single file or sequence in a folder.\n        if is_sequence:\n            filename = re.sub(r\"(.*)\\.(\\d+)\\.(rs.*)\", \"\\\\1.$F4.\\\\3\", path)\n            filename = os.path.join(path, filename)\n        else:\n            filename = path\n\n        filename = os.path.normpath(filename)\n        filename = filename.replace(\"\\\\\", \"/\")\n\n        return filename\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/load_usd_layer.py",
    "content": "from openpype.pipeline import (\n    load,\n    get_representation_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.hosts.houdini.api import lib\n\n\nclass USDSublayerLoader(load.LoaderPlugin):\n    \"\"\"Sublayer USD file in Solaris\"\"\"\n\n    families = [\n        \"usd\",\n        \"usdCamera\",\n    ]\n    label = \"Sublayer USD\"\n    representations = [\"usd\", \"usda\", \"usdlc\", \"usdnc\", \"abc\"]\n    order = 1\n\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n\n        import os\n        import hou\n\n        # Format file name, Houdini only wants forward slashes\n        file_path = self.filepath_from_context(context)\n        file_path = os.path.normpath(file_path)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        # Get the root node\n        stage = hou.node(\"/stage\")\n\n        # Define node name\n        namespace = namespace if namespace else context[\"asset\"][\"name\"]\n        node_name = \"{}_{}\".format(namespace, name) if namespace else name\n\n        # Create USD reference\n        container = stage.createNode(\"sublayer\", node_name=node_name)\n        container.setParms({\"filepath1\": file_path})\n        container.moveToGoodPosition()\n\n        # Imprint it manually\n        data = {\n            \"schema\": \"openpype:container-2.0\",\n            \"id\": AVALON_CONTAINER_ID,\n            \"name\": node_name,\n            \"namespace\": namespace,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": str(context[\"representation\"][\"_id\"]),\n        }\n\n        # todo: add folder=\"Avalon\"\n        lib.imprint(container, data)\n\n        return container\n\n    def update(self, container, representation):\n\n        node = container[\"node\"]\n\n        # Update the file path\n        file_path = get_representation_path(representation)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        # Update attributes\n        node.setParms(\n            {\n                \"filepath1\": file_path,\n                \"representation\": str(representation[\"_id\"]),\n            }\n        )\n\n        # Reload files\n        node.parm(\"reload\").pressButton()\n\n    def remove(self, container):\n\n        node = container[\"node\"]\n        node.destroy()\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/load_usd_reference.py",
    "content": "from openpype.pipeline import (\n    load,\n    get_representation_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.hosts.houdini.api import lib\n\n\nclass USDReferenceLoader(load.LoaderPlugin):\n    \"\"\"Reference USD file in Solaris\"\"\"\n\n    families = [\n        \"usd\",\n        \"usdCamera\",\n    ]\n    label = \"Reference USD\"\n    representations = [\"usd\", \"usda\", \"usdlc\", \"usdnc\", \"abc\"]\n    order = -8\n\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n\n        import os\n        import hou\n\n        # Format file name, Houdini only wants forward slashes\n        file_path = self.filepath_from_context(context)\n        file_path = os.path.normpath(file_path)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        # Get the root node\n        stage = hou.node(\"/stage\")\n\n        # Define node name\n        namespace = namespace if namespace else context[\"asset\"][\"name\"]\n        node_name = \"{}_{}\".format(namespace, name) if namespace else name\n\n        # Create USD reference\n        container = stage.createNode(\"reference\", node_name=node_name)\n        container.setParms({\"filepath1\": file_path})\n        container.moveToGoodPosition()\n\n        # Imprint it manually\n        data = {\n            \"schema\": \"openpype:container-2.0\",\n            \"id\": AVALON_CONTAINER_ID,\n            \"name\": node_name,\n            \"namespace\": namespace,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": str(context[\"representation\"][\"_id\"]),\n        }\n\n        # todo: add folder=\"Avalon\"\n        lib.imprint(container, data)\n\n        return container\n\n    def update(self, container, representation):\n\n        node = container[\"node\"]\n\n        # Update the file path\n        file_path = get_representation_path(representation)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        # Update attributes\n        node.setParms(\n            {\n                \"filepath1\": file_path,\n                \"representation\": str(representation[\"_id\"]),\n            }\n        )\n\n        # Reload files\n        node.parm(\"reload\").pressButton()\n\n    def remove(self, container):\n\n        node = container[\"node\"]\n        node.destroy()\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/load_vdb.py",
    "content": "import os\nimport re\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.houdini.api import pipeline\n\n\nclass VdbLoader(load.LoaderPlugin):\n    \"\"\"Load VDB\"\"\"\n\n    families = [\"vdbcache\"]\n    label = \"Load VDB\"\n    representations = [\"vdb\"]\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n\n        import hou\n\n        # Get the root node\n        obj = hou.node(\"/obj\")\n\n        # Define node name\n        namespace = namespace if namespace else context[\"asset\"][\"name\"]\n        node_name = \"{}_{}\".format(namespace, name) if namespace else name\n\n        # Create a new geo node\n        container = obj.createNode(\"geo\", node_name=node_name)\n\n        # Remove the file node, it only loads static meshes\n        # Houdini 17 has removed the file node from the geo node\n        file_node = container.node(\"file1\")\n        if file_node:\n            file_node.destroy()\n\n        # Explicitly create a file node\n        file_node = container.createNode(\"file\", node_name=node_name)\n        path = self.filepath_from_context(context)\n        file_node.setParms(\n            {\"file\": self.format_path(path, context[\"representation\"])})\n\n        # Set display on last node\n        file_node.setDisplayFlag(True)\n\n        nodes = [container, file_node]\n        self[:] = nodes\n\n        return pipeline.containerise(\n            node_name,\n            namespace,\n            nodes,\n            context,\n            self.__class__.__name__,\n            suffix=\"\",\n        )\n\n    @staticmethod\n    def format_path(path, representation):\n        \"\"\"Format file path correctly for single vdb or vdb sequence.\"\"\"\n        if not os.path.exists(path):\n            raise RuntimeError(\"Path does not exist: %s\" % path)\n\n        is_sequence = bool(representation[\"context\"].get(\"frame\"))\n        # The path is either a single file or sequence in a folder.\n        if not is_sequence:\n            filename = path\n        else:\n            filename = re.sub(r\"(.*)\\.(\\d+)\\.vdb$\", \"\\\\1.$F4.vdb\", path)\n\n            filename = os.path.join(path, filename)\n\n        filename = os.path.normpath(filename)\n        filename = filename.replace(\"\\\\\", \"/\")\n\n        return filename\n\n    def update(self, container, representation):\n\n        node = container[\"node\"]\n        try:\n            file_node = next(\n                n for n in node.children() if n.type().name() == \"file\"\n            )\n        except StopIteration:\n            self.log.error(\"Could not find node of type `alembic`\")\n            return\n\n        # Update the file path\n        file_path = get_representation_path(representation)\n        file_path = self.format_path(file_path, representation)\n\n        file_node.setParms({\"file\": file_path})\n\n        # Update attribute\n        node.setParms({\"representation\": str(representation[\"_id\"])})\n\n    def remove(self, container):\n\n        node = container[\"node\"]\n        node.destroy()\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/load/show_usdview.py",
    "content": "import os\nimport platform\nimport subprocess\n\nfrom openpype.lib.vendor_bin_utils import find_executable\nfrom openpype.pipeline import load\n\n\nclass ShowInUsdview(load.LoaderPlugin):\n    \"\"\"Open USD file in usdview\"\"\"\n\n    label = \"Show in usdview\"\n    representations = [\"*\"]\n    families = [\"*\"]\n    extensions = {\"usd\", \"usda\", \"usdlc\", \"usdnc\", \"abc\"}\n    order = 15\n\n    icon = \"code-fork\"\n    color = \"white\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        from pathlib import Path\n\n        if platform.system() == \"Windows\":\n            executable = \"usdview.bat\"\n        else:\n            executable = \"usdview\"\n\n        usdview = find_executable(executable)\n        if not usdview:\n            raise RuntimeError(\"Unable to find usdview\")\n\n        # For some reason Windows can return the path like:\n        # C:/PROGRA~1/SIDEEF~1/HOUDIN~1.435/bin/usdview\n        # convert to resolved path so `subprocess` can take it\n        usdview = str(Path(usdview).resolve().as_posix())\n\n        filepath = self.filepath_from_context(context)\n        filepath = os.path.normpath(filepath)\n        filepath = filepath.replace(\"\\\\\", \"/\")\n\n        if not os.path.exists(filepath):\n            self.log.error(\"File does not exist: %s\" % filepath)\n            return\n\n        self.log.info(\"Start houdini variant of usdview...\")\n\n        subprocess.Popen([usdview, filepath, \"--renderer\", \"GL\"])\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_active_state.py",
    "content": "import pyblish.api\nimport hou\n\n\nclass CollectInstanceActiveState(pyblish.api.InstancePlugin):\n    \"\"\"Collect default active state for instance from its node bypass state.\n\n    This is done at the very end of the CollectorOrder so that any required\n    collecting of data iterating over instances (with InstancePlugin) will\n    actually collect the data for when the user enables the state in the UI.\n    Otherwise potentially required data might have skipped collecting.\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.299\n    families = [\"*\"]\n    hosts = [\"houdini\"]\n    label = \"Instance Active State\"\n\n    def process(self, instance):\n\n        # Must have node to check for bypass state\n        if len(instance) == 0:\n            return\n\n        # Check bypass state and reverse\n        active = True\n        node = hou.node(instance.data.get(\"instance_node\"))\n        if hasattr(node, \"isBypassed\"):\n            active = not node.isBypassed()\n\n        # Set instance active state\n        instance.data.update(\n            {\n                \"active\": active,\n                # temporarily translation of `active` to `publish` till\n                # issue has been resolved:\n                # https://github.com/pyblish/pyblish-base/issues/307\n                \"publish\": active,\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py",
    "content": "import os\nimport re\n\nimport hou\nimport pyblish.api\n\nfrom openpype.hosts.houdini.api import colorspace\nfrom openpype.hosts.houdini.api.lib import (\n    evalParmNoFrame, get_color_management_preferences)\n\n\nclass CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin):\n    \"\"\"Collect Arnold ROP Render Products\n\n    Collects the instance.data[\"files\"] for the render products.\n\n    Provides:\n        instance    -> files\n\n    \"\"\"\n\n    label = \"Arnold ROP Render Products\"\n    # This specific order value is used so that\n    # this plugin runs after CollectFrames\n    order = pyblish.api.CollectorOrder + 0.11\n    hosts = [\"houdini\"]\n    families = [\"arnold_rop\"]\n\n    def process(self, instance):\n\n        rop = hou.node(instance.data.get(\"instance_node\"))\n\n        # Collect chunkSize\n        chunk_size_parm = rop.parm(\"chunkSize\")\n        if chunk_size_parm:\n            chunk_size = int(chunk_size_parm.eval())\n            instance.data[\"chunkSize\"] = chunk_size\n            self.log.debug(\"Chunk Size: %s\" % chunk_size)\n\n        default_prefix = evalParmNoFrame(rop, \"ar_picture\")\n        render_products = []\n\n        # Store whether we are splitting the render job (export + render)\n        split_render = bool(rop.parm(\"ar_ass_export_enable\").eval())\n        instance.data[\"splitRender\"] = split_render\n        export_prefix = None\n        export_products = []\n        if split_render:\n            export_prefix = evalParmNoFrame(\n                rop, \"ar_ass_file\", pad_character=\"0\"\n            )\n            beauty_export_product = self.get_render_product_name(\n                prefix=export_prefix,\n                suffix=None)\n            export_products.append(beauty_export_product)\n            self.log.debug(\n                \"Found export product: {}\".format(beauty_export_product)\n            )\n            instance.data[\"ifdFile\"] = beauty_export_product\n            instance.data[\"exportFiles\"] = list(export_products)\n\n        # Default beauty AOV\n        beauty_product = self.get_render_product_name(prefix=default_prefix,\n                                                      suffix=None)\n        render_products.append(beauty_product)\n\n        files_by_aov = {\n            \"\": self.generate_expected_files(instance, beauty_product)\n        }\n\n        num_aovs = rop.evalParm(\"ar_aovs\")\n        for index in range(1, num_aovs + 1):\n            # Skip disabled AOVs\n            if not rop.evalParm(\"ar_enable_aov{}\".format(index)):\n                continue\n\n            if rop.evalParm(\"ar_aov_exr_enable_layer_name{}\".format(index)):\n                label = rop.evalParm(\"ar_aov_exr_layer_name{}\".format(index))\n            else:\n                label = evalParmNoFrame(rop, \"ar_aov_label{}\".format(index))\n\n            aov_product = self.get_render_product_name(default_prefix,\n                                                       suffix=label)\n            render_products.append(aov_product)\n            files_by_aov[label] = self.generate_expected_files(instance,\n                                                               aov_product)\n\n        for product in render_products:\n            self.log.debug(\"Found render product: {}\".format(product))\n\n        instance.data[\"files\"] = list(render_products)\n        instance.data[\"renderProducts\"] = colorspace.ARenderProduct()\n\n        # For now by default do NOT try to publish the rendered output\n        instance.data[\"publishJobState\"] = \"Suspended\"\n        instance.data[\"attachTo\"] = []      # stub required data\n\n        if \"expectedFiles\" not in instance.data:\n            instance.data[\"expectedFiles\"] = list()\n        instance.data[\"expectedFiles\"].append(files_by_aov)\n\n        # update the colorspace data\n        colorspace_data = get_color_management_preferences()\n        instance.data[\"colorspaceConfig\"] = colorspace_data[\"config\"]\n        instance.data[\"colorspaceDisplay\"] = colorspace_data[\"display\"]\n        instance.data[\"colorspaceView\"] = colorspace_data[\"view\"]\n\n    def get_render_product_name(self, prefix, suffix):\n        \"\"\"Return the output filename using the AOV prefix and suffix\"\"\"\n\n        # When AOV is explicitly defined in prefix we just swap it out\n        # directly with the AOV suffix to embed it.\n        # Note: ${AOV} seems to be evaluated in the parameter as %AOV%\n        if \"%AOV%\" in prefix:\n            # It seems that when some special separator characters are present\n            # before the %AOV% token that Redshift will secretly remove it if\n            # there is no suffix for the current product, for example:\n            # foo_%AOV% -> foo.exr\n            pattern = \"%AOV%\" if suffix else \"[._-]?%AOV%\"\n            product_name = re.sub(pattern,\n                                  suffix,\n                                  prefix,\n                                  flags=re.IGNORECASE)\n        else:\n            if suffix:\n                # Add \".{suffix}\" before the extension\n                prefix_base, ext = os.path.splitext(prefix)\n                product_name = prefix_base + \".\" + suffix + ext\n            else:\n                product_name = prefix\n\n        return product_name\n\n    def generate_expected_files(self, instance, path):\n        \"\"\"Create expected files in instance data\"\"\"\n\n        dir = os.path.dirname(path)\n        file = os.path.basename(path)\n\n        if \"#\" in file:\n            def replace(match):\n                return \"%0{}d\".format(len(match.group()))\n\n            file = re.sub(\"#+\", replace, file)\n\n        if \"%\" not in file:\n            return path\n\n        expected_files = []\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n\n        for i in range(int(start), (int(end) + 1)):\n            expected_files.append(\n                os.path.join(dir, (file % i)).replace(\"\\\\\", \"/\"))\n\n        return expected_files\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_asset_handles.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collector plugin for frames data on ROP instances.\"\"\"\nimport hou  # noqa\nimport pyblish.api\nfrom openpype.lib import BoolDef\nfrom openpype.pipeline import OpenPypePyblishPluginMixin\n\n\nclass CollectAssetHandles(pyblish.api.InstancePlugin,\n                          OpenPypePyblishPluginMixin):\n    \"\"\"Apply asset handles.\n\n    If instance does not have:\n        - frameStart\n        - frameEnd\n        - handleStart\n        - handleEnd\n    But it does have:\n        - frameStartHandle\n        - frameEndHandle\n\n    Then we will retrieve the asset's handles to compute\n    the exclusive frame range and actual handle ranges.\n    \"\"\"\n\n    hosts = [\"houdini\"]\n\n    # This specific order value is used so that\n    # this plugin runs after CollectAnatomyInstanceData\n    order = pyblish.api.CollectorOrder + 0.499\n\n    label = \"Collect Asset Handles\"\n    use_asset_handles = True\n\n    def process(self, instance):\n        # Only process instances without already existing handles data\n        # but that do have frameStartHandle and frameEndHandle defined\n        # like the data collected from CollectRopFrameRange\n        if \"frameStartHandle\" not in instance.data:\n            return\n        if \"frameEndHandle\" not in instance.data:\n            return\n\n        has_existing_data = {\n            \"handleStart\",\n            \"handleEnd\",\n            \"frameStart\",\n            \"frameEnd\"\n        }.issubset(instance.data)\n        if has_existing_data:\n            return\n\n        attr_values = self.get_attr_values_from_data(instance.data)\n        if attr_values.get(\"use_handles\", self.use_asset_handles):\n            asset_data = instance.data[\"assetEntity\"][\"data\"]\n            handle_start = asset_data.get(\"handleStart\", 0)\n            handle_end = asset_data.get(\"handleEnd\", 0)\n        else:\n            handle_start = 0\n            handle_end = 0\n\n        frame_start = instance.data[\"frameStartHandle\"] + handle_start\n        frame_end = instance.data[\"frameEndHandle\"] - handle_end\n\n        instance.data.update({\n            \"handleStart\": handle_start,\n            \"handleEnd\": handle_end,\n            \"frameStart\": frame_start,\n            \"frameEnd\": frame_end\n        })\n\n        # Log debug message about the collected frame range\n        if attr_values.get(\"use_handles\", self.use_asset_handles):\n            self.log.debug(\n                \"Full Frame range with Handles \"\n                \"[{frame_start_handle} - {frame_end_handle}]\"\n                .format(\n                    frame_start_handle=instance.data[\"frameStartHandle\"],\n                    frame_end_handle=instance.data[\"frameEndHandle\"]\n                )\n            )\n        else:\n            self.log.debug(\n                \"Use handles is deactivated for this instance, \"\n                \"start and end handles are set to 0.\"\n            )\n\n        # Log collected frame range to the user\n        message = \"Frame range [{frame_start} - {frame_end}]\".format(\n            frame_start=frame_start,\n            frame_end=frame_end\n        )\n        if handle_start or handle_end:\n            message += \" with handles [{handle_start}]-[{handle_end}]\".format(\n                handle_start=handle_start,\n                handle_end=handle_end\n            )\n        self.log.info(message)\n\n        if instance.data.get(\"byFrameStep\", 1.0) != 1.0:\n            self.log.info(\n                \"Frame steps {}\".format(instance.data[\"byFrameStep\"]))\n\n        # Add frame range to label if the instance has a frame range.\n        label = instance.data.get(\"label\", instance.data[\"name\"])\n        instance.data[\"label\"] = (\n            \"{label} [{frame_start_handle} - {frame_end_handle}]\"\n            .format(\n                label=label,\n                frame_start_handle=instance.data[\"frameStartHandle\"],\n                frame_end_handle=instance.data[\"frameEndHandle\"]\n            )\n        )\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            BoolDef(\"use_handles\",\n                    tooltip=\"Disable this if you want the publisher to\"\n                    \" ignore start and end handles specified in the\"\n                    \" asset data for this publish instance\",\n                    default=cls.use_asset_handles,\n                    label=\"Use asset handles\")\n        ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_cache_farm.py",
    "content": "import os\nimport pyblish.api\nimport hou\nfrom openpype.hosts.houdini.api import lib\n\n\nclass CollectDataforCache(pyblish.api.InstancePlugin):\n    \"\"\"Collect data for caching to Deadline.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.04\n    families = [\"ass\", \"pointcache\",\n                \"mantraifd\", \"redshiftproxy\",\n                \"vdbcache\"]\n    hosts = [\"houdini\"]\n    targets = [\"local\", \"remote\"]\n    label = \"Collect Data for Cache\"\n\n    def process(self, instance):\n        creator_attribute = instance.data[\"creator_attributes\"]\n        farm_enabled = creator_attribute[\"farm\"]\n        instance.data[\"farm\"] = farm_enabled\n        if not farm_enabled:\n            self.log.debug(\"Caching on farm is disabled. \"\n                           \"Skipping farm collecting.\")\n            return\n        # Why do we need this particular collector to collect the expected\n        # output files from a ROP node. Don't we have a dedicated collector\n        # for that yet?\n        # Collect expected files\n        ropnode = hou.node(instance.data[\"instance_node\"])\n        output_parm = lib.get_output_parameter(ropnode)\n        expected_filepath = output_parm.eval()\n        instance.data.setdefault(\"files\", list())\n        instance.data.setdefault(\"expectedFiles\", list())\n        if instance.data.get(\"frames\"):\n            files = self.get_files(instance, expected_filepath)\n            # list of files\n            instance.data[\"files\"].extend(files)\n        else:\n            # single file\n            instance.data[\"files\"].append(output_parm.eval())\n        cache_files = {\"_\": instance.data[\"files\"]}\n        # Convert instance family to pointcache if it is bgeo or abc\n        # because ???\n        for family in instance.data[\"families\"]:\n            if family == \"bgeo\" or \"abc\":\n                instance.data[\"family\"] = \"pointcache\"\n                break\n        instance.data.update({\n            \"plugin\": \"Houdini\",\n            \"publish\": True\n        })\n        instance.data[\"families\"].append(\"publish.hou\")\n        instance.data[\"expectedFiles\"].append(cache_files)\n\n        self.log.debug(\"{}\".format(instance.data))\n\n    def get_files(self, instance, output_parm):\n        \"\"\"Get the files with the frame range data\n\n        Args:\n            instance (_type_): instance\n            output_parm (_type_): path of output parameter\n\n        Returns:\n            files: a list of files\n        \"\"\"\n        directory = os.path.dirname(output_parm)\n\n        files = [\n            os.path.join(directory, frame).replace(\"\\\\\", \"/\")\n            for frame in instance.data[\"frames\"]\n        ]\n\n        return files\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_chunk_size.py",
    "content": "import pyblish.api\nfrom openpype.lib import NumberDef\nfrom openpype.pipeline import OpenPypePyblishPluginMixin\n\n\nclass CollectChunkSize(pyblish.api.InstancePlugin,\n                       OpenPypePyblishPluginMixin):\n    \"\"\"Collect chunk size for cache submission to Deadline.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.05\n    families = [\"ass\", \"pointcache\",\n                \"vdbcache\", \"mantraifd\",\n                \"redshiftproxy\"]\n    hosts = [\"houdini\"]\n    targets = [\"local\", \"remote\"]\n    label = \"Collect Chunk Size\"\n    chunk_size = 999999\n\n    def process(self, instance):\n        # need to get the chunk size info from the setting\n        attr_values = self.get_attr_values_from_data(instance.data)\n        instance.data[\"chunkSize\"] = attr_values.get(\"chunkSize\")\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            NumberDef(\"chunkSize\",\n                      minimum=1,\n                      maximum=999999,\n                      decimals=0,\n                      default=cls.chunk_size,\n                      label=\"Frame Per Task\")\n        ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_current_file.py",
    "content": "import os\nimport hou\n\nimport pyblish.api\n\n\nclass CollectHoudiniCurrentFile(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current working file into context\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.1\n    label = \"Houdini Current File\"\n    hosts = [\"houdini\"]\n\n    def process(self, context):\n        \"\"\"Inject the current working file\"\"\"\n\n        current_file = hou.hipFile.path()\n        if not os.path.exists(current_file):\n            # By default, Houdini will even point a new scene to a path.\n            # However if the file is not saved at all and does not exist,\n            # we assume the user never set it.\n            current_file = \"\"\n\n        elif os.path.basename(current_file) == \"untitled.hip\":\n            # Due to even a new file being called 'untitled.hip' we are unable\n            # to confirm the current scene was ever saved because the file\n            # could have existed already. We will allow it if the file exists,\n            # but show a warning for this edge case to clarify the potential\n            # false positive.\n            self.log.warning(\n                \"Current file is 'untitled.hip' and we are \"\n                \"unable to detect whether the current scene is \"\n                \"saved correctly.\"\n            )\n\n        context.data[\"currentFile\"] = current_file\n        self.log.info('Current workfile path: {}'.format(current_file))\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_frames.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collector plugin for frames data on ROP instances.\"\"\"\nimport os\nimport re\n\nimport hou  # noqa\nimport pyblish.api\nfrom openpype.hosts.houdini.api import lib\n\n\nclass CollectFrames(pyblish.api.InstancePlugin):\n    \"\"\"Collect all frames which would be saved from the ROP nodes\"\"\"\n\n    # This specific order value is used so that\n    # this plugin runs after CollectRopFrameRange\n    order = pyblish.api.CollectorOrder + 0.1\n    label = \"Collect Frames\"\n    families = [\"vdbcache\", \"imagesequence\", \"ass\",\n                \"mantraifd\", \"redshiftproxy\", \"review\",\n                \"bgeo\"]\n\n    def process(self, instance):\n\n        ropnode = hou.node(instance.data[\"instance_node\"])\n\n        start_frame = instance.data.get(\"frameStartHandle\", None)\n        end_frame = instance.data.get(\"frameEndHandle\", None)\n\n        output_parm = lib.get_output_parameter(ropnode)\n        if start_frame is not None:\n            # When rendering only a single frame still explicitly\n            # get the name for that particular frame instead of current frame\n            output = output_parm.evalAtFrame(start_frame)\n        else:\n            self.log.warning(\"Using current frame: {}\".format(hou.frame()))\n            output = output_parm.eval()\n\n        _, ext = lib.splitext(\n            output, allowed_multidot_extensions=[\n                \".ass.gz\", \".bgeo.sc\", \".bgeo.gz\",\n                \".bgeo.lzma\", \".bgeo.bz2\"])\n        file_name = os.path.basename(output)\n        result = file_name\n\n        # Get the filename pattern match from the output\n        # path, so we can compute all frames that would\n        # come out from rendering the ROP node if there\n        # is a frame pattern in the name\n        pattern = r\"\\w+\\.(\\d+)\" + re.escape(ext)\n        match = re.match(pattern, file_name)\n\n        if match and start_frame is not None:\n\n            # Check if frames are bigger than 1 (file collection)\n            # override the result\n            if end_frame - start_frame > 0:\n                result = self.create_file_list(\n                    match, int(start_frame), int(end_frame)\n                )\n\n        # todo: `frames` currently conflicts with \"explicit frames\" for a\n        #       for a custom frame list. So this should be refactored.\n        instance.data.update({\"frames\": result})\n\n    @staticmethod\n    def create_file_list(match, start_frame, end_frame):\n        \"\"\"Collect files based on frame range and `regex.match`\n\n        Args:\n            match(re.match): match object\n            start_frame(int): start of the animation\n            end_frame(int): end of the animation\n\n        Returns:\n            list\n\n        \"\"\"\n\n        # Get the padding length\n        frame = match.group(1)\n        padding = len(frame)\n\n        # Get the parts of the filename surrounding the frame number,\n        # so we can put our own frame numbers in.\n        span = match.span(1)\n        prefix = match.string[: span[0]]\n        suffix = match.string[span[1]:]\n\n        # Generate filenames for all frames\n        result = []\n        for i in range(start_frame, end_frame + 1):\n\n            # Format frame number by the padding amount\n            str_frame = \"{number:0{width}d}\".format(number=i, width=padding)\n\n            file_name = prefix + str_frame + suffix\n            result.append(file_name)\n\n        return result\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_inputs.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import registered_host\n\n\ndef collect_input_containers(nodes):\n    \"\"\"Collect containers that contain any of the node in `nodes`.\n\n    This will return any loaded Avalon container that contains at least one of\n    the nodes. As such, the Avalon container is an input for it. Or in short,\n    there are member nodes of that container.\n\n    Returns:\n        list: Input avalon containers\n\n    \"\"\"\n\n    # Lookup by node ids\n    lookup = frozenset(nodes)\n\n    containers = []\n    host = registered_host()\n    for container in host.ls():\n\n        node = container[\"node\"]\n\n        # Usually the loaded containers don't have any complex references\n        # and the contained children should be all we need. So we disregard\n        # checking for .references() on the nodes.\n        members = set(node.allSubChildren())\n        members.add(node)  # include the node itself\n\n        # If there's an intersection\n        if not lookup.isdisjoint(members):\n            containers.append(container)\n\n    return containers\n\n\ndef iter_upstream(node):\n    \"\"\"Yields all upstream inputs for the current node.\n\n    This includes all `node.inputAncestors()` but also traverses through all\n    `node.references()` for the node itself and for any of the upstream nodes.\n    This method has no max-depth and will collect all upstream inputs.\n\n    Yields:\n        hou.Node: The upstream nodes, including references.\n\n    \"\"\"\n\n    upstream = node.inputAncestors(\n        include_ref_inputs=True, follow_subnets=True\n    )\n\n    # Initialize process queue with the node's ancestors itself\n    queue = list(upstream)\n    collected = set(upstream)\n\n    # Traverse upstream references for all nodes and yield them as we\n    # process the queue.\n    while queue:\n        upstream_node = queue.pop()\n        yield upstream_node\n\n        # Find its references that are not collected yet.\n        references = upstream_node.references()\n        references = [n for n in references if n not in collected]\n\n        queue.extend(references)\n        collected.update(references)\n\n        # Include the references' ancestors that have not been collected yet.\n        for reference in references:\n            ancestors = reference.inputAncestors(\n                include_ref_inputs=True, follow_subnets=True\n            )\n            ancestors = [n for n in ancestors if n not in collected]\n\n            queue.extend(ancestors)\n            collected.update(ancestors)\n\n\nclass CollectUpstreamInputs(pyblish.api.InstancePlugin):\n    \"\"\"Collect source input containers used for this publish.\n\n    This will include `inputs` data of which loaded publishes were used in the\n    generation of this publish. This leaves an upstream trace to what was used\n    as input.\n\n    \"\"\"\n\n    label = \"Collect Inputs\"\n    order = pyblish.api.CollectorOrder + 0.4\n    hosts = [\"houdini\"]\n\n    def process(self, instance):\n        # We can't get the \"inputAncestors\" directly from the ROP\n        # node, so we find the related output node (set in SOP/COP path)\n        # and include that together with its ancestors\n        output = instance.data.get(\"output_node\")\n\n        if output is None:\n            # If no valid output node is set then ignore it as validation\n            # will be checking those cases.\n            self.log.debug(\n                \"No output node found, skipping collecting of inputs..\"\n            )\n            return\n\n        # Collect all upstream parents\n        nodes = list(iter_upstream(output))\n        nodes.append(output)\n\n        # Collect containers for the given set of nodes\n        containers = collect_input_containers(nodes)\n\n        inputs = [c[\"representation\"] for c in containers]\n        instance.data[\"inputRepresentations\"] = inputs\n        self.log.debug(\"Collected inputs: %s\" % inputs)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_instances.py",
    "content": "import hou\n\nimport pyblish.api\n\nfrom openpype.hosts.houdini.api import lib\n\n\nclass CollectInstances(pyblish.api.ContextPlugin):\n    \"\"\"Gather instances by all node in out graph and pre-defined attributes\n\n    This collector takes into account assets that are associated with\n    an specific node and marked with a unique identifier;\n\n    Identifier:\n        id (str): \"pyblish.avalon.instance\n\n    Specific node:\n        The specific node is important because it dictates in which way the\n        subset is being exported.\n\n        alembic: will export Alembic file which supports cascading attributes\n                 like 'cbId' and 'path'\n        geometry: Can export a wide range of file types, default out\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.01\n    label = \"Collect Instances\"\n    hosts = [\"houdini\"]\n\n    def process(self, context):\n\n        nodes = hou.node(\"/out\").children()\n        nodes += hou.node(\"/obj\").children()\n\n        # Include instances in USD stage only when it exists so it\n        # remains backwards compatible with version before houdini 18\n        stage = hou.node(\"/stage\")\n        if stage:\n            nodes += stage.recursiveGlob(\"*\", filter=hou.nodeTypeFilter.Rop)\n\n        for node in nodes:\n\n            if not node.parm(\"id\"):\n                continue\n\n            if node.evalParm(\"id\") != \"pyblish.avalon.instance\":\n                continue\n\n            # instance was created by new creator code, skip it as\n            # it is already collected.\n            if node.parm(\"creator_identifier\"):\n                continue\n\n            has_family = node.evalParm(\"family\")\n            assert has_family, \"'%s' is missing 'family'\" % node.name()\n\n            self.log.info(\n                \"Processing legacy instance node {}\".format(node.path())\n            )\n\n            data = lib.read(node)\n            # Check bypass state and reverse\n            if hasattr(node, \"isBypassed\"):\n                data.update({\"active\": not node.isBypassed()})\n\n            # temporarily translation of `active` to `publish` till issue has\n            # been resolved.\n            # https://github.com/pyblish/pyblish-base/issues/307\n            if \"active\" in data:\n                data[\"publish\"] = data[\"active\"]\n\n            # Create nice name if the instance has a frame range.\n            label = data.get(\"name\", node.name())\n            label += \" (%s)\" % data[\"asset\"]  # include asset in name\n\n            instance = context.create_instance(label)\n\n            # Include `families` using `family` data\n            instance.data[\"families\"] = [instance.data[\"family\"]]\n\n            instance[:] = [node]\n            instance.data[\"instance_node\"] = node.path()\n            instance.data.update(data)\n\n        def sort_by_family(instance):\n            \"\"\"Sort by family\"\"\"\n            return instance.data.get(\"families\", instance.data.get(\"family\"))\n\n        # Sort/grouped by family (preserving local index)\n        context[:] = sorted(context, key=sort_by_family)\n\n        return context\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py",
    "content": "import hou\nimport pyblish.api\nfrom openpype.hosts.houdini.api import lib\nimport openpype.hosts.houdini.api.usd as hou_usdlib\nimport openpype.lib.usdlib as usdlib\n\n\nclass CollectInstancesUsdLayered(pyblish.api.ContextPlugin):\n    \"\"\"Collect Instances from a ROP Network and its configured layer paths.\n\n    The output nodes of the ROP node will only be published when *any* of the\n    layers remain set to 'publish' by the user.\n\n    This works differently from most of our Avalon instances in the pipeline.\n    As opposed to storing `pyblish.avalon.instance` as id on the node we store\n    `pyblish.avalon.usdlayered`.\n\n    Additionally this instance has no need for storing family, asset, subset\n    or name on the nodes. Instead all information is retrieved solely from\n    the output filepath, which is an Avalon URI:\n        avalon://{asset}/{subset}.{representation}\n\n    Each final ROP node is considered a dependency for any of the Configured\n    Save Path layers it sets along the way. As such, the instances shown in\n    the Pyblish UI are solely the configured layers. The encapsulating usd\n    files are generated whenever *any* of the dependencies is published.\n\n    These dependency instances are stored in:\n        instance.data[\"publishDependencies\"]\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.01\n    label = \"Collect Instances (USD Configured Layers)\"\n    hosts = [\"houdini\"]\n\n    def process(self, context):\n\n        stage = hou.node(\"/stage\")\n        if not stage:\n            # Likely Houdini version <18\n            return\n\n        nodes = stage.recursiveGlob(\"*\", filter=hou.nodeTypeFilter.Rop)\n        for node in nodes:\n\n            if not node.parm(\"id\"):\n                continue\n\n            if node.evalParm(\"id\") != \"pyblish.avalon.usdlayered\":\n                continue\n\n            has_family = node.evalParm(\"family\")\n            assert has_family, \"'%s' is missing 'family'\" % node.name()\n\n            self.process_node(node, context)\n\n        def sort_by_family(instance):\n            \"\"\"Sort by family\"\"\"\n            return instance.data.get(\"families\", instance.data.get(\"family\"))\n\n        # Sort/grouped by family (preserving local index)\n        context[:] = sorted(context, key=sort_by_family)\n\n        return context\n\n    def process_node(self, node, context):\n\n        # Allow a single ROP node or a full ROP network of USD ROP nodes\n        # to be processed as a single entry that should \"live together\" on\n        # a publish.\n        if node.type().name() == \"ropnet\":\n            # All rop nodes inside ROP Network\n            ropnodes = node.recursiveGlob(\"*\", filter=hou.nodeTypeFilter.Rop)\n        else:\n            # A single node\n            ropnodes = [node]\n\n        data = lib.read(node)\n\n        # Don't use the explicit \"colorbleed.usd.layered\" family for publishing\n        # instead use the \"colorbleed.usd\" family to integrate.\n        data[\"publishFamilies\"] = [\"colorbleed.usd\"]\n\n        # For now group ALL of them into USD Layer subset group\n        # Allow this subset to be grouped into a USD Layer on creation\n        data[\"subsetGroup\"] = \"USD Layer\"\n\n        instances = list()\n        dependencies = []\n        for ropnode in ropnodes:\n\n            # Create a dependency instance per ROP Node.\n            lopoutput = ropnode.evalParm(\"lopoutput\")\n            dependency_save_data = self.get_save_data(lopoutput)\n            dependency = context.create_instance(dependency_save_data[\"name\"])\n            dependency.append(ropnode)\n            dependency.data.update(data)\n            dependency.data.update(dependency_save_data)\n            dependency.data[\"family\"] = \"colorbleed.usd.dependency\"\n            dependency.data[\"optional\"] = False\n            dependencies.append(dependency)\n\n            # Hide the dependency instance from the context\n            context.pop()\n\n            # Get all configured layers for this USD ROP node\n            # and create a Pyblish instance for each one\n            layers = hou_usdlib.get_configured_save_layers(ropnode)\n            for layer in layers:\n                save_path = hou_usdlib.get_layer_save_path(layer)\n                save_data = self.get_save_data(save_path)\n                if not save_data:\n                    continue\n                self.log.info(save_path)\n\n                instance = context.create_instance(save_data[\"name\"])\n                instance[:] = [node]\n\n                # Set the instance data\n                instance.data.update(data)\n                instance.data.update(save_data)\n                instance.data[\"usdLayer\"] = layer\n\n                instances.append(instance)\n\n        # Store the collected ROP node dependencies\n        self.log.debug(\"Collected dependencies: %s\" % (dependencies,))\n        for instance in instances:\n            instance.data[\"publishDependencies\"] = dependencies\n\n    def get_save_data(self, save_path):\n\n        # Resolve Avalon URI\n        uri_data = usdlib.parse_avalon_uri(save_path)\n        if not uri_data:\n            self.log.warning(\"Non Avalon URI Layer Path: %s\" % save_path)\n            return {}\n\n        # Collect asset + subset from URI\n        name = \"{subset} ({asset})\".format(**uri_data)\n        fname = \"{asset}_{subset}.{ext}\".format(**uri_data)\n\n        data = dict(uri_data)\n        data[\"usdSavePath\"] = save_path\n        data[\"usdFilename\"] = fname\n        data[\"name\"] = name\n        return data\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_karma_rop.py",
    "content": "import re\nimport os\n\nimport hou\nimport pyblish.api\n\nfrom openpype.hosts.houdini.api.lib import (\n    evalParmNoFrame,\n    get_color_management_preferences\n)\nfrom openpype.hosts.houdini.api import (\n    colorspace\n)\n\n\nclass CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin):\n    \"\"\"Collect Karma Render Products\n\n    Collects the instance.data[\"files\"] for the multipart render product.\n\n    Provides:\n        instance    -> files\n\n    \"\"\"\n\n    label = \"Karma ROP Render Products\"\n    # This specific order value is used so that\n    # this plugin runs after CollectFrames\n    order = pyblish.api.CollectorOrder + 0.11\n    hosts = [\"houdini\"]\n    families = [\"karma_rop\"]\n\n    def process(self, instance):\n\n        rop = hou.node(instance.data.get(\"instance_node\"))\n\n        # Collect chunkSize\n        chunk_size_parm = rop.parm(\"chunkSize\")\n        if chunk_size_parm:\n            chunk_size = int(chunk_size_parm.eval())\n            instance.data[\"chunkSize\"] = chunk_size\n            self.log.debug(\"Chunk Size: %s\" % chunk_size)\n\n            default_prefix = evalParmNoFrame(rop, \"picture\")\n            render_products = []\n\n            # Default beauty AOV\n            beauty_product = self.get_render_product_name(\n                prefix=default_prefix, suffix=None\n            )\n            render_products.append(beauty_product)\n\n            files_by_aov = {\n                \"beauty\": self.generate_expected_files(instance,\n                                                       beauty_product)\n            }\n\n            filenames = list(render_products)\n            instance.data[\"files\"] = filenames\n            instance.data[\"renderProducts\"] = colorspace.ARenderProduct()\n\n        for product in render_products:\n            self.log.debug(\"Found render product: %s\" % product)\n\n        if \"expectedFiles\" not in instance.data:\n            instance.data[\"expectedFiles\"] = list()\n        instance.data[\"expectedFiles\"].append(files_by_aov)\n\n        # update the colorspace data\n        colorspace_data = get_color_management_preferences()\n        instance.data[\"colorspaceConfig\"] = colorspace_data[\"config\"]\n        instance.data[\"colorspaceDisplay\"] = colorspace_data[\"display\"]\n        instance.data[\"colorspaceView\"] = colorspace_data[\"view\"]\n\n    def get_render_product_name(self, prefix, suffix):\n        product_name = prefix\n        if suffix:\n            # Add \".{suffix}\" before the extension\n            prefix_base, ext = os.path.splitext(prefix)\n            product_name = \"{}.{}{}\".format(prefix_base, suffix, ext)\n\n        return product_name\n\n    def generate_expected_files(self, instance, path):\n        \"\"\"Create expected files in instance data\"\"\"\n\n        dir = os.path.dirname(path)\n        file = os.path.basename(path)\n\n        if \"#\" in file:\n            def replace(match):\n                return \"%0{}d\".format(len(match.group()))\n\n            file = re.sub(\"#+\", replace, file)\n\n        if \"%\" not in file:\n            return path\n\n        expected_files = []\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n\n        for i in range(int(start), (int(end) + 1)):\n            expected_files.append(\n                os.path.join(dir, (file % i)).replace(\"\\\\\", \"/\"))\n\n        return expected_files\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py",
    "content": "import re\nimport os\n\nimport hou\nimport pyblish.api\n\nfrom openpype.hosts.houdini.api.lib import (\n    evalParmNoFrame,\n    get_color_management_preferences\n)\nfrom openpype.hosts.houdini.api import (\n    colorspace\n)\n\n\nclass CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):\n    \"\"\"Collect Mantra Render Products\n\n    Collects the instance.data[\"files\"] for the render products.\n\n    Provides:\n        instance    -> files\n\n    \"\"\"\n\n    label = \"Mantra ROP Render Products\"\n    # This specific order value is used so that\n    # this plugin runs after CollectFrames\n    order = pyblish.api.CollectorOrder + 0.11\n    hosts = [\"houdini\"]\n    families = [\"mantra_rop\"]\n\n    def process(self, instance):\n\n        rop = hou.node(instance.data.get(\"instance_node\"))\n\n        # Collect chunkSize\n        chunk_size_parm = rop.parm(\"chunkSize\")\n        if chunk_size_parm:\n            chunk_size = int(chunk_size_parm.eval())\n            instance.data[\"chunkSize\"] = chunk_size\n            self.log.debug(\"Chunk Size: %s\" % chunk_size)\n\n            default_prefix = evalParmNoFrame(rop, \"vm_picture\")\n            render_products = []\n\n            # Store whether we are splitting the render job (export + render)\n            split_render = bool(rop.parm(\"soho_outputmode\").eval())\n            instance.data[\"splitRender\"] = split_render\n            export_prefix = None\n            export_products = []\n            if split_render:\n                export_prefix = evalParmNoFrame(\n                    rop, \"soho_diskfile\", pad_character=\"0\"\n                )\n                beauty_export_product = self.get_render_product_name(\n                    prefix=export_prefix,\n                    suffix=None)\n                export_products.append(beauty_export_product)\n                self.log.debug(\n                    \"Found export product: {}\".format(beauty_export_product)\n                )\n                instance.data[\"ifdFile\"] = beauty_export_product\n                instance.data[\"exportFiles\"] = list(export_products)\n\n            # Default beauty AOV\n            beauty_product = self.get_render_product_name(\n                prefix=default_prefix, suffix=None\n            )\n            render_products.append(beauty_product)\n\n            files_by_aov = {\n                \"beauty\": self.generate_expected_files(instance,\n                                                       beauty_product)\n            }\n\n            aov_numbers = rop.evalParm(\"vm_numaux\")\n            if aov_numbers > 0:\n                # get the filenames of the AOVs\n                for i in range(1, aov_numbers + 1):\n                    var = rop.evalParm(\"vm_variable_plane%d\" % i)\n                    if var:\n                        aov_name = \"vm_filename_plane%d\" % i\n                        aov_boolean = \"vm_usefile_plane%d\" % i\n                        aov_enabled = rop.evalParm(aov_boolean)\n                        has_aov_path = rop.evalParm(aov_name)\n                        if has_aov_path and aov_enabled == 1:\n                            aov_prefix = evalParmNoFrame(rop, aov_name)\n                            aov_product = self.get_render_product_name(\n                                prefix=aov_prefix, suffix=None\n                            )\n                            render_products.append(aov_product)\n\n                            files_by_aov[var] = self.generate_expected_files(instance, aov_product)     # noqa\n\n        for product in render_products:\n            self.log.debug(\"Found render product: %s\" % product)\n\n        filenames = list(render_products)\n        instance.data[\"files\"] = filenames\n        instance.data[\"renderProducts\"] = colorspace.ARenderProduct()\n\n        # For now by default do NOT try to publish the rendered output\n        instance.data[\"publishJobState\"] = \"Suspended\"\n        instance.data[\"attachTo\"] = []      # stub required data\n\n        if \"expectedFiles\" not in instance.data:\n            instance.data[\"expectedFiles\"] = list()\n        instance.data[\"expectedFiles\"].append(files_by_aov)\n\n        # update the colorspace data\n        colorspace_data = get_color_management_preferences()\n        instance.data[\"colorspaceConfig\"] = colorspace_data[\"config\"]\n        instance.data[\"colorspaceDisplay\"] = colorspace_data[\"display\"]\n        instance.data[\"colorspaceView\"] = colorspace_data[\"view\"]\n\n    def get_render_product_name(self, prefix, suffix):\n        product_name = prefix\n        if suffix:\n            # Add \".{suffix}\" before the extension\n            prefix_base, ext = os.path.splitext(prefix)\n            product_name = prefix_base + \".\" + suffix + ext\n\n        return product_name\n\n    def generate_expected_files(self, instance, path):\n        \"\"\"Create expected files in instance data\"\"\"\n\n        dir = os.path.dirname(path)\n        file = os.path.basename(path)\n\n        if \"#\" in file:\n            def replace(match):\n                return \"%0{}d\".format(len(match.group()))\n\n            file = re.sub(\"#+\", replace, file)\n\n        if \"%\" not in file:\n            return path\n\n        expected_files = []\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n\n        for i in range(int(start), (int(end) + 1)):\n            expected_files.append(\n                os.path.join(dir, (file % i)).replace(\"\\\\\", \"/\"))\n\n        return expected_files\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_output_node.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import KnownPublishError\n\n\nclass CollectOutputSOPPath(pyblish.api.InstancePlugin):\n    \"\"\"Collect the out node's SOP/COP Path value.\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    families = [\n        \"pointcache\",\n        \"camera\",\n        \"vdbcache\",\n        \"imagesequence\",\n        \"usd\",\n        \"usdrender\",\n        \"redshiftproxy\",\n        \"staticMesh\"\n    ]\n\n    hosts = [\"houdini\"]\n    label = \"Collect Output Node Path\"\n\n    def process(self, instance):\n\n        import hou\n\n        node = hou.node(instance.data[\"instance_node\"])\n\n        # Get sop path\n        node_type = node.type().name()\n        if node_type == \"geometry\":\n            out_node = node.parm(\"soppath\").evalAsNode()\n\n        elif node_type == \"alembic\":\n\n            # Alembic can switch between using SOP Path or object\n            if node.parm(\"use_sop_path\").eval():\n                out_node = node.parm(\"sop_path\").evalAsNode()\n            else:\n                root = node.parm(\"root\").eval()\n                objects = node.parm(\"objects\").eval()\n                path = root + \"/\" + objects\n                out_node = hou.node(path)\n\n        elif node_type == \"comp\":\n            out_node = node.parm(\"coppath\").evalAsNode()\n\n        elif node_type == \"usd\" or node_type == \"usdrender\":\n            out_node = node.parm(\"loppath\").evalAsNode()\n\n        elif node_type == \"usd_rop\" or node_type == \"usdrender_rop\":\n            # Inside Solaris e.g. /stage (not in ROP context)\n            # When incoming connection is present it takes it directly\n            inputs = node.inputs()\n            if inputs:\n                out_node = inputs[0]\n            else:\n                out_node = node.parm(\"loppath\").evalAsNode()\n\n        elif node_type == \"Redshift_Proxy_Output\":\n            out_node = node.parm(\"RS_archive_sopPath\").evalAsNode()\n\n        elif node_type == \"filmboxfbx\":\n            out_node = node.parm(\"startnode\").evalAsNode()\n\n        else:\n            raise KnownPublishError(\n                \"ROP node type '{}' is not supported.\".format(node_type)\n            )\n\n        if not out_node:\n            self.log.warning(\"No output node collected.\")\n            return\n\n        self.log.debug(\"Output node: %s\" % out_node.path())\n        instance.data[\"output_node\"] = out_node\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_pointcache_type.py",
    "content": "\"\"\"Collector for pointcache types.\n\nThis will add additional family to pointcache instance based on\nthe creator_identifier parameter.\n\"\"\"\nimport pyblish.api\n\n\nclass CollectPointcacheType(pyblish.api.InstancePlugin):\n    \"\"\"Collect data type for pointcache instance.\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    hosts = [\"houdini\"]\n    families = [\"pointcache\"]\n    label = \"Collect type of pointcache\"\n\n    def process(self, instance):\n        if instance.data[\"creator_identifier\"] == \"io.openpype.creators.houdini.bgeo\":  # noqa: E501\n            instance.data[\"families\"] += [\"bgeo\"]\n        elif instance.data[\"creator_identifier\"] == \"io.openpype.creators.houdini.pointcache\":  # noqa: E501\n            instance.data[\"families\"] += [\"abc\"]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py",
    "content": "import re\nimport os\n\nimport hou\nimport pyblish.api\n\nfrom openpype.hosts.houdini.api.lib import (\n    evalParmNoFrame,\n    get_color_management_preferences\n)\nfrom openpype.hosts.houdini.api import (\n    colorspace\n)\n\n\nclass CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):\n    \"\"\"Collect USD Render Products\n\n    Collects the instance.data[\"files\"] for the render products.\n\n    Provides:\n        instance    -> files\n\n    \"\"\"\n\n    label = \"Redshift ROP Render Products\"\n    # This specific order value is used so that\n    # this plugin runs after CollectFrames\n    order = pyblish.api.CollectorOrder + 0.11\n    hosts = [\"houdini\"]\n    families = [\"redshift_rop\"]\n\n    def process(self, instance):\n        rop = hou.node(instance.data.get(\"instance_node\"))\n\n        # Collect chunkSize\n        chunk_size_parm = rop.parm(\"chunkSize\")\n        if chunk_size_parm:\n            chunk_size = int(chunk_size_parm.eval())\n            instance.data[\"chunkSize\"] = chunk_size\n            self.log.debug(\"Chunk Size: %s\" % chunk_size)\n\n        default_prefix = evalParmNoFrame(rop, \"RS_outputFileNamePrefix\")\n        beauty_suffix = rop.evalParm(\"RS_outputBeautyAOVSuffix\")\n        # Store whether we are splitting the render job (export + render)\n        split_render = bool(rop.parm(\"RS_archive_enable\").eval())\n        instance.data[\"splitRender\"] = split_render\n        export_products = []\n        if split_render:\n            export_prefix = evalParmNoFrame(\n                rop, \"RS_archive_file\", pad_character=\"0\"\n            )\n            beauty_export_product = self.get_render_product_name(\n                prefix=export_prefix,\n                suffix=None)\n            export_products.append(beauty_export_product)\n            self.log.debug(\n                \"Found export product: {}\".format(beauty_export_product)\n            )\n            instance.data[\"ifdFile\"] = beauty_export_product\n            instance.data[\"exportFiles\"] = list(export_products)\n\n        # Default beauty AOV\n        beauty_product = self.get_render_product_name(\n            prefix=default_prefix, suffix=beauty_suffix\n        )\n        render_products = [beauty_product]\n        files_by_aov = {\n            \"_\": self.generate_expected_files(instance,\n                                              beauty_product)}\n\n        num_aovs = rop.evalParm(\"RS_aov\")\n        for index in range(num_aovs):\n            i = index + 1\n\n            # Skip disabled AOVs\n            if not rop.evalParm(f\"RS_aovEnable_{i}\"):\n                continue\n\n            aov_suffix = rop.evalParm(f\"RS_aovSuffix_{i}\")\n            aov_prefix = evalParmNoFrame(rop, f\"RS_aovCustomPrefix_{i}\")\n            if not aov_prefix:\n                aov_prefix = default_prefix\n\n            aov_product = self.get_render_product_name(aov_prefix, aov_suffix)\n            render_products.append(aov_product)\n\n            files_by_aov[aov_suffix] = self.generate_expected_files(instance,\n                                                                    aov_product)    # noqa\n\n        for product in render_products:\n            self.log.debug(\"Found render product: %s\" % product)\n\n        filenames = list(render_products)\n        instance.data[\"files\"] = filenames\n        instance.data[\"renderProducts\"] = colorspace.ARenderProduct()\n\n        # For now by default do NOT try to publish the rendered output\n        instance.data[\"publishJobState\"] = \"Suspended\"\n        instance.data[\"attachTo\"] = []      # stub required data\n\n        if \"expectedFiles\" not in instance.data:\n            instance.data[\"expectedFiles\"] = []\n        instance.data[\"expectedFiles\"].append(files_by_aov)\n\n        # update the colorspace data\n        colorspace_data = get_color_management_preferences()\n        instance.data[\"colorspaceConfig\"] = colorspace_data[\"config\"]\n        instance.data[\"colorspaceDisplay\"] = colorspace_data[\"display\"]\n        instance.data[\"colorspaceView\"] = colorspace_data[\"view\"]\n\n    def get_render_product_name(self, prefix, suffix):\n        \"\"\"Return the output filename using the AOV prefix and suffix\"\"\"\n\n        # When AOV is explicitly defined in prefix we just swap it out\n        # directly with the AOV suffix to embed it.\n        # Note: ${AOV} seems to be evaluated in the parameter as %AOV%\n        has_aov_in_prefix = \"%AOV%\" in prefix\n        if has_aov_in_prefix:\n            # It seems that when some special separator characters are present\n            # before the %AOV% token that Redshift will secretly remove it if\n            # there is no suffix for the current product, for example:\n            # foo_%AOV% -> foo.exr\n            pattern = \"%AOV%\" if suffix else \"[._-]?%AOV%\"\n            product_name = re.sub(pattern, suffix, prefix, flags=re.IGNORECASE)\n        else:\n            if suffix:\n                # Add \".{suffix}\" before the extension\n                prefix_base, ext = os.path.splitext(prefix)\n                product_name = prefix_base + \".\" + suffix + ext\n            else:\n                product_name = prefix\n\n        return product_name\n\n    def generate_expected_files(self, instance, path):\n        \"\"\"Create expected files in instance data\"\"\"\n\n        dir = os.path.dirname(path)\n        file = os.path.basename(path)\n\n        if \"#\" in file:\n            def replace(match):\n                return \"%0{}d\".format(len(match.group()))\n\n            file = re.sub(\"#+\", replace, file)\n\n        if \"%\" not in file:\n            return path\n\n        expected_files = []\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n\n        for i in range(int(start), (int(end) + 1)):\n            expected_files.append(\n                os.path.join(dir, (file % i)).replace(\"\\\\\", \"/\"))\n\n        return expected_files\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_remote_publish.py",
    "content": "import pyblish.api\n\nimport hou\nfrom openpype.pipeline.publish import RepairAction\nfrom openpype.hosts.houdini.api import lib\n\n\nclass CollectRemotePublishSettings(pyblish.api.ContextPlugin):\n    \"\"\"Collect custom settings of the Remote Publish node.\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    families = [\"*\"]\n    hosts = [\"houdini\"]\n    targets = [\"deadline\"]\n    label = \"Remote Publish Submission Settings\"\n    actions = [RepairAction]\n\n    def process(self, context):\n\n        node = hou.node(\"/out/REMOTE_PUBLISH\")\n        if not node:\n            return\n\n        attributes = lib.read(node)\n\n        # Debug the settings we have collected\n        for key, value in sorted(attributes.items()):\n            self.log.debug(\"Collected %s: %s\" % (key, value))\n\n        context.data.update(attributes)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_render_products.py",
    "content": "import re\nimport os\n\nimport hou\nimport pxr.UsdRender\n\nimport pyblish.api\n\n\ndef get_var_changed(variable=None):\n    \"\"\"Return changed variables and operators that use it.\n\n    Note: `varchange` hscript states that it forces a recook of the nodes\n          that use Variables. That was tested in Houdini\n          18.0.391.\n\n    Args:\n        variable (str, Optional): A specific variable to query the operators\n            for. When None is provided it will return all variables that have\n            had recent changes and require a recook. Defaults to None.\n\n    Returns:\n        dict: Variable that changed with the operators that use it.\n\n    \"\"\"\n    cmd = \"varchange -V\"\n    if variable:\n        cmd += \" {0}\".format(variable)\n    output, _ = hou.hscript(cmd)\n\n    changed = {}\n    for line in output.split(\"Variable: \"):\n        if not line.strip():\n            continue\n\n        split = line.split()\n        var = split[0]\n        operators = split[1:]\n        changed[var] = operators\n\n    return changed\n\n\nclass CollectRenderProducts(pyblish.api.InstancePlugin):\n    \"\"\"Collect USD Render Products.\"\"\"\n\n    label = \"Collect Render Products\"\n    order = pyblish.api.CollectorOrder + 0.4\n    hosts = [\"houdini\"]\n    families = [\"usdrender\"]\n\n    def process(self, instance):\n\n        node = instance.data.get(\"output_node\")\n        if not node:\n            rop_path = instance.data[\"instance_node\"].path()\n            raise RuntimeError(\n                \"No output node found. Make sure to connect an \"\n                \"input to the USD ROP: %s\" % rop_path\n            )\n\n        # Workaround Houdini 18.0.391 bug where $HIPNAME doesn't automatically\n        # update after scene save.\n        if hou.applicationVersion() == (18, 0, 391):\n            self.log.debug(\n                \"Checking for recook to workaround \" \"$HIPNAME refresh bug...\"\n            )\n            changed = get_var_changed(\"HIPNAME\").get(\"HIPNAME\")\n            if changed:\n                self.log.debug(\"Recooking for $HIPNAME refresh bug...\")\n                for operator in changed:\n                    hou.node(operator).cook(force=True)\n\n                # Make sure to recook any 'cache' nodes in the history chain\n                chain = [node]\n                chain.extend(node.inputAncestors())\n                for input_node in chain:\n                    if input_node.type().name() == \"cache\":\n                        input_node.cook(force=True)\n\n        stage = node.stage()\n\n        filenames = []\n        for prim in stage.Traverse():\n\n            if not prim.IsA(pxr.UsdRender.Product):\n                continue\n\n            # Get Render Product Name\n            product = pxr.UsdRender.Product(prim)\n\n            # We force taking it from any random time sample as opposed to\n            # \"default\" that the USD Api falls back to since that won't return\n            # time sampled values if they were set per time sample.\n            name = product.GetProductNameAttr().Get(time=0)\n            dirname = os.path.dirname(name)\n            basename = os.path.basename(name)\n\n            dollarf_regex = r\"(\\$F([0-9]?))\"\n            frame_regex = r\"^(.+\\.)([0-9]+)(\\.[a-zA-Z]+)$\"\n            if re.match(dollarf_regex, basename):\n                # TODO: Confirm this actually is allowed USD stages and HUSK\n                # Substitute $F\n                def replace(match):\n                    \"\"\"Replace $F4 with padded #.\"\"\"\n                    padding = int(match.group(2)) if match.group(2) else 1\n                    return \"#\" * padding\n\n                filename_base = re.sub(dollarf_regex, replace, basename)\n                filename = os.path.join(dirname, filename_base)\n            else:\n                # Substitute basename.0001.ext\n                def replace(match):\n                    prefix, frame, ext = match.groups()\n                    padding = \"#\" * len(frame)\n                    return prefix + padding + ext\n\n                filename_base = re.sub(frame_regex, replace, basename)\n                filename = os.path.join(dirname, filename_base)\n                filename = filename.replace(\"\\\\\", \"/\")\n\n            assert \"#\" in filename, (\n                \"Couldn't resolve render product name \"\n                \"with frame number: %s\" % name\n            )\n\n            filenames.append(filename)\n\n            prim_path = str(prim.GetPath())\n            self.log.info(\"Collected %s name: %s\" % (prim_path, filename))\n\n        # Filenames for Deadline\n        instance.data[\"files\"] = filenames\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_review_data.py",
    "content": "import hou\nimport pyblish.api\n\n\nclass CollectHoudiniReviewData(pyblish.api.InstancePlugin):\n    \"\"\"Collect Review Data.\"\"\"\n\n    label = \"Collect Review Data\"\n    # This specific order value is used so that\n    # this plugin runs after CollectRopFrameRange\n    order = pyblish.api.CollectorOrder + 0.1\n    hosts = [\"houdini\"]\n    families = [\"review\"]\n\n    def process(self, instance):\n\n        # This fixes the burnin having the incorrect start/end timestamps\n        # because without this it would take it from the context instead\n        # which isn't the actual frame range that this instance renders.\n        instance.data[\"handleStart\"] = 0\n        instance.data[\"handleEnd\"] = 0\n        instance.data[\"fps\"] = instance.context.data[\"fps\"]\n\n        # Enable ftrack functionality\n        instance.data.setdefault(\"families\", []).append('ftrack')\n\n        # Get the camera from the rop node to collect the focal length\n        ropnode_path = instance.data[\"instance_node\"]\n        ropnode = hou.node(ropnode_path)\n\n        camera_path = ropnode.parm(\"camera\").eval()\n        camera_node = hou.node(camera_path)\n        if not camera_node:\n            self.log.warning(\"No valid camera node found on review node: \"\n                             \"{}\".format(camera_path))\n            return\n\n        # Collect focal length.\n        focal_length_parm = camera_node.parm(\"focal\")\n        if not focal_length_parm:\n            self.log.warning(\"No 'focal' (focal length) parameter found on \"\n                             \"camera: {}\".format(camera_path))\n            return\n\n        if focal_length_parm.isTimeDependent():\n            start = instance.data[\"frameStartHandle\"]\n            end = instance.data[\"frameEndHandle\"] + 1\n            focal_length = [\n                focal_length_parm.evalAsFloatAtFrame(t)\n                for t in range(int(start), int(end))\n            ]\n        else:\n            focal_length = focal_length_parm.evalAsFloat()\n\n        # Store focal length in `burninDataMembers`\n        burnin_members = instance.data.setdefault(\"burninDataMembers\", {})\n        burnin_members[\"focalLength\"] = focal_length\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collector plugin for frames data on ROP instances.\"\"\"\nimport hou  # noqa\nimport pyblish.api\nfrom openpype.hosts.houdini.api import lib\n\n\nclass CollectRopFrameRange(pyblish.api.InstancePlugin):\n    \"\"\"Collect all frames which would be saved from the ROP nodes\"\"\"\n\n    hosts = [\"houdini\"]\n    order = pyblish.api.CollectorOrder\n    label = \"Collect RopNode Frame Range\"\n\n    def process(self, instance):\n\n        node_path = instance.data.get(\"instance_node\")\n        if node_path is None:\n            # Instance without instance node like a workfile instance\n            self.log.debug(\n                \"No instance node found for instance: {}\".format(instance)\n            )\n            return\n\n        ropnode = hou.node(node_path)\n        frame_data = lib.get_frame_data(\n            ropnode, self.log\n        )\n\n        if not frame_data:\n            return\n\n        # Log debug message about the collected frame range\n        self.log.debug(\n            \"Collected frame_data: {}\".format(frame_data)\n        )\n\n        instance.data.update(frame_data)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collector for staticMesh types. \"\"\"\n\nimport pyblish.api\n\n\nclass CollectStaticMeshType(pyblish.api.InstancePlugin):\n    \"\"\"Collect data type for fbx instance.\"\"\"\n\n    hosts = [\"houdini\"]\n    families = [\"staticMesh\"]\n    label = \"Collect type of staticMesh\"\n\n    order = pyblish.api.CollectorOrder\n\n    def process(self, instance):\n\n        if instance.data[\"creator_identifier\"] == \"io.openpype.creators.houdini.staticmesh.fbx\":  # noqa: E501\n            # Marking this instance as FBX triggers the FBX extractor.\n            instance.data[\"families\"] += [\"fbx\"]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py",
    "content": "import pyblish.api\n\nfrom openpype.client import (\n    get_subset_by_name,\n    get_asset_by_name,\n    get_asset_name_identifier,\n)\nimport openpype.lib.usdlib as usdlib\n\n\nclass CollectUsdBootstrap(pyblish.api.InstancePlugin):\n    \"\"\"Collect special Asset/Shot bootstrap instances if those are needed.\n\n    Some specific subsets are intended to be part of the default structure\n    of an \"Asset\" or \"Shot\" in our USD pipeline. For example, for an Asset\n    we layer a Model and Shade USD file over each other and expose that in\n    a Asset USD file, ready to use.\n\n    On the first publish of any of the components of a Asset or Shot the\n    missing pieces are bootstrapped and generated in the pipeline too. This\n    means that on the very first publish of your model the Asset USD file\n    will exist too.\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.35\n    label = \"Collect USD Bootstrap\"\n    hosts = [\"houdini\"]\n    families = [\"usd\", \"usd.layered\"]\n\n    def process(self, instance):\n\n        # Detect whether the current subset is a subset in a pipeline\n        def get_bootstrap(instance):\n            instance_subset = instance.data[\"subset\"]\n            for name, layers in usdlib.PIPELINE.items():\n                if instance_subset in set(layers):\n                    return name  # e.g. \"asset\"\n                    break\n            else:\n                return\n\n        bootstrap = get_bootstrap(instance)\n        if bootstrap:\n            self.add_bootstrap(instance, bootstrap)\n\n        # Check if any of the dependencies requires a bootstrap\n        for dependency in instance.data.get(\"publishDependencies\", list()):\n            bootstrap = get_bootstrap(dependency)\n            if bootstrap:\n                self.add_bootstrap(dependency, bootstrap)\n\n    def add_bootstrap(self, instance, bootstrap):\n\n        self.log.debug(\"Add bootstrap for: %s\" % bootstrap)\n\n        project_name = instance.context.data[\"projectName\"]\n        asset_name = instance.data[\"asset\"]\n        asset_doc = get_asset_by_name(project_name, asset_name)\n        assert asset_doc, \"Asset must exist: %s\" % asset_name\n\n        # Check which are not about to be created and don't exist yet\n        required = {\"shot\": [\"usdShot\"], \"asset\": [\"usdAsset\"]}.get(bootstrap)\n\n        require_all_layers = instance.data.get(\"requireAllLayers\", False)\n        if require_all_layers:\n            # USD files load fine in usdview and Houdini even when layered or\n            # referenced files do not exist. So by default we don't require\n            # the layers to exist.\n            layers = usdlib.PIPELINE.get(bootstrap)\n            if layers:\n                required += list(layers)\n\n        self.log.debug(\"Checking required bootstrap: %s\" % required)\n        for subset_name in required:\n            if self._subset_exists(\n                project_name, instance, subset_name, asset_doc\n            ):\n                continue\n\n            self.log.debug(\n                \"Creating {0} USD bootstrap: {1} {2}\".format(\n                    bootstrap, asset_name, subset_name\n                )\n            )\n\n            new = instance.context.create_instance(subset_name)\n            new.data[\"subset\"] = subset_name\n            new.data[\"label\"] = \"{0} ({1})\".format(subset_name, asset_name)\n            new.data[\"family\"] = \"usd.bootstrap\"\n            new.data[\"comment\"] = \"Automated bootstrap USD file.\"\n            new.data[\"publishFamilies\"] = [\"usd\"]\n\n            # Do not allow the user to toggle this instance\n            new.data[\"optional\"] = False\n\n            # Copy some data from the instance for which we bootstrap\n            for key in [\"asset\"]:\n                new.data[key] = instance.data[key]\n\n    def _subset_exists(self, project_name, instance, subset_name, asset_doc):\n        \"\"\"Return whether subset exists in current context or in database.\"\"\"\n        # Allow it to be created during this publish session\n        context = instance.context\n\n        asset_doc_name = get_asset_name_identifier(asset_doc)\n        for inst in context:\n            if (\n                inst.data[\"subset\"] == subset_name\n                and inst.data[\"asset\"] == asset_doc_name\n            ):\n                return True\n\n        # Or, if they already exist in the database we can\n        # skip them too.\n        if get_subset_by_name(\n            project_name, subset_name, asset_doc[\"_id\"], fields=[\"_id\"]\n        ):\n            return True\n        return False\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_usd_layers.py",
    "content": "import os\n\nimport pyblish.api\nimport openpype.hosts.houdini.api.usd as usdlib\n\nimport hou\n\n\nclass CollectUsdLayers(pyblish.api.InstancePlugin):\n    \"\"\"Collect the USD Layers that have configured save paths.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.35\n    label = \"Collect USD Layers\"\n    hosts = [\"houdini\"]\n    families = [\"usd\"]\n\n    def process(self, instance):\n\n        output = instance.data.get(\"output_node\")\n        if not output:\n            self.log.debug(\"No output node found..\")\n            return\n\n        rop_node = hou.node(instance.data[\"instance_node\"])\n\n        save_layers = []\n        for layer in usdlib.get_configured_save_layers(rop_node):\n\n            info = layer.rootPrims.get(\"HoudiniLayerInfo\")\n            save_path = info.customData.get(\"HoudiniSavePath\")\n            creator = info.customData.get(\"HoudiniCreatorNode\")\n\n            self.log.debug(\"Found configured save path: \"\n                           \"%s -> %s\" % (layer, save_path))\n\n            # Log node that configured this save path\n            if creator:\n                self.log.debug(\"Created by: %s\" % creator)\n\n            save_layers.append((layer, save_path))\n\n        # Store on the instance\n        instance.data[\"usdConfiguredSavePaths\"] = save_layers\n\n        # Create configured layer instances so User can disable updating\n        # specific configured layers for publishing.\n        context = instance.context\n        for layer, save_path in save_layers:\n            name = os.path.basename(save_path)\n            label = \"{0} -> {1}\".format(instance.data[\"name\"], name)\n            layer_inst = context.create_instance(name)\n\n            family = \"usdlayer\"\n            layer_inst.data[\"family\"] = family\n            layer_inst.data[\"families\"] = [family]\n            layer_inst.data[\"subset\"] = \"__stub__\"\n            layer_inst.data[\"label\"] = label\n            layer_inst.data[\"asset\"] = instance.data[\"asset\"]\n            layer_inst.data[\"instance_node\"] = instance.data[\"instance_node\"]\n            # include same USD ROP\n            layer_inst.append(rop_node)\n            # include layer data\n            layer_inst.append((layer, save_path))\n\n            # Allow this subset to be grouped into a USD Layer on creation\n            layer_inst.data[\"subsetGroup\"] = \"USD Layer\"\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_vray_rop.py",
    "content": "import re\nimport os\n\nimport hou\nimport pyblish.api\n\nfrom openpype.hosts.houdini.api.lib import (\n    evalParmNoFrame,\n    get_color_management_preferences\n)\nfrom openpype.hosts.houdini.api import (\n    colorspace\n)\n\n\nclass CollectVrayROPRenderProducts(pyblish.api.InstancePlugin):\n    \"\"\"Collect Vray Render Products\n\n    Collects the instance.data[\"files\"] for the render products.\n\n    Provides:\n        instance    -> files\n\n    \"\"\"\n\n    label = \"VRay ROP Render Products\"\n    # This specific order value is used so that\n    # this plugin runs after CollectFrames\n    order = pyblish.api.CollectorOrder + 0.11\n    hosts = [\"houdini\"]\n    families = [\"vray_rop\"]\n\n    def process(self, instance):\n\n        rop = hou.node(instance.data.get(\"instance_node\"))\n\n        # Collect chunkSize\n        chunk_size_parm = rop.parm(\"chunkSize\")\n        if chunk_size_parm:\n            chunk_size = int(chunk_size_parm.eval())\n            instance.data[\"chunkSize\"] = chunk_size\n            self.log.debug(\"Chunk Size: %s\" % chunk_size)\n\n        default_prefix = evalParmNoFrame(rop, \"SettingsOutput_img_file_path\")\n        render_products = []\n        # TODO: add render elements if render element\n\n        # Store whether we are splitting the render job in an export + render\n        split_render = rop.parm(\"render_export_mode\").eval() == \"2\"\n        instance.data[\"splitRender\"] = split_render\n        export_prefix = None\n        export_products = []\n        if split_render:\n            export_prefix = evalParmNoFrame(\n                rop, \"render_export_filepath\", pad_character=\"0\"\n            )\n            beauty_export_product = self.get_render_product_name(\n                prefix=export_prefix,\n                suffix=None)\n            export_products.append(beauty_export_product)\n            self.log.debug(\n                \"Found export product: {}\".format(beauty_export_product)\n            )\n            instance.data[\"ifdFile\"] = beauty_export_product\n            instance.data[\"exportFiles\"] = list(export_products)\n\n        beauty_product = self.get_render_product_name(default_prefix)\n        render_products.append(beauty_product)\n        files_by_aov = {\n            \"\": self.generate_expected_files(instance,\n                                                      beauty_product)}\n\n        if instance.data.get(\"RenderElement\", True):\n            render_element = self.get_render_element_name(rop, default_prefix)\n            if render_element:\n                for aov, renderpass in render_element.items():\n                    render_products.append(renderpass)\n                    files_by_aov[aov] = self.generate_expected_files(\n                        instance, renderpass)\n\n\n        for product in render_products:\n            self.log.debug(\"Found render product: %s\" % product)\n        filenames = list(render_products)\n        instance.data[\"files\"] = filenames\n        instance.data[\"renderProducts\"] = colorspace.ARenderProduct()\n\n        # For now by default do NOT try to publish the rendered output\n        instance.data[\"publishJobState\"] = \"Suspended\"\n        instance.data[\"attachTo\"] = []      # stub required data\n\n        if \"expectedFiles\" not in instance.data:\n            instance.data[\"expectedFiles\"] = list()\n        instance.data[\"expectedFiles\"].append(files_by_aov)\n        self.log.debug(\"expectedFiles:{}\".format(files_by_aov))\n\n        # update the colorspace data\n        colorspace_data = get_color_management_preferences()\n        instance.data[\"colorspaceConfig\"] = colorspace_data[\"config\"]\n        instance.data[\"colorspaceDisplay\"] = colorspace_data[\"display\"]\n        instance.data[\"colorspaceView\"] = colorspace_data[\"view\"]\n\n    def get_render_product_name(self, prefix, suffix=\"<reName>\"):\n        \"\"\"Return the beauty output filename if render element enabled\n        \"\"\"\n        # Remove aov suffix from the product: `prefix.aov_suffix` -> `prefix`\n        aov_parm = \".{}\".format(suffix)\n        return prefix.replace(aov_parm, \"\")\n\n    def get_render_element_name(self, node, prefix, suffix=\"<reName>\"):\n        \"\"\"Return the output filename using the AOV prefix and suffix\n        \"\"\"\n        render_element_dict = {}\n        # need a rewrite\n        re_path = node.evalParm(\"render_network_render_channels\")\n        if re_path:\n            node_children = hou.node(re_path).children()\n            for element in node_children:\n                if element.shaderName() != \"vray:SettingsRenderChannels\":\n                    aov = str(element)\n                    render_product = prefix.replace(suffix, aov)\n                    render_element_dict[aov] = render_product\n        return render_element_dict\n\n    def generate_expected_files(self, instance, path):\n        \"\"\"Create expected files in instance data\"\"\"\n\n        dir = os.path.dirname(path)\n        file = os.path.basename(path)\n\n        if \"#\" in file:\n            def replace(match):\n                return \"%0{}d\".format(len(match.group()))\n\n            file = re.sub(\"#+\", replace, file)\n\n        if \"%\" not in file:\n            return path\n\n        expected_files = []\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n\n        for i in range(int(start), (int(end) + 1)):\n            expected_files.append(\n                os.path.join(dir, (file % i)).replace(\"\\\\\", \"/\"))\n\n        return expected_files\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_workfile.py",
    "content": "import os\n\nimport pyblish.api\n\n\nclass CollectWorkfile(pyblish.api.InstancePlugin):\n    \"\"\"Inject workfile representation into instance\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.01\n    label = \"Houdini Workfile Data\"\n    hosts = [\"houdini\"]\n    families = [\"workfile\"]\n\n    def process(self, instance):\n\n        current_file = instance.context.data[\"currentFile\"]\n        folder, file = os.path.split(current_file)\n        filename, ext = os.path.splitext(file)\n\n        instance.data.update({\n            \"setMembers\": [current_file],\n            \"frameStart\": instance.context.data['frameStart'],\n            \"frameEnd\": instance.context.data['frameEnd'],\n            \"handleStart\": instance.context.data['handleStart'],\n            \"handleEnd\": instance.context.data['handleEnd']\n        })\n\n        instance.data['representations'] = [{\n            'name': ext.lstrip(\".\"),\n            'ext': ext.lstrip(\".\"),\n            'files': file,\n            \"stagingDir\": folder,\n        }]\n\n        self.log.debug('Collected workfile instance: {}'.format(file))\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/collect_workscene_fps.py",
    "content": "import pyblish.api\nimport hou\n\n\nclass CollectWorksceneFPS(pyblish.api.ContextPlugin):\n    \"\"\"Get the FPS of the work scene.\"\"\"\n\n    label = \"Workscene FPS\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"houdini\"]\n\n    def process(self, context):\n        fps = hou.fps()\n        self.log.info(\"Workscene FPS: %s\" % fps)\n        context.data.update({\"fps\": fps})\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/extract_alembic.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.houdini.api.lib import render_rop\n\nimport hou\n\n\nclass ExtractAlembic(publish.Extractor):\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract Alembic\"\n    hosts = [\"houdini\"]\n    families = [\"abc\", \"camera\"]\n    targets = [\"local\", \"remote\"]\n\n    def process(self, instance):\n        if instance.data.get(\"farm\"):\n            self.log.debug(\"Should be processed on farm, skipping.\")\n            return\n\n        ropnode = hou.node(instance.data[\"instance_node\"])\n\n        # Get the filename from the filename parameter\n        output = ropnode.evalParm(\"filename\")\n        staging_dir = os.path.dirname(output)\n        instance.data[\"stagingDir\"] = staging_dir\n\n        file_name = os.path.basename(output)\n\n        # We run the render\n        self.log.info(\"Writing alembic '%s' to '%s'\" % (file_name,\n                                                        staging_dir))\n\n        render_rop(ropnode)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'abc',\n            'ext': 'abc',\n            'files': file_name,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/extract_ass.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.houdini.api.lib import render_rop\n\nimport hou\n\n\nclass ExtractAss(publish.Extractor):\n\n    order = pyblish.api.ExtractorOrder + 0.1\n    label = \"Extract Ass\"\n    families = [\"ass\"]\n    hosts = [\"houdini\"]\n    targets = [\"local\", \"remote\"]\n\n    def process(self, instance):\n        if instance.data.get(\"farm\"):\n            self.log.debug(\"Should be processed on farm, skipping.\")\n            return\n        ropnode = hou.node(instance.data[\"instance_node\"])\n\n        # Get the filename from the filename parameter\n        # `.evalParm(parameter)` will make sure all tokens are resolved\n        output = ropnode.evalParm(\"ar_ass_file\")\n        staging_dir = os.path.dirname(output)\n        instance.data[\"stagingDir\"] = staging_dir\n        file_name = os.path.basename(output)\n\n        # We run the render\n        self.log.info(\"Writing ASS '%s' to '%s'\" % (file_name, staging_dir))\n\n        render_rop(ropnode)\n\n        # Unfortunately user interrupting the extraction does not raise an\n        # error and thus still continues to the integrator. To capture that\n        # we make sure all files exist\n        files = instance.data[\"frames\"]\n        missing = []\n        for file_name in files:\n            full_path = os.path.normpath(os.path.join(staging_dir, file_name))\n            if not os.path.exists(full_path):\n                missing.append(full_path)\n\n        if missing:\n            raise RuntimeError(\"Failed to complete Arnold ass extraction. \"\n                               \"Missing output files: {}\".format(missing))\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        # Allow ass.gz extension as well\n        ext = \"ass.gz\" if file_name.endswith(\".ass.gz\") else \"ass\"\n\n        representation = {\n            'name': 'ass',\n            'ext': ext,\n            \"files\": files,\n            \"stagingDir\": staging_dir,\n            \"frameStart\": instance.data[\"frameStartHandle\"],\n            \"frameEnd\": instance.data[\"frameEndHandle\"],\n        }\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/extract_bgeo.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.houdini.api.lib import render_rop\nfrom openpype.hosts.houdini.api import lib\n\nimport hou\n\n\nclass ExtractBGEO(publish.Extractor):\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract BGEO\"\n    hosts = [\"houdini\"]\n    families = [\"bgeo\"]\n\n    def process(self, instance):\n        if instance.data.get(\"farm\"):\n            self.log.debug(\"Should be processed on farm, skipping.\")\n            return\n        ropnode = hou.node(instance.data[\"instance_node\"])\n\n        # Get the filename from the filename parameter\n        output = ropnode.evalParm(\"sopoutput\")\n        staging_dir, file_name = os.path.split(output)\n        instance.data[\"stagingDir\"] = staging_dir\n\n        # We run the render\n        self.log.info(\"Writing bgeo files '{}' to '{}'.\".format(\n            file_name, staging_dir))\n\n        # write files\n        render_rop(ropnode)\n\n        output = instance.data[\"frames\"]\n\n        _, ext = lib.splitext(\n            output[0], allowed_multidot_extensions=[\n                \".ass.gz\", \".bgeo.sc\", \".bgeo.gz\",\n                \".bgeo.lzma\", \".bgeo.bz2\"])\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            \"name\": \"bgeo\",\n            \"ext\": ext.lstrip(\".\"),\n            \"files\": output,\n            \"stagingDir\": staging_dir,\n            \"frameStart\": instance.data[\"frameStartHandle\"],\n            \"frameEnd\": instance.data[\"frameEndHandle\"]\n        }\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/extract_composite.py",
    "content": "import os\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.houdini.api.lib import render_rop, splitext\n\nimport hou\n\n\nclass ExtractComposite(publish.Extractor):\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract Composite (Image Sequence)\"\n    hosts = [\"houdini\"]\n    families = [\"imagesequence\"]\n\n    def process(self, instance):\n\n        ropnode = hou.node(instance.data[\"instance_node\"])\n\n        # Get the filename from the copoutput parameter\n        # `.evalParm(parameter)` will make sure all tokens are resolved\n        output = ropnode.evalParm(\"copoutput\")\n        staging_dir = os.path.dirname(output)\n        instance.data[\"stagingDir\"] = staging_dir\n        file_name = os.path.basename(output)\n\n        self.log.info(\"Writing comp '%s' to '%s'\" % (file_name, staging_dir))\n\n        render_rop(ropnode)\n\n        output = instance.data[\"frames\"]\n        _, ext = splitext(output[0], [])\n        ext = ext.lstrip(\".\")\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            \"name\": ext,\n            \"ext\": ext,\n            \"files\": output,\n            \"stagingDir\": staging_dir,\n            \"frameStart\": instance.data[\"frameStartHandle\"],\n            \"frameEnd\": instance.data[\"frameEndHandle\"],\n        }\n\n        from pprint import pformat\n\n        self.log.info(pformat(representation))\n\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/extract_fbx.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Fbx Extractor for houdini. \"\"\"\n\nimport os\nimport pyblish.api\nfrom openpype.pipeline import publish\nfrom openpype.hosts.houdini.api.lib import render_rop\n\nimport hou\n\n\nclass ExtractFBX(publish.Extractor):\n\n    label = \"Extract FBX\"\n    families = [\"fbx\"]\n    hosts = [\"houdini\"]\n\n    order = pyblish.api.ExtractorOrder + 0.1\n\n    def process(self, instance):\n\n        # get rop node\n        ropnode = hou.node(instance.data.get(\"instance_node\"))\n        output_file = ropnode.evalParm(\"sopoutput\")\n\n        # get staging_dir and file_name\n        staging_dir = os.path.normpath(os.path.dirname(output_file))\n        file_name = os.path.basename(output_file)\n\n        # render rop\n        self.log.debug(\"Writing FBX '%s' to '%s'\", file_name, staging_dir)\n        render_rop(ropnode)\n\n        # prepare representation\n        representation = {\n            \"name\": \"fbx\",\n            \"ext\": \"fbx\",\n            \"files\": file_name,\n            \"stagingDir\": staging_dir\n        }\n\n        # A single frame may also be rendered without start/end frame.\n        if \"frameStartHandle\" in instance.data and \"frameEndHandle\" in instance.data:  # noqa\n            representation[\"frameStart\"] = instance.data[\"frameStartHandle\"]\n            representation[\"frameEnd\"] = instance.data[\"frameEndHandle\"]\n\n        # set value type for 'representations' key to list\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        # update instance data\n        instance.data[\"stagingDir\"] = staging_dir\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/extract_hda.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nfrom pprint import pformat\nimport pyblish.api\nfrom openpype.pipeline import publish\nimport hou\n\n\nclass ExtractHDA(publish.Extractor):\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract HDA\"\n    hosts = [\"houdini\"]\n    families = [\"hda\"]\n\n    def process(self, instance):\n        self.log.info(pformat(instance.data))\n        hda_node = hou.node(instance.data.get(\"instance_node\"))\n        hda_def = hda_node.type().definition()\n        hda_options = hda_def.options()\n        hda_options.setSaveInitialParmsAndContents(True)\n\n        next_version = instance.data[\"anatomyData\"][\"version\"]\n        self.log.info(\"setting version: {}\".format(next_version))\n        hda_def.setVersion(str(next_version))\n        hda_def.setOptions(hda_options)\n        hda_def.save(hda_def.libraryFilePath(), hda_node, hda_options)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        file = os.path.basename(hda_def.libraryFilePath())\n        staging_dir = os.path.dirname(hda_def.libraryFilePath())\n        self.log.info(\"Using HDA from {}\".format(hda_def.libraryFilePath()))\n\n        representation = {\n            'name': 'hda',\n            'ext': 'hda',\n            'files': file,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/extract_mantra_ifd.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\n\nimport hou\n\n\nclass ExtractMantraIFD(publish.Extractor):\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract Mantra ifd\"\n    hosts = [\"houdini\"]\n    families = [\"mantraifd\"]\n    targets = [\"local\", \"remote\"]\n\n    def process(self, instance):\n        if instance.data.get(\"farm\"):\n            self.log.debug(\"Should be processed on farm, skipping.\")\n            return\n\n        ropnode = hou.node(instance.data.get(\"instance_node\"))\n        output = ropnode.evalParm(\"soho_diskfile\")\n        staging_dir = os.path.dirname(output)\n        instance.data[\"stagingDir\"] = staging_dir\n\n        files = instance.data[\"frames\"]\n        missing_frames = [\n            frame\n            for frame in instance.data[\"frames\"]\n            if not os.path.exists(\n                os.path.normpath(os.path.join(staging_dir, frame)))\n        ]\n        if missing_frames:\n            raise RuntimeError(\"Failed to complete Mantra ifd extraction. \"\n                               \"Missing output files: {}\".format(\n                                   missing_frames))\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'ifd',\n            'ext': 'ifd',\n            'files': files,\n            \"stagingDir\": staging_dir,\n            \"frameStart\": instance.data[\"frameStart\"],\n            \"frameEnd\": instance.data[\"frameEnd\"],\n        }\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/extract_opengl.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.houdini.api.lib import render_rop\n\nimport hou\n\n\nclass ExtractOpenGL(publish.Extractor):\n\n    order = pyblish.api.ExtractorOrder - 0.01\n    label = \"Extract OpenGL\"\n    families = [\"review\"]\n    hosts = [\"houdini\"]\n\n    def process(self, instance):\n        ropnode = hou.node(instance.data.get(\"instance_node\"))\n\n        output = ropnode.evalParm(\"picture\")\n        staging_dir = os.path.normpath(os.path.dirname(output))\n        instance.data[\"stagingDir\"] = staging_dir\n        file_name = os.path.basename(output)\n\n        self.log.info(\"Extracting '%s' to '%s'\" % (file_name,\n                                                   staging_dir))\n\n        render_rop(ropnode)\n\n        output = instance.data[\"frames\"]\n\n        tags = [\"review\"]\n        if not instance.data.get(\"keepImages\"):\n            tags.append(\"delete\")\n\n        representation = {\n            \"name\": instance.data[\"imageFormat\"],\n            \"ext\": instance.data[\"imageFormat\"],\n            \"files\": output,\n            \"stagingDir\": staging_dir,\n            \"frameStart\": instance.data[\"frameStartHandle\"],\n            \"frameEnd\": instance.data[\"frameEndHandle\"],\n            \"tags\": tags,\n            \"preview\": True,\n            \"camera_name\": instance.data.get(\"review_camera\")\n        }\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.houdini.api.lib import render_rop\n\nimport hou\n\n\nclass ExtractRedshiftProxy(publish.Extractor):\n\n    order = pyblish.api.ExtractorOrder + 0.1\n    label = \"Extract Redshift Proxy\"\n    families = [\"redshiftproxy\"]\n    hosts = [\"houdini\"]\n    targets = [\"local\", \"remote\"]\n\n    def process(self, instance):\n        if instance.data.get(\"farm\"):\n            self.log.debug(\"Should be processed on farm, skipping.\")\n            return\n        ropnode = hou.node(instance.data.get(\"instance_node\"))\n\n        # Get the filename from the filename parameter\n        # `.evalParm(parameter)` will make sure all tokens are resolved\n        output = ropnode.evalParm(\"RS_archive_file\")\n        staging_dir = os.path.normpath(os.path.dirname(output))\n        instance.data[\"stagingDir\"] = staging_dir\n        file_name = os.path.basename(output)\n\n        self.log.info(\"Writing Redshift Proxy '%s' to '%s'\" % (file_name,\n                                                               staging_dir))\n\n        render_rop(ropnode)\n\n        output = instance.data[\"frames\"]\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            \"name\": \"rs\",\n            \"ext\": \"rs\",\n            \"files\": output,\n            \"stagingDir\": staging_dir,\n        }\n\n        # A single frame may also be rendered without start/end frame.\n        if \"frameStartHandle\" in instance.data and \"frameEndHandle\" in instance.data:  # noqa\n            representation[\"frameStart\"] = instance.data[\"frameStartHandle\"]\n            representation[\"frameEnd\"] = instance.data[\"frameEndHandle\"]\n\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/extract_usd.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.houdini.api.lib import render_rop\n\nimport hou\n\nclass ExtractUSD(publish.Extractor):\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract USD\"\n    hosts = [\"houdini\"]\n    families = [\"usd\",\n                \"usdModel\",\n                \"usdSetDress\"]\n\n    def process(self, instance):\n\n        ropnode = hou.node(instance.data.get(\"instance_node\"))\n\n        # Get the filename from the filename parameter\n        output = ropnode.evalParm(\"lopoutput\")\n        staging_dir = os.path.dirname(output)\n        instance.data[\"stagingDir\"] = staging_dir\n        file_name = os.path.basename(output)\n\n        self.log.info(\"Writing USD '%s' to '%s'\" % (file_name, staging_dir))\n\n        render_rop(ropnode)\n\n        assert os.path.exists(output), \"Output does not exist: %s\" % output\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'usd',\n            'ext': 'usd',\n            'files': file_name,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/extract_usd_layered.py",
    "content": "import os\nimport contextlib\nimport hou\nimport sys\nfrom collections import deque\n\nimport pyblish.api\n\nfrom openpype.client import (\n    get_asset_by_name,\n    get_subset_by_name,\n    get_last_version_by_subset_id,\n    get_representation_by_name,\n)\nfrom openpype.pipeline import (\n    get_representation_path,\n    publish,\n)\nimport openpype.hosts.houdini.api.usd as hou_usdlib\nfrom openpype.hosts.houdini.api.lib import render_rop\n\n\nclass ExitStack(object):\n    \"\"\"Context manager for dynamic management of a stack of exit callbacks.\n\n    For example:\n\n        with ExitStack() as stack:\n            files = [stack.enter_context(open(fname)) for fname in filenames]\n            # All opened files will automatically be closed at the end of\n            # the with statement, even if attempts to open files later\n            # in the list raise an exception\n\n    \"\"\"\n\n    def __init__(self):\n        self._exit_callbacks = deque()\n\n    def pop_all(self):\n        \"\"\"Preserve the context stack by transferring it to a new instance\"\"\"\n        new_stack = type(self)()\n        new_stack._exit_callbacks = self._exit_callbacks\n        self._exit_callbacks = deque()\n        return new_stack\n\n    def _push_cm_exit(self, cm, cm_exit):\n        \"\"\"Helper to correctly register callbacks to __exit__ methods\"\"\"\n\n        def _exit_wrapper(*exc_details):\n            return cm_exit(cm, *exc_details)\n\n        _exit_wrapper.__self__ = cm\n        self.push(_exit_wrapper)\n\n    def push(self, exit):\n        \"\"\"Registers a callback with the standard __exit__ method signature.\n\n        Can suppress exceptions the same way __exit__ methods can.\n\n        Also accepts any object with an __exit__ method (registering a call\n        to the method instead of the object itself)\n\n        \"\"\"\n        # We use an unbound method rather than a bound method to follow\n        # the standard lookup behaviour for special methods\n        _cb_type = type(exit)\n        try:\n            exit_method = _cb_type.__exit__\n        except AttributeError:\n            # Not a context manager, so assume its a callable\n            self._exit_callbacks.append(exit)\n        else:\n            self._push_cm_exit(exit, exit_method)\n        return exit  # Allow use as a decorator\n\n    def callback(self, callback, *args, **kwds):\n        \"\"\"Registers an arbitrary callback and arguments.\n\n        Cannot suppress exceptions.\n        \"\"\"\n\n        def _exit_wrapper(exc_type, exc, tb):\n            callback(*args, **kwds)\n\n        # We changed the signature, so using @wraps is not appropriate, but\n        # setting __wrapped__ may still help with introspection\n        _exit_wrapper.__wrapped__ = callback\n        self.push(_exit_wrapper)\n        return callback  # Allow use as a decorator\n\n    def enter_context(self, cm):\n        \"\"\"Enters the supplied context manager\n\n        If successful, also pushes its __exit__ method as a callback and\n        returns the result of the __enter__ method.\n        \"\"\"\n        # We look up the special methods on the type to match the with\n        # statement\n        _cm_type = type(cm)\n        _exit = _cm_type.__exit__\n        result = _cm_type.__enter__(cm)\n        self._push_cm_exit(cm, _exit)\n        return result\n\n    def close(self):\n        \"\"\"Immediately unwind the context stack\"\"\"\n        self.__exit__(None, None, None)\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, *exc_details):\n        # We manipulate the exception state so it behaves as though\n        # we were actually nesting multiple with statements\n        frame_exc = sys.exc_info()[1]\n\n        def _fix_exception_context(new_exc, old_exc):\n            while 1:\n                exc_context = new_exc.__context__\n                if exc_context in (None, frame_exc):\n                    break\n                new_exc = exc_context\n            new_exc.__context__ = old_exc\n\n        # Callbacks are invoked in LIFO order to match the behaviour of\n        # nested context managers\n        suppressed_exc = False\n        while self._exit_callbacks:\n            cb = self._exit_callbacks.pop()\n            try:\n                if cb(*exc_details):\n                    suppressed_exc = True\n                    exc_details = (None, None, None)\n            except Exception:\n                new_exc_details = sys.exc_info()\n                # simulate the stack of exceptions by setting the context\n                _fix_exception_context(new_exc_details[1], exc_details[1])\n                if not self._exit_callbacks:\n                    raise\n                exc_details = new_exc_details\n        return suppressed_exc\n\n\n@contextlib.contextmanager\ndef parm_values(overrides):\n    \"\"\"Override Parameter values during the context.\"\"\"\n\n    originals = []\n    try:\n        for parm, value in overrides:\n            originals.append((parm, parm.eval()))\n            parm.set(value)\n        yield\n    finally:\n        for parm, value in originals:\n            # Parameter might not exist anymore so first\n            # check whether it's still valid\n            if hou.parm(parm.path()):\n                parm.set(value)\n\n\nclass ExtractUSDLayered(publish.Extractor):\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract Layered USD\"\n    hosts = [\"houdini\"]\n    families = [\"usdLayered\", \"usdShade\"]\n\n    # Force Output Processors so it will always save any file\n    # into our unique staging directory with processed Avalon paths\n    output_processors = [\"avalon_uri_processor\", \"stagingdir_processor\"]\n\n    def process(self, instance):\n\n        self.log.info(\"Extracting: %s\" % instance)\n\n        staging_dir = self.staging_dir(instance)\n        fname = instance.data.get(\"usdFilename\")\n\n        # The individual rop nodes are collected as \"publishDependencies\"\n        dependencies = instance.data[\"publishDependencies\"]\n        ropnodes = [dependency[0] for dependency in dependencies]\n        assert all(\n            node.type().name() in {\"usd\", \"usd_rop\"} for node in ropnodes\n        )\n\n        # Main ROP node, either a USD Rop or ROP network with\n        # multiple USD ROPs\n        node = hou.node(instance.data[\"instance_node\"])\n\n        # Collect any output dependencies that have not been processed yet\n        # during extraction of other instances\n        outputs = [fname]\n        active_dependencies = [\n            dep\n            for dep in dependencies\n            if dep.data.get(\"publish\", True)\n            and not dep.data.get(\"_isExtracted\", False)\n        ]\n        for dependency in active_dependencies:\n            outputs.append(dependency.data[\"usdFilename\"])\n\n        pattern = r\"*[/\\]{0} {0}\"\n        save_pattern = \" \".join(pattern.format(fname) for fname in outputs)\n\n        # Run a stack of context managers before we start the render to\n        # temporarily adjust USD ROP settings for our publish output.\n        rop_overrides = {\n            # This sets staging directory on the processor to force our\n            # output files to end up in the Staging Directory.\n            \"stagingdiroutputprocessor_stagingDir\": staging_dir,\n            # Force the Avalon URI Output Processor to refactor paths for\n            # references, payloads and layers to published paths.\n            \"avalonurioutputprocessor_use_publish_paths\": True,\n            # Only write out specific USD files based on our outputs\n            \"savepattern\": save_pattern,\n        }\n        overrides = list()\n        with ExitStack() as stack:\n\n            for ropnode in ropnodes:\n                manager = hou_usdlib.outputprocessors(\n                    ropnode,\n                    processors=self.output_processors,\n                    disable_all_others=True,\n                )\n                stack.enter_context(manager)\n\n                # Some of these must be added after we enter the output\n                # processor context manager because those parameters only\n                # exist when the Output Processor is added to the ROP node.\n                for name, value in rop_overrides.items():\n                    parm = ropnode.parm(name)\n                    assert parm, \"Parm not found: %s.%s\" % (\n                        ropnode.path(),\n                        name,\n                    )\n                    overrides.append((parm, value))\n\n            stack.enter_context(parm_values(overrides))\n\n            # Render the single ROP node or the full ROP network\n            render_rop(node)\n\n        # Assert all output files in the Staging Directory\n        for output_fname in outputs:\n            path = os.path.join(staging_dir, output_fname)\n            assert os.path.exists(path), \"Output file must exist: %s\" % path\n\n        # Set up the dependency for publish if they have new content\n        # compared to previous publishes\n        project_name = instance.context.data[\"projectName\"]\n        for dependency in active_dependencies:\n            dependency_fname = dependency.data[\"usdFilename\"]\n\n            filepath = os.path.join(staging_dir, dependency_fname)\n            similar = self._compare_with_latest_publish(\n                project_name, dependency, filepath\n            )\n            if similar:\n                # Deactivate this dependency\n                self.log.debug(\n                    \"Dependency matches previous publish version,\"\n                    \" deactivating %s for publish\" % dependency\n                )\n                dependency.data[\"publish\"] = False\n            else:\n                self.log.debug(\"Extracted dependency: %s\" % dependency)\n                # This dependency should be published\n                dependency.data[\"files\"] = [dependency_fname]\n                dependency.data[\"stagingDir\"] = staging_dir\n                dependency.data[\"_isExtracted\"] = True\n\n        # Store the created files on the instance\n        if \"files\" not in instance.data:\n            instance.data[\"files\"] = []\n        instance.data[\"files\"].append(fname)\n\n    def _compare_with_latest_publish(self, project_name, dependency, new_file):\n        import filecmp\n\n        _, ext = os.path.splitext(new_file)\n\n        # Compare this dependency with the latest published version\n        # to detect whether we should make this into a new publish\n        # version. If not, skip it.\n        asset = get_asset_by_name(\n            project_name, dependency.data[\"asset\"], fields=[\"_id\"]\n        )\n        subset = get_subset_by_name(\n            project_name,\n            dependency.data[\"subset\"],\n            asset[\"_id\"],\n            fields=[\"_id\"]\n        )\n        if not subset:\n            # Subset doesn't exist yet. Definitely new file\n            self.log.debug(\"No existing subset..\")\n            return False\n\n        version = get_last_version_by_subset_id(\n            project_name, subset[\"_id\"], fields=[\"_id\"]\n        )\n        if not version:\n            self.log.debug(\"No existing version..\")\n            return False\n\n        representation = get_representation_by_name(\n            project_name, ext.lstrip(\".\"), version[\"_id\"]\n        )\n        if not representation:\n            self.log.debug(\"No existing representation..\")\n            return False\n\n        old_file = get_representation_path(representation)\n        if not os.path.exists(old_file):\n            return False\n\n        return filecmp.cmp(old_file, new_file)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.houdini.api.lib import render_rop\n\nimport hou\n\n\nclass ExtractVDBCache(publish.Extractor):\n\n    order = pyblish.api.ExtractorOrder + 0.1\n    label = \"Extract VDB Cache\"\n    families = [\"vdbcache\"]\n    hosts = [\"houdini\"]\n\n    def process(self, instance):\n        if instance.data.get(\"farm\"):\n            self.log.debug(\"Should be processed on farm, skipping.\")\n            return\n        ropnode = hou.node(instance.data[\"instance_node\"])\n\n        # Get the filename from the filename parameter\n        # `.evalParm(parameter)` will make sure all tokens are resolved\n        sop_output = ropnode.evalParm(\"sopoutput\")\n        staging_dir = os.path.normpath(os.path.dirname(sop_output))\n        instance.data[\"stagingDir\"] = staging_dir\n        file_name = os.path.basename(sop_output)\n\n        self.log.info(\"Writing VDB '%s' to '%s'\" % (file_name, staging_dir))\n\n        render_rop(ropnode)\n\n        output = instance.data[\"frames\"]\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            \"name\": \"vdb\",\n            \"ext\": \"vdb\",\n            \"files\": output,\n            \"stagingDir\": staging_dir,\n            \"frameStart\": instance.data[\"frameStartHandle\"],\n            \"frameEnd\": instance.data[\"frameEndHandle\"],\n        }\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Invalid VDB</title>\n<description>\n## Invalid VDB output\n\nAll primitives of the output geometry must be VDBs, no other primitive\ntypes are allowed. That means that regardless of the amount of VDBs in the\ngeometry it will have an equal amount of VDBs, points, primitives and\nvertices since each VDB primitive is one point, one vertex and one VDB.\n\nThis validation only checks the geometry on the first frame of the export\nframe range.\n\n\n\n</description>\n<detail>\n### Detailed Info\n\nROP node `{rop_path}` is set to export SOP path `{sop_path}`.\n\n{message}\n\n</detail>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/increment_current_file.py",
    "content": "import pyblish.api\n\nfrom openpype.lib import version_up\nfrom openpype.pipeline import registered_host\nfrom openpype.pipeline.publish import get_errored_plugins_from_context\nfrom openpype.hosts.houdini.api import HoudiniHost\nfrom openpype.pipeline.publish import KnownPublishError\n\n\nclass IncrementCurrentFile(pyblish.api.ContextPlugin):\n    \"\"\"Increment the current file.\n\n    Saves the current scene with an increased version number.\n\n    \"\"\"\n\n    label = \"Increment current file\"\n    order = pyblish.api.IntegratorOrder + 9.0\n    hosts = [\"houdini\"]\n    families = [\"workfile\",\n                \"redshift_rop\",\n                \"arnold_rop\",\n                \"mantra_rop\",\n                \"karma_rop\",\n                \"usdrender\",\n                \"publish.hou\"]\n    optional = True\n\n    def process(self, context):\n\n        errored_plugins = get_errored_plugins_from_context(context)\n        if any(\n            plugin.__name__ == \"HoudiniSubmitPublishDeadline\"\n            for plugin in errored_plugins\n        ):\n            raise KnownPublishError(\n                \"Skipping incrementing current file because \"\n                \"submission to deadline failed.\"\n            )\n\n        # Filename must not have changed since collecting\n        host = registered_host()  # type: HoudiniHost\n        current_file = host.current_file()\n        if context.data[\"currentFile\"] != current_file:\n            raise KnownPublishError(\n                \"Collected filename mismatches from current scene name.\"\n            )\n\n        new_filepath = version_up(current_file)\n        host.save_workfile(new_filepath)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/save_scene.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import registered_host\n\n\nclass SaveCurrentScene(pyblish.api.ContextPlugin):\n    \"\"\"Save current scene\"\"\"\n\n    label = \"Save current file\"\n    order = pyblish.api.ExtractorOrder - 0.49\n    hosts = [\"houdini\"]\n\n    def process(self, context):\n\n        # Filename must not have changed since collecting\n        host = registered_host()\n        current_file = host.get_current_workfile()\n        assert context.data['currentFile'] == current_file, (\n            \"Collected filename from current scene name.\"\n        )\n\n        if host.workfile_has_unsaved_changes():\n            self.log.info(\"Saving current file: {}\".format(current_file))\n            host.save_workfile(current_file)\n        else:\n            self.log.debug(\"No unsaved changes, skipping file save..\")\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\n\nfrom collections import defaultdict\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):\n    \"\"\"Validate Alembic ROP Primitive to Detail attribute is consistent.\n\n    The Alembic ROP crashes Houdini whenever an attribute in the \"Primitive to\n    Detail\" parameter exists on only a part of the primitives that belong to\n    the same hierarchy path. Whenever it encounters inconsistent values,\n    specifically where some are empty as opposed to others then Houdini\n    crashes. (Tested in Houdini 17.5.229)\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder + 0.1\n    families = [\"abc\"]\n    hosts = [\"houdini\"]\n    label = \"Validate Primitive to Detail (Abc)\"\n\n    def process(self, instance):\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Primitives found with inconsistent primitive \"\n                 \"to detail attributes. See log.\"),\n                title=self.label\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n        import hou  # noqa\n        output_node = instance.data.get(\"output_node\")\n        rop_node = hou.node(instance.data[\"instance_node\"])\n        if output_node is None:\n            cls.log.error(\n                \"SOP Output node in '%s' does not exist. \"\n                \"Ensure a valid SOP output path is set.\" % rop_node.path()\n            )\n\n            return [rop_node.path()]\n\n        pattern = rop_node.parm(\"prim_to_detail_pattern\").eval().strip()\n        if not pattern:\n            cls.log.debug(\n                \"Alembic ROP has no 'Primitive to Detail' pattern. \"\n                \"Validation is ignored..\"\n            )\n            return\n\n        build_from_path = rop_node.parm(\"build_from_path\").eval()\n        if not build_from_path:\n            cls.log.debug(\n                \"Alembic ROP has 'Build from Path' disabled. \"\n                \"Validation is ignored..\"\n            )\n            return\n\n        path_attr = rop_node.parm(\"path_attrib\").eval()\n        if not path_attr:\n            cls.log.error(\n                \"The Alembic ROP node has no Path Attribute\"\n                \"value set, but 'Build Hierarchy from Attribute'\"\n                \"is enabled.\"\n            )\n            return [rop_node.path()]\n\n        # Let's assume each attribute is explicitly named for now and has no\n        # wildcards for Primitive to Detail. This simplifies the check.\n        cls.log.debug(\"Checking Primitive to Detail pattern: %s\" % pattern)\n        cls.log.debug(\"Checking with path attribute: %s\" % path_attr)\n\n        if not hasattr(output_node, \"geometry\"):\n            # In the case someone has explicitly set an Object\n            # node instead of a SOP node in Geometry context\n            # then for now we ignore - this allows us to also\n            # export object transforms.\n            cls.log.warning(\"No geometry output node found, skipping check..\")\n            return\n\n        # Check if the primitive attribute exists\n        frame = instance.data.get(\"frameStart\", 0)\n        geo = output_node.geometryAtFrame(frame)\n\n        # If there are no primitives on the start frame then it might be\n        # something that is emitted over time. As such we can't actually\n        # validate whether the attributes exist, because they won't exist\n        # yet. In that case, just warn the user and allow it.\n        if len(geo.iterPrims()) == 0:\n            cls.log.warning(\n                \"No primitives found on current frame. Validation\"\n                \" for Primitive to Detail will be skipped.\"\n            )\n            return\n\n        attrib = geo.findPrimAttrib(path_attr)\n        if not attrib:\n            cls.log.info(\n                \"Geometry Primitives are missing \"\n                \"path attribute: `%s`\" % path_attr\n            )\n            return [output_node.path()]\n\n        # Ensure at least a single string value is present\n        if not attrib.strings():\n            cls.log.info(\n                \"Primitive path attribute has no \"\n                \"string values: %s\" % path_attr\n            )\n            return [output_node.path()]\n\n        paths = None\n        for attr in pattern.split(\" \"):\n            if not attr.strip():\n                # Ignore empty values\n                continue\n\n            # Check if the primitive attribute exists\n            attrib = geo.findPrimAttrib(attr)\n            if not attrib:\n                # It is allowed to not have the attribute at all\n                continue\n\n            # The issue can only happen if at least one string attribute is\n            # present. So we ignore cases with no values whatsoever.\n            if not attrib.strings():\n                continue\n\n            check = defaultdict(set)\n            values = geo.primStringAttribValues(attr)\n            if paths is None:\n                paths = geo.primStringAttribValues(path_attr)\n\n            for path, value in zip(paths, values):\n                check[path].add(value)\n\n            for path, values in check.items():\n                # Whenever a single path has multiple values for the\n                # Primitive to Detail attribute then we consider it\n                # inconsistent and invalidate the ROP node's content.\n                if len(values) > 1:\n                    cls.log.warning(\n                        \"Path has multiple values: %s (path: %s)\"\n                        % (list(values), path)\n                    )\n                    return [output_node.path()]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_alembic_face_sets.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nimport hou\n\nclass ValidateAlembicROPFaceSets(pyblish.api.InstancePlugin):\n    \"\"\"Validate Face Sets are disabled for extraction to pointcache.\n\n    When groups are saved as Face Sets with the Alembic these show up\n    as shadingEngine connections in Maya - however, with animated groups\n    these connections in Maya won't work as expected, it won't update per\n    frame. Additionally, it can break shader assignments in some cases\n    where it requires to first break this connection to allow a shader to\n    be assigned.\n\n    It is allowed to include Face Sets, so only an issue is logged to\n    identify that it could introduce issues down the pipeline.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder + 0.1\n    families = [\"abc\"]\n    hosts = [\"houdini\"]\n    label = \"Validate Alembic ROP Face Sets\"\n\n    def process(self, instance):\n\n        rop = hou.node(instance.data[\"instance_node\"])\n        facesets = rop.parm(\"facesets\").eval()\n\n        # 0 = No Face Sets\n        # 1 = Save Non-Empty Groups as Face Sets\n        # 2 = Save All Groups As Face Sets\n        if facesets != 0:\n            self.log.warning(\n                \"Alembic ROP saves 'Face Sets' for Geometry. \"\n                \"Are you sure you want this?\"\n            )\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_alembic_input_node.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\nimport hou\n\n\nclass ValidateAlembicInputNode(pyblish.api.InstancePlugin):\n    \"\"\"Validate that the node connected to the output is correct.\n\n    The connected node cannot be of the following types for Alembic:\n        - VDB\n        - Volume\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder + 0.1\n    families = [\"abc\"]\n    hosts = [\"houdini\"]\n    label = \"Validate Input Node (Abc)\"\n\n    def process(self, instance):\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Primitive types found that are not supported \"\n                 \"for Alembic output.\"),\n                title=self.label\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        invalid_prim_types = [\"VDB\", \"Volume\"]\n        output_node = instance.data.get(\"output_node\")\n\n        if output_node is None:\n            node = hou.node(instance.data[\"instance_node\"])\n            cls.log.error(\n                \"SOP Output node in '%s' does not exist. \"\n                \"Ensure a valid SOP output path is set.\" % node.path()\n            )\n\n            return [node.path()]\n\n        if not hasattr(output_node, \"geometry\"):\n            # In the case someone has explicitly set an Object\n            # node instead of a SOP node in Geometry context\n            # then for now we ignore - this allows us to also\n            # export object transforms.\n            cls.log.warning(\"No geometry output node found, skipping check..\")\n            return\n\n        frame = instance.data.get(\"frameStart\", 0)\n        geo = output_node.geometryAtFrame(frame)\n\n        invalid = False\n        for prim_type in invalid_prim_types:\n            if geo.countPrimType(prim_type) > 0:\n                cls.log.error(\n                    \"Found a primitive which is of type '%s' !\" % prim_type\n                )\n                invalid = True\n\n        if invalid:\n            return [instance]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_animation_settings.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import PublishValidationError\nfrom openpype.hosts.houdini.api import lib\nimport hou\n\n\nclass ValidateAnimationSettings(pyblish.api.InstancePlugin):\n    \"\"\"Validate if the unexpanded string contains the frame ('$F') token\n\n    This validator will only check the output parameter of the node if\n    the Valid Frame Range is not set to 'Render Current Frame'\n\n    Rules:\n        If you render out a frame range it is mandatory to have the\n        frame token - '$F4' or similar - to ensure that each frame gets\n        written. If this is not the case you will override the same file\n        every time a frame is written out.\n\n    Examples:\n        Good: 'my_vbd_cache.$F4.vdb'\n        Bad: 'my_vbd_cache.vdb'\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Frame Settings\"\n    families = [\"vdbcache\"]\n\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Output settings do no match for '%s'\" % instance\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        node = hou.node(instance.data[\"instance_node\"])\n        # Check trange parm, 0 means Render Current Frame\n        frame_range = node.evalParm(\"trange\")\n        if frame_range == 0:\n            return []\n\n        output_parm = lib.get_output_parameter(node)\n        unexpanded_str = output_parm.unexpandedString()\n\n        if \"$F\" not in unexpanded_str:\n            cls.log.error(\"No frame token found in '%s'\" % node.path())\n            return [instance]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_bypass.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\nimport hou\n\nclass ValidateBypassed(pyblish.api.InstancePlugin):\n    \"\"\"Validate all primitives build hierarchy from attribute when enabled.\n\n    The name of the attribute must exist on the prims and have the same name\n    as Build Hierarchy from Attribute's `Path Attribute` value on the Alembic\n    ROP node whenever Build Hierarchy from Attribute is enabled.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder - 0.1\n    families = [\"*\"]\n    hosts = [\"houdini\"]\n    label = \"Validate ROP Bypass\"\n\n    def process(self, instance):\n\n        if len(instance) == 0:\n            # Ignore instances without any nodes\n            # e.g. in memory bootstrap instances\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            rop = invalid[0]\n            raise PublishValidationError(\n                (\"ROP node {} is set to bypass, publishing cannot \"\n                 \"continue.\".format(rop.path())),\n                title=self.label\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        rop = hou.node(instance.data[\"instance_node\"])\n        if hasattr(rop, \"isBypassed\") and rop.isBypassed():\n            return [rop]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_camera_rop.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validator plugin for Houdini Camera ROP settings.\"\"\"\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateCameraROP(pyblish.api.InstancePlugin):\n    \"\"\"Validate Camera ROP settings.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"camera\"]\n    hosts = [\"houdini\"]\n    label = \"Camera ROP\"\n\n    def process(self, instance):\n\n        import hou\n\n        node = hou.node(instance.data.get(\"instance_node\"))\n        if node.parm(\"use_sop_path\").eval():\n            raise PublishValidationError(\n                (\"Alembic ROP for Camera export should not be \"\n                 \"set to 'Use Sop Path'. Please disable.\"),\n                title=self.label\n            )\n\n        # Get the root and objects parameter of the Alembic ROP node\n        root = node.parm(\"root\").eval()\n        objects = node.parm(\"objects\").eval()\n        errors = []\n        if not root:\n            errors.append(\"Root parameter must be set on Alembic ROP\")\n        if not root.startswith(\"/\"):\n            errors.append(\"Root parameter must start with slash /\")\n        if not objects:\n            errors.append(\"Objects parameter must be set on Alembic ROP\")\n        if len(objects.split(\" \")) != 1:\n            errors.append(\"Must have only a single object.\")\n\n        if errors:\n            for error in errors:\n                self.log.error(error)\n            raise PublishValidationError(\n                \"Some checks failed, see validator log.\",\n                title=self.label)\n\n        # Check if the object exists and is a camera\n        path = root + \"/\" + objects\n        camera = hou.node(path)\n\n        if not camera:\n            raise PublishValidationError(\n                \"Camera path does not exist: %s\" % path,\n                title=self.label)\n\n        if camera.type().name() != \"cam\":\n            raise PublishValidationError(\n                (\"Object set in Alembic ROP is not a camera: \"\n                 \"{} (type: {})\").format(camera, camera.type().name()),\n                title=self.label)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_cop_output_node.py",
    "content": "# -*- coding: utf-8 -*-\nimport sys\nimport pyblish.api\nimport six\n\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateCopOutputNode(pyblish.api.InstancePlugin):\n    \"\"\"Validate the instance COP Output Node.\n\n    This will ensure:\n        - The COP Path is set.\n        - The COP Path refers to an existing object.\n        - The COP Path node is a COP node.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"imagesequence\"]\n    hosts = [\"houdini\"]\n    label = \"Validate COP Output Node\"\n\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Output node(s) `{}` are incorrect. \"\n                 \"See plug-in log for details.\").format(invalid),\n                title=self.label\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        import hou\n\n        try:\n            output_node = instance.data[\"output_node\"]\n        except KeyError:\n            six.reraise(\n                PublishValidationError,\n                PublishValidationError(\n                    \"Can't determine COP output node.\",\n                    title=cls.__name__),\n                sys.exc_info()[2]\n            )\n\n        if output_node is None:\n            node = hou.node(instance.data.get(\"instance_node\"))\n            cls.log.error(\n                \"COP Output node in '%s' does not exist. \"\n                \"Ensure a valid COP output path is set.\" % node.path()\n            )\n\n            return [node.path()]\n\n        # Output node must be a Sop node.\n        if not isinstance(output_node, hou.CopNode):\n            cls.log.error(\n                \"Output node %s is not a COP node. \"\n                \"COP Path must point to a COP node, \"\n                \"instead found category type: %s\"\n                % (output_node.path(), output_node.type().category().name())\n            )\n            return [output_node.path()]\n\n        # For the sake of completeness also assert the category type\n        # is Cop2 to avoid potential edge case scenarios even though\n        # the isinstance check above should be stricter than this category\n        if output_node.type().category().name() != \"Cop2\":\n            raise PublishValidationError(\n                (\"Output node %s is not of category Cop2. \"\n                 \"This is a bug...\").format(output_node.path()),\n                title=cls.label)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\nfrom openpype.hosts.houdini.api.action import (\n    SelectInvalidAction,\n    SelectROPAction,\n)\nfrom openpype.hosts.houdini.api.lib import get_obj_node_output\nimport hou\n\n\nclass ValidateFBXOutputNode(pyblish.api.InstancePlugin):\n    \"\"\"Validate the instance Output Node.\n\n    This will ensure:\n        - The Output Node Path is set.\n        - The Output Node Path refers to an existing object.\n        - The Output Node is a Sop or Obj node.\n        - The Output Node has geometry data.\n        - The Output Node doesn't include invalid primitive types.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"fbx\"]\n    hosts = [\"houdini\"]\n    label = \"Validate FBX Output Node\"\n    actions = [SelectROPAction, SelectInvalidAction]\n\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            nodes = [n.path() for n in invalid]\n            raise PublishValidationError(\n                \"See log for details. \"\n                \"Invalid nodes: {0}\".format(nodes),\n                title=\"Invalid output node(s)\"\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n        output_node = instance.data.get(\"output_node\")\n\n        # Check if The Output Node Path is set and\n        #  refers to an existing object.\n        if output_node is None:\n            rop_node = hou.node(instance.data[\"instance_node\"])\n            cls.log.error(\n                \"Output node in '%s' does not exist. \"\n                \"Ensure a valid output path is set.\", rop_node.path()\n            )\n\n            return [rop_node]\n\n        # Check if the Output Node is a Sop or an Obj node\n        #  also, list all sop output nodes inside as well as\n        #  invalid empty nodes.\n        all_out_sops = []\n        invalid = []\n\n        # if output_node is an ObjSubnet or an ObjNetwork\n        if output_node.childTypeCategory() == hou.objNodeTypeCategory():\n            for node in output_node.allSubChildren():\n                if node.type().name() == \"geo\":\n                    out = get_obj_node_output(node)\n                    if out:\n                        all_out_sops.append(out)\n                    else:\n                        invalid.append(node)  # empty_objs\n                        cls.log.error(\n                            \"Geo Obj Node '%s' is empty!\",\n                            node.path()\n                        )\n            if not all_out_sops:\n                invalid.append(output_node)  # empty_objs\n                cls.log.error(\n                    \"Output Node '%s' is empty!\",\n                    node.path()\n                )\n\n        # elif output_node is an ObjNode\n        elif output_node.type().name() == \"geo\":\n            out = get_obj_node_output(output_node)\n            if out:\n                all_out_sops.append(out)\n            else:\n                invalid.append(node)  # empty_objs\n                cls.log.error(\n                    \"Output Node '%s' is empty!\",\n                    node.path()\n                )\n\n        # elif output_node is a SopNode\n        elif output_node.type().category().name() == \"Sop\":\n            all_out_sops.append(output_node)\n\n        # Then it's a wrong node type\n        else:\n            cls.log.error(\n                \"Output node %s is not a SOP or OBJ Geo or OBJ SubNet node. \"\n                \"Instead found category type: %s %s\",\n                output_node.path(), output_node.type().category().name(),\n                output_node.type().name()\n            )\n            return [output_node]\n\n        # Check if all output sop nodes have geometry\n        #  and don't contain invalid prims\n        invalid_prim_types = [\"VDB\", \"Volume\"]\n        for sop_node in all_out_sops:\n            # Empty Geometry test\n            if not hasattr(sop_node, \"geometry\"):\n                invalid.append(sop_node)  # empty_geometry\n                cls.log.error(\n                    \"Sop node '%s' doesn't include any prims.\",\n                    sop_node.path()\n                )\n                continue\n\n            frame = instance.data.get(\"frameStart\", 0)\n            geo = sop_node.geometryAtFrame(frame)\n            if len(geo.iterPrims()) == 0:\n                invalid.append(sop_node)  # empty_geometry\n                cls.log.error(\n                    \"Sop node '%s' doesn't include any prims.\",\n                    sop_node.path()\n                )\n                continue\n\n            # Invalid Prims test\n            for prim_type in invalid_prim_types:\n                if geo.countPrimType(prim_type) > 0:\n                    invalid.append(sop_node)  # invalid_prims\n                    cls.log.error(\n                        \"Sop node '%s' includes invalid prims of type '%s'.\",\n                        sop_node.path(), prim_type\n                    )\n\n        if invalid:\n            return invalid\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_file_extension.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nimport pyblish.api\n\nfrom openpype.hosts.houdini.api import lib\nfrom openpype.pipeline import PublishValidationError\n\nimport hou\n\n\nclass ValidateFileExtension(pyblish.api.InstancePlugin):\n    \"\"\"Validate the output file extension fits the output family.\n\n    File extensions:\n        - Pointcache must be .abc\n        - Camera must be .abc\n        - VDB must be .vdb\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"camera\", \"vdbcache\"]\n    hosts = [\"houdini\"]\n    label = \"Output File Extension\"\n\n    family_extensions = {\n        \"camera\": \".abc\",\n        \"vdbcache\": \".vdb\",\n    }\n\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"ROP node has incorrect file extension: {}\".format(invalid),\n                title=self.label\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        # Get ROP node from instance\n        node = hou.node(instance.data[\"instance_node\"])\n\n        # Create lookup for current family in instance\n        families = []\n        family = instance.data.get(\"family\", None)\n        if family:\n            families.append(family)\n        families = set(families)\n\n        # Perform extension check\n        output = lib.get_output_parameter(node).eval()\n        _, output_extension = os.path.splitext(output)\n\n        for family in families:\n            extension = cls.family_extensions.get(family, None)\n            if extension is None:\n                raise PublishValidationError(\n                    \"Unsupported family: {}\".format(family),\n                    title=cls.label)\n\n            if output_extension != extension:\n                return [node.path()]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_frame_range.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\nfrom openpype.pipeline.publish import RepairAction\nfrom openpype.hosts.houdini.api.action import SelectInvalidAction\n\nimport hou\n\n\nclass DisableUseAssetHandlesAction(RepairAction):\n    label = \"Disable use asset handles\"\n    icon = \"mdi.toggle-switch-off\"\n\n\nclass ValidateFrameRange(pyblish.api.InstancePlugin):\n    \"\"\"Validate Frame Range.\n\n    Due to the usage of start and end handles,\n    then Frame Range must be >= (start handle + end handle)\n    which results that frameEnd be smaller than frameStart\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder - 0.1\n    hosts = [\"houdini\"]\n    label = \"Validate Frame Range\"\n    actions = [DisableUseAssetHandlesAction, SelectInvalidAction]\n\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                title=\"Invalid Frame Range\",\n                message=(\n                    \"Invalid frame range because the instance \"\n                    \"start frame ({0[frameStart]}) is higher than \"\n                    \"the end frame ({0[frameEnd]})\"\n                    .format(instance.data)\n                ),\n                description=(\n                    \"## Invalid Frame Range\\n\"\n                    \"The frame range for the instance is invalid because \"\n                    \"the start frame is higher than the end frame.\\n\\nThis \"\n                    \"is likely due to asset handles being applied to your \"\n                    \"instance or the ROP node's start frame \"\n                    \"is set higher than the end frame.\\n\\nIf your ROP frame \"\n                    \"range is correct and you do not want to apply asset \"\n                    \"handles make sure to disable Use asset handles on the \"\n                    \"publish instance.\"\n                )\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        if not instance.data.get(\"instance_node\"):\n            return\n\n        rop_node = hou.node(instance.data[\"instance_node\"])\n        frame_start = instance.data.get(\"frameStart\")\n        frame_end = instance.data.get(\"frameEnd\")\n\n        if frame_start is None or frame_end is None:\n            cls.log.debug(\n                \"Skipping frame range validation for \"\n                \"instance without frame data: {}\".format(rop_node.path())\n            )\n            return\n\n        if frame_start > frame_end:\n            cls.log.info(\n                \"The ROP node render range is set to \"\n                \"{0[frameStartHandle]} - {0[frameEndHandle]} \"\n                \"The asset handles applied to the instance are start handle \"\n                \"{0[handleStart]} and end handle {0[handleEnd]}\"\n                .format(instance.data)\n            )\n            return [rop_node]\n\n    @classmethod\n    def repair(cls, instance):\n\n        if not cls.get_invalid(instance):\n            # Already fixed\n            return\n\n        # Disable use asset handles\n        context = instance.context\n        create_context = context.data[\"create_context\"]\n        instance_id = instance.data.get(\"instance_id\")\n        if not instance_id:\n            cls.log.debug(\"'{}' must have instance id\"\n                          .format(instance))\n            return\n\n        created_instance = create_context.get_instance_by_id(instance_id)\n        if not instance_id:\n            cls.log.debug(\"Unable to find instance '{}' by id\"\n                          .format(instance))\n            return\n\n        created_instance.publish_attributes[\"CollectAssetHandles\"][\"use_handles\"] = False  # noqa\n\n        create_context.save_changes()\n        cls.log.debug(\"use asset handles is turned off for '{}'\"\n                      .format(instance))\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_frame_token.py",
    "content": "import pyblish.api\n\nfrom openpype.hosts.houdini.api import lib\nimport hou\n\n\nclass ValidateFrameToken(pyblish.api.InstancePlugin):\n    \"\"\"Validate if the unexpanded string contains the frame ('$F') token.\n\n    This validator will *only* check the output parameter of the node if\n    the Valid Frame Range is not set to 'Render Current Frame'\n\n    Rules:\n        If you render out a frame range it is mandatory to have the\n        frame token - '$F4' or similar - to ensure that each frame gets\n        written. If this is not the case you will override the same file\n        every time a frame is written out.\n\n    Examples:\n        Good: 'my_vbd_cache.$F4.vdb'\n        Bad: 'my_vbd_cache.vdb'\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Frame Token\"\n    families = [\"vdbcache\"]\n\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise RuntimeError(\n                \"Output settings do no match for '%s'\" % instance\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        node = hou.node(instance.data[\"instance_node\"])\n        # Check trange parm, 0 means Render Current Frame\n        frame_range = node.evalParm(\"trange\")\n        if frame_range == 0:\n            return []\n\n        output_parm = lib.get_output_parameter(node)\n        unexpanded_str = output_parm.unexpandedString()\n\n        if \"$F\" not in unexpanded_str:\n            cls.log.error(\"No frame token found in '%s'\" % node.path())\n            return [instance]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\nimport hou\n\n\nclass ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin):\n    \"\"\"Validate the Houdini instance runs a non Apprentice license.\n\n    USD ROPs:\n        When extracting USD files from an apprentice Houdini license,\n        the resulting files will get \"scrambled\" with a license protection\n        and get a special .usdnc suffix.\n\n        This currently breaks the Subset/representation pipeline so we disallow\n        any publish with apprentice license.\n\n    Alembic ROPs:\n        Houdini Apprentice does not export Alembic.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"usd\", \"abc\", \"fbx\", \"camera\"]\n    hosts = [\"houdini\"]\n    label = \"Houdini Apprentice License\"\n\n    def process(self, instance):\n\n        if hou.isApprentice():\n            # Find which family was matched with the plug-in\n            families = {instance.data[\"family\"]}\n            families.update(instance.data.get(\"families\", []))\n            disallowed_families = families.intersection(self.families)\n            families = \" \".join(sorted(disallowed_families)).title()\n\n            raise PublishValidationError(\n                \"{} publishing requires a non apprentice license.\"\n                .format(families),\n                title=self.label)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validator for correct naming of Static Meshes.\"\"\"\nimport pyblish.api\nfrom openpype.pipeline import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.pipeline.publish import ValidateContentsOrder\n\nfrom openpype.hosts.houdini.api.action import SelectInvalidAction\nfrom openpype.hosts.houdini.api.lib import get_output_children\n\n\nclass ValidateMeshIsStatic(pyblish.api.InstancePlugin,\n                           OptionalPyblishPluginMixin):\n    \"\"\"Validate mesh is static.\n\n    It checks if output node is time dependent.\n    \"\"\"\n\n    families = [\"staticMesh\"]\n    hosts = [\"houdini\"]\n    label = \"Validate Mesh is Static\"\n    order = ValidateContentsOrder + 0.1\n    actions = [SelectInvalidAction]\n\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            nodes = [n.path() for n in invalid]\n            raise PublishValidationError(\n                \"See log for details. \"\n                \"Invalid nodes: {0}\".format(nodes)\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        invalid = []\n\n        output_node = instance.data.get(\"output_node\")\n        if output_node is None:\n            cls.log.debug(\n                \"No Output Node, skipping check..\"\n            )\n            return\n\n        all_outputs = get_output_children(output_node)\n\n        for output in all_outputs:\n            if output.isTimeDependent():\n                invalid.append(output)\n                cls.log.error(\n                    \"Output node '%s' is time dependent.\",\n                    output.path()\n                )\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_mkpaths_toggled.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateIntermediateDirectoriesChecked(pyblish.api.InstancePlugin):\n    \"\"\"Validate Create Intermediate Directories is enabled on ROP node.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"pointcache\", \"camera\", \"vdbcache\"]\n    hosts = [\"houdini\"]\n    label = \"Create Intermediate Directories Checked\"\n\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Found ROP node with Create Intermediate \"\n                 \"Directories turned off: {}\".format(invalid)),\n                title=self.label)\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        result = []\n\n        for node in instance[:]:\n            if node.parm(\"mkpath\").eval() != 1:\n                cls.log.error(\"Invalid settings found on `%s`\" % node.path())\n                result.append(node.path())\n\n        return result\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_no_errors.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nimport hou\nfrom openpype.pipeline import PublishValidationError\n\n\ndef cook_in_range(node, start, end):\n    current = hou.intFrame()\n    if start >= current >= end:\n        # Allow cooking current frame since we're in frame range\n        node.cook(force=False)\n    else:\n        node.cook(force=False, frame_range=(start, start))\n\n\ndef get_errors(node):\n    \"\"\"Get cooking errors.\n\n    If node already has errors check whether it needs to recook\n    If so, then recook first to see if that solves it.\n\n    \"\"\"\n    if node.errors() and node.needsToCook():\n        node.cook()\n\n    return node.errors()\n\n\nclass ValidateNoErrors(pyblish.api.InstancePlugin):\n    \"\"\"Validate the Instance has no current cooking errors.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    hosts = [\"houdini\"]\n    label = \"Validate no errors\"\n\n    def process(self, instance):\n\n        validate_nodes = []\n\n        if len(instance) > 0:\n            validate_nodes.append(hou.node(instance.data.get(\"instance_node\")))\n        output_node = instance.data.get(\"output_node\")\n        if output_node:\n            validate_nodes.append(output_node)\n\n        for node in validate_nodes:\n            self.log.debug(\"Validating for errors: %s\" % node.path())\n            errors = get_errors(node)\n\n            if errors:\n                # If there are current errors, then try an unforced cook\n                # to see whether the error will disappear.\n                self.log.debug(\n                    \"Recooking to revalidate error \"\n                    \"is up to date for: %s\" % node.path()\n                )\n                current_frame = hou.intFrame()\n                start = instance.data.get(\"frameStart\", current_frame)\n                end = instance.data.get(\"frameEnd\", current_frame)\n                cook_in_range(node, start=start, end=end)\n\n            # Check for errors again after the forced recook\n            errors = get_errors(node)\n            if errors:\n                self.log.error(errors)\n                raise PublishValidationError(\n                    \"Node has errors: {}\".format(node.path()),\n                    title=self.label)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    RepairAction,\n)\n\nimport hou\n\n\nclass AddDefaultPathAction(RepairAction):\n    label = \"Add a default path attribute\"\n    icon = \"mdi.pencil-plus-outline\"\n\n\nclass ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin):\n    \"\"\"Validate all primitives build hierarchy from attribute when enabled.\n\n    The name of the attribute must exist on the prims and have the same name\n    as Build Hierarchy from Attribute's `Path Attribute` value on the Alembic\n    ROP node whenever Build Hierarchy from Attribute is enabled.\n\n    \"\"\"\n\n    order = ValidateContentsOrder + 0.1\n    families = [\"abc\"]\n    hosts = [\"houdini\"]\n    label = \"Validate Prims Hierarchy Path\"\n    actions = [AddDefaultPathAction]\n\n    def process(self, instance):\n        invalid = self.get_invalid(instance)\n        if invalid:\n            nodes = [n.path() for n in invalid]\n            raise PublishValidationError(\n                \"See log for details. \" \"Invalid nodes: {0}\".format(nodes),\n                title=self.label\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        output_node = instance.data.get(\"output_node\")\n        rop_node = hou.node(instance.data[\"instance_node\"])\n\n        if output_node is None:\n            cls.log.error(\n                \"SOP Output node in '%s' does not exist. \"\n                \"Ensure a valid SOP output path is set.\", rop_node.path()\n            )\n\n            return [rop_node]\n\n        build_from_path = rop_node.parm(\"build_from_path\").eval()\n        if not build_from_path:\n            cls.log.debug(\n                \"Alembic ROP has 'Build from Path' disabled. \"\n                \"Validation is ignored..\"\n            )\n            return\n\n        path_attr = rop_node.parm(\"path_attrib\").eval()\n        if not path_attr:\n            cls.log.error(\n                \"The Alembic ROP node has no Path Attribute\"\n                \"value set, but 'Build Hierarchy from Attribute'\"\n                \"is enabled.\"\n            )\n            return [rop_node]\n\n        cls.log.debug(\"Checking for attribute: %s\", path_attr)\n\n        if not hasattr(output_node, \"geometry\"):\n            # In the case someone has explicitly set an Object\n            # node instead of a SOP node in Geometry context\n            # then for now we ignore - this allows us to also\n            # export object transforms.\n            cls.log.warning(\"No geometry output node found, skipping check..\")\n            return\n\n        # Check if the primitive attribute exists\n        frame = instance.data.get(\"frameStart\", 0)\n        geo = output_node.geometryAtFrame(frame)\n\n        # If there are no primitives on the current frame then we can't\n        # check whether the path names are correct. So we'll just issue a\n        # warning that the check can't be done consistently and skip\n        # validation.\n        if len(geo.iterPrims()) == 0:\n            cls.log.warning(\n                \"No primitives found on current frame. Validation\"\n                \" for primitive hierarchy paths will be skipped,\"\n                \" thus can't be validated.\"\n            )\n            return\n\n        # Check if there are any values for the primitives\n        attrib = geo.findPrimAttrib(path_attr)\n        if not attrib:\n            cls.log.info(\n                \"Geometry Primitives are missing \"\n                \"path attribute: `%s`\", path_attr\n            )\n            return [output_node]\n\n        # Ensure at least a single string value is present\n        if not attrib.strings():\n            cls.log.info(\n                \"Primitive path attribute has no \"\n                \"string values: %s\", path_attr\n            )\n            return [output_node]\n\n        paths = geo.primStringAttribValues(path_attr)\n        # Ensure all primitives are set to a valid path\n        # Collect all invalid primitive numbers\n        invalid_prims = [i for i, path in enumerate(paths) if not path]\n        if invalid_prims:\n            num_prims = len(geo.iterPrims())  # faster than len(geo.prims())\n            cls.log.info(\n                \"Prims have no value for attribute `%s` \"\n                \"(%s of %s prims)\", path_attr, len(invalid_prims), num_prims\n            )\n            return [output_node]\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"Add a default path attribute Action.\n\n        It is a helper action more than a repair action,\n        used to add a default single value for the path.\n        \"\"\"\n\n        rop_node = hou.node(instance.data[\"instance_node\"])\n        output_node = rop_node.parm(\"sop_path\").evalAsNode()\n\n        if not output_node:\n            cls.log.debug(\n                \"Action isn't performed, invalid SOP Path on %s\",\n                rop_node\n            )\n            return\n\n        # This check to prevent the action from running multiple times.\n        # git_invalid only returns [output_node] when\n        #   path attribute is the problem\n        if cls.get_invalid(instance) != [output_node]:\n            return\n\n        path_attr = rop_node.parm(\"path_attrib\").eval()\n\n        path_node = output_node.parent().createNode(\"name\", \"AUTO_PATH\")\n        path_node.parm(\"attribname\").set(path_attr)\n        path_node.parm(\"name1\").set('`opname(\"..\")`/`opname(\"..\")`Shape')\n\n        cls.log.debug(\n            \"'%s' was created. It adds '%s' with a default single value\",\n            path_node, path_attr\n        )\n\n        path_node.setGenericFlag(hou.nodeFlag.DisplayComment, True)\n        path_node.setComment(\n            'Auto path node was created automatically by '\n            '\"Add a default path attribute\"'\n            '\\nFeel free to modify or replace it.'\n        )\n\n        if output_node.type().name() in [\"null\", \"output\"]:\n            # Connect before\n            path_node.setFirstInput(output_node.input(0))\n            path_node.moveToGoodPosition()\n            output_node.setFirstInput(path_node)\n            output_node.moveToGoodPosition()\n        else:\n            # Connect after\n            path_node.setFirstInput(output_node)\n            rop_node.parm(\"sop_path\").set(path_node.path())\n            path_node.moveToGoodPosition()\n\n            cls.log.debug(\n                \"SOP path on '%s' updated to new output node '%s'\",\n                rop_node, path_node\n            )\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_remote_publish.py",
    "content": "# -*-coding: utf-8 -*-\nimport pyblish.api\n\nfrom openpype.hosts.houdini.api import lib\nfrom openpype.pipeline.publish import RepairContextAction\nfrom openpype.pipeline import PublishValidationError\n\nimport hou\n\n\nclass ValidateRemotePublishOutNode(pyblish.api.ContextPlugin):\n    \"\"\"Validate the remote publish out node exists for Deadline to trigger.\"\"\"\n\n    order = pyblish.api.ValidatorOrder - 0.4\n    families = [\"*\"]\n    hosts = [\"houdini\"]\n    targets = [\"deadline\"]\n    label = \"Remote Publish ROP node\"\n    actions = [RepairContextAction]\n\n    def process(self, context):\n\n        cmd = \"import colorbleed.lib; colorbleed.lib.publish_remote()\"\n\n        node = hou.node(\"/out/REMOTE_PUBLISH\")\n        if not node:\n            raise RuntimeError(\"Missing REMOTE_PUBLISH node.\")\n\n        # We ensure it's a shell node and that it has the pre-render script\n        # set correctly. Plus the shell script it will trigger should be\n        # completely empty (doing nothing)\n        if node.type().name() != \"shell\":\n            self.raise_error(\"Must be shell ROP node\")\n        if node.parm(\"command\").eval() != \"\":\n            self.raise_error(\"Must have no command\")\n        if node.parm(\"shellexec\").eval():\n            self.raise_error(\"Must not execute in shell\")\n        if node.parm(\"prerender\").eval() != cmd:\n            self.raise_error(\"REMOTE_PUBLISH node does not have \"\n                             \"correct prerender script.\")\n        if node.parm(\"lprerender\").eval() != \"python\":\n            self.raise_error(\"REMOTE_PUBLISH node prerender script \"\n                             \"type not set to 'python'\")\n\n    @classmethod\n    def repair(cls, context):\n        \"\"\"(Re)create the node if it fails to pass validation.\"\"\"\n        lib.create_remote_publish_node(force=True)\n\n    def raise_error(self, message):\n        raise PublishValidationError(message)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_remote_publish_enabled.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\n\nimport hou\nfrom openpype.pipeline.publish import RepairContextAction\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateRemotePublishEnabled(pyblish.api.ContextPlugin):\n    \"\"\"Validate the remote publish node is *not* bypassed.\"\"\"\n\n    order = pyblish.api.ValidatorOrder - 0.39\n    families = [\"*\"]\n    hosts = [\"houdini\"]\n    targets = [\"deadline\"]\n    label = \"Remote Publish ROP enabled\"\n    actions = [RepairContextAction]\n\n    def process(self, context):\n\n        node = hou.node(\"/out/REMOTE_PUBLISH\")\n        if not node:\n            raise PublishValidationError(\n                \"Missing REMOTE_PUBLISH node.\", title=self.label)\n\n        if node.isBypassed():\n            raise PublishValidationError(\n                \"REMOTE_PUBLISH must not be bypassed.\", title=self.label)\n\n    @classmethod\n    def repair(cls, context):\n        \"\"\"(Re)create the node if it fails to pass validation.\"\"\"\n\n        node = hou.node(\"/out/REMOTE_PUBLISH\")\n        if not node:\n            raise PublishValidationError(\n                \"Missing REMOTE_PUBLISH node.\", title=cls.label)\n\n        cls.log.info(\"Disabling bypass on /out/REMOTE_PUBLISH\")\n        node.bypass(False)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.pipeline.publish import RepairAction\nfrom openpype.hosts.houdini.api.action import SelectROPAction\n\nimport os\nimport hou\n\n\nclass SetDefaultViewSpaceAction(RepairAction):\n    label = \"Set default view colorspace\"\n    icon = \"mdi.monitor\"\n\n\nclass ValidateReviewColorspace(pyblish.api.InstancePlugin,\n                               OptionalPyblishPluginMixin):\n    \"\"\"Validate Review Colorspace parameters.\n\n    It checks if 'OCIO Colorspace' parameter was set to valid value.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder + 0.1\n    families = [\"review\"]\n    hosts = [\"houdini\"]\n    label = \"Validate Review Colorspace\"\n    actions = [SetDefaultViewSpaceAction, SelectROPAction]\n\n    optional = True\n\n    def process(self, instance):\n\n        if not self.is_active(instance.data):\n            return\n\n        if os.getenv(\"OCIO\") is None:\n            self.log.debug(\n                \"Using Houdini's Default Color Management, \"\n                \" skipping check..\"\n            )\n            return\n\n        rop_node = hou.node(instance.data[\"instance_node\"])\n        if rop_node.evalParm(\"colorcorrect\") != 2:\n            # any colorspace settings other than default requires\n            # 'Color Correct' parm to be set to 'OpenColorIO'\n            raise PublishValidationError(\n                \"'Color Correction' parm on '{}' ROP must be set to\"\n                \" 'OpenColorIO'\".format(rop_node.path())\n            )\n\n        if rop_node.evalParm(\"ociocolorspace\") not in \\\n                hou.Color.ocio_spaces():\n\n            raise PublishValidationError(\n                \"Invalid value: Colorspace name doesn't exist.\\n\"\n                \"Check 'OCIO Colorspace' parameter on '{}' ROP\"\n                .format(rop_node.path())\n            )\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"Set Default View Space Action.\n\n        It is a helper action more than a repair action,\n        used to set colorspace on opengl node to the default view.\n        \"\"\"\n        from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace  # noqa\n\n        rop_node = hou.node(instance.data[\"instance_node\"])\n\n        if rop_node.evalParm(\"colorcorrect\") != 2:\n            rop_node.setParms({\"colorcorrect\": 2})\n            cls.log.debug(\n                \"'Color Correction' parm on '{}' has been set to\"\n                \" 'OpenColorIO'\".format(rop_node.path())\n            )\n\n        # Get default view colorspace name\n        default_view_space = get_default_display_view_colorspace()\n\n        rop_node.setParms({\"ociocolorspace\": default_view_space})\n        cls.log.info(\n            \"'OCIO Colorspace' parm on '{}' has been set to \"\n            \"the default view color space '{}'\"\n            .format(rop_node, default_view_space)\n        )\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_scene_review.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\nimport hou\n\n\nclass ValidateSceneReview(pyblish.api.InstancePlugin):\n    \"\"\"Validator Some Scene Settings before publishing the review\n        1. Scene Path\n        2. Resolution\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"review\"]\n    hosts = [\"houdini\"]\n    label = \"Scene Setting for review\"\n\n    def process(self, instance):\n\n        report = []\n        instance_node = hou.node(instance.data.get(\"instance_node\"))\n\n        invalid = self.get_invalid_scene_path(instance_node)\n        if invalid:\n            report.append(invalid)\n\n        invalid = self.get_invalid_camera_path(instance_node)\n        if invalid:\n            report.append(invalid)\n\n        invalid = self.get_invalid_resolution(instance_node)\n        if invalid:\n            report.extend(invalid)\n\n        if report:\n            raise PublishValidationError(\n                \"\\n\\n\".join(report),\n                title=self.label)\n\n    def get_invalid_scene_path(self, rop_node):\n        scene_path_parm = rop_node.parm(\"scenepath\")\n        scene_path_node = scene_path_parm.evalAsNode()\n        if not scene_path_node:\n            path = scene_path_parm.evalAsString()\n            return \"Scene path does not exist: '{}'\".format(path)\n\n    def get_invalid_camera_path(self, rop_node):\n        camera_path_parm = rop_node.parm(\"camera\")\n        camera_node = camera_path_parm.evalAsNode()\n        path = camera_path_parm.evalAsString()\n        if not camera_node:\n            return \"Camera path does not exist: '{}'\".format(path)\n        type_name = camera_node.type().name()\n        if type_name != \"cam\":\n            return \"Camera path is not a camera: '{}' (type: {})\".format(\n                path, type_name\n            )\n\n    def get_invalid_resolution(self, rop_node):\n\n        # The resolution setting is only used when Override Camera Resolution\n        # is enabled. So we skip validation if it is disabled.\n        override = rop_node.parm(\"tres\").eval()\n        if not override:\n            return\n\n        invalid = []\n        res_width = rop_node.parm(\"res1\").eval()\n        res_height = rop_node.parm(\"res2\").eval()\n        if res_width == 0:\n            invalid.append(\"Override Resolution width is set to zero.\")\n        if res_height == 0:\n            invalid.append(\"Override Resolution height is set to zero\")\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\nfrom openpype.hosts.houdini.api.action import (\n    SelectInvalidAction,\n    SelectROPAction,\n)\n\nimport hou\n\n\nclass ValidateSopOutputNode(pyblish.api.InstancePlugin):\n    \"\"\"Validate the instance SOP Output Node.\n\n    This will ensure:\n        - The SOP Path is set.\n        - The SOP Path refers to an existing object.\n        - The SOP Path node is a SOP node.\n        - The SOP Path node has at least one input connection (has an input)\n        - The SOP Path has geometry data.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"pointcache\", \"vdbcache\"]\n    hosts = [\"houdini\"]\n    label = \"Validate Output Node (SOP)\"\n    actions = [SelectROPAction, SelectInvalidAction]\n\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Output node(s) are incorrect\",\n                title=\"Invalid output node(s)\"\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n        output_node = instance.data.get(\"output_node\")\n\n        if output_node is None:\n            node = hou.node(instance.data[\"instance_node\"])\n            cls.log.error(\n                \"SOP Output node in '%s' does not exist. \"\n                \"Ensure a valid SOP output path is set.\" % node.path()\n            )\n\n            return [node]\n\n        # Output node must be a Sop node.\n        if not isinstance(output_node, hou.SopNode):\n            cls.log.error(\n                \"Output node %s is not a SOP node. \"\n                \"SOP Path must point to a SOP node, \"\n                \"instead found category type: %s\"\n                % (output_node.path(), output_node.type().category().name())\n            )\n            return [output_node]\n\n        # For the sake of completeness also assert the category type\n        # is Sop to avoid potential edge case scenarios even though\n        # the isinstance check above should be stricter than this category\n        if output_node.type().category().name() != \"Sop\":\n            raise PublishValidationError(\n                (\"Output node {} is not of category Sop. \"\n                 \"This is a bug.\").format(output_node.path()),\n                title=cls.label)\n\n        # Ensure the node is cooked and succeeds to cook so we can correctly\n        # check for its geometry data.\n        if output_node.needsToCook():\n            cls.log.debug(\"Cooking node: %s\" % output_node.path())\n            try:\n                output_node.cook()\n            except hou.Error as exc:\n                cls.log.error(\"Cook failed: %s\" % exc)\n                cls.log.error(output_node.errors()[0])\n                return [output_node]\n\n        # Ensure the output node has at least Geometry data\n        if not output_node.geometry():\n            cls.log.error(\n                \"Output node `%s` has no geometry data.\" % output_node.path()\n            )\n            return [output_node]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_subset_name.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validator for correct naming of Static Meshes.\"\"\"\nimport pyblish.api\nfrom openpype.pipeline import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    RepairAction,\n)\nfrom openpype.hosts.houdini.api.action import SelectInvalidAction\nfrom openpype.pipeline.create import get_subset_name\n\nimport hou\n\n\nclass FixSubsetNameAction(RepairAction):\n    label = \"Fix Subset Name\"\n\n\nclass ValidateSubsetName(pyblish.api.InstancePlugin,\n                         OptionalPyblishPluginMixin):\n    \"\"\"Validate Subset name.\n\n    \"\"\"\n\n    families = [\"staticMesh\"]\n    hosts = [\"houdini\"]\n    label = \"Validate Subset Name\"\n    order = ValidateContentsOrder + 0.1\n    actions = [FixSubsetNameAction, SelectInvalidAction]\n\n    optional = True\n\n    def process(self, instance):\n\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            nodes = [n.path() for n in invalid]\n            raise PublishValidationError(\n                \"See log for details. \"\n                \"Invalid nodes: {0}\".format(nodes)\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        invalid = []\n\n        rop_node = hou.node(instance.data[\"instance_node\"])\n\n        # Check subset name\n        asset_doc = instance.data[\"assetEntity\"]\n        subset_name = get_subset_name(\n            family=instance.data[\"family\"],\n            variant=instance.data[\"variant\"],\n            task_name=instance.data[\"task\"],\n            asset_doc=asset_doc,\n            dynamic_data={\"asset\": asset_doc[\"name\"]}\n        )\n\n        if instance.data.get(\"subset\") != subset_name:\n            invalid.append(rop_node)\n            cls.log.error(\n                \"Invalid subset name on rop node '%s' should be '%s'.\",\n                rop_node.path(), subset_name\n            )\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n        rop_node = hou.node(instance.data[\"instance_node\"])\n\n        # Check subset name\n        asset_doc = instance.data[\"assetEntity\"]\n        subset_name = get_subset_name(\n            family=instance.data[\"family\"],\n            variant=instance.data[\"variant\"],\n            task_name=instance.data[\"task\"],\n            asset_doc=asset_doc,\n            dynamic_data={\"asset\": asset_doc[\"name\"]}\n        )\n\n        instance.data[\"subset\"] = subset_name\n        rop_node.parm(\"subset\").set(subset_name)\n\n        cls.log.debug(\n            \"Subset name on rop node '%s' has been set to '%s'.\",\n            rop_node.path(), subset_name\n        )\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validator for correct naming of Static Meshes.\"\"\"\nimport pyblish.api\nfrom openpype.pipeline import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.pipeline.publish import ValidateContentsOrder\n\nfrom openpype.hosts.houdini.api.action import SelectInvalidAction\nfrom openpype.hosts.houdini.api.lib import get_output_children\n\nimport hou\n\n\nclass ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin,\n                                   OptionalPyblishPluginMixin):\n    \"\"\"Validate name of Unreal Static Mesh.\n\n    This validator checks if output node name has a collision prefix:\n            - UBX\n            - UCP\n            - USP\n            - UCX\n\n    This validator also checks if subset name is correct\n            - {static mesh prefix}_{Asset-Name}{Variant}.\n\n    \"\"\"\n\n    families = [\"staticMesh\"]\n    hosts = [\"houdini\"]\n    label = \"Unreal Static Mesh Name (FBX)\"\n    order = ValidateContentsOrder + 0.1\n    actions = [SelectInvalidAction]\n\n    optional = True\n    collision_prefixes = []\n    static_mesh_prefix = \"\"\n\n    @classmethod\n    def apply_settings(cls, project_settings, system_settings):\n\n        settings = (\n            project_settings[\"houdini\"][\"create\"][\"CreateStaticMesh\"]\n        )\n        cls.collision_prefixes = settings[\"collision_prefixes\"]\n        cls.static_mesh_prefix = settings[\"static_mesh_prefix\"]\n\n    def process(self, instance):\n\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            nodes = [n.path() for n in invalid]\n            raise PublishValidationError(\n                \"See log for details. \"\n                \"Invalid nodes: {0}\".format(nodes)\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        invalid = []\n\n        rop_node = hou.node(instance.data[\"instance_node\"])\n        output_node = instance.data.get(\"output_node\")\n        if output_node is None:\n            cls.log.debug(\n                \"No Output Node, skipping check..\"\n            )\n            return\n\n        if rop_node.evalParm(\"buildfrompath\"):\n            # This validator doesn't support naming check if\n            # building hierarchy from path' is used\n            cls.log.info(\n                \"Using 'Build Hierarchy from Path Attribute', skipping check..\"\n            )\n            return\n\n        # Check nodes names\n        all_outputs = get_output_children(output_node, include_sops=False)\n        for output in all_outputs:\n            for prefix in cls.collision_prefixes:\n                if output.name().startswith(prefix):\n                    invalid.append(output)\n                    cls.log.error(\n                        \"Invalid node name: Node '%s' \"\n                        \"includes a collision prefix '%s'\",\n                        output.path(), prefix\n                    )\n                    break\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_usd_layer_path_backslashes.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\n\nimport openpype.hosts.houdini.api.usd as hou_usdlib\nfrom openpype.pipeline import PublishValidationError\n\nimport hou\n\n\nclass ValidateUSDLayerPathBackslashes(pyblish.api.InstancePlugin):\n    \"\"\"Validate USD loaded paths have no backslashes.\n\n    This is a crucial validation for HUSK USD rendering as Houdini's\n    USD Render ROP will fail to write out a .usd file for rendering that\n    correctly preserves the backslashes, e.g. it will incorrectly convert a\n    '\\t' to a TAB character disallowing HUSK to find those specific files.\n\n    This validation is redundant for usdModel since that flattens the model\n    before write. As such it will never have any used layers with a path.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"usdSetDress\", \"usdShade\", \"usd\", \"usdrender\"]\n    hosts = [\"houdini\"]\n    label = \"USD Layer path backslashes\"\n    optional = True\n\n    def process(self, instance):\n\n        rop = hou.node(instance.data.get(\"instance_node\"))\n        lop_path = hou_usdlib.get_usd_rop_loppath(rop)\n        stage = lop_path.stage(apply_viewport_overrides=False)\n\n        invalid = []\n        for layer in stage.GetUsedLayers():\n            references = layer.externalReferences\n\n            for ref in references:\n\n                # Ignore anonymous layers\n                if ref.startswith(\"anon:\"):\n                    continue\n\n                # If any backslashes in the path consider it invalid\n                if \"\\\\\" in ref:\n                    self.log.error(\"Found invalid path: %s\" % ref)\n                    invalid.append(layer)\n\n        if invalid:\n            raise PublishValidationError((\n                \"Loaded layers have backslashes. \"\n                \"This is invalid for HUSK USD rendering.\"),\n                title=self.label)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_usd_model_and_shade.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\n\nimport openpype.hosts.houdini.api.usd as hou_usdlib\nfrom openpype.pipeline import PublishValidationError\n\nfrom pxr import UsdShade, UsdRender, UsdLux\n\nimport hou\n\n\ndef fullname(o):\n    \"\"\"Get fully qualified class name\"\"\"\n    module = o.__module__\n    if module is None or module == str.__module__:\n        return o.__name__\n    return module + \".\" + o.__name__\n\n\nclass ValidateUsdModel(pyblish.api.InstancePlugin):\n    \"\"\"Validate USD Model.\n\n    Disallow Shaders, Render settings, products and vars and Lux lights.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"usdModel\"]\n    hosts = [\"houdini\"]\n    label = \"Validate USD Model\"\n    optional = True\n\n    disallowed = [\n        UsdShade.Shader,\n        UsdRender.Settings,\n        UsdRender.Product,\n        UsdRender.Var,\n        UsdLux.Light,\n    ]\n\n    def process(self, instance):\n\n        rop = hou.node(instance.data.get(\"instance_node\"))\n        lop_path = hou_usdlib.get_usd_rop_loppath(rop)\n        stage = lop_path.stage(apply_viewport_overrides=False)\n\n        invalid = []\n        for prim in stage.Traverse():\n\n            for klass in self.disallowed:\n                if klass(prim):\n                    # Get full class name without pxr. prefix\n                    name = fullname(klass).split(\"pxr.\", 1)[-1]\n                    path = str(prim.GetPath())\n                    self.log.warning(\"Disallowed %s: %s\" % (name, path))\n\n                    invalid.append(prim)\n\n        if invalid:\n            prim_paths = sorted([str(prim.GetPath()) for prim in invalid])\n            raise PublishValidationError(\n                \"Found invalid primitives: {}\".format(prim_paths))\n\n\nclass ValidateUsdShade(ValidateUsdModel):\n    \"\"\"Validate usdShade.\n\n    Disallow Render settings, products, vars and Lux lights.\n\n    \"\"\"\n\n    families = [\"usdShade\"]\n    label = \"Validate USD Shade\"\n\n    disallowed = [\n        UsdRender.Settings,\n        UsdRender.Product,\n        UsdRender.Var,\n        UsdLux.Light,\n    ]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_usd_output_node.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateUSDOutputNode(pyblish.api.InstancePlugin):\n    \"\"\"Validate the instance USD LOPs Output Node.\n\n    This will ensure:\n        - The LOP Path is set.\n        - The LOP Path refers to an existing object.\n        - The LOP Path node is a LOP node.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"usd\"]\n    hosts = [\"houdini\"]\n    label = \"Validate Output Node (USD)\"\n\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Output node(s) `{}` are incorrect. \"\n                 \"See plug-in log for details.\").format(invalid),\n                title=self.label\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        import hou\n\n        output_node = instance.data[\"output_node\"]\n\n        if output_node is None:\n            node = hou.node(instance.data.get(\"instance_node\"))\n            cls.log.error(\n                \"USD node '%s' LOP path does not exist. \"\n                \"Ensure a valid LOP path is set.\" % node.path()\n            )\n\n            return [node.path()]\n\n        # Output node must be a Sop node.\n        if not isinstance(output_node, hou.LopNode):\n            cls.log.error(\n                \"Output node %s is not a LOP node. \"\n                \"LOP Path must point to a LOP node, \"\n                \"instead found category type: %s\"\n                % (output_node.path(), output_node.type().category().name())\n            )\n            return [output_node.path()]\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_usd_render_product_names.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nimport pyblish.api\n\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateUSDRenderProductNames(pyblish.api.InstancePlugin):\n    \"\"\"Validate USD Render Product names are correctly set absolute paths.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"usdrender\"]\n    hosts = [\"houdini\"]\n    label = \"Validate USD Render Product Names\"\n    optional = True\n\n    def process(self, instance):\n\n        invalid = []\n        for filepath in instance.data[\"files\"]:\n\n            if not filepath:\n                invalid.append(\"Detected empty output filepath.\")\n\n            if not os.path.isabs(filepath):\n                invalid.append(\n                    \"Output file path is not absolute path: %s\" % filepath\n                )\n\n        if invalid:\n            for message in invalid:\n                self.log.error(message)\n            raise PublishValidationError(\n                \"USD Render Paths are invalid.\", title=self.label)\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_usd_setdress.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\n\nimport openpype.hosts.houdini.api.usd as hou_usdlib\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateUsdSetDress(pyblish.api.InstancePlugin):\n    \"\"\"Validate USD Set Dress.\n\n    Must only have references or payloads. May not generate new mesh or\n    flattened meshes.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"usdSetDress\"]\n    hosts = [\"houdini\"]\n    label = \"Validate USD Set Dress\"\n    optional = True\n\n    def process(self, instance):\n\n        from pxr import UsdGeom\n        import hou\n\n        rop = hou.node(instance.data.get(\"instance_node\"))\n        lop_path = hou_usdlib.get_usd_rop_loppath(rop)\n        stage = lop_path.stage(apply_viewport_overrides=False)\n\n        invalid = []\n        for node in stage.Traverse():\n\n            if UsdGeom.Mesh(node):\n                # This solely checks whether there is any USD involved\n                # in this Prim's Stack and doesn't accurately tell us\n                # whether it was generated locally or not.\n                # TODO: More accurately track whether the Prim was created\n                #       in the local scene\n                stack = node.GetPrimStack()\n                for sdf in stack:\n                    path = sdf.layer.realPath\n                    if path:\n                        break\n                else:\n                    prim_path = node.GetPath()\n                    self.log.error(\n                        \"%s is not referenced geometry.\" % prim_path\n                    )\n                    invalid.append(node)\n\n        if invalid:\n            raise PublishValidationError((\n                \"SetDress contains local geometry. \"\n                \"This is not allowed, it must be an assembly \"\n                \"of referenced assets.\"),\n                title=self.label\n            )\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_usd_shade_model_exists.py",
    "content": "# -*- coding: utf-8 -*-\nimport re\n\nimport pyblish.api\n\nfrom openpype.client import get_subset_by_name\nfrom openpype.pipeline.publish import ValidateContentsOrder\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateUSDShadeModelExists(pyblish.api.InstancePlugin):\n    \"\"\"Validate the Instance has no current cooking errors.\"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"houdini\"]\n    families = [\"usdShade\"]\n    label = \"USD Shade model exists\"\n\n    def process(self, instance):\n        project_name = instance.context.data[\"projectName\"]\n        asset_name = instance.data[\"asset\"]\n        subset = instance.data[\"subset\"]\n\n        # Assume shading variation starts after a dot separator\n        shade_subset = subset.split(\".\", 1)[0]\n        model_subset = re.sub(\"^usdShade\", \"usdModel\", shade_subset)\n\n        asset_doc = instance.data.get(\"assetEntity\")\n        if not asset_doc:\n            raise RuntimeError(\"Asset document is not filled on instance.\")\n\n        subset_doc = get_subset_by_name(\n            project_name, model_subset, asset_doc[\"_id\"], fields=[\"_id\"]\n        )\n        if not subset_doc:\n            raise PublishValidationError(\n                (\"USD Model subset not found: \"\n                 \"{} ({})\").format(model_subset, asset_name),\n                title=self.label\n            )\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_usd_shade_workspace.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\nimport hou\n\n\nclass ValidateUsdShadeWorkspace(pyblish.api.InstancePlugin):\n    \"\"\"Validate USD Shading Workspace is correct version.\n\n    There have been some issues with outdated/erroneous Shading Workspaces\n    so this is to confirm everything is set as it should.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    hosts = [\"houdini\"]\n    families = [\"usdShade\"]\n    label = \"USD Shade Workspace\"\n\n    def process(self, instance):\n\n        rop = hou.node(instance.data.get(\"instance_node\"))\n        workspace = rop.parent()\n\n        definition = workspace.type().definition()\n        name = definition.nodeType().name()\n        library = definition.libraryFilePath()\n\n        all_definitions = hou.hda.definitionsInFile(library)\n        node_type, version = name.rsplit(\":\", 1)\n        version = float(version)\n\n        highest = version\n        for other_definition in all_definitions:\n            other_name = other_definition.nodeType().name()\n            other_node_type, other_version = other_name.rsplit(\":\", 1)\n            other_version = float(other_version)\n\n            if node_type != other_node_type:\n                continue\n\n            # Get the highest version\n            highest = max(highest, other_version)\n\n        if version != highest:\n            raise PublishValidationError(\n                (\"Shading Workspace is not the latest version.\"\n                 \" Found {}. Latest is {}.\").format(version, highest),\n                title=self.label\n            )\n\n        # There were some issues with the editable node not having the right\n        # configured path. So for now let's assure that is correct to.from\n        value = (\n            'avalon://`chs(\"../asset_name\")`/'\n            'usdShade`chs(\"../model_variantname1\")`.usd'\n        )\n        rop_value = rop.parm(\"lopoutput\").rawValue()\n        if rop_value != value:\n            raise PublishValidationError(\n                (\"Shading Workspace has invalid 'lopoutput'\"\n                 \" parameter value. The Shading Workspace\"\n                 \" needs to be reset to its default values.\"),\n                title=self.label\n            )\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py",
    "content": "# -*- coding: utf-8 -*-\nimport contextlib\n\nimport pyblish.api\nimport hou\n\nfrom openpype.pipeline import PublishXmlValidationError\nfrom openpype.hosts.houdini.api.action import SelectInvalidAction\n\n\ndef group_consecutive_numbers(nums):\n    \"\"\"\n    Args:\n        nums (list): List of sorted integer numbers.\n\n    Yields:\n        str: Group ranges as {start}-{end} if more than one number in the range\n            else it yields {end}\n\n    \"\"\"\n    start = None\n    end = None\n\n    def _result(a, b):\n        if a == b:\n            return \"{}\".format(a)\n        else:\n            return \"{}-{}\".format(a, b)\n\n    for num in nums:\n        if start is None:\n            start = num\n            end = num\n        elif num == end + 1:\n            end = num\n        else:\n            yield _result(start, end)\n            start = num\n            end = num\n    if start is not None:\n        yield _result(start, end)\n\n\n@contextlib.contextmanager\ndef update_mode_context(mode):\n    original = hou.updateModeSetting()\n    try:\n        hou.setUpdateMode(mode)\n        yield\n    finally:\n        hou.setUpdateMode(original)\n\n\ndef get_geometry_at_frame(sop_node, frame, force=True):\n    \"\"\"Return geometry at frame but force a cooked value.\"\"\"\n    if not hasattr(sop_node, \"geometry\"):\n        return\n    with update_mode_context(hou.updateMode.AutoUpdate):\n        sop_node.cook(force=force, frame_range=(frame, frame))\n        return sop_node.geometryAtFrame(frame)\n\n\nclass ValidateVDBOutputNode(pyblish.api.InstancePlugin):\n    \"\"\"Validate that the node connected to the output node is of type VDB.\n\n    All primitives of the output geometry must be VDBs, no other primitive\n    types are allowed. That means that regardless of the amount of VDBs in the\n    geometry it will have an equal amount of VDBs, points, primitives and\n    vertices since each VDB primitive is one point, one vertex and one VDB.\n\n    This validation only checks the geometry on the first frame of the export\n    frame range for optimization purposes.\n\n    A VDB is an inherited type of Prim, holds the following data:\n        - Primitives: 1\n        - Points: 1\n        - Vertices: 1\n        - VDBs: 1\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder + 0.1\n    families = [\"vdbcache\"]\n    hosts = [\"houdini\"]\n    label = \"Validate Output Node (VDB)\"\n    actions = [SelectInvalidAction]\n\n    def process(self, instance):\n        invalid_nodes, message = self.get_invalid_with_message(instance)\n        if invalid_nodes:\n\n            # instance_node is str, but output_node is hou.Node so we convert\n            output = instance.data.get(\"output_node\")\n            output_path = output.path() if output else None\n\n            raise PublishXmlValidationError(\n                self,\n                \"Invalid VDB content: {}\".format(message),\n                formatting_data={\n                    \"message\": message,\n                    \"rop_path\": instance.data.get(\"instance_node\"),\n                    \"sop_path\": output_path\n                }\n            )\n\n    @classmethod\n    def get_invalid_with_message(cls, instance):\n\n        node = instance.data.get(\"output_node\")\n        if node is None:\n            instance_node = instance.data.get(\"instance_node\")\n            error = (\n                \"SOP path is not correctly set on \"\n                \"ROP node `{}`.\".format(instance_node)\n            )\n            return [hou.node(instance_node), error]\n\n        frame = instance.data.get(\"frameStart\", 0)\n        geometry = get_geometry_at_frame(node, frame)\n        if geometry is None:\n            # No geometry data on this node, maybe the node hasn't cooked?\n            error = (\n                \"SOP node `{}` has no geometry data. \"\n                \"Was it unable to cook?\".format(node.path())\n            )\n            return [node, error]\n\n        num_prims = geometry.intrinsicValue(\"primitivecount\")\n        num_points = geometry.intrinsicValue(\"pointcount\")\n        if num_prims == 0 and num_points == 0:\n            # Since we are only checking the first frame it doesn't mean there\n            # won't be VDB prims in a few frames. As such we'll assume for now\n            # the user knows what he or she is doing\n            cls.log.warning(\n                \"SOP node `{}` has no primitives on start frame {}. \"\n                \"Validation is skipped and it is assumed elsewhere in the \"\n                \"frame range VDB prims and only VDB prims will exist.\"\n                \"\".format(node.path(), int(frame))\n            )\n            return [None, None]\n\n        num_vdb_prims = geometry.countPrimType(hou.primType.VDB)\n        cls.log.debug(\"Detected {} VDB primitives\".format(num_vdb_prims))\n        if num_prims != num_vdb_prims:\n            # There's at least one primitive that is not a VDB.\n            # Search them and report them to the artist.\n            prims = geometry.prims()\n            invalid_prims = [prim for prim in prims\n                             if not isinstance(prim, hou.VDB)]\n            if invalid_prims:\n                # Log prim numbers as consecutive ranges so logging isn't very\n                # slow for large number of primitives\n                error = (\n                    \"Found non-VDB primitives for `{}`. \"\n                    \"Primitive indices {} are not VDB primitives.\".format(\n                        node.path(),\n                        \", \".join(group_consecutive_numbers(\n                            prim.number() for prim in invalid_prims\n                        ))\n                    )\n                )\n                return [node, error]\n\n        if num_points != num_vdb_prims:\n            # We have points unrelated to the VDB primitives.\n            error = (\n                \"The number of primitives and points do not match in '{}'. \"\n                \"This likely means you have unconnected points, which we do \"\n                \"not allow in the VDB output.\".format(node.path()))\n            return [node, error]\n\n        return [None, None]\n\n    @classmethod\n    def get_invalid(cls, instance):\n        nodes, _ = cls.get_invalid_with_message(instance)\n        return nodes\n"
  },
  {
    "path": "openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nimport hou\nfrom openpype.pipeline import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.pipeline.publish import RepairAction\n\n\nclass ValidateWorkfilePaths(\n        pyblish.api.InstancePlugin, OptionalPyblishPluginMixin):\n    \"\"\"Validate workfile paths so they are absolute.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"workfile\"]\n    hosts = [\"houdini\"]\n    label = \"Validate Workfile Paths\"\n    actions = [RepairAction]\n    optional = True\n\n    node_types = [\"file\", \"alembic\"]\n    prohibited_vars = [\"$HIP\", \"$JOB\"]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid()\n        self.log.debug(\n            \"Checking node types: {}\".format(\", \".join(self.node_types)))\n        self.log.debug(\n            \"Searching prohibited vars: {}\".format(\n                \", \".join(self.prohibited_vars)\n            )\n        )\n\n        if invalid:\n            all_container_vars = set()\n            for param in invalid:\n                value = param.unexpandedString()\n                contained_vars = [\n                    var for var in self.prohibited_vars\n                    if var in value\n                ]\n                all_container_vars.update(contained_vars)\n\n                self.log.error(\n                    \"Parm {} contains prohibited vars {}: {}\".format(\n                        param.path(),\n                        \", \".join(contained_vars),\n                        value)\n                )\n\n            message = (\n                \"Prohibited vars {} found in parameter values\".format(\n                    \", \".join(all_container_vars)\n                )\n            )\n            raise PublishValidationError(message, title=self.label)\n\n    @classmethod\n    def get_invalid(cls):\n        invalid = []\n        for param, _ in hou.fileReferences():\n            # it might return None for some reason\n            if not param:\n                continue\n            # skip nodes we are not interested in\n            if param.node().type().name() not in cls.node_types:\n                continue\n\n            if any(\n                    v for v in cls.prohibited_vars\n                    if v in param.unexpandedString()):\n                invalid.append(param)\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n        invalid = cls.get_invalid()\n        for param in invalid:\n            cls.log.info(\"Processing: {}\".format(param.path()))\n            cls.log.info(\"Replacing {} for {}\".format(\n                param.unexpandedString(),\n                hou.text.expandString(param.unexpandedString())))\n            param.set(hou.text.expandString(param.unexpandedString()))\n"
  },
  {
    "path": "openpype/hosts/houdini/startup/MainMenuCommon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<mainMenu>\n    <menuBar>\n        <subMenu id=\"openpype_menu\">\n            <labelExpression><![CDATA[\nimport os\nreturn os.environ.get(\"AVALON_LABEL\") or \"AYON\"\n]]></labelExpression>\n            <actionItem id=\"asset_name\">\n                    <labelExpression><![CDATA[\nfrom openpype.pipeline import get_current_asset_name, get_current_task_name\nlabel = \"{}, {}\".format(get_current_asset_name(), get_current_task_name())\nreturn label\n]]></labelExpression>\n            </actionItem>\n\n            <separatorItem/>\n\n            <scriptItem id=\"ayon_create\">\n                <label>Create...</label>\n                <scriptCode><![CDATA[\nimport hou\nfrom openpype.tools.utils import host_tools\nparent = hou.qt.mainWindow()\nhost_tools.show_publisher(parent, tab=\"create\")\n]]></scriptCode>\n            </scriptItem>\n\n            <scriptItem id=\"ayon_load\">\n                <label>Load...</label>\n                <scriptCode><![CDATA[\nimport hou\nfrom openpype.tools.utils import host_tools\nparent = hou.qt.mainWindow()\nhost_tools.show_loader(parent=parent, use_context=True)\n]]> </scriptCode>\n            </scriptItem>\n\n            <scriptItem id=\"publish\">\n                <label>Publish...</label>\n                <scriptCode><![CDATA[\nimport hou\nfrom openpype.tools.utils import host_tools\nparent = hou.qt.mainWindow()\nhost_tools.show_publisher(parent, tab=\"publish\")\n]]></scriptCode>\n            </scriptItem>\n\n            <scriptItem id=\"ayon_manage\">\n                <label>Manage...</label>\n                <scriptCode><![CDATA[\nimport hou\nfrom openpype.tools.utils import host_tools\nparent = hou.qt.mainWindow()\nhost_tools.show_scene_inventory(parent)\n]]></scriptCode>\n            </scriptItem>\n\n            <scriptItem id=\"library_load\">\n                <label>Library...</label>\n                <scriptCode><![CDATA[\nimport hou\nfrom openpype.tools.utils import host_tools\nparent = hou.qt.mainWindow()\nhost_tools.show_library_loader(parent=parent)\n]]></scriptCode>\n            </scriptItem>\n\n            <separatorItem/>\n\n            <scriptItem id=\"workfiles\">\n                <label>Work Files...</label>\n                <scriptCode><![CDATA[\nimport hou\nfrom openpype.tools.utils import host_tools\nparent = hou.qt.mainWindow()\nhost_tools.show_workfiles(parent)\n]]></scriptCode>\n            </scriptItem>\n\n            <scriptItem id=\"set_frame_range\">\n                <label>Set Frame Range</label>\n                <scriptCode><![CDATA[\nimport openpype.hosts.houdini.api.lib\nopenpype.hosts.houdini.api.lib.reset_framerange()\n]]></scriptCode>\n            </scriptItem>\n\n            <scriptItem id=\"update_context_vars\">\n                <label>Update Houdini Vars</label>\n                <scriptCode><![CDATA[\nimport openpype.hosts.houdini.api.lib\nopenpype.hosts.houdini.api.lib.update_houdini_vars_context_dialog()\n]]></scriptCode>\n            </scriptItem>\n\n            <separatorItem/>\n            <scriptItem id=\"experimental_tools\">\n                <label>Experimental tools...</label>\n                <scriptCode><![CDATA[\nimport hou\nfrom openpype.tools.utils import host_tools\nparent = hou.qt.mainWindow()\nhost_tools.show_experimental_tools_dialog(parent)\n]]></scriptCode>\n            </scriptItem>\n        </subMenu>\n    </menuBar>\n</mainMenu>\n"
  },
  {
    "path": "openpype/hosts/houdini/startup/python2.7libs/pythonrc.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"OpenPype startup script.\"\"\"\nfrom openpype.pipeline import install_host\nfrom openpype.hosts.houdini.api import HoudiniHost\nfrom openpype import AYON_SERVER_ENABLED\n\n\ndef main():\n    print(\"Installing {} ...\".format(\n        \"AYON\" if AYON_SERVER_ENABLED else \"OpenPype\"))\n    install_host(HoudiniHost())\n\n\nmain()\n"
  },
  {
    "path": "openpype/hosts/houdini/startup/python3.10libs/pythonrc.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"OpenPype startup script.\"\"\"\nfrom openpype.pipeline import install_host\nfrom openpype.hosts.houdini.api import HoudiniHost\nfrom openpype import AYON_SERVER_ENABLED\n\n\ndef main():\n    print(\"Installing {} ...\".format(\n        \"AYON\" if AYON_SERVER_ENABLED else \"OpenPype\"))\n    install_host(HoudiniHost())\n\n\nmain()\n"
  },
  {
    "path": "openpype/hosts/houdini/startup/python3.7libs/pythonrc.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"OpenPype startup script.\"\"\"\nfrom openpype.pipeline import install_host\nfrom openpype.hosts.houdini.api import HoudiniHost\nfrom openpype import AYON_SERVER_ENABLED\n\n\ndef main():\n    print(\"Installing {} ...\".format(\n        \"AYON\" if AYON_SERVER_ENABLED else \"OpenPype\"))\n    install_host(HoudiniHost())\n\n\nmain()\n"
  },
  {
    "path": "openpype/hosts/houdini/startup/python3.9libs/pythonrc.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"OpenPype startup script.\"\"\"\nfrom openpype.pipeline import install_host\nfrom openpype.hosts.houdini.api import HoudiniHost\nfrom openpype import AYON_SERVER_ENABLED\n\n\ndef main():\n    print(\"Installing {} ...\".format(\n        \"AYON\" if AYON_SERVER_ENABLED else \"OpenPype\"))\n    install_host(HoudiniHost())\n\n\nmain()\n"
  },
  {
    "path": "openpype/hosts/max/__init__.py",
    "content": "from .addon import (\n    MaxAddon,\n    MAX_HOST_DIR,\n)\n\n\n__all__ = (\n    \"MaxAddon\",\n    \"MAX_HOST_DIR\",\n)\n"
  },
  {
    "path": "openpype/hosts/max/addon.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nMAX_HOST_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass MaxAddon(OpenPypeModule, IHostAddon):\n    name = \"max\"\n    host_name = \"max\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        # Remove auto screen scale factor for Qt\n        # - let 3dsmax decide it's value\n        env.pop(\"QT_AUTO_SCREEN_SCALE_FACTOR\", None)\n\n    def get_workfile_extensions(self):\n        return [\".max\"]\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [\n            os.path.join(MAX_HOST_DIR, \"hooks\")\n        ]\n"
  },
  {
    "path": "openpype/hosts/max/api/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Public API for 3dsmax\"\"\"\n\nfrom .pipeline import (\n    MaxHost,\n)\n\n\nfrom .lib import (\n    maintained_selection,\n    lsattr,\n    get_all_children\n)\n\n__all__ = [\n    \"MaxHost\",\n    \"maintained_selection\",\n    \"lsattr\",\n    \"get_all_children\"\n]\n"
  },
  {
    "path": "openpype/hosts/max/api/action.py",
    "content": "from pymxs import runtime as rt\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import get_errored_instances_from_context\n\n\nclass SelectInvalidAction(pyblish.api.Action):\n    \"\"\"Select invalid objects in Blender when a publish plug-in failed.\"\"\"\n    label = \"Select Invalid\"\n    on = \"failed\"\n    icon = \"search\"\n\n    def process(self, context, plugin):\n        errored_instances = get_errored_instances_from_context(context,\n                                                               plugin=plugin)\n\n        # Get the invalid nodes for the plug-ins\n        self.log.info(\"Finding invalid nodes...\")\n        invalid = list()\n        for instance in errored_instances:\n            invalid_nodes = plugin.get_invalid(instance)\n            if invalid_nodes:\n                if isinstance(invalid_nodes, (list, tuple)):\n                    invalid.extend(invalid_nodes)\n                else:\n                    self.log.warning(\n                        \"Failed plug-in doesn't have any selectable objects.\"\n                    )\n\n        if not invalid:\n            self.log.info(\"No invalid nodes found.\")\n            return\n        invalid_names = [obj.name for obj in invalid if isinstance(obj, str)]\n        if not invalid_names:\n            invalid_names = [obj.name for obj, _ in invalid]\n            invalid = [obj for obj, _ in invalid]\n        self.log.info(\n            \"Selecting invalid objects: %s\", \", \".join(invalid_names)\n        )\n\n        rt.Select(invalid)\n"
  },
  {
    "path": "openpype/hosts/max/api/colorspace.py",
    "content": "import attr\nfrom pymxs import runtime as rt\n\n\n@attr.s\nclass LayerMetadata(object):\n    \"\"\"Data class for Render Layer metadata.\"\"\"\n    frameStart = attr.ib()\n    frameEnd = attr.ib()\n\n\n@attr.s\nclass RenderProduct(object):\n    \"\"\"Getting Colorspace as\n    Specific Render Product Parameter for submitting\n    publish job.\n    \"\"\"\n    colorspace = attr.ib()                      # colorspace\n    view = attr.ib()\n    productName = attr.ib(default=None)\n\n\nclass ARenderProduct(object):\n\n    def __init__(self):\n        \"\"\"Constructor.\"\"\"\n        # Initialize\n        self.layer_data = self._get_layer_data()\n        self.layer_data.products = self.get_colorspace_data()\n\n    def _get_layer_data(self):\n        return LayerMetadata(\n            frameStart=int(rt.rendStart),\n            frameEnd=int(rt.rendEnd),\n        )\n\n    def get_colorspace_data(self):\n        \"\"\"To be implemented by renderer class.\n        This should return a list of RenderProducts.\n        Returns:\n            list: List of RenderProduct\n        \"\"\"\n        colorspace_data = [\n            RenderProduct(\n                colorspace=\"sRGB\",\n                view=\"ACES 1.0\",\n                productName=\"\"\n            )\n        ]\n        return colorspace_data\n"
  },
  {
    "path": "openpype/hosts/max/api/lib.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Library of functions useful for 3dsmax pipeline.\"\"\"\nimport contextlib\nimport logging\nimport json\nfrom typing import Any, Dict, Union\n\nimport six\nfrom openpype.pipeline import get_current_project_name, colorspace\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline.context_tools import (\n    get_current_project, get_current_project_asset)\nfrom openpype.style import load_stylesheet\nfrom pymxs import runtime as rt\n\n\nJSON_PREFIX = \"JSON::\"\nlog = logging.getLogger(\"openpype.hosts.max\")\n\n\ndef get_main_window():\n    \"\"\"Acquire Max's main window\"\"\"\n    from qtpy import QtWidgets\n    top_widgets = QtWidgets.QApplication.topLevelWidgets()\n    name = \"QmaxApplicationWindow\"\n    for widget in top_widgets:\n        if (\n            widget.inherits(\"QMainWindow\")\n            and widget.metaObject().className() == name\n        ):\n            return widget\n    raise RuntimeError('Count not find 3dsMax main window.')\n\n\ndef imprint(node_name: str, data: dict) -> bool:\n    node = rt.GetNodeByName(node_name)\n    if not node:\n        return False\n\n    for k, v in data.items():\n        if isinstance(v, (dict, list)):\n            rt.SetUserProp(node, k, f\"{JSON_PREFIX}{json.dumps(v)}\")\n        else:\n            rt.SetUserProp(node, k, v)\n\n    return True\n\n\ndef lsattr(\n        attr: str,\n        value: Union[str, None] = None,\n        root: Union[str, None] = None) -> list:\n    \"\"\"List nodes having attribute with specified value.\n\n    Args:\n        attr (str): Attribute name to match.\n        value (str, Optional): Value to match, of omitted, all nodes\n            with specified attribute are returned no matter of value.\n        root (str, Optional): Root node name. If omitted, scene root is used.\n\n    Returns:\n        list of nodes.\n    \"\"\"\n    root = rt.RootNode if root is None else rt.GetNodeByName(root)\n\n    def output_node(node, nodes):\n        nodes.append(node)\n        for child in node.Children:\n            output_node(child, nodes)\n\n    nodes = []\n    output_node(root, nodes)\n    return [\n        n for n in nodes\n        if rt.GetUserProp(n, attr) == value\n    ] if value else [\n        n for n in nodes\n        if rt.GetUserProp(n, attr)\n    ]\n\n\ndef read(container) -> dict:\n    data = {}\n    props = rt.GetUserPropBuffer(container)\n    # this shouldn't happen but let's guard against it anyway\n    if not props:\n        return data\n\n    for line in props.split(\"\\r\\n\"):\n        try:\n            key, value = line.split(\"=\")\n        except ValueError:\n            # if the line cannot be split we can't really parse it\n            continue\n\n        value = value.strip()\n        if isinstance(value.strip(), six.string_types) and \\\n                value.startswith(JSON_PREFIX):\n            with contextlib.suppress(json.JSONDecodeError):\n                value = json.loads(value[len(JSON_PREFIX):])\n\n        # default value behavior\n        # convert maxscript boolean values\n        if value == \"true\":\n            value = True\n        elif value == \"false\":\n            value = False\n\n        data[key.strip()] = value\n\n    data[\"instance_node\"] = container.Name\n\n    return data\n\n\n@contextlib.contextmanager\ndef maintained_selection():\n    previous_selection = rt.GetCurrentSelection()\n    try:\n        yield\n    finally:\n        if previous_selection:\n            rt.Select(previous_selection)\n        else:\n            rt.Select()\n\n\ndef get_all_children(parent, node_type=None):\n    \"\"\"Handy function to get all the children of a given node\n\n    Args:\n        parent (3dsmax Node1): Node to get all children of.\n        node_type (None, runtime.class): give class to check for\n            e.g. rt.FFDBox/rt.GeometryClass etc.\n\n    Returns:\n        list: list of all children of the parent node\n    \"\"\"\n    def list_children(node):\n        children = []\n        for c in node.Children:\n            children.append(c)\n            children = children + list_children(c)\n        return children\n    child_list = list_children(parent)\n\n    return ([x for x in child_list if rt.SuperClassOf(x) == node_type]\n            if node_type else child_list)\n\n\ndef get_current_renderer():\n    \"\"\"\n    Notes:\n        Get current renderer for Max\n\n    Returns:\n        \"{Current Renderer}:{Current Renderer}\"\n        e.g. \"Redshift_Renderer:Redshift_Renderer\"\n    \"\"\"\n    return rt.renderers.production\n\n\ndef get_default_render_folder(project_setting=None):\n    return (project_setting[\"max\"]\n                           [\"RenderSettings\"]\n                           [\"default_render_image_folder\"])\n\n\ndef set_render_frame_range(start_frame, end_frame):\n    \"\"\"\n    Note:\n        Frame range can be specified in different types. Possible values are:\n        * `1` - Single frame.\n        * `2` - Active time segment ( animationRange ).\n        * `3` - User specified Range.\n        * `4` - User specified Frame pickup string (for example `1,3,5-12`).\n\n    Todo:\n        Current type is hard-coded, there should be a custom setting for this.\n    \"\"\"\n    rt.rendTimeType = 3\n    if start_frame is not None and end_frame is not None:\n        rt.rendStart = int(start_frame)\n        rt.rendEnd = int(end_frame)\n\n\ndef get_multipass_setting(project_setting=None):\n    return (project_setting[\"max\"]\n                           [\"RenderSettings\"]\n                           [\"multipass\"])\n\n\ndef set_scene_resolution(width: int, height: int):\n    \"\"\"Set the render resolution\n\n    Args:\n        width(int): value of the width\n        height(int): value of the height\n\n    Returns:\n        None\n\n    \"\"\"\n    # make sure the render dialog is closed\n    # for the update of resolution\n    # Changing the Render Setup dialog settings should be done\n    # with the actual Render Setup dialog in a closed state.\n    if rt.renderSceneDialog.isOpen():\n        rt.renderSceneDialog.close()\n\n    rt.renderWidth = width\n    rt.renderHeight = height\n\n\ndef reset_scene_resolution():\n    \"\"\"Apply the scene resolution from the project definition\n\n    scene resolution can be overwritten by an asset if the asset.data contains\n    any information regarding scene resolution .\n    Returns:\n        None\n    \"\"\"\n    data = [\"data.resolutionWidth\", \"data.resolutionHeight\"]\n    project_resolution = get_current_project(fields=data)\n    project_resolution_data = project_resolution[\"data\"]\n    asset_resolution = get_current_project_asset(fields=data)\n    asset_resolution_data = asset_resolution[\"data\"]\n    # Set project resolution\n    project_width = int(project_resolution_data.get(\"resolutionWidth\", 1920))\n    project_height = int(project_resolution_data.get(\"resolutionHeight\", 1080))\n    width = int(asset_resolution_data.get(\"resolutionWidth\", project_width))\n    height = int(asset_resolution_data.get(\"resolutionHeight\", project_height))\n\n    set_scene_resolution(width, height)\n\n\ndef get_frame_range(asset_doc=None) -> Union[Dict[str, Any], None]:\n    \"\"\"Get the current assets frame range and handles.\n\n    Args:\n        asset_doc (dict): Asset Entity Data\n\n    Returns:\n        dict: with frame start, frame end, handle start, handle end.\n    \"\"\"\n    # Set frame start/end\n    if asset_doc is None:\n        asset_doc = get_current_project_asset()\n\n    data = asset_doc[\"data\"]\n    frame_start = data.get(\"frameStart\")\n    frame_end = data.get(\"frameEnd\")\n\n    if frame_start is None or frame_end is None:\n        return {}\n\n    frame_start = int(frame_start)\n    frame_end = int(frame_end)\n    handle_start = int(data.get(\"handleStart\", 0))\n    handle_end = int(data.get(\"handleEnd\", 0))\n    frame_start_handle = frame_start - handle_start\n    frame_end_handle = frame_end + handle_end\n\n    return {\n        \"frameStart\": frame_start,\n        \"frameEnd\": frame_end,\n        \"handleStart\": handle_start,\n        \"handleEnd\": handle_end,\n        \"frameStartHandle\": frame_start_handle,\n        \"frameEndHandle\": frame_end_handle,\n    }\n\n\ndef reset_frame_range(fps: bool = True):\n    \"\"\"Set frame range to current asset.\n    This is part of 3dsmax documentation:\n\n    animationRange: A System Global variable which lets you get and\n        set an Interval value that defines the start and end frames\n        of the Active Time Segment.\n    frameRate: A System Global variable which lets you get\n            and set an Integer value that defines the current\n            scene frame rate in frames-per-second.\n    \"\"\"\n    if fps:\n        data_fps = get_current_project(fields=[\"data.fps\"])\n        fps_number = float(data_fps[\"data\"][\"fps\"])\n        rt.frameRate = fps_number\n    frame_range = get_frame_range()\n\n    set_timeline(\n        frame_range[\"frameStartHandle\"], frame_range[\"frameEndHandle\"])\n    set_render_frame_range(\n        frame_range[\"frameStartHandle\"], frame_range[\"frameEndHandle\"])\n\n\ndef reset_unit_scale():\n    \"\"\"Apply the unit scale setting to 3dsMax\n    \"\"\"\n    project_name = get_current_project_name()\n    settings = get_project_settings(project_name).get(\"max\")\n    scene_scale = settings.get(\"unit_scale_settings\",\n                               {}).get(\"scene_unit_scale\")\n    if scene_scale:\n        rt.units.DisplayType = rt.Name(\"Metric\")\n        rt.units.MetricType = rt.Name(scene_scale)\n    else:\n        rt.units.DisplayType = rt.Name(\"Generic\")\n\n\ndef convert_unit_scale():\n    \"\"\"Convert system unit scale in 3dsMax\n    for fbx export\n\n    Returns:\n        str: unit scale\n    \"\"\"\n    unit_scale_dict = {\n        \"millimeters\": \"mm\",\n        \"centimeters\": \"cm\",\n        \"meters\": \"m\",\n        \"kilometers\": \"km\"\n    }\n    current_unit_scale = rt.Execute(\"units.MetricType as string\")\n    return unit_scale_dict[current_unit_scale]\n\n\ndef set_context_setting():\n    \"\"\"Apply the project settings from the project definition\n\n    Settings can be overwritten by an asset if the asset.data contains\n    any information regarding those settings.\n\n    Examples of settings:\n        frame range\n        resolution\n\n    Returns:\n        None\n    \"\"\"\n    reset_scene_resolution()\n    reset_frame_range()\n    reset_colorspace()\n    reset_unit_scale()\n\n\ndef get_max_version():\n    \"\"\"\n    Args:\n    get max version date for deadline\n\n    Returns:\n        #(25000, 62, 0, 25, 0, 0, 997, 2023, \"\")\n        max_info[7] = max version date\n    \"\"\"\n    max_info = rt.MaxVersion()\n    return max_info[7]\n\n\ndef is_headless():\n    \"\"\"Check if 3dsMax runs in batch mode.\n    If it returns True, it runs in 3dsbatch.exe\n    If it returns False, it runs in 3dsmax.exe\n    \"\"\"\n    return rt.maxops.isInNonInteractiveMode()\n\n\ndef set_timeline(frameStart, frameEnd):\n    \"\"\"Set frame range for timeline editor in Max\n    \"\"\"\n    rt.animationRange = rt.interval(frameStart, frameEnd)\n    return rt.animationRange\n\n\ndef reset_colorspace():\n    \"\"\"OCIO Configuration\n    Supports in 3dsMax 2024+\n\n    \"\"\"\n    if int(get_max_version()) < 2024:\n        return\n    project_name = get_current_project_name()\n    colorspace_mgr = rt.ColorPipelineMgr\n    project_settings = get_project_settings(project_name)\n\n    max_config_data = colorspace.get_imageio_config(\n        project_name, \"max\", project_settings)\n    if max_config_data:\n        ocio_config_path = max_config_data[\"path\"]\n        colorspace_mgr = rt.ColorPipelineMgr\n        colorspace_mgr.Mode = rt.Name(\"OCIO_Custom\")\n        colorspace_mgr.OCIOConfigPath = ocio_config_path\n\n\ndef check_colorspace():\n    parent = get_main_window()\n    if parent is None:\n        log.info(\"Skipping outdated pop-up \"\n                 \"because Max main window can't be found.\")\n    if int(get_max_version()) >= 2024:\n        color_mgr = rt.ColorPipelineMgr\n        project_name = get_current_project_name()\n        project_settings = get_project_settings(project_name)\n        max_config_data = colorspace.get_imageio_config(\n            project_name, \"max\", project_settings)\n        if max_config_data and color_mgr.Mode != rt.Name(\"OCIO_Custom\"):\n            if not is_headless():\n                from openpype.widgets import popup\n                dialog = popup.Popup(parent=parent)\n                dialog.setWindowTitle(\"Warning: Wrong OCIO Mode\")\n                dialog.setMessage(\"This scene has wrong OCIO \"\n                                  \"Mode setting.\")\n                dialog.setButtonText(\"Fix\")\n                dialog.setStyleSheet(load_stylesheet())\n                dialog.on_clicked.connect(reset_colorspace)\n                dialog.show()\n\ndef unique_namespace(namespace, format=\"%02d\",\n                     prefix=\"\", suffix=\"\", con_suffix=\"CON\"):\n    \"\"\"Return unique namespace\n\n    Arguments:\n        namespace (str): Name of namespace to consider\n        format (str, optional): Formatting of the given iteration number\n        suffix (str, optional): Only consider namespaces with this suffix.\n        con_suffix: max only, for finding the name of the master container\n\n    >>> unique_namespace(\"bar\")\n    # bar01\n    >>> unique_namespace(\":hello\")\n    # :hello01\n    >>> unique_namespace(\"bar:\", suffix=\"_NS\")\n    # bar01_NS:\n\n    \"\"\"\n\n    def current_namespace():\n        current = namespace\n        # When inside a namespace Max adds no trailing :\n        if not current.endswith(\":\"):\n            current += \":\"\n        return current\n\n    # Always check against the absolute namespace root\n    # There's no clash with :x if we're defining namespace :a:x\n    ROOT = \":\" if namespace.startswith(\":\") else current_namespace()\n\n    # Strip trailing `:` tokens since we might want to add a suffix\n    start = \":\" if namespace.startswith(\":\") else \"\"\n    end = \":\" if namespace.endswith(\":\") else \"\"\n    namespace = namespace.strip(\":\")\n    if \":\" in namespace:\n        # Split off any nesting that we don't uniqify anyway.\n        parents, namespace = namespace.rsplit(\":\", 1)\n        start += parents + \":\"\n        ROOT += start\n\n    iteration = 1\n    increment_version = True\n    while increment_version:\n        nr_namespace = namespace + format % iteration\n        unique = prefix + nr_namespace + suffix\n        container_name = f\"{unique}:{namespace}{con_suffix}\"\n        if not rt.getNodeByName(container_name):\n            name_space = start + unique + end\n            increment_version = False\n            return name_space\n        else:\n            increment_version = True\n        iteration += 1\n\n\ndef get_namespace(container_name):\n    \"\"\"Get the namespace and name of the sub-container\n\n    Args:\n        container_name (str): the name of master container\n\n    Raises:\n        RuntimeError: when there is no master container found\n\n    Returns:\n        namespace (str): namespace of the sub-container\n        name (str): name of the sub-container\n    \"\"\"\n    node = rt.getNodeByName(container_name)\n    if not node:\n        raise RuntimeError(\"Master Container Not Found..\")\n    name = rt.getUserProp(node, \"name\")\n    namespace = rt.getUserProp(node, \"namespace\")\n    return namespace, name\n\n\ndef object_transform_set(container_children):\n    \"\"\"A function which allows to store the transform of\n    previous loaded object(s)\n    Args:\n        container_children(list): A list of nodes\n\n    Returns:\n        transform_set (dict): A dict with all transform data of\n        the previous loaded object(s)\n    \"\"\"\n    transform_set = {}\n    for node in container_children:\n        name = f\"{node.name}.transform\"\n        transform_set[name] = node.pos\n        name = f\"{node.name}.scale\"\n        transform_set[name] = node.scale\n    return transform_set\n\n\ndef get_plugins() -> list:\n    \"\"\"Get all loaded plugins in 3dsMax\n\n    Returns:\n        plugin_info_list: a list of loaded plugins\n    \"\"\"\n    manager = rt.PluginManager\n    count = manager.pluginDllCount\n    plugin_info_list = []\n    for p in range(1, count + 1):\n        plugin_info = manager.pluginDllName(p)\n        plugin_info_list.append(plugin_info)\n\n    return plugin_info_list\n\n\n@contextlib.contextmanager\ndef render_resolution(width, height):\n    \"\"\"Set render resolution option during context\n\n    Args:\n        width (int): render width\n        height (int): render height\n    \"\"\"\n    current_renderWidth = rt.renderWidth\n    current_renderHeight = rt.renderHeight\n    try:\n        rt.renderWidth = width\n        rt.renderHeight = height\n        yield\n    finally:\n        rt.renderWidth = current_renderWidth\n        rt.renderHeight = current_renderHeight\n\n\n@contextlib.contextmanager\ndef suspended_refresh():\n    \"\"\"Suspended refresh for scene and modify panel redraw.\n    \"\"\"\n    if is_headless():\n        yield\n        return\n    rt.disableSceneRedraw()\n    rt.suspendEditing()\n    try:\n        yield\n\n    finally:\n        rt.enableSceneRedraw()\n        rt.resumeEditing()\n"
  },
  {
    "path": "openpype/hosts/max/api/lib_renderproducts.py",
    "content": "# Render Element Example : For scanline render, VRay\n# https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-E8F75D47-B998-4800-A3A5-610E22913CFC\n# arnold\n# https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_3ds_max_ax_maxscript_commands_ax_renderview_commands_html\nimport os\n\nfrom pymxs import runtime as rt\n\nfrom openpype.hosts.max.api.lib import get_current_renderer\nfrom openpype.pipeline import get_current_project_name\nfrom openpype.settings import get_project_settings\n\n\nclass RenderProducts(object):\n\n    def __init__(self, project_settings=None):\n        self._project_settings = project_settings\n        if not self._project_settings:\n            self._project_settings = get_project_settings(\n                get_current_project_name()\n            )\n\n    def get_beauty(self, container):\n        render_dir = os.path.dirname(rt.rendOutputFilename)\n\n        output_file = os.path.join(render_dir, container)\n\n        setting = self._project_settings\n        img_fmt = setting[\"max\"][\"RenderSettings\"][\"image_format\"]   # noqa\n\n        start_frame = int(rt.rendStart)\n        end_frame = int(rt.rendEnd) + 1\n\n        return {\n            \"beauty\": self.get_expected_beauty(\n                output_file, start_frame, end_frame, img_fmt\n            )\n        }\n\n    def get_multiple_beauty(self, outputs, cameras):\n        beauty_output_frames = dict()\n        for output, camera in zip(outputs, cameras):\n            filename, ext = os.path.splitext(output)\n            filename = filename.replace(\".\", \"\")\n            ext = ext.replace(\".\", \"\")\n            start_frame = int(rt.rendStart)\n            end_frame = int(rt.rendEnd) + 1\n            new_beauty = self.get_expected_beauty(\n                filename, start_frame, end_frame, ext\n            )\n            beauty_output = ({\n                f\"{camera}_beauty\": new_beauty\n            })\n            beauty_output_frames.update(beauty_output)\n        return beauty_output_frames\n\n    def get_multiple_aovs(self, outputs, cameras):\n        renderer_class = get_current_renderer()\n        renderer = str(renderer_class).split(\":\")[0]\n        aovs_frames = {}\n        for output, camera in zip(outputs, cameras):\n            filename, ext = os.path.splitext(output)\n            filename = filename.replace(\".\", \"\")\n            ext = ext.replace(\".\", \"\")\n            start_frame = int(rt.rendStart)\n            end_frame = int(rt.rendEnd) + 1\n\n            if renderer in [\n                \"ART_Renderer\",\n                \"V_Ray_6_Hotfix_3\",\n                \"V_Ray_GPU_6_Hotfix_3\",\n                \"Default_Scanline_Renderer\",\n                \"Quicksilver_Hardware_Renderer\",\n            ]:\n                render_name = self.get_render_elements_name()\n                if render_name:\n                    for name in render_name:\n                        aovs_frames.update({\n                            f\"{camera}_{name}\": self.get_expected_aovs(\n                                filename, name, start_frame,\n                                end_frame, ext)\n                        })\n            elif renderer == \"Redshift_Renderer\":\n                render_name = self.get_render_elements_name()\n                if render_name:\n                    rs_aov_files = rt.Execute(\"renderers.current.separateAovFiles\")     # noqa\n                    # this doesn't work, always returns False\n                    # rs_AovFiles = rt.RedShift_Renderer().separateAovFiles\n                    if ext == \"exr\" and not rs_aov_files:\n                        for name in render_name:\n                            if name == \"RsCryptomatte\":\n                                aovs_frames.update({\n                                    f\"{camera}_{name}\": self.get_expected_aovs(\n                                        filename, name, start_frame,\n                                        end_frame, ext)\n                                })\n                    else:\n                        for name in render_name:\n                            aovs_frames.update({\n                                f\"{camera}_{name}\": self.get_expected_aovs(\n                                    filename, name, start_frame,\n                                    end_frame, ext)\n                            })\n            elif renderer == \"Arnold\":\n                render_name = self.get_arnold_product_name()\n                if render_name:\n                    for name in render_name:\n                        aovs_frames.update({\n                            f\"{camera}_{name}\": self.get_expected_arnold_product(   # noqa\n                                filename, name, start_frame,\n                                end_frame, ext)\n                        })\n            elif renderer in [\n                \"V_Ray_6_Hotfix_3\",\n                \"V_Ray_GPU_6_Hotfix_3\"\n            ]:\n                if ext != \"exr\":\n                    render_name = self.get_render_elements_name()\n                    if render_name:\n                        for name in render_name:\n                            aovs_frames.update({\n                                f\"{camera}_{name}\": self.get_expected_aovs(\n                                    filename, name, start_frame,\n                                    end_frame, ext)\n                            })\n\n        return aovs_frames\n\n    def get_aovs(self, container):\n        render_dir = os.path.dirname(rt.rendOutputFilename)\n\n        output_file = os.path.join(render_dir,\n                                   container)\n\n        setting = self._project_settings\n        img_fmt = setting[\"max\"][\"RenderSettings\"][\"image_format\"]   # noqa\n\n        start_frame = int(rt.rendStart)\n        end_frame = int(rt.rendEnd) + 1\n        renderer_class = get_current_renderer()\n        renderer = str(renderer_class).split(\":\")[0]\n        render_dict = {}\n\n        if renderer in [\n            \"ART_Renderer\",\n            \"V_Ray_6_Hotfix_3\",\n            \"V_Ray_GPU_6_Hotfix_3\",\n            \"Default_Scanline_Renderer\",\n            \"Quicksilver_Hardware_Renderer\",\n        ]:\n            render_name = self.get_render_elements_name()\n            if render_name:\n                for name in render_name:\n                    render_dict.update({\n                        name: self.get_expected_aovs(\n                            output_file, name, start_frame,\n                            end_frame, img_fmt)\n                    })\n        elif renderer == \"Redshift_Renderer\":\n            render_name = self.get_render_elements_name()\n            if render_name:\n                rs_aov_files = rt.Execute(\"renderers.current.separateAovFiles\")\n                # this doesn't work, always returns False\n                # rs_AovFiles = rt.RedShift_Renderer().separateAovFiles\n                if img_fmt == \"exr\" and not rs_aov_files:\n                    for name in render_name:\n                        if name == \"RsCryptomatte\":\n                            render_dict.update({\n                                name: self.get_expected_aovs(\n                                    output_file, name, start_frame,\n                                    end_frame, img_fmt)\n                            })\n                else:\n                    for name in render_name:\n                        render_dict.update({\n                            name: self.get_expected_aovs(\n                                output_file, name, start_frame,\n                                end_frame, img_fmt)\n                        })\n\n        elif renderer == \"Arnold\":\n            render_name = self.get_arnold_product_name()\n            if render_name:\n                for name in render_name:\n                    render_dict.update({\n                        name: self.get_expected_arnold_product(\n                            output_file, name, start_frame,\n                            end_frame, img_fmt)\n                    })\n        elif renderer in [\n            \"V_Ray_6_Hotfix_3\",\n            \"V_Ray_GPU_6_Hotfix_3\"\n        ]:\n            if img_fmt != \"exr\":\n                render_name = self.get_render_elements_name()\n                if render_name:\n                    for name in render_name:\n                        render_dict.update({\n                            name: self.get_expected_aovs(\n                                output_file, name, start_frame,\n                                end_frame, img_fmt)      # noqa\n                        })\n\n        return render_dict\n\n    def get_expected_beauty(self, folder, start_frame, end_frame, fmt):\n        beauty_frame_range = []\n        for f in range(start_frame, end_frame):\n            frame = \"%04d\" % f\n            beauty_output = f\"{folder}.{frame}.{fmt}\"\n            beauty_output = beauty_output.replace(\"\\\\\", \"/\")\n            beauty_frame_range.append(beauty_output)\n\n        return beauty_frame_range\n\n    def get_arnold_product_name(self):\n        \"\"\"Get all the Arnold AOVs name\"\"\"\n        aov_name = []\n\n        amw = rt.MaxToAOps.AOVsManagerWindow()\n        aov_mgr = rt.renderers.current.AOVManager\n        # Check if there is any aov group set in AOV manager\n        aov_group_num = len(aov_mgr.drivers)\n        if aov_group_num < 1:\n            return\n        for i in range(aov_group_num):\n            # get the specific AOV group\n            aov_name.extend(aov.name for aov in aov_mgr.drivers[i].aov_list)\n        # close the AOVs manager window\n        amw.close()\n\n        return aov_name\n\n    def get_expected_arnold_product(self, folder, name,\n                                    start_frame, end_frame, fmt):\n        \"\"\"Get all the expected Arnold AOVs\"\"\"\n        aov_list = []\n        for f in range(start_frame, end_frame):\n            frame = \"%04d\" % f\n            render_element = f\"{folder}_{name}.{frame}.{fmt}\"\n            render_element = render_element.replace(\"\\\\\", \"/\")\n            aov_list.append(render_element)\n\n        return aov_list\n\n    def get_render_elements_name(self):\n        \"\"\"Get all the render element names for general \"\"\"\n        render_name = []\n        render_elem = rt.maxOps.GetCurRenderElementMgr()\n        render_elem_num = render_elem.NumRenderElements()\n        if render_elem_num < 1:\n            return\n        # get render elements from the renders\n        for i in range(render_elem_num):\n            renderlayer_name = render_elem.GetRenderElement(i)\n            if renderlayer_name.enabled:\n                target, renderpass = str(renderlayer_name).split(\":\")\n                render_name.append(renderpass)\n\n        return render_name\n\n    def get_expected_aovs(self, folder, name,\n                          start_frame, end_frame, fmt):\n        \"\"\"Get all the expected render element output files. \"\"\"\n        render_elements = []\n        for f in range(start_frame, end_frame):\n            frame = \"%04d\" % f\n            render_element = f\"{folder}_{name}.{frame}.{fmt}\"\n            render_element = render_element.replace(\"\\\\\", \"/\")\n            render_elements.append(render_element)\n\n        return render_elements\n\n    def image_format(self):\n        return self._project_settings[\"max\"][\"RenderSettings\"][\"image_format\"]  # noqa\n"
  },
  {
    "path": "openpype/hosts/max/api/lib_rendersettings.py",
    "content": "import os\nfrom pymxs import runtime as rt\nfrom openpype.lib import Logger\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import get_current_project_name\nfrom openpype.pipeline.context_tools import get_current_project_asset\n\nfrom openpype.hosts.max.api.lib import (\n    set_render_frame_range,\n    get_current_renderer,\n    get_default_render_folder\n)\n\n\nclass RenderSettings(object):\n\n    log = Logger.get_logger(\"RenderSettings\")\n\n    _aov_chars = {\n        \"dot\": \".\",\n        \"dash\": \"-\",\n        \"underscore\": \"_\"\n    }\n\n    def __init__(self, project_settings=None):\n        \"\"\"\n        Set up the naming convention for the render\n        elements for the deadline submission\n        \"\"\"\n\n        self._project_settings = project_settings\n        if not self._project_settings:\n            self._project_settings = get_project_settings(\n                get_current_project_name()\n            )\n\n    def set_render_camera(self, selection):\n        for sel in selection:\n            # to avoid Attribute Error from pymxs wrapper\n            if rt.classOf(sel) in rt.Camera.classes:\n                rt.viewport.setCamera(sel)\n                return\n        raise RuntimeError(\"Active Camera not found\")\n\n    def render_output(self, container):\n        folder = rt.maxFilePath\n        # hard-coded, should be customized in the setting\n        file = rt.maxFileName\n        folder = folder.replace(\"\\\\\", \"/\")\n        # hard-coded, set the renderoutput path\n        setting = self._project_settings\n        render_folder = get_default_render_folder(setting)\n        filename, ext = os.path.splitext(file)\n        output_dir = os.path.join(folder,\n                                  render_folder,\n                                  filename)\n        if not os.path.exists(output_dir):\n            os.makedirs(output_dir)\n        # hard-coded, should be customized in the setting\n        context = get_current_project_asset()\n\n        # get project resolution\n        width = context[\"data\"].get(\"resolutionWidth\")\n        height = context[\"data\"].get(\"resolutionHeight\")\n        # Set Frame Range\n        frame_start = context[\"data\"].get(\"frame_start\")\n        frame_end = context[\"data\"].get(\"frame_end\")\n        set_render_frame_range(frame_start, frame_end)\n        # get the production render\n        renderer_class = get_current_renderer()\n        renderer = str(renderer_class).split(\":\")[0]\n\n        img_fmt = self._project_settings[\"max\"][\"RenderSettings\"][\"image_format\"]   # noqa\n        output = os.path.join(output_dir, container)\n        try:\n            aov_separator = self._aov_chars[(\n                self._project_settings[\"max\"]\n                                      [\"RenderSettings\"]\n                                      [\"aov_separator\"]\n            )]\n        except KeyError:\n            aov_separator = \".\"\n        output_filename = f\"{output}..{img_fmt}\"\n        output_filename = output_filename.replace(\"{aov_separator}\",\n                                                  aov_separator)\n        rt.rendOutputFilename = output_filename\n        if renderer == \"VUE_File_Renderer\":\n            return\n        # TODO: Finish the arnold render setup\n        if renderer == \"Arnold\":\n            self.arnold_setup()\n\n        if renderer in [\n            \"ART_Renderer\",\n            \"Redshift_Renderer\",\n            \"V_Ray_6_Hotfix_3\",\n            \"V_Ray_GPU_6_Hotfix_3\",\n            \"Default_Scanline_Renderer\",\n            \"Quicksilver_Hardware_Renderer\",\n        ]:\n            self.render_element_layer(output, width, height, img_fmt)\n\n        rt.rendSaveFile = True\n\n        if rt.renderSceneDialog.isOpen():\n            rt.renderSceneDialog.close()\n\n    def arnold_setup(self):\n        # get Arnold RenderView run in the background\n        # for setting up renderable camera\n        arv = rt.MAXToAOps.ArnoldRenderView()\n        render_camera = rt.viewport.GetCamera()\n        if render_camera:\n            arv.setOption(\"Camera\", str(render_camera))\n\n        # TODO: add AOVs and extension\n        img_fmt = self._project_settings[\"max\"][\"RenderSettings\"][\"image_format\"]   # noqa\n        setup_cmd = (\n            f\"\"\"\n        amw = MaxtoAOps.AOVsManagerWindow()\n        amw.close()\n        aovmgr = renderers.current.AOVManager\n        aovmgr.drivers = #()\n        img_fmt = \"{img_fmt}\"\n        if img_fmt == \"png\" then driver = ArnoldPNGDriver()\n        if img_fmt == \"jpg\" then driver = ArnoldJPEGDriver()\n        if img_fmt == \"exr\" then driver = ArnoldEXRDriver()\n        if img_fmt == \"tif\" then driver = ArnoldTIFFDriver()\n        if img_fmt == \"tiff\" then driver = ArnoldTIFFDriver()\n        append aovmgr.drivers driver\n        aovmgr.drivers[1].aov_list = #()\n            \"\"\")\n\n        rt.execute(setup_cmd)\n        arv.close()\n\n    def render_element_layer(self, dir, width, height, ext):\n        \"\"\"For Renderers with render elements\"\"\"\n        rt.renderWidth = width\n        rt.renderHeight = height\n        render_elem = rt.maxOps.GetCurRenderElementMgr()\n        render_elem_num = render_elem.NumRenderElements()\n        if render_elem_num < 0:\n            return\n\n        for i in range(render_elem_num):\n            renderlayer_name = render_elem.GetRenderElement(i)\n            target, renderpass = str(renderlayer_name).split(\":\")\n            aov_name = f\"{dir}_{renderpass}..{ext}\"\n            render_elem.SetRenderElementFileName(i, aov_name)\n\n    def get_render_output(self, container, output_dir):\n        output = os.path.join(output_dir, container)\n        img_fmt = self._project_settings[\"max\"][\"RenderSettings\"][\"image_format\"]   # noqa\n        output_filename = f\"{output}..{img_fmt}\"\n        return output_filename\n\n    def get_render_element(self):\n        orig_render_elem = []\n        render_elem = rt.maxOps.GetCurRenderElementMgr()\n        render_elem_num = render_elem.NumRenderElements()\n        if render_elem_num < 0:\n            return\n\n        for i in range(render_elem_num):\n            render_element = render_elem.GetRenderElementFilename(i)\n            orig_render_elem.append(render_element)\n\n        return orig_render_elem\n\n    def get_batch_render_elements(self, container,\n                                  output_dir, camera):\n        render_element_list = list()\n        output = os.path.join(output_dir, container)\n        render_elem = rt.maxOps.GetCurRenderElementMgr()\n        render_elem_num = render_elem.NumRenderElements()\n        if render_elem_num < 0:\n            return\n        img_fmt = self._project_settings[\"max\"][\"RenderSettings\"][\"image_format\"]   # noqa\n\n        for i in range(render_elem_num):\n            renderlayer_name = render_elem.GetRenderElement(i)\n            target, renderpass = str(renderlayer_name).split(\":\")\n            aov_name = f\"{output}_{camera}_{renderpass}..{img_fmt}\"\n            render_element_list.append(aov_name)\n        return render_element_list\n\n    def get_batch_render_output(self, camera):\n        target_layer_no = rt.batchRenderMgr.FindView(camera)\n        target_layer = rt.batchRenderMgr.GetView(target_layer_no)\n        return target_layer.outputFilename\n\n    def batch_render_elements(self, camera):\n        target_layer_no = rt.batchRenderMgr.FindView(camera)\n        target_layer = rt.batchRenderMgr.GetView(target_layer_no)\n        outputfilename = target_layer.outputFilename\n        directory = os.path.dirname(outputfilename)\n        render_elem = rt.maxOps.GetCurRenderElementMgr()\n        render_elem_num = render_elem.NumRenderElements()\n        if render_elem_num < 0:\n            return\n        ext = self._project_settings[\"max\"][\"RenderSettings\"][\"image_format\"]   # noqa\n\n        for i in range(render_elem_num):\n            renderlayer_name = render_elem.GetRenderElement(i)\n            target, renderpass = str(renderlayer_name).split(\":\")\n            aov_name = f\"{directory}_{camera}_{renderpass}..{ext}\"\n            render_elem.SetRenderElementFileName(i, aov_name)\n\n    def batch_render_layer(self, container,\n                           output_dir, cameras):\n        outputs = list()\n        output = os.path.join(output_dir, container)\n        img_fmt = self._project_settings[\"max\"][\"RenderSettings\"][\"image_format\"]   # noqa\n        for cam in cameras:\n            camera = rt.getNodeByName(cam)\n            layer_no = rt.batchRenderMgr.FindView(cam)\n            renderlayer = None\n            if layer_no == 0:\n                renderlayer = rt.batchRenderMgr.CreateView(camera)\n            else:\n                renderlayer = rt.batchRenderMgr.GetView(layer_no)\n            # use camera name as renderlayer name\n            renderlayer.name = cam\n            renderlayer.outputFilename = f\"{output}_{cam}..{img_fmt}\"\n            outputs.append(renderlayer.outputFilename)\n        return outputs\n"
  },
  {
    "path": "openpype/hosts/max/api/menu.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"3dsmax menu definition of AYON.\"\"\"\nimport os\nfrom qtpy import QtWidgets, QtCore\nfrom pymxs import runtime as rt\n\nfrom openpype.tools.utils import host_tools\nfrom openpype.hosts.max.api import lib\n\n\nclass OpenPypeMenu(object):\n    \"\"\"Object representing OpenPype/AYON menu.\n\n    This is using \"hack\" to inject itself before \"Help\" menu of 3dsmax.\n    For some reason `postLoadingMenus` event doesn't fire, and main menu\n    if probably re-initialized by menu templates, se we wait for at least\n    1 event Qt event loop before trying to insert.\n\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.main_widget = self.get_main_widget()\n        self.menu = None\n\n        timer = QtCore.QTimer()\n        # set number of event loops to wait.\n        timer.setInterval(1)\n        timer.timeout.connect(self._on_timer)\n        timer.start()\n\n        self._timer = timer\n        self._counter = 0\n\n    def _on_timer(self):\n        if self._counter < 1:\n            self._counter += 1\n            return\n\n        self._counter = 0\n        self._timer.stop()\n        self.build_openpype_menu()\n\n    @staticmethod\n    def get_main_widget():\n        \"\"\"Get 3dsmax main window.\"\"\"\n        return QtWidgets.QWidget.find(rt.windows.getMAXHWND())\n\n    def get_main_menubar(self) -> QtWidgets.QMenuBar:\n        \"\"\"Get main Menubar by 3dsmax main window.\"\"\"\n        return list(self.main_widget.findChildren(QtWidgets.QMenuBar))[0]\n\n    def get_or_create_openpype_menu(\n            self, name: str = \"&Openpype\",\n            before: str = \"&Help\") -> QtWidgets.QAction:\n        \"\"\"Create AYON menu.\n\n        Args:\n            name (str, Optional): AYON menu name.\n            before (str, Optional): Name of the 3dsmax main menu item to\n                add AYON menu before.\n\n        Returns:\n            QtWidgets.QAction: AYON menu action.\n\n        \"\"\"\n        if self.menu is not None:\n            return self.menu\n\n        menu_bar = self.get_main_menubar()\n        menu_items = menu_bar.findChildren(\n            QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly)\n        help_action = None\n        for item in menu_items:\n            if name in item.title():\n                # we already have OpenPype menu\n                return item\n\n            if before in item.title():\n                help_action = item.menuAction()\n        tab_menu_label = os.environ.get(\"AVALON_LABEL\") or \"AYON\"\n        op_menu = QtWidgets.QMenu(\"&{}\".format(tab_menu_label))\n        menu_bar.insertMenu(help_action, op_menu)\n\n        self.menu = op_menu\n        return op_menu\n\n    def build_openpype_menu(self) -> QtWidgets.QAction:\n        \"\"\"Build items in AYON menu.\"\"\"\n        openpype_menu = self.get_or_create_openpype_menu()\n        load_action = QtWidgets.QAction(\"Load...\", openpype_menu)\n        load_action.triggered.connect(self.load_callback)\n        openpype_menu.addAction(load_action)\n\n        publish_action = QtWidgets.QAction(\"Publish...\", openpype_menu)\n        publish_action.triggered.connect(self.publish_callback)\n        openpype_menu.addAction(publish_action)\n\n        manage_action = QtWidgets.QAction(\"Manage...\", openpype_menu)\n        manage_action.triggered.connect(self.manage_callback)\n        openpype_menu.addAction(manage_action)\n\n        library_action = QtWidgets.QAction(\"Library...\", openpype_menu)\n        library_action.triggered.connect(self.library_callback)\n        openpype_menu.addAction(library_action)\n\n        openpype_menu.addSeparator()\n\n        workfiles_action = QtWidgets.QAction(\"Work Files...\", openpype_menu)\n        workfiles_action.triggered.connect(self.workfiles_callback)\n        openpype_menu.addAction(workfiles_action)\n\n        openpype_menu.addSeparator()\n\n        res_action = QtWidgets.QAction(\"Set Resolution\", openpype_menu)\n        res_action.triggered.connect(self.resolution_callback)\n        openpype_menu.addAction(res_action)\n\n        frame_action = QtWidgets.QAction(\"Set Frame Range\", openpype_menu)\n        frame_action.triggered.connect(self.frame_range_callback)\n        openpype_menu.addAction(frame_action)\n\n        colorspace_action = QtWidgets.QAction(\"Set Colorspace\", openpype_menu)\n        colorspace_action.triggered.connect(self.colorspace_callback)\n        openpype_menu.addAction(colorspace_action)\n\n        unit_scale_action = QtWidgets.QAction(\"Set Unit Scale\", openpype_menu)\n        unit_scale_action.triggered.connect(self.unit_scale_callback)\n        openpype_menu.addAction(unit_scale_action)\n\n        return openpype_menu\n\n    def load_callback(self):\n        \"\"\"Callback to show Loader tool.\"\"\"\n        host_tools.show_loader(parent=self.main_widget)\n\n    def publish_callback(self):\n        \"\"\"Callback to show Publisher tool.\"\"\"\n        host_tools.show_publisher(parent=self.main_widget)\n\n    def manage_callback(self):\n        \"\"\"Callback to show Scene Manager/Inventory tool.\"\"\"\n        host_tools.show_scene_inventory(parent=self.main_widget)\n\n    def library_callback(self):\n        \"\"\"Callback to show Library Loader tool.\"\"\"\n        host_tools.show_library_loader(parent=self.main_widget)\n\n    def workfiles_callback(self):\n        \"\"\"Callback to show Workfiles tool.\"\"\"\n        host_tools.show_workfiles(parent=self.main_widget)\n\n    def resolution_callback(self):\n        \"\"\"Callback to reset scene resolution\"\"\"\n        return lib.reset_scene_resolution()\n\n    def frame_range_callback(self):\n        \"\"\"Callback to reset frame range\"\"\"\n        return lib.reset_frame_range()\n\n    def colorspace_callback(self):\n        \"\"\"Callback to reset colorspace\"\"\"\n        return lib.reset_colorspace()\n\n    def unit_scale_callback(self):\n        \"\"\"Callback to reset unit scale\"\"\"\n        return lib.reset_unit_scale()\n"
  },
  {
    "path": "openpype/hosts/max/api/pipeline.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Pipeline tools for OpenPype Houdini integration.\"\"\"\nimport os\nimport logging\nfrom operator import attrgetter\n\nimport json\n\nfrom openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost\nimport pyblish.api\nfrom openpype.pipeline import (\n    register_creator_plugin_path,\n    register_loader_plugin_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.hosts.max.api.menu import OpenPypeMenu\nfrom openpype.hosts.max.api import lib\nfrom openpype.hosts.max.api.plugin import MS_CUSTOM_ATTRIB\nfrom openpype.hosts.max import MAX_HOST_DIR\n\nfrom pymxs import runtime as rt  # noqa\n\nlog = logging.getLogger(\"openpype.hosts.max\")\n\nPLUGINS_DIR = os.path.join(MAX_HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\nINVENTORY_PATH = os.path.join(PLUGINS_DIR, \"inventory\")\n\n\nclass MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):\n\n    name = \"max\"\n    menu = None\n\n    def __init__(self):\n        super(MaxHost, self).__init__()\n        self._op_events = {}\n        self._has_been_setup = False\n\n    def install(self):\n        pyblish.api.register_host(\"max\")\n\n        pyblish.api.register_plugin_path(PUBLISH_PATH)\n        register_loader_plugin_path(LOAD_PATH)\n        register_creator_plugin_path(CREATE_PATH)\n\n        # self._register_callbacks()\n        self.menu = OpenPypeMenu()\n\n        self._has_been_setup = True\n\n        def context_setting():\n            return lib.set_context_setting()\n\n        rt.callbacks.addScript(rt.Name('systemPostNew'),\n                               context_setting)\n\n        rt.callbacks.addScript(rt.Name('filePostOpen'),\n                               lib.check_colorspace)\n        rt.callbacks.addScript(rt.Name('postWorkspaceChange'),\n                               self._deferred_menu_creation)\n\n    def workfile_has_unsaved_changes(self):\n        return rt.getSaveRequired()\n\n    def get_workfile_extensions(self):\n        return [\".max\"]\n\n    def save_workfile(self, dst_path=None):\n        rt.saveMaxFile(dst_path)\n        return dst_path\n\n    def open_workfile(self, filepath):\n        rt.checkForSave()\n        rt.loadMaxFile(filepath)\n        return filepath\n\n    def get_current_workfile(self):\n        return os.path.join(rt.maxFilePath, rt.maxFileName)\n\n    def get_containers(self):\n        return ls()\n\n    def _register_callbacks(self):\n        rt.callbacks.removeScripts(id=rt.name(\"OpenPypeCallbacks\"))\n\n        rt.callbacks.addScript(\n            rt.Name(\"postLoadingMenus\"),\n            self._deferred_menu_creation, id=rt.Name('OpenPypeCallbacks'))\n\n    def _deferred_menu_creation(self):\n        self.log.info(\"Building menu ...\")\n        self.menu = OpenPypeMenu()\n\n    @staticmethod\n    def create_context_node():\n        \"\"\"Helper for creating context holding node.\"\"\"\n\n        root_scene = rt.rootScene\n\n        create_attr_script = (\"\"\"\nattributes \"OpenPypeContext\"\n(\n    parameters main rollout:params\n    (\n        context type: #string\n    )\n\n    rollout params \"OpenPype Parameters\"\n    (\n        editText editTextContext \"Context\" type: #string\n    )\n)\n        \"\"\")\n\n        attr = rt.execute(create_attr_script)\n        rt.custAttributes.add(root_scene, attr)\n\n        return root_scene.OpenPypeContext.context\n\n    def update_context_data(self, data, changes):\n        try:\n            _ = rt.rootScene.OpenPypeContext.context\n        except AttributeError:\n            # context node doesn't exists\n            self.create_context_node()\n\n        rt.rootScene.OpenPypeContext.context = json.dumps(data)\n\n    def get_context_data(self):\n        try:\n            context = rt.rootScene.OpenPypeContext.context\n        except AttributeError:\n            # context node doesn't exists\n            context = self.create_context_node()\n        if not context:\n            context = \"{}\"\n        return json.loads(context)\n\n    def save_file(self, dst_path=None):\n        # Force forwards slashes to avoid segfault\n        dst_path = dst_path.replace(\"\\\\\", \"/\")\n        rt.saveMaxFile(dst_path)\n\n\ndef ls() -> list:\n    \"\"\"Get all OpenPype instances.\"\"\"\n    objs = rt.objects\n    containers = [\n        obj for obj in objs\n        if rt.getUserProp(obj, \"id\") == AVALON_CONTAINER_ID\n    ]\n\n    for container in sorted(containers, key=attrgetter(\"name\")):\n        yield lib.read(container)\n\n\ndef containerise(name: str, nodes: list, context,\n                 namespace=None, loader=None, suffix=\"_CON\"):\n    data = {\n        \"schema\": \"openpype:container-2.0\",\n        \"id\": AVALON_CONTAINER_ID,\n        \"name\": name,\n        \"namespace\": namespace or \"\",\n        \"loader\": loader,\n        \"representation\": context[\"representation\"][\"_id\"],\n    }\n    container_name = f\"{namespace}:{name}{suffix}\"\n    container = rt.container(name=container_name)\n    import_custom_attribute_data(container, nodes)\n    if not lib.imprint(container_name, data):\n        print(f\"imprinting of {container_name} failed.\")\n    return container\n\n\ndef load_custom_attribute_data():\n    \"\"\"Re-loading the AYON custom parameter built by the creator\n\n    Returns:\n        attribute: re-loading the custom OP attributes set in Maxscript\n    \"\"\"\n    return rt.Execute(MS_CUSTOM_ATTRIB)\n\n\ndef import_custom_attribute_data(container: str, selections: list):\n    \"\"\"Importing the Openpype/AYON custom parameter built by the creator\n\n    Args:\n        container (str): target container which adds custom attributes\n        selections (list): nodes to be added into\n        group in custom attributes\n    \"\"\"\n    attrs = load_custom_attribute_data()\n    modifier = rt.EmptyModifier()\n    rt.addModifier(container, modifier)\n    container.modifiers[0].name = \"OP Data\"\n    rt.custAttributes.add(container.modifiers[0], attrs)\n    node_list = []\n    sel_list = []\n    for i in selections:\n        node_ref = rt.NodeTransformMonitor(node=i)\n        node_list.append(node_ref)\n        sel_list.append(str(i))\n\n    # Setting the property\n    rt.setProperty(\n        container.modifiers[0].openPypeData,\n        \"all_handles\", node_list)\n    rt.setProperty(\n        container.modifiers[0].openPypeData,\n        \"sel_list\", sel_list)\n\n\ndef update_custom_attribute_data(container: str, selections: list):\n    \"\"\"Updating the AYON custom parameter built by the creator\n\n    Args:\n        container (str): target container which adds custom attributes\n        selections (list): nodes to be added into\n        group in custom attributes\n    \"\"\"\n    if container.modifiers[0].name == \"OP Data\":\n        rt.deleteModifier(container, container.modifiers[0])\n    import_custom_attribute_data(container, selections)\n\n\ndef get_previous_loaded_object(container: str):\n    \"\"\"Get previous loaded_object through the OP data\n\n    Args:\n        container (str): the container which stores the OP data\n\n    Returns:\n        node_list(list): list of nodes which are previously loaded\n    \"\"\"\n    node_list = []\n    sel_list = rt.getProperty(container.modifiers[0].openPypeData, \"sel_list\")\n    for obj in rt.Objects:\n        if str(obj) in sel_list:\n            node_list.append(obj)\n    return node_list\n"
  },
  {
    "path": "openpype/hosts/max/api/plugin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"3dsmax specific Avalon/Pyblish plugin definitions.\"\"\"\nfrom abc import ABCMeta\n\nimport six\nfrom pymxs import runtime as rt\n\nfrom openpype.lib import BoolDef\nfrom openpype.pipeline import CreatedInstance, Creator, CreatorError\n\nfrom .lib import imprint, lsattr, read\n\nMS_CUSTOM_ATTRIB = \"\"\"attributes \"openPypeData\"\n(\n    parameters main rollout:OPparams\n    (\n        all_handles type:#maxObjectTab tabSize:0 tabSizeVariable:on\n        sel_list type:#stringTab tabSize:0 tabSizeVariable:on\n    )\n\n    rollout OPparams \"OP Parameters\"\n    (\n        listbox list_node \"Node References\" items:#()\n        button button_add \"Add to Container\"\n        button button_del \"Delete from Container\"\n\n        fn node_to_name the_node =\n        (\n            handle = the_node.handle\n            obj_name = the_node.name\n            handle_name = obj_name + \"<\" + handle as string + \">\"\n            return handle_name\n        )\n        fn nodes_to_add node =\n        (\n            sceneObjs = #()\n            if classOf node == Container do return false\n            n = node as string\n            for obj in Objects do\n            (\n                tmp_obj = obj as string\n                append sceneObjs tmp_obj\n            )\n            if sel_list != undefined do\n            (\n                for obj in sel_list do\n                (\n                    idx = findItem sceneObjs obj\n                    if idx do\n                    (\n                        deleteItem sceneObjs idx\n                    )\n                )\n            )\n            idx = findItem sceneObjs n\n            if idx then return true else false\n        )\n\n        fn nodes_to_rmv node =\n        (\n            n = node as string\n            idx = findItem sel_list n\n            if idx then return true else false\n        )\n\n        on button_add pressed do\n        (\n            current_sel = selectByName title:\"Select Objects to add to\n            the Container\" buttontext:\"Add\" filter:nodes_to_add\n            if current_sel == undefined then return False\n            temp_arr = #()\n            i_node_arr = #()\n            for c in current_sel do\n            (\n                handle_name = node_to_name c\n                node_ref = NodeTransformMonitor node:c\n                idx = finditem list_node.items handle_name\n                if idx do (\n                    continue\n                )\n                name = c as string\n                append temp_arr handle_name\n                append i_node_arr node_ref\n                append sel_list name\n            )\n            all_handles = join i_node_arr all_handles\n            list_node.items = join temp_arr list_node.items\n        )\n\n        on button_del pressed do\n        (\n            current_sel = selectByName title:\"Select Objects to remove\n            from the Container\" buttontext:\"Remove\" filter: nodes_to_rmv\n            if current_sel == undefined or current_sel.count == 0 then\n            (\n                return False\n            )\n            temp_arr = #()\n            i_node_arr = #()\n            new_i_node_arr = #()\n            new_temp_arr = #()\n\n            for c in current_sel do\n            (\n                node_ref = NodeTransformMonitor node:c as string\n                handle_name = node_to_name c\n                n = c as string\n                tmp_all_handles = #()\n                for i in all_handles do\n                (\n                    tmp = i as string\n                    append tmp_all_handles tmp\n                )\n                idx = finditem tmp_all_handles node_ref\n                if idx do\n                (\n                    new_i_node_arr = DeleteItem all_handles idx\n\n                )\n                idx = finditem list_node.items handle_name\n                if idx do\n                (\n                    new_temp_arr = DeleteItem list_node.items idx\n                )\n                idx = finditem sel_list n\n                if idx do\n                (\n                    sel_list = DeleteItem sel_list idx\n                )\n            )\n            all_handles = join i_node_arr new_i_node_arr\n            list_node.items = join temp_arr new_temp_arr\n        )\n\n        on OPparams open do\n        (\n            if all_handles.count != 0 then\n            (\n                temp_arr = #()\n                for x in all_handles do\n                (\n                    if x.node == undefined do continue\n                    handle_name = node_to_name x.node\n                    append temp_arr handle_name\n                )\n                list_node.items = temp_arr\n            )\n        )\n    )\n)\"\"\"\n\n\nclass OpenPypeCreatorError(CreatorError):\n    pass\n\n\nclass MaxCreatorBase(object):\n\n    @staticmethod\n    def cache_subsets(shared_data):\n        if shared_data.get(\"max_cached_subsets\") is not None:\n            return shared_data\n\n        shared_data[\"max_cached_subsets\"] = {}\n        cached_instances = lsattr(\"id\", \"pyblish.avalon.instance\")\n        for i in cached_instances:\n            creator_id = rt.GetUserProp(i, \"creator_identifier\")\n            if creator_id not in shared_data[\"max_cached_subsets\"]:\n                shared_data[\"max_cached_subsets\"][creator_id] = [i.name]\n            else:\n                shared_data[\n                    \"max_cached_subsets\"][creator_id].append(i.name)\n        return shared_data\n\n    @staticmethod\n    def create_instance_node(node):\n        \"\"\"Create instance node.\n\n        If the supplied node is existing node, it will be used to hold the\n        instance, otherwise new node of type Dummy will be created.\n\n        Args:\n            node (rt.MXSWrapperBase, str): Node or node name to use.\n\n        Returns:\n            instance\n        \"\"\"\n        if isinstance(node, str):\n            node = rt.Container(name=node)\n\n        attrs = rt.Execute(MS_CUSTOM_ATTRIB)\n        modifier = rt.EmptyModifier()\n        rt.addModifier(node, modifier)\n        node.modifiers[0].name = \"OP Data\"\n        rt.custAttributes.add(node.modifiers[0], attrs)\n\n        return node\n\n\n@six.add_metaclass(ABCMeta)\nclass MaxCreator(Creator, MaxCreatorBase):\n    selected_nodes = []\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        if pre_create_data.get(\"use_selection\"):\n            self.selected_nodes = rt.GetCurrentSelection()\n        if rt.getNodeByName(subset_name):\n            raise CreatorError(f\"'{subset_name}' is already created..\")\n\n        instance_node = self.create_instance_node(subset_name)\n        instance_data[\"instance_node\"] = instance_node.name\n        instance = CreatedInstance(\n            self.family,\n            subset_name,\n            instance_data,\n            self\n        )\n        if pre_create_data.get(\"use_selection\"):\n\n            node_list = []\n            sel_list = []\n            for i in self.selected_nodes:\n                node_ref = rt.NodeTransformMonitor(node=i)\n                node_list.append(node_ref)\n                sel_list.append(str(i))\n\n            # Setting the property\n            rt.setProperty(\n                instance_node.modifiers[0].openPypeData,\n                \"all_handles\", node_list)\n            rt.setProperty(\n                instance_node.modifiers[0].openPypeData,\n                \"sel_list\", sel_list)\n\n        self._add_instance_to_context(instance)\n        imprint(instance_node.name, instance.data_to_store())\n\n        return instance\n\n    def collect_instances(self):\n        self.cache_subsets(self.collection_shared_data)\n        for instance in self.collection_shared_data[\"max_cached_subsets\"].get(self.identifier, []):  # noqa\n            created_instance = CreatedInstance.from_existing(\n                read(rt.GetNodeByName(instance)), self\n            )\n            self._add_instance_to_context(created_instance)\n\n    def update_instances(self, update_list):\n        for created_inst, changes in update_list:\n            instance_node = created_inst.get(\"instance_node\")\n            new_values = {\n                key: changes[key].new_value\n                for key in changes.changed_keys\n            }\n            subset = new_values.get(\"subset\", \"\")\n            if subset and instance_node != subset:\n                node = rt.getNodeByName(instance_node)\n                new_subset_name = new_values[\"subset\"]\n                if rt.getNodeByName(new_subset_name):\n                    raise CreatorError(\n                        \"The subset '{}' already exists.\".format(\n                            new_subset_name))\n                instance_node = new_subset_name\n                created_inst[\"instance_node\"] = instance_node\n                node.name = instance_node\n\n            imprint(\n                instance_node,\n                created_inst.data_to_store(),\n            )\n\n    def remove_instances(self, instances):\n        \"\"\"Remove specified instance from the scene.\n\n        This is only removing `id` parameter so instance is no longer\n        instance, because it might contain valuable data for artist.\n\n        \"\"\"\n        for instance in instances:\n            instance_node = rt.GetNodeByName(\n                instance.data.get(\"instance_node\"))\n            if instance_node:\n                count = rt.custAttributes.count(instance_node.modifiers[0])\n                rt.custAttributes.delete(instance_node.modifiers[0], count)\n                rt.Delete(instance_node)\n\n            self._remove_instance_from_context(instance)\n\n    def get_pre_create_attr_defs(self):\n        return [\n            BoolDef(\"use_selection\", label=\"Use selection\")\n        ]\n"
  },
  {
    "path": "openpype/hosts/max/api/preview_animation.py",
    "content": "import logging\nimport contextlib\nfrom pymxs import runtime as rt\nfrom .lib import get_max_version, render_resolution\n\nlog = logging.getLogger(\"openpype.hosts.max\")\n\n\n@contextlib.contextmanager\ndef play_preview_when_done(has_autoplay):\n    \"\"\"Set preview playback option during context\n\n    Args:\n        has_autoplay (bool): autoplay during creating\n            preview animation\n    \"\"\"\n    current_playback = rt.preferences.playPreviewWhenDone\n    try:\n        rt.preferences.playPreviewWhenDone = has_autoplay\n        yield\n    finally:\n        rt.preferences.playPreviewWhenDone = current_playback\n\n\n@contextlib.contextmanager\ndef viewport_layout_and_camera(camera, layout=\"layout_1\"):\n    \"\"\"Set viewport layout and camera during context\n    ***For 3dsMax 2024+\n    Args:\n        camera (str): viewport camera\n        layout (str): layout to use in viewport, defaults to `layout_1`\n            Use None to not change viewport layout during context.\n    \"\"\"\n    original_camera = rt.viewport.getCamera()\n    original_layout = rt.viewport.getLayout()\n    if not original_camera:\n        # if there is no original camera\n        # use the current camera as original\n        original_camera = rt.getNodeByName(camera)\n    review_camera = rt.getNodeByName(camera)\n    try:\n        if layout is not None:\n            layout = rt.Name(layout)\n            if rt.viewport.getLayout() != layout:\n                rt.viewport.setLayout(layout)\n        rt.viewport.setCamera(review_camera)\n        yield\n    finally:\n        rt.viewport.setLayout(original_layout)\n        rt.viewport.setCamera(original_camera)\n\n\n@contextlib.contextmanager\ndef viewport_preference_setting(general_viewport,\n                                nitrous_manager,\n                                nitrous_viewport,\n                                vp_button_mgr):\n    \"\"\"Function to set viewport setting during context\n    ***For Max Version < 2024\n    Args:\n        camera (str): Viewport camera for review render\n        general_viewport (dict): General viewport setting\n        nitrous_manager (dict): Nitrous graphic manager\n        nitrous_viewport (dict): Nitrous setting for\n            preview animation\n        vp_button_mgr (dict): Viewport button manager Setting\n        preview_preferences (dict): Preview Preferences Setting\n    \"\"\"\n    orig_vp_grid = rt.viewport.getGridVisibility(1)\n    orig_vp_bkg = rt.viewport.IsSolidBackgroundColorMode()\n\n    nitrousGraphicMgr = rt.NitrousGraphicsManager\n    viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting()\n    vp_button_mgr_original = {\n        key: getattr(rt.ViewportButtonMgr, key) for key in vp_button_mgr\n    }\n    nitrous_manager_original = {\n        key: getattr(nitrousGraphicMgr, key) for key in nitrous_manager\n    }\n    nitrous_viewport_original = {\n        key: getattr(viewport_setting, key) for key in nitrous_viewport\n    }\n\n    try:\n        rt.viewport.setGridVisibility(1, general_viewport[\"dspGrid\"])\n        rt.viewport.EnableSolidBackgroundColorMode(general_viewport[\"dspBkg\"])\n        for key, value in vp_button_mgr.items():\n            setattr(rt.ViewportButtonMgr, key, value)\n        for key, value in nitrous_manager.items():\n            setattr(nitrousGraphicMgr, key, value)\n        for key, value in nitrous_viewport.items():\n            if nitrous_viewport[key] != nitrous_viewport_original[key]:\n                setattr(viewport_setting, key, value)\n        yield\n\n    finally:\n        rt.viewport.setGridVisibility(1, orig_vp_grid)\n        rt.viewport.EnableSolidBackgroundColorMode(orig_vp_bkg)\n        for key, value in vp_button_mgr_original.items():\n            setattr(rt.ViewportButtonMgr, key, value)\n        for key, value in nitrous_manager_original.items():\n            setattr(nitrousGraphicMgr, key, value)\n        for key, value in nitrous_viewport_original.items():\n            setattr(viewport_setting, key, value)\n\n\ndef _render_preview_animation_max_2024(\n        filepath, start, end, percentSize, ext, viewport_options):\n    \"\"\"Render viewport preview with MaxScript using `CreateAnimation`.\n    ****For 3dsMax 2024+\n    Args:\n        filepath (str): filepath for render output without frame number and\n            extension, for example: /path/to/file\n        start (int): startFrame\n        end (int): endFrame\n        percentSize (float): render resolution multiplier by 100\n            e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x\n        viewport_options (dict): viewport setting options, e.g.\n            {\"vpStyle\": \"defaultshading\", \"vpPreset\": \"highquality\"}\n    Returns:\n        list: Created files\n    \"\"\"\n    # the percentSize argument must be integer\n    percent = int(percentSize)\n    filepath = filepath.replace(\"\\\\\", \"/\")\n    preview_output = f\"{filepath}..{ext}\"\n    frame_template = f\"{filepath}.{{:04d}}.{ext}\"\n    job_args = []\n    for key, value in viewport_options.items():\n        if isinstance(value, bool):\n            if value:\n                job_args.append(f\"{key}:{value}\")\n        elif isinstance(value, str):\n            if key == \"vpStyle\":\n                if value == \"Realistic\":\n                    value = \"defaultshading\"\n                elif value == \"Shaded\":\n                    log.warning(\n                        \"'Shaded' Mode not supported in \"\n                        \"preview animation in Max 2024.\\n\"\n                        \"Using 'defaultshading' instead.\")\n                    value = \"defaultshading\"\n                elif value == \"ConsistentColors\":\n                    value = \"flatcolor\"\n                else:\n                    value = value.lower()\n            elif key == \"vpPreset\":\n                if value == \"Quality\":\n                    value = \"highquality\"\n                elif value == \"Customize\":\n                    value = \"userdefined\"\n                else:\n                    value = value.lower()\n            job_args.append(f\"{key}: #{value}\")\n\n    job_str = (\n        f'CreatePreview filename:\"{preview_output}\" outputAVI:false '\n        f\"percentSize:{percent} start:{start} end:{end} \"\n        f\"{' '.join(job_args)} \"\n        \"autoPlay:false\"\n    )\n    rt.completeRedraw()\n    rt.execute(job_str)\n    # Return the created files\n    return [frame_template.format(frame) for frame in range(start, end + 1)]\n\n\ndef _render_preview_animation_max_pre_2024(\n        filepath, startFrame, endFrame,\n        width, height, percentSize, ext):\n    \"\"\"Render viewport animation by creating bitmaps\n    ***For 3dsMax Version <2024\n    Args:\n        filepath (str): filepath without frame numbers and extension\n        startFrame (int): start frame\n        endFrame (int): end frame\n        width (int): render resolution width\n        height (int): render resolution height\n        percentSize (float): render resolution multiplier by 100\n            e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x\n        ext (str): image extension\n    Returns:\n        list: Created filepaths\n    \"\"\"\n\n    # get the screenshot\n    percent = percentSize / 100.0\n    res_width = width * percent\n    res_height = height * percent\n    frame_template = \"{}.{{:04}}.{}\".format(filepath, ext)\n    frame_template.replace(\"\\\\\", \"/\")\n    files = []\n    user_cancelled = False\n    for frame in range(startFrame, endFrame + 1):\n        rt.sliderTime = frame\n        filepath = frame_template.format(frame)\n        preview_res = rt.bitmap(\n            res_width, res_height, filename=filepath\n        )\n        dib = rt.gw.getViewportDib()\n        dib_width = float(dib.width)\n        dib_height = float(dib.height)\n        # aspect ratio\n        viewportRatio = dib_width / dib_height\n        renderRatio = float(res_width / res_height)\n        if viewportRatio < renderRatio:\n            heightCrop = (dib_width / renderRatio)\n            topEdge = int((dib_height - heightCrop) / 2.0)\n            tempImage_bmp = rt.bitmap(dib_width, heightCrop)\n            src_box_value = rt.Box2(0, topEdge, dib_width, heightCrop)\n            rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0))\n            rt.copy(tempImage_bmp, preview_res)\n            rt.close(tempImage_bmp)\n        elif viewportRatio > renderRatio:\n            widthCrop = dib_height * renderRatio\n            leftEdge = int((dib_width - widthCrop) / 2.0)\n            tempImage_bmp = rt.bitmap(widthCrop, dib_height)\n            src_box_value = rt.Box2(leftEdge, 0, widthCrop, dib_height)\n            rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0))\n            rt.copy(tempImage_bmp, preview_res)\n            rt.close(tempImage_bmp)\n        else:\n            rt.copy(dib, preview_res)\n        rt.save(preview_res)\n        rt.close(preview_res)\n        rt.close(dib)\n        files.append(filepath)\n        if rt.keyboard.escPressed:\n            user_cancelled = True\n            break\n    # clean up the cache\n    rt.gc(delayed=True)\n    if user_cancelled:\n        raise RuntimeError(\"User cancelled rendering of viewport animation.\")\n    return files\n\n\ndef render_preview_animation(\n        filepath,\n        ext,\n        camera,\n        start_frame=None,\n        end_frame=None,\n        percentSize=100.0,\n        width=1920,\n        height=1080,\n        viewport_options=None):\n    \"\"\"Render camera review animation\n    Args:\n        filepath (str): filepath to render to, without frame number and\n            extension\n        ext (str): output file extension\n        camera (str): viewport camera for preview render\n        start_frame (int): start frame\n        end_frame (int): end frame\n        percentSize (float): render resolution multiplier by 100\n            e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x\n        width (int): render resolution width\n        height (int): render resolution height\n        viewport_options (dict): viewport setting options\n    Returns:\n        list: Rendered output files\n    \"\"\"\n    if start_frame is None:\n        start_frame = int(rt.animationRange.start)\n    if end_frame is None:\n        end_frame = int(rt.animationRange.end)\n\n    if viewport_options is None:\n        viewport_options = viewport_options_for_preview_animation()\n    with play_preview_when_done(False):\n        with viewport_layout_and_camera(camera):\n            if int(get_max_version()) < 2024:\n                with viewport_preference_setting(\n                        viewport_options[\"general_viewport\"],\n                        viewport_options[\"nitrous_manager\"],\n                        viewport_options[\"nitrous_viewport\"],\n                        viewport_options[\"vp_btn_mgr\"]\n                ):\n                    return _render_preview_animation_max_pre_2024(\n                        filepath,\n                        start_frame,\n                        end_frame,\n                        width,\n                        height,\n                        percentSize,\n                        ext\n                    )\n            else:\n                with render_resolution(width, height):\n                    return _render_preview_animation_max_2024(\n                        filepath,\n                        start_frame,\n                        end_frame,\n                        percentSize,\n                        ext,\n                        viewport_options\n                    )\n\n\ndef viewport_options_for_preview_animation():\n    \"\"\"Get default viewport options for `render_preview_animation`.\n\n    Returns:\n        dict: viewport setting options\n    \"\"\"\n    # viewport_options should be the dictionary\n    if int(get_max_version()) < 2024:\n        return {\n            \"visualStyleMode\": \"defaultshading\",\n            \"viewportPreset\": \"highquality\",\n            \"vpTexture\": False,\n            \"dspGeometry\": True,\n            \"dspShapes\": False,\n            \"dspLights\": False,\n            \"dspCameras\": False,\n            \"dspHelpers\": False,\n            \"dspParticles\": True,\n            \"dspBones\": False,\n            \"dspBkg\": True,\n            \"dspGrid\": False,\n            \"dspSafeFrame\": False,\n            \"dspFrameNums\": False\n        }\n    else:\n        viewport_options = {}\n        viewport_options[\"general_viewport\"] = {\n            \"dspBkg\": True,\n            \"dspGrid\": False\n        }\n        viewport_options[\"nitrous_manager\"] = {\n            \"AntialiasingQuality\": \"None\"\n        }\n        viewport_options[\"nitrous_viewport\"] = {\n            \"VisualStyleMode\": \"defaultshading\",\n            \"ViewportPreset\": \"highquality\",\n            \"UseTextureEnabled\": False\n        }\n        viewport_options[\"vp_btn_mgr\"] = {\n            \"EnableButtons\": False}\n        return viewport_options\n"
  },
  {
    "path": "openpype/hosts/max/hooks/force_startup_script.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Pre-launch to force 3ds max startup script.\"\"\"\nimport os\nfrom openpype.hosts.max import MAX_HOST_DIR\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass ForceStartupScript(PreLaunchHook):\n    \"\"\"Inject OpenPype environment to 3ds max.\n\n    Note that this works in combination whit 3dsmax startup script that\n    is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH\n    environment.\n\n    Hook `GlobalHostDataHook` must be executed before this hook.\n    \"\"\"\n    app_groups = {\"3dsmax\", \"adsk_3dsmax\"}\n    order = 11\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        startup_args = [\n            \"-U\",\n            \"MAXScript\",\n            os.path.join(MAX_HOST_DIR, \"startup\", \"startup.ms\"),\n        ]\n        self.launch_context.launch_args.append(startup_args)\n"
  },
  {
    "path": "openpype/hosts/max/hooks/inject_python.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Pre-launch hook to inject python environment.\"\"\"\nimport os\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass InjectPythonPath(PreLaunchHook):\n    \"\"\"Inject OpenPype environment to 3dsmax.\n\n    Note that this works in combination whit 3dsmax startup script that\n    is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH\n    environment.\n\n    Hook `GlobalHostDataHook` must be executed before this hook.\n    \"\"\"\n    app_groups = {\"3dsmax\", \"adsk_3dsmax\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        self.launch_context.env[\"MAX_PYTHONPATH\"] = os.environ[\"PYTHONPATH\"]\n"
  },
  {
    "path": "openpype/hosts/max/hooks/set_paths.py",
    "content": "from openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass SetPath(PreLaunchHook):\n    \"\"\"Set current dir to workdir.\n\n    Hook `GlobalHostDataHook` must be executed before this hook.\n    \"\"\"\n    app_groups = {\"max\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        workdir = self.launch_context.env.get(\"AVALON_WORKDIR\", \"\")\n        if not workdir:\n            self.log.warning(\"BUG: Workdir is not filled.\")\n            return\n\n        self.launch_context.kwargs[\"cwd\"] = workdir\n"
  },
  {
    "path": "openpype/hosts/max/plugins/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/max/plugins/create/create_camera.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating camera.\"\"\"\nfrom openpype.hosts.max.api import plugin\n\n\nclass CreateCamera(plugin.MaxCreator):\n    \"\"\"Creator plugin for Camera.\"\"\"\n    identifier = \"io.openpype.creators.max.camera\"\n    label = \"Camera\"\n    family = \"camera\"\n    icon = \"gear\"\n"
  },
  {
    "path": "openpype/hosts/max/plugins/create/create_maxScene.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating raw max scene.\"\"\"\nfrom openpype.hosts.max.api import plugin\n\n\nclass CreateMaxScene(plugin.MaxCreator):\n    \"\"\"Creator plugin for 3ds max scenes.\"\"\"\n    identifier = \"io.openpype.creators.max.maxScene\"\n    label = \"Max Scene\"\n    family = \"maxScene\"\n    icon = \"gear\"\n"
  },
  {
    "path": "openpype/hosts/max/plugins/create/create_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for model.\"\"\"\nfrom openpype.hosts.max.api import plugin\n\n\nclass CreateModel(plugin.MaxCreator):\n    \"\"\"Creator plugin for Model.\"\"\"\n    identifier = \"io.openpype.creators.max.model\"\n    label = \"Model\"\n    family = \"model\"\n    icon = \"gear\"\n"
  },
  {
    "path": "openpype/hosts/max/plugins/create/create_pointcache.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating pointcache alembics.\"\"\"\nfrom openpype.hosts.max.api import plugin\n\n\nclass CreatePointCache(plugin.MaxCreator):\n    \"\"\"Creator plugin for Point caches.\"\"\"\n    identifier = \"io.openpype.creators.max.pointcache\"\n    label = \"Point Cache\"\n    family = \"pointcache\"\n    icon = \"gear\"\n"
  },
  {
    "path": "openpype/hosts/max/plugins/create/create_pointcloud.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating point cloud.\"\"\"\nfrom openpype.hosts.max.api import plugin\n\n\nclass CreatePointCloud(plugin.MaxCreator):\n    \"\"\"Creator plugin for Point Clouds.\"\"\"\n    identifier = \"io.openpype.creators.max.pointcloud\"\n    label = \"Point Cloud\"\n    family = \"pointcloud\"\n    icon = \"gear\"\n"
  },
  {
    "path": "openpype/hosts/max/plugins/create/create_redshift_proxy.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating camera.\"\"\"\nfrom openpype.hosts.max.api import plugin\nfrom openpype.pipeline import CreatedInstance\n\n\nclass CreateRedshiftProxy(plugin.MaxCreator):\n    identifier = \"io.openpype.creators.max.redshiftproxy\"\n    label = \"Redshift Proxy\"\n    family = \"redshiftproxy\"\n    icon = \"gear\"\n"
  },
  {
    "path": "openpype/hosts/max/plugins/create/create_render.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating camera.\"\"\"\nimport os\nfrom openpype.hosts.max.api import plugin\nfrom openpype.lib import BoolDef\nfrom openpype.hosts.max.api.lib_rendersettings import RenderSettings\n\n\nclass CreateRender(plugin.MaxCreator):\n    \"\"\"Creator plugin for Renders.\"\"\"\n    identifier = \"io.openpype.creators.max.render\"\n    label = \"Render\"\n    family = \"maxrender\"\n    icon = \"gear\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        from pymxs import runtime as rt\n        file = rt.maxFileName\n        filename, _ = os.path.splitext(file)\n        instance_data[\"AssetName\"] = filename\n        instance_data[\"multiCamera\"] = pre_create_data.get(\"multi_cam\")\n        num_of_renderlayer = rt.batchRenderMgr.numViews\n        if num_of_renderlayer > 0:\n            rt.batchRenderMgr.DeleteView(num_of_renderlayer)\n\n        instance = super(CreateRender, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)\n\n        container_name = instance.data.get(\"instance_node\")\n        # set output paths for rendering(mandatory for deadline)\n        RenderSettings().render_output(container_name)\n        # TODO: create multiple camera options\n        if self.selected_nodes:\n            selected_nodes_name = []\n            for sel in self.selected_nodes:\n                name = sel.name\n                selected_nodes_name.append(name)\n            RenderSettings().batch_render_layer(\n                container_name, filename,\n                selected_nodes_name)\n\n    def get_pre_create_attr_defs(self):\n        attrs = super(CreateRender, self).get_pre_create_attr_defs()\n        return attrs + [\n            BoolDef(\"multi_cam\",\n                    label=\"Multiple Cameras Submission\",\n                    default=False),\n        ]\n"
  },
  {
    "path": "openpype/hosts/max/plugins/create/create_review.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating review in Max.\"\"\"\nfrom openpype.hosts.max.api import plugin\nfrom openpype.lib import BoolDef, EnumDef, NumberDef\n\n\nclass CreateReview(plugin.MaxCreator):\n    \"\"\"Review in 3dsMax\"\"\"\n\n    identifier = \"io.openpype.creators.max.review\"\n    label = \"Review\"\n    family = \"review\"\n    icon = \"video-camera\"\n\n    review_width = 1920\n    review_height = 1080\n    percentSize = 100\n    keep_images = False\n    image_format = \"png\"\n    visual_style = \"Realistic\"\n    viewport_preset = \"Quality\"\n    vp_texture = True\n    anti_aliasing = \"None\"\n\n    def apply_settings(self, project_settings):\n        settings = project_settings[\"max\"][\"CreateReview\"]  # noqa\n\n        # Take some defaults from settings\n        self.review_width = settings.get(\"review_width\", self.review_width)\n        self.review_height = settings.get(\"review_height\", self.review_height)\n        self.percentSize = settings.get(\"percentSize\", self.percentSize)\n        self.keep_images = settings.get(\"keep_images\", self.keep_images)\n        self.image_format = settings.get(\"image_format\", self.image_format)\n        self.visual_style = settings.get(\"visual_style\", self.visual_style)\n        self.viewport_preset = settings.get(\n            \"viewport_preset\", self.viewport_preset)\n        self.anti_aliasing = settings.get(\n            \"anti_aliasing\", self.anti_aliasing)\n        self.vp_texture = settings.get(\"vp_texture\", self.vp_texture)\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # Transfer settings from pre create to instance\n        creator_attributes = instance_data.setdefault(\n            \"creator_attributes\", dict())\n        for key in [\"imageFormat\",\n                    \"keepImages\",\n                    \"review_width\",\n                    \"review_height\",\n                    \"percentSize\",\n                    \"visualStyleMode\",\n                    \"viewportPreset\",\n                    \"antialiasingQuality\",\n                    \"vpTexture\"]:\n            if key in pre_create_data:\n                creator_attributes[key] = pre_create_data[key]\n\n        super(CreateReview, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)\n\n    def get_instance_attr_defs(self):\n        image_format_enum = [\"exr\", \"jpg\", \"png\", \"tga\"]\n\n        visual_style_preset_enum = [\n            \"Realistic\", \"Shaded\", \"Facets\",\n            \"ConsistentColors\", \"HiddenLine\",\n            \"Wireframe\", \"BoundingBox\", \"Ink\",\n            \"ColorInk\", \"Acrylic\", \"Tech\", \"Graphite\",\n            \"ColorPencil\", \"Pastel\", \"Clay\", \"ModelAssist\"\n        ]\n        preview_preset_enum = [\n            \"Quality\", \"Standard\", \"Performance\",\n            \"DXMode\", \"Customize\"]\n        anti_aliasing_enum = [\"None\", \"2X\", \"4X\", \"8X\"]\n\n        return [\n            NumberDef(\"review_width\",\n                      label=\"Review width\",\n                      decimals=0,\n                      minimum=0,\n                      default=self.review_width),\n            NumberDef(\"review_height\",\n                      label=\"Review height\",\n                      decimals=0,\n                      minimum=0,\n                      default=self.review_height),\n            NumberDef(\"percentSize\",\n                      label=\"Percent of Output\",\n                      default=self.percentSize,\n                      minimum=1,\n                      decimals=0),\n            BoolDef(\"keepImages\",\n                    label=\"Keep Image Sequences\",\n                    default=self.keep_images),\n            EnumDef(\"imageFormat\",\n                    image_format_enum,\n                    default=self.image_format,\n                    label=\"Image Format Options\"),\n            EnumDef(\"visualStyleMode\",\n                    visual_style_preset_enum,\n                    default=self.visual_style,\n                    label=\"Preference\"),\n            EnumDef(\"viewportPreset\",\n                    preview_preset_enum,\n                    default=self.viewport_preset,\n                    label=\"Preview Preset\"),\n            EnumDef(\"antialiasingQuality\",\n                    anti_aliasing_enum,\n                    default=self.anti_aliasing,\n                    label=\"Anti-aliasing Quality\"),\n            BoolDef(\"vpTexture\",\n                    label=\"Viewport Texture\",\n                    default=self.vp_texture)\n        ]\n\n    def get_pre_create_attr_defs(self):\n        # Use same attributes as for instance attributes\n        attrs = super().get_pre_create_attr_defs()\n        return attrs + self.get_instance_attr_defs()\n"
  },
  {
    "path": "openpype/hosts/max/plugins/create/create_tycache.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating TyCache.\"\"\"\nfrom openpype.hosts.max.api import plugin\n\n\nclass CreateTyCache(plugin.MaxCreator):\n    \"\"\"Creator plugin for TyCache.\"\"\"\n    identifier = \"io.openpype.creators.max.tycache\"\n    label = \"TyCache\"\n    family = \"tycache\"\n    icon = \"gear\"\n"
  },
  {
    "path": "openpype/hosts/max/plugins/create/create_workfile.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating workfiles.\"\"\"\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import CreatedInstance, AutoCreator\nfrom openpype.client import get_asset_by_name, get_asset_name_identifier\nfrom openpype.hosts.max.api import plugin\nfrom openpype.hosts.max.api.lib import read, imprint\nfrom pymxs import runtime as rt\n\n\nclass CreateWorkfile(plugin.MaxCreatorBase, AutoCreator):\n    \"\"\"Workfile auto-creator.\"\"\"\n    identifier = \"io.openpype.creators.max.workfile\"\n    label = \"Workfile\"\n    family = \"workfile\"\n    icon = \"fa5.file\"\n\n    default_variant = \"Main\"\n\n    def create(self):\n        variant = self.default_variant\n        current_instance = next(\n            (\n                instance for instance in self.create_context.instances\n                if instance.creator_identifier == self.identifier\n            ), None)\n        project_name = self.project_name\n        asset_name = self.create_context.get_current_asset_name()\n        task_name = self.create_context.get_current_task_name()\n        host_name = self.create_context.host_name\n\n        if current_instance is None:\n            current_instance_asset = None\n        elif AYON_SERVER_ENABLED:\n            current_instance_asset = current_instance[\"folderPath\"]\n        else:\n            current_instance_asset = current_instance[\"asset\"]\n\n        if current_instance is None:\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                variant, task_name, asset_doc, project_name, host_name\n            )\n            data = {\n                \"task\": task_name,\n                \"variant\": variant\n            }\n            if AYON_SERVER_ENABLED:\n                data[\"folderPath\"] = asset_name\n            else:\n                data[\"asset\"] = asset_name\n\n            data.update(\n                self.get_dynamic_data(\n                    variant, task_name, asset_doc,\n                    project_name, host_name, current_instance)\n            )\n            self.log.info(\"Auto-creating workfile instance...\")\n            instance_node = self.create_node(subset_name)\n            data[\"instance_node\"] = instance_node.name\n            current_instance = CreatedInstance(\n                self.family, subset_name, data, self\n            )\n            self._add_instance_to_context(current_instance)\n            imprint(instance_node.name, current_instance.data)\n        elif (\n            current_instance_asset != asset_name\n            or current_instance[\"task\"] != task_name\n        ):\n            # Update instance context if is not the same\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                variant, task_name, asset_doc, project_name, host_name\n            )\n            asset_name = get_asset_name_identifier(asset_doc)\n\n            if AYON_SERVER_ENABLED:\n                current_instance[\"folderPath\"] = asset_name\n            else:\n                current_instance[\"asset\"] = asset_name\n            current_instance[\"task\"] = task_name\n            current_instance[\"subset\"] = subset_name\n\n    def collect_instances(self):\n        self.cache_subsets(self.collection_shared_data)\n        for instance in self.collection_shared_data[\"max_cached_subsets\"].get(self.identifier, []):  # noqa\n            if not rt.getNodeByName(instance):\n                continue\n            created_instance = CreatedInstance.from_existing(\n                read(rt.GetNodeByName(instance)), self\n            )\n            self._add_instance_to_context(created_instance)\n\n    def update_instances(self, update_list):\n        for created_inst, _ in update_list:\n            instance_node = created_inst.get(\"instance_node\")\n            imprint(\n                instance_node,\n                created_inst.data_to_store()\n            )\n\n    def remove_instances(self, instances):\n        \"\"\"Remove specified instance from the scene.\n\n        This is only removing `id` parameter so instance is no longer\n        instance, because it might contain valuable data for artist.\n\n        \"\"\"\n        for instance in instances:\n            instance_node = rt.GetNodeByName(\n                instance.data.get(\"instance_node\"))\n            if instance_node:\n                rt.Delete(instance_node)\n\n            self._remove_instance_from_context(instance)\n\n    def create_node(self, subset_name):\n        if rt.getNodeByName(subset_name):\n            node = rt.getNodeByName(subset_name)\n            return node\n        node = rt.Container(name=subset_name)\n        node.isHidden = True\n        return node\n"
  },
  {
    "path": "openpype/hosts/max/plugins/load/load_camera_fbx.py",
    "content": "import os\n\nfrom openpype.hosts.max.api import lib, maintained_selection\nfrom openpype.hosts.max.api.lib import (\n    unique_namespace,\n    get_namespace,\n    object_transform_set\n)\nfrom openpype.hosts.max.api.pipeline import (\n    containerise,\n    get_previous_loaded_object,\n    update_custom_attribute_data\n)\nfrom openpype.pipeline import get_representation_path, load\n\n\nclass FbxLoader(load.LoaderPlugin):\n    \"\"\"Fbx Loader.\"\"\"\n\n    families = [\"camera\"]\n    representations = [\"fbx\"]\n    order = -9\n    icon = \"code-fork\"\n    color = \"white\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        from pymxs import runtime as rt\n        filepath = self.filepath_from_context(context)\n        filepath = os.path.normpath(filepath)\n        rt.FBXImporterSetParam(\"Animation\", True)\n        rt.FBXImporterSetParam(\"Camera\", True)\n        rt.FBXImporterSetParam(\"AxisConversionMethod\", True)\n        rt.FBXImporterSetParam(\"Mode\", rt.Name(\"create\"))\n        rt.FBXImporterSetParam(\"Preserveinstances\", True)\n        rt.ImportFile(\n            filepath,\n            rt.name(\"noPrompt\"),\n            using=rt.FBXIMP)\n\n        namespace = unique_namespace(\n            name + \"_\",\n            suffix=\"_\",\n        )\n        selections = rt.GetCurrentSelection()\n\n        for selection in selections:\n            selection.name = f\"{namespace}:{selection.name}\"\n\n        return containerise(\n            name, selections, context,\n            namespace, loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        from pymxs import runtime as rt\n\n        path = get_representation_path(representation)\n        node_name = container[\"instance_node\"]\n        node = rt.getNodeByName(node_name)\n        namespace, _ = get_namespace(node_name)\n\n        node_list = get_previous_loaded_object(node)\n        rt.Select(node_list)\n        prev_fbx_objects = rt.GetCurrentSelection()\n        transform_data = object_transform_set(prev_fbx_objects)\n        for prev_fbx_obj in prev_fbx_objects:\n            if rt.isValidNode(prev_fbx_obj):\n                rt.Delete(prev_fbx_obj)\n\n        rt.FBXImporterSetParam(\"Animation\", True)\n        rt.FBXImporterSetParam(\"Camera\", True)\n        rt.FBXImporterSetParam(\"Mode\", rt.Name(\"merge\"))\n        rt.FBXImporterSetParam(\"AxisConversionMethod\", True)\n        rt.FBXImporterSetParam(\"Preserveinstances\", True)\n        rt.ImportFile(\n            path, rt.name(\"noPrompt\"), using=rt.FBXIMP)\n        current_fbx_objects = rt.GetCurrentSelection()\n        fbx_objects = []\n        for fbx_object in current_fbx_objects:\n            fbx_object.name = f\"{namespace}:{fbx_object.name}\"\n            fbx_objects.append(fbx_object)\n            fbx_transform = f\"{fbx_object.name}.transform\"\n            if fbx_transform in transform_data.keys():\n                fbx_object.pos = transform_data[fbx_transform] or 0\n                fbx_object.scale = transform_data[\n                    f\"{fbx_object.name}.scale\"] or 0\n\n        update_custom_attribute_data(node, fbx_objects)\n        lib.imprint(container[\"instance_node\"], {\n            \"representation\": str(representation[\"_id\"])\n        })\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        from pymxs import runtime as rt\n\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        rt.Delete(node)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/load/load_max_scene.py",
    "content": "import os\n\nfrom openpype.hosts.max.api import lib\nfrom openpype.hosts.max.api.lib import (\n    unique_namespace,\n    get_namespace,\n    object_transform_set\n)\nfrom openpype.hosts.max.api.pipeline import (\n    containerise, get_previous_loaded_object,\n    update_custom_attribute_data\n)\nfrom openpype.pipeline import get_representation_path, load\n\n\nclass MaxSceneLoader(load.LoaderPlugin):\n    \"\"\"Max Scene Loader.\"\"\"\n\n    families = [\"camera\",\n                \"maxScene\",\n                \"model\"]\n\n    representations = [\"max\"]\n    order = -8\n    icon = \"code-fork\"\n    color = \"green\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        from pymxs import runtime as rt\n        path = self.filepath_from_context(context)\n        path = os.path.normpath(path)\n        # import the max scene by using \"merge file\"\n        path = path.replace('\\\\', '/')\n        rt.MergeMaxFile(path, quiet=True, includeFullGroup=True)\n        max_objects = rt.getLastMergedNodes()\n        max_object_names = [obj.name for obj in max_objects]\n        # implement the OP/AYON custom attributes before load\n        max_container = []\n        namespace = unique_namespace(\n            name + \"_\",\n            suffix=\"_\",\n        )\n        for max_obj, obj_name in zip(max_objects, max_object_names):\n            max_obj.name = f\"{namespace}:{obj_name}\"\n            max_container.append(rt.getNodeByName(max_obj.name))\n        return containerise(\n            name, max_container, context,\n            namespace, loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        from pymxs import runtime as rt\n\n        path = get_representation_path(representation)\n        node_name = container[\"instance_node\"]\n        node = rt.getNodeByName(node_name)\n        namespace, _ = get_namespace(node_name)\n        # delete the old container with attribute\n        # delete old duplicate\n        # use the modifier OP data to delete the data\n        node_list = get_previous_loaded_object(node)\n        rt.select(node_list)\n        prev_max_objects = rt.GetCurrentSelection()\n        transform_data = object_transform_set(prev_max_objects)\n\n        for prev_max_obj in prev_max_objects:\n            if rt.isValidNode(prev_max_obj):  # noqa\n                rt.Delete(prev_max_obj)\n        rt.MergeMaxFile(path, quiet=True)\n\n        current_max_objects = rt.getLastMergedNodes()\n\n        current_max_object_names = [obj.name for obj\n                                    in current_max_objects]\n\n        max_objects = []\n        for max_obj, obj_name in zip(current_max_objects,\n                                     current_max_object_names):\n            max_obj.name = f\"{namespace}:{obj_name}\"\n            max_objects.append(max_obj)\n            max_transform = f\"{max_obj.name}.transform\"\n            if max_transform in transform_data.keys():\n                max_obj.pos = transform_data[max_transform] or 0\n                max_obj.scale = transform_data[\n                    f\"{max_obj.name}.scale\"] or 0\n\n        update_custom_attribute_data(node, max_objects)\n        lib.imprint(container[\"instance_node\"], {\n            \"representation\": str(representation[\"_id\"])\n        })\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        from pymxs import runtime as rt\n\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        rt.Delete(node)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/load/load_model.py",
    "content": "import os\nfrom openpype.pipeline import load, get_representation_path\nfrom openpype.hosts.max.api.pipeline import (\n    containerise,\n    get_previous_loaded_object\n)\nfrom openpype.hosts.max.api import lib\nfrom openpype.hosts.max.api.lib import (\n    maintained_selection, unique_namespace\n)\n\n\nclass ModelAbcLoader(load.LoaderPlugin):\n    \"\"\"Loading model with the Alembic loader.\"\"\"\n\n    families = [\"model\"]\n    label = \"Load Model with Alembic\"\n    representations = [\"abc\"]\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        from pymxs import runtime as rt\n\n        file_path = os.path.normpath(self.filepath_from_context(context))\n\n        abc_before = {\n            c\n            for c in rt.rootNode.Children\n            if rt.classOf(c) == rt.AlembicContainer\n        }\n\n        rt.AlembicImport.ImportToRoot = False\n        rt.AlembicImport.CustomAttributes = True\n        rt.AlembicImport.UVs = True\n        rt.AlembicImport.VertexColors = True\n        rt.importFile(file_path, rt.name(\"noPrompt\"), using=rt.AlembicImport)\n\n        abc_after = {\n            c\n            for c in rt.rootNode.Children\n            if rt.classOf(c) == rt.AlembicContainer\n        }\n\n        # This should yield new AlembicContainer node\n        abc_containers = abc_after.difference(abc_before)\n\n        if len(abc_containers) != 1:\n            self.log.error(\"Something failed when loading.\")\n\n        abc_container = abc_containers.pop()\n\n        namespace = unique_namespace(\n            name + \"_\",\n            suffix=\"_\",\n        )\n        abc_objects = []\n        for abc_object in abc_container.Children:\n            abc_object.name = f\"{namespace}:{abc_object.name}\"\n            abc_objects.append(abc_object)\n        # rename the abc container with namespace\n        abc_container_name = f\"{namespace}:{name}\"\n        abc_container.name = abc_container_name\n        abc_objects.append(abc_container)\n\n        return containerise(\n            name, abc_objects, context,\n            namespace, loader=self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n        from pymxs import runtime as rt\n\n        path = get_representation_path(representation)\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        node_list = [n for n in get_previous_loaded_object(node)\n                     if rt.ClassOf(n) == rt.AlembicContainer]\n        with maintained_selection():\n            rt.Select(node_list)\n\n            for alembic in rt.Selection:\n                abc = rt.GetNodeByName(alembic.name)\n                rt.Select(abc.Children)\n                for abc_con in abc.Children:\n                    abc_con.source = path\n                    rt.Select(abc_con.Children)\n                    for abc_obj in abc_con.Children:\n                        abc_obj.source = path\n        lib.imprint(\n            container[\"instance_node\"],\n            {\"representation\": str(representation[\"_id\"])},\n        )\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        from pymxs import runtime as rt\n\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        rt.Delete(node)\n\n    @staticmethod\n    def get_container_children(parent, type_name):\n        from pymxs import runtime as rt\n\n        def list_children(node):\n            children = []\n            for c in node.Children:\n                children.append(c)\n                children += list_children(c)\n            return children\n\n        filtered = []\n        for child in list_children(parent):\n            class_type = str(rt.ClassOf(child.baseObject))\n            if class_type == type_name:\n                filtered.append(child)\n\n        return filtered\n"
  },
  {
    "path": "openpype/hosts/max/plugins/load/load_model_fbx.py",
    "content": "import os\nfrom openpype.pipeline import load, get_representation_path\nfrom openpype.hosts.max.api.pipeline import (\n    containerise, get_previous_loaded_object,\n    update_custom_attribute_data\n)\nfrom openpype.hosts.max.api import lib\nfrom openpype.hosts.max.api.lib import (\n    unique_namespace,\n    get_namespace,\n    object_transform_set\n)\nfrom openpype.hosts.max.api.lib import maintained_selection\n\n\nclass FbxModelLoader(load.LoaderPlugin):\n    \"\"\"Fbx Model Loader.\"\"\"\n\n    families = [\"model\"]\n    representations = [\"fbx\"]\n    order = -9\n    icon = \"code-fork\"\n    color = \"white\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        from pymxs import runtime as rt\n        filepath = self.filepath_from_context(context)\n        filepath = os.path.normpath(filepath)\n        rt.FBXImporterSetParam(\"Animation\", False)\n        rt.FBXImporterSetParam(\"Cameras\", False)\n        rt.FBXImporterSetParam(\"Mode\", rt.Name(\"create\"))\n        rt.FBXImporterSetParam(\"Preserveinstances\", True)\n        rt.importFile(\n            filepath, rt.name(\"noPrompt\"), using=rt.FBXIMP)\n\n        namespace = unique_namespace(\n            name + \"_\",\n            suffix=\"_\",\n        )\n        selections = rt.GetCurrentSelection()\n\n        for selection in selections:\n            selection.name = f\"{namespace}:{selection.name}\"\n\n        return containerise(\n            name, selections, context,\n            namespace, loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        from pymxs import runtime as rt\n\n        path = get_representation_path(representation)\n        node_name = container[\"instance_node\"]\n        node = rt.getNodeByName(node_name)\n        if not node:\n            rt.Container(name=node_name)\n        namespace, _ = get_namespace(node_name)\n\n        node_list = get_previous_loaded_object(node)\n        rt.Select(node_list)\n        prev_fbx_objects = rt.GetCurrentSelection()\n        transform_data = object_transform_set(prev_fbx_objects)\n        for prev_fbx_obj in prev_fbx_objects:\n            if rt.isValidNode(prev_fbx_obj):\n                rt.Delete(prev_fbx_obj)\n\n        rt.FBXImporterSetParam(\"Animation\", False)\n        rt.FBXImporterSetParam(\"Cameras\", False)\n        rt.FBXImporterSetParam(\"Mode\", rt.Name(\"create\"))\n        rt.FBXImporterSetParam(\"Preserveinstances\", True)\n        rt.importFile(path, rt.name(\"noPrompt\"), using=rt.FBXIMP)\n        current_fbx_objects = rt.GetCurrentSelection()\n        fbx_objects = []\n        for fbx_object in current_fbx_objects:\n            fbx_object.name = f\"{namespace}:{fbx_object.name}\"\n            fbx_objects.append(fbx_object)\n            fbx_transform = f\"{fbx_object.name}.transform\"\n            if fbx_transform in transform_data.keys():\n                fbx_object.pos = transform_data[fbx_transform] or 0\n                fbx_object.scale = transform_data[\n                    f\"{fbx_object.name}.scale\"] or 0\n\n        with maintained_selection():\n            rt.Select(node)\n        update_custom_attribute_data(node, fbx_objects)\n        lib.imprint(container[\"instance_node\"], {\n            \"representation\": str(representation[\"_id\"])\n        })\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        from pymxs import runtime as rt\n\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        rt.Delete(node)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/load/load_model_obj.py",
    "content": "import os\n\nfrom openpype.hosts.max.api import lib\nfrom openpype.hosts.max.api.lib import (\n    unique_namespace,\n    get_namespace,\n    maintained_selection,\n    object_transform_set\n)\nfrom openpype.hosts.max.api.lib import maintained_selection\nfrom openpype.hosts.max.api.pipeline import (\n    containerise,\n    get_previous_loaded_object,\n    update_custom_attribute_data\n)\nfrom openpype.pipeline import get_representation_path, load\n\n\nclass ObjLoader(load.LoaderPlugin):\n    \"\"\"Obj Loader.\"\"\"\n\n    families = [\"model\"]\n    representations = [\"obj\"]\n    order = -9\n    icon = \"code-fork\"\n    color = \"white\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        from pymxs import runtime as rt\n\n        filepath = os.path.normpath(self.filepath_from_context(context))\n        self.log.debug(\"Executing command to import..\")\n\n        rt.Execute(f'importFile @\"{filepath}\" #noPrompt using:ObjImp')\n\n        namespace = unique_namespace(\n            name + \"_\",\n            suffix=\"_\",\n        )\n        # create \"missing\" container for obj import\n        selections = rt.GetCurrentSelection()\n        # get current selection\n        for selection in selections:\n            selection.name = f\"{namespace}:{selection.name}\"\n        return containerise(\n            name, selections, context,\n            namespace, loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        from pymxs import runtime as rt\n\n        path = get_representation_path(representation)\n        node_name = container[\"instance_node\"]\n        node = rt.getNodeByName(node_name)\n        namespace, _ = get_namespace(node_name)\n        node_list = get_previous_loaded_object(node)\n        rt.Select(node_list)\n        previous_objects = rt.GetCurrentSelection()\n        transform_data = object_transform_set(previous_objects)\n        for prev_obj in previous_objects:\n            if rt.isValidNode(prev_obj):\n                rt.Delete(prev_obj)\n\n        rt.Execute(f'importFile @\"{path}\" #noPrompt using:ObjImp')\n        # get current selection\n        selections = rt.GetCurrentSelection()\n        for selection in selections:\n            selection.name = f\"{namespace}:{selection.name}\"\n            selection_transform = f\"{selection.name}.transform\"\n            if selection_transform in transform_data.keys():\n                selection.pos = transform_data[selection_transform] or 0\n                selection.scale = transform_data[\n                    f\"{selection.name}.scale\"] or 0\n        update_custom_attribute_data(node, selections)\n        with maintained_selection():\n            rt.Select(node)\n\n        lib.imprint(node_name, {\n            \"representation\": str(representation[\"_id\"])\n        })\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        from pymxs import runtime as rt\n\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        rt.Delete(node)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/load/load_model_usd.py",
    "content": "import os\n\nfrom pymxs import runtime as rt\nfrom openpype.pipeline.load import LoadError\nfrom openpype.hosts.max.api import lib\nfrom openpype.hosts.max.api.lib import (\n    unique_namespace,\n    get_namespace,\n    object_transform_set,\n    get_plugins\n)\nfrom openpype.hosts.max.api.lib import maintained_selection\nfrom openpype.hosts.max.api.pipeline import (\n    containerise,\n    get_previous_loaded_object,\n    update_custom_attribute_data\n)\nfrom openpype.pipeline import get_representation_path, load\n\n\nclass ModelUSDLoader(load.LoaderPlugin):\n    \"\"\"Loading model with the USD loader.\"\"\"\n\n    families = [\"model\"]\n    label = \"Load Model(USD)\"\n    representations = [\"usda\"]\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        # asset_filepath\n        plugin_info = get_plugins()\n        if \"usdimport.dli\" not in plugin_info:\n            raise LoadError(\"No USDImporter loaded/installed in Max..\")\n        filepath = os.path.normpath(self.filepath_from_context(context))\n        import_options = rt.USDImporter.CreateOptions()\n        base_filename = os.path.basename(filepath)\n        _, ext = os.path.splitext(base_filename)\n        log_filepath = filepath.replace(ext, \"txt\")\n\n        rt.LogPath = log_filepath\n        rt.LogLevel = rt.Name(\"info\")\n        rt.USDImporter.importFile(filepath,\n                                  importOptions=import_options)\n        namespace = unique_namespace(\n            name + \"_\",\n            suffix=\"_\",\n        )\n        asset = rt.GetNodeByName(name)\n        usd_objects = []\n\n        for usd_asset in asset.Children:\n            usd_asset.name = f\"{namespace}:{usd_asset.name}\"\n            usd_objects.append(usd_asset)\n\n        asset_name = f\"{namespace}:{name}\"\n        asset.name = asset_name\n        # need to get the correct container after renamed\n        asset = rt.GetNodeByName(asset_name)\n        usd_objects.append(asset)\n\n        return containerise(\n            name, usd_objects, context,\n            namespace, loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        path = get_representation_path(representation)\n        node_name = container[\"instance_node\"]\n        node = rt.GetNodeByName(node_name)\n        namespace, name = get_namespace(node_name)\n        node_list = get_previous_loaded_object(node)\n        rt.Select(node_list)\n        prev_objects = [sel for sel in rt.GetCurrentSelection()\n                        if sel != rt.Container\n                        and sel.name != node_name]\n        transform_data = object_transform_set(prev_objects)\n        for n in prev_objects:\n            rt.Delete(n)\n\n        import_options = rt.USDImporter.CreateOptions()\n        base_filename = os.path.basename(path)\n        _, ext = os.path.splitext(base_filename)\n        log_filepath = path.replace(ext, \"txt\")\n\n        rt.LogPath = log_filepath\n        rt.LogLevel = rt.Name(\"info\")\n        rt.USDImporter.importFile(\n            path, importOptions=import_options)\n\n        asset = rt.GetNodeByName(name)\n        usd_objects = []\n        for children in asset.Children:\n            children.name = f\"{namespace}:{children.name}\"\n            usd_objects.append(children)\n            children_transform = f\"{children.name}.transform\"\n            if children_transform in transform_data.keys():\n                children.pos = transform_data[children_transform] or 0\n                children.scale = transform_data[\n                    f\"{children.name}.scale\"] or 0\n\n        asset.name = f\"{namespace}:{asset.name}\"\n        usd_objects.append(asset)\n        update_custom_attribute_data(node, usd_objects)\n        with maintained_selection():\n            rt.Select(node)\n\n        lib.imprint(node_name, {\n            \"representation\": str(representation[\"_id\"])\n        })\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        rt.Delete(node)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/load/load_pointcache.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Simple alembic loader for 3dsmax.\n\nBecause of limited api, alembics can be only loaded, but not easily updated.\n\n\"\"\"\nimport os\nfrom openpype.pipeline import load, get_representation_path\nfrom openpype.hosts.max.api import lib, maintained_selection\nfrom openpype.hosts.max.api.lib import unique_namespace\nfrom openpype.hosts.max.api.pipeline import (\n    containerise,\n    get_previous_loaded_object\n)\n\n\nclass AbcLoader(load.LoaderPlugin):\n    \"\"\"Alembic loader.\"\"\"\n\n    families = [\"camera\", \"animation\", \"pointcache\"]\n    label = \"Load Alembic\"\n    representations = [\"abc\"]\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        from pymxs import runtime as rt\n\n        file_path = self.filepath_from_context(context)\n        file_path = os.path.normpath(file_path)\n\n        abc_before = {\n            c\n            for c in rt.rootNode.Children\n            if rt.classOf(c) == rt.AlembicContainer\n        }\n\n        rt.AlembicImport.ImportToRoot = False\n        rt.importFile(file_path, rt.name(\"noPrompt\"), using=rt.AlembicImport)\n\n        abc_after = {\n            c\n            for c in rt.rootNode.Children\n            if rt.classOf(c) == rt.AlembicContainer\n        }\n\n        # This should yield new AlembicContainer node\n        abc_containers = abc_after.difference(abc_before)\n\n        if len(abc_containers) != 1:\n            self.log.error(\"Something failed when loading.\")\n\n        abc_container = abc_containers.pop()\n        selections = rt.GetCurrentSelection()\n        for abc in selections:\n            for cam_shape in abc.Children:\n                cam_shape.playbackType = 0\n\n        namespace = unique_namespace(\n            name + \"_\",\n            suffix=\"_\",\n        )\n        abc_objects = []\n        for abc_object in abc_container.Children:\n            abc_object.name = f\"{namespace}:{abc_object.name}\"\n            abc_objects.append(abc_object)\n        # rename the abc container with namespace\n        abc_container_name = f\"{namespace}:{name}\"\n        abc_container.name = abc_container_name\n        abc_objects.append(abc_container)\n\n        return containerise(\n            name, abc_objects, context,\n            namespace, loader=self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n        from pymxs import runtime as rt\n\n        path = get_representation_path(representation)\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        abc_container = [n for n in get_previous_loaded_object(node)\n                         if rt.ClassOf(n) == rt.AlembicContainer]\n        with maintained_selection():\n            rt.Select(abc_container)\n\n            for alembic in rt.Selection:\n                abc = rt.GetNodeByName(alembic.name)\n                rt.Select(abc.Children)\n                for abc_con in abc.Children:\n                    abc_con.source = path\n                    rt.Select(abc_con.Children)\n                    for abc_obj in abc_con.Children:\n                        abc_obj.source = path\n        lib.imprint(\n            container[\"instance_node\"],\n            {\"representation\": str(representation[\"_id\"])},\n        )\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        from pymxs import runtime as rt\n\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        rt.Delete(node)\n\n    @staticmethod\n    def get_container_children(parent, type_name):\n        from pymxs import runtime as rt\n\n        def list_children(node):\n            children = []\n            for c in node.Children:\n                children.append(c)\n                children += list_children(c)\n            return children\n\n        filtered = []\n        for child in list_children(parent):\n            class_type = str(rt.classOf(child.baseObject))\n            if class_type == type_name:\n                filtered.append(child)\n\n        return filtered\n"
  },
  {
    "path": "openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py",
    "content": "import os\nfrom openpype.pipeline import load, get_representation_path\nfrom openpype.pipeline.load import LoadError\nfrom openpype.hosts.max.api.pipeline import (\n    containerise,\n    get_previous_loaded_object,\n    update_custom_attribute_data\n)\n\nfrom openpype.hosts.max.api.lib import (\n    unique_namespace,\n    get_namespace,\n    object_transform_set,\n    get_plugins\n)\nfrom openpype.hosts.max.api import lib\nfrom pymxs import runtime as rt\n\n\nclass OxAbcLoader(load.LoaderPlugin):\n    \"\"\"Ornatrix Alembic loader.\"\"\"\n\n    families = [\"camera\", \"animation\", \"pointcache\"]\n    label = \"Load Alembic with Ornatrix\"\n    representations = [\"abc\"]\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n    postfix = \"param\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        plugin_list = get_plugins()\n        if \"ephere.plugins.autodesk.max.ornatrix.dlo\" not in plugin_list:\n            raise LoadError(\"Ornatrix plugin not \"\n                            \"found/installed in Max yet..\")\n\n        file_path = os.path.normpath(self.filepath_from_context(context))\n        rt.AlembicImport.ImportToRoot = True\n        rt.AlembicImport.CustomAttributes = True\n        rt.importFile(\n            file_path, rt.name(\"noPrompt\"),\n            using=rt.Ornatrix_Alembic_Importer)\n\n        scene_object = []\n        for obj in rt.rootNode.Children:\n            obj_type = rt.ClassOf(obj)\n            if str(obj_type).startswith(\"Ox_\"):\n                scene_object.append(obj)\n\n        namespace = unique_namespace(\n            name + \"_\",\n            suffix=\"_\",\n        )\n        abc_container = []\n        for abc in scene_object:\n            abc.name = f\"{namespace}:{abc.name}\"\n            abc_container.append(abc)\n\n        return containerise(\n            name, abc_container, context,\n            namespace, loader=self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n        path = get_representation_path(representation)\n        node_name = container[\"instance_node\"]\n        namespace, name = get_namespace(node_name)\n        node = rt.getNodeByName(node_name)\n        node_list = get_previous_loaded_object(node)\n        rt.Select(node_list)\n        selections = rt.getCurrentSelection()\n        transform_data = object_transform_set(selections)\n        for prev_obj in selections:\n            if rt.isValidNode(prev_obj):\n                rt.Delete(prev_obj)\n\n        rt.AlembicImport.ImportToRoot = False\n        rt.AlembicImport.CustomAttributes = True\n        rt.importFile(\n            path, rt.name(\"noPrompt\"),\n            using=rt.Ornatrix_Alembic_Importer)\n\n        scene_object = []\n        for obj in rt.rootNode.Children:\n            obj_type = rt.ClassOf(obj)\n            if str(obj_type).startswith(\"Ox_\"):\n                scene_object.append(obj)\n        ox_abc_objects = []\n        for abc in scene_object:\n            abc.Parent = container\n            abc.name = f\"{namespace}:{abc.name}\"\n            ox_abc_objects.append(abc)\n            ox_transform = f\"{abc.name}.transform\"\n            if ox_transform in transform_data.keys():\n                abc.pos = transform_data[ox_transform] or 0\n                abc.scale = transform_data[f\"{abc.name}.scale\"] or 0\n        update_custom_attribute_data(node, ox_abc_objects)\n        lib.imprint(\n            container[\"instance_node\"],\n            {\"representation\": str(representation[\"_id\"])},\n        )\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        rt.Delete(node)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/load/load_pointcloud.py",
    "content": "import os\n\nfrom openpype.hosts.max.api import lib, maintained_selection\nfrom openpype.hosts.max.api.lib import (\n    unique_namespace,\n\n)\nfrom openpype.hosts.max.api.pipeline import (\n    containerise,\n    get_previous_loaded_object,\n    update_custom_attribute_data\n)\nfrom openpype.pipeline import get_representation_path, load\n\n\nclass PointCloudLoader(load.LoaderPlugin):\n    \"\"\"Point Cloud Loader.\"\"\"\n\n    families = [\"pointcloud\"]\n    representations = [\"prt\"]\n    order = -8\n    icon = \"code-fork\"\n    color = \"green\"\n    postfix = \"param\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        \"\"\"load point cloud by tyCache\"\"\"\n        from pymxs import runtime as rt\n        filepath = os.path.normpath(self.filepath_from_context(context))\n        obj = rt.tyCache()\n        obj.filename = filepath\n\n        namespace = unique_namespace(\n            name + \"_\",\n            suffix=\"_\",\n        )\n        obj.name = f\"{namespace}:{obj.name}\"\n\n        return containerise(\n            name, [obj], context,\n            namespace, loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        \"\"\"update the container\"\"\"\n        from pymxs import runtime as rt\n\n        path = get_representation_path(representation)\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        node_list = get_previous_loaded_object(node)\n        update_custom_attribute_data(\n            node, node_list)\n        with maintained_selection():\n            rt.Select(node_list)\n            for prt in rt.Selection:\n                prt.filename = path\n        lib.imprint(container[\"instance_node\"], {\n            \"representation\": str(representation[\"_id\"])\n        })\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        \"\"\"remove the container\"\"\"\n        from pymxs import runtime as rt\n\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        rt.Delete(node)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/load/load_redshift_proxy.py",
    "content": "import os\nimport clique\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path\n)\nfrom openpype.pipeline.load import LoadError\nfrom openpype.hosts.max.api.pipeline import (\n    containerise,\n    update_custom_attribute_data,\n    get_previous_loaded_object\n)\nfrom openpype.hosts.max.api import lib\nfrom openpype.hosts.max.api.lib import (\n    unique_namespace,\n    get_plugins\n)\n\n\nclass RedshiftProxyLoader(load.LoaderPlugin):\n    \"\"\"Load rs files with Redshift Proxy\"\"\"\n\n    label = \"Load Redshift Proxy\"\n    families = [\"redshiftproxy\"]\n    representations = [\"rs\"]\n    order = -9\n    icon = \"code-fork\"\n    color = \"white\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        from pymxs import runtime as rt\n        plugin_info = get_plugins()\n        if \"redshift4max.dlr\" not in plugin_info:\n            raise LoadError(\"Redshift not loaded/installed in Max..\")\n        filepath = self.filepath_from_context(context)\n        rs_proxy = rt.RedshiftProxy()\n        rs_proxy.file = filepath\n        files_in_folder = os.listdir(os.path.dirname(filepath))\n        collections, remainder = clique.assemble(files_in_folder)\n        if collections:\n            rs_proxy.is_sequence = True\n\n        namespace = unique_namespace(\n            name + \"_\",\n            suffix=\"_\",\n        )\n        rs_proxy.name = f\"{namespace}:{rs_proxy.name}\"\n\n        return containerise(\n            name, [rs_proxy], context,\n            namespace, loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        from pymxs import runtime as rt\n\n        path = get_representation_path(representation)\n        node = rt.getNodeByName(container[\"instance_node\"])\n        node_list = get_previous_loaded_object(node)\n        rt.Select(node_list)\n        update_custom_attribute_data(\n            node, rt.Selection)\n        for proxy in rt.Selection:\n            proxy.file = path\n\n        lib.imprint(container[\"instance_node\"], {\n            \"representation\": str(representation[\"_id\"])\n        })\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        from pymxs import runtime as rt\n\n        node = rt.getNodeByName(container[\"instance_node\"])\n        rt.delete(node)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/load/load_tycache.py",
    "content": "import os\nfrom openpype.hosts.max.api import lib, maintained_selection\nfrom openpype.hosts.max.api.lib import (\n    unique_namespace,\n\n)\nfrom openpype.hosts.max.api.pipeline import (\n    containerise,\n    get_previous_loaded_object,\n    update_custom_attribute_data\n)\nfrom openpype.pipeline import get_representation_path, load\n\n\nclass TyCacheLoader(load.LoaderPlugin):\n    \"\"\"TyCache Loader.\"\"\"\n\n    families = [\"tycache\"]\n    representations = [\"tyc\"]\n    order = -8\n    icon = \"code-fork\"\n    color = \"green\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        \"\"\"Load tyCache\"\"\"\n        from pymxs import runtime as rt\n        filepath = os.path.normpath(self.filepath_from_context(context))\n        obj = rt.tyCache()\n        obj.filename = filepath\n\n        namespace = unique_namespace(\n            name + \"_\",\n            suffix=\"_\",\n        )\n        obj.name = f\"{namespace}:{obj.name}\"\n\n        return containerise(\n            name, [obj], context,\n            namespace, loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        \"\"\"update the container\"\"\"\n        from pymxs import runtime as rt\n\n        path = get_representation_path(representation)\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        node_list = get_previous_loaded_object(node)\n        update_custom_attribute_data(node, node_list)\n        with maintained_selection():\n            for tyc in node_list:\n                tyc.filename = path\n        lib.imprint(container[\"instance_node\"], {\n            \"representation\": str(representation[\"_id\"])\n        })\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        \"\"\"remove the container\"\"\"\n        from pymxs import runtime as rt\n\n        node = rt.GetNodeByName(container[\"instance_node\"])\n        rt.Delete(node)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/collect_frame_range.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom pymxs import runtime as rt\n\n\nclass CollectFrameRange(pyblish.api.InstancePlugin):\n    \"\"\"Collect Frame Range.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.01\n    label = \"Collect Frame Range\"\n    hosts = ['max']\n    families = [\"camera\", \"maxrender\",\n                \"pointcache\", \"pointcloud\",\n                \"review\", \"redshiftproxy\"]\n\n    def process(self, instance):\n        if instance.data[\"family\"] == \"maxrender\":\n            instance.data[\"frameStartHandle\"] = int(rt.rendStart)\n            instance.data[\"frameEndHandle\"] = int(rt.rendEnd)\n        else:\n            instance.data[\"frameStartHandle\"] = int(rt.animationRange.start)\n            instance.data[\"frameEndHandle\"] = int(rt.animationRange.end)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/collect_members.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect instance members.\"\"\"\nimport pyblish.api\nfrom pymxs import runtime as rt\n\n\nclass CollectMembers(pyblish.api.InstancePlugin):\n    \"\"\"Collect Set Members.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.01\n    label = \"Collect Instance Members\"\n    hosts = ['max']\n\n    def process(self, instance):\n        if instance.data[\"family\"] == \"workfile\":\n            self.log.debug(\"Skipping Actions for workfile family.\")\n            self.log.debug(\"{}\".format(instance.data[\"subset\"]))\n            return\n        if instance.data.get(\"instance_node\"):\n            container = rt.GetNodeByName(instance.data[\"instance_node\"])\n            instance.data[\"members\"] = [\n                member.node for member\n                in container.modifiers[0].openPypeData.all_handles\n            ]\n            self.log.debug(\"{}\".format(instance.data[\"members\"]))\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/collect_render.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect Render\"\"\"\nimport os\nimport pyblish.api\n\nfrom pymxs import runtime as rt\nfrom openpype.pipeline.publish import KnownPublishError\nfrom openpype.hosts.max.api import colorspace\nfrom openpype.hosts.max.api.lib import get_max_version, get_current_renderer\nfrom openpype.hosts.max.api.lib_rendersettings import RenderSettings\nfrom openpype.hosts.max.api.lib_renderproducts import RenderProducts\n\n\nclass CollectRender(pyblish.api.InstancePlugin):\n    \"\"\"Collect Render for Deadline\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.02\n    label = \"Collect 3dsmax Render Layers\"\n    hosts = ['max']\n    families = [\"maxrender\"]\n\n    def process(self, instance):\n        context = instance.context\n        folder = rt.maxFilePath\n        file = rt.maxFileName\n        current_file = os.path.join(folder, file)\n        filepath = current_file.replace(\"\\\\\", \"/\")\n        context.data['currentFile'] = current_file\n\n        files_by_aov = RenderProducts().get_beauty(instance.name)\n        aovs = RenderProducts().get_aovs(instance.name)\n        files_by_aov.update(aovs)\n\n        camera = rt.viewport.GetCamera()\n        if instance.data.get(\"members\"):\n            camera_list = [member for member in instance.data[\"members\"]\n                           if rt.ClassOf(member) == rt.Camera.Classes]\n            if camera_list:\n                camera = camera_list[-1]\n\n        instance.data[\"cameras\"] = [camera.name] if camera else None        # noqa\n\n        if instance.data.get(\"multiCamera\"):\n            cameras = instance.data.get(\"members\")\n            if not cameras:\n                raise KnownPublishError(\"There should be at least\"\n                                        \" one renderable camera in container\")\n            sel_cam = [\n                c.name for c in cameras\n                if rt.classOf(c) in rt.Camera.classes]\n            container_name = instance.data.get(\"instance_node\")\n            render_dir = os.path.dirname(rt.rendOutputFilename)\n            outputs = RenderSettings().batch_render_layer(\n                container_name, render_dir, sel_cam\n            )\n\n            instance.data[\"cameras\"] = sel_cam\n\n            files_by_aov = RenderProducts().get_multiple_beauty(\n                outputs, sel_cam)\n            aovs = RenderProducts().get_multiple_aovs(\n                outputs, sel_cam)\n            files_by_aov.update(aovs)\n\n        if \"expectedFiles\" not in instance.data:\n            instance.data[\"expectedFiles\"] = list()\n            instance.data[\"files\"] = list()\n            instance.data[\"expectedFiles\"].append(files_by_aov)\n            instance.data[\"files\"].append(files_by_aov)\n\n        img_format = RenderProducts().image_format()\n        # OCIO config not support in\n        # most of the 3dsmax renderers\n        # so this is currently hard coded\n        # TODO: add options for redshift/vray ocio config\n        instance.data[\"colorspaceConfig\"] = \"\"\n        instance.data[\"colorspaceDisplay\"] = \"sRGB\"\n        instance.data[\"colorspaceView\"] = \"ACES 1.0 SDR-video\"\n\n        if int(get_max_version()) >= 2024:\n            colorspace_mgr = rt.ColorPipelineMgr      # noqa\n            display = next(\n                (display for display in colorspace_mgr.GetDisplayList()))\n            view_transform = next(\n                (view for view in colorspace_mgr.GetViewList(display)))\n            instance.data[\"colorspaceConfig\"] = colorspace_mgr.OCIOConfigPath\n            instance.data[\"colorspaceDisplay\"] = display\n            instance.data[\"colorspaceView\"] = view_transform\n\n        instance.data[\"renderProducts\"] = colorspace.ARenderProduct()\n        instance.data[\"publishJobState\"] = \"Suspended\"\n        instance.data[\"attachTo\"] = []\n        renderer_class = get_current_renderer()\n        renderer = str(renderer_class).split(\":\")[0]\n        # also need to get the render dir for conversion\n        data = {\n            \"asset\": instance.data[\"asset\"],\n            \"subset\": str(instance.name),\n            \"publish\": True,\n            \"maxversion\": str(get_max_version()),\n            \"imageFormat\": img_format,\n            \"family\": 'maxrender',\n            \"families\": ['maxrender'],\n            \"renderer\": renderer,\n            \"source\": filepath,\n            \"plugin\": \"3dsmax\",\n            \"frameStart\": instance.data[\"frameStartHandle\"],\n            \"frameEnd\": instance.data[\"frameEndHandle\"],\n            \"farm\": True\n        }\n        instance.data.update(data)\n\n        # TODO: this should be unified with maya and its \"multipart\" flag\n        #       on instance.\n        if renderer == \"Redshift_Renderer\":\n            instance.data.update(\n                {\"separateAovFiles\": rt.Execute(\n                    \"renderers.current.separateAovFiles\")})\n\n        self.log.info(\"data: {0}\".format(data))\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/collect_review.py",
    "content": "# dont forget getting the focal length for burnin\n\"\"\"Collect Review\"\"\"\nimport pyblish.api\n\nfrom pymxs import runtime as rt\nfrom openpype.lib import BoolDef\nfrom openpype.hosts.max.api.lib import get_max_version\nfrom openpype.pipeline.publish import (\n    OpenPypePyblishPluginMixin,\n    KnownPublishError\n)\n\n\nclass CollectReview(pyblish.api.InstancePlugin,\n                    OpenPypePyblishPluginMixin):\n    \"\"\"Collect Review Data for Preview Animation\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.02\n    label = \"Collect Review Data\"\n    hosts = ['max']\n    families = [\"review\"]\n\n    def process(self, instance):\n        nodes = instance.data[\"members\"]\n\n        def is_camera(node):\n            is_camera_class = rt.classOf(node) in rt.Camera.classes\n            return is_camera_class and rt.isProperty(node, \"fov\")\n\n        # Use first camera in instance\n        cameras = [node for node in nodes if is_camera(node)]\n        if cameras:\n            if len(cameras) > 1:\n                self.log.warning(\n                    \"Found more than one camera in instance, using first \"\n                    f\"one found: {cameras[0]}\"\n                )\n            camera = cameras[0]\n            camera_name = camera.name\n            focal_length = camera.fov\n        else:\n            raise KnownPublishError(\n                \"Unable to find a valid camera in 'Review' container.\"\n                \" Only native max Camera supported. \"\n                f\"Found objects: {nodes}\"\n            )\n        creator_attrs = instance.data[\"creator_attributes\"]\n        attr_values = self.get_attr_values_from_data(instance.data)\n\n        general_preview_data = {\n            \"review_camera\": camera_name,\n            \"frameStart\": instance.data[\"frameStartHandle\"],\n            \"frameEnd\": instance.data[\"frameEndHandle\"],\n            \"percentSize\": creator_attrs[\"percentSize\"],\n            \"imageFormat\": creator_attrs[\"imageFormat\"],\n            \"keepImages\": creator_attrs[\"keepImages\"],\n            \"fps\": instance.context.data[\"fps\"],\n            \"review_width\": creator_attrs[\"review_width\"],\n            \"review_height\": creator_attrs[\"review_height\"],\n        }\n\n        if int(get_max_version()) >= 2024:\n            colorspace_mgr = rt.ColorPipelineMgr      # noqa\n            display = next(\n                (display for display in colorspace_mgr.GetDisplayList()))\n            view_transform = next(\n                (view for view in colorspace_mgr.GetViewList(display)))\n            instance.data[\"colorspaceConfig\"] = colorspace_mgr.OCIOConfigPath\n            instance.data[\"colorspaceDisplay\"] = display\n            instance.data[\"colorspaceView\"] = view_transform\n\n            preview_data = {\n                \"vpStyle\": creator_attrs[\"visualStyleMode\"],\n                \"vpPreset\": creator_attrs[\"viewportPreset\"],\n                \"vpTextures\": creator_attrs[\"vpTexture\"],\n                \"dspGeometry\": attr_values.get(\"dspGeometry\"),\n                \"dspShapes\": attr_values.get(\"dspShapes\"),\n                \"dspLights\": attr_values.get(\"dspLights\"),\n                \"dspCameras\": attr_values.get(\"dspCameras\"),\n                \"dspHelpers\": attr_values.get(\"dspHelpers\"),\n                \"dspParticles\": attr_values.get(\"dspParticles\"),\n                \"dspBones\": attr_values.get(\"dspBones\"),\n                \"dspBkg\": attr_values.get(\"dspBkg\"),\n                \"dspGrid\": attr_values.get(\"dspGrid\"),\n                \"dspSafeFrame\": attr_values.get(\"dspSafeFrame\"),\n                \"dspFrameNums\": attr_values.get(\"dspFrameNums\")\n            }\n        else:\n            general_viewport = {\n                \"dspBkg\": attr_values.get(\"dspBkg\"),\n                \"dspGrid\": attr_values.get(\"dspGrid\")\n            }\n            nitrous_manager = {\n                \"AntialiasingQuality\": creator_attrs[\"antialiasingQuality\"],\n            }\n            nitrous_viewport = {\n                \"VisualStyleMode\": creator_attrs[\"visualStyleMode\"],\n                \"ViewportPreset\": creator_attrs[\"viewportPreset\"],\n                \"UseTextureEnabled\": creator_attrs[\"vpTexture\"]\n            }\n            preview_data = {\n                \"general_viewport\": general_viewport,\n                \"nitrous_manager\": nitrous_manager,\n                \"nitrous_viewport\": nitrous_viewport,\n                \"vp_btn_mgr\": {\"EnableButtons\": False}\n            }\n\n        # Enable ftrack functionality\n        instance.data.setdefault(\"families\", []).append('ftrack')\n\n        burnin_members = instance.data.setdefault(\"burninDataMembers\", {})\n        burnin_members[\"focalLength\"] = focal_length\n\n        instance.data.update(general_preview_data)\n        instance.data[\"viewport_options\"] = preview_data\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            BoolDef(\"dspGeometry\",\n                    label=\"Geometry\",\n                    default=True),\n            BoolDef(\"dspShapes\",\n                    label=\"Shapes\",\n                    default=False),\n            BoolDef(\"dspLights\",\n                    label=\"Lights\",\n                    default=False),\n            BoolDef(\"dspCameras\",\n                    label=\"Cameras\",\n                    default=False),\n            BoolDef(\"dspHelpers\",\n                    label=\"Helpers\",\n                    default=False),\n            BoolDef(\"dspParticles\",\n                    label=\"Particle Systems\",\n                    default=True),\n            BoolDef(\"dspBones\",\n                    label=\"Bone Objects\",\n                    default=False),\n            BoolDef(\"dspBkg\",\n                    label=\"Background\",\n                    default=True),\n            BoolDef(\"dspGrid\",\n                    label=\"Active Grid\",\n                    default=False),\n            BoolDef(\"dspSafeFrame\",\n                    label=\"Safe Frames\",\n                    default=False),\n            BoolDef(\"dspFrameNums\",\n                    label=\"Frame Numbers\",\n                    default=False)\n        ]\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/collect_tycache_attributes.py",
    "content": "import pyblish.api\n\nfrom openpype.lib import EnumDef, TextDef\nfrom openpype.pipeline.publish import OpenPypePyblishPluginMixin\n\n\nclass CollectTyCacheData(pyblish.api.InstancePlugin,\n                         OpenPypePyblishPluginMixin):\n    \"\"\"Collect Channel Attributes for TyCache Export\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.02\n    label = \"Collect tyCache attribute Data\"\n    hosts = ['max']\n    families = [\"tycache\"]\n\n    def process(self, instance):\n        attr_values = self.get_attr_values_from_data(instance.data)\n        attributes = {}\n        for attr_key in attr_values.get(\"tycacheAttributes\", []):\n            attributes[attr_key] = True\n\n        for key in [\"tycacheLayer\", \"tycacheObjectName\"]:\n            attributes[key] = attr_values.get(key, \"\")\n\n        # Collect the selected channel data before exporting\n        instance.data[\"tyc_attrs\"] = attributes\n        self.log.debug(\n            f\"Found tycache attributes: {attributes}\"\n        )\n\n    @classmethod\n    def get_attribute_defs(cls):\n        # TODO: Support the attributes with maxObject array\n        tyc_attr_enum = [\"tycacheChanAge\", \"tycacheChanGroups\",\n                         \"tycacheChanPos\", \"tycacheChanRot\",\n                         \"tycacheChanScale\", \"tycacheChanVel\",\n                         \"tycacheChanSpin\", \"tycacheChanShape\",\n                         \"tycacheChanMatID\", \"tycacheChanMapping\",\n                         \"tycacheChanMaterials\", \"tycacheChanCustomFloat\"\n                         \"tycacheChanCustomVector\", \"tycacheChanCustomTM\",\n                         \"tycacheChanPhysX\", \"tycacheMeshBackup\",\n                         \"tycacheCreateObject\",\n                         \"tycacheCreateObjectIfNotCreated\",\n                         \"tycacheAdditionalCloth\",\n                         \"tycacheAdditionalSkin\",\n                         \"tycacheAdditionalSkinID\",\n                         \"tycacheAdditionalSkinIDValue\",\n                         \"tycacheAdditionalTerrain\",\n                         \"tycacheAdditionalVDB\",\n                         \"tycacheAdditionalSplinePaths\",\n                         \"tycacheAdditionalGeo\",\n                         \"tycacheAdditionalGeoActivateModifiers\",\n                         \"tycacheSplines\",\n                         \"tycacheSplinesAdditionalSplines\"\n                         ]\n        tyc_default_attrs = [\"tycacheChanGroups\", \"tycacheChanPos\",\n                             \"tycacheChanRot\", \"tycacheChanScale\",\n                             \"tycacheChanVel\", \"tycacheChanShape\",\n                             \"tycacheChanMatID\", \"tycacheChanMapping\",\n                             \"tycacheChanMaterials\",\n                             \"tycacheCreateObjectIfNotCreated\"]\n        return [\n            EnumDef(\"tycacheAttributes\",\n                    tyc_attr_enum,\n                    default=tyc_default_attrs,\n                    multiselection=True,\n                    label=\"TyCache Attributes\"),\n            TextDef(\"tycacheLayer\",\n                    label=\"TyCache Layer\",\n                    tooltip=\"Name of tycache layer\",\n                    default=\"$(tyFlowLayer)\"),\n            TextDef(\"tycacheObjectName\",\n                    label=\"TyCache Object Name\",\n                    tooltip=\"TyCache Object Name\",\n                    default=\"$(tyFlowName)_tyCache\")\n        ]\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/collect_workfile.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect current work file.\"\"\"\nimport os\nimport pyblish.api\n\nfrom pymxs import runtime as rt\n\n\nclass CollectWorkfile(pyblish.api.InstancePlugin):\n    \"\"\"Inject the current working file into context\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.01\n    label = \"Collect 3dsmax Workfile\"\n    hosts = ['max']\n\n    def process(self, instance):\n        \"\"\"Inject the current working file.\"\"\"\n        context = instance.context\n        folder = rt.maxFilePath\n        file = rt.maxFileName\n        if not folder or not file:\n            self.log.error(\"Scene is not saved.\")\n        current_file = os.path.join(folder, file)\n\n        context.data['currentFile'] = current_file\n\n        ext = os.path.splitext(file)[-1].lstrip(\".\")\n\n        data = {}\n\n        # create instance\n        subset = instance.data[\"subset\"]\n\n        data.update({\n            \"subset\": subset,\n            \"asset\": context.data[\"asset\"],\n            \"label\": subset,\n            \"publish\": True,\n            \"family\": 'workfile',\n            \"families\": ['workfile'],\n            \"setMembers\": [current_file],\n            \"frameStart\": context.data['frameStart'],\n            \"frameEnd\": context.data['frameEnd'],\n            \"handleStart\": context.data['handleStart'],\n            \"handleEnd\": context.data['handleEnd']\n        })\n\n        data['representations'] = [{\n            'name': ext.lstrip(\".\"),\n            'ext': ext.lstrip(\".\"),\n            'files': file,\n            \"stagingDir\": folder,\n        }]\n\n        instance.data.update(data)\n        self.log.info('Collected data: {}'.format(data))\n        self.log.info('Collected instance: {}'.format(file))\n        self.log.info('Scene path: {}'.format(current_file))\n        self.log.info('staging Dir: {}'.format(folder))\n        self.log.info('subset: {}'.format(subset))\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/extract_alembic.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nExport alembic file.\n\nNote:\n    Parameters on AlembicExport (AlembicExport.Parameter):\n\n    ParticleAsMesh (bool): Sets whether particle shapes are exported\n        as meshes.\n    AnimTimeRange (enum): How animation is saved:\n        #CurrentFrame: saves current frame\n        #TimeSlider: saves the active time segments on time slider (default)\n        #StartEnd: saves a range specified by the Step\n    StartFrame (int)\n    EnFrame (int)\n    ShapeSuffix (bool): When set to true, appends the string \"Shape\" to the\n        name of each exported mesh. This property is set to false by default.\n    SamplesPerFrame (int): Sets the number of animation samples per frame.\n    Hidden (bool): When true, export hidden geometry.\n    UVs (bool): When true, export the mesh UV map channel.\n    Normals (bool): When true, export the mesh normals.\n    VertexColors (bool): When true, export the mesh vertex color map 0 and the\n        current vertex color display data when it differs\n    ExtraChannels (bool): When true, export the mesh extra map channels\n        (map channels greater than channel 1)\n    Velocity (bool): When true, export the meh vertex and particle velocity\n        data.\n    MaterialIDs (bool): When true, export the mesh material ID as\n        Alembic face sets.\n    Visibility (bool): When true, export the node visibility data.\n    LayerName (bool): When true, export the node layer name as an Alembic\n        object property.\n    MaterialName (bool): When true, export the geometry node material name as\n        an Alembic object property\n    ObjectID (bool): When true, export the geometry node g-buffer object ID as\n        an Alembic object property.\n    CustomAttributes (bool): When true, export the node and its modifiers\n        custom attributes into an Alembic object compound property.\n\"\"\"\nimport os\nimport pyblish.api\nfrom openpype.pipeline import publish, OptionalPyblishPluginMixin\nfrom pymxs import runtime as rt\nfrom openpype.hosts.max.api import maintained_selection\nfrom openpype.hosts.max.api.lib import suspended_refresh\nfrom openpype.lib import BoolDef\n\n\nclass ExtractAlembic(publish.Extractor,\n                     OptionalPyblishPluginMixin):\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract Pointcache\"\n    hosts = [\"max\"]\n    families = [\"pointcache\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        parent_dir = self.staging_dir(instance)\n        file_name = \"{name}.abc\".format(**instance.data)\n        path = os.path.join(parent_dir, file_name)\n\n        with suspended_refresh():\n            self._set_abc_attributes(instance)\n            with maintained_selection():\n                # select and export\n                node_list = instance.data[\"members\"]\n                rt.Select(node_list)\n                rt.exportFile(\n                    path,\n                    rt.name(\"noPrompt\"),\n                    selectedOnly=True,\n                    using=rt.AlembicExport,\n                )\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            \"name\": \"abc\",\n            \"ext\": \"abc\",\n            \"files\": file_name,\n            \"stagingDir\": parent_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n    def _set_abc_attributes(self, instance):\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n        attr_values = self.get_attr_values_from_data(instance.data)\n        custom_attrs = attr_values.get(\"custom_attrs\", False)\n        if not custom_attrs:\n            self.log.debug(\n                \"No Custom Attributes included in this abc export...\")\n        rt.AlembicExport.ArchiveType = rt.Name(\"ogawa\")\n        rt.AlembicExport.CoordinateSystem = rt.Name(\"maya\")\n        rt.AlembicExport.StartFrame = start\n        rt.AlembicExport.EndFrame = end\n        rt.AlembicExport.CustomAttributes = custom_attrs\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            BoolDef(\"custom_attrs\",\n                    label=\"Custom Attributes\",\n                    default=False),\n        ]\n\n\nclass ExtractCameraAlembic(ExtractAlembic):\n    \"\"\"Extract Camera with AlembicExport.\"\"\"\n\n    label = \"Extract Alembic Camera\"\n    families = [\"camera\"]\n\n\nclass ExtractModel(ExtractAlembic):\n    \"\"\"Extract Geometry in Alembic Format\"\"\"\n    label = \"Extract Geometry (Alembic)\"\n    families = [\"model\"]\n\n    def _set_abc_attributes(self, instance):\n        attr_values = self.get_attr_values_from_data(instance.data)\n        custom_attrs = attr_values.get(\"custom_attrs\", False)\n        if not custom_attrs:\n            self.log.debug(\n                \"No Custom Attributes included in this abc export...\")\n        rt.AlembicExport.ArchiveType = rt.name(\"ogawa\")\n        rt.AlembicExport.CoordinateSystem = rt.name(\"maya\")\n        rt.AlembicExport.CustomAttributes = custom_attrs\n        rt.AlembicExport.UVs = True\n        rt.AlembicExport.VertexColors = True\n        rt.AlembicExport.PreserveInstances = True\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/extract_fbx.py",
    "content": "import os\nimport pyblish.api\nfrom openpype.pipeline import publish, OptionalPyblishPluginMixin\nfrom pymxs import runtime as rt\nfrom openpype.hosts.max.api import maintained_selection\nfrom openpype.hosts.max.api.lib import convert_unit_scale\n\n\nclass ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin):\n    \"\"\"\n    Extract Geometry in FBX Format\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.05\n    label = \"Extract FBX\"\n    hosts = [\"max\"]\n    families = [\"model\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        stagingdir = self.staging_dir(instance)\n        filename = \"{name}.fbx\".format(**instance.data)\n        filepath = os.path.join(stagingdir, filename)\n        self._set_fbx_attributes()\n\n        with maintained_selection():\n            # select and export\n            node_list = instance.data[\"members\"]\n            rt.Select(node_list)\n            rt.exportFile(\n                filepath,\n                rt.name(\"noPrompt\"),\n                selectedOnly=True,\n                using=rt.FBXEXP,\n            )\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            \"name\": \"fbx\",\n            \"ext\": \"fbx\",\n            \"files\": filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(representation)\n        self.log.info(\n            \"Extracted instance '%s' to: %s\" % (instance.name, filepath)\n        )\n\n    def _set_fbx_attributes(self):\n        unit_scale = convert_unit_scale()\n        rt.FBXExporterSetParam(\"Animation\", False)\n        rt.FBXExporterSetParam(\"Cameras\", False)\n        rt.FBXExporterSetParam(\"Lights\", False)\n        rt.FBXExporterSetParam(\"PointCache\", False)\n        rt.FBXExporterSetParam(\"AxisConversionMethod\", \"Animation\")\n        rt.FBXExporterSetParam(\"UpAxis\", \"Y\")\n        rt.FBXExporterSetParam(\"Preserveinstances\", True)\n        if unit_scale:\n            rt.FBXExporterSetParam(\"ConvertUnit\", unit_scale)\n\n\nclass ExtractCameraFbx(ExtractModelFbx):\n    \"\"\"Extract Camera with FbxExporter.\"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.2\n    label = \"Extract Fbx Camera\"\n    families = [\"camera\"]\n    optional = True\n\n    def _set_fbx_attributes(self):\n        unit_scale = convert_unit_scale()\n        rt.FBXExporterSetParam(\"Animation\", True)\n        rt.FBXExporterSetParam(\"Cameras\", True)\n        rt.FBXExporterSetParam(\"AxisConversionMethod\", \"Animation\")\n        rt.FBXExporterSetParam(\"UpAxis\", \"Y\")\n        rt.FBXExporterSetParam(\"Preserveinstances\", True)\n        if unit_scale:\n            rt.FBXExporterSetParam(\"ConvertUnit\", unit_scale)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/extract_max_scene_raw.py",
    "content": "import os\nimport pyblish.api\nfrom openpype.pipeline import publish, OptionalPyblishPluginMixin\nfrom pymxs import runtime as rt\n\n\nclass ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin):\n    \"\"\"\n    Extract Raw Max Scene with SaveSelected\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.2\n    label = \"Extract Max Scene (Raw)\"\n    hosts = [\"max\"]\n    families = [\"camera\", \"maxScene\", \"model\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # publish the raw scene for camera\n        self.log.debug(\"Extracting Raw Max Scene ...\")\n\n        stagingdir = self.staging_dir(instance)\n        filename = \"{name}.max\".format(**instance.data)\n\n        max_path = os.path.join(stagingdir, filename)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        nodes = instance.data[\"members\"]\n        rt.saveNodes(nodes, max_path, quiet=True)\n\n        self.log.info(\"Performing Extraction ...\")\n\n        representation = {\n            \"name\": \"max\",\n            \"ext\": \"max\",\n            \"files\": filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(representation)\n        self.log.info(\n            \"Extracted instance '%s' to: %s\" % (instance.name, max_path)\n        )\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/extract_model_obj.py",
    "content": "import os\nimport pyblish.api\nfrom openpype.pipeline import publish, OptionalPyblishPluginMixin\nfrom pymxs import runtime as rt\nfrom openpype.hosts.max.api import maintained_selection\nfrom openpype.hosts.max.api.lib import suspended_refresh\nfrom openpype.pipeline.publish import KnownPublishError\n\n\nclass ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin):\n    \"\"\"\n    Extract Geometry in OBJ Format\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.05\n    label = \"Extract OBJ\"\n    hosts = [\"max\"]\n    families = [\"model\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        stagingdir = self.staging_dir(instance)\n        filename = \"{name}.obj\".format(**instance.data)\n        filepath = os.path.join(stagingdir, filename)\n\n        with suspended_refresh():\n            with maintained_selection():\n                # select and export\n                node_list = instance.data[\"members\"]\n                rt.Select(node_list)\n                rt.exportFile(\n                    filepath,\n                    rt.name(\"noPrompt\"),\n                    selectedOnly=True,\n                    using=rt.ObjExp,\n                )\n        if not os.path.exists(filepath):\n            raise KnownPublishError(\n                \"File {} wasn't produced by 3ds max, please check the logs.\")\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            \"name\": \"obj\",\n            \"ext\": \"obj\",\n            \"files\": filename,\n            \"stagingDir\": stagingdir,\n        }\n\n        instance.data[\"representations\"].append(representation)\n        self.log.info(\n            \"Extracted instance '%s' to: %s\" % (instance.name, filepath)\n        )\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/extract_model_usd.py",
    "content": "import os\n\nimport pyblish.api\nfrom pymxs import runtime as rt\n\nfrom openpype.hosts.max.api import maintained_selection\nfrom openpype.pipeline import OptionalPyblishPluginMixin, publish\n\n\nclass ExtractModelUSD(publish.Extractor,\n                      OptionalPyblishPluginMixin):\n    \"\"\"Extract Geometry in USDA Format.\"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.05\n    label = \"Extract Geometry (USD)\"\n    hosts = [\"max\"]\n    families = [\"model\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        self.log.info(\"Extracting Geometry ...\")\n\n        stagingdir = self.staging_dir(instance)\n        asset_filename = \"{name}.usda\".format(**instance.data)\n        asset_filepath = os.path.join(stagingdir,\n                                      asset_filename)\n        self.log.info(f\"Writing USD '{asset_filepath}' to '{stagingdir}'\")\n\n        log_filename = \"{name}.txt\".format(**instance.data)\n        log_filepath = os.path.join(stagingdir,\n                                    log_filename)\n        self.log.info(f\"Writing log '{log_filepath}' to '{stagingdir}'\")\n\n        # get the nodes which need to be exported\n        export_options = self.get_export_options(log_filepath)\n        with maintained_selection():\n            # select and export\n            node_list = instance.data[\"members\"]\n            rt.Select(node_list)\n            rt.USDExporter.ExportFile(asset_filepath,\n                                      exportOptions=export_options,\n                                      contentSource=rt.Name(\"selected\"),\n                                      nodeList=node_list)\n\n        self.log.info(\"Performing Extraction ...\")\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'usda',\n            'ext': 'usda',\n            'files': asset_filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        log_representation = {\n            'name': 'txt',\n            'ext': 'txt',\n            'files': log_filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(log_representation)\n\n        self.log.info(\n            f\"Extracted instance '{instance.name}' to: {asset_filepath}\")\n\n    @staticmethod\n    def get_export_options(log_path):\n        \"\"\"Set Export Options for USD Exporter\"\"\"\n\n        export_options = rt.USDExporter.createOptions()\n\n        export_options.Meshes = True\n        export_options.Shapes = False\n        export_options.Lights = False\n        export_options.Cameras = False\n        export_options.Materials = False\n        export_options.MeshFormat = rt.Name('fromScene')\n        export_options.FileFormat = rt.Name('ascii')\n        export_options.UpAxis = rt.Name('y')\n        export_options.LogLevel = rt.Name('info')\n        export_options.LogPath = log_path\n        export_options.PreserveEdgeOrientation = True\n        export_options.TimeMode = rt.Name('current')\n\n        rt.USDexporter.UIOptions = export_options\n\n        return export_options\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/extract_pointcloud.py",
    "content": "import os\n\nimport pyblish.api\nfrom pymxs import runtime as rt\n\nfrom openpype.hosts.max.api import maintained_selection\nfrom openpype.pipeline import publish\n\n\nclass ExtractPointCloud(publish.Extractor):\n    \"\"\"\n    Extract PRT format with tyFlow operators.\n\n    Notes:\n        Currently only works for the default partition setting\n\n    Args:\n        self.export_particle(): sets up all job arguments for attributes\n            to be exported in MAXscript\n\n        self.get_operators(): get the export_particle operator\n\n        self.get_custom_attr(): get all custom channel attributes from Openpype\n            setting and sets it as job arguments before exporting\n\n        self.get_files(): get the files with tyFlow naming convention\n            before publishing\n\n        self.partition_output_name(): get the naming with partition settings.\n\n        self.get_partition(): get partition value\n\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.2\n    label = \"Extract Point Cloud\"\n    hosts = [\"max\"]\n    families = [\"pointcloud\"]\n    settings = []\n\n    def process(self, instance):\n        self.settings = self.get_setting(instance)\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n        self.log.info(\"Extracting PRT...\")\n\n        stagingdir = self.staging_dir(instance)\n        filename = \"{name}.prt\".format(**instance.data)\n        path = os.path.join(stagingdir, filename)\n\n        with maintained_selection():\n            job_args = self.export_particle(instance.data[\"members\"],\n                                            start,\n                                            end,\n                                            path)\n\n            for job in job_args:\n                rt.Execute(job)\n\n        self.log.info(\"Performing Extraction ...\")\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        self.log.info(\"Writing PRT with TyFlow Plugin...\")\n        filenames = self.get_files(\n            instance.data[\"members\"], path, start, end)\n        self.log.debug(f\"filenames: {filenames}\")\n\n        partition = self.partition_output_name(\n            instance.data[\"members\"])\n\n        representation = {\n            'name': 'prt',\n            'ext': 'prt',\n            'files': filenames if len(filenames) > 1 else filenames[0],\n            \"stagingDir\": stagingdir,\n            \"outputName\": partition         # partition value\n        }\n        instance.data[\"representations\"].append(representation)\n        self.log.info(f\"Extracted instance '{instance.name}' to: {path}\")\n\n    def export_particle(self,\n                        members,\n                        start,\n                        end,\n                        filepath):\n        \"\"\"Sets up all job arguments for attributes.\n\n        Those attributes are to be exported in MAX Script.\n\n        Args:\n            members (list): Member nodes of the instance.\n            start (int): Start frame.\n            end (int): End frame.\n            filepath (str): Path to PRT file.\n\n        Returns:\n            list of arguments for MAX Script.\n\n        \"\"\"\n        job_args = []\n        opt_list = self.get_operators(members)\n        for operator in opt_list:\n            start_frame = f\"{operator}.frameStart={start}\"\n            job_args.append(start_frame)\n            end_frame = f\"{operator}.frameEnd={end}\"\n            job_args.append(end_frame)\n            filepath = filepath.replace(\"\\\\\", \"/\")\n            prt_filename = f'{operator}.PRTFilename=\"{filepath}\"'\n            job_args.append(prt_filename)\n            # Partition\n            mode = f\"{operator}.PRTPartitionsMode=2\"\n            job_args.append(mode)\n\n            additional_args = self.get_custom_attr(operator)\n            job_args.extend(iter(additional_args))\n            prt_export = f\"{operator}.exportPRT()\"\n            job_args.append(prt_export)\n\n        return job_args\n\n    @staticmethod\n    def get_operators(members):\n        \"\"\"Get Export Particles Operator.\n\n        Args:\n            members (list): Instance members.\n\n        Returns:\n            list of particle operators\n\n        \"\"\"\n        opt_list = []\n        for member in members:\n            obj = member.baseobject\n        # TODO: to see if it can be used maxscript instead\n            anim_names = rt.GetSubAnimNames(obj)\n            for anim_name in anim_names:\n                sub_anim = rt.GetSubAnim(obj, anim_name)\n                boolean = rt.IsProperty(sub_anim, \"Export_Particles\")\n                if boolean:\n                        event_name = sub_anim.Name\n                        opt = f\"${member.Name}.{event_name}.export_particles\"\n                        opt_list.append(opt)\n\n        return opt_list\n\n    @staticmethod\n    def get_setting(instance):\n        project_setting = instance.context.data[\"project_settings\"]\n        return project_setting[\"max\"][\"PointCloud\"]\n\n    def get_custom_attr(self, operator):\n        \"\"\"Get Custom Attributes\"\"\"\n\n        custom_attr_list = []\n        attr_settings = self.settings[\"attribute\"]\n        for key, value in attr_settings.items():\n            custom_attr = \"{0}.PRTChannels_{1}=True\".format(operator,\n                                                            value)\n            self.log.debug(\n                \"{0} will be added as custom attribute\".format(key)\n            )\n            custom_attr_list.append(custom_attr)\n\n        return custom_attr_list\n\n    def get_files(self,\n                  container,\n                  path,\n                  start_frame,\n                  end_frame):\n        \"\"\"Get file names for tyFlow.\n\n        Set the filenames accordingly to the tyFlow file\n        naming extension for the publishing purpose\n\n        Actual File Output from tyFlow::\n            <SceneFile>__part<PartitionStart>of<PartitionCount>.<frame>.prt\n\n            e.g. tyFlow_cloth_CCCS_blobbyFill_001__part1of1_00004.prt\n\n        Args:\n            container: Instance node.\n            path (str): Output directory.\n            start_frame (int): Start frame.\n            end_frame (int): End frame.\n\n        Returns:\n            list of filenames\n\n        \"\"\"\n        filenames = []\n        filename = os.path.basename(path)\n        orig_name, ext = os.path.splitext(filename)\n        partition_count, partition_start = self.get_partition(container)\n        for frame in range(int(start_frame), int(end_frame) + 1):\n            actual_name = \"{}__part{:03}of{}_{:05}\".format(orig_name,\n                                                           partition_start,\n                                                           partition_count,\n                                                           frame)\n            actual_filename = path.replace(orig_name, actual_name)\n            filenames.append(os.path.basename(actual_filename))\n\n        return filenames\n\n    def partition_output_name(self, container):\n        \"\"\"Get partition output name.\n\n        Partition output name set for mapping\n        the published file output.\n\n        Todo:\n            Customizes the setting for the output.\n\n        Args:\n            container: Instance node.\n\n        Returns:\n            str: Partition name.\n\n        \"\"\"\n        partition_count, partition_start = self.get_partition(container)\n        return f\"_part{partition_start:03}of{partition_count}\"\n\n    def get_partition(self, container):\n        \"\"\"Get Partition value.\n\n        Args:\n            container: Instance node.\n\n        \"\"\"\n        opt_list = self.get_operators(container)\n        # TODO: This looks strange? Iterating over\n        #   the opt_list but returning from inside?\n        for operator in opt_list:\n            count = rt.Execute(f'{operator}.PRTPartitionsCount')\n            start = rt.Execute(f'{operator}.PRTPartitionsFrom')\n\n            return count, start\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/extract_redshift_proxy.py",
    "content": "import os\nimport pyblish.api\nfrom openpype.pipeline import publish\nfrom pymxs import runtime as rt\nfrom openpype.hosts.max.api import maintained_selection\n\n\nclass ExtractRedshiftProxy(publish.Extractor):\n    \"\"\"\n    Extract Redshift Proxy with rsProxy\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.1\n    label = \"Extract RedShift Proxy\"\n    hosts = [\"max\"]\n    families = [\"redshiftproxy\"]\n\n    def process(self, instance):\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n\n        self.log.debug(\"Extracting Redshift Proxy...\")\n        stagingdir = self.staging_dir(instance)\n        rs_filename = \"{name}.rs\".format(**instance.data)\n        rs_filepath = os.path.join(stagingdir, rs_filename)\n        rs_filepath = rs_filepath.replace(\"\\\\\", \"/\")\n\n        rs_filenames = self.get_rsfiles(instance, start, end)\n\n        with maintained_selection():\n            # select and export\n            node_list = instance.data[\"members\"]\n            rt.Select(node_list)\n            # Redshift rsProxy command\n            # rsProxy fp selected compress connectivity startFrame endFrame\n            # camera warnExisting transformPivotToOrigin\n            rt.rsProxy(rs_filepath, 1, 0, 0, start, end, 0, 1, 1)\n\n        self.log.info(\"Performing Extraction ...\")\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'rs',\n            'ext': 'rs',\n            'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0],    # noqa\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(representation)\n        self.log.info(\"Extracted instance '%s' to: %s\" % (instance.name,\n                                                          stagingdir))\n\n    def get_rsfiles(self, instance, startFrame, endFrame):\n        rs_filenames = []\n        rs_name = instance.data[\"name\"]\n        for frame in range(startFrame, endFrame + 1):\n            rs_filename = \"%s.%04d.rs\" % (rs_name, frame)\n            rs_filenames.append(rs_filename)\n\n        return rs_filenames\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/extract_review_animation.py",
    "content": "import os\nimport pyblish.api\nfrom openpype.pipeline import publish\nfrom openpype.hosts.max.api.preview_animation import (\n    render_preview_animation\n)\n\n\nclass ExtractReviewAnimation(publish.Extractor):\n    \"\"\"\n    Extract Review by Review Animation\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder + 0.001\n    label = \"Extract Review Animation\"\n    hosts = [\"max\"]\n    families = [\"review\"]\n\n    def process(self, instance):\n        staging_dir = self.staging_dir(instance)\n        ext = instance.data.get(\"imageFormat\")\n        start = int(instance.data[\"frameStart\"])\n        end = int(instance.data[\"frameEnd\"])\n        filepath = os.path.join(staging_dir, instance.name)\n        self.log.debug(\n            \"Writing Review Animation to '{}'\".format(filepath))\n\n        review_camera = instance.data[\"review_camera\"]\n        viewport_options = instance.data.get(\"viewport_options\", {})\n        files = render_preview_animation(\n            filepath,\n            ext,\n            review_camera,\n            start,\n            end,\n            percentSize=instance.data[\"percentSize\"],\n            width=instance.data[\"review_width\"],\n            height=instance.data[\"review_height\"],\n            viewport_options=viewport_options)\n\n        filenames = [os.path.basename(path) for path in files]\n\n        tags = [\"review\"]\n        if not instance.data.get(\"keepImages\"):\n            tags.append(\"delete\")\n\n        self.log.debug(\"Performing Extraction ...\")\n\n        representation = {\n            \"name\": instance.data[\"imageFormat\"],\n            \"ext\": instance.data[\"imageFormat\"],\n            \"files\": filenames,\n            \"stagingDir\": staging_dir,\n            \"frameStart\": instance.data[\"frameStartHandle\"],\n            \"frameEnd\": instance.data[\"frameEndHandle\"],\n            \"tags\": tags,\n            \"preview\": True,\n            \"camera_name\": review_camera\n        }\n        self.log.debug(f\"{representation}\")\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/extract_thumbnail.py",
    "content": "import os\nimport pyblish.api\nfrom openpype.pipeline import publish\nfrom openpype.hosts.max.api.preview_animation import render_preview_animation\n\n\nclass ExtractThumbnail(publish.Extractor):\n    \"\"\"Extract Thumbnail for Review\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract Thumbnail\"\n    hosts = [\"max\"]\n    families = [\"review\"]\n\n    def process(self, instance):\n        ext = instance.data.get(\"imageFormat\")\n        frame = int(instance.data[\"frameStart\"])\n        staging_dir = self.staging_dir(instance)\n        filepath = os.path.join(\n            staging_dir, f\"{instance.name}_thumbnail\")\n        self.log.debug(\"Writing Thumbnail to '{}'\".format(filepath))\n\n        review_camera = instance.data[\"review_camera\"]\n        viewport_options = instance.data.get(\"viewport_options\", {})\n        files = render_preview_animation(\n            filepath,\n            ext,\n            review_camera,\n            start_frame=frame,\n            end_frame=frame,\n            percentSize=instance.data[\"percentSize\"],\n            width=instance.data[\"review_width\"],\n            height=instance.data[\"review_height\"],\n            viewport_options=viewport_options)\n\n        thumbnail = next(os.path.basename(path) for path in files)\n\n        representation = {\n            \"name\": \"thumbnail\",\n            \"ext\": ext,\n            \"files\": thumbnail,\n            \"stagingDir\": staging_dir,\n            \"thumbnail\": True\n        }\n\n        self.log.debug(f\"{representation}\")\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/extract_tycache.py",
    "content": "import os\n\nimport pyblish.api\nfrom pymxs import runtime as rt\n\nfrom openpype.hosts.max.api import maintained_selection\nfrom openpype.pipeline import publish\n\n\nclass ExtractTyCache(publish.Extractor):\n    \"\"\"Extract tycache format with tyFlow operators.\n    Notes:\n        - TyCache only works for TyFlow Pro Plugin.\n\n    Methods:\n        self.get_export_particles_job_args(): sets up all job arguments\n            for attributes to be exported in MAXscript\n\n        self.get_operators(): get the export_particle operator\n\n        self.get_files(): get the files with tyFlow naming convention\n            before publishing\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.2\n    label = \"Extract TyCache\"\n    hosts = [\"max\"]\n    families = [\"tycache\"]\n\n    def process(self, instance):\n        # TODO: let user decide the param\n        start = int(instance.context.data[\"frameStart\"])\n        end = int(instance.context.data.get(\"frameEnd\"))\n        self.log.debug(\"Extracting Tycache...\")\n\n        stagingdir = self.staging_dir(instance)\n        filename = \"{name}.tyc\".format(**instance.data)\n        path = os.path.join(stagingdir, filename)\n        filenames = self.get_files(instance, start, end)\n        additional_attributes = instance.data.get(\"tyc_attrs\", {})\n\n        with maintained_selection():\n            job_args = self.get_export_particles_job_args(\n                instance.data[\"members\"],\n                start, end, path,\n                additional_attributes)\n            for job in job_args:\n                rt.Execute(job)\n        representations = instance.data.setdefault(\"representations\", [])\n        representation = {\n            'name': 'tyc',\n            'ext': 'tyc',\n            'files': filenames if len(filenames) > 1 else filenames[0],\n            \"stagingDir\": stagingdir,\n        }\n        representations.append(representation)\n\n        # Get the tyMesh filename for extraction\n        mesh_filename = f\"{instance.name}__tyMesh.tyc\"\n        mesh_repres = {\n            'name': 'tyMesh',\n            'ext': 'tyc',\n            'files': mesh_filename,\n            \"stagingDir\": stagingdir,\n            \"outputName\": '__tyMesh'\n        }\n        representations.append(mesh_repres)\n\n        # Get the material filename of which assigned in\n        # tyCache for extraction\n        material_filename = f\"{instance.name}__tyMtl.mat\"\n        full_material_name = os.path.join(stagingdir, material_filename)\n        full_material_name = full_material_name.replace(\"\\\\\", \"/\")\n        if os.path.exists(full_material_name):\n            mateiral_repres = {\n                \"name\": 'tyMtl',\n                \"ext\": 'mat',\n                'files': material_filename,\n                'stagingDir': stagingdir,\n                \"outputName\": '__tyMtl'\n            }\n            representations.append(mateiral_repres)\n\n        self.log.debug(f\"Extracted instance '{instance.name}' to: {filenames}\")\n\n    def get_files(self, instance, start_frame, end_frame):\n        \"\"\"Get file names for tyFlow in tyCache format.\n\n        Set the filenames accordingly to the tyCache file\n        naming extension(.tyc) for the publishing purpose\n\n        Actual File Output from tyFlow in tyCache format:\n        <InstanceName>__tyPart_<frame>.tyc\n\n        e.g. tycacheMain__tyPart_00000.tyc\n\n        Args:\n            instance (pyblish.api.Instance): instance.\n            start_frame (int): Start frame.\n            end_frame (int): End frame.\n\n        Returns:\n            filenames(list): list of filenames\n\n        \"\"\"\n        filenames = []\n        for frame in range(int(start_frame), int(end_frame) + 1):\n            filename = f\"{instance.name}__tyPart_{frame:05}.tyc\"\n            filenames.append(filename)\n        return filenames\n\n    def get_export_particles_job_args(self, members, start, end,\n                                      filepath, additional_attributes):\n        \"\"\"Sets up all job arguments for attributes.\n\n        Those attributes are to be exported in MAX Script.\n\n        Args:\n            members (list): Member nodes of the instance.\n            start (int): Start frame.\n            end (int): End frame.\n            filepath (str): Output path of the TyCache file.\n            additional_attributes (dict): channel attributes data\n                which needed to be exported\n\n        Returns:\n            list of arguments for MAX Script.\n\n        \"\"\"\n        settings = {\n            \"exportMode\": 2,\n            \"frameStart\": start,\n            \"frameEnd\": end,\n            \"tyCacheFilename\": filepath.replace(\"\\\\\", \"/\")\n        }\n        settings.update(additional_attributes)\n\n        job_args = []\n        for operator in self.get_operators(members):\n            for key, value in settings.items():\n                if isinstance(value, str):\n                    # embed in quotes\n                    value = f'\"{value}\"'\n\n                job_args.append(f\"{operator}.{key}={value}\")\n            job_args.append(f\"{operator}.exportTyCache()\")\n        return job_args\n\n    @staticmethod\n    def get_operators(members):\n        \"\"\"Get Export Particles Operator.\n\n        Args:\n            members (list): Instance members.\n\n        Returns:\n            list of particle operators\n\n        \"\"\"\n        opt_list = []\n        for member in members:\n            obj = member.baseobject\n            # TODO: see if it can use maxscript instead\n            anim_names = rt.GetSubAnimNames(obj)\n            for anim_name in anim_names:\n                sub_anim = rt.GetSubAnim(obj, anim_name)\n                boolean = rt.IsProperty(sub_anim, \"Export_Particles\")\n                if boolean:\n                    event_name = sub_anim.Name\n                    opt = f\"${member.Name}.{event_name}.export_particles\"\n                    opt_list.append(opt)\n\n        return opt_list\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/increment_workfile_version.py",
    "content": "import pyblish.api\nfrom openpype.lib import version_up\nfrom pymxs import runtime as rt\n\n\nclass IncrementWorkfileVersion(pyblish.api.ContextPlugin):\n    \"\"\"Increment current workfile version.\"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.9\n    label = \"Increment Workfile Version\"\n    hosts = [\"max\"]\n    families = [\"workfile\"]\n\n    def process(self, context):\n        path = context.data[\"currentFile\"]\n        filepath = version_up(path)\n\n        rt.saveMaxFile(filepath)\n        self.log.info(\"Incrementing file version\")\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/save_scene.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import registered_host\n\n\nclass SaveCurrentScene(pyblish.api.ContextPlugin):\n    \"\"\"Save current scene\"\"\"\n\n    label = \"Save current file\"\n    order = pyblish.api.ExtractorOrder - 0.49\n    hosts = [\"max\"]\n    families = [\"maxrender\", \"workfile\"]\n\n    def process(self, context):\n        host = registered_host()\n        current_file = host.get_current_workfile()\n\n        assert context.data[\"currentFile\"] == current_file\n\n        if host.workfile_has_unsaved_changes():\n            self.log.info(f\"Saving current file: {current_file}\")\n            host.save_workfile(current_file)\n        else:\n            self.log.debug(\"No unsaved changes, skipping file save..\")\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py",
    "content": "import pyblish.api\nimport os\nimport sys\nimport tempfile\n\nfrom pymxs import runtime as rt\nfrom openpype.lib import run_subprocess\nfrom openpype.hosts.max.api.lib_rendersettings import RenderSettings\nfrom openpype.hosts.max.api.lib_renderproducts import RenderProducts\n\n\nclass SaveScenesForCamera(pyblish.api.InstancePlugin):\n    \"\"\"Save scene files for multiple cameras without\n    editing the original scene before deadline submission\n\n    \"\"\"\n\n    label = \"Save Scene files for cameras\"\n    order = pyblish.api.ExtractorOrder - 0.48\n    hosts = [\"max\"]\n    families = [\"maxrender\"]\n\n    def process(self, instance):\n        if not instance.data.get(\"multiCamera\"):\n            self.log.debug(\n                \"Multi Camera disabled. \"\n                \"Skipping to save scene files for cameras\")\n            return\n        current_folder = rt.maxFilePath\n        current_filename = rt.maxFileName\n        current_filepath = os.path.join(current_folder, current_filename)\n        camera_scene_files = []\n        scripts = []\n        filename, ext = os.path.splitext(current_filename)\n        fmt = RenderProducts().image_format()\n        cameras = instance.data.get(\"cameras\")\n        if not cameras:\n            return\n        new_folder = f\"{current_folder}_{filename}\"\n        os.makedirs(new_folder, exist_ok=True)\n        for camera in cameras:\n            new_output = RenderSettings().get_batch_render_output(camera)       # noqa\n            new_output = new_output.replace(\"\\\\\", \"/\")\n            new_filename = f\"{filename}_{camera}{ext}\"\n            new_filepath = os.path.join(new_folder, new_filename)\n            new_filepath = new_filepath.replace(\"\\\\\", \"/\")\n            camera_scene_files.append(new_filepath)\n            RenderSettings().batch_render_elements(camera)\n            rt.rendOutputFilename = new_output\n            rt.saveMaxFile(current_filepath)\n            script = (\"\"\"\nfrom pymxs import runtime as rt\nimport os\nfilename = \"{filename}\"\nnew_filepath = \"{new_filepath}\"\nnew_output = \"{new_output}\"\ncamera = \"{camera}\"\nrt.rendOutputFilename = new_output\ndirectory = os.path.dirname(rt.rendOutputFilename)\ndirectory = os.path.join(directory, filename)\nrender_elem = rt.maxOps.GetCurRenderElementMgr()\nrender_elem_num = render_elem.NumRenderElements()\nif render_elem_num > 0:\n    ext = \"{ext}\"\n    for i in range(render_elem_num):\n        renderlayer_name = render_elem.GetRenderElement(i)\n        target, renderpass = str(renderlayer_name).split(\":\")\n        aov_name =  f\"{{directory}}_{camera}_{{renderpass}}..{ext}\"\n        render_elem.SetRenderElementFileName(i, aov_name)\nrt.saveMaxFile(new_filepath)\n        \"\"\").format(filename=instance.name,\n                    new_filepath=new_filepath,\n                    new_output=new_output,\n                    camera=camera,\n                    ext=fmt)\n            scripts.append(script)\n\n        maxbatch_exe = os.path.join(\n            os.path.dirname(sys.executable), \"3dsmaxbatch\")\n        maxbatch_exe = maxbatch_exe.replace(\"\\\\\", \"/\")\n        if sys.platform == \"windows\":\n            maxbatch_exe += \".exe\"\n            maxbatch_exe = os.path.normpath(maxbatch_exe)\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            tmp_script_path = os.path.join(\n                tmp_dir_name, \"extract_scene_files.py\")\n            self.log.info(\"Using script file: {}\".format(tmp_script_path))\n\n            with open(tmp_script_path, \"wt\") as tmp:\n                for script in scripts:\n                    tmp.write(script + \"\\n\")\n\n            try:\n                current_filepath = current_filepath.replace(\"\\\\\", \"/\")\n                tmp_script_path = tmp_script_path.replace(\"\\\\\", \"/\")\n                run_subprocess([maxbatch_exe, tmp_script_path,\n                                \"-sceneFile\", current_filepath])\n            except RuntimeError:\n                self.log.debug(\"Checking the scene files existing\")\n\n        for camera_scene in camera_scene_files:\n            if not os.path.exists(camera_scene):\n                self.log.error(\"Camera scene files not existed yet!\")\n                raise RuntimeError(\"MaxBatch.exe doesn't run as expected\")\n            self.log.debug(f\"Found Camera scene:{camera_scene}\")\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_attributes.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validator for Attributes.\"\"\"\nfrom pyblish.api import ContextPlugin, ValidatorOrder\nfrom pymxs import runtime as rt\n\nfrom openpype.pipeline.publish import (\n    OptionalPyblishPluginMixin,\n    PublishValidationError,\n    RepairContextAction\n)\n\n\ndef has_property(object_name, property_name):\n    \"\"\"Return whether an object has a property with given name\"\"\"\n    return rt.Execute(f'isProperty {object_name} \"{property_name}\"')\n\n\ndef is_matching_value(object_name, property_name, value):\n    \"\"\"Return whether an existing property matches value `value\"\"\"\n    property_value = rt.Execute(f\"{object_name}.{property_name}\")\n\n    # Wrap property value if value is a string valued attributes\n    # starting with a `#`\n    if (\n        isinstance(value, str) and\n        value.startswith(\"#\") and\n        not value.endswith(\")\")\n    ):\n        # prefix value with `#`\n        # not applicable for #() array value type\n        # and only applicable for enum i.e. #bob, #sally\n        property_value = f\"#{property_value}\"\n\n    return property_value == value\n\n\nclass ValidateAttributes(OptionalPyblishPluginMixin,\n                         ContextPlugin):\n    \"\"\"Validates attributes in the project setting are consistent\n    with the nodes from MaxWrapper Class in 3ds max.\n    E.g. \"renderers.current.separateAovFiles\",\n         \"renderers.production.PrimaryGIEngine\"\n    Admin(s) need to put the dict below and enable this validator for a check:\n    {\n       \"renderers.current\":{\n            \"separateAovFiles\" : True\n        },\n        \"renderers.production\":{\n            \"PrimaryGIEngine\": \"#RS_GIENGINE_BRUTE_FORCE\"\n        }\n        ....\n    }\n\n    \"\"\"\n\n    order = ValidatorOrder\n    hosts = [\"max\"]\n    label = \"Attributes\"\n    actions = [RepairContextAction]\n    optional = True\n\n    @classmethod\n    def get_invalid(cls, context):\n        attributes = (\n            context.data[\"project_settings\"][\"max\"][\"publish\"]\n                        [\"ValidateAttributes\"][\"attributes\"]\n        )\n        if not attributes:\n            return\n        invalid = []\n        for object_name, required_properties in attributes.items():\n            if not rt.Execute(f\"isValidValue {object_name}\"):\n                # Skip checking if the node does not\n                # exist in MaxWrapper Class\n                cls.log.debug(f\"Unable to find '{object_name}'.\"\n                              \" Skipping validation of attributes.\")\n                continue\n\n            for property_name, value in required_properties.items():\n                if not has_property(object_name, property_name):\n                    cls.log.error(\n                        \"Non-existing property: \"\n                        f\"{object_name}.{property_name}\")\n                    invalid.append((object_name, property_name))\n\n                if not is_matching_value(object_name, property_name, value):\n                    cls.log.error(\n                        f\"Invalid value for: {object_name}.{property_name}\"\n                        f\" should be: {value}\")\n                    invalid.append((object_name, property_name))\n\n        return invalid\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            self.log.debug(\"Skipping Validate Attributes...\")\n            return\n        invalid_attributes = self.get_invalid(context)\n        if invalid_attributes:\n            bullet_point_invalid_statement = \"\\n\".join(\n                \"- {}\".format(invalid) for invalid\n                in invalid_attributes\n            )\n            report = (\n                \"Required Attribute(s) have invalid value(s).\\n\\n\"\n                f\"{bullet_point_invalid_statement}\\n\\n\"\n                \"You can use repair action to fix them if they are not\\n\"\n                \"unknown property value(s).\"\n            )\n            raise PublishValidationError(\n                report, title=\"Invalid Value(s) for Required Attribute(s)\")\n\n    @classmethod\n    def repair(cls, context):\n        attributes = (\n            context.data[\"project_settings\"][\"max\"][\"publish\"]\n                        [\"ValidateAttributes\"][\"attributes\"]\n        )\n        invalid_attributes = cls.get_invalid(context)\n        for attrs in invalid_attributes:\n            prop, attr = attrs\n            value = attributes[prop][attr]\n            if isinstance(value, str) and not value.startswith(\"#\"):\n                attribute_fix = '{}.{}=\"{}\"'.format(\n                    prop, attr, value\n                )\n            else:\n                attribute_fix = \"{}.{}={}\".format(\n                    prop, attr, value\n                )\n            rt.Execute(attribute_fix)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_camera_attributes.py",
    "content": "import pyblish.api\nfrom pymxs import runtime as rt\n\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\nfrom openpype.hosts.max.api.action import SelectInvalidAction\n\n\nclass ValidateCameraAttributes(OptionalPyblishPluginMixin,\n                               pyblish.api.InstancePlugin):\n    \"\"\"Validates Camera has no invalid attribute properties\n    or values.(For 3dsMax Cameras only)\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = ['camera']\n    hosts = ['max']\n    label = 'Validate Camera Attributes'\n    actions = [SelectInvalidAction, RepairAction]\n    optional = True\n\n    DEFAULTS = [\"fov\", \"nearrange\", \"farrange\",\n                \"nearclip\", \"farclip\"]\n    CAM_TYPE = [\"Freecamera\", \"Targetcamera\",\n                \"Physical\"]\n\n    @classmethod\n    def get_invalid(cls, instance):\n        invalid = []\n        if rt.units.DisplayType != rt.Name(\"Generic\"):\n            cls.log.warning(\n                \"Generic Type is not used as a scene unit\\n\\n\"\n                \"sure you tweak the settings with your own values\\n\\n\"\n                \"before validation.\")\n        cameras = instance.data[\"members\"]\n        project_settings = instance.context.data[\"project_settings\"].get(\"max\")\n        cam_attr_settings = (\n            project_settings[\"publish\"][\"ValidateCameraAttributes\"]\n        )\n        for camera in cameras:\n            if str(rt.ClassOf(camera)) not in cls.CAM_TYPE:\n                cls.log.debug(\n                    \"Skipping camera created from external plugin..\")\n                continue\n            for attr in cls.DEFAULTS:\n                default_value = cam_attr_settings.get(attr)\n                if default_value == float(0):\n                    cls.log.debug(\n                        f\"the value of {attr} in setting set to\"\n                        \" zero. Skipping the check.\")\n                    continue\n                if round(rt.getProperty(camera, attr), 1) != default_value:\n                    cls.log.error(\n                        f\"Invalid attribute value for {camera.name}:{attr} \"\n                        f\"(should be: {default_value}))\")\n                    invalid.append(camera)\n\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            self.log.debug(\"Skipping Validate Camera Attributes.\")\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\n                \"Invalid camera attributes found. See log.\")\n\n    @classmethod\n    def repair(cls, instance):\n        invalid_cameras = cls.get_invalid(instance)\n        project_settings = instance.context.data[\"project_settings\"].get(\"max\")\n        cam_attr_settings = (\n            project_settings[\"publish\"][\"ValidateCameraAttributes\"]\n        )\n        for camera in invalid_cameras:\n            for attr in cls.DEFAULTS:\n                expected_value = cam_attr_settings.get(attr)\n                if expected_value == float(0):\n                    cls.log.debug(\n                        f\"the value of {attr} in setting set to zero.\")\n                    continue\n                rt.setProperty(camera, attr, expected_value)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_camera_contents.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\nfrom pymxs import runtime as rt\n\n\nclass ValidateCameraContent(pyblish.api.InstancePlugin):\n    \"\"\"Validates Camera instance contents.\n\n    A Camera instance may only hold a SINGLE camera's transform\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"camera\", \"review\"]\n    hosts = [\"max\"]\n    label = \"Camera Contents\"\n    camera_type = [\"$Free_Camera\", \"$Target_Camera\",\n                   \"$Physical_Camera\", \"$Target\"]\n\n    def process(self, instance):\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError((\"Camera instance must only include\"\n                                          \"camera (and camera target). \"\n                                          f\"Invalid content {invalid}\"))\n\n    def get_invalid(self, instance):\n        \"\"\"\n        Get invalid nodes if the instance is not camera\n        \"\"\"\n        invalid = []\n        container = instance.data[\"instance_node\"]\n        self.log.info(f\"Validating camera content for {container}\")\n\n        selection_list = instance.data[\"members\"]\n        for sel in selection_list:\n            # to avoid Attribute Error from pymxs wrapper\n            sel_tmp = str(sel)\n            found = any(sel_tmp.startswith(cam) for cam in self.camera_type)\n            if not found:\n                self.log.error(\"Camera not found\")\n                invalid.append(sel)\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_frame_range.py",
    "content": "import pyblish.api\n\nfrom pymxs import runtime as rt\nfrom openpype.pipeline import (\n    OptionalPyblishPluginMixin\n)\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    KnownPublishError\n)\nfrom openpype.hosts.max.api.lib import get_frame_range, set_timeline\n\n\nclass ValidateFrameRange(pyblish.api.InstancePlugin,\n                         OptionalPyblishPluginMixin):\n    \"\"\"Validates the frame ranges.\n\n    This is an optional validator checking if the frame range on instance\n    matches the frame range specified for the asset.\n\n    It also validates render frame ranges of render layers.\n\n    Repair action will change everything to match the asset frame range.\n\n    This can be turned off by the artist to allow custom ranges.\n    \"\"\"\n\n    label = \"Validate Frame Range\"\n    order = ValidateContentsOrder\n    families = [\"camera\", \"maxrender\",\n                \"pointcache\", \"pointcloud\",\n                \"review\", \"redshiftproxy\"]\n    hosts = [\"max\"]\n    optional = True\n    actions = [RepairAction]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            self.log.debug(\"Skipping Validate Frame Range...\")\n            return\n\n        frame_range = get_frame_range(\n            asset_doc=instance.data[\"assetEntity\"])\n\n        inst_frame_start = instance.data.get(\"frameStartHandle\")\n        inst_frame_end = instance.data.get(\"frameEndHandle\")\n        if inst_frame_start is None or inst_frame_end is None:\n            raise KnownPublishError(\n                \"Missing frame start and frame end on \"\n                \"instance to to validate.\"\n            )\n        frame_start_handle = frame_range[\"frameStartHandle\"]\n        frame_end_handle = frame_range[\"frameEndHandle\"]\n        errors = []\n        if frame_start_handle != inst_frame_start:\n            errors.append(\n                f\"Start frame ({inst_frame_start}) on instance does not match \" # noqa\n                f\"with the start frame ({frame_start_handle}) set on the asset data. \")    # noqa\n        if frame_end_handle != inst_frame_end:\n            errors.append(\n                f\"End frame ({inst_frame_end}) on instance does not match \"\n                f\"with the end frame ({frame_end_handle}) \"\n                \"from the asset data. \")\n\n        if errors:\n            bullet_point_errors = \"\\n\".join(\n                \"- {}\".format(error) for error in errors\n            )\n            report = (\n                \"Frame range settings are incorrect.\\n\\n\"\n                f\"{bullet_point_errors}\\n\\n\"\n                \"You can use repair action to fix it.\"\n            )\n            raise PublishValidationError(report, title=\"Frame Range incorrect\")\n\n    @classmethod\n    def repair(cls, instance):\n        frame_range = get_frame_range()\n        frame_start_handle = frame_range[\"frameStartHandle\"]\n        frame_end_handle = frame_range[\"frameEndHandle\"]\n\n        if instance.data[\"family\"] == \"maxrender\":\n            rt.rendStart = frame_start_handle\n            rt.rendEnd = frame_end_handle\n        else:\n            set_timeline(frame_start_handle, frame_end_handle)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_instance_has_members.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateInstanceHasMembers(pyblish.api.InstancePlugin):\n    \"\"\"Validates Instance has members.\n\n    Check if MaxScene containers includes any contents underneath.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"camera\",\n                \"model\",\n                \"maxScene\",\n                \"review\",\n                \"pointcache\",\n                \"pointcloud\",\n                \"redshiftproxy\"]\n    hosts = [\"max\"]\n    label = \"Container Contents\"\n\n    def process(self, instance):\n        if not instance.data[\"members\"]:\n            raise PublishValidationError(\"No content found in the container\")\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_instance_in_context.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validate if instance asset is the same as context asset.\"\"\"\nfrom __future__ import absolute_import\n\nimport pyblish.api\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.hosts.max.api.action import SelectInvalidAction\nfrom pymxs import runtime as rt\n\n\nclass ValidateInstanceInContext(pyblish.api.InstancePlugin,\n                                OptionalPyblishPluginMixin):\n    \"\"\"Validator to check if instance asset match context asset.\n\n    When working in per-shot style you always publish data in context of\n    current asset (shot). This validator checks if this is so. It is optional\n    so it can be disabled when needed.\n\n    Action on this validator will select invalid instances.\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Instance in same Context\"\n    optional = True\n    hosts = [\"max\"]\n    actions = [SelectInvalidAction, RepairAction]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        instance_node = rt.getNodeByName(instance.data.get(\n            \"instance_node\", \"\"))\n        if not instance_node:\n            return\n        asset_name_attr = \"folderPath\" if AYON_SERVER_ENABLED else \"asset\"\n        asset = rt.getUserProp(instance_node, asset_name_attr)\n        context_asset = self.get_context_asset(instance)\n        task = rt.getUserProp(instance_node, \"task\")\n        context_task = self.get_context_task(instance)\n        if asset != context_asset:\n            raise PublishValidationError(\n                message=(\n                    \"Instance '{}' publishes to different asset than current \"\n                    \"context: {}. Current context: {}\".format(\n                        instance.name, asset, context_asset\n                    )\n                ),\n                description=(\n                    \"## Publishing to a different asset\\n\"\n                    \"There are publish instances present which are publishing \"\n                    \"into a different asset than your current context.\\n\\n\"\n                    \"Usually this is not what you want but there can be cases \"\n                    \"where you might want to publish into another asset or \"\n                    \"shot. If that's the case you can disable the validation \"\n                    \"on the instance to ignore it.\"\n                )\n            )\n        if task != context_task:\n            raise PublishValidationError(\n                message=(\n                    \"Instance '{}' publishes to different task than current \"\n                    \"context: {}. Current context: {}\".format(\n                        instance.name, task, context_task\n                    )\n                ),\n                description=(\n                    \"## Publishing to a different asset\\n\"\n                    \"There are publish instances present which are publishing \"\n                    \"into a different asset than your current context.\\n\\n\"\n                    \"Usually this is not what you want but there can be cases \"\n                    \"where you might want to publish into another asset or \"\n                    \"shot. If that's the case you can disable the validation \"\n                    \"on the instance to ignore it.\"\n                )\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n        asset_name_attr = \"folderPath\" if AYON_SERVER_ENABLED else \"asset\"\n        node = rt.getNodeByName(instance.data[\"instance_node\"])\n        asset = rt.getUserProp(node, asset_name_attr)\n        context_asset = cls.get_context_asset(instance)\n        if asset != context_asset:\n            return instance.data[\"instance_node\"]\n\n    @classmethod\n    def repair(cls, instance):\n        context_asset = cls.get_context_asset(instance)\n        context_task = cls.get_context_task(instance)\n        instance_node = rt.getNodeByName(instance.data.get(\n            \"instance_node\", \"\"))\n        if not instance_node:\n            return\n        asset_name_attr = \"folderPath\" if AYON_SERVER_ENABLED else \"asset\"\n        rt.SetUserProp(instance_node, asset_name_attr, context_asset)\n        rt.SetUserProp(instance_node, \"task\", context_task)\n\n    @staticmethod\n    def get_context_asset(instance):\n        return instance.context.data[\"asset\"]\n\n    @staticmethod\n    def get_context_task(instance):\n        return instance.context.data[\"task\"]\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_loaded_plugin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validator for Loaded Plugin.\"\"\"\nimport os\nimport pyblish.api\nfrom pymxs import runtime as rt\n\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\nfrom openpype.hosts.max.api.lib import get_plugins\n\n\nclass ValidateLoadedPlugin(OptionalPyblishPluginMixin,\n                           pyblish.api.InstancePlugin):\n    \"\"\"Validates if the specific plugin is loaded in 3ds max.\n    Studio Admin(s) can add the plugins they want to check in validation\n    via studio defined project settings\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    hosts = [\"max\"]\n    label = \"Validate Loaded Plugins\"\n    optional = True\n    actions = [RepairAction]\n\n    family_plugins_mapping = {}\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        family_plugins_mapping = cls.family_plugins_mapping\n        if not family_plugins_mapping:\n            return\n\n        invalid = []\n        # Find all plug-in requirements for current instance\n        instance_families = {instance.data[\"family\"]}\n        instance_families.update(instance.data.get(\"families\", []))\n        cls.log.debug(\"Checking plug-in validation \"\n                      f\"for instance families: {instance_families}\")\n        all_required_plugins = set()\n\n        for mapping in family_plugins_mapping:\n            # Check for matching families\n            if not mapping:\n                return\n\n            match_families = {fam.strip() for fam in mapping[\"families\"]}\n            has_match = \"*\" in match_families or match_families.intersection(\n                instance_families)\n\n            if not has_match:\n                continue\n\n            cls.log.debug(\n                f\"Found plug-in family requirements: {match_families}\")\n            required_plugins = [\n                # match lowercase and format with os.environ to allow\n                # plugin names defined by max version, e.g. {3DSMAX_VERSION}\n                plugin.format(**os.environ).lower()\n                for plugin in mapping[\"plugins\"]\n                # ignore empty fields in settings\n                if plugin.strip()\n            ]\n\n            all_required_plugins.update(required_plugins)\n\n        if not all_required_plugins:\n            # Instance has no plug-in requirements\n            return\n\n        # get all DLL loaded plugins in Max and their plugin index\n        available_plugins = {\n            plugin_name.lower(): index for index, plugin_name in enumerate(\n                get_plugins())\n        }\n        # validate the required plug-ins\n        for plugin in sorted(all_required_plugins):\n            plugin_index = available_plugins.get(plugin)\n            if plugin_index is None:\n                debug_msg = (\n                    f\"Plugin {plugin} does not exist\"\n                    \" in 3dsMax Plugin List.\"\n                )\n                invalid.append((plugin, debug_msg))\n                continue\n            if not rt.pluginManager.isPluginDllLoaded(plugin_index):\n                debug_msg = f\"Plugin {plugin} not loaded.\"\n                invalid.append((plugin, debug_msg))\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            self.log.debug(\"Skipping Validate Loaded Plugin...\")\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            bullet_point_invalid_statement = \"\\n\".join(\n                \"- {}\".format(message) for _, message in invalid\n            )\n            report = (\n                \"Required plugins are not loaded.\\n\\n\"\n                f\"{bullet_point_invalid_statement}\\n\\n\"\n                \"You can use repair action to load the plugin.\"\n            )\n            raise PublishValidationError(\n                report, title=\"Missing Required Plugins\")\n\n    @classmethod\n    def repair(cls, instance):\n        # get all DLL loaded plugins in Max and their plugin index\n        invalid = cls.get_invalid(instance)\n        if not invalid:\n            return\n\n        # get all DLL loaded plugins in Max and their plugin index\n        available_plugins = {\n            plugin_name.lower(): index for index, plugin_name in enumerate(\n                get_plugins())\n        }\n\n        for invalid_plugin, _ in invalid:\n            plugin_index = available_plugins.get(invalid_plugin)\n\n            if plugin_index is None:\n                cls.log.warning(\n                    f\"Can't enable missing plugin: {invalid_plugin}\")\n                continue\n\n            if not rt.pluginManager.isPluginDllLoaded(plugin_index):\n                rt.pluginManager.loadPluginDll(plugin_index)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_model_contents.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom pymxs import runtime as rt\n\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateModelContent(pyblish.api.InstancePlugin):\n    \"\"\"Validates Model instance contents.\n\n    A model instance may only hold either geometry-related\n    object(excluding Shapes) or editable meshes.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"model\"]\n    hosts = [\"max\"]\n    label = \"Model Contents\"\n\n    def process(self, instance):\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError((\"Model instance must only include\"\n                                          \"Geometry and Editable Mesh. \"\n                                          f\"Invalid types on: {invalid}\"))\n\n    def get_invalid(self, instance):\n        \"\"\"\n        Get invalid nodes if the instance is not camera\n        \"\"\"\n        invalid = []\n        container = instance.data[\"instance_node\"]\n        self.log.info(f\"Validating model content for {container}\")\n\n        selection_list = instance.data[\"members\"]\n        for sel in selection_list:\n            if rt.ClassOf(sel) in rt.Camera.classes:\n                invalid.append(sel)\n            if rt.ClassOf(sel) in rt.Light.classes:\n                invalid.append(sel)\n            if rt.ClassOf(sel) in rt.Shape.classes:\n                invalid.append(sel)\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_pointcloud.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import PublishValidationError\nfrom pymxs import runtime as rt\n\n\nclass ValidatePointCloud(pyblish.api.InstancePlugin):\n    \"\"\"Validate that work file was saved.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"pointcloud\"]\n    hosts = [\"max\"]\n    label = \"Validate Point Cloud\"\n\n    def process(self, instance):\n        \"\"\"\n        Notes:\n            1. Validate if the export mode of Export Particle is at PRT format\n            2. Validate the partition count and range set as default value\n                Partition Count : 100\n                Partition Range : 1 to 1\n            3. Validate if the custom attribute(s) exist as parameter(s)\n                of export_particle operator\n\n        \"\"\"\n        report = []\n\n        if self.validate_export_mode(instance):\n            report.append(\"The export mode is not at PRT\")\n\n        if self.validate_partition_value(instance):\n            report.append((\"tyFlow Partition setting is \"\n                           \"not at the default value\"))\n\n        invalid_attribute = self.validate_custom_attribute(instance)\n        if invalid_attribute:\n            report.append((\"Custom Attribute not found \"\n                           f\":{invalid_attribute}\"))\n\n        if report:\n            raise PublishValidationError(f\"{report}\")\n\n    def validate_custom_attribute(self, instance):\n        invalid = []\n        container = instance.data[\"instance_node\"]\n        self.log.info(\n            f\"Validating tyFlow custom attributes for {container}\")\n\n        selection_list = instance.data[\"members\"]\n\n        project_settings = instance.context.data[\"project_settings\"]\n        attr_settings = project_settings[\"max\"][\"PointCloud\"][\"attribute\"]\n        for sel in selection_list:\n            obj = sel.baseobject\n            anim_names = rt.GetSubAnimNames(obj)\n            for anim_name in anim_names:\n                # get all the names of the related tyFlow nodes\n                sub_anim = rt.GetSubAnim(obj, anim_name)\n                if rt.IsProperty(sub_anim, \"Export_Particles\"):\n                    event_name = sub_anim.name\n                    opt = \"${0}.{1}.export_particles\".format(sel.name,\n                                                             event_name)\n                    for key, value in attr_settings.items():\n                        custom_attr = \"{0}.PRTChannels_{1}\".format(opt,\n                                                                   value)\n                        try:\n                            rt.Execute(custom_attr)\n                        except RuntimeError:\n                            invalid.append(key)\n\n        return invalid\n\n    def validate_partition_value(self, instance):\n        invalid = []\n        container = instance.data[\"instance_node\"]\n        self.log.info(\n            f\"Validating tyFlow partition value for {container}\")\n\n        selection_list = instance.data[\"members\"]\n        for sel in selection_list:\n            obj = sel.baseobject\n            anim_names = rt.GetSubAnimNames(obj)\n            for anim_name in anim_names:\n                # get all the names of the related tyFlow nodes\n                sub_anim = rt.GetSubAnim(obj, anim_name)\n                if rt.IsProperty(sub_anim, \"Export_Particles\"):\n                    event_name = sub_anim.name\n                    opt = \"${0}.{1}.export_particles\".format(sel.name,\n                                                             event_name)\n                    count = rt.Execute(f'{opt}.PRTPartitionsCount')\n                    if count != 100:\n                        invalid.append(count)\n                    start = rt.Execute(f'{opt}.PRTPartitionsFrom')\n                    if start != 1:\n                        invalid.append(start)\n                    end = rt.Execute(f'{opt}.PRTPartitionsTo')\n                    if end != 1:\n                        invalid.append(end)\n\n        return invalid\n\n    def validate_export_mode(self, instance):\n        invalid = []\n        container = instance.data[\"instance_node\"]\n        self.log.info(\n            f\"Validating tyFlow export mode for {container}\")\n\n        con = rt.GetNodeByName(container)\n        selection_list = list(con.Children)\n        for sel in selection_list:\n            obj = sel.baseobject\n            anim_names = rt.GetSubAnimNames(obj)\n            for anim_name in anim_names:\n                # get all the names of the related tyFlow nodes\n                sub_anim = rt.GetSubAnim(obj, anim_name)\n                # check if there is export particle operator\n                boolean = rt.IsProperty(sub_anim, \"Export_Particles\")\n                event_name = sub_anim.name\n                if boolean:\n                    opt = f\"${sel.name}.{event_name}.export_particles\"\n                    export_mode = rt.Execute(f'{opt}.exportMode')\n                    if export_mode != 1:\n                        invalid.append(export_mode)\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_renderable_camera.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin)\nfrom openpype.pipeline.publish import RepairAction\nfrom openpype.hosts.max.api.lib import get_current_renderer\n\nfrom pymxs import runtime as rt\n\n\nclass ValidateRenderableCamera(pyblish.api.InstancePlugin,\n                               OptionalPyblishPluginMixin):\n    \"\"\"Validates Renderable Camera\n\n    Check if the renderable camera used for rendering\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"maxrender\"]\n    hosts = [\"max\"]\n    label = \"Renderable Camera\"\n    optional = True\n    actions = [RepairAction]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        if not instance.data[\"cameras\"]:\n            raise PublishValidationError(\n                \"No renderable Camera found in scene.\"\n            )\n\n    @classmethod\n    def repair(cls, instance):\n\n        rt.viewport.setType(rt.Name(\"view_camera\"))\n        camera = rt.viewport.GetCamera()\n        cls.log.info(f\"Camera {camera} set as renderable camera\")\n        renderer_class = get_current_renderer()\n        renderer = str(renderer_class).split(\":\")[0]\n        if renderer == \"Arnold\":\n            arv = rt.MAXToAOps.ArnoldRenderView()\n            arv.setOption(\"Camera\", str(camera))\n            arv.close()\n        instance.data[\"cameras\"] = [camera.name]\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\nfrom pymxs import runtime as rt\nfrom openpype.pipeline.publish import RepairAction\nfrom openpype.hosts.max.api.lib import get_current_renderer\n\n\nclass ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin):\n    \"\"\"\n    Validates Redshift as the current renderer for creating\n    Redshift Proxy\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"redshiftproxy\"]\n    hosts = [\"max\"]\n    label = \"Redshift Renderer\"\n    actions = [RepairAction]\n\n    def process(self, instance):\n        invalid = self.get_redshift_renderer(instance)\n        if invalid:\n            raise PublishValidationError(\"Please install Redshift for 3dsMax\"\n                                         \" before using the Redshift proxy instance\")   # noqa\n        invalid = self.get_current_renderer(instance)\n        if invalid:\n            raise PublishValidationError(\"The Redshift proxy extraction\"\n                                         \"discontinued since the current renderer is not Redshift\")  # noqa\n\n    def get_redshift_renderer(self, instance):\n        invalid = list()\n        max_renderers_list = str(rt.RendererClass.classes)\n        if \"Redshift_Renderer\" not in max_renderers_list:\n            invalid.append(max_renderers_list)\n\n        return invalid\n\n    def get_current_renderer(self, instance):\n        invalid = list()\n        renderer_class = get_current_renderer()\n        current_renderer = str(renderer_class).split(\":\")[0]\n        if current_renderer != \"Redshift_Renderer\":\n            invalid.append(current_renderer)\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n        for Renderer in rt.RendererClass.classes:\n            renderer = Renderer()\n            if \"Redshift_Renderer\" in str(renderer):\n                rt.renderers.production = renderer\n                break\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_renderpasses.py",
    "content": "import os\nimport pyblish.api\nfrom pymxs import runtime as rt\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.hosts.max.api.lib_rendersettings import RenderSettings\n\n\nclass ValidateRenderPasses(OptionalPyblishPluginMixin,\n                           pyblish.api.InstancePlugin):\n    \"\"\"Validates Render Passes before Deadline Submission\n    \"\"\"\n\n    order = ValidateContentsOrder\n    families = [\"maxrender\"]\n    hosts = [\"max\"]\n    label = \"Validate Render Passes\"\n    optional = True\n    actions = [RepairAction]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            bullet_point_invalid_statement = \"\\n\".join(\n                f\"- {err_type}: {filepath}\" for err_type, filepath\n                in invalid\n            )\n            report = (\n                \"Invalid render passes found.\\n\\n\"\n                f\"{bullet_point_invalid_statement}\\n\\n\"\n                \"You can use repair action to fix the invalid filepath.\"\n            )\n            raise PublishValidationError(\n                report, title=\"Invalid Render Passes\")\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Function to get invalid beauty render outputs and\n        render elements.\n\n        1. Check Render Output Folder matches the name of\n           the current Max Scene, e.g.\n             The name of the current Max scene:\n               John_Doe.max\n             The expected render output directory:\n               {root[work]}/{project[name]}/{hierarchy}/{asset}/\n               work/{task[name]}/render/3dsmax/John_Doe/\n\n        2. Check image extension(s) of the render output(s)\n           matches the image format in OP/AYON setting, e.g.\n               The current image format in settings: png\n               The expected render outputs: John_Doe.png\n\n        3. Check filename of render element ends with the name of\n           render element from the 3dsMax Render Element Manager.\n           e.g. The name of render element: RsCryptomatte\n            The expected filename: {InstanceName}_RsCryptomatte.png\n\n        Args:\n            instance (pyblish.api.Instance): instance\n            filename (str): filename of the Max scene\n\n        Returns:\n            list: list of invalid filename which doesn't match\n                with the project name\n        \"\"\"\n        invalid = []\n        file = rt.maxFileName\n        filename, ext = os.path.splitext(file)\n        if filename not in rt.rendOutputFilename:\n            cls.log.error(\n                \"Render output folder must include \"\n                f\" the max scene name {filename} \"\n            )\n            invalid_folder_name = os.path.dirname(\n                rt.rendOutputFilename).replace(\n                    \"\\\\\", \"/\").split(\"/\")[-1]\n            invalid.append((\"Invalid Render Output Folder\",\n                            invalid_folder_name))\n        beauty_fname = os.path.basename(rt.rendOutputFilename)\n        beauty_name, ext = os.path.splitext(beauty_fname)\n        invalid_filenames = cls.get_invalid_filenames(\n            instance, beauty_name)\n        invalid.extend(invalid_filenames)\n        invalid_image_format = cls.get_invalid_image_format(\n            instance, ext.lstrip(\".\"))\n        invalid.extend(invalid_image_format)\n        renderer = instance.data[\"renderer\"]\n        if renderer in [\n            \"ART_Renderer\",\n            \"Redshift_Renderer\",\n            \"V_Ray_6_Hotfix_3\",\n            \"V_Ray_GPU_6_Hotfix_3\",\n            \"Default_Scanline_Renderer\",\n            \"Quicksilver_Hardware_Renderer\",\n        ]:\n            render_elem = rt.maxOps.GetCurRenderElementMgr()\n            render_elem_num = render_elem.NumRenderElements()\n            for i in range(render_elem_num):\n                renderlayer_name = render_elem.GetRenderElement(i)\n                renderpass = str(renderlayer_name).split(\":\")[-1]\n                rend_file = render_elem.GetRenderElementFilename(i)\n                if not rend_file:\n                    cls.log.error(\n                        f\"No filepath for render element {renderpass}\")\n                    invalid.append((f\"Invalid {renderpass}\",\n                                    \"No filepath\"))\n                rend_fname, ext = os.path.splitext(\n                    os.path.basename(rend_file))\n                invalid_filenames = cls.get_invalid_filenames(\n                    instance, rend_fname, renderpass=renderpass)\n                invalid.extend(invalid_filenames)\n                invalid_image_format = cls.get_invalid_image_format(\n                    instance, ext)\n                invalid.extend(invalid_image_format)\n        elif renderer == \"Arnold\":\n            cls.log.debug(\n                \"Renderpass validation does not support Arnold yet,\"\n                \" validation skipped...\")\n\n        return invalid\n\n    @classmethod\n    def get_invalid_filenames(cls, instance, file_name, renderpass=None):\n        \"\"\"Function to get invalid filenames from render outputs.\n\n        Args:\n            instance (pyblish.api.Instance): instance\n            file_name (str): name of the file\n            renderpass (str, optional): name of the renderpass.\n                Defaults to None.\n\n        Returns:\n            list: invalid filenames\n        \"\"\"\n        invalid = []\n        if instance.name not in file_name:\n            cls.log.error(\"The renderpass should have instance name inside.\")\n            invalid.append((f\"Invalid instance name\",\n                            file_name))\n        if renderpass is not None:\n            if not file_name.rstrip(\".\").endswith(renderpass):\n                cls.log.error(\n                    f\"Filename for {renderpass} should \"\n                    f\"end with {renderpass}\"\n                )\n                invalid.append((f\"Invalid {renderpass}\",\n                                os.path.basename(file_name)))\n        return invalid\n\n    @classmethod\n    def get_invalid_image_format(cls, instance, ext):\n        \"\"\"Function to check if the image format of the render outputs\n        aligns with that in the setting.\n\n        Args:\n            instance (pyblish.api.Instance): instance\n            ext (str): image extension\n\n        Returns:\n            list: list of files with invalid image format\n        \"\"\"\n        invalid = []\n        settings = instance.context.data[\"project_settings\"].get(\"max\")\n        image_format = settings[\"RenderSettings\"][\"image_format\"]\n        ext = ext.lstrip(\".\")\n        if ext != image_format:\n            msg = (\n                f\"Invalid image format {ext} for render outputs.\\n\"\n                f\"Should be: {image_format}\")\n            cls.log.error(msg)\n            invalid.append((msg, ext))\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n        container = instance.data.get(\"instance_node\")\n        # TODO: need to rename the function of render_output\n        RenderSettings().render_output(container)\n        cls.log.debug(\"Finished repairing the render output \"\n                      \"folder and filenames.\")\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_resolution_setting.py",
    "content": "import pyblish.api\nfrom pymxs import runtime as rt\nfrom openpype.pipeline import (\n    OptionalPyblishPluginMixin\n)\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    PublishValidationError\n)\nfrom openpype.hosts.max.api.lib import reset_scene_resolution\n\n\nclass ValidateResolutionSetting(pyblish.api.InstancePlugin,\n                                OptionalPyblishPluginMixin):\n    \"\"\"Validate the resolution setting aligned with DB\"\"\"\n\n    order = pyblish.api.ValidatorOrder - 0.01\n    families = [\"maxrender\"]\n    hosts = [\"max\"]\n    label = \"Validate Resolution Setting\"\n    optional = True\n    actions = [RepairAction]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        width, height = self.get_db_resolution(instance)\n        current_width = rt.renderWidth\n        current_height = rt.renderHeight\n        if current_width != width and current_height != height:\n            raise PublishValidationError(\"Resolution Setting \"\n                                         \"not matching resolution \"\n                                         \"set on asset or shot.\")\n        if current_width != width:\n            raise PublishValidationError(\"Width in Resolution Setting \"\n                                         \"not matching resolution set \"\n                                         \"on asset or shot.\")\n\n        if current_height != height:\n            raise PublishValidationError(\"Height in Resolution Setting \"\n                                         \"not matching resolution set \"\n                                         \"on asset or shot.\")\n\n    def get_db_resolution(self, instance):\n        asset_doc = instance.data[\"assetEntity\"]\n        project_doc = instance.context.data[\"projectEntity\"]\n        for data in [asset_doc[\"data\"], project_doc[\"data\"]]:\n            if \"resolutionWidth\" in data and \"resolutionHeight\" in data:\n                width = data[\"resolutionWidth\"]\n                height = data[\"resolutionHeight\"]\n                return int(width), int(height)\n\n        # Defaults if not found in asset document or project document\n        return 1920, 1080\n\n    @classmethod\n    def repair(cls, instance):\n        reset_scene_resolution()\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_scene_saved.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\nfrom pymxs import runtime as rt\n\n\nclass ValidateSceneSaved(pyblish.api.InstancePlugin):\n    \"\"\"Validate that workfile was saved.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"workfile\"]\n    hosts = [\"max\"]\n    label = \"Validate Workfile is saved\"\n\n    def process(self, instance):\n        if not rt.maxFilePath or not rt.maxFileName:\n            raise PublishValidationError(\n                \"Workfile is not saved\", title=self.label)\n"
  },
  {
    "path": "openpype/hosts/max/plugins/publish/validate_tyflow_data.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import PublishValidationError\nfrom pymxs import runtime as rt\n\n\nclass ValidateTyFlowData(pyblish.api.InstancePlugin):\n    \"\"\"Validate TyFlow plugins or relevant operators are set correctly.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"pointcloud\", \"tycache\"]\n    hosts = [\"max\"]\n    label = \"TyFlow Data\"\n\n    def process(self, instance):\n        \"\"\"\n        Notes:\n            1. Validate the container only include tyFlow objects\n            2. Validate if tyFlow operator Export Particle exists\n\n        \"\"\"\n\n        invalid_object = self.get_tyflow_object(instance)\n        if invalid_object:\n            self.log.error(f\"Non tyFlow object found: {invalid_object}\")\n\n        invalid_operator = self.get_tyflow_operator(instance)\n        if invalid_operator:\n            self.log.error(\n                \"Operator 'Export Particles' not found in tyFlow editor.\")\n        if invalid_object or invalid_operator:\n            raise PublishValidationError(\n                \"issues occurred\",\n                description=\"Container should only include tyFlow object \"\n                \"and tyflow operator 'Export Particle' should be in \"\n                \"the tyFlow editor.\")\n\n    def get_tyflow_object(self, instance):\n        \"\"\"Get the nodes which are not tyFlow object(s)\n        and editable mesh(es)\n\n        Args:\n            instance (pyblish.api.Instance): instance\n\n        Returns:\n            list: invalid nodes which are not tyFlow\n                object(s) and editable mesh(es).\n        \"\"\"\n        container = instance.data[\"instance_node\"]\n        self.log.debug(f\"Validating tyFlow container for {container}\")\n\n        allowed_classes = [rt.tyFlow, rt.Editable_Mesh]\n        return [\n            member for member in instance.data[\"members\"]\n            if rt.ClassOf(member) not in allowed_classes\n        ]\n\n    def get_tyflow_operator(self, instance):\n        \"\"\"Check if the Export Particle Operators in the node\n        connections.\n\n        Args:\n            instance (str): instance node\n\n        Returns:\n            invalid(list): list of invalid nodes which do\n            not consist of Export Particle Operators as parts\n            of the node connections\n        \"\"\"\n        invalid = []\n        members = instance.data[\"members\"]\n        for member in members:\n            obj = member.baseobject\n\n            # There must be at least one animation with export\n            # particles enabled\n            has_export_particles = False\n            anim_names = rt.GetSubAnimNames(obj)\n            for anim_name in anim_names:\n                # get name of the related tyFlow node\n                sub_anim = rt.GetSubAnim(obj, anim_name)\n                # check if there is export particle operator\n                if rt.IsProperty(sub_anim, \"Export_Particles\"):\n                    has_export_particles = True\n                    break\n\n            if not has_export_particles:\n                invalid.append(member)\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/max/startup/startup.ms",
    "content": "-- OpenPype Init Script\n(\n    local sysPath = dotNetClass \"System.IO.Path\"\n\tlocal sysDir = dotNetClass \"System.IO.Directory\"\n\tlocal localScript = getThisScriptFilename()\n    local startup = sysPath.Combine (sysPath.GetDirectoryName localScript) \"startup.py\"\n\n    local pythonpath = systemTools.getEnvVariable \"MAX_PYTHONPATH\"\n    systemTools.setEnvVariable \"PYTHONPATH\" pythonpath\n    \n    /*opens the create menu on startup to ensure users are presented with a useful default view.*/\n    max create mode\n\n    python.ExecuteFile startup\n)\n"
  },
  {
    "path": "openpype/hosts/max/startup/startup.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nimport sys\n\n# this might happen in some 3dsmax version where PYTHONPATH isn't added\n# to sys.path automatically\nfor path in os.environ[\"PYTHONPATH\"].split(os.pathsep):\n    if path and path not in sys.path:\n        sys.path.append(path)\n\nfrom openpype.hosts.max.api import MaxHost\nfrom openpype.pipeline import install_host\n\nhost = MaxHost()\ninstall_host(host)\n"
  },
  {
    "path": "openpype/hosts/maya/__init__.py",
    "content": "from .addon import (\n    MayaAddon,\n    MAYA_ROOT_DIR,\n)\n\n\n__all__ = (\n    \"MayaAddon\",\n    \"MAYA_ROOT_DIR\",\n)\n"
  },
  {
    "path": "openpype/hosts/maya/addon.py",
    "content": "import os\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nMAYA_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass MayaAddon(OpenPypeModule, IHostAddon):\n    name = \"maya\"\n    host_name = \"maya\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        # Add requirements to PYTHONPATH\n        new_python_paths = [\n            os.path.join(MAYA_ROOT_DIR, \"startup\")\n        ]\n        old_python_path = env.get(\"PYTHONPATH\") or \"\"\n        for path in old_python_path.split(os.pathsep):\n            if not path:\n                continue\n\n            norm_path = os.path.normpath(path)\n            if norm_path not in new_python_paths:\n                new_python_paths.append(norm_path)\n\n        env[\"PYTHONPATH\"] = os.pathsep.join(new_python_paths)\n\n        # Set default environments\n        envs = {\n            \"OPENPYPE_LOG_NO_COLORS\": \"Yes\",\n            # For python module 'qtpy'\n            \"QT_API\": \"PySide2\",\n            # For python module 'Qt'\n            \"QT_PREFERRED_BINDING\": \"PySide2\"\n        }\n        for key, value in envs.items():\n            env[key] = value\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [\n            os.path.join(MAYA_ROOT_DIR, \"hooks\")\n        ]\n\n    def get_workfile_extensions(self):\n        return [\".ma\", \".mb\"]\n"
  },
  {
    "path": "openpype/hosts/maya/api/__init__.py",
    "content": "\"\"\"Public API\n\nAnything that isn't defined here is INTERNAL and unreliable for external use.\n\n\"\"\"\n\nfrom .pipeline import (\n    uninstall,\n\n    ls,\n    containerise,\n    MayaHost,\n)\nfrom .plugin import (\n    Creator,\n    Loader\n)\n\nfrom .workio import (\n    open_file,\n    save_file,\n    current_file,\n    has_unsaved_changes,\n    file_extensions,\n    work_root\n)\n\nfrom .lib import (\n    lsattr,\n    lsattrs,\n    read,\n\n    apply_shaders,\n    maintained_selection,\n    suspended_refresh,\n\n    unique_namespace,\n)\n\n\n__all__ = [\n    \"uninstall\",\n\n    \"ls\",\n    \"containerise\",\n    \"MayaHost\",\n\n    \"Creator\",\n    \"Loader\",\n\n    # Workfiles API\n    \"open_file\",\n    \"save_file\",\n    \"current_file\",\n    \"has_unsaved_changes\",\n    \"file_extensions\",\n    \"work_root\",\n\n    # Utility functions\n    \"lsattr\",\n    \"lsattrs\",\n    \"read\",\n\n    \"unique_namespace\",\n\n    \"apply_shaders\",\n    \"maintained_selection\",\n    \"suspended_refresh\",\n\n]\n\n# Backwards API compatibility\nopen = open_file\nsave = save_file\n"
  },
  {
    "path": "openpype/hosts/maya/api/action.py",
    "content": "# absolute_import is needed to counter the `module has no cmds error` in Maya\nfrom __future__ import absolute_import\n\nimport pyblish.api\n\nfrom openpype.client import get_asset_by_name\nfrom openpype.pipeline.publish import get_errored_instances_from_context\n\n\nclass GenerateUUIDsOnInvalidAction(pyblish.api.Action):\n    \"\"\"Generate UUIDs on the invalid nodes in the instance.\n\n    Invalid nodes are those returned by the plugin's `get_invalid` method.\n    As such it is the plug-in's responsibility to ensure the nodes that\n    receive new UUIDs are actually invalid.\n\n    Requires:\n        - instance.data[\"asset\"]\n\n    \"\"\"\n\n    label = \"Regenerate UUIDs\"\n    on = \"failed\"  # This action is only available on a failed plug-in\n    icon = \"wrench\"  # Icon from Awesome Icon\n\n    def process(self, context, plugin):\n\n        from maya import cmds\n\n        self.log.info(\"Finding bad nodes..\")\n\n        errored_instances = get_errored_instances_from_context(context)\n\n        # Apply pyblish logic to get the instances for the plug-in\n        instances = pyblish.api.instances_by_plugin(errored_instances, plugin)\n\n        # Get the nodes from the all instances that ran through this plug-in\n        all_invalid = []\n        for instance in instances:\n            invalid = plugin.get_invalid(instance)\n\n            # Don't allow referenced nodes to get their ids regenerated to\n            # avoid loaded content getting messed up with reference edits\n            if invalid:\n                referenced = {node for node in invalid if\n                              cmds.referenceQuery(node, isNodeReferenced=True)}\n                if referenced:\n                    self.log.warning(\"Skipping UUID generation on referenced \"\n                                     \"nodes: {}\".format(list(referenced)))\n                    invalid = [node for node in invalid\n                               if node not in referenced]\n\n            if invalid:\n\n                self.log.info(\"Fixing instance {}\".format(instance.name))\n                self._update_id_attribute(instance, invalid)\n\n                all_invalid.extend(invalid)\n\n        if not all_invalid:\n            self.log.info(\"No invalid nodes found.\")\n            return\n\n        all_invalid = list(set(all_invalid))\n        self.log.info(\"Generated ids on nodes: {0}\".format(all_invalid))\n\n    def _update_id_attribute(self, instance, nodes):\n        \"\"\"Delete the id attribute\n\n        Args:\n            instance: The instance we're fixing for\n            nodes (list): all nodes to regenerate ids on\n        \"\"\"\n\n        from . import lib\n\n        # Expecting this is called on validators in which case 'assetEntity'\n        #   should be always available, but kept a way to query it by name.\n        asset_doc = instance.data.get(\"assetEntity\")\n        if not asset_doc:\n            asset_name = instance.data[\"asset\"]\n            project_name = instance.context.data[\"projectName\"]\n            self.log.info((\n                \"Asset is not stored on instance.\"\n                \" Querying by name \\\"{}\\\" from project \\\"{}\\\"\"\n            ).format(asset_name, project_name))\n            asset_doc = get_asset_by_name(\n                project_name, asset_name, fields=[\"_id\"]\n            )\n\n        for node, _id in lib.generate_ids(nodes, asset_id=asset_doc[\"_id\"]):\n            lib.set_id(node, _id, overwrite=True)\n\n\nclass SelectInvalidAction(pyblish.api.Action):\n    \"\"\"Select invalid nodes in Maya when plug-in failed.\n\n    To retrieve the invalid nodes this assumes a static `get_invalid()`\n    method is available on the plugin.\n\n    \"\"\"\n    label = \"Select invalid\"\n    on = \"failed\"  # This action is only available on a failed plug-in\n    icon = \"search\"  # Icon from Awesome Icon\n\n    def process(self, context, plugin):\n\n        try:\n            from maya import cmds\n        except ImportError:\n            raise ImportError(\"Current host is not Maya\")\n\n        errored_instances = get_errored_instances_from_context(context,\n                                                               plugin=plugin)\n\n        # Get the invalid nodes for the plug-ins\n        self.log.info(\"Finding invalid nodes..\")\n        invalid = list()\n        for instance in errored_instances:\n            invalid_nodes = plugin.get_invalid(instance)\n            if invalid_nodes:\n                if isinstance(invalid_nodes, (list, tuple)):\n                    invalid.extend(invalid_nodes)\n                else:\n                    self.log.warning(\"Plug-in returned to be invalid, \"\n                                     \"but has no selectable nodes.\")\n\n        # Ensure unique (process each node only once)\n        invalid = list(set(invalid))\n\n        if invalid:\n            self.log.info(\"Selecting invalid nodes: %s\" % \", \".join(invalid))\n            cmds.select(invalid, replace=True, noExpand=True)\n        else:\n            self.log.info(\"No invalid nodes found.\")\n            cmds.select(deselect=True)\n"
  },
  {
    "path": "openpype/hosts/maya/api/alembic.py",
    "content": "import json\nimport logging\nimport os\n\nfrom maya import cmds  # noqa\n\nfrom openpype.hosts.maya.api.lib import evaluation\n\nlog = logging.getLogger(__name__)\n\n# The maya alembic export types\nALEMBIC_ARGS = {\n    \"attr\": (list, tuple),\n    \"attrPrefix\": (list, tuple),\n    \"autoSubd\": bool,\n    \"dataFormat\": str,\n    \"dontSkipUnwrittenFrames\": bool,\n    \"endFrame\": float,\n    \"eulerFilter\": bool,\n    \"frameRange\": str,  # \"start end\"; overrides startFrame & endFrame\n    \"frameRelativeSample\": float,\n    \"melPerFrameCallback\": str,\n    \"melPostJobCallback\": str,\n    \"noNormals\": bool,\n    \"preRoll\": bool,\n    \"preRollStartFrame\": int,\n    \"pythonPerFrameCallback\": str,\n    \"pythonPostJobCallback\": str,\n    \"renderableOnly\": bool,\n    \"root\": (list, tuple),\n    \"selection\": bool,\n    \"startFrame\": float,\n    \"step\": float,\n    \"stripNamespaces\": bool,\n    \"userAttr\": (list, tuple),\n    \"userAttrPrefix\": (list, tuple),\n    \"uvWrite\": bool,\n    \"uvsOnly\": bool,\n    \"verbose\": bool,\n    \"wholeFrameGeo\": bool,\n    \"worldSpace\": bool,\n    \"writeColorSets\": bool,\n    \"writeCreases\": bool,  # Maya 2015 Ext1+\n    \"writeFaceSets\": bool,\n    \"writeUVSets\": bool,   # Maya 2017+\n    \"writeVisibility\": bool,\n}\n\n\ndef extract_alembic(\n    file,\n    attr=None,\n    attrPrefix=None,\n    dataFormat=\"ogawa\",\n    endFrame=None,\n    eulerFilter=True,\n    frameRange=\"\",\n    noNormals=False,\n    preRoll=False,\n    preRollStartFrame=0,\n    renderableOnly=False,\n    root=None,\n    selection=True,\n    startFrame=None,\n    step=1.0,\n    stripNamespaces=True,\n    uvWrite=True,\n    verbose=False,\n    wholeFrameGeo=False,\n    worldSpace=False,\n    writeColorSets=False,\n    writeCreases=False,\n    writeNormals=False,\n    writeFaceSets=False,\n    writeUVSets=False,\n    writeVisibility=False\n):\n    \"\"\"Extract a single Alembic Cache.\n\n    This extracts an Alembic cache using the `-selection` flag to minimize\n    the extracted content to solely what was Collected into the instance.\n\n    Arguments:\n\n        startFrame (float): Start frame of output. Ignored if `frameRange`\n            provided.\n\n        endFrame (float): End frame of output. Ignored if `frameRange`\n            provided.\n\n        frameRange (tuple or str): Two-tuple with start and end frame or a\n            string formatted as: \"startFrame endFrame\". This argument\n            overrides `startFrame` and `endFrame` arguments.\n\n        eulerFilter (bool): When on, X, Y, and Z rotation data is filtered with\n            an Euler filter. Euler filtering helps resolve irregularities in\n            rotations especially if X, Y, and Z rotations exceed 360 degrees.\n            Defaults to True.\n\n        noNormals (bool): When on, normal data from the original polygon\n            objects is not included in the exported Alembic cache file.\n\n        preRoll (bool): This frame range will not be sampled.\n            Defaults to False.\n\n        renderableOnly (bool): When on, any non-renderable nodes or hierarchy,\n            such as hidden objects, are not included in the Alembic file.\n            Defaults to False.\n\n        selection (bool): Write out all all selected nodes from the\n            active selection list that are descendents of the roots specified\n            with -root. Defaults to False.\n\n        uvWrite (bool): When on, UV data from polygon meshes and subdivision\n            objects are written to the Alembic file. Only the current UV map is\n            included.\n\n        writeColorSets (bool): Write all color sets on MFnMeshes as\n            color 3 or color 4 indexed geometry parameters with face varying\n            scope. Defaults to False.\n\n        writeFaceSets (bool): Write all Face sets on MFnMeshes.\n            Defaults to False.\n\n        wholeFrameGeo (bool): Data for geometry will only be written\n            out on whole frames. Defaults to False.\n\n        worldSpace (bool): When on, the top node in the node hierarchy is\n            stored as world space. By default, these nodes are stored as local\n            space. Defaults to False.\n\n        writeVisibility (bool): Visibility state will be stored in\n            the Alembic file.  Otherwise everything written out is treated as\n            visible. Defaults to False.\n\n        writeUVSets (bool): Write all uv sets on MFnMeshes as vector\n            2 indexed geometry parameters with face varying scope. Defaults to\n            False.\n\n        writeCreases (bool): If the mesh has crease edges or crease\n            vertices, the mesh (OPolyMesh) would now be written out as an OSubD\n            and crease info will be stored in the Alembic file. Otherwise,\n            creases info won't be preserved in Alembic file unless a custom\n            Boolean attribute SubDivisionMesh has been added to mesh node and\n            its value is true. Defaults to False.\n\n        dataFormat (str): The data format to use for the cache,\n                          defaults to \"ogawa\"\n\n        step (float): The time interval (expressed in frames) at\n            which the frame range is sampled. Additional samples around each\n            frame can be specified with -frs. Defaults to 1.0.\n\n        attr (list of str, optional): A specific geometric attribute to write\n            out. Defaults to [].\n\n        attrPrefix (list of str, optional): Prefix filter for determining which\n            geometric attributes to write out. Defaults to [\"ABC_\"].\n\n        root (list of str): Maya dag path which will be parented to\n            the root of the Alembic file. Defaults to [], which means the\n            entire scene will be written out.\n\n        stripNamespaces (bool): When on, any namespaces associated with the\n            exported objects are removed from the Alembic file. For example, an\n            object with the namespace taco:foo:bar appears as bar in the\n            Alembic file.\n\n        verbose (bool): When on, outputs frame number information to the\n            Script Editor or output window during extraction.\n\n        preRollStartFrame (float): The frame to start scene\n            evaluation at.  This is used to set the starting frame for time\n            dependent translations and can be used to evaluate run-up that\n            isn't actually translated. Defaults to 0.\n    \"\"\"\n\n    # Ensure alembic exporter is loaded\n    cmds.loadPlugin('AbcExport', quiet=True)\n\n    # Alembic Exporter requires forward slashes\n    file = file.replace('\\\\', '/')\n\n    # Ensure list arguments are valid.\n    attr = attr or []\n    attrPrefix = attrPrefix or []\n    root = root or []\n\n    # Pass the start and end frame on as `frameRange` so that it\n    # never conflicts with that argument\n    if not frameRange:\n        # Fallback to maya timeline if no start or end frame provided.\n        if startFrame is None:\n            startFrame = cmds.playbackOptions(query=True,\n                                              animationStartTime=True)\n        if endFrame is None:\n            endFrame = cmds.playbackOptions(query=True,\n                                            animationEndTime=True)\n\n        # Ensure valid types are converted to frame range\n        assert isinstance(startFrame, ALEMBIC_ARGS[\"startFrame\"])\n        assert isinstance(endFrame, ALEMBIC_ARGS[\"endFrame\"])\n        frameRange = \"{0} {1}\".format(startFrame, endFrame)\n    else:\n        # Allow conversion from tuple for `frameRange`\n        if isinstance(frameRange, (list, tuple)):\n            assert len(frameRange) == 2\n            frameRange = \"{0} {1}\".format(frameRange[0], frameRange[1])\n\n    # Assemble options\n    options = {\n        \"selection\": selection,\n        \"frameRange\": frameRange,\n        \"eulerFilter\": eulerFilter,\n        \"noNormals\": noNormals,\n        \"preRoll\": preRoll,\n        \"renderableOnly\": renderableOnly,\n        \"uvWrite\": uvWrite,\n        \"writeColorSets\": writeColorSets,\n        \"writeFaceSets\": writeFaceSets,\n        \"wholeFrameGeo\": wholeFrameGeo,\n        \"worldSpace\": worldSpace,\n        \"writeVisibility\": writeVisibility,\n        \"writeUVSets\": writeUVSets,\n        \"writeCreases\": writeCreases,\n        \"dataFormat\": dataFormat,\n        \"step\": step,\n        \"attr\": attr,\n        \"attrPrefix\": attrPrefix,\n        \"stripNamespaces\": stripNamespaces,\n        \"verbose\": verbose,\n        \"preRollStartFrame\": preRollStartFrame\n    }\n\n    # Validate options\n    for key, value in options.copy().items():\n\n        # Discard unknown options\n        if key not in ALEMBIC_ARGS:\n            log.warning(\"extract_alembic() does not support option '%s'. \"\n                        \"Flag will be ignored..\", key)\n            options.pop(key)\n            continue\n\n        # Validate value type\n        valid_types = ALEMBIC_ARGS[key]\n        if not isinstance(value, valid_types):\n            raise TypeError(\"Alembic option unsupported type: \"\n                            \"{0} (expected {1})\".format(value, valid_types))\n\n        # Ignore empty values, like an empty string, since they mess up how\n        # job arguments are built\n        if isinstance(value, (list, tuple)):\n            value = [x for x in value if x.strip()]\n\n            # Ignore option completely if no values remaining\n            if not value:\n                options.pop(key)\n                continue\n\n            options[key] = value\n\n    # The `writeCreases` argument was changed to `autoSubd` in Maya 2018+\n    maya_version = int(cmds.about(version=True))\n    if maya_version >= 2018:\n        options['autoSubd'] = options.pop('writeCreases', False)\n\n    # Format the job string from options\n    job_args = list()\n    for key, value in options.items():\n        if isinstance(value, (list, tuple)):\n            for entry in value:\n                job_args.append(\"-{} {}\".format(key, entry))\n        elif isinstance(value, bool):\n            # Add only when state is set to True\n            if value:\n                job_args.append(\"-{0}\".format(key))\n        else:\n            job_args.append(\"-{0} {1}\".format(key, value))\n\n    job_str = \" \".join(job_args)\n    job_str += ' -file \"%s\"' % file\n\n    # Ensure output directory exists\n    parent_dir = os.path.dirname(file)\n    if not os.path.exists(parent_dir):\n        os.makedirs(parent_dir)\n\n    if verbose:\n        log.debug(\"Preparing Alembic export with options: %s\",\n                  json.dumps(options, indent=4))\n        log.debug(\"Extracting Alembic with job arguments: %s\", job_str)\n\n    # Perform extraction\n    print(\"Alembic Job Arguments : {}\".format(job_str))\n\n    # Disable the parallel evaluation temporarily to ensure no buggy\n    # exports are made. (PLN-31)\n    # TODO: Make sure this actually fixes the issues\n    with evaluation(\"off\"):\n        cmds.AbcExport(j=job_str, verbose=verbose)\n\n    if verbose:\n        log.debug(\"Extracted Alembic to: %s\", file)\n\n    return file\n"
  },
  {
    "path": "openpype/hosts/maya/api/commands.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"OpenPype script commands to be used directly in Maya.\"\"\"\nfrom maya import cmds\n\nfrom openpype.client import get_asset_by_name, get_project\nfrom openpype.pipeline import get_current_project_name, get_current_asset_name\n\n\nclass ToolWindows:\n\n    _windows = {}\n\n    @classmethod\n    def get_window(cls, tool):\n        \"\"\"Get widget for specific tool.\n\n        Args:\n            tool (str): Name of the tool.\n\n        Returns:\n            Stored widget.\n\n        \"\"\"\n        try:\n            return cls._windows[tool]\n        except KeyError:\n            return None\n\n    @classmethod\n    def set_window(cls, tool, window):\n        \"\"\"Set widget for the tool.\n\n        Args:\n            tool (str): Name of the tool.\n            window (QtWidgets.QWidget): Widget\n\n        \"\"\"\n        cls._windows[tool] = window\n\n\ndef edit_shader_definitions():\n    from qtpy import QtWidgets\n    from openpype.hosts.maya.api.shader_definition_editor import (\n        ShaderDefinitionsEditor\n    )\n    from openpype.tools.utils import qt_app_context\n\n    top_level_widgets = QtWidgets.QApplication.topLevelWidgets()\n    main_window = next(widget for widget in top_level_widgets\n                       if widget.objectName() == \"MayaWindow\")\n\n    with qt_app_context():\n        window = ToolWindows.get_window(\"shader_definition_editor\")\n        if not window:\n            window = ShaderDefinitionsEditor(parent=main_window)\n            ToolWindows.set_window(\"shader_definition_editor\", window)\n        window.show()\n\n\ndef _resolution_from_document(doc):\n    if not doc or \"data\" not in doc:\n        print(\"Entered document is not valid. \\\"{}\\\"\".format(str(doc)))\n        return None\n\n    resolution_width = doc[\"data\"].get(\"resolutionWidth\")\n    resolution_height = doc[\"data\"].get(\"resolutionHeight\")\n    # Backwards compatibility\n    if resolution_width is None or resolution_height is None:\n        resolution_width = doc[\"data\"].get(\"resolution_width\")\n        resolution_height = doc[\"data\"].get(\"resolution_height\")\n\n    # Make sure both width and height are set\n    if resolution_width is None or resolution_height is None:\n        cmds.warning(\n            \"No resolution information found for \\\"{}\\\"\".format(doc[\"name\"])\n        )\n        return None\n\n    return int(resolution_width), int(resolution_height)\n\n\ndef reset_resolution():\n    # Default values\n    resolution_width = 1920\n    resolution_height = 1080\n\n    # Get resolution from asset\n    project_name = get_current_project_name()\n    asset_name = get_current_asset_name()\n    asset_doc = get_asset_by_name(project_name, asset_name)\n    resolution = _resolution_from_document(asset_doc)\n    # Try get resolution from project\n    if resolution is None:\n        # TODO go through visualParents\n        print((\n            \"Asset \\\"{}\\\" does not have set resolution.\"\n            \" Trying to get resolution from project\"\n        ).format(asset_name))\n        project_doc = get_project(project_name)\n        resolution = _resolution_from_document(project_doc)\n\n    if resolution is None:\n        msg = \"Using default resolution {}x{}\"\n    else:\n        resolution_width, resolution_height = resolution\n        msg = \"Setting resolution to {}x{}\"\n\n    print(msg.format(resolution_width, resolution_height))\n\n    # set for different renderers\n    # arnold, vray, redshift, renderman\n\n    renderer = cmds.getAttr(\"defaultRenderGlobals.currentRenderer\").lower()\n    # handle various renderman names\n    if renderer.startswith(\"renderman\"):\n        renderer = \"renderman\"\n\n    # default attributes are usable for Arnold, Renderman and Redshift\n    width_attr_name = \"defaultResolution.width\"\n    height_attr_name = \"defaultResolution.height\"\n\n    # Vray has its own way\n    if renderer == \"vray\":\n        width_attr_name = \"vraySettings.width\"\n        height_attr_name = \"vraySettings.height\"\n\n    cmds.setAttr(width_attr_name, resolution_width)\n    cmds.setAttr(height_attr_name, resolution_height)\n"
  },
  {
    "path": "openpype/hosts/maya/api/customize.py",
    "content": "\"\"\"A set of commands that install overrides to Maya's UI\"\"\"\n\nimport os\nimport logging\n\nfrom functools import partial\n\nimport maya.cmds as cmds\nimport maya.mel as mel\n\nfrom openpype import resources\nfrom openpype.tools.utils import host_tools\nfrom .lib import get_main_window\nfrom ..tools import show_look_assigner\n\nlog = logging.getLogger(__name__)\n\nCOMPONENT_MASK_ORIGINAL = {}\n\n\ndef override_component_mask_commands():\n    \"\"\"Override component mask ctrl+click behavior.\n\n    This implements special behavior for Maya's component\n    mask menu items where a ctrl+click will instantly make\n    it an isolated behavior disabling all others.\n\n    Tested in Maya 2016 and 2018\n\n    \"\"\"\n    log.info(\"Installing override_component_mask_commands..\")\n\n    # Get all object mask buttons\n    buttons = cmds.formLayout(\"objectMaskIcons\",\n                              query=True,\n                              childArray=True)\n    # Skip the triangle list item\n    buttons = [btn for btn in buttons if btn != \"objPickMenuLayout\"]\n\n    def on_changed_callback(raw_command, state):\n        \"\"\"New callback\"\"\"\n\n        # If \"control\" is held force the toggled one to on and\n        # toggle the others based on whether any of the buttons\n        # was remaining active after the toggle, if not then\n        # enable all\n        if cmds.getModifiers() == 4:  # = CTRL\n            state = True\n            active = [cmds.iconTextCheckBox(btn, query=True, value=True)\n                      for btn in buttons]\n            if any(active):\n                cmds.selectType(allObjects=False)\n            else:\n                cmds.selectType(allObjects=True)\n\n        # Replace #1 with the current button state\n        cmd = raw_command.replace(\" #1\", \" {}\".format(int(state)))\n        mel.eval(cmd)\n\n    for btn in buttons:\n\n        # Store a reference to the original command so that if\n        # we rerun this override command it doesn't recursively\n        # try to implement the fix. (This also allows us to\n        # \"uninstall\" the behavior later)\n        if btn not in COMPONENT_MASK_ORIGINAL:\n            original = cmds.iconTextCheckBox(btn, query=True, cc=True)\n            COMPONENT_MASK_ORIGINAL[btn] = original\n\n        # Assign the special callback\n        original = COMPONENT_MASK_ORIGINAL[btn]\n        new_fn = partial(on_changed_callback, original)\n        cmds.iconTextCheckBox(btn, edit=True, cc=new_fn)\n\n\ndef override_toolbox_ui():\n    \"\"\"Add custom buttons in Toolbox as replacement for Maya web help icon.\"\"\"\n    icons = resources.get_resource(\"icons\")\n    parent_widget = get_main_window()\n\n    # Ensure the maya web icon on toolbox exists\n    button_names = [\n        # Maya 2022.1+ with maya.cmds.iconTextStaticLabel\n        \"ToolBox|MainToolboxLayout|mayaHomeToolboxButton\",\n        # Older with maya.cmds.iconTextButton\n        \"ToolBox|MainToolboxLayout|mayaWebButton\"\n    ]\n    for name in button_names:\n        if cmds.control(name, query=True, exists=True):\n            web_button = name\n            break\n    else:\n        # Button does not exist\n        log.warning(\"Can't find Maya Home/Web button to override toolbox ui..\")\n        return\n\n    cmds.control(web_button, edit=True, visible=False)\n\n    # real = 32, but 36 with padding - according to toolbox mel script\n    icon_size = 36\n    parent = web_button.rsplit(\"|\", 1)[0]\n\n    # Ensure the parent is a formLayout\n    if not cmds.objectTypeUI(parent) == \"formLayout\":\n        return\n\n    # Create our controls\n    controls = []\n\n    controls.append(\n        cmds.iconTextButton(\n            \"pype_toolbox_lookmanager\",\n            annotation=\"Look Manager\",\n            label=\"Look Manager\",\n            image=os.path.join(icons, \"lookmanager.png\"),\n            command=show_look_assigner,\n            width=icon_size,\n            height=icon_size,\n            parent=parent\n        )\n    )\n\n    controls.append(\n        cmds.iconTextButton(\n            \"pype_toolbox_workfiles\",\n            annotation=\"Work Files\",\n            label=\"Work Files\",\n            image=os.path.join(icons, \"workfiles.png\"),\n            command=lambda: host_tools.show_workfiles(\n                parent=parent_widget\n            ),\n            width=icon_size,\n            height=icon_size,\n            parent=parent\n        )\n    )\n\n    controls.append(\n        cmds.iconTextButton(\n            \"pype_toolbox_loader\",\n            annotation=\"Loader\",\n            label=\"Loader\",\n            image=os.path.join(icons, \"loader.png\"),\n            command=lambda: host_tools.show_loader(\n                parent=parent_widget, use_context=True\n            ),\n            width=icon_size,\n            height=icon_size,\n            parent=parent\n        )\n    )\n\n    controls.append(\n        cmds.iconTextButton(\n            \"pype_toolbox_manager\",\n            annotation=\"Inventory\",\n            label=\"Inventory\",\n            image=os.path.join(icons, \"inventory.png\"),\n            command=lambda: host_tools.show_scene_inventory(\n                parent=parent_widget\n            ),\n            width=icon_size,\n            height=icon_size,\n            parent=parent\n        )\n    )\n\n    # Add the buttons on the bottom and stack\n    # them above each other with side padding\n    controls.reverse()\n    for i, control in enumerate(controls):\n        previous = controls[i - 1] if i > 0 else web_button\n\n        cmds.formLayout(parent, edit=True,\n                        attachControl=[control, \"bottom\", 0, previous],\n                        attachForm=([control, \"left\", 1],\n                                    [control, \"right\", 1]))\n"
  },
  {
    "path": "openpype/hosts/maya/api/exitstack.py",
    "content": "\"\"\"Backwards compatible implementation of ExitStack for Python 2.\n\nExitStack contextmanager was implemented with Python 3.3.\nAs long as we supportPython 2 hosts we can use this backwards\ncompatible implementation to support bothPython 2 and Python 3.\n\nInstead of using ExitStack from contextlib, use it from this module:\n\n>>> from openpype.hosts.maya.api.exitstack import ExitStack\n\nIt will provide the appropriate ExitStack implementation for the current\nrunning Python version.\n\n\"\"\"\n# TODO: Remove the entire script once dropping Python 2 support.\nimport contextlib\nif getattr(contextlib, \"nested\", None):\n    from contextlib import ExitStack    # noqa\nelse:\n    import sys\n    from collections import deque\n\n    class ExitStack(object):\n\n        \"\"\"Context manager for dynamic management of a stack of exit callbacks\n\n        For example:\n\n            with ExitStack() as stack:\n                files = [stack.enter_context(open(fname))\n                for fname in filenames]\n                # All opened files will automatically be closed at the end of\n                # the with statement, even if attempts to open files later\n                # in the list raise an exception\n\n        \"\"\"\n        def __init__(self):\n            self._exit_callbacks = deque()\n\n        def pop_all(self):\n            \"\"\"Preserve the context stack by transferring\n            it to a new instance\"\"\"\n            new_stack = type(self)()\n            new_stack._exit_callbacks = self._exit_callbacks\n            self._exit_callbacks = deque()\n            return new_stack\n\n        def _push_cm_exit(self, cm, cm_exit):\n            \"\"\"Helper to correctly register callbacks\n            to __exit__ methods\"\"\"\n            def _exit_wrapper(*exc_details):\n                return cm_exit(cm, *exc_details)\n            _exit_wrapper.__self__ = cm\n            self.push(_exit_wrapper)\n\n        def push(self, exit):\n            \"\"\"Registers a callback with the standard __exit__ method signature\n\n            Can suppress exceptions the same way __exit__ methods can.\n\n            Also accepts any object with an __exit__ method (registering a call\n            to the method instead of the object itself)\n            \"\"\"\n            # We use an unbound method rather than a bound method to follow\n            # the standard lookup behaviour for special methods\n            _cb_type = type(exit)\n            try:\n                exit_method = _cb_type.__exit__\n            except AttributeError:\n                # Not a context manager, so assume its a callable\n                self._exit_callbacks.append(exit)\n            else:\n                self._push_cm_exit(exit, exit_method)\n            return exit     # Allow use as a decorator\n\n        def callback(self, callback, *args, **kwds):\n            \"\"\"Registers an arbitrary callback and arguments.\n\n            Cannot suppress exceptions.\n            \"\"\"\n            def _exit_wrapper(exc_type, exc, tb):\n                callback(*args, **kwds)\n            # We changed the signature, so using @wraps is not appropriate, but\n            # setting __wrapped__ may still help with introspection\n            _exit_wrapper.__wrapped__ = callback\n            self.push(_exit_wrapper)\n            return callback     # Allow use as a decorator\n\n        def enter_context(self, cm):\n            \"\"\"Enters the supplied context manager\n\n            If successful, also pushes its __exit__ method as a callback and\n            returns the result of the __enter__ method.\n            \"\"\"\n            # We look up the special methods on the type to\n            # match the with statement\n            _cm_type = type(cm)\n            _exit = _cm_type.__exit__\n            result = _cm_type.__enter__(cm)\n            self._push_cm_exit(cm, _exit)\n            return result\n\n        def close(self):\n            \"\"\"Immediately unwind the context stack\"\"\"\n            self.__exit__(None, None, None)\n\n        def __enter__(self):\n            return self\n\n        def __exit__(self, *exc_details):\n            # We manipulate the exception state so it behaves as though\n            # we were actually nesting multiple with statements\n            frame_exc = sys.exc_info()[1]\n\n            def _fix_exception_context(new_exc, old_exc):\n                while 1:\n                    exc_context = new_exc.__context__\n                    if exc_context in (None, frame_exc):\n                        break\n                    new_exc = exc_context\n                new_exc.__context__ = old_exc\n\n            # Callbacks are invoked in LIFO order to match the behaviour of\n            # nested context managers\n            suppressed_exc = False\n            while self._exit_callbacks:\n                cb = self._exit_callbacks.pop()\n                try:\n                    if cb(*exc_details):\n                        suppressed_exc = True\n                        exc_details = (None, None, None)\n                except Exception:\n                    new_exc_details = sys.exc_info()\n                    # simulate the stack of exceptions by setting the context\n                    _fix_exception_context(new_exc_details[1], exc_details[1])\n                    if not self._exit_callbacks:\n                        raise\n                    exc_details = new_exc_details\n            return suppressed_exc\n"
  },
  {
    "path": "openpype/hosts/maya/api/fbx.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tools to work with FBX.\"\"\"\nimport logging\n\nfrom pyblish.api import Instance\n\nfrom maya import cmds  # noqa\nimport maya.mel as mel  # noqa\nfrom openpype.hosts.maya.api.lib import maintained_selection\n\n\nclass FBXExtractor:\n    \"\"\"Extract FBX from Maya.\n\n        This extracts reproducible FBX exports ignoring any of the settings set\n        on the local machine in the FBX export options window.\n\n        All export settings are applied with the `FBXExport*` commands prior\n        to the `FBXExport` call itself. The options can be overridden with\n        their\n        nice names as seen in the \"options\" property on this class.\n\n        For more information on FBX exports see:\n        - https://knowledge.autodesk.com/support/maya/learn-explore/caas\n        /CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-6CCE943A-2ED4-4CEE-96D4\n        -9CB19C28F4E0-htm.html\n        - http://forums.cgsociety.org/archive/index.php?t-1032853.html\n        - https://groups.google.com/forum/#!msg/python_inside_maya/cLkaSo361oE\n        /LKs9hakE28kJ\n\n        \"\"\"\n    @property\n    def options(self):\n        \"\"\"Overridable options for FBX Export\n\n        Given in the following format\n            - {NAME: EXPECTED TYPE}\n\n        If the overridden option's type does not match,\n        the option is not included and a warning is logged.\n\n        \"\"\"\n\n        return {\n            \"cameras\": bool,\n            \"smoothingGroups\": bool,\n            \"hardEdges\": bool,\n            \"tangents\": bool,\n            \"smoothMesh\": bool,\n            \"instances\": bool,\n            # \"referencedContainersContent\": bool, # deprecated in Maya 2016+\n            \"bakeComplexAnimation\": int,\n            \"bakeComplexStart\": int,\n            \"bakeComplexEnd\": int,\n            \"bakeComplexStep\": int,\n            \"bakeResampleAnimation\": bool,\n            \"useSceneName\": bool,\n            \"quaternion\": str,  # \"euler\"\n            \"shapes\": bool,\n            \"skins\": bool,\n            \"constraints\": bool,\n            \"lights\": bool,\n            \"embeddedTextures\": bool,\n            \"inputConnections\": bool,\n            \"upAxis\": str,  # x, y or z,\n            \"triangulate\": bool,\n            \"fileVersion\": str,\n            \"skeletonDefinitions\": bool,\n            \"referencedAssetsContent\": bool\n        }\n\n    @property\n    def default_options(self):\n        \"\"\"The default options for FBX extraction.\n\n        This includes shapes, skins, constraints, lights and incoming\n        connections and exports with the Y-axis as up-axis.\n\n        By default this uses the time sliders start and end time.\n\n        \"\"\"\n\n        start_frame = int(cmds.playbackOptions(query=True,\n                                               animationStartTime=True))\n        end_frame = int(cmds.playbackOptions(query=True,\n                                             animationEndTime=True))\n\n        return {\n            \"cameras\": False,\n            \"smoothingGroups\": True,\n            \"hardEdges\": False,\n            \"tangents\": False,\n            \"smoothMesh\": True,\n            \"instances\": False,\n            \"bakeComplexAnimation\": True,\n            \"bakeComplexStart\": start_frame,\n            \"bakeComplexEnd\": end_frame,\n            \"bakeComplexStep\": 1,\n            \"bakeResampleAnimation\": True,\n            \"useSceneName\": False,\n            \"quaternion\": \"euler\",\n            \"shapes\": True,\n            \"skins\": True,\n            \"constraints\": False,\n            \"lights\": True,\n            \"embeddedTextures\": False,\n            \"inputConnections\": True,\n            \"upAxis\": \"y\",\n            \"triangulate\": False,\n            \"fileVersion\": \"FBX202000\",\n            \"skeletonDefinitions\": False,\n            \"referencedAssetsContent\": False\n        }\n\n    def __init__(self, log=None):\n        # Ensure FBX plug-in is loaded\n        self.log = log or logging.getLogger(self.__class__.__name__)\n        cmds.loadPlugin(\"fbxmaya\", quiet=True)\n\n    def parse_overrides(self, instance, options):\n        \"\"\"Inspect data of instance to determine overridden options\n\n        An instance may supply any of the overridable options\n        as data, the option is then added to the extraction.\n\n        \"\"\"\n\n        for key in instance.data:\n            if key not in self.options:\n                continue\n\n            # Ensure the data is of correct type\n            value = instance.data[key]\n            if not isinstance(value, self.options[key]):\n                self.log.warning(\n                    \"Overridden attribute {key} was of \"\n                    \"the wrong type: {invalid_type} \"\n                    \"- should have been {valid_type}\".format(\n                        key=key,\n                        invalid_type=type(value).__name__,\n                        valid_type=self.options[key].__name__))\n                continue\n\n            options[key] = value\n\n        return options\n\n    def set_options_from_instance(self, instance):\n        # type: (Instance) -> None\n        \"\"\"Sets FBX export options from data in the instance.\n\n        Args:\n            instance (Instance): Instance data.\n\n        \"\"\"\n        # Parse export options\n        options = self.default_options\n        options = self.parse_overrides(instance, options)\n        self.log.debug(\"Export options: {0}\".format(options))\n\n        # Collect the start and end including handles\n        start = instance.data.get(\"frameStartHandle\") or \\\n            instance.context.data.get(\"frameStartHandle\")\n        end = instance.data.get(\"frameEndHandle\") or \\\n            instance.context.data.get(\"frameEndHandle\")\n\n        options['bakeComplexStart'] = start\n        options['bakeComplexEnd'] = end\n\n        # First apply the default export settings to be fully consistent\n        # each time for successive publishes\n        mel.eval(\"FBXResetExport\")\n\n        # Apply the FBX overrides through MEL since the commands\n        # only work correctly in MEL according to online\n        # available discussions on the topic\n        _iteritems = getattr(options, \"iteritems\", options.items)\n        for option, value in _iteritems():\n            key = option[0].upper() + option[1:]  # uppercase first letter\n\n            # Boolean must be passed as lower-case strings\n            # as to MEL standards\n            if isinstance(value, bool):\n                value = str(value).lower()\n\n            template = \"FBXExport{0} {1}\" if key == \"UpAxis\" else \\\n                \"FBXExport{0} -v {1}\"  # noqa\n            cmd = template.format(key, value)\n            self.log.debug(cmd)\n            mel.eval(cmd)\n\n        # Never show the UI or generate a log\n        mel.eval(\"FBXExportShowUI -v false\")\n        mel.eval(\"FBXExportGenerateLog -v false\")\n\n    @staticmethod\n    def export(members, path):\n        # type: (list, str) -> None\n        \"\"\"Export members as FBX with given path.\n\n        Args:\n            members (list): List of members to export.\n            path (str): Path to use for export.\n\n        \"\"\"\n        # The export requires forward slashes because we need\n        # to format it into a string in a mel expression\n        path = path.replace(\"\\\\\", \"/\")\n        with maintained_selection():\n            cmds.select(members, r=True, noExpand=True)\n            mel.eval('FBXExport -f \"{}\" -s'.format(path))\n"
  },
  {
    "path": "openpype/hosts/maya/api/gltf.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tools to work with GLTF.\"\"\"\nimport logging\n\nfrom maya import cmds, mel  # noqa\n\nlog = logging.getLogger(__name__)\n\n_gltf_options = {\n    \"of\": str,                  # outputFolder\n    \"cpr\": str,                 # copyright\n    \"sno\": bool,                # selectedNodeOnly\n    \"sn\": str,                  # sceneName\n    \"glb\": bool,                # binary\n    \"nbu\": bool,                # niceBufferURIs\n    \"hbu\": bool,                # hashBufferURI\n    \"ext\": bool,                # externalTextures\n    \"ivt\": int,                 # initialValuesTime\n    \"acn\": str,                 # animationClipName\n    \"ast\": int,                 # animationClipStartTime\n    \"aet\": int,                 # animationClipEndTime\n    \"afr\": float,               # animationClipFrameRate\n    \"dsa\": int,                 # detectStepAnimations\n    \"mpa\": str,                 # meshPrimitiveAttributes\n    \"bpa\": str,                 # blendPrimitiveAttributes\n    \"i32\": bool,                # force32bitIndices\n    \"ssm\": bool,                # skipStandardMaterials\n    \"eut\": bool,                 # excludeUnusedTexcoord\n    \"dm\": bool,                 # defaultMaterial\n    \"cm\": bool,                 # colorizeMaterials\n    \"dmy\": str,                 # dumpMaya\n    \"dgl\": str,                 # dumpGLTF\n    \"imd\": str,                 # ignoreMeshDeformers\n    \"ssc\": bool,                # skipSkinClusters\n    \"sbs\": bool,                # skipBlendShapes\n    \"rvp\": bool,                # redrawViewport\n    \"vno\": bool                 # visibleNodesOnly\n}\n\n\ndef extract_gltf(parent_dir,\n                 filename,\n                 **kwargs):\n\n    \"\"\"Sets GLTF export options from data in the instance.\n\n    \"\"\"\n\n    cmds.loadPlugin('maya2glTF', quiet=True)\n    # load the UI to run mel command\n    mel.eval(\"maya2glTF_UI()\")\n\n    parent_dir = parent_dir.replace('\\\\', '/')\n    options = {\n        \"dsa\": 1,\n        \"glb\": True\n    }\n    options.update(kwargs)\n\n    for key, value in options.copy().items():\n        if key not in _gltf_options:\n            log.warning(\"extract_gltf() does not support option '%s'. \"\n                        \"Flag will be ignored..\", key)\n            options.pop(key)\n            options.pop(value)\n            continue\n\n    job_args = list()\n    default_opt = \"maya2glTF -of \\\"{0}\\\" -sn \\\"{1}\\\"\".format(parent_dir, filename) # noqa\n    job_args.append(default_opt)\n\n    for key, value in options.items():\n        if isinstance(value, str):\n            job_args.append(\"-{0} \\\"{1}\\\"\".format(key, value))\n        elif isinstance(value, bool):\n            if value:\n                job_args.append(\"-{0}\".format(key))\n        else:\n            job_args.append(\"-{0} {1}\".format(key, value))\n\n    job_str = \" \".join(job_args)\n    log.info(\"{}\".format(job_str))\n    mel.eval(job_str)\n\n    # close the gltf export after finish the export\n    gltf_UI = \"maya2glTF_exporter_window\"\n    if cmds.window(gltf_UI, q=True, exists=True):\n        cmds.deleteUI(gltf_UI)\n"
  },
  {
    "path": "openpype/hosts/maya/api/lib.py",
    "content": "\"\"\"Standalone helper functions\"\"\"\n\nimport os\nimport copy\nfrom pprint import pformat\nimport sys\nimport uuid\nimport re\n\nimport json\nimport logging\nimport contextlib\nimport capture\nfrom .exitstack import ExitStack\nfrom collections import OrderedDict, defaultdict\nfrom math import ceil\nfrom six import string_types\n\nfrom maya import cmds, mel\nfrom maya.api import OpenMaya\n\nfrom openpype.client import (\n    get_project,\n    get_asset_by_name,\n    get_subsets,\n    get_last_versions,\n    get_representation_by_name\n)\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import (\n    get_current_project_name,\n    get_current_asset_name,\n    get_current_task_name,\n    discover_loader_plugins,\n    loaders_from_representation,\n    get_representation_path,\n    load_container,\n    registered_host\n)\nfrom openpype.lib import NumberDef\nfrom openpype.pipeline.context_tools import get_current_project_asset\nfrom openpype.pipeline.create import CreateContext\nfrom openpype.lib.profiles_filtering import filter_profiles\n\n\nself = sys.modules[__name__]\nself._parent = None\n\nlog = logging.getLogger(__name__)\n\nIS_HEADLESS = not hasattr(cmds, \"about\") or cmds.about(batch=True)\nATTRIBUTE_DICT = {\"int\": {\"attributeType\": \"long\"},\n                  \"str\": {\"dataType\": \"string\"},\n                  \"unicode\": {\"dataType\": \"string\"},\n                  \"float\": {\"attributeType\": \"double\"},\n                  \"bool\": {\"attributeType\": \"bool\"}}\n\nSHAPE_ATTRS = {\"castsShadows\",\n               \"receiveShadows\",\n               \"motionBlur\",\n               \"primaryVisibility\",\n               \"smoothShading\",\n               \"visibleInReflections\",\n               \"visibleInRefractions\",\n               \"doubleSided\",\n               \"opposite\"}\n\n\nDEFAULT_MATRIX = [1.0, 0.0, 0.0, 0.0,\n                  0.0, 1.0, 0.0, 0.0,\n                  0.0, 0.0, 1.0, 0.0,\n                  0.0, 0.0, 0.0, 1.0]\n\nINT_FPS = {15, 24, 25, 30, 48, 50, 60, 44100, 48000}\nFLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94}\n\n\nDISPLAY_LIGHTS_ENUM = [\n    {\"label\": \"Use Project Settings\", \"value\": \"project_settings\"},\n    {\"label\": \"Default Lighting\", \"value\": \"default\"},\n    {\"label\": \"All Lights\", \"value\": \"all\"},\n    {\"label\": \"Selected Lights\", \"value\": \"selected\"},\n    {\"label\": \"Flat Lighting\", \"value\": \"flat\"},\n    {\"label\": \"No Lights\", \"value\": \"none\"}\n]\n\n\ndef get_main_window():\n    \"\"\"Acquire Maya's main window\"\"\"\n    from qtpy import QtWidgets\n\n    if self._parent is None:\n        self._parent = {\n            widget.objectName(): widget\n            for widget in QtWidgets.QApplication.topLevelWidgets()\n        }[\"MayaWindow\"]\n    return self._parent\n\n\n@contextlib.contextmanager\ndef suspended_refresh(suspend=True):\n    \"\"\"Suspend viewport refreshes\n\n    cmds.ogs(pause=True) is a toggle so we cant pass False.\n    \"\"\"\n    if IS_HEADLESS:\n        yield\n        return\n\n    original_state = cmds.ogs(query=True, pause=True)\n    try:\n        if suspend and not original_state:\n            cmds.ogs(pause=True)\n        yield\n    finally:\n        if suspend and not original_state:\n            cmds.ogs(pause=True)\n\n\n@contextlib.contextmanager\ndef maintained_selection():\n    \"\"\"Maintain selection during context\n\n    Example:\n        >>> scene = cmds.file(new=True, force=True)\n        >>> node = cmds.createNode(\"transform\", name=\"Test\")\n        >>> cmds.select(\"persp\")\n        >>> with maintained_selection():\n        ...     cmds.select(\"Test\", replace=True)\n        >>> \"Test\" in cmds.ls(selection=True)\n        False\n\n    \"\"\"\n\n    previous_selection = cmds.ls(selection=True)\n    try:\n        yield\n    finally:\n        if previous_selection:\n            cmds.select(previous_selection,\n                        replace=True,\n                        noExpand=True)\n        else:\n            cmds.select(clear=True)\n\n\ndef reload_all_udim_tile_previews():\n    \"\"\"Regenerate all UDIM tile preview in texture file\"\"\"\n    for texture_file in cmds.ls(type=\"file\"):\n        if cmds.getAttr(\"{}.uvTilingMode\".format(texture_file)) > 0:\n            cmds.ogs(regenerateUVTilePreview=texture_file)\n\n\n@contextlib.contextmanager\ndef panel_camera(panel, camera):\n    \"\"\"Set modelPanel's camera during the context.\n\n    Arguments:\n        panel (str): modelPanel name.\n        camera (str): camera name.\n\n    \"\"\"\n    original_camera = cmds.modelPanel(panel, query=True, camera=True)\n    try:\n        cmds.modelPanel(panel, edit=True, camera=camera)\n        yield\n    finally:\n        cmds.modelPanel(panel, edit=True, camera=original_camera)\n\n\ndef render_capture_preset(preset):\n    \"\"\"Capture playblast with a preset.\n\n    To generate the preset use `generate_capture_preset`.\n\n    Args:\n        preset (dict): preset options\n\n    Returns:\n        str: Output path of `capture.capture`\n    \"\"\"\n\n    # Force a refresh at the start of the timeline\n    # TODO (Question): Why do we need to do this? What bug does it solve?\n    #   Is this for simulations?\n    cmds.refresh(force=True)\n    refresh_frame_int = int(cmds.playbackOptions(query=True, minTime=True))\n    cmds.currentTime(refresh_frame_int - 1, edit=True)\n    cmds.currentTime(refresh_frame_int, edit=True)\n    log.debug(\n        \"Using preset: {}\".format(\n            json.dumps(preset, indent=4, sort_keys=True)\n        )\n    )\n    preset = copy.deepcopy(preset)\n    # not supported by `capture` so we pop it off of the preset\n    reload_textures = preset[\"viewport_options\"].pop(\"loadTextures\", False)\n    panel = preset.pop(\"panel\")\n    with ExitStack() as stack:\n        stack.enter_context(maintained_time())\n        stack.enter_context(panel_camera(panel, preset[\"camera\"]))\n        stack.enter_context(viewport_default_options(panel, preset))\n        if reload_textures:\n            # Force immediate texture loading when to ensure\n            # all textures have loaded before the playblast starts\n            stack.enter_context(material_loading_mode(mode=\"immediate\"))\n            # Regenerate all UDIM tiles previews\n            reload_all_udim_tile_previews()\n        path = capture.capture(log=self.log, **preset)\n\n    return path\n\n\ndef generate_capture_preset(instance, camera, path,\n                            start=None, end=None, capture_preset=None):\n    \"\"\"Function for getting all the data of preset options for\n    playblast capturing\n\n    Args:\n        instance (pyblish.api.Instance): instance\n        camera (str): review camera\n        path (str): filepath\n        start (int): frameStart\n        end (int): frameEnd\n        capture_preset (dict): capture preset\n\n    Returns:\n        dict: Resulting preset\n    \"\"\"\n    preset = load_capture_preset(data=capture_preset)\n\n    preset[\"camera\"] = camera\n    preset[\"start_frame\"] = start\n    preset[\"end_frame\"] = end\n    preset[\"filename\"] = path\n    preset[\"overwrite\"] = True\n    preset[\"panel\"] = instance.data[\"panel\"]\n\n    # Disable viewer since we use the rendering logic for publishing\n    # We don't want to open the generated playblast in a viewer directly.\n    preset[\"viewer\"] = False\n\n    # \"isolate_view\" will already have been applied at creation, so we'll\n    # ignore it here.\n    preset.pop(\"isolate_view\")\n\n    # Set resolution variables from capture presets\n    width_preset = capture_preset[\"Resolution\"][\"width\"]\n    height_preset = capture_preset[\"Resolution\"][\"height\"]\n\n    # Set resolution variables from asset values\n    asset_data = instance.data[\"assetEntity\"][\"data\"]\n    asset_width = asset_data.get(\"resolutionWidth\")\n    asset_height = asset_data.get(\"resolutionHeight\")\n    review_instance_width = instance.data.get(\"review_width\")\n    review_instance_height = instance.data.get(\"review_height\")\n\n    # Use resolution from instance if review width/height is set\n    # Otherwise use the resolution from preset if it has non-zero values\n    # Otherwise fall back to asset width x height\n    # Else define no width, then `capture.capture` will use render resolution\n    if review_instance_width and review_instance_height:\n        preset[\"width\"] = review_instance_width\n        preset[\"height\"] = review_instance_height\n    elif width_preset and height_preset:\n        preset[\"width\"] = width_preset\n        preset[\"height\"] = height_preset\n    elif asset_width and asset_height:\n        preset[\"width\"] = asset_width\n        preset[\"height\"] = asset_height\n\n    # Isolate view is requested by having objects in the set besides a\n    # camera. If there is only 1 member it'll be the camera because we\n    # validate to have 1 camera only.\n    if instance.data[\"isolate\"] and len(instance.data[\"setMembers\"]) > 1:\n        preset[\"isolate\"] = instance.data[\"setMembers\"]\n\n    # Override camera options\n    # Enforce persisting camera depth of field\n    camera_options = preset.setdefault(\"camera_options\", {})\n    camera_options[\"depthOfField\"] = cmds.getAttr(\n        \"{0}.depthOfField\".format(camera)\n    )\n\n    # Use Pan/Zoom from instance data instead of from preset\n    preset.pop(\"pan_zoom\", None)\n    camera_options[\"panZoomEnabled\"] = instance.data[\"panZoom\"]\n\n    # Override viewport options by instance data\n    viewport_options = preset.setdefault(\"viewport_options\", {})\n    viewport_options[\"displayLights\"] = instance.data[\"displayLights\"]\n    viewport_options[\"imagePlane\"] = instance.data.get(\"imagePlane\", True)\n\n    # Override transparency if requested.\n    transparency = instance.data.get(\"transparency\", 0)\n    if transparency != 0:\n        preset[\"viewport2_options\"][\"transparencyAlgorithm\"] = transparency\n\n    # Update preset with current panel setting\n    # if override_viewport_options is turned off\n    if not capture_preset[\"Viewport Options\"][\"override_viewport_options\"]:\n        panel_preset = capture.parse_view(preset[\"panel\"])\n        panel_preset.pop(\"camera\")\n        preset.update(panel_preset)\n\n    return preset\n\n\n@contextlib.contextmanager\ndef viewport_default_options(panel, preset):\n    \"\"\"Context manager used by `render_capture_preset`.\n\n    We need to explicitly enable some viewport changes so the viewport is\n    refreshed ahead of playblasting.\n\n    \"\"\"\n    # TODO: Clarify in the docstring WHY we need to set it ahead of\n    #  playblasting. What issues does it solve?\n    viewport_defaults = {}\n    try:\n        keys = [\n            \"useDefaultMaterial\",\n            \"wireframeOnShaded\",\n            \"xray\",\n            \"jointXray\",\n            \"backfaceCulling\",\n            \"textures\"\n        ]\n        for key in keys:\n            viewport_defaults[key] = cmds.modelEditor(\n                panel, query=True, **{key: True}\n            )\n            if preset[\"viewport_options\"].get(key):\n                cmds.modelEditor(\n                    panel, edit=True, **{key: True}\n                )\n        yield\n    finally:\n        # Restoring viewport options.\n        if viewport_defaults:\n            cmds.modelEditor(\n                panel, edit=True, **viewport_defaults\n            )\n\n\n@contextlib.contextmanager\ndef material_loading_mode(mode=\"immediate\"):\n    \"\"\"Set material loading mode during context\"\"\"\n    original = cmds.displayPref(query=True, materialLoadingMode=True)\n    cmds.displayPref(materialLoadingMode=mode)\n    try:\n        yield\n    finally:\n        cmds.displayPref(materialLoadingMode=original)\n\n\ndef get_namespace(node):\n    \"\"\"Return namespace of given node\"\"\"\n    node_name = node.rsplit(\"|\", 1)[-1]\n    if \":\" in node_name:\n        return node_name.rsplit(\":\", 1)[0]\n    else:\n        return \"\"\n\n\ndef strip_namespace(node, namespace):\n    \"\"\"Strip given namespace from node path.\n\n    The namespace will only be stripped from names\n    if it starts with that namespace. If the namespace\n    occurs within another namespace it's not removed.\n\n    Examples:\n        >>> strip_namespace(\"namespace:node\", namespace=\"namespace:\")\n        \"node\"\n        >>> strip_namespace(\"hello:world:node\", namespace=\"hello:world\")\n        \"node\"\n        >>> strip_namespace(\"hello:world:node\", namespace=\"hello\")\n        \"world:node\"\n        >>> strip_namespace(\"hello:world:node\", namespace=\"world\")\n        \"hello:world:node\"\n        >>> strip_namespace(\"ns:group|ns:node\", namespace=\"ns\")\n        \"group|node\"\n\n    Returns:\n        str: Node name without given starting namespace.\n\n    \"\"\"\n\n    # Ensure namespace ends with `:`\n    if not namespace.endswith(\":\"):\n        namespace = \"{}:\".format(namespace)\n\n    # The long path for a node can also have the namespace\n    # in its parents so we need to remove it from each\n    return \"|\".join(\n        name[len(namespace):] if name.startswith(namespace) else name\n        for name in node.split(\"|\")\n    )\n\n\ndef get_custom_namespace(custom_namespace):\n    \"\"\"Return unique namespace.\n\n    The input namespace can contain a single group\n    of '#' number tokens to indicate where the namespace's\n    unique index should go. The amount of tokens defines\n    the zero padding of the number, e.g ### turns into 001.\n\n    Warning: Note that a namespace will always be\n        prefixed with a _ if it starts with a digit\n\n    Example:\n        >>> get_custom_namespace(\"myspace_##_\")\n        # myspace_01_\n        >>> get_custom_namespace(\"##_myspace\")\n        # _01_myspace\n        >>> get_custom_namespace(\"myspace##\")\n        # myspace01\n\n    \"\"\"\n    split = re.split(\"([#]+)\", custom_namespace, 1)\n\n    if len(split) == 3:\n        base, padding, suffix = split\n        padding = \"%0{}d\".format(len(padding))\n    else:\n        base = split[0]\n        padding = \"%02d\"  # default padding\n        suffix = \"\"\n\n    return unique_namespace(\n        base,\n        format=padding,\n        prefix=\"_\" if not base or base[0].isdigit() else \"\",\n        suffix=suffix\n    )\n\n\ndef unique_namespace(namespace, format=\"%02d\", prefix=\"\", suffix=\"\"):\n    \"\"\"Return unique namespace\n\n    Arguments:\n        namespace (str): Name of namespace to consider\n        format (str, optional): Formatting of the given iteration number\n        suffix (str, optional): Only consider namespaces with this suffix.\n\n    >>> unique_namespace(\"bar\")\n    # bar01\n    >>> unique_namespace(\":hello\")\n    # :hello01\n    >>> unique_namespace(\"bar:\", suffix=\"_NS\")\n    # bar01_NS:\n\n    \"\"\"\n\n    def current_namespace():\n        current = cmds.namespaceInfo(currentNamespace=True,\n                                     absoluteName=True)\n        # When inside a namespace Maya adds no trailing :\n        if not current.endswith(\":\"):\n            current += \":\"\n        return current\n\n    # Always check against the absolute namespace root\n    # There's no clash with :x if we're defining namespace :a:x\n    ROOT = \":\" if namespace.startswith(\":\") else current_namespace()\n\n    # Strip trailing `:` tokens since we might want to add a suffix\n    start = \":\" if namespace.startswith(\":\") else \"\"\n    end = \":\" if namespace.endswith(\":\") else \"\"\n    namespace = namespace.strip(\":\")\n    if \":\" in namespace:\n        # Split off any nesting that we don't uniqify anyway.\n        parents, namespace = namespace.rsplit(\":\", 1)\n        start += parents + \":\"\n        ROOT += start\n\n    def exists(n):\n        # Check for clash with nodes and namespaces\n        fullpath = ROOT + n\n        return cmds.objExists(fullpath) or cmds.namespace(exists=fullpath)\n\n    iteration = 1\n    while True:\n        nr_namespace = namespace + format % iteration\n        unique = prefix + nr_namespace + suffix\n\n        if not exists(unique):\n            return start + unique + end\n\n        iteration += 1\n\n\ndef read(node):\n    \"\"\"Return user-defined attributes from `node`\"\"\"\n\n    data = dict()\n\n    for attr in cmds.listAttr(node, userDefined=True) or list():\n        try:\n            value = cmds.getAttr(node + \".\" + attr, asString=True)\n\n        except RuntimeError:\n            # For Message type attribute or others that have connections,\n            # take source node name as value.\n            source = cmds.listConnections(node + \".\" + attr,\n                                          source=True,\n                                          destination=False)\n            source = cmds.ls(source, long=True) or [None]\n            value = source[0]\n\n        except ValueError:\n            # Some attributes cannot be read directly,\n            # such as mesh and color attributes. These\n            # are considered non-essential to this\n            # particular publishing pipeline.\n            value = None\n\n        data[attr] = value\n\n    return data\n\n\ndef matrix_equals(a, b, tolerance=1e-10):\n    \"\"\"\n    Compares two matrices with an imperfection tolerance\n\n    Args:\n        a (list, tuple): the matrix to check\n        b (list, tuple): the matrix to check against\n        tolerance (float): the precision of the differences\n\n    Returns:\n        bool : True or False\n\n    \"\"\"\n    if not all(abs(x - y) < tolerance for x, y in zip(a, b)):\n        return False\n    return True\n\n\ndef float_round(num, places=0, direction=ceil):\n    return direction(num * (10**places)) / float(10**places)\n\n\ndef pairwise(iterable):\n    \"\"\"s -> (s0,s1), (s2,s3), (s4, s5), ...\"\"\"\n    from six.moves import zip\n\n    a = iter(iterable)\n    return zip(a, a)\n\n\ndef collect_animation_defs(fps=False):\n    \"\"\"Get the basic animation attribute defintions for the publisher.\n\n    Returns:\n        OrderedDict\n\n    \"\"\"\n\n    # get scene values as defaults\n    frame_start = cmds.playbackOptions(query=True, minTime=True)\n    frame_end = cmds.playbackOptions(query=True, maxTime=True)\n    frame_start_handle = cmds.playbackOptions(\n        query=True, animationStartTime=True\n    )\n    frame_end_handle = cmds.playbackOptions(query=True, animationEndTime=True)\n\n    handle_start = frame_start - frame_start_handle\n    handle_end = frame_end_handle - frame_end\n\n    # build attributes\n    defs = [\n        NumberDef(\"frameStart\",\n                  label=\"Frame Start\",\n                  default=frame_start,\n                  decimals=0),\n        NumberDef(\"frameEnd\",\n                  label=\"Frame End\",\n                  default=frame_end,\n                  decimals=0),\n        NumberDef(\"handleStart\",\n                  label=\"Handle Start\",\n                  default=handle_start,\n                  decimals=0),\n        NumberDef(\"handleEnd\",\n                  label=\"Handle End\",\n                  default=handle_end,\n                  decimals=0),\n        NumberDef(\"step\",\n                  label=\"Step size\",\n                  tooltip=\"A smaller step size means more samples and larger \"\n                          \"output files.\\n\"\n                          \"A 1.0 step size is a single sample every frame.\\n\"\n                          \"A 0.5 step size is two samples per frame.\\n\"\n                          \"A 0.2 step size is five samples per frame.\",\n                  default=1.0,\n                  decimals=3),\n    ]\n\n    if fps:\n        current_fps = mel.eval('currentTimeUnitToFPS()')\n        fps_def = NumberDef(\n            \"fps\", label=\"FPS\", default=current_fps, decimals=5\n        )\n        defs.append(fps_def)\n\n    return defs\n\n\ndef imprint(node, data):\n    \"\"\"Write `data` to `node` as userDefined attributes\n\n    Arguments:\n        node (str): Long name of node\n        data (dict): Dictionary of key/value pairs\n\n    Example:\n        >>> from maya import cmds\n        >>> def compute():\n        ...   return 6\n        ...\n        >>> cube, generator = cmds.polyCube()\n        >>> imprint(cube, {\n        ...   \"regularString\": \"myFamily\",\n        ...   \"computedValue\": lambda: compute()\n        ... })\n        ...\n        >>> cmds.getAttr(cube + \".computedValue\")\n        6\n\n    \"\"\"\n\n    for key, value in data.items():\n\n        if callable(value):\n            # Support values evaluated at imprint\n            value = value()\n\n        if isinstance(value, bool):\n            add_type = {\"attributeType\": \"bool\"}\n            set_type = {\"keyable\": False, \"channelBox\": True}\n        elif isinstance(value, string_types):\n            add_type = {\"dataType\": \"string\"}\n            set_type = {\"type\": \"string\"}\n        elif isinstance(value, int):\n            add_type = {\"attributeType\": \"long\"}\n            set_type = {\"keyable\": False, \"channelBox\": True}\n        elif isinstance(value, float):\n            add_type = {\"attributeType\": \"double\"}\n            set_type = {\"keyable\": False, \"channelBox\": True}\n        elif isinstance(value, (list, tuple)):\n            add_type = {\"attributeType\": \"enum\", \"enumName\": \":\".join(value)}\n            set_type = {\"keyable\": False, \"channelBox\": True}\n            value = 0  # enum default\n        else:\n            raise TypeError(\"Unsupported type: %r\" % type(value))\n\n        cmds.addAttr(node, longName=key, **add_type)\n        cmds.setAttr(node + \".\" + key, value, **set_type)\n\n\ndef lsattr(attr, value=None):\n    \"\"\"Return nodes matching `key` and `value`\n\n    Arguments:\n        attr (str): Name of Maya attribute\n        value (object, optional): Value of attribute. If none\n            is provided, return all nodes with this attribute.\n\n    Example:\n        >> lsattr(\"id\", \"myId\")\n        [\"myNode\"]\n        >> lsattr(\"id\")\n        [\"myNode\", \"myOtherNode\"]\n\n    \"\"\"\n\n    if value is None:\n        return cmds.ls(\"*.%s\" % attr,\n                       recursive=True,\n                       objectsOnly=True,\n                       long=True)\n    return lsattrs({attr: value})\n\n\ndef lsattrs(attrs):\n    \"\"\"Return nodes with the given attribute(s).\n\n    Arguments:\n        attrs (dict): Name and value pairs of expected matches\n\n    Example:\n        >>> # Return nodes with an `age` of five.\n        >>> lsattrs({\"age\": \"five\"})\n        >>> # Return nodes with both `age` and `color` of five and blue.\n        >>> lsattrs({\"age\": \"five\", \"color\": \"blue\"})\n\n    Return:\n         list: matching nodes.\n\n    \"\"\"\n\n    dep_fn = OpenMaya.MFnDependencyNode()\n    dag_fn = OpenMaya.MFnDagNode()\n    selection_list = OpenMaya.MSelectionList()\n\n    first_attr = next(iter(attrs))\n\n    try:\n        selection_list.add(\"*.{0}\".format(first_attr),\n                           searchChildNamespaces=True)\n    except RuntimeError as exc:\n        if str(exc).endswith(\"Object does not exist\"):\n            return []\n\n    matches = set()\n    for i in range(selection_list.length()):\n        node = selection_list.getDependNode(i)\n        if node.hasFn(OpenMaya.MFn.kDagNode):\n            fn_node = dag_fn.setObject(node)\n            full_path_names = [path.fullPathName()\n                               for path in fn_node.getAllPaths()]\n        else:\n            fn_node = dep_fn.setObject(node)\n            full_path_names = [fn_node.name()]\n\n        for attr in attrs:\n            try:\n                plug = fn_node.findPlug(attr, True)\n                if plug.asString() != attrs[attr]:\n                    break\n            except RuntimeError:\n                break\n        else:\n            matches.update(full_path_names)\n\n    return list(matches)\n\n\n@contextlib.contextmanager\ndef attribute_values(attr_values):\n    \"\"\"Remaps node attributes to values during context.\n\n    Arguments:\n        attr_values (dict): Dictionary with (attr, value)\n\n    \"\"\"\n\n    original = [(attr, cmds.getAttr(attr)) for attr in attr_values]\n    try:\n        for attr, value in attr_values.items():\n            if isinstance(value, string_types):\n                cmds.setAttr(attr, value, type=\"string\")\n            else:\n                cmds.setAttr(attr, value)\n        yield\n    finally:\n        for attr, value in original:\n            if isinstance(value, string_types):\n                cmds.setAttr(attr, value, type=\"string\")\n            elif value is None and cmds.getAttr(attr, type=True) == \"string\":\n                # In some cases the maya.cmds.getAttr command returns None\n                # for string attributes but this value cannot assigned.\n                # Note: After setting it once to \"\" it will then return \"\"\n                #       instead of None. So this would only happen once.\n                cmds.setAttr(attr, \"\", type=\"string\")\n            else:\n                cmds.setAttr(attr, value)\n\n\n@contextlib.contextmanager\ndef keytangent_default(in_tangent_type='auto',\n                       out_tangent_type='auto'):\n    \"\"\"Set the default keyTangent for new keys during this context\"\"\"\n\n    original_itt = cmds.keyTangent(query=True, g=True, itt=True)[0]\n    original_ott = cmds.keyTangent(query=True, g=True, ott=True)[0]\n    cmds.keyTangent(g=True, itt=in_tangent_type)\n    cmds.keyTangent(g=True, ott=out_tangent_type)\n    try:\n        yield\n    finally:\n        cmds.keyTangent(g=True, itt=original_itt)\n        cmds.keyTangent(g=True, ott=original_ott)\n\n\n@contextlib.contextmanager\ndef undo_chunk():\n    \"\"\"Open a undo chunk during context.\"\"\"\n\n    try:\n        cmds.undoInfo(openChunk=True)\n        yield\n    finally:\n        cmds.undoInfo(closeChunk=True)\n\n\n@contextlib.contextmanager\ndef evaluation(mode=\"off\"):\n    \"\"\"Set the evaluation manager during context.\n\n    Arguments:\n        mode (str): The mode to apply during context.\n            \"off\": The standard DG evaluation (stable)\n            \"serial\": A serial DG evaluation\n            \"parallel\": The Maya 2016+ parallel evaluation\n\n    \"\"\"\n\n    original = cmds.evaluationManager(query=True, mode=1)[0]\n    try:\n        cmds.evaluationManager(mode=mode)\n        yield\n    finally:\n        cmds.evaluationManager(mode=original)\n\n\n@contextlib.contextmanager\ndef empty_sets(sets, force=False):\n    \"\"\"Remove all members of the sets during the context\"\"\"\n\n    assert isinstance(sets, (list, tuple))\n\n    original = dict()\n    original_connections = []\n\n    # Store original state\n    for obj_set in sets:\n        members = cmds.sets(obj_set, query=True)\n        original[obj_set] = members\n\n    try:\n        for obj_set in sets:\n            cmds.sets(clear=obj_set)\n            if force:\n                # Break all connections if force is enabled, this way we\n                # prevent Maya from exporting any reference nodes which are\n                # connected with placeHolder[x] attributes\n                plug = \"%s.dagSetMembers\" % obj_set\n                connections = cmds.listConnections(plug,\n                                                   source=True,\n                                                   destination=False,\n                                                   plugs=True,\n                                                   connections=True) or []\n                original_connections.extend(connections)\n                for dest, src in pairwise(connections):\n                    cmds.disconnectAttr(src, dest)\n        yield\n    finally:\n\n        for dest, src in pairwise(original_connections):\n            cmds.connectAttr(src, dest)\n\n        # Restore original members\n        _iteritems = getattr(original, \"iteritems\", original.items)\n        for origin_set, members in _iteritems():\n            cmds.sets(members, forceElement=origin_set)\n\n\n@contextlib.contextmanager\ndef renderlayer(layer):\n    \"\"\"Set the renderlayer during the context\n\n    Arguments:\n        layer (str): Name of layer to switch to.\n\n    \"\"\"\n\n    original = cmds.editRenderLayerGlobals(query=True,\n                                           currentRenderLayer=True)\n\n    try:\n        cmds.editRenderLayerGlobals(currentRenderLayer=layer)\n        yield\n    finally:\n        cmds.editRenderLayerGlobals(currentRenderLayer=original)\n\n\nclass delete_after(object):\n    \"\"\"Context Manager that will delete collected nodes after exit.\n\n    This allows to ensure the nodes added to the context are deleted\n    afterwards. This is useful if you want to ensure nodes are deleted\n    even if an error is raised.\n\n    Examples:\n        with delete_after() as delete_bin:\n            cube = maya.cmds.polyCube()\n            delete_bin.extend(cube)\n            # cube exists\n        # cube deleted\n\n    \"\"\"\n\n    def __init__(self, nodes=None):\n\n        self._nodes = list()\n\n        if nodes:\n            self.extend(nodes)\n\n    def append(self, node):\n        self._nodes.append(node)\n\n    def extend(self, nodes):\n        self._nodes.extend(nodes)\n\n    def __iter__(self):\n        return iter(self._nodes)\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, type, value, traceback):\n        if self._nodes:\n            cmds.delete(self._nodes)\n\n\ndef get_current_renderlayer():\n    return cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True)\n\n\ndef get_renderer(layer):\n    with renderlayer(layer):\n        return cmds.getAttr(\"defaultRenderGlobals.currentRenderer\")\n\n\n@contextlib.contextmanager\ndef no_undo(flush=False):\n    \"\"\"Disable the undo queue during the context\n\n    Arguments:\n        flush (bool): When True the undo queue will be emptied when returning\n            from the context losing all undo history. Defaults to False.\n\n    \"\"\"\n    original = cmds.undoInfo(query=True, state=True)\n    keyword = 'state' if flush else 'stateWithoutFlush'\n\n    try:\n        cmds.undoInfo(**{keyword: False})\n        yield\n    finally:\n        cmds.undoInfo(**{keyword: original})\n\n\ndef get_shader_assignments_from_shapes(shapes, components=True):\n    \"\"\"Return the shape assignment per related shading engines.\n\n    Returns a dictionary where the keys are shadingGroups and the values are\n    lists of assigned shapes or shape-components.\n\n    Since `maya.cmds.sets` returns shader members on the shapes as components\n    on the transform we correct that in this method too.\n\n    For the 'shapes' this will return a dictionary like:\n        {\n            \"shadingEngineX\": [\"nodeX\", \"nodeY\"],\n            \"shadingEngineY\": [\"nodeA\", \"nodeB\"]\n        }\n\n    Args:\n        shapes (list): The shapes to collect the assignments for.\n        components (bool): Whether to include the component assignments.\n\n    Returns:\n        dict: The {shadingEngine: shapes} relationships\n\n    \"\"\"\n\n    shapes = cmds.ls(shapes,\n                     long=True,\n                     shapes=True,\n                     objectsOnly=True)\n    if not shapes:\n        return {}\n\n    # Collect shading engines and their shapes\n    assignments = defaultdict(list)\n    for shape in shapes:\n\n        # Get unique shading groups for the shape\n        shading_groups = cmds.listConnections(shape,\n                                              source=False,\n                                              destination=True,\n                                              plugs=False,\n                                              connections=False,\n                                              type=\"shadingEngine\") or []\n        shading_groups = list(set(shading_groups))\n        for shading_group in shading_groups:\n            assignments[shading_group].append(shape)\n\n    if components:\n        # Note: Components returned from maya.cmds.sets are \"listed\" as if\n        # being assigned to the transform like: pCube1.f[0] as opposed\n        # to pCubeShape1.f[0] so we correct that here too.\n\n        # Build a mapping from parent to shapes to include in lookup.\n        transforms = {shape.rsplit(\"|\", 1)[0]: shape for shape in shapes}\n        lookup = set(shapes) | set(transforms.keys())\n\n        component_assignments = defaultdict(list)\n        for shading_group in assignments.keys():\n            members = cmds.ls(cmds.sets(shading_group, query=True), long=True)\n            for member in members:\n\n                node = member.split(\".\", 1)[0]\n                if node not in lookup:\n                    continue\n\n                # Component\n                if \".\" in member:\n\n                    # Fix transform to shape as shaders are assigned to shapes\n                    if node in transforms:\n                        shape = transforms[node]\n                        component = member.split(\".\", 1)[1]\n                        member = \"{0}.{1}\".format(shape, component)\n\n                component_assignments[shading_group].append(member)\n        assignments = component_assignments\n\n    return dict(assignments)\n\n\n@contextlib.contextmanager\ndef shader(nodes, shadingEngine=\"initialShadingGroup\"):\n    \"\"\"Assign a shader to nodes during the context\"\"\"\n\n    shapes = cmds.ls(nodes, dag=1, objectsOnly=1, shapes=1, long=1)\n    original = get_shader_assignments_from_shapes(shapes)\n\n    try:\n        # Assign override shader\n        if shapes:\n            cmds.sets(shapes, edit=True, forceElement=shadingEngine)\n        yield\n    finally:\n\n        # Assign original shaders\n        for sg, members in original.items():\n            if members:\n                cmds.sets(members, edit=True, forceElement=sg)\n\n\n@contextlib.contextmanager\ndef displaySmoothness(nodes,\n                      divisionsU=0,\n                      divisionsV=0,\n                      pointsWire=4,\n                      pointsShaded=1,\n                      polygonObject=1):\n    \"\"\"Set the displaySmoothness during the context\"\"\"\n\n    # Ensure only non-intermediate shapes\n    nodes = cmds.ls(nodes,\n                    dag=1,\n                    shapes=1,\n                    long=1,\n                    noIntermediate=True)\n\n    def parse(node):\n        \"\"\"Parse the current state of a node\"\"\"\n        state = {}\n        for key in [\"divisionsU\",\n                    \"divisionsV\",\n                    \"pointsWire\",\n                    \"pointsShaded\",\n                    \"polygonObject\"]:\n            value = cmds.displaySmoothness(node, query=1, **{key: True})\n            if value is not None:\n                state[key] = value[0]\n        return state\n\n    originals = dict((node, parse(node)) for node in nodes)\n\n    try:\n        # Apply current state\n        cmds.displaySmoothness(nodes,\n                               divisionsU=divisionsU,\n                               divisionsV=divisionsV,\n                               pointsWire=pointsWire,\n                               pointsShaded=pointsShaded,\n                               polygonObject=polygonObject)\n        yield\n    finally:\n        # Revert state\n        _iteritems = getattr(originals, \"iteritems\", originals.items)\n        for node, state in _iteritems():\n            if state:\n                cmds.displaySmoothness(node, **state)\n\n\n@contextlib.contextmanager\ndef no_display_layers(nodes):\n    \"\"\"Ensure nodes are not in a displayLayer during context.\n\n    Arguments:\n        nodes (list): The nodes to remove from any display layer.\n\n    \"\"\"\n\n    # Ensure long names\n    nodes = cmds.ls(nodes, long=True)\n\n    # Get the original state\n    lookup = set(nodes)\n    original = {}\n    for layer in cmds.ls(type='displayLayer'):\n\n        # Skip default layer\n        if layer == \"defaultLayer\":\n            continue\n\n        members = cmds.editDisplayLayerMembers(layer,\n                                               query=True,\n                                               fullNames=True)\n        if not members:\n            continue\n        members = set(members)\n\n        included = lookup.intersection(members)\n        if included:\n            original[layer] = list(included)\n\n    try:\n        # Add all nodes to default layer\n        cmds.editDisplayLayerMembers(\"defaultLayer\", nodes, noRecurse=True)\n        yield\n    finally:\n        # Restore original members\n        _iteritems = getattr(original, \"iteritems\", original.items)\n        for layer, members in _iteritems():\n            cmds.editDisplayLayerMembers(layer, members, noRecurse=True)\n\n\n@contextlib.contextmanager\ndef namespaced(namespace, new=True, relative_names=None):\n    \"\"\"Work inside namespace during context\n\n    Args:\n        new (bool): When enabled this will rename the namespace to a unique\n            namespace if the input namespace already exists.\n\n    Yields:\n        str: The namespace that is used during the context\n\n    \"\"\"\n    original = cmds.namespaceInfo(cur=True, absoluteName=True)\n    original_relative_names = cmds.namespace(query=True, relativeNames=True)\n    if new:\n        namespace = unique_namespace(namespace)\n        cmds.namespace(add=namespace)\n    if relative_names is not None:\n        cmds.namespace(relativeNames=relative_names)\n    try:\n        cmds.namespace(set=namespace)\n        yield namespace\n    finally:\n        cmds.namespace(set=original)\n        if relative_names is not None:\n            cmds.namespace(relativeNames=original_relative_names)\n\n\n@contextlib.contextmanager\ndef maintained_selection_api():\n    \"\"\"Maintain selection using the Maya Python API.\n\n    Warning: This is *not* added to the undo stack.\n\n    \"\"\"\n    original = OpenMaya.MGlobal.getActiveSelectionList()\n    try:\n        yield\n    finally:\n        OpenMaya.MGlobal.setActiveSelectionList(original)\n\n\n@contextlib.contextmanager\ndef tool(context):\n    \"\"\"Set a tool context during the context manager.\n\n    \"\"\"\n    original = cmds.currentCtx()\n    try:\n        cmds.setToolTo(context)\n        yield\n    finally:\n        cmds.setToolTo(original)\n\n\ndef polyConstraint(components, *args, **kwargs):\n    \"\"\"Return the list of *components* with the constraints applied.\n\n    A wrapper around Maya's `polySelectConstraint` to retrieve its results as\n    a list without altering selections. For a list of possible constraints\n    see `maya.cmds.polySelectConstraint` documentation.\n\n    Arguments:\n        components (list): List of components of polygon meshes\n\n    Returns:\n        list: The list of components filtered by the given constraints.\n\n    \"\"\"\n\n    kwargs.pop('mode', None)\n\n    with no_undo(flush=False):\n        # Reverting selection to the original selection using\n        # `maya.cmds.select` can be slow in rare cases where previously\n        # `maya.cmds.polySelectConstraint` had set constrain to \"All and Next\"\n        # and the \"Random\" setting was activated. To work around this we\n        # revert to the original selection using the Maya API. This is safe\n        # since we're not generating any undo change anyway.\n        with tool(\"selectSuperContext\"):\n            # Selection can be very slow when in a manipulator mode.\n            # So we force the selection context which is fast.\n            with maintained_selection_api():\n                # Apply constraint using mode=2 (current and next) so\n                # it applies to the selection made before it; because just\n                # a `maya.cmds.select()` call will not trigger the constraint.\n                with reset_polySelectConstraint():\n                    cmds.select(components, r=1, noExpand=True)\n                    cmds.polySelectConstraint(*args, mode=2, **kwargs)\n                    result = cmds.ls(selection=True)\n                    cmds.select(clear=True)\n                    return result\n\n\n@contextlib.contextmanager\ndef reset_polySelectConstraint(reset=True):\n    \"\"\"Context during which the given polyConstraint settings are disabled.\n\n    The original settings are restored after the context.\n\n    \"\"\"\n\n    original = cmds.polySelectConstraint(query=True, stateString=True)\n\n    try:\n        if reset:\n            # Ensure command is available in mel\n            # This can happen when running standalone\n            if not mel.eval(\"exists resetPolySelectConstraint\"):\n                mel.eval(\"source polygonConstraint\")\n\n            # Reset all parameters\n            mel.eval(\"resetPolySelectConstraint;\")\n        cmds.polySelectConstraint(disable=True)\n        yield\n    finally:\n        mel.eval(original)\n\n\ndef is_visible(node,\n               displayLayer=True,\n               intermediateObject=True,\n               parentHidden=True,\n               visibility=True):\n    \"\"\"Is `node` visible?\n\n    Returns whether a node is hidden by one of the following methods:\n    - The node exists (always checked)\n    - The node must be a dagNode (always checked)\n    - The node's visibility is off.\n    - The node is set as intermediate Object.\n    - The node is in a disabled displayLayer.\n    - Whether any of its parent nodes is hidden.\n\n    Roughly based on: http://ewertb.soundlinker.com/mel/mel.098.php\n\n    Returns:\n        bool: Whether the node is visible in the scene\n\n    \"\"\"\n\n    # Only existing objects can be visible\n    if not cmds.objExists(node):\n        return False\n\n    # Only dagNodes can be visible\n    if not cmds.objectType(node, isAType='dagNode'):\n        return False\n\n    if visibility:\n        if not cmds.getAttr('{0}.visibility'.format(node)):\n            return False\n\n    if intermediateObject and cmds.objectType(node, isAType='shape'):\n        if cmds.getAttr('{0}.intermediateObject'.format(node)):\n            return False\n\n    if displayLayer:\n        # Display layers set overrideEnabled and overrideVisibility on members\n        if cmds.attributeQuery('overrideEnabled', node=node, exists=True):\n            override_enabled = cmds.getAttr('{}.overrideEnabled'.format(node))\n            override_visibility = cmds.getAttr('{}.overrideVisibility'.format(\n                node))\n            if override_enabled and not override_visibility:\n                return False\n\n    if parentHidden:\n        parents = cmds.listRelatives(node, parent=True, fullPath=True)\n        if parents:\n            parent = parents[0]\n            if not is_visible(parent,\n                              displayLayer=displayLayer,\n                              intermediateObject=False,\n                              parentHidden=parentHidden,\n                              visibility=visibility):\n                return False\n\n    return True\n\n# region ID\ndef get_id_required_nodes(referenced_nodes=False, nodes=None):\n    \"\"\"Filter out any node which are locked (reference) or readOnly\n\n    Args:\n        referenced_nodes (bool): set True to filter out reference nodes\n        nodes (list, Optional): nodes to consider\n    Returns:\n        nodes (set): list of filtered nodes\n    \"\"\"\n\n    lookup = None\n    if nodes is None:\n        # Consider all nodes\n        nodes = cmds.ls()\n    else:\n        # Build a lookup for the only allowed nodes in output based\n        # on `nodes` input of the function (+ ensure long names)\n        lookup = set(cmds.ls(nodes, long=True))\n\n    def _node_type_exists(node_type):\n        try:\n            cmds.nodeType(node_type, isTypeName=True)\n            return True\n        except RuntimeError:\n            return False\n\n    # `readOnly` flag is obsolete as of Maya 2016 therefore we explicitly\n    # remove default nodes and reference nodes\n    camera_shapes = [\"frontShape\", \"sideShape\", \"topShape\", \"perspShape\"]\n\n    ignore = set()\n    if not referenced_nodes:\n        ignore |= set(cmds.ls(long=True, referencedNodes=True))\n\n    # list all defaultNodes to filter out from the rest\n    ignore |= set(cmds.ls(long=True, defaultNodes=True))\n    ignore |= set(cmds.ls(camera_shapes, long=True))\n\n    # Remove Turtle from the result of `cmds.ls` if Turtle is loaded\n    # TODO: This should be a less specific check for a single plug-in.\n    if _node_type_exists(\"ilrBakeLayer\"):\n        ignore |= set(cmds.ls(type=\"ilrBakeLayer\", long=True))\n\n    # Establish set of nodes types to include\n    types = [\"objectSet\", \"file\", \"mesh\", \"nurbsCurve\", \"nurbsSurface\"]\n\n    # Check if plugin nodes are available for Maya by checking if the plugin\n    # is loaded\n    if cmds.pluginInfo(\"pgYetiMaya\", query=True, loaded=True):\n        types.append(\"pgYetiMaya\")\n\n    # We *always* ignore intermediate shapes, so we filter them out directly\n    nodes = cmds.ls(nodes, type=types, long=True, noIntermediate=True)\n\n    # The items which need to pass the id to their parent\n    # Add the collected transform to the nodes\n    dag = cmds.ls(nodes, type=\"dagNode\", long=True)  # query only dag nodes\n    transforms = cmds.listRelatives(dag,\n                                    parent=True,\n                                    fullPath=True) or []\n\n    nodes = set(nodes)\n    nodes |= set(transforms)\n\n    nodes -= ignore  # Remove the ignored nodes\n    if not nodes:\n        return nodes\n\n    # Ensure only nodes from the input `nodes` are returned when a\n    # filter was applied on function call because we also iterated\n    # to parents and alike\n    if lookup is not None:\n        nodes &= lookup\n\n    # Avoid locked nodes\n    nodes_list = list(nodes)\n    locked = cmds.lockNode(nodes_list, query=True, lock=True)\n    for node, lock in zip(nodes_list, locked):\n        if lock:\n            log.warning(\"Skipping locked node: %s\" % node)\n            nodes.remove(node)\n\n    return nodes\n\n\ndef get_id(node):\n    \"\"\"Get the `cbId` attribute of the given node.\n\n    Args:\n        node (str): the name of the node to retrieve the attribute from\n    Returns:\n        str\n\n    \"\"\"\n    if node is None:\n        return\n\n    sel = OpenMaya.MSelectionList()\n    sel.add(node)\n\n    api_node = sel.getDependNode(0)\n    fn = OpenMaya.MFnDependencyNode(api_node)\n\n    if not fn.hasAttribute(\"cbId\"):\n        return\n\n    try:\n        return fn.findPlug(\"cbId\", False).asString()\n    except RuntimeError:\n        log.warning(\"Failed to retrieve cbId on %s\", node)\n        return\n\n\ndef generate_ids(nodes, asset_id=None):\n    \"\"\"Returns new unique ids for the given nodes.\n\n    Note: This does not assign the new ids, it only generates the values.\n\n    To assign new ids using this method:\n    >>> nodes = [\"a\", \"b\", \"c\"]\n    >>> for node, id in generate_ids(nodes):\n    >>>     set_id(node, id)\n\n    To also override any existing values (and assign regenerated ids):\n    >>> nodes = [\"a\", \"b\", \"c\"]\n    >>> for node, id in generate_ids(nodes):\n    >>>     set_id(node, id, overwrite=True)\n\n    Args:\n        nodes (list): List of nodes.\n        asset_id (str or bson.ObjectId): The database id for the *asset* to\n            generate for. When None provided the current asset in the\n            active session is used.\n\n    Returns:\n        list: A list of (node, id) tuples.\n\n    \"\"\"\n\n    if asset_id is None:\n        # Get the asset ID from the database for the asset of current context\n        project_name = get_current_project_name()\n        asset_name = get_current_asset_name()\n        asset_doc = get_asset_by_name(project_name, asset_name, fields=[\"_id\"])\n        assert asset_doc, \"No current asset found in Session\"\n        asset_id = asset_doc['_id']\n\n    node_ids = []\n    for node in nodes:\n        _, uid = str(uuid.uuid4()).rsplit(\"-\", 1)\n        unique_id = \"{}:{}\".format(asset_id, uid)\n        node_ids.append((node, unique_id))\n\n    return node_ids\n\n\ndef set_id(node, unique_id, overwrite=False):\n    \"\"\"Add cbId to `node` unless one already exists.\n\n    Args:\n        node (str): the node to add the \"cbId\" on\n        unique_id (str): The unique node id to assign.\n            This should be generated by `generate_ids`.\n        overwrite (bool, optional): When True overrides the current value even\n            if `node` already has an id. Defaults to False.\n\n    Returns:\n        None\n\n    \"\"\"\n\n    exists = cmds.attributeQuery(\"cbId\", node=node, exists=True)\n\n    # Add the attribute if it does not exist yet\n    if not exists:\n        cmds.addAttr(node, longName=\"cbId\", dataType=\"string\")\n\n    # Set the value\n    if not exists or overwrite:\n        attr = \"{0}.cbId\".format(node)\n        cmds.setAttr(attr, unique_id, type=\"string\")\n\n\ndef get_attribute(plug,\n                  asString=False,\n                  expandEnvironmentVariables=False,\n                  **kwargs):\n    \"\"\"Maya getAttr with some fixes based on `pymel.core.general.getAttr()`.\n\n    Like Pymel getAttr this applies some changes to `maya.cmds.getAttr`\n      - maya pointlessly returned vector results as a tuple wrapped in a list\n        (ex.  '[(1,2,3)]'). This command unpacks the vector for you.\n      - when getting a multi-attr, maya would raise an error, but this will\n        return a list of values for the multi-attr\n      - added support for getting message attributes by returning the\n        connections instead\n\n    Note that the asString + expandEnvironmentVariables argument naming\n    convention matches the `maya.cmds.getAttr` arguments so that it can\n    act as a direct replacement for it.\n\n    Args:\n        plug (str): Node's attribute plug as `node.attribute`\n        asString (bool): Return string value for enum attributes instead\n            of the index. Note that the return value can be dependent on the\n            UI language Maya is running in.\n        expandEnvironmentVariables (bool): Expand any environment variable and\n            (tilde characters on UNIX) found in string attributes which are\n            returned.\n\n    Kwargs:\n        Supports the keyword arguments of `maya.cmds.getAttr`\n\n    Returns:\n        object: The value of the maya attribute.\n\n    \"\"\"\n    attr_type = cmds.getAttr(plug, type=True)\n    if asString:\n        kwargs[\"asString\"] = True\n    if expandEnvironmentVariables:\n        kwargs[\"expandEnvironmentVariables\"] = True\n    try:\n        res = cmds.getAttr(plug, **kwargs)\n    except RuntimeError:\n        if attr_type == \"message\":\n            return cmds.listConnections(plug)\n\n        node, attr = plug.split(\".\", 1)\n        children = cmds.attributeQuery(attr, node=node, listChildren=True)\n        if children:\n            return [\n                get_attribute(\"{}.{}\".format(node, child))\n                for child in children\n            ]\n\n        raise\n\n    # Convert vector result wrapped in tuple\n    if isinstance(res, list) and len(res):\n        if isinstance(res[0], tuple) and len(res):\n            if attr_type in {'pointArray', 'vectorArray'}:\n                return res\n            return res[0]\n\n    return res\n\n\ndef set_attribute(attribute, value, node):\n    \"\"\"Adjust attributes based on the value from the attribute data\n\n    If an attribute does not exists on the target it will be added with\n    the dataType being controlled by the value type.\n\n    Args:\n        attribute (str): name of the attribute to change\n        value: the value to change to attribute to\n        node (str): name of the node\n\n    Returns:\n        None\n    \"\"\"\n\n    value_type = type(value).__name__\n    kwargs = ATTRIBUTE_DICT[value_type]\n    if not cmds.attributeQuery(attribute, node=node, exists=True):\n        log.debug(\"Creating attribute '{}' on \"\n                  \"'{}'\".format(attribute, node))\n        cmds.addAttr(node, longName=attribute, **kwargs)\n\n    node_attr = \"{}.{}\".format(node, attribute)\n    enum_type = cmds.attributeQuery(attribute, node=node, enum=True)\n    if enum_type and value_type == \"str\":\n        enum_string_values = cmds.attributeQuery(\n            attribute, node=node, listEnum=True\n        )[0].split(\":\")\n        cmds.setAttr(\n            \"{}.{}\".format(node, attribute), enum_string_values.index(value)\n        )\n    elif \"dataType\" in kwargs:\n        attr_type = kwargs[\"dataType\"]\n        cmds.setAttr(node_attr, value, type=attr_type)\n    else:\n        cmds.setAttr(node_attr, value)\n\n\ndef apply_attributes(attributes, nodes_by_id):\n    \"\"\"Alter the attributes to match the state when publishing\n\n    Apply attribute settings from the publish to the node in the scene based\n    on the UUID which is stored in the cbId attribute.\n\n    Args:\n        attributes (list): list of dictionaries\n        nodes_by_id (dict): collection of nodes based on UUID\n                           {uuid: [node, node]}\n\n    \"\"\"\n\n    for attr_data in attributes:\n        nodes = nodes_by_id[attr_data[\"uuid\"]]\n        attr_value = attr_data[\"attributes\"]\n        for node in nodes:\n            for attr, value in attr_value.items():\n                set_attribute(attr, value, node)\n\n\ndef get_container_members(container):\n    \"\"\"Returns the members of a container.\n    This includes the nodes from any loaded references in the container.\n    \"\"\"\n    if isinstance(container, dict):\n        # Assume it's a container dictionary\n        container = container[\"objectName\"]\n\n    members = cmds.sets(container, query=True) or []\n    members = cmds.ls(members, long=True, objectsOnly=True) or []\n    all_members = set(members)\n\n    # Include any referenced nodes from any reference in the container\n    # This is required since we've removed adding ALL nodes of a reference\n    # into the container set and only add the reference node now.\n    for ref in cmds.ls(members, exactType=\"reference\", objectsOnly=True):\n\n        # Ignore any `:sharedReferenceNode`\n        if ref.rsplit(\":\", 1)[-1].startswith(\"sharedReferenceNode\"):\n            continue\n\n        # Ignore _UNKNOWN_REF_NODE_ (PLN-160)\n        if ref.rsplit(\":\", 1)[-1].startswith(\"_UNKNOWN_REF_NODE_\"):\n            continue\n\n        reference_members = cmds.referenceQuery(ref, nodes=True, dagPath=True)\n        reference_members = cmds.ls(reference_members,\n                                    long=True,\n                                    objectsOnly=True)\n        all_members.update(reference_members)\n\n    return list(all_members)\n\n\n# region LOOKDEV\ndef list_looks(project_name, asset_id):\n    \"\"\"Return all look subsets for the given asset\n\n    This assumes all look subsets start with \"look*\" in their names.\n    \"\"\"\n    # # get all subsets with look leading in\n    # the name associated with the asset\n    # TODO this should probably look for family 'look' instead of checking\n    #   subset name that can not start with family\n    subset_docs = get_subsets(project_name, asset_ids=[asset_id])\n    return [\n        subset_doc\n        for subset_doc in subset_docs\n        if subset_doc[\"name\"].startswith(\"look\")\n    ]\n\n\ndef assign_look_by_version(nodes, version_id):\n    \"\"\"Assign nodes a specific published look version by id.\n\n    This assumes the nodes correspond with the asset.\n\n    Args:\n        nodes(list): nodes to assign look to\n        version_id (bson.ObjectId): database id of the version\n\n    Returns:\n        None\n    \"\"\"\n\n    project_name = get_current_project_name()\n\n    # Get representations of shader file and relationships\n    look_representation = get_representation_by_name(\n        project_name, \"ma\", version_id\n    )\n    json_representation = get_representation_by_name(\n        project_name, \"json\", version_id\n    )\n\n    # See if representation is already loaded, if so reuse it.\n    host = registered_host()\n    representation_id = str(look_representation['_id'])\n    for container in host.ls():\n        if (container['loader'] == \"LookLoader\" and\n                container['representation'] == representation_id):\n            log.info(\"Reusing loaded look ..\")\n            container_node = container['objectName']\n            break\n    else:\n        log.info(\"Using look for the first time ..\")\n\n        # Load file\n        _loaders = discover_loader_plugins()\n        loaders = loaders_from_representation(_loaders, representation_id)\n        Loader = next((i for i in loaders if i.__name__ == \"LookLoader\"), None)\n        if Loader is None:\n            raise RuntimeError(\"Could not find LookLoader, this is a bug\")\n\n        # Reference the look file\n        with maintained_selection():\n            container_node = load_container(Loader, look_representation)\n\n    # Get container members\n    shader_nodes = get_container_members(container_node)\n\n    # Load relationships\n    shader_relation = get_representation_path(json_representation)\n    with open(shader_relation, \"r\") as f:\n        relationships = json.load(f)\n\n    # Assign relationships\n    apply_shaders(relationships, shader_nodes, nodes)\n\n\ndef assign_look(nodes, subset=\"lookDefault\"):\n    \"\"\"Assigns a look to a node.\n\n    Optimizes the nodes by grouping by asset id and finding\n    related subset by name.\n\n    Args:\n        nodes (list): all nodes to assign the look to\n        subset (str): name of the subset to find\n    \"\"\"\n\n    # Group all nodes per asset id\n    grouped = defaultdict(list)\n    for node in nodes:\n        pype_id = get_id(node)\n        if not pype_id:\n            continue\n\n        parts = pype_id.split(\":\", 1)\n        grouped[parts[0]].append(node)\n\n    project_name = get_current_project_name()\n    subset_docs = get_subsets(\n        project_name, subset_names=[subset], asset_ids=grouped.keys()\n    )\n    subset_docs_by_asset_id = {\n        str(subset_doc[\"parent\"]): subset_doc\n        for subset_doc in subset_docs\n    }\n    subset_ids = {\n        subset_doc[\"_id\"]\n        for subset_doc in subset_docs_by_asset_id.values()\n    }\n    last_version_docs = get_last_versions(\n        project_name,\n        subset_ids=subset_ids,\n        fields=[\"_id\", \"name\", \"data.families\"]\n    )\n    last_version_docs_by_subset_id = {\n        last_version_doc[\"parent\"]: last_version_doc\n        for last_version_doc in last_version_docs\n    }\n\n    for asset_id, asset_nodes in grouped.items():\n        # create objectId for database\n        subset_doc = subset_docs_by_asset_id.get(asset_id)\n        if not subset_doc:\n            log.warning(\"No subset '{}' found for {}\".format(subset, asset_id))\n            continue\n\n        last_version = last_version_docs_by_subset_id.get(subset_doc[\"_id\"])\n        if not last_version:\n            log.warning((\n                \"Not found last version for subset '{}' on asset with id {}\"\n            ).format(subset, asset_id))\n            continue\n\n        families = last_version.get(\"data\", {}).get(\"families\") or []\n        if \"look\" not in families:\n            log.warning((\n                \"Last version for subset '{}' on asset with id {}\"\n                \" does not have look family\"\n            ).format(subset, asset_id))\n            continue\n\n        log.debug(\"Assigning look '{}' <v{:03d}>\".format(\n            subset, last_version[\"name\"]))\n\n        assign_look_by_version(asset_nodes, last_version[\"_id\"])\n\n\ndef apply_shaders(relationships, shadernodes, nodes):\n    \"\"\"Link shadingEngine to the right nodes based on relationship data\n\n    Relationship data is constructed of a collection of `sets` and `attributes`\n    `sets` corresponds with the shaderEngines found in the lookdev.\n    Each set has the keys `name`, `members` and `uuid`, the `members`\n    hold a collection of node information `name` and `uuid`.\n\n    Args:\n        relationships (dict): relationship data\n        shadernodes (list): list of nodes of the shading objectSets (includes\n        VRayObjectProperties and shadingEngines)\n        nodes (list): list of nodes to apply shader to\n\n    Returns:\n        None\n    \"\"\"\n\n    attributes = relationships.get(\"attributes\", [])\n    shader_data = relationships.get(\"relationships\", {})\n\n    shading_engines = cmds.ls(shadernodes, type=\"objectSet\", long=True)\n    assert shading_engines, \"Error in retrieving objectSets from reference\"\n\n    # region compute lookup\n    nodes_by_id = defaultdict(list)\n    for node in nodes:\n        nodes_by_id[get_id(node)].append(node)\n\n    shading_engines_by_id = defaultdict(list)\n    for shad in shading_engines:\n        shading_engines_by_id[get_id(shad)].append(shad)\n    # endregion\n\n    # region assign shading engines and other sets\n    for data in shader_data.values():\n        # collect all unique IDs of the set members\n        shader_uuid = data[\"uuid\"]\n        member_uuids = [member[\"uuid\"] for member in data[\"members\"]]\n\n        filtered_nodes = list()\n        for m_uuid in member_uuids:\n            filtered_nodes.extend(nodes_by_id[m_uuid])\n\n        id_shading_engines = shading_engines_by_id[shader_uuid]\n        if not id_shading_engines:\n            log.error(\"No shader found with cbId \"\n                      \"'{}'\".format(shader_uuid))\n            continue\n        elif len(id_shading_engines) > 1:\n            log.error(\"Skipping shader assignment. \"\n                      \"More than one shader found with cbId \"\n                      \"'{}'. (found: {})\".format(shader_uuid,\n                                                 id_shading_engines))\n            continue\n\n        if not filtered_nodes:\n            log.warning(\"No nodes found for shading engine \"\n                        \"'{0}'\".format(id_shading_engines[0]))\n            continue\n        try:\n            cmds.sets(filtered_nodes, forceElement=id_shading_engines[0])\n        except RuntimeError as rte:\n            log.error(\"Error during shader assignment: {}\".format(rte))\n\n    # endregion\n\n    apply_attributes(attributes, nodes_by_id)\n\n\n# endregion LOOKDEV\ndef get_isolate_view_sets():\n    \"\"\"Return isolate view sets of all modelPanels.\n\n    Returns:\n        list: all sets related to isolate view\n\n    \"\"\"\n\n    view_sets = set()\n    for panel in cmds.getPanel(type=\"modelPanel\") or []:\n        view_set = cmds.modelEditor(panel, query=True, viewObjects=True)\n        if view_set:\n            view_sets.add(view_set)\n\n    return view_sets\n\n\ndef get_related_sets(node):\n    \"\"\"Return objectSets that are relationships for a look for `node`.\n\n    Filters out based on:\n    - id attribute is NOT `pyblish.avalon.container`\n    - shapes and deformer shapes (alembic creates meshShapeDeformed)\n    - set name ends with any from a predefined list\n    - set in not in viewport set (isolate selected for example)\n\n    Args:\n        node (str): name of the current node to check\n\n    Returns:\n        list: The related sets\n\n    \"\"\"\n\n    # Ignore specific suffices\n    ignore_suffices = [\"out_SET\", \"controls_SET\", \"_INST\", \"_CON\"]\n\n    # Default nodes to ignore\n    defaults = {\"defaultLightSet\", \"defaultObjectSet\"}\n\n    # Ids to ignore\n    ignored = {\"pyblish.avalon.instance\", \"pyblish.avalon.container\"}\n\n    view_sets = get_isolate_view_sets()\n\n    sets = cmds.listSets(object=node, extendToShape=False)\n    if not sets:\n        return []\n\n    # Fix 'no object matches name' errors on nodes returned by listSets.\n    # In rare cases it can happen that a node is added to an internal maya\n    # set inaccessible by maya commands, for example check some nodes\n    # returned by `cmds.listSets(allSets=True)`\n    sets = cmds.ls(sets)\n\n    # Ignore `avalon.container`\n    sets = [s for s in sets if\n            not cmds.attributeQuery(\"id\", node=s, exists=True) or\n            not cmds.getAttr(\"%s.id\" % s) in ignored]\n\n    # Exclude deformer sets (`type=2` for `maya.cmds.listSets`)\n    deformer_sets = cmds.listSets(object=node,\n                                  extendToShape=False,\n                                  type=2) or []\n    deformer_sets = set(deformer_sets)  # optimize lookup\n    sets = [s for s in sets if s not in deformer_sets]\n\n    # Ignore when the set has a specific suffix\n    sets = [s for s in sets if not any(s.endswith(x) for x in ignore_suffices)]\n\n    # Ignore viewport filter view sets (from isolate select and\n    # viewports)\n    sets = [s for s in sets if s not in view_sets]\n    sets = [s for s in sets if s not in defaults]\n\n    return sets\n\n\ndef get_container_transforms(container, members=None, root=False):\n    \"\"\"Retrieve the root node of the container content\n\n    When a container is created through a Loader the content\n    of the file will be grouped under a transform. The name of the root\n    transform is stored in the container information\n\n    Args:\n        container (dict): the container\n        members (list): optional and convenience argument\n        root (bool): return highest node in hierarchy if True\n\n    Returns:\n        root (list / str):\n    \"\"\"\n\n    if not members:\n        members = get_container_members(container)\n\n    results = cmds.ls(members, type=\"transform\", long=True)\n    if root:\n        root = get_highest_in_hierarchy(results)\n        if root:\n            results = root[0]\n\n    return results\n\n\ndef get_highest_in_hierarchy(nodes):\n    \"\"\"Return highest nodes in the hierarchy that are in the `nodes` list.\n\n    The \"highest in hierarchy\" are the nodes closest to world: top-most level.\n\n    Args:\n        nodes (list): The nodes in which find the highest in hierarchies.\n\n    Returns:\n        list: The highest nodes from the input nodes.\n\n    \"\"\"\n\n    # Ensure we use long names\n    nodes = cmds.ls(nodes, long=True)\n    lookup = set(nodes)\n\n    highest = []\n    for node in nodes:\n        # If no parents are within the nodes input list\n        # then this is a highest node\n        if not any(n in lookup for n in iter_parents(node)):\n            highest.append(node)\n\n    return highest\n\n\ndef iter_parents(node):\n    \"\"\"Iter parents of node from its long name.\n\n    Note: The `node` *must* be the long node name.\n\n    Args:\n        node (str): Node long name.\n\n    Yields:\n        str: All parent node names (long names)\n\n    \"\"\"\n    while True:\n        split = node.rsplit(\"|\", 1)\n        if len(split) == 1 or not split[0]:\n            return\n\n        node = split[0]\n        yield node\n\n\ndef remove_other_uv_sets(mesh):\n    \"\"\"Remove all other UV sets than the current UV set.\n\n    Keep only current UV set and ensure it's the renamed to default 'map1'.\n\n    \"\"\"\n\n    uvSets = cmds.polyUVSet(mesh, query=True, allUVSets=True)\n    current = cmds.polyUVSet(mesh, query=True, currentUVSet=True)[0]\n\n    # Copy over to map1\n    if current != 'map1':\n        cmds.polyUVSet(mesh, uvSet=current, newUVSet='map1', copy=True)\n        cmds.polyUVSet(mesh, currentUVSet=True, uvSet='map1')\n        current = 'map1'\n\n    # Delete all non-current UV sets\n    deleteUVSets = [uvSet for uvSet in uvSets if uvSet != current]\n    uvSet = None\n\n    # Maya Bug (tested in 2015/2016):\n    # In some cases the API's MFnMesh will report less UV sets than\n    # maya.cmds.polyUVSet. This seems to happen when the deletion of UV sets\n    # has not triggered a cleanup of the UVSet array attribute on the mesh\n    # node. It will still have extra entries in the attribute, though it will\n    # not show up in API or UI. Nevertheless it does show up in\n    # maya.cmds.polyUVSet. To ensure we clean up the array we'll force delete\n    # the extra remaining 'indices' that we don't want.\n\n    # TODO: Implement a better fix\n    # The best way to fix would be to get the UVSet indices from api with\n    # MFnMesh (to ensure we keep correct ones) and then only force delete the\n    # other entries in the array attribute on the node. But for now we're\n    # deleting all entries except first one. Note that the first entry could\n    # never be removed (the default 'map1' always exists and is supposed to\n    # be undeletable.)\n    try:\n        for uvSet in deleteUVSets:\n            cmds.polyUVSet(mesh, delete=True, uvSet=uvSet)\n    except RuntimeError as exc:\n        log.warning('Error uvSet: %s - %s', uvSet, exc)\n        indices = cmds.getAttr('{0}.uvSet'.format(mesh),\n                               multiIndices=True)\n        if not indices:\n            log.warning(\"No uv set found indices for: %s\", mesh)\n            return\n\n        # Delete from end to avoid shifting indices\n        # and remove the indices in the attribute\n        indices = reversed(indices[1:])\n        for i in indices:\n            attr = '{0}.uvSet[{1}]'.format(mesh, i)\n            cmds.removeMultiInstance(attr, b=True)\n\n\ndef get_node_parent(node):\n    \"\"\"Return full path name for parent of node\"\"\"\n    parents = cmds.listRelatives(node, parent=True, fullPath=True)\n    return parents[0] if parents else None\n\n\ndef get_id_from_sibling(node, history_only=True):\n    \"\"\"Return first node id in the history chain that matches this node.\n\n    The nodes in history must be of the exact same node type and must be\n    parented under the same parent.\n\n    Optionally, if no matching node is found from the history, all the\n    siblings of the node that are of the same type are checked.\n    Additionally to having the same parent, the sibling must be marked as\n    'intermediate object'.\n\n    Args:\n        node (str): node to retrieve the history from\n        history_only (bool): if True and if nothing found in history,\n            look for an 'intermediate object' in all the node's siblings\n            of same type\n\n    Returns:\n        str or None: The id from the sibling node or None when no id found\n            on any valid nodes in the history or siblings.\n\n    \"\"\"\n\n    node = cmds.ls(node, long=True)[0]\n\n    # Find all similar nodes in history\n    history = cmds.listHistory(node)\n    node_type = cmds.nodeType(node)\n    similar_nodes = cmds.ls(history, exactType=node_type, long=True)\n\n    # Exclude itself\n    similar_nodes = [x for x in similar_nodes if x != node]\n\n    # The node *must be* under the same parent\n    parent = get_node_parent(node)\n    similar_nodes = [i for i in similar_nodes if get_node_parent(i) == parent]\n\n    # Check all of the remaining similar nodes and take the first one\n    # with an id and assume it's the original.\n    for similar_node in similar_nodes:\n        _id = get_id(similar_node)\n        if _id:\n            return _id\n\n    if not history_only:\n        # Get siblings of same type\n        similar_nodes = cmds.listRelatives(parent,\n                                           type=node_type,\n                                           fullPath=True)\n        similar_nodes = cmds.ls(similar_nodes, exactType=node_type, long=True)\n\n        # Exclude itself\n        similar_nodes = [x for x in similar_nodes if x != node]\n\n        # Get all unique ids from siblings in order since\n        # we consistently take the first one found\n        sibling_ids = OrderedDict()\n        for similar_node in similar_nodes:\n            # Check if \"intermediate object\"\n            if not cmds.getAttr(similar_node + \".intermediateObject\"):\n                continue\n\n            _id = get_id(similar_node)\n            if not _id:\n                continue\n\n            if _id in sibling_ids:\n                sibling_ids[_id].append(similar_node)\n            else:\n                sibling_ids[_id] = [similar_node]\n\n        if sibling_ids:\n            first_id, found_nodes = next(iter(sibling_ids.items()))\n\n            # Log a warning if we've found multiple unique ids\n            if len(sibling_ids) > 1:\n                log.warning((\"Found more than 1 intermediate shape with\"\n                             \" unique id for '{}'. Using id of first\"\n                             \" found: '{}'\".format(node, found_nodes[0])))\n\n            return first_id\n\n\ndef set_scene_fps(fps, update=True):\n    \"\"\"Set FPS from project configuration\n\n    Args:\n        fps (int, float): desired FPS\n        update(bool): toggle update animation, default is True\n\n    Returns:\n        None\n\n    \"\"\"\n\n    fps_mapping = {\n        '15': 'game',\n        '24': 'film',\n        '25': 'pal',\n        '30': 'ntsc',\n        '48': 'show',\n        '50': 'palf',\n        '60': 'ntscf',\n        '23.976023976023978': '23.976fps',\n        '29.97002997002997': '29.97fps',\n        '47.952047952047955': '47.952fps',\n        '59.94005994005994': '59.94fps',\n        '44100': '44100fps',\n        '48000': '48000fps'\n    }\n\n    unit = fps_mapping.get(str(convert_to_maya_fps(fps)), None)\n    if unit is None:\n        raise ValueError(\"Unsupported FPS value: `%s`\" % fps)\n\n    # Get time slider current state\n    start_frame = cmds.playbackOptions(query=True, minTime=True)\n    end_frame = cmds.playbackOptions(query=True, maxTime=True)\n\n    # Get animation data\n    animation_start = cmds.playbackOptions(query=True, animationStartTime=True)\n    animation_end = cmds.playbackOptions(query=True, animationEndTime=True)\n\n    current_frame = cmds.currentTime(query=True)\n\n    log.info(\"Setting scene FPS to: '{}'\".format(unit))\n    cmds.currentUnit(time=unit, updateAnimation=update)\n\n    # Set time slider data back to previous state\n    cmds.playbackOptions(edit=True, minTime=start_frame)\n    cmds.playbackOptions(edit=True, maxTime=end_frame)\n\n    # Set animation data\n    cmds.playbackOptions(edit=True, animationStartTime=animation_start)\n    cmds.playbackOptions(edit=True, animationEndTime=animation_end)\n\n    cmds.currentTime(current_frame, edit=True, update=True)\n\n    # Force file stated to 'modified'\n    cmds.file(modified=True)\n\n\ndef set_scene_resolution(width, height, pixelAspect):\n    \"\"\"Set the render resolution\n\n    Args:\n        width(int): value of the width\n        height(int): value of the height\n\n    Returns:\n        None\n\n    \"\"\"\n\n    control_node = \"defaultResolution\"\n    current_renderer = cmds.getAttr(\"defaultRenderGlobals.currentRenderer\")\n    aspect_ratio_attr = \"deviceAspectRatio\"\n\n    # Give VRay a helping hand as it is slightly different from the rest\n    if current_renderer == \"vray\":\n        aspect_ratio_attr = \"aspectRatio\"\n        vray_node = \"vraySettings\"\n        if cmds.objExists(vray_node):\n            control_node = vray_node\n        else:\n            log.error(\"Can't set VRay resolution because there is no node \"\n                      \"named: `%s`\" % vray_node)\n\n    log.info(\"Setting scene resolution to: %s x %s\" % (width, height))\n    cmds.setAttr(\"%s.width\" % control_node, width)\n    cmds.setAttr(\"%s.height\" % control_node, height)\n\n    deviceAspectRatio = ((float(width) / float(height)) * float(pixelAspect))\n    cmds.setAttr(\n        \"{}.{}\".format(control_node, aspect_ratio_attr), deviceAspectRatio)\n    cmds.setAttr(\"%s.pixelAspect\" % control_node, pixelAspect)\n\n\ndef get_fps_for_current_context():\n    \"\"\"Get fps that should be set for current context.\n\n    Todos:\n        - Skip project value.\n        - Merge logic with 'get_frame_range' and 'reset_scene_resolution' ->\n            all the values in the functions can be collected at one place as\n            they have same requirements.\n\n    Returns:\n        Union[int, float]: FPS value.\n    \"\"\"\n\n    project_name = get_current_project_name()\n    asset_name = get_current_asset_name()\n    asset_doc = get_asset_by_name(\n        project_name, asset_name, fields=[\"data.fps\"]\n    ) or {}\n    fps = asset_doc.get(\"data\", {}).get(\"fps\")\n    if not fps:\n        project_doc = get_project(project_name, fields=[\"data.fps\"]) or {}\n        fps = project_doc.get(\"data\", {}).get(\"fps\")\n\n        if not fps:\n            fps = 25\n\n    return convert_to_maya_fps(fps)\n\n\ndef get_frame_range(include_animation_range=False):\n    \"\"\"Get the current assets frame range and handles.\n\n    Args:\n        include_animation_range (bool, optional): Whether to include\n            `animationStart` and `animationEnd` keys to define the outer\n            range of the timeline. It is excluded by default.\n\n    Returns:\n        dict: Asset's expected frame range values.\n\n    \"\"\"\n\n    # Set frame start/end\n    project_name = get_current_project_name()\n    asset_name = get_current_asset_name()\n    asset = get_asset_by_name(project_name, asset_name)\n\n    frame_start = asset[\"data\"].get(\"frameStart\")\n    frame_end = asset[\"data\"].get(\"frameEnd\")\n\n    if frame_start is None or frame_end is None:\n        cmds.warning(\"No edit information found for %s\" % asset_name)\n        return\n\n    handle_start = asset[\"data\"].get(\"handleStart\") or 0\n    handle_end = asset[\"data\"].get(\"handleEnd\") or 0\n\n    frame_range = {\n        \"frameStart\": frame_start,\n        \"frameEnd\": frame_end,\n        \"handleStart\": handle_start,\n        \"handleEnd\": handle_end\n    }\n    if include_animation_range:\n        # The animation range values are only included to define whether\n        # the Maya time slider should include the handles or not.\n        # Some usages of this function use the full dictionary to define\n        # instance attributes for which we want to exclude the animation\n        # keys. That is why these are excluded by default.\n        task_name = get_current_task_name()\n        settings = get_project_settings(project_name)\n        include_handles_settings = settings[\"maya\"][\"include_handles\"]\n        current_task = asset.get(\"data\").get(\"tasks\").get(task_name)\n\n        animation_start = frame_start\n        animation_end = frame_end\n\n        include_handles = include_handles_settings[\"include_handles_default\"]\n        for item in include_handles_settings[\"per_task_type\"]:\n            if current_task[\"type\"] in item[\"task_type\"]:\n                include_handles = item[\"include_handles\"]\n                break\n        if include_handles:\n            animation_start -= int(handle_start)\n            animation_end += int(handle_end)\n\n        frame_range[\"animationStart\"] = animation_start\n        frame_range[\"animationEnd\"] = animation_end\n\n    return frame_range\n\n\ndef reset_frame_range(playback=True, render=True, fps=True):\n    \"\"\"Set frame range to current asset\n\n    Args:\n        playback (bool, Optional): Whether to set the maya timeline playback\n            frame range. Defaults to True.\n        render (bool, Optional): Whether to set the maya render frame range.\n            Defaults to True.\n        fps (bool, Optional): Whether to set scene FPS. Defaults to True.\n    \"\"\"\n    if fps:\n        set_scene_fps(get_fps_for_current_context())\n\n    frame_range = get_frame_range(include_animation_range=True)\n    if not frame_range:\n        # No frame range data found for asset\n        return\n\n    frame_start = frame_range[\"frameStart\"]\n    frame_end = frame_range[\"frameEnd\"]\n    animation_start = frame_range[\"animationStart\"]\n    animation_end = frame_range[\"animationEnd\"]\n\n    if playback:\n        cmds.playbackOptions(\n            minTime=frame_start,\n            maxTime=frame_end,\n            animationStartTime=animation_start,\n            animationEndTime=animation_end\n        )\n        cmds.currentTime(frame_start)\n\n    if render:\n        cmds.setAttr(\"defaultRenderGlobals.startFrame\", animation_start)\n        cmds.setAttr(\"defaultRenderGlobals.endFrame\", animation_end)\n\n\ndef reset_scene_resolution():\n    \"\"\"Apply the scene resolution  from the project definition\n\n    scene resolution can be overwritten by an asset if the asset.data contains\n    any information regarding scene resolution .\n\n    Returns:\n        None\n    \"\"\"\n\n    project_name = get_current_project_name()\n    project_doc = get_project(project_name)\n    project_data = project_doc[\"data\"]\n    asset_data = get_current_project_asset()[\"data\"]\n\n    # Set project resolution\n    width_key = \"resolutionWidth\"\n    height_key = \"resolutionHeight\"\n    pixelAspect_key = \"pixelAspect\"\n\n    width = asset_data.get(width_key, project_data.get(width_key, 1920))\n    height = asset_data.get(height_key, project_data.get(height_key, 1080))\n    pixelAspect = asset_data.get(pixelAspect_key,\n                                 project_data.get(pixelAspect_key, 1))\n\n    set_scene_resolution(width, height, pixelAspect)\n\n\ndef set_context_settings():\n    \"\"\"Apply the project settings from the project definition\n\n    Settings can be overwritten by an asset if the asset.data contains\n    any information regarding those settings.\n\n    Examples of settings:\n        fps\n        resolution\n        renderer\n\n    Returns:\n        None\n    \"\"\"\n\n\n    # Set project fps\n    set_scene_fps(get_fps_for_current_context())\n\n    reset_scene_resolution()\n\n    # Set frame range.\n    reset_frame_range()\n\n    # Set colorspace\n    set_colorspace()\n\n\n# Valid FPS\ndef validate_fps():\n    \"\"\"Validate current scene FPS and show pop-up when it is incorrect\n\n    Returns:\n        bool\n\n    \"\"\"\n\n    expected_fps = get_fps_for_current_context()\n    current_fps = mel.eval('currentTimeUnitToFPS()')\n\n    fps_match = current_fps == expected_fps\n    if not fps_match and not IS_HEADLESS:\n        from openpype.widgets import popup\n\n        parent = get_main_window()\n\n        dialog = popup.PopupUpdateKeys(parent=parent)\n        dialog.setModal(True)\n        dialog.setWindowTitle(\"Maya scene does not match project FPS\")\n        dialog.setMessage(\n            \"Scene {} FPS does not match project {} FPS\".format(\n                current_fps, expected_fps\n            )\n        )\n        dialog.setButtonText(\"Fix\")\n\n        # Set new text for button (add optional argument for the popup?)\n        toggle = dialog.widgets[\"toggle\"]\n        update = toggle.isChecked()\n        dialog.on_clicked_state.connect(\n            lambda: set_scene_fps(expected_fps, update)\n        )\n\n        dialog.show()\n\n        return False\n\n    return fps_match\n\n\ndef bake(nodes,\n         frame_range=None,\n         step=1.0,\n         simulation=True,\n         preserve_outside_keys=False,\n         disable_implicit_control=True,\n         shape=True):\n    \"\"\"Bake the given nodes over the time range.\n\n    This will bake all attributes of the node, including custom attributes.\n\n    Args:\n        nodes (list): Names of transform nodes, eg. camera, light.\n        frame_range (list): frame range with start and end frame.\n            or if None then takes timeSliderRange\n        simulation (bool): Whether to perform a full simulation of the\n            attributes over time.\n        preserve_outside_keys (bool): Keep keys that are outside of the baked\n            range.\n        disable_implicit_control (bool): When True will disable any\n            constraints to the object.\n        shape (bool): When True also bake attributes on the children shapes.\n        step (float): The step size to sample by.\n\n    Returns:\n        None\n\n    \"\"\"\n\n    # Parse inputs\n    if not nodes:\n        return\n\n    assert isinstance(nodes, (list, tuple)), \"Nodes must be a list or tuple\"\n\n    # If frame range is None fall back to time slider playback time range\n    if frame_range is None:\n        frame_range = [cmds.playbackOptions(query=True, minTime=True),\n                       cmds.playbackOptions(query=True, maxTime=True)]\n\n    # If frame range is single frame bake one frame more,\n    # otherwise maya.cmds.bakeResults gets confused\n    if frame_range[1] == frame_range[0]:\n        frame_range[1] += 1\n\n    # Bake it\n    with keytangent_default(in_tangent_type='auto',\n                            out_tangent_type='auto'):\n        cmds.bakeResults(nodes,\n                         simulation=simulation,\n                         preserveOutsideKeys=preserve_outside_keys,\n                         disableImplicitControl=disable_implicit_control,\n                         shape=shape,\n                         sampleBy=step,\n                         time=(frame_range[0], frame_range[1]))\n\n\ndef bake_to_world_space(nodes,\n                        frame_range=None,\n                        simulation=True,\n                        preserve_outside_keys=False,\n                        disable_implicit_control=True,\n                        shape=True,\n                        step=1.0):\n    \"\"\"Bake the nodes to world space transformation (incl. other attributes)\n\n    Bakes the transforms to world space (while maintaining all its animated\n    attributes and settings) by duplicating the node. Then parents it to world\n    and constrains to the original.\n\n    Other attributes are also baked by connecting all attributes directly.\n    Baking is then done using Maya's bakeResults command.\n\n    See `bake` for the argument documentation.\n\n    Returns:\n         list: The newly created and baked node names.\n\n    \"\"\"\n    @contextlib.contextmanager\n    def _unlock_attr(attr):\n        \"\"\"Unlock attribute during context if it is locked\"\"\"\n        if not cmds.getAttr(attr, lock=True):\n            # If not locked, do nothing\n            yield\n            return\n        try:\n            cmds.setAttr(attr, lock=False)\n            yield\n        finally:\n            cmds.setAttr(attr, lock=True)\n\n    def _get_attrs(node):\n        \"\"\"Workaround for buggy shape attribute listing with listAttr\n\n        This will only return keyable settable attributes that have an\n        incoming connections (those that have a reason to be baked).\n\n        Technically this *may* fail to return attributes driven by complex\n        expressions for which maya makes no connections, e.g. doing actual\n        `setAttr` calls in expressions.\n\n        Arguments:\n            node (str): The node to list attributes for.\n\n        Returns:\n            list: Keyable attributes with incoming connections.\n                The attribute may be locked.\n\n        \"\"\"\n        attrs = cmds.listAttr(node,\n                              write=True,\n                              scalar=True,\n                              settable=True,\n                              connectable=True,\n                              keyable=True,\n                              shortNames=True) or []\n        valid_attrs = []\n        for attr in attrs:\n            node_attr = '{0}.{1}'.format(node, attr)\n\n            # Sometimes Maya returns 'non-existent' attributes for shapes\n            # so we filter those out\n            if not cmds.attributeQuery(attr, node=node, exists=True):\n                continue\n\n            # We only need those that have a connection, just to be safe\n            # that it's actually keyable/connectable anyway.\n            if cmds.connectionInfo(node_attr,\n                                   isDestination=True):\n                valid_attrs.append(attr)\n\n        return valid_attrs\n\n    transform_attrs = {\"t\", \"r\", \"s\",\n                       \"tx\", \"ty\", \"tz\",\n                       \"rx\", \"ry\", \"rz\",\n                       \"sx\", \"sy\", \"sz\"}\n\n    world_space_nodes = []\n    with ExitStack() as stack:\n        delete_bin = stack.enter_context(delete_after())\n        # Create the duplicate nodes that are in world-space connected to\n        # the originals\n        for node in nodes:\n\n            # Duplicate the node\n            short_name = node.rsplit(\"|\", 1)[-1]\n            new_name = \"{0}_baked\".format(short_name)\n            new_node = cmds.duplicate(node,\n                                      name=new_name,\n                                      renameChildren=True)[0]  # noqa\n\n            # Parent new node to world\n            if cmds.listRelatives(new_node, parent=True):\n                new_node = cmds.parent(new_node, world=True)[0]\n\n            # Temporarily unlock and passthrough connect all attributes\n            # so we can bake them over time\n            # Skip transform attributes because we will constrain them later\n            attrs = set(_get_attrs(node)) - transform_attrs\n            for attr in attrs:\n                orig_node_attr = \"{}.{}\".format(node, attr)\n                new_node_attr = \"{}.{}\".format(new_node, attr)\n\n                # unlock during context to avoid connection errors\n                stack.enter_context(_unlock_attr(new_node_attr))\n                cmds.connectAttr(orig_node_attr,\n                                 new_node_attr,\n                                 force=True)\n\n            # If shapes are also baked then also temporarily unlock and\n            # passthrough connect all shape attributes for baking\n            if shape:\n                children_shapes = cmds.listRelatives(new_node,\n                                                     children=True,\n                                                     fullPath=True,\n                                                     shapes=True)\n                if children_shapes:\n                    orig_children_shapes = cmds.listRelatives(node,\n                                                              children=True,\n                                                              fullPath=True,\n                                                              shapes=True)\n                    for orig_shape, new_shape in zip(orig_children_shapes,\n                                                     children_shapes):\n                        attrs = _get_attrs(orig_shape)\n                        for attr in attrs:\n                            orig_node_attr = \"{}.{}\".format(orig_shape, attr)\n                            new_node_attr = \"{}.{}\".format(new_shape, attr)\n\n                            # unlock during context to avoid connection errors\n                            stack.enter_context(_unlock_attr(new_node_attr))\n                            cmds.connectAttr(orig_node_attr,\n                                             new_node_attr,\n                                             force=True)\n\n            # Constraint transforms\n            for attr in transform_attrs:\n                transform_attr = \"{}.{}\".format(new_node, attr)\n                stack.enter_context(_unlock_attr(transform_attr))\n            delete_bin.extend(cmds.parentConstraint(node, new_node, mo=False))\n            delete_bin.extend(cmds.scaleConstraint(node, new_node, mo=False))\n\n            world_space_nodes.append(new_node)\n\n        bake(world_space_nodes,\n             frame_range=frame_range,\n             step=step,\n             simulation=simulation,\n             preserve_outside_keys=preserve_outside_keys,\n             disable_implicit_control=disable_implicit_control,\n             shape=shape)\n\n    return world_space_nodes\n\n\ndef load_capture_preset(data):\n    \"\"\"Convert OpenPype Extract Playblast settings to `capture` arguments\n\n    Input data is the settings from:\n        `project_settings/maya/publish/ExtractPlayblast/capture_preset`\n\n    Args:\n        data (dict): Capture preset settings from OpenPype settings\n\n    Returns:\n        dict: `capture.capture` compatible keyword arguments\n\n    \"\"\"\n\n    options = dict()\n    viewport_options = dict()\n    viewport2_options = dict()\n    camera_options = dict()\n\n    # Straight key-value match from settings to capture arguments\n    options.update(data[\"Codec\"])\n    options.update(data[\"Generic\"])\n    options.update(data[\"Resolution\"])\n\n    camera_options.update(data['Camera Options'])\n    viewport_options.update(data[\"Renderer\"])\n\n    # DISPLAY OPTIONS\n    disp_options = {}\n    for key, value in data['Display Options'].items():\n        if key.startswith('background'):\n            # Convert background, backgroundTop, backgroundBottom colors\n            if len(value) == 4:\n                # Ignore alpha + convert RGB to float\n                value = [\n                    float(value[0]) / 255,\n                    float(value[1]) / 255,\n                    float(value[2]) / 255\n                ]\n            disp_options[key] = value\n        elif key == \"displayGradient\":\n            disp_options[key] = value\n\n    options['display_options'] = disp_options\n\n    # Viewport Options has a mixture of Viewport2 Options and Viewport Options\n    # to pass along to capture. So we'll need to differentiate between the two\n    VIEWPORT2_OPTIONS = {\n        \"textureMaxResolution\",\n        \"renderDepthOfField\",\n        \"ssaoEnable\",\n        \"ssaoSamples\",\n        \"ssaoAmount\",\n        \"ssaoRadius\",\n        \"ssaoFilterRadius\",\n        \"hwFogStart\",\n        \"hwFogEnd\",\n        \"hwFogAlpha\",\n        \"hwFogFalloff\",\n        \"hwFogColorR\",\n        \"hwFogColorG\",\n        \"hwFogColorB\",\n        \"hwFogDensity\",\n        \"motionBlurEnable\",\n        \"motionBlurSampleCount\",\n        \"motionBlurShutterOpenFraction\",\n        \"lineAAEnable\"\n    }\n    for key, value in data['Viewport Options'].items():\n\n        # There are some keys we want to ignore\n        if key in {\"override_viewport_options\", \"high_quality\"}:\n            continue\n\n        # First handle special cases where we do value conversion to\n        # separate option values\n        if key == 'textureMaxResolution':\n            viewport2_options['textureMaxResolution'] = value\n            if value > 0:\n                viewport2_options['enableTextureMaxRes'] = True\n                viewport2_options['textureMaxResMode'] = 1\n            else:\n                viewport2_options['enableTextureMaxRes'] = False\n                viewport2_options['textureMaxResMode'] = 0\n\n        elif key == 'multiSample':\n            viewport2_options['multiSampleEnable'] = value > 0\n            viewport2_options['multiSampleCount'] = value\n\n        elif key == 'alphaCut':\n            viewport2_options['transparencyAlgorithm'] = 5\n            viewport2_options['transparencyQuality'] = 1\n\n        elif key == 'hwFogFalloff':\n            # Settings enum value string to integer\n            viewport2_options['hwFogFalloff'] = int(value)\n\n        # Then handle Viewport 2.0 Options\n        elif key in VIEWPORT2_OPTIONS:\n            viewport2_options[key] = value\n\n        # Then assume remainder is Viewport Options\n        else:\n            viewport_options[key] = value\n\n    options['viewport_options'] = viewport_options\n    options['viewport2_options'] = viewport2_options\n    options['camera_options'] = camera_options\n\n    # use active sound track\n    scene = capture.parse_active_scene()\n    options['sound'] = scene['sound']\n\n    return options\n\n\ndef get_attr_in_layer(attr, layer):\n    \"\"\"Return attribute value in specified renderlayer.\n\n    Same as cmds.getAttr but this gets the attribute's value in a\n    given render layer without having to switch to it.\n\n    Warning for parent attribute overrides:\n        Attributes that have render layer overrides to their parent attribute\n        are not captured correctly since they do not have a direct connection.\n        For example, an override to sphere.rotate when querying sphere.rotateX\n        will not return correctly!\n\n    Note: This is much faster for Maya's renderLayer system, yet the code\n        does no optimized query for render setup.\n\n    Args:\n        attr (str): attribute name, ex. \"node.attribute\"\n        layer (str): layer name\n\n    Returns:\n        The return value from `maya.cmds.getAttr`\n\n    \"\"\"\n\n    try:\n        if cmds.mayaHasRenderSetup():\n            from . import lib_rendersetup\n            return lib_rendersetup.get_attr_in_layer(attr, layer)\n    except AttributeError:\n        pass\n\n    # Ignore complex query if we're in the layer anyway\n    current_layer = cmds.editRenderLayerGlobals(query=True,\n                                                currentRenderLayer=True)\n    if layer == current_layer:\n        return cmds.getAttr(attr)\n\n    connections = cmds.listConnections(attr,\n                                       plugs=True,\n                                       source=False,\n                                       destination=True,\n                                       type=\"renderLayer\") or []\n    connections = filter(lambda x: x.endswith(\".plug\"), connections)\n    if not connections:\n        return cmds.getAttr(attr)\n\n    # Some value types perform a conversion when assigning\n    # TODO: See if there's a maya method to allow this conversion\n    # instead of computing it ourselves.\n    attr_type = cmds.getAttr(attr, type=True)\n    conversion = None\n    if attr_type == \"time\":\n        conversion = mel.eval('currentTimeUnitToFPS()')  # returns float\n    elif attr_type == \"doubleAngle\":\n        # Radians to Degrees: 180 / pi\n        # TODO: This will likely only be correct when Maya units are set\n        #       to degrees\n        conversion = 57.2957795131\n    elif attr_type == \"doubleLinear\":\n        raise NotImplementedError(\"doubleLinear conversion not implemented.\")\n\n    for connection in connections:\n        if connection.startswith(layer + \".\"):\n            attr_split = connection.split(\".\")\n            if attr_split[0] == layer:\n                attr = \".\".join(attr_split[0:-1])\n                value = cmds.getAttr(\"%s.value\" % attr)\n                if conversion:\n                    value *= conversion\n                return value\n\n    else:\n        # When connections are present, but none\n        # to the specific renderlayer than the layer\n        # should have the \"defaultRenderLayer\"'s value\n        layer = \"defaultRenderLayer\"\n        for connection in connections:\n            if connection.startswith(layer):\n                attr_split = connection.split(\".\")\n                if attr_split[0] == \"defaultRenderLayer\":\n                    attr = \".\".join(attr_split[0:-1])\n                    value = cmds.getAttr(\"%s.value\" % attr)\n                    if conversion:\n                        value *= conversion\n                    return value\n\n    return cmds.getAttr(attr)\n\n\ndef fix_incompatible_containers():\n    \"\"\"Backwards compatibility: old containers to use new ReferenceLoader\"\"\"\n    old_loaders = {\n        \"MayaAsciiLoader\",\n        \"AbcLoader\",\n        \"ModelLoader\",\n        \"CameraLoader\",\n        \"RigLoader\",\n        \"FBXLoader\"\n    }\n    host = registered_host()\n    for container in host.ls():\n        loader = container['loader']\n        if loader in old_loaders:\n            log.info(\n                \"Converting legacy container loader {} to \"\n                \"ReferenceLoader: {}\".format(loader, container[\"objectName\"])\n            )\n            cmds.setAttr(container[\"objectName\"] + \".loader\",\n                         \"ReferenceLoader\", type=\"string\")\n\n\ndef update_content_on_context_change():\n    \"\"\"\n    This will update scene content to match new asset on context change\n    \"\"\"\n    scene_sets = cmds.listSets(allSets=True)\n    asset_doc = get_current_project_asset()\n    new_asset = asset_doc[\"name\"]\n    new_data = asset_doc[\"data\"]\n    for s in scene_sets:\n        try:\n            if cmds.getAttr(\"{}.id\".format(s)) == \"pyblish.avalon.instance\":\n                attr = cmds.listAttr(s)\n                print(s)\n                if \"asset\" in attr:\n                    print(\"  - setting asset to: [ {} ]\".format(new_asset))\n                    cmds.setAttr(\"{}.asset\".format(s),\n                                 new_asset, type=\"string\")\n                if \"frameStart\" in attr:\n                    cmds.setAttr(\"{}.frameStart\".format(s),\n                                 new_data[\"frameStart\"])\n                if \"frameEnd\" in attr:\n                    cmds.setAttr(\"{}.frameEnd\".format(s),\n                                 new_data[\"frameEnd\"],)\n        except ValueError:\n            pass\n\n\ndef show_message(title, msg):\n    from qtpy import QtWidgets\n    from openpype.widgets import message_window\n\n    # Find maya main window\n    top_level_widgets = {w.objectName(): w for w in\n                         QtWidgets.QApplication.topLevelWidgets()}\n\n    parent = top_level_widgets.get(\"MayaWindow\", None)\n    if parent is None:\n        pass\n    else:\n        message_window.message(title=title, message=msg, parent=parent)\n\n\ndef iter_shader_edits(relationships, shader_nodes, nodes_by_id, label=None):\n    \"\"\"Yield edits as a set of actions.\"\"\"\n\n    attributes = relationships.get(\"attributes\", [])\n    shader_data = relationships.get(\"relationships\", {})\n\n    shading_engines = cmds.ls(shader_nodes, type=\"objectSet\", long=True)\n    assert shading_engines, \"Error in retrieving objectSets from reference\"\n\n    # region compute lookup\n    shading_engines_by_id = defaultdict(list)\n    for shad in shading_engines:\n        shading_engines_by_id[get_id(shad)].append(shad)\n    # endregion\n\n    # region assign shading engines and other sets\n    for data in shader_data.values():\n        # collect all unique IDs of the set members\n        shader_uuid = data[\"uuid\"]\n        member_uuids = [\n            (member[\"uuid\"], member.get(\"components\"))\n            for member in data[\"members\"]]\n\n        filtered_nodes = list()\n        for _uuid, components in member_uuids:\n            nodes = nodes_by_id.get(_uuid, None)\n            if nodes is None:\n                continue\n\n            if components:\n                # Assign to the components\n                nodes = [\".\".join([node, components]) for node in nodes]\n\n            filtered_nodes.extend(nodes)\n\n        id_shading_engines = shading_engines_by_id[shader_uuid]\n        if not id_shading_engines:\n            log.error(\"{} - No shader found with cbId \"\n                      \"'{}'\".format(label, shader_uuid))\n            continue\n        elif len(id_shading_engines) > 1:\n            log.error(\"{} - Skipping shader assignment. \"\n                      \"More than one shader found with cbId \"\n                      \"'{}'. (found: {})\".format(label, shader_uuid,\n                                                 id_shading_engines))\n            continue\n\n        if not filtered_nodes:\n            log.warning(\"{} - No nodes found for shading engine \"\n                        \"'{}'\".format(label, id_shading_engines[0]))\n            continue\n\n        yield {\"action\": \"assign\",\n               \"uuid\": data[\"uuid\"],\n               \"nodes\": filtered_nodes,\n               \"shader\": id_shading_engines[0]}\n\n    for data in attributes:\n        nodes = nodes_by_id.get(data[\"uuid\"], [])\n        attr_value = data[\"attributes\"]\n        yield {\"action\": \"setattr\",\n               \"uuid\": data[\"uuid\"],\n               \"nodes\": nodes,\n               \"attributes\": attr_value}\n\n\ndef set_colorspace():\n    \"\"\"Set Colorspace from project configuration\"\"\"\n\n    project_name = get_current_project_name()\n    imageio = get_project_settings(project_name)[\"maya\"][\"imageio\"]\n\n    # ocio compatibility variables\n    ocio_v2_maya_version = 2022\n    maya_version = int(cmds.about(version=True))\n    ocio_v2_support = use_ocio_v2 = maya_version >= ocio_v2_maya_version\n    is_ocio_set = bool(os.environ.get(\"OCIO\"))\n\n    use_workfile_settings = imageio.get(\"workfile\", {}).get(\"enabled\")\n    if use_workfile_settings:\n        root_dict = imageio[\"workfile\"]\n    else:\n        # TODO: deprecated code from 3.15.5 - remove\n        # Maya 2022+ introduces new OCIO v2 color management settings that\n        # can override the old color management preferences. OpenPype has\n        # separate settings for both so we fall back when necessary.\n        use_ocio_v2 = imageio[\"colorManagementPreference_v2\"][\"enabled\"]\n        if use_ocio_v2 and not ocio_v2_support:\n            # Fallback to legacy behavior with a warning\n            log.warning(\n                \"Color Management Preference v2 is enabled but not \"\n                \"supported by current Maya version: {} (< {}). Falling \"\n                \"back to legacy settings.\".format(\n                    maya_version, ocio_v2_maya_version)\n            )\n\n        if use_ocio_v2:\n            root_dict = imageio[\"colorManagementPreference_v2\"]\n        else:\n            root_dict = imageio[\"colorManagementPreference\"]\n\n        if not isinstance(root_dict, dict):\n            msg = \"set_colorspace(): argument should be dictionary\"\n            log.error(msg)\n            return\n\n    # backward compatibility\n    # TODO: deprecated code from 3.15.5 - remove with deprecated code above\n    view_name = root_dict.get(\"viewTransform\")\n    if view_name is None:\n        view_name = root_dict.get(\"viewName\")\n\n    log.debug(\">> root_dict: {}\".format(pformat(root_dict)))\n    if not root_dict:\n        return\n\n    # set color spaces for rendering space and view transforms\n    def _colormanage(**kwargs):\n        \"\"\"Wrapper around `cmds.colorManagementPrefs`.\n\n        This logs errors instead of raising an error so color management\n        settings get applied as much as possible.\n\n        \"\"\"\n        assert len(kwargs) == 1, \"Must receive one keyword argument\"\n        try:\n            cmds.colorManagementPrefs(edit=True, **kwargs)\n            log.debug(\"Setting Color Management Preference: {}\".format(kwargs))\n        except RuntimeError as exc:\n            log.error(exc)\n\n    # enable color management\n    cmds.colorManagementPrefs(edit=True, cmEnabled=True)\n    cmds.colorManagementPrefs(edit=True, ocioRulesEnabled=True)\n\n    if use_ocio_v2:\n        log.info(\"Using Maya OCIO v2\")\n        if not is_ocio_set:\n            # Set the Maya 2022+ default OCIO v2 config file path\n            log.info(\"Setting default Maya OCIO v2 config\")\n            # Note: Setting \"\" as value also sets this default however\n            # introduces a bug where launching a file on startup will prompt\n            # to save the empty scene before it, so we set using the path.\n            # This value has been the same for 2022, 2023 and 2024\n            path = \"<MAYA_RESOURCES>/OCIO-configs/Maya2022-default/config.ocio\"\n            cmds.colorManagementPrefs(edit=True, configFilePath=path)\n\n        # set rendering space and view transform\n        _colormanage(renderingSpaceName=root_dict[\"renderSpace\"])\n        _colormanage(viewName=view_name)\n        _colormanage(displayName=root_dict[\"displayName\"])\n    else:\n        log.info(\"Using Maya OCIO v1 (legacy)\")\n        if not is_ocio_set:\n            # Set the Maya default config file path\n            log.info(\"Setting default Maya OCIO v1 legacy config\")\n            cmds.colorManagementPrefs(edit=True, configFilePath=\"legacy\")\n\n        # set rendering space and view transform\n        _colormanage(renderingSpaceName=root_dict[\"renderSpace\"])\n        _colormanage(viewTransformName=view_name)\n\n\n@contextlib.contextmanager\ndef parent_nodes(nodes, parent=None):\n    # type: (list, str) -> list\n    \"\"\"Context manager to un-parent provided nodes and return them back.\"\"\"\n\n    def _as_mdagpath(node):\n        \"\"\"Return MDagPath for node path.\"\"\"\n        if not node:\n            return\n        sel = OpenMaya.MSelectionList()\n        sel.add(node)\n        return sel.getDagPath(0)\n\n    # We can only parent dag nodes so we ensure input contains only dag nodes\n    nodes = cmds.ls(nodes, type=\"dagNode\", long=True)\n    if not nodes:\n        # opt-out early\n        yield\n        return\n\n    parent_node_path = None\n    delete_parent = False\n    if parent:\n        if not cmds.objExists(parent):\n            parent_node = cmds.createNode(\"transform\",\n                                          name=parent,\n                                          skipSelect=False)\n            delete_parent = True\n        else:\n            parent_node = parent\n        parent_node_path = cmds.ls(parent_node, long=True)[0]\n\n    # Store original parents\n    node_parents = []\n    for node in nodes:\n        node_parent = get_node_parent(node)\n        node_parents.append((_as_mdagpath(node), _as_mdagpath(node_parent)))\n\n    try:\n        for node, node_parent in node_parents:\n            node_parent_path = node_parent.fullPathName() if node_parent else None  # noqa\n            if node_parent_path == parent_node_path:\n                # Already a child\n                continue\n\n            if parent_node_path:\n                cmds.parent(node.fullPathName(), parent_node_path)\n            else:\n                cmds.parent(node.fullPathName(), world=True)\n\n        yield\n    finally:\n        # Reparent to original parents\n        for node, original_parent in node_parents:\n            node_path = node.fullPathName()\n            if not node_path:\n                # Node must have been deleted\n                continue\n\n            node_parent_path = get_node_parent(node_path)\n\n            original_parent_path = None\n            if original_parent:\n                original_parent_path = original_parent.fullPathName()\n                if not original_parent_path:\n                    # Original parent node must have been deleted\n                    continue\n\n            if node_parent_path != original_parent_path:\n                if not original_parent_path:\n                    cmds.parent(node_path, world=True)\n                else:\n                    cmds.parent(node_path, original_parent_path)\n\n        if delete_parent:\n            cmds.delete(parent_node_path)\n\n\n@contextlib.contextmanager\ndef maintained_time():\n    ct = cmds.currentTime(query=True)\n    try:\n        yield\n    finally:\n        cmds.currentTime(ct, edit=True)\n\n\ndef iter_visible_nodes_in_range(nodes, start, end):\n    \"\"\"Yield nodes that are visible in start-end frame range.\n\n    - Ignores intermediateObjects completely.\n    - Considers animated visibility attributes + upstream visibilities.\n\n    This is optimized for large scenes where some nodes in the parent\n    hierarchy might have some input connections to the visibilities,\n    e.g. key, driven keys, connections to other attributes, etc.\n\n    This only does a single time step to `start` if current frame is\n    not inside frame range since the assumption is made that changing\n    a frame isn't so slow that it beats querying all visibility\n    plugs through MDGContext on another frame.\n\n    Args:\n        nodes (list): List of node names to consider.\n        start (int, float): Start frame.\n        end (int, float): End frame.\n\n    Returns:\n        list: List of node names. These will be long full path names so\n            might have a longer name than the input nodes.\n\n    \"\"\"\n    # States we consider per node\n    VISIBLE = 1  # always visible\n    INVISIBLE = 0  # always invisible\n    ANIMATED = -1  # animated visibility\n\n    # Ensure integers\n    start = int(start)\n    end = int(end)\n\n    # Consider only non-intermediate dag nodes and use the \"long\" names.\n    nodes = cmds.ls(nodes, long=True, noIntermediate=True, type=\"dagNode\")\n    if not nodes:\n        return\n\n    with maintained_time():\n        # Go to first frame of the range if the current time is outside\n        # the queried range so can directly query all visible nodes on\n        # that frame.\n        current_time = cmds.currentTime(query=True)\n        if not (start <= current_time <= end):\n            cmds.currentTime(start)\n\n        visible = cmds.ls(nodes, long=True, visible=True)\n        for node in visible:\n            yield node\n        if len(visible) == len(nodes) or start == end:\n            # All are visible on frame one, so they are at least visible once\n            # inside the frame range.\n            return\n\n    # For the invisible ones check whether its visibility and/or\n    # any of its parents visibility attributes are animated. If so, it might\n    # get visible on other frames in the range.\n    def memodict(f):\n        \"\"\"Memoization decorator for a function taking a single argument.\n\n        See: http://code.activestate.com/recipes/\n             578231-probably-the-fastest-memoization-decorator-in-the-/\n        \"\"\"\n\n        class memodict(dict):\n            def __missing__(self, key):\n                ret = self[key] = f(key)\n                return ret\n\n        return memodict().__getitem__\n\n    @memodict\n    def get_state(node):\n        plug = node + \".visibility\"\n        connections = cmds.listConnections(plug,\n                                           source=True,\n                                           destination=False)\n        if connections:\n            return ANIMATED\n        else:\n            return VISIBLE if cmds.getAttr(plug) else INVISIBLE\n\n    visible = set(visible)\n    invisible = [node for node in nodes if node not in visible]\n    always_invisible = set()\n    # Iterate over the nodes by short to long names to iterate the highest\n    # in hierarchy nodes first. So the collected data can be used from the\n    # cache for parent queries in next iterations.\n    node_dependencies = dict()\n    for node in sorted(invisible, key=len):\n\n        state = get_state(node)\n        if state == INVISIBLE:\n            always_invisible.add(node)\n            continue\n\n        # If not always invisible by itself we should go through and check\n        # the parents to see if any of them are always invisible. For those\n        # that are \"ANIMATED\" we consider that this node is dependent on\n        # that attribute, we store them as dependency.\n        dependencies = set()\n        if state == ANIMATED:\n            dependencies.add(node)\n\n        traversed_parents = list()\n        for parent in iter_parents(node):\n\n            if parent in always_invisible or get_state(parent) == INVISIBLE:\n                # When parent is always invisible then consider this parent,\n                # this node we started from and any of the parents we\n                # have traversed in-between to be *always invisible*\n                always_invisible.add(parent)\n                always_invisible.add(node)\n                always_invisible.update(traversed_parents)\n                break\n\n            # If we have traversed the parent before and its visibility\n            # was dependent on animated visibilities then we can just extend\n            # its dependencies for to those for this node and break further\n            # iteration upwards.\n            parent_dependencies = node_dependencies.get(parent, None)\n            if parent_dependencies is not None:\n                dependencies.update(parent_dependencies)\n                break\n\n            state = get_state(parent)\n            if state == ANIMATED:\n                dependencies.add(parent)\n\n            traversed_parents.append(parent)\n\n        if node not in always_invisible and dependencies:\n            node_dependencies[node] = dependencies\n\n    if not node_dependencies:\n        return\n\n    # Now we only have to check the visibilities for nodes that have animated\n    # visibility dependencies upstream. The fastest way to check these\n    # visibility attributes across different frames is with Python api 2.0\n    # so we do that.\n    @memodict\n    def get_visibility_mplug(node):\n        \"\"\"Return api 2.0 MPlug with cached memoize decorator\"\"\"\n        sel = OpenMaya.MSelectionList()\n        sel.add(node)\n        dag = sel.getDagPath(0)\n        return OpenMaya.MFnDagNode(dag).findPlug(\"visibility\", True)\n\n    @contextlib.contextmanager\n    def dgcontext(mtime):\n        \"\"\"MDGContext context manager\"\"\"\n        context = OpenMaya.MDGContext(mtime)\n        try:\n            previous = context.makeCurrent()\n            yield context\n        finally:\n            previous.makeCurrent()\n\n    # We skip the first frame as we already used that frame to check for\n    # overall visibilities. And end+1 to include the end frame.\n    scene_units = OpenMaya.MTime.uiUnit()\n    for frame in range(start + 1, end + 1):\n        mtime = OpenMaya.MTime(frame, unit=scene_units)\n\n        # Build little cache so we don't query the same MPlug's value\n        # again if it was checked on this frame and also is a dependency\n        # for another node\n        frame_visibilities = {}\n        with dgcontext(mtime) as context:\n            for node, dependencies in list(node_dependencies.items()):\n                for dependency in dependencies:\n                    dependency_visible = frame_visibilities.get(dependency,\n                                                                None)\n                    if dependency_visible is None:\n                        mplug = get_visibility_mplug(dependency)\n                        dependency_visible = mplug.asBool(context)\n                        frame_visibilities[dependency] = dependency_visible\n\n                    if not dependency_visible:\n                        # One dependency is not visible, thus the\n                        # node is not visible.\n                        break\n\n                else:\n                    # All dependencies are visible.\n                    yield node\n                    # Remove node with dependencies for next frame iterations\n                    # because it was visible at least once.\n                    node_dependencies.pop(node)\n\n        # If no more nodes to process break the frame iterations..\n        if not node_dependencies:\n            break\n\n\ndef get_attribute_input(attr):\n    connections = cmds.listConnections(attr, plugs=True, destination=False)\n    return connections[0] if connections else None\n\n\ndef convert_to_maya_fps(fps):\n    \"\"\"Convert any fps to supported Maya framerates.\"\"\"\n    float_framerates = [\n        23.976023976023978,\n        # WTF is 29.97 df vs fps?\n        29.97002997002997,\n        47.952047952047955,\n        59.94005994005994\n    ]\n    # 44100 fps evaluates as 41000.0. Why? Omitting for now.\n    int_framerates = [\n        2,\n        3,\n        4,\n        5,\n        6,\n        8,\n        10,\n        12,\n        15,\n        16,\n        20,\n        24,\n        25,\n        30,\n        40,\n        48,\n        50,\n        60,\n        75,\n        80,\n        90,\n        100,\n        120,\n        125,\n        150,\n        200,\n        240,\n        250,\n        300,\n        375,\n        400,\n        500,\n        600,\n        750,\n        1200,\n        1500,\n        2000,\n        3000,\n        6000,\n        48000\n    ]\n\n    # If input fps is a whole number we'll return.\n    if float(fps).is_integer():\n        # Validate fps is part of Maya's fps selection.\n        if int(fps) not in int_framerates:\n            raise ValueError(\n                \"Framerate \\\"{}\\\" is not supported in Maya\".format(fps)\n            )\n        return int(fps)\n    else:\n        # Differences to supported float frame rates.\n        differences = []\n        for i in float_framerates:\n            differences.append(abs(i - fps))\n\n        # Validate difference does not stray too far from supported framerates.\n        min_difference = min(differences)\n        min_index = differences.index(min_difference)\n        supported_framerate = float_framerates[min_index]\n        if min_difference > 0.1:\n            raise ValueError(\n                \"Framerate \\\"{}\\\" strays too far from any supported framerate\"\n                \" in Maya. Closest supported framerate is \\\"{}\\\"\".format(\n                    fps, supported_framerate\n                )\n            )\n\n        return supported_framerate\n\n\ndef write_xgen_file(data, filepath):\n    \"\"\"Overwrites data in .xgen files.\n\n    Quite naive approach to mainly overwrite \"xgDataPath\" and \"xgProjectPath\".\n\n    Args:\n        data (dict): Dictionary of key, value. Key matches with xgen file.\n        For example:\n            {\"xgDataPath\": \"some/path\"}\n        filepath (string): Absolute path of .xgen file.\n    \"\"\"\n    # Generate regex lookup for line to key basically\n    # match any of the keys in `\\t{key}\\t\\t`\n    keys = \"|\".join(re.escape(key) for key in data.keys())\n    re_keys = re.compile(\"^\\t({})\\t\\t\".format(keys))\n\n    lines = []\n    with open(filepath, \"r\") as f:\n        for line in f:\n            match = re_keys.match(line)\n            if match:\n                key = match.group(1)\n                value = data[key]\n                line = \"\\t{}\\t\\t{}\\n\".format(key, value)\n\n            lines.append(line)\n\n    with open(filepath, \"w\") as f:\n        f.writelines(lines)\n\n\ndef get_color_management_preferences():\n    \"\"\"Get and resolve OCIO preferences.\"\"\"\n    data = {\n        # Is color management enabled.\n        \"enabled\": cmds.colorManagementPrefs(\n            query=True, cmEnabled=True\n        ),\n        \"rendering_space\": cmds.colorManagementPrefs(\n            query=True, renderingSpaceName=True\n        ),\n        \"output_transform\": cmds.colorManagementPrefs(\n            query=True, outputTransformName=True\n        ),\n        \"output_transform_enabled\": cmds.colorManagementPrefs(\n            query=True, outputTransformEnabled=True\n        ),\n        \"view_transform\": cmds.colorManagementPrefs(\n            query=True, viewTransformName=True\n        )\n    }\n\n    # Split view and display from view_transform. view_transform comes in\n    # format of \"{view} ({display})\".\n    regex = re.compile(r\"^(?P<view>.+) \\((?P<display>.+)\\)$\")\n    if int(cmds.about(version=True)) <= 2020:\n        # view_transform comes in format of \"{view} {display}\" in 2020.\n        regex = re.compile(r\"^(?P<view>.+) (?P<display>.+)$\")\n\n    match = regex.match(data[\"view_transform\"])\n    if not match:\n        raise ValueError(\n            \"Unable to parse view and display from Maya view transform: '{}' \"\n            \"using regex '{}'\".format(data[\"view_transform\"], regex.pattern)\n        )\n\n    data.update({\n        \"display\": match.group(\"display\"),\n        \"view\": match.group(\"view\")\n    })\n\n    # Get config absolute path.\n    path = cmds.colorManagementPrefs(\n        query=True, configFilePath=True\n    )\n\n    # The OCIO config supports a custom <MAYA_RESOURCES> token.\n    maya_resources_token = \"<MAYA_RESOURCES>\"\n    maya_resources_path = OpenMaya.MGlobal.getAbsolutePathToResources()\n    path = path.replace(maya_resources_token, maya_resources_path)\n\n    data[\"config\"] = path\n\n    return data\n\n\ndef get_color_management_output_transform():\n    preferences = get_color_management_preferences()\n    colorspace = preferences[\"rendering_space\"]\n    if preferences[\"output_transform_enabled\"]:\n        colorspace = preferences[\"output_transform\"]\n    return colorspace\n\n\ndef image_info(file_path):\n    # type: (str) -> dict\n    \"\"\"Based on tha texture path, get its bit depth and format information.\n    Take reference from makeTx.py in Arnold:\n        ImageInfo(filename): Get Image Information for colorspace\n        AiTextureGetFormat(filename): Get Texture Format\n        AiTextureGetBitDepth(filename): Get Texture bit depth\n    Args:\n        file_path (str): Path to the texture file.\n    Returns:\n        dict: Dictionary with the information about the texture file.\n    \"\"\"\n    from arnold import (\n        AiTextureGetBitDepth,\n        AiTextureGetFormat\n    )\n    # Get Texture Information\n    img_info = {'filename': file_path}\n    if os.path.isfile(file_path):\n        img_info['bit_depth'] = AiTextureGetBitDepth(file_path)  # noqa\n        img_info['format'] = AiTextureGetFormat(file_path)  # noqa\n    else:\n        img_info['bit_depth'] = 8\n        img_info['format'] = \"unknown\"\n    return img_info\n\n\ndef guess_colorspace(img_info):\n    # type: (dict) -> str\n    \"\"\"Guess the colorspace of the input image filename.\n    Note:\n        Reference from makeTx.py\n    Args:\n        img_info (dict): Image info generated by :func:`image_info`\n    Returns:\n        str: color space name use in the `--colorconvert`\n             option of maketx.\n    \"\"\"\n    from arnold import (\n        AiTextureInvalidate,\n        # types\n        AI_TYPE_BYTE,\n        AI_TYPE_INT,\n        AI_TYPE_UINT\n    )\n    try:\n        if img_info['bit_depth'] <= 16:\n            if img_info['format'] in (AI_TYPE_BYTE, AI_TYPE_INT, AI_TYPE_UINT): # noqa\n                return 'sRGB'\n            else:\n                return 'linear'\n        # now discard the image file as AiTextureGetFormat has loaded it\n        AiTextureInvalidate(img_info['filename'])       # noqa\n    except ValueError:\n        print((\"[maketx] Error: Could not guess\"\n               \"colorspace for {}\").format(img_info[\"filename\"]))\n        return \"linear\"\n\n\ndef len_flattened(components):\n    \"\"\"Return the length of the list as if it was flattened.\n\n    Maya will return consecutive components as a single entry\n    when requesting with `maya.cmds.ls` without the `flatten`\n    flag. Though enabling `flatten` on a large list (e.g. millions)\n    will result in a slow result. This command will return the amount\n    of entries in a non-flattened list by parsing the result with\n    regex.\n\n    Args:\n        components (list): The non-flattened components.\n\n    Returns:\n        int: The amount of entries.\n\n    \"\"\"\n    assert isinstance(components, (list, tuple))\n    n = 0\n\n    pattern = re.compile(r\"\\[(\\d+):(\\d+)\\]\")\n    for c in components:\n        match = pattern.search(c)\n        if match:\n            start, end = match.groups()\n            n += int(end) - int(start) + 1\n        else:\n            n += 1\n    return n\n\n\ndef get_all_children(nodes):\n    \"\"\"Return all children of `nodes` including each instanced child.\n    Using maya.cmds.listRelatives(allDescendents=True) includes only the first\n    instance. As such, this function acts as an optimal replacement with a\n    focus on a fast query.\n\n    \"\"\"\n\n    sel = OpenMaya.MSelectionList()\n    traversed = set()\n    iterator = OpenMaya.MItDag(OpenMaya.MItDag.kDepthFirst)\n    for node in nodes:\n\n        if node in traversed:\n            # Ignore if already processed as a child\n            # before\n            continue\n\n        sel.clear()\n        sel.add(node)\n        dag = sel.getDagPath(0)\n\n        iterator.reset(dag)\n        # ignore self\n        iterator.next()  # noqa: B305\n        while not iterator.isDone():\n\n            path = iterator.fullPathName()\n\n            if path in traversed:\n                iterator.prune()\n                iterator.next()  # noqa: B305\n                continue\n\n            traversed.add(path)\n            iterator.next()  # noqa: B305\n\n    return list(traversed)\n\n\ndef get_capture_preset(task_name, task_type, subset, project_settings, log):\n    \"\"\"Get capture preset for playblasting.\n\n    Logic for transitioning from old style capture preset to new capture preset\n    profiles.\n\n    Args:\n        task_name (str): Task name.\n        take_type (str): Task type.\n        subset (str): Subset name.\n        project_settings (dict): Project settings.\n        log (object): Logging object.\n    \"\"\"\n    capture_preset = None\n    filtering_criteria = {\n        \"hosts\": \"maya\",\n        \"families\": \"review\",\n        \"task_names\": task_name,\n        \"task_types\": task_type,\n        \"subset\": subset\n    }\n\n    plugin_settings = project_settings[\"maya\"][\"publish\"][\"ExtractPlayblast\"]\n    if plugin_settings[\"profiles\"]:\n        profile = filter_profiles(\n            plugin_settings[\"profiles\"],\n            filtering_criteria,\n            logger=log\n        )\n        capture_preset = profile.get(\"capture_preset\")\n    else:\n        log.warning(\"No profiles present for Extract Playblast\")\n\n    # Backward compatibility for deprecated Extract Playblast settings\n    # without profiles.\n    if capture_preset is None:\n        log.debug(\n            \"Falling back to deprecated Extract Playblast capture preset \"\n            \"because no new style playblast profiles are defined.\"\n        )\n        capture_preset = plugin_settings[\"capture_preset\"]\n\n    return capture_preset or {}\n\n\ndef get_reference_node(members, log=None):\n    \"\"\"Get the reference node from the container members\n    Args:\n        members: list of node names\n\n    Returns:\n        str: Reference node name.\n\n    \"\"\"\n\n    # Collect the references without .placeHolderList[] attributes as\n    # unique entries (objects only) and skipping the sharedReferenceNode.\n    references = set()\n    for ref in cmds.ls(members, exactType=\"reference\", objectsOnly=True):\n\n        # Ignore any `:sharedReferenceNode`\n        if ref.rsplit(\":\", 1)[-1].startswith(\"sharedReferenceNode\"):\n            continue\n\n        # Ignore _UNKNOWN_REF_NODE_ (PLN-160)\n        if ref.rsplit(\":\", 1)[-1].startswith(\"_UNKNOWN_REF_NODE_\"):\n            continue\n\n        references.add(ref)\n\n    assert references, \"No reference node found in container\"\n\n    # Get highest reference node (least parents)\n    highest = min(references,\n                  key=lambda x: len(get_reference_node_parents(x)))\n\n    # Warn the user when we're taking the highest reference node\n    if len(references) > 1:\n        if not log:\n            log = logging.getLogger(__name__)\n\n        log.warning(\"More than one reference node found in \"\n                    \"container, using highest reference node: \"\n                    \"%s (in: %s)\", highest, list(references))\n\n    return highest\n\n\ndef get_reference_node_parents(ref):\n    \"\"\"Return all parent reference nodes of reference node\n\n    Args:\n        ref (str): reference node.\n\n    Returns:\n        list: The upstream parent reference nodes.\n\n    \"\"\"\n    parent = cmds.referenceQuery(ref,\n                                 referenceNode=True,\n                                 parent=True)\n    parents = []\n    while parent:\n        parents.append(parent)\n        parent = cmds.referenceQuery(parent,\n                                     referenceNode=True,\n                                     parent=True)\n    return parents\n\n\ndef create_rig_animation_instance(\n    nodes, context, namespace, options=None, log=None\n):\n    \"\"\"Create an animation publish instance for loaded rigs.\n\n    See the RecreateRigAnimationInstance inventory action on how to use this\n    for loaded rig containers.\n\n    Arguments:\n        nodes (list): Member nodes of the rig instance.\n        context (dict): Representation context of the rig container\n        namespace (str): Namespace of the rig container\n        options (dict, optional): Additional loader data\n        log (logging.Logger, optional): Logger to log to if provided\n\n    Returns:\n        None\n\n    \"\"\"\n    if options is None:\n        options = {}\n    name = context[\"representation\"][\"name\"]\n    output = next((node for node in nodes if\n                   node.endswith(\"out_SET\")), None)\n    controls = next((node for node in nodes if\n                     node.endswith(\"controls_SET\")), None)\n    if name != \"fbx\":\n        assert output, \"No out_SET in rig, this is a bug.\"\n        assert controls, \"No controls_SET in rig, this is a bug.\"\n\n    anim_skeleton = next((node for node in nodes if\n                          node.endswith(\"skeletonAnim_SET\")), None)\n    skeleton_mesh = next((node for node in nodes if\n                          node.endswith(\"skeletonMesh_SET\")), None)\n\n    # Find the roots amongst the loaded nodes\n    roots = (\n        cmds.ls(nodes, assemblies=True, long=True) or\n        get_highest_in_hierarchy(nodes)\n    )\n    assert roots, \"No root nodes in rig, this is a bug.\"\n\n    custom_subset = options.get(\"animationSubsetName\")\n    if custom_subset:\n        formatting_data = {\n            \"asset\": context[\"asset\"],\n            \"subset\": context['subset']['name'],\n            \"family\": (\n                context['subset']['data'].get('family') or\n                context['subset']['data']['families'][0]\n            )\n        }\n        namespace = get_custom_namespace(\n            custom_subset.format(\n                **formatting_data\n            )\n        )\n\n    if log:\n        log.info(\"Creating subset: {}\".format(namespace))\n\n    # Fill creator identifier\n    creator_identifier = \"io.openpype.creators.maya.animation\"\n\n    host = registered_host()\n    create_context = CreateContext(host)\n    # Create the animation instance\n    rig_sets = [output, controls, anim_skeleton, skeleton_mesh]\n    # Remove sets that this particular rig does not have\n    rig_sets = [s for s in rig_sets if s is not None]\n    with maintained_selection():\n        cmds.select(rig_sets + roots, noExpand=True)\n        create_context.create(\n            creator_identifier=creator_identifier,\n            variant=namespace,\n            pre_create_data={\"use_selection\": True}\n        )\n"
  },
  {
    "path": "openpype/hosts/maya/api/lib_renderproducts.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Module handling expected render output from Maya.\n\nThis module is used in :mod:`collect_render` and :mod:`collect_vray_scene`.\n\nNote:\n    To implement new renderer, just create new class inheriting from\n    :class:`ARenderProducts` and add it to :func:`RenderProducts.get()`.\n\nAttributes:\n    R_SINGLE_FRAME (:class:`re.Pattern`): Find single frame number.\n    R_FRAME_RANGE (:class:`re.Pattern`): Find frame range.\n    R_FRAME_NUMBER (:class:`re.Pattern`): Find frame number in string.\n    R_LAYER_TOKEN (:class:`re.Pattern`): Find layer token in image prefixes.\n    R_AOV_TOKEN (:class:`re.Pattern`): Find AOV token in image prefixes.\n    R_SUBSTITUTE_AOV_TOKEN (:class:`re.Pattern`): Find and substitute AOV token\n        in image prefixes.\n    R_REMOVE_AOV_TOKEN (:class:`re.Pattern`): Find and remove AOV token in\n        image prefixes.\n    R_CLEAN_FRAME_TOKEN (:class:`re.Pattern`): Find and remove unfilled\n        Renderman frame token in image prefix.\n    R_CLEAN_EXT_TOKEN (:class:`re.Pattern`): Find and remove unfilled Renderman\n        extension token in image prefix.\n    R_SUBSTITUTE_LAYER_TOKEN (:class:`re.Pattern`): Find and substitute render\n        layer token in image prefixes.\n    R_SUBSTITUTE_SCENE_TOKEN (:class:`re.Pattern`): Find and substitute scene\n        token in image prefixes.\n    R_SUBSTITUTE_CAMERA_TOKEN (:class:`re.Pattern`): Find and substitute camera\n        token in image prefixes.\n    IMAGE_PREFIXES (dict): Mapping between renderers and their respective\n        image prefix attribute names.\n\nThanks:\n    Roy Nieterau (BigRoy) / Colorbleed for overhaul of original\n    *expected_files*.\n\n\"\"\"\n\nimport logging\nimport re\nimport os\nfrom abc import ABCMeta, abstractmethod\n\nimport six\nimport attr\n\nfrom . import lib\nfrom . import lib_rendersetup\nfrom openpype.pipeline.colorspace import get_ocio_config_views\n\nfrom maya import cmds, mel\n\nlog = logging.getLogger(__name__)\n\nR_SINGLE_FRAME = re.compile(r\"^(-?)\\d+$\")\nR_FRAME_RANGE = re.compile(r\"^(?P<sf>(-?)\\d+)-(?P<ef>(-?)\\d+)$\")\nR_FRAME_NUMBER = re.compile(r\".+\\.(?P<frame>[0-9]+)\\..+\")\nR_LAYER_TOKEN = re.compile(\n    r\".*((?:%l)|(?:<layer>)|(?:<renderlayer>)).*\", re.IGNORECASE\n)\nR_AOV_TOKEN = re.compile(r\".*%a.*|.*<aov>.*|.*<renderpass>.*\", re.IGNORECASE)\nR_SUBSTITUTE_AOV_TOKEN = re.compile(r\"%a|<aov>|<renderpass>\", re.IGNORECASE)\nR_REMOVE_AOV_TOKEN = re.compile(\n    r\"_%a|\\.%a|_<aov>|\\.<aov>|_<renderpass>|\\.<renderpass>\", re.IGNORECASE)\n# to remove unused renderman tokens\nR_CLEAN_FRAME_TOKEN = re.compile(r\"\\.?<f\\d>\\.?\", re.IGNORECASE)\nR_CLEAN_EXT_TOKEN = re.compile(r\"\\.?<ext>\\.?\", re.IGNORECASE)\n\nR_SUBSTITUTE_LAYER_TOKEN = re.compile(\n    r\"%l|<layer>|<renderlayer>\", re.IGNORECASE\n)\nR_SUBSTITUTE_CAMERA_TOKEN = re.compile(r\"%c|<camera>\", re.IGNORECASE)\nR_SUBSTITUTE_SCENE_TOKEN = re.compile(r\"%s|<scene>\", re.IGNORECASE)\n\n# not sure about the renderman image prefix\nIMAGE_PREFIXES = {\n    \"vray\": \"vraySettings.fileNamePrefix\",\n    \"arnold\": \"defaultRenderGlobals.imageFilePrefix\",\n    \"renderman\": \"rmanGlobals.imageFileFormat\",\n    \"redshift\": \"defaultRenderGlobals.imageFilePrefix\",\n    \"mayahardware2\": \"defaultRenderGlobals.imageFilePrefix\"\n}\n\nRENDERMAN_IMAGE_DIR = \"<scene>/<layer>\"\n\n\ndef has_tokens(string, tokens):\n    \"\"\"Return whether any of tokens is in input string (case-insensitive)\"\"\"\n    pattern = \"({})\".format(\"|\".join(re.escape(token) for token in tokens))\n    match = re.search(pattern, string, re.IGNORECASE)\n    return bool(match)\n\n\n@attr.s\nclass LayerMetadata(object):\n    \"\"\"Data class for Render Layer metadata.\"\"\"\n    frameStart = attr.ib()\n    frameEnd = attr.ib()\n    cameras = attr.ib()\n    sceneName = attr.ib()\n    layerName = attr.ib()\n    renderer = attr.ib()\n    defaultExt = attr.ib()\n    filePrefix = attr.ib()\n    frameStep = attr.ib(default=1)\n    padding = attr.ib(default=4)\n\n    # Render Products\n    products = attr.ib(init=False, default=attr.Factory(list))\n\n    # The AOV separator token. Note that not all renderers define an explicit\n    # render separator but allow to put the AOV/RenderPass token anywhere in\n    # the file path prefix. For those renderers we'll fall back to whatever\n    # is between the last occurrences of <RenderLayer> and <RenderPass> tokens.\n    aov_separator = attr.ib(default=\"_\")\n\n\n@attr.s\nclass RenderProduct(object):\n    \"\"\"Describes an image or other file-like artifact produced by a render.\n\n    Warning:\n        This currently does NOT return as a product PER render camera.\n        A single Render Product will generate files per camera. E.g. with two\n        cameras each render product generates two sequences on disk assuming\n        the file path prefix correctly uses the <Camera> tokens.\n\n    \"\"\"\n    productName = attr.ib()\n    ext = attr.ib()                             # extension\n    colorspace = attr.ib()                      # colorspace\n    aov = attr.ib(default=None)                 # source aov\n    driver = attr.ib(default=None)              # source driver\n    multipart = attr.ib(default=False)          # multichannel file\n    camera = attr.ib(default=None)              # used only when rendering\n    #                                             from multiple cameras\n\n\ndef get(layer, render_instance=None):\n    # type: (str, object) -> ARenderProducts\n    \"\"\"Get render details and products for given renderer and render layer.\n\n    Args:\n        layer (str): Name of render layer\n        render_instance (pyblish.api.Instance): Publish instance.\n            If not provided an empty mock instance is used.\n\n    Returns:\n        ARenderProducts: The correct RenderProducts instance for that\n            renderlayer.\n\n    Raises:\n        :exc:`UnsupportedRendererException`: If requested renderer\n            is not supported. It needs to be implemented by extending\n            :class:`ARenderProducts` and added to this methods ``if``\n            statement.\n\n    \"\"\"\n\n    if render_instance is None:\n        # For now produce a mock instance\n        class Instance(object):\n            data = {}\n        render_instance = Instance()\n\n    renderer_name = lib.get_attr_in_layer(\n        \"defaultRenderGlobals.currentRenderer\",\n        layer=layer\n    )\n\n    renderer = {\n        \"arnold\": RenderProductsArnold,\n        \"vray\": RenderProductsVray,\n        \"redshift\": RenderProductsRedshift,\n        \"renderman\": RenderProductsRenderman,\n        \"mayahardware2\": RenderProductsMayaHardware\n    }.get(renderer_name.lower(), None)\n    if renderer is None:\n        raise UnsupportedRendererException(\n            \"Unsupported renderer: {}\".format(renderer_name)\n        )\n\n    return renderer(layer, render_instance)\n\n\n@six.add_metaclass(ABCMeta)\nclass ARenderProducts:\n    \"\"\"Abstract class with common code for all renderers.\n\n    Attributes:\n        renderer (str): name of renderer.\n\n    \"\"\"\n\n    renderer = None\n\n    def __init__(self, layer, render_instance):\n        \"\"\"Constructor.\"\"\"\n        self.layer = layer\n        self.render_instance = render_instance\n        self.multipart = self.get_multipart()\n\n        # Initialize\n        self.layer_data = self._get_layer_data()\n        self.layer_data.products = self.get_render_products()\n\n    def get_multipart(self):\n        raise NotImplementedError(\n            \"The render product implementation does not have a \"\n            \"\\\"get_multipart\\\" method.\"\n        )\n\n    def has_camera_token(self):\n        # type: () -> bool\n        \"\"\"Check if camera token is in image prefix.\n\n        Returns:\n            bool: True/False if camera token is present.\n\n        \"\"\"\n        return \"<camera>\" in self.layer_data.filePrefix.lower()\n\n    @abstractmethod\n    def get_render_products(self):\n        \"\"\"To be implemented by renderer class.\n\n        This should return a list of RenderProducts.\n\n        Returns:\n            list: List of RenderProduct\n\n        \"\"\"\n\n    @staticmethod\n    def sanitize_camera_name(camera):\n        # type: (str) -> str\n        \"\"\"Sanitize camera name.\n\n        Remove Maya illegal characters from camera name.\n\n        Args:\n            camera (str): Maya camera name.\n\n        Returns:\n            (str): sanitized camera name\n\n        Example:\n            >>> ARenderProducts.sanizite_camera_name('test:camera_01')\n            test_camera_01\n\n        \"\"\"\n        return re.sub('[^0-9a-zA-Z_]+', '_', camera)\n\n    def get_renderer_prefix(self):\n        # type: () -> str\n        \"\"\"Return prefix for specific renderer.\n\n        This is for most renderers the same and can be overridden if needed.\n\n        Returns:\n            str: String with image prefix containing tokens\n\n        Raises:\n            :exc:`UnsupportedRendererException`: If we requested image\n                prefix for renderer we know nothing about.\n                See :data:`IMAGE_PREFIXES` for mapping of renderers and\n                image prefixes.\n\n        \"\"\"\n        try:\n            prefix_attr = IMAGE_PREFIXES[self.renderer]\n        except KeyError:\n            raise UnsupportedRendererException(\n                \"Unsupported renderer {}\".format(self.renderer)\n            )\n\n        # Note: When this attribute is never set (e.g. on maya launch) then\n        # this can return None even though it is a string attribute\n        prefix = self._get_attr(prefix_attr)\n\n        if not prefix:\n            # Fall back to scene name by default\n            log.warning(\"Image prefix not set, using <Scene>\")\n            prefix = \"<Scene>\"\n\n        return prefix\n\n    def get_render_attribute(self, attribute):\n        \"\"\"Get attribute from render options.\n\n        Args:\n            attribute (str): name of attribute to be looked up.\n\n        Returns:\n            Attribute value\n\n        \"\"\"\n        return self._get_attr(\"defaultRenderGlobals\", attribute)\n\n    def _get_attr(self, node_attr, attribute=None):\n        \"\"\"Return the value of the attribute in the renderlayer\n\n        For readability this allows passing in the attribute in two ways.\n\n            As a single argument:\n                _get_attr(\"node.attr\")\n            Or as two arguments:\n                _get_attr(\"node\", \"attr\")\n\n        Returns:\n            Value of the attribute inside the layer this instance is set to.\n\n        \"\"\"\n\n        if attribute is None:\n            plug = node_attr\n        else:\n            plug = \"{}.{}\".format(node_attr, attribute)\n\n        return lib.get_attr_in_layer(plug, layer=self.layer)\n\n    @staticmethod\n    def extract_separator(file_prefix):\n        \"\"\"Extract AOV separator character from the prefix.\n\n        Default behavior extracts the part between\n        last occurrences of <RenderLayer> and <RenderPass>\n\n        Todo:\n            This code also triggers for V-Ray which overrides it explicitly\n            so this code will invalidly debug log it couldn't extract the\n            AOV separator even though it does set it in RenderProductsVray.\n\n        Args:\n            file_prefix (str): File prefix with tokens.\n\n        Returns:\n            str or None: prefix character if it can be extracted.\n        \"\"\"\n        layer_tokens = [\"<renderlayer>\", \"<layer>\"]\n        aov_tokens = [\"<aov>\", \"<renderpass>\"]\n\n        def match_last(tokens, text):\n            \"\"\"regex match the last occurrence from a list of tokens\"\"\"\n            pattern = \"(?:.*)({})\".format(\"|\".join(tokens))\n            return re.search(pattern, text, re.IGNORECASE)\n\n        layer_match = match_last(layer_tokens, file_prefix)\n        aov_match = match_last(aov_tokens, file_prefix)\n        separator = None\n        if layer_match and aov_match:\n            matches = sorted((layer_match, aov_match),\n                             key=lambda match: match.end(1))\n            separator = file_prefix[matches[0].end(1):matches[1].start(1)]\n        return separator\n\n    def _get_layer_data(self):\n        # type: () -> LayerMetadata\n        #                      ______________________________________________\n        # ____________________/ ____________________________________________/\n        # 1 -  get scene name  /__________________/\n        # ____________________/\n        _, scene_basename = os.path.split(cmds.file(q=True, loc=True))\n        scene_name, _ = os.path.splitext(scene_basename)\n        kwargs = {}\n        file_prefix = self.get_renderer_prefix()\n\n        # If the Render Layer belongs to a Render Setup layer then the\n        # output name is based on the Render Setup Layer name without\n        # the `rs_` prefix.\n        layer_name = self.layer\n        rs_layer = lib_rendersetup.get_rendersetup_layer(layer_name)\n        if rs_layer:\n            layer_name = rs_layer\n\n        if self.layer == \"defaultRenderLayer\":\n            # defaultRenderLayer renders as masterLayer\n            layer_name = \"masterLayer\"\n\n        separator = self.extract_separator(file_prefix)\n        if separator:\n            kwargs[\"aov_separator\"] = separator\n        else:\n            log.debug(\"Couldn't extract aov separator from \"\n                      \"file prefix: {}\".format(file_prefix))\n\n        # todo: Support Custom Frames sequences 0,5-10,100-120\n        #       Deadline allows submitting renders with a custom frame list\n        #       to support those cases we might want to allow 'custom frames'\n        #       to be overridden to `ExpectFiles` class?\n        return LayerMetadata(\n            frameStart=int(self.get_render_attribute(\"startFrame\")),\n            frameEnd=int(self.get_render_attribute(\"endFrame\")),\n            frameStep=int(self.get_render_attribute(\"byFrameStep\")),\n            padding=int(self.get_render_attribute(\"extensionPadding\")),\n            # if we have <camera> token in prefix path we'll expect output for\n            # every renderable camera in layer.\n            cameras=self.get_renderable_cameras(),\n            sceneName=scene_name,\n            layerName=layer_name,\n            renderer=self.renderer,\n            defaultExt=self._get_attr(\"defaultRenderGlobals.imfPluginKey\"),\n            filePrefix=file_prefix,\n            **kwargs\n        )\n\n    def _generate_file_sequence(\n            self, layer_data,\n            force_aov_name=None,\n            force_ext=None,\n            force_cameras=None):\n        # type: (LayerMetadata, str, str, list) -> list\n        expected_files = []\n        cameras = force_cameras or layer_data.cameras\n        ext = force_ext or layer_data.defaultExt\n        for cam in cameras:\n            file_prefix = layer_data.filePrefix\n            mappings = (\n                (R_SUBSTITUTE_SCENE_TOKEN, layer_data.sceneName),\n                (R_SUBSTITUTE_LAYER_TOKEN, layer_data.layerName),\n                (R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)),\n                # this is required to remove unfilled aov token, for example\n                # in Redshift\n                (R_REMOVE_AOV_TOKEN, \"\") if not force_aov_name \\\n                else (R_SUBSTITUTE_AOV_TOKEN, force_aov_name),\n\n                (R_CLEAN_FRAME_TOKEN, \"\"),\n                (R_CLEAN_EXT_TOKEN, \"\"),\n            )\n\n            for regex, value in mappings:\n                file_prefix = re.sub(regex, value, file_prefix)\n\n            for frame in range(\n                    int(layer_data.frameStart),\n                    int(layer_data.frameEnd) + 1,\n                    int(layer_data.frameStep),\n            ):\n                frame_str = str(frame).rjust(layer_data.padding, \"0\")\n                expected_files.append(\n                    \"{}.{}.{}\".format(file_prefix, frame_str, ext)\n                )\n        return expected_files\n\n    def get_files(self, product):\n        # type: (RenderProduct) -> list\n        \"\"\"Return list of expected files.\n\n        It will translate render token strings  ('<RenderPass>', etc.) to\n        their values. This task is tricky as every renderer deals with this\n        differently. That's why we expose `get_files` as a method on the\n        Renderer class so it can be overridden for complex cases.\n\n        Args:\n            product (RenderProduct): Render product to be used for file\n                generation.\n\n        Returns:\n            List of files\n\n        \"\"\"\n        return self._generate_file_sequence(\n            self.layer_data,\n            force_aov_name=product.productName,\n            force_ext=product.ext,\n            force_cameras=[product.camera]\n        )\n\n    def get_renderable_cameras(self):\n        # type: () -> list\n        \"\"\"Get all renderable camera transforms.\n\n        Returns:\n            list: list of renderable cameras.\n\n        \"\"\"\n\n        renderable_cameras = [\n            cam for cam in cmds.ls(cameras=True)\n            if self._get_attr(cam, \"renderable\")\n        ]\n\n        # The output produces a sanitized name for <Camera> using its\n        # shortest unique path of the transform so we'll return\n        # at least that unique path. This could include a parent\n        # name too when two cameras have the same name but are\n        # in a different hierarchy, e.g. \"group1|cam\" and \"group2|cam\"\n        def get_name(camera):\n            return cmds.ls(cmds.listRelatives(camera,\n                                              parent=True,\n                                              fullPath=True))[0]\n\n        return [get_name(cam) for cam in renderable_cameras]\n\n\nclass RenderProductsArnold(ARenderProducts):\n    \"\"\"Render products for Arnold renderer.\n\n    References:\n        mtoa.utils.getFileName()\n        mtoa.utils.ui.common.updateArnoldTargetFilePreview()\n\n    Notes:\n        - Output Denoising AOVs are not currently included.\n        - Only Frame/Animation ext: name.#.ext is supported.\n        - Use Custom extension is not supported.\n        - <RenderPassType> and <RenderPassFileGroup> tokens not tested\n        - With Merge AOVs but <RenderPass> in File Name Prefix Arnold\n          will still NOT merge the aovs. This class correctly resolves\n          it - but user should be aware.\n        - File Path Prefix overrides per AOV driver are not implemented\n\n    Attributes:\n        aiDriverExtension (dict): Arnold AOV driver extension mapping.\n            Is there a better way?\n        renderer (str): name of renderer.\n\n    \"\"\"\n    renderer = \"arnold\"\n    aiDriverExtension = {\n        \"jpeg\": \"jpg\",\n        \"exr\": \"exr\",\n        \"deepexr\": \"exr\",\n        \"png\": \"png\",\n        \"tiff\": \"tif\",\n        \"mtoa_shaders\": \"ass\",  # TODO: research what those last two should be\n        \"maya\": \"\",\n    }\n\n    def get_renderer_prefix(self):\n\n        prefix = super(RenderProductsArnold, self).get_renderer_prefix()\n        merge_aovs = self._get_attr(\"defaultArnoldDriver.mergeAOVs\")\n        if not merge_aovs and \"<renderpass>\" not in prefix.lower():\n            # When Merge AOVs is disabled and <renderpass> token not present\n            # then Arnold prepends <RenderPass>/ to the output path.\n            # todo: It's untested what happens if AOV driver has an\n            #       an explicit override path prefix.\n            prefix = \"<RenderPass>/\" + prefix\n\n        return prefix\n\n    def get_multipart(self):\n        multipart = False\n        multilayer = bool(self._get_attr(\"defaultArnoldDriver.multipart\"))\n        merge_AOVs = bool(self._get_attr(\"defaultArnoldDriver.mergeAOVs\"))\n        if multilayer or merge_AOVs:\n            multipart = True\n\n        return multipart\n\n    def _get_aov_render_products(self, aov, cameras=None):\n        \"\"\"Return all render products for the AOV\"\"\"\n\n        products = []\n        aov_name = self._get_attr(aov, \"name\")\n        ai_drivers = cmds.listConnections(\"{}.outputs\".format(aov),\n                                          source=True,\n                                          destination=False,\n                                          type=\"aiAOVDriver\") or []\n        if not cameras:\n            cameras = [\n                self.sanitize_camera_name(\n                    self.get_renderable_cameras()[0]\n                )\n            ]\n\n        for ai_driver in ai_drivers:\n            colorspace = self._get_colorspace(\n                ai_driver + \".colorManagement\"\n            )\n            # todo: check aiAOVDriver.prefix as it could have\n            #       a custom path prefix set for this driver\n\n            # Skip Drivers set only for GUI\n            # 0: GUI, 1: Batch, 2: GUI and Batch\n            output_mode = self._get_attr(ai_driver, \"outputMode\")\n            if output_mode == 0:  # GUI only\n                log.warning(\"%s has Output Mode set to GUI, \"\n                            \"skipping...\", ai_driver)\n                continue\n\n            ai_translator = self._get_attr(ai_driver, \"aiTranslator\")\n            try:\n                ext = self.aiDriverExtension[ai_translator]\n            except KeyError:\n                raise AOVError(\n                    \"Unrecognized arnold driver format \"\n                    \"for AOV - {}\".format(aov_name)\n                )\n\n            # If aov RGBA is selected, arnold will translate it to `beauty`\n            name = aov_name\n            if name == \"RGBA\":\n                name = \"beauty\"\n\n            # Support Arnold light groups for AOVs\n            # Global AOV: When disabled the main layer is\n            #             not written: `{pass}`\n            # All Light Groups: When enabled, a `{pass}_lgroups` file is\n            #                   written and is always merged into a\n            #                   single file\n            # Light Groups List: When set, a product per light\n            #                    group is written\n            #                    e.g. {pass}_front, {pass}_rim\n            global_aov = self._get_attr(aov, \"globalAov\")\n            if global_aov:\n                for camera in cameras:\n                    product = RenderProduct(\n                        productName=name,\n                        ext=ext,\n                        aov=aov_name,\n                        driver=ai_driver,\n                        multipart=self.multipart,\n                        camera=camera,\n                        colorspace=colorspace\n                    )\n                    products.append(product)\n\n            all_light_groups = self._get_attr(aov, \"lightGroups\")\n            if all_light_groups:\n                # All light groups is enabled. A single multipart\n                # Render Product\n                for camera in cameras:\n                    product = RenderProduct(\n                        productName=name + \"_lgroups\",\n                        ext=ext,\n                        aov=aov_name,\n                        driver=ai_driver,\n                        # Always multichannel output\n                        multipart=True,\n                        camera=camera,\n                        colorspace=colorspace\n                    )\n                    products.append(product)\n            else:\n                value = self._get_attr(aov, \"lightGroupsList\")\n                if not value:\n                    continue\n                selected_light_groups = value.strip().split()\n                for light_group in selected_light_groups:\n                    # Render Product per selected light group\n                    aov_light_group_name = \"{}_{}\".format(name, light_group)\n                    for camera in cameras:\n                        product = RenderProduct(\n                            productName=aov_light_group_name,\n                            aov=aov_name,\n                            driver=ai_driver,\n                            ext=ext,\n                            camera=camera,\n                            colorspace=colorspace\n                        )\n                        products.append(product)\n\n        return products\n\n    def _get_colorspace(self, attribute):\n        \"\"\"Resolve colorspace from Arnold settings.\"\"\"\n\n        def _view_transform():\n            preferences = lib.get_color_management_preferences()\n            views_data = get_ocio_config_views(preferences[\"config\"])\n            view_data = views_data[\n                \"{}/{}\".format(preferences[\"display\"], preferences[\"view\"])\n            ]\n            return view_data[\"colorspace\"]\n\n        def _raw():\n            preferences = lib.get_color_management_preferences()\n            return preferences[\"rendering_space\"]\n\n        resolved_values = {\n            \"Raw\": _raw,\n            \"Use View Transform\": _view_transform,\n            # Default. Same as Maya Preferences.\n            \"Use Output Transform\": lib.get_color_management_output_transform\n        }\n        return resolved_values[self._get_attr(attribute)]()\n\n    def get_render_products(self):\n        \"\"\"Get all AOVs.\n\n        See Also:\n            :func:`ARenderProducts.get_render_products()`\n\n        Raises:\n            :class:`AOVError`: If AOV cannot be determined.\n\n        \"\"\"\n\n        if not cmds.ls(\"defaultArnoldRenderOptions\", type=\"aiOptions\"):\n            # this occurs when Render Setting windows was not opened yet. In\n            # such case there are no Arnold options created so query for AOVs\n            # will fail. We terminate here as there are no AOVs specified then.\n            # This state will most probably fail later on some Validator\n            # anyway.\n            return []\n\n        # check if camera token is in prefix. If so, and we have list of\n        # renderable cameras, generate render product for each and every\n        # of them.\n        cameras = [\n            self.sanitize_camera_name(c)\n            for c in self.get_renderable_cameras()\n        ]\n\n        default_ext = self._get_attr(\"defaultRenderGlobals.imfPluginKey\")\n        colorspace = self._get_colorspace(\n            \"defaultArnoldDriver.colorManagement\"\n        )\n        beauty_products = [\n            RenderProduct(\n                productName=\"beauty\",\n                ext=default_ext,\n                driver=\"defaultArnoldDriver\",\n                camera=camera,\n                colorspace=colorspace\n            ) for camera in cameras\n        ]\n\n        # AOVs > Legacy > Maya Render View > Mode\n        aovs_enabled = bool(\n            self._get_attr(\"defaultArnoldRenderOptions.aovMode\")\n        )\n        if not aovs_enabled:\n            return beauty_products\n\n        # Common > File Output > Merge AOVs or <RenderPass>\n        # We don't need to check for Merge AOVs due to overridden\n        # `get_renderer_prefix()` behavior which forces <renderpass>\n        has_renderpass_token = (\n            \"<renderpass>\" in self.layer_data.filePrefix.lower()\n        )\n        if not has_renderpass_token:\n            for product in beauty_products:\n                product.multipart = True\n            return beauty_products\n\n        # AOVs are set to be rendered separately. We should expect\n        # <RenderPass> token in path.\n        # handle aovs from references\n        use_ref_aovs = self.render_instance.data.get(\n            \"useReferencedAovs\", False) or False\n\n        aovs = cmds.ls(type=\"aiAOV\")\n        if not use_ref_aovs:\n            ref_aovs = cmds.ls(type=\"aiAOV\", referencedNodes=True)\n            aovs = list(set(aovs) - set(ref_aovs))\n\n        products = []\n\n        # Append the AOV products\n        for aov in aovs:\n            enabled = self._get_attr(aov, \"enabled\")\n            if not enabled:\n                continue\n\n            # For now stick to the legacy output format.\n            aov_products = self._get_aov_render_products(aov, cameras)\n            products.extend(aov_products)\n\n        if all(product.aov != \"RGBA\" for product in products):\n            # Append default 'beauty' as this is arnolds default.\n            # However, it is excluded whenever a RGBA pass is enabled.\n            # For legibility add the beauty layer as first entry\n            products += beauty_products\n\n        # TODO: Output Denoising AOVs?\n\n        return products\n\n\nclass RenderProductsVray(ARenderProducts):\n    \"\"\"Expected files for V-Ray renderer.\n\n    Notes:\n        - \"Disabled\" animation incorrectly returns frames in filename\n        - \"Renumber Frames\" is not supported\n\n    Reference:\n        vrayAddRenderElementImpl() in vrayCreateRenderElementsTab.mel\n\n    \"\"\"\n    # todo: detect whether rendering with V-Ray GPU + whether AOV is supported\n\n    renderer = \"vray\"\n\n    def get_multipart(self):\n        multipart = False\n        image_format = self._get_attr(\"vraySettings.imageFormatStr\")\n        if image_format == \"exr (multichannel)\":\n            multipart = True\n\n        return multipart\n\n    def get_renderer_prefix(self):\n        # type: () -> str\n        \"\"\"Get image prefix for V-Ray.\n\n        This overrides :func:`ARenderProducts.get_renderer_prefix()` as\n        we must add `<aov>` token manually. This is done only for\n        non-multipart outputs, where `<aov>` token doesn't make sense.\n\n        See also:\n            :func:`ARenderProducts.get_renderer_prefix()`\n\n        \"\"\"\n        prefix = super(RenderProductsVray, self).get_renderer_prefix()\n        if self.multipart:\n            return prefix\n        aov_separator = self._get_aov_separator()\n        prefix = \"{}{}<aov>\".format(prefix, aov_separator)\n        return prefix\n\n    def _get_aov_separator(self):\n        # type: () -> str\n        \"\"\"Return the V-Ray AOV/Render Elements separator\"\"\"\n        return self._get_attr(\n            \"vraySettings.fileNameRenderElementSeparator\"\n        )\n\n    def _get_layer_data(self):\n        # type: () -> LayerMetadata\n        \"\"\"Override to get vray specific extension.\"\"\"\n        layer_data = super(RenderProductsVray, self)._get_layer_data()\n\n        default_ext = self._get_attr(\"vraySettings.imageFormatStr\")\n        if default_ext in [\"exr (multichannel)\", \"exr (deep)\"]:\n            default_ext = \"exr\"\n        layer_data.defaultExt = default_ext\n        layer_data.padding = self._get_attr(\"vraySettings.fileNamePadding\")\n\n        layer_data.aov_separator = self._get_aov_separator()\n\n        return layer_data\n\n    def get_render_products(self):\n        \"\"\"Get all AOVs.\n\n        See Also:\n            :func:`ARenderProducts.get_render_products()`\n\n        \"\"\"\n        if not cmds.ls(\"vraySettings\", type=\"VRaySettingsNode\"):\n            # this occurs when Render Setting windows was not opened yet. In\n            # such case there are no VRay options created so query for AOVs\n            # will fail. We terminate here as there are no AOVs specified then.\n            # This state will most probably fail later on some Validator\n            # anyway.\n            return []\n\n        cameras = [\n            self.sanitize_camera_name(c)\n            for c in self.get_renderable_cameras()\n        ]\n\n        image_format_str = self._get_attr(\"vraySettings.imageFormatStr\")\n        default_ext = image_format_str\n        if default_ext in {\"exr (multichannel)\", \"exr (deep)\"}:\n            default_ext = \"exr\"\n\n        colorspace = lib.get_color_management_output_transform()\n        products = []\n\n        # add beauty as default when not disabled\n        dont_save_rgb = self._get_attr(\"vraySettings.dontSaveRgbChannel\")\n        if not dont_save_rgb:\n            for camera in cameras:\n                products.append(\n                    RenderProduct(\n                        productName=\"\",\n                        ext=default_ext,\n                        camera=camera,\n                        colorspace=colorspace,\n                        multipart=self.multipart\n                    )\n                )\n\n        # separate alpha file\n        separate_alpha = self._get_attr(\"vraySettings.separateAlpha\")\n        if separate_alpha:\n            for camera in cameras:\n                products.append(\n                    RenderProduct(\n                        productName=\"Alpha\",\n                        ext=default_ext,\n                        camera=camera,\n                        colorspace=colorspace,\n                        multipart=self.multipart\n                    )\n                )\n        if self.multipart:\n            # AOVs are merged in m-channel file, only main layer is rendered\n            return products\n\n        # handle aovs from references\n        use_ref_aovs = self.render_instance.data.get(\n            \"useReferencedAovs\", False) or False\n\n        # this will have list of all aovs no matter if they are coming from\n        # reference or not.\n        aov_types = [\"VRayRenderElement\", \"VRayRenderElementSet\"]\n        aovs = cmds.ls(type=aov_types)\n        if not use_ref_aovs:\n            ref_aovs = cmds.ls(type=aov_types, referencedNodes=True) or []\n            aovs = list(set(aovs) - set(ref_aovs))\n\n        for aov in aovs:\n            enabled = self._get_attr(aov, \"enabled\")\n            if not enabled:\n                continue\n\n            class_type = self._get_attr(aov + \".vrayClassType\")\n            if class_type == \"LightMixElement\":\n                # Special case which doesn't define a name by itself but\n                # instead seems to output multiple Render Products,\n                # specifically \"Self_Illumination\" and \"Environment\"\n                product_names = [\"Self_Illumination\", \"Environment\"]\n                for camera in cameras:\n                    for name in product_names:\n                        product = RenderProduct(productName=name,\n                                                ext=default_ext,\n                                                aov=aov,\n                                                camera=camera,\n                                                colorspace=colorspace)\n                        products.append(product)\n                # Continue as we've processed this special case AOV\n                continue\n\n            aov_name = self._get_vray_aov_name(aov)\n            for camera in cameras:\n                product = RenderProduct(\n                    productName=aov_name,\n                    ext=default_ext,\n                    aov=aov,\n                    camera=camera,\n                    colorspace=colorspace\n                )\n                products.append(product)\n\n        return products\n\n    def _get_vray_aov_attr(self, node, prefix):\n        \"\"\"Get value for attribute that starts with key in name\n\n        V-Ray AOVs have attribute names that include the type\n        of AOV in the attribute name, for example:\n            - vray_filename_rawdiffuse\n            - vray_filename_velocity\n            - vray_name_gi\n            - vray_explicit_name_extratex\n\n        To simplify querying the \"vray_filename\" or \"vray_name\"\n        attributes we just find the first attribute that has\n        that particular \"{prefix}_\" in the attribute name.\n\n        Args:\n            node (str): AOV node name\n            prefix (str): Prefix of the attribute name.\n\n        Returns:\n            Value of the attribute if it exists, else None\n\n        \"\"\"\n        attrs = cmds.listAttr(node, string=\"{}_*\".format(prefix))\n        if not attrs:\n            return None\n\n        assert len(attrs) == 1, \"Found more than one attribute: %s\" % attrs\n        attr = attrs[0]\n\n        return self._get_attr(node, attr)\n\n    def _get_vray_aov_name(self, node):\n        \"\"\"Get AOVs name from Vray.\n\n        Args:\n            node (str): aov node name.\n\n        Returns:\n            str: aov name.\n\n        \"\"\"\n\n        vray_explicit_name = self._get_vray_aov_attr(node,\n                                                     \"vray_explicit_name\")\n        vray_filename = self._get_vray_aov_attr(node, \"vray_filename\")\n        vray_name = self._get_vray_aov_attr(node, \"vray_name\")\n        final_name = vray_explicit_name or vray_filename or vray_name or None\n\n        class_type = self._get_attr(node, \"vrayClassType\")\n        if not vray_explicit_name:\n            # Explicit name takes precedence and overrides completely\n            # otherwise add the connected node names to the special cases\n            # Any namespace colon ':' gets replaced to underscore '_'\n            # so we sanitize using `sanitize_camera_name`\n            def _get_source_name(node, attr):\n                \"\"\"Return sanitized name of input connection to attribute\"\"\"\n                plug = \"{}.{}\".format(node, attr)\n                connections = cmds.listConnections(plug,\n                                                   source=True,\n                                                   destination=False)\n                if connections:\n                    return self.sanitize_camera_name(connections[0])\n\n            if class_type == \"MaterialSelectElement\":\n                # Name suffix is based on the connected material or set\n                attrs = [\n                    \"vray_mtllist_mtlselect\",\n                    \"vray_mtl_mtlselect\"\n                ]\n                for attribute in attrs:\n                    name = _get_source_name(node, attribute)\n                    if name:\n                        final_name += '_{}'.format(name)\n                        break\n                else:\n                    log.warning(\"Material Select Element has no \"\n                                \"selected materials: %s\", node)\n\n            elif class_type == \"ExtraTexElement\":\n                # Name suffix is based on the connected textures\n                extratex_type = self._get_attr(node, \"vray_type_extratex\")\n                attr = {\n                    0: \"vray_texture_extratex\",\n                    1: \"vray_float_texture_extratex\",\n                    2: \"vray_int_texture_extratex\",\n                }.get(extratex_type)\n                name = _get_source_name(node, attr)\n                if name:\n                    final_name += '_{}'.format(name)\n                else:\n                    log.warning(\"Extratex Element has no incoming texture\")\n\n        assert final_name, \"Output filename not defined for AOV: %s\" % node\n\n        return final_name\n\n\nclass RenderProductsRedshift(ARenderProducts):\n    \"\"\"Expected files for Redshift renderer.\n\n    Notes:\n        - `get_files()` only supports rendering with frames, like \"animation\"\n\n    Attributes:\n\n        unmerged_aovs (list): Name of aovs that are not merged into resulting\n            exr and we need them specified in Render Products output.\n\n    \"\"\"\n\n    renderer = \"redshift\"\n    unmerged_aovs = {\"Cryptomatte\"}\n\n    def get_files(self, product):\n        # When outputting AOVs we need to replace Redshift specific AOV tokens\n        # with Maya render tokens for generating file sequences. We validate to\n        # a specific AOV fileprefix so we only need to account for one\n        # replacement.\n        if not product.multipart and product.driver:\n            file_prefix = self._get_attr(product.driver + \".filePrefix\")\n            self.layer_data.filePrefix = file_prefix.replace(\n                \"<BeautyPath>/<BeautyFile>\",\n                \"<Scene>/<RenderLayer>/<RenderLayer>\"\n            )\n\n        return super(RenderProductsRedshift, self).get_files(product)\n\n    def get_multipart(self):\n        # For Redshift we don't directly return upon forcing multilayer\n        # due to some AOVs still being written into separate files,\n        # like Cryptomatte.\n        # AOVs are merged in multi-channel file\n        multipart = False\n        force_layer = bool(\n            self._get_attr(\"redshiftOptions.exrForceMultilayer\")\n        )\n        if force_layer:\n            multipart = True\n\n        return multipart\n\n    def get_renderer_prefix(self):\n        \"\"\"Get image prefix for Redshift.\n\n        This overrides :func:`ARenderProducts.get_renderer_prefix()` as\n        we must add `<aov>` token manually. This is done only for\n        non-multipart outputs, where `<aov>` token doesn't make sense.\n\n        See also:\n            :func:`ARenderProducts.get_renderer_prefix()`\n\n        \"\"\"\n        prefix = super(RenderProductsRedshift, self).get_renderer_prefix()\n        if self.multipart:\n            return prefix\n        separator = self.extract_separator(prefix)\n        prefix = \"{}{}<aov>\".format(prefix, separator or \"_\")\n        return prefix\n\n    def get_render_products(self):\n        \"\"\"Get all AOVs.\n\n        See Also:\n            :func:`ARenderProducts.get_render_products()`\n\n        \"\"\"\n\n        if not cmds.ls(\"redshiftOptions\", type=\"RedshiftOptions\"):\n            # this occurs when Render Setting windows was not opened yet. In\n            # such case there are no Redshift options created so query for AOVs\n            # will fail. We terminate here as there are no AOVs specified then.\n            # This state will most probably fail later on some Validator\n            # anyway.\n            return []\n\n        cameras = [\n            self.sanitize_camera_name(c)\n            for c in self.get_renderable_cameras()\n        ]\n\n        # Get Redshift Extension from image format\n        image_format = self._get_attr(\"redshiftOptions.imageFormat\")  # integer\n        ext = mel.eval(\"redshiftGetImageExtension(%i)\" % image_format)\n\n        use_ref_aovs = self.render_instance.data.get(\n            \"useReferencedAovs\", False) or False\n\n        aovs = cmds.ls(type=\"RedshiftAOV\")\n        if not use_ref_aovs:\n            ref_aovs = cmds.ls(type=\"RedshiftAOV\", referencedNodes=True)\n            aovs = list(set(aovs) - set(ref_aovs))\n\n        products = []\n        light_groups_enabled = False\n        has_beauty_aov = False\n        colorspace = lib.get_color_management_output_transform()\n        for aov in aovs:\n            enabled = self._get_attr(aov, \"enabled\")\n            if not enabled:\n                continue\n\n            aov_type = self._get_attr(aov, \"aovType\")\n            if self.multipart and aov_type not in self.unmerged_aovs:\n                continue\n\n            # Any AOVs that still get processed, like Cryptomatte\n            # by themselves are not multipart files.\n\n            # Redshift skips rendering of masterlayer without AOV suffix\n            # when a Beauty AOV is rendered. It overrides the main layer.\n            if aov_type == \"Beauty\":\n                has_beauty_aov = True\n\n            aov_name = self._get_attr(aov, \"name\")\n\n            # Support light Groups\n            light_groups = []\n            if self._get_attr(aov, \"supportsLightGroups\"):\n                all_light_groups = self._get_attr(aov, \"allLightGroups\")\n                if all_light_groups:\n                    # All light groups is enabled\n                    light_groups = self._get_redshift_light_groups()\n                else:\n                    value = self._get_attr(aov, \"lightGroupList\")\n                    # note: string value can return None when never set\n                    if value:\n                        selected_light_groups = value.strip().split()\n                        light_groups = selected_light_groups\n\n                for light_group in light_groups:\n                    aov_light_group_name = \"{}_{}\".format(aov_name,\n                                                          light_group)\n                    for camera in cameras:\n                        product = RenderProduct(\n                            productName=aov_light_group_name,\n                            aov=aov_name,\n                            ext=ext,\n                            multipart=False,\n                            camera=camera,\n                            driver=aov,\n                            colorspace=colorspace)\n                        products.append(product)\n\n            if light_groups:\n                light_groups_enabled = True\n\n            # Redshift AOV Light Select always renders the global AOV\n            # even when light groups are present so we don't need to\n            # exclude it when light groups are active\n            for camera in cameras:\n                product = RenderProduct(productName=aov_name,\n                                        aov=aov_name,\n                                        ext=ext,\n                                        multipart=False,\n                                        camera=camera,\n                                        driver=aov,\n                                        colorspace=colorspace)\n                products.append(product)\n\n        # When a Beauty AOV is added manually, it will be rendered as\n        # 'Beauty_other' in file name and \"standard\" beauty will have\n        # 'Beauty' in its name. When disabled, standard output will be\n        # without `Beauty`. Except when using light groups.\n        if light_groups_enabled:\n            return products\n\n        beauty_name = \"BeautyAux\" if has_beauty_aov else \"\"\n        for camera in cameras:\n            products.insert(0,\n                            RenderProduct(productName=beauty_name,\n                                          ext=ext,\n                                          multipart=self.multipart,\n                                          camera=camera,\n                                          colorspace=colorspace))\n\n        return products\n\n    @staticmethod\n    def _get_redshift_light_groups():\n        return sorted(mel.eval(\"redshiftAllAovLightGroups\"))\n\n\nclass RenderProductsRenderman(ARenderProducts):\n    \"\"\"Expected files for Renderman renderer.\n\n    Warning:\n        This is very rudimentary and needs more love and testing.\n    \"\"\"\n\n    renderer = \"renderman\"\n    unmerged_aovs = {\"PxrCryptomatte\"}\n\n    def get_multipart(self):\n        # Implemented as display specific in \"get_render_products\".\n        return False\n\n    def get_render_products(self):\n        \"\"\"Get all AOVs.\n\n        See Also:\n            :func:`ARenderProducts.get_render_products()`\n\n        \"\"\"\n        from rfm2.api.displays import get_displays  # noqa\n\n        colorspace = lib.get_color_management_output_transform()\n\n        cameras = [\n            self.sanitize_camera_name(c)\n            for c in self.get_renderable_cameras()\n        ]\n\n        if not cameras:\n            cameras = [\n                self.sanitize_camera_name(\n                    self.get_renderable_cameras()[0])\n            ]\n        products = []\n\n        # NOTE: This is guessing extensions from renderman display types.\n        #       Some of them are just framebuffers, d_texture format can be\n        #       set in display setting. We set those now to None, but it\n        #       should be handled more gracefully.\n        display_types = {\n            \"d_deepexr\": \"exr\",\n            \"d_it\": None,\n            \"d_null\": None,\n            \"d_openexr\": \"exr\",\n            \"d_png\": \"png\",\n            \"d_pointcloud\": \"ptc\",\n            \"d_targa\": \"tga\",\n            \"d_texture\": None,\n            \"d_tiff\": \"tif\"\n        }\n\n        displays = get_displays(override_dst=\"render\")[\"displays\"]\n        for name, display in displays.items():\n            enabled = display[\"params\"][\"enable\"][\"value\"]\n            if not enabled:\n                continue\n\n            # Skip display types not producing any file output.\n            # Is there a better way to do it?\n            if not display_types.get(display[\"driverNode\"][\"type\"]):\n                continue\n\n            has_cryptomatte = cmds.ls(type=self.unmerged_aovs)\n            matte_enabled = False\n            if has_cryptomatte:\n                for cryptomatte in has_cryptomatte:\n                    cryptomatte_aov = cryptomatte\n                    matte_name = \"cryptomatte\"\n                    rman_globals = cmds.listConnections(cryptomatte +\n                                                        \".message\")\n                    if rman_globals:\n                        matte_enabled = True\n\n            aov_name = name\n            if aov_name == \"rmanDefaultDisplay\":\n                aov_name = \"beauty\"\n\n            extensions = display_types.get(\n                display[\"driverNode\"][\"type\"], \"exr\")\n\n            for camera in cameras:\n                # Create render product and set it as multipart only on\n                # display types supporting it. In all other cases, Renderman\n                # will create separate output per channel.\n                if display[\"driverNode\"][\"type\"] in [\"d_openexr\", \"d_deepexr\", \"d_tiff\"]:  # noqa\n                    product = RenderProduct(\n                        productName=aov_name,\n                        ext=extensions,\n                        camera=camera,\n                        multipart=True,\n                        colorspace=colorspace\n                    )\n\n                    if has_cryptomatte and matte_enabled:\n                        cryptomatte = RenderProduct(\n                            productName=matte_name,\n                            aov=cryptomatte_aov,\n                            ext=extensions,\n                            camera=camera,\n                            multipart=True,\n                            colorspace=colorspace\n                        )\n                else:\n                    # this code should handle the case where no multipart\n                    # capable format is selected. But since it involves\n                    # shady logic to determine what channel become what\n                    # lets not do that as all productions will use exr anyway.\n                    \"\"\"\n                    for channel in display['params']['displayChannels']['value']:  # noqa\n                        product = RenderProduct(\n                            productName=\"{}_{}\".format(aov_name, channel),\n                            ext=extensions,\n                            camera=camera,\n                            multipart=False\n                        )\n                    \"\"\"\n                    raise UnsupportedImageFormatException(\n                        \"Only exr, deep exr and tiff formats are supported.\")\n\n                products.append(product)\n\n                if has_cryptomatte and matte_enabled:\n                    products.append(cryptomatte)\n\n        return products\n\n    def get_files(self, product):\n        \"\"\"Get expected files.\n\n        \"\"\"\n        files = super(RenderProductsRenderman, self).get_files(product)\n\n        layer_data = self.layer_data\n        new_files = []\n\n        resolved_image_dir = re.sub(\"<scene>\", layer_data.sceneName, RENDERMAN_IMAGE_DIR, flags=re.IGNORECASE)  # noqa: E501\n        resolved_image_dir = re.sub(\"<layer>\", layer_data.layerName, resolved_image_dir, flags=re.IGNORECASE)  # noqa: E501\n        for file in files:\n            new_file = \"{}/{}\".format(resolved_image_dir, file)\n            new_files.append(new_file)\n\n        return new_files\n\n\nclass RenderProductsMayaHardware(ARenderProducts):\n    \"\"\"Expected files for MayaHardware renderer.\"\"\"\n\n    renderer = \"mayahardware2\"\n\n    extensions = [\n        {\"label\": \"JPEG\", \"index\": 8, \"extension\": \"jpg\"},\n        {\"label\": \"PNG\", \"index\": 32, \"extension\": \"png\"},\n        {\"label\": \"EXR(exr)\", \"index\": 40, \"extension\": \"exr\"}\n    ]\n\n    def get_multipart(self):\n        # MayaHardware does not support multipart EXRs.\n        return False\n\n    def _get_extension(self, value):\n        result = None\n        if isinstance(value, int):\n            extensions = {\n                extension[\"index\"]: extension[\"extension\"]\n                for extension in self.extensions\n            }\n            try:\n                result = extensions[value]\n            except KeyError:\n                raise NotImplementedError(\n                    \"Could not find extension for {}\".format(value)\n                )\n\n        if isinstance(value, six.string_types):\n            extensions = {\n                extension[\"label\"]: extension[\"extension\"]\n                for extension in self.extensions\n            }\n            try:\n                result = extensions[value]\n            except KeyError:\n                raise NotImplementedError(\n                    \"Could not find extension for {}\".format(value)\n                )\n\n        if not result:\n            raise NotImplementedError(\n                \"Could not find extension for {}\".format(value)\n            )\n\n        return result\n\n    def get_render_products(self):\n        \"\"\"Get all AOVs.\n        See Also:\n            :func:`ARenderProducts.get_render_products()`\n        \"\"\"\n        ext = self._get_extension(\n            self._get_attr(\"defaultRenderGlobals.imageFormat\")\n        )\n\n        products = []\n        for cam in self.get_renderable_cameras():\n            product = RenderProduct(\n                productName=\"beauty\",\n                ext=ext,\n                camera=cam,\n                colorspace=lib.get_color_management_output_transform()\n            )\n            products.append(product)\n\n        return products\n\n\nclass AOVError(Exception):\n    \"\"\"Custom exception for determining AOVs.\"\"\"\n\n\nclass UnsupportedRendererException(Exception):\n    \"\"\"Custom exception.\n\n    Raised when requesting data from unsupported renderer.\n    \"\"\"\n\n\nclass UnsupportedImageFormatException(Exception):\n    \"\"\"Custom exception to report unsupported output image format.\"\"\"\n"
  },
  {
    "path": "openpype/hosts/maya/api/lib_rendersettings.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Class for handling Render Settings.\"\"\"\nimport six\nimport sys\n\nfrom openpype.lib import Logger\nfrom openpype.settings import get_project_settings\n\nfrom openpype.pipeline import CreatorError, get_current_project_name\nfrom openpype.pipeline.context_tools import get_current_project_asset\nfrom openpype.hosts.maya.api.lib import reset_frame_range\n\n\nclass RenderSettings(object):\n\n    _image_prefix_nodes = {\n        'vray': 'vraySettings.fileNamePrefix',\n        'arnold': 'defaultRenderGlobals.imageFilePrefix',\n        'renderman': 'rmanGlobals.imageFileFormat',\n        'redshift': 'defaultRenderGlobals.imageFilePrefix',\n        'mayahardware2': 'defaultRenderGlobals.imageFilePrefix'\n    }\n\n    _aov_chars = {\n        \"dot\": \".\",\n        \"dash\": \"-\",\n        \"underscore\": \"_\"\n    }\n\n    log = Logger.get_logger(\"RenderSettings\")\n\n    @classmethod\n    def get_image_prefix_attr(cls, renderer):\n        return cls._image_prefix_nodes[renderer]\n\n    @staticmethod\n    def get_padding_attr(renderer):\n        \"\"\"Return attribute for renderer that defines frame padding amount\"\"\"\n        if renderer == \"vray\":\n            return \"vraySettings.fileNamePadding\"\n        else:\n            return \"defaultRenderGlobals.extensionPadding\"\n\n    def __init__(self, project_settings=None):\n        if not project_settings:\n            project_settings = get_project_settings(\n                get_current_project_name()\n            )\n        render_settings = project_settings[\"maya\"][\"RenderSettings\"]\n        image_prefixes = {\n            \"vray\": render_settings[\"vray_renderer\"][\"image_prefix\"],\n            \"arnold\": render_settings[\"arnold_renderer\"][\"image_prefix\"],\n            \"renderman\": render_settings[\"renderman_renderer\"][\"image_prefix\"],\n            \"redshift\": render_settings[\"redshift_renderer\"][\"image_prefix\"]\n        }\n\n        # TODO probably should be stored to more explicit attribute\n        # Renderman only\n        renderman_settings = render_settings[\"renderman_renderer\"]\n        _image_dir = {\n            \"renderman\": renderman_settings[\"image_dir\"],\n            \"cryptomatte\": renderman_settings[\"cryptomatte_dir\"],\n            \"imageDisplay\": renderman_settings[\"imageDisplay_dir\"],\n            \"watermark\": renderman_settings[\"watermark_dir\"]\n        }\n        self._image_prefixes = image_prefixes\n        self._image_dir = _image_dir\n        self._project_settings = project_settings\n\n    def set_default_renderer_settings(self, renderer=None):\n        \"\"\"Set basic settings based on renderer.\"\"\"\n        # Not all hosts can import this module.\n        from maya import cmds  # noqa: F401\n        import maya.mel as mel  # noqa: F401\n\n        if not renderer:\n            renderer = cmds.getAttr(\n                'defaultRenderGlobals.currentRenderer').lower()\n\n        asset_doc = get_current_project_asset()\n        # project_settings/maya/create/CreateRender/aov_separator\n        try:\n            aov_separator = self._aov_chars[(\n                self._project_settings[\"maya\"]\n                                      [\"RenderSettings\"]\n                                      [\"aov_separator\"]\n            )]\n        except KeyError:\n            aov_separator = \"_\"\n        reset_frame = self._project_settings[\"maya\"][\"RenderSettings\"][\"reset_current_frame\"] # noqa\n\n        if reset_frame:\n            start_frame = cmds.getAttr(\"defaultRenderGlobals.startFrame\")\n            cmds.currentTime(start_frame, edit=True)\n\n        if renderer in self._image_prefix_nodes:\n            prefix = self._image_prefixes[renderer]\n            prefix = prefix.replace(\"{aov_separator}\", aov_separator)\n            cmds.setAttr(self._image_prefix_nodes[renderer],\n                        prefix, type=\"string\")  # noqa\n        else:\n            print(\"{0} isn't a supported renderer to autoset settings.\".format(renderer)) # noqa\n        # TODO: handle not having res values in the doc\n        width = asset_doc[\"data\"].get(\"resolutionWidth\")\n        height = asset_doc[\"data\"].get(\"resolutionHeight\")\n\n        if renderer == \"arnold\":\n            # set renderer settings for Arnold from project settings\n            self._set_arnold_settings(width, height)\n\n        if renderer == \"vray\":\n            self._set_vray_settings(aov_separator, width, height)\n\n        if renderer == \"redshift\":\n            self._set_redshift_settings(width, height)\n            mel.eval(\"redshiftUpdateActiveAovList\")\n\n        if renderer == \"renderman\":\n            image_dir = self._image_dir[\"renderman\"]\n            cmds.setAttr(\"rmanGlobals.imageOutputDir\",\n                         image_dir, type=\"string\")\n            self._set_renderman_settings(width, height,\n                                         aov_separator)\n\n    def _set_arnold_settings(self, width, height):\n        \"\"\"Sets settings for Arnold.\"\"\"\n        from mtoa.core import createOptions  # noqa\n        from mtoa.aovs import AOVInterface  # noqa\n        # Not all hosts can import this module.\n        from maya import cmds  # noqa: F401\n        import maya.mel as mel  # noqa: F401\n\n        createOptions()\n        render_settings = self._project_settings[\"maya\"][\"RenderSettings\"]\n        arnold_render_presets = render_settings[\"arnold_renderer\"] # noqa\n        # Force resetting settings and AOV list to avoid having to deal with\n        # AOV checking logic, for now.\n        # This is a work around because the standard\n        # function to revert render settings does not reset AOVs list in MtoA\n        # Fetch current aovs in case there's any.\n        current_aovs = AOVInterface().getAOVs()\n        remove_aovs = render_settings[\"remove_aovs\"]\n        if remove_aovs:\n        # Remove fetched AOVs\n            AOVInterface().removeAOVs(current_aovs)\n        mel.eval(\"unifiedRenderGlobalsRevertToDefault\")\n        img_ext = arnold_render_presets[\"image_format\"]\n        img_prefix = arnold_render_presets[\"image_prefix\"]\n        aovs = arnold_render_presets[\"aov_list\"]\n        img_tiled = arnold_render_presets[\"tiled\"]\n        multi_exr = arnold_render_presets[\"multilayer_exr\"]\n        additional_options = arnold_render_presets[\"additional_options\"]\n        for aov in aovs:\n            if aov in current_aovs and not remove_aovs:\n                continue\n            AOVInterface('defaultArnoldRenderOptions').addAOV(aov)\n\n        cmds.setAttr(\"defaultResolution.width\", width)\n        cmds.setAttr(\"defaultResolution.height\", height)\n\n        self._set_global_output_settings()\n\n        cmds.setAttr(\n            \"defaultRenderGlobals.imageFilePrefix\", img_prefix, type=\"string\")\n\n        cmds.setAttr(\n            \"defaultArnoldDriver.ai_translator\", img_ext, type=\"string\")\n\n        cmds.setAttr(\n            \"defaultArnoldDriver.exrTiled\", img_tiled)\n\n        cmds.setAttr(\n            \"defaultArnoldDriver.mergeAOVs\", multi_exr)\n        self._additional_attribs_setter(additional_options)\n        reset_frame_range(playback=False, fps=False, render=True)\n\n    def _set_redshift_settings(self, width, height):\n        \"\"\"Sets settings for Redshift.\"\"\"\n        # Not all hosts can import this module.\n        from maya import cmds  # noqa: F401\n        import maya.mel as mel  # noqa: F401\n\n        render_settings = self._project_settings[\"maya\"][\"RenderSettings\"]\n        redshift_render_presets = render_settings[\"redshift_renderer\"]\n\n        remove_aovs = render_settings[\"remove_aovs\"]\n        all_rs_aovs = cmds.ls(type='RedshiftAOV')\n        if remove_aovs:\n            for aov in all_rs_aovs:\n                enabled = cmds.getAttr(\"{}.enabled\".format(aov))\n                if enabled:\n                    cmds.delete(aov)\n\n        redshift_aovs = redshift_render_presets[\"aov_list\"]\n        # list all the aovs\n        all_rs_aovs = cmds.ls(type='RedshiftAOV')\n        for rs_aov in redshift_aovs:\n            rs_layername = \"rsAov_{}\".format(rs_aov.replace(\" \", \"\"))\n            if rs_layername in all_rs_aovs:\n                continue\n            cmds.rsCreateAov(type=rs_aov)\n        # update the AOV list\n        mel.eval(\"redshiftUpdateActiveAovList\")\n\n        rs_p_engine = redshift_render_presets[\"primary_gi_engine\"]\n        rs_s_engine = redshift_render_presets[\"secondary_gi_engine\"]\n\n        if int(rs_p_engine) or int(rs_s_engine) != 0:\n            cmds.setAttr(\"redshiftOptions.GIEnabled\", 1)\n            if int(rs_p_engine) == 0:\n                # reset the primary GI Engine as default\n                cmds.setAttr(\"redshiftOptions.primaryGIEngine\", 4)\n            if int(rs_s_engine) == 0:\n                # reset the secondary GI Engine as default\n                cmds.setAttr(\"redshiftOptions.secondaryGIEngine\", 2)\n        else:\n            cmds.setAttr(\"redshiftOptions.GIEnabled\", 0)\n\n        cmds.setAttr(\"redshiftOptions.primaryGIEngine\", int(rs_p_engine))\n        cmds.setAttr(\"redshiftOptions.secondaryGIEngine\", int(rs_s_engine))\n\n        additional_options = redshift_render_presets[\"additional_options\"]\n        ext = redshift_render_presets[\"image_format\"]\n        img_exts = [\"iff\", \"exr\", \"tif\", \"png\", \"tga\", \"jpg\"]\n        img_ext = img_exts.index(ext)\n\n        self._set_global_output_settings()\n        cmds.setAttr(\"redshiftOptions.imageFormat\", img_ext)\n        cmds.setAttr(\"defaultResolution.width\", width)\n        cmds.setAttr(\"defaultResolution.height\", height)\n        self._additional_attribs_setter(additional_options)\n\n    def _set_renderman_settings(self, width, height, aov_separator):\n        \"\"\"Sets settings for Renderman\"\"\"\n        # Not all hosts can import this module.\n        from maya import cmds  # noqa: F401\n        import maya.mel as mel  # noqa: F401\n\n        rman_render_presets = (\n            self._project_settings\n            [\"maya\"]\n            [\"RenderSettings\"]\n            [\"renderman_renderer\"]\n        )\n        display_filters = rman_render_presets[\"display_filters\"]\n        d_filters_number = len(display_filters)\n        for i in range(d_filters_number):\n            d_node = cmds.ls(typ=display_filters[i])\n            if len(d_node) > 0:\n                filter_nodes = d_node[0]\n            else:\n                filter_nodes = cmds.createNode(display_filters[i])\n\n            cmds.connectAttr(filter_nodes + \".message\",\n                             \"rmanGlobals.displayFilters[%i]\" % i,\n                             force=True)\n            if filter_nodes.startswith(\"PxrImageDisplayFilter\"):\n                imageDisplay_dir = self._image_dir[\"imageDisplay\"]\n                imageDisplay_dir = imageDisplay_dir.replace(\"{aov_separator}\",\n                                                            aov_separator)\n                cmds.setAttr(filter_nodes + \".filename\",\n                             imageDisplay_dir, type=\"string\")\n\n        sample_filters = rman_render_presets[\"sample_filters\"]\n        s_filters_number = len(sample_filters)\n        for n in range(s_filters_number):\n            s_node = cmds.ls(typ=sample_filters[n])\n            if len(s_node) > 0:\n                filter_nodes = s_node[0]\n            else:\n                filter_nodes = cmds.createNode(sample_filters[n])\n\n            cmds.connectAttr(filter_nodes + \".message\",\n                             \"rmanGlobals.sampleFilters[%i]\" % n,\n                             force=True)\n\n            if filter_nodes.startswith(\"PxrCryptomatte\"):\n                matte_dir = self._image_dir[\"cryptomatte\"]\n                matte_dir = matte_dir.replace(\"{aov_separator}\",\n                                              aov_separator)\n                cmds.setAttr(filter_nodes + \".filename\",\n                             matte_dir, type=\"string\")\n            elif filter_nodes.startswith(\"PxrWatermarkFilter\"):\n                watermark_dir = self._image_dir[\"watermark\"]\n                watermark_dir = watermark_dir.replace(\"{aov_separator}\",\n                                                      aov_separator)\n                cmds.setAttr(filter_nodes + \".filename\",\n                             watermark_dir, type=\"string\")\n\n        additional_options = rman_render_presets[\"additional_options\"]\n\n        self._set_global_output_settings()\n        cmds.setAttr(\"defaultResolution.width\", width)\n        cmds.setAttr(\"defaultResolution.height\", height)\n        self._additional_attribs_setter(additional_options)\n\n    def _set_vray_settings(self, aov_separator, width, height):\n        # type: (str, int, int) -> None\n        \"\"\"Sets important settings for Vray.\"\"\"\n        # Not all hosts can import this module.\n        from maya import cmds  # noqa: F401\n        import maya.mel as mel  # noqa: F401\n\n\n        settings = cmds.ls(type=\"VRaySettingsNode\")\n        node = settings[0] if settings else cmds.createNode(\"VRaySettingsNode\")\n        render_settings = self._project_settings[\"maya\"][\"RenderSettings\"]\n        vray_render_presets = render_settings[\"vray_renderer\"]\n        # vrayRenderElement\n        remove_aovs = render_settings[\"remove_aovs\"]\n        all_vray_aovs = cmds.ls(type='VRayRenderElement')\n        lightSelect_aovs = cmds.ls(type='VRayRenderElementSet')\n        if remove_aovs:\n            for aov in all_vray_aovs:\n                # remove all aovs except LightSelect\n                enabled = cmds.getAttr(\"{}.enabled\".format(aov))\n                if enabled:\n                    cmds.delete(aov)\n            # remove LightSelect\n            for light_aovs in lightSelect_aovs:\n                light_enabled = cmds.getAttr(\"{}.enabled\".format(light_aovs))\n                if light_enabled:\n                    cmds.delete(lightSelect_aovs)\n\n        vray_aovs = vray_render_presets[\"aov_list\"]\n        for renderlayer in vray_aovs:\n            renderElement = \"vrayAddRenderElement {}\".format(renderlayer)\n            RE_name = mel.eval(renderElement)\n            # if there is more than one same render element\n            if RE_name.endswith(\"1\"):\n                cmds.delete(RE_name)\n        # Set aov separator\n        # First we need to explicitly set the UI items in Render Settings\n        # because that is also what V-Ray updates to when that Render Settings\n        # UI did initialize before and refreshes again.\n        MENU = \"vrayRenderElementSeparator\"\n        if cmds.optionMenuGrp(MENU, query=True, exists=True):\n            items = cmds.optionMenuGrp(MENU, query=True, ill=True)\n            separators = [cmds.menuItem(i, query=True, label=True) for i in items]  # noqa: E501\n            try:\n                sep_idx = separators.index(aov_separator)\n            except ValueError:\n                six.reraise(\n                    CreatorError,\n                    CreatorError(\n                        \"AOV character {} not in {}\".format(\n                            aov_separator, separators)),\n                    sys.exc_info()[2])\n\n            cmds.optionMenuGrp(MENU, edit=True, select=sep_idx + 1)\n\n        # Set the render element attribute as string. This is also what V-Ray\n        # sets whenever the `vrayRenderElementSeparator` menu items switch\n        cmds.setAttr(\n            \"{}.fileNameRenderElementSeparator\".format(node),\n            aov_separator,\n            type=\"string\"\n        )\n\n        # Set render file format to exr\n        ext = vray_render_presets[\"image_format\"]\n        cmds.setAttr(\"{}.imageFormatStr\".format(node), ext, type=\"string\")\n\n        # animType\n        cmds.setAttr(\"{}.animType\".format(node), 1)\n\n        # resolution\n        cmds.setAttr(\"{}.width\".format(node), width)\n        cmds.setAttr(\"{}.height\".format(node), height)\n\n        additional_options = vray_render_presets[\"additional_options\"]\n\n        self._additional_attribs_setter(additional_options)\n\n    @staticmethod\n    def _set_global_output_settings():\n        # Not all hosts can import this module.\n        from maya import cmds  # noqa: F401\n        import maya.mel as mel  # noqa: F401\n\n        # enable animation\n        cmds.setAttr(\"defaultRenderGlobals.outFormatControl\", 0)\n        cmds.setAttr(\"defaultRenderGlobals.animation\", 1)\n        cmds.setAttr(\"defaultRenderGlobals.putFrameBeforeExt\", 1)\n        cmds.setAttr(\"defaultRenderGlobals.extensionPadding\", 4)\n\n    def _additional_attribs_setter(self, additional_attribs):\n        # Not all hosts can import this module.\n        from maya import cmds  # noqa: F401\n        import maya.mel as mel  # noqa: F401\n\n        for item in additional_attribs:\n            attribute, value = item\n            attribute = str(attribute)  # ensure str conversion from settings\n            attribute_type = cmds.getAttr(attribute, type=True)\n            if attribute_type in {\"long\", \"bool\"}:\n                cmds.setAttr(attribute, int(value))\n            elif attribute_type == \"string\":\n                cmds.setAttr(attribute, str(value), type=\"string\")\n            elif attribute_type in {\"double\", \"doubleAngle\", \"doubleLinear\"}:\n                cmds.setAttr(attribute, float(value))\n            else:\n                self.log.error(\n                    \"Attribute {attribute} can not be set due to unsupported \"\n                    \"type: {attribute_type}\".format(\n                        attribute=attribute,\n                        attribute_type=attribute_type)\n                )\n"
  },
  {
    "path": "openpype/hosts/maya/api/lib_rendersetup.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Code to get attributes from render layer without switching to it.\n\nhttps://github.com/Colorbleed/colorbleed-config/blob/acre/colorbleed/maya/lib_rendersetup.py\nCredits: Roy Nieterau (BigRoy) / Colorbleed\nModified for use in OpenPype\n\n\"\"\"\n\nfrom maya import cmds\nimport maya.api.OpenMaya as om\nimport logging\n\nimport maya.app.renderSetup.model.utils as utils\nfrom maya.app.renderSetup.model import renderSetup\nfrom maya.app.renderSetup.model.override import (\n    AbsOverride,\n    RelOverride,\n    UniqueOverride\n)\n\nfrom openpype.hosts.maya.api.lib import get_attribute\n\nEXACT_MATCH = 0\nPARENT_MATCH = 1\nCLIENT_MATCH = 2\n\nDEFAULT_RENDER_LAYER = \"defaultRenderLayer\"\n\nlog = logging.getLogger(__name__)\n\n\ndef get_rendersetup_layer(layer):\n    \"\"\"Return render setup layer name.\n\n    This also converts names from legacy renderLayer node name to render setup\n    name.\n\n    Note: `defaultRenderLayer` is not a renderSetupLayer node but it is however\n          the valid layer name for Render Setup - so we return that as is.\n\n    Example:\n        >>> for legacy_layer in cmds.ls(type=\"renderLayer\"):\n        >>>    layer = get_rendersetup_layer(legacy_layer)\n\n    Returns:\n        str or None: Returns renderSetupLayer node name if `layer` is a valid\n            layer name in legacy renderlayers or render setup layers.\n            Returns None if the layer can't be found or Render Setup is\n            currently disabled.\n\n\n    \"\"\"\n    if layer == DEFAULT_RENDER_LAYER:\n        # defaultRenderLayer doesn't have a `renderSetupLayer`\n        return layer\n\n    if not cmds.mayaHasRenderSetup():\n        return None\n\n    if not cmds.objExists(layer):\n        return None\n\n    if cmds.nodeType(layer) == \"renderSetupLayer\":\n        return layer\n\n    # By default Render Setup renames the legacy renderlayer\n    # to `rs_<layername>` but lets not rely on that as the\n    # layer node can be renamed manually\n    connections = cmds.listConnections(layer + \".message\",\n                                       type=\"renderSetupLayer\",\n                                       exactType=True,\n                                       source=False,\n                                       destination=True,\n                                       plugs=True) or []\n    return next((conn.split(\".\", 1)[0] for conn in connections\n                 if conn.endswith(\".legacyRenderLayer\")), None)\n\n\ndef get_attr_in_layer(node_attr, layer):\n    \"\"\"Return attribute value in Render Setup layer.\n\n    This will only work for attributes which can be\n    retrieved with `maya.cmds.getAttr` and for which\n    Relative and Absolute overrides are applicable.\n\n    Examples:\n        >>> get_attr_in_layer(\"defaultResolution.width\", layer=\"layer1\")\n        >>> get_attr_in_layer(\"defaultRenderGlobals.startFrame\", layer=\"layer\")\n        >>> get_attr_in_layer(\"transform.translate\", layer=\"layer3\")\n\n    Args:\n        attr (str): attribute name as 'node.attribute'\n        layer (str): layer name\n\n    Returns:\n        object: attribute value in layer\n\n    \"\"\"\n\n    def _layer_needs_update(layer):\n        \"\"\"Return whether layer needs updating.\"\"\"\n        # Use `getattr` as e.g. DEFAULT_RENDER_LAYER does not have\n        # the attribute\n        return getattr(layer, \"needsMembershipUpdate\", False) or \\\n            getattr(layer, \"needsApplyUpdate\", False)\n\n    def get_default_layer_value(node_attr_):\n        \"\"\"Return attribute value in `DEFAULT_RENDER_LAYER`.\"\"\"\n        inputs = cmds.listConnections(node_attr_,\n                                      source=True,\n                                      destination=False,\n                                      # We want to skip conversion nodes since\n                                      # an override to `endFrame` could have\n                                      # a `unitToTimeConversion` node\n                                      # in-between\n                                      skipConversionNodes=True,\n                                      type=\"applyOverride\") or []\n        if inputs:\n            override = inputs[0]\n            history_overrides = cmds.ls(cmds.listHistory(override,\n                                                         pruneDagObjects=True),\n                                        type=\"applyOverride\")\n            node = history_overrides[-1] if history_overrides else override\n            node_attr_ = node + \".original\"\n\n        return get_attribute(node_attr_, asString=True)\n\n    layer = get_rendersetup_layer(layer)\n    rs = renderSetup.instance()\n    current_layer = rs.getVisibleRenderLayer()\n    if current_layer.name() == layer:\n\n        # Ensure layer is up-to-date\n        if _layer_needs_update(current_layer):\n            try:\n                rs.switchToLayer(current_layer)\n            except RuntimeError:\n                # Some cases can cause errors on switching\n                # the first time with Render Setup layers\n                # e.g. different overrides to compounds\n                # and its children plugs. So we just force\n                # it another time. If it then still fails\n                # we will let it error out.\n                rs.switchToLayer(current_layer)\n\n        return get_attribute(node_attr, asString=True)\n\n    overrides = get_attr_overrides(node_attr, layer)\n    default_layer_value = get_default_layer_value(node_attr)\n    if not overrides:\n        return default_layer_value\n\n    value = default_layer_value\n    for match, layer_override, index in overrides:\n        if isinstance(layer_override, AbsOverride):\n            # Absolute override\n            value = get_attribute(layer_override.name() + \".attrValue\")\n            if match == EXACT_MATCH:\n                # value = value\n                pass\n            elif match == PARENT_MATCH:\n                value = value[index]\n            elif match == CLIENT_MATCH:\n                value[index] = value\n\n        elif isinstance(layer_override, RelOverride):\n            # Relative override\n            # Value = Original * Multiply + Offset\n            multiply = get_attribute(layer_override.name() + \".multiply\")\n            offset = get_attribute(layer_override.name() + \".offset\")\n\n            if match == EXACT_MATCH:\n                value = value * multiply + offset\n            elif match == PARENT_MATCH:\n                value = value * multiply[index] + offset[index]\n            elif match == CLIENT_MATCH:\n                value[index] = value[index] * multiply + offset\n\n        else:\n            raise TypeError(\"Unsupported override: %s\" % layer_override)\n\n    return value\n\n\ndef get_attr_overrides(node_attr, layer,\n                       skip_disabled=True,\n                       skip_local_render=True,\n                       stop_at_absolute_override=True):\n    \"\"\"Return all Overrides applicable to the attribute.\n\n    Overrides are returned as a 3-tuple:\n        (Match, Override, Index)\n\n    Match:\n        This is any of EXACT_MATCH, PARENT_MATCH, CLIENT_MATCH\n        and defines whether the override is exactly on the\n        plug, on the parent or on a child plug.\n\n    Override:\n        This is the RenderSetup Override instance.\n\n    Index:\n        This is the Plug index under the parent or for\n        the child that matches. The EXACT_MATCH index will\n        always be None. For PARENT_MATCH the index is which\n        index the plug is under the parent plug. For CLIENT_MATCH\n        the index is which child index matches the plug.\n\n    Args:\n        node_attr (str): attribute name as 'node.attribute'\n        layer (str): layer name\n        skip_disabled (bool): exclude disabled overrides\n        skip_local_render (bool): exclude overrides marked\n            as local render.\n        stop_at_absolute_override: exclude overrides prior\n            to the last absolute override as they have\n            no influence on the resulting value.\n\n    Returns:\n        list: Ordered Overrides in order of strength\n\n    \"\"\"\n\n    def get_mplug_children(plug):\n        \"\"\"Return children MPlugs of compound `MPlug`.\"\"\"\n        children = []\n        if plug.isCompound:\n            for i in range(plug.numChildren()):\n                children.append(plug.child(i))\n        return children\n\n    def get_mplug_names(mplug):\n        \"\"\"Return long and short name of `MPlug`.\"\"\"\n        long_name = mplug.partialName(useLongNames=True)\n        short_name = mplug.partialName(useLongNames=False)\n        return {long_name, short_name}\n\n    def iter_override_targets(override):\n        try:\n            for target in override._targets():\n                yield target\n        except AssertionError:\n            # Workaround: There is a bug where the private `_targets()` method\n            #             fails on some attribute plugs. For example overrides\n            #             to the defaultRenderGlobals.endFrame\n            #             (Tested in Maya 2020.2)\n            log.debug(\"Workaround for %s\" % override)\n            from maya.app.renderSetup.common.utils import findPlug\n\n            attr = override.attributeName()\n            if isinstance(override, UniqueOverride):\n                node = override.targetNodeName()\n                yield findPlug(node, attr)\n            else:\n                nodes = override.parent().selector().nodes()\n                for node in nodes:\n                    if cmds.attributeQuery(attr, node=node, exists=True):\n                        yield findPlug(node, attr)\n\n    # Get the MPlug for the node.attr\n    sel = om.MSelectionList()\n    sel.add(node_attr)\n    plug = sel.getPlug(0)\n\n    layer = get_rendersetup_layer(layer)\n    if layer == DEFAULT_RENDER_LAYER:\n        # DEFAULT_RENDER_LAYER will never have overrides\n        # since it's the default layer\n        return []\n\n    rs_layer = renderSetup.instance().getRenderLayer(layer)\n    if rs_layer is None:\n        # Renderlayer does not exist\n        return\n\n    # Get any parent or children plugs as we also\n    # want to include them in the attribute match\n    # for overrides\n    parent = plug.parent() if plug.isChild else None\n    parent_index = None\n    if parent:\n        parent_index = get_mplug_children(parent).index(plug)\n\n    children = get_mplug_children(plug)\n\n    # Create lookup for the attribute by both long\n    # and short names\n    attr_names = get_mplug_names(plug)\n    for child in children:\n        attr_names.update(get_mplug_names(child))\n    if parent:\n        attr_names.update(get_mplug_names(parent))\n\n        # Get all overrides of the layer\n    # And find those that are relevant to the attribute\n    plug_overrides = []\n\n    # Iterate over the overrides in reverse so we get the last\n    # overrides first and can \"break\" whenever an absolute\n    # override is reached\n    layer_overrides = list(utils.getOverridesRecursive(rs_layer))\n    for layer_override in reversed(layer_overrides):\n\n        if skip_disabled and not layer_override.isEnabled():\n            # Ignore disabled overrides\n            continue\n\n        if skip_local_render and layer_override.isLocalRender():\n            continue\n\n        # The targets list can be very large so we'll do\n        # a quick filter by attribute name to detect whether\n        # it matches the attribute name, or its parent or child\n        if layer_override.attributeName() not in attr_names:\n            continue\n\n        override_match = None\n        for override_plug in iter_override_targets(layer_override):\n\n            override_match = None\n            if plug == override_plug:\n                override_match = (EXACT_MATCH, layer_override, None)\n\n            elif parent and override_plug == parent:\n                override_match = (PARENT_MATCH, layer_override, parent_index)\n\n            elif children and override_plug in children:\n                child_index = children.index(override_plug)\n                override_match = (CLIENT_MATCH, layer_override, child_index)\n\n            if override_match:\n                plug_overrides.append(override_match)\n                break\n\n        if (\n                override_match and\n                stop_at_absolute_override and\n                isinstance(layer_override, AbsOverride) and\n                # When the override is only on a child plug then it doesn't\n                # override the entire value so we not stop at this override\n                not override_match[0] == CLIENT_MATCH\n        ):\n            # If override is absolute override, then BREAK out\n            # of parent loop we don't need to look any further as\n            # this is the absolute override\n            break\n\n    return reversed(plug_overrides)\n\n\ndef get_shader_in_layer(node, layer):\n    \"\"\"Return the assigned shader in a renderlayer without switching layers.\n\n    This has been developed and tested for Legacy Renderlayers and *not* for\n    Render Setup.\n\n    Note: This will also return the shader for any face assignments, however\n        it will *not* return the components they are assigned to. This could\n        be implemented, but since Maya's renderlayers are famous for breaking\n        with face assignments there has been no need for this function to\n        support that.\n\n    Returns:\n        list: The list of assigned shaders in the given layer.\n\n    \"\"\"\n\n    def _get_connected_shader(plug):\n        \"\"\"Return current shader\"\"\"\n        return cmds.listConnections(plug,\n                                    source=False,\n                                    destination=True,\n                                    plugs=False,\n                                    connections=False,\n                                    type=\"shadingEngine\") or []\n\n    # We check the instObjGroups (shader connection) for layer overrides.\n    plug = node + \".instObjGroups\"\n\n    # Ignore complex query if we're in the layer anyway (optimization)\n    current_layer = cmds.editRenderLayerGlobals(query=True,\n                                                currentRenderLayer=True)\n    if layer == current_layer:\n        return _get_connected_shader(plug)\n\n    connections = cmds.listConnections(plug,\n                                       plugs=True,\n                                       source=False,\n                                       destination=True,\n                                       type=\"renderLayer\") or []\n    connections = filter(lambda x: x.endswith(\".outPlug\"), connections)\n    if not connections:\n        # If no overrides anywhere on the shader, just get the current shader\n        return _get_connected_shader(plug)\n\n    def _get_override(connections, layer):\n        \"\"\"Return the overridden connection for that layer in connections\"\"\"\n        # If there's an override on that layer, return that.\n        for connection in connections:\n            if (connection.startswith(layer + \".outAdjustments\") and\n                    connection.endswith(\".outPlug\")):\n\n                # This is a shader override on that layer so get the shader\n                # connected to .outValue of the .outAdjustment[i]\n                out_adjustment = connection.rsplit(\".\", 1)[0]\n                connection_attr = out_adjustment + \".outValue\"\n                override = cmds.listConnections(connection_attr) or []\n\n                return override\n\n    override_shader = _get_override(connections, layer)\n    if override_shader is not None:\n        return override_shader\n    else:\n        # Get the override for \"defaultRenderLayer\" (=masterLayer)\n        return _get_override(connections, layer=\"defaultRenderLayer\")\n"
  },
  {
    "path": "openpype/hosts/maya/api/menu.py",
    "content": "import os\nimport logging\nfrom functools import partial\n\nfrom qtpy import QtWidgets, QtGui\n\nimport maya.utils\nimport maya.cmds as cmds\n\nfrom openpype.pipeline import (\n    get_current_asset_name,\n    get_current_task_name\n)\nfrom openpype.pipeline.workfile import BuildWorkfile\nfrom openpype.tools.utils import host_tools\nfrom openpype.hosts.maya.api import lib, lib_rendersettings\nfrom .lib import get_main_window, IS_HEADLESS\nfrom ..tools import show_look_assigner\n\nfrom .workfile_template_builder import (\n    create_placeholder,\n    update_placeholder,\n    build_workfile_template,\n    update_workfile_template,\n)\n\nlog = logging.getLogger(__name__)\n\nMENU_NAME = \"op_maya_menu\"\n\n\ndef _get_menu(menu_name=None):\n    \"\"\"Return the menu instance if it currently exists in Maya\"\"\"\n    if menu_name is None:\n        menu_name = MENU_NAME\n\n    widgets = {w.objectName(): w for w in QtWidgets.QApplication.allWidgets()}\n    return widgets.get(menu_name)\n\n\ndef get_context_label():\n    return \"{}, {}\".format(\n        get_current_asset_name(),\n        get_current_task_name()\n    )\n\n\ndef install(project_settings):\n    if cmds.about(batch=True):\n        log.info(\"Skipping openpype.menu initialization in batch mode..\")\n        return\n\n    def add_menu():\n        pyblish_icon = host_tools.get_pyblish_icon()\n        parent_widget = get_main_window()\n        cmds.menu(\n            MENU_NAME,\n            label=os.environ.get(\"AVALON_LABEL\") or \"OpenPype\",\n            tearOff=True,\n            parent=\"MayaWindow\"\n        )\n\n        # Create context menu\n        cmds.menuItem(\n            \"currentContext\",\n            label=get_context_label(),\n            parent=MENU_NAME,\n            enable=False\n        )\n\n        cmds.setParent(\"..\", menu=True)\n\n        cmds.menuItem(divider=True)\n\n        cmds.menuItem(\n            \"Create...\",\n            command=lambda *args: host_tools.show_publisher(\n                parent=parent_widget,\n                tab=\"create\"\n            )\n        )\n\n        cmds.menuItem(\n            \"Load...\",\n            command=lambda *args: host_tools.show_loader(\n                parent=parent_widget,\n                use_context=True\n            )\n        )\n\n        cmds.menuItem(\n            \"Publish...\",\n            command=lambda *args: host_tools.show_publisher(\n                parent=parent_widget,\n                tab=\"publish\"\n            ),\n            image=pyblish_icon\n        )\n\n        cmds.menuItem(\n            \"Manage...\",\n            command=lambda *args: host_tools.show_scene_inventory(\n                parent=parent_widget\n            )\n        )\n\n        cmds.menuItem(\n            \"Library...\",\n            command=lambda *args: host_tools.show_library_loader(\n                parent=parent_widget\n            )\n        )\n\n        cmds.menuItem(divider=True)\n\n        cmds.menuItem(\n            \"Work Files...\",\n            command=lambda *args: host_tools.show_workfiles(\n                parent=parent_widget\n            ),\n        )\n\n        cmds.menuItem(\n            \"Set Frame Range\",\n            command=lambda *args: lib.reset_frame_range()\n        )\n\n        cmds.menuItem(\n            \"Set Resolution\",\n            command=lambda *args: lib.reset_scene_resolution()\n        )\n\n        cmds.menuItem(\n            \"Set Colorspace\",\n            command=lambda *args: lib.set_colorspace(),\n        )\n\n        cmds.menuItem(\n            \"Set Render Settings\",\n            command=lambda *args: lib_rendersettings.RenderSettings().set_default_renderer_settings()    # noqa\n        )\n\n        cmds.menuItem(divider=True, parent=MENU_NAME)\n        cmds.menuItem(\n            \"Build First Workfile\",\n            parent=MENU_NAME,\n            command=lambda *args: BuildWorkfile().process()\n        )\n\n        cmds.menuItem(\n            \"Look assigner...\",\n            command=lambda *args: show_look_assigner(\n                parent_widget\n            )\n        )\n\n        cmds.menuItem(\n            \"Experimental tools...\",\n            command=lambda *args: host_tools.show_experimental_tools_dialog(\n                parent_widget\n            )\n        )\n\n        builder_menu = cmds.menuItem(\n            \"Template Builder\",\n            subMenu=True,\n            tearOff=True,\n            parent=MENU_NAME\n        )\n        cmds.menuItem(\n            \"Create Placeholder\",\n            parent=builder_menu,\n            command=create_placeholder\n        )\n        cmds.menuItem(\n            \"Update Placeholder\",\n            parent=builder_menu,\n            command=update_placeholder\n        )\n        cmds.menuItem(\n            \"Build Workfile from template\",\n            parent=builder_menu,\n            command=build_workfile_template\n        )\n        cmds.menuItem(\n            \"Update Workfile from template\",\n            parent=builder_menu,\n            command=update_workfile_template\n        )\n\n        cmds.setParent(MENU_NAME, menu=True)\n\n    def add_scripts_menu(project_settings):\n        try:\n            import scriptsmenu.launchformaya as launchformaya\n        except ImportError:\n            log.warning(\n                \"Skipping studio.menu install, because \"\n                \"'scriptsmenu' module seems unavailable.\"\n            )\n            return\n\n        config = project_settings[\"maya\"][\"scriptsmenu\"][\"definition\"]\n        _menu = project_settings[\"maya\"][\"scriptsmenu\"][\"name\"]\n\n        if not config:\n            log.warning(\"Skipping studio menu, no definition found.\")\n            return\n\n        # run the launcher for Maya menu\n        studio_menu = launchformaya.main(\n            title=_menu.title(),\n            objectName=_menu.title().lower().replace(\" \", \"_\")\n        )\n\n        # apply configuration\n        studio_menu.build_from_configuration(studio_menu, config)\n\n    # Allow time for uninstallation to finish.\n    # We use Maya's executeDeferred instead of QTimer.singleShot\n    # so that it only gets called after Maya UI has initialized too.\n    # This is crucial with Maya 2020+ which initializes without UI\n    # first as a QCoreApplication\n    maya.utils.executeDeferred(add_menu)\n    cmds.evalDeferred(partial(add_scripts_menu, project_settings),\n                      lowestPriority=True)\n\n\ndef uninstall():\n    menu = _get_menu()\n    if menu:\n        log.info(\"Attempting to uninstall ...\")\n\n        try:\n            menu.deleteLater()\n            del menu\n        except Exception as e:\n            log.error(e)\n\n\ndef popup():\n    \"\"\"Pop-up the existing menu near the mouse cursor.\"\"\"\n    menu = _get_menu()\n    cursor = QtGui.QCursor()\n    point = cursor.pos()\n    menu.exec_(point)\n\n\ndef update_menu_task_label():\n    \"\"\"Update the task label in Avalon menu to current session\"\"\"\n\n    if IS_HEADLESS:\n        return\n\n    object_name = \"{}|currentContext\".format(MENU_NAME)\n    if not cmds.menuItem(object_name, query=True, exists=True):\n        log.warning(\"Can't find menuItem: {}\".format(object_name))\n        return\n\n    label = get_context_label()\n    cmds.menuItem(object_name, edit=True, label=label)\n"
  },
  {
    "path": "openpype/hosts/maya/api/pipeline.py",
    "content": "import json\nimport base64\nimport os\nimport errno\nimport logging\nimport contextlib\nimport shutil\n\nfrom maya import utils, cmds, OpenMaya\nimport maya.api.OpenMaya as om\n\nimport pyblish.api\n\nfrom openpype.settings import get_project_settings\nfrom openpype.host import (\n    HostBase,\n    IWorkfileHost,\n    ILoadHost,\n    IPublishHost,\n    HostDirmap,\n)\nfrom openpype.tools.utils import host_tools\nfrom openpype.tools.workfiles.lock_dialog import WorkfileLockDialog\nfrom openpype.lib import (\n    register_event_callback,\n    emit_event\n)\nfrom openpype.pipeline import (\n    legacy_io,\n    get_current_project_name,\n    register_loader_plugin_path,\n    register_inventory_action_path,\n    register_creator_plugin_path,\n    deregister_loader_plugin_path,\n    deregister_inventory_action_path,\n    deregister_creator_plugin_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.pipeline.load import any_outdated_containers\nfrom openpype.pipeline.workfile.lock_workfile import (\n    create_workfile_lock,\n    remove_workfile_lock,\n    is_workfile_locked,\n    is_workfile_lock_enabled\n)\nfrom openpype.hosts.maya import MAYA_ROOT_DIR\nfrom openpype.hosts.maya.lib import create_workspace_mel\n\nfrom . import menu, lib\nfrom .workfile_template_builder import MayaPlaceholderLoadPlugin\nfrom .workio import (\n    open_file,\n    save_file,\n    file_extensions,\n    has_unsaved_changes,\n    work_root,\n    current_file\n)\n\nlog = logging.getLogger(\"openpype.hosts.maya\")\n\nPLUGINS_DIR = os.path.join(MAYA_ROOT_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\nINVENTORY_PATH = os.path.join(PLUGINS_DIR, \"inventory\")\n\nAVALON_CONTAINERS = \":AVALON_CONTAINERS\"\n\n\nclass MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):\n    name = \"maya\"\n\n    def __init__(self):\n        super(MayaHost, self).__init__()\n        self._op_events = {}\n\n    def install(self):\n        project_name = get_current_project_name()\n        project_settings = get_project_settings(project_name)\n        # process path mapping\n        dirmap_processor = MayaDirmap(\"maya\", project_name, project_settings)\n        dirmap_processor.process_dirmap()\n\n        pyblish.api.register_plugin_path(PUBLISH_PATH)\n        pyblish.api.register_host(\"mayabatch\")\n        pyblish.api.register_host(\"mayapy\")\n        pyblish.api.register_host(\"maya\")\n\n        register_loader_plugin_path(LOAD_PATH)\n        register_creator_plugin_path(CREATE_PATH)\n        register_inventory_action_path(INVENTORY_PATH)\n        self.log.info(PUBLISH_PATH)\n\n        self.log.info(\"Installing callbacks ... \")\n        register_event_callback(\"init\", on_init)\n\n        _set_project()\n\n        if lib.IS_HEADLESS:\n            self.log.info((\n                \"Running in headless mode, skipping Maya save/open/new\"\n                \" callback installation..\"\n            ))\n\n            return\n\n        self._register_callbacks()\n\n        menu.install(project_settings)\n\n        register_event_callback(\"save\", on_save)\n        register_event_callback(\"open\", on_open)\n        register_event_callback(\"new\", on_new)\n        register_event_callback(\"before.save\", on_before_save)\n        register_event_callback(\"after.save\", on_after_save)\n        register_event_callback(\"before.close\", on_before_close)\n        register_event_callback(\"before.file.open\", before_file_open)\n        register_event_callback(\"taskChanged\", on_task_changed)\n        register_event_callback(\"workfile.open.before\", before_workfile_open)\n        register_event_callback(\"workfile.save.before\", before_workfile_save)\n        register_event_callback(\n            \"workfile.save.before\", workfile_save_before_xgen\n        )\n        register_event_callback(\"workfile.save.after\", after_workfile_save)\n\n    def open_workfile(self, filepath):\n        return open_file(filepath)\n\n    def save_workfile(self, filepath=None):\n        return save_file(filepath)\n\n    def work_root(self, session):\n        return work_root(session)\n\n    def get_current_workfile(self):\n        return current_file()\n\n    def workfile_has_unsaved_changes(self):\n        return has_unsaved_changes()\n\n    def get_workfile_extensions(self):\n        return file_extensions()\n\n    def get_containers(self):\n        return ls()\n\n    def get_workfile_build_placeholder_plugins(self):\n        return [\n            MayaPlaceholderLoadPlugin\n        ]\n\n    @contextlib.contextmanager\n    def maintained_selection(self):\n        with lib.maintained_selection():\n            yield\n\n    def get_context_data(self):\n        data = cmds.fileInfo(\"OpenPypeContext\", query=True)\n        if not data:\n            return {}\n\n        data = data[0]  # Maya seems to return a list\n        decoded = base64.b64decode(data).decode(\"utf-8\")\n        return json.loads(decoded)\n\n    def update_context_data(self, data, changes):\n        json_str = json.dumps(data)\n        encoded = base64.b64encode(json_str.encode(\"utf-8\"))\n        return cmds.fileInfo(\"OpenPypeContext\", encoded)\n\n    def _register_callbacks(self):\n        for handler, event in self._op_events.copy().items():\n            if event is None:\n                continue\n\n            try:\n                OpenMaya.MMessage.removeCallback(event)\n                self._op_events[handler] = None\n            except RuntimeError as exc:\n                self.log.info(exc)\n\n        self._op_events[_on_scene_save] = OpenMaya.MSceneMessage.addCallback(\n            OpenMaya.MSceneMessage.kBeforeSave, _on_scene_save\n        )\n\n        self._op_events[_after_scene_save] = (\n            OpenMaya.MSceneMessage.addCallback(\n                OpenMaya.MSceneMessage.kAfterSave,\n                _after_scene_save\n            )\n        )\n\n        self._op_events[_before_scene_save] = (\n            OpenMaya.MSceneMessage.addCheckCallback(\n                OpenMaya.MSceneMessage.kBeforeSaveCheck,\n                _before_scene_save\n            )\n        )\n\n        self._op_events[_on_scene_new] = OpenMaya.MSceneMessage.addCallback(\n            OpenMaya.MSceneMessage.kAfterNew, _on_scene_new\n        )\n\n        self._op_events[_on_maya_initialized] = (\n            OpenMaya.MSceneMessage.addCallback(\n                OpenMaya.MSceneMessage.kMayaInitialized,\n                _on_maya_initialized\n            )\n        )\n\n        self._op_events[_on_scene_open] = (\n            OpenMaya.MSceneMessage.addCallback(\n                OpenMaya.MSceneMessage.kAfterOpen,\n                _on_scene_open\n            )\n        )\n\n        self._op_events[_before_scene_open] = (\n            OpenMaya.MSceneMessage.addCallback(\n                OpenMaya.MSceneMessage.kBeforeOpen,\n                _before_scene_open\n            )\n        )\n\n        self._op_events[_before_close_maya] = (\n            OpenMaya.MSceneMessage.addCallback(\n                OpenMaya.MSceneMessage.kMayaExiting,\n                _before_close_maya\n            )\n        )\n\n        self.log.info(\"Installed event handler _on_scene_save..\")\n        self.log.info(\"Installed event handler _before_scene_save..\")\n        self.log.info(\"Installed event handler _on_after_save..\")\n        self.log.info(\"Installed event handler _on_scene_new..\")\n        self.log.info(\"Installed event handler _on_maya_initialized..\")\n        self.log.info(\"Installed event handler _on_scene_open..\")\n        self.log.info(\"Installed event handler _check_lock_file..\")\n        self.log.info(\"Installed event handler _before_close_maya..\")\n\n\ndef _set_project():\n    \"\"\"Sets the maya project to the current Session's work directory.\n\n    Returns:\n        None\n\n    \"\"\"\n    workdir = legacy_io.Session[\"AVALON_WORKDIR\"]\n\n    try:\n        os.makedirs(workdir)\n    except OSError as e:\n        # An already existing working directory is fine.\n        if e.errno == errno.EEXIST:\n            pass\n        else:\n            raise\n\n    cmds.workspace(workdir, openWorkspace=True)\n\n\ndef _on_maya_initialized(*args):\n    emit_event(\"init\")\n\n    if cmds.about(batch=True):\n        log.warning(\"Running batch mode ...\")\n        return\n\n    # Keep reference to the main Window, once a main window exists.\n    lib.get_main_window()\n\n\ndef _on_scene_new(*args):\n    emit_event(\"new\")\n\n\ndef _after_scene_save(*arg):\n    emit_event(\"after.save\")\n\n\ndef _on_scene_save(*args):\n    emit_event(\"save\")\n\n\ndef _on_scene_open(*args):\n    emit_event(\"open\")\n\n\ndef _before_close_maya(*args):\n    emit_event(\"before.close\")\n\n\ndef _before_scene_open(*args):\n    emit_event(\"before.file.open\")\n\n\ndef _before_scene_save(return_code, client_data):\n\n    # Default to allowing the action. Registered\n    # callbacks can optionally set this to False\n    # in order to block the operation.\n    OpenMaya.MScriptUtil.setBool(return_code, True)\n\n    emit_event(\n        \"before.save\",\n        {\"return_code\": return_code}\n    )\n\n\ndef _remove_workfile_lock():\n    \"\"\"Remove workfile lock on current file\"\"\"\n    if not handle_workfile_locks():\n        return\n    filepath = current_file()\n    log.info(\"Removing lock on current file {}...\".format(filepath))\n    if filepath:\n        remove_workfile_lock(filepath)\n\n\ndef handle_workfile_locks():\n    if lib.IS_HEADLESS:\n        return False\n    project_name = get_current_project_name()\n    return is_workfile_lock_enabled(MayaHost.name, project_name)\n\n\ndef uninstall():\n    pyblish.api.deregister_plugin_path(PUBLISH_PATH)\n    pyblish.api.deregister_host(\"mayabatch\")\n    pyblish.api.deregister_host(\"mayapy\")\n    pyblish.api.deregister_host(\"maya\")\n\n    deregister_loader_plugin_path(LOAD_PATH)\n    deregister_creator_plugin_path(CREATE_PATH)\n    deregister_inventory_action_path(INVENTORY_PATH)\n\n    menu.uninstall()\n\n\ndef parse_container(container):\n    \"\"\"Return the container node's full container data.\n\n    Args:\n        container (str): A container node name.\n\n    Returns:\n        dict: The container schema data for this container node.\n\n    \"\"\"\n    data = lib.read(container)\n\n    # Backwards compatibility pre-schemas for containers\n    data[\"schema\"] = data.get(\"schema\", \"openpype:container-1.0\")\n\n    # Append transient data\n    data[\"objectName\"] = container\n\n    return data\n\n\ndef _ls():\n    \"\"\"Yields Avalon container node names.\n\n    Used by `ls()` to retrieve the nodes and then query the full container's\n    data.\n\n    Yields:\n        str: Avalon container node name (objectSet)\n\n    \"\"\"\n\n    def _maya_iterate(iterator):\n        \"\"\"Helper to iterate a maya iterator\"\"\"\n        while not iterator.isDone():\n            yield iterator.thisNode()\n            iterator.next()\n\n    ids = {AVALON_CONTAINER_ID,\n           # Backwards compatibility\n           \"pyblish.mindbender.container\"}\n\n    # Iterate over all 'set' nodes in the scene to detect whether\n    # they have the avalon container \".id\" attribute.\n    fn_dep = om.MFnDependencyNode()\n    iterator = om.MItDependencyNodes(om.MFn.kSet)\n    for mobject in _maya_iterate(iterator):\n        if mobject.apiTypeStr != \"kSet\":\n            # Only match by exact type\n            continue\n\n        fn_dep.setObject(mobject)\n        if not fn_dep.hasAttribute(\"id\"):\n            continue\n\n        plug = fn_dep.findPlug(\"id\", True)\n        value = plug.asString()\n        if value in ids:\n            yield fn_dep.name()\n\n\ndef ls():\n    \"\"\"Yields containers from active Maya scene\n\n    This is the host-equivalent of api.ls(), but instead of listing\n    assets on disk, it lists assets already loaded in Maya; once loaded\n    they are called 'containers'\n\n    Yields:\n        dict: container\n\n    \"\"\"\n    container_names = _ls()\n    for container in sorted(container_names):\n        yield parse_container(container)\n\n\ndef containerise(name,\n                 namespace,\n                 nodes,\n                 context,\n                 loader=None,\n                 suffix=\"CON\"):\n    \"\"\"Bundle `nodes` into an assembly and imprint it with metadata\n\n    Containerisation enables a tracking of version, author and origin\n    for loaded assets.\n\n    Arguments:\n        name (str): Name of resulting assembly\n        namespace (str): Namespace under which to host container\n        nodes (list): Long names of nodes to containerise\n        context (dict): Asset information\n        loader (str, optional): Name of loader used to produce this container.\n        suffix (str, optional): Suffix of container, defaults to `_CON`.\n\n    Returns:\n        container (str): Name of container assembly\n\n    \"\"\"\n    container = cmds.sets(nodes, name=\"%s_%s_%s\" % (namespace, name, suffix))\n\n    data = [\n        (\"schema\", \"openpype:container-2.0\"),\n        (\"id\", AVALON_CONTAINER_ID),\n        (\"name\", name),\n        (\"namespace\", namespace),\n        (\"loader\", loader),\n        (\"representation\", context[\"representation\"][\"_id\"]),\n    ]\n\n    for key, value in data:\n        cmds.addAttr(container, longName=key, dataType=\"string\")\n        cmds.setAttr(container + \".\" + key, str(value), type=\"string\")\n\n    main_container = cmds.ls(AVALON_CONTAINERS, type=\"objectSet\")\n    if not main_container:\n        main_container = cmds.sets(empty=True, name=AVALON_CONTAINERS)\n\n        # Implement #399: Maya 2019+ hide AVALON_CONTAINERS on creation..\n        if cmds.attributeQuery(\"hiddenInOutliner\",\n                               node=main_container,\n                               exists=True):\n            cmds.setAttr(main_container + \".hiddenInOutliner\", True)\n    else:\n        main_container = main_container[0]\n\n    cmds.sets(container, addElement=main_container)\n\n    # Implement #399: Maya 2019+ hide containers in outliner\n    if cmds.attributeQuery(\"hiddenInOutliner\",\n                           node=container,\n                           exists=True):\n        cmds.setAttr(container + \".hiddenInOutliner\", True)\n\n    return container\n\n\ndef on_init():\n    log.info(\"Running callback on init..\")\n\n    def safe_deferred(fn):\n        \"\"\"Execute deferred the function in a try-except\"\"\"\n\n        def _fn():\n            \"\"\"safely call in deferred callback\"\"\"\n            try:\n                fn()\n            except Exception as exc:\n                print(exc)\n\n        try:\n            utils.executeDeferred(_fn)\n        except Exception as exc:\n            print(exc)\n\n    # Force load Alembic so referenced alembics\n    # work correctly on scene open\n    cmds.loadPlugin(\"AbcImport\", quiet=True)\n    cmds.loadPlugin(\"AbcExport\", quiet=True)\n\n    # Force load objExport plug-in (requested by artists)\n    cmds.loadPlugin(\"objExport\", quiet=True)\n\n    if not lib.IS_HEADLESS:\n        launch_workfiles = os.environ.get(\"WORKFILES_STARTUP\")\n        if launch_workfiles:\n            safe_deferred(host_tools.show_workfiles)\n\n        from .customize import (\n            override_component_mask_commands,\n            override_toolbox_ui\n        )\n        safe_deferred(override_component_mask_commands)\n        safe_deferred(override_toolbox_ui)\n\n\ndef on_before_save():\n    \"\"\"Run validation for scene's FPS prior to saving\"\"\"\n    return lib.validate_fps()\n\n\ndef on_after_save():\n    \"\"\"Check if there is a lockfile after save\"\"\"\n    check_lock_on_current_file()\n\n\ndef check_lock_on_current_file():\n\n    \"\"\"Check if there is a user opening the file\"\"\"\n    if not handle_workfile_locks():\n        return\n    log.info(\"Running callback on checking the lock file...\")\n\n    # add the lock file when opening the file\n    filepath = current_file()\n    # Skip if current file is 'untitled'\n    if not filepath:\n        return\n\n    if is_workfile_locked(filepath):\n        # add lockfile dialog\n        workfile_dialog = WorkfileLockDialog(filepath)\n        if not workfile_dialog.exec_():\n            cmds.file(new=True)\n            return\n\n    create_workfile_lock(filepath)\n\n\ndef on_before_close():\n    \"\"\"Delete the lock file after user quitting the Maya Scene\"\"\"\n    log.info(\"Closing Maya...\")\n    # delete the lock file\n    filepath = current_file()\n    if handle_workfile_locks():\n        remove_workfile_lock(filepath)\n\n\ndef before_file_open():\n    \"\"\"check lock file when the file changed\"\"\"\n    # delete the lock file\n    _remove_workfile_lock()\n\n\ndef on_save():\n    \"\"\"Automatically add IDs to new nodes\n\n    Any transform of a mesh, without an existing ID, is given one\n    automatically on file save.\n    \"\"\"\n    log.info(\"Running callback on save..\")\n    # remove lockfile if users jumps over from one scene to another\n    _remove_workfile_lock()\n\n    # Generate ids of the current context on nodes in the scene\n    nodes = lib.get_id_required_nodes(referenced_nodes=False)\n    for node, new_id in lib.generate_ids(nodes):\n        lib.set_id(node, new_id, overwrite=False)\n\n\ndef on_open():\n    \"\"\"On scene open let's assume the containers have changed.\"\"\"\n\n    from openpype.widgets import popup\n\n    # Validate FPS after update_task_from_path to\n    # ensure it is using correct FPS for the asset\n    lib.validate_fps()\n    lib.fix_incompatible_containers()\n\n    if any_outdated_containers():\n        log.warning(\"Scene has outdated content.\")\n\n        # Find maya main window\n        parent = lib.get_main_window()\n        if parent is None:\n            log.info(\"Skipping outdated content pop-up \"\n                     \"because Maya window can't be found.\")\n        else:\n\n            # Show outdated pop-up\n            def _on_show_inventory():\n                host_tools.show_scene_inventory(parent=parent)\n\n            dialog = popup.Popup(parent=parent)\n            dialog.setWindowTitle(\"Maya scene has outdated content\")\n            dialog.setMessage(\"There are outdated containers in \"\n                              \"your Maya scene.\")\n            dialog.on_clicked.connect(_on_show_inventory)\n            dialog.show()\n\n    # create lock file for the maya scene\n    check_lock_on_current_file()\n\n\ndef on_new():\n    \"\"\"Set project resolution and fps when create a new file\"\"\"\n    log.info(\"Running callback on new..\")\n    with lib.suspended_refresh():\n        lib.set_context_settings()\n\n    _remove_workfile_lock()\n\n\ndef on_task_changed():\n    \"\"\"Wrapped function of app initialize and maya's on task changed\"\"\"\n    # Run\n    menu.update_menu_task_label()\n\n    workdir = legacy_io.Session[\"AVALON_WORKDIR\"]\n    if os.path.exists(workdir):\n        log.info(\"Updating Maya workspace for task change to %s\", workdir)\n        _set_project()\n\n        # Set Maya fileDialog's start-dir to /scenes\n        frule_scene = cmds.workspace(fileRuleEntry=\"scene\")\n        cmds.optionVar(stringValue=(\"browserLocationmayaBinaryscene\",\n                                    workdir + \"/\" + frule_scene))\n\n    else:\n        log.warning((\n            \"Can't set project for new context because path does not exist: {}\"\n        ).format(workdir))\n\n    with lib.suspended_refresh():\n        lib.set_context_settings()\n        lib.update_content_on_context_change()\n\n\ndef before_workfile_open():\n    if handle_workfile_locks():\n        _remove_workfile_lock()\n\n\ndef before_workfile_save(event):\n    project_name = get_current_project_name()\n    if handle_workfile_locks():\n        _remove_workfile_lock()\n    workdir_path = event[\"workdir_path\"]\n    if workdir_path:\n        create_workspace_mel(workdir_path, project_name)\n\n\ndef workfile_save_before_xgen(event):\n    \"\"\"Manage Xgen external files when switching context.\n\n    Xgen has various external files that needs to be unique and relative to the\n    workfile, so we need to copy and potentially overwrite these files when\n    switching context.\n\n    Args:\n        event (Event) - openpype/lib/events.py\n    \"\"\"\n    if not cmds.pluginInfo(\"xgenToolkit\", query=True, loaded=True):\n        return\n\n    import xgenm\n\n    current_work_dir = legacy_io.Session[\"AVALON_WORKDIR\"].replace(\"\\\\\", \"/\")\n    expected_work_dir = event.data[\"workdir_path\"].replace(\"\\\\\", \"/\")\n    if current_work_dir == expected_work_dir:\n        return\n\n    palettes = cmds.ls(type=\"xgmPalette\", long=True)\n    if not palettes:\n        return\n\n    transfers = []\n    overwrites = []\n    attribute_changes = {}\n    attrs = [\"xgFileName\", \"xgBaseFile\"]\n    for palette in palettes:\n        sanitized_palette = palette.replace(\"|\", \"\")\n        project_path = xgenm.getAttr(\"xgProjectPath\", sanitized_palette)\n        _, maya_extension = os.path.splitext(event.data[\"filename\"])\n\n        for attr in attrs:\n            node_attr = \"{}.{}\".format(palette, attr)\n            attr_value = cmds.getAttr(node_attr)\n\n            if not attr_value:\n                continue\n\n            source = os.path.join(project_path, attr_value)\n\n            attr_value = event.data[\"filename\"].replace(\n                maya_extension,\n                \"__{}{}\".format(\n                    sanitized_palette.replace(\":\", \"__\"),\n                    os.path.splitext(attr_value)[1]\n                )\n            )\n            target = os.path.join(expected_work_dir, attr_value)\n\n            transfers.append((source, target))\n            attribute_changes[node_attr] = attr_value\n\n        relative_path = xgenm.getAttr(\n            \"xgDataPath\", sanitized_palette\n        ).split(os.pathsep)[0]\n        absolute_path = relative_path.replace(\"${PROJECT}\", project_path)\n        for root, _, files in os.walk(absolute_path):\n            for f in files:\n                source = os.path.join(root, f).replace(\"\\\\\", \"/\")\n                target = source.replace(project_path, expected_work_dir + \"/\")\n                transfers.append((source, target))\n                if os.path.exists(target):\n                    overwrites.append(target)\n\n    # Ask user about overwriting files.\n    if overwrites:\n        log.warning(\n            \"WARNING! Potential loss of data.\\n\\n\"\n            \"Found duplicate Xgen files in new context.\\n{}\".format(\n                \"\\n\".join(overwrites)\n            )\n        )\n        return\n\n    for source, destination in transfers:\n        if not os.path.exists(os.path.dirname(destination)):\n            os.makedirs(os.path.dirname(destination))\n        shutil.copy(source, destination)\n\n    for attribute, value in attribute_changes.items():\n        cmds.setAttr(attribute, value, type=\"string\")\n\n\ndef after_workfile_save(event):\n    workfile_name = event[\"filename\"]\n    if (\n        handle_workfile_locks()\n        and workfile_name\n        and not is_workfile_locked(workfile_name)\n    ):\n        create_workfile_lock(workfile_name)\n\n\nclass MayaDirmap(HostDirmap):\n    def on_enable_dirmap(self):\n        cmds.dirmap(en=True)\n\n    def dirmap_routine(self, source_path, destination_path):\n        cmds.dirmap(m=(source_path, destination_path))\n        cmds.dirmap(m=(destination_path, source_path))\n"
  },
  {
    "path": "openpype/hosts/maya/api/plugin.py",
    "content": "import json\nimport os\nfrom abc import ABCMeta\n\nimport qargparse\nimport six\nfrom maya import cmds\nfrom maya.app.renderSetup.model import renderSetup\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.lib import BoolDef, Logger\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import (\n    AVALON_CONTAINER_ID,\n    Anatomy,\n\n    CreatedInstance,\n    Creator as NewCreator,\n    AutoCreator,\n    HiddenCreator,\n\n    CreatorError,\n    LegacyCreator,\n    LoaderPlugin,\n    get_representation_path,\n)\nfrom openpype.pipeline.load import LoadError\nfrom openpype.client import get_asset_by_name\nfrom openpype.pipeline.create import get_subset_name\n\nfrom . import lib\nfrom .lib import imprint, read\nfrom .pipeline import containerise\n\nlog = Logger.get_logger()\n\n\ndef _get_attr(node, attr, default=None):\n    \"\"\"Helper to get attribute which allows attribute to not exist.\"\"\"\n    if not cmds.attributeQuery(attr, node=node, exists=True):\n        return default\n    return cmds.getAttr(\"{}.{}\".format(node, attr))\n\n\n# Backwards compatibility: these functions has been moved to lib.\ndef get_reference_node(*args, **kwargs):\n    \"\"\"Get the reference node from the container members\n\n    Deprecated:\n        This function was moved and will be removed in 3.16.x.\n    \"\"\"\n    msg = \"Function 'get_reference_node' has been moved.\"\n    log.warning(msg)\n    cmds.warning(msg)\n    return lib.get_reference_node(*args, **kwargs)\n\n\ndef get_reference_node_parents(*args, **kwargs):\n    \"\"\"\n    Deprecated:\n        This function was moved and will be removed in 3.16.x.\n    \"\"\"\n    msg = \"Function 'get_reference_node_parents' has been moved.\"\n    log.warning(msg)\n    cmds.warning(msg)\n    return lib.get_reference_node_parents(*args, **kwargs)\n\n\nclass Creator(LegacyCreator):\n    defaults = ['Main']\n\n    def process(self):\n        nodes = list()\n\n        with lib.undo_chunk():\n            if (self.options or {}).get(\"useSelection\"):\n                nodes = cmds.ls(selection=True)\n\n            instance = cmds.sets(nodes, name=self.name)\n            lib.imprint(instance, self.data)\n\n        return instance\n\n\n@six.add_metaclass(ABCMeta)\nclass MayaCreatorBase(object):\n\n    @staticmethod\n    def cache_subsets(shared_data):\n        \"\"\"Cache instances for Creators to shared data.\n\n        Create `maya_cached_subsets` key when needed in shared data and\n        fill it with all collected instances from the scene under its\n        respective creator identifiers.\n\n        If legacy instances are detected in the scene, create\n        `maya_cached_legacy_subsets` there and fill it with\n        all legacy subsets under family as a key.\n\n        Args:\n            Dict[str, Any]: Shared data.\n\n        Return:\n            Dict[str, Any]: Shared data dictionary.\n\n        \"\"\"\n        if shared_data.get(\"maya_cached_subsets\") is None:\n            cache = dict()\n            cache_legacy = dict()\n\n            for node in cmds.ls(type=\"objectSet\"):\n\n                if _get_attr(node, attr=\"id\") != \"pyblish.avalon.instance\":\n                    continue\n\n                creator_id = _get_attr(node, attr=\"creator_identifier\")\n                if creator_id is not None:\n                    # creator instance\n                    cache.setdefault(creator_id, []).append(node)\n                else:\n                    # legacy instance\n                    family = _get_attr(node, attr=\"family\")\n                    if family is None:\n                        # must be a broken instance\n                        continue\n\n                    cache_legacy.setdefault(family, []).append(node)\n\n            shared_data[\"maya_cached_subsets\"] = cache\n            shared_data[\"maya_cached_legacy_subsets\"] = cache_legacy\n        return shared_data\n\n    def get_publish_families(self):\n        \"\"\"Return families for the instances of this creator.\n\n        Allow a Creator to define multiple families so that a creator can\n        e.g. specify `usd` and `usdMaya` and another USD creator can also\n        specify `usd` but apply different extractors like `usdMultiverse`.\n\n        There is no need to override this method if you only have the\n        primary family defined by the `family` property as that will always\n        be set.\n\n        Returns:\n            list: families for instances of this creator\n\n        \"\"\"\n        return []\n\n    def imprint_instance_node(self, node, data):\n\n        # We never store the instance_node as value on the node since\n        # it's the node name itself\n        data.pop(\"instance_node\", None)\n        data.pop(\"instance_id\", None)\n\n        # Don't store `families` since it's up to the creator itself\n        # to define the initial publish families - not a stored attribute of\n        # `families`\n        data.pop(\"families\", None)\n\n        # We store creator attributes at the root level and assume they\n        # will not clash in names with `subset`, `task`, etc. and other\n        # default names. This is just so these attributes in many cases\n        # are still editable in the maya UI by artists.\n        # note: pop to move to end of dict to sort attributes last on the node\n        creator_attributes = data.pop(\"creator_attributes\", {})\n\n        # We only flatten value types which `imprint` function supports\n        json_creator_attributes = {}\n        for key, value in dict(creator_attributes).items():\n            if isinstance(value, (list, tuple, dict)):\n                creator_attributes.pop(key)\n                json_creator_attributes[key] = value\n\n        # Flatten remaining creator attributes to the node itself\n        data.update(creator_attributes)\n\n        # We know the \"publish_attributes\" will be complex data of\n        # settings per plugins, we'll store this as a flattened json structure\n        # pop to move to end of dict to sort attributes last on the node\n        data[\"publish_attributes\"] = json.dumps(\n            data.pop(\"publish_attributes\", {})\n        )\n\n        # Persist the non-flattened creator attributes (special value types,\n        # like multiselection EnumDef)\n        data[\"creator_attributes\"] = json.dumps(json_creator_attributes)\n\n        # Since we flattened the data structure for creator attributes we want\n        # to correctly detect which flattened attributes should end back in the\n        # creator attributes when reading the data from the node, so we store\n        # the relevant keys as a string\n        data[\"__creator_attributes_keys\"] = \",\".join(creator_attributes.keys())\n\n        # Kill any existing attributes just so we can imprint cleanly again\n        for attr in data.keys():\n            if cmds.attributeQuery(attr, node=node, exists=True):\n                cmds.deleteAttr(\"{}.{}\".format(node, attr))\n\n        return imprint(node, data)\n\n    def read_instance_node(self, node):\n        node_data = read(node)\n\n        # Never care about a cbId attribute on the object set\n        # being read as 'data'\n        node_data.pop(\"cbId\", None)\n\n        # Make sure we convert any creator attributes from the json string\n        creator_attributes = node_data.get(\"creator_attributes\")\n        if creator_attributes:\n            node_data[\"creator_attributes\"] = json.loads(creator_attributes)\n        else:\n            node_data[\"creator_attributes\"] = {}\n\n        # Move the relevant attributes into \"creator_attributes\" that\n        # we flattened originally\n        creator_attribute_keys = node_data.pop(\"__creator_attributes_keys\",\n                                               \"\").split(\",\")\n        for key in creator_attribute_keys:\n            if key in node_data:\n                node_data[\"creator_attributes\"][key] = node_data.pop(key)\n\n        # Make sure we convert any publish attributes from the json string\n        publish_attributes = node_data.get(\"publish_attributes\")\n        if publish_attributes:\n            node_data[\"publish_attributes\"] = json.loads(publish_attributes)\n\n        # Explicitly re-parse the node name\n        node_data[\"instance_node\"] = node\n        node_data[\"instance_id\"] = node\n\n        # If the creator plug-in specifies\n        families = self.get_publish_families()\n        if families:\n            node_data[\"families\"] = families\n\n        return node_data\n\n    def _default_collect_instances(self):\n        self.cache_subsets(self.collection_shared_data)\n        cached_subsets = self.collection_shared_data[\"maya_cached_subsets\"]\n        for node in cached_subsets.get(self.identifier, []):\n            node_data = self.read_instance_node(node)\n\n            created_instance = CreatedInstance.from_existing(node_data, self)\n            self._add_instance_to_context(created_instance)\n\n    def _default_update_instances(self, update_list):\n        for created_inst, _changes in update_list:\n            data = created_inst.data_to_store()\n            node = data.get(\"instance_node\")\n\n            self.imprint_instance_node(node, data)\n\n    def _default_remove_instances(self, instances):\n        \"\"\"Remove specified instance from the scene.\n\n        This is only removing `id` parameter so instance is no longer\n        instance, because it might contain valuable data for artist.\n\n        \"\"\"\n        for instance in instances:\n            node = instance.data.get(\"instance_node\")\n            if node:\n                cmds.delete(node)\n\n            self._remove_instance_from_context(instance)\n\n\n@six.add_metaclass(ABCMeta)\nclass MayaCreator(NewCreator, MayaCreatorBase):\n\n    settings_category = \"maya\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        members = list()\n        if pre_create_data.get(\"use_selection\"):\n            members = cmds.ls(selection=True)\n\n        # Allow a Creator to define multiple families\n        publish_families = self.get_publish_families()\n        if publish_families:\n            families = instance_data.setdefault(\"families\", [])\n            for family in self.get_publish_families():\n                if family not in families:\n                    families.append(family)\n\n        with lib.undo_chunk():\n            instance_node = cmds.sets(members, name=subset_name)\n            instance_data[\"instance_node\"] = instance_node\n            instance = CreatedInstance(\n                self.family,\n                subset_name,\n                instance_data,\n                self)\n            self._add_instance_to_context(instance)\n\n            self.imprint_instance_node(instance_node,\n                                       data=instance.data_to_store())\n            return instance\n\n    def collect_instances(self):\n        return self._default_collect_instances()\n\n    def update_instances(self, update_list):\n        return self._default_update_instances(update_list)\n\n    def remove_instances(self, instances):\n        return self._default_remove_instances(instances)\n\n    def get_pre_create_attr_defs(self):\n        return [\n            BoolDef(\"use_selection\",\n                    label=\"Use selection\",\n                    default=True)\n        ]\n\n\nclass MayaAutoCreator(AutoCreator, MayaCreatorBase):\n    \"\"\"Automatically triggered creator for Maya.\n\n    The plugin is not visible in UI, and 'create' method does not expect\n        any arguments.\n    \"\"\"\n\n    settings_category = \"maya\"\n\n    def collect_instances(self):\n        return self._default_collect_instances()\n\n    def update_instances(self, update_list):\n        return self._default_update_instances(update_list)\n\n    def remove_instances(self, instances):\n        return self._default_remove_instances(instances)\n\n\nclass MayaHiddenCreator(HiddenCreator, MayaCreatorBase):\n    \"\"\"Hidden creator for Maya.\n\n    The plugin is not visible in UI, and it does not have strictly defined\n        arguments for 'create' method.\n    \"\"\"\n\n    settings_category = \"maya\"\n\n    def create(self, *args, **kwargs):\n        return MayaCreator.create(self, *args, **kwargs)\n\n    def collect_instances(self):\n        return self._default_collect_instances()\n\n    def update_instances(self, update_list):\n        return self._default_update_instances(update_list)\n\n    def remove_instances(self, instances):\n        return self._default_remove_instances(instances)\n\n\ndef ensure_namespace(namespace):\n    \"\"\"Make sure the namespace exists.\n\n    Args:\n        namespace (str): The preferred namespace name.\n\n    Returns:\n        str: The generated or existing namespace\n\n    \"\"\"\n    exists = cmds.namespace(exists=namespace)\n    if exists:\n        return namespace\n    else:\n        return cmds.namespace(add=namespace)\n\n\nclass RenderlayerCreator(NewCreator, MayaCreatorBase):\n    \"\"\"Creator which creates an instance per renderlayer in the workfile.\n\n    Create and manages renderlayer subset per renderLayer in workfile.\n    This generates a singleton node in the scene which, if it exists, tells the\n    Creator to collect Maya rendersetup renderlayers as individual instances.\n    As such, triggering create doesn't actually create the instance node per\n    layer but only the node which tells the Creator it may now collect\n    an instance per renderlayer.\n\n    \"\"\"\n\n    # These are required to be overridden in subclass\n    singleton_node_name = \"\"\n\n    # These are optional to be overridden in subclass\n    layer_instance_prefix = None\n\n    def _get_singleton_node(self, return_all=False):\n        nodes = lib.lsattr(\"pre_creator_identifier\", self.identifier)\n        if nodes:\n            return nodes if return_all else nodes[0]\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # A Renderlayer is never explicitly created using the create method.\n        # Instead, renderlayers from the scene are collected. Thus \"create\"\n        # would only ever be called to say, 'hey, please refresh collect'\n        self.create_singleton_node()\n\n        # if no render layers are present, create default one with\n        # asterisk selector\n        rs = renderSetup.instance()\n        if not rs.getRenderLayers():\n            render_layer = rs.createRenderLayer(\"Main\")\n            collection = render_layer.createCollection(\"defaultCollection\")\n            collection.getSelector().setPattern('*')\n\n        # By RenderLayerCreator.create we make it so that the renderlayer\n        # instances directly appear even though it just collects scene\n        # renderlayers. This doesn't actually 'create' any scene contents.\n        self.collect_instances()\n\n    def create_singleton_node(self):\n        if self._get_singleton_node():\n            raise CreatorError(\"A Render instance already exists - only \"\n                               \"one can be configured.\")\n\n        with lib.undo_chunk():\n            node = cmds.sets(empty=True, name=self.singleton_node_name)\n            lib.imprint(node, data={\n                \"pre_creator_identifier\": self.identifier\n            })\n\n        return node\n\n    def collect_instances(self):\n\n        # We only collect if the global render instance exists\n        if not self._get_singleton_node():\n            return\n\n        rs = renderSetup.instance()\n        layers = rs.getRenderLayers()\n        for layer in layers:\n            layer_instance_node = self.find_layer_instance_node(layer)\n            if layer_instance_node:\n                data = self.read_instance_node(layer_instance_node)\n                instance = CreatedInstance.from_existing(data, creator=self)\n            else:\n                # No existing scene instance node for this layer. Note that\n                # this instance will not have the `instance_node` data yet\n                # until it's been saved/persisted at least once.\n                project_name = self.create_context.get_current_project_name()\n                asset_name = self.create_context.get_current_asset_name()\n                instance_data = {\n                    \"task\": self.create_context.get_current_task_name(),\n                    \"variant\": layer.name(),\n                }\n                if AYON_SERVER_ENABLED:\n                    instance_data[\"folderPath\"] = asset_name\n                else:\n                    instance_data[\"asset\"] = asset_name\n                asset_doc = get_asset_by_name(project_name, asset_name)\n                subset_name = self.get_subset_name(\n                    layer.name(),\n                    instance_data[\"task\"],\n                    asset_doc,\n                    project_name)\n\n                instance = CreatedInstance(\n                    family=self.family,\n                    subset_name=subset_name,\n                    data=instance_data,\n                    creator=self\n                )\n\n            instance.transient_data[\"layer\"] = layer\n            self._add_instance_to_context(instance)\n\n    def find_layer_instance_node(self, layer):\n        connected_sets = cmds.listConnections(\n            \"{}.message\".format(layer.name()),\n            source=False,\n            destination=True,\n            type=\"objectSet\"\n        ) or []\n\n        for node in connected_sets:\n            if not cmds.attributeQuery(\"creator_identifier\",\n                                       node=node,\n                                       exists=True):\n                continue\n\n            creator_identifier = cmds.getAttr(node + \".creator_identifier\")\n            if creator_identifier == self.identifier:\n                self.log.info(\"Found node: {}\".format(node))\n                return node\n\n    def _create_layer_instance_node(self, layer):\n\n        # We only collect if a CreateRender instance exists\n        create_render_set = self._get_singleton_node()\n        if not create_render_set:\n            raise CreatorError(\"Creating a renderlayer instance node is not \"\n                               \"allowed if no 'CreateRender' instance exists\")\n\n        namespace = \"_{}\".format(self.singleton_node_name)\n        namespace = ensure_namespace(namespace)\n\n        name = \"{}:{}\".format(namespace, layer.name())\n        render_set = cmds.sets(name=name, empty=True)\n\n        # Keep an active link with the renderlayer so we can retrieve it\n        # later by a physical maya connection instead of relying on the layer\n        # name\n        cmds.addAttr(render_set, longName=\"renderlayer\", at=\"message\")\n        cmds.connectAttr(\"{}.message\".format(layer.name()),\n                         \"{}.renderlayer\".format(render_set), force=True)\n\n        # Add the set to the 'CreateRender' set.\n        cmds.sets(render_set, forceElement=create_render_set)\n\n        return render_set\n\n    def update_instances(self, update_list):\n        # We only generate the persisting layer data into the scene once\n        # we save with the UI on e.g. validate or publish\n        for instance, _changes in update_list:\n            instance_node = instance.data.get(\"instance_node\")\n\n            # Ensure a node exists to persist the data to\n            if not instance_node:\n                layer = instance.transient_data[\"layer\"]\n                instance_node = self._create_layer_instance_node(layer)\n                instance.data[\"instance_node\"] = instance_node\n\n            self.imprint_instance_node(instance_node,\n                                       data=instance.data_to_store())\n\n    def imprint_instance_node(self, node, data):\n        # Do not ever try to update the `renderlayer` since it'll try\n        # to remove the attribute and recreate it but fail to keep it a\n        # message attribute link. We only ever imprint that on the initial\n        # node creation.\n        # TODO: Improve how this is handled\n        data.pop(\"renderlayer\", None)\n        data.get(\"creator_attributes\", {}).pop(\"renderlayer\", None)\n\n        return super(RenderlayerCreator, self).imprint_instance_node(node,\n                                                                     data=data)\n\n    def remove_instances(self, instances):\n        \"\"\"Remove specified instances from the scene.\n\n        This is only removing `id` parameter so instance is no longer\n        instance, because it might contain valuable data for artist.\n\n        \"\"\"\n        # Instead of removing the single instance or renderlayers we instead\n        # remove the CreateRender node this creator relies on to decide whether\n        # it should collect anything at all.\n        nodes = self._get_singleton_node(return_all=True)\n        if nodes:\n            cmds.delete(nodes)\n\n        # Remove ALL the instances even if only one gets deleted\n        for instance in list(self.create_context.instances):\n            if instance.get(\"creator_identifier\") == self.identifier:\n                self._remove_instance_from_context(instance)\n\n                # Remove the stored settings per renderlayer too\n                node = instance.data.get(\"instance_node\")\n                if node and cmds.objExists(node):\n                    cmds.delete(node)\n\n    def get_subset_name(\n        self,\n        variant,\n        task_name,\n        asset_doc,\n        project_name,\n        host_name=None,\n        instance=None\n    ):\n        # creator.family != 'render' as expected\n        return get_subset_name(self.layer_instance_prefix,\n                               variant,\n                               task_name,\n                               asset_doc,\n                               project_name)\n\n\nclass Loader(LoaderPlugin):\n    hosts = [\"maya\"]\n\n    load_settings = {}  # defined in settings\n\n    @classmethod\n    def apply_settings(cls, project_settings, system_settings):\n        super(Loader, cls).apply_settings(project_settings, system_settings)\n        cls.load_settings = project_settings['maya']['load']\n\n    def get_custom_namespace_and_group(self, context, options, loader_key):\n        \"\"\"Queries Settings to get custom template for namespace and group.\n\n        Group template might be empty >> this forces to not wrap imported items\n        into separate group.\n\n        Args:\n            context (dict)\n            options (dict): artist modifiable options from dialog\n            loader_key (str): key to get separate configuration from Settings\n                ('reference_loader'|'import_loader')\n        \"\"\"\n\n        options[\"attach_to_root\"] = True\n        try:\n            custom_naming = self.load_settings[loader_key]\n        except KeyError:\n            self.log.warning(\n                \"No settings found for {} in settings, falling back to \"\n                \"ReferenceLoader defaults.\".format(loader_key))\n            custom_naming = self.load_settings[\"reference_loader\"]\n\n        if not custom_naming['namespace']:\n            raise LoadError(\"No namespace specified in \"\n                            \"Maya ReferenceLoader settings\")\n        elif not custom_naming['group_name']:\n            self.log.debug(\"No custom group_name, no group will be created.\")\n            options[\"attach_to_root\"] = False\n\n        asset = context['asset']\n        subset = context['subset']\n        formatting_data = {\n            \"asset_name\": asset['name'],\n            \"asset_type\": asset['type'],\n            \"folder\": {\n                \"name\": asset[\"name\"],\n            },\n            \"subset\": subset['name'],\n            \"family\": (\n                subset['data'].get('family') or\n                subset['data']['families'][0]\n            )\n        }\n\n        custom_namespace = custom_naming['namespace'].format(\n            **formatting_data\n        )\n\n        custom_group_name = custom_naming['group_name'].format(\n            **formatting_data\n        )\n\n        return custom_group_name, custom_namespace, options\n\n\nclass ReferenceLoader(Loader):\n    \"\"\"A basic ReferenceLoader for Maya\n\n    This will implement the basic behavior for a loader to inherit from that\n    will containerize the reference and will implement the `remove` and\n    `update` logic.\n\n    \"\"\"\n\n    options = [\n        qargparse.Integer(\n            \"count\",\n            label=\"Count\",\n            default=1,\n            min=1,\n            help=\"How many times to load?\"\n        ),\n        qargparse.Double3(\n            \"offset\",\n            label=\"Position Offset\",\n            help=\"Offset loaded models for easier selection.\"\n        ),\n        qargparse.Boolean(\n            \"attach_to_root\",\n            label=\"Group imported asset\",\n            default=True,\n            help=\"Should a group be created to encapsulate\"\n                 \" imported representation ?\"\n        )\n    ]\n\n    def load(\n        self,\n        context,\n        name=None,\n        namespace=None,\n        options=None\n    ):\n        path = self.filepath_from_context(context)\n        assert os.path.exists(path), \"%s does not exist.\" % path\n\n        custom_group_name, custom_namespace, options = \\\n            self.get_custom_namespace_and_group(context, options,\n                                                \"reference_loader\")\n\n        count = options.get(\"count\") or 1\n\n        loaded_containers = []\n        for c in range(0, count):\n            namespace = lib.get_custom_namespace(custom_namespace)\n            group_name = \"{}:{}\".format(\n                namespace,\n                custom_group_name\n            )\n\n            options['group_name'] = group_name\n\n            # Offset loaded subset\n            if \"offset\" in options:\n                offset = [i * c for i in options[\"offset\"]]\n                options[\"translate\"] = offset\n\n            self.log.info(options)\n\n            self.process_reference(\n                context=context,\n                name=name,\n                namespace=namespace,\n                options=options\n            )\n\n            # Only containerize if any nodes were loaded by the Loader\n            nodes = self[:]\n            if not nodes:\n                return\n\n            ref_node = lib.get_reference_node(nodes, self.log)\n            container = containerise(\n                name=name,\n                namespace=namespace,\n                nodes=[ref_node],\n                context=context,\n                loader=self.__class__.__name__\n            )\n            loaded_containers.append(container)\n            self._organize_containers(nodes, container)\n            c += 1\n\n        return loaded_containers\n\n    def process_reference(self, context, name, namespace, options):\n        \"\"\"To be implemented by subclass\"\"\"\n        raise NotImplementedError(\"Must be implemented by subclass\")\n\n    def update(self, container, representation):\n        from maya import cmds\n\n        from openpype.hosts.maya.api.lib import get_container_members\n\n        node = container[\"objectName\"]\n\n        path = get_representation_path(representation)\n\n        # Get reference node from container members\n        members = get_container_members(node)\n        reference_node = lib.get_reference_node(members, self.log)\n        namespace = cmds.referenceQuery(reference_node, namespace=True)\n\n        file_type = {\n            \"ma\": \"mayaAscii\",\n            \"mb\": \"mayaBinary\",\n            \"abc\": \"Alembic\",\n            \"fbx\": \"FBX\",\n            \"usd\": \"USD Import\"\n        }.get(representation[\"name\"])\n\n        assert file_type, \"Unsupported representation: %s\" % representation\n\n        assert os.path.exists(path), \"%s does not exist.\" % path\n\n        # Need to save alembic settings and reapply, cause referencing resets\n        # them to incoming data.\n        alembic_attrs = [\"speed\", \"offset\", \"cycleType\", \"time\"]\n        alembic_data = {}\n        if representation[\"name\"] == \"abc\":\n            alembic_nodes = cmds.ls(\n                \"{}:*\".format(namespace), type=\"AlembicNode\"\n            )\n            if alembic_nodes:\n                for attr in alembic_attrs:\n                    node_attr = \"{}.{}\".format(alembic_nodes[0], attr)\n                    data = {\n                        \"input\": lib.get_attribute_input(node_attr),\n                        \"value\": cmds.getAttr(node_attr)\n                    }\n\n                    alembic_data[attr] = data\n            else:\n                self.log.debug(\"No alembic nodes found in {}\".format(members))\n\n        try:\n            path = self.prepare_root_value(path,\n                                           representation[\"context\"]\n                                                         [\"project\"]\n                                                         [\"name\"])\n            content = cmds.file(path,\n                                loadReference=reference_node,\n                                type=file_type,\n                                returnNewNodes=True)\n        except RuntimeError as exc:\n            # When changing a reference to a file that has load errors the\n            # command will raise an error even if the file is still loaded\n            # correctly (e.g. when raising errors on Arnold attributes)\n            # When the file is loaded and has content, we consider it's fine.\n            if not cmds.referenceQuery(reference_node, isLoaded=True):\n                raise\n\n            content = cmds.referenceQuery(reference_node,\n                                          nodes=True,\n                                          dagPath=True)\n            if not content:\n                raise\n\n            self.log.warning(\"Ignoring file read error:\\n%s\", exc)\n\n        self._organize_containers(content, container[\"objectName\"])\n\n        # Reapply alembic settings.\n        if representation[\"name\"] == \"abc\" and alembic_data:\n            alembic_nodes = cmds.ls(\n                \"{}:*\".format(namespace), type=\"AlembicNode\"\n            )\n            if alembic_nodes:\n                alembic_node = alembic_nodes[0]  # assume single AlembicNode\n                for attr, data in alembic_data.items():\n                    node_attr = \"{}.{}\".format(alembic_node, attr)\n                    input = lib.get_attribute_input(node_attr)\n                    if data[\"input\"]:\n                        if data[\"input\"] != input:\n                            cmds.connectAttr(\n                                data[\"input\"], node_attr, force=True\n                            )\n                    else:\n                        if input:\n                            cmds.disconnectAttr(input, node_attr)\n                        cmds.setAttr(node_attr, data[\"value\"])\n\n        # Fix PLN-40 for older containers created with Avalon that had the\n        # `.verticesOnlySet` set to True.\n        if cmds.getAttr(\"{}.verticesOnlySet\".format(node)):\n            self.log.info(\"Setting %s.verticesOnlySet to False\", node)\n            cmds.setAttr(\"{}.verticesOnlySet\".format(node), False)\n\n        # Remove any placeHolderList attribute entries from the set that\n        # are remaining from nodes being removed from the referenced file.\n        members = cmds.sets(node, query=True)\n        invalid = [x for x in members if \".placeHolderList\" in x]\n        if invalid:\n            cmds.sets(invalid, remove=node)\n\n        # Update metadata\n        cmds.setAttr(\"{}.representation\".format(node),\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n\n        # When an animation or pointcache gets connected to an Xgen container,\n        # the compound attribute \"xgenContainers\" gets created. When animation\n        # containers gets updated we also need to update the cacheFileName on\n        # the Xgen collection.\n        compound_name = \"xgenContainers\"\n        if cmds.objExists(\"{}.{}\".format(node, compound_name)):\n            import xgenm\n            container_amount = cmds.getAttr(\n                \"{}.{}\".format(node, compound_name), size=True\n            )\n            # loop through all compound children\n            for i in range(container_amount):\n                attr = \"{}.{}[{}].container\".format(node, compound_name, i)\n                objectset = cmds.listConnections(attr)[0]\n                reference_node = cmds.sets(objectset, query=True)[0]\n                palettes = cmds.ls(\n                    cmds.referenceQuery(reference_node, nodes=True),\n                    type=\"xgmPalette\"\n                )\n                for palette in palettes:\n                    for description in xgenm.descriptions(palette):\n                        xgenm.setAttr(\n                            \"cacheFileName\",\n                            path.replace(\"\\\\\", \"/\"),\n                            palette,\n                            description,\n                            \"SplinePrimitive\"\n                        )\n\n            # Refresh UI and viewport.\n            de = xgenm.xgGlobal.DescriptionEditor\n            de.refresh(\"Full\")\n\n    def remove(self, container):\n        \"\"\"Remove an existing `container` from Maya scene\n\n        Deprecated; this functionality is replaced by `api.remove()`\n\n        Arguments:\n            container (openpype:container-1.0): Which container\n                to remove from scene.\n\n        \"\"\"\n        from maya import cmds\n\n        node = container[\"objectName\"]\n\n        # Assume asset has been referenced\n        members = cmds.sets(node, query=True)\n        reference_node = lib.get_reference_node(members, self.log)\n\n        assert reference_node, (\"Imported container not supported; \"\n                                \"container must be referenced.\")\n\n        self.log.info(\"Removing '%s' from Maya..\" % container[\"name\"])\n\n        namespace = cmds.referenceQuery(reference_node, namespace=True)\n        fname = cmds.referenceQuery(reference_node, filename=True)\n        cmds.file(fname, removeReference=True)\n\n        try:\n            cmds.delete(node)\n        except ValueError:\n            # Already implicitly deleted by Maya upon removing reference\n            pass\n\n        try:\n            # If container is not automatically cleaned up by May (issue #118)\n            cmds.namespace(removeNamespace=namespace,\n                           deleteNamespaceContent=True)\n        except RuntimeError:\n            pass\n\n    def prepare_root_value(self, file_url, project_name):\n        \"\"\"Replace root value with env var placeholder.\n\n        Use ${OPENPYPE_ROOT_WORK} (or any other root) instead of proper root\n        value when storing referenced url into a workfile.\n        Useful for remote workflows with SiteSync.\n\n        Args:\n            file_url (str)\n            project_name (dict)\n        Returns:\n            (str)\n        \"\"\"\n        settings = get_project_settings(project_name)\n        use_env_var_as_root = (settings[\"maya\"]\n                                       [\"maya-dirmap\"]\n                                       [\"use_env_var_as_root\"])\n        if use_env_var_as_root:\n            anatomy = Anatomy(project_name)\n            file_url = anatomy.replace_root_with_env_key(file_url, '${{{}}}')\n\n        return file_url\n\n    @staticmethod\n    def _organize_containers(nodes, container):\n        # type: (list, str) -> None\n        \"\"\"Put containers in loaded data to correct hierarchy.\"\"\"\n        for node in nodes:\n            id_attr = \"{}.id\".format(node)\n            if not cmds.attributeQuery(\"id\", node=node, exists=True):\n                continue\n            if cmds.getAttr(id_attr) == AVALON_CONTAINER_ID:\n                cmds.sets(node, forceElement=container)\n"
  },
  {
    "path": "openpype/hosts/maya/api/render_setup_tools.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Export stuff in render setup layer context.\n\nExport Maya nodes from Render Setup layer as if flattened in that layer instead\nof exporting the defaultRenderLayer as Maya forces by default\n\nCredits: Roy Nieterau (BigRoy) / Colorbleed\nModified for use in OpenPype\n\n\"\"\"\n\nimport os\nimport contextlib\n\nfrom maya import cmds\nfrom maya.app.renderSetup.model import renderSetup\n\nfrom .lib import pairwise\n\n\n@contextlib.contextmanager\ndef _allow_export_from_render_setup_layer():\n    \"\"\"Context manager to override Maya settings to allow RS layer export\"\"\"\n    try:\n\n        rs = renderSetup.instance()\n\n        # Exclude Render Setup nodes from the export\n        rs._setAllRSNodesDoNotWrite(True)\n\n        # Disable Render Setup forcing the switch to master layer\n        os.environ[\"MAYA_BATCH_RENDER_EXPORT\"] = \"1\"\n\n        yield\n\n    finally:\n        # Reset original state\n        rs._setAllRSNodesDoNotWrite(False)\n        os.environ.pop(\"MAYA_BATCH_RENDER_EXPORT\", None)\n\n\ndef export_in_rs_layer(path, nodes, export=None):\n    \"\"\"Export nodes from Render Setup layer.\n\n    When exporting from Render Setup layer Maya by default\n    forces a switch to the defaultRenderLayer as such making\n    it impossible to export the contents of a Render Setup\n    layer. Maya presents this warning message:\n        # Warning: Exporting Render Setup master layer content #\n\n    This function however avoids the renderlayer switch and\n    exports from the Render Setup layer as if the edits were\n    'flattened' in the master layer.\n\n    It does so by:\n        - Allowing export from Render Setup Layer\n        - Enforce Render Setup nodes to NOT be written on export\n        - Disconnect connections from any `applyOverride` nodes\n          to flatten the values (so they are written correctly)*\n    *Connection overrides like Shader Override and Material\n    Overrides export correctly out of the box since they don't\n    create an intermediate connection to an 'applyOverride' node.\n    However, any scalar override (absolute or relative override)\n    will get input connections in the layer so we'll break those\n    to 'store' the values on the attribute itself and write value\n    out instead.\n\n    Args:\n        path (str): File path to export to.\n        nodes (list): Maya nodes to export.\n        export (callable, optional): Callback to be used for exporting. If\n            not specified, default export to `.ma` will be called.\n\n    Returns:\n        None\n\n    Raises:\n        AssertionError: When not in a Render Setup layer an\n            AssertionError is raised. This command assumes\n            you are currently in a Render Setup layer.\n\n    \"\"\"\n    rs = renderSetup.instance()\n    assert rs.getVisibleRenderLayer().name() != \"defaultRenderLayer\", \\\n        (\"Export in Render Setup layer is only supported when in \"\n         \"Render Setup layer\")\n\n    # Break connection to any value overrides\n    history = cmds.listHistory(nodes) or []\n    nodes_all = list(\n        set(cmds.ls(nodes + history, long=True, objectsOnly=True)))\n    overrides = cmds.listConnections(nodes_all,\n                                     source=True,\n                                     destination=False,\n                                     type=\"applyOverride\",\n                                     plugs=True,\n                                     connections=True) or []\n    for dest, src in pairwise(overrides):\n        # Even after disconnecting the values\n        # should be preserved as they were\n        # Note: animated overrides would be lost for export\n        cmds.disconnectAttr(src, dest)\n\n    # Export Selected\n    with _allow_export_from_render_setup_layer():\n        cmds.select(nodes, noExpand=True)\n        if export:\n            export()\n        else:\n            cmds.file(path,\n                      force=True,\n                      typ=\"mayaAscii\",\n                      exportSelected=True,\n                      preserveReferences=False,\n                      channels=True,\n                      constraints=True,\n                      expressions=True,\n                      constructionHistory=True)\n\n    if overrides:\n        # If we have broken override connections then Maya\n        # is unaware that the Render Setup layer is in an\n        # invalid state. So let's 'hard reset' the state\n        # by going to default render layer and switching back\n        layer = rs.getVisibleRenderLayer()\n        rs.switchToLayer(None)\n        rs.switchToLayer(layer)\n"
  },
  {
    "path": "openpype/hosts/maya/api/setdress.py",
    "content": "import logging\nimport json\nimport os\n\nimport contextlib\nimport copy\n\nimport six\n\nfrom maya import cmds\n\nfrom openpype.client import (\n    get_version_by_name,\n    get_last_version_by_subset_id,\n    get_representation_by_id,\n    get_representation_by_name,\n    get_representation_parents,\n)\nfrom openpype.pipeline import (\n    schema,\n    discover_loader_plugins,\n    loaders_from_representation,\n    load_container,\n    update_container,\n    remove_container,\n    get_representation_path,\n    get_current_project_name,\n)\nfrom openpype.hosts.maya.api.lib import (\n    matrix_equals,\n    unique_namespace,\n    get_container_transforms,\n    DEFAULT_MATRIX\n)\n\nlog = logging.getLogger(\"PackageLoader\")\n\n\ndef to_namespace(node, namespace):\n    \"\"\"Return node name as if it's inside the namespace.\n\n    Args:\n        node (str): Node name\n        namespace (str): Namespace\n\n    Returns:\n        str: The node in the namespace.\n\n    \"\"\"\n    namespace_prefix = \"|{}:\".format(namespace)\n    node = namespace_prefix.join(node.split(\"|\"))\n    return node\n\n\n@contextlib.contextmanager\ndef namespaced(namespace, new=True):\n    \"\"\"Work inside namespace during context\n\n    Args:\n        new (bool): When enabled this will rename the namespace to a unique\n            namespace if the input namespace already exists.\n\n    Yields:\n        str: The namespace that is used during the context\n\n    \"\"\"\n    original = cmds.namespaceInfo(cur=True)\n    if new:\n        namespace = unique_namespace(namespace)\n        cmds.namespace(add=namespace)\n\n    try:\n        cmds.namespace(set=namespace)\n        yield namespace\n    finally:\n        cmds.namespace(set=original)\n\n\n@contextlib.contextmanager\ndef unlocked(nodes):\n\n    # Get node state by Maya's uuid\n    nodes = cmds.ls(nodes, long=True)\n    uuids = cmds.ls(nodes, uuid=True)\n    states = cmds.lockNode(nodes, query=True, lock=True)\n    states = {uuid: state for uuid, state in zip(uuids, states)}\n    originals = {uuid: node for uuid, node in zip(uuids, nodes)}\n\n    try:\n        cmds.lockNode(nodes, lock=False)\n        yield\n    finally:\n        # Reapply original states\n        _iteritems = getattr(states, \"iteritems\", states.items)\n        for uuid, state in _iteritems():\n            nodes_from_id = cmds.ls(uuid, long=True)\n            if nodes_from_id:\n                node = nodes_from_id[0]\n            else:\n                log.debug(\"Falling back to node name: %s\", node)\n                node = originals[uuid]\n                if not cmds.objExists(node):\n                    log.warning(\"Unable to find: %s\", node)\n                    continue\n            cmds.lockNode(node, lock=state)\n\n\ndef load_package(filepath, name, namespace=None):\n    \"\"\"Load a package that was gathered elsewhere.\n\n    A package is a group of published instances, possibly with additional data\n    in a hierarchy.\n\n    \"\"\"\n\n    if namespace is None:\n        # Define a unique namespace for the package\n        namespace = os.path.basename(filepath).split(\".\")[0]\n        unique_namespace(namespace)\n    assert isinstance(namespace, six.string_types)\n\n    # Load the setdress package data\n    with open(filepath, \"r\") as fp:\n        data = json.load(fp)\n\n    # Load the setdress alembic hierarchy\n    #   We import this into the namespace in which we'll load the package's\n    #   instances into afterwards.\n    alembic = filepath.replace(\".json\", \".abc\")\n    hierarchy = cmds.file(alembic,\n                          reference=True,\n                          namespace=namespace,\n                          returnNewNodes=True,\n                          groupReference=True,\n                          groupName=\"{}:{}\".format(namespace, name),\n                          typ=\"Alembic\")\n\n    # Get the top root node (the reference group)\n    root = \"{}:{}\".format(namespace, name)\n\n    containers = []\n    all_loaders = discover_loader_plugins()\n    for representation_id, instances in data.items():\n\n        # Find the compatible loaders\n        loaders = loaders_from_representation(\n            all_loaders, representation_id\n        )\n\n        for instance in instances:\n            container = _add(instance=instance,\n                             representation_id=representation_id,\n                             loaders=loaders,\n                             namespace=namespace,\n                             root=root)\n            containers.append(container)\n\n    # TODO: Do we want to cripple? Or do we want to add a 'parent' parameter?\n    # Cripple the original avalon containers so they don't show up in the\n    # manager\n    # for container in containers:\n    #     cmds.setAttr(\"%s.id\" % container,\n    #                  \"setdress.container\",\n    #                  type=\"string\")\n\n    # TODO: Lock all loaded nodes\n    #   This is to ensure the hierarchy remains unaltered by the artists\n    # for node in nodes:\n    #      cmds.lockNode(node, lock=True)\n\n    return containers + hierarchy\n\n\ndef _add(instance, representation_id, loaders, namespace, root=\"|\"):\n    \"\"\"Add an item from the package\n\n    Args:\n        instance (dict):\n        representation_id (str):\n        loaders (list):\n        namespace (str):\n\n    Returns:\n        str: The created Avalon container.\n\n    \"\"\"\n\n    # Process within the namespace\n    with namespaced(namespace, new=False) as namespace:\n\n        # Get the used loader\n        Loader = next((x for x in loaders if\n                       x.__name__ == instance['loader']),\n                      None)\n\n        if Loader is None:\n            log.warning(\"Loader is missing: %s. Skipping %s\",\n                        instance['loader'], instance)\n            raise RuntimeError(\"Loader is missing.\")\n\n        container = load_container(\n            Loader,\n            representation_id,\n            namespace=instance['namespace']\n        )\n\n        # Get the root from the loaded container\n        loaded_root = get_container_transforms({\"objectName\": container},\n                                               root=True)\n\n        # Apply matrix to root node (if any matrix edits)\n        matrix = instance.get(\"matrix\", None)\n        if matrix:\n            cmds.xform(loaded_root, objectSpace=True, matrix=matrix)\n\n        # Parent into the setdress hierarchy\n        # Namespace is missing from parent node(s), add namespace\n        # manually\n        parent = root + to_namespace(instance[\"parent\"], namespace)\n        cmds.parent(loaded_root, parent, relative=True)\n\n        return container\n\n\n# Store root nodes based on representation and namespace\ndef _instances_by_namespace(data):\n    \"\"\"Rebuild instance data so we can look it up by namespace.\n\n    Note that the `representation` is added into the instance's\n    data with a `representation` key.\n\n    Args:\n        data (dict): scene build data\n\n    Returns:\n        dict\n\n    \"\"\"\n    result = {}\n    # Add new assets\n    for representation_id, instances in data.items():\n\n        # Ensure we leave the source data unaltered\n        instances = copy.deepcopy(instances)\n        for instance in instances:\n            instance['representation'] = representation_id\n            result[instance['namespace']] = instance\n\n    return result\n\n\ndef get_contained_containers(container):\n    \"\"\"Get the Avalon containers in this container\n\n    Args:\n        container (dict): The container dict.\n\n    Returns:\n        list: A list of member container dictionaries.\n\n    \"\"\"\n\n    from .pipeline import parse_container\n\n    # Get avalon containers in this package setdress container\n    containers = []\n    members = cmds.sets(container['objectName'], query=True)\n    for node in cmds.ls(members, type=\"objectSet\"):\n        try:\n            member_container = parse_container(node)\n            containers.append(member_container)\n        except schema.ValidationError:\n            pass\n\n    return containers\n\n\ndef update_package_version(container, version):\n    \"\"\"\n    Update package by version number\n\n    Args:\n        container (dict): container data of the container node\n        version (int): the new version number of the package\n\n    Returns:\n        None\n\n    \"\"\"\n\n    # Versioning (from `core.maya.pipeline`)\n    project_name = get_current_project_name()\n    current_representation = get_representation_by_id(\n        project_name, container[\"representation\"]\n    )\n\n    assert current_representation is not None, \"This is a bug\"\n\n    version_doc, subset_doc, asset_doc, project_doc = (\n        get_representation_parents(project_name, current_representation)\n    )\n\n    if version == -1:\n        new_version = get_last_version_by_subset_id(\n            project_name, subset_doc[\"_id\"]\n        )\n    else:\n        new_version = get_version_by_name(\n            project_name, version, subset_doc[\"_id\"]\n        )\n\n    assert new_version is not None, \"This is a bug\"\n\n    # Get the new representation (new file)\n    new_representation = get_representation_by_name(\n        project_name, current_representation[\"name\"], new_version[\"_id\"]\n    )\n\n    update_package(container, new_representation)\n\n\ndef update_package(set_container, representation):\n    \"\"\"Update any matrix changes in the scene based on the new data\n\n    Args:\n        set_container (dict): container data from `ls()`\n        representation (dict): the representation document from the database\n\n    Returns:\n        None\n\n    \"\"\"\n\n    # Load the original package data\n    project_name = get_current_project_name()\n    current_representation = get_representation_by_id(\n        project_name, set_container[\"representation\"]\n    )\n\n    current_file = get_representation_path(current_representation)\n    assert current_file.endswith(\".json\")\n    with open(current_file, \"r\") as fp:\n        current_data = json.load(fp)\n\n    # Load the new package data\n    new_file = get_representation_path(representation)\n    assert new_file.endswith(\".json\")\n    with open(new_file, \"r\") as fp:\n        new_data = json.load(fp)\n\n    # Update scene content\n    containers = get_contained_containers(set_container)\n    update_scene(set_container, containers, current_data, new_data, new_file)\n\n    # TODO: This should be handled by the pipeline itself\n    cmds.setAttr(set_container['objectName'] + \".representation\",\n                 str(representation['_id']), type=\"string\")\n\n\ndef update_scene(set_container, containers, current_data, new_data, new_file):\n    \"\"\"Updates the hierarchy, assets and their matrix\n\n    Updates the following within the scene:\n        * Setdress hierarchy alembic\n        * Matrix\n        * Parenting\n        * Representations\n\n    It removes any assets which are not present in the new build data\n\n    Args:\n        set_container (dict): the setdress container of the scene\n        containers (list): the list of containers under the setdress container\n        current_data (dict): the current build data of the setdress\n        new_data (dict): the new build data of the setdres\n\n    Returns:\n        processed_containers (list): all new and updated containers\n\n    \"\"\"\n\n    set_namespace = set_container['namespace']\n    project_name = get_current_project_name()\n\n    # Update the setdress hierarchy alembic\n    set_root = get_container_transforms(set_container, root=True)\n    set_hierarchy_root = cmds.listRelatives(set_root, fullPath=True)[0]\n    set_hierarchy_reference = cmds.referenceQuery(set_hierarchy_root,\n                                                  referenceNode=True)\n    new_alembic = new_file.replace(\".json\", \".abc\")\n    assert os.path.exists(new_alembic), \"%s does not exist.\" % new_alembic\n    with unlocked(cmds.listRelatives(set_root, ad=True, fullPath=True)):\n        cmds.file(new_alembic,\n                  loadReference=set_hierarchy_reference,\n                  type=\"Alembic\")\n\n    identity = DEFAULT_MATRIX[:]\n\n    processed_namespaces = set()\n    processed_containers = list()\n\n    new_lookup = _instances_by_namespace(new_data)\n    old_lookup = _instances_by_namespace(current_data)\n    for container in containers:\n        container_ns = container['namespace']\n\n        # Consider it processed here, even it it fails we want to store that\n        # the namespace was already available.\n        processed_namespaces.add(container_ns)\n        processed_containers.append(container['objectName'])\n\n        if container_ns in new_lookup:\n            root = get_container_transforms(container, root=True)\n            if not root:\n                log.error(\"Can't find root for %s\", container['objectName'])\n                continue\n\n            old_instance = old_lookup.get(container_ns, {})\n            new_instance = new_lookup[container_ns]\n\n            # Update the matrix\n            # check matrix against old_data matrix to find local overrides\n            current_matrix = cmds.xform(root,\n                                        query=True,\n                                        matrix=True,\n                                        objectSpace=True)\n\n            original_matrix = old_instance.get(\"matrix\", identity)\n            has_matrix_override = not matrix_equals(current_matrix,\n                                                    original_matrix)\n\n            if has_matrix_override:\n                log.warning(\"Matrix override preserved on %s\", container_ns)\n            else:\n                new_matrix = new_instance.get(\"matrix\", identity)\n                cmds.xform(root, matrix=new_matrix, objectSpace=True)\n\n            # Update the parenting\n            if old_instance.get(\"parent\", None) != new_instance[\"parent\"]:\n\n                parent = to_namespace(new_instance['parent'], set_namespace)\n                if not cmds.objExists(parent):\n                    log.error(\"Can't find parent %s\", parent)\n                    continue\n\n                # Set the new parent\n                cmds.lockNode(root, lock=False)\n                root = cmds.parent(root, parent, relative=True)\n                cmds.lockNode(root, lock=True)\n\n            # Update the representation\n            representation_current = container['representation']\n            representation_old = old_instance['representation']\n            representation_new = new_instance['representation']\n            has_representation_override = (representation_current !=\n                                           representation_old)\n\n            if representation_new != representation_current:\n\n                if has_representation_override:\n                    log.warning(\"Your scene had local representation \"\n                                \"overrides within the set. New \"\n                                \"representations not loaded for %s.\",\n                                container_ns)\n                    continue\n\n                # We check it against the current 'loader' in the scene instead\n                # of the original data of the package that was loaded because\n                # an Artist might have made scene local overrides\n                if new_instance['loader'] != container['loader']:\n                    log.warning(\"Loader is switched - local edits will be \"\n                                \"lost. Removing: %s\",\n                                container_ns)\n\n                    # Remove this from the \"has been processed\" list so it's\n                    # considered as new element and added afterwards.\n                    processed_containers.pop()\n                    processed_namespaces.remove(container_ns)\n                    remove_container(container)\n                    continue\n\n                # Check whether the conversion can be done by the Loader.\n                # They *must* use the same asset, subset and Loader for\n                # `update_container` to make sense.\n                old = get_representation_by_id(\n                    project_name, representation_current\n                )\n                new = get_representation_by_id(\n                    project_name, representation_new\n                )\n                is_valid = compare_representations(old=old, new=new)\n                if not is_valid:\n                    log.error(\"Skipping: %s. See log for details.\",\n                              container_ns)\n                    continue\n\n                new_version = new[\"context\"][\"version\"]\n                update_container(container, version=new_version)\n\n        else:\n            # Remove this container because it's not in the new data\n            log.warning(\"Removing content: %s\", container_ns)\n            remove_container(container)\n\n    # Add new assets\n    all_loaders = discover_loader_plugins()\n    for representation_id, instances in new_data.items():\n\n        # Find the compatible loaders\n        loaders = loaders_from_representation(\n            all_loaders, representation_id\n        )\n        for instance in instances:\n\n            # Already processed in update functionality\n            if instance['namespace'] in processed_namespaces:\n                continue\n\n            container = _add(instance=instance,\n                             representation_id=representation_id,\n                             loaders=loaders,\n                             namespace=set_container['namespace'],\n                             root=set_root)\n\n            # Add to the setdress container\n            cmds.sets(container,\n                      addElement=set_container['objectName'])\n\n            processed_containers.append(container)\n\n    return processed_containers\n\n\ndef compare_representations(old, new):\n    \"\"\"Check if the old representation given can be updated\n\n    Due to limitations of the `update_container` function we cannot allow\n    differences in the following data:\n\n    * Representation name (extension)\n    * Asset name\n    * Subset name (variation)\n\n    If any of those data values differs, the function will raise an\n    RuntimeError\n\n    Args:\n        old(dict): representation data from the database\n        new(dict): representation data from the database\n\n    Returns:\n        bool: False if the representation is not invalid else True\n    \"\"\"\n\n    if new[\"name\"] != old[\"name\"]:\n        log.error(\"Cannot switch extensions\")\n        return False\n\n    new_context = new[\"context\"]\n    old_context = old[\"context\"]\n\n    if new_context[\"asset\"] != old_context[\"asset\"]:\n        log.error(\"Changing assets between updates is \"\n                  \"not supported.\")\n        return False\n\n    if new_context[\"subset\"] != old_context[\"subset\"]:\n        log.error(\"Changing subsets between updates is \"\n                  \"not supported.\")\n        return False\n\n    return True\n"
  },
  {
    "path": "openpype/hosts/maya/api/shader_definition_editor.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Editor for shader definitions.\n\nShader names are stored as simple text file over GridFS in mongodb.\n\n\"\"\"\nimport os\nfrom qtpy import QtWidgets, QtCore, QtGui\nfrom openpype.client.mongo import OpenPypeMongoConnection\nfrom openpype import resources\nimport gridfs\n\n\nDEFINITION_FILENAME = \"{}/maya/shader_definition.txt\".format(\n    os.getenv(\"AVALON_PROJECT\"))\n\n\nclass ShaderDefinitionsEditor(QtWidgets.QWidget):\n    \"\"\"Widget serving as simple editor for shader name definitions.\"\"\"\n\n    # name of the file used to store definitions\n\n    def __init__(self, parent=None):\n        super(ShaderDefinitionsEditor, self).__init__(parent)\n        self._mongo = OpenPypeMongoConnection.get_mongo_client()\n        self._gridfs = gridfs.GridFS(\n            self._mongo[os.getenv(\"OPENPYPE_DATABASE_NAME\")])\n        self._editor = None\n\n        self._original_content = self._read_definition_file()\n\n        self.setObjectName(\"shaderDefinitionEditor\")\n        self.setWindowTitle(\"OpenPype shader name definition editor\")\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n        self.setWindowFlags(QtCore.Qt.Window)\n        self.setParent(parent)\n        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)\n        self.resize(750, 500)\n\n        self._setup_ui()\n        self._reload()\n\n    def _setup_ui(self):\n        \"\"\"Setup UI of Widget.\"\"\"\n        layout = QtWidgets.QVBoxLayout(self)\n        label = QtWidgets.QLabel()\n        label.setText(\"Put shader names here - one name per line:\")\n        layout.addWidget(label)\n        self._editor = QtWidgets.QPlainTextEdit()\n        self._editor.setStyleSheet(\"border: none;\")\n        layout.addWidget(self._editor)\n\n        btn_layout = QtWidgets.QHBoxLayout()\n        save_btn = QtWidgets.QPushButton(\"Save\")\n        save_btn.clicked.connect(self._save)\n\n        reload_btn = QtWidgets.QPushButton(\"Reload\")\n        reload_btn.clicked.connect(self._reload)\n\n        exit_btn = QtWidgets.QPushButton(\"Exit\")\n        exit_btn.clicked.connect(self._close)\n\n        btn_layout.addWidget(reload_btn)\n        btn_layout.addWidget(save_btn)\n        btn_layout.addWidget(exit_btn)\n\n        layout.addLayout(btn_layout)\n\n    def _read_definition_file(self, file=None):\n        \"\"\"Read definition file from database.\n\n        Args:\n            file (gridfs.grid_file.GridOut, Optional): File to read. If not\n                set, new query will be issued to find it.\n\n        Returns:\n            str: Content of the file or empty string if file doesn't exist.\n\n        \"\"\"\n        content = \"\"\n        if not file:\n            file = self._gridfs.find_one(\n                {\"filename\": DEFINITION_FILENAME})\n        if not file:\n            print(\">>> [SNDE]: nothing in database yet\")\n            return content\n        content = file.read()\n        file.close()\n        return content\n\n    def _write_definition_file(self, content, force=False):\n        \"\"\"Write content as definition to file in database.\n\n        Before file is written, check is made if its content has not\n        changed. If is changed, warning is issued to user if he wants\n        it to overwrite. Note: GridFs doesn't allow changing file content.\n        You need to delete existing file and create new one.\n\n        Args:\n            content (str): Content to write.\n\n        Raises:\n            ContentException: If file is changed in database while\n                editor is running.\n        \"\"\"\n        file = self._gridfs.find_one(\n            {\"filename\": DEFINITION_FILENAME})\n        if file:\n            content_check = self._read_definition_file(file)\n            if content == content_check:\n                print(\">>> [SNDE]: content not changed\")\n                return\n            if self._original_content != content_check:\n                if not force:\n                    raise ContentException(\"Content changed\")\n            print(\">>> [SNDE]: overwriting data\")\n            file.close()\n            self._gridfs.delete(file._id)\n\n        file = self._gridfs.new_file(\n            filename=DEFINITION_FILENAME,\n            content_type='text/plain',\n            encoding='utf-8')\n        file.write(content)\n        file.close()\n        QtCore.QTimer.singleShot(200, self._reset_style)\n        self._editor.setStyleSheet(\"border: 1px solid #33AF65;\")\n        self._original_content = content\n\n    def _reset_style(self):\n        \"\"\"Reset editor style back.\n\n        Used to visually indicate save.\n\n        \"\"\"\n        self._editor.setStyleSheet(\"border: none;\")\n\n    def _close(self):\n        self.hide()\n\n    def closeEvent(self, event):\n        event.ignore()\n        self.hide()\n\n    def _reload(self):\n        print(\">>> [SNDE]: reloading\")\n        self._set_content(self._read_definition_file())\n\n    def _save(self):\n        try:\n            self._write_definition_file(content=self._editor.toPlainText())\n        except ContentException:\n            # content has changed meanwhile\n            print(\">>> [SNDE]: content has changed\")\n            self._show_overwrite_warning()\n\n    def _set_content(self, content):\n        self._editor.setPlainText(content)\n\n    def _show_overwrite_warning(self):\n        reply = QtWidgets.QMessageBox.question(\n            self,\n            \"Warning\",\n            (\"Content you are editing was changed meanwhile in database.\\n\"\n             \"Please, reload and solve the conflict.\"),\n            QtWidgets.QMessageBox.OK)\n\n        if reply == QtWidgets.QMessageBox.OK:\n            # do nothing\n            pass\n\n\nclass ContentException(Exception):\n    \"\"\"This is risen during save if file is changed in database.\"\"\"\n    pass\n"
  },
  {
    "path": "openpype/hosts/maya/api/workfile_template_builder.py",
    "content": "import json\n\nfrom maya import cmds\n\nfrom openpype.pipeline import registered_host, get_current_asset_name\nfrom openpype.pipeline.workfile.workfile_template_builder import (\n    TemplateAlreadyImported,\n    AbstractTemplateBuilder,\n    PlaceholderPlugin,\n    LoadPlaceholderItem,\n    PlaceholderLoadMixin,\n)\nfrom openpype.tools.workfile_template_build import (\n    WorkfileBuildPlaceholderDialog,\n)\n\nfrom .lib import read, imprint, get_reference_node, get_main_window\n\nPLACEHOLDER_SET = \"PLACEHOLDERS_SET\"\n\n\nclass MayaTemplateBuilder(AbstractTemplateBuilder):\n    \"\"\"Concrete implementation of AbstractTemplateBuilder for maya\"\"\"\n\n    use_legacy_creators = True\n\n    def import_template(self, path):\n        \"\"\"Import template into current scene.\n        Block if a template is already loaded.\n\n        Args:\n            path (str): A path to current template (usually given by\n            get_template_preset implementation)\n\n        Returns:\n            bool: Whether the template was successfully imported or not\n        \"\"\"\n\n        if cmds.objExists(PLACEHOLDER_SET):\n            raise TemplateAlreadyImported((\n                \"Build template already loaded\\n\"\n                \"Clean scene if needed (File > New Scene)\"\n            ))\n\n        cmds.sets(name=PLACEHOLDER_SET, empty=True)\n        new_nodes = cmds.file(\n            path,\n            i=True,\n            returnNewNodes=True,\n            preserveReferences=True,\n            loadReferenceDepth=\"all\",\n        )\n\n        # make default cameras non-renderable\n        default_cameras = [cam for cam in cmds.ls(cameras=True)\n                           if cmds.camera(cam, query=True, startupCamera=True)]\n        for cam in default_cameras:\n            if not cmds.attributeQuery(\"renderable\", node=cam, exists=True):\n                self.log.debug(\n                    \"Camera {} has no attribute 'renderable'\".format(cam)\n                )\n                continue\n            cmds.setAttr(\"{}.renderable\".format(cam), 0)\n\n        cmds.setAttr(PLACEHOLDER_SET + \".hiddenInOutliner\", True)\n\n        imported_sets = cmds.ls(new_nodes, set=True)\n        if not imported_sets:\n            return True\n\n        # update imported sets information\n        asset_name = get_current_asset_name()\n        for node in imported_sets:\n            if not cmds.attributeQuery(\"id\", node=node, exists=True):\n                continue\n            if cmds.getAttr(\"{}.id\".format(node)) != \"pyblish.avalon.instance\":\n                continue\n            if not cmds.attributeQuery(\"asset\", node=node, exists=True):\n                continue\n\n            cmds.setAttr(\n                \"{}.asset\".format(node), asset_name, type=\"string\")\n\n        return True\n\n\nclass MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin):\n    identifier = \"maya.load\"\n    label = \"Maya load\"\n\n    def _collect_scene_placeholders(self):\n        # Cache placeholder data to shared data\n        placeholder_nodes = self.builder.get_shared_populate_data(\n            \"placeholder_nodes\"\n        )\n        if placeholder_nodes is None:\n            attributes = cmds.ls(\"*.plugin_identifier\", long=True)\n            placeholder_nodes = {}\n            for attribute in attributes:\n                node_name = attribute.rpartition(\".\")[0]\n                placeholder_nodes[node_name] = (\n                    self._parse_placeholder_node_data(node_name)\n                )\n\n            self.builder.set_shared_populate_data(\n                \"placeholder_nodes\", placeholder_nodes\n            )\n        return placeholder_nodes\n\n    def _parse_placeholder_node_data(self, node_name):\n        placeholder_data = read(node_name)\n        parent_name = (\n            cmds.getAttr(node_name + \".parent\", asString=True)\n            or node_name.rpartition(\"|\")[0]\n            or \"\"\n        )\n        if parent_name:\n            siblings = cmds.listRelatives(parent_name, children=True)\n        else:\n            siblings = cmds.ls(assemblies=True)\n        node_shortname = node_name.rpartition(\"|\")[2]\n        current_index = cmds.getAttr(node_name + \".index\", asString=True)\n        if current_index < 0:\n            current_index = siblings.index(node_shortname)\n\n        placeholder_data.update({\n            \"parent\": parent_name,\n            \"index\": current_index\n        })\n        return placeholder_data\n\n    def _create_placeholder_name(self, placeholder_data):\n        placeholder_name_parts = placeholder_data[\"builder_type\"].split(\"_\")\n\n        pos = 1\n        # add family in any\n        placeholder_family = placeholder_data[\"family\"]\n        if placeholder_family:\n            placeholder_name_parts.insert(pos, placeholder_family)\n            pos += 1\n\n        # add loader arguments if any\n        loader_args = placeholder_data[\"loader_args\"]\n        if loader_args:\n            loader_args = json.loads(loader_args.replace('\\'', '\\\"'))\n            values = [v for v in loader_args.values()]\n            for value in values:\n                placeholder_name_parts.insert(pos, value)\n                pos += 1\n\n        placeholder_name = \"_\".join(placeholder_name_parts)\n\n        return placeholder_name.capitalize()\n\n    def _get_loaded_repre_ids(self):\n        loaded_representation_ids = self.builder.get_shared_populate_data(\n            \"loaded_representation_ids\"\n        )\n        if loaded_representation_ids is None:\n            try:\n                containers = cmds.sets(\"AVALON_CONTAINERS\", q=True)\n            except ValueError:\n                containers = []\n\n            loaded_representation_ids = {\n                cmds.getAttr(container + \".representation\")\n                for container in containers\n            }\n            self.builder.set_shared_populate_data(\n                \"loaded_representation_ids\", loaded_representation_ids\n            )\n        return loaded_representation_ids\n\n    def create_placeholder(self, placeholder_data):\n        selection = cmds.ls(selection=True)\n        if len(selection) > 1:\n            raise ValueError(\"More then one item are selected\")\n\n        parent = selection[0] if selection else None\n\n        placeholder_data[\"plugin_identifier\"] = self.identifier\n\n        placeholder_name = self._create_placeholder_name(placeholder_data)\n\n        placeholder = cmds.spaceLocator(name=placeholder_name)[0]\n        if parent:\n            placeholder = cmds.parent(placeholder, selection[0])[0]\n\n        imprint(placeholder, placeholder_data)\n\n        # Add helper attributes to keep placeholder info\n        cmds.addAttr(\n            placeholder,\n            longName=\"parent\",\n            hidden=True,\n            dataType=\"string\"\n        )\n        cmds.addAttr(\n            placeholder,\n            longName=\"index\",\n            hidden=True,\n            attributeType=\"short\",\n            defaultValue=-1\n        )\n\n        cmds.setAttr(placeholder + \".parent\", \"\", type=\"string\")\n\n    def update_placeholder(self, placeholder_item, placeholder_data):\n        node_name = placeholder_item.scene_identifier\n        new_values = {}\n        for key, value in placeholder_data.items():\n            placeholder_value = placeholder_item.data.get(key)\n            if value != placeholder_value:\n                new_values[key] = value\n                placeholder_item.data[key] = value\n\n        for key in new_values.keys():\n            cmds.deleteAttr(node_name + \".\" + key)\n\n        imprint(node_name, new_values)\n\n    def collect_placeholders(self):\n        output = []\n        scene_placeholders = self._collect_scene_placeholders()\n        for node_name, placeholder_data in scene_placeholders.items():\n            if placeholder_data.get(\"plugin_identifier\") != self.identifier:\n                continue\n\n            # TODO do data validations and maybe upgrades if they are invalid\n            output.append(\n                LoadPlaceholderItem(node_name, placeholder_data, self)\n            )\n\n        return output\n\n    def populate_placeholder(self, placeholder):\n        self.populate_load_placeholder(placeholder)\n\n    def repopulate_placeholder(self, placeholder):\n        repre_ids = self._get_loaded_repre_ids()\n        self.populate_load_placeholder(placeholder, repre_ids)\n\n    def get_placeholder_options(self, options=None):\n        return self.get_load_plugin_options(options)\n\n    def post_placeholder_process(self, placeholder, failed):\n        \"\"\"Cleanup placeholder after load of its corresponding representations.\n\n        Args:\n            placeholder (PlaceholderItem): Item which was just used to load\n                representation.\n            failed (bool): Loading of representation failed.\n        \"\"\"\n        # Hide placeholder and add them to placeholder set\n        node = placeholder.scene_identifier\n\n        cmds.sets(node, addElement=PLACEHOLDER_SET)\n        cmds.hide(node)\n        cmds.setAttr(node + \".hiddenInOutliner\", True)\n\n    def delete_placeholder(self, placeholder):\n        \"\"\"Remove placeholder if building was successful\"\"\"\n        cmds.delete(placeholder.scene_identifier)\n\n    def load_succeed(self, placeholder, container):\n        self._parent_in_hierarchy(placeholder, container)\n\n    def _parent_in_hierarchy(self, placeholder, container):\n        \"\"\"Parent loaded container to placeholder's parent.\n\n        ie : Set loaded content as placeholder's sibling\n\n        Args:\n            container (str): Placeholder loaded containers\n        \"\"\"\n\n        if not container:\n            return\n\n        roots = cmds.sets(container, q=True)\n        ref_node = None\n        try:\n            ref_node = get_reference_node(roots)\n        except AssertionError as e:\n            self.log.info(e.args[0])\n\n        nodes_to_parent = []\n        for root in roots:\n            if ref_node:\n                ref_root = cmds.referenceQuery(root, nodes=True)[0]\n                ref_root = (\n                    cmds.listRelatives(ref_root, parent=True, path=True) or\n                    [ref_root]\n                )\n                nodes_to_parent.extend(ref_root)\n                continue\n            if root.endswith(\"_RN\"):\n                # Backwards compatibility for hardcoded reference names.\n                refRoot = cmds.referenceQuery(root, n=True)[0]\n                refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot]\n                nodes_to_parent.extend(refRoot)\n            elif root not in cmds.listSets(allSets=True):\n                nodes_to_parent.append(root)\n\n            elif not cmds.sets(root, q=True):\n                return\n\n        # Move loaded nodes to correct index in outliner hierarchy\n        placeholder_form = cmds.xform(\n            placeholder.scene_identifier,\n            q=True,\n            matrix=True,\n            worldSpace=True\n        )\n        scene_parent = cmds.listRelatives(\n            placeholder.scene_identifier, parent=True, fullPath=True\n        )\n        for node in set(nodes_to_parent):\n            cmds.reorder(node, front=True)\n            cmds.reorder(node, relative=placeholder.data[\"index\"])\n            cmds.xform(node, matrix=placeholder_form, ws=True)\n            if scene_parent:\n                cmds.parent(node, scene_parent)\n            else:\n                cmds.parent(node, world=True)\n\n        holding_sets = cmds.listSets(object=placeholder.scene_identifier)\n        if not holding_sets:\n            return\n        for holding_set in holding_sets:\n            cmds.sets(roots, forceElement=holding_set)\n\n\ndef build_workfile_template(*args):\n    builder = MayaTemplateBuilder(registered_host())\n    builder.build_template()\n\n\ndef update_workfile_template(*args):\n    builder = MayaTemplateBuilder(registered_host())\n    builder.rebuild_template()\n\n\ndef create_placeholder(*args):\n    host = registered_host()\n    builder = MayaTemplateBuilder(host)\n    window = WorkfileBuildPlaceholderDialog(host, builder,\n                                            parent=get_main_window())\n    window.show()\n\n\ndef update_placeholder(*args):\n    host = registered_host()\n    builder = MayaTemplateBuilder(host)\n    placeholder_items_by_id = {\n        placeholder_item.scene_identifier: placeholder_item\n        for placeholder_item in builder.get_placeholders()\n    }\n    placeholder_items = []\n    for node_name in cmds.ls(selection=True, long=True):\n        if node_name in placeholder_items_by_id:\n            placeholder_items.append(placeholder_items_by_id[node_name])\n\n    # TODO show UI at least\n    if len(placeholder_items) == 0:\n        raise ValueError(\"No node selected\")\n\n    if len(placeholder_items) > 1:\n        raise ValueError(\"Too many selected nodes\")\n\n    placeholder_item = placeholder_items[0]\n    window = WorkfileBuildPlaceholderDialog(host, builder,\n                                            parent=get_main_window())\n    window.set_update_mode(placeholder_item)\n    window.exec_()\n"
  },
  {
    "path": "openpype/hosts/maya/api/workio.py",
    "content": "\"\"\"Host API required Work Files tool\"\"\"\nimport os\nfrom maya import cmds\n\n\ndef file_extensions():\n    return [\".ma\", \".mb\"]\n\n\ndef has_unsaved_changes():\n    return cmds.file(query=True, modified=True)\n\n\ndef save_file(filepath):\n    cmds.file(rename=filepath)\n    ext = os.path.splitext(filepath)[1]\n    if ext == \".mb\":\n        file_type = \"mayaBinary\"\n    else:\n        file_type = \"mayaAscii\"\n    cmds.file(save=True, type=file_type)\n\n\ndef open_file(filepath):\n    return cmds.file(filepath, open=True, force=True)\n\n\ndef current_file():\n\n    current_filepath = cmds.file(query=True, sceneName=True)\n    if not current_filepath:\n        return None\n\n    return current_filepath\n\n\ndef work_root(session):\n    work_dir = session[\"AVALON_WORKDIR\"]\n    scene_dir = None\n\n    # Query scene file rule from workspace.mel if it exists in WORKDIR\n    # We are parsing the workspace.mel manually as opposed to temporarily\n    # setting the Workspace in Maya in a context manager since Maya had a\n    # tendency to crash on frequently changing the workspace when this\n    # function was called many times as one scrolled through Work Files assets.\n    workspace_mel = os.path.join(work_dir, \"workspace.mel\")\n    if os.path.exists(workspace_mel):\n        scene_rule = 'workspace -fr \"scene\" '\n        # We need to use builtins as `open` is overridden by the workio API\n        open_file = __builtins__[\"open\"]\n        with open_file(workspace_mel, \"r\") as f:\n            for line in f:\n                if line.strip().startswith(scene_rule):\n                    # remainder == \"rule\";\n                    remainder = line[len(scene_rule):]\n                    # scene_dir == rule\n                    scene_dir = remainder.split('\"')[1]\n    else:\n        # We can't query a workspace that does not exist\n        # so we return similar to what we do in other hosts.\n        scene_dir = session.get(\"AVALON_SCENEDIR\")\n\n    if scene_dir:\n        return os.path.join(work_dir, scene_dir)\n    else:\n        return work_dir\n"
  },
  {
    "path": "openpype/hosts/maya/hooks/pre_auto_load_plugins.py",
    "content": "from openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass MayaPreAutoLoadPlugins(PreLaunchHook):\n    \"\"\"Define -noAutoloadPlugins command flag.\"\"\"\n\n    # Before AddLastWorkfileToLaunchArgs\n    order = 9\n    app_groups = {\"maya\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n\n        # Ignore if there's no last workfile to start.\n        if not self.data.get(\"start_last_workfile\"):\n            return\n\n        maya_settings = self.data[\"project_settings\"][\"maya\"]\n        enabled = maya_settings[\"explicit_plugins_loading\"][\"enabled\"]\n        if enabled:\n            # Force disable the `AddLastWorkfileToLaunchArgs`.\n            self.data.pop(\"start_last_workfile\")\n\n            # Force post initialization so our dedicated plug-in load can run\n            # prior to Maya opening a scene file.\n            key = \"OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION\"\n            self.launch_context.env[key] = \"1\"\n\n            self.log.debug(\"Explicit plugins loading.\")\n            self.launch_context.launch_args.append(\"-noAutoloadPlugins\")\n"
  },
  {
    "path": "openpype/hosts/maya/hooks/pre_copy_mel.py",
    "content": "from openpype.lib.applications import PreLaunchHook, LaunchTypes\nfrom openpype.hosts.maya.lib import create_workspace_mel\n\n\nclass PreCopyMel(PreLaunchHook):\n    \"\"\"Copy workspace.mel to workdir.\n\n    Hook `GlobalHostDataHook` must be executed before this hook.\n    \"\"\"\n    app_groups = {\"maya\", \"mayapy\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        project_doc = self.data[\"project_doc\"]\n        workdir = self.launch_context.env.get(\"AVALON_WORKDIR\")\n        if not workdir:\n            self.log.warning(\"BUG: Workdir is not filled.\")\n            return\n\n        project_settings = self.data[\"project_settings\"]\n        create_workspace_mel(workdir, project_doc[\"name\"], project_settings)\n"
  },
  {
    "path": "openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py",
    "content": "from openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass MayaPreOpenWorkfilePostInitialization(PreLaunchHook):\n    \"\"\"Define whether open last workfile should run post initialize.\"\"\"\n\n    # Before AddLastWorkfileToLaunchArgs.\n    order = 9\n    app_groups = {\"maya\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n\n        # Ignore if there's no last workfile to start.\n        if not self.data.get(\"start_last_workfile\"):\n            return\n\n        maya_settings = self.data[\"project_settings\"][\"maya\"]\n        enabled = maya_settings[\"open_workfile_post_initialization\"]\n        if enabled:\n            # Force disable the `AddLastWorkfileToLaunchArgs`.\n            self.data.pop(\"start_last_workfile\")\n\n            self.log.debug(\"Opening workfile post initialization.\")\n            key = \"OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION\"\n            self.launch_context.env[key] = \"1\"\n"
  },
  {
    "path": "openpype/hosts/maya/lib.py",
    "content": "import os\nfrom openpype.settings import get_project_settings\nfrom openpype.lib import Logger\n\n\ndef create_workspace_mel(workdir, project_name, project_settings=None):\n    dst_filepath = os.path.join(workdir, \"workspace.mel\")\n    if os.path.exists(dst_filepath):\n        return\n\n    if not os.path.exists(workdir):\n        os.makedirs(workdir)\n\n    if not project_settings:\n        project_settings = get_project_settings(project_name)\n    mel_script = project_settings[\"maya\"].get(\"mel_workspace\")\n\n    # Skip if mel script in settings is empty\n    if not mel_script:\n        log = Logger.get_logger(\"create_workspace_mel\")\n        log.debug(\"File 'workspace.mel' not created. Settings value is empty.\")\n        return\n\n    with open(dst_filepath, \"w\") as mel_file:\n        mel_file.write(mel_script)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/maya/plugins/create/convert_legacy.py",
    "content": "from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin\nfrom openpype.hosts.maya.api import plugin\nfrom openpype.hosts.maya.api.lib import read\n\nfrom openpype.client import get_asset_by_name\n\nfrom maya import cmds\nfrom maya.app.renderSetup.model import renderSetup\n\n\nclass MayaLegacyConvertor(SubsetConvertorPlugin,\n                          plugin.MayaCreatorBase):\n    \"\"\"Find and convert any legacy subsets in the scene.\n\n    This Convertor will find all legacy subsets in the scene and will\n    transform them to the current system. Since the old subsets doesn't\n    retain any information about their original creators, the only mapping\n    we can do is based on their families.\n\n    Its limitation is that you can have multiple creators creating subset\n    of the same family and there is no way to handle it. This code should\n    nevertheless cover all creators that came with OpenPype.\n\n    \"\"\"\n    identifier = \"io.openpype.creators.maya.legacy\"\n\n    # Cases where the identifier or new family doesn't correspond to the\n    # original family on the legacy instances\n    special_family_conversions = {\n        \"rendering\": \"io.openpype.creators.maya.renderlayer\",\n    }\n\n    def find_instances(self):\n\n        self.cache_subsets(self.collection_shared_data)\n        legacy = self.collection_shared_data.get(\"maya_cached_legacy_subsets\")\n        if not legacy:\n            return\n\n        self.add_convertor_item(\"Convert legacy instances\")\n\n    def convert(self):\n        self.remove_convertor_item()\n\n        # We can't use the collected shared data cache here\n        # we re-query it here directly to convert all found.\n        cache = {}\n        self.cache_subsets(cache)\n        legacy = cache.get(\"maya_cached_legacy_subsets\")\n        if not legacy:\n            return\n\n        # From all current new style manual creators find the mapping\n        # from family to identifier\n        family_to_id = {}\n        for identifier, creator in self.create_context.creators.items():\n            family = getattr(creator, \"family\", None)\n            if not family:\n                continue\n\n            if family in family_to_id:\n                # We have a clash of family -> identifier. Multiple\n                # new style creators use the same family\n                self.log.warning(\"Clash on family->identifier: \"\n                                 \"{}\".format(identifier))\n            family_to_id[family] = identifier\n\n        family_to_id.update(self.special_family_conversions)\n\n        # We also embed the current 'task' into the instance since legacy\n        # instances didn't store that data on the instances. The old style\n        # logic was thus to be live to the current task to begin with.\n        data = dict()\n        data[\"task\"] = self.create_context.get_current_task_name()\n        for family, instance_nodes in legacy.items():\n            if family not in family_to_id:\n                self.log.warning(\n                    \"Unable to convert legacy instance with family '{}'\"\n                    \" because there is no matching new creator's family\"\n                    \"\".format(family)\n                )\n                continue\n\n            creator_id = family_to_id[family]\n            creator = self.create_context.creators[creator_id]\n            data[\"creator_identifier\"] = creator_id\n\n            if isinstance(creator, plugin.RenderlayerCreator):\n                self._convert_per_renderlayer(instance_nodes, data, creator)\n            else:\n                self._convert_regular(instance_nodes, data)\n\n    def _convert_regular(self, instance_nodes, data):\n        # We only imprint the creator identifier for it to identify\n        # as the new style creator\n        for instance_node in instance_nodes:\n            self.imprint_instance_node(instance_node,\n                                       data=data.copy())\n\n    def _convert_per_renderlayer(self, instance_nodes, data, creator):\n        # Split the instance into an instance per layer\n        rs = renderSetup.instance()\n        layers = rs.getRenderLayers()\n        if not layers:\n            self.log.error(\n                \"Can't convert legacy renderlayer instance because no existing\"\n                \" renderSetup layers exist in the scene.\"\n            )\n            return\n\n        creator_attribute_names = {\n            attr_def.key for attr_def in creator.get_instance_attr_defs()\n        }\n\n        for instance_node in instance_nodes:\n\n            # Ensure we have the new style singleton node generated\n            # TODO: Make function public\n            singleton_node = creator._get_singleton_node()\n            if singleton_node:\n                self.log.error(\n                    \"Can't convert legacy renderlayer instance '{}' because\"\n                    \" new style instance '{}' already exists\".format(\n                        instance_node,\n                        singleton_node\n                    )\n                )\n                continue\n\n            creator.create_singleton_node()\n\n            # We are creating new nodes to replace the original instance\n            # Copy the attributes of the original instance to the new node\n            original_data = read(instance_node)\n\n            # The family gets converted to the new family (this is due to\n            # \"rendering\" family being converted to \"renderlayer\" family)\n            original_data[\"family\"] = creator.family\n\n            # recreate subset name as without it would be\n            # `renderingMain` vs correct `renderMain`\n            project_name = self.create_context.get_current_project_name()\n            asset_doc = get_asset_by_name(project_name,\n                                          original_data[\"asset\"])\n            subset_name = creator.get_subset_name(\n                original_data[\"variant\"],\n                data[\"task\"],\n                asset_doc,\n                project_name)\n            original_data[\"subset\"] = subset_name\n\n            # Convert to creator attributes when relevant\n            creator_attributes = {}\n            for key in list(original_data.keys()):\n                # Iterate in order of the original attributes to preserve order\n                # in the output creator attributes\n                if key in creator_attribute_names:\n                    creator_attributes[key] = original_data.pop(key)\n            original_data[\"creator_attributes\"] = creator_attributes\n\n            # For layer in maya layers\n            for layer in layers:\n                layer_instance_node = creator.find_layer_instance_node(layer)\n                if not layer_instance_node:\n                    # TODO: Make function public\n                    layer_instance_node = creator._create_layer_instance_node(\n                        layer\n                    )\n\n                # Transfer the main attributes of the original instance\n                layer_data = original_data.copy()\n                layer_data.update(data)\n\n                self.imprint_instance_node(layer_instance_node,\n                                           data=layer_data)\n\n            # Delete the legacy instance node\n            cmds.delete(instance_node)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_animation_pointcache.py",
    "content": "from maya import cmds\n\nfrom openpype.hosts.maya.api import lib, plugin\n\nfrom openpype.lib import (\n    BoolDef,\n    NumberDef,\n)\nfrom openpype.pipeline import CreatedInstance\n\n\ndef _get_animation_attr_defs(cls):\n    \"\"\"Get Animation generic definitions.\"\"\"\n    defs = lib.collect_animation_defs()\n    defs.extend(\n        [\n            BoolDef(\"farm\", label=\"Submit to Farm\"),\n            NumberDef(\"priority\", label=\"Farm job Priority\", default=50),\n            BoolDef(\"refresh\", label=\"Refresh viewport during export\"),\n            BoolDef(\n                \"includeParentHierarchy\", label=\"Include Parent Hierarchy\"\n            ),\n            BoolDef(\n                \"includeUserDefinedAttributes\",\n                label=\"Include User Defined Attributes\"\n            ),\n        ]\n    )\n\n    return defs\n\n\ndef extract_alembic_attributes(node_data, class_name):\n    \"\"\"This is a legacy transfer of creator attributes to publish attributes\n    for ExtractAlembic/ExtractAnimation plugin.\n    \"\"\"\n    publish_attributes = node_data[\"publish_attributes\"]\n\n    if class_name in publish_attributes:\n        return node_data\n\n    extract_alembic_flags = [\n        \"writeColorSets\",\n        \"writeFaceSets\",\n        \"writeNormals\",\n        \"renderableOnly\",\n        \"visibleOnly\",\n        \"worldSpace\",\n        \"renderableOnly\"\n    ]\n    extract_alembic_attributes = [\n        \"attr\",\n        \"attrPrefix\",\n        \"visibleOnly\"\n    ]\n    attributes = extract_alembic_flags + extract_alembic_attributes\n    plugin_attributes = {\"flags\": []}\n    for attr in attributes:\n        if attr not in node_data[\"creator_attributes\"].keys():\n            continue\n        value = node_data[\"creator_attributes\"].pop(attr)\n\n        if value and attr in extract_alembic_flags:\n            plugin_attributes[\"flags\"].append(attr)\n\n        if attr in extract_alembic_attributes:\n            plugin_attributes[attr] = value\n\n    publish_attributes[class_name] = plugin_attributes\n\n    return node_data\n\n\nclass CreateAnimation(plugin.MayaHiddenCreator):\n    \"\"\"Animation output for character rigs\n\n    We hide the animation creator from the UI since the creation of it is\n    automated upon loading a rig. There's an inventory action to recreate it\n    for loaded rigs if by chance someone deleted the animation instance.\n    \"\"\"\n\n    identifier = \"io.openpype.creators.maya.animation\"\n    name = \"animationDefault\"\n    label = \"Animation\"\n    family = \"animation\"\n    icon = \"male\"\n\n    write_color_sets = False\n    write_face_sets = False\n    include_parent_hierarchy = False\n    include_user_defined_attributes = False\n\n    def collect_instances(self):\n        try:\n            cached_subsets = self.collection_shared_data[\"maya_cached_subsets\"]\n        except KeyError:\n            self.cache_subsets(self.collection_shared_data)\n            cached_subsets = self.collection_shared_data[\"maya_cached_subsets\"]\n\n        for node in cached_subsets.get(self.identifier, []):\n            node_data = self.read_instance_node(node)\n\n            node_data = extract_alembic_attributes(\n                node_data, \"ExtractAnimation\"\n            )\n\n            created_instance = CreatedInstance.from_existing(node_data, self)\n            self._add_instance_to_context(created_instance)\n\n    def get_instance_attr_defs(self):\n        super(CreateAnimation, self).get_instance_attr_defs()\n        defs = _get_animation_attr_defs(self)\n        return defs\n\n\nclass CreatePointCache(plugin.MayaCreator):\n    \"\"\"Alembic pointcache for animated data\"\"\"\n\n    identifier = \"io.openpype.creators.maya.pointcache\"\n    label = \"Pointcache\"\n    family = \"pointcache\"\n    icon = \"gears\"\n    write_color_sets = False\n    write_face_sets = False\n    include_user_defined_attributes = False\n\n    def collect_instances(self):\n        try:\n            cached_subsets = self.collection_shared_data[\"maya_cached_subsets\"]\n        except KeyError:\n            self.cache_subsets(self.collection_shared_data)\n            cached_subsets = self.collection_shared_data[\"maya_cached_subsets\"]\n\n        for node in cached_subsets.get(self.identifier, []):\n            node_data = self.read_instance_node(node)\n\n            node_data = extract_alembic_attributes(node_data, \"ExtractAlembic\")\n\n            created_instance = CreatedInstance.from_existing(node_data, self)\n            self._add_instance_to_context(created_instance)\n\n    def get_instance_attr_defs(self):\n        super(CreatePointCache, self).get_instance_attr_defs()\n        defs = _get_animation_attr_defs(self)\n        return defs\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        instance = super(CreatePointCache, self).create(\n            subset_name, instance_data, pre_create_data\n        )\n        instance_node = instance.get(\"instance_node\")\n\n        # For Arnold standin proxy\n        proxy_set = cmds.sets(name=instance_node + \"_proxy_SET\", empty=True)\n        cmds.sets(proxy_set, forceElement=instance_node)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_arnold_scene_source.py",
    "content": "from maya import cmds\n\nfrom openpype.hosts.maya.api import (\n    lib,\n    plugin\n)\nfrom openpype.lib import (\n    NumberDef,\n    BoolDef\n)\n\n\nclass CreateArnoldSceneSource(plugin.MayaCreator):\n    \"\"\"Arnold Scene Source\"\"\"\n\n    identifier = \"io.openpype.creators.maya.ass\"\n    label = \"Arnold Scene Source\"\n    family = \"ass\"\n    icon = \"cube\"\n    settings_name = \"CreateAss\"\n\n    expandProcedurals = False\n    motionBlur = True\n    motionBlurKeys = 2\n    motionBlurLength = 0.5\n    maskOptions = False\n    maskCamera = False\n    maskLight = False\n    maskShape = False\n    maskShader = False\n    maskOverride = False\n    maskDriver = False\n    maskFilter = False\n    maskColor_manager = False\n    maskOperator = False\n\n    def get_instance_attr_defs(self):\n\n        defs = lib.collect_animation_defs()\n\n        defs.extend([\n            BoolDef(\"expandProcedural\",\n                    label=\"Expand Procedural\",\n                    default=self.expandProcedurals),\n            BoolDef(\"motionBlur\",\n                    label=\"Motion Blur\",\n                    default=self.motionBlur),\n            NumberDef(\"motionBlurKeys\",\n                      label=\"Motion Blur Keys\",\n                      decimals=0,\n                      default=self.motionBlurKeys),\n            NumberDef(\"motionBlurLength\",\n                      label=\"Motion Blur Length\",\n                      decimals=3,\n                      default=self.motionBlurLength),\n\n            # Masks\n            BoolDef(\"maskOptions\",\n                    label=\"Export Options\",\n                    default=self.maskOptions),\n            BoolDef(\"maskCamera\",\n                    label=\"Export Cameras\",\n                    default=self.maskCamera),\n            BoolDef(\"maskLight\",\n                    label=\"Export Lights\",\n                    default=self.maskLight),\n            BoolDef(\"maskShape\",\n                    label=\"Export Shapes\",\n                    default=self.maskShape),\n            BoolDef(\"maskShader\",\n                    label=\"Export Shaders\",\n                    default=self.maskShader),\n            BoolDef(\"maskOverride\",\n                    label=\"Export Override Nodes\",\n                    default=self.maskOverride),\n            BoolDef(\"maskDriver\",\n                    label=\"Export Drivers\",\n                    default=self.maskDriver),\n            BoolDef(\"maskFilter\",\n                    label=\"Export Filters\",\n                    default=self.maskFilter),\n            BoolDef(\"maskOperator\",\n                    label=\"Export Operators\",\n                    default=self.maskOperator),\n            BoolDef(\"maskColor_manager\",\n                    label=\"Export Color Managers\",\n                    default=self.maskColor_manager),\n        ])\n\n        return defs\n\n\nclass CreateArnoldSceneSourceProxy(CreateArnoldSceneSource):\n    \"\"\"Arnold Scene Source Proxy\n\n    This product type facilitates working with proxy geometry in the viewport.\n    \"\"\"\n\n    identifier = \"io.openpype.creators.maya.assproxy\"\n    label = \"Arnold Scene Source Proxy\"\n    family = \"assProxy\"\n    icon = \"cube\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        instance = super(CreateArnoldSceneSource, self).create(\n            subset_name, instance_data, pre_create_data\n        )\n\n        instance_node = instance.get(\"instance_node\")\n\n        proxy = cmds.sets(name=instance_node + \"_proxy_SET\", empty=True)\n        cmds.sets([proxy], forceElement=instance_node)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_assembly.py",
    "content": "from openpype.hosts.maya.api import plugin\n\n\nclass CreateAssembly(plugin.MayaCreator):\n    \"\"\"A grouped package of loaded content\"\"\"\n\n    identifier = \"io.openpype.creators.maya.assembly\"\n    label = \"Assembly\"\n    family = \"assembly\"\n    icon = \"cubes\"\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_camera.py",
    "content": "from openpype.hosts.maya.api import (\n    lib,\n    plugin\n)\nfrom openpype.lib import BoolDef\n\n\nclass CreateCamera(plugin.MayaCreator):\n    \"\"\"Single baked camera\"\"\"\n\n    identifier = \"io.openpype.creators.maya.camera\"\n    label = \"Camera\"\n    family = \"camera\"\n    icon = \"video-camera\"\n\n    def get_instance_attr_defs(self):\n\n        defs = lib.collect_animation_defs()\n\n        defs.extend([\n            BoolDef(\"bakeToWorldSpace\",\n                    label=\"Bake to World-Space\",\n                    tooltip=\"Bake to World-Space\",\n                    default=True),\n        ])\n\n        return defs\n\n\nclass CreateCameraRig(plugin.MayaCreator):\n    \"\"\"Complex hierarchy with camera.\"\"\"\n\n    identifier = \"io.openpype.creators.maya.camerarig\"\n    label = \"Camera Rig\"\n    family = \"camerarig\"\n    icon = \"video-camera\"\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_layout.py",
    "content": "from openpype.hosts.maya.api import plugin\nfrom openpype.lib import BoolDef\n\n\nclass CreateLayout(plugin.MayaCreator):\n    \"\"\"A grouped package of loaded content\"\"\"\n\n    identifier = \"io.openpype.creators.maya.layout\"\n    label = \"Layout\"\n    family = \"layout\"\n    icon = \"cubes\"\n\n    def get_instance_attr_defs(self):\n\n        return [\n            BoolDef(\"groupLoadedAssets\",\n                    label=\"Group Loaded Assets\",\n                    tooltip=\"Enable this when you want to publish group of \"\n                            \"loaded asset\",\n                    default=False)\n        ]\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_look.py",
    "content": "from openpype.hosts.maya.api import (\n    plugin,\n    lib\n)\nfrom openpype.lib import (\n    BoolDef,\n    TextDef\n)\n\n\nclass CreateLook(plugin.MayaCreator):\n    \"\"\"Shader connections defining shape look\"\"\"\n\n    identifier = \"io.openpype.creators.maya.look\"\n    label = \"Look\"\n    family = \"look\"\n    icon = \"paint-brush\"\n\n    make_tx = True\n    rs_tex = False\n\n    def get_instance_attr_defs(self):\n\n        return [\n            # TODO: This value should actually get set on create!\n            TextDef(\"renderLayer\",\n                    # TODO: Bug: Hidden attribute's label is still shown in UI?\n                    hidden=True,\n                    default=lib.get_current_renderlayer(),\n                    label=\"Renderlayer\",\n                    tooltip=\"Renderlayer to extract the look from\"),\n            BoolDef(\"maketx\",\n                    label=\"MakeTX\",\n                    tooltip=\"Whether to generate .tx files for your textures\",\n                    default=self.make_tx),\n            BoolDef(\"rstex\",\n                    label=\"Convert textures to .rstex\",\n                    tooltip=\"Whether to generate Redshift .rstex files for \"\n                            \"your textures\",\n                    default=self.rs_tex)\n        ]\n\n    def get_pre_create_attr_defs(self):\n        # Show same attributes on create but include use selection\n        defs = super(CreateLook, self).get_pre_create_attr_defs()\n        defs.extend(self.get_instance_attr_defs())\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_matchmove.py",
    "content": "from openpype.hosts.maya.api import (\n    lib,\n    plugin\n)\nfrom openpype.lib import BoolDef\n\n\nclass CreateMatchmove(plugin.MayaCreator):\n    \"\"\"Instance for more complex setup of cameras.\n\n    Might contain multiple cameras, geometries etc.\n\n    It is expected to be extracted into .abc or .ma\n    \"\"\"\n\n    identifier = \"io.openpype.creators.maya.matchmove\"\n    label = \"Matchmove\"\n    family = \"matchmove\"\n    icon = \"video-camera\"\n\n    def get_instance_attr_defs(self):\n\n        defs = lib.collect_animation_defs()\n\n        defs.extend([\n            BoolDef(\"bakeToWorldSpace\",\n                    label=\"Bake Cameras to World-Space\",\n                    tooltip=\"Bake Cameras to World-Space\",\n                    default=True),\n        ])\n\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_maya_usd.py",
    "content": "from openpype.hosts.maya.api import plugin, lib\nfrom openpype.lib import (\n    BoolDef,\n    EnumDef,\n    TextDef\n)\n\nfrom maya import cmds\n\n\nclass CreateMayaUsd(plugin.MayaCreator):\n    \"\"\"Create Maya USD Export\"\"\"\n\n    identifier = \"io.openpype.creators.maya.mayausd\"\n    label = \"Maya USD\"\n    family = \"usd\"\n    icon = \"cubes\"\n    description = \"Create Maya USD Export\"\n\n    cache = {}\n\n    def get_publish_families(self):\n        return [\"usd\", \"mayaUsd\"]\n\n    def get_instance_attr_defs(self):\n\n        if \"jobContextItems\" not in self.cache:\n            # Query once instead of per instance\n            job_context_items = {}\n            try:\n                cmds.loadPlugin(\"mayaUsdPlugin\", quiet=True)\n                job_context_items = {\n                    cmds.mayaUSDListJobContexts(jobContext=name): name\n                    for name in cmds.mayaUSDListJobContexts(export=True) or []\n                }\n            except RuntimeError:\n                # Likely `mayaUsdPlugin` plug-in not available\n                self.log.warning(\"Unable to retrieve available job \"\n                                 \"contexts for `mayaUsdPlugin` exports\")\n\n            if not job_context_items:\n                # enumdef multiselection may not be empty\n                job_context_items = [\"<placeholder; do not use>\"]\n\n            self.cache[\"jobContextItems\"] = job_context_items\n\n        defs = lib.collect_animation_defs()\n        defs.extend([\n            EnumDef(\"defaultUSDFormat\",\n                    label=\"File format\",\n                    items={\n                        \"usdc\": \"Binary\",\n                        \"usda\": \"ASCII\"\n                    },\n                    default=\"usdc\"),\n            BoolDef(\"stripNamespaces\",\n                    label=\"Strip Namespaces\",\n                    tooltip=(\n                        \"Remove namespaces during export. By default, \"\n                        \"namespaces are exported to the USD file in the \"\n                        \"following format: nameSpaceExample_pPlatonic1\"\n                    ),\n                    default=True),\n            BoolDef(\"mergeTransformAndShape\",\n                    label=\"Merge Transform and Shape\",\n                    tooltip=(\n                        \"Combine Maya transform and shape into a single USD\"\n                        \"prim that has transform and geometry, for all\"\n                        \" \\\"geometric primitives\\\" (gprims).\\n\"\n                        \"This results in smaller and faster scenes. Gprims \"\n                        \"will be \\\"unpacked\\\" back into transform and shape \"\n                        \"nodes when imported into Maya from USD.\"\n                    ),\n                    default=True),\n            BoolDef(\"includeUserDefinedAttributes\",\n                    label=\"Include User Defined Attributes\",\n                    tooltip=(\n                        \"Whether to include all custom maya attributes found \"\n                        \"on nodes as metadata (userProperties) in USD.\"\n                    ),\n                    default=False),\n            TextDef(\"attr\",\n                    label=\"Custom Attributes\",\n                    default=\"\",\n                    placeholder=\"attr1, attr2\"),\n            TextDef(\"attrPrefix\",\n                    label=\"Custom Attributes Prefix\",\n                    default=\"\",\n                    placeholder=\"prefix1, prefix2\"),\n            EnumDef(\"jobContext\",\n                    label=\"Job Context\",\n                    items=self.cache[\"jobContextItems\"],\n                    tooltip=(\n                        \"Specifies an additional export context to handle.\\n\"\n                        \"These usually contain extra schemas, primitives,\\n\"\n                        \"and materials that are to be exported for a \"\n                        \"specific\\ntask, a target renderer for example.\"\n                    ),\n                    multiselection=True),\n        ])\n\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_mayascene.py",
    "content": "from openpype.hosts.maya.api import plugin\n\n\nclass CreateMayaScene(plugin.MayaCreator):\n    \"\"\"Raw Maya Scene file export\"\"\"\n\n    identifier = \"io.openpype.creators.maya.mayascene\"\n    name = \"mayaScene\"\n    label = \"Maya Scene\"\n    family = \"mayaScene\"\n    icon = \"file-archive-o\"\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_model.py",
    "content": "from openpype.hosts.maya.api import plugin\nfrom openpype.lib import (\n    BoolDef,\n    TextDef\n)\n\n\nclass CreateModel(plugin.MayaCreator):\n    \"\"\"Polygonal static geometry\"\"\"\n\n    identifier = \"io.openpype.creators.maya.model\"\n    label = \"Model\"\n    family = \"model\"\n    icon = \"cube\"\n    default_variants = [\"Main\", \"Proxy\", \"_MD\", \"_HD\", \"_LD\"]\n\n    write_color_sets = False\n    write_face_sets = False\n\n    def get_instance_attr_defs(self):\n\n        return [\n            BoolDef(\"writeColorSets\",\n                    label=\"Write vertex colors\",\n                    tooltip=\"Write vertex colors with the geometry\",\n                    default=self.write_color_sets),\n            BoolDef(\"writeFaceSets\",\n                    label=\"Write face sets\",\n                    tooltip=\"Write face sets with the geometry\",\n                    default=self.write_face_sets),\n            BoolDef(\"includeParentHierarchy\",\n                    label=\"Include Parent Hierarchy\",\n                    tooltip=\"Whether to include parent hierarchy of nodes in \"\n                            \"the publish instance\",\n                    default=False),\n            TextDef(\"attr\",\n                    label=\"Custom Attributes\",\n                    default=\"\",\n                    placeholder=\"attr1, attr2\"),\n            TextDef(\"attrPrefix\",\n                    label=\"Custom Attributes Prefix\",\n                    placeholder=\"prefix1, prefix2\")\n        ]\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_multishot_layout.py",
    "content": "from ayon_api import (\n    get_folder_by_name,\n    get_folder_by_path,\n    get_folders,\n)\nfrom maya import cmds  # noqa: F401\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import get_assets\nfrom openpype.hosts.maya.api import plugin\nfrom openpype.lib import BoolDef, EnumDef, TextDef\nfrom openpype.pipeline import (\n    Creator,\n    get_current_asset_name,\n    get_current_project_name,\n)\nfrom openpype.pipeline.create import CreatorError\n\n\nclass CreateMultishotLayout(plugin.MayaCreator):\n    \"\"\"Create a multi-shot layout in the Maya scene.\n\n    This creator will create a Camera Sequencer in the Maya scene based on\n    the shots found under the specified folder. The shots will be added to\n    the sequencer in the order of their clipIn and clipOut values. For each\n    shot a Layout will be created.\n\n    \"\"\"\n    identifier = \"io.openpype.creators.maya.multishotlayout\"\n    label = \"Multi-shot Layout\"\n    family = \"layout\"\n    icon = \"project-diagram\"\n\n    def get_pre_create_attr_defs(self):\n        # Present artist with a list of parents of the current context\n        # to choose from. This will be used to get the shots under the\n        # selected folder to create the Camera Sequencer.\n\n        \"\"\"\n        Todo: `get_folder_by_name` should be switched to `get_folder_by_path`\n              once the fork to pure AYON is done.\n\n        Warning: this will not work for projects where the asset name\n                 is not unique across the project until the switch mentioned\n                 above is done.\n        \"\"\"\n\n        project_name = get_current_project_name()\n        folder_path = get_current_asset_name()\n        if \"/\" in folder_path:\n            current_folder = get_folder_by_path(project_name, folder_path)\n        else:\n            current_folder = get_folder_by_name(\n                project_name, folder_name=folder_path\n            )\n\n        current_path_parts = current_folder[\"path\"].split(\"/\")\n\n        # populate the list with parents of the current folder\n        # this will create menu items like:\n        # [\n        #   {\n        #       \"value\": \"\",\n        #       \"label\": \"project (shots directly under the project)\"\n        #   }, {\n        #       \"value\": \"shots/shot_01\", \"label\": \"shot_01 (current)\"\n        #   }, {\n        #       \"value\": \"shots\", \"label\": \"shots\"\n        #   }\n        # ]\n\n        # add the project as the first item\n        items_with_label = [\n            {\n                \"label\": f\"{self.project_name} \"\n                         \"(shots directly under the project)\",\n                \"value\": \"\"\n            }\n        ]\n\n        # go through the current folder path and add each part to the list,\n        # but mark the current folder.\n        for part_idx, part in enumerate(current_path_parts):\n            label = part\n            if label == current_folder[\"name\"]:\n                label = f\"{label} (current)\"\n\n            value = \"/\".join(current_path_parts[:part_idx + 1])\n\n            items_with_label.append({\"label\": label, \"value\": value})\n\n        return [\n            EnumDef(\"shotParent\",\n                    default=current_folder[\"name\"],\n                    label=\"Shot Parent Folder\",\n                    items=items_with_label,\n                    ),\n            BoolDef(\"groupLoadedAssets\",\n                    label=\"Group Loaded Assets\",\n                    tooltip=\"Enable this when you want to publish group of \"\n                            \"loaded asset\",\n                    default=False),\n            TextDef(\"taskName\",\n                    label=\"Associated Task Name\",\n                    tooltip=(\"Task name to be associated \"\n                             \"with the created Layout\"),\n                    default=\"layout\"),\n        ]\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        shots = list(\n            self.get_related_shots(folder_path=pre_create_data[\"shotParent\"])\n        )\n        if not shots:\n            # There are no shot folders under the specified folder.\n            # We are raising an error here but in the future we might\n            # want to create a new shot folders by publishing the layouts\n            # and shot defined in the sequencer. Sort of editorial publish\n            # in side of Maya.\n            raise CreatorError((\n                \"No shots found under the specified \"\n                f\"folder: {pre_create_data['shotParent']}.\"))\n\n        # Get layout creator\n        layout_creator_id = \"io.openpype.creators.maya.layout\"\n        layout_creator: Creator = self.create_context.creators.get(\n            layout_creator_id)\n        if not layout_creator:\n            raise CreatorError(\n                f\"Creator {layout_creator_id} not found.\")\n\n        # Get OpenPype style asset documents for the shots\n        op_asset_docs = get_assets(\n            self.project_name, [s[\"id\"] for s in shots])\n        asset_docs_by_id = {doc[\"_id\"]: doc for doc in op_asset_docs}\n        for shot in shots:\n            # we are setting shot name to be displayed in the sequencer to\n            # `shot name (shot label)` if the label is set, otherwise just\n            # `shot name`. So far, labels are used only when the name is set\n            # with characters that are not allowed in the shot name.\n            if not shot[\"active\"]:\n                continue\n\n            # get task for shot\n            asset_doc = asset_docs_by_id[shot[\"id\"]]\n\n            tasks = asset_doc.get(\"data\").get(\"tasks\").keys()\n            layout_task = None\n            if pre_create_data[\"taskName\"] in tasks:\n                layout_task = pre_create_data[\"taskName\"]\n\n            shot_name = f\"{shot['name']}%s\" % (\n                f\" ({shot['label']})\" if shot[\"label\"] else \"\")\n            cmds.shot(sequenceStartTime=shot[\"attrib\"][\"clipIn\"],\n                      sequenceEndTime=shot[\"attrib\"][\"clipOut\"],\n                      shotName=shot_name)\n\n            # Create layout instance by the layout creator\n\n            instance_data = {\n                \"folderPath\": shot[\"path\"],\n                \"variant\": layout_creator.get_default_variant()\n            }\n            if layout_task:\n                instance_data[\"task\"] = layout_task\n\n            layout_creator.create(\n                subset_name=layout_creator.get_subset_name(\n                    layout_creator.get_default_variant(),\n                    self.create_context.get_current_task_name(),\n                    asset_doc,\n                    self.project_name),\n                instance_data=instance_data,\n                pre_create_data={\n                    \"groupLoadedAssets\": pre_create_data[\"groupLoadedAssets\"]\n                }\n            )\n\n    def get_related_shots(self, folder_path: str):\n        \"\"\"Get all shots related to the current asset.\n\n        Get all folders of type Shot under specified folder.\n\n        Args:\n            folder_path (str): Path of the folder.\n\n        Returns:\n            list: List of dicts with folder data.\n\n        \"\"\"\n        # if folder_path is None, project is selected as a root\n        # and its name is used as a parent id\n        parent_id = self.project_name\n        if folder_path:\n            current_folder = get_folder_by_path(\n                project_name=self.project_name,\n                folder_path=folder_path,\n            )\n            parent_id = current_folder[\"id\"]\n\n        # get all child folders of the current one\n        return get_folders(\n            project_name=self.project_name,\n            parent_ids=[parent_id],\n            fields=[\n                \"attrib.clipIn\", \"attrib.clipOut\",\n                \"attrib.frameStart\", \"attrib.frameEnd\",\n                \"name\", \"label\", \"path\", \"folderType\", \"id\"\n            ]\n        )\n\n\n# blast this creator if Ayon server is not enabled\nif not AYON_SERVER_ENABLED:\n    del CreateMultishotLayout\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_multiverse_look.py",
    "content": "from openpype.hosts.maya.api import plugin\nfrom openpype.lib import (\n    BoolDef,\n    EnumDef\n)\n\n\nclass CreateMultiverseLook(plugin.MayaCreator):\n    \"\"\"Create Multiverse Look\"\"\"\n\n    identifier = \"io.openpype.creators.maya.mvlook\"\n    label = \"Multiverse Look\"\n    family = \"mvLook\"\n    icon = \"cubes\"\n\n    def get_instance_attr_defs(self):\n\n        return [\n            EnumDef(\"fileFormat\",\n                    label=\"File Format\",\n                    tooltip=\"USD export file format\",\n                    items=[\"usda\", \"usd\"],\n                    default=\"usda\"),\n            BoolDef(\"publishMipMap\",\n                    label=\"Publish MipMap\",\n                    default=True),\n        ]\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_multiverse_usd.py",
    "content": "from openpype.hosts.maya.api import plugin, lib\nfrom openpype.lib import (\n    BoolDef,\n    NumberDef,\n    TextDef,\n    EnumDef\n)\n\n\nclass CreateMultiverseUsd(plugin.MayaCreator):\n    \"\"\"Create Multiverse USD Asset\"\"\"\n\n    identifier = \"io.openpype.creators.maya.mvusdasset\"\n    label = \"Multiverse USD Asset\"\n    family = \"usd\"\n    icon = \"cubes\"\n    description = \"Create Multiverse USD Asset\"\n\n    def get_publish_families(self):\n        return [\"usd\", \"mvUsd\"]\n\n    def get_instance_attr_defs(self):\n\n        defs = lib.collect_animation_defs(fps=True)\n        defs.extend([\n            EnumDef(\"fileFormat\",\n                    label=\"File format\",\n                    items=[\"usd\", \"usda\", \"usdz\"],\n                    default=\"usd\"),\n            BoolDef(\"stripNamespaces\",\n                    label=\"Strip Namespaces\",\n                    default=True),\n            BoolDef(\"mergeTransformAndShape\",\n                    label=\"Merge Transform and Shape\",\n                    default=False),\n            BoolDef(\"writeAncestors\",\n                    label=\"Write Ancestors\",\n                    default=True),\n            BoolDef(\"flattenParentXforms\",\n                    label=\"Flatten Parent Xforms\",\n                    default=False),\n            BoolDef(\"writeSparseOverrides\",\n                    label=\"Write Sparse Overrides\",\n                    default=False),\n            BoolDef(\"useMetaPrimPath\",\n                    label=\"Use Meta Prim Path\",\n                    default=False),\n            TextDef(\"customRootPath\",\n                    label=\"Custom Root Path\",\n                    default=''),\n            TextDef(\"customAttributes\",\n                    label=\"Custom Attributes\",\n                    tooltip=\"Comma-separated list of attribute names\",\n                    default=''),\n            TextDef(\"nodeTypesToIgnore\",\n                    label=\"Node Types to Ignore\",\n                    tooltip=\"Comma-separated list of node types to be ignored\",\n                    default=''),\n            BoolDef(\"writeMeshes\",\n                    label=\"Write Meshes\",\n                    default=True),\n            BoolDef(\"writeCurves\",\n                    label=\"Write Curves\",\n                    default=True),\n            BoolDef(\"writeParticles\",\n                    label=\"Write Particles\",\n                    default=True),\n            BoolDef(\"writeCameras\",\n                    label=\"Write Cameras\",\n                    default=False),\n            BoolDef(\"writeLights\",\n                    label=\"Write Lights\",\n                    default=False),\n            BoolDef(\"writeJoints\",\n                    label=\"Write Joints\",\n                    default=False),\n            BoolDef(\"writeCollections\",\n                    label=\"Write Collections\",\n                    default=False),\n            BoolDef(\"writePositions\",\n                    label=\"Write Positions\",\n                    default=True),\n            BoolDef(\"writeNormals\",\n                    label=\"Write Normals\",\n                    default=True),\n            BoolDef(\"writeUVs\",\n                    label=\"Write UVs\",\n                    default=True),\n            BoolDef(\"writeColorSets\",\n                    label=\"Write Color Sets\",\n                    default=False),\n            BoolDef(\"writeTangents\",\n                    label=\"Write Tangents\",\n                    default=False),\n            BoolDef(\"writeRefPositions\",\n                    label=\"Write Ref Positions\",\n                    default=True),\n            BoolDef(\"writeBlendShapes\",\n                    label=\"Write BlendShapes\",\n                    default=False),\n            BoolDef(\"writeDisplayColor\",\n                    label=\"Write Display Color\",\n                    default=True),\n            BoolDef(\"writeSkinWeights\",\n                    label=\"Write Skin Weights\",\n                    default=False),\n            BoolDef(\"writeMaterialAssignment\",\n                    label=\"Write Material Assignment\",\n                    default=False),\n            BoolDef(\"writeHardwareShader\",\n                    label=\"Write Hardware Shader\",\n                    default=False),\n            BoolDef(\"writeShadingNetworks\",\n                    label=\"Write Shading Networks\",\n                    default=False),\n            BoolDef(\"writeTransformMatrix\",\n                    label=\"Write Transform Matrix\",\n                    default=True),\n            BoolDef(\"writeUsdAttributes\",\n                    label=\"Write USD Attributes\",\n                    default=True),\n            BoolDef(\"writeInstancesAsReferences\",\n                    label=\"Write Instances as References\",\n                    default=False),\n            BoolDef(\"timeVaryingTopology\",\n                    label=\"Time Varying Topology\",\n                    default=False),\n            TextDef(\"customMaterialNamespace\",\n                    label=\"Custom Material Namespace\",\n                    default=''),\n            NumberDef(\"numTimeSamples\",\n                      label=\"Num Time Samples\",\n                      default=1),\n            NumberDef(\"timeSamplesSpan\",\n                      label=\"Time Samples Span\",\n                      default=0.0),\n        ])\n\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_multiverse_usd_comp.py",
    "content": "from openpype.hosts.maya.api import plugin, lib\nfrom openpype.lib import (\n    BoolDef,\n    NumberDef,\n    EnumDef\n)\n\n\nclass CreateMultiverseUsdComp(plugin.MayaCreator):\n    \"\"\"Create Multiverse USD Composition\"\"\"\n\n    identifier = \"io.openpype.creators.maya.mvusdcomposition\"\n    label = \"Multiverse USD Composition\"\n    family = \"mvUsdComposition\"\n    icon = \"cubes\"\n\n    def get_instance_attr_defs(self):\n\n        defs = lib.collect_animation_defs(fps=True)\n        defs.extend([\n            EnumDef(\"fileFormat\",\n                    label=\"File format\",\n                    items=[\"usd\", \"usda\"],\n                    default=\"usd\"),\n            BoolDef(\"stripNamespaces\",\n                    label=\"Strip Namespaces\",\n                    default=False),\n            BoolDef(\"mergeTransformAndShape\",\n                    label=\"Merge Transform and Shape\",\n                    default=False),\n            BoolDef(\"flattenContent\",\n                    label=\"Flatten Content\",\n                    default=False),\n            BoolDef(\"writeAsCompoundLayers\",\n                    label=\"Write As Compound Layers\",\n                    default=False),\n            BoolDef(\"writePendingOverrides\",\n                    label=\"Write Pending Overrides\",\n                    default=False),\n            NumberDef(\"numTimeSamples\",\n                      label=\"Num Time Samples\",\n                      default=1),\n            NumberDef(\"timeSamplesSpan\",\n                      label=\"Time Samples Span\",\n                      default=0.0),\n        ])\n\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py",
    "content": "from openpype.hosts.maya.api import plugin, lib\nfrom openpype.lib import (\n    BoolDef,\n    NumberDef,\n    EnumDef\n)\n\n\nclass CreateMultiverseUsdOver(plugin.MayaCreator):\n    \"\"\"Create Multiverse USD Override\"\"\"\n\n    identifier = \"io.openpype.creators.maya.mvusdoverride\"\n    label = \"Multiverse USD Override\"\n    family = \"mvUsdOverride\"\n    icon = \"cubes\"\n\n    def get_instance_attr_defs(self):\n        defs = lib.collect_animation_defs(fps=True)\n        defs.extend([\n            EnumDef(\"fileFormat\",\n                    label=\"File format\",\n                    items=[\"usd\", \"usda\"],\n                    default=\"usd\"),\n            BoolDef(\"writeAll\",\n                    label=\"Write All\",\n                    default=False),\n            BoolDef(\"writeTransforms\",\n                    label=\"Write Transforms\",\n                    default=True),\n            BoolDef(\"writeVisibility\",\n                    label=\"Write Visibility\",\n                    default=True),\n            BoolDef(\"writeAttributes\",\n                    label=\"Write Attributes\",\n                    default=True),\n            BoolDef(\"writeMaterials\",\n                    label=\"Write Materials\",\n                    default=True),\n            BoolDef(\"writeVariants\",\n                    label=\"Write Variants\",\n                    default=True),\n            BoolDef(\"writeVariantsDefinition\",\n                    label=\"Write Variants Definition\",\n                    default=True),\n            BoolDef(\"writeActiveState\",\n                    label=\"Write Active State\",\n                    default=True),\n            BoolDef(\"writeNamespaces\",\n                    label=\"Write Namespaces\",\n                    default=False),\n            NumberDef(\"numTimeSamples\",\n                      label=\"Num Time Samples\",\n                      default=1),\n            NumberDef(\"timeSamplesSpan\",\n                      label=\"Time Samples Span\",\n                      default=0.0),\n        ])\n\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_proxy_abc.py",
    "content": "from openpype.hosts.maya.api import (\n    lib,\n    plugin\n)\nfrom openpype.lib import (\n    BoolDef,\n    TextDef\n)\n\n\nclass CreateProxyAlembic(plugin.MayaCreator):\n    \"\"\"Proxy Alembic for animated data\"\"\"\n\n    identifier = \"io.openpype.creators.maya.proxyabc\"\n    label = \"Proxy Alembic\"\n    family = \"proxyAbc\"\n    icon = \"gears\"\n    write_color_sets = False\n    write_face_sets = False\n\n    def get_instance_attr_defs(self):\n\n        defs = lib.collect_animation_defs()\n\n        defs.extend([\n            BoolDef(\"writeColorSets\",\n                    label=\"Write vertex colors\",\n                    tooltip=\"Write vertex colors with the geometry\",\n                    default=self.write_color_sets),\n            BoolDef(\"writeFaceSets\",\n                    label=\"Write face sets\",\n                    tooltip=\"Write face sets with the geometry\",\n                    default=self.write_face_sets),\n            BoolDef(\"worldSpace\",\n                    label=\"World-Space Export\",\n                    default=True),\n            TextDef(\"nameSuffix\",\n                    label=\"Name Suffix for Bounding Box\",\n                    default=\"_BBox\",\n                    placeholder=\"_BBox\"),\n            TextDef(\"attr\",\n                    label=\"Custom Attributes\",\n                    default=\"\",\n                    placeholder=\"attr1, attr2\"),\n            TextDef(\"attrPrefix\",\n                    label=\"Custom Attributes Prefix\",\n                    placeholder=\"prefix1, prefix2\")\n        ])\n\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_redshift_proxy.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator of Redshift proxy subset types.\"\"\"\n\nfrom openpype.hosts.maya.api import plugin, lib\nfrom openpype.lib import BoolDef\n\n\nclass CreateRedshiftProxy(plugin.MayaCreator):\n    \"\"\"Create instance of Redshift Proxy subset.\"\"\"\n\n    identifier = \"io.openpype.creators.maya.redshiftproxy\"\n    label = \"Redshift Proxy\"\n    family = \"redshiftproxy\"\n    icon = \"gears\"\n\n    def get_instance_attr_defs(self):\n\n        defs = [\n            BoolDef(\"animation\",\n                    label=\"Export animation\",\n                    default=False)\n        ]\n\n        defs.extend(lib.collect_animation_defs())\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_render.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Create ``Render`` instance in Maya.\"\"\"\n\nfrom openpype.hosts.maya.api import (\n    lib_rendersettings,\n    plugin\n)\nfrom openpype.pipeline import CreatorError\nfrom openpype.lib import (\n    BoolDef,\n    NumberDef,\n)\n\n\nclass CreateRenderlayer(plugin.RenderlayerCreator):\n    \"\"\"Create and manages renderlayer subset per renderLayer in workfile.\n\n    This generates a single node in the scene which tells the Creator to if\n    it exists collect Maya rendersetup renderlayers as individual instances.\n    As such, triggering create doesn't actually create the instance node per\n    layer but only the node which tells the Creator it may now collect\n    the renderlayers.\n\n    \"\"\"\n\n    identifier = \"io.openpype.creators.maya.renderlayer\"\n    family = \"renderlayer\"\n    label = \"Render\"\n    icon = \"eye\"\n\n    layer_instance_prefix = \"render\"\n    singleton_node_name = \"renderingMain\"\n\n    render_settings = {}\n\n    @classmethod\n    def apply_settings(cls, project_settings):\n        cls.render_settings = project_settings[\"maya\"][\"RenderSettings\"]\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # Only allow a single render instance to exist\n        if self._get_singleton_node():\n            raise CreatorError(\"A Render instance already exists - only \"\n                               \"one can be configured.\")\n\n        # Apply default project render settings on create\n        if self.render_settings.get(\"apply_render_settings\"):\n            lib_rendersettings.RenderSettings().set_default_renderer_settings()\n\n        super(CreateRenderlayer, self).create(subset_name,\n                                              instance_data,\n                                              pre_create_data)\n\n    def get_instance_attr_defs(self):\n        \"\"\"Create instance settings.\"\"\"\n\n        return [\n            BoolDef(\"review\",\n                    label=\"Review\",\n                    tooltip=\"Mark as reviewable\",\n                    default=True),\n            BoolDef(\"extendFrames\",\n                    label=\"Extend Frames\",\n                    tooltip=\"Extends the frames on top of the previous \"\n                            \"publish.\\nIf the previous was 1001-1050 and you \"\n                            \"would now submit 1020-1070 only the new frames \"\n                            \"1051-1070 would be rendered and published \"\n                            \"together with the previously rendered frames.\\n\"\n                            \"If 'overrideExistingFrame' is enabled it *will* \"\n                            \"render any existing frames.\",\n                    default=False),\n            BoolDef(\"overrideExistingFrame\",\n                    label=\"Override Existing Frame\",\n                    tooltip=\"Override existing rendered frames \"\n                            \"(if they exist).\",\n                    default=True),\n\n            # TODO: Should these move to submit_maya_deadline plugin?\n            # Tile rendering\n            BoolDef(\"tileRendering\",\n                    label=\"Enable tiled rendering\",\n                    default=False),\n            NumberDef(\"tilesX\",\n                      label=\"Tiles X\",\n                      default=2,\n                      minimum=1,\n                      decimals=0),\n            NumberDef(\"tilesY\",\n                      label=\"Tiles Y\",\n                      default=2,\n                      minimum=1,\n                      decimals=0),\n\n            # Additional settings\n            BoolDef(\"convertToScanline\",\n                    label=\"Convert to Scanline\",\n                    tooltip=\"Convert the output images to scanline images\",\n                    default=False),\n            BoolDef(\"useReferencedAovs\",\n                    label=\"Use Referenced AOVs\",\n                    tooltip=\"Consider the AOVs from referenced scenes as well\",\n                    default=False),\n\n            BoolDef(\"renderSetupIncludeLights\",\n                    label=\"Render Setup Include Lights\",\n                    default=self.render_settings.get(\"enable_all_lights\",\n                                                     False))\n        ]\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_rendersetup.py",
    "content": "from openpype.hosts.maya.api import plugin\nfrom openpype.pipeline import CreatorError\n\n\nclass CreateRenderSetup(plugin.MayaCreator):\n    \"\"\"Create rendersetup template json data\"\"\"\n\n    identifier = \"io.openpype.creators.maya.rendersetup\"\n    label = \"Render Setup Preset\"\n    family = \"rendersetup\"\n    icon = \"tablet\"\n\n    def get_pre_create_attr_defs(self):\n        # Do not show the \"use_selection\" setting from parent class\n        return []\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        existing_instance = None\n        for instance in self.create_context.instances:\n            if instance.family == self.family:\n                existing_instance = instance\n                break\n\n        if existing_instance:\n            raise CreatorError(\"A RenderSetup instance already exists - only \"\n                               \"one can be configured.\")\n\n        super(CreateRenderSetup, self).create(subset_name,\n                                              instance_data,\n                                              pre_create_data)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_review.py",
    "content": "import json\n\nfrom maya import cmds\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.hosts.maya.api import (\n    lib,\n    plugin\n)\nfrom openpype.lib import (\n    BoolDef,\n    NumberDef,\n    EnumDef\n)\nfrom openpype.pipeline import CreatedInstance\nfrom openpype.client import get_asset_by_name\n\nTRANSPARENCIES = [\n    \"preset\",\n    \"simple\",\n    \"object sorting\",\n    \"weighted average\",\n    \"depth peeling\",\n    \"alpha cut\"\n]\n\n\nclass CreateReview(plugin.MayaCreator):\n    \"\"\"Playblast reviewable\"\"\"\n\n    identifier = \"io.openpype.creators.maya.review\"\n    label = \"Review\"\n    family = \"review\"\n    icon = \"video-camera\"\n\n    useMayaTimeline = True\n    panZoom = False\n\n    # Overriding \"create\" method to prefill values from settings.\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        members = list()\n        if pre_create_data.get(\"use_selection\"):\n            members = cmds.ls(selection=True)\n\n        project_name = self.project_name\n        if AYON_SERVER_ENABLED:\n            asset_name = instance_data[\"folderPath\"]\n        else:\n            asset_name = instance_data[\"asset\"]\n        asset_doc = get_asset_by_name(project_name, asset_name)\n        task_name = instance_data[\"task\"]\n        preset = lib.get_capture_preset(\n            task_name,\n            asset_doc[\"data\"][\"tasks\"][task_name][\"type\"],\n            subset_name,\n            self.project_settings,\n            self.log\n        )\n        self.log.debug(\n            \"Using preset: {}\".format(\n                json.dumps(preset, indent=4, sort_keys=True)\n            )\n        )\n\n        with lib.undo_chunk():\n            instance_node = cmds.sets(members, name=subset_name)\n            instance_data[\"instance_node\"] = instance_node\n            instance = CreatedInstance(\n                self.family,\n                subset_name,\n                instance_data,\n                self)\n\n            creator_attribute_defs_by_key = {\n                x.key: x for x in instance.creator_attribute_defs\n            }\n            mapping = {\n                \"review_width\": preset[\"Resolution\"][\"width\"],\n                \"review_height\": preset[\"Resolution\"][\"height\"],\n                \"isolate\": preset[\"Generic\"][\"isolate_view\"],\n                \"imagePlane\": preset[\"Viewport Options\"][\"imagePlane\"],\n                \"panZoom\": preset[\"Generic\"][\"pan_zoom\"]\n            }\n            for key, value in mapping.items():\n                creator_attribute_defs_by_key[key].default = value\n\n            self._add_instance_to_context(instance)\n\n            self.imprint_instance_node(instance_node,\n                                       data=instance.data_to_store())\n            return instance\n\n    def get_instance_attr_defs(self):\n\n        defs = lib.collect_animation_defs()\n\n        # Option for using Maya or asset frame range in settings.\n        if not self.useMayaTimeline:\n            # Update the defaults to be the asset frame range\n            frame_range = lib.get_frame_range()\n            defs_by_key = {attr_def.key: attr_def for attr_def in defs}\n            for key, value in frame_range.items():\n                if key not in defs_by_key:\n                    raise RuntimeError(\"Attribute definition not found to be \"\n                                       \"updated for key: {}\".format(key))\n                attr_def = defs_by_key[key]\n                attr_def.default = value\n\n        defs.extend([\n            NumberDef(\"review_width\",\n                      label=\"Review width\",\n                      tooltip=\"A value of zero will use the asset resolution.\",\n                      decimals=0,\n                      minimum=0,\n                      default=0),\n            NumberDef(\"review_height\",\n                      label=\"Review height\",\n                      tooltip=\"A value of zero will use the asset resolution.\",\n                      decimals=0,\n                      minimum=0,\n                      default=0),\n            BoolDef(\"keepImages\",\n                    label=\"Keep Images\",\n                    tooltip=\"Whether to also publish along the image sequence \"\n                            \"next to the video reviewable.\",\n                    default=False),\n            BoolDef(\"isolate\",\n                    label=\"Isolate render members of instance\",\n                    tooltip=\"When enabled only the members of the instance \"\n                            \"will be included in the playblast review.\",\n                    default=False),\n            BoolDef(\"imagePlane\",\n                    label=\"Show Image Plane\",\n                    default=True),\n            EnumDef(\"transparency\",\n                    label=\"Transparency\",\n                    items=TRANSPARENCIES),\n            BoolDef(\"panZoom\",\n                    label=\"Enable camera pan/zoom\",\n                    default=True),\n            EnumDef(\"displayLights\",\n                    label=\"Display Lights\",\n                    items=lib.DISPLAY_LIGHTS_ENUM),\n        ])\n\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_rig.py",
    "content": "from maya import cmds\n\nfrom openpype.hosts.maya.api import plugin\n\n\nclass CreateRig(plugin.MayaCreator):\n    \"\"\"Artist-friendly rig with controls to direct motion\"\"\"\n\n    identifier = \"io.openpype.creators.maya.rig\"\n    label = \"Rig\"\n    family = \"rig\"\n    icon = \"wheelchair\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        instance = super(CreateRig, self).create(subset_name,\n                                                 instance_data,\n                                                 pre_create_data)\n\n        instance_node = instance.get(\"instance_node\")\n\n        self.log.info(\"Creating Rig instance set up ...\")\n        # TODO：change name (_controls_SET -> _rigs_SET)\n        controls = cmds.sets(name=subset_name + \"_controls_SET\", empty=True)\n        # TODO：change name (_out_SET -> _geo_SET)\n        pointcache = cmds.sets(name=subset_name + \"_out_SET\", empty=True)\n        skeleton = cmds.sets(\n            name=subset_name + \"_skeletonAnim_SET\", empty=True)\n        skeleton_mesh = cmds.sets(\n            name=subset_name + \"_skeletonMesh_SET\", empty=True)\n        cmds.sets([controls, pointcache,\n                   skeleton, skeleton_mesh], forceElement=instance_node)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_setdress.py",
    "content": "from openpype.hosts.maya.api import plugin\nfrom openpype.lib import BoolDef\n\n\nclass CreateSetDress(plugin.MayaCreator):\n    \"\"\"A grouped package of loaded content\"\"\"\n\n    identifier = \"io.openpype.creators.maya.setdress\"\n    label = \"Set Dress\"\n    family = \"setdress\"\n    icon = \"cubes\"\n    default_variants = [\"Main\", \"Anim\"]\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\"exactSetMembersOnly\",\n                    label=\"Exact Set Members Only\",\n                    default=True)\n        ]\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator for Unreal Skeletal Meshes.\"\"\"\nfrom openpype.hosts.maya.api import plugin, lib\nfrom openpype.lib import (\n    BoolDef,\n    TextDef\n)\n\nfrom maya import cmds  # noqa\n\n\nclass CreateUnrealSkeletalMesh(plugin.MayaCreator):\n    \"\"\"Unreal Static Meshes with collisions.\"\"\"\n\n    identifier = \"io.openpype.creators.maya.unrealskeletalmesh\"\n    label = \"Unreal - Skeletal Mesh\"\n    family = \"skeletalMesh\"\n    icon = \"thumbs-up\"\n    dynamic_subset_keys = [\"asset\"]\n\n    # Defined in settings\n    joint_hints = set()\n\n    def apply_settings(self, project_settings):\n        \"\"\"Apply project settings to creator\"\"\"\n        settings = (\n            project_settings[\"maya\"][\"create\"][\"CreateUnrealSkeletalMesh\"]\n        )\n        self.joint_hints = set(settings.get(\"joint_hints\", []))\n\n    def get_dynamic_data(\n        self, variant, task_name, asset_doc, project_name, host_name, instance\n    ):\n        \"\"\"\n        The default subset name templates for Unreal include {asset} and thus\n        we should pass that along as dynamic data.\n        \"\"\"\n        dynamic_data = super(CreateUnrealSkeletalMesh, self).get_dynamic_data(\n            variant, task_name, asset_doc, project_name, host_name, instance\n        )\n        dynamic_data[\"asset\"] = asset_doc[\"name\"]\n        return dynamic_data\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        with lib.undo_chunk():\n            instance = super(CreateUnrealSkeletalMesh, self).create(\n                subset_name, instance_data, pre_create_data)\n            instance_node = instance.get(\"instance_node\")\n\n            # We reorganize the geometry that was originally added into the\n            # set into either 'joints_SET' or 'geometry_SET' based on the\n            # joint_hints from project settings\n            members = cmds.sets(instance_node, query=True) or []\n            cmds.sets(clear=instance_node)\n\n            geometry_set = cmds.sets(name=\"geometry_SET\", empty=True)\n            joints_set = cmds.sets(name=\"joints_SET\", empty=True)\n\n            cmds.sets([geometry_set, joints_set], forceElement=instance_node)\n\n            for node in members:\n                if node in self.joint_hints:\n                    cmds.sets(node, forceElement=joints_set)\n                else:\n                    cmds.sets(node, forceElement=geometry_set)\n\n    def get_instance_attr_defs(self):\n\n        defs = lib.collect_animation_defs()\n\n        defs.extend([\n            BoolDef(\"renderableOnly\",\n                    label=\"Renderable Only\",\n                    tooltip=\"Only export renderable visible shapes\",\n                    default=False),\n            BoolDef(\"visibleOnly\",\n                    label=\"Visible Only\",\n                    tooltip=\"Only export dag objects visible during \"\n                            \"frame range\",\n                    default=False),\n            BoolDef(\"includeParentHierarchy\",\n                    label=\"Include Parent Hierarchy\",\n                    tooltip=\"Whether to include parent hierarchy of nodes in \"\n                            \"the publish instance\",\n                    default=False),\n            BoolDef(\"worldSpace\",\n                    label=\"World-Space Export\",\n                    default=True),\n            BoolDef(\"refresh\",\n                    label=\"Refresh viewport during export\",\n                    default=False),\n            TextDef(\"attr\",\n                    label=\"Custom Attributes\",\n                    default=\"\",\n                    placeholder=\"attr1, attr2\"),\n            TextDef(\"attrPrefix\",\n                    label=\"Custom Attributes Prefix\",\n                    placeholder=\"prefix1, prefix2\")\n        ])\n\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator for Unreal Static Meshes.\"\"\"\nfrom openpype.hosts.maya.api import plugin, lib\nfrom maya import cmds  # noqa\n\n\nclass CreateUnrealStaticMesh(plugin.MayaCreator):\n    \"\"\"Unreal Static Meshes with collisions.\"\"\"\n\n    identifier = \"io.openpype.creators.maya.unrealstaticmesh\"\n    label = \"Unreal - Static Mesh\"\n    family = \"staticMesh\"\n    icon = \"cube\"\n    dynamic_subset_keys = [\"asset\"]\n\n    # Defined in settings\n    collision_prefixes = []\n\n    def apply_settings(self, project_settings):\n        \"\"\"Apply project settings to creator\"\"\"\n        settings = project_settings[\"maya\"][\"create\"][\"CreateUnrealStaticMesh\"]\n        self.collision_prefixes = settings[\"collision_prefixes\"]\n\n    def get_dynamic_data(\n        self, variant, task_name, asset_doc, project_name, host_name, instance\n    ):\n        \"\"\"\n        The default subset name templates for Unreal include {asset} and thus\n        we should pass that along as dynamic data.\n        \"\"\"\n        dynamic_data = super(CreateUnrealStaticMesh, self).get_dynamic_data(\n            variant, task_name, asset_doc, project_name, host_name, instance\n        )\n        dynamic_data[\"asset\"] = asset_doc[\"name\"]\n        return dynamic_data\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        with lib.undo_chunk():\n            instance = super(CreateUnrealStaticMesh, self).create(\n                subset_name, instance_data, pre_create_data)\n            instance_node = instance.get(\"instance_node\")\n\n            # We reorganize the geometry that was originally added into the\n            # set into either 'collision_SET' or 'geometry_SET' based on the\n            # collision_prefixes from project settings\n            members = cmds.sets(instance_node, query=True)\n            cmds.sets(clear=instance_node)\n\n            geometry_set = cmds.sets(name=\"geometry_SET\", empty=True)\n            collisions_set = cmds.sets(name=\"collisions_SET\", empty=True)\n\n            cmds.sets([geometry_set, collisions_set],\n                      forceElement=instance_node)\n\n            members = cmds.ls(members, long=True) or []\n            children = cmds.listRelatives(members, allDescendents=True,\n                                          fullPath=True) or []\n            transforms = cmds.ls(members + children, type=\"transform\")\n            for transform in transforms:\n\n                if not cmds.listRelatives(transform,\n                                          type=\"shape\",\n                                          noIntermediate=True):\n                    # Exclude all transforms that have no direct shapes\n                    continue\n\n                if self.has_collision_prefix(transform):\n                    cmds.sets(transform, forceElement=collisions_set)\n                else:\n                    cmds.sets(transform, forceElement=geometry_set)\n\n    def has_collision_prefix(self, node_path):\n        \"\"\"Return whether node name of path matches collision prefix.\n\n        If the node name matches the collision prefix we add it to the\n        `collisions_SET` instead of the `geometry_SET`.\n\n        Args:\n            node_path (str): Maya node path.\n\n        Returns:\n            bool: Whether the node should be considered a collision mesh.\n\n        \"\"\"\n        node_name = node_path.rsplit(\"|\", 1)[-1]\n        for prefix in self.collision_prefixes:\n            if node_name.startswith(prefix):\n                return True\n        return False\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_unreal_yeticache.py",
    "content": "from openpype.hosts.maya.api import (\n    lib,\n    plugin\n)\nfrom openpype.lib import NumberDef\n\n\nclass CreateYetiCache(plugin.MayaCreator):\n    \"\"\"Output for procedural plugin nodes of Yeti \"\"\"\n\n    identifier = \"io.openpype.creators.maya.unrealyeticache\"\n    label = \"Unreal - Yeti Cache\"\n    family = \"yeticacheUE\"\n    icon = \"pagelines\"\n\n    def get_instance_attr_defs(self):\n\n        defs = [\n            NumberDef(\"preroll\",\n                      label=\"Preroll\",\n                      minimum=0,\n                      default=0,\n                      decimals=0)\n        ]\n\n        # Add animation data without step and handles\n        defs.extend(lib.collect_animation_defs())\n        remove = {\"step\", \"handleStart\", \"handleEnd\"}\n        defs = [attr_def for attr_def in defs if attr_def.key not in remove]\n\n        # Add samples after frame range\n        defs.append(\n            NumberDef(\"samples\",\n                      label=\"Samples\",\n                      default=3,\n                      decimals=0)\n        )\n\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_vrayproxy.py",
    "content": "from openpype.hosts.maya.api import (\n    plugin,\n    lib\n)\nfrom openpype.lib import BoolDef\n\n\nclass CreateVrayProxy(plugin.MayaCreator):\n    \"\"\"Alembic pointcache for animated data\"\"\"\n\n    identifier = \"io.openpype.creators.maya.vrayproxy\"\n    label = \"VRay Proxy\"\n    family = \"vrayproxy\"\n    icon = \"gears\"\n\n    vrmesh = True\n    alembic = True\n\n    def get_instance_attr_defs(self):\n\n        defs = [\n            BoolDef(\"animation\",\n                    label=\"Export Animation\",\n                    default=False)\n        ]\n\n        # Add time range attributes but remove some attributes\n        # which this instance actually doesn't use\n        defs.extend(lib.collect_animation_defs())\n        remove = {\"handleStart\", \"handleEnd\", \"step\"}\n        defs = [attr_def for attr_def in defs if attr_def.key not in remove]\n\n        defs.extend([\n            BoolDef(\"vertexColors\",\n                    label=\"Write vertex colors\",\n                    tooltip=\"Write vertex colors with the geometry\",\n                    default=False),\n            BoolDef(\"vrmesh\",\n                    label=\"Export VRayMesh\",\n                    tooltip=\"Publish a .vrmesh (VRayMesh) file for \"\n                            \"this VRayProxy\",\n                    default=self.vrmesh),\n            BoolDef(\"alembic\",\n                    label=\"Export Alembic\",\n                    tooltip=\"Publish a .abc (Alembic) file for \"\n                            \"this VRayProxy\",\n                    default=self.alembic),\n        ])\n\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_vrayscene.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Create instance of vrayscene.\"\"\"\n\nfrom openpype.hosts.maya.api import (\n    lib_rendersettings,\n    plugin\n)\nfrom openpype.pipeline import CreatorError\nfrom openpype.lib import BoolDef\n\n\nclass CreateVRayScene(plugin.RenderlayerCreator):\n    \"\"\"Create Vray Scene.\"\"\"\n\n    identifier = \"io.openpype.creators.maya.vrayscene\"\n\n    family = \"vrayscene\"\n    label = \"VRay Scene\"\n    icon = \"cubes\"\n\n    render_settings = {}\n    singleton_node_name = \"vraysceneMain\"\n\n    @classmethod\n    def apply_settings(cls, project_settings):\n        cls.render_settings = project_settings[\"maya\"][\"RenderSettings\"]\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # Only allow a single render instance to exist\n        if self._get_singleton_node():\n            raise CreatorError(\"A Render instance already exists - only \"\n                               \"one can be configured.\")\n\n        super(CreateVRayScene, self).create(subset_name,\n                                            instance_data,\n                                            pre_create_data)\n\n        # Apply default project render settings on create\n        if self.render_settings.get(\"apply_render_settings\"):\n            lib_rendersettings.RenderSettings().set_default_renderer_settings()\n\n    def get_instance_attr_defs(self):\n        \"\"\"Create instance settings.\"\"\"\n\n        return [\n            BoolDef(\"vraySceneMultipleFiles\",\n                    label=\"V-Ray Scene Multiple Files\",\n                    default=False),\n            BoolDef(\"exportOnFarm\",\n                    label=\"Export on farm\",\n                    default=False)\n        ]\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_workfile.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating workfiles.\"\"\"\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import CreatedInstance, AutoCreator\nfrom openpype.client import get_asset_by_name, get_asset_name_identifier\nfrom openpype.hosts.maya.api import plugin\nfrom maya import cmds\n\n\nclass CreateWorkfile(plugin.MayaCreatorBase, AutoCreator):\n    \"\"\"Workfile auto-creator.\"\"\"\n    identifier = \"io.openpype.creators.maya.workfile\"\n    label = \"Workfile\"\n    family = \"workfile\"\n    icon = \"fa5.file\"\n\n    default_variant = \"Main\"\n\n    def create(self):\n\n        variant = self.default_variant\n        current_instance = next(\n            (\n                instance for instance in self.create_context.instances\n                if instance.creator_identifier == self.identifier\n            ), None)\n\n        project_name = self.project_name\n        asset_name = self.create_context.get_current_asset_name()\n        task_name = self.create_context.get_current_task_name()\n        host_name = self.create_context.host_name\n\n        if current_instance is None:\n            current_instance_asset = None\n        elif AYON_SERVER_ENABLED:\n            current_instance_asset = current_instance[\"folderPath\"]\n        else:\n            current_instance_asset = current_instance[\"asset\"]\n\n        if current_instance is None:\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                variant, task_name, asset_doc, project_name, host_name\n            )\n            data = {\n                \"task\": task_name,\n                \"variant\": variant\n            }\n            if AYON_SERVER_ENABLED:\n                data[\"folderPath\"] = asset_name\n            else:\n                data[\"asset\"] = asset_name\n\n            data.update(\n                self.get_dynamic_data(\n                    variant, task_name, asset_doc,\n                    project_name, host_name, current_instance)\n            )\n            self.log.info(\"Auto-creating workfile instance...\")\n            current_instance = CreatedInstance(\n                self.family, subset_name, data, self\n            )\n            self._add_instance_to_context(current_instance)\n        elif (\n            current_instance_asset != asset_name\n            or current_instance[\"task\"] != task_name\n        ):\n            # Update instance context if is not the same\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                variant, task_name, asset_doc, project_name, host_name\n            )\n            asset_name = get_asset_name_identifier(asset_doc)\n\n            if AYON_SERVER_ENABLED:\n                current_instance[\"folderPath\"] = asset_name\n            else:\n                current_instance[\"asset\"] = asset_name\n            current_instance[\"task\"] = task_name\n            current_instance[\"subset\"] = subset_name\n\n    def collect_instances(self):\n        self.cache_subsets(self.collection_shared_data)\n        cached_subsets = self.collection_shared_data[\"maya_cached_subsets\"]\n        for node in cached_subsets.get(self.identifier, []):\n            node_data = self.read_instance_node(node)\n\n            created_instance = CreatedInstance.from_existing(node_data, self)\n            self._add_instance_to_context(created_instance)\n\n    def update_instances(self, update_list):\n        for created_inst, _changes in update_list:\n            data = created_inst.data_to_store()\n            node = data.get(\"instance_node\")\n            if not node:\n                node = self.create_node()\n                created_inst[\"instance_node\"] = node\n                data = created_inst.data_to_store()\n\n            self.imprint_instance_node(node, data)\n\n    def create_node(self):\n        node = cmds.sets(empty=True, name=\"workfileMain\")\n        cmds.setAttr(node + \".hiddenInOutliner\", True)\n        return node\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_xgen.py",
    "content": "from openpype.hosts.maya.api import plugin\n\n\nclass CreateXgen(plugin.MayaCreator):\n    \"\"\"Xgen\"\"\"\n\n    identifier = \"io.openpype.creators.maya.xgen\"\n    label = \"Xgen\"\n    family = \"xgen\"\n    icon = \"pagelines\"\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_yeti_cache.py",
    "content": "from openpype.hosts.maya.api import (\n    lib,\n    plugin\n)\nfrom openpype.lib import NumberDef\n\n\nclass CreateYetiCache(plugin.MayaCreator):\n    \"\"\"Output for procedural plugin nodes of Yeti \"\"\"\n\n    identifier = \"io.openpype.creators.maya.yeticache\"\n    label = \"Yeti Cache\"\n    family = \"yeticache\"\n    icon = \"pagelines\"\n\n    def get_instance_attr_defs(self):\n\n        defs = [\n            NumberDef(\"preroll\",\n                      label=\"Preroll\",\n                      minimum=0,\n                      default=0,\n                      decimals=0)\n        ]\n\n        # Add animation data without step and handles\n        defs.extend(lib.collect_animation_defs())\n        remove = {\"step\", \"handleStart\", \"handleEnd\"}\n        defs = [attr_def for attr_def in defs if attr_def.key not in remove]\n\n        # Add samples after frame range\n        defs.append(\n            NumberDef(\"samples\",\n                      label=\"Samples\",\n                      default=3,\n                      decimals=0)\n        )\n\n        return defs\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/create/create_yeti_rig.py",
    "content": "from maya import cmds\n\nfrom openpype.hosts.maya.api import (\n    lib,\n    plugin\n)\n\n\nclass CreateYetiRig(plugin.MayaCreator):\n    \"\"\"Output for procedural plugin nodes ( Yeti / XGen / etc)\"\"\"\n\n    identifier = \"io.openpype.creators.maya.yetirig\"\n    label = \"Yeti Rig\"\n    family = \"yetiRig\"\n    icon = \"usb\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        with lib.undo_chunk():\n            instance = super(CreateYetiRig, self).create(subset_name,\n                                                         instance_data,\n                                                         pre_create_data)\n            instance_node = instance.get(\"instance_node\")\n\n            self.log.info(\"Creating Rig instance set up ...\")\n            input_meshes = cmds.sets(name=\"input_SET\", empty=True)\n            cmds.sets(input_meshes, forceElement=instance_node)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/inventory/connect_geometry.py",
    "content": "from maya import cmds\n\nfrom openpype.pipeline import InventoryAction, get_representation_context\nfrom openpype.hosts.maya.api.lib import get_id\n\n\nclass ConnectGeometry(InventoryAction):\n    \"\"\"Connect geometries within containers.\n\n    Source container will connect to the target containers, by searching for\n    matching geometry IDs (cbid).\n    Source containers are of family; \"animation\" and \"pointcache\".\n    The connection with be done with a live world space blendshape.\n    \"\"\"\n\n    label = \"Connect Geometry\"\n    icon = \"link\"\n    color = \"white\"\n\n    def process(self, containers):\n        # Validate selection is more than 1.\n        message = (\n            \"Only 1 container selected. 2+ containers needed for this action.\"\n        )\n        if len(containers) == 1:\n            self.display_warning(message)\n            return\n\n        # Categorize containers by family.\n        containers_by_family = {}\n        for container in containers:\n            family = get_representation_context(\n                container[\"representation\"]\n            )[\"subset\"][\"data\"][\"family\"]\n            try:\n                containers_by_family[family].append(container)\n            except KeyError:\n                containers_by_family[family] = [container]\n\n        # Validate to only 1 source container.\n        source_containers = containers_by_family.get(\"animation\", [])\n        source_containers += containers_by_family.get(\"pointcache\", [])\n        source_container_namespaces = [\n            x[\"namespace\"] for x in source_containers\n        ]\n        message = (\n            \"{} animation containers selected:\\n\\n{}\\n\\nOnly select 1 of type \"\n            \"\\\"animation\\\" or \\\"pointcache\\\".\".format(\n                len(source_containers), source_container_namespaces\n            )\n        )\n        if len(source_containers) != 1:\n            self.display_warning(message)\n            return\n\n        source_object = source_containers[0][\"objectName\"]\n\n        # Collect matching geometry transforms based cbId attribute.\n        target_containers = []\n        for family, containers in containers_by_family.items():\n            if family in [\"animation\", \"pointcache\"]:\n                continue\n\n            target_containers.extend(containers)\n\n        source_data = self.get_container_data(source_object)\n        matches = []\n        node_types = set()\n        for target_container in target_containers:\n            target_data = self.get_container_data(\n                target_container[\"objectName\"]\n            )\n            node_types.update(target_data[\"node_types\"])\n            for id, transform in target_data[\"ids\"].items():\n                source_match = source_data[\"ids\"].get(id)\n                if source_match:\n                    matches.append([source_match, transform])\n\n        # Message user about what is about to happen.\n        if not matches:\n            self.display_warning(\"No matching geometries found.\")\n            return\n\n        message = \"Connecting geometries:\\n\\n\"\n        for match in matches:\n            message += \"{} > {}\\n\".format(match[0], match[1])\n\n        choice = self.display_warning(message, show_cancel=True)\n        if choice is False:\n            return\n\n        # Setup live worldspace blendshape connection.\n        for source, target in matches:\n            blendshape = cmds.blendShape(source, target)[0]\n            cmds.setAttr(blendshape + \".origin\", 0)\n            cmds.setAttr(blendshape + \".\" + target.split(\":\")[-1], 1)\n\n        # Update Xgen if in any of the containers.\n        if \"xgmPalette\" in node_types:\n            cmds.xgmPreview()\n\n    def get_container_data(self, container):\n        \"\"\"Collects data about the container nodes.\n\n        Args:\n            container (dict): Container instance.\n\n        Returns:\n            data (dict):\n                \"node_types\": All node types in container nodes.\n                \"ids\": If the node is a mesh, we collect its parent transform\n                    id.\n        \"\"\"\n        data = {\"node_types\": set(), \"ids\": {}}\n        ref_node = cmds.sets(container, query=True, nodesOnly=True)[0]\n        for node in cmds.referenceQuery(ref_node, nodes=True):\n            node_type = cmds.nodeType(node)\n            data[\"node_types\"].add(node_type)\n\n            # Only interested in mesh transforms for connecting geometry with\n            # blendshape.\n            if node_type != \"mesh\":\n                continue\n\n            transform = cmds.listRelatives(node, parent=True)[0]\n            data[\"ids\"][get_id(transform)] = transform\n\n        return data\n\n    def display_warning(self, message, show_cancel=False):\n        \"\"\"Show feedback to user.\n\n        Returns:\n            bool\n        \"\"\"\n\n        from qtpy import QtWidgets\n\n        accept = QtWidgets.QMessageBox.Ok\n        if show_cancel:\n            buttons = accept | QtWidgets.QMessageBox.Cancel\n        else:\n            buttons = accept\n\n        state = QtWidgets.QMessageBox.warning(\n            None,\n            \"\",\n            message,\n            buttons=buttons,\n            defaultButton=accept\n        )\n\n        return state == accept\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/inventory/connect_xgen.py",
    "content": "from maya import cmds\nimport xgenm\n\nfrom openpype.pipeline import (\n    InventoryAction, get_representation_context, get_representation_path\n)\n\n\nclass ConnectXgen(InventoryAction):\n    \"\"\"Connect Xgen with an animation or pointcache.\n    \"\"\"\n\n    label = \"Connect Xgen\"\n    icon = \"link\"\n    color = \"white\"\n\n    def process(self, containers):\n        # Validate selection is more than 1.\n        message = (\n            \"Only 1 container selected. 2+ containers needed for this action.\"\n        )\n        if len(containers) == 1:\n            self.display_warning(message)\n            return\n\n        # Categorize containers by family.\n        containers_by_family = {}\n        for container in containers:\n            family = get_representation_context(\n                container[\"representation\"]\n            )[\"subset\"][\"data\"][\"family\"]\n            try:\n                containers_by_family[family].append(container)\n            except KeyError:\n                containers_by_family[family] = [container]\n\n        # Validate to only 1 source container.\n        source_containers = containers_by_family.get(\"animation\", [])\n        source_containers += containers_by_family.get(\"pointcache\", [])\n        source_container_namespaces = [\n            x[\"namespace\"] for x in source_containers\n        ]\n        message = (\n            \"{} animation containers selected:\\n\\n{}\\n\\nOnly select 1 of type \"\n            \"\\\"animation\\\" or \\\"pointcache\\\".\".format(\n                len(source_containers), source_container_namespaces\n            )\n        )\n        if len(source_containers) != 1:\n            self.display_warning(message)\n            return\n\n        source_container = source_containers[0]\n        source_object = source_container[\"objectName\"]\n\n        # Validate source representation is an alembic.\n        source_path = get_representation_path(\n            get_representation_context(\n                source_container[\"representation\"]\n            )[\"representation\"]\n        ).replace(\"\\\\\", \"/\")\n        message = \"Animation container \\\"{}\\\" is not an alembic:\\n{}\".format(\n            source_container[\"namespace\"], source_path\n        )\n        if not source_path.endswith(\".abc\"):\n            self.display_warning(message)\n            return\n\n        # Target containers.\n        target_containers = []\n        for family, containers in containers_by_family.items():\n            if family in [\"animation\", \"pointcache\"]:\n                continue\n\n            target_containers.extend(containers)\n\n        # Inform user of connections from source representation to target\n        # descriptions.\n        descriptions_data = []\n        connections_msg = \"\"\n        for target_container in target_containers:\n            reference_node = cmds.sets(\n                target_container[\"objectName\"], query=True\n            )[0]\n            palettes = cmds.ls(\n                cmds.referenceQuery(reference_node, nodes=True),\n                type=\"xgmPalette\"\n            )\n            for palette in palettes:\n                for description in xgenm.descriptions(palette):\n                    descriptions_data.append([palette, description])\n                    connections_msg += \"\\n{}/{}\".format(palette, description)\n\n        message = \"Connecting \\\"{}\\\" to:\\n\".format(\n            source_container[\"namespace\"]\n        )\n        message += connections_msg\n        choice = self.display_warning(message, show_cancel=True)\n        if choice is False:\n            return\n\n        # Recreate \"xgenContainers\" attribute to reset.\n        compound_name = \"xgenContainers\"\n        attr = \"{}.{}\".format(source_object, compound_name)\n        if cmds.objExists(attr):\n            cmds.deleteAttr(attr)\n\n        cmds.addAttr(\n            source_object,\n            longName=compound_name,\n            attributeType=\"compound\",\n            numberOfChildren=1,\n            multi=True\n        )\n\n        # Connect target containers.\n        for target_container in target_containers:\n            cmds.addAttr(\n                source_object,\n                longName=\"container\",\n                attributeType=\"message\",\n                parent=compound_name\n            )\n            index = target_containers.index(target_container)\n            cmds.connectAttr(\n                target_container[\"objectName\"] + \".message\",\n                source_object + \".{}[{}].container\".format(\n                    compound_name, index\n                )\n            )\n\n        # Setup cache on Xgen\n        object = \"SplinePrimitive\"\n        for palette, description in descriptions_data:\n            xgenm.setAttr(\"useCache\", \"true\", palette, description, object)\n            xgenm.setAttr(\"liveMode\", \"false\", palette, description, object)\n            xgenm.setAttr(\n                \"cacheFileName\", source_path, palette, description, object\n            )\n\n        # Refresh UI and viewport.\n        de = xgenm.xgGlobal.DescriptionEditor\n        de.refresh(\"Full\")\n\n    def display_warning(self, message, show_cancel=False):\n        \"\"\"Show feedback to user.\n\n        Returns:\n            bool\n        \"\"\"\n\n        from qtpy import QtWidgets\n\n        accept = QtWidgets.QMessageBox.Ok\n        if show_cancel:\n            buttons = accept | QtWidgets.QMessageBox.Cancel\n        else:\n            buttons = accept\n\n        state = QtWidgets.QMessageBox.warning(\n            None,\n            \"\",\n            message,\n            buttons=buttons,\n            defaultButton=accept\n        )\n\n        return state == accept\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/inventory/connect_yeti_rig.py",
    "content": "import os\nimport json\nfrom collections import defaultdict\n\nfrom maya import cmds\n\nfrom openpype.pipeline import (\n    InventoryAction, get_representation_context, get_representation_path\n)\nfrom openpype.hosts.maya.api.lib import get_container_members, get_id\n\n\nclass ConnectYetiRig(InventoryAction):\n    \"\"\"Connect Yeti Rig with an animation or pointcache.\"\"\"\n\n    label = \"Connect Yeti Rig\"\n    icon = \"link\"\n    color = \"white\"\n\n    def process(self, containers):\n        # Validate selection is more than 1.\n        message = (\n            \"Only 1 container selected. 2+ containers needed for this action.\"\n        )\n        if len(containers) == 1:\n            self.display_warning(message)\n            return\n\n        # Categorize containers by family.\n        containers_by_family = defaultdict(list)\n        for container in containers:\n            family = get_representation_context(\n                container[\"representation\"]\n            )[\"subset\"][\"data\"][\"family\"]\n            containers_by_family[family].append(container)\n\n        # Validate to only 1 source container.\n        source_containers = containers_by_family.get(\"animation\", [])\n        source_containers += containers_by_family.get(\"pointcache\", [])\n        source_container_namespaces = [\n            x[\"namespace\"] for x in source_containers\n        ]\n        message = (\n            \"{} animation containers selected:\\n\\n{}\\n\\nOnly select 1 of type \"\n            \"\\\"animation\\\" or \\\"pointcache\\\".\".format(\n                len(source_containers), source_container_namespaces\n            )\n        )\n        if len(source_containers) != 1:\n            self.display_warning(message)\n            return\n\n        source_container = source_containers[0]\n        source_ids = self.nodes_by_id(source_container)\n\n        # Target containers.\n        target_ids = {}\n        inputs = []\n\n        yeti_rig_containers = containers_by_family.get(\"yetiRig\")\n        if not yeti_rig_containers:\n            self.display_warning(\n                \"Select at least one yetiRig container\"\n            )\n            return\n\n        for container in yeti_rig_containers:\n            target_ids.update(self.nodes_by_id(container))\n\n            maya_file = get_representation_path(\n                get_representation_context(\n                    container[\"representation\"]\n                )[\"representation\"]\n            )\n            _, ext = os.path.splitext(maya_file)\n            settings_file = maya_file.replace(ext, \".rigsettings\")\n            if not os.path.exists(settings_file):\n                continue\n\n            with open(settings_file) as f:\n                inputs.extend(json.load(f)[\"inputs\"])\n\n            # Compare loaded connections to scene.\n            for input in inputs:\n                source_node = source_ids.get(input[\"sourceID\"])\n                target_node = target_ids.get(input[\"destinationID\"])\n\n                if not source_node or not target_node:\n                    self.log.debug(\n                        \"Could not find nodes for input:\\n\" +\n                        json.dumps(input, indent=4, sort_keys=True)\n                    )\n                    continue\n                source_attr, target_attr = input[\"connections\"]\n\n                if not cmds.attributeQuery(\n                    source_attr, node=source_node, exists=True\n                ):\n                    self.log.debug(\n                        \"Could not find attribute {} on node {} for \"\n                        \"input:\\n{}\".format(\n                            source_attr,\n                            source_node,\n                            json.dumps(input, indent=4, sort_keys=True)\n                        )\n                    )\n                    continue\n\n                if not cmds.attributeQuery(\n                    target_attr, node=target_node, exists=True\n                ):\n                    self.log.debug(\n                        \"Could not find attribute {} on node {} for \"\n                        \"input:\\n{}\".format(\n                            target_attr,\n                            target_node,\n                            json.dumps(input, indent=4, sort_keys=True)\n                        )\n                    )\n                    continue\n\n                source_plug = \"{}.{}\".format(\n                    source_node, source_attr\n                )\n                target_plug = \"{}.{}\".format(\n                    target_node, target_attr\n                )\n                if cmds.isConnected(\n                    source_plug, target_plug, ignoreUnitConversion=True\n                ):\n                    self.log.debug(\n                        \"Connection already exists: {} -> {}\".format(\n                            source_plug, target_plug\n                        )\n                    )\n                    continue\n\n                cmds.connectAttr(source_plug, target_plug, force=True)\n                self.log.debug(\n                    \"Connected attributes: {} -> {}\".format(\n                        source_plug, target_plug\n                    )\n                )\n\n    def nodes_by_id(self, container):\n        ids = {}\n        for member in get_container_members(container):\n            id = get_id(member)\n            if not id:\n                continue\n            ids[id] = member\n\n        return ids\n\n    def display_warning(self, message, show_cancel=False):\n        \"\"\"Show feedback to user.\n\n        Returns:\n            bool\n        \"\"\"\n\n        from qtpy import QtWidgets\n\n        accept = QtWidgets.QMessageBox.Ok\n        if show_cancel:\n            buttons = accept | QtWidgets.QMessageBox.Cancel\n        else:\n            buttons = accept\n\n        state = QtWidgets.QMessageBox.warning(\n            None,\n            \"\",\n            message,\n            buttons=buttons,\n            defaultButton=accept\n        )\n\n        return state == accept\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/inventory/import_modelrender.py",
    "content": "import re\nimport json\n\nfrom openpype.client import (\n    get_representation_by_id,\n    get_representations\n)\nfrom openpype.pipeline import (\n    InventoryAction,\n    get_representation_context,\n    get_current_project_name,\n)\nfrom openpype.hosts.maya.api.lib import (\n    maintained_selection,\n    apply_shaders\n)\n\n\nclass ImportModelRender(InventoryAction):\n\n    label = \"Import Model Render Sets\"\n    icon = \"industry\"\n    color = \"#55DDAA\"\n\n    scene_type_regex = \"meta.render.m[ab]\"\n    look_data_type = \"meta.render.json\"\n\n    @staticmethod\n    def is_compatible(container):\n        return (\n            container.get(\"loader\") == \"ReferenceLoader\"\n            and container.get(\"name\", \"\").startswith(\"model\")\n        )\n\n    def process(self, containers):\n        from maya import cmds  # noqa: F401\n\n        project_name = get_current_project_name()\n        for container in containers:\n            con_name = container[\"objectName\"]\n            nodes = []\n            for n in cmds.sets(con_name, query=True, nodesOnly=True) or []:\n                if cmds.nodeType(n) == \"reference\":\n                    nodes += cmds.referenceQuery(n, nodes=True)\n                else:\n                    nodes.append(n)\n\n            repr_doc = get_representation_by_id(\n                project_name, container[\"representation\"], fields=[\"parent\"]\n            )\n            version_id = repr_doc[\"parent\"]\n\n            print(\"Importing render sets for model %r\" % con_name)\n            self.assign_model_render_by_version(nodes, version_id)\n\n    def assign_model_render_by_version(self, nodes, version_id):\n        \"\"\"Assign nodes a specific published model render data version by id.\n\n        This assumes the nodes correspond with the asset.\n\n        Args:\n            nodes(list): nodes to assign render data to\n            version_id (bson.ObjectId): database id of the version of model\n\n        Returns:\n            None\n        \"\"\"\n\n        from maya import cmds  # noqa: F401\n\n        project_name = get_current_project_name()\n        repre_docs = get_representations(\n            project_name, version_ids=[version_id], fields=[\"_id\", \"name\"]\n        )\n        # Get representations of shader file and relationships\n        json_repre = None\n        look_repres = []\n        scene_type_regex = re.compile(self.scene_type_regex)\n        for repre_doc in repre_docs:\n            repre_name = repre_doc[\"name\"]\n            if repre_name == self.look_data_type:\n                json_repre = repre_doc\n                continue\n\n            if scene_type_regex.fullmatch(repre_name):\n                look_repres.append(repre_doc)\n\n        look_repre = look_repres[0] if look_repres else None\n        # QUESTION shouldn't be json representation validated too?\n        if not look_repre:\n            print(\"No model render sets for this model version..\")\n            return\n\n        context = get_representation_context(look_repre[\"_id\"])\n        maya_file = self.filepath_from_context(context)\n\n        context = get_representation_context(json_repre[\"_id\"])\n        json_file = self.filepath_from_context(context)\n\n        # Import the look file\n        with maintained_selection():\n            shader_nodes = cmds.file(maya_file,\n                                     i=True,  # import\n                                     returnNewNodes=True)\n            # imprint context data\n\n        # Load relationships\n        shader_relation = json_file\n        with open(shader_relation, \"r\") as f:\n            relationships = json.load(f)\n\n        # Assign relationships\n        apply_shaders(relationships, shader_nodes, nodes)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/inventory/import_reference.py",
    "content": "from maya import cmds\n\nfrom openpype.pipeline import InventoryAction\nfrom openpype.hosts.maya.api.lib import get_reference_node\n\n\nclass ImportReference(InventoryAction):\n    \"\"\"Imports selected reference to inside of the file.\"\"\"\n\n    label = \"Import Reference\"\n    icon = \"download\"\n    color = \"#d8d8d8\"\n\n    def process(self, containers):\n        for container in containers:\n            if container[\"loader\"] != \"ReferenceLoader\":\n                print(\"Not a reference, skipping\")\n                continue\n\n            node = container[\"objectName\"]\n            members = cmds.sets(node, query=True, nodesOnly=True)\n            ref_node = get_reference_node(members)\n\n            ref_file = cmds.referenceQuery(ref_node, f=True)\n            cmds.file(ref_file, importReference=True)\n\n        return True  # return anything to trigger model refresh\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py",
    "content": "from openpype.pipeline import (\n    InventoryAction,\n    get_representation_context\n)\nfrom openpype.hosts.maya.api.lib import (\n    create_rig_animation_instance,\n    get_container_members,\n)\n\n\nclass RecreateRigAnimationInstance(InventoryAction):\n    \"\"\"Recreate animation publish instance for loaded rigs\"\"\"\n\n    label = \"Recreate rig animation instance\"\n    icon = \"wrench\"\n    color = \"#888888\"\n\n    @staticmethod\n    def is_compatible(container):\n        return (\n            container.get(\"loader\") == \"ReferenceLoader\"\n            and container.get(\"name\", \"\").startswith(\"rig\")\n        )\n\n    def process(self, containers):\n\n        for container in containers:\n            # todo: delete an existing entry if it exist or skip creation\n\n            namespace = container[\"namespace\"]\n            representation_id = container[\"representation\"]\n            context = get_representation_context(representation_id)\n            nodes = get_container_members(container)\n\n            create_rig_animation_instance(nodes, context, namespace)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/inventory/select_containers.py",
    "content": "from maya import cmds\n\nfrom openpype.pipeline import InventoryAction, registered_host\nfrom openpype.hosts.maya.api.lib import get_container_members\n\n\nclass SelectInScene(InventoryAction):\n    \"\"\"Select nodes in the scene from selected containers in scene inventory\"\"\"\n\n    label = \"Select in scene\"\n    icon = \"search\"\n    color = \"#888888\"\n    order = 99\n\n    def process(self, containers):\n\n        all_members = []\n        for container in containers:\n            members = get_container_members(container)\n            all_members.extend(members)\n        cmds.select(all_members, replace=True, noExpand=True)\n\n\nclass HighlightBySceneSelection(InventoryAction):\n    \"\"\"Select containers in scene inventory from the current scene selection\"\"\"\n\n    label = \"Highlight by scene selection\"\n    icon = \"search\"\n    color = \"#888888\"\n    order = 100\n\n    def process(self, containers):\n\n        selection = set(cmds.ls(selection=True, long=True, objectsOnly=True))\n        host = registered_host()\n\n        to_select = []\n        for container in host.get_containers():\n            members = get_container_members(container)\n            if any(member in selection for member in members):\n                to_select.append(container[\"objectName\"])\n\n        return {\n            \"objectNames\": to_select,\n            \"options\": {\"clear\": True}\n        }\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/_load_animation.py",
    "content": "import openpype.hosts.maya.api.plugin\nimport maya.cmds as cmds\n\n\ndef _process_reference(file_url, name, namespace, options):\n    \"\"\"Load files by referencing scene in Maya.\n\n    Args:\n        file_url (str): fileapth of the objects to be loaded\n        name (str): subset name\n        namespace (str): namespace\n        options (dict): dict of storing the param\n\n    Returns:\n        list: list of object nodes\n    \"\"\"\n    from openpype.hosts.maya.api.lib import unique_namespace\n    # Get name from asset being loaded\n    # Assuming name is subset name from the animation, we split the number\n    # suffix from the name to ensure the namespace is unique\n    name = name.split(\"_\")[0]\n    ext = file_url.split(\".\")[-1]\n    namespace = unique_namespace(\n        \"{}_\".format(name),\n        format=\"%03d\",\n        suffix=\"_{}\".format(ext)\n    )\n\n    attach_to_root = options.get(\"attach_to_root\", True)\n    group_name = options[\"group_name\"]\n\n    # no group shall be created\n    if not attach_to_root:\n        group_name = namespace\n\n    nodes = cmds.file(file_url,\n                      namespace=namespace,\n                      sharedReferenceFile=False,\n                      groupReference=attach_to_root,\n                      groupName=group_name,\n                      reference=True,\n                      returnNewNodes=True)\n    return nodes\n\n\nclass AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):\n    \"\"\"Loader to reference an Alembic file\"\"\"\n\n    families = [\"animation\",\n                \"camera\",\n                \"pointcache\"]\n    representations = [\"abc\"]\n\n    label = \"Reference animation\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def process_reference(self, context, name, namespace, options):\n\n        cmds.loadPlugin(\"AbcImport.mll\", quiet=True)\n        # hero_001 (abc)\n        # asset_counter{optional}\n        path = self.filepath_from_context(context)\n        file_url = self.prepare_root_value(path,\n                                           context[\"project\"][\"name\"])\n\n        nodes = _process_reference(file_url, name, namespace, options)\n        # load colorbleed ID attribute\n        self[:] = nodes\n\n        return nodes\n\n\nclass FbxLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):\n    \"\"\"Loader to reference an Fbx files\"\"\"\n\n    families = [\"animation\",\n                \"camera\"]\n    representations = [\"fbx\"]\n\n    label = \"Reference animation\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def process_reference(self, context, name, namespace, options):\n\n        cmds.loadPlugin(\"fbx4maya.mll\", quiet=True)\n\n        path = self.filepath_from_context(context)\n        file_url = self.prepare_root_value(path,\n                                           context[\"project\"][\"name\"])\n\n        nodes = _process_reference(file_url, name, namespace, options)\n\n        self[:] = nodes\n\n        return nodes\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/actions.py",
    "content": "\"\"\"A module containing generic loader actions that will display in the Loader.\n\n\"\"\"\nimport qargparse\nfrom openpype.pipeline import load\nfrom openpype.hosts.maya.api.lib import (\n    maintained_selection,\n    get_custom_namespace\n)\nimport openpype.hosts.maya.api.plugin\n\n\nclass SetFrameRangeLoader(load.LoaderPlugin):\n    \"\"\"Set frame range excluding pre- and post-handles\"\"\"\n\n    families = [\"animation\",\n                \"camera\",\n                \"proxyAbc\",\n                \"pointcache\"]\n    representations = [\"abc\"]\n\n    label = \"Set frame range\"\n    order = 11\n    icon = \"clock-o\"\n    color = \"white\"\n\n    def load(self, context, name, namespace, data):\n\n        import maya.cmds as cmds\n\n        version = context['version']\n        version_data = version.get(\"data\", {})\n\n        start = version_data.get(\"frameStart\", None)\n        end = version_data.get(\"frameEnd\", None)\n\n        if start is None or end is None:\n            print(\"Skipping setting frame range because start or \"\n                  \"end frame data is missing..\")\n            return\n\n        cmds.playbackOptions(minTime=start,\n                             maxTime=end,\n                             animationStartTime=start,\n                             animationEndTime=end)\n\n\nclass SetFrameRangeWithHandlesLoader(load.LoaderPlugin):\n    \"\"\"Set frame range including pre- and post-handles\"\"\"\n\n    families = [\"animation\",\n                \"camera\",\n                \"proxyAbc\",\n                \"pointcache\"]\n    representations = [\"abc\"]\n\n    label = \"Set frame range (with handles)\"\n    order = 12\n    icon = \"clock-o\"\n    color = \"white\"\n\n    def load(self, context, name, namespace, data):\n\n        import maya.cmds as cmds\n\n        version = context['version']\n        version_data = version.get(\"data\", {})\n\n        start = version_data.get(\"frameStart\", None)\n        end = version_data.get(\"frameEnd\", None)\n\n        if start is None or end is None:\n            print(\"Skipping setting frame range because start or \"\n                  \"end frame data is missing..\")\n            return\n\n        # Include handles\n        start -= version_data.get(\"handleStart\", 0)\n        end += version_data.get(\"handleEnd\", 0)\n\n        cmds.playbackOptions(minTime=start,\n                             maxTime=end,\n                             animationStartTime=start,\n                             animationEndTime=end)\n\n\nclass ImportMayaLoader(openpype.hosts.maya.api.plugin.Loader):\n    \"\"\"Import action for Maya (unmanaged)\n\n    Warning:\n        The loaded content will be unmanaged and is *not* visible in the\n        scene inventory. It's purely intended to merge content into your scene\n        so you could also use it as a new base.\n\n    \"\"\"\n    representations = [\"ma\", \"mb\", \"obj\"]\n    families = [\n        \"model\",\n        \"pointcache\",\n        \"proxyAbc\",\n        \"animation\",\n        \"mayaAscii\",\n        \"mayaScene\",\n        \"setdress\",\n        \"layout\",\n        \"camera\",\n        \"rig\",\n        \"camerarig\",\n        \"staticMesh\",\n        \"workfile\"\n    ]\n\n    label = \"Import\"\n    order = 10\n    icon = \"arrow-circle-down\"\n    color = \"#775555\"\n\n    options = [\n        qargparse.Boolean(\n            \"clean_import\",\n            label=\"Clean import\",\n            default=False,\n            help=\"Should all occurrences of cbId be purged?\"\n        )\n    ]\n\n    def load(self, context, name=None, namespace=None, data=None):\n        import maya.cmds as cmds\n\n        choice = self.display_warning()\n        if choice is False:\n            return\n\n        custom_group_name, custom_namespace, options = \\\n            self.get_custom_namespace_and_group(context, data,\n                                                \"import_loader\")\n\n        namespace = get_custom_namespace(custom_namespace)\n\n        if not options.get(\"attach_to_root\", True):\n            custom_group_name = namespace\n\n        path = self.filepath_from_context(context)\n        with maintained_selection():\n            nodes = cmds.file(path,\n                              i=True,\n                              preserveReferences=True,\n                              namespace=namespace,\n                              returnNewNodes=True,\n                              groupReference=options.get(\"attach_to_root\",\n                                                         True),\n                              groupName=custom_group_name)\n\n            if data.get(\"clean_import\", False):\n                remove_attributes = [\"cbId\"]\n                for node in nodes:\n                    for attr in remove_attributes:\n                        if cmds.attributeQuery(attr, node=node, exists=True):\n                            full_attr = \"{}.{}\".format(node, attr)\n                            print(\"Removing {}\".format(full_attr))\n                            cmds.deleteAttr(full_attr)\n\n        # We do not containerize imported content, it remains unmanaged\n        return\n\n    def display_warning(self):\n        \"\"\"Show warning to ensure the user can't import models by accident\n\n        Returns:\n            bool\n\n        \"\"\"\n\n        from qtpy import QtWidgets\n\n        accept = QtWidgets.QMessageBox.Ok\n        buttons = accept | QtWidgets.QMessageBox.Cancel\n\n        message = \"Are you sure you want import this\"\n        state = QtWidgets.QMessageBox.warning(None,\n                                              \"Are you sure?\",\n                                              message,\n                                              buttons=buttons,\n                                              defaultButton=accept)\n\n        return state == accept\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_arnold_standin.py",
    "content": "import os\nimport clique\n\nimport maya.cmds as cmds\n\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import (\n    load,\n    legacy_io,\n    get_representation_path\n)\nfrom openpype.hosts.maya.api.lib import (\n    unique_namespace,\n    get_attribute_input,\n    maintained_selection,\n    convert_to_maya_fps\n)\nfrom openpype.hosts.maya.api.pipeline import containerise\n\n\ndef is_sequence(files):\n    sequence = False\n    collections, remainder = clique.assemble(files, minimum_items=1)\n    if collections:\n        sequence = True\n    return sequence\n\n\ndef get_current_session_fps():\n    session_fps = float(legacy_io.Session.get('AVALON_FPS', 25))\n    return convert_to_maya_fps(session_fps)\n\n\nclass ArnoldStandinLoader(load.LoaderPlugin):\n    \"\"\"Load as Arnold standin\"\"\"\n\n    families = [\n        \"ass\",\n        \"assProxy\",\n        \"animation\",\n        \"model\",\n        \"proxyAbc\",\n        \"pointcache\",\n        \"usd\"\n    ]\n    representations = [\"ass\", \"abc\", \"usda\", \"usdc\", \"usd\"]\n\n    label = \"Load as Arnold standin\"\n    order = -5\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, options):\n        if not cmds.pluginInfo(\"mtoa\", query=True, loaded=True):\n            cmds.loadPlugin(\"mtoa\")\n            # Create defaultArnoldRenderOptions before creating aiStandin\n            # which tries to connect it. Since we load the plugin and directly\n            # create aiStandin without the defaultArnoldRenderOptions,\n            # we need to create the render options for aiStandin creation.\n            from mtoa.core import createOptions\n            createOptions()\n\n        import mtoa.ui.arnoldmenu\n\n        version = context['version']\n        version_data = version.get(\"data\", {})\n\n        self.log.info(\"version_data: {}\\n\".format(version_data))\n\n        asset = context['asset']['name']\n        namespace = namespace or unique_namespace(\n            asset + \"_\",\n            prefix=\"_\" if asset[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        # Root group\n        label = \"{}:{}\".format(namespace, name)\n        root = cmds.group(name=label, empty=True)\n\n        # Set color.\n        settings = get_project_settings(context[\"project\"][\"name\"])\n        color = settings['maya']['load']['colors'].get('ass')\n        if color is not None:\n            cmds.setAttr(root + \".useOutlinerColor\", True)\n            cmds.setAttr(\n                root + \".outlinerColor\", color[0], color[1], color[2]\n            )\n\n        with maintained_selection():\n            # Create transform with shape\n            transform_name = label + \"_standin\"\n\n            standin_shape = mtoa.ui.arnoldmenu.createStandIn()\n            standin = cmds.listRelatives(standin_shape, parent=True)[0]\n            standin = cmds.rename(standin, transform_name)\n            standin_shape = cmds.listRelatives(standin, shapes=True)[0]\n\n            cmds.parent(standin, root)\n\n            # Set the standin filepath\n            repre_path = self.filepath_from_context(context)\n            path, operator = self._setup_proxy(\n                standin_shape, repre_path, namespace\n            )\n            cmds.setAttr(standin_shape + \".dso\", path, type=\"string\")\n            sequence = is_sequence(os.listdir(os.path.dirname(repre_path)))\n            cmds.setAttr(standin_shape + \".useFrameExtension\", sequence)\n\n            fps = version[\"data\"].get(\"fps\") or get_current_session_fps()\n            cmds.setAttr(standin_shape + \".abcFPS\", float(fps))\n\n        nodes = [root, standin, standin_shape]\n        if operator is not None:\n            nodes.append(operator)\n        self[:] = nodes\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n    def get_next_free_multi_index(self, attr_name):\n        \"\"\"Find the next unconnected multi index at the input attribute.\"\"\"\n        for index in range(10000000):\n            connection_info = cmds.connectionInfo(\n                \"{}[{}]\".format(attr_name, index),\n                sourceFromDestination=True\n            )\n            if len(connection_info or []) == 0:\n                return index\n\n    def _get_proxy_path(self, path):\n        basename_split = os.path.basename(path).split(\".\")\n        proxy_basename = (\n            basename_split[0] + \"_proxy.\" + \".\".join(basename_split[1:])\n        )\n        proxy_path = \"/\".join([os.path.dirname(path), proxy_basename])\n        return proxy_basename, proxy_path\n\n    def _update_operators(self, string_replace_operator, proxy_basename, path):\n        cmds.setAttr(\n            string_replace_operator + \".match\",\n            proxy_basename.split(\".\")[0],\n            type=\"string\"\n        )\n        cmds.setAttr(\n            string_replace_operator + \".replace\",\n            os.path.basename(path).split(\".\")[0],\n            type=\"string\"\n        )\n\n    def _setup_proxy(self, shape, path, namespace):\n        proxy_basename, proxy_path = self._get_proxy_path(path)\n\n        options_node = \"defaultArnoldRenderOptions\"\n        merge_operator = get_attribute_input(options_node + \".operator\")\n        if merge_operator is None:\n            merge_operator = cmds.createNode(\"aiMerge\")\n            cmds.connectAttr(\n                merge_operator + \".message\", options_node + \".operator\"\n            )\n\n        merge_operator = merge_operator.split(\".\")[0]\n\n        string_replace_operator = cmds.createNode(\n            \"aiStringReplace\", name=namespace + \":string_replace_operator\"\n        )\n        node_type = \"alembic\" if path.endswith(\".abc\") else \"procedural\"\n        cmds.setAttr(\n            string_replace_operator + \".selection\",\n            \"*.(@node=='{}')\".format(node_type),\n            type=\"string\"\n        )\n        self._update_operators(string_replace_operator, proxy_basename, path)\n\n        cmds.connectAttr(\n            string_replace_operator + \".out\",\n            \"{}.inputs[{}]\".format(\n                merge_operator,\n                self.get_next_free_multi_index(merge_operator + \".inputs\")\n            )\n        )\n\n        # We setup the string operator no matter whether there is a proxy or\n        # not. This makes it easier to update since the string operator will\n        # always be created. Return original path to use for standin.\n        if not os.path.exists(proxy_path):\n            return path, string_replace_operator\n\n        return proxy_path, string_replace_operator\n\n    def update(self, container, representation):\n        # Update the standin\n        members = cmds.sets(container['objectName'], query=True)\n        for member in members:\n            if cmds.nodeType(member) == \"aiStringReplace\":\n                string_replace_operator = member\n\n            shapes = cmds.listRelatives(member, shapes=True)\n            if not shapes:\n                continue\n            if cmds.nodeType(shapes[0]) == \"aiStandIn\":\n                standin = shapes[0]\n\n        path = get_representation_path(representation)\n        proxy_basename, proxy_path = self._get_proxy_path(path)\n\n        # Whether there is proxy or not, we still update the string operator.\n        # If no proxy exists, the string operator won't replace anything.\n        self._update_operators(string_replace_operator, proxy_basename, path)\n\n        dso_path = path\n        if os.path.exists(proxy_path):\n            dso_path = proxy_path\n        cmds.setAttr(standin + \".dso\", dso_path, type=\"string\")\n\n        sequence = is_sequence(os.listdir(os.path.dirname(path)))\n        cmds.setAttr(standin + \".useFrameExtension\", sequence)\n\n        cmds.setAttr(\n            container[\"objectName\"] + \".representation\",\n            str(representation[\"_id\"]),\n            type=\"string\"\n        )\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        members = cmds.sets(container['objectName'], query=True)\n        cmds.lockNode(members, lock=False)\n        cmds.delete([container['objectName']] + members)\n\n        # Clean up the namespace\n        try:\n            cmds.namespace(removeNamespace=container['namespace'],\n                           deleteNamespaceContent=True)\n        except RuntimeError:\n            pass\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_assembly.py",
    "content": "import maya.cmds as cmds\n\nfrom openpype.pipeline import (\n    load,\n    remove_container\n)\n\nfrom openpype.hosts.maya.api.pipeline import containerise\nfrom openpype.hosts.maya.api.lib import unique_namespace\nfrom openpype.hosts.maya.api import setdress\n\n\nclass AssemblyLoader(load.LoaderPlugin):\n\n    families = [\"assembly\"]\n    representations = [\"json\"]\n\n    label = \"Load Set Dress\"\n    order = -9\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data):\n\n        asset = context['asset']['name']\n        namespace = namespace or unique_namespace(\n            asset + \"_\",\n            prefix=\"_\" if asset[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        containers = setdress.load_package(\n            filepath=self.filepath_from_context(context),\n            name=name,\n            namespace=namespace\n        )\n\n        self[:] = containers\n\n        # Only containerize if any nodes were loaded by the Loader\n        nodes = self[:]\n        if not nodes:\n            return\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n\n        return setdress.update_package(container, representation)\n\n    def remove(self, container):\n        \"\"\"Remove all sub containers\"\"\"\n\n        # Remove all members\n        member_containers = setdress.get_contained_containers(container)\n        for member_container in member_containers:\n            self.log.info(\"Removing container %s\",\n                          member_container['objectName'])\n            remove_container(member_container)\n\n        # Remove alembic hierarchy reference\n        # TODO: Check whether removing all contained references is safe enough\n        members = cmds.sets(container['objectName'], query=True) or []\n        references = cmds.ls(members, type=\"reference\")\n        for reference in references:\n            self.log.info(\"Removing %s\", reference)\n            fname = cmds.referenceQuery(reference, filename=True)\n            cmds.file(fname, removeReference=True)\n\n        # Delete container and its contents\n        if cmds.objExists(container['objectName']):\n            members = cmds.sets(container['objectName'], query=True) or []\n            cmds.delete([container['objectName']] + members)\n\n        # TODO: Ensure namespace is gone\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_audio.py",
    "content": "from maya import cmds, mel\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.maya.api.pipeline import containerise\nfrom openpype.hosts.maya.api.lib import unique_namespace, get_container_members\n\n\nclass AudioLoader(load.LoaderPlugin):\n    \"\"\"Specific loader of audio.\"\"\"\n\n    families = [\"audio\"]\n    label = \"Load audio\"\n    representations = [\"wav\"]\n    icon = \"volume-up\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data):\n\n        start_frame = cmds.playbackOptions(query=True, min=True)\n        sound_node = cmds.sound(\n            file=self.filepath_from_context(context), offset=start_frame\n        )\n        cmds.timeControl(\n            mel.eval(\"$gPlayBackSlider=$gPlayBackSlider\"),\n            edit=True,\n            sound=sound_node,\n            displaySound=True\n        )\n\n        asset = context[\"asset\"][\"name\"]\n        namespace = namespace or unique_namespace(\n            asset + \"_\",\n            prefix=\"_\" if asset[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=[sound_node],\n            context=context,\n            loader=self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n\n        members = get_container_members(container)\n        audio_nodes = cmds.ls(members, type=\"audio\")\n\n        assert audio_nodes is not None, \"Audio node not found.\"\n        audio_node = audio_nodes[0]\n\n        current_sound = cmds.timeControl(\n            mel.eval(\"$gPlayBackSlider=$gPlayBackSlider\"),\n            query=True,\n            sound=True\n        )\n        activate_sound = current_sound == audio_node\n\n        path = get_representation_path(representation)\n\n        cmds.sound(\n            audio_node,\n            edit=True,\n            file=path\n        )\n\n        # The source start + end does not automatically update itself to the\n        # length of thew new audio file, even though maya does do that when\n        # creating a new audio node. So to update we compute it manually.\n        # This would however override any source start and source end a user\n        # might have done on the original audio node after load.\n        audio_frame_count = cmds.getAttr(\"{}.frameCount\".format(audio_node))\n        audio_sample_rate = cmds.getAttr(\"{}.sampleRate\".format(audio_node))\n        duration_in_seconds = audio_frame_count / audio_sample_rate\n        fps = mel.eval('currentTimeUnitToFPS()')  # workfile FPS\n        source_start = 0\n        source_end = (duration_in_seconds * fps)\n        cmds.setAttr(\"{}.sourceStart\".format(audio_node), source_start)\n        cmds.setAttr(\"{}.sourceEnd\".format(audio_node), source_end)\n\n        if activate_sound:\n            # maya by default deactivates it from timeline on file change\n            cmds.timeControl(\n                mel.eval(\"$gPlayBackSlider=$gPlayBackSlider\"),\n                edit=True,\n                sound=audio_node,\n                displaySound=True\n            )\n\n        cmds.setAttr(\n            container[\"objectName\"] + \".representation\",\n            str(representation[\"_id\"]),\n            type=\"string\"\n        )\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        members = cmds.sets(container['objectName'], query=True)\n        cmds.lockNode(members, lock=False)\n        cmds.delete([container['objectName']] + members)\n\n        # Clean up the namespace\n        try:\n            cmds.namespace(removeNamespace=container['namespace'],\n                           deleteNamespaceContent=True)\n        except RuntimeError:\n            pass\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_gpucache.py",
    "content": "import os\n\nimport maya.cmds as cmds\n\nfrom openpype.hosts.maya.api.pipeline import containerise\nfrom openpype.hosts.maya.api.lib import unique_namespace\nfrom openpype.pipeline import (\n    load,\n    get_representation_path\n)\nfrom openpype.settings import get_project_settings\n\n\nclass GpuCacheLoader(load.LoaderPlugin):\n    \"\"\"Load Alembic as gpuCache\"\"\"\n\n    families = [\"model\", \"animation\", \"proxyAbc\", \"pointcache\"]\n    representations = [\"abc\", \"gpu_cache\"]\n\n    label = \"Load Gpu Cache\"\n    order = -5\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data):\n\n        asset = context['asset']['name']\n        namespace = namespace or unique_namespace(\n            asset + \"_\",\n            prefix=\"_\" if asset[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        cmds.loadPlugin(\"gpuCache\", quiet=True)\n\n        # Root group\n        label = \"{}:{}\".format(namespace, name)\n        root = cmds.group(name=label, empty=True)\n\n        project_name = context[\"project\"][\"name\"]\n        settings = get_project_settings(project_name)\n        colors = settings['maya']['load']['colors']\n        c = colors.get('model')\n        if c is not None:\n            cmds.setAttr(root + \".useOutlinerColor\", 1)\n            cmds.setAttr(\n                root + \".outlinerColor\",\n                (float(c[0]) / 255), (float(c[1]) / 255), (float(c[2]) / 255)\n            )\n\n        # Create transform with shape\n        transform_name = label + \"_GPU\"\n        transform = cmds.createNode(\"transform\", name=transform_name,\n                                    parent=root)\n        cache = cmds.createNode(\"gpuCache\",\n                                parent=transform,\n                                name=\"{0}Shape\".format(transform_name))\n\n        # Set the cache filepath\n        path = self.filepath_from_context(context)\n        cmds.setAttr(cache + '.cacheFileName', path, type=\"string\")\n        cmds.setAttr(cache + '.cacheGeomPath', \"|\", type=\"string\")    # root\n\n        # Lock parenting of the transform and cache\n        cmds.lockNode([transform, cache], lock=True)\n\n        nodes = [root, transform, cache]\n        self[:] = nodes\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        path = get_representation_path(representation)\n\n        # Update the cache\n        members = cmds.sets(container['objectName'], query=True)\n        caches = cmds.ls(members, type=\"gpuCache\", long=True)\n\n        assert len(caches) == 1, \"This is a bug\"\n\n        for cache in caches:\n            cmds.setAttr(cache + \".cacheFileName\", path, type=\"string\")\n\n        cmds.setAttr(container[\"objectName\"] + \".representation\",\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        members = cmds.sets(container['objectName'], query=True)\n        cmds.lockNode(members, lock=False)\n        cmds.delete([container['objectName']] + members)\n\n        # Clean up the namespace\n        try:\n            cmds.namespace(removeNamespace=container['namespace'],\n                           deleteNamespaceContent=True)\n        except RuntimeError:\n            pass\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_image.py",
    "content": "import os\nimport copy\n\nfrom openpype.lib import EnumDef\nfrom openpype.pipeline import (\n    load,\n    get_representation_context,\n    get_current_host_name,\n)\nfrom openpype.pipeline.load.utils import get_representation_path_from_context\nfrom openpype.pipeline.colorspace import (\n    get_imageio_file_rules_colorspace_from_filepath,\n    get_imageio_config,\n    get_imageio_file_rules\n)\nfrom openpype.settings import get_project_settings\n\nfrom openpype.hosts.maya.api.pipeline import containerise\nfrom openpype.hosts.maya.api.lib import (\n    unique_namespace,\n    namespaced\n)\n\nfrom maya import cmds\n\n\ndef create_texture():\n    \"\"\"Create place2dTexture with file node with uv connections\n\n    Mimics Maya \"file [Texture]\" creation.\n    \"\"\"\n\n    place = cmds.shadingNode(\"place2dTexture\", asUtility=True, name=\"place2d\")\n    file = cmds.shadingNode(\"file\", asTexture=True, name=\"file\")\n\n    connections = [\"coverage\", \"translateFrame\", \"rotateFrame\", \"rotateUV\",\n                   \"mirrorU\", \"mirrorV\", \"stagger\", \"wrapV\", \"wrapU\",\n                   \"repeatUV\", \"offset\", \"noiseUV\", \"vertexUvThree\",\n                   \"vertexUvTwo\", \"vertexUvOne\", \"vertexCameraOne\"]\n    for attr in connections:\n        src = \"{}.{}\".format(place, attr)\n        dest = \"{}.{}\".format(file, attr)\n        cmds.connectAttr(src, dest)\n\n    cmds.connectAttr(place + '.outUV', file + '.uvCoord')\n    cmds.connectAttr(place + '.outUvFilterSize', file + '.uvFilterSize')\n\n    return file, place\n\n\ndef create_projection():\n    \"\"\"Create texture with place3dTexture and projection\n\n    Mimics Maya \"file [Projection]\" creation.\n    \"\"\"\n\n    file, place = create_texture()\n    projection = cmds.shadingNode(\"projection\", asTexture=True,\n                                  name=\"projection\")\n    place3d = cmds.shadingNode(\"place3dTexture\", asUtility=True,\n                               name=\"place3d\")\n\n    cmds.connectAttr(place3d + '.worldInverseMatrix[0]',\n                     projection + \".placementMatrix\")\n    cmds.connectAttr(file + '.outColor', projection + \".image\")\n\n    return file, place, projection, place3d\n\n\ndef create_stencil():\n    \"\"\"Create texture with extra place2dTexture offset and stencil\n\n    Mimics Maya \"file [Stencil]\" creation.\n    \"\"\"\n\n    file, place = create_texture()\n\n    place_stencil = cmds.shadingNode(\"place2dTexture\", asUtility=True,\n                                     name=\"place2d_stencil\")\n    stencil = cmds.shadingNode(\"stencil\", asTexture=True, name=\"stencil\")\n\n    for src_attr, dest_attr in [\n        (\"outUV\", \"uvCoord\"),\n        (\"outUvFilterSize\", \"uvFilterSize\")\n    ]:\n        src_plug = \"{}.{}\".format(place_stencil, src_attr)\n        cmds.connectAttr(src_plug, \"{}.{}\".format(place, dest_attr))\n        cmds.connectAttr(src_plug, \"{}.{}\".format(stencil, dest_attr))\n\n    return file, place, stencil, place_stencil\n\n\nclass FileNodeLoader(load.LoaderPlugin):\n    \"\"\"File node loader.\"\"\"\n\n    families = [\"image\", \"plate\", \"render\"]\n    label = \"Load file node\"\n    representations = [\"exr\", \"tif\", \"png\", \"jpg\"]\n    icon = \"image\"\n    color = \"orange\"\n    order = 2\n\n    options = [\n        EnumDef(\n            \"mode\",\n            items={\n                \"texture\": \"Texture\",\n                \"projection\": \"Projection\",\n                \"stencil\": \"Stencil\"\n            },\n            default=\"texture\",\n            label=\"Texture Mode\"\n        )\n    ]\n\n    def load(self, context, name, namespace, data):\n\n        asset = context['asset']['name']\n        namespace = namespace or unique_namespace(\n            asset + \"_\",\n            prefix=\"_\" if asset[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        with namespaced(namespace, new=True) as namespace:\n            # Create the nodes within the namespace\n            nodes = {\n                \"texture\": create_texture,\n                \"projection\": create_projection,\n                \"stencil\": create_stencil\n            }[data.get(\"mode\", \"texture\")]()\n\n        file_node = cmds.ls(nodes, type=\"file\")[0]\n\n        self._apply_representation_context(context, file_node)\n\n        # For ease of access for the user select all the nodes and select\n        # the file node last so that UI shows its attributes by default\n        cmds.select(list(nodes) + [file_node], replace=True)\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n\n        members = cmds.sets(container['objectName'], query=True)\n        file_node = cmds.ls(members, type=\"file\")[0]\n\n        context = get_representation_context(representation)\n        self._apply_representation_context(context, file_node)\n\n        # Update representation\n        cmds.setAttr(\n            container[\"objectName\"] + \".representation\",\n            str(representation[\"_id\"]),\n            type=\"string\"\n        )\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        members = cmds.sets(container['objectName'], query=True)\n        cmds.lockNode(members, lock=False)\n        cmds.delete([container['objectName']] + members)\n\n        # Clean up the namespace\n        try:\n            cmds.namespace(removeNamespace=container['namespace'],\n                           deleteNamespaceContent=True)\n        except RuntimeError:\n            pass\n\n    def _apply_representation_context(self, context, file_node):\n        \"\"\"Update the file node to match the context.\n\n        This sets the file node's attributes for:\n            - file path\n            - udim tiling mode (if it is an udim tile)\n            - use frame extension (if it is a sequence)\n            - colorspace\n\n        \"\"\"\n\n        repre_context = context[\"representation\"][\"context\"]\n        has_frames = repre_context.get(\"frame\") is not None\n        has_udim = repre_context.get(\"udim\") is not None\n\n        # Set UV tiling mode if UDIM tiles\n        if has_udim:\n            cmds.setAttr(file_node + \".uvTilingMode\", 3)    # UDIM-tiles\n        else:\n            cmds.setAttr(file_node + \".uvTilingMode\", 0)    # off\n\n        # Enable sequence if publish has `startFrame` and `endFrame` and\n        # `startFrame != endFrame`\n        if has_frames and self._is_sequence(context):\n            # When enabling useFrameExtension maya automatically\n            # connects an expression to <file>.frameExtension to set\n            # the current frame. However, this expression  is generated\n            # with some delay and thus it'll show a warning if frame 0\n            # doesn't exist because we're explicitly setting the <f>\n            # token.\n            cmds.setAttr(file_node + \".useFrameExtension\", True)\n        else:\n            cmds.setAttr(file_node + \".useFrameExtension\", False)\n\n        # Set the file node path attribute\n        path = self._format_path(context)\n        cmds.setAttr(file_node + \".fileTextureName\", path, type=\"string\")\n\n        # Set colorspace\n        colorspace = self._get_colorspace(context)\n        if colorspace:\n            cmds.setAttr(file_node + \".colorSpace\", colorspace, type=\"string\")\n        else:\n            self.log.debug(\"Unknown colorspace - setting colorspace skipped.\")\n\n    def _is_sequence(self, context):\n        \"\"\"Check whether frameStart and frameEnd are not the same.\"\"\"\n        version = context.get(\"version\", {})\n        representation = context.get(\"representation\", {})\n\n        for doc in [representation, version]:\n            # Frame range can be set on version or representation.\n            # When set on representation it overrides version data.\n            data = doc.get(\"data\", {})\n            start = data.get(\"frameStartHandle\", data.get(\"frameStart\", None))\n            end = data.get(\"frameEndHandle\", data.get(\"frameEnd\", None))\n\n            if start is None or end is None:\n                continue\n\n            if start != end:\n                return True\n            else:\n                return False\n\n        return False\n\n    def _get_colorspace(self, context):\n        \"\"\"Return colorspace of the file to load.\n\n        Retrieves the explicit colorspace from the publish. If no colorspace\n        data is stored with published content then project imageio settings\n        are used to make an assumption of the colorspace based on the file\n        rules. If no file rules match then None is returned.\n\n        Returns:\n            str or None: The colorspace of the file or None if not detected.\n\n        \"\"\"\n\n        # We can't apply color spaces if management is not enabled\n        if not cmds.colorManagementPrefs(query=True, cmEnabled=True):\n            return\n\n        representation = context[\"representation\"]\n        colorspace_data = representation.get(\"data\", {}).get(\"colorspaceData\")\n        if colorspace_data:\n            return colorspace_data[\"colorspace\"]\n\n        # Assume colorspace from filepath based on project settings\n        project_name = context[\"project\"][\"name\"]\n        host_name = get_current_host_name()\n        project_settings = get_project_settings(project_name)\n\n        config_data = get_imageio_config(\n            project_name, host_name,\n            project_settings=project_settings\n        )\n\n        # ignore if host imageio is not enabled\n        if not config_data:\n            return\n\n        file_rules = get_imageio_file_rules(\n            project_name, host_name,\n            project_settings=project_settings\n        )\n\n        path = get_representation_path_from_context(context)\n        colorspace = get_imageio_file_rules_colorspace_from_filepath(\n            path,\n            host_name,\n            project_name,\n            config_data=config_data,\n            file_rules=file_rules,\n            project_settings=project_settings\n        )\n\n        return colorspace\n\n    def _format_path(self, context):\n        \"\"\"Format the path with correct tokens for frames and udim tiles.\"\"\"\n\n        context = copy.deepcopy(context)\n        representation = context[\"representation\"]\n        template = representation.get(\"data\", {}).get(\"template\")\n        if not template:\n            # No template to find token locations for\n            return get_representation_path_from_context(context)\n\n        def _placeholder(key):\n            # Substitute with a long placeholder value so that potential\n            # custom formatting with padding doesn't find its way into\n            # our formatting, so that <f> wouldn't be padded as 0<f>\n            return \"___{}___\".format(key)\n\n        # We format UDIM and Frame numbers with their specific tokens. To do so\n        # we in-place change the representation context data to format the path\n        # with our own data\n        tokens = {\n            \"frame\": \"<f>\",\n            \"udim\": \"<UDIM>\"\n        }\n        has_tokens = False\n        repre_context = representation[\"context\"]\n        for key, _token in tokens.items():\n            if key in repre_context:\n                repre_context[key] = _placeholder(key)\n                has_tokens = True\n\n        # Replace with our custom template that has the tokens set\n        representation[\"data\"][\"template\"] = template\n        path = get_representation_path_from_context(context)\n\n        if has_tokens:\n            for key, token in tokens.items():\n                if key in repre_context:\n                    path = path.replace(_placeholder(key), token)\n\n        return path\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_image_plane.py",
    "content": "from qtpy import QtWidgets, QtCore\n\nfrom openpype.client import (\n    get_asset_by_id,\n    get_subset_by_id,\n    get_version_by_id,\n)\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n    get_current_project_name,\n)\nfrom openpype.hosts.maya.api.pipeline import containerise\nfrom openpype.hosts.maya.api.lib import (\n    unique_namespace,\n    namespaced,\n    pairwise,\n    get_container_members\n)\n\nfrom maya import cmds\n\n\ndef disconnect_inputs(plug):\n    overrides = cmds.listConnections(plug,\n                                     source=True,\n                                     destination=False,\n                                     plugs=True,\n                                     connections=True) or []\n    for dest, src in pairwise(overrides):\n        cmds.disconnectAttr(src, dest)\n\n\nclass CameraWindow(QtWidgets.QDialog):\n\n    def __init__(self, cameras):\n        super(CameraWindow, self).__init__()\n        self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)\n\n        self.camera = None\n\n        self.widgets = {\n            \"label\": QtWidgets.QLabel(\"Select camera for image plane.\"),\n            \"list\": QtWidgets.QListWidget(),\n            \"staticImagePlane\": QtWidgets.QCheckBox(),\n            \"showInAllViews\": QtWidgets.QCheckBox(),\n            \"warning\": QtWidgets.QLabel(\"No cameras selected!\"),\n            \"buttons\": QtWidgets.QWidget(),\n            \"okButton\": QtWidgets.QPushButton(\"Ok\"),\n            \"cancelButton\": QtWidgets.QPushButton(\"Cancel\")\n        }\n\n        # Build warning.\n        self.widgets[\"warning\"].setVisible(False)\n        self.widgets[\"warning\"].setStyleSheet(\"color: red\")\n\n        # Build list.\n        for camera in cameras:\n            self.widgets[\"list\"].addItem(camera)\n\n\n        # Build buttons.\n        layout = QtWidgets.QHBoxLayout(self.widgets[\"buttons\"])\n        layout.addWidget(self.widgets[\"okButton\"])\n        layout.addWidget(self.widgets[\"cancelButton\"])\n\n        # Build layout.\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(self.widgets[\"label\"])\n        layout.addWidget(self.widgets[\"list\"])\n        layout.addWidget(self.widgets[\"buttons\"])\n        layout.addWidget(self.widgets[\"warning\"])\n\n        self.widgets[\"okButton\"].pressed.connect(self.on_ok_pressed)\n        self.widgets[\"cancelButton\"].pressed.connect(self.on_cancel_pressed)\n        self.widgets[\"list\"].itemPressed.connect(self.on_list_itemPressed)\n\n    def on_list_itemPressed(self, item):\n        self.camera = item.text()\n\n    def on_ok_pressed(self):\n        if self.camera is None:\n            self.widgets[\"warning\"].setVisible(True)\n            return\n\n        self.close()\n\n    def on_cancel_pressed(self):\n        self.camera = None\n        self.close()\n\n\nclass ImagePlaneLoader(load.LoaderPlugin):\n    \"\"\"Specific loader of plate for image planes on selected camera.\"\"\"\n\n    families = [\"image\", \"plate\", \"render\"]\n    label = \"Load imagePlane\"\n    representations = [\"mov\", \"exr\", \"preview\", \"png\", \"jpg\"]\n    icon = \"image\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data, options=None):\n\n        image_plane_depth = 1000\n        asset = context['asset']['name']\n        namespace = namespace or unique_namespace(\n            asset + \"_\",\n            prefix=\"_\" if asset[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        # Get camera from user selection.\n        # is_static_image_plane = None\n        # is_in_all_views = None\n        camera = data.get(\"camera\") if data else None\n\n        if not camera:\n            cameras = cmds.ls(type=\"camera\")\n\n            # Cameras by names\n            camera_names = {}\n            for camera in cameras:\n                parent = cmds.listRelatives(camera, parent=True, path=True)[0]\n                camera_names[parent] = camera\n\n            camera_names[\"Create new camera.\"] = \"create-camera\"\n            window = CameraWindow(camera_names.keys())\n            window.exec_()\n            # Skip if no camera was selected (Dialog was closed)\n            if window.camera not in camera_names:\n                return\n            camera = camera_names[window.camera]\n\n        if camera == \"create-camera\":\n            camera = cmds.createNode(\"camera\")\n\n        if camera is None:\n            return\n\n        try:\n            cmds.setAttr(\"{}.displayResolution\".format(camera), True)\n            cmds.setAttr(\"{}.farClipPlane\".format(camera),\n                         image_plane_depth * 10)\n        except RuntimeError:\n            pass\n\n        # Create image plane\n        with namespaced(namespace):\n            # Create inside the namespace\n            image_plane_transform, image_plane_shape = cmds.imagePlane(\n                fileName=context[\"representation\"][\"data\"][\"path\"],\n                camera=camera\n            )\n        start_frame = cmds.playbackOptions(query=True, min=True)\n        end_frame = cmds.playbackOptions(query=True, max=True)\n\n        for attr, value in {\n            \"depth\": image_plane_depth,\n            \"frameOffset\": 0,\n            \"frameIn\": start_frame,\n            \"frameOut\": end_frame,\n            \"frameCache\": end_frame,\n            \"useFrameExtension\": True\n        }.items():\n            plug = \"{}.{}\".format(image_plane_shape, attr)\n            cmds.setAttr(plug, value)\n\n        movie_representations = [\"mov\", \"preview\"]\n        if context[\"representation\"][\"name\"] in movie_representations:\n            cmds.setAttr(image_plane_shape + \".type\", 2)\n\n        # Ask user whether to use sequence or still image.\n        if context[\"representation\"][\"name\"] == \"exr\":\n            # Ensure OpenEXRLoader plugin is loaded.\n            cmds.loadPlugin(\"OpenEXRLoader\", quiet=True)\n\n            message = (\n                \"Hold image sequence on first frame?\"\n                \"\\n{} files available.\".format(\n                    len(context[\"representation\"][\"files\"])\n                )\n            )\n            reply = QtWidgets.QMessageBox.information(\n                None,\n                \"Frame Hold.\",\n                message,\n                QtWidgets.QMessageBox.Yes,\n                QtWidgets.QMessageBox.No\n            )\n            if reply == QtWidgets.QMessageBox.Yes:\n                frame_extension_plug = \"{}.frameExtension\".format(image_plane_shape)  # noqa\n\n                # Remove current frame expression\n                disconnect_inputs(frame_extension_plug)\n\n                cmds.setAttr(frame_extension_plug, start_frame)\n\n        new_nodes = [image_plane_transform, image_plane_shape]\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=new_nodes,\n            context=context,\n            loader=self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n\n        members = get_container_members(container)\n        image_planes = cmds.ls(members, type=\"imagePlane\")\n        assert image_planes, \"Image plane not found.\"\n        image_plane_shape = image_planes[0]\n\n        path = get_representation_path(representation)\n        cmds.setAttr(\"{}.imageName\".format(image_plane_shape),\n                     path,\n                     type=\"string\")\n        cmds.setAttr(\"{}.representation\".format(container[\"objectName\"]),\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n\n        # Set frame range.\n        project_name = get_current_project_name()\n        version = get_version_by_id(\n            project_name, representation[\"parent\"], fields=[\"parent\"]\n        )\n        subset = get_subset_by_id(\n            project_name, version[\"parent\"], fields=[\"parent\"]\n        )\n        asset = get_asset_by_id(\n            project_name, subset[\"parent\"], fields=[\"parent\"]\n        )\n        start_frame = asset[\"data\"][\"frameStart\"]\n        end_frame = asset[\"data\"][\"frameEnd\"]\n\n        for attr, value in {\n            \"frameOffset\": 0,\n            \"frameIn\": start_frame,\n            \"frameOut\": end_frame,\n            \"frameCache\": end_frame\n        }:\n            plug = \"{}.{}\".format(image_plane_shape, attr)\n            cmds.setAttr(plug, value)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        members = cmds.sets(container['objectName'], query=True)\n        cmds.lockNode(members, lock=False)\n        cmds.delete([container['objectName']] + members)\n\n        # Clean up the namespace\n        try:\n            cmds.namespace(removeNamespace=container['namespace'],\n                           deleteNamespaceContent=True)\n        except RuntimeError:\n            pass\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_look.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Look loader.\"\"\"\nimport json\nfrom collections import defaultdict\n\nfrom qtpy import QtWidgets\n\nfrom openpype.client import get_representation_by_name\nfrom openpype.pipeline import (\n    get_current_project_name,\n    get_representation_path,\n)\nimport openpype.hosts.maya.api.plugin\nfrom openpype.hosts.maya.api import lib\nfrom openpype.widgets.message_window import ScrollMessageBox\n\nfrom openpype.hosts.maya.api.lib import get_reference_node\n\n\nclass LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):\n    \"\"\"Specific loader for lookdev\"\"\"\n\n    families = [\"look\"]\n    representations = [\"ma\"]\n\n    label = \"Reference look\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def process_reference(self, context, name, namespace, options):\n        from maya import cmds\n\n        with lib.maintained_selection():\n            file_url = self.prepare_root_value(\n                file_url=self.filepath_from_context(context),\n                project_name=context[\"project\"][\"name\"]\n            )\n            nodes = cmds.file(file_url,\n                              namespace=namespace,\n                              reference=True,\n                              returnNewNodes=True)\n\n        self[:] = nodes\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n        \"\"\"\n            Called by Scene Inventory when look should be updated to current\n            version.\n            If any reference edits cannot be applied, eg. shader renamed and\n            material not present, reference is unloaded and cleaned.\n            All failed edits are highlighted to the user via message box.\n\n        Args:\n            container: object that has look to be updated\n            representation: (dict): relationship data to get proper\n                                       representation from DB and persisted\n                                       data in .json\n        Returns:\n            None\n        \"\"\"\n        from maya import cmds\n\n        # Get reference node from container members\n        members = lib.get_container_members(container)\n        reference_node = get_reference_node(members, log=self.log)\n\n        shader_nodes = cmds.ls(members, type='shadingEngine')\n        orig_nodes = set(self._get_nodes_with_shader(shader_nodes))\n\n        # Trigger the regular reference update on the ReferenceLoader\n        super(LookLoader, self).update(container, representation)\n\n        # get new applied shaders and nodes from new version\n        shader_nodes = cmds.ls(members, type='shadingEngine')\n        nodes = set(self._get_nodes_with_shader(shader_nodes))\n\n        project_name = get_current_project_name()\n        json_representation = get_representation_by_name(\n            project_name, \"json\", representation[\"parent\"]\n        )\n\n        # Load relationships\n        shader_relation = get_representation_path(json_representation)\n        with open(shader_relation, \"r\") as f:\n            json_data = json.load(f)\n\n        # update of reference could result in failed edits - material is not\n        # present because of renaming etc. If so highlight failed edits to user\n        failed_edits = cmds.referenceQuery(reference_node,\n                                           editStrings=True,\n                                           failedEdits=True,\n                                           successfulEdits=False)\n        if failed_edits:\n            # clean references - removes failed reference edits\n            cmds.file(cr=reference_node)  # cleanReference\n\n            # reapply shading groups from json representation on orig nodes\n            lib.apply_shaders(json_data, shader_nodes, orig_nodes)\n\n            msg = [\"During reference update some edits failed.\",\n                   \"All successful edits were kept intact.\\n\",\n                   \"Failed and removed edits:\"]\n            msg.extend(failed_edits)\n\n            msg = ScrollMessageBox(QtWidgets.QMessageBox.Warning,\n                                   \"Some reference edit failed\",\n                                   msg)\n            msg.exec_()\n\n        attributes = json_data.get(\"attributes\", [])\n\n        # region compute lookup\n        nodes_by_id = defaultdict(list)\n        for node in nodes:\n            nodes_by_id[lib.get_id(node)].append(node)\n        lib.apply_attributes(attributes, nodes_by_id)\n\n    def _get_nodes_with_shader(self, shader_nodes):\n        \"\"\"\n            Returns list of nodes belonging to specific shaders\n        Args:\n            shader_nodes: <list> of Shader groups\n        Returns\n            <list> node names\n        \"\"\"\n        from maya import cmds\n\n        for shader in shader_nodes:\n            future = cmds.listHistory(shader, future=True)\n            connections = cmds.listConnections(future,\n                                               type='mesh')\n            if connections:\n                # Ensure unique entries only to optimize query and results\n                connections = list(set(connections))\n                return cmds.listRelatives(connections,\n                                          shapes=True,\n                                          fullPath=True) or []\n        return []\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_matchmove.py",
    "content": "# -*- coding: utf-8 -*-\nfrom maya import cmds, mel  # noqa: F401\n\nfrom openpype.hosts.maya.api.pipeline import containerise\nfrom openpype.hosts.maya.api import lib, Loader\nfrom openpype.pipeline.load import get_representation_path, LoadError\n\n\nclass MatchmoveLoader(Loader):\n    \"\"\"Run matchmove script to create track in scene.\n\n    Supported script types are .py and .mel\n\n    TODO: there might be error in the scripts exported from\n          3DEqualizer that it is trying to set frame attribute\n          on camera image plane and then add expression for\n          image sequence. Maya will throw RuntimeError at that\n          point that will stop processing rest of the script and\n          the container will not be created. We should somehow handle\n          this - maybe even by patching the mel script on-the-fly.\n\n    \"\"\"\n\n    families = [\"matchmove\"]\n    representations = [\"py\", \"mel\"]\n    defaults = [\"Camera\", \"Object\", \"Mocap\"]\n\n    label = \"Run matchmove script\"\n    icon = \"empire\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, options):\n\n        path = self.filepath_from_context(context)\n        custom_group_name, custom_namespace, options = \\\n            self.get_custom_namespace_and_group(\n                context, options, \"matchmove_loader\")\n\n        namespace = lib.get_custom_namespace(custom_namespace)\n\n        nodes = self._load_nodes_from_script(path)\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n        # type: (dict, dict) -> None\n        \"\"\"Update container with specified representation.\"\"\"\n        self.remove(container)\n\n        path = get_representation_path(representation)\n        namespace = container[\"namespace\"]\n        print(f\">>> loading from {path}\")\n        try:\n            nodes = self._load_nodes_from_script(path)\n        except RuntimeError as e:\n            raise LoadError(\"Failed to load matchmove script.\") from e\n\n        return containerise(\n            name=container[\"name\"],\n            namespace=namespace,\n            nodes=nodes,\n            context=representation[\"context\"],\n            loader=self.__class__.__name__\n        )\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        \"\"\"Delete container and its contents.\"\"\"\n\n        if cmds.objExists(container['objectName']):\n            members = cmds.sets(container['objectName'], query=True) or []\n            cmds.delete([container['objectName']] + members)\n\n    def _load_nodes_from_script(self, path):\n        # type: (str) -> list\n        \"\"\"Load nodes from script.\n\n        This will execute py or mel script and resulting\n        nodes will be returned.\n\n        Args:\n            path (str): path to script\n\n        Returns:\n            list: list of created nodes\n\n        \"\"\"\n        previous_nodes = set(cmds.ls(long=True))\n\n        if path.lower().endswith(\".py\"):\n            exec(open(path).read())\n\n        elif path.lower().endswith(\".mel\"):\n            mel.eval(open(path).read())\n\n        else:\n            self.log.error(\"Unsupported script type\")\n\n        current_nodes = set(cmds.ls(long=True))\n        return list(current_nodes - previous_nodes)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_maya_usd.py",
    "content": "# -*- coding: utf-8 -*-\nimport maya.cmds as cmds\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.pipeline.load import get_representation_path_from_context\nfrom openpype.hosts.maya.api.lib import (\n    namespaced,\n    unique_namespace\n)\nfrom openpype.hosts.maya.api.pipeline import containerise\n\n\nclass MayaUsdLoader(load.LoaderPlugin):\n    \"\"\"Read USD data in a Maya USD Proxy\"\"\"\n\n    families = [\"model\", \"usd\", \"pointcache\", \"animation\"]\n    representations = [\"usd\", \"usda\", \"usdc\", \"usdz\", \"abc\"]\n\n    label = \"Load USD to Maya Proxy\"\n    order = -1\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, options=None):\n        asset = context['asset']['name']\n        namespace = namespace or unique_namespace(\n            asset + \"_\",\n            prefix=\"_\" if asset[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        # Make sure we can load the plugin\n        cmds.loadPlugin(\"mayaUsdPlugin\", quiet=True)\n\n        path = get_representation_path_from_context(context)\n\n        # Create the shape\n        cmds.namespace(addNamespace=namespace)\n        with namespaced(namespace, new=False):\n            transform = cmds.createNode(\"transform\",\n                                        name=name,\n                                        skipSelect=True)\n            proxy = cmds.createNode('mayaUsdProxyShape',\n                                    name=\"{}Shape\".format(name),\n                                    parent=transform,\n                                    skipSelect=True)\n\n            cmds.connectAttr(\"time1.outTime\", \"{}.time\".format(proxy))\n            cmds.setAttr(\"{}.filePath\".format(proxy), path, type=\"string\")\n\n            # By default, we force the proxy to not use a shared stage because\n            # when doing so Maya will quite easily allow to save into the\n            # loaded usd file. Since we are loading published files we want to\n            # avoid altering them. Unshared stages also save their edits into\n            # the workfile as an artist might expect it to do.\n            cmds.setAttr(\"{}.shareStage\".format(proxy), False)\n            # cmds.setAttr(\"{}.shareStage\".format(proxy), lock=True)\n\n        nodes = [transform, proxy]\n        self[:] = nodes\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        # type: (dict, dict) -> None\n        \"\"\"Update container with specified representation.\"\"\"\n        node = container['objectName']\n        assert cmds.objExists(node), \"Missing container\"\n\n        members = cmds.sets(node, query=True) or []\n        shapes = cmds.ls(members, type=\"mayaUsdProxyShape\")\n\n        path = get_representation_path(representation)\n        for shape in shapes:\n            cmds.setAttr(\"{}.filePath\".format(shape), path, type=\"string\")\n\n        cmds.setAttr(\"{}.representation\".format(node),\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        # type: (dict) -> None\n        \"\"\"Remove loaded container.\"\"\"\n        # Delete container and its contents\n        if cmds.objExists(container['objectName']):\n            members = cmds.sets(container['objectName'], query=True) or []\n            cmds.delete([container['objectName']] + members)\n\n        # Remove the namespace, if empty\n        namespace = container['namespace']\n        if cmds.namespace(exists=namespace):\n            members = cmds.namespaceInfo(namespace, listNamespace=True)\n            if not members:\n                cmds.namespace(removeNamespace=namespace)\n            else:\n                self.log.warning(\"Namespace not deleted because it \"\n                                 \"still has members: %s\", namespace)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_multiverse_usd.py",
    "content": "# -*- coding: utf-8 -*-\nimport maya.cmds as cmds\nfrom maya import mel\nimport os\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path\n)\nfrom openpype.hosts.maya.api.lib import (\n    maintained_selection,\n    namespaced,\n    unique_namespace\n)\nfrom openpype.hosts.maya.api.pipeline import containerise\nfrom openpype.client import get_representation_by_id\n\n\nclass MultiverseUsdLoader(load.LoaderPlugin):\n    \"\"\"Read USD data in a Multiverse Compound\"\"\"\n\n    families = [\"model\", \"usd\", \"mvUsdComposition\", \"mvUsdOverride\",\n                \"pointcache\", \"animation\"]\n    representations = [\"usd\", \"usda\", \"usdc\", \"usdz\", \"abc\"]\n\n    label = \"Load USD to Multiverse\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, options=None):\n        asset = context['asset']['name']\n        namespace = namespace or unique_namespace(\n            asset + \"_\",\n            prefix=\"_\" if asset[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        path = self.filepath_from_context(context)\n\n        # Make sure we can load the plugin\n        cmds.loadPlugin(\"MultiverseForMaya\", quiet=True)\n        import multiverse\n\n        # Create the shape\n        with maintained_selection():\n            cmds.namespace(addNamespace=namespace)\n            with namespaced(namespace, new=False):\n                shape = multiverse.CreateUsdCompound(path)\n                transform = cmds.listRelatives(\n                    shape, parent=True, fullPath=True)[0]\n\n        nodes = [transform, shape]\n        self[:] = nodes\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        # type: (dict, dict) -> None\n        \"\"\"Update container with specified representation.\"\"\"\n        node = container['objectName']\n        assert cmds.objExists(node), \"Missing container\"\n\n        members = cmds.sets(node, query=True) or []\n        shapes = cmds.ls(members, type=\"mvUsdCompoundShape\")\n        assert shapes, \"Cannot find mvUsdCompoundShape in container\"\n\n        project_name = representation[\"context\"][\"project\"][\"name\"]\n        prev_representation_id = cmds.getAttr(\"{}.representation\".format(node))\n        prev_representation = get_representation_by_id(project_name,\n                                                       prev_representation_id)\n        prev_path = os.path.normpath(prev_representation[\"data\"][\"path\"])\n\n        # Make sure we can load the plugin\n        cmds.loadPlugin(\"MultiverseForMaya\", quiet=True)\n        import multiverse\n\n        for shape in shapes:\n\n            asset_paths = multiverse.GetUsdCompoundAssetPaths(shape)\n            asset_paths = [os.path.normpath(p) for p in asset_paths]\n\n            assert asset_paths.count(prev_path) == 1, \\\n                \"Couldn't find matching path (or too many)\"\n            prev_path_idx = asset_paths.index(prev_path)\n\n            path = get_representation_path(representation)\n            asset_paths[prev_path_idx] = path\n\n            multiverse.SetUsdCompoundAssetPaths(shape, asset_paths)\n\n        cmds.setAttr(\"{}.representation\".format(node),\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n        mel.eval('refreshEditorTemplates;')\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        # type: (dict) -> None\n        \"\"\"Remove loaded container.\"\"\"\n        # Delete container and its contents\n        if cmds.objExists(container['objectName']):\n            members = cmds.sets(container['objectName'], query=True) or []\n            cmds.delete([container['objectName']] + members)\n\n        # Remove the namespace, if empty\n        namespace = container['namespace']\n        if cmds.namespace(exists=namespace):\n            members = cmds.namespaceInfo(namespace, listNamespace=True)\n            if not members:\n                cmds.namespace(removeNamespace=namespace)\n            else:\n                self.log.warning(\"Namespace not deleted because it \"\n                                 \"still has members: %s\", namespace)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_multiverse_usd_over.py",
    "content": "# -*- coding: utf-8 -*-\nimport maya.cmds as cmds\nfrom maya import mel\nimport os\n\nimport qargparse\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path\n)\nfrom openpype.hosts.maya.api.lib import (\n    maintained_selection\n)\nfrom openpype.hosts.maya.api.pipeline import containerise\nfrom openpype.client import get_representation_by_id\n\n\nclass MultiverseUsdOverLoader(load.LoaderPlugin):\n    \"\"\"Reference file\"\"\"\n\n    families = [\"mvUsdOverride\"]\n    representations = [\"usda\", \"usd\", \"udsz\"]\n\n    label = \"Load Usd Override into Compound\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    options = [\n        qargparse.String(\n            \"Which Compound\",\n            label=\"Compound\",\n            help=\"Select which compound to add this as a layer to.\"\n        )\n    ]\n\n    def load(self, context, name=None, namespace=None, options=None):\n        current_usd = cmds.ls(selection=True,\n                              type=\"mvUsdCompoundShape\",\n                              dag=True,\n                              long=True)\n        if len(current_usd) != 1:\n            self.log.error(\"Current selection invalid: '{}', \"\n                           \"must contain exactly 1 mvUsdCompoundShape.\"\n                           \"\".format(current_usd))\n            return\n\n        # Make sure we can load the plugin\n        cmds.loadPlugin(\"MultiverseForMaya\", quiet=True)\n        import multiverse\n\n        path = self.filepath_from_context(context)\n        nodes = current_usd\n        with maintained_selection():\n            multiverse.AddUsdCompoundAssetPath(current_usd[0], path)\n\n        namespace = current_usd[0].split(\"|\")[1].split(\":\")[0]\n\n        container = containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n        cmds.addAttr(container, longName=\"mvUsdCompoundShape\",\n                     niceName=\"mvUsdCompoundShape\", dataType=\"string\")\n        cmds.setAttr(container + \".mvUsdCompoundShape\",\n                     current_usd[0], type=\"string\")\n\n        return container\n\n    def update(self, container, representation):\n        # type: (dict, dict) -> None\n        \"\"\"Update container with specified representation.\"\"\"\n\n        cmds.loadPlugin(\"MultiverseForMaya\", quiet=True)\n        import multiverse\n\n        node = container['objectName']\n        assert cmds.objExists(node), \"Missing container\"\n\n        members = cmds.sets(node, query=True) or []\n        shapes = cmds.ls(members, type=\"mvUsdCompoundShape\")\n        assert shapes, \"Cannot find mvUsdCompoundShape in container\"\n\n        mvShape = container['mvUsdCompoundShape']\n        assert mvShape, \"Missing mv source\"\n\n        project_name = representation[\"context\"][\"project\"][\"name\"]\n        prev_representation_id = cmds.getAttr(\"{}.representation\".format(node))\n        prev_representation = get_representation_by_id(project_name,\n                                                       prev_representation_id)\n        prev_path = os.path.normpath(prev_representation[\"data\"][\"path\"])\n\n        path = get_representation_path(representation)\n\n        for shape in shapes:\n            asset_paths = multiverse.GetUsdCompoundAssetPaths(shape)\n            asset_paths = [os.path.normpath(p) for p in asset_paths]\n\n            assert asset_paths.count(prev_path) == 1, \\\n                \"Couldn't find matching path (or too many)\"\n            prev_path_idx = asset_paths.index(prev_path)\n            asset_paths[prev_path_idx] = path\n            multiverse.SetUsdCompoundAssetPaths(shape, asset_paths)\n\n        cmds.setAttr(\"{}.representation\".format(node),\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n        mel.eval('refreshEditorTemplates;')\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        # type: (dict) -> None\n        \"\"\"Remove loaded container.\"\"\"\n        # Delete container and its contents\n        if cmds.objExists(container['objectName']):\n            members = cmds.sets(container['objectName'], query=True) or []\n            cmds.delete([container['objectName']] + members)\n\n        # Remove the namespace, if empty\n        namespace = container['namespace']\n        if cmds.namespace(exists=namespace):\n            members = cmds.namespaceInfo(namespace, listNamespace=True)\n            if not members:\n                cmds.namespace(removeNamespace=namespace)\n            else:\n                self.log.warning(\"Namespace not deleted because it \"\n                                 \"still has members: %s\", namespace)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_redshift_proxy.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Loader for Redshift proxy.\"\"\"\nimport os\nimport clique\n\nimport maya.cmds as cmds\n\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import (\n    load,\n    get_representation_path\n)\nfrom openpype.hosts.maya.api.lib import (\n    namespaced,\n    maintained_selection,\n    unique_namespace\n)\nfrom openpype.hosts.maya.api.pipeline import containerise\n\n\nclass RedshiftProxyLoader(load.LoaderPlugin):\n    \"\"\"Load Redshift proxy\"\"\"\n\n    families = [\"redshiftproxy\"]\n    representations = [\"rs\"]\n\n    label = \"Import Redshift Proxy\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, options=None):\n        \"\"\"Plugin entry point.\"\"\"\n        try:\n            family = context[\"representation\"][\"context\"][\"family\"]\n        except ValueError:\n            family = \"redshiftproxy\"\n\n        asset_name = context['asset'][\"name\"]\n        namespace = namespace or unique_namespace(\n            asset_name + \"_\",\n            prefix=\"_\" if asset_name[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        # Ensure Redshift for Maya is loaded.\n        cmds.loadPlugin(\"redshift4maya\", quiet=True)\n\n        path = self.filepath_from_context(context)\n        with maintained_selection():\n            cmds.namespace(addNamespace=namespace)\n            with namespaced(namespace, new=False):\n                nodes, group_node = self.create_rs_proxy(name, path)\n\n        self[:] = nodes\n        if not nodes:\n            return\n\n        # colour the group node\n        project_name = context[\"project\"][\"name\"]\n        settings = get_project_settings(project_name)\n        colors = settings['maya']['load']['colors']\n        c = colors.get(family)\n        if c is not None:\n            cmds.setAttr(\"{0}.useOutlinerColor\".format(group_node), 1)\n            cmds.setAttr(\"{0}.outlinerColor\".format(group_node),\n                         c[0], c[1], c[2])\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n\n        node = container['objectName']\n        assert cmds.objExists(node), \"Missing container\"\n\n        members = cmds.sets(node, query=True) or []\n        rs_meshes = cmds.ls(members, type=\"RedshiftProxyMesh\")\n        assert rs_meshes, \"Cannot find RedshiftProxyMesh in container\"\n\n        filename = get_representation_path(representation)\n\n        for rs_mesh in rs_meshes:\n            cmds.setAttr(\"{}.fileName\".format(rs_mesh),\n                         filename,\n                         type=\"string\")\n\n        # Update metadata\n        cmds.setAttr(\"{}.representation\".format(node),\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n\n    def remove(self, container):\n\n        # Delete container and its contents\n        if cmds.objExists(container['objectName']):\n            members = cmds.sets(container['objectName'], query=True) or []\n            cmds.delete([container['objectName']] + members)\n\n        # Remove the namespace, if empty\n        namespace = container['namespace']\n        if cmds.namespace(exists=namespace):\n            members = cmds.namespaceInfo(namespace, listNamespace=True)\n            if not members:\n                cmds.namespace(removeNamespace=namespace)\n            else:\n                self.log.warning(\"Namespace not deleted because it \"\n                                 \"still has members: %s\", namespace)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def create_rs_proxy(self, name, path):\n        \"\"\"Creates Redshift Proxies showing a proxy object.\n\n        Args:\n            name (str): Proxy name.\n            path (str): Path to proxy file.\n\n        Returns:\n            (str, str): Name of mesh with Redshift proxy and its parent\n                transform.\n\n        \"\"\"\n        rs_mesh = cmds.createNode(\n            'RedshiftProxyMesh', name=\"{}_RS\".format(name))\n        mesh_shape = cmds.createNode(\"mesh\", name=\"{}_GEOShape\".format(name))\n\n        cmds.setAttr(\"{}.fileName\".format(rs_mesh),\n                     path,\n                     type=\"string\")\n\n        cmds.connectAttr(\"{}.outMesh\".format(rs_mesh),\n                         \"{}.inMesh\".format(mesh_shape))\n\n        # TODO: use the assigned shading group as shaders if existed\n        # assign default shader to redshift proxy\n        if cmds.ls(\"initialShadingGroup\", type=\"shadingEngine\"):\n            cmds.sets(mesh_shape, forceElement=\"initialShadingGroup\")\n\n        group_node = cmds.group(empty=True, name=\"{}_GRP\".format(name))\n        mesh_transform = cmds.listRelatives(mesh_shape,\n                                            parent=True, fullPath=True)\n        cmds.parent(mesh_transform, group_node)\n        nodes = [rs_mesh, mesh_shape, group_node]\n\n        # determine if we need to enable animation support\n        files_in_folder = os.listdir(os.path.dirname(path))\n        collections, remainder = clique.assemble(files_in_folder)\n\n        if collections:\n            cmds.setAttr(\"{}.useFrameExtension\".format(rs_mesh), 1)\n\n        return nodes, group_node\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_reference.py",
    "content": "import os\nimport difflib\nimport contextlib\n\nfrom maya import cmds\nimport qargparse\n\nfrom openpype.settings import get_project_settings\nimport openpype.hosts.maya.api.plugin\nfrom openpype.hosts.maya.api.lib import (\n    maintained_selection,\n    get_container_members,\n    parent_nodes,\n    create_rig_animation_instance\n)\n\n\n@contextlib.contextmanager\ndef preserve_modelpanel_cameras(container, log=None):\n    \"\"\"Preserve camera members of container in the modelPanels.\n\n    This is used to ensure a camera remains in the modelPanels after updating\n    to a new version.\n\n    \"\"\"\n\n    # Get the modelPanels that used the old camera\n    members = get_container_members(container)\n    old_cameras = set(cmds.ls(members, type=\"camera\", long=True))\n    if not old_cameras:\n        # No need to manage anything\n        yield\n        return\n\n    panel_cameras = {}\n    for panel in cmds.getPanel(type=\"modelPanel\"):\n        cam = cmds.ls(cmds.modelPanel(panel, query=True, camera=True),\n                      long=True)[0]\n\n        # Often but not always maya returns the transform from the\n        # modelPanel as opposed to the camera shape, so we convert it\n        # to explicitly be the camera shape\n        if cmds.nodeType(cam) != \"camera\":\n            cam = cmds.listRelatives(cam,\n                                     children=True,\n                                     fullPath=True,\n                                     type=\"camera\")[0]\n        if cam in old_cameras:\n            panel_cameras[panel] = cam\n\n    if not panel_cameras:\n        # No need to manage anything\n        yield\n        return\n\n    try:\n        yield\n    finally:\n        new_members = get_container_members(container)\n        new_cameras = set(cmds.ls(new_members, type=\"camera\", long=True))\n        if not new_cameras:\n            return\n\n        for panel, cam_name in panel_cameras.items():\n            new_camera = None\n            if cam_name in new_cameras:\n                new_camera = cam_name\n            elif len(new_cameras) == 1:\n                new_camera = next(iter(new_cameras))\n            else:\n                # Multiple cameras in the updated container but not an exact\n                # match detected by name. Find the closest match\n                matches = difflib.get_close_matches(word=cam_name,\n                                                    possibilities=new_cameras,\n                                                    n=1)\n                if matches:\n                    new_camera = matches[0]  # best match\n                    if log:\n                        log.info(\"Camera in '{}' restored with \"\n                                 \"closest match camera: {} (before: {})\"\n                                 .format(panel, new_camera, cam_name))\n\n            if not new_camera:\n                # Unable to find the camera to re-apply in the modelpanel\n                continue\n\n            cmds.modelPanel(panel, edit=True, camera=new_camera)\n\n\nclass ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):\n    \"\"\"Reference file\"\"\"\n\n    families = [\"model\",\n                \"pointcache\",\n                \"proxyAbc\",\n                \"animation\",\n                \"mayaAscii\",\n                \"mayaScene\",\n                \"setdress\",\n                \"layout\",\n                \"camera\",\n                \"rig\",\n                \"camerarig\",\n                \"staticMesh\",\n                \"skeletalMesh\",\n                \"mvLook\",\n                \"matchmove\"]\n\n    representations = [\"ma\", \"abc\", \"fbx\", \"mb\"]\n\n    label = \"Reference\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def process_reference(self, context, name, namespace, options):\n        import maya.cmds as cmds\n\n        try:\n            family = context[\"representation\"][\"context\"][\"family\"]\n        except ValueError:\n            family = \"model\"\n\n        project_name = context[\"project\"][\"name\"]\n        # True by default to keep legacy behaviours\n        attach_to_root = options.get(\"attach_to_root\", True)\n        group_name = options[\"group_name\"]\n\n        # no group shall be created\n        if not attach_to_root:\n            group_name = namespace\n\n        kwargs = {}\n        if \"file_options\" in options:\n            kwargs[\"options\"] = options[\"file_options\"]\n        if \"file_type\" in options:\n            kwargs[\"type\"] = options[\"file_type\"]\n\n        path = self.filepath_from_context(context)\n        with maintained_selection():\n            cmds.loadPlugin(\"AbcImport.mll\", quiet=True)\n\n            file_url = self.prepare_root_value(path, project_name)\n            nodes = cmds.file(file_url,\n                              namespace=namespace,\n                              sharedReferenceFile=False,\n                              reference=True,\n                              returnNewNodes=True,\n                              groupReference=attach_to_root,\n                              groupName=group_name,\n                              **kwargs)\n\n            shapes = cmds.ls(nodes, shapes=True, long=True)\n\n            new_nodes = (list(set(nodes) - set(shapes)))\n\n            # if there are cameras, try to lock their transforms\n            self._lock_camera_transforms(new_nodes)\n\n            current_namespace = cmds.namespaceInfo(currentNamespace=True)\n\n            if current_namespace != \":\":\n                group_name = current_namespace + \":\" + group_name\n\n            self[:] = new_nodes\n\n            if attach_to_root:\n                group_name = \"|\" + group_name\n                roots = cmds.listRelatives(group_name,\n                                           children=True,\n                                           fullPath=True) or []\n\n                if family not in {\"layout\", \"setdress\",\n                                  \"mayaAscii\", \"mayaScene\"}:\n                    # QUESTION Why do we need to exclude these families?\n                    with parent_nodes(roots, parent=None):\n                        cmds.xform(group_name, zeroTransformPivots=True)\n\n                settings = get_project_settings(project_name)\n\n                display_handle = settings['maya']['load'].get(\n                    'reference_loader', {}\n                ).get('display_handle', True)\n                cmds.setAttr(\n                    \"{}.displayHandle\".format(group_name), display_handle\n                )\n\n                colors = settings['maya']['load']['colors']\n                c = colors.get(family)\n                if c is not None:\n                    cmds.setAttr(\"{}.useOutlinerColor\".format(group_name), 1)\n                    cmds.setAttr(\"{}.outlinerColor\".format(group_name),\n                                 (float(c[0]) / 255),\n                                 (float(c[1]) / 255),\n                                 (float(c[2]) / 255))\n\n                cmds.setAttr(\n                    \"{}.displayHandle\".format(group_name), display_handle\n                )\n                # get bounding box\n                bbox = cmds.exactWorldBoundingBox(group_name)\n                # get pivot position on world space\n                pivot = cmds.xform(group_name, q=True, sp=True, ws=True)\n                # center of bounding box\n                cx = (bbox[0] + bbox[3]) / 2\n                cy = (bbox[1] + bbox[4]) / 2\n                cz = (bbox[2] + bbox[5]) / 2\n                # add pivot position to calculate offset\n                cx = cx + pivot[0]\n                cy = cy + pivot[1]\n                cz = cz + pivot[2]\n                # set selection handle offset to center of bounding box\n                cmds.setAttr(\"{}.selectHandleX\".format(group_name), cx)\n                cmds.setAttr(\"{}.selectHandleY\".format(group_name), cy)\n                cmds.setAttr(\"{}.selectHandleZ\".format(group_name), cz)\n\n            if family == \"rig\":\n                self._post_process_rig(namespace, context, options)\n            else:\n                if \"translate\" in options:\n                    if not attach_to_root and new_nodes:\n                        root_nodes = cmds.ls(new_nodes, assemblies=True,\n                                             long=True)\n                        # we assume only a single root is ever loaded\n                        group_name = root_nodes[0]\n                    cmds.setAttr(\"{}.translate\".format(group_name),\n                                 *options[\"translate\"])\n            return new_nodes\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n        with preserve_modelpanel_cameras(container, log=self.log):\n            super(ReferenceLoader, self).update(container, representation)\n\n        # We also want to lock camera transforms on any new cameras in the\n        # reference or for a camera which might have changed names.\n        members = get_container_members(container)\n        self._lock_camera_transforms(members)\n\n    def _post_process_rig(self, namespace, context, options):\n\n        nodes = self[:]\n        create_rig_animation_instance(\n            nodes, context, namespace, options=options, log=self.log\n        )\n\n    def _lock_camera_transforms(self, nodes):\n        cameras = cmds.ls(nodes, type=\"camera\")\n        if not cameras:\n            return\n\n        # Check the Maya version, lockTransform has been introduced since\n        # Maya 2016.5 Ext 2\n        version = int(cmds.about(version=True))\n        if version >= 2016:\n            for camera in cameras:\n                cmds.camera(camera, edit=True, lockTransform=True)\n        else:\n            self.log.warning(\"This version of Maya does not support locking of\"\n                             \" transforms of cameras.\")\n\n\nclass MayaUSDReferenceLoader(ReferenceLoader):\n    \"\"\"Reference USD file to native Maya nodes using MayaUSDImport reference\"\"\"\n\n    label = \"Reference Maya USD\"\n    families = [\"usd\"]\n    representations = [\"usd\"]\n    extensions = {\"usd\", \"usda\", \"usdc\"}\n\n    options = ReferenceLoader.options + [\n        qargparse.Boolean(\n            \"readAnimData\",\n            label=\"Load anim data\",\n            default=True,\n            help=\"Load animation data from USD file\"\n        ),\n        qargparse.Boolean(\n            \"useAsAnimationCache\",\n            label=\"Use as animation cache\",\n            default=True,\n            help=(\n                \"Imports geometry prims with time-sampled point data using a \"\n                \"point-based deformer that references the imported \"\n                \"USD file.\\n\"\n                \"This provides better import and playback performance when \"\n                \"importing time-sampled geometry from USD, and should \"\n                \"reduce the weight of the resulting Maya scene.\"\n            )\n        ),\n        qargparse.Boolean(\n            \"importInstances\",\n            label=\"Import instances\",\n            default=True,\n            help=(\n                \"Import USD instanced geometries as Maya instanced shapes. \"\n                \"Will flatten the scene otherwise.\"\n            )\n        ),\n        qargparse.String(\n            \"primPath\",\n            label=\"Prim Path\",\n            default=\"/\",\n            help=(\n                \"Name of the USD scope where traversing will begin.\\n\"\n                \"The prim at the specified primPath (including the prim) will \"\n                \"be imported.\\n\"\n                \"Specifying the pseudo-root (/) means you want \"\n                \"to import everything in the file.\\n\"\n                \"If the passed prim path is empty, it will first try to \"\n                \"import the defaultPrim for the rootLayer if it exists.\\n\"\n                \"Otherwise, it will behave as if the pseudo-root was passed \"\n                \"in.\"\n            )\n        )\n    ]\n\n    file_type = \"USD Import\"\n\n    def process_reference(self, context, name, namespace, options):\n        cmds.loadPlugin(\"mayaUsdPlugin\", quiet=True)\n\n        def bool_option(key, default):\n            # Shorthand for getting optional boolean file option from options\n            value = int(bool(options.get(key, default)))\n            return \"{}={}\".format(key, value)\n\n        def string_option(key, default):\n            # Shorthand for getting optional string file option from options\n            value = str(options.get(key, default))\n            return \"{}={}\".format(key, value)\n\n        options[\"file_options\"] = \";\".join([\n            string_option(\"primPath\", default=\"/\"),\n            bool_option(\"importInstances\", default=True),\n            bool_option(\"useAsAnimationCache\", default=True),\n            bool_option(\"readAnimData\", default=True),\n            # TODO: Expose more parameters\n            # \"preferredMaterial=none\",\n            # \"importRelativeTextures=Automatic\",\n            # \"useCustomFrameRange=0\",\n            # \"startTime=0\",\n            # \"endTime=0\",\n            # \"importUSDZTextures=0\"\n        ])\n        options[\"file_type\"] = self.file_type\n\n        return super(MayaUSDReferenceLoader, self).process_reference(\n            context, name, namespace, options\n        )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_rendersetup.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Load and update RenderSetup settings.\n\nWorking with RenderSetup setting is Maya is done utilizing json files.\nWhen this json is loaded, it will overwrite all settings on RenderSetup\ninstance.\n\"\"\"\n\nimport json\nimport sys\nimport six\n\nfrom openpype.pipeline import (\n    load,\n    get_representation_path\n)\nfrom openpype.hosts.maya.api import lib\nfrom openpype.hosts.maya.api.pipeline import containerise\n\nfrom maya import cmds\nimport maya.app.renderSetup.model.renderSetup as renderSetup\n\n\nclass RenderSetupLoader(load.LoaderPlugin):\n    \"\"\"Load json preset for RenderSetup overwriting current one.\"\"\"\n\n    families = [\"rendersetup\"]\n    representations = [\"json\"]\n    defaults = ['Main']\n\n    label = \"Load RenderSetup template\"\n    icon = \"tablet\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data):\n        \"\"\"Load RenderSetup settings.\"\"\"\n\n        # from openpype.hosts.maya.api.lib import namespaced\n\n        asset = context['asset']['name']\n        namespace = namespace or lib.unique_namespace(\n            asset + \"_\",\n            prefix=\"_\" if asset[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n        path = self.filepath_from_context(context)\n        self.log.info(\">>> loading json [ {} ]\".format(path))\n        with open(path, \"r\") as file:\n            renderSetup.instance().decode(\n                json.load(file), renderSetup.DECODE_AND_OVERWRITE, None)\n\n        nodes = []\n        null = cmds.sets(name=\"null_SET\", empty=True)\n        nodes.append(null)\n\n        self[:] = nodes\n        if not nodes:\n            return\n\n        self.log.info(\">>> containerising [ {} ]\".format(name))\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n    def remove(self, container):\n        \"\"\"Remove RenderSetup settings instance.\"\"\"\n        from maya import cmds\n\n        container_name = container[\"objectName\"]\n\n        self.log.info(\"Removing '%s' from Maya..\" % container[\"name\"])\n\n        container_content = cmds.sets(container_name, query=True)\n        nodes = cmds.ls(container_content, long=True)\n\n        nodes.append(container_name)\n\n        try:\n            cmds.delete(nodes)\n        except ValueError:\n            # Already implicitly deleted by Maya upon removing reference\n            pass\n\n    def update(self, container, representation):\n        \"\"\"Update RenderSetup setting by overwriting existing settings.\"\"\"\n        lib.show_message(\n            \"Render setup update\",\n            \"Render setup setting will be overwritten by new version. All \"\n            \"setting specified by user not included in loaded version \"\n            \"will be lost.\")\n        path = get_representation_path(representation)\n        with open(path, \"r\") as file:\n            try:\n                renderSetup.instance().decode(\n                    json.load(file), renderSetup.DECODE_AND_OVERWRITE, None)\n            except Exception:\n                self.log.error(\"There were errors during loading\")\n                six.reraise(*sys.exc_info())\n\n        # Update metadata\n        node = container[\"objectName\"]\n        cmds.setAttr(\"{}.representation\".format(node),\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n        self.log.info(\"... updated\")\n\n    def switch(self, container, representation):\n        \"\"\"Switch representations.\"\"\"\n        self.update(container, representation)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py",
    "content": "import os\n\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import (\n    load,\n    get_representation_path\n)\n# TODO aiVolume doesn't automatically set velocity fps correctly, set manual?\n\n\nclass LoadVDBtoArnold(load.LoaderPlugin):\n    \"\"\"Load OpenVDB for Arnold in aiVolume\"\"\"\n\n    families = [\"vdbcache\"]\n    representations = [\"vdb\"]\n\n    label = \"Load VDB to Arnold\"\n    icon = \"cloud\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data):\n\n        from maya import cmds\n        from openpype.hosts.maya.api.pipeline import containerise\n        from openpype.hosts.maya.api.lib import unique_namespace\n\n        try:\n            family = context[\"representation\"][\"context\"][\"family\"]\n        except ValueError:\n            family = \"vdbcache\"\n\n        # Check if the plugin for arnold is available on the pc\n        try:\n            cmds.loadPlugin(\"mtoa\", quiet=True)\n        except Exception as exc:\n            self.log.error(\"Encountered exception:\\n%s\" % exc)\n            return\n\n        asset = context['asset']\n        asset_name = asset[\"name\"]\n        namespace = namespace or unique_namespace(\n            asset_name + \"_\",\n            prefix=\"_\" if asset_name[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        # Root group\n        label = \"{}:{}\".format(namespace, name)\n        root = cmds.group(name=label, empty=True)\n\n        project_name = context[\"project\"][\"name\"]\n        settings = get_project_settings(project_name)\n        colors = settings['maya']['load']['colors']\n\n        c = colors.get(family)\n        if c is not None:\n            cmds.setAttr(root + \".useOutlinerColor\", 1)\n            cmds.setAttr(root + \".outlinerColor\",\n                         (float(c[0]) / 255),\n                         (float(c[1]) / 255),\n                         (float(c[2]) / 255)\n                         )\n\n        # Create VRayVolumeGrid\n        grid_node = cmds.createNode(\"aiVolume\",\n                                    name=\"{}Shape\".format(root),\n                                    parent=root)\n\n        path = self.filepath_from_context(context)\n        self._set_path(grid_node,\n                       path=path,\n                       representation=context[\"representation\"])\n\n        # Lock the shape node so the user can't delete the transform/shape\n        # as if it was referenced\n        cmds.lockNode(grid_node, lock=True)\n\n        nodes = [root, grid_node]\n        self[:] = nodes\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n\n        from maya import cmds\n\n        path = get_representation_path(representation)\n\n        # Find VRayVolumeGrid\n        members = cmds.sets(container['objectName'], query=True)\n        grid_nodes = cmds.ls(members, type=\"aiVolume\", long=True)\n        assert len(grid_nodes) == 1, \"This is a bug\"\n\n        # Update the VRayVolumeGrid\n        self._set_path(grid_nodes[0], path=path, representation=representation)\n\n        # Update container representation\n        cmds.setAttr(container[\"objectName\"] + \".representation\",\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n\n        from maya import cmds\n\n        # Get all members of the avalon container, ensure they are unlocked\n        # and delete everything\n        members = cmds.sets(container['objectName'], query=True)\n        cmds.lockNode(members, lock=False)\n        cmds.delete([container['objectName']] + members)\n\n        # Clean up the namespace\n        try:\n            cmds.namespace(removeNamespace=container['namespace'],\n                           deleteNamespaceContent=True)\n        except RuntimeError:\n            pass\n\n    @staticmethod\n    def _set_path(grid_node,\n                  path,\n                  representation):\n        \"\"\"Apply the settings for the VDB path to the aiVolume node\"\"\"\n        from maya import cmds\n\n        if not os.path.exists(path):\n            raise RuntimeError(\"Path does not exist: %s\" % path)\n\n        is_sequence = bool(representation[\"context\"].get(\"frame\"))\n        cmds.setAttr(grid_node + \".useFrameExtension\", is_sequence)\n\n        # Set file path\n        cmds.setAttr(grid_node + \".filename\", path, type=\"string\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py",
    "content": "import os\n\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import (\n    load,\n    get_representation_path\n)\n\n\nclass LoadVDBtoRedShift(load.LoaderPlugin):\n    \"\"\"Load OpenVDB in a Redshift Volume Shape\n\n    Note that the RedshiftVolumeShape is created without a RedshiftVolume\n    shader assigned. To get the Redshift volume to render correctly assign\n    a RedshiftVolume shader (in the Hypershade) and set the density, scatter\n    and emission channels to the channel names of the volumes in the VDB file.\n\n    \"\"\"\n\n    families = [\"vdbcache\"]\n    representations = [\"vdb\"]\n\n    label = \"Load VDB to RedShift\"\n    icon = \"cloud\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n\n        from maya import cmds\n        from openpype.hosts.maya.api.pipeline import containerise\n        from openpype.hosts.maya.api.lib import unique_namespace\n\n        try:\n            family = context[\"representation\"][\"context\"][\"family\"]\n        except ValueError:\n            family = \"vdbcache\"\n\n        # Check if the plugin for redshift is available on the pc\n        try:\n            cmds.loadPlugin(\"redshift4maya\", quiet=True)\n        except Exception as exc:\n            self.log.error(\"Encountered exception:\\n%s\" % exc)\n            return\n\n        # Check if viewport drawing engine is Open GL Core (compat)\n        render_engine = None\n        compatible = \"OpenGL\"\n        if cmds.optionVar(exists=\"vp2RenderingEngine\"):\n            render_engine = cmds.optionVar(query=\"vp2RenderingEngine\")\n\n        if not render_engine or not render_engine.startswith(compatible):\n            raise RuntimeError(\"Current scene's settings are incompatible.\"\n                               \"See Preferences > Display > Viewport 2.0 to \"\n                               \"set the render engine to '%s<type>'\"\n                               % compatible)\n\n        asset = context['asset']\n\n        asset_name = asset[\"name\"]\n        namespace = namespace or unique_namespace(\n            asset_name + \"_\",\n            prefix=\"_\" if asset_name[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        # Root group\n        label = \"{}:{}\".format(namespace, name)\n        root = cmds.createNode(\"transform\", name=label)\n\n        project_name = context[\"project\"][\"name\"]\n        settings = get_project_settings(project_name)\n        colors = settings['maya']['load']['colors']\n\n        c = colors.get(family)\n        if c is not None:\n            cmds.setAttr(root + \".useOutlinerColor\", 1)\n            cmds.setAttr(root + \".outlinerColor\",\n                (float(c[0])/255),\n                (float(c[1])/255),\n                (float(c[2])/255)\n            )\n\n        # Create VR\n        volume_node = cmds.createNode(\"RedshiftVolumeShape\",\n                                      name=\"{}RVSShape\".format(label),\n                                      parent=root)\n\n        self._set_path(volume_node,\n                       path=self.filepath_from_context(context),\n                       representation=context[\"representation\"])\n\n        nodes = [root, volume_node]\n        self[:] = nodes\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        from maya import cmds\n\n        path = get_representation_path(representation)\n\n        # Find VRayVolumeGrid\n        members = cmds.sets(container['objectName'], query=True)\n        grid_nodes = cmds.ls(members, type=\"RedshiftVolumeShape\", long=True)\n        assert len(grid_nodes) == 1, \"This is a bug\"\n\n        # Update the VRayVolumeGrid\n        self._set_path(grid_nodes[0], path=path, representation=representation)\n\n        # Update container representation\n        cmds.setAttr(container[\"objectName\"] + \".representation\",\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n\n    def remove(self, container):\n        from maya import cmds\n\n        # Get all members of the avalon container, ensure they are unlocked\n        # and delete everything\n        members = cmds.sets(container['objectName'], query=True)\n        cmds.lockNode(members, lock=False)\n        cmds.delete([container['objectName']] + members)\n\n        # Clean up the namespace\n        try:\n            cmds.namespace(removeNamespace=container['namespace'],\n                           deleteNamespaceContent=True)\n        except RuntimeError:\n            pass\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    @staticmethod\n    def _set_path(grid_node,\n                  path,\n                  representation):\n        \"\"\"Apply the settings for the VDB path to the RedshiftVolumeShape\"\"\"\n        from maya import cmds\n\n        if not os.path.exists(path):\n            raise RuntimeError(\"Path does not exist: %s\" % path)\n\n        is_sequence = bool(representation[\"context\"].get(\"frame\"))\n        cmds.setAttr(grid_node + \".useFrameExtension\", is_sequence)\n\n        # Set file path\n        cmds.setAttr(grid_node + \".fileName\", path, type=\"string\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_vdb_to_vray.py",
    "content": "import os\n\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import (\n    load,\n    get_representation_path\n)\n\nfrom maya import cmds\n\n# List of 3rd Party Channels Mapping names for VRayVolumeGrid\n# See: https://docs.chaosgroup.com/display/VRAY4MAYA/Input\n#      #Input-3rdPartyChannelsMapping\nTHIRD_PARTY_CHANNELS = {\n    2: \"Smoke\",\n    1: \"Temperature\",\n    10: \"Fuel\",\n    4: \"Velocity.x\",\n    5: \"Velocity.y\",\n    6: \"Velocity.z\",\n    7: \"Red\",\n    8: \"Green\",\n    9: \"Blue\",\n    14: \"Wavelet Energy\",\n    19: \"Wavelet.u\",\n    20: \"Wavelet.v\",\n    21: \"Wavelet.w\",\n    # These are not in UI or documentation but V-Ray does seem to set these.\n    15: \"AdvectionOrigin.x\",\n    16: \"AdvectionOrigin.y\",\n    17: \"AdvectionOrigin.z\",\n\n}\n\n\ndef _fix_duplicate_vvg_callbacks():\n    \"\"\"Workaround to kill duplicate VRayVolumeGrids attribute callbacks.\n\n    This fixes a huge lag in Maya on switching 3rd Party Channels Mappings\n    or to different .vdb file paths because it spams an attribute changed\n    callback: `vvgUserChannelMappingsUpdateUI`.\n\n    ChaosGroup bug ticket: 154-008-9890\n\n    Found with:\n        - Maya 2019.2 on Windows 10\n        - V-Ray: V-Ray Next for Maya, update 1 version 4.12.01.00001\n\n    Bug still present in:\n        - Maya 2022.1 on Windows 10\n        - V-Ray 5 for Maya, Update 2.1 (v5.20.01 from Dec 16 2021)\n\n    \"\"\"\n    # todo(roy): Remove when new V-Ray release fixes duplicate calls\n\n    jobs = cmds.scriptJob(listJobs=True)\n\n    matched = set()\n    for entry in jobs:\n        # Remove the number\n        index, callback = entry.split(\":\", 1)\n        callback = callback.strip()\n\n        # Detect whether it is a `vvgUserChannelMappingsUpdateUI`\n        # attribute change callback\n        if callback.startswith('\"-runOnce\" 1 \"-attributeChange\" \"'):\n            if '\"vvgUserChannelMappingsUpdateUI(' in callback:\n                if callback in matched:\n                    # If we've seen this callback before then\n                    # delete the duplicate callback\n                    cmds.scriptJob(kill=int(index))\n                else:\n                    matched.add(callback)\n\n\nclass LoadVDBtoVRay(load.LoaderPlugin):\n    \"\"\"Load OpenVDB in a V-Ray Volume Grid\"\"\"\n\n    families = [\"vdbcache\"]\n    representations = [\"vdb\"]\n\n    label = \"Load VDB to VRay\"\n    icon = \"cloud\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data):\n\n        from openpype.hosts.maya.api.lib import unique_namespace\n        from openpype.hosts.maya.api.pipeline import containerise\n\n        path = self.filepath_from_context(context)\n        assert os.path.exists(path), (\n            \"Path does not exist: %s\" % path\n        )\n\n        try:\n            family = context[\"representation\"][\"context\"][\"family\"]\n        except ValueError:\n            family = \"vdbcache\"\n\n        # Ensure V-ray is loaded with the vrayvolumegrid\n        if not cmds.pluginInfo(\"vrayformaya\", query=True, loaded=True):\n            cmds.loadPlugin(\"vrayformaya\")\n        if not cmds.pluginInfo(\"vrayvolumegrid\", query=True, loaded=True):\n            cmds.loadPlugin(\"vrayvolumegrid\")\n\n        # Check if viewport drawing engine is Open GL Core (compat)\n        render_engine = None\n        compatible = \"OpenGLCoreProfileCompat\"\n        if cmds.optionVar(exists=\"vp2RenderingEngine\"):\n            render_engine = cmds.optionVar(query=\"vp2RenderingEngine\")\n\n        if not render_engine or render_engine != compatible:\n            self.log.warning(\"Current scene's settings are incompatible.\"\n                             \"See Preferences > Display > Viewport 2.0 to \"\n                             \"set the render engine to '%s'\" % compatible)\n\n        asset = context['asset']\n        asset_name = asset[\"name\"]\n        namespace = namespace or unique_namespace(\n            asset_name + \"_\",\n            prefix=\"_\" if asset_name[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        # Root group\n        label = \"{}:{}_VDB\".format(namespace, name)\n        root = cmds.group(name=label, empty=True)\n\n        project_name = context[\"project\"][\"name\"]\n        settings = get_project_settings(project_name)\n        colors = settings['maya']['load']['colors']\n\n        c = colors.get(family)\n        if c is not None:\n            cmds.setAttr(root + \".useOutlinerColor\", 1)\n            cmds.setAttr(root + \".outlinerColor\",\n                         float(c[0]) / 255,\n                         float(c[1]) / 255,\n                         float(c[2]) / 255)\n\n        # Create VRayVolumeGrid\n        grid_node = cmds.createNode(\"VRayVolumeGrid\",\n                                    name=\"{}Shape\".format(label),\n                                    parent=root)\n\n        # Ensure .currentTime is connected to time1.outTime\n        cmds.connectAttr(\"time1.outTime\", grid_node + \".currentTime\")\n\n        # Set path\n        self._set_path(grid_node, path, show_preset_popup=True)\n\n        # Lock the shape node so the user can't delete the transform/shape\n        # as if it was referenced\n        cmds.lockNode(grid_node, lock=True)\n\n        nodes = [root, grid_node]\n        self[:] = nodes\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n    def _set_path(self, grid_node, path, show_preset_popup=True):\n\n        from openpype.hosts.maya.api.lib import attribute_values\n        from maya import cmds\n\n        def _get_filename_from_folder(path):\n            # Using the sequence of .vdb files we check the frame range, etc.\n            # to set the filename with #### padding.\n            files = sorted(x for x in os.listdir(path) if x.endswith(\".vdb\"))\n            if not files:\n                raise RuntimeError(\"Couldn't find .vdb files in: %s\" % path)\n\n            if len(files) == 1:\n                # Ensure check for single file is also done in folder\n                fname = files[0]\n            else:\n                # Sequence\n                import clique\n                # todo: check support for negative frames as input\n                collections, remainder = clique.assemble(files)\n                assert len(collections) == 1, (\n                    \"Must find a single image sequence, \"\n                    \"found: %s\" % (collections,)\n                )\n                collection = collections[0]\n\n                fname = collection.format('{head}{{padding}}{tail}')\n                padding = collection.padding\n                if padding == 0:\n                    # Clique doesn't provide padding if the frame number never\n                    # starts with a zero and thus has never any visual padding.\n                    # So we fall back to the smallest frame number as padding.\n                    padding = min(len(str(i)) for i in collection.indexes)\n\n                # Supply frame/padding with # signs\n                padding_str = \"#\" * padding\n                fname = fname.format(padding=padding_str)\n\n            return os.path.join(path, fname)\n\n        # The path is either a single file or sequence in a folder so\n        # we do a quick lookup for our files\n        if os.path.isfile(path):\n            path = os.path.dirname(path)\n        path = _get_filename_from_folder(path)\n\n        # Even when not applying a preset V-Ray will reset the 3rd Party\n        # Channels Mapping of the VRayVolumeGrid when setting the .inPath\n        # value. As such we try and preserve the values ourselves.\n        # Reported as ChaosGroup bug ticket: 154-011-2909 \n        # todo(roy): Remove when new V-Ray release preserves values\n        original_user_mapping = cmds.getAttr(grid_node + \".usrchmap\") or \"\"\n\n        # Workaround for V-Ray bug: fix lag on path change, see function\n        _fix_duplicate_vvg_callbacks()\n\n        # Suppress preset pop-up if we want.\n        popup_attr = \"{0}.inDontOfferPresets\".format(grid_node)\n        popup = {popup_attr: not show_preset_popup}\n        with attribute_values(popup):\n            cmds.setAttr(grid_node + \".inPath\", path, type=\"string\")\n\n        # Reapply the 3rd Party channels user mapping when no preset popup\n        # was shown to the user\n        if not show_preset_popup:\n            channels = cmds.getAttr(grid_node + \".usrchmapallch\").split(\";\")\n            channels = set(channels)  # optimize lookup\n            restored_mapping = \"\"\n            for entry in original_user_mapping.split(\";\"):\n                if not entry:\n                    # Ignore empty entries\n                    continue\n\n                # If 3rd Party Channels selection channel still exists then\n                # add it again.\n                index, channel = entry.split(\",\")\n                attr = THIRD_PARTY_CHANNELS.get(int(index),\n                                                # Fallback for when a mapping\n                                                # was set that is not in the\n                                                # documentation\n                                                \"???\")\n                if channel in channels:\n                    restored_mapping += entry + \";\"\n                else:\n                    self.log.warning(\"Can't preserve '%s' mapping due to \"\n                                     \"missing channel '%s' on node: \"\n                                     \"%s\" % (attr, channel, grid_node))\n\n            if restored_mapping:\n                cmds.setAttr(grid_node + \".usrchmap\",\n                             restored_mapping,\n                             type=\"string\")\n\n    def update(self, container, representation):\n\n        path = get_representation_path(representation)\n\n        # Find VRayVolumeGrid\n        members = cmds.sets(container['objectName'], query=True)\n        grid_nodes = cmds.ls(members, type=\"VRayVolumeGrid\", long=True)\n        assert len(grid_nodes) > 0, \"This is a bug\"\n\n        # Update the VRayVolumeGrid\n        for grid_node in grid_nodes:\n            self._set_path(grid_node, path=path, show_preset_popup=False)\n\n        # Update container representation\n        cmds.setAttr(container[\"objectName\"] + \".representation\",\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n\n        # Get all members of the avalon container, ensure they are unlocked\n        # and delete everything\n        members = cmds.sets(container['objectName'], query=True)\n        cmds.lockNode(members, lock=False)\n        cmds.delete([container['objectName']] + members)\n\n        # Clean up the namespace\n        try:\n            cmds.namespace(removeNamespace=container['namespace'],\n                           deleteNamespaceContent=True)\n        except RuntimeError:\n            pass\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_vrayproxy.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Loader for Vray Proxy files.\n\nIf there are Alembics published along vray proxy (in the same version),\nloader will use them instead of native vray vrmesh format.\n\n\"\"\"\nimport os\n\nimport maya.cmds as cmds\n\nfrom openpype.client import get_representation_by_name\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import (\n    load,\n    get_current_project_name,\n    get_representation_path,\n)\nfrom openpype.hosts.maya.api.lib import (\n    maintained_selection,\n    namespaced,\n    unique_namespace\n)\nfrom openpype.hosts.maya.api.pipeline import containerise\n\n\nclass VRayProxyLoader(load.LoaderPlugin):\n    \"\"\"Load VRay Proxy with Alembic or VrayMesh.\"\"\"\n\n    families = [\"vrayproxy\", \"model\", \"pointcache\", \"animation\"]\n    representations = [\"vrmesh\", \"abc\"]\n\n    label = \"Import VRay Proxy\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, options=None):\n        # type: (dict, str, str, dict) -> None\n        \"\"\"Loader entry point.\n\n        Args:\n            context (dict): Loaded representation context.\n            name (str): Name of container.\n            namespace (str): Optional namespace name.\n            options (dict): Optional loader options.\n\n        \"\"\"\n\n        try:\n            family = context[\"representation\"][\"context\"][\"family\"]\n        except ValueError:\n            family = \"vrayproxy\"\n\n        #  get all representations for this version\n        filename = self._get_abc(context[\"version\"][\"_id\"])\n        if not filename:\n            filename = self.filepath_from_context(context)\n\n        asset_name = context['asset'][\"name\"]\n        namespace = namespace or unique_namespace(\n            asset_name + \"_\",\n            prefix=\"_\" if asset_name[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        # Ensure V-Ray for Maya is loaded.\n        cmds.loadPlugin(\"vrayformaya\", quiet=True)\n\n        with maintained_selection():\n            cmds.namespace(addNamespace=namespace)\n            with namespaced(namespace, new=False):\n                nodes, group_node = self.create_vray_proxy(\n                    name, filename=filename)\n\n        self[:] = nodes\n        if not nodes:\n            return\n\n        # colour the group node\n        project_name = context[\"project\"][\"name\"]\n        settings = get_project_settings(project_name)\n        colors = settings['maya']['load']['colors']\n        c = colors.get(family)\n        if c is not None:\n            cmds.setAttr(\"{0}.useOutlinerColor\".format(group_node), 1)\n            cmds.setAttr(\n                \"{0}.outlinerColor\".format(group_node),\n                (float(c[0]) / 255),\n                (float(c[1]) / 255),\n                (float(c[2]) / 255)\n            )\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n        # type: (dict, dict) -> None\n        \"\"\"Update container with specified representation.\"\"\"\n        node = container['objectName']\n        assert cmds.objExists(node), \"Missing container\"\n\n        members = cmds.sets(node, query=True) or []\n        vraymeshes = cmds.ls(members, type=\"VRayProxy\")\n        assert vraymeshes, \"Cannot find VRayMesh in container\"\n\n        #  get all representations for this version\n        filename = (\n            self._get_abc(representation[\"parent\"])\n            or get_representation_path(representation)\n        )\n\n        for vray_mesh in vraymeshes:\n            cmds.setAttr(\"{}.fileName\".format(vray_mesh),\n                         filename,\n                         type=\"string\")\n\n        # Update metadata\n        cmds.setAttr(\"{}.representation\".format(node),\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n\n    def remove(self, container):\n        # type: (dict) -> None\n        \"\"\"Remove loaded container.\"\"\"\n        # Delete container and its contents\n        if cmds.objExists(container['objectName']):\n            members = cmds.sets(container['objectName'], query=True) or []\n            cmds.delete([container['objectName']] + members)\n\n        # Remove the namespace, if empty\n        namespace = container['namespace']\n        if cmds.namespace(exists=namespace):\n            members = cmds.namespaceInfo(namespace, listNamespace=True)\n            if not members:\n                cmds.namespace(removeNamespace=namespace)\n            else:\n                self.log.warning(\"Namespace not deleted because it \"\n                                 \"still has members: %s\", namespace)\n\n    def switch(self, container, representation):\n        # type: (dict, dict) -> None\n        \"\"\"Switch loaded representation.\"\"\"\n        self.update(container, representation)\n\n    def create_vray_proxy(self, name, filename):\n        # type: (str, str) -> (list, str)\n        \"\"\"Re-create the structure created by VRay to support vrmeshes\n\n        Args:\n            name (str): Name of the asset.\n            filename (str): File name of vrmesh.\n\n        Returns:\n            nodes(list)\n\n        \"\"\"\n\n        if name is None:\n            name = os.path.splitext(os.path.basename(filename))[0]\n\n        parent = cmds.createNode(\"transform\", name=name)\n        proxy = cmds.createNode(\n            \"VRayProxy\", name=\"{}Shape\".format(name), parent=parent)\n        cmds.setAttr(proxy + \".fileName\", filename, type=\"string\")\n        cmds.connectAttr(\"time1.outTime\", proxy + \".currentFrame\")\n\n        return [parent, proxy], parent\n\n    def _get_abc(self, version_id):\n        # type: (str) -> str\n        \"\"\"Get abc representation file path if present.\n\n        If here is published Alembic (abc) representation published along\n        vray proxy, get is file path.\n\n        Args:\n            version_id (str): Version hash id.\n\n        Returns:\n            str: Path to file.\n            None: If abc not found.\n\n        \"\"\"\n        self.log.debug(\n            \"Looking for abc in published representations of this version.\")\n        project_name = get_current_project_name()\n        abc_rep = get_representation_by_name(project_name, \"abc\", version_id)\n        if abc_rep:\n            self.log.debug(\"Found, we'll link alembic to vray proxy.\")\n            file_name = get_representation_path(abc_rep)\n            self.log.debug(\"File: {}\".format(file_name))\n            return file_name\n\n        return \"\"\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_vrayscene.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nimport maya.cmds as cmds  # noqa\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import (\n    load,\n    get_representation_path\n)\nfrom openpype.hosts.maya.api.lib import (\n    maintained_selection,\n    namespaced,\n    unique_namespace\n)\nfrom openpype.hosts.maya.api.pipeline import containerise\n\n\nclass VRaySceneLoader(load.LoaderPlugin):\n    \"\"\"Load Vray scene\"\"\"\n\n    families = [\"vrayscene_layer\"]\n    representations = [\"vrscene\"]\n\n    label = \"Import VRay Scene\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data):\n\n\n        try:\n            family = context[\"representation\"][\"context\"][\"family\"]\n        except ValueError:\n            family = \"vrayscene_layer\"\n\n        asset_name = context['asset'][\"name\"]\n        namespace = namespace or unique_namespace(\n            asset_name + \"_\",\n            prefix=\"_\" if asset_name[0].isdigit() else \"\",\n            suffix=\"_\",\n        )\n\n        # Ensure V-Ray for Maya is loaded.\n        cmds.loadPlugin(\"vrayformaya\", quiet=True)\n\n        with maintained_selection():\n            cmds.namespace(addNamespace=namespace)\n            with namespaced(namespace, new=False):\n                nodes, root_node = self.create_vray_scene(\n                    name,\n                    filename=self.filepath_from_context(context)\n                )\n\n        self[:] = nodes\n        if not nodes:\n            return\n\n        # colour the group node\n        project_name = context[\"project\"][\"name\"]\n        settings = get_project_settings(project_name)\n        colors = settings['maya']['load']['colors']\n        c = colors.get(family)\n        if c is not None:\n            cmds.setAttr(\"{0}.useOutlinerColor\".format(root_node), 1)\n            cmds.setAttr(\"{0}.outlinerColor\".format(root_node),\n                (float(c[0])/255),\n                (float(c[1])/255),\n                (float(c[2])/255)\n            )\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__)\n\n    def update(self, container, representation):\n\n        node = container['objectName']\n        assert cmds.objExists(node), \"Missing container\"\n\n        members = cmds.sets(node, query=True) or []\n        vraymeshes = cmds.ls(members, type=\"VRayScene\")\n        assert vraymeshes, \"Cannot find VRayScene in container\"\n\n        filename = get_representation_path(representation)\n\n        for vray_mesh in vraymeshes:\n            cmds.setAttr(\"{}.FilePath\".format(vray_mesh),\n                         filename,\n                         type=\"string\")\n\n        # Update metadata\n        cmds.setAttr(\"{}.representation\".format(node),\n                     str(representation[\"_id\"]),\n                     type=\"string\")\n\n    def remove(self, container):\n\n        # Delete container and its contents\n        if cmds.objExists(container['objectName']):\n            members = cmds.sets(container['objectName'], query=True) or []\n            cmds.delete([container['objectName']] + members)\n\n        # Remove the namespace, if empty\n        namespace = container['namespace']\n        if cmds.namespace(exists=namespace):\n            members = cmds.namespaceInfo(namespace, listNamespace=True)\n            if not members:\n                cmds.namespace(removeNamespace=namespace)\n            else:\n                self.log.warning(\"Namespace not deleted because it \"\n                                 \"still has members: %s\", namespace)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def create_vray_scene(self, name, filename):\n        \"\"\"Re-create the structure created by VRay to support vrscenes\n\n        Args:\n            name(str): name of the asset\n\n        Returns:\n            nodes(list)\n        \"\"\"\n\n        # Create nodes\n        mesh_node_name = \"VRayScene_{}\".format(name)\n\n        trans = cmds.createNode(\n            \"transform\", name=mesh_node_name)\n        vray_scene = cmds.createNode(\n            \"VRayScene\", name=\"{}_VRSCN\".format(mesh_node_name), parent=trans)\n        mesh = cmds.createNode(\n            \"mesh\", name=\"{}_Shape\".format(mesh_node_name), parent=trans)\n\n        cmds.connectAttr(\n            \"{}.outMesh\".format(vray_scene), \"{}.inMesh\".format(mesh))\n\n        cmds.setAttr(\"{}.FilePath\".format(vray_scene), filename, type=\"string\")\n\n        # Lock the shape nodes so the user cannot delete these\n        cmds.lockNode(mesh, lock=True)\n        cmds.lockNode(vray_scene, lock=True)\n\n        # Create important connections\n        cmds.connectAttr(\"time1.outTime\",\n                         \"{0}.inputTime\".format(trans))\n\n        # Connect mesh to initialShadingGroup\n        cmds.sets([mesh], forceElement=\"initialShadingGroup\")\n\n        nodes = [trans, vray_scene, mesh]\n\n        # Fix: Force refresh so the mesh shows correctly after creation\n        cmds.refresh()\n\n        return nodes, trans\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_xgen.py",
    "content": "import os\nimport shutil\n\nimport maya.cmds as cmds\nimport xgenm\n\nfrom qtpy import QtWidgets\n\nimport openpype.hosts.maya.api.plugin\nfrom openpype.hosts.maya.api.lib import (\n    maintained_selection,\n    get_container_members,\n    attribute_values,\n    write_xgen_file\n)\nfrom openpype.hosts.maya.api import current_file\nfrom openpype.pipeline import get_representation_path\n\n\nclass XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):\n    \"\"\"Load Xgen as reference\"\"\"\n\n    families = [\"xgen\"]\n    representations = [\"ma\", \"mb\"]\n\n    label = \"Reference Xgen\"\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def get_xgen_xgd_paths(self, palette):\n        _, maya_extension = os.path.splitext(current_file())\n        xgen_file = current_file().replace(\n            maya_extension,\n            \"__{}.xgen\".format(palette.replace(\"|\", \"\").replace(\":\", \"__\"))\n        )\n        xgd_file = xgen_file.replace(\".xgen\", \".xgd\")\n        return xgen_file, xgd_file\n\n    def process_reference(self, context, name, namespace, options):\n        # Validate workfile has a path.\n        if current_file() is None:\n            QtWidgets.QMessageBox.warning(\n                None,\n                \"\",\n                \"Current workfile has not been saved. Please save the workfile\"\n                \" before loading an Xgen.\"\n            )\n            return\n\n        maya_filepath = self.prepare_root_value(\n            file_url=self.filepath_from_context(context),\n            project_name=context[\"project\"][\"name\"]\n        )\n\n        # Reference xgen. Xgen does not like being referenced in under a group.\n        with maintained_selection():\n            nodes = cmds.file(\n                maya_filepath,\n                namespace=namespace,\n                sharedReferenceFile=False,\n                reference=True,\n                returnNewNodes=True\n            )\n\n            xgen_palette = cmds.ls(\n                nodes, type=\"xgmPalette\", long=True\n            )[0].replace(\"|\", \"\")\n\n            xgen_file, xgd_file = self.get_xgen_xgd_paths(xgen_palette)\n            self.set_palette_attributes(xgen_palette, xgen_file, xgd_file)\n\n            # Change the cache and disk values of xgDataPath and xgProjectPath\n            # to ensure paths are setup correctly.\n            project_path = os.path.dirname(current_file()).replace(\"\\\\\", \"/\")\n            xgenm.setAttr(\"xgProjectPath\", project_path, xgen_palette)\n            data_path = \"${{PROJECT}}xgen/collections/{};{}\".format(\n                xgen_palette.replace(\":\", \"__ns__\"),\n                xgenm.getAttr(\"xgDataPath\", xgen_palette)\n            )\n            xgenm.setAttr(\"xgDataPath\", data_path, xgen_palette)\n\n            data = {\"xgProjectPath\": project_path, \"xgDataPath\": data_path}\n            write_xgen_file(data, xgen_file)\n\n            # This create an expression attribute of float. If we did not add\n            # any changes to collection, then Xgen does not create an xgd file\n            # on save. This gives errors when launching the workfile again due\n            # to trying to find the xgd file.\n            name = \"custom_float_ignore\"\n            if name not in xgenm.customAttrs(xgen_palette):\n                xgenm.addCustomAttr(\n                    \"custom_float_ignore\", xgen_palette\n                )\n\n            shapes = cmds.ls(nodes, shapes=True, long=True)\n\n            new_nodes = (list(set(nodes) - set(shapes)))\n\n            self[:] = new_nodes\n\n        return new_nodes\n\n    def set_palette_attributes(self, xgen_palette, xgen_file, xgd_file):\n        cmds.setAttr(\n            \"{}.xgBaseFile\".format(xgen_palette),\n            os.path.basename(xgen_file),\n            type=\"string\"\n        )\n        cmds.setAttr(\n            \"{}.xgFileName\".format(xgen_palette),\n            os.path.basename(xgd_file),\n            type=\"string\"\n        )\n        cmds.setAttr(\"{}.xgExportAsDelta\".format(xgen_palette), True)\n\n    def update(self, container, representation):\n        \"\"\"Workflow for updating Xgen.\n\n        - Export changes to delta file.\n        - Copy and overwrite the workspace .xgen file.\n        - Set collection attributes to not include delta files.\n        - Update xgen maya file reference.\n        - Apply the delta file changes.\n        - Reset collection attributes to include delta files.\n\n        We have to do this workflow because when using referencing of the xgen\n        collection, Maya implicitly imports the Xgen data from the xgen file so\n        we dont have any control over when adding the delta file changes.\n\n        There is an implicit increment of the xgen and delta files, due to\n        using the workfile basename.\n        \"\"\"\n        # Storing current description to try and maintain later.\n        current_description = (\n            xgenm.xgGlobal.DescriptionEditor.currentDescription()\n        )\n\n        container_node = container[\"objectName\"]\n        members = get_container_members(container_node)\n        xgen_palette = cmds.ls(\n            members, type=\"xgmPalette\", long=True\n        )[0].replace(\"|\", \"\")\n        xgen_file, xgd_file = self.get_xgen_xgd_paths(xgen_palette)\n\n        # Export current changes to apply later.\n        xgenm.createDelta(xgen_palette.replace(\"|\", \"\"), xgd_file)\n\n        self.set_palette_attributes(xgen_palette, xgen_file, xgd_file)\n\n        maya_file = get_representation_path(representation)\n        _, extension = os.path.splitext(maya_file)\n        new_xgen_file = maya_file.replace(extension, \".xgen\")\n        data_path = \"\"\n        with open(new_xgen_file, \"r\") as f:\n            for line in f:\n                if line.startswith(\"\\txgDataPath\"):\n                    line = line.rstrip()\n                    data_path = line.split(\"\\t\")[-1]\n                    break\n\n        project_path = os.path.dirname(current_file()).replace(\"\\\\\", \"/\")\n        data_path = \"${{PROJECT}}xgen/collections/{};{}\".format(\n            xgen_palette.replace(\":\", \"__ns__\"),\n            data_path\n        )\n        data = {\"xgProjectPath\": project_path, \"xgDataPath\": data_path}\n        shutil.copy(new_xgen_file, xgen_file)\n        write_xgen_file(data, xgen_file)\n\n        attribute_data = {\n            \"{}.xgFileName\".format(xgen_palette): os.path.basename(xgen_file),\n            \"{}.xgBaseFile\".format(xgen_palette): \"\",\n            \"{}.xgExportAsDelta\".format(xgen_palette): False\n        }\n        with attribute_values(attribute_data):\n            super().update(container, representation)\n\n            xgenm.applyDelta(xgen_palette.replace(\"|\", \"\"), xgd_file)\n\n        # Restore current selected description if it exists.\n        if cmds.objExists(current_description):\n            xgenm.xgGlobal.DescriptionEditor.setCurrentDescription(\n                current_description\n            )\n        # Full UI refresh.\n        xgenm.xgGlobal.DescriptionEditor.refresh(\"Full\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_yeti_cache.py",
    "content": "import os\nimport json\nimport re\nfrom collections import defaultdict\n\nimport clique\nfrom maya import cmds\n\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import (\n    load,\n    get_representation_path\n)\nfrom openpype.hosts.maya.api import lib\nfrom openpype.hosts.maya.api.pipeline import containerise\n\n\n# Do not reset these values on update but only apply on first load\n# to preserve any potential local overrides\nSKIP_UPDATE_ATTRS = {\n    \"displayOutput\",\n    \"viewportDensity\",\n    \"viewportWidth\",\n    \"viewportLength\",\n}\n\n\ndef set_attribute(node, attr, value):\n    \"\"\"Wrapper of set attribute which ignores None values\"\"\"\n    if value is None:\n        return\n    lib.set_attribute(node, attr, value)\n\n\nclass YetiCacheLoader(load.LoaderPlugin):\n    \"\"\"Load Yeti Cache with one or more Yeti nodes\"\"\"\n\n    families = [\"yeticache\", \"yetiRig\"]\n    representations = [\"fur\"]\n\n    label = \"Load Yeti Cache\"\n    order = -9\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        \"\"\"Loads a .fursettings file defining how to load .fur sequences\n\n        A single yeticache or yetiRig can have more than a single pgYetiMaya\n        nodes and thus load more than a single yeti.fur sequence.\n\n        The .fursettings file defines what the node names should be and also\n        what \"cbId\" attribute they should receive to match the original source\n        and allow published looks to also work for Yeti rigs and its caches.\n\n        \"\"\"\n\n        try:\n            family = context[\"representation\"][\"context\"][\"family\"]\n        except ValueError:\n            family = \"yeticache\"\n\n        # Build namespace\n        asset = context[\"asset\"]\n        if namespace is None:\n            namespace = self.create_namespace(asset[\"name\"])\n\n        # Ensure Yeti is loaded\n        if not cmds.pluginInfo(\"pgYetiMaya\", query=True, loaded=True):\n            cmds.loadPlugin(\"pgYetiMaya\", quiet=True)\n\n        # Create Yeti cache nodes according to settings\n        path = self.filepath_from_context(context)\n        settings = self.read_settings(path)\n        nodes = []\n        for node in settings[\"nodes\"]:\n            nodes.extend(self.create_node(namespace, node))\n\n        group_name = \"{}:{}\".format(namespace, name)\n        group_node = cmds.group(nodes, name=group_name)\n        project_name = context[\"project\"][\"name\"]\n\n        settings = get_project_settings(project_name)\n        colors = settings['maya']['load']['colors']\n\n        c = colors.get(family)\n        if c is not None:\n            cmds.setAttr(group_node + \".useOutlinerColor\", 1)\n            cmds.setAttr(group_node + \".outlinerColor\",\n                (float(c[0])/255),\n                (float(c[1])/255),\n                (float(c[2])/255)\n            )\n\n        nodes.append(group_node)\n\n        self[:] = nodes\n\n        return containerise(\n            name=name,\n            namespace=namespace,\n            nodes=nodes,\n            context=context,\n            loader=self.__class__.__name__\n        )\n\n    def remove(self, container):\n\n        from maya import cmds\n\n        namespace = container[\"namespace\"]\n        container_name = container[\"objectName\"]\n\n        self.log.info(\"Removing '%s' from Maya..\" % container[\"name\"])\n\n        container_content = cmds.sets(container_name, query=True)\n        nodes = cmds.ls(container_content, long=True)\n\n        nodes.append(container_name)\n\n        try:\n            cmds.delete(nodes)\n        except ValueError:\n            # Already implicitly deleted by Maya upon removing reference\n            pass\n\n        cmds.namespace(removeNamespace=namespace, deleteNamespaceContent=True)\n\n    def update(self, container, representation):\n\n        namespace = container[\"namespace\"]\n        container_node = container[\"objectName\"]\n\n        path = get_representation_path(representation)\n        settings = self.read_settings(path)\n\n        # Collect scene information of asset\n        set_members = lib.get_container_members(container)\n        container_root = lib.get_container_transforms(container,\n                                                      members=set_members,\n                                                      root=True)\n        scene_nodes = cmds.ls(set_members, type=\"pgYetiMaya\", long=True)\n\n        # Build lookup with cbId as keys\n        scene_lookup = defaultdict(list)\n        for node in scene_nodes:\n            cb_id = lib.get_id(node)\n            scene_lookup[cb_id].append(node)\n\n        # Re-assemble metadata with cbId as keys\n        meta_data_lookup = {n[\"cbId\"]: n for n in settings[\"nodes\"]}\n\n        # Delete nodes by \"cbId\" that are not in the updated version\n        to_delete_lookup = {cb_id for cb_id in scene_lookup.keys() if\n                            cb_id not in meta_data_lookup}\n        if to_delete_lookup:\n\n            # Get nodes and remove entry from lookup\n            to_remove = []\n            for _id in to_delete_lookup:\n                # Get all related nodes\n                shapes = scene_lookup[_id]\n                # Get the parents of all shapes under the ID\n                transforms = cmds.listRelatives(shapes,\n                                                parent=True,\n                                                fullPath=True) or []\n                to_remove.extend(shapes + transforms)\n\n                # Remove id from lookup\n                scene_lookup.pop(_id, None)\n\n            cmds.delete(to_remove)\n\n        for cb_id, node_settings in meta_data_lookup.items():\n\n            if cb_id not in scene_lookup:\n                # Create new nodes\n                self.log.info(\"Creating new nodes ..\")\n\n                new_nodes = self.create_node(namespace, node_settings)\n                cmds.sets(new_nodes, addElement=container_node)\n                cmds.parent(new_nodes, container_root)\n\n            else:\n                # Update the matching nodes\n                scene_nodes = scene_lookup[cb_id]\n                lookup_result = meta_data_lookup[cb_id][\"name\"]\n\n                # Remove namespace if any (e.g.: \"character_01_:head_YNShape\")\n                node_name = lookup_result.rsplit(\":\", 1)[-1]\n\n                for scene_node in scene_nodes:\n\n                    # Get transform node, this makes renaming easier\n                    transforms = cmds.listRelatives(scene_node,\n                                                    parent=True,\n                                                    fullPath=True) or []\n                    assert len(transforms) == 1, \"This is a bug!\"\n\n                    # Get scene node's namespace and rename the transform node\n                    lead = scene_node.rsplit(\":\", 1)[0]\n                    namespace = \":{}\".format(lead.rsplit(\"|\")[-1])\n\n                    new_shape_name = \"{}:{}\".format(namespace, node_name)\n                    new_trans_name = new_shape_name.rsplit(\"Shape\", 1)[0]\n\n                    transform_node = transforms[0]\n                    cmds.rename(transform_node,\n                                new_trans_name,\n                                ignoreShape=False)\n\n                    # Get the newly named shape node\n                    yeti_nodes = cmds.listRelatives(new_trans_name,\n                                                    children=True)\n                    yeti_node = yeti_nodes[0]\n\n                    for attr, value in node_settings[\"attrs\"].items():\n                        if attr in SKIP_UPDATE_ATTRS:\n                            continue\n                        set_attribute(attr, value, yeti_node)\n\n        cmds.setAttr(\"{}.representation\".format(container_node),\n                     str(representation[\"_id\"]),\n                     typ=\"string\")\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    # helper functions\n    def create_namespace(self, asset):\n        \"\"\"Create a unique namespace\n        Args:\n            asset (dict): asset information\n\n        \"\"\"\n\n        asset_name = \"{}_\".format(asset)\n        prefix = \"_\" if asset_name[0].isdigit()else \"\"\n        namespace = lib.unique_namespace(\n            asset_name,\n            prefix=prefix,\n            suffix=\"_\"\n        )\n\n        return namespace\n\n    def get_cache_node_filepath(self, root, node_name):\n        \"\"\"Get the cache file path for one of the yeti nodes.\n\n        All caches with more than 1 frame need cache file name set with `%04d`\n        If the cache has only one frame we return the file name as we assume\n        it is a snapshot.\n\n        This expects the files to be named after the \"node name\" through\n        exports with <Name> in Yeti.\n\n        Args:\n            root(str): Folder containing cache files to search in.\n            node_name(str): Node name to search cache files for\n\n        Returns:\n            str: Cache file path value needed for cacheFileName attribute\n\n        \"\"\"\n\n        name = node_name.replace(\":\", \"_\")\n        pattern = r\"^({name})(\\.[0-9]+)?(\\.fur)$\".format(name=re.escape(name))\n\n        files = [fname for fname in os.listdir(root) if re.match(pattern,\n                                                                 fname)]\n        if not files:\n            self.log.error(\"Could not find cache files for '{}' \"\n                           \"with pattern {}\".format(node_name, pattern))\n            return\n\n        if len(files) == 1:\n            # Single file\n            return os.path.join(root, files[0])\n\n        # Get filename for the sequence with padding\n        collections, remainder = clique.assemble(files)\n        assert not remainder, \"This is a bug\"\n        assert len(collections) == 1, \"This is a bug\"\n        collection = collections[0]\n\n        # Formats name as {head}%d{tail} like cache.%04d.fur\n        fname = collection.format(\"{head}{padding}{tail}\")\n        return os.path.join(root, fname)\n\n    def create_node(self, namespace, node_settings):\n        \"\"\"Create nodes with the correct namespace and settings\n\n        Args:\n            namespace(str): namespace\n            node_settings(dict): Single \"nodes\" entry from .fursettings file.\n\n        Returns:\n             list: Created nodes\n\n        \"\"\"\n        nodes = []\n\n        # Get original names and ids\n        orig_transform_name = node_settings[\"transform\"][\"name\"]\n        orig_shape_name = node_settings[\"name\"]\n\n        # Add namespace\n        transform_name = \"{}:{}\".format(namespace, orig_transform_name)\n        shape_name = \"{}:{}\".format(namespace, orig_shape_name)\n\n        # Create pgYetiMaya node\n        transform_node = cmds.createNode(\"transform\",\n                                         name=transform_name)\n        yeti_node = cmds.createNode(\"pgYetiMaya\",\n                                    name=shape_name,\n                                    parent=transform_node)\n\n        lib.set_id(transform_node, node_settings[\"transform\"][\"cbId\"])\n        lib.set_id(yeti_node, node_settings[\"cbId\"])\n\n        nodes.extend([transform_node, yeti_node])\n\n        # Update attributes with defaults\n        attributes = node_settings[\"attrs\"]\n        attributes.update({\n            \"verbosity\": 2,\n            \"fileMode\": 1,\n\n            # Fix render stats, like Yeti's own\n            # ../scripts/pgYetiNode.mel script\n            \"visibleInReflections\": True,\n            \"visibleInRefractions\": True\n        })\n\n        if \"viewportDensity\" not in attributes:\n            attributes[\"viewportDensity\"] = 0.1\n\n        # Apply attributes to pgYetiMaya node\n        for attr, value in attributes.items():\n            set_attribute(attr, value, yeti_node)\n\n        # Connect to the time node\n        cmds.connectAttr(\"time1.outTime\", \"%s.currentTime\" % yeti_node)\n\n        return nodes\n\n    def read_settings(self, path):\n        \"\"\"Read .fursettings file and compute some additional attributes\"\"\"\n\n        with open(path, \"r\") as fp:\n            fur_settings = json.load(fp)\n\n        if \"nodes\" not in fur_settings:\n            raise RuntimeError(\"Encountered invalid data, \"\n                               \"expected 'nodes' in fursettings.\")\n\n        # Compute the cache file name values we want to set for the nodes\n        root = os.path.dirname(path)\n        for node in fur_settings[\"nodes\"]:\n            cache_filename = self.get_cache_node_filepath(\n                root=root, node_name=node[\"name\"])\n\n            attrs = node.get(\"attrs\", {})       # allow 'attrs' to not exist\n            attrs[\"cacheFileName\"] = cache_filename\n            node[\"attrs\"] = attrs\n\n        return fur_settings\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/load/load_yeti_rig.py",
    "content": "import maya.cmds as cmds\n\nfrom openpype.settings import get_current_project_settings\nimport openpype.hosts.maya.api.plugin\nfrom openpype.hosts.maya.api import lib\n\n\nclass YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):\n    \"\"\"This loader will load Yeti rig.\"\"\"\n\n    families = [\"yetiRig\"]\n    representations = [\"ma\"]\n\n    label = \"Load Yeti Rig\"\n    order = -9\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    def process_reference(\n        self, context, name=None, namespace=None, options=None\n    ):\n        path = self.filepath_from_context(context)\n\n        attach_to_root = options.get(\"attach_to_root\", True)\n        group_name = options[\"group_name\"]\n\n        # no group shall be created\n        if not attach_to_root:\n            group_name = namespace\n\n        with lib.maintained_selection():\n            file_url = self.prepare_root_value(\n                path, context[\"project\"][\"name\"]\n            )\n            nodes = cmds.file(\n                file_url,\n                namespace=namespace,\n                reference=True,\n                returnNewNodes=True,\n                groupReference=attach_to_root,\n                groupName=group_name\n            )\n\n        settings = get_current_project_settings()\n        colors = settings[\"maya\"][\"load\"][\"colors\"]\n        c = colors.get(\"yetiRig\")\n        if c is not None:\n            cmds.setAttr(group_name + \".useOutlinerColor\", 1)\n            cmds.setAttr(\n                group_name + \".outlinerColor\",\n                (float(c[0]) / 255), (float(c[1]) / 255), (float(c[2]) / 255)\n            )\n        self[:] = nodes\n\n        return nodes\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_animation.py",
    "content": "import pyblish.api\n\nimport maya.cmds as cmds\n\n\nclass CollectAnimationOutputGeometry(pyblish.api.InstancePlugin):\n    \"\"\"Collect out hierarchy data for instance.\n\n    Collect all hierarchy nodes which reside in the out_SET of the animation\n    instance or point cache instance. This is to unify the logic of retrieving\n    that specific data. This eliminates the need to write two separate pieces\n    of logic to fetch all hierarchy nodes.\n\n    Results in a list of nodes from the content of the instances\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.4\n    families = [\"animation\"]\n    label = \"Collect Animation\"\n    hosts = [\"maya\"]\n\n    ignore_type = [\"constraints\"]\n\n    def process(self, instance):\n        \"\"\"Collect the hierarchy nodes\"\"\"\n\n        family = instance.data[\"family\"]\n        out_set = next((i for i in instance.data[\"setMembers\"] if\n                        i.endswith(\"out_SET\")), None)\n\n        if out_set is None:\n            warning = \"Expecting out_SET for instance of family '%s'\" % family\n            self.log.warning(warning)\n            return\n\n        members = cmds.ls(cmds.sets(out_set, query=True), long=True)\n\n        # Get all the relatives of the members\n        descendants = cmds.listRelatives(members,\n                                         allDescendents=True,\n                                         fullPath=True) or []\n        descendants = cmds.ls(descendants, noIntermediate=True, long=True)\n\n        # Add members and descendants together for a complete overview\n\n        hierarchy = members + descendants\n\n        # Ignore certain node types (e.g. constraints)\n        ignore = cmds.ls(hierarchy, type=self.ignore_type, long=True)\n        if ignore:\n            ignore = set(ignore)\n            hierarchy = [node for node in hierarchy if node not in ignore]\n\n        # Store data in the instance for the validator\n        instance.data[\"out_hierarchy\"] = hierarchy\n\n        if instance.data.get(\"farm\"):\n            instance.data[\"families\"].append(\"publish.farm\")\n\n        # User defined attributes.\n        instance.data[\"includeUserDefinedAttributes\"] = (\n            instance.data[\"creator_attributes\"][\"includeUserDefinedAttributes\"]\n        )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nfrom openpype.hosts.maya.api.lib import get_all_children\n\n\nclass CollectArnoldSceneSource(pyblish.api.InstancePlugin):\n    \"\"\"Collect Arnold Scene Source data.\"\"\"\n\n    # Offset to be after renderable camera collection.\n    order = pyblish.api.CollectorOrder + 0.2\n    label = \"Collect Arnold Scene Source\"\n    families = [\"ass\", \"assProxy\"]\n\n    def process(self, instance):\n        instance.data[\"members\"] = []\n        for set_member in instance.data[\"setMembers\"]:\n            if cmds.nodeType(set_member) != \"objectSet\":\n                instance.data[\"members\"].extend(self.get_hierarchy(set_member))\n                continue\n\n            members = cmds.sets(set_member, query=True)\n            members = cmds.ls(members, long=True)\n            if members is None:\n                self.log.warning(\n                    \"Skipped empty instance: \\\"%s\\\" \" % set_member\n                )\n                continue\n            if set_member.endswith(\"proxy_SET\"):\n                instance.data[\"proxy\"] = self.get_hierarchy(members)\n\n        # Use camera in object set if present else default to render globals\n        # camera.\n        cameras = cmds.ls(type=\"camera\", long=True)\n        renderable = [c for c in cameras if cmds.getAttr(\"%s.renderable\" % c)]\n        if renderable:\n            camera = renderable[0]\n            for node in instance.data[\"members\"]:\n                camera_shapes = cmds.listRelatives(\n                    node, shapes=True, type=\"camera\"\n                )\n                if camera_shapes:\n                    camera = node\n            instance.data[\"camera\"] = camera\n        else:\n            self.log.debug(\"No renderable cameras found.\")\n\n        self.log.debug(\"data: {}\".format(instance.data))\n\n    def get_hierarchy(self, nodes):\n        \"\"\"Return nodes with all their children\"\"\"\n        nodes = cmds.ls(nodes, long=True)\n        if not nodes:\n            return []\n        children = get_all_children(nodes)\n        # Make sure nodes merged with children only\n        # contains unique entries\n        return list(set(nodes + children))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_assembly.py",
    "content": "from collections import defaultdict\nimport pyblish.api\n\nfrom maya import cmds, mel\nfrom openpype.hosts.maya import api\nfrom openpype.hosts.maya.api import lib\n\n# TODO : Publish of assembly: -unique namespace for all assets, VALIDATOR!\n\n\nclass CollectAssembly(pyblish.api.InstancePlugin):\n    \"\"\"Collect all relevant assembly items\n\n    Collected data:\n\n        * File name\n        * Compatible loader\n        * Matrix per instance\n        * Namespace\n\n    Note: GPU caches are currently not supported in the pipeline. There is no\n    logic yet which supports the swapping of GPU cache to renderable objects.\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.49\n    label = \"Assembly\"\n    families = [\"assembly\"]\n\n    def process(self, instance):\n\n        # Find containers\n        containers = api.ls()\n\n        # Get all content from the instance\n        instance_lookup = set(cmds.ls(instance, type=\"transform\", long=True))\n        data = defaultdict(list)\n\n        hierarchy_nodes = []\n        for container in containers:\n\n            root = lib.get_container_transforms(container, root=True)\n            if not root or root not in instance_lookup:\n                continue\n\n            # Retrieve the hierarchy\n            parent = cmds.listRelatives(root, parent=True, fullPath=True)[0]\n            hierarchy_nodes.append(parent)\n\n            # Temporary warning for GPU cache which are not supported yet\n            loader = container[\"loader\"]\n            if loader == \"GpuCacheLoader\":\n                self.log.warning(\"GPU Cache Loader is currently not supported\"\n                                 \"in the pipeline, we will export it tho\")\n\n            # Gather info for new data entry\n            representation_id = container[\"representation\"]\n            instance_data = {\"loader\": loader,\n                             \"parent\": parent,\n                             \"namespace\": container[\"namespace\"]}\n\n            # Check if matrix differs from default and store changes\n            matrix_data = self.get_matrix_data(root)\n            if matrix_data:\n                instance_data[\"matrix\"] = matrix_data\n\n            data[representation_id].append(instance_data)\n\n        instance.data[\"scenedata\"] = dict(data)\n        instance.data[\"nodesHierarchy\"] = list(set(hierarchy_nodes))\n\n    def get_file_rule(self, rule):\n        return mel.eval('workspace -query -fileRuleEntry \"{}\"'.format(rule))\n\n    def get_matrix_data(self, node):\n        \"\"\"Get the matrix of all members when they are not default\n\n        Each matrix which differs from the default will be stored in a\n        dictionary\n\n        Args:\n            members (list): list of transform nmodes\n        Returns:\n            dict\n        \"\"\"\n\n        matrix = cmds.xform(node, query=True, matrix=True)\n        if matrix == lib.DEFAULT_MATRIX:\n            return\n\n        return matrix\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_current_file.py",
    "content": "\nimport pyblish.api\n\nfrom maya import cmds\n\n\nclass CollectCurrentFile(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current working file.\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.4\n    label = \"Maya Current File\"\n    hosts = ['maya']\n\n    def process(self, context):\n        \"\"\"Inject the current working file\"\"\"\n        context.data['currentFile'] = cmds.file(query=True, sceneName=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_fbx_animation.py",
    "content": "# -*- coding: utf-8 -*-\nfrom maya import cmds  # noqa\nimport pyblish.api\nfrom openpype.pipeline import OptionalPyblishPluginMixin\n\n\nclass CollectFbxAnimation(pyblish.api.InstancePlugin,\n                          OptionalPyblishPluginMixin):\n    \"\"\"Collect Animated Rig Data for FBX Extractor.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.2\n    label = \"Collect Fbx Animation\"\n    hosts = [\"maya\"]\n    families = [\"animation\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        skeleton_sets = [\n            i for i in instance\n            if i.endswith(\"skeletonAnim_SET\")\n        ]\n        if not skeleton_sets:\n            return\n\n        instance.data[\"families\"].append(\"animation.fbx\")\n        instance.data[\"animated_skeleton\"] = []\n        for skeleton_set in skeleton_sets:\n            skeleton_content = cmds.sets(skeleton_set, query=True)\n            self.log.debug(\n                \"Collected animated skeleton data: {}\".format(\n                    skeleton_content\n                ))\n            if skeleton_content:\n                instance.data[\"animated_skeleton\"] = skeleton_content\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_fbx_camera.py",
    "content": "# -*- coding: utf-8 -*-\nfrom maya import cmds  # noqa\nimport pyblish.api\n\n\nclass CollectFbxCamera(pyblish.api.InstancePlugin):\n    \"\"\"Collect Camera for FBX export.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.2\n    label = \"Collect Camera for FBX export\"\n    families = [\"camera\"]\n\n    def process(self, instance):\n        if not instance.data.get(\"families\"):\n            instance.data[\"families\"] = []\n\n        if \"fbx\" not in instance.data[\"families\"]:\n            instance.data[\"families\"].append(\"fbx\")\n\n        instance.data[\"cameras\"] = True\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_file_dependencies.py",
    "content": "import json\n\nfrom maya import cmds\n\nimport pyblish.api\n\n\nclass CollectFileDependencies(pyblish.api.ContextPlugin):\n    \"\"\"Gather all files referenced in this scene.\"\"\"\n\n    label = \"Collect File Dependencies\"\n    order = pyblish.api.CollectorOrder - 0.49\n    hosts = [\"maya\"]\n\n    def process(self, context):\n        dependencies = []\n        for node in cmds.ls(type=\"file\"):\n            path = cmds.getAttr(\"{}.{}\".format(node, \"fileTextureName\"))\n            if path not in dependencies:\n                dependencies.append(path)\n\n        for node in cmds.ls(type=\"AlembicNode\"):\n            path = cmds.getAttr(\"{}.{}\".format(node, \"abc_File\"))\n            if path not in dependencies:\n                dependencies.append(path)\n\n        context.data[\"fileDependencies\"] = dependencies\n        self.log.debug(json.dumps(dependencies, indent=4))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_gltf.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\n\n\nclass CollectGLTF(pyblish.api.InstancePlugin):\n    \"\"\"Collect Assets for GLTF/GLB export.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.2\n    label = \"Collect Asset for GLTF/GLB export\"\n    families = [\"model\", \"animation\", \"pointcache\"]\n\n    def process(self, instance):\n        if not instance.data.get(\"families\"):\n            instance.data[\"families\"] = []\n\n        if \"gltf\" not in instance.data[\"families\"]:\n            instance.data[\"families\"].append(\"gltf\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_history.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\n\nclass CollectMayaHistory(pyblish.api.InstancePlugin):\n    \"\"\"Collect history for instances from the Maya scene\n\n    Note:\n        This removes render layers collected in the history\n\n    This is separate from Collect Instances so we can target it towards only\n    specific family types.\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.1\n    hosts = [\"maya\"]\n    label = \"Maya History\"\n    families = [\"rig\"]\n\n    def process(self, instance):\n\n        kwargs = {}\n        if int(cmds.about(version=True)) >= 2020:\n            # New flag since Maya 2020 which makes cmds.listHistory faster\n            kwargs = {\"fastIteration\": True}\n        else:\n            self.log.debug(\"Ignoring `fastIteration` flag before Maya 2020..\")\n\n        # Collect the history with long names\n        history = set(cmds.listHistory(instance, leaf=False, **kwargs) or [])\n        history = cmds.ls(list(history), long=True)\n\n        # Exclude invalid nodes (like renderlayers)\n        exclude = cmds.ls(type=\"renderLayer\", long=True)\n        if exclude:\n            exclude = set(exclude)  # optimize lookup\n            history = [x for x in history if x not in exclude]\n\n        # Combine members with history\n        members = instance[:] + history\n        members = list(set(members))    # ensure unique\n\n        # Update the instance\n        instance[:] = members\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_inputs.py",
    "content": "import copy\n\nfrom maya import cmds\nimport maya.api.OpenMaya as om\nimport pyblish.api\n\nfrom openpype.pipeline import registered_host\nfrom openpype.hosts.maya.api.lib import get_container_members\nfrom openpype.hosts.maya.api.lib_rendersetup import get_shader_in_layer\n\n\ndef iter_history(nodes,\n                 filter=om.MFn.kInvalid,\n                 direction=om.MItDependencyGraph.kUpstream):\n    \"\"\"Iterate unique upstream history for list of nodes.\n\n    This acts as a replacement to maya.cmds.listHistory.\n    It's faster by about 2x-3x. It returns less than\n    maya.cmds.listHistory as it excludes the input nodes\n    from the output (unless an input node was history\n    for another input node). It also excludes duplicates.\n\n    Args:\n        nodes (list): Maya node names to start search from.\n        filter (om.MFn.Type): Filter to only specific types.\n            e.g. to dag nodes using om.MFn.kDagNode\n        direction (om.MItDependencyGraph.Direction): Direction to traverse in.\n            Defaults to upstream.\n\n    Yields:\n        str: Node names in upstream history.\n\n    \"\"\"\n    if not nodes:\n        return\n\n    sel = om.MSelectionList()\n    for node in nodes:\n        sel.add(node)\n\n    it = om.MItDependencyGraph(sel.getDependNode(0))  # init iterator\n    handle = om.MObjectHandle\n\n    traversed = set()\n    fn_dep = om.MFnDependencyNode()\n    fn_dag = om.MFnDagNode()\n    for i in range(sel.length()):\n\n        start_node = sel.getDependNode(i)\n        start_node_hash = handle(start_node).hashCode()\n        if start_node_hash in traversed:\n            continue\n\n        it.resetTo(start_node,\n                   filter=filter,\n                   direction=direction)\n        while not it.isDone():\n\n            node = it.currentNode()\n            node_hash = handle(node).hashCode()\n\n            if node_hash in traversed:\n                it.prune()\n                it.next()  # noqa: B305\n                continue\n\n            traversed.add(node_hash)\n\n            if node.hasFn(om.MFn.kDagNode):\n                fn_dag.setObject(node)\n                yield fn_dag.fullPathName()\n            else:\n                fn_dep.setObject(node)\n                yield fn_dep.name()\n\n            it.next()  # noqa: B305\n\n\ndef collect_input_containers(containers, nodes):\n    \"\"\"Collect containers that contain any of the node in `nodes`.\n\n    This will return any loaded Avalon container that contains at least one of\n    the nodes. As such, the Avalon container is an input for it. Or in short,\n    there are member nodes of that container.\n\n    Returns:\n        list: Input avalon containers\n\n    \"\"\"\n    # Assume the containers have collected their cached '_members' data\n    # in the collector.\n    return [container for container in containers\n            if any(node in container[\"_members\"] for node in nodes)]\n\n\nclass CollectUpstreamInputs(pyblish.api.InstancePlugin):\n    \"\"\"Collect input source inputs for this publish.\n\n    This will include `inputs` data of which loaded publishes were used in the\n    generation of this publish. This leaves an upstream trace to what was used\n    as input.\n\n    \"\"\"\n\n    label = \"Collect Inputs\"\n    order = pyblish.api.CollectorOrder + 0.34\n    hosts = [\"maya\"]\n\n    def process(self, instance):\n\n        # For large scenes the querying of \"host.ls()\" can be relatively slow\n        # e.g. up to a second. Many instances calling it easily slows this\n        # down. As such, we cache it so we trigger it only once.\n        # todo: Instead of hidden cache make \"CollectContainers\" plug-in\n        cache_key = \"__cache_containers\"\n        scene_containers = instance.context.data.get(cache_key, None)\n        if scene_containers is None:\n            # Query the scenes' containers if there's no cache yet\n            host = registered_host()\n            scene_containers = list(host.ls())\n            for container in scene_containers:\n                # Embed the members into the container dictionary\n                container_members = set(get_container_members(container))\n                container[\"_members\"] = container_members\n            instance.context.data[\"__cache_containers\"] = scene_containers\n\n        # Collect the relevant input containers for this instance\n        if \"renderlayer\" in set(instance.data.get(\"families\", [])):\n            # Special behavior for renderlayers\n            self.log.debug(\"Collecting renderlayer inputs....\")\n            containers = self._collect_renderlayer_inputs(scene_containers,\n                                                          instance)\n\n        else:\n            # Basic behavior\n            nodes = instance[:]\n\n            # Include any input connections of history with long names\n            # For optimization purposes only trace upstream from shape nodes\n            # looking for used dag nodes. This way having just a constraint\n            # on a transform is also ignored which tended to give irrelevant\n            # inputs for the majority of our use cases. We tend to care more\n            # about geometry inputs.\n            shapes = cmds.ls(nodes,\n                             type=(\"mesh\", \"nurbsSurface\", \"nurbsCurve\"),\n                             noIntermediate=True)\n            if shapes:\n                history = list(iter_history(shapes, filter=om.MFn.kShape))\n                history = cmds.ls(history, long=True)\n\n                # Include the transforms in the collected history as shapes\n                # are excluded from containers\n                transforms = cmds.listRelatives(cmds.ls(history, shapes=True),\n                                                parent=True,\n                                                fullPath=True,\n                                                type=\"transform\")\n                if transforms:\n                    history.extend(transforms)\n\n                if history:\n                    nodes = list(set(nodes + history))\n\n            # Collect containers for the given set of nodes\n            containers = collect_input_containers(scene_containers,\n                                                  nodes)\n\n        inputs = [c[\"representation\"] for c in containers]\n        instance.data[\"inputRepresentations\"] = inputs\n        self.log.debug(\"Collected inputs: %s\" % inputs)\n\n    def _collect_renderlayer_inputs(self, scene_containers, instance):\n        \"\"\"Collects inputs from nodes in renderlayer, incl. shaders + camera\"\"\"\n\n        # Get the renderlayer\n        renderlayer = instance.data.get(\"renderlayer\")\n\n        if renderlayer == \"defaultRenderLayer\":\n            # Assume all loaded containers in the scene are inputs\n            # for the masterlayer\n            return copy.deepcopy(scene_containers)\n        else:\n            # Get the members of the layer\n            members = cmds.editRenderLayerMembers(renderlayer,\n                                                  query=True,\n                                                  fullNames=True) or []\n\n            # In some cases invalid objects are returned from\n            # `editRenderLayerMembers` so we filter them out\n            members = cmds.ls(members, long=True)\n\n            # Include all children\n            children = cmds.listRelatives(members,\n                                          allDescendents=True,\n                                          fullPath=True) or []\n            members.extend(children)\n\n            # Include assigned shaders in renderlayer\n            shapes = cmds.ls(members, shapes=True, long=True)\n            shaders = set()\n            for shape in shapes:\n                shape_shaders = get_shader_in_layer(shape, layer=renderlayer)\n                if not shape_shaders:\n                    continue\n                shaders.update(shape_shaders)\n            members.extend(shaders)\n\n            # Explicitly include the camera being rendered in renderlayer\n            cameras = instance.data.get(\"cameras\")\n            members.extend(cameras)\n\n            containers = collect_input_containers(scene_containers, members)\n\n        return containers\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_instances.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nfrom openpype.hosts.maya.api.lib import get_all_children\n\n\nclass CollectNewInstances(pyblish.api.InstancePlugin):\n    \"\"\"Gather members for instances and pre-defined attribute\n\n    This collector takes into account assets that are associated with\n    an objectSet and marked with a unique identifier;\n\n    Identifier:\n        id (str): \"pyblish.avalon.instance\"\n\n    Limitations:\n        - Does not take into account nodes connected to those\n            within an objectSet. Extractors are assumed to export\n            with history preserved, but this limits what they will\n            be able to achieve and the amount of data available\n            to validators. An additional collector could also\n            append this input data into the instance, as we do\n            for `pype.rig` with collect_history.\n\n    \"\"\"\n\n    label = \"Collect New Instance Data\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"maya\"]\n\n    valid_empty_families = {\"workfile\", \"renderlayer\"}\n\n    def process(self, instance):\n\n        objset = instance.data.get(\"instance_node\")\n        if not objset:\n            self.log.debug(\"Instance has no `instance_node` data\")\n\n        # TODO: We might not want to do this in the future\n        # Merge creator attributes into instance.data just backwards compatible\n        # code still runs as expected\n        creator_attributes = instance.data.get(\"creator_attributes\", {})\n        if creator_attributes:\n            instance.data.update(creator_attributes)\n\n        members = cmds.sets(objset, query=True) or []\n        if members:\n            # Collect members\n            members = cmds.ls(members, long=True) or []\n\n            dag_members = cmds.ls(members, type=\"dagNode\", long=True)\n            children = get_all_children(dag_members)\n            children = cmds.ls(children, noIntermediate=True, long=True)\n            parents = (\n                self.get_all_parents(members)\n                if creator_attributes.get(\"includeParentHierarchy\", True)\n                else []\n            )\n            members_hierarchy = list(set(members + children + parents))\n\n            instance[:] = members_hierarchy\n\n        elif instance.data[\"family\"] not in self.valid_empty_families:\n            self.log.warning(\"Empty instance: \\\"%s\\\" \" % objset)\n        # Store the exact members of the object set\n        instance.data[\"setMembers\"] = members\n\n        # TODO: This might make more sense as a separate collector\n        # Convert frame values to integers\n        for attr_name in (\n            \"handleStart\", \"handleEnd\", \"frameStart\", \"frameEnd\",\n        ):\n            value = instance.data.get(attr_name)\n            if value is not None:\n                instance.data[attr_name] = int(value)\n\n        # Append start frame and end frame to label if present\n        if \"frameStart\" in instance.data and \"frameEnd\" in instance.data:\n            # Take handles from context if not set locally on the instance\n            for key in [\"handleStart\", \"handleEnd\"]:\n                if key not in instance.data:\n                    value = instance.context.data[key]\n                    if value is not None:\n                        value = int(value)\n                    instance.data[key] = value\n\n            instance.data[\"frameStartHandle\"] = int(\n                instance.data[\"frameStart\"] - instance.data[\"handleStart\"]\n            )\n            instance.data[\"frameEndHandle\"] = int(\n                instance.data[\"frameEnd\"] + instance.data[\"handleEnd\"]\n            )\n\n    def get_all_parents(self, nodes):\n        \"\"\"Get all parents by using string operations (optimization)\n\n        Args:\n            nodes (list): the nodes which are found in the objectSet\n\n        Returns:\n            list\n        \"\"\"\n\n        parents = []\n        for node in nodes:\n            splitted = node.split(\"|\")\n            items = [\"|\".join(splitted[0:i]) for i in range(2, len(splitted))]\n            parents.extend(items)\n\n        return list(set(parents))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_look.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Maya look collector.\"\"\"\nimport re\nimport os\nimport glob\n\nfrom maya import cmds  # noqa\nimport pyblish.api\nfrom openpype.hosts.maya.api import lib\n\nSHAPE_ATTRS = [\"castsShadows\",\n               \"receiveShadows\",\n               \"motionBlur\",\n               \"primaryVisibility\",\n               \"smoothShading\",\n               \"visibleInReflections\",\n               \"visibleInRefractions\",\n               \"doubleSided\",\n               \"opposite\"]\nSHAPE_ATTRS = set(SHAPE_ATTRS)\n\n\ndef get_pxr_multitexture_file_attrs(node):\n    attrs = []\n    for i in range(9):\n        if cmds.attributeQuery(\"filename{}\".format(i), node=node, ex=True):\n            file = cmds.getAttr(\"{}.filename{}\".format(node, i))\n            if file:\n                attrs.append(\"filename{}\".format(i))\n    return attrs\n\n\nFILE_NODES = {\n    # maya\n    \"file\": \"fileTextureName\",\n    # arnold (mtoa)\n    \"aiImage\": \"filename\",\n    # redshift\n    \"RedshiftNormalMap\": \"tex0\",\n    # renderman\n    \"PxrBump\": \"filename\",\n    \"PxrNormalMap\": \"filename\",\n    \"PxrMultiTexture\": get_pxr_multitexture_file_attrs,\n    \"PxrPtexture\": \"filename\",\n    \"PxrTexture\": \"filename\"\n}\n\nRENDER_SET_TYPES = [\n    \"VRayDisplacement\",\n    \"VRayLightMesh\",\n    \"VRayObjectProperties\",\n    \"RedshiftObjectId\",\n    \"RedshiftMeshParameters\",\n]\n\n# Keep only node types that actually exist\nall_node_types = set(cmds.allNodeTypes())\nfor node_type in list(FILE_NODES.keys()):\n    if node_type not in all_node_types:\n        FILE_NODES.pop(node_type)\n\nfor node_type in RENDER_SET_TYPES:\n    if node_type not in all_node_types:\n        RENDER_SET_TYPES.remove(node_type)\ndel all_node_types\n\n# Cache pixar dependency node types so we can perform a type lookup against it\nPXR_NODES = set()\nif cmds.pluginInfo(\"RenderMan_for_Maya\", query=True, loaded=True):\n    PXR_NODES = set(\n        cmds.pluginInfo(\"RenderMan_for_Maya\",\n                        query=True,\n                        dependNode=True)\n    )\n\n\ndef get_attributes(dictionary, attr, node=None):\n    # type: (dict, str, str) -> list\n    if callable(dictionary[attr]):\n        val = dictionary[attr](node)\n    else:\n        val = dictionary.get(attr, [])\n\n    return val if isinstance(val, list) else [val]\n\n\ndef get_look_attrs(node):\n    \"\"\"Returns attributes of a node that are important for the look.\n\n    These are the \"changed\" attributes (those that have edits applied\n    in the current scene).\n\n    Returns:\n        list: Attribute names to extract\n\n    \"\"\"\n    # When referenced get only attributes that are \"changed since file open\"\n    # which includes any reference edits, otherwise take *all* user defined\n    # attributes\n    is_referenced = cmds.referenceQuery(node, isNodeReferenced=True)\n    result = cmds.listAttr(node, userDefined=True,\n                           changedSinceFileOpen=is_referenced) or []\n\n    # `cbId` is added when a scene is saved, ignore by default\n    if \"cbId\" in result:\n        result.remove(\"cbId\")\n\n    # For shapes allow render stat changes\n    if cmds.objectType(node, isAType=\"shape\"):\n        attrs = cmds.listAttr(node, changedSinceFileOpen=True) or []\n        for attr in attrs:\n            if attr in SHAPE_ATTRS or \\\n                    attr not in SHAPE_ATTRS and attr.startswith('ai'):\n                result.append(attr)\n    return result\n\n\ndef node_uses_image_sequence(node, node_path):\n    # type: (str, str) -> bool\n    \"\"\"Return whether file node uses an image sequence or single image.\n\n    Determine if a node uses an image sequence or just a single image,\n    not always obvious from its file path alone.\n\n    Args:\n        node (str): Name of the Maya node\n        node_path (str): The file path of the node\n\n    Returns:\n        bool: True if node uses an image sequence\n\n    \"\"\"\n\n    # useFrameExtension indicates an explicit image sequence\n    try:\n        use_frame_extension = cmds.getAttr('%s.useFrameExtension' % node)\n    except ValueError:\n        use_frame_extension = False\n    if use_frame_extension:\n        return True\n\n    # The following tokens imply a sequence\n    patterns = [\"<udim>\", \"<tile>\", \"<uvtile>\",\n                \"u<u>_v<v>\", \"<frame0\", \"<f4>\"]\n    node_path_lowered = node_path.lower()\n    return any(pattern in node_path_lowered for pattern in patterns)\n\n\ndef seq_to_glob(path):\n    \"\"\"Takes an image sequence path and returns it in glob format,\n    with the frame number replaced by a '*'.\n\n    Image sequences may be numerical sequences, e.g. /path/to/file.1001.exr\n    will return as /path/to/file.*.exr.\n\n    Image sequences may also use tokens to denote sequences, e.g.\n    /path/to/texture.<UDIM>.tif will return as /path/to/texture.*.tif.\n\n    Args:\n        path (str): the image sequence path\n\n    Returns:\n        str: Return glob string that matches the filename pattern.\n\n    \"\"\"\n\n    if path is None:\n        return path\n\n    # If any of the patterns, convert the pattern\n    patterns = {\n        \"<udim>\": \"<udim>\",\n        \"<tile>\": \"<tile>\",\n        \"<uvtile>\": \"<uvtile>\",\n        \"#\": \"#\",\n        \"u<u>_v<v>\": \"<u>|<v>\",\n        \"<frame0\": \"<frame0\\d+>\",\n        \"<f>\": \"<f>\"\n    }\n\n    lower = path.lower()\n    has_pattern = False\n    for pattern, regex_pattern in patterns.items():\n        if pattern in lower:\n            path = re.sub(regex_pattern, \"*\", path, flags=re.IGNORECASE)\n            has_pattern = True\n\n    if has_pattern:\n        return path\n\n    base = os.path.basename(path)\n    matches = list(re.finditer(r'\\d+', base))\n    if matches:\n        match = matches[-1]\n        new_base = '{0}*{1}'.format(base[:match.start()],\n                                    base[match.end():])\n        head = os.path.dirname(path)\n        return os.path.join(head, new_base)\n    else:\n        return path\n\n\ndef get_file_node_paths(node):\n    # type: (str) -> list\n    \"\"\"Get the file path used by a Maya file node.\n\n    Args:\n        node (str): Name of the Maya file node\n\n    Returns:\n        list: the file paths in use\n\n    \"\"\"\n    # if the path appears to be sequence, use computedFileTextureNamePattern,\n    # this preserves the <> tag\n    if cmds.attributeQuery('computedFileTextureNamePattern',\n                           node=node,\n                           exists=True):\n        plug = '{0}.computedFileTextureNamePattern'.format(node)\n        texture_pattern = cmds.getAttr(plug)\n\n        patterns = [\"<udim>\",\n                    \"<tile>\",\n                    \"u<u>_v<v>\",\n                    \"<f>\",\n                    \"<frame0\",\n                    \"<uvtile>\"]\n        lower = texture_pattern.lower()\n        if any(pattern in lower for pattern in patterns):\n            return [texture_pattern]\n\n    try:\n        file_attributes = get_attributes(\n            FILE_NODES, cmds.nodeType(node), node)\n    except AttributeError:\n        file_attributes = \"fileTextureName\"\n\n    files = []\n    for file_attr in file_attributes:\n        if cmds.attributeQuery(file_attr, node=node, exists=True):\n            files.append(cmds.getAttr(\"{}.{}\".format(node, file_attr)))\n\n    return files\n\n\ndef get_file_node_files(node):\n    \"\"\"Return the file paths related to the file node\n\n    Note:\n        Will only return existing files. Returns an empty list\n        if not valid existing files are linked.\n\n    Returns:\n        list: List of full file paths.\n\n    \"\"\"\n    paths = get_file_node_paths(node)\n\n    # For sequences get all files and filter to only existing files\n    result = []\n    for path in paths:\n        if node_uses_image_sequence(node, path):\n            glob_pattern = seq_to_glob(path)\n            result.extend(glob.glob(glob_pattern))\n        elif os.path.exists(path):\n            result.append(path)\n\n    return result\n\n\nclass CollectLook(pyblish.api.InstancePlugin):\n    \"\"\"Collect look data for instance.\n\n    For the shapes/transforms of the referenced object to collect look for\n    retrieve the user-defined attributes (like V-ray attributes) and their\n    values as they were created in the current scene.\n\n    For the members of the instance collect the sets (shadingEngines and\n    other sets, e.g. VRayDisplacement) they are in along with the exact\n    membership relations.\n\n    Collects:\n        lookAttributes (list): Nodes in instance with their altered attributes\n        lookSetRelations (list): Sets and their memberships\n        lookSets (list): List of set names included in the look\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.2\n    families = [\"look\"]\n    label = \"Collect Look\"\n    hosts = [\"maya\"]\n    maketx = True\n\n    def process(self, instance):\n        \"\"\"Collect the Look in the instance with the correct layer settings\"\"\"\n        renderlayer = instance.data.get(\"renderlayer\", \"defaultRenderLayer\")\n        with lib.renderlayer(renderlayer):\n            self.collect(instance)\n\n    def collect(self, instance):\n        \"\"\"Collect looks.\n\n        Args:\n            instance: Instance to collect.\n\n        \"\"\"\n        self.log.debug(\"Looking for look associations \"\n                       \"for %s\" % instance.data['name'])\n\n        # Lookup set (optimization)\n        instance_lookup = set(cmds.ls(instance, long=True))\n\n        # Discover related object sets\n        self.log.debug(\"Gathering sets ...\")\n        sets = self.collect_sets(instance)\n\n        # Lookup set (optimization)\n        instance_lookup = set(cmds.ls(instance, long=True))\n\n        self.log.debug(\"Gathering set relations ...\")\n        # Ensure iteration happen in a list to allow removing keys from the\n        # dict within the loop\n        for obj_set in list(sets):\n            self.log.debug(\"From {}\".format(obj_set))\n            # Get all nodes of the current objectSet (shadingEngine)\n            for member in cmds.ls(cmds.sets(obj_set, query=True), long=True):\n                member_data = self.collect_member_data(member,\n                                                       instance_lookup)\n                if member_data:\n                    # Add information of the node to the members list\n                    sets[obj_set][\"members\"].append(member_data)\n\n            # Remove sets that didn't have any members assigned in the end\n            # Thus the data will be limited to only what we need.\n            if not sets[obj_set][\"members\"]:\n                self.log.debug(\n                    \"Removing redundant set information: {}\".format(obj_set)\n                )\n                sets.pop(obj_set, None)\n\n        self.log.debug(\"Gathering attribute changes to instance members..\")\n        attributes = self.collect_attributes_changed(instance)\n\n        # Store data on the instance\n        instance.data[\"lookData\"] = {\n            \"attributes\": attributes,\n            \"relationships\": sets\n        }\n\n        # Collect file nodes used by shading engines (if we have any)\n        files = []\n        look_sets = list(sets.keys())\n        shader_attrs = [\n            \"surfaceShader\",\n            \"volumeShader\",\n            \"displacementShader\",\n            \"aiSurfaceShader\",\n            \"aiVolumeShader\",\n            \"rman__surface\",\n            \"rman__displacement\"\n        ]\n        if look_sets:\n            self.log.debug(\"Found look sets: {}\".format(look_sets))\n\n            # Get all material attrs for all look sets to retrieve their inputs\n            existing_attrs = []\n            for look in look_sets:\n                for attr in shader_attrs:\n                    if cmds.attributeQuery(attr, node=look, exists=True):\n                        existing_attrs.append(\"{}.{}\".format(look, attr))\n\n            materials = cmds.listConnections(existing_attrs,\n                                             source=True,\n                                             destination=False) or []\n\n            self.log.debug(\"Found materials:\\n{}\".format(materials))\n\n            self.log.debug(\"Found the following sets:\\n{}\".format(look_sets))\n            # Get the entire node chain of the look sets\n            # history = cmds.listHistory(look_sets, allConnections=True)\n            # if materials list is empty, listHistory() will crash with\n            # RuntimeError\n            history = set()\n            if materials:\n                history = set(\n                    cmds.listHistory(materials, allConnections=True))\n\n            # Since we retrieved history only of the connected materials\n            # connected to the look sets above we now add direct history\n            # for some of the look sets directly\n            # handling render attribute sets\n\n            # Maya (at least 2024) crashes with Warning when render set type\n            # isn't available. cmds.ls() will return empty list\n            if RENDER_SET_TYPES:\n                render_sets = cmds.ls(look_sets, type=RENDER_SET_TYPES)\n                if render_sets:\n                    history.update(\n                        cmds.listHistory(render_sets,\n                                         future=False,\n                                         pruneDagObjects=True)\n                        or []\n                    )\n\n            # Ensure unique entries only\n            history = list(history)\n\n            files = cmds.ls(history,\n                            # It's important only node types are passed that\n                            # exist (e.g. for loaded plugins) because otherwise\n                            # the result will turn back empty\n                            type=list(FILE_NODES.keys()),\n                            long=True)\n\n            # Sort for log readability\n            files.sort()\n\n        self.log.debug(\"Collected file nodes:\\n{}\".format(files))\n        # Collect textures if any file nodes are found\n        resources = []\n        for node in files:  # sort for log readability\n            resources.extend(self.collect_resources(node))\n        instance.data[\"resources\"] = resources\n        self.log.debug(\"Collected resources: {}\".format(resources))\n\n        # Log warning when no relevant sets were retrieved for the look.\n        if (\n            not instance.data[\"lookData\"][\"relationships\"]\n            and \"model\" not in self.families\n        ):\n            self.log.warning(\"No sets found for the nodes in the \"\n                             \"instance: %s\" % instance[:])\n\n        # Ensure unique shader sets\n        # Add shader sets to the instance for unify ID validation\n        instance.extend(shader for shader in look_sets if shader\n                        not in instance_lookup)\n\n        self.log.debug(\"Collected look for %s\" % instance)\n\n    def collect_sets(self, instance):\n        \"\"\"Collect all objectSets which are of importance for publishing\n\n        It checks if all nodes in the instance are related to any objectSet\n        which need to be\n\n        Args:\n            instance (list): all nodes to be published\n\n        Returns:\n            dict\n        \"\"\"\n\n        sets = {}\n        for node in instance:\n            related_sets = lib.get_related_sets(node)\n            if not related_sets:\n                continue\n\n            for objset in related_sets:\n                if objset in sets:\n                    continue\n\n                sets[objset] = {\"uuid\": lib.get_id(objset), \"members\": list()}\n\n        return sets\n\n    def collect_member_data(self, member, instance_members):\n        \"\"\"Get all information of the node\n        Args:\n            member (str): the name of the node to check\n            instance_members (set): the collected instance members\n\n        Returns:\n            dict\n\n        \"\"\"\n\n        node, components = (member.rsplit(\".\", 1) + [None])[:2]\n\n        # Only include valid members of the instance\n        if node not in instance_members:\n            return\n\n        node_id = lib.get_id(node)\n        if not node_id:\n            self.log.error(\"Member '{}' has no attribute 'cbId'\".format(node))\n            return\n\n        member_data = {\"name\": node, \"uuid\": node_id}\n        if components:\n            member_data[\"components\"] = components\n\n        return member_data\n\n    def collect_attributes_changed(self, instance):\n        \"\"\"Collect all userDefined attributes which have changed\n\n        Each node gets checked for user defined attributes which have been\n        altered during development. Each changes gets logged in a dictionary\n\n        [{name: node,\n          uuid: uuid,\n          attributes: {attribute: value}}]\n\n        Args:\n            instance (list): all nodes which will be published\n\n        Returns:\n            list\n        \"\"\"\n\n        attributes = []\n        for node in instance:\n\n            # Collect changes to \"custom\" attributes\n            node_attrs = get_look_attrs(node)\n\n            # Only include if there are any properties we care about\n            if not node_attrs:\n                continue\n\n            self.log.debug(\n                \"Node \\\"{0}\\\" attributes: {1}\".format(node, node_attrs)\n            )\n\n            node_attributes = {}\n            for attr in node_attrs:\n                if not cmds.attributeQuery(attr, node=node, exists=True):\n                    continue\n                attribute = \"{}.{}\".format(node, attr)\n                # We don't support mixed-type attributes yet.\n                if cmds.attributeQuery(attr, node=node, multi=True):\n                    self.log.warning(\"Attribute '{}' is mixed-type and is \"\n                                     \"not supported yet.\".format(attribute))\n                    continue\n                if cmds.getAttr(attribute, type=True) == \"message\":\n                    continue\n                node_attributes[attr] = cmds.getAttr(attribute, asString=True)\n            # Only include if there are any properties we care about\n            if not node_attributes:\n                continue\n            attributes.append({\"name\": node,\n                               \"uuid\": lib.get_id(node),\n                               \"attributes\": node_attributes})\n\n        return attributes\n\n    def collect_resources(self, node):\n        \"\"\"Collect the link to the file(s) used (resource)\n        Args:\n            node (str): name of the node\n\n        Returns:\n            dict\n        \"\"\"\n        if cmds.nodeType(node) not in FILE_NODES:\n            self.log.error(\n                \"Unsupported file node: {}\".format(cmds.nodeType(node)))\n            raise AssertionError(\"Unsupported file node\")\n\n        self.log.debug(\n            \"Collecting resource: {} ({})\".format(node, cmds.nodeType(node))\n        )\n\n        attributes = get_attributes(FILE_NODES, cmds.nodeType(node), node)\n        for attribute in attributes:\n            source = cmds.getAttr(\"{}.{}\".format(\n                node,\n                attribute\n            ))\n\n            self.log.debug(\"  - file source: {}\".format(source))\n            color_space_attr = \"{}.colorSpace\".format(node)\n            try:\n                color_space = cmds.getAttr(color_space_attr)\n            except ValueError:\n                # node doesn't have colorspace attribute\n                color_space = \"Raw\"\n\n            # Compare with the computed file path, e.g. the one with\n            # the <UDIM> pattern in it, to generate some logging information\n            # about this difference\n            # Only for file nodes with `fileTextureName` attribute\n            if attribute == \"fileTextureName\":\n                computed_source = cmds.getAttr(\n                    \"{}.computedFileTextureNamePattern\".format(node)\n                )\n                if source != computed_source:\n                    self.log.debug(\"Detected computed file pattern difference \"\n                                   \"from original pattern: {0} \"\n                                   \"({1} -> {2})\".format(node,\n                                                         source,\n                                                         computed_source))\n\n            # renderman allows nodes to have filename attribute empty while\n            # you can have another incoming connection from different node.\n            if not source and cmds.nodeType(node) in PXR_NODES:\n                self.log.debug(\"Renderman: source is empty, skipping...\")\n                continue\n            # We replace backslashes with forward slashes because V-Ray\n            # can't handle the UDIM files with the backslashes in the\n            # paths as the computed patterns\n            source = source.replace(\"\\\\\", \"/\")\n\n            files = get_file_node_files(node)\n            if len(files) == 0:\n                self.log.debug(\"No valid files found from node `%s`\" % node)\n\n            self.log.debug(\"collection of resource done:\")\n            self.log.debug(\"  - node: {}\".format(node))\n            self.log.debug(\"  - attribute: {}\".format(attribute))\n            self.log.debug(\"  - source: {}\".format(source))\n            self.log.debug(\"  - file: {}\".format(files))\n            self.log.debug(\"  - color space: {}\".format(color_space))\n\n            # Define the resource\n            yield {\n                \"node\": node,\n                # here we are passing not only attribute, but with node again\n                # this should be simplified and changed extractor.\n                \"attribute\": \"{}.{}\".format(node, attribute),\n                \"source\": source,  # required for resources\n                \"files\": files,\n                \"color_space\": color_space\n            }  # required for resources\n\n\nclass CollectModelRenderSets(CollectLook):\n    \"\"\"Collect render attribute sets for model instance.\n\n    Collects additional render attribute sets so they can be\n    published with model.\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.21\n    families = [\"model\"]\n    label = \"Collect Model Render Sets\"\n    hosts = [\"maya\"]\n    maketx = True\n\n    def collect_sets(self, instance):\n        \"\"\"Collect all related objectSets except shadingEngines\n\n        Args:\n            instance (list): all nodes to be published\n\n        Returns:\n            dict\n        \"\"\"\n\n        sets = {}\n        for node in instance:\n            related_sets = lib.get_related_sets(node)\n            if not related_sets:\n                continue\n\n            for objset in related_sets:\n                if objset in sets:\n                    continue\n\n                if \"shadingEngine\" in cmds.nodeType(objset, inherited=True):\n                    continue\n\n                sets[objset] = {\"uuid\": lib.get_id(objset), \"members\": list()}\n\n        return sets\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_maya_scene_time.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\n\nclass CollectMayaSceneTime(pyblish.api.InstancePlugin):\n    \"\"\"Collect Maya Scene playback range\n\n    This allows to reproduce the playback range for the content to be loaded.\n    It does *not* limit the extracted data to only data inside that time range.\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.2\n    label = 'Collect Maya Scene Time'\n    families = [\"mayaScene\"]\n\n    def process(self, instance):\n        instance.data.update({\n            \"frameStart\": int(\n                cmds.playbackOptions(query=True, minTime=True)),\n            \"frameEnd\": int(\n                cmds.playbackOptions(query=True, maxTime=True)),\n            \"frameStartHandle\": int(\n                cmds.playbackOptions(query=True, animationStartTime=True)),\n            \"frameEndHandle\": int(\n                cmds.playbackOptions(query=True, animationEndTime=True))\n        })\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_maya_units.py",
    "content": "import maya.cmds as cmds\nimport maya.mel as mel\n\nimport pyblish.api\n\n\nclass CollectMayaUnits(pyblish.api.ContextPlugin):\n    \"\"\"Collect Maya's scene units.\"\"\"\n\n    label = \"Maya Units\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"maya\"]\n\n    def process(self, context):\n\n        # Get the current linear units\n        units = cmds.currentUnit(query=True, linear=True)\n\n        # Get the current angular units ('deg' or 'rad')\n        units_angle = cmds.currentUnit(query=True, angle=True)\n\n        # Get the current time units\n        # Using the mel command is simpler than using\n        # `cmds.currentUnit(q=1, time=1)`. Otherwise we\n        # have to parse the returned string value to FPS\n        fps = mel.eval('currentTimeUnitToFPS()')\n\n        context.data['linearUnits'] = units\n        context.data['angularUnits'] = units_angle\n        context.data['fps'] = fps\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_maya_workspace.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom maya import cmds\n\n\nclass CollectMayaWorkspace(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current workspace into context\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.5\n    label = \"Maya Workspace\"\n\n    hosts = ['maya']\n\n    def process(self, context):\n        workspace = cmds.workspace(rootDirectory=True, query=True)\n        if not workspace:\n            # Project has not been set. Files will\n            # instead end up next to the working file.\n            workspace = cmds.workspace(dir=True, query=True)\n\n        # Maya returns forward-slashes by default\n        normalised = os.path.normpath(workspace)\n\n        context.set_data('workspaceDir', value=normalised)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_model.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\n\nclass CollectModelData(pyblish.api.InstancePlugin):\n    \"\"\"Collect model data\n\n    Ensures always only a single frame is extracted (current frame).\n\n    Note:\n        This is a workaround so that the `pype.model` family can use the\n        same pointcache extractor implementation as animation and pointcaches.\n        This always enforces the \"current\" frame to be published.\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.2\n    label = 'Collect Model Data'\n    families = [\"model\"]\n\n    def process(self, instance):\n        # Extract only current frame (override)\n        frame = cmds.currentTime(query=True)\n        instance.data[\"frameStart\"] = frame\n        instance.data[\"frameEnd\"] = frame\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_multiverse_look.py",
    "content": "import glob\nimport os\nimport re\n\nfrom maya import cmds\nimport pyblish.api\nfrom openpype.hosts.maya.api import lib\n\nSHAPE_ATTRS = [\"castsShadows\",\n               \"receiveShadows\",\n               \"motionBlur\",\n               \"primaryVisibility\",\n               \"smoothShading\",\n               \"visibleInReflections\",\n               \"visibleInRefractions\",\n               \"doubleSided\",\n               \"opposite\"]\n\nSHAPE_ATTRS = set(SHAPE_ATTRS)\nCOLOUR_SPACES = ['sRGB', 'linear', 'auto']\nMIPMAP_EXTENSIONS = ['tdl']\n\n\nclass _NodeTypeAttrib(object):\n    \"\"\"docstring for _NodeType\"\"\"\n\n    def __init__(self, name, fname, computed_fname=None, colour_space=None):\n        self.name = name\n        self.fname = fname\n        self.computed_fname = computed_fname or fname\n        self.colour_space = colour_space or \"colorSpace\"\n\n    def get_fname(self, node):\n        return \"{}.{}\".format(node, self.fname)\n\n    def get_computed_fname(self, node):\n        return \"{}.{}\".format(node, self.computed_fname)\n\n    def get_colour_space(self, node):\n        return \"{}.{}\".format(node, self.colour_space)\n\n    def __str__(self):\n        return \"_NodeTypeAttrib(name={}, fname={}, \"\n        \"computed_fname={}, colour_space={})\".format(\n            self.name, self.fname, self.computed_fname, self.colour_space)\n\n\nNODETYPES = {\n    \"file\": [_NodeTypeAttrib(\"file\", \"fileTextureName\",\n                             \"computedFileTextureNamePattern\")],\n    \"aiImage\": [_NodeTypeAttrib(\"aiImage\", \"filename\")],\n    \"RedshiftNormalMap\": [_NodeTypeAttrib(\"RedshiftNormalMap\", \"tex0\")],\n    \"dlTexture\": [_NodeTypeAttrib(\"dlTexture\", \"textureFile\",\n                                  None, \"textureFile_meta_colorspace\")],\n    \"dlTriplanar\": [_NodeTypeAttrib(\"dlTriplanar\", \"colorTexture\",\n                                    None, \"colorTexture_meta_colorspace\"),\n                    _NodeTypeAttrib(\"dlTriplanar\", \"floatTexture\",\n                                    None, \"floatTexture_meta_colorspace\"),\n                    _NodeTypeAttrib(\"dlTriplanar\", \"heightTexture\",\n                                    None, \"heightTexture_meta_colorspace\")]\n}\n\n\ndef get_file_paths_for_node(node):\n    \"\"\"Gets all the file paths in this node.\n\n    Returns all filepaths that this node references. Some node types only\n    reference one, but others, like dlTriplanar, can reference 3.\n\n    Args:\n        node (str): Name of the Maya node\n\n    Returns\n        list(str): A list with all evaluated maya attributes for filepaths.\n    \"\"\"\n\n    node_type = cmds.nodeType(node)\n    if node_type not in NODETYPES:\n        return []\n\n    paths = []\n    for node_type_attr in NODETYPES[node_type]:\n        fname = cmds.getAttr(\"{}.{}\".format(node, node_type_attr.fname))\n        paths.append(fname)\n    return paths\n\n\ndef node_uses_image_sequence(node):\n    \"\"\"Return whether file node uses an image sequence or single image.\n\n    Determine if a node uses an image sequence or just a single image,\n    not always obvious from its file path alone.\n\n    Args:\n        node (str): Name of the Maya node\n\n    Returns:\n        bool: True if node uses an image sequence\n\n    \"\"\"\n\n    # useFrameExtension indicates an explicit image sequence\n    paths = get_file_node_paths(node)\n    paths = [path.lower() for path in paths]\n\n    # The following tokens imply a sequence\n    patterns = [\"<udim>\", \"<tile>\", \"<uvtile>\", \"u<u>_v<v>\", \"<frame0\"]\n\n    def pattern_in_paths(patterns, paths):\n        \"\"\"Helper function for checking to see if a pattern is contained\n        in the list of paths\"\"\"\n        for pattern in patterns:\n            for path in paths:\n                if pattern in path:\n                    return True\n        return False\n\n    node_type = cmds.nodeType(node)\n    if node_type == 'dlTexture':\n        return (cmds.getAttr('{}.useImageSequence'.format(node)) or\n                pattern_in_paths(patterns, paths))\n    elif node_type == \"file\":\n        return (cmds.getAttr('{}.useFrameExtension'.format(node)) or\n                pattern_in_paths(patterns, paths))\n    return False\n\n\ndef seq_to_glob(path):\n    \"\"\"Takes an image sequence path and returns it in glob format,\n    with the frame number replaced by a '*'.\n\n    Image sequences may be numerical sequences, e.g. /path/to/file.1001.exr\n    will return as /path/to/file.*.exr.\n\n    Image sequences may also use tokens to denote sequences, e.g.\n    /path/to/texture.<UDIM>.tif will return as /path/to/texture.*.tif.\n\n    Args:\n        path (str): the image sequence path\n\n    Returns:\n        str: Return glob string that matches the filename pattern.\n\n    \"\"\"\n\n    if path is None:\n        return path\n\n    # If any of the patterns, convert the pattern\n    patterns = {\n        \"<udim>\": \"<udim>\",\n        \"<tile>\": \"<tile>\",\n        \"<uvtile>\": \"<uvtile>\",\n        \"#\": \"#\",\n        \"u<u>_v<v>\": \"<u>|<v>\",\n        \"<frame0\": \"<frame0\\d+>\",  # noqa - copied from collect_look.py\n        \"<f>\": \"<f>\"\n    }\n\n    lower = path.lower()\n    has_pattern = False\n    for pattern, regex_pattern in patterns.items():\n        if pattern in lower:\n            path = re.sub(regex_pattern, \"*\", path, flags=re.IGNORECASE)\n            has_pattern = True\n\n    if has_pattern:\n        return path\n\n    base = os.path.basename(path)\n    matches = list(re.finditer(r'\\d+', base))\n    if matches:\n        match = matches[-1]\n        new_base = '{0}*{1}'.format(base[:match.start()],\n                                    base[match.end():])\n        head = os.path.dirname(path)\n        return os.path.join(head, new_base)\n    else:\n        return path\n\n\ndef get_file_node_paths(node):\n    \"\"\"Get the file path used by a Maya file node.\n\n    Args:\n        node (str): Name of the Maya file node\n\n    Returns:\n        str: the file path in use\n\n    \"\"\"\n    # if the path appears to be sequence, use computedFileTextureNamePattern,\n    # this preserves the <> tag\n    if cmds.attributeQuery('computedFileTextureNamePattern',\n                           node=node,\n                           exists=True):\n        plug = '{0}.computedFileTextureNamePattern'.format(node)\n        texture_pattern = cmds.getAttr(plug)\n\n        patterns = [\"<udim>\",\n                    \"<tile>\",\n                    \"u<u>_v<v>\",\n                    \"<f>\",\n                    \"<frame0\",\n                    \"<uvtile>\"]\n        lower = texture_pattern.lower()\n        if any(pattern in lower for pattern in patterns):\n            return [texture_pattern]\n\n    return get_file_paths_for_node(node)\n\n\ndef get_file_node_files(node):\n    \"\"\"Return the file paths related to the file node\n\n    Note:\n        Will only return existing files. Returns an empty list\n        if not valid existing files are linked.\n\n    Returns:\n        list: List of full file paths.\n\n    \"\"\"\n\n    paths = get_file_node_paths(node)\n    paths = [cmds.workspace(expandName=path) for path in paths]\n    if node_uses_image_sequence(node):\n        globs = []\n        for path in paths:\n            globs += glob.glob(seq_to_glob(path))\n        return globs\n    else:\n        return list(filter(lambda x: os.path.exists(x), paths))\n\n\ndef get_mipmap(fname):\n    for colour_space in COLOUR_SPACES:\n        for mipmap_ext in MIPMAP_EXTENSIONS:\n            mipmap_fname = '.'.join([fname, colour_space, mipmap_ext])\n            if os.path.exists(mipmap_fname):\n                return mipmap_fname\n    return None\n\n\ndef is_mipmap(fname):\n    ext = os.path.splitext(fname)[1][1:]\n    if ext in MIPMAP_EXTENSIONS:\n        return True\n    return False\n\n\nclass CollectMultiverseLookData(pyblish.api.InstancePlugin):\n    \"\"\"Collect Multiverse Look\n\n    Searches through the overrides finding all material overrides. From there\n    it extracts the shading group and then finds all texture files in the\n    shading group network. It also checks for mipmap versions of texture files\n    and adds them to the resources to get published.\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.2\n    label = 'Collect Multiverse Look'\n    families = [\"mvLook\"]\n\n    def process(self, instance):\n        # Load plugin first\n        cmds.loadPlugin(\"MultiverseForMaya\", quiet=True)\n        import multiverse\n\n        self.log.debug(\"Processing mvLook for '{}'\".format(instance))\n\n        nodes = set()\n        for node in instance:\n            # We want only mvUsdCompoundShape nodes.\n            nodes_of_interest = cmds.ls(node,\n                                        dag=True,\n                                        shapes=False,\n                                        type=\"mvUsdCompoundShape\",\n                                        noIntermediate=True,\n                                        long=True)\n            nodes.update(nodes_of_interest)\n\n        sets = {}\n        instance.data[\"resources\"] = []\n        publishMipMap = instance.data[\"publishMipMap\"]\n\n        for node in nodes:\n            self.log.debug(\"Getting resources for '{}'\".format(node))\n\n            # We know what nodes need to be collected, now we need to\n            # extract the materials overrides.\n            overrides = multiverse.ListMaterialOverridePrims(node)\n            for override in overrides:\n                matOver = multiverse.GetMaterialOverride(node, override)\n\n                if isinstance(matOver, multiverse.MaterialSourceShadingGroup):\n                    # We now need to grab the shadingGroup so add it to the\n                    # sets we pass down the pipe.\n                    shadingGroup = matOver.shadingGroupName\n                    self.log.debug(\"ShadingGroup = '{}'\".format(shadingGroup))\n                    sets[shadingGroup] = {\"uuid\": lib.get_id(\n                        shadingGroup), \"members\": list()}\n\n                    # The SG may reference files, add those too!\n                    history = cmds.listHistory(\n                        shadingGroup, allConnections=True)\n\n                    # We need to iterate over node_types since `cmds.ls` may\n                    # error out if we don't have the appropriate plugin loaded.\n                    files = []\n                    for node_type in NODETYPES.keys():\n                        files += cmds.ls(history,\n                                         type=node_type,\n                                         long=True)\n\n                    for f in files:\n                        resources = self.collect_resource(f, publishMipMap)\n                        instance.data[\"resources\"] += resources\n\n                elif isinstance(matOver, multiverse.MaterialSourceUsdPath):\n                    # TODO: Handle this later.\n                    pass\n\n        # Store data on the instance for validators, extractos, etc.\n        instance.data[\"lookData\"] = {\n            \"attributes\": [],\n            \"relationships\": sets\n        }\n\n    def collect_resource(self, node, publishMipMap):\n        \"\"\"Collect the link to the file(s) used (resource)\n        Args:\n            node (str): name of the node\n\n        Returns:\n            dict\n        \"\"\"\n\n        node_type = cmds.nodeType(node)\n        self.log.debug(\"processing: {}/{}\".format(node, node_type))\n\n        if node_type not in NODETYPES:\n            self.log.error(\"Unsupported file node: {}\".format(node_type))\n            raise AssertionError(\"Unsupported file node\")\n\n        resources = []\n        for node_type_attr in NODETYPES[node_type]:\n            fname_attrib = node_type_attr.get_fname(node)\n            computed_fname_attrib = node_type_attr.get_computed_fname(node)\n            colour_space_attrib = node_type_attr.get_colour_space(node)\n\n            source = cmds.getAttr(fname_attrib)\n            color_space = \"Raw\"\n            try:\n                color_space = cmds.getAttr(colour_space_attrib)\n            except ValueError:\n                # node doesn't have colorspace attribute, use \"Raw\" from before\n                pass\n            # Compare with the computed file path, e.g. the one with the <UDIM>\n            # pattern in it, to generate some logging information about this\n            # difference\n            # computed_attribute = \"{}.computedFileTextureNamePattern\".format(node)  # noqa\n            computed_source = cmds.getAttr(computed_fname_attrib)\n            if source != computed_source:\n                self.log.debug(\"Detected computed file pattern difference \"\n                               \"from original pattern: {0} \"\n                               \"({1} -> {2})\".format(node,\n                                                     source,\n                                                     computed_source))\n\n            # We replace backslashes with forward slashes because V-Ray\n            # can't handle the UDIM files with the backslashes in the\n            # paths as the computed patterns\n            source = source.replace(\"\\\\\", \"/\")\n\n            files = get_file_node_files(node)\n            files = self.handle_files(files, publishMipMap)\n            if len(files) == 0:\n                self.log.error(\"No valid files found from node `%s`\" % node)\n\n            self.log.debug(\"collection of resource done:\")\n            self.log.debug(\"  - node: {}\".format(node))\n            self.log.debug(\"  - attribute: {}\".format(fname_attrib))\n            self.log.debug(\"  - source: {}\".format(source))\n            self.log.debug(\"  - file: {}\".format(files))\n            self.log.debug(\"  - color space: {}\".format(color_space))\n\n            # Define the resource\n            resource = {\"node\": node,\n                        \"attribute\": fname_attrib,\n                        \"source\": source,  # required for resources\n                        \"files\": files,\n                        \"color_space\": color_space}  # required for resources\n            resources.append(resource)\n        return resources\n\n    def handle_files(self, files, publishMipMap):\n        \"\"\"This will go through all the files and make sure that they are\n        either already mipmapped or have a corresponding mipmap sidecar and\n        add that to the list.\"\"\"\n        if not publishMipMap:\n            return files\n\n        extra_files = []\n        self.log.debug(\"Expecting MipMaps, going to look for them.\")\n        for fname in files:\n            self.log.debug(\"Checking '{}' for mipmaps\".format(fname))\n            if is_mipmap(fname):\n                self.log.debug(\" - file is already MipMap, skipping.\")\n                continue\n\n            mipmap = get_mipmap(fname)\n            if mipmap:\n                self.log.debug(\" mipmap found for '{}'\".format(fname))\n                extra_files.append(mipmap)\n            else:\n                self.log.warning(\" no mipmap found for '{}'\".format(fname))\n        return files + extra_files\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_pointcache.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\n\nclass CollectPointcache(pyblish.api.InstancePlugin):\n    \"\"\"Collect pointcache data for instance.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.4\n    families = [\"pointcache\"]\n    label = \"Collect Pointcache\"\n    hosts = [\"maya\"]\n\n    def process(self, instance):\n        if instance.data.get(\"farm\"):\n            instance.data[\"families\"].append(\"publish.farm\")\n\n        proxy_set = None\n        for node in cmds.ls(instance.data[\"setMembers\"],\n                            exactType=\"objectSet\"):\n            # Find proxy_SET objectSet in the instance for proxy meshes\n            if node.endswith(\"proxy_SET\"):\n                members = cmds.sets(node, query=True)\n                if members is None:\n                    self.log.debug(\"Skipped empty proxy_SET: \\\"%s\\\" \" % node)\n                    continue\n                self.log.debug(\"Found proxy set: {}\".format(node))\n\n                proxy_set = node\n                instance.data[\"proxy\"] = []\n                instance.data[\"proxyRoots\"] = []\n                for member in members:\n                    instance.data[\"proxy\"].extend(cmds.ls(member, long=True))\n                    instance.data[\"proxyRoots\"].extend(\n                        cmds.ls(member, long=True)\n                    )\n                    instance.data[\"proxy\"].extend(\n                        cmds.listRelatives(member, shapes=True, fullPath=True)\n                    )\n                self.log.debug(\n                    \"Found proxy members: {}\".format(instance.data[\"proxy\"])\n                )\n                break\n\n        if proxy_set:\n            instance.remove(proxy_set)\n            instance.data[\"setMembers\"].remove(proxy_set)\n\n        # User defined attributes.\n        instance.data[\"includeUserDefinedAttributes\"] = (\n            instance.data[\"creator_attributes\"][\"includeUserDefinedAttributes\"]\n        )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_remove_marked.py",
    "content": "import pyblish.api\n\n\nclass CollectRemoveMarked(pyblish.api.ContextPlugin):\n    \"\"\"Remove marked data\n\n    Remove instances that have 'remove' in their instance.data\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.499\n    label = 'Remove Marked Instances'\n\n    def process(self, context):\n\n        self.log.debug(context)\n        # make ftrack publishable\n        instances_to_remove = []\n        for instance in context:\n            if instance.data.get('remove'):\n                instances_to_remove.append(instance)\n\n        for instance in instances_to_remove:\n            context.remove(instance)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_render.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect render data.\n\nThis collector will go through render layers in maya and prepare all data\nneeded to create instances and their representations for submission and\npublishing on farm.\n\nRequires:\n    instance    -> families\n    instance    -> setMembers\n\n    context     -> currentFile\n    context     -> workspaceDir\n    context     -> user\n\n    session     -> AVALON_ASSET\n\nOptional:\n\nProvides:\n    instance    -> label\n    instance    -> subset\n    instance    -> attachTo\n    instance    -> setMembers\n    instance    -> publish\n    instance    -> frameStart\n    instance    -> frameEnd\n    instance    -> byFrameStep\n    instance    -> renderer\n    instance    -> family\n    instance    -> families\n    instance    -> asset\n    instance    -> time\n    instance    -> author\n    instance    -> source\n    instance    -> expectedFiles\n    instance    -> resolutionWidth\n    instance    -> resolutionHeight\n    instance    -> pixelAspect\n\"\"\"\n\nimport os\nimport platform\nimport json\n\nfrom maya import cmds\n\nimport pyblish.api\n\nfrom openpype.pipeline import KnownPublishError\nfrom openpype.lib import get_formatted_current_time\nfrom openpype.hosts.maya.api.lib_renderproducts import (\n    get as get_layer_render_products,\n    UnsupportedRendererException\n)\nfrom openpype.hosts.maya.api import lib\n\n\nclass CollectMayaRender(pyblish.api.InstancePlugin):\n    \"\"\"Gather all publishable render layers from renderSetup.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.01\n    hosts = [\"maya\"]\n    families = [\"renderlayer\"]\n    label = \"Collect Render Layers\"\n    sync_workfile_version = False\n\n    _aov_chars = {\n        \"dot\": \".\",\n        \"dash\": \"-\",\n        \"underscore\": \"_\"\n    }\n\n    def process(self, instance):\n\n        # TODO: Re-add force enable of workfile instance?\n        # TODO: Re-add legacy layer support with LAYER_ prefix but in Creator\n        # TODO: Set and collect active state of RenderLayer in Creator using\n        #       renderlayer.isRenderable()\n        context = instance.context\n\n        layer = instance.data[\"transientData\"][\"layer\"]\n        objset = instance.data.get(\"instance_node\")\n        filepath = context.data[\"currentFile\"].replace(\"\\\\\", \"/\")\n        workspace = context.data[\"workspaceDir\"]\n\n        # check if layer is renderable\n        if not layer.isRenderable():\n            msg = \"Render layer [ {} ] is not \" \"renderable\".format(\n                layer.name()\n            )\n            self.log.warning(msg)\n\n        # detect if there are sets (subsets) to attach render to\n        sets = cmds.sets(objset, query=True) or []\n        attach_to = []\n        for s in sets:\n            if not cmds.attributeQuery(\"family\", node=s, exists=True):\n                continue\n\n            attach_to.append(\n                {\n                    \"version\": None,  # we need integrator for that\n                    \"subset\": s,\n                    \"family\": cmds.getAttr(\"{}.family\".format(s)),\n                }\n            )\n            self.log.debug(\" -> attach render to: {}\".format(s))\n\n        layer_name = layer.name()\n\n        # collect all frames we are expecting to be rendered\n        # return all expected files for all cameras and aovs in given\n        # frame range\n        try:\n            layer_render_products = get_layer_render_products(layer.name())\n        except UnsupportedRendererException as exc:\n            raise KnownPublishError(exc)\n        render_products = layer_render_products.layer_data.products\n        assert render_products, \"no render products generated\"\n        expected_files = []\n        multipart = False\n        for product in render_products:\n            if product.multipart:\n                multipart = True\n            product_name = product.productName\n            if product.camera and layer_render_products.has_camera_token():\n                product_name = \"{}{}\".format(\n                    product.camera,\n                    \"_{}\".format(product_name) if product_name else \"\")\n            expected_files.append(\n                {\n                    product_name: layer_render_products.get_files(\n                        product)\n                })\n\n        has_cameras = any(product.camera for product in render_products)\n        assert has_cameras, \"No render cameras found.\"\n\n        self.log.debug(\"multipart: {}\".format(\n            multipart))\n        assert expected_files, \"no file names were generated, this is a bug\"\n        self.log.debug(\n            \"expected files: {}\".format(\n                json.dumps(expected_files, indent=4, sort_keys=True)\n            )\n        )\n\n        # if we want to attach render to subset, check if we have AOV's\n        # in expectedFiles. If so, raise error as we cannot attach AOV\n        # (considered to be subset on its own) to another subset\n        if attach_to:\n            assert isinstance(expected_files, list), (\n                \"attaching multiple AOVs or renderable cameras to \"\n                \"subset is not supported\"\n            )\n\n        # append full path\n        aov_dict = {}\n        image_directory = os.path.join(\n            cmds.workspace(query=True, rootDirectory=True),\n            cmds.workspace(fileRuleEntry=\"images\")\n        )\n        # replace relative paths with absolute. Render products are\n        # returned as list of dictionaries.\n        publish_meta_path = None\n        for aov in expected_files:\n            full_paths = []\n            aov_first_key = list(aov.keys())[0]\n            for file in aov[aov_first_key]:\n                full_path = os.path.join(image_directory, file)\n                full_path = full_path.replace(\"\\\\\", \"/\")\n                full_paths.append(full_path)\n                publish_meta_path = os.path.dirname(full_path)\n            aov_dict[aov_first_key] = full_paths\n        full_exp_files = [aov_dict]\n        self.log.debug(full_exp_files)\n\n        if publish_meta_path is None:\n            raise KnownPublishError(\"Unable to detect any expected output \"\n                                    \"images for: {}. Make sure you have a \"\n                                    \"renderable camera and a valid frame \"\n                                    \"range set for your renderlayer.\"\n                                    \"\".format(instance.name))\n\n        frame_start_render = int(self.get_render_attribute(\n            \"startFrame\", layer=layer_name))\n        frame_end_render = int(self.get_render_attribute(\n            \"endFrame\", layer=layer_name))\n\n        if (int(context.data[\"frameStartHandle\"]) == frame_start_render\n                and int(context.data[\"frameEndHandle\"]) == frame_end_render):  # noqa: W503, E501\n\n            handle_start = context.data[\"handleStart\"]\n            handle_end = context.data[\"handleEnd\"]\n            frame_start = context.data[\"frameStart\"]\n            frame_end = context.data[\"frameEnd\"]\n            frame_start_handle = context.data[\"frameStartHandle\"]\n            frame_end_handle = context.data[\"frameEndHandle\"]\n        else:\n            handle_start = 0\n            handle_end = 0\n            frame_start = frame_start_render\n            frame_end = frame_end_render\n            frame_start_handle = frame_start_render\n            frame_end_handle = frame_end_render\n\n        # find common path to store metadata\n        # so if image prefix is branching to many directories\n        # metadata file will be located in top-most common\n        # directory.\n        # TODO: use `os.path.commonpath()` after switch to Python 3\n        publish_meta_path = os.path.normpath(publish_meta_path)\n        common_publish_meta_path = os.path.splitdrive(\n            publish_meta_path)[0]\n        if common_publish_meta_path:\n            common_publish_meta_path += os.path.sep\n        for part in publish_meta_path.replace(\n                common_publish_meta_path, \"\").split(os.path.sep):\n            common_publish_meta_path = os.path.join(\n                common_publish_meta_path, part)\n            if part == layer_name:\n                break\n\n        # TODO: replace this terrible linux hotfix with real solution :)\n        if platform.system().lower() in [\"linux\", \"darwin\"]:\n            common_publish_meta_path = \"/\" + common_publish_meta_path\n\n        self.log.debug(\n            \"Publish meta path: {}\".format(common_publish_meta_path))\n\n        # Get layer specific settings, might be overrides\n        colorspace_data = lib.get_color_management_preferences()\n        data = {\n            \"farm\": True,\n            \"attachTo\": attach_to,\n\n            \"multipartExr\": multipart,\n            \"review\": instance.data.get(\"review\") or False,\n\n            # Frame range\n            \"handleStart\": handle_start,\n            \"handleEnd\": handle_end,\n            \"frameStart\": frame_start,\n            \"frameEnd\": frame_end,\n            \"frameStartHandle\": frame_start_handle,\n            \"frameEndHandle\": frame_end_handle,\n            \"byFrameStep\": int(\n                self.get_render_attribute(\"byFrameStep\",\n                                          layer=layer_name)),\n\n            # Renderlayer\n            \"renderer\": self.get_render_attribute(\n                \"currentRenderer\", layer=layer_name).lower(),\n            \"setMembers\": layer._getLegacyNodeName(),  # legacy renderlayer\n            \"renderlayer\": layer_name,\n\n            # todo: is `time` and `author` still needed?\n            \"time\": get_formatted_current_time(),\n            \"author\": context.data[\"user\"],\n\n            # Add source to allow tracing back to the scene from\n            # which was submitted originally\n            \"source\": filepath,\n            \"expectedFiles\": full_exp_files,\n            \"publishRenderMetadataFolder\": common_publish_meta_path,\n            \"renderProducts\": layer_render_products,\n            \"resolutionWidth\": lib.get_attr_in_layer(\n                \"defaultResolution.width\", layer=layer_name\n            ),\n            \"resolutionHeight\": lib.get_attr_in_layer(\n                \"defaultResolution.height\", layer=layer_name\n            ),\n            \"pixelAspect\": lib.get_attr_in_layer(\n                \"defaultResolution.pixelAspect\", layer=layer_name\n            ),\n\n            # todo: Following are likely not needed due to collecting from the\n            #       instance itself if they are attribute definitions\n            \"tileRendering\": instance.data.get(\"tileRendering\") or False,  # noqa: E501\n            \"tilesX\": instance.data.get(\"tilesX\") or 2,\n            \"tilesY\": instance.data.get(\"tilesY\") or 2,\n            \"convertToScanline\": instance.data.get(\n                \"convertToScanline\") or False,\n            \"useReferencedAovs\": instance.data.get(\n                \"useReferencedAovs\") or instance.data.get(\n                    \"vrayUseReferencedAovs\") or False,\n            \"aovSeparator\": layer_render_products.layer_data.aov_separator,  # noqa: E501\n            \"renderSetupIncludeLights\": instance.data.get(\n                \"renderSetupIncludeLights\"\n            ),\n            \"colorspaceConfig\": colorspace_data[\"config\"],\n            \"colorspaceDisplay\": colorspace_data[\"display\"],\n            \"colorspaceView\": colorspace_data[\"view\"],\n        }\n\n        rr_settings = (\n            context.data[\"system_settings\"][\"modules\"][\"royalrender\"]\n        )\n        if rr_settings[\"enabled\"]:\n            data[\"rrPathName\"] = instance.data.get(\"rrPathName\")\n            self.log.debug(data[\"rrPathName\"])\n\n        if self.sync_workfile_version:\n            data[\"version\"] = context.data[\"version\"]\n            for _instance in context:\n                if _instance.data['family'] == \"workfile\":\n                    _instance.data[\"version\"] = context.data[\"version\"]\n\n        # Define nice label\n        label = \"{0} ({1})\".format(layer_name, instance.data[\"asset\"])\n        label += \"  [{0}-{1}]\".format(\n            int(data[\"frameStartHandle\"]), int(data[\"frameEndHandle\"])\n        )\n        data[\"label\"] = label\n\n        # Override frames should be False if extendFrames is False. This is\n        # to ensure it doesn't go off doing crazy unpredictable things\n        extend_frames = instance.data.get(\"extendFrames\", False)\n        if not extend_frames:\n            instance.data[\"overrideExistingFrame\"] = False\n\n        # Update the instace\n        instance.data.update(data)\n\n    @staticmethod\n    def get_render_attribute(attr, layer):\n        \"\"\"Get attribute from render options.\n\n        Args:\n            attr (str): name of attribute to be looked up\n            layer (str): name of render layer\n\n        Returns:\n            Attribute value\n\n        \"\"\"\n        return lib.get_attr_in_layer(\n            \"defaultRenderGlobals.{}\".format(attr), layer=layer\n        )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_render_layer_aovs.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\nfrom openpype.hosts.maya.api import lib\n\n\nclass CollectRenderLayerAOVS(pyblish.api.InstancePlugin):\n    \"\"\"Collect all render layer's AOVs / Render Elements that will render.\n\n    This collector is important to be able to Extend Frames.\n\n    Technical information:\n    Each renderer uses different logic to work with render passes.\n    VRay - RenderElement\n        Simple node connection to the actual renderLayer node\n\n    Arnold - AOV:\n        Uses its own render settings node and connects an aiOAV to it\n\n    Redshift - AOV:\n        Uses its own render settings node and RedshiftAOV node. It is not\n        connected but all AOVs are enabled for all render layers by default.\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.02\n    label = \"Render Elements / AOVs\"\n    hosts = [\"maya\"]\n    families = [\"renderlayer\"]\n\n    def process(self, instance):\n\n        # Check if Extend Frames is toggled\n        if not instance.data(\"extendFrames\", False):\n            return\n\n        # Get renderer\n        renderer = instance.data[\"renderer\"]\n        self.log.debug(\"Renderer found: {}\".format(renderer))\n\n        rp_node_types = {\"vray\": [\"VRayRenderElement\", \"VRayRenderElementSet\"],\n                         \"arnold\": [\"aiAOV\"],\n                         \"redshift\": [\"RedshiftAOV\"]}\n\n        if renderer not in rp_node_types.keys():\n            self.log.error(\"Unsupported renderer found: '{}'\".format(renderer))\n            return\n\n        result = []\n\n        # Collect all AOVs / Render Elements\n        layer = instance.data[\"renderlayer\"]\n        node_type = rp_node_types[renderer]\n        render_elements = cmds.ls(type=node_type)\n\n        # Check if AOVs / Render Elements are enabled\n        for element in render_elements:\n            enabled = lib.get_attr_in_layer(\"{}.enabled\".format(element),\n                                            layer=layer)\n            if not enabled:\n                continue\n\n            pass_name = self.get_pass_name(renderer, element)\n            render_pass = \"%s.%s\" % (instance.data[\"subset\"], pass_name)\n\n            result.append(render_pass)\n\n        self.log.debug(\"Found {} render elements / AOVs for \"\n                       \"'{}'\".format(len(result), instance.data[\"subset\"]))\n\n        instance.data[\"renderPasses\"] = result\n\n    def get_pass_name(self, renderer, node):\n\n        if renderer == \"vray\":\n\n            # Get render element pass type\n            vray_node_attr = next(attr for attr in cmds.listAttr(node)\n                                  if attr.startswith(\"vray_name\"))\n            pass_type = vray_node_attr.rsplit(\"_\", 1)[-1]\n\n            # Support V-Ray extratex explicit name (if set by user)\n            if pass_type == \"extratex\":\n                explicit_attr = \"{}.vray_explicit_name_extratex\".format(node)\n                explicit_name = cmds.getAttr(explicit_attr)\n                if explicit_name:\n                    return explicit_name\n\n            # Node type is in the attribute name but we need to check if value\n            # of the attribute as it can be changed\n            return cmds.getAttr(\"{}.{}\".format(node, vray_node_attr))\n\n        elif renderer in [\"arnold\", \"redshift\"]:\n            return cmds.getAttr(\"{}.name\".format(node))\n        else:\n            raise RuntimeError(\"Unsupported renderer: '{}'\".format(renderer))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_renderable_camera.py",
    "content": "import pyblish.api\n\nfrom maya import cmds\n\nfrom openpype.hosts.maya.api.lib_rendersetup import get_attr_in_layer\n\n\nclass CollectRenderableCamera(pyblish.api.InstancePlugin):\n    \"\"\"Collect the renderable camera(s) for the render layer\"\"\"\n\n    # Offset to be after renderlayer collection.\n    order = pyblish.api.CollectorOrder + 0.02\n    label = \"Collect Renderable Camera(s)\"\n    hosts = [\"maya\"]\n    families = [\"vrayscene_layer\",\n                \"renderlayer\"]\n\n    def process(self, instance):\n        if \"vrayscene_layer\" in instance.data.get(\"families\", []):\n            layer = instance.data.get(\"layer\")\n        else:\n            layer = instance.data[\"renderlayer\"]\n\n        cameras = cmds.ls(type=\"camera\", long=True)\n        renderable = [cam for cam in cameras if\n                      get_attr_in_layer(\"{}.renderable\".format(cam), layer)]\n\n        self.log.debug(\n            \"Found renderable cameras %s: %s\", len(renderable), renderable\n        )\n\n        instance.data[\"cameras\"] = renderable\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_review.py",
    "content": "from maya import cmds, mel\n\nimport pyblish.api\n\nfrom openpype.client import get_subset_by_name\nfrom openpype.pipeline import KnownPublishError\nfrom openpype.hosts.maya.api import lib\n\n\nclass CollectReview(pyblish.api.InstancePlugin):\n    \"\"\"Collect Review data\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.3\n    label = 'Collect Review Data'\n    families = [\"review\"]\n\n    def process(self, instance):\n\n        # Get panel.\n        instance.data[\"panel\"] = cmds.playblast(\n            activeEditor=True\n        ).rsplit(\"|\", 1)[-1]\n\n        # get cameras\n        members = instance.data['setMembers']\n        self.log.debug('members: {}'.format(members))\n        cameras = cmds.ls(members, long=True, dag=True, cameras=True)\n        camera = cameras[0] if cameras else None\n\n        context = instance.context\n        objectset = {\n            i.data.get(\"instance_node\") for i in context\n        }\n\n        # Collect display lights.\n        display_lights = instance.data.get(\"displayLights\", \"default\")\n        if display_lights == \"project_settings\":\n            settings = instance.context.data[\"project_settings\"]\n            settings = settings[\"maya\"][\"publish\"][\"ExtractPlayblast\"]\n            settings = settings[\"capture_preset\"][\"Viewport Options\"]\n            display_lights = settings[\"displayLights\"]\n\n        # Collect camera focal length.\n        burninDataMembers = instance.data.get(\"burninDataMembers\", {})\n        if camera is not None:\n            attr = camera + \".focalLength\"\n            if lib.get_attribute_input(attr):\n                start = instance.data[\"frameStart\"]\n                end = instance.data[\"frameEnd\"] + 1\n                time_range = range(int(start), int(end))\n                focal_length = [cmds.getAttr(attr, time=t) for t in time_range]\n            else:\n                focal_length = cmds.getAttr(attr)\n\n            burninDataMembers[\"focalLength\"] = focal_length\n\n        # Account for nested instances like model.\n        reviewable_subsets = list(set(members) & objectset)\n        if reviewable_subsets:\n            if len(reviewable_subsets) > 1:\n                raise KnownPublishError(\n                    \"Multiple attached subsets for review are not supported. \"\n                    \"Attached: {}\".format(\", \".join(reviewable_subsets))\n                )\n\n            reviewable_subset = reviewable_subsets[0]\n            self.log.debug(\n                \"Subset attached to review: {}\".format(reviewable_subset)\n            )\n\n            # Find the relevant publishing instance in the current context\n            reviewable_inst = next(inst for inst in context\n                                   if inst.name == reviewable_subset)\n            data = reviewable_inst.data\n\n            self.log.debug(\n                'Adding review family to {}'.format(reviewable_subset)\n            )\n            if data.get('families'):\n                data['families'].append('review')\n            else:\n                data['families'] = ['review']\n\n            data[\"cameras\"] = cameras\n            data['review_camera'] = camera\n            data['frameStartFtrack'] = instance.data[\"frameStartHandle\"]\n            data['frameEndFtrack'] = instance.data[\"frameEndHandle\"]\n            data['frameStartHandle'] = instance.data[\"frameStartHandle\"]\n            data['frameEndHandle'] = instance.data[\"frameEndHandle\"]\n            data['handleStart'] = instance.data[\"handleStart\"]\n            data['handleEnd'] = instance.data[\"handleEnd\"]\n            data[\"frameStart\"] = instance.data[\"frameStart\"]\n            data[\"frameEnd\"] = instance.data[\"frameEnd\"]\n            data['step'] = instance.data['step']\n            # this (with other time related data) should be set on\n            # representations. Once plugins like Extract Review start\n            # using representations, this should be removed from here\n            # as Extract Playblast is already adding fps to representation.\n            data['fps'] = context.data['fps']\n            data['review_width'] = instance.data['review_width']\n            data['review_height'] = instance.data['review_height']\n            data[\"isolate\"] = instance.data[\"isolate\"]\n            data[\"panZoom\"] = instance.data.get(\"panZoom\", False)\n            data[\"panel\"] = instance.data[\"panel\"]\n            data[\"displayLights\"] = display_lights\n            data[\"burninDataMembers\"] = burninDataMembers\n\n            for key, value in instance.data[\"publish_attributes\"].items():\n                data[\"publish_attributes\"][key] = value\n\n            # The review instance must be active\n            cmds.setAttr(str(instance) + '.active', 1)\n\n            instance.data['remove'] = True\n\n        else:\n            project_name = instance.context.data[\"projectName\"]\n            asset_doc = instance.context.data['assetEntity']\n            task = instance.context.data[\"task\"]\n            legacy_subset_name = task + 'Review'\n            subset_doc = get_subset_by_name(\n                project_name,\n                legacy_subset_name,\n                asset_doc[\"_id\"],\n                fields=[\"_id\"]\n            )\n            if subset_doc:\n                self.log.debug(\"Existing subsets found, keep legacy name.\")\n                instance.data['subset'] = legacy_subset_name\n\n            instance.data[\"cameras\"] = cameras\n            instance.data['review_camera'] = camera\n            instance.data['frameStartFtrack'] = \\\n                instance.data[\"frameStartHandle\"]\n            instance.data['frameEndFtrack'] = \\\n                instance.data[\"frameEndHandle\"]\n            instance.data[\"displayLights\"] = display_lights\n            instance.data[\"burninDataMembers\"] = burninDataMembers\n            # this (with other time related data) should be set on\n            # representations. Once plugins like Extract Review start\n            # using representations, this should be removed from here\n            # as Extract Playblast is already adding fps to representation.\n            instance.data[\"fps\"] = instance.context.data[\"fps\"]\n\n            # make ftrack publishable\n            instance.data.setdefault(\"families\", []).append('ftrack')\n\n            cmds.setAttr(str(instance) + '.active', 1)\n\n            # Collect audio\n            playback_slider = mel.eval('$tmpVar=$gPlayBackSlider')\n            audio_name = cmds.timeControl(playback_slider,\n                                          query=True,\n                                          sound=True)\n            display_sounds = cmds.timeControl(\n                playback_slider, query=True, displaySound=True\n            )\n\n            def get_audio_node_data(node):\n                return {\n                    \"offset\": cmds.getAttr(\"{}.offset\".format(node)),\n                    \"filename\": cmds.getAttr(\"{}.filename\".format(node))\n                }\n\n            audio_data = []\n\n            if audio_name:\n                audio_data.append(get_audio_node_data(audio_name))\n\n            elif display_sounds:\n                start_frame = int(cmds.playbackOptions(query=True, min=True))\n                end_frame = int(cmds.playbackOptions(query=True, max=True))\n\n                for node in cmds.ls(type=\"audio\"):\n                    # Check if frame range and audio range intersections,\n                    # for whether to include this audio node or not.\n                    duration = cmds.getAttr(\"{}.duration\".format(node))\n                    start_audio = cmds.getAttr(\"{}.offset\".format(node))\n                    end_audio = start_audio + duration\n\n                    if start_audio <= end_frame and end_audio > start_frame:\n                        audio_data.append(get_audio_node_data(node))\n\n            instance.data[\"audio\"] = audio_data\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_rig_sets.py",
    "content": "import pyblish.api\nfrom maya import cmds\n\n\nclass CollectRigSets(pyblish.api.InstancePlugin):\n    \"\"\"Ensure rig contains pipeline-critical content\n\n    Every rig must contain at least two object sets:\n        \"controls_SET\" - Set of all animatable controls\n        \"out_SET\" - Set of all cacheable meshes\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.05\n    label = \"Collect Rig Sets\"\n    hosts = [\"maya\"]\n    families = [\"rig\"]\n\n    accepted_output = [\"mesh\", \"transform\"]\n    accepted_controllers = [\"transform\"]\n\n    def process(self, instance):\n\n        # Find required sets by suffix\n        searching = {\"controls_SET\", \"out_SET\",\n                     \"skeletonAnim_SET\", \"skeletonMesh_SET\"}\n        found = {}\n        for node in cmds.ls(instance, exactType=\"objectSet\"):\n            for suffix in searching:\n                if node.endswith(suffix):\n                    found[suffix] = node\n                    searching.remove(suffix)\n                    break\n            if not searching:\n                break\n\n        self.log.debug(\"Found sets: {}\".format(found))\n        rig_sets = instance.data.setdefault(\"rig_sets\", {})\n        for name, objset in found.items():\n            rig_sets[name] = objset\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py",
    "content": "# -*- coding: utf-8 -*-\nfrom maya import cmds  # noqa\nimport pyblish.api\n\n\nclass CollectSkeletonMesh(pyblish.api.InstancePlugin):\n    \"\"\"Collect Static Rig Data for FBX Extractor.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.2\n    label = \"Collect Skeleton Mesh\"\n    hosts = [\"maya\"]\n    families = [\"rig\"]\n\n    def process(self, instance):\n        skeleton_mesh_set = instance.data[\"rig_sets\"].get(\n            \"skeletonMesh_SET\")\n        if not skeleton_mesh_set:\n            self.log.debug(\n                \"No skeletonMesh_SET found. \"\n                \"Skipping collecting of skeleton mesh...\"\n            )\n            return\n\n        # Store current frame to ensure single frame export\n        frame = cmds.currentTime(query=True)\n        instance.data[\"frameStart\"] = frame\n        instance.data[\"frameEnd\"] = frame\n\n        instance.data[\"skeleton_mesh\"] = []\n\n        skeleton_mesh_content = cmds.sets(\n            skeleton_mesh_set, query=True) or []\n        if not skeleton_mesh_content:\n            self.log.debug(\n                \"No object nodes in skeletonMesh_SET. \"\n                \"Skipping collecting of skeleton mesh...\"\n            )\n            return\n        instance.data[\"families\"] += [\"rig.fbx\"]\n        instance.data[\"skeleton_mesh\"] = skeleton_mesh_content\n        self.log.debug(\n            \"Collected skeletonMesh_SET members: {}\".format(\n                skeleton_mesh_content\n            ))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py",
    "content": "# -*- coding: utf-8 -*-\nfrom maya import cmds  # noqa\nimport pyblish.api\n\n\nclass CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin):\n    \"\"\"Collect Unreal Skeletal Mesh.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.2\n    label = \"Collect Unreal Skeletal Meshes\"\n    families = [\"skeletalMesh\"]\n\n    def process(self, instance):\n        frame = cmds.currentTime(query=True)\n        instance.data[\"frameStart\"] = frame\n        instance.data[\"frameEnd\"] = frame\n\n        geo_sets = [\n            i for i in instance[:]\n            if i.lower().startswith(\"geometry_set\")\n        ]\n\n        joint_sets = [\n            i for i in instance[:]\n            if i.lower().startswith(\"joints_set\")\n        ]\n\n        instance.data[\"geometry\"] = []\n        instance.data[\"joints\"] = []\n\n        for geo_set in geo_sets:\n            geo_content = cmds.ls(cmds.sets(geo_set, query=True), long=True)\n            if geo_content:\n                instance.data[\"geometry\"] += geo_content\n\n        for join_set in joint_sets:\n            join_content = cmds.ls(cmds.sets(join_set, query=True), long=True)\n            if join_content:\n                instance.data[\"joints\"] += join_content\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py",
    "content": "# -*- coding: utf-8 -*-\nfrom maya import cmds  # noqa\nimport pyblish.api\nfrom pprint import pformat\n\n\nclass CollectUnrealStaticMesh(pyblish.api.InstancePlugin):\n    \"\"\"Collect Unreal Static Mesh.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.2\n    label = \"Collect Unreal Static Meshes\"\n    families = [\"staticMesh\"]\n\n    def process(self, instance):\n        geometry_set = [\n            i for i in instance\n            if i.startswith(\"geometry_SET\")\n        ]\n        instance.data[\"geometryMembers\"] = cmds.sets(\n            geometry_set, query=True)\n\n        self.log.debug(\"geometry: {}\".format(\n            pformat(instance.data.get(\"geometryMembers\"))))\n\n        collision_set = [\n            i for i in instance\n            if i.startswith(\"collisions_SET\")\n        ]\n        instance.data[\"collisionMembers\"] = cmds.sets(\n            collision_set, query=True)\n\n        self.log.debug(\"collisions: {}\".format(\n            pformat(instance.data.get(\"collisionMembers\"))))\n\n        frame = cmds.currentTime(query=True)\n        instance.data[\"frameStart\"] = frame\n        instance.data[\"frameEnd\"] = frame\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_user_defined_attributes.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\n\nclass CollectUserDefinedAttributes(pyblish.api.InstancePlugin):\n    \"\"\"Collect user defined attributes for nodes in instance.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.45\n    families = [\"pointcache\", \"animation\", \"usd\"]\n    label = \"Collect User Defined Attributes\"\n    hosts = [\"maya\"]\n\n    def process(self, instance):\n\n        # Collect user defined attributes.\n        if not instance.data.get(\"includeUserDefinedAttributes\", False):\n            return\n\n        if \"out_hierarchy\" in instance.data:\n            # animation family\n            nodes = instance.data[\"out_hierarchy\"]\n        else:\n            nodes = instance[:]\n        if not nodes:\n            return\n\n        shapes = cmds.listRelatives(nodes, shapes=True, fullPath=True) or []\n        nodes = set(nodes).union(shapes)\n\n        attrs = cmds.listAttr(list(nodes), userDefined=True) or []\n        user_defined_attributes = list(sorted(set(attrs)))\n        instance.data[\"userDefinedAttributes\"] = user_defined_attributes\n\n        self.log.debug(\n            \"Collected user defined attributes: {}\".format(\n                \", \".join(user_defined_attributes)\n            )\n        )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_vrayproxy.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect Vray Proxy.\"\"\"\nimport pyblish.api\n\n\nclass CollectVrayProxy(pyblish.api.InstancePlugin):\n    \"\"\"Collect Vray Proxy instance.\n\n    Add `pointcache` family for it.\n    \"\"\"\n    order = pyblish.api.CollectorOrder + 0.01\n    label = \"Collect Vray Proxy\"\n    families = [\"vrayproxy\"]\n\n    def process(self, instance):\n        \"\"\"Collector entry point.\"\"\"\n        if not instance.data.get('families'):\n            instance.data[\"families\"] = []\n\n        if instance.data.get(\"vrmesh\"):\n            instance.data[\"families\"].append(\"vrayproxy.vrmesh\")\n\n        if instance.data.get(\"alembic\"):\n            instance.data[\"families\"].append(\"vrayproxy.alembic\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_vrayscene.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect Vray Scene and prepare it for extraction and publishing.\"\"\"\nimport re\n\nimport maya.app.renderSetup.model.renderSetup as renderSetup\nfrom maya import cmds\n\nimport pyblish.api\n\nfrom openpype.pipeline import legacy_io\nfrom openpype.lib import get_formatted_current_time\nfrom openpype.hosts.maya.api import lib\n\n\nclass CollectVrayScene(pyblish.api.InstancePlugin):\n    \"\"\"Collect Vray Scene.\n\n    If export on farm is checked, job is created to export it.\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.01\n    label = \"Collect Vray Scene\"\n    families = [\"vrayscene\"]\n\n    def process(self, instance):\n        \"\"\"Collector entry point.\"\"\"\n\n        context = instance.context\n\n        layer = instance.data[\"transientData\"][\"layer\"]\n        layer_name = layer.name()\n\n        renderer = self.get_render_attribute(\"currentRenderer\",\n                                             layer=layer_name)\n        if renderer != \"vray\":\n            self.log.warning(\"Layer '{}' renderer is not set to V-Ray\".format(\n                layer_name\n            ))\n\n        # collect all frames we are expecting to be rendered\n        frame_start_render = int(self.get_render_attribute(\n            \"startFrame\", layer=layer_name))\n        frame_end_render = int(self.get_render_attribute(\n            \"endFrame\", layer=layer_name))\n\n        if (int(context.data['frameStartHandle']) == frame_start_render\n                and int(context.data['frameEndHandle']) == frame_end_render):  # noqa: W503, E501\n\n            handle_start = context.data['handleStart']\n            handle_end = context.data['handleEnd']\n            frame_start = context.data['frameStart']\n            frame_end = context.data['frameEnd']\n            frame_start_handle = context.data['frameStartHandle']\n            frame_end_handle = context.data['frameEndHandle']\n        else:\n            handle_start = 0\n            handle_end = 0\n            frame_start = frame_start_render\n            frame_end = frame_end_render\n            frame_start_handle = frame_start_render\n            frame_end_handle = frame_end_render\n\n        # Get layer specific settings, might be overrides\n        data = {\n            \"subset\": layer_name,\n            \"layer\": layer_name,\n            # TODO: This likely needs fixing now\n            # Before refactor: cmds.sets(layer, q=True) or [\"*\"]\n            \"setMembers\": [\"*\"],\n            \"review\": False,\n            \"publish\": True,\n            \"handleStart\": handle_start,\n            \"handleEnd\": handle_end,\n            \"frameStart\": frame_start,\n            \"frameEnd\": frame_end,\n            \"frameStartHandle\": frame_start_handle,\n            \"frameEndHandle\": frame_end_handle,\n            \"byFrameStep\": int(\n                self.get_render_attribute(\"byFrameStep\",\n                                          layer=layer_name)),\n            \"renderer\": renderer,\n            # instance subset\n            \"family\": \"vrayscene_layer\",\n            \"families\": [\"vrayscene_layer\"],\n            \"time\": get_formatted_current_time(),\n            \"author\": context.data[\"user\"],\n            # Add source to allow tracing back to the scene from\n            # which was submitted originally\n            \"source\": context.data[\"currentFile\"].replace(\"\\\\\", \"/\"),\n            \"resolutionWidth\": lib.get_attr_in_layer(\n                \"defaultResolution.height\", layer=layer_name\n            ),\n            \"resolutionHeight\": lib.get_attr_in_layer(\n                \"defaultResolution.width\", layer=layer_name\n            ),\n            \"pixelAspect\": lib.get_attr_in_layer(\n                \"defaultResolution.pixelAspect\", layer=layer_name\n            ),\n            \"priority\": instance.data.get(\"priority\"),\n            \"useMultipleSceneFiles\": instance.data.get(\n                \"vraySceneMultipleFiles\")\n        }\n\n        instance.data.update(data)\n\n        # Define nice label\n        label = \"{0} ({1})\".format(layer_name, instance.data[\"asset\"])\n        label += \"  [{0}-{1}]\".format(\n            int(data[\"frameStartHandle\"]), int(data[\"frameEndHandle\"])\n        )\n        instance.data[\"label\"] = label\n\n    def get_render_attribute(self, attr, layer):\n        \"\"\"Get attribute from render options.\n\n        Args:\n            attr (str): name of attribute to be looked up.\n\n        Returns:\n            Attribute value\n\n        \"\"\"\n        return lib.get_attr_in_layer(\n            \"defaultRenderGlobals.{}\".format(attr), layer=layer\n        )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_workfile.py",
    "content": "import os\nimport pyblish.api\n\n\nclass CollectWorkfileData(pyblish.api.InstancePlugin):\n    \"\"\"Inject data into Workfile instance\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.01\n    label = \"Maya Workfile\"\n    hosts = ['maya']\n    families = [\"workfile\"]\n\n    def process(self, instance):\n        \"\"\"Inject the current working file\"\"\"\n\n        context = instance.context\n        current_file = instance.context.data['currentFile']\n        folder, file = os.path.split(current_file)\n        filename, ext = os.path.splitext(file)\n\n        data = {  # noqa\n            \"setMembers\": [current_file],\n            \"frameStart\": context.data['frameStart'],\n            \"frameEnd\": context.data['frameEnd'],\n            \"handleStart\": context.data['handleStart'],\n            \"handleEnd\": context.data['handleEnd']\n        }\n\n        data['representations'] = [{\n            'name': ext.lstrip(\".\"),\n            'ext': ext.lstrip(\".\"),\n            'files': file,\n            \"stagingDir\": folder,\n        }]\n\n        instance.data.update(data)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_workscene_fps.py",
    "content": "import pyblish.api\nfrom maya import mel\n\n\nclass CollectWorksceneFPS(pyblish.api.ContextPlugin):\n    \"\"\"Get the FPS of the work scene\"\"\"\n\n    label = \"Workscene FPS\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"maya\"]\n\n    def process(self, context):\n        fps = mel.eval('currentTimeUnitToFPS()')\n        self.log.info(\"Workscene FPS: %s\" % fps)\n        context.data.update({\"fps\": fps})\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_xgen.py",
    "content": "import os\n\nfrom maya import cmds\n\nimport pyblish.api\nfrom openpype.hosts.maya.api.lib import get_attribute_input\n\n\nclass CollectXgen(pyblish.api.InstancePlugin):\n    \"\"\"Collect Xgen\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.499999\n    label = \"Collect Xgen\"\n    families = [\"xgen\"]\n\n    def process(self, instance):\n        data = {\n            \"xgmPalettes\": cmds.ls(instance, type=\"xgmPalette\", long=True),\n            \"xgmDescriptions\": cmds.ls(\n                instance, type=\"xgmDescription\", long=True\n            ),\n            \"xgmSubdPatches\": cmds.ls(instance, type=\"xgmSubdPatch\", long=True)\n        }\n        data[\"xgenNodes\"] = (\n            data[\"xgmPalettes\"] +\n            data[\"xgmDescriptions\"] +\n            data[\"xgmSubdPatches\"]\n        )\n\n        if data[\"xgmPalettes\"]:\n            data[\"xgmPalette\"] = data[\"xgmPalettes\"][0]\n\n        data[\"xgenConnections\"] = set()\n        for node in data[\"xgmSubdPatches\"]:\n            connected_transform = get_attribute_input(\n                node + \".transform\"\n            ).split(\".\")[0]\n            data[\"xgenConnections\"].add(connected_transform)\n\n        # Collect all files under palette root as resources.\n        import xgenm\n\n        data_path = xgenm.getAttr(\n            \"xgDataPath\", data[\"xgmPalette\"].replace(\"|\", \"\")\n        ).split(os.pathsep)[0]\n        data_path = data_path.replace(\n            \"${PROJECT}\",\n            xgenm.getAttr(\"xgProjectPath\", data[\"xgmPalette\"].replace(\"|\", \"\"))\n        )\n        transfers = []\n\n        # Since we are duplicating this palette when extracting we predict that\n        # the name will be the basename without namespaces.\n        predicted_palette_name = data[\"xgmPalette\"].split(\":\")[-1]\n        predicted_palette_name = predicted_palette_name.replace(\"|\", \"\")\n\n        for root, _, files in os.walk(data_path):\n            for file in files:\n                source = os.path.join(root, file).replace(\"\\\\\", \"/\")\n                destination = os.path.join(\n                    instance.data[\"resourcesDir\"],\n                    \"collections\",\n                    predicted_palette_name,\n                    source.replace(data_path, \"\")[1:]\n                )\n                transfers.append((source, destination.replace(\"\\\\\", \"/\")))\n\n        data[\"transfers\"] = transfers\n\n        self.log.debug(data)\n        instance.data.update(data)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_yeti_cache.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\nfrom openpype.hosts.maya.api import lib\n\n\nSETTINGS = {\n    # Preview\n    \"displayOutput\",\n    \"colorR\", \"colorG\", \"colorB\",\n    \"viewportDensity\",\n    \"viewportWidth\",\n    \"viewportLength\",\n    # Render attributes\n    \"renderDensity\",\n    \"renderWidth\",\n    \"renderLength\",\n    \"increaseRenderBounds\",\n    \"imageSearchPath\",\n    # Pipeline specific\n    \"cbId\"\n}\n\n\nclass CollectYetiCache(pyblish.api.InstancePlugin):\n    \"\"\"Collect all information of the Yeti caches\n\n    The information contains the following attributes per Yeti node\n\n    - \"renderDensity\"\n    - \"renderWidth\"\n    - \"renderLength\"\n    - \"increaseRenderBounds\"\n    - \"imageSearchPath\"\n\n    Other information is the name of the transform and it's Colorbleed ID\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.45\n    label = \"Collect Yeti Cache\"\n    families = [\"yetiRig\", \"yeticache\", \"yeticacheUE\"]\n    hosts = [\"maya\"]\n\n    def process(self, instance):\n\n        # Collect fur settings\n        settings = {\"nodes\": []}\n\n        # Get yeti nodes and their transforms\n        yeti_shapes = cmds.ls(instance, type=\"pgYetiMaya\")\n        for shape in yeti_shapes:\n\n            # Get specific node attributes\n            attr_data = {}\n            for attr in SETTINGS:\n                current = cmds.getAttr(\"%s.%s\" % (shape, attr))\n                # change None to empty string as Maya doesn't support\n                # NoneType in attributes\n                if current is None:\n                    current = \"\"\n                attr_data[attr] = current\n\n            # Get transform data\n            parent = cmds.listRelatives(shape, parent=True)[0]\n            transform_data = {\"name\": parent, \"cbId\": lib.get_id(parent)}\n\n            shape_data = {\n                \"transform\": transform_data,\n                \"name\": shape,\n                \"cbId\": lib.get_id(shape),\n                \"attrs\": attr_data,\n            }\n\n            settings[\"nodes\"].append(shape_data)\n\n        instance.data[\"fursettings\"] = settings\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/collect_yeti_rig.py",
    "content": "import os\nimport re\n\nfrom maya import cmds\n\nimport pyblish.api\n\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import KnownPublishError\n\n\nSETTINGS = {\"renderDensity\",\n            \"renderWidth\",\n            \"renderLength\",\n            \"increaseRenderBounds\",\n            \"imageSearchPath\",\n            \"cbId\"}\n\n\nclass CollectYetiRig(pyblish.api.InstancePlugin):\n    \"\"\"Collect all information of the Yeti Rig\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.4\n    label = \"Collect Yeti Rig\"\n    families = [\"yetiRig\"]\n    hosts = [\"maya\"]\n\n    def process(self, instance):\n\n        assert \"input_SET\" in instance.data[\"setMembers\"], (\n            \"Yeti Rig must have an input_SET\")\n\n        input_connections = self.collect_input_connections(instance)\n\n        # Collect any textures if used\n        yeti_resources = []\n        yeti_nodes = cmds.ls(instance[:], type=\"pgYetiMaya\", long=True)\n        for node in yeti_nodes:\n            # Get Yeti resources (textures)\n            resources = self.get_yeti_resources(node)\n            yeti_resources.extend(resources)\n\n        instance.data[\"rigsettings\"] = {\"inputs\": input_connections}\n\n        instance.data[\"resources\"] = yeti_resources\n\n        # Force frame range for yeti cache export for the rig\n        start = cmds.playbackOptions(query=True, animationStartTime=True)\n        for key in [\"frameStart\", \"frameEnd\",\n                    \"frameStartHandle\", \"frameEndHandle\"]:\n            instance.data[key] = start\n        instance.data[\"preroll\"] = 0\n\n    def collect_input_connections(self, instance):\n        \"\"\"Collect the inputs for all nodes in the input_SET\"\"\"\n\n        # Get the input meshes information\n        input_content = cmds.ls(cmds.sets(\"input_SET\", query=True), long=True)\n\n        # Include children\n        input_content += cmds.listRelatives(input_content,\n                                            allDescendents=True,\n                                            fullPath=True) or []\n\n        # Ignore intermediate objects\n        input_content = cmds.ls(input_content, long=True, noIntermediate=True)\n        if not input_content:\n            return []\n\n        # Store all connections\n        connections = cmds.listConnections(input_content,\n                                           source=True,\n                                           destination=False,\n                                           connections=True,\n                                           # Only allow inputs from dagNodes\n                                           # (avoid display layers, etc.)\n                                           type=\"dagNode\",\n                                           plugs=True) or []\n        connections = cmds.ls(connections, long=True)      # Ensure long names\n\n        inputs = []\n        for dest, src in lib.pairwise(connections):\n            source_node, source_attr = src.split(\".\", 1)\n            dest_node, dest_attr = dest.split(\".\", 1)\n\n            # Ensure the source of the connection is not included in the\n            # current instance's hierarchy. If so, we ignore that connection\n            # as we will want to preserve it even over a publish.\n            if source_node in instance:\n                self.log.debug(\"Ignoring input connection between nodes \"\n                               \"inside the instance: %s -> %s\" % (src, dest))\n                continue\n\n            inputs.append({\"connections\": [source_attr, dest_attr],\n                           \"sourceID\": lib.get_id(source_node),\n                           \"destinationID\": lib.get_id(dest_node)})\n\n        return inputs\n\n    def get_yeti_resources(self, node):\n        \"\"\"Get all resource file paths\n\n        If a texture is a sequence it gathers all sibling files to ensure\n        the texture sequence is complete.\n\n        References can be used in the Yeti graph, this means that it is\n        possible to load previously caches files. The information will need\n        to be stored and, if the file not publish, copied to the resource\n        folder.\n\n        Args:\n            node (str): node name of the pgYetiMaya node\n\n        Returns:\n            list\n        \"\"\"\n        resources = []\n\n        image_search_paths = cmds.getAttr(\"{}.imageSearchPath\".format(node))\n        if image_search_paths:\n\n            # TODO: Somehow this uses OS environment path separator, `:` vs `;`\n            # Later on check whether this is pipeline OS cross-compatible.\n            image_search_paths = [p for p in\n                                  image_search_paths.split(os.path.pathsep) if p]\n\n            # find all ${TOKEN} tokens and replace them with $TOKEN env. variable\n            image_search_paths = self._replace_tokens(image_search_paths)\n\n        # List all related textures\n        texture_nodes = cmds.pgYetiGraph(\n            node, listNodes=True, type=\"texture\")\n        texture_filenames = [\n            cmds.pgYetiGraph(\n                node, node=texture_node,\n                param=\"file_name\", getParamValue=True)\n            for texture_node in texture_nodes\n        ]\n        self.log.debug(\"Found %i texture(s)\" % len(texture_filenames))\n\n        # Get all reference nodes\n        reference_nodes = cmds.pgYetiGraph(node,\n                                           listNodes=True,\n                                           type=\"reference\")\n        self.log.debug(\"Found %i reference node(s)\" % len(reference_nodes))\n\n        # Collect all texture files\n        # find all ${TOKEN} tokens and replace them with $TOKEN env. variable\n        texture_filenames = self._replace_tokens(texture_filenames)\n        for texture in texture_filenames:\n\n            files = []\n            if os.path.isabs(texture):\n                self.log.debug(\"Texture is absolute path, ignoring \"\n                               \"image search paths for: %s\" % texture)\n                files = self.search_textures(texture)\n            else:\n                for root in image_search_paths:\n                    filepath = os.path.join(root, texture)\n                    files = self.search_textures(filepath)\n                    if files:\n                        # Break out on first match in search paths..\n                        break\n\n            if not files:\n                raise KnownPublishError(\n                    \"No texture found for: %s \"\n                    \"(searched: %s)\" % (texture, image_search_paths))\n\n            item = {\n                \"files\": files,\n                \"source\": texture,\n                \"node\": node\n            }\n\n            resources.append(item)\n\n        # For now validate that every texture has at least a single file\n        # resolved. Since a 'resource' does not have the requirement of having\n        # a `files` explicitly mapped it's not explicitly validated.\n        # TODO: Validate this as a validator\n        invalid_resources = []\n        for resource in resources:\n            if not resource['files']:\n                invalid_resources.append(resource)\n        if invalid_resources:\n            raise RuntimeError(\"Invalid resources\")\n\n        # Collect all referenced files\n        for reference_node in reference_nodes:\n            ref_file = cmds.pgYetiGraph(node,\n                                        node=reference_node,\n                                        param=\"reference_file\",\n                                        getParamValue=True)\n\n            # Create resource dict\n            item = {\n                \"source\": ref_file,\n                \"node\": node,\n                \"graphnode\": reference_node,\n                \"param\": \"reference_file\",\n                \"files\": []\n            }\n\n            ref_file_name = os.path.basename(ref_file)\n            if \"%04d\" in ref_file_name:\n                item[\"files\"] = self.get_sequence(ref_file)\n            else:\n                if os.path.exists(ref_file) and os.path.isfile(ref_file):\n                    item[\"files\"] = [ref_file]\n\n            if not item[\"files\"]:\n                self.log.warning(\"Reference node '%s' has no valid file \"\n                                 \"path set: %s\" % (reference_node, ref_file))\n                # TODO: This should allow to pass and fail in Validator instead\n                raise RuntimeError(\"Reference node  must be a full file path!\")\n\n            resources.append(item)\n\n        return resources\n\n    def search_textures(self, filepath):\n        \"\"\"Search all texture files on disk.\n\n        This also parses to full sequences for those with dynamic patterns\n        like <UDIM> and %04d in the filename.\n\n        Args:\n            filepath (str): The full path to the file, including any\n                dynamic patterns like <UDIM> or %04d\n\n        Returns:\n            list: The files found on disk\n\n        \"\"\"\n        filename = os.path.basename(filepath)\n\n        # Collect full sequence if it matches a sequence pattern\n        if len(filename.split(\".\")) > 2:\n\n            # For UDIM based textures (tiles)\n            if \"<UDIM>\" in filename:\n                sequences = self.get_sequence(filepath,\n                                              pattern=\"<UDIM>\")\n                if sequences:\n                    return sequences\n\n            # Frame/time - Based textures (animated masks f.e)\n            elif \"%04d\" in filename:\n                sequences = self.get_sequence(filepath,\n                                              pattern=\"%04d\")\n                if sequences:\n                    return sequences\n\n        # Assuming it is a fixed name (single file)\n        if os.path.exists(filepath):\n            return [filepath]\n\n        return []\n\n    def get_sequence(self, filepath, pattern=\"%04d\"):\n        \"\"\"Get sequence from filename.\n\n        This will only return files if they exist on disk as it tries\n        to collect the sequence using the filename pattern and searching\n        for them on disk.\n\n        Supports negative frame ranges like -001, 0000, 0001 and -0001,\n        0000, 0001.\n\n        Arguments:\n            filepath (str): The full path to filename containing the given\n            pattern.\n            pattern (str): The pattern to swap with the variable frame number.\n\n        Returns:\n            list: file sequence.\n\n        \"\"\"\n        import clique\n\n        escaped = re.escape(filepath)\n        re_pattern = escaped.replace(pattern, \"-?[0-9]+\")\n\n        source_dir = os.path.dirname(filepath)\n        files = [f for f in os.listdir(source_dir)\n                 if re.match(re_pattern, f)]\n\n        pattern = [clique.PATTERNS[\"frames\"]]\n        collection, remainder = clique.assemble(files, patterns=pattern)\n\n        return collection\n\n    def _replace_tokens(self, strings):\n        env_re = re.compile(r\"\\$\\{(\\w+)\\}\")\n\n        replaced = []\n        for s in strings:\n            matches = re.finditer(env_re, s)\n            for m in matches:\n                try:\n                    s = s.replace(m.group(), os.environ[m.group(1)])\n                except KeyError:\n                    msg = \"Cannot find requested {} in environment\".format(\n                        m.group(1))\n                    self.log.error(msg)\n                    raise RuntimeError(msg)\n            replaced.append(s)\n        return replaced\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/determine_future_version.py",
    "content": "import pyblish\n\nclass DetermineFutureVersion(pyblish.api.InstancePlugin):\n    \"\"\"\n    This will determine version of subset if we want render to be attached to.\n    \"\"\"\n    label = \"Determine Subset Version\"\n    order = pyblish.api.IntegratorOrder\n    hosts = [\"maya\"]\n    families = [\"renderlayer\"]\n\n    def process(self, instance):\n        context = instance.context\n        attach_to_subsets = [s[\"subset\"] for s in instance.data['attachTo']]\n\n        if not attach_to_subsets:\n            return\n\n        for i in context:\n            if i.data[\"subset\"] in attach_to_subsets:\n                # # this will get corresponding subset in attachTo list\n                # # so we can set version there\n                sub = next(item for item in instance.data['attachTo'] if item[\"subset\"] == i.data[\"subset\"])  # noqa: E501\n\n                sub[\"version\"] = i.data.get(\"version\", 1)\n                self.log.info(\"render will be attached to {} v{}\".format(\n                        sub[\"subset\"], sub[\"version\"]\n                ))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py",
    "content": "import maya.api.OpenMaya as om\nimport maya.api.OpenMayaUI as omui\n\nimport pyblish.api\nimport tempfile\n\nfrom openpype.hosts.maya.api.lib import IS_HEADLESS\n\n\nclass ExtractActiveViewThumbnail(pyblish.api.InstancePlugin):\n    \"\"\"Set instance thumbnail to a screengrab of current active viewport.\n\n    This makes it so that if an instance does not have a thumbnail set yet that\n    it will get a thumbnail of the currently active view at the time of\n    publishing as a fallback.\n\n    \"\"\"\n    order = pyblish.api.ExtractorOrder + 0.49\n    label = \"Active View Thumbnail\"\n    families = [\"workfile\"]\n    hosts = [\"maya\"]\n\n    def process(self, instance):\n        if IS_HEADLESS:\n            self.log.debug(\n                \"Skip extraction of active view thumbnail, due to being in\"\n                \"headless mode.\"\n            )\n            return\n\n        thumbnail = instance.data.get(\"thumbnailPath\")\n        if not thumbnail:\n            view_thumbnail = self.get_view_thumbnail(instance)\n            if not view_thumbnail:\n                return\n\n            self.log.debug(\"Setting instance thumbnail path to: {}\".format(\n                view_thumbnail\n            ))\n            instance.data[\"thumbnailPath\"] = view_thumbnail\n\n    def get_view_thumbnail(self, instance):\n        cache_key = \"__maya_view_thumbnail\"\n        context = instance.context\n\n        if cache_key not in context.data:\n            # Generate only a single thumbnail, even for multiple instances\n            with tempfile.NamedTemporaryFile(suffix=\"_thumbnail.jpg\",\n                                             delete=False) as f:\n                path = f.name\n\n            view = omui.M3dView.active3dView()\n            image = om.MImage()\n            view.readColorBuffer(image, True)\n            image.writeToFile(path, \"jpg\")\n            self.log.debug(\"Generated thumbnail: {}\".format(path))\n\n            context.data[\"cleanupFullPaths\"].append(path)\n            context.data[cache_key] = path\n        return context.data[cache_key]\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py",
    "content": "import os\nfrom collections import defaultdict\nimport json\n\nfrom maya import cmds\nimport arnold\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api import lib\n\n\nclass ExtractArnoldSceneSource(publish.Extractor):\n    \"\"\"Extract the content of the instance to an Arnold Scene Source file.\"\"\"\n\n    label = \"Extract Arnold Scene Source\"\n    hosts = [\"maya\"]\n    families = [\"ass\"]\n    asciiAss = True\n\n    def _pre_process(self, instance, staging_dir):\n        file_path = os.path.join(staging_dir, \"{}.ass\".format(instance.name))\n\n        # Mask\n        mask = arnold.AI_NODE_ALL\n\n        node_types = {\n            \"options\": arnold.AI_NODE_OPTIONS,\n            \"camera\": arnold.AI_NODE_CAMERA,\n            \"light\": arnold.AI_NODE_LIGHT,\n            \"shape\": arnold.AI_NODE_SHAPE,\n            \"shader\": arnold.AI_NODE_SHADER,\n            \"override\": arnold.AI_NODE_OVERRIDE,\n            \"driver\": arnold.AI_NODE_DRIVER,\n            \"filter\": arnold.AI_NODE_FILTER,\n            \"color_manager\": arnold.AI_NODE_COLOR_MANAGER,\n            \"operator\": arnold.AI_NODE_OPERATOR\n        }\n\n        for key in node_types.keys():\n            if instance.data.get(\"mask\" + key.title()):\n                mask = mask ^ node_types[key]\n\n        # Motion blur\n        attribute_data = {\n            \"defaultArnoldRenderOptions.motion_blur_enable\": instance.data.get(\n                \"motionBlur\", True\n            ),\n            \"defaultArnoldRenderOptions.motion_steps\": instance.data.get(\n                \"motionBlurKeys\", 2\n            ),\n            \"defaultArnoldRenderOptions.motion_frames\": instance.data.get(\n                \"motionBlurLength\", 0.5\n            )\n        }\n\n        # Write out .ass file\n        kwargs = {\n            \"filename\": file_path,\n            \"startFrame\": instance.data.get(\"frameStartHandle\", 1),\n            \"endFrame\": instance.data.get(\"frameEndHandle\", 1),\n            \"frameStep\": instance.data.get(\"step\", 1),\n            \"selected\": True,\n            \"asciiAss\": self.asciiAss,\n            \"shadowLinks\": True,\n            \"lightLinks\": True,\n            \"boundingBox\": True,\n            \"expandProcedurals\": instance.data.get(\"expandProcedurals\", False),\n            \"camera\": instance.data[\"camera\"],\n            \"mask\": mask\n        }\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        return attribute_data, kwargs\n\n    def process(self, instance):\n        staging_dir = self.staging_dir(instance)\n        attribute_data, kwargs = self._pre_process(instance, staging_dir)\n\n        filenames = self._extract(\n            instance.data[\"members\"], attribute_data, kwargs\n        )\n\n        self._post_process(\n            instance, filenames, staging_dir, kwargs[\"startFrame\"]\n        )\n\n    def _post_process(self, instance, filenames, staging_dir, frame_start):\n        nodes_by_id = self._nodes_by_id(instance[:])\n        representation = {\n            \"name\": \"ass\",\n            \"ext\": \"ass\",\n            \"files\": filenames if len(filenames) > 1 else filenames[0],\n            \"stagingDir\": staging_dir,\n            \"frameStart\": frame_start\n        }\n\n        instance.data[\"representations\"].append(representation)\n\n        json_path = os.path.join(\n            staging_dir, \"{}.json\".format(instance.name)\n        )\n        with open(json_path, \"w\") as f:\n            json.dump(nodes_by_id, f)\n\n        representation = {\n            \"name\": \"json\",\n            \"ext\": \"json\",\n            \"files\": os.path.basename(json_path),\n            \"stagingDir\": staging_dir\n        }\n\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\n            \"Extracted instance {} to: {}\".format(instance.name, staging_dir)\n        )\n\n    def _nodes_by_id(self, nodes):\n        nodes_by_id = defaultdict(list)\n\n        for node in nodes:\n            id = lib.get_id(node)\n\n            if id is None:\n                continue\n\n            # Converting Maya hierarchy separator \"|\" to Arnold separator \"/\".\n            nodes_by_id[id].append(node.replace(\"|\", \"/\"))\n\n        return nodes_by_id\n\n    def _extract(self, nodes, attribute_data, kwargs):\n        filenames = []\n        with lib.attribute_values(attribute_data):\n            with lib.maintained_selection():\n                self.log.debug(\n                    \"Writing: {}\".format(nodes)\n                )\n                cmds.select(nodes, noExpand=True)\n\n                self.log.debug(\n                    \"Extracting ass sequence with: {}\".format(kwargs)\n                )\n\n                exported_files = cmds.arnoldExportAss(**kwargs)\n\n                for file in exported_files:\n                    filenames.append(os.path.split(file)[1])\n\n                self.log.debug(\"Exported: {}\".format(filenames))\n\n        return filenames\n\n\nclass ExtractArnoldSceneSourceProxy(ExtractArnoldSceneSource):\n    \"\"\"Extract the content of the instance to an Arnold Scene Source file.\"\"\"\n\n    label = \"Extract Arnold Scene Source Proxy\"\n    hosts = [\"maya\"]\n    families = [\"assProxy\"]\n    asciiAss = True\n\n    def process(self, instance):\n        staging_dir = self.staging_dir(instance)\n        attribute_data, kwargs = self._pre_process(instance, staging_dir)\n\n        filenames, _ = self._duplicate_extract(\n            instance.data[\"members\"], attribute_data, kwargs\n        )\n\n        self._post_process(\n            instance, filenames, staging_dir, kwargs[\"startFrame\"]\n        )\n\n        kwargs[\"filename\"] = os.path.join(\n            staging_dir, \"{}_proxy.ass\".format(instance.name)\n        )\n\n        filenames, _ = self._duplicate_extract(\n            instance.data[\"proxy\"], attribute_data, kwargs\n        )\n\n        representation = {\n            \"name\": \"proxy\",\n            \"ext\": \"ass\",\n            \"files\": filenames if len(filenames) > 1 else filenames[0],\n            \"stagingDir\": staging_dir,\n            \"frameStart\": kwargs[\"startFrame\"],\n            \"outputName\": \"proxy\"\n        }\n\n        instance.data[\"representations\"].append(representation)\n\n    def _duplicate_extract(self, nodes, attribute_data, kwargs):\n        self.log.debug(\n            \"Writing {} with:\\n{}\".format(kwargs[\"filename\"], kwargs)\n        )\n        filenames = []\n        # Duplicating nodes so they are direct children of the world. This\n        # makes the hierarchy of any exported ass file the same.\n        with lib.delete_after() as delete_bin:\n            duplicate_nodes = []\n            for node in nodes:\n                # Only interested in transforms:\n                if cmds.nodeType(node) != \"transform\":\n                    continue\n\n                # Only interested in transforms with shapes.\n                shapes = cmds.listRelatives(\n                    node, shapes=True, noIntermediate=True\n                )\n                if not shapes:\n                    continue\n\n                basename = cmds.duplicate(node)[0]\n                parents = cmds.ls(node, long=True)[0].split(\"|\")[:-1]\n                duplicate_transform = \"|\".join(parents + [basename])\n\n                if cmds.listRelatives(duplicate_transform, parent=True):\n                    duplicate_transform = cmds.parent(\n                        duplicate_transform, world=True\n                    )[0]\n\n                basename = node.rsplit(\"|\", 1)[-1].rsplit(\":\", 1)[-1]\n                duplicate_transform = cmds.rename(\n                    duplicate_transform, basename\n                )\n\n                # Discard children nodes that are not shapes\n                shapes = cmds.listRelatives(\n                    duplicate_transform, shapes=True, fullPath=True\n                )\n                children = cmds.listRelatives(\n                    duplicate_transform, children=True, fullPath=True\n                )\n                cmds.delete(set(children) - set(shapes))\n\n                duplicate_nodes.append(duplicate_transform)\n                duplicate_nodes.extend(shapes)\n                delete_bin.append(duplicate_transform)\n\n            nodes_by_id = self._nodes_by_id(duplicate_nodes)\n            filenames = self._extract(duplicate_nodes, attribute_data, kwargs)\n\n        return filenames, nodes_by_id\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_assembly.py",
    "content": "import os\nimport json\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.lib import extract_alembic\n\nfrom maya import cmds\n\n\nclass ExtractAssembly(publish.Extractor):\n    \"\"\"Produce an alembic of just point positions and normals.\n\n    Positions and normals are preserved, but nothing more,\n    for plain and predictable point caches.\n\n    \"\"\"\n\n    label = \"Extract Assembly\"\n    hosts = [\"maya\"]\n    families = [\"assembly\"]\n\n    def process(self, instance):\n\n        staging_dir = self.staging_dir(instance)\n        hierarchy_filename = \"{}.abc\".format(instance.name)\n        hierarchy_path = os.path.join(staging_dir, hierarchy_filename)\n        json_filename = \"{}.json\".format(instance.name)\n        json_path = os.path.join(staging_dir, json_filename)\n\n        self.log.debug(\"Dumping scene data for debugging ..\")\n        with open(json_path, \"w\") as filepath:\n            json.dump(instance.data[\"scenedata\"], filepath, ensure_ascii=False)\n\n        self.log.debug(\"Extracting pointcache ..\")\n        cmds.select(instance.data[\"nodesHierarchy\"])\n\n        # Run basic alembic exporter\n        extract_alembic(file=hierarchy_path,\n                        startFrame=1.0,\n                        endFrame=1.0,\n                        **{\"step\": 1.0,\n                           \"attr\": [\"cbId\"],\n                           \"writeVisibility\": True,\n                           \"writeCreases\": True,\n                           \"uvWrite\": True,\n                           \"selection\": True})\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation_abc = {\n            'name': 'abc',\n            'ext': 'abc',\n            'files': hierarchy_filename,\n            \"stagingDir\": staging_dir\n        }\n        instance.data[\"representations\"].append(representation_abc)\n\n        representation_json = {\n            'name': 'json',\n            'ext': 'json',\n            'files': json_filename,\n            \"stagingDir\": staging_dir\n        }\n        instance.data[\"representations\"].append(representation_json)\n        # Remove data\n        instance.data.pop(\"scenedata\", None)\n\n        cmds.select(clear=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_camera_alembic.py",
    "content": "import os\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api import lib\n\n\nclass ExtractCameraAlembic(publish.Extractor,\n                           publish.OptionalPyblishPluginMixin):\n    \"\"\"Extract a Camera as Alembic.\n\n    The camera gets baked to world space by default. Only when the instance's\n    `bakeToWorldSpace` is set to False it will include its full hierarchy.\n\n    'camera' family expects only single camera, if multiple cameras are needed,\n    'matchmove' is better choice.\n\n    \"\"\"\n\n    label = \"Extract Camera (Alembic)\"\n    hosts = [\"maya\"]\n    families = [\"camera\", \"matchmove\"]\n    bake_attributes = []\n\n    def process(self, instance):\n\n        # Collect the start and end including handles\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n\n        step = instance.data.get(\"step\", 1.0)\n        bake_to_worldspace = instance.data(\"bakeToWorldSpace\", True)\n\n        # get cameras\n        members = instance.data['setMembers']\n        cameras = cmds.ls(members, leaf=True, long=True,\n                          dag=True, type=\"camera\")\n\n        # validate required settings\n        assert isinstance(step, float), \"Step must be a float value\"\n\n        # Define extract output file path\n        dir_path = self.staging_dir(instance)\n        if not os.path.exists(dir_path):\n            os.makedirs(dir_path)\n        filename = \"{0}.abc\".format(instance.name)\n        path = os.path.join(dir_path, filename)\n\n        # Perform alembic extraction\n        member_shapes = cmds.ls(\n            members, leaf=True, shapes=True, long=True, dag=True)\n        with lib.maintained_selection():\n            cmds.select(\n                member_shapes,\n                replace=True, noExpand=True)\n\n            # Enforce forward slashes for AbcExport because we're\n            # embedding it into a job string\n            path = path.replace(\"\\\\\", \"/\")\n\n            job_str = ' -selection -dataFormat \"ogawa\" '\n            job_str += ' -attrPrefix cb'\n            job_str += ' -frameRange {0} {1} '.format(start, end)\n            job_str += ' -step {0} '.format(step)\n\n            if bake_to_worldspace:\n                job_str += ' -worldSpace'\n\n                # if baked, drop the camera hierarchy to maintain\n                # clean output and backwards compatibility\n                camera_roots = cmds.listRelatives(\n                    cameras, parent=True, fullPath=True)\n                for camera_root in camera_roots:\n                    job_str += ' -root {0}'.format(camera_root)\n\n                for member in members:\n                    descendants = cmds.listRelatives(member,\n                                                     allDescendents=True,\n                                                     fullPath=True) or []\n                    shapes = cmds.ls(descendants, shapes=True,\n                                     noIntermediate=True, long=True)\n                    cameras = cmds.ls(shapes, type=\"camera\", long=True)\n                    if cameras:\n                        if not set(shapes) - set(cameras):\n                            continue\n                        self.log.warning((\n                            \"Camera hierarchy contains additional geometry. \"\n                            \"Extraction will fail.\")\n                        )\n                    transform = cmds.listRelatives(\n                        member, parent=True, fullPath=True)\n                    transform = transform[0] if transform else member\n                    job_str += ' -root {0}'.format(transform)\n\n            job_str += ' -file \"{0}\"'.format(path)\n\n            # bake specified attributes in preset\n            assert isinstance(self.bake_attributes, (list, tuple)), (\n                \"Attributes to bake must be specified as a list\"\n            )\n            for attr in self.bake_attributes:\n                self.log.debug(\"Adding {} attribute\".format(attr))\n                job_str += \" -attr {0}\".format(attr)\n\n            with lib.evaluation(\"off\"):\n                with lib.suspended_refresh():\n                    cmds.AbcExport(j=job_str, verbose=False)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'abc',\n            'ext': 'abc',\n            'files': filename,\n            \"stagingDir\": dir_path,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '{0}' to: {1}\".format(\n            instance.name, path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract camera as Maya Scene.\"\"\"\nimport os\nimport itertools\nimport contextlib\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api import lib\nfrom openpype.lib import (\n    BoolDef\n)\n\n\ndef massage_ma_file(path):\n    \"\"\"Clean up .ma file for backwards compatibility.\n\n    Massage the .ma of baked camera to stay\n    backwards compatible with older versions\n    of Fusion (6.4)\n\n    \"\"\"\n    # Get open file's lines\n    f = open(path, \"r+\")\n    lines = f.readlines()\n    f.seek(0)  # reset to start of file\n\n    # Rewrite the file\n    for line in lines:\n        # Skip all 'rename -uid' lines\n        stripped = line.strip()\n        if stripped.startswith(\"rename -uid \"):\n            continue\n\n        f.write(line)\n\n    f.truncate()  # remove remainder\n    f.close()\n\n\ndef grouper(iterable, n, fillvalue=None):\n    \"\"\"Collect data into fixed-length chunks or blocks.\n\n    Examples:\n        grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx\n\n    \"\"\"\n    args = [iter(iterable)] * n\n    from six.moves import zip_longest\n    return zip_longest(fillvalue=fillvalue, *args)\n\n\ndef unlock(plug):\n    \"\"\"Unlocks attribute and disconnects inputs for a plug.\n\n    This will also recursively unlock the attribute\n    upwards to any parent attributes for compound\n    attributes, to ensure it's fully unlocked and free\n    to change the value.\n\n    \"\"\"\n    node, attr = plug.rsplit(\".\", 1)\n\n    # Unlock attribute\n    cmds.setAttr(plug, lock=False)\n\n    # Also unlock any parent attribute (if compound)\n    parents = cmds.attributeQuery(attr, node=node, listParent=True)\n    if parents:\n        for parent in parents:\n            unlock(\"{0}.{1}\".format(node, parent))\n\n    # Break incoming connections\n    connections = cmds.listConnections(plug,\n                                       source=True,\n                                       destination=False,\n                                       plugs=True,\n                                       connections=True)\n    if connections:\n        for destination, source in grouper(connections, 2):\n            cmds.disconnectAttr(source, destination)\n\n\nclass ExtractCameraMayaScene(publish.Extractor,\n                             publish.OptionalPyblishPluginMixin):\n    \"\"\"Extract a Camera as Maya Scene.\n\n    This will create a duplicate of the camera that will be baked *with*\n    substeps and handles for the required frames. This temporary duplicate\n    will be published.\n\n    The cameras gets baked to world space by default. Only when the instance's\n    `bakeToWorldSpace` is set to False it will include its full hierarchy.\n\n    'camera' family expects only single camera, if multiple cameras are needed,\n    'matchmove' is better choice.\n\n    Note:\n        The extracted Maya ascii file gets \"massaged\" removing the uuid values\n        so they are valid for older versions of Fusion (e.g. 6.4)\n\n    \"\"\"\n\n    label = \"Extract Camera (Maya Scene)\"\n    hosts = [\"maya\"]\n    families = [\"camera\", \"matchmove\"]\n    scene_type = \"ma\"\n\n    keep_image_planes = True\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        # get settings\n        ext_mapping = (\n            instance.context.data[\"project_settings\"][\"maya\"][\"ext_mapping\"]\n        )\n        if ext_mapping:\n            self.log.debug(\"Looking in settings for scene type ...\")\n            # use extension mapping for first family found\n            for family in self.families:\n                try:\n                    self.scene_type = ext_mapping[family]\n                    self.log.debug(\n                        \"Using {} as scene type\".format(self.scene_type))\n                    break\n                except KeyError:\n                    # no preset found\n                    pass\n\n        # Collect the start and end including handles\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n\n        step = instance.data.get(\"step\", 1.0)\n        bake_to_worldspace = instance.data(\"bakeToWorldSpace\", True)\n\n        if not bake_to_worldspace:\n            self.log.warning(\"Camera (Maya Scene) export only supports world\"\n                             \"space baked camera extractions. The disabled \"\n                             \"bake to world space is ignored...\")\n\n        # get cameras\n        members = set(cmds.ls(instance.data['setMembers'], leaf=True,\n                      shapes=True, long=True, dag=True))\n        cameras = set(cmds.ls(members, leaf=True, shapes=True, long=True,\n                      dag=True, type=\"camera\"))\n\n        # validate required settings\n        assert isinstance(step, float), \"Step must be a float value\"\n        transforms = cmds.listRelatives(list(cameras),\n                                        parent=True, fullPath=True)\n\n        # Define extract output file path\n        dir_path = self.staging_dir(instance)\n        filename = \"{0}.{1}\".format(instance.name, self.scene_type)\n        path = os.path.join(dir_path, filename)\n\n        # Perform extraction\n        with lib.maintained_selection():\n            with lib.evaluation(\"off\"):\n                with lib.suspended_refresh():\n                    if bake_to_worldspace:\n                        baked = lib.bake_to_world_space(\n                            transforms,\n                            frame_range=[start, end],\n                            step=step\n                        )\n                        baked_camera_shapes = set(cmds.ls(baked,\n                                                  type=\"camera\",\n                                                  dag=True,\n                                                  shapes=True,\n                                                  long=True))\n\n                        members.update(baked_camera_shapes)\n                        members.difference_update(cameras)\n                    else:\n                        baked_camera_shapes = cmds.ls(list(cameras),\n                                                      type=\"camera\",\n                                                      dag=True,\n                                                      shapes=True,\n                                                      long=True)\n\n                    attrs = {\"backgroundColorR\": 0.0,\n                             \"backgroundColorG\": 0.0,\n                             \"backgroundColorB\": 0.0,\n                             \"overscan\": 1.0}\n\n                    # Fix PLN-178: Don't allow background color to be non-black\n                    for cam, (attr, value) in itertools.product(cmds.ls(\n                            baked_camera_shapes, type=\"camera\", dag=True,\n                            long=True), attrs.items()):\n                        plug = \"{0}.{1}\".format(cam, attr)\n                        unlock(plug)\n                        cmds.setAttr(plug, value)\n\n                    attr_values = self.get_attr_values_from_data(\n                        instance.data)\n                    keep_image_planes = attr_values.get(\"keep_image_planes\")\n\n                    with transfer_image_planes(sorted(cameras),\n                                               sorted(baked_camera_shapes),\n                                               keep_image_planes):\n\n                        self.log.info(\"Performing extraction..\")\n                        cmds.select(cmds.ls(list(members), dag=True,\n                                            shapes=True, long=True),\n                                    noExpand=True)\n                        cmds.file(path,\n                                  force=True,\n                                  typ=\"mayaAscii\" if self.scene_type == \"ma\" else \"mayaBinary\",  # noqa: E501\n                                  exportSelected=True,\n                                  preserveReferences=False,\n                                  constructionHistory=False,\n                                  channels=True,  # allow animation\n                                  constraints=False,\n                                  shader=False,\n                                  expressions=False)\n\n                    # Delete the baked hierarchy\n                    if bake_to_worldspace:\n                        cmds.delete(baked)\n                    if self.scene_type == \"ma\":\n                        massage_ma_file(path)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': self.scene_type,\n            'ext': self.scene_type,\n            'files': filename,\n            \"stagingDir\": dir_path,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '{0}' to: {1}\".format(\n            instance.name, path))\n\n    @classmethod\n    def get_attribute_defs(cls):\n        defs = super(ExtractCameraMayaScene, cls).get_attribute_defs()\n\n        defs.extend([\n            BoolDef(\"keep_image_planes\",\n                    label=\"Keep Image Planes\",\n                    tooltip=\"Preserving connected image planes on camera\",\n                    default=cls.keep_image_planes),\n\n        ])\n\n        return defs\n\n\n@contextlib.contextmanager\ndef transfer_image_planes(source_cameras, target_cameras,\n                          keep_input_connections):\n    \"\"\"Reattaches image planes to baked or original cameras.\n\n    Baked cameras are duplicates of original ones.\n    This attaches it to duplicated camera properly and after\n    export it reattaches it back to original to keep image plane in workfile.\n    \"\"\"\n    originals = {}\n    try:\n        for source_camera, target_camera in zip(source_cameras,\n                                                target_cameras):\n            image_plane_plug = \"{}.imagePlane\".format(source_camera)\n            image_planes = cmds.listConnections(image_plane_plug,\n                                                source=True,\n                                                destination=False,\n                                                type=\"imagePlane\") or []\n\n            # Split of the parent path they are attached - we want\n            # the image plane node name if attached to a camera.\n            # TODO: Does this still mean the image plane name is unique?\n            image_planes = [x.split(\"->\", 1)[-1] for x in image_planes]\n\n            if not image_planes:\n                continue\n\n            originals[source_camera] = []\n            for image_plane in image_planes:\n                if keep_input_connections:\n                    if source_camera == target_camera:\n                        continue\n                    _attach_image_plane(target_camera, image_plane)\n                else:  # explicitly detach image planes\n                    cmds.imagePlane(image_plane, edit=True, detach=True)\n                originals[source_camera].append(image_plane)\n        yield\n    finally:\n        for camera, image_planes in originals.items():\n            for image_plane in image_planes:\n                _attach_image_plane(camera, image_plane)\n\n\ndef _attach_image_plane(camera, image_plane):\n    cmds.imagePlane(image_plane, edit=True, detach=True)\n    cmds.imagePlane(image_plane, edit=True, camera=camera)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_fbx.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\n\nfrom maya import cmds  # noqa\nimport maya.mel as mel  # noqa\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.lib import maintained_selection\nfrom openpype.hosts.maya.api import fbx\n\n\nclass ExtractFBX(publish.Extractor):\n    \"\"\"Extract FBX from Maya.\n\n    This extracts reproducible FBX exports ignoring any of the\n    settings set on the local machine in the FBX export options window.\n\n    \"\"\"\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract FBX\"\n    families = [\"fbx\"]\n\n    def process(self, instance):\n        fbx_exporter = fbx.FBXExtractor(log=self.log)\n\n        # Define output path\n        staging_dir = self.staging_dir(instance)\n        filename = \"{0}.fbx\".format(instance.name)\n        path = os.path.join(staging_dir, filename)\n\n        # The export requires forward slashes because we need\n        # to format it into a string in a mel expression\n        path = path.replace('\\\\', '/')\n\n        self.log.debug(\"Extracting FBX to: {0}\".format(path))\n\n        members = instance.data[\"setMembers\"]\n        self.log.debug(\"Members: {0}\".format(members))\n        self.log.debug(\"Instance: {0}\".format(instance[:]))\n\n        fbx_exporter.set_options_from_instance(instance)\n\n        # Export\n        with maintained_selection():\n            fbx_exporter.export(members, path)\n            cmds.select(members, r=1, noExpand=True)\n            mel.eval('FBXExport -f \"{}\" -s'.format(path))\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'fbx',\n            'ext': 'fbx',\n            'files': filename,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extract FBX successful to: {0}\".format(path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_fbx_animation.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\n\nfrom maya import cmds  # noqa\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api import fbx\nfrom openpype.hosts.maya.api.lib import (\n    namespaced, get_namespace, strip_namespace\n)\n\n\nclass ExtractFBXAnimation(publish.Extractor):\n    \"\"\"Extract Rig in FBX format from Maya.\n\n    This extracts the rig in fbx with the constraints\n    and referenced asset content included.\n    This also optionally extract animated rig in fbx with\n    geometries included.\n\n    \"\"\"\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract Animation (FBX)\"\n    hosts = [\"maya\"]\n    families = [\"animation.fbx\"]\n\n    def process(self, instance):\n        # Define output path\n        staging_dir = self.staging_dir(instance)\n        filename = \"{0}.fbx\".format(instance.name)\n        path = os.path.join(staging_dir, filename)\n        path = path.replace(\"\\\\\", \"/\")\n\n        fbx_exporter = fbx.FBXExtractor(log=self.log)\n        out_members = instance.data.get(\"animated_skeleton\", [])\n        # Export\n        instance.data[\"constraints\"] = True\n        instance.data[\"skeletonDefinitions\"] = True\n        instance.data[\"referencedAssetsContent\"] = True\n        fbx_exporter.set_options_from_instance(instance)\n        # Export from the rig's namespace so that the exported\n        # FBX does not include the namespace but preserves the node\n        # names as existing in the rig workfile\n        if not out_members:\n            skeleton_set = [\n                i for i in instance\n                if i.endswith(\"skeletonAnim_SET\")\n            ]\n            self.log.debug(\n                \"Top group of animated skeleton not found in \"\n                \"{}.\\nSkipping fbx animation extraction.\".format(skeleton_set))\n            return\n\n        namespace = get_namespace(out_members[0])\n        relative_out_members = [\n            strip_namespace(node, namespace) for node in out_members\n        ]\n        with namespaced(\n            \":\" + namespace,\n            new=False,\n            relative_names=True\n        ) as namespace:\n            fbx_exporter.export(relative_out_members, path)\n\n        representations = instance.data.setdefault(\"representations\", [])\n        representations.append({\n            'name': 'fbx',\n            'ext': 'fbx',\n            'files': filename,\n            \"stagingDir\": staging_dir\n        })\n\n        self.log.debug(\n            \"Extracted FBX animation to: {0}\".format(path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_gltf.py",
    "content": "import os\n\nfrom maya import cmds, mel\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api import lib\nfrom openpype.hosts.maya.api.gltf import extract_gltf\n\n\nclass ExtractGLB(publish.Extractor):\n\n    order = pyblish.api.ExtractorOrder\n    hosts = [\"maya\"]\n    label = \"Extract GLB\"\n    families = [\"gltf\"]\n\n    def process(self, instance):\n        staging_dir = self.staging_dir(instance)\n        filename = \"{0}.glb\".format(instance.name)\n        path = os.path.join(staging_dir, filename)\n\n        cmds.loadPlugin(\"maya2glTF\", quiet=True)\n\n        nodes = instance[:]\n\n        start_frame = instance.data('frameStart') or \\\n                      int(cmds.playbackOptions(query=True,\n                                               animationStartTime=True))# noqa\n        end_frame = instance.data('frameEnd') or \\\n                    int(cmds.playbackOptions(query=True,\n                                             animationEndTime=True)) # noqa\n        fps = mel.eval('currentTimeUnitToFPS()')\n\n        options = {\n            \"sno\": True,    # selectedNodeOnly\n            \"nbu\": True,    # .bin instead of .bin0\n            \"ast\": start_frame,\n            \"aet\": end_frame,\n            \"afr\": fps,\n            \"dsa\": 1,\n            \"acn\": instance.name,\n            \"glb\": True,\n            \"vno\": True    # visibleNodeOnly\n        }\n\n        self.log.debug(\"Extracting GLB to: {}\".format(path))\n        with lib.maintained_selection():\n            cmds.select(nodes, hi=True, noExpand=True)\n            extract_gltf(staging_dir,\n                         instance.name,\n                         **options)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'glb',\n            'ext': 'glb',\n            'files': filename,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extract GLB successful to: {0}\".format(path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_gpu_cache.py",
    "content": "import json\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\n\n\nclass ExtractGPUCache(publish.Extractor):\n    \"\"\"Extract the content of the instance to a GPU cache file.\"\"\"\n\n    label = \"GPU Cache\"\n    hosts = [\"maya\"]\n    families = [\"model\", \"animation\", \"pointcache\"]\n    step = 1.0\n    stepSave = 1\n    optimize = True\n    optimizationThreshold = 40000\n    optimizeAnimationsForMotionBlur = True\n    writeMaterials = True\n    useBaseTessellation = True\n\n    def process(self, instance):\n        cmds.loadPlugin(\"gpuCache\", quiet=True)\n\n        staging_dir = self.staging_dir(instance)\n        filename = \"{}_gpu_cache\".format(instance.name)\n\n        # Write out GPU cache file.\n        kwargs = {\n            \"directory\": staging_dir,\n            \"fileName\": filename,\n            \"saveMultipleFiles\": False,\n            \"simulationRate\": self.step,\n            \"sampleMultiplier\": self.stepSave,\n            \"optimize\": self.optimize,\n            \"optimizationThreshold\": self.optimizationThreshold,\n            \"optimizeAnimationsForMotionBlur\": (\n                self.optimizeAnimationsForMotionBlur\n            ),\n            \"writeMaterials\": self.writeMaterials,\n            \"useBaseTessellation\": self.useBaseTessellation\n        }\n        self.log.debug(\n            \"Extract {} with:\\n{}\".format(\n                instance[:], json.dumps(kwargs, indent=4, sort_keys=True)\n            )\n        )\n        cmds.gpuCache(instance[:], **kwargs)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            \"name\": \"gpu_cache\",\n            \"ext\": \"abc\",\n            \"files\": filename + \".abc\",\n            \"stagingDir\": staging_dir,\n            \"outputName\": \"gpu_cache\"\n        }\n\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\n            \"Extracted instance {} to: {}\".format(instance.name, staging_dir)\n        )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_import_reference.py",
    "content": "import os\nimport sys\n\nfrom maya import cmds\n\nimport pyblish.api\nimport tempfile\n\nfrom openpype.lib import run_subprocess\nfrom openpype.pipeline import publish\nfrom openpype.pipeline.publish import OptionalPyblishPluginMixin\nfrom openpype.hosts.maya.api import lib\n\n\nclass ExtractImportReference(publish.Extractor,\n                             OptionalPyblishPluginMixin):\n    \"\"\"\n\n        Extract the scene with imported reference.\n        The temp scene with imported reference is\n        published for rendering if this extractor is activated\n\n    \"\"\"\n\n    label = \"Extract Import Reference\"\n    order = pyblish.api.ExtractorOrder - 0.48\n    hosts = [\"maya\"]\n    families = [\"renderlayer\", \"workfile\"]\n    optional = True\n    tmp_format = \"_tmp\"\n\n    @classmethod\n    def apply_settings(cls, project_settings):\n        cls.active = project_settings[\"deadline\"][\"publish\"][\"MayaSubmitDeadline\"][\"import_reference\"] # noqa\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        ext_mapping = (\n            instance.context.data[\"project_settings\"][\"maya\"][\"ext_mapping\"]\n        )\n        if ext_mapping:\n            self.log.debug(\"Looking in settings for scene type ...\")\n            # use extension mapping for first family found\n            for family in self.families:\n                try:\n                    self.scene_type = ext_mapping[family]\n                    self.log.debug(\n                        \"Using {} as scene type\".format(self.scene_type))\n                    break\n\n                except KeyError:\n                    # set scene type to ma\n                    self.scene_type = \"ma\"\n\n        _scene_type = (\"mayaAscii\"\n                       if self.scene_type == \"ma\"\n                       else \"mayaBinary\")\n\n        dir_path = self.staging_dir(instance)\n        # named the file with imported reference\n        if instance.name == \"Main\":\n            return\n        tmp_name = instance.name + self.tmp_format\n        current_name = cmds.file(query=True, sceneName=True)\n        ref_scene_name = \"{0}.{1}\".format(tmp_name, self.scene_type)\n\n        reference_path = os.path.join(dir_path, ref_scene_name)\n        tmp_path = os.path.dirname(current_name) + \"/\" + ref_scene_name\n\n        self.log.debug(\"Performing extraction..\")\n\n        # This generates script for mayapy to take care of reference\n        # importing outside current session. It is passing current scene\n        # name and destination scene name.\n        script = (\"\"\"\n# -*- coding: utf-8 -*-\n'''Script to import references to given scene.'''\nimport maya.standalone\nmaya.standalone.initialize()\n# scene names filled by caller\ncurrent_name = \"{current_name}\"\nref_scene_name = \"{ref_scene_name}\"\nprint(\">>> Opening {{}} ...\".format(current_name))\ncmds.file(current_name, open=True, force=True)\nprint(\">>> Processing references\")\nall_reference = cmds.file(q=True, reference=True) or []\nfor ref in all_reference:\n    if cmds.referenceQuery(ref, il=True):\n        cmds.file(ref, importReference=True)\n\n        nested_ref = cmds.file(q=True, reference=True)\n        if nested_ref:\n            for new_ref in nested_ref:\n                if new_ref not in all_reference:\n                    all_reference.append(new_ref)\n\nprint(\">>> Finish importing references\")\nprint(\">>> Saving scene as {{}}\".format(ref_scene_name))\n\ncmds.file(rename=ref_scene_name)\ncmds.file(save=True, force=True)\nprint(\"*** Done\")\n        \"\"\").format(current_name=current_name, ref_scene_name=tmp_path)\n        mayapy_exe = os.path.join(os.getenv(\"MAYA_LOCATION\"), \"bin\", \"mayapy\")\n        if sys.platform == \"windows\":\n            mayapy_exe += \".exe\"\n            mayapy_exe = os.path.normpath(mayapy_exe)\n        # can't use TemporaryNamedFile as that can't be opened in another\n        # process until handles are closed by context manager.\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            tmp_script_path = os.path.join(tmp_dir_name, \"import_ref.py\")\n            self.log.debug(\"Using script file: {}\".format(tmp_script_path))\n            with open(tmp_script_path, \"wt\") as tmp:\n                tmp.write(script)\n\n            try:\n                run_subprocess([mayapy_exe, tmp_script_path])\n            except Exception:\n                self.log.error(\"Import reference failed\", exc_info=True)\n                raise\n\n        with lib.maintained_selection():\n            cmds.select(all=True, noExpand=True)\n            cmds.file(reference_path,\n                      force=True,\n                      typ=_scene_type,\n                      exportSelected=True,\n                      channels=True,\n                      constraints=True,\n                      shader=True,\n                      expressions=True,\n                      constructionHistory=True)\n\n        instance.context.data[\"currentFile\"] = tmp_path\n\n        if \"files\" not in instance.data:\n            instance.data[\"files\"] = []\n        instance.data[\"files\"].append(ref_scene_name)\n\n        if instance.data.get(\"representations\") is None:\n            instance.data[\"representations\"] = []\n\n        ref_representation = {\n            \"name\": self.scene_type,\n            \"ext\": self.scene_type,\n            \"files\": ref_scene_name,\n            \"stagingDir\": os.path.dirname(current_name),\n            \"outputName\": \"imported\"\n        }\n        self.log.debug(ref_representation)\n\n        instance.data[\"representations\"].append(ref_representation)\n\n        self.log.debug(\"Extracted instance '%s' to : '%s'\" % (ref_scene_name,\n                                                              reference_path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_layout.py",
    "content": "import math\nimport os\nimport json\n\nfrom maya import cmds\nfrom maya.api import OpenMaya as om\n\nfrom openpype.client import get_representation_by_id\nfrom openpype.pipeline import publish\n\n\nclass ExtractLayout(publish.Extractor):\n    \"\"\"Extract a layout.\"\"\"\n\n    label = \"Extract Layout\"\n    hosts = [\"maya\"]\n    families = [\"layout\"]\n    project_container = \"AVALON_CONTAINERS\"\n    optional = True\n\n    def process(self, instance):\n        # Define extract output file path\n        stagingdir = self.staging_dir(instance)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction..\")\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        json_data = []\n        # TODO representation queries can be refactored to be faster\n        project_name = instance.context.data[\"projectName\"]\n\n        for asset in cmds.sets(str(instance), query=True):\n            # Find the container\n            project_container = self.project_container\n            container_list = cmds.ls(project_container)\n            if len(container_list) == 0:\n                self.log.warning(\"Project container is not found!\")\n                self.log.warning(\"The asset(s) may not be properly loaded after published\") # noqa\n                continue\n\n            grp_loaded_ass = instance.data.get(\"groupLoadedAssets\", False)\n            if grp_loaded_ass:\n                asset_list = cmds.listRelatives(asset, children=True)\n                for asset in asset_list:\n                    grp_name = asset.split(':')[0]\n            else:\n                grp_name = asset.split(':')[0]\n            containers = cmds.ls(\"{}*_CON\".format(grp_name))\n            if len(containers) == 0:\n                self.log.warning(\"{} isn't from the loader\".format(asset))\n                self.log.warning(\"It may not be properly loaded after published\") # noqa\n                continue\n            container = containers[0]\n\n            representation_id = cmds.getAttr(\n                \"{}.representation\".format(container))\n\n            representation = get_representation_by_id(\n                project_name,\n                representation_id,\n                fields=[\"parent\", \"context.family\"]\n            )\n\n            self.log.debug(representation)\n\n            version_id = representation.get(\"parent\")\n            family = representation.get(\"context\").get(\"family\")\n\n            json_element = {\n                \"family\": family,\n                \"instance_name\": cmds.getAttr(\n                    \"{}.namespace\".format(container)),\n                \"representation\": str(representation_id),\n                \"version\": str(version_id)\n            }\n\n            loc = cmds.xform(asset, query=True, translation=True)\n            rot = cmds.xform(asset, query=True, rotation=True, euler=True)\n            scl = cmds.xform(asset, query=True, relative=True, scale=True)\n\n            json_element[\"transform\"] = {\n                \"translation\": {\n                    \"x\": loc[0],\n                    \"y\": loc[1],\n                    \"z\": loc[2]\n                },\n                \"rotation\": {\n                    \"x\": math.radians(rot[0]),\n                    \"y\": math.radians(rot[1]),\n                    \"z\": math.radians(rot[2])\n                },\n                \"scale\": {\n                    \"x\": scl[0],\n                    \"y\": scl[1],\n                    \"z\": scl[2]\n                }\n            }\n\n            row_length = 4\n            t_matrix_list = cmds.xform(asset, query=True, matrix=True)\n\n            transform_mm = om.MMatrix(t_matrix_list)\n            transform = om.MTransformationMatrix(transform_mm)\n\n            t = transform.translation(om.MSpace.kWorld)\n            t = om.MVector(t.x, t.z, -t.y)\n            transform.setTranslation(t, om.MSpace.kWorld)\n            transform.rotateBy(\n                om.MEulerRotation(math.radians(-90), 0, 0), om.MSpace.kWorld)\n            transform.scaleBy([1.0, 1.0, -1.0], om.MSpace.kObject)\n\n            t_matrix_list = list(transform.asMatrix())\n\n            t_matrix = []\n            for i in range(0, len(t_matrix_list), row_length):\n                t_matrix.append(t_matrix_list[i:i + row_length])\n\n            json_element[\"transform_matrix\"] = [\n                list(row)\n                for row in t_matrix\n            ]\n\n            basis_list = [\n                1, 0, 0, 0,\n                0, 1, 0, 0,\n                0, 0, -1, 0,\n                0, 0, 0, 1\n            ]\n\n            basis_mm = om.MMatrix(basis_list)\n            basis = om.MTransformationMatrix(basis_mm)\n\n            b_matrix_list = list(basis.asMatrix())\n            b_matrix = []\n\n            for i in range(0, len(b_matrix_list), row_length):\n                b_matrix.append(b_matrix_list[i:i + row_length])\n\n            json_element[\"basis\"] = []\n            for row in b_matrix:\n                json_element[\"basis\"].append(list(row))\n\n            json_data.append(json_element)\n\n        json_filename = \"{}.json\".format(instance.name)\n        json_path = os.path.join(stagingdir, json_filename)\n\n        with open(json_path, \"w+\") as file:\n            json.dump(json_data, fp=file, indent=2)\n\n        json_representation = {\n            'name': 'json',\n            'ext': 'json',\n            'files': json_filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(json_representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\",\n                       instance.name, json_representation)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_look.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Maya look extractor.\"\"\"\nimport sys\nfrom abc import ABCMeta, abstractmethod\nfrom collections import OrderedDict\nimport contextlib\nimport json\nimport logging\nimport os\nimport tempfile\nimport six\nimport attr\n\nimport pyblish.api\n\nfrom maya import cmds  # noqa\n\nfrom openpype.lib import (\n    find_executable,\n    source_hash,\n    run_subprocess,\n    get_oiio_tool_args,\n    ToolNotFoundError,\n)\n\nfrom openpype.pipeline import legacy_io, publish, KnownPublishError\nfrom openpype.hosts.maya.api import lib\nfrom openpype import AYON_SERVER_ENABLED\n\n# Modes for transfer\nCOPY = 1\nHARDLINK = 2\n\n\n@attr.s\nclass TextureResult(object):\n    \"\"\"The resulting texture of a processed file for a resource\"\"\"\n    # Path to the file\n    path = attr.ib()\n    # Colorspace of the resulting texture. This might not be the input\n    # colorspace of the texture if a TextureProcessor has processed the file.\n    colorspace = attr.ib()\n    # Hash generated for the texture using openpype.lib.source_hash\n    file_hash = attr.ib()\n    # The transfer mode, e.g. COPY or HARDLINK\n    transfer_mode = attr.ib()\n\n\ndef find_paths_by_hash(texture_hash):\n    \"\"\"Find the texture hash key in the dictionary.\n\n    All paths that originate from it.\n\n    Args:\n        texture_hash (str): Hash of the texture.\n\n    Return:\n        str: path to texture if found.\n\n    \"\"\"\n    if AYON_SERVER_ENABLED:\n        raise KnownPublishError(\n            \"This is a bug. \\\"find_paths_by_hash\\\" is not compatible with \"\n            \"AYON.\"\n        )\n\n    key = \"data.sourceHashes.{0}\".format(texture_hash)\n    return legacy_io.distinct(key, {\"type\": \"version\"})\n\n\n@contextlib.contextmanager\ndef no_workspace_dir():\n    \"\"\"Force maya to a fake temporary workspace directory.\n\n    Note: This is not maya.cmds.workspace 'rootDirectory' but the 'directory'\n\n    This helps to avoid Maya automatically remapping image paths to files\n    relative to the currently set directory.\n\n    \"\"\"\n\n    # Store current workspace\n    original = cmds.workspace(query=True, directory=True)\n\n    # Set a fake workspace\n    fake_workspace_dir = tempfile.mkdtemp()\n    cmds.workspace(directory=fake_workspace_dir)\n\n    try:\n        yield\n    finally:\n        try:\n            cmds.workspace(directory=original)\n        except RuntimeError:\n            # If the original workspace directory didn't exist either\n            # ignore the fact that it fails to reset it to the old path\n            pass\n\n        # Remove the temporary directory\n        os.rmdir(fake_workspace_dir)\n\n\n@six.add_metaclass(ABCMeta)\nclass TextureProcessor:\n\n    extension = None\n\n    def __init__(self, log=None):\n        if log is None:\n            log = logging.getLogger(self.__class__.__name__)\n        self.log = log\n\n    def apply_settings(self, system_settings, project_settings):\n        \"\"\"Apply OpenPype system/project settings to the TextureProcessor\n\n        Args:\n            system_settings (dict): OpenPype system settings\n            project_settings (dict): OpenPype project settings\n\n        Returns:\n            None\n\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def process(self,\n                source,\n                colorspace,\n                color_management,\n                staging_dir):\n        \"\"\"Process the `source` texture.\n\n        Must be implemented on inherited class.\n\n        This must always return a TextureResult even when it does not generate\n        a texture. If it doesn't generate a texture then it should return a\n        TextureResult using the input path and colorspace.\n\n        Args:\n            source (str): Path to source file.\n            colorspace (str): Colorspace of the source file.\n            color_management (dict): Maya Color management data from\n                `lib.get_color_management_preferences`\n            staging_dir (str): Output directory to write to.\n\n        Returns:\n            TextureResult: The resulting texture information.\n\n        \"\"\"\n        pass\n\n    def __repr__(self):\n        # Log instance as class name\n        return self.__class__.__name__\n\n\nclass MakeRSTexBin(TextureProcessor):\n    \"\"\"Make `.rstexbin` using `redshiftTextureProcessor`\"\"\"\n\n    extension = \".rstexbin\"\n\n    def process(self,\n                source,\n                colorspace,\n                color_management,\n                staging_dir):\n\n        texture_processor_path = self.get_redshift_tool(\n            \"redshiftTextureProcessor\"\n        )\n        if not texture_processor_path:\n            raise KnownPublishError(\"Must have Redshift available.\")\n\n        subprocess_args = [\n            texture_processor_path,\n            source\n        ]\n\n        # if color management is enabled we pass color space information\n        if color_management[\"enabled\"]:\n            config_path = color_management[\"config\"]\n            if not os.path.exists(config_path):\n                raise RuntimeError(\"OCIO config not found at: \"\n                                   \"{}\".format(config_path))\n\n            if not os.getenv(\"OCIO\"):\n                self.log.debug(\n                    \"OCIO environment variable not set.\"\n                    \"Setting it with OCIO config from Maya.\"\n                )\n                os.environ[\"OCIO\"] = config_path\n\n            self.log.debug(\"converting colorspace {0} to redshift render \"\n                           \"colorspace\".format(colorspace))\n            subprocess_args.extend([\"-cs\", colorspace])\n\n        hash_args = [\"rstex\"]\n        texture_hash = source_hash(source, *hash_args)\n\n        # Redshift stores the output texture next to the input but with\n        # the extension replaced to `.rstexbin`\n        basename, ext = os.path.splitext(source)\n        destination = \"{}{}\".format(basename, self.extension)\n\n        self.log.debug(\" \".join(subprocess_args))\n        try:\n            run_subprocess(subprocess_args, logger=self.log)\n        except Exception:\n            self.log.error(\"Texture .rstexbin conversion failed\",\n                           exc_info=True)\n            six.reraise(*sys.exc_info())\n\n        return TextureResult(\n            path=destination,\n            file_hash=texture_hash,\n            colorspace=colorspace,\n            transfer_mode=COPY\n        )\n\n    @staticmethod\n    def get_redshift_tool(tool_name):\n        \"\"\"Path to redshift texture processor.\n\n        On Windows it adds .exe extension if missing from tool argument.\n\n        Args:\n            tool_name (string): Tool name.\n\n        Returns:\n            str: Full path to redshift texture processor executable.\n        \"\"\"\n        if \"REDSHIFT_COREDATAPATH\" not in os.environ:\n            raise RuntimeError(\"Must have Redshift available.\")\n\n        redshift_tool_path = os.path.join(\n            os.environ[\"REDSHIFT_COREDATAPATH\"],\n            \"bin\",\n            tool_name\n        )\n\n        return find_executable(redshift_tool_path)\n\n\nclass MakeTX(TextureProcessor):\n    \"\"\"Make `.tx` using `maketx` with some default settings.\n\n    Some hardcoded arguments passed to `maketx` are based on the defaults used\n    in Arnold's txManager tool.\n\n    \"\"\"\n\n    extension = \".tx\"\n\n    def __init__(self, log=None):\n        super(MakeTX, self).__init__(log=log)\n        self.extra_args = []\n\n    def apply_settings(self, system_settings, project_settings):\n        # Allow extra maketx arguments from project settings\n        args_settings = (\n            project_settings[\"maya\"][\"publish\"]\n            .get(\"ExtractLook\", {}).get(\"maketx_arguments\", [])\n        )\n        extra_args = []\n        for arg_data in args_settings:\n            argument = arg_data[\"argument\"]\n            parameters = arg_data[\"parameters\"]\n            if not argument:\n                self.log.debug(\"Ignoring empty parameter from \"\n                               \"`maketx_arguments` setting..\")\n                continue\n\n            extra_args.append(argument)\n            extra_args.extend(parameters)\n\n        self.extra_args = extra_args\n\n    def process(self,\n                source,\n                colorspace,\n                color_management,\n                staging_dir):\n        \"\"\"Process the texture.\n\n        This function requires the `maketx` executable to be available in an\n        OpenImageIO toolset detectable by OpenPype.\n\n        Args:\n            source (str): Path to source file.\n            colorspace (str): Colorspace of the source file.\n            color_management (dict): Maya Color management data from\n                `lib.get_color_management_preferences`\n            staging_dir (str): Output directory to write to.\n\n        Returns:\n            TextureResult: The resulting texture information.\n\n        \"\"\"\n\n        try:\n            maketx_args = get_oiio_tool_args(\"maketx\")\n        except ToolNotFoundError:\n            raise KnownPublishError(\n                \"OpenImageIO is not available on the machine\")\n\n        # Define .tx filepath in staging if source file is not .tx\n        fname, ext = os.path.splitext(os.path.basename(source))\n        if ext == \".tx\":\n            # Do nothing if the source file is already a .tx file.\n            return TextureResult(\n                path=source,\n                file_hash=source_hash(source),\n                colorspace=colorspace,\n                transfer_mode=COPY\n            )\n\n        # Hardcoded default arguments for maketx conversion based on Arnold's\n        # txManager in Maya\n        args = [\n            # unpremultiply before conversion (recommended when alpha present)\n            \"--unpremult\",\n            # use oiio-optimized settings for tile-size, planarconfig, metadata\n            \"--oiio\",\n            \"--filter\", \"lanczos3\",\n        ]\n        if color_management[\"enabled\"]:\n            config_path = color_management[\"config\"]\n            if not os.path.exists(config_path):\n                raise RuntimeError(\"OCIO config not found at: \"\n                                   \"{}\".format(config_path))\n\n            render_colorspace = color_management[\"rendering_space\"]\n\n            self.log.debug(\"tx: converting colorspace {0} \"\n                          \"-> {1}\".format(colorspace,\n                                          render_colorspace))\n            args.extend([\"--colorconvert\", colorspace, render_colorspace])\n            args.extend([\"--colorconfig\", config_path])\n\n        else:\n            # Maya Color management is disabled. We cannot rely on an OCIO\n            self.log.debug(\"tx: Maya color management is disabled. No color \"\n                           \"conversion will be applied to .tx conversion for: \"\n                           \"{}\".format(source))\n            # Assume linear\n            render_colorspace = \"linear\"\n\n        # Note: The texture hash is only reliable if we include any potential\n        # conversion arguments provide to e.g. `maketx`\n        hash_args = [\"maketx\"] + args + self.extra_args\n        texture_hash = source_hash(source, *hash_args)\n\n        # Ensure folder exists\n        resources_dir = os.path.join(staging_dir, \"resources\")\n        if not os.path.exists(resources_dir):\n            os.makedirs(resources_dir)\n\n        self.log.debug(\"Generating .tx file for %s ..\" % source)\n\n        subprocess_args = maketx_args + [\n            \"-v\",  # verbose\n            \"-u\",  # update mode\n            # --checknan doesn't influence the output file but aborts the\n            # conversion if it finds any. So we can avoid it for the file hash\n            \"--checknan\",\n            source\n        ]\n\n        subprocess_args.extend(args)\n        if self.extra_args:\n            subprocess_args.extend(self.extra_args)\n\n        # Add source hash attribute after other arguments for log readability\n        # Note: argument is excluded from the hash since it is the hash itself\n        subprocess_args.extend([\n            \"--sattrib\",\n            \"sourceHash\",\n            texture_hash\n        ])\n\n        destination = os.path.join(resources_dir, fname + \".tx\")\n        subprocess_args.extend([\"-o\", destination])\n\n        # We want to make sure we are explicit about what OCIO config gets\n        # used. So when we supply no --colorconfig flag that no fallback to\n        # an OCIO env var occurs.\n        env = os.environ.copy()\n        env.pop(\"OCIO\", None)\n\n        self.log.debug(\" \".join(subprocess_args))\n        try:\n            run_subprocess(subprocess_args, env=env)\n        except Exception:\n            self.log.error(\"Texture maketx conversion failed\",\n                           exc_info=True)\n            raise\n\n        return TextureResult(\n            path=destination,\n            file_hash=texture_hash,\n            colorspace=render_colorspace,\n            transfer_mode=COPY\n        )\n\n    @staticmethod\n    def _has_arnold():\n        \"\"\"Return whether the arnold package is available and importable.\"\"\"\n        try:\n            import arnold  # noqa: F401\n            return True\n        except (ImportError, ModuleNotFoundError):\n            return False\n\n\nclass ExtractLook(publish.Extractor):\n    \"\"\"Extract Look (Maya Scene + JSON)\n\n    Only extracts the sets (shadingEngines and alike) alongside a .json file\n    that stores it relationships for the sets and \"attribute\" data for the\n    instance members.\n\n    \"\"\"\n\n    label = \"Extract Look (Maya Scene + JSON)\"\n    hosts = [\"maya\"]\n    families = [\"look\", \"mvLook\"]\n    order = pyblish.api.ExtractorOrder + 0.2\n    scene_type = \"ma\"\n    look_data_type = \"json\"\n\n    def get_maya_scene_type(self, instance):\n        \"\"\"Get Maya scene type from settings.\n\n        Args:\n            instance (pyblish.api.Instance): Instance with collected\n                project settings.\n\n        \"\"\"\n        ext_mapping = (\n            instance.context.data[\"project_settings\"][\"maya\"][\"ext_mapping\"]\n        )\n        if ext_mapping:\n            self.log.debug(\"Looking in settings for scene type ...\")\n            # use extension mapping for first family found\n            for family in self.families:\n                try:\n                    self.scene_type = ext_mapping[family]\n                    self.log.debug(\n                        \"Using {} as scene type\".format(self.scene_type))\n                    break\n                except KeyError:\n                    # no preset found\n                    pass\n\n        return \"mayaAscii\" if self.scene_type == \"ma\" else \"mayaBinary\"\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\n\n        Args:\n            instance: Instance to process.\n\n        \"\"\"\n        _scene_type = self.get_maya_scene_type(instance)\n\n        # Define extract output file path\n        dir_path = self.staging_dir(instance)\n        maya_fname = \"{0}.{1}\".format(instance.name, self.scene_type)\n        json_fname = \"{0}.{1}\".format(instance.name, self.look_data_type)\n        maya_path = os.path.join(dir_path, maya_fname)\n        json_path = os.path.join(dir_path, json_fname)\n\n        # Remove all members of the sets so they are not included in the\n        # exported file by accident\n        self.log.debug(\"Processing sets..\")\n        lookdata = instance.data[\"lookData\"]\n        relationships = lookdata[\"relationships\"]\n        sets = list(relationships.keys())\n        if not sets:\n            self.log.debug(\"No sets found for the look\")\n            return\n\n        # Specify texture processing executables to activate\n        # TODO: Load these more dynamically once we support more processors\n        processors = []\n        context = instance.context\n        for key, Processor in {\n            # Instance data key to texture processor mapping\n            \"maketx\": MakeTX,\n            \"rstex\": MakeRSTexBin\n        }.items():\n            if instance.data.get(key, False):\n                processor = Processor(log=self.log)\n                processor.apply_settings(context.data[\"system_settings\"],\n                                         context.data[\"project_settings\"])\n                processors.append(processor)\n\n        if processors:\n            self.log.debug(\"Collected texture processors: \"\n                           \"{}\".format(processors))\n\n        self.log.debug(\"Processing resources..\")\n        results = self.process_resources(instance,\n                                         staging_dir=dir_path,\n                                         processors=processors)\n        transfers = results[\"fileTransfers\"]\n        hardlinks = results[\"fileHardlinks\"]\n        hashes = results[\"fileHashes\"]\n        remap = results[\"attrRemap\"]\n\n        # Extract in correct render layer\n        self.log.debug(\"Extracting look maya scene file: {}\".format(maya_path))\n        layer = instance.data.get(\"renderlayer\", \"defaultRenderLayer\")\n        with lib.renderlayer(layer):\n            # TODO: Ensure membership edits don't become renderlayer overrides\n            with lib.empty_sets(sets, force=True):\n                # To avoid Maya trying to automatically remap the file\n                # textures relative to the `workspace -directory` we force\n                # it to a fake temporary workspace. This fixes textures\n                # getting incorrectly remapped.\n                with no_workspace_dir():\n                    with lib.attribute_values(remap):\n                        with lib.maintained_selection():\n                            cmds.select(sets, noExpand=True)\n                            cmds.file(\n                                maya_path,\n                                force=True,\n                                typ=_scene_type,\n                                exportSelected=True,\n                                preserveReferences=False,\n                                channels=True,\n                                constraints=True,\n                                expressions=True,\n                                constructionHistory=True,\n                            )\n\n        # Write the JSON data\n        data = {\n            \"attributes\": lookdata[\"attributes\"],\n            \"relationships\": relationships\n        }\n\n        self.log.debug(\"Extracting json file: {}\".format(json_path))\n        with open(json_path, \"w\") as f:\n            json.dump(data, f)\n\n        if \"files\" not in instance.data:\n            instance.data[\"files\"] = []\n        if \"hardlinks\" not in instance.data:\n            instance.data[\"hardlinks\"] = []\n        if \"transfers\" not in instance.data:\n            instance.data[\"transfers\"] = []\n\n        instance.data[\"files\"].append(maya_fname)\n        instance.data[\"files\"].append(json_fname)\n\n        if instance.data.get(\"representations\") is None:\n            instance.data[\"representations\"] = []\n\n        instance.data[\"representations\"].append(\n            {\n                \"name\": self.scene_type,\n                \"ext\": self.scene_type,\n                \"files\": os.path.basename(maya_fname),\n                \"stagingDir\": os.path.dirname(maya_fname),\n            }\n        )\n        instance.data[\"representations\"].append(\n            {\n                \"name\": self.look_data_type,\n                \"ext\": self.look_data_type,\n                \"files\": os.path.basename(json_fname),\n                \"stagingDir\": os.path.dirname(json_fname),\n            }\n        )\n\n        # Set up the resources transfers/links for the integrator\n        instance.data[\"transfers\"].extend(transfers)\n        instance.data[\"hardlinks\"].extend(hardlinks)\n\n        # Source hash for the textures\n        instance.data[\"sourceHashes\"] = hashes\n\n        self.log.debug(\"Extracted instance '%s' to: %s\" % (instance.name,\n                                                           maya_path))\n\n    def _set_resource_result_colorspace(self, resource, colorspace):\n        \"\"\"Update resource resulting colorspace after texture processing\"\"\"\n        if \"result_color_space\" in resource:\n            if resource[\"result_color_space\"] == colorspace:\n                return\n\n            self.log.warning(\n                \"Resource already has a resulting colorspace but is now \"\n                \"being overridden to a new one: {} -> {}\".format(\n                    resource[\"result_color_space\"], colorspace\n                )\n            )\n        resource[\"result_color_space\"] = colorspace\n\n    def process_resources(self, instance, staging_dir, processors):\n        \"\"\"Process all resources in the instance.\n\n        It is assumed that all resources are nodes using file textures.\n\n        Extract the textures to transfer, possibly convert with maketx and\n        remap the node paths to the destination path. Note that a source\n        might be included more than once amongst the resources as they could\n        be the input file to multiple nodes.\n\n        \"\"\"\n\n        resources = instance.data[\"resources\"]\n        color_management = lib.get_color_management_preferences()\n\n        # TODO: Temporary disable all hardlinking, due to the feature not being\n        # used or properly working.\n        self.log.info(\n            \"Forcing copy instead of hardlink.\"\n        )\n        force_copy = True\n\n        if not force_copy and platform.system().lower() == \"windows\":\n            # Temporary fix to NOT create hardlinks on windows machines\n            self.log.warning(\n                \"Forcing copy instead of hardlink due to issues on Windows...\"\n            )\n            force_copy = True\n\n        destinations_cache = {}\n\n        def get_resource_destination_cached(path):\n            \"\"\"Get resource destination with cached result per filepath\"\"\"\n            if path not in destinations_cache:\n                destination = self.get_resource_destination(\n                    path, instance.data[\"resourcesDir\"], processors)\n                destinations_cache[path] = destination\n            return destinations_cache[path]\n\n        # Process all resource's individual files\n        processed_files = {}\n        transfers = []\n        hardlinks = []\n        hashes = {}\n        remap = OrderedDict()\n        for resource in resources:\n            colorspace = resource[\"color_space\"]\n\n            for filepath in resource[\"files\"]:\n                filepath = os.path.normpath(filepath)\n\n                if filepath in processed_files:\n                    # The file was already processed, likely due to usage by\n                    # another resource in the scene. We confirm here it\n                    # didn't do color spaces different than the current\n                    # resource.\n                    processed_file = processed_files[filepath]\n                    self.log.debug(\n                        \"File was already processed. Likely used by another \"\n                        \"resource too: {}\".format(filepath)\n                    )\n\n                    if colorspace != processed_file[\"color_space\"]:\n                        self.log.warning(\n                            \"File '{}' was already processed using colorspace \"\n                            \"'{}' instead of the current resource's \"\n                            \"colorspace '{}'. The already processed texture \"\n                            \"result's colorspace '{}' will be used.\"\n                            \"\".format(filepath,\n                                      colorspace,\n                                      processed_file[\"color_space\"],\n                                      processed_file[\"result_color_space\"]))\n\n                    self._set_resource_result_colorspace(\n                        resource,\n                        colorspace=processed_file[\"result_color_space\"]\n                    )\n                    continue\n\n                texture_result = self._process_texture(\n                    filepath,\n                    processors=processors,\n                    staging_dir=staging_dir,\n                    force_copy=force_copy,\n                    color_management=color_management,\n                    colorspace=colorspace\n                )\n\n                # Set the resulting color space on the resource\n                self._set_resource_result_colorspace(\n                    resource, colorspace=texture_result.colorspace\n                )\n\n                processed_files[filepath] = {\n                    \"color_space\": colorspace,\n                    \"result_color_space\": texture_result.colorspace,\n                }\n\n                source = texture_result.path\n                destination = get_resource_destination_cached(source)\n                if force_copy or texture_result.transfer_mode == COPY:\n                    transfers.append((source, destination))\n                    self.log.debug('file will be copied {} -> {}'.format(\n                        source, destination))\n                elif texture_result.transfer_mode == HARDLINK:\n                    hardlinks.append((source, destination))\n                    self.log.debug('file will be hardlinked {} -> {}'.format(\n                        source, destination))\n\n                # Store the hashes from hash to destination to include in the\n                # database\n                hashes[texture_result.file_hash] = destination\n\n            # Set up remapping attributes for the node during the publish\n            # The order of these can be important if one attribute directly\n            # affects another, e.g. we set colorspace after filepath because\n            # maya sometimes tries to guess the colorspace when changing\n            # filepaths (which is avoidable, but we don't want to have those\n            # attributes changed in the resulting publish)\n            # Remap filepath to publish destination\n            # TODO It would be much better if we could use the destination path\n            #   from the actual processed texture results, but since the\n            #   attribute will need to preserve tokens like <f>, <udim> etc for\n            #   now we will define the output path from the attribute value\n            #   including the tokens to persist them.\n            filepath_attr = resource[\"attribute\"]\n            remap[filepath_attr] = get_resource_destination_cached(\n                resource[\"source\"]\n            )\n\n            # Preserve color space values (force value after filepath change)\n            # This will also trigger in the same order at end of context to\n            # ensure after context it's still the original value.\n            node = resource[\"node\"]\n            if cmds.attributeQuery(\"colorSpace\", node=node, exists=True):\n                color_space_attr = \"{}.colorSpace\".format(node)\n                remap[color_space_attr] = resource[\"result_color_space\"]\n\n        self.log.debug(\"Finished remapping destinations ...\")\n\n        return {\n            \"fileTransfers\": transfers,\n            \"fileHardlinks\": hardlinks,\n            \"fileHashes\": hashes,\n            \"attrRemap\": remap,\n        }\n\n    def get_resource_destination(self, filepath, resources_dir, processors):\n        \"\"\"Get resource destination path.\n\n        This is utility function to change path if resource file name is\n        changed by some external tool like `maketx`.\n\n        Args:\n            filepath (str): Resource source path\n            resources_dir (str): Destination dir for resources in publish.\n            processors (list): Texture processors converting resource.\n\n        Returns:\n            str: Path to resource file\n\n        \"\"\"\n        # Compute destination location\n        basename, ext = os.path.splitext(os.path.basename(filepath))\n\n        # Get extension from the last processor\n        for processor in reversed(processors):\n            processor_ext = processor.extension\n            if processor_ext and ext != processor_ext:\n                self.log.debug(\"Processor {} overrides extension to '{}' \"\n                               \"for path: {}\".format(processor,\n                                                     processor_ext,\n                                                     filepath))\n                ext = processor_ext\n            break\n\n        return os.path.join(\n            resources_dir, basename + ext\n        )\n\n    def _get_existing_hashed_texture(self, texture_hash):\n        \"\"\"Return the first found filepath from a texture hash\"\"\"\n\n        # If source has been published before with the same settings,\n        # then don't reprocess but hardlink from the original\n        existing = find_paths_by_hash(texture_hash)\n        if existing:\n            source = next((p for p in existing if os.path.exists(p)), None)\n            if source:\n                return source\n            else:\n                self.log.warning(\n                    \"Paths not found on disk, \"\n                    \"skipping hardlink: {}\".format(existing)\n                )\n\n    def _process_texture(self,\n                         filepath,\n                         processors,\n                         staging_dir,\n                         force_copy,\n                         color_management,\n                         colorspace):\n        \"\"\"Process a single texture file on disk for publishing.\n\n        This will:\n            1. Check whether it's already published, if so it will do hardlink\n                (if the texture hash is found and force copy is not enabled)\n            2. It will process the texture using the supplied texture\n                processors like MakeTX and MakeRSTexBin if enabled.\n            3. Compute the destination path for the source file.\n\n        Args:\n            filepath (str): The source file path to process.\n            processors (list): List of TextureProcessor processing the texture\n            staging_dir (str): The staging directory to write to.\n            force_copy (bool): Whether to force a copy even if a file hash\n                might have existed already in the project, otherwise\n                hardlinking the existing file is allowed.\n            color_management (dict): Maya's Color Management settings from\n                `lib.get_color_management_preferences`\n            colorspace (str): The source colorspace of the resources this\n                texture belongs to.\n\n        Returns:\n            TextureResult: The texture result information.\n        \"\"\"\n\n        if len(processors) > 1:\n            raise KnownPublishError(\n                \"More than one texture processor not supported. \"\n                \"Current processors enabled: {}\".format(processors)\n            )\n\n        for processor in processors:\n            self.log.debug(\"Processing texture {} with processor {}\".format(\n                filepath, processor\n            ))\n\n            processed_result = processor.process(filepath,\n                                                 colorspace,\n                                                 color_management,\n                                                 staging_dir)\n            if not processed_result:\n                raise RuntimeError(\"Texture Processor {} returned \"\n                                   \"no result.\".format(processor))\n            self.log.debug(\"Generated processed \"\n                           \"texture: {}\".format(processed_result.path))\n\n            # TODO: Currently all processors force copy instead of allowing\n            #       hardlinks using source hashes. This should be refactored\n            return processed_result\n\n        # No texture processing for this file\n        texture_hash = source_hash(filepath)\n        if not force_copy:\n            existing = self._get_existing_hashed_texture(filepath)\n            if existing:\n                self.log.debug(\"Found hash in database, preparing hardlink..\")\n                return TextureResult(\n                    path=filepath,\n                    file_hash=texture_hash,\n                    colorspace=colorspace,\n                    transfer_mode=HARDLINK\n                )\n\n        return TextureResult(\n            path=filepath,\n            file_hash=texture_hash,\n            colorspace=colorspace,\n            transfer_mode=COPY\n        )\n\n\nclass ExtractModelRenderSets(ExtractLook):\n    \"\"\"Extract model render attribute sets as model metadata\n\n    Only extracts the render attrib sets (NO shadingEngines) alongside\n    a .json file that stores it relationships for the sets and \"attribute\"\n    data for the instance members.\n\n    \"\"\"\n\n    label = \"Model Render Sets\"\n    hosts = [\"maya\"]\n    families = [\"model\"]\n    scene_type_prefix = \"meta.render.\"\n    look_data_type = \"meta.render.json\"\n\n    def get_maya_scene_type(self, instance):\n        typ = super(ExtractModelRenderSets, self).get_maya_scene_type(instance)\n        # add prefix\n        self.scene_type = self.scene_type_prefix + self.scene_type\n\n        return typ\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract data as Maya scene (raw).\"\"\"\nimport os\n\nfrom maya import cmds\n\nfrom openpype.hosts.maya.api.lib import maintained_selection\nfrom openpype.pipeline import AVALON_CONTAINER_ID, publish\nfrom openpype.pipeline.publish import OpenPypePyblishPluginMixin\nfrom openpype.lib import BoolDef\n\n\nclass ExtractMayaSceneRaw(publish.Extractor, OpenPypePyblishPluginMixin):\n    \"\"\"Extract as Maya Scene (raw).\n\n    This will preserve all references, construction history, etc.\n    \"\"\"\n\n    label = \"Maya Scene (Raw)\"\n    hosts = [\"maya\"]\n    families = [\"mayaAscii\",\n                \"mayaScene\",\n                \"setdress\",\n                \"layout\",\n                \"camerarig\"]\n    scene_type = \"ma\"\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            BoolDef(\n                \"preserve_references\",\n                label=\"Preserve References\",\n                tooltip=(\n                    \"When enabled references will still be references \"\n                    \"in the published file.\\nWhen disabled the references \"\n                    \"are imported into the published file generating a \"\n                    \"file without references.\"\n                ),\n                default=True\n            )\n        ]\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        ext_mapping = (\n            instance.context.data[\"project_settings\"][\"maya\"][\"ext_mapping\"]\n        )\n        if ext_mapping:\n            self.log.debug(\"Looking in settings for scene type ...\")\n            # use extension mapping for first family found\n            for family in self.families:\n                try:\n                    self.scene_type = ext_mapping[family]\n                    self.log.debug(\n                        \"Using {} as scene type\".format(self.scene_type))\n                    break\n                except KeyError:\n                    # no preset found\n                    pass\n        # Define extract output file path\n        dir_path = self.staging_dir(instance)\n        filename = \"{0}.{1}\".format(instance.name, self.scene_type)\n        path = os.path.join(dir_path, filename)\n\n        # Whether to include all nodes in the instance (including those from\n        # history) or only use the exact set members\n        members_only = instance.data.get(\"exactSetMembersOnly\", False)\n        if members_only:\n            members = instance.data.get(\"setMembers\", list())\n            if not members:\n                raise RuntimeError(\"Can't export 'exact set members only' \"\n                                   \"when set is empty.\")\n        else:\n            members = instance[:]\n\n        selection = members\n        if set(self.add_for_families).intersection(\n                set(instance.data.get(\"families\", []))) or \\\n                instance.data.get(\"family\") in self.add_for_families:\n            selection += self._get_loaded_containers(members)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction ...\")\n        attribute_values = self.get_attr_values_from_data(\n            instance.data\n        )\n        with maintained_selection():\n            cmds.select(selection, noExpand=True)\n            cmds.file(path,\n                      force=True,\n                      typ=\"mayaAscii\" if self.scene_type == \"ma\" else \"mayaBinary\",  # noqa: E501\n                      exportSelected=True,\n                      preserveReferences=attribute_values[\n                          \"preserve_references\"\n                      ],\n                      constructionHistory=True,\n                      shader=True,\n                      constraints=True,\n                      expressions=True)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': self.scene_type,\n            'ext': self.scene_type,\n            'files': filename,\n            \"stagingDir\": dir_path\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\" % (instance.name,\n                                                           path))\n\n    @staticmethod\n    def _get_loaded_containers(members):\n        # type: (list) -> list\n        refs_to_include = {\n            cmds.referenceQuery(node, referenceNode=True)\n            for node in members\n            if cmds.referenceQuery(node, isNodeReferenced=True)\n        }\n\n        members_with_refs = refs_to_include.union(members)\n\n        obj_sets = cmds.ls(\"*.id\", long=True, type=\"objectSet\", recursive=True,\n                           objectsOnly=True)\n\n        loaded_containers = []\n        for obj_set in obj_sets:\n\n            if not cmds.attributeQuery(\"id\", node=obj_set, exists=True):\n                continue\n\n            id_attr = \"{}.id\".format(obj_set)\n            if cmds.getAttr(id_attr) != AVALON_CONTAINER_ID:\n                continue\n\n            set_content = set(cmds.sets(obj_set, query=True))\n            if set_content.intersection(members_with_refs):\n                loaded_containers.append(obj_set)\n\n        return loaded_containers\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_maya_usd.py",
    "content": "import os\nimport six\nimport json\nimport contextlib\n\nfrom maya import cmds\n\nimport pyblish.api\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.lib import maintained_selection\n\n\n@contextlib.contextmanager\ndef usd_export_attributes(nodes, attrs=None, attr_prefixes=None, mapping=None):\n    \"\"\"Define attributes for the given nodes that should be exported.\n\n    MayaUSDExport will export custom attributes if the Maya node has a\n    string attribute `USD_UserExportedAttributesJson` that provides an\n    export mapping for the maya attributes. This context manager will try\n    to autogenerate such an attribute during the export to include attributes\n    for the export.\n\n    Arguments:\n        nodes (List[str]): Nodes to process.\n        attrs (Optional[List[str]]): Full name of attributes to include.\n        attr_prefixes (Optional[List[str]]): Prefixes of attributes to include.\n        mapping (Optional[Dict[Dict]]): A mapping per attribute name for the\n            conversion to a USD attribute, including renaming, defining type,\n            converting attribute precision, etc. This match the usual\n            `USD_UserExportedAttributesJson` json mapping of `mayaUSDExport`.\n            When no mapping provided for an attribute it will use `{}` as\n            value.\n\n    Examples:\n          >>> with usd_export_attributes(\n          >>>     [\"pCube1\"], attrs=\"myDoubleAttributeAsFloat\", mapping={\n          >>>         \"myDoubleAttributeAsFloat\": {\n          >>>           \"usdAttrName\": \"my:namespace:attrib\",\n          >>>           \"translateMayaDoubleToUsdSinglePrecision\": True,\n          >>>         }\n          >>> })\n\n    \"\"\"\n    # todo: this might be better done with a custom export chaser\n    #   see `chaser` argument for `mayaUSDExport`\n\n    import maya.api.OpenMaya as om\n\n    if not attrs and not attr_prefixes:\n        # context manager does nothing\n        yield\n        return\n\n    if attrs is None:\n        attrs = []\n    if attr_prefixes is None:\n        attr_prefixes = []\n    if mapping is None:\n        mapping = {}\n\n    usd_json_attr = \"USD_UserExportedAttributesJson\"\n    strings = attrs + [\"{}*\".format(prefix) for prefix in attr_prefixes]\n    context_state = {}\n    for node in set(nodes):\n        node_attrs = cmds.listAttr(node, st=strings)\n        if not node_attrs:\n            # Nothing to do for this node\n            continue\n\n        node_attr_data = {}\n        for node_attr in set(node_attrs):\n            node_attr_data[node_attr] = mapping.get(node_attr, {})\n\n        if cmds.attributeQuery(usd_json_attr, node=node, exists=True):\n            existing_node_attr_value = cmds.getAttr(\n                \"{}.{}\".format(node, usd_json_attr)\n            )\n            if existing_node_attr_value and existing_node_attr_value != \"{}\":\n                # Any existing attribute mappings in an existing\n                # `USD_UserExportedAttributesJson` attribute always take\n                # precedence over what this function tries to imprint\n                existing_node_attr_data = json.loads(existing_node_attr_value)\n                node_attr_data.update(existing_node_attr_data)\n\n        context_state[node] = json.dumps(node_attr_data)\n\n    sel = om.MSelectionList()\n    dg_mod = om.MDGModifier()\n    fn_string = om.MFnStringData()\n    fn_typed = om.MFnTypedAttribute()\n    try:\n        for node, value in context_state.items():\n            data = fn_string.create(value)\n            sel.clear()\n            if cmds.attributeQuery(usd_json_attr, node=node, exists=True):\n                # Set the attribute value\n                sel.add(\"{}.{}\".format(node, usd_json_attr))\n                plug = sel.getPlug(0)\n                dg_mod.newPlugValue(plug, data)\n            else:\n                # Create attribute with the value as default value\n                sel.add(node)\n                node_obj = sel.getDependNode(0)\n                attr_obj = fn_typed.create(usd_json_attr,\n                                           usd_json_attr,\n                                           om.MFnData.kString,\n                                           data)\n                dg_mod.addAttribute(node_obj, attr_obj)\n        dg_mod.doIt()\n        yield\n    finally:\n        dg_mod.undoIt()\n\n\nclass ExtractMayaUsd(publish.Extractor):\n    \"\"\"Extractor for Maya USD Asset data.\n\n    Upon publish a .usd (or .usdz) asset file will typically be written.\n    \"\"\"\n\n    label = \"Extract Maya USD Asset\"\n    hosts = [\"maya\"]\n    families = [\"mayaUsd\"]\n\n    @property\n    def options(self):\n        \"\"\"Overridable options for Maya USD Export\n\n        Given in the following format\n            - {NAME: EXPECTED TYPE}\n\n        If the overridden option's type does not match,\n        the option is not included and a warning is logged.\n\n        \"\"\"\n\n        # TODO: Support more `mayaUSDExport` parameters\n        return {\n            \"defaultUSDFormat\": str,\n            \"stripNamespaces\": bool,\n            \"mergeTransformAndShape\": bool,\n            \"exportDisplayColor\": bool,\n            \"exportColorSets\": bool,\n            \"exportInstances\": bool,\n            \"exportUVs\": bool,\n            \"exportVisibility\": bool,\n            \"exportComponentTags\": bool,\n            \"exportRefsAsInstanceable\": bool,\n            \"eulerFilter\": bool,\n            \"renderableOnly\": bool,\n            \"jobContext\": (list, None)  # optional list\n            # \"worldspace\": bool,\n        }\n\n    @property\n    def default_options(self):\n        \"\"\"The default options for Maya USD Export.\"\"\"\n\n        # TODO: Support more `mayaUSDExport` parameters\n        return {\n            \"defaultUSDFormat\": \"usdc\",\n            \"stripNamespaces\": False,\n            \"mergeTransformAndShape\": False,\n            \"exportDisplayColor\": False,\n            \"exportColorSets\": True,\n            \"exportInstances\": True,\n            \"exportUVs\": True,\n            \"exportVisibility\": True,\n            \"exportComponentTags\": True,\n            \"exportRefsAsInstanceable\": False,\n            \"eulerFilter\": True,\n            \"renderableOnly\": False,\n            \"jobContext\": None\n            # \"worldspace\": False\n        }\n\n    def parse_overrides(self, instance, options):\n        \"\"\"Inspect data of instance to determine overridden options\"\"\"\n\n        for key in instance.data:\n            if key not in self.options:\n                continue\n\n            # Ensure the data is of correct type\n            value = instance.data[key]\n            if isinstance(value, six.text_type):\n                value = str(value)\n            if not isinstance(value, self.options[key]):\n                self.log.warning(\n                    \"Overridden attribute {key} was of \"\n                    \"the wrong type: {invalid_type} \"\n                    \"- should have been {valid_type}\".format(\n                        key=key,\n                        invalid_type=type(value).__name__,\n                        valid_type=self.options[key].__name__))\n                continue\n\n            options[key] = value\n\n        return options\n\n    def filter_members(self, members):\n        # Can be overridden by inherited classes\n        return members\n\n    def process(self, instance):\n\n        # Load plugin first\n        cmds.loadPlugin(\"mayaUsdPlugin\", quiet=True)\n\n        # Define output file path\n        staging_dir = self.staging_dir(instance)\n        file_name = \"{0}.usd\".format(instance.name)\n        file_path = os.path.join(staging_dir, file_name)\n        file_path = file_path.replace('\\\\', '/')\n\n        # Parse export options\n        options = self.default_options\n        options = self.parse_overrides(instance, options)\n        self.log.debug(\"Export options: {0}\".format(options))\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction ...\")\n\n        members = instance.data(\"setMembers\")\n        self.log.debug('Collected objects: {}'.format(members))\n        members = self.filter_members(members)\n        if not members:\n            self.log.error('No members!')\n            return\n\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n\n        def parse_attr_str(attr_str):\n            result = list()\n            for attr in attr_str.split(\",\"):\n                attr = attr.strip()\n                if not attr:\n                    continue\n                result.append(attr)\n            return result\n\n        attrs = parse_attr_str(instance.data.get(\"attr\", \"\"))\n        attrs += instance.data.get(\"userDefinedAttributes\", [])\n        attrs += [\"cbId\"]\n        attr_prefixes = parse_attr_str(instance.data.get(\"attrPrefix\", \"\"))\n\n        self.log.debug('Exporting USD: {} / {}'.format(file_path, members))\n        with maintained_selection():\n            with usd_export_attributes(instance[:],\n                                       attrs=attrs,\n                                       attr_prefixes=attr_prefixes):\n                cmds.mayaUSDExport(file=file_path,\n                                   frameRange=(start, end),\n                                   frameStride=instance.data.get(\"step\", 1.0),\n                                   exportRoots=members,\n                                   **options)\n\n        representation = {\n            'name': \"usd\",\n            'ext': \"usd\",\n            'files': file_name,\n            'stagingDir': staging_dir\n        }\n        instance.data.setdefault(\"representations\", []).append(representation)\n\n        self.log.debug(\n            \"Extracted instance {} to {}\".format(instance.name, file_path)\n        )\n\n\nclass ExtractMayaUsdAnim(ExtractMayaUsd):\n    \"\"\"Extractor for Maya USD Animation Sparse Cache data.\n\n    This will extract the sparse cache data from the scene and generate a\n    USD file with all the animation data.\n\n    Upon publish a .usd sparse cache will be written.\n    \"\"\"\n    label = \"Extract Maya USD Animation Sparse Cache\"\n    families = [\"animation\", \"mayaUsd\"]\n    match = pyblish.api.Subset\n\n    def filter_members(self, members):\n        out_set = next((i for i in members if i.endswith(\"out_SET\")), None)\n\n        if out_set is None:\n            self.log.warning(\"Expecting out_SET\")\n            return None\n\n        members = cmds.ls(cmds.sets(out_set, query=True), long=True)\n        return members\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract model as Maya Scene.\"\"\"\nimport os\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api import lib\n\n\nclass ExtractModel(publish.Extractor,\n                   publish.OptionalPyblishPluginMixin):\n    \"\"\"Extract as Model (Maya Scene).\n\n    Only extracts contents based on the original \"setMembers\" data to ensure\n    publishing the least amount of required shapes. From that it only takes\n    the shapes that are not intermediateObjects\n\n    During export it sets a temporary context to perform a clean extraction.\n    The context ensures:\n        - Smooth preview is turned off for the geometry\n        - Default shader is assigned (no materials are exported)\n        - Remove display layers\n\n    \"\"\"\n\n    label = \"Model (Maya Scene)\"\n    hosts = [\"maya\"]\n    families = [\"model\"]\n    scene_type = \"ma\"\n    optional = True\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        ext_mapping = (\n            instance.context.data[\"project_settings\"][\"maya\"][\"ext_mapping\"]\n        )\n        if ext_mapping:\n            self.log.debug(\"Looking in settings for scene type ...\")\n            # use extension mapping for first family found\n            for family in self.families:\n                try:\n                    self.scene_type = ext_mapping[family]\n                    self.log.debug(\n                        \"Using {} as scene type\".format(self.scene_type))\n                    break\n                except KeyError:\n                    # no preset found\n                    pass\n        # Define extract output file path\n        stagingdir = self.staging_dir(instance)\n        filename = \"{0}.{1}\".format(instance.name, self.scene_type)\n        path = os.path.join(stagingdir, filename)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction ...\")\n\n        # Get only the shape contents we need in such a way that we avoid\n        # taking along intermediateObjects\n        members = instance.data(\"setMembers\")\n        members = cmds.ls(members,\n                          dag=True,\n                          shapes=True,\n                          type=(\"mesh\", \"nurbsCurve\"),\n                          noIntermediate=True,\n                          long=True)\n\n        with lib.no_display_layers(instance):\n            with lib.displaySmoothness(members,\n                                       divisionsU=0,\n                                       divisionsV=0,\n                                       pointsWire=4,\n                                       pointsShaded=1,\n                                       polygonObject=1):\n                with lib.shader(members,\n                                shadingEngine=\"initialShadingGroup\"):\n                    with lib.maintained_selection():\n                        cmds.select(members, noExpand=True)\n                        cmds.file(path,\n                                  force=True,\n                                  typ=\"mayaAscii\" if self.scene_type == \"ma\" else \"mayaBinary\",  # noqa: E501\n                                  exportSelected=True,\n                                  preserveReferences=False,\n                                  channels=False,\n                                  constraints=False,\n                                  expressions=False,\n                                  constructionHistory=False)\n\n                        # Store reference for integration\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': self.scene_type,\n            'ext': self.scene_type,\n            'files': filename,\n            \"stagingDir\": stagingdir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\" % (instance.name,\n                                                           path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_multiverse_look.py",
    "content": "import os\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.lib import maintained_selection\n\n\nclass ExtractMultiverseLook(publish.Extractor):\n    \"\"\"Extractor for Multiverse USD look data.\n\n    This will extract:\n\n    - the shading networks that are assigned in MEOW as Maya material overrides\n      to a Multiverse Compound\n    - settings for a Multiverse Write Override operation.\n\n    Relevant settings are visible in the Maya set node created by a Multiverse\n    USD Look instance creator.\n\n    The input data contained in the set is:\n\n    - a single Multiverse Compound node with any number of Maya material\n      overrides (typically set in MEOW)\n\n    Upon publish two files will be written:\n\n    - a .usda override file containing material assignment information\n    - a .ma file containing shading networks\n\n    Note: when layering the material assignment override on a loaded Compound,\n          remember to set a matching attribute override with the namespace of\n          the loaded compound in order for the material assignment to resolve.\n    \"\"\"\n\n    label = \"Extract Multiverse USD Look\"\n    hosts = [\"maya\"]\n    families = [\"mvLook\"]\n    scene_type = \"usda\"\n    file_formats = [\"usda\", \"usd\"]\n\n    @property\n    def options(self):\n        \"\"\"Overridable options for Multiverse USD Export\n\n        Given in the following format\n            - {NAME: EXPECTED TYPE}\n\n        If the overridden option's type does not match,\n        the option is not included and a warning is logged.\n\n        \"\"\"\n\n        return {\n            \"writeAll\": bool,\n            \"writeTransforms\": bool,\n            \"writeVisibility\": bool,\n            \"writeAttributes\": bool,\n            \"writeMaterials\": bool,\n            \"writeVariants\": bool,\n            \"writeVariantsDefinition\": bool,\n            \"writeActiveState\": bool,\n            \"writeNamespaces\": bool,\n            \"numTimeSamples\": int,\n            \"timeSamplesSpan\": float\n        }\n\n    @property\n    def default_options(self):\n        \"\"\"The default options for Multiverse USD extraction.\"\"\"\n\n        return {\n            \"writeAll\": False,\n            \"writeTransforms\": False,\n            \"writeVisibility\": False,\n            \"writeAttributes\": True,\n            \"writeMaterials\": True,\n            \"writeVariants\": False,\n            \"writeVariantsDefinition\": False,\n            \"writeActiveState\": False,\n            \"writeNamespaces\": True,\n            \"numTimeSamples\": 1,\n            \"timeSamplesSpan\": 0.0\n        }\n\n    def get_file_format(self, instance):\n        fileFormat = instance.data[\"fileFormat\"]\n        if fileFormat in range(len(self.file_formats)):\n            self.scene_type = self.file_formats[fileFormat]\n\n    def process(self, instance):\n        # Load plugin first\n        cmds.loadPlugin(\"MultiverseForMaya\", quiet=True)\n\n        # Define output file path\n        staging_dir = self.staging_dir(instance)\n        self.get_file_format(instance)\n        file_name = \"{0}.{1}\".format(instance.name, self.scene_type)\n        file_path = os.path.join(staging_dir, file_name)\n        file_path = file_path.replace('\\\\', '/')\n\n        # Parse export options\n        options = self.default_options\n        self.log.debug(\"Export options: {0}\".format(options))\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction ...\")\n\n        with maintained_selection():\n            members = instance.data(\"setMembers\")\n            members = cmds.ls(members,\n                              dag=True,\n                              shapes=False,\n                              type=\"mvUsdCompoundShape\",\n                              noIntermediate=True,\n                              long=True)\n            self.log.debug('Collected object {}'.format(members))\n            if len(members) > 1:\n                self.log.error('More than one member: {}'.format(members))\n\n            import multiverse\n\n            over_write_opts = multiverse.OverridesWriteOptions()\n            options_discard_keys = {\n                \"numTimeSamples\",\n                \"timeSamplesSpan\",\n                \"frameStart\",\n                \"frameEnd\",\n                \"handleStart\",\n                \"handleEnd\",\n                \"step\",\n                \"fps\"\n            }\n            for key, value in options.items():\n                if key in options_discard_keys:\n                    continue\n                setattr(over_write_opts, key, value)\n\n            for member in members:\n                # @TODO: Make sure there is only one here.\n\n                self.log.debug(\"Writing Override for '{}'\".format(member))\n                multiverse.WriteOverrides(file_path, member, over_write_opts)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': self.scene_type,\n            'ext': self.scene_type,\n            'files': file_name,\n            'stagingDir': staging_dir\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance {} to {}\".format(\n            instance.name, file_path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py",
    "content": "import os\nimport six\n\nfrom maya import cmds\nfrom maya import mel\n\nimport pyblish.api\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.lib import maintained_selection\n\n\nclass ExtractMultiverseUsd(publish.Extractor):\n    \"\"\"Extractor for Multiverse USD Asset data.\n\n    This will extract settings for a Multiverse Write Asset operation:\n    they are visible in the Maya set node created by a Multiverse USD\n    Asset instance creator.\n\n    The input data contained in the set is:\n\n    - a single hierarchy of Maya nodes. Multiverse supports a variety of Maya\n      nodes such as transforms, mesh, curves, particles, instances, particle\n      instancers, pfx, MASH, lights, cameras, joints, connected materials,\n      shading networks etc. including many of their attributes.\n\n    Upon publish a .usd (or .usdz) asset file will be typically written.\n    \"\"\"\n\n    label = \"Extract Multiverse USD Asset\"\n    hosts = [\"maya\"]\n    families = [\"mvUsd\"]\n    scene_type = \"usd\"\n    file_formats = [\"usd\", \"usda\", \"usdz\"]\n\n    @property\n    def options(self):\n        \"\"\"Overridable options for Multiverse USD Export\n\n        Given in the following format\n            - {NAME: EXPECTED TYPE}\n\n        If the overridden option's type does not match,\n        the option is not included and a warning is logged.\n\n        \"\"\"\n\n        return {\n            \"stripNamespaces\": bool,\n            \"mergeTransformAndShape\": bool,\n            \"writeAncestors\": bool,\n            \"flattenParentXforms\": bool,\n            \"writeSparseOverrides\": bool,\n            \"useMetaPrimPath\": bool,\n            \"customRootPath\": str,\n            \"customAttributes\": str,\n            \"nodeTypesToIgnore\": str,\n            \"writeMeshes\": bool,\n            \"writeCurves\": bool,\n            \"writeParticles\": bool,\n            \"writeCameras\": bool,\n            \"writeLights\": bool,\n            \"writeJoints\": bool,\n            \"writeCollections\": bool,\n            \"writePositions\": bool,\n            \"writeNormals\": bool,\n            \"writeUVs\": bool,\n            \"writeColorSets\": bool,\n            \"writeTangents\": bool,\n            \"writeRefPositions\": bool,\n            \"writeBlendShapes\": bool,\n            \"writeDisplayColor\": bool,\n            \"writeSkinWeights\": bool,\n            \"writeMaterialAssignment\": bool,\n            \"writeHardwareShader\": bool,\n            \"writeShadingNetworks\": bool,\n            \"writeTransformMatrix\": bool,\n            \"writeUsdAttributes\": bool,\n            \"writeInstancesAsReferences\": bool,\n            \"timeVaryingTopology\": bool,\n            \"customMaterialNamespace\": str,\n            \"numTimeSamples\": int,\n            \"timeSamplesSpan\": float\n        }\n\n    @property\n    def default_options(self):\n        \"\"\"The default options for Multiverse USD extraction.\"\"\"\n\n        return {\n            \"stripNamespaces\": False,\n            \"mergeTransformAndShape\": False,\n            \"writeAncestors\": False,\n            \"flattenParentXforms\": False,\n            \"writeSparseOverrides\": False,\n            \"useMetaPrimPath\": False,\n            \"customRootPath\": str(),\n            \"customAttributes\": str(),\n            \"nodeTypesToIgnore\": str(),\n            \"writeMeshes\": True,\n            \"writeCurves\": True,\n            \"writeParticles\": True,\n            \"writeCameras\": False,\n            \"writeLights\": False,\n            \"writeJoints\": False,\n            \"writeCollections\": False,\n            \"writePositions\": True,\n            \"writeNormals\": True,\n            \"writeUVs\": True,\n            \"writeColorSets\": False,\n            \"writeTangents\": False,\n            \"writeRefPositions\": False,\n            \"writeBlendShapes\": False,\n            \"writeDisplayColor\": False,\n            \"writeSkinWeights\": False,\n            \"writeMaterialAssignment\": False,\n            \"writeHardwareShader\": False,\n            \"writeShadingNetworks\": False,\n            \"writeTransformMatrix\": True,\n            \"writeUsdAttributes\": False,\n            \"writeInstancesAsReferences\": False,\n            \"timeVaryingTopology\": False,\n            \"customMaterialNamespace\": str(),\n            \"numTimeSamples\": 1,\n            \"timeSamplesSpan\": 0.0\n        }\n\n    def parse_overrides(self, instance, options):\n        \"\"\"Inspect data of instance to determine overridden options\"\"\"\n\n        for key in instance.data:\n            if key not in self.options:\n                continue\n\n            # Ensure the data is of correct type\n            value = instance.data[key]\n            if isinstance(value, six.text_type):\n                value = str(value)\n            if not isinstance(value, self.options[key]):\n                self.log.warning(\n                    \"Overridden attribute {key} was of \"\n                    \"the wrong type: {invalid_type} \"\n                    \"- should have been {valid_type}\".format(\n                        key=key,\n                        invalid_type=type(value).__name__,\n                        valid_type=self.options[key].__name__))\n                continue\n\n            options[key] = value\n\n        return options\n\n    def get_default_options(self):\n        return self.default_options\n\n    def filter_members(self, members):\n        return members\n\n    def process(self, instance):\n\n        # Load plugin first\n        cmds.loadPlugin(\"MultiverseForMaya\", quiet=True)\n\n        # Define output file path\n        staging_dir = self.staging_dir(instance)\n        file_format = instance.data.get(\"fileFormat\", 0)\n        if file_format in range(len(self.file_formats)):\n            self.scene_type = self.file_formats[file_format]\n        file_name = \"{0}.{1}\".format(instance.name, self.scene_type)\n        file_path = os.path.join(staging_dir, file_name)\n        file_path = file_path.replace('\\\\', '/')\n\n        # Parse export options\n        options = self.get_default_options()\n        options = self.parse_overrides(instance, options)\n        self.log.debug(\"Export options: {0}\".format(options))\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction ...\")\n\n        with maintained_selection():\n            members = instance.data(\"setMembers\")\n            self.log.debug('Collected objects: {}'.format(members))\n            members = self.filter_members(members)\n            if not members:\n                self.log.error('No members!')\n                return\n            self.log.debug(' - filtered: {}'.format(members))\n\n            import multiverse\n\n            time_opts = None\n            frame_start = instance.data['frameStart']\n            frame_end = instance.data['frameEnd']\n            if frame_end != frame_start:\n                time_opts = multiverse.TimeOptions()\n\n                time_opts.writeTimeRange = True\n\n                handle_start = instance.data['handleStart']\n                handle_end = instance.data['handleEnd']\n\n                time_opts.frameRange = (\n                    frame_start - handle_start, frame_end + handle_end)\n                time_opts.frameIncrement = instance.data['step']\n                time_opts.numTimeSamples = instance.data.get(\n                    'numTimeSamples', options['numTimeSamples'])\n                time_opts.timeSamplesSpan = instance.data.get(\n                    'timeSamplesSpan', options['timeSamplesSpan'])\n                time_opts.framePerSecond = instance.data.get(\n                    'fps', mel.eval('currentTimeUnitToFPS()'))\n\n            asset_write_opts = multiverse.AssetWriteOptions(time_opts)\n            options_discard_keys = {\n                'numTimeSamples',\n                'timeSamplesSpan',\n                'frameStart',\n                'frameEnd',\n                'handleStart',\n                'handleEnd',\n                'step',\n                'fps'\n            }\n            self.log.debug(\"Write Options:\")\n            for key, value in options.items():\n                if key in options_discard_keys:\n                    continue\n\n                self.log.debug(\" - {}={}\".format(key, value))\n                setattr(asset_write_opts, key, value)\n\n            self.log.debug('WriteAsset: {} / {}'.format(file_path, members))\n            multiverse.WriteAsset(file_path, members, asset_write_opts)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': self.scene_type,\n            'ext': self.scene_type,\n            'files': file_name,\n            'stagingDir': staging_dir\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance {} to {}\".format(\n            instance.name, file_path))\n\n\nclass ExtractMultiverseUsdAnim(ExtractMultiverseUsd):\n    \"\"\"Extractor for Multiverse USD Animation Sparse Cache data.\n\n    This will extract the sparse cache data from the scene and generate a\n    USD file with all the animation data.\n\n    Upon publish a .usd sparse cache will be written.\n    \"\"\"\n    label = \"Extract Multiverse USD Animation Sparse Cache\"\n    families = [\"animation\", \"usd\"]\n    match = pyblish.api.Subset\n\n    def get_default_options(self):\n        anim_options = self.default_options\n        anim_options[\"writeSparseOverrides\"] = True\n        anim_options[\"writeUsdAttributes\"] = True\n        anim_options[\"stripNamespaces\"] = True\n        return anim_options\n\n    def filter_members(self, members):\n        out_set = next((i for i in members if i.endswith(\"out_SET\")), None)\n\n        if out_set is None:\n            self.log.warning(\"Expecting out_SET\")\n            return None\n\n        members = cmds.ls(cmds.sets(out_set, query=True), long=True)\n        return members\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_multiverse_usd_comp.py",
    "content": "import os\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.lib import maintained_selection\n\n\nclass ExtractMultiverseUsdComposition(publish.Extractor):\n    \"\"\"Extractor of Multiverse USD Composition data.\n\n    This will extract settings for a Multiverse Write Composition operation:\n    they are visible in the Maya set node created by a Multiverse USD\n    Composition instance creator.\n\n    The input data contained in the set is either:\n\n    - a single hierarchy consisting of several Multiverse Compound nodes, with\n      any number of layers, and Maya transform nodes\n    - a single Compound node with more than one layer (in this case the \"Write\n      as Compound Layers\" option should be set).\n\n    Upon publish a .usda composition file will be written.\n    \"\"\"\n\n    label = \"Extract Multiverse USD Composition\"\n    hosts = [\"maya\"]\n    families = [\"mvUsdComposition\"]\n    scene_type = \"usd\"\n    # Order of `fileFormat` must match create_multiverse_usd_comp.py\n    file_formats = [\"usda\", \"usd\"]\n\n    @property\n    def options(self):\n        \"\"\"Overridable options for Multiverse USD Export\n\n        Given in the following format\n            - {NAME: EXPECTED TYPE}\n\n        If the overridden option's type does not match,\n        the option is not included and a warning is logged.\n\n        \"\"\"\n\n        return {\n            \"stripNamespaces\": bool,\n            \"mergeTransformAndShape\": bool,\n            \"flattenContent\": bool,\n            \"writeAsCompoundLayers\": bool,\n            \"writePendingOverrides\": bool,\n            \"numTimeSamples\": int,\n            \"timeSamplesSpan\": float\n        }\n\n    @property\n    def default_options(self):\n        \"\"\"The default options for Multiverse USD extraction.\"\"\"\n\n        return {\n            \"stripNamespaces\": True,\n            \"mergeTransformAndShape\": False,\n            \"flattenContent\": False,\n            \"writeAsCompoundLayers\": False,\n            \"writePendingOverrides\": False,\n            \"numTimeSamples\": 1,\n            \"timeSamplesSpan\": 0.0\n        }\n\n    def parse_overrides(self, instance, options):\n        \"\"\"Inspect data of instance to determine overridden options\"\"\"\n\n        for key in instance.data:\n            if key not in self.options:\n                continue\n\n            # Ensure the data is of correct type\n            value = instance.data[key]\n            if not isinstance(value, self.options[key]):\n                self.log.warning(\n                    \"Overridden attribute {key} was of \"\n                    \"the wrong type: {invalid_type} \"\n                    \"- should have been {valid_type}\".format(\n                        key=key,\n                        invalid_type=type(value).__name__,\n                        valid_type=self.options[key].__name__))\n                continue\n\n            options[key] = value\n\n        return options\n\n    def process(self, instance):\n        # Load plugin first\n        cmds.loadPlugin(\"MultiverseForMaya\", quiet=True)\n\n        # Define output file path\n        staging_dir = self.staging_dir(instance)\n        file_format = instance.data.get(\"fileFormat\", 0)\n        if file_format in range(len(self.file_formats)):\n            self.scene_type = self.file_formats[file_format]\n        file_name = \"{0}.{1}\".format(instance.name, self.scene_type)\n        file_path = os.path.join(staging_dir, file_name)\n        file_path = file_path.replace('\\\\', '/')\n\n        # Parse export options\n        options = self.default_options\n        options = self.parse_overrides(instance, options)\n        self.log.debug(\"Export options: {0}\".format(options))\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction ...\")\n\n        with maintained_selection():\n            members = instance.data(\"setMembers\")\n            self.log.debug('Collected object {}'.format(members))\n\n            import multiverse\n\n            time_opts = None\n            frame_start = instance.data['frameStart']\n            frame_end = instance.data['frameEnd']\n            handle_start = instance.data['handleStart']\n            handle_end = instance.data['handleEnd']\n            step = instance.data['step']\n            fps = instance.data['fps']\n            if frame_end != frame_start:\n                time_opts = multiverse.TimeOptions()\n\n                time_opts.writeTimeRange = True\n                time_opts.frameRange = (\n                    frame_start - handle_start, frame_end + handle_end)\n                time_opts.frameIncrement = step\n                time_opts.numTimeSamples = instance.data[\"numTimeSamples\"]\n                time_opts.timeSamplesSpan = instance.data[\"timeSamplesSpan\"]\n                time_opts.framePerSecond = fps\n\n            comp_write_opts = multiverse.CompositionWriteOptions()\n\n            \"\"\"\n            OP tells MV to write to a staging directory, and then moves the\n            file to it's final publish directory. By default, MV write relative\n            paths, but these paths will break when the referencing file moves.\n            This option forces writes to absolute paths, which is ok within OP\n            because all published assets have static paths, and MV can only\n            reference published assets. When a proper UsdAssetResolver is used,\n            this won't be needed.\n            \"\"\"\n            comp_write_opts.forceAbsolutePaths = True\n\n            options_discard_keys = {\n                'numTimeSamples',\n                'timeSamplesSpan',\n                'frameStart',\n                'frameEnd',\n                'handleStart',\n                'handleEnd',\n                'step',\n                'fps'\n            }\n            for key, value in options.items():\n                if key in options_discard_keys:\n                    continue\n                setattr(comp_write_opts, key, value)\n\n            multiverse.WriteComposition(file_path, members, comp_write_opts)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': self.scene_type,\n            'ext': self.scene_type,\n            'files': file_name,\n            'stagingDir': staging_dir\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance {} to {}\".format(instance.name,\n                                                            file_path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py",
    "content": "import os\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.lib import maintained_selection\n\nfrom maya import cmds\n\n\nclass ExtractMultiverseUsdOverride(publish.Extractor):\n    \"\"\"Extractor for Multiverse USD Override data.\n\n    This will extract settings for a Multiverse Write Override operation:\n    they are visible in the Maya set node created by a Multiverse USD\n    Override instance creator.\n\n    The input data contained in the set is:\n\n    - a single Multiverse Compound node with any number of overrides (typically\n      set in MEOW)\n\n    Upon publish a .usda override file will be written.\n    \"\"\"\n\n    label = \"Extract Multiverse USD Override\"\n    hosts = [\"maya\"]\n    families = [\"mvUsdOverride\"]\n    scene_type = \"usd\"\n    # Order of `fileFormat` must match create_multiverse_usd_over.py\n    file_formats = [\"usda\", \"usd\"]\n\n    @property\n    def options(self):\n        \"\"\"Overridable options for Multiverse USD Export\n\n        Given in the following format\n            - {NAME: EXPECTED TYPE}\n\n        If the overridden option's type does not match,\n        the option is not included and a warning is logged.\n\n        \"\"\"\n\n        return {\n            \"writeAll\": bool,\n            \"writeTransforms\": bool,\n            \"writeVisibility\": bool,\n            \"writeAttributes\": bool,\n            \"writeMaterials\": bool,\n            \"writeVariants\": bool,\n            \"writeVariantsDefinition\": bool,\n            \"writeActiveState\": bool,\n            \"writeNamespaces\": bool,\n            \"numTimeSamples\": int,\n            \"timeSamplesSpan\": float\n        }\n\n    @property\n    def default_options(self):\n        \"\"\"The default options for Multiverse USD extraction.\"\"\"\n\n        return {\n            \"writeAll\": False,\n            \"writeTransforms\": True,\n            \"writeVisibility\": True,\n            \"writeAttributes\": True,\n            \"writeMaterials\": True,\n            \"writeVariants\": True,\n            \"writeVariantsDefinition\": True,\n            \"writeActiveState\": True,\n            \"writeNamespaces\": False,\n            \"numTimeSamples\": 1,\n            \"timeSamplesSpan\": 0.0\n        }\n\n    def process(self, instance):\n        # Load plugin first\n        cmds.loadPlugin(\"MultiverseForMaya\", quiet=True)\n\n        # Define output file path\n        staging_dir = self.staging_dir(instance)\n        file_format = instance.data.get(\"fileFormat\", 0)\n        if file_format in range(len(self.file_formats)):\n            self.scene_type = self.file_formats[file_format]\n        file_name = \"{0}.{1}\".format(instance.name, self.scene_type)\n        file_path = os.path.join(staging_dir, file_name)\n        file_path = file_path.replace(\"\\\\\", \"/\")\n\n        # Parse export options\n        options = self.default_options\n        self.log.debug(\"Export options: {0}\".format(options))\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction ...\")\n\n        with maintained_selection():\n            members = instance.data(\"setMembers\")\n            members = cmds.ls(members,\n                              dag=True,\n                              shapes=False,\n                              type=\"mvUsdCompoundShape\",\n                              noIntermediate=True,\n                              long=True)\n            self.log.debug(\"Collected object {}\".format(members))\n\n            # TODO: Deal with asset, composition, override with options.\n            import multiverse\n\n            time_opts = None\n            frame_start = instance.data[\"frameStart\"]\n            frame_end = instance.data[\"frameEnd\"]\n            handle_start = instance.data[\"handleStart\"]\n            handle_end = instance.data[\"handleEnd\"]\n            step = instance.data[\"step\"]\n            fps = instance.data[\"fps\"]\n            if frame_end != frame_start:\n                time_opts = multiverse.TimeOptions()\n\n                time_opts.writeTimeRange = True\n                time_opts.frameRange = (\n                    frame_start - handle_start, frame_end + handle_end)\n                time_opts.frameIncrement = step\n                time_opts.numTimeSamples = instance.data[\"numTimeSamples\"]\n                time_opts.timeSamplesSpan = instance.data[\"timeSamplesSpan\"]\n                time_opts.framePerSecond = fps\n\n            over_write_opts = multiverse.OverridesWriteOptions(time_opts)\n            options_discard_keys = {\n                \"numTimeSamples\",\n                \"timeSamplesSpan\",\n                \"frameStart\",\n                \"frameEnd\",\n                \"handleStart\",\n                \"handleEnd\",\n                \"step\",\n                \"fps\"\n            }\n            for key, value in options.items():\n                if key in options_discard_keys:\n                    continue\n                setattr(over_write_opts, key, value)\n\n            for member in members:\n                multiverse.WriteOverrides(file_path, member, over_write_opts)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': self.scene_type,\n            'ext': self.scene_type,\n            'files': file_name,\n            'stagingDir': staging_dir\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance {} to {}\".format(\n            instance.name, file_path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_obj.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\n\nfrom maya import cmds\nimport pyblish.api\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api import lib\n\n\nclass ExtractObj(publish.Extractor):\n    \"\"\"Extract OBJ from Maya.\n\n    This extracts reproducible OBJ exports ignoring any of the settings\n    set on the local machine in the OBJ export options window.\n\n    \"\"\"\n    order = pyblish.api.ExtractorOrder\n    hosts = [\"maya\"]\n    label = \"Extract OBJ\"\n    families = [\"model\"]\n\n    def process(self, instance):\n\n        # Define output path\n\n        staging_dir = self.staging_dir(instance)\n        filename = \"{0}.obj\".format(instance.name)\n        path = os.path.join(staging_dir, filename)\n\n        # The export requires forward slashes because we need to\n        # format it into a string in a mel expression\n\n        self.log.debug(\"Extracting OBJ to: {0}\".format(path))\n\n        members = instance.data(\"setMembers\")\n        members = cmds.ls(members,\n                          dag=True,\n                          shapes=True,\n                          type=(\"mesh\", \"nurbsCurve\"),\n                          noIntermediate=True,\n                          long=True)\n        self.log.debug(\"Members: {0}\".format(members))\n        self.log.debug(\"Instance: {0}\".format(instance[:]))\n\n        if not cmds.pluginInfo('objExport', query=True, loaded=True):\n            cmds.loadPlugin('objExport')\n\n        # Export\n        with lib.no_display_layers(instance):\n            with lib.displaySmoothness(members,\n                                       divisionsU=0,\n                                       divisionsV=0,\n                                       pointsWire=4,\n                                       pointsShaded=1,\n                                       polygonObject=1):\n                with lib.shader(members,\n                                shadingEngine=\"initialShadingGroup\"):\n                    with lib.maintained_selection():\n                        cmds.select(members, noExpand=True)\n                        cmds.file(path,\n                                  exportSelected=True,\n                                  type='OBJexport',\n                                  preserveReferences=True,\n                                  force=True)\n\n        if \"representation\" not in instance.data:\n            instance.data[\"representation\"] = []\n\n        representation = {\n            'name': 'obj',\n            'ext': 'obj',\n            'files': filename,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extract OBJ successful to: {0}\".format(path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_playblast.py",
    "content": "import os\n\nimport clique\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api import lib\n\nfrom maya import cmds\n\n\nclass ExtractPlayblast(publish.Extractor):\n    \"\"\"Extract viewport playblast.\n\n    Takes review camera and creates review Quicktime video based on viewport\n    capture.\n\n    \"\"\"\n\n    label = \"Extract Playblast\"\n    hosts = [\"maya\"]\n    families = [\"review\"]\n    optional = True\n    capture_preset = {}\n    profiles = None\n\n    def process(self, instance):\n        self.log.debug(\"Extracting playblast..\")\n\n        # get scene fps\n        fps = instance.data.get(\"fps\") or instance.context.data.get(\"fps\")\n\n        # if start and end frames cannot be determined, get them\n        # from Maya timeline\n        start = instance.data.get(\"frameStartFtrack\")\n        end = instance.data.get(\"frameEndFtrack\")\n        if start is None:\n            start = cmds.playbackOptions(query=True, animationStartTime=True)\n        if end is None:\n            end = cmds.playbackOptions(query=True, animationEndTime=True)\n\n        self.log.debug(\"start: {}, end: {}\".format(start, end))\n        task_data = instance.data[\"anatomyData\"].get(\"task\", {})\n        capture_preset = lib.get_capture_preset(\n            task_data.get(\"name\"),\n            task_data.get(\"type\"),\n            instance.data[\"subset\"],\n            instance.context.data[\"project_settings\"],\n            self.log\n        )\n        stagingdir = self.staging_dir(instance)\n        filename = instance.name\n        path = os.path.join(stagingdir, filename)\n        self.log.debug(\"Outputting images to %s\" % path)\n        # get cameras\n        camera = instance.data[\"review_camera\"]\n        preset = lib.generate_capture_preset(\n            instance, camera, path,\n            start=start, end=end,\n            capture_preset=capture_preset)\n        lib.render_capture_preset(preset)\n\n        # Find playblast sequence\n        collected_files = os.listdir(stagingdir)\n        patterns = [clique.PATTERNS[\"frames\"]]\n        collections, remainder = clique.assemble(collected_files,\n                                                 minimum_items=1,\n                                                 patterns=patterns)\n\n        self.log.debug(\"Searching playblast collection for: %s\", path)\n        frame_collection = None\n        for collection in collections:\n            filebase = collection.format(\"{head}\").rstrip(\".\")\n            self.log.debug(\"Checking collection head: %s\", filebase)\n            if filebase in path:\n                frame_collection = collection\n                self.log.debug(\n                    \"Found playblast collection: %s\", frame_collection\n                )\n\n        tags = [\"review\"]\n        if not instance.data.get(\"keepImages\"):\n            tags.append(\"delete\")\n\n        # Add camera node name to representation data\n        camera_node_name = cmds.listRelatives(camera, parent=True)[0]\n\n        collected_files = list(frame_collection)\n        # single frame file shouldn't be in list, only as a string\n        if len(collected_files) == 1:\n            collected_files = collected_files[0]\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            \"name\": capture_preset[\"Codec\"][\"compression\"],\n            \"ext\": capture_preset[\"Codec\"][\"compression\"],\n            \"files\": collected_files,\n            \"stagingDir\": stagingdir,\n            \"frameStart\": int(start),\n            \"frameEnd\": int(end),\n            \"fps\": fps,\n            \"tags\": tags,\n            \"camera_name\": camera_node_name\n        }\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_pointcache.py",
    "content": "import os\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.alembic import extract_alembic\nfrom openpype.hosts.maya.api.lib import (\n    suspended_refresh,\n    maintained_selection,\n    iter_visible_nodes_in_range,\n)\nfrom openpype.lib import (\n    BoolDef,\n    TextDef,\n    NumberDef,\n    EnumDef,\n    UISeparatorDef,\n    UILabelDef,\n)\nfrom openpype.pipeline.publish import OpenPypePyblishPluginMixin\n\n\nclass ExtractAlembic(publish.Extractor, OpenPypePyblishPluginMixin):\n    \"\"\"Produce an alembic of just point positions and normals.\n\n    Positions and normals, uvs, creases are preserved, but nothing more,\n    for plain and predictable point caches.\n\n    Plugin can run locally or remotely (on a farm - if instance is marked with\n    \"farm\" it will be skipped in local processing, but processed on farm)\n    \"\"\"\n\n    label = \"Extract Pointcache (Alembic)\"\n    hosts = [\"maya\"]\n    families = [\"pointcache\", \"model\", \"vrayproxy.alembic\"]\n    targets = [\"local\", \"remote\"]\n    flags = []\n    attr = []\n    attrPrefix = []\n    dataFormat = \"ogawa\"\n    melPerFrameCallback = \"\"\n    melPostJobCallback = \"\"\n    preRollStartFrame = 0\n    pythonPerFrameCallback = \"\"\n    pythonPostJobCallback = \"\"\n    userAttr = \"\"\n    userAttrPrefix = \"\"\n    visibleOnly = False\n    overrides = []\n\n    def process(self, instance):\n        if instance.data.get(\"farm\"):\n            self.log.debug(\"Should be processed on farm, skipping.\")\n            return\n\n        nodes, roots = self.get_members_and_roots(instance)\n\n        # Collect the start and end including handles\n        start = float(instance.data.get(\"frameStartHandle\", 1))\n        end = float(instance.data.get(\"frameEndHandle\", 1))\n\n        attribute_values = self.get_attr_values_from_data(\n            instance.data\n        )\n\n        attrs = [\n            attr.strip()\n            for attr in attribute_values.get(\"attr\", \"\").split(\";\")\n            if attr.strip()\n        ]\n        attrs += instance.data.get(\"userDefinedAttributes\", [])\n        attrs += [\"cbId\"]\n\n        attr_prefixes = [\n            attr.strip()\n            for attr in attribute_values.get(\"attrPrefix\", \"\").split(\";\")\n            if attr.strip()\n        ]\n\n        self.log.debug(\"Extracting pointcache...\")\n        dirname = self.staging_dir(instance)\n\n        parent_dir = self.staging_dir(instance)\n        filename = \"{name}.abc\".format(**instance.data)\n        path = os.path.join(parent_dir, filename)\n\n        root = None\n        if not instance.data.get(\"includeParentHierarchy\", True):\n            # Set the root nodes if we don't want to include parents\n            # The roots are to be considered the ones that are the actual\n            # direct members of the set\n            root = roots\n\n        args = {\n            \"file\": path,\n            \"attr\": attrs,\n            \"attrPrefix\": attr_prefixes,\n            \"dataFormat\": attribute_values.get(\"dataFormat\", \"ogawa\"),\n            \"endFrame\": end,\n            \"eulerFilter\": False,\n            \"noNormals\": False,\n            \"preRoll\": False,\n            \"preRollStartFrame\": attribute_values.get(\n                \"preRollStartFrame\", 0\n            ),\n            \"renderableOnly\": False,\n            \"root\": root,\n            \"selection\": True,\n            \"startFrame\": start,\n            \"step\": instance.data.get(\n                \"creator_attributes\", {}\n            ).get(\"step\", 1.0),\n            \"stripNamespaces\": False,\n            \"uvWrite\": False,\n            \"verbose\": False,\n            \"wholeFrameGeo\": False,\n            \"worldSpace\": False,\n            \"writeColorSets\": False,\n            \"writeCreases\": False,\n            \"writeFaceSets\": False,\n            \"writeUVSets\": False,\n            \"writeVisibility\": False,\n        }\n\n        # Export flags are defined as default enabled flags plus publisher\n        # enabled flags.\n        non_exposed_flags = list(set(self.flags) - set(self.overrides))\n        flags = attribute_values[\"flags\"] + non_exposed_flags\n        for flag in flags:\n            args[flag] = True\n\n        if instance.data.get(\"visibleOnly\", False):\n            # If we only want to include nodes that are visible in the frame\n            # range then we need to do our own check. Alembic's `visibleOnly`\n            # flag does not filter out those that are only hidden on some\n            # frames as it counts \"animated\" or \"connected\" visibilities as\n            # if it's always visible.\n            nodes = list(\n                iter_visible_nodes_in_range(nodes, start=start, end=end)\n            )\n\n        suspend = not instance.data.get(\"refresh\", False)\n        with suspended_refresh(suspend=suspend):\n            with maintained_selection():\n                cmds.select(nodes, noExpand=True)\n                self.log.debug(\n                    \"Running `extract_alembic` with the arguments: {}\".format(\n                        args\n                    )\n                )\n                extract_alembic(**args)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            \"name\": \"abc\",\n            \"ext\": \"abc\",\n            \"files\": filename,\n            \"stagingDir\": dirname,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        if not instance.data.get(\"stagingDir_persistent\", False):\n            instance.context.data[\"cleanupFullPaths\"].append(path)\n\n        self.log.debug(\"Extracted {} to {}\".format(instance, dirname))\n\n        # Extract proxy.\n        if not instance.data.get(\"proxy\"):\n            self.log.debug(\"No proxy nodes found. Skipping proxy extraction.\")\n            return\n\n        path = path.replace(\".abc\", \"_proxy.abc\")\n        args[\"file\"] = path\n        if not instance.data.get(\"includeParentHierarchy\", True):\n            # Set the root nodes if we don't want to include parents\n            # The roots are to be considered the ones that are the actual\n            # direct members of the set\n            args[\"root\"] = instance.data[\"proxyRoots\"]\n\n        with suspended_refresh(suspend=suspend):\n            with maintained_selection():\n                cmds.select(instance.data[\"proxy\"])\n                extract_alembic(**args)\n\n        representation = {\n            \"name\": \"proxy\",\n            \"ext\": \"abc\",\n            \"files\": os.path.basename(path),\n            \"stagingDir\": dirname,\n            \"outputName\": \"proxy\",\n        }\n        instance.data[\"representations\"].append(representation)\n\n    def get_members_and_roots(self, instance):\n        return instance[:], instance.data.get(\"setMembers\")\n\n    @classmethod\n    def get_attribute_defs(cls):\n        override_defs = {\n            \"attr\": {\n                \"def\": TextDef,\n                \"kwargs\": {\n                    \"label\": \"Custom Attributes\",\n                    \"placeholder\": \"attr1; attr2; ...\",\n                }\n            },\n            \"attrPrefix\": {\n                \"def\": TextDef,\n                \"kwargs\": {\n                    \"label\": \"Custom Attributes Prefix\",\n                    \"placeholder\": \"prefix1; prefix2; ...\",\n                }\n            },\n            \"dataFormat\": {\n                \"def\": EnumDef,\n                \"kwargs\": {\n                    \"label\": \"Data Format\",\n                    \"items\": [\"ogawa\", \"HDF\"],\n                }\n            },\n            \"melPerFrameCallback\": {\n                \"def\": TextDef,\n                \"kwargs\": {\n                    \"label\": \"melPerFrameCallback\",\n                }\n            },\n            \"melPostJobCallback\": {\n                \"def\": TextDef,\n                \"kwargs\": {\n                    \"label\": \"melPostJobCallback\",\n                }\n            },\n            \"preRollStartFrame\": {\n                \"def\": NumberDef,\n                \"kwargs\": {\n                    \"label\": \"Start frame for preroll\",\n                    \"tooltip\": (\n                        \"The frame to start scene evaluation at. This is used\"\n                        \" to set the starting frame for time dependent \"\n                        \"translations and can be used to evaluate run-up that\"\n                        \" isn't actually translated.\"\n                    ),\n                }\n            },\n            \"pythonPerFrameCallback\": {\n                \"def\": TextDef,\n                \"kwargs\": {\n                    \"label\": \"pythonPerFrameCallback\",\n                }\n            },\n            \"pythonPostJobCallback\": {\n                \"def\": TextDef,\n                \"kwargs\": {\n                    \"label\": \"pythonPostJobCallback\",\n                }\n            },\n            \"userAttr\": {\n                \"def\": TextDef,\n                \"kwargs\": {\n                    \"label\": \"userAttr\",\n                }\n            },\n            \"userAttrPrefix\": {\n                \"def\": TextDef,\n                \"kwargs\": {\n                    \"label\": \"userAttrPrefix\",\n                }\n            },\n            \"visibleOnly\": {\n                \"def\": BoolDef,\n                \"kwargs\": {\n                    \"label\": \"Visible Only\",\n                }\n            }\n        }\n\n        defs = super(ExtractAlembic, cls).get_attribute_defs()\n\n        defs.extend([\n            UISeparatorDef(\"sep_alembic_options\"),\n            UILabelDef(\"Alembic Options\"),\n        ])\n\n        # The Arguments that can be modified by the Publisher\n        overrides = set(getattr(cls, \"overrides\", set()))\n\n        # What we have set in the Settings as defaults.\n        flags = set(getattr(cls, \"flags\", set()))\n\n        enabled_flags = [x for x in flags if x in overrides]\n        flags = overrides - set(override_defs.keys())\n        if flags:\n            defs.append(\n                EnumDef(\n                    \"flags\",\n                    flags,\n                    default=enabled_flags,\n                    multiselection=True,\n                    label=\"Export Flags\",\n                )\n            )\n\n        for key, value in override_defs.items():\n            if key not in overrides:\n                continue\n\n            kwargs = value[\"kwargs\"]\n            kwargs[\"default\"] = getattr(cls, key, None)\n            defs.append(\n                value[\"def\"](key, **value[\"kwargs\"])\n            )\n\n        defs.append(\n            UISeparatorDef(\"sep_alembic_options\")\n        )\n\n        return defs\n\n\nclass ExtractAnimation(ExtractAlembic):\n    label = \"Extract Animation (Alembic)\"\n    families = [\"animation\"]\n\n    def get_members_and_roots(self, instance):\n        # Collect the out set nodes\n        out_sets = [node for node in instance if node.endswith(\"out_SET\")]\n        if len(out_sets) != 1:\n            raise RuntimeError(\n                \"Couldn't find exactly one out_SET: \" \"{0}\".format(out_sets)\n            )\n        out_set = out_sets[0]\n        roots = cmds.sets(out_set, query=True)\n\n        # Include all descendants\n        nodes = (\n            roots\n            + cmds.listRelatives(roots, allDescendents=True, fullPath=True)\n            or []\n        )\n\n        return nodes, roots\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_proxy_abc.py",
    "content": "import os\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.alembic import extract_alembic\nfrom openpype.hosts.maya.api.lib import (\n    suspended_refresh,\n    maintained_selection,\n    iter_visible_nodes_in_range\n)\n\n\nclass ExtractProxyAlembic(publish.Extractor):\n    \"\"\"Produce an alembic for bounding box geometry\n    \"\"\"\n\n    label = \"Extract Proxy (Alembic)\"\n    hosts = [\"maya\"]\n    families = [\"proxyAbc\"]\n\n    def process(self, instance):\n        name_suffix = instance.data.get(\"nameSuffix\")\n        # Collect the start and end including handles\n        start = float(instance.data.get(\"frameStartHandle\", 1))\n        end = float(instance.data.get(\"frameEndHandle\", 1))\n\n        attrs = instance.data.get(\"attr\", \"\").split(\";\")\n        attrs = [value for value in attrs if value.strip()]\n        attrs += [\"cbId\"]\n\n        attr_prefixes = instance.data.get(\"attrPrefix\", \"\").split(\";\")\n        attr_prefixes = [value for value in attr_prefixes if value.strip()]\n\n        self.log.debug(\"Extracting Proxy Alembic..\")\n        dirname = self.staging_dir(instance)\n\n        filename = \"{name}.abc\".format(**instance.data)\n        path = os.path.join(dirname, filename)\n\n        proxy_root = self.create_proxy_geometry(instance,\n                                                name_suffix,\n                                                start,\n                                                end)\n\n        options = {\n            \"step\": instance.data.get(\"step\", 1.0),\n            \"attr\": attrs,\n            \"attrPrefix\": attr_prefixes,\n            \"writeVisibility\": True,\n            \"writeCreases\": True,\n            \"writeColorSets\": instance.data.get(\"writeColorSets\", False),\n            \"writeFaceSets\": instance.data.get(\"writeFaceSets\", False),\n            \"uvWrite\": True,\n            \"selection\": True,\n            \"worldSpace\": instance.data.get(\"worldSpace\", True),\n            \"root\": proxy_root\n        }\n\n        if int(cmds.about(version=True)) >= 2017:\n            # Since Maya 2017 alembic supports multiple uv sets - write them.\n            options[\"writeUVSets\"] = True\n\n        with suspended_refresh():\n            with maintained_selection():\n                cmds.select(proxy_root, hi=True, noExpand=True)\n                extract_alembic(file=path,\n                                startFrame=start,\n                                endFrame=end,\n                                **options)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'abc',\n            'ext': 'abc',\n            'files': filename,\n            \"stagingDir\": dirname\n        }\n        instance.data[\"representations\"].append(representation)\n\n        if not instance.data.get(\"stagingDir_persistent\", False):\n            instance.context.data[\"cleanupFullPaths\"].append(path)\n\n        self.log.debug(\"Extracted {} to {}\".format(instance, dirname))\n        # remove the bounding box\n        bbox_master = cmds.ls(\"bbox_grp\")\n        cmds.delete(bbox_master)\n\n    def create_proxy_geometry(self, instance, name_suffix, start, end):\n        nodes = instance[:]\n        nodes = list(iter_visible_nodes_in_range(nodes,\n                                                 start=start,\n                                                 end=end))\n\n        inst_selection = cmds.ls(nodes, long=True)\n        cmds.geomToBBox(inst_selection,\n                        nameSuffix=name_suffix,\n                        keepOriginal=True,\n                        single=False,\n                        bakeAnimation=True,\n                        startTime=start,\n                        endTime=end)\n        # create master group for bounding\n        # boxes as the main root\n        master_group = cmds.group(name=\"bbox_grp\")\n        bbox_sel = cmds.ls(master_group, long=True)\n        self.log.debug(\"proxy_root: {}\".format(bbox_sel))\n        return bbox_sel\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Redshift Proxy extractor.\"\"\"\nimport os\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.lib import maintained_selection\n\n\nclass ExtractRedshiftProxy(publish.Extractor):\n    \"\"\"Extract the content of the instance to a redshift proxy file.\"\"\"\n\n    label = \"Redshift Proxy (.rs)\"\n    hosts = [\"maya\"]\n    families = [\"redshiftproxy\"]\n\n    def process(self, instance):\n        \"\"\"Extractor entry point.\"\"\"\n\n        staging_dir = self.staging_dir(instance)\n        file_name = \"{}.rs\".format(instance.name)\n        file_path = os.path.join(staging_dir, file_name)\n\n        anim_on = instance.data[\"animation\"]\n        rs_options = \"exportConnectivity=0;enableCompression=1;keepUnused=0;\"\n        repr_files = file_name\n\n        if not anim_on:\n            # Remove animation information because it is not required for\n            # non-animated subsets\n            keys = [\"frameStart\",\n                    \"frameEnd\",\n                    \"handleStart\",\n                    \"handleEnd\",\n                    \"frameStartHandle\",\n                    \"frameEndHandle\"]\n            for key in keys:\n                instance.data.pop(key, None)\n\n        else:\n            start_frame = instance.data[\"frameStartHandle\"]\n            end_frame = instance.data[\"frameEndHandle\"]\n            rs_options = \"{}startFrame={};endFrame={};frameStep={};\".format(\n                rs_options, start_frame,\n                end_frame, instance.data[\"step\"]\n            )\n\n            root, ext = os.path.splitext(file_path)\n            # Padding is taken from number of digits of the end_frame.\n            # Not sure where Redshift is taking it.\n            repr_files = [\n                \"{}.{}{}\".format(os.path.basename(root), str(frame).rjust(4, \"0\"), ext)     # noqa: E501\n                for frame in range(\n                    int(start_frame),\n                    int(end_frame) + 1,\n                    int(instance.data[\"step\"])\n            )]\n        # vertex_colors = instance.data.get(\"vertexColors\", False)\n\n        # Write out rs file\n        self.log.debug(\"Writing: '%s'\" % file_path)\n        with maintained_selection():\n            cmds.select(instance.data[\"setMembers\"], noExpand=True)\n            cmds.file(file_path,\n                      pr=False,\n                      force=True,\n                      type=\"Redshift Proxy\",\n                      exportSelected=True,\n                      options=rs_options)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        self.log.debug(\"Files: {}\".format(repr_files))\n\n        representation = {\n            'name': 'rs',\n            'ext': 'rs',\n            'files': repr_files,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\"\n                       % (instance.name, staging_dir))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_rendersetup.py",
    "content": "import os\nimport json\n\nimport maya.app.renderSetup.model.renderSetup as renderSetup\nfrom openpype.pipeline import publish\n\n\nclass ExtractRenderSetup(publish.Extractor):\n    \"\"\"\n    Produce renderSetup template file\n\n    This will save whole renderSetup to json file for later use.\n    \"\"\"\n\n    label = \"Extract RenderSetup\"\n    hosts = [\"maya\"]\n    families = [\"rendersetup\"]\n\n    def process(self, instance):\n        parent_dir = self.staging_dir(instance)\n        json_filename = \"{}.json\".format(instance.name)\n        json_path = os.path.join(parent_dir, json_filename)\n\n        with open(json_path, \"w+\") as file:\n            json.dump(\n                renderSetup.instance().encode(None),\n                fp=file, indent=2, sort_keys=True)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'json',\n            'ext': 'json',\n            'files': json_filename,\n            \"stagingDir\": parent_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\n            \"Extracted instance '%s' to: %s\" % (instance.name, json_path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_rig.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract rig as Maya Scene.\"\"\"\nimport os\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.lib import maintained_selection\n\n\nclass ExtractRig(publish.Extractor):\n    \"\"\"Extract rig as Maya Scene.\"\"\"\n\n    label = \"Extract Rig (Maya Scene)\"\n    hosts = [\"maya\"]\n    families = [\"rig\"]\n    scene_type = \"ma\"\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        ext_mapping = (\n            instance.context.data[\"project_settings\"][\"maya\"][\"ext_mapping\"]\n        )\n        if ext_mapping:\n            self.log.debug(\"Looking in settings for scene type ...\")\n            # use extension mapping for first family found\n            for family in self.families:\n                try:\n                    self.scene_type = ext_mapping[family]\n                    self.log.debug(\n                        \"Using '.{}' as scene type\".format(self.scene_type))\n                    break\n                except AttributeError:\n                    # no preset found\n                    pass\n        # Define extract output file path\n        dir_path = self.staging_dir(instance)\n        filename = \"{0}.{1}\".format(instance.name, self.scene_type)\n        path = os.path.join(dir_path, filename)\n\n        # Perform extraction\n        self.log.debug(\"Performing extraction ...\")\n        with maintained_selection():\n            cmds.select(instance, noExpand=True)\n            cmds.file(path,\n                      force=True,\n                      typ=\"mayaAscii\" if self.scene_type == \"ma\" else \"mayaBinary\",  # noqa: E501\n                      exportSelected=True,\n                      preserveReferences=False,\n                      channels=True,\n                      constraints=True,\n                      expressions=True,\n                      constructionHistory=True)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': self.scene_type,\n            'ext': self.scene_type,\n            'files': filename,\n            \"stagingDir\": dir_path\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\", instance.name, path)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\n\nfrom maya import cmds  # noqa\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.pipeline.publish import OptionalPyblishPluginMixin\nfrom openpype.hosts.maya.api import fbx\n\n\nclass ExtractSkeletonMesh(publish.Extractor,\n                          OptionalPyblishPluginMixin):\n    \"\"\"Extract Rig in FBX format from Maya.\n\n    This extracts the rig in fbx with the constraints\n    and referenced asset content included.\n    This also optionally extract animated rig in fbx with\n    geometries included.\n\n    \"\"\"\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract Skeleton Mesh\"\n    hosts = [\"maya\"]\n    families = [\"rig.fbx\"]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        # Define output path\n        staging_dir = self.staging_dir(instance)\n        filename = \"{0}.fbx\".format(instance.name)\n        path = os.path.join(staging_dir, filename)\n\n        fbx_exporter = fbx.FBXExtractor(log=self.log)\n        out_set = instance.data.get(\"skeleton_mesh\", [])\n\n        instance.data[\"constraints\"] = True\n        instance.data[\"skeletonDefinitions\"] = True\n\n        fbx_exporter.set_options_from_instance(instance)\n\n        # Export\n        fbx_exporter.export(out_set, path)\n\n        representations = instance.data.setdefault(\"representations\", [])\n        representations.append({\n            'name': 'fbx',\n            'ext': 'fbx',\n            'files': filename,\n            \"stagingDir\": staging_dir\n        })\n\n        self.log.debug(\"Extract FBX to: {0}\".format(path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_thumbnail.py",
    "content": "import os\nimport glob\nimport tempfile\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api import lib\n\n\nclass ExtractThumbnail(publish.Extractor):\n    \"\"\"Extract viewport thumbnail.\n\n    Takes review camera and creates a thumbnail based on viewport\n    capture.\n\n    \"\"\"\n\n    label = \"Thumbnail\"\n    hosts = [\"maya\"]\n    families = [\"review\"]\n\n    def process(self, instance):\n        self.log.debug(\"Extracting thumbnail..\")\n\n        camera = instance.data[\"review_camera\"]\n\n        task_data = instance.data[\"anatomyData\"].get(\"task\", {})\n        capture_preset = lib.get_capture_preset(\n            task_data.get(\"name\"),\n            task_data.get(\"type\"),\n            instance.data[\"subset\"],\n            instance.context.data[\"project_settings\"],\n            self.log\n        )\n\n        # Create temp directory for thumbnail\n        # - this is to avoid \"override\" of source file\n        dst_staging = tempfile.mkdtemp(prefix=\"pyblish_tmp_thumbnail\")\n        self.log.debug(\n            \"Create temp directory {} for thumbnail\".format(dst_staging)\n        )\n        # Store new staging to cleanup paths\n        filename = instance.name\n        path = os.path.join(dst_staging, filename)\n\n        self.log.debug(\"Outputting images to %s\" % path)\n\n        preset = lib.generate_capture_preset(\n            instance, camera, path,\n            start=1, end=1,\n            capture_preset=capture_preset)\n\n        preset[\"camera_options\"].update({\n            \"displayGateMask\": False,\n            \"displayResolution\": False,\n            \"displayFilmGate\": False,\n            \"displayFieldChart\": False,\n            \"displaySafeAction\": False,\n            \"displaySafeTitle\": False,\n            \"displayFilmPivot\": False,\n            \"displayFilmOrigin\": False,\n            \"overscan\": 1.0,\n        })\n        path = lib.render_capture_preset(preset)\n\n        playblast = self._fix_playblast_output_path(path)\n\n        _, thumbnail = os.path.split(playblast)\n\n        self.log.debug(\"file list  {}\".format(thumbnail))\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            \"name\": \"thumbnail\",\n            \"ext\": \"jpg\",\n            \"files\": thumbnail,\n            \"stagingDir\": dst_staging,\n            \"thumbnail\": True\n        }\n        instance.data[\"representations\"].append(representation)\n\n    def _fix_playblast_output_path(self, filepath):\n        \"\"\"Workaround a bug in maya.cmds.playblast to return correct filepath.\n\n        When the `viewer` argument is set to False and maya.cmds.playblast\n        does not automatically open the playblasted file the returned\n        filepath does not have the file's extension added correctly.\n\n        To workaround this we just glob.glob() for any file extensions and\n         assume the latest modified file is the correct file and return it.\n\n        \"\"\"\n        # Catch cancelled playblast\n        if filepath is None:\n            self.log.warning(\"Playblast did not result in output path. \"\n                             \"Playblast is probably interrupted.\")\n            return None\n\n        # Fix: playblast not returning correct filename (with extension)\n        # Lets assume the most recently modified file is the correct one.\n        if not os.path.exists(filepath):\n            directory = os.path.dirname(filepath)\n            filename = os.path.basename(filepath)\n            # check if the filepath is has frame based filename\n            # example : capture.####.png\n            parts = filename.split(\".\")\n            if len(parts) == 3:\n                query = os.path.join(directory, \"{}.*.{}\".format(parts[0],\n                                                                 parts[-1]))\n                files = glob.glob(query)\n            else:\n                files = glob.glob(\"{}.*\".format(filepath))\n\n            if not files:\n                raise RuntimeError(\"Couldn't find playblast from: \"\n                                   \"{0}\".format(filepath))\n            filepath = max(files, key=os.path.getmtime)\n\n        return filepath\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Create Unreal Skeletal Mesh data to be extracted as FBX.\"\"\"\nimport os\nfrom contextlib import contextmanager\n\nfrom maya import cmds  # noqa\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.alembic import extract_alembic\nfrom openpype.hosts.maya.api.lib import (\n    suspended_refresh,\n    maintained_selection\n)\n\n\nclass ExtractUnrealSkeletalMeshAbc(publish.Extractor):\n    \"\"\"Extract Unreal Skeletal Mesh as FBX from Maya. \"\"\"\n\n    label = \"Extract Unreal Skeletal Mesh - Alembic\"\n    hosts = [\"maya\"]\n    families = [\"skeletalMesh\"]\n    optional = True\n\n    def process(self, instance):\n        self.log.debug(\"Extracting pointcache..\")\n\n        geo = cmds.listRelatives(\n            instance.data.get(\"geometry\"), allDescendents=True, fullPath=True)\n        joints = cmds.listRelatives(\n            instance.data.get(\"joints\"), allDescendents=True, fullPath=True)\n\n        nodes = geo + joints\n\n        attrs = instance.data.get(\"attr\", \"\").split(\";\")\n        attrs = [value for value in attrs if value.strip()]\n        attrs += [\"cbId\"]\n\n        attr_prefixes = instance.data.get(\"attrPrefix\", \"\").split(\";\")\n        attr_prefixes = [value for value in attr_prefixes if value.strip()]\n\n        # Define output path\n        staging_dir = self.staging_dir(instance)\n        filename = \"{0}.abc\".format(instance.name)\n        path = os.path.join(staging_dir, filename)\n\n        # The export requires forward slashes because we need\n        # to format it into a string in a mel expression\n        path = path.replace('\\\\', '/')\n\n        self.log.debug(\"Extracting ABC to: {0}\".format(path))\n        self.log.debug(\"Members: {0}\".format(nodes))\n        self.log.debug(\"Instance: {0}\".format(instance[:]))\n\n        options = {\n            \"step\": instance.data.get(\"step\", 1.0),\n            \"attr\": attrs,\n            \"attrPrefix\": attr_prefixes,\n            \"writeVisibility\": True,\n            \"writeCreases\": True,\n            \"writeColorSets\": instance.data.get(\"writeColorSets\", False),\n            \"writeFaceSets\": instance.data.get(\"writeFaceSets\", False),\n            \"uvWrite\": True,\n            \"selection\": True,\n            \"worldSpace\": instance.data.get(\"worldSpace\", True)\n        }\n\n        self.log.debug(\"Options: {}\".format(options))\n\n        if int(cmds.about(version=True)) >= 2017:\n            # Since Maya 2017 alembic supports multiple uv sets - write them.\n            options[\"writeUVSets\"] = True\n\n        if not instance.data.get(\"includeParentHierarchy\", True):\n            # Set the root nodes if we don't want to include parents\n            # The roots are to be considered the ones that are the actual\n            # direct members of the set\n            options[\"root\"] = instance.data.get(\"setMembers\")\n\n        with suspended_refresh(suspend=instance.data.get(\"refresh\", False)):\n            with maintained_selection():\n                cmds.select(nodes, noExpand=True)\n                extract_alembic(file=path,\n                                # startFrame=start,\n                                # endFrame=end,\n                                **options)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'abc',\n            'ext': 'abc',\n            'files': filename,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extract ABC successful to: {0}\".format(path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Create Unreal Skeletal Mesh data to be extracted as FBX.\"\"\"\nimport os\nfrom contextlib import contextmanager\n\nfrom maya import cmds  # noqa\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api import fbx\n\n\n@contextmanager\ndef renamed(original_name, renamed_name):\n    # type: (str, str) -> None\n    try:\n        cmds.rename(original_name, renamed_name)\n        yield\n    finally:\n        cmds.rename(renamed_name, original_name)\n\n\nclass ExtractUnrealSkeletalMeshFbx(publish.Extractor):\n    \"\"\"Extract Unreal Skeletal Mesh as FBX from Maya. \"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.1\n    label = \"Extract Unreal Skeletal Mesh - FBX\"\n    families = [\"skeletalMesh\"]\n    optional = True\n\n    def process(self, instance):\n        fbx_exporter = fbx.FBXExtractor(log=self.log)\n\n        # Define output path\n        staging_dir = self.staging_dir(instance)\n        filename = \"{0}.fbx\".format(instance.name)\n        path = os.path.join(staging_dir, filename)\n\n        geo = instance.data.get(\"geometry\")\n        joints = instance.data.get(\"joints\")\n\n        to_extract = geo + joints\n\n        # The export requires forward slashes because we need\n        # to format it into a string in a mel expression\n        path = path.replace('\\\\', '/')\n\n        self.log.debug(\"Extracting FBX to: {0}\".format(path))\n        self.log.debug(\"Members: {0}\".format(to_extract))\n        self.log.debug(\"Instance: {0}\".format(instance[:]))\n\n        fbx_exporter.set_options_from_instance(instance)\n\n        # This magic is done for variants. To let Unreal merge correctly\n        # existing data, top node must have the same name. So for every\n        # variant we extract we need to rename top node of the rig correctly.\n        # It is finally done in context manager so it won't affect current\n        # scene.\n\n        # we rely on hierarchy under one root.\n        original_parent = to_extract[0].split(\"|\")[1]\n\n        parent_node = instance.data.get(\"asset\")\n        # this needs to be done for AYON\n        # WARNING: since AYON supports duplicity of asset names,\n        #          this needs to be refactored throughout the pipeline.\n        parent_node = parent_node.split(\"/\")[-1]\n\n        renamed_to_extract = []\n        for node in to_extract:\n            node_path = node.split(\"|\")\n            node_path[1] = parent_node\n            renamed_to_extract.append(\"|\".join(node_path))\n\n        with renamed(original_parent, parent_node):\n            self.log.debug(\"Extracting: {}\".format(renamed_to_extract, path))\n            fbx_exporter.export(renamed_to_extract, path)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'fbx',\n            'ext': 'fbx',\n            'files': filename,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extract FBX successful to: {0}\".format(path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Create Unreal Static Mesh data to be extracted as FBX.\"\"\"\nimport os\n\nfrom maya import cmds  # noqa\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.lib import (\n    parent_nodes,\n    maintained_selection\n)\nfrom openpype.hosts.maya.api import fbx\n\n\nclass ExtractUnrealStaticMesh(publish.Extractor):\n    \"\"\"Extract Unreal Static Mesh as FBX from Maya. \"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.1\n    label = \"Extract Unreal Static Mesh\"\n    families = [\"staticMesh\"]\n\n    def process(self, instance):\n        members = instance.data.get(\"geometryMembers\", [])\n        if instance.data.get(\"collisionMembers\"):\n            members = members + instance.data.get(\"collisionMembers\")\n\n        fbx_exporter = fbx.FBXExtractor(log=self.log)\n\n        # Define output path\n        staging_dir = self.staging_dir(instance)\n        filename = \"{0}.fbx\".format(instance.name)\n        path = os.path.join(staging_dir, filename)\n\n        # The export requires forward slashes because we need\n        # to format it into a string in a mel expression\n        path = path.replace('\\\\', '/')\n\n        self.log.debug(\"Extracting FBX to: {0}\".format(path))\n        self.log.debug(\"Members: {0}\".format(members))\n        self.log.debug(\"Instance: {0}\".format(instance[:]))\n\n        fbx_exporter.set_options_from_instance(instance)\n\n        with maintained_selection():\n            with parent_nodes(members):\n                self.log.debug(\"Un-parenting: {}\".format(members))\n                fbx_exporter.export(members, path)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'fbx',\n            'ext': 'fbx',\n            'files': filename,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extract FBX successful to: {0}\".format(path))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py",
    "content": "import os\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\n\n\nclass ExtractUnrealYetiCache(publish.Extractor):\n    \"\"\"Producing Yeti cache files using scene time range.\n\n    This will extract Yeti cache file sequence and fur settings.\n    \"\"\"\n\n    label = \"Extract Yeti Cache (Unreal)\"\n    hosts = [\"maya\"]\n    families = [\"yeticacheUE\"]\n\n    def process(self, instance):\n\n        yeti_nodes = cmds.ls(instance, type=\"pgYetiMaya\")\n        if not yeti_nodes:\n            raise RuntimeError(\"No pgYetiMaya nodes found in the instance\")\n\n        # Define extract output file path\n        dirname = self.staging_dir(instance)\n\n        # Collect information for writing cache\n        start_frame = instance.data[\"frameStartHandle\"]\n        end_frame = instance.data[\"frameEndHandle\"]\n        preroll = instance.data[\"preroll\"]\n        if preroll > 0:\n            start_frame -= preroll\n\n        kwargs = {}\n        samples = instance.data.get(\"samples\", 0)\n        if samples == 0:\n            kwargs.update({\"sampleTimes\": \"0.0 1.0\"})\n        else:\n            kwargs.update({\"samples\": samples})\n\n        self.log.debug(f\"Writing out cache {start_frame} - {end_frame}\")\n        filename = f\"{instance.name}.abc\"\n        path = os.path.join(dirname, filename)\n        cmds.pgYetiCommand(yeti_nodes,\n                           writeAlembic=path,\n                           range=(start_frame, end_frame),\n                           asUnrealAbc=True,\n                           **kwargs)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'abc',\n            'ext': 'abc',\n            'files': filename,\n            'stagingDir': dirname\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(f\"Extracted {instance} to {dirname}\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_vrayproxy.py",
    "content": "import os\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.lib import maintained_selection\n\n\nclass ExtractVRayProxy(publish.Extractor):\n    \"\"\"Extract the content of the instance to a vrmesh file\n\n    Things to pay attention to:\n        - If animation is toggled, are the frames correct\n        -\n    \"\"\"\n\n    label = \"VRay Proxy (.vrmesh)\"\n    hosts = [\"maya\"]\n    families = [\"vrayproxy.vrmesh\"]\n\n    def process(self, instance):\n\n        staging_dir = self.staging_dir(instance)\n        file_name = \"{}.vrmesh\".format(instance.name)\n        file_path = os.path.join(staging_dir, file_name)\n\n        anim_on = instance.data[\"animation\"]\n        if not anim_on:\n            # Remove animation information because it is not required for\n            # non-animated subsets\n            keys = [\"frameStart\", \"frameEnd\",\n                    \"handleStart\", \"handleEnd\",\n                    \"frameStartHandle\", \"frameEndHandle\"]\n            for key in keys:\n                instance.data.pop(key, None)\n\n            start_frame = 1\n            end_frame = 1\n        else:\n            start_frame = instance.data[\"frameStartHandle\"]\n            end_frame = instance.data[\"frameEndHandle\"]\n\n        vertex_colors = instance.data.get(\"vertexColors\", False)\n\n        # Write out vrmesh file\n        self.log.debug(\"Writing: '%s'\" % file_path)\n        with maintained_selection():\n            cmds.select(instance.data[\"setMembers\"], noExpand=True)\n            cmds.vrayCreateProxy(exportType=1,\n                                 dir=staging_dir,\n                                 fname=file_name,\n                                 animOn=anim_on,\n                                 animType=3,\n                                 startFrame=start_frame,\n                                 endFrame=end_frame,\n                                 vertexColorsOn=vertex_colors,\n                                 ignoreHiddenObjects=True,\n                                 createProxyNode=False)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': 'vrmesh',\n            'ext': 'vrmesh',\n            'files': file_name,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\"\n                       % (instance.name, staging_dir))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_vrayscene.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract vrayscene from specified families.\"\"\"\nimport os\nimport re\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.render_setup_tools import export_in_rs_layer\nfrom openpype.hosts.maya.api.lib import maintained_selection\n\nfrom maya import cmds\n\n\nclass ExtractVrayscene(publish.Extractor):\n    \"\"\"Extractor for vrscene.\"\"\"\n\n    label = \"VRay Scene (.vrscene)\"\n    hosts = [\"maya\"]\n    families = [\"vrayscene_layer\"]\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        if instance.data.get(\"exportOnFarm\"):\n            self.log.debug(\"vrayscenes will be exported on farm.\")\n            raise NotImplementedError(\n                \"exporting vrayscenes is not implemented\")\n\n        # handle sequence\n        if instance.data.get(\"vraySceneMultipleFiles\"):\n            self.log.debug(\"vrayscenes will be exported on farm.\")\n            raise NotImplementedError(\n                \"exporting vrayscene sequences not implemented yet\")\n\n        vray_settings = cmds.ls(type=\"VRaySettingsNode\")\n        if not vray_settings:\n            node = cmds.createNode(\"VRaySettingsNode\")\n        else:\n            node = vray_settings[0]\n\n        # setMembers on vrayscene_layer should contain layer name.\n        layer_name = instance.data.get(\"layer\")\n\n        staging_dir = self.staging_dir(instance)\n        template = cmds.getAttr(\"{}.vrscene_filename\".format(node))\n        start_frame = instance.data.get(\n            \"frameStartHandle\") if instance.data.get(\n                \"vraySceneMultipleFiles\") else None\n        formatted_name = self.format_vray_output_filename(\n            os.path.basename(instance.data.get(\"source\")),\n            layer_name,\n            template,\n            start_frame\n        )\n\n        file_path = os.path.join(\n            staging_dir, \"vrayscene\", *formatted_name.split(\"/\"))\n\n        # Write out vrscene file\n        self.log.debug(\"Writing: '%s'\" % file_path)\n        with maintained_selection():\n            if \"*\" not in instance.data[\"setMembers\"]:\n                self.log.debug(\n                    \"Exporting: {}\".format(instance.data[\"setMembers\"]))\n                set_members = instance.data[\"setMembers\"]\n                cmds.select(set_members, noExpand=True)\n            else:\n                self.log.debug(\"Exporting all ...\")\n                set_members = cmds.ls(\n                    long=True, objectsOnly=True,\n                    geometry=True, lights=True, cameras=True)\n                cmds.select(set_members, noExpand=True)\n\n            self.log.debug(\"Appending layer name {}\".format(layer_name))\n            set_members.append(layer_name)\n\n            export_in_rs_layer(\n                file_path,\n                set_members,\n                export=lambda: cmds.file(\n                    file_path, type=\"V-Ray Scene\",\n                    pr=True, es=True, force=True))\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        files = file_path\n\n        representation = {\n            'name': 'vrscene',\n            'ext': 'vrscene',\n            'files': os.path.basename(files),\n            \"stagingDir\": os.path.dirname(files),\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '%s' to: %s\"\n                       % (instance.name, staging_dir))\n\n    @staticmethod\n    def format_vray_output_filename(\n            filename, layer, template, start_frame=None):\n        \"\"\"Format the expected output file of the Export job.\n\n        Example:\n            filename: /mnt/projects/foo/shot010_v006.mb\n            template: <Scene>/<Layer>/<Layer>\n            result: \"shot010_v006/CHARS/CHARS.vrscene\"\n\n        Args:\n            filename (str): path to scene file.\n            layer (str): layer name.\n            template (str): token template.\n            start_frame (int, optional): start frame - if set we use\n                multiple files export mode.\n\n        Returns:\n            str: formatted path.\n\n        \"\"\"\n        # format template to match pythons format specs\n        template = re.sub(r\"<(\\w+?)>\", r\"{\\1}\", template.lower())\n\n        # Ensure filename has no extension\n        file_name, _ = os.path.splitext(filename)\n        mapping = {\n            \"scene\": file_name,\n            \"layer\": layer\n        }\n\n        output_path = template.format(**mapping)\n\n        if start_frame:\n            filename_zero = \"{}_{:04d}.vrscene\".format(\n                output_path, start_frame)\n        else:\n            filename_zero = \"{}.vrscene\".format(output_path)\n\n        result = filename_zero.replace(\"\\\\\", \"/\")\n\n        return result\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py",
    "content": "import os\nimport shutil\nimport copy\n\nfrom maya import cmds\n\nimport pyblish.api\nfrom openpype.hosts.maya.api.alembic import extract_alembic\nfrom openpype.pipeline import publish\nfrom openpype.lib import StringTemplate\n\n\nclass ExtractWorkfileXgen(publish.Extractor):\n    \"\"\"Extract Workfile Xgen.\n\n    When submitting a render, we need to prep Xgen side car files.\n    \"\"\"\n\n    # Offset to run before workfile scene save.\n    order = pyblish.api.ExtractorOrder - 0.499\n    label = \"Extract Workfile Xgen\"\n    families = [\"workfile\"]\n    hosts = [\"maya\"]\n\n    def get_render_max_frame_range(self, context):\n        \"\"\"Return start to end frame range including all renderlayers in\n        context.\n\n         This will return the full frame range which includes all frames of the\n         renderlayer instances to be published/submitted.\n\n         Args:\n            context (pyblish.api.Context): Current publishing context.\n\n         Returns:\n            tuple or None: Start frame, end frame tuple if any renderlayers\n                found. Otherwise None is returned.\n\n         \"\"\"\n\n        def _is_active_renderlayer(i):\n            \"\"\"Return whether instance is active renderlayer\"\"\"\n            if not i.data.get(\"publish\", True):\n                return False\n\n            is_renderlayer = (\n                \"renderlayer\" in i.data.get(\"families\", []) or\n                i.data[\"family\"] == \"renderlayer\"\n            )\n            return is_renderlayer\n\n        start_frame = None\n        end_frame = None\n        for instance in context:\n            if not _is_active_renderlayer(instance):\n                # Only consider renderlyare instances\n                continue\n\n            render_start_frame = instance.data[\"frameStart\"]\n            render_end_frame = instance.data[\"frameEnd\"]\n\n            if start_frame is None:\n                start_frame = render_start_frame\n            else:\n                start_frame = min(start_frame, render_start_frame)\n\n            if end_frame is None:\n                end_frame = render_end_frame\n            else:\n                end_frame = max(end_frame, render_end_frame)\n\n        if start_frame is None or end_frame is None:\n            return\n\n        return start_frame, end_frame\n\n    def process(self, instance):\n        transfers = []\n\n        # Validate there is any palettes in the scene.\n        if not cmds.ls(type=\"xgmPalette\"):\n            self.log.debug(\n                \"No collections found in the scene. Skipping Xgen extraction.\"\n            )\n            return\n\n        import xgenm\n\n        # Validate to extract only when we are publishing a renderlayer as\n        # well.\n        render_range = self.get_render_max_frame_range(instance.context)\n        if not render_range:\n            self.log.debug(\n                \"No publishable renderlayers found in context. Skipping Xgen\"\n                \" extraction.\"\n            )\n            return\n\n        start_frame, end_frame = render_range\n\n        # We decrement start frame and increment end frame so motion blur will\n        # render correctly.\n        start_frame -= 1\n        end_frame += 1\n\n        # Extract patches alembic.\n        path_no_ext, _ = os.path.splitext(instance.context.data[\"currentFile\"])\n        kwargs = {\"attrPrefix\": [\"xgen\"], \"stripNamespaces\": True}\n        alembic_files = []\n        for palette in cmds.ls(type=\"xgmPalette\"):\n            patch_names = []\n            for description in xgenm.descriptions(palette):\n                for name in xgenm.boundGeometry(palette, description):\n                    patch_names.append(name)\n\n            alembic_file = \"{}__{}.abc\".format(\n                path_no_ext, palette.replace(\":\", \"__ns__\")\n            )\n            extract_alembic(\n                alembic_file,\n                root=patch_names,\n                selection=False,\n                startFrame=float(start_frame),\n                endFrame=float(end_frame),\n                verbose=True,\n                **kwargs\n            )\n            alembic_files.append(alembic_file)\n\n        template_data = copy.deepcopy(instance.data[\"anatomyData\"])\n        published_maya_path = StringTemplate(\n            instance.context.data[\"anatomy\"].templates[\"publish\"][\"file\"]\n        ).format(template_data)\n        published_basename, _ = os.path.splitext(published_maya_path)\n\n        for source in alembic_files:\n            destination = os.path.join(\n                os.path.dirname(instance.data[\"resourcesDir\"]),\n                os.path.basename(\n                    source.replace(path_no_ext, published_basename)\n                )\n            )\n            transfers.append((source, destination))\n\n        # Validate that we are using the published workfile.\n        deadline_settings = instance.context.get(\"deadline\")\n        if deadline_settings:\n            publish_settings = deadline_settings[\"publish\"]\n            if not publish_settings[\"MayaSubmitDeadline\"][\"use_published\"]:\n                self.log.debug(\n                    \"Not using the published workfile. Abort Xgen extraction.\"\n                )\n                return\n\n        # Collect Xgen and Delta files.\n        xgen_files = []\n        sources = []\n        current_dir = os.path.dirname(instance.context.data[\"currentFile\"])\n        attrs = [\"xgFileName\", \"xgBaseFile\"]\n        for palette in cmds.ls(type=\"xgmPalette\"):\n            for attr in attrs:\n                source = os.path.join(\n                    current_dir, cmds.getAttr(palette + \".\" + attr)\n                )\n                if not os.path.exists(source):\n                    continue\n\n                ext = os.path.splitext(source)[1]\n                if ext == \".xgen\":\n                    xgen_files.append(source)\n                if ext == \".xgd\":\n                    sources.append(source)\n\n        # Copy .xgen file to temporary location and modify.\n        staging_dir = self.staging_dir(instance)\n        for source in xgen_files:\n            destination = os.path.join(staging_dir, os.path.basename(source))\n            shutil.copy(source, destination)\n\n            lines = []\n            with open(destination, \"r\") as f:\n                for line in [line.rstrip() for line in f]:\n                    if line.startswith(\"\\txgProjectPath\"):\n                        path = os.path.dirname(instance.data[\"resourcesDir\"])\n                        line = \"\\txgProjectPath\\t\\t{}/\".format(\n                            path.replace(\"\\\\\", \"/\")\n                        )\n\n                    lines.append(line)\n\n            with open(destination, \"w\") as f:\n                f.write(\"\\n\".join(lines))\n\n            sources.append(destination)\n\n        # Add resource files to workfile instance.\n        for source in sources:\n            basename = os.path.basename(source)\n            destination = os.path.join(\n                os.path.dirname(instance.data[\"resourcesDir\"]), basename\n            )\n            transfers.append((source, destination))\n\n        destination_dir = os.path.join(\n            instance.data[\"resourcesDir\"], \"collections\"\n        )\n        for palette in cmds.ls(type=\"xgmPalette\"):\n            project_path = xgenm.getAttr(\"xgProjectPath\", palette)\n            data_path = xgenm.getAttr(\"xgDataPath\", palette)\n            data_path = data_path.replace(\"${PROJECT}\", project_path)\n            for path in data_path.split(\";\"):\n                for root, _, files in os.walk(path):\n                    for f in files:\n                        source = os.path.join(root, f)\n                        destination = \"{}/{}{}\".format(\n                            destination_dir,\n                            palette.replace(\":\", \"__ns__\"),\n                            source.replace(path, \"\")\n                        )\n                        transfers.append((source, destination))\n\n        for source, destination in transfers:\n            self.log.debug(\"Transfer: {} > {}\".format(source, destination))\n\n        instance.data[\"transfers\"] = transfers\n\n        # Set palette attributes in preparation for workfile publish.\n        attrs = {\"xgFileName\": None, \"xgBaseFile\": \"\"}\n        data = {}\n        for palette in cmds.ls(type=\"xgmPalette\"):\n            attrs[\"xgFileName\"] = \"resources/{}.xgen\".format(\n                palette.replace(\":\", \"__ns__\")\n            )\n            for attr, value in attrs.items():\n                node_attr = palette + \".\" + attr\n\n                old_value = cmds.getAttr(node_attr)\n                try:\n                    data[palette][attr] = old_value\n                except KeyError:\n                    data[palette] = {attr: old_value}\n\n                cmds.setAttr(node_attr, value, type=\"string\")\n                self.log.debug(\n                    \"Setting \\\"{}\\\" on \\\"{}\\\"\".format(value, node_attr)\n                )\n\n            cmds.setAttr(palette + \".\" + \"xgExportAsDelta\", False)\n\n        instance.data[\"xgenAttributes\"] = data\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_xgen.py",
    "content": "import os\nimport copy\nimport tempfile\n\nfrom maya import cmds\nimport xgenm\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api.lib import (\n    maintained_selection, attribute_values, write_xgen_file, delete_after\n)\nfrom openpype.lib import StringTemplate\n\n\nclass ExtractXgen(publish.Extractor):\n    \"\"\"Extract Xgen\n\n    Workflow:\n    - Duplicate nodes used for patches.\n    - Export palette and import onto duplicate nodes.\n    - Export/Publish duplicate nodes and palette.\n    - Export duplicate palette to .xgen file and add to publish.\n    - Publish all xgen files as resources.\n    \"\"\"\n\n    label = \"Extract Xgen\"\n    hosts = [\"maya\"]\n    families = [\"xgen\"]\n    scene_type = \"ma\"\n\n    def process(self, instance):\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        staging_dir = self.staging_dir(instance)\n        maya_filename = \"{}.{}\".format(instance.data[\"name\"], self.scene_type)\n        maya_filepath = os.path.join(staging_dir, maya_filename)\n\n        # Get published xgen file name.\n        template_data = copy.deepcopy(instance.data[\"anatomyData\"])\n        template_data.update({\"ext\": \"xgen\"})\n        templates = instance.context.data[\"anatomy\"].templates[\"publish\"]\n        xgen_filename = StringTemplate(templates[\"file\"]).format(template_data)\n\n        xgen_path = os.path.join(\n            self.staging_dir(instance), xgen_filename\n        ).replace(\"\\\\\", \"/\")\n        type = \"mayaAscii\" if self.scene_type == \"ma\" else \"mayaBinary\"\n\n        # Duplicate xgen setup.\n        with delete_after() as delete_bin:\n            duplicate_nodes = []\n            # Collect nodes to export.\n            for node in instance.data[\"xgenConnections\"]:\n                # Duplicate_transform subd patch geometry.\n                duplicate_transform = cmds.duplicate(node)[0]\n                delete_bin.append(duplicate_transform)\n\n                # Discard the children.\n                shapes = cmds.listRelatives(duplicate_transform, shapes=True)\n                children = cmds.listRelatives(\n                    duplicate_transform, children=True\n                )\n                cmds.delete(set(children) - set(shapes))\n\n                if cmds.listRelatives(duplicate_transform, parent=True):\n                    duplicate_transform = cmds.parent(\n                        duplicate_transform, world=True\n                    )[0]\n\n                duplicate_nodes.append(duplicate_transform)\n\n            # Export temp xgen palette files.\n            temp_xgen_path = os.path.join(\n                tempfile.gettempdir(), \"temp.xgen\"\n            ).replace(\"\\\\\", \"/\")\n            xgenm.exportPalette(\n                instance.data[\"xgmPalette\"].replace(\"|\", \"\"), temp_xgen_path\n            )\n            self.log.debug(\"Extracted to {}\".format(temp_xgen_path))\n\n            # Import xgen onto the duplicate.\n            with maintained_selection():\n                cmds.select(duplicate_nodes)\n                palette = xgenm.importPalette(temp_xgen_path, [])\n\n            delete_bin.append(palette)\n\n            # Copy shading assignments.\n            nodes = (\n                instance.data[\"xgmDescriptions\"] +\n                instance.data[\"xgmSubdPatches\"]\n            )\n            for node in nodes:\n                target_node = node.split(\":\")[-1]\n                shading_engine = cmds.listConnections(\n                    node, type=\"shadingEngine\"\n                )[0]\n                cmds.sets(target_node, edit=True, forceElement=shading_engine)\n\n            # Export duplicated palettes.\n            xgenm.exportPalette(palette, xgen_path)\n\n            # Export Maya file.\n            attribute_data = {\"{}.xgFileName\".format(palette): xgen_filename}\n            with attribute_values(attribute_data):\n                with maintained_selection():\n                    cmds.select(duplicate_nodes + [palette])\n                    cmds.file(\n                        maya_filepath,\n                        force=True,\n                        type=type,\n                        exportSelected=True,\n                        preserveReferences=False,\n                        constructionHistory=True,\n                        shader=True,\n                        constraints=True,\n                        expressions=True\n                    )\n\n            self.log.debug(\"Extracted to {}\".format(maya_filepath))\n\n        if os.path.exists(temp_xgen_path):\n            os.remove(temp_xgen_path)\n\n        data = {\n            \"xgDataPath\": os.path.join(\n                instance.data[\"resourcesDir\"],\n                \"collections\",\n                palette.replace(\":\", \"__ns__\")\n            ).replace(\"\\\\\", \"/\"),\n            \"xgProjectPath\": os.path.dirname(\n                instance.data[\"resourcesDir\"]\n            ).replace(\"\\\\\", \"/\")\n        }\n        write_xgen_file(data, xgen_path)\n\n        # Adding representations.\n        representation = {\n            \"name\": \"xgen\",\n            \"ext\": \"xgen\",\n            \"files\": xgen_filename,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n\n        representation = {\n            \"name\": self.scene_type,\n            \"ext\": self.scene_type,\n            \"files\": maya_filename,\n            \"stagingDir\": staging_dir\n        }\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_yeti_cache.py",
    "content": "import os\nimport json\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\n\n\nclass ExtractYetiCache(publish.Extractor):\n    \"\"\"Producing Yeti cache files using scene time range.\n\n    This will extract Yeti cache file sequence and fur settings.\n    \"\"\"\n\n    label = \"Extract Yeti Cache\"\n    hosts = [\"maya\"]\n    families = [\"yetiRig\", \"yeticache\"]\n\n    def process(self, instance):\n\n        yeti_nodes = cmds.ls(instance, type=\"pgYetiMaya\")\n        if not yeti_nodes:\n            raise RuntimeError(\"No pgYetiMaya nodes found in the instance\")\n\n        # Define extract output file path\n        dirname = self.staging_dir(instance)\n\n        # Collect information for writing cache\n        start_frame = instance.data[\"frameStartHandle\"]\n        end_frame = instance.data[\"frameEndHandle\"]\n        preroll = instance.data[\"preroll\"]\n        if preroll > 0:\n            start_frame -= preroll\n\n        kwargs = {}\n        samples = instance.data.get(\"samples\", 0)\n        if samples == 0:\n            kwargs.update({\"sampleTimes\": \"0.0 1.0\"})\n        else:\n            kwargs.update({\"samples\": samples})\n\n        self.log.debug(\n            \"Writing out cache {} - {}\".format(start_frame, end_frame))\n        # Start writing the files for snap shot\n        # <NAME> will be replace by the Yeti node name\n        path = os.path.join(dirname, \"<NAME>.%04d.fur\")\n        cmds.pgYetiCommand(yeti_nodes,\n                           writeCache=path,\n                           range=(start_frame, end_frame),\n                           updateViewport=False,\n                           generatePreview=False,\n                           **kwargs)\n\n        cache_files = [x for x in os.listdir(dirname) if x.endswith(\".fur\")]\n\n        self.log.debug(\"Writing metadata file\")\n        settings = instance.data[\"fursettings\"]\n        fursettings_path = os.path.join(dirname, \"yeti.fursettings\")\n        with open(fursettings_path, \"w\") as fp:\n            json.dump(settings, fp, ensure_ascii=False)\n\n        # build representations\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        self.log.debug(\"cache files: {}\".format(cache_files[0]))\n\n        # Workaround: We do not explicitly register these files with the\n        # representation solely so that we can write multiple sequences\n        # a single Subset without renaming - it's a bit of a hack\n        # TODO: Implement better way to manage this sort of integration\n        if 'transfers' not in instance.data:\n            instance.data['transfers'] = []\n\n        publish_dir = instance.data[\"publishDir\"]\n        for cache_filename in cache_files:\n            src = os.path.join(dirname, cache_filename)\n            dst = os.path.join(publish_dir, os.path.basename(cache_filename))\n            instance.data['transfers'].append([src, dst])\n\n        instance.data[\"representations\"].append(\n            {\n                'name': 'fur',\n                'ext': 'fursettings',\n                'files': os.path.basename(fursettings_path),\n                'stagingDir': dirname\n            }\n        )\n\n        self.log.debug(\"Extracted {} to {}\".format(instance, dirname))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/extract_yeti_rig.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract Yeti rig.\"\"\"\n\nimport os\nimport json\nimport contextlib\n\nfrom maya import cmds\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.maya.api import lib\n\n\n@contextlib.contextmanager\ndef disconnect_plugs(settings, members):\n    \"\"\"Disconnect and store attribute connections.\"\"\"\n    members = cmds.ls(members, long=True)\n    original_connections = []\n    try:\n        for input in settings[\"inputs\"]:\n\n            # Get source shapes\n            source_nodes = lib.lsattr(\"cbId\", input[\"sourceID\"])\n            if not source_nodes:\n                continue\n\n            source = next(s for s in source_nodes if s not in members)\n\n            # Get destination shapes (the shapes used as hook up)\n            destination_nodes = lib.lsattr(\"cbId\", input[\"destinationID\"])\n            destination = next(i for i in destination_nodes if i in members)\n\n            # Create full connection\n            connections = input[\"connections\"]\n            src_attribute = \"%s.%s\" % (source, connections[0])\n            dst_attribute = \"%s.%s\" % (destination, connections[1])\n\n            # Check if there is an actual connection\n            if not cmds.isConnected(src_attribute, dst_attribute):\n                print(\"No connection between %s and %s\" % (\n                    src_attribute, dst_attribute))\n                continue\n\n            # Break and store connection\n            cmds.disconnectAttr(src_attribute, dst_attribute)\n            original_connections.append([src_attribute, dst_attribute])\n        yield\n    finally:\n        # Restore previous connections\n        for connection in original_connections:\n            try:\n                cmds.connectAttr(connection[0], connection[1])\n            except Exception as e:\n                print(e)\n                continue\n\n\n@contextlib.contextmanager\ndef yetigraph_attribute_values(assumed_destination, resources):\n    \"\"\"Get values from Yeti attributes in graph.\"\"\"\n    try:\n        for resource in resources:\n            if \"graphnode\" not in resource:\n                continue\n\n            fname = os.path.basename(resource[\"source\"])\n            new_fpath = os.path.join(assumed_destination, fname)\n            new_fpath = new_fpath.replace(\"\\\\\", \"/\")\n\n            try:\n                cmds.pgYetiGraph(resource[\"node\"],\n                                 node=resource[\"graphnode\"],\n                                 param=resource[\"param\"],\n                                 setParamValueString=new_fpath)\n            except Exception as exc:\n                print(\">>> Exception:\", exc)\n        yield\n\n    finally:\n        for resource in resources:\n            if \"graphnode\" not in resources:\n                continue\n\n            try:\n                cmds.pgYetiGraph(resource[\"node\"],\n                                 node=resource[\"graphnode\"],\n                                 param=resource[\"param\"],\n                                 setParamValue=resource[\"source\"])\n            except RuntimeError:\n                pass\n\n\nclass ExtractYetiRig(publish.Extractor):\n    \"\"\"Extract the Yeti rig to a Maya Scene and write the Yeti rig data.\"\"\"\n\n    label = \"Extract Yeti Rig\"\n    hosts = [\"maya\"]\n    families = [\"yetiRig\"]\n    scene_type = \"ma\"\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        ext_mapping = (\n            instance.context.data[\"project_settings\"][\"maya\"][\"ext_mapping\"]\n        )\n        if ext_mapping:\n            self.log.debug(\"Looking in settings for scene type ...\")\n            # use extension mapping for first family found\n            for family in self.families:\n                try:\n                    self.scene_type = ext_mapping[family]\n                    self.log.debug(\n                        \"Using {} as scene type\".format(self.scene_type))\n                    break\n                except KeyError:\n                    # no preset found\n                    pass\n        yeti_nodes = cmds.ls(instance, type=\"pgYetiMaya\")\n        if not yeti_nodes:\n            raise RuntimeError(\"No pgYetiMaya nodes found in the instance\")\n\n        # Define extract output file path\n        dirname = self.staging_dir(instance)\n        settings_path = os.path.join(dirname, \"yeti.rigsettings\")\n\n        # Yeti related staging dirs\n        maya_path = os.path.join(dirname,\n                                 \"yeti_rig.{}\".format(self.scene_type))\n\n        self.log.debug(\"Writing metadata file: {}\".format(settings_path))\n\n        image_search_path = resources_dir = instance.data[\"resourcesDir\"]\n\n        settings = instance.data.get(\"rigsettings\", None)\n        assert settings, \"Yeti rig settings were not collected.\"\n        settings[\"imageSearchPath\"] = image_search_path\n        with open(settings_path, \"w\") as fp:\n            json.dump(settings, fp, ensure_ascii=False)\n\n        # add textures to transfers\n        if 'transfers' not in instance.data:\n            instance.data['transfers'] = []\n\n        for resource in instance.data.get('resources', []):\n            for file in resource['files']:\n                src = file\n                dst = os.path.join(image_search_path, os.path.basename(file))\n                instance.data['transfers'].append([src, dst])\n\n                self.log.debug(\"adding transfer {} -> {}\". format(src, dst))\n\n        # Ensure the imageSearchPath is being remapped to the publish folder\n        attr_value = {\"%s.imageSearchPath\" % n: str(image_search_path) for\n                      n in yeti_nodes}\n\n        # Get input_SET members\n        input_set = next(i for i in instance if i == \"input_SET\")\n\n        # Get all items\n        set_members = cmds.sets(input_set, query=True) or []\n        set_members += cmds.listRelatives(set_members,\n                                          allDescendents=True,\n                                          fullPath=True) or []\n        members = cmds.ls(set_members, long=True)\n\n        nodes = instance.data[\"setMembers\"]\n        resources = instance.data.get(\"resources\", {})\n        with disconnect_plugs(settings, members):\n            with yetigraph_attribute_values(resources_dir, resources):\n                with lib.attribute_values(attr_value):\n                    cmds.select(nodes, noExpand=True)\n                    cmds.file(maya_path,\n                              force=True,\n                              exportSelected=True,\n                              typ=\"mayaAscii\" if self.scene_type == \"ma\" else \"mayaBinary\",  # noqa: E501\n                              preserveReferences=False,\n                              constructionHistory=True,\n                              shader=False)\n\n        # Ensure files can be stored\n        # build representations\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        self.log.debug(\"rig file: {}\".format(maya_path))\n        instance.data[\"representations\"].append(\n            {\n                'name': self.scene_type,\n                'ext': self.scene_type,\n                'files': os.path.basename(maya_path),\n                'stagingDir': dirname\n            }\n        )\n        self.log.debug(\"settings file: {}\".format(settings_path))\n        instance.data[\"representations\"].append(\n            {\n                'name': 'rigsettings',\n                'ext': 'rigsettings',\n                'files': os.path.basename(settings_path),\n                'stagingDir': dirname\n            }\n        )\n\n        self.log.debug(\"Extracted {} to {}\".format(instance, dirname))\n\n        cmds.select(clear=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/help/submit_maya_remote_publish_deadline.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Errors found</title>\n<description>\n## Publish process has errors\n\nAt least one plugin failed before this plugin, job won't be sent to Deadline for processing before all issues are fixed.\n\n### How to repair?\n\nCheck all failing plugins (should be highlighted in red) and fix issues if possible.\n</description>\n\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/help/validate_maya_units.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Maya scene units</title>\n<description>## Invalid maya scene units\n\nDetected invalid maya scene units:\n\n{issues}\n\n</description>\n<detail>\n### How to repair?\n\nYou can automatically repair the scene units by clicking the Repair action on\nthe right.\n\nAfter that restart publishing with Reload button.\n</detail>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/help/validate_node_ids.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Missing node ids</title>\n<description>## Nodes found with missing `cbId`\n\nNodes were detected in your scene which are missing required `cbId`\nattributes for identification further in the pipeline.\n\n### How to repair?\n\nThe node ids are auto-generated on scene save, and thus the easiest fix is to\nsave your scene again.\n\nAfter that restart publishing with Reload button.\n</description>\n<detail>\n### Invalid nodes\n\n{nodes}\n\n\n### How could this happen?\n\nThis often happens if you've generated new nodes but haven't saved your scene\nafter creating the new nodes.\n</detail>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Skeletal Mesh Top Node</title>\n<description>## Skeletal meshes needs common root\n\nSkeletal meshes and their joints must be under one common root.\n\n### How to repair?\n\nMake sure all geometry and joints resides under same root.\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/increment_current_file_deadline.py",
    "content": "import pyblish.api\n\n\nclass IncrementCurrentFileDeadline(pyblish.api.ContextPlugin):\n    \"\"\"Increment the current file.\n\n    Saves the current maya scene with an increased version number.\n\n    \"\"\"\n\n    label = \"Increment current file\"\n    order = pyblish.api.IntegratorOrder + 9.0\n    hosts = [\"maya\"]\n    families = [\"workfile\"]\n    optional = True\n\n    def process(self, context):\n\n        from maya import cmds\n        from openpype.lib import version_up\n        from openpype.pipeline.publish import get_errored_plugins_from_context\n\n        errored_plugins = get_errored_plugins_from_context(context)\n        if any(plugin.__name__ == \"MayaSubmitDeadline\"\n                for plugin in errored_plugins):\n            raise RuntimeError(\"Skipping incrementing current file because \"\n                               \"submission to deadline failed.\")\n\n        current_filepath = context.data[\"currentFile\"]\n        new_filepath = version_up(current_filepath)\n\n        # # Ensure the suffix is .ma because we're saving to `mayaAscii` type\n        if new_filepath.endswith(\".ma\"):\n            fileType = \"mayaAscii\"\n        elif new_filepath.endswith(\".mb\"):\n            fileType = \"mayaBinary\"\n\n        cmds.file(rename=new_filepath)\n        cmds.file(save=True, force=True, type=fileType)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\n\nclass ResetXgenAttributes(pyblish.api.InstancePlugin):\n    \"\"\"Reset Xgen attributes.\n\n    When the incremental save of the workfile triggers, the Xgen attributes\n    changes so this plugin will change it back to the values before publishing.\n    \"\"\"\n\n    label = \"Reset Xgen Attributes.\"\n    # Offset to run after workfile increment plugin.\n    order = pyblish.api.IntegratorOrder + 10.0\n    families = [\"workfile\"]\n\n    def process(self, instance):\n        xgen_attributes = instance.data.get(\"xgenAttributes\", {})\n        if not xgen_attributes:\n            return\n\n        for palette, data in xgen_attributes.items():\n            for attr, value in data.items():\n                node_attr = \"{}.{}\".format(palette, attr)\n                self.log.debug(\n                    \"Setting \\\"{}\\\" on \\\"{}\\\"\".format(value, node_attr)\n                )\n                cmds.setAttr(node_attr, value, type=\"string\")\n            cmds.setAttr(palette + \".xgExportAsDelta\", True)\n\n        # Need to save the scene, cause the attribute changes above does not\n        # mark the scene as modified so user can exit without committing the\n        # changes.\n        self.log.debug(\"Saving changes.\")\n        cmds.file(save=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/save_scene.py",
    "content": "import pyblish.api\nfrom openpype.pipeline.workfile.lock_workfile import (\n    is_workfile_lock_enabled,\n    remove_workfile_lock\n)\n\n\nclass SaveCurrentScene(pyblish.api.ContextPlugin):\n    \"\"\"Save current scene\n\n    \"\"\"\n\n    label = \"Save current file\"\n    order = pyblish.api.ExtractorOrder - 0.49\n    hosts = [\"maya\"]\n    families = [\"renderlayer\", \"workfile\"]\n\n    def process(self, context):\n        import maya.cmds as cmds\n\n        current = cmds.file(query=True, sceneName=True)\n        assert context.data['currentFile'] == current\n\n        # If file has no modifications, skip forcing a file save\n        if not cmds.file(query=True, modified=True):\n            self.log.debug(\"Skipping file save as there \"\n                           \"are no modifications..\")\n            return\n        project_name = context.data[\"projectName\"]\n        project_settings = context.data[\"project_settings\"]\n        # remove lockfile before saving\n        if is_workfile_lock_enabled(\"maya\", project_name, project_settings):\n            remove_workfile_lock(current)\n        self.log.info(\"Saving current file: {}\".format(current))\n        cmds.file(save=True, force=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_alembic_options_defaults.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import OptionalPyblishPluginMixin\nfrom openpype.pipeline.publish import RepairAction, PublishValidationError\n\n\nclass ValidateAlembicOptionsDefaults(\n    pyblish.api.InstancePlugin, OptionalPyblishPluginMixin\n):\n    \"\"\"Validate the attributes on the instance are defaults.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"pointcache\", \"animation\"]\n    hosts = [\"maya\"]\n    label = \"Validate Alembic Options Defaults\"\n    actions = [RepairAction]\n    optional = True\n\n    @classmethod\n    def _get_plugin_name(self, publish_attributes):\n        for key in [\"ExtractAnimation\", \"ExtractAlembic\"]:\n            if key in publish_attributes.keys():\n                return key\n\n    @classmethod\n    def _get_settings(self, context):\n        maya_settings = context.data[\"project_settings\"][\"maya\"]\n        settings = maya_settings[\"publish\"][\"ExtractAlembic\"]\n        # Flags are a special case since they are a combination of overrides\n        # and default flags from the settings.\n        settings[\"flags\"] = [\n            x for x in settings[\"flags\"] if x in settings[\"overrides\"]\n        ]\n        return settings\n\n    @classmethod\n    def _get_publish_attributes(self, instance):\n        attributes = instance.data[\"publish_attributes\"][\n            self._get_plugin_name(\n                instance.data[\"publish_attributes\"]\n            )\n        ]\n\n        settings = self._get_settings(instance.context)\n\n        # Flags are a special case since they are a combination of exposed\n        # flags and default flags from the settings. So we need to add the\n        # default flags from the settings and ensure unique items.\n        non_exposed_flags = [\n            x for x in settings[\"flags\"] if x not in settings[\"overrides\"]\n        ]\n        attributes[\"flags\"] = attributes[\"flags\"] + non_exposed_flags\n\n        return attributes\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        settings = self._get_settings(instance.context)\n\n        attributes = self._get_publish_attributes(instance)\n\n        msg = (\n            \"Alembic Extract setting \\\"{}\\\" is not the default value:\"\n            \"\\nCurrent: {}\"\n            \"\\nDefault Value: {}\\n\"\n        )\n        errors = []\n        for key, value in attributes.items():\n            default_value = settings[key]\n\n            # Lists are best to compared sorted since we cant rely on the order\n            # of the items.\n            if isinstance(value, list):\n                value = sorted(value)\n                default_value = sorted(default_value)\n\n            if value != default_value:\n                errors.append(msg.format(key, value, default_value))\n\n        if errors:\n            raise PublishValidationError(\"\\n\".join(errors))\n\n    @classmethod\n    def repair(cls, instance):\n        # Find create instance twin.\n        create_context = instance.context.data[\"create_context\"]\n        create_instance = None\n        for Instance in create_context.instances:\n            if Instance.data[\"instance_id\"] == instance.data[\"instance_id\"]:\n                create_instance = Instance\n                break\n\n        assert create_instance is not None\n\n        # Set the settings values on the create context then save to workfile.\n        publish_attributes = instance.data[\"publish_attributes\"]\n        plugin_name = cls._get_plugin_name(publish_attributes)\n        attributes = cls._get_publish_attributes(instance)\n        settings = cls._get_settings(instance.context)\n        create_publish_attributes = create_instance.data[\"publish_attributes\"]\n        for key in attributes.keys():\n            create_publish_attributes[plugin_name][key] = settings[key]\n\n        create_context.save_changes()\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_animated_reference.py",
    "content": "import pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin\n)\nfrom maya import cmds\n\n\nclass ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin,\n                                   OptionalPyblishPluginMixin):\n    \"\"\"Validate all nodes in skeletonAnim_SET are referenced\"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"animation.fbx\"]\n    label = \"Animated Reference Rig\"\n    accepted_controllers = [\"transform\", \"locator\"]\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        animated_sets = instance.data.get(\"animated_skeleton\", [])\n        if not animated_sets:\n            self.log.debug(\n                \"No nodes found in skeletonAnim_SET. \"\n                \"Skipping validation of animated reference rig...\"\n            )\n            return\n\n        for animated_reference in animated_sets:\n            is_referenced = cmds.referenceQuery(\n                animated_reference, isNodeReferenced=True)\n            if not bool(is_referenced):\n                raise PublishValidationError(\n                    \"All the content in skeletonAnim_SET\"\n                    \" should be referenced nodes\"\n                )\n        invalid_controls = self.validate_controls(animated_sets)\n        if invalid_controls:\n            raise PublishValidationError(\n                \"All the content in skeletonAnim_SET\"\n                \" should be transforms\"\n            )\n\n    @classmethod\n    def validate_controls(self, set_members):\n        \"\"\"Check if the controller set contains only accepted node types.\n\n        Checks if all its set members are within the hierarchy of the root\n        Checks if the node types of the set members valid\n\n        Args:\n            set_members: list of nodes of the skeleton_anim_set\n            hierarchy: list of nodes which reside under the root node\n\n        Returns:\n            errors (list)\n        \"\"\"\n\n        # Validate control types\n        invalid = []\n        set_members = cmds.ls(set_members, long=True)\n        for node in set_members:\n            if cmds.nodeType(node) not in self.accepted_controllers:\n                invalid.append(node)\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_animation_content.py",
    "content": "import pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateAnimationContent(pyblish.api.InstancePlugin,\n                               OptionalPyblishPluginMixin):\n    \"\"\"Adheres to the content of 'animation' product type\n\n    - Must have collected `out_hierarchy` data.\n    - All nodes in `out_hierarchy` must be in the instance.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"animation\"]\n    label = \"Animation Content\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        out_set = next((i for i in instance.data[\"setMembers\"] if\n                        i.endswith(\"out_SET\")), None)\n\n        assert out_set, (\"Instance '%s' has no objectSet named: `OUT_set`. \"\n                         \"If this instance is an unloaded reference, \"\n                         \"please deactivate by toggling the 'Active' attribute\"\n                         % instance.name)\n\n        assert 'out_hierarchy' in instance.data, \"Missing `out_hierarchy` data\"\n\n        out_sets = [node for node in instance if node.endswith(\"out_SET\")]\n        msg = \"Couldn't find exactly one out_SET: {0}\".format(out_sets)\n        assert len(out_sets) == 1, msg\n\n        # All nodes in the `out_hierarchy` must be among the nodes that are\n        # in the instance. The nodes in the instance are found from the top\n        # group, as such this tests whether all nodes are under that top group.\n\n        lookup = set(instance[:])\n        invalid = [node for node in instance.data['out_hierarchy'] if\n                   node not in lookup]\n\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Animation content is invalid. See log.\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py",
    "content": "import maya.cmds as cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin,\n                                OptionalPyblishPluginMixin):\n    \"\"\"Validate if deformed shapes have related IDs to the original shapes\n\n    When a deformer is applied in the scene on a referenced mesh that already\n    had deformers then Maya will create a new shape node for the mesh that\n    does not have the original id. This validator checks whether the ids are\n    valid on all the shape nodes in the instance.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    families = ['animation', \"pointcache\", \"proxyAbc\"]\n    hosts = ['maya']\n    label = 'Animation Out Set Related Node Ids'\n    actions = [\n        openpype.hosts.maya.api.action.SelectInvalidAction,\n        RepairAction\n    ]\n    optional = False\n\n    def process(self, instance):\n        \"\"\"Process all meshes\"\"\"\n        if not self.is_active(instance.data):\n            return\n        # Ensure all nodes have a cbId and a related ID to the original shapes\n        # if a deformer has been created on the shape\n        invalid = self.get_invalid(instance)\n        if invalid:\n            # TODO: Message formatting can be improved\n            raise PublishValidationError(\"Nodes found with mismatching \"\n                                         \"IDs: {0}\".format(invalid),\n                                         title=\"Invalid node ids\")\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Get all nodes which do not match the criteria\"\"\"\n\n        invalid = []\n        types_to_skip = [\"locator\"]\n\n        # get asset id\n        nodes = instance.data.get(\"out_hierarchy\", instance[:])\n        for node in nodes:\n\n            # We only check when the node is *not* referenced\n            if cmds.referenceQuery(node, isNodeReferenced=True):\n                continue\n\n            # Check if node is a shape as deformers only work on shapes\n            obj_type = cmds.objectType(node, isAType=\"shape\")\n            if not obj_type:\n                continue\n\n            # Skip specific types\n            if cmds.objectType(node) in types_to_skip:\n                continue\n\n            # Get the current id of the node\n            node_id = lib.get_id(node)\n            if not node_id:\n                invalid.append(node)\n                continue\n\n            history_id = lib.get_id_from_sibling(node)\n            if history_id is not None and node_id != history_id:\n                invalid.append(node)\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n\n        for node in cls.get_invalid(instance):\n            # Get the original id from history\n            history_id = lib.get_id_from_sibling(node)\n            if not history_id:\n                cls.log.error(\"Could not find ID in history for '%s'\", node)\n                continue\n\n            lib.set_id(node, history_id, overwrite=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder, PublishValidationError\n)\nfrom openpype.hosts.maya.api.lib import is_visible\n\n\nclass ValidateArnoldSceneSource(pyblish.api.InstancePlugin):\n    \"\"\"Validate Arnold Scene Source.\n\n    Ensure no nodes are hidden.\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"ass\", \"assProxy\"]\n    label = \"Validate Arnold Scene Source\"\n\n    def process(self, instance):\n        # Validate against having nodes hidden, which will result in the\n        # extraction to ignore the node.\n        nodes = instance.data[\"members\"] + instance.data.get(\"proxy\", [])\n        nodes = [x for x in nodes if cmds.objectType(x, isAType='dagNode')]\n        hidden_nodes = [\n            x for x in nodes if not is_visible(x, intermediateObject=False)\n        ]\n        if hidden_nodes:\n            raise PublishValidationError(\n                \"Found hidden nodes:\\n\\n{}\\n\\nPlease unhide for\"\n                \" publishing.\".format(\"\\n\".join(hidden_nodes))\n            )\n\n\nclass ValidateArnoldSceneSourceProxy(pyblish.api.InstancePlugin):\n    \"\"\"Validate Arnold Scene Source Proxy.\n\n    When using proxies we need the nodes to share the same names and not be\n    parent to the world. This ends up needing at least two groups with content\n    nodes and proxy nodes in another.\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"assProxy\"]\n    label = \"Validate Arnold Scene Source Proxy\"\n\n    def _get_nodes_by_name(self, nodes):\n        ungrouped_nodes = []\n        nodes_by_name = {}\n        parents = []\n        for node in nodes:\n            node_split = node.split(\"|\")\n            if len(node_split) == 2:\n                ungrouped_nodes.append(node)\n\n            parent = \"|\".join(node_split[:-1])\n            if parent:\n                parents.append(parent)\n\n            node_name = node.rsplit(\"|\", 1)[-1].rsplit(\":\", 1)[-1]\n            nodes_by_name[node_name] = node\n\n        return ungrouped_nodes, nodes_by_name, parents\n\n    def process(self, instance):\n        # Validate against nodes directly parented to world.\n        ungrouped_nodes = []\n\n        nodes, content_nodes_by_name, content_parents = (\n            self._get_nodes_by_name(instance.data[\"members\"])\n        )\n        ungrouped_nodes.extend(nodes)\n\n        nodes, proxy_nodes_by_name, proxy_parents = self._get_nodes_by_name(\n            instance.data.get(\"proxy\", [])\n        )\n        ungrouped_nodes.extend(nodes)\n\n        if ungrouped_nodes:\n            raise PublishValidationError(\n                \"Found nodes parented to the world: {}\\n\"\n                \"All nodes need to be grouped.\".format(ungrouped_nodes)\n            )\n\n        # Validate for content and proxy nodes amount being the same.\n        if len(instance.data[\"members\"]) != len(instance.data[\"proxy\"]):\n            raise PublishValidationError(\n                \"Amount of content nodes ({}) and proxy nodes ({}) needs to \"\n                \"be the same.\\nContent nodes: {}\\nProxy nodes:{}\".format(\n                    len(instance.data[\"members\"]),\n                    len(instance.data[\"proxy\"]),\n                    instance.data[\"members\"],\n                    instance.data[\"proxy\"]\n                )\n            )\n\n        # Validate against content and proxy nodes sharing same parent.\n        if list(set(content_parents) & set(proxy_parents)):\n            raise PublishValidationError(\n                \"Content and proxy nodes cannot share the same parent.\"\n            )\n\n        # Validate for content and proxy nodes sharing same names.\n        sorted_content_names = sorted(content_nodes_by_name.keys())\n        sorted_proxy_names = sorted(proxy_nodes_by_name.keys())\n        odd_content_names = list(\n            set(sorted_content_names) - set(sorted_proxy_names)\n        )\n        odd_content_nodes = [\n            content_nodes_by_name[x] for x in odd_content_names\n        ]\n        odd_proxy_names = list(\n            set(sorted_proxy_names) - set(sorted_content_names)\n        )\n        odd_proxy_nodes = [\n            proxy_nodes_by_name[x] for x in odd_proxy_names\n        ]\n        if not sorted_content_names == sorted_proxy_names:\n            raise PublishValidationError(\n                \"Content and proxy nodes need to share the same names.\\n\"\n                \"Content nodes not matching: {}\\n\"\n                \"Proxy nodes not matching: {}\".format(\n                    odd_content_nodes, odd_proxy_nodes\n                )\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py",
    "content": "import pyblish.api\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder, PublishValidationError, RepairAction\n)\n\n\nclass ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin):\n    \"\"\"Validate Arnold Scene Source Cbid.\n\n    It is required for the proxy and content nodes to share the same cbid.\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"assProxy\"]\n    label = \"Validate Arnold Scene Source CBID\"\n    actions = [RepairAction]\n\n    @staticmethod\n    def _get_nodes_by_name(nodes):\n        nodes_by_name = {}\n        for node in nodes:\n            node_name = node.rsplit(\"|\", 1)[-1].rsplit(\":\", 1)[-1]\n            nodes_by_name[node_name] = node\n\n        return nodes_by_name\n\n    @classmethod\n    def get_invalid_couples(cls, instance):\n        nodes_by_name = cls._get_nodes_by_name(instance.data[\"members\"])\n        proxy_nodes_by_name = cls._get_nodes_by_name(instance.data[\"proxy\"])\n\n        invalid_couples = []\n        for content_name, content_node in nodes_by_name.items():\n            proxy_node = proxy_nodes_by_name.get(content_name, None)\n\n            if not proxy_node:\n                cls.log.debug(\n                    \"Content node '{}' has no matching proxy node.\".format(\n                        content_node\n                    )\n                )\n                continue\n\n            content_id = lib.get_id(content_node)\n            proxy_id = lib.get_id(proxy_node)\n            if content_id != proxy_id:\n                invalid_couples.append((content_node, proxy_node))\n\n        return invalid_couples\n\n    def process(self, instance):\n        # Proxy validation.\n        if not instance.data[\"proxy\"]:\n            return\n\n        # Validate for proxy nodes sharing the same cbId as content nodes.\n        invalid_couples = self.get_invalid_couples(instance)\n        if invalid_couples:\n            raise PublishValidationError(\n                \"Found proxy nodes with mismatching cbid:\\n{}\".format(\n                    invalid_couples\n                )\n            )\n\n    @classmethod\n    def repair(cls, instance):\n        for content_node, proxy_node in cls.get_invalid_couples(instance):\n            lib.set_id(proxy_node, lib.get_id(content_node), overwrite=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py",
    "content": "import os\nimport types\n\nimport maya.cmds as cmds\nfrom mtoa.core import createOptions\n\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateAssRelativePaths(pyblish.api.InstancePlugin,\n                               OptionalPyblishPluginMixin):\n    \"\"\"Ensure exporting ass file has set relative texture paths\"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = ['ass']\n    label = \"ASS has relative texture paths\"\n    actions = [RepairAction]\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        # we cannot ask this until user open render settings as\n        # `defaultArnoldRenderOptions` doesn't exist\n        errors = []\n\n        try:\n            absolute_texture = cmds.getAttr(\n                \"defaultArnoldRenderOptions.absolute_texture_paths\")\n            absolute_procedural = cmds.getAttr(\n                \"defaultArnoldRenderOptions.absolute_procedural_paths\")\n            texture_search_path = cmds.getAttr(\n                \"defaultArnoldRenderOptions.tspath\"\n            )\n            procedural_search_path = cmds.getAttr(\n                \"defaultArnoldRenderOptions.pspath\"\n            )\n        except ValueError:\n            raise PublishValidationError(\n                \"Default Arnold options has not been created yet.\"\n            )\n\n        scene_dir, scene_basename = os.path.split(cmds.file(q=True, loc=True))\n        scene_name, _ = os.path.splitext(scene_basename)\n\n        if self.maya_is_true(absolute_texture):\n            errors.append(\"Texture path is set to be absolute\")\n        if self.maya_is_true(absolute_procedural):\n            errors.append(\"Procedural path is set to be absolute\")\n\n        anatomy = instance.context.data[\"anatomy\"]\n\n        # Use project root variables for multiplatform support, see:\n        # https://docs.arnoldrenderer.com/display/A5AFMUG/Search+Path\n        # ':' as path separator is supported by Arnold for all platforms.\n        keys = anatomy.root_environments().keys()\n        paths = []\n        for k in keys:\n            paths.append(\"[{}]\".format(k))\n\n        self.log.debug(\"discovered roots: {}\".format(\":\".join(paths)))\n\n        if \":\".join(paths) not in texture_search_path:\n            errors.append((\n                \"Project roots {} are not in texture_search_path: {}\"\n            ).format(paths, texture_search_path))\n\n        if \":\".join(paths) not in procedural_search_path:\n            errors.append((\n                \"Project roots {} are not in procedural_search_path: {}\"\n            ).format(paths, procedural_search_path))\n\n        if errors:\n            raise PublishValidationError(\"\\n\".join(errors))\n\n    @classmethod\n    def repair(cls, instance):\n        createOptions()\n\n        texture_path = cmds.getAttr(\"defaultArnoldRenderOptions.tspath\")\n        procedural_path = cmds.getAttr(\"defaultArnoldRenderOptions.pspath\")\n\n        # Use project root variables for multiplatform support, see:\n        # https://docs.arnoldrenderer.com/display/A5AFMUG/Search+Path\n        # ':' as path separator is supported by Arnold for all platforms.\n        anatomy = instance.context.data[\"anatomy\"]\n        keys = anatomy.root_environments().keys()\n        paths = []\n        for k in keys:\n            paths.append(\"[{}]\".format(k))\n\n        cmds.setAttr(\n            \"defaultArnoldRenderOptions.tspath\",\n            \":\".join([p for p in paths + [texture_path] if p]),\n            type=\"string\"\n        )\n        cmds.setAttr(\n            \"defaultArnoldRenderOptions.absolute_texture_paths\",\n            False\n        )\n\n        cmds.setAttr(\n            \"defaultArnoldRenderOptions.pspath\",\n            \":\".join([p for p in paths + [procedural_path] if p]),\n            type=\"string\"\n        )\n        cmds.setAttr(\n            \"defaultArnoldRenderOptions.absolute_procedural_paths\",\n            False\n        )\n\n    @staticmethod\n    def find_absolute_path(relative_path, all_root_paths):\n        for root_path in all_root_paths:\n            possible_path = os.path.join(root_path, relative_path)\n            if os.path.exists(possible_path):\n                return possible_path\n\n    def maya_is_true(self, attr_val):\n        \"\"\"\n        Whether a Maya attr evaluates to True.\n        When querying an attribute value from an ambiguous object the\n        Maya API will return a list of values, which need to be properly\n        handled to evaluate properly.\n        \"\"\"\n        if isinstance(attr_val, bool):\n            return attr_val\n        elif isinstance(attr_val, (list, types.GeneratorType)):\n            return any(attr_val)\n        else:\n            return bool(attr_val)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_assembly_name.py",
    "content": "import pyblish.api\nimport maya.cmds as cmds\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateAssemblyName(pyblish.api.InstancePlugin,\n                           OptionalPyblishPluginMixin):\n    \"\"\" Ensure Assembly name ends with `GRP`\n\n    Check if assembly name ends with `_GRP` string.\n    \"\"\"\n\n    label = \"Validate Assembly Name\"\n    order = pyblish.api.ValidatorOrder\n    families = [\"assembly\"]\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    active = False\n    optional = True\n\n    @classmethod\n    def get_invalid(cls, instance):\n        cls.log.debug(\"Checking name of {}\".format(instance.name))\n\n        content_instance = instance.data.get(\"setMembers\", None)\n        if not content_instance:\n            cls.log.error(\"Instance has no nodes!\")\n            return True\n\n        # All children will be included in the extracted export so we also\n        # validate *all* descendents of the set members and we skip any\n        # intermediate shapes\n        descendants = cmds.listRelatives(content_instance,\n                                         allDescendents=True,\n                                         fullPath=True) or []\n        descendants = cmds.ls(\n            descendants, noIntermediate=True, type=\"transform\")\n        content_instance = list(set(content_instance + descendants))\n        assemblies = cmds.ls(content_instance, assemblies=True, long=True)\n\n        invalid = []\n        for cr in assemblies:\n            if not cr.endswith('_GRP'):\n                cls.log.error(\"{} doesn't end with _GRP\".format(cr))\n                invalid.append(cr)\n\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\"Found {} invalid named assembly \"\n                               \"items\".format(len(invalid)))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py",
    "content": "import pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateAssemblyNamespaces(pyblish.api.InstancePlugin,\n                                 OptionalPyblishPluginMixin):\n    \"\"\"Ensure namespaces are not nested\n\n    In the outliner an item in a normal namespace looks as following:\n        props_desk_01_:modelDefault\n\n    Any namespace which diverts from that is illegal, example of an illegal\n    namespace:\n        room_study_01_:props_desk_01_:modelDefault\n\n    \"\"\"\n\n    label = \"Validate Assembly Namespaces\"\n    order = pyblish.api.ValidatorOrder\n    families = [\"assembly\"]\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        self.log.debug(\"Checking namespace for %s\" % instance.name)\n        if self.get_invalid(instance):\n            raise PublishValidationError(\"Nested namespaces found\")\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        from maya import cmds\n\n        invalid = []\n        for item in cmds.ls(instance):\n            item_parts = item.split(\"|\", 1)[0].rsplit(\":\")\n            if len(item_parts[:-1]) > 1:\n                invalid.append(item)\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py",
    "content": "import pyblish.api\nfrom maya import cmds\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    RepairAction,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateAssemblyModelTransforms(pyblish.api.InstancePlugin,\n                                      OptionalPyblishPluginMixin):\n    \"\"\"Verify only root nodes of the loaded asset have transformations.\n\n    Note: This check is temporary and is subject to change.\n\n    Example outliner:\n    <> means referenced\n    ===================================================================\n\n    setdress_GRP|\n        props_GRP|\n            barrel_01_:modelDefault|        [can have transforms]\n                <> barrel_01_:barrel_GRP    [CAN'T have transforms]\n\n            fence_01_:modelDefault|         [can have transforms]\n                <> fence_01_:fence_GRP      [CAN'T have transforms]\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder + 0.49\n    label = \"Assembly Model Transforms\"\n    families = [\"assembly\"]\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               RepairAction]\n\n    prompt_message = (\"You are about to reset the matrix to the default values.\"\n                      \" This can alter the look of your scene. \"\n                      \"Are you sure you want to continue?\")\n\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Found {} invalid transforms of assembly \"\n                 \"items\").format(len(invalid)))\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        from openpype.hosts.maya.api import lib\n\n        # Get all transforms in the loaded containers\n        container_roots = cmds.listRelatives(instance.data[\"nodesHierarchy\"],\n                                             children=True,\n                                             type=\"transform\",\n                                             fullPath=True)\n\n        transforms_in_container = cmds.listRelatives(container_roots,\n                                                     allDescendents=True,\n                                                     type=\"transform\",\n                                                     fullPath=True)\n\n        # Extra check due to the container roots still being passed through\n        transforms_in_container = [i for i in transforms_in_container if i\n                                   not in container_roots]\n\n        # Ensure all are identity matrix\n        invalid = []\n        for transform in transforms_in_container:\n            node_matrix = cmds.xform(transform,\n                                     query=True,\n                                     matrix=True,\n                                     objectSpace=True)\n            if not lib.matrix_equals(node_matrix, lib.DEFAULT_MATRIX):\n                invalid.append(transform)\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"Reset matrix for illegally transformed nodes\n\n        We want to ensure the user knows the reset will alter the look of\n        the current scene because the transformations were done on asset\n        nodes instead of the asset top node.\n\n        Args:\n            instance:\n\n        Returns:\n            None\n\n        \"\"\"\n\n        from qtpy import QtWidgets\n\n        from openpype.hosts.maya.api import lib\n\n        # Store namespace in variable, cosmetics thingy\n        choice = QtWidgets.QMessageBox.warning(\n            None,\n            \"Matrix reset\",\n            cls.prompt_message,\n            QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel\n        )\n\n        invalid = cls.get_invalid(instance)\n        if not invalid:\n            cls.log.info(\"No invalid nodes\")\n            return\n\n        if choice:\n            cmds.xform(invalid, matrix=lib.DEFAULT_MATRIX, objectSpace=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_attributes.py",
    "content": "from collections import defaultdict\n\nimport pyblish.api\nfrom maya import cmds\n\nfrom openpype.hosts.maya.api.lib import set_attribute\nfrom openpype.pipeline.publish import (\n    OptionalPyblishPluginMixin, PublishValidationError, RepairAction,\n    ValidateContentsOrder)\n\n\nclass ValidateAttributes(pyblish.api.InstancePlugin,\n                         OptionalPyblishPluginMixin):\n    \"\"\"Ensure attributes are consistent.\n\n    Attributes to validate and their values comes from the\n    \"maya/attributes.json\" preset, which needs this structure:\n        {\n          \"family\": {\n            \"node_name.attribute_name\": attribute_value\n          }\n        }\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Attributes\"\n    hosts = [\"maya\"]\n    actions = [RepairAction]\n    optional = True\n\n    attributes = None\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # Check for preset existence.\n        if not self.attributes:\n            return\n\n        invalid = self.get_invalid(instance, compute=True)\n        if invalid:\n            raise PublishValidationError(\n                \"Found attributes with invalid values: {}\".format(invalid)\n            )\n\n    @classmethod\n    def get_invalid(cls, instance, compute=False):\n        if compute:\n            return cls.get_invalid_attributes(instance)\n        else:\n            return instance.data.get(\"invalid_attributes\", [])\n\n    @classmethod\n    def get_invalid_attributes(cls, instance):\n        invalid_attributes = []\n\n        # Filter families.\n        families = [instance.data[\"family\"]]\n        families += instance.data.get(\"families\", [])\n        families = set(families) & set(cls.attributes.keys())\n        if not families:\n            return []\n\n        # Get all attributes to validate.\n        attributes = defaultdict(dict)\n        for family in families:\n            if family not in cls.attributes:\n                # No attributes to validate for family\n                continue\n\n            for preset_attr, preset_value in cls.attributes[family].items():\n                node_name, attribute_name = preset_attr.split(\".\", 1)\n                attributes[node_name][attribute_name] = preset_value\n\n        if not attributes:\n            return []\n\n        # Get invalid attributes.\n        nodes = cmds.ls(long=True)\n        for node in nodes:\n            node_name = node.rsplit(\"|\", 1)[-1].rsplit(\":\", 1)[-1]\n            if node_name not in attributes:\n                continue\n\n            for attr_name, expected in attributes[node_name].items():\n\n                # Skip if attribute does not exist\n                if not cmds.attributeQuery(attr_name, node=node, exists=True):\n                    continue\n\n                plug = \"{}.{}\".format(node, attr_name)\n                value = cmds.getAttr(plug)\n                if value != expected:\n                    invalid_attributes.append(\n                        {\n                            \"attribute\": plug,\n                            \"expected\": expected,\n                            \"current\": value\n                        }\n                    )\n\n        instance.data[\"invalid_attributes\"] = invalid_attributes\n        return invalid_attributes\n\n    @classmethod\n    def repair(cls, instance):\n        invalid = cls.get_invalid(instance)\n        for data in invalid:\n            node, attr = data[\"attribute\"].split(\".\", 1)\n            value = data[\"expected\"]\n            set_attribute(node=node, attribute=attr, value=value)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_camera_attributes.py",
    "content": "import pyblish.api\nfrom maya import cmds\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateCameraAttributes(pyblish.api.InstancePlugin,\n                               OptionalPyblishPluginMixin):\n    \"\"\"Validates Camera has no invalid attribute keys or values.\n\n    The Alembic file format does not a specific subset of attributes as such\n    we validate that no values are set there as the output will not match the\n    current scene. For example the preScale, film offsets and film roll.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    families = ['camera']\n    hosts = ['maya']\n    label = 'Camera Attributes'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = True\n\n    DEFAULTS = [\n        (\"filmFitOffset\", 0.0),\n        (\"horizontalFilmOffset\", 0.0),\n        (\"verticalFilmOffset\", 0.0),\n        (\"preScale\", 1.0),\n        (\"filmTranslateH\", 0.0),\n        (\"filmTranslateV\", 0.0),\n        (\"filmRollValue\", 0.0)\n    ]\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        # get cameras\n        members = instance.data['setMembers']\n        shapes = cmds.ls(members, dag=True, shapes=True, long=True)\n        cameras = cmds.ls(shapes, type='camera', long=True)\n\n        invalid = set()\n        for cam in cameras:\n\n            for attr, default_value in cls.DEFAULTS:\n                plug = \"{}.{}\".format(cam, attr)\n                value = cmds.getAttr(plug)\n\n                # Check if is default value\n                if value != default_value:\n                    cls.log.warning(\"Invalid attribute value: {0} \"\n                                    \"(should be: {1}))\".format(plug,\n                                                               default_value))\n                    invalid.add(cam)\n\n                if cmds.listConnections(plug, source=True, destination=False):\n                    # TODO: Validate correctly whether value always correct\n                    cls.log.warning(\"%s has incoming connections, validation \"\n                                    \"is unpredictable.\" % plug)\n\n        return list(invalid)\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\n                \"Invalid camera attributes: {}\".format(invalid))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_camera_contents.py",
    "content": "import pyblish.api\nfrom maya import cmds\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin)\n\n\nclass ValidateCameraContents(pyblish.api.InstancePlugin,\n                             OptionalPyblishPluginMixin):\n    \"\"\"Validates Camera instance contents.\n\n    A Camera instance may only hold a SINGLE camera's transform, nothing else.\n\n    It may hold a \"locator\" as shape, but different shapes are down the\n    hierarchy.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    families = ['camera']\n    hosts = ['maya']\n    label = 'Camera Contents'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    validate_shapes = True\n    optional = False\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        # get cameras\n        members = instance.data['setMembers']\n        shapes = cmds.ls(members, dag=True, shapes=True, long=True)\n\n        # single camera\n        invalid = []\n        cameras = cmds.ls(shapes, type='camera', long=True)\n        if len(cameras) != 1:\n            cls.log.error(\"Camera instance must have a single camera. \"\n                          \"Found {0}: {1}\".format(len(cameras), cameras))\n            invalid.extend(cameras)\n\n            # We need to check this edge case because returning an extended\n            # list when there are no actual cameras results in\n            # still an empty 'invalid' list\n            if len(cameras) < 1:\n                if members:\n                    # If there are members in the instance return all of\n                    # them as 'invalid' so the user can still select invalid\n                    cls.log.error(\"No cameras found in instance \"\n                                  \"members: {}\".format(members))\n                    return members\n\n                raise PublishValidationError(\n                    \"No cameras found in empty instance.\")\n\n        if not cls.validate_shapes:\n            cls.log.debug(\"Not validating shapes in the camera content\"\n                          \" because 'validate shapes' is disabled\")\n            return invalid\n\n        # non-camera shapes\n        valid_shapes = cmds.ls(shapes, type=('camera', 'locator'), long=True)\n        shapes = set(shapes) - set(valid_shapes)\n        if shapes:\n            shapes = list(shapes)\n            cls.log.error(\"Camera instance should only contain camera \"\n                          \"shapes. Found: {0}\".format(shapes))\n            invalid.extend(shapes)\n\n        invalid = list(set(invalid))\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\"Invalid camera contents: \"\n                               \"{0}\".format(invalid))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_color_sets.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateMeshOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError,\n    RepairAction\n)\n\n\nclass ValidateColorSets(pyblish.api.Validator,\n                        OptionalPyblishPluginMixin):\n    \"\"\"Validate all meshes in the instance have unlocked normals\n\n    These can be removed manually through:\n        Modeling > Mesh Display > Color Sets Editor\n\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = ['maya']\n    families = ['model']\n    label = 'Mesh ColorSets'\n    actions = [\n        openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction\n    ]\n    optional = True\n\n    @staticmethod\n    def has_color_sets(mesh):\n        \"\"\"Return whether a mesh node has locked normals\"\"\"\n        return cmds.polyColorSet(mesh,\n                                 allColorSets=True,\n                                 query=True)\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Return the meshes with ColorSets in instance\"\"\"\n\n        meshes = cmds.ls(instance, type='mesh', long=True)\n        return [mesh for mesh in meshes if cls.has_color_sets(mesh)]\n\n    def process(self, instance):\n        \"\"\"Raise invalid when any of the meshes have ColorSets\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\n                message=\"Meshes found with Color Sets: {0}\".format(invalid)\n            )\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"Remove all Color Sets on the meshes in this instance.\"\"\"\n        invalid = cls.get_invalid(instance)\n        for mesh in invalid:\n            for set in cmds.polyColorSet(mesh, acs=True, q=True):\n                cmds.polyColorSet(mesh, colorSet=set, delete=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py",
    "content": "import pyblish.api\n\nfrom maya import cmds\nfrom openpype.pipeline.publish import (\n    context_plugin_should_run,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin,\n                                             OptionalPyblishPluginMixin):\n    \"\"\"Validate if current render layer has a renderable camera\n\n    There is a bug in Redshift which occurs when the current render layer\n    at file open has no renderable camera. The error raised is as follows:\n\n    \"No renderable cameras found. Aborting render\"\n\n    This error is raised even if that render layer will not be rendered.\n\n    \"\"\"\n\n    label = \"Current Render Layer Has Renderable Camera\"\n    order = pyblish.api.ValidatorOrder\n    hosts = [\"maya\"]\n    families = [\"renderlayer\"]\n    optional = False\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            return\n        # Workaround bug pyblish-base#250\n        if not context_plugin_should_run(self, context):\n            return\n\n        layer = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True)\n        cameras = cmds.ls(type=\"camera\", long=True)\n        renderable = any(c for c in cameras if cmds.getAttr(c + \".renderable\"))\n        assert renderable, (\"Current render layer '%s' has no renderable \"\n                            \"camera\" % layer)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_cycle_error.py",
    "content": "import pyblish.api\nfrom maya import cmds\n\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api.lib import maintained_selection\nfrom openpype.pipeline.publish import (\n    OptionalPyblishPluginMixin, PublishValidationError, ValidateContentsOrder)\n\n\nclass ValidateCycleError(pyblish.api.InstancePlugin,\n                         OptionalPyblishPluginMixin):\n    \"\"\"Validate nodes produce no cycle errors.\"\"\"\n\n    order = ValidateContentsOrder + 0.05\n    label = \"Cycle Errors\"\n    hosts = [\"maya\"]\n    families = [\"rig\"]\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Nodes produce a cycle error: {}\".format(invalid))\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        with maintained_selection():\n            cmds.select(instance[:], noExpand=True)\n            plugs = cmds.cycleCheck(all=False,  # check selection only\n                                    list=True)\n            invalid = cmds.ls(plugs, objectsOnly=True, long=True)\n            return invalid\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_frame_range.py",
    "content": "import pyblish.api\n\nfrom maya import cmds\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.hosts.maya.api.lib_rendersetup import (\n    get_attr_overrides,\n    get_attr_in_layer,\n)\nfrom maya.app.renderSetup.model.override import AbsOverride\n\n\nclass ValidateFrameRange(pyblish.api.InstancePlugin,\n                         OptionalPyblishPluginMixin):\n    \"\"\"Validates the frame ranges.\n\n    This is an optional validator checking if the frame range on instance\n    matches the frame range specified for the asset.\n\n    It also validates render frame ranges of render layers.\n\n    Repair action will change everything to match the asset frame range.\n\n    This can be turned off by the artist to allow custom ranges.\n    \"\"\"\n\n    label = \"Validate Frame Range\"\n    order = ValidateContentsOrder\n    families = [\"animation\",\n                \"pointcache\",\n                \"camera\",\n                \"proxyAbc\",\n                \"renderlayer\",\n                \"review\",\n                \"yeticache\"]\n    optional = True\n    actions = [RepairAction]\n    exclude_families = []\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        context = instance.context\n        if instance.data.get(\"tileRendering\"):\n            self.log.debug(\n                \"Skipping frame range validation because \"\n                \"tile rendering is enabled.\"\n            )\n            return\n\n        frame_start_handle = int(context.data.get(\"frameStartHandle\"))\n        frame_end_handle = int(context.data.get(\"frameEndHandle\"))\n        handle_start = int(context.data.get(\"handleStart\"))\n        handle_end = int(context.data.get(\"handleEnd\"))\n        frame_start = int(context.data.get(\"frameStart\"))\n        frame_end = int(context.data.get(\"frameEnd\"))\n\n        inst_start = int(instance.data.get(\"frameStartHandle\"))\n        inst_end = int(instance.data.get(\"frameEndHandle\"))\n        inst_frame_start = int(instance.data.get(\"frameStart\"))\n        inst_frame_end = int(instance.data.get(\"frameEnd\"))\n        inst_handle_start = int(instance.data.get(\"handleStart\"))\n        inst_handle_end = int(instance.data.get(\"handleEnd\"))\n\n        # basic sanity checks\n        assert frame_start_handle <= frame_end_handle, (\n            \"start frame is lower then end frame\")\n\n        # compare with data on instance\n        errors = []\n        if [ef for ef in self.exclude_families\n                if instance.data[\"family\"] in ef]:\n            return\n        if (inst_start != frame_start_handle):\n            errors.append(\"Instance start frame [ {} ] doesn't \"\n                          \"match the one set on asset [ {} ]: \"\n                          \"{}/{}/{}/{} (handle/start/end/handle)\".format(\n                              inst_start,\n                              frame_start_handle,\n                              handle_start, frame_start, frame_end, handle_end\n                          ))\n\n        if (inst_end != frame_end_handle):\n            errors.append(\"Instance end frame [ {} ] doesn't \"\n                          \"match the one set on asset [ {} ]: \"\n                          \"{}/{}/{}/{} (handle/start/end/handle)\".format(\n                              inst_end,\n                              frame_end_handle,\n                              handle_start, frame_start, frame_end, handle_end\n                          ))\n\n        checks = {\n            \"frame start\": (frame_start, inst_frame_start),\n            \"frame end\": (frame_end, inst_frame_end),\n            \"handle start\": (handle_start, inst_handle_start),\n            \"handle end\": (handle_end, inst_handle_end)\n        }\n        for label, values in checks.items():\n            if values[0] != values[1]:\n                errors.append(\n                    \"{} on instance ({}) does not match with the asset \"\n                    \"({}).\".format(label.title(), values[1], values[0])\n                )\n\n        if errors:\n            report = \"Frame range settings are incorrect.\\n\\n\"\n            for error in errors:\n                report += \"- {}\\n\\n\".format(error)\n\n            raise PublishValidationError(report, title=\"Frame Range incorrect\")\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"\n        Repair instance container to match asset data.\n        \"\"\"\n\n        if \"renderlayer\" in instance.data.get(\"families\"):\n            # Special behavior for renderlayers\n            cls.repair_renderlayer(instance)\n            return\n\n        node = instance.data[\"name\"]\n        context = instance.context\n\n        frame_start_handle = int(context.data.get(\"frameStartHandle\"))\n        frame_end_handle = int(context.data.get(\"frameEndHandle\"))\n        handle_start = int(context.data.get(\"handleStart\"))\n        handle_end = int(context.data.get(\"handleEnd\"))\n        frame_start = int(context.data.get(\"frameStart\"))\n        frame_end = int(context.data.get(\"frameEnd\"))\n\n        # Start\n        if cmds.attributeQuery(\"handleStart\", node=node, exists=True):\n            cmds.setAttr(\"{}.handleStart\".format(node), handle_start)\n            cmds.setAttr(\"{}.frameStart\".format(node), frame_start)\n        else:\n            # Include start handle in frame start if no separate handleStart\n            # attribute exists on the node\n            cmds.setAttr(\"{}.frameStart\".format(node), frame_start_handle)\n\n        # End\n        if cmds.attributeQuery(\"handleEnd\", node=node, exists=True):\n            cmds.setAttr(\"{}.handleEnd\".format(node), handle_end)\n            cmds.setAttr(\"{}.frameEnd\".format(node), frame_end)\n        else:\n            # Include end handle in frame end if no separate handleEnd\n            # attribute exists on the node\n            cmds.setAttr(\"{}.frameEnd\".format(node), frame_end_handle)\n\n    @classmethod\n    def repair_renderlayer(cls, instance):\n        \"\"\"Apply frame range in render settings\"\"\"\n\n        layer = instance.data[\"renderlayer\"]\n        context = instance.context\n\n        start_attr = \"defaultRenderGlobals.startFrame\"\n        end_attr = \"defaultRenderGlobals.endFrame\"\n\n        frame_start_handle = int(context.data.get(\"frameStartHandle\"))\n        frame_end_handle = int(context.data.get(\"frameEndHandle\"))\n\n        cls._set_attr_in_layer(start_attr, layer, frame_start_handle)\n        cls._set_attr_in_layer(end_attr, layer, frame_end_handle)\n\n    @classmethod\n    def _set_attr_in_layer(cls, node_attr, layer, value):\n\n        if get_attr_in_layer(node_attr, layer=layer) == value:\n            # Already ok. This can happen if you have multiple renderlayers\n            # validated and there are no frame range overrides. The first\n            # layer's repair would have fixed the global value already\n            return\n\n        overrides = list(get_attr_overrides(node_attr, layer=layer))\n        if overrides:\n            # We set the last absolute override if it is an absolute override\n            # otherwise we'll add an Absolute override\n            last_override = overrides[-1][1]\n            if not isinstance(last_override, AbsOverride):\n                collection = last_override.parent()\n                node, attr = node_attr.split(\".\", 1)\n                last_override = collection.createAbsoluteOverride(node, attr)\n\n            cls.log.debug(\"Setting {attr} absolute override in \"\n                          \"layer '{layer}': {value}\".format(layer=layer,\n                                                            attr=node_attr,\n                                                            value=value))\n            cmds.setAttr(last_override.name() + \".attrValue\", value)\n\n        else:\n            # Set the attribute directly\n            # (Note that this will set the global attribute)\n            cls.log.debug(\"Setting global {attr}: {value}\".format(\n                attr=node_attr,\n                value=value\n            ))\n            cmds.setAttr(node_attr, value)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_glsl_material.py",
    "content": "import os\nfrom maya import cmds\n\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder\n)\nfrom openpype.pipeline import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateGLSLMaterial(pyblish.api.InstancePlugin,\n                           OptionalPyblishPluginMixin):\n    \"\"\"\n    Validate if the asset uses GLSL Shader\n    \"\"\"\n\n    order = ValidateContentsOrder + 0.1\n    families = ['gltf']\n    hosts = ['maya']\n    label = 'GLSL Shader for GLTF'\n    actions = [RepairAction]\n    optional = True\n    active = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        shading_grp = self.get_material_from_shapes(instance)\n        if not shading_grp:\n            raise PublishValidationError(\"No shading group found\")\n        invalid = self.get_texture_shader_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\"Non GLSL Shader found: \"\n                                         \"{0}\".format(invalid))\n\n    def get_material_from_shapes(self, instance):\n        shapes = cmds.ls(instance, type=\"mesh\", long=True)\n        for shape in shapes:\n            shading_grp = cmds.listConnections(shape,\n                                               destination=True,\n                                               type=\"shadingEngine\")\n\n            return shading_grp or []\n\n    def get_texture_shader_invalid(self, instance):\n\n        invalid = set()\n        shading_grp = self.get_material_from_shapes(instance)\n        for shading_group in shading_grp:\n            material_name = \"{}.surfaceShader\".format(shading_group)\n            material = cmds.listConnections(material_name,\n                                            source=True,\n                                            destination=False,\n                                            type=\"GLSLShader\")\n\n            if not material:\n                # add material name\n                material = cmds.listConnections(material_name)[0]\n                invalid.add(material)\n\n        return list(invalid)\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"\n        Repair instance by assigning GLSL Shader\n        to the material\n        \"\"\"\n        cls.assign_glsl_shader(instance)\n        return\n\n    @classmethod\n    def assign_glsl_shader(cls, instance):\n        \"\"\"\n        Converting StingrayPBS material to GLSL Shaders\n        for the glb export through Maya2GLTF plugin\n        \"\"\"\n\n        meshes = cmds.ls(instance, type=\"mesh\", long=True)\n        cls.log.debug(\"meshes: {}\".format(meshes))\n        # load the glsl shader plugin\n        cmds.loadPlugin(\"glslShader\", quiet=True)\n\n        for mesh in meshes:\n            # create glsl shader\n            glsl = cmds.createNode('GLSLShader')\n            glsl_shading_grp = cmds.sets(name=glsl + \"SG\", empty=True,\n                                         renderable=True, noSurfaceShader=True)\n            cmds.connectAttr(glsl + \".outColor\",\n                             glsl_shading_grp + \".surfaceShader\")\n\n            # load the maya2gltf shader\n            ogsfx_path = instance.context.data[\"project_settings\"][\"maya\"][\"publish\"][\"ExtractGLB\"][\"ogsfx_path\"]  # noqa\n            if not os.path.exists(ogsfx_path):\n                if ogsfx_path:\n                    # if custom ogsfx path is not specified\n                    # the log below is the warning for the user\n                    cls.log.warning(\"ogsfx shader file \"\n                                    \"not found in {}\".format(ogsfx_path))\n\n                cls.log.debug(\"Searching the ogsfx shader file in \"\n                              \"default maya directory...\")\n                # re-direct to search the ogsfx path in maya_dir\n                ogsfx_path = os.getenv(\"MAYA_APP_DIR\") + ogsfx_path\n                if not os.path.exists(ogsfx_path):\n                    raise PublishValidationError(\"The ogsfx shader file does not \"      # noqa\n                                                 \"exist: {}\".format(ogsfx_path))        # noqa\n\n            cmds.setAttr(glsl + \".shader\", ogsfx_path, typ=\"string\")\n            # list the materials used for the assets\n            shading_grp = cmds.listConnections(mesh,\n                                               destination=True,\n                                               type=\"shadingEngine\")\n\n            # get the materials related to the selected assets\n            for material in shading_grp:\n                pbs_shader = cmds.listConnections(material,\n                                                  destination=True,\n                                                  type=\"StingrayPBS\")\n                if pbs_shader:\n                    cls.pbs_shader_conversion(pbs_shader, glsl)\n                # setting up to relink the texture if\n                # the mesh is with aiStandardSurface\n                arnold_shader = cmds.listConnections(material,\n                                                     destination=True,\n                                                     type=\"aiStandardSurface\")\n                if arnold_shader:\n                    cls.arnold_shader_conversion(arnold_shader, glsl)\n\n            cmds.sets(mesh, forceElement=str(glsl_shading_grp))\n\n    @classmethod\n    def pbs_shader_conversion(cls, main_shader, glsl):\n\n        cls.log.debug(\"StringrayPBS detected \"\n                      \"-> Can do texture conversion\")\n\n        for shader in main_shader:\n            # get the file textures related to the PBS Shader\n            albedo = cmds.listConnections(shader +\n                                          \".TEX_color_map\")\n            if albedo:\n                dif_output = albedo[0] + \".outColor\"\n                # get the glsl_shader input\n                # reconnect the file nodes to maya2gltf shader\n                glsl_dif = glsl + \".u_BaseColorTexture\"\n                cmds.connectAttr(dif_output, glsl_dif)\n\n            # connect orm map if there is one\n            orm_packed = cmds.listConnections(shader +\n                                              \".TEX_ao_map\")\n            if orm_packed:\n                orm_output = orm_packed[0] + \".outColor\"\n\n                mtl = glsl + \".u_MetallicTexture\"\n                ao = glsl + \".u_OcclusionTexture\"\n                rough = glsl + \".u_RoughnessTexture\"\n\n                cmds.connectAttr(orm_output, mtl)\n                cmds.connectAttr(orm_output, ao)\n                cmds.connectAttr(orm_output, rough)\n\n            # connect nrm map if there is one\n            nrm = cmds.listConnections(shader +\n                                       \".TEX_normal_map\")\n            if nrm:\n                nrm_output = nrm[0] + \".outColor\"\n                glsl_nrm = glsl + \".u_NormalTexture\"\n                cmds.connectAttr(nrm_output, glsl_nrm)\n\n    @classmethod\n    def arnold_shader_conversion(cls, main_shader, glsl):\n        cls.log.debug(\"aiStandardSurface detected \"\n                      \"-> Can do texture conversion\")\n\n        for shader in main_shader:\n            # get the file textures related to the PBS Shader\n            albedo = cmds.listConnections(shader + \".baseColor\")\n            if albedo:\n                dif_output = albedo[0] + \".outColor\"\n                # get the glsl_shader input\n                # reconnect the file nodes to maya2gltf shader\n                glsl_dif = glsl + \".u_BaseColorTexture\"\n                cmds.connectAttr(dif_output, glsl_dif)\n\n            orm_packed = cmds.listConnections(shader +\n                                              \".specularRoughness\")\n            if orm_packed:\n                orm_output = orm_packed[0] + \".outColor\"\n\n                mtl = glsl + \".u_MetallicTexture\"\n                ao = glsl + \".u_OcclusionTexture\"\n                rough = glsl + \".u_RoughnessTexture\"\n\n                cmds.connectAttr(orm_output, mtl)\n                cmds.connectAttr(orm_output, ao)\n                cmds.connectAttr(orm_output, rough)\n\n            # connect nrm map if there is one\n            bump_node = cmds.listConnections(shader +\n                                             \".normalCamera\")\n            if bump_node:\n                for bump in bump_node:\n                    nrm = cmds.listConnections(bump +\n                                               \".bumpValue\")\n                    if nrm:\n                        nrm_output = nrm[0] + \".outColor\"\n                        glsl_nrm = glsl + \".u_NormalTexture\"\n                        cmds.connectAttr(nrm_output, glsl_nrm)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py",
    "content": "\nfrom maya import cmds\n\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateGLSLPlugin(pyblish.api.InstancePlugin,\n                         OptionalPyblishPluginMixin):\n    \"\"\"\n    Validate if the asset uses GLSL Shader\n    \"\"\"\n\n    order = ValidateContentsOrder + 0.15\n    families = ['gltf']\n    hosts = ['maya']\n    label = 'maya2glTF plugin'\n    actions = [RepairAction]\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        if not cmds.pluginInfo(\"maya2glTF\", query=True, loaded=True):\n            raise PublishValidationError(\"maya2glTF is not loaded\")\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"\n        Repair instance by enabling the plugin\n        \"\"\"\n        return cmds.loadPlugin(\"maya2glTF\", quiet=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_instance_has_members.py",
    "content": "import pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError\n)\n\n\nclass ValidateInstanceHasMembers(pyblish.api.InstancePlugin):\n    \"\"\"Validates instance objectSet has *any* members.\"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    label = 'Instance has members'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    @classmethod\n    def get_invalid(cls, instance):\n        invalid = list()\n        if not instance.data.get(\"setMembers\"):\n            objectset_name = instance.data['name']\n            invalid.append(objectset_name)\n\n        return invalid\n\n    def process(self, instance):\n        # Allow renderlayer, rendersetup and workfile to be empty\n        skip_families = {\"workfile\", \"renderlayer\", \"rendersetup\"}\n        if instance.data.get(\"family\") in skip_families:\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            # Invalid will always be a single entry, we log the single name\n            name = invalid[0]\n            raise PublishValidationError(\n                title=\"Empty instance\",\n                message=\"Instance '{0}' is empty\".format(name)\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_instance_in_context.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validate if instance asset is the same as context asset.\"\"\"\nfrom __future__ import absolute_import\n\nimport pyblish.api\nfrom openpype import AYON_SERVER_ENABLED\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\nfrom maya import cmds\n\n\nclass ValidateInstanceInContext(pyblish.api.InstancePlugin,\n                                OptionalPyblishPluginMixin):\n    \"\"\"Validator to check if instance asset match context asset.\n\n    When working in per-shot style you always publish data in context of\n    current asset (shot). This validator checks if this is so. It is optional\n    so it can be disabled when needed.\n\n    Action on this validator will select invalid instances in Outliner.\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Instance in same Context\"\n    optional = True\n    hosts = [\"maya\"]\n    actions = [\n        openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction\n    ]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        asset = instance.data.get(\"asset\")\n        context_asset = self.get_context_asset(instance)\n        if asset != context_asset:\n            raise PublishValidationError(\n                message=(\n                    \"Instance '{}' publishes to different asset than current \"\n                    \"context: {}. Current context: {}\".format(\n                        instance.name, asset, context_asset\n                    )\n                ),\n                description=(\n                    \"## Publishing to a different asset\\n\"\n                    \"There are publish instances present which are publishing \"\n                    \"into a different asset than your current context.\\n\\n\"\n                    \"Usually this is not what you want but there can be cases \"\n                    \"where you might want to publish into another asset or \"\n                    \"shot. If that's the case you can disable the validation \"\n                    \"on the instance to ignore it.\"\n                )\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n        return [instance.data[\"instance_node\"]]\n\n    @classmethod\n    def repair(cls, instance):\n        context_asset = cls.get_context_asset(instance)\n        instance_node = instance.data[\"instance_node\"]\n        if AYON_SERVER_ENABLED:\n            asset_name_attr = \"folderPath\"\n        else:\n            asset_name_attr = \"asset\"\n        cmds.setAttr(\n            \"{}.{}\".format(instance_node, asset_name_attr),\n            context_asset,\n            type=\"string\"\n        )\n\n    @staticmethod\n    def get_context_asset(instance):\n        return instance.context.data[\"asset\"]\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_instance_subset.py",
    "content": "import pyblish.api\nimport string\n\nimport six\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError\n)\n\n# Allow only characters, numbers and underscore\nallowed = set(string.ascii_lowercase +\n              string.ascii_uppercase +\n              string.digits +\n              '_')\n\n\ndef validate_name(subset):\n    return all(x in allowed for x in subset)\n\n\nclass ValidateSubsetName(pyblish.api.InstancePlugin):\n    \"\"\"Validates subset name has only valid characters\"\"\"\n\n    order = ValidateContentsOrder\n    families = [\"*\"]\n    label = \"Subset Name\"\n\n    def process(self, instance):\n\n        subset = instance.data.get(\"subset\", None)\n\n        # Ensure subset data\n        if subset is None:\n            raise PublishValidationError(\"Instance is missing subset \"\n                               \"name: {0}\".format(subset))\n\n        if not isinstance(subset, six.string_types):\n            raise TypeError(\"Instance subset name must be string, \"\n                            \"got: {0} ({1})\".format(subset, type(subset)))\n\n        # Ensure is not empty subset\n        if not subset:\n            raise ValueError(\"Instance subset name is \"\n                             \"empty: {0}\".format(subset))\n\n        # Validate subset characters\n        if not validate_name(subset):\n            raise ValueError(\"Instance subset name contains invalid \"\n                             \"characters: {0}\".format(subset))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_instancer_content.py",
    "content": "import maya.cmds as cmds\nimport pyblish.api\n\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateInstancerContent(pyblish.api.InstancePlugin,\n                               OptionalPyblishPluginMixin):\n    \"\"\"Validates that all meshes in the instance have object IDs.\n\n    This skips a check on intermediate objects because we consider them\n    not important.\n    \"\"\"\n    order = pyblish.api.ValidatorOrder\n    label = 'Instancer Content'\n    families = ['instancer']\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        error = False\n        members = instance.data['setMembers']\n        export_members = instance.data['exactExportMembers']\n\n        self.log.debug(\"Contents {0}\".format(members))\n\n        if not len(members) == len(cmds.ls(members, type=\"instancer\")):\n            self.log.error(\"Instancer can only contain instancers\")\n            error = True\n\n        # TODO: Implement better check for particles are cached\n        if not cmds.ls(export_members, type=\"nucleus\"):\n            self.log.error(\"Instancer must have a connected nucleus\")\n            error = True\n\n        if not cmds.ls(export_members, type=\"cacheFile\"):\n            self.log.error(\"Instancer must be cached\")\n            error = True\n\n        hidden = self.check_geometry_hidden(export_members)\n        if not hidden:\n            error = True\n            self.log.error(\"Instancer input geometry must be hidden \"\n                           \"the scene. Invalid: {0}\".format(hidden))\n\n        # Ensure all in one group\n        parents = cmds.listRelatives(members,\n                                     allParents=True,\n                                     fullPath=True) or []\n        roots = list(set(cmds.ls(parents, assemblies=True, long=True)))\n        if len(roots) > 1:\n            self.log.error(\"Instancer should all be contained in a single \"\n                           \"group. Current roots: {0}\".format(roots))\n            error = True\n\n        if error:\n            raise PublishValidationError(\n                \"Instancer Content is invalid. See log.\")\n\n    def check_geometry_hidden(self, export_members):\n\n        # Ensure all instanced geometry is hidden\n        shapes = cmds.ls(export_members,\n                         dag=True,\n                         shapes=True,\n                         noIntermediate=True)\n        meshes = cmds.ls(shapes, type=\"mesh\")\n\n        visible = [node for node in meshes\n                   if lib.is_visible(node,\n                                     displayLayer=False,\n                                     intermediateObject=False)]\n        if visible:\n            return False\n\n        return True\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py",
    "content": "import os\nimport re\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\ndef is_cache_resource(resource):\n    \"\"\"Return whether resource is a cacheFile resource\"\"\"\n    required = set([\"maya\", \"node\", \"cacheFile\"])\n    tags = resource.get(\"tags\", [])\n    return required.issubset(tags)\n\n\ndef valdidate_files(files):\n    for f in files:\n        assert os.path.exists(f)\n        assert f.endswith(\".mcx\") or f.endswith(\".mcc\")\n\n    return True\n\n\ndef filter_ticks(files):\n    tick_files = set()\n    ticks = set()\n    for path in files:\n        match = re.match(\".+Tick([0-9]+).mcx$\", os.path.basename(path))\n        if match:\n            tick_files.add(path)\n            num = match.group(1)\n            ticks.add(int(num))\n\n    return tick_files, ticks\n\n\nclass ValidateInstancerFrameRanges(pyblish.api.InstancePlugin,\n                                   OptionalPyblishPluginMixin):\n    \"\"\"Validates all instancer particle systems are cached correctly.\n\n    This means they should have the files/frames as required by the start-end\n    frame (including handles).\n\n    This also checks the files exist and checks the \"ticks\" (substeps) files.\n\n    \"\"\"\n    order = pyblish.api.ValidatorOrder\n    label = 'Instancer Cache Frame Ranges'\n    families = ['instancer']\n    optional = False\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        import pyseq\n\n        start_frame = instance.data.get(\"frameStart\", 0)\n        end_frame = instance.data.get(\"frameEnd\", 0)\n        required = range(int(start_frame), int(end_frame) + 1)\n\n        invalid = list()\n        resources = instance.data.get(\"resources\", [])\n\n        for resource in resources:\n            if not is_cache_resource(resource):\n                continue\n\n            node = resource['node']\n            all_files = resource['files'][:]\n            all_lookup = set(all_files)\n\n            # The first file is usually the .xml description file.\n            xml = all_files.pop(0)\n            assert xml.endswith(\".xml\")\n\n            # Ensure all files exist (including ticks)\n            # The remainder file paths should be the .mcx or .mcc files\n            valdidate_files(all_files)\n\n            # Maya particle caches support substeps by saving out additional\n            # files that end with a Tick60.mcx, Tick120.mcx, etc. suffix.\n            # To avoid `pyseq` getting confused we filter those out and then\n            # for each file (except the last frame) check that at least all\n            # ticks exist.\n\n            tick_files, ticks = filter_ticks(all_files)\n            if tick_files:\n                files = [f for f in all_files if f not in tick_files]\n            else:\n                files = all_files\n\n            sequences = pyseq.get_sequences(files)\n            if len(sequences) != 1:\n                invalid.append(node)\n                cls.log.warning(\"More than one sequence found? \"\n                                \"{0} {1}\".format(node, files))\n                cls.log.warning(\"Found caches: {0}\".format(sequences))\n                continue\n\n            sequence = sequences[0]\n            cls.log.debug(\"Found sequence: {0}\".format(sequence))\n\n            start = sequence.start()\n            end = sequence.end()\n\n            if start > start_frame or end < end_frame:\n                invalid.append(node)\n                cls.log.warning(\"Sequence does not have enough \"\n                                \"frames: {0}-{1} (requires: {2}-{3})\"\n                                \"\".format(start, end,\n                                          start_frame,\n                                          end_frame))\n                continue\n\n            # Ensure all frames are present\n            missing = set(sequence.missing())\n            if missing:\n                required_missing = [x for x in required if x in missing]\n                if required_missing:\n                    invalid.append(node)\n                    cls.log.warning(\"Sequence is missing required frames: \"\n                                    \"{0}\".format(required_missing))\n                    continue\n\n            # Ensure all tick files (substep) exist for the files in the folder\n            # for the frames required by the time range.\n            if ticks:\n                ticks = list(sorted(ticks))\n                cls.log.debug(\"Found ticks: {0} \"\n                              \"(substeps: {1})\".format(ticks, len(ticks)))\n\n                # Check all frames except the last since we don't\n                # require subframes after our time range.\n                tick_check_frames = set(required[:-1])\n\n                # Check all frames\n                for item in sequence:\n                    frame = item.frame\n                    if not frame:\n                        invalid.append(node)\n                        cls.log.error(\"Path is not a frame in sequence: \"\n                                      \"{0}\".format(item))\n                        continue\n\n                    # Not required for our time range\n                    if frame not in tick_check_frames:\n                        continue\n\n                    path = item.path\n                    for num in ticks:\n                        base, ext = os.path.splitext(path)\n                        tick_file = base + \"Tick{0}\".format(num) + ext\n                        if tick_file not in all_lookup:\n                            invalid.append(node)\n                            cls.log.warning(\"Tick file found that is not \"\n                                            \"in cache query filenames: \"\n                                            \"{0}\".format(tick_file))\n\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            self.log.error(\"Invalid nodes: {0}\".format(invalid))\n            raise PublishValidationError(\n                (\"Invalid particle caches in instance. \"\n                 \"See logs for details.\"))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py",
    "content": "import os\nimport pyblish.api\nimport maya.cmds as cmds\n\nfrom openpype.pipeline.publish import (\n    RepairContextAction,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateLoadedPlugin(pyblish.api.ContextPlugin,\n                           OptionalPyblishPluginMixin):\n    \"\"\"Ensure there are no unauthorized loaded plugins\"\"\"\n\n    label = \"Loaded Plugin\"\n    order = pyblish.api.ValidatorOrder\n    host = [\"maya\"]\n    actions = [RepairContextAction]\n    optional = True\n\n    @classmethod\n    def get_invalid(cls, context):\n\n        invalid = []\n        loaded_plugin = cmds.pluginInfo(query=True, listPlugins=True)\n        # get variable from OpenPype settings\n        whitelist_native_plugins = cls.whitelist_native_plugins\n        authorized_plugins = cls.authorized_plugins or []\n\n        for plugin in loaded_plugin:\n            if not whitelist_native_plugins and os.getenv('MAYA_LOCATION') \\\n                    in cmds.pluginInfo(plugin, query=True, path=True):\n                continue\n            if plugin not in authorized_plugins:\n                invalid.append(plugin)\n\n        return invalid\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            return\n        invalid = self.get_invalid(context)\n        if invalid:\n            raise PublishValidationError(\n                \"Found forbidden plugin name: {}\".format(\", \".join(invalid))\n            )\n\n    @classmethod\n    def repair(cls, context):\n        \"\"\"Unload forbidden plugins\"\"\"\n\n        for plugin in cls.get_invalid(context):\n            cmds.pluginInfo(plugin, edit=True, autoload=False)\n            cmds.unloadPlugin(plugin, force=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_look_contents.py",
    "content": "import pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    ValidateContentsOrder\n)\n\n\nfrom maya import cmds  # noqa\n\n\nclass ValidateLookContents(pyblish.api.InstancePlugin):\n    \"\"\"Validate look instance contents\n\n    Rules:\n        * Look data must have `relationships` and `attributes` keys.\n        * At least one relationship must be collection.\n        * All relationship object sets at least have an ID value\n\n    Tip:\n        * When no node IDs are found on shadingEngines please save your scene\n        and try again.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    families = ['look']\n    hosts = ['maya']\n    label = 'Look Data Contents'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance\"\"\"\n\n        if not instance[:]:\n            raise PublishValidationError(\"Instance is empty\")\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\"'{}' has invalid look \"\n                               \"content\".format(instance.name))\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Get all invalid nodes\"\"\"\n\n        # check if data has the right attributes and content\n        attributes = cls.validate_lookdata_attributes(instance)\n        # check the looks for ID\n        looks = cls.validate_looks(instance)\n        # check if file nodes have valid files\n        files = cls.validate_files(instance)\n\n        invalid = looks + attributes + files\n\n        return invalid\n\n    @classmethod\n    def validate_lookdata_attributes(cls, instance):\n        \"\"\"Check if the lookData has the required attributes\n\n        Args:\n            instance\n\n        \"\"\"\n\n        invalid = set()\n\n        keys = [\"relationships\", \"attributes\"]\n        lookdata = instance.data[\"lookData\"]\n        for key in keys:\n            if key not in lookdata:\n                cls.log.error(\"Look Data has no key \"\n                              \"'{}'\".format(key))\n                invalid.add(instance.name)\n\n        # Validate at least one single relationship is collected\n        if not lookdata[\"relationships\"]:\n            cls.log.error(\"Look '%s' has no \"\n                          \"`relationships`\" % instance.name)\n            invalid.add(instance.name)\n\n        # Check if attributes are on a node with an ID, crucial for rebuild!\n        for attr_changes in lookdata[\"attributes\"]:\n            if not attr_changes[\"uuid\"] and not attr_changes[\"attributes\"]:\n                cls.log.error(\"Node '%s' has no cbId, please set the \"\n                              \"attributes to its children if it has any\"\n                              % attr_changes[\"name\"])\n                invalid.add(instance.name)\n\n        return list(invalid)\n\n    @classmethod\n    def validate_looks(cls, instance):\n\n        looks = instance.data[\"lookData\"][\"relationships\"]\n        invalid = []\n        for name, data in looks.items():\n            if not data[\"uuid\"]:\n                cls.log.error(\"Look '{}' has no UUID\".format(name))\n                invalid.append(name)\n\n        return invalid\n\n    @classmethod\n    def validate_files(cls, instance):\n\n        invalid = []\n\n        resources = instance.data.get(\"resources\", [])\n        for resource in resources:\n            files = resource[\"files\"]\n            if len(files) == 0:\n                node = resource[\"node\"]\n                cls.log.error(\"File node '%s' uses no or non-existing \"\n                              \"files\" % node)\n                invalid.append(node)\n\n        return invalid\n\n    @classmethod\n    def validate_renderer(cls, instance):\n        # TODO: Rewrite this to be more specific and configurable\n        renderer = cmds.getAttr(\n            'defaultRenderGlobals.currentRenderer').lower()\n        do_maketx = instance.data.get(\"maketx\", False)\n        do_rstex = instance.data.get(\"rstex\", False)\n        processors = []\n\n        if do_maketx:\n            processors.append('arnold')\n        if do_rstex:\n            processors.append('redshift')\n\n        for processor in processors:\n            if processor == renderer:\n                continue\n            else:\n                cls.log.error(\"Converted texture does not match current renderer.\") # noqa\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_look_default_shaders_connections.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    RepairContextAction,\n    PublishValidationError\n)\n\n\nclass ValidateLookDefaultShadersConnections(pyblish.api.ContextPlugin):\n    \"\"\"Validate default shaders in the scene have their default connections.\n\n    For example the standardSurface1 or lambert1 (maya 2023 and before) could\n    potentially be disconnected from the initialShadingGroup. As such it's not\n    lambert1 that will be identified as the default shader which can have\n    unpredictable results.\n\n    To fix the default connections need to be made again. See the logs for\n    more details on which connections are missing.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder - 0.4999\n    families = ['look']\n    hosts = ['maya']\n    label = 'Look Default Shader Connections'\n    actions = [RepairContextAction]\n\n    # The default connections to check\n    DEFAULTS = {\n        \"initialShadingGroup.surfaceShader\": [\"standardSurface1.outColor\",\n                                              \"lambert1.outColor\"],\n        \"initialParticleSE.surfaceShader\": [\"standardSurface1.outColor\",\n                                            \"lambert1.outColor\"],\n        \"initialParticleSE.volumeShader\": [\"particleCloud1.outColor\"]\n    }\n\n    def process(self, context):\n\n        if self.get_invalid():\n            raise PublishValidationError(\n                \"Default shaders in your scene do not have their \"\n                \"default shader connections. Please repair them to continue.\"\n            )\n\n    @classmethod\n    def get_invalid(cls):\n\n        # Process as usual\n        invalid = list()\n        for plug, valid_inputs in cls.DEFAULTS.items():\n            inputs = cmds.listConnections(plug,\n                                          source=True,\n                                          destination=False,\n                                          plugs=True) or None\n            if not inputs or inputs[0] not in valid_inputs:\n                cls.log.error(\n                    \"{0} is not connected to {1}. This can result in \"\n                    \"unexpected behavior. Please reconnect to continue.\"\n                    \"\".format(plug, \" or \".join(valid_inputs))\n                )\n                invalid.append(plug)\n\n        return invalid\n\n    @classmethod\n    def repair(cls, context):\n        invalid = cls.get_invalid()\n        for plug in invalid:\n            valid_inputs = cls.DEFAULTS[plug]\n            for valid_input in valid_inputs:\n                if cmds.objExists(valid_input):\n                    cls.log.info(\n                        \"Connecting {} -> {}\".format(valid_input, plug)\n                    )\n                    cmds.connectAttr(valid_input, plug, force=True)\n                    break\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_look_id_reference_edits.py",
    "content": "from collections import defaultdict\nfrom maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError\n)\n\n\nclass ValidateLookIdReferenceEdits(pyblish.api.InstancePlugin):\n    \"\"\"Validate nodes in look have no reference edits to cbId.\n\n    Note:\n        This only validates the cbId edits on the referenced nodes that are\n        used in the look. For example, a transform can have its cbId changed\n        without being invalidated when it is not used in the look's assignment.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    families = ['look']\n    hosts = ['maya']\n    label = 'Look Id Reference Edits'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               RepairAction]\n\n    def process(self, instance):\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\"Invalid nodes %s\" % (invalid,))\n\n    @staticmethod\n    def get_invalid(instance):\n\n        # Collect all referenced members\n        references = defaultdict(set)\n        relationships = instance.data[\"lookData\"][\"relationships\"]\n        for relationship in relationships.values():\n            for member in relationship['members']:\n                node = member[\"name\"]\n\n                if cmds.referenceQuery(node, isNodeReferenced=True):\n                    ref = cmds.referenceQuery(node, referenceNode=True)\n                    references[ref].add(node)\n\n        # Validate whether any has changes to 'cbId' attribute\n        invalid = list()\n        for ref, nodes in references.items():\n            edits = cmds.referenceQuery(editAttrs=True,\n                                        editNodes=True,\n                                        showDagPath=True,\n                                        showNamespace=True,\n                                        onReferenceNode=ref)\n            for edit in edits:\n\n                # Ensure it is an attribute ending with .cbId\n                # thus also ignore just node edits (like parenting)\n                if not edit.endswith(\".cbId\"):\n                    continue\n\n                # Ensure the attribute is 'cbId' (and not a nested attribute)\n                node, attr = edit.split(\".\", 1)\n                if attr != \"cbId\":\n                    continue\n\n                if node in nodes:\n                    invalid.append(node)\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n\n        invalid = cls.get_invalid(instance)\n\n        # Group invalid nodes by reference node\n        references = defaultdict(set)\n        for node in invalid:\n            ref = cmds.referenceQuery(node, referenceNode=True)\n            references[ref].add(node)\n\n        # Remove the reference edits on the nodes per reference node\n        for ref, nodes in references.items():\n            for node in nodes:\n\n                # Somehow this only works if you run the the removal\n                # per edit command.\n                for command in [\"addAttr\",\n                                \"connectAttr\",\n                                \"deleteAttr\",\n                                \"disconnectAttr\",\n                                \"setAttr\"]:\n                    cmds.referenceEdit(\"{}.cbId\".format(node),\n                                       removeEdits=True,\n                                       successfulEdits=True,\n                                       failedEdits=True,\n                                       editCommand=command,\n                                       onReferenceNode=ref)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_look_no_default_shaders.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError\n)\n\n\nclass ValidateLookNoDefaultShaders(pyblish.api.InstancePlugin):\n    \"\"\"Validate if any node has a connection to a default shader.\n\n    This checks whether the look has any members of:\n    - lambert1\n    - initialShadingGroup\n    - initialParticleSE\n    - particleCloud1\n\n    If any of those is present it will raise an error. A look is not allowed\n    to have any of the \"default\" shaders present in a scene as they can\n    introduce problems when referenced (overriding local scene shaders).\n\n    To fix this no shape nodes in the look must have any of default shaders\n    applied.\n\n    \"\"\"\n\n    order = ValidateContentsOrder + 0.01\n    families = ['look']\n    hosts = ['maya']\n    label = 'Look No Default Shaders'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    DEFAULT_SHADERS = {\"lambert1\", \"initialShadingGroup\",\n                      \"initialParticleSE\", \"particleCloud1\"}\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance\"\"\"\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\"Invalid node relationships found: \"\n                               \"{0}\".format(invalid))\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        invalid = set()\n        for node in instance:\n            # Get shading engine connections\n            shaders = cmds.listConnections(node, type=\"shadingEngine\") or []\n\n            # Check for any disallowed connections on *all* nodes\n            if any(s in cls.DEFAULT_SHADERS for s in shaders):\n\n                # Explicitly log each individual \"wrong\" connection.\n                for s in shaders:\n                    if s in cls.DEFAULT_SHADERS:\n                        cls.log.error(\"Node has unallowed connection to \"\n                                      \"'{}': {}\".format(s, node))\n\n                invalid.add(node)\n\n        return list(invalid)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_look_sets.py",
    "content": "import pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError\n)\n\n\nclass ValidateLookSets(pyblish.api.InstancePlugin):\n    \"\"\"Validate if any sets relationships are not being collected.\n\n    A shader can be assigned to a node that is missing a Colorbleed ID.\n    Because it is missing the ID it has not been collected in the instance.\n    This validator ensures those relationships and thus considers it invalid\n    if a relationship was not collected.\n\n    When the relationship needs to be maintained the artist might need to\n    create a different* relationship or ensure the node has the Colorbleed ID.\n\n    *The relationship might be too broad (assigned to top node of hierarchy).\n    This can be countered by creating the relationship on the shape or its\n    transform. In essence, ensure item the shader is assigned to has the\n    Colorbleed ID!\n\n    Examples:\n\n    - Displacement objectSets (like V-Ray):\n\n        It is best practice to add the transform of the shape to the\n        displacement objectSet. Any parent groups will not work as groups\n        do not receive a Colorbleed Id. As such the assignments need to be\n        made to the shapes and their transform.\n\n        Example content:\n            [asset_GRP|geometry_GRP|body_GES,\n             asset_GRP|geometry_GRP|L_eye_GES,\n             asset_GRP|geometry_GRP|R_eye_GES,\n             asset_GRP|geometry_GRP|wings_GEO]\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    families = ['look']\n    hosts = ['maya']\n    label = 'Look Sets'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance\"\"\"\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\"'{}' has invalid look \"\n                               \"content\".format(instance.name))\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Get all invalid nodes\"\"\"\n\n        relationships = instance.data[\"lookData\"][\"relationships\"]\n        invalid = []\n\n        renderlayer = instance.data.get(\"renderlayer\", \"defaultRenderLayer\")\n        with lib.renderlayer(renderlayer):\n            for node in instance:\n                # get the connected objectSets of the node\n                sets = lib.get_related_sets(node)\n                if not sets:\n                    continue\n\n                # check if any objectSets are not present ion the relationships\n                missing_sets = [s for s in sets if s not in relationships]\n                if missing_sets:\n                    for missing_set in missing_sets:\n                        cls.log.debug(missing_set)\n\n                        if '_SET' not in missing_set:\n                            # A set of this node is not coming along, this is wrong!\n                            cls.log.error(\"Missing sets '{}' for node \"\n                                          \"'{}'\".format(missing_sets, node))\n                            invalid.append(node)\n                            continue\n\n                # Ensure the node is in the sets that are collected\n                for shader_set, data in relationships.items():\n                    if shader_set not in sets:\n                        # no need to check for a set if the node\n                        # isn't in it anyway\n                        continue\n\n                    member_nodes = [member['name'] for member in\n                                    data['members']]\n                    if node not in member_nodes:\n                        # The node is not found in the collected set\n                        # relationships\n                        cls.log.error(\"Missing '{}' in collected set node \"\n                                      \"'{}'\".format(node, shader_set))\n                        invalid.append(node)\n\n                        continue\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_look_shading_group.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateShadingEngine(pyblish.api.InstancePlugin,\n                            OptionalPyblishPluginMixin):\n    \"\"\"Validate all shading engines are named after the surface material.\n\n    Shading engines should be named \"{surface_shader}SG\"\n    \"\"\"\n    order = ValidateContentsOrder\n    families = [\"look\"]\n    hosts = [\"maya\"]\n    label = \"Look Shading Engine Naming\"\n    actions = [\n        openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction\n    ]\n    optional = True\n\n    # The default connections to check\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Found shading engines with incorrect naming:\"\n                \"\\n{}\".format(invalid)\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n        shapes = cmds.ls(instance, type=[\"nurbsSurface\", \"mesh\"], long=True)\n        invalid = []\n        for shape in shapes:\n            shading_engines = cmds.listConnections(\n                shape, destination=True, type=\"shadingEngine\"\n            ) or []\n            for shading_engine in shading_engines:\n                name = (\n                    cmds.listConnections(shading_engine + \".surfaceShader\")[0]\n                    + \"SG\"\n                )\n                if shading_engine != name:\n                    invalid.append(shading_engine)\n\n        return list(set(invalid))\n\n    @classmethod\n    def repair(cls, instance):\n        shading_engines = cls.get_invalid(instance)\n        for shading_engine in shading_engines:\n            name = (\n                cmds.listConnections(shading_engine + \".surfaceShader\")[0]\n                + \"SG\"\n            )\n            cmds.rename(shading_engine, name)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_look_single_shader.py",
    "content": "import pyblish.api\nfrom maya import cmds\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    PublishValidationError, ValidateContentsOrder)\n\n\nclass ValidateSingleShader(pyblish.api.InstancePlugin):\n    \"\"\"Validate all nurbsSurfaces and meshes have exactly one shader assigned.\n\n    This will error if a shape has no shaders or more than one shader.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    families = ['look']\n    hosts = ['maya']\n    label = 'Look Single Shader Per Shape'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    # The default connections to check\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Found shapes which don't have a single shader \"\n                 \"assigned:\\n{}\").format(invalid))\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        # Get all shapes from the instance\n        shapes = cmds.ls(instance, type=[\"nurbsSurface\", \"mesh\"], long=True)\n\n        # Check the number of connected shadingEngines per shape\n        no_shaders = []\n        more_than_one_shaders = []\n        for shape in shapes:\n            shading_engines = cmds.listConnections(shape,\n                                                   destination=True,\n                                                   type=\"shadingEngine\") or []\n\n            # Only interested in unique shading engines.\n            shading_engines = list(set(shading_engines))\n\n            if not shading_engines:\n                no_shaders.append(shape)\n            elif len(shading_engines) > 1:\n                more_than_one_shaders.append(shape)\n\n        if no_shaders:\n            cls.log.error(\"No shaders found on: {}\".format(no_shaders))\n        if more_than_one_shaders:\n            cls.log.error(\"More than one shader found on: \"\n                          \"{}\".format(more_than_one_shaders))\n\n        return no_shaders + more_than_one_shaders\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_maya_units.py",
    "content": "import maya.cmds as cmds\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.lib as mayalib\nfrom openpype.pipeline.context_tools import get_current_project_asset\nfrom openpype.pipeline.publish import (\n    RepairContextAction,\n    ValidateSceneOrder,\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateMayaUnits(pyblish.api.ContextPlugin,\n                        OptionalPyblishPluginMixin):\n    \"\"\"Check if the Maya units are set correct\"\"\"\n\n    order = ValidateSceneOrder\n    label = \"Maya Units\"\n    hosts = ['maya']\n    actions = [RepairContextAction]\n\n    validate_linear_units = True\n    linear_units = \"cm\"\n\n    validate_angular_units = True\n    angular_units = \"deg\"\n\n    validate_fps = True\n\n    nice_message_format = (\n        \"- <b>{setting}</b> must be <b>{required_value}</b>.  \"\n        \"Your scene is set to <b>{current_value}</b>\"\n    )\n    log_message_format = (\n        \"Maya scene {setting} must be '{required_value}'. \"\n        \"Current value is '{current_value}'.\"\n    )\n    optional = False\n\n    @classmethod\n    def apply_settings(cls, project_settings):\n        \"\"\"Apply project settings to creator\"\"\"\n        settings = (\n            project_settings[\"maya\"][\"publish\"][\"ValidateMayaUnits\"]\n        )\n\n        cls.validate_linear_units = settings.get(\"validate_linear_units\",\n                                                 cls.validate_linear_units)\n        cls.linear_units = settings.get(\"linear_units\", cls.linear_units)\n        cls.validate_angular_units = settings.get(\"validate_angular_units\",\n                                                  cls.validate_angular_units)\n        cls.angular_units = settings.get(\"angular_units\", cls.angular_units)\n        cls.validate_fps = settings.get(\"validate_fps\", cls.validate_fps)\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            return\n        # Collected units\n        linearunits = context.data.get('linearUnits')\n        angularunits = context.data.get('angularUnits')\n\n        fps = context.data.get('fps')\n\n        asset_doc = context.data[\"assetEntity\"]\n        asset_fps = mayalib.convert_to_maya_fps(asset_doc[\"data\"][\"fps\"])\n\n        self.log.info('Units (linear): {0}'.format(linearunits))\n        self.log.info('Units (angular): {0}'.format(angularunits))\n        self.log.info('Units (time): {0} FPS'.format(fps))\n\n        invalid = []\n\n        # Check if units are correct\n        if (\n            self.validate_linear_units\n            and linearunits\n            and linearunits != self.linear_units\n        ):\n            invalid.append({\n                \"setting\": \"Linear units\",\n                \"required_value\": self.linear_units,\n                \"current_value\": linearunits\n            })\n\n        if (\n            self.validate_angular_units\n            and angularunits\n            and angularunits != self.angular_units\n        ):\n            invalid.append({\n                \"setting\": \"Angular units\",\n                \"required_value\": self.angular_units,\n                \"current_value\": angularunits\n            })\n\n        if self.validate_fps and fps and fps != asset_fps:\n            invalid.append({\n                \"setting\": \"FPS\",\n                \"required_value\": asset_fps,\n                \"current_value\": fps\n            })\n\n        if invalid:\n\n            issues = []\n            for data in invalid:\n                self.log.error(self.log_message_format.format(**data))\n                issues.append(self.nice_message_format.format(**data))\n            issues = \"\\n\".join(issues)\n\n            raise PublishXmlValidationError(\n                plugin=self,\n                message=\"Invalid maya scene units\",\n                formatting_data={\"issues\": issues}\n            )\n\n    @classmethod\n    def repair(cls, context):\n        \"\"\"Fix the current FPS setting of the scene, set to PAL(25.0 fps)\"\"\"\n\n        cls.log.info(\"Setting angular unit to '{}'\".format(cls.angular_units))\n        cmds.currentUnit(angle=cls.angular_units)\n        current_angle = cmds.currentUnit(query=True, angle=True)\n        cls.log.debug(current_angle)\n\n        cls.log.info(\"Setting linear unit to '{}'\".format(cls.linear_units))\n        cmds.currentUnit(linear=cls.linear_units)\n        current_linear = cmds.currentUnit(query=True, linear=True)\n        cls.log.debug(current_linear)\n\n        cls.log.info(\"Setting time unit to match project\")\n        # TODO replace query with using 'context.data[\"assetEntity\"]'\n        asset_doc = get_current_project_asset()\n        asset_fps = asset_doc[\"data\"][\"fps\"]\n        mayalib.set_scene_fps(asset_fps)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py",
    "content": "from maya import cmds\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api.lib import (\n    maintained_selection,\n    delete_after,\n    undo_chunk,\n    get_attribute,\n    set_attribute\n)\nfrom openpype.pipeline.publish import (\n    OptionalPyblishPluginMixin,\n    RepairAction,\n    ValidateMeshOrder,\n    PublishValidationError\n)\n\n\nclass ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin,\n                                   OptionalPyblishPluginMixin):\n    \"\"\"Validate the mesh has default Arnold attributes.\n\n    It compares all Arnold attributes from a default mesh. This is to ensure\n    later published looks can discover non-default Arnold attributes.\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = [\"maya\"]\n    families = [\"model\"]\n    label = \"Mesh Arnold Attributes\"\n    actions = [\n        openpype.hosts.maya.api.action.SelectInvalidAction,\n        RepairAction\n    ]\n\n    optional = True\n\n    # cache (will be `dict` when cached)\n    arnold_mesh_defaults = None\n\n    @classmethod\n    def get_default_attributes(cls):\n\n        if cls.arnold_mesh_defaults is not None:\n            # Use from cache\n            return cls.arnold_mesh_defaults\n\n        # Get default arnold attribute values for mesh type.\n        defaults = {}\n        with delete_after() as tmp:\n            transform = cmds.createNode(\"transform\", skipSelect=True)\n            tmp.append(transform)\n\n            mesh = cmds.createNode(\"mesh\", parent=transform, skipSelect=True)\n            arnold_attributes = cmds.listAttr(mesh,\n                                              string=\"ai*\",\n                                              fromPlugin=True) or []\n            for attr in arnold_attributes:\n                plug = \"{}.{}\".format(mesh, attr)\n                try:\n                    defaults[attr] = get_attribute(plug)\n                except PublishValidationError:\n                    cls.log.debug(\"Ignoring arnold attribute: {}\".format(attr))\n\n        cls.arnold_mesh_defaults = defaults  # assign cache\n        return defaults\n\n    @classmethod\n    def get_invalid_attributes(cls, instance, compute=False):\n        invalid = []\n\n        if compute:\n\n            meshes = cmds.ls(instance, type=\"mesh\", long=True)\n            if not meshes:\n                return []\n\n            # Compare the values against the defaults\n            defaults = cls.get_default_attributes()\n            for mesh in meshes:\n                for attr_name, default_value in defaults.items():\n                    plug = \"{}.{}\".format(mesh, attr_name)\n                    if get_attribute(plug) != default_value:\n                        invalid.append(plug)\n\n            instance.data[\"nondefault_arnold_attributes\"] = invalid\n\n        return instance.data.get(\"nondefault_arnold_attributes\", [])\n\n    @classmethod\n    def get_invalid(cls, instance):\n        invalid_attrs = cls.get_invalid_attributes(instance, compute=False)\n        invalid_nodes = set(attr.split(\".\", 1)[0] for attr in invalid_attrs)\n        return sorted(invalid_nodes)\n\n    @classmethod\n    def repair(cls, instance):\n        with maintained_selection():\n            with undo_chunk():\n                defaults = cls.get_default_attributes()\n                attributes = cls.get_invalid_attributes(\n                    instance, compute=False\n                )\n                for attr in attributes:\n                    node, attr_name = attr.split(\".\", 1)\n                    value = defaults[attr_name]\n                    set_attribute(\n                        node=node,\n                        attribute=attr_name,\n                        value=value\n                    )\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        if not cmds.pluginInfo(\"mtoa\", query=True, loaded=True):\n            # Arnold attributes only exist if plug-in is loaded\n            return\n\n        invalid = self.get_invalid_attributes(instance, compute=True)\n        if invalid:\n            raise PublishValidationError(\n                \"Non-default Arnold attributes found in instance:\"\n                \" {0}\".format(invalid)\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_empty.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateMeshOrder,\n    PublishValidationError\n)\n\n\nclass ValidateMeshEmpty(pyblish.api.InstancePlugin):\n    \"\"\"Validate meshes have some vertices.\n\n    Its possible to have meshes without any vertices. To replicate\n    this issue, delete all faces/polygons then all edges.\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = [\"maya\"]\n    families = [\"model\"]\n    label = \"Mesh Empty\"\n    actions = [\n        openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction\n    ]\n\n    @classmethod\n    def repair(cls, instance):\n        invalid = cls.get_invalid(instance)\n        for node in invalid:\n            cmds.delete(node)\n\n    @classmethod\n    def get_invalid(cls, instance):\n        invalid = []\n\n        meshes = cmds.ls(instance, type=\"mesh\", long=True)\n        for mesh in meshes:\n            num_vertices = cmds.polyEvaluate(mesh, vertex=True)\n\n            if num_vertices == 0:\n                cls.log.warning(\n                    \"\\\"{}\\\" does not have any vertices.\".format(mesh)\n                )\n                invalid.append(mesh)\n\n        return invalid\n\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Meshes found in instance without any vertices: %s\" % invalid\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_has_uv.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateMeshOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\nfrom openpype.hosts.maya.api.lib import len_flattened\n\n\nclass ValidateMeshHasUVs(pyblish.api.InstancePlugin,\n                         OptionalPyblishPluginMixin):\n    \"\"\"Validate the current mesh has UVs.\n\n    It validates whether the current UV set has non-zero UVs and\n    at least more than the vertex count. It's not really bulletproof,\n    but a simple quick validation to check if there are likely\n    UVs for every face.\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = ['maya']\n    families = ['model']\n    label = 'Mesh Has UVs'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = True\n\n    @classmethod\n    def get_invalid(cls, instance):\n        invalid = []\n\n        for node in cmds.ls(instance, type='mesh'):\n            num_vertices = cmds.polyEvaluate(node, vertex=True)\n\n            if num_vertices == 0:\n                cls.log.warning(\n                    \"Skipping \\\"{}\\\", cause it does not have any \"\n                    \"vertices.\".format(node)\n                )\n                continue\n\n            uv = cmds.polyEvaluate(node, uv=True)\n\n            if uv == 0:\n                invalid.append(node)\n                continue\n\n            vertex = cmds.polyEvaluate(node, vertex=True)\n            if uv < vertex:\n                # Workaround:\n                # Maya can have instanced UVs in a single mesh, for example\n                # imported from an Alembic. With instanced UVs the UV count\n                # from `maya.cmds.polyEvaluate(uv=True)` will only result in\n                # the unique UV count instead of for all vertices.\n                #\n                # Note: Maya can save instanced UVs to `mayaAscii` but cannot\n                #       load this as instanced. So saving, opening and saving\n                #       again will lose this information.\n                map_attr = \"{}.map[*]\".format(node)\n                uv_to_vertex = cmds.polyListComponentConversion(map_attr,\n                                                                toVertex=True)\n                uv_vertex_count = len_flattened(uv_to_vertex)\n                if uv_vertex_count < vertex:\n                    invalid.append(node)\n                else:\n                    cls.log.warning(\"Node has instanced UV points: \"\n                                    \"{0}\".format(node))\n\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n\n            names = \"<br>\".join(\n                \" - {}\".format(node) for node in invalid\n            )\n\n            raise PublishValidationError(\n                title=\"Mesh has missing UVs\",\n                message=\"Model meshes are required to have UVs.<br><br>\"\n                        \"Meshes detected with invalid or missing UVs:<br>\"\n                        \"{0}\".format(names)\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateMeshOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateMeshLaminaFaces(pyblish.api.InstancePlugin,\n                              OptionalPyblishPluginMixin):\n    \"\"\"Validate meshes don't have lamina faces.\n\n    Lamina faces share all of their edges.\n\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = ['maya']\n    families = ['model']\n    label = 'Mesh Lamina Faces'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = True\n\n    @staticmethod\n    def get_invalid(instance):\n        meshes = cmds.ls(instance, type='mesh', long=True)\n        invalid = [mesh for mesh in meshes if\n                   cmds.polyInfo(mesh, laminaFaces=True)]\n\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance 'objectSet'\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise ValueError(\"Meshes found with lamina faces: \"\n                             \"{0}\".format(invalid))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateMeshNgons(pyblish.api.Validator,\n                        OptionalPyblishPluginMixin):\n    \"\"\"Ensure that meshes don't have ngons\n\n    Ngon are faces with more than 4 sides.\n\n    To debug the problem on the meshes you can use Maya's modeling\n    tool: \"Mesh > Cleanup...\"\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"model\"]\n    label = \"Mesh ngons\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = True\n\n    @staticmethod\n    def get_invalid(instance):\n\n        meshes = cmds.ls(instance, type='mesh', long=True)\n\n        # Get all faces\n        faces = ['{0}.f[*]'.format(node) for node in meshes]\n\n        # Filter to n-sided polygon faces (ngons)\n        invalid = lib.polyConstraint(faces,\n                                     t=0x0008,  # type=face\n                                     size=3)    # size=nsided\n\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance \"objectSet\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise ValueError(\"Meshes found with n-gons\"\n                             \"values: {0}\".format(invalid))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateMeshOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\ndef _as_report_list(values, prefix=\"- \", suffix=\"\\n\"):\n    \"\"\"Return list as bullet point list for a report\"\"\"\n    if not values:\n        return \"\"\n    return prefix + (suffix + prefix).join(values)\n\n\nclass ValidateMeshNoNegativeScale(pyblish.api.Validator,\n                                  OptionalPyblishPluginMixin):\n    \"\"\"Ensure that meshes don't have a negative scale.\n\n    Using negatively scaled proxies in a VRayMesh results in inverted\n    normals. As such we want to avoid this.\n\n    We also avoid this on the rig or model because these are often the\n    previous steps for those that are cached to proxies so we can catch this\n    issue early.\n\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = ['maya']\n    families = ['model']\n    label = 'Mesh No Negative Scale'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    @staticmethod\n    def get_invalid(instance):\n        meshes = cmds.ls(instance,\n                         type='mesh',\n                         long=True,\n                         noIntermediate=True)\n\n        invalid = []\n        for mesh in meshes:\n            transform = cmds.listRelatives(mesh, parent=True, fullPath=True)[0]\n            scale = cmds.getAttr(\"{0}.scale\".format(transform))[0]\n\n            if any(x < 0 for x in scale):\n                invalid.append(mesh)\n\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance 'objectSet'\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\n                \"Meshes found with negative scale:\\n\\n{0}\".format(\n                    _as_report_list(sorted(invalid))\n                ),\n                title=\"Negative scale\"\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_non_manifold.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateMeshOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\ndef _as_report_list(values, prefix=\"- \", suffix=\"\\n\"):\n    \"\"\"Return list as bullet point list for a report\"\"\"\n    if not values:\n        return \"\"\n    return prefix + (suffix + prefix).join(values)\n\n\nclass ValidateMeshNonManifold(pyblish.api.Validator,\n                              OptionalPyblishPluginMixin):\n    \"\"\"Ensure that meshes don't have non-manifold edges or vertices\n\n    To debug the problem on the meshes you can use Maya's modeling\n    tool: \"Mesh > Cleanup...\"\n\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = ['maya']\n    families = ['model']\n    label = 'Mesh Non-Manifold Edges/Vertices'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = True\n\n    @staticmethod\n    def get_invalid(instance):\n\n        meshes = cmds.ls(instance, type='mesh', long=True)\n\n        invalid = []\n        for mesh in meshes:\n            if (cmds.polyInfo(mesh, nonManifoldVertices=True) or\n                    cmds.polyInfo(mesh, nonManifoldEdges=True)):\n                invalid.append(mesh)\n\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance 'objectSet'\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\n                \"Meshes found with non-manifold edges/vertices:\\n\\n{0}\".format(\n                    _as_report_list(sorted(invalid))\n                ),\n                title=\"Non-Manifold Edges/Vertices\"\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_non_zero_edge.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    ValidateMeshOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\nclass ValidateMeshNonZeroEdgeLength(pyblish.api.InstancePlugin,\n                                    OptionalPyblishPluginMixin):\n    \"\"\"Validate meshes don't have edges with a zero length.\n\n    Based on Maya's polyCleanup 'Edges with zero length'.\n\n    Note:\n        This can be slow for high-res meshes.\n\n    \"\"\"\n\n    order = ValidateMeshOrder\n    families = ['model']\n    hosts = ['maya']\n    label = 'Mesh Edge Length Non Zero'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = True\n\n    __tolerance = 1e-5\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Return the invalid edges.\n\n        Also see:\n\n        http://help.autodesk.com/view/MAYAUL/2015/ENU/?guid=Mesh__Cleanup\n\n        \"\"\"\n\n        meshes = cmds.ls(instance, type='mesh', long=True)\n        if not meshes:\n            return list()\n\n        valid_meshes = []\n        for mesh in meshes:\n            num_vertices = cmds.polyEvaluate(mesh, vertex=True)\n\n            if num_vertices == 0:\n                cls.log.warning(\n                    \"Skipping \\\"{}\\\", cause it does not have any \"\n                    \"vertices.\".format(mesh)\n                )\n                continue\n\n            valid_meshes.append(mesh)\n\n        # Get all edges\n        edges = ['{0}.e[*]'.format(node) for node in valid_meshes]\n\n        # Filter by constraint on edge length\n        invalid = lib.polyConstraint(edges,\n                                     t=0x8000,  # type=edge\n                                     length=1,\n                                     lengthbound=(0, cls.__tolerance))\n\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Process all meshes\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            label = \"Meshes found with zero edge length\"\n            raise PublishValidationError(\n                message=\"{}: {}\".format(label, invalid),\n                title=label,\n                description=\"{}:\\n- \".format(label) + \"\\n- \".join(invalid)\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py",
    "content": "from maya import cmds\nimport maya.api.OpenMaya as om2\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateMeshOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\ndef _as_report_list(values, prefix=\"- \", suffix=\"\\n\"):\n    \"\"\"Return list as bullet point list for a report\"\"\"\n    if not values:\n        return \"\"\n    return prefix + (suffix + prefix).join(values)\n\n\nclass ValidateMeshNormalsUnlocked(pyblish.api.Validator,\n                                  OptionalPyblishPluginMixin):\n    \"\"\"Validate all meshes in the instance have unlocked normals\n\n    These can be unlocked manually through:\n        Modeling > Mesh Display > Unlock Normals\n\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = ['maya']\n    families = ['model']\n    label = 'Mesh Normals Unlocked'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               RepairAction]\n    optional = True\n\n    @staticmethod\n    def has_locked_normals(mesh):\n        \"\"\"Return whether mesh has at least one locked normal\"\"\"\n\n        sel = om2.MGlobal.getSelectionListByName(mesh)\n        node = sel.getDependNode(0)\n        fn_mesh = om2.MFnMesh(node)\n        _, normal_ids = fn_mesh.getNormalIds()\n        for normal_id in normal_ids:\n            if fn_mesh.isNormalLocked(normal_id):\n                return True\n        return False\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Return the meshes with locked normals in instance\"\"\"\n\n        meshes = cmds.ls(instance, type='mesh', long=True)\n        return [mesh for mesh in meshes if cls.has_locked_normals(mesh)]\n\n    def process(self, instance):\n        \"\"\"Raise invalid when any of the meshes have locked normals\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\n                \"Meshes found with locked normals:\\n\\n{0}\".format(\n                    _as_report_list(sorted(invalid))\n                ),\n                title=\"Locked normals\"\n            )\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"Unlocks all normals on the meshes in this instance.\"\"\"\n        invalid = cls.get_invalid(instance)\n        for mesh in invalid:\n            cmds.polyNormalPerVertex(mesh, unFreezeNormal=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_overlapping_uvs.py",
    "content": "import math\nfrom six.moves import xrange\n\nfrom maya import cmds\nimport maya.api.OpenMaya as om\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateMeshOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\ndef _as_report_list(values, prefix=\"- \", suffix=\"\\n\"):\n    \"\"\"Return list as bullet point list for a report\"\"\"\n    if not values:\n        return \"\"\n    return prefix + (suffix + prefix).join(values)\n\n\nclass GetOverlappingUVs(object):\n\n    def _createBoundingCircle(self, meshfn):\n        \"\"\" Represent a face by center and radius\n\n            :param meshfn: MFnMesh class\n            :type meshfn: :class:`maya.api.OpenMaya.MFnMesh`\n            :returns: (center, radius)\n            :rtype: tuple\n        \"\"\"\n        center = []\n        radius = []\n        for i in xrange(meshfn.numPolygons):  # noqa: F821\n            # get uvs from face\n            uarray = []\n            varray = []\n            for j in range(len(meshfn.getPolygonVertices(i))):\n                uv = meshfn.getPolygonUV(i, j)\n                uarray.append(uv[0])\n                varray.append(uv[1])\n\n            # loop through all vertices to construct edges/rays\n            cu = 0.0\n            cv = 0.0\n            for j in range(len(uarray)):\n                cu += uarray[j]\n                cv += varray[j]\n\n            cu /= len(uarray)\n            cv /= len(varray)\n            rsqr = 0.0\n            for j in range(len(varray)):\n                du = uarray[j] - cu\n                dv = varray[j] - cv\n                dsqr = du * du + dv * dv\n                rsqr = dsqr if dsqr > rsqr else rsqr\n\n            center.append(cu)\n            center.append(cv)\n            radius.append(math.sqrt(rsqr))\n\n        return center, radius\n\n    def _createRayGivenFace(self, meshfn, faceId):\n        \"\"\" Represent a face by a series of edges(rays), i.e.\n\n            :param meshfn: MFnMesh class\n            :type meshfn: :class:`maya.api.OpenMaya.MFnMesh`\n            :param faceId: face id\n            :type faceId: int\n            :returns: False if no valid uv's.\n                      \"\"(True, orig, vec)\"\" or \"\"(False, None, None)\"\"\n            :rtype: tuple\n\n            .. code-block:: python\n\n            orig = [orig1u, orig1v, orig2u, orig2v, ... ]\n            vec  = [vec1u,  vec1v,  vec2u,  vec2v,  ... ]\n        \"\"\"\n        orig = []\n        vec = []\n        # get uvs\n        uarray = []\n        varray = []\n        for i in range(len(meshfn.getPolygonVertices(faceId))):\n            uv = meshfn.getPolygonUV(faceId, i)\n            uarray.append(uv[0])\n            varray.append(uv[1])\n\n        if len(uarray) == 0 or len(varray) == 0:\n            return (False, None, None)\n\n        # loop through all vertices to construct edges/rays\n        u = uarray[-1]\n        v = varray[-1]\n        for i in xrange(len(uarray)):  # noqa: F821\n            orig.append(uarray[i])\n            orig.append(varray[i])\n            vec.append(u - uarray[i])\n            vec.append(v - varray[i])\n            u = uarray[i]\n            v = varray[i]\n\n        return (True, orig, vec)\n\n    def _checkCrossingEdges(self,\n                            face1Orig,\n                            face1Vec,\n                            face2Orig,\n                            face2Vec):\n        \"\"\" Check if there are crossing edges between two faces.\n            Return True if there are crossing edges and False otherwise.\n\n            :param face1Orig: origin of face 1\n            :type face1Orig: tuple\n            :param face1Vec: face 1 edges\n            :type face1Vec: list\n            :param face2Orig: origin of face 2\n            :type face2Orig: tuple\n            :param face2Vec: face 2 edges\n            :type face2Vec: list\n\n            A face is represented by a series of edges(rays), i.e.\n            .. code-block:: python\n\n               faceOrig[] = [orig1u, orig1v, orig2u, orig2v, ... ]\n               faceVec[]  = [vec1u,  vec1v,  vec2u,  vec2v,  ... ]\n        \"\"\"\n        face1Size = len(face1Orig)\n        face2Size = len(face2Orig)\n        for i in xrange(0, face1Size, 2):  # noqa: F821\n            o1x = face1Orig[i]\n            o1y = face1Orig[i+1]\n            v1x = face1Vec[i]\n            v1y = face1Vec[i+1]\n            n1x = v1y\n            n1y = -v1x\n            for j in xrange(0, face2Size, 2):  # noqa: F821\n                # Given ray1(O1, V1) and ray2(O2, V2)\n                # Normal of ray1 is (V1.y, V1.x)\n                o2x = face2Orig[j]\n                o2y = face2Orig[j+1]\n                v2x = face2Vec[j]\n                v2y = face2Vec[j+1]\n                n2x = v2y\n                n2y = -v2x\n\n                # Find t for ray2\n                # t = [(o1x-o2x)n1x + (o1y-o2y)n1y] /\n                # (v2x * n1x + v2y * n1y)\n                denum = v2x * n1x + v2y * n1y\n                # Edges are parallel if denum is close to 0.\n                if math.fabs(denum) < 0.000001:\n                    continue\n                t2 = ((o1x-o2x) * n1x + (o1y-o2y) * n1y) / denum\n                if (t2 < 0.00001 or t2 > 0.99999):\n                    continue\n\n                # Find t for ray1\n                # t = [(o2x-o1x)n2x\n                # + (o2y-o1y)n2y] / (v1x * n2x + v1y * n2y)\n                denum = v1x * n2x + v1y * n2y\n                # Edges are parallel if denum is close to 0.\n                if math.fabs(denum) < 0.000001:\n                    continue\n                t1 = ((o2x-o1x) * n2x + (o2y-o1y) * n2y) / denum\n\n                # Edges intersect\n                if (t1 > 0.00001 and t1 < 0.99999):\n                    return 1\n\n        return 0\n\n    def _getOverlapUVFaces(self, meshName):\n        \"\"\" Return overlapping faces\n\n            :param meshName: name of mesh\n            :type meshName: str\n            :returns: list of overlapping faces\n            :rtype: list\n        \"\"\"\n        faces = []\n        # find polygon mesh node\n        selList = om.MSelectionList()\n        selList.add(meshName)\n        mesh = selList.getDependNode(0)\n        if mesh.apiType() == om.MFn.kTransform:\n            dagPath = selList.getDagPath(0)\n            dagFn = om.MFnDagNode(dagPath)\n            child = dagFn.child(0)\n            if child.apiType() != om.MFn.kMesh:\n                raise Exception(\"Can't find polygon mesh\")\n            mesh = child\n        meshfn = om.MFnMesh(mesh)\n\n        center, radius = self._createBoundingCircle(meshfn)\n        for i in xrange(meshfn.numPolygons):  # noqa: F821\n            rayb1, face1Orig, face1Vec = self._createRayGivenFace(meshfn, i)\n            if not rayb1:\n                continue\n            cui = center[2*i]\n            cvi = center[2*i+1]\n            ri = radius[i]\n            # Exclude the degenerate face\n            # if(area(face1Orig) < 0.000001) continue;\n            # Loop through face j where j != i\n            for j in range(i+1, meshfn.numPolygons):\n                cuj = center[2*j]\n                cvj = center[2*j+1]\n                rj = radius[j]\n                du = cuj - cui\n                dv = cvj - cvi\n                dsqr = du * du + dv * dv\n                # Quick rejection if bounding circles don't overlap\n                if (dsqr >= (ri + rj) * (ri + rj)):\n                    continue\n\n                rayb2, face2Orig, face2Vec = self._createRayGivenFace(meshfn,\n                                                                      j)\n                if not rayb2:\n                    continue\n                # Exclude the degenerate face\n                # if(area(face2Orig) < 0.000001): continue;\n                if self._checkCrossingEdges(face1Orig,\n                                            face1Vec,\n                                            face2Orig,\n                                            face2Vec):\n                    face1 = '%s.f[%d]' % (meshfn.name(), i)\n                    face2 = '%s.f[%d]' % (meshfn.name(), j)\n                    if face1 not in faces:\n                        faces.append(face1)\n                    if face2 not in faces:\n                        faces.append(face2)\n        return faces\n\n\nclass ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin,\n                                    OptionalPyblishPluginMixin):\n    \"\"\" Validate the current mesh overlapping UVs.\n\n    It validates whether the current UVs are overlapping or not.\n    It is optional to warn publisher about it.\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = ['maya']\n    families = ['model']\n    label = 'Mesh Has Overlapping UVs'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = True\n\n    @classmethod\n    def _get_overlapping_uvs(cls, mesh):\n        \"\"\"Return overlapping UVs of mesh.\n\n        Args:\n            mesh (str): Mesh node name\n\n        Returns:\n            list: Overlapping uvs for the input mesh in all uv sets.\n\n        \"\"\"\n        ovl = GetOverlappingUVs()\n\n        # Store original uv set\n        original_current_uv_set = cmds.polyUVSet(mesh,\n                                                 query=True,\n                                                 currentUVSet=True)[0]\n\n        overlapping_faces = []\n        for uv_set in cmds.polyUVSet(mesh, query=True, allUVSets=True):\n            cmds.polyUVSet(mesh, currentUVSet=True, uvSet=uv_set)\n            overlapping_faces.extend(ovl._getOverlapUVFaces(mesh))\n\n        # Restore original uv set\n        cmds.polyUVSet(mesh, currentUVSet=True, uvSet=original_current_uv_set)\n\n        return overlapping_faces\n\n    @classmethod\n    def get_invalid(cls, instance, compute=False):\n\n        if compute:\n            invalid = []\n            for node in cmds.ls(instance, type=\"mesh\"):\n                faces = cls._get_overlapping_uvs(node)\n                invalid.extend(faces)\n\n            instance.data[\"overlapping_faces\"] = invalid\n\n        return instance.data.get(\"overlapping_faces\", [])\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance, compute=True)\n        if invalid:\n            raise PublishValidationError(\n                \"Meshes found with overlapping UVs:\\n\\n{0}\".format(\n                    _as_report_list(sorted(invalid))\n                ),\n                title=\"Overlapping UVs\"\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_shader_connections.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateMeshOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\ndef pairs(iterable):\n    \"\"\"Iterate over iterable per group of two\"\"\"\n    a = iter(iterable)\n    for i, y in zip(a, a):\n        yield i, y\n\n\ndef get_invalid_sets(shapes):\n    \"\"\"Return invalid sets for the given shapes.\n\n    This takes a list of shape nodes to cache the set members for overlapping\n    sets in the queries. This avoids many Maya set member queries.\n\n    Returns:\n        dict: Dictionary of shapes and their invalid sets, e.g.\n            {\"pCubeShape\": [\"set1\", \"set2\"]}\n\n    \"\"\"\n\n    cache = dict()\n    invalid = dict()\n\n    # Collect the sets from the shape\n    for shape in shapes:\n        invalid_sets = []\n        sets = cmds.listSets(object=shape, t=1, extendToShape=False) or []\n        for set_ in sets:\n\n            members = cache.get(set_, None)\n            if members is None:\n                members = set(cmds.ls(cmds.sets(set_,\n                                                query=True,\n                                                nodesOnly=True), long=True))\n                cache[set_] = members\n\n            # If the shape is not actually present as a member of the set\n            # consider it invalid\n            if shape not in members:\n                invalid_sets.append(set_)\n\n        if invalid_sets:\n            invalid[shape] = invalid_sets\n\n    return invalid\n\n\ndef disconnect(node_a, node_b):\n    \"\"\"Remove all connections between node a and b.\"\"\"\n\n    # Disconnect outputs\n    outputs = cmds.listConnections(node_a,\n                                   plugs=True,\n                                   connections=True,\n                                   source=False,\n                                   destination=True)\n    for output, destination in pairs(outputs):\n        if destination.split(\".\", 1)[0] == node_b:\n            cmds.disconnectAttr(output, destination)\n\n    # Disconnect inputs\n    inputs = cmds.listConnections(node_a,\n                                  plugs=True,\n                                  connections=True,\n                                  source=True,\n                                  destination=False)\n    for input, source in pairs(inputs):\n        if source.split(\".\", 1)[0] == node_b:\n            cmds.disconnectAttr(source, input)\n\n\nclass ValidateMeshShaderConnections(pyblish.api.InstancePlugin,\n                                    OptionalPyblishPluginMixin):\n    \"\"\"Ensure mesh shading engine connections are valid.\n\n    In some scenarios Maya keeps connections to multiple shaders even if just\n    a single one is assigned on the shape.\n\n    These are related sets returned by `maya.cmds.listSets` that don't\n    actually have the shape as member.\n\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = ['maya']\n    families = ['model']\n    label = \"Mesh Shader Connections\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               RepairAction]\n    optional = True\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance 'objectSet'\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\"Shapes found with invalid shader \"\n                               \"connections: {0}\".format(invalid))\n\n    @staticmethod\n    def get_invalid(instance):\n\n        nodes = instance[:]\n        shapes = cmds.ls(nodes, noIntermediate=True, long=True, type=\"mesh\")\n        invalid = get_invalid_sets(shapes).keys()\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n\n        shapes = cls.get_invalid(instance)\n        invalid = get_invalid_sets(shapes)\n        for shape, invalid_sets in invalid.items():\n            for set_node in invalid_sets:\n                disconnect(shape, set_node)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_single_uv_set.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateMeshOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateMeshSingleUVSet(pyblish.api.InstancePlugin,\n                              OptionalPyblishPluginMixin):\n    \"\"\"Warn on multiple UV sets existing for each polygon mesh.\n\n    On versions prior to Maya 2017 this will force no multiple uv sets because\n    the Alembic exports in Maya prior to 2017 don't support writing multiple\n    UV sets.\n\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = ['maya']\n    families = ['model', 'pointcache']\n    optional = True\n    label = \"Mesh Single UV Set\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               RepairAction]\n\n    @staticmethod\n    def get_invalid(instance):\n\n        meshes = cmds.ls(instance, type='mesh', long=True)\n\n        invalid = []\n        for mesh in meshes:\n            uvSets = cmds.polyUVSet(mesh,\n                                    query=True,\n                                    allUVSets=True) or []\n\n            # ensure unique (sometimes maya will list 'map1' twice)\n            uvSets = set(uvSets)\n\n            if len(uvSets) != 1:\n                invalid.append(mesh)\n\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance 'objectSet'\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n\n            message = \"Nodes found with multiple UV sets: {0}\".format(invalid)\n\n            # Maya 2017 and up allows multiple UV sets in Alembic exports\n            # so we allow it, yet just warn the user to ensure they know about\n            # the other UV sets.\n            allowed = int(cmds.about(version=True)) >= 2017\n\n            if allowed:\n                self.log.warning(message)\n            else:\n                raise ValueError(message)\n\n    @classmethod\n    def repair(cls, instance):\n        for mesh in cls.get_invalid(instance):\n            lib.remove_other_uv_sets(mesh)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_uv_set_map1.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateMeshOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateMeshUVSetMap1(pyblish.api.InstancePlugin,\n                            OptionalPyblishPluginMixin):\n    \"\"\"Validate model's default set exists and is named 'map1'.\n\n    In Maya meshes by default have a uv set named \"map1\" that cannot be\n    deleted. It can be renamed however, introducing some issues with some\n    renderers. As such we ensure the first (default) UV set index is named\n    \"map1\".\n\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = ['maya']\n    families = ['model']\n    optional = True\n    label = \"Mesh has map1 UV Set\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               RepairAction]\n\n    @staticmethod\n    def get_invalid(instance):\n\n        meshes = cmds.ls(instance, type='mesh', long=True)\n\n        invalid = []\n        for mesh in meshes:\n\n            # Get existing mapping of uv sets by index\n            indices = cmds.polyUVSet(mesh, query=True, allUVSetsIndices=True)\n            maps = cmds.polyUVSet(mesh, query=True, allUVSets=True)\n            mapping = dict(zip(indices, maps))\n\n            # Get the uv set at index zero.\n            name = mapping[0]\n            if name != \"map1\":\n                invalid.append(mesh)\n\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance 'objectSet'\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise ValueError(\"Meshes found without 'map1' \"\n                             \"UV set: {0}\".format(invalid))\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"Rename uv map at index zero to map1\"\"\"\n\n        for mesh in cls.get_invalid(instance):\n\n            # Get existing mapping of uv sets by index\n            indices = cmds.polyUVSet(mesh, query=True, allUVSetsIndices=True)\n            maps = cmds.polyUVSet(mesh, query=True, allUVSets=True)\n            mapping = dict(zip(indices, maps))\n\n            # Ensure there is no uv set named map1 to avoid\n            # a clash on renaming the \"default uv set\" to map1\n            existing = set(maps)\n            if \"map1\" in existing:\n\n                # Find a unique name index\n                i = 2\n                while True:\n                    name = \"map{0}\".format(i)\n                    if name not in existing:\n                        break\n                    i += 1\n\n                cls.log.warning(\"Renaming clashing uv set name on mesh\"\n                                \" %s to '%s'\", mesh, name)\n\n                cmds.polyUVSet(mesh,\n                               rename=True,\n                               uvSet=\"map1\",\n                               newUVSet=name)\n\n            # Rename the initial index to map1\n            original = mapping[0]\n            cmds.polyUVSet(mesh,\n                           rename=True,\n                           uvSet=original,\n                           newUVSet=\"map1\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py",
    "content": "import pyblish.api\nfrom maya import cmds\n\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api.lib import len_flattened\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    RepairAction,\n    ValidateMeshOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin,\n                                    OptionalPyblishPluginMixin):\n    \"\"\"Validate meshes have only vertices that are connected to edges.\n\n    Maya can have invalid geometry with vertices that have no edges or\n    faces connected to them.\n\n    In Maya 2016 EXT 2 and later there's a command to fix this:\n        `maya.cmds.polyClean(mesh, cleanVertices=True)`\n\n    In older versions of Maya it works to select the invalid vertices\n    and merge the components.\n\n    To find these invalid vertices select all vertices of the mesh\n    that are visible in the viewport (drag to select), afterwards\n    invert your selection (Ctrl + Shift + I). The remaining selection\n    contains the invalid vertices.\n\n    \"\"\"\n\n    order = ValidateMeshOrder\n    hosts = ['maya']\n    families = ['model']\n    label = 'Mesh Vertices Have Edges'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               RepairAction]\n    optional = True\n\n    @classmethod\n    def repair(cls, instance):\n\n        # This fix only works in Maya 2016 EXT2 and newer\n        if float(cmds.about(version=True)) <= 2016.0:\n            raise PublishValidationError(\n                (\"Repair not supported in Maya version below \"\n                 \"2016 EXT 2\"))\n\n        invalid = cls.get_invalid(instance)\n        for node in invalid:\n            cmds.polyClean(node, cleanVertices=True)\n\n    @classmethod\n    def get_invalid(cls, instance):\n        invalid = []\n\n        meshes = cmds.ls(instance, type=\"mesh\", long=True)\n        for mesh in meshes:\n            num_vertices = cmds.polyEvaluate(mesh, vertex=True)\n\n            if num_vertices == 0:\n                cls.log.warning(\n                    \"Skipping \\\"{}\\\", cause it does not have any \"\n                    \"vertices.\".format(mesh)\n                )\n                continue\n\n            # Vertices from all edges\n            edges = \"%s.e[*]\" % mesh\n            vertices = cmds.polyListComponentConversion(edges, toVertex=True)\n            num_vertices_from_edges = len_flattened(vertices)\n\n            if num_vertices != num_vertices_from_edges:\n                invalid.append(mesh)\n\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Meshes found in instance with vertices that \"\n                 \"have no edges: {}\").format(invalid))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_model_content.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateModelContent(pyblish.api.InstancePlugin,\n                           OptionalPyblishPluginMixin):\n    \"\"\"Adheres to the content of 'model' product type\n\n    - Must have one top group. (configurable)\n    - Must only contain: transforms, meshes and groups\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"model\"]\n    label = \"Model Content\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    validate_top_group = True\n    optional = False\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        content_instance = instance.data.get(\"setMembers\", None)\n        if not content_instance:\n            cls.log.error(\"Instance has no nodes!\")\n            return [instance.data[\"name\"]]\n\n        # All children will be included in the extracted export so we also\n        # validate *all* descendents of the set members and we skip any\n        # intermediate shapes\n        descendants = cmds.listRelatives(content_instance,\n                                         allDescendents=True,\n                                         fullPath=True) or []\n        descendants = cmds.ls(descendants, noIntermediate=True, long=True)\n        content_instance = list(set(content_instance + descendants))\n\n        # Ensure only valid node types\n        allowed = ('mesh', 'transform', 'nurbsCurve', 'nurbsSurface', 'locator')\n        nodes = cmds.ls(content_instance, long=True)\n        valid = cmds.ls(content_instance, long=True, type=allowed)\n        invalid = set(nodes) - set(valid)\n\n        if invalid:\n            cls.log.error(\"These nodes are not allowed: %s\" % invalid)\n            return list(invalid)\n\n        if not valid:\n            cls.log.error(\"No valid nodes in the instance\")\n            return True\n\n        # Ensure it has shapes\n        shapes = cmds.ls(valid, long=True, shapes=True)\n        if not shapes:\n            cls.log.error(\"No shapes in the model instance\")\n            return True\n\n        # Top group\n        top_parents = set([x.split(\"|\")[1] for x in content_instance])\n        if cls.validate_top_group and len(top_parents) != 1:\n            cls.log.error(\"Must have exactly one top group\")\n            return top_parents\n\n        def _is_visible(node):\n            \"\"\"Return whether node is visible\"\"\"\n            return lib.is_visible(node,\n                                  displayLayer=False,\n                                  intermediateObject=True,\n                                  parentHidden=True,\n                                  visibility=True)\n\n        # The roots must be visible (the assemblies)\n        for parent in top_parents:\n            if not _is_visible(parent):\n                cls.log.error(\"Invisible parent (root node) is not \"\n                              \"allowed: {0}\".format(parent))\n                invalid.add(parent)\n\n        # Ensure at least one shape is visible\n        if not any(_is_visible(shape) for shape in shapes):\n            cls.log.error(\"No visible shapes in the model instance\")\n            invalid.update(shapes)\n\n        return list(invalid)\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\n                title=\"Model content is invalid\",\n                message=\"See log for more details\"\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_model_name.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validate model nodes names.\"\"\"\nimport os\nimport platform\nimport re\n\nimport gridfs\nimport pyblish.api\nfrom maya import cmds\n\nimport openpype.hosts.maya.api.action\nfrom openpype.client.mongo import OpenPypeMongoConnection\nfrom openpype.hosts.maya.api.shader_definition_editor import (\n    DEFINITION_FILENAME)\nfrom openpype.pipeline import legacy_io\nfrom openpype.pipeline.publish import (\n    OptionalPyblishPluginMixin, PublishValidationError, ValidateContentsOrder)\n\n\nclass ValidateModelName(pyblish.api.InstancePlugin,\n                        OptionalPyblishPluginMixin):\n    \"\"\"Validate name of model\n\n    starts with (somename)_###_(materialID)_GEO\n    materialID must be present in list\n    padding number doesn't have limit\n\n    \"\"\"\n    optional = True\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"model\"]\n    label = \"Model Name\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    material_file = None\n    database_file = DEFINITION_FILENAME\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Get invalid nodes.\"\"\"\n        use_db = cls.database\n\n        def is_group(group_name):\n            \"\"\"Find out if supplied transform is group or not.\"\"\"\n            try:\n                children = cmds.listRelatives(group_name, children=True)\n                for child in children:\n                    if not cmds.ls(child, transforms=True):\n                        return False\n                return True\n            except Exception:\n                return False\n\n        invalid = []\n        content_instance = instance.data.get(\"setMembers\", None)\n        if not content_instance:\n            cls.log.error(\"Instance has no nodes!\")\n            return True\n        pass\n\n        # validate top level group name\n        assemblies = cmds.ls(content_instance, assemblies=True, long=True)\n        if len(assemblies) != 1:\n            cls.log.error(\"Must have exactly one top group\")\n            return assemblies or True\n        top_group = assemblies[0]\n        regex = cls.top_level_regex\n        r = re.compile(regex)\n        m = r.match(top_group)\n        project_name = instance.context.data[\"projectName\"]\n        current_asset_name = instance.context.data[\"asset\"]\n        if m is None:\n            cls.log.error(\"invalid name on: {}\".format(top_group))\n            cls.log.error(\"name doesn't match regex {}\".format(regex))\n            invalid.append(top_group)\n        else:\n            if \"asset\" in r.groupindex:\n                if m.group(\"asset\") != current_asset_name:\n                    cls.log.error(\"Invalid asset name in top level group.\")\n                    return top_group\n            if \"subset\" in r.groupindex:\n                if m.group(\"subset\") != instance.data.get(\"subset\"):\n                    cls.log.error(\"Invalid subset name in top level group.\")\n                    return top_group\n            if \"project\" in r.groupindex:\n                if m.group(\"project\") != project_name:\n                    cls.log.error(\"Invalid project name in top level group.\")\n                    return top_group\n\n        descendants = cmds.listRelatives(content_instance,\n                                         allDescendents=True,\n                                         fullPath=True) or []\n\n        descendants = cmds.ls(descendants, noIntermediate=True, long=True)\n        trns = cmds.ls(descendants, long=False, type='transform')\n\n        # filter out groups\n        filtered = [node for node in trns if not is_group(node)]\n\n        # load shader list file as utf-8\n        shaders = []\n        if not use_db:\n            material_file = cls.material_file[platform.system().lower()]\n            if material_file:\n                if os.path.isfile(material_file):\n                    shader_file = open(material_file, \"r\")\n                    shaders = shader_file.readlines()\n                    shader_file.close()\n            else:\n                cls.log.error(\"Missing shader name definition file.\")\n                return True\n        else:\n            client = OpenPypeMongoConnection.get_mongo_client()\n            fs = gridfs.GridFS(client[os.getenv(\"OPENPYPE_DATABASE_NAME\")])\n            shader_file = fs.find_one({\"filename\": cls.database_file})\n            if not shader_file:\n                cls.log.error(\"Missing shader name definition in database.\")\n                return True\n            shaders = shader_file.read().splitlines()\n            shader_file.close()\n\n        # strip line endings from list\n        shaders = [s.rstrip() for s in shaders if s.rstrip()]\n\n        # compile regex for testing names\n        regex = cls.regex\n        r = re.compile(regex)\n\n        for obj in filtered:\n            cls.log.debug(\"testing: {}\".format(obj))\n            m = r.match(obj)\n            if m is None:\n                cls.log.error(\"invalid name on: {}\".format(obj))\n                invalid.append(obj)\n            else:\n                # if we have shader files and shader named group is in\n                # regex, test this group against names in shader file\n                if \"shader\" in r.groupindex and shaders:\n                    try:\n                        if not m.group('shader') in shaders:\n                            cls.log.error(\n                                \"invalid materialID on: {0} ({1})\".format(\n                                    obj, m.group('shader')))\n                            invalid.append(obj)\n                    except IndexError:\n                        # shader named group doesn't match\n                        cls.log.error(\n                            \"shader group doesn't match: {}\".format(obj))\n                        invalid.append(obj)\n\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\n                \"Model naming is invalid. See the log.\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py",
    "content": "import os\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\nCOLOUR_SPACES = ['sRGB', 'linear', 'auto']\nMIPMAP_EXTENSIONS = ['tdl']\n\n\nclass ValidateMvLookContents(pyblish.api.InstancePlugin,\n                             OptionalPyblishPluginMixin):\n    order = ValidateContentsOrder\n    families = ['mvLook']\n    hosts = ['maya']\n    label = 'Validate mvLook Data'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    # Allow this validation step to be skipped when you just need to\n    # get things pushed through.\n    optional = True\n\n    # These intents get enforced checks, other ones get warnings.\n    enforced_intents = ['-', 'Final']\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        intent = instance.context.data['intent']['value']\n        publishMipMap = instance.data[\"publishMipMap\"]\n        enforced = True\n        if intent in self.enforced_intents:\n            self.log.debug(\"This validation will be enforced: '{}'\"\n                           .format(intent))\n        else:\n            enforced = False\n            self.log.debug(\"This validation will NOT be enforced: '{}'\"\n                           .format(intent))\n\n        if not instance[:]:\n            raise PublishValidationError(\"Instance is empty\")\n\n        invalid = set()\n\n        resources = instance.data.get(\"resources\", [])\n        for resource in resources:\n            files = resource[\"files\"]\n            self.log.debug(\n                \"Resource '{}', files: [{}]\".format(resource, files))\n            node = resource[\"node\"]\n            if len(files) == 0:\n                self.log.error(\"File node '{}' uses no or non-existing \"\n                               \"files\".format(node))\n                invalid.add(node)\n                continue\n            for fname in files:\n                if not self.valid_file(fname):\n                    self.log.error(\"File node '{}'/'{}' is not valid\"\n                                   .format(node, fname))\n                    invalid.add(node)\n\n                if publishMipMap and not self.is_or_has_mipmap(fname, files):\n                    msg = \"File node '{}'/'{}' does not have a mipmap\".format(\n                        node, fname)\n                    if enforced:\n                        invalid.add(node)\n                        self.log.error(msg)\n                        raise PublishValidationError(msg)\n                    else:\n                        self.log.warning(msg)\n\n        if invalid:\n            raise PublishValidationError(\n                \"'{}' has invalid look content\".format(instance.name)\n            )\n\n    def valid_file(self, fname):\n        self.log.debug(\"Checking validity of '{}'\".format(fname))\n        if not os.path.exists(fname):\n            return False\n        if os.path.getsize(fname) == 0:\n            return False\n        return True\n\n    def is_or_has_mipmap(self, fname, files):\n        ext = os.path.splitext(fname)[1][1:]\n        if ext in MIPMAP_EXTENSIONS:\n            self.log.debug(\"  - Is a mipmap '{}'\".format(fname))\n            return True\n\n        for colour_space in COLOUR_SPACES:\n            for mipmap_ext in MIPMAP_EXTENSIONS:\n                mipmap_fname = '.'.join([fname, colour_space, mipmap_ext])\n                if mipmap_fname in files:\n                    self.log.debug(\n                        \"  - Has a mipmap '{}'\".format(mipmap_fname))\n                    return True\n        return False\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_no_animation.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\ndef _as_report_list(values, prefix=\"- \", suffix=\"\\n\"):\n    \"\"\"Return list as bullet point list for a report\"\"\"\n    if not values:\n        return \"\"\n    return prefix + (suffix + prefix).join(values)\n\n\nclass ValidateNoAnimation(pyblish.api.Validator,\n                          OptionalPyblishPluginMixin):\n    \"\"\"Ensure no keyframes on nodes in the Instance.\n\n    Even though a Model would extract without animCurves correctly this avoids\n    getting different output from a model when extracted from a different\n    frame than the first frame. (Might be overly restrictive though)\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"No Animation\"\n    hosts = [\"maya\"]\n    families = [\"model\"]\n    optional = True\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Keyframes found on:\\n\\n{0}\".format(\n                    _as_report_list(sorted(invalid))\n                ),\n                title=\"Keyframes on model\"\n            )\n\n    @staticmethod\n    def get_invalid(instance):\n\n        nodes = instance[:]\n        if not nodes:\n            return []\n\n        curves = cmds.keyframe(nodes, query=True, name=True)\n        if curves:\n            return list(set(cmds.listConnections(curves)))\n\n        return []\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_no_default_camera.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\ndef _as_report_list(values, prefix=\"- \", suffix=\"\\n\"):\n    \"\"\"Return list as bullet point list for a report\"\"\"\n    if not values:\n        return \"\"\n    return prefix + (suffix + prefix).join(values)\n\n\nclass ValidateNoDefaultCameras(pyblish.api.InstancePlugin,\n                               OptionalPyblishPluginMixin):\n    \"\"\"Ensure no default (startup) cameras are in the instance.\n\n    This might be unnecessary. In the past there were some issues with\n    referencing/importing files that contained the start up cameras overriding\n    settings when being loaded and sometimes being skipped.\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = ['camera']\n    label = \"No Default Cameras\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    @staticmethod\n    def get_invalid(instance):\n        cameras = cmds.ls(instance, type='camera', long=True)\n        return [cam for cam in cameras if\n                cmds.camera(cam, query=True, startupCamera=True)]\n\n    def process(self, instance):\n        \"\"\"Process all the cameras in the instance\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Default cameras found:\\n\\n{0}\".format(\n                    _as_report_list(sorted(invalid))\n                ),\n                title=\"Default cameras\"\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_no_namespace.py",
    "content": "import maya.cmds as cmds\n\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\nimport openpype.hosts.maya.api.action\n\n\ndef _as_report_list(values, prefix=\"- \", suffix=\"\\n\"):\n    \"\"\"Return list as bullet point list for a report\"\"\"\n    if not values:\n        return \"\"\n    return prefix + (suffix + prefix).join(values)\n\n\ndef get_namespace(node_name):\n    # ensure only node's name (not parent path)\n    node_name = node_name.rsplit(\"|\", 1)[-1]\n    # ensure only namespace\n    return node_name.rpartition(\":\")[0]\n\n\nclass ValidateNoNamespace(pyblish.api.InstancePlugin,\n                          OptionalPyblishPluginMixin):\n    \"\"\"Ensure the nodes don't have a namespace\"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = ['model']\n    label = 'No Namespaces'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               RepairAction]\n    optional = False\n\n    @staticmethod\n    def get_invalid(instance):\n        nodes = cmds.ls(instance, long=True)\n        return [node for node in nodes if get_namespace(node)]\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\n                \"Namespaces found:\\n\\n{0}\".format(\n                    _as_report_list(sorted(invalid))\n                ),\n                title=\"Namespaces in model\"\n            )\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"Remove all namespaces from the nodes in the instance\"\"\"\n\n        invalid = cls.get_invalid(instance)\n\n        # Iterate over the nodes by long to short names to iterate the lowest\n        # in hierarchy nodes first. This way we avoid having renamed parents\n        # before renaming children nodes\n        for node in sorted(invalid, key=len, reverse=True):\n\n            node_name = node.rsplit(\"|\", 1)[-1]\n            node_name_without_namespace = node_name.rsplit(\":\")[-1]\n            cmds.rename(node, node_name_without_namespace)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_no_null_transforms.py",
    "content": "import maya.cmds as cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\ndef _as_report_list(values, prefix=\"- \", suffix=\"\\n\"):\n    \"\"\"Return list as bullet point list for a report\"\"\"\n    if not values:\n        return \"\"\n    return prefix + (suffix + prefix).join(values)\n\n\ndef has_shape_children(node):\n    # Check if any descendants\n    allDescendents = cmds.listRelatives(node,\n                                        allDescendents=True,\n                                        fullPath=True)\n    if not allDescendents:\n        return False\n\n    # Check if there are any shapes at all\n    shapes = cmds.ls(allDescendents, shapes=True)\n    if not shapes:\n        return False\n\n    # Check if all descendent shapes are intermediateObjects;\n    # if so we consider this node a null node and return False.\n    if all(cmds.getAttr('{0}.intermediateObject'.format(x)) for x in shapes):\n        return False\n\n    return True\n\n\nclass ValidateNoNullTransforms(pyblish.api.InstancePlugin,\n                               OptionalPyblishPluginMixin):\n    \"\"\"Ensure no null transforms are in the scene.\n\n    Warning:\n        Transforms with only intermediate shapes are also considered null\n        transforms. These transform nodes could potentially be used in your\n        construction history, so take care when automatically fixing this or\n        when deleting the empty transforms manually.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = ['model']\n    label = 'No Empty/Null Transforms'\n    actions = [RepairAction,\n               openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    @staticmethod\n    def get_invalid(instance):\n        \"\"\"Return invalid transforms in instance\"\"\"\n\n        transforms = cmds.ls(instance, type='transform', long=True)\n\n        invalid = []\n        for transform in transforms:\n            if not has_shape_children(transform):\n                invalid.append(transform)\n\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Process all the transform nodes in the instance \"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Empty transforms found without shapes:\\n\\n{0}\".format(\n                    _as_report_list(sorted(invalid))\n                ),\n                title=\"Empty transforms\"\n            )\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"Delete all null transforms.\n\n        Note: If the node is used elsewhere (eg. connection to attributes or\n        in history) deletion might mess up things.\n\n        \"\"\"\n        invalid = cls.get_invalid(instance)\n        if invalid:\n            cmds.delete(invalid)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_no_unknown_nodes.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\ndef _as_report_list(values, prefix=\"- \", suffix=\"\\n\"):\n    \"\"\"Return list as bullet point list for a report\"\"\"\n    if not values:\n        return \"\"\n    return prefix + (suffix + prefix).join(values)\n\n\nclass ValidateNoUnknownNodes(pyblish.api.InstancePlugin,\n                             OptionalPyblishPluginMixin):\n    \"\"\"Checks to see if there are any unknown nodes in the instance.\n\n    This often happens if nodes from plug-ins are used but are not available\n    on this machine.\n\n    Note: Some studios use unknown nodes to store data on (as attributes)\n        because it's a lightweight node.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = ['model', 'rig']\n    optional = True\n    label = \"Unknown Nodes\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    @staticmethod\n    def get_invalid(instance):\n        return cmds.ls(instance, type='unknown')\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Unknown nodes found:\\n\\n{0}\".format(\n                    _as_report_list(sorted(invalid))\n                ),\n                title=\"Unknown nodes\"\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_no_vraymesh.py",
    "content": "import pyblish.api\nfrom maya import cmds\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\ndef _as_report_list(values, prefix=\"- \", suffix=\"\\n\"):\n    \"\"\"Return list as bullet point list for a report\"\"\"\n    if not values:\n        return \"\"\n    return prefix + (suffix + prefix).join(values)\n\n\nclass ValidateNoVRayMesh(pyblish.api.InstancePlugin,\n                         OptionalPyblishPluginMixin):\n    \"\"\"Validate there are no VRayMesh objects in the instance\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = 'No V-Ray Proxies (VRayMesh)'\n    families = [\"pointcache\"]\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        if not cmds.pluginInfo(\"vrayformaya\", query=True, loaded=True):\n            return\n\n        shapes = cmds.ls(instance,\n                         shapes=True,\n                         type=\"mesh\")\n\n        inputs = cmds.listConnections(shapes,\n                                      destination=False,\n                                      source=True) or []\n        vray_meshes = cmds.ls(inputs, type='VRayMesh')\n        if vray_meshes:\n            raise PublishValidationError(\n                \"Meshes that are V-Ray Proxies should not be in an Alembic \"\n                \"pointcache.\\n\"\n                \"Found V-Ray proxies:\\n\\n{}\".format(\n                    _as_report_list(sorted(vray_meshes))\n                ),\n                title=\"V-Ray Proxies in pointcache\"\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_node_ids.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidatePipelineOrder,\n    PublishXmlValidationError\n)\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\n\n\nclass ValidateNodeIDs(pyblish.api.InstancePlugin):\n    \"\"\"Validate nodes have a Colorbleed Id.\n\n    When IDs are missing from nodes *save your scene* and they should be\n    automatically generated because IDs are created on non-referenced nodes\n    in Maya upon scene save.\n\n    \"\"\"\n\n    order = ValidatePipelineOrder\n    label = 'Instance Nodes Have ID'\n    hosts = ['maya']\n    families = [\"model\",\n                \"look\",\n                \"rig\",\n                \"pointcache\",\n                \"animation\",\n                \"yetiRig\",\n                \"assembly\"]\n\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               openpype.hosts.maya.api.action.GenerateUUIDsOnInvalidAction]\n\n    def process(self, instance):\n        \"\"\"Process all meshes\"\"\"\n\n        # Ensure all nodes have a cbId\n        invalid = self.get_invalid(instance)\n        if invalid:\n            names = \"\\n\".join(\n                \"- {}\".format(node) for node in invalid\n            )\n            raise PublishXmlValidationError(\n                plugin=self,\n                message=\"Nodes found without IDs: {}\".format(invalid),\n                formatting_data={\"nodes\": names}\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Return the member nodes that are invalid\"\"\"\n\n        # We do want to check the referenced nodes as it might be\n        # part of the end product.\n        id_nodes = lib.get_id_required_nodes(referenced_nodes=True,\n                                             nodes=instance[:])\n        invalid = [n for n in id_nodes if not lib.get_id(n)]\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_node_ids_deformed_shapes.py",
    "content": "import pyblish.api\nfrom maya import cmds\n\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    PublishValidationError, RepairAction, ValidateContentsOrder)\n\n\nclass ValidateNodeIdsDeformedShape(pyblish.api.InstancePlugin):\n    \"\"\"Validate if deformed shapes have related IDs to the original shapes.\n\n    When a deformer is applied in the scene on a referenced mesh that already\n    had deformers then Maya will create a new shape node for the mesh that\n    does not have the original id. This validator checks whether the ids are\n    valid on all the shape nodes in the instance.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    families = ['look']\n    hosts = ['maya']\n    label = 'Deformed shape ids'\n    actions = [\n        openpype.hosts.maya.api.action.SelectInvalidAction,\n        RepairAction\n    ]\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance\"\"\"\n\n        # Ensure all nodes have a cbId and a related ID to the original shapes\n        # if a deformer has been created on the shape\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Shapes found that are considered 'Deformed'\"\n                 \"without object ids: {0}\").format(invalid))\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Get all nodes which do not match the criteria\"\"\"\n\n        shapes = cmds.ls(instance[:],\n                         dag=True,\n                         leaf=True,\n                         shapes=True,\n                         long=True,\n                         noIntermediate=True)\n\n        invalid = []\n        for shape in shapes:\n            history_id = lib.get_id_from_sibling(shape)\n            if history_id:\n                current_id = lib.get_id(shape)\n                if current_id != history_id:\n                    invalid.append(shape)\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n\n        for node in cls.get_invalid(instance):\n            # Get the original id from history\n            history_id = lib.get_id_from_sibling(node)\n            if not history_id:\n                cls.log.error(\"Could not find ID in history for '%s'\", node)\n                continue\n\n            lib.set_id(node, history_id, overwrite=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_node_ids_in_database.py",
    "content": "import pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.client import get_assets\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline import legacy_io\nfrom openpype.pipeline.publish import (\n    PublishValidationError, ValidatePipelineOrder)\n\n\nclass ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin):\n    \"\"\"Validate if the CB Id is related to an asset in the database\n\n    All nodes with the `cbId` attribute will be validated to ensure that\n    the loaded asset in the scene is related to the current project.\n\n    Tip: If there is an asset which is being reused from a different project\n    please ensure the asset is republished in the new project\n\n    \"\"\"\n\n    order = ValidatePipelineOrder\n    label = 'Node Ids in Database'\n    hosts = ['maya']\n    families = [\"*\"]\n\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               openpype.hosts.maya.api.action.GenerateUUIDsOnInvalidAction]\n\n    def process(self, instance):\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Found asset IDs which are not related to \"\n                 \"current project in instance: `{}`\").format(instance.name))\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        invalid = []\n\n        # Get all id required nodes\n        id_required_nodes = lib.get_id_required_nodes(referenced_nodes=True,\n                                                      nodes=instance[:])\n\n        # check ids against database ids\n        project_name = legacy_io.active_project()\n        asset_docs = get_assets(project_name, fields=[\"_id\"])\n        db_asset_ids = {\n            str(asset_doc[\"_id\"])\n            for asset_doc in asset_docs\n        }\n\n        # Get all asset IDs\n        for node in id_required_nodes:\n            cb_id = lib.get_id(node)\n\n            # Ignore nodes without id, those are validated elsewhere\n            if not cb_id:\n                continue\n\n            asset_id = cb_id.split(\":\", 1)[0]\n            if asset_id not in db_asset_ids:\n                cls.log.error(\"`%s` has unassociated asset ID\" % node)\n                invalid.append(node)\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_node_ids_related.py",
    "content": "import pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    OptionalPyblishPluginMixin, PublishValidationError, ValidatePipelineOrder)\n\n\nclass ValidateNodeIDsRelated(pyblish.api.InstancePlugin,\n                             OptionalPyblishPluginMixin):\n    \"\"\"Validate nodes have a related Colorbleed Id to the instance.data[asset]\n\n    \"\"\"\n\n    order = ValidatePipelineOrder\n    label = 'Node Ids Related (ID)'\n    hosts = ['maya']\n    families = [\"model\",\n                \"look\",\n                \"rig\"]\n    optional = True\n\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               openpype.hosts.maya.api.action.GenerateUUIDsOnInvalidAction]\n\n    def process(self, instance):\n        \"\"\"Process all nodes in instance (including hierarchy)\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        # Ensure all nodes have a cbId\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Nodes IDs found that are not related to asset \"\n                 \"'{}' : {}\").format(instance.data['asset'], invalid))\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Return the member nodes that are invalid\"\"\"\n        invalid = list()\n\n        asset_id = str(instance.data['assetEntity'][\"_id\"])\n\n        # We do want to check the referenced nodes as we it might be\n        # part of the end product\n        for node in instance:\n\n            _id = lib.get_id(node)\n            if not _id:\n                continue\n\n            node_asset_id = _id.split(\":\", 1)[0]\n            if node_asset_id != asset_id:\n                invalid.append(node)\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py",
    "content": "from collections import defaultdict\n\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    ValidatePipelineOrder,\n    PublishValidationError\n)\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\n\n\nclass ValidateNodeIdsUnique(pyblish.api.InstancePlugin):\n    \"\"\"Validate the nodes in the instance have a unique Colorbleed Id\n\n    Here we ensure that what has been added to the instance is unique\n    \"\"\"\n\n    order = ValidatePipelineOrder\n    label = 'Non Duplicate Instance Members (ID)'\n    hosts = ['maya']\n    families = [\"model\",\n                \"look\",\n                \"rig\",\n                \"yetiRig\"]\n\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               openpype.hosts.maya.api.action.GenerateUUIDsOnInvalidAction]\n\n    def process(self, instance):\n        \"\"\"Process all meshes\"\"\"\n\n        # Ensure all nodes have a cbId\n        invalid = self.get_invalid(instance)\n        if invalid:\n            label = \"Nodes found with non-unique asset IDs\"\n            raise PublishValidationError(\n                message=\"{}: {}\".format(label, invalid),\n                title=\"Non-unique asset ids on nodes\",\n                description=\"{}\\n- {}\".format(label,\n                                              \"\\n- \".join(sorted(invalid)))\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Return the member nodes that are invalid\"\"\"\n\n        # Check only non intermediate shapes\n        # todo: must the instance itself ensure to have no intermediates?\n        # todo: how come there are intermediates?\n        from maya import cmds\n        instance_members = cmds.ls(instance, noIntermediate=True, long=True)\n\n        # Collect each id with their members\n        ids = defaultdict(list)\n        for member in instance_members:\n            object_id = lib.get_id(member)\n            if not object_id:\n                continue\n            ids[object_id].append(member)\n\n        # Take only the ids with more than one member\n        invalid = list()\n        _iteritems = getattr(ids, \"iteritems\", ids.items)\n        for _ids, members in _iteritems():\n            if len(members) > 1:\n                cls.log.error(\"ID found on multiple nodes: '%s'\" % members)\n                invalid.extend(members)\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_node_no_ghosting.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateNodeNoGhosting(pyblish.api.InstancePlugin,\n                             OptionalPyblishPluginMixin):\n    \"\"\"Ensure nodes do not have ghosting enabled.\n\n    If one would publish towards a non-Maya format it's likely that stats\n    like ghosting won't be exported, eg. exporting to Alembic.\n\n    Instead of creating many micro-managing checks (like this one) to ensure\n    attributes have not been changed from their default it could be more\n    efficient to export to a format that will never hold such data anyway.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = ['model', 'rig']\n    label = \"No Ghosting\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    _attributes = {'ghosting': 0}\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        # Transforms and shapes seem to have ghosting\n        nodes = cmds.ls(instance, long=True, type=['transform', 'shape'])\n        invalid = []\n        for node in nodes:\n            _iteritems = getattr(\n                cls._attributes, \"iteritems\", cls._attributes.items\n            )\n            for attr, required_value in _iteritems():\n                if cmds.attributeQuery(attr, node=node, exists=True):\n\n                    value = cmds.getAttr('{0}.{1}'.format(node, attr))\n                    if value != required_value:\n                        invalid.append(node)\n\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise ValueError(\"Nodes with ghosting enabled found: \"\n                             \"{0}\".format(invalid))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py",
    "content": "import os\n\nfrom maya import cmds\n\nimport pyblish.api\n\nfrom openpype.hosts.maya.api.lib import pairwise\nfrom openpype.hosts.maya.api.action import SelectInvalidAction\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidatePluginPathAttributes(pyblish.api.InstancePlugin,\n                                   OptionalPyblishPluginMixin):\n    \"\"\"\n    Validate plug-in path attributes point to existing file paths.\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = [\"workfile\"]\n    label = \"Plug-in Path Attributes\"\n    actions = [SelectInvalidAction]\n    optional = False\n\n    # Attributes are defined in project settings\n    attribute = []\n\n    @classmethod\n    def get_invalid(cls, instance):\n        invalid = list()\n\n        file_attrs = cls.attribute\n        if not file_attrs:\n            return invalid\n\n        # Consider only valid node types to avoid \"Unknown object type\" warning\n        all_node_types = set(cmds.allNodeTypes())\n        node_types = [\n            key for key in file_attrs.keys()\n            if key in all_node_types\n        ]\n\n        for node, node_type in pairwise(cmds.ls(type=node_types,\n                                                showType=True)):\n            # get the filepath\n            file_attr = \"{}.{}\".format(node, file_attrs[node_type])\n            filepath = cmds.getAttr(file_attr)\n\n            if filepath and not os.path.exists(filepath):\n                cls.log.error(\"{} '{}' uses non-existing filepath: {}\"\n                              .format(node_type, node, filepath))\n                invalid.append(node)\n\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Process all directories Set as Filenames in Non-Maya Nodes\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                title=\"Plug-in Path Attributes\",\n                message=\"Non-existent filepath found on nodes: {}\".format(\n                    \", \".join(invalid)\n                ),\n                description=(\n                    \"## Plug-in nodes use invalid filepaths\\n\"\n                    \"The workfile contains nodes from plug-ins that use \"\n                    \"filepaths which do not exist.\\n\\n\"\n                    \"Please make sure their filepaths are correct and the \"\n                    \"files exist on disk.\"\n                )\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_render_image_rule.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom maya import cmds\n\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    RepairAction,\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateRenderImageRule(pyblish.api.InstancePlugin,\n                              OptionalPyblishPluginMixin):\n    \"\"\"Validates Maya Workpace \"images\" file rule matches project settings.\n\n    This validates against the configured default render image folder:\n        Studio Settings > Project > Maya >\n        Render Settings > Default render image folder.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Images File Rule (Workspace)\"\n    hosts = [\"maya\"]\n    families = [\"renderlayer\"]\n    actions = [RepairAction]\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        required_images_rule = os.path.normpath(\n            self.get_default_render_image_folder(instance)\n        )\n        current_images_rule = os.path.normpath(\n            cmds.workspace(fileRuleEntry=\"images\")\n        )\n\n        if current_images_rule != required_images_rule:\n            raise PublishValidationError(\n                (\n                    \"Invalid workspace `images` file rule value: '{}'. \"\n                    \"Must be set to: '{}'\"\n                ).format(current_images_rule, required_images_rule))\n\n    @classmethod\n    def repair(cls, instance):\n\n        required_images_rule = cls.get_default_render_image_folder(instance)\n        current_images_rule = cmds.workspace(fileRuleEntry=\"images\")\n\n        if current_images_rule != required_images_rule:\n            cmds.workspace(fileRule=(\"images\", required_images_rule))\n            cmds.workspace(saveWorkspace=True)\n\n    @classmethod\n    def get_default_render_image_folder(cls, instance):\n        staging_dir = instance.data.get(\"stagingDir\")\n        if staging_dir:\n            cls.log.debug(\n                \"Staging dir found: \\\"{}\\\". Ignoring setting from \"\n                \"`project_settings/maya/RenderSettings/\"\n                \"default_render_image_folder`.\".format(staging_dir)\n            )\n            return staging_dir\n\n        return instance.context.data.get('project_settings')\\\n            .get('maya') \\\n            .get('RenderSettings') \\\n            .get('default_render_image_folder')\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_render_no_default_cameras.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin,\n                                     OptionalPyblishPluginMixin):\n    \"\"\"Ensure no default (startup) cameras are to be rendered.\"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = ['renderlayer']\n    label = \"No Default Cameras Renderable\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    @staticmethod\n    def get_invalid(instance):\n\n        renderable = set(instance.data[\"cameras\"])\n\n        # Collect default cameras\n        cameras = cmds.ls(type='camera', long=True)\n        defaults = set(cam for cam in cameras if\n                       cmds.camera(cam, query=True, startupCamera=True))\n\n        return [cam for cam in renderable if cam in defaults]\n\n    def process(self, instance):\n        \"\"\"Process all the cameras in the instance\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                title=\"Rendering default cameras\",\n                message=\"Renderable default cameras \"\n                        \"found: {0}\".format(invalid))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_render_single_camera.py",
    "content": "import re\n\nimport pyblish.api\nfrom maya import cmds\n\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api.lib_rendersettings import RenderSettings\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateRenderSingleCamera(pyblish.api.InstancePlugin,\n                                 OptionalPyblishPluginMixin):\n    \"\"\"Validate renderable camera count for layer and <Camera> token.\n\n    Pipeline is supporting multiple renderable cameras per layer, but image\n    prefix must contain <Camera> token.\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Render Single Camera\"\n    hosts = ['maya']\n    families = [\"renderlayer\",\n                \"vrayscene\"]\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    R_CAMERA_TOKEN = re.compile(r'%c|<camera>', re.IGNORECASE)\n\n    def process(self, instance):\n        \"\"\"Process all the cameras in the instance\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\"Invalid cameras for render.\")\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        cameras = instance.data.get(\"cameras\", [])\n        renderer = cmds.getAttr('defaultRenderGlobals.currentRenderer').lower()\n        # handle various renderman names\n        if renderer.startswith('renderman'):\n            renderer = 'renderman'\n\n        file_prefix = cmds.getAttr(\n            RenderSettings.get_image_prefix_attr(renderer)\n        )\n\n\n        if len(cameras) > 1:\n            if re.search(cls.R_CAMERA_TOKEN, file_prefix):\n                # if there is <Camera> token in prefix and we have more then\n                # 1 camera, all is ok.\n                return\n            cls.log.error(\"Multiple renderable cameras found for %s: %s \" %\n                          (instance.data[\"setMembers\"], cameras))\n            return [instance.data[\"setMembers\"]] + cameras\n\n        elif len(cameras) < 1:\n            cls.log.error(\"No renderable cameras found for %s \" %\n                          instance.data[\"setMembers\"])\n            return [instance.data[\"setMembers\"]]\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py",
    "content": "import pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.client import get_subset_by_name\nfrom openpype.pipeline import legacy_io\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateRenderLayerAOVs(pyblish.api.InstancePlugin,\n                              OptionalPyblishPluginMixin):\n    \"\"\"Validate created AOVs / RenderElement is registered in the database\n\n    Each render element is registered as a product which is formatted based on\n    the render layer and the render element, example:\n\n        <render layer>.<render element>\n\n    This translates to something like this:\n\n        CHAR.diffuse\n\n    This check is needed to ensure the render output is still complete\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder + 0.1\n    label = \"Render Passes / AOVs Are Registered\"\n    hosts = [\"maya\"]\n    families = [\"renderlayer\"]\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Found unregistered subsets: {}\".format(invalid))\n\n    def get_invalid(self, instance):\n        invalid = []\n\n        project_name = legacy_io.active_project()\n        asset_doc = instance.data[\"assetEntity\"]\n        render_passes = instance.data.get(\"renderPasses\", [])\n        for render_pass in render_passes:\n            is_valid = self.validate_subset_registered(\n                project_name, asset_doc, render_pass\n            )\n            if not is_valid:\n                invalid.append(render_pass)\n\n        return invalid\n\n    def validate_subset_registered(self, project_name, asset_doc, subset_name):\n        \"\"\"Check if subset is registered in the database under the asset\"\"\"\n\n        return get_subset_by_name(\n            project_name, subset_name, asset_doc[\"_id\"], fields=[\"_id\"]\n        )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_rendersettings.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Maya validator for render settings.\"\"\"\nimport re\nfrom collections import OrderedDict\n\nfrom maya import cmds, mel\n\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n)\nfrom openpype.hosts.maya.api import lib\nfrom openpype.hosts.maya.api.lib_rendersettings import RenderSettings\n\n\ndef convert_to_int_or_float(string_value):\n    # Order of types are important here since float can convert string\n    # representation of integer.\n    types = [int, float]\n    for t in types:\n        try:\n            result = t(string_value)\n        except ValueError:\n            continue\n        else:\n            return result\n\n    # Neither integer or float.\n    return string_value\n\n\ndef get_redshift_image_format_labels():\n    \"\"\"Return nice labels for Redshift image formats.\"\"\"\n    var = \"$g_redshiftImageFormatLabels\"\n    return mel.eval(\"{0}={0}\".format(var))\n\n\nclass ValidateRenderSettings(pyblish.api.InstancePlugin):\n    \"\"\"Validates the global render settings\n\n    * File Name Prefix must start with: `<Scene>`\n        all other token are customizable but sane values for Arnold are:\n\n        `<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>`\n\n        <Camera> token is supported also, useful for multiple renderable\n        cameras per render layer.\n\n        For Redshift omit <RenderPass> token. Redshift will append it\n        automatically if AOVs are enabled and if you user Multipart EXR\n        it doesn't make much sense.\n\n    * Frame Padding must be:\n        * default: 4\n\n    * Animation must be toggle on, in Render Settings - Common tab:\n        * vray: Animation on standard of specific\n        * arnold: Frame / Animation ext: Any choice without \"(Single Frame)\"\n        * redshift: Animation toggled on\n\n    NOTE:\n        The repair function of this plugin does not repair the animation\n        setting of the render settings due to multiple possibilities.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Render Settings\"\n    hosts = [\"maya\"]\n    families = [\"renderlayer\"]\n    actions = [RepairAction]\n\n    ImagePrefixes = {\n        'mentalray': 'defaultRenderGlobals.imageFilePrefix',\n        'vray': 'vraySettings.fileNamePrefix',\n        'arnold': 'defaultRenderGlobals.imageFilePrefix',\n        'renderman': 'rmanGlobals.imageFileFormat',\n        'redshift': 'defaultRenderGlobals.imageFilePrefix',\n        'mayahardware2': 'defaultRenderGlobals.imageFilePrefix',\n    }\n\n    ImagePrefixTokens = {\n        'mentalray': '<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>',  # noqa: E501\n        'arnold': '<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>',  # noqa: E501\n        'redshift': '<Scene>/<RenderLayer>/<RenderLayer>',\n        'vray': '<Scene>/<Layer>/<Layer>',\n        'renderman': '<layer>{aov_separator}<aov>.<f4>.<ext>',\n        'mayahardware2': '<Scene>/<RenderLayer>/<RenderLayer>',\n    }\n\n    _aov_chars = {\n        \"dot\": \".\",\n        \"dash\": \"-\",\n        \"underscore\": \"_\"\n    }\n\n    redshift_AOV_prefix = \"<BeautyPath>/<BeautyFile>{aov_separator}<RenderPass>\"  # noqa: E501\n\n    renderman_dir_prefix = \"<scene>/<layer>\"\n\n    R_AOV_TOKEN = re.compile(\n        r'%a|<aov>|<renderpass>', re.IGNORECASE)\n    R_LAYER_TOKEN = re.compile(\n        r'%l|<layer>|<renderlayer>', re.IGNORECASE)\n    R_CAMERA_TOKEN = re.compile(r'%c|Camera>')\n    R_SCENE_TOKEN = re.compile(r'%s|<scene>', re.IGNORECASE)\n\n    DEFAULT_PADDING = 4\n    VRAY_PREFIX = \"<Scene>/<Layer>/<Layer>\"\n    DEFAULT_PREFIX = \"<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>\"\n\n    def process(self, instance):\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                title=\"Invalid Render Settings\",\n                message=(\"Invalid render settings found \"\n                         \"for '{}'!\".format(instance.name))\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        invalid = False\n\n        renderer = instance.data['renderer']\n        layer = instance.data['renderlayer']\n        cameras = instance.data.get(\"cameras\", [])\n\n        # Prefix attribute can return None when a value was never set\n        prefix = lib.get_attr_in_layer(cls.ImagePrefixes[renderer],\n                                       layer=layer) or \"\"\n        padding = lib.get_attr_in_layer(\n            attr=RenderSettings.get_padding_attr(renderer),\n            layer=layer\n        )\n\n        anim_override = lib.get_attr_in_layer(\"defaultRenderGlobals.animation\",\n                                              layer=layer)\n\n        prefix = prefix.replace(\n            \"{aov_separator}\", instance.data.get(\"aovSeparator\", \"_\"))\n\n        default_prefix = cls.ImagePrefixTokens[renderer]\n\n        if not anim_override:\n            invalid = True\n            cls.log.error(\"Animation needs to be enabled. Use the same \"\n                          \"frame for start and end to render single frame\")\n\n        if not re.search(cls.R_LAYER_TOKEN, prefix):\n            invalid = True\n            cls.log.error(\"Wrong image prefix [ {} ] - \"\n                          \"doesn't have: '<renderlayer>' or \"\n                          \"'<layer>' token\".format(prefix))\n\n        if len(cameras) > 1 and not re.search(cls.R_CAMERA_TOKEN, prefix):\n            invalid = True\n            cls.log.error(\"Wrong image prefix [ {} ] - \"\n                          \"doesn't have: '<Camera>' token\".format(prefix))\n            cls.log.error(\n                \"Note that to needs to have capital 'C' at the beginning\")\n\n        # renderer specific checks\n        if renderer == \"vray\":\n            vray_settings = cmds.ls(type=\"VRaySettingsNode\")\n            if not vray_settings:\n                node = cmds.createNode(\"VRaySettingsNode\")\n            else:\n                node = vray_settings[0]\n\n            scene_sep = cmds.getAttr(\n                \"{}.fileNameRenderElementSeparator\".format(node))\n            if scene_sep != instance.data.get(\"aovSeparator\", \"_\"):\n                cls.log.error(\"AOV separator is not set correctly.\")\n                invalid = True\n\n        if renderer == \"redshift\":\n            redshift_AOV_prefix = cls.redshift_AOV_prefix.replace(\n                \"{aov_separator}\", instance.data.get(\"aovSeparator\", \"_\")\n            )\n            if re.search(cls.R_AOV_TOKEN, prefix):\n                invalid = True\n                cls.log.error((\"Do not use AOV token [ {} ] - \"\n                               \"Redshift is using image prefixes per AOV so \"\n                               \"it doesn't make much sense using it in global\"\n                               \"image prefix\").format(prefix))\n            # get redshift AOVs\n            rs_aovs = cmds.ls(type=\"RedshiftAOV\", referencedNodes=False)\n            for aov in rs_aovs:\n                aov_prefix = cmds.getAttr(\"{}.filePrefix\".format(aov))\n                # check their image prefix\n                if aov_prefix != redshift_AOV_prefix:\n                    cls.log.error((\"AOV ({}) image prefix is not set \"\n                                   \"correctly {} != {}\").format(\n                        cmds.getAttr(\"{}.name\".format(aov)),\n                        aov_prefix,\n                        redshift_AOV_prefix\n                    ))\n                    invalid = True\n\n                # check aov file format\n                aov_ext = cmds.getAttr(\"{}.fileFormat\".format(aov))\n                default_ext = cmds.getAttr(\"redshiftOptions.imageFormat\")\n                aov_type = cmds.getAttr(\"{}.aovType\".format(aov))\n                if aov_type == \"Cryptomatte\":\n                    # redshift Cryptomatte AOV always uses \"Cryptomatte (EXR)\"\n                    # so we ignore validating file format for it.\n                    pass\n\n                elif default_ext != aov_ext:\n                    labels = get_redshift_image_format_labels()\n                    cls.log.error(\n                        \"AOV file format {} does not match global file format \"\n                        \"{}\".format(labels[aov_ext], labels[default_ext])\n                    )\n                    invalid = True\n\n        if renderer == \"renderman\":\n            file_prefix = cmds.getAttr(\"rmanGlobals.imageFileFormat\")\n            dir_prefix = cmds.getAttr(\"rmanGlobals.imageOutputDir\")\n\n            if file_prefix.lower() != prefix.lower():\n                invalid = True\n                cls.log.error(\"Wrong image prefix [ {} ]\".format(file_prefix))\n\n            if dir_prefix.lower() != cls.renderman_dir_prefix.lower():\n                invalid = True\n                cls.log.error(\"Wrong directory prefix [ {} ]\".format(\n                    dir_prefix))\n\n        if renderer == \"arnold\":\n            multipart = cmds.getAttr(\"defaultArnoldDriver.mergeAOVs\")\n            if multipart:\n                if re.search(cls.R_AOV_TOKEN, prefix):\n                    invalid = True\n                    cls.log.error(\"Wrong image prefix [ {} ] - \"\n                                  \"You can't use '<renderpass>' token \"\n                                  \"with merge AOVs turned on\".format(prefix))\n                default_prefix = re.sub(\n                    cls.R_AOV_TOKEN, \"\", default_prefix)\n                # remove aov token from prefix to pass validation\n                default_prefix = default_prefix.split(\"{aov_separator}\")[0]\n            elif not re.search(cls.R_AOV_TOKEN, prefix):\n                invalid = True\n                cls.log.error(\"Wrong image prefix [ {} ] - \"\n                              \"doesn't have: '<renderpass>' or \"\n                              \"token\".format(prefix))\n\n        default_prefix = default_prefix.replace(\n            \"{aov_separator}\", instance.data.get(\"aovSeparator\", \"_\"))\n        if prefix.lower() != default_prefix.lower():\n            cls.log.warning(\"warning: prefix differs from \"\n                            \"recommended {}\".format(\n                                default_prefix))\n\n        if padding != cls.DEFAULT_PADDING:\n            invalid = True\n            cls.log.error(\"Expecting padding of {} ( {} )\".format(\n                cls.DEFAULT_PADDING, \"0\" * cls.DEFAULT_PADDING))\n\n        # load validation definitions from settings\n        settings_lights_flag = instance.context.data[\"project_settings\"].get(\n            \"maya\", {}).get(\n            \"RenderSettings\", {}).get(\n            \"enable_all_lights\", False)\n\n        instance_lights_flag = instance.data.get(\"renderSetupIncludeLights\")\n        if settings_lights_flag != instance_lights_flag:\n            cls.log.warning(\n                \"Instance flag for \\\"Render Setup Include Lights\\\" is set to \"\n                \"{} and Settings flag is set to {}\".format(\n                    instance_lights_flag, settings_lights_flag\n                )\n            )\n\n        # go through definitions and test if such node.attribute exists.\n        # if so, compare its value from the one required.\n        for data in cls.get_nodes(instance, renderer):\n            for node in data[\"nodes\"]:\n                try:\n                    render_value = cmds.getAttr(\n                        \"{}.{}\".format(node, data[\"attribute\"])\n                    )\n                except PublishValidationError:\n                    invalid = True\n                    cls.log.error(\n                        \"Cannot get value of {}.{}\".format(\n                            node, data[\"attribute\"]\n                        )\n                    )\n                else:\n                    if render_value not in data[\"values\"]:\n                        invalid = True\n                        cls.log.error(\n                            \"Invalid value {} set on {}.{}. Expecting \"\n                            \"{}\".format(\n                                render_value,\n                                node,\n                                data[\"attribute\"],\n                                data[\"values\"]\n                            )\n                        )\n\n        return invalid\n\n    @classmethod\n    def get_nodes(cls, instance, renderer):\n        maya_settings = instance.context.data[\"project_settings\"][\"maya\"]\n        validation_settings = (\n            maya_settings[\"publish\"][\"ValidateRenderSettings\"].get(\n                \"{}_render_attributes\".format(renderer)\n            ) or []\n        )\n        result = []\n        for attr, values in OrderedDict(validation_settings).items():\n            values = [convert_to_int_or_float(v) for v in values if v]\n\n            # Validate the settings has values.\n            if not values:\n                cls.log.error(\n                    \"Settings for {} is missing values.\".format(attr)\n                )\n                continue\n\n            cls.log.debug(\"{}: {}\".format(attr, values))\n            if \".\" not in attr:\n                cls.log.warning(\n                    \"Skipping invalid attribute defined in validation \"\n                    \"settings: \\\"{}\\\"\".format(attr)\n                )\n                continue\n\n            node_type, attribute_name = attr.split(\".\", 1)\n\n            # first get node of that type\n            nodes = cmds.ls(type=node_type)\n\n            if not nodes:\n                cls.log.warning(\n                    \"No nodes of type \\\"{}\\\" found.\".format(node_type)\n                )\n                continue\n\n            result.append(\n                {\n                    \"attribute\": attribute_name,\n                    \"nodes\": nodes,\n                    \"values\": values\n                }\n            )\n\n        return result\n\n    @classmethod\n    def repair(cls, instance):\n        renderer = instance.data['renderer']\n        layer_node = instance.data['setMembers']\n        redshift_AOV_prefix = cls.redshift_AOV_prefix.replace(\n            \"{aov_separator}\", instance.data.get(\"aovSeparator\", \"_\")\n        )\n        default_prefix = cls.ImagePrefixTokens[renderer].replace(\n            \"{aov_separator}\", instance.data.get(\"aovSeparator\", \"_\")\n        )\n\n        for data in cls.get_nodes(instance, renderer):\n            if not data[\"values\"]:\n                continue\n            for node in data[\"nodes\"]:\n                lib.set_attribute(data[\"attribute\"], data[\"values\"][0], node)\n        with lib.renderlayer(layer_node):\n\n            # Repair animation must be enabled\n            cmds.setAttr(\"defaultRenderGlobals.animation\", True)\n\n            # Repair prefix\n            if renderer == \"arnold\":\n                multipart = cmds.getAttr(\"defaultArnoldDriver.mergeAOVs\")\n                if multipart:\n                    separator_variations = [\n                        \"_<RenderPass>\",\n                        \"<RenderPass>_\",\n                        \"<RenderPass>\",\n                    ]\n                    for variant in separator_variations:\n                        default_prefix = default_prefix.replace(variant, \"\")\n\n            if renderer != \"renderman\":\n                prefix_attr = RenderSettings.get_image_prefix_attr(renderer)\n                fname_prefix = default_prefix\n                cmds.setAttr(prefix_attr, fname_prefix, type=\"string\")\n\n                # Repair padding\n                padding_attr = RenderSettings.get_padding_attr(renderer)\n                cmds.setAttr(padding_attr, cls.DEFAULT_PADDING)\n            else:\n                # renderman handles stuff differently\n                cmds.setAttr(\"rmanGlobals.imageFileFormat\",\n                             default_prefix,\n                             type=\"string\")\n                cmds.setAttr(\"rmanGlobals.imageOutputDir\",\n                             cls.renderman_dir_prefix,\n                             type=\"string\")\n\n            if renderer == \"vray\":\n                vray_settings = cmds.ls(type=\"VRaySettingsNode\")\n                if not vray_settings:\n                    node = cmds.createNode(\"VRaySettingsNode\")\n                else:\n                    node = vray_settings[0]\n\n                cmds.optionMenuGrp(\"vrayRenderElementSeparator\",\n                                   v=instance.data.get(\"aovSeparator\", \"_\"))\n                cmds.setAttr(\n                    \"{}.fileNameRenderElementSeparator\".format(node),\n                    instance.data.get(\"aovSeparator\", \"_\"),\n                    type=\"string\"\n                )\n\n            if renderer == \"redshift\":\n                # get redshift AOVs\n                rs_aovs = cmds.ls(type=\"RedshiftAOV\", referencedNodes=False)\n                for aov in rs_aovs:\n                    # fix AOV prefixes\n                    cmds.setAttr(\n                        \"{}.filePrefix\".format(aov),\n                        redshift_AOV_prefix, type=\"string\")\n                    # fix AOV file format\n                    default_ext = cmds.getAttr(\n                        \"redshiftOptions.imageFormat\", asString=True)\n                    cmds.setAttr(\n                        \"{}.fileFormat\".format(aov), default_ext)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_resolution.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom maya import cmds\nfrom openpype.pipeline.publish import RepairAction\nfrom openpype.hosts.maya.api import lib\nfrom openpype.hosts.maya.api.lib import reset_scene_resolution\n\n\nclass ValidateResolution(pyblish.api.InstancePlugin,\n                         OptionalPyblishPluginMixin):\n    \"\"\"Validate the render resolution setting aligned with DB\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    families = [\"renderlayer\"]\n    hosts = [\"maya\"]\n    label = \"Validate Resolution\"\n    actions = [RepairAction]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid_resolution(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Render resolution is invalid. See log for details.\",\n                description=(\n                    \"Wrong render resolution setting. \"\n                    \"Please use repair button to fix it.\\n\\n\"\n                    \"If current renderer is V-Ray, \"\n                    \"make sure vraySettings node has been created.\"\n                )\n            )\n\n    @classmethod\n    def get_invalid_resolution(cls, instance):\n        width, height, pixelAspect = cls.get_db_resolution(instance)\n        current_renderer = instance.data[\"renderer\"]\n        layer = instance.data[\"renderlayer\"]\n        invalid = False\n        if current_renderer == \"vray\":\n            vray_node = \"vraySettings\"\n            if cmds.objExists(vray_node):\n                current_width = lib.get_attr_in_layer(\n                    \"{}.width\".format(vray_node), layer=layer)\n                current_height = lib.get_attr_in_layer(\n                    \"{}.height\".format(vray_node), layer=layer)\n                current_pixelAspect = lib.get_attr_in_layer(\n                    \"{}.pixelAspect\".format(vray_node), layer=layer\n                )\n            else:\n                cls.log.error(\n                    \"Can't detect VRay resolution because there is no node \"\n                    \"named: `{}`\".format(vray_node)\n                )\n                return True\n        else:\n            current_width = lib.get_attr_in_layer(\n                \"defaultResolution.width\", layer=layer)\n            current_height = lib.get_attr_in_layer(\n                \"defaultResolution.height\", layer=layer)\n            current_pixelAspect = lib.get_attr_in_layer(\n                \"defaultResolution.pixelAspect\", layer=layer\n            )\n        if current_width != width or current_height != height:\n            cls.log.error(\n                \"Render resolution {}x{} does not match \"\n                \"asset resolution {}x{}\".format(\n                    current_width, current_height,\n                    width, height\n                ))\n            invalid = True\n        if current_pixelAspect != pixelAspect:\n            cls.log.error(\n                \"Render pixel aspect {} does not match \"\n                \"asset pixel aspect {}\".format(\n                    current_pixelAspect, pixelAspect\n                ))\n            invalid = True\n        return invalid\n\n    @classmethod\n    def get_db_resolution(cls, instance):\n        asset_doc = instance.data[\"assetEntity\"]\n        project_doc = instance.context.data[\"projectEntity\"]\n        for data in [asset_doc[\"data\"], project_doc[\"data\"]]:\n            if (\n                \"resolutionWidth\" in data and\n                \"resolutionHeight\" in data and\n                \"pixelAspect\" in data\n            ):\n                width = data[\"resolutionWidth\"]\n                height = data[\"resolutionHeight\"]\n                pixelAspect = data[\"pixelAspect\"]\n                return int(width), int(height), float(pixelAspect)\n\n        # Defaults if not found in asset document or project document\n        return 1920, 1080, 1.0\n\n    @classmethod\n    def repair(cls, instance):\n        # Usually without renderlayer overrides the renderlayers\n        # all share the same resolution value - so fixing the first\n        # will have fixed all the others too. It's much faster to\n        # check whether it's invalid first instead of switching\n        # into all layers individually\n        if not cls.get_invalid_resolution(instance):\n            cls.log.debug(\n                \"Nothing to repair on instance: {}\".format(instance)\n            )\n            return\n        layer_node = instance.data['setMembers']\n        with lib.renderlayer(layer_node):\n            reset_scene_resolution()\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_resources.py",
    "content": "import os\nfrom collections import defaultdict\n\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError\n)\n\n\nclass ValidateResources(pyblish.api.InstancePlugin):\n    \"\"\"Validates mapped resources.\n\n    These are external files to the current application, for example\n    these could be textures, image planes, cache files or other linked\n    media.\n\n    This validates:\n        - The resources have unique filenames (without extension)\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Resources Unique\"\n\n    def process(self, instance):\n\n        resources = instance.data.get(\"resources\", [])\n        if not resources:\n            self.log.debug(\"No resources to validate..\")\n            return\n\n        basenames = defaultdict(set)\n\n        for resource in resources:\n            files = resource.get(\"files\", [])\n            for filename in files:\n\n                # Use normalized paths in comparison and ignore case\n                # sensitivity\n                filename = os.path.normpath(filename).lower()\n\n                basename = os.path.splitext(os.path.basename(filename))[0]\n                basenames[basename].add(filename)\n\n        invalid_resources = list()\n        for basename, sources in basenames.items():\n            if len(sources) > 1:\n                invalid_resources.extend(sources)\n\n                self.log.error(\n                    \"Non-unique resource name: {0}\"\n                    \"{0} (sources: {1})\".format(\n                        basename,\n                        list(sources)\n                    )\n                )\n\n        if invalid_resources:\n            raise PublishValidationError(\"Invalid resources in instance.\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_review.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder, PublishValidationError\n)\n\n\nclass ValidateReview(pyblish.api.InstancePlugin):\n    \"\"\"Validate review.\"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Validate Review\"\n    families = [\"review\"]\n\n    def process(self, instance):\n        cameras = instance.data[\"cameras\"]\n\n        # validate required settings\n        if len(cameras) == 0:\n            raise PublishValidationError(\n                \"No camera found in review instance: {}\".format(instance)\n            )\n        elif len(cameras) > 2:\n            raise PublishValidationError(\n                \"Only a single camera is allowed for a review instance but \"\n                \"more than one camera found in review instance: {}. \"\n                \"Cameras found: {}\".format(instance, \", \".join(cameras))\n            )\n\n        self.log.debug('camera: {}'.format(instance.data[\"review_camera\"]))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_rig_contents.py",
    "content": "import pyblish.api\nfrom maya import cmds\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateRigContents(pyblish.api.InstancePlugin,\n                          OptionalPyblishPluginMixin):\n    \"\"\"Ensure rig contains pipeline-critical content\n\n    Every rig must contain at least two object sets:\n        \"controls_SET\" - Set of all animatable controls\n        \"out_SET\" - Set of all cacheable meshes\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Rig Contents\"\n    hosts = [\"maya\"]\n    families = [\"rig\"]\n    action = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = True\n\n    accepted_output = [\"mesh\", \"transform\"]\n    accepted_controllers = [\"transform\"]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Invalid rig content. See log for details.\")\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        # Find required sets by suffix\n        required, rig_sets = cls.get_nodes(instance)\n\n        cls.validate_missing_objectsets(instance, required, rig_sets)\n\n        controls_set = rig_sets[\"controls_SET\"]\n        out_set = rig_sets[\"out_SET\"]\n\n        # Ensure contents in sets and retrieve long path for all objects\n        output_content = cmds.sets(out_set, query=True) or []\n        if not output_content:\n            raise PublishValidationError(\"Must have members in rig out_SET\")\n        output_content = cmds.ls(output_content, long=True)\n\n        controls_content = cmds.sets(controls_set, query=True) or []\n        if not controls_content:\n            raise PublishValidationError(\n                \"Must have members in rig controls_SET\"\n            )\n        controls_content = cmds.ls(controls_content, long=True)\n\n        rig_content = output_content + controls_content\n        invalid_hierarchy = cls.invalid_hierarchy(instance, rig_content)\n\n        # Additional validations\n        invalid_geometry = cls.validate_geometry(output_content)\n        invalid_controls = cls.validate_controls(controls_content)\n\n        error = False\n        if invalid_hierarchy:\n            cls.log.error(\"Found nodes which reside outside of root group \"\n                           \"while they are set up for publishing.\"\n                           \"\\n%s\" % invalid_hierarchy)\n            error = True\n\n        if invalid_controls:\n            cls.log.error(\"Only transforms can be part of the controls_SET.\"\n                           \"\\n%s\" % invalid_controls)\n            error = True\n\n        if invalid_geometry:\n            cls.log.error(\"Only meshes can be part of the out_SET\\n%s\"\n                           % invalid_geometry)\n            error = True\n        if error:\n            return invalid_hierarchy + invalid_controls + invalid_geometry\n\n    @classmethod\n    def validate_missing_objectsets(cls, instance,\n                                    required_objsets, rig_sets):\n        \"\"\"Validate missing objectsets in rig sets\n\n        Args:\n            instance (str): instance\n            required_objsets (list): list of objectset names\n            rig_sets (list): list of rig sets\n\n        Raises:\n            PublishValidationError: When the error is raised, it will show\n                which instance has the missing object sets\n        \"\"\"\n        missing = [\n            key for key in required_objsets if key not in rig_sets\n        ]\n        if missing:\n            raise PublishValidationError(\n                \"%s is missing sets: %s\" % (instance, \", \".join(missing))\n            )\n\n    @classmethod\n    def invalid_hierarchy(cls, instance, content):\n        \"\"\"\n        Check if all rig set members are within the hierarchy of the rig root\n\n        Args:\n            instance (str): instance\n            content (list): list of content from rig sets\n\n        Raises:\n            PublishValidationError: It means no dag nodes in\n                the rig instance\n\n        Returns:\n            list: invalid hierarchy\n        \"\"\"\n        # Ensure there are at least some transforms or dag nodes\n        # in the rig instance\n        set_members = instance.data['setMembers']\n        if not cmds.ls(set_members, type=\"dagNode\", long=True):\n            raise PublishValidationError(\n                \"No dag nodes in the rig instance. \"\n                \"(Empty instance?)\"\n            )\n        # Validate members are inside the hierarchy from root node\n        root_nodes = cmds.ls(set_members, assemblies=True, long=True)\n        hierarchy = cmds.listRelatives(root_nodes, allDescendents=True,\n                                       fullPath=True) + root_nodes\n        hierarchy = set(hierarchy)\n        invalid_hierarchy = []\n        for node in content:\n            if node not in hierarchy:\n                invalid_hierarchy.append(node)\n        return invalid_hierarchy\n\n    @classmethod\n    def validate_geometry(cls, set_members):\n        \"\"\"\n        Checks if the node types of the set members valid\n\n        Args:\n            set_members: list of nodes of the controls_set\n            hierarchy: list of nodes which reside under the root node\n\n        Returns:\n            errors (list)\n        \"\"\"\n\n        # Validate all shape types\n        invalid = []\n        shapes = cmds.listRelatives(set_members,\n                                    allDescendents=True,\n                                    shapes=True,\n                                    fullPath=True) or []\n        all_shapes = cmds.ls(set_members + shapes, long=True, shapes=True)\n        for shape in all_shapes:\n            if cmds.nodeType(shape) not in cls.accepted_output:\n                invalid.append(shape)\n\n    @classmethod\n    def validate_controls(cls, set_members):\n        \"\"\"\n        Checks if the control set members are allowed node types.\n        Checks if the node types of the set members valid\n\n        Args:\n            set_members: list of nodes of the controls_set\n            hierarchy: list of nodes which reside under the root node\n\n        Returns:\n            errors (list)\n        \"\"\"\n\n        # Validate control types\n        invalid = []\n        for node in set_members:\n            if cmds.nodeType(node) not in cls.accepted_controllers:\n                invalid.append(node)\n\n        return invalid\n\n    @classmethod\n    def get_nodes(cls, instance):\n        \"\"\"Get the target objectsets and rig sets nodes\n\n        Args:\n            instance (str): instance\n\n        Returns:\n            tuple: 2-tuple of list of objectsets,\n                list of rig sets nodes\n        \"\"\"\n        objectsets = [\"controls_SET\", \"out_SET\"]\n        rig_sets_nodes = instance.data.get(\"rig_sets\", [])\n        return objectsets, rig_sets_nodes\n\n\nclass ValidateSkeletonRigContents(ValidateRigContents):\n    \"\"\"Ensure skeleton rigs contains pipeline-critical content\n\n    The rigs optionally contain at least two object sets:\n        \"skeletonMesh_SET\" - Set of the skinned meshes\n                             with bone hierarchies\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Skeleton Rig Contents\"\n    hosts = [\"maya\"]\n    families = [\"rig.fbx\"]\n    optional = True\n\n    @classmethod\n    def get_invalid(cls, instance):\n        objectsets, skeleton_mesh_nodes = cls.get_nodes(instance)\n        cls.validate_missing_objectsets(\n            instance, objectsets, instance.data[\"rig_sets\"])\n\n        # Ensure contents in sets and retrieve long path for all objects\n        output_content = instance.data.get(\"skeleton_mesh\", [])\n        output_content = cmds.ls(skeleton_mesh_nodes, long=True)\n\n        invalid_hierarchy = cls.invalid_hierarchy(\n            instance, output_content)\n        invalid_geometry = cls.validate_geometry(output_content)\n\n        error = False\n        if invalid_hierarchy:\n            cls.log.error(\"Found nodes which reside outside of root group \"\n                          \"while they are set up for publishing.\"\n                          \"\\n%s\" % invalid_hierarchy)\n            error = True\n        if invalid_geometry:\n            cls.log.error(\"Found nodes which reside outside of root group \"\n                          \"while they are set up for publishing.\"\n                          \"\\n%s\" % invalid_hierarchy)\n            error = True\n        if error:\n            return invalid_hierarchy + invalid_geometry\n\n    @classmethod\n    def get_nodes(cls, instance):\n        \"\"\"Get the target objectsets and rig sets nodes\n\n        Args:\n            instance (str): instance\n\n        Returns:\n            tuple: 2-tuple of list of objectsets,\n                list of rig sets nodes\n        \"\"\"\n        objectsets = [\"skeletonMesh_SET\"]\n        skeleton_mesh_nodes = instance.data.get(\"skeleton_mesh\", [])\n        return objectsets, skeleton_mesh_nodes\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_rig_controllers.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    RepairAction,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api.lib import undo_chunk\n\n\nclass ValidateRigControllers(pyblish.api.InstancePlugin,\n                             OptionalPyblishPluginMixin):\n    \"\"\"Validate rig controllers.\n\n    Controls must have the transformation attributes on their default\n    values of translate zero, rotate zero and scale one when they are\n    unlocked attributes.\n\n    Unlocked keyable attributes may not have any incoming connections. If\n    these connections are required for the rig then lock the attributes.\n\n    The visibility attribute must be locked.\n\n    Note that `repair` will:\n        - Lock all visibility attributes\n        - Reset all default values for translate, rotate, scale\n        - Break all incoming connections to keyable attributes\n\n    \"\"\"\n    order = ValidateContentsOrder + 0.05\n    label = \"Rig Controllers\"\n    hosts = [\"maya\"]\n    families = [\"rig\"]\n    optional = True\n    actions = [RepairAction,\n               openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    # Default controller values\n    CONTROLLER_DEFAULTS = {\n        \"translateX\": 0,\n        \"translateY\": 0,\n        \"translateZ\": 0,\n        \"rotateX\": 0,\n        \"rotateY\": 0,\n        \"rotateZ\": 0,\n        \"scaleX\": 1,\n        \"scaleY\": 1,\n        \"scaleZ\": 1\n    }\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                '{} failed, see log information'.format(self.label)\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        controls_set = cls.get_node(instance)\n        if not controls_set:\n            cls.log.error(\n                \"Must have 'controls_SET' in rig instance\"\n            )\n            return [instance.data[\"instance_node\"]]\n\n        controls = cmds.sets(controls_set, query=True)\n\n        # Ensure all controls are within the top group\n        lookup = set(instance[:])\n        if not all(control in lookup for control in cmds.ls(controls,\n                                                            long=True)):\n            cls.log.error(\n                \"All controls must be inside the rig's group.\"\n            )\n            return [controls_set]\n\n        # Validate all controls\n        has_connections = list()\n        has_unlocked_visibility = list()\n        has_non_default_values = list()\n        for control in controls:\n            if cls.get_connected_attributes(control):\n                has_connections.append(control)\n\n            # check if visibility is locked\n            attribute = \"{}.visibility\".format(control)\n            locked = cmds.getAttr(attribute, lock=True)\n            if not locked:\n                has_unlocked_visibility.append(control)\n\n            if cls.get_non_default_attributes(control):\n                has_non_default_values.append(control)\n\n        if has_connections:\n            cls.log.error(\"Controls have input connections: \"\n                          \"%s\" % has_connections)\n\n        if has_non_default_values:\n            cls.log.error(\"Controls have non-default values: \"\n                          \"%s\" % has_non_default_values)\n\n        if has_unlocked_visibility:\n            cls.log.error(\"Controls have unlocked visibility \"\n                          \"attribute: %s\" % has_unlocked_visibility)\n\n        invalid = []\n        if (has_connections or\n                has_unlocked_visibility or\n                has_non_default_values):\n            invalid = set()\n            invalid.update(has_connections)\n            invalid.update(has_non_default_values)\n            invalid.update(has_unlocked_visibility)\n            invalid = list(invalid)\n            cls.log.error(\"Invalid rig controllers. See log for details.\")\n\n        return invalid\n\n    @classmethod\n    def get_non_default_attributes(cls, control):\n        \"\"\"Return attribute plugs with non-default values\n\n        Args:\n            control (str): Name of control node.\n\n        Returns:\n            list: The invalid plugs\n\n        \"\"\"\n\n        invalid = []\n        for attr, default in cls.CONTROLLER_DEFAULTS.items():\n            if cmds.attributeQuery(attr, node=control, exists=True):\n                plug = \"{}.{}\".format(control, attr)\n\n                # Ignore locked attributes\n                locked = cmds.getAttr(plug, lock=True)\n                if locked:\n                    continue\n\n                value = cmds.getAttr(plug)\n                if value != default:\n                    cls.log.warning(\"Control non-default value: \"\n                                    \"%s = %s\" % (plug, value))\n                    invalid.append(plug)\n\n        return invalid\n\n    @staticmethod\n    def get_connected_attributes(control):\n        \"\"\"Return attribute plugs with incoming connections.\n\n        This will also ensure no (driven) keys on unlocked keyable attributes.\n\n        Args:\n            control (str): Name of control node.\n\n        Returns:\n            list: The invalid plugs\n\n        \"\"\"\n        import maya.cmds as mc\n\n        # Support controls without any attributes returning None\n        attributes = mc.listAttr(control, keyable=True, scalar=True) or []\n        invalid = []\n        for attr in attributes:\n            plug = \"{}.{}\".format(control, attr)\n\n            # Ignore locked attributes\n            locked = cmds.getAttr(plug, lock=True)\n            if locked:\n                continue\n\n            # Ignore proxy connections.\n            if (cmds.addAttr(plug, query=True, exists=True) and\n                    cmds.addAttr(plug, query=True, usedAsProxy=True)):\n                continue\n\n            # Check for incoming connections\n            if cmds.listConnections(plug, source=True, destination=False):\n                invalid.append(plug)\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n\n        controls_set = cls.get_node(instance)\n        if not controls_set:\n            cls.log.error(\n                \"Unable to repair because no 'controls_SET' found in rig \"\n                \"instance: {}\".format(instance)\n            )\n            return\n\n        # Use a single undo chunk\n        with undo_chunk():\n            controls = cmds.sets(controls_set, query=True)\n            for control in controls:\n\n                # Lock visibility\n                attr = \"{}.visibility\".format(control)\n                locked = cmds.getAttr(attr, lock=True)\n                if not locked:\n                    cls.log.info(\"Locking visibility for %s\" % control)\n                    cmds.setAttr(attr, lock=True)\n\n                # Remove incoming connections\n                invalid_plugs = cls.get_connected_attributes(control)\n                if invalid_plugs:\n                    for plug in invalid_plugs:\n                        cls.log.info(\"Breaking input connection to %s\" % plug)\n                        source = cmds.listConnections(plug,\n                                                      source=True,\n                                                      destination=False,\n                                                      plugs=True)[0]\n                        cmds.disconnectAttr(source, plug)\n\n                # Reset non-default values\n                invalid_plugs = cls.get_non_default_attributes(control)\n                if invalid_plugs:\n                    for plug in invalid_plugs:\n                        attr = plug.split(\".\")[-1]\n                        default = cls.CONTROLLER_DEFAULTS[attr]\n                        cls.log.info(\"Setting %s to %s\" % (plug, default))\n                        cmds.setAttr(plug, default)\n\n    @classmethod\n    def get_node(cls, instance):\n        \"\"\"Get target object nodes from controls_SET\n\n        Args:\n            instance (str): instance\n\n        Returns:\n            list: list of object nodes from controls_SET\n        \"\"\"\n        return instance.data[\"rig_sets\"].get(\"controls_SET\")\n\n\nclass ValidateSkeletonRigControllers(ValidateRigControllers):\n    \"\"\"Validate rig controller for skeletonAnim_SET\n\n    Controls must have the transformation attributes on their default\n    values of translate zero, rotate zero and scale one when they are\n    unlocked attributes.\n\n    Unlocked keyable attributes may not have any incoming connections. If\n    these connections are required for the rig then lock the attributes.\n\n    The visibility attribute must be locked.\n\n    Note that `repair` will:\n        - Lock all visibility attributes\n        - Reset all default values for translate, rotate, scale\n        - Break all incoming connections to keyable attributes\n\n    \"\"\"\n    order = ValidateContentsOrder + 0.05\n    label = \"Skeleton Rig Controllers\"\n    hosts = [\"maya\"]\n    families = [\"rig.fbx\"]\n\n    # Default controller values\n    CONTROLLER_DEFAULTS = {\n        \"translateX\": 0,\n        \"translateY\": 0,\n        \"translateZ\": 0,\n        \"rotateX\": 0,\n        \"rotateY\": 0,\n        \"rotateZ\": 0,\n        \"scaleX\": 1,\n        \"scaleY\": 1,\n        \"scaleZ\": 1\n    }\n\n    @classmethod\n    def get_node(cls, instance):\n        \"\"\"Get target object nodes from skeletonMesh_SET\n\n        Args:\n            instance (str): instance\n\n        Returns:\n            list: list of object nodes from skeletonMesh_SET\n        \"\"\"\n        return instance.data[\"rig_sets\"].get(\"skeletonMesh_SET\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    RepairAction,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\nfrom openpype.hosts.maya.api import lib\nimport openpype.hosts.maya.api.action\n\n\nclass ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin,\n                                             OptionalPyblishPluginMixin):\n    \"\"\"Validate rig control curves have no keyable arnold attributes.\n\n    The Arnold plug-in will create curve attributes like:\n        - aiRenderCurve\n        - aiCurveWidth\n        - aiSampleRate\n        - aiCurveShaderR\n        - aiCurveShaderG\n        - aiCurveShaderB\n\n    Unfortunately these attributes visible in the channelBox are *keyable*\n    by default and visible in the channelBox. As such pressing a regular \"S\"\n    set key shortcut will set keys on these attributes too, thus cluttering\n    the animator's scene.\n\n    This validator will ensure they are hidden or unkeyable attributes.\n\n    \"\"\"\n    order = ValidateContentsOrder + 0.05\n    label = \"Rig Controllers (Arnold Attributes)\"\n    hosts = [\"maya\"]\n    families = [\"rig\"]\n    optional = False\n    actions = [RepairAction,\n               openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    attributes = [\n        \"rcurve\",\n        \"cwdth\",\n        \"srate\",\n        \"ai_curve_shaderr\",\n        \"ai_curve_shaderg\",\n        \"ai_curve_shaderb\"\n    ]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError('{} failed, see log '\n                               'information'.format(self.label))\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        controls_set = instance.data[\"rig_sets\"].get(\"controls_SET\")\n        if not controls_set:\n            return []\n\n        controls = cmds.sets(controls_set, query=True) or []\n        if not controls:\n            return []\n\n        shapes = cmds.ls(controls,\n                         dag=True,\n                         leaf=True,\n                         long=True,\n                         shapes=True,\n                         noIntermediate=True)\n        curves = cmds.ls(shapes, type=\"nurbsCurve\", long=True)\n\n        invalid = list()\n        for node in curves:\n\n            for attribute in cls.attributes:\n                if cmds.attributeQuery(attribute, node=node, exists=True):\n                    plug = \"{}.{}\".format(node, attribute)\n                    if cmds.getAttr(plug, keyable=True):\n                        invalid.append(node)\n                        break\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n\n        invalid = cls.get_invalid(instance)\n        with lib.undo_chunk():\n            for node in invalid:\n                for attribute in cls.attributes:\n                    if cmds.attributeQuery(attribute, node=node, exists=True):\n                        plug = \"{}.{}\".format(node, attribute)\n                        cmds.setAttr(plug, channelBox=False, keyable=False)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_rig_joints_hidden.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateRigJointsHidden(pyblish.api.InstancePlugin,\n                              OptionalPyblishPluginMixin):\n    \"\"\"Validate all joints are hidden visually.\n\n    This includes being hidden:\n        - visibility off,\n        - in a display layer that has visibility off,\n        - having hidden parents or\n        - being an intermediate object.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = ['rig']\n    label = \"Joints Hidden\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               RepairAction]\n    optional = True\n\n    @staticmethod\n    def get_invalid(instance):\n        joints = cmds.ls(instance, type='joint', long=True)\n        return [j for j in joints if lib.is_visible(j, displayLayer=True)]\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance 'objectSet'\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\n                \"Visible joints found: {0}\".format(invalid))\n\n    @classmethod\n    def repair(cls, instance):\n        import maya.mel as mel\n        mel.eval(\"HideJoints\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py",
    "content": "import maya.cmds as cmds\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin,\n                               OptionalPyblishPluginMixin):\n    \"\"\"Validate if deformed shapes have related IDs to the original shapes.\n\n    When a deformer is applied in the scene on a referenced mesh that already\n    had deformers then Maya will create a new shape node for the mesh that\n    does not have the original id. This validator checks whether the ids are\n    valid on all the shape nodes in the instance.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    families = [\"rig\"]\n    hosts = ['maya']\n    label = 'Rig Out Set Node Ids'\n    actions = [\n        openpype.hosts.maya.api.action.SelectInvalidAction,\n        RepairAction\n    ]\n    allow_history_only = False\n    optional = False\n\n    def process(self, instance):\n        \"\"\"Process all meshes\"\"\"\n        if not self.is_active(instance.data):\n            return\n        # Ensure all nodes have a cbId and a related ID to the original shapes\n        # if a deformer has been created on the shape\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Nodes found with mismatching IDs: {0}\".format(invalid)\n            )\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Get all nodes which do not match the criteria\"\"\"\n\n        out_set = cls.get_node(instance)\n        if not out_set:\n            return []\n\n        invalid = []\n        members = cmds.sets(out_set, query=True)\n        shapes = cmds.ls(members,\n                         dag=True,\n                         leaf=True,\n                         shapes=True,\n                         long=True,\n                         noIntermediate=True)\n\n        for shape in shapes:\n            sibling_id = lib.get_id_from_sibling(\n                shape,\n                history_only=cls.allow_history_only\n            )\n            if sibling_id:\n                current_id = lib.get_id(shape)\n                if current_id != sibling_id:\n                    invalid.append(shape)\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n\n        for node in cls.get_invalid(instance):\n            # Get the original id from sibling\n            sibling_id = lib.get_id_from_sibling(\n                node,\n                history_only=cls.allow_history_only\n            )\n            if not sibling_id:\n                cls.log.error(\"Could not find ID in siblings for '%s'\", node)\n                continue\n\n            lib.set_id(node, sibling_id, overwrite=True)\n\n    @classmethod\n    def get_node(cls, instance):\n        \"\"\"Get target object nodes from out_SET\n\n        Args:\n            instance (str): instance\n\n        Returns:\n            list: list of object nodes from out_SET\n        \"\"\"\n        return instance.data[\"rig_sets\"].get(\"out_SET\")\n\n\nclass ValidateSkeletonRigOutSetNodeIds(ValidateRigOutSetNodeIds):\n    \"\"\"Validate if deformed shapes have related IDs to the original shapes\n    from skeleton set.\n\n    When a deformer is applied in the scene on a referenced mesh that already\n    had deformers then Maya will create a new shape node for the mesh that\n    does not have the original id. This validator checks whether the ids are\n    valid on all the shape nodes in the instance.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    families = [\"rig.fbx\"]\n    hosts = ['maya']\n    label = 'Skeleton Rig Out Set Node Ids'\n    optional = False\n\n    @classmethod\n    def get_node(cls, instance):\n        \"\"\"Get target object nodes from skeletonMesh_SET\n\n        Args:\n            instance (str): instance\n\n        Returns:\n            list: list of object nodes from skeletonMesh_SET\n        \"\"\"\n        return instance.data[\"rig_sets\"].get(\n            \"skeletonMesh_SET\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py",
    "content": "from collections import defaultdict\n\nfrom maya import cmds\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api.lib import get_id, set_id\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError\n)\n\n\ndef get_basename(node):\n    \"\"\"Return node short name without namespace\"\"\"\n    return node.rsplit(\"|\", 1)[-1].rsplit(\":\", 1)[-1]\n\n\nclass ValidateRigOutputIds(pyblish.api.InstancePlugin):\n    \"\"\"Validate rig output ids.\n\n    Ids must share the same id as similarly named nodes in the scene. This is\n    to ensure the id from the model is preserved through animation.\n\n    \"\"\"\n    order = ValidateContentsOrder + 0.05\n    label = \"Rig Output Ids\"\n    hosts = [\"maya\"]\n    families = [\"rig\"]\n    actions = [RepairAction,\n               openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    def process(self, instance):\n        invalid = self.get_invalid(instance, compute=True)\n        if invalid:\n            raise PublishValidationError(\"Found nodes with mismatched IDs.\")\n\n    @classmethod\n    def get_invalid(cls, instance, compute=False):\n        invalid_matches = cls.get_invalid_matches(instance, compute=compute)\n        return list(invalid_matches.keys())\n\n    @classmethod\n    def get_invalid_matches(cls, instance, compute=False):\n        invalid = {}\n\n        if compute:\n            out_set = cls.get_node(instance)\n            if not out_set:\n                instance.data[\"mismatched_output_ids\"] = invalid\n                return invalid\n\n            instance_nodes = cmds.sets(out_set, query=True, nodesOnly=True)\n            instance_nodes = cmds.ls(instance_nodes, long=True)\n            for node in instance_nodes:\n                shapes = cmds.listRelatives(node, shapes=True, fullPath=True)\n                if shapes:\n                    instance_nodes.extend(shapes)\n\n            scene_nodes = cmds.ls(type=\"transform\", long=True)\n            scene_nodes += cmds.ls(type=\"mesh\", long=True)\n            scene_nodes = set(scene_nodes) - set(instance_nodes)\n\n            scene_nodes_by_basename = defaultdict(list)\n            for node in scene_nodes:\n                basename = get_basename(node)\n                scene_nodes_by_basename[basename].append(node)\n\n            for instance_node in instance_nodes:\n                basename = get_basename(instance_node)\n                if basename not in scene_nodes_by_basename:\n                    continue\n\n                matches = scene_nodes_by_basename[basename]\n\n                ids = set(get_id(node) for node in matches)\n                ids.add(get_id(instance_node))\n\n                if len(ids) > 1:\n                    cls.log.error(\n                        \"\\\"{}\\\" id mismatch to: {}\".format(\n                            instance_node, matches\n                        )\n                    )\n                    invalid[instance_node] = matches\n\n            instance.data[\"mismatched_output_ids\"] = invalid\n        else:\n            invalid = instance.data[\"mismatched_output_ids\"]\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n        invalid_matches = cls.get_invalid_matches(instance)\n\n        multiple_ids_match = []\n        for instance_node, matches in invalid_matches.items():\n            ids = set(get_id(node) for node in matches)\n\n            # If there are multiple scene ids matched, and error needs to be\n            # raised for manual correction.\n            if len(ids) > 1:\n                multiple_ids_match.append({\"node\": instance_node,\n                                           \"matches\": matches})\n                continue\n\n            id_to_set = next(iter(ids))\n            set_id(instance_node, id_to_set, overwrite=True)\n\n        if multiple_ids_match:\n            raise PublishValidationError(\n                \"Multiple matched ids found. Please repair manually: \"\n                \"{}\".format(multiple_ids_match)\n            )\n\n    @classmethod\n    def get_node(cls, instance):\n        \"\"\"Get target object nodes from out_SET\n\n        Args:\n            instance (str): instance\n\n        Returns:\n            list: list of object nodes from out_SET\n        \"\"\"\n        return instance.data[\"rig_sets\"].get(\"out_SET\")\n\n\nclass ValidateSkeletonRigOutputIds(ValidateRigOutputIds):\n    \"\"\"Validate rig output ids from the skeleton sets.\n\n    Ids must share the same id as similarly named nodes in the scene. This is\n    to ensure the id from the model is preserved through animation.\n\n    \"\"\"\n    order = ValidateContentsOrder + 0.05\n    label = \"Skeleton Rig Output Ids\"\n    hosts = [\"maya\"]\n    families = [\"rig.fbx\"]\n\n    @classmethod\n    def get_node(cls, instance):\n        \"\"\"Get target object nodes from skeletonMesh_SET\n\n        Args:\n            instance (str): instance\n\n        Returns:\n            list: list of object nodes from skeletonMesh_SET\n        \"\"\"\n        return instance.data[\"rig_sets\"].get(\"skeletonMesh_SET\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py",
    "content": "import os\n\nimport maya.cmds as cmds\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    PublishValidationError, ValidatePipelineOrder)\n\n\ndef is_subdir(path, root_dir):\n    \"\"\" Returns whether path is a subdirectory (or file) within root_dir \"\"\"\n    path = os.path.realpath(path)\n    root_dir = os.path.realpath(root_dir)\n\n    # If not on same drive\n    if os.path.splitdrive(path)[0].lower() != os.path.splitdrive(root_dir)[0].lower():  # noqa: E501\n        return False\n\n    # Get 'relative path' (can contain ../ which means going up)\n    relative = os.path.relpath(path, root_dir)\n\n    # Check if the path starts by going up, if so it's not a subdirectory. :)\n    if relative.startswith(os.pardir) or relative == os.curdir:\n        return False\n    else:\n        return True\n\n\nclass ValidateSceneSetWorkspace(pyblish.api.ContextPlugin):\n    \"\"\"Validate the scene is inside the currently set Maya workspace\"\"\"\n\n    order = ValidatePipelineOrder\n    hosts = ['maya']\n    label = 'Maya Workspace Set'\n\n    def process(self, context):\n\n        scene_name = cmds.file(query=True, sceneName=True)\n        if not scene_name:\n            raise PublishValidationError(\n                \"Scene hasn't been saved. Workspace can't be validated.\")\n\n        root_dir = cmds.workspace(query=True, rootDirectory=True)\n\n        if not is_subdir(scene_name, root_dir):\n            raise PublishValidationError(\n                \"Maya workspace is not set correctly.\\n\\n\"\n                f\"Current workfile `{scene_name}` is not inside the \"\n                \"current Maya project root directory `{root_dir}`.\\n\\n\"\n                \"Please use Workfile app to re-save.\"\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_setdress_root.py",
    "content": "import pyblish.api\nfrom openpype.pipeline.publish import ValidateContentsOrder\n\n\nclass ValidateSetdressRoot(pyblish.api.InstancePlugin):\n    \"\"\"Validate if set dress top root node is published.\"\"\"\n\n    order = ValidateContentsOrder\n    label = \"SetDress Root\"\n    hosts = [\"maya\"]\n    families = [\"setdress\"]\n\n    def process(self, instance):\n        from maya import cmds\n\n        if instance.data.get(\"exactSetMembersOnly\"):\n            return\n\n        set_member = instance.data[\"setMembers\"]\n        root = cmds.ls(set_member, assemblies=True, long=True)\n\n        if not root or root[0] not in set_member:\n            raise Exception(\"Setdress top root node is not being published.\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_shader_name.py",
    "content": "import re\n\nimport pyblish.api\nfrom maya import cmds\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    OptionalPyblishPluginMixin, PublishValidationError, ValidateContentsOrder)\n\n\nclass ValidateShaderName(pyblish.api.InstancePlugin,\n                         OptionalPyblishPluginMixin):\n    \"\"\"Validate shader name assigned.\n\n       It should be <assetName>_<*>_SHD\n\n    \"\"\"\n    optional = True\n    order = ValidateContentsOrder\n    families = [\"look\"]\n    hosts = ['maya']\n    label = 'Validate Shaders Name'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    regex = r'(?P<asset>.*)_(.*)_SHD'\n\n    # The default connections to check\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Found shapes with invalid shader names \"\n                 \"assigned:\\n{}\").format(invalid))\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        invalid = []\n\n        # Get all shapes from the instance\n        content_instance = instance.data.get(\"setMembers\", None)\n        if not content_instance:\n            cls.log.error(\"Instance has no nodes!\")\n            return True\n        pass\n        descendants = cmds.listRelatives(content_instance,\n                                         allDescendents=True,\n                                         fullPath=True) or []\n\n        descendants = cmds.ls(descendants, noIntermediate=True, long=True)\n        shapes = cmds.ls(descendants, type=[\"nurbsSurface\", \"mesh\"], long=True)\n        asset_name = instance.data.get(\"asset\")\n\n        # Check the number of connected shadingEngines per shape\n        regex_compile = re.compile(cls.regex)\n        error_message = \"object {0} has invalid shader name {1}\"\n        for shape in shapes:\n            shading_engines = cmds.listConnections(shape,\n                                                   destination=True,\n                                                   type=\"shadingEngine\") or []\n            shaders = cmds.ls(\n                cmds.listConnections(shading_engines), materials=1\n            )\n\n            for shader in shaders:\n                m = regex_compile.match(shader)\n                if m is None:\n                    invalid.append(shape)\n                    cls.log.error(error_message.format(shape, shader))\n                else:\n                    if 'asset' in regex_compile.groupindex:\n                        if m.group('asset') != asset_name:\n                            invalid.append(shape)\n                            message = error_message\n                            message += \" with missing asset name \\\"{2}\\\"\"\n                            cls.log.error(\n                                message.format(shape, shader, asset_name)\n                            )\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_shape_default_names.py",
    "content": "import re\n\nfrom maya import cmds\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    RepairAction,\n    OptionalPyblishPluginMixin\n)\n\n\ndef short_name(node):\n    return node.rsplit(\"|\", 1)[-1].rsplit(\":\", 1)[-1]\n\n\nclass ValidateShapeDefaultNames(pyblish.api.InstancePlugin,\n                                OptionalPyblishPluginMixin):\n    \"\"\"Validates that Shape names are using Maya's default format.\n\n    When you create a new polygon cube Maya will name the transform\n    and shape respectively:\n    - ['pCube1', 'pCubeShape1']\n    If you rename it to `bar1` it will become:\n    - ['bar1', 'barShape1']\n    Then if you rename it to `bar` it will become:\n    - ['bar', 'barShape']\n    Rename it again to `bar1` it will differ as opposed to before:\n    - ['bar1', 'bar1Shape']\n    Note that bar1Shape != barShape1\n    Thus the suffix number can be either in front of Shape or behind it.\n    Then it becomes harder to define where what number should be when a\n    node contains multiple shapes, for example with many controls in\n    rigs existing of multiple curves.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = ['model']\n    optional = True\n    label = \"Shape Default Naming\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               RepairAction]\n\n    @staticmethod\n    def _define_default_name(shape):\n        parent = cmds.listRelatives(shape, parent=True, fullPath=True)[0]\n        transform = short_name(parent)\n        return '{0}Shape'.format(transform)\n\n    @staticmethod\n    def _is_valid(shape):\n        \"\"\" Return whether the shape's name is similar to Maya's default. \"\"\"\n        transform = cmds.listRelatives(shape, parent=True, fullPath=True)[0]\n\n        transform_name = short_name(transform)\n        shape_name = short_name(shape)\n\n        # A Shape's name can be either {transform}{numSuffix}\n        # Shape or {transform}Shape{numSuffix}\n        # Upon renaming nodes in Maya that is\n        # the pattern Maya will act towards.\n        transform_no_num = transform_name.rstrip(\"0123456789\")\n        pattern = '^{transform}[0-9]*Shape[0-9]*$'.format(\n            transform=transform_no_num)\n\n        if re.match(pattern, shape_name):\n            return True\n        else:\n            return False\n\n    @classmethod\n    def get_invalid(cls, instance):\n        shapes = cmds.ls(instance, shapes=True, long=True)\n        return [shape for shape in shapes if not cls._is_valid(shape)]\n\n    def process(self, instance):\n        \"\"\"Process all the shape nodes in the instance\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise ValueError(\"Incorrectly named shapes \"\n                             \"found: {0}\".format(invalid))\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"Process all the shape nodes in the instance\"\"\"\n        for shape in cls.get_invalid(instance):\n            correct_shape_name = cls._define_default_name(shape)\n            cmds.rename(shape, correct_shape_name)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_shape_render_stats.py",
    "content": "import pyblish.api\n\nfrom maya import cmds\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateMeshOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateShapeRenderStats(pyblish.api.Validator,\n                               OptionalPyblishPluginMixin):\n    \"\"\"Ensure all render stats are set to the default values.\"\"\"\n\n    order = ValidateMeshOrder\n    hosts = ['maya']\n    families = ['model']\n    label = 'Shape Default Render Stats'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction,\n               RepairAction]\n    optional = True\n\n    defaults = {'castsShadows': 1,\n                'receiveShadows': 1,\n                'motionBlur': 1,\n                'primaryVisibility': 1,\n                'smoothShading': 1,\n                'visibleInReflections': 1,\n                'visibleInRefractions': 1,\n                'doubleSided': 1,\n                'opposite': 0}\n\n    @classmethod\n    def get_invalid(cls, instance):\n        # It seems the \"surfaceShape\" and those derived from it have\n        # `renderStat` attributes.\n        shapes = cmds.ls(instance, long=True, type='surfaceShape')\n        invalid = []\n        for shape in shapes:\n            _iteritems = getattr(cls.defaults, \"iteritems\", cls.defaults.items)\n            for attr, default_value in _iteritems():\n                if cmds.attributeQuery(attr, node=shape, exists=True):\n                    value = cmds.getAttr('{}.{}'.format(shape, attr))\n                    if value != default_value:\n                        invalid.append(shape)\n\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise ValueError(\"Shapes with non-default renderStats \"\n                             \"found: {0}\".format(invalid))\n\n    @classmethod\n    def repair(cls, instance):\n        for shape in cls.get_invalid(instance):\n            _iteritems = getattr(cls.defaults, \"iteritems\", cls.defaults.items)\n            for attr, default_value in _iteritems():\n\n                if cmds.attributeQuery(attr, node=shape, exists=True):\n                    plug = '{0}.{1}'.format(shape, attr)\n                    value = cmds.getAttr(plug)\n                    if value != default_value:\n                        cmds.setAttr(plug, default_value)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_shape_zero.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    RepairAction,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateShapeZero(pyblish.api.Validator,\n                        OptionalPyblishPluginMixin):\n    \"\"\"Shape components may not have any \"tweak\" values\n\n    To solve this issue, try freezing the shapes.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"model\"]\n    label = \"Shape Zero (Freeze)\"\n    actions = [\n        openpype.hosts.maya.api.action.SelectInvalidAction,\n        RepairAction\n    ]\n    optional = True\n\n    @staticmethod\n    def get_invalid(instance):\n        \"\"\"Returns the invalid shapes in the instance.\n\n        This is the same as checking:\n        - all(pnt == [0,0,0] for pnt in shape.pnts[:])\n\n        Returns:\n            list: Shape with non freezed vertex\n\n        \"\"\"\n\n        shapes = cmds.ls(instance, type=\"shape\")\n\n        invalid = []\n        for shape in shapes:\n            if cmds.polyCollapseTweaks(shape, q=True, hasVertexTweaks=True):\n                invalid.append(shape)\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n        invalid_shapes = cls.get_invalid(instance)\n        if not invalid_shapes:\n            return\n\n        with lib.maintained_selection():\n            with lib.tool(\"selectSuperContext\"):\n                for shape in invalid_shapes:\n                    cmds.polyCollapseTweaks(shape)\n                    # cmds.polyCollapseTweaks keeps selecting the geometry\n                    # after each command. When running on many meshes\n                    # after one another this tends to get really heavy\n                    cmds.select(clear=True)\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance \"objectSet\"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                title=\"Shape Component Tweaks\",\n                message=\"Shapes found with non-zero component tweaks: '{}'\"\n                        \"\".format(\", \".join(invalid)),\n                description=(\n                    \"## Shapes found with component tweaks\\n\"\n                    \"Shapes were detected that have component tweaks on their \"\n                    \"components. Please remove the component tweaks to \"\n                    \"continue.\\n\\n\"\n                    \"### Repair\\n\"\n                    \"The repair action will try to *freeze* the component \"\n                    \"tweaks into the shapes, which is usually the correct fix \"\n                    \"if the mesh has no construction history (= has its \"\n                    \"history deleted).\"),\n                detail=(\n                    \"Maya allows to store component tweaks within shape nodes \"\n                    \"which are applied between its `inMesh` and `outMesh` \"\n                    \"connections resulting in the output of a shape node \"\n                    \"differing from the input. We usually want to avoid this \"\n                    \"for published meshes (in particular for Maya scenes) as \"\n                    \"it can have unintended results when using these meshes \"\n                    \"as intermediate meshes since it applies positional \"\n                    \"differences without being visible edits in the node \"\n                    \"graph.\\n\\n\"\n                    \"These tweaks are traditionally stored in the `.pnts` \"\n                    \"attribute of shapes.\")\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_single_assembly.py",
    "content": "import pyblish.api\nfrom openpype.pipeline.publish import ValidateContentsOrder\n\n\nclass ValidateSingleAssembly(pyblish.api.InstancePlugin):\n    \"\"\"Ensure the content of the instance is grouped in a single hierarchy\n\n    The instance must have a single root node containing all the content.\n    This root node *must* be a top group in the outliner.\n\n    Example outliner:\n        root_GRP\n            -- geometry_GRP\n               -- mesh_GEO\n            -- controls_GRP\n               -- control_CTL\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = ['rig']\n    label = 'Single Assembly'\n\n    def process(self, instance):\n        from maya import cmds\n\n        assemblies = cmds.ls(instance, assemblies=True)\n\n        # ensure unique (somehow `maya.cmds.ls` doesn't manage that)\n        assemblies = set(assemblies)\n\n        assert len(assemblies) > 0, (\n            \"One assembly required for: %s (currently empty?)\" % instance)\n        assert len(assemblies) < 2, (\n            'Multiple assemblies found: %s' % assemblies)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin\n)\n\nfrom maya import cmds\n\n\nclass ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin,\n                                    OptionalPyblishPluginMixin):\n    \"\"\"Validates that nodes has common root.\"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"skeletalMesh\"]\n    label = \"Skeletal Mesh Top Node\"\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        geo = instance.data.get(\"geometry\")\n        joints = instance.data.get(\"joints\")\n\n        joints_parents = cmds.ls(joints, long=True)\n        geo_parents = cmds.ls(geo, long=True)\n\n        parents_set = {\n            parent.split(\"|\")[1] for parent in (joints_parents + geo_parents)\n        }\n\n        self.log.debug(parents_set)\n\n        if len(set(parents_set)) > 2:\n            raise PublishXmlValidationError(\n                self,\n                \"Multiple roots on geometry or joints.\"\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_skeletalmesh_triangulated.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\n\nfrom openpype.hosts.maya.api.action import (\n    SelectInvalidAction,\n)\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishValidationError\n)\n\n\nfrom maya import cmds\n\n\nclass ValidateSkeletalMeshTriangulated(pyblish.api.InstancePlugin):\n    \"\"\"Validates that the geometry has been triangulated.\"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"skeletalMesh\"]\n    label = \"Skeletal Mesh Triangulated\"\n    optional = True\n    actions = [\n        SelectInvalidAction,\n        RepairAction\n    ]\n\n    def process(self, instance):\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"The following objects needs to be triangulated: \"\n                \"{}\".format(invalid))\n\n    @classmethod\n    def get_invalid(cls, instance):\n        geo = instance.data.get(\"geometry\")\n\n        invalid = []\n\n        for obj in cmds.listRelatives(\n                cmds.ls(geo), allDescendents=True, fullPath=True):\n            n_triangles = cmds.polyEvaluate(obj, triangle=True)\n            n_faces = cmds.polyEvaluate(obj, face=True)\n\n            if not (isinstance(n_triangles, int) and isinstance(n_faces, int)):\n                continue\n\n            # We check if the number of triangles is equal to the number of\n            # faces for each transform node.\n            # If it is, the object is triangulated.\n            if cmds.objectType(obj, i=\"transform\") and n_triangles != n_faces:\n                invalid.append(obj)\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n        for node in cls.get_invalid(instance):\n            cmds.polyTriangulate(node)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Plugin for validating naming conventions.\"\"\"\nfrom maya import cmds\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\nclass ValidateSkeletonTopGroupHierarchy(pyblish.api.InstancePlugin,\n                                        OptionalPyblishPluginMixin):\n    \"\"\"Validates top group hierarchy in the SETs\n    Make sure the object inside the SETs are always top\n    group of the hierarchy\n\n    \"\"\"\n    order = ValidateContentsOrder + 0.05\n    label = \"Skeleton Rig Top Group Hierarchy\"\n    families = [\"rig.fbx\"]\n\n    def process(self, instance):\n        invalid = []\n        skeleton_mesh_data = instance.data(\"skeleton_mesh\", [])\n        if skeleton_mesh_data:\n            invalid = self.get_top_hierarchy(skeleton_mesh_data)\n            if invalid:\n                raise PublishValidationError(\n                    \"The skeletonMesh_SET includes the object which \"\n                    \"is not at the top hierarchy: {}\".format(invalid))\n\n    def get_top_hierarchy(self, targets):\n        targets = cmds.ls(targets, long=True)  # ensure long names\n        non_top_hierarchy_list = [\n            target for target in targets if target.count(\"|\") > 2\n        ]\n        return non_top_hierarchy_list\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin,\n                                     OptionalPyblishPluginMixin):\n    \"\"\"Validate skinClusters on meshes have valid member relationships.\n\n    In rare cases it can happen that a mesh has a skinCluster in its history\n    but it is *not* included in the deformer relationship history. If this is\n    the case then FBX will not export the skinning.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = ['fbx']\n    label = \"Skincluster Deformer Relationships\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    def process(self, instance):\n        \"\"\"Process all the transform nodes in the instance\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise ValueError(\"Invalid skinCluster relationships \"\n                             \"found on meshes: {0}\".format(invalid))\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        meshes = cmds.ls(instance, type=\"mesh\", noIntermediate=True, long=True)\n        invalid = list()\n\n        for mesh in meshes:\n            history = cmds.listHistory(mesh) or []\n            skins = cmds.ls(history, type=\"skinCluster\")\n\n            # Ensure at most one skinCluster\n            assert len(skins) <= 1, \"Cannot have more than one skinCluster\"\n\n            if skins:\n                skin = skins[0]\n\n                # Ensure the mesh is also in the skinCluster set\n                # otherwise the skin will not be exported correctly\n                # by the FBX Exporter.\n                deformer_sets = cmds.listSets(object=mesh, type=2)\n                for deformer_set in deformer_sets:\n                    used_by = cmds.listConnections(deformer_set + \".usedBy\",\n                                                   source=True,\n                                                   destination=False)\n\n                    # Ignore those that don't seem to have a usedBy connection\n                    if not used_by:\n                        continue\n\n                    # We have a matching deformer set relationship\n                    if skin in set(used_by):\n                        break\n\n                else:\n                    invalid.append(mesh)\n                    cls.log.warning(\n                        \"Mesh has skinCluster in history but is not included \"\n                        \"in its deformer relationship set: \"\n                        \"{0} (skinCluster: {1})\".format(mesh, skin)\n                    )\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_step_size.py",
    "content": "import pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateStepSize(pyblish.api.InstancePlugin,\n                       OptionalPyblishPluginMixin):\n    \"\"\"Validates the step size for the instance is in a valid range.\n\n    For example the `step` size should never be lower or equal to zero.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = 'Step size'\n    families = ['camera',\n                'pointcache',\n                'animation']\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n    MIN = 0.01\n    MAX = 1.0\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        objset = instance.data['name']\n        step = instance.data.get(\"step\", 1.0)\n\n        if step < cls.MIN or step > cls.MAX:\n            cls.log.warning(\"Step size is outside of valid range: {0} \"\n                            \"(valid: {1} to {2})\".format(step,\n                                                         cls.MIN,\n                                                         cls.MAX))\n            return objset\n\n        return []\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                \"Invalid instances found: {0}\".format(invalid))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_transform_naming_suffix.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Plugin for validating naming conventions.\"\"\"\nfrom maya import cmds\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\nclass ValidateTransformNamingSuffix(pyblish.api.InstancePlugin,\n                                    OptionalPyblishPluginMixin):\n    \"\"\"Validates transform suffix based on the type of its children shapes.\n\n    Suffices must be:\n        - mesh:\n            _GEO (regular geometry)\n            _GES (geometry to be smoothed at render)\n            _GEP (proxy geometry; usually not to be rendered)\n            _OSD (open subdiv smooth at rendertime)\n        - nurbsCurve: _CRV\n        - nurbsSurface: _NRB\n        - locator: _LOC\n        - null/group: _GRP\n    Suffices can also be overridden by project settings.\n\n    .. warning::\n        This grabs the first child shape as a reference and doesn't use the\n        others in the check.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = ['maya']\n    families = ['model']\n    optional = True\n    label = 'Suffix Naming Conventions'\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    SUFFIX_NAMING_TABLE = {\"mesh\": [\"_GEO\", \"_GES\", \"_GEP\", \"_OSD\"],\n                           \"nurbsCurve\": [\"_CRV\"],\n                           \"nurbsSurface\": [\"_NRB\"],\n                           \"locator\": [\"_LOC\"],\n                           \"group\": [\"_GRP\"]}\n\n    ALLOW_IF_NOT_IN_SUFFIX_TABLE = True\n\n    @classmethod\n    def get_table_for_invalid(cls):\n        ss = []\n        for k, v in cls.SUFFIX_NAMING_TABLE.items():\n            ss.append(\" - <b>{}</b>: {}\".format(k, \", \".join(v)))\n        return \"<br>\".join(ss)\n\n    @staticmethod\n    def is_valid_name(node_name, shape_type,\n                      SUFFIX_NAMING_TABLE, ALLOW_IF_NOT_IN_SUFFIX_TABLE):\n        \"\"\"Return whether node's name is correct.\n\n        The correctness for a transform's suffix is dependent on what\n        `shape_type` it holds. E.g. a transform with a mesh might need and\n        `_GEO` suffix.\n\n        When `shape_type` is None the transform doesn't have any direct\n        children shapes.\n\n        Args:\n            node_name (str): Node name.\n            shape_type (str): Type of node.\n            SUFFIX_NAMING_TABLE (dict): Mapping dict for suffixes.\n            ALLOW_IF_NOT_IN_SUFFIX_TABLE (dict): Filter dict.\n\n        \"\"\"\n        if shape_type not in SUFFIX_NAMING_TABLE:\n            return ALLOW_IF_NOT_IN_SUFFIX_TABLE\n        else:\n            suffices = SUFFIX_NAMING_TABLE[shape_type]\n            for suffix in suffices:\n                if node_name.endswith(suffix):\n                    return True\n            return False\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Get invalid nodes in instance.\n\n        Args:\n            instance (:class:`pyblish.api.Instance`): published instance.\n\n        \"\"\"\n        transforms = cmds.ls(instance, type='transform', long=True)\n\n        invalid = []\n        for transform in transforms:\n            shapes = cmds.listRelatives(transform,\n                                        shapes=True,\n                                        fullPath=True,\n                                        noIntermediate=True)\n\n            shape_type = cmds.nodeType(shapes[0]) if shapes else \"group\"\n            if not cls.is_valid_name(transform, shape_type,\n                                     cls.SUFFIX_NAMING_TABLE,\n                                     cls.ALLOW_IF_NOT_IN_SUFFIX_TABLE):\n                invalid.append(transform)\n\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance.\n\n        Args:\n            instance (:class:`pyblish.api.Instance`): published instance.\n\n        \"\"\"\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            valid = self.get_table_for_invalid()\n\n            names = \"<br>\".join(\n                \" - {}\".format(node) for node in invalid\n            )\n            valid = valid.replace(\"\\n\", \"<br>\")\n\n            raise PublishValidationError(\n                title=\"Invalid naming suffix\",\n                message=\"Valid suffixes are:<br>{0}<br><br>\"\n                        \"Incorrectly named geometry transforms:<br>{1}\"\n                        \"\".format(valid, names))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_transform_zero.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateTransformZero(pyblish.api.Validator,\n                            OptionalPyblishPluginMixin):\n    \"\"\"Transforms can't have any values\n\n    To solve this issue, try freezing the transforms. So long\n    as the transforms, rotation and scale values are zero,\n    you're all good.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"model\"]\n    label = \"Transform Zero (Freeze)\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n\n    _identity = [1.0, 0.0, 0.0, 0.0,\n                 0.0, 1.0, 0.0, 0.0,\n                 0.0, 0.0, 1.0, 0.0,\n                 0.0, 0.0, 0.0, 1.0]\n    _tolerance = 1e-30\n    optional = True\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Returns the invalid transforms in the instance.\n\n        This is the same as checking:\n        - translate == [0, 0, 0] and rotate == [0, 0, 0] and\n          scale == [1, 1, 1] and shear == [0, 0, 0]\n\n        .. note::\n            This will also catch camera transforms if those\n            are in the instances.\n\n        Returns:\n            list: Transforms that are not identity matrix\n\n        \"\"\"\n\n        transforms = cmds.ls(instance, type=\"transform\")\n\n        invalid = []\n        for transform in transforms:\n            if ('_LOC' in transform) or ('_loc' in transform):\n                continue\n            mat = cmds.xform(transform, q=1, matrix=True, objectSpace=True)\n            if not all(abs(x-y) < cls._tolerance\n                       for x, y in zip(cls._identity, mat)):\n                invalid.append(transform)\n\n        return invalid\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance \"objectSet\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n\n            names = \"<br>\".join(\n                \" - {}\".format(node) for node in invalid\n            )\n\n            raise PublishValidationError(\n                title=\"Transform Zero\",\n                message=\"The model publish allows no transformations. You must\"\n                        \" <b>freeze transformations</b> to continue.<br><br>\"\n                        \"Nodes found with transform values: \"\n                        \"{0}\".format(names))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_unique_names.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateUniqueNames(pyblish.api.Validator,\n                          OptionalPyblishPluginMixin):\n    \"\"\"transform names should be unique\n\n    ie: using cmds.ls(someNodeName) should always return shortname\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"model\"]\n    label = \"Unique transform name\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = True\n\n    @staticmethod\n    def get_invalid(instance):\n        \"\"\"Returns the invalid transforms in the instance.\n\n        Returns:\n            list: Non-unique name transforms.\n\n        \"\"\"\n\n        return [tr for tr in cmds.ls(instance, type=\"transform\")\n                if '|' in tr]\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance \"objectSet\"\"\"\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise ValueError(\"Nodes found with none unique names. \"\n                             \"values: {0}\".format(invalid))\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py",
    "content": "# -*- coding: utf-8 -*-\n\nfrom maya import cmds\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateMeshOrder,\n    OptionalPyblishPluginMixin\n)\nimport openpype.hosts.maya.api.action\n\n\nclass ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin,\n                                     OptionalPyblishPluginMixin):\n    \"\"\"Validate if mesh is made of triangles for Unreal Engine\"\"\"\n\n    order = ValidateMeshOrder\n    hosts = [\"maya\"]\n    families = [\"staticMesh\"]\n    label = \"Mesh is Triangulated\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    active = False\n\n    @classmethod\n    def get_invalid(cls, instance):\n        invalid = []\n        meshes = cmds.ls(instance, type=\"mesh\", long=True)\n        for mesh in meshes:\n            faces = cmds.polyEvaluate(mesh, f=True)\n            tris = cmds.polyEvaluate(mesh, t=True)\n            if faces != tris:\n                invalid.append(mesh)\n\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        assert len(invalid) == 0, (\n            \"Found meshes without triangles\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validator for correct naming of Static Meshes.\"\"\"\nimport re\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline import legacy_io\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\nclass ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin,\n                                   OptionalPyblishPluginMixin):\n    \"\"\"Validate name of Unreal Static Mesh\n\n    Unreals naming convention states that staticMesh should start with `SM`\n    prefix - SM_[Name]_## (Eg. SM_sube_01).These prefixes can be configured\n    in Settings UI. This plugin also validates other types of\n    meshes - collision meshes:\n\n    UBX_[RenderMeshName]*:\n                             Boxes are created with the Box objects type in\n                             Max or with the Cube polygonal primitive in Maya.\n                             You cannot move the vertices around or deform it\n                             in any way to make it something other than a\n                             rectangular prism, or else it will not work.\n\n    UCP_[RenderMeshName]*:\n                             Capsules are created with the Capsule object type.\n                             The capsule does not need to have many segments\n                             (8 is a good number) at all because it is\n                             converted into a true capsule for collision. Like\n                             boxes, you should not move the individual\n                             vertices around.\n\n    USP_[RenderMeshName]*:\n                             Spheres are created with the Sphere object type.\n                             The sphere does not need to have many segments\n                             (8 is a good number) at all because it is\n                             converted into a true sphere for collision. Like\n                             boxes, you should not move the individual\n                             vertices around.\n\n    UCX_[RenderMeshName]*:\n                             Convex objects can be any completely closed\n                             convex 3D shape. For example, a box can also be\n                             a convex object\n\n    This validator also checks if collision mesh [RenderMeshName] matches one\n    of SM_[RenderMeshName].\n\n    \"\"\"\n    optional = True\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"staticMesh\"]\n    label = \"Unreal Static Mesh Name\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    regex_mesh = r\"(?P<renderName>.*))\"\n    regex_collision = r\"(?P<renderName>.*)\"\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        invalid = []\n\n        collision_prefixes = (\n            instance.context.data[\"project_settings\"]\n            [\"maya\"]\n            [\"create\"]\n            [\"CreateUnrealStaticMesh\"]\n            [\"collision_prefixes\"]\n        )\n\n        if cls.validate_mesh:\n            # compile regex for testing names\n            regex_mesh = \"{}{}\".format(\n                (\"_\" + cls.static_mesh_prefix) or \"\", cls.regex_mesh\n            )\n            sm_r = re.compile(regex_mesh)\n            if not sm_r.match(instance.data.get(\"subset\")):\n                cls.log.error(\"Mesh doesn't comply with name validation.\")\n                return True\n\n        if cls.validate_collision:\n            collision_set = instance.data.get(\"collisionMembers\", None)\n            # soft-fail is there are no collision objects\n            if not collision_set:\n                cls.log.warning(\"No collision objects to validate.\")\n                return False\n\n            regex_collision = \"{}{}_(\\\\d+)\".format(\n                \"(?P<prefix>({}))_\".format(\n                    \"|\".join(\"{0}\".format(p) for p in collision_prefixes)\n                ) or \"\", cls.regex_collision\n            )\n\n            cl_r = re.compile(regex_collision)\n\n            asset_name = instance.data[\"assetEntity\"][\"name\"]\n            mesh_name = \"{}{}\".format(asset_name,\n                                      instance.data.get(\"variant\", []))\n\n            for obj in collision_set:\n                cl_m = cl_r.match(obj)\n                if not cl_m:\n                    cls.log.error(\"{} is invalid\".format(obj))\n                    invalid.append(obj)\n                else:\n                    expected_collision = \"{}_{}\".format(\n                        cl_m.group(\"prefix\"),\n                        mesh_name\n                    )\n\n                    if not obj.startswith(expected_collision):\n\n                        cls.log.error(\n                            \"Collision object name doesn't match \"\n                            \"static mesh name\"\n                        )\n                        cls.log.error(\"{}_{} != {}_{}*\".format(\n                            cl_m.group(\"prefix\"),\n                            cl_m.group(\"renderName\"),\n                            cl_m.group(\"prefix\"),\n                            mesh_name,\n                        ))\n                        invalid.append(obj)\n\n        return invalid\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        if not self.validate_mesh and not self.validate_collision:\n            self.log.debug(\"Validation of both mesh and collision names\"\n                           \"is disabled.\")\n            return\n\n        if not instance.data.get(\"collisionMembers\", None):\n            self.log.debug(\"There are no collision objects to validate\")\n            return\n\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\"Model naming is invalid. See log.\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py",
    "content": "# -*- coding: utf-8 -*-\n\nfrom maya import cmds\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    RepairAction,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateUnrealUpAxis(pyblish.api.ContextPlugin,\n                           OptionalPyblishPluginMixin):\n    \"\"\"Validate if Z is set as up axis in Maya\"\"\"\n\n    optional = True\n    active = False\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"staticMesh\"]\n    label = \"Unreal Up-Axis check\"\n    actions = [RepairAction]\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            return\n\n        assert cmds.upAxis(q=True, axis=True) == \"z\", (\n            \"Invalid axis set as up axis\"\n        )\n\n    @classmethod\n    def repair(cls, instance):\n        cmds.upAxis(axis=\"z\", rotateView=True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_visible_only.py",
    "content": "import pyblish.api\n\nfrom openpype.hosts.maya.api.lib import iter_visible_nodes_in_range\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin,\n                                 OptionalPyblishPluginMixin):\n    \"\"\"Validates at least a single node is visible in frame range.\n\n    This validation only validates if the `visibleOnly` flag is enabled\n    on the instance - otherwise the validation is skipped.\n\n    \"\"\"\n    order = ValidateContentsOrder + 0.05\n    label = \"Alembic Visible Only\"\n    hosts = [\"maya\"]\n    families = [\"pointcache\", \"animation\"]\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        if not instance.data.get(\"visibleOnly\", False):\n            self.log.debug(\"Visible only is disabled. Validation skipped..\")\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            start, end = self.get_frame_range(instance)\n            raise PublishValidationError(\"No visible nodes found in \"\n                               \"frame range {}-{}.\".format(start, end))\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        if instance.data[\"family\"] == \"animation\":\n            # Special behavior to use the nodes in out_SET\n            nodes = instance.data[\"out_hierarchy\"]\n        else:\n            nodes = instance[:]\n\n        start, end = cls.get_frame_range(instance)\n        if not any(iter_visible_nodes_in_range(nodes, start, end)):\n            # Return the nodes we have considered so the user can identify\n            # them with the select invalid action\n            return nodes\n\n    @staticmethod\n    def get_frame_range(instance):\n        data = instance.data\n        return data[\"frameStartHandle\"], data[\"frameEndHandle\"]\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_vray.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nfrom openpype.pipeline.publish import PublishValidationError\n\n\nclass ValidateVray(pyblish.api.InstancePlugin):\n    \"\"\"Validate general Vray setup.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = 'VRay'\n    hosts = [\"maya\"]\n    families = [\"vrayproxy\"]\n\n    def process(self, instance):\n        # Validate vray plugin is loaded.\n        if not cmds.pluginInfo(\"vrayformaya\", query=True, loaded=True):\n            raise PublishValidationError(\"Vray plugin is not loaded.\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py",
    "content": "import pyblish.api\nfrom maya import cmds\n\nfrom openpype.hosts.maya.api import lib\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    RepairAction,\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateVRayDistributedRendering(pyblish.api.InstancePlugin,\n                                       OptionalPyblishPluginMixin):\n    \"\"\"Validate V-Ray Distributed Rendering is ignored in batch mode.\n\n    Whenever Distributed Rendering is enabled for V-Ray in the render settings\n    ensure that the \"Ignore in batch mode\" is enabled so the submitted job\n    won't try to render each frame with all machines resulting in faulty\n    errors.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"VRay Distributed Rendering\"\n    families = [\"renderlayer\"]\n    actions = [RepairAction]\n    optional = False\n\n    # V-Ray attribute names\n    enabled_attr = \"vraySettings.sys_distributed_rendering_on\"\n    ignored_attr = \"vraySettings.sys_distributed_rendering_ignore_batch\"\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        if instance.data.get(\"renderer\") != \"vray\":\n            # If not V-Ray ignore..\n            return\n\n        vray_settings = cmds.ls(\"vraySettings\", type=\"VRaySettingsNode\")\n        assert vray_settings, \"Please ensure a VRay Settings Node is present\"\n\n        renderlayer = instance.data['renderlayer']\n\n        if not lib.get_attr_in_layer(self.enabled_attr, layer=renderlayer):\n            # If not distributed rendering enabled, ignore..\n            return\n\n        # If distributed rendering is enabled but it is *not* set to ignore\n        # during batch mode we invalidate the instance\n        if not lib.get_attr_in_layer(self.ignored_attr, layer=renderlayer):\n            raise PublishValidationError(\n                (\"Renderlayer has distributed rendering enabled \"\n                 \"but is not set to ignore in batch mode.\"))\n\n    @classmethod\n    def repair(cls, instance):\n\n        renderlayer = instance.data.get(\"renderlayer\")\n        with lib.renderlayer(renderlayer):\n            cls.log.debug(\"Enabling Distributed Rendering \"\n                          \"ignore in batch mode..\")\n            cmds.setAttr(cls.ignored_attr, True)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validate if there are AOVs pulled from references.\"\"\"\nimport pyblish.api\nimport types\nfrom maya import cmds\n\nfrom openpype.pipeline.publish import (\n    RepairContextAction,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin,\n                                 OptionalPyblishPluginMixin):\n    \"\"\"Validate whether the V-Ray Render Elements (AOVs) include references.\n\n    This will check if there are AOVs pulled from references. If\n    `Vray Use Referenced Aovs` is checked on render instance, u must add those\n    manually to Render Elements as Pype will expect them to be rendered.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = 'VRay Referenced AOVs'\n    hosts = ['maya']\n    families = ['renderlayer']\n    actions = [RepairContextAction]\n    optional = False\n\n    def process(self, instance):\n        \"\"\"Plugin main entry point.\"\"\"\n        if not self.is_active(instance.data):\n            return\n        if instance.data.get(\"renderer\") != \"vray\":\n            # If not V-Ray ignore..\n            return\n\n        ref_aovs = cmds.ls(\n            type=[\"VRayRenderElement\", \"VRayRenderElementSet\"],\n            referencedNodes=True)\n        ref_aovs_enabled = ValidateVrayReferencedAOVs.maya_is_true(\n            cmds.getAttr(\"vraySettings.relements_usereferenced\"))\n\n        if not instance.data.get(\"vrayUseReferencedAovs\"):\n            if ref_aovs_enabled and ref_aovs:\n                self.log.warning((\n                    \"Referenced AOVs are enabled in Vray \"\n                    \"Render Settings and are detected in scene, but \"\n                    \"Pype render instance option for referenced AOVs is \"\n                    \"disabled. Those AOVs will be rendered but not published \"\n                    \"by Pype.\"\n                ))\n                self.log.warning(\", \".join(ref_aovs))\n        else:\n            if not ref_aovs:\n                self.log.warning((\n                    \"Use of referenced AOVs enabled but there are none \"\n                    \"in the scene.\"\n                ))\n            if not ref_aovs_enabled:\n                self.log.error((\n                    \"'Use referenced' not enabled in Vray Render Settings.\"\n                ))\n                raise AssertionError(\"Invalid render settings\")\n\n    @classmethod\n    def repair(cls, context):\n        \"\"\"Repair action.\"\"\"\n        vray_settings = cmds.ls(type=\"VRaySettingsNode\")\n        if not vray_settings:\n            node = cmds.createNode(\"VRaySettingsNode\")\n        else:\n            node = vray_settings[0]\n\n        cmds.setAttr(\"{}.relements_usereferenced\".format(node), True)\n\n    @staticmethod\n    def maya_is_true(attr_val):\n        \"\"\"Whether a Maya attr evaluates to True.\n\n        When querying an attribute value from an ambiguous object the\n        Maya API will return a list of values, which need to be properly\n        handled to evaluate properly.\n\n        Args:\n            attr_val (mixed): Maya attribute to be evaluated as bool.\n\n        Returns:\n            bool: cast Maya attribute to Pythons boolean value.\n\n        \"\"\"\n        if isinstance(attr_val, bool):\n            return attr_val\n        elif isinstance(attr_val, (list, types.GeneratorType)):\n            return any(attr_val)\n        else:\n            return bool(attr_val)\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_vray_translator_settings.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validate VRay Translator settings.\"\"\"\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    context_plugin_should_run,\n    RepairContextAction,\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\nfrom maya import cmds\n\n\nclass ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin,\n                                    OptionalPyblishPluginMixin):\n    \"\"\"Validate VRay Translator settings for extracting vrscenes.\"\"\"\n\n    order = ValidateContentsOrder\n    label = \"VRay Translator Settings\"\n    families = [\"vrayscene_layer\"]\n    actions = [RepairContextAction]\n    optional = False\n\n    def process(self, context):\n        \"\"\"Plugin entry point.\"\"\"\n        if not self.is_active(context.data):\n            return\n        # Workaround bug pyblish-base#250\n        if not context_plugin_should_run(self, context):\n            return\n\n        invalid = self.get_invalid(context)\n        if invalid:\n            raise PublishValidationError(\n                message=\"Found invalid VRay Translator settings\",\n                title=self.label\n            )\n\n    @classmethod\n    def get_invalid(cls, context):\n        \"\"\"Get invalid instances.\"\"\"\n        invalid = False\n\n        # Get vraySettings node\n        vray_settings = cmds.ls(type=\"VRaySettingsNode\")\n        if not vray_settings:\n            raise PublishValidationError(\n                \"Please ensure a VRay Settings Node is present\",\n                title=cls.label\n            )\n\n        node = vray_settings[0]\n\n        if cmds.setAttr(\"{}.vrscene_render_on\".format(node)):\n            cls.log.error(\n                \"Render is enabled, for export it should be disabled\")\n            invalid = True\n\n        if not cmds.getAttr(\"{}.vrscene_on\".format(node)):\n            cls.log.error(\"Export vrscene not enabled\")\n            invalid = True\n\n        for instance in context:\n            if \"vrayscene_layer\" not in instance.data.get(\"families\"):\n                continue\n\n            if instance.data.get(\"vraySceneMultipleFiles\"):\n                if not cmds.getAttr(\"{}.misc_eachFrameInFile\".format(node)):\n                    cls.log.error(\"Each Frame in File not enabled\")\n                    invalid = True\n            else:\n                if cmds.getAttr(\"{}.misc_eachFrameInFile\".format(node)):\n                    cls.log.error(\"Each Frame in File is enabled\")\n                    invalid = True\n\n        vrscene_filename = cmds.getAttr(\"{}.vrscene_filename\".format(node))\n        if vrscene_filename != \"vrayscene/<Scene>/<Layer>/<Layer>\":\n            cls.log.error(\"Template for file name is wrong\")\n            invalid = True\n\n        return invalid\n\n    @classmethod\n    def repair(cls, context):\n        \"\"\"Repair invalid settings.\"\"\"\n        vray_settings = cmds.ls(type=\"VRaySettingsNode\")\n        if not vray_settings:\n            node = cmds.createNode(\"VRaySettingsNode\")\n        else:\n            node = vray_settings[0]\n\n        cmds.setAttr(\"{}.vrscene_render_on\".format(node), False)\n        cmds.setAttr(\"{}.vrscene_on\".format(node), True)\n        for instance in context:\n            if \"vrayscene\" not in instance.data.get(\"families\"):\n                continue\n\n            if instance.data.get(\"vraySceneMultipleFiles\"):\n                cmds.setAttr(\"{}.misc_eachFrameInFile\".format(node), True)\n            else:\n                cmds.setAttr(\"{}.misc_eachFrameInFile\".format(node), False)\n        cmds.setAttr(\"{}.vrscene_filename\".format(node),\n                     \"vrayscene/<Scene>/<Layer>/<Layer>\",\n                     type=\"string\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_vrayproxy.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import KnownPublishError\nfrom openpype.pipeline.publish import OptionalPyblishPluginMixin\n\n\nclass ValidateVrayProxy(pyblish.api.InstancePlugin,\n                        OptionalPyblishPluginMixin):\n\n    order = pyblish.api.ValidatorOrder\n    label = \"VRay Proxy Settings\"\n    hosts = [\"maya\"]\n    families = [\"vrayproxy\"]\n    optional = False\n\n    def process(self, instance):\n        data = instance.data\n        if not self.is_active(data):\n            return\n        if not data[\"setMembers\"]:\n            raise KnownPublishError(\n                \"'%s' is empty! This is a bug\" % instance.name\n            )\n\n        if data[\"animation\"]:\n            if data[\"frameEnd\"] < data[\"frameStart\"]:\n                raise KnownPublishError(\n                    \"End frame is smaller than start frame\"\n                )\n\n        if not data[\"vrmesh\"] and not data[\"alembic\"]:\n            raise KnownPublishError(\n                \"Both vrmesh and alembic are off. Needs at least one to\"\n                \" publish.\"\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_vrayproxy_members.py",
    "content": "import pyblish.api\n\nfrom maya import cmds\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateVrayProxyMembers(pyblish.api.InstancePlugin,\n                               OptionalPyblishPluginMixin):\n    \"\"\"Validate whether the V-Ray Proxy instance has shape members\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = 'VRay Proxy Members'\n    hosts = ['maya']\n    families = ['vrayproxy']\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n\n        if invalid:\n            raise PublishValidationError(\"'%s' is invalid VRay Proxy for \"\n                               \"export!\" % instance.name)\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        shapes = cmds.ls(instance,\n                         shapes=True,\n                         noIntermediate=True,\n                         long=True)\n\n        if not shapes:\n            cls.log.error(\"'%s' contains no shapes.\" % instance.name)\n\n            # Return the instance itself\n            return [instance.name]\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_xgen.py",
    "content": "import json\n\nimport maya.cmds as cmds\nimport xgenm\n\nimport pyblish.api\nfrom openpype.pipeline.publish import PublishValidationError\n\n\nclass ValidateXgen(pyblish.api.InstancePlugin):\n    \"\"\"Validate Xgen data.\"\"\"\n\n    label = \"Validate Xgen\"\n    order = pyblish.api.ValidatorOrder\n    host = [\"maya\"]\n    families = [\"xgen\"]\n\n    def process(self, instance):\n        set_members = instance.data.get(\"setMembers\")\n\n        # Only 1 collection/node per instance.\n        if len(set_members) != 1:\n            raise PublishValidationError(\n                \"Only one collection per instance is allowed.\"\n                \" Found:\\n{}\".format(set_members)\n            )\n\n        # Only xgen palette node is allowed.\n        node_type = cmds.nodeType(set_members[0])\n        if node_type != \"xgmPalette\":\n            raise PublishValidationError(\n                \"Only node of type \\\"xgmPalette\\\" are allowed. Referred to as\"\n                \" \\\"collection\\\" in the Maya UI.\"\n                \" Node type found: {}\".format(node_type)\n            )\n\n        # Cant have inactive modifiers in collection cause Xgen will try and\n        # look for them when loading.\n        palette = instance.data[\"xgmPalette\"].replace(\"|\", \"\")\n        inactive_modifiers = {}\n        for description in instance.data[\"xgmDescriptions\"]:\n            description = description.split(\"|\")[-2]\n            modifier_names = xgenm.fxModules(palette, description)\n            for name in modifier_names:\n                attr = xgenm.getAttr(\"active\", palette, description, name)\n                # Attribute value are lowercase strings of false/true.\n                if attr == \"false\":\n                    try:\n                        inactive_modifiers[description].append(name)\n                    except KeyError:\n                        inactive_modifiers[description] = [name]\n\n        if inactive_modifiers:\n            raise PublishValidationError(\n                \"There are inactive modifiers on the collection. \"\n                \"Please delete these:\\n{}\".format(\n                    json.dumps(inactive_modifiers, indent=4, sort_keys=True)\n                )\n            )\n\n        # We need a namespace else there will be a naming conflict when\n        # extracting because of stripping namespaces and parenting to world.\n        node_names = [instance.data[\"xgmPalette\"]]\n        node_names.extend(instance.data[\"xgenConnections\"])\n        non_namespaced_nodes = [n for n in node_names if \":\" not in n]\n        if non_namespaced_nodes:\n            raise PublishValidationError(\n                \"Could not find namespace on {}. Namespace is required for\"\n                \" xgen publishing.\".format(non_namespaced_nodes)\n            )\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py",
    "content": "from maya import cmds\n\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin,\n                                        OptionalPyblishPluginMixin):\n    \"\"\"Check if the render script callbacks will be used during the rendering\n\n    In order to ensure the render tasks are executed properly we need to check\n    if the pre and post render callbacks are actually used.\n\n    For example:\n        Yeti is not loaded but its callback scripts are still set in the\n        render settings. This will cause an error because Maya tries to find\n        and execute the callbacks.\n\n    Developer note:\n         The pre and post render callbacks cannot be overridden\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Yeti Render Script Callbacks\"\n    hosts = [\"maya\"]\n    families = [\"renderlayer\"]\n    optional = False\n\n    # Settings per renderer\n    callbacks = {\n        \"vray\": {\n            \"pre\": \"catch(`pgYetiVRayPreRender`)\",\n            \"post\": \"catch(`pgYetiVRayPostRender`)\"\n        },\n        \"arnold\": {\n            \"pre\": \"pgYetiPreRender\"\n        }\n    }\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise ValueError(\"Invalid render callbacks found for '%s'!\"\n                             % instance.name)\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        yeti_loaded = cmds.pluginInfo(\"pgYetiMaya\", query=True, loaded=True)\n\n        if not yeti_loaded and not cmds.ls(type=\"pgYetiMaya\"):\n            # The yeti plug-in is available and loaded so at\n            # this point we don't really care whether the scene\n            # has any yeti callback set or not since if the callback\n            # is there it wouldn't error and if it weren't then\n            # nothing happens because there are no yeti nodes.\n            cls.log.debug(\n                \"Yeti is loaded but no yeti nodes were found. \"\n                \"Callback validation skipped..\"\n            )\n            return False\n\n        renderer = instance.data[\"renderer\"]\n        if renderer == \"redshift\":\n            cls.log.debug(\"Redshift ignores any pre and post render callbacks\")\n            return False\n\n        callback_lookup = cls.callbacks.get(renderer, {})\n        if not callback_lookup:\n            cls.log.warning(\"Renderer '%s' is not supported in this plugin\"\n                            % renderer)\n            return False\n\n        pre_mel = cmds.getAttr(\"defaultRenderGlobals.preMel\") or \"\"\n        post_mel = cmds.getAttr(\"defaultRenderGlobals.postMel\") or \"\"\n\n        if pre_mel.strip():\n            cls.log.debug(\"Found pre mel: `%s`\" % pre_mel)\n\n        if post_mel.strip():\n            cls.log.debug(\"Found post mel: `%s`\" % post_mel)\n\n        # Strip callbacks and turn into a set for quick lookup\n        pre_callbacks = {cmd.strip() for cmd in pre_mel.split(\";\")}\n        post_callbacks = {cmd.strip() for cmd in post_mel.split(\";\")}\n\n        pre_script = callback_lookup.get(\"pre\", \"\")\n        post_script = callback_lookup.get(\"post\", \"\")\n\n        # If Yeti is not loaded\n        invalid = False\n        if not yeti_loaded:\n            if pre_script and pre_script in pre_callbacks:\n                cls.log.error(\"Found pre render callback '%s' which is not \"\n                              \"uses!\" % pre_script)\n                invalid = True\n\n            if post_script and post_script in post_callbacks:\n                cls.log.error(\"Found post render callback '%s which is \"\n                              \"not used!\" % post_script)\n                invalid = True\n\n        # If Yeti is loaded\n        else:\n            if pre_script and pre_script not in pre_callbacks:\n                cls.log.error(\n                    \"Could not find required pre render callback \"\n                    \"`%s`\" % pre_script)\n                invalid = True\n\n            if post_script and post_script not in post_callbacks:\n                cls.log.error(\n                    \"Could not find required post render callback\"\n                    \" `%s`\" % post_script)\n                invalid = True\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py",
    "content": "import pyblish.api\nimport maya.cmds as cmds\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateYetiRigCacheState(pyblish.api.InstancePlugin,\n                                OptionalPyblishPluginMixin):\n    \"\"\"Validate the I/O attributes of the node\n\n    Every pgYetiMaya cache node per instance should have:\n        1. Input Mode is set to `None`\n        2. Input Cache File Name is empty\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Yeti Rig Cache State\"\n    hosts = [\"maya\"]\n    families = [\"yetiRig\"]\n    actions = [RepairAction,\n               openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\"Nodes have incorrect I/O settings\")\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        invalid = []\n\n        yeti_nodes = cmds.ls(instance, type=\"pgYetiMaya\")\n        for node in yeti_nodes:\n            # Check reading state\n            state = cmds.getAttr(\"%s.fileMode\" % node)\n            if state == 1:\n                cls.log.error(\"Node `%s` is set to mode `cache`\" % node)\n                invalid.append(node)\n                continue\n\n            # Check reading state\n            has_cache = cmds.getAttr(\"%s.cacheFileName\" % node)\n            if has_cache:\n                cls.log.error(\"Node `%s` has a cache file set\" % node)\n                invalid.append(node)\n                continue\n\n        return invalid\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"Repair all errors\"\"\"\n\n        # Create set to ensure all nodes only pass once\n        invalid = cls.get_invalid(instance)\n        for node in invalid:\n            cmds.setAttr(\"%s.fileMode\" % node, 0)\n            cmds.setAttr(\"%s.cacheFileName\" % node, \"\", type=\"string\")\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py",
    "content": "from maya import cmds\n\nimport pyblish.api\n\nimport openpype.hosts.maya.api.action\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateYetiRigInputShapesInInstance(pyblish.api.Validator,\n                                           OptionalPyblishPluginMixin):\n    \"\"\"Validate if all input nodes are part of the instance's hierarchy\"\"\"\n\n    order = ValidateContentsOrder\n    hosts = [\"maya\"]\n    families = [\"yetiRig\"]\n    label = \"Yeti Rig Input Shapes In Instance\"\n    actions = [openpype.hosts.maya.api.action.SelectInvalidAction]\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\"Yeti Rig has invalid input meshes\")\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        input_set = next((i for i in instance if i == \"input_SET\"), None)\n        assert input_set, \"Current %s instance has no `input_SET`\" % instance\n\n        # Get all children, we do not care about intermediates\n        input_nodes = cmds.ls(cmds.sets(input_set, query=True), long=True)\n        dag = cmds.ls(input_nodes, dag=True, long=True)\n        shapes = cmds.ls(dag, long=True, shapes=True, noIntermediate=True)\n\n        # Allow publish without input meshes.\n        if not shapes:\n            cls.log.debug(\"Found no input meshes for %s, skipping ...\"\n                          % instance)\n            return []\n\n        # check if input node is part of groomRig instance\n        instance_lookup = set(instance[:])\n        invalid = [s for s in shapes if s not in instance_lookup]\n\n        return invalid\n"
  },
  {
    "path": "openpype/hosts/maya/plugins/publish/validate_yeti_rig_settings.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import (\n    PublishValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ValidateYetiRigSettings(pyblish.api.InstancePlugin,\n                              OptionalPyblishPluginMixin):\n    \"\"\"Validate Yeti Rig Settings have collected input connections.\n\n    The input connections are collected for the nodes in the `input_SET`.\n    When no input connections are found a warning is logged but it is allowed\n    to pass validation.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Yeti Rig Settings\"\n    families = [\"yetiRig\"]\n    optional = False\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishValidationError(\n                (\"Detected invalid Yeti Rig data. (See log) \"\n                 \"Tip: Save the scene\"))\n\n    @classmethod\n    def get_invalid(cls, instance):\n\n        rigsettings = instance.data.get(\"rigsettings\", None)\n        if rigsettings is None:\n            cls.log.error(\"MAJOR ERROR: No rig settings found!\")\n            return True\n\n        # Get inputs\n        inputs = rigsettings.get(\"inputs\", [])\n        if not inputs:\n            # Empty rig settings dictionary\n            cls.log.warning(\"No rig inputs found. This can happen when \"\n                            \"the rig has no inputs from outside the rig.\")\n            return False\n\n        for input in inputs:\n            source_id = input[\"sourceID\"]\n            if source_id is None:\n                cls.log.error(\"Discovered source with 'None' as ID, please \"\n                              \"check if the input shape has a cbId\")\n                return True\n\n            destination_id = input[\"destinationID\"]\n            if destination_id is None:\n                cls.log.error(\"Discovered None as destination ID value\")\n                return True\n\n        return False\n"
  },
  {
    "path": "openpype/hosts/maya/startup/userSetup.py",
    "content": "import os\n\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import install_host, get_current_project_name\nfrom openpype.hosts.maya.api import MayaHost\n\nfrom maya import cmds\n\n\nhost = MayaHost()\ninstall_host(host)\n\nprint(\"Starting OpenPype usersetup...\")\n\nproject_name = get_current_project_name()\nsettings = get_project_settings(project_name)\n\n# Loading plugins explicitly.\nexplicit_plugins_loading = settings[\"maya\"][\"explicit_plugins_loading\"]\nif explicit_plugins_loading[\"enabled\"]:\n    def _explicit_load_plugins():\n        for plugin in explicit_plugins_loading[\"plugins_to_load\"]:\n            if plugin[\"enabled\"]:\n                print(\"Loading plug-in: \" + plugin[\"name\"])\n                try:\n                    cmds.loadPlugin(plugin[\"name\"], quiet=True)\n                except RuntimeError as e:\n                    print(e)\n\n    # We need to load plugins deferred as loading them directly does not work\n    # correctly due to Maya's initialization.\n    cmds.evalDeferred(\n        _explicit_load_plugins,\n        lowestPriority=True\n    )\n\n# Open Workfile Post Initialization.\nkey = \"OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION\"\nif bool(int(os.environ.get(key, \"0\"))):\n    def _log_and_open():\n        path = os.environ[\"AVALON_LAST_WORKFILE\"]\n        print(\"Opening \\\"{}\\\"\".format(path))\n        cmds.file(path, open=True, force=True)\n    cmds.evalDeferred(\n        _log_and_open,\n        lowestPriority=True\n    )\n\n\nprint(\"Finished OpenPype usersetup.\")\n"
  },
  {
    "path": "openpype/hosts/maya/tools/__init__.py",
    "content": "from openpype.tools.utils.host_tools import qt_app_context\n\n\nclass MayaToolsSingleton:\n    _look_assigner = None\n\n\ndef get_look_assigner_tool(parent):\n    \"\"\"Create, cache and return look assigner tool window.\"\"\"\n    if MayaToolsSingleton._look_assigner is None:\n        from .mayalookassigner import MayaLookAssignerWindow\n        mayalookassigner_window = MayaLookAssignerWindow(parent)\n        MayaToolsSingleton._look_assigner = mayalookassigner_window\n    return MayaToolsSingleton._look_assigner\n\n\ndef show_look_assigner(parent=None):\n    \"\"\"Look manager is Maya specific tool for look management.\"\"\"\n\n    with qt_app_context():\n        look_assigner_tool = get_look_assigner_tool(parent)\n        look_assigner_tool.show()\n\n        # Pull window to the front.\n        look_assigner_tool.raise_()\n        look_assigner_tool.activateWindow()\n        look_assigner_tool.showNormal()\n"
  },
  {
    "path": "openpype/hosts/maya/tools/mayalookassigner/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Colorbleed\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "openpype/hosts/maya/tools/mayalookassigner/__init__.py",
    "content": "from .app import (\n    MayaLookAssignerWindow,\n    show\n)\n\n\n__all__ = [\n    \"MayaLookAssignerWindow\",\n    \"show\"]\n"
  },
  {
    "path": "openpype/hosts/maya/tools/mayalookassigner/alembic.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tools for loading looks to vray proxies.\"\"\"\nimport os\nfrom collections import defaultdict\nimport logging\n\nimport six\n\nimport alembic.Abc\n\n\nlog = logging.getLogger(__name__)\n\n\ndef get_alembic_paths_by_property(filename, attr, verbose=False):\n    # type: (str, str, bool) -> dict\n    \"\"\"Return attribute value per objects in the Alembic file.\n\n    Reads an Alembic archive hierarchy and retrieves the\n    value from the `attr` properties on the objects.\n\n    Args:\n        filename (str): Full path to Alembic archive to read.\n        attr (str): Id attribute.\n        verbose (bool): Whether to verbosely log missing attributes.\n\n    Returns:\n        dict: Mapping of node full path with its id\n\n    \"\"\"\n    # Normalize alembic path\n    filename = os.path.normpath(filename)\n    filename = filename.replace(\"\\\\\", \"/\")\n    filename = str(filename)  # path must be string\n\n    try:\n        archive = alembic.Abc.IArchive(filename)\n    except RuntimeError:\n        # invalid alembic file - probably vrmesh\n        log.warning(\"{} is not an alembic file\".format(filename))\n        return {}\n    root = archive.getTop()\n\n    iterator = list(root.children)\n    obj_ids = {}\n\n    for obj in iterator:\n        name = obj.getFullName()\n\n        # include children for coming iterations\n        iterator.extend(obj.children)\n\n        props = obj.getProperties()\n        if props.getNumProperties() == 0:\n            # Skip those without properties, e.g. '/materials' in a gpuCache\n            continue\n\n        # THe custom attribute is under the properties' first container under\n        # the \".arbGeomParams\"\n        prop = props.getProperty(0)  # get base property\n\n        _property = None\n        try:\n            geo_params = prop.getProperty('.arbGeomParams')\n            _property = geo_params.getProperty(attr)\n        except KeyError:\n            if verbose:\n                log.debug(\"Missing attr on: {0}\".format(name))\n            continue\n\n        if not _property.isConstant():\n            log.warning(\"Id not constant on: {0}\".format(name))\n\n        # Get first value sample\n        value = _property.getValue()[0]\n\n        obj_ids[name] = value\n\n    return obj_ids\n\n\ndef get_alembic_ids_cache(path):\n    # type: (str) -> dict\n    \"\"\"Build a id to node mapping in Alembic file.\n\n    Nodes without IDs are ignored.\n\n    Returns:\n        dict: Mapping of id to nodes in the Alembic.\n\n    \"\"\"\n    node_ids = get_alembic_paths_by_property(path, attr=\"cbId\")\n    id_nodes = defaultdict(list)\n    for node, _id in six.iteritems(node_ids):\n        id_nodes[_id].append(node)\n\n    return dict(six.iteritems(id_nodes))\n"
  },
  {
    "path": "openpype/hosts/maya/tools/mayalookassigner/app.py",
    "content": "import sys\nimport time\nimport logging\n\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype import style\nfrom openpype.client import get_last_version_by_subset_id\nfrom openpype.pipeline import get_current_project_name\nfrom openpype.tools.utils.lib import qt_app_context\nfrom openpype.hosts.maya.api.lib import (\n    assign_look_by_version,\n    get_main_window\n)\n\nfrom maya import cmds\n# old api for MFileIO\nimport maya.OpenMaya\nimport maya.api.OpenMaya as om\n\nfrom .widgets import (\n    AssetOutliner,\n    LookOutliner\n)\nfrom .commands import (\n    get_workfile,\n    remove_unused_looks\n)\nfrom .vray_proxies import vrayproxy_assign_look\nfrom . import arnold_standin\n\nmodule = sys.modules[__name__]\nmodule.window = None\n\n\nclass MayaLookAssignerWindow(QtWidgets.QWidget):\n\n    def __init__(self, parent=None):\n        super(MayaLookAssignerWindow, self).__init__(parent=parent)\n\n        self.log = logging.getLogger(__name__)\n\n        # Store callback references\n        self._callbacks = []\n        self._connections_set_up = False\n\n        filename = get_workfile()\n\n        self.setObjectName(\"lookManager\")\n        self.setWindowTitle(\"Look Manager 1.4.0 - [{}]\".format(filename))\n        self.setWindowFlags(QtCore.Qt.Window)\n        self.setParent(parent)\n\n        self.resize(750, 500)\n\n        self.setup_ui()\n\n        # Force refresh check on initialization\n        self._on_renderlayer_switch()\n\n    def setup_ui(self):\n        \"\"\"Build the UI\"\"\"\n\n        main_splitter = QtWidgets.QSplitter(self)\n\n        # Assets (left)\n        asset_outliner = AssetOutliner(main_splitter)\n\n        # Looks (right)\n        looks_widget = QtWidgets.QWidget(main_splitter)\n\n        look_outliner = LookOutliner(looks_widget)  # Database look overview\n\n        assign_selected = QtWidgets.QCheckBox(\n            \"Assign to selected only\", looks_widget\n        )\n        assign_selected.setToolTip(\"Whether to assign only to selected nodes \"\n                                   \"or to the full asset\")\n        remove_unused_btn = QtWidgets.QPushButton(\n            \"Remove Unused Looks\", looks_widget\n        )\n\n        looks_layout = QtWidgets.QVBoxLayout(looks_widget)\n        looks_layout.addWidget(look_outliner)\n        looks_layout.addWidget(assign_selected)\n        looks_layout.addWidget(remove_unused_btn)\n\n        main_splitter.addWidget(asset_outliner)\n        main_splitter.addWidget(looks_widget)\n        main_splitter.setSizes([350, 200])\n\n        # Footer\n        status = QtWidgets.QStatusBar(self)\n        status.setSizeGripEnabled(False)\n        status.setFixedHeight(25)\n        warn_layer = QtWidgets.QLabel(\n            \"Current Layer is not defaultRenderLayer\", self\n        )\n        warn_layer.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)\n        warn_layer.setStyleSheet(\"color: #DD5555; font-weight: bold;\")\n        warn_layer.setFixedHeight(25)\n\n        footer = QtWidgets.QHBoxLayout()\n        footer.setContentsMargins(0, 0, 0, 0)\n        footer.addWidget(status)\n        footer.addWidget(warn_layer)\n\n        # Build up widgets\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setSpacing(0)\n        main_layout.addWidget(main_splitter)\n        main_layout.addLayout(footer)\n\n        # Set column width\n        asset_outliner.view.setColumnWidth(0, 200)\n        look_outliner.view.setColumnWidth(0, 150)\n\n        asset_outliner.selection_changed.connect(\n            self.on_asset_selection_changed)\n\n        asset_outliner.refreshed.connect(\n            lambda: self.echo(\"Loaded assets..\")\n        )\n\n        look_outliner.menu_apply_action.connect(self.on_process_selected)\n        remove_unused_btn.clicked.connect(remove_unused_looks)\n\n        # Open widgets\n        self.asset_outliner = asset_outliner\n        self.look_outliner = look_outliner\n        self.status = status\n        self.warn_layer = warn_layer\n\n        # Buttons\n        self.remove_unused = remove_unused_btn\n        self.assign_selected = assign_selected\n\n        self._first_show = True\n\n    def setup_connections(self):\n        \"\"\"Connect interactive widgets with actions\"\"\"\n        if self._connections_set_up:\n            return\n\n        # Maya renderlayer switch callback\n        callback = om.MEventMessage.addEventCallback(\n            \"renderLayerManagerChange\",\n            self._on_renderlayer_switch\n        )\n        self._callbacks.append(callback)\n        self._connections_set_up = True\n\n    def remove_connection(self):\n        # Delete callbacks\n        for callback in self._callbacks:\n            om.MMessage.removeCallback(callback)\n\n        self._callbacks = []\n        self._connections_set_up = False\n\n    def showEvent(self, event):\n        self.setup_connections()\n        super(MayaLookAssignerWindow, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self.setStyleSheet(style.load_stylesheet())\n\n    def closeEvent(self, event):\n        self.remove_connection()\n        super(MayaLookAssignerWindow, self).closeEvent(event)\n\n    def _on_renderlayer_switch(self, *args):\n        \"\"\"Callback that updates on Maya renderlayer switch\"\"\"\n\n        if maya.OpenMaya.MFileIO.isNewingFile():\n            # Don't perform a check during file open or file new as\n            # the renderlayers will not be in a valid state yet.\n            return\n\n        layer = cmds.editRenderLayerGlobals(query=True,\n                                            currentRenderLayer=True)\n        if layer != \"defaultRenderLayer\":\n            self.warn_layer.show()\n        else:\n            self.warn_layer.hide()\n\n    def echo(self, message):\n        self.status.showMessage(message, 1500)\n\n    def refresh(self):\n        \"\"\"Refresh the content\"\"\"\n\n        # Get all containers and information\n        self.asset_outliner.clear()\n        found_items = self.asset_outliner.get_all_assets()\n        if not found_items:\n            self.look_outliner.clear()\n\n    def on_asset_selection_changed(self):\n        \"\"\"Get selected items from asset loader and fill look outliner\"\"\"\n\n        items = self.asset_outliner.get_selected_items()\n        self.look_outliner.clear()\n        self.look_outliner.add_items(items)\n\n    def on_process_selected(self):\n        \"\"\"Process all selected looks for the selected assets\"\"\"\n\n        assets = self.asset_outliner.get_selected_items()\n        assert assets, \"No asset selected\"\n\n        # Collect the looks we want to apply (by name)\n        look_items = self.look_outliner.get_selected_items()\n        looks = {look[\"subset\"] for look in look_items}\n\n        selection = self.assign_selected.isChecked()\n        asset_nodes = self.asset_outliner.get_nodes(selection=selection)\n\n        project_name = get_current_project_name()\n        start = time.time()\n        for i, (asset, item) in enumerate(asset_nodes.items()):\n\n            # Label prefix\n            prefix = \"({}/{})\".format(i + 1, len(asset_nodes))\n\n            # Assign the first matching look relevant for this asset\n            # (since assigning multiple to the same nodes makes no sense)\n            assign_look = next((subset for subset in item[\"looks\"]\n                                if subset[\"name\"] in looks), None)\n            if not assign_look:\n                self.echo(\n                    \"{} No matching selected look for {}\".format(prefix, asset)\n                )\n                continue\n\n            # Get the latest version of this asset's look subset\n            version = get_last_version_by_subset_id(\n                project_name, assign_look[\"_id\"], fields=[\"_id\"]\n            )\n\n            subset_name = assign_look[\"name\"]\n            self.echo(\"{} Assigning {} to {}\\t\".format(\n                prefix, subset_name, asset\n            ))\n            nodes = item[\"nodes\"]\n\n            # Assign Vray Proxy look.\n            if cmds.pluginInfo('vrayformaya', query=True, loaded=True):\n                self.echo(\"Getting vray proxy nodes ...\")\n                vray_proxies = set(cmds.ls(type=\"VRayProxy\", long=True))\n\n                for vp in vray_proxies:\n                    if vp in nodes:\n                        vrayproxy_assign_look(vp, subset_name)\n\n                nodes = list(set(nodes).difference(vray_proxies))\n            else:\n                self.echo(\n                    \"Could not assign to VRayProxy because vrayformaya plugin \"\n                    \"is not loaded.\"\n                )\n\n            # Assign Arnold Standin look.\n            if cmds.pluginInfo(\"mtoa\", query=True, loaded=True):\n                arnold_standins = set(cmds.ls(type=\"aiStandIn\", long=True))\n\n                for standin in arnold_standins:\n                    if standin in nodes:\n                        arnold_standin.assign_look(standin, subset_name)\n\n                nodes = list(set(nodes).difference(arnold_standins))\n            else:\n                self.echo(\n                    \"Could not assign to aiStandIn because mtoa plugin is not \"\n                    \"loaded.\"\n                )\n\n            # Assign look\n            if nodes:\n                assign_look_by_version(nodes, version_id=version[\"_id\"])\n\n        end = time.time()\n\n        self.echo(\"Finished assigning.. ({0:.3f}s)\".format(end - start))\n\n\ndef show():\n    \"\"\"Display Loader GUI\n\n    Arguments:\n        debug (bool, optional): Run loader in debug-mode,\n            defaults to False\n\n    \"\"\"\n\n    try:\n        module.window.close()\n        del module.window\n    except (RuntimeError, AttributeError):\n        pass\n\n    # Get Maya main window\n    mainwindow = get_main_window()\n\n    with qt_app_context():\n        window = MayaLookAssignerWindow(parent=mainwindow)\n        window.show()\n\n        module.window = window\n"
  },
  {
    "path": "openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py",
    "content": "import os\nimport json\nfrom collections import defaultdict\nimport logging\n\nfrom maya import cmds\n\nfrom openpype.pipeline import legacy_io\nfrom openpype.client import get_last_version_by_subset_name\nfrom openpype.hosts.maya import api\nfrom . import lib\nfrom .alembic import get_alembic_ids_cache\nfrom .usd import is_usd_lib_supported, get_usd_ids_cache\n\n\nlog = logging.getLogger(__name__)\n\n\nATTRIBUTE_MAPPING = {\n    \"primaryVisibility\": \"visibility\",  # Camera\n    \"castsShadows\": \"visibility\",  # Shadow\n    \"receiveShadows\": \"receive_shadows\",\n    \"aiSelfShadows\": \"self_shadows\",\n    \"aiOpaque\": \"opaque\",\n    \"aiMatte\": \"matte\",\n    \"aiVisibleInDiffuseTransmission\": \"visibility\",\n    \"aiVisibleInSpecularTransmission\": \"visibility\",\n    \"aiVisibleInVolume\": \"visibility\",\n    \"aiVisibleInDiffuseReflection\": \"visibility\",\n    \"aiVisibleInSpecularReflection\": \"visibility\",\n    \"aiSubdivUvSmoothing\": \"subdiv_uv_smoothing\",\n    \"aiDispHeight\": \"disp_height\",\n    \"aiDispPadding\": \"disp_padding\",\n    \"aiDispZeroValue\": \"disp_zero_value\",\n    \"aiStepSize\": \"step_size\",\n    \"aiVolumePadding\": \"volume_padding\",\n    \"aiSubdivType\": \"subdiv_type\",\n    \"aiSubdivIterations\": \"subdiv_iterations\"\n}\n\n\ndef calculate_visibility_mask(attributes):\n    # https://arnoldsupport.com/2018/11/21/backdoor-setting-visibility/\n    mapping = {\n        \"primaryVisibility\": 1,  # Camera\n        \"castsShadows\": 2,  # Shadow\n        \"aiVisibleInDiffuseTransmission\": 4,\n        \"aiVisibleInSpecularTransmission\": 8,\n        \"aiVisibleInVolume\": 16,\n        \"aiVisibleInDiffuseReflection\": 32,\n        \"aiVisibleInSpecularReflection\": 64\n    }\n    mask = 255\n    for attr, value in mapping.items():\n        if attributes.get(attr, True):\n            continue\n\n        mask -= value\n\n    return mask\n\n\ndef get_nodes_by_id(standin):\n    \"\"\"Get node id from aiStandIn via json sidecar.\n\n    Args:\n        standin (string): aiStandIn node.\n\n    Returns:\n        (dict): Dictionary with node full name/path and id.\n    \"\"\"\n    path = cmds.getAttr(standin + \".dso\")\n\n    if path.endswith(\".abc\"):\n        # Support alembic files directly\n        return get_alembic_ids_cache(path)\n\n    elif (\n        is_usd_lib_supported and\n        any(path.endswith(ext) for ext in [\".usd\", \".usda\", \".usdc\"])\n    ):\n        # Support usd files directly\n        return get_usd_ids_cache(path)\n\n    json_path = None\n    for f in os.listdir(os.path.dirname(path)):\n        if f.endswith(\".json\"):\n            json_path = os.path.join(os.path.dirname(path), f)\n            break\n\n    if not json_path:\n        log.warning(\"Could not find json file for {}.\".format(standin))\n        return {}\n\n    with open(json_path, \"r\") as f:\n        return json.load(f)\n\n\ndef shading_engine_assignments(shading_engine, attribute, nodes, assignments):\n    \"\"\"Full assignments with shader or disp_map.\n\n    Args:\n        shading_engine (string): Shading engine for material.\n        attribute (string): \"surfaceShader\" or \"displacementShader\"\n        nodes: (list): Nodes paths relative to aiStandIn.\n        assignments (dict): Assignments by nodes.\n    \"\"\"\n    shader_inputs = cmds.listConnections(\n        shading_engine + \".\" + attribute, source=True\n    )\n    if not shader_inputs:\n        log.info(\n            \"Shading engine \\\"{}\\\" missing input \\\"{}\\\"\".format(\n                shading_engine, attribute\n            )\n        )\n        return\n\n    # Strip off component assignments\n    for i, node in enumerate(nodes):\n        if \".\" in node:\n            log.warning(\n                \"Converting face assignment to full object assignment. This \"\n                \"conversion can be lossy: {}\".format(node)\n            )\n            nodes[i] = node.split(\".\")[0]\n\n    shader_type = \"shader\" if attribute == \"surfaceShader\" else \"disp_map\"\n    assignment = \"{}='{}'\".format(shader_type, shader_inputs[0])\n    for node in nodes:\n        assignments[node].append(assignment)\n\n\ndef assign_look(standin, subset):\n    log.info(\"Assigning {} to {}.\".format(subset, standin))\n\n    nodes_by_id = get_nodes_by_id(standin)\n\n    # Group by asset id so we run over the look per asset\n    node_ids_by_asset_id = defaultdict(set)\n    for node_id in nodes_by_id:\n        asset_id = node_id.split(\":\", 1)[0]\n        node_ids_by_asset_id[asset_id].add(node_id)\n\n    project_name = legacy_io.active_project()\n    for asset_id, node_ids in node_ids_by_asset_id.items():\n\n        # Get latest look version\n        version = get_last_version_by_subset_name(\n            project_name,\n            subset_name=subset,\n            asset_id=asset_id,\n            fields=[\"_id\"]\n        )\n        if not version:\n            log.info(\"Didn't find last version for subset name {}\".format(\n                subset\n            ))\n            continue\n\n        relationships = lib.get_look_relationships(version[\"_id\"])\n        shader_nodes, container_node = lib.load_look(version[\"_id\"])\n        namespace = shader_nodes[0].split(\":\")[0]\n\n        # Get only the node ids and paths related to this asset\n        # And get the shader edits the look supplies\n        asset_nodes_by_id = {\n            node_id: nodes_by_id[node_id] for node_id in node_ids\n        }\n        edits = list(\n            api.lib.iter_shader_edits(\n                relationships, shader_nodes, asset_nodes_by_id\n            )\n        )\n\n        # Create assignments\n        node_assignments = {}\n        for edit in edits:\n            for node in edit[\"nodes\"]:\n                if node not in node_assignments:\n                    node_assignments[node] = []\n\n            if edit[\"action\"] == \"assign\":\n                if not cmds.ls(edit[\"shader\"], type=\"shadingEngine\"):\n                    log.info(\"Skipping non-shader: %s\" % edit[\"shader\"])\n                    continue\n\n                shading_engine_assignments(\n                    shading_engine=edit[\"shader\"],\n                    attribute=\"surfaceShader\",\n                    nodes=edit[\"nodes\"],\n                    assignments=node_assignments\n                )\n                shading_engine_assignments(\n                    shading_engine=edit[\"shader\"],\n                    attribute=\"displacementShader\",\n                    nodes=edit[\"nodes\"],\n                    assignments=node_assignments\n                )\n\n            if edit[\"action\"] == \"setattr\":\n                visibility = False\n                for attr, value in edit[\"attributes\"].items():\n                    if attr not in ATTRIBUTE_MAPPING:\n                        log.warning(\n                            \"Skipping setting attribute {} on {} because it is\"\n                            \" not recognized.\".format(attr, edit[\"nodes\"])\n                        )\n                        continue\n\n                    if isinstance(value, str):\n                        value = \"'{}'\".format(value)\n\n                    if ATTRIBUTE_MAPPING[attr] == \"visibility\":\n                        visibility = True\n                        continue\n\n                    assignment = \"{}={}\".format(ATTRIBUTE_MAPPING[attr], value)\n\n                    for node in edit[\"nodes\"]:\n                        node_assignments[node].append(assignment)\n\n                if visibility:\n                    mask = calculate_visibility_mask(edit[\"attributes\"])\n                    assignment = \"visibility={}\".format(mask)\n\n                    for node in edit[\"nodes\"]:\n                        node_assignments[node].append(assignment)\n\n        # Assign shader\n        # Clear all current shader assignments\n        plug = standin + \".operators\"\n        num = cmds.getAttr(plug, size=True)\n        for i in reversed(range(num)):\n            cmds.removeMultiInstance(\"{}[{}]\".format(plug, i), b=True)\n\n        # Create new assignment overrides\n        index = 0\n        for node, assignments in node_assignments.items():\n            if not assignments:\n                continue\n\n            with api.lib.maintained_selection():\n                operator = cmds.createNode(\"aiSetParameter\")\n                operator = cmds.rename(operator, namespace + \":\" + operator)\n\n            cmds.setAttr(operator + \".selection\", node, type=\"string\")\n            for i, assignment in enumerate(assignments):\n                cmds.setAttr(\n                    \"{}.assignment[{}]\".format(operator, i),\n                    assignment,\n                    type=\"string\"\n                )\n\n                cmds.connectAttr(\n                    operator + \".out\", \"{}[{}]\".format(plug, index)\n                )\n\n                index += 1\n\n            cmds.sets(operator, edit=True, addElement=container_node)\n"
  },
  {
    "path": "openpype/hosts/maya/tools/mayalookassigner/commands.py",
    "content": "import os\nimport logging\nfrom collections import defaultdict\n\nimport maya.cmds as cmds\n\nfrom openpype.client import get_assets, get_asset_name_identifier\nfrom openpype.pipeline import (\n    remove_container,\n    registered_host,\n    get_current_project_name,\n)\nfrom openpype.hosts.maya.api import lib\n\nfrom .vray_proxies import get_alembic_ids_cache\nfrom . import arnold_standin\n\nlog = logging.getLogger(__name__)\n\n\ndef get_workfile():\n    path = cmds.file(query=True, sceneName=True) or \"untitled\"\n    return os.path.basename(path)\n\n\ndef get_workfolder():\n    return os.path.dirname(cmds.file(query=True, sceneName=True))\n\n\ndef select(nodes):\n    cmds.select(nodes)\n\n\ndef get_namespace_from_node(node):\n    \"\"\"Get the namespace from the given node\n\n    Args:\n        node (str): name of the node\n\n    Returns:\n        namespace (str)\n\n    \"\"\"\n    parts = node.rsplit(\"|\", 1)[-1].rsplit(\":\", 1)\n    return parts[0] if len(parts) > 1 else u\":\"\n\n\ndef get_selected_nodes():\n    \"\"\"Get information from current selection\"\"\"\n\n    selection = cmds.ls(selection=True, long=True)\n    hierarchy = lib.get_all_children(selection)\n    return list(set(selection + hierarchy))\n\n\ndef get_all_asset_nodes():\n    \"\"\"Get all assets from the scene, container based\n\n    Returns:\n        list: list of dictionaries\n    \"\"\"\n    return cmds.ls(dag=True, noIntermediate=True, long=True)\n\n\ndef create_asset_id_hash(nodes):\n    \"\"\"Create a hash based on cbId attribute value\n    Args:\n        nodes (list): a list of nodes\n\n    Returns:\n        dict\n    \"\"\"\n    node_id_hash = defaultdict(list)\n    for node in nodes:\n        # iterate over content of reference node\n        if cmds.nodeType(node) == \"reference\":\n            ref_hashes = create_asset_id_hash(\n                list(set(cmds.referenceQuery(node, nodes=True, dp=True))))\n            for asset_id, ref_nodes in ref_hashes.items():\n                node_id_hash[asset_id] += ref_nodes\n        elif cmds.pluginInfo('vrayformaya', query=True,\n                             loaded=True) and cmds.nodeType(\n                node) == \"VRayProxy\":\n            path = cmds.getAttr(\"{}.fileName\".format(node))\n            ids = get_alembic_ids_cache(path)\n            for k, _ in ids.items():\n                id = k.split(\":\")[0]\n                node_id_hash[id].append(node)\n        elif cmds.nodeType(node) == \"aiStandIn\":\n            for id, _ in arnold_standin.get_nodes_by_id(node).items():\n                id = id.split(\":\")[0]\n                node_id_hash[id].append(node)\n        else:\n            value = lib.get_id(node)\n            if value is None:\n                continue\n\n            asset_id = value.split(\":\")[0]\n            node_id_hash[asset_id].append(node)\n\n    return dict(node_id_hash)\n\n\ndef create_items_from_nodes(nodes):\n    \"\"\"Create an item for the view based the container and content of it\n\n    It fetches the look document based on the asset ID found in the content.\n    The item will contain all important information for the tool to work.\n\n    If there is an asset ID which is not registered in the project's collection\n    it will log a warning message.\n\n    Args:\n        nodes (list): list of maya nodes\n\n    Returns:\n        list of dicts\n\n    \"\"\"\n\n    asset_view_items = []\n\n    id_hashes = create_asset_id_hash(nodes)\n\n    if not id_hashes:\n        log.warning(\"No id hashes\")\n        return asset_view_items\n\n    project_name = get_current_project_name()\n    asset_ids = set(id_hashes.keys())\n    fields = {\"_id\", \"name\", \"data.parents\"}\n    asset_docs = get_assets(project_name, asset_ids, fields=fields)\n    asset_docs_by_id = {\n        str(asset_doc[\"_id\"]): asset_doc\n        for asset_doc in asset_docs\n    }\n\n    for asset_id, id_nodes in id_hashes.items():\n        asset_doc = asset_docs_by_id.get(asset_id)\n        # Skip if asset id is not found\n        if not asset_doc:\n            log.warning(\n                \"Id found on {num} nodes for which no asset is found database,\"\n                \" skipping '{asset_id}'\".format(\n                    num=len(nodes),\n                    asset_id=asset_id\n                )\n            )\n            continue\n\n        # Collect available look subsets for this asset\n        looks = lib.list_looks(project_name, asset_doc[\"_id\"])\n\n        # Collect namespaces the asset is found in\n        namespaces = set()\n        for node in id_nodes:\n            namespace = get_namespace_from_node(node)\n            namespaces.add(namespace)\n\n        label = get_asset_name_identifier(asset_doc)\n        asset_view_items.append({\n            \"label\": label,\n            \"asset\": asset_doc,\n            \"looks\": looks,\n            \"namespaces\": namespaces\n        })\n\n    return asset_view_items\n\n\ndef remove_unused_looks():\n    \"\"\"Removes all loaded looks for which none of the shaders are used.\n\n    This will cleanup all loaded \"LookLoader\" containers that are unused in\n    the current scene.\n\n    \"\"\"\n\n    host = registered_host()\n\n    unused = []\n    for container in host.ls():\n        if container['loader'] == \"LookLoader\":\n            members = lib.get_container_members(container['objectName'])\n            look_sets = cmds.ls(members, type=\"objectSet\")\n            for look_set in look_sets:\n                # If the set is used than we consider this look *in use*\n                if cmds.sets(look_set, query=True):\n                    break\n            else:\n                unused.append(container)\n\n    for container in unused:\n        log.info(\"Removing unused look container: %s\", container['objectName'])\n        remove_container(container)\n\n    log.info(\"Finished removing unused looks. (see log for details)\")\n"
  },
  {
    "path": "openpype/hosts/maya/tools/mayalookassigner/lib.py",
    "content": "import json\nimport logging\n\nfrom openpype.pipeline import (\n    legacy_io,\n    get_representation_path,\n    registered_host,\n    discover_loader_plugins,\n    loaders_from_representation,\n    load_container\n)\nfrom openpype.client import get_representation_by_name\nfrom openpype.hosts.maya.api import lib\n\n\nlog = logging.getLogger(__name__)\n\n\ndef get_look_relationships(version_id):\n    # type: (str) -> dict\n    \"\"\"Get relations for the look.\n\n    Args:\n        version_id (str): Parent version Id.\n\n    Returns:\n        dict: Dictionary of relations.\n    \"\"\"\n\n    project_name = legacy_io.active_project()\n    json_representation = get_representation_by_name(\n        project_name, representation_name=\"json\", version_id=version_id\n    )\n\n    # Load relationships\n    shader_relation = get_representation_path(json_representation)\n    with open(shader_relation, \"r\") as f:\n        relationships = json.load(f)\n\n    return relationships\n\n\ndef load_look(version_id):\n    # type: (str) -> list\n    \"\"\"Load look from version.\n\n    Get look from version and invoke Loader for it.\n\n    Args:\n        version_id (str): Version ID\n\n    Returns:\n        list of shader nodes.\n\n    \"\"\"\n\n    project_name = legacy_io.active_project()\n    # Get representations of shader file and relationships\n    look_representation = get_representation_by_name(\n        project_name, representation_name=\"ma\", version_id=version_id\n    )\n\n    # See if representation is already loaded, if so reuse it.\n    host = registered_host()\n    representation_id = str(look_representation['_id'])\n    for container in host.ls():\n        if (container['loader'] == \"LookLoader\" and\n                container['representation'] == representation_id):\n            log.info(\"Reusing loaded look ...\")\n            container_node = container['objectName']\n            break\n    else:\n        log.info(\"Using look for the first time ...\")\n\n        # Load file\n        all_loaders = discover_loader_plugins()\n        loaders = loaders_from_representation(all_loaders, representation_id)\n        loader = next(\n            (i for i in loaders if i.__name__ == \"LookLoader\"), None)\n        if loader is None:\n            raise RuntimeError(\"Could not find LookLoader, this is a bug\")\n\n        # Reference the look file\n        with lib.maintained_selection():\n            container_node = load_container(loader, look_representation)[0]\n\n    return lib.get_container_members(container_node), container_node\n"
  },
  {
    "path": "openpype/hosts/maya/tools/mayalookassigner/models.py",
    "content": "from collections import defaultdict\n\nfrom qtpy import QtCore\nimport qtawesome\n\nfrom openpype.tools.utils import models\nfrom openpype.style import get_default_entity_icon_color\n\n\nclass AssetModel(models.TreeModel):\n\n    Columns = [\"label\"]\n\n    def __init__(self, *args, **kwargs):\n        super(AssetModel, self).__init__(*args, **kwargs)\n\n        self._icon_color = get_default_entity_icon_color()\n\n    def add_items(self, items):\n        \"\"\"\n        Add items to model with needed data\n        Args:\n            items(list): collection of item data\n\n        Returns:\n            None\n        \"\"\"\n\n        self.beginResetModel()\n\n        # Add the items sorted by label\n        sorter = lambda x: x[\"label\"]\n\n        for item in sorted(items, key=sorter):\n\n            asset_item = models.Item()\n            asset_item.update(item)\n            asset_item[\"icon\"] = \"folder\"\n\n            # Add namespace children\n            namespaces = item[\"namespaces\"]\n            for namespace in sorted(namespaces):\n                child = models.Item()\n                child.update(item)\n                child.update({\n                    \"label\": (namespace if namespace != \":\"\n                              else \"(no namespace)\"),\n                    \"namespace\": namespace,\n                    \"looks\": item[\"looks\"],\n                    \"icon\": \"folder-o\"\n                })\n                asset_item.add_child(child)\n\n            self.add_child(asset_item)\n\n        self.endResetModel()\n\n    def data(self, index, role):\n\n        if not index.isValid():\n            return\n\n        if role == models.TreeModel.ItemRole:\n            node = index.internalPointer()\n            return node\n\n        # Add icon\n        if role == QtCore.Qt.DecorationRole:\n            if index.column() == 0:\n                node = index.internalPointer()\n                icon = node.get(\"icon\")\n                if icon:\n                    return qtawesome.icon(\n                        \"fa.{0}\".format(icon),\n                        color=self._icon_color\n                    )\n\n        return super(AssetModel, self).data(index, role)\n\n\nclass LookModel(models.TreeModel):\n    \"\"\"Model displaying a list of looks and matches for assets\"\"\"\n\n    Columns = [\"label\", \"match\"]\n\n    def add_items(self, items):\n        \"\"\"Add items to model with needed data\n\n        An item exists of:\n            {\n                \"subset\": 'name of subset',\n                \"asset\": asset_document\n            }\n\n        Args:\n            items(list): collection of item data\n\n        Returns:\n            None\n        \"\"\"\n\n        self.beginResetModel()\n\n        # Collect the assets per look name (from the items of the AssetModel)\n        look_subsets = defaultdict(list)\n        for asset_item in items:\n            asset = asset_item[\"asset\"]\n            for look in asset_item[\"looks\"]:\n                look_subsets[look[\"name\"]].append(asset)\n\n        for subset in sorted(look_subsets.keys()):\n            assets = look_subsets[subset]\n\n            # Define nice label without \"look\" prefix for readability\n            label = subset if not subset.startswith(\"look\") else subset[4:]\n\n            item_node = models.Item()\n            item_node[\"label\"] = label\n            item_node[\"subset\"] = subset\n\n            # Amount of matching assets for this look\n            item_node[\"match\"] = len(assets)\n\n            # Store the assets that have this subset available\n            item_node[\"assets\"] = assets\n\n            self.add_child(item_node)\n\n        self.endResetModel()\n"
  },
  {
    "path": "openpype/hosts/maya/tools/mayalookassigner/usd.py",
    "content": "from collections import defaultdict\n\ntry:\n    from pxr import Usd\n    is_usd_lib_supported = True\nexcept ImportError:\n    is_usd_lib_supported = False\n\n\ndef get_usd_ids_cache(path):\n    # type: (str) -> dict\n    \"\"\"Build a id to node mapping in a USD file.\n\n    Nodes without IDs are ignored.\n\n    Returns:\n        dict: Mapping of id to nodes in the USD file.\n\n    \"\"\"\n    if not is_usd_lib_supported:\n        raise RuntimeError(\"No pxr.Usd python library available.\")\n\n    stage = Usd.Stage.Open(path)\n    ids = {}\n    for prim in stage.Traverse():\n        attr = prim.GetAttribute(\"userProperties:cbId\")\n        if not attr.IsValid():\n            continue\n        value = attr.Get()\n        if not value:\n            continue\n        path = str(prim.GetPath())\n        ids[path] = value\n\n    cache = defaultdict(list)\n    for path, value in ids.items():\n        cache[value].append(path)\n    return dict(cache)\n"
  },
  {
    "path": "openpype/hosts/maya/tools/mayalookassigner/views.py",
    "content": "from qtpy import QtWidgets, QtCore\n\n\nclass View(QtWidgets.QTreeView):\n    data_changed = QtCore.Signal()\n\n    def __init__(self, parent=None):\n        super(View, self).__init__(parent=parent)\n\n        # view settings\n        self.setAlternatingRowColors(False)\n        self.setSortingEnabled(True)\n        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)\n        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n\n    def get_indices(self):\n        \"\"\"Get the selected rows\"\"\"\n        selection_model = self.selectionModel()\n        return selection_model.selectedRows()\n\n    def extend_to_children(self, indices):\n        \"\"\"Extend the indices to the children indices.\n\n        Top-level indices are extended to its children indices. Sub-items\n        are kept as is.\n\n        :param indices: The indices to extend.\n        :type indices: list\n\n        :return: The children indices\n        :rtype: list\n        \"\"\"\n\n        subitems = set()\n        for i in indices:\n            valid_parent = i.parent().isValid()\n            if valid_parent and i not in subitems:\n                subitems.add(i)\n            else:\n                # is top level node\n                model = i.model()\n                rows = model.rowCount(parent=i)\n                for row in range(rows):\n                    child = model.index(row, 0, parent=i)\n                    subitems.add(child)\n\n        return list(subitems)\n"
  },
  {
    "path": "openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tools for loading looks to vray proxies.\"\"\"\nfrom collections import defaultdict\nimport logging\n\nfrom maya import cmds\n\nfrom openpype.client import get_last_version_by_subset_name\nfrom openpype.pipeline import get_current_project_name\nimport openpype.hosts.maya.lib as maya_lib\nfrom . import lib\nfrom .alembic import get_alembic_ids_cache\n\n\nlog = logging.getLogger(__name__)\n\n\ndef assign_vrayproxy_shaders(vrayproxy, assignments):\n    # type: (str, dict) -> None\n    \"\"\"Assign shaders to content of Vray Proxy.\n\n    This will create shader overrides on Vray Proxy to assign shaders to its\n    content.\n\n    Todo:\n        Allow to optimize and assign a single shader to multiple shapes at\n        once or maybe even set it to the highest available path?\n\n    Args:\n        vrayproxy (str): Name of Vray Proxy\n        assignments (dict): Mapping of shader assignments.\n\n    Returns:\n        None\n\n    \"\"\"\n    # Clear all current shader assignments\n    plug = vrayproxy + \".shaders\"\n    num = cmds.getAttr(plug, size=True)\n    for i in reversed(range(num)):\n        cmds.removeMultiInstance(\"{}[{}]\".format(plug, i), b=True)\n\n    # Create new assignment overrides\n    index = 0\n    for material, paths in assignments.items():\n        for path in paths:\n            plug = \"{}.shaders[{}]\".format(vrayproxy, index)\n            cmds.setAttr(plug + \".shadersNames\", path, type=\"string\")\n            cmds.connectAttr(material + \".outColor\",\n                             plug + \".shadersConnections\", force=True)\n            index += 1\n\n\ndef vrayproxy_assign_look(vrayproxy, subset=\"lookDefault\"):\n    # type: (str, str) -> None\n    \"\"\"Assign look to vray proxy.\n\n    Args:\n        vrayproxy (str): Name of vrayproxy to apply look to.\n        subset (str): Name of look subset.\n\n    Returns:\n        None\n\n    \"\"\"\n    path = cmds.getAttr(vrayproxy + \".fileName\")\n\n    nodes_by_id = get_alembic_ids_cache(path)\n    if not nodes_by_id:\n        log.warning(\"Alembic file has no cbId attributes: %s\" % path)\n        return\n\n    # Group by asset id so we run over the look per asset\n    node_ids_by_asset_id = defaultdict(set)\n    for node_id in nodes_by_id:\n        asset_id = node_id.split(\":\", 1)[0]\n        node_ids_by_asset_id[asset_id].add(node_id)\n\n    project_name = get_current_project_name()\n    for asset_id, node_ids in node_ids_by_asset_id.items():\n\n        # Get latest look version\n        version = get_last_version_by_subset_name(\n            project_name,\n            subset_name=subset,\n            asset_id=asset_id,\n            fields=[\"_id\"]\n        )\n        if not version:\n            print(\"Didn't find last version for subset name {}\".format(\n                subset\n            ))\n            continue\n\n        relationships = lib.get_look_relationships(version[\"_id\"])\n        shadernodes, _ = lib.load_look(version[\"_id\"])\n\n        # Get only the node ids and paths related to this asset\n        # And get the shader edits the look supplies\n        asset_nodes_by_id = {\n            node_id: nodes_by_id[node_id] for node_id in node_ids\n        }\n        edits = list(\n            maya_lib.iter_shader_edits(\n                relationships, shadernodes, asset_nodes_by_id\n            )\n        )\n\n        # Create assignments\n        assignments = {}\n        for edit in edits:\n            if edit[\"action\"] == \"assign\":\n                nodes = edit[\"nodes\"]\n                shader = edit[\"shader\"]\n                if not cmds.ls(shader, type=\"shadingEngine\"):\n                    print(\"Skipping non-shader: %s\" % shader)\n                    continue\n\n                inputs = cmds.listConnections(\n                    shader + \".surfaceShader\", source=True)\n                if not inputs:\n                    print(\"Shading engine missing material: %s\" % shader)\n\n                # Strip off component assignments\n                for i, node in enumerate(nodes):\n                    if \".\" in node:\n                        log.warning(\n                            (\"Converting face assignment to full object \"\n                             \"assignment. This conversion can be lossy: \"\n                             \"{}\").format(node))\n                        nodes[i] = node.split(\".\")[0]\n\n                material = inputs[0]\n                assignments[material] = nodes\n\n        assign_vrayproxy_shaders(vrayproxy, assignments)\n"
  },
  {
    "path": "openpype/hosts/maya/tools/mayalookassigner/widgets.py",
    "content": "import logging\nfrom collections import defaultdict\n\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.client import get_asset_name_identifier\nfrom openpype.tools.utils.models import TreeModel\nfrom openpype.tools.utils.lib import (\n    preserve_expanded_rows,\n    preserve_selection,\n)\n\nfrom .models import (\n    AssetModel,\n    LookModel\n)\nfrom . import commands\nfrom .views import View\n\nfrom maya import cmds\n\n\nclass AssetOutliner(QtWidgets.QWidget):\n    refreshed = QtCore.Signal()\n    selection_changed = QtCore.Signal()\n\n    def __init__(self, parent=None):\n        super(AssetOutliner, self).__init__(parent)\n\n        title = QtWidgets.QLabel(\"Assets\", self)\n        title.setAlignment(QtCore.Qt.AlignCenter)\n        title.setStyleSheet(\"font-weight: bold; font-size: 12px\")\n\n        model = AssetModel()\n        view = View(self)\n        view.setModel(model)\n        view.customContextMenuRequested.connect(self.right_mouse_menu)\n        view.setSortingEnabled(False)\n        view.setHeaderHidden(True)\n        view.setIndentation(10)\n\n        from_all_asset_btn = QtWidgets.QPushButton(\n            \"Get All Assets\", self\n        )\n        from_selection_btn = QtWidgets.QPushButton(\n            \"Get Assets From Selection\", self\n        )\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(title)\n        layout.addWidget(from_all_asset_btn)\n        layout.addWidget(from_selection_btn)\n        layout.addWidget(view)\n\n        # Build connections\n        from_selection_btn.clicked.connect(self.get_selected_assets)\n        from_all_asset_btn.clicked.connect(self.get_all_assets)\n\n        selection_model = view.selectionModel()\n        selection_model.selectionChanged.connect(self.selection_changed)\n\n        self.view = view\n        self.model = model\n\n        self.log = logging.getLogger(__name__)\n\n    def clear(self):\n        self.model.clear()\n\n        # fix looks remaining visible when no items present after \"refresh\"\n        # todo: figure out why this workaround is needed.\n        self.selection_changed.emit()\n\n    def add_items(self, items):\n        \"\"\"Add new items to the outliner\"\"\"\n\n        self.model.add_items(items)\n        self.refreshed.emit()\n\n    def get_selected_items(self):\n        \"\"\"Get current selected items from view\n\n        Returns:\n            list: list of dictionaries\n        \"\"\"\n\n        selection_model = self.view.selectionModel()\n        return [row.data(TreeModel.ItemRole)\n                for row in selection_model.selectedRows(0)]\n\n    def get_all_assets(self):\n        \"\"\"Add all items from the current scene\"\"\"\n\n        with preserve_expanded_rows(self.view):\n            with preserve_selection(self.view):\n                self.clear()\n                nodes = commands.get_all_asset_nodes()\n                items = commands.create_items_from_nodes(nodes)\n                self.add_items(items)\n                return len(items) > 0\n\n    def get_selected_assets(self):\n        \"\"\"Add all selected items from the current scene\"\"\"\n\n        with preserve_expanded_rows(self.view):\n            with preserve_selection(self.view):\n                self.clear()\n                nodes = commands.get_selected_nodes()\n                items = commands.create_items_from_nodes(nodes)\n                self.add_items(items)\n\n    def get_nodes(self, selection=False):\n        \"\"\"Find the nodes in the current scene per asset.\"\"\"\n\n        items = self.get_selected_items()\n\n        # Collect all nodes by hash (optimization)\n        if not selection:\n            nodes = cmds.ls(dag=True, long=True)\n        else:\n            nodes = commands.get_selected_nodes()\n        id_nodes = commands.create_asset_id_hash(nodes)\n\n        # Collect the asset item entries per asset\n        # and collect the namespaces we'd like to apply\n        assets = {}\n        asset_namespaces = defaultdict(set)\n        for item in items:\n            asset_id = str(item[\"asset\"][\"_id\"])\n            asset_name = get_asset_name_identifier(item[\"asset\"])\n            asset_namespaces[asset_name].add(item.get(\"namespace\"))\n\n            if asset_name in assets:\n                continue\n\n            assets[asset_name] = item\n            assets[asset_name][\"nodes\"] = id_nodes.get(asset_id, [])\n\n        # Filter nodes to namespace (if only namespaces were selected)\n        for asset_name in assets:\n            namespaces = asset_namespaces[asset_name]\n\n            # When None is present there should be no filtering\n            if None in namespaces:\n                continue\n\n            # Else only namespaces are selected and *not* the top entry so\n            # we should filter to only those namespaces.\n            nodes = assets[asset_name][\"nodes\"]\n            nodes = [node for node in nodes if\n                     commands.get_namespace_from_node(node) in namespaces]\n            assets[asset_name][\"nodes\"] = nodes\n\n        return assets\n\n    def select_asset_from_items(self):\n        \"\"\"Select nodes from listed asset\"\"\"\n\n        items = self.get_nodes(selection=False)\n        nodes = []\n        for item in items.values():\n            nodes.extend(item[\"nodes\"])\n\n        commands.select(nodes)\n\n    def right_mouse_menu(self, pos):\n        \"\"\"Build RMB menu for asset outliner\"\"\"\n\n        active = self.view.currentIndex()  # index under mouse\n        active = active.sibling(active.row(), 0)  # get first column\n        globalpos = self.view.viewport().mapToGlobal(pos)\n\n        menu = QtWidgets.QMenu(self.view)\n\n        # Direct assignment\n        apply_action = QtWidgets.QAction(menu, text=\"Select nodes\")\n        apply_action.triggered.connect(self.select_asset_from_items)\n\n        if not active.isValid():\n            apply_action.setEnabled(False)\n\n        menu.addAction(apply_action)\n\n        menu.exec_(globalpos)\n\n\nclass LookOutliner(QtWidgets.QWidget):\n    menu_apply_action = QtCore.Signal()\n\n    def __init__(self, parent=None):\n        super(LookOutliner, self).__init__(parent)\n\n        # Looks from database\n        title = QtWidgets.QLabel(\"Looks\", self)\n        title.setAlignment(QtCore.Qt.AlignCenter)\n        title.setStyleSheet(\"font-weight: bold; font-size: 12px\")\n        title.setAlignment(QtCore.Qt.AlignCenter)\n\n        model = LookModel()\n\n        # Proxy for dynamic sorting\n        proxy = QtCore.QSortFilterProxyModel()\n        proxy.setSourceModel(model)\n\n        view = View(self)\n        view.setModel(proxy)\n        view.setMinimumHeight(180)\n        view.setToolTip(\"Use right mouse button menu for direct actions\")\n        view.customContextMenuRequested.connect(self.right_mouse_menu)\n        view.sortByColumn(0, QtCore.Qt.AscendingOrder)\n\n        # look manager layout\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(10)\n        layout.addWidget(title)\n        layout.addWidget(view)\n\n        self.view = view\n        self.model = model\n\n    def clear(self):\n        self.model.clear()\n\n    def add_items(self, items):\n        self.model.add_items(items)\n\n    def get_selected_items(self):\n        \"\"\"Get current selected items from view\n\n        Returns:\n            list: list of dictionaries\n        \"\"\"\n\n        items = [i.data(TreeModel.ItemRole) for i in self.view.get_indices()]\n        return [item for item in items if item is not None]\n\n    def right_mouse_menu(self, pos):\n        \"\"\"Build RMB menu for look view\"\"\"\n\n        active = self.view.currentIndex()  # index under mouse\n        active = active.sibling(active.row(), 0)  # get first column\n        globalpos = self.view.viewport().mapToGlobal(pos)\n\n        if not active.isValid():\n            return\n\n        menu = QtWidgets.QMenu(self.view)\n\n        # Direct assignment\n        apply_action = QtWidgets.QAction(menu, text=\"Assign looks..\")\n        apply_action.triggered.connect(self.menu_apply_action)\n\n        menu.addAction(apply_action)\n\n        menu.exec_(globalpos)\n"
  },
  {
    "path": "openpype/hosts/nuke/__init__.py",
    "content": "from .addon import (\n    NUKE_ROOT_DIR,\n    NukeAddon,\n)\n\n\n__all__ = (\n    \"NUKE_ROOT_DIR\",\n    \"NukeAddon\",\n)\n"
  },
  {
    "path": "openpype/hosts/nuke/addon.py",
    "content": "import os\nimport platform\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nNUKE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass NukeAddon(OpenPypeModule, IHostAddon):\n    name = \"nuke\"\n    host_name = \"nuke\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        # Add requirements to NUKE_PATH\n        new_nuke_paths = [\n            os.path.join(NUKE_ROOT_DIR, \"startup\")\n        ]\n        old_nuke_path = env.get(\"NUKE_PATH\") or \"\"\n        for path in old_nuke_path.split(os.pathsep):\n            if not path:\n                continue\n\n            norm_path = os.path.normpath(path)\n            if norm_path not in new_nuke_paths:\n                new_nuke_paths.append(norm_path)\n\n        env[\"NUKE_PATH\"] = os.pathsep.join(new_nuke_paths)\n        # Remove auto screen scale factor for Qt\n        # - let Nuke decide it's value\n        env.pop(\"QT_AUTO_SCREEN_SCALE_FACTOR\", None)\n        # Remove tkinter library paths if are set\n        env.pop(\"TK_LIBRARY\", None)\n        env.pop(\"TCL_LIBRARY\", None)\n\n        # Add vendor to PYTHONPATH\n        python_path = env[\"PYTHONPATH\"]\n        python_path_parts = []\n        if python_path:\n            python_path_parts = python_path.split(os.pathsep)\n        vendor_path = os.path.join(NUKE_ROOT_DIR, \"vendor\")\n        python_path_parts.insert(0, vendor_path)\n        env[\"PYTHONPATH\"] = os.pathsep.join(python_path_parts)\n\n        # Set default values if are not already set via settings\n        defaults = {\n            \"LOGLEVEL\": \"DEBUG\"\n        }\n        for key, value in defaults.items():\n            if not env.get(key):\n                env[key] = value\n\n        # Try to add QuickTime to PATH\n        quick_time_path = \"C:/Program Files (x86)/QuickTime/QTSystem\"\n        if platform.system() == \"windows\" and os.path.exists(quick_time_path):\n            path_value = env.get(\"PATH\") or \"\"\n            path_paths = [\n                path\n                for path in path_value.split(os.pathsep)\n                if path\n            ]\n            path_paths.append(quick_time_path)\n            env[\"PATH\"] = os.pathsep.join(path_paths)\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [\n            os.path.join(NUKE_ROOT_DIR, \"hooks\")\n        ]\n\n    def get_workfile_extensions(self):\n        return [\".nk\"]\n"
  },
  {
    "path": "openpype/hosts/nuke/api/__init__.py",
    "content": "from .workio import (\n    file_extensions,\n    has_unsaved_changes,\n    save_file,\n    open_file,\n    current_file,\n    work_root,\n)\nfrom .command import (\n    viewer_update_and_undo_stop\n)\nfrom .plugin import (\n    NukeCreator,\n    NukeWriteCreator,\n    NukeCreatorError,\n    OpenPypeCreator,\n    get_instance_group_node_childs,\n    get_colorspace_from_node\n)\nfrom .pipeline import (\n    NukeHost,\n\n    ls,\n\n    list_instances,\n    remove_instance,\n    select_instance,\n\n    containerise,\n    parse_container,\n    update_container,\n\n)\nfrom .lib import (\n    INSTANCE_DATA_KNOB,\n    ROOT_DATA_KNOB,\n    maintained_selection,\n    reset_selection,\n    select_nodes,\n    get_view_process_node,\n    duplicate_node,\n    convert_knob_value_to_correct_type,\n    get_node_data,\n    set_node_data,\n    update_node_data,\n    create_write_node,\n    link_knobs\n)\nfrom .utils import (\n    colorspace_exists_on_node,\n    get_colorspace_list\n)\n\nfrom .actions import (\n    SelectInvalidAction,\n    SelectInstanceNodeAction\n)\n\n__all__ = (\n    \"file_extensions\",\n    \"has_unsaved_changes\",\n    \"save_file\",\n    \"open_file\",\n    \"current_file\",\n    \"work_root\",\n\n    \"viewer_update_and_undo_stop\",\n\n    \"NukeCreator\",\n    \"NukeWriteCreator\",\n    \"NukeCreatorError\",\n    \"OpenPypeCreator\",\n    \"NukeHost\",\n    \"get_instance_group_node_childs\",\n    \"get_colorspace_from_node\",\n\n    \"ls\",\n\n    \"list_instances\",\n    \"remove_instance\",\n    \"select_instance\",\n\n    \"containerise\",\n    \"parse_container\",\n    \"update_container\",\n\n    \"INSTANCE_DATA_KNOB\",\n    \"ROOT_DATA_KNOB\",\n    \"maintained_selection\",\n    \"reset_selection\",\n    \"select_nodes\",\n    \"get_view_process_node\",\n    \"duplicate_node\",\n    \"convert_knob_value_to_correct_type\",\n    \"get_node_data\",\n    \"set_node_data\",\n    \"update_node_data\",\n    \"create_write_node\",\n    \"link_knobs\",\n\n    \"colorspace_exists_on_node\",\n    \"get_colorspace_list\",\n\n    \"SelectInvalidAction\",\n    \"SelectInstanceNodeAction\"\n)\n"
  },
  {
    "path": "openpype/hosts/nuke/api/actions.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import get_errored_instances_from_context\nfrom .lib import (\n    reset_selection,\n    select_nodes\n)\n\n\nclass SelectInvalidAction(pyblish.api.Action):\n    \"\"\"Select invalid nodes in Nuke when plug-in failed.\n\n    To retrieve the invalid nodes this assumes a static `get_invalid()`\n    method is available on the plugin.\n\n    \"\"\"\n    label = \"Select invalid nodes\"\n    on = \"failed\"  # This action is only available on a failed plug-in\n    icon = \"search\"  # Icon from Awesome Icon\n\n    def process(self, context, plugin):\n\n        errored_instances = get_errored_instances_from_context(context,\n                                                               plugin=plugin)\n\n        # Get the invalid nodes for the plug-ins\n        self.log.info(\"Finding invalid nodes..\")\n        invalid = set()\n        for instance in errored_instances:\n            invalid_nodes = plugin.get_invalid(instance)\n\n            if invalid_nodes:\n                if isinstance(invalid_nodes, (list, tuple)):\n                    invalid.update(invalid_nodes)\n                else:\n                    self.log.warning(\"Plug-in returned to be invalid, \"\n                                     \"but has no selectable nodes.\")\n\n        if invalid:\n            self.log.info(\"Selecting invalid nodes: {}\".format(invalid))\n            reset_selection()\n            select_nodes(invalid)\n        else:\n            self.log.info(\"No invalid nodes found.\")\n\n\nclass SelectInstanceNodeAction(pyblish.api.Action):\n    \"\"\"Select instance node for failed plugin.\"\"\"\n    label = \"Select instance node\"\n    on = \"failed\"  # This action is only available on a failed plug-in\n    icon = \"mdi.cursor-default-click\"\n\n    def process(self, context, plugin):\n\n        # Get the errored instances for the plug-in\n        errored_instances = get_errored_instances_from_context(\n            context, plugin)\n\n        # Get the invalid nodes for the plug-ins\n        self.log.info(\"Finding instance nodes..\")\n        nodes = set()\n        for instance in errored_instances:\n            instance_node = instance.data.get(\"transientData\", {}).get(\"node\")\n            if not instance_node:\n                raise RuntimeError(\n                    \"No transientData['node'] found on instance: {}\".format(\n                        instance\n                    )\n                )\n            nodes.add(instance_node)\n\n        if nodes:\n            self.log.info(\"Selecting instance nodes: {}\".format(nodes))\n            reset_selection()\n            select_nodes(nodes)\n        else:\n            self.log.info(\"No instance nodes found.\")\n"
  },
  {
    "path": "openpype/hosts/nuke/api/command.py",
    "content": "import logging\nimport contextlib\nimport nuke\n\nlog = logging.getLogger(__name__)\n\n\n@contextlib.contextmanager\ndef viewer_update_and_undo_stop():\n    \"\"\"Lock viewer from updating and stop recording undo steps\"\"\"\n    try:\n        # stop active viewer to update any change\n        viewer = nuke.activeViewer()\n        if viewer:\n            viewer.stop()\n        else:\n            log.warning(\"No available active Viewer\")\n        nuke.Undo.disable()\n        yield\n    finally:\n        nuke.Undo.enable()\n"
  },
  {
    "path": "openpype/hosts/nuke/api/constants.py",
    "content": "import os\n\n\nASSIST = bool(os.getenv(\"NUKEASSIST\"))\n"
  },
  {
    "path": "openpype/hosts/nuke/api/gizmo_menu.py",
    "content": "import os\nimport re\nimport nuke\n\nfrom openpype.lib import Logger\n\nlog = Logger.get_logger(__name__)\n\n\nclass GizmoMenu():\n    def __init__(self, title, icon=None):\n\n        self.toolbar = self._create_toolbar_menu(\n            title,\n            icon=icon\n        )\n\n        self._script_actions = []\n\n    def _create_toolbar_menu(self, name, icon=None):\n        nuke_node_menu = nuke.menu(\"Nodes\")\n        return nuke_node_menu.addMenu(\n            name,\n            icon=icon\n        )\n\n    def _make_menu_path(self, path, icon=None):\n        parent = self.toolbar\n        for folder in re.split(r\"/|\\\\\", path):\n            if not folder:\n                continue\n            existing_menu = parent.findItem(folder)\n            if existing_menu:\n                parent = existing_menu\n            else:\n                parent = parent.addMenu(folder, icon=icon)\n\n        return parent\n\n    def build_from_configuration(self, configuration):\n        for menu in configuration:\n            # Construct parent path else parent is toolbar\n            parent = self.toolbar\n            gizmo_toolbar_path = menu.get(\"gizmo_toolbar_path\")\n            if gizmo_toolbar_path:\n                parent = self._make_menu_path(gizmo_toolbar_path)\n\n            for item in menu[\"sub_gizmo_list\"]:\n                assert isinstance(item, dict), \"Configuration is wrong!\"\n\n                if not item.get(\"title\"):\n                    continue\n\n                item_type = item.get(\"sourcetype\")\n\n                if item_type == \"python\":\n                    parent.addCommand(\n                        item[\"title\"],\n                        command=str(item[\"command\"]),\n                        icon=item.get(\"icon\"),\n                        shortcut=item.get(\"shortcut\")\n                    )\n                elif item_type == \"file\":\n                    parent.addCommand(\n                        item['title'],\n                        \"nuke.createNode('{}')\".format(item.get('file_name')),\n                        shortcut=item.get('shortcut')\n                    )\n\n                # add separator\n                # Special behavior for separators\n                elif item_type == \"separator\":\n                    parent.addSeparator()\n\n                # add submenu\n                # items should hold a collection of submenu items (dict)\n                elif item_type == \"menu\":\n                    # assert \"items\" in item, \"Menu is missing 'items' key\"\n                    parent.addMenu(\n                        item['title'],\n                        icon=item.get('icon')\n                    )\n\n    def add_gizmo_path(self, gizmo_paths):\n        for gizmo_path in gizmo_paths:\n            if os.path.isdir(gizmo_path):\n                for folder in os.listdir(gizmo_path):\n                    if os.path.isdir(os.path.join(gizmo_path, folder)):\n                        nuke.pluginAddPath(os.path.join(gizmo_path, folder))\n                nuke.pluginAddPath(gizmo_path)\n            else:\n                log.warning(\"This path doesn't exist: {}\".format(gizmo_path))\n"
  },
  {
    "path": "openpype/hosts/nuke/api/lib.py",
    "content": "import os\nfrom pprint import pformat\nimport re\nimport json\nimport six\nimport functools\nimport warnings\nimport platform\nimport tempfile\nimport contextlib\nfrom collections import OrderedDict\n\nimport nuke\nfrom qtpy import QtCore, QtWidgets\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_project,\n    get_asset_by_name,\n    get_versions,\n    get_last_versions,\n    get_representations,\n)\n\nfrom openpype.host import HostDirmap\nfrom openpype.tools.utils import host_tools\nfrom openpype.pipeline.workfile.workfile_template_builder import (\n    TemplateProfileNotFound\n)\nfrom openpype.lib import (\n    env_value_to_bool,\n    Logger,\n    get_version_from_path,\n    StringTemplate,\n)\n\nfrom openpype.settings import (\n    get_project_settings,\n    get_current_project_settings,\n)\nfrom openpype.modules import ModulesManager\nfrom openpype.pipeline.template_data import get_template_data_with_names\nfrom openpype.pipeline import (\n    discover_legacy_creator_plugins,\n    Anatomy,\n    get_current_host_name,\n    get_current_project_name,\n    get_current_asset_name,\n)\nfrom openpype.pipeline.context_tools import (\n    get_custom_workfile_template_from_session\n)\nfrom openpype.pipeline.colorspace import get_imageio_config\nfrom openpype.pipeline.workfile import BuildWorkfile\nfrom . import gizmo_menu\nfrom .constants import ASSIST\n\nfrom .workio import save_file\nfrom .utils import get_node_outputs\n\nlog = Logger.get_logger(__name__)\n\n_NODE_TAB_NAME = \"{}\".format(os.getenv(\"AVALON_LABEL\") or \"Avalon\")\nAVALON_LABEL = os.getenv(\"AVALON_LABEL\") or \"Avalon\"\nAVALON_TAB = \"{}\".format(AVALON_LABEL)\nAVALON_DATA_GROUP = \"{}DataGroup\".format(AVALON_LABEL.capitalize())\nEXCLUDED_KNOB_TYPE_ON_READ = (\n    20,  # Tab Knob\n    26,  # Text Knob (But for backward compatibility, still be read\n         #  if value is not an empty string.)\n)\nJSON_PREFIX = \"JSON:::\"\nROOT_DATA_KNOB = \"publish_context\"\nINSTANCE_DATA_KNOB = \"publish_instance\"\n\n\nclass DeprecatedWarning(DeprecationWarning):\n    pass\n\n\ndef deprecated(new_destination):\n    \"\"\"Mark functions as deprecated.\n\n    It will result in a warning being emitted when the function is used.\n    \"\"\"\n\n    func = None\n    if callable(new_destination):\n        func = new_destination\n        new_destination = None\n\n    def _decorator(decorated_func):\n        if new_destination is None:\n            warning_message = (\n                \" Please check content of deprecated function to figure out\"\n                \" possible replacement.\"\n            )\n        else:\n            warning_message = \" Please replace your usage with '{}'.\".format(\n                new_destination\n            )\n\n        @functools.wraps(decorated_func)\n        def wrapper(*args, **kwargs):\n            warnings.simplefilter(\"always\", DeprecatedWarning)\n            warnings.warn(\n                (\n                    \"Call to deprecated function '{}'\"\n                    \"\\nFunction was moved or removed.{}\"\n                ).format(decorated_func.__name__, warning_message),\n                category=DeprecatedWarning,\n                stacklevel=4\n            )\n            return decorated_func(*args, **kwargs)\n        return wrapper\n\n    if func is None:\n        return _decorator\n    return _decorator(func)\n\n\nclass Context:\n    main_window = None\n    context_action_item = None\n    project_name = os.getenv(\"AVALON_PROJECT\")\n    # Workfile related code\n    workfiles_launched = False\n    workfiles_tool_timer = None\n\n    # Seems unused\n    _project_doc = None\n\n\ndef get_main_window():\n    \"\"\"Acquire Nuke's main window\"\"\"\n    if Context.main_window is None:\n\n        top_widgets = QtWidgets.QApplication.topLevelWidgets()\n        name = \"Foundry::UI::DockMainWindow\"\n        for widget in top_widgets:\n            if (\n                widget.inherits(\"QMainWindow\")\n                and widget.metaObject().className() == name\n            ):\n                Context.main_window = widget\n                break\n    return Context.main_window\n\n\ndef set_node_data(node, knobname, data):\n    \"\"\"Write data to node invisible knob\n\n    Will create new in case it doesn't exists\n    or update the one already created.\n\n    Args:\n        node (nuke.Node): node object\n        knobname (str): knob name\n        data (dict): data to be stored in knob\n    \"\"\"\n    # if exists then update data\n    if knobname in node.knobs():\n        log.debug(\"Updating knobname `{}` on node `{}`\".format(\n            knobname, node.name()\n        ))\n        update_node_data(node, knobname, data)\n        return\n\n    log.debug(\"Creating knobname `{}` on node `{}`\".format(\n        knobname, node.name()\n    ))\n    # else create new\n    knob_value = JSON_PREFIX + json.dumps(data)\n    knob = nuke.String_Knob(knobname)\n    knob.setValue(knob_value)\n    knob.setFlag(nuke.INVISIBLE)\n    node.addKnob(knob)\n\n\ndef get_node_data(node, knobname):\n    \"\"\"Read data from node.\n\n    Args:\n        node (nuke.Node): node object\n        knobname (str): knob name\n\n    Returns:\n        dict: data stored in knob\n    \"\"\"\n    if knobname not in node.knobs():\n        return\n\n    rawdata = node[knobname].getValue()\n    if (\n        isinstance(rawdata, six.string_types)\n        and rawdata.startswith(JSON_PREFIX)\n    ):\n        try:\n            return json.loads(rawdata[len(JSON_PREFIX):])\n        except json.JSONDecodeError:\n            return\n\n\ndef update_node_data(node, knobname, data):\n    \"\"\"Update already present data.\n\n    Args:\n        node (nuke.Node): node object\n        knobname (str): knob name\n        data (dict): data to update knob value\n    \"\"\"\n    knob = node[knobname]\n    node_data = get_node_data(node, knobname) or {}\n    node_data.update(data)\n    knob_value = JSON_PREFIX + json.dumps(node_data)\n    knob.setValue(knob_value)\n\n\nclass Knobby(object):\n    \"\"\"[DEPRECATED] For creating knob which it's type isn't\n                    mapped in `create_knobs`\n\n    Args:\n        type (string): Nuke knob type name\n        value: Value to be set with `Knob.setValue`, put `None` if not required\n        flags (list, optional): Knob flags to be set with `Knob.setFlag`\n        *args: Args other than knob name for initializing knob class\n\n    \"\"\"\n\n    def __init__(self, type, value, flags=None, *args):\n        self.type = type\n        self.value = value\n        self.flags = flags or []\n        self.args = args\n\n    def create(self, name, nice=None):\n        knob_cls = getattr(nuke, self.type)\n        knob = knob_cls(name, nice, *self.args)\n        if self.value is not None:\n            knob.setValue(self.value)\n        for flag in self.flags:\n            knob.setFlag(flag)\n        return knob\n\n    @staticmethod\n    def nice_naming(key):\n        \"\"\"Convert camelCase name into UI Display Name\"\"\"\n        words = re.findall('[A-Z][^A-Z]*', key[0].upper() + key[1:])\n        return \" \".join(words)\n\n\ndef create_knobs(data, tab=None):\n    \"\"\"Create knobs by data\n\n    Depending on the type of each dict value and creates the correct Knob.\n\n    Mapped types:\n        bool: nuke.Boolean_Knob\n        int: nuke.Int_Knob\n        float: nuke.Double_Knob\n        list: nuke.Enumeration_Knob\n        six.string_types: nuke.String_Knob\n\n        dict: If it's a nested dict (all values are dict), will turn into\n            A tabs group. Or just a knobs group.\n\n    Args:\n        data (dict): collection of attributes and their value\n        tab (string, optional): Knobs' tab name\n\n    Returns:\n        list: A list of `nuke.Knob` objects\n\n    \"\"\"\n    def nice_naming(key):\n        \"\"\"Convert camelCase name into UI Display Name\"\"\"\n        words = re.findall('[A-Z][^A-Z]*', key[0].upper() + key[1:])\n        return \" \".join(words)\n\n    # Turn key-value pairs into knobs\n    knobs = list()\n\n    if tab:\n        knobs.append(nuke.Tab_Knob(tab))\n\n    for key, value in data.items():\n        # Knob name\n        if isinstance(key, tuple):\n            name, nice = key\n        else:\n            name, nice = key, nice_naming(key)\n\n        # Create knob by value type\n        if isinstance(value, Knobby):\n            knobby = value\n            knob = knobby.create(name, nice)\n\n        elif isinstance(value, float):\n            knob = nuke.Double_Knob(name, nice)\n            knob.setValue(value)\n\n        elif isinstance(value, bool):\n            knob = nuke.Boolean_Knob(name, nice)\n            knob.setValue(value)\n            knob.setFlag(nuke.STARTLINE)\n\n        elif isinstance(value, int):\n            knob = nuke.Int_Knob(name, nice)\n            knob.setValue(value)\n\n        elif isinstance(value, six.string_types):\n            knob = nuke.String_Knob(name, nice)\n            knob.setValue(value)\n\n        elif isinstance(value, list):\n            knob = nuke.Enumeration_Knob(name, nice, value)\n\n        elif isinstance(value, dict):\n            if all(isinstance(v, dict) for v in value.values()):\n                # Create a group of tabs\n                begain = nuke.BeginTabGroup_Knob()\n                end = nuke.EndTabGroup_Knob()\n                begain.setName(name)\n                end.setName(name + \"_End\")\n                knobs.append(begain)\n                for k, v in value.items():\n                    knobs += create_knobs(v, tab=k)\n                knobs.append(end)\n            else:\n                # Create a group of knobs\n                knobs.append(nuke.Tab_Knob(\n                    name, nice, nuke.TABBEGINCLOSEDGROUP))\n                knobs += create_knobs(value)\n                knobs.append(\n                    nuke.Tab_Knob(name + \"_End\", nice, nuke.TABENDGROUP))\n            continue\n\n        else:\n            raise TypeError(\"Unsupported type: %r\" % type(value))\n\n        knobs.append(knob)\n\n    return knobs\n\n\ndef imprint(node, data, tab=None):\n    \"\"\"Store attributes with value on node\n\n    Parse user data into Node knobs.\n    Use `collections.OrderedDict` to ensure knob order.\n\n    Args:\n        node(nuke.Node): node object from Nuke\n        data(dict): collection of attributes and their value\n\n    Returns:\n        None\n\n    Examples:\n        ```\n        import nuke\n        from openpype.hosts.nuke.api import lib\n\n        node = nuke.createNode(\"NoOp\")\n        data = {\n            # Regular type of attributes\n            \"myList\": [\"x\", \"y\", \"z\"],\n            \"myBool\": True,\n            \"myFloat\": 0.1,\n            \"myInt\": 5,\n\n            # Creating non-default imprint type of knob\n            \"MyFilePath\": lib.Knobby(\"File_Knob\", \"/file/path\"),\n            \"divider\": lib.Knobby(\"Text_Knob\", \"\"),\n\n            # Manual nice knob naming\n            (\"my_knob\", \"Nice Knob Name\"): \"some text\",\n\n            # dict type will be created as knob group\n            \"KnobGroup\": {\n                \"knob1\": 5,\n                \"knob2\": \"hello\",\n                \"knob3\": [\"a\", \"b\"],\n            },\n\n            # Nested dict will be created as tab group\n            \"TabGroup\": {\n                \"tab1\": {\"count\": 5},\n                \"tab2\": {\"isGood\": True},\n                \"tab3\": {\"direction\": [\"Left\", \"Right\"]},\n            },\n        }\n        lib.imprint(node, data, tab=\"Demo\")\n\n        ```\n\n    \"\"\"\n    for knob in create_knobs(data, tab):\n        node.addKnob(knob)\n\n\n@deprecated\ndef add_publish_knob(node):\n    \"\"\"[DEPRECATED] Add Publish knob to node\n\n    Arguments:\n        node (nuke.Node): nuke node to be processed\n\n    Returns:\n        node (nuke.Node): processed nuke node\n\n    \"\"\"\n    if \"publish\" not in node.knobs():\n        body = OrderedDict()\n        body[(\"divd\", \"Publishing\")] = Knobby(\"Text_Knob\", '')\n        body[\"publish\"] = True\n        imprint(node, body)\n    return node\n\n\n@deprecated(\"openpype.hosts.nuke.api.lib.set_node_data\")\ndef set_avalon_knob_data(node, data=None, prefix=\"avalon:\"):\n    \"\"\"[DEPRECATED] Sets data into nodes's avalon knob\n\n    This function is still used but soon will be deprecated.\n    Use `set_node_data` instead.\n\n    Arguments:\n        node (nuke.Node): Nuke node to imprint with data,\n        data (dict, optional): Data to be imprinted into AvalonTab\n        prefix (str, optional): filtering prefix\n\n    Returns:\n        node (nuke.Node)\n\n    Examples:\n        data = {\n            'asset': 'sq020sh0280',\n            'family': 'render',\n            'subset': 'subsetMain'\n        }\n    \"\"\"\n    data = data or dict()\n    create = OrderedDict()\n\n    tab_name = AVALON_TAB\n    editable = [\"asset\", \"subset\", \"name\", \"namespace\"]\n\n    existed_knobs = node.knobs()\n\n    for key, value in data.items():\n        knob_name = prefix + key\n        gui_name = key\n\n        if knob_name in existed_knobs:\n            # Set value\n            try:\n                node[knob_name].setValue(value)\n            except TypeError:\n                node[knob_name].setValue(str(value))\n        else:\n            # New knob\n            name = (knob_name, gui_name)  # Hide prefix on GUI\n            if key in editable:\n                create[name] = value\n            else:\n                create[name] = Knobby(\"String_Knob\",\n                                      str(value),\n                                      flags=[nuke.READ_ONLY])\n    if tab_name in existed_knobs:\n        tab_name = None\n    else:\n        tab = OrderedDict()\n        warn = Knobby(\"Text_Knob\", \"Warning! Do not change following data!\")\n        divd = Knobby(\"Text_Knob\", \"\")\n        head = [\n            ((\"warn\", \"\"), warn),\n            ((\"divd\", \"\"), divd),\n        ]\n        tab[AVALON_DATA_GROUP] = OrderedDict(head + list(create.items()))\n        create = tab\n\n    imprint(node, create, tab=tab_name)\n    return node\n\n\n@deprecated(\"openpype.hosts.nuke.api.lib.get_node_data\")\ndef get_avalon_knob_data(node, prefix=\"avalon:\", create=True):\n    \"\"\"[DEPRECATED]  Gets a data from nodes's avalon knob\n\n    This function is still used but soon will be deprecated.\n    Use `get_node_data` instead.\n\n    Arguments:\n        node (obj): Nuke node to search for data,\n        prefix (str, optional): filtering prefix\n\n    Returns:\n        data (dict)\n    \"\"\"\n\n    data = {}\n    if AVALON_TAB not in node.knobs():\n        return data\n\n    # check if lists\n    if not isinstance(prefix, list):\n        prefix = [prefix]\n\n    # loop prefix\n    for p in prefix:\n        # check if the node is avalon tracked\n        try:\n            # check if data available on the node\n            test = node[AVALON_DATA_GROUP].value()\n            log.debug(\"Only testing if data available: `{}`\".format(test))\n        except NameError as e:\n            # if it doesn't then create it\n            log.debug(\"Creating avalon knob: `{}`\".format(e))\n            if create:\n                node = set_avalon_knob_data(node)\n                return get_avalon_knob_data(node)\n            return {}\n\n        # get data from filtered knobs\n        data.update({k.replace(p, ''): node[k].value()\n                    for k in node.knobs().keys()\n                    if p in k})\n\n    return data\n\n\n@deprecated\ndef fix_data_for_node_create(data):\n    \"\"\"[DEPRECATED] Fixing data to be used for nuke knobs\n    \"\"\"\n    for k, v in data.items():\n        if isinstance(v, six.text_type):\n            data[k] = str(v)\n        if str(v).startswith(\"0x\"):\n            data[k] = int(v, 16)\n    return data\n\n\n@deprecated\ndef add_write_node_legacy(name, **kwarg):\n    \"\"\"[DEPRECATED] Adding nuke write node\n    Arguments:\n        name (str): nuke node name\n        kwarg (attrs): data for nuke knobs\n    Returns:\n        node (obj): nuke write node\n    \"\"\"\n    use_range_limit = kwarg.get(\"use_range_limit\", None)\n\n    w = nuke.createNode(\n        \"Write\",\n        \"name {}\".format(name),\n        inpanel=False\n    )\n\n    w[\"file\"].setValue(kwarg[\"file\"])\n\n    for k, v in kwarg.items():\n        if \"frame_range\" in k:\n            continue\n        log.info([k, v])\n        try:\n            w[k].setValue(v)\n        except KeyError as e:\n            log.debug(e)\n            continue\n\n    if use_range_limit:\n        w[\"use_limit\"].setValue(True)\n        w[\"first\"].setValue(kwarg[\"frame_range\"][0])\n        w[\"last\"].setValue(kwarg[\"frame_range\"][1])\n\n    return w\n\n\ndef add_write_node(name, file_path, knobs, **kwarg):\n    \"\"\"Adding nuke write node\n\n    Arguments:\n        name (str): nuke node name\n        kwarg (attrs): data for nuke knobs\n\n    Returns:\n        node (obj): nuke write node\n    \"\"\"\n    use_range_limit = kwarg.get(\"use_range_limit\", None)\n\n    w = nuke.createNode(\n        \"Write\",\n        \"name {}\".format(name),\n        inpanel=False\n    )\n\n    w[\"file\"].setValue(file_path)\n\n    # finally add knob overrides\n    set_node_knobs_from_settings(w, knobs, **kwarg)\n\n    if use_range_limit:\n        w[\"use_limit\"].setValue(True)\n        w[\"first\"].setValue(kwarg[\"frame_range\"][0])\n        w[\"last\"].setValue(kwarg[\"frame_range\"][1])\n\n    return w\n\n\ndef read_avalon_data(node):\n    \"\"\"Return user-defined knobs from given `node`\n\n    Args:\n        node (nuke.Node): Nuke node object\n\n    Returns:\n        list: A list of nuke.Knob object\n\n    \"\"\"\n    def compat_prefixed(knob_name):\n        if knob_name.startswith(\"avalon:\"):\n            return knob_name[len(\"avalon:\"):]\n        elif knob_name.startswith(\"ak:\"):\n            return knob_name[len(\"ak:\"):]\n\n    data = dict()\n\n    pattern = (\"(?<=addUserKnob {)\"\n               \"([0-9]*) (\\\\S*)\"  # Matching knob type and knob name\n               \"(?=[ |}])\")\n    tcl_script = node.writeKnobs(nuke.WRITE_USER_KNOB_DEFS)\n    result = re.search(pattern, tcl_script)\n\n    if result:\n        first_user_knob = result.group(2)\n        # Collect user knobs from the end of the knob list\n        for knob in reversed(node.allKnobs()):\n            knob_name = knob.name()\n            if not knob_name:\n                # Ignore unnamed knob\n                continue\n\n            knob_type = nuke.knob(knob.fullyQualifiedName(), type=True)\n            value = knob.value()\n\n            if (\n                knob_type not in EXCLUDED_KNOB_TYPE_ON_READ or\n                # For compating read-only string data that imprinted\n                # by `nuke.Text_Knob`.\n                (knob_type == 26 and value)\n            ):\n                key = compat_prefixed(knob_name)\n                if key is not None:\n                    data[key] = value\n\n            if knob_name == first_user_knob:\n                break\n\n    return data\n\n\ndef get_node_path(path, padding=4):\n    \"\"\"Get filename for the Nuke write with padded number as '#'\n\n    Arguments:\n        path (str): The path to render to.\n\n    Returns:\n        tuple: head, padding, tail (extension)\n\n    Examples:\n        >>> get_frame_path(\"test.exr\")\n        ('test', 4, '.exr')\n\n        >>> get_frame_path(\"filename.#####.tif\")\n        ('filename.', 5, '.tif')\n\n        >>> get_frame_path(\"foobar##.tif\")\n        ('foobar', 2, '.tif')\n\n        >>> get_frame_path(\"foobar_%08d.tif\")\n        ('foobar_', 8, '.tif')\n    \"\"\"\n    filename, ext = os.path.splitext(path)\n\n    # Find a final number group\n    if '%' in filename:\n        match = re.match('.*?(%[0-9]+d)$', filename)\n        if match:\n            padding = int(match.group(1).replace('%', '').replace('d', ''))\n            # remove number from end since fusion\n            # will swap it with the frame number\n            filename = filename.replace(match.group(1), '')\n    elif '#' in filename:\n        match = re.match('.*?(#+)$', filename)\n\n        if match:\n            padding = len(match.group(1))\n            # remove number from end since fusion\n            # will swap it with the frame number\n            filename = filename.replace(match.group(1), '')\n\n    return filename, padding, ext\n\n\ndef get_nuke_imageio_settings():\n    return get_project_settings(Context.project_name)[\"nuke\"][\"imageio\"]\n\n\n@deprecated(\"openpype.hosts.nuke.api.lib.get_nuke_imageio_settings\")\ndef get_created_node_imageio_setting_legacy(nodeclass, creator, subset):\n    '''[DEPRECATED]  Get preset data for dataflow (fileType, compression, bitDepth)\n    '''\n\n    assert any([creator, nodeclass]), nuke.message(\n        \"`{}`: Missing mandatory kwargs `host`, `cls`\".format(__file__))\n\n    imageio_nodes = get_nuke_imageio_settings()[\"nodes\"]\n    required_nodes = imageio_nodes[\"requiredNodes\"]\n\n    # HACK: for backward compatibility this needs to be optional\n    override_nodes = imageio_nodes.get(\"overrideNodes\", [])\n\n    imageio_node = None\n    for node in required_nodes:\n        log.info(node)\n        if (\n                nodeclass in node[\"nukeNodeClass\"]\n                and creator in node[\"plugins\"]\n        ):\n            imageio_node = node\n            break\n\n    log.debug(\"__ imageio_node: {}\".format(imageio_node))\n\n    # find matching override node\n    override_imageio_node = None\n    for onode in override_nodes:\n        log.info(onode)\n        if nodeclass not in node[\"nukeNodeClass\"]:\n            continue\n\n        if creator not in node[\"plugins\"]:\n            continue\n\n        if (\n            onode[\"subsets\"]\n            and not any(\n                re.search(s.lower(), subset.lower())\n                for s in onode[\"subsets\"]\n            )\n        ):\n            continue\n\n        override_imageio_node = onode\n        break\n\n    log.debug(\"__ override_imageio_node: {}\".format(override_imageio_node))\n    # add overrides to imageio_node\n    if override_imageio_node:\n        # get all knob names in imageio_node\n        knob_names = [k[\"name\"] for k in imageio_node[\"knobs\"]]\n\n        for oknob in override_imageio_node[\"knobs\"]:\n            for knob in imageio_node[\"knobs\"]:\n                # override matching knob name\n                if oknob[\"name\"] == knob[\"name\"]:\n                    log.debug(\n                        \"_ overriding knob: `{}` > `{}`\".format(\n                            knob, oknob\n                        ))\n                    if not oknob[\"value\"]:\n                        # remove original knob if no value found in oknob\n                        imageio_node[\"knobs\"].remove(knob)\n                    else:\n                        # override knob value with oknob's\n                        knob[\"value\"] = oknob[\"value\"]\n\n                # add missing knobs into imageio_node\n                if oknob[\"name\"] not in knob_names:\n                    log.debug(\n                        \"_ adding knob: `{}`\".format(oknob))\n                    imageio_node[\"knobs\"].append(oknob)\n                    knob_names.append(oknob[\"name\"])\n\n    log.info(\"ImageIO node: {}\".format(imageio_node))\n    return imageio_node\n\n\ndef get_imageio_node_setting(node_class, plugin_name, subset):\n    ''' Get preset data for dataflow (fileType, compression, bitDepth)\n    '''\n    imageio_nodes = get_nuke_imageio_settings()[\"nodes\"]\n    required_nodes = imageio_nodes[\"requiredNodes\"]\n\n    imageio_node = None\n    for node in required_nodes:\n        log.info(node)\n        if (\n                node_class in node[\"nukeNodeClass\"]\n                and plugin_name in node[\"plugins\"]\n        ):\n            imageio_node = node\n            break\n\n    log.debug(\"__ imageio_node: {}\".format(imageio_node))\n\n    if not imageio_node:\n        return\n\n    # find overrides and update knobs with them\n    get_imageio_node_override_setting(\n        node_class,\n        plugin_name,\n        subset,\n        imageio_node[\"knobs\"]\n    )\n\n    log.info(\"ImageIO node: {}\".format(imageio_node))\n    return imageio_node\n\n\ndef get_imageio_node_override_setting(\n    node_class, plugin_name, subset, knobs_settings\n):\n    ''' Get imageio node overrides from settings\n    '''\n    imageio_nodes = get_nuke_imageio_settings()[\"nodes\"]\n    override_nodes = imageio_nodes[\"overrideNodes\"]\n\n    # find matching override node\n    override_imageio_node = None\n    for onode in override_nodes:\n        log.debug(\"__ onode: {}\".format(onode))\n        log.debug(\"__ subset: {}\".format(subset))\n        if node_class not in onode[\"nukeNodeClass\"]:\n            continue\n\n        if plugin_name not in onode[\"plugins\"]:\n            continue\n\n        if (\n            onode[\"subsets\"]\n            and not any(\n                re.search(s.lower(), subset.lower())\n                for s in onode[\"subsets\"]\n            )\n        ):\n            continue\n\n        override_imageio_node = onode\n        break\n\n    log.debug(\"__ override_imageio_node: {}\".format(override_imageio_node))\n    # add overrides to imageio_node\n    if override_imageio_node:\n        # get all knob names in imageio_node\n        knob_names = [k[\"name\"] for k in knobs_settings]\n\n        for oknob in override_imageio_node[\"knobs\"]:\n            for knob in knobs_settings:\n                # override matching knob name\n                if oknob[\"name\"] == knob[\"name\"]:\n                    log.debug(\n                        \"_ overriding knob: `{}` > `{}`\".format(\n                            knob, oknob\n                        ))\n                    if not oknob[\"value\"]:\n                        # remove original knob if no value found in oknob\n                        knobs_settings.remove(knob)\n                    else:\n                        # override knob value with oknob's\n                        knob[\"value\"] = oknob[\"value\"]\n\n                # add missing knobs into imageio_node\n                if oknob[\"name\"] not in knob_names:\n                    log.debug(\n                        \"_ adding knob: `{}`\".format(oknob))\n                    knobs_settings.append(oknob)\n                    knob_names.append(oknob[\"name\"])\n\n    return knobs_settings\n\n\ndef get_imageio_input_colorspace(filename):\n    ''' Get input file colorspace based on regex in settings.\n    '''\n    imageio_regex_inputs = (\n        get_nuke_imageio_settings()[\"regexInputs\"][\"inputs\"])\n\n    preset_clrsp = None\n    for regexInput in imageio_regex_inputs:\n        if bool(re.search(regexInput[\"regex\"], filename)):\n            preset_clrsp = str(regexInput[\"colorspace\"])\n\n    return preset_clrsp\n\n\ndef get_view_process_node():\n    reset_selection()\n\n    ipn_node = None\n    for v_ in nuke.allNodes(filter=\"Viewer\"):\n        ipn = v_['input_process_node'].getValue()\n        ipn_node = nuke.toNode(ipn)\n\n        # skip if no input node is set\n        if not ipn:\n            continue\n\n        if ipn == \"VIEWER_INPUT\" and not ipn_node:\n            # since it is set by default we can ignore it\n            # nobody usually use this but use it if\n            # it exists in nodes\n            continue\n\n        if not ipn_node:\n            # in case a Viewer node is transferred from\n            # different workfile with old values\n            raise NameError((\n                \"Input process node name '{}' set in \"\n                \"Viewer '{}' is doesn't exists in nodes\"\n            ).format(ipn, v_.name()))\n\n        ipn_node.setSelected(True)\n\n    if ipn_node:\n        return duplicate_node(ipn_node)\n\n\ndef on_script_load():\n    ''' Callback for ffmpeg support\n    '''\n    if nuke.env[\"LINUX\"]:\n        nuke.tcl('load ffmpegReader')\n        nuke.tcl('load ffmpegWriter')\n    else:\n        nuke.tcl('load movReader')\n        nuke.tcl('load movWriter')\n\n\ndef check_inventory_versions():\n    \"\"\"\n    Actual version idetifier of Loaded containers\n\n    Any time this function is run it will check all nodes and filter only\n    Loader nodes for its version. It will get all versions from database\n    and check if the node is having actual version. If not then it will color\n    it to red.\n    \"\"\"\n    from .pipeline import parse_container\n\n    # get all Loader nodes by avalon attribute metadata\n    node_with_repre_id = []\n    repre_ids = set()\n    # Find all containers and collect it's node and representation ids\n    for node in nuke.allNodes():\n        container = parse_container(node)\n\n        if container:\n            node = nuke.toNode(container[\"objectName\"])\n            avalon_knob_data = read_avalon_data(node)\n            repre_id = avalon_knob_data[\"representation\"]\n\n            repre_ids.add(repre_id)\n            node_with_repre_id.append((node, repre_id))\n\n    # Skip if nothing was found\n    if not repre_ids:\n        return\n\n    project_name = get_current_project_name()\n    # Find representations based on found containers\n    repre_docs = get_representations(\n        project_name,\n        representation_ids=repre_ids,\n        fields=[\"_id\", \"parent\"]\n    )\n    # Store representations by id and collect version ids\n    repre_docs_by_id = {}\n    version_ids = set()\n    for repre_doc in repre_docs:\n        # Use stringed representation id to match value in containers\n        repre_id = str(repre_doc[\"_id\"])\n        repre_docs_by_id[repre_id] = repre_doc\n        version_ids.add(repre_doc[\"parent\"])\n\n    version_docs = get_versions(\n        project_name, version_ids, fields=[\"_id\", \"name\", \"parent\"]\n    )\n    # Store versions by id and collect subset ids\n    version_docs_by_id = {}\n    subset_ids = set()\n    for version_doc in version_docs:\n        version_docs_by_id[version_doc[\"_id\"]] = version_doc\n        subset_ids.add(version_doc[\"parent\"])\n\n    # Query last versions based on subset ids\n    last_versions_by_subset_id = get_last_versions(\n        project_name, subset_ids=subset_ids, fields=[\"_id\", \"parent\"]\n    )\n\n    # Loop through collected container nodes and their representation ids\n    for item in node_with_repre_id:\n        # Some python versions of nuke can't unfold tuple in for loop\n        node, repre_id = item\n        repre_doc = repre_docs_by_id.get(repre_id)\n        # Failsafe for not finding the representation.\n        if not repre_doc:\n            log.warning((\n                \"Could not find the representation on node \\\"{}\\\"\"\n            ).format(node.name()))\n            continue\n\n        version_id = repre_doc[\"parent\"]\n        version_doc = version_docs_by_id.get(version_id)\n        if not version_doc:\n            log.warning((\n                \"Could not find the version on node \\\"{}\\\"\"\n            ).format(node.name()))\n            continue\n\n        # Get last version based on subset id\n        subset_id = version_doc[\"parent\"]\n        last_version = last_versions_by_subset_id[subset_id]\n        # Check if last version is same as current version\n        if last_version[\"_id\"] == version_doc[\"_id\"]:\n            color_value = \"0x4ecd25ff\"\n        else:\n            color_value = \"0xd84f20ff\"\n        node[\"tile_color\"].setValue(int(color_value, 16))\n\n\ndef writes_version_sync():\n    ''' Callback synchronizing version of publishable write nodes\n    '''\n    try:\n        rootVersion = get_version_from_path(nuke.root().name())\n        padding = len(rootVersion)\n        new_version = \"v\" + str(\"{\" + \":0>{}\".format(padding) + \"}\").format(\n            int(rootVersion)\n        )\n        log.debug(\"new_version: {}\".format(new_version))\n    except Exception:\n        return\n\n    for each in nuke.allNodes(filter=\"Write\"):\n        # check if the node is avalon tracked\n        if _NODE_TAB_NAME not in each.knobs():\n            continue\n\n        avalon_knob_data = read_avalon_data(each)\n\n        try:\n            if avalon_knob_data[\"families\"] not in [\"render\"]:\n                log.debug(avalon_knob_data[\"families\"])\n                continue\n\n            node_file = each[\"file\"].value()\n\n            node_version = \"v\" + get_version_from_path(node_file)\n            log.debug(\"node_version: {}\".format(node_version))\n\n            node_new_file = node_file.replace(node_version, new_version)\n            each[\"file\"].setValue(node_new_file)\n            if not os.path.isdir(os.path.dirname(node_new_file)):\n                log.warning(\"Path does not exist! I am creating it.\")\n                os.makedirs(os.path.dirname(node_new_file))\n        except Exception as e:\n            log.warning(\n                \"Write node: `{}` has no version in path: {}\".format(\n                    each.name(), e))\n\n\ndef version_up_script():\n    ''' Raising working script's version\n    '''\n    import nukescripts\n    nukescripts.script_and_write_nodes_version_up()\n\n\ndef check_subsetname_exists(nodes, subset_name):\n    \"\"\"\n    Checking if node is not already created to secure there is no duplicity\n\n    Arguments:\n        nodes (list): list of nuke.Node objects\n        subset_name (str): name we try to find\n\n    Returns:\n        bool: True of False\n    \"\"\"\n    return next((True for n in nodes\n                 if subset_name in read_avalon_data(n).get(\"subset\", \"\")),\n                False)\n\n\ndef format_anatomy(data):\n    ''' Helping function for formatting of anatomy paths\n\n    Arguments:\n        data (dict): dictionary with attributes used for formatting\n\n    Return:\n        path (str)\n    '''\n\n    project_name = get_current_project_name()\n    anatomy = Anatomy(project_name)\n    log.debug(\"__ anatomy.templates: {}\".format(anatomy.templates))\n\n    padding = None\n    if \"frame_padding\" in anatomy.templates.keys():\n        padding = int(anatomy.templates[\"frame_padding\"])\n    elif \"render\" in anatomy.templates.keys():\n        padding = int(\n            anatomy.templates[\"render\"].get(\n                \"frame_padding\"\n            )\n        )\n\n    version = data.get(\"version\", None)\n    if not version:\n        file = script_name()\n        data[\"version\"] = get_version_from_path(file)\n\n    if AYON_SERVER_ENABLED:\n        asset_name = data[\"folderPath\"]\n    else:\n        asset_name = data[\"asset\"]\n    task_name = data[\"task\"]\n    host_name = get_current_host_name()\n    context_data = get_template_data_with_names(\n        project_name, asset_name, task_name, host_name\n    )\n    data.update(context_data)\n    data.update({\n        \"subset\": data[\"subset\"],\n        \"family\": data[\"family\"],\n        \"frame\": \"#\" * padding,\n    })\n    return anatomy.format(data)\n\n\ndef script_name():\n    ''' Returns nuke script path\n    '''\n    return nuke.root().knob(\"name\").value()\n\n\ndef add_button_write_to_read(node):\n    name = \"createReadNode\"\n    label = \"Read From Rendered\"\n    value = \"import write_to_read;\\\n        write_to_read.write_to_read(nuke.thisNode(), allow_relative=False)\"\n    knob = nuke.PyScript_Knob(name, label, value)\n    knob.clearFlag(nuke.STARTLINE)\n    node.addKnob(knob)\n\n\ndef add_button_clear_rendered(node, path):\n    name = \"clearRendered\"\n    label = \"Clear Rendered\"\n    value = \"import clear_rendered;\\\n        clear_rendered.clear_rendered(\\\"{}\\\")\".format(path)\n    knob = nuke.PyScript_Knob(name, label, value)\n    node.addKnob(knob)\n\n\ndef create_prenodes(\n    prev_node,\n    nodes_setting,\n    plugin_name=None,\n    subset=None,\n    **kwargs\n):\n    last_node = None\n    for_dependency = {}\n    for name, node in nodes_setting.items():\n        # get attributes\n        nodeclass = node[\"nodeclass\"]\n        knobs = node[\"knobs\"]\n\n        # create node\n        now_node = nuke.createNode(\n            nodeclass,\n            \"name {}\".format(name),\n            inpanel=False\n        )\n\n        # add for dependency linking\n        for_dependency[name] = {\n            \"node\": now_node,\n            \"dependent\": node[\"dependent\"]\n        }\n\n        if all([plugin_name, subset]):\n            # find imageio overrides\n            get_imageio_node_override_setting(\n                now_node.Class(),\n                plugin_name,\n                subset,\n                knobs\n            )\n\n        # add data to knob\n        set_node_knobs_from_settings(now_node, knobs, **kwargs)\n\n        # switch actual node to previous\n        last_node = now_node\n\n    for _node_name, node_prop in for_dependency.items():\n        if not node_prop[\"dependent\"]:\n            node_prop[\"node\"].setInput(\n                0, prev_node)\n        elif node_prop[\"dependent\"] in for_dependency:\n            _prev_node = for_dependency[node_prop[\"dependent\"]][\"node\"]\n            node_prop[\"node\"].setInput(\n                0, _prev_node)\n        else:\n            log.warning(\"Dependency has wrong name of node: {}\".format(\n                node_prop\n            ))\n\n    return last_node\n\n\ndef create_write_node(\n    name,\n    data,\n    input=None,\n    prenodes=None,\n    linked_knobs=None,\n    **kwargs\n):\n    ''' Creating write node which is group node\n\n    Arguments:\n        name (str): name of node\n        data (dict): creator write instance data\n        input (node)[optional]: selected node to connect to\n        prenodes (dict)[optional]:\n            nodes to be created before write with dependency\n        review (bool)[optional]: adding review knob\n        farm (bool)[optional]: rendering workflow target\n        kwargs (dict)[optional]: additional key arguments for formatting\n\n    Example:\n        prenodes = {\n            \"nodeName\": {\n                \"nodeclass\": \"Reformat\",\n                \"dependent\": [\n                    following_node_01,\n                    ...\n                ],\n                \"knobs\": [\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"knobname\",\n                        \"value\": \"knob value\"\n                    },\n                    ...\n                ]\n            },\n            ...\n        }\n\n\n    Return:\n        node (obj): group node with avalon data as Knobs\n    '''\n    prenodes = prenodes or {}\n\n    # filtering variables\n    plugin_name = data[\"creator\"]\n    subset = data[\"subset\"]\n\n    # get knob settings for write node\n    imageio_writes = get_imageio_node_setting(\n        node_class=\"Write\",\n        plugin_name=plugin_name,\n        subset=subset\n    )\n\n    for knob in imageio_writes[\"knobs\"]:\n        if knob[\"name\"] == \"file_type\":\n            ext = knob[\"value\"]\n\n    data.update({\n        \"imageio_writes\": imageio_writes,\n        \"ext\": ext\n    })\n    anatomy_filled = format_anatomy(data)\n\n    # build file path to workfiles\n    fdir = str(anatomy_filled[\"work\"][\"folder\"]).replace(\"\\\\\", \"/\")\n    data[\"work\"] = fdir\n    fpath = StringTemplate(data[\"fpath_template\"]).format_strict(data)\n\n    # create directory\n    if not os.path.isdir(os.path.dirname(fpath)):\n        log.warning(\"Path does not exist! I am creating it.\")\n        os.makedirs(os.path.dirname(fpath))\n\n    GN = nuke.createNode(\"Group\", \"name {}\".format(name))\n\n    prev_node = None\n    with GN:\n        if input:\n            input_name = str(input.name()).replace(\" \", \"\")\n            # if connected input node was defined\n            prev_node = nuke.createNode(\n                \"Input\",\n                \"name {}\".format(input_name),\n                inpanel=False\n            )\n        else:\n            # generic input node connected to nothing\n            prev_node = nuke.createNode(\n                \"Input\",\n                \"name {}\".format(\"rgba\"),\n                inpanel=False\n            )\n\n        # creating pre-write nodes `prenodes`\n        last_prenode = create_prenodes(\n            prev_node,\n            prenodes,\n            plugin_name,\n            subset,\n            **kwargs\n        )\n        if last_prenode:\n            prev_node = last_prenode\n\n        # creating write node\n        write_node = now_node = add_write_node(\n            \"inside_{}\".format(name),\n            fpath,\n            imageio_writes[\"knobs\"],\n            **data\n        )\n        # connect to previous node\n        now_node.setInput(0, prev_node)\n\n        # switch actual node to previous\n        prev_node = now_node\n\n        now_node = nuke.createNode(\"Output\", \"name Output1\", inpanel=False)\n\n        # connect to previous node\n        now_node.setInput(0, prev_node)\n\n    # add divider\n    GN.addKnob(nuke.Text_Knob('', 'Rendering'))\n\n    # Add linked knobs.\n    linked_knob_names = []\n\n    # add input linked knobs and create group only if any input\n    if linked_knobs:\n        linked_knob_names.append(\"_grp-start_\")\n        linked_knob_names.extend(linked_knobs)\n        linked_knob_names.append(\"_grp-end_\")\n\n    linked_knob_names.append(\"Render\")\n\n    for _k_name in linked_knob_names:\n        if \"_grp-start_\" in _k_name:\n            knob = nuke.Tab_Knob(\n                \"rnd_attr\", \"Rendering attributes\", nuke.TABBEGINCLOSEDGROUP)\n            GN.addKnob(knob)\n        elif \"_grp-end_\" in _k_name:\n            knob = nuke.Tab_Knob(\n                \"rnd_attr_end\", \"Rendering attributes\", nuke.TABENDGROUP)\n            GN.addKnob(knob)\n        else:\n            if \"___\" in _k_name:\n                # add divider\n                GN.addKnob(nuke.Text_Knob(\"\"))\n            else:\n                # add linked knob by _k_name\n                link = nuke.Link_Knob(\"\")\n                link.makeLink(write_node.name(), _k_name)\n                link.setName(_k_name)\n\n                # make render\n                if \"Render\" in _k_name:\n                    link.setLabel(\"Render Local\")\n                link.setFlag(0x1000)\n                GN.addKnob(link)\n\n    # adding write to read button\n    add_button_write_to_read(GN)\n\n    # adding write to read button\n    add_button_clear_rendered(GN, os.path.dirname(fpath))\n\n    # set tile color\n    tile_color = next(\n        iter(\n            k[\"value\"] for k in imageio_writes[\"knobs\"]\n            if \"tile_color\" in k[\"name\"]\n        ), [255, 0, 0, 255]\n    )\n    GN[\"tile_color\"].setValue(\n        color_gui_to_int(tile_color))\n\n    return GN\n\n\n@deprecated(\"openpype.hosts.nuke.api.lib.create_write_node\")\ndef create_write_node_legacy(\n    name, data, input=None, prenodes=None,\n    review=True, linked_knobs=None, farm=True\n):\n    ''' Creating write node which is group node\n\n    Arguments:\n        name (str): name of node\n        data (dict): data to be imprinted\n        input (node): selected node to connect to\n        prenodes (list, optional): list of lists, definitions for nodes\n                                to be created before write\n        review (bool): adding review knob\n\n    Example:\n        prenodes = [\n            {\n                \"nodeName\": {\n                    \"class\": \"\"  # string\n                    \"knobs\": [\n                        (\"knobName\": value),\n                        ...\n                    ],\n                    \"dependent\": [\n                        following_node_01,\n                        ...\n                    ]\n                }\n            },\n            ...\n        ]\n\n    Return:\n        node (obj): group node with avalon data as Knobs\n    '''\n    knob_overrides = data.get(\"knobs\", [])\n    nodeclass = data[\"nodeclass\"]\n    creator = data[\"creator\"]\n    subset = data[\"subset\"]\n\n    imageio_writes = get_created_node_imageio_setting_legacy(\n        nodeclass, creator, subset\n    )\n    for knob in imageio_writes[\"knobs\"]:\n        if knob[\"name\"] == \"file_type\":\n            representation = knob[\"value\"]\n\n    host_name = get_current_host_name()\n    try:\n        data.update({\n            \"app\": host_name,\n            \"imageio_writes\": imageio_writes,\n            \"representation\": representation,\n        })\n        anatomy_filled = format_anatomy(data)\n\n    except Exception as e:\n        msg = \"problem with resolving anatomy template: {}\".format(e)\n        log.error(msg)\n        nuke.message(msg)\n\n    # build file path to workfiles\n    fdir = str(anatomy_filled[\"work\"][\"folder\"]).replace(\"\\\\\", \"/\")\n    fpath = data[\"fpath_template\"].format(\n        work=fdir, version=data[\"version\"], subset=data[\"subset\"],\n        frame=data[\"frame\"],\n        ext=representation\n    )\n\n    # create directory\n    if not os.path.isdir(os.path.dirname(fpath)):\n        log.warning(\"Path does not exist! I am creating it.\")\n        os.makedirs(os.path.dirname(fpath))\n\n    _data = OrderedDict({\n        \"file\": fpath\n    })\n\n    # adding dataflow template\n    log.debug(\"imageio_writes: `{}`\".format(imageio_writes))\n    for knob in imageio_writes[\"knobs\"]:\n        _data[knob[\"name\"]] = knob[\"value\"]\n\n    _data = fix_data_for_node_create(_data)\n\n    log.debug(\"_data: `{}`\".format(_data))\n\n    if \"frame_range\" in data.keys():\n        _data[\"frame_range\"] = data.get(\"frame_range\", None)\n        log.debug(\"_data[frame_range]: `{}`\".format(_data[\"frame_range\"]))\n\n    GN = nuke.createNode(\"Group\", \"name {}\".format(name))\n\n    prev_node = None\n    with GN:\n        if input:\n            input_name = str(input.name()).replace(\" \", \"\")\n            # if connected input node was defined\n            prev_node = nuke.createNode(\n                \"Input\", \"name {}\".format(input_name))\n        else:\n            # generic input node connected to nothing\n            prev_node = nuke.createNode(\n                \"Input\",\n                \"name {}\".format(\"rgba\"),\n                inpanel=False\n            )\n        # creating pre-write nodes `prenodes`\n        if prenodes:\n            for node in prenodes:\n                # get attributes\n                pre_node_name = node[\"name\"]\n                klass = node[\"class\"]\n                knobs = node[\"knobs\"]\n                dependent = node[\"dependent\"]\n\n                # create node\n                now_node = nuke.createNode(\n                    klass,\n                    \"name {}\".format(pre_node_name),\n                    inpanel=False\n                )\n\n                # add data to knob\n                for _knob in knobs:\n                    knob, value = _knob\n                    try:\n                        now_node[knob].value()\n                    except NameError:\n                        log.warning(\n                            \"knob `{}` does not exist on node `{}`\".format(\n                                knob, now_node[\"name\"].value()\n                            ))\n                        continue\n\n                    if not knob and not value:\n                        continue\n\n                    log.info((knob, value))\n\n                    if isinstance(value, str):\n                        if \"[\" in value:\n                            now_node[knob].setExpression(value)\n                    else:\n                        now_node[knob].setValue(value)\n\n                # connect to previous node\n                if dependent:\n                    if isinstance(dependent, (tuple or list)):\n                        for i, node_name in enumerate(dependent):\n                            input_node = nuke.createNode(\n                                \"Input\",\n                                \"name {}\".format(node_name),\n                                inpanel=False\n                            )\n                            now_node.setInput(1, input_node)\n\n                    elif isinstance(dependent, str):\n                        input_node = nuke.createNode(\n                            \"Input\",\n                            \"name {}\".format(node_name),\n                            inpanel=False\n                        )\n                        now_node.setInput(0, input_node)\n\n                else:\n                    now_node.setInput(0, prev_node)\n\n                # switch actual node to previous\n                prev_node = now_node\n\n        # creating write node\n\n        write_node = now_node = add_write_node_legacy(\n            \"inside_{}\".format(name),\n            **_data\n        )\n        # connect to previous node\n        now_node.setInput(0, prev_node)\n\n        # switch actual node to previous\n        prev_node = now_node\n\n        now_node = nuke.createNode(\"Output\", \"name Output1\", inpanel=False)\n\n        # connect to previous node\n        now_node.setInput(0, prev_node)\n\n    # imprinting group node\n    set_avalon_knob_data(GN, data[\"avalon\"])\n    add_publish_knob(GN)\n    add_rendering_knobs(GN, farm)\n\n    if review:\n        add_review_knob(GN)\n\n    # add divider\n    GN.addKnob(nuke.Text_Knob('', 'Rendering'))\n\n    # Add linked knobs.\n    linked_knob_names = []\n\n    # add input linked knobs and create group only if any input\n    if linked_knobs:\n        linked_knob_names.append(\"_grp-start_\")\n        linked_knob_names.extend(linked_knobs)\n        linked_knob_names.append(\"_grp-end_\")\n\n    linked_knob_names.append(\"Render\")\n\n    for _k_name in linked_knob_names:\n        if \"_grp-start_\" in _k_name:\n            knob = nuke.Tab_Knob(\n                \"rnd_attr\", \"Rendering attributes\", nuke.TABBEGINCLOSEDGROUP)\n            GN.addKnob(knob)\n        elif \"_grp-end_\" in _k_name:\n            knob = nuke.Tab_Knob(\n                \"rnd_attr_end\", \"Rendering attributes\", nuke.TABENDGROUP)\n            GN.addKnob(knob)\n        else:\n            if \"___\" in _k_name:\n                # add divider\n                GN.addKnob(nuke.Text_Knob(\"\"))\n            else:\n                # add linked knob by _k_name\n                link = nuke.Link_Knob(\"\")\n                link.makeLink(write_node.name(), _k_name)\n                link.setName(_k_name)\n\n                # make render\n                if \"Render\" in _k_name:\n                    link.setLabel(\"Render Local\")\n                link.setFlag(0x1000)\n                GN.addKnob(link)\n\n    # adding write to read button\n    add_button_write_to_read(GN)\n\n    # adding write to read button\n    add_button_clear_rendered(GN, os.path.dirname(fpath))\n\n    # Deadline tab.\n    add_deadline_tab(GN)\n\n    # open the our Tab as default\n    GN[_NODE_TAB_NAME].setFlag(0)\n\n    # set tile color\n    tile_color = _data.get(\"tile_color\", \"0xff0000ff\")\n    GN[\"tile_color\"].setValue(tile_color)\n\n    # override knob values from settings\n    for knob in knob_overrides:\n        knob_type = knob[\"type\"]\n        knob_name = knob[\"name\"]\n        knob_value = knob[\"value\"]\n        if knob_name not in GN.knobs():\n            continue\n        if not knob_value:\n            continue\n\n        # set correctly knob types\n        if knob_type == \"string\":\n            knob_value = str(knob_value)\n        if knob_type == \"number\":\n            knob_value = int(knob_value)\n        if knob_type == \"decimal_number\":\n            knob_value = float(knob_value)\n        if knob_type == \"bool\":\n            knob_value = bool(knob_value)\n        if knob_type in [\"2d_vector\", \"3d_vector\", \"color\", \"box\"]:\n            knob_value = list(knob_value)\n\n        GN[knob_name].setValue(knob_value)\n\n    return GN\n\n\ndef set_node_knobs_from_settings(node, knob_settings, **kwargs):\n    \"\"\" Overriding knob values from settings\n\n    Using `schema_nuke_knob_inputs` for knob type definitions.\n\n    Args:\n        node (nuke.Node): nuke node\n        knob_settings (list): list of dict. Keys are `type`, `name`, `value`\n        kwargs (dict)[optional]: keys for formattable knob settings\n    \"\"\"\n    for knob in knob_settings:\n        log.debug(\"__ knob: {}\".format(pformat(knob)))\n        knob_type = knob[\"type\"]\n        knob_name = knob[\"name\"]\n\n        if knob_name not in node.knobs():\n            continue\n\n        if knob_type == \"expression\":\n            knob_expression = knob[\"expression\"]\n            node[knob_name].setExpression(\n                knob_expression\n            )\n            continue\n\n        # first deal with formattable knob settings\n        if knob_type == \"formatable\":\n            template = knob[\"template\"]\n            to_type = knob[\"to_type\"]\n            try:\n                _knob_value = template.format(\n                    **kwargs\n                )\n            except KeyError as msg:\n                raise KeyError(\n                    \"Not able to format expression: {}\".format(msg))\n\n            # convert value to correct type\n            if to_type == \"2d_vector\":\n                knob_value = _knob_value.split(\";\").split(\",\")\n            else:\n                knob_value = _knob_value\n\n            knob_type = to_type\n\n        else:\n            knob_value = knob[\"value\"]\n\n        if not knob_value:\n            continue\n\n        knob_value = convert_knob_value_to_correct_type(\n            knob_type, knob_value)\n\n        node[knob_name].setValue(knob_value)\n\n\ndef convert_knob_value_to_correct_type(knob_type, knob_value):\n    # first convert string types to string\n    # just to ditch unicode\n    if isinstance(knob_value, six.text_type):\n        knob_value = str(knob_value)\n\n    # set correctly knob types\n    if knob_type == \"bool\":\n        knob_value = bool(knob_value)\n    elif knob_type == \"decimal_number\":\n        knob_value = float(knob_value)\n    elif knob_type == \"number\":\n        knob_value = int(knob_value)\n    elif knob_type == \"text\":\n        knob_value = knob_value\n    elif knob_type == \"color_gui\":\n        knob_value = color_gui_to_int(knob_value)\n    elif knob_type in [\"2d_vector\", \"3d_vector\", \"color\", \"box\"]:\n        knob_value = [float(val_) for val_ in knob_value]\n\n    return knob_value\n\n\ndef color_gui_to_int(color_gui):\n    hex_value = (\n        \"0x{0:0>2x}{1:0>2x}{2:0>2x}{3:0>2x}\").format(*color_gui)\n    return int(hex_value, 16)\n\n\n@deprecated\ndef add_rendering_knobs(node, farm=True):\n    ''' Adds additional rendering knobs to given node\n\n    Arguments:\n        node (obj): nuke node object to be fixed\n\n    Return:\n        node (obj): with added knobs\n    '''\n    knob_options = [\"Use existing frames\", \"Local\"]\n    if farm:\n        knob_options.append(\"On farm\")\n\n    if \"render\" not in node.knobs():\n        knob = nuke.Enumeration_Knob(\"render\", \"\", knob_options)\n        knob.clearFlag(nuke.STARTLINE)\n        node.addKnob(knob)\n    return node\n\n\n@deprecated\ndef add_review_knob(node):\n    ''' Adds additional review knob to given node\n\n    Arguments:\n        node (obj): nuke node object to be fixed\n\n    Return:\n        node (obj): with added knob\n    '''\n    if \"review\" not in node.knobs():\n        knob = nuke.Boolean_Knob(\"review\", \"Review\")\n        knob.setValue(True)\n        node.addKnob(knob)\n    return node\n\n\n@deprecated\ndef add_deadline_tab(node):\n    # TODO: remove this as it is only linked to legacy create\n    node.addKnob(nuke.Tab_Knob(\"Deadline\"))\n\n    knob = nuke.Int_Knob(\"deadlinePriority\", \"Priority\")\n    knob.setValue(50)\n    node.addKnob(knob)\n\n    knob = nuke.Int_Knob(\"deadlineChunkSize\", \"Chunk Size\")\n    knob.setValue(0)\n    node.addKnob(knob)\n\n    knob = nuke.Int_Knob(\"deadlineConcurrentTasks\", \"Concurrent tasks\")\n    # zero as default will get value from Settings during collection\n    # instead of being an explicit user override, see precollect_write.py\n    knob.setValue(0)\n    node.addKnob(knob)\n\n    knob = nuke.Text_Knob(\"divd\", '')\n    knob.setValue('')\n    node.addKnob(knob)\n\n    knob = nuke.Boolean_Knob(\"suspend_publish\", \"Suspend publish\")\n    knob.setValue(False)\n    node.addKnob(knob)\n\n\n@deprecated\ndef get_deadline_knob_names():\n    # TODO: remove this as it is only linked to legacy\n    # validate_write_deadline_tab\n    return [\n        \"Deadline\",\n        \"deadlineChunkSize\",\n        \"deadlinePriority\",\n        \"deadlineConcurrentTasks\"\n    ]\n\n\ndef create_backdrop(label=\"\", color=None, layer=0,\n                    nodes=None):\n    \"\"\"\n    Create Backdrop node\n\n    Arguments:\n        color (str): nuke compatible string with color code\n        layer (int): layer of node usually used (self.pos_layer - 1)\n        label (str): the message\n        nodes (list): list of nodes to be wrapped into backdrop\n\n    \"\"\"\n    assert isinstance(nodes, list), \"`nodes` should be a list of nodes\"\n\n    # Calculate bounds for the backdrop node.\n    bdX = min([node.xpos() for node in nodes])\n    bdY = min([node.ypos() for node in nodes])\n    bdW = max([node.xpos() + node.screenWidth() for node in nodes]) - bdX\n    bdH = max([node.ypos() + node.screenHeight() for node in nodes]) - bdY\n\n    # Expand the bounds to leave a little border. Elements are offsets\n    # for left, top, right and bottom edges respectively\n    left, top, right, bottom = (-20, -65, 20, 60)\n    bdX += left\n    bdY += top\n    bdW += (right - left)\n    bdH += (bottom - top)\n\n    bdn = nuke.createNode(\"BackdropNode\")\n    bdn[\"z_order\"].setValue(layer)\n\n    if color:\n        bdn[\"tile_color\"].setValue(int(color, 16))\n\n    bdn[\"xpos\"].setValue(bdX)\n    bdn[\"ypos\"].setValue(bdY)\n    bdn[\"bdwidth\"].setValue(bdW)\n    bdn[\"bdheight\"].setValue(bdH)\n\n    if label:\n        bdn[\"label\"].setValue(label)\n\n    bdn[\"note_font_size\"].setValue(20)\n    return bdn\n\n\nclass WorkfileSettings(object):\n    \"\"\"\n    All settings for workfile will be set\n\n    This object is setting all possible root settings to the workfile.\n    Including Colorspace, Frame ranges, Resolution format. It can set it\n    to Root node or to any given node.\n\n    Arguments:\n        root (node): nuke's root node\n        nodes (list): list of nuke's nodes\n        nodes_filter (list): filtering classes for nodes\n\n    \"\"\"\n\n    def __init__(self, root_node=None, nodes=None, **kwargs):\n        project_doc = kwargs.get(\"project\")\n        if project_doc is None:\n            project_name = get_current_project_name()\n            project_doc = get_project(project_name)\n        else:\n            project_name = project_doc[\"name\"]\n\n        Context._project_doc = project_doc\n        self._project_name = project_name\n        self._asset = (\n            kwargs.get(\"asset_name\")\n            or get_current_asset_name()\n        )\n        self._asset_entity = get_asset_by_name(project_name, self._asset)\n        self._root_node = root_node or nuke.root()\n        self._nodes = self.get_nodes(nodes=nodes)\n\n        self.data = kwargs\n\n    def get_nodes(self, nodes=None, nodes_filter=None):\n\n        if not isinstance(nodes, list) and not isinstance(nodes_filter, list):\n            return [n for n in nuke.allNodes()]\n        elif not isinstance(nodes, list) and isinstance(nodes_filter, list):\n            nodes = list()\n            for filter in nodes_filter:\n                [nodes.append(n) for n in nuke.allNodes(filter=filter)]\n            return nodes\n        elif isinstance(nodes, list) and not isinstance(nodes_filter, list):\n            return [n for n in self._nodes]\n        elif isinstance(nodes, list) and isinstance(nodes_filter, list):\n            for filter in nodes_filter:\n                return [n for n in self._nodes if filter in n.Class()]\n\n    def set_viewers_colorspace(self, viewer_dict):\n        ''' Adds correct colorspace to viewer\n\n        Arguments:\n            viewer_dict (dict): adjustments from presets\n\n        '''\n        if not isinstance(viewer_dict, dict):\n            msg = \"set_viewers_colorspace(): argument should be dictionary\"\n            log.error(msg)\n            nuke.message(msg)\n            return\n\n        filter_knobs = [\n            \"viewerProcess\",\n            \"wipe_position\"\n        ]\n\n        erased_viewers = []\n        for v in nuke.allNodes(filter=\"Viewer\"):\n            # set viewProcess to preset from settings\n            v[\"viewerProcess\"].setValue(\n                str(viewer_dict[\"viewerProcess\"])\n            )\n\n            if str(viewer_dict[\"viewerProcess\"]) \\\n                    not in v[\"viewerProcess\"].value():\n                copy_inputs = v.dependencies()\n                copy_knobs = {k: v[k].value() for k in v.knobs()\n                              if k not in filter_knobs}\n\n                # delete viewer with wrong settings\n                erased_viewers.append(v[\"name\"].value())\n                nuke.delete(v)\n\n                # create new viewer\n                nv = nuke.createNode(\"Viewer\")\n\n                # connect to original inputs\n                for i, n in enumerate(copy_inputs):\n                    nv.setInput(i, n)\n\n                # set copied knobs\n                for k, v in copy_knobs.items():\n                    print(k, v)\n                    nv[k].setValue(v)\n\n                # set viewerProcess\n                nv[\"viewerProcess\"].setValue(str(viewer_dict[\"viewerProcess\"]))\n\n        if erased_viewers:\n            log.warning(\n                \"Attention! Viewer nodes {} were erased.\"\n                \"It had wrong color profile\".format(erased_viewers))\n\n    def set_root_colorspace(self, imageio_host):\n        ''' Adds correct colorspace to root\n\n        Arguments:\n            imageio_host (dict): host colorspace configurations\n\n        '''\n        config_data = get_imageio_config(\n            project_name=get_current_project_name(),\n            host_name=\"nuke\"\n        )\n\n        workfile_settings = imageio_host[\"workfile\"]\n        viewer_process_settings = imageio_host[\"viewer\"][\"viewerProcess\"]\n\n        if not config_data:\n            # TODO: backward compatibility for old projects - remove later\n            # perhaps old project overrides is having it set to older version\n            # with use of `customOCIOConfigPath`\n            resolved_path = None\n            if workfile_settings.get(\"customOCIOConfigPath\"):\n                unresolved_path = workfile_settings[\"customOCIOConfigPath\"]\n                ocio_paths = unresolved_path[platform.system().lower()]\n\n                for ocio_p in ocio_paths:\n                    resolved_path = str(ocio_p).format(**os.environ)\n                    if not os.path.exists(resolved_path):\n                        continue\n\n            if resolved_path:\n                # set values to root\n                self._root_node[\"colorManagement\"].setValue(\"OCIO\")\n                self._root_node[\"OCIO_config\"].setValue(\"custom\")\n                self._root_node[\"customOCIOConfigPath\"].setValue(\n                    resolved_path)\n            else:\n                # no ocio config found and no custom path used\n                if self._root_node[\"colorManagement\"].value() \\\n                        not in str(workfile_settings[\"colorManagement\"]):\n                    self._root_node[\"colorManagement\"].setValue(\n                        str(workfile_settings[\"colorManagement\"]))\n\n                # second set ocio version\n                if self._root_node[\"OCIO_config\"].value() \\\n                        not in str(workfile_settings[\"OCIO_config\"]):\n                    self._root_node[\"OCIO_config\"].setValue(\n                        str(workfile_settings[\"OCIO_config\"]))\n\n        else:\n            # OCIO config path is defined from prelaunch hook\n            self._root_node[\"colorManagement\"].setValue(\"OCIO\")\n\n            # print previous settings in case some were found in workfile\n            residual_path = self._root_node[\"customOCIOConfigPath\"].value()\n            if residual_path:\n                log.info(\"Residual OCIO config path found: `{}`\".format(\n                    residual_path\n                ))\n\n        # we dont need the key anymore\n        workfile_settings.pop(\"customOCIOConfigPath\", None)\n        workfile_settings.pop(\"colorManagement\", None)\n        workfile_settings.pop(\"OCIO_config\", None)\n\n        # get monitor lut from settings respecting Nuke version differences\n        monitor_lut = workfile_settings.pop(\"monitorLut\", None)\n        monitor_lut_data = self._get_monitor_settings(\n            viewer_process_settings, monitor_lut)\n\n        # set monitor related knobs luts (MonitorOut, Thumbnails)\n        for knob, value_ in monitor_lut_data.items():\n            workfile_settings[knob] = value_\n\n        # then set the rest\n        for knob, value_ in workfile_settings.items():\n            # skip unfilled ocio config path\n            # it will be dict in value\n            if isinstance(value_, dict):\n                continue\n            # skip empty values\n            if not value_:\n                continue\n            if self._root_node[knob].value() not in value_:\n                self._root_node[knob].setValue(str(value_))\n                log.debug(\"nuke.root()['{}'] changed to: {}\".format(\n                    knob, value_))\n\n        # set ocio config path\n        if config_data:\n            config_path = config_data[\"path\"].replace(\"\\\\\", \"/\")\n            log.info(\"OCIO config path found: `{}`\".format(\n                config_path))\n\n            # check if there's a mismatch between environment and settings\n            correct_settings = self._is_settings_matching_environment(\n                config_data)\n\n            # if there's no mismatch between environment and settings\n            if correct_settings:\n                self._set_ocio_config_path_to_workfile(config_data)\n\n    def _get_monitor_settings(self, viewer_lut, monitor_lut):\n        \"\"\" Get monitor settings from viewer and monitor lut\n\n        Args:\n            viewer_lut (str): viewer lut string\n            monitor_lut (str): monitor lut string\n\n        Returns:\n            dict: monitor settings\n        \"\"\"\n        output_data = {}\n        m_display, m_viewer = get_viewer_config_from_string(monitor_lut)\n        v_display, v_viewer = get_viewer_config_from_string(viewer_lut)\n\n        # set monitor lut differently for nuke version 14\n        if nuke.NUKE_VERSION_MAJOR >= 14:\n            output_data[\"monitorOutLUT\"] = create_viewer_profile_string(\n                m_viewer, m_display, path_like=False)\n            # monitorLut=thumbnails - viewerProcess makes more sense\n            output_data[\"monitorLut\"] = create_viewer_profile_string(\n                v_viewer, v_display, path_like=False)\n\n        if nuke.NUKE_VERSION_MAJOR == 13:\n            output_data[\"monitorOutLUT\"] = create_viewer_profile_string(\n                m_viewer, m_display, path_like=False)\n            # monitorLut=thumbnails - viewerProcess makes more sense\n            output_data[\"monitorLut\"] = create_viewer_profile_string(\n                v_viewer, v_display, path_like=True)\n        if nuke.NUKE_VERSION_MAJOR <= 12:\n            output_data[\"monitorLut\"] = create_viewer_profile_string(\n                m_viewer, m_display, path_like=True)\n\n        return output_data\n\n    def _is_settings_matching_environment(self, config_data):\n        \"\"\" Check if OCIO config path is different from environment\n\n        Args:\n            config_data (dict): OCIO config data from settings\n\n        Returns:\n            bool: True if settings are matching environment, False otherwise\n        \"\"\"\n        current_ocio_path = os.environ[\"OCIO\"]\n        settings_ocio_path = config_data[\"path\"]\n\n        # normalize all paths to forward slashes\n        current_ocio_path = current_ocio_path.replace(\"\\\\\", \"/\")\n        settings_ocio_path = settings_ocio_path.replace(\"\\\\\", \"/\")\n\n        if current_ocio_path != settings_ocio_path:\n            message = \"\"\"\nIt seems like there's a mismatch between the OCIO config path set in your Nuke\nsettings and the actual path set in your OCIO environment.\n\nTo resolve this, please follow these steps:\n1. Close Nuke if it's currently open.\n2. Reopen Nuke.\n\nPlease note the paths for your reference:\n\n- The OCIO environment path currently set:\n  `{env_path}`\n\n- The path in your current Nuke settings:\n  `{settings_path}`\n\nReopening Nuke should synchronize these paths and resolve any discrepancies.\n\"\"\"\n            nuke.message(\n                message.format(\n                    env_path=current_ocio_path,\n                    settings_path=settings_ocio_path\n                )\n            )\n            return False\n\n        return True\n\n    def _set_ocio_config_path_to_workfile(self, config_data):\n        \"\"\" Set OCIO config path to workfile\n\n        Path set into nuke workfile. It is trying to replace path with\n        environment variable if possible. If not, it will set it as it is.\n        It also saves the script to apply the change, but only if it's not\n        empty Untitled script.\n\n        Args:\n            config_data (dict): OCIO config data from settings\n\n        \"\"\"\n        # replace path with env var if possible\n        ocio_path = self._replace_ocio_path_with_env_var(config_data)\n\n        log.info(\"Setting OCIO config path to: `{}`\".format(\n            ocio_path))\n\n        self._root_node[\"customOCIOConfigPath\"].setValue(\n            ocio_path\n        )\n        self._root_node[\"OCIO_config\"].setValue(\"custom\")\n\n        # only save script if it's not empty\n        if self._root_node[\"name\"].value() != \"\":\n            log.info(\"Saving script to apply OCIO config path change.\")\n            nuke.scriptSave()\n\n    def _get_included_vars(self, config_template):\n        \"\"\" Get all environment variables included in template\n\n        Args:\n            config_template (str): OCIO config template from settings\n\n        Returns:\n            list: list of environment variables included in template\n        \"\"\"\n        # resolve all environments for whitelist variables\n        included_vars = [\n            \"BUILTIN_OCIO_ROOT\",\n        ]\n\n        # include all project root related env vars\n        for env_var in os.environ:\n            if env_var.startswith(\"OPENPYPE_PROJECT_ROOT_\"):\n                included_vars.append(env_var)\n\n        # use regex to find env var in template with format {ENV_VAR}\n        # this way we make sure only template used env vars are included\n        env_var_regex = r\"\\{([A-Z0-9_]+)\\}\"\n        env_var = re.findall(env_var_regex, config_template)\n        if env_var:\n            included_vars.append(env_var[0])\n\n        return included_vars\n\n    def _replace_ocio_path_with_env_var(self, config_data):\n        \"\"\" Replace OCIO config path with environment variable\n\n        Environment variable is added as TCL expression to path. TCL expression\n        is also replacing backward slashes found in path for windows\n        formatted values.\n\n        Args:\n            config_data (str): OCIO config dict from settings\n\n        Returns:\n            str: OCIO config path with environment variable TCL expression\n        \"\"\"\n        config_path = config_data[\"path\"].replace(\"\\\\\", \"/\")\n        config_template = config_data[\"template\"]\n\n        included_vars = self._get_included_vars(config_template)\n\n        # make sure we return original path if no env var is included\n        new_path = config_path\n\n        for env_var in included_vars:\n            env_path = os.getenv(env_var)\n            if not env_path:\n                continue\n\n            # it has to be directory current process can see\n            if not os.path.isdir(env_path):\n                continue\n\n            # make sure paths are in same format\n            env_path = env_path.replace(\"\\\\\", \"/\")\n            path = config_path.replace(\"\\\\\", \"/\")\n\n            # check if env_path is in path and replace to first found positive\n            if env_path in path:\n                # with regsub we make sure path format of slashes is correct\n                resub_expr = (\n                    \"[regsub -all {{\\\\\\\\}} [getenv {}] \\\"/\\\"]\").format(env_var)\n\n                new_path = path.replace(\n                    env_path, resub_expr\n                )\n                break\n\n        return new_path\n\n    def set_writes_colorspace(self):\n        ''' Adds correct colorspace to write node dict\n\n        '''\n        for node in nuke.allNodes(filter=\"Group\", group=self._root_node):\n            log.info(\"Setting colorspace to `{}`\".format(node.name()))\n\n            # get data from avalon knob\n            avalon_knob_data = read_avalon_data(node)\n            node_data = get_node_data(node, INSTANCE_DATA_KNOB)\n\n            if (\n                # backward compatibility\n                # TODO: remove this once old avalon data api will be removed\n                avalon_knob_data\n                and avalon_knob_data.get(\"id\") != \"pyblish.avalon.instance\"\n            ):\n                continue\n            elif (\n                node_data\n                and node_data.get(\"id\") != \"pyblish.avalon.instance\"\n            ):\n                continue\n\n            if (\n                # backward compatibility\n                # TODO: remove this once old avalon data api will be removed\n                avalon_knob_data\n                and \"creator\" not in avalon_knob_data\n            ):\n                continue\n            elif (\n                node_data\n                and \"creator_identifier\" not in node_data\n            ):\n                continue\n\n            nuke_imageio_writes = None\n            if avalon_knob_data:\n                # establish families\n                families = [avalon_knob_data[\"family\"]]\n                if avalon_knob_data.get(\"families\"):\n                    families.append(avalon_knob_data.get(\"families\"))\n\n                nuke_imageio_writes = get_imageio_node_setting(\n                    node_class=avalon_knob_data[\"families\"],\n                    plugin_name=avalon_knob_data[\"creator\"],\n                    subset=avalon_knob_data[\"subset\"]\n                )\n            elif node_data:\n                nuke_imageio_writes = get_write_node_template_attr(node)\n\n            log.debug(\"nuke_imageio_writes: `{}`\".format(nuke_imageio_writes))\n\n            if not nuke_imageio_writes:\n                return\n\n            write_node = None\n\n            # get into the group node\n            node.begin()\n            for x in nuke.allNodes():\n                if x.Class() == \"Write\":\n                    write_node = x\n            node.end()\n\n            if not write_node:\n                return\n\n            try:\n                # write all knobs to node\n                for knob in nuke_imageio_writes[\"knobs\"]:\n                    value = knob[\"value\"]\n                    if isinstance(value, six.text_type):\n                        value = str(value)\n                    if str(value).startswith(\"0x\"):\n                        value = int(value, 16)\n\n                    log.debug(\"knob: {}| value: {}\".format(\n                        knob[\"name\"], value\n                    ))\n                    write_node[knob[\"name\"]].setValue(value)\n            except TypeError:\n                log.warning(\n                    \"Legacy workflow didn't work, switching to current\")\n\n                set_node_knobs_from_settings(\n                    write_node, nuke_imageio_writes[\"knobs\"])\n\n    def set_reads_colorspace(self, read_clrs_inputs):\n        \"\"\" Setting colorspace to Read nodes\n\n        Looping through all read nodes and tries to set colorspace based\n        on regex rules in presets\n        \"\"\"\n        changes = {}\n        for n in nuke.allNodes():\n            file = nuke.filename(n)\n            if n.Class() != \"Read\":\n                continue\n\n            # check if any colorspace presets for read is matching\n            preset_clrsp = None\n\n            for input in read_clrs_inputs:\n                if not bool(re.search(input[\"regex\"], file)):\n                    continue\n                preset_clrsp = input[\"colorspace\"]\n\n            if preset_clrsp is not None:\n                current = n[\"colorspace\"].value()\n                future = str(preset_clrsp)\n                if current != future:\n                    changes[n.name()] = {\n                        \"from\": current,\n                        \"to\": future\n                    }\n\n        log.debug(changes)\n        if changes:\n            msg = \"Read nodes are not set to correct colorspace:\\n\\n\"\n            for nname, knobs in changes.items():\n                msg += (\n                    \" - node: '{0}' is now '{1}' but should be '{2}'\\n\"\n                ).format(nname, knobs[\"from\"], knobs[\"to\"])\n\n            msg += \"\\nWould you like to change it?\"\n\n            if nuke.ask(msg):\n                for nname, knobs in changes.items():\n                    n = nuke.toNode(nname)\n                    n[\"colorspace\"].setValue(knobs[\"to\"])\n                    log.info(\n                        \"Setting `{0}` to `{1}`\".format(\n                            nname,\n                            knobs[\"to\"]))\n\n    def set_colorspace(self):\n        ''' Setting colorspace following presets\n        '''\n        # get imageio\n        nuke_colorspace = get_nuke_imageio_settings()\n\n        log.info(\"Setting colorspace to workfile...\")\n        try:\n            self.set_root_colorspace(nuke_colorspace)\n        except AttributeError as _error:\n            msg = \"Set Colorspace to workfile error: {}\".format(_error)\n            nuke.message(msg)\n\n        log.info(\"Setting colorspace to viewers...\")\n        try:\n            self.set_viewers_colorspace(nuke_colorspace[\"viewer\"])\n        except AttributeError as _error:\n            msg = \"Set Colorspace to viewer error: {}\".format(_error)\n            nuke.message(msg)\n\n        log.info(\"Setting colorspace to write nodes...\")\n        try:\n            self.set_writes_colorspace()\n        except AttributeError as _error:\n            nuke.message(_error)\n            log.error(_error)\n\n        log.info(\"Setting colorspace to read nodes...\")\n        read_clrs_inputs = nuke_colorspace[\"regexInputs\"].get(\"inputs\", [])\n        if read_clrs_inputs:\n            self.set_reads_colorspace(read_clrs_inputs)\n\n    def reset_frame_range_handles(self):\n        \"\"\"Set frame range to current asset\"\"\"\n\n        if \"data\" not in self._asset_entity:\n            msg = \"Asset {} don't have set any 'data'\".format(self._asset)\n            log.warning(msg)\n            nuke.message(msg)\n            return\n\n        asset_data = self._asset_entity[\"data\"]\n\n        missing_cols = []\n        check_cols = [\"fps\", \"frameStart\", \"frameEnd\",\n                      \"handleStart\", \"handleEnd\"]\n\n        for col in check_cols:\n            if col not in asset_data:\n                missing_cols.append(col)\n\n        if len(missing_cols) > 0:\n            missing = \", \".join(missing_cols)\n            msg = \"'{}' are not set for asset '{}'!\".format(\n                missing, self._asset)\n            log.warning(msg)\n            nuke.message(msg)\n            return\n\n        # get handles values\n        handle_start = asset_data[\"handleStart\"]\n        handle_end = asset_data[\"handleEnd\"]\n\n        fps = float(asset_data[\"fps\"])\n        frame_start_handle = int(asset_data[\"frameStart\"]) - handle_start\n        frame_end_handle = int(asset_data[\"frameEnd\"]) + handle_end\n\n        self._root_node[\"lock_range\"].setValue(False)\n        self._root_node[\"fps\"].setValue(fps)\n        self._root_node[\"first_frame\"].setValue(frame_start_handle)\n        self._root_node[\"last_frame\"].setValue(frame_end_handle)\n        self._root_node[\"lock_range\"].setValue(True)\n\n        # update node graph so knobs are updated\n        update_node_graph()\n\n        frame_range = '{0}-{1}'.format(\n            int(asset_data[\"frameStart\"]),\n            int(asset_data[\"frameEnd\"])\n        )\n\n        for node in nuke.allNodes(filter=\"Viewer\"):\n            node['frame_range'].setValue(frame_range)\n            node['frame_range_lock'].setValue(True)\n            node['frame_range'].setValue(frame_range)\n            node['frame_range_lock'].setValue(True)\n\n        if not ASSIST:\n            set_node_data(\n                self._root_node,\n                INSTANCE_DATA_KNOB,\n                {\n                    \"handleStart\": int(handle_start),\n                    \"handleEnd\": int(handle_end)\n                }\n            )\n        else:\n            log.warning(\n                \"NukeAssist mode is not allowing \"\n                \"updating custom knobs...\"\n            )\n\n    def reset_resolution(self):\n        \"\"\"Set resolution to project resolution.\"\"\"\n        log.info(\"Resetting resolution\")\n        project_name = get_current_project_name()\n        asset_data = self._asset_entity[\"data\"]\n\n        format_data = {\n            \"width\": int(asset_data.get(\n                'resolutionWidth',\n                asset_data.get('resolution_width'))),\n            \"height\": int(asset_data.get(\n                'resolutionHeight',\n                asset_data.get('resolution_height'))),\n            \"pixel_aspect\": asset_data.get(\n                'pixelAspect',\n                asset_data.get('pixel_aspect', 1)),\n            \"name\": project_name\n        }\n\n        if any(x_ for x_ in format_data.values() if x_ is None):\n            msg = (\"Missing set shot attributes in DB.\"\n                   \"\\nContact your supervisor!.\"\n                   \"\\n\\nWidth: `{width}`\"\n                   \"\\nHeight: `{height}`\"\n                   \"\\nPixel Aspect: `{pixel_aspect}`\").format(**format_data)\n            log.error(msg)\n            nuke.message(msg)\n\n        existing_format = None\n        for format in nuke.formats():\n            if format_data[\"name\"] == format.name():\n                existing_format = format\n                break\n\n        if existing_format:\n            # Enforce existing format to be correct.\n            existing_format.setWidth(format_data[\"width\"])\n            existing_format.setHeight(format_data[\"height\"])\n            existing_format.setPixelAspect(format_data[\"pixel_aspect\"])\n        else:\n            format_string = self.make_format_string(**format_data)\n            log.info(\"Creating new format: {}\".format(format_string))\n            nuke.addFormat(format_string)\n\n        nuke.root()[\"format\"].setValue(format_data[\"name\"])\n        log.info(\"Format is set.\")\n\n        # update node graph so knobs are updated\n        update_node_graph()\n\n    def make_format_string(self, **kwargs):\n        if kwargs.get(\"r\"):\n            return (\n                \"{width} \"\n                \"{height} \"\n                \"{x} \"\n                \"{y} \"\n                \"{r} \"\n                \"{t} \"\n                \"{pixel_aspect:.2f} \"\n                \"{name}\".format(**kwargs)\n            )\n        else:\n            return (\n                \"{width} \"\n                \"{height} \"\n                \"{pixel_aspect:.2f} \"\n                \"{name}\".format(**kwargs)\n            )\n\n    def set_context_settings(self):\n        # replace reset resolution from avalon core to pype's\n        self.reset_resolution()\n        # replace reset resolution from avalon core to pype's\n        self.reset_frame_range_handles()\n        # add colorspace menu item\n        self.set_colorspace()\n\n    def set_favorites(self):\n        from .utils import set_context_favorites\n\n        work_dir = os.getenv(\"AVALON_WORKDIR\")\n        asset = get_current_asset_name()\n        favorite_items = OrderedDict()\n\n        # project\n        # get project's root and split to parts\n        projects_root = os.path.normpath(work_dir.split(\n            Context.project_name)[0])\n        # add project name\n        project_dir = os.path.join(projects_root, Context.project_name) + \"/\"\n        # add to favorites\n        favorite_items.update({\"Project dir\": project_dir.replace(\"\\\\\", \"/\")})\n\n        # asset\n        asset_root = os.path.normpath(work_dir.split(\n            asset)[0])\n        # add asset name\n        asset_dir = os.path.join(asset_root, asset) + \"/\"\n        # add to favorites\n        favorite_items.update({\"Shot dir\": asset_dir.replace(\"\\\\\", \"/\")})\n\n        # workdir\n        favorite_items.update({\"Work dir\": work_dir.replace(\"\\\\\", \"/\")})\n\n        set_context_favorites(favorite_items)\n\n\ndef get_write_node_template_attr(node):\n    ''' Gets all defined data from presets\n\n    '''\n\n    # TODO: add identifiers to settings and rename settings key\n    plugin_names_mapping = {\n        \"create_write_image\": \"CreateWriteImage\",\n        \"create_write_prerender\": \"CreateWritePrerender\",\n        \"create_write_render\": \"CreateWriteRender\"\n    }\n    # get avalon data from node\n    node_data = get_node_data(node, INSTANCE_DATA_KNOB)\n    identifier = node_data[\"creator_identifier\"]\n\n    # return template data\n    return get_imageio_node_setting(\n        node_class=\"Write\",\n        plugin_name=plugin_names_mapping[identifier],\n        subset=node_data[\"subset\"]\n    )\n\n\ndef get_dependent_nodes(nodes):\n    \"\"\"Get all dependent nodes connected to the list of nodes.\n\n    Looking for connections outside of the nodes in incoming argument.\n\n    Arguments:\n        nodes (list): list of nuke.Node objects\n\n    Returns:\n        connections_in: dictionary of nodes and its dependencies\n        connections_out: dictionary of nodes and its dependency\n    \"\"\"\n\n    connections_in = dict()\n    connections_out = dict()\n    node_names = [n.name() for n in nodes]\n    for node in nodes:\n        inputs = node.dependencies()\n        outputs = node.dependent()\n        # collect all inputs outside\n        test_in = [(i, n) for i, n in enumerate(inputs)\n                   if n.name() not in node_names]\n        if test_in:\n            connections_in.update({\n                node: test_in\n            })\n        # collect all outputs outside\n        test_out = [i for i in outputs if i.name() not in node_names]\n        if test_out:\n            # only one dependent node is allowed\n            connections_out.update({\n                node: test_out[-1]\n            })\n\n    return connections_in, connections_out\n\n\ndef update_node_graph():\n    # Resetting frame will update knob values\n    try:\n        root_node_lock = nuke.root()[\"lock_range\"].value()\n        nuke.root()[\"lock_range\"].setValue(not root_node_lock)\n        nuke.root()[\"lock_range\"].setValue(root_node_lock)\n\n        current_frame = nuke.frame()\n        nuke.frame(1)\n        nuke.frame(int(current_frame))\n    except Exception as error:\n        log.warning(error)\n\n\ndef find_free_space_to_paste_nodes(\n    nodes,\n    group=nuke.root(),\n    direction=\"right\",\n    offset=300\n):\n    \"\"\"\n    For getting coordinates in DAG (node graph) for placing new nodes\n\n    Arguments:\n        nodes (list): list of nuke.Node objects\n        group (nuke.Node) [optional]: object in which context it is\n        direction (str) [optional]: where we want it to be placed\n                                    [left, right, top, bottom]\n        offset (int) [optional]: what offset it is from rest of nodes\n\n    Returns:\n        xpos (int): x coordinace in DAG\n        ypos (int): y coordinace in DAG\n    \"\"\"\n    if len(nodes) == 0:\n        return 0, 0\n\n    group_xpos = list()\n    group_ypos = list()\n\n    # get local coordinates of all nodes\n    nodes_xpos = [n.xpos() for n in nodes] + \\\n                 [n.xpos() + n.screenWidth() for n in nodes]\n\n    nodes_ypos = [n.ypos() for n in nodes] + \\\n                 [n.ypos() + n.screenHeight() for n in nodes]\n\n    # get complete screen size of all nodes to be placed in\n    nodes_screen_width = max(nodes_xpos) - min(nodes_xpos)\n    nodes_screen_heigth = max(nodes_ypos) - min(nodes_ypos)\n\n    # get screen size (r,l,t,b) of all nodes in `group`\n    with group:\n        group_xpos = [n.xpos() for n in nuke.allNodes() if n not in nodes] + \\\n                     [n.xpos() + n.screenWidth() for n in nuke.allNodes()\n                      if n not in nodes]\n        group_ypos = [n.ypos() for n in nuke.allNodes() if n not in nodes] + \\\n                     [n.ypos() + n.screenHeight() for n in nuke.allNodes()\n                      if n not in nodes]\n\n        # calc output left\n        if direction in \"left\":\n            xpos = min(group_xpos) - abs(nodes_screen_width) - abs(offset)\n            ypos = min(group_ypos)\n            return xpos, ypos\n        # calc output right\n        if direction in \"right\":\n            xpos = max(group_xpos) + abs(offset)\n            ypos = min(group_ypos)\n            return xpos, ypos\n        # calc output top\n        if direction in \"top\":\n            xpos = min(group_xpos)\n            ypos = min(group_ypos) - abs(nodes_screen_heigth) - abs(offset)\n            return xpos, ypos\n        # calc output bottom\n        if direction in \"bottom\":\n            xpos = min(group_xpos)\n            ypos = max(group_ypos) + abs(offset)\n            return xpos, ypos\n\n\n@contextlib.contextmanager\ndef maintained_selection(exclude_nodes=None):\n    \"\"\"Maintain selection during context\n\n    Maintain selection during context and unselect\n    all nodes after context is done.\n\n    Arguments:\n        exclude_nodes (list[nuke.Node]): list of nodes to be unselected\n                                         before context is done\n\n    Example:\n        >>> with maintained_selection():\n        ...     node[\"selected\"].setValue(True)\n        >>> print(node[\"selected\"].value())\n        False\n    \"\"\"\n    if exclude_nodes:\n        for node in exclude_nodes:\n            node[\"selected\"].setValue(False)\n\n    previous_selection = nuke.selectedNodes()\n\n    try:\n        yield\n    finally:\n        # unselect all selection in case there is some\n        reset_selection()\n\n        # and select all previously selected nodes\n        if previous_selection:\n            select_nodes(previous_selection)\n\n\n@contextlib.contextmanager\ndef swap_node_with_dependency(old_node, new_node):\n    \"\"\" Swap node with dependency\n\n    Swap node with dependency and reconnect all inputs and outputs.\n    It removes old node.\n\n    Arguments:\n        old_node (nuke.Node): node to be replaced\n        new_node (nuke.Node): node to replace with\n\n    Example:\n        >>> old_node_name = old_node[\"name\"].value()\n        >>> print(old_node_name)\n        old_node_name_01\n        >>> with swap_node_with_dependency(old_node, new_node) as node_name:\n        ...     new_node[\"name\"].setValue(node_name)\n        >>> print(new_node[\"name\"].value())\n        old_node_name_01\n    \"\"\"\n    # preserve position\n    xpos, ypos = old_node.xpos(), old_node.ypos()\n    # preserve selection after all is done\n    outputs = get_node_outputs(old_node)\n    inputs = old_node.dependencies()\n    node_name = old_node[\"name\"].value()\n\n    try:\n        nuke.delete(old_node)\n\n        yield node_name\n    finally:\n\n        # Reconnect inputs\n        for i, node in enumerate(inputs):\n            new_node.setInput(i, node)\n        # Reconnect outputs\n        if outputs:\n            for n, pipes in outputs.items():\n                for i in pipes:\n                    n.setInput(i, new_node)\n        # return to original position\n        new_node.setXYpos(xpos, ypos)\n\n\ndef reset_selection():\n    \"\"\"Deselect all selected nodes\"\"\"\n    for node in nuke.selectedNodes():\n        node[\"selected\"].setValue(False)\n\n\ndef select_nodes(nodes):\n    \"\"\"Selects all inputted nodes\n\n    Arguments:\n        nodes (Union[list, tuple, set]): nuke nodes to be selected\n    \"\"\"\n    assert isinstance(nodes, (list, tuple, set)), \\\n        \"nodes has to be list, tuple or set\"\n\n    for node in nodes:\n        node[\"selected\"].setValue(True)\n\n\ndef launch_workfiles_app():\n    \"\"\"Show workfiles tool on nuke launch.\n\n    Trigger to show workfiles tool on application launch. Can be executed only\n    once all other calls are ignored.\n\n    Workfiles tool show is deferred after application initialization using\n    QTimer.\n    \"\"\"\n\n    if Context.workfiles_launched:\n        return\n\n    Context.workfiles_launched = True\n\n    # get all imortant settings\n    open_at_start = env_value_to_bool(\n        env_key=\"OPENPYPE_WORKFILE_TOOL_ON_START\",\n        default=None)\n\n    # return if none is defined\n    if not open_at_start:\n        return\n\n    # Show workfiles tool using timer\n    # - this will be probably triggered during initialization in that case\n    #   the application is not be able to show uis so it must be\n    #   deferred using timer\n    # - timer should be processed when initialization ends\n    #       When applications starts to process events.\n    timer = QtCore.QTimer()\n    timer.timeout.connect(_launch_workfile_app)\n    timer.setInterval(100)\n    Context.workfiles_tool_timer = timer\n    timer.start()\n\n\ndef _launch_workfile_app():\n    # Safeguard to not show window when application is still starting up\n    #   or is already closing down.\n    closing_down = QtWidgets.QApplication.closingDown()\n    starting_up = QtWidgets.QApplication.startingUp()\n\n    # Stop the timer if application finished start up of is closing down\n    if closing_down or not starting_up:\n        Context.workfiles_tool_timer.stop()\n        Context.workfiles_tool_timer = None\n\n    # Skip if application is starting up or closing down\n    if starting_up or closing_down:\n        return\n\n    # Make sure on top is enabled on first show so the window is not hidden\n    #   under main nuke window\n    #   - this happened on Centos 7 and it is because the focus of nuke\n    #       changes to the main window after showing because of initialization\n    #       which moves workfiles tool under it\n    host_tools.show_workfiles(parent=None, on_top=True)\n\n\n@deprecated(\"openpype.hosts.nuke.api.lib.start_workfile_template_builder\")\ndef process_workfile_builder():\n    \"\"\" [DEPRECATED] Process workfile builder on nuke start\n\n    This function is deprecated and will be removed in future versions.\n    Use settings for `project_settings/nuke/templated_workfile_build` which are\n    supported by api `start_workfile_template_builder()`.\n    \"\"\"\n\n    # to avoid looping of the callback, remove it!\n    nuke.removeOnCreate(process_workfile_builder, nodeClass=\"Root\")\n\n    # get state from settings\n    project_settings = get_current_project_settings()\n    workfile_builder = project_settings[\"nuke\"].get(\n        \"workfile_builder\", {})\n\n    # get settings\n    create_fv_on = workfile_builder.get(\"create_first_version\") or None\n    builder_on = workfile_builder.get(\"builder_on_start\") or None\n\n    last_workfile_path = os.environ.get(\"AVALON_LAST_WORKFILE\")\n\n    # generate first version in file not existing and feature is enabled\n    if create_fv_on and not os.path.exists(last_workfile_path):\n        # get custom template path if any\n        custom_template_path = get_custom_workfile_template_from_session(\n            project_settings=project_settings\n        )\n\n        # if custom template is defined\n        if custom_template_path:\n            log.info(\"Adding nodes from `{}`...\".format(\n                custom_template_path\n            ))\n            try:\n                # import nodes into current script\n                nuke.nodePaste(custom_template_path)\n            except RuntimeError:\n                raise RuntimeError((\n                    \"Template defined for project: {} is not working. \"\n                    \"Talk to your manager for an advise\").format(\n                        custom_template_path))\n\n        # if builder at start is defined\n        if builder_on:\n            log.info(\"Building nodes from presets...\")\n            # build nodes by defined presets\n            BuildWorkfile().process()\n\n        log.info(\"Saving script as version `{}`...\".format(\n            last_workfile_path\n        ))\n        # safe file as version\n        save_file(last_workfile_path)\n        return\n\n\ndef start_workfile_template_builder():\n    from .workfile_template_builder import (\n        build_workfile_template\n    )\n\n    # remove callback since it would be duplicating the workfile\n    nuke.removeOnCreate(start_workfile_template_builder, nodeClass=\"Root\")\n\n    # to avoid looping of the callback, remove it!\n    log.info(\"Starting workfile template builder...\")\n    try:\n        build_workfile_template(workfile_creation_enabled=True)\n    except TemplateProfileNotFound:\n        log.warning(\"Template profile not found. Skipping...\")\n\n\n@deprecated\ndef recreate_instance(origin_node, avalon_data=None):\n    \"\"\"Recreate input instance to different data\n\n    Args:\n        origin_node (nuke.Node): Nuke node to be recreating from\n        avalon_data (dict, optional): data to be used in new node avalon_data\n\n    Returns:\n        nuke.Node: newly created node\n    \"\"\"\n    knobs_wl = [\"render\", \"publish\", \"review\", \"ypos\",\n                \"use_limit\", \"first\", \"last\"]\n    # get data from avalon knobs\n    data = get_avalon_knob_data(\n        origin_node)\n\n    # add input data to avalon data\n    if avalon_data:\n        data.update(avalon_data)\n\n    # capture all node knobs allowed in op_knobs\n    knobs_data = {k: origin_node[k].value()\n                  for k in origin_node.knobs()\n                  for key in knobs_wl\n                  if key in k}\n\n    # get node dependencies\n    inputs = origin_node.dependencies()\n    outputs = origin_node.dependent()\n\n    # remove the node\n    nuke.delete(origin_node)\n\n    # create new node\n    # get appropriate plugin class\n    creator_plugin = None\n    for Creator in discover_legacy_creator_plugins():\n        if Creator.__name__ == data[\"creator\"]:\n            creator_plugin = Creator\n            break\n\n    # create write node with creator\n    new_node_name = data[\"subset\"]\n    new_node = creator_plugin(new_node_name, data[\"asset\"]).process()\n\n    # white listed knobs to the new node\n    for _k, _v in knobs_data.items():\n        try:\n            print(_k, _v)\n            new_node[_k].setValue(_v)\n        except Exception as e:\n            print(e)\n\n    # connect to original inputs\n    for i, n in enumerate(inputs):\n        new_node.setInput(i, n)\n\n    # connect to outputs\n    if len(outputs) > 0:\n        for dn in outputs:\n            dn.setInput(0, new_node)\n\n    return new_node\n\n\ndef add_scripts_menu():\n    try:\n        from scriptsmenu import launchfornuke\n    except ImportError:\n        log.warning(\n            \"Skipping studio.menu install, because \"\n            \"'scriptsmenu' module seems unavailable.\"\n        )\n        return\n\n    # load configuration of custom menu\n    project_name = get_current_project_name()\n    project_settings = get_project_settings(project_name)\n    config = project_settings[\"nuke\"][\"scriptsmenu\"][\"definition\"]\n    _menu = project_settings[\"nuke\"][\"scriptsmenu\"][\"name\"]\n\n    if not config:\n        log.warning(\"Skipping studio menu, no definition found.\")\n        return\n\n    # run the launcher for Maya menu\n    studio_menu = launchfornuke.main(title=_menu.title())\n\n    # apply configuration\n    studio_menu.build_from_configuration(studio_menu, config)\n\n\ndef add_scripts_gizmo():\n\n    # load configuration of custom menu\n    project_name = get_current_project_name()\n    project_settings = get_project_settings(project_name)\n    platform_name = platform.system().lower()\n\n    for gizmo_settings in project_settings[\"nuke\"][\"gizmo\"]:\n        gizmo_list_definition = gizmo_settings[\"gizmo_definition\"]\n        toolbar_name = gizmo_settings[\"toolbar_menu_name\"]\n        # gizmo_toolbar_path = gizmo_settings[\"gizmo_toolbar_path\"]\n        gizmo_source_dir = gizmo_settings.get(\n            \"gizmo_source_dir\", {}).get(platform_name)\n        toolbar_icon_path = gizmo_settings.get(\n            \"toolbar_icon_path\", {}).get(platform_name)\n\n        if not gizmo_source_dir:\n            log.debug(\"Skipping studio gizmo `{}`, \"\n                      \"no gizmo path found.\".format(toolbar_name)\n                      )\n            return\n\n        if not gizmo_list_definition:\n            log.debug(\"Skipping studio gizmo `{}`, \"\n                      \"no definition found.\".format(toolbar_name)\n                      )\n            return\n\n        if toolbar_icon_path:\n            try:\n                toolbar_icon_path = toolbar_icon_path.format(**os.environ)\n            except KeyError as e:\n                log.error(\n                    \"This environment variable doesn't exist: {}\".format(e)\n                )\n\n        existing_gizmo_path = []\n        for source_dir in gizmo_source_dir:\n            try:\n                resolve_source_dir = source_dir.format(**os.environ)\n            except KeyError as e:\n                log.error(\n                    \"This environment variable doesn't exist: {}\".format(e)\n                )\n                continue\n            if not os.path.exists(resolve_source_dir):\n                log.warning(\n                    \"The source of gizmo `{}` does not exists\".format(\n                        resolve_source_dir\n                    )\n                )\n                continue\n            existing_gizmo_path.append(resolve_source_dir)\n\n        # run the launcher for Nuke toolbar\n        toolbar_menu = gizmo_menu.GizmoMenu(\n            title=toolbar_name,\n            icon=toolbar_icon_path\n        )\n\n        # apply configuration\n        toolbar_menu.add_gizmo_path(existing_gizmo_path)\n        toolbar_menu.build_from_configuration(gizmo_list_definition)\n\n\nclass NukeDirmap(HostDirmap):\n    def __init__(self, file_name, *args, **kwargs):\n        \"\"\"\n        Args:\n            file_name (str): full path of referenced file from workfiles\n            *args (tuple): Positional arguments for 'HostDirmap' class\n            **kwargs (dict): Keyword arguments for 'HostDirmap' class\n        \"\"\"\n\n        self.file_name = file_name\n        super(NukeDirmap, self).__init__(*args, **kwargs)\n\n    def on_enable_dirmap(self):\n        pass\n\n    def dirmap_routine(self, source_path, destination_path):\n        source_path = source_path.lower().replace(os.sep, '/')\n        destination_path = destination_path.lower().replace(os.sep, '/')\n        log.debug(\"Map: {} with: {}->{}\".format(self.file_name,\n                                                source_path, destination_path))\n        if platform.system().lower() == \"windows\":\n            self.file_name = self.file_name.lower().replace(\n                source_path, destination_path)\n        else:\n            self.file_name = self.file_name.replace(\n                source_path, destination_path)\n\n\nclass DirmapCache:\n    \"\"\"Caching class to get settings and sync_module easily and only once.\"\"\"\n    _project_name = None\n    _project_settings = None\n    _sync_module_discovered = False\n    _sync_module = None\n    _mapping = None\n\n    @classmethod\n    def project_name(cls):\n        if cls._project_name is None:\n            cls._project_name = os.getenv(\"AVALON_PROJECT\")\n        return cls._project_name\n\n    @classmethod\n    def project_settings(cls):\n        if cls._project_settings is None:\n            cls._project_settings = get_project_settings(cls.project_name())\n        return cls._project_settings\n\n    @classmethod\n    def sync_module(cls):\n        if not cls._sync_module_discovered:\n            cls._sync_module_discovered = True\n            cls._sync_module = ModulesManager().modules_by_name.get(\n                \"sync_server\")\n        return cls._sync_module\n\n    @classmethod\n    def mapping(cls):\n        return cls._mapping\n\n    @classmethod\n    def set_mapping(cls, mapping):\n        cls._mapping = mapping\n\n\ndef dirmap_file_name_filter(file_name):\n    \"\"\"Nuke callback function with single full path argument.\n\n        Checks project settings for potential mapping from source to dest.\n    \"\"\"\n\n    dirmap_processor = NukeDirmap(\n        file_name,\n        \"nuke\",\n        DirmapCache.project_name(),\n        DirmapCache.project_settings(),\n        DirmapCache.sync_module(),\n    )\n    if not DirmapCache.mapping():\n        DirmapCache.set_mapping(dirmap_processor.get_mappings())\n\n    dirmap_processor.process_dirmap(DirmapCache.mapping())\n    if os.path.exists(dirmap_processor.file_name):\n        return dirmap_processor.file_name\n    return file_name\n\n\n@contextlib.contextmanager\ndef node_tempfile():\n    \"\"\"Create a temp file where node is pasted during duplication.\n\n    This is to avoid using clipboard for node duplication.\n    \"\"\"\n\n    tmp_file = tempfile.NamedTemporaryFile(\n        mode=\"w\", prefix=\"openpype_nuke_temp_\", suffix=\".nk\", delete=False\n    )\n    tmp_file.close()\n    node_tempfile_path = tmp_file.name\n\n    try:\n        # Yield the path where node can be copied\n        yield node_tempfile_path\n\n    finally:\n        # Remove the file at the end\n        os.remove(node_tempfile_path)\n\n\ndef duplicate_node(node):\n    reset_selection()\n\n    # select required node for duplication\n    node.setSelected(True)\n\n    with node_tempfile() as filepath:\n        # copy selected to temp filepath\n        nuke.nodeCopy(filepath)\n\n        # reset selection\n        reset_selection()\n\n        # paste node and selection is on it only\n        dupli_node = nuke.nodePaste(filepath)\n\n    # reset selection\n    reset_selection()\n\n    return dupli_node\n\n\ndef get_group_io_nodes(nodes):\n    \"\"\"Get the input and the output of a group of nodes.\"\"\"\n\n    if not nodes:\n        raise ValueError(\"there is no nodes in the list\")\n\n    input_node = None\n    output_node = None\n\n    if len(nodes) == 1:\n        input_node = output_node = nodes[0]\n\n    else:\n        for node in nodes:\n            if \"Input\" in node.name():\n                input_node = node\n\n            if \"Output\" in node.name():\n                output_node = node\n\n            if input_node is not None and output_node is not None:\n                break\n\n        if input_node is None:\n            log.warning(\"No Input found\")\n\n        if output_node is None:\n            log.warning(\"No Output found\")\n\n    return input_node, output_node\n\n\ndef get_extreme_positions(nodes):\n    \"\"\"Get the 4 numbers that represent the box of a group of nodes.\"\"\"\n\n    if not nodes:\n        raise ValueError(\"there is no nodes in the list\")\n\n    nodes_xpos = [n.xpos() for n in nodes] + \\\n        [n.xpos() + n.screenWidth() for n in nodes]\n\n    nodes_ypos = [n.ypos() for n in nodes] + \\\n        [n.ypos() + n.screenHeight() for n in nodes]\n\n    min_x, min_y = (min(nodes_xpos), min(nodes_ypos))\n    max_x, max_y = (max(nodes_xpos), max(nodes_ypos))\n    return min_x, min_y, max_x, max_y\n\n\ndef refresh_node(node):\n    \"\"\"Correct a bug caused by the multi-threading of nuke.\n\n    Refresh the node to make sure that it takes the desired attributes.\n    \"\"\"\n\n    x = node.xpos()\n    y = node.ypos()\n    nuke.autoplaceSnap(node)\n    node.setXYpos(x, y)\n\n\ndef refresh_nodes(nodes):\n    for node in nodes:\n        refresh_node(node)\n\n\ndef get_names_from_nodes(nodes):\n    \"\"\"Get list of nodes names.\n\n    Args:\n        nodes(List[nuke.Node]): List of nodes to convert into names.\n\n    Returns:\n        List[str]: Name of passed nodes.\n    \"\"\"\n\n    return [\n        node.name()\n        for node in nodes\n    ]\n\n\ndef get_nodes_by_names(names):\n    \"\"\"Get list of nuke nodes based on their names.\n\n    Args:\n        names (List[str]): List of node names to be found.\n\n    Returns:\n        List[nuke.Node]: List of nodes found by name.\n    \"\"\"\n\n    return [\n        nuke.toNode(name)\n        for name in names\n    ]\n\n\ndef get_viewer_config_from_string(input_string):\n    \"\"\"Convert string to display and viewer string\n\n    Args:\n        input_string (str): string with viewer\n\n    Raises:\n        IndexError: if more then one slash in input string\n        IndexError: if missing closing bracket\n\n    Returns:\n        tuple[str]: display, viewer\n    \"\"\"\n    display = None\n    viewer = input_string\n    # check if () or / or \\ in name\n    if \"/\" in viewer:\n        split = viewer.split(\"/\")\n\n        # rise if more then one column\n        if len(split) > 2:\n            raise IndexError((\n                \"Viewer Input string is not correct. \"\n                \"more then two `/` slashes! {}\"\n            ).format(input_string))\n\n        viewer = split[1]\n        display = split[0]\n    elif \"(\" in viewer:\n        pattern = r\"([\\w\\d\\s\\.\\-]+).*[(](.*)[)]\"\n        result_ = re.findall(pattern, viewer)\n        try:\n            result_ = result_.pop()\n            display = str(result_[1]).rstrip()\n            viewer = str(result_[0]).rstrip()\n        except IndexError:\n            raise IndexError((\n                \"Viewer Input string is not correct. \"\n                \"Missing bracket! {}\"\n            ).format(input_string))\n\n    return (display, viewer)\n\n\ndef create_viewer_profile_string(viewer, display=None, path_like=False):\n    \"\"\"Convert viewer and display to string\n\n    Args:\n        viewer (str): viewer name\n        display (Optional[str]): display name\n        path_like (Optional[bool]): if True, return path like string\n\n    Returns:\n        str: viewer config string\n    \"\"\"\n    if not display:\n        return viewer\n\n    if path_like:\n        return \"{}/{}\".format(display, viewer)\n    return \"{} ({})\".format(viewer, display)\n\n\ndef get_filenames_without_hash(filename, frame_start, frame_end):\n    \"\"\"Get filenames without frame hash\n        i.e. \"renderCompositingMain.baking.0001.exr\"\n\n    Args:\n        filename (str): filename with frame hash\n        frame_start (str): start of the frame\n        frame_end (str): end of the frame\n\n    Returns:\n        list: filename per frame of the sequence\n    \"\"\"\n    filenames = []\n    for frame in range(int(frame_start), (int(frame_end) + 1)):\n        if \"#\" in filename:\n            # use regex to convert #### to {:0>4}\n            def replace(match):\n                return \"{{:0>{}}}\".format(len(match.group()))\n            filename_without_hashes = re.sub(\"#+\", replace, filename)\n            new_filename = filename_without_hashes.format(frame)\n            filenames.append(new_filename)\n    return filenames\n\n\ndef create_camera_node_by_version():\n    \"\"\"Function to create the camera with the latest node class\n    For Nuke version 14.0 or later, the Camera4 camera node class\n        would be used\n    For the version before, the Camera2 camera node class\n        would be used\n    Returns:\n        Node: camera node\n    \"\"\"\n    nuke_number_version = nuke.NUKE_VERSION_MAJOR\n    if nuke_number_version >= 14:\n        return nuke.createNode(\"Camera4\")\n    else:\n        return nuke.createNode(\"Camera2\")\n\n\ndef link_knobs(knobs, node, group_node):\n    \"\"\"Link knobs from inside `group_node`\"\"\"\n\n    missing_knobs = []\n    for knob in knobs:\n        if knob in group_node.knobs():\n            continue\n\n        if knob not in node.knobs().keys():\n            missing_knobs.append(knob)\n\n        link = nuke.Link_Knob(\"\")\n        link.makeLink(node.name(), knob)\n        link.setName(knob)\n        link.setFlag(0x1000)\n        group_node.addKnob(link)\n\n    if missing_knobs:\n        raise ValueError(\n            \"Write node exposed knobs missing:\\n\\n{}\\n\\nPlease review\"\n            \" project settings.\".format(\"\\n\".join(missing_knobs))\n        )\n"
  },
  {
    "path": "openpype/hosts/nuke/api/pipeline.py",
    "content": "import nuke\n\nimport os\nimport importlib\nfrom collections import OrderedDict, defaultdict\n\nimport pyblish.api\n\nimport openpype\nfrom openpype.host import (\n    HostBase,\n    IWorkfileHost,\n    ILoadHost,\n    IPublishHost\n)\nfrom openpype.settings import get_current_project_settings\nfrom openpype.lib import register_event_callback, Logger\nfrom openpype.pipeline import (\n    register_loader_plugin_path,\n    register_creator_plugin_path,\n    register_inventory_action_path,\n    AVALON_CONTAINER_ID,\n    get_current_asset_name,\n    get_current_task_name,\n)\nfrom openpype.pipeline.workfile import BuildWorkfile\nfrom openpype.tools.utils import host_tools\n\nfrom .command import viewer_update_and_undo_stop\nfrom .lib import (\n    Context,\n    ROOT_DATA_KNOB,\n    INSTANCE_DATA_KNOB,\n    get_main_window,\n    add_publish_knob,\n    WorkfileSettings,\n    # TODO: remove this once workfile builder will be removed\n    process_workfile_builder,\n    start_workfile_template_builder,\n    launch_workfiles_app,\n    check_inventory_versions,\n    set_avalon_knob_data,\n    read_avalon_data,\n    on_script_load,\n    dirmap_file_name_filter,\n    add_scripts_menu,\n    add_scripts_gizmo,\n    get_node_data,\n    set_node_data\n)\nfrom .workfile_template_builder import (\n    NukePlaceholderLoadPlugin,\n    NukePlaceholderCreatePlugin,\n    build_workfile_template,\n    create_placeholder,\n    update_placeholder,\n)\nfrom .workio import (\n    open_file,\n    save_file,\n    file_extensions,\n    has_unsaved_changes,\n    work_root,\n    current_file\n)\nfrom .constants import ASSIST\nfrom . import push_to_project\n\nlog = Logger.get_logger(__name__)\n\nHOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.nuke.__file__))\nPLUGINS_DIR = os.path.join(HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\nINVENTORY_PATH = os.path.join(PLUGINS_DIR, \"inventory\")\n\nMENU_LABEL = os.environ[\"AVALON_LABEL\"]\n\n# registering pyblish gui regarding settings in presets\nif os.getenv(\"PYBLISH_GUI\", None):\n    pyblish.api.register_gui(os.getenv(\"PYBLISH_GUI\", None))\n\n\nclass NukeHost(\n    HostBase, IWorkfileHost, ILoadHost, IPublishHost\n):\n    name = \"nuke\"\n\n    def open_workfile(self, filepath):\n        return open_file(filepath)\n\n    def save_workfile(self, filepath=None):\n        return save_file(filepath)\n\n    def work_root(self, session):\n        return work_root(session)\n\n    def get_current_workfile(self):\n        return current_file()\n\n    def workfile_has_unsaved_changes(self):\n        return has_unsaved_changes()\n\n    def get_workfile_extensions(self):\n        return file_extensions()\n\n    def get_workfile_build_placeholder_plugins(self):\n        return [\n            NukePlaceholderLoadPlugin,\n            NukePlaceholderCreatePlugin\n        ]\n\n    def get_containers(self):\n        return ls()\n\n    def install(self):\n        ''' Installing all requarements for Nuke host\n        '''\n\n        pyblish.api.register_host(\"nuke\")\n\n        self.log.info(\"Registering Nuke plug-ins..\")\n        pyblish.api.register_plugin_path(PUBLISH_PATH)\n        register_loader_plugin_path(LOAD_PATH)\n        register_creator_plugin_path(CREATE_PATH)\n        register_inventory_action_path(INVENTORY_PATH)\n\n        # Register Avalon event for workfiles loading.\n        register_event_callback(\"workio.open_file\", check_inventory_versions)\n        register_event_callback(\"taskChanged\", change_context_label)\n\n        _install_menu()\n\n        # add script menu\n        add_scripts_menu()\n        add_scripts_gizmo()\n\n        add_nuke_callbacks()\n\n        launch_workfiles_app()\n\n    def get_context_data(self):\n        root_node = nuke.root()\n        return get_node_data(root_node, ROOT_DATA_KNOB)\n\n    def update_context_data(self, data, changes):\n        root_node = nuke.root()\n        set_node_data(root_node, ROOT_DATA_KNOB, data)\n\n\ndef add_nuke_callbacks():\n    \"\"\" Adding all available nuke callbacks\n    \"\"\"\n    nuke_settings = get_current_project_settings()[\"nuke\"]\n    workfile_settings = WorkfileSettings()\n\n    # Set context settings.\n    nuke.addOnCreate(\n        workfile_settings.set_context_settings, nodeClass=\"Root\")\n\n    # adding favorites to file browser\n    nuke.addOnCreate(workfile_settings.set_favorites, nodeClass=\"Root\")\n\n    # template builder callbacks\n    nuke.addOnCreate(start_workfile_template_builder, nodeClass=\"Root\")\n\n    # TODO: remove this callback once workfile builder will be removed\n    nuke.addOnCreate(process_workfile_builder, nodeClass=\"Root\")\n\n    # fix ffmpeg settings on script\n    nuke.addOnScriptLoad(on_script_load)\n\n    # set checker for last versions on loaded containers\n    nuke.addOnScriptLoad(check_inventory_versions)\n    nuke.addOnScriptSave(check_inventory_versions)\n\n    # set apply all workfile settings on script load and save\n    nuke.addOnScriptLoad(WorkfileSettings().set_context_settings)\n\n\n    if nuke_settings[\"nuke-dirmap\"][\"enabled\"]:\n        log.info(\"Added Nuke's dir-mapping callback ...\")\n        # Add dirmap for file paths.\n        nuke.addFilenameFilter(dirmap_file_name_filter)\n\n    log.info(\"Added Nuke callbacks ...\")\n\n\ndef reload_config():\n    \"\"\"Attempt to reload pipeline at run-time.\n\n    CAUTION: This is primarily for development and debugging purposes.\n\n    \"\"\"\n\n    for module in (\n        \"openpype.hosts.nuke.api.actions\",\n        \"openpype.hosts.nuke.api.menu\",\n        \"openpype.hosts.nuke.api.plugin\",\n        \"openpype.hosts.nuke.api.lib\",\n    ):\n        log.info(\"Reloading module: {}...\".format(module))\n\n        module = importlib.import_module(module)\n\n        try:\n            importlib.reload(module)\n        except AttributeError as e:\n            from importlib import reload\n            log.warning(\"Cannot reload module: {}\".format(e))\n            reload(module)\n\n\ndef _show_workfiles():\n    # Make sure parent is not set\n    # - this makes Workfiles tool as separated window which\n    #   avoid issues with reopening\n    # - it is possible to explicitly change on top flag of the tool\n    host_tools.show_workfiles(parent=None, on_top=False)\n\n\ndef get_context_label():\n    return \"{0}, {1}\".format(\n        get_current_asset_name(),\n        get_current_task_name()\n    )\n\n\ndef _install_menu():\n    \"\"\"Install Avalon menu into Nuke's main menu bar.\"\"\"\n\n    # uninstall original avalon menu\n    main_window = get_main_window()\n    menubar = nuke.menu(\"Nuke\")\n    menu = menubar.addMenu(MENU_LABEL)\n\n    if not ASSIST:\n        label = get_context_label()\n        context_action_item = menu.addCommand(\"Context\")\n        context_action_item.setEnabled(False)\n\n        Context.context_action_item = context_action_item\n\n        context_action = context_action_item.action()\n        context_action.setText(label)\n\n        # add separator after context label\n        menu.addSeparator()\n\n    menu.addCommand(\n        \"Work Files...\",\n        _show_workfiles\n    )\n\n    menu.addSeparator()\n    if not ASSIST:\n        # only add parent if nuke version is 14 or higher\n        # known issue with no solution yet\n        menu.addCommand(\n            \"Create...\",\n            lambda: host_tools.show_publisher(\n                parent=main_window,\n                tab=\"create\"\n            )\n        )\n        # only add parent if nuke version is 14 or higher\n        # known issue with no solution yet\n        menu.addCommand(\n            \"Publish...\",\n            lambda: host_tools.show_publisher(\n                parent=main_window,\n                tab=\"publish\"\n            )\n        )\n\n    menu.addCommand(\n        \"Load...\",\n        lambda: host_tools.show_loader(\n            parent=main_window,\n            use_context=True\n        )\n    )\n    menu.addCommand(\n        \"Manage...\",\n        lambda: host_tools.show_scene_inventory(parent=main_window)\n    )\n    menu.addSeparator()\n    menu.addCommand(\n        \"Library...\",\n        lambda: host_tools.show_library_loader(\n            parent=main_window\n        )\n    )\n    menu.addSeparator()\n    menu.addCommand(\n        \"Set Resolution\",\n        lambda: WorkfileSettings().reset_resolution()\n    )\n    menu.addCommand(\n        \"Set Frame Range\",\n        lambda: WorkfileSettings().reset_frame_range_handles()\n    )\n    menu.addCommand(\n        \"Set Colorspace\",\n        lambda: WorkfileSettings().set_colorspace()\n    )\n    menu.addCommand(\n        \"Apply All Settings\",\n        lambda: WorkfileSettings().set_context_settings()\n    )\n\n    menu.addSeparator()\n    menu.addCommand(\n        \"Build Workfile\",\n        lambda: BuildWorkfile().process()\n    )\n\n    menu_template = menu.addMenu(\"Template Builder\")  # creating template menu\n    menu_template.addCommand(\n        \"Build Workfile from template\",\n        lambda: build_workfile_template()\n    )\n\n    if not ASSIST:\n        menu_template.addSeparator()\n        menu_template.addCommand(\n            \"Create Place Holder\",\n            lambda: create_placeholder()\n        )\n        menu_template.addCommand(\n            \"Update Place Holder\",\n            lambda: update_placeholder()\n        )\n\n    menu.addSeparator()\n    menu.addCommand(\n        \"Experimental tools...\",\n        lambda: host_tools.show_experimental_tools_dialog(parent=main_window)\n    )\n    menu.addCommand(\n        \"Push to Project\",\n        lambda: push_to_project.main()\n    )\n    menu.addSeparator()\n    # add reload pipeline only in debug mode\n    if bool(os.getenv(\"NUKE_DEBUG\")):\n        menu.addSeparator()\n        menu.addCommand(\"Reload Pipeline\", reload_config)\n\n    # adding shortcuts\n    add_shortcuts_from_presets()\n\n\ndef change_context_label():\n    if ASSIST:\n        return\n\n    context_action_item = Context.context_action_item\n    if context_action_item is None:\n        return\n    context_action = context_action_item.action()\n\n    old_label = context_action.text()\n    new_label = get_context_label()\n\n    context_action.setText(new_label)\n\n    log.info(\"Task label changed from `{}` to `{}`\".format(\n        old_label, new_label))\n\n\ndef add_shortcuts_from_presets():\n    menubar = nuke.menu(\"Nuke\")\n    nuke_presets = get_current_project_settings()[\"nuke\"][\"general\"]\n\n    if nuke_presets.get(\"menu\"):\n        menu_label_mapping = {\n            \"create\": \"Create...\",\n            \"manage\": \"Manage...\",\n            \"load\": \"Load...\",\n            \"build_workfile\": \"Build Workfile\",\n            \"publish\": \"Publish...\"\n        }\n\n        for command_name, shortcut_str in nuke_presets.get(\"menu\").items():\n            log.info(\"menu_name `{}` | menu_label `{}`\".format(\n                command_name, MENU_LABEL\n            ))\n            log.info(\"Adding Shortcut `{}` to `{}`\".format(\n                shortcut_str, command_name\n            ))\n            try:\n                menu = menubar.findItem(MENU_LABEL)\n                item_label = menu_label_mapping[command_name]\n                menuitem = menu.findItem(item_label)\n                menuitem.setShortcut(shortcut_str)\n            except (AttributeError, KeyError) as e:\n                log.error(e)\n\n\ndef containerise(node,\n                 name,\n                 namespace,\n                 context,\n                 loader=None,\n                 data=None):\n    \"\"\"Bundle `node` into an assembly and imprint it with metadata\n\n    Containerisation enables a tracking of version, author and origin\n    for loaded assets.\n\n    Arguments:\n        node (nuke.Node): Nuke's node object to imprint as container\n        name (str): Name of resulting assembly\n        namespace (str): Namespace under which to host container\n        context (dict): Asset information\n        loader (str, optional): Name of node used to produce this container.\n\n    Returns:\n        node (nuke.Node): containerised nuke's node object\n\n    \"\"\"\n    data = OrderedDict(\n        [\n            (\"schema\", \"openpype:container-2.0\"),\n            (\"id\", AVALON_CONTAINER_ID),\n            (\"name\", name),\n            (\"namespace\", namespace),\n            (\"loader\", str(loader)),\n            (\"representation\", context[\"representation\"][\"_id\"]),\n        ],\n\n        **data or dict()\n    )\n\n    set_avalon_knob_data(node, data)\n\n    # set tab to first native\n    node.setTab(0)\n\n    return node\n\n\ndef parse_container(node):\n    \"\"\"Returns containerised data of a node\n\n    Reads the imprinted data from `containerise`.\n\n    Arguments:\n        node (nuke.Node): Nuke's node object to read imprinted data\n\n    Returns:\n        dict: The container schema data for this container node.\n\n    \"\"\"\n    data = read_avalon_data(node)\n\n    # If not all required data return the empty container\n    required = [\"schema\", \"id\", \"name\",\n                \"namespace\", \"loader\", \"representation\"]\n    if not all(key in data for key in required):\n        return\n\n    # Store the node's name\n    data.update({\n        \"objectName\": node.fullName(),\n        \"node\": node,\n    })\n\n    return data\n\n\ndef update_container(node, keys=None):\n    \"\"\"Returns node with updateted containder data\n\n    Arguments:\n        node (nuke.Node): The node in Nuke to imprint as container,\n        keys (dict, optional): data which should be updated\n\n    Returns:\n        node (nuke.Node): nuke node with updated container data\n\n    Raises:\n        TypeError on given an invalid container node\n\n    \"\"\"\n    keys = keys or dict()\n\n    container = parse_container(node)\n    if not container:\n        raise TypeError(\"Not a valid container node.\")\n\n    container.update(keys)\n    node = set_avalon_knob_data(node, container)\n\n    return node\n\n\ndef ls():\n    \"\"\"List available containers.\n\n    This function is used by the Container Manager in Nuke. You'll\n    need to implement a for-loop that then *yields* one Container at\n    a time.\n\n    See the `container.json` schema for details on how it should look,\n    and the Maya equivalent, which is in `avalon.maya.pipeline`\n    \"\"\"\n    all_nodes = nuke.allNodes(recurseGroups=False)\n\n    nodes = [n for n in all_nodes]\n\n    for n in nodes:\n        container = parse_container(n)\n        if container:\n            yield container\n\n\ndef list_instances(creator_id=None):\n    \"\"\"List all created instances to publish from current workfile.\n\n    For SubsetManager\n\n    Args:\n        creator_id (Optional[str]): creator identifier\n\n    Returns:\n        (list) of dictionaries matching instances format\n    \"\"\"\n    instances_by_order = defaultdict(list)\n    subset_instances = []\n    instance_ids = set()\n\n    for node in nuke.allNodes(recurseGroups=True):\n\n        if node.Class() in [\"Viewer\", \"Dot\"]:\n            continue\n\n        try:\n            if node[\"disable\"].value():\n                continue\n        except NameError:\n            # pass if disable knob doesn't exist\n            pass\n\n        # get data from avalon knob\n        instance_data = get_node_data(\n            node, INSTANCE_DATA_KNOB)\n\n        if not instance_data:\n            continue\n\n        if instance_data[\"id\"] != \"pyblish.avalon.instance\":\n            continue\n\n        if creator_id and instance_data[\"creator_identifier\"] != creator_id:\n            continue\n\n        instance_id = instance_data.get(\"instance_id\")\n        if not instance_id:\n            pass\n        elif instance_id in instance_ids:\n            instance_data.pop(\"instance_id\")\n        else:\n            instance_ids.add(instance_id)\n\n        # node name could change, so update subset name data\n        _update_subset_name_data(instance_data, node)\n\n        if \"render_order\" not in node.knobs():\n            subset_instances.append((node, instance_data))\n            continue\n\n        order = int(node[\"render_order\"].value())\n        instances_by_order[order].append((node, instance_data))\n\n    # Sort instances based on order attribute or subset name.\n    # TODO: remove in future Publisher enhanced with sorting\n    ordered_instances = []\n    for key in sorted(instances_by_order.keys()):\n        instances_by_subset = defaultdict(list)\n        for node, data_ in instances_by_order[key]:\n            instances_by_subset[data_[\"subset\"]].append((node, data_))\n        for subkey in sorted(instances_by_subset.keys()):\n            ordered_instances.extend(instances_by_subset[subkey])\n\n    instances_by_subset = defaultdict(list)\n    for node, data_ in subset_instances:\n        instances_by_subset[data_[\"subset\"]].append((node, data_))\n    for key in sorted(instances_by_subset.keys()):\n        ordered_instances.extend(instances_by_subset[key])\n\n    return ordered_instances\n\n\ndef _update_subset_name_data(instance_data, node):\n    \"\"\"Update subset name data in instance data.\n\n    Args:\n        instance_data (dict): instance creator data\n        node (nuke.Node): nuke node\n    \"\"\"\n    # make sure node name is subset name\n    old_subset_name = instance_data[\"subset\"]\n    old_variant = instance_data[\"variant\"]\n    subset_name_root = old_subset_name.replace(old_variant, \"\")\n\n    new_subset_name = node.name()\n    new_variant = new_subset_name.replace(subset_name_root, \"\")\n\n    instance_data[\"subset\"] = new_subset_name\n    instance_data[\"variant\"] = new_variant\n\n\ndef remove_instance(instance):\n    \"\"\"Remove instance from current workfile metadata.\n\n    For SubsetManager\n\n    Args:\n        instance (dict): instance representation from subsetmanager model\n    \"\"\"\n    instance_node = instance.transient_data[\"node\"]\n    instance_knob = instance_node.knobs()[INSTANCE_DATA_KNOB]\n    instance_node.removeKnob(instance_knob)\n    nuke.delete(instance_node)\n\n\ndef select_instance(instance):\n    \"\"\"\n        Select instance in Node View\n\n        Args:\n            instance (dict): instance representation from subsetmanager model\n    \"\"\"\n    instance_node = instance.transient_data[\"node\"]\n    instance_node[\"selected\"].setValue(True)\n"
  },
  {
    "path": "openpype/hosts/nuke/api/plugin.py",
    "content": "import nuke\nimport re\nimport os\nimport sys\nimport six\nimport random\nimport string\nfrom collections import OrderedDict, defaultdict\nfrom abc import abstractmethod\n\nfrom openpype.settings import get_current_project_settings\nfrom openpype.lib import (\n    BoolDef,\n    EnumDef\n)\nfrom openpype.pipeline import (\n    LegacyCreator,\n    LoaderPlugin,\n    CreatorError,\n    Creator as NewCreator,\n    CreatedInstance,\n    get_current_task_name\n)\nfrom openpype.pipeline.colorspace import (\n    get_display_view_colorspace_name,\n    get_colorspace_settings_from_publish_context,\n    set_colorspace_data_to_representation\n)\nfrom openpype.lib.transcoding import (\n    VIDEO_EXTENSIONS\n)\nfrom .lib import (\n    INSTANCE_DATA_KNOB,\n    Knobby,\n    check_subsetname_exists,\n    maintained_selection,\n    get_avalon_knob_data,\n    set_avalon_knob_data,\n    add_publish_knob,\n    get_nuke_imageio_settings,\n    set_node_knobs_from_settings,\n    set_node_data,\n    get_node_data,\n    get_view_process_node,\n    get_viewer_config_from_string,\n    deprecated,\n    get_filenames_without_hash,\n    link_knobs\n)\nfrom .pipeline import (\n    list_instances,\n    remove_instance\n)\n\n\ndef _collect_and_cache_nodes(creator):\n    key = \"openpype.nuke.nodes\"\n    if key not in creator.collection_shared_data:\n        instances_by_identifier = defaultdict(list)\n        for item in list_instances():\n            _, instance_data = item\n            identifier = instance_data[\"creator_identifier\"]\n            instances_by_identifier[identifier].append(item)\n        creator.collection_shared_data[key] = instances_by_identifier\n    return creator.collection_shared_data[key]\n\n\nclass NukeCreatorError(CreatorError):\n    pass\n\n\nclass NukeCreator(NewCreator):\n    selected_nodes = []\n\n    def pass_pre_attributes_to_instance(\n        self,\n        instance_data,\n        pre_create_data,\n        keys=None\n    ):\n        if not keys:\n            keys = pre_create_data.keys()\n\n        creator_attrs = instance_data[\"creator_attributes\"] = {}\n        for pass_key in keys:\n            creator_attrs[pass_key] = pre_create_data[pass_key]\n\n    def check_existing_subset(self, subset_name):\n        \"\"\"Make sure subset name is unique.\n\n        It search within all nodes recursively\n        and checks if subset name is found in\n        any node having instance data knob.\n\n        Arguments:\n            subset_name (str): Subset name\n        \"\"\"\n\n        for node in nuke.allNodes(recurseGroups=True):\n            # make sure testing node is having instance knob\n            if INSTANCE_DATA_KNOB not in node.knobs().keys():\n                continue\n            node_data = get_node_data(node, INSTANCE_DATA_KNOB)\n\n            if not node_data:\n                # a node has no instance data\n                continue\n\n            # test if subset name is matching\n            if node_data.get(\"subset\") == subset_name:\n                raise NukeCreatorError(\n                    (\n                        \"A publish instance for '{}' already exists \"\n                        \"in nodes! Please change the variant \"\n                        \"name to ensure unique output.\"\n                    ).format(subset_name)\n                )\n\n    def create_instance_node(\n        self,\n        node_name,\n        knobs=None,\n        parent=None,\n        node_type=None\n    ):\n        \"\"\"Create node representing instance.\n\n        Arguments:\n            node_name (str): Name of the new node.\n            knobs (OrderedDict): node knobs name and values\n            parent (str): Name of the parent node.\n            node_type (str, optional): Nuke node Class.\n\n        Returns:\n            nuke.Node: Newly created instance node.\n\n        \"\"\"\n        node_type = node_type or \"NoOp\"\n\n        node_knobs = knobs or {}\n\n        # set parent node\n        parent_node = nuke.root()\n        if parent:\n            parent_node = nuke.toNode(parent)\n\n        try:\n            with parent_node:\n                created_node = nuke.createNode(node_type)\n                created_node[\"name\"].setValue(node_name)\n\n                for key, values in node_knobs.items():\n                    if key in created_node.knobs():\n                        created_node[\"key\"].setValue(values)\n        except Exception as _err:\n            raise NukeCreatorError(\"Creating have failed: {}\".format(_err))\n\n        return created_node\n\n    def set_selected_nodes(self, pre_create_data):\n        if pre_create_data.get(\"use_selection\"):\n            self.selected_nodes = nuke.selectedNodes()\n            if self.selected_nodes == []:\n                raise NukeCreatorError(\"Creator error: No active selection\")\n        else:\n            self.selected_nodes = []\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        # make sure selected nodes are added\n        self.set_selected_nodes(pre_create_data)\n\n        # make sure subset name is unique\n        self.check_existing_subset(subset_name)\n\n        try:\n            instance_node = self.create_instance_node(\n                subset_name,\n                node_type=instance_data.pop(\"node_type\", None)\n            )\n            instance = CreatedInstance(\n                self.family,\n                subset_name,\n                instance_data,\n                self\n            )\n\n            instance.transient_data[\"node\"] = instance_node\n\n            self._add_instance_to_context(instance)\n\n            set_node_data(\n                instance_node, INSTANCE_DATA_KNOB, instance.data_to_store())\n\n            return instance\n\n        except Exception as er:\n            six.reraise(\n                NukeCreatorError,\n                NukeCreatorError(\"Creator error: {}\".format(er)),\n                sys.exc_info()[2])\n\n    def collect_instances(self):\n        cached_instances = _collect_and_cache_nodes(self)\n        attr_def_keys = {\n            attr_def.key\n            for attr_def in self.get_instance_attr_defs()\n        }\n        attr_def_keys.discard(None)\n\n        for (node, data) in cached_instances[self.identifier]:\n            created_instance = CreatedInstance.from_existing(\n                data, self\n            )\n            created_instance.transient_data[\"node\"] = node\n            self._add_instance_to_context(created_instance)\n\n            for key in (\n                set(created_instance[\"creator_attributes\"].keys())\n                - attr_def_keys\n            ):\n                created_instance[\"creator_attributes\"].pop(key)\n\n    def update_instances(self, update_list):\n        for created_inst, changes in update_list:\n            instance_node = created_inst.transient_data[\"node\"]\n\n            # update instance node name if subset name changed\n            if \"subset\" in changes.changed_keys:\n                instance_node[\"name\"].setValue(\n                    changes[\"subset\"].new_value\n                )\n\n            # in case node is not existing anymore (user erased it manually)\n            try:\n                instance_node.fullName()\n            except ValueError:\n                self.remove_instances([created_inst])\n                continue\n\n            set_node_data(\n                instance_node,\n                INSTANCE_DATA_KNOB,\n                created_inst.data_to_store()\n            )\n\n    def remove_instances(self, instances):\n        for instance in instances:\n            remove_instance(instance)\n            self._remove_instance_from_context(instance)\n\n    def get_pre_create_attr_defs(self):\n        return [\n            BoolDef(\n                \"use_selection\",\n                default=not self.create_context.headless,\n                label=\"Use selection\"\n            )\n        ]\n\n    def get_creator_settings(self, project_settings, settings_key=None):\n        if not settings_key:\n            settings_key = self.__class__.__name__\n        return project_settings[\"nuke\"][\"create\"][settings_key]\n\n\nclass NukeWriteCreator(NukeCreator):\n    \"\"\"Add Publishable Write node\"\"\"\n\n    identifier = \"create_write\"\n    label = \"Create Write\"\n    family = \"write\"\n    icon = \"sign-out\"\n\n    def get_linked_knobs(self):\n        linked_knobs = []\n        if \"channels\" in self.instance_attributes:\n            linked_knobs.append(\"channels\")\n        if \"ordered\" in self.instance_attributes:\n            linked_knobs.append(\"render_order\")\n        if \"use_range_limit\" in self.instance_attributes:\n            linked_knobs.extend([\"___\", \"first\", \"last\", \"use_limit\"])\n\n        return linked_knobs\n\n    def integrate_links(self, node, outputs=True):\n        # skip if no selection\n        if not self.selected_node:\n            return\n\n        # collect dependencies\n        input_nodes = [self.selected_node]\n        dependent_nodes = self.selected_node.dependent() if outputs else []\n\n        # relinking to collected connections\n        for i, input in enumerate(input_nodes):\n            node.setInput(i, input)\n\n        # make it nicer in graph\n        node.autoplace()\n\n        # relink also dependent nodes\n        for dep_nodes in dependent_nodes:\n            dep_nodes.setInput(0, node)\n\n    def set_selected_nodes(self, pre_create_data):\n        if pre_create_data.get(\"use_selection\"):\n            selected_nodes = nuke.selectedNodes()\n            if selected_nodes == []:\n                raise NukeCreatorError(\"Creator error: No active selection\")\n            elif len(selected_nodes) > 1:\n                NukeCreatorError(\"Creator error: Select only one camera node\")\n            self.selected_node = selected_nodes[0]\n        else:\n            self.selected_node = None\n\n    def get_pre_create_attr_defs(self):\n        attr_defs = [\n            BoolDef(\"use_selection\", label=\"Use selection\"),\n            self._get_render_target_enum()\n        ]\n        return attr_defs\n\n    def get_instance_attr_defs(self):\n        attr_defs = [\n            self._get_render_target_enum(),\n        ]\n        # add reviewable attribute\n        if \"reviewable\" in self.instance_attributes:\n            attr_defs.append(self._get_reviewable_bool())\n\n        return attr_defs\n\n    def _get_render_target_enum(self):\n        rendering_targets = {\n            \"local\": \"Local machine rendering\",\n            \"frames\": \"Use existing frames\"\n        }\n        if (\"farm_rendering\" in self.instance_attributes):\n            rendering_targets[\"frames_farm\"] = \"Use existing frames - farm\"\n            rendering_targets[\"farm\"] = \"Farm rendering\"\n\n        return EnumDef(\n            \"render_target\",\n            items=rendering_targets,\n            label=\"Render target\"\n        )\n\n    def _get_reviewable_bool(self):\n        return BoolDef(\n            \"review\",\n            default=True,\n            label=\"Review\"\n        )\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # make sure selected nodes are added\n        self.set_selected_nodes(pre_create_data)\n\n        # make sure subset name is unique\n        self.check_existing_subset(subset_name)\n\n        instance_node = self.create_instance_node(\n            subset_name,\n            instance_data\n        )\n\n        try:\n            instance = CreatedInstance(\n                self.family,\n                subset_name,\n                instance_data,\n                self\n            )\n\n            instance.transient_data[\"node\"] = instance_node\n\n            self._add_instance_to_context(instance)\n\n            set_node_data(\n                instance_node, INSTANCE_DATA_KNOB, instance.data_to_store())\n\n            return instance\n\n        except Exception as er:\n            six.reraise(\n                NukeCreatorError,\n                NukeCreatorError(\"Creator error: {}\".format(er)),\n                sys.exc_info()[2]\n            )\n\n    def apply_settings(self, project_settings):\n        \"\"\"Method called on initialization of plugin to apply settings.\"\"\"\n\n        # plugin settings\n        plugin_settings = self.get_creator_settings(project_settings)\n\n        # individual attributes\n        self.instance_attributes = plugin_settings.get(\n            \"instance_attributes\") or self.instance_attributes\n        self.prenodes = plugin_settings[\"prenodes\"]\n        self.default_variants = plugin_settings.get(\n            \"default_variants\") or self.default_variants\n        self.temp_rendering_path_template = (\n            plugin_settings.get(\"temp_rendering_path_template\")\n            or self.temp_rendering_path_template\n        )\n\n\nclass OpenPypeCreator(LegacyCreator):\n    \"\"\"Pype Nuke Creator class wrapper\"\"\"\n    node_color = \"0xdfea5dff\"\n\n    def __init__(self, *args, **kwargs):\n        super(OpenPypeCreator, self).__init__(*args, **kwargs)\n        if check_subsetname_exists(\n                nuke.allNodes(),\n                self.data[\"subset\"]):\n            msg = (\"The subset name `{0}` is already used on a node in\"\n                   \"this workfile.\".format(self.data[\"subset\"]))\n            self.log.error(msg + \"\\n\\nPlease use other subset name!\")\n            raise NameError(\"`{0}: {1}\".format(__name__, msg))\n        return\n\n    def process(self):\n        from nukescripts import autoBackdrop\n\n        instance = None\n\n        if (self.options or {}).get(\"useSelection\"):\n\n            nodes = nuke.selectedNodes()\n            if not nodes:\n                nuke.message(\"Please select nodes that you \"\n                             \"wish to add to a container\")\n                return\n\n            elif len(nodes) == 1:\n                # only one node is selected\n                instance = nodes[0]\n\n        if not instance:\n            # Not using selection or multiple nodes selected\n            bckd_node = autoBackdrop()\n            bckd_node[\"tile_color\"].setValue(int(self.node_color, 16))\n            bckd_node[\"note_font_size\"].setValue(24)\n            bckd_node[\"label\"].setValue(\"[{}]\".format(self.name))\n\n            instance = bckd_node\n\n        # add avalon knobs\n        set_avalon_knob_data(instance, self.data)\n        add_publish_knob(instance)\n\n        return instance\n\n\ndef get_instance_group_node_childs(instance):\n    \"\"\"Return list of instance group node children\n\n    Args:\n        instance (pyblish.Instance): pyblish instance\n\n    Returns:\n        list: [nuke.Node]\n    \"\"\"\n    node = instance.data[\"transientData\"][\"node\"]\n\n    if node.Class() != \"Group\":\n        return\n\n    # collect child nodes\n    child_nodes = []\n    # iterate all nodes\n    for node in nuke.allNodes(group=node):\n        # add contained nodes to instance's node list\n        child_nodes.append(node)\n\n    return child_nodes\n\n\ndef get_colorspace_from_node(node):\n    # Add version data to instance\n    colorspace = node[\"colorspace\"].value()\n\n    # remove default part of the string\n    if \"default (\" in colorspace:\n        colorspace = re.sub(r\"default.\\(|\\)\", \"\", colorspace)\n\n    return colorspace\n\n\ndef get_review_presets_config():\n    settings = get_current_project_settings()\n    review_profiles = (\n        settings[\"global\"]\n        [\"publish\"]\n        [\"ExtractReview\"]\n        [\"profiles\"]\n    )\n\n    outputs = {}\n    for profile in review_profiles:\n        outputs.update(profile.get(\"outputs\", {}))\n\n    return [str(name) for name, _prop in outputs.items()]\n\n\nclass NukeLoader(LoaderPlugin):\n    container_id_knob = \"containerId\"\n    container_id = None\n\n    def reset_container_id(self):\n        self.container_id = \"\".join(random.choice(\n            string.ascii_uppercase + string.digits) for _ in range(10))\n\n    def get_container_id(self, node):\n        id_knob = node.knobs().get(self.container_id_knob)\n        return id_knob.value() if id_knob else None\n\n    def get_members(self, source):\n        \"\"\"Return nodes that has same \"containerId\" as `source`\"\"\"\n        source_id = self.get_container_id(source)\n        return [node for node in nuke.allNodes(recurseGroups=True)\n                if self.get_container_id(node) == source_id\n                and node is not source] if source_id else []\n\n    def set_as_member(self, node):\n        source_id = self.get_container_id(node)\n\n        if source_id:\n            node[self.container_id_knob].setValue(source_id)\n        else:\n            HIDEN_FLAG = 0x00040000\n            _knob = Knobby(\n                \"String_Knob\",\n                self.container_id,\n                flags=[\n                    nuke.READ_ONLY,\n                    HIDEN_FLAG\n                ])\n            knob = _knob.create(self.container_id_knob)\n            node.addKnob(knob)\n\n    def clear_members(self, parent_node):\n        parent_class = parent_node.Class()\n        members = self.get_members(parent_node)\n\n        dependent_nodes = None\n        for node in members:\n            _depndc = [n for n in node.dependent() if n not in members]\n            if not _depndc:\n                continue\n\n            dependent_nodes = _depndc\n            break\n\n        for member in members:\n            if member.Class() == parent_class:\n                continue\n            self.log.info(\"removing node: `{}\".format(member.name()))\n            nuke.delete(member)\n\n        return dependent_nodes\n\n\nclass ExporterReview(object):\n    \"\"\"\n    Base class object for generating review data from Nuke\n\n    Args:\n        klass (pyblish.plugin): pyblish plugin parent\n        instance (pyblish.instance): instance of pyblish context\n\n    \"\"\"\n    data = None\n    publish_on_farm = False\n\n    def __init__(self,\n                 klass,\n                 instance,\n                 multiple_presets=True\n                 ):\n\n        self.log = klass.log\n        self.instance = instance\n        self.multiple_presets = multiple_presets\n        self.path_in = self.instance.data.get(\"path\", None)\n        self.staging_dir = self.instance.data[\"stagingDir\"]\n        self.collection = self.instance.data.get(\"collection\", None)\n        self.data = {\"representations\": []}\n\n    def get_file_info(self):\n        if self.collection:\n            # get path\n            self.fname = os.path.basename(\n                self.collection.format(\"{head}{padding}{tail}\")\n            )\n            self.fhead = self.collection.format(\"{head}\")\n\n            # get first and last frame\n            self.first_frame = min(self.collection.indexes)\n            self.last_frame = max(self.collection.indexes)\n\n            # make sure slate frame is not included\n            frame_start_handle = self.instance.data[\"frameStartHandle\"]\n            if frame_start_handle > self.first_frame:\n                self.first_frame = frame_start_handle\n\n        else:\n            self.fname = os.path.basename(self.path_in)\n            self.fhead = os.path.splitext(self.fname)[0] + \".\"\n            self.first_frame = self.instance.data[\"frameStartHandle\"]\n            self.last_frame = self.instance.data[\"frameEndHandle\"]\n\n        if \"#\" in self.fhead:\n            self.fhead = self.fhead.replace(\"#\", \"\")[:-1]\n\n    def get_representation_data(\n        self, tags=None, range=False,\n        custom_tags=None, colorspace=None\n    ):\n        \"\"\" Add representation data to self.data\n\n        Args:\n            tags (list[str], optional): list of defined tags.\n                                        Defaults to None.\n            range (bool, optional): flag for adding ranges.\n                                    Defaults to False.\n            custom_tags (list[str], optional): user inputted custom tags.\n                                               Defaults to None.\n        \"\"\"\n        add_tags = tags or []\n        repre = {\n            \"name\": self.name,\n            \"ext\": self.ext,\n            \"files\": self.file,\n            \"stagingDir\": self.staging_dir,\n            \"tags\": [self.name.replace(\"_\", \"-\")] + add_tags\n        }\n\n        if custom_tags:\n            repre[\"custom_tags\"] = custom_tags\n\n        if range:\n            repre.update({\n                \"frameStart\": self.first_frame,\n                \"frameEnd\": self.last_frame,\n            })\n        if \".{}\".format(self.ext) not in VIDEO_EXTENSIONS:\n            filenames = get_filenames_without_hash(\n                self.file, self.first_frame, self.last_frame)\n            repre[\"files\"] = filenames\n\n        if self.multiple_presets:\n            repre[\"outputName\"] = self.name\n\n        if self.publish_on_farm:\n            repre[\"tags\"].append(\"publish_on_farm\")\n\n        # add colorspace data to representation\n        if colorspace:\n            set_colorspace_data_to_representation(\n                repre,\n                self.instance.context.data,\n                colorspace=colorspace,\n                log=self.log\n            )\n        self.data[\"representations\"].append(repre)\n\n    def get_imageio_baking_profile(self):\n        from . import lib as opnlib\n        nuke_imageio = opnlib.get_nuke_imageio_settings()\n\n        # TODO: this is only securing backward compatibility lets remove\n        # this once all projects's anatomy are updated to newer config\n        if \"baking\" in nuke_imageio.keys():\n            return nuke_imageio[\"baking\"][\"viewerProcess\"]\n        else:\n            return nuke_imageio[\"viewer\"][\"viewerProcess\"]\n\n\nclass ExporterReviewLut(ExporterReview):\n    \"\"\"\n    Generator object for review lut from Nuke\n\n    Args:\n        klass (pyblish.plugin): pyblish plugin parent\n        instance (pyblish.instance): instance of pyblish context\n\n\n    \"\"\"\n    _temp_nodes = []\n\n    def __init__(self,\n                 klass,\n                 instance,\n                 name=None,\n                 ext=None,\n                 cube_size=None,\n                 lut_size=None,\n                 lut_style=None,\n                 multiple_presets=True):\n        # initialize parent class\n        super(ExporterReviewLut, self).__init__(\n            klass, instance, multiple_presets)\n\n        # deal with now lut defined in viewer lut\n        if hasattr(klass, \"viewer_lut_raw\"):\n            self.viewer_lut_raw = klass.viewer_lut_raw\n        else:\n            self.viewer_lut_raw = False\n\n        self.name = name or \"baked_lut\"\n        self.ext = ext or \"cube\"\n        self.cube_size = cube_size or 32\n        self.lut_size = lut_size or 1024\n        self.lut_style = lut_style or \"linear\"\n\n        # set frame start / end and file name to self\n        self.get_file_info()\n\n        self.log.info(\"File info was set...\")\n\n        self.file = self.fhead + self.name + \".{}\".format(self.ext)\n        self.path = os.path.join(\n            self.staging_dir, self.file).replace(\"\\\\\", \"/\")\n\n    def clean_nodes(self):\n        for node in self._temp_nodes:\n            nuke.delete(node)\n        self._temp_nodes = []\n        self.log.info(\"Deleted nodes...\")\n\n    def generate_lut(self, **kwargs):\n        bake_viewer_process = kwargs[\"bake_viewer_process\"]\n        bake_viewer_input_process_node = kwargs[\n            \"bake_viewer_input_process\"]\n\n        # ---------- start nodes creation\n\n        # CMSTestPattern\n        cms_node = nuke.createNode(\"CMSTestPattern\")\n        cms_node[\"cube_size\"].setValue(self.cube_size)\n        # connect\n        self._temp_nodes.append(cms_node)\n        self.previous_node = cms_node\n\n        if bake_viewer_process:\n            # Node View Process\n            if bake_viewer_input_process_node:\n                ipn = get_view_process_node()\n                if ipn is not None:\n                    # connect\n                    ipn.setInput(0, self.previous_node)\n                    self._temp_nodes.append(ipn)\n                    self.previous_node = ipn\n                    self.log.debug(\n                        \"ViewProcess...   `{}`\".format(self._temp_nodes))\n\n            if not self.viewer_lut_raw:\n                # OCIODisplay\n                dag_node = nuke.createNode(\"OCIODisplay\")\n                # connect\n                dag_node.setInput(0, self.previous_node)\n                self._temp_nodes.append(dag_node)\n                self.previous_node = dag_node\n                self.log.debug(\n                    \"OCIODisplay...   `{}`\".format(self._temp_nodes))\n\n        # GenerateLUT\n        gen_lut_node = nuke.createNode(\"GenerateLUT\")\n        gen_lut_node[\"file\"].setValue(self.path)\n        gen_lut_node[\"file_type\"].setValue(\".{}\".format(self.ext))\n        gen_lut_node[\"lut1d\"].setValue(self.lut_size)\n        gen_lut_node[\"style1d\"].setValue(self.lut_style)\n        # connect\n        gen_lut_node.setInput(0, self.previous_node)\n        self._temp_nodes.append(gen_lut_node)\n        # ---------- end nodes creation\n\n        # Export lut file\n        nuke.execute(\n            gen_lut_node.name(),\n            int(self.first_frame),\n            int(self.first_frame))\n\n        self.log.info(\"Exported...\")\n\n        # ---------- generate representation data\n        self.get_representation_data()\n\n        # ---------- Clean up\n        self.clean_nodes()\n\n        return self.data\n\n\nclass ExporterReviewMov(ExporterReview):\n    \"\"\"\n    Metaclass for generating review mov files\n\n    Args:\n        klass (pyblish.plugin): pyblish plugin parent\n        instance (pyblish.instance): instance of pyblish context\n\n    \"\"\"\n    _temp_nodes = {}\n\n    def __init__(self,\n                 klass,\n                 instance,\n                 name=None,\n                 ext=None,\n                 multiple_presets=True\n                 ):\n        # initialize parent class\n        super(ExporterReviewMov, self).__init__(\n            klass, instance, multiple_presets)\n        # passing presets for nodes to self\n        self.nodes = klass.nodes if hasattr(klass, \"nodes\") else {}\n\n        # deal with now lut defined in viewer lut\n        self.viewer_lut_raw = klass.viewer_lut_raw\n        self.write_colorspace = instance.data[\"colorspace\"]\n\n        self.name = name or \"baked\"\n        self.ext = ext or \"mov\"\n\n        # set frame start / end and file name to self\n        self.get_file_info()\n\n        self.log.info(\"File info was set...\")\n\n        if \".{}\".format(self.ext) in VIDEO_EXTENSIONS:\n            self.file = \"{}{}.{}\".format(\n                self.fhead, self.name, self.ext)\n        else:\n            # Output is image (or image sequence)\n            # When the file is an image it's possible it\n            # has extra information after the `fhead` that\n            # we want to preserve, e.g. like frame numbers\n            # or frames hashes like `####`\n            filename_no_ext = os.path.splitext(\n                os.path.basename(self.path_in))[0]\n            after_head = filename_no_ext[len(self.fhead):]\n            self.file = \"{}{}.{}.{}\".format(\n                self.fhead, self.name, after_head, self.ext)\n        self.path = os.path.join(\n            self.staging_dir, self.file).replace(\"\\\\\", \"/\")\n\n    def clean_nodes(self, node_name):\n        for node in self._temp_nodes[node_name]:\n            nuke.delete(node)\n        self._temp_nodes[node_name] = []\n        self.log.info(\"Deleted nodes...\")\n\n    def render(self, render_node_name):\n        self.log.info(\"Rendering...  \")\n        # Render Write node\n        nuke.execute(\n            render_node_name,\n            int(self.first_frame),\n            int(self.last_frame))\n\n        self.log.info(\"Rendered...\")\n\n    def save_file(self):\n        import shutil\n        with maintained_selection():\n            self.log.info(\"Saving nodes as file...  \")\n            # create nk path\n            path = os.path.splitext(self.path)[0] + \".nk\"\n            # save file to the path\n            if not os.path.exists(os.path.dirname(path)):\n                os.makedirs(os.path.dirname(path))\n            shutil.copyfile(self.instance.context.data[\"currentFile\"], path)\n\n        self.log.info(\"Nodes exported...\")\n        return path\n\n    def generate_mov(self, farm=False, **kwargs):\n        # colorspace data\n        colorspace = None\n        # get colorspace settings\n        # get colorspace data from context\n        config_data, _ = get_colorspace_settings_from_publish_context(\n            self.instance.context.data)\n\n        add_tags = []\n        self.publish_on_farm = farm\n        read_raw = kwargs[\"read_raw\"]\n        bake_viewer_process = kwargs[\"bake_viewer_process\"]\n        bake_viewer_input_process_node = kwargs[\n            \"bake_viewer_input_process\"]\n        viewer_process_override = kwargs[\n            \"viewer_process_override\"]\n\n        baking_view_profile = (\n            viewer_process_override or self.get_imageio_baking_profile())\n\n        fps = self.instance.context.data[\"fps\"]\n\n        self.log.debug(\">> baking_view_profile   `{}`\".format(\n            baking_view_profile))\n\n        add_custom_tags = kwargs.get(\"add_custom_tags\", [])\n\n        self.log.info(\n            \"__ add_custom_tags: `{0}`\".format(add_custom_tags))\n\n        subset = self.instance.data[\"subset\"]\n        self._temp_nodes[subset] = []\n\n        # Read node\n        r_node = nuke.createNode(\"Read\")\n        r_node[\"file\"].setValue(self.path_in)\n        r_node[\"first\"].setValue(self.first_frame)\n        r_node[\"origfirst\"].setValue(self.first_frame)\n        r_node[\"last\"].setValue(self.last_frame)\n        r_node[\"origlast\"].setValue(self.last_frame)\n        r_node[\"colorspace\"].setValue(self.write_colorspace)\n\n        # do not rely on defaults, set explicitly\n        # to be sure it is set correctly\n        r_node[\"frame_mode\"].setValue(\"expression\")\n        r_node[\"frame\"].setValue(\"\")\n\n        if read_raw:\n            r_node[\"raw\"].setValue(1)\n\n        # connect to Read node\n        self._shift_to_previous_node_and_temp(subset, r_node, \"Read...   `{}`\")\n\n        # add reformat node\n        reformat_nodes_config = kwargs[\"reformat_nodes_config\"]\n        if reformat_nodes_config[\"enabled\"]:\n            reposition_nodes = reformat_nodes_config[\"reposition_nodes\"]\n            for reposition_node in reposition_nodes:\n                node_class = reposition_node[\"node_class\"]\n                knobs = reposition_node[\"knobs\"]\n                node = nuke.createNode(node_class)\n                set_node_knobs_from_settings(node, knobs)\n\n                # connect in order\n                self._connect_to_above_nodes(\n                    node, subset, \"Reposition node...   `{}`\"\n                )\n            # append reformated tag\n            add_tags.append(\"reformated\")\n\n        # only create colorspace baking if toggled on\n        if bake_viewer_process:\n            if bake_viewer_input_process_node:\n                # View Process node\n                ipn = get_view_process_node()\n                if ipn is not None:\n                    # connect to ViewProcess node\n                    self._connect_to_above_nodes(ipn, subset, \"ViewProcess...   `{}`\")\n\n            if not self.viewer_lut_raw:\n                # OCIODisplay\n                dag_node = nuke.createNode(\"OCIODisplay\")\n\n                # assign display\n                display, viewer = get_viewer_config_from_string(\n                    str(baking_view_profile)\n                )\n                if display:\n                    dag_node[\"display\"].setValue(display)\n\n                # assign viewer\n                dag_node[\"view\"].setValue(viewer)\n\n                if config_data:\n                    # convert display and view to colorspace\n                    colorspace = get_display_view_colorspace_name(\n                        config_path=config_data[\"path\"],\n                        display=display,\n                        view=viewer\n                    )\n\n                self._connect_to_above_nodes(dag_node, subset, \"OCIODisplay...   `{}`\")\n        # Write node\n        write_node = nuke.createNode(\"Write\")\n        self.log.debug(\"Path: {}\".format(self.path))\n        write_node[\"file\"].setValue(str(self.path))\n        write_node[\"file_type\"].setValue(str(self.ext))\n        # Knobs `meta_codec` and `mov64_codec` are not available on centos.\n        # TODO shouldn't this come from settings on outputs?\n        try:\n            write_node[\"meta_codec\"].setValue(\"ap4h\")\n        except Exception:\n            self.log.info(\"`meta_codec` knob was not found\")\n\n        try:\n            write_node[\"mov64_codec\"].setValue(\"ap4h\")\n            write_node[\"mov64_fps\"].setValue(float(fps))\n        except Exception:\n            self.log.info(\"`mov64_codec` knob was not found\")\n\n        try:\n            write_node[\"mov64_write_timecode\"].setValue(1)\n        except Exception:\n            self.log.info(\"`mov64_write_timecode` knob was not found\")\n\n        write_node[\"raw\"].setValue(1)\n        # connect\n        write_node.setInput(0, self.previous_node)\n        self._temp_nodes[subset].append(write_node)\n        self.log.debug(\"Write...   `{}`\".format(self._temp_nodes[subset]))\n        # ---------- end nodes creation\n\n        # ---------- render or save to nk\n        if self.publish_on_farm:\n            nuke.scriptSave()\n            path_nk = self.save_file()\n            self.data.update({\n                \"bakeScriptPath\": path_nk,\n                \"bakeWriteNodeName\": write_node.name(),\n                \"bakeRenderPath\": self.path\n            })\n        else:\n            self.render(write_node.name())\n\n        # ---------- generate representation data\n        self.get_representation_data(\n            tags=[\"review\", \"need_thumbnail\", \"delete\"] + add_tags,\n            custom_tags=add_custom_tags,\n            range=True,\n            colorspace=colorspace\n        )\n\n        self.log.debug(\"Representation...   `{}`\".format(self.data))\n\n        self.clean_nodes(subset)\n        nuke.scriptSave()\n\n        return self.data\n\n    def _shift_to_previous_node_and_temp(self, subset, node, message):\n        self._temp_nodes[subset].append(node)\n        self.previous_node = node\n        self.log.debug(message.format(self._temp_nodes[subset]))\n\n    def _connect_to_above_nodes(self, node, subset, message):\n        node.setInput(0, self.previous_node)\n        self._shift_to_previous_node_and_temp(subset, node, message)\n\n\n@deprecated(\"openpype.hosts.nuke.api.plugin.NukeWriteCreator\")\nclass AbstractWriteRender(OpenPypeCreator):\n    \"\"\"Abstract creator to gather similar implementation for Write creators\"\"\"\n    name = \"\"\n    label = \"\"\n    hosts = [\"nuke\"]\n    n_class = \"Write\"\n    family = \"render\"\n    icon = \"sign-out\"\n    defaults = [\"Main\", \"Mask\"]\n    knobs = []\n    prenodes = {}\n\n    def __init__(self, *args, **kwargs):\n        super(AbstractWriteRender, self).__init__(*args, **kwargs)\n\n        data = OrderedDict()\n\n        data[\"family\"] = self.family\n        data[\"families\"] = self.n_class\n\n        for k, v in self.data.items():\n            if k not in data.keys():\n                data.update({k: v})\n\n        self.data = data\n        self.nodes = nuke.selectedNodes()\n\n    def process(self):\n\n        inputs = []\n        outputs = []\n        instance = nuke.toNode(self.data[\"subset\"])\n        selected_node = None\n\n        # use selection\n        if (self.options or {}).get(\"useSelection\"):\n            nodes = self.nodes\n\n            if not (len(nodes) < 2):\n                msg = (\"Select only one node. \"\n                       \"The node you want to connect to, \"\n                       \"or tick off `Use selection`\")\n                self.log.error(msg)\n                nuke.message(msg)\n                return\n\n            if len(nodes) == 0:\n                msg = (\n                    \"No nodes selected. Please select a single node to connect\"\n                    \" to or tick off `Use selection`\"\n                )\n                self.log.error(msg)\n                nuke.message(msg)\n                return\n\n            selected_node = nodes[0]\n            inputs = [selected_node]\n            outputs = selected_node.dependent()\n\n            if instance:\n                if (instance.name() in selected_node.name()):\n                    selected_node = instance.dependencies()[0]\n\n        # if node already exist\n        if instance:\n            # collect input / outputs\n            inputs = instance.dependencies()\n            outputs = instance.dependent()\n            selected_node = inputs[0]\n            # remove old one\n            nuke.delete(instance)\n\n        # recreate new\n        write_data = {\n            \"nodeclass\": self.n_class,\n            \"families\": [self.family],\n            \"avalon\": self.data,\n            \"subset\": self.data[\"subset\"],\n            \"knobs\": self.knobs\n        }\n\n        # add creator data\n        creator_data = {\"creator\": self.__class__.__name__}\n        self.data.update(creator_data)\n        write_data.update(creator_data)\n\n        write_node = self._create_write_node(\n            selected_node,\n            inputs,\n            outputs,\n            write_data\n        )\n\n        # relinking to collected connections\n        for i, input in enumerate(inputs):\n            write_node.setInput(i, input)\n\n        write_node.autoplace()\n\n        for output in outputs:\n            output.setInput(0, write_node)\n\n        write_node = self._modify_write_node(write_node)\n\n        return write_node\n\n    def is_legacy(self):\n        \"\"\"Check if it needs to run legacy code\n\n        In case where `type` key is missing in single\n        knob it is legacy project anatomy.\n\n        Returns:\n            bool: True if legacy\n        \"\"\"\n        imageio_nodes = get_nuke_imageio_settings()[\"nodes\"]\n        node = imageio_nodes[\"requiredNodes\"][0]\n        if \"type\" not in node[\"knobs\"][0]:\n            # if type is not yet in project anatomy\n            return True\n        elif next(iter(\n            _k for _k in node[\"knobs\"]\n            if _k.get(\"type\") == \"__legacy__\"\n        ), None):\n            # in case someone re-saved anatomy\n            # with old configuration\n            return True\n\n    @abstractmethod\n    def _create_write_node(self, selected_node, inputs, outputs, write_data):\n        \"\"\"Family dependent implementation of Write node creation\n\n        Args:\n            selected_node (nuke.Node)\n            inputs (list of nuke.Node) - input dependencies (what is connected)\n            outputs (list of nuke.Node) - output dependencies\n            write_data (dict) - values used to fill Knobs\n        Returns:\n            node (nuke.Node): group node with  data as Knobs\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def _modify_write_node(self, write_node):\n        \"\"\"Family dependent modification of created 'write_node'\n\n        Returns:\n            node (nuke.Node): group node with data as Knobs\n        \"\"\"\n        pass\n\n\ndef convert_to_valid_instaces():\n    \"\"\" Check and convert to latest publisher instances\n\n    Also save as new minor version of workfile.\n    \"\"\"\n    def family_to_identifier(family):\n        mapping = {\n            \"render\": \"create_write_render\",\n            \"prerender\": \"create_write_prerender\",\n            \"still\": \"create_write_image\",\n            \"model\": \"create_model\",\n            \"camera\": \"create_camera\",\n            \"nukenodes\": \"create_backdrop\",\n            \"gizmo\": \"create_gizmo\",\n            \"source\": \"create_source\"\n\n        }\n        return mapping[family]\n\n    from openpype.hosts.nuke.api import workio\n\n    task_name = get_current_task_name()\n\n    # save into new workfile\n    current_file = workio.current_file()\n\n    # add file suffex if not\n    if \"_publisherConvert\" not in current_file:\n        new_workfile = (\n            current_file[:-3]\n            + \"_publisherConvert\"\n            + current_file[-3:]\n        )\n    else:\n        new_workfile = current_file\n\n    path = new_workfile.replace(\"\\\\\", \"/\")\n    nuke.scriptSaveAs(new_workfile, overwrite=1)\n    nuke.Root()[\"name\"].setValue(path)\n    nuke.Root()[\"project_directory\"].setValue(os.path.dirname(path))\n    nuke.Root().setModified(False)\n\n    _remove_old_knobs(nuke.Root())\n\n    # loop all nodes and convert\n    for node in nuke.allNodes(recurseGroups=True):\n        transfer_data = {\n            \"creator_attributes\": {}\n        }\n        creator_attr = transfer_data[\"creator_attributes\"]\n\n        if node.Class() in [\"Viewer\", \"Dot\"]:\n            continue\n\n        if get_node_data(node, INSTANCE_DATA_KNOB):\n            continue\n\n        # get data from avalon knob\n        avalon_knob_data = get_avalon_knob_data(\n            node, [\"avalon:\", \"ak:\"])\n\n        if not avalon_knob_data:\n            continue\n\n        if avalon_knob_data[\"id\"] != \"pyblish.avalon.instance\":\n            continue\n\n        transfer_data.update({\n            k: v for k, v in avalon_knob_data.items()\n            if k not in [\"families\", \"creator\"]\n        })\n\n        transfer_data[\"task\"] = task_name\n\n        family = avalon_knob_data[\"family\"]\n        # establish families\n        families_ak = avalon_knob_data.get(\"families\", [])\n\n        if \"suspend_publish\" in node.knobs():\n            creator_attr[\"suspended_publish\"] = (\n                node[\"suspend_publish\"].value())\n\n        # get review knob value\n        if \"review\" in node.knobs():\n            creator_attr[\"review\"] = (\n                node[\"review\"].value())\n\n        if \"publish\" in node.knobs():\n            transfer_data[\"active\"] = (\n                node[\"publish\"].value())\n\n        # add idetifier\n        transfer_data[\"creator_identifier\"] = family_to_identifier(family)\n\n        # Add all nodes in group instances.\n        if node.Class() == \"Group\":\n            # only alter families for render family\n            if families_ak and \"write\" in families_ak.lower():\n                target = node[\"render\"].value()\n                if target == \"Use existing frames\":\n                    creator_attr[\"render_target\"] = \"frames\"\n                elif target == \"Local\":\n                    # Local rendering\n                    creator_attr[\"render_target\"] = \"local\"\n                elif target == \"On farm\":\n                    # Farm rendering\n                    creator_attr[\"render_target\"] = \"farm\"\n\n                if \"deadlinePriority\" in node.knobs():\n                    transfer_data[\"farm_priority\"] = (\n                        node[\"deadlinePriority\"].value())\n                if \"deadlineChunkSize\" in node.knobs():\n                    creator_attr[\"farm_chunk\"] = (\n                        node[\"deadlineChunkSize\"].value())\n                if \"deadlineConcurrentTasks\" in node.knobs():\n                    creator_attr[\"farm_concurrency\"] = (\n                        node[\"deadlineConcurrentTasks\"].value())\n\n        _remove_old_knobs(node)\n\n        # add new instance knob with transfer data\n        set_node_data(\n            node, INSTANCE_DATA_KNOB, transfer_data)\n\n    nuke.scriptSave()\n\n\ndef _remove_old_knobs(node):\n    remove_knobs = [\n        \"review\", \"publish\", \"render\", \"suspend_publish\", \"warn\", \"divd\",\n        \"OpenpypeDataGroup\", \"OpenpypeDataGroup_End\", \"deadlinePriority\",\n        \"deadlineChunkSize\", \"deadlineConcurrentTasks\", \"Deadline\"\n    ]\n    print(node.name())\n\n    # remove all old knobs\n    for knob in node.allKnobs():\n        try:\n            if knob.name() in remove_knobs:\n                node.removeKnob(knob)\n            elif \"avalon\" in knob.name():\n                node.removeKnob(knob)\n        except ValueError:\n            pass\n\n\ndef exposed_write_knobs(settings, plugin_name, instance_node):\n    exposed_knobs = settings[\"nuke\"][\"create\"][plugin_name].get(\n        \"exposed_knobs\", []\n    )\n    if exposed_knobs:\n        instance_node.addKnob(nuke.Text_Knob('', 'Write Knobs'))\n    write_node = nuke.allNodes(group=instance_node, filter=\"Write\")[0]\n    link_knobs(exposed_knobs, write_node, instance_node)\n"
  },
  {
    "path": "openpype/hosts/nuke/api/push_to_project.py",
    "content": "from collections import defaultdict\nimport shutil\nimport os\n\nfrom openpype.client import get_project, get_asset_by_id\nfrom openpype.settings import get_system_settings, get_project_settings\nfrom openpype.pipeline import Anatomy, registered_host\nfrom openpype.pipeline.template_data import get_template_data\nfrom openpype.pipeline.workfile import get_workdir_with_workdir_data\nfrom openpype.tools.push_to_project.app import show\n\nfrom .utils import bake_gizmos_recursively\n\nimport nuke\n\n\ndef bake_container(container):\n    \"\"\"Bake containers to read nodes.\"\"\"\n\n    node = container[\"node\"]\n\n    # Fetch knobs to remove in order.\n    knobs_to_remove = []\n    remove = False\n    for count in range(0, node.numKnobs()):\n        knob = node.knob(count)\n\n        # All knobs from \"OpenPype\" tab knob onwards.\n        if knob.name() == \"OpenPype\":\n            remove = True\n\n        if remove:\n            knobs_to_remove.append(knob)\n\n        # Dont remove knobs from \"containerId\" onwards.\n        if knob.name() == \"containerId\":\n            remove = False\n\n    # Knobs needs to be remove in reverse order, because child knobs needs to\n    # be remove first.\n    for knob in reversed(knobs_to_remove):\n        node.removeKnob(knob)\n\n    node[\"tile_color\"].setValue(0)\n\n\ndef main():\n    context = show(\"\", \"\", False, True)\n\n    if context is None:\n        return\n\n    # Get workfile path to save to.\n    project_name = context[\"project_name\"]\n    project_doc = get_project(project_name)\n    asset_doc = get_asset_by_id(project_name, context[\"asset_id\"])\n    task_name = context[\"task_name\"]\n    host = registered_host()\n    system_settings = get_system_settings()\n    project_settings = get_project_settings(project_name)\n    anatomy = Anatomy(project_name)\n\n    workdir_data = get_template_data(\n        project_doc, asset_doc, task_name, host.name, system_settings\n    )\n\n    workdir = get_workdir_with_workdir_data(\n        workdir_data,\n        project_name,\n        anatomy,\n        project_settings=project_settings\n    )\n\n    # Save current workfile.\n    current_file = host.current_file()\n    host.save_file(current_file)\n\n    for container in host.ls():\n        bake_container(container)\n\n    # Bake gizmos.\n    bake_gizmos_recursively()\n\n    # Copy all read node files to \"resources\" folder next to workfile and\n    # change file path.\n    first_frame = int(nuke.root()[\"first_frame\"].value())\n    last_frame = int(nuke.root()[\"last_frame\"].value())\n    files_by_node_name = defaultdict(set)\n    nodes_by_name = {}\n    for count in range(first_frame, last_frame + 1):\n        nuke.frame(count)\n        for node in nuke.allNodes(filter=\"Read\"):\n            files_by_node_name[node.name()].add(\n                nuke.filename(node, nuke.REPLACE)\n            )\n            nodes_by_name[node.name()] = node\n\n    resources_dir = os.path.join(workdir, \"resources\")\n    for name, files in files_by_node_name.items():\n        dir = os.path.join(resources_dir, name)\n        if not os.path.exists(dir):\n            os.makedirs(dir)\n\n        for f in files:\n            shutil.copy(f, os.path.join(dir, os.path.basename(f)))\n\n        node = nodes_by_name[name]\n        path = node[\"file\"].value().replace(os.path.dirname(f), dir)\n        node[\"file\"].setValue(path.replace(\"\\\\\", \"/\"))\n\n    # Save current workfile to new context.\n    basename = os.path.basename(current_file)\n    host.save_file(os.path.join(workdir, basename))\n\n    # Open current contex workfile.\n    host.open_file(current_file)\n"
  },
  {
    "path": "openpype/hosts/nuke/api/utils.py",
    "content": "import os\nimport re\n\nimport nuke\n\nfrom openpype import resources\nfrom qtpy import QtWidgets\n\n\ndef set_context_favorites(favorites=None):\n    \"\"\" Adding favorite folders to nuke's browser\n\n    Arguments:\n        favorites (dict): couples of {name:path}\n    \"\"\"\n    favorites = favorites or {}\n    icon_path = resources.get_resource(\"icons\", \"folder-favorite.png\")\n    for name, path in favorites.items():\n        nuke.addFavoriteDir(\n            name,\n            path,\n            nuke.IMAGE | nuke.SCRIPT | nuke.GEO,\n            icon=icon_path)\n\n\ndef get_node_outputs(node):\n    '''\n    Return a dictionary of the nodes and pipes that are connected to node\n    '''\n    dep_dict = {}\n    dependencies = node.dependent(nuke.INPUTS | nuke.HIDDEN_INPUTS)\n    for d in dependencies:\n        dep_dict[d] = []\n        for i in range(d.inputs()):\n            if d.input(i) == node:\n                dep_dict[d].append(i)\n    return dep_dict\n\n\ndef is_node_gizmo(node):\n    '''\n    return True if node is gizmo\n    '''\n    return 'gizmo_file' in node.knobs()\n\n\ndef gizmo_is_nuke_default(gizmo):\n    '''Check if gizmo is in default install path'''\n    plug_dir = os.path.join(os.path.dirname(\n        nuke.env['ExecutablePath']), 'plugins')\n    return gizmo.filename().startswith(plug_dir)\n\n\ndef bake_gizmos_recursively(in_group=None):\n    \"\"\"Converting a gizmo to group\n\n    Arguments:\n        is_group (nuke.Node)[optonal]: group node or all nodes\n    \"\"\"\n    from .lib import maintained_selection\n    if in_group is None:\n        in_group = nuke.Root()\n    # preserve selection after all is done\n    with maintained_selection():\n        # jump to the group\n        with in_group:\n            for node in nuke.allNodes():\n                if is_node_gizmo(node) and not gizmo_is_nuke_default(node):\n                    with node:\n                        outputs = get_node_outputs(node)\n                        group = node.makeGroup()\n                        # Reconnect inputs and outputs if any\n                        if outputs:\n                            for n, pipes in outputs.items():\n                                for i in pipes:\n                                    n.setInput(i, group)\n                        for i in range(node.inputs()):\n                            group.setInput(i, node.input(i))\n                        # set node position and name\n                        group.setXYpos(node.xpos(), node.ypos())\n                        name = node.name()\n                        nuke.delete(node)\n                        group.setName(name)\n                        node = group\n\n                if node.Class() == \"Group\":\n                    bake_gizmos_recursively(node)\n\n\ndef colorspace_exists_on_node(node, colorspace_name):\n    \"\"\" Check if colorspace exists on node\n\n    Look through all options in the colorspace knob, and see if we have an\n    exact match to one of the items.\n\n    Args:\n        node (nuke.Node): nuke node object\n        colorspace_name (str): color profile name\n\n    Returns:\n        bool: True if exists\n    \"\"\"\n    try:\n        colorspace_knob = node['colorspace']\n    except ValueError:\n        # knob is not available on input node\n        return False\n\n    return colorspace_name in get_colorspace_list(colorspace_knob)\n\n\ndef get_colorspace_list(colorspace_knob):\n    \"\"\"Get available colorspace profile names\n\n    Args:\n        colorspace_knob (nuke.Knob): nuke knob object\n\n    Returns:\n        list: list of strings names of profiles\n    \"\"\"\n    results = []\n\n    # This pattern is to match with roles which uses an indentation and\n    # parentheses with original colorspace. The value returned from the\n    # colorspace is the string before the indentation, so we'll need to\n    # convert the values to match with value returned from the knob,\n    # ei. knob.value().\n    pattern = r\".*\\t.* \\(.*\\)\"\n    for colorspace in nuke.getColorspaceList(colorspace_knob):\n        match = re.search(pattern, colorspace)\n        if match:\n            results.append(colorspace.split(\"\\t\", 1)[0])\n        else:\n            results.append(colorspace)\n\n    return results\n\n\ndef is_headless():\n    \"\"\"\n    Returns:\n        bool: headless\n    \"\"\"\n    return QtWidgets.QApplication.instance() is None\n"
  },
  {
    "path": "openpype/hosts/nuke/api/workfile_template_builder.py",
    "content": "import collections\nimport nuke\nfrom openpype.pipeline import registered_host\nfrom openpype.pipeline.workfile.workfile_template_builder import (\n    AbstractTemplateBuilder,\n    PlaceholderPlugin,\n    LoadPlaceholderItem,\n    CreatePlaceholderItem,\n    PlaceholderLoadMixin,\n    PlaceholderCreateMixin\n)\nfrom openpype.tools.workfile_template_build import (\n    WorkfileBuildPlaceholderDialog,\n)\nfrom .lib import (\n    find_free_space_to_paste_nodes,\n    get_extreme_positions,\n    get_group_io_nodes,\n    imprint,\n    refresh_node,\n    refresh_nodes,\n    reset_selection,\n    get_names_from_nodes,\n    get_nodes_by_names,\n    select_nodes,\n    duplicate_node,\n    node_tempfile,\n    get_main_window,\n    WorkfileSettings,\n)\n\nPLACEHOLDER_SET = \"PLACEHOLDERS_SET\"\n\n\nclass NukeTemplateBuilder(AbstractTemplateBuilder):\n    \"\"\"Concrete implementation of AbstractTemplateBuilder for nuke\"\"\"\n\n    def import_template(self, path):\n        \"\"\"Import template into current scene.\n        Block if a template is already loaded.\n\n        Args:\n            path (str): A path to current template (usually given by\n            get_template_preset implementation)\n\n        Returns:\n            bool: Whether the template was successfully imported or not\n        \"\"\"\n\n        # TODO check if the template is already imported\n\n        nuke.nodePaste(path)\n        reset_selection()\n\n        return True\n\nclass NukePlaceholderPlugin(PlaceholderPlugin):\n    node_color = 4278190335\n\n    def _collect_scene_placeholders(self):\n        # Cache placeholder data to shared data\n        placeholder_nodes = self.builder.get_shared_populate_data(\n            \"placeholder_nodes\"\n        )\n        if placeholder_nodes is None:\n            placeholder_nodes = {}\n            all_groups = collections.deque()\n            all_groups.append(nuke.thisGroup())\n            while all_groups:\n                group = all_groups.popleft()\n                for node in group.nodes():\n                    if isinstance(node, nuke.Group):\n                        all_groups.append(node)\n\n                    node_knobs = node.knobs()\n                    if (\n                        \"is_placeholder\" not in node_knobs\n                        or not node.knob(\"is_placeholder\").value()\n                    ):\n                        continue\n\n                    if \"empty\" in node_knobs and node.knob(\"empty\").value():\n                        continue\n\n                    placeholder_nodes[node.fullName()] = node\n\n            self.builder.set_shared_populate_data(\n                \"placeholder_nodes\", placeholder_nodes\n            )\n        return placeholder_nodes\n\n    def create_placeholder(self, placeholder_data):\n        placeholder_data[\"plugin_identifier\"] = self.identifier\n\n        placeholder = nuke.nodes.NoOp()\n        placeholder.setName(\"PLACEHOLDER\")\n        placeholder.knob(\"tile_color\").setValue(self.node_color)\n\n        imprint(placeholder, placeholder_data)\n        imprint(placeholder, {\"is_placeholder\": True})\n        placeholder.knob(\"is_placeholder\").setVisible(False)\n\n    def update_placeholder(self, placeholder_item, placeholder_data):\n        node = nuke.toNode(placeholder_item.scene_identifier)\n        imprint(node, placeholder_data)\n\n    def _parse_placeholder_node_data(self, node):\n        placeholder_data = {}\n        for key in self.get_placeholder_keys():\n            knob = node.knob(key)\n            value = None\n            if knob is not None:\n                value = knob.getValue()\n            placeholder_data[key] = value\n        return placeholder_data\n\n    def delete_placeholder(self, placeholder):\n        \"\"\"Remove placeholder if building was successful\"\"\"\n        placeholder_node = nuke.toNode(placeholder.scene_identifier)\n        nuke.delete(placeholder_node)\n\n\nclass NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin):\n    identifier = \"nuke.load\"\n    label = \"Nuke load\"\n\n    def _parse_placeholder_node_data(self, node):\n        placeholder_data = super(\n            NukePlaceholderLoadPlugin, self\n        )._parse_placeholder_node_data(node)\n\n        node_knobs = node.knobs()\n        nb_children = 0\n        if \"nb_children\" in node_knobs:\n            nb_children = int(node_knobs[\"nb_children\"].getValue())\n        placeholder_data[\"nb_children\"] = nb_children\n\n        siblings = []\n        if \"siblings\" in node_knobs:\n            siblings = node_knobs[\"siblings\"].values()\n        placeholder_data[\"siblings\"] = siblings\n\n        node_full_name = node.fullName()\n        placeholder_data[\"group_name\"] = node_full_name.rpartition(\".\")[0]\n        placeholder_data[\"last_loaded\"] = []\n        placeholder_data[\"delete\"] = False\n        return placeholder_data\n\n    def _get_loaded_repre_ids(self):\n        loaded_representation_ids = self.builder.get_shared_populate_data(\n            \"loaded_representation_ids\"\n        )\n        if loaded_representation_ids is None:\n            loaded_representation_ids = set()\n            for node in nuke.allNodes():\n                if \"repre_id\" in node.knobs():\n                    loaded_representation_ids.add(\n                        node.knob(\"repre_id\").getValue()\n                    )\n\n            self.builder.set_shared_populate_data(\n                \"loaded_representation_ids\", loaded_representation_ids\n            )\n        return loaded_representation_ids\n\n    def _before_placeholder_load(self, placeholder):\n        placeholder.data[\"nodes_init\"] = nuke.allNodes()\n\n    def _before_repre_load(self, placeholder, representation):\n        placeholder.data[\"last_repre_id\"] = str(representation[\"_id\"])\n\n    def collect_placeholders(self):\n        output = []\n        scene_placeholders = self._collect_scene_placeholders()\n        for node_name, node in scene_placeholders.items():\n            plugin_identifier_knob = node.knob(\"plugin_identifier\")\n            if (\n                plugin_identifier_knob is None\n                or plugin_identifier_knob.getValue() != self.identifier\n            ):\n                continue\n\n            placeholder_data = self._parse_placeholder_node_data(node)\n            # TODO do data validations and maybe updgrades if are invalid\n            output.append(\n                LoadPlaceholderItem(node_name, placeholder_data, self)\n            )\n\n        return output\n\n    def populate_placeholder(self, placeholder):\n        self.populate_load_placeholder(placeholder)\n\n    def repopulate_placeholder(self, placeholder):\n        repre_ids = self._get_loaded_repre_ids()\n        self.populate_load_placeholder(placeholder, repre_ids)\n\n    def get_placeholder_options(self, options=None):\n        return self.get_load_plugin_options(options)\n\n    def post_placeholder_process(self, placeholder, failed):\n        \"\"\"Cleanup placeholder after load of its corresponding representations.\n\n        Args:\n            placeholder (PlaceholderItem): Item which was just used to load\n                representation.\n            failed (bool): Loading of representation failed.\n        \"\"\"\n        # deselect all selected nodes\n        placeholder_node = nuke.toNode(placeholder.scene_identifier)\n\n        # getting the latest nodes added\n        # TODO get from shared populate data!\n        nodes_init = placeholder.data[\"nodes_init\"]\n        nodes_loaded = list(set(nuke.allNodes()) - set(nodes_init))\n        self.log.debug(\"Loaded nodes: {}\".format(nodes_loaded))\n        if not nodes_loaded:\n            return\n\n        placeholder.data[\"delete\"] = True\n\n        nodes_loaded = self._move_to_placeholder_group(\n            placeholder, nodes_loaded\n        )\n        placeholder.data[\"last_loaded\"] = nodes_loaded\n        refresh_nodes(nodes_loaded)\n\n        # positioning of the loaded nodes\n        min_x, min_y, _, _ = get_extreme_positions(nodes_loaded)\n        for node in nodes_loaded:\n            xpos = (node.xpos() - min_x) + placeholder_node.xpos()\n            ypos = (node.ypos() - min_y) + placeholder_node.ypos()\n            node.setXYpos(xpos, ypos)\n        refresh_nodes(nodes_loaded)\n\n        # fix the problem of z_order for backdrops\n        self._fix_z_order(placeholder)\n\n        if placeholder.data.get(\"keep_placeholder\"):\n            self._imprint_siblings(placeholder)\n\n        if placeholder.data[\"nb_children\"] == 0:\n            # save initial nodes positions and dimensions, update them\n            # and set inputs and outputs of loaded nodes\n            if placeholder.data.get(\"keep_placeholder\"):\n                self._imprint_inits()\n                self._update_nodes(placeholder, nuke.allNodes(), nodes_loaded)\n\n            self._set_loaded_connections(placeholder)\n\n        elif placeholder.data[\"siblings\"]:\n            # create copies of placeholder siblings for the new loaded nodes,\n            # set their inputs and outputs and update all nodes positions and\n            # dimensions and siblings names\n\n            siblings = get_nodes_by_names(placeholder.data[\"siblings\"])\n            refresh_nodes(siblings)\n            copies = self._create_sib_copies(placeholder)\n            new_nodes = list(copies.values())  # copies nodes\n            self._update_nodes(new_nodes, nodes_loaded)\n            placeholder_node.removeKnob(placeholder_node.knob(\"siblings\"))\n            new_nodes_name = get_names_from_nodes(new_nodes)\n            imprint(placeholder_node, {\"siblings\": new_nodes_name})\n            self._set_copies_connections(placeholder, copies)\n\n            self._update_nodes(\n                nuke.allNodes(),\n                new_nodes + nodes_loaded,\n                20\n            )\n\n            new_siblings = get_names_from_nodes(new_nodes)\n            placeholder.data[\"siblings\"] = new_siblings\n\n        else:\n            # if the placeholder doesn't have siblings, the loaded\n            # nodes will be placed in a free space\n\n            xpointer, ypointer = find_free_space_to_paste_nodes(\n                nodes_loaded, direction=\"bottom\", offset=200\n            )\n            node = nuke.createNode(\"NoOp\")\n            reset_selection()\n            nuke.delete(node)\n            for node in nodes_loaded:\n                xpos = (node.xpos() - min_x) + xpointer\n                ypos = (node.ypos() - min_y) + ypointer\n                node.setXYpos(xpos, ypos)\n\n        placeholder.data[\"nb_children\"] += 1\n        reset_selection()\n\n        # go back to root group\n        nuke.root().begin()\n\n    def _move_to_placeholder_group(self, placeholder, nodes_loaded):\n        \"\"\"\n        opening the placeholder's group and copying loaded nodes in it.\n\n        Returns :\n            nodes_loaded (list): the new list of pasted nodes\n        \"\"\"\n\n        groups_name = placeholder.data[\"group_name\"]\n        reset_selection()\n        select_nodes(nodes_loaded)\n        if groups_name:\n            with node_tempfile() as filepath:\n                nuke.nodeCopy(filepath)\n                for node in nuke.selectedNodes():\n                    nuke.delete(node)\n                group = nuke.toNode(groups_name)\n                group.begin()\n                nuke.nodePaste(filepath)\n                nodes_loaded = nuke.selectedNodes()\n        return nodes_loaded\n\n    def _fix_z_order(self, placeholder):\n        \"\"\"Fix the problem of z_order when a backdrop is loaded.\"\"\"\n\n        nodes_loaded = placeholder.data[\"last_loaded\"]\n        loaded_backdrops = []\n        bd_orders = set()\n        for node in nodes_loaded:\n            if isinstance(node, nuke.BackdropNode):\n                loaded_backdrops.append(node)\n                bd_orders.add(node.knob(\"z_order\").getValue())\n\n        if not bd_orders:\n            return\n\n        sib_orders = set()\n        for node_name in placeholder.data[\"siblings\"]:\n            node = nuke.toNode(node_name)\n            if isinstance(node, nuke.BackdropNode):\n                sib_orders.add(node.knob(\"z_order\").getValue())\n\n        if not sib_orders:\n            return\n\n        min_order = min(bd_orders)\n        max_order = max(sib_orders)\n        for backdrop_node in loaded_backdrops:\n            z_order = backdrop_node.knob(\"z_order\").getValue()\n            backdrop_node.knob(\"z_order\").setValue(\n                z_order + max_order - min_order + 1)\n\n    def _imprint_siblings(self, placeholder):\n        \"\"\"\n        - add siblings names to placeholder attributes (nodes loaded with it)\n        - add Id to the attributes of all the other nodes\n        \"\"\"\n\n        loaded_nodes = placeholder.data[\"last_loaded\"]\n        loaded_nodes_set = set(loaded_nodes)\n        data = {\"repre_id\": str(placeholder.data[\"last_repre_id\"])}\n\n        for node in loaded_nodes:\n            node_knobs = node.knobs()\n            if \"builder_type\" not in node_knobs:\n                # save the id of representation for all imported nodes\n                imprint(node, data)\n                node.knob(\"repre_id\").setVisible(False)\n                refresh_node(node)\n                continue\n\n            if (\n                \"is_placeholder\" not in node_knobs\n                or (\n                    \"is_placeholder\" in node_knobs\n                    and node.knob(\"is_placeholder\").value()\n                )\n            ):\n                siblings = list(loaded_nodes_set - {node})\n                siblings_name = get_names_from_nodes(siblings)\n                siblings = {\"siblings\": siblings_name}\n                imprint(node, siblings)\n\n    def _imprint_inits(self):\n        \"\"\"Add initial positions and dimensions to the attributes\"\"\"\n\n        for node in nuke.allNodes():\n            refresh_node(node)\n            imprint(node, {\"x_init\": node.xpos(), \"y_init\": node.ypos()})\n            node.knob(\"x_init\").setVisible(False)\n            node.knob(\"y_init\").setVisible(False)\n            width = node.screenWidth()\n            height = node.screenHeight()\n            if \"bdwidth\" in node.knobs():\n                imprint(node, {\"w_init\": width, \"h_init\": height})\n                node.knob(\"w_init\").setVisible(False)\n                node.knob(\"h_init\").setVisible(False)\n            refresh_node(node)\n\n    def _update_nodes(\n        self, placeholder, nodes, considered_nodes, offset_y=None\n    ):\n        \"\"\"Adjust backdrop nodes dimensions and positions.\n\n        Considering some nodes sizes.\n\n        Args:\n            nodes (list): list of nodes to update\n            considered_nodes (list): list of nodes to consider while updating\n                positions and dimensions\n            offset (int): distance between copies\n        \"\"\"\n\n        placeholder_node = nuke.toNode(placeholder.scene_identifier)\n\n        min_x, min_y, max_x, max_y = get_extreme_positions(considered_nodes)\n\n        diff_x = diff_y = 0\n        contained_nodes = []  # for backdrops\n\n        if offset_y is None:\n            width_ph = placeholder_node.screenWidth()\n            height_ph = placeholder_node.screenHeight()\n            diff_y = max_y - min_y - height_ph\n            diff_x = max_x - min_x - width_ph\n            contained_nodes = [placeholder_node]\n            min_x = placeholder_node.xpos()\n            min_y = placeholder_node.ypos()\n        else:\n            siblings = get_nodes_by_names(placeholder.data[\"siblings\"])\n            minX, _, maxX, _ = get_extreme_positions(siblings)\n            diff_y = max_y - min_y + 20\n            diff_x = abs(max_x - min_x - maxX + minX)\n            contained_nodes = considered_nodes\n\n        if diff_y <= 0 and diff_x <= 0:\n            return\n\n        for node in nodes:\n            refresh_node(node)\n\n            if (\n                node == placeholder_node\n                or node in considered_nodes\n            ):\n                continue\n\n            if (\n                not isinstance(node, nuke.BackdropNode)\n                or (\n                    isinstance(node, nuke.BackdropNode)\n                    and not set(contained_nodes) <= set(node.getNodes())\n                )\n            ):\n                if offset_y is None and node.xpos() >= min_x:\n                    node.setXpos(node.xpos() + diff_x)\n\n                if node.ypos() >= min_y:\n                    node.setYpos(node.ypos() + diff_y)\n\n            else:\n                width = node.screenWidth()\n                height = node.screenHeight()\n                node.knob(\"bdwidth\").setValue(width + diff_x)\n                node.knob(\"bdheight\").setValue(height + diff_y)\n\n            refresh_node(node)\n\n    def _set_loaded_connections(self, placeholder):\n        \"\"\"\n        set inputs and outputs of loaded nodes\"\"\"\n\n        placeholder_node = nuke.toNode(placeholder.scene_identifier)\n        input_node, output_node = get_group_io_nodes(\n            placeholder.data[\"last_loaded\"]\n        )\n        for node in placeholder_node.dependent():\n            for idx in range(node.inputs()):\n                if node.input(idx) == placeholder_node and output_node:\n                    node.setInput(idx, output_node)\n\n        for node in placeholder_node.dependencies():\n            for idx in range(placeholder_node.inputs()):\n                if placeholder_node.input(idx) == node and input_node:\n                    input_node.setInput(0, node)\n\n    def _create_sib_copies(self, placeholder):\n        \"\"\" creating copies of the palce_holder siblings (the ones who were\n        loaded with it) for the new nodes added\n\n        Returns :\n            copies (dict) : with copied nodes names and their copies\n        \"\"\"\n\n        copies = {}\n        siblings = get_nodes_by_names(placeholder.data[\"siblings\"])\n        for node in siblings:\n            new_node = duplicate_node(node)\n\n            x_init = int(new_node.knob(\"x_init\").getValue())\n            y_init = int(new_node.knob(\"y_init\").getValue())\n            new_node.setXYpos(x_init, y_init)\n            if isinstance(new_node, nuke.BackdropNode):\n                w_init = new_node.knob(\"w_init\").getValue()\n                h_init = new_node.knob(\"h_init\").getValue()\n                new_node.knob(\"bdwidth\").setValue(w_init)\n                new_node.knob(\"bdheight\").setValue(h_init)\n                refresh_node(node)\n\n            if \"repre_id\" in node.knobs().keys():\n                node.removeKnob(node.knob(\"repre_id\"))\n            copies[node.name()] = new_node\n        return copies\n\n    def _set_copies_connections(self, placeholder, copies):\n        \"\"\"Set inputs and outputs of the copies.\n\n        Args:\n            copies (dict): Copied nodes by their names.\n        \"\"\"\n\n        last_input, last_output = get_group_io_nodes(\n            placeholder.data[\"last_loaded\"]\n        )\n        siblings = get_nodes_by_names(placeholder.data[\"siblings\"])\n        siblings_input, siblings_output = get_group_io_nodes(siblings)\n        copy_input = copies[siblings_input.name()]\n        copy_output = copies[siblings_output.name()]\n\n        for node_init in siblings:\n            if node_init == siblings_output:\n                continue\n\n            node_copy = copies[node_init.name()]\n            for node in node_init.dependent():\n                for idx in range(node.inputs()):\n                    if node.input(idx) != node_init:\n                        continue\n\n                    if node in siblings:\n                        copies[node.name()].setInput(idx, node_copy)\n                    else:\n                        last_input.setInput(0, node_copy)\n\n            for node in node_init.dependencies():\n                for idx in range(node_init.inputs()):\n                    if node_init.input(idx) != node:\n                        continue\n\n                    if node_init == siblings_input:\n                        copy_input.setInput(idx, node)\n                    elif node in siblings:\n                        node_copy.setInput(idx, copies[node.name()])\n                    else:\n                        node_copy.setInput(idx, last_output)\n\n        siblings_input.setInput(0, copy_output)\n\n\nclass NukePlaceholderCreatePlugin(\n    NukePlaceholderPlugin, PlaceholderCreateMixin\n):\n    identifier = \"nuke.create\"\n    label = \"Nuke create\"\n\n    def _parse_placeholder_node_data(self, node):\n        placeholder_data = super(\n            NukePlaceholderCreatePlugin, self\n        )._parse_placeholder_node_data(node)\n\n        node_knobs = node.knobs()\n        nb_children = 0\n        if \"nb_children\" in node_knobs:\n            nb_children = int(node_knobs[\"nb_children\"].getValue())\n        placeholder_data[\"nb_children\"] = nb_children\n\n        siblings = []\n        if \"siblings\" in node_knobs:\n            siblings = node_knobs[\"siblings\"].values()\n        placeholder_data[\"siblings\"] = siblings\n\n        node_full_name = node.fullName()\n        placeholder_data[\"group_name\"] = node_full_name.rpartition(\".\")[0]\n        placeholder_data[\"last_loaded\"] = []\n        placeholder_data[\"delete\"] = False\n        return placeholder_data\n\n    def _before_instance_create(self, placeholder):\n        placeholder.data[\"nodes_init\"] = nuke.allNodes()\n\n    def collect_placeholders(self):\n        output = []\n        scene_placeholders = self._collect_scene_placeholders()\n        for node_name, node in scene_placeholders.items():\n            plugin_identifier_knob = node.knob(\"plugin_identifier\")\n            if (\n                plugin_identifier_knob is None\n                or plugin_identifier_knob.getValue() != self.identifier\n            ):\n                continue\n\n            placeholder_data = self._parse_placeholder_node_data(node)\n\n            output.append(\n                CreatePlaceholderItem(node_name, placeholder_data, self)\n            )\n\n        return output\n\n    def populate_placeholder(self, placeholder):\n        self.populate_create_placeholder(placeholder)\n\n    def repopulate_placeholder(self, placeholder):\n        self.populate_create_placeholder(placeholder)\n\n    def get_placeholder_options(self, options=None):\n        return self.get_create_plugin_options(options)\n\n    def post_placeholder_process(self, placeholder, failed):\n        \"\"\"Cleanup placeholder after load of its corresponding representations.\n\n        Args:\n            placeholder (PlaceholderItem): Item which was just used to load\n                representation.\n            failed (bool): Loading of representation failed.\n        \"\"\"\n        # deselect all selected nodes\n        placeholder_node = nuke.toNode(placeholder.scene_identifier)\n\n        # getting the latest nodes added\n        nodes_init = placeholder.data[\"nodes_init\"]\n        nodes_created = list(set(nuke.allNodes()) - set(nodes_init))\n        self.log.debug(\"Created nodes: {}\".format(nodes_created))\n        if not nodes_created:\n            return\n\n        placeholder.data[\"delete\"] = True\n\n        nodes_created = self._move_to_placeholder_group(\n            placeholder, nodes_created\n        )\n        placeholder.data[\"last_created\"] = nodes_created\n        refresh_nodes(nodes_created)\n\n        # positioning of the created nodes\n        min_x, min_y, _, _ = get_extreme_positions(nodes_created)\n        for node in nodes_created:\n            xpos = (node.xpos() - min_x) + placeholder_node.xpos()\n            ypos = (node.ypos() - min_y) + placeholder_node.ypos()\n            node.setXYpos(xpos, ypos)\n        refresh_nodes(nodes_created)\n\n        # fix the problem of z_order for backdrops\n        self._fix_z_order(placeholder)\n\n        if placeholder.data.get(\"keep_placeholder\"):\n            self._imprint_siblings(placeholder)\n\n        if placeholder.data[\"nb_children\"] == 0:\n            # save initial nodes positions and dimensions, update them\n            # and set inputs and outputs of created nodes\n\n            if placeholder.data.get(\"keep_placeholder\"):\n                self._imprint_inits()\n                self._update_nodes(placeholder, nuke.allNodes(), nodes_created)\n\n            self._set_created_connections(placeholder)\n\n        elif placeholder.data[\"siblings\"]:\n            # create copies of placeholder siblings for the new created nodes,\n            # set their inputs and outputs and update all nodes positions and\n            # dimensions and siblings names\n\n            siblings = get_nodes_by_names(placeholder.data[\"siblings\"])\n            refresh_nodes(siblings)\n            copies = self._create_sib_copies(placeholder)\n            new_nodes = list(copies.values())  # copies nodes\n            self._update_nodes(new_nodes, nodes_created)\n            placeholder_node.removeKnob(placeholder_node.knob(\"siblings\"))\n            new_nodes_name = get_names_from_nodes(new_nodes)\n            imprint(placeholder_node, {\"siblings\": new_nodes_name})\n            self._set_copies_connections(placeholder, copies)\n\n            self._update_nodes(\n                nuke.allNodes(),\n                new_nodes + nodes_created,\n                20\n            )\n\n            new_siblings = get_names_from_nodes(new_nodes)\n            placeholder.data[\"siblings\"] = new_siblings\n\n        else:\n            # if the placeholder doesn't have siblings, the created\n            # nodes will be placed in a free space\n\n            xpointer, ypointer = find_free_space_to_paste_nodes(\n                nodes_created, direction=\"bottom\", offset=200\n            )\n            node = nuke.createNode(\"NoOp\")\n            reset_selection()\n            nuke.delete(node)\n            for node in nodes_created:\n                xpos = (node.xpos() - min_x) + xpointer\n                ypos = (node.ypos() - min_y) + ypointer\n                node.setXYpos(xpos, ypos)\n\n        placeholder.data[\"nb_children\"] += 1\n        reset_selection()\n\n        # go back to root group\n        nuke.root().begin()\n\n    def _move_to_placeholder_group(self, placeholder, nodes_created):\n        \"\"\"\n        opening the placeholder's group and copying created nodes in it.\n\n        Returns :\n            nodes_created (list): the new list of pasted nodes\n        \"\"\"\n        groups_name = placeholder.data[\"group_name\"]\n        reset_selection()\n        select_nodes(nodes_created)\n        if groups_name:\n            with node_tempfile() as filepath:\n                nuke.nodeCopy(filepath)\n                for node in nuke.selectedNodes():\n                    nuke.delete(node)\n                group = nuke.toNode(groups_name)\n                group.begin()\n                nuke.nodePaste(filepath)\n                nodes_created = nuke.selectedNodes()\n        return nodes_created\n\n    def _fix_z_order(self, placeholder):\n        \"\"\"Fix the problem of z_order when a backdrop is create.\"\"\"\n\n        nodes_created = placeholder.data[\"last_created\"]\n        created_backdrops = []\n        bd_orders = set()\n        for node in nodes_created:\n            if isinstance(node, nuke.BackdropNode):\n                created_backdrops.append(node)\n                bd_orders.add(node.knob(\"z_order\").getValue())\n\n        if not bd_orders:\n            return\n\n        sib_orders = set()\n        for node_name in placeholder.data[\"siblings\"]:\n            node = nuke.toNode(node_name)\n            if isinstance(node, nuke.BackdropNode):\n                sib_orders.add(node.knob(\"z_order\").getValue())\n\n        if not sib_orders:\n            return\n\n        min_order = min(bd_orders)\n        max_order = max(sib_orders)\n        for backdrop_node in created_backdrops:\n            z_order = backdrop_node.knob(\"z_order\").getValue()\n            backdrop_node.knob(\"z_order\").setValue(\n                z_order + max_order - min_order + 1)\n\n    def _imprint_siblings(self, placeholder):\n        \"\"\"\n        - add siblings names to placeholder attributes (nodes created with it)\n        - add Id to the attributes of all the other nodes\n        \"\"\"\n\n        created_nodes = placeholder.data[\"last_created\"]\n        created_nodes_set = set(created_nodes)\n\n        for node in created_nodes:\n            node_knobs = node.knobs()\n\n            if (\n                \"is_placeholder\" not in node_knobs\n                or (\n                    \"is_placeholder\" in node_knobs\n                    and node.knob(\"is_placeholder\").value()\n                )\n            ):\n                siblings = list(created_nodes_set - {node})\n                siblings_name = get_names_from_nodes(siblings)\n                siblings = {\"siblings\": siblings_name}\n                imprint(node, siblings)\n\n    def _imprint_inits(self):\n        \"\"\"Add initial positions and dimensions to the attributes\"\"\"\n\n        for node in nuke.allNodes():\n            refresh_node(node)\n            imprint(node, {\"x_init\": node.xpos(), \"y_init\": node.ypos()})\n            node.knob(\"x_init\").setVisible(False)\n            node.knob(\"y_init\").setVisible(False)\n            width = node.screenWidth()\n            height = node.screenHeight()\n            if \"bdwidth\" in node.knobs():\n                imprint(node, {\"w_init\": width, \"h_init\": height})\n                node.knob(\"w_init\").setVisible(False)\n                node.knob(\"h_init\").setVisible(False)\n            refresh_node(node)\n\n    def _update_nodes(\n        self, placeholder, nodes, considered_nodes, offset_y=None\n    ):\n        \"\"\"Adjust backdrop nodes dimensions and positions.\n\n        Considering some nodes sizes.\n\n        Args:\n            nodes (list): list of nodes to update\n            considered_nodes (list): list of nodes to consider while updating\n                positions and dimensions\n            offset (int): distance between copies\n        \"\"\"\n\n        placeholder_node = nuke.toNode(placeholder.scene_identifier)\n\n        min_x, min_y, max_x, max_y = get_extreme_positions(considered_nodes)\n\n        diff_x = diff_y = 0\n        contained_nodes = []  # for backdrops\n\n        if offset_y is None:\n            width_ph = placeholder_node.screenWidth()\n            height_ph = placeholder_node.screenHeight()\n            diff_y = max_y - min_y - height_ph\n            diff_x = max_x - min_x - width_ph\n            contained_nodes = [placeholder_node]\n            min_x = placeholder_node.xpos()\n            min_y = placeholder_node.ypos()\n        else:\n            siblings = get_nodes_by_names(placeholder.data[\"siblings\"])\n            minX, _, maxX, _ = get_extreme_positions(siblings)\n            diff_y = max_y - min_y + 20\n            diff_x = abs(max_x - min_x - maxX + minX)\n            contained_nodes = considered_nodes\n\n        if diff_y <= 0 and diff_x <= 0:\n            return\n\n        for node in nodes:\n            refresh_node(node)\n\n            if (\n                node == placeholder_node\n                or node in considered_nodes\n            ):\n                continue\n\n            if (\n                not isinstance(node, nuke.BackdropNode)\n                or (\n                    isinstance(node, nuke.BackdropNode)\n                    and not set(contained_nodes) <= set(node.getNodes())\n                )\n            ):\n                if offset_y is None and node.xpos() >= min_x:\n                    node.setXpos(node.xpos() + diff_x)\n\n                if node.ypos() >= min_y:\n                    node.setYpos(node.ypos() + diff_y)\n\n            else:\n                width = node.screenWidth()\n                height = node.screenHeight()\n                node.knob(\"bdwidth\").setValue(width + diff_x)\n                node.knob(\"bdheight\").setValue(height + diff_y)\n\n            refresh_node(node)\n\n    def _set_created_connections(self, placeholder):\n        \"\"\"\n        set inputs and outputs of created nodes\"\"\"\n\n        placeholder_node = nuke.toNode(placeholder.scene_identifier)\n        input_node, output_node = get_group_io_nodes(\n            placeholder.data[\"last_created\"]\n        )\n        for node in placeholder_node.dependent():\n            for idx in range(node.inputs()):\n                if node.input(idx) == placeholder_node and output_node:\n                    node.setInput(idx, output_node)\n\n        for node in placeholder_node.dependencies():\n            for idx in range(placeholder_node.inputs()):\n                if placeholder_node.input(idx) == node and input_node:\n                    input_node.setInput(0, node)\n\n    def _create_sib_copies(self, placeholder):\n        \"\"\" creating copies of the palce_holder siblings (the ones who were\n        created with it) for the new nodes added\n\n        Returns :\n            copies (dict) : with copied nodes names and their copies\n        \"\"\"\n\n        copies = {}\n        siblings = get_nodes_by_names(placeholder.data[\"siblings\"])\n        for node in siblings:\n            new_node = duplicate_node(node)\n\n            x_init = int(new_node.knob(\"x_init\").getValue())\n            y_init = int(new_node.knob(\"y_init\").getValue())\n            new_node.setXYpos(x_init, y_init)\n            if isinstance(new_node, nuke.BackdropNode):\n                w_init = new_node.knob(\"w_init\").getValue()\n                h_init = new_node.knob(\"h_init\").getValue()\n                new_node.knob(\"bdwidth\").setValue(w_init)\n                new_node.knob(\"bdheight\").setValue(h_init)\n                refresh_node(node)\n\n            if \"repre_id\" in node.knobs().keys():\n                node.removeKnob(node.knob(\"repre_id\"))\n            copies[node.name()] = new_node\n        return copies\n\n    def _set_copies_connections(self, placeholder, copies):\n        \"\"\"Set inputs and outputs of the copies.\n\n        Args:\n            copies (dict): Copied nodes by their names.\n        \"\"\"\n\n        last_input, last_output = get_group_io_nodes(\n            placeholder.data[\"last_created\"]\n        )\n        siblings = get_nodes_by_names(placeholder.data[\"siblings\"])\n        siblings_input, siblings_output = get_group_io_nodes(siblings)\n        copy_input = copies[siblings_input.name()]\n        copy_output = copies[siblings_output.name()]\n\n        for node_init in siblings:\n            if node_init == siblings_output:\n                continue\n\n            node_copy = copies[node_init.name()]\n            for node in node_init.dependent():\n                for idx in range(node.inputs()):\n                    if node.input(idx) != node_init:\n                        continue\n\n                    if node in siblings:\n                        copies[node.name()].setInput(idx, node_copy)\n                    else:\n                        last_input.setInput(0, node_copy)\n\n            for node in node_init.dependencies():\n                for idx in range(node_init.inputs()):\n                    if node_init.input(idx) != node:\n                        continue\n\n                    if node_init == siblings_input:\n                        copy_input.setInput(idx, node)\n                    elif node in siblings:\n                        node_copy.setInput(idx, copies[node.name()])\n                    else:\n                        node_copy.setInput(idx, last_output)\n\n        siblings_input.setInput(0, copy_output)\n\n\ndef build_workfile_template(*args, **kwargs):\n    builder = NukeTemplateBuilder(registered_host())\n    builder.build_template(*args, **kwargs)\n\n    # set all settings to shot context default\n    WorkfileSettings().set_context_settings()\n\n\ndef update_workfile_template(*args):\n    builder = NukeTemplateBuilder(registered_host())\n    builder.rebuild_template()\n\n\ndef create_placeholder(*args):\n    host = registered_host()\n    builder = NukeTemplateBuilder(host)\n    window = WorkfileBuildPlaceholderDialog(host, builder,\n                                            parent=get_main_window())\n    window.show()\n\n\ndef update_placeholder(*args):\n    host = registered_host()\n    builder = NukeTemplateBuilder(host)\n    placeholder_items_by_id = {\n        placeholder_item.scene_identifier: placeholder_item\n        for placeholder_item in builder.get_placeholders()\n    }\n    placeholder_items = []\n    for node in nuke.selectedNodes():\n        node_name = node.fullName()\n        if node_name in placeholder_items_by_id:\n            placeholder_items.append(placeholder_items_by_id[node_name])\n\n    # TODO show UI at least\n    if len(placeholder_items) == 0:\n        raise ValueError(\"No node selected\")\n\n    if len(placeholder_items) > 1:\n        raise ValueError(\"Too many selected nodes\")\n\n    placeholder_item = placeholder_items[0]\n    window = WorkfileBuildPlaceholderDialog(host, builder,\n                                            parent=get_main_window())\n    window.set_update_mode(placeholder_item)\n    window.exec_()\n"
  },
  {
    "path": "openpype/hosts/nuke/api/workio.py",
    "content": "\"\"\"Host API required Work Files tool\"\"\"\nimport os\nimport nuke\nimport shutil\nfrom .utils import is_headless\n\n\ndef file_extensions():\n    return [\".nk\"]\n\n\ndef has_unsaved_changes():\n    return nuke.root().modified()\n\n\ndef save_file(filepath):\n    path = filepath.replace(\"\\\\\", \"/\")\n    nuke.scriptSaveAs(path, overwrite=1)\n    nuke.Root()[\"name\"].setValue(path)\n    nuke.Root()[\"project_directory\"].setValue(os.path.dirname(path))\n    nuke.Root().setModified(False)\n\n\ndef open_file(filepath):\n\n    def read_script(nuke_script):\n        nuke.scriptClear()\n        nuke.scriptReadFile(nuke_script)\n        nuke.Root()[\"name\"].setValue(nuke_script)\n        nuke.Root()[\"project_directory\"].setValue(os.path.dirname(nuke_script))\n        nuke.Root().setModified(False)\n\n    filepath = filepath.replace(\"\\\\\", \"/\")\n\n    # To remain in the same window, we have to clear the script and read\n    # in the contents of the workfile.\n    # Nuke Preferences can be read after the script is read.\n    read_script(filepath)\n\n    if not is_headless():\n        autosave = nuke.toNode(\"preferences\")[\"AutoSaveName\"].evaluate()\n        autosave_prmpt = \"Autosave detected.\\n\" \\\n                         \"Would you like to load the autosave file?\"  # noqa\n        if os.path.isfile(autosave) and nuke.ask(autosave_prmpt):\n            try:\n                # Overwrite the filepath with autosave\n                shutil.copy(autosave, filepath)\n                # Now read the (auto-saved) script again\n                read_script(filepath)\n            except shutil.Error as err:\n                nuke.message(\n                    \"Detected autosave file could not be used.\\n{}\"\n\n                    .format(err))\n\n    return True\n\n\ndef current_file():\n    current_file = nuke.root().name()\n\n    # Unsaved current file\n    if current_file == 'Root':\n        return None\n\n    return os.path.normpath(current_file).replace(\"\\\\\", \"/\")\n\n\ndef work_root(session):\n\n    work_dir = session[\"AVALON_WORKDIR\"]\n    scene_dir = session.get(\"AVALON_SCENEDIR\")\n    if scene_dir:\n        path = os.path.join(work_dir, scene_dir)\n    else:\n        path = work_dir\n\n    return os.path.normpath(path).replace(\"\\\\\", \"/\")\n"
  },
  {
    "path": "openpype/hosts/nuke/hooks/pre_nukeassist_setup.py",
    "content": "from openpype.lib.applications import PreLaunchHook\n\n\nclass PrelaunchNukeAssistHook(PreLaunchHook):\n    \"\"\"\n    Adding flag when nukeassist\n    \"\"\"\n    app_groups = {\"nukeassist\"}\n    launch_types = set()\n\n    def execute(self):\n        self.launch_context.env[\"NUKEASSIST\"] = \"1\"\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/nuke/plugins/create/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/nuke/plugins/create/convert_legacy.py",
    "content": "from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin\nfrom openpype.hosts.nuke.api.lib import (\n    INSTANCE_DATA_KNOB,\n    get_node_data,\n    get_avalon_knob_data,\n    AVALON_TAB,\n)\nfrom openpype.hosts.nuke.api.plugin import convert_to_valid_instaces\n\nimport nuke\n\n\nclass LegacyConverted(SubsetConvertorPlugin):\n    identifier = \"legacy.converter\"\n\n    def find_instances(self):\n\n        legacy_found = False\n        # search for first available legacy item\n        for node in nuke.allNodes(recurseGroups=True):\n            if node.Class() in [\"Viewer\", \"Dot\"]:\n                continue\n\n            if get_node_data(node, INSTANCE_DATA_KNOB):\n                continue\n\n            if AVALON_TAB not in node.knobs():\n                continue\n\n            # get data from avalon knob\n            avalon_knob_data = get_avalon_knob_data(\n                node, [\"avalon:\", \"ak:\"], create=False)\n\n            if not avalon_knob_data:\n                continue\n\n            if avalon_knob_data[\"id\"] != \"pyblish.avalon.instance\":\n                continue\n\n            # catch and break\n            legacy_found = True\n            break\n\n        if legacy_found:\n            # if not item do not add legacy instance converter\n            self.add_convertor_item(\"Convert legacy instances\")\n\n    def convert(self):\n        # loop all instances and convert them\n        convert_to_valid_instaces()\n        # remove legacy item if all is fine\n        self.remove_convertor_item()\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/create/create_backdrop.py",
    "content": "from nukescripts import autoBackdrop\n\nfrom openpype.hosts.nuke.api import (\n    NukeCreator,\n    maintained_selection,\n    select_nodes\n)\n\n\nclass CreateBackdrop(NukeCreator):\n    \"\"\"Add Publishable Backdrop\"\"\"\n\n    identifier = \"create_backdrop\"\n    label = \"Nukenodes (backdrop)\"\n    family = \"nukenodes\"\n    icon = \"file-archive-o\"\n    maintain_selection = True\n\n    # plugin attributes\n    node_color = \"0xdfea5dff\"\n\n    def create_instance_node(\n        self,\n        node_name,\n        knobs=None,\n        parent=None,\n        node_type=None\n    ):\n        with maintained_selection():\n            if len(self.selected_nodes) >= 1:\n                select_nodes(self.selected_nodes)\n\n            created_node = autoBackdrop()\n            created_node[\"name\"].setValue(node_name)\n            created_node[\"tile_color\"].setValue(int(self.node_color, 16))\n            created_node[\"note_font_size\"].setValue(24)\n            created_node[\"label\"].setValue(\"[{}]\".format(node_name))\n\n            return created_node\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # make sure subset name is unique\n        self.check_existing_subset(subset_name)\n\n        instance = super(CreateBackdrop, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data\n        )\n\n        return instance\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/create/create_camera.py",
    "content": "import nuke\nfrom openpype.hosts.nuke.api import (\n    NukeCreator,\n    NukeCreatorError,\n    maintained_selection\n)\nfrom openpype.hosts.nuke.api.lib import (\n    create_camera_node_by_version\n)\n\n\nclass CreateCamera(NukeCreator):\n    \"\"\"Add Publishable Camera\"\"\"\n\n    identifier = \"create_camera\"\n    label = \"Camera (3d)\"\n    family = \"camera\"\n    icon = \"camera\"\n\n    # plugin attributes\n    node_color = \"0xff9100ff\"\n\n    def create_instance_node(\n        self,\n        node_name,\n        knobs=None,\n        parent=None,\n        node_type=None\n    ):\n        with maintained_selection():\n            if self.selected_nodes:\n                node = self.selected_nodes[0]\n                if node.Class() != \"Camera3\":\n                    raise NukeCreatorError(\n                        \"Creator error: Select only camera node type\")\n                created_node = self.selected_nodes[0]\n            else:\n                created_node = create_camera_node_by_version()\n\n            created_node[\"tile_color\"].setValue(\n                int(self.node_color, 16))\n\n            created_node[\"name\"].setValue(node_name)\n\n            return created_node\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # make sure subset name is unique\n        self.check_existing_subset(subset_name)\n\n        instance = super(CreateCamera, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data\n        )\n\n        return instance\n\n    def set_selected_nodes(self, pre_create_data):\n        if pre_create_data.get(\"use_selection\"):\n            self.selected_nodes = nuke.selectedNodes()\n            if self.selected_nodes == []:\n                raise NukeCreatorError(\n                    \"Creator error: No active selection\")\n            elif len(self.selected_nodes) > 1:\n                raise NukeCreatorError(\n                    \"Creator error: Select only one camera node\")\n        else:\n            self.selected_nodes = []\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/create/create_gizmo.py",
    "content": "import nuke\nfrom openpype.hosts.nuke.api import (\n    NukeCreator,\n    NukeCreatorError,\n    maintained_selection\n)\n\n\nclass CreateGizmo(NukeCreator):\n    \"\"\"Add Publishable Group as gizmo\"\"\"\n\n    identifier = \"create_gizmo\"\n    label = \"Gizmo (group)\"\n    family = \"gizmo\"\n    icon = \"file-archive-o\"\n    default_variants = [\"ViewerInput\", \"Lut\", \"Effect\"]\n\n    # plugin attributes\n    node_color = \"0x7533c1ff\"\n\n    def create_instance_node(\n        self,\n        node_name,\n        knobs=None,\n        parent=None,\n        node_type=None\n    ):\n        with maintained_selection():\n            if self.selected_nodes:\n                node = self.selected_nodes[0]\n                if node.Class() != \"Group\":\n                    raise NukeCreatorError(\n                        \"Creator error: Select only 'Group' node type\")\n                created_node = node\n            else:\n                created_node = nuke.collapseToGroup()\n\n            created_node[\"tile_color\"].setValue(\n                int(self.node_color, 16))\n\n            created_node[\"name\"].setValue(node_name)\n\n            return created_node\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # make sure subset name is unique\n        self.check_existing_subset(subset_name)\n\n        instance = super(CreateGizmo, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data\n        )\n\n        return instance\n\n    def set_selected_nodes(self, pre_create_data):\n        if pre_create_data.get(\"use_selection\"):\n            self.selected_nodes = nuke.selectedNodes()\n            if self.selected_nodes == []:\n                raise NukeCreatorError(\"Creator error: No active selection\")\n            elif len(self.selected_nodes) > 1:\n                NukeCreatorError(\"Creator error: Select only one 'Group' node\")\n        else:\n            self.selected_nodes = []\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/create/create_model.py",
    "content": "import nuke\nfrom openpype.hosts.nuke.api import (\n    NukeCreator,\n    NukeCreatorError,\n    maintained_selection\n)\n\n\nclass CreateModel(NukeCreator):\n    \"\"\"Add Publishable Camera\"\"\"\n\n    identifier = \"create_model\"\n    label = \"Model (3d)\"\n    family = \"model\"\n    icon = \"cube\"\n    default_variants = [\"Main\"]\n\n    # plugin attributes\n    node_color = \"0xff3200ff\"\n\n    def create_instance_node(\n        self,\n        node_name,\n        knobs=None,\n        parent=None,\n        node_type=None\n    ):\n        with maintained_selection():\n            if self.selected_nodes:\n                node = self.selected_nodes[0]\n                if node.Class() != \"Scene\":\n                    raise NukeCreatorError(\n                        \"Creator error: Select only 'Scene' node type\")\n                created_node = node\n            else:\n                created_node = nuke.createNode(\"Scene\")\n\n            created_node[\"tile_color\"].setValue(\n                int(self.node_color, 16))\n\n            created_node[\"name\"].setValue(node_name)\n\n            return created_node\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # make sure subset name is unique\n        self.check_existing_subset(subset_name)\n\n        instance = super(CreateModel, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data\n        )\n\n        return instance\n\n    def set_selected_nodes(self, pre_create_data):\n        if pre_create_data.get(\"use_selection\"):\n            self.selected_nodes = nuke.selectedNodes()\n            if self.selected_nodes == []:\n                raise NukeCreatorError(\"Creator error: No active selection\")\n            elif len(self.selected_nodes) > 1:\n                NukeCreatorError(\"Creator error: Select only one 'Scene' node\")\n        else:\n            self.selected_nodes = []\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/create/create_source.py",
    "content": "import nuke\nimport six\nimport sys\nfrom openpype.hosts.nuke.api import (\n    INSTANCE_DATA_KNOB,\n    NukeCreator,\n    NukeCreatorError,\n    set_node_data\n)\nfrom openpype.pipeline import (\n    CreatedInstance\n)\n\n\nclass CreateSource(NukeCreator):\n    \"\"\"Add Publishable Read with source\"\"\"\n\n    identifier = \"create_source\"\n    label = \"Source (read)\"\n    family = \"source\"\n    icon = \"film\"\n    default_variants = [\"Effect\", \"Backplate\", \"Fire\", \"Smoke\"]\n\n    # plugin attributes\n    node_color = \"0xff9100ff\"\n\n    def create_instance_node(\n        self,\n        node_name,\n        read_node\n    ):\n        read_node[\"tile_color\"].setValue(\n            int(self.node_color, 16))\n        read_node[\"name\"].setValue(node_name)\n\n        return read_node\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        # make sure selected nodes are added\n        self.set_selected_nodes(pre_create_data)\n\n        try:\n            for read_node in self.selected_nodes:\n                if read_node.Class() != 'Read':\n                    continue\n\n                node_name = read_node.name()\n                _subset_name = subset_name + node_name\n\n                # make sure subset name is unique\n                self.check_existing_subset(_subset_name)\n\n                instance_node = self.create_instance_node(\n                    _subset_name,\n                    read_node\n                )\n                instance = CreatedInstance(\n                    self.family,\n                    _subset_name,\n                    instance_data,\n                    self\n                )\n\n                instance.transient_data[\"node\"] = instance_node\n\n                self._add_instance_to_context(instance)\n\n                set_node_data(\n                    instance_node,\n                    INSTANCE_DATA_KNOB,\n                    instance.data_to_store()\n                )\n\n        except Exception as er:\n            six.reraise(\n                NukeCreatorError,\n                NukeCreatorError(\"Creator error: {}\".format(er)),\n                sys.exc_info()[2])\n\n    def set_selected_nodes(self, pre_create_data):\n        if pre_create_data.get(\"use_selection\"):\n            self.selected_nodes = nuke.selectedNodes()\n            if self.selected_nodes == []:\n                raise NukeCreatorError(\"Creator error: No active selection\")\n        else:\n            NukeCreatorError(\n                \"Creator error: only supported with active selection\")\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/create/create_write_image.py",
    "content": "import nuke\nimport sys\nimport six\n\nfrom openpype.pipeline import (\n    CreatedInstance\n)\nfrom openpype.lib import (\n    BoolDef,\n    NumberDef,\n    UISeparatorDef,\n    EnumDef\n)\nfrom openpype.hosts.nuke import api as napi\nfrom openpype.hosts.nuke.api.plugin import exposed_write_knobs\n\n\nclass CreateWriteImage(napi.NukeWriteCreator):\n    identifier = \"create_write_image\"\n    label = \"Image (write)\"\n    family = \"image\"\n    icon = \"sign-out\"\n\n    instance_attributes = [\n        \"use_range_limit\"\n    ]\n    default_variants = [\n        \"StillFrame\",\n        \"MPFrame\",\n        \"LayoutFrame\"\n    ]\n    temp_rendering_path_template = (\n        \"{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}\")\n\n    def get_pre_create_attr_defs(self):\n        attr_defs = [\n            BoolDef(\n                \"use_selection\",\n                default=not self.create_context.headless,\n                label=\"Use selection\"\n            ),\n            self._get_render_target_enum(),\n            UISeparatorDef(),\n            self._get_frame_source_number()\n        ]\n        return attr_defs\n\n    def _get_render_target_enum(self):\n        rendering_targets = {\n            \"local\": \"Local machine rendering\",\n            \"frames\": \"Use existing frames\"\n        }\n\n        return EnumDef(\n            \"render_target\",\n            items=rendering_targets,\n            label=\"Render target\"\n        )\n\n    def _get_frame_source_number(self):\n        return NumberDef(\n            \"active_frame\",\n            label=\"Active frame\",\n            default=nuke.frame()\n        )\n\n    def create_instance_node(self, subset_name, instance_data):\n\n        # add fpath_template\n        write_data = {\n            \"creator\": self.__class__.__name__,\n            \"subset\": subset_name,\n            \"fpath_template\": self.temp_rendering_path_template\n        }\n        write_data.update(instance_data)\n\n        created_node = napi.create_write_node(\n            subset_name,\n            write_data,\n            input=self.selected_node,\n            prenodes=self.prenodes,\n            linked_knobs=self.get_linked_knobs(),\n            **{\n                \"frame\": nuke.frame()\n            }\n        )\n\n        self._add_frame_range_limit(created_node, instance_data)\n\n        self.integrate_links(created_node, outputs=True)\n\n        return created_node\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        subset_name = subset_name.format(**pre_create_data)\n\n        # pass values from precreate to instance\n        self.pass_pre_attributes_to_instance(\n            instance_data,\n            pre_create_data,\n            [\n                \"active_frame\",\n                \"render_target\"\n            ]\n        )\n\n        # make sure selected nodes are added\n        self.set_selected_nodes(pre_create_data)\n\n        # make sure subset name is unique\n        self.check_existing_subset(subset_name)\n\n        instance_node = self.create_instance_node(\n            subset_name,\n            instance_data,\n        )\n\n        try:\n            instance = CreatedInstance(\n                self.family,\n                subset_name,\n                instance_data,\n                self\n            )\n\n            instance.transient_data[\"node\"] = instance_node\n\n            self._add_instance_to_context(instance)\n\n            napi.set_node_data(\n                instance_node,\n                napi.INSTANCE_DATA_KNOB,\n                instance.data_to_store()\n            )\n\n            exposed_write_knobs(\n                self.project_settings, self.__class__.__name__, instance_node\n            )\n\n            return instance\n\n        except Exception as er:\n            six.reraise(\n                napi.NukeCreatorError,\n                napi.NukeCreatorError(\"Creator error: {}\".format(er)),\n                sys.exc_info()[2]\n            )\n\n    def _add_frame_range_limit(self, write_node, instance_data):\n        if \"use_range_limit\" not in self.instance_attributes:\n            return\n\n        active_frame = (\n            instance_data[\"creator_attributes\"].get(\"active_frame\"))\n\n        write_node.begin()\n        for n in nuke.allNodes():\n            # get write node\n            if n.Class() in \"Write\":\n                w_node = n\n        write_node.end()\n\n        w_node[\"use_limit\"].setValue(True)\n        w_node[\"first\"].setValue(active_frame or nuke.frame())\n        w_node[\"last\"].setExpression(\"first\")\n\n        return write_node\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/create/create_write_prerender.py",
    "content": "import nuke\nimport sys\nimport six\n\nfrom openpype.pipeline import (\n    CreatedInstance\n)\nfrom openpype.lib import (\n    BoolDef\n)\nfrom openpype.hosts.nuke import api as napi\nfrom openpype.hosts.nuke.api.plugin import exposed_write_knobs\n\n\nclass CreateWritePrerender(napi.NukeWriteCreator):\n    identifier = \"create_write_prerender\"\n    label = \"Prerender (write)\"\n    family = \"prerender\"\n    icon = \"sign-out\"\n\n    instance_attributes = [\n        \"use_range_limit\"\n    ]\n    default_variants = [\n        \"Key01\",\n        \"Bg01\",\n        \"Fg01\",\n        \"Branch01\",\n        \"Part01\"\n    ]\n    temp_rendering_path_template = (\n        \"{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}\")\n\n    # Before write node render.\n    order = 90\n\n    def get_pre_create_attr_defs(self):\n        attr_defs = [\n            BoolDef(\n                \"use_selection\",\n                default=not self.create_context.headless,\n                label=\"Use selection\"\n            ),\n            self._get_render_target_enum()\n        ]\n        return attr_defs\n\n    def create_instance_node(self, subset_name, instance_data):\n        # add fpath_template\n        write_data = {\n            \"creator\": self.__class__.__name__,\n            \"subset\": subset_name,\n            \"fpath_template\": self.temp_rendering_path_template\n        }\n\n        write_data.update(instance_data)\n\n        # get width and height\n        if self.selected_node:\n            width, height = (\n                self.selected_node.width(), self.selected_node.height())\n        else:\n            actual_format = nuke.root().knob('format').value()\n            width, height = (actual_format.width(), actual_format.height())\n\n        created_node = napi.create_write_node(\n            subset_name,\n            write_data,\n            input=self.selected_node,\n            prenodes=self.prenodes,\n            linked_knobs=self.get_linked_knobs(),\n            **{\n                \"width\": width,\n                \"height\": height\n            }\n        )\n\n        self._add_frame_range_limit(created_node)\n\n        self.integrate_links(created_node, outputs=True)\n\n        return created_node\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # pass values from precreate to instance\n        self.pass_pre_attributes_to_instance(\n            instance_data,\n            pre_create_data,\n            [\n                \"render_target\"\n            ]\n        )\n\n        # make sure selected nodes are added\n        self.set_selected_nodes(pre_create_data)\n\n        # make sure subset name is unique\n        self.check_existing_subset(subset_name)\n\n        instance_node = self.create_instance_node(\n            subset_name,\n            instance_data\n        )\n\n        try:\n            instance = CreatedInstance(\n                self.family,\n                subset_name,\n                instance_data,\n                self\n            )\n\n            instance.transient_data[\"node\"] = instance_node\n\n            self._add_instance_to_context(instance)\n\n            napi.set_node_data(\n                instance_node,\n                napi.INSTANCE_DATA_KNOB,\n                instance.data_to_store()\n            )\n\n            exposed_write_knobs(\n                self.project_settings, self.__class__.__name__, instance_node\n            )\n\n            return instance\n\n        except Exception as er:\n            six.reraise(\n                napi.NukeCreatorError,\n                napi.NukeCreatorError(\"Creator error: {}\".format(er)),\n                sys.exc_info()[2]\n            )\n\n    def _add_frame_range_limit(self, write_node):\n        if \"use_range_limit\" not in self.instance_attributes:\n            return\n\n        write_node.begin()\n        for n in nuke.allNodes():\n            # get write node\n            if n.Class() in \"Write\":\n                w_node = n\n        write_node.end()\n\n        w_node[\"use_limit\"].setValue(True)\n        w_node[\"first\"].setValue(nuke.root()[\"first_frame\"].value())\n        w_node[\"last\"].setValue(nuke.root()[\"last_frame\"].value())\n\n        return write_node\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/create/create_write_render.py",
    "content": "import nuke\nimport sys\nimport six\n\nfrom openpype.pipeline import (\n    CreatedInstance\n)\nfrom openpype.lib import (\n    BoolDef\n)\nfrom openpype.hosts.nuke import api as napi\nfrom openpype.hosts.nuke.api.plugin import exposed_write_knobs\n\n\nclass CreateWriteRender(napi.NukeWriteCreator):\n    identifier = \"create_write_render\"\n    label = \"Render (write)\"\n    family = \"render\"\n    icon = \"sign-out\"\n\n    instance_attributes = [\n        \"reviewable\"\n    ]\n    default_variants = [\n        \"Main\",\n        \"Mask\"\n    ]\n    temp_rendering_path_template = (\n        \"{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}\")\n\n    def get_pre_create_attr_defs(self):\n        attr_defs = [\n            BoolDef(\n                \"use_selection\",\n                default=not self.create_context.headless,\n                label=\"Use selection\"\n            ),\n            self._get_render_target_enum()\n        ]\n        return attr_defs\n\n    def create_instance_node(self, subset_name, instance_data):\n        # add fpath_template\n        write_data = {\n            \"creator\": self.__class__.__name__,\n            \"subset\": subset_name,\n            \"fpath_template\": self.temp_rendering_path_template\n        }\n\n        write_data.update(instance_data)\n\n        # get width and height\n        if self.selected_node:\n            width, height = (\n                self.selected_node.width(), self.selected_node.height())\n        else:\n            actual_format = nuke.root().knob('format').value()\n            width, height = (actual_format.width(), actual_format.height())\n\n        self.log.debug(\">>>>>>> : {}\".format(self.instance_attributes))\n        self.log.debug(\">>>>>>> : {}\".format(self.get_linked_knobs()))\n\n        created_node = napi.create_write_node(\n            subset_name,\n            write_data,\n            input=self.selected_node,\n            prenodes=self.prenodes,\n            linked_knobs=self.get_linked_knobs(),\n            **{\n                \"width\": width,\n                \"height\": height\n            }\n        )\n\n        self.integrate_links(created_node, outputs=False)\n\n        return created_node\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # pass values from precreate to instance\n        self.pass_pre_attributes_to_instance(\n            instance_data,\n            pre_create_data,\n            [\n                \"render_target\"\n            ]\n        )\n        # make sure selected nodes are added\n        self.set_selected_nodes(pre_create_data)\n\n        # make sure subset name is unique\n        self.check_existing_subset(subset_name)\n\n        instance_node = self.create_instance_node(\n            subset_name,\n            instance_data\n        )\n\n        try:\n            instance = CreatedInstance(\n                self.family,\n                subset_name,\n                instance_data,\n                self\n            )\n\n            instance.transient_data[\"node\"] = instance_node\n\n            self._add_instance_to_context(instance)\n\n            napi.set_node_data(\n                instance_node,\n                napi.INSTANCE_DATA_KNOB,\n                instance.data_to_store()\n            )\n\n            exposed_write_knobs(\n                self.project_settings, self.__class__.__name__, instance_node\n            )\n\n            return instance\n\n        except Exception as er:\n            six.reraise(\n                napi.NukeCreatorError,\n                napi.NukeCreatorError(\"Creator error: {}\".format(er)),\n                sys.exc_info()[2]\n            )\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/create/workfile_creator.py",
    "content": "import openpype.hosts.nuke.api as api\nfrom openpype.client import get_asset_by_name\nfrom openpype.pipeline import (\n    AutoCreator,\n    CreatedInstance,\n)\nfrom openpype.hosts.nuke.api import (\n    INSTANCE_DATA_KNOB,\n    set_node_data\n)\nimport nuke\n\n\nclass WorkfileCreator(AutoCreator):\n    identifier = \"workfile\"\n    family = \"workfile\"\n\n    default_variant = \"Main\"\n\n    def get_instance_attr_defs(self):\n        return []\n\n    def collect_instances(self):\n        root_node = nuke.root()\n        instance_data = api.get_node_data(\n            root_node, api.INSTANCE_DATA_KNOB\n        )\n\n        project_name = self.create_context.get_current_project_name()\n        asset_name = self.create_context.get_current_asset_name()\n        task_name = self.create_context.get_current_task_name()\n        host_name = self.create_context.host_name\n\n        asset_doc = get_asset_by_name(project_name, asset_name)\n        subset_name = self.get_subset_name(\n            self.default_variant, task_name, asset_doc,\n            project_name, host_name\n        )\n        instance_data.update({\n            \"asset\": asset_name,\n            \"task\": task_name,\n            \"variant\": self.default_variant\n        })\n        instance_data.update(self.get_dynamic_data(\n            self.default_variant, task_name, asset_doc,\n            project_name, host_name, instance_data\n        ))\n\n        instance = CreatedInstance(\n            self.family, subset_name, instance_data, self\n        )\n        instance.transient_data[\"node\"] = root_node\n        self._add_instance_to_context(instance)\n\n    def update_instances(self, update_list):\n        for created_inst, _changes in update_list:\n            instance_node = created_inst.transient_data[\"node\"]\n\n            set_node_data(\n                instance_node,\n                INSTANCE_DATA_KNOB,\n                created_inst.data_to_store()\n            )\n\n    def create(self, options=None):\n        # no need to create if it is created\n        # in `collect_instances`\n        pass\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/inventory/repair_old_loaders.py",
    "content": "from openpype.lib import Logger\nfrom openpype.pipeline import InventoryAction\nfrom openpype.hosts.nuke.api.lib import set_avalon_knob_data\n\n\nclass RepairOldLoaders(InventoryAction):\n\n    label = \"Repair Old Loaders\"\n    icon = \"gears\"\n    color = \"#cc0000\"\n\n    log = Logger.get_logger(__name__)\n\n    def process(self, containers):\n        import nuke\n        new_loader = \"LoadClip\"\n\n        for cdata in containers:\n            orig_loader = cdata[\"loader\"]\n            orig_name = cdata[\"objectName\"]\n            if orig_loader not in [\"LoadSequence\", \"LoadMov\"]:\n                self.log.warning(\n                    \"This repair action is only working on \"\n                    \"`LoadSequence` and `LoadMov` Loaders\")\n                continue\n\n            new_name = orig_name.replace(orig_loader, new_loader)\n            node = nuke.toNode(cdata[\"objectName\"])\n\n            cdata.update({\n                \"loader\": new_loader,\n                \"objectName\": new_name\n            })\n            node[\"name\"].setValue(new_name)\n            # get data from avalon knob\n            set_avalon_knob_data(node, cdata)\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/inventory/select_containers.py",
    "content": "from openpype.pipeline import InventoryAction\nfrom openpype.hosts.nuke.api.command import viewer_update_and_undo_stop\n\n\nclass SelectContainers(InventoryAction):\n\n    label = \"Select Containers\"\n    icon = \"mouse-pointer\"\n    color = \"#d8d8d8\"\n\n    def process(self, containers):\n        import nuke\n\n        nodes = [nuke.toNode(i[\"objectName\"]) for i in containers]\n\n        with viewer_update_and_undo_stop():\n            # clear previous_selection\n            [n['selected'].setValue(False) for n in nodes]\n            # Select tool\n            for node in nodes:\n                node[\"selected\"].setValue(True)\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/actions.py",
    "content": "\"\"\"A module containing generic loader actions that will display in the Loader.\n\n\"\"\"\n\nfrom openpype.lib import Logger\nfrom openpype.pipeline import load\n\nlog = Logger.get_logger(__name__)\n\n\nclass SetFrameRangeLoader(load.LoaderPlugin):\n    \"\"\"Set frame range excluding pre- and post-handles\"\"\"\n\n    families = [\"animation\",\n                \"camera\",\n                \"write\",\n                \"yeticache\",\n                \"pointcache\"]\n    representations = [\"*\"]\n    extensions = {\"*\"}\n\n    label = \"Set frame range\"\n    order = 11\n    icon = \"clock-o\"\n    color = \"white\"\n\n    def load(self, context, name, namespace, data):\n\n        from openpype.hosts.nuke.api import lib\n\n        version = context['version']\n        version_data = version.get(\"data\", {})\n\n        start = version_data.get(\"frameStart\", None)\n        end = version_data.get(\"frameEnd\", None)\n\n        log.info(\"start: {}, end: {}\".format(start, end))\n        if start is None or end is None:\n            log.info(\"Skipping setting frame range because start or \"\n                     \"end frame data is missing..\")\n            return\n\n        lib.update_frame_range(start, end)\n\n\nclass SetFrameRangeWithHandlesLoader(load.LoaderPlugin):\n    \"\"\"Set frame range including pre- and post-handles\"\"\"\n\n    families = [\"animation\",\n                \"camera\",\n                \"write\",\n                \"yeticache\",\n                \"pointcache\"]\n    representations = [\"*\"]\n\n    label = \"Set frame range (with handles)\"\n    order = 12\n    icon = \"clock-o\"\n    color = \"white\"\n\n    def load(self, context, name, namespace, data):\n\n        from openpype.hosts.nuke.api import lib\n\n        version = context['version']\n        version_data = version.get(\"data\", {})\n\n        start = version_data.get(\"frameStart\", None)\n        end = version_data.get(\"frameEnd\", None)\n\n        if start is None or end is None:\n            print(\"Skipping setting frame range because start or \"\n                  \"end frame data is missing..\")\n            return\n\n        # Include handles\n        start -= version_data.get(\"handleStart\", 0)\n        end += version_data.get(\"handleEnd\", 0)\n\n        lib.update_frame_range(start, end)\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/load_backdrop.py",
    "content": "import nuke\nimport nukescripts\n\nfrom openpype.client import (\n    get_version_by_id,\n    get_last_version_by_subset_id,\n)\nfrom openpype.pipeline import (\n    load,\n    get_current_project_name,\n    get_representation_path,\n)\nfrom openpype.hosts.nuke.api.lib import (\n    find_free_space_to_paste_nodes,\n    maintained_selection,\n    reset_selection,\n    select_nodes,\n    get_avalon_knob_data,\n    set_avalon_knob_data\n)\nfrom openpype.hosts.nuke.api.command import viewer_update_and_undo_stop\nfrom openpype.hosts.nuke.api import containerise, update_container\n\n\nclass LoadBackdropNodes(load.LoaderPlugin):\n    \"\"\"Loading Published Backdrop nodes (workfile, nukenodes)\"\"\"\n\n    families = [\"workfile\", \"nukenodes\", \"matchmove\"]\n    representations = [\"*\"]\n    extensions = {\"nk\"}\n\n    label = \"Import Nuke Nodes\"\n    order = 0\n    icon = \"eye\"\n    color = \"white\"\n    node_color = \"0x7533c1ff\"\n\n    def load(self, context, name, namespace, data):\n        \"\"\"\n        Loading function to import .nk file into script and wrap\n        it on backdrop\n\n        Arguments:\n            context (dict): context of version\n            name (str): name of the version\n            namespace (str): asset name\n            data (dict): compulsory attribute > not used\n\n        Returns:\n            nuke node: containerised nuke node object\n        \"\"\"\n\n        # get main variables\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        vname = version.get(\"name\", None)\n        namespace = namespace or context['asset']['name']\n        colorspace = version_data.get(\"colorspace\", None)\n        object_name = \"{}_{}\".format(name, namespace)\n\n        # prepare data for imprinting\n        # add additional metadata from the version to imprint to Avalon knob\n        add_keys = [\"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"version\": vname,\n            \"colorspaceInput\": colorspace\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # getting file path\n        file = self.filepath_from_context(context).replace(\"\\\\\", \"/\")\n\n        # adding nodes to node graph\n        # just in case we are in group lets jump out of it\n        nuke.endGroup()\n\n        # Get mouse position\n        n = nuke.createNode(\"NoOp\")\n        xcursor, ycursor = (n.xpos(), n.ypos())\n        reset_selection()\n        nuke.delete(n)\n\n        bdn_frame = 50\n\n        with maintained_selection():\n\n            # add group from nk\n            nuke.nodePaste(file)\n\n            # get all pasted nodes\n            new_nodes = list()\n            nodes = nuke.selectedNodes()\n\n            # get pointer position in DAG\n            xpointer, ypointer = find_free_space_to_paste_nodes(\n                nodes, direction=\"right\", offset=200 + bdn_frame\n            )\n\n            # reset position to all nodes and replace inputs and output\n            for n in nodes:\n                reset_selection()\n                xpos = (n.xpos() - xcursor) + xpointer\n                ypos = (n.ypos() - ycursor) + ypointer\n                n.setXYpos(xpos, ypos)\n\n                # replace Input nodes for dots\n                if n.Class() in \"Input\":\n                    dot = nuke.createNode(\"Dot\")\n                    new_name = n.name().replace(\"INP\", \"DOT\")\n                    dot.setName(new_name)\n                    dot[\"label\"].setValue(new_name)\n                    dot.setXYpos(xpos, ypos)\n                    new_nodes.append(dot)\n\n                    # rewire\n                    dep = n.dependent()\n                    for d in dep:\n                        index = next((i for i, dpcy in enumerate(\n                                      d.dependencies())\n                                      if n is dpcy), 0)\n                        d.setInput(index, dot)\n\n                    # remove Input node\n                    reset_selection()\n                    nuke.delete(n)\n                    continue\n\n                # replace Input nodes for dots\n                elif n.Class() in \"Output\":\n                    dot = nuke.createNode(\"Dot\")\n                    new_name = n.name() + \"_DOT\"\n                    dot.setName(new_name)\n                    dot[\"label\"].setValue(new_name)\n                    dot.setXYpos(xpos, ypos)\n                    new_nodes.append(dot)\n\n                    # rewire\n                    dep = next((d for d in n.dependencies()), None)\n                    if dep:\n                        dot.setInput(0, dep)\n\n                    # remove Input node\n                    reset_selection()\n                    nuke.delete(n)\n                    continue\n                else:\n                    new_nodes.append(n)\n\n            # reselect nodes with new Dot instead of Inputs and Output\n            reset_selection()\n            select_nodes(new_nodes)\n            # place on backdrop\n            bdn = nukescripts.autoBackdrop()\n\n            # add frame offset\n            xpos = bdn.xpos() - bdn_frame\n            ypos = bdn.ypos() - bdn_frame\n            bdwidth = bdn[\"bdwidth\"].value() + (bdn_frame*2)\n            bdheight = bdn[\"bdheight\"].value() + (bdn_frame*2)\n\n            bdn[\"xpos\"].setValue(xpos)\n            bdn[\"ypos\"].setValue(ypos)\n            bdn[\"bdwidth\"].setValue(bdwidth)\n            bdn[\"bdheight\"].setValue(bdheight)\n\n            bdn[\"name\"].setValue(object_name)\n            bdn[\"label\"].setValue(\"Version tracked frame: \\n`{}`\\n\\nPLEASE DO NOT REMOVE OR MOVE \\nANYTHING FROM THIS FRAME!\".format(object_name))\n            bdn[\"note_font_size\"].setValue(20)\n\n            return containerise(\n                node=bdn,\n                name=name,\n                namespace=namespace,\n                context=context,\n                loader=self.__class__.__name__,\n                data=data_imprint)\n\n    def update(self, container, representation):\n        \"\"\"Update the Loader's path\n\n        Nuke automatically tries to reset some variables when changing\n        the loader's path to a new file. These automatic changes are to its\n        inputs:\n\n        \"\"\"\n\n        # get main variables\n        # Get version from io\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n\n        # get corresponding node\n        GN = container[\"node\"]\n\n        file = get_representation_path(representation).replace(\"\\\\\", \"/\")\n\n        name = container['name']\n        version_data = version_doc.get(\"data\", {})\n        vname = version_doc.get(\"name\", None)\n        namespace = container['namespace']\n        colorspace = version_data.get(\"colorspace\", None)\n        object_name = \"{}_{}\".format(name, namespace)\n\n        add_keys = [\"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"representation\": str(representation[\"_id\"]),\n            \"version\": vname,\n            \"colorspaceInput\": colorspace,\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # adding nodes to node graph\n        # just in case we are in group lets jump out of it\n        nuke.endGroup()\n\n        with maintained_selection():\n            xpos = GN.xpos()\n            ypos = GN.ypos()\n            avalon_data = get_avalon_knob_data(GN)\n            nuke.delete(GN)\n            # add group from nk\n            nuke.nodePaste(file)\n\n            GN = nuke.selectedNode()\n            set_avalon_knob_data(GN, avalon_data)\n            GN.setXYpos(xpos, ypos)\n            GN[\"name\"].setValue(object_name)\n\n        # get all versions in list\n        last_version_doc = get_last_version_by_subset_id(\n            project_name, version_doc[\"parent\"], fields=[\"_id\"]\n        )\n\n        # change color of node\n        if version_doc[\"_id\"] == last_version_doc[\"_id\"]:\n            color_value = self.node_color\n        else:\n            color_value = \"0xd88467ff\"\n        GN[\"tile_color\"].setValue(int(color_value, 16))\n\n        self.log.info(\"updated to version: {}\".format(version_doc.get(\"name\")))\n\n        return update_container(GN, data_imprint)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        node = container[\"node\"]\n        with viewer_update_and_undo_stop():\n            nuke.delete(node)\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/load_camera_abc.py",
    "content": "import nuke\n\nfrom openpype.client import (\n    get_version_by_id,\n    get_last_version_by_subset_id\n)\nfrom openpype.pipeline import (\n    load,\n    get_current_project_name,\n    get_representation_path,\n)\nfrom openpype.hosts.nuke.api import (\n    containerise,\n    update_container,\n    viewer_update_and_undo_stop\n)\nfrom openpype.hosts.nuke.api.lib import (\n    maintained_selection\n)\n\n\nclass AlembicCameraLoader(load.LoaderPlugin):\n    \"\"\"\n    This will load alembic camera into script.\n    \"\"\"\n\n    families = [\"camera\"]\n    representations = [\"*\"]\n    extensions = {\"abc\"}\n\n    label = \"Load Alembic Camera\"\n    icon = \"camera\"\n    color = \"orange\"\n    node_color = \"0x3469ffff\"\n\n    def load(self, context, name, namespace, data):\n        # get main variables\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        vname = version.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        fps = version_data.get(\"fps\") or nuke.root()[\"fps\"].getValue()\n        namespace = namespace or context['asset']['name']\n        object_name = \"{}_{}\".format(name, namespace)\n\n        # prepare data for imprinting\n        # add additional metadata from the version to imprint to Avalon knob\n        add_keys = [\"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"frameStart\": first,\n            \"frameEnd\": last,\n            \"version\": vname,\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # getting file path\n        file = self.filepath_from_context(context).replace(\"\\\\\", \"/\")\n\n        with maintained_selection():\n            camera_node = nuke.createNode(\n                \"Camera2\",\n                \"name {} file {} read_from_file True\".format(\n                    object_name, file),\n                inpanel=False\n            )\n\n            camera_node.forceValidate()\n            camera_node[\"frame_rate\"].setValue(float(fps))\n\n            # workaround because nuke's bug is not adding\n            # animation keys properly\n            xpos = camera_node.xpos()\n            ypos = camera_node.ypos()\n            nuke.nodeCopy(\"%clipboard%\")\n            nuke.delete(camera_node)\n            nuke.nodePaste(\"%clipboard%\")\n            camera_node = nuke.toNode(object_name)\n            camera_node.setXYpos(xpos, ypos)\n\n        # color node by correct color by actual version\n        self.node_version_color(version, camera_node)\n\n        return containerise(\n            node=camera_node,\n            name=name,\n            namespace=namespace,\n            context=context,\n            loader=self.__class__.__name__,\n            data=data_imprint)\n\n    def update(self, container, representation):\n        \"\"\"\n            Called by Scene Inventory when look should be updated to current\n            version.\n            If any reference edits cannot be applied, eg. shader renamed and\n            material not present, reference is unloaded and cleaned.\n            All failed edits are highlighted to the user via message box.\n\n        Args:\n            container: object that has look to be updated\n            representation: (dict): relationship data to get proper\n                                    representation from DB and persisted\n                                    data in .json\n        Returns:\n            None\n        \"\"\"\n        # Get version from io\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n\n        # get main variables\n        version_data = version_doc.get(\"data\", {})\n        vname = version_doc.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        fps = version_data.get(\"fps\") or nuke.root()[\"fps\"].getValue()\n\n        # prepare data for imprinting\n        # add additional metadata from the version to imprint to Avalon knob\n        add_keys = [\"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"representation\": str(representation[\"_id\"]),\n            \"frameStart\": first,\n            \"frameEnd\": last,\n            \"version\": vname\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # getting file path\n        file = get_representation_path(representation).replace(\"\\\\\", \"/\")\n\n        with maintained_selection():\n            camera_node = container[\"node\"]\n            camera_node['selected'].setValue(True)\n\n            # collect input output dependencies\n            dependencies = camera_node.dependencies()\n            dependent = camera_node.dependent()\n\n            camera_node[\"frame_rate\"].setValue(float(fps))\n            camera_node[\"file\"].setValue(file)\n\n            # workaround because nuke's bug is\n            # not adding animation keys properly\n            xpos = camera_node.xpos()\n            ypos = camera_node.ypos()\n            nuke.nodeCopy(\"%clipboard%\")\n            camera_name = camera_node.name()\n            nuke.delete(camera_node)\n            nuke.nodePaste(\"%clipboard%\")\n            camera_node = nuke.toNode(camera_name)\n            camera_node.setXYpos(xpos, ypos)\n\n            # link to original input nodes\n            for i, input in enumerate(dependencies):\n                camera_node.setInput(i, input)\n            # link to original output nodes\n            for d in dependent:\n                index = next((i for i, dpcy in enumerate(\n                              d.dependencies())\n                              if camera_node is dpcy), 0)\n                d.setInput(index, camera_node)\n\n        # color node by correct color by actual version\n        self.node_version_color(version_doc, camera_node)\n\n        self.log.info(\"updated to version: {}\".format(version_doc.get(\"name\")))\n\n        return update_container(camera_node, data_imprint)\n\n    def node_version_color(self, version_doc, node):\n        \"\"\" Coloring a node by correct color by actual version\n        \"\"\"\n        # get all versions in list\n        project_name = get_current_project_name()\n        last_version_doc = get_last_version_by_subset_id(\n            project_name, version_doc[\"parent\"], fields=[\"_id\"]\n        )\n\n        # change color of node\n        if version_doc[\"_id\"] == last_version_doc[\"_id\"]:\n            color_value = self.node_color\n        else:\n            color_value = \"0xd88467ff\"\n        node[\"tile_color\"].setValue(int(color_value, 16))\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        node = container[\"node\"]\n        with viewer_update_and_undo_stop():\n            nuke.delete(node)\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/load_clip.py",
    "content": "import nuke\nimport qargparse\nfrom pprint import pformat\nfrom copy import deepcopy\nfrom openpype.lib import Logger\nfrom openpype.client import (\n    get_version_by_id,\n    get_last_version_by_subset_id,\n)\nfrom openpype.pipeline import (\n    get_current_project_name,\n    get_representation_path,\n)\nfrom openpype.pipeline.colorspace import (\n    get_imageio_file_rules_colorspace_from_filepath\n)\nfrom openpype.hosts.nuke.api.lib import (\n    get_imageio_input_colorspace,\n    maintained_selection\n)\nfrom openpype.hosts.nuke.api import (\n    containerise,\n    update_container,\n    viewer_update_and_undo_stop,\n    colorspace_exists_on_node\n)\nfrom openpype.lib.transcoding import (\n    VIDEO_EXTENSIONS,\n    IMAGE_EXTENSIONS\n)\nfrom openpype.hosts.nuke.api import plugin\n\n\nclass LoadClip(plugin.NukeLoader):\n    \"\"\"Load clip into Nuke\n\n    Either it is image sequence or video file.\n    \"\"\"\n    log = Logger.get_logger(__name__)\n\n    families = [\n        \"source\",\n        \"plate\",\n        \"render\",\n        \"prerender\",\n        \"review\"\n    ]\n    representations = [\"*\"]\n    extensions = set(\n        ext.lstrip(\".\") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)\n    )\n\n    label = \"Load Clip\"\n    order = -20\n    icon = \"file-video-o\"\n    color = \"white\"\n\n    # Loaded from settings\n    _representations = []\n\n    script_start = int(nuke.root()[\"first_frame\"].value())\n\n    # option gui\n    options_defaults = {\n        \"start_at_workfile\": True,\n        \"add_retime\": True\n    }\n\n    node_name_template = \"{class_name}_{ext}\"\n\n    @classmethod\n    def get_options(cls, *args):\n        return [\n            qargparse.Boolean(\n                \"start_at_workfile\",\n                help=\"Load at workfile start frame\",\n                default=cls.options_defaults[\"start_at_workfile\"]\n            ),\n            qargparse.Boolean(\n                \"add_retime\",\n                help=\"Load with retime\",\n                default=cls.options_defaults[\"add_retime\"]\n            )\n        ]\n\n    @classmethod\n    def get_representations(cls):\n        return cls._representations or cls.representations\n\n    def load(self, context, name, namespace, options):\n        \"\"\"Load asset via database\n        \"\"\"\n        representation = context[\"representation\"]\n        # reset container id so it is always unique for each instance\n        self.reset_container_id()\n\n        is_sequence = len(representation[\"files\"]) > 1\n\n        if is_sequence:\n            context[\"representation\"] = \\\n                self._representation_with_hash_in_frame(\n                    representation\n            )\n\n        filepath = self.filepath_from_context(context)\n        filepath = filepath.replace(\"\\\\\", \"/\")\n\n        start_at_workfile = options.get(\n            \"start_at_workfile\", self.options_defaults[\"start_at_workfile\"])\n\n        add_retime = options.get(\n            \"add_retime\", self.options_defaults[\"add_retime\"])\n\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        repre_id = representation[\"_id\"]\n\n        self.log.debug(\"_ version_data: {}\\n\".format(\n            pformat(version_data)))\n        self.log.debug(\n            \"Representation id `{}` \".format(repre_id))\n\n        self.handle_start = version_data.get(\"handleStart\", 0)\n        self.handle_end = version_data.get(\"handleEnd\", 0)\n\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        first -= self.handle_start\n        last += self.handle_end\n\n        if not is_sequence:\n            duration = last - first\n            first = 1\n            last = first + duration\n\n        # Fallback to asset name when namespace is None\n        if namespace is None:\n            namespace = context['asset']['name']\n\n        if not filepath:\n            self.log.warning(\n                \"Representation id `{}` is failing to load\".format(repre_id))\n            return\n\n        read_name = self._get_node_name(representation)\n\n        # Create the Loader with the filename path set\n        read_node = nuke.createNode(\n            \"Read\",\n            \"name {}\".format(read_name),\n            inpanel=False\n        )\n\n        # to avoid multiple undo steps for rest of process\n        # we will switch off undo-ing\n        with viewer_update_and_undo_stop():\n            read_node[\"file\"].setValue(filepath)\n\n            self.set_colorspace_to_node(\n                read_node, filepath, version, representation)\n\n            self._set_range_to_node(read_node, first, last, start_at_workfile)\n\n            # add additional metadata from the version to imprint Avalon knob\n            add_keys = [\"frameStart\", \"frameEnd\",\n                        \"source\", \"colorspace\", \"author\", \"fps\", \"version\",\n                        \"handleStart\", \"handleEnd\"]\n\n            data_imprint = {}\n            for key in add_keys:\n                if key == 'version':\n                    version_doc = context[\"version\"]\n                    if version_doc[\"type\"] == \"hero_version\":\n                        version = \"hero\"\n                    else:\n                        version = version_doc.get(\"name\")\n\n                    if version:\n                        data_imprint[key] = version\n\n                elif key == 'colorspace':\n                    colorspace = representation[\"data\"].get(key)\n                    colorspace = colorspace or version_data.get(key)\n                    data_imprint[\"db_colorspace\"] = colorspace\n                else:\n                    value_ = context[\"version\"]['data'].get(\n                        key, str(None))\n                    if isinstance(value_, (str)):\n                        value_ = value_.replace(\"\\\\\", \"/\")\n                    data_imprint[key] = value_\n\n            if add_retime and version_data.get(\"retime\", None):\n                data_imprint[\"addRetime\"] = True\n\n            read_node[\"tile_color\"].setValue(int(\"0x4ecd25ff\", 16))\n\n            container = containerise(\n                read_node,\n                name=name,\n                namespace=namespace,\n                context=context,\n                loader=self.__class__.__name__,\n                data=data_imprint)\n\n        if add_retime and version_data.get(\"retime\", None):\n            self._make_retimes(read_node, version_data)\n\n        self.set_as_member(read_node)\n\n        return container\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def _representation_with_hash_in_frame(self, representation):\n        \"\"\"Convert frame key value to padded hash\n\n        Args:\n            representation (dict): representation data\n\n        Returns:\n            dict: altered representation data\n        \"\"\"\n        representation = deepcopy(representation)\n        context = representation[\"context\"]\n\n        # Get the frame from the context and hash it\n        frame = context[\"frame\"]\n        hashed_frame = \"#\" * len(str(frame))\n\n        # Replace the frame with the hash in the originalBasename\n        if (\n            \"{originalBasename}\" in representation[\"data\"][\"template\"]\n        ):\n            origin_basename = context[\"originalBasename\"]\n            context[\"originalBasename\"] = origin_basename.replace(\n                frame, hashed_frame\n            )\n\n        # Replace the frame with the hash in the frame\n        representation[\"context\"][\"frame\"] = hashed_frame\n        return representation\n\n    def update(self, container, representation):\n        \"\"\"Update the Loader's path\n\n        Nuke automatically tries to reset some variables when changing\n        the loader's path to a new file. These automatic changes are to its\n        inputs:\n\n        \"\"\"\n\n        is_sequence = len(representation[\"files\"]) > 1\n\n        read_node = container[\"node\"]\n\n        if is_sequence:\n            representation = self._representation_with_hash_in_frame(\n                representation\n            )\n\n        filepath = get_representation_path(representation).replace(\"\\\\\", \"/\")\n        self.log.debug(\"_ filepath: {}\".format(filepath))\n\n        start_at_workfile = \"start at\" in read_node['frame_mode'].value()\n\n        add_retime = [\n            key for key in read_node.knobs().keys()\n            if \"addRetime\" in key\n        ]\n\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n\n        version_data = version_doc.get(\"data\", {})\n        repre_id = representation[\"_id\"]\n\n        # colorspace profile\n        colorspace = representation[\"data\"].get(\"colorspace\")\n        colorspace = colorspace or version_data.get(\"colorspace\")\n\n        self.handle_start = version_data.get(\"handleStart\", 0)\n        self.handle_end = version_data.get(\"handleEnd\", 0)\n\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        first -= self.handle_start\n        last += self.handle_end\n\n        if not is_sequence:\n            duration = last - first\n            first = 1\n            last = first + duration\n\n        if not filepath:\n            self.log.warning(\n                \"Representation id `{}` is failing to load\".format(repre_id))\n            return\n\n        read_node[\"file\"].setValue(filepath)\n\n        # to avoid multiple undo steps for rest of process\n        # we will switch off undo-ing\n        with viewer_update_and_undo_stop():\n            self.set_colorspace_to_node(\n                read_node, filepath, version_doc, representation)\n\n            self._set_range_to_node(read_node, first, last, start_at_workfile)\n\n            updated_dict = {\n                \"representation\": str(representation[\"_id\"]),\n                \"frameStart\": str(first),\n                \"frameEnd\": str(last),\n                \"version\": str(version_doc.get(\"name\")),\n                \"db_colorspace\": colorspace,\n                \"source\": version_data.get(\"source\"),\n                \"handleStart\": str(self.handle_start),\n                \"handleEnd\": str(self.handle_end),\n                \"fps\": str(version_data.get(\"fps\")),\n                \"author\": version_data.get(\"author\")\n            }\n\n            last_version_doc = get_last_version_by_subset_id(\n                project_name, version_doc[\"parent\"], fields=[\"_id\"]\n            )\n            # change color of read_node\n            if version_doc[\"_id\"] == last_version_doc[\"_id\"]:\n                color_value = \"0x4ecd25ff\"\n            else:\n                color_value = \"0xd84f20ff\"\n            read_node[\"tile_color\"].setValue(int(color_value, 16))\n\n            # Update the imprinted representation\n            update_container(\n                read_node,\n                updated_dict\n            )\n            self.log.info(\n                \"updated to version: {}\".format(version_doc.get(\"name\"))\n            )\n\n        if add_retime and version_data.get(\"retime\", None):\n            self._make_retimes(read_node, version_data)\n        else:\n            self.clear_members(read_node)\n\n        self.set_as_member(read_node)\n\n    def set_colorspace_to_node(\n            self,\n            read_node,\n            filepath,\n            version_doc,\n            representation_doc,\n    ):\n        \"\"\"Set colorspace to read node.\n\n        Sets colorspace with available names validation.\n\n        Args:\n            read_node (nuke.Node): The nuke's read node\n            filepath (str): file path\n            version_doc (dict): version document\n            representation_doc (dict): representation document\n\n        \"\"\"\n        used_colorspace = self._get_colorspace_data(\n            version_doc, representation_doc, filepath)\n\n        if (\n            used_colorspace\n            and colorspace_exists_on_node(read_node, used_colorspace)\n        ):\n            self.log.info(f\"Used colorspace: {used_colorspace}\")\n            read_node[\"colorspace\"].setValue(used_colorspace)\n        else:\n            self.log.info(\"Colorspace not set...\")\n\n    def remove(self, container):\n        read_node = container[\"node\"]\n        assert read_node.Class() == \"Read\", \"Must be Read\"\n\n        with viewer_update_and_undo_stop():\n            members = self.get_members(read_node)\n            nuke.delete(read_node)\n            for member in members:\n                nuke.delete(member)\n\n    def _set_range_to_node(self, read_node, first, last, start_at_workfile):\n        read_node['origfirst'].setValue(int(first))\n        read_node['first'].setValue(int(first))\n        read_node['origlast'].setValue(int(last))\n        read_node['last'].setValue(int(last))\n\n        # set start frame depending on workfile or version\n        self._loader_shift(read_node, start_at_workfile)\n\n    def _make_retimes(self, parent_node, version_data):\n        ''' Create all retime and timewarping nodes with copied animation '''\n        speed = version_data.get('speed', 1)\n        time_warp_nodes = version_data.get('timewarps', [])\n        last_node = None\n        source_id = self.get_container_id(parent_node)\n        self.log.debug(\"__ source_id: {}\".format(source_id))\n        self.log.debug(\"__ members: {}\".format(\n            self.get_members(parent_node)))\n\n        dependent_nodes = self.clear_members(parent_node)\n\n        with maintained_selection():\n            parent_node['selected'].setValue(True)\n\n            if speed != 1:\n                rtn = nuke.createNode(\n                    \"Retime\",\n                    \"speed {}\".format(speed))\n\n                rtn[\"before\"].setValue(\"continue\")\n                rtn[\"after\"].setValue(\"continue\")\n                rtn[\"input.first_lock\"].setValue(True)\n                rtn[\"input.first\"].setValue(\n                    self.script_start\n                )\n                self.set_as_member(rtn)\n                last_node = rtn\n\n            if time_warp_nodes != []:\n                start_anim = self.script_start + (self.handle_start / speed)\n                for timewarp in time_warp_nodes:\n                    twn = nuke.createNode(\n                        timewarp[\"Class\"],\n                        \"name {}\".format(timewarp[\"name\"])\n                    )\n                    if isinstance(timewarp[\"lookup\"], list):\n                        # if array for animation\n                        twn[\"lookup\"].setAnimated()\n                        for i, value in enumerate(timewarp[\"lookup\"]):\n                            twn[\"lookup\"].setValueAt(\n                                (start_anim + i) + value,\n                                (start_anim + i))\n                    else:\n                        # if static value `int`\n                        twn[\"lookup\"].setValue(timewarp[\"lookup\"])\n\n                    self.set_as_member(twn)\n                    last_node = twn\n\n            if dependent_nodes:\n                # connect to original inputs\n                for i, n in enumerate(dependent_nodes):\n                    last_node.setInput(i, n)\n\n    def _loader_shift(self, read_node, workfile_start=False):\n        \"\"\" Set start frame of read node to a workfile start\n\n        Args:\n            read_node (nuke.Node): The nuke's read node\n            workfile_start (bool): set workfile start frame if true\n\n        \"\"\"\n        if workfile_start:\n            read_node['frame_mode'].setValue(\"start at\")\n            read_node['frame'].setValue(str(self.script_start))\n\n    def _get_node_name(self, representation):\n\n        repre_cont = representation[\"context\"]\n        name_data = {\n            \"asset\": repre_cont[\"asset\"],\n            \"subset\": repre_cont[\"subset\"],\n            \"representation\": representation[\"name\"],\n            \"ext\": repre_cont[\"representation\"],\n            \"id\": representation[\"_id\"],\n            \"class_name\": self.__class__.__name__\n        }\n\n        return self.node_name_template.format(**name_data)\n\n    def _get_colorspace_data(self, version_doc, representation_doc, filepath):\n        \"\"\"Get colorspace data from version and representation documents\n\n        Args:\n            version_doc (dict): version document\n            representation_doc (dict): representation document\n            filepath (str): file path\n\n        Returns:\n            Any[str,None]: colorspace name or None\n        \"\"\"\n        # Get backward compatible colorspace key.\n        colorspace = representation_doc[\"data\"].get(\"colorspace\")\n        self.log.debug(\n            f\"Colorspace from representation colorspace: {colorspace}\"\n        )\n\n        # Get backward compatible version data key if colorspace is not found.\n        colorspace = colorspace or version_doc[\"data\"].get(\"colorspace\")\n        self.log.debug(f\"Colorspace from version colorspace: {colorspace}\")\n\n        # Get colorspace from representation colorspaceData if colorspace is\n        # not found.\n        colorspace_data = representation_doc[\"data\"].get(\"colorspaceData\", {})\n        colorspace = colorspace or colorspace_data.get(\"colorspace\")\n        self.log.debug(\n            f\"Colorspace from representation colorspaceData: {colorspace}\"\n        )\n\n        print(f\"Colorspace found: {colorspace}\")\n\n        # check if any filerules are not applicable\n        new_parsed_colorspace = get_imageio_file_rules_colorspace_from_filepath( # noqa\n            filepath, \"nuke\", get_current_project_name()\n        )\n        self.log.debug(f\"Colorspace new filerules: {new_parsed_colorspace}\")\n\n        # colorspace from `project_settings/nuke/imageio/regexInputs`\n        old_parsed_colorspace = get_imageio_input_colorspace(filepath)\n        self.log.debug(f\"Colorspace old filerules: {old_parsed_colorspace}\")\n\n        return (\n            new_parsed_colorspace\n            or old_parsed_colorspace\n            or colorspace\n        )\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/load_effects.py",
    "content": "import json\nfrom collections import OrderedDict\nimport nuke\nimport six\n\nfrom openpype.client import (\n    get_version_by_id,\n    get_last_version_by_subset_id,\n)\nfrom openpype.pipeline import (\n    load,\n    get_current_project_name,\n    get_representation_path,\n)\nfrom openpype.hosts.nuke.api import (\n    containerise,\n    update_container,\n    viewer_update_and_undo_stop\n)\n\n\nclass LoadEffects(load.LoaderPlugin):\n    \"\"\"Loading colorspace soft effect exported from nukestudio\"\"\"\n\n    families = [\"effect\"]\n    representations = [\"*\"]\n    extensions = {\"json\"}\n\n    label = \"Load Effects - nodes\"\n    order = 0\n    icon = \"cc\"\n    color = \"white\"\n    ignore_attr = [\"useLifetime\"]\n\n\n    def load(self, context, name, namespace, data):\n        \"\"\"\n        Loading function to get the soft effects to particular read node\n\n        Arguments:\n            context (dict): context of version\n            name (str): name of the version\n            namespace (str): asset name\n            data (dict): compulsory attribute > not used\n\n        Returns:\n            nuke node: containerised nuke node object\n        \"\"\"\n        # get main variables\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        vname = version.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        workfile_first_frame = int(nuke.root()[\"first_frame\"].getValue())\n        namespace = namespace or context['asset']['name']\n        colorspace = version_data.get(\"colorspace\", None)\n        object_name = \"{}_{}\".format(name, namespace)\n\n        # prepare data for imprinting\n        # add additional metadata from the version to imprint to Avalon knob\n        add_keys = [\"frameStart\", \"frameEnd\", \"handleStart\", \"handleEnd\",\n                    \"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"frameStart\": first,\n            \"frameEnd\": last,\n            \"version\": vname,\n            \"colorspaceInput\": colorspace,\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # getting file path\n        file = self.filepath_from_context(context).replace(\"\\\\\", \"/\")\n\n        # getting data from json file with unicode conversion\n        with open(file, \"r\") as f:\n            json_f = {self.byteify(key): self.byteify(value)\n                      for key, value in json.load(f).items()}\n\n        # get correct order of nodes by positions on track and subtrack\n        nodes_order = self.reorder_nodes(json_f)\n\n        # adding nodes to node graph\n        # just in case we are in group lets jump out of it\n        nuke.endGroup()\n\n        GN = nuke.createNode(\n            \"Group\",\n            \"name {}_1\".format(object_name),\n            inpanel=False\n        )\n\n        # adding content to the group node\n        with GN:\n            pre_node = nuke.createNode(\"Input\")\n            pre_node[\"name\"].setValue(\"rgb\")\n\n            for ef_name, ef_val in nodes_order.items():\n                node = nuke.createNode(ef_val[\"class\"])\n                for k, v in ef_val[\"node\"].items():\n                    if k in self.ignore_attr:\n                        continue\n\n                    try:\n                        node[k].value()\n                    except NameError as e:\n                        self.log.warning(e)\n                        continue\n\n                    if isinstance(v, list) and len(v) > 4:\n                        node[k].setAnimated()\n                        for i, value in enumerate(v):\n                            if isinstance(value, list):\n                                for ci, cv in enumerate(value):\n                                    node[k].setValueAt(\n                                        cv,\n                                        (workfile_first_frame + i),\n                                        ci)\n                            else:\n                                node[k].setValueAt(\n                                    value,\n                                    (workfile_first_frame + i))\n                    else:\n                        node[k].setValue(v)\n                node.setInput(0, pre_node)\n                pre_node = node\n\n            output = nuke.createNode(\"Output\")\n            output.setInput(0, pre_node)\n\n        # try to find parent read node\n        self.connect_read_node(GN, namespace, json_f[\"assignTo\"])\n\n        GN[\"tile_color\"].setValue(int(\"0x3469ffff\", 16))\n\n        self.log.info(\"Loaded lut setup: `{}`\".format(GN[\"name\"].value()))\n\n        return containerise(\n            node=GN,\n            name=name,\n            namespace=namespace,\n            context=context,\n            loader=self.__class__.__name__,\n            data=data_imprint)\n\n    def update(self, container, representation):\n        \"\"\"Update the Loader's path\n\n        Nuke automatically tries to reset some variables when changing\n        the loader's path to a new file. These automatic changes are to its\n        inputs:\n\n        \"\"\"\n        # get main variables\n        # Get version from io\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n\n        # get corresponding node\n        GN = container[\"node\"]\n\n        file = get_representation_path(representation).replace(\"\\\\\", \"/\")\n        name = container['name']\n        version_data = version_doc.get(\"data\", {})\n        vname = version_doc.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        workfile_first_frame = int(nuke.root()[\"first_frame\"].getValue())\n        namespace = container['namespace']\n        colorspace = version_data.get(\"colorspace\", None)\n        object_name = \"{}_{}\".format(name, namespace)\n\n        add_keys = [\"frameStart\", \"frameEnd\", \"handleStart\", \"handleEnd\",\n                    \"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"representation\": str(representation[\"_id\"]),\n            \"frameStart\": first,\n            \"frameEnd\": last,\n            \"version\": vname,\n            \"colorspaceInput\": colorspace\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # Update the imprinted representation\n        update_container(\n            GN,\n            data_imprint\n        )\n\n        # getting data from json file with unicode conversion\n        with open(file, \"r\") as f:\n            json_f = {self.byteify(key): self.byteify(value)\n                      for key, value in json.load(f).items()}\n\n        # get correct order of nodes by positions on track and subtrack\n        nodes_order = self.reorder_nodes(json_f)\n\n        # adding nodes to node graph\n        # just in case we are in group lets jump out of it\n        nuke.endGroup()\n\n        # adding content to the group node\n        with GN:\n            # first remove all nodes\n            [nuke.delete(n) for n in nuke.allNodes()]\n\n            # create input node\n            pre_node = nuke.createNode(\"Input\")\n            pre_node[\"name\"].setValue(\"rgb\")\n\n            for _, ef_val in nodes_order.items():\n                node = nuke.createNode(ef_val[\"class\"])\n                for k, v in ef_val[\"node\"].items():\n                    if k in self.ignore_attr:\n                        continue\n\n                    try:\n                        node[k].value()\n                    except NameError as e:\n                        self.log.warning(e)\n                        continue\n\n                    if isinstance(v, list) and len(v) > 4:\n                        node[k].setAnimated()\n                        for i, value in enumerate(v):\n                            if isinstance(value, list):\n                                for ci, cv in enumerate(value):\n                                    node[k].setValueAt(\n                                        cv,\n                                        (workfile_first_frame + i),\n                                        ci)\n                            else:\n                                node[k].setValueAt(\n                                    value,\n                                    (workfile_first_frame + i))\n                    else:\n                        node[k].setValue(v)\n                node.setInput(0, pre_node)\n                pre_node = node\n\n            # create output node\n            output = nuke.createNode(\"Output\")\n            output.setInput(0, pre_node)\n\n        # try to find parent read node\n        self.connect_read_node(GN, namespace, json_f[\"assignTo\"])\n\n        last_version_doc = get_last_version_by_subset_id(\n            project_name, version_doc[\"parent\"], fields=[\"_id\"]\n        )\n\n        # change color of node\n        if version_doc[\"_id\"] == last_version_doc[\"_id\"]:\n            color_value = \"0x3469ffff\"\n        else:\n            color_value = \"0xd84f20ff\"\n\n        GN[\"tile_color\"].setValue(int(color_value, 16))\n\n        self.log.info(\"updated to version: {}\".format(version_doc.get(\"name\")))\n\n    def connect_read_node(self, group_node, asset, subset):\n        \"\"\"\n        Finds read node and selects it\n\n        Arguments:\n            asset (str): asset name\n\n        Returns:\n            nuke node: node is selected\n            None: if nothing found\n        \"\"\"\n        search_name = \"{0}_{1}\".format(asset, subset)\n\n        node = [\n            n for n in nuke.allNodes(filter=\"Read\")\n            if search_name in n[\"file\"].value()\n        ]\n        if len(node) > 0:\n            rn = node[0]\n        else:\n            rn = None\n\n        # Parent read node has been found\n        # solving connections\n        if rn:\n            dep_nodes = rn.dependent()\n\n            if len(dep_nodes) > 0:\n                for dn in dep_nodes:\n                    dn.setInput(0, group_node)\n\n            group_node.setInput(0, rn)\n            group_node.autoplace()\n\n    def reorder_nodes(self, data):\n        new_order = OrderedDict()\n        trackNums = [v[\"trackIndex\"] for k, v in data.items()\n                     if isinstance(v, dict)]\n        subTrackNums = [v[\"subTrackIndex\"] for k, v in data.items()\n                        if isinstance(v, dict)]\n\n        for trackIndex in range(\n                min(trackNums), max(trackNums) + 1):\n            for subTrackIndex in range(\n                    min(subTrackNums), max(subTrackNums) + 1):\n                item = self.get_item(data, trackIndex, subTrackIndex)\n                if item is not {}:\n                    new_order.update(item)\n        return new_order\n\n    def get_item(self, data, trackIndex, subTrackIndex):\n        return {key: val for key, val in data.items()\n                if isinstance(val, dict)\n                if subTrackIndex == val[\"subTrackIndex\"]\n                if trackIndex == val[\"trackIndex\"]}\n\n    def byteify(self, input):\n        \"\"\"\n        Converts unicode strings to strings\n        It goes through all dictionary\n\n        Arguments:\n            input (dict/str): input\n\n        Returns:\n            dict: with fixed values and keys\n\n        \"\"\"\n\n        if isinstance(input, dict):\n            return {self.byteify(key): self.byteify(value)\n                    for key, value in input.items()}\n        elif isinstance(input, list):\n            return [self.byteify(element) for element in input]\n        elif isinstance(input, six.text_type):\n            return str(input)\n        else:\n            return input\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        node = container[\"node\"]\n        with viewer_update_and_undo_stop():\n            nuke.delete(node)\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/load_effects_ip.py",
    "content": "import json\nfrom collections import OrderedDict\nimport six\nimport nuke\n\nfrom openpype.client import (\n    get_version_by_id,\n    get_last_version_by_subset_id,\n)\nfrom openpype.pipeline import (\n    load,\n    get_current_project_name,\n    get_representation_path,\n)\nfrom openpype.hosts.nuke.api import lib\nfrom openpype.hosts.nuke.api import (\n    containerise,\n    update_container,\n    viewer_update_and_undo_stop\n)\n\n\nclass LoadEffectsInputProcess(load.LoaderPlugin):\n    \"\"\"Loading colorspace soft effect exported from nukestudio\"\"\"\n\n    families = [\"effect\"]\n    representations = [\"*\"]\n    extensions = {\"json\"}\n\n    label = \"Load Effects - Input Process\"\n    order = 0\n    icon = \"eye\"\n    color = \"#cc0000\"\n    ignore_attr = [\"useLifetime\"]\n\n    def load(self, context, name, namespace, data):\n        \"\"\"\n        Loading function to get the soft effects to particular read node\n\n        Arguments:\n            context (dict): context of version\n            name (str): name of the version\n            namespace (str): asset name\n            data (dict): compulsory attribute > not used\n\n        Returns:\n            nuke node: containerised nuke node object\n        \"\"\"\n\n        # get main variables\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        vname = version.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        workfile_first_frame = int(nuke.root()[\"first_frame\"].getValue())\n        namespace = namespace or context['asset']['name']\n        colorspace = version_data.get(\"colorspace\", None)\n        object_name = \"{}_{}\".format(name, namespace)\n\n        # prepare data for imprinting\n        # add additional metadata from the version to imprint to Avalon knob\n        add_keys = [\"frameStart\", \"frameEnd\", \"handleStart\", \"handleEnd\",\n                    \"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"frameStart\": first,\n            \"frameEnd\": last,\n            \"version\": vname,\n            \"colorspaceInput\": colorspace,\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # getting file path\n        file = self.filepath_from_context(context).replace(\"\\\\\", \"/\")\n\n        # getting data from json file with unicode conversion\n        with open(file, \"r\") as f:\n            json_f = {self.byteify(key): self.byteify(value)\n                      for key, value in json.load(f).items()}\n\n        # get correct order of nodes by positions on track and subtrack\n        nodes_order = self.reorder_nodes(json_f)\n\n        # adding nodes to node graph\n        # just in case we are in group lets jump out of it\n        nuke.endGroup()\n\n        GN = nuke.createNode(\n            \"Group\",\n            \"name {}_1\".format(object_name),\n            inpanel=False\n        )\n\n        # adding content to the group node\n        with GN:\n            pre_node = nuke.createNode(\"Input\")\n            pre_node[\"name\"].setValue(\"rgb\")\n\n            for _, ef_val in nodes_order.items():\n                node = nuke.createNode(ef_val[\"class\"])\n                for k, v in ef_val[\"node\"].items():\n                    if k in self.ignore_attr:\n                        continue\n\n                    try:\n                        node[k].value()\n                    except NameError as e:\n                        self.log.warning(e)\n                        continue\n\n                    if isinstance(v, list) and len(v) > 4:\n                        node[k].setAnimated()\n                        for i, value in enumerate(v):\n                            if isinstance(value, list):\n                                for ci, cv in enumerate(value):\n                                    node[k].setValueAt(\n                                        cv,\n                                        (workfile_first_frame + i),\n                                        ci)\n                            else:\n                                node[k].setValueAt(\n                                    value,\n                                    (workfile_first_frame + i))\n                    else:\n                        node[k].setValue(v)\n\n                node.setInput(0, pre_node)\n                pre_node = node\n\n            output = nuke.createNode(\"Output\")\n            output.setInput(0, pre_node)\n\n        # try to place it under Viewer1\n        if not self.connect_active_viewer(GN):\n            nuke.delete(GN)\n            return\n\n        GN[\"tile_color\"].setValue(int(\"0x3469ffff\", 16))\n\n        self.log.info(\"Loaded lut setup: `{}`\".format(GN[\"name\"].value()))\n\n        return containerise(\n            node=GN,\n            name=name,\n            namespace=namespace,\n            context=context,\n            loader=self.__class__.__name__,\n            data=data_imprint)\n\n    def update(self, container, representation):\n        \"\"\"Update the Loader's path\n\n        Nuke automatically tries to reset some variables when changing\n        the loader's path to a new file. These automatic changes are to its\n        inputs:\n\n        \"\"\"\n\n        # get main variables\n        # Get version from io\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n\n        # get corresponding node\n        GN = container[\"node\"]\n\n        file = get_representation_path(representation).replace(\"\\\\\", \"/\")\n        version_data = version_doc.get(\"data\", {})\n        vname = version_doc.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        workfile_first_frame = int(nuke.root()[\"first_frame\"].getValue())\n        colorspace = version_data.get(\"colorspace\", None)\n\n        add_keys = [\"frameStart\", \"frameEnd\", \"handleStart\", \"handleEnd\",\n                    \"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"representation\": str(representation[\"_id\"]),\n            \"frameStart\": first,\n            \"frameEnd\": last,\n            \"version\": vname,\n            \"colorspaceInput\": colorspace,\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # Update the imprinted representation\n        update_container(\n            GN,\n            data_imprint\n        )\n\n        # getting data from json file with unicode conversion\n        with open(file, \"r\") as f:\n            json_f = {self.byteify(key): self.byteify(value)\n                      for key, value in json.load(f).items()}\n\n        # get correct order of nodes by positions on track and subtrack\n        nodes_order = self.reorder_nodes(json_f)\n\n        # adding nodes to node graph\n        # just in case we are in group lets jump out of it\n        nuke.endGroup()\n\n        # adding content to the group node\n        with GN:\n            # first remove all nodes\n            [nuke.delete(n) for n in nuke.allNodes()]\n\n            # create input node\n            pre_node = nuke.createNode(\"Input\")\n            pre_node[\"name\"].setValue(\"rgb\")\n\n            for _, ef_val in nodes_order.items():\n                node = nuke.createNode(ef_val[\"class\"])\n                for k, v in ef_val[\"node\"].items():\n                    if k in self.ignore_attr:\n                        continue\n\n                    try:\n                        node[k].value()\n                    except NameError as e:\n                        self.log.warning(e)\n                        continue\n\n                    if isinstance(v, list) and len(v) > 4:\n                        node[k].setAnimated()\n                        for i, value in enumerate(v):\n                            if isinstance(value, list):\n                                for ci, cv in enumerate(value):\n                                    node[k].setValueAt(\n                                        cv,\n                                        (workfile_first_frame + i),\n                                        ci)\n                            else:\n                                node[k].setValueAt(\n                                    value,\n                                    (workfile_first_frame + i))\n                    else:\n                        node[k].setValue(v)\n                node.setInput(0, pre_node)\n                pre_node = node\n\n            # create output node\n            output = nuke.createNode(\"Output\")\n            output.setInput(0, pre_node)\n\n        # get all versions in list\n        last_version_doc = get_last_version_by_subset_id(\n            project_name, version_doc[\"parent\"], fields=[\"_id\"]\n        )\n\n        # change color of node\n        if version_doc[\"_id\"] == last_version_doc[\"_id\"]:\n            color_value = \"0x3469ffff\"\n        else:\n            color_value = \"0xd84f20ff\"\n        GN[\"tile_color\"].setValue(int(color_value, 16))\n\n        self.log.info(\"updated to version: {}\".format(version_doc.get(\"name\")))\n\n    def connect_active_viewer(self, group_node):\n        \"\"\"\n        Finds Active viewer and\n        place the node under it, also adds\n        name of group into Input Process of the viewer\n\n        Arguments:\n            group_node (nuke node): nuke group node object\n\n        \"\"\"\n        group_node_name = group_node[\"name\"].value()\n\n        viewer = [n for n in nuke.allNodes() if \"Viewer1\" in n[\"name\"].value()]\n        if len(viewer) > 0:\n            viewer = viewer[0]\n        else:\n            msg = str(\"Please create Viewer node before you \"\n                      \"run this action again\")\n            self.log.error(msg)\n            nuke.message(msg)\n            return None\n\n        # get coordinates of Viewer1\n        xpos = viewer[\"xpos\"].value()\n        ypos = viewer[\"ypos\"].value()\n\n        ypos += 150\n\n        viewer[\"ypos\"].setValue(ypos)\n\n        # set coordinates to group node\n        group_node[\"xpos\"].setValue(xpos)\n        group_node[\"ypos\"].setValue(ypos + 50)\n\n        # add group node name to Viewer Input Process\n        viewer[\"input_process_node\"].setValue(group_node_name)\n\n        # put backdrop under\n        lib.create_backdrop(\n            label=\"Input Process\",\n            layer=2,\n            nodes=[viewer, group_node],\n            color=\"0x7c7faaff\")\n\n        return True\n\n    def reorder_nodes(self, data):\n        new_order = OrderedDict()\n        trackNums = [v[\"trackIndex\"] for k, v in data.items()\n                     if isinstance(v, dict)]\n        subTrackNums = [v[\"subTrackIndex\"] for k, v in data.items()\n                        if isinstance(v, dict)]\n\n        for trackIndex in range(\n                min(trackNums), max(trackNums) + 1):\n            for subTrackIndex in range(\n                    min(subTrackNums), max(subTrackNums) + 1):\n                item = self.get_item(data, trackIndex, subTrackIndex)\n                if item is not {}:\n                    new_order.update(item)\n        return new_order\n\n    def get_item(self, data, trackIndex, subTrackIndex):\n        return {key: val for key, val in data.items()\n                if isinstance(val, dict)\n                if subTrackIndex == val[\"subTrackIndex\"]\n                if trackIndex == val[\"trackIndex\"]}\n\n    def byteify(self, input):\n        \"\"\"\n        Converts unicode strings to strings\n        It goes through all dictionary\n\n        Arguments:\n            input (dict/str): input\n\n        Returns:\n            dict: with fixed values and keys\n\n        \"\"\"\n\n        if isinstance(input, dict):\n            return {self.byteify(key): self.byteify(value)\n                    for key, value in input.items()}\n        elif isinstance(input, list):\n            return [self.byteify(element) for element in input]\n        elif isinstance(input, six.text_type):\n            return str(input)\n        else:\n            return input\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        node = container[\"node\"]\n        with viewer_update_and_undo_stop():\n            nuke.delete(node)\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/load_gizmo.py",
    "content": "import nuke\n\nfrom openpype.client import (\n    get_version_by_id,\n    get_last_version_by_subset_id,\n)\nfrom openpype.pipeline import (\n    load,\n    get_current_project_name,\n    get_representation_path,\n)\nfrom openpype.hosts.nuke.api.lib import (\n    maintained_selection,\n    get_avalon_knob_data,\n    set_avalon_knob_data,\n    swap_node_with_dependency,\n)\nfrom openpype.hosts.nuke.api import (\n    containerise,\n    update_container,\n    viewer_update_and_undo_stop\n)\n\n\nclass LoadGizmo(load.LoaderPlugin):\n    \"\"\"Loading nuke Gizmo\"\"\"\n\n    families = [\"gizmo\", \"lensDistortion\"]\n    representations = [\"*\"]\n    extensions = {\"nk\"}\n\n    label = \"Load Gizmo\"\n    order = 0\n    icon = \"dropbox\"\n    color = \"white\"\n    node_color = \"0x75338eff\"\n\n    def load(self, context, name, namespace, data):\n        \"\"\"\n        Loading function to get Gizmo into node graph\n\n        Arguments:\n            context (dict): context of version\n            name (str): name of the version\n            namespace (str): asset name\n            data (dict): compulsory attribute > not used\n\n        Returns:\n            nuke node: containerized nuke node object\n        \"\"\"\n\n        # get main variables\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        vname = version.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        namespace = namespace or context['asset']['name']\n        colorspace = version_data.get(\"colorspace\", None)\n        object_name = \"{}_{}\".format(name, namespace)\n\n        # prepare data for imprinting\n        # add additional metadata from the version to imprint to Avalon knob\n        add_keys = [\"frameStart\", \"frameEnd\", \"handleStart\", \"handleEnd\",\n                    \"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"frameStart\": first,\n            \"frameEnd\": last,\n            \"version\": vname,\n            \"colorspaceInput\": colorspace\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # getting file path\n        file = self.filepath_from_context(context).replace(\"\\\\\", \"/\")\n\n        # adding nodes to node graph\n        # just in case we are in group lets jump out of it\n        nuke.endGroup()\n\n        with maintained_selection():\n            # add group from nk\n            nuke.nodePaste(file)\n\n            group_node = nuke.selectedNode()\n\n            group_node[\"name\"].setValue(object_name)\n\n            return containerise(\n                node=group_node,\n                name=name,\n                namespace=namespace,\n                context=context,\n                loader=self.__class__.__name__,\n                data=data_imprint)\n\n    def update(self, container, representation):\n        \"\"\"Update the Loader's path\n\n        Nuke automatically tries to reset some variables when changing\n        the loader's path to a new file. These automatic changes are to its\n        inputs:\n\n        \"\"\"\n\n        # get main variables\n        # Get version from io\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n\n        # get corresponding node\n        group_node = container[\"node\"]\n\n        file = get_representation_path(representation).replace(\"\\\\\", \"/\")\n        name = container['name']\n        version_data = version_doc.get(\"data\", {})\n        vname = version_doc.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        namespace = container['namespace']\n        colorspace = version_data.get(\"colorspace\", None)\n        object_name = \"{}_{}\".format(name, namespace)\n\n        add_keys = [\"frameStart\", \"frameEnd\", \"handleStart\", \"handleEnd\",\n                    \"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"representation\": str(representation[\"_id\"]),\n            \"frameStart\": first,\n            \"frameEnd\": last,\n            \"version\": vname,\n            \"colorspaceInput\": colorspace\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # capture pipeline metadata\n        avalon_data = get_avalon_knob_data(group_node)\n\n        # adding nodes to node graph\n        # just in case we are in group lets jump out of it\n        nuke.endGroup()\n\n        with maintained_selection([group_node]):\n            # insert nuke script to the script\n            nuke.nodePaste(file)\n            # convert imported to selected node\n            new_group_node = nuke.selectedNode()\n            # swap nodes with maintained connections\n            with swap_node_with_dependency(\n                    group_node, new_group_node) as node_name:\n                new_group_node[\"name\"].setValue(node_name)\n                # set updated pipeline metadata\n                set_avalon_knob_data(new_group_node, avalon_data)\n\n        last_version_doc = get_last_version_by_subset_id(\n            project_name, version_doc[\"parent\"], fields=[\"_id\"]\n        )\n\n        # change color of node\n        if version_doc[\"_id\"] == last_version_doc[\"_id\"]:\n            color_value = self.node_color\n        else:\n            color_value = \"0xd88467ff\"\n\n        new_group_node[\"tile_color\"].setValue(int(color_value, 16))\n\n        self.log.info(\"updated to version: {}\".format(version_doc.get(\"name\")))\n\n        return update_container(new_group_node, data_imprint)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        node = container[\"node\"]\n        with viewer_update_and_undo_stop():\n            nuke.delete(node)\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/load_gizmo_ip.py",
    "content": "import nuke\nimport six\n\nfrom openpype.client import (\n    get_version_by_id,\n    get_last_version_by_subset_id,\n)\nfrom openpype.pipeline import (\n    load,\n    get_current_project_name,\n    get_representation_path,\n)\nfrom openpype.hosts.nuke.api.lib import (\n    maintained_selection,\n    create_backdrop,\n    get_avalon_knob_data,\n    set_avalon_knob_data,\n    swap_node_with_dependency,\n)\nfrom openpype.hosts.nuke.api import (\n    containerise,\n    update_container,\n    viewer_update_and_undo_stop\n)\n\n\nclass LoadGizmoInputProcess(load.LoaderPlugin):\n    \"\"\"Loading colorspace soft effect exported from nukestudio\"\"\"\n\n    families = [\"gizmo\"]\n    representations = [\"*\"]\n    extensions = {\"nk\"}\n\n    label = \"Load Gizmo - Input Process\"\n    order = 0\n    icon = \"eye\"\n    color = \"#cc0000\"\n    node_color = \"0x7533c1ff\"\n\n    def load(self, context, name, namespace, data):\n        \"\"\"\n        Loading function to get Gizmo as Input Process on viewer\n\n        Arguments:\n            context (dict): context of version\n            name (str): name of the version\n            namespace (str): asset name\n            data (dict): compulsory attribute > not used\n\n        Returns:\n            nuke node: containerized nuke node object\n        \"\"\"\n\n        # get main variables\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        vname = version.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        namespace = namespace or context['asset']['name']\n        colorspace = version_data.get(\"colorspace\", None)\n        object_name = \"{}_{}\".format(name, namespace)\n\n        # prepare data for imprinting\n        # add additional metadata from the version to imprint to Avalon knob\n        add_keys = [\"frameStart\", \"frameEnd\", \"handleStart\", \"handleEnd\",\n                    \"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"frameStart\": first,\n            \"frameEnd\": last,\n            \"version\": vname,\n            \"colorspaceInput\": colorspace\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # getting file path\n        file = self.filepath_from_context(context).replace(\"\\\\\", \"/\")\n\n        # adding nodes to node graph\n        # just in case we are in group lets jump out of it\n        nuke.endGroup()\n\n        with maintained_selection():\n            # add group from nk\n            nuke.nodePaste(file)\n\n            group_node = nuke.selectedNode()\n\n            group_node[\"name\"].setValue(object_name)\n\n            # try to place it under Viewer1\n            if not self.connect_active_viewer(group_node):\n                nuke.delete(group_node)\n                return\n\n            return containerise(\n                node=group_node,\n                name=name,\n                namespace=namespace,\n                context=context,\n                loader=self.__class__.__name__,\n                data=data_imprint)\n\n    def update(self, container, representation):\n        \"\"\"Update the Loader's path\n\n        Nuke automatically tries to reset some variables when changing\n        the loader's path to a new file. These automatic changes are to its\n        inputs:\n\n        \"\"\"\n\n        # get main variables\n        # Get version from io\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n\n        # get corresponding node\n        group_node = container[\"node\"]\n\n        file = get_representation_path(representation).replace(\"\\\\\", \"/\")\n        name = container['name']\n        version_data = version_doc.get(\"data\", {})\n        vname = version_doc.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        namespace = container['namespace']\n        colorspace = version_data.get(\"colorspace\", None)\n        object_name = \"{}_{}\".format(name, namespace)\n\n        add_keys = [\"frameStart\", \"frameEnd\", \"handleStart\", \"handleEnd\",\n                    \"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"representation\": str(representation[\"_id\"]),\n            \"frameStart\": first,\n            \"frameEnd\": last,\n            \"version\": vname,\n            \"colorspaceInput\": colorspace\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # capture pipeline metadata\n        avalon_data = get_avalon_knob_data(group_node)\n\n        # adding nodes to node graph\n        # just in case we are in group lets jump out of it\n        nuke.endGroup()\n\n        with maintained_selection([group_node]):\n            # insert nuke script to the script\n            nuke.nodePaste(file)\n            # convert imported to selected node\n            new_group_node = nuke.selectedNode()\n            # swap nodes with maintained connections\n            with swap_node_with_dependency(\n                    group_node, new_group_node) as node_name:\n                new_group_node[\"name\"].setValue(node_name)\n                # set updated pipeline metadata\n                set_avalon_knob_data(new_group_node, avalon_data)\n\n        last_version_doc = get_last_version_by_subset_id(\n            project_name, version_doc[\"parent\"], fields=[\"_id\"]\n        )\n\n        # change color of node\n        if version_doc[\"_id\"] == last_version_doc[\"_id\"]:\n            color_value = self.node_color\n        else:\n            color_value = \"0xd88467ff\"\n        new_group_node[\"tile_color\"].setValue(int(color_value, 16))\n\n        self.log.info(\"updated to version: {}\".format(version_doc.get(\"name\")))\n\n        return update_container(new_group_node, data_imprint)\n\n    def connect_active_viewer(self, group_node):\n        \"\"\"\n        Finds Active viewer and\n        place the node under it, also adds\n        name of group into Input Process of the viewer\n\n        Arguments:\n            group_node (nuke node): nuke group node object\n\n        \"\"\"\n        group_node_name = group_node[\"name\"].value()\n\n        viewer = [n for n in nuke.allNodes() if \"Viewer1\" in n[\"name\"].value()]\n        if len(viewer) > 0:\n            viewer = viewer[0]\n        else:\n            msg = str(\"Please create Viewer node before you \"\n                      \"run this action again\")\n            self.log.error(msg)\n            nuke.message(msg)\n            return None\n\n        # get coordinates of Viewer1\n        xpos = viewer[\"xpos\"].value()\n        ypos = viewer[\"ypos\"].value()\n\n        ypos += 150\n\n        viewer[\"ypos\"].setValue(ypos)\n\n        # set coordinates to group node\n        group_node[\"xpos\"].setValue(xpos)\n        group_node[\"ypos\"].setValue(ypos + 50)\n\n        # add group node name to Viewer Input Process\n        viewer[\"input_process_node\"].setValue(group_node_name)\n\n        # put backdrop under\n        create_backdrop(\n            label=\"Input Process\",\n            layer=2,\n            nodes=[viewer, group_node],\n            color=\"0x7c7faaff\"\n        )\n\n        return True\n\n    def get_item(self, data, trackIndex, subTrackIndex):\n        return {key: val for key, val in data.items()\n                if subTrackIndex == val[\"subTrackIndex\"]\n                if trackIndex == val[\"trackIndex\"]}\n\n    def byteify(self, input):\n        \"\"\"\n        Converts unicode strings to strings\n        It goes through all dictionary\n\n        Arguments:\n            input (dict/str): input\n\n        Returns:\n            dict: with fixed values and keys\n\n        \"\"\"\n\n        if isinstance(input, dict):\n            return {self.byteify(key): self.byteify(value)\n                    for key, value in input.items()}\n        elif isinstance(input, list):\n            return [self.byteify(element) for element in input]\n        elif isinstance(input, six.text_type):\n            return str(input)\n        else:\n            return input\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        node = container[\"node\"]\n        with viewer_update_and_undo_stop():\n            nuke.delete(node)\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/load_image.py",
    "content": "import nuke\n\nimport qargparse\n\nfrom openpype.client import (\n    get_version_by_id,\n    get_last_version_by_subset_id,\n)\nfrom openpype.pipeline import (\n    load,\n    get_current_project_name,\n    get_representation_path,\n)\nfrom openpype.hosts.nuke.api.lib import (\n    get_imageio_input_colorspace\n)\nfrom openpype.hosts.nuke.api import (\n    containerise,\n    update_container,\n    viewer_update_and_undo_stop\n)\nfrom openpype.lib.transcoding import (\n    IMAGE_EXTENSIONS\n)\n\n\nclass LoadImage(load.LoaderPlugin):\n    \"\"\"Load still image into Nuke\"\"\"\n\n    families = [\n        \"render2d\",\n        \"source\",\n        \"plate\",\n        \"render\",\n        \"prerender\",\n        \"review\",\n        \"image\"\n    ]\n    representations = [\"*\"]\n    extensions = set(\n        ext.lstrip(\".\") for ext in IMAGE_EXTENSIONS\n    )\n\n    label = \"Load Image\"\n    order = -10\n    icon = \"image\"\n    color = \"white\"\n\n    # Loaded from settings\n    _representations = []\n\n    node_name_template = \"{class_name}_{ext}\"\n\n    options = [\n        qargparse.Integer(\n            \"frame_number\",\n            label=\"Frame Number\",\n            default=int(nuke.root()[\"first_frame\"].getValue()),\n            min=1,\n            max=999999,\n            help=\"What frame is reading from?\"\n        )\n    ]\n\n    @classmethod\n    def get_representations(cls):\n        return cls._representations or cls.representations\n\n    def load(self, context, name, namespace, options):\n        self.log.info(\"__ options: `{}`\".format(options))\n        frame_number = options.get(\n            \"frame_number\", int(nuke.root()[\"first_frame\"].getValue())\n        )\n\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        repr_id = context[\"representation\"][\"_id\"]\n\n        self.log.info(\"version_data: {}\\n\".format(version_data))\n        self.log.debug(\n            \"Representation id `{}` \".format(repr_id))\n\n        last = first = int(frame_number)\n\n        # Fallback to asset name when namespace is None\n        if namespace is None:\n            namespace = context['asset']['name']\n\n        file = self.filepath_from_context(context)\n\n        if not file:\n            repr_id = context[\"representation\"][\"_id\"]\n            self.log.warning(\n                \"Representation id `{}` is failing to load\".format(repr_id))\n            return\n\n        file = file.replace(\"\\\\\", \"/\")\n\n        representation = context[\"representation\"]\n        repr_cont = representation[\"context\"]\n        frame = repr_cont.get(\"frame\")\n        if frame:\n            padding = len(frame)\n            file = file.replace(\n                frame,\n                format(frame_number, \"0{}\".format(padding)))\n\n        read_name = self._get_node_name(representation)\n\n        # Create the Loader with the filename path set\n        with viewer_update_and_undo_stop():\n            r = nuke.createNode(\n                \"Read\",\n                \"name {}\".format(read_name),\n                inpanel=False\n            )\n\n            r[\"file\"].setValue(file)\n\n            # Set colorspace defined in version data\n            colorspace = context[\"version\"][\"data\"].get(\"colorspace\")\n            if colorspace:\n                r[\"colorspace\"].setValue(str(colorspace))\n\n            preset_clrsp = get_imageio_input_colorspace(file)\n\n            if preset_clrsp is not None:\n                r[\"colorspace\"].setValue(preset_clrsp)\n\n            r[\"origfirst\"].setValue(first)\n            r[\"first\"].setValue(first)\n            r[\"origlast\"].setValue(last)\n            r[\"last\"].setValue(last)\n\n            # add additional metadata from the version to imprint Avalon knob\n            add_keys = [\"source\", \"colorspace\", \"author\", \"fps\", \"version\"]\n\n            data_imprint = {\n                \"frameStart\": first,\n                \"frameEnd\": last\n            }\n            for k in add_keys:\n                if k == 'version':\n                    data_imprint.update({k: context[\"version\"]['name']})\n                else:\n                    data_imprint.update(\n                        {k: context[\"version\"]['data'].get(k, str(None))})\n\n            r[\"tile_color\"].setValue(int(\"0x4ecd25ff\", 16))\n\n            return containerise(r,\n                                name=name,\n                                namespace=namespace,\n                                context=context,\n                                loader=self.__class__.__name__,\n                                data=data_imprint)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n        \"\"\"Update the Loader's path\n\n        Nuke automatically tries to reset some variables when changing\n        the loader's path to a new file. These automatic changes are to its\n        inputs:\n\n        \"\"\"\n        node = container[\"node\"]\n        frame_number = node[\"first\"].value()\n\n        assert node.Class() == \"Read\", \"Must be Read\"\n\n        repr_cont = representation[\"context\"]\n\n        file = get_representation_path(representation)\n\n        if not file:\n            repr_id = representation[\"_id\"]\n            self.log.warning(\n                \"Representation id `{}` is failing to load\".format(repr_id))\n            return\n\n        file = file.replace(\"\\\\\", \"/\")\n\n        frame = repr_cont.get(\"frame\")\n        if frame:\n            padding = len(frame)\n            file = file.replace(\n                frame,\n                format(frame_number, \"0{}\".format(padding)))\n\n        # Get start frame from version data\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n        last_version_doc = get_last_version_by_subset_id(\n            project_name, version_doc[\"parent\"], fields=[\"_id\"]\n        )\n\n        version_data = version_doc.get(\"data\", {})\n\n        last = first = int(frame_number)\n\n        # Set the global in to the start frame of the sequence\n        node[\"file\"].setValue(file)\n        node[\"origfirst\"].setValue(first)\n        node[\"first\"].setValue(first)\n        node[\"origlast\"].setValue(last)\n        node[\"last\"].setValue(last)\n\n        updated_dict = {}\n        updated_dict.update({\n            \"representation\": str(representation[\"_id\"]),\n            \"frameStart\": str(first),\n            \"frameEnd\": str(last),\n            \"version\": str(version_doc.get(\"name\")),\n            \"colorspace\": version_data.get(\"colorspace\"),\n            \"source\": version_data.get(\"source\"),\n            \"fps\": str(version_data.get(\"fps\")),\n            \"author\": version_data.get(\"author\")\n        })\n\n        # change color of node\n        if version_doc[\"_id\"] == last_version_doc[\"_id\"]:\n            color_value = \"0x4ecd25ff\"\n        else:\n            color_value = \"0xd84f20ff\"\n        node[\"tile_color\"].setValue(int(color_value, 16))\n\n        # Update the imprinted representation\n        update_container(\n            node,\n            updated_dict\n        )\n        self.log.info(\"updated to version: {}\".format(version_doc.get(\"name\")))\n\n    def remove(self, container):\n        node = container[\"node\"]\n        assert node.Class() == \"Read\", \"Must be Read\"\n\n        with viewer_update_and_undo_stop():\n            nuke.delete(node)\n\n    def _get_node_name(self, representation):\n\n        repre_cont = representation[\"context\"]\n        name_data = {\n            \"asset\": repre_cont[\"asset\"],\n            \"subset\": repre_cont[\"subset\"],\n            \"representation\": representation[\"name\"],\n            \"ext\": repre_cont[\"representation\"],\n            \"id\": representation[\"_id\"],\n            \"class_name\": self.__class__.__name__\n        }\n\n        return self.node_name_template.format(**name_data)\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/load_matchmove.py",
    "content": "import nuke\nfrom openpype.pipeline import load\n\n\nclass MatchmoveLoader(load.LoaderPlugin):\n    \"\"\"\n    This will run matchmove script to create track in script.\n    \"\"\"\n\n    families = [\"matchmove\"]\n    representations = [\"*\"]\n    extensions = {\"py\"}\n\n    defaults = [\"Camera\", \"Object\"]\n\n    label = \"Run matchmove script\"\n    icon = \"empire\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data):\n        path = self.filepath_from_context(context)\n        if path.lower().endswith(\".py\"):\n            exec(open(path).read())\n\n        else:\n            msg = \"Unsupported script type\"\n            self.log.error(msg)\n            nuke.message(msg)\n\n        return True\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/load_model.py",
    "content": "import nuke\n\nfrom openpype.client import (\n    get_version_by_id,\n    get_last_version_by_subset_id,\n)\nfrom openpype.pipeline import (\n    load,\n    get_current_project_name,\n    get_representation_path,\n)\nfrom openpype.hosts.nuke.api.lib import maintained_selection\nfrom openpype.hosts.nuke.api import (\n    containerise,\n    update_container,\n    viewer_update_and_undo_stop\n)\n\n\nclass AlembicModelLoader(load.LoaderPlugin):\n    \"\"\"\n    This will load alembic model or anim into script.\n    \"\"\"\n\n    families = [\"model\", \"pointcache\", \"animation\"]\n    representations = [\"*\"]\n    extensions = {\"abc\"}\n\n    label = \"Load Alembic\"\n    icon = \"cube\"\n    color = \"orange\"\n    node_color = \"0x4ecd91ff\"\n\n    def load(self, context, name, namespace, data):\n        # get main variables\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        vname = version.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        fps = version_data.get(\"fps\") or nuke.root()[\"fps\"].getValue()\n        namespace = namespace or context['asset']['name']\n        object_name = \"{}_{}\".format(name, namespace)\n\n        # prepare data for imprinting\n        # add additional metadata from the version to imprint to Avalon knob\n        add_keys = [\"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"frameStart\": first,\n            \"frameEnd\": last,\n            \"version\": vname\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # getting file path\n        file = self.filepath_from_context(context).replace(\"\\\\\", \"/\")\n\n        with maintained_selection():\n            model_node = nuke.createNode(\n                \"ReadGeo2\",\n                \"name {} file {} \".format(\n                    object_name, file),\n                inpanel=False\n            )\n\n            model_node.forceValidate()\n\n            # Ensure all items are imported and selected.\n            scene_view = model_node.knob('scene_view')\n            scene_view.setImportedItems(scene_view.getAllItems())\n            scene_view.setSelectedItems(scene_view.getAllItems())\n\n            model_node[\"frame_rate\"].setValue(float(fps))\n\n            # workaround because nuke's bug is not adding\n            # animation keys properly\n            xpos = model_node.xpos()\n            ypos = model_node.ypos()\n            nuke.nodeCopy(\"%clipboard%\")\n            nuke.delete(model_node)\n            nuke.nodePaste(\"%clipboard%\")\n            model_node = nuke.toNode(object_name)\n            model_node.setXYpos(xpos, ypos)\n\n        # color node by correct color by actual version\n        self.node_version_color(version, model_node)\n\n        return containerise(\n            node=model_node,\n            name=name,\n            namespace=namespace,\n            context=context,\n            loader=self.__class__.__name__,\n            data=data_imprint)\n\n    def update(self, container, representation):\n        \"\"\"\n            Called by Scene Inventory when look should be updated to current\n            version.\n            If any reference edits cannot be applied, eg. shader renamed and\n            material not present, reference is unloaded and cleaned.\n            All failed edits are highlighted to the user via message box.\n\n        Args:\n            container: object that has look to be updated\n            representation: (dict): relationship data to get proper\n                                    representation from DB and persisted\n                                    data in .json\n        Returns:\n            None\n        \"\"\"\n        # Get version from io\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n\n        # get corresponding node\n        model_node = container[\"node\"]\n\n        # get main variables\n        version_data = version_doc.get(\"data\", {})\n        vname = version_doc.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n        fps = version_data.get(\"fps\") or nuke.root()[\"fps\"].getValue()\n\n        # prepare data for imprinting\n        # add additional metadata from the version to imprint to Avalon knob\n        add_keys = [\"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n            \"representation\": str(representation[\"_id\"]),\n            \"frameStart\": first,\n            \"frameEnd\": last,\n            \"version\": vname\n        }\n\n        for k in add_keys:\n            data_imprint.update({k: version_data[k]})\n\n        # getting file path\n        file = get_representation_path(representation).replace(\"\\\\\", \"/\")\n\n        with maintained_selection():\n            model_node['selected'].setValue(True)\n\n            # collect input output dependencies\n            dependencies = model_node.dependencies()\n            dependent = model_node.dependent()\n\n            model_node[\"frame_rate\"].setValue(float(fps))\n            model_node[\"file\"].setValue(file)\n\n            # Ensure all items are imported and selected.\n            scene_view = model_node.knob('scene_view')\n            scene_view.setImportedItems(scene_view.getAllItems())\n            scene_view.setSelectedItems(scene_view.getAllItems())\n\n            # workaround because nuke's bug is\n            # not adding animation keys properly\n            xpos = model_node.xpos()\n            ypos = model_node.ypos()\n            nuke.nodeCopy(\"%clipboard%\")\n            nuke.delete(model_node)\n\n            # paste the node back and set the position\n            nuke.nodePaste(\"%clipboard%\")\n            model_node = nuke.selectedNode()\n            model_node.setXYpos(xpos, ypos)\n\n            # link to original input nodes\n            for i, input in enumerate(dependencies):\n                model_node.setInput(i, input)\n            # link to original output nodes\n            for d in dependent:\n                index = next((i for i, dpcy in enumerate(\n                              d.dependencies())\n                              if model_node is dpcy), 0)\n                d.setInput(index, model_node)\n\n        # color node by correct color by actual version\n        self.node_version_color(version_doc, model_node)\n\n        self.log.info(\"updated to version: {}\".format(version_doc.get(\"name\")))\n\n        return update_container(model_node, data_imprint)\n\n    def node_version_color(self, version, node):\n        \"\"\" Coloring a node by correct color by actual version\"\"\"\n\n        project_name = get_current_project_name()\n        last_version_doc = get_last_version_by_subset_id(\n            project_name, version[\"parent\"], fields=[\"_id\"]\n        )\n\n        # change color of node\n        if version[\"_id\"] == last_version_doc[\"_id\"]:\n            color_value = self.node_color\n        else:\n            color_value = \"0xd88467ff\"\n        node[\"tile_color\"].setValue(int(color_value, 16))\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        node = nuke.toNode(container['objectName'])\n        with viewer_update_and_undo_stop():\n            nuke.delete(node)\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/load_ociolook.py",
    "content": "import os\nimport json\nimport secrets\nimport nuke\nimport six\n\nfrom openpype.client import (\n    get_version_by_id,\n    get_last_version_by_subset_id\n)\nfrom openpype.pipeline import (\n    load,\n    get_current_project_name,\n    get_representation_path,\n)\nfrom openpype.hosts.nuke.api import (\n    containerise,\n    viewer_update_and_undo_stop,\n    update_container,\n)\n\n\nclass LoadOcioLookNodes(load.LoaderPlugin):\n    \"\"\"Loading Ocio look to the nuke.Node graph\"\"\"\n\n    families = [\"ociolook\"]\n    representations = [\"*\"]\n    extensions = {\"json\"}\n\n    label = \"Load OcioLook [nodes]\"\n    order = 0\n    icon = \"cc\"\n    color = \"white\"\n    ignore_attr = [\"useLifetime\"]\n\n    # plugin attributes\n    current_node_color = \"0x4ecd91ff\"\n    old_node_color = \"0xd88467ff\"\n\n    # json file variables\n    schema_version = 1\n\n    def load(self, context, name, namespace, data):\n        \"\"\"\n        Loading function to get the soft effects to particular read node\n\n        Arguments:\n            context (dict): context of version\n            name (str): name of the version\n            namespace (str): asset name\n            data (dict): compulsory attribute > not used\n\n        Returns:\n            nuke.Node: containerized nuke.Node object\n        \"\"\"\n        namespace = namespace or context['asset']['name']\n        suffix = secrets.token_hex(nbytes=4)\n        node_name = \"{}_{}_{}\".format(\n            name, namespace, suffix)\n\n        # getting file path\n        filepath = self.filepath_from_context(context)\n\n        json_f = self._load_json_data(filepath)\n\n        group_node = self._create_group_node(\n            filepath, json_f[\"data\"])\n        # renaming group node\n        group_node[\"name\"].setValue(node_name)\n\n        self._node_version_color(context[\"version\"], group_node)\n\n        self.log.info(\n            \"Loaded lut setup: `{}`\".format(group_node[\"name\"].value()))\n\n        return containerise(\n            node=group_node,\n            name=name,\n            namespace=namespace,\n            context=context,\n            loader=self.__class__.__name__\n        )\n\n    def _create_group_node(\n        self,\n        filepath,\n        data,\n        group_node=None\n    ):\n        \"\"\"Creates group node with all the nodes inside.\n\n        Creating mainly `OCIOFileTransform` nodes with `OCIOColorSpace` nodes\n        in between - in case those are needed.\n\n        Arguments:\n            filepath (str): path to json file\n            data (dict): data from json file\n            group_node (Optional[nuke.Node]): group node or None\n\n        Returns:\n            nuke.Node: group node with all the nodes inside\n        \"\"\"\n        # get corresponding node\n\n        root_working_colorspace = nuke.root()[\"workingSpaceLUT\"].value()\n\n        dir_path = os.path.dirname(filepath)\n        all_files = os.listdir(dir_path)\n\n        ocio_working_colorspace = _colorspace_name_by_type(\n            data[\"ocioLookWorkingSpace\"])\n\n        # adding nodes to node graph\n        # just in case we are in group lets jump out of it\n        nuke.endGroup()\n\n        input_node = None\n        output_node = None\n        if group_node:\n            # remove all nodes between Input and Output nodes\n            for node in group_node.nodes():\n                if node.Class() not in [\"Input\", \"Output\"]:\n                    nuke.delete(node)\n                elif node.Class() == \"Input\":\n                    input_node = node\n                elif node.Class() == \"Output\":\n                    output_node = node\n        else:\n            group_node = nuke.createNode(\n                \"Group\",\n                inpanel=False\n            )\n\n        # adding content to the group node\n        with group_node:\n            pre_colorspace = root_working_colorspace\n\n            # reusing input node if it exists during update\n            if input_node:\n                pre_node = input_node\n            else:\n                pre_node = nuke.createNode(\"Input\")\n                pre_node[\"name\"].setValue(\"rgb\")\n\n            # Compare script working colorspace with ocio working colorspace\n            # found in json file and convert to json's if needed\n            if pre_colorspace != ocio_working_colorspace:\n                pre_node = _add_ocio_colorspace_node(\n                    pre_node,\n                    pre_colorspace,\n                    ocio_working_colorspace\n                )\n                pre_colorspace = ocio_working_colorspace\n\n            for ocio_item in data[\"ocioLookItems\"]:\n                input_space = _colorspace_name_by_type(\n                    ocio_item[\"input_colorspace\"])\n                output_space = _colorspace_name_by_type(\n                    ocio_item[\"output_colorspace\"])\n\n                # making sure we are set to correct colorspace for otio item\n                if pre_colorspace != input_space:\n                    pre_node = _add_ocio_colorspace_node(\n                        pre_node,\n                        pre_colorspace,\n                        input_space\n                    )\n\n                node = nuke.createNode(\"OCIOFileTransform\")\n\n                # file path from lut representation\n                extension = ocio_item[\"ext\"]\n                item_name = ocio_item[\"name\"]\n\n                item_lut_file = next(\n                    (\n                        file for file in all_files\n                        if file.endswith(extension)\n                    ),\n                    None\n                )\n                if not item_lut_file:\n                    raise ValueError(\n                        \"File with extension '{}' not \"\n                        \"found in directory\".format(extension)\n                    )\n\n                item_lut_path = os.path.join(\n                    dir_path, item_lut_file).replace(\"\\\\\", \"/\")\n                node[\"file\"].setValue(item_lut_path)\n                node[\"name\"].setValue(item_name)\n                node[\"direction\"].setValue(ocio_item[\"direction\"])\n                node[\"interpolation\"].setValue(ocio_item[\"interpolation\"])\n                node[\"working_space\"].setValue(input_space)\n\n                pre_node.autoplace()\n                node.setInput(0, pre_node)\n                node.autoplace()\n                # pass output space into pre_colorspace for next iteration\n                # or for output node comparison\n                pre_colorspace = output_space\n                pre_node = node\n\n            # making sure we are back in script working colorspace\n            if pre_colorspace != root_working_colorspace:\n                pre_node = _add_ocio_colorspace_node(\n                    pre_node,\n                    pre_colorspace,\n                    root_working_colorspace\n                )\n\n            # reusing output node if it exists during update\n            if not output_node:\n                output = nuke.createNode(\"Output\")\n            else:\n                output = output_node\n\n            output.setInput(0, pre_node)\n\n        return group_node\n\n    def update(self, container, representation):\n\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n\n        group_node = container[\"node\"]\n\n        filepath = get_representation_path(representation)\n\n        json_f = self._load_json_data(filepath)\n\n        group_node = self._create_group_node(\n            filepath,\n            json_f[\"data\"],\n            group_node\n        )\n\n        self._node_version_color(version_doc, group_node)\n\n        self.log.info(\"Updated lut setup: `{}`\".format(\n            group_node[\"name\"].value()))\n\n        return update_container(\n            group_node, {\"representation\": str(representation[\"_id\"])})\n\n    def _load_json_data(self, filepath):\n        # getting data from json file with unicode conversion\n        with open(filepath, \"r\") as _file:\n            json_f = {self._bytify(key): self._bytify(value)\n                      for key, value in json.load(_file).items()}\n\n        # check if the version in json_f is the same as plugin version\n        if json_f[\"version\"] != self.schema_version:\n            raise KeyError(\n                \"Version of json file is not the same as plugin version\")\n\n        return json_f\n\n    def _bytify(self, input):\n        \"\"\"\n        Converts unicode strings to strings\n        It goes through all dictionary\n\n        Arguments:\n            input (dict/str): input\n\n        Returns:\n            dict: with fixed values and keys\n\n        \"\"\"\n\n        if isinstance(input, dict):\n            return {self._bytify(key): self._bytify(value)\n                    for key, value in input.items()}\n        elif isinstance(input, list):\n            return [self._bytify(element) for element in input]\n        elif isinstance(input, six.text_type):\n            return str(input)\n        else:\n            return input\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def remove(self, container):\n        node = nuke.toNode(container['objectName'])\n        with viewer_update_and_undo_stop():\n            nuke.delete(node)\n\n    def _node_version_color(self, version, node):\n        \"\"\" Coloring a node by correct color by actual version\"\"\"\n\n        project_name = get_current_project_name()\n        last_version_doc = get_last_version_by_subset_id(\n            project_name, version[\"parent\"], fields=[\"_id\"]\n        )\n\n        # change color of node\n        if version[\"_id\"] == last_version_doc[\"_id\"]:\n            color_value = self.current_node_color\n        else:\n            color_value = self.old_node_color\n        node[\"tile_color\"].setValue(int(color_value, 16))\n\n\ndef _colorspace_name_by_type(colorspace_data):\n    \"\"\"\n    Returns colorspace name by type\n\n    Arguments:\n        colorspace_data (dict): colorspace data\n\n    Returns:\n        str: colorspace name\n    \"\"\"\n    if colorspace_data[\"type\"] == \"colorspaces\":\n        return colorspace_data[\"name\"]\n    elif colorspace_data[\"type\"] == \"roles\":\n        return colorspace_data[\"colorspace\"]\n    else:\n        raise KeyError(\"Unknown colorspace type: {}\".format(\n            colorspace_data[\"type\"]))\n\n\ndef _add_ocio_colorspace_node(pre_node, input_space, output_space):\n    \"\"\"\n    Adds OCIOColorSpace node to the node graph\n\n    Arguments:\n        pre_node (nuke.Node): node to connect to\n        input_space (str): input colorspace\n        output_space (str): output colorspace\n\n    Returns:\n        nuke.Node: node with OCIOColorSpace node\n    \"\"\"\n    node = nuke.createNode(\"OCIOColorSpace\")\n    node.setInput(0, pre_node)\n    node[\"in_colorspace\"].setValue(input_space)\n    node[\"out_colorspace\"].setValue(output_space)\n\n    pre_node.autoplace()\n    node.setInput(0, pre_node)\n    node.autoplace()\n\n    return node\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/load/load_script_precomp.py",
    "content": "import nuke\n\nfrom openpype.client import (\n    get_version_by_id,\n    get_last_version_by_subset_id,\n)\nfrom openpype.pipeline import (\n    get_current_project_name,\n    load,\n    get_representation_path,\n)\nfrom openpype.hosts.nuke.api.lib import get_avalon_knob_data\nfrom openpype.hosts.nuke.api import (\n    containerise,\n    update_container,\n    viewer_update_and_undo_stop\n)\n\n\nclass LinkAsGroup(load.LoaderPlugin):\n    \"\"\"Copy the published file to be pasted at the desired location\"\"\"\n\n    families = [\"workfile\", \"nukenodes\"]\n    representations = [\"*\"]\n    extensions = {\"nk\"}\n\n    label = \"Load Precomp\"\n    order = 0\n    icon = \"file\"\n    color = \"#cc0000\"\n\n    def load(self, context, name, namespace, data):\n        # for k, v in context.items():\n        #     log.info(\"key: `{}`, value: {}\\n\".format(k, v))\n        version = context['version']\n        version_data = version.get(\"data\", {})\n\n        vname = version.get(\"name\", None)\n        first = version_data.get(\"frameStart\", None)\n        last = version_data.get(\"frameEnd\", None)\n\n        # Fallback to asset name when namespace is None\n        if namespace is None:\n            namespace = context['asset']['name']\n\n        file = self.filepath_from_context(context).replace(\"\\\\\", \"/\")\n        self.log.info(\"file: {}\\n\".format(file))\n\n        self.log.info(\"versionData: {}\\n\".format(context[\"version\"][\"data\"]))\n\n        # add additional metadata from the version to imprint to Avalon knob\n        add_keys = [\"frameStart\", \"frameEnd\", \"handleStart\", \"handleEnd\",\n                    \"source\", \"author\", \"fps\"]\n\n        data_imprint = {\n                \"startingFrame\": first,\n                \"frameStart\": first,\n                \"frameEnd\": last,\n                \"version\": vname\n        }\n        for k in add_keys:\n            data_imprint.update({k: context[\"version\"]['data'][k]})\n\n        # group context is set to precomp, so back up one level.\n        nuke.endGroup()\n\n        # P = nuke.nodes.LiveGroup(\"file {}\".format(file))\n        P = nuke.createNode(\n            \"Precomp\",\n            \"file {}\".format(file),\n            inpanel=False\n        )\n\n        # Set colorspace defined in version data\n        colorspace = context[\"version\"][\"data\"].get(\"colorspace\", None)\n        self.log.info(\"colorspace: {}\\n\".format(colorspace))\n\n        P[\"name\"].setValue(\"{}_{}\".format(name, namespace))\n        P[\"useOutput\"].setValue(True)\n\n        with P:\n            # iterate through all nodes in group node and find pype writes\n            writes = [n.name() for n in nuke.allNodes()\n                      if n.Class() == \"Group\"\n                      if get_avalon_knob_data(n)]\n\n            if writes:\n                # create panel for selecting output\n                panel_choices = \" \".join(writes)\n                panel_label = \"Select write node for output\"\n                p = nuke.Panel(\"Select Write Node\")\n                p.addEnumerationPulldown(\n                    panel_label, panel_choices)\n                p.show()\n                P[\"output\"].setValue(p.value(panel_label))\n\n        P[\"tile_color\"].setValue(0xff0ff0ff)\n\n        return containerise(\n                     node=P,\n                     name=name,\n                     namespace=namespace,\n                     context=context,\n                     loader=self.__class__.__name__,\n                     data=data_imprint)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n        \"\"\"Update the Loader's path\n\n        Nuke automatically tries to reset some variables when changing\n        the loader's path to a new file. These automatic changes are to its\n        inputs:\n\n        \"\"\"\n        node = container[\"node\"]\n\n        root = get_representation_path(representation).replace(\"\\\\\", \"/\")\n\n        # Get start frame from version data\n        project_name = get_current_project_name()\n        version_doc = get_version_by_id(project_name, representation[\"parent\"])\n        last_version_doc = get_last_version_by_subset_id(\n            project_name, version_doc[\"parent\"], fields=[\"_id\"]\n        )\n\n        updated_dict = {}\n        version_data = version_doc[\"data\"]\n        updated_dict.update({\n            \"representation\": str(representation[\"_id\"]),\n            \"frameEnd\": version_data.get(\"frameEnd\"),\n            \"version\": version_doc.get(\"name\"),\n            \"colorspace\": version_data.get(\"colorspace\"),\n            \"source\": version_data.get(\"source\"),\n            \"fps\": version_data.get(\"fps\"),\n            \"author\": version_data.get(\"author\")\n        })\n\n        # Update the imprinted representation\n        update_container(\n            node,\n            updated_dict\n        )\n\n        node[\"file\"].setValue(root)\n\n        # change color of node\n        if version_doc[\"_id\"] == last_version_doc[\"_id\"]:\n            color_value = \"0xff0ff0ff\"\n        else:\n            color_value = \"0xd84f20ff\"\n        node[\"tile_color\"].setValue(int(color_value, 16))\n\n        self.log.info(\"updated to version: {}\".format(version_doc.get(\"name\")))\n\n    def remove(self, container):\n        node = container[\"node\"]\n        with viewer_update_and_undo_stop():\n            nuke.delete(node)\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/collect_backdrop.py",
    "content": "from pprint import pformat\nimport pyblish.api\nfrom openpype.hosts.nuke.api import lib as pnlib\nimport nuke\n\n\nclass CollectBackdrops(pyblish.api.InstancePlugin):\n    \"\"\"Collect Backdrop node instance and its content\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.22\n    label = \"Collect Backdrop\"\n    hosts = [\"nuke\"]\n    families = [\"nukenodes\"]\n\n    def process(self, instance):\n        self.log.debug(pformat(instance.data))\n\n        bckn = instance.data[\"transientData\"][\"node\"]\n\n        # define size of the backdrop\n        left = bckn.xpos()\n        top = bckn.ypos()\n        right = left + bckn['bdwidth'].value()\n        bottom = top + bckn['bdheight'].value()\n\n        instance.data[\"transientData\"][\"childNodes\"] = []\n        # iterate all nodes\n        for node in nuke.allNodes():\n\n            # exclude viewer\n            if node.Class() == \"Viewer\":\n                continue\n\n            # find all related nodes\n            if (node.xpos() > left) \\\n                and (node.xpos() + node.screenWidth() < right) \\\n                    and (node.ypos() > top) \\\n                    and (node.ypos() + node.screenHeight() < bottom):\n\n                # add contained nodes to instance's node list\n                instance.data[\"transientData\"][\"childNodes\"].append(node)\n\n        # get all connections from outside of backdrop\n        nodes = instance.data[\"transientData\"][\"childNodes\"]\n        connections_in, connections_out = pnlib.get_dependent_nodes(nodes)\n        instance.data[\"transientData\"][\"nodeConnectionsIn\"] = connections_in\n        instance.data[\"transientData\"][\"nodeConnectionsOut\"] = connections_out\n\n        # make label nicer\n        instance.data[\"label\"] = \"{0} ({1} nodes)\".format(\n            bckn.name(), len(instance.data[\"transientData\"][\"childNodes\"]))\n\n        # get version\n        version = instance.context.data.get('version')\n\n        if version:\n            instance.data['version'] = version\n\n        self.log.debug(\"Backdrop instance collected: `{}`\".format(instance))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/collect_context_data.py",
    "content": "import os\nimport nuke\nimport pyblish.api\nfrom openpype.lib import get_version_from_path\nimport openpype.hosts.nuke.api as napi\nfrom openpype.pipeline import KnownPublishError\n\n\nclass CollectContextData(pyblish.api.ContextPlugin):\n    \"\"\"Collect current context publish.\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.499\n    label = \"Collect context data\"\n    hosts = ['nuke']\n\n    def process(self, context):  # sourcery skip: avoid-builtin-shadow\n        root_node = nuke.root()\n\n        current_file = os.path.normpath(root_node.name())\n\n        if current_file.lower() == \"root\":\n            raise KnownPublishError(\n                \"Workfile is not correct file name. \\n\"\n                \"Use workfile tool to manage the name correctly.\"\n            )\n\n        # Get frame range\n        first_frame = int(root_node[\"first_frame\"].getValue())\n        last_frame = int(root_node[\"last_frame\"].getValue())\n\n        # get instance data from root\n        root_instance_context = napi.get_node_data(\n            root_node, napi.INSTANCE_DATA_KNOB\n        )\n\n        handle_start = root_instance_context[\"handleStart\"]\n        handle_end = root_instance_context[\"handleEnd\"]\n\n        # Get format\n        format = root_node['format'].value()\n        resolution_width = format.width()\n        resolution_height = format.height()\n        pixel_aspect = format.pixelAspect()\n\n        script_data = {\n            \"frameStart\": first_frame + handle_start,\n            \"frameEnd\": last_frame - handle_end,\n            \"resolutionWidth\": resolution_width,\n            \"resolutionHeight\": resolution_height,\n            \"pixelAspect\": pixel_aspect,\n\n            \"handleStart\": handle_start,\n            \"handleEnd\": handle_end,\n            \"step\": 1,\n            \"fps\": root_node['fps'].value(),\n\n            \"currentFile\": current_file,\n            \"version\": int(get_version_from_path(current_file)),\n\n            \"host\": pyblish.api.current_host(),\n            \"hostVersion\": nuke.NUKE_VERSION_STRING\n        }\n\n        context.data[\"scriptData\"] = script_data\n        context.data.update(script_data)\n\n        self.log.debug('Context from Nuke script collected')\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/collect_framerate.py",
    "content": "import nuke\n\nimport pyblish.api\n\n\nclass CollectFramerate(pyblish.api.ContextPlugin):\n    \"\"\"Collect framerate.\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = \"Collect Framerate\"\n    hosts = [\n        \"nuke\",\n        \"nukeassist\"\n    ]\n\n    def process(self, context):\n        context.data[\"fps\"] = nuke.root()[\"fps\"].getValue()\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/collect_gizmo.py",
    "content": "import pyblish.api\nimport nuke\n\n\nclass CollectGizmo(pyblish.api.InstancePlugin):\n    \"\"\"Collect Gizmo (group) node instance and its content\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.22\n    label = \"Collect Gizmo (group)\"\n    hosts = [\"nuke\"]\n    families = [\"gizmo\"]\n\n    def process(self, instance):\n\n        gizmo_node = instance.data[\"transientData\"][\"node\"]\n\n        # add family to familiess\n        instance.data[\"families\"].insert(0, instance.data[\"family\"])\n        # make label nicer\n        instance.data[\"label\"] = gizmo_node.name()\n\n        # Get frame range\n        handle_start = instance.context.data[\"handleStart\"]\n        handle_end = instance.context.data[\"handleEnd\"]\n        first_frame = int(nuke.root()[\"first_frame\"].getValue())\n        last_frame = int(nuke.root()[\"last_frame\"].getValue())\n\n        # Add version data to instance\n        version_data = {\n            \"handleStart\": handle_start,\n            \"handleEnd\": handle_end,\n            \"frameStart\": first_frame + handle_start,\n            \"frameEnd\": last_frame - handle_end,\n            \"colorspace\": nuke.root().knob('workingSpaceLUT').value(),\n            \"families\": [instance.data[\"family\"]] + instance.data[\"families\"],\n            \"subset\": instance.data[\"subset\"],\n            \"fps\": instance.context.data[\"fps\"]\n        }\n\n        instance.data.update({\n            \"versionData\": version_data,\n            \"frameStart\": first_frame,\n            \"frameEnd\": last_frame\n        })\n        self.log.debug(\"Gizmo instance collected: `{}`\".format(instance))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/collect_model.py",
    "content": "import pyblish.api\nimport nuke\n\n\nclass CollectModel(pyblish.api.InstancePlugin):\n    \"\"\"Collect Model node instance and its content\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.22\n    label = \"Collect Model\"\n    hosts = [\"nuke\"]\n    families = [\"model\"]\n\n    def process(self, instance):\n\n        geo_node = instance.data[\"transientData\"][\"node\"]\n\n        # add family to familiess\n        instance.data[\"families\"].insert(0, instance.data[\"family\"])\n        # make label nicer\n        instance.data[\"label\"] = geo_node.name()\n\n        # Get frame range\n        handle_start = instance.context.data[\"handleStart\"]\n        handle_end = instance.context.data[\"handleEnd\"]\n        first_frame = int(nuke.root()[\"first_frame\"].getValue())\n        last_frame = int(nuke.root()[\"last_frame\"].getValue())\n\n        # Add version data to instance\n        version_data = {\n            \"handleStart\": handle_start,\n            \"handleEnd\": handle_end,\n            \"frameStart\": first_frame + handle_start,\n            \"frameEnd\": last_frame - handle_end,\n            \"colorspace\": nuke.root().knob('workingSpaceLUT').value(),\n            \"families\": [instance.data[\"family\"]] + instance.data[\"families\"],\n            \"subset\": instance.data[\"subset\"],\n            \"fps\": instance.context.data[\"fps\"]\n        }\n\n        instance.data.update({\n            \"versionData\": version_data,\n            \"frameStart\": first_frame,\n            \"frameEnd\": last_frame\n        })\n        self.log.debug(\"Model instance collected: `{}`\".format(instance))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py",
    "content": "import nuke\nimport pyblish.api\n\n\nclass CollectInstanceData(pyblish.api.InstancePlugin):\n    \"\"\"Collect Nuke instance data\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.49\n    label = \"Collect Nuke Instance Data\"\n    hosts = [\"nuke\", \"nukeassist\"]\n\n    # presets\n    sync_workfile_version_on_families = []\n\n    def process(self, instance):\n        family = instance.data[\"family\"]\n\n        # Get format\n        root = nuke.root()\n        format_ = root['format'].value()\n        resolution_width = format_.width()\n        resolution_height = format_.height()\n        pixel_aspect = format_.pixelAspect()\n\n        # sync workfile version\n        if family in self.sync_workfile_version_on_families:\n            self.log.debug(\n                \"Syncing version with workfile for '{}'\".format(\n                    family\n                )\n            )\n            # get version to instance for integration\n            instance.data['version'] = instance.context.data['version']\n\n        instance.data.update({\n            \"step\": 1,\n            \"fps\": root['fps'].value(),\n            \"resolutionWidth\": resolution_width,\n            \"resolutionHeight\": resolution_height,\n            \"pixelAspect\": pixel_aspect\n\n        })\n\n        # add creator attributes to instance\n        creator_attributes = instance.data[\"creator_attributes\"]\n        instance.data.update(creator_attributes)\n\n        # add review family if review activated on instance\n        if instance.data.get(\"review\"):\n            instance.data[\"families\"].append(\"review\")\n\n        self.log.debug(\"Collected instance: {}\".format(\n            instance.data))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/collect_reads.py",
    "content": "import os\nimport re\nimport nuke\nimport pyblish.api\n\n\nclass CollectNukeReads(pyblish.api.InstancePlugin):\n    \"\"\"Collect all read nodes.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.04\n    label = \"Collect Source Reads\"\n    hosts = [\"nuke\", \"nukeassist\"]\n    families = [\"source\"]\n\n    def process(self, instance):\n        self.log.debug(\"checking instance: {}\".format(instance))\n\n        node = instance.data[\"transientData\"][\"node\"]\n        if node.Class() != \"Read\":\n            return\n\n        file_path = node[\"file\"].value()\n        file_name = os.path.basename(file_path)\n        items = file_name.split(\".\")\n\n        if len(items) < 2:\n            raise ValueError\n\n        ext = items[-1]\n\n        # Get frame range\n        handle_start = instance.context.data[\"handleStart\"]\n        handle_end = instance.context.data[\"handleEnd\"]\n        first_frame = node['first'].value()\n        last_frame = node['last'].value()\n\n        # colorspace\n        colorspace = node[\"colorspace\"].value()\n        if \"default\" in colorspace:\n            colorspace = colorspace.replace(\"default (\", \"\").replace(\")\", \"\")\n\n        # # Easier way to sequence - Not tested\n        # isSequence = True\n        # if first_frame == last_frame:\n        #     isSequence = False\n\n        isSequence = False\n        if len(items) > 1:\n            sequence = items[-2]\n            hash_regex = re.compile(r'([#*])')\n            seq_regex = re.compile(r'[%0-9*d]')\n            hash_match = re.match(hash_regex, sequence)\n            seq_match = re.match(seq_regex, sequence)\n            if hash_match or seq_match:\n                isSequence = True\n\n        # get source path\n        path = nuke.filename(node)\n        source_dir = os.path.dirname(path)\n        self.log.debug('source dir: {}'.format(source_dir))\n\n        if isSequence:\n            source_files = [f for f in os.listdir(source_dir)\n                            if ext in f\n                            if items[0] in f]\n        else:\n            source_files = file_name\n\n        # Include start and end render frame in label\n        name = node.name()\n        label = \"{0} ({1}-{2})\".format(\n            name,\n            int(first_frame),\n            int(last_frame)\n        )\n\n        self.log.debug(\"collected_frames: {}\".format(label))\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': ext,\n            'ext': ext,\n            'files': source_files,\n            \"stagingDir\": source_dir,\n            \"frameStart\": \"%0{}d\".format(\n                len(str(last_frame))) % first_frame\n        }\n        instance.data[\"representations\"].append(representation)\n\n        transfer = node[\"publish\"] if \"publish\" in node.knobs() else False\n        instance.data['transfer'] = transfer\n\n        # Add version data to instance\n        version_data = {\n            \"handleStart\": handle_start,\n            \"handleEnd\": handle_end,\n            \"frameStart\": first_frame + handle_start,\n            \"frameEnd\": last_frame - handle_end,\n            \"colorspace\": colorspace,\n            \"families\": [instance.data[\"family\"]],\n            \"subset\": instance.data[\"subset\"],\n            \"fps\": instance.context.data[\"fps\"]\n        }\n\n        instance.data.update({\n            \"versionData\": version_data,\n            \"path\": path,\n            \"stagingDir\": source_dir,\n            \"ext\": ext,\n            \"label\": label,\n            \"frameStart\": first_frame,\n            \"frameEnd\": last_frame,\n            \"colorspace\": colorspace,\n            \"handleStart\": handle_start,\n            \"handleEnd\": handle_end,\n            \"step\": 1,\n            \"fps\": int(nuke.root()['fps'].value())\n        })\n\n        self.log.debug(\"instance.data: {}\".format(instance.data))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/collect_slate_node.py",
    "content": "import pyblish.api\nimport nuke\n\n\nclass CollectSlate(pyblish.api.InstancePlugin):\n    \"\"\"Check if SLATE node is in scene and connected to rendering tree\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.002\n    label = \"Collect Slate Node\"\n    hosts = [\"nuke\"]\n    families = [\"render\"]\n\n    def process(self, instance):\n        node = instance.data[\"transientData\"][\"node\"]\n\n        slate = next(\n            (\n                n_ for n_ in nuke.allNodes()\n                if \"slate\" in n_.name().lower()\n                if not n_[\"disable\"].getValue() and\n                \"publish_instance\" not in n_.knobs()  # Exclude instance nodes.\n            ),\n            None\n        )\n\n        if slate:\n            # check if slate node is connected to write node tree\n            slate_check = 0\n            slate_node = None\n            while slate_check == 0:\n                try:\n                    node = node.dependencies()[0]\n                    if slate.name() in node.name():\n                        slate_node = node\n                        slate_check = 1\n                except IndexError:\n                    break\n\n            if slate_node:\n                instance.data[\"slateNode\"] = slate_node\n                instance.data[\"slate\"] = True\n                instance.data[\"families\"].append(\"slate\")\n                self.log.debug(\n                    \"Slate node is in node graph: `{}`\".format(slate.name()))\n                self.log.debug(\n                    \"__ instance.data: `{}`\".format(instance.data))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/collect_workfile.py",
    "content": "import os\nimport nuke\nimport pyblish.api\n\n\nclass CollectWorkfile(pyblish.api.InstancePlugin):\n    \"\"\"Collect current script for publish.\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = \"Collect Workfile\"\n    hosts = ['nuke']\n    families = [\"workfile\"]\n\n    def process(self, instance):  # sourcery skip: avoid-builtin-shadow\n\n        script_data = instance.context.data[\"scriptData\"]\n        current_file = os.path.normpath(nuke.root().name())\n\n        # creating instances per write node\n        staging_dir = os.path.dirname(current_file)\n        base_name = os.path.basename(current_file)\n\n        # creating representation\n        representation = {\n            'name': 'nk',\n            'ext': 'nk',\n            'files': base_name,\n            \"stagingDir\": staging_dir,\n        }\n\n        # creating instance data\n        instance.data.update({\n            \"name\": base_name,\n            \"representations\": [representation]\n        })\n\n        # adding basic script data\n        instance.data.update(script_data)\n\n        self.log.debug(\n            \"Collected current script version: {}\".format(current_file)\n        )\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/collect_writes.py",
    "content": "import os\nimport nuke\nimport pyblish.api\nfrom openpype.hosts.nuke import api as napi\nfrom openpype.pipeline import publish\n\n\nclass CollectNukeWrites(pyblish.api.InstancePlugin,\n                        publish.ColormanagedPyblishPluginMixin):\n    \"\"\"Collect all write nodes.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.0021\n    label = \"Collect Writes\"\n    hosts = [\"nuke\", \"nukeassist\"]\n    families = [\"render\", \"prerender\", \"image\"]\n\n    # cache\n    _write_nodes = {}\n    _frame_ranges = {}\n\n    def process(self, instance):\n\n        group_node = instance.data[\"transientData\"][\"node\"]\n        render_target = instance.data[\"render_target\"]\n\n        write_node = self._write_node_helper(instance)\n\n        if write_node is None:\n            self.log.warning(\n                \"Created node '{}' is missing write node!\".format(\n                    group_node.name()\n                )\n            )\n            return\n\n        # get colorspace and add to version data\n        colorspace = napi.get_colorspace_from_node(write_node)\n\n        if render_target == \"frames\":\n            self._set_existing_files_data(instance, colorspace)\n\n        elif render_target == \"frames_farm\":\n            collected_frames = self._set_existing_files_data(\n                instance, colorspace)\n\n            self._set_expected_files(instance, collected_frames)\n\n            self._add_farm_instance_data(instance)\n\n        elif render_target == \"farm\":\n            self._add_farm_instance_data(instance)\n\n        # set additional instance data\n        self._set_additional_instance_data(instance, render_target, colorspace)\n\n    def _set_existing_files_data(self, instance, colorspace):\n        \"\"\"Set existing files data to instance data.\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n            colorspace (str): colorspace\n\n        Returns:\n            list: collected frames\n        \"\"\"\n        collected_frames = self._get_collected_frames(instance)\n\n        representation = self._get_existing_frames_representation(\n            instance, collected_frames\n        )\n\n        # inject colorspace data\n        self.set_representation_colorspace(\n            representation, instance.context,\n            colorspace=colorspace\n        )\n\n        instance.data[\"representations\"].append(representation)\n\n        return collected_frames\n\n    def _set_expected_files(self, instance, collected_frames):\n        \"\"\"Set expected files to instance data.\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n            collected_frames (list): collected frames\n        \"\"\"\n        write_node = self._write_node_helper(instance)\n\n        write_file_path = nuke.filename(write_node)\n        output_dir = os.path.dirname(write_file_path)\n\n        instance.data[\"expectedFiles\"] = [\n            os.path.join(output_dir, source_file)\n            for source_file in collected_frames\n        ]\n\n    def _get_frame_range_data(self, instance):\n        \"\"\"Get frame range data from instance.\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n\n        Returns:\n            tuple: first_frame, last_frame\n        \"\"\"\n\n        instance_name = instance.data[\"name\"]\n\n        if self._frame_ranges.get(instance_name):\n            # return cashed write node\n            return self._frame_ranges[instance_name]\n\n        write_node = self._write_node_helper(instance)\n\n        # Get frame range from workfile\n        first_frame = int(nuke.root()[\"first_frame\"].getValue())\n        last_frame = int(nuke.root()[\"last_frame\"].getValue())\n\n        # Get frame range from write node if activated\n        if write_node[\"use_limit\"].getValue():\n            first_frame = int(write_node[\"first\"].getValue())\n            last_frame = int(write_node[\"last\"].getValue())\n\n        # add to cache\n        self._frame_ranges[instance_name] = (first_frame, last_frame)\n\n        return first_frame, last_frame\n\n    def _set_additional_instance_data(\n        self, instance, render_target, colorspace\n    ):\n        \"\"\"Set additional instance data.\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n            render_target (str): render target\n            colorspace (str): colorspace\n        \"\"\"\n        family = instance.data[\"family\"]\n\n        # add targeted family to families\n        instance.data[\"families\"].append(\n            \"{}.{}\".format(family, render_target)\n        )\n        self.log.debug(\"Appending render target to families: {}.{}\".format(\n            family, render_target)\n        )\n\n        write_node = self._write_node_helper(instance)\n\n        # Determine defined file type\n        ext = write_node[\"file_type\"].value()\n\n        # get frame range data\n        handle_start = instance.context.data[\"handleStart\"]\n        handle_end = instance.context.data[\"handleEnd\"]\n        first_frame, last_frame = self._get_frame_range_data(instance)\n\n        # get output paths\n        write_file_path = nuke.filename(write_node)\n        output_dir = os.path.dirname(write_file_path)\n\n        # TODO: remove this when we have proper colorspace support\n        version_data = {\n            \"colorspace\": colorspace\n        }\n\n        instance.data.update({\n            \"versionData\": version_data,\n            \"path\": write_file_path,\n            \"outputDir\": output_dir,\n            \"ext\": ext,\n            \"colorspace\": colorspace\n        })\n\n        if family == \"render\":\n            instance.data.update({\n                \"handleStart\": handle_start,\n                \"handleEnd\": handle_end,\n                \"frameStart\": first_frame + handle_start,\n                \"frameEnd\": last_frame - handle_end,\n                \"frameStartHandle\": first_frame,\n                \"frameEndHandle\": last_frame,\n            })\n        else:\n            instance.data.update({\n                \"handleStart\": 0,\n                \"handleEnd\": 0,\n                \"frameStart\": first_frame,\n                \"frameEnd\": last_frame,\n                \"frameStartHandle\": first_frame,\n                \"frameEndHandle\": last_frame,\n            })\n\n        # TODO temporarily set stagingDir as persistent for backward\n        # compatibility. This is mainly focused on `renders`folders which\n        # were previously not cleaned up (and could be used in read notes)\n        # this logic should be removed and replaced with custom staging dir\n        instance.data[\"stagingDir_persistent\"] = True\n\n    def _write_node_helper(self, instance):\n        \"\"\"Helper function to get write node from instance.\n\n        Also sets instance transient data with child nodes.\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n\n        Returns:\n            nuke.Node: write node\n        \"\"\"\n        instance_name = instance.data[\"name\"]\n\n        if self._write_nodes.get(instance_name):\n            # return cashed write node\n            return self._write_nodes[instance_name]\n\n        # get all child nodes from group node\n        child_nodes = napi.get_instance_group_node_childs(instance)\n\n        # set child nodes to instance transient data\n        instance.data[\"transientData\"][\"childNodes\"] = child_nodes\n\n        write_node = None\n        for node_ in child_nodes:\n            if node_.Class() == \"Write\":\n                write_node = node_\n\n        if write_node:\n            # for slate frame extraction\n            instance.data[\"transientData\"][\"writeNode\"] = write_node\n            # add to cache\n            self._write_nodes[instance_name] = write_node\n\n            return self._write_nodes[instance_name]\n\n    def _get_existing_frames_representation(\n        self,\n        instance,\n        collected_frames\n    ):\n        \"\"\"Get existing frames representation.\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n            collected_frames (list): collected frames\n\n        Returns:\n            dict: representation\n        \"\"\"\n\n        first_frame, last_frame = self._get_frame_range_data(instance)\n\n        write_node = self._write_node_helper(instance)\n\n        write_file_path = nuke.filename(write_node)\n        output_dir = os.path.dirname(write_file_path)\n\n        # Determine defined file type\n        ext = write_node[\"file_type\"].value()\n\n        representation = {\n            \"name\": ext,\n            \"ext\": ext,\n            \"stagingDir\": output_dir,\n            \"tags\": []\n        }\n\n        # set slate frame\n        collected_frames = self._add_slate_frame_to_collected_frames(\n            instance,\n            collected_frames,\n            first_frame,\n            last_frame\n        )\n\n        if len(collected_frames) == 1:\n            representation['files'] = collected_frames.pop()\n        else:\n            representation['files'] = collected_frames\n\n        return representation\n\n    def _get_frame_start_str(self, first_frame, last_frame):\n        \"\"\"Get frame start string.\n\n        Args:\n            first_frame (int): first frame\n            last_frame (int): last frame\n\n        Returns:\n            str: frame start string\n        \"\"\"\n        # convert first frame to string with padding\n        return (\n            \"{{:0{}d}}\".format(len(str(last_frame)))\n        ).format(first_frame)\n\n    def _add_slate_frame_to_collected_frames(\n        self,\n        instance,\n        collected_frames,\n        first_frame,\n        last_frame\n    ):\n        \"\"\"Add slate frame to collected frames.\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n            collected_frames (list): collected frames\n            first_frame (int): first frame\n            last_frame (int): last frame\n\n        Returns:\n            list: collected frames\n        \"\"\"\n        frame_start_str = self._get_frame_start_str(first_frame, last_frame)\n        frame_length = int(last_frame - first_frame + 1)\n\n        # this will only run if slate frame is not already\n        # rendered from previews publishes\n        if (\n            \"slate\" in instance.data[\"families\"]\n            and frame_length == len(collected_frames)\n        ):\n            frame_slate_str = self._get_frame_start_str(\n                first_frame - 1,\n                last_frame\n            )\n\n            slate_frame = collected_frames[0].replace(\n                frame_start_str, frame_slate_str)\n            collected_frames.insert(0, slate_frame)\n\n        return collected_frames\n\n    def _add_farm_instance_data(self, instance):\n        \"\"\"Add farm publishing related instance data.\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n        \"\"\"\n\n        # make sure rendered sequence on farm will\n        # be used for extract review\n        if not instance.data.get(\"review\"):\n            instance.data[\"useSequenceForReview\"] = False\n\n        # Farm rendering\n        instance.data.update({\n            \"transfer\": False,\n            \"farm\": True  # to skip integrate\n        })\n        self.log.info(\"Farm rendering ON ...\")\n\n    def _get_collected_frames(self, instance):\n        \"\"\"Get collected frames.\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n\n        Returns:\n            list: collected frames\n        \"\"\"\n\n        first_frame, last_frame = self._get_frame_range_data(instance)\n\n        write_node = self._write_node_helper(instance)\n\n        write_file_path = nuke.filename(write_node)\n        output_dir = os.path.dirname(write_file_path)\n\n        # get file path knob\n        node_file_knob = write_node[\"file\"]\n        # list file paths based on input frames\n        expected_paths = list(sorted({\n            node_file_knob.evaluate(frame)\n            for frame in range(first_frame, last_frame + 1)\n        }))\n\n        # convert only to base names\n        expected_filenames = {\n            os.path.basename(filepath)\n            for filepath in expected_paths\n        }\n\n        # make sure files are existing at folder\n        collected_frames = [\n            filename\n            for filename in os.listdir(output_dir)\n            if filename in expected_filenames\n        ]\n\n        return collected_frames\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/extract_backdrop.py",
    "content": "import os\n\nimport nuke\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.nuke.api.lib import (\n    maintained_selection,\n    reset_selection,\n    select_nodes\n)\n\n\nclass ExtractBackdropNode(publish.Extractor):\n    \"\"\"Extracting content of backdrop nodes\n\n    Will create nuke script only with containing nodes.\n    Also it will solve Input and Output nodes.\n\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract Backdrop\"\n    hosts = [\"nuke\"]\n    families = [\"nukenodes\"]\n\n    def process(self, instance):\n        tmp_nodes = []\n        child_nodes = instance.data[\"transientData\"][\"childNodes\"]\n        # all connections outside of backdrop\n        connections_in = instance.data[\"transientData\"][\"nodeConnectionsIn\"]\n        connections_out = instance.data[\"transientData\"][\"nodeConnectionsOut\"]\n        self.log.debug(\"_ connections_in: `{}`\".format(connections_in))\n        self.log.debug(\"_ connections_out: `{}`\".format(connections_out))\n\n        # Define extract output file path\n        stagingdir = self.staging_dir(instance)\n        filename = \"{0}.nk\".format(instance.name)\n        path = os.path.join(stagingdir, filename)\n\n        # maintain selection\n        with maintained_selection():\n            # create input child_nodes and name them as passing node (*_INP)\n            for n, inputs in connections_in.items():\n                for i, input in inputs:\n                    inpn = nuke.createNode(\"Input\")\n                    inpn[\"name\"].setValue(\"{}_{}_INP\".format(n.name(), i))\n                    n.setInput(i, inpn)\n                    inpn.setXYpos(input.xpos(), input.ypos())\n                    child_nodes.append(inpn)\n                    tmp_nodes.append(inpn)\n\n            reset_selection()\n\n            # connect output node\n            for n, output in connections_out.items():\n                opn = nuke.createNode(\"Output\")\n                output.setInput(\n                    next((i for i, d in enumerate(output.dependencies())\n                          if d.name() in n.name()), 0), opn)\n                opn.setInput(0, n)\n                opn.autoplace()\n                child_nodes.append(opn)\n                tmp_nodes.append(opn)\n                reset_selection()\n\n            # select child_nodes to copy\n            reset_selection()\n            select_nodes(child_nodes)\n            # create tmp nk file\n            # save file to the path\n            nuke.nodeCopy(path)\n\n            # Clean up\n            for tn in tmp_nodes:\n                nuke.delete(tn)\n\n            # restore original connections\n            # reconnect input node\n            for n, inputs in connections_in.items():\n                for i, input in inputs:\n                    n.setInput(i, input)\n\n            # reconnect output node\n            for n, output in connections_out.items():\n                output.setInput(\n                    next((i for i, d in enumerate(output.dependencies())\n                          if d.name() in n.name()), 0), n)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        # create representation\n        representation = {\n            'name': 'nk',\n            'ext': 'nk',\n            'files': filename,\n            \"stagingDir\": stagingdir\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '{}' to: {}\".format(\n            instance.name, path))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/extract_camera.py",
    "content": "import os\nimport math\nfrom pprint import pformat\n\nimport nuke\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.nuke.api.lib import maintained_selection\n\n\nclass ExtractCamera(publish.Extractor):\n    \"\"\" 3D camera extractor\n    \"\"\"\n    label = 'Extract Camera'\n    order = pyblish.api.ExtractorOrder\n    families = [\"camera\"]\n    hosts = [\"nuke\"]\n\n    # presets\n    write_geo_knobs = [\n        (\"file_type\", \"abc\"),\n        (\"storageFormat\", \"Ogawa\"),\n        (\"writeGeometries\", False),\n        (\"writePointClouds\", False),\n        (\"writeAxes\", False)\n    ]\n\n    def process(self, instance):\n        camera_node = instance.data[\"transientData\"][\"node\"]\n        handle_start = instance.context.data[\"handleStart\"]\n        handle_end = instance.context.data[\"handleEnd\"]\n        first_frame = int(nuke.root()[\"first_frame\"].getValue())\n        last_frame = int(nuke.root()[\"last_frame\"].getValue())\n        step = 1\n        output_range = str(nuke.FrameRange(first_frame, last_frame, step))\n\n        rm_nodes = []\n        self.log.debug(\"Creating additional nodes for 3D Camera Extractor\")\n        subset = instance.data[\"subset\"]\n        staging_dir = self.staging_dir(instance)\n\n        # get extension form preset\n        extension = next((k[1] for k in self.write_geo_knobs\n                          if k[0] == \"file_type\"), None)\n        if not extension:\n            raise RuntimeError(\n                \"Bad config for extension in presets. \"\n                \"Talk to your supervisor or pipeline admin\")\n\n        # create file name and path\n        filename = subset + \".{}\".format(extension)\n        file_path = os.path.join(staging_dir, filename).replace(\"\\\\\", \"/\")\n\n        with maintained_selection():\n            # bake camera with axeses onto word coordinate XYZ\n            rm_n = bakeCameraWithAxeses(\n                camera_node, output_range)\n            rm_nodes.append(rm_n)\n\n            # create scene node\n            rm_n = nuke.createNode(\"Scene\")\n            rm_nodes.append(rm_n)\n\n            # create write geo node\n            wg_n = nuke.createNode(\"WriteGeo\")\n            wg_n[\"file\"].setValue(file_path)\n            # add path to write to\n            for k, v in self.write_geo_knobs:\n                wg_n[k].setValue(v)\n            rm_nodes.append(wg_n)\n\n            # write out camera\n            nuke.execute(\n                wg_n,\n                int(first_frame),\n                int(last_frame)\n            )\n            # erase additional nodes\n            for n in rm_nodes:\n                nuke.delete(n)\n\n        # create representation data\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': extension,\n            'ext': extension,\n            'files': filename,\n            \"stagingDir\": staging_dir,\n            \"frameStart\": first_frame,\n            \"frameEnd\": last_frame\n        }\n        instance.data[\"representations\"].append(representation)\n\n        instance.data.update({\n            \"path\": file_path,\n            \"outputDir\": staging_dir,\n            \"ext\": extension,\n            \"handleStart\": handle_start,\n            \"handleEnd\": handle_end,\n            \"frameStart\": first_frame + handle_start,\n            \"frameEnd\": last_frame - handle_end,\n            \"frameStartHandle\": first_frame,\n            \"frameEndHandle\": last_frame,\n        })\n\n        self.log.debug(\"Extracted instance '{0}' to: {1}\".format(\n            instance.name, file_path))\n\n\ndef bakeCameraWithAxeses(camera_node, output_range):\n    \"\"\" Baking all perent hierarchy of axeses into camera\n    with transposition onto word XYZ coordinance\n    \"\"\"\n    bakeFocal = False\n    bakeHaperture = False\n    bakeVaperture = False\n\n    camera_matrix = camera_node['world_matrix']\n\n    new_cam_n = nuke.createNode(\"Camera2\")\n    new_cam_n.setInput(0, None)\n    new_cam_n['rotate'].setAnimated()\n    new_cam_n['translate'].setAnimated()\n\n    old_focal = camera_node['focal']\n    if old_focal.isAnimated() and not (old_focal.animation(0).constant()):\n        new_cam_n['focal'].setAnimated()\n        bakeFocal = True\n    else:\n        new_cam_n['focal'].setValue(old_focal.value())\n\n    old_haperture = camera_node['haperture']\n    if old_haperture.isAnimated() and not (\n            old_haperture.animation(0).constant()):\n        new_cam_n['haperture'].setAnimated()\n        bakeHaperture = True\n    else:\n        new_cam_n['haperture'].setValue(old_haperture.value())\n\n    old_vaperture = camera_node['vaperture']\n    if old_vaperture.isAnimated() and not (\n            old_vaperture.animation(0).constant()):\n        new_cam_n['vaperture'].setAnimated()\n        bakeVaperture = True\n    else:\n        new_cam_n['vaperture'].setValue(old_vaperture.value())\n\n    new_cam_n['win_translate'].setValue(camera_node['win_translate'].value())\n    new_cam_n['win_scale'].setValue(camera_node['win_scale'].value())\n\n    for x in nuke.FrameRange(output_range):\n        math_matrix = nuke.math.Matrix4()\n        for y in range(camera_matrix.height()):\n            for z in range(camera_matrix.width()):\n                matrix_pointer = z + (y * camera_matrix.width())\n                math_matrix[matrix_pointer] = camera_matrix.getValueAt(\n                    x, (y + (z * camera_matrix.width())))\n\n        rot_matrix = nuke.math.Matrix4(math_matrix)\n        rot_matrix.rotationOnly()\n        rot = rot_matrix.rotationsZXY()\n\n        new_cam_n['rotate'].setValueAt(math.degrees(rot[0]), x, 0)\n        new_cam_n['rotate'].setValueAt(math.degrees(rot[1]), x, 1)\n        new_cam_n['rotate'].setValueAt(math.degrees(rot[2]), x, 2)\n        new_cam_n['translate'].setValueAt(\n            camera_matrix.getValueAt(x, 3), x, 0)\n        new_cam_n['translate'].setValueAt(\n            camera_matrix.getValueAt(x, 7), x, 1)\n        new_cam_n['translate'].setValueAt(\n            camera_matrix.getValueAt(x, 11), x, 2)\n\n        if bakeFocal:\n            new_cam_n['focal'].setValueAt(old_focal.getValueAt(x), x)\n        if bakeHaperture:\n            new_cam_n['haperture'].setValueAt(old_haperture.getValueAt(x), x)\n        if bakeVaperture:\n            new_cam_n['vaperture'].setValueAt(old_vaperture.getValueAt(x), x)\n\n    return new_cam_n\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/extract_gizmo.py",
    "content": "import os\nimport nuke\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.nuke.api import utils as pnutils\nfrom openpype.hosts.nuke.api.lib import (\n    maintained_selection,\n    reset_selection,\n    select_nodes\n)\n\n\nclass ExtractGizmo(publish.Extractor):\n    \"\"\"Extracting Gizmo (Group) node\n\n    Will create nuke script only with the Gizmo node.\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract Gizmo (group)\"\n    hosts = [\"nuke\"]\n    families = [\"gizmo\"]\n\n    def process(self, instance):\n        tmp_nodes = []\n        orig_grpn = instance.data[\"transientData\"][\"node\"]\n\n        # Define extract output file path\n        stagingdir = self.staging_dir(instance)\n        filename = \"{0}.nk\".format(instance.name)\n        path = os.path.join(stagingdir, filename)\n\n        # maintain selection\n        with maintained_selection():\n            orig_grpn_name = orig_grpn.name()\n            tmp_grpn_name = orig_grpn_name + \"_tmp\"\n            # select original group node\n            select_nodes([orig_grpn])\n\n            # copy to clipboard\n            nuke.nodeCopy(\"%clipboard%\")\n\n            # reset selection to none\n            reset_selection()\n\n            # paste clipboard\n            nuke.nodePaste(\"%clipboard%\")\n\n            # assign pasted node\n            copy_grpn = nuke.selectedNode()\n            copy_grpn.setXYpos((orig_grpn.xpos() + 120), orig_grpn.ypos())\n\n            # convert gizmos to groups\n            pnutils.bake_gizmos_recursively(copy_grpn)\n\n            # add to temporary nodes\n            tmp_nodes.append(copy_grpn)\n\n            # swap names\n            orig_grpn.setName(tmp_grpn_name)\n            copy_grpn.setName(orig_grpn_name)\n\n            # create tmp nk file\n            # save file to the path\n            nuke.nodeCopy(path)\n\n            # Clean up\n            for tn in tmp_nodes:\n                nuke.delete(tn)\n\n            # rename back to original\n            orig_grpn.setName(orig_grpn_name)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        # create representation\n        representation = {\n            'name': 'gizmo',\n            'ext': 'nk',\n            'files': filename,\n            \"stagingDir\": stagingdir\n        }\n        instance.data[\"representations\"].append(representation)\n\n        self.log.debug(\"Extracted instance '{}' to: {}\".format(\n            instance.name, path))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/extract_model.py",
    "content": "import os\nfrom pprint import pformat\nimport nuke\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.nuke.api.lib import (\n    maintained_selection,\n    select_nodes\n)\n\n\nclass ExtractModel(publish.Extractor):\n    \"\"\" 3D model extractor\n    \"\"\"\n    label = 'Extract Model'\n    order = pyblish.api.ExtractorOrder\n    families = [\"model\"]\n    hosts = [\"nuke\"]\n\n    # presets\n    write_geo_knobs = [\n        (\"file_type\", \"abc\"),\n        (\"storageFormat\", \"Ogawa\"),\n        (\"writeGeometries\", True),\n        (\"writePointClouds\", False),\n        (\"writeAxes\", False)\n    ]\n\n    def process(self, instance):\n        handle_start = instance.context.data[\"handleStart\"]\n        handle_end = instance.context.data[\"handleEnd\"]\n        first_frame = int(nuke.root()[\"first_frame\"].getValue())\n        last_frame = int(nuke.root()[\"last_frame\"].getValue())\n\n        self.log.debug(\"instance.data: `{}`\".format(\n            pformat(instance.data)))\n\n        rm_nodes = []\n        model_node = instance.data[\"transientData\"][\"node\"]\n\n        self.log.debug(\"Creating additional nodes for Extract Model\")\n        subset = instance.data[\"subset\"]\n        staging_dir = self.staging_dir(instance)\n\n        extension = next((k[1] for k in self.write_geo_knobs\n                          if k[0] == \"file_type\"), None)\n        if not extension:\n            raise RuntimeError(\n                \"Bad config for extension in presets. \"\n                \"Talk to your supervisor or pipeline admin\")\n\n        # create file name and path\n        filename = subset + \".{}\".format(extension)\n        file_path = os.path.join(staging_dir, filename).replace(\"\\\\\", \"/\")\n\n        with maintained_selection():\n            # select model node\n            select_nodes([model_node])\n\n            # create write geo node\n            wg_n = nuke.createNode(\"WriteGeo\")\n            wg_n[\"file\"].setValue(file_path)\n            # add path to write to\n            for k, v in self.write_geo_knobs:\n                wg_n[k].setValue(v)\n            rm_nodes.append(wg_n)\n\n            # write out model\n            nuke.execute(\n                wg_n,\n                int(first_frame),\n                int(last_frame)\n            )\n            # erase additional nodes\n            for n in rm_nodes:\n                nuke.delete(n)\n\n            self.log.debug(\"Filepath: {}\".format(file_path))\n\n        # create representation data\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            'name': extension,\n            'ext': extension,\n            'files': filename,\n            \"stagingDir\": staging_dir,\n            \"frameStart\": first_frame,\n            \"frameEnd\": last_frame\n        }\n        instance.data[\"representations\"].append(representation)\n\n        instance.data.update({\n            \"path\": file_path,\n            \"outputDir\": staging_dir,\n            \"ext\": extension,\n            \"handleStart\": handle_start,\n            \"handleEnd\": handle_end,\n            \"frameStart\": first_frame + handle_start,\n            \"frameEnd\": last_frame - handle_end,\n            \"frameStartHandle\": first_frame,\n            \"frameEndHandle\": last_frame,\n        })\n\n        self.log.debug(\"Extracted instance '{0}' to: {1}\".format(\n            instance.name, file_path))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/extract_ouput_node.py",
    "content": "import nuke\nimport pyblish.api\nfrom openpype.hosts.nuke.api.lib import maintained_selection\n\n\nclass CreateOutputNode(pyblish.api.ContextPlugin):\n    \"\"\"Adding output node for each output write node\n    So when latly user will want to Load .nk as LifeGroup or Precomp\n    Nuke will not complain about missing Output node\n    \"\"\"\n    label = 'Output Node Create'\n    order = pyblish.api.ExtractorOrder + 0.4\n    families = [\"workfile\"]\n    hosts = ['nuke']\n\n    def process(self, context):\n        # capture selection state\n        with maintained_selection():\n\n            active_node = [\n                inst.data.get(\"transientData\", {}).get(\"node\")\n                for inst in context\n                if inst.data.get(\"transientData\", {}).get(\"node\")\n                if inst.data.get(\n                    \"transientData\", {}).get(\"node\").Class() != \"Root\"\n            ]\n\n            if active_node:\n                active_node = active_node.pop()\n                self.log.debug(\"Active node: {}\".format(active_node))\n                active_node['selected'].setValue(True)\n\n            # select only instance render node\n            output_node = nuke.createNode(\"Output\")\n\n            # deselect all and select the original selection\n            output_node['selected'].setValue(False)\n\n            # save script\n            nuke.scriptSave()\n\n            # add node to instance node list\n            context.data[\"outputNode\"] = output_node\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/extract_output_directory.py",
    "content": "import os\n\nimport pyblish.api\n\n\nclass ExtractOutputDirectory(pyblish.api.InstancePlugin):\n    \"\"\"Extracts the output path for any collection or single output_path.\"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.05\n    label = \"Output Directory\"\n    optional = True\n\n    # targets = [\"process\"]\n\n    def process(self, instance):\n\n        path = None\n\n        if \"output_path\" in instance.data.keys():\n            path = instance.data[\"path\"]\n\n        if not path:\n            return\n\n        if not os.path.exists(os.path.dirname(path)):\n            os.makedirs(os.path.dirname(path))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/extract_render_local.py",
    "content": "import os\nimport shutil\n\nimport pyblish.api\nimport clique\nimport nuke\nfrom openpype.hosts.nuke import api as napi\nfrom openpype.pipeline import publish\nfrom openpype.lib import collect_frames\n\n\nclass NukeRenderLocal(publish.Extractor,\n                      publish.ColormanagedPyblishPluginMixin):\n    \"\"\"Render the current Nuke composition locally.\n\n    Extract the result of savers by starting a comp render\n    This will run the local render of Fusion.\n\n    Allows to use last published frames and overwrite only specific ones\n    (set in instance.data.get(\"frames_to_fix\"))\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder\n    label = \"Render Local\"\n    hosts = [\"nuke\"]\n    families = [\"render.local\", \"prerender.local\", \"image.local\"]\n\n    def process(self, instance):\n        child_nodes = (\n            instance.data.get(\"transientData\", {}).get(\"childNodes\")\n            or instance\n        )\n\n        node = None\n        for x in child_nodes:\n            if x.Class() == \"Write\":\n                node = x\n\n        self.log.debug(\"instance collected: {}\".format(instance.data))\n\n        node_subset_name = instance.data.get(\"name\", None)\n\n        first_frame = instance.data.get(\"frameStartHandle\", None)\n        last_frame = instance.data.get(\"frameEndHandle\", None)\n\n        filenames = []\n        node_file = node[\"file\"]\n        # Collect expected filepaths for each frame\n        # - for cases that output is still image is first created set of\n        #   paths which is then sorted and converted to list\n        expected_paths = list(sorted({\n            node_file.evaluate(frame)\n            for frame in range(first_frame, last_frame + 1)\n        }))\n        # Extract only filenames for representation\n        filenames.extend([\n            os.path.basename(filepath)\n            for filepath in expected_paths\n        ])\n\n        # Ensure output directory exists.\n        out_dir = os.path.dirname(expected_paths[0])\n        if not os.path.exists(out_dir):\n            os.makedirs(out_dir)\n\n        frames_to_render = [(first_frame, last_frame)]\n\n        frames_to_fix = instance.data.get(\"frames_to_fix\")\n        if instance.data.get(\"last_version_published_files\") and frames_to_fix:\n            frames_to_render = self._get_frames_to_render(frames_to_fix)\n            anatomy = instance.context.data[\"anatomy\"]\n            self._copy_last_published(anatomy, instance, out_dir,\n                                      filenames)\n\n        for render_first_frame, render_last_frame in frames_to_render:\n\n            self.log.info(\"Starting render\")\n            self.log.info(\"Start frame: {}\".format(render_first_frame))\n            self.log.info(\"End frame: {}\".format(render_last_frame))\n\n            # Render frames\n            nuke.execute(\n                str(node_subset_name),\n                int(render_first_frame),\n                int(render_last_frame)\n            )\n\n        ext = node[\"file_type\"].value()\n        colorspace = napi.get_colorspace_from_node(node)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        if len(filenames) == 1:\n            repre = {\n                'name': ext,\n                'ext': ext,\n                'files': filenames[0],\n                \"stagingDir\": out_dir\n            }\n        else:\n            repre = {\n                'name': ext,\n                'ext': ext,\n                'frameStart': (\n                    \"{{:0>{}}}\"\n                    .format(len(str(last_frame)))\n                    .format(first_frame)\n                ),\n                'files': filenames,\n                \"stagingDir\": out_dir\n            }\n\n        # inject colorspace data\n        self.set_representation_colorspace(\n            repre, instance.context,\n            colorspace=colorspace\n        )\n\n        instance.data[\"representations\"].append(repre)\n\n        self.log.debug(\"Extracted instance '{0}' to: {1}\".format(\n            instance.name,\n            out_dir\n        ))\n\n        families = instance.data[\"families\"]\n        # redefinition of families\n        if \"render.local\" in families:\n            instance.data['family'] = 'render'\n            families.remove('render.local')\n            families.insert(0, \"render2d\")\n            instance.data[\"anatomyData\"][\"family\"] = \"render\"\n        elif \"prerender.local\" in families:\n            instance.data['family'] = 'prerender'\n            families.remove('prerender.local')\n            families.insert(0, \"prerender\")\n            instance.data[\"anatomyData\"][\"family\"] = \"prerender\"\n        elif \"image.local\" in families:\n            instance.data['family'] = 'image'\n            families.remove('image.local')\n            instance.data[\"anatomyData\"][\"family\"] = \"image\"\n        instance.data[\"families\"] = families\n\n        collections, remainder = clique.assemble(filenames)\n        self.log.debug('collections: {}'.format(str(collections)))\n\n        if collections:\n            collection = collections[0]\n            instance.data['collection'] = collection\n\n        self.log.info('Finished render')\n\n        self.log.debug(\"_ instance.data: {}\".format(instance.data))\n\n    def _copy_last_published(self, anatomy, instance, out_dir,\n                             expected_filenames):\n        \"\"\"Copies last published files to temporary out_dir.\n\n        These are base of files which will be extended/fixed for specific\n        frames.\n        Renames published file to expected file name based on frame, eg.\n        test_project_test_asset_subset_v005.1001.exr > new_render.1001.exr\n        \"\"\"\n        last_published = instance.data[\"last_version_published_files\"]\n        last_published_and_frames = collect_frames(last_published)\n\n        expected_and_frames = collect_frames(expected_filenames)\n        frames_and_expected = {v: k for k, v in expected_and_frames.items()}\n        for file_path, frame in last_published_and_frames.items():\n            file_path = anatomy.fill_root(file_path)\n            if not os.path.exists(file_path):\n                continue\n            target_file_name = frames_and_expected.get(frame)\n            if not target_file_name:\n                continue\n\n            out_path = os.path.join(out_dir, target_file_name)\n            self.log.debug(\"Copying '{}' -> '{}'\".format(file_path, out_path))\n            shutil.copy(file_path, out_path)\n\n            # TODO shouldn't this be uncommented\n            # instance.context.data[\"cleanupFullPaths\"].append(out_path)\n\n    def _get_frames_to_render(self, frames_to_fix):\n        \"\"\"Return list of frame range tuples to render\n\n        Args:\n            frames_to_fix (str): specific or range of frames to be rerendered\n             (1005, 1009-1010)\n        Returns:\n            (list): [(1005, 1005), (1009-1010)]\n        \"\"\"\n        frames_to_render = []\n\n        for frame_range in frames_to_fix.split(\",\"):\n            if frame_range.isdigit():\n                render_first_frame = frame_range\n                render_last_frame = frame_range\n            elif '-' in frame_range:\n                frames = frame_range.split('-')\n                render_first_frame = int(frames[0])\n                render_last_frame = int(frames[1])\n            else:\n                raise ValueError(\"Wrong format of frames to fix {}\"\n                                 .format(frames_to_fix))\n            frames_to_render.append((render_first_frame,\n                                     render_last_frame))\n        return frames_to_render\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/extract_review_data.py",
    "content": "import os\nfrom pprint import pformat\nimport pyblish.api\n\nfrom openpype.pipeline import publish\n\n\nclass ExtractReviewData(publish.Extractor):\n    \"\"\"Extracts review tag into available representation\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder + 0.01\n    # order = pyblish.api.CollectorOrder + 0.499\n    label = \"Extract Review Data\"\n\n    families = [\"review\"]\n    hosts = [\"nuke\"]\n\n    def process(self, instance):\n        fpath = instance.data[\"path\"]\n        ext = os.path.splitext(fpath)[-1][1:]\n\n        representations = instance.data.get(\"representations\", [])\n\n        # review can be removed since `ProcessSubmittedJobOnFarm` will create\n        # reviewable representation if needed\n        if (\n            instance.data.get(\"farm\")\n            and \"review\" in instance.data[\"families\"]\n        ):\n            instance.data[\"families\"].remove(\"review\")\n\n        # iterate representations and add `review` tag\n        for repre in representations:\n            if ext != repre[\"ext\"]:\n                continue\n\n            if not repre.get(\"tags\"):\n                repre[\"tags\"] = []\n\n            if \"review\" not in repre[\"tags\"]:\n                repre[\"tags\"].append(\"review\")\n\n            self.log.debug(\"Matching representation: {}\".format(\n                pformat(repre)\n            ))\n\n        instance.data[\"representations\"] = representations\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py",
    "content": "import os\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.nuke.api import plugin\nfrom openpype.hosts.nuke.api.lib import maintained_selection\n\n\nclass ExtractReviewDataLut(publish.Extractor):\n    \"\"\"Extracts movie and thumbnail with baked in luts\n\n    must be run after extract_render_local.py\n\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder + 0.005\n    label = \"Extract Review Data Lut\"\n\n    families = [\"review\"]\n    hosts = [\"nuke\"]\n\n    def process(self, instance):\n        self.log.debug(\"Creating staging dir...\")\n        if \"representations\" in instance.data:\n            staging_dir = instance.data[\n                \"representations\"][0][\"stagingDir\"].replace(\"\\\\\", \"/\")\n            instance.data[\"stagingDir\"] = staging_dir\n            instance.data[\"representations\"][0][\"tags\"] = [\"review\"]\n        else:\n            instance.data[\"representations\"] = []\n            # get output path\n            render_path = instance.data['path']\n            staging_dir = os.path.normpath(os.path.dirname(render_path))\n            instance.data[\"stagingDir\"] = staging_dir\n\n        self.log.debug(\n            \"StagingDir `{0}`...\".format(instance.data[\"stagingDir\"]))\n\n        # generate data\n        with maintained_selection():\n            exporter = plugin.ExporterReviewLut(\n                self, instance\n                )\n            data = exporter.generate_lut()\n\n            # assign to representations\n            instance.data[\"lutPath\"] = os.path.join(\n                exporter.stagingDir, exporter.file).replace(\"\\\\\", \"/\")\n            instance.data[\"representations\"] += data[\"representations\"]\n\n        # review can be removed since `ProcessSubmittedJobOnFarm` will create\n        # reviewable representation if needed\n        if (\n            instance.data.get(\"farm\")\n            and \"review\" in instance.data[\"families\"]\n        ):\n            instance.data[\"families\"].remove(\"review\")\n\n        self.log.debug(\n            \"_ lutPath: {}\".format(instance.data[\"lutPath\"]))\n        self.log.debug(\n            \"_ representations: {}\".format(instance.data[\"representations\"]))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py",
    "content": "import os\nimport re\nfrom pprint import pformat\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.nuke.api import plugin\nfrom openpype.hosts.nuke.api.lib import maintained_selection\n\n\nclass ExtractReviewIntermediates(publish.Extractor):\n    \"\"\"Extracting intermediate videos or sequences with\n    thumbnail for transcoding.\n\n    must be run after extract_render_local.py\n\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder + 0.01\n    label = \"Extract Review Intermediates\"\n\n    families = [\"review\"]\n    hosts = [\"nuke\"]\n\n    # presets\n    viewer_lut_raw = None\n    outputs = {}\n\n    @classmethod\n    def apply_settings(cls, project_settings):\n        \"\"\"Apply the settings from the deprecated\n        ExtractReviewDataMov plugin for backwards compatibility\n        \"\"\"\n        nuke_publish = project_settings[\"nuke\"][\"publish\"]\n        deprecated_setting = nuke_publish[\"ExtractReviewDataMov\"]\n        current_setting = nuke_publish.get(\"ExtractReviewIntermediates\")\n        if not deprecated_setting[\"enabled\"] and (\n            not current_setting[\"enabled\"]\n        ):\n            cls.enabled = False\n\n        if deprecated_setting[\"enabled\"]:\n            # Use deprecated settings if they are still enabled\n            cls.viewer_lut_raw = deprecated_setting[\"viewer_lut_raw\"]\n            cls.outputs = deprecated_setting[\"outputs\"]\n        elif current_setting is None:\n            pass\n        elif current_setting[\"enabled\"]:\n            cls.viewer_lut_raw = current_setting[\"viewer_lut_raw\"]\n            cls.outputs = current_setting[\"outputs\"]\n\n    def process(self, instance):\n        families = set(instance.data[\"families\"])\n\n        # add main family to make sure all families are compared\n        families.add(instance.data[\"family\"])\n\n        task_type = instance.context.data[\"taskType\"]\n        subset = instance.data[\"subset\"]\n        self.log.debug(\"Creating staging dir...\")\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        staging_dir = os.path.normpath(\n            os.path.dirname(instance.data[\"path\"]))\n\n        instance.data[\"stagingDir\"] = staging_dir\n\n        self.log.debug(\n            \"StagingDir `{0}`...\".format(instance.data[\"stagingDir\"]))\n\n        self.log.debug(\"Outputs: {}\".format(self.outputs))\n\n        # generate data\n        with maintained_selection():\n            generated_repres = []\n            for o_name, o_data in self.outputs.items():\n                self.log.debug(\n                    \"o_name: {}, o_data: {}\".format(o_name, pformat(o_data)))\n                f_families = o_data[\"filter\"][\"families\"]\n                f_task_types = o_data[\"filter\"][\"task_types\"]\n                f_subsets = o_data[\"filter\"][\"subsets\"]\n\n                self.log.debug(\n                    \"f_families `{}` > families: {}\".format(\n                        f_families, families))\n\n                self.log.debug(\n                    \"f_task_types `{}` > task_type: {}\".format(\n                        f_task_types, task_type))\n\n                self.log.debug(\n                    \"f_subsets `{}` > subset: {}\".format(\n                        f_subsets, subset))\n\n                # test if family found in context\n                # using intersection to make sure all defined\n                # families are present in combination\n                if f_families and not families.intersection(f_families):\n                    continue\n\n                # test task types from filter\n                if f_task_types and task_type not in f_task_types:\n                    continue\n\n                # test subsets from filter\n                if f_subsets and not any(\n                        re.search(s, subset) for s in f_subsets):\n                    continue\n\n                self.log.debug(\n                    \"Baking output `{}` with settings: {}\".format(\n                        o_name, o_data)\n                )\n\n                # check if settings have more then one preset\n                # so we dont need to add outputName to representation\n                # in case there is only one preset\n                multiple_presets = len(self.outputs.keys()) > 1\n\n                # adding bake presets to instance data for other plugins\n                if not instance.data.get(\"bakePresets\"):\n                    instance.data[\"bakePresets\"] = {}\n                # add preset to bakePresets\n                instance.data[\"bakePresets\"][o_name] = o_data\n\n                # create exporter instance\n                exporter = plugin.ExporterReviewMov(\n                    self, instance, o_name, o_data[\"extension\"],\n                    multiple_presets)\n\n                if instance.data.get(\"farm\"):\n                    if \"review\" in instance.data[\"families\"]:\n                        instance.data[\"families\"].remove(\"review\")\n\n                    data = exporter.generate_mov(farm=True, **o_data)\n\n                    self.log.debug(\n                        \"_ data: {}\".format(data))\n\n                    if not instance.data.get(\"bakingNukeScripts\"):\n                        instance.data[\"bakingNukeScripts\"] = []\n\n                    instance.data[\"bakingNukeScripts\"].append({\n                        \"bakeRenderPath\": data.get(\"bakeRenderPath\"),\n                        \"bakeScriptPath\": data.get(\"bakeScriptPath\"),\n                        \"bakeWriteNodeName\": data.get(\"bakeWriteNodeName\")\n                    })\n                else:\n                    data = exporter.generate_mov(**o_data)\n\n                # add representation generated by exporter\n                generated_repres.extend(data[\"representations\"])\n                self.log.debug(\n                    \"__ generated_repres: {}\".format(generated_repres))\n\n        if generated_repres:\n            # assign to representations\n            instance.data[\"representations\"] += generated_repres\n            instance.data[\"useSequenceForReview\"] = False\n        else:\n            instance.data[\"families\"].remove(\"review\")\n            self.log.debug(\n                \"Removing `review` from families. \"\n                \"Not available baking profile.\"\n            )\n            self.log.debug(instance.data[\"families\"])\n\n        self.log.debug(\n            \"_ representations: {}\".format(\n                instance.data[\"representations\"]))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/extract_script_save.py",
    "content": "import nuke\nimport pyblish.api\n\n\nclass ExtractScriptSave(pyblish.api.Extractor):\n    \"\"\"Save current Nuke workfile script\"\"\"\n    label = 'Script Save'\n    order = pyblish.api.Extractor.order - 0.1\n    hosts = ['nuke']\n\n    def process(self, instance):\n\n        self.log.debug('Saving current script')\n        nuke.scriptSave()\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/extract_slate_frame.py",
    "content": "import os\nfrom pprint import pformat\nimport nuke\nimport copy\n\nimport pyblish.api\nimport six\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.nuke.api import (\n    maintained_selection,\n    duplicate_node,\n    get_view_process_node\n)\n\n\nclass ExtractSlateFrame(publish.Extractor):\n    \"\"\"Extracts movie and thumbnail with baked in luts\n\n    must be run after extract_render_local.py\n\n    \"\"\"\n\n    order = pyblish.api.ExtractorOrder + 0.011\n    label = \"Extract Slate Frame\"\n\n    families = [\"slate\"]\n    hosts = [\"nuke\"]\n\n    # Settings values\n    key_value_mapping = {\n        \"f_submission_note\": [True, \"{comment}\"],\n        \"f_submitting_for\": [True, \"{intent[value]}\"],\n        \"f_vfx_scope_of_work\": [False, \"\"]\n    }\n\n    def process(self, instance):\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        self._create_staging_dir(instance)\n\n        with maintained_selection():\n            self.log.debug(\"instance: {}\".format(instance))\n            self.log.debug(\"instance.data[families]: {}\".format(\n                instance.data[\"families\"]))\n\n            if instance.data.get(\"bakePresets\"):\n                for o_name, o_data in instance.data[\"bakePresets\"].items():\n                    self.log.debug(\"_ o_name: {}, o_data: {}\".format(\n                        o_name, pformat(o_data)))\n                    self.render_slate(\n                        instance,\n                        o_name,\n                        o_data[\"bake_viewer_process\"],\n                        o_data[\"bake_viewer_input_process\"]\n                    )\n            else:\n                # backward compatibility\n                self.render_slate(instance)\n\n            # also render image to sequence\n            self._render_slate_to_sequence(instance)\n\n    def _create_staging_dir(self, instance):\n\n        self.log.debug(\"Creating staging dir...\")\n\n        staging_dir = os.path.normpath(\n            os.path.dirname(instance.data[\"path\"]))\n\n        instance.data[\"stagingDir\"] = staging_dir\n\n        self.log.debug(\n            \"StagingDir `{0}`...\".format(instance.data[\"stagingDir\"]))\n\n    def _check_frames_exists(self, instance):\n        # rendering path from group write node\n        fpath = instance.data[\"path\"]\n\n        # instance frame range with handles\n        first = instance.data[\"frameStartHandle\"]\n        last = instance.data[\"frameEndHandle\"]\n\n        padding = fpath.count('#')\n\n        test_path_template = fpath\n        if padding:\n            repl_string = \"#\" * padding\n            test_path_template = fpath.replace(\n                repl_string, \"%0{}d\".format(padding))\n\n        for frame in range(first, last + 1):\n            test_file = test_path_template % frame\n            if not os.path.exists(test_file):\n                self.log.debug(\"__ test_file: `{}`\".format(test_file))\n                return None\n\n        return True\n\n    def render_slate(\n        self,\n        instance,\n        output_name=None,\n        bake_viewer_process=True,\n        bake_viewer_input_process=True\n    ):\n        \"\"\"Slate frame renderer\n\n        Args:\n            instance (PyblishInstance): Pyblish instance with subset data\n            output_name (str, optional):\n                Slate variation name. Defaults to None.\n            bake_viewer_process (bool, optional):\n                Switch for viewer profile baking. Defaults to True.\n            bake_viewer_input_process (bool, optional):\n                Switch for input process node baking. Defaults to True.\n        \"\"\"\n        slate_node = instance.data[\"slateNode\"]\n\n        # rendering path from group write node\n        fpath = instance.data[\"path\"]\n\n        # instance frame range with handles\n        first_frame = instance.data[\"frameStartHandle\"]\n        last_frame = instance.data[\"frameEndHandle\"]\n\n        # fill slate node with comments\n        self.add_comment_slate_node(instance, slate_node)\n\n        # solve output name if any is set\n        _output_name = output_name or \"\"\n        if _output_name:\n            _output_name = \"_\" + _output_name\n\n        slate_first_frame = first_frame - 1\n\n        collection = instance.data.get(\"collection\", None)\n\n        if collection:\n            # get path\n            fname = os.path.basename(collection.format(\n                \"{head}{padding}{tail}\"))\n            fhead = collection.format(\"{head}\")\n        else:\n            fname = os.path.basename(fpath)\n            fhead = os.path.splitext(fname)[0] + \".\"\n\n        if \"#\" in fhead:\n            fhead = fhead.replace(\"#\", \"\")[:-1]\n\n        self.log.debug(\"__ first_frame: {}\".format(first_frame))\n        self.log.debug(\"__ slate_first_frame: {}\".format(slate_first_frame))\n\n        above_slate_node = slate_node.dependencies().pop()\n        # fallback if files does not exists\n        if self._check_frames_exists(instance):\n            # Read node\n            r_node = nuke.createNode(\"Read\")\n            r_node[\"file\"].setValue(fpath)\n            r_node[\"first\"].setValue(first_frame)\n            r_node[\"origfirst\"].setValue(first_frame)\n            r_node[\"last\"].setValue(last_frame)\n            r_node[\"origlast\"].setValue(last_frame)\n            r_node[\"colorspace\"].setValue(instance.data[\"colorspace\"])\n            previous_node = r_node\n            temporary_nodes = [previous_node]\n\n            # adding copy metadata node for correct frame metadata\n            cm_node = nuke.createNode(\"CopyMetaData\")\n            cm_node.setInput(0, previous_node)\n            cm_node.setInput(1, above_slate_node)\n            previous_node = cm_node\n            temporary_nodes.append(cm_node)\n\n        else:\n            previous_node = above_slate_node\n            temporary_nodes = []\n\n        # only create colorspace baking if toggled on\n        if bake_viewer_process:\n            if bake_viewer_input_process:\n                # get input process and connect it to baking\n                ipn = get_view_process_node()\n                if ipn is not None:\n                    ipn.setInput(0, previous_node)\n                    previous_node = ipn\n                    temporary_nodes.append(ipn)\n\n            # add duplicate slate node and connect to previous\n            duply_slate_node = duplicate_node(slate_node)\n            duply_slate_node.setInput(0, previous_node)\n            previous_node = duply_slate_node\n            temporary_nodes.append(duply_slate_node)\n\n            # add viewer display transformation node\n            dag_node = nuke.createNode(\"OCIODisplay\")\n            dag_node.setInput(0, previous_node)\n            previous_node = dag_node\n            temporary_nodes.append(dag_node)\n\n        else:\n            # add duplicate slate node and connect to previous\n            duply_slate_node = duplicate_node(slate_node)\n            duply_slate_node.setInput(0, previous_node)\n            previous_node = duply_slate_node\n            temporary_nodes.append(duply_slate_node)\n\n        # create write node\n        write_node = nuke.createNode(\"Write\")\n        file = fhead[:-1] + _output_name + \"_slate.png\"\n        path = os.path.join(\n            instance.data[\"stagingDir\"], file).replace(\"\\\\\", \"/\")\n\n        # add slate path to `slateFrames` instance data attr\n        if not instance.data.get(\"slateFrames\"):\n            instance.data[\"slateFrames\"] = {}\n\n        instance.data[\"slateFrames\"][output_name or \"*\"] = path\n\n        # create write node\n        write_node[\"file\"].setValue(path)\n        write_node[\"file_type\"].setValue(\"png\")\n        write_node[\"raw\"].setValue(1)\n        write_node.setInput(0, previous_node)\n        temporary_nodes.append(write_node)\n\n        # Render frames\n        nuke.execute(\n            write_node.name(), int(slate_first_frame), int(slate_first_frame))\n\n        # Clean up\n        for node in temporary_nodes:\n            nuke.delete(node)\n\n    def _render_slate_to_sequence(self, instance):\n        # set slate frame\n        first_frame = instance.data[\"frameStartHandle\"]\n        last_frame = instance.data[\"frameEndHandle\"]\n        slate_first_frame = first_frame - 1\n\n        # render slate as sequence frame\n        nuke.execute(\n            instance.data[\"name\"],\n            int(slate_first_frame),\n            int(slate_first_frame)\n        )\n\n        # Add file to representation files\n        # - get write node\n        write_node = instance.data[\"transientData\"][\"writeNode\"]\n        # - evaluate filepaths for first frame and slate frame\n        first_filename = os.path.basename(\n            write_node[\"file\"].evaluate(first_frame))\n        slate_filename = os.path.basename(\n            write_node[\"file\"].evaluate(slate_first_frame))\n\n        # Find matching representation based on first filename\n        matching_repre = None\n        is_sequence = None\n        for repre in instance.data[\"representations\"]:\n            files = repre[\"files\"]\n            if (\n                not isinstance(files, six.string_types)\n                and first_filename in files\n            ):\n                matching_repre = repre\n                is_sequence = True\n                break\n\n            elif files == first_filename:\n                matching_repre = repre\n                is_sequence = False\n                break\n\n        if not matching_repre:\n            self.log.info(\n                \"Matching representation was not found.\"\n                \" Representation files were not filled with slate.\"\n            )\n            return\n\n        # Add frame to matching representation files\n        if not is_sequence:\n            matching_repre[\"files\"] = [first_filename, slate_filename]\n        elif slate_filename not in matching_repre[\"files\"]:\n            matching_repre[\"files\"].insert(0, slate_filename)\n            matching_repre[\"frameStart\"] = (\n                \"{{:0>{}}}\"\n                .format(len(str(last_frame)))\n                .format(slate_first_frame)\n            )\n            self.log.debug(\n                \"__ matching_repre: {}\".format(pformat(matching_repre)))\n\n        self.log.info(\"Added slate frame to representation files\")\n\n    def add_comment_slate_node(self, instance, node):\n\n        comment = instance.data[\"comment\"]\n        intent = instance.context.data.get(\"intent\")\n        if not isinstance(intent, dict):\n            intent = {\n                \"label\": intent,\n                \"value\": intent\n            }\n\n        fill_data = copy.deepcopy(instance.data[\"anatomyData\"])\n        fill_data.update({\n            \"custom\": copy.deepcopy(\n                instance.data.get(\"customData\") or {}\n            ),\n            \"comment\": comment,\n            \"intent\": intent\n        })\n\n        for key, _values in self.key_value_mapping.items():\n            enabled, template = _values\n            if not enabled:\n                self.log.debug(\"Key \\\"{}\\\" is disabled\".format(key))\n                continue\n\n            try:\n                value = template.format(**fill_data)\n\n            except ValueError:\n                self.log.warning(\n                    \"Couldn't fill template \\\"{}\\\" with data: {}\".format(\n                        template, fill_data\n                    ),\n                    exc_info=True\n                )\n                continue\n\n            except KeyError:\n                self.log.warning(\n                    (\n                        \"Template contains unknown key.\"\n                        \" Template \\\"{}\\\" Data: {}\"\n                    ).format(template, fill_data),\n                    exc_info=True\n                )\n                continue\n\n            try:\n                node[key].setValue(value)\n                self.log.debug(\"Change key \\\"{}\\\" to value \\\"{}\\\"\".format(\n                    key, value\n                ))\n            except NameError:\n                self.log.warning((\n                    \"Failed to set value \\\"{0}\\\" on node attribute \\\"{0}\\\"\"\n                ).format(value))\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n    <error id=\"main\">\n        <title>Shot/Asset name</title>\n        <description>\n## Publishing to a different asset context\n\nThere are publish instances present which are publishing into a different asset than your current context.\n\nUsually this is not what you want but there can be cases where you might want to publish into another asset/shot or task.\n\nIf that's the case you can disable the validation on the instance to ignore it.\n\nThe wrong node's name is: \\`{node_name}\\`\n\n### Correct context keys and values:\n\n\\`{correct_values}\\`\n\n### Wrong keys and values:\n\n\\`{wrong_values}\\`.\n\n\n## How to repair?\n\n1. Use \\\"Repair\\\" button.\n2. Hit Reload button on the publisher.\n        </description>\n    </error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/help/validate_backdrop.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n    <error id=\"multiple_outputs\">\n        <title>Found multiple outputs</title>\n        <description>\n## Invalid output amount\n\nBackdrop is having more than one outgoing connections.\n\n### How to repair?\n\n1. Use button `Center node in node graph` and navigate to the backdrop.\n2. Reorganize nodes the way only one outgoing connection is present.\n3. Hit reload button on the publisher.\n        </description>\n        <detail>\n### How could this happen?\n\nMore than one node, which are found above the backdrop, are linked downstream or more output connections from a node also linked downstream.\n        </detail>\n    </error>\n    <error id=\"no_nodes\">\n        <title>Empty backdrop</title>\n        <description>\n## Invalid empty backdrop\n\nBackdrop is empty and no nodes are found above it.\n\n### How to repair?\n\n1. Use button `Center node in node graph` and navigate to the backdrop.\n2. Add any node above it or delete it.\n3. Hit reload button on the publisher.\n        </description>\n    </error>\n</root>"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/help/validate_gizmo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n    <error id=\"multiple_outputs\">\n        <title>Found multiple outputs</title>\n        <description>\n## Invalid amount of Output nodes\n\nGroup node `{node_name}` is having more than one Output node.\n\n### How to repair?\n\n1. Use button `Open Group`.\n2. Remove redundant Output node.\n3. Hit reload button on the publisher.\n        </description>\n        <detail>\n### How could this happen?\n\nPerhaps you had created exciently more than one Output node.\n        </detail>\n    </error>\n    <error id=\"no_inputs\">\n        <title>Missing Input nodes</title>\n        <description>\n## Missing Input nodes\n\nMake sure there is at least one connected Input node inside the group node with name `{node_name}`\n\n### How to repair?\n\n1. Use button `Open Group`.\n2. Add at least one Input node and connect to other nodes.\n3. Hit reload button on the publisher.\n        </description>\n    </error>\n</root>"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/help/validate_knobs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n    <error id=\"main\">\n        <title>Knobs value</title>\n        <description>\n## Invalid node's knobs values\n\nFollowing node knobs needs to be repaired:\n\n{invalid_items}\n\n### How to repair?\n\n1. Use Repair button.\n2. Hit Reload button on the publisher.\n        </description>\n    </error>\n</root>"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/help/validate_output_resolution.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n    <error id=\"main\">\n        <title>Output format</title>\n        <description>\n## Invalid format setting\n\nEither the Reformat node inside of the render group is missing or the Reformat node output format knob is not set to `root.format`.\n\n### How to repair?\n\n1. Use Repair button.\n2. Hit Reload button on the publisher.\n        </description>\n    </error>\n</root>"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/help/validate_proxy_mode.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n    <error id=\"main\">\n        <title>Proxy mode</title>\n        <description>\n## Invalid proxy mode value\n\nNuke is set to use Proxy. This is not supported by publisher.\n\n### How to repair?\n\n1. Use Repair button.\n2. Hit Reload button on the publisher.\n        </description>\n    </error>\n</root>"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/help/validate_rendered_frames.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n    <error id=\"main\">\n        <title>Rendered Frames</title>\n        <description>\n## Missing Rendered Frames\n\nRender node \"{node_name}\" is set to \"Use existing frames\", but frames are missing.\n\n### How to repair?\n\n1. Use Repair button.\n2. Set different target.\n2. Hit Reload button on the publisher.\n        </description>\n    </error>\n</root>"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/help/validate_script_attributes.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n    <error id=\"main\">\n        <title>Script attributes</title>\n        <description>\n## Invalid Script attributes\n\nFollowing script root attributes need to be fixed:\n\n{failed_attributes}\n\n### How to repair?\n\n1. Use Repair.\n2. Hit Reload button on the publisher.\n        </description>\n    </error>\n</root>"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n    <error id=\"main\">\n        <title>Knobs values</title>\n        <description>\n            ## Invalid node's knobs values\n\n            Following write node knobs needs to be repaired:\n\n            {xml_msg}\n\n            ### How to repair?\n\n            1. Use Repair button.\n            2. Hit Reload button on the publisher.\n        </description>\n    </error>\n    <error id=\"legacy\">\n        <title>Legacy knob types</title>\n        <description>\n            ## Knobs are in obsolete configuration\n\n            Settings needs to be fixed.\n\n            ### How to repair?\n\n            Contact your supervisor or fix it in project settings at\n            'project_settings/nuke/imageio/nodes/requiredNodes' at knobs.\n            Each '__legacy__' type has to be defined accordingly to its type.\n        </description>\n    </error>\n</root>"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/increment_script_version.py",
    "content": "\nimport nuke\nimport pyblish.api\n\n\nclass IncrementScriptVersion(pyblish.api.ContextPlugin):\n    \"\"\"Increment current script version.\"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.9\n    label = \"Increment Script Version\"\n    optional = True\n    families = [\"workfile\"]\n    hosts = ['nuke']\n\n    def process(self, context):\n\n        assert all(result[\"success\"] for result in context.data[\"results\"]), (\n            \"Publishing not successful so version is not increased.\")\n\n        from openpype.lib import version_up\n        path = context.data[\"currentFile\"]\n        nuke.scriptSaveAs(version_up(path))\n        self.log.info('Incrementing script version')\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/remove_ouput_node.py",
    "content": "import nuke\nimport pyblish.api\n\n\nclass RemoveOutputNode(pyblish.api.ContextPlugin):\n    \"\"\"Removing output node for each output write node\n\n    \"\"\"\n    label = 'Output Node Remove'\n    order = pyblish.api.IntegratorOrder + 0.4\n    families = [\"workfile\"]\n    hosts = ['nuke']\n\n    def process(self, context):\n        try:\n            output_node = context.data[\"outputNode\"]\n            name = output_node[\"name\"].value()\n            self.log.info(\"Removing output node: '{}'\".format(name))\n\n            nuke.delete(output_node)\n        except Exception:\n            return\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/validate_asset_context.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Validate if instance asset is the same as context asset.\"\"\"\nfrom __future__ import absolute_import\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    RepairAction,\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.hosts.nuke.api import SelectInstanceNodeAction\n\n\nclass ValidateCorrectAssetContext(\n    pyblish.api.InstancePlugin,\n    OptionalPyblishPluginMixin\n):\n    \"\"\"Validator to check if instance asset context match context asset.\n\n    When working in per-shot style you always publish data in context of\n    current asset (shot). This validator checks if this is so. It is optional\n    so it can be disabled when needed.\n\n    Checking `asset` and `task` keys.\n    \"\"\"\n    order = ValidateContentsOrder\n    label = \"Validate asset context\"\n    hosts = [\"nuke\"]\n    actions = [\n        RepairAction,\n        SelectInstanceNodeAction\n    ]\n    optional = True\n\n    @classmethod\n    def apply_settings(cls, project_settings):\n        \"\"\"Apply deprecated settings from project settings.\n        \"\"\"\n        nuke_publish = project_settings[\"nuke\"][\"publish\"]\n        if \"ValidateCorrectAssetName\" in nuke_publish:\n            settings = nuke_publish[\"ValidateCorrectAssetName\"]\n        else:\n            settings = nuke_publish[\"ValidateCorrectAssetContext\"]\n\n        cls.enabled = settings[\"enabled\"]\n        cls.optional = settings[\"optional\"]\n        cls.active = settings[\"active\"]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid_keys = self.get_invalid(instance)\n\n        if not invalid_keys:\n            return\n\n        message_values = {\n            \"node_name\": instance.data[\"transientData\"][\"node\"].name(),\n            \"correct_values\": \", \".join([\n                \"{} > {}\".format(_key, instance.context.data[_key])\n                for _key in invalid_keys\n            ]),\n            \"wrong_values\": \", \".join([\n                \"{} > {}\".format(_key, instance.data.get(_key))\n                for _key in invalid_keys\n            ])\n        }\n\n        msg = (\n            \"Instance `{node_name}` has wrong context keys:\\n\"\n            \"Correct: `{correct_values}` | Wrong: `{wrong_values}`\").format(\n                **message_values)\n\n        self.log.debug(msg)\n\n        raise PublishXmlValidationError(\n            self, msg, formatting_data=message_values\n        )\n\n    @classmethod\n    def get_invalid(cls, instance):\n        \"\"\"Get invalid keys from instance data and context data.\"\"\"\n\n        invalid_keys = []\n        testing_keys = [\"asset\", \"task\"]\n        for _key in testing_keys:\n            if _key not in instance.data:\n                invalid_keys.append(_key)\n                continue\n            if instance.data[_key] != instance.context.data[_key]:\n                invalid_keys.append(_key)\n\n        return invalid_keys\n\n    @classmethod\n    def repair(cls, instance):\n        \"\"\"Repair instance data with context data.\"\"\"\n        invalid_keys = cls.get_invalid(instance)\n\n        create_context = instance.context.data[\"create_context\"]\n\n        instance_id = instance.data.get(\"instance_id\")\n        created_instance = create_context.get_instance_by_id(\n            instance_id\n        )\n        for _key in invalid_keys:\n            created_instance[_key] = instance.context.data[_key]\n\n        create_context.save_changes()\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/validate_backdrop.py",
    "content": "import nuke\nimport pyblish\nfrom openpype.hosts.nuke import api as napi\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin\n)\n\nclass SelectCenterInNodeGraph(pyblish.api.Action):\n    \"\"\"\n    Centering failed instance node in node grap\n    \"\"\"\n\n    label = \"Center node in node graph\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n\n        # Get the errored instances\n        failed = []\n        for result in context.data[\"results\"]:\n            if (result[\"error\"] is not None and result[\"instance\"] is not None\n               and result[\"instance\"] not in failed):\n                failed.append(result[\"instance\"])\n\n        # Apply pyblish.logic to get the instances for the plug-in\n        instances = pyblish.api.instances_by_plugin(failed, plugin)\n\n        all_xC = []\n        all_yC = []\n\n        # maintain selection\n        with napi.maintained_selection():\n            # collect all failed nodes xpos and ypos\n            for instance in instances:\n                bdn = instance.data[\"transientData\"][\"node\"]\n                xC = bdn.xpos() + bdn.screenWidth() / 2\n                yC = bdn.ypos() + bdn.screenHeight() / 2\n\n                all_xC.append(xC)\n                all_yC.append(yC)\n\n        self.log.debug(\"all_xC: `{}`\".format(all_xC))\n        self.log.debug(\"all_yC: `{}`\".format(all_yC))\n\n        # zoom to nodes in node graph\n        nuke.zoom(2, [min(all_xC), min(all_yC)])\n\n\nclass ValidateBackdrop(\n    pyblish.api.InstancePlugin,\n    OptionalPyblishPluginMixin\n):\n    \"\"\" Validate amount of nodes on backdrop node in case user\n    forgotten to add nodes above the publishing backdrop node.\n    \"\"\"\n\n    order = ValidateContentsOrder\n    optional = True\n    families = [\"nukenodes\"]\n    label = \"Validate Backdrop\"\n    hosts = [\"nuke\"]\n    actions = [SelectCenterInNodeGraph]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        child_nodes = instance.data[\"transientData\"][\"childNodes\"]\n        connections_out = instance.data[\"transientData\"][\"nodeConnectionsOut\"]\n\n        msg_multiple_outputs = (\n            \"Only one outcoming connection from \"\n            \"\\\"{}\\\" is allowed\").format(instance.data[\"name\"])\n\n        if len(connections_out.keys()) > 1:\n            raise PublishXmlValidationError(\n                self,\n                msg_multiple_outputs,\n                \"multiple_outputs\"\n            )\n\n        msg_no_nodes = \"No content on backdrop node: \\\"{}\\\"\".format(\n            instance.data[\"name\"])\n\n        self.log.debug(\n            \"Amount of nodes on instance: {}\".format(\n                len(child_nodes))\n        )\n\n        if child_nodes == []:\n            raise PublishXmlValidationError(\n                self,\n                msg_no_nodes,\n                \"no_nodes\"\n            )\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/validate_exposed_knobs.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import get_errored_instances_from_context\nfrom openpype.hosts.nuke.api.lib import link_knobs\nfrom openpype.pipeline.publish import (\n    OptionalPyblishPluginMixin,\n    PublishValidationError\n)\n\n\nclass RepairExposedKnobs(pyblish.api.Action):\n    label = \"Repair\"\n    on = \"failed\"\n    icon = \"wrench\"\n\n    def process(self, context, plugin):\n        instances = get_errored_instances_from_context(context)\n\n        for instance in instances:\n            child_nodes = (\n                instance.data.get(\"transientData\", {}).get(\"childNodes\")\n                or instance\n            )\n\n            write_group_node = instance.data[\"transientData\"][\"node\"]\n            # get write node from inside of group\n            write_node = None\n            for x in child_nodes:\n                if x.Class() == \"Write\":\n                    write_node = x\n\n            plugin_name = plugin.families_mapping[instance.data[\"family\"]]\n            nuke_settings = instance.context.data[\"project_settings\"][\"nuke\"]\n            create_settings = nuke_settings[\"create\"][plugin_name]\n            exposed_knobs = create_settings[\"exposed_knobs\"]\n            link_knobs(exposed_knobs, write_node, write_group_node)\n\n\nclass ValidateExposedKnobs(\n    OptionalPyblishPluginMixin,\n    pyblish.api.InstancePlugin\n):\n    \"\"\" Validate write node exposed knobs.\n\n    Compare exposed linked knobs to settings.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    optional = True\n    families = [\"render\", \"prerender\", \"image\"]\n    label = \"Validate Exposed Knobs\"\n    actions = [RepairExposedKnobs]\n    hosts = [\"nuke\"]\n    families_mapping = {\n        \"render\": \"CreateWriteRender\",\n        \"prerender\": \"CreateWritePrerender\",\n        \"image\": \"CreateWriteImage\"\n    }\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        plugin = self.families_mapping[instance.data[\"family\"]]\n        group_node = instance.data[\"transientData\"][\"node\"]\n        nuke_settings = instance.context.data[\"project_settings\"][\"nuke\"]\n        create_settings = nuke_settings[\"create\"][plugin]\n        exposed_knobs = create_settings.get(\"exposed_knobs\", [])\n        unexposed_knobs = []\n        for knob in exposed_knobs:\n            if knob not in group_node.knobs():\n                unexposed_knobs.append(knob)\n\n        if unexposed_knobs:\n            raise PublishValidationError(\n                \"Missing exposed knobs: {}\".format(unexposed_knobs)\n            )\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/validate_gizmo.py",
    "content": "import pyblish\nfrom openpype.pipeline import PublishXmlValidationError\nfrom openpype.hosts.nuke import api as napi\nimport nuke\n\n\nclass OpenFailedGroupNode(pyblish.api.Action):\n    \"\"\"\n    Centering failed instance node in node grap\n    \"\"\"\n\n    label = \"Open Group\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n\n        # Get the errored instances\n        failed = []\n        for result in context.data[\"results\"]:\n            if (result[\"error\"] is not None and result[\"instance\"] is not None\n                    and result[\"instance\"] not in failed):\n                failed.append(result[\"instance\"])\n\n        # Apply pyblish.logic to get the instances for the plug-in\n        instances = pyblish.api.instances_by_plugin(failed, plugin)\n\n        # maintain selection\n        with napi.maintained_selection():\n            # collect all failed nodes xpos and ypos\n            for instance in instances:\n                grpn = instance.data[\"transientData\"][\"node\"]\n                nuke.showDag(grpn)\n\n\nclass ValidateGizmo(pyblish.api.InstancePlugin):\n    \"\"\"Validate amount of output nodes in gizmo (group) node\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    optional = True\n    families = [\"gizmo\"]\n    label = \"Validate Gizmo (group)\"\n    hosts = [\"nuke\"]\n    actions = [OpenFailedGroupNode]\n\n    def process(self, instance):\n        grpn = instance.data[\"transientData\"][\"node\"]\n\n        with grpn:\n            connections_out = nuke.allNodes('Output')\n            if len(connections_out) > 1:\n                msg_multiple_outputs = (\n                    \"Only one outcoming connection from \"\n                    \"\\\"{}\\\" is allowed\").format(instance.data[\"name\"])\n\n                raise PublishXmlValidationError(\n                    self, msg_multiple_outputs, \"multiple_outputs\",\n                    {\"node_name\": grpn[\"name\"].value()}\n                )\n\n            connections_in = nuke.allNodes('Input')\n            if len(connections_in) == 0:\n                msg_missing_inputs = (\n                    \"At least one Input node has to be inside Group: \"\n                    \"\\\"{}\\\"\").format(instance.data[\"name\"])\n\n                raise PublishXmlValidationError(\n                    self, msg_missing_inputs, \"no_inputs\",\n                    {\"node_name\": grpn[\"name\"].value()}\n                )\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/validate_knobs.py",
    "content": "import nuke\nimport six\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    RepairContextAction,\n    PublishXmlValidationError,\n)\n\n\nclass ValidateKnobs(pyblish.api.ContextPlugin):\n    \"\"\"Ensure knobs are consistent.\n\n    Knobs to validate and their values comes from the\n\n    Controlled by plugin settings that require json in following structure:\n        \"ValidateKnobs\": {\n            \"enabled\": true,\n            \"knobs\": {\n                \"family\": {\n                    \"knob_name\": knob_value\n                    }\n                }\n            }\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Knobs\"\n    hosts = [\"nuke\"]\n    actions = [RepairContextAction]\n    optional = True\n\n    def process(self, context):\n        invalid = self.get_invalid(context, compute=True)\n        if invalid:\n            invalid_items = [\n                (\n                    \"Node __{node_name}__ with knob _{label}_ \"\n                    \"expecting _{expected}_, \"\n                    \"but is set to _{current}_\"\n                ).format(**i)\n                for i in invalid\n            ]\n            raise PublishXmlValidationError(\n                self,\n                \"Found knobs with invalid values:\\n{}\".format(invalid),\n                formatting_data={\n                    \"invalid_items\": \"\\n\".join(invalid_items)}\n            )\n\n    @classmethod\n    def get_invalid(cls, context, compute=False):\n        invalid = context.data.get(\"invalid_knobs\", [])\n        if compute:\n            invalid = cls.get_invalid_knobs(context)\n\n        return invalid\n\n    @classmethod\n    def get_invalid_knobs(cls, context):\n        invalid_knobs = []\n\n        for instance in context:\n\n            # Filter families.\n            families = [instance.data[\"family\"]]\n            families += instance.data.get(\"families\", [])\n\n            # Get all knobs to validate.\n            knobs = {}\n            for family in families:\n                # check if dot in family\n                if \".\" in family:\n                    family = family.split(\".\")[0]\n\n                # avoid families not in settings\n                if family not in cls.knobs:\n                    continue\n\n                # get presets of knobs\n                for preset in cls.knobs[family]:\n                    knobs[preset] = cls.knobs[family][preset]\n\n            # Get invalid knobs.\n            nodes = []\n\n            for node in nuke.allNodes():\n                nodes.append(node)\n                if node.Class() == \"Group\":\n                    node.begin()\n                    nodes.extend(iter(nuke.allNodes()))\n                    node.end()\n\n            for node in nodes:\n                for knob in node.knobs():\n                    if knob not in knobs.keys():\n                        continue\n\n                    expected = knobs[knob]\n                    if node[knob].value() != expected:\n                        invalid_knobs.append(\n                            {\n                                \"node_name\": node.name(),\n                                \"knob\": node[knob],\n                                \"name\": node[knob].name(),\n                                \"label\": node[knob].label(),\n                                \"expected\": expected,\n                                \"current\": node[knob].value()\n                            }\n                        )\n\n        context.data[\"invalid_knobs\"] = invalid_knobs\n        return invalid_knobs\n\n    @classmethod\n    def repair(cls, instance):\n        invalid = cls.get_invalid(instance)\n        for data in invalid:\n            # TODO: will need to improve type definitions\n            # with the new settings for knob types\n            if isinstance(data[\"expected\"], six.text_type):\n                data[\"knob\"].setValue(str(data[\"expected\"]))\n                continue\n\n            data[\"knob\"].setValue(data[\"expected\"])\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/validate_output_resolution.py",
    "content": "import pyblish.api\n\nfrom openpype.hosts.nuke import api as napi\nfrom openpype.pipeline.publish import RepairAction\nfrom openpype.pipeline import (\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin\n)\n\nimport nuke\n\n\nclass ValidateOutputResolution(\n    OptionalPyblishPluginMixin,\n    pyblish.api.InstancePlugin\n):\n    \"\"\"Validates Output Resolution.\n\n    It is making sure the resolution of write's input is the same as\n    Format definition of script in Root node.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    optional = True\n    families = [\"render\"]\n    label = \"Validate Write resolution\"\n    hosts = [\"nuke\"]\n    actions = [RepairAction]\n\n    missing_msg = \"Missing Reformat node in render group node\"\n    resolution_msg = \"Reformat is set to wrong format\"\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        invalid = self.get_invalid(instance)\n        if invalid:\n            raise PublishXmlValidationError(self, invalid)\n\n    @classmethod\n    def get_reformat(cls, instance):\n        child_nodes = (\n            instance.data.get(\"transientData\", {}).get(\"childNodes\")\n            or instance\n        )\n\n        reformat = None\n        for inode in child_nodes:\n            if inode.Class() != \"Reformat\":\n                continue\n            reformat = inode\n\n        return reformat\n\n    @classmethod\n    def get_invalid(cls, instance):\n        def _check_resolution(instance, reformat):\n            root_width = instance.data[\"resolutionWidth\"]\n            root_height = instance.data[\"resolutionHeight\"]\n\n            write_width = reformat.format().width()\n            write_height = reformat.format().height()\n\n            if (root_width != write_width) or (root_height != write_height):\n                return None\n            else:\n                return True\n\n        # check if reformat is in render node\n        reformat = cls.get_reformat(instance)\n        if not reformat:\n            return cls.missing_msg\n\n        # check if reformat is set to correct root format\n        correct_format = _check_resolution(instance, reformat)\n        if not correct_format:\n            return cls.resolution_msg\n\n    @classmethod\n    def repair(cls, instance):\n        child_nodes = (\n            instance.data.get(\"transientData\", {}).get(\"childNodes\")\n            or instance\n        )\n\n        invalid = cls.get_invalid(instance)\n        grp_node = instance.data[\"transientData\"][\"node\"]\n\n        if cls.missing_msg == invalid:\n            # make sure we are inside of the group node\n            with grp_node:\n                # find input node and select it\n                _input = None\n                for inode in child_nodes:\n                    if inode.Class() != \"Input\":\n                        continue\n                    _input = inode\n\n                # add reformat node under it\n                with napi.maintained_selection():\n                    _input['selected'].setValue(True)\n                    _rfn = nuke.createNode(\"Reformat\", \"name Reformat01\")\n                    _rfn[\"resize\"].setValue(0)\n                    _rfn[\"black_outside\"].setValue(1)\n\n                cls.log.info(\"Adding reformat node\")\n\n        if cls.resolution_msg == invalid:\n            reformat = cls.get_reformat(instance)\n            reformat[\"format\"].setValue(nuke.root()[\"format\"].value())\n            cls.log.info(\"Fixing reformat to root.format\")\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py",
    "content": "import pyblish\nimport nuke\nfrom openpype.pipeline import PublishXmlValidationError\n\n\nclass FixProxyMode(pyblish.api.Action):\n    \"\"\"\n    Togger off proxy switch OFF\n    \"\"\"\n\n    label = \"Repair\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n        rootNode = nuke.root()\n        rootNode[\"proxy\"].setValue(False)\n\n\nclass ValidateProxyMode(pyblish.api.ContextPlugin):\n    \"\"\"Validate active proxy mode\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Proxy Mode\"\n    hosts = [\"nuke\"]\n    actions = [FixProxyMode]\n\n    def process(self, context):\n\n        rootNode = nuke.root()\n        isProxy = rootNode[\"proxy\"].value()\n\n        if isProxy:\n            raise PublishXmlValidationError(\n                self, \"Proxy mode should be toggled OFF\"\n            )\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py",
    "content": "import os\nimport pyblish.api\nimport clique\nfrom openpype.pipeline import PublishXmlValidationError\nfrom openpype.pipeline.publish import get_errored_instances_from_context\n\n\nclass RepairActionBase(pyblish.api.Action):\n    on = \"failed\"\n    icon = \"wrench\"\n\n    @staticmethod\n    def get_instance(context, plugin):\n        # Get the errored instances\n        return get_errored_instances_from_context(context, plugin=plugin)\n\n    def repair_knob(self, context, instances, state):\n        create_context = context.data[\"create_context\"]\n        for instance in instances:\n            # Reset the render knob\n            instance_id = instance.data.get(\"instance_id\")\n            created_instance = create_context.get_instance_by_id(\n                instance_id\n            )\n            created_instance.creator_attributes[\"render_target\"] = state\n            self.log.info(\"Rendering toggled to `{}`\".format(state))\n\n        create_context.save_changes()\n\n\nclass RepairCollectionActionToLocal(RepairActionBase):\n    label = \"Repair - rerender with \\\"Local\\\"\"\n\n    def process(self, context, plugin):\n        instances = self.get_instance(context, plugin)\n        self.repair_knob(context, instances, \"local\")\n\n\nclass RepairCollectionActionToFarm(RepairActionBase):\n    label = \"Repair - rerender with \\\"On farm\\\"\"\n\n    def process(self, context, plugin):\n        instances = self.get_instance(context, plugin)\n        self.repair_knob(context, instances, \"farm\")\n\n\nclass ValidateRenderedFrames(pyblish.api.InstancePlugin):\n    \"\"\" Validates file output. \"\"\"\n\n    order = pyblish.api.ValidatorOrder + 0.1\n    families = [\"render\", \"prerender\", \"still\"]\n\n    label = \"Validate rendered frame\"\n    hosts = [\"nuke\", \"nukestudio\"]\n    actions = [RepairCollectionActionToLocal, RepairCollectionActionToFarm]\n\n    def process(self, instance):\n        node = instance.data[\"transientData\"][\"node\"]\n\n        f_data = {\n            \"node_name\": node.name()\n        }\n\n        for repre in instance.data[\"representations\"]:\n\n            if not repre.get(\"files\"):\n                msg = (\"no frames were collected, \"\n                       \"you need to render them.\\n\"\n                       \"Check properties of write node (group) and\"\n                       \"select 'Local' option in 'Publish' dropdown.\")\n                self.log.error(msg)\n                raise PublishXmlValidationError(\n                    self, msg, formatting_data=f_data)\n\n            if isinstance(repre[\"files\"], str):\n                return\n\n            collections, remainder = clique.assemble(repre[\"files\"])\n            self.log.debug(\"collections: {}\".format(str(collections)))\n            self.log.debug(\"remainder: {}\".format(str(remainder)))\n\n            collection = collections[0]\n\n            f_start_h = instance.data[\"frameStartHandle\"]\n            f_end_h = instance.data[\"frameEndHandle\"]\n\n            frame_length = int(f_end_h - f_start_h + 1)\n\n            if frame_length != 1:\n                if len(collections) != 1:\n                    msg = \"There are multiple collections in the folder\"\n                    self.log.error(msg)\n                    raise PublishXmlValidationError(\n                        self, msg, formatting_data=f_data)\n\n                if not collection.is_contiguous():\n                    msg = \"Some frames appear to be missing\"\n                    self.log.error(msg)\n                    raise PublishXmlValidationError(\n                        self, msg, formatting_data=f_data)\n\n            collected_frames_len = len(collection.indexes)\n            coll_start = min(collection.indexes)\n            coll_end = max(collection.indexes)\n\n            self.log.debug(\"frame_length: {}\".format(frame_length))\n            self.log.debug(\"collected_frames_len: {}\".format(\n                collected_frames_len))\n            self.log.debug(\"f_start_h-f_end_h: {}-{}\".format(\n                f_start_h, f_end_h))\n            self.log.debug(\n                \"coll_start-coll_end: {}-{}\".format(coll_start, coll_end))\n\n            self.log.debug(\n                \"len(collection.indexes): {}\".format(collected_frames_len)\n            )\n\n            if (\"slate\" in instance.data[\"families\"]) \\\n                    and (frame_length != collected_frames_len):\n                collected_frames_len -= 1\n                f_start_h += 1\n\n            if (\n                collected_frames_len != frame_length\n                and coll_start <= f_start_h\n                and coll_end >= f_end_h\n            ):\n                raise PublishXmlValidationError(\n                    self, (\n                        \"{} missing frames. Use repair to \"\n                        \"render all frames\"\n                    ).format(__name__), formatting_data=f_data\n                )\n\n            instance.data[\"collection\"] = collection\n\n            return\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/validate_script_attributes.py",
    "content": "from copy import deepcopy\nimport pyblish.api\nfrom openpype.pipeline import (\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.pipeline.publish import RepairAction\nfrom openpype.hosts.nuke.api.lib import (\n    WorkfileSettings\n)\n\n\nclass ValidateScriptAttributes(\n    OptionalPyblishPluginMixin,\n    pyblish.api.InstancePlugin\n):\n    \"\"\" Validates file output. \"\"\"\n\n    order = pyblish.api.ValidatorOrder + 0.1\n    families = [\"workfile\"]\n    label = \"Validate script attributes\"\n    hosts = [\"nuke\"]\n    optional = True\n    actions = [RepairAction]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        script_data = deepcopy(instance.context.data[\"scriptData\"])\n\n        asset = instance.data[\"assetEntity\"]\n\n        # These attributes will be checked\n        attributes = [\n            \"fps\",\n            \"frameStart\",\n            \"frameEnd\",\n            \"resolutionWidth\",\n            \"resolutionHeight\",\n            \"handleStart\",\n            \"handleEnd\"\n        ]\n\n        # get only defined attributes from asset data\n        asset_attributes = {\n            attr: asset[\"data\"][attr]\n            for attr in attributes\n            if attr in asset[\"data\"]\n        }\n        # fix frame values to include handles\n        asset_attributes[\"fps\"] = float(\"{0:.4f}\".format(\n            asset_attributes[\"fps\"]))\n        script_data[\"fps\"] = float(\"{0:.4f}\".format(\n            script_data[\"fps\"]))\n\n        # Compare asset's values Nukescript X Database\n        not_matching = []\n        for attr in attributes:\n            self.log.debug(\n                \"Asset vs Script attribute \\\"{}\\\": {}, {}\".format(\n                    attr,\n                    asset_attributes[attr],\n                    script_data[attr]\n                )\n            )\n            if asset_attributes[attr] != script_data[attr]:\n                not_matching.append({\n                    \"name\": attr,\n                    \"expected\": asset_attributes[attr],\n                    \"actual\": script_data[attr]\n                })\n\n        # Raise error if not matching\n        if not_matching:\n            msg = \"Following attributes are not set correctly: \\n{}\"\n            attrs_wrong_str = \"\\n\".join([\n                (\n                    \"`{0}` is set to `{1}`, \"\n                    \"but should be set to `{2}`\"\n                ).format(at[\"name\"], at[\"actual\"], at[\"expected\"])\n                for at in not_matching\n            ])\n            attrs_wrong_html = \"<br/>\".join([\n                (\n                    \"-- __{0}__ is set to __{1}__, \"\n                    \"but should be set to __{2}__\"\n                ).format(at[\"name\"], at[\"actual\"], at[\"expected\"])\n                for at in not_matching\n            ])\n            raise PublishXmlValidationError(\n                self, msg.format(attrs_wrong_str),\n                formatting_data={\n                    \"failed_attributes\": attrs_wrong_html\n                }\n            )\n\n    @classmethod\n    def repair(cls, instance):\n        cls.log.debug(\"__ repairing instance: {}\".format(instance))\n        WorkfileSettings().set_context_settings()\n"
  },
  {
    "path": "openpype/hosts/nuke/plugins/publish/validate_write_nodes.py",
    "content": "from collections import defaultdict\n\nimport pyblish.api\nfrom openpype.pipeline.publish import get_errored_instances_from_context\nfrom openpype.hosts.nuke.api.lib import (\n    get_write_node_template_attr,\n    set_node_knobs_from_settings,\n    color_gui_to_int\n)\n\nfrom openpype.pipeline.publish import (\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass RepairNukeWriteNodeAction(pyblish.api.Action):\n    label = \"Repair\"\n    on = \"failed\"\n    icon = \"wrench\"\n\n    def process(self, context, plugin):\n        instances = get_errored_instances_from_context(context)\n\n        for instance in instances:\n            child_nodes = (\n                instance.data.get(\"transientData\", {}).get(\"childNodes\")\n                or instance\n            )\n\n            write_group_node = instance.data[\"transientData\"][\"node\"]\n            # get write node from inside of group\n            write_node = None\n            for x in child_nodes:\n                if x.Class() == \"Write\":\n                    write_node = x\n\n            correct_data = get_write_node_template_attr(write_group_node)\n\n            set_node_knobs_from_settings(write_node, correct_data[\"knobs\"])\n\n            self.log.debug(\"Node attributes were fixed\")\n\n\nclass ValidateNukeWriteNode(\n    OptionalPyblishPluginMixin,\n    pyblish.api.InstancePlugin\n):\n    \"\"\" Validate Write node's knobs.\n\n    Compare knobs on write node inside the render group\n    with settings. At the moment supporting only `file` knob.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    optional = True\n    families = [\"render\"]\n    label = \"Validate write node\"\n    actions = [RepairNukeWriteNodeAction]\n    hosts = [\"nuke\"]\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        child_nodes = (\n            instance.data.get(\"transientData\", {}).get(\"childNodes\")\n            or instance\n        )\n\n        write_group_node = instance.data[\"transientData\"][\"node\"]\n\n        # get write node from inside of group\n        write_node = None\n        for x in child_nodes:\n            if x.Class() == \"Write\":\n                write_node = x\n\n        if write_node is None:\n            return\n\n        correct_data = get_write_node_template_attr(write_group_node)\n\n        check = []\n\n        # Collect key values of same type in a list.\n        values_by_name = defaultdict(list)\n        for knob_data in correct_data[\"knobs\"]:\n            values_by_name[knob_data[\"name\"]].append(knob_data[\"value\"])\n\n        for knob_data in correct_data[\"knobs\"]:\n            knob_type = knob_data[\"type\"]\n\n            if (\n                knob_type == \"__legacy__\"\n            ):\n                raise PublishXmlValidationError(\n                    self, (\n                        \"Please update data in settings 'project_settings\"\n                        \"/nuke/imageio/nodes/requiredNodes'\"\n                    ),\n                    key=\"legacy\"\n                )\n\n            key = knob_data[\"name\"]\n            values = values_by_name[key]\n            node_value = write_node[key].value()\n\n            # fix type differences\n            fixed_values = []\n            for value in values:\n                if type(node_value) in (int, float):\n                    try:\n                        if isinstance(value, list):\n                            value = color_gui_to_int(value)\n                        else:\n                            value = float(value)\n                            node_value = float(node_value)\n                    except ValueError:\n                        value = str(value)\n                else:\n                    value = str(value)\n                    node_value = str(node_value)\n\n                fixed_values.append(value)\n\n            if (\n                node_value not in fixed_values\n                and key != \"file\"\n                and key != \"tile_color\"\n            ):\n                check.append([key, fixed_values, write_node[key].value()])\n\n        if check:\n            self._make_error(check)\n\n    def _make_error(self, check):\n        # sourcery skip: merge-assign-and-aug-assign, move-assign-in-block\n        dbg_msg = \"Write node's knobs values are not correct!\\n\"\n        msg_add = \"Knob '{0}' > Expected: `{1}` > Current: `{2}`\"\n\n        details = [\n            msg_add.format(item[0], item[1], item[2])\n            for item in check\n        ]\n        xml_msg = \"<br/>\".join(details)\n        dbg_msg += \"\\n\\t\".join(details)\n\n        raise PublishXmlValidationError(\n            self, dbg_msg, formatting_data={\"xml_msg\": xml_msg}\n        )\n"
  },
  {
    "path": "openpype/hosts/nuke/startup/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/nuke/startup/clear_rendered.py",
    "content": "import os\n\nfrom openpype.lib import Logger\n\n\ndef clear_rendered(dir_path):\n    log = Logger.get_logger(__name__)\n\n    for _f in os.listdir(dir_path):\n        _f_path = os.path.join(dir_path, _f)\n        log.info(\"Removing: `{}`\".format(_f_path))\n        os.remove(_f_path)\n"
  },
  {
    "path": "openpype/hosts/nuke/startup/custom_write_node.py",
    "content": "\"\"\" OpenPype custom script for setting up write nodes for non-publish \"\"\"\nimport os\nimport nuke\nimport nukescripts\nfrom openpype.pipeline import Anatomy\nfrom openpype.hosts.nuke.api.lib import (\n    set_node_knobs_from_settings,\n    get_nuke_imageio_settings\n)\n\n\ntemp_rendering_path_template = (\n    \"{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}\")\n\nknobs_setting = {\n    \"knobs\": [\n        {\n            \"type\": \"text\",\n            \"name\": \"file_type\",\n            \"value\": \"exr\"\n        },\n        {\n            \"type\": \"text\",\n            \"name\": \"datatype\",\n            \"value\": \"16 bit half\"\n        },\n        {\n            \"type\": \"text\",\n            \"name\": \"compression\",\n            \"value\": \"Zip (1 scanline)\"\n        },\n        {\n            \"type\": \"bool\",\n            \"name\": \"autocrop\",\n            \"value\": True\n        },\n        {\n            \"type\": \"color_gui\",\n            \"name\": \"tile_color\",\n            \"value\": [\n                186,\n                35,\n                35,\n                255\n            ]\n        },\n        {\n            \"type\": \"text\",\n            \"name\": \"channels\",\n            \"value\": \"rgb\"\n        },\n        {\n            \"type\": \"bool\",\n            \"name\": \"create_directories\",\n            \"value\": True\n        }\n    ]\n}\n\n\nclass WriteNodeKnobSettingPanel(nukescripts.PythonPanel):\n    \"\"\" Write Node's Knobs Settings Panel \"\"\"\n    def __init__(self):\n        nukescripts.PythonPanel.__init__(self, \"Set Knobs Value(Write Node)\")\n\n        preset_name, _ = self.get_node_knobs_setting()\n        # create knobs\n\n        self.selected_preset_name = nuke.Enumeration_Knob(\n            'preset_selector', 'presets', preset_name)\n        # add knobs to panel\n        self.addKnob(self.selected_preset_name)\n\n    def process(self):\n        \"\"\" Process the panel values. \"\"\"\n        write_selected_nodes = [\n            selected_nodes for selected_nodes in nuke.selectedNodes()\n            if selected_nodes.Class() == \"Write\"]\n\n        selected_preset = self.selected_preset_name.value()\n        ext = None\n        knobs = knobs_setting[\"knobs\"]\n        preset_name, node_knobs_presets = (\n            self.get_node_knobs_setting(selected_preset)\n        )\n\n        if selected_preset and preset_name:\n            if not node_knobs_presets:\n                nuke.message(\n                    \"No knobs value found in subset group..\"\n                    \"\\nDefault setting will be used..\")\n            else:\n                knobs = node_knobs_presets\n\n        ext_knob_list = [knob for knob in knobs if knob[\"name\"] == \"file_type\"]\n        if not ext_knob_list:\n            nuke.message(\n                \"ERROR: No file type found in the subset's knobs.\"\n                \"\\nPlease add one to complete setting up the node\")\n            return\n        else:\n            for knob in ext_knob_list:\n                ext = knob[\"value\"]\n\n        anatomy = Anatomy()\n\n        frame_padding = int(\n            anatomy.templates[\"render\"].get(\n                \"frame_padding\"\n            )\n        )\n        for write_node in write_selected_nodes:\n            # data for mapping the path\n            data = {\n                \"work\": os.getenv(\"AVALON_WORKDIR\"),\n                \"subset\": write_node[\"name\"].value(),\n                \"frame\": \"#\" * frame_padding,\n                \"ext\": ext\n            }\n            file_path = temp_rendering_path_template.format(**data)\n            file_path = file_path.replace(\"\\\\\", \"/\")\n            write_node[\"file\"].setValue(file_path)\n            set_node_knobs_from_settings(write_node, knobs)\n\n    def get_node_knobs_setting(self, selected_preset=None):\n        preset_name = []\n        knobs_nodes = []\n        settings = [\n            node_settings for node_settings\n            in get_nuke_imageio_settings()[\"nodes\"][\"overrideNodes\"]\n            if node_settings[\"nukeNodeClass\"] == \"Write\"\n            and node_settings[\"subsets\"]\n        ]\n        if not settings:\n            return\n\n        for i, _ in enumerate(settings):\n            if selected_preset in settings[i][\"subsets\"]:\n                knobs_nodes = settings[i][\"knobs\"]\n\n        for setting in settings:\n            for subset in setting[\"subsets\"]:\n                preset_name.append(subset)\n\n        return preset_name, knobs_nodes\n\n\ndef main():\n    p_ = WriteNodeKnobSettingPanel()\n    if p_.showModalDialog():\n        print(p_.process())\n"
  },
  {
    "path": "openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py",
    "content": "\"\"\" OpenPype custom script for resetting read nodes start frame values \"\"\"\n\nimport nuke\nimport nukescripts\n\n\nclass FrameSettingsPanel(nukescripts.PythonPanel):\n    \"\"\" Frame Settings Panel \"\"\"\n    def __init__(self):\n        nukescripts.PythonPanel.__init__(self, \"Set Frame Start (Read Node)\")\n\n        # create knobs\n        self.frame = nuke.Int_Knob(\n            'frame', 'Frame Number')\n        self.selected = nuke.Boolean_Knob(\"selection\")\n        # add knobs to panel\n        self.addKnob(self.selected)\n        self.addKnob(self.frame)\n\n        # set values\n        self.selected.setValue(False)\n        self.frame.setValue(nuke.root().firstFrame())\n\n    def process(self):\n        \"\"\" Process the panel values. \"\"\"\n        # get values\n        frame = self.frame.value()\n        if self.selected.value():\n            # selected nodes processing\n            if not nuke.selectedNodes():\n                return\n            for rn_ in nuke.selectedNodes():\n                if rn_.Class() != \"Read\":\n                    continue\n                rn_[\"frame_mode\"].setValue(\"start_at\")\n                rn_[\"frame\"].setValue(str(frame))\n        else:\n            # all nodes processing\n            for rn_ in nuke.allNodes(filter=\"Read\"):\n                rn_[\"frame_mode\"].setValue(\"start_at\")\n                rn_[\"frame\"].setValue(str(frame))\n\n\ndef main():\n    p_ = FrameSettingsPanel()\n    if p_.showModalDialog():\n        print(p_.process())\n"
  },
  {
    "path": "openpype/hosts/nuke/startup/menu.py",
    "content": "from openpype.pipeline import install_host\nfrom openpype.hosts.nuke.api import NukeHost\n\nhost = NukeHost()\ninstall_host(host)\n"
  },
  {
    "path": "openpype/hosts/nuke/startup/write_to_read.py",
    "content": "import re\nimport os\nimport glob\nimport nuke\nfrom openpype.lib import Logger\nlog = Logger.get_logger(__name__)\n\nSINGLE_FILE_FORMATS = ['avi', 'mp4', 'mxf', 'mov', 'mpg', 'mpeg', 'wmv', 'm4v',\n                       'm2v']\n\n\ndef evaluate_filepath_new(\n        k_value, k_eval, project_dir, first_frame, allow_relative):\n\n    # get combined relative path\n    combined_relative_path = None\n    if k_eval is not None and project_dir is not None:\n        combined_relative_path = os.path.abspath(\n            os.path.join(project_dir, k_eval))\n        combined_relative_path = combined_relative_path.replace('\\\\', '/')\n        filetype = combined_relative_path.split('.')[-1]\n        frame_number = re.findall(r'\\d+', combined_relative_path)[-1]\n        basename = combined_relative_path[: combined_relative_path.rfind(\n            frame_number)]\n        filepath_glob = basename + '*' + filetype\n        glob_search_results = glob.glob(filepath_glob)\n        if len(glob_search_results) <= 0:\n            combined_relative_path = None\n\n    try:\n        # k_value = k_value % first_frame\n        if os.path.isdir(os.path.basename(k_value)):\n            # doesn't check for file, only parent dir\n            filepath = k_value\n        elif os.path.exists(k_eval):\n            filepath = k_eval\n        elif not isinstance(project_dir, type(None)) and \\\n                not isinstance(combined_relative_path, type(None)):\n            filepath = combined_relative_path\n\n        filepath = os.path.abspath(filepath)\n    except Exception as E:\n        log.error(\"Cannot create Read node. Perhaps it needs to be \\\n                  rendered first :) Error: `{}`\".format(E))\n        return None\n\n    filepath = filepath.replace('\\\\', '/')\n    # assumes last number is a sequence counter\n    current_frame = re.findall(r'\\d+', filepath)[-1]\n    padding = len(current_frame)\n    basename = filepath[: filepath.rfind(current_frame)]\n    filetype = filepath.split('.')[-1]\n\n    # sequence or not?\n    if filetype in SINGLE_FILE_FORMATS:\n        pass\n    else:\n        # Image sequence needs hashes\n        # to do still with no number not handled\n        filepath = basename + '#' * padding + '.' + filetype\n\n    # relative path? make it relative again\n    if allow_relative:\n        if (not isinstance(project_dir, type(None))) and project_dir != \"\":\n            filepath = filepath.replace(project_dir, '.')\n\n    # get first and last frame from disk\n    frames = []\n    firstframe = 0\n    lastframe = 0\n    filepath_glob = basename + '*' + filetype\n    glob_search_results = glob.glob(filepath_glob)\n    for f in glob_search_results:\n        frame = re.findall(r'\\d+', f)[-1]\n        frames.append(frame)\n    frames = sorted(frames)\n    firstframe = frames[0]\n    lastframe = frames[len(frames) - 1]\n\n    if int(lastframe) < 0:\n        lastframe = firstframe\n\n    return filepath, firstframe, lastframe\n\n\ndef create_read_node(ndata, comp_start):\n    read = nuke.createNode('Read', 'file \"' + ndata['filepath'] + '\"')\n    read.knob('colorspace').setValue(int(ndata['colorspace']))\n    read.knob('raw').setValue(ndata['rawdata'])\n    read.knob('first').setValue(int(ndata['firstframe']))\n    read.knob('last').setValue(int(ndata['lastframe']))\n    read.knob('origfirst').setValue(int(ndata['firstframe']))\n    read.knob('origlast').setValue(int(ndata['lastframe']))\n    if comp_start == int(ndata['firstframe']):\n        read.knob('frame_mode').setValue(\"1\")\n        read.knob('frame').setValue(str(comp_start))\n    else:\n        read.knob('frame_mode').setValue(\"0\")\n    read.knob('xpos').setValue(ndata['new_xpos'])\n    read.knob('ypos').setValue(ndata['new_ypos'])\n    nuke.inputs(read, 0)\n    return\n\n\ndef write_to_read(gn,\n                  allow_relative=False):\n\n    comp_start = nuke.Root().knob('first_frame').value()\n    project_dir = nuke.Root().knob('project_directory').getValue()\n    if not os.path.exists(project_dir):\n        project_dir = nuke.Root().knob('project_directory').evaluate()\n\n    group_read_nodes = []\n    with gn:\n        height = gn.screenHeight()  # get group height and position\n        new_xpos = int(gn.knob('xpos').value())\n        new_ypos = int(gn.knob('ypos').value()) + height + 20\n        group_writes = [n for n in nuke.allNodes() if n.Class() == \"Write\"]\n        if group_writes != []:\n            # there can be only 1 write node, taking first\n            n = group_writes[0]\n\n            if n.knob('file') is not None:\n                myfile, firstFrame, lastFrame = evaluate_filepath_new(\n                    n.knob('file').getValue(),\n                    n.knob('file').evaluate(),\n                    project_dir,\n                    comp_start,\n                    allow_relative\n                )\n                if not myfile:\n                    return\n\n                # get node data\n                ndata = {\n                    'filepath': myfile,\n                    'firstframe': int(firstFrame),\n                    'lastframe': int(lastFrame),\n                    'new_xpos': new_xpos,\n                    'new_ypos': new_ypos,\n                    'colorspace': n.knob('colorspace').getValue(),\n                    'rawdata': n.knob('raw').value(),\n                    'write_frame_mode': str(n.knob('frame_mode').value()),\n                    'write_frame': n.knob('frame').value()\n                }\n                group_read_nodes.append(ndata)\n\n    # create reads in one go\n    for oneread in group_read_nodes:\n        # create read node\n        create_read_node(oneread, comp_start)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/__init__.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n# Copyright 2007 Google Inc. All Rights Reserved.\n\n__version__ = '3.20.1'\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/any_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/any.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x19google/protobuf/any.proto\\x12\\x0fgoogle.protobuf\\\"&\\n\\x03\\x41ny\\x12\\x10\\n\\x08type_url\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x0c\\x42v\\n\\x13\\x63om.google.protobufB\\x08\\x41nyProtoP\\x01Z,google.golang.org/protobuf/types/known/anypb\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.any_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\010AnyProtoP\\001Z,google.golang.org/protobuf/types/known/anypb\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _ANY._serialized_start=46\n  _ANY._serialized_end=84\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/api_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/api.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2\nfrom google.protobuf import type_pb2 as google_dot_protobuf_dot_type__pb2\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x19google/protobuf/api.proto\\x12\\x0fgoogle.protobuf\\x1a$google/protobuf/source_context.proto\\x1a\\x1agoogle/protobuf/type.proto\\\"\\x81\\x02\\n\\x03\\x41pi\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12(\\n\\x07methods\\x18\\x02 \\x03(\\x0b\\x32\\x17.google.protobuf.Method\\x12(\\n\\x07options\\x18\\x03 \\x03(\\x0b\\x32\\x17.google.protobuf.Option\\x12\\x0f\\n\\x07version\\x18\\x04 \\x01(\\t\\x12\\x36\\n\\x0esource_context\\x18\\x05 \\x01(\\x0b\\x32\\x1e.google.protobuf.SourceContext\\x12&\\n\\x06mixins\\x18\\x06 \\x03(\\x0b\\x32\\x16.google.protobuf.Mixin\\x12\\'\\n\\x06syntax\\x18\\x07 \\x01(\\x0e\\x32\\x17.google.protobuf.Syntax\\\"\\xd5\\x01\\n\\x06Method\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x18\\n\\x10request_type_url\\x18\\x02 \\x01(\\t\\x12\\x19\\n\\x11request_streaming\\x18\\x03 \\x01(\\x08\\x12\\x19\\n\\x11response_type_url\\x18\\x04 \\x01(\\t\\x12\\x1a\\n\\x12response_streaming\\x18\\x05 \\x01(\\x08\\x12(\\n\\x07options\\x18\\x06 \\x03(\\x0b\\x32\\x17.google.protobuf.Option\\x12\\'\\n\\x06syntax\\x18\\x07 \\x01(\\x0e\\x32\\x17.google.protobuf.Syntax\\\"#\\n\\x05Mixin\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0c\\n\\x04root\\x18\\x02 \\x01(\\tBv\\n\\x13\\x63om.google.protobufB\\x08\\x41piProtoP\\x01Z,google.golang.org/protobuf/types/known/apipb\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.api_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\010ApiProtoP\\001Z,google.golang.org/protobuf/types/known/apipb\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _API._serialized_start=113\n  _API._serialized_end=370\n  _METHOD._serialized_start=373\n  _METHOD._serialized_end=586\n  _MIXIN._serialized_start=588\n  _MIXIN._serialized_end=623\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/compiler/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/compiler/plugin_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/compiler/plugin.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n%google/protobuf/compiler/plugin.proto\\x12\\x18google.protobuf.compiler\\x1a google/protobuf/descriptor.proto\\\"F\\n\\x07Version\\x12\\r\\n\\x05major\\x18\\x01 \\x01(\\x05\\x12\\r\\n\\x05minor\\x18\\x02 \\x01(\\x05\\x12\\r\\n\\x05patch\\x18\\x03 \\x01(\\x05\\x12\\x0e\\n\\x06suffix\\x18\\x04 \\x01(\\t\\\"\\xba\\x01\\n\\x14\\x43odeGeneratorRequest\\x12\\x18\\n\\x10\\x66ile_to_generate\\x18\\x01 \\x03(\\t\\x12\\x11\\n\\tparameter\\x18\\x02 \\x01(\\t\\x12\\x38\\n\\nproto_file\\x18\\x0f \\x03(\\x0b\\x32$.google.protobuf.FileDescriptorProto\\x12;\\n\\x10\\x63ompiler_version\\x18\\x03 \\x01(\\x0b\\x32!.google.protobuf.compiler.Version\\\"\\xc1\\x02\\n\\x15\\x43odeGeneratorResponse\\x12\\r\\n\\x05\\x65rror\\x18\\x01 \\x01(\\t\\x12\\x1a\\n\\x12supported_features\\x18\\x02 \\x01(\\x04\\x12\\x42\\n\\x04\\x66ile\\x18\\x0f \\x03(\\x0b\\x32\\x34.google.protobuf.compiler.CodeGeneratorResponse.File\\x1a\\x7f\\n\\x04\\x46ile\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x17\\n\\x0finsertion_point\\x18\\x02 \\x01(\\t\\x12\\x0f\\n\\x07\\x63ontent\\x18\\x0f \\x01(\\t\\x12?\\n\\x13generated_code_info\\x18\\x10 \\x01(\\x0b\\x32\\\".google.protobuf.GeneratedCodeInfo\\\"8\\n\\x07\\x46\\x65\\x61ture\\x12\\x10\\n\\x0c\\x46\\x45\\x41TURE_NONE\\x10\\x00\\x12\\x1b\\n\\x17\\x46\\x45\\x41TURE_PROTO3_OPTIONAL\\x10\\x01\\x42W\\n\\x1c\\x63om.google.protobuf.compilerB\\x0cPluginProtosZ)google.golang.org/protobuf/types/pluginpb')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.compiler.plugin_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\034com.google.protobuf.compilerB\\014PluginProtosZ)google.golang.org/protobuf/types/pluginpb'\n  _VERSION._serialized_start=101\n  _VERSION._serialized_end=171\n  _CODEGENERATORREQUEST._serialized_start=174\n  _CODEGENERATORREQUEST._serialized_end=360\n  _CODEGENERATORRESPONSE._serialized_start=363\n  _CODEGENERATORRESPONSE._serialized_end=684\n  _CODEGENERATORRESPONSE_FILE._serialized_start=499\n  _CODEGENERATORRESPONSE_FILE._serialized_end=626\n  _CODEGENERATORRESPONSE_FEATURE._serialized_start=628\n  _CODEGENERATORRESPONSE_FEATURE._serialized_end=684\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/descriptor.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Descriptors essentially contain exactly the information found in a .proto\nfile, in types that make this information accessible in Python.\n\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\nimport threading\nimport warnings\n\nfrom google.protobuf.internal import api_implementation\n\n_USE_C_DESCRIPTORS = False\nif api_implementation.Type() == 'cpp':\n  # Used by MakeDescriptor in cpp mode\n  import binascii\n  import os\n  from google.protobuf.pyext import _message\n  _USE_C_DESCRIPTORS = True\n\n\nclass Error(Exception):\n  \"\"\"Base error for this module.\"\"\"\n\n\nclass TypeTransformationError(Error):\n  \"\"\"Error transforming between python proto type and corresponding C++ type.\"\"\"\n\n\nif _USE_C_DESCRIPTORS:\n  # This metaclass allows to override the behavior of code like\n  #     isinstance(my_descriptor, FieldDescriptor)\n  # and make it return True when the descriptor is an instance of the extension\n  # type written in C++.\n  class DescriptorMetaclass(type):\n    def __instancecheck__(cls, obj):\n      if super(DescriptorMetaclass, cls).__instancecheck__(obj):\n        return True\n      if isinstance(obj, cls._C_DESCRIPTOR_CLASS):\n        return True\n      return False\nelse:\n  # The standard metaclass; nothing changes.\n  DescriptorMetaclass = type\n\n\nclass _Lock(object):\n  \"\"\"Wrapper class of threading.Lock(), which is allowed by 'with'.\"\"\"\n\n  def __new__(cls):\n    self = object.__new__(cls)\n    self._lock = threading.Lock()  # pylint: disable=protected-access\n    return self\n\n  def __enter__(self):\n    self._lock.acquire()\n\n  def __exit__(self, exc_type, exc_value, exc_tb):\n    self._lock.release()\n\n\n_lock = threading.Lock()\n\n\ndef _Deprecated(name):\n  if _Deprecated.count > 0:\n    _Deprecated.count -= 1\n    warnings.warn(\n        'Call to deprecated create function %s(). Note: Create unlinked '\n        'descriptors is going to go away. Please use get/find descriptors from '\n        'generated code or query the descriptor_pool.'\n        % name,\n        category=DeprecationWarning, stacklevel=3)\n\n\n# Deprecated warnings will print 100 times at most which should be enough for\n# users to notice and do not cause timeout.\n_Deprecated.count = 100\n\n\n_internal_create_key = object()\n\n\nclass DescriptorBase(metaclass=DescriptorMetaclass):\n\n  \"\"\"Descriptors base class.\n\n  This class is the base of all descriptor classes. It provides common options\n  related functionality.\n\n  Attributes:\n    has_options:  True if the descriptor has non-default options.  Usually it\n        is not necessary to read this -- just call GetOptions() which will\n        happily return the default instance.  However, it's sometimes useful\n        for efficiency, and also useful inside the protobuf implementation to\n        avoid some bootstrapping issues.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    # The class, or tuple of classes, that are considered as \"virtual\n    # subclasses\" of this descriptor class.\n    _C_DESCRIPTOR_CLASS = ()\n\n  def __init__(self, options, serialized_options, options_class_name):\n    \"\"\"Initialize the descriptor given its options message and the name of the\n    class of the options message. The name of the class is required in case\n    the options message is None and has to be created.\n    \"\"\"\n    self._options = options\n    self._options_class_name = options_class_name\n    self._serialized_options = serialized_options\n\n    # Does this descriptor have non-default options?\n    self.has_options = (options is not None) or (serialized_options is not None)\n\n  def _SetOptions(self, options, options_class_name):\n    \"\"\"Sets the descriptor's options\n\n    This function is used in generated proto2 files to update descriptor\n    options. It must not be used outside proto2.\n    \"\"\"\n    self._options = options\n    self._options_class_name = options_class_name\n\n    # Does this descriptor have non-default options?\n    self.has_options = options is not None\n\n  def GetOptions(self):\n    \"\"\"Retrieves descriptor options.\n\n    This method returns the options set or creates the default options for the\n    descriptor.\n    \"\"\"\n    if self._options:\n      return self._options\n\n    from google.protobuf import descriptor_pb2\n    try:\n      options_class = getattr(descriptor_pb2,\n                              self._options_class_name)\n    except AttributeError:\n      raise RuntimeError('Unknown options class name %s!' %\n                         (self._options_class_name))\n\n    with _lock:\n      if self._serialized_options is None:\n        self._options = options_class()\n      else:\n        self._options = _ParseOptions(options_class(),\n                                      self._serialized_options)\n\n      return self._options\n\n\nclass _NestedDescriptorBase(DescriptorBase):\n  \"\"\"Common class for descriptors that can be nested.\"\"\"\n\n  def __init__(self, options, options_class_name, name, full_name,\n               file, containing_type, serialized_start=None,\n               serialized_end=None, serialized_options=None):\n    \"\"\"Constructor.\n\n    Args:\n      options: Protocol message options or None\n        to use default message options.\n      options_class_name (str): The class name of the above options.\n      name (str): Name of this protocol message type.\n      full_name (str): Fully-qualified name of this protocol message type,\n        which will include protocol \"package\" name and the name of any\n        enclosing types.\n      file (FileDescriptor): Reference to file info.\n      containing_type: if provided, this is a nested descriptor, with this\n        descriptor as parent, otherwise None.\n      serialized_start: The start index (inclusive) in block in the\n        file.serialized_pb that describes this descriptor.\n      serialized_end: The end index (exclusive) in block in the\n        file.serialized_pb that describes this descriptor.\n      serialized_options: Protocol message serialized options or None.\n    \"\"\"\n    super(_NestedDescriptorBase, self).__init__(\n        options, serialized_options, options_class_name)\n\n    self.name = name\n    # TODO(falk): Add function to calculate full_name instead of having it in\n    #             memory?\n    self.full_name = full_name\n    self.file = file\n    self.containing_type = containing_type\n\n    self._serialized_start = serialized_start\n    self._serialized_end = serialized_end\n\n  def CopyToProto(self, proto):\n    \"\"\"Copies this to the matching proto in descriptor_pb2.\n\n    Args:\n      proto: An empty proto instance from descriptor_pb2.\n\n    Raises:\n      Error: If self couldn't be serialized, due to to few constructor\n        arguments.\n    \"\"\"\n    if (self.file is not None and\n        self._serialized_start is not None and\n        self._serialized_end is not None):\n      proto.ParseFromString(self.file.serialized_pb[\n          self._serialized_start:self._serialized_end])\n    else:\n      raise Error('Descriptor does not contain serialization.')\n\n\nclass Descriptor(_NestedDescriptorBase):\n\n  \"\"\"Descriptor for a protocol message type.\n\n  Attributes:\n      name (str): Name of this protocol message type.\n      full_name (str): Fully-qualified name of this protocol message type,\n          which will include protocol \"package\" name and the name of any\n          enclosing types.\n      containing_type (Descriptor): Reference to the descriptor of the type\n          containing us, or None if this is top-level.\n      fields (list[FieldDescriptor]): Field descriptors for all fields in\n          this type.\n      fields_by_number (dict(int, FieldDescriptor)): Same\n          :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed\n          by \"number\" attribute in each FieldDescriptor.\n      fields_by_name (dict(str, FieldDescriptor)): Same\n          :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed by\n          \"name\" attribute in each :class:`FieldDescriptor`.\n      nested_types (list[Descriptor]): Descriptor references\n          for all protocol message types nested within this one.\n      nested_types_by_name (dict(str, Descriptor)): Same Descriptor\n          objects as in :attr:`nested_types`, but indexed by \"name\" attribute\n          in each Descriptor.\n      enum_types (list[EnumDescriptor]): :class:`EnumDescriptor` references\n          for all enums contained within this type.\n      enum_types_by_name (dict(str, EnumDescriptor)): Same\n          :class:`EnumDescriptor` objects as in :attr:`enum_types`, but\n          indexed by \"name\" attribute in each EnumDescriptor.\n      enum_values_by_name (dict(str, EnumValueDescriptor)): Dict mapping\n          from enum value name to :class:`EnumValueDescriptor` for that value.\n      extensions (list[FieldDescriptor]): All extensions defined directly\n          within this message type (NOT within a nested type).\n      extensions_by_name (dict(str, FieldDescriptor)): Same FieldDescriptor\n          objects as :attr:`extensions`, but indexed by \"name\" attribute of each\n          FieldDescriptor.\n      is_extendable (bool):  Does this type define any extension ranges?\n      oneofs (list[OneofDescriptor]): The list of descriptors for oneof fields\n          in this message.\n      oneofs_by_name (dict(str, OneofDescriptor)): Same objects as in\n          :attr:`oneofs`, but indexed by \"name\" attribute.\n      file (FileDescriptor): Reference to file descriptor.\n\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.Descriptor\n\n    def __new__(\n        cls,\n        name=None,\n        full_name=None,\n        filename=None,\n        containing_type=None,\n        fields=None,\n        nested_types=None,\n        enum_types=None,\n        extensions=None,\n        options=None,\n        serialized_options=None,\n        is_extendable=True,\n        extension_ranges=None,\n        oneofs=None,\n        file=None,  # pylint: disable=redefined-builtin\n        serialized_start=None,\n        serialized_end=None,\n        syntax=None,\n        create_key=None):\n      _message.Message._CheckCalledFromGeneratedFile()\n      return _message.default_pool.FindMessageTypeByName(full_name)\n\n  # NOTE(tmarek): The file argument redefining a builtin is nothing we can\n  # fix right now since we don't know how many clients already rely on the\n  # name of the argument.\n  def __init__(self, name, full_name, filename, containing_type, fields,\n               nested_types, enum_types, extensions, options=None,\n               serialized_options=None,\n               is_extendable=True, extension_ranges=None, oneofs=None,\n               file=None, serialized_start=None, serialized_end=None,  # pylint: disable=redefined-builtin\n               syntax=None, create_key=None):\n    \"\"\"Arguments to __init__() are as described in the description\n    of Descriptor fields above.\n\n    Note that filename is an obsolete argument, that is not used anymore.\n    Please use file.name to access this as an attribute.\n    \"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('Descriptor')\n\n    super(Descriptor, self).__init__(\n        options, 'MessageOptions', name, full_name, file,\n        containing_type, serialized_start=serialized_start,\n        serialized_end=serialized_end, serialized_options=serialized_options)\n\n    # We have fields in addition to fields_by_name and fields_by_number,\n    # so that:\n    #   1. Clients can index fields by \"order in which they're listed.\"\n    #   2. Clients can easily iterate over all fields with the terse\n    #      syntax: for f in descriptor.fields: ...\n    self.fields = fields\n    for field in self.fields:\n      field.containing_type = self\n    self.fields_by_number = dict((f.number, f) for f in fields)\n    self.fields_by_name = dict((f.name, f) for f in fields)\n    self._fields_by_camelcase_name = None\n\n    self.nested_types = nested_types\n    for nested_type in nested_types:\n      nested_type.containing_type = self\n    self.nested_types_by_name = dict((t.name, t) for t in nested_types)\n\n    self.enum_types = enum_types\n    for enum_type in self.enum_types:\n      enum_type.containing_type = self\n    self.enum_types_by_name = dict((t.name, t) for t in enum_types)\n    self.enum_values_by_name = dict(\n        (v.name, v) for t in enum_types for v in t.values)\n\n    self.extensions = extensions\n    for extension in self.extensions:\n      extension.extension_scope = self\n    self.extensions_by_name = dict((f.name, f) for f in extensions)\n    self.is_extendable = is_extendable\n    self.extension_ranges = extension_ranges\n    self.oneofs = oneofs if oneofs is not None else []\n    self.oneofs_by_name = dict((o.name, o) for o in self.oneofs)\n    for oneof in self.oneofs:\n      oneof.containing_type = self\n    self.syntax = syntax or \"proto2\"\n\n  @property\n  def fields_by_camelcase_name(self):\n    \"\"\"Same FieldDescriptor objects as in :attr:`fields`, but indexed by\n    :attr:`FieldDescriptor.camelcase_name`.\n    \"\"\"\n    if self._fields_by_camelcase_name is None:\n      self._fields_by_camelcase_name = dict(\n          (f.camelcase_name, f) for f in self.fields)\n    return self._fields_by_camelcase_name\n\n  def EnumValueName(self, enum, value):\n    \"\"\"Returns the string name of an enum value.\n\n    This is just a small helper method to simplify a common operation.\n\n    Args:\n      enum: string name of the Enum.\n      value: int, value of the enum.\n\n    Returns:\n      string name of the enum value.\n\n    Raises:\n      KeyError if either the Enum doesn't exist or the value is not a valid\n        value for the enum.\n    \"\"\"\n    return self.enum_types_by_name[enum].values_by_number[value].name\n\n  def CopyToProto(self, proto):\n    \"\"\"Copies this to a descriptor_pb2.DescriptorProto.\n\n    Args:\n      proto: An empty descriptor_pb2.DescriptorProto.\n    \"\"\"\n    # This function is overridden to give a better doc comment.\n    super(Descriptor, self).CopyToProto(proto)\n\n\n# TODO(robinson): We should have aggressive checking here,\n# for example:\n#   * If you specify a repeated field, you should not be allowed\n#     to specify a default value.\n#   * [Other examples here as needed].\n#\n# TODO(robinson): for this and other *Descriptor classes, we\n# might also want to lock things down aggressively (e.g.,\n# prevent clients from setting the attributes).  Having\n# stronger invariants here in general will reduce the number\n# of runtime checks we must do in reflection.py...\nclass FieldDescriptor(DescriptorBase):\n\n  \"\"\"Descriptor for a single field in a .proto file.\n\n  Attributes:\n    name (str): Name of this field, exactly as it appears in .proto.\n    full_name (str): Name of this field, including containing scope.  This is\n      particularly relevant for extensions.\n    index (int): Dense, 0-indexed index giving the order that this\n      field textually appears within its message in the .proto file.\n    number (int): Tag number declared for this field in the .proto file.\n\n    type (int): (One of the TYPE_* constants below) Declared type.\n    cpp_type (int): (One of the CPPTYPE_* constants below) C++ type used to\n      represent this field.\n\n    label (int): (One of the LABEL_* constants below) Tells whether this\n      field is optional, required, or repeated.\n    has_default_value (bool): True if this field has a default value defined,\n      otherwise false.\n    default_value (Varies): Default value of this field.  Only\n      meaningful for non-repeated scalar fields.  Repeated fields\n      should always set this to [], and non-repeated composite\n      fields should always set this to None.\n\n    containing_type (Descriptor): Descriptor of the protocol message\n      type that contains this field.  Set by the Descriptor constructor\n      if we're passed into one.\n      Somewhat confusingly, for extension fields, this is the\n      descriptor of the EXTENDED message, not the descriptor\n      of the message containing this field.  (See is_extension and\n      extension_scope below).\n    message_type (Descriptor): If a composite field, a descriptor\n      of the message type contained in this field.  Otherwise, this is None.\n    enum_type (EnumDescriptor): If this field contains an enum, a\n      descriptor of that enum.  Otherwise, this is None.\n\n    is_extension: True iff this describes an extension field.\n    extension_scope (Descriptor): Only meaningful if is_extension is True.\n      Gives the message that immediately contains this extension field.\n      Will be None iff we're a top-level (file-level) extension field.\n\n    options (descriptor_pb2.FieldOptions): Protocol message field options or\n      None to use default field options.\n\n    containing_oneof (OneofDescriptor): If the field is a member of a oneof\n      union, contains its descriptor. Otherwise, None.\n\n    file (FileDescriptor): Reference to file descriptor.\n  \"\"\"\n\n  # Must be consistent with C++ FieldDescriptor::Type enum in\n  # descriptor.h.\n  #\n  # TODO(robinson): Find a way to eliminate this repetition.\n  TYPE_DOUBLE         = 1\n  TYPE_FLOAT          = 2\n  TYPE_INT64          = 3\n  TYPE_UINT64         = 4\n  TYPE_INT32          = 5\n  TYPE_FIXED64        = 6\n  TYPE_FIXED32        = 7\n  TYPE_BOOL           = 8\n  TYPE_STRING         = 9\n  TYPE_GROUP          = 10\n  TYPE_MESSAGE        = 11\n  TYPE_BYTES          = 12\n  TYPE_UINT32         = 13\n  TYPE_ENUM           = 14\n  TYPE_SFIXED32       = 15\n  TYPE_SFIXED64       = 16\n  TYPE_SINT32         = 17\n  TYPE_SINT64         = 18\n  MAX_TYPE            = 18\n\n  # Must be consistent with C++ FieldDescriptor::CppType enum in\n  # descriptor.h.\n  #\n  # TODO(robinson): Find a way to eliminate this repetition.\n  CPPTYPE_INT32       = 1\n  CPPTYPE_INT64       = 2\n  CPPTYPE_UINT32      = 3\n  CPPTYPE_UINT64      = 4\n  CPPTYPE_DOUBLE      = 5\n  CPPTYPE_FLOAT       = 6\n  CPPTYPE_BOOL        = 7\n  CPPTYPE_ENUM        = 8\n  CPPTYPE_STRING      = 9\n  CPPTYPE_MESSAGE     = 10\n  MAX_CPPTYPE         = 10\n\n  _PYTHON_TO_CPP_PROTO_TYPE_MAP = {\n      TYPE_DOUBLE: CPPTYPE_DOUBLE,\n      TYPE_FLOAT: CPPTYPE_FLOAT,\n      TYPE_ENUM: CPPTYPE_ENUM,\n      TYPE_INT64: CPPTYPE_INT64,\n      TYPE_SINT64: CPPTYPE_INT64,\n      TYPE_SFIXED64: CPPTYPE_INT64,\n      TYPE_UINT64: CPPTYPE_UINT64,\n      TYPE_FIXED64: CPPTYPE_UINT64,\n      TYPE_INT32: CPPTYPE_INT32,\n      TYPE_SFIXED32: CPPTYPE_INT32,\n      TYPE_SINT32: CPPTYPE_INT32,\n      TYPE_UINT32: CPPTYPE_UINT32,\n      TYPE_FIXED32: CPPTYPE_UINT32,\n      TYPE_BYTES: CPPTYPE_STRING,\n      TYPE_STRING: CPPTYPE_STRING,\n      TYPE_BOOL: CPPTYPE_BOOL,\n      TYPE_MESSAGE: CPPTYPE_MESSAGE,\n      TYPE_GROUP: CPPTYPE_MESSAGE\n      }\n\n  # Must be consistent with C++ FieldDescriptor::Label enum in\n  # descriptor.h.\n  #\n  # TODO(robinson): Find a way to eliminate this repetition.\n  LABEL_OPTIONAL      = 1\n  LABEL_REQUIRED      = 2\n  LABEL_REPEATED      = 3\n  MAX_LABEL           = 3\n\n  # Must be consistent with C++ constants kMaxNumber, kFirstReservedNumber,\n  # and kLastReservedNumber in descriptor.h\n  MAX_FIELD_NUMBER = (1 << 29) - 1\n  FIRST_RESERVED_FIELD_NUMBER = 19000\n  LAST_RESERVED_FIELD_NUMBER = 19999\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.FieldDescriptor\n\n    def __new__(cls, name, full_name, index, number, type, cpp_type, label,\n                default_value, message_type, enum_type, containing_type,\n                is_extension, extension_scope, options=None,\n                serialized_options=None,\n                has_default_value=True, containing_oneof=None, json_name=None,\n                file=None, create_key=None):  # pylint: disable=redefined-builtin\n      _message.Message._CheckCalledFromGeneratedFile()\n      if is_extension:\n        return _message.default_pool.FindExtensionByName(full_name)\n      else:\n        return _message.default_pool.FindFieldByName(full_name)\n\n  def __init__(self, name, full_name, index, number, type, cpp_type, label,\n               default_value, message_type, enum_type, containing_type,\n               is_extension, extension_scope, options=None,\n               serialized_options=None,\n               has_default_value=True, containing_oneof=None, json_name=None,\n               file=None, create_key=None):  # pylint: disable=redefined-builtin\n    \"\"\"The arguments are as described in the description of FieldDescriptor\n    attributes above.\n\n    Note that containing_type may be None, and may be set later if necessary\n    (to deal with circular references between message types, for example).\n    Likewise for extension_scope.\n    \"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('FieldDescriptor')\n\n    super(FieldDescriptor, self).__init__(\n        options, serialized_options, 'FieldOptions')\n    self.name = name\n    self.full_name = full_name\n    self.file = file\n    self._camelcase_name = None\n    if json_name is None:\n      self.json_name = _ToJsonName(name)\n    else:\n      self.json_name = json_name\n    self.index = index\n    self.number = number\n    self.type = type\n    self.cpp_type = cpp_type\n    self.label = label\n    self.has_default_value = has_default_value\n    self.default_value = default_value\n    self.containing_type = containing_type\n    self.message_type = message_type\n    self.enum_type = enum_type\n    self.is_extension = is_extension\n    self.extension_scope = extension_scope\n    self.containing_oneof = containing_oneof\n    if api_implementation.Type() == 'cpp':\n      if is_extension:\n        self._cdescriptor = _message.default_pool.FindExtensionByName(full_name)\n      else:\n        self._cdescriptor = _message.default_pool.FindFieldByName(full_name)\n    else:\n      self._cdescriptor = None\n\n  @property\n  def camelcase_name(self):\n    \"\"\"Camelcase name of this field.\n\n    Returns:\n      str: the name in CamelCase.\n    \"\"\"\n    if self._camelcase_name is None:\n      self._camelcase_name = _ToCamelCase(self.name)\n    return self._camelcase_name\n\n  @property\n  def has_presence(self):\n    \"\"\"Whether the field distinguishes between unpopulated and default values.\n\n    Raises:\n      RuntimeError: singular field that is not linked with message nor file.\n    \"\"\"\n    if self.label == FieldDescriptor.LABEL_REPEATED:\n      return False\n    if (self.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE or\n        self.containing_oneof):\n      return True\n    if hasattr(self.file, 'syntax'):\n      return self.file.syntax == 'proto2'\n    if hasattr(self.message_type, 'syntax'):\n      return self.message_type.syntax == 'proto2'\n    raise RuntimeError(\n        'has_presence is not ready to use because field %s is not'\n        ' linked with message type nor file' % self.full_name)\n\n  @staticmethod\n  def ProtoTypeToCppProtoType(proto_type):\n    \"\"\"Converts from a Python proto type to a C++ Proto Type.\n\n    The Python ProtocolBuffer classes specify both the 'Python' datatype and the\n    'C++' datatype - and they're not the same. This helper method should\n    translate from one to another.\n\n    Args:\n      proto_type: the Python proto type (descriptor.FieldDescriptor.TYPE_*)\n    Returns:\n      int: descriptor.FieldDescriptor.CPPTYPE_*, the C++ type.\n    Raises:\n      TypeTransformationError: when the Python proto type isn't known.\n    \"\"\"\n    try:\n      return FieldDescriptor._PYTHON_TO_CPP_PROTO_TYPE_MAP[proto_type]\n    except KeyError:\n      raise TypeTransformationError('Unknown proto_type: %s' % proto_type)\n\n\nclass EnumDescriptor(_NestedDescriptorBase):\n\n  \"\"\"Descriptor for an enum defined in a .proto file.\n\n  Attributes:\n    name (str): Name of the enum type.\n    full_name (str): Full name of the type, including package name\n      and any enclosing type(s).\n\n    values (list[EnumValueDescriptor]): List of the values\n      in this enum.\n    values_by_name (dict(str, EnumValueDescriptor)): Same as :attr:`values`,\n      but indexed by the \"name\" field of each EnumValueDescriptor.\n    values_by_number (dict(int, EnumValueDescriptor)): Same as :attr:`values`,\n      but indexed by the \"number\" field of each EnumValueDescriptor.\n    containing_type (Descriptor): Descriptor of the immediate containing\n      type of this enum, or None if this is an enum defined at the\n      top level in a .proto file.  Set by Descriptor's constructor\n      if we're passed into one.\n    file (FileDescriptor): Reference to file descriptor.\n    options (descriptor_pb2.EnumOptions): Enum options message or\n      None to use default enum options.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.EnumDescriptor\n\n    def __new__(cls, name, full_name, filename, values,\n                containing_type=None, options=None,\n                serialized_options=None, file=None,  # pylint: disable=redefined-builtin\n                serialized_start=None, serialized_end=None, create_key=None):\n      _message.Message._CheckCalledFromGeneratedFile()\n      return _message.default_pool.FindEnumTypeByName(full_name)\n\n  def __init__(self, name, full_name, filename, values,\n               containing_type=None, options=None,\n               serialized_options=None, file=None,  # pylint: disable=redefined-builtin\n               serialized_start=None, serialized_end=None, create_key=None):\n    \"\"\"Arguments are as described in the attribute description above.\n\n    Note that filename is an obsolete argument, that is not used anymore.\n    Please use file.name to access this as an attribute.\n    \"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('EnumDescriptor')\n\n    super(EnumDescriptor, self).__init__(\n        options, 'EnumOptions', name, full_name, file,\n        containing_type, serialized_start=serialized_start,\n        serialized_end=serialized_end, serialized_options=serialized_options)\n\n    self.values = values\n    for value in self.values:\n      value.type = self\n    self.values_by_name = dict((v.name, v) for v in values)\n    # Values are reversed to ensure that the first alias is retained.\n    self.values_by_number = dict((v.number, v) for v in reversed(values))\n\n  def CopyToProto(self, proto):\n    \"\"\"Copies this to a descriptor_pb2.EnumDescriptorProto.\n\n    Args:\n      proto (descriptor_pb2.EnumDescriptorProto): An empty descriptor proto.\n    \"\"\"\n    # This function is overridden to give a better doc comment.\n    super(EnumDescriptor, self).CopyToProto(proto)\n\n\nclass EnumValueDescriptor(DescriptorBase):\n\n  \"\"\"Descriptor for a single value within an enum.\n\n  Attributes:\n    name (str): Name of this value.\n    index (int): Dense, 0-indexed index giving the order that this\n      value appears textually within its enum in the .proto file.\n    number (int): Actual number assigned to this enum value.\n    type (EnumDescriptor): :class:`EnumDescriptor` to which this value\n      belongs.  Set by :class:`EnumDescriptor`'s constructor if we're\n      passed into one.\n    options (descriptor_pb2.EnumValueOptions): Enum value options message or\n      None to use default enum value options options.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.EnumValueDescriptor\n\n    def __new__(cls, name, index, number,\n                type=None,  # pylint: disable=redefined-builtin\n                options=None, serialized_options=None, create_key=None):\n      _message.Message._CheckCalledFromGeneratedFile()\n      # There is no way we can build a complete EnumValueDescriptor with the\n      # given parameters (the name of the Enum is not known, for example).\n      # Fortunately generated files just pass it to the EnumDescriptor()\n      # constructor, which will ignore it, so returning None is good enough.\n      return None\n\n  def __init__(self, name, index, number,\n               type=None,  # pylint: disable=redefined-builtin\n               options=None, serialized_options=None, create_key=None):\n    \"\"\"Arguments are as described in the attribute description above.\"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('EnumValueDescriptor')\n\n    super(EnumValueDescriptor, self).__init__(\n        options, serialized_options, 'EnumValueOptions')\n    self.name = name\n    self.index = index\n    self.number = number\n    self.type = type\n\n\nclass OneofDescriptor(DescriptorBase):\n  \"\"\"Descriptor for a oneof field.\n\n  Attributes:\n    name (str): Name of the oneof field.\n    full_name (str): Full name of the oneof field, including package name.\n    index (int): 0-based index giving the order of the oneof field inside\n      its containing type.\n    containing_type (Descriptor): :class:`Descriptor` of the protocol message\n      type that contains this field.  Set by the :class:`Descriptor` constructor\n      if we're passed into one.\n    fields (list[FieldDescriptor]): The list of field descriptors this\n      oneof can contain.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.OneofDescriptor\n\n    def __new__(\n        cls, name, full_name, index, containing_type, fields, options=None,\n        serialized_options=None, create_key=None):\n      _message.Message._CheckCalledFromGeneratedFile()\n      return _message.default_pool.FindOneofByName(full_name)\n\n  def __init__(\n      self, name, full_name, index, containing_type, fields, options=None,\n      serialized_options=None, create_key=None):\n    \"\"\"Arguments are as described in the attribute description above.\"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('OneofDescriptor')\n\n    super(OneofDescriptor, self).__init__(\n        options, serialized_options, 'OneofOptions')\n    self.name = name\n    self.full_name = full_name\n    self.index = index\n    self.containing_type = containing_type\n    self.fields = fields\n\n\nclass ServiceDescriptor(_NestedDescriptorBase):\n\n  \"\"\"Descriptor for a service.\n\n  Attributes:\n    name (str): Name of the service.\n    full_name (str): Full name of the service, including package name.\n    index (int): 0-indexed index giving the order that this services\n      definition appears within the .proto file.\n    methods (list[MethodDescriptor]): List of methods provided by this\n      service.\n    methods_by_name (dict(str, MethodDescriptor)): Same\n      :class:`MethodDescriptor` objects as in :attr:`methods_by_name`, but\n      indexed by \"name\" attribute in each :class:`MethodDescriptor`.\n    options (descriptor_pb2.ServiceOptions): Service options message or\n      None to use default service options.\n    file (FileDescriptor): Reference to file info.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.ServiceDescriptor\n\n    def __new__(\n        cls,\n        name=None,\n        full_name=None,\n        index=None,\n        methods=None,\n        options=None,\n        serialized_options=None,\n        file=None,  # pylint: disable=redefined-builtin\n        serialized_start=None,\n        serialized_end=None,\n        create_key=None):\n      _message.Message._CheckCalledFromGeneratedFile()  # pylint: disable=protected-access\n      return _message.default_pool.FindServiceByName(full_name)\n\n  def __init__(self, name, full_name, index, methods, options=None,\n               serialized_options=None, file=None,  # pylint: disable=redefined-builtin\n               serialized_start=None, serialized_end=None, create_key=None):\n    if create_key is not _internal_create_key:\n      _Deprecated('ServiceDescriptor')\n\n    super(ServiceDescriptor, self).__init__(\n        options, 'ServiceOptions', name, full_name, file,\n        None, serialized_start=serialized_start,\n        serialized_end=serialized_end, serialized_options=serialized_options)\n    self.index = index\n    self.methods = methods\n    self.methods_by_name = dict((m.name, m) for m in methods)\n    # Set the containing service for each method in this service.\n    for method in self.methods:\n      method.containing_service = self\n\n  def FindMethodByName(self, name):\n    \"\"\"Searches for the specified method, and returns its descriptor.\n\n    Args:\n      name (str): Name of the method.\n    Returns:\n      MethodDescriptor or None: the descriptor for the requested method, if\n      found.\n    \"\"\"\n    return self.methods_by_name.get(name, None)\n\n  def CopyToProto(self, proto):\n    \"\"\"Copies this to a descriptor_pb2.ServiceDescriptorProto.\n\n    Args:\n      proto (descriptor_pb2.ServiceDescriptorProto): An empty descriptor proto.\n    \"\"\"\n    # This function is overridden to give a better doc comment.\n    super(ServiceDescriptor, self).CopyToProto(proto)\n\n\nclass MethodDescriptor(DescriptorBase):\n\n  \"\"\"Descriptor for a method in a service.\n\n  Attributes:\n    name (str): Name of the method within the service.\n    full_name (str): Full name of method.\n    index (int): 0-indexed index of the method inside the service.\n    containing_service (ServiceDescriptor): The service that contains this\n      method.\n    input_type (Descriptor): The descriptor of the message that this method\n      accepts.\n    output_type (Descriptor): The descriptor of the message that this method\n      returns.\n    client_streaming (bool): Whether this method uses client streaming.\n    server_streaming (bool): Whether this method uses server streaming.\n    options (descriptor_pb2.MethodOptions or None): Method options message, or\n      None to use default method options.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.MethodDescriptor\n\n    def __new__(cls,\n                name,\n                full_name,\n                index,\n                containing_service,\n                input_type,\n                output_type,\n                client_streaming=False,\n                server_streaming=False,\n                options=None,\n                serialized_options=None,\n                create_key=None):\n      _message.Message._CheckCalledFromGeneratedFile()  # pylint: disable=protected-access\n      return _message.default_pool.FindMethodByName(full_name)\n\n  def __init__(self,\n               name,\n               full_name,\n               index,\n               containing_service,\n               input_type,\n               output_type,\n               client_streaming=False,\n               server_streaming=False,\n               options=None,\n               serialized_options=None,\n               create_key=None):\n    \"\"\"The arguments are as described in the description of MethodDescriptor\n    attributes above.\n\n    Note that containing_service may be None, and may be set later if necessary.\n    \"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('MethodDescriptor')\n\n    super(MethodDescriptor, self).__init__(\n        options, serialized_options, 'MethodOptions')\n    self.name = name\n    self.full_name = full_name\n    self.index = index\n    self.containing_service = containing_service\n    self.input_type = input_type\n    self.output_type = output_type\n    self.client_streaming = client_streaming\n    self.server_streaming = server_streaming\n\n  def CopyToProto(self, proto):\n    \"\"\"Copies this to a descriptor_pb2.MethodDescriptorProto.\n\n    Args:\n      proto (descriptor_pb2.MethodDescriptorProto): An empty descriptor proto.\n\n    Raises:\n      Error: If self couldn't be serialized, due to too few constructor\n        arguments.\n    \"\"\"\n    if self.containing_service is not None:\n      from google.protobuf import descriptor_pb2\n      service_proto = descriptor_pb2.ServiceDescriptorProto()\n      self.containing_service.CopyToProto(service_proto)\n      proto.CopyFrom(service_proto.method[self.index])\n    else:\n      raise Error('Descriptor does not contain a service.')\n\n\nclass FileDescriptor(DescriptorBase):\n  \"\"\"Descriptor for a file. Mimics the descriptor_pb2.FileDescriptorProto.\n\n  Note that :attr:`enum_types_by_name`, :attr:`extensions_by_name`, and\n  :attr:`dependencies` fields are only set by the\n  :py:mod:`google.protobuf.message_factory` module, and not by the generated\n  proto code.\n\n  Attributes:\n    name (str): Name of file, relative to root of source tree.\n    package (str): Name of the package\n    syntax (str): string indicating syntax of the file (can be \"proto2\" or\n      \"proto3\")\n    serialized_pb (bytes): Byte string of serialized\n      :class:`descriptor_pb2.FileDescriptorProto`.\n    dependencies (list[FileDescriptor]): List of other :class:`FileDescriptor`\n      objects this :class:`FileDescriptor` depends on.\n    public_dependencies (list[FileDescriptor]): A subset of\n      :attr:`dependencies`, which were declared as \"public\".\n    message_types_by_name (dict(str, Descriptor)): Mapping from message names\n      to their :class:`Descriptor`.\n    enum_types_by_name (dict(str, EnumDescriptor)): Mapping from enum names to\n      their :class:`EnumDescriptor`.\n    extensions_by_name (dict(str, FieldDescriptor)): Mapping from extension\n      names declared at file scope to their :class:`FieldDescriptor`.\n    services_by_name (dict(str, ServiceDescriptor)): Mapping from services'\n      names to their :class:`ServiceDescriptor`.\n    pool (DescriptorPool): The pool this descriptor belongs to.  When not\n      passed to the constructor, the global default pool is used.\n  \"\"\"\n\n  if _USE_C_DESCRIPTORS:\n    _C_DESCRIPTOR_CLASS = _message.FileDescriptor\n\n    def __new__(cls, name, package, options=None,\n                serialized_options=None, serialized_pb=None,\n                dependencies=None, public_dependencies=None,\n                syntax=None, pool=None, create_key=None):\n      # FileDescriptor() is called from various places, not only from generated\n      # files, to register dynamic proto files and messages.\n      # pylint: disable=g-explicit-bool-comparison\n      if serialized_pb == b'':\n        # Cpp generated code must be linked in if serialized_pb is ''\n        try:\n          return _message.default_pool.FindFileByName(name)\n        except KeyError:\n          raise RuntimeError('Please link in cpp generated lib for %s' % (name))\n      elif serialized_pb:\n        return _message.default_pool.AddSerializedFile(serialized_pb)\n      else:\n        return super(FileDescriptor, cls).__new__(cls)\n\n  def __init__(self, name, package, options=None,\n               serialized_options=None, serialized_pb=None,\n               dependencies=None, public_dependencies=None,\n               syntax=None, pool=None, create_key=None):\n    \"\"\"Constructor.\"\"\"\n    if create_key is not _internal_create_key:\n      _Deprecated('FileDescriptor')\n\n    super(FileDescriptor, self).__init__(\n        options, serialized_options, 'FileOptions')\n\n    if pool is None:\n      from google.protobuf import descriptor_pool\n      pool = descriptor_pool.Default()\n    self.pool = pool\n    self.message_types_by_name = {}\n    self.name = name\n    self.package = package\n    self.syntax = syntax or \"proto2\"\n    self.serialized_pb = serialized_pb\n\n    self.enum_types_by_name = {}\n    self.extensions_by_name = {}\n    self.services_by_name = {}\n    self.dependencies = (dependencies or [])\n    self.public_dependencies = (public_dependencies or [])\n\n  def CopyToProto(self, proto):\n    \"\"\"Copies this to a descriptor_pb2.FileDescriptorProto.\n\n    Args:\n      proto: An empty descriptor_pb2.FileDescriptorProto.\n    \"\"\"\n    proto.ParseFromString(self.serialized_pb)\n\n\ndef _ParseOptions(message, string):\n  \"\"\"Parses serialized options.\n\n  This helper function is used to parse serialized options in generated\n  proto2 files. It must not be used outside proto2.\n  \"\"\"\n  message.ParseFromString(string)\n  return message\n\n\ndef _ToCamelCase(name):\n  \"\"\"Converts name to camel-case and returns it.\"\"\"\n  capitalize_next = False\n  result = []\n\n  for c in name:\n    if c == '_':\n      if result:\n        capitalize_next = True\n    elif capitalize_next:\n      result.append(c.upper())\n      capitalize_next = False\n    else:\n      result += c\n\n  # Lower-case the first letter.\n  if result and result[0].isupper():\n    result[0] = result[0].lower()\n  return ''.join(result)\n\n\ndef _OptionsOrNone(descriptor_proto):\n  \"\"\"Returns the value of the field `options`, or None if it is not set.\"\"\"\n  if descriptor_proto.HasField('options'):\n    return descriptor_proto.options\n  else:\n    return None\n\n\ndef _ToJsonName(name):\n  \"\"\"Converts name to Json name and returns it.\"\"\"\n  capitalize_next = False\n  result = []\n\n  for c in name:\n    if c == '_':\n      capitalize_next = True\n    elif capitalize_next:\n      result.append(c.upper())\n      capitalize_next = False\n    else:\n      result += c\n\n  return ''.join(result)\n\n\ndef MakeDescriptor(desc_proto, package='', build_file_if_cpp=True,\n                   syntax=None):\n  \"\"\"Make a protobuf Descriptor given a DescriptorProto protobuf.\n\n  Handles nested descriptors. Note that this is limited to the scope of defining\n  a message inside of another message. Composite fields can currently only be\n  resolved if the message is defined in the same scope as the field.\n\n  Args:\n    desc_proto: The descriptor_pb2.DescriptorProto protobuf message.\n    package: Optional package name for the new message Descriptor (string).\n    build_file_if_cpp: Update the C++ descriptor pool if api matches.\n                       Set to False on recursion, so no duplicates are created.\n    syntax: The syntax/semantics that should be used.  Set to \"proto3\" to get\n            proto3 field presence semantics.\n  Returns:\n    A Descriptor for protobuf messages.\n  \"\"\"\n  if api_implementation.Type() == 'cpp' and build_file_if_cpp:\n    # The C++ implementation requires all descriptors to be backed by the same\n    # definition in the C++ descriptor pool. To do this, we build a\n    # FileDescriptorProto with the same definition as this descriptor and build\n    # it into the pool.\n    from google.protobuf import descriptor_pb2\n    file_descriptor_proto = descriptor_pb2.FileDescriptorProto()\n    file_descriptor_proto.message_type.add().MergeFrom(desc_proto)\n\n    # Generate a random name for this proto file to prevent conflicts with any\n    # imported ones. We need to specify a file name so the descriptor pool\n    # accepts our FileDescriptorProto, but it is not important what that file\n    # name is actually set to.\n    proto_name = binascii.hexlify(os.urandom(16)).decode('ascii')\n\n    if package:\n      file_descriptor_proto.name = os.path.join(package.replace('.', '/'),\n                                                proto_name + '.proto')\n      file_descriptor_proto.package = package\n    else:\n      file_descriptor_proto.name = proto_name + '.proto'\n\n    _message.default_pool.Add(file_descriptor_proto)\n    result = _message.default_pool.FindFileByName(file_descriptor_proto.name)\n\n    if _USE_C_DESCRIPTORS:\n      return result.message_types_by_name[desc_proto.name]\n\n  full_message_name = [desc_proto.name]\n  if package: full_message_name.insert(0, package)\n\n  # Create Descriptors for enum types\n  enum_types = {}\n  for enum_proto in desc_proto.enum_type:\n    full_name = '.'.join(full_message_name + [enum_proto.name])\n    enum_desc = EnumDescriptor(\n        enum_proto.name, full_name, None, [\n            EnumValueDescriptor(enum_val.name, ii, enum_val.number,\n                                create_key=_internal_create_key)\n            for ii, enum_val in enumerate(enum_proto.value)],\n        create_key=_internal_create_key)\n    enum_types[full_name] = enum_desc\n\n  # Create Descriptors for nested types\n  nested_types = {}\n  for nested_proto in desc_proto.nested_type:\n    full_name = '.'.join(full_message_name + [nested_proto.name])\n    # Nested types are just those defined inside of the message, not all types\n    # used by fields in the message, so no loops are possible here.\n    nested_desc = MakeDescriptor(nested_proto,\n                                 package='.'.join(full_message_name),\n                                 build_file_if_cpp=False,\n                                 syntax=syntax)\n    nested_types[full_name] = nested_desc\n\n  fields = []\n  for field_proto in desc_proto.field:\n    full_name = '.'.join(full_message_name + [field_proto.name])\n    enum_desc = None\n    nested_desc = None\n    if field_proto.json_name:\n      json_name = field_proto.json_name\n    else:\n      json_name = None\n    if field_proto.HasField('type_name'):\n      type_name = field_proto.type_name\n      full_type_name = '.'.join(full_message_name +\n                                [type_name[type_name.rfind('.')+1:]])\n      if full_type_name in nested_types:\n        nested_desc = nested_types[full_type_name]\n      elif full_type_name in enum_types:\n        enum_desc = enum_types[full_type_name]\n      # Else type_name references a non-local type, which isn't implemented\n    field = FieldDescriptor(\n        field_proto.name, full_name, field_proto.number - 1,\n        field_proto.number, field_proto.type,\n        FieldDescriptor.ProtoTypeToCppProtoType(field_proto.type),\n        field_proto.label, None, nested_desc, enum_desc, None, False, None,\n        options=_OptionsOrNone(field_proto), has_default_value=False,\n        json_name=json_name, create_key=_internal_create_key)\n    fields.append(field)\n\n  desc_name = '.'.join(full_message_name)\n  return Descriptor(desc_proto.name, desc_name, None, None, fields,\n                    list(nested_types.values()), list(enum_types.values()), [],\n                    options=_OptionsOrNone(desc_proto),\n                    create_key=_internal_create_key)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/descriptor_database.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Provides a container for DescriptorProtos.\"\"\"\n\n__author__ = 'matthewtoia@google.com (Matt Toia)'\n\nimport warnings\n\n\nclass Error(Exception):\n  pass\n\n\nclass DescriptorDatabaseConflictingDefinitionError(Error):\n  \"\"\"Raised when a proto is added with the same name & different descriptor.\"\"\"\n\n\nclass DescriptorDatabase(object):\n  \"\"\"A container accepting FileDescriptorProtos and maps DescriptorProtos.\"\"\"\n\n  def __init__(self):\n    self._file_desc_protos_by_file = {}\n    self._file_desc_protos_by_symbol = {}\n\n  def Add(self, file_desc_proto):\n    \"\"\"Adds the FileDescriptorProto and its types to this database.\n\n    Args:\n      file_desc_proto: The FileDescriptorProto to add.\n    Raises:\n      DescriptorDatabaseConflictingDefinitionError: if an attempt is made to\n        add a proto with the same name but different definition than an\n        existing proto in the database.\n    \"\"\"\n    proto_name = file_desc_proto.name\n    if proto_name not in self._file_desc_protos_by_file:\n      self._file_desc_protos_by_file[proto_name] = file_desc_proto\n    elif self._file_desc_protos_by_file[proto_name] != file_desc_proto:\n      raise DescriptorDatabaseConflictingDefinitionError(\n          '%s already added, but with different descriptor.' % proto_name)\n    else:\n      return\n\n    # Add all the top-level descriptors to the index.\n    package = file_desc_proto.package\n    for message in file_desc_proto.message_type:\n      for name in _ExtractSymbols(message, package):\n        self._AddSymbol(name, file_desc_proto)\n    for enum in file_desc_proto.enum_type:\n      self._AddSymbol(('.'.join((package, enum.name))), file_desc_proto)\n      for enum_value in enum.value:\n        self._file_desc_protos_by_symbol[\n            '.'.join((package, enum_value.name))] = file_desc_proto\n    for extension in file_desc_proto.extension:\n      self._AddSymbol(('.'.join((package, extension.name))), file_desc_proto)\n    for service in file_desc_proto.service:\n      self._AddSymbol(('.'.join((package, service.name))), file_desc_proto)\n\n  def FindFileByName(self, name):\n    \"\"\"Finds the file descriptor proto by file name.\n\n    Typically the file name is a relative path ending to a .proto file. The\n    proto with the given name will have to have been added to this database\n    using the Add method or else an error will be raised.\n\n    Args:\n      name: The file name to find.\n\n    Returns:\n      The file descriptor proto matching the name.\n\n    Raises:\n      KeyError if no file by the given name was added.\n    \"\"\"\n\n    return self._file_desc_protos_by_file[name]\n\n  def FindFileContainingSymbol(self, symbol):\n    \"\"\"Finds the file descriptor proto containing the specified symbol.\n\n    The symbol should be a fully qualified name including the file descriptor's\n    package and any containing messages. Some examples:\n\n    'some.package.name.Message'\n    'some.package.name.Message.NestedEnum'\n    'some.package.name.Message.some_field'\n\n    The file descriptor proto containing the specified symbol must be added to\n    this database using the Add method or else an error will be raised.\n\n    Args:\n      symbol: The fully qualified symbol name.\n\n    Returns:\n      The file descriptor proto containing the symbol.\n\n    Raises:\n      KeyError if no file contains the specified symbol.\n    \"\"\"\n    try:\n      return self._file_desc_protos_by_symbol[symbol]\n    except KeyError:\n      # Fields, enum values, and nested extensions are not in\n      # _file_desc_protos_by_symbol. Try to find the top level\n      # descriptor. Non-existent nested symbol under a valid top level\n      # descriptor can also be found. The behavior is the same with\n      # protobuf C++.\n      top_level, _, _ = symbol.rpartition('.')\n      try:\n        return self._file_desc_protos_by_symbol[top_level]\n      except KeyError:\n        # Raise the original symbol as a KeyError for better diagnostics.\n        raise KeyError(symbol)\n\n  def FindFileContainingExtension(self, extendee_name, extension_number):\n    # TODO(jieluo): implement this API.\n    return None\n\n  def FindAllExtensionNumbers(self, extendee_name):\n    # TODO(jieluo): implement this API.\n    return []\n\n  def _AddSymbol(self, name, file_desc_proto):\n    if name in self._file_desc_protos_by_symbol:\n      warn_msg = ('Conflict register for file \"' + file_desc_proto.name +\n                  '\": ' + name +\n                  ' is already defined in file \"' +\n                  self._file_desc_protos_by_symbol[name].name + '\"')\n      warnings.warn(warn_msg, RuntimeWarning)\n    self._file_desc_protos_by_symbol[name] = file_desc_proto\n\n\ndef _ExtractSymbols(desc_proto, package):\n  \"\"\"Pulls out all the symbols from a descriptor proto.\n\n  Args:\n    desc_proto: The proto to extract symbols from.\n    package: The package containing the descriptor type.\n\n  Yields:\n    The fully qualified name found in the descriptor.\n  \"\"\"\n  message_name = package + '.' + desc_proto.name if package else desc_proto.name\n  yield message_name\n  for nested_type in desc_proto.nested_type:\n    for symbol in _ExtractSymbols(nested_type, message_name):\n      yield symbol\n  for enum_type in desc_proto.enum_type:\n    yield '.'.join((message_name, enum_type.name))\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/descriptor_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/descriptor.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nif _descriptor._USE_C_DESCRIPTORS == False:\n  DESCRIPTOR = _descriptor.FileDescriptor(\n    name='google/protobuf/descriptor.proto',\n    package='google.protobuf',\n    syntax='proto2',\n    serialized_options=None,\n    create_key=_descriptor._internal_create_key,\n    serialized_pb=b'\\n google/protobuf/descriptor.proto\\x12\\x0fgoogle.protobuf\\\"G\\n\\x11\\x46ileDescriptorSet\\x12\\x32\\n\\x04\\x66ile\\x18\\x01 \\x03(\\x0b\\x32$.google.protobuf.FileDescriptorProto\\\"\\xdb\\x03\\n\\x13\\x46ileDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0f\\n\\x07package\\x18\\x02 \\x01(\\t\\x12\\x12\\n\\ndependency\\x18\\x03 \\x03(\\t\\x12\\x19\\n\\x11public_dependency\\x18\\n \\x03(\\x05\\x12\\x17\\n\\x0fweak_dependency\\x18\\x0b \\x03(\\x05\\x12\\x36\\n\\x0cmessage_type\\x18\\x04 \\x03(\\x0b\\x32 .google.protobuf.DescriptorProto\\x12\\x37\\n\\tenum_type\\x18\\x05 \\x03(\\x0b\\x32$.google.protobuf.EnumDescriptorProto\\x12\\x38\\n\\x07service\\x18\\x06 \\x03(\\x0b\\x32\\'.google.protobuf.ServiceDescriptorProto\\x12\\x38\\n\\textension\\x18\\x07 \\x03(\\x0b\\x32%.google.protobuf.FieldDescriptorProto\\x12-\\n\\x07options\\x18\\x08 \\x01(\\x0b\\x32\\x1c.google.protobuf.FileOptions\\x12\\x39\\n\\x10source_code_info\\x18\\t \\x01(\\x0b\\x32\\x1f.google.protobuf.SourceCodeInfo\\x12\\x0e\\n\\x06syntax\\x18\\x0c \\x01(\\t\\\"\\xa9\\x05\\n\\x0f\\x44\\x65scriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x34\\n\\x05\\x66ield\\x18\\x02 \\x03(\\x0b\\x32%.google.protobuf.FieldDescriptorProto\\x12\\x38\\n\\textension\\x18\\x06 \\x03(\\x0b\\x32%.google.protobuf.FieldDescriptorProto\\x12\\x35\\n\\x0bnested_type\\x18\\x03 \\x03(\\x0b\\x32 .google.protobuf.DescriptorProto\\x12\\x37\\n\\tenum_type\\x18\\x04 \\x03(\\x0b\\x32$.google.protobuf.EnumDescriptorProto\\x12H\\n\\x0f\\x65xtension_range\\x18\\x05 \\x03(\\x0b\\x32/.google.protobuf.DescriptorProto.ExtensionRange\\x12\\x39\\n\\noneof_decl\\x18\\x08 \\x03(\\x0b\\x32%.google.protobuf.OneofDescriptorProto\\x12\\x30\\n\\x07options\\x18\\x07 \\x01(\\x0b\\x32\\x1f.google.protobuf.MessageOptions\\x12\\x46\\n\\x0ereserved_range\\x18\\t \\x03(\\x0b\\x32..google.protobuf.DescriptorProto.ReservedRange\\x12\\x15\\n\\rreserved_name\\x18\\n \\x03(\\t\\x1a\\x65\\n\\x0e\\x45xtensionRange\\x12\\r\\n\\x05start\\x18\\x01 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x02 \\x01(\\x05\\x12\\x37\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32&.google.protobuf.ExtensionRangeOptions\\x1a+\\n\\rReservedRange\\x12\\r\\n\\x05start\\x18\\x01 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x02 \\x01(\\x05\\\"g\\n\\x15\\x45xtensionRangeOptions\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\xd5\\x05\\n\\x14\\x46ieldDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0e\\n\\x06number\\x18\\x03 \\x01(\\x05\\x12:\\n\\x05label\\x18\\x04 \\x01(\\x0e\\x32+.google.protobuf.FieldDescriptorProto.Label\\x12\\x38\\n\\x04type\\x18\\x05 \\x01(\\x0e\\x32*.google.protobuf.FieldDescriptorProto.Type\\x12\\x11\\n\\ttype_name\\x18\\x06 \\x01(\\t\\x12\\x10\\n\\x08\\x65xtendee\\x18\\x02 \\x01(\\t\\x12\\x15\\n\\rdefault_value\\x18\\x07 \\x01(\\t\\x12\\x13\\n\\x0boneof_index\\x18\\t \\x01(\\x05\\x12\\x11\\n\\tjson_name\\x18\\n \\x01(\\t\\x12.\\n\\x07options\\x18\\x08 \\x01(\\x0b\\x32\\x1d.google.protobuf.FieldOptions\\x12\\x17\\n\\x0fproto3_optional\\x18\\x11 \\x01(\\x08\\\"\\xb6\\x02\\n\\x04Type\\x12\\x0f\\n\\x0bTYPE_DOUBLE\\x10\\x01\\x12\\x0e\\n\\nTYPE_FLOAT\\x10\\x02\\x12\\x0e\\n\\nTYPE_INT64\\x10\\x03\\x12\\x0f\\n\\x0bTYPE_UINT64\\x10\\x04\\x12\\x0e\\n\\nTYPE_INT32\\x10\\x05\\x12\\x10\\n\\x0cTYPE_FIXED64\\x10\\x06\\x12\\x10\\n\\x0cTYPE_FIXED32\\x10\\x07\\x12\\r\\n\\tTYPE_BOOL\\x10\\x08\\x12\\x0f\\n\\x0bTYPE_STRING\\x10\\t\\x12\\x0e\\n\\nTYPE_GROUP\\x10\\n\\x12\\x10\\n\\x0cTYPE_MESSAGE\\x10\\x0b\\x12\\x0e\\n\\nTYPE_BYTES\\x10\\x0c\\x12\\x0f\\n\\x0bTYPE_UINT32\\x10\\r\\x12\\r\\n\\tTYPE_ENUM\\x10\\x0e\\x12\\x11\\n\\rTYPE_SFIXED32\\x10\\x0f\\x12\\x11\\n\\rTYPE_SFIXED64\\x10\\x10\\x12\\x0f\\n\\x0bTYPE_SINT32\\x10\\x11\\x12\\x0f\\n\\x0bTYPE_SINT64\\x10\\x12\\\"C\\n\\x05Label\\x12\\x12\\n\\x0eLABEL_OPTIONAL\\x10\\x01\\x12\\x12\\n\\x0eLABEL_REQUIRED\\x10\\x02\\x12\\x12\\n\\x0eLABEL_REPEATED\\x10\\x03\\\"T\\n\\x14OneofDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12.\\n\\x07options\\x18\\x02 \\x01(\\x0b\\x32\\x1d.google.protobuf.OneofOptions\\\"\\xa4\\x02\\n\\x13\\x45numDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x38\\n\\x05value\\x18\\x02 \\x03(\\x0b\\x32).google.protobuf.EnumValueDescriptorProto\\x12-\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32\\x1c.google.protobuf.EnumOptions\\x12N\\n\\x0ereserved_range\\x18\\x04 \\x03(\\x0b\\x32\\x36.google.protobuf.EnumDescriptorProto.EnumReservedRange\\x12\\x15\\n\\rreserved_name\\x18\\x05 \\x03(\\t\\x1a/\\n\\x11\\x45numReservedRange\\x12\\r\\n\\x05start\\x18\\x01 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x02 \\x01(\\x05\\\"l\\n\\x18\\x45numValueDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0e\\n\\x06number\\x18\\x02 \\x01(\\x05\\x12\\x32\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32!.google.protobuf.EnumValueOptions\\\"\\x90\\x01\\n\\x16ServiceDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x36\\n\\x06method\\x18\\x02 \\x03(\\x0b\\x32&.google.protobuf.MethodDescriptorProto\\x12\\x30\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32\\x1f.google.protobuf.ServiceOptions\\\"\\xc1\\x01\\n\\x15MethodDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x12\\n\\ninput_type\\x18\\x02 \\x01(\\t\\x12\\x13\\n\\x0boutput_type\\x18\\x03 \\x01(\\t\\x12/\\n\\x07options\\x18\\x04 \\x01(\\x0b\\x32\\x1e.google.protobuf.MethodOptions\\x12\\x1f\\n\\x10\\x63lient_streaming\\x18\\x05 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x1f\\n\\x10server_streaming\\x18\\x06 \\x01(\\x08:\\x05\\x66\\x61lse\\\"\\xa5\\x06\\n\\x0b\\x46ileOptions\\x12\\x14\\n\\x0cjava_package\\x18\\x01 \\x01(\\t\\x12\\x1c\\n\\x14java_outer_classname\\x18\\x08 \\x01(\\t\\x12\\\"\\n\\x13java_multiple_files\\x18\\n \\x01(\\x08:\\x05\\x66\\x61lse\\x12)\\n\\x1djava_generate_equals_and_hash\\x18\\x14 \\x01(\\x08\\x42\\x02\\x18\\x01\\x12%\\n\\x16java_string_check_utf8\\x18\\x1b \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x46\\n\\x0coptimize_for\\x18\\t \\x01(\\x0e\\x32).google.protobuf.FileOptions.OptimizeMode:\\x05SPEED\\x12\\x12\\n\\ngo_package\\x18\\x0b \\x01(\\t\\x12\\\"\\n\\x13\\x63\\x63_generic_services\\x18\\x10 \\x01(\\x08:\\x05\\x66\\x61lse\\x12$\\n\\x15java_generic_services\\x18\\x11 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\\"\\n\\x13py_generic_services\\x18\\x12 \\x01(\\x08:\\x05\\x66\\x61lse\\x12#\\n\\x14php_generic_services\\x18* \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x19\\n\\ndeprecated\\x18\\x17 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x1e\\n\\x10\\x63\\x63_enable_arenas\\x18\\x1f \\x01(\\x08:\\x04true\\x12\\x19\\n\\x11objc_class_prefix\\x18$ \\x01(\\t\\x12\\x18\\n\\x10\\x63sharp_namespace\\x18% \\x01(\\t\\x12\\x14\\n\\x0cswift_prefix\\x18\\' \\x01(\\t\\x12\\x18\\n\\x10php_class_prefix\\x18( \\x01(\\t\\x12\\x15\\n\\rphp_namespace\\x18) \\x01(\\t\\x12\\x1e\\n\\x16php_metadata_namespace\\x18, \\x01(\\t\\x12\\x14\\n\\x0cruby_package\\x18- \\x01(\\t\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption\\\":\\n\\x0cOptimizeMode\\x12\\t\\n\\x05SPEED\\x10\\x01\\x12\\r\\n\\tCODE_SIZE\\x10\\x02\\x12\\x10\\n\\x0cLITE_RUNTIME\\x10\\x03*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08&\\x10\\'\\\"\\x84\\x02\\n\\x0eMessageOptions\\x12&\\n\\x17message_set_wire_format\\x18\\x01 \\x01(\\x08:\\x05\\x66\\x61lse\\x12.\\n\\x1fno_standard_descriptor_accessor\\x18\\x02 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x19\\n\\ndeprecated\\x18\\x03 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x11\\n\\tmap_entry\\x18\\x07 \\x01(\\x08\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08\\x04\\x10\\x05J\\x04\\x08\\x05\\x10\\x06J\\x04\\x08\\x06\\x10\\x07J\\x04\\x08\\x08\\x10\\tJ\\x04\\x08\\t\\x10\\n\\\"\\xbe\\x03\\n\\x0c\\x46ieldOptions\\x12:\\n\\x05\\x63type\\x18\\x01 \\x01(\\x0e\\x32#.google.protobuf.FieldOptions.CType:\\x06STRING\\x12\\x0e\\n\\x06packed\\x18\\x02 \\x01(\\x08\\x12?\\n\\x06jstype\\x18\\x06 \\x01(\\x0e\\x32$.google.protobuf.FieldOptions.JSType:\\tJS_NORMAL\\x12\\x13\\n\\x04lazy\\x18\\x05 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x1e\\n\\x0funverified_lazy\\x18\\x0f \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x19\\n\\ndeprecated\\x18\\x03 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x13\\n\\x04weak\\x18\\n \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption\\\"/\\n\\x05\\x43Type\\x12\\n\\n\\x06STRING\\x10\\x00\\x12\\x08\\n\\x04\\x43ORD\\x10\\x01\\x12\\x10\\n\\x0cSTRING_PIECE\\x10\\x02\\\"5\\n\\x06JSType\\x12\\r\\n\\tJS_NORMAL\\x10\\x00\\x12\\r\\n\\tJS_STRING\\x10\\x01\\x12\\r\\n\\tJS_NUMBER\\x10\\x02*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08\\x04\\x10\\x05\\\"^\\n\\x0cOneofOptions\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\x93\\x01\\n\\x0b\\x45numOptions\\x12\\x13\\n\\x0b\\x61llow_alias\\x18\\x02 \\x01(\\x08\\x12\\x19\\n\\ndeprecated\\x18\\x03 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08\\x05\\x10\\x06\\\"}\\n\\x10\\x45numValueOptions\\x12\\x19\\n\\ndeprecated\\x18\\x01 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"{\\n\\x0eServiceOptions\\x12\\x19\\n\\ndeprecated\\x18! \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\xad\\x02\\n\\rMethodOptions\\x12\\x19\\n\\ndeprecated\\x18! \\x01(\\x08:\\x05\\x66\\x61lse\\x12_\\n\\x11idempotency_level\\x18\\\" \\x01(\\x0e\\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\\x13IDEMPOTENCY_UNKNOWN\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption\\\"P\\n\\x10IdempotencyLevel\\x12\\x17\\n\\x13IDEMPOTENCY_UNKNOWN\\x10\\x00\\x12\\x13\\n\\x0fNO_SIDE_EFFECTS\\x10\\x01\\x12\\x0e\\n\\nIDEMPOTENT\\x10\\x02*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\x9e\\x02\\n\\x13UninterpretedOption\\x12;\\n\\x04name\\x18\\x02 \\x03(\\x0b\\x32-.google.protobuf.UninterpretedOption.NamePart\\x12\\x18\\n\\x10identifier_value\\x18\\x03 \\x01(\\t\\x12\\x1a\\n\\x12positive_int_value\\x18\\x04 \\x01(\\x04\\x12\\x1a\\n\\x12negative_int_value\\x18\\x05 \\x01(\\x03\\x12\\x14\\n\\x0c\\x64ouble_value\\x18\\x06 \\x01(\\x01\\x12\\x14\\n\\x0cstring_value\\x18\\x07 \\x01(\\x0c\\x12\\x17\\n\\x0f\\x61ggregate_value\\x18\\x08 \\x01(\\t\\x1a\\x33\\n\\x08NamePart\\x12\\x11\\n\\tname_part\\x18\\x01 \\x02(\\t\\x12\\x14\\n\\x0cis_extension\\x18\\x02 \\x02(\\x08\\\"\\xd5\\x01\\n\\x0eSourceCodeInfo\\x12:\\n\\x08location\\x18\\x01 \\x03(\\x0b\\x32(.google.protobuf.SourceCodeInfo.Location\\x1a\\x86\\x01\\n\\x08Location\\x12\\x10\\n\\x04path\\x18\\x01 \\x03(\\x05\\x42\\x02\\x10\\x01\\x12\\x10\\n\\x04span\\x18\\x02 \\x03(\\x05\\x42\\x02\\x10\\x01\\x12\\x18\\n\\x10leading_comments\\x18\\x03 \\x01(\\t\\x12\\x19\\n\\x11trailing_comments\\x18\\x04 \\x01(\\t\\x12!\\n\\x19leading_detached_comments\\x18\\x06 \\x03(\\t\\\"\\xa7\\x01\\n\\x11GeneratedCodeInfo\\x12\\x41\\n\\nannotation\\x18\\x01 \\x03(\\x0b\\x32-.google.protobuf.GeneratedCodeInfo.Annotation\\x1aO\\n\\nAnnotation\\x12\\x10\\n\\x04path\\x18\\x01 \\x03(\\x05\\x42\\x02\\x10\\x01\\x12\\x13\\n\\x0bsource_file\\x18\\x02 \\x01(\\t\\x12\\r\\n\\x05\\x62\\x65gin\\x18\\x03 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x04 \\x01(\\x05\\x42~\\n\\x13\\x63om.google.protobufB\\x10\\x44\\x65scriptorProtosH\\x01Z-google.golang.org/protobuf/types/descriptorpb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1aGoogle.Protobuf.Reflection'\n  )\nelse:\n  DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n google/protobuf/descriptor.proto\\x12\\x0fgoogle.protobuf\\\"G\\n\\x11\\x46ileDescriptorSet\\x12\\x32\\n\\x04\\x66ile\\x18\\x01 \\x03(\\x0b\\x32$.google.protobuf.FileDescriptorProto\\\"\\xdb\\x03\\n\\x13\\x46ileDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0f\\n\\x07package\\x18\\x02 \\x01(\\t\\x12\\x12\\n\\ndependency\\x18\\x03 \\x03(\\t\\x12\\x19\\n\\x11public_dependency\\x18\\n \\x03(\\x05\\x12\\x17\\n\\x0fweak_dependency\\x18\\x0b \\x03(\\x05\\x12\\x36\\n\\x0cmessage_type\\x18\\x04 \\x03(\\x0b\\x32 .google.protobuf.DescriptorProto\\x12\\x37\\n\\tenum_type\\x18\\x05 \\x03(\\x0b\\x32$.google.protobuf.EnumDescriptorProto\\x12\\x38\\n\\x07service\\x18\\x06 \\x03(\\x0b\\x32\\'.google.protobuf.ServiceDescriptorProto\\x12\\x38\\n\\textension\\x18\\x07 \\x03(\\x0b\\x32%.google.protobuf.FieldDescriptorProto\\x12-\\n\\x07options\\x18\\x08 \\x01(\\x0b\\x32\\x1c.google.protobuf.FileOptions\\x12\\x39\\n\\x10source_code_info\\x18\\t \\x01(\\x0b\\x32\\x1f.google.protobuf.SourceCodeInfo\\x12\\x0e\\n\\x06syntax\\x18\\x0c \\x01(\\t\\\"\\xa9\\x05\\n\\x0f\\x44\\x65scriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x34\\n\\x05\\x66ield\\x18\\x02 \\x03(\\x0b\\x32%.google.protobuf.FieldDescriptorProto\\x12\\x38\\n\\textension\\x18\\x06 \\x03(\\x0b\\x32%.google.protobuf.FieldDescriptorProto\\x12\\x35\\n\\x0bnested_type\\x18\\x03 \\x03(\\x0b\\x32 .google.protobuf.DescriptorProto\\x12\\x37\\n\\tenum_type\\x18\\x04 \\x03(\\x0b\\x32$.google.protobuf.EnumDescriptorProto\\x12H\\n\\x0f\\x65xtension_range\\x18\\x05 \\x03(\\x0b\\x32/.google.protobuf.DescriptorProto.ExtensionRange\\x12\\x39\\n\\noneof_decl\\x18\\x08 \\x03(\\x0b\\x32%.google.protobuf.OneofDescriptorProto\\x12\\x30\\n\\x07options\\x18\\x07 \\x01(\\x0b\\x32\\x1f.google.protobuf.MessageOptions\\x12\\x46\\n\\x0ereserved_range\\x18\\t \\x03(\\x0b\\x32..google.protobuf.DescriptorProto.ReservedRange\\x12\\x15\\n\\rreserved_name\\x18\\n \\x03(\\t\\x1a\\x65\\n\\x0e\\x45xtensionRange\\x12\\r\\n\\x05start\\x18\\x01 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x02 \\x01(\\x05\\x12\\x37\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32&.google.protobuf.ExtensionRangeOptions\\x1a+\\n\\rReservedRange\\x12\\r\\n\\x05start\\x18\\x01 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x02 \\x01(\\x05\\\"g\\n\\x15\\x45xtensionRangeOptions\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\xd5\\x05\\n\\x14\\x46ieldDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0e\\n\\x06number\\x18\\x03 \\x01(\\x05\\x12:\\n\\x05label\\x18\\x04 \\x01(\\x0e\\x32+.google.protobuf.FieldDescriptorProto.Label\\x12\\x38\\n\\x04type\\x18\\x05 \\x01(\\x0e\\x32*.google.protobuf.FieldDescriptorProto.Type\\x12\\x11\\n\\ttype_name\\x18\\x06 \\x01(\\t\\x12\\x10\\n\\x08\\x65xtendee\\x18\\x02 \\x01(\\t\\x12\\x15\\n\\rdefault_value\\x18\\x07 \\x01(\\t\\x12\\x13\\n\\x0boneof_index\\x18\\t \\x01(\\x05\\x12\\x11\\n\\tjson_name\\x18\\n \\x01(\\t\\x12.\\n\\x07options\\x18\\x08 \\x01(\\x0b\\x32\\x1d.google.protobuf.FieldOptions\\x12\\x17\\n\\x0fproto3_optional\\x18\\x11 \\x01(\\x08\\\"\\xb6\\x02\\n\\x04Type\\x12\\x0f\\n\\x0bTYPE_DOUBLE\\x10\\x01\\x12\\x0e\\n\\nTYPE_FLOAT\\x10\\x02\\x12\\x0e\\n\\nTYPE_INT64\\x10\\x03\\x12\\x0f\\n\\x0bTYPE_UINT64\\x10\\x04\\x12\\x0e\\n\\nTYPE_INT32\\x10\\x05\\x12\\x10\\n\\x0cTYPE_FIXED64\\x10\\x06\\x12\\x10\\n\\x0cTYPE_FIXED32\\x10\\x07\\x12\\r\\n\\tTYPE_BOOL\\x10\\x08\\x12\\x0f\\n\\x0bTYPE_STRING\\x10\\t\\x12\\x0e\\n\\nTYPE_GROUP\\x10\\n\\x12\\x10\\n\\x0cTYPE_MESSAGE\\x10\\x0b\\x12\\x0e\\n\\nTYPE_BYTES\\x10\\x0c\\x12\\x0f\\n\\x0bTYPE_UINT32\\x10\\r\\x12\\r\\n\\tTYPE_ENUM\\x10\\x0e\\x12\\x11\\n\\rTYPE_SFIXED32\\x10\\x0f\\x12\\x11\\n\\rTYPE_SFIXED64\\x10\\x10\\x12\\x0f\\n\\x0bTYPE_SINT32\\x10\\x11\\x12\\x0f\\n\\x0bTYPE_SINT64\\x10\\x12\\\"C\\n\\x05Label\\x12\\x12\\n\\x0eLABEL_OPTIONAL\\x10\\x01\\x12\\x12\\n\\x0eLABEL_REQUIRED\\x10\\x02\\x12\\x12\\n\\x0eLABEL_REPEATED\\x10\\x03\\\"T\\n\\x14OneofDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12.\\n\\x07options\\x18\\x02 \\x01(\\x0b\\x32\\x1d.google.protobuf.OneofOptions\\\"\\xa4\\x02\\n\\x13\\x45numDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x38\\n\\x05value\\x18\\x02 \\x03(\\x0b\\x32).google.protobuf.EnumValueDescriptorProto\\x12-\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32\\x1c.google.protobuf.EnumOptions\\x12N\\n\\x0ereserved_range\\x18\\x04 \\x03(\\x0b\\x32\\x36.google.protobuf.EnumDescriptorProto.EnumReservedRange\\x12\\x15\\n\\rreserved_name\\x18\\x05 \\x03(\\t\\x1a/\\n\\x11\\x45numReservedRange\\x12\\r\\n\\x05start\\x18\\x01 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x02 \\x01(\\x05\\\"l\\n\\x18\\x45numValueDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0e\\n\\x06number\\x18\\x02 \\x01(\\x05\\x12\\x32\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32!.google.protobuf.EnumValueOptions\\\"\\x90\\x01\\n\\x16ServiceDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x36\\n\\x06method\\x18\\x02 \\x03(\\x0b\\x32&.google.protobuf.MethodDescriptorProto\\x12\\x30\\n\\x07options\\x18\\x03 \\x01(\\x0b\\x32\\x1f.google.protobuf.ServiceOptions\\\"\\xc1\\x01\\n\\x15MethodDescriptorProto\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x12\\n\\ninput_type\\x18\\x02 \\x01(\\t\\x12\\x13\\n\\x0boutput_type\\x18\\x03 \\x01(\\t\\x12/\\n\\x07options\\x18\\x04 \\x01(\\x0b\\x32\\x1e.google.protobuf.MethodOptions\\x12\\x1f\\n\\x10\\x63lient_streaming\\x18\\x05 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x1f\\n\\x10server_streaming\\x18\\x06 \\x01(\\x08:\\x05\\x66\\x61lse\\\"\\xa5\\x06\\n\\x0b\\x46ileOptions\\x12\\x14\\n\\x0cjava_package\\x18\\x01 \\x01(\\t\\x12\\x1c\\n\\x14java_outer_classname\\x18\\x08 \\x01(\\t\\x12\\\"\\n\\x13java_multiple_files\\x18\\n \\x01(\\x08:\\x05\\x66\\x61lse\\x12)\\n\\x1djava_generate_equals_and_hash\\x18\\x14 \\x01(\\x08\\x42\\x02\\x18\\x01\\x12%\\n\\x16java_string_check_utf8\\x18\\x1b \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x46\\n\\x0coptimize_for\\x18\\t \\x01(\\x0e\\x32).google.protobuf.FileOptions.OptimizeMode:\\x05SPEED\\x12\\x12\\n\\ngo_package\\x18\\x0b \\x01(\\t\\x12\\\"\\n\\x13\\x63\\x63_generic_services\\x18\\x10 \\x01(\\x08:\\x05\\x66\\x61lse\\x12$\\n\\x15java_generic_services\\x18\\x11 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\\"\\n\\x13py_generic_services\\x18\\x12 \\x01(\\x08:\\x05\\x66\\x61lse\\x12#\\n\\x14php_generic_services\\x18* \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x19\\n\\ndeprecated\\x18\\x17 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x1e\\n\\x10\\x63\\x63_enable_arenas\\x18\\x1f \\x01(\\x08:\\x04true\\x12\\x19\\n\\x11objc_class_prefix\\x18$ \\x01(\\t\\x12\\x18\\n\\x10\\x63sharp_namespace\\x18% \\x01(\\t\\x12\\x14\\n\\x0cswift_prefix\\x18\\' \\x01(\\t\\x12\\x18\\n\\x10php_class_prefix\\x18( \\x01(\\t\\x12\\x15\\n\\rphp_namespace\\x18) \\x01(\\t\\x12\\x1e\\n\\x16php_metadata_namespace\\x18, \\x01(\\t\\x12\\x14\\n\\x0cruby_package\\x18- \\x01(\\t\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption\\\":\\n\\x0cOptimizeMode\\x12\\t\\n\\x05SPEED\\x10\\x01\\x12\\r\\n\\tCODE_SIZE\\x10\\x02\\x12\\x10\\n\\x0cLITE_RUNTIME\\x10\\x03*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08&\\x10\\'\\\"\\x84\\x02\\n\\x0eMessageOptions\\x12&\\n\\x17message_set_wire_format\\x18\\x01 \\x01(\\x08:\\x05\\x66\\x61lse\\x12.\\n\\x1fno_standard_descriptor_accessor\\x18\\x02 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x19\\n\\ndeprecated\\x18\\x03 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x11\\n\\tmap_entry\\x18\\x07 \\x01(\\x08\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08\\x04\\x10\\x05J\\x04\\x08\\x05\\x10\\x06J\\x04\\x08\\x06\\x10\\x07J\\x04\\x08\\x08\\x10\\tJ\\x04\\x08\\t\\x10\\n\\\"\\xbe\\x03\\n\\x0c\\x46ieldOptions\\x12:\\n\\x05\\x63type\\x18\\x01 \\x01(\\x0e\\x32#.google.protobuf.FieldOptions.CType:\\x06STRING\\x12\\x0e\\n\\x06packed\\x18\\x02 \\x01(\\x08\\x12?\\n\\x06jstype\\x18\\x06 \\x01(\\x0e\\x32$.google.protobuf.FieldOptions.JSType:\\tJS_NORMAL\\x12\\x13\\n\\x04lazy\\x18\\x05 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x1e\\n\\x0funverified_lazy\\x18\\x0f \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x19\\n\\ndeprecated\\x18\\x03 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x13\\n\\x04weak\\x18\\n \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption\\\"/\\n\\x05\\x43Type\\x12\\n\\n\\x06STRING\\x10\\x00\\x12\\x08\\n\\x04\\x43ORD\\x10\\x01\\x12\\x10\\n\\x0cSTRING_PIECE\\x10\\x02\\\"5\\n\\x06JSType\\x12\\r\\n\\tJS_NORMAL\\x10\\x00\\x12\\r\\n\\tJS_STRING\\x10\\x01\\x12\\r\\n\\tJS_NUMBER\\x10\\x02*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08\\x04\\x10\\x05\\\"^\\n\\x0cOneofOptions\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\x93\\x01\\n\\x0b\\x45numOptions\\x12\\x13\\n\\x0b\\x61llow_alias\\x18\\x02 \\x01(\\x08\\x12\\x19\\n\\ndeprecated\\x18\\x03 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02J\\x04\\x08\\x05\\x10\\x06\\\"}\\n\\x10\\x45numValueOptions\\x12\\x19\\n\\ndeprecated\\x18\\x01 \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"{\\n\\x0eServiceOptions\\x12\\x19\\n\\ndeprecated\\x18! \\x01(\\x08:\\x05\\x66\\x61lse\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\xad\\x02\\n\\rMethodOptions\\x12\\x19\\n\\ndeprecated\\x18! \\x01(\\x08:\\x05\\x66\\x61lse\\x12_\\n\\x11idempotency_level\\x18\\\" \\x01(\\x0e\\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\\x13IDEMPOTENCY_UNKNOWN\\x12\\x43\\n\\x14uninterpreted_option\\x18\\xe7\\x07 \\x03(\\x0b\\x32$.google.protobuf.UninterpretedOption\\\"P\\n\\x10IdempotencyLevel\\x12\\x17\\n\\x13IDEMPOTENCY_UNKNOWN\\x10\\x00\\x12\\x13\\n\\x0fNO_SIDE_EFFECTS\\x10\\x01\\x12\\x0e\\n\\nIDEMPOTENT\\x10\\x02*\\t\\x08\\xe8\\x07\\x10\\x80\\x80\\x80\\x80\\x02\\\"\\x9e\\x02\\n\\x13UninterpretedOption\\x12;\\n\\x04name\\x18\\x02 \\x03(\\x0b\\x32-.google.protobuf.UninterpretedOption.NamePart\\x12\\x18\\n\\x10identifier_value\\x18\\x03 \\x01(\\t\\x12\\x1a\\n\\x12positive_int_value\\x18\\x04 \\x01(\\x04\\x12\\x1a\\n\\x12negative_int_value\\x18\\x05 \\x01(\\x03\\x12\\x14\\n\\x0c\\x64ouble_value\\x18\\x06 \\x01(\\x01\\x12\\x14\\n\\x0cstring_value\\x18\\x07 \\x01(\\x0c\\x12\\x17\\n\\x0f\\x61ggregate_value\\x18\\x08 \\x01(\\t\\x1a\\x33\\n\\x08NamePart\\x12\\x11\\n\\tname_part\\x18\\x01 \\x02(\\t\\x12\\x14\\n\\x0cis_extension\\x18\\x02 \\x02(\\x08\\\"\\xd5\\x01\\n\\x0eSourceCodeInfo\\x12:\\n\\x08location\\x18\\x01 \\x03(\\x0b\\x32(.google.protobuf.SourceCodeInfo.Location\\x1a\\x86\\x01\\n\\x08Location\\x12\\x10\\n\\x04path\\x18\\x01 \\x03(\\x05\\x42\\x02\\x10\\x01\\x12\\x10\\n\\x04span\\x18\\x02 \\x03(\\x05\\x42\\x02\\x10\\x01\\x12\\x18\\n\\x10leading_comments\\x18\\x03 \\x01(\\t\\x12\\x19\\n\\x11trailing_comments\\x18\\x04 \\x01(\\t\\x12!\\n\\x19leading_detached_comments\\x18\\x06 \\x03(\\t\\\"\\xa7\\x01\\n\\x11GeneratedCodeInfo\\x12\\x41\\n\\nannotation\\x18\\x01 \\x03(\\x0b\\x32-.google.protobuf.GeneratedCodeInfo.Annotation\\x1aO\\n\\nAnnotation\\x12\\x10\\n\\x04path\\x18\\x01 \\x03(\\x05\\x42\\x02\\x10\\x01\\x12\\x13\\n\\x0bsource_file\\x18\\x02 \\x01(\\t\\x12\\r\\n\\x05\\x62\\x65gin\\x18\\x03 \\x01(\\x05\\x12\\x0b\\n\\x03\\x65nd\\x18\\x04 \\x01(\\x05\\x42~\\n\\x13\\x63om.google.protobufB\\x10\\x44\\x65scriptorProtosH\\x01Z-google.golang.org/protobuf/types/descriptorpb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1aGoogle.Protobuf.Reflection')\n\nif _descriptor._USE_C_DESCRIPTORS == False:\n  _FIELDDESCRIPTORPROTO_TYPE = _descriptor.EnumDescriptor(\n    name='Type',\n    full_name='google.protobuf.FieldDescriptorProto.Type',\n    filename=None,\n    file=DESCRIPTOR,\n    create_key=_descriptor._internal_create_key,\n    values=[\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_DOUBLE', index=0, number=1,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_FLOAT', index=1, number=2,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_INT64', index=2, number=3,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_UINT64', index=3, number=4,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_INT32', index=4, number=5,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_FIXED64', index=5, number=6,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_FIXED32', index=6, number=7,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_BOOL', index=7, number=8,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_STRING', index=8, number=9,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_GROUP', index=9, number=10,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_MESSAGE', index=10, number=11,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_BYTES', index=11, number=12,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_UINT32', index=12, number=13,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_ENUM', index=13, number=14,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_SFIXED32', index=14, number=15,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_SFIXED64', index=15, number=16,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_SINT32', index=16, number=17,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='TYPE_SINT64', index=17, number=18,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n    ],\n    containing_type=None,\n    serialized_options=None,\n  )\n  _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_TYPE)\n\n  _FIELDDESCRIPTORPROTO_LABEL = _descriptor.EnumDescriptor(\n    name='Label',\n    full_name='google.protobuf.FieldDescriptorProto.Label',\n    filename=None,\n    file=DESCRIPTOR,\n    create_key=_descriptor._internal_create_key,\n    values=[\n      _descriptor.EnumValueDescriptor(\n        name='LABEL_OPTIONAL', index=0, number=1,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='LABEL_REQUIRED', index=1, number=2,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='LABEL_REPEATED', index=2, number=3,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n    ],\n    containing_type=None,\n    serialized_options=None,\n  )\n  _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_LABEL)\n\n  _FILEOPTIONS_OPTIMIZEMODE = _descriptor.EnumDescriptor(\n    name='OptimizeMode',\n    full_name='google.protobuf.FileOptions.OptimizeMode',\n    filename=None,\n    file=DESCRIPTOR,\n    create_key=_descriptor._internal_create_key,\n    values=[\n      _descriptor.EnumValueDescriptor(\n        name='SPEED', index=0, number=1,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='CODE_SIZE', index=1, number=2,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='LITE_RUNTIME', index=2, number=3,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n    ],\n    containing_type=None,\n    serialized_options=None,\n  )\n  _sym_db.RegisterEnumDescriptor(_FILEOPTIONS_OPTIMIZEMODE)\n\n  _FIELDOPTIONS_CTYPE = _descriptor.EnumDescriptor(\n    name='CType',\n    full_name='google.protobuf.FieldOptions.CType',\n    filename=None,\n    file=DESCRIPTOR,\n    create_key=_descriptor._internal_create_key,\n    values=[\n      _descriptor.EnumValueDescriptor(\n        name='STRING', index=0, number=0,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='CORD', index=1, number=1,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='STRING_PIECE', index=2, number=2,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n    ],\n    containing_type=None,\n    serialized_options=None,\n  )\n  _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_CTYPE)\n\n  _FIELDOPTIONS_JSTYPE = _descriptor.EnumDescriptor(\n    name='JSType',\n    full_name='google.protobuf.FieldOptions.JSType',\n    filename=None,\n    file=DESCRIPTOR,\n    create_key=_descriptor._internal_create_key,\n    values=[\n      _descriptor.EnumValueDescriptor(\n        name='JS_NORMAL', index=0, number=0,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='JS_STRING', index=1, number=1,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='JS_NUMBER', index=2, number=2,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n    ],\n    containing_type=None,\n    serialized_options=None,\n  )\n  _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_JSTYPE)\n\n  _METHODOPTIONS_IDEMPOTENCYLEVEL = _descriptor.EnumDescriptor(\n    name='IdempotencyLevel',\n    full_name='google.protobuf.MethodOptions.IdempotencyLevel',\n    filename=None,\n    file=DESCRIPTOR,\n    create_key=_descriptor._internal_create_key,\n    values=[\n      _descriptor.EnumValueDescriptor(\n        name='IDEMPOTENCY_UNKNOWN', index=0, number=0,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='NO_SIDE_EFFECTS', index=1, number=1,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n      _descriptor.EnumValueDescriptor(\n        name='IDEMPOTENT', index=2, number=2,\n        serialized_options=None,\n        type=None,\n        create_key=_descriptor._internal_create_key),\n    ],\n    containing_type=None,\n    serialized_options=None,\n  )\n  _sym_db.RegisterEnumDescriptor(_METHODOPTIONS_IDEMPOTENCYLEVEL)\n\n\n  _FILEDESCRIPTORSET = _descriptor.Descriptor(\n    name='FileDescriptorSet',\n    full_name='google.protobuf.FileDescriptorSet',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='file', full_name='google.protobuf.FileDescriptorSet.file', index=0,\n        number=1, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _FILEDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='FileDescriptorProto',\n    full_name='google.protobuf.FileDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.FileDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='package', full_name='google.protobuf.FileDescriptorProto.package', index=1,\n        number=2, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='dependency', full_name='google.protobuf.FileDescriptorProto.dependency', index=2,\n        number=3, type=9, cpp_type=9, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='public_dependency', full_name='google.protobuf.FileDescriptorProto.public_dependency', index=3,\n        number=10, type=5, cpp_type=1, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='weak_dependency', full_name='google.protobuf.FileDescriptorProto.weak_dependency', index=4,\n        number=11, type=5, cpp_type=1, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='message_type', full_name='google.protobuf.FileDescriptorProto.message_type', index=5,\n        number=4, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='enum_type', full_name='google.protobuf.FileDescriptorProto.enum_type', index=6,\n        number=5, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='service', full_name='google.protobuf.FileDescriptorProto.service', index=7,\n        number=6, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='extension', full_name='google.protobuf.FileDescriptorProto.extension', index=8,\n        number=7, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.FileDescriptorProto.options', index=9,\n        number=8, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='source_code_info', full_name='google.protobuf.FileDescriptorProto.source_code_info', index=10,\n        number=9, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='syntax', full_name='google.protobuf.FileDescriptorProto.syntax', index=11,\n        number=12, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _DESCRIPTORPROTO_EXTENSIONRANGE = _descriptor.Descriptor(\n    name='ExtensionRange',\n    full_name='google.protobuf.DescriptorProto.ExtensionRange',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='start', full_name='google.protobuf.DescriptorProto.ExtensionRange.start', index=0,\n        number=1, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='end', full_name='google.protobuf.DescriptorProto.ExtensionRange.end', index=1,\n        number=2, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.DescriptorProto.ExtensionRange.options', index=2,\n        number=3, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _DESCRIPTORPROTO_RESERVEDRANGE = _descriptor.Descriptor(\n    name='ReservedRange',\n    full_name='google.protobuf.DescriptorProto.ReservedRange',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='start', full_name='google.protobuf.DescriptorProto.ReservedRange.start', index=0,\n        number=1, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='end', full_name='google.protobuf.DescriptorProto.ReservedRange.end', index=1,\n        number=2, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _DESCRIPTORPROTO = _descriptor.Descriptor(\n    name='DescriptorProto',\n    full_name='google.protobuf.DescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.DescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='field', full_name='google.protobuf.DescriptorProto.field', index=1,\n        number=2, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='extension', full_name='google.protobuf.DescriptorProto.extension', index=2,\n        number=6, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='nested_type', full_name='google.protobuf.DescriptorProto.nested_type', index=3,\n        number=3, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='enum_type', full_name='google.protobuf.DescriptorProto.enum_type', index=4,\n        number=4, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='extension_range', full_name='google.protobuf.DescriptorProto.extension_range', index=5,\n        number=5, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='oneof_decl', full_name='google.protobuf.DescriptorProto.oneof_decl', index=6,\n        number=8, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.DescriptorProto.options', index=7,\n        number=7, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='reserved_range', full_name='google.protobuf.DescriptorProto.reserved_range', index=8,\n        number=9, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='reserved_name', full_name='google.protobuf.DescriptorProto.reserved_name', index=9,\n        number=10, type=9, cpp_type=9, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[_DESCRIPTORPROTO_EXTENSIONRANGE, _DESCRIPTORPROTO_RESERVEDRANGE, ],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _EXTENSIONRANGEOPTIONS = _descriptor.Descriptor(\n    name='ExtensionRangeOptions',\n    full_name='google.protobuf.ExtensionRangeOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.ExtensionRangeOptions.uninterpreted_option', index=0,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _FIELDDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='FieldDescriptorProto',\n    full_name='google.protobuf.FieldDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.FieldDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='number', full_name='google.protobuf.FieldDescriptorProto.number', index=1,\n        number=3, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='label', full_name='google.protobuf.FieldDescriptorProto.label', index=2,\n        number=4, type=14, cpp_type=8, label=1,\n        has_default_value=False, default_value=1,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='type', full_name='google.protobuf.FieldDescriptorProto.type', index=3,\n        number=5, type=14, cpp_type=8, label=1,\n        has_default_value=False, default_value=1,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='type_name', full_name='google.protobuf.FieldDescriptorProto.type_name', index=4,\n        number=6, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='extendee', full_name='google.protobuf.FieldDescriptorProto.extendee', index=5,\n        number=2, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='default_value', full_name='google.protobuf.FieldDescriptorProto.default_value', index=6,\n        number=7, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='oneof_index', full_name='google.protobuf.FieldDescriptorProto.oneof_index', index=7,\n        number=9, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='json_name', full_name='google.protobuf.FieldDescriptorProto.json_name', index=8,\n        number=10, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.FieldDescriptorProto.options', index=9,\n        number=8, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='proto3_optional', full_name='google.protobuf.FieldDescriptorProto.proto3_optional', index=10,\n        number=17, type=8, cpp_type=7, label=1,\n        has_default_value=False, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n      _FIELDDESCRIPTORPROTO_TYPE,\n      _FIELDDESCRIPTORPROTO_LABEL,\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _ONEOFDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='OneofDescriptorProto',\n    full_name='google.protobuf.OneofDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.OneofDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.OneofDescriptorProto.options', index=1,\n        number=2, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE = _descriptor.Descriptor(\n    name='EnumReservedRange',\n    full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='start', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.start', index=0,\n        number=1, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='end', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.end', index=1,\n        number=2, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _ENUMDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='EnumDescriptorProto',\n    full_name='google.protobuf.EnumDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.EnumDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='value', full_name='google.protobuf.EnumDescriptorProto.value', index=1,\n        number=2, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.EnumDescriptorProto.options', index=2,\n        number=3, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='reserved_range', full_name='google.protobuf.EnumDescriptorProto.reserved_range', index=3,\n        number=4, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='reserved_name', full_name='google.protobuf.EnumDescriptorProto.reserved_name', index=4,\n        number=5, type=9, cpp_type=9, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[_ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE, ],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _ENUMVALUEDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='EnumValueDescriptorProto',\n    full_name='google.protobuf.EnumValueDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.EnumValueDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='number', full_name='google.protobuf.EnumValueDescriptorProto.number', index=1,\n        number=2, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.EnumValueDescriptorProto.options', index=2,\n        number=3, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _SERVICEDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='ServiceDescriptorProto',\n    full_name='google.protobuf.ServiceDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.ServiceDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='method', full_name='google.protobuf.ServiceDescriptorProto.method', index=1,\n        number=2, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.ServiceDescriptorProto.options', index=2,\n        number=3, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _METHODDESCRIPTORPROTO = _descriptor.Descriptor(\n    name='MethodDescriptorProto',\n    full_name='google.protobuf.MethodDescriptorProto',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.MethodDescriptorProto.name', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='input_type', full_name='google.protobuf.MethodDescriptorProto.input_type', index=1,\n        number=2, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='output_type', full_name='google.protobuf.MethodDescriptorProto.output_type', index=2,\n        number=3, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='options', full_name='google.protobuf.MethodDescriptorProto.options', index=3,\n        number=4, type=11, cpp_type=10, label=1,\n        has_default_value=False, default_value=None,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='client_streaming', full_name='google.protobuf.MethodDescriptorProto.client_streaming', index=4,\n        number=5, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='server_streaming', full_name='google.protobuf.MethodDescriptorProto.server_streaming', index=5,\n        number=6, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _FILEOPTIONS = _descriptor.Descriptor(\n    name='FileOptions',\n    full_name='google.protobuf.FileOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='java_package', full_name='google.protobuf.FileOptions.java_package', index=0,\n        number=1, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='java_outer_classname', full_name='google.protobuf.FileOptions.java_outer_classname', index=1,\n        number=8, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='java_multiple_files', full_name='google.protobuf.FileOptions.java_multiple_files', index=2,\n        number=10, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='java_generate_equals_and_hash', full_name='google.protobuf.FileOptions.java_generate_equals_and_hash', index=3,\n        number=20, type=8, cpp_type=7, label=1,\n        has_default_value=False, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='java_string_check_utf8', full_name='google.protobuf.FileOptions.java_string_check_utf8', index=4,\n        number=27, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='optimize_for', full_name='google.protobuf.FileOptions.optimize_for', index=5,\n        number=9, type=14, cpp_type=8, label=1,\n        has_default_value=True, default_value=1,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='go_package', full_name='google.protobuf.FileOptions.go_package', index=6,\n        number=11, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='cc_generic_services', full_name='google.protobuf.FileOptions.cc_generic_services', index=7,\n        number=16, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='java_generic_services', full_name='google.protobuf.FileOptions.java_generic_services', index=8,\n        number=17, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='py_generic_services', full_name='google.protobuf.FileOptions.py_generic_services', index=9,\n        number=18, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='php_generic_services', full_name='google.protobuf.FileOptions.php_generic_services', index=10,\n        number=42, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.FileOptions.deprecated', index=11,\n        number=23, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='cc_enable_arenas', full_name='google.protobuf.FileOptions.cc_enable_arenas', index=12,\n        number=31, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=True,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='objc_class_prefix', full_name='google.protobuf.FileOptions.objc_class_prefix', index=13,\n        number=36, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='csharp_namespace', full_name='google.protobuf.FileOptions.csharp_namespace', index=14,\n        number=37, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='swift_prefix', full_name='google.protobuf.FileOptions.swift_prefix', index=15,\n        number=39, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='php_class_prefix', full_name='google.protobuf.FileOptions.php_class_prefix', index=16,\n        number=40, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='php_namespace', full_name='google.protobuf.FileOptions.php_namespace', index=17,\n        number=41, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='php_metadata_namespace', full_name='google.protobuf.FileOptions.php_metadata_namespace', index=18,\n        number=44, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='ruby_package', full_name='google.protobuf.FileOptions.ruby_package', index=19,\n        number=45, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.FileOptions.uninterpreted_option', index=20,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n      _FILEOPTIONS_OPTIMIZEMODE,\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _MESSAGEOPTIONS = _descriptor.Descriptor(\n    name='MessageOptions',\n    full_name='google.protobuf.MessageOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='message_set_wire_format', full_name='google.protobuf.MessageOptions.message_set_wire_format', index=0,\n        number=1, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='no_standard_descriptor_accessor', full_name='google.protobuf.MessageOptions.no_standard_descriptor_accessor', index=1,\n        number=2, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.MessageOptions.deprecated', index=2,\n        number=3, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='map_entry', full_name='google.protobuf.MessageOptions.map_entry', index=3,\n        number=7, type=8, cpp_type=7, label=1,\n        has_default_value=False, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.MessageOptions.uninterpreted_option', index=4,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _FIELDOPTIONS = _descriptor.Descriptor(\n    name='FieldOptions',\n    full_name='google.protobuf.FieldOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='ctype', full_name='google.protobuf.FieldOptions.ctype', index=0,\n        number=1, type=14, cpp_type=8, label=1,\n        has_default_value=True, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='packed', full_name='google.protobuf.FieldOptions.packed', index=1,\n        number=2, type=8, cpp_type=7, label=1,\n        has_default_value=False, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='jstype', full_name='google.protobuf.FieldOptions.jstype', index=2,\n        number=6, type=14, cpp_type=8, label=1,\n        has_default_value=True, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='lazy', full_name='google.protobuf.FieldOptions.lazy', index=3,\n        number=5, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='unverified_lazy', full_name='google.protobuf.FieldOptions.unverified_lazy', index=4,\n        number=15, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.FieldOptions.deprecated', index=5,\n        number=3, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='weak', full_name='google.protobuf.FieldOptions.weak', index=6,\n        number=10, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.FieldOptions.uninterpreted_option', index=7,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n      _FIELDOPTIONS_CTYPE,\n      _FIELDOPTIONS_JSTYPE,\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _ONEOFOPTIONS = _descriptor.Descriptor(\n    name='OneofOptions',\n    full_name='google.protobuf.OneofOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.OneofOptions.uninterpreted_option', index=0,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _ENUMOPTIONS = _descriptor.Descriptor(\n    name='EnumOptions',\n    full_name='google.protobuf.EnumOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='allow_alias', full_name='google.protobuf.EnumOptions.allow_alias', index=0,\n        number=2, type=8, cpp_type=7, label=1,\n        has_default_value=False, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.EnumOptions.deprecated', index=1,\n        number=3, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.EnumOptions.uninterpreted_option', index=2,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _ENUMVALUEOPTIONS = _descriptor.Descriptor(\n    name='EnumValueOptions',\n    full_name='google.protobuf.EnumValueOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.EnumValueOptions.deprecated', index=0,\n        number=1, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.EnumValueOptions.uninterpreted_option', index=1,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _SERVICEOPTIONS = _descriptor.Descriptor(\n    name='ServiceOptions',\n    full_name='google.protobuf.ServiceOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.ServiceOptions.deprecated', index=0,\n        number=33, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.ServiceOptions.uninterpreted_option', index=1,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _METHODOPTIONS = _descriptor.Descriptor(\n    name='MethodOptions',\n    full_name='google.protobuf.MethodOptions',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='deprecated', full_name='google.protobuf.MethodOptions.deprecated', index=0,\n        number=33, type=8, cpp_type=7, label=1,\n        has_default_value=True, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='idempotency_level', full_name='google.protobuf.MethodOptions.idempotency_level', index=1,\n        number=34, type=14, cpp_type=8, label=1,\n        has_default_value=True, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='uninterpreted_option', full_name='google.protobuf.MethodOptions.uninterpreted_option', index=2,\n        number=999, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n      _METHODOPTIONS_IDEMPOTENCYLEVEL,\n    ],\n    serialized_options=None,\n    is_extendable=True,\n    syntax='proto2',\n    extension_ranges=[(1000, 536870912), ],\n    oneofs=[\n    ],\n  )\n\n\n  _UNINTERPRETEDOPTION_NAMEPART = _descriptor.Descriptor(\n    name='NamePart',\n    full_name='google.protobuf.UninterpretedOption.NamePart',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name_part', full_name='google.protobuf.UninterpretedOption.NamePart.name_part', index=0,\n        number=1, type=9, cpp_type=9, label=2,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='is_extension', full_name='google.protobuf.UninterpretedOption.NamePart.is_extension', index=1,\n        number=2, type=8, cpp_type=7, label=2,\n        has_default_value=False, default_value=False,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _UNINTERPRETEDOPTION = _descriptor.Descriptor(\n    name='UninterpretedOption',\n    full_name='google.protobuf.UninterpretedOption',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='name', full_name='google.protobuf.UninterpretedOption.name', index=0,\n        number=2, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='identifier_value', full_name='google.protobuf.UninterpretedOption.identifier_value', index=1,\n        number=3, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='positive_int_value', full_name='google.protobuf.UninterpretedOption.positive_int_value', index=2,\n        number=4, type=4, cpp_type=4, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='negative_int_value', full_name='google.protobuf.UninterpretedOption.negative_int_value', index=3,\n        number=5, type=3, cpp_type=2, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='double_value', full_name='google.protobuf.UninterpretedOption.double_value', index=4,\n        number=6, type=1, cpp_type=5, label=1,\n        has_default_value=False, default_value=float(0),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='string_value', full_name='google.protobuf.UninterpretedOption.string_value', index=5,\n        number=7, type=12, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\",\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='aggregate_value', full_name='google.protobuf.UninterpretedOption.aggregate_value', index=6,\n        number=8, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[_UNINTERPRETEDOPTION_NAMEPART, ],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _SOURCECODEINFO_LOCATION = _descriptor.Descriptor(\n    name='Location',\n    full_name='google.protobuf.SourceCodeInfo.Location',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='path', full_name='google.protobuf.SourceCodeInfo.Location.path', index=0,\n        number=1, type=5, cpp_type=1, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='span', full_name='google.protobuf.SourceCodeInfo.Location.span', index=1,\n        number=2, type=5, cpp_type=1, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='leading_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_comments', index=2,\n        number=3, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='trailing_comments', full_name='google.protobuf.SourceCodeInfo.Location.trailing_comments', index=3,\n        number=4, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='leading_detached_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_detached_comments', index=4,\n        number=6, type=9, cpp_type=9, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _SOURCECODEINFO = _descriptor.Descriptor(\n    name='SourceCodeInfo',\n    full_name='google.protobuf.SourceCodeInfo',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='location', full_name='google.protobuf.SourceCodeInfo.location', index=0,\n        number=1, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[_SOURCECODEINFO_LOCATION, ],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n\n  _GENERATEDCODEINFO_ANNOTATION = _descriptor.Descriptor(\n    name='Annotation',\n    full_name='google.protobuf.GeneratedCodeInfo.Annotation',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='path', full_name='google.protobuf.GeneratedCodeInfo.Annotation.path', index=0,\n        number=1, type=5, cpp_type=1, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='source_file', full_name='google.protobuf.GeneratedCodeInfo.Annotation.source_file', index=1,\n        number=2, type=9, cpp_type=9, label=1,\n        has_default_value=False, default_value=b\"\".decode('utf-8'),\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='begin', full_name='google.protobuf.GeneratedCodeInfo.Annotation.begin', index=2,\n        number=3, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n      _descriptor.FieldDescriptor(\n        name='end', full_name='google.protobuf.GeneratedCodeInfo.Annotation.end', index=3,\n        number=4, type=5, cpp_type=1, label=1,\n        has_default_value=False, default_value=0,\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _GENERATEDCODEINFO = _descriptor.Descriptor(\n    name='GeneratedCodeInfo',\n    full_name='google.protobuf.GeneratedCodeInfo',\n    filename=None,\n    file=DESCRIPTOR,\n    containing_type=None,\n    create_key=_descriptor._internal_create_key,\n    fields=[\n      _descriptor.FieldDescriptor(\n        name='annotation', full_name='google.protobuf.GeneratedCodeInfo.annotation', index=0,\n        number=1, type=11, cpp_type=10, label=3,\n        has_default_value=False, default_value=[],\n        message_type=None, enum_type=None, containing_type=None,\n        is_extension=False, extension_scope=None,\n        serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    ],\n    extensions=[\n    ],\n    nested_types=[_GENERATEDCODEINFO_ANNOTATION, ],\n    enum_types=[\n    ],\n    serialized_options=None,\n    is_extendable=False,\n    syntax='proto2',\n    extension_ranges=[],\n    oneofs=[\n    ],\n  )\n\n  _FILEDESCRIPTORSET.fields_by_name['file'].message_type = _FILEDESCRIPTORPROTO\n  _FILEDESCRIPTORPROTO.fields_by_name['message_type'].message_type = _DESCRIPTORPROTO\n  _FILEDESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO\n  _FILEDESCRIPTORPROTO.fields_by_name['service'].message_type = _SERVICEDESCRIPTORPROTO\n  _FILEDESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO\n  _FILEDESCRIPTORPROTO.fields_by_name['options'].message_type = _FILEOPTIONS\n  _FILEDESCRIPTORPROTO.fields_by_name['source_code_info'].message_type = _SOURCECODEINFO\n  _DESCRIPTORPROTO_EXTENSIONRANGE.fields_by_name['options'].message_type = _EXTENSIONRANGEOPTIONS\n  _DESCRIPTORPROTO_EXTENSIONRANGE.containing_type = _DESCRIPTORPROTO\n  _DESCRIPTORPROTO_RESERVEDRANGE.containing_type = _DESCRIPTORPROTO\n  _DESCRIPTORPROTO.fields_by_name['field'].message_type = _FIELDDESCRIPTORPROTO\n  _DESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO\n  _DESCRIPTORPROTO.fields_by_name['nested_type'].message_type = _DESCRIPTORPROTO\n  _DESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO\n  _DESCRIPTORPROTO.fields_by_name['extension_range'].message_type = _DESCRIPTORPROTO_EXTENSIONRANGE\n  _DESCRIPTORPROTO.fields_by_name['oneof_decl'].message_type = _ONEOFDESCRIPTORPROTO\n  _DESCRIPTORPROTO.fields_by_name['options'].message_type = _MESSAGEOPTIONS\n  _DESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _DESCRIPTORPROTO_RESERVEDRANGE\n  _EXTENSIONRANGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _FIELDDESCRIPTORPROTO.fields_by_name['label'].enum_type = _FIELDDESCRIPTORPROTO_LABEL\n  _FIELDDESCRIPTORPROTO.fields_by_name['type'].enum_type = _FIELDDESCRIPTORPROTO_TYPE\n  _FIELDDESCRIPTORPROTO.fields_by_name['options'].message_type = _FIELDOPTIONS\n  _FIELDDESCRIPTORPROTO_TYPE.containing_type = _FIELDDESCRIPTORPROTO\n  _FIELDDESCRIPTORPROTO_LABEL.containing_type = _FIELDDESCRIPTORPROTO\n  _ONEOFDESCRIPTORPROTO.fields_by_name['options'].message_type = _ONEOFOPTIONS\n  _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE.containing_type = _ENUMDESCRIPTORPROTO\n  _ENUMDESCRIPTORPROTO.fields_by_name['value'].message_type = _ENUMVALUEDESCRIPTORPROTO\n  _ENUMDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMOPTIONS\n  _ENUMDESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE\n  _ENUMVALUEDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMVALUEOPTIONS\n  _SERVICEDESCRIPTORPROTO.fields_by_name['method'].message_type = _METHODDESCRIPTORPROTO\n  _SERVICEDESCRIPTORPROTO.fields_by_name['options'].message_type = _SERVICEOPTIONS\n  _METHODDESCRIPTORPROTO.fields_by_name['options'].message_type = _METHODOPTIONS\n  _FILEOPTIONS.fields_by_name['optimize_for'].enum_type = _FILEOPTIONS_OPTIMIZEMODE\n  _FILEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _FILEOPTIONS_OPTIMIZEMODE.containing_type = _FILEOPTIONS\n  _MESSAGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _FIELDOPTIONS.fields_by_name['ctype'].enum_type = _FIELDOPTIONS_CTYPE\n  _FIELDOPTIONS.fields_by_name['jstype'].enum_type = _FIELDOPTIONS_JSTYPE\n  _FIELDOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _FIELDOPTIONS_CTYPE.containing_type = _FIELDOPTIONS\n  _FIELDOPTIONS_JSTYPE.containing_type = _FIELDOPTIONS\n  _ONEOFOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _ENUMOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _ENUMVALUEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _SERVICEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _METHODOPTIONS.fields_by_name['idempotency_level'].enum_type = _METHODOPTIONS_IDEMPOTENCYLEVEL\n  _METHODOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION\n  _METHODOPTIONS_IDEMPOTENCYLEVEL.containing_type = _METHODOPTIONS\n  _UNINTERPRETEDOPTION_NAMEPART.containing_type = _UNINTERPRETEDOPTION\n  _UNINTERPRETEDOPTION.fields_by_name['name'].message_type = _UNINTERPRETEDOPTION_NAMEPART\n  _SOURCECODEINFO_LOCATION.containing_type = _SOURCECODEINFO\n  _SOURCECODEINFO.fields_by_name['location'].message_type = _SOURCECODEINFO_LOCATION\n  _GENERATEDCODEINFO_ANNOTATION.containing_type = _GENERATEDCODEINFO\n  _GENERATEDCODEINFO.fields_by_name['annotation'].message_type = _GENERATEDCODEINFO_ANNOTATION\n  DESCRIPTOR.message_types_by_name['FileDescriptorSet'] = _FILEDESCRIPTORSET\n  DESCRIPTOR.message_types_by_name['FileDescriptorProto'] = _FILEDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['DescriptorProto'] = _DESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['ExtensionRangeOptions'] = _EXTENSIONRANGEOPTIONS\n  DESCRIPTOR.message_types_by_name['FieldDescriptorProto'] = _FIELDDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['OneofDescriptorProto'] = _ONEOFDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['EnumDescriptorProto'] = _ENUMDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['EnumValueDescriptorProto'] = _ENUMVALUEDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['ServiceDescriptorProto'] = _SERVICEDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['MethodDescriptorProto'] = _METHODDESCRIPTORPROTO\n  DESCRIPTOR.message_types_by_name['FileOptions'] = _FILEOPTIONS\n  DESCRIPTOR.message_types_by_name['MessageOptions'] = _MESSAGEOPTIONS\n  DESCRIPTOR.message_types_by_name['FieldOptions'] = _FIELDOPTIONS\n  DESCRIPTOR.message_types_by_name['OneofOptions'] = _ONEOFOPTIONS\n  DESCRIPTOR.message_types_by_name['EnumOptions'] = _ENUMOPTIONS\n  DESCRIPTOR.message_types_by_name['EnumValueOptions'] = _ENUMVALUEOPTIONS\n  DESCRIPTOR.message_types_by_name['ServiceOptions'] = _SERVICEOPTIONS\n  DESCRIPTOR.message_types_by_name['MethodOptions'] = _METHODOPTIONS\n  DESCRIPTOR.message_types_by_name['UninterpretedOption'] = _UNINTERPRETEDOPTION\n  DESCRIPTOR.message_types_by_name['SourceCodeInfo'] = _SOURCECODEINFO\n  DESCRIPTOR.message_types_by_name['GeneratedCodeInfo'] = _GENERATEDCODEINFO\n  _sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\nelse:\n  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.descriptor_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  _FILEDESCRIPTORSET._serialized_start=53\n  _FILEDESCRIPTORSET._serialized_end=124\n  _FILEDESCRIPTORPROTO._serialized_start=127\n  _FILEDESCRIPTORPROTO._serialized_end=602\n  _DESCRIPTORPROTO._serialized_start=605\n  _DESCRIPTORPROTO._serialized_end=1286\n  _DESCRIPTORPROTO_EXTENSIONRANGE._serialized_start=1140\n  _DESCRIPTORPROTO_EXTENSIONRANGE._serialized_end=1241\n  _DESCRIPTORPROTO_RESERVEDRANGE._serialized_start=1243\n  _DESCRIPTORPROTO_RESERVEDRANGE._serialized_end=1286\n  _EXTENSIONRANGEOPTIONS._serialized_start=1288\n  _EXTENSIONRANGEOPTIONS._serialized_end=1391\n  _FIELDDESCRIPTORPROTO._serialized_start=1394\n  _FIELDDESCRIPTORPROTO._serialized_end=2119\n  _FIELDDESCRIPTORPROTO_TYPE._serialized_start=1740\n  _FIELDDESCRIPTORPROTO_TYPE._serialized_end=2050\n  _FIELDDESCRIPTORPROTO_LABEL._serialized_start=2052\n  _FIELDDESCRIPTORPROTO_LABEL._serialized_end=2119\n  _ONEOFDESCRIPTORPROTO._serialized_start=2121\n  _ONEOFDESCRIPTORPROTO._serialized_end=2205\n  _ENUMDESCRIPTORPROTO._serialized_start=2208\n  _ENUMDESCRIPTORPROTO._serialized_end=2500\n  _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._serialized_start=2453\n  _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._serialized_end=2500\n  _ENUMVALUEDESCRIPTORPROTO._serialized_start=2502\n  _ENUMVALUEDESCRIPTORPROTO._serialized_end=2610\n  _SERVICEDESCRIPTORPROTO._serialized_start=2613\n  _SERVICEDESCRIPTORPROTO._serialized_end=2757\n  _METHODDESCRIPTORPROTO._serialized_start=2760\n  _METHODDESCRIPTORPROTO._serialized_end=2953\n  _FILEOPTIONS._serialized_start=2956\n  _FILEOPTIONS._serialized_end=3761\n  _FILEOPTIONS_OPTIMIZEMODE._serialized_start=3686\n  _FILEOPTIONS_OPTIMIZEMODE._serialized_end=3744\n  _MESSAGEOPTIONS._serialized_start=3764\n  _MESSAGEOPTIONS._serialized_end=4024\n  _FIELDOPTIONS._serialized_start=4027\n  _FIELDOPTIONS._serialized_end=4473\n  _FIELDOPTIONS_CTYPE._serialized_start=4354\n  _FIELDOPTIONS_CTYPE._serialized_end=4401\n  _FIELDOPTIONS_JSTYPE._serialized_start=4403\n  _FIELDOPTIONS_JSTYPE._serialized_end=4456\n  _ONEOFOPTIONS._serialized_start=4475\n  _ONEOFOPTIONS._serialized_end=4569\n  _ENUMOPTIONS._serialized_start=4572\n  _ENUMOPTIONS._serialized_end=4719\n  _ENUMVALUEOPTIONS._serialized_start=4721\n  _ENUMVALUEOPTIONS._serialized_end=4846\n  _SERVICEOPTIONS._serialized_start=4848\n  _SERVICEOPTIONS._serialized_end=4971\n  _METHODOPTIONS._serialized_start=4974\n  _METHODOPTIONS._serialized_end=5275\n  _METHODOPTIONS_IDEMPOTENCYLEVEL._serialized_start=5184\n  _METHODOPTIONS_IDEMPOTENCYLEVEL._serialized_end=5264\n  _UNINTERPRETEDOPTION._serialized_start=5278\n  _UNINTERPRETEDOPTION._serialized_end=5564\n  _UNINTERPRETEDOPTION_NAMEPART._serialized_start=5513\n  _UNINTERPRETEDOPTION_NAMEPART._serialized_end=5564\n  _SOURCECODEINFO._serialized_start=5567\n  _SOURCECODEINFO._serialized_end=5780\n  _SOURCECODEINFO_LOCATION._serialized_start=5646\n  _SOURCECODEINFO_LOCATION._serialized_end=5780\n  _GENERATEDCODEINFO._serialized_start=5783\n  _GENERATEDCODEINFO._serialized_end=5950\n  _GENERATEDCODEINFO_ANNOTATION._serialized_start=5871\n  _GENERATEDCODEINFO_ANNOTATION._serialized_end=5950\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/descriptor_pool.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Provides DescriptorPool to use as a container for proto2 descriptors.\n\nThe DescriptorPool is used in conjection with a DescriptorDatabase to maintain\na collection of protocol buffer descriptors for use when dynamically creating\nmessage types at runtime.\n\nFor most applications protocol buffers should be used via modules generated by\nthe protocol buffer compiler tool. This should only be used when the type of\nprotocol buffers used in an application or library cannot be predetermined.\n\nBelow is a straightforward example on how to use this class::\n\n  pool = DescriptorPool()\n  file_descriptor_protos = [ ... ]\n  for file_descriptor_proto in file_descriptor_protos:\n    pool.Add(file_descriptor_proto)\n  my_message_descriptor = pool.FindMessageTypeByName('some.package.MessageType')\n\nThe message descriptor can be used in conjunction with the message_factory\nmodule in order to create a protocol buffer class that can be encoded and\ndecoded.\n\nIf you want to get a Python class for the specified proto, use the\nhelper functions inside google.protobuf.message_factory\ndirectly instead of this class.\n\"\"\"\n\n__author__ = 'matthewtoia@google.com (Matt Toia)'\n\nimport collections\nimport warnings\n\nfrom google.protobuf import descriptor\nfrom google.protobuf import descriptor_database\nfrom google.protobuf import text_encoding\n\n\n_USE_C_DESCRIPTORS = descriptor._USE_C_DESCRIPTORS  # pylint: disable=protected-access\n\n\ndef _Deprecated(func):\n  \"\"\"Mark functions as deprecated.\"\"\"\n\n  def NewFunc(*args, **kwargs):\n    warnings.warn(\n        'Call to deprecated function %s(). Note: Do add unlinked descriptors '\n        'to descriptor_pool is wrong. Use Add() or AddSerializedFile() '\n        'instead.' % func.__name__,\n        category=DeprecationWarning)\n    return func(*args, **kwargs)\n  NewFunc.__name__ = func.__name__\n  NewFunc.__doc__ = func.__doc__\n  NewFunc.__dict__.update(func.__dict__)\n  return NewFunc\n\n\ndef _NormalizeFullyQualifiedName(name):\n  \"\"\"Remove leading period from fully-qualified type name.\n\n  Due to b/13860351 in descriptor_database.py, types in the root namespace are\n  generated with a leading period. This function removes that prefix.\n\n  Args:\n    name (str): The fully-qualified symbol name.\n\n  Returns:\n    str: The normalized fully-qualified symbol name.\n  \"\"\"\n  return name.lstrip('.')\n\n\ndef _OptionsOrNone(descriptor_proto):\n  \"\"\"Returns the value of the field `options`, or None if it is not set.\"\"\"\n  if descriptor_proto.HasField('options'):\n    return descriptor_proto.options\n  else:\n    return None\n\n\ndef _IsMessageSetExtension(field):\n  return (field.is_extension and\n          field.containing_type.has_options and\n          field.containing_type.GetOptions().message_set_wire_format and\n          field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and\n          field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL)\n\n\nclass DescriptorPool(object):\n  \"\"\"A collection of protobufs dynamically constructed by descriptor protos.\"\"\"\n\n  if _USE_C_DESCRIPTORS:\n\n    def __new__(cls, descriptor_db=None):\n      # pylint: disable=protected-access\n      return descriptor._message.DescriptorPool(descriptor_db)\n\n  def __init__(self, descriptor_db=None):\n    \"\"\"Initializes a Pool of proto buffs.\n\n    The descriptor_db argument to the constructor is provided to allow\n    specialized file descriptor proto lookup code to be triggered on demand. An\n    example would be an implementation which will read and compile a file\n    specified in a call to FindFileByName() and not require the call to Add()\n    at all. Results from this database will be cached internally here as well.\n\n    Args:\n      descriptor_db: A secondary source of file descriptors.\n    \"\"\"\n\n    self._internal_db = descriptor_database.DescriptorDatabase()\n    self._descriptor_db = descriptor_db\n    self._descriptors = {}\n    self._enum_descriptors = {}\n    self._service_descriptors = {}\n    self._file_descriptors = {}\n    self._toplevel_extensions = {}\n    # TODO(jieluo): Remove _file_desc_by_toplevel_extension after\n    # maybe year 2020 for compatibility issue (with 3.4.1 only).\n    self._file_desc_by_toplevel_extension = {}\n    self._top_enum_values = {}\n    # We store extensions in two two-level mappings: The first key is the\n    # descriptor of the message being extended, the second key is the extension\n    # full name or its tag number.\n    self._extensions_by_name = collections.defaultdict(dict)\n    self._extensions_by_number = collections.defaultdict(dict)\n\n  def _CheckConflictRegister(self, desc, desc_name, file_name):\n    \"\"\"Check if the descriptor name conflicts with another of the same name.\n\n    Args:\n      desc: Descriptor of a message, enum, service, extension or enum value.\n      desc_name (str): the full name of desc.\n      file_name (str): The file name of descriptor.\n    \"\"\"\n    for register, descriptor_type in [\n        (self._descriptors, descriptor.Descriptor),\n        (self._enum_descriptors, descriptor.EnumDescriptor),\n        (self._service_descriptors, descriptor.ServiceDescriptor),\n        (self._toplevel_extensions, descriptor.FieldDescriptor),\n        (self._top_enum_values, descriptor.EnumValueDescriptor)]:\n      if desc_name in register:\n        old_desc = register[desc_name]\n        if isinstance(old_desc, descriptor.EnumValueDescriptor):\n          old_file = old_desc.type.file.name\n        else:\n          old_file = old_desc.file.name\n\n        if not isinstance(desc, descriptor_type) or (\n            old_file != file_name):\n          error_msg = ('Conflict register for file \"' + file_name +\n                       '\": ' + desc_name +\n                       ' is already defined in file \"' +\n                       old_file + '\". Please fix the conflict by adding '\n                       'package name on the proto file, or use different '\n                       'name for the duplication.')\n          if isinstance(desc, descriptor.EnumValueDescriptor):\n            error_msg += ('\\nNote: enum values appear as '\n                          'siblings of the enum type instead of '\n                          'children of it.')\n\n          raise TypeError(error_msg)\n\n        return\n\n  def Add(self, file_desc_proto):\n    \"\"\"Adds the FileDescriptorProto and its types to this pool.\n\n    Args:\n      file_desc_proto (FileDescriptorProto): The file descriptor to add.\n    \"\"\"\n\n    self._internal_db.Add(file_desc_proto)\n\n  def AddSerializedFile(self, serialized_file_desc_proto):\n    \"\"\"Adds the FileDescriptorProto and its types to this pool.\n\n    Args:\n      serialized_file_desc_proto (bytes): A bytes string, serialization of the\n        :class:`FileDescriptorProto` to add.\n\n    Returns:\n      FileDescriptor: Descriptor for the added file.\n    \"\"\"\n\n    # pylint: disable=g-import-not-at-top\n    from google.protobuf import descriptor_pb2\n    file_desc_proto = descriptor_pb2.FileDescriptorProto.FromString(\n        serialized_file_desc_proto)\n    file_desc = self._ConvertFileProtoToFileDescriptor(file_desc_proto)\n    file_desc.serialized_pb = serialized_file_desc_proto\n    return file_desc\n\n  # Add Descriptor to descriptor pool is dreprecated. Please use Add()\n  # or AddSerializedFile() to add a FileDescriptorProto instead.\n  @_Deprecated\n  def AddDescriptor(self, desc):\n    self._AddDescriptor(desc)\n\n  # Never call this method. It is for internal usage only.\n  def _AddDescriptor(self, desc):\n    \"\"\"Adds a Descriptor to the pool, non-recursively.\n\n    If the Descriptor contains nested messages or enums, the caller must\n    explicitly register them. This method also registers the FileDescriptor\n    associated with the message.\n\n    Args:\n      desc: A Descriptor.\n    \"\"\"\n    if not isinstance(desc, descriptor.Descriptor):\n      raise TypeError('Expected instance of descriptor.Descriptor.')\n\n    self._CheckConflictRegister(desc, desc.full_name, desc.file.name)\n\n    self._descriptors[desc.full_name] = desc\n    self._AddFileDescriptor(desc.file)\n\n  # Add EnumDescriptor to descriptor pool is dreprecated. Please use Add()\n  # or AddSerializedFile() to add a FileDescriptorProto instead.\n  @_Deprecated\n  def AddEnumDescriptor(self, enum_desc):\n    self._AddEnumDescriptor(enum_desc)\n\n  # Never call this method. It is for internal usage only.\n  def _AddEnumDescriptor(self, enum_desc):\n    \"\"\"Adds an EnumDescriptor to the pool.\n\n    This method also registers the FileDescriptor associated with the enum.\n\n    Args:\n      enum_desc: An EnumDescriptor.\n    \"\"\"\n\n    if not isinstance(enum_desc, descriptor.EnumDescriptor):\n      raise TypeError('Expected instance of descriptor.EnumDescriptor.')\n\n    file_name = enum_desc.file.name\n    self._CheckConflictRegister(enum_desc, enum_desc.full_name, file_name)\n    self._enum_descriptors[enum_desc.full_name] = enum_desc\n\n    # Top enum values need to be indexed.\n    # Count the number of dots to see whether the enum is toplevel or nested\n    # in a message. We cannot use enum_desc.containing_type at this stage.\n    if enum_desc.file.package:\n      top_level = (enum_desc.full_name.count('.')\n                   - enum_desc.file.package.count('.') == 1)\n    else:\n      top_level = enum_desc.full_name.count('.') == 0\n    if top_level:\n      file_name = enum_desc.file.name\n      package = enum_desc.file.package\n      for enum_value in enum_desc.values:\n        full_name = _NormalizeFullyQualifiedName(\n            '.'.join((package, enum_value.name)))\n        self._CheckConflictRegister(enum_value, full_name, file_name)\n        self._top_enum_values[full_name] = enum_value\n    self._AddFileDescriptor(enum_desc.file)\n\n  # Add ServiceDescriptor to descriptor pool is dreprecated. Please use Add()\n  # or AddSerializedFile() to add a FileDescriptorProto instead.\n  @_Deprecated\n  def AddServiceDescriptor(self, service_desc):\n    self._AddServiceDescriptor(service_desc)\n\n  # Never call this method. It is for internal usage only.\n  def _AddServiceDescriptor(self, service_desc):\n    \"\"\"Adds a ServiceDescriptor to the pool.\n\n    Args:\n      service_desc: A ServiceDescriptor.\n    \"\"\"\n\n    if not isinstance(service_desc, descriptor.ServiceDescriptor):\n      raise TypeError('Expected instance of descriptor.ServiceDescriptor.')\n\n    self._CheckConflictRegister(service_desc, service_desc.full_name,\n                                service_desc.file.name)\n    self._service_descriptors[service_desc.full_name] = service_desc\n\n  # Add ExtensionDescriptor to descriptor pool is dreprecated. Please use Add()\n  # or AddSerializedFile() to add a FileDescriptorProto instead.\n  @_Deprecated\n  def AddExtensionDescriptor(self, extension):\n    self._AddExtensionDescriptor(extension)\n\n  # Never call this method. It is for internal usage only.\n  def _AddExtensionDescriptor(self, extension):\n    \"\"\"Adds a FieldDescriptor describing an extension to the pool.\n\n    Args:\n      extension: A FieldDescriptor.\n\n    Raises:\n      AssertionError: when another extension with the same number extends the\n        same message.\n      TypeError: when the specified extension is not a\n        descriptor.FieldDescriptor.\n    \"\"\"\n    if not (isinstance(extension, descriptor.FieldDescriptor) and\n            extension.is_extension):\n      raise TypeError('Expected an extension descriptor.')\n\n    if extension.extension_scope is None:\n      self._toplevel_extensions[extension.full_name] = extension\n\n    try:\n      existing_desc = self._extensions_by_number[\n          extension.containing_type][extension.number]\n    except KeyError:\n      pass\n    else:\n      if extension is not existing_desc:\n        raise AssertionError(\n            'Extensions \"%s\" and \"%s\" both try to extend message type \"%s\" '\n            'with field number %d.' %\n            (extension.full_name, existing_desc.full_name,\n             extension.containing_type.full_name, extension.number))\n\n    self._extensions_by_number[extension.containing_type][\n        extension.number] = extension\n    self._extensions_by_name[extension.containing_type][\n        extension.full_name] = extension\n\n    # Also register MessageSet extensions with the type name.\n    if _IsMessageSetExtension(extension):\n      self._extensions_by_name[extension.containing_type][\n          extension.message_type.full_name] = extension\n\n  @_Deprecated\n  def AddFileDescriptor(self, file_desc):\n    self._InternalAddFileDescriptor(file_desc)\n\n  # Never call this method. It is for internal usage only.\n  def _InternalAddFileDescriptor(self, file_desc):\n    \"\"\"Adds a FileDescriptor to the pool, non-recursively.\n\n    If the FileDescriptor contains messages or enums, the caller must explicitly\n    register them.\n\n    Args:\n      file_desc: A FileDescriptor.\n    \"\"\"\n\n    self._AddFileDescriptor(file_desc)\n    # TODO(jieluo): This is a temporary solution for FieldDescriptor.file.\n    # FieldDescriptor.file is added in code gen. Remove this solution after\n    # maybe 2020 for compatibility reason (with 3.4.1 only).\n    for extension in file_desc.extensions_by_name.values():\n      self._file_desc_by_toplevel_extension[\n          extension.full_name] = file_desc\n\n  def _AddFileDescriptor(self, file_desc):\n    \"\"\"Adds a FileDescriptor to the pool, non-recursively.\n\n    If the FileDescriptor contains messages or enums, the caller must explicitly\n    register them.\n\n    Args:\n      file_desc: A FileDescriptor.\n    \"\"\"\n\n    if not isinstance(file_desc, descriptor.FileDescriptor):\n      raise TypeError('Expected instance of descriptor.FileDescriptor.')\n    self._file_descriptors[file_desc.name] = file_desc\n\n  def FindFileByName(self, file_name):\n    \"\"\"Gets a FileDescriptor by file name.\n\n    Args:\n      file_name (str): The path to the file to get a descriptor for.\n\n    Returns:\n      FileDescriptor: The descriptor for the named file.\n\n    Raises:\n      KeyError: if the file cannot be found in the pool.\n    \"\"\"\n\n    try:\n      return self._file_descriptors[file_name]\n    except KeyError:\n      pass\n\n    try:\n      file_proto = self._internal_db.FindFileByName(file_name)\n    except KeyError as error:\n      if self._descriptor_db:\n        file_proto = self._descriptor_db.FindFileByName(file_name)\n      else:\n        raise error\n    if not file_proto:\n      raise KeyError('Cannot find a file named %s' % file_name)\n    return self._ConvertFileProtoToFileDescriptor(file_proto)\n\n  def FindFileContainingSymbol(self, symbol):\n    \"\"\"Gets the FileDescriptor for the file containing the specified symbol.\n\n    Args:\n      symbol (str): The name of the symbol to search for.\n\n    Returns:\n      FileDescriptor: Descriptor for the file that contains the specified\n      symbol.\n\n    Raises:\n      KeyError: if the file cannot be found in the pool.\n    \"\"\"\n\n    symbol = _NormalizeFullyQualifiedName(symbol)\n    try:\n      return self._InternalFindFileContainingSymbol(symbol)\n    except KeyError:\n      pass\n\n    try:\n      # Try fallback database. Build and find again if possible.\n      self._FindFileContainingSymbolInDb(symbol)\n      return self._InternalFindFileContainingSymbol(symbol)\n    except KeyError:\n      raise KeyError('Cannot find a file containing %s' % symbol)\n\n  def _InternalFindFileContainingSymbol(self, symbol):\n    \"\"\"Gets the already built FileDescriptor containing the specified symbol.\n\n    Args:\n      symbol (str): The name of the symbol to search for.\n\n    Returns:\n      FileDescriptor: Descriptor for the file that contains the specified\n      symbol.\n\n    Raises:\n      KeyError: if the file cannot be found in the pool.\n    \"\"\"\n    try:\n      return self._descriptors[symbol].file\n    except KeyError:\n      pass\n\n    try:\n      return self._enum_descriptors[symbol].file\n    except KeyError:\n      pass\n\n    try:\n      return self._service_descriptors[symbol].file\n    except KeyError:\n      pass\n\n    try:\n      return self._top_enum_values[symbol].type.file\n    except KeyError:\n      pass\n\n    try:\n      return self._file_desc_by_toplevel_extension[symbol]\n    except KeyError:\n      pass\n\n    # Try fields, enum values and nested extensions inside a message.\n    top_name, _, sub_name = symbol.rpartition('.')\n    try:\n      message = self.FindMessageTypeByName(top_name)\n      assert (sub_name in message.extensions_by_name or\n              sub_name in message.fields_by_name or\n              sub_name in message.enum_values_by_name)\n      return message.file\n    except (KeyError, AssertionError):\n      raise KeyError('Cannot find a file containing %s' % symbol)\n\n  def FindMessageTypeByName(self, full_name):\n    \"\"\"Loads the named descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the descriptor to load.\n\n    Returns:\n      Descriptor: The descriptor for the named type.\n\n    Raises:\n      KeyError: if the message cannot be found in the pool.\n    \"\"\"\n\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    if full_name not in self._descriptors:\n      self._FindFileContainingSymbolInDb(full_name)\n    return self._descriptors[full_name]\n\n  def FindEnumTypeByName(self, full_name):\n    \"\"\"Loads the named enum descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the enum descriptor to load.\n\n    Returns:\n      EnumDescriptor: The enum descriptor for the named type.\n\n    Raises:\n      KeyError: if the enum cannot be found in the pool.\n    \"\"\"\n\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    if full_name not in self._enum_descriptors:\n      self._FindFileContainingSymbolInDb(full_name)\n    return self._enum_descriptors[full_name]\n\n  def FindFieldByName(self, full_name):\n    \"\"\"Loads the named field descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the field descriptor to load.\n\n    Returns:\n      FieldDescriptor: The field descriptor for the named field.\n\n    Raises:\n      KeyError: if the field cannot be found in the pool.\n    \"\"\"\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    message_name, _, field_name = full_name.rpartition('.')\n    message_descriptor = self.FindMessageTypeByName(message_name)\n    return message_descriptor.fields_by_name[field_name]\n\n  def FindOneofByName(self, full_name):\n    \"\"\"Loads the named oneof descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the oneof descriptor to load.\n\n    Returns:\n      OneofDescriptor: The oneof descriptor for the named oneof.\n\n    Raises:\n      KeyError: if the oneof cannot be found in the pool.\n    \"\"\"\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    message_name, _, oneof_name = full_name.rpartition('.')\n    message_descriptor = self.FindMessageTypeByName(message_name)\n    return message_descriptor.oneofs_by_name[oneof_name]\n\n  def FindExtensionByName(self, full_name):\n    \"\"\"Loads the named extension descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the extension descriptor to load.\n\n    Returns:\n      FieldDescriptor: The field descriptor for the named extension.\n\n    Raises:\n      KeyError: if the extension cannot be found in the pool.\n    \"\"\"\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    try:\n      # The proto compiler does not give any link between the FileDescriptor\n      # and top-level extensions unless the FileDescriptorProto is added to\n      # the DescriptorDatabase, but this can impact memory usage.\n      # So we registered these extensions by name explicitly.\n      return self._toplevel_extensions[full_name]\n    except KeyError:\n      pass\n    message_name, _, extension_name = full_name.rpartition('.')\n    try:\n      # Most extensions are nested inside a message.\n      scope = self.FindMessageTypeByName(message_name)\n    except KeyError:\n      # Some extensions are defined at file scope.\n      scope = self._FindFileContainingSymbolInDb(full_name)\n    return scope.extensions_by_name[extension_name]\n\n  def FindExtensionByNumber(self, message_descriptor, number):\n    \"\"\"Gets the extension of the specified message with the specified number.\n\n    Extensions have to be registered to this pool by calling :func:`Add` or\n    :func:`AddExtensionDescriptor`.\n\n    Args:\n      message_descriptor (Descriptor): descriptor of the extended message.\n      number (int): Number of the extension field.\n\n    Returns:\n      FieldDescriptor: The descriptor for the extension.\n\n    Raises:\n      KeyError: when no extension with the given number is known for the\n        specified message.\n    \"\"\"\n    try:\n      return self._extensions_by_number[message_descriptor][number]\n    except KeyError:\n      self._TryLoadExtensionFromDB(message_descriptor, number)\n      return self._extensions_by_number[message_descriptor][number]\n\n  def FindAllExtensions(self, message_descriptor):\n    \"\"\"Gets all the known extensions of a given message.\n\n    Extensions have to be registered to this pool by build related\n    :func:`Add` or :func:`AddExtensionDescriptor`.\n\n    Args:\n      message_descriptor (Descriptor): Descriptor of the extended message.\n\n    Returns:\n      list[FieldDescriptor]: Field descriptors describing the extensions.\n    \"\"\"\n    # Fallback to descriptor db if FindAllExtensionNumbers is provided.\n    if self._descriptor_db and hasattr(\n        self._descriptor_db, 'FindAllExtensionNumbers'):\n      full_name = message_descriptor.full_name\n      all_numbers = self._descriptor_db.FindAllExtensionNumbers(full_name)\n      for number in all_numbers:\n        if number in self._extensions_by_number[message_descriptor]:\n          continue\n        self._TryLoadExtensionFromDB(message_descriptor, number)\n\n    return list(self._extensions_by_number[message_descriptor].values())\n\n  def _TryLoadExtensionFromDB(self, message_descriptor, number):\n    \"\"\"Try to Load extensions from descriptor db.\n\n    Args:\n      message_descriptor: descriptor of the extended message.\n      number: the extension number that needs to be loaded.\n    \"\"\"\n    if not self._descriptor_db:\n      return\n    # Only supported when FindFileContainingExtension is provided.\n    if not hasattr(\n        self._descriptor_db, 'FindFileContainingExtension'):\n      return\n\n    full_name = message_descriptor.full_name\n    file_proto = self._descriptor_db.FindFileContainingExtension(\n        full_name, number)\n\n    if file_proto is None:\n      return\n\n    try:\n      self._ConvertFileProtoToFileDescriptor(file_proto)\n    except:\n      warn_msg = ('Unable to load proto file %s for extension number %d.' %\n                  (file_proto.name, number))\n      warnings.warn(warn_msg, RuntimeWarning)\n\n  def FindServiceByName(self, full_name):\n    \"\"\"Loads the named service descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the service descriptor to load.\n\n    Returns:\n      ServiceDescriptor: The service descriptor for the named service.\n\n    Raises:\n      KeyError: if the service cannot be found in the pool.\n    \"\"\"\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    if full_name not in self._service_descriptors:\n      self._FindFileContainingSymbolInDb(full_name)\n    return self._service_descriptors[full_name]\n\n  def FindMethodByName(self, full_name):\n    \"\"\"Loads the named service method descriptor from the pool.\n\n    Args:\n      full_name (str): The full name of the method descriptor to load.\n\n    Returns:\n      MethodDescriptor: The method descriptor for the service method.\n\n    Raises:\n      KeyError: if the method cannot be found in the pool.\n    \"\"\"\n    full_name = _NormalizeFullyQualifiedName(full_name)\n    service_name, _, method_name = full_name.rpartition('.')\n    service_descriptor = self.FindServiceByName(service_name)\n    return service_descriptor.methods_by_name[method_name]\n\n  def _FindFileContainingSymbolInDb(self, symbol):\n    \"\"\"Finds the file in descriptor DB containing the specified symbol.\n\n    Args:\n      symbol (str): The name of the symbol to search for.\n\n    Returns:\n      FileDescriptor: The file that contains the specified symbol.\n\n    Raises:\n      KeyError: if the file cannot be found in the descriptor database.\n    \"\"\"\n    try:\n      file_proto = self._internal_db.FindFileContainingSymbol(symbol)\n    except KeyError as error:\n      if self._descriptor_db:\n        file_proto = self._descriptor_db.FindFileContainingSymbol(symbol)\n      else:\n        raise error\n    if not file_proto:\n      raise KeyError('Cannot find a file containing %s' % symbol)\n    return self._ConvertFileProtoToFileDescriptor(file_proto)\n\n  def _ConvertFileProtoToFileDescriptor(self, file_proto):\n    \"\"\"Creates a FileDescriptor from a proto or returns a cached copy.\n\n    This method also has the side effect of loading all the symbols found in\n    the file into the appropriate dictionaries in the pool.\n\n    Args:\n      file_proto: The proto to convert.\n\n    Returns:\n      A FileDescriptor matching the passed in proto.\n    \"\"\"\n    if file_proto.name not in self._file_descriptors:\n      built_deps = list(self._GetDeps(file_proto.dependency))\n      direct_deps = [self.FindFileByName(n) for n in file_proto.dependency]\n      public_deps = [direct_deps[i] for i in file_proto.public_dependency]\n\n      file_descriptor = descriptor.FileDescriptor(\n          pool=self,\n          name=file_proto.name,\n          package=file_proto.package,\n          syntax=file_proto.syntax,\n          options=_OptionsOrNone(file_proto),\n          serialized_pb=file_proto.SerializeToString(),\n          dependencies=direct_deps,\n          public_dependencies=public_deps,\n          # pylint: disable=protected-access\n          create_key=descriptor._internal_create_key)\n      scope = {}\n\n      # This loop extracts all the message and enum types from all the\n      # dependencies of the file_proto. This is necessary to create the\n      # scope of available message types when defining the passed in\n      # file proto.\n      for dependency in built_deps:\n        scope.update(self._ExtractSymbols(\n            dependency.message_types_by_name.values()))\n        scope.update((_PrefixWithDot(enum.full_name), enum)\n                     for enum in dependency.enum_types_by_name.values())\n\n      for message_type in file_proto.message_type:\n        message_desc = self._ConvertMessageDescriptor(\n            message_type, file_proto.package, file_descriptor, scope,\n            file_proto.syntax)\n        file_descriptor.message_types_by_name[message_desc.name] = (\n            message_desc)\n\n      for enum_type in file_proto.enum_type:\n        file_descriptor.enum_types_by_name[enum_type.name] = (\n            self._ConvertEnumDescriptor(enum_type, file_proto.package,\n                                        file_descriptor, None, scope, True))\n\n      for index, extension_proto in enumerate(file_proto.extension):\n        extension_desc = self._MakeFieldDescriptor(\n            extension_proto, file_proto.package, index, file_descriptor,\n            is_extension=True)\n        extension_desc.containing_type = self._GetTypeFromScope(\n            file_descriptor.package, extension_proto.extendee, scope)\n        self._SetFieldType(extension_proto, extension_desc,\n                           file_descriptor.package, scope)\n        file_descriptor.extensions_by_name[extension_desc.name] = (\n            extension_desc)\n        self._file_desc_by_toplevel_extension[extension_desc.full_name] = (\n            file_descriptor)\n\n      for desc_proto in file_proto.message_type:\n        self._SetAllFieldTypes(file_proto.package, desc_proto, scope)\n\n      if file_proto.package:\n        desc_proto_prefix = _PrefixWithDot(file_proto.package)\n      else:\n        desc_proto_prefix = ''\n\n      for desc_proto in file_proto.message_type:\n        desc = self._GetTypeFromScope(\n            desc_proto_prefix, desc_proto.name, scope)\n        file_descriptor.message_types_by_name[desc_proto.name] = desc\n\n      for index, service_proto in enumerate(file_proto.service):\n        file_descriptor.services_by_name[service_proto.name] = (\n            self._MakeServiceDescriptor(service_proto, index, scope,\n                                        file_proto.package, file_descriptor))\n\n      self._file_descriptors[file_proto.name] = file_descriptor\n\n    # Add extensions to the pool\n    file_desc = self._file_descriptors[file_proto.name]\n    for extension in file_desc.extensions_by_name.values():\n      self._AddExtensionDescriptor(extension)\n    for message_type in file_desc.message_types_by_name.values():\n      for extension in message_type.extensions:\n        self._AddExtensionDescriptor(extension)\n\n    return file_desc\n\n  def _ConvertMessageDescriptor(self, desc_proto, package=None, file_desc=None,\n                                scope=None, syntax=None):\n    \"\"\"Adds the proto to the pool in the specified package.\n\n    Args:\n      desc_proto: The descriptor_pb2.DescriptorProto protobuf message.\n      package: The package the proto should be located in.\n      file_desc: The file containing this message.\n      scope: Dict mapping short and full symbols to message and enum types.\n      syntax: string indicating syntax of the file (\"proto2\" or \"proto3\")\n\n    Returns:\n      The added descriptor.\n    \"\"\"\n\n    if package:\n      desc_name = '.'.join((package, desc_proto.name))\n    else:\n      desc_name = desc_proto.name\n\n    if file_desc is None:\n      file_name = None\n    else:\n      file_name = file_desc.name\n\n    if scope is None:\n      scope = {}\n\n    nested = [\n        self._ConvertMessageDescriptor(\n            nested, desc_name, file_desc, scope, syntax)\n        for nested in desc_proto.nested_type]\n    enums = [\n        self._ConvertEnumDescriptor(enum, desc_name, file_desc, None,\n                                    scope, False)\n        for enum in desc_proto.enum_type]\n    fields = [self._MakeFieldDescriptor(field, desc_name, index, file_desc)\n              for index, field in enumerate(desc_proto.field)]\n    extensions = [\n        self._MakeFieldDescriptor(extension, desc_name, index, file_desc,\n                                  is_extension=True)\n        for index, extension in enumerate(desc_proto.extension)]\n    oneofs = [\n        # pylint: disable=g-complex-comprehension\n        descriptor.OneofDescriptor(\n            desc.name,\n            '.'.join((desc_name, desc.name)),\n            index,\n            None,\n            [],\n            _OptionsOrNone(desc),\n            # pylint: disable=protected-access\n            create_key=descriptor._internal_create_key)\n        for index, desc in enumerate(desc_proto.oneof_decl)\n    ]\n    extension_ranges = [(r.start, r.end) for r in desc_proto.extension_range]\n    if extension_ranges:\n      is_extendable = True\n    else:\n      is_extendable = False\n    desc = descriptor.Descriptor(\n        name=desc_proto.name,\n        full_name=desc_name,\n        filename=file_name,\n        containing_type=None,\n        fields=fields,\n        oneofs=oneofs,\n        nested_types=nested,\n        enum_types=enums,\n        extensions=extensions,\n        options=_OptionsOrNone(desc_proto),\n        is_extendable=is_extendable,\n        extension_ranges=extension_ranges,\n        file=file_desc,\n        serialized_start=None,\n        serialized_end=None,\n        syntax=syntax,\n        # pylint: disable=protected-access\n        create_key=descriptor._internal_create_key)\n    for nested in desc.nested_types:\n      nested.containing_type = desc\n    for enum in desc.enum_types:\n      enum.containing_type = desc\n    for field_index, field_desc in enumerate(desc_proto.field):\n      if field_desc.HasField('oneof_index'):\n        oneof_index = field_desc.oneof_index\n        oneofs[oneof_index].fields.append(fields[field_index])\n        fields[field_index].containing_oneof = oneofs[oneof_index]\n\n    scope[_PrefixWithDot(desc_name)] = desc\n    self._CheckConflictRegister(desc, desc.full_name, desc.file.name)\n    self._descriptors[desc_name] = desc\n    return desc\n\n  def _ConvertEnumDescriptor(self, enum_proto, package=None, file_desc=None,\n                             containing_type=None, scope=None, top_level=False):\n    \"\"\"Make a protobuf EnumDescriptor given an EnumDescriptorProto protobuf.\n\n    Args:\n      enum_proto: The descriptor_pb2.EnumDescriptorProto protobuf message.\n      package: Optional package name for the new message EnumDescriptor.\n      file_desc: The file containing the enum descriptor.\n      containing_type: The type containing this enum.\n      scope: Scope containing available types.\n      top_level: If True, the enum is a top level symbol. If False, the enum\n          is defined inside a message.\n\n    Returns:\n      The added descriptor\n    \"\"\"\n\n    if package:\n      enum_name = '.'.join((package, enum_proto.name))\n    else:\n      enum_name = enum_proto.name\n\n    if file_desc is None:\n      file_name = None\n    else:\n      file_name = file_desc.name\n\n    values = [self._MakeEnumValueDescriptor(value, index)\n              for index, value in enumerate(enum_proto.value)]\n    desc = descriptor.EnumDescriptor(name=enum_proto.name,\n                                     full_name=enum_name,\n                                     filename=file_name,\n                                     file=file_desc,\n                                     values=values,\n                                     containing_type=containing_type,\n                                     options=_OptionsOrNone(enum_proto),\n                                     # pylint: disable=protected-access\n                                     create_key=descriptor._internal_create_key)\n    scope['.%s' % enum_name] = desc\n    self._CheckConflictRegister(desc, desc.full_name, desc.file.name)\n    self._enum_descriptors[enum_name] = desc\n\n    # Add top level enum values.\n    if top_level:\n      for value in values:\n        full_name = _NormalizeFullyQualifiedName(\n            '.'.join((package, value.name)))\n        self._CheckConflictRegister(value, full_name, file_name)\n        self._top_enum_values[full_name] = value\n\n    return desc\n\n  def _MakeFieldDescriptor(self, field_proto, message_name, index,\n                           file_desc, is_extension=False):\n    \"\"\"Creates a field descriptor from a FieldDescriptorProto.\n\n    For message and enum type fields, this method will do a look up\n    in the pool for the appropriate descriptor for that type. If it\n    is unavailable, it will fall back to the _source function to\n    create it. If this type is still unavailable, construction will\n    fail.\n\n    Args:\n      field_proto: The proto describing the field.\n      message_name: The name of the containing message.\n      index: Index of the field\n      file_desc: The file containing the field descriptor.\n      is_extension: Indication that this field is for an extension.\n\n    Returns:\n      An initialized FieldDescriptor object\n    \"\"\"\n\n    if message_name:\n      full_name = '.'.join((message_name, field_proto.name))\n    else:\n      full_name = field_proto.name\n\n    if field_proto.json_name:\n      json_name = field_proto.json_name\n    else:\n      json_name = None\n\n    return descriptor.FieldDescriptor(\n        name=field_proto.name,\n        full_name=full_name,\n        index=index,\n        number=field_proto.number,\n        type=field_proto.type,\n        cpp_type=None,\n        message_type=None,\n        enum_type=None,\n        containing_type=None,\n        label=field_proto.label,\n        has_default_value=False,\n        default_value=None,\n        is_extension=is_extension,\n        extension_scope=None,\n        options=_OptionsOrNone(field_proto),\n        json_name=json_name,\n        file=file_desc,\n        # pylint: disable=protected-access\n        create_key=descriptor._internal_create_key)\n\n  def _SetAllFieldTypes(self, package, desc_proto, scope):\n    \"\"\"Sets all the descriptor's fields's types.\n\n    This method also sets the containing types on any extensions.\n\n    Args:\n      package: The current package of desc_proto.\n      desc_proto: The message descriptor to update.\n      scope: Enclosing scope of available types.\n    \"\"\"\n\n    package = _PrefixWithDot(package)\n\n    main_desc = self._GetTypeFromScope(package, desc_proto.name, scope)\n\n    if package == '.':\n      nested_package = _PrefixWithDot(desc_proto.name)\n    else:\n      nested_package = '.'.join([package, desc_proto.name])\n\n    for field_proto, field_desc in zip(desc_proto.field, main_desc.fields):\n      self._SetFieldType(field_proto, field_desc, nested_package, scope)\n\n    for extension_proto, extension_desc in (\n        zip(desc_proto.extension, main_desc.extensions)):\n      extension_desc.containing_type = self._GetTypeFromScope(\n          nested_package, extension_proto.extendee, scope)\n      self._SetFieldType(extension_proto, extension_desc, nested_package, scope)\n\n    for nested_type in desc_proto.nested_type:\n      self._SetAllFieldTypes(nested_package, nested_type, scope)\n\n  def _SetFieldType(self, field_proto, field_desc, package, scope):\n    \"\"\"Sets the field's type, cpp_type, message_type and enum_type.\n\n    Args:\n      field_proto: Data about the field in proto format.\n      field_desc: The descriptor to modify.\n      package: The package the field's container is in.\n      scope: Enclosing scope of available types.\n    \"\"\"\n    if field_proto.type_name:\n      desc = self._GetTypeFromScope(package, field_proto.type_name, scope)\n    else:\n      desc = None\n\n    if not field_proto.HasField('type'):\n      if isinstance(desc, descriptor.Descriptor):\n        field_proto.type = descriptor.FieldDescriptor.TYPE_MESSAGE\n      else:\n        field_proto.type = descriptor.FieldDescriptor.TYPE_ENUM\n\n    field_desc.cpp_type = descriptor.FieldDescriptor.ProtoTypeToCppProtoType(\n        field_proto.type)\n\n    if (field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE\n        or field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP):\n      field_desc.message_type = desc\n\n    if field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM:\n      field_desc.enum_type = desc\n\n    if field_proto.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n      field_desc.has_default_value = False\n      field_desc.default_value = []\n    elif field_proto.HasField('default_value'):\n      field_desc.has_default_value = True\n      if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or\n          field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT):\n        field_desc.default_value = float(field_proto.default_value)\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING:\n        field_desc.default_value = field_proto.default_value\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL:\n        field_desc.default_value = field_proto.default_value.lower() == 'true'\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM:\n        field_desc.default_value = field_desc.enum_type.values_by_name[\n            field_proto.default_value].number\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES:\n        field_desc.default_value = text_encoding.CUnescape(\n            field_proto.default_value)\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE:\n        field_desc.default_value = None\n      else:\n        # All other types are of the \"int\" type.\n        field_desc.default_value = int(field_proto.default_value)\n    else:\n      field_desc.has_default_value = False\n      if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or\n          field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT):\n        field_desc.default_value = 0.0\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING:\n        field_desc.default_value = u''\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL:\n        field_desc.default_value = False\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM:\n        field_desc.default_value = field_desc.enum_type.values[0].number\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES:\n        field_desc.default_value = b''\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE:\n        field_desc.default_value = None\n      elif field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP:\n        field_desc.default_value = None\n      else:\n        # All other types are of the \"int\" type.\n        field_desc.default_value = 0\n\n    field_desc.type = field_proto.type\n\n  def _MakeEnumValueDescriptor(self, value_proto, index):\n    \"\"\"Creates a enum value descriptor object from a enum value proto.\n\n    Args:\n      value_proto: The proto describing the enum value.\n      index: The index of the enum value.\n\n    Returns:\n      An initialized EnumValueDescriptor object.\n    \"\"\"\n\n    return descriptor.EnumValueDescriptor(\n        name=value_proto.name,\n        index=index,\n        number=value_proto.number,\n        options=_OptionsOrNone(value_proto),\n        type=None,\n        # pylint: disable=protected-access\n        create_key=descriptor._internal_create_key)\n\n  def _MakeServiceDescriptor(self, service_proto, service_index, scope,\n                             package, file_desc):\n    \"\"\"Make a protobuf ServiceDescriptor given a ServiceDescriptorProto.\n\n    Args:\n      service_proto: The descriptor_pb2.ServiceDescriptorProto protobuf message.\n      service_index: The index of the service in the File.\n      scope: Dict mapping short and full symbols to message and enum types.\n      package: Optional package name for the new message EnumDescriptor.\n      file_desc: The file containing the service descriptor.\n\n    Returns:\n      The added descriptor.\n    \"\"\"\n\n    if package:\n      service_name = '.'.join((package, service_proto.name))\n    else:\n      service_name = service_proto.name\n\n    methods = [self._MakeMethodDescriptor(method_proto, service_name, package,\n                                          scope, index)\n               for index, method_proto in enumerate(service_proto.method)]\n    desc = descriptor.ServiceDescriptor(\n        name=service_proto.name,\n        full_name=service_name,\n        index=service_index,\n        methods=methods,\n        options=_OptionsOrNone(service_proto),\n        file=file_desc,\n        # pylint: disable=protected-access\n        create_key=descriptor._internal_create_key)\n    self._CheckConflictRegister(desc, desc.full_name, desc.file.name)\n    self._service_descriptors[service_name] = desc\n    return desc\n\n  def _MakeMethodDescriptor(self, method_proto, service_name, package, scope,\n                            index):\n    \"\"\"Creates a method descriptor from a MethodDescriptorProto.\n\n    Args:\n      method_proto: The proto describing the method.\n      service_name: The name of the containing service.\n      package: Optional package name to look up for types.\n      scope: Scope containing available types.\n      index: Index of the method in the service.\n\n    Returns:\n      An initialized MethodDescriptor object.\n    \"\"\"\n    full_name = '.'.join((service_name, method_proto.name))\n    input_type = self._GetTypeFromScope(\n        package, method_proto.input_type, scope)\n    output_type = self._GetTypeFromScope(\n        package, method_proto.output_type, scope)\n    return descriptor.MethodDescriptor(\n        name=method_proto.name,\n        full_name=full_name,\n        index=index,\n        containing_service=None,\n        input_type=input_type,\n        output_type=output_type,\n        client_streaming=method_proto.client_streaming,\n        server_streaming=method_proto.server_streaming,\n        options=_OptionsOrNone(method_proto),\n        # pylint: disable=protected-access\n        create_key=descriptor._internal_create_key)\n\n  def _ExtractSymbols(self, descriptors):\n    \"\"\"Pulls out all the symbols from descriptor protos.\n\n    Args:\n      descriptors: The messages to extract descriptors from.\n    Yields:\n      A two element tuple of the type name and descriptor object.\n    \"\"\"\n\n    for desc in descriptors:\n      yield (_PrefixWithDot(desc.full_name), desc)\n      for symbol in self._ExtractSymbols(desc.nested_types):\n        yield symbol\n      for enum in desc.enum_types:\n        yield (_PrefixWithDot(enum.full_name), enum)\n\n  def _GetDeps(self, dependencies, visited=None):\n    \"\"\"Recursively finds dependencies for file protos.\n\n    Args:\n      dependencies: The names of the files being depended on.\n      visited: The names of files already found.\n\n    Yields:\n      Each direct and indirect dependency.\n    \"\"\"\n\n    visited = visited or set()\n    for dependency in dependencies:\n      if dependency not in visited:\n        visited.add(dependency)\n        dep_desc = self.FindFileByName(dependency)\n        yield dep_desc\n        public_files = [d.name for d in dep_desc.public_dependencies]\n        yield from self._GetDeps(public_files, visited)\n\n  def _GetTypeFromScope(self, package, type_name, scope):\n    \"\"\"Finds a given type name in the current scope.\n\n    Args:\n      package: The package the proto should be located in.\n      type_name: The name of the type to be found in the scope.\n      scope: Dict mapping short and full symbols to message and enum types.\n\n    Returns:\n      The descriptor for the requested type.\n    \"\"\"\n    if type_name not in scope:\n      components = _PrefixWithDot(package).split('.')\n      while components:\n        possible_match = '.'.join(components + [type_name])\n        if possible_match in scope:\n          type_name = possible_match\n          break\n        else:\n          components.pop(-1)\n    return scope[type_name]\n\n\ndef _PrefixWithDot(name):\n  return name if name.startswith('.') else '.%s' % name\n\n\nif _USE_C_DESCRIPTORS:\n  # TODO(amauryfa): This pool could be constructed from Python code, when we\n  # support a flag like 'use_cpp_generated_pool=True'.\n  # pylint: disable=protected-access\n  _DEFAULT = descriptor._message.default_pool\nelse:\n  _DEFAULT = DescriptorPool()\n\n\ndef Default():\n  return _DEFAULT\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/duration_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/duration.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x1egoogle/protobuf/duration.proto\\x12\\x0fgoogle.protobuf\\\"*\\n\\x08\\x44uration\\x12\\x0f\\n\\x07seconds\\x18\\x01 \\x01(\\x03\\x12\\r\\n\\x05nanos\\x18\\x02 \\x01(\\x05\\x42\\x83\\x01\\n\\x13\\x63om.google.protobufB\\rDurationProtoP\\x01Z1google.golang.org/protobuf/types/known/durationpb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.duration_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\rDurationProtoP\\001Z1google.golang.org/protobuf/types/known/durationpb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _DURATION._serialized_start=51\n  _DURATION._serialized_end=93\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/empty_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/empty.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x1bgoogle/protobuf/empty.proto\\x12\\x0fgoogle.protobuf\\\"\\x07\\n\\x05\\x45mptyB}\\n\\x13\\x63om.google.protobufB\\nEmptyProtoP\\x01Z.google.golang.org/protobuf/types/known/emptypb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.empty_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\nEmptyProtoP\\001Z.google.golang.org/protobuf/types/known/emptypb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _EMPTY._serialized_start=48\n  _EMPTY._serialized_end=55\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/field_mask_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/field_mask.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n google/protobuf/field_mask.proto\\x12\\x0fgoogle.protobuf\\\"\\x1a\\n\\tFieldMask\\x12\\r\\n\\x05paths\\x18\\x01 \\x03(\\tB\\x85\\x01\\n\\x13\\x63om.google.protobufB\\x0e\\x46ieldMaskProtoP\\x01Z2google.golang.org/protobuf/types/known/fieldmaskpb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.field_mask_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\016FieldMaskProtoP\\001Z2google.golang.org/protobuf/types/known/fieldmaskpb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _FIELDMASK._serialized_start=53\n  _FIELDMASK._serialized_end=79\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/_parameterized.py",
    "content": "#! /usr/bin/env python\n#\n# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Adds support for parameterized tests to Python's unittest TestCase class.\n\nA parameterized test is a method in a test case that is invoked with different\nargument tuples.\n\nA simple example:\n\n  class AdditionExample(parameterized.TestCase):\n    @parameterized.parameters(\n       (1, 2, 3),\n       (4, 5, 9),\n       (1, 1, 3))\n    def testAddition(self, op1, op2, result):\n      self.assertEqual(result, op1 + op2)\n\n\nEach invocation is a separate test case and properly isolated just\nlike a normal test method, with its own setUp/tearDown cycle. In the\nexample above, there are three separate testcases, one of which will\nfail due to an assertion error (1 + 1 != 3).\n\nParameters for individual test cases can be tuples (with positional parameters)\nor dictionaries (with named parameters):\n\n  class AdditionExample(parameterized.TestCase):\n    @parameterized.parameters(\n       {'op1': 1, 'op2': 2, 'result': 3},\n       {'op1': 4, 'op2': 5, 'result': 9},\n    )\n    def testAddition(self, op1, op2, result):\n      self.assertEqual(result, op1 + op2)\n\nIf a parameterized test fails, the error message will show the\noriginal test name (which is modified internally) and the arguments\nfor the specific invocation, which are part of the string returned by\nthe shortDescription() method on test cases.\n\nThe id method of the test, used internally by the unittest framework,\nis also modified to show the arguments. To make sure that test names\nstay the same across several invocations, object representations like\n\n  >>> class Foo(object):\n  ...  pass\n  >>> repr(Foo())\n  '<__main__.Foo object at 0x23d8610>'\n\nare turned into '<__main__.Foo>'. For even more descriptive names,\nespecially in test logs, you can use the named_parameters decorator. In\nthis case, only tuples are supported, and the first parameters has to\nbe a string (or an object that returns an apt name when converted via\nstr()):\n\n  class NamedExample(parameterized.TestCase):\n    @parameterized.named_parameters(\n       ('Normal', 'aa', 'aaa', True),\n       ('EmptyPrefix', '', 'abc', True),\n       ('BothEmpty', '', '', True))\n    def testStartsWith(self, prefix, string, result):\n      self.assertEqual(result, strings.startswith(prefix))\n\nNamed tests also have the benefit that they can be run individually\nfrom the command line:\n\n  $ testmodule.py NamedExample.testStartsWithNormal\n  .\n  --------------------------------------------------------------------\n  Ran 1 test in 0.000s\n\n  OK\n\nParameterized Classes\n=====================\nIf invocation arguments are shared across test methods in a single\nTestCase class, instead of decorating all test methods\nindividually, the class itself can be decorated:\n\n  @parameterized.parameters(\n    (1, 2, 3)\n    (4, 5, 9))\n  class ArithmeticTest(parameterized.TestCase):\n    def testAdd(self, arg1, arg2, result):\n      self.assertEqual(arg1 + arg2, result)\n\n    def testSubtract(self, arg2, arg2, result):\n      self.assertEqual(result - arg1, arg2)\n\nInputs from Iterables\n=====================\nIf parameters should be shared across several test cases, or are dynamically\ncreated from other sources, a single non-tuple iterable can be passed into\nthe decorator. This iterable will be used to obtain the test cases:\n\n  class AdditionExample(parameterized.TestCase):\n    @parameterized.parameters(\n      c.op1, c.op2, c.result for c in testcases\n    )\n    def testAddition(self, op1, op2, result):\n      self.assertEqual(result, op1 + op2)\n\n\nSingle-Argument Test Methods\n============================\nIf a test method takes only one argument, the single argument does not need to\nbe wrapped into a tuple:\n\n  class NegativeNumberExample(parameterized.TestCase):\n    @parameterized.parameters(\n       -1, -3, -4, -5\n    )\n    def testIsNegative(self, arg):\n      self.assertTrue(IsNegative(arg))\n\"\"\"\n\n__author__ = 'tmarek@google.com (Torsten Marek)'\n\nimport functools\nimport re\nimport types\nimport unittest\nimport uuid\n\ntry:\n  # Since python 3\n  import collections.abc as collections_abc\nexcept ImportError:\n  # Won't work after python 3.8\n  import collections as collections_abc\n\nADDR_RE = re.compile(r'\\<([a-zA-Z0-9_\\-\\.]+) object at 0x[a-fA-F0-9]+\\>')\n_SEPARATOR = uuid.uuid1().hex\n_FIRST_ARG = object()\n_ARGUMENT_REPR = object()\n\n\ndef _CleanRepr(obj):\n  return ADDR_RE.sub(r'<\\1>', repr(obj))\n\n\n# Helper function formerly from the unittest module, removed from it in\n# Python 2.7.\ndef _StrClass(cls):\n  return '%s.%s' % (cls.__module__, cls.__name__)\n\n\ndef _NonStringIterable(obj):\n  return (isinstance(obj, collections_abc.Iterable) and\n          not isinstance(obj, str))\n\n\ndef _FormatParameterList(testcase_params):\n  if isinstance(testcase_params, collections_abc.Mapping):\n    return ', '.join('%s=%s' % (argname, _CleanRepr(value))\n                     for argname, value in testcase_params.items())\n  elif _NonStringIterable(testcase_params):\n    return ', '.join(map(_CleanRepr, testcase_params))\n  else:\n    return _FormatParameterList((testcase_params,))\n\n\nclass _ParameterizedTestIter(object):\n  \"\"\"Callable and iterable class for producing new test cases.\"\"\"\n\n  def __init__(self, test_method, testcases, naming_type):\n    \"\"\"Returns concrete test functions for a test and a list of parameters.\n\n    The naming_type is used to determine the name of the concrete\n    functions as reported by the unittest framework. If naming_type is\n    _FIRST_ARG, the testcases must be tuples, and the first element must\n    have a string representation that is a valid Python identifier.\n\n    Args:\n      test_method: The decorated test method.\n      testcases: (list of tuple/dict) A list of parameter\n                 tuples/dicts for individual test invocations.\n      naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR.\n    \"\"\"\n    self._test_method = test_method\n    self.testcases = testcases\n    self._naming_type = naming_type\n\n  def __call__(self, *args, **kwargs):\n    raise RuntimeError('You appear to be running a parameterized test case '\n                       'without having inherited from parameterized.'\n                       'TestCase. This is bad because none of '\n                       'your test cases are actually being run.')\n\n  def __iter__(self):\n    test_method = self._test_method\n    naming_type = self._naming_type\n\n    def MakeBoundParamTest(testcase_params):\n      @functools.wraps(test_method)\n      def BoundParamTest(self):\n        if isinstance(testcase_params, collections_abc.Mapping):\n          test_method(self, **testcase_params)\n        elif _NonStringIterable(testcase_params):\n          test_method(self, *testcase_params)\n        else:\n          test_method(self, testcase_params)\n\n      if naming_type is _FIRST_ARG:\n        # Signal the metaclass that the name of the test function is unique\n        # and descriptive.\n        BoundParamTest.__x_use_name__ = True\n        BoundParamTest.__name__ += str(testcase_params[0])\n        testcase_params = testcase_params[1:]\n      elif naming_type is _ARGUMENT_REPR:\n        # __x_extra_id__ is used to pass naming information to the __new__\n        # method of TestGeneratorMetaclass.\n        # The metaclass will make sure to create a unique, but nondescriptive\n        # name for this test.\n        BoundParamTest.__x_extra_id__ = '(%s)' % (\n            _FormatParameterList(testcase_params),)\n      else:\n        raise RuntimeError('%s is not a valid naming type.' % (naming_type,))\n\n      BoundParamTest.__doc__ = '%s(%s)' % (\n          BoundParamTest.__name__, _FormatParameterList(testcase_params))\n      if test_method.__doc__:\n        BoundParamTest.__doc__ += '\\n%s' % (test_method.__doc__,)\n      return BoundParamTest\n    return (MakeBoundParamTest(c) for c in self.testcases)\n\n\ndef _IsSingletonList(testcases):\n  \"\"\"True iff testcases contains only a single non-tuple element.\"\"\"\n  return len(testcases) == 1 and not isinstance(testcases[0], tuple)\n\n\ndef _ModifyClass(class_object, testcases, naming_type):\n  assert not getattr(class_object, '_id_suffix', None), (\n      'Cannot add parameters to %s,'\n      ' which already has parameterized methods.' % (class_object,))\n  class_object._id_suffix = id_suffix = {}\n  # We change the size of __dict__ while we iterate over it,\n  # which Python 3.x will complain about, so use copy().\n  for name, obj in class_object.__dict__.copy().items():\n    if (name.startswith(unittest.TestLoader.testMethodPrefix)\n        and isinstance(obj, types.FunctionType)):\n      delattr(class_object, name)\n      methods = {}\n      _UpdateClassDictForParamTestCase(\n          methods, id_suffix, name,\n          _ParameterizedTestIter(obj, testcases, naming_type))\n      for name, meth in methods.items():\n        setattr(class_object, name, meth)\n\n\ndef _ParameterDecorator(naming_type, testcases):\n  \"\"\"Implementation of the parameterization decorators.\n\n  Args:\n    naming_type: The naming type.\n    testcases: Testcase parameters.\n\n  Returns:\n    A function for modifying the decorated object.\n  \"\"\"\n  def _Apply(obj):\n    if isinstance(obj, type):\n      _ModifyClass(\n          obj,\n          list(testcases) if not isinstance(testcases, collections_abc.Sequence)\n          else testcases,\n          naming_type)\n      return obj\n    else:\n      return _ParameterizedTestIter(obj, testcases, naming_type)\n\n  if _IsSingletonList(testcases):\n    assert _NonStringIterable(testcases[0]), (\n        'Single parameter argument must be a non-string iterable')\n    testcases = testcases[0]\n\n  return _Apply\n\n\ndef parameters(*testcases):  # pylint: disable=invalid-name\n  \"\"\"A decorator for creating parameterized tests.\n\n  See the module docstring for a usage example.\n  Args:\n    *testcases: Parameters for the decorated method, either a single\n                iterable, or a list of tuples/dicts/objects (for tests\n                with only one argument).\n\n  Returns:\n     A test generator to be handled by TestGeneratorMetaclass.\n  \"\"\"\n  return _ParameterDecorator(_ARGUMENT_REPR, testcases)\n\n\ndef named_parameters(*testcases):  # pylint: disable=invalid-name\n  \"\"\"A decorator for creating parameterized tests.\n\n  See the module docstring for a usage example. The first element of\n  each parameter tuple should be a string and will be appended to the\n  name of the test method.\n\n  Args:\n    *testcases: Parameters for the decorated method, either a single\n                iterable, or a list of tuples.\n\n  Returns:\n     A test generator to be handled by TestGeneratorMetaclass.\n  \"\"\"\n  return _ParameterDecorator(_FIRST_ARG, testcases)\n\n\nclass TestGeneratorMetaclass(type):\n  \"\"\"Metaclass for test cases with test generators.\n\n  A test generator is an iterable in a testcase that produces callables. These\n  callables must be single-argument methods. These methods are injected into\n  the class namespace and the original iterable is removed. If the name of the\n  iterable conforms to the test pattern, the injected methods will be picked\n  up as tests by the unittest framework.\n\n  In general, it is supposed to be used in conjunction with the\n  parameters decorator.\n  \"\"\"\n\n  def __new__(mcs, class_name, bases, dct):\n    dct['_id_suffix'] = id_suffix = {}\n    for name, obj in dct.copy().items():\n      if (name.startswith(unittest.TestLoader.testMethodPrefix) and\n          _NonStringIterable(obj)):\n        iterator = iter(obj)\n        dct.pop(name)\n        _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator)\n\n    return type.__new__(mcs, class_name, bases, dct)\n\n\ndef _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator):\n  \"\"\"Adds individual test cases to a dictionary.\n\n  Args:\n    dct: The target dictionary.\n    id_suffix: The dictionary for mapping names to test IDs.\n    name: The original name of the test case.\n    iterator: The iterator generating the individual test cases.\n  \"\"\"\n  for idx, func in enumerate(iterator):\n    assert callable(func), 'Test generators must yield callables, got %r' % (\n        func,)\n    if getattr(func, '__x_use_name__', False):\n      new_name = func.__name__\n    else:\n      new_name = '%s%s%d' % (name, _SEPARATOR, idx)\n    assert new_name not in dct, (\n        'Name of parameterized test case \"%s\" not unique' % (new_name,))\n    dct[new_name] = func\n    id_suffix[new_name] = getattr(func, '__x_extra_id__', '')\n\n\nclass TestCase(unittest.TestCase, metaclass=TestGeneratorMetaclass):\n  \"\"\"Base class for test cases using the parameters decorator.\"\"\"\n\n  def _OriginalName(self):\n    return self._testMethodName.split(_SEPARATOR)[0]\n\n  def __str__(self):\n    return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__))\n\n  def id(self):  # pylint: disable=invalid-name\n    \"\"\"Returns the descriptive ID of the test.\n\n    This is used internally by the unittesting framework to get a name\n    for the test to be used in reports.\n\n    Returns:\n      The test id.\n    \"\"\"\n    return '%s.%s%s' % (_StrClass(self.__class__),\n                        self._OriginalName(),\n                        self._id_suffix.get(self._testMethodName, ''))\n\n\ndef CoopTestCase(other_base_class):\n  \"\"\"Returns a new base class with a cooperative metaclass base.\n\n  This enables the TestCase to be used in combination\n  with other base classes that have custom metaclasses, such as\n  mox.MoxTestBase.\n\n  Only works with metaclasses that do not override type.__new__.\n\n  Example:\n\n    import google3\n    import mox\n\n    from google3.testing.pybase import parameterized\n\n    class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)):\n      ...\n\n  Args:\n    other_base_class: (class) A test case base class.\n\n  Returns:\n    A new class object.\n  \"\"\"\n  metaclass = type(\n      'CoopMetaclass',\n      (other_base_class.__metaclass__,\n       TestGeneratorMetaclass), {})\n  return metaclass(\n      'CoopTestCase',\n      (other_base_class, TestCase), {})\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/api_implementation.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Determine which implementation of the protobuf API is used in this process.\n\"\"\"\n\nimport os\nimport sys\nimport warnings\n\ntry:\n  # pylint: disable=g-import-not-at-top\n  from google.protobuf.internal import _api_implementation\n  # The compile-time constants in the _api_implementation module can be used to\n  # switch to a certain implementation of the Python API at build time.\n  _api_version = _api_implementation.api_version\nexcept ImportError:\n  _api_version = -1  # Unspecified by compiler flags.\n\nif _api_version == 1:\n  raise ValueError('api_version=1 is no longer supported.')\n\n\n_default_implementation_type = ('cpp' if _api_version > 0 else 'python')\n\n\n# This environment variable can be used to switch to a certain implementation\n# of the Python API, overriding the compile-time constants in the\n# _api_implementation module. Right now only 'python' and 'cpp' are valid\n# values. Any other value will be ignored.\n_implementation_type = os.getenv('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION',\n                                 _default_implementation_type)\n\nif _implementation_type != 'python':\n  _implementation_type = 'cpp'\n\nif 'PyPy' in sys.version and _implementation_type == 'cpp':\n  warnings.warn('PyPy does not work yet with cpp protocol buffers. '\n                'Falling back to the python implementation.')\n  _implementation_type = 'python'\n\n\n# Detect if serialization should be deterministic by default\ntry:\n  # The presence of this module in a build allows the proto implementation to\n  # be upgraded merely via build deps.\n  #\n  # NOTE: Merely importing this automatically enables deterministic proto\n  # serialization for C++ code, but we still need to export it as a boolean so\n  # that we can do the same for `_implementation_type == 'python'`.\n  #\n  # NOTE2: It is possible for C++ code to enable deterministic serialization by\n  # default _without_ affecting Python code, if the C++ implementation is not in\n  # use by this module.  That is intended behavior, so we don't actually expose\n  # this boolean outside of this module.\n  #\n  # pylint: disable=g-import-not-at-top,unused-import\n  from google.protobuf import enable_deterministic_proto_serialization\n  _python_deterministic_proto_serialization = True\nexcept ImportError:\n  _python_deterministic_proto_serialization = False\n\n\n# Usage of this function is discouraged. Clients shouldn't care which\n# implementation of the API is in use. Note that there is no guarantee\n# that differences between APIs will be maintained.\n# Please don't use this function if possible.\ndef Type():\n  return _implementation_type\n\n\ndef _SetType(implementation_type):\n  \"\"\"Never use! Only for protobuf benchmark.\"\"\"\n  global _implementation_type\n  _implementation_type = implementation_type\n\n\n# See comment on 'Type' above.\ndef Version():\n  return 2\n\n\n# For internal use only\ndef IsPythonDefaultSerializationDeterministic():\n  return _python_deterministic_proto_serialization\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/builder.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Builds descriptors, message classes and services for generated _pb2.py.\n\nThis file is only called in python generated _pb2.py files. It builds\ndescriptors, message classes and services that users can directly use\nin generated code.\n\"\"\"\n\n__author__ = 'jieluo@google.com (Jie Luo)'\n\nfrom google.protobuf.internal import enum_type_wrapper\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\n\n_sym_db = _symbol_database.Default()\n\n\ndef BuildMessageAndEnumDescriptors(file_des, module):\n  \"\"\"Builds message and enum descriptors.\n\n  Args:\n    file_des: FileDescriptor of the .proto file\n    module: Generated _pb2 module\n  \"\"\"\n\n  def BuildNestedDescriptors(msg_des, prefix):\n    for (name, nested_msg) in msg_des.nested_types_by_name.items():\n      module_name = prefix + name.upper()\n      module[module_name] = nested_msg\n      BuildNestedDescriptors(nested_msg, module_name + '_')\n    for enum_des in msg_des.enum_types:\n      module[prefix + enum_des.name.upper()] = enum_des\n\n  for (name, msg_des) in file_des.message_types_by_name.items():\n    module_name = '_' + name.upper()\n    module[module_name] = msg_des\n    BuildNestedDescriptors(msg_des, module_name + '_')\n\n\ndef BuildTopDescriptorsAndMessages(file_des, module_name, module):\n  \"\"\"Builds top level descriptors and message classes.\n\n  Args:\n    file_des: FileDescriptor of the .proto file\n    module_name: str, the name of generated _pb2 module\n    module: Generated _pb2 module\n  \"\"\"\n\n  def BuildMessage(msg_des):\n    create_dict = {}\n    for (name, nested_msg) in msg_des.nested_types_by_name.items():\n      create_dict[name] = BuildMessage(nested_msg)\n    create_dict['DESCRIPTOR'] = msg_des\n    create_dict['__module__'] = module_name\n    message_class = _reflection.GeneratedProtocolMessageType(\n        msg_des.name, (_message.Message,), create_dict)\n    _sym_db.RegisterMessage(message_class)\n    return message_class\n\n  # top level enums\n  for (name, enum_des) in file_des.enum_types_by_name.items():\n    module['_' + name.upper()] = enum_des\n    module[name] = enum_type_wrapper.EnumTypeWrapper(enum_des)\n    for enum_value in enum_des.values:\n      module[enum_value.name] = enum_value.number\n\n  # top level extensions\n  for (name, extension_des) in file_des.extensions_by_name.items():\n    module[name.upper() + '_FIELD_NUMBER'] = extension_des.number\n    module[name] = extension_des\n\n  # services\n  for (name, service) in file_des.services_by_name.items():\n    module['_' + name.upper()] = service\n\n  # Build messages.\n  for (name, msg_des) in file_des.message_types_by_name.items():\n    module[name] = BuildMessage(msg_des)\n\n\ndef BuildServices(file_des, module_name, module):\n  \"\"\"Builds services classes and services stub class.\n\n  Args:\n    file_des: FileDescriptor of the .proto file\n    module_name: str, the name of generated _pb2 module\n    module: Generated _pb2 module\n  \"\"\"\n  # pylint: disable=g-import-not-at-top\n  from google.protobuf import service as _service\n  from google.protobuf import service_reflection\n  # pylint: enable=g-import-not-at-top\n  for (name, service) in file_des.services_by_name.items():\n    module[name] = service_reflection.GeneratedServiceType(\n        name, (_service.Service,),\n        dict(DESCRIPTOR=service, __module__=module_name))\n    stub_name = name + '_Stub'\n    module[stub_name] = service_reflection.GeneratedServiceStubType(\n        stub_name, (module[name],),\n        dict(DESCRIPTOR=service, __module__=module_name))\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/containers.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Contains container classes to represent different protocol buffer types.\n\nThis file defines container classes which represent categories of protocol\nbuffer field types which need extra maintenance. Currently these categories\nare:\n\n-   Repeated scalar fields - These are all repeated fields which aren't\n    composite (e.g. they are of simple types like int32, string, etc).\n-   Repeated composite fields - Repeated fields which are composite. This\n    includes groups and nested messages.\n\"\"\"\n\nimport collections.abc\nimport copy\nimport pickle\nfrom typing import (\n    Any,\n    Iterable,\n    Iterator,\n    List,\n    MutableMapping,\n    MutableSequence,\n    NoReturn,\n    Optional,\n    Sequence,\n    TypeVar,\n    Union,\n    overload,\n)\n\n\n_T = TypeVar('_T')\n_K = TypeVar('_K')\n_V = TypeVar('_V')\n\n\nclass BaseContainer(Sequence[_T]):\n  \"\"\"Base container class.\"\"\"\n\n  # Minimizes memory usage and disallows assignment to other attributes.\n  __slots__ = ['_message_listener', '_values']\n\n  def __init__(self, message_listener: Any) -> None:\n    \"\"\"\n    Args:\n      message_listener: A MessageListener implementation.\n        The RepeatedScalarFieldContainer will call this object's\n        Modified() method when it is modified.\n    \"\"\"\n    self._message_listener = message_listener\n    self._values = []\n\n  @overload\n  def __getitem__(self, key: int) -> _T:\n    ...\n\n  @overload\n  def __getitem__(self, key: slice) -> List[_T]:\n    ...\n\n  def __getitem__(self, key):\n    \"\"\"Retrieves item by the specified key.\"\"\"\n    return self._values[key]\n\n  def __len__(self) -> int:\n    \"\"\"Returns the number of elements in the container.\"\"\"\n    return len(self._values)\n\n  def __ne__(self, other: Any) -> bool:\n    \"\"\"Checks if another instance isn't equal to this one.\"\"\"\n    # The concrete classes should define __eq__.\n    return not self == other\n\n  __hash__ = None\n\n  def __repr__(self) -> str:\n    return repr(self._values)\n\n  def sort(self, *args, **kwargs) -> None:\n    # Continue to support the old sort_function keyword argument.\n    # This is expected to be a rare occurrence, so use LBYL to avoid\n    # the overhead of actually catching KeyError.\n    if 'sort_function' in kwargs:\n      kwargs['cmp'] = kwargs.pop('sort_function')\n    self._values.sort(*args, **kwargs)\n\n  def reverse(self) -> None:\n    self._values.reverse()\n\n\n# TODO(slebedev): Remove this. BaseContainer does *not* conform to\n# MutableSequence, only its subclasses do.\ncollections.abc.MutableSequence.register(BaseContainer)\n\n\nclass RepeatedScalarFieldContainer(BaseContainer[_T], MutableSequence[_T]):\n  \"\"\"Simple, type-checked, list-like container for holding repeated scalars.\"\"\"\n\n  # Disallows assignment to other attributes.\n  __slots__ = ['_type_checker']\n\n  def __init__(\n      self,\n      message_listener: Any,\n      type_checker: Any,\n  ) -> None:\n    \"\"\"Args:\n\n      message_listener: A MessageListener implementation. The\n      RepeatedScalarFieldContainer will call this object's Modified() method\n      when it is modified.\n      type_checker: A type_checkers.ValueChecker instance to run on elements\n      inserted into this container.\n    \"\"\"\n    super().__init__(message_listener)\n    self._type_checker = type_checker\n\n  def append(self, value: _T) -> None:\n    \"\"\"Appends an item to the list. Similar to list.append().\"\"\"\n    self._values.append(self._type_checker.CheckValue(value))\n    if not self._message_listener.dirty:\n      self._message_listener.Modified()\n\n  def insert(self, key: int, value: _T) -> None:\n    \"\"\"Inserts the item at the specified position. Similar to list.insert().\"\"\"\n    self._values.insert(key, self._type_checker.CheckValue(value))\n    if not self._message_listener.dirty:\n      self._message_listener.Modified()\n\n  def extend(self, elem_seq: Iterable[_T]) -> None:\n    \"\"\"Extends by appending the given iterable. Similar to list.extend().\"\"\"\n    if elem_seq is None:\n      return\n    try:\n      elem_seq_iter = iter(elem_seq)\n    except TypeError:\n      if not elem_seq:\n        # silently ignore falsy inputs :-/.\n        # TODO(ptucker): Deprecate this behavior. b/18413862\n        return\n      raise\n\n    new_values = [self._type_checker.CheckValue(elem) for elem in elem_seq_iter]\n    if new_values:\n      self._values.extend(new_values)\n    self._message_listener.Modified()\n\n  def MergeFrom(\n      self,\n      other: Union['RepeatedScalarFieldContainer[_T]', Iterable[_T]],\n  ) -> None:\n    \"\"\"Appends the contents of another repeated field of the same type to this\n    one. We do not check the types of the individual fields.\n    \"\"\"\n    self._values.extend(other)\n    self._message_listener.Modified()\n\n  def remove(self, elem: _T):\n    \"\"\"Removes an item from the list. Similar to list.remove().\"\"\"\n    self._values.remove(elem)\n    self._message_listener.Modified()\n\n  def pop(self, key: Optional[int] = -1) -> _T:\n    \"\"\"Removes and returns an item at a given index. Similar to list.pop().\"\"\"\n    value = self._values[key]\n    self.__delitem__(key)\n    return value\n\n  @overload\n  def __setitem__(self, key: int, value: _T) -> None:\n    ...\n\n  @overload\n  def __setitem__(self, key: slice, value: Iterable[_T]) -> None:\n    ...\n\n  def __setitem__(self, key, value) -> None:\n    \"\"\"Sets the item on the specified position.\"\"\"\n    if isinstance(key, slice):\n      if key.step is not None:\n        raise ValueError('Extended slices not supported')\n      self._values[key] = map(self._type_checker.CheckValue, value)\n      self._message_listener.Modified()\n    else:\n      self._values[key] = self._type_checker.CheckValue(value)\n      self._message_listener.Modified()\n\n  def __delitem__(self, key: Union[int, slice]) -> None:\n    \"\"\"Deletes the item at the specified position.\"\"\"\n    del self._values[key]\n    self._message_listener.Modified()\n\n  def __eq__(self, other: Any) -> bool:\n    \"\"\"Compares the current instance with another one.\"\"\"\n    if self is other:\n      return True\n    # Special case for the same type which should be common and fast.\n    if isinstance(other, self.__class__):\n      return other._values == self._values\n    # We are presumably comparing against some other sequence type.\n    return other == self._values\n\n  def __deepcopy__(\n      self,\n      unused_memo: Any = None,\n  ) -> 'RepeatedScalarFieldContainer[_T]':\n    clone = RepeatedScalarFieldContainer(\n        copy.deepcopy(self._message_listener), self._type_checker)\n    clone.MergeFrom(self)\n    return clone\n\n  def __reduce__(self, **kwargs) -> NoReturn:\n    raise pickle.PickleError(\n        \"Can't pickle repeated scalar fields, convert to list first\")\n\n\n# TODO(slebedev): Constrain T to be a subtype of Message.\nclass RepeatedCompositeFieldContainer(BaseContainer[_T], MutableSequence[_T]):\n  \"\"\"Simple, list-like container for holding repeated composite fields.\"\"\"\n\n  # Disallows assignment to other attributes.\n  __slots__ = ['_message_descriptor']\n\n  def __init__(self, message_listener: Any, message_descriptor: Any) -> None:\n    \"\"\"\n    Note that we pass in a descriptor instead of the generated directly,\n    since at the time we construct a _RepeatedCompositeFieldContainer we\n    haven't yet necessarily initialized the type that will be contained in the\n    container.\n\n    Args:\n      message_listener: A MessageListener implementation.\n        The RepeatedCompositeFieldContainer will call this object's\n        Modified() method when it is modified.\n      message_descriptor: A Descriptor instance describing the protocol type\n        that should be present in this container.  We'll use the\n        _concrete_class field of this descriptor when the client calls add().\n    \"\"\"\n    super().__init__(message_listener)\n    self._message_descriptor = message_descriptor\n\n  def add(self, **kwargs: Any) -> _T:\n    \"\"\"Adds a new element at the end of the list and returns it. Keyword\n    arguments may be used to initialize the element.\n    \"\"\"\n    new_element = self._message_descriptor._concrete_class(**kwargs)\n    new_element._SetListener(self._message_listener)\n    self._values.append(new_element)\n    if not self._message_listener.dirty:\n      self._message_listener.Modified()\n    return new_element\n\n  def append(self, value: _T) -> None:\n    \"\"\"Appends one element by copying the message.\"\"\"\n    new_element = self._message_descriptor._concrete_class()\n    new_element._SetListener(self._message_listener)\n    new_element.CopyFrom(value)\n    self._values.append(new_element)\n    if not self._message_listener.dirty:\n      self._message_listener.Modified()\n\n  def insert(self, key: int, value: _T) -> None:\n    \"\"\"Inserts the item at the specified position by copying.\"\"\"\n    new_element = self._message_descriptor._concrete_class()\n    new_element._SetListener(self._message_listener)\n    new_element.CopyFrom(value)\n    self._values.insert(key, new_element)\n    if not self._message_listener.dirty:\n      self._message_listener.Modified()\n\n  def extend(self, elem_seq: Iterable[_T]) -> None:\n    \"\"\"Extends by appending the given sequence of elements of the same type\n\n    as this one, copying each individual message.\n    \"\"\"\n    message_class = self._message_descriptor._concrete_class\n    listener = self._message_listener\n    values = self._values\n    for message in elem_seq:\n      new_element = message_class()\n      new_element._SetListener(listener)\n      new_element.MergeFrom(message)\n      values.append(new_element)\n    listener.Modified()\n\n  def MergeFrom(\n      self,\n      other: Union['RepeatedCompositeFieldContainer[_T]', Iterable[_T]],\n  ) -> None:\n    \"\"\"Appends the contents of another repeated field of the same type to this\n    one, copying each individual message.\n    \"\"\"\n    self.extend(other)\n\n  def remove(self, elem: _T) -> None:\n    \"\"\"Removes an item from the list. Similar to list.remove().\"\"\"\n    self._values.remove(elem)\n    self._message_listener.Modified()\n\n  def pop(self, key: Optional[int] = -1) -> _T:\n    \"\"\"Removes and returns an item at a given index. Similar to list.pop().\"\"\"\n    value = self._values[key]\n    self.__delitem__(key)\n    return value\n\n  @overload\n  def __setitem__(self, key: int, value: _T) -> None:\n    ...\n\n  @overload\n  def __setitem__(self, key: slice, value: Iterable[_T]) -> None:\n    ...\n\n  def __setitem__(self, key, value):\n    # This method is implemented to make RepeatedCompositeFieldContainer\n    # structurally compatible with typing.MutableSequence. It is\n    # otherwise unsupported and will always raise an error.\n    raise TypeError(\n        f'{self.__class__.__name__} object does not support item assignment')\n\n  def __delitem__(self, key: Union[int, slice]) -> None:\n    \"\"\"Deletes the item at the specified position.\"\"\"\n    del self._values[key]\n    self._message_listener.Modified()\n\n  def __eq__(self, other: Any) -> bool:\n    \"\"\"Compares the current instance with another one.\"\"\"\n    if self is other:\n      return True\n    if not isinstance(other, self.__class__):\n      raise TypeError('Can only compare repeated composite fields against '\n                      'other repeated composite fields.')\n    return self._values == other._values\n\n\nclass ScalarMap(MutableMapping[_K, _V]):\n  \"\"\"Simple, type-checked, dict-like container for holding repeated scalars.\"\"\"\n\n  # Disallows assignment to other attributes.\n  __slots__ = ['_key_checker', '_value_checker', '_values', '_message_listener',\n               '_entry_descriptor']\n\n  def __init__(\n      self,\n      message_listener: Any,\n      key_checker: Any,\n      value_checker: Any,\n      entry_descriptor: Any,\n  ) -> None:\n    \"\"\"\n    Args:\n      message_listener: A MessageListener implementation.\n        The ScalarMap will call this object's Modified() method when it\n        is modified.\n      key_checker: A type_checkers.ValueChecker instance to run on keys\n        inserted into this container.\n      value_checker: A type_checkers.ValueChecker instance to run on values\n        inserted into this container.\n      entry_descriptor: The MessageDescriptor of a map entry: key and value.\n    \"\"\"\n    self._message_listener = message_listener\n    self._key_checker = key_checker\n    self._value_checker = value_checker\n    self._entry_descriptor = entry_descriptor\n    self._values = {}\n\n  def __getitem__(self, key: _K) -> _V:\n    try:\n      return self._values[key]\n    except KeyError:\n      key = self._key_checker.CheckValue(key)\n      val = self._value_checker.DefaultValue()\n      self._values[key] = val\n      return val\n\n  def __contains__(self, item: _K) -> bool:\n    # We check the key's type to match the strong-typing flavor of the API.\n    # Also this makes it easier to match the behavior of the C++ implementation.\n    self._key_checker.CheckValue(item)\n    return item in self._values\n\n  @overload\n  def get(self, key: _K) -> Optional[_V]:\n    ...\n\n  @overload\n  def get(self, key: _K, default: _T) -> Union[_V, _T]:\n    ...\n\n  # We need to override this explicitly, because our defaultdict-like behavior\n  # will make the default implementation (from our base class) always insert\n  # the key.\n  def get(self, key, default=None):\n    if key in self:\n      return self[key]\n    else:\n      return default\n\n  def __setitem__(self, key: _K, value: _V) -> _T:\n    checked_key = self._key_checker.CheckValue(key)\n    checked_value = self._value_checker.CheckValue(value)\n    self._values[checked_key] = checked_value\n    self._message_listener.Modified()\n\n  def __delitem__(self, key: _K) -> None:\n    del self._values[key]\n    self._message_listener.Modified()\n\n  def __len__(self) -> int:\n    return len(self._values)\n\n  def __iter__(self) -> Iterator[_K]:\n    return iter(self._values)\n\n  def __repr__(self) -> str:\n    return repr(self._values)\n\n  def MergeFrom(self, other: 'ScalarMap[_K, _V]') -> None:\n    self._values.update(other._values)\n    self._message_listener.Modified()\n\n  def InvalidateIterators(self) -> None:\n    # It appears that the only way to reliably invalidate iterators to\n    # self._values is to ensure that its size changes.\n    original = self._values\n    self._values = original.copy()\n    original[None] = None\n\n  # This is defined in the abstract base, but we can do it much more cheaply.\n  def clear(self) -> None:\n    self._values.clear()\n    self._message_listener.Modified()\n\n  def GetEntryClass(self) -> Any:\n    return self._entry_descriptor._concrete_class\n\n\nclass MessageMap(MutableMapping[_K, _V]):\n  \"\"\"Simple, type-checked, dict-like container for with submessage values.\"\"\"\n\n  # Disallows assignment to other attributes.\n  __slots__ = ['_key_checker', '_values', '_message_listener',\n               '_message_descriptor', '_entry_descriptor']\n\n  def __init__(\n      self,\n      message_listener: Any,\n      message_descriptor: Any,\n      key_checker: Any,\n      entry_descriptor: Any,\n  ) -> None:\n    \"\"\"\n    Args:\n      message_listener: A MessageListener implementation.\n        The ScalarMap will call this object's Modified() method when it\n        is modified.\n      key_checker: A type_checkers.ValueChecker instance to run on keys\n        inserted into this container.\n      value_checker: A type_checkers.ValueChecker instance to run on values\n        inserted into this container.\n      entry_descriptor: The MessageDescriptor of a map entry: key and value.\n    \"\"\"\n    self._message_listener = message_listener\n    self._message_descriptor = message_descriptor\n    self._key_checker = key_checker\n    self._entry_descriptor = entry_descriptor\n    self._values = {}\n\n  def __getitem__(self, key: _K) -> _V:\n    key = self._key_checker.CheckValue(key)\n    try:\n      return self._values[key]\n    except KeyError:\n      new_element = self._message_descriptor._concrete_class()\n      new_element._SetListener(self._message_listener)\n      self._values[key] = new_element\n      self._message_listener.Modified()\n      return new_element\n\n  def get_or_create(self, key: _K) -> _V:\n    \"\"\"get_or_create() is an alias for getitem (ie. map[key]).\n\n    Args:\n      key: The key to get or create in the map.\n\n    This is useful in cases where you want to be explicit that the call is\n    mutating the map.  This can avoid lint errors for statements like this\n    that otherwise would appear to be pointless statements:\n\n      msg.my_map[key]\n    \"\"\"\n    return self[key]\n\n  @overload\n  def get(self, key: _K) -> Optional[_V]:\n    ...\n\n  @overload\n  def get(self, key: _K, default: _T) -> Union[_V, _T]:\n    ...\n\n  # We need to override this explicitly, because our defaultdict-like behavior\n  # will make the default implementation (from our base class) always insert\n  # the key.\n  def get(self, key, default=None):\n    if key in self:\n      return self[key]\n    else:\n      return default\n\n  def __contains__(self, item: _K) -> bool:\n    item = self._key_checker.CheckValue(item)\n    return item in self._values\n\n  def __setitem__(self, key: _K, value: _V) -> NoReturn:\n    raise ValueError('May not set values directly, call my_map[key].foo = 5')\n\n  def __delitem__(self, key: _K) -> None:\n    key = self._key_checker.CheckValue(key)\n    del self._values[key]\n    self._message_listener.Modified()\n\n  def __len__(self) -> int:\n    return len(self._values)\n\n  def __iter__(self) -> Iterator[_K]:\n    return iter(self._values)\n\n  def __repr__(self) -> str:\n    return repr(self._values)\n\n  def MergeFrom(self, other: 'MessageMap[_K, _V]') -> None:\n    # pylint: disable=protected-access\n    for key in other._values:\n      # According to documentation: \"When parsing from the wire or when merging,\n      # if there are duplicate map keys the last key seen is used\".\n      if key in self:\n        del self[key]\n      self[key].CopyFrom(other[key])\n    # self._message_listener.Modified() not required here, because\n    # mutations to submessages already propagate.\n\n  def InvalidateIterators(self) -> None:\n    # It appears that the only way to reliably invalidate iterators to\n    # self._values is to ensure that its size changes.\n    original = self._values\n    self._values = original.copy()\n    original[None] = None\n\n  # This is defined in the abstract base, but we can do it much more cheaply.\n  def clear(self) -> None:\n    self._values.clear()\n    self._message_listener.Modified()\n\n  def GetEntryClass(self) -> Any:\n    return self._entry_descriptor._concrete_class\n\n\nclass _UnknownField:\n  \"\"\"A parsed unknown field.\"\"\"\n\n  # Disallows assignment to other attributes.\n  __slots__ = ['_field_number', '_wire_type', '_data']\n\n  def __init__(self, field_number, wire_type, data):\n    self._field_number = field_number\n    self._wire_type = wire_type\n    self._data = data\n    return\n\n  def __lt__(self, other):\n    # pylint: disable=protected-access\n    return self._field_number < other._field_number\n\n  def __eq__(self, other):\n    if self is other:\n      return True\n    # pylint: disable=protected-access\n    return (self._field_number == other._field_number and\n            self._wire_type == other._wire_type and\n            self._data == other._data)\n\n\nclass UnknownFieldRef:  # pylint: disable=missing-class-docstring\n\n  def __init__(self, parent, index):\n    self._parent = parent\n    self._index = index\n\n  def _check_valid(self):\n    if not self._parent:\n      raise ValueError('UnknownField does not exist. '\n                       'The parent message might be cleared.')\n    if self._index >= len(self._parent):\n      raise ValueError('UnknownField does not exist. '\n                       'The parent message might be cleared.')\n\n  @property\n  def field_number(self):\n    self._check_valid()\n    # pylint: disable=protected-access\n    return self._parent._internal_get(self._index)._field_number\n\n  @property\n  def wire_type(self):\n    self._check_valid()\n    # pylint: disable=protected-access\n    return self._parent._internal_get(self._index)._wire_type\n\n  @property\n  def data(self):\n    self._check_valid()\n    # pylint: disable=protected-access\n    return self._parent._internal_get(self._index)._data\n\n\nclass UnknownFieldSet:\n  \"\"\"UnknownField container\"\"\"\n\n  # Disallows assignment to other attributes.\n  __slots__ = ['_values']\n\n  def __init__(self):\n    self._values = []\n\n  def __getitem__(self, index):\n    if self._values is None:\n      raise ValueError('UnknownFields does not exist. '\n                       'The parent message might be cleared.')\n    size = len(self._values)\n    if index < 0:\n      index += size\n    if index < 0 or index >= size:\n      raise IndexError('index %d out of range'.index)\n\n    return UnknownFieldRef(self, index)\n\n  def _internal_get(self, index):\n    return self._values[index]\n\n  def __len__(self):\n    if self._values is None:\n      raise ValueError('UnknownFields does not exist. '\n                       'The parent message might be cleared.')\n    return len(self._values)\n\n  def _add(self, field_number, wire_type, data):\n    unknown_field = _UnknownField(field_number, wire_type, data)\n    self._values.append(unknown_field)\n    return unknown_field\n\n  def __iter__(self):\n    for i in range(len(self)):\n      yield UnknownFieldRef(self, i)\n\n  def _extend(self, other):\n    if other is None:\n      return\n    # pylint: disable=protected-access\n    self._values.extend(other._values)\n\n  def __eq__(self, other):\n    if self is other:\n      return True\n    # Sort unknown fields because their order shouldn't\n    # affect equality test.\n    values = list(self._values)\n    if other is None:\n      return not values\n    values.sort()\n    # pylint: disable=protected-access\n    other_values = sorted(other._values)\n    return values == other_values\n\n  def _clear(self):\n    for value in self._values:\n      # pylint: disable=protected-access\n      if isinstance(value._data, UnknownFieldSet):\n        value._data._clear()  # pylint: disable=protected-access\n    self._values = None\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/decoder.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Code for decoding protocol buffer primitives.\n\nThis code is very similar to encoder.py -- read the docs for that module first.\n\nA \"decoder\" is a function with the signature:\n  Decode(buffer, pos, end, message, field_dict)\nThe arguments are:\n  buffer:     The string containing the encoded message.\n  pos:        The current position in the string.\n  end:        The position in the string where the current message ends.  May be\n              less than len(buffer) if we're reading a sub-message.\n  message:    The message object into which we're parsing.\n  field_dict: message._fields (avoids a hashtable lookup).\nThe decoder reads the field and stores it into field_dict, returning the new\nbuffer position.  A decoder for a repeated field may proactively decode all of\nthe elements of that field, if they appear consecutively.\n\nNote that decoders may throw any of the following:\n  IndexError:  Indicates a truncated message.\n  struct.error:  Unpacking of a fixed-width field failed.\n  message.DecodeError:  Other errors.\n\nDecoders are expected to raise an exception if they are called with pos > end.\nThis allows callers to be lax about bounds checking:  it's fineto read past\n\"end\" as long as you are sure that someone else will notice and throw an\nexception later on.\n\nSomething up the call stack is expected to catch IndexError and struct.error\nand convert them to message.DecodeError.\n\nDecoders are constructed using decoder constructors with the signature:\n  MakeDecoder(field_number, is_repeated, is_packed, key, new_default)\nThe arguments are:\n  field_number:  The field number of the field we want to decode.\n  is_repeated:   Is the field a repeated field? (bool)\n  is_packed:     Is the field a packed field? (bool)\n  key:           The key to use when looking up the field within field_dict.\n                 (This is actually the FieldDescriptor but nothing in this\n                 file should depend on that.)\n  new_default:   A function which takes a message object as a parameter and\n                 returns a new instance of the default value for this field.\n                 (This is called for repeated fields and sub-messages, when an\n                 instance does not already exist.)\n\nAs with encoders, we define a decoder constructor for every type of field.\nThen, for every field of every message class we construct an actual decoder.\nThat decoder goes into a dict indexed by tag, so when we decode a message\nwe repeatedly read a tag, look up the corresponding decoder, and invoke it.\n\"\"\"\n\n__author__ = 'kenton@google.com (Kenton Varda)'\n\nimport math\nimport struct\n\nfrom google.protobuf.internal import containers\nfrom google.protobuf.internal import encoder\nfrom google.protobuf.internal import wire_format\nfrom google.protobuf import message\n\n\n# This is not for optimization, but rather to avoid conflicts with local\n# variables named \"message\".\n_DecodeError = message.DecodeError\n\n\ndef _VarintDecoder(mask, result_type):\n  \"\"\"Return an encoder for a basic varint value (does not include tag).\n\n  Decoded values will be bitwise-anded with the given mask before being\n  returned, e.g. to limit them to 32 bits.  The returned decoder does not\n  take the usual \"end\" parameter -- the caller is expected to do bounds checking\n  after the fact (often the caller can defer such checking until later).  The\n  decoder returns a (value, new_pos) pair.\n  \"\"\"\n\n  def DecodeVarint(buffer, pos):\n    result = 0\n    shift = 0\n    while 1:\n      b = buffer[pos]\n      result |= ((b & 0x7f) << shift)\n      pos += 1\n      if not (b & 0x80):\n        result &= mask\n        result = result_type(result)\n        return (result, pos)\n      shift += 7\n      if shift >= 64:\n        raise _DecodeError('Too many bytes when decoding varint.')\n  return DecodeVarint\n\n\ndef _SignedVarintDecoder(bits, result_type):\n  \"\"\"Like _VarintDecoder() but decodes signed values.\"\"\"\n\n  signbit = 1 << (bits - 1)\n  mask = (1 << bits) - 1\n\n  def DecodeVarint(buffer, pos):\n    result = 0\n    shift = 0\n    while 1:\n      b = buffer[pos]\n      result |= ((b & 0x7f) << shift)\n      pos += 1\n      if not (b & 0x80):\n        result &= mask\n        result = (result ^ signbit) - signbit\n        result = result_type(result)\n        return (result, pos)\n      shift += 7\n      if shift >= 64:\n        raise _DecodeError('Too many bytes when decoding varint.')\n  return DecodeVarint\n\n# All 32-bit and 64-bit values are represented as int.\n_DecodeVarint = _VarintDecoder((1 << 64) - 1, int)\n_DecodeSignedVarint = _SignedVarintDecoder(64, int)\n\n# Use these versions for values which must be limited to 32 bits.\n_DecodeVarint32 = _VarintDecoder((1 << 32) - 1, int)\n_DecodeSignedVarint32 = _SignedVarintDecoder(32, int)\n\n\ndef ReadTag(buffer, pos):\n  \"\"\"Read a tag from the memoryview, and return a (tag_bytes, new_pos) tuple.\n\n  We return the raw bytes of the tag rather than decoding them.  The raw\n  bytes can then be used to look up the proper decoder.  This effectively allows\n  us to trade some work that would be done in pure-python (decoding a varint)\n  for work that is done in C (searching for a byte string in a hash table).\n  In a low-level language it would be much cheaper to decode the varint and\n  use that, but not in Python.\n\n  Args:\n    buffer: memoryview object of the encoded bytes\n    pos: int of the current position to start from\n\n  Returns:\n    Tuple[bytes, int] of the tag data and new position.\n  \"\"\"\n  start = pos\n  while buffer[pos] & 0x80:\n    pos += 1\n  pos += 1\n\n  tag_bytes = buffer[start:pos].tobytes()\n  return tag_bytes, pos\n\n\n# --------------------------------------------------------------------\n\n\ndef _SimpleDecoder(wire_type, decode_value):\n  \"\"\"Return a constructor for a decoder for fields of a particular type.\n\n  Args:\n      wire_type:  The field's wire type.\n      decode_value:  A function which decodes an individual value, e.g.\n        _DecodeVarint()\n  \"\"\"\n\n  def SpecificDecoder(field_number, is_repeated, is_packed, key, new_default,\n                      clear_if_default=False):\n    if is_packed:\n      local_DecodeVarint = _DecodeVarint\n      def DecodePackedField(buffer, pos, end, message, field_dict):\n        value = field_dict.get(key)\n        if value is None:\n          value = field_dict.setdefault(key, new_default(message))\n        (endpoint, pos) = local_DecodeVarint(buffer, pos)\n        endpoint += pos\n        if endpoint > end:\n          raise _DecodeError('Truncated message.')\n        while pos < endpoint:\n          (element, pos) = decode_value(buffer, pos)\n          value.append(element)\n        if pos > endpoint:\n          del value[-1]   # Discard corrupt value.\n          raise _DecodeError('Packed element was truncated.')\n        return pos\n      return DecodePackedField\n    elif is_repeated:\n      tag_bytes = encoder.TagBytes(field_number, wire_type)\n      tag_len = len(tag_bytes)\n      def DecodeRepeatedField(buffer, pos, end, message, field_dict):\n        value = field_dict.get(key)\n        if value is None:\n          value = field_dict.setdefault(key, new_default(message))\n        while 1:\n          (element, new_pos) = decode_value(buffer, pos)\n          value.append(element)\n          # Predict that the next tag is another copy of the same repeated\n          # field.\n          pos = new_pos + tag_len\n          if buffer[new_pos:pos] != tag_bytes or new_pos >= end:\n            # Prediction failed.  Return.\n            if new_pos > end:\n              raise _DecodeError('Truncated message.')\n            return new_pos\n      return DecodeRepeatedField\n    else:\n      def DecodeField(buffer, pos, end, message, field_dict):\n        (new_value, pos) = decode_value(buffer, pos)\n        if pos > end:\n          raise _DecodeError('Truncated message.')\n        if clear_if_default and not new_value:\n          field_dict.pop(key, None)\n        else:\n          field_dict[key] = new_value\n        return pos\n      return DecodeField\n\n  return SpecificDecoder\n\n\ndef _ModifiedDecoder(wire_type, decode_value, modify_value):\n  \"\"\"Like SimpleDecoder but additionally invokes modify_value on every value\n  before storing it.  Usually modify_value is ZigZagDecode.\n  \"\"\"\n\n  # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but\n  # not enough to make a significant difference.\n\n  def InnerDecode(buffer, pos):\n    (result, new_pos) = decode_value(buffer, pos)\n    return (modify_value(result), new_pos)\n  return _SimpleDecoder(wire_type, InnerDecode)\n\n\ndef _StructPackDecoder(wire_type, format):\n  \"\"\"Return a constructor for a decoder for a fixed-width field.\n\n  Args:\n      wire_type:  The field's wire type.\n      format:  The format string to pass to struct.unpack().\n  \"\"\"\n\n  value_size = struct.calcsize(format)\n  local_unpack = struct.unpack\n\n  # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but\n  # not enough to make a significant difference.\n\n  # Note that we expect someone up-stack to catch struct.error and convert\n  # it to _DecodeError -- this way we don't have to set up exception-\n  # handling blocks every time we parse one value.\n\n  def InnerDecode(buffer, pos):\n    new_pos = pos + value_size\n    result = local_unpack(format, buffer[pos:new_pos])[0]\n    return (result, new_pos)\n  return _SimpleDecoder(wire_type, InnerDecode)\n\n\ndef _FloatDecoder():\n  \"\"\"Returns a decoder for a float field.\n\n  This code works around a bug in struct.unpack for non-finite 32-bit\n  floating-point values.\n  \"\"\"\n\n  local_unpack = struct.unpack\n\n  def InnerDecode(buffer, pos):\n    \"\"\"Decode serialized float to a float and new position.\n\n    Args:\n      buffer: memoryview of the serialized bytes\n      pos: int, position in the memory view to start at.\n\n    Returns:\n      Tuple[float, int] of the deserialized float value and new position\n      in the serialized data.\n    \"\"\"\n    # We expect a 32-bit value in little-endian byte order.  Bit 1 is the sign\n    # bit, bits 2-9 represent the exponent, and bits 10-32 are the significand.\n    new_pos = pos + 4\n    float_bytes = buffer[pos:new_pos].tobytes()\n\n    # If this value has all its exponent bits set, then it's non-finite.\n    # In Python 2.4, struct.unpack will convert it to a finite 64-bit value.\n    # To avoid that, we parse it specially.\n    if (float_bytes[3:4] in b'\\x7F\\xFF' and float_bytes[2:3] >= b'\\x80'):\n      # If at least one significand bit is set...\n      if float_bytes[0:3] != b'\\x00\\x00\\x80':\n        return (math.nan, new_pos)\n      # If sign bit is set...\n      if float_bytes[3:4] == b'\\xFF':\n        return (-math.inf, new_pos)\n      return (math.inf, new_pos)\n\n    # Note that we expect someone up-stack to catch struct.error and convert\n    # it to _DecodeError -- this way we don't have to set up exception-\n    # handling blocks every time we parse one value.\n    result = local_unpack('<f', float_bytes)[0]\n    return (result, new_pos)\n  return _SimpleDecoder(wire_format.WIRETYPE_FIXED32, InnerDecode)\n\n\ndef _DoubleDecoder():\n  \"\"\"Returns a decoder for a double field.\n\n  This code works around a bug in struct.unpack for not-a-number.\n  \"\"\"\n\n  local_unpack = struct.unpack\n\n  def InnerDecode(buffer, pos):\n    \"\"\"Decode serialized double to a double and new position.\n\n    Args:\n      buffer: memoryview of the serialized bytes.\n      pos: int, position in the memory view to start at.\n\n    Returns:\n      Tuple[float, int] of the decoded double value and new position\n      in the serialized data.\n    \"\"\"\n    # We expect a 64-bit value in little-endian byte order.  Bit 1 is the sign\n    # bit, bits 2-12 represent the exponent, and bits 13-64 are the significand.\n    new_pos = pos + 8\n    double_bytes = buffer[pos:new_pos].tobytes()\n\n    # If this value has all its exponent bits set and at least one significand\n    # bit set, it's not a number.  In Python 2.4, struct.unpack will treat it\n    # as inf or -inf.  To avoid that, we treat it specially.\n    if ((double_bytes[7:8] in b'\\x7F\\xFF')\n        and (double_bytes[6:7] >= b'\\xF0')\n        and (double_bytes[0:7] != b'\\x00\\x00\\x00\\x00\\x00\\x00\\xF0')):\n      return (math.nan, new_pos)\n\n    # Note that we expect someone up-stack to catch struct.error and convert\n    # it to _DecodeError -- this way we don't have to set up exception-\n    # handling blocks every time we parse one value.\n    result = local_unpack('<d', double_bytes)[0]\n    return (result, new_pos)\n  return _SimpleDecoder(wire_format.WIRETYPE_FIXED64, InnerDecode)\n\n\ndef EnumDecoder(field_number, is_repeated, is_packed, key, new_default,\n                clear_if_default=False):\n  \"\"\"Returns a decoder for enum field.\"\"\"\n  enum_type = key.enum_type\n  if is_packed:\n    local_DecodeVarint = _DecodeVarint\n    def DecodePackedField(buffer, pos, end, message, field_dict):\n      \"\"\"Decode serialized packed enum to its value and a new position.\n\n      Args:\n        buffer: memoryview of the serialized bytes.\n        pos: int, position in the memory view to start at.\n        end: int, end position of serialized data\n        message: Message object to store unknown fields in\n        field_dict: Map[Descriptor, Any] to store decoded values in.\n\n      Returns:\n        int, new position in serialized data.\n      \"\"\"\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      (endpoint, pos) = local_DecodeVarint(buffer, pos)\n      endpoint += pos\n      if endpoint > end:\n        raise _DecodeError('Truncated message.')\n      while pos < endpoint:\n        value_start_pos = pos\n        (element, pos) = _DecodeSignedVarint32(buffer, pos)\n        # pylint: disable=protected-access\n        if element in enum_type.values_by_number:\n          value.append(element)\n        else:\n          if not message._unknown_fields:\n            message._unknown_fields = []\n          tag_bytes = encoder.TagBytes(field_number,\n                                       wire_format.WIRETYPE_VARINT)\n\n          message._unknown_fields.append(\n              (tag_bytes, buffer[value_start_pos:pos].tobytes()))\n          if message._unknown_field_set is None:\n            message._unknown_field_set = containers.UnknownFieldSet()\n          message._unknown_field_set._add(\n              field_number, wire_format.WIRETYPE_VARINT, element)\n          # pylint: enable=protected-access\n      if pos > endpoint:\n        if element in enum_type.values_by_number:\n          del value[-1]   # Discard corrupt value.\n        else:\n          del message._unknown_fields[-1]\n          # pylint: disable=protected-access\n          del message._unknown_field_set._values[-1]\n          # pylint: enable=protected-access\n        raise _DecodeError('Packed element was truncated.')\n      return pos\n    return DecodePackedField\n  elif is_repeated:\n    tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_VARINT)\n    tag_len = len(tag_bytes)\n    def DecodeRepeatedField(buffer, pos, end, message, field_dict):\n      \"\"\"Decode serialized repeated enum to its value and a new position.\n\n      Args:\n        buffer: memoryview of the serialized bytes.\n        pos: int, position in the memory view to start at.\n        end: int, end position of serialized data\n        message: Message object to store unknown fields in\n        field_dict: Map[Descriptor, Any] to store decoded values in.\n\n      Returns:\n        int, new position in serialized data.\n      \"\"\"\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      while 1:\n        (element, new_pos) = _DecodeSignedVarint32(buffer, pos)\n        # pylint: disable=protected-access\n        if element in enum_type.values_by_number:\n          value.append(element)\n        else:\n          if not message._unknown_fields:\n            message._unknown_fields = []\n          message._unknown_fields.append(\n              (tag_bytes, buffer[pos:new_pos].tobytes()))\n          if message._unknown_field_set is None:\n            message._unknown_field_set = containers.UnknownFieldSet()\n          message._unknown_field_set._add(\n              field_number, wire_format.WIRETYPE_VARINT, element)\n        # pylint: enable=protected-access\n        # Predict that the next tag is another copy of the same repeated\n        # field.\n        pos = new_pos + tag_len\n        if buffer[new_pos:pos] != tag_bytes or new_pos >= end:\n          # Prediction failed.  Return.\n          if new_pos > end:\n            raise _DecodeError('Truncated message.')\n          return new_pos\n    return DecodeRepeatedField\n  else:\n    def DecodeField(buffer, pos, end, message, field_dict):\n      \"\"\"Decode serialized repeated enum to its value and a new position.\n\n      Args:\n        buffer: memoryview of the serialized bytes.\n        pos: int, position in the memory view to start at.\n        end: int, end position of serialized data\n        message: Message object to store unknown fields in\n        field_dict: Map[Descriptor, Any] to store decoded values in.\n\n      Returns:\n        int, new position in serialized data.\n      \"\"\"\n      value_start_pos = pos\n      (enum_value, pos) = _DecodeSignedVarint32(buffer, pos)\n      if pos > end:\n        raise _DecodeError('Truncated message.')\n      if clear_if_default and not enum_value:\n        field_dict.pop(key, None)\n        return pos\n      # pylint: disable=protected-access\n      if enum_value in enum_type.values_by_number:\n        field_dict[key] = enum_value\n      else:\n        if not message._unknown_fields:\n          message._unknown_fields = []\n        tag_bytes = encoder.TagBytes(field_number,\n                                     wire_format.WIRETYPE_VARINT)\n        message._unknown_fields.append(\n            (tag_bytes, buffer[value_start_pos:pos].tobytes()))\n        if message._unknown_field_set is None:\n          message._unknown_field_set = containers.UnknownFieldSet()\n        message._unknown_field_set._add(\n            field_number, wire_format.WIRETYPE_VARINT, enum_value)\n        # pylint: enable=protected-access\n      return pos\n    return DecodeField\n\n\n# --------------------------------------------------------------------\n\n\nInt32Decoder = _SimpleDecoder(\n    wire_format.WIRETYPE_VARINT, _DecodeSignedVarint32)\n\nInt64Decoder = _SimpleDecoder(\n    wire_format.WIRETYPE_VARINT, _DecodeSignedVarint)\n\nUInt32Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint32)\nUInt64Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint)\n\nSInt32Decoder = _ModifiedDecoder(\n    wire_format.WIRETYPE_VARINT, _DecodeVarint32, wire_format.ZigZagDecode)\nSInt64Decoder = _ModifiedDecoder(\n    wire_format.WIRETYPE_VARINT, _DecodeVarint, wire_format.ZigZagDecode)\n\n# Note that Python conveniently guarantees that when using the '<' prefix on\n# formats, they will also have the same size across all platforms (as opposed\n# to without the prefix, where their sizes depend on the C compiler's basic\n# type sizes).\nFixed32Decoder  = _StructPackDecoder(wire_format.WIRETYPE_FIXED32, '<I')\nFixed64Decoder  = _StructPackDecoder(wire_format.WIRETYPE_FIXED64, '<Q')\nSFixed32Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED32, '<i')\nSFixed64Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED64, '<q')\nFloatDecoder = _FloatDecoder()\nDoubleDecoder = _DoubleDecoder()\n\nBoolDecoder = _ModifiedDecoder(\n    wire_format.WIRETYPE_VARINT, _DecodeVarint, bool)\n\n\ndef StringDecoder(field_number, is_repeated, is_packed, key, new_default,\n                  clear_if_default=False):\n  \"\"\"Returns a decoder for a string field.\"\"\"\n\n  local_DecodeVarint = _DecodeVarint\n\n  def _ConvertToUnicode(memview):\n    \"\"\"Convert byte to unicode.\"\"\"\n    byte_str = memview.tobytes()\n    try:\n      value = str(byte_str, 'utf-8')\n    except UnicodeDecodeError as e:\n      # add more information to the error message and re-raise it.\n      e.reason = '%s in field: %s' % (e, key.full_name)\n      raise\n\n    return value\n\n  assert not is_packed\n  if is_repeated:\n    tag_bytes = encoder.TagBytes(field_number,\n                                 wire_format.WIRETYPE_LENGTH_DELIMITED)\n    tag_len = len(tag_bytes)\n    def DecodeRepeatedField(buffer, pos, end, message, field_dict):\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      while 1:\n        (size, pos) = local_DecodeVarint(buffer, pos)\n        new_pos = pos + size\n        if new_pos > end:\n          raise _DecodeError('Truncated string.')\n        value.append(_ConvertToUnicode(buffer[pos:new_pos]))\n        # Predict that the next tag is another copy of the same repeated field.\n        pos = new_pos + tag_len\n        if buffer[new_pos:pos] != tag_bytes or new_pos == end:\n          # Prediction failed.  Return.\n          return new_pos\n    return DecodeRepeatedField\n  else:\n    def DecodeField(buffer, pos, end, message, field_dict):\n      (size, pos) = local_DecodeVarint(buffer, pos)\n      new_pos = pos + size\n      if new_pos > end:\n        raise _DecodeError('Truncated string.')\n      if clear_if_default and not size:\n        field_dict.pop(key, None)\n      else:\n        field_dict[key] = _ConvertToUnicode(buffer[pos:new_pos])\n      return new_pos\n    return DecodeField\n\n\ndef BytesDecoder(field_number, is_repeated, is_packed, key, new_default,\n                 clear_if_default=False):\n  \"\"\"Returns a decoder for a bytes field.\"\"\"\n\n  local_DecodeVarint = _DecodeVarint\n\n  assert not is_packed\n  if is_repeated:\n    tag_bytes = encoder.TagBytes(field_number,\n                                 wire_format.WIRETYPE_LENGTH_DELIMITED)\n    tag_len = len(tag_bytes)\n    def DecodeRepeatedField(buffer, pos, end, message, field_dict):\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      while 1:\n        (size, pos) = local_DecodeVarint(buffer, pos)\n        new_pos = pos + size\n        if new_pos > end:\n          raise _DecodeError('Truncated string.')\n        value.append(buffer[pos:new_pos].tobytes())\n        # Predict that the next tag is another copy of the same repeated field.\n        pos = new_pos + tag_len\n        if buffer[new_pos:pos] != tag_bytes or new_pos == end:\n          # Prediction failed.  Return.\n          return new_pos\n    return DecodeRepeatedField\n  else:\n    def DecodeField(buffer, pos, end, message, field_dict):\n      (size, pos) = local_DecodeVarint(buffer, pos)\n      new_pos = pos + size\n      if new_pos > end:\n        raise _DecodeError('Truncated string.')\n      if clear_if_default and not size:\n        field_dict.pop(key, None)\n      else:\n        field_dict[key] = buffer[pos:new_pos].tobytes()\n      return new_pos\n    return DecodeField\n\n\ndef GroupDecoder(field_number, is_repeated, is_packed, key, new_default):\n  \"\"\"Returns a decoder for a group field.\"\"\"\n\n  end_tag_bytes = encoder.TagBytes(field_number,\n                                   wire_format.WIRETYPE_END_GROUP)\n  end_tag_len = len(end_tag_bytes)\n\n  assert not is_packed\n  if is_repeated:\n    tag_bytes = encoder.TagBytes(field_number,\n                                 wire_format.WIRETYPE_START_GROUP)\n    tag_len = len(tag_bytes)\n    def DecodeRepeatedField(buffer, pos, end, message, field_dict):\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      while 1:\n        value = field_dict.get(key)\n        if value is None:\n          value = field_dict.setdefault(key, new_default(message))\n        # Read sub-message.\n        pos = value.add()._InternalParse(buffer, pos, end)\n        # Read end tag.\n        new_pos = pos+end_tag_len\n        if buffer[pos:new_pos] != end_tag_bytes or new_pos > end:\n          raise _DecodeError('Missing group end tag.')\n        # Predict that the next tag is another copy of the same repeated field.\n        pos = new_pos + tag_len\n        if buffer[new_pos:pos] != tag_bytes or new_pos == end:\n          # Prediction failed.  Return.\n          return new_pos\n    return DecodeRepeatedField\n  else:\n    def DecodeField(buffer, pos, end, message, field_dict):\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      # Read sub-message.\n      pos = value._InternalParse(buffer, pos, end)\n      # Read end tag.\n      new_pos = pos+end_tag_len\n      if buffer[pos:new_pos] != end_tag_bytes or new_pos > end:\n        raise _DecodeError('Missing group end tag.')\n      return new_pos\n    return DecodeField\n\n\ndef MessageDecoder(field_number, is_repeated, is_packed, key, new_default):\n  \"\"\"Returns a decoder for a message field.\"\"\"\n\n  local_DecodeVarint = _DecodeVarint\n\n  assert not is_packed\n  if is_repeated:\n    tag_bytes = encoder.TagBytes(field_number,\n                                 wire_format.WIRETYPE_LENGTH_DELIMITED)\n    tag_len = len(tag_bytes)\n    def DecodeRepeatedField(buffer, pos, end, message, field_dict):\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      while 1:\n        # Read length.\n        (size, pos) = local_DecodeVarint(buffer, pos)\n        new_pos = pos + size\n        if new_pos > end:\n          raise _DecodeError('Truncated message.')\n        # Read sub-message.\n        if value.add()._InternalParse(buffer, pos, new_pos) != new_pos:\n          # The only reason _InternalParse would return early is if it\n          # encountered an end-group tag.\n          raise _DecodeError('Unexpected end-group tag.')\n        # Predict that the next tag is another copy of the same repeated field.\n        pos = new_pos + tag_len\n        if buffer[new_pos:pos] != tag_bytes or new_pos == end:\n          # Prediction failed.  Return.\n          return new_pos\n    return DecodeRepeatedField\n  else:\n    def DecodeField(buffer, pos, end, message, field_dict):\n      value = field_dict.get(key)\n      if value is None:\n        value = field_dict.setdefault(key, new_default(message))\n      # Read length.\n      (size, pos) = local_DecodeVarint(buffer, pos)\n      new_pos = pos + size\n      if new_pos > end:\n        raise _DecodeError('Truncated message.')\n      # Read sub-message.\n      if value._InternalParse(buffer, pos, new_pos) != new_pos:\n        # The only reason _InternalParse would return early is if it encountered\n        # an end-group tag.\n        raise _DecodeError('Unexpected end-group tag.')\n      return new_pos\n    return DecodeField\n\n\n# --------------------------------------------------------------------\n\nMESSAGE_SET_ITEM_TAG = encoder.TagBytes(1, wire_format.WIRETYPE_START_GROUP)\n\ndef MessageSetItemDecoder(descriptor):\n  \"\"\"Returns a decoder for a MessageSet item.\n\n  The parameter is the message Descriptor.\n\n  The message set message looks like this:\n    message MessageSet {\n      repeated group Item = 1 {\n        required int32 type_id = 2;\n        required string message = 3;\n      }\n    }\n  \"\"\"\n\n  type_id_tag_bytes = encoder.TagBytes(2, wire_format.WIRETYPE_VARINT)\n  message_tag_bytes = encoder.TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED)\n  item_end_tag_bytes = encoder.TagBytes(1, wire_format.WIRETYPE_END_GROUP)\n\n  local_ReadTag = ReadTag\n  local_DecodeVarint = _DecodeVarint\n  local_SkipField = SkipField\n\n  def DecodeItem(buffer, pos, end, message, field_dict):\n    \"\"\"Decode serialized message set to its value and new position.\n\n    Args:\n      buffer: memoryview of the serialized bytes.\n      pos: int, position in the memory view to start at.\n      end: int, end position of serialized data\n      message: Message object to store unknown fields in\n      field_dict: Map[Descriptor, Any] to store decoded values in.\n\n    Returns:\n      int, new position in serialized data.\n    \"\"\"\n    message_set_item_start = pos\n    type_id = -1\n    message_start = -1\n    message_end = -1\n\n    # Technically, type_id and message can appear in any order, so we need\n    # a little loop here.\n    while 1:\n      (tag_bytes, pos) = local_ReadTag(buffer, pos)\n      if tag_bytes == type_id_tag_bytes:\n        (type_id, pos) = local_DecodeVarint(buffer, pos)\n      elif tag_bytes == message_tag_bytes:\n        (size, message_start) = local_DecodeVarint(buffer, pos)\n        pos = message_end = message_start + size\n      elif tag_bytes == item_end_tag_bytes:\n        break\n      else:\n        pos = SkipField(buffer, pos, end, tag_bytes)\n        if pos == -1:\n          raise _DecodeError('Missing group end tag.')\n\n    if pos > end:\n      raise _DecodeError('Truncated message.')\n\n    if type_id == -1:\n      raise _DecodeError('MessageSet item missing type_id.')\n    if message_start == -1:\n      raise _DecodeError('MessageSet item missing message.')\n\n    extension = message.Extensions._FindExtensionByNumber(type_id)\n    # pylint: disable=protected-access\n    if extension is not None:\n      value = field_dict.get(extension)\n      if value is None:\n        message_type = extension.message_type\n        if not hasattr(message_type, '_concrete_class'):\n          # pylint: disable=protected-access\n          message._FACTORY.GetPrototype(message_type)\n        value = field_dict.setdefault(\n            extension, message_type._concrete_class())\n      if value._InternalParse(buffer, message_start,message_end) != message_end:\n        # The only reason _InternalParse would return early is if it encountered\n        # an end-group tag.\n        raise _DecodeError('Unexpected end-group tag.')\n    else:\n      if not message._unknown_fields:\n        message._unknown_fields = []\n      message._unknown_fields.append(\n          (MESSAGE_SET_ITEM_TAG, buffer[message_set_item_start:pos].tobytes()))\n      if message._unknown_field_set is None:\n        message._unknown_field_set = containers.UnknownFieldSet()\n      message._unknown_field_set._add(\n          type_id,\n          wire_format.WIRETYPE_LENGTH_DELIMITED,\n          buffer[message_start:message_end].tobytes())\n      # pylint: enable=protected-access\n\n    return pos\n\n  return DecodeItem\n\n# --------------------------------------------------------------------\n\ndef MapDecoder(field_descriptor, new_default, is_message_map):\n  \"\"\"Returns a decoder for a map field.\"\"\"\n\n  key = field_descriptor\n  tag_bytes = encoder.TagBytes(field_descriptor.number,\n                               wire_format.WIRETYPE_LENGTH_DELIMITED)\n  tag_len = len(tag_bytes)\n  local_DecodeVarint = _DecodeVarint\n  # Can't read _concrete_class yet; might not be initialized.\n  message_type = field_descriptor.message_type\n\n  def DecodeMap(buffer, pos, end, message, field_dict):\n    submsg = message_type._concrete_class()\n    value = field_dict.get(key)\n    if value is None:\n      value = field_dict.setdefault(key, new_default(message))\n    while 1:\n      # Read length.\n      (size, pos) = local_DecodeVarint(buffer, pos)\n      new_pos = pos + size\n      if new_pos > end:\n        raise _DecodeError('Truncated message.')\n      # Read sub-message.\n      submsg.Clear()\n      if submsg._InternalParse(buffer, pos, new_pos) != new_pos:\n        # The only reason _InternalParse would return early is if it\n        # encountered an end-group tag.\n        raise _DecodeError('Unexpected end-group tag.')\n\n      if is_message_map:\n        value[submsg.key].CopyFrom(submsg.value)\n      else:\n        value[submsg.key] = submsg.value\n\n      # Predict that the next tag is another copy of the same repeated field.\n      pos = new_pos + tag_len\n      if buffer[new_pos:pos] != tag_bytes or new_pos == end:\n        # Prediction failed.  Return.\n        return new_pos\n\n  return DecodeMap\n\n# --------------------------------------------------------------------\n# Optimization is not as heavy here because calls to SkipField() are rare,\n# except for handling end-group tags.\n\ndef _SkipVarint(buffer, pos, end):\n  \"\"\"Skip a varint value.  Returns the new position.\"\"\"\n  # Previously ord(buffer[pos]) raised IndexError when pos is out of range.\n  # With this code, ord(b'') raises TypeError.  Both are handled in\n  # python_message.py to generate a 'Truncated message' error.\n  while ord(buffer[pos:pos+1].tobytes()) & 0x80:\n    pos += 1\n  pos += 1\n  if pos > end:\n    raise _DecodeError('Truncated message.')\n  return pos\n\ndef _SkipFixed64(buffer, pos, end):\n  \"\"\"Skip a fixed64 value.  Returns the new position.\"\"\"\n\n  pos += 8\n  if pos > end:\n    raise _DecodeError('Truncated message.')\n  return pos\n\n\ndef _DecodeFixed64(buffer, pos):\n  \"\"\"Decode a fixed64.\"\"\"\n  new_pos = pos + 8\n  return (struct.unpack('<Q', buffer[pos:new_pos])[0], new_pos)\n\n\ndef _SkipLengthDelimited(buffer, pos, end):\n  \"\"\"Skip a length-delimited value.  Returns the new position.\"\"\"\n\n  (size, pos) = _DecodeVarint(buffer, pos)\n  pos += size\n  if pos > end:\n    raise _DecodeError('Truncated message.')\n  return pos\n\n\ndef _SkipGroup(buffer, pos, end):\n  \"\"\"Skip sub-group.  Returns the new position.\"\"\"\n\n  while 1:\n    (tag_bytes, pos) = ReadTag(buffer, pos)\n    new_pos = SkipField(buffer, pos, end, tag_bytes)\n    if new_pos == -1:\n      return pos\n    pos = new_pos\n\n\ndef _DecodeUnknownFieldSet(buffer, pos, end_pos=None):\n  \"\"\"Decode UnknownFieldSet.  Returns the UnknownFieldSet and new position.\"\"\"\n\n  unknown_field_set = containers.UnknownFieldSet()\n  while end_pos is None or pos < end_pos:\n    (tag_bytes, pos) = ReadTag(buffer, pos)\n    (tag, _) = _DecodeVarint(tag_bytes, 0)\n    field_number, wire_type = wire_format.UnpackTag(tag)\n    if wire_type == wire_format.WIRETYPE_END_GROUP:\n      break\n    (data, pos) = _DecodeUnknownField(buffer, pos, wire_type)\n    # pylint: disable=protected-access\n    unknown_field_set._add(field_number, wire_type, data)\n\n  return (unknown_field_set, pos)\n\n\ndef _DecodeUnknownField(buffer, pos, wire_type):\n  \"\"\"Decode a unknown field.  Returns the UnknownField and new position.\"\"\"\n\n  if wire_type == wire_format.WIRETYPE_VARINT:\n    (data, pos) = _DecodeVarint(buffer, pos)\n  elif wire_type == wire_format.WIRETYPE_FIXED64:\n    (data, pos) = _DecodeFixed64(buffer, pos)\n  elif wire_type == wire_format.WIRETYPE_FIXED32:\n    (data, pos) = _DecodeFixed32(buffer, pos)\n  elif wire_type == wire_format.WIRETYPE_LENGTH_DELIMITED:\n    (size, pos) = _DecodeVarint(buffer, pos)\n    data = buffer[pos:pos+size].tobytes()\n    pos += size\n  elif wire_type == wire_format.WIRETYPE_START_GROUP:\n    (data, pos) = _DecodeUnknownFieldSet(buffer, pos)\n  elif wire_type == wire_format.WIRETYPE_END_GROUP:\n    return (0, -1)\n  else:\n    raise _DecodeError('Wrong wire type in tag.')\n\n  return (data, pos)\n\n\ndef _EndGroup(buffer, pos, end):\n  \"\"\"Skipping an END_GROUP tag returns -1 to tell the parent loop to break.\"\"\"\n\n  return -1\n\n\ndef _SkipFixed32(buffer, pos, end):\n  \"\"\"Skip a fixed32 value.  Returns the new position.\"\"\"\n\n  pos += 4\n  if pos > end:\n    raise _DecodeError('Truncated message.')\n  return pos\n\n\ndef _DecodeFixed32(buffer, pos):\n  \"\"\"Decode a fixed32.\"\"\"\n\n  new_pos = pos + 4\n  return (struct.unpack('<I', buffer[pos:new_pos])[0], new_pos)\n\n\ndef _RaiseInvalidWireType(buffer, pos, end):\n  \"\"\"Skip function for unknown wire types.  Raises an exception.\"\"\"\n\n  raise _DecodeError('Tag had invalid wire type.')\n\ndef _FieldSkipper():\n  \"\"\"Constructs the SkipField function.\"\"\"\n\n  WIRETYPE_TO_SKIPPER = [\n      _SkipVarint,\n      _SkipFixed64,\n      _SkipLengthDelimited,\n      _SkipGroup,\n      _EndGroup,\n      _SkipFixed32,\n      _RaiseInvalidWireType,\n      _RaiseInvalidWireType,\n      ]\n\n  wiretype_mask = wire_format.TAG_TYPE_MASK\n\n  def SkipField(buffer, pos, end, tag_bytes):\n    \"\"\"Skips a field with the specified tag.\n\n    |pos| should point to the byte immediately after the tag.\n\n    Returns:\n        The new position (after the tag value), or -1 if the tag is an end-group\n        tag (in which case the calling loop should break).\n    \"\"\"\n\n    # The wire type is always in the first byte since varints are little-endian.\n    wire_type = ord(tag_bytes[0:1]) & wiretype_mask\n    return WIRETYPE_TO_SKIPPER[wire_type](buffer, pos, end)\n\n  return SkipField\n\nSkipField = _FieldSkipper()\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/encoder.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Code for encoding protocol message primitives.\n\nContains the logic for encoding every logical protocol field type\ninto one of the 5 physical wire types.\n\nThis code is designed to push the Python interpreter's performance to the\nlimits.\n\nThe basic idea is that at startup time, for every field (i.e. every\nFieldDescriptor) we construct two functions:  a \"sizer\" and an \"encoder\".  The\nsizer takes a value of this field's type and computes its byte size.  The\nencoder takes a writer function and a value.  It encodes the value into byte\nstrings and invokes the writer function to write those strings.  Typically the\nwriter function is the write() method of a BytesIO.\n\nWe try to do as much work as possible when constructing the writer and the\nsizer rather than when calling them.  In particular:\n* We copy any needed global functions to local variables, so that we do not need\n  to do costly global table lookups at runtime.\n* Similarly, we try to do any attribute lookups at startup time if possible.\n* Every field's tag is encoded to bytes at startup, since it can't change at\n  runtime.\n* Whatever component of the field size we can compute at startup, we do.\n* We *avoid* sharing code if doing so would make the code slower and not sharing\n  does not burden us too much.  For example, encoders for repeated fields do\n  not just call the encoders for singular fields in a loop because this would\n  add an extra function call overhead for every loop iteration; instead, we\n  manually inline the single-value encoder into the loop.\n* If a Python function lacks a return statement, Python actually generates\n  instructions to pop the result of the last statement off the stack, push\n  None onto the stack, and then return that.  If we really don't care what\n  value is returned, then we can save two instructions by returning the\n  result of the last statement.  It looks funny but it helps.\n* We assume that type and bounds checking has happened at a higher level.\n\"\"\"\n\n__author__ = 'kenton@google.com (Kenton Varda)'\n\nimport struct\n\nfrom google.protobuf.internal import wire_format\n\n\n# This will overflow and thus become IEEE-754 \"infinity\".  We would use\n# \"float('inf')\" but it doesn't work on Windows pre-Python-2.6.\n_POS_INF = 1e10000\n_NEG_INF = -_POS_INF\n\n\ndef _VarintSize(value):\n  \"\"\"Compute the size of a varint value.\"\"\"\n  if value <= 0x7f: return 1\n  if value <= 0x3fff: return 2\n  if value <= 0x1fffff: return 3\n  if value <= 0xfffffff: return 4\n  if value <= 0x7ffffffff: return 5\n  if value <= 0x3ffffffffff: return 6\n  if value <= 0x1ffffffffffff: return 7\n  if value <= 0xffffffffffffff: return 8\n  if value <= 0x7fffffffffffffff: return 9\n  return 10\n\n\ndef _SignedVarintSize(value):\n  \"\"\"Compute the size of a signed varint value.\"\"\"\n  if value < 0: return 10\n  if value <= 0x7f: return 1\n  if value <= 0x3fff: return 2\n  if value <= 0x1fffff: return 3\n  if value <= 0xfffffff: return 4\n  if value <= 0x7ffffffff: return 5\n  if value <= 0x3ffffffffff: return 6\n  if value <= 0x1ffffffffffff: return 7\n  if value <= 0xffffffffffffff: return 8\n  if value <= 0x7fffffffffffffff: return 9\n  return 10\n\n\ndef _TagSize(field_number):\n  \"\"\"Returns the number of bytes required to serialize a tag with this field\n  number.\"\"\"\n  # Just pass in type 0, since the type won't affect the tag+type size.\n  return _VarintSize(wire_format.PackTag(field_number, 0))\n\n\n# --------------------------------------------------------------------\n# In this section we define some generic sizers.  Each of these functions\n# takes parameters specific to a particular field type, e.g. int32 or fixed64.\n# It returns another function which in turn takes parameters specific to a\n# particular field, e.g. the field number and whether it is repeated or packed.\n# Look at the next section to see how these are used.\n\n\ndef _SimpleSizer(compute_value_size):\n  \"\"\"A sizer which uses the function compute_value_size to compute the size of\n  each value.  Typically compute_value_size is _VarintSize.\"\"\"\n\n  def SpecificSizer(field_number, is_repeated, is_packed):\n    tag_size = _TagSize(field_number)\n    if is_packed:\n      local_VarintSize = _VarintSize\n      def PackedFieldSize(value):\n        result = 0\n        for element in value:\n          result += compute_value_size(element)\n        return result + local_VarintSize(result) + tag_size\n      return PackedFieldSize\n    elif is_repeated:\n      def RepeatedFieldSize(value):\n        result = tag_size * len(value)\n        for element in value:\n          result += compute_value_size(element)\n        return result\n      return RepeatedFieldSize\n    else:\n      def FieldSize(value):\n        return tag_size + compute_value_size(value)\n      return FieldSize\n\n  return SpecificSizer\n\n\ndef _ModifiedSizer(compute_value_size, modify_value):\n  \"\"\"Like SimpleSizer, but modify_value is invoked on each value before it is\n  passed to compute_value_size.  modify_value is typically ZigZagEncode.\"\"\"\n\n  def SpecificSizer(field_number, is_repeated, is_packed):\n    tag_size = _TagSize(field_number)\n    if is_packed:\n      local_VarintSize = _VarintSize\n      def PackedFieldSize(value):\n        result = 0\n        for element in value:\n          result += compute_value_size(modify_value(element))\n        return result + local_VarintSize(result) + tag_size\n      return PackedFieldSize\n    elif is_repeated:\n      def RepeatedFieldSize(value):\n        result = tag_size * len(value)\n        for element in value:\n          result += compute_value_size(modify_value(element))\n        return result\n      return RepeatedFieldSize\n    else:\n      def FieldSize(value):\n        return tag_size + compute_value_size(modify_value(value))\n      return FieldSize\n\n  return SpecificSizer\n\n\ndef _FixedSizer(value_size):\n  \"\"\"Like _SimpleSizer except for a fixed-size field.  The input is the size\n  of one value.\"\"\"\n\n  def SpecificSizer(field_number, is_repeated, is_packed):\n    tag_size = _TagSize(field_number)\n    if is_packed:\n      local_VarintSize = _VarintSize\n      def PackedFieldSize(value):\n        result = len(value) * value_size\n        return result + local_VarintSize(result) + tag_size\n      return PackedFieldSize\n    elif is_repeated:\n      element_size = value_size + tag_size\n      def RepeatedFieldSize(value):\n        return len(value) * element_size\n      return RepeatedFieldSize\n    else:\n      field_size = value_size + tag_size\n      def FieldSize(value):\n        return field_size\n      return FieldSize\n\n  return SpecificSizer\n\n\n# ====================================================================\n# Here we declare a sizer constructor for each field type.  Each \"sizer\n# constructor\" is a function that takes (field_number, is_repeated, is_packed)\n# as parameters and returns a sizer, which in turn takes a field value as\n# a parameter and returns its encoded size.\n\n\nInt32Sizer = Int64Sizer = EnumSizer = _SimpleSizer(_SignedVarintSize)\n\nUInt32Sizer = UInt64Sizer = _SimpleSizer(_VarintSize)\n\nSInt32Sizer = SInt64Sizer = _ModifiedSizer(\n    _SignedVarintSize, wire_format.ZigZagEncode)\n\nFixed32Sizer = SFixed32Sizer = FloatSizer  = _FixedSizer(4)\nFixed64Sizer = SFixed64Sizer = DoubleSizer = _FixedSizer(8)\n\nBoolSizer = _FixedSizer(1)\n\n\ndef StringSizer(field_number, is_repeated, is_packed):\n  \"\"\"Returns a sizer for a string field.\"\"\"\n\n  tag_size = _TagSize(field_number)\n  local_VarintSize = _VarintSize\n  local_len = len\n  assert not is_packed\n  if is_repeated:\n    def RepeatedFieldSize(value):\n      result = tag_size * len(value)\n      for element in value:\n        l = local_len(element.encode('utf-8'))\n        result += local_VarintSize(l) + l\n      return result\n    return RepeatedFieldSize\n  else:\n    def FieldSize(value):\n      l = local_len(value.encode('utf-8'))\n      return tag_size + local_VarintSize(l) + l\n    return FieldSize\n\n\ndef BytesSizer(field_number, is_repeated, is_packed):\n  \"\"\"Returns a sizer for a bytes field.\"\"\"\n\n  tag_size = _TagSize(field_number)\n  local_VarintSize = _VarintSize\n  local_len = len\n  assert not is_packed\n  if is_repeated:\n    def RepeatedFieldSize(value):\n      result = tag_size * len(value)\n      for element in value:\n        l = local_len(element)\n        result += local_VarintSize(l) + l\n      return result\n    return RepeatedFieldSize\n  else:\n    def FieldSize(value):\n      l = local_len(value)\n      return tag_size + local_VarintSize(l) + l\n    return FieldSize\n\n\ndef GroupSizer(field_number, is_repeated, is_packed):\n  \"\"\"Returns a sizer for a group field.\"\"\"\n\n  tag_size = _TagSize(field_number) * 2\n  assert not is_packed\n  if is_repeated:\n    def RepeatedFieldSize(value):\n      result = tag_size * len(value)\n      for element in value:\n        result += element.ByteSize()\n      return result\n    return RepeatedFieldSize\n  else:\n    def FieldSize(value):\n      return tag_size + value.ByteSize()\n    return FieldSize\n\n\ndef MessageSizer(field_number, is_repeated, is_packed):\n  \"\"\"Returns a sizer for a message field.\"\"\"\n\n  tag_size = _TagSize(field_number)\n  local_VarintSize = _VarintSize\n  assert not is_packed\n  if is_repeated:\n    def RepeatedFieldSize(value):\n      result = tag_size * len(value)\n      for element in value:\n        l = element.ByteSize()\n        result += local_VarintSize(l) + l\n      return result\n    return RepeatedFieldSize\n  else:\n    def FieldSize(value):\n      l = value.ByteSize()\n      return tag_size + local_VarintSize(l) + l\n    return FieldSize\n\n\n# --------------------------------------------------------------------\n# MessageSet is special: it needs custom logic to compute its size properly.\n\n\ndef MessageSetItemSizer(field_number):\n  \"\"\"Returns a sizer for extensions of MessageSet.\n\n  The message set message looks like this:\n    message MessageSet {\n      repeated group Item = 1 {\n        required int32 type_id = 2;\n        required string message = 3;\n      }\n    }\n  \"\"\"\n  static_size = (_TagSize(1) * 2 + _TagSize(2) + _VarintSize(field_number) +\n                 _TagSize(3))\n  local_VarintSize = _VarintSize\n\n  def FieldSize(value):\n    l = value.ByteSize()\n    return static_size + local_VarintSize(l) + l\n\n  return FieldSize\n\n\n# --------------------------------------------------------------------\n# Map is special: it needs custom logic to compute its size properly.\n\n\ndef MapSizer(field_descriptor, is_message_map):\n  \"\"\"Returns a sizer for a map field.\"\"\"\n\n  # Can't look at field_descriptor.message_type._concrete_class because it may\n  # not have been initialized yet.\n  message_type = field_descriptor.message_type\n  message_sizer = MessageSizer(field_descriptor.number, False, False)\n\n  def FieldSize(map_value):\n    total = 0\n    for key in map_value:\n      value = map_value[key]\n      # It's wasteful to create the messages and throw them away one second\n      # later since we'll do the same for the actual encode.  But there's not an\n      # obvious way to avoid this within the current design without tons of code\n      # duplication. For message map, value.ByteSize() should be called to\n      # update the status.\n      entry_msg = message_type._concrete_class(key=key, value=value)\n      total += message_sizer(entry_msg)\n      if is_message_map:\n        value.ByteSize()\n    return total\n\n  return FieldSize\n\n# ====================================================================\n# Encoders!\n\n\ndef _VarintEncoder():\n  \"\"\"Return an encoder for a basic varint value (does not include tag).\"\"\"\n\n  local_int2byte = struct.Struct('>B').pack\n\n  def EncodeVarint(write, value, unused_deterministic=None):\n    bits = value & 0x7f\n    value >>= 7\n    while value:\n      write(local_int2byte(0x80|bits))\n      bits = value & 0x7f\n      value >>= 7\n    return write(local_int2byte(bits))\n\n  return EncodeVarint\n\n\ndef _SignedVarintEncoder():\n  \"\"\"Return an encoder for a basic signed varint value (does not include\n  tag).\"\"\"\n\n  local_int2byte = struct.Struct('>B').pack\n\n  def EncodeSignedVarint(write, value, unused_deterministic=None):\n    if value < 0:\n      value += (1 << 64)\n    bits = value & 0x7f\n    value >>= 7\n    while value:\n      write(local_int2byte(0x80|bits))\n      bits = value & 0x7f\n      value >>= 7\n    return write(local_int2byte(bits))\n\n  return EncodeSignedVarint\n\n\n_EncodeVarint = _VarintEncoder()\n_EncodeSignedVarint = _SignedVarintEncoder()\n\n\ndef _VarintBytes(value):\n  \"\"\"Encode the given integer as a varint and return the bytes.  This is only\n  called at startup time so it doesn't need to be fast.\"\"\"\n\n  pieces = []\n  _EncodeVarint(pieces.append, value, True)\n  return b\"\".join(pieces)\n\n\ndef TagBytes(field_number, wire_type):\n  \"\"\"Encode the given tag and return the bytes.  Only called at startup.\"\"\"\n\n  return bytes(_VarintBytes(wire_format.PackTag(field_number, wire_type)))\n\n# --------------------------------------------------------------------\n# As with sizers (see above), we have a number of common encoder\n# implementations.\n\n\ndef _SimpleEncoder(wire_type, encode_value, compute_value_size):\n  \"\"\"Return a constructor for an encoder for fields of a particular type.\n\n  Args:\n      wire_type:  The field's wire type, for encoding tags.\n      encode_value:  A function which encodes an individual value, e.g.\n        _EncodeVarint().\n      compute_value_size:  A function which computes the size of an individual\n        value, e.g. _VarintSize().\n  \"\"\"\n\n  def SpecificEncoder(field_number, is_repeated, is_packed):\n    if is_packed:\n      tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n      local_EncodeVarint = _EncodeVarint\n      def EncodePackedField(write, value, deterministic):\n        write(tag_bytes)\n        size = 0\n        for element in value:\n          size += compute_value_size(element)\n        local_EncodeVarint(write, size, deterministic)\n        for element in value:\n          encode_value(write, element, deterministic)\n      return EncodePackedField\n    elif is_repeated:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeRepeatedField(write, value, deterministic):\n        for element in value:\n          write(tag_bytes)\n          encode_value(write, element, deterministic)\n      return EncodeRepeatedField\n    else:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeField(write, value, deterministic):\n        write(tag_bytes)\n        return encode_value(write, value, deterministic)\n      return EncodeField\n\n  return SpecificEncoder\n\n\ndef _ModifiedEncoder(wire_type, encode_value, compute_value_size, modify_value):\n  \"\"\"Like SimpleEncoder but additionally invokes modify_value on every value\n  before passing it to encode_value.  Usually modify_value is ZigZagEncode.\"\"\"\n\n  def SpecificEncoder(field_number, is_repeated, is_packed):\n    if is_packed:\n      tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n      local_EncodeVarint = _EncodeVarint\n      def EncodePackedField(write, value, deterministic):\n        write(tag_bytes)\n        size = 0\n        for element in value:\n          size += compute_value_size(modify_value(element))\n        local_EncodeVarint(write, size, deterministic)\n        for element in value:\n          encode_value(write, modify_value(element), deterministic)\n      return EncodePackedField\n    elif is_repeated:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeRepeatedField(write, value, deterministic):\n        for element in value:\n          write(tag_bytes)\n          encode_value(write, modify_value(element), deterministic)\n      return EncodeRepeatedField\n    else:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeField(write, value, deterministic):\n        write(tag_bytes)\n        return encode_value(write, modify_value(value), deterministic)\n      return EncodeField\n\n  return SpecificEncoder\n\n\ndef _StructPackEncoder(wire_type, format):\n  \"\"\"Return a constructor for an encoder for a fixed-width field.\n\n  Args:\n      wire_type:  The field's wire type, for encoding tags.\n      format:  The format string to pass to struct.pack().\n  \"\"\"\n\n  value_size = struct.calcsize(format)\n\n  def SpecificEncoder(field_number, is_repeated, is_packed):\n    local_struct_pack = struct.pack\n    if is_packed:\n      tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n      local_EncodeVarint = _EncodeVarint\n      def EncodePackedField(write, value, deterministic):\n        write(tag_bytes)\n        local_EncodeVarint(write, len(value) * value_size, deterministic)\n        for element in value:\n          write(local_struct_pack(format, element))\n      return EncodePackedField\n    elif is_repeated:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeRepeatedField(write, value, unused_deterministic=None):\n        for element in value:\n          write(tag_bytes)\n          write(local_struct_pack(format, element))\n      return EncodeRepeatedField\n    else:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeField(write, value, unused_deterministic=None):\n        write(tag_bytes)\n        return write(local_struct_pack(format, value))\n      return EncodeField\n\n  return SpecificEncoder\n\n\ndef _FloatingPointEncoder(wire_type, format):\n  \"\"\"Return a constructor for an encoder for float fields.\n\n  This is like StructPackEncoder, but catches errors that may be due to\n  passing non-finite floating-point values to struct.pack, and makes a\n  second attempt to encode those values.\n\n  Args:\n      wire_type:  The field's wire type, for encoding tags.\n      format:  The format string to pass to struct.pack().\n  \"\"\"\n\n  value_size = struct.calcsize(format)\n  if value_size == 4:\n    def EncodeNonFiniteOrRaise(write, value):\n      # Remember that the serialized form uses little-endian byte order.\n      if value == _POS_INF:\n        write(b'\\x00\\x00\\x80\\x7F')\n      elif value == _NEG_INF:\n        write(b'\\x00\\x00\\x80\\xFF')\n      elif value != value:           # NaN\n        write(b'\\x00\\x00\\xC0\\x7F')\n      else:\n        raise\n  elif value_size == 8:\n    def EncodeNonFiniteOrRaise(write, value):\n      if value == _POS_INF:\n        write(b'\\x00\\x00\\x00\\x00\\x00\\x00\\xF0\\x7F')\n      elif value == _NEG_INF:\n        write(b'\\x00\\x00\\x00\\x00\\x00\\x00\\xF0\\xFF')\n      elif value != value:                         # NaN\n        write(b'\\x00\\x00\\x00\\x00\\x00\\x00\\xF8\\x7F')\n      else:\n        raise\n  else:\n    raise ValueError('Can\\'t encode floating-point values that are '\n                     '%d bytes long (only 4 or 8)' % value_size)\n\n  def SpecificEncoder(field_number, is_repeated, is_packed):\n    local_struct_pack = struct.pack\n    if is_packed:\n      tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n      local_EncodeVarint = _EncodeVarint\n      def EncodePackedField(write, value, deterministic):\n        write(tag_bytes)\n        local_EncodeVarint(write, len(value) * value_size, deterministic)\n        for element in value:\n          # This try/except block is going to be faster than any code that\n          # we could write to check whether element is finite.\n          try:\n            write(local_struct_pack(format, element))\n          except SystemError:\n            EncodeNonFiniteOrRaise(write, element)\n      return EncodePackedField\n    elif is_repeated:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeRepeatedField(write, value, unused_deterministic=None):\n        for element in value:\n          write(tag_bytes)\n          try:\n            write(local_struct_pack(format, element))\n          except SystemError:\n            EncodeNonFiniteOrRaise(write, element)\n      return EncodeRepeatedField\n    else:\n      tag_bytes = TagBytes(field_number, wire_type)\n      def EncodeField(write, value, unused_deterministic=None):\n        write(tag_bytes)\n        try:\n          write(local_struct_pack(format, value))\n        except SystemError:\n          EncodeNonFiniteOrRaise(write, value)\n      return EncodeField\n\n  return SpecificEncoder\n\n\n# ====================================================================\n# Here we declare an encoder constructor for each field type.  These work\n# very similarly to sizer constructors, described earlier.\n\n\nInt32Encoder = Int64Encoder = EnumEncoder = _SimpleEncoder(\n    wire_format.WIRETYPE_VARINT, _EncodeSignedVarint, _SignedVarintSize)\n\nUInt32Encoder = UInt64Encoder = _SimpleEncoder(\n    wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize)\n\nSInt32Encoder = SInt64Encoder = _ModifiedEncoder(\n    wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize,\n    wire_format.ZigZagEncode)\n\n# Note that Python conveniently guarantees that when using the '<' prefix on\n# formats, they will also have the same size across all platforms (as opposed\n# to without the prefix, where their sizes depend on the C compiler's basic\n# type sizes).\nFixed32Encoder  = _StructPackEncoder(wire_format.WIRETYPE_FIXED32, '<I')\nFixed64Encoder  = _StructPackEncoder(wire_format.WIRETYPE_FIXED64, '<Q')\nSFixed32Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED32, '<i')\nSFixed64Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED64, '<q')\nFloatEncoder    = _FloatingPointEncoder(wire_format.WIRETYPE_FIXED32, '<f')\nDoubleEncoder   = _FloatingPointEncoder(wire_format.WIRETYPE_FIXED64, '<d')\n\n\ndef BoolEncoder(field_number, is_repeated, is_packed):\n  \"\"\"Returns an encoder for a boolean field.\"\"\"\n\n  false_byte = b'\\x00'\n  true_byte = b'\\x01'\n  if is_packed:\n    tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n    local_EncodeVarint = _EncodeVarint\n    def EncodePackedField(write, value, deterministic):\n      write(tag_bytes)\n      local_EncodeVarint(write, len(value), deterministic)\n      for element in value:\n        if element:\n          write(true_byte)\n        else:\n          write(false_byte)\n    return EncodePackedField\n  elif is_repeated:\n    tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_VARINT)\n    def EncodeRepeatedField(write, value, unused_deterministic=None):\n      for element in value:\n        write(tag_bytes)\n        if element:\n          write(true_byte)\n        else:\n          write(false_byte)\n    return EncodeRepeatedField\n  else:\n    tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_VARINT)\n    def EncodeField(write, value, unused_deterministic=None):\n      write(tag_bytes)\n      if value:\n        return write(true_byte)\n      return write(false_byte)\n    return EncodeField\n\n\ndef StringEncoder(field_number, is_repeated, is_packed):\n  \"\"\"Returns an encoder for a string field.\"\"\"\n\n  tag = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n  local_EncodeVarint = _EncodeVarint\n  local_len = len\n  assert not is_packed\n  if is_repeated:\n    def EncodeRepeatedField(write, value, deterministic):\n      for element in value:\n        encoded = element.encode('utf-8')\n        write(tag)\n        local_EncodeVarint(write, local_len(encoded), deterministic)\n        write(encoded)\n    return EncodeRepeatedField\n  else:\n    def EncodeField(write, value, deterministic):\n      encoded = value.encode('utf-8')\n      write(tag)\n      local_EncodeVarint(write, local_len(encoded), deterministic)\n      return write(encoded)\n    return EncodeField\n\n\ndef BytesEncoder(field_number, is_repeated, is_packed):\n  \"\"\"Returns an encoder for a bytes field.\"\"\"\n\n  tag = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n  local_EncodeVarint = _EncodeVarint\n  local_len = len\n  assert not is_packed\n  if is_repeated:\n    def EncodeRepeatedField(write, value, deterministic):\n      for element in value:\n        write(tag)\n        local_EncodeVarint(write, local_len(element), deterministic)\n        write(element)\n    return EncodeRepeatedField\n  else:\n    def EncodeField(write, value, deterministic):\n      write(tag)\n      local_EncodeVarint(write, local_len(value), deterministic)\n      return write(value)\n    return EncodeField\n\n\ndef GroupEncoder(field_number, is_repeated, is_packed):\n  \"\"\"Returns an encoder for a group field.\"\"\"\n\n  start_tag = TagBytes(field_number, wire_format.WIRETYPE_START_GROUP)\n  end_tag = TagBytes(field_number, wire_format.WIRETYPE_END_GROUP)\n  assert not is_packed\n  if is_repeated:\n    def EncodeRepeatedField(write, value, deterministic):\n      for element in value:\n        write(start_tag)\n        element._InternalSerialize(write, deterministic)\n        write(end_tag)\n    return EncodeRepeatedField\n  else:\n    def EncodeField(write, value, deterministic):\n      write(start_tag)\n      value._InternalSerialize(write, deterministic)\n      return write(end_tag)\n    return EncodeField\n\n\ndef MessageEncoder(field_number, is_repeated, is_packed):\n  \"\"\"Returns an encoder for a message field.\"\"\"\n\n  tag = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)\n  local_EncodeVarint = _EncodeVarint\n  assert not is_packed\n  if is_repeated:\n    def EncodeRepeatedField(write, value, deterministic):\n      for element in value:\n        write(tag)\n        local_EncodeVarint(write, element.ByteSize(), deterministic)\n        element._InternalSerialize(write, deterministic)\n    return EncodeRepeatedField\n  else:\n    def EncodeField(write, value, deterministic):\n      write(tag)\n      local_EncodeVarint(write, value.ByteSize(), deterministic)\n      return value._InternalSerialize(write, deterministic)\n    return EncodeField\n\n\n# --------------------------------------------------------------------\n# As before, MessageSet is special.\n\n\ndef MessageSetItemEncoder(field_number):\n  \"\"\"Encoder for extensions of MessageSet.\n\n  The message set message looks like this:\n    message MessageSet {\n      repeated group Item = 1 {\n        required int32 type_id = 2;\n        required string message = 3;\n      }\n    }\n  \"\"\"\n  start_bytes = b\"\".join([\n      TagBytes(1, wire_format.WIRETYPE_START_GROUP),\n      TagBytes(2, wire_format.WIRETYPE_VARINT),\n      _VarintBytes(field_number),\n      TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED)])\n  end_bytes = TagBytes(1, wire_format.WIRETYPE_END_GROUP)\n  local_EncodeVarint = _EncodeVarint\n\n  def EncodeField(write, value, deterministic):\n    write(start_bytes)\n    local_EncodeVarint(write, value.ByteSize(), deterministic)\n    value._InternalSerialize(write, deterministic)\n    return write(end_bytes)\n\n  return EncodeField\n\n\n# --------------------------------------------------------------------\n# As before, Map is special.\n\n\ndef MapEncoder(field_descriptor):\n  \"\"\"Encoder for extensions of MessageSet.\n\n  Maps always have a wire format like this:\n    message MapEntry {\n      key_type key = 1;\n      value_type value = 2;\n    }\n    repeated MapEntry map = N;\n  \"\"\"\n  # Can't look at field_descriptor.message_type._concrete_class because it may\n  # not have been initialized yet.\n  message_type = field_descriptor.message_type\n  encode_message = MessageEncoder(field_descriptor.number, False, False)\n\n  def EncodeField(write, value, deterministic):\n    value_keys = sorted(value.keys()) if deterministic else value\n    for key in value_keys:\n      entry_msg = message_type._concrete_class(key=key, value=value[key])\n      encode_message(write, entry_msg, deterministic)\n\n  return EncodeField\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/enum_type_wrapper.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"A simple wrapper around enum types to expose utility functions.\n\nInstances are created as properties with the same name as the enum they wrap\non proto classes.  For usage, see:\n  reflection_test.py\n\"\"\"\n\n__author__ = 'rabsatt@google.com (Kevin Rabsatt)'\n\n\nclass EnumTypeWrapper(object):\n  \"\"\"A utility for finding the names of enum values.\"\"\"\n\n  DESCRIPTOR = None\n\n  # This is a type alias, which mypy typing stubs can type as\n  # a genericized parameter constrained to an int, allowing subclasses\n  # to be typed with more constraint in .pyi stubs\n  # Eg.\n  # def MyGeneratedEnum(Message):\n  #   ValueType = NewType('ValueType', int)\n  #   def Name(self, number: MyGeneratedEnum.ValueType) -> str\n  ValueType = int\n\n  def __init__(self, enum_type):\n    \"\"\"Inits EnumTypeWrapper with an EnumDescriptor.\"\"\"\n    self._enum_type = enum_type\n    self.DESCRIPTOR = enum_type  # pylint: disable=invalid-name\n\n  def Name(self, number):  # pylint: disable=invalid-name\n    \"\"\"Returns a string containing the name of an enum value.\"\"\"\n    try:\n      return self._enum_type.values_by_number[number].name\n    except KeyError:\n      pass  # fall out to break exception chaining\n\n    if not isinstance(number, int):\n      raise TypeError(\n          'Enum value for {} must be an int, but got {} {!r}.'.format(\n              self._enum_type.name, type(number), number))\n    else:\n      # repr here to handle the odd case when you pass in a boolean.\n      raise ValueError('Enum {} has no name defined for value {!r}'.format(\n          self._enum_type.name, number))\n\n  def Value(self, name):  # pylint: disable=invalid-name\n    \"\"\"Returns the value corresponding to the given enum name.\"\"\"\n    try:\n      return self._enum_type.values_by_name[name].number\n    except KeyError:\n      pass  # fall out to break exception chaining\n    raise ValueError('Enum {} has no value defined for name {!r}'.format(\n        self._enum_type.name, name))\n\n  def keys(self):\n    \"\"\"Return a list of the string names in the enum.\n\n    Returns:\n      A list of strs, in the order they were defined in the .proto file.\n    \"\"\"\n\n    return [value_descriptor.name\n            for value_descriptor in self._enum_type.values]\n\n  def values(self):\n    \"\"\"Return a list of the integer values in the enum.\n\n    Returns:\n      A list of ints, in the order they were defined in the .proto file.\n    \"\"\"\n\n    return [value_descriptor.number\n            for value_descriptor in self._enum_type.values]\n\n  def items(self):\n    \"\"\"Return a list of the (name, value) pairs of the enum.\n\n    Returns:\n      A list of (str, int) pairs, in the order they were defined\n      in the .proto file.\n    \"\"\"\n    return [(value_descriptor.name, value_descriptor.number)\n            for value_descriptor in self._enum_type.values]\n\n  def __getattr__(self, name):\n    \"\"\"Returns the value corresponding to the given enum name.\"\"\"\n    try:\n      return super(\n          EnumTypeWrapper,\n          self).__getattribute__('_enum_type').values_by_name[name].number\n    except KeyError:\n      pass  # fall out to break exception chaining\n    raise AttributeError('Enum {} has no value defined for name {!r}'.format(\n        self._enum_type.name, name))\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/extension_dict.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Contains _ExtensionDict class to represent extensions.\n\"\"\"\n\nfrom google.protobuf.internal import type_checkers\nfrom google.protobuf.descriptor import FieldDescriptor\n\n\ndef _VerifyExtensionHandle(message, extension_handle):\n  \"\"\"Verify that the given extension handle is valid.\"\"\"\n\n  if not isinstance(extension_handle, FieldDescriptor):\n    raise KeyError('HasExtension() expects an extension handle, got: %s' %\n                   extension_handle)\n\n  if not extension_handle.is_extension:\n    raise KeyError('\"%s\" is not an extension.' % extension_handle.full_name)\n\n  if not extension_handle.containing_type:\n    raise KeyError('\"%s\" is missing a containing_type.'\n                   % extension_handle.full_name)\n\n  if extension_handle.containing_type is not message.DESCRIPTOR:\n    raise KeyError('Extension \"%s\" extends message type \"%s\", but this '\n                   'message is of type \"%s\".' %\n                   (extension_handle.full_name,\n                    extension_handle.containing_type.full_name,\n                    message.DESCRIPTOR.full_name))\n\n\n# TODO(robinson): Unify error handling of \"unknown extension\" crap.\n# TODO(robinson): Support iteritems()-style iteration over all\n# extensions with the \"has\" bits turned on?\nclass _ExtensionDict(object):\n\n  \"\"\"Dict-like container for Extension fields on proto instances.\n\n  Note that in all cases we expect extension handles to be\n  FieldDescriptors.\n  \"\"\"\n\n  def __init__(self, extended_message):\n    \"\"\"\n    Args:\n      extended_message: Message instance for which we are the Extensions dict.\n    \"\"\"\n    self._extended_message = extended_message\n\n  def __getitem__(self, extension_handle):\n    \"\"\"Returns the current value of the given extension handle.\"\"\"\n\n    _VerifyExtensionHandle(self._extended_message, extension_handle)\n\n    result = self._extended_message._fields.get(extension_handle)\n    if result is not None:\n      return result\n\n    if extension_handle.label == FieldDescriptor.LABEL_REPEATED:\n      result = extension_handle._default_constructor(self._extended_message)\n    elif extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE:\n      message_type = extension_handle.message_type\n      if not hasattr(message_type, '_concrete_class'):\n        # pylint: disable=protected-access\n        self._extended_message._FACTORY.GetPrototype(message_type)\n      assert getattr(extension_handle.message_type, '_concrete_class', None), (\n          'Uninitialized concrete class found for field %r (message type %r)'\n          % (extension_handle.full_name,\n             extension_handle.message_type.full_name))\n      result = extension_handle.message_type._concrete_class()\n      try:\n        result._SetListener(self._extended_message._listener_for_children)\n      except ReferenceError:\n        pass\n    else:\n      # Singular scalar -- just return the default without inserting into the\n      # dict.\n      return extension_handle.default_value\n\n    # Atomically check if another thread has preempted us and, if not, swap\n    # in the new object we just created.  If someone has preempted us, we\n    # take that object and discard ours.\n    # WARNING:  We are relying on setdefault() being atomic.  This is true\n    #   in CPython but we haven't investigated others.  This warning appears\n    #   in several other locations in this file.\n    result = self._extended_message._fields.setdefault(\n        extension_handle, result)\n\n    return result\n\n  def __eq__(self, other):\n    if not isinstance(other, self.__class__):\n      return False\n\n    my_fields = self._extended_message.ListFields()\n    other_fields = other._extended_message.ListFields()\n\n    # Get rid of non-extension fields.\n    my_fields = [field for field in my_fields if field.is_extension]\n    other_fields = [field for field in other_fields if field.is_extension]\n\n    return my_fields == other_fields\n\n  def __ne__(self, other):\n    return not self == other\n\n  def __len__(self):\n    fields = self._extended_message.ListFields()\n    # Get rid of non-extension fields.\n    extension_fields = [field for field in fields if field[0].is_extension]\n    return len(extension_fields)\n\n  def __hash__(self):\n    raise TypeError('unhashable object')\n\n  # Note that this is only meaningful for non-repeated, scalar extension\n  # fields.  Note also that we may have to call _Modified() when we do\n  # successfully set a field this way, to set any necessary \"has\" bits in the\n  # ancestors of the extended message.\n  def __setitem__(self, extension_handle, value):\n    \"\"\"If extension_handle specifies a non-repeated, scalar extension\n    field, sets the value of that field.\n    \"\"\"\n\n    _VerifyExtensionHandle(self._extended_message, extension_handle)\n\n    if (extension_handle.label == FieldDescriptor.LABEL_REPEATED or\n        extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE):\n      raise TypeError(\n          'Cannot assign to extension \"%s\" because it is a repeated or '\n          'composite type.' % extension_handle.full_name)\n\n    # It's slightly wasteful to lookup the type checker each time,\n    # but we expect this to be a vanishingly uncommon case anyway.\n    type_checker = type_checkers.GetTypeChecker(extension_handle)\n    # pylint: disable=protected-access\n    self._extended_message._fields[extension_handle] = (\n        type_checker.CheckValue(value))\n    self._extended_message._Modified()\n\n  def __delitem__(self, extension_handle):\n    self._extended_message.ClearExtension(extension_handle)\n\n  def _FindExtensionByName(self, name):\n    \"\"\"Tries to find a known extension with the specified name.\n\n    Args:\n      name: Extension full name.\n\n    Returns:\n      Extension field descriptor.\n    \"\"\"\n    return self._extended_message._extensions_by_name.get(name, None)\n\n  def _FindExtensionByNumber(self, number):\n    \"\"\"Tries to find a known extension with the field number.\n\n    Args:\n      number: Extension field number.\n\n    Returns:\n      Extension field descriptor.\n    \"\"\"\n    return self._extended_message._extensions_by_number.get(number, None)\n\n  def __iter__(self):\n    # Return a generator over the populated extension fields\n    return (f[0] for f in self._extended_message.ListFields()\n            if f[0].is_extension)\n\n  def __contains__(self, extension_handle):\n    _VerifyExtensionHandle(self._extended_message, extension_handle)\n\n    if extension_handle not in self._extended_message._fields:\n      return False\n\n    if extension_handle.label == FieldDescriptor.LABEL_REPEATED:\n      return bool(self._extended_message._fields.get(extension_handle))\n\n    if extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE:\n      value = self._extended_message._fields.get(extension_handle)\n      # pylint: disable=protected-access\n      return value is not None and value._is_present_in_parent\n\n    return True\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/message_listener.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Defines a listener interface for observing certain\nstate transitions on Message objects.\n\nAlso defines a null implementation of this interface.\n\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\n\nclass MessageListener(object):\n\n  \"\"\"Listens for modifications made to a message.  Meant to be registered via\n  Message._SetListener().\n\n  Attributes:\n    dirty:  If True, then calling Modified() would be a no-op.  This can be\n            used to avoid these calls entirely in the common case.\n  \"\"\"\n\n  def Modified(self):\n    \"\"\"Called every time the message is modified in such a way that the parent\n    message may need to be updated.  This currently means either:\n    (a) The message was modified for the first time, so the parent message\n        should henceforth mark the message as present.\n    (b) The message's cached byte size became dirty -- i.e. the message was\n        modified for the first time after a previous call to ByteSize().\n        Therefore the parent should also mark its byte size as dirty.\n    Note that (a) implies (b), since new objects start out with a client cached\n    size (zero).  However, we document (a) explicitly because it is important.\n\n    Modified() will *only* be called in response to one of these two events --\n    not every time the sub-message is modified.\n\n    Note that if the listener's |dirty| attribute is true, then calling\n    Modified at the moment would be a no-op, so it can be skipped.  Performance-\n    sensitive callers should check this attribute directly before calling since\n    it will be true most of the time.\n    \"\"\"\n\n    raise NotImplementedError\n\n\nclass NullMessageListener(object):\n\n  \"\"\"No-op MessageListener implementation.\"\"\"\n\n  def Modified(self):\n    pass\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/message_set_extensions_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/internal/message_set_extensions.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n5google/protobuf/internal/message_set_extensions.proto\\x12\\x18google.protobuf.internal\\\"\\x1e\\n\\x0eTestMessageSet*\\x08\\x08\\x04\\x10\\xff\\xff\\xff\\xff\\x07:\\x02\\x08\\x01\\\"\\xa5\\x01\\n\\x18TestMessageSetExtension1\\x12\\t\\n\\x01i\\x18\\x0f \\x01(\\x05\\x32~\\n\\x15message_set_extension\\x12(.google.protobuf.internal.TestMessageSet\\x18\\xab\\xff\\xf6. \\x01(\\x0b\\x32\\x32.google.protobuf.internal.TestMessageSetExtension1\\\"\\xa7\\x01\\n\\x18TestMessageSetExtension2\\x12\\x0b\\n\\x03str\\x18\\x19 \\x01(\\t2~\\n\\x15message_set_extension\\x12(.google.protobuf.internal.TestMessageSet\\x18\\xca\\xff\\xf6. \\x01(\\x0b\\x32\\x32.google.protobuf.internal.TestMessageSetExtension2\\\"(\\n\\x18TestMessageSetExtension3\\x12\\x0c\\n\\x04text\\x18# \\x01(\\t:\\x7f\\n\\x16message_set_extension3\\x12(.google.protobuf.internal.TestMessageSet\\x18\\xdf\\xff\\xf6. \\x01(\\x0b\\x32\\x32.google.protobuf.internal.TestMessageSetExtension3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.message_set_extensions_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n  TestMessageSet.RegisterExtension(message_set_extension3)\n  TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION1.extensions_by_name['message_set_extension'])\n  TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION2.extensions_by_name['message_set_extension'])\n\n  DESCRIPTOR._options = None\n  _TESTMESSAGESET._options = None\n  _TESTMESSAGESET._serialized_options = b'\\010\\001'\n  _TESTMESSAGESET._serialized_start=83\n  _TESTMESSAGESET._serialized_end=113\n  _TESTMESSAGESETEXTENSION1._serialized_start=116\n  _TESTMESSAGESETEXTENSION1._serialized_end=281\n  _TESTMESSAGESETEXTENSION2._serialized_start=284\n  _TESTMESSAGESETEXTENSION2._serialized_end=451\n  _TESTMESSAGESETEXTENSION3._serialized_start=453\n  _TESTMESSAGESETEXTENSION3._serialized_end=493\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/missing_enum_values_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/internal/missing_enum_values.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n2google/protobuf/internal/missing_enum_values.proto\\x12\\x1fgoogle.protobuf.python.internal\\\"\\xc1\\x02\\n\\x0eTestEnumValues\\x12X\\n\\x14optional_nested_enum\\x18\\x01 \\x01(\\x0e\\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\\x12X\\n\\x14repeated_nested_enum\\x18\\x02 \\x03(\\x0e\\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\\x12Z\\n\\x12packed_nested_enum\\x18\\x03 \\x03(\\x0e\\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnumB\\x02\\x10\\x01\\\"\\x1f\\n\\nNestedEnum\\x12\\x08\\n\\x04ZERO\\x10\\x00\\x12\\x07\\n\\x03ONE\\x10\\x01\\\"\\xd3\\x02\\n\\x15TestMissingEnumValues\\x12_\\n\\x14optional_nested_enum\\x18\\x01 \\x01(\\x0e\\x32\\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\\x12_\\n\\x14repeated_nested_enum\\x18\\x02 \\x03(\\x0e\\x32\\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\\x12\\x61\\n\\x12packed_nested_enum\\x18\\x03 \\x03(\\x0e\\x32\\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnumB\\x02\\x10\\x01\\\"\\x15\\n\\nNestedEnum\\x12\\x07\\n\\x03TWO\\x10\\x02\\\"\\x1b\\n\\nJustString\\x12\\r\\n\\x05\\x64ummy\\x18\\x01 \\x02(\\t')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.missing_enum_values_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  _TESTENUMVALUES.fields_by_name['packed_nested_enum']._options = None\n  _TESTENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\\020\\001'\n  _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._options = None\n  _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\\020\\001'\n  _TESTENUMVALUES._serialized_start=88\n  _TESTENUMVALUES._serialized_end=409\n  _TESTENUMVALUES_NESTEDENUM._serialized_start=378\n  _TESTENUMVALUES_NESTEDENUM._serialized_end=409\n  _TESTMISSINGENUMVALUES._serialized_start=412\n  _TESTMISSINGENUMVALUES._serialized_end=751\n  _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_start=730\n  _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_end=751\n  _JUSTSTRING._serialized_start=753\n  _JUSTSTRING._serialized_end=780\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/internal/more_extensions_dynamic.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom google.protobuf.internal import more_extensions_pb2 as google_dot_protobuf_dot_internal_dot_more__extensions__pb2\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n6google/protobuf/internal/more_extensions_dynamic.proto\\x12\\x18google.protobuf.internal\\x1a.google/protobuf/internal/more_extensions.proto\\\"\\x1f\\n\\x12\\x44ynamicMessageType\\x12\\t\\n\\x01\\x61\\x18\\x01 \\x01(\\x05:J\\n\\x17\\x64ynamic_int32_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x64 \\x01(\\x05:z\\n\\x19\\x64ynamic_message_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x65 \\x01(\\x0b\\x32,.google.protobuf.internal.DynamicMessageType:\\x83\\x01\\n\\\"repeated_dynamic_message_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x66 \\x03(\\x0b\\x32,.google.protobuf.internal.DynamicMessageType')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_dynamic_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n  google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_int32_extension)\n  google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_message_extension)\n  google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(repeated_dynamic_message_extension)\n\n  DESCRIPTOR._options = None\n  _DYNAMICMESSAGETYPE._serialized_start=132\n  _DYNAMICMESSAGETYPE._serialized_end=163\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/more_extensions_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/internal/more_extensions.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n.google/protobuf/internal/more_extensions.proto\\x12\\x18google.protobuf.internal\\\"\\x99\\x01\\n\\x0fTopLevelMessage\\x12\\x41\\n\\nsubmessage\\x18\\x01 \\x01(\\x0b\\x32).google.protobuf.internal.ExtendedMessageB\\x02(\\x01\\x12\\x43\\n\\x0enested_message\\x18\\x02 \\x01(\\x0b\\x32\\'.google.protobuf.internal.NestedMessageB\\x02(\\x01\\\"R\\n\\rNestedMessage\\x12\\x41\\n\\nsubmessage\\x18\\x01 \\x01(\\x0b\\x32).google.protobuf.internal.ExtendedMessageB\\x02(\\x01\\\"K\\n\\x0f\\x45xtendedMessage\\x12\\x17\\n\\x0eoptional_int32\\x18\\xe9\\x07 \\x01(\\x05\\x12\\x18\\n\\x0frepeated_string\\x18\\xea\\x07 \\x03(\\t*\\x05\\x08\\x01\\x10\\xe8\\x07\\\"-\\n\\x0e\\x46oreignMessage\\x12\\x1b\\n\\x13\\x66oreign_message_int\\x18\\x01 \\x01(\\x05:I\\n\\x16optional_int_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x01 \\x01(\\x05:w\\n\\x1aoptional_message_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x02 \\x01(\\x0b\\x32(.google.protobuf.internal.ForeignMessage:I\\n\\x16repeated_int_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x03 \\x03(\\x05:w\\n\\x1arepeated_message_extension\\x12).google.protobuf.internal.ExtendedMessage\\x18\\x04 \\x03(\\x0b\\x32(.google.protobuf.internal.ForeignMessage')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n  ExtendedMessage.RegisterExtension(optional_int_extension)\n  ExtendedMessage.RegisterExtension(optional_message_extension)\n  ExtendedMessage.RegisterExtension(repeated_int_extension)\n  ExtendedMessage.RegisterExtension(repeated_message_extension)\n\n  DESCRIPTOR._options = None\n  _TOPLEVELMESSAGE.fields_by_name['submessage']._options = None\n  _TOPLEVELMESSAGE.fields_by_name['submessage']._serialized_options = b'(\\001'\n  _TOPLEVELMESSAGE.fields_by_name['nested_message']._options = None\n  _TOPLEVELMESSAGE.fields_by_name['nested_message']._serialized_options = b'(\\001'\n  _NESTEDMESSAGE.fields_by_name['submessage']._options = None\n  _NESTEDMESSAGE.fields_by_name['submessage']._serialized_options = b'(\\001'\n  _TOPLEVELMESSAGE._serialized_start=77\n  _TOPLEVELMESSAGE._serialized_end=230\n  _NESTEDMESSAGE._serialized_start=232\n  _NESTEDMESSAGE._serialized_end=314\n  _EXTENDEDMESSAGE._serialized_start=316\n  _EXTENDEDMESSAGE._serialized_end=391\n  _FOREIGNMESSAGE._serialized_start=393\n  _FOREIGNMESSAGE._serialized_end=438\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/more_messages_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/internal/more_messages.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n,google/protobuf/internal/more_messages.proto\\x12\\x18google.protobuf.internal\\\"h\\n\\x10OutOfOrderFields\\x12\\x17\\n\\x0foptional_sint32\\x18\\x05 \\x01(\\x11\\x12\\x17\\n\\x0foptional_uint32\\x18\\x03 \\x01(\\r\\x12\\x16\\n\\x0eoptional_int32\\x18\\x01 \\x01(\\x05*\\x04\\x08\\x04\\x10\\x05*\\x04\\x08\\x02\\x10\\x03\\\"\\xcd\\x02\\n\\x05\\x63lass\\x12\\x1b\\n\\tint_field\\x18\\x01 \\x01(\\x05R\\x08json_int\\x12\\n\\n\\x02if\\x18\\x02 \\x01(\\x05\\x12(\\n\\x02\\x61s\\x18\\x03 \\x01(\\x0e\\x32\\x1c.google.protobuf.internal.is\\x12\\x30\\n\\nenum_field\\x18\\x04 \\x01(\\x0e\\x32\\x1c.google.protobuf.internal.is\\x12>\\n\\x11nested_enum_field\\x18\\x05 \\x01(\\x0e\\x32#.google.protobuf.internal.class.for\\x12;\\n\\x0enested_message\\x18\\x06 \\x01(\\x0b\\x32#.google.protobuf.internal.class.try\\x1a\\x1c\\n\\x03try\\x12\\r\\n\\x05\\x66ield\\x18\\x01 \\x01(\\x05*\\x06\\x08\\xe7\\x07\\x10\\x90N\\\"\\x1c\\n\\x03\\x66or\\x12\\x0b\\n\\x07\\x64\\x65\\x66\\x61ult\\x10\\x00\\x12\\x08\\n\\x04True\\x10\\x01*\\x06\\x08\\xe7\\x07\\x10\\x90N\\\"?\\n\\x0b\\x45xtendClass20\\n\\x06return\\x12\\x1f.google.protobuf.internal.class\\x18\\xea\\x07 \\x01(\\x05\\\"~\\n\\x0fTestFullKeyword\\x12:\\n\\x06\\x66ield1\\x18\\x01 \\x01(\\x0b\\x32*.google.protobuf.internal.OutOfOrderFields\\x12/\\n\\x06\\x66ield2\\x18\\x02 \\x01(\\x0b\\x32\\x1f.google.protobuf.internal.class\\\"\\xa5\\x0f\\n\\x11LotsNestedMessage\\x1a\\x04\\n\\x02\\x42\\x30\\x1a\\x04\\n\\x02\\x42\\x31\\x1a\\x04\\n\\x02\\x42\\x32\\x1a\\x04\\n\\x02\\x42\\x33\\x1a\\x04\\n\\x02\\x42\\x34\\x1a\\x04\\n\\x02\\x42\\x35\\x1a\\x04\\n\\x02\\x42\\x36\\x1a\\x04\\n\\x02\\x42\\x37\\x1a\\x04\\n\\x02\\x42\\x38\\x1a\\x04\\n\\x02\\x42\\x39\\x1a\\x05\\n\\x03\\x42\\x31\\x30\\x1a\\x05\\n\\x03\\x42\\x31\\x31\\x1a\\x05\\n\\x03\\x42\\x31\\x32\\x1a\\x05\\n\\x03\\x42\\x31\\x33\\x1a\\x05\\n\\x03\\x42\\x31\\x34\\x1a\\x05\\n\\x03\\x42\\x31\\x35\\x1a\\x05\\n\\x03\\x42\\x31\\x36\\x1a\\x05\\n\\x03\\x42\\x31\\x37\\x1a\\x05\\n\\x03\\x42\\x31\\x38\\x1a\\x05\\n\\x03\\x42\\x31\\x39\\x1a\\x05\\n\\x03\\x42\\x32\\x30\\x1a\\x05\\n\\x03\\x42\\x32\\x31\\x1a\\x05\\n\\x03\\x42\\x32\\x32\\x1a\\x05\\n\\x03\\x42\\x32\\x33\\x1a\\x05\\n\\x03\\x42\\x32\\x34\\x1a\\x05\\n\\x03\\x42\\x32\\x35\\x1a\\x05\\n\\x03\\x42\\x32\\x36\\x1a\\x05\\n\\x03\\x42\\x32\\x37\\x1a\\x05\\n\\x03\\x42\\x32\\x38\\x1a\\x05\\n\\x03\\x42\\x32\\x39\\x1a\\x05\\n\\x03\\x42\\x33\\x30\\x1a\\x05\\n\\x03\\x42\\x33\\x31\\x1a\\x05\\n\\x03\\x42\\x33\\x32\\x1a\\x05\\n\\x03\\x42\\x33\\x33\\x1a\\x05\\n\\x03\\x42\\x33\\x34\\x1a\\x05\\n\\x03\\x42\\x33\\x35\\x1a\\x05\\n\\x03\\x42\\x33\\x36\\x1a\\x05\\n\\x03\\x42\\x33\\x37\\x1a\\x05\\n\\x03\\x42\\x33\\x38\\x1a\\x05\\n\\x03\\x42\\x33\\x39\\x1a\\x05\\n\\x03\\x42\\x34\\x30\\x1a\\x05\\n\\x03\\x42\\x34\\x31\\x1a\\x05\\n\\x03\\x42\\x34\\x32\\x1a\\x05\\n\\x03\\x42\\x34\\x33\\x1a\\x05\\n\\x03\\x42\\x34\\x34\\x1a\\x05\\n\\x03\\x42\\x34\\x35\\x1a\\x05\\n\\x03\\x42\\x34\\x36\\x1a\\x05\\n\\x03\\x42\\x34\\x37\\x1a\\x05\\n\\x03\\x42\\x34\\x38\\x1a\\x05\\n\\x03\\x42\\x34\\x39\\x1a\\x05\\n\\x03\\x42\\x35\\x30\\x1a\\x05\\n\\x03\\x42\\x35\\x31\\x1a\\x05\\n\\x03\\x42\\x35\\x32\\x1a\\x05\\n\\x03\\x42\\x35\\x33\\x1a\\x05\\n\\x03\\x42\\x35\\x34\\x1a\\x05\\n\\x03\\x42\\x35\\x35\\x1a\\x05\\n\\x03\\x42\\x35\\x36\\x1a\\x05\\n\\x03\\x42\\x35\\x37\\x1a\\x05\\n\\x03\\x42\\x35\\x38\\x1a\\x05\\n\\x03\\x42\\x35\\x39\\x1a\\x05\\n\\x03\\x42\\x36\\x30\\x1a\\x05\\n\\x03\\x42\\x36\\x31\\x1a\\x05\\n\\x03\\x42\\x36\\x32\\x1a\\x05\\n\\x03\\x42\\x36\\x33\\x1a\\x05\\n\\x03\\x42\\x36\\x34\\x1a\\x05\\n\\x03\\x42\\x36\\x35\\x1a\\x05\\n\\x03\\x42\\x36\\x36\\x1a\\x05\\n\\x03\\x42\\x36\\x37\\x1a\\x05\\n\\x03\\x42\\x36\\x38\\x1a\\x05\\n\\x03\\x42\\x36\\x39\\x1a\\x05\\n\\x03\\x42\\x37\\x30\\x1a\\x05\\n\\x03\\x42\\x37\\x31\\x1a\\x05\\n\\x03\\x42\\x37\\x32\\x1a\\x05\\n\\x03\\x42\\x37\\x33\\x1a\\x05\\n\\x03\\x42\\x37\\x34\\x1a\\x05\\n\\x03\\x42\\x37\\x35\\x1a\\x05\\n\\x03\\x42\\x37\\x36\\x1a\\x05\\n\\x03\\x42\\x37\\x37\\x1a\\x05\\n\\x03\\x42\\x37\\x38\\x1a\\x05\\n\\x03\\x42\\x37\\x39\\x1a\\x05\\n\\x03\\x42\\x38\\x30\\x1a\\x05\\n\\x03\\x42\\x38\\x31\\x1a\\x05\\n\\x03\\x42\\x38\\x32\\x1a\\x05\\n\\x03\\x42\\x38\\x33\\x1a\\x05\\n\\x03\\x42\\x38\\x34\\x1a\\x05\\n\\x03\\x42\\x38\\x35\\x1a\\x05\\n\\x03\\x42\\x38\\x36\\x1a\\x05\\n\\x03\\x42\\x38\\x37\\x1a\\x05\\n\\x03\\x42\\x38\\x38\\x1a\\x05\\n\\x03\\x42\\x38\\x39\\x1a\\x05\\n\\x03\\x42\\x39\\x30\\x1a\\x05\\n\\x03\\x42\\x39\\x31\\x1a\\x05\\n\\x03\\x42\\x39\\x32\\x1a\\x05\\n\\x03\\x42\\x39\\x33\\x1a\\x05\\n\\x03\\x42\\x39\\x34\\x1a\\x05\\n\\x03\\x42\\x39\\x35\\x1a\\x05\\n\\x03\\x42\\x39\\x36\\x1a\\x05\\n\\x03\\x42\\x39\\x37\\x1a\\x05\\n\\x03\\x42\\x39\\x38\\x1a\\x05\\n\\x03\\x42\\x39\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x30\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x31\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x32\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x33\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x34\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x35\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x36\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x37\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x38\\x39\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x30\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x31\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x32\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x33\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x34\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x35\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x36\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x37\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x38\\x1a\\x06\\n\\x04\\x42\\x31\\x39\\x39\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x30\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x31\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x32\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x33\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x34\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x35\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x36\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x37\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x38\\x1a\\x06\\n\\x04\\x42\\x32\\x30\\x39\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x30\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x31\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x32\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x33\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x34\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x35\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x36\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x37\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x38\\x1a\\x06\\n\\x04\\x42\\x32\\x31\\x39\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x30\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x31\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x32\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x33\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x34\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x35\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x36\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x37\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x38\\x1a\\x06\\n\\x04\\x42\\x32\\x32\\x39\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x30\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x31\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x32\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x33\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x34\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x35\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x36\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x37\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x38\\x1a\\x06\\n\\x04\\x42\\x32\\x33\\x39\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x30\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x31\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x32\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x33\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x34\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x35\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x36\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x37\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x38\\x1a\\x06\\n\\x04\\x42\\x32\\x34\\x39\\x1a\\x06\\n\\x04\\x42\\x32\\x35\\x30\\x1a\\x06\\n\\x04\\x42\\x32\\x35\\x31\\x1a\\x06\\n\\x04\\x42\\x32\\x35\\x32\\x1a\\x06\\n\\x04\\x42\\x32\\x35\\x33\\x1a\\x06\\n\\x04\\x42\\x32\\x35\\x34\\x1a\\x06\\n\\x04\\x42\\x32\\x35\\x35*\\x1b\\n\\x02is\\x12\\x0b\\n\\x07\\x64\\x65\\x66\\x61ult\\x10\\x00\\x12\\x08\\n\\x04\\x65lse\\x10\\x01:C\\n\\x0foptional_uint64\\x12*.google.protobuf.internal.OutOfOrderFields\\x18\\x04 \\x01(\\x04:B\\n\\x0eoptional_int64\\x12*.google.protobuf.internal.OutOfOrderFields\\x18\\x02 \\x01(\\x03:2\\n\\x08\\x63ontinue\\x12\\x1f.google.protobuf.internal.class\\x18\\xe9\\x07 \\x01(\\x05:2\\n\\x04with\\x12#.google.protobuf.internal.class.try\\x18\\xe9\\x07 \\x01(\\x05')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_messages_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n  OutOfOrderFields.RegisterExtension(optional_uint64)\n  OutOfOrderFields.RegisterExtension(optional_int64)\n  globals()['class'].RegisterExtension(globals()['continue'])\n  getattr(globals()['class'], 'try').RegisterExtension(globals()['with'])\n  globals()['class'].RegisterExtension(_EXTENDCLASS.extensions_by_name['return'])\n\n  DESCRIPTOR._options = None\n  _IS._serialized_start=2669\n  _IS._serialized_end=2696\n  _OUTOFORDERFIELDS._serialized_start=74\n  _OUTOFORDERFIELDS._serialized_end=178\n  _CLASS._serialized_start=181\n  _CLASS._serialized_end=514\n  _CLASS_TRY._serialized_start=448\n  _CLASS_TRY._serialized_end=476\n  _CLASS_FOR._serialized_start=478\n  _CLASS_FOR._serialized_end=506\n  _EXTENDCLASS._serialized_start=516\n  _EXTENDCLASS._serialized_end=579\n  _TESTFULLKEYWORD._serialized_start=581\n  _TESTFULLKEYWORD._serialized_end=707\n  _LOTSNESTEDMESSAGE._serialized_start=710\n  _LOTSNESTEDMESSAGE._serialized_end=2667\n  _LOTSNESTEDMESSAGE_B0._serialized_start=731\n  _LOTSNESTEDMESSAGE_B0._serialized_end=735\n  _LOTSNESTEDMESSAGE_B1._serialized_start=737\n  _LOTSNESTEDMESSAGE_B1._serialized_end=741\n  _LOTSNESTEDMESSAGE_B2._serialized_start=743\n  _LOTSNESTEDMESSAGE_B2._serialized_end=747\n  _LOTSNESTEDMESSAGE_B3._serialized_start=749\n  _LOTSNESTEDMESSAGE_B3._serialized_end=753\n  _LOTSNESTEDMESSAGE_B4._serialized_start=755\n  _LOTSNESTEDMESSAGE_B4._serialized_end=759\n  _LOTSNESTEDMESSAGE_B5._serialized_start=761\n  _LOTSNESTEDMESSAGE_B5._serialized_end=765\n  _LOTSNESTEDMESSAGE_B6._serialized_start=767\n  _LOTSNESTEDMESSAGE_B6._serialized_end=771\n  _LOTSNESTEDMESSAGE_B7._serialized_start=773\n  _LOTSNESTEDMESSAGE_B7._serialized_end=777\n  _LOTSNESTEDMESSAGE_B8._serialized_start=779\n  _LOTSNESTEDMESSAGE_B8._serialized_end=783\n  _LOTSNESTEDMESSAGE_B9._serialized_start=785\n  _LOTSNESTEDMESSAGE_B9._serialized_end=789\n  _LOTSNESTEDMESSAGE_B10._serialized_start=791\n  _LOTSNESTEDMESSAGE_B10._serialized_end=796\n  _LOTSNESTEDMESSAGE_B11._serialized_start=798\n  _LOTSNESTEDMESSAGE_B11._serialized_end=803\n  _LOTSNESTEDMESSAGE_B12._serialized_start=805\n  _LOTSNESTEDMESSAGE_B12._serialized_end=810\n  _LOTSNESTEDMESSAGE_B13._serialized_start=812\n  _LOTSNESTEDMESSAGE_B13._serialized_end=817\n  _LOTSNESTEDMESSAGE_B14._serialized_start=819\n  _LOTSNESTEDMESSAGE_B14._serialized_end=824\n  _LOTSNESTEDMESSAGE_B15._serialized_start=826\n  _LOTSNESTEDMESSAGE_B15._serialized_end=831\n  _LOTSNESTEDMESSAGE_B16._serialized_start=833\n  _LOTSNESTEDMESSAGE_B16._serialized_end=838\n  _LOTSNESTEDMESSAGE_B17._serialized_start=840\n  _LOTSNESTEDMESSAGE_B17._serialized_end=845\n  _LOTSNESTEDMESSAGE_B18._serialized_start=847\n  _LOTSNESTEDMESSAGE_B18._serialized_end=852\n  _LOTSNESTEDMESSAGE_B19._serialized_start=854\n  _LOTSNESTEDMESSAGE_B19._serialized_end=859\n  _LOTSNESTEDMESSAGE_B20._serialized_start=861\n  _LOTSNESTEDMESSAGE_B20._serialized_end=866\n  _LOTSNESTEDMESSAGE_B21._serialized_start=868\n  _LOTSNESTEDMESSAGE_B21._serialized_end=873\n  _LOTSNESTEDMESSAGE_B22._serialized_start=875\n  _LOTSNESTEDMESSAGE_B22._serialized_end=880\n  _LOTSNESTEDMESSAGE_B23._serialized_start=882\n  _LOTSNESTEDMESSAGE_B23._serialized_end=887\n  _LOTSNESTEDMESSAGE_B24._serialized_start=889\n  _LOTSNESTEDMESSAGE_B24._serialized_end=894\n  _LOTSNESTEDMESSAGE_B25._serialized_start=896\n  _LOTSNESTEDMESSAGE_B25._serialized_end=901\n  _LOTSNESTEDMESSAGE_B26._serialized_start=903\n  _LOTSNESTEDMESSAGE_B26._serialized_end=908\n  _LOTSNESTEDMESSAGE_B27._serialized_start=910\n  _LOTSNESTEDMESSAGE_B27._serialized_end=915\n  _LOTSNESTEDMESSAGE_B28._serialized_start=917\n  _LOTSNESTEDMESSAGE_B28._serialized_end=922\n  _LOTSNESTEDMESSAGE_B29._serialized_start=924\n  _LOTSNESTEDMESSAGE_B29._serialized_end=929\n  _LOTSNESTEDMESSAGE_B30._serialized_start=931\n  _LOTSNESTEDMESSAGE_B30._serialized_end=936\n  _LOTSNESTEDMESSAGE_B31._serialized_start=938\n  _LOTSNESTEDMESSAGE_B31._serialized_end=943\n  _LOTSNESTEDMESSAGE_B32._serialized_start=945\n  _LOTSNESTEDMESSAGE_B32._serialized_end=950\n  _LOTSNESTEDMESSAGE_B33._serialized_start=952\n  _LOTSNESTEDMESSAGE_B33._serialized_end=957\n  _LOTSNESTEDMESSAGE_B34._serialized_start=959\n  _LOTSNESTEDMESSAGE_B34._serialized_end=964\n  _LOTSNESTEDMESSAGE_B35._serialized_start=966\n  _LOTSNESTEDMESSAGE_B35._serialized_end=971\n  _LOTSNESTEDMESSAGE_B36._serialized_start=973\n  _LOTSNESTEDMESSAGE_B36._serialized_end=978\n  _LOTSNESTEDMESSAGE_B37._serialized_start=980\n  _LOTSNESTEDMESSAGE_B37._serialized_end=985\n  _LOTSNESTEDMESSAGE_B38._serialized_start=987\n  _LOTSNESTEDMESSAGE_B38._serialized_end=992\n  _LOTSNESTEDMESSAGE_B39._serialized_start=994\n  _LOTSNESTEDMESSAGE_B39._serialized_end=999\n  _LOTSNESTEDMESSAGE_B40._serialized_start=1001\n  _LOTSNESTEDMESSAGE_B40._serialized_end=1006\n  _LOTSNESTEDMESSAGE_B41._serialized_start=1008\n  _LOTSNESTEDMESSAGE_B41._serialized_end=1013\n  _LOTSNESTEDMESSAGE_B42._serialized_start=1015\n  _LOTSNESTEDMESSAGE_B42._serialized_end=1020\n  _LOTSNESTEDMESSAGE_B43._serialized_start=1022\n  _LOTSNESTEDMESSAGE_B43._serialized_end=1027\n  _LOTSNESTEDMESSAGE_B44._serialized_start=1029\n  _LOTSNESTEDMESSAGE_B44._serialized_end=1034\n  _LOTSNESTEDMESSAGE_B45._serialized_start=1036\n  _LOTSNESTEDMESSAGE_B45._serialized_end=1041\n  _LOTSNESTEDMESSAGE_B46._serialized_start=1043\n  _LOTSNESTEDMESSAGE_B46._serialized_end=1048\n  _LOTSNESTEDMESSAGE_B47._serialized_start=1050\n  _LOTSNESTEDMESSAGE_B47._serialized_end=1055\n  _LOTSNESTEDMESSAGE_B48._serialized_start=1057\n  _LOTSNESTEDMESSAGE_B48._serialized_end=1062\n  _LOTSNESTEDMESSAGE_B49._serialized_start=1064\n  _LOTSNESTEDMESSAGE_B49._serialized_end=1069\n  _LOTSNESTEDMESSAGE_B50._serialized_start=1071\n  _LOTSNESTEDMESSAGE_B50._serialized_end=1076\n  _LOTSNESTEDMESSAGE_B51._serialized_start=1078\n  _LOTSNESTEDMESSAGE_B51._serialized_end=1083\n  _LOTSNESTEDMESSAGE_B52._serialized_start=1085\n  _LOTSNESTEDMESSAGE_B52._serialized_end=1090\n  _LOTSNESTEDMESSAGE_B53._serialized_start=1092\n  _LOTSNESTEDMESSAGE_B53._serialized_end=1097\n  _LOTSNESTEDMESSAGE_B54._serialized_start=1099\n  _LOTSNESTEDMESSAGE_B54._serialized_end=1104\n  _LOTSNESTEDMESSAGE_B55._serialized_start=1106\n  _LOTSNESTEDMESSAGE_B55._serialized_end=1111\n  _LOTSNESTEDMESSAGE_B56._serialized_start=1113\n  _LOTSNESTEDMESSAGE_B56._serialized_end=1118\n  _LOTSNESTEDMESSAGE_B57._serialized_start=1120\n  _LOTSNESTEDMESSAGE_B57._serialized_end=1125\n  _LOTSNESTEDMESSAGE_B58._serialized_start=1127\n  _LOTSNESTEDMESSAGE_B58._serialized_end=1132\n  _LOTSNESTEDMESSAGE_B59._serialized_start=1134\n  _LOTSNESTEDMESSAGE_B59._serialized_end=1139\n  _LOTSNESTEDMESSAGE_B60._serialized_start=1141\n  _LOTSNESTEDMESSAGE_B60._serialized_end=1146\n  _LOTSNESTEDMESSAGE_B61._serialized_start=1148\n  _LOTSNESTEDMESSAGE_B61._serialized_end=1153\n  _LOTSNESTEDMESSAGE_B62._serialized_start=1155\n  _LOTSNESTEDMESSAGE_B62._serialized_end=1160\n  _LOTSNESTEDMESSAGE_B63._serialized_start=1162\n  _LOTSNESTEDMESSAGE_B63._serialized_end=1167\n  _LOTSNESTEDMESSAGE_B64._serialized_start=1169\n  _LOTSNESTEDMESSAGE_B64._serialized_end=1174\n  _LOTSNESTEDMESSAGE_B65._serialized_start=1176\n  _LOTSNESTEDMESSAGE_B65._serialized_end=1181\n  _LOTSNESTEDMESSAGE_B66._serialized_start=1183\n  _LOTSNESTEDMESSAGE_B66._serialized_end=1188\n  _LOTSNESTEDMESSAGE_B67._serialized_start=1190\n  _LOTSNESTEDMESSAGE_B67._serialized_end=1195\n  _LOTSNESTEDMESSAGE_B68._serialized_start=1197\n  _LOTSNESTEDMESSAGE_B68._serialized_end=1202\n  _LOTSNESTEDMESSAGE_B69._serialized_start=1204\n  _LOTSNESTEDMESSAGE_B69._serialized_end=1209\n  _LOTSNESTEDMESSAGE_B70._serialized_start=1211\n  _LOTSNESTEDMESSAGE_B70._serialized_end=1216\n  _LOTSNESTEDMESSAGE_B71._serialized_start=1218\n  _LOTSNESTEDMESSAGE_B71._serialized_end=1223\n  _LOTSNESTEDMESSAGE_B72._serialized_start=1225\n  _LOTSNESTEDMESSAGE_B72._serialized_end=1230\n  _LOTSNESTEDMESSAGE_B73._serialized_start=1232\n  _LOTSNESTEDMESSAGE_B73._serialized_end=1237\n  _LOTSNESTEDMESSAGE_B74._serialized_start=1239\n  _LOTSNESTEDMESSAGE_B74._serialized_end=1244\n  _LOTSNESTEDMESSAGE_B75._serialized_start=1246\n  _LOTSNESTEDMESSAGE_B75._serialized_end=1251\n  _LOTSNESTEDMESSAGE_B76._serialized_start=1253\n  _LOTSNESTEDMESSAGE_B76._serialized_end=1258\n  _LOTSNESTEDMESSAGE_B77._serialized_start=1260\n  _LOTSNESTEDMESSAGE_B77._serialized_end=1265\n  _LOTSNESTEDMESSAGE_B78._serialized_start=1267\n  _LOTSNESTEDMESSAGE_B78._serialized_end=1272\n  _LOTSNESTEDMESSAGE_B79._serialized_start=1274\n  _LOTSNESTEDMESSAGE_B79._serialized_end=1279\n  _LOTSNESTEDMESSAGE_B80._serialized_start=1281\n  _LOTSNESTEDMESSAGE_B80._serialized_end=1286\n  _LOTSNESTEDMESSAGE_B81._serialized_start=1288\n  _LOTSNESTEDMESSAGE_B81._serialized_end=1293\n  _LOTSNESTEDMESSAGE_B82._serialized_start=1295\n  _LOTSNESTEDMESSAGE_B82._serialized_end=1300\n  _LOTSNESTEDMESSAGE_B83._serialized_start=1302\n  _LOTSNESTEDMESSAGE_B83._serialized_end=1307\n  _LOTSNESTEDMESSAGE_B84._serialized_start=1309\n  _LOTSNESTEDMESSAGE_B84._serialized_end=1314\n  _LOTSNESTEDMESSAGE_B85._serialized_start=1316\n  _LOTSNESTEDMESSAGE_B85._serialized_end=1321\n  _LOTSNESTEDMESSAGE_B86._serialized_start=1323\n  _LOTSNESTEDMESSAGE_B86._serialized_end=1328\n  _LOTSNESTEDMESSAGE_B87._serialized_start=1330\n  _LOTSNESTEDMESSAGE_B87._serialized_end=1335\n  _LOTSNESTEDMESSAGE_B88._serialized_start=1337\n  _LOTSNESTEDMESSAGE_B88._serialized_end=1342\n  _LOTSNESTEDMESSAGE_B89._serialized_start=1344\n  _LOTSNESTEDMESSAGE_B89._serialized_end=1349\n  _LOTSNESTEDMESSAGE_B90._serialized_start=1351\n  _LOTSNESTEDMESSAGE_B90._serialized_end=1356\n  _LOTSNESTEDMESSAGE_B91._serialized_start=1358\n  _LOTSNESTEDMESSAGE_B91._serialized_end=1363\n  _LOTSNESTEDMESSAGE_B92._serialized_start=1365\n  _LOTSNESTEDMESSAGE_B92._serialized_end=1370\n  _LOTSNESTEDMESSAGE_B93._serialized_start=1372\n  _LOTSNESTEDMESSAGE_B93._serialized_end=1377\n  _LOTSNESTEDMESSAGE_B94._serialized_start=1379\n  _LOTSNESTEDMESSAGE_B94._serialized_end=1384\n  _LOTSNESTEDMESSAGE_B95._serialized_start=1386\n  _LOTSNESTEDMESSAGE_B95._serialized_end=1391\n  _LOTSNESTEDMESSAGE_B96._serialized_start=1393\n  _LOTSNESTEDMESSAGE_B96._serialized_end=1398\n  _LOTSNESTEDMESSAGE_B97._serialized_start=1400\n  _LOTSNESTEDMESSAGE_B97._serialized_end=1405\n  _LOTSNESTEDMESSAGE_B98._serialized_start=1407\n  _LOTSNESTEDMESSAGE_B98._serialized_end=1412\n  _LOTSNESTEDMESSAGE_B99._serialized_start=1414\n  _LOTSNESTEDMESSAGE_B99._serialized_end=1419\n  _LOTSNESTEDMESSAGE_B100._serialized_start=1421\n  _LOTSNESTEDMESSAGE_B100._serialized_end=1427\n  _LOTSNESTEDMESSAGE_B101._serialized_start=1429\n  _LOTSNESTEDMESSAGE_B101._serialized_end=1435\n  _LOTSNESTEDMESSAGE_B102._serialized_start=1437\n  _LOTSNESTEDMESSAGE_B102._serialized_end=1443\n  _LOTSNESTEDMESSAGE_B103._serialized_start=1445\n  _LOTSNESTEDMESSAGE_B103._serialized_end=1451\n  _LOTSNESTEDMESSAGE_B104._serialized_start=1453\n  _LOTSNESTEDMESSAGE_B104._serialized_end=1459\n  _LOTSNESTEDMESSAGE_B105._serialized_start=1461\n  _LOTSNESTEDMESSAGE_B105._serialized_end=1467\n  _LOTSNESTEDMESSAGE_B106._serialized_start=1469\n  _LOTSNESTEDMESSAGE_B106._serialized_end=1475\n  _LOTSNESTEDMESSAGE_B107._serialized_start=1477\n  _LOTSNESTEDMESSAGE_B107._serialized_end=1483\n  _LOTSNESTEDMESSAGE_B108._serialized_start=1485\n  _LOTSNESTEDMESSAGE_B108._serialized_end=1491\n  _LOTSNESTEDMESSAGE_B109._serialized_start=1493\n  _LOTSNESTEDMESSAGE_B109._serialized_end=1499\n  _LOTSNESTEDMESSAGE_B110._serialized_start=1501\n  _LOTSNESTEDMESSAGE_B110._serialized_end=1507\n  _LOTSNESTEDMESSAGE_B111._serialized_start=1509\n  _LOTSNESTEDMESSAGE_B111._serialized_end=1515\n  _LOTSNESTEDMESSAGE_B112._serialized_start=1517\n  _LOTSNESTEDMESSAGE_B112._serialized_end=1523\n  _LOTSNESTEDMESSAGE_B113._serialized_start=1525\n  _LOTSNESTEDMESSAGE_B113._serialized_end=1531\n  _LOTSNESTEDMESSAGE_B114._serialized_start=1533\n  _LOTSNESTEDMESSAGE_B114._serialized_end=1539\n  _LOTSNESTEDMESSAGE_B115._serialized_start=1541\n  _LOTSNESTEDMESSAGE_B115._serialized_end=1547\n  _LOTSNESTEDMESSAGE_B116._serialized_start=1549\n  _LOTSNESTEDMESSAGE_B116._serialized_end=1555\n  _LOTSNESTEDMESSAGE_B117._serialized_start=1557\n  _LOTSNESTEDMESSAGE_B117._serialized_end=1563\n  _LOTSNESTEDMESSAGE_B118._serialized_start=1565\n  _LOTSNESTEDMESSAGE_B118._serialized_end=1571\n  _LOTSNESTEDMESSAGE_B119._serialized_start=1573\n  _LOTSNESTEDMESSAGE_B119._serialized_end=1579\n  _LOTSNESTEDMESSAGE_B120._serialized_start=1581\n  _LOTSNESTEDMESSAGE_B120._serialized_end=1587\n  _LOTSNESTEDMESSAGE_B121._serialized_start=1589\n  _LOTSNESTEDMESSAGE_B121._serialized_end=1595\n  _LOTSNESTEDMESSAGE_B122._serialized_start=1597\n  _LOTSNESTEDMESSAGE_B122._serialized_end=1603\n  _LOTSNESTEDMESSAGE_B123._serialized_start=1605\n  _LOTSNESTEDMESSAGE_B123._serialized_end=1611\n  _LOTSNESTEDMESSAGE_B124._serialized_start=1613\n  _LOTSNESTEDMESSAGE_B124._serialized_end=1619\n  _LOTSNESTEDMESSAGE_B125._serialized_start=1621\n  _LOTSNESTEDMESSAGE_B125._serialized_end=1627\n  _LOTSNESTEDMESSAGE_B126._serialized_start=1629\n  _LOTSNESTEDMESSAGE_B126._serialized_end=1635\n  _LOTSNESTEDMESSAGE_B127._serialized_start=1637\n  _LOTSNESTEDMESSAGE_B127._serialized_end=1643\n  _LOTSNESTEDMESSAGE_B128._serialized_start=1645\n  _LOTSNESTEDMESSAGE_B128._serialized_end=1651\n  _LOTSNESTEDMESSAGE_B129._serialized_start=1653\n  _LOTSNESTEDMESSAGE_B129._serialized_end=1659\n  _LOTSNESTEDMESSAGE_B130._serialized_start=1661\n  _LOTSNESTEDMESSAGE_B130._serialized_end=1667\n  _LOTSNESTEDMESSAGE_B131._serialized_start=1669\n  _LOTSNESTEDMESSAGE_B131._serialized_end=1675\n  _LOTSNESTEDMESSAGE_B132._serialized_start=1677\n  _LOTSNESTEDMESSAGE_B132._serialized_end=1683\n  _LOTSNESTEDMESSAGE_B133._serialized_start=1685\n  _LOTSNESTEDMESSAGE_B133._serialized_end=1691\n  _LOTSNESTEDMESSAGE_B134._serialized_start=1693\n  _LOTSNESTEDMESSAGE_B134._serialized_end=1699\n  _LOTSNESTEDMESSAGE_B135._serialized_start=1701\n  _LOTSNESTEDMESSAGE_B135._serialized_end=1707\n  _LOTSNESTEDMESSAGE_B136._serialized_start=1709\n  _LOTSNESTEDMESSAGE_B136._serialized_end=1715\n  _LOTSNESTEDMESSAGE_B137._serialized_start=1717\n  _LOTSNESTEDMESSAGE_B137._serialized_end=1723\n  _LOTSNESTEDMESSAGE_B138._serialized_start=1725\n  _LOTSNESTEDMESSAGE_B138._serialized_end=1731\n  _LOTSNESTEDMESSAGE_B139._serialized_start=1733\n  _LOTSNESTEDMESSAGE_B139._serialized_end=1739\n  _LOTSNESTEDMESSAGE_B140._serialized_start=1741\n  _LOTSNESTEDMESSAGE_B140._serialized_end=1747\n  _LOTSNESTEDMESSAGE_B141._serialized_start=1749\n  _LOTSNESTEDMESSAGE_B141._serialized_end=1755\n  _LOTSNESTEDMESSAGE_B142._serialized_start=1757\n  _LOTSNESTEDMESSAGE_B142._serialized_end=1763\n  _LOTSNESTEDMESSAGE_B143._serialized_start=1765\n  _LOTSNESTEDMESSAGE_B143._serialized_end=1771\n  _LOTSNESTEDMESSAGE_B144._serialized_start=1773\n  _LOTSNESTEDMESSAGE_B144._serialized_end=1779\n  _LOTSNESTEDMESSAGE_B145._serialized_start=1781\n  _LOTSNESTEDMESSAGE_B145._serialized_end=1787\n  _LOTSNESTEDMESSAGE_B146._serialized_start=1789\n  _LOTSNESTEDMESSAGE_B146._serialized_end=1795\n  _LOTSNESTEDMESSAGE_B147._serialized_start=1797\n  _LOTSNESTEDMESSAGE_B147._serialized_end=1803\n  _LOTSNESTEDMESSAGE_B148._serialized_start=1805\n  _LOTSNESTEDMESSAGE_B148._serialized_end=1811\n  _LOTSNESTEDMESSAGE_B149._serialized_start=1813\n  _LOTSNESTEDMESSAGE_B149._serialized_end=1819\n  _LOTSNESTEDMESSAGE_B150._serialized_start=1821\n  _LOTSNESTEDMESSAGE_B150._serialized_end=1827\n  _LOTSNESTEDMESSAGE_B151._serialized_start=1829\n  _LOTSNESTEDMESSAGE_B151._serialized_end=1835\n  _LOTSNESTEDMESSAGE_B152._serialized_start=1837\n  _LOTSNESTEDMESSAGE_B152._serialized_end=1843\n  _LOTSNESTEDMESSAGE_B153._serialized_start=1845\n  _LOTSNESTEDMESSAGE_B153._serialized_end=1851\n  _LOTSNESTEDMESSAGE_B154._serialized_start=1853\n  _LOTSNESTEDMESSAGE_B154._serialized_end=1859\n  _LOTSNESTEDMESSAGE_B155._serialized_start=1861\n  _LOTSNESTEDMESSAGE_B155._serialized_end=1867\n  _LOTSNESTEDMESSAGE_B156._serialized_start=1869\n  _LOTSNESTEDMESSAGE_B156._serialized_end=1875\n  _LOTSNESTEDMESSAGE_B157._serialized_start=1877\n  _LOTSNESTEDMESSAGE_B157._serialized_end=1883\n  _LOTSNESTEDMESSAGE_B158._serialized_start=1885\n  _LOTSNESTEDMESSAGE_B158._serialized_end=1891\n  _LOTSNESTEDMESSAGE_B159._serialized_start=1893\n  _LOTSNESTEDMESSAGE_B159._serialized_end=1899\n  _LOTSNESTEDMESSAGE_B160._serialized_start=1901\n  _LOTSNESTEDMESSAGE_B160._serialized_end=1907\n  _LOTSNESTEDMESSAGE_B161._serialized_start=1909\n  _LOTSNESTEDMESSAGE_B161._serialized_end=1915\n  _LOTSNESTEDMESSAGE_B162._serialized_start=1917\n  _LOTSNESTEDMESSAGE_B162._serialized_end=1923\n  _LOTSNESTEDMESSAGE_B163._serialized_start=1925\n  _LOTSNESTEDMESSAGE_B163._serialized_end=1931\n  _LOTSNESTEDMESSAGE_B164._serialized_start=1933\n  _LOTSNESTEDMESSAGE_B164._serialized_end=1939\n  _LOTSNESTEDMESSAGE_B165._serialized_start=1941\n  _LOTSNESTEDMESSAGE_B165._serialized_end=1947\n  _LOTSNESTEDMESSAGE_B166._serialized_start=1949\n  _LOTSNESTEDMESSAGE_B166._serialized_end=1955\n  _LOTSNESTEDMESSAGE_B167._serialized_start=1957\n  _LOTSNESTEDMESSAGE_B167._serialized_end=1963\n  _LOTSNESTEDMESSAGE_B168._serialized_start=1965\n  _LOTSNESTEDMESSAGE_B168._serialized_end=1971\n  _LOTSNESTEDMESSAGE_B169._serialized_start=1973\n  _LOTSNESTEDMESSAGE_B169._serialized_end=1979\n  _LOTSNESTEDMESSAGE_B170._serialized_start=1981\n  _LOTSNESTEDMESSAGE_B170._serialized_end=1987\n  _LOTSNESTEDMESSAGE_B171._serialized_start=1989\n  _LOTSNESTEDMESSAGE_B171._serialized_end=1995\n  _LOTSNESTEDMESSAGE_B172._serialized_start=1997\n  _LOTSNESTEDMESSAGE_B172._serialized_end=2003\n  _LOTSNESTEDMESSAGE_B173._serialized_start=2005\n  _LOTSNESTEDMESSAGE_B173._serialized_end=2011\n  _LOTSNESTEDMESSAGE_B174._serialized_start=2013\n  _LOTSNESTEDMESSAGE_B174._serialized_end=2019\n  _LOTSNESTEDMESSAGE_B175._serialized_start=2021\n  _LOTSNESTEDMESSAGE_B175._serialized_end=2027\n  _LOTSNESTEDMESSAGE_B176._serialized_start=2029\n  _LOTSNESTEDMESSAGE_B176._serialized_end=2035\n  _LOTSNESTEDMESSAGE_B177._serialized_start=2037\n  _LOTSNESTEDMESSAGE_B177._serialized_end=2043\n  _LOTSNESTEDMESSAGE_B178._serialized_start=2045\n  _LOTSNESTEDMESSAGE_B178._serialized_end=2051\n  _LOTSNESTEDMESSAGE_B179._serialized_start=2053\n  _LOTSNESTEDMESSAGE_B179._serialized_end=2059\n  _LOTSNESTEDMESSAGE_B180._serialized_start=2061\n  _LOTSNESTEDMESSAGE_B180._serialized_end=2067\n  _LOTSNESTEDMESSAGE_B181._serialized_start=2069\n  _LOTSNESTEDMESSAGE_B181._serialized_end=2075\n  _LOTSNESTEDMESSAGE_B182._serialized_start=2077\n  _LOTSNESTEDMESSAGE_B182._serialized_end=2083\n  _LOTSNESTEDMESSAGE_B183._serialized_start=2085\n  _LOTSNESTEDMESSAGE_B183._serialized_end=2091\n  _LOTSNESTEDMESSAGE_B184._serialized_start=2093\n  _LOTSNESTEDMESSAGE_B184._serialized_end=2099\n  _LOTSNESTEDMESSAGE_B185._serialized_start=2101\n  _LOTSNESTEDMESSAGE_B185._serialized_end=2107\n  _LOTSNESTEDMESSAGE_B186._serialized_start=2109\n  _LOTSNESTEDMESSAGE_B186._serialized_end=2115\n  _LOTSNESTEDMESSAGE_B187._serialized_start=2117\n  _LOTSNESTEDMESSAGE_B187._serialized_end=2123\n  _LOTSNESTEDMESSAGE_B188._serialized_start=2125\n  _LOTSNESTEDMESSAGE_B188._serialized_end=2131\n  _LOTSNESTEDMESSAGE_B189._serialized_start=2133\n  _LOTSNESTEDMESSAGE_B189._serialized_end=2139\n  _LOTSNESTEDMESSAGE_B190._serialized_start=2141\n  _LOTSNESTEDMESSAGE_B190._serialized_end=2147\n  _LOTSNESTEDMESSAGE_B191._serialized_start=2149\n  _LOTSNESTEDMESSAGE_B191._serialized_end=2155\n  _LOTSNESTEDMESSAGE_B192._serialized_start=2157\n  _LOTSNESTEDMESSAGE_B192._serialized_end=2163\n  _LOTSNESTEDMESSAGE_B193._serialized_start=2165\n  _LOTSNESTEDMESSAGE_B193._serialized_end=2171\n  _LOTSNESTEDMESSAGE_B194._serialized_start=2173\n  _LOTSNESTEDMESSAGE_B194._serialized_end=2179\n  _LOTSNESTEDMESSAGE_B195._serialized_start=2181\n  _LOTSNESTEDMESSAGE_B195._serialized_end=2187\n  _LOTSNESTEDMESSAGE_B196._serialized_start=2189\n  _LOTSNESTEDMESSAGE_B196._serialized_end=2195\n  _LOTSNESTEDMESSAGE_B197._serialized_start=2197\n  _LOTSNESTEDMESSAGE_B197._serialized_end=2203\n  _LOTSNESTEDMESSAGE_B198._serialized_start=2205\n  _LOTSNESTEDMESSAGE_B198._serialized_end=2211\n  _LOTSNESTEDMESSAGE_B199._serialized_start=2213\n  _LOTSNESTEDMESSAGE_B199._serialized_end=2219\n  _LOTSNESTEDMESSAGE_B200._serialized_start=2221\n  _LOTSNESTEDMESSAGE_B200._serialized_end=2227\n  _LOTSNESTEDMESSAGE_B201._serialized_start=2229\n  _LOTSNESTEDMESSAGE_B201._serialized_end=2235\n  _LOTSNESTEDMESSAGE_B202._serialized_start=2237\n  _LOTSNESTEDMESSAGE_B202._serialized_end=2243\n  _LOTSNESTEDMESSAGE_B203._serialized_start=2245\n  _LOTSNESTEDMESSAGE_B203._serialized_end=2251\n  _LOTSNESTEDMESSAGE_B204._serialized_start=2253\n  _LOTSNESTEDMESSAGE_B204._serialized_end=2259\n  _LOTSNESTEDMESSAGE_B205._serialized_start=2261\n  _LOTSNESTEDMESSAGE_B205._serialized_end=2267\n  _LOTSNESTEDMESSAGE_B206._serialized_start=2269\n  _LOTSNESTEDMESSAGE_B206._serialized_end=2275\n  _LOTSNESTEDMESSAGE_B207._serialized_start=2277\n  _LOTSNESTEDMESSAGE_B207._serialized_end=2283\n  _LOTSNESTEDMESSAGE_B208._serialized_start=2285\n  _LOTSNESTEDMESSAGE_B208._serialized_end=2291\n  _LOTSNESTEDMESSAGE_B209._serialized_start=2293\n  _LOTSNESTEDMESSAGE_B209._serialized_end=2299\n  _LOTSNESTEDMESSAGE_B210._serialized_start=2301\n  _LOTSNESTEDMESSAGE_B210._serialized_end=2307\n  _LOTSNESTEDMESSAGE_B211._serialized_start=2309\n  _LOTSNESTEDMESSAGE_B211._serialized_end=2315\n  _LOTSNESTEDMESSAGE_B212._serialized_start=2317\n  _LOTSNESTEDMESSAGE_B212._serialized_end=2323\n  _LOTSNESTEDMESSAGE_B213._serialized_start=2325\n  _LOTSNESTEDMESSAGE_B213._serialized_end=2331\n  _LOTSNESTEDMESSAGE_B214._serialized_start=2333\n  _LOTSNESTEDMESSAGE_B214._serialized_end=2339\n  _LOTSNESTEDMESSAGE_B215._serialized_start=2341\n  _LOTSNESTEDMESSAGE_B215._serialized_end=2347\n  _LOTSNESTEDMESSAGE_B216._serialized_start=2349\n  _LOTSNESTEDMESSAGE_B216._serialized_end=2355\n  _LOTSNESTEDMESSAGE_B217._serialized_start=2357\n  _LOTSNESTEDMESSAGE_B217._serialized_end=2363\n  _LOTSNESTEDMESSAGE_B218._serialized_start=2365\n  _LOTSNESTEDMESSAGE_B218._serialized_end=2371\n  _LOTSNESTEDMESSAGE_B219._serialized_start=2373\n  _LOTSNESTEDMESSAGE_B219._serialized_end=2379\n  _LOTSNESTEDMESSAGE_B220._serialized_start=2381\n  _LOTSNESTEDMESSAGE_B220._serialized_end=2387\n  _LOTSNESTEDMESSAGE_B221._serialized_start=2389\n  _LOTSNESTEDMESSAGE_B221._serialized_end=2395\n  _LOTSNESTEDMESSAGE_B222._serialized_start=2397\n  _LOTSNESTEDMESSAGE_B222._serialized_end=2403\n  _LOTSNESTEDMESSAGE_B223._serialized_start=2405\n  _LOTSNESTEDMESSAGE_B223._serialized_end=2411\n  _LOTSNESTEDMESSAGE_B224._serialized_start=2413\n  _LOTSNESTEDMESSAGE_B224._serialized_end=2419\n  _LOTSNESTEDMESSAGE_B225._serialized_start=2421\n  _LOTSNESTEDMESSAGE_B225._serialized_end=2427\n  _LOTSNESTEDMESSAGE_B226._serialized_start=2429\n  _LOTSNESTEDMESSAGE_B226._serialized_end=2435\n  _LOTSNESTEDMESSAGE_B227._serialized_start=2437\n  _LOTSNESTEDMESSAGE_B227._serialized_end=2443\n  _LOTSNESTEDMESSAGE_B228._serialized_start=2445\n  _LOTSNESTEDMESSAGE_B228._serialized_end=2451\n  _LOTSNESTEDMESSAGE_B229._serialized_start=2453\n  _LOTSNESTEDMESSAGE_B229._serialized_end=2459\n  _LOTSNESTEDMESSAGE_B230._serialized_start=2461\n  _LOTSNESTEDMESSAGE_B230._serialized_end=2467\n  _LOTSNESTEDMESSAGE_B231._serialized_start=2469\n  _LOTSNESTEDMESSAGE_B231._serialized_end=2475\n  _LOTSNESTEDMESSAGE_B232._serialized_start=2477\n  _LOTSNESTEDMESSAGE_B232._serialized_end=2483\n  _LOTSNESTEDMESSAGE_B233._serialized_start=2485\n  _LOTSNESTEDMESSAGE_B233._serialized_end=2491\n  _LOTSNESTEDMESSAGE_B234._serialized_start=2493\n  _LOTSNESTEDMESSAGE_B234._serialized_end=2499\n  _LOTSNESTEDMESSAGE_B235._serialized_start=2501\n  _LOTSNESTEDMESSAGE_B235._serialized_end=2507\n  _LOTSNESTEDMESSAGE_B236._serialized_start=2509\n  _LOTSNESTEDMESSAGE_B236._serialized_end=2515\n  _LOTSNESTEDMESSAGE_B237._serialized_start=2517\n  _LOTSNESTEDMESSAGE_B237._serialized_end=2523\n  _LOTSNESTEDMESSAGE_B238._serialized_start=2525\n  _LOTSNESTEDMESSAGE_B238._serialized_end=2531\n  _LOTSNESTEDMESSAGE_B239._serialized_start=2533\n  _LOTSNESTEDMESSAGE_B239._serialized_end=2539\n  _LOTSNESTEDMESSAGE_B240._serialized_start=2541\n  _LOTSNESTEDMESSAGE_B240._serialized_end=2547\n  _LOTSNESTEDMESSAGE_B241._serialized_start=2549\n  _LOTSNESTEDMESSAGE_B241._serialized_end=2555\n  _LOTSNESTEDMESSAGE_B242._serialized_start=2557\n  _LOTSNESTEDMESSAGE_B242._serialized_end=2563\n  _LOTSNESTEDMESSAGE_B243._serialized_start=2565\n  _LOTSNESTEDMESSAGE_B243._serialized_end=2571\n  _LOTSNESTEDMESSAGE_B244._serialized_start=2573\n  _LOTSNESTEDMESSAGE_B244._serialized_end=2579\n  _LOTSNESTEDMESSAGE_B245._serialized_start=2581\n  _LOTSNESTEDMESSAGE_B245._serialized_end=2587\n  _LOTSNESTEDMESSAGE_B246._serialized_start=2589\n  _LOTSNESTEDMESSAGE_B246._serialized_end=2595\n  _LOTSNESTEDMESSAGE_B247._serialized_start=2597\n  _LOTSNESTEDMESSAGE_B247._serialized_end=2603\n  _LOTSNESTEDMESSAGE_B248._serialized_start=2605\n  _LOTSNESTEDMESSAGE_B248._serialized_end=2611\n  _LOTSNESTEDMESSAGE_B249._serialized_start=2613\n  _LOTSNESTEDMESSAGE_B249._serialized_end=2619\n  _LOTSNESTEDMESSAGE_B250._serialized_start=2621\n  _LOTSNESTEDMESSAGE_B250._serialized_end=2627\n  _LOTSNESTEDMESSAGE_B251._serialized_start=2629\n  _LOTSNESTEDMESSAGE_B251._serialized_end=2635\n  _LOTSNESTEDMESSAGE_B252._serialized_start=2637\n  _LOTSNESTEDMESSAGE_B252._serialized_end=2643\n  _LOTSNESTEDMESSAGE_B253._serialized_start=2645\n  _LOTSNESTEDMESSAGE_B253._serialized_end=2651\n  _LOTSNESTEDMESSAGE_B254._serialized_start=2653\n  _LOTSNESTEDMESSAGE_B254._serialized_end=2659\n  _LOTSNESTEDMESSAGE_B255._serialized_start=2661\n  _LOTSNESTEDMESSAGE_B255._serialized_end=2667\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/no_package_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/internal/no_package.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n)google/protobuf/internal/no_package.proto\\\";\\n\\x10NoPackageMessage\\x12\\'\\n\\x0fno_package_enum\\x18\\x01 \\x01(\\x0e\\x32\\x0e.NoPackageEnum*?\\n\\rNoPackageEnum\\x12\\x16\\n\\x12NO_PACKAGE_VALUE_0\\x10\\x00\\x12\\x16\\n\\x12NO_PACKAGE_VALUE_1\\x10\\x01')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.no_package_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  _NOPACKAGEENUM._serialized_start=106\n  _NOPACKAGEENUM._serialized_end=169\n  _NOPACKAGEMESSAGE._serialized_start=45\n  _NOPACKAGEMESSAGE._serialized_end=104\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/python_message.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n# This code is meant to work on Python 2.4 and above only.\n#\n# TODO(robinson): Helpers for verbose, common checks like seeing if a\n# descriptor's cpp_type is CPPTYPE_MESSAGE.\n\n\"\"\"Contains a metaclass and helper functions used to create\nprotocol message classes from Descriptor objects at runtime.\n\nRecall that a metaclass is the \"type\" of a class.\n(A class is to a metaclass what an instance is to a class.)\n\nIn this case, we use the GeneratedProtocolMessageType metaclass\nto inject all the useful functionality into the classes\noutput by the protocol compiler at compile-time.\n\nThe upshot of all this is that the real implementation\ndetails for ALL pure-Python protocol buffers are *here in\nthis file*.\n\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\nfrom io import BytesIO\nimport struct\nimport sys\nimport weakref\n\n# We use \"as\" to avoid name collisions with variables.\nfrom google.protobuf.internal import api_implementation\nfrom google.protobuf.internal import containers\nfrom google.protobuf.internal import decoder\nfrom google.protobuf.internal import encoder\nfrom google.protobuf.internal import enum_type_wrapper\nfrom google.protobuf.internal import extension_dict\nfrom google.protobuf.internal import message_listener as message_listener_mod\nfrom google.protobuf.internal import type_checkers\nfrom google.protobuf.internal import well_known_types\nfrom google.protobuf.internal import wire_format\nfrom google.protobuf import descriptor as descriptor_mod\nfrom google.protobuf import message as message_mod\nfrom google.protobuf import text_format\n\n_FieldDescriptor = descriptor_mod.FieldDescriptor\n_AnyFullTypeName = 'google.protobuf.Any'\n_ExtensionDict = extension_dict._ExtensionDict\n\nclass GeneratedProtocolMessageType(type):\n\n  \"\"\"Metaclass for protocol message classes created at runtime from Descriptors.\n\n  We add implementations for all methods described in the Message class.  We\n  also create properties to allow getting/setting all fields in the protocol\n  message.  Finally, we create slots to prevent users from accidentally\n  \"setting\" nonexistent fields in the protocol message, which then wouldn't get\n  serialized / deserialized properly.\n\n  The protocol compiler currently uses this metaclass to create protocol\n  message classes at runtime.  Clients can also manually create their own\n  classes at runtime, as in this example:\n\n  mydescriptor = Descriptor(.....)\n  factory = symbol_database.Default()\n  factory.pool.AddDescriptor(mydescriptor)\n  MyProtoClass = factory.GetPrototype(mydescriptor)\n  myproto_instance = MyProtoClass()\n  myproto.foo_field = 23\n  ...\n  \"\"\"\n\n  # Must be consistent with the protocol-compiler code in\n  # proto2/compiler/internal/generator.*.\n  _DESCRIPTOR_KEY = 'DESCRIPTOR'\n\n  def __new__(cls, name, bases, dictionary):\n    \"\"\"Custom allocation for runtime-generated class types.\n\n    We override __new__ because this is apparently the only place\n    where we can meaningfully set __slots__ on the class we're creating(?).\n    (The interplay between metaclasses and slots is not very well-documented).\n\n    Args:\n      name: Name of the class (ignored, but required by the\n        metaclass protocol).\n      bases: Base classes of the class we're constructing.\n        (Should be message.Message).  We ignore this field, but\n        it's required by the metaclass protocol\n      dictionary: The class dictionary of the class we're\n        constructing.  dictionary[_DESCRIPTOR_KEY] must contain\n        a Descriptor object describing this protocol message\n        type.\n\n    Returns:\n      Newly-allocated class.\n\n    Raises:\n      RuntimeError: Generated code only work with python cpp extension.\n    \"\"\"\n    descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY]\n\n    if isinstance(descriptor, str):\n      raise RuntimeError('The generated code only work with python cpp '\n                         'extension, but it is using pure python runtime.')\n\n    # If a concrete class already exists for this descriptor, don't try to\n    # create another.  Doing so will break any messages that already exist with\n    # the existing class.\n    #\n    # The C++ implementation appears to have its own internal `PyMessageFactory`\n    # to achieve similar results.\n    #\n    # This most commonly happens in `text_format.py` when using descriptors from\n    # a custom pool; it calls symbol_database.Global().getPrototype() on a\n    # descriptor which already has an existing concrete class.\n    new_class = getattr(descriptor, '_concrete_class', None)\n    if new_class:\n      return new_class\n\n    if descriptor.full_name in well_known_types.WKTBASES:\n      bases += (well_known_types.WKTBASES[descriptor.full_name],)\n    _AddClassAttributesForNestedExtensions(descriptor, dictionary)\n    _AddSlots(descriptor, dictionary)\n\n    superclass = super(GeneratedProtocolMessageType, cls)\n    new_class = superclass.__new__(cls, name, bases, dictionary)\n    return new_class\n\n  def __init__(cls, name, bases, dictionary):\n    \"\"\"Here we perform the majority of our work on the class.\n    We add enum getters, an __init__ method, implementations\n    of all Message methods, and properties for all fields\n    in the protocol type.\n\n    Args:\n      name: Name of the class (ignored, but required by the\n        metaclass protocol).\n      bases: Base classes of the class we're constructing.\n        (Should be message.Message).  We ignore this field, but\n        it's required by the metaclass protocol\n      dictionary: The class dictionary of the class we're\n        constructing.  dictionary[_DESCRIPTOR_KEY] must contain\n        a Descriptor object describing this protocol message\n        type.\n    \"\"\"\n    descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY]\n\n    # If this is an _existing_ class looked up via `_concrete_class` in the\n    # __new__ method above, then we don't need to re-initialize anything.\n    existing_class = getattr(descriptor, '_concrete_class', None)\n    if existing_class:\n      assert existing_class is cls, (\n          'Duplicate `GeneratedProtocolMessageType` created for descriptor %r'\n          % (descriptor.full_name))\n      return\n\n    cls._decoders_by_tag = {}\n    if (descriptor.has_options and\n        descriptor.GetOptions().message_set_wire_format):\n      cls._decoders_by_tag[decoder.MESSAGE_SET_ITEM_TAG] = (\n          decoder.MessageSetItemDecoder(descriptor), None)\n\n    # Attach stuff to each FieldDescriptor for quick lookup later on.\n    for field in descriptor.fields:\n      _AttachFieldHelpers(cls, field)\n\n    descriptor._concrete_class = cls  # pylint: disable=protected-access\n    _AddEnumValues(descriptor, cls)\n    _AddInitMethod(descriptor, cls)\n    _AddPropertiesForFields(descriptor, cls)\n    _AddPropertiesForExtensions(descriptor, cls)\n    _AddStaticMethods(cls)\n    _AddMessageMethods(descriptor, cls)\n    _AddPrivateHelperMethods(descriptor, cls)\n\n    superclass = super(GeneratedProtocolMessageType, cls)\n    superclass.__init__(name, bases, dictionary)\n\n\n# Stateless helpers for GeneratedProtocolMessageType below.\n# Outside clients should not access these directly.\n#\n# I opted not to make any of these methods on the metaclass, to make it more\n# clear that I'm not really using any state there and to keep clients from\n# thinking that they have direct access to these construction helpers.\n\n\ndef _PropertyName(proto_field_name):\n  \"\"\"Returns the name of the public property attribute which\n  clients can use to get and (in some cases) set the value\n  of a protocol message field.\n\n  Args:\n    proto_field_name: The protocol message field name, exactly\n      as it appears (or would appear) in a .proto file.\n  \"\"\"\n  # TODO(robinson): Escape Python keywords (e.g., yield), and test this support.\n  # nnorwitz makes my day by writing:\n  # \"\"\"\n  # FYI.  See the keyword module in the stdlib. This could be as simple as:\n  #\n  # if keyword.iskeyword(proto_field_name):\n  #   return proto_field_name + \"_\"\n  # return proto_field_name\n  # \"\"\"\n  # Kenton says:  The above is a BAD IDEA.  People rely on being able to use\n  #   getattr() and setattr() to reflectively manipulate field values.  If we\n  #   rename the properties, then every such user has to also make sure to apply\n  #   the same transformation.  Note that currently if you name a field \"yield\",\n  #   you can still access it just fine using getattr/setattr -- it's not even\n  #   that cumbersome to do so.\n  # TODO(kenton):  Remove this method entirely if/when everyone agrees with my\n  #   position.\n  return proto_field_name\n\n\ndef _AddSlots(message_descriptor, dictionary):\n  \"\"\"Adds a __slots__ entry to dictionary, containing the names of all valid\n  attributes for this message type.\n\n  Args:\n    message_descriptor: A Descriptor instance describing this message type.\n    dictionary: Class dictionary to which we'll add a '__slots__' entry.\n  \"\"\"\n  dictionary['__slots__'] = ['_cached_byte_size',\n                             '_cached_byte_size_dirty',\n                             '_fields',\n                             '_unknown_fields',\n                             '_unknown_field_set',\n                             '_is_present_in_parent',\n                             '_listener',\n                             '_listener_for_children',\n                             '__weakref__',\n                             '_oneofs']\n\n\ndef _IsMessageSetExtension(field):\n  return (field.is_extension and\n          field.containing_type.has_options and\n          field.containing_type.GetOptions().message_set_wire_format and\n          field.type == _FieldDescriptor.TYPE_MESSAGE and\n          field.label == _FieldDescriptor.LABEL_OPTIONAL)\n\n\ndef _IsMapField(field):\n  return (field.type == _FieldDescriptor.TYPE_MESSAGE and\n          field.message_type.has_options and\n          field.message_type.GetOptions().map_entry)\n\n\ndef _IsMessageMapField(field):\n  value_type = field.message_type.fields_by_name['value']\n  return value_type.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE\n\n\ndef _AttachFieldHelpers(cls, field_descriptor):\n  is_repeated = (field_descriptor.label == _FieldDescriptor.LABEL_REPEATED)\n  is_packable = (is_repeated and\n                 wire_format.IsTypePackable(field_descriptor.type))\n  is_proto3 = field_descriptor.containing_type.syntax == 'proto3'\n  if not is_packable:\n    is_packed = False\n  elif field_descriptor.containing_type.syntax == 'proto2':\n    is_packed = (field_descriptor.has_options and\n                field_descriptor.GetOptions().packed)\n  else:\n    has_packed_false = (field_descriptor.has_options and\n                        field_descriptor.GetOptions().HasField('packed') and\n                        field_descriptor.GetOptions().packed == False)\n    is_packed = not has_packed_false\n  is_map_entry = _IsMapField(field_descriptor)\n\n  if is_map_entry:\n    field_encoder = encoder.MapEncoder(field_descriptor)\n    sizer = encoder.MapSizer(field_descriptor,\n                             _IsMessageMapField(field_descriptor))\n  elif _IsMessageSetExtension(field_descriptor):\n    field_encoder = encoder.MessageSetItemEncoder(field_descriptor.number)\n    sizer = encoder.MessageSetItemSizer(field_descriptor.number)\n  else:\n    field_encoder = type_checkers.TYPE_TO_ENCODER[field_descriptor.type](\n        field_descriptor.number, is_repeated, is_packed)\n    sizer = type_checkers.TYPE_TO_SIZER[field_descriptor.type](\n        field_descriptor.number, is_repeated, is_packed)\n\n  field_descriptor._encoder = field_encoder\n  field_descriptor._sizer = sizer\n  field_descriptor._default_constructor = _DefaultValueConstructorForField(\n      field_descriptor)\n\n  def AddDecoder(wiretype, is_packed):\n    tag_bytes = encoder.TagBytes(field_descriptor.number, wiretype)\n    decode_type = field_descriptor.type\n    if (decode_type == _FieldDescriptor.TYPE_ENUM and\n        type_checkers.SupportsOpenEnums(field_descriptor)):\n      decode_type = _FieldDescriptor.TYPE_INT32\n\n    oneof_descriptor = None\n    clear_if_default = False\n    if field_descriptor.containing_oneof is not None:\n      oneof_descriptor = field_descriptor\n    elif (is_proto3 and not is_repeated and\n          field_descriptor.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE):\n      clear_if_default = True\n\n    if is_map_entry:\n      is_message_map = _IsMessageMapField(field_descriptor)\n\n      field_decoder = decoder.MapDecoder(\n          field_descriptor, _GetInitializeDefaultForMap(field_descriptor),\n          is_message_map)\n    elif decode_type == _FieldDescriptor.TYPE_STRING:\n      field_decoder = decoder.StringDecoder(\n          field_descriptor.number, is_repeated, is_packed,\n          field_descriptor, field_descriptor._default_constructor,\n          clear_if_default)\n    elif field_descriptor.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n      field_decoder = type_checkers.TYPE_TO_DECODER[decode_type](\n          field_descriptor.number, is_repeated, is_packed,\n          field_descriptor, field_descriptor._default_constructor)\n    else:\n      field_decoder = type_checkers.TYPE_TO_DECODER[decode_type](\n          field_descriptor.number, is_repeated, is_packed,\n          # pylint: disable=protected-access\n          field_descriptor, field_descriptor._default_constructor,\n          clear_if_default)\n\n    cls._decoders_by_tag[tag_bytes] = (field_decoder, oneof_descriptor)\n\n  AddDecoder(type_checkers.FIELD_TYPE_TO_WIRE_TYPE[field_descriptor.type],\n             False)\n\n  if is_repeated and wire_format.IsTypePackable(field_descriptor.type):\n    # To support wire compatibility of adding packed = true, add a decoder for\n    # packed values regardless of the field's options.\n    AddDecoder(wire_format.WIRETYPE_LENGTH_DELIMITED, True)\n\n\ndef _AddClassAttributesForNestedExtensions(descriptor, dictionary):\n  extensions = descriptor.extensions_by_name\n  for extension_name, extension_field in extensions.items():\n    assert extension_name not in dictionary\n    dictionary[extension_name] = extension_field\n\n\ndef _AddEnumValues(descriptor, cls):\n  \"\"\"Sets class-level attributes for all enum fields defined in this message.\n\n  Also exporting a class-level object that can name enum values.\n\n  Args:\n    descriptor: Descriptor object for this message type.\n    cls: Class we're constructing for this message type.\n  \"\"\"\n  for enum_type in descriptor.enum_types:\n    setattr(cls, enum_type.name, enum_type_wrapper.EnumTypeWrapper(enum_type))\n    for enum_value in enum_type.values:\n      setattr(cls, enum_value.name, enum_value.number)\n\n\ndef _GetInitializeDefaultForMap(field):\n  if field.label != _FieldDescriptor.LABEL_REPEATED:\n    raise ValueError('map_entry set on non-repeated field %s' % (\n        field.name))\n  fields_by_name = field.message_type.fields_by_name\n  key_checker = type_checkers.GetTypeChecker(fields_by_name['key'])\n\n  value_field = fields_by_name['value']\n  if _IsMessageMapField(field):\n    def MakeMessageMapDefault(message):\n      return containers.MessageMap(\n          message._listener_for_children, value_field.message_type, key_checker,\n          field.message_type)\n    return MakeMessageMapDefault\n  else:\n    value_checker = type_checkers.GetTypeChecker(value_field)\n    def MakePrimitiveMapDefault(message):\n      return containers.ScalarMap(\n          message._listener_for_children, key_checker, value_checker,\n          field.message_type)\n    return MakePrimitiveMapDefault\n\ndef _DefaultValueConstructorForField(field):\n  \"\"\"Returns a function which returns a default value for a field.\n\n  Args:\n    field: FieldDescriptor object for this field.\n\n  The returned function has one argument:\n    message: Message instance containing this field, or a weakref proxy\n      of same.\n\n  That function in turn returns a default value for this field.  The default\n    value may refer back to |message| via a weak reference.\n  \"\"\"\n\n  if _IsMapField(field):\n    return _GetInitializeDefaultForMap(field)\n\n  if field.label == _FieldDescriptor.LABEL_REPEATED:\n    if field.has_default_value and field.default_value != []:\n      raise ValueError('Repeated field default value not empty list: %s' % (\n          field.default_value))\n    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n      # We can't look at _concrete_class yet since it might not have\n      # been set.  (Depends on order in which we initialize the classes).\n      message_type = field.message_type\n      def MakeRepeatedMessageDefault(message):\n        return containers.RepeatedCompositeFieldContainer(\n            message._listener_for_children, field.message_type)\n      return MakeRepeatedMessageDefault\n    else:\n      type_checker = type_checkers.GetTypeChecker(field)\n      def MakeRepeatedScalarDefault(message):\n        return containers.RepeatedScalarFieldContainer(\n            message._listener_for_children, type_checker)\n      return MakeRepeatedScalarDefault\n\n  if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n    # _concrete_class may not yet be initialized.\n    message_type = field.message_type\n    def MakeSubMessageDefault(message):\n      assert getattr(message_type, '_concrete_class', None), (\n          'Uninitialized concrete class found for field %r (message type %r)'\n          % (field.full_name, message_type.full_name))\n      result = message_type._concrete_class()\n      result._SetListener(\n          _OneofListener(message, field)\n          if field.containing_oneof is not None\n          else message._listener_for_children)\n      return result\n    return MakeSubMessageDefault\n\n  def MakeScalarDefault(message):\n    # TODO(protobuf-team): This may be broken since there may not be\n    # default_value.  Combine with has_default_value somehow.\n    return field.default_value\n  return MakeScalarDefault\n\n\ndef _ReraiseTypeErrorWithFieldName(message_name, field_name):\n  \"\"\"Re-raise the currently-handled TypeError with the field name added.\"\"\"\n  exc = sys.exc_info()[1]\n  if len(exc.args) == 1 and type(exc) is TypeError:\n    # simple TypeError; add field name to exception message\n    exc = TypeError('%s for field %s.%s' % (str(exc), message_name, field_name))\n\n  # re-raise possibly-amended exception with original traceback:\n  raise exc.with_traceback(sys.exc_info()[2])\n\n\ndef _AddInitMethod(message_descriptor, cls):\n  \"\"\"Adds an __init__ method to cls.\"\"\"\n\n  def _GetIntegerEnumValue(enum_type, value):\n    \"\"\"Convert a string or integer enum value to an integer.\n\n    If the value is a string, it is converted to the enum value in\n    enum_type with the same name.  If the value is not a string, it's\n    returned as-is.  (No conversion or bounds-checking is done.)\n    \"\"\"\n    if isinstance(value, str):\n      try:\n        return enum_type.values_by_name[value].number\n      except KeyError:\n        raise ValueError('Enum type %s: unknown label \"%s\"' % (\n            enum_type.full_name, value))\n    return value\n\n  def init(self, **kwargs):\n    self._cached_byte_size = 0\n    self._cached_byte_size_dirty = len(kwargs) > 0\n    self._fields = {}\n    # Contains a mapping from oneof field descriptors to the descriptor\n    # of the currently set field in that oneof field.\n    self._oneofs = {}\n\n    # _unknown_fields is () when empty for efficiency, and will be turned into\n    # a list if fields are added.\n    self._unknown_fields = ()\n    # _unknown_field_set is None when empty for efficiency, and will be\n    # turned into UnknownFieldSet struct if fields are added.\n    self._unknown_field_set = None      # pylint: disable=protected-access\n    self._is_present_in_parent = False\n    self._listener = message_listener_mod.NullMessageListener()\n    self._listener_for_children = _Listener(self)\n    for field_name, field_value in kwargs.items():\n      field = _GetFieldByName(message_descriptor, field_name)\n      if field is None:\n        raise TypeError('%s() got an unexpected keyword argument \"%s\"' %\n                        (message_descriptor.name, field_name))\n      if field_value is None:\n        # field=None is the same as no field at all.\n        continue\n      if field.label == _FieldDescriptor.LABEL_REPEATED:\n        copy = field._default_constructor(self)\n        if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:  # Composite\n          if _IsMapField(field):\n            if _IsMessageMapField(field):\n              for key in field_value:\n                copy[key].MergeFrom(field_value[key])\n            else:\n              copy.update(field_value)\n          else:\n            for val in field_value:\n              if isinstance(val, dict):\n                copy.add(**val)\n              else:\n                copy.add().MergeFrom(val)\n        else:  # Scalar\n          if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM:\n            field_value = [_GetIntegerEnumValue(field.enum_type, val)\n                           for val in field_value]\n          copy.extend(field_value)\n        self._fields[field] = copy\n      elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n        copy = field._default_constructor(self)\n        new_val = field_value\n        if isinstance(field_value, dict):\n          new_val = field.message_type._concrete_class(**field_value)\n        try:\n          copy.MergeFrom(new_val)\n        except TypeError:\n          _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name)\n        self._fields[field] = copy\n      else:\n        if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM:\n          field_value = _GetIntegerEnumValue(field.enum_type, field_value)\n        try:\n          setattr(self, field_name, field_value)\n        except TypeError:\n          _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name)\n\n  init.__module__ = None\n  init.__doc__ = None\n  cls.__init__ = init\n\n\ndef _GetFieldByName(message_descriptor, field_name):\n  \"\"\"Returns a field descriptor by field name.\n\n  Args:\n    message_descriptor: A Descriptor describing all fields in message.\n    field_name: The name of the field to retrieve.\n  Returns:\n    The field descriptor associated with the field name.\n  \"\"\"\n  try:\n    return message_descriptor.fields_by_name[field_name]\n  except KeyError:\n    raise ValueError('Protocol message %s has no \"%s\" field.' %\n                     (message_descriptor.name, field_name))\n\n\ndef _AddPropertiesForFields(descriptor, cls):\n  \"\"\"Adds properties for all fields in this protocol message type.\"\"\"\n  for field in descriptor.fields:\n    _AddPropertiesForField(field, cls)\n\n  if descriptor.is_extendable:\n    # _ExtensionDict is just an adaptor with no state so we allocate a new one\n    # every time it is accessed.\n    cls.Extensions = property(lambda self: _ExtensionDict(self))\n\n\ndef _AddPropertiesForField(field, cls):\n  \"\"\"Adds a public property for a protocol message field.\n  Clients can use this property to get and (in the case\n  of non-repeated scalar fields) directly set the value\n  of a protocol message field.\n\n  Args:\n    field: A FieldDescriptor for this field.\n    cls: The class we're constructing.\n  \"\"\"\n  # Catch it if we add other types that we should\n  # handle specially here.\n  assert _FieldDescriptor.MAX_CPPTYPE == 10\n\n  constant_name = field.name.upper() + '_FIELD_NUMBER'\n  setattr(cls, constant_name, field.number)\n\n  if field.label == _FieldDescriptor.LABEL_REPEATED:\n    _AddPropertiesForRepeatedField(field, cls)\n  elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n    _AddPropertiesForNonRepeatedCompositeField(field, cls)\n  else:\n    _AddPropertiesForNonRepeatedScalarField(field, cls)\n\n\nclass _FieldProperty(property):\n  __slots__ = ('DESCRIPTOR',)\n\n  def __init__(self, descriptor, getter, setter, doc):\n    property.__init__(self, getter, setter, doc=doc)\n    self.DESCRIPTOR = descriptor\n\n\ndef _AddPropertiesForRepeatedField(field, cls):\n  \"\"\"Adds a public property for a \"repeated\" protocol message field.  Clients\n  can use this property to get the value of the field, which will be either a\n  RepeatedScalarFieldContainer or RepeatedCompositeFieldContainer (see\n  below).\n\n  Note that when clients add values to these containers, we perform\n  type-checking in the case of repeated scalar fields, and we also set any\n  necessary \"has\" bits as a side-effect.\n\n  Args:\n    field: A FieldDescriptor for this field.\n    cls: The class we're constructing.\n  \"\"\"\n  proto_field_name = field.name\n  property_name = _PropertyName(proto_field_name)\n\n  def getter(self):\n    field_value = self._fields.get(field)\n    if field_value is None:\n      # Construct a new object to represent this field.\n      field_value = field._default_constructor(self)\n\n      # Atomically check if another thread has preempted us and, if not, swap\n      # in the new object we just created.  If someone has preempted us, we\n      # take that object and discard ours.\n      # WARNING:  We are relying on setdefault() being atomic.  This is true\n      #   in CPython but we haven't investigated others.  This warning appears\n      #   in several other locations in this file.\n      field_value = self._fields.setdefault(field, field_value)\n    return field_value\n  getter.__module__ = None\n  getter.__doc__ = 'Getter for %s.' % proto_field_name\n\n  # We define a setter just so we can throw an exception with a more\n  # helpful error message.\n  def setter(self, new_value):\n    raise AttributeError('Assignment not allowed to repeated field '\n                         '\"%s\" in protocol message object.' % proto_field_name)\n\n  doc = 'Magic attribute generated for \"%s\" proto field.' % proto_field_name\n  setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc))\n\n\ndef _AddPropertiesForNonRepeatedScalarField(field, cls):\n  \"\"\"Adds a public property for a nonrepeated, scalar protocol message field.\n  Clients can use this property to get and directly set the value of the field.\n  Note that when the client sets the value of a field by using this property,\n  all necessary \"has\" bits are set as a side-effect, and we also perform\n  type-checking.\n\n  Args:\n    field: A FieldDescriptor for this field.\n    cls: The class we're constructing.\n  \"\"\"\n  proto_field_name = field.name\n  property_name = _PropertyName(proto_field_name)\n  type_checker = type_checkers.GetTypeChecker(field)\n  default_value = field.default_value\n  is_proto3 = field.containing_type.syntax == 'proto3'\n\n  def getter(self):\n    # TODO(protobuf-team): This may be broken since there may not be\n    # default_value.  Combine with has_default_value somehow.\n    return self._fields.get(field, default_value)\n  getter.__module__ = None\n  getter.__doc__ = 'Getter for %s.' % proto_field_name\n\n  clear_when_set_to_default = is_proto3 and not field.containing_oneof\n\n  def field_setter(self, new_value):\n    # pylint: disable=protected-access\n    # Testing the value for truthiness captures all of the proto3 defaults\n    # (0, 0.0, enum 0, and False).\n    try:\n      new_value = type_checker.CheckValue(new_value)\n    except TypeError as e:\n      raise TypeError(\n          'Cannot set %s to %.1024r: %s' % (field.full_name, new_value, e))\n    if clear_when_set_to_default and not new_value:\n      self._fields.pop(field, None)\n    else:\n      self._fields[field] = new_value\n    # Check _cached_byte_size_dirty inline to improve performance, since scalar\n    # setters are called frequently.\n    if not self._cached_byte_size_dirty:\n      self._Modified()\n\n  if field.containing_oneof:\n    def setter(self, new_value):\n      field_setter(self, new_value)\n      self._UpdateOneofState(field)\n  else:\n    setter = field_setter\n\n  setter.__module__ = None\n  setter.__doc__ = 'Setter for %s.' % proto_field_name\n\n  # Add a property to encapsulate the getter/setter.\n  doc = 'Magic attribute generated for \"%s\" proto field.' % proto_field_name\n  setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc))\n\n\ndef _AddPropertiesForNonRepeatedCompositeField(field, cls):\n  \"\"\"Adds a public property for a nonrepeated, composite protocol message field.\n  A composite field is a \"group\" or \"message\" field.\n\n  Clients can use this property to get the value of the field, but cannot\n  assign to the property directly.\n\n  Args:\n    field: A FieldDescriptor for this field.\n    cls: The class we're constructing.\n  \"\"\"\n  # TODO(robinson): Remove duplication with similar method\n  # for non-repeated scalars.\n  proto_field_name = field.name\n  property_name = _PropertyName(proto_field_name)\n\n  def getter(self):\n    field_value = self._fields.get(field)\n    if field_value is None:\n      # Construct a new object to represent this field.\n      field_value = field._default_constructor(self)\n\n      # Atomically check if another thread has preempted us and, if not, swap\n      # in the new object we just created.  If someone has preempted us, we\n      # take that object and discard ours.\n      # WARNING:  We are relying on setdefault() being atomic.  This is true\n      #   in CPython but we haven't investigated others.  This warning appears\n      #   in several other locations in this file.\n      field_value = self._fields.setdefault(field, field_value)\n    return field_value\n  getter.__module__ = None\n  getter.__doc__ = 'Getter for %s.' % proto_field_name\n\n  # We define a setter just so we can throw an exception with a more\n  # helpful error message.\n  def setter(self, new_value):\n    raise AttributeError('Assignment not allowed to composite field '\n                         '\"%s\" in protocol message object.' % proto_field_name)\n\n  # Add a property to encapsulate the getter.\n  doc = 'Magic attribute generated for \"%s\" proto field.' % proto_field_name\n  setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc))\n\n\ndef _AddPropertiesForExtensions(descriptor, cls):\n  \"\"\"Adds properties for all fields in this protocol message type.\"\"\"\n  extensions = descriptor.extensions_by_name\n  for extension_name, extension_field in extensions.items():\n    constant_name = extension_name.upper() + '_FIELD_NUMBER'\n    setattr(cls, constant_name, extension_field.number)\n\n  # TODO(amauryfa): Migrate all users of these attributes to functions like\n  #   pool.FindExtensionByNumber(descriptor).\n  if descriptor.file is not None:\n    # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available.\n    pool = descriptor.file.pool\n    cls._extensions_by_number = pool._extensions_by_number[descriptor]\n    cls._extensions_by_name = pool._extensions_by_name[descriptor]\n\ndef _AddStaticMethods(cls):\n  # TODO(robinson): This probably needs to be thread-safe(?)\n  def RegisterExtension(extension_handle):\n    extension_handle.containing_type = cls.DESCRIPTOR\n    # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available.\n    # pylint: disable=protected-access\n    cls.DESCRIPTOR.file.pool._AddExtensionDescriptor(extension_handle)\n    _AttachFieldHelpers(cls, extension_handle)\n  cls.RegisterExtension = staticmethod(RegisterExtension)\n\n  def FromString(s):\n    message = cls()\n    message.MergeFromString(s)\n    return message\n  cls.FromString = staticmethod(FromString)\n\n\ndef _IsPresent(item):\n  \"\"\"Given a (FieldDescriptor, value) tuple from _fields, return true if the\n  value should be included in the list returned by ListFields().\"\"\"\n\n  if item[0].label == _FieldDescriptor.LABEL_REPEATED:\n    return bool(item[1])\n  elif item[0].cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n    return item[1]._is_present_in_parent\n  else:\n    return True\n\n\ndef _AddListFieldsMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n\n  def ListFields(self):\n    all_fields = [item for item in self._fields.items() if _IsPresent(item)]\n    all_fields.sort(key = lambda item: item[0].number)\n    return all_fields\n\n  cls.ListFields = ListFields\n\n_PROTO3_ERROR_TEMPLATE = \\\n  ('Protocol message %s has no non-repeated submessage field \"%s\" '\n   'nor marked as optional')\n_PROTO2_ERROR_TEMPLATE = 'Protocol message %s has no non-repeated field \"%s\"'\n\ndef _AddHasFieldMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n\n  is_proto3 = (message_descriptor.syntax == \"proto3\")\n  error_msg = _PROTO3_ERROR_TEMPLATE if is_proto3 else _PROTO2_ERROR_TEMPLATE\n\n  hassable_fields = {}\n  for field in message_descriptor.fields:\n    if field.label == _FieldDescriptor.LABEL_REPEATED:\n      continue\n    # For proto3, only submessages and fields inside a oneof have presence.\n    if (is_proto3 and field.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE and\n        not field.containing_oneof):\n      continue\n    hassable_fields[field.name] = field\n\n  # Has methods are supported for oneof descriptors.\n  for oneof in message_descriptor.oneofs:\n    hassable_fields[oneof.name] = oneof\n\n  def HasField(self, field_name):\n    try:\n      field = hassable_fields[field_name]\n    except KeyError:\n      raise ValueError(error_msg % (message_descriptor.full_name, field_name))\n\n    if isinstance(field, descriptor_mod.OneofDescriptor):\n      try:\n        return HasField(self, self._oneofs[field].name)\n      except KeyError:\n        return False\n    else:\n      if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n        value = self._fields.get(field)\n        return value is not None and value._is_present_in_parent\n      else:\n        return field in self._fields\n\n  cls.HasField = HasField\n\n\ndef _AddClearFieldMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def ClearField(self, field_name):\n    try:\n      field = message_descriptor.fields_by_name[field_name]\n    except KeyError:\n      try:\n        field = message_descriptor.oneofs_by_name[field_name]\n        if field in self._oneofs:\n          field = self._oneofs[field]\n        else:\n          return\n      except KeyError:\n        raise ValueError('Protocol message %s has no \"%s\" field.' %\n                         (message_descriptor.name, field_name))\n\n    if field in self._fields:\n      # To match the C++ implementation, we need to invalidate iterators\n      # for map fields when ClearField() happens.\n      if hasattr(self._fields[field], 'InvalidateIterators'):\n        self._fields[field].InvalidateIterators()\n\n      # Note:  If the field is a sub-message, its listener will still point\n      #   at us.  That's fine, because the worst than can happen is that it\n      #   will call _Modified() and invalidate our byte size.  Big deal.\n      del self._fields[field]\n\n      if self._oneofs.get(field.containing_oneof, None) is field:\n        del self._oneofs[field.containing_oneof]\n\n    # Always call _Modified() -- even if nothing was changed, this is\n    # a mutating method, and thus calling it should cause the field to become\n    # present in the parent message.\n    self._Modified()\n\n  cls.ClearField = ClearField\n\n\ndef _AddClearExtensionMethod(cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def ClearExtension(self, extension_handle):\n    extension_dict._VerifyExtensionHandle(self, extension_handle)\n\n    # Similar to ClearField(), above.\n    if extension_handle in self._fields:\n      del self._fields[extension_handle]\n    self._Modified()\n  cls.ClearExtension = ClearExtension\n\n\ndef _AddHasExtensionMethod(cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def HasExtension(self, extension_handle):\n    extension_dict._VerifyExtensionHandle(self, extension_handle)\n    if extension_handle.label == _FieldDescriptor.LABEL_REPEATED:\n      raise KeyError('\"%s\" is repeated.' % extension_handle.full_name)\n\n    if extension_handle.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n      value = self._fields.get(extension_handle)\n      return value is not None and value._is_present_in_parent\n    else:\n      return extension_handle in self._fields\n  cls.HasExtension = HasExtension\n\ndef _InternalUnpackAny(msg):\n  \"\"\"Unpacks Any message and returns the unpacked message.\n\n  This internal method is different from public Any Unpack method which takes\n  the target message as argument. _InternalUnpackAny method does not have\n  target message type and need to find the message type in descriptor pool.\n\n  Args:\n    msg: An Any message to be unpacked.\n\n  Returns:\n    The unpacked message.\n  \"\"\"\n  # TODO(amauryfa): Don't use the factory of generated messages.\n  # To make Any work with custom factories, use the message factory of the\n  # parent message.\n  # pylint: disable=g-import-not-at-top\n  from google.protobuf import symbol_database\n  factory = symbol_database.Default()\n\n  type_url = msg.type_url\n\n  if not type_url:\n    return None\n\n  # TODO(haberman): For now we just strip the hostname.  Better logic will be\n  # required.\n  type_name = type_url.split('/')[-1]\n  descriptor = factory.pool.FindMessageTypeByName(type_name)\n\n  if descriptor is None:\n    return None\n\n  message_class = factory.GetPrototype(descriptor)\n  message = message_class()\n\n  message.ParseFromString(msg.value)\n  return message\n\n\ndef _AddEqualsMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def __eq__(self, other):\n    if (not isinstance(other, message_mod.Message) or\n        other.DESCRIPTOR != self.DESCRIPTOR):\n      return False\n\n    if self is other:\n      return True\n\n    if self.DESCRIPTOR.full_name == _AnyFullTypeName:\n      any_a = _InternalUnpackAny(self)\n      any_b = _InternalUnpackAny(other)\n      if any_a and any_b:\n        return any_a == any_b\n\n    if not self.ListFields() == other.ListFields():\n      return False\n\n    # TODO(jieluo): Fix UnknownFieldSet to consider MessageSet extensions,\n    # then use it for the comparison.\n    unknown_fields = list(self._unknown_fields)\n    unknown_fields.sort()\n    other_unknown_fields = list(other._unknown_fields)\n    other_unknown_fields.sort()\n    return unknown_fields == other_unknown_fields\n\n  cls.__eq__ = __eq__\n\n\ndef _AddStrMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def __str__(self):\n    return text_format.MessageToString(self)\n  cls.__str__ = __str__\n\n\ndef _AddReprMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def __repr__(self):\n    return text_format.MessageToString(self)\n  cls.__repr__ = __repr__\n\n\ndef _AddUnicodeMethod(unused_message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n\n  def __unicode__(self):\n    return text_format.MessageToString(self, as_utf8=True).decode('utf-8')\n  cls.__unicode__ = __unicode__\n\n\ndef _BytesForNonRepeatedElement(value, field_number, field_type):\n  \"\"\"Returns the number of bytes needed to serialize a non-repeated element.\n  The returned byte count includes space for tag information and any\n  other additional space associated with serializing value.\n\n  Args:\n    value: Value we're serializing.\n    field_number: Field number of this value.  (Since the field number\n      is stored as part of a varint-encoded tag, this has an impact\n      on the total bytes required to serialize the value).\n    field_type: The type of the field.  One of the TYPE_* constants\n      within FieldDescriptor.\n  \"\"\"\n  try:\n    fn = type_checkers.TYPE_TO_BYTE_SIZE_FN[field_type]\n    return fn(field_number, value)\n  except KeyError:\n    raise message_mod.EncodeError('Unrecognized field type: %d' % field_type)\n\n\ndef _AddByteSizeMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n\n  def ByteSize(self):\n    if not self._cached_byte_size_dirty:\n      return self._cached_byte_size\n\n    size = 0\n    descriptor = self.DESCRIPTOR\n    if descriptor.GetOptions().map_entry:\n      # Fields of map entry should always be serialized.\n      size = descriptor.fields_by_name['key']._sizer(self.key)\n      size += descriptor.fields_by_name['value']._sizer(self.value)\n    else:\n      for field_descriptor, field_value in self.ListFields():\n        size += field_descriptor._sizer(field_value)\n      for tag_bytes, value_bytes in self._unknown_fields:\n        size += len(tag_bytes) + len(value_bytes)\n\n    self._cached_byte_size = size\n    self._cached_byte_size_dirty = False\n    self._listener_for_children.dirty = False\n    return size\n\n  cls.ByteSize = ByteSize\n\n\ndef _AddSerializeToStringMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n\n  def SerializeToString(self, **kwargs):\n    # Check if the message has all of its required fields set.\n    if not self.IsInitialized():\n      raise message_mod.EncodeError(\n          'Message %s is missing required fields: %s' % (\n          self.DESCRIPTOR.full_name, ','.join(self.FindInitializationErrors())))\n    return self.SerializePartialToString(**kwargs)\n  cls.SerializeToString = SerializeToString\n\n\ndef _AddSerializePartialToStringMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n\n  def SerializePartialToString(self, **kwargs):\n    out = BytesIO()\n    self._InternalSerialize(out.write, **kwargs)\n    return out.getvalue()\n  cls.SerializePartialToString = SerializePartialToString\n\n  def InternalSerialize(self, write_bytes, deterministic=None):\n    if deterministic is None:\n      deterministic = (\n          api_implementation.IsPythonDefaultSerializationDeterministic())\n    else:\n      deterministic = bool(deterministic)\n\n    descriptor = self.DESCRIPTOR\n    if descriptor.GetOptions().map_entry:\n      # Fields of map entry should always be serialized.\n      descriptor.fields_by_name['key']._encoder(\n          write_bytes, self.key, deterministic)\n      descriptor.fields_by_name['value']._encoder(\n          write_bytes, self.value, deterministic)\n    else:\n      for field_descriptor, field_value in self.ListFields():\n        field_descriptor._encoder(write_bytes, field_value, deterministic)\n      for tag_bytes, value_bytes in self._unknown_fields:\n        write_bytes(tag_bytes)\n        write_bytes(value_bytes)\n  cls._InternalSerialize = InternalSerialize\n\n\ndef _AddMergeFromStringMethod(message_descriptor, cls):\n  \"\"\"Helper for _AddMessageMethods().\"\"\"\n  def MergeFromString(self, serialized):\n    serialized = memoryview(serialized)\n    length = len(serialized)\n    try:\n      if self._InternalParse(serialized, 0, length) != length:\n        # The only reason _InternalParse would return early is if it\n        # encountered an end-group tag.\n        raise message_mod.DecodeError('Unexpected end-group tag.')\n    except (IndexError, TypeError):\n      # Now ord(buf[p:p+1]) == ord('') gets TypeError.\n      raise message_mod.DecodeError('Truncated message.')\n    except struct.error as e:\n      raise message_mod.DecodeError(e)\n    return length   # Return this for legacy reasons.\n  cls.MergeFromString = MergeFromString\n\n  local_ReadTag = decoder.ReadTag\n  local_SkipField = decoder.SkipField\n  decoders_by_tag = cls._decoders_by_tag\n\n  def InternalParse(self, buffer, pos, end):\n    \"\"\"Create a message from serialized bytes.\n\n    Args:\n      self: Message, instance of the proto message object.\n      buffer: memoryview of the serialized data.\n      pos: int, position to start in the serialized data.\n      end: int, end position of the serialized data.\n\n    Returns:\n      Message object.\n    \"\"\"\n    # Guard against internal misuse, since this function is called internally\n    # quite extensively, and its easy to accidentally pass bytes.\n    assert isinstance(buffer, memoryview)\n    self._Modified()\n    field_dict = self._fields\n    # pylint: disable=protected-access\n    unknown_field_set = self._unknown_field_set\n    while pos != end:\n      (tag_bytes, new_pos) = local_ReadTag(buffer, pos)\n      field_decoder, field_desc = decoders_by_tag.get(tag_bytes, (None, None))\n      if field_decoder is None:\n        if not self._unknown_fields:   # pylint: disable=protected-access\n          self._unknown_fields = []    # pylint: disable=protected-access\n        if unknown_field_set is None:\n          # pylint: disable=protected-access\n          self._unknown_field_set = containers.UnknownFieldSet()\n          # pylint: disable=protected-access\n          unknown_field_set = self._unknown_field_set\n        # pylint: disable=protected-access\n        (tag, _) = decoder._DecodeVarint(tag_bytes, 0)\n        field_number, wire_type = wire_format.UnpackTag(tag)\n        if field_number == 0:\n          raise message_mod.DecodeError('Field number 0 is illegal.')\n        # TODO(jieluo): remove old_pos.\n        old_pos = new_pos\n        (data, new_pos) = decoder._DecodeUnknownField(\n            buffer, new_pos, wire_type)  # pylint: disable=protected-access\n        if new_pos == -1:\n          return pos\n        # pylint: disable=protected-access\n        unknown_field_set._add(field_number, wire_type, data)\n        # TODO(jieluo): remove _unknown_fields.\n        new_pos = local_SkipField(buffer, old_pos, end, tag_bytes)\n        if new_pos == -1:\n          return pos\n        self._unknown_fields.append(\n            (tag_bytes, buffer[old_pos:new_pos].tobytes()))\n        pos = new_pos\n      else:\n        pos = field_decoder(buffer, new_pos, end, self, field_dict)\n        if field_desc:\n          self._UpdateOneofState(field_desc)\n    return pos\n  cls._InternalParse = InternalParse\n\n\ndef _AddIsInitializedMethod(message_descriptor, cls):\n  \"\"\"Adds the IsInitialized and FindInitializationError methods to the\n  protocol message class.\"\"\"\n\n  required_fields = [field for field in message_descriptor.fields\n                           if field.label == _FieldDescriptor.LABEL_REQUIRED]\n\n  def IsInitialized(self, errors=None):\n    \"\"\"Checks if all required fields of a message are set.\n\n    Args:\n      errors:  A list which, if provided, will be populated with the field\n               paths of all missing required fields.\n\n    Returns:\n      True iff the specified message has all required fields set.\n    \"\"\"\n\n    # Performance is critical so we avoid HasField() and ListFields().\n\n    for field in required_fields:\n      if (field not in self._fields or\n          (field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE and\n           not self._fields[field]._is_present_in_parent)):\n        if errors is not None:\n          errors.extend(self.FindInitializationErrors())\n        return False\n\n    for field, value in list(self._fields.items()):  # dict can change size!\n      if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n        if field.label == _FieldDescriptor.LABEL_REPEATED:\n          if (field.message_type.has_options and\n              field.message_type.GetOptions().map_entry):\n            continue\n          for element in value:\n            if not element.IsInitialized():\n              if errors is not None:\n                errors.extend(self.FindInitializationErrors())\n              return False\n        elif value._is_present_in_parent and not value.IsInitialized():\n          if errors is not None:\n            errors.extend(self.FindInitializationErrors())\n          return False\n\n    return True\n\n  cls.IsInitialized = IsInitialized\n\n  def FindInitializationErrors(self):\n    \"\"\"Finds required fields which are not initialized.\n\n    Returns:\n      A list of strings.  Each string is a path to an uninitialized field from\n      the top-level message, e.g. \"foo.bar[5].baz\".\n    \"\"\"\n\n    errors = []  # simplify things\n\n    for field in required_fields:\n      if not self.HasField(field.name):\n        errors.append(field.name)\n\n    for field, value in self.ListFields():\n      if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n        if field.is_extension:\n          name = '(%s)' % field.full_name\n        else:\n          name = field.name\n\n        if _IsMapField(field):\n          if _IsMessageMapField(field):\n            for key in value:\n              element = value[key]\n              prefix = '%s[%s].' % (name, key)\n              sub_errors = element.FindInitializationErrors()\n              errors += [prefix + error for error in sub_errors]\n          else:\n            # ScalarMaps can't have any initialization errors.\n            pass\n        elif field.label == _FieldDescriptor.LABEL_REPEATED:\n          for i in range(len(value)):\n            element = value[i]\n            prefix = '%s[%d].' % (name, i)\n            sub_errors = element.FindInitializationErrors()\n            errors += [prefix + error for error in sub_errors]\n        else:\n          prefix = name + '.'\n          sub_errors = value.FindInitializationErrors()\n          errors += [prefix + error for error in sub_errors]\n\n    return errors\n\n  cls.FindInitializationErrors = FindInitializationErrors\n\n\ndef _FullyQualifiedClassName(klass):\n  module = klass.__module__\n  name = getattr(klass, '__qualname__', klass.__name__)\n  if module in (None, 'builtins', '__builtin__'):\n    return name\n  return module + '.' + name\n\n\ndef _AddMergeFromMethod(cls):\n  LABEL_REPEATED = _FieldDescriptor.LABEL_REPEATED\n  CPPTYPE_MESSAGE = _FieldDescriptor.CPPTYPE_MESSAGE\n\n  def MergeFrom(self, msg):\n    if not isinstance(msg, cls):\n      raise TypeError(\n          'Parameter to MergeFrom() must be instance of same class: '\n          'expected %s got %s.' % (_FullyQualifiedClassName(cls),\n                                   _FullyQualifiedClassName(msg.__class__)))\n\n    assert msg is not self\n    self._Modified()\n\n    fields = self._fields\n\n    for field, value in msg._fields.items():\n      if field.label == LABEL_REPEATED:\n        field_value = fields.get(field)\n        if field_value is None:\n          # Construct a new object to represent this field.\n          field_value = field._default_constructor(self)\n          fields[field] = field_value\n        field_value.MergeFrom(value)\n      elif field.cpp_type == CPPTYPE_MESSAGE:\n        if value._is_present_in_parent:\n          field_value = fields.get(field)\n          if field_value is None:\n            # Construct a new object to represent this field.\n            field_value = field._default_constructor(self)\n            fields[field] = field_value\n          field_value.MergeFrom(value)\n      else:\n        self._fields[field] = value\n        if field.containing_oneof:\n          self._UpdateOneofState(field)\n\n    if msg._unknown_fields:\n      if not self._unknown_fields:\n        self._unknown_fields = []\n      self._unknown_fields.extend(msg._unknown_fields)\n      # pylint: disable=protected-access\n      if self._unknown_field_set is None:\n        self._unknown_field_set = containers.UnknownFieldSet()\n      self._unknown_field_set._extend(msg._unknown_field_set)\n\n  cls.MergeFrom = MergeFrom\n\n\ndef _AddWhichOneofMethod(message_descriptor, cls):\n  def WhichOneof(self, oneof_name):\n    \"\"\"Returns the name of the currently set field inside a oneof, or None.\"\"\"\n    try:\n      field = message_descriptor.oneofs_by_name[oneof_name]\n    except KeyError:\n      raise ValueError(\n          'Protocol message has no oneof \"%s\" field.' % oneof_name)\n\n    nested_field = self._oneofs.get(field, None)\n    if nested_field is not None and self.HasField(nested_field.name):\n      return nested_field.name\n    else:\n      return None\n\n  cls.WhichOneof = WhichOneof\n\n\ndef _Clear(self):\n  # Clear fields.\n  self._fields = {}\n  self._unknown_fields = ()\n  # pylint: disable=protected-access\n  if self._unknown_field_set is not None:\n    self._unknown_field_set._clear()\n    self._unknown_field_set = None\n\n  self._oneofs = {}\n  self._Modified()\n\n\ndef _UnknownFields(self):\n  if self._unknown_field_set is None:  # pylint: disable=protected-access\n    # pylint: disable=protected-access\n    self._unknown_field_set = containers.UnknownFieldSet()\n  return self._unknown_field_set    # pylint: disable=protected-access\n\n\ndef _DiscardUnknownFields(self):\n  self._unknown_fields = []\n  self._unknown_field_set = None      # pylint: disable=protected-access\n  for field, value in self.ListFields():\n    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:\n      if _IsMapField(field):\n        if _IsMessageMapField(field):\n          for key in value:\n            value[key].DiscardUnknownFields()\n      elif field.label == _FieldDescriptor.LABEL_REPEATED:\n        for sub_message in value:\n          sub_message.DiscardUnknownFields()\n      else:\n        value.DiscardUnknownFields()\n\n\ndef _SetListener(self, listener):\n  if listener is None:\n    self._listener = message_listener_mod.NullMessageListener()\n  else:\n    self._listener = listener\n\n\ndef _AddMessageMethods(message_descriptor, cls):\n  \"\"\"Adds implementations of all Message methods to cls.\"\"\"\n  _AddListFieldsMethod(message_descriptor, cls)\n  _AddHasFieldMethod(message_descriptor, cls)\n  _AddClearFieldMethod(message_descriptor, cls)\n  if message_descriptor.is_extendable:\n    _AddClearExtensionMethod(cls)\n    _AddHasExtensionMethod(cls)\n  _AddEqualsMethod(message_descriptor, cls)\n  _AddStrMethod(message_descriptor, cls)\n  _AddReprMethod(message_descriptor, cls)\n  _AddUnicodeMethod(message_descriptor, cls)\n  _AddByteSizeMethod(message_descriptor, cls)\n  _AddSerializeToStringMethod(message_descriptor, cls)\n  _AddSerializePartialToStringMethod(message_descriptor, cls)\n  _AddMergeFromStringMethod(message_descriptor, cls)\n  _AddIsInitializedMethod(message_descriptor, cls)\n  _AddMergeFromMethod(cls)\n  _AddWhichOneofMethod(message_descriptor, cls)\n  # Adds methods which do not depend on cls.\n  cls.Clear = _Clear\n  cls.UnknownFields = _UnknownFields\n  cls.DiscardUnknownFields = _DiscardUnknownFields\n  cls._SetListener = _SetListener\n\n\ndef _AddPrivateHelperMethods(message_descriptor, cls):\n  \"\"\"Adds implementation of private helper methods to cls.\"\"\"\n\n  def Modified(self):\n    \"\"\"Sets the _cached_byte_size_dirty bit to true,\n    and propagates this to our listener iff this was a state change.\n    \"\"\"\n\n    # Note:  Some callers check _cached_byte_size_dirty before calling\n    #   _Modified() as an extra optimization.  So, if this method is ever\n    #   changed such that it does stuff even when _cached_byte_size_dirty is\n    #   already true, the callers need to be updated.\n    if not self._cached_byte_size_dirty:\n      self._cached_byte_size_dirty = True\n      self._listener_for_children.dirty = True\n      self._is_present_in_parent = True\n      self._listener.Modified()\n\n  def _UpdateOneofState(self, field):\n    \"\"\"Sets field as the active field in its containing oneof.\n\n    Will also delete currently active field in the oneof, if it is different\n    from the argument. Does not mark the message as modified.\n    \"\"\"\n    other_field = self._oneofs.setdefault(field.containing_oneof, field)\n    if other_field is not field:\n      del self._fields[other_field]\n      self._oneofs[field.containing_oneof] = field\n\n  cls._Modified = Modified\n  cls.SetInParent = Modified\n  cls._UpdateOneofState = _UpdateOneofState\n\n\nclass _Listener(object):\n\n  \"\"\"MessageListener implementation that a parent message registers with its\n  child message.\n\n  In order to support semantics like:\n\n    foo.bar.baz.qux = 23\n    assert foo.HasField('bar')\n\n  ...child objects must have back references to their parents.\n  This helper class is at the heart of this support.\n  \"\"\"\n\n  def __init__(self, parent_message):\n    \"\"\"Args:\n      parent_message: The message whose _Modified() method we should call when\n        we receive Modified() messages.\n    \"\"\"\n    # This listener establishes a back reference from a child (contained) object\n    # to its parent (containing) object.  We make this a weak reference to avoid\n    # creating cyclic garbage when the client finishes with the 'parent' object\n    # in the tree.\n    if isinstance(parent_message, weakref.ProxyType):\n      self._parent_message_weakref = parent_message\n    else:\n      self._parent_message_weakref = weakref.proxy(parent_message)\n\n    # As an optimization, we also indicate directly on the listener whether\n    # or not the parent message is dirty.  This way we can avoid traversing\n    # up the tree in the common case.\n    self.dirty = False\n\n  def Modified(self):\n    if self.dirty:\n      return\n    try:\n      # Propagate the signal to our parents iff this is the first field set.\n      self._parent_message_weakref._Modified()\n    except ReferenceError:\n      # We can get here if a client has kept a reference to a child object,\n      # and is now setting a field on it, but the child's parent has been\n      # garbage-collected.  This is not an error.\n      pass\n\n\nclass _OneofListener(_Listener):\n  \"\"\"Special listener implementation for setting composite oneof fields.\"\"\"\n\n  def __init__(self, parent_message, field):\n    \"\"\"Args:\n      parent_message: The message whose _Modified() method we should call when\n        we receive Modified() messages.\n      field: The descriptor of the field being set in the parent message.\n    \"\"\"\n    super(_OneofListener, self).__init__(parent_message)\n    self._field = field\n\n  def Modified(self):\n    \"\"\"Also updates the state of the containing oneof in the parent message.\"\"\"\n    try:\n      self._parent_message_weakref._UpdateOneofState(self._field)\n      super(_OneofListener, self).Modified()\n    except ReferenceError:\n      pass\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/type_checkers.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Provides type checking routines.\n\nThis module defines type checking utilities in the forms of dictionaries:\n\nVALUE_CHECKERS: A dictionary of field types and a value validation object.\nTYPE_TO_BYTE_SIZE_FN: A dictionary with field types and a size computing\n  function.\nTYPE_TO_SERIALIZE_METHOD: A dictionary with field types and serialization\n  function.\nFIELD_TYPE_TO_WIRE_TYPE: A dictionary with field typed and their\n  corresponding wire types.\nTYPE_TO_DESERIALIZE_METHOD: A dictionary with field types and deserialization\n  function.\n\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\nimport ctypes\nimport numbers\n\nfrom google.protobuf.internal import decoder\nfrom google.protobuf.internal import encoder\nfrom google.protobuf.internal import wire_format\nfrom google.protobuf import descriptor\n\n_FieldDescriptor = descriptor.FieldDescriptor\n\n\ndef TruncateToFourByteFloat(original):\n  return ctypes.c_float(original).value\n\n\ndef ToShortestFloat(original):\n  \"\"\"Returns the shortest float that has same value in wire.\"\"\"\n  # All 4 byte floats have between 6 and 9 significant digits, so we\n  # start with 6 as the lower bound.\n  # It has to be iterative because use '.9g' directly can not get rid\n  # of the noises for most values. For example if set a float_field=0.9\n  # use '.9g' will print 0.899999976.\n  precision = 6\n  rounded = float('{0:.{1}g}'.format(original, precision))\n  while TruncateToFourByteFloat(rounded) != original:\n    precision += 1\n    rounded = float('{0:.{1}g}'.format(original, precision))\n  return rounded\n\n\ndef SupportsOpenEnums(field_descriptor):\n  return field_descriptor.containing_type.syntax == 'proto3'\n\n\ndef GetTypeChecker(field):\n  \"\"\"Returns a type checker for a message field of the specified types.\n\n  Args:\n    field: FieldDescriptor object for this field.\n\n  Returns:\n    An instance of TypeChecker which can be used to verify the types\n    of values assigned to a field of the specified type.\n  \"\"\"\n  if (field.cpp_type == _FieldDescriptor.CPPTYPE_STRING and\n      field.type == _FieldDescriptor.TYPE_STRING):\n    return UnicodeValueChecker()\n  if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM:\n    if SupportsOpenEnums(field):\n      # When open enums are supported, any int32 can be assigned.\n      return _VALUE_CHECKERS[_FieldDescriptor.CPPTYPE_INT32]\n    else:\n      return EnumValueChecker(field.enum_type)\n  return _VALUE_CHECKERS[field.cpp_type]\n\n\n# None of the typecheckers below make any attempt to guard against people\n# subclassing builtin types and doing weird things.  We're not trying to\n# protect against malicious clients here, just people accidentally shooting\n# themselves in the foot in obvious ways.\nclass TypeChecker(object):\n\n  \"\"\"Type checker used to catch type errors as early as possible\n  when the client is setting scalar fields in protocol messages.\n  \"\"\"\n\n  def __init__(self, *acceptable_types):\n    self._acceptable_types = acceptable_types\n\n  def CheckValue(self, proposed_value):\n    \"\"\"Type check the provided value and return it.\n\n    The returned value might have been normalized to another type.\n    \"\"\"\n    if not isinstance(proposed_value, self._acceptable_types):\n      message = ('%.1024r has type %s, but expected one of: %s' %\n                 (proposed_value, type(proposed_value), self._acceptable_types))\n      raise TypeError(message)\n    return proposed_value\n\n\nclass TypeCheckerWithDefault(TypeChecker):\n\n  def __init__(self, default_value, *acceptable_types):\n    TypeChecker.__init__(self, *acceptable_types)\n    self._default_value = default_value\n\n  def DefaultValue(self):\n    return self._default_value\n\n\nclass BoolValueChecker(object):\n  \"\"\"Type checker used for bool fields.\"\"\"\n\n  def CheckValue(self, proposed_value):\n    if not hasattr(proposed_value, '__index__') or (\n        type(proposed_value).__module__ == 'numpy' and\n        type(proposed_value).__name__ == 'ndarray'):\n      message = ('%.1024r has type %s, but expected one of: %s' %\n                 (proposed_value, type(proposed_value), (bool, int)))\n      raise TypeError(message)\n    return bool(proposed_value)\n\n  def DefaultValue(self):\n    return False\n\n\n# IntValueChecker and its subclasses perform integer type-checks\n# and bounds-checks.\nclass IntValueChecker(object):\n\n  \"\"\"Checker used for integer fields.  Performs type-check and range check.\"\"\"\n\n  def CheckValue(self, proposed_value):\n    if not hasattr(proposed_value, '__index__') or (\n        type(proposed_value).__module__ == 'numpy' and\n        type(proposed_value).__name__ == 'ndarray'):\n      message = ('%.1024r has type %s, but expected one of: %s' %\n                 (proposed_value, type(proposed_value), (int,)))\n      raise TypeError(message)\n\n    if not self._MIN <= int(proposed_value) <= self._MAX:\n      raise ValueError('Value out of range: %d' % proposed_value)\n    # We force all values to int to make alternate implementations where the\n    # distinction is more significant (e.g. the C++ implementation) simpler.\n    proposed_value = int(proposed_value)\n    return proposed_value\n\n  def DefaultValue(self):\n    return 0\n\n\nclass EnumValueChecker(object):\n\n  \"\"\"Checker used for enum fields.  Performs type-check and range check.\"\"\"\n\n  def __init__(self, enum_type):\n    self._enum_type = enum_type\n\n  def CheckValue(self, proposed_value):\n    if not isinstance(proposed_value, numbers.Integral):\n      message = ('%.1024r has type %s, but expected one of: %s' %\n                 (proposed_value, type(proposed_value), (int,)))\n      raise TypeError(message)\n    if int(proposed_value) not in self._enum_type.values_by_number:\n      raise ValueError('Unknown enum value: %d' % proposed_value)\n    return proposed_value\n\n  def DefaultValue(self):\n    return self._enum_type.values[0].number\n\n\nclass UnicodeValueChecker(object):\n\n  \"\"\"Checker used for string fields.\n\n  Always returns a unicode value, even if the input is of type str.\n  \"\"\"\n\n  def CheckValue(self, proposed_value):\n    if not isinstance(proposed_value, (bytes, str)):\n      message = ('%.1024r has type %s, but expected one of: %s' %\n                 (proposed_value, type(proposed_value), (bytes, str)))\n      raise TypeError(message)\n\n    # If the value is of type 'bytes' make sure that it is valid UTF-8 data.\n    if isinstance(proposed_value, bytes):\n      try:\n        proposed_value = proposed_value.decode('utf-8')\n      except UnicodeDecodeError:\n        raise ValueError('%.1024r has type bytes, but isn\\'t valid UTF-8 '\n                         'encoding. Non-UTF-8 strings must be converted to '\n                         'unicode objects before being added.' %\n                         (proposed_value))\n    else:\n      try:\n        proposed_value.encode('utf8')\n      except UnicodeEncodeError:\n        raise ValueError('%.1024r isn\\'t a valid unicode string and '\n                         'can\\'t be encoded in UTF-8.'%\n                         (proposed_value))\n\n    return proposed_value\n\n  def DefaultValue(self):\n    return u\"\"\n\n\nclass Int32ValueChecker(IntValueChecker):\n  # We're sure to use ints instead of longs here since comparison may be more\n  # efficient.\n  _MIN = -2147483648\n  _MAX = 2147483647\n\n\nclass Uint32ValueChecker(IntValueChecker):\n  _MIN = 0\n  _MAX = (1 << 32) - 1\n\n\nclass Int64ValueChecker(IntValueChecker):\n  _MIN = -(1 << 63)\n  _MAX = (1 << 63) - 1\n\n\nclass Uint64ValueChecker(IntValueChecker):\n  _MIN = 0\n  _MAX = (1 << 64) - 1\n\n\n# The max 4 bytes float is about 3.4028234663852886e+38\n_FLOAT_MAX = float.fromhex('0x1.fffffep+127')\n_FLOAT_MIN = -_FLOAT_MAX\n_INF = float('inf')\n_NEG_INF = float('-inf')\n\n\nclass DoubleValueChecker(object):\n  \"\"\"Checker used for double fields.\n\n  Performs type-check and range check.\n  \"\"\"\n\n  def CheckValue(self, proposed_value):\n    \"\"\"Check and convert proposed_value to float.\"\"\"\n    if (not hasattr(proposed_value, '__float__') and\n        not hasattr(proposed_value, '__index__')) or (\n            type(proposed_value).__module__ == 'numpy' and\n            type(proposed_value).__name__ == 'ndarray'):\n      message = ('%.1024r has type %s, but expected one of: int, float' %\n                 (proposed_value, type(proposed_value)))\n      raise TypeError(message)\n    return float(proposed_value)\n\n  def DefaultValue(self):\n    return 0.0\n\n\nclass FloatValueChecker(DoubleValueChecker):\n  \"\"\"Checker used for float fields.\n\n  Performs type-check and range check.\n\n  Values exceeding a 32-bit float will be converted to inf/-inf.\n  \"\"\"\n\n  def CheckValue(self, proposed_value):\n    \"\"\"Check and convert proposed_value to float.\"\"\"\n    converted_value = super().CheckValue(proposed_value)\n    # This inf rounding matches the C++ proto SafeDoubleToFloat logic.\n    if converted_value > _FLOAT_MAX:\n      return _INF\n    if converted_value < _FLOAT_MIN:\n      return _NEG_INF\n\n    return TruncateToFourByteFloat(converted_value)\n\n# Type-checkers for all scalar CPPTYPEs.\n_VALUE_CHECKERS = {\n    _FieldDescriptor.CPPTYPE_INT32: Int32ValueChecker(),\n    _FieldDescriptor.CPPTYPE_INT64: Int64ValueChecker(),\n    _FieldDescriptor.CPPTYPE_UINT32: Uint32ValueChecker(),\n    _FieldDescriptor.CPPTYPE_UINT64: Uint64ValueChecker(),\n    _FieldDescriptor.CPPTYPE_DOUBLE: DoubleValueChecker(),\n    _FieldDescriptor.CPPTYPE_FLOAT: FloatValueChecker(),\n    _FieldDescriptor.CPPTYPE_BOOL: BoolValueChecker(),\n    _FieldDescriptor.CPPTYPE_STRING: TypeCheckerWithDefault(b'', bytes),\n}\n\n\n# Map from field type to a function F, such that F(field_num, value)\n# gives the total byte size for a value of the given type.  This\n# byte size includes tag information and any other additional space\n# associated with serializing \"value\".\nTYPE_TO_BYTE_SIZE_FN = {\n    _FieldDescriptor.TYPE_DOUBLE: wire_format.DoubleByteSize,\n    _FieldDescriptor.TYPE_FLOAT: wire_format.FloatByteSize,\n    _FieldDescriptor.TYPE_INT64: wire_format.Int64ByteSize,\n    _FieldDescriptor.TYPE_UINT64: wire_format.UInt64ByteSize,\n    _FieldDescriptor.TYPE_INT32: wire_format.Int32ByteSize,\n    _FieldDescriptor.TYPE_FIXED64: wire_format.Fixed64ByteSize,\n    _FieldDescriptor.TYPE_FIXED32: wire_format.Fixed32ByteSize,\n    _FieldDescriptor.TYPE_BOOL: wire_format.BoolByteSize,\n    _FieldDescriptor.TYPE_STRING: wire_format.StringByteSize,\n    _FieldDescriptor.TYPE_GROUP: wire_format.GroupByteSize,\n    _FieldDescriptor.TYPE_MESSAGE: wire_format.MessageByteSize,\n    _FieldDescriptor.TYPE_BYTES: wire_format.BytesByteSize,\n    _FieldDescriptor.TYPE_UINT32: wire_format.UInt32ByteSize,\n    _FieldDescriptor.TYPE_ENUM: wire_format.EnumByteSize,\n    _FieldDescriptor.TYPE_SFIXED32: wire_format.SFixed32ByteSize,\n    _FieldDescriptor.TYPE_SFIXED64: wire_format.SFixed64ByteSize,\n    _FieldDescriptor.TYPE_SINT32: wire_format.SInt32ByteSize,\n    _FieldDescriptor.TYPE_SINT64: wire_format.SInt64ByteSize\n    }\n\n\n# Maps from field types to encoder constructors.\nTYPE_TO_ENCODER = {\n    _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleEncoder,\n    _FieldDescriptor.TYPE_FLOAT: encoder.FloatEncoder,\n    _FieldDescriptor.TYPE_INT64: encoder.Int64Encoder,\n    _FieldDescriptor.TYPE_UINT64: encoder.UInt64Encoder,\n    _FieldDescriptor.TYPE_INT32: encoder.Int32Encoder,\n    _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Encoder,\n    _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Encoder,\n    _FieldDescriptor.TYPE_BOOL: encoder.BoolEncoder,\n    _FieldDescriptor.TYPE_STRING: encoder.StringEncoder,\n    _FieldDescriptor.TYPE_GROUP: encoder.GroupEncoder,\n    _FieldDescriptor.TYPE_MESSAGE: encoder.MessageEncoder,\n    _FieldDescriptor.TYPE_BYTES: encoder.BytesEncoder,\n    _FieldDescriptor.TYPE_UINT32: encoder.UInt32Encoder,\n    _FieldDescriptor.TYPE_ENUM: encoder.EnumEncoder,\n    _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Encoder,\n    _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Encoder,\n    _FieldDescriptor.TYPE_SINT32: encoder.SInt32Encoder,\n    _FieldDescriptor.TYPE_SINT64: encoder.SInt64Encoder,\n    }\n\n\n# Maps from field types to sizer constructors.\nTYPE_TO_SIZER = {\n    _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleSizer,\n    _FieldDescriptor.TYPE_FLOAT: encoder.FloatSizer,\n    _FieldDescriptor.TYPE_INT64: encoder.Int64Sizer,\n    _FieldDescriptor.TYPE_UINT64: encoder.UInt64Sizer,\n    _FieldDescriptor.TYPE_INT32: encoder.Int32Sizer,\n    _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Sizer,\n    _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Sizer,\n    _FieldDescriptor.TYPE_BOOL: encoder.BoolSizer,\n    _FieldDescriptor.TYPE_STRING: encoder.StringSizer,\n    _FieldDescriptor.TYPE_GROUP: encoder.GroupSizer,\n    _FieldDescriptor.TYPE_MESSAGE: encoder.MessageSizer,\n    _FieldDescriptor.TYPE_BYTES: encoder.BytesSizer,\n    _FieldDescriptor.TYPE_UINT32: encoder.UInt32Sizer,\n    _FieldDescriptor.TYPE_ENUM: encoder.EnumSizer,\n    _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Sizer,\n    _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Sizer,\n    _FieldDescriptor.TYPE_SINT32: encoder.SInt32Sizer,\n    _FieldDescriptor.TYPE_SINT64: encoder.SInt64Sizer,\n    }\n\n\n# Maps from field type to a decoder constructor.\nTYPE_TO_DECODER = {\n    _FieldDescriptor.TYPE_DOUBLE: decoder.DoubleDecoder,\n    _FieldDescriptor.TYPE_FLOAT: decoder.FloatDecoder,\n    _FieldDescriptor.TYPE_INT64: decoder.Int64Decoder,\n    _FieldDescriptor.TYPE_UINT64: decoder.UInt64Decoder,\n    _FieldDescriptor.TYPE_INT32: decoder.Int32Decoder,\n    _FieldDescriptor.TYPE_FIXED64: decoder.Fixed64Decoder,\n    _FieldDescriptor.TYPE_FIXED32: decoder.Fixed32Decoder,\n    _FieldDescriptor.TYPE_BOOL: decoder.BoolDecoder,\n    _FieldDescriptor.TYPE_STRING: decoder.StringDecoder,\n    _FieldDescriptor.TYPE_GROUP: decoder.GroupDecoder,\n    _FieldDescriptor.TYPE_MESSAGE: decoder.MessageDecoder,\n    _FieldDescriptor.TYPE_BYTES: decoder.BytesDecoder,\n    _FieldDescriptor.TYPE_UINT32: decoder.UInt32Decoder,\n    _FieldDescriptor.TYPE_ENUM: decoder.EnumDecoder,\n    _FieldDescriptor.TYPE_SFIXED32: decoder.SFixed32Decoder,\n    _FieldDescriptor.TYPE_SFIXED64: decoder.SFixed64Decoder,\n    _FieldDescriptor.TYPE_SINT32: decoder.SInt32Decoder,\n    _FieldDescriptor.TYPE_SINT64: decoder.SInt64Decoder,\n    }\n\n# Maps from field type to expected wiretype.\nFIELD_TYPE_TO_WIRE_TYPE = {\n    _FieldDescriptor.TYPE_DOUBLE: wire_format.WIRETYPE_FIXED64,\n    _FieldDescriptor.TYPE_FLOAT: wire_format.WIRETYPE_FIXED32,\n    _FieldDescriptor.TYPE_INT64: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_UINT64: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_INT32: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_FIXED64: wire_format.WIRETYPE_FIXED64,\n    _FieldDescriptor.TYPE_FIXED32: wire_format.WIRETYPE_FIXED32,\n    _FieldDescriptor.TYPE_BOOL: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_STRING:\n      wire_format.WIRETYPE_LENGTH_DELIMITED,\n    _FieldDescriptor.TYPE_GROUP: wire_format.WIRETYPE_START_GROUP,\n    _FieldDescriptor.TYPE_MESSAGE:\n      wire_format.WIRETYPE_LENGTH_DELIMITED,\n    _FieldDescriptor.TYPE_BYTES:\n      wire_format.WIRETYPE_LENGTH_DELIMITED,\n    _FieldDescriptor.TYPE_UINT32: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_ENUM: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_SFIXED32: wire_format.WIRETYPE_FIXED32,\n    _FieldDescriptor.TYPE_SFIXED64: wire_format.WIRETYPE_FIXED64,\n    _FieldDescriptor.TYPE_SINT32: wire_format.WIRETYPE_VARINT,\n    _FieldDescriptor.TYPE_SINT64: wire_format.WIRETYPE_VARINT,\n    }\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/well_known_types.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Contains well known classes.\n\nThis files defines well known classes which need extra maintenance including:\n  - Any\n  - Duration\n  - FieldMask\n  - Struct\n  - Timestamp\n\"\"\"\n\n__author__ = 'jieluo@google.com (Jie Luo)'\n\nimport calendar\nimport collections.abc\nimport datetime\n\nfrom google.protobuf.descriptor import FieldDescriptor\n\n_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S'\n_NANOS_PER_SECOND = 1000000000\n_NANOS_PER_MILLISECOND = 1000000\n_NANOS_PER_MICROSECOND = 1000\n_MILLIS_PER_SECOND = 1000\n_MICROS_PER_SECOND = 1000000\n_SECONDS_PER_DAY = 24 * 3600\n_DURATION_SECONDS_MAX = 315576000000\n\n\nclass Any(object):\n  \"\"\"Class for Any Message type.\"\"\"\n\n  __slots__ = ()\n\n  def Pack(self, msg, type_url_prefix='type.googleapis.com/',\n           deterministic=None):\n    \"\"\"Packs the specified message into current Any message.\"\"\"\n    if len(type_url_prefix) < 1 or type_url_prefix[-1] != '/':\n      self.type_url = '%s/%s' % (type_url_prefix, msg.DESCRIPTOR.full_name)\n    else:\n      self.type_url = '%s%s' % (type_url_prefix, msg.DESCRIPTOR.full_name)\n    self.value = msg.SerializeToString(deterministic=deterministic)\n\n  def Unpack(self, msg):\n    \"\"\"Unpacks the current Any message into specified message.\"\"\"\n    descriptor = msg.DESCRIPTOR\n    if not self.Is(descriptor):\n      return False\n    msg.ParseFromString(self.value)\n    return True\n\n  def TypeName(self):\n    \"\"\"Returns the protobuf type name of the inner message.\"\"\"\n    # Only last part is to be used: b/25630112\n    return self.type_url.split('/')[-1]\n\n  def Is(self, descriptor):\n    \"\"\"Checks if this Any represents the given protobuf type.\"\"\"\n    return '/' in self.type_url and self.TypeName() == descriptor.full_name\n\n\n_EPOCH_DATETIME_NAIVE = datetime.datetime.utcfromtimestamp(0)\n_EPOCH_DATETIME_AWARE = datetime.datetime.fromtimestamp(\n    0, tz=datetime.timezone.utc)\n\n\nclass Timestamp(object):\n  \"\"\"Class for Timestamp message type.\"\"\"\n\n  __slots__ = ()\n\n  def ToJsonString(self):\n    \"\"\"Converts Timestamp to RFC 3339 date string format.\n\n    Returns:\n      A string converted from timestamp. The string is always Z-normalized\n      and uses 3, 6 or 9 fractional digits as required to represent the\n      exact time. Example of the return format: '1972-01-01T10:00:20.021Z'\n    \"\"\"\n    nanos = self.nanos % _NANOS_PER_SECOND\n    total_sec = self.seconds + (self.nanos - nanos) // _NANOS_PER_SECOND\n    seconds = total_sec % _SECONDS_PER_DAY\n    days = (total_sec - seconds) // _SECONDS_PER_DAY\n    dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(days, seconds)\n\n    result = dt.isoformat()\n    if (nanos % 1e9) == 0:\n      # If there are 0 fractional digits, the fractional\n      # point '.' should be omitted when serializing.\n      return result + 'Z'\n    if (nanos % 1e6) == 0:\n      # Serialize 3 fractional digits.\n      return result + '.%03dZ' % (nanos / 1e6)\n    if (nanos % 1e3) == 0:\n      # Serialize 6 fractional digits.\n      return result + '.%06dZ' % (nanos / 1e3)\n    # Serialize 9 fractional digits.\n    return result + '.%09dZ' % nanos\n\n  def FromJsonString(self, value):\n    \"\"\"Parse a RFC 3339 date string format to Timestamp.\n\n    Args:\n      value: A date string. Any fractional digits (or none) and any offset are\n          accepted as long as they fit into nano-seconds precision.\n          Example of accepted format: '1972-01-01T10:00:20.021-05:00'\n\n    Raises:\n      ValueError: On parsing problems.\n    \"\"\"\n    if not isinstance(value, str):\n      raise ValueError('Timestamp JSON value not a string: {!r}'.format(value))\n    timezone_offset = value.find('Z')\n    if timezone_offset == -1:\n      timezone_offset = value.find('+')\n    if timezone_offset == -1:\n      timezone_offset = value.rfind('-')\n    if timezone_offset == -1:\n      raise ValueError(\n          'Failed to parse timestamp: missing valid timezone offset.')\n    time_value = value[0:timezone_offset]\n    # Parse datetime and nanos.\n    point_position = time_value.find('.')\n    if point_position == -1:\n      second_value = time_value\n      nano_value = ''\n    else:\n      second_value = time_value[:point_position]\n      nano_value = time_value[point_position + 1:]\n    if 't' in second_value:\n      raise ValueError(\n          'time data \\'{0}\\' does not match format \\'%Y-%m-%dT%H:%M:%S\\', '\n          'lowercase \\'t\\' is not accepted'.format(second_value))\n    date_object = datetime.datetime.strptime(second_value, _TIMESTAMPFOMAT)\n    td = date_object - datetime.datetime(1970, 1, 1)\n    seconds = td.seconds + td.days * _SECONDS_PER_DAY\n    if len(nano_value) > 9:\n      raise ValueError(\n          'Failed to parse Timestamp: nanos {0} more than '\n          '9 fractional digits.'.format(nano_value))\n    if nano_value:\n      nanos = round(float('0.' + nano_value) * 1e9)\n    else:\n      nanos = 0\n    # Parse timezone offsets.\n    if value[timezone_offset] == 'Z':\n      if len(value) != timezone_offset + 1:\n        raise ValueError('Failed to parse timestamp: invalid trailing'\n                         ' data {0}.'.format(value))\n    else:\n      timezone = value[timezone_offset:]\n      pos = timezone.find(':')\n      if pos == -1:\n        raise ValueError(\n            'Invalid timezone offset value: {0}.'.format(timezone))\n      if timezone[0] == '+':\n        seconds -= (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60\n      else:\n        seconds += (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60\n    # Set seconds and nanos\n    self.seconds = int(seconds)\n    self.nanos = int(nanos)\n\n  def GetCurrentTime(self):\n    \"\"\"Get the current UTC into Timestamp.\"\"\"\n    self.FromDatetime(datetime.datetime.utcnow())\n\n  def ToNanoseconds(self):\n    \"\"\"Converts Timestamp to nanoseconds since epoch.\"\"\"\n    return self.seconds * _NANOS_PER_SECOND + self.nanos\n\n  def ToMicroseconds(self):\n    \"\"\"Converts Timestamp to microseconds since epoch.\"\"\"\n    return (self.seconds * _MICROS_PER_SECOND +\n            self.nanos // _NANOS_PER_MICROSECOND)\n\n  def ToMilliseconds(self):\n    \"\"\"Converts Timestamp to milliseconds since epoch.\"\"\"\n    return (self.seconds * _MILLIS_PER_SECOND +\n            self.nanos // _NANOS_PER_MILLISECOND)\n\n  def ToSeconds(self):\n    \"\"\"Converts Timestamp to seconds since epoch.\"\"\"\n    return self.seconds\n\n  def FromNanoseconds(self, nanos):\n    \"\"\"Converts nanoseconds since epoch to Timestamp.\"\"\"\n    self.seconds = nanos // _NANOS_PER_SECOND\n    self.nanos = nanos % _NANOS_PER_SECOND\n\n  def FromMicroseconds(self, micros):\n    \"\"\"Converts microseconds since epoch to Timestamp.\"\"\"\n    self.seconds = micros // _MICROS_PER_SECOND\n    self.nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND\n\n  def FromMilliseconds(self, millis):\n    \"\"\"Converts milliseconds since epoch to Timestamp.\"\"\"\n    self.seconds = millis // _MILLIS_PER_SECOND\n    self.nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND\n\n  def FromSeconds(self, seconds):\n    \"\"\"Converts seconds since epoch to Timestamp.\"\"\"\n    self.seconds = seconds\n    self.nanos = 0\n\n  def ToDatetime(self, tzinfo=None):\n    \"\"\"Converts Timestamp to a datetime.\n\n    Args:\n      tzinfo: A datetime.tzinfo subclass; defaults to None.\n\n    Returns:\n      If tzinfo is None, returns a timezone-naive UTC datetime (with no timezone\n      information, i.e. not aware that it's UTC).\n\n      Otherwise, returns a timezone-aware datetime in the input timezone.\n    \"\"\"\n    delta = datetime.timedelta(\n        seconds=self.seconds,\n        microseconds=_RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND))\n    if tzinfo is None:\n      return _EPOCH_DATETIME_NAIVE + delta\n    else:\n      return _EPOCH_DATETIME_AWARE.astimezone(tzinfo) + delta\n\n  def FromDatetime(self, dt):\n    \"\"\"Converts datetime to Timestamp.\n\n    Args:\n      dt: A datetime. If it's timezone-naive, it's assumed to be in UTC.\n    \"\"\"\n    # Using this guide: http://wiki.python.org/moin/WorkingWithTime\n    # And this conversion guide: http://docs.python.org/library/time.html\n\n    # Turn the date parameter into a tuple (struct_time) that can then be\n    # manipulated into a long value of seconds.  During the conversion from\n    # struct_time to long, the source date in UTC, and so it follows that the\n    # correct transformation is calendar.timegm()\n    self.seconds = calendar.timegm(dt.utctimetuple())\n    self.nanos = dt.microsecond * _NANOS_PER_MICROSECOND\n\n\nclass Duration(object):\n  \"\"\"Class for Duration message type.\"\"\"\n\n  __slots__ = ()\n\n  def ToJsonString(self):\n    \"\"\"Converts Duration to string format.\n\n    Returns:\n      A string converted from self. The string format will contains\n      3, 6, or 9 fractional digits depending on the precision required to\n      represent the exact Duration value. For example: \"1s\", \"1.010s\",\n      \"1.000000100s\", \"-3.100s\"\n    \"\"\"\n    _CheckDurationValid(self.seconds, self.nanos)\n    if self.seconds < 0 or self.nanos < 0:\n      result = '-'\n      seconds = - self.seconds + int((0 - self.nanos) // 1e9)\n      nanos = (0 - self.nanos) % 1e9\n    else:\n      result = ''\n      seconds = self.seconds + int(self.nanos // 1e9)\n      nanos = self.nanos % 1e9\n    result += '%d' % seconds\n    if (nanos % 1e9) == 0:\n      # If there are 0 fractional digits, the fractional\n      # point '.' should be omitted when serializing.\n      return result + 's'\n    if (nanos % 1e6) == 0:\n      # Serialize 3 fractional digits.\n      return result + '.%03ds' % (nanos / 1e6)\n    if (nanos % 1e3) == 0:\n      # Serialize 6 fractional digits.\n      return result + '.%06ds' % (nanos / 1e3)\n    # Serialize 9 fractional digits.\n    return result + '.%09ds' % nanos\n\n  def FromJsonString(self, value):\n    \"\"\"Converts a string to Duration.\n\n    Args:\n      value: A string to be converted. The string must end with 's'. Any\n          fractional digits (or none) are accepted as long as they fit into\n          precision. For example: \"1s\", \"1.01s\", \"1.0000001s\", \"-3.100s\n\n    Raises:\n      ValueError: On parsing problems.\n    \"\"\"\n    if not isinstance(value, str):\n      raise ValueError('Duration JSON value not a string: {!r}'.format(value))\n    if len(value) < 1 or value[-1] != 's':\n      raise ValueError(\n          'Duration must end with letter \"s\": {0}.'.format(value))\n    try:\n      pos = value.find('.')\n      if pos == -1:\n        seconds = int(value[:-1])\n        nanos = 0\n      else:\n        seconds = int(value[:pos])\n        if value[0] == '-':\n          nanos = int(round(float('-0{0}'.format(value[pos: -1])) *1e9))\n        else:\n          nanos = int(round(float('0{0}'.format(value[pos: -1])) *1e9))\n      _CheckDurationValid(seconds, nanos)\n      self.seconds = seconds\n      self.nanos = nanos\n    except ValueError as e:\n      raise ValueError(\n          'Couldn\\'t parse duration: {0} : {1}.'.format(value, e))\n\n  def ToNanoseconds(self):\n    \"\"\"Converts a Duration to nanoseconds.\"\"\"\n    return self.seconds * _NANOS_PER_SECOND + self.nanos\n\n  def ToMicroseconds(self):\n    \"\"\"Converts a Duration to microseconds.\"\"\"\n    micros = _RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND)\n    return self.seconds * _MICROS_PER_SECOND + micros\n\n  def ToMilliseconds(self):\n    \"\"\"Converts a Duration to milliseconds.\"\"\"\n    millis = _RoundTowardZero(self.nanos, _NANOS_PER_MILLISECOND)\n    return self.seconds * _MILLIS_PER_SECOND + millis\n\n  def ToSeconds(self):\n    \"\"\"Converts a Duration to seconds.\"\"\"\n    return self.seconds\n\n  def FromNanoseconds(self, nanos):\n    \"\"\"Converts nanoseconds to Duration.\"\"\"\n    self._NormalizeDuration(nanos // _NANOS_PER_SECOND,\n                            nanos % _NANOS_PER_SECOND)\n\n  def FromMicroseconds(self, micros):\n    \"\"\"Converts microseconds to Duration.\"\"\"\n    self._NormalizeDuration(\n        micros // _MICROS_PER_SECOND,\n        (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND)\n\n  def FromMilliseconds(self, millis):\n    \"\"\"Converts milliseconds to Duration.\"\"\"\n    self._NormalizeDuration(\n        millis // _MILLIS_PER_SECOND,\n        (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND)\n\n  def FromSeconds(self, seconds):\n    \"\"\"Converts seconds to Duration.\"\"\"\n    self.seconds = seconds\n    self.nanos = 0\n\n  def ToTimedelta(self):\n    \"\"\"Converts Duration to timedelta.\"\"\"\n    return datetime.timedelta(\n        seconds=self.seconds, microseconds=_RoundTowardZero(\n            self.nanos, _NANOS_PER_MICROSECOND))\n\n  def FromTimedelta(self, td):\n    \"\"\"Converts timedelta to Duration.\"\"\"\n    self._NormalizeDuration(td.seconds + td.days * _SECONDS_PER_DAY,\n                            td.microseconds * _NANOS_PER_MICROSECOND)\n\n  def _NormalizeDuration(self, seconds, nanos):\n    \"\"\"Set Duration by seconds and nanos.\"\"\"\n    # Force nanos to be negative if the duration is negative.\n    if seconds < 0 and nanos > 0:\n      seconds += 1\n      nanos -= _NANOS_PER_SECOND\n    self.seconds = seconds\n    self.nanos = nanos\n\n\ndef _CheckDurationValid(seconds, nanos):\n  if seconds < -_DURATION_SECONDS_MAX or seconds > _DURATION_SECONDS_MAX:\n    raise ValueError(\n        'Duration is not valid: Seconds {0} must be in range '\n        '[-315576000000, 315576000000].'.format(seconds))\n  if nanos <= -_NANOS_PER_SECOND or nanos >= _NANOS_PER_SECOND:\n    raise ValueError(\n        'Duration is not valid: Nanos {0} must be in range '\n        '[-999999999, 999999999].'.format(nanos))\n  if (nanos < 0 and seconds > 0) or (nanos > 0 and seconds < 0):\n    raise ValueError(\n        'Duration is not valid: Sign mismatch.')\n\n\ndef _RoundTowardZero(value, divider):\n  \"\"\"Truncates the remainder part after division.\"\"\"\n  # For some languages, the sign of the remainder is implementation\n  # dependent if any of the operands is negative. Here we enforce\n  # \"rounded toward zero\" semantics. For example, for (-5) / 2 an\n  # implementation may give -3 as the result with the remainder being\n  # 1. This function ensures we always return -2 (closer to zero).\n  result = value // divider\n  remainder = value % divider\n  if result < 0 and remainder > 0:\n    return result + 1\n  else:\n    return result\n\n\nclass FieldMask(object):\n  \"\"\"Class for FieldMask message type.\"\"\"\n\n  __slots__ = ()\n\n  def ToJsonString(self):\n    \"\"\"Converts FieldMask to string according to proto3 JSON spec.\"\"\"\n    camelcase_paths = []\n    for path in self.paths:\n      camelcase_paths.append(_SnakeCaseToCamelCase(path))\n    return ','.join(camelcase_paths)\n\n  def FromJsonString(self, value):\n    \"\"\"Converts string to FieldMask according to proto3 JSON spec.\"\"\"\n    if not isinstance(value, str):\n      raise ValueError('FieldMask JSON value not a string: {!r}'.format(value))\n    self.Clear()\n    if value:\n      for path in value.split(','):\n        self.paths.append(_CamelCaseToSnakeCase(path))\n\n  def IsValidForDescriptor(self, message_descriptor):\n    \"\"\"Checks whether the FieldMask is valid for Message Descriptor.\"\"\"\n    for path in self.paths:\n      if not _IsValidPath(message_descriptor, path):\n        return False\n    return True\n\n  def AllFieldsFromDescriptor(self, message_descriptor):\n    \"\"\"Gets all direct fields of Message Descriptor to FieldMask.\"\"\"\n    self.Clear()\n    for field in message_descriptor.fields:\n      self.paths.append(field.name)\n\n  def CanonicalFormFromMask(self, mask):\n    \"\"\"Converts a FieldMask to the canonical form.\n\n    Removes paths that are covered by another path. For example,\n    \"foo.bar\" is covered by \"foo\" and will be removed if \"foo\"\n    is also in the FieldMask. Then sorts all paths in alphabetical order.\n\n    Args:\n      mask: The original FieldMask to be converted.\n    \"\"\"\n    tree = _FieldMaskTree(mask)\n    tree.ToFieldMask(self)\n\n  def Union(self, mask1, mask2):\n    \"\"\"Merges mask1 and mask2 into this FieldMask.\"\"\"\n    _CheckFieldMaskMessage(mask1)\n    _CheckFieldMaskMessage(mask2)\n    tree = _FieldMaskTree(mask1)\n    tree.MergeFromFieldMask(mask2)\n    tree.ToFieldMask(self)\n\n  def Intersect(self, mask1, mask2):\n    \"\"\"Intersects mask1 and mask2 into this FieldMask.\"\"\"\n    _CheckFieldMaskMessage(mask1)\n    _CheckFieldMaskMessage(mask2)\n    tree = _FieldMaskTree(mask1)\n    intersection = _FieldMaskTree()\n    for path in mask2.paths:\n      tree.IntersectPath(path, intersection)\n    intersection.ToFieldMask(self)\n\n  def MergeMessage(\n      self, source, destination,\n      replace_message_field=False, replace_repeated_field=False):\n    \"\"\"Merges fields specified in FieldMask from source to destination.\n\n    Args:\n      source: Source message.\n      destination: The destination message to be merged into.\n      replace_message_field: Replace message field if True. Merge message\n          field if False.\n      replace_repeated_field: Replace repeated field if True. Append\n          elements of repeated field if False.\n    \"\"\"\n    tree = _FieldMaskTree(self)\n    tree.MergeMessage(\n        source, destination, replace_message_field, replace_repeated_field)\n\n\ndef _IsValidPath(message_descriptor, path):\n  \"\"\"Checks whether the path is valid for Message Descriptor.\"\"\"\n  parts = path.split('.')\n  last = parts.pop()\n  for name in parts:\n    field = message_descriptor.fields_by_name.get(name)\n    if (field is None or\n        field.label == FieldDescriptor.LABEL_REPEATED or\n        field.type != FieldDescriptor.TYPE_MESSAGE):\n      return False\n    message_descriptor = field.message_type\n  return last in message_descriptor.fields_by_name\n\n\ndef _CheckFieldMaskMessage(message):\n  \"\"\"Raises ValueError if message is not a FieldMask.\"\"\"\n  message_descriptor = message.DESCRIPTOR\n  if (message_descriptor.name != 'FieldMask' or\n      message_descriptor.file.name != 'google/protobuf/field_mask.proto'):\n    raise ValueError('Message {0} is not a FieldMask.'.format(\n        message_descriptor.full_name))\n\n\ndef _SnakeCaseToCamelCase(path_name):\n  \"\"\"Converts a path name from snake_case to camelCase.\"\"\"\n  result = []\n  after_underscore = False\n  for c in path_name:\n    if c.isupper():\n      raise ValueError(\n          'Fail to print FieldMask to Json string: Path name '\n          '{0} must not contain uppercase letters.'.format(path_name))\n    if after_underscore:\n      if c.islower():\n        result.append(c.upper())\n        after_underscore = False\n      else:\n        raise ValueError(\n            'Fail to print FieldMask to Json string: The '\n            'character after a \"_\" must be a lowercase letter '\n            'in path name {0}.'.format(path_name))\n    elif c == '_':\n      after_underscore = True\n    else:\n      result += c\n\n  if after_underscore:\n    raise ValueError('Fail to print FieldMask to Json string: Trailing \"_\" '\n                     'in path name {0}.'.format(path_name))\n  return ''.join(result)\n\n\ndef _CamelCaseToSnakeCase(path_name):\n  \"\"\"Converts a field name from camelCase to snake_case.\"\"\"\n  result = []\n  for c in path_name:\n    if c == '_':\n      raise ValueError('Fail to parse FieldMask: Path name '\n                       '{0} must not contain \"_\"s.'.format(path_name))\n    if c.isupper():\n      result += '_'\n      result += c.lower()\n    else:\n      result += c\n  return ''.join(result)\n\n\nclass _FieldMaskTree(object):\n  \"\"\"Represents a FieldMask in a tree structure.\n\n  For example, given a FieldMask \"foo.bar,foo.baz,bar.baz\",\n  the FieldMaskTree will be:\n      [_root] -+- foo -+- bar\n            |       |\n            |       +- baz\n            |\n            +- bar --- baz\n  In the tree, each leaf node represents a field path.\n  \"\"\"\n\n  __slots__ = ('_root',)\n\n  def __init__(self, field_mask=None):\n    \"\"\"Initializes the tree by FieldMask.\"\"\"\n    self._root = {}\n    if field_mask:\n      self.MergeFromFieldMask(field_mask)\n\n  def MergeFromFieldMask(self, field_mask):\n    \"\"\"Merges a FieldMask to the tree.\"\"\"\n    for path in field_mask.paths:\n      self.AddPath(path)\n\n  def AddPath(self, path):\n    \"\"\"Adds a field path into the tree.\n\n    If the field path to add is a sub-path of an existing field path\n    in the tree (i.e., a leaf node), it means the tree already matches\n    the given path so nothing will be added to the tree. If the path\n    matches an existing non-leaf node in the tree, that non-leaf node\n    will be turned into a leaf node with all its children removed because\n    the path matches all the node's children. Otherwise, a new path will\n    be added.\n\n    Args:\n      path: The field path to add.\n    \"\"\"\n    node = self._root\n    for name in path.split('.'):\n      if name not in node:\n        node[name] = {}\n      elif not node[name]:\n        # Pre-existing empty node implies we already have this entire tree.\n        return\n      node = node[name]\n    # Remove any sub-trees we might have had.\n    node.clear()\n\n  def ToFieldMask(self, field_mask):\n    \"\"\"Converts the tree to a FieldMask.\"\"\"\n    field_mask.Clear()\n    _AddFieldPaths(self._root, '', field_mask)\n\n  def IntersectPath(self, path, intersection):\n    \"\"\"Calculates the intersection part of a field path with this tree.\n\n    Args:\n      path: The field path to calculates.\n      intersection: The out tree to record the intersection part.\n    \"\"\"\n    node = self._root\n    for name in path.split('.'):\n      if name not in node:\n        return\n      elif not node[name]:\n        intersection.AddPath(path)\n        return\n      node = node[name]\n    intersection.AddLeafNodes(path, node)\n\n  def AddLeafNodes(self, prefix, node):\n    \"\"\"Adds leaf nodes begin with prefix to this tree.\"\"\"\n    if not node:\n      self.AddPath(prefix)\n    for name in node:\n      child_path = prefix + '.' + name\n      self.AddLeafNodes(child_path, node[name])\n\n  def MergeMessage(\n      self, source, destination,\n      replace_message, replace_repeated):\n    \"\"\"Merge all fields specified by this tree from source to destination.\"\"\"\n    _MergeMessage(\n        self._root, source, destination, replace_message, replace_repeated)\n\n\ndef _StrConvert(value):\n  \"\"\"Converts value to str if it is not.\"\"\"\n  # This file is imported by c extension and some methods like ClearField\n  # requires string for the field name. py2/py3 has different text\n  # type and may use unicode.\n  if not isinstance(value, str):\n    return value.encode('utf-8')\n  return value\n\n\ndef _MergeMessage(\n    node, source, destination, replace_message, replace_repeated):\n  \"\"\"Merge all fields specified by a sub-tree from source to destination.\"\"\"\n  source_descriptor = source.DESCRIPTOR\n  for name in node:\n    child = node[name]\n    field = source_descriptor.fields_by_name[name]\n    if field is None:\n      raise ValueError('Error: Can\\'t find field {0} in message {1}.'.format(\n          name, source_descriptor.full_name))\n    if child:\n      # Sub-paths are only allowed for singular message fields.\n      if (field.label == FieldDescriptor.LABEL_REPEATED or\n          field.cpp_type != FieldDescriptor.CPPTYPE_MESSAGE):\n        raise ValueError('Error: Field {0} in message {1} is not a singular '\n                         'message field and cannot have sub-fields.'.format(\n                             name, source_descriptor.full_name))\n      if source.HasField(name):\n        _MergeMessage(\n            child, getattr(source, name), getattr(destination, name),\n            replace_message, replace_repeated)\n      continue\n    if field.label == FieldDescriptor.LABEL_REPEATED:\n      if replace_repeated:\n        destination.ClearField(_StrConvert(name))\n      repeated_source = getattr(source, name)\n      repeated_destination = getattr(destination, name)\n      repeated_destination.MergeFrom(repeated_source)\n    else:\n      if field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE:\n        if replace_message:\n          destination.ClearField(_StrConvert(name))\n        if source.HasField(name):\n          getattr(destination, name).MergeFrom(getattr(source, name))\n      else:\n        setattr(destination, name, getattr(source, name))\n\n\ndef _AddFieldPaths(node, prefix, field_mask):\n  \"\"\"Adds the field paths descended from node to field_mask.\"\"\"\n  if not node and prefix:\n    field_mask.paths.append(prefix)\n    return\n  for name in sorted(node):\n    if prefix:\n      child_path = prefix + '.' + name\n    else:\n      child_path = name\n    _AddFieldPaths(node[name], child_path, field_mask)\n\n\ndef _SetStructValue(struct_value, value):\n  if value is None:\n    struct_value.null_value = 0\n  elif isinstance(value, bool):\n    # Note: this check must come before the number check because in Python\n    # True and False are also considered numbers.\n    struct_value.bool_value = value\n  elif isinstance(value, str):\n    struct_value.string_value = value\n  elif isinstance(value, (int, float)):\n    struct_value.number_value = value\n  elif isinstance(value, (dict, Struct)):\n    struct_value.struct_value.Clear()\n    struct_value.struct_value.update(value)\n  elif isinstance(value, (list, ListValue)):\n    struct_value.list_value.Clear()\n    struct_value.list_value.extend(value)\n  else:\n    raise ValueError('Unexpected type')\n\n\ndef _GetStructValue(struct_value):\n  which = struct_value.WhichOneof('kind')\n  if which == 'struct_value':\n    return struct_value.struct_value\n  elif which == 'null_value':\n    return None\n  elif which == 'number_value':\n    return struct_value.number_value\n  elif which == 'string_value':\n    return struct_value.string_value\n  elif which == 'bool_value':\n    return struct_value.bool_value\n  elif which == 'list_value':\n    return struct_value.list_value\n  elif which is None:\n    raise ValueError('Value not set')\n\n\nclass Struct(object):\n  \"\"\"Class for Struct message type.\"\"\"\n\n  __slots__ = ()\n\n  def __getitem__(self, key):\n    return _GetStructValue(self.fields[key])\n\n  def __contains__(self, item):\n    return item in self.fields\n\n  def __setitem__(self, key, value):\n    _SetStructValue(self.fields[key], value)\n\n  def __delitem__(self, key):\n    del self.fields[key]\n\n  def __len__(self):\n    return len(self.fields)\n\n  def __iter__(self):\n    return iter(self.fields)\n\n  def keys(self):  # pylint: disable=invalid-name\n    return self.fields.keys()\n\n  def values(self):  # pylint: disable=invalid-name\n    return [self[key] for key in self]\n\n  def items(self):  # pylint: disable=invalid-name\n    return [(key, self[key]) for key in self]\n\n  def get_or_create_list(self, key):\n    \"\"\"Returns a list for this key, creating if it didn't exist already.\"\"\"\n    if not self.fields[key].HasField('list_value'):\n      # Clear will mark list_value modified which will indeed create a list.\n      self.fields[key].list_value.Clear()\n    return self.fields[key].list_value\n\n  def get_or_create_struct(self, key):\n    \"\"\"Returns a struct for this key, creating if it didn't exist already.\"\"\"\n    if not self.fields[key].HasField('struct_value'):\n      # Clear will mark struct_value modified which will indeed create a struct.\n      self.fields[key].struct_value.Clear()\n    return self.fields[key].struct_value\n\n  def update(self, dictionary):  # pylint: disable=invalid-name\n    for key, value in dictionary.items():\n      _SetStructValue(self.fields[key], value)\n\ncollections.abc.MutableMapping.register(Struct)\n\n\nclass ListValue(object):\n  \"\"\"Class for ListValue message type.\"\"\"\n\n  __slots__ = ()\n\n  def __len__(self):\n    return len(self.values)\n\n  def append(self, value):\n    _SetStructValue(self.values.add(), value)\n\n  def extend(self, elem_seq):\n    for value in elem_seq:\n      self.append(value)\n\n  def __getitem__(self, index):\n    \"\"\"Retrieves item by the specified index.\"\"\"\n    return _GetStructValue(self.values.__getitem__(index))\n\n  def __setitem__(self, index, value):\n    _SetStructValue(self.values.__getitem__(index), value)\n\n  def __delitem__(self, key):\n    del self.values[key]\n\n  def items(self):\n    for i in range(len(self)):\n      yield self[i]\n\n  def add_struct(self):\n    \"\"\"Appends and returns a struct value as the next value in the list.\"\"\"\n    struct_value = self.values.add().struct_value\n    # Clear will mark struct_value modified which will indeed create a struct.\n    struct_value.Clear()\n    return struct_value\n\n  def add_list(self):\n    \"\"\"Appends and returns a list value as the next value in the list.\"\"\"\n    list_value = self.values.add().list_value\n    # Clear will mark list_value modified which will indeed create a list.\n    list_value.Clear()\n    return list_value\n\ncollections.abc.MutableSequence.register(ListValue)\n\n\nWKTBASES = {\n    'google.protobuf.Any': Any,\n    'google.protobuf.Duration': Duration,\n    'google.protobuf.FieldMask': FieldMask,\n    'google.protobuf.ListValue': ListValue,\n    'google.protobuf.Struct': Struct,\n    'google.protobuf.Timestamp': Timestamp,\n}\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/internal/wire_format.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Constants and static functions to support protocol buffer wire format.\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\nimport struct\nfrom google.protobuf import descriptor\nfrom google.protobuf import message\n\n\nTAG_TYPE_BITS = 3  # Number of bits used to hold type info in a proto tag.\nTAG_TYPE_MASK = (1 << TAG_TYPE_BITS) - 1  # 0x7\n\n# These numbers identify the wire type of a protocol buffer value.\n# We use the least-significant TAG_TYPE_BITS bits of the varint-encoded\n# tag-and-type to store one of these WIRETYPE_* constants.\n# These values must match WireType enum in google/protobuf/wire_format.h.\nWIRETYPE_VARINT = 0\nWIRETYPE_FIXED64 = 1\nWIRETYPE_LENGTH_DELIMITED = 2\nWIRETYPE_START_GROUP = 3\nWIRETYPE_END_GROUP = 4\nWIRETYPE_FIXED32 = 5\n_WIRETYPE_MAX = 5\n\n\n# Bounds for various integer types.\nINT32_MAX = int((1 << 31) - 1)\nINT32_MIN = int(-(1 << 31))\nUINT32_MAX = (1 << 32) - 1\n\nINT64_MAX = (1 << 63) - 1\nINT64_MIN = -(1 << 63)\nUINT64_MAX = (1 << 64) - 1\n\n# \"struct\" format strings that will encode/decode the specified formats.\nFORMAT_UINT32_LITTLE_ENDIAN = '<I'\nFORMAT_UINT64_LITTLE_ENDIAN = '<Q'\nFORMAT_FLOAT_LITTLE_ENDIAN = '<f'\nFORMAT_DOUBLE_LITTLE_ENDIAN = '<d'\n\n\n# We'll have to provide alternate implementations of AppendLittleEndian*() on\n# any architectures where these checks fail.\nif struct.calcsize(FORMAT_UINT32_LITTLE_ENDIAN) != 4:\n  raise AssertionError('Format \"I\" is not a 32-bit number.')\nif struct.calcsize(FORMAT_UINT64_LITTLE_ENDIAN) != 8:\n  raise AssertionError('Format \"Q\" is not a 64-bit number.')\n\n\ndef PackTag(field_number, wire_type):\n  \"\"\"Returns an unsigned 32-bit integer that encodes the field number and\n  wire type information in standard protocol message wire format.\n\n  Args:\n    field_number: Expected to be an integer in the range [1, 1 << 29)\n    wire_type: One of the WIRETYPE_* constants.\n  \"\"\"\n  if not 0 <= wire_type <= _WIRETYPE_MAX:\n    raise message.EncodeError('Unknown wire type: %d' % wire_type)\n  return (field_number << TAG_TYPE_BITS) | wire_type\n\n\ndef UnpackTag(tag):\n  \"\"\"The inverse of PackTag().  Given an unsigned 32-bit number,\n  returns a (field_number, wire_type) tuple.\n  \"\"\"\n  return (tag >> TAG_TYPE_BITS), (tag & TAG_TYPE_MASK)\n\n\ndef ZigZagEncode(value):\n  \"\"\"ZigZag Transform:  Encodes signed integers so that they can be\n  effectively used with varint encoding.  See wire_format.h for\n  more details.\n  \"\"\"\n  if value >= 0:\n    return value << 1\n  return (value << 1) ^ (~0)\n\n\ndef ZigZagDecode(value):\n  \"\"\"Inverse of ZigZagEncode().\"\"\"\n  if not value & 0x1:\n    return value >> 1\n  return (value >> 1) ^ (~0)\n\n\n\n# The *ByteSize() functions below return the number of bytes required to\n# serialize \"field number + type\" information and then serialize the value.\n\n\ndef Int32ByteSize(field_number, int32):\n  return Int64ByteSize(field_number, int32)\n\n\ndef Int32ByteSizeNoTag(int32):\n  return _VarUInt64ByteSizeNoTag(0xffffffffffffffff & int32)\n\n\ndef Int64ByteSize(field_number, int64):\n  # Have to convert to uint before calling UInt64ByteSize().\n  return UInt64ByteSize(field_number, 0xffffffffffffffff & int64)\n\n\ndef UInt32ByteSize(field_number, uint32):\n  return UInt64ByteSize(field_number, uint32)\n\n\ndef UInt64ByteSize(field_number, uint64):\n  return TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(uint64)\n\n\ndef SInt32ByteSize(field_number, int32):\n  return UInt32ByteSize(field_number, ZigZagEncode(int32))\n\n\ndef SInt64ByteSize(field_number, int64):\n  return UInt64ByteSize(field_number, ZigZagEncode(int64))\n\n\ndef Fixed32ByteSize(field_number, fixed32):\n  return TagByteSize(field_number) + 4\n\n\ndef Fixed64ByteSize(field_number, fixed64):\n  return TagByteSize(field_number) + 8\n\n\ndef SFixed32ByteSize(field_number, sfixed32):\n  return TagByteSize(field_number) + 4\n\n\ndef SFixed64ByteSize(field_number, sfixed64):\n  return TagByteSize(field_number) + 8\n\n\ndef FloatByteSize(field_number, flt):\n  return TagByteSize(field_number) + 4\n\n\ndef DoubleByteSize(field_number, double):\n  return TagByteSize(field_number) + 8\n\n\ndef BoolByteSize(field_number, b):\n  return TagByteSize(field_number) + 1\n\n\ndef EnumByteSize(field_number, enum):\n  return UInt32ByteSize(field_number, enum)\n\n\ndef StringByteSize(field_number, string):\n  return BytesByteSize(field_number, string.encode('utf-8'))\n\n\ndef BytesByteSize(field_number, b):\n  return (TagByteSize(field_number)\n          + _VarUInt64ByteSizeNoTag(len(b))\n          + len(b))\n\n\ndef GroupByteSize(field_number, message):\n  return (2 * TagByteSize(field_number)  # START and END group.\n          + message.ByteSize())\n\n\ndef MessageByteSize(field_number, message):\n  return (TagByteSize(field_number)\n          + _VarUInt64ByteSizeNoTag(message.ByteSize())\n          + message.ByteSize())\n\n\ndef MessageSetItemByteSize(field_number, msg):\n  # First compute the sizes of the tags.\n  # There are 2 tags for the beginning and ending of the repeated group, that\n  # is field number 1, one with field number 2 (type_id) and one with field\n  # number 3 (message).\n  total_size = (2 * TagByteSize(1) + TagByteSize(2) + TagByteSize(3))\n\n  # Add the number of bytes for type_id.\n  total_size += _VarUInt64ByteSizeNoTag(field_number)\n\n  message_size = msg.ByteSize()\n\n  # The number of bytes for encoding the length of the message.\n  total_size += _VarUInt64ByteSizeNoTag(message_size)\n\n  # The size of the message.\n  total_size += message_size\n  return total_size\n\n\ndef TagByteSize(field_number):\n  \"\"\"Returns the bytes required to serialize a tag with this field number.\"\"\"\n  # Just pass in type 0, since the type won't affect the tag+type size.\n  return _VarUInt64ByteSizeNoTag(PackTag(field_number, 0))\n\n\n# Private helper function for the *ByteSize() functions above.\n\ndef _VarUInt64ByteSizeNoTag(uint64):\n  \"\"\"Returns the number of bytes required to serialize a single varint\n  using boundary value comparisons. (unrolled loop optimization -WPierce)\n  uint64 must be unsigned.\n  \"\"\"\n  if uint64 <= 0x7f: return 1\n  if uint64 <= 0x3fff: return 2\n  if uint64 <= 0x1fffff: return 3\n  if uint64 <= 0xfffffff: return 4\n  if uint64 <= 0x7ffffffff: return 5\n  if uint64 <= 0x3ffffffffff: return 6\n  if uint64 <= 0x1ffffffffffff: return 7\n  if uint64 <= 0xffffffffffffff: return 8\n  if uint64 <= 0x7fffffffffffffff: return 9\n  if uint64 > UINT64_MAX:\n    raise message.EncodeError('Value out of range: %d' % uint64)\n  return 10\n\n\nNON_PACKABLE_TYPES = (\n  descriptor.FieldDescriptor.TYPE_STRING,\n  descriptor.FieldDescriptor.TYPE_GROUP,\n  descriptor.FieldDescriptor.TYPE_MESSAGE,\n  descriptor.FieldDescriptor.TYPE_BYTES\n)\n\n\ndef IsTypePackable(field_type):\n  \"\"\"Return true iff packable = true is valid for fields of this type.\n\n  Args:\n    field_type: a FieldDescriptor::Type value.\n\n  Returns:\n    True iff fields of this type are packable.\n  \"\"\"\n  return field_type not in NON_PACKABLE_TYPES\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/json_format.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Contains routines for printing protocol messages in JSON format.\n\nSimple usage example:\n\n  # Create a proto object and serialize it to a json format string.\n  message = my_proto_pb2.MyMessage(foo='bar')\n  json_string = json_format.MessageToJson(message)\n\n  # Parse a json format string to proto object.\n  message = json_format.Parse(json_string, my_proto_pb2.MyMessage())\n\"\"\"\n\n__author__ = 'jieluo@google.com (Jie Luo)'\n\n\nimport base64\nfrom collections import OrderedDict\nimport json\nimport math\nfrom operator import methodcaller\nimport re\nimport sys\n\nfrom google.protobuf.internal import type_checkers\nfrom google.protobuf import descriptor\nfrom google.protobuf import symbol_database\n\n\n_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S'\n_INT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT32,\n                        descriptor.FieldDescriptor.CPPTYPE_UINT32,\n                        descriptor.FieldDescriptor.CPPTYPE_INT64,\n                        descriptor.FieldDescriptor.CPPTYPE_UINT64])\n_INT64_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT64,\n                          descriptor.FieldDescriptor.CPPTYPE_UINT64])\n_FLOAT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_FLOAT,\n                          descriptor.FieldDescriptor.CPPTYPE_DOUBLE])\n_INFINITY = 'Infinity'\n_NEG_INFINITY = '-Infinity'\n_NAN = 'NaN'\n\n_UNPAIRED_SURROGATE_PATTERN = re.compile(\n    u'[\\ud800-\\udbff](?![\\udc00-\\udfff])|(?<![\\ud800-\\udbff])[\\udc00-\\udfff]')\n\n_VALID_EXTENSION_NAME = re.compile(r'\\[[a-zA-Z0-9\\._]*\\]$')\n\n\nclass Error(Exception):\n  \"\"\"Top-level module error for json_format.\"\"\"\n\n\nclass SerializeToJsonError(Error):\n  \"\"\"Thrown if serialization to JSON fails.\"\"\"\n\n\nclass ParseError(Error):\n  \"\"\"Thrown in case of parsing error.\"\"\"\n\n\ndef MessageToJson(\n    message,\n    including_default_value_fields=False,\n    preserving_proto_field_name=False,\n    indent=2,\n    sort_keys=False,\n    use_integers_for_enums=False,\n    descriptor_pool=None,\n    float_precision=None,\n    ensure_ascii=True):\n  \"\"\"Converts protobuf message to JSON format.\n\n  Args:\n    message: The protocol buffers message instance to serialize.\n    including_default_value_fields: If True, singular primitive fields,\n        repeated fields, and map fields will always be serialized.  If\n        False, only serialize non-empty fields.  Singular message fields\n        and oneof fields are not affected by this option.\n    preserving_proto_field_name: If True, use the original proto field\n        names as defined in the .proto file. If False, convert the field\n        names to lowerCamelCase.\n    indent: The JSON object will be pretty-printed with this indent level.\n        An indent level of 0 or negative will only insert newlines.\n    sort_keys: If True, then the output will be sorted by field names.\n    use_integers_for_enums: If true, print integers instead of enum names.\n    descriptor_pool: A Descriptor Pool for resolving types. If None use the\n        default.\n    float_precision: If set, use this to specify float field valid digits.\n    ensure_ascii: If True, strings with non-ASCII characters are escaped.\n        If False, Unicode strings are returned unchanged.\n\n  Returns:\n    A string containing the JSON formatted protocol buffer message.\n  \"\"\"\n  printer = _Printer(\n      including_default_value_fields,\n      preserving_proto_field_name,\n      use_integers_for_enums,\n      descriptor_pool,\n      float_precision=float_precision)\n  return printer.ToJsonString(message, indent, sort_keys, ensure_ascii)\n\n\ndef MessageToDict(\n    message,\n    including_default_value_fields=False,\n    preserving_proto_field_name=False,\n    use_integers_for_enums=False,\n    descriptor_pool=None,\n    float_precision=None):\n  \"\"\"Converts protobuf message to a dictionary.\n\n  When the dictionary is encoded to JSON, it conforms to proto3 JSON spec.\n\n  Args:\n    message: The protocol buffers message instance to serialize.\n    including_default_value_fields: If True, singular primitive fields,\n        repeated fields, and map fields will always be serialized.  If\n        False, only serialize non-empty fields.  Singular message fields\n        and oneof fields are not affected by this option.\n    preserving_proto_field_name: If True, use the original proto field\n        names as defined in the .proto file. If False, convert the field\n        names to lowerCamelCase.\n    use_integers_for_enums: If true, print integers instead of enum names.\n    descriptor_pool: A Descriptor Pool for resolving types. If None use the\n        default.\n    float_precision: If set, use this to specify float field valid digits.\n\n  Returns:\n    A dict representation of the protocol buffer message.\n  \"\"\"\n  printer = _Printer(\n      including_default_value_fields,\n      preserving_proto_field_name,\n      use_integers_for_enums,\n      descriptor_pool,\n      float_precision=float_precision)\n  # pylint: disable=protected-access\n  return printer._MessageToJsonObject(message)\n\n\ndef _IsMapEntry(field):\n  return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and\n          field.message_type.has_options and\n          field.message_type.GetOptions().map_entry)\n\n\nclass _Printer(object):\n  \"\"\"JSON format printer for protocol message.\"\"\"\n\n  def __init__(\n      self,\n      including_default_value_fields=False,\n      preserving_proto_field_name=False,\n      use_integers_for_enums=False,\n      descriptor_pool=None,\n      float_precision=None):\n    self.including_default_value_fields = including_default_value_fields\n    self.preserving_proto_field_name = preserving_proto_field_name\n    self.use_integers_for_enums = use_integers_for_enums\n    self.descriptor_pool = descriptor_pool\n    if float_precision:\n      self.float_format = '.{}g'.format(float_precision)\n    else:\n      self.float_format = None\n\n  def ToJsonString(self, message, indent, sort_keys, ensure_ascii):\n    js = self._MessageToJsonObject(message)\n    return json.dumps(\n        js, indent=indent, sort_keys=sort_keys, ensure_ascii=ensure_ascii)\n\n  def _MessageToJsonObject(self, message):\n    \"\"\"Converts message to an object according to Proto3 JSON Specification.\"\"\"\n    message_descriptor = message.DESCRIPTOR\n    full_name = message_descriptor.full_name\n    if _IsWrapperMessage(message_descriptor):\n      return self._WrapperMessageToJsonObject(message)\n    if full_name in _WKTJSONMETHODS:\n      return methodcaller(_WKTJSONMETHODS[full_name][0], message)(self)\n    js = {}\n    return self._RegularMessageToJsonObject(message, js)\n\n  def _RegularMessageToJsonObject(self, message, js):\n    \"\"\"Converts normal message according to Proto3 JSON Specification.\"\"\"\n    fields = message.ListFields()\n\n    try:\n      for field, value in fields:\n        if self.preserving_proto_field_name:\n          name = field.name\n        else:\n          name = field.json_name\n        if _IsMapEntry(field):\n          # Convert a map field.\n          v_field = field.message_type.fields_by_name['value']\n          js_map = {}\n          for key in value:\n            if isinstance(key, bool):\n              if key:\n                recorded_key = 'true'\n              else:\n                recorded_key = 'false'\n            else:\n              recorded_key = str(key)\n            js_map[recorded_key] = self._FieldToJsonObject(\n                v_field, value[key])\n          js[name] = js_map\n        elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n          # Convert a repeated field.\n          js[name] = [self._FieldToJsonObject(field, k)\n                      for k in value]\n        elif field.is_extension:\n          name = '[%s]' % field.full_name\n          js[name] = self._FieldToJsonObject(field, value)\n        else:\n          js[name] = self._FieldToJsonObject(field, value)\n\n      # Serialize default value if including_default_value_fields is True.\n      if self.including_default_value_fields:\n        message_descriptor = message.DESCRIPTOR\n        for field in message_descriptor.fields:\n          # Singular message fields and oneof fields will not be affected.\n          if ((field.label != descriptor.FieldDescriptor.LABEL_REPEATED and\n               field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE) or\n              field.containing_oneof):\n            continue\n          if self.preserving_proto_field_name:\n            name = field.name\n          else:\n            name = field.json_name\n          if name in js:\n            # Skip the field which has been serialized already.\n            continue\n          if _IsMapEntry(field):\n            js[name] = {}\n          elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n            js[name] = []\n          else:\n            js[name] = self._FieldToJsonObject(field, field.default_value)\n\n    except ValueError as e:\n      raise SerializeToJsonError(\n          'Failed to serialize {0} field: {1}.'.format(field.name, e))\n\n    return js\n\n  def _FieldToJsonObject(self, field, value):\n    \"\"\"Converts field value according to Proto3 JSON Specification.\"\"\"\n    if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n      return self._MessageToJsonObject(value)\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM:\n      if self.use_integers_for_enums:\n        return value\n      if field.enum_type.full_name == 'google.protobuf.NullValue':\n        return None\n      enum_value = field.enum_type.values_by_number.get(value, None)\n      if enum_value is not None:\n        return enum_value.name\n      else:\n        if field.file.syntax == 'proto3':\n          return value\n        raise SerializeToJsonError('Enum field contains an integer value '\n                                   'which can not mapped to an enum value.')\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING:\n      if field.type == descriptor.FieldDescriptor.TYPE_BYTES:\n        # Use base64 Data encoding for bytes\n        return base64.b64encode(value).decode('utf-8')\n      else:\n        return value\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL:\n      return bool(value)\n    elif field.cpp_type in _INT64_TYPES:\n      return str(value)\n    elif field.cpp_type in _FLOAT_TYPES:\n      if math.isinf(value):\n        if value < 0.0:\n          return _NEG_INFINITY\n        else:\n          return _INFINITY\n      if math.isnan(value):\n        return _NAN\n      if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT:\n        if self.float_format:\n          return float(format(value, self.float_format))\n        else:\n          return type_checkers.ToShortestFloat(value)\n\n    return value\n\n  def _AnyMessageToJsonObject(self, message):\n    \"\"\"Converts Any message according to Proto3 JSON Specification.\"\"\"\n    if not message.ListFields():\n      return {}\n    # Must print @type first, use OrderedDict instead of {}\n    js = OrderedDict()\n    type_url = message.type_url\n    js['@type'] = type_url\n    sub_message = _CreateMessageFromTypeUrl(type_url, self.descriptor_pool)\n    sub_message.ParseFromString(message.value)\n    message_descriptor = sub_message.DESCRIPTOR\n    full_name = message_descriptor.full_name\n    if _IsWrapperMessage(message_descriptor):\n      js['value'] = self._WrapperMessageToJsonObject(sub_message)\n      return js\n    if full_name in _WKTJSONMETHODS:\n      js['value'] = methodcaller(_WKTJSONMETHODS[full_name][0],\n                                 sub_message)(self)\n      return js\n    return self._RegularMessageToJsonObject(sub_message, js)\n\n  def _GenericMessageToJsonObject(self, message):\n    \"\"\"Converts message according to Proto3 JSON Specification.\"\"\"\n    # Duration, Timestamp and FieldMask have ToJsonString method to do the\n    # convert. Users can also call the method directly.\n    return message.ToJsonString()\n\n  def _ValueMessageToJsonObject(self, message):\n    \"\"\"Converts Value message according to Proto3 JSON Specification.\"\"\"\n    which = message.WhichOneof('kind')\n    # If the Value message is not set treat as null_value when serialize\n    # to JSON. The parse back result will be different from original message.\n    if which is None or which == 'null_value':\n      return None\n    if which == 'list_value':\n      return self._ListValueMessageToJsonObject(message.list_value)\n    if which == 'struct_value':\n      value = message.struct_value\n    else:\n      value = getattr(message, which)\n    oneof_descriptor = message.DESCRIPTOR.fields_by_name[which]\n    return self._FieldToJsonObject(oneof_descriptor, value)\n\n  def _ListValueMessageToJsonObject(self, message):\n    \"\"\"Converts ListValue message according to Proto3 JSON Specification.\"\"\"\n    return [self._ValueMessageToJsonObject(value)\n            for value in message.values]\n\n  def _StructMessageToJsonObject(self, message):\n    \"\"\"Converts Struct message according to Proto3 JSON Specification.\"\"\"\n    fields = message.fields\n    ret = {}\n    for key in fields:\n      ret[key] = self._ValueMessageToJsonObject(fields[key])\n    return ret\n\n  def _WrapperMessageToJsonObject(self, message):\n    return self._FieldToJsonObject(\n        message.DESCRIPTOR.fields_by_name['value'], message.value)\n\n\ndef _IsWrapperMessage(message_descriptor):\n  return message_descriptor.file.name == 'google/protobuf/wrappers.proto'\n\n\ndef _DuplicateChecker(js):\n  result = {}\n  for name, value in js:\n    if name in result:\n      raise ParseError('Failed to load JSON: duplicate key {0}.'.format(name))\n    result[name] = value\n  return result\n\n\ndef _CreateMessageFromTypeUrl(type_url, descriptor_pool):\n  \"\"\"Creates a message from a type URL.\"\"\"\n  db = symbol_database.Default()\n  pool = db.pool if descriptor_pool is None else descriptor_pool\n  type_name = type_url.split('/')[-1]\n  try:\n    message_descriptor = pool.FindMessageTypeByName(type_name)\n  except KeyError:\n    raise TypeError(\n        'Can not find message descriptor by type_url: {0}'.format(type_url))\n  message_class = db.GetPrototype(message_descriptor)\n  return message_class()\n\n\ndef Parse(text,\n          message,\n          ignore_unknown_fields=False,\n          descriptor_pool=None,\n          max_recursion_depth=100):\n  \"\"\"Parses a JSON representation of a protocol message into a message.\n\n  Args:\n    text: Message JSON representation.\n    message: A protocol buffer message to merge into.\n    ignore_unknown_fields: If True, do not raise errors for unknown fields.\n    descriptor_pool: A Descriptor Pool for resolving types. If None use the\n      default.\n    max_recursion_depth: max recursion depth of JSON message to be\n      deserialized. JSON messages over this depth will fail to be\n      deserialized. Default value is 100.\n\n  Returns:\n    The same message passed as argument.\n\n  Raises::\n    ParseError: On JSON parsing problems.\n  \"\"\"\n  if not isinstance(text, str):\n    text = text.decode('utf-8')\n  try:\n    js = json.loads(text, object_pairs_hook=_DuplicateChecker)\n  except ValueError as e:\n    raise ParseError('Failed to load JSON: {0}.'.format(str(e)))\n  return ParseDict(js, message, ignore_unknown_fields, descriptor_pool,\n                   max_recursion_depth)\n\n\ndef ParseDict(js_dict,\n              message,\n              ignore_unknown_fields=False,\n              descriptor_pool=None,\n              max_recursion_depth=100):\n  \"\"\"Parses a JSON dictionary representation into a message.\n\n  Args:\n    js_dict: Dict representation of a JSON message.\n    message: A protocol buffer message to merge into.\n    ignore_unknown_fields: If True, do not raise errors for unknown fields.\n    descriptor_pool: A Descriptor Pool for resolving types. If None use the\n      default.\n    max_recursion_depth: max recursion depth of JSON message to be\n      deserialized. JSON messages over this depth will fail to be\n      deserialized. Default value is 100.\n\n  Returns:\n    The same message passed as argument.\n  \"\"\"\n  parser = _Parser(ignore_unknown_fields, descriptor_pool, max_recursion_depth)\n  parser.ConvertMessage(js_dict, message, '')\n  return message\n\n\n_INT_OR_FLOAT = (int, float)\n\n\nclass _Parser(object):\n  \"\"\"JSON format parser for protocol message.\"\"\"\n\n  def __init__(self, ignore_unknown_fields, descriptor_pool,\n               max_recursion_depth):\n    self.ignore_unknown_fields = ignore_unknown_fields\n    self.descriptor_pool = descriptor_pool\n    self.max_recursion_depth = max_recursion_depth\n    self.recursion_depth = 0\n\n  def ConvertMessage(self, value, message, path):\n    \"\"\"Convert a JSON object into a message.\n\n    Args:\n      value: A JSON object.\n      message: A WKT or regular protocol message to record the data.\n      path: parent path to log parse error info.\n\n    Raises:\n      ParseError: In case of convert problems.\n    \"\"\"\n    self.recursion_depth += 1\n    if self.recursion_depth > self.max_recursion_depth:\n      raise ParseError('Message too deep. Max recursion depth is {0}'.format(\n          self.max_recursion_depth))\n    message_descriptor = message.DESCRIPTOR\n    full_name = message_descriptor.full_name\n    if not path:\n      path = message_descriptor.name\n    if _IsWrapperMessage(message_descriptor):\n      self._ConvertWrapperMessage(value, message, path)\n    elif full_name in _WKTJSONMETHODS:\n      methodcaller(_WKTJSONMETHODS[full_name][1], value, message, path)(self)\n    else:\n      self._ConvertFieldValuePair(value, message, path)\n    self.recursion_depth -= 1\n\n  def _ConvertFieldValuePair(self, js, message, path):\n    \"\"\"Convert field value pairs into regular message.\n\n    Args:\n      js: A JSON object to convert the field value pairs.\n      message: A regular protocol message to record the data.\n      path: parent path to log parse error info.\n\n    Raises:\n      ParseError: In case of problems converting.\n    \"\"\"\n    names = []\n    message_descriptor = message.DESCRIPTOR\n    fields_by_json_name = dict((f.json_name, f)\n                               for f in message_descriptor.fields)\n    for name in js:\n      try:\n        field = fields_by_json_name.get(name, None)\n        if not field:\n          field = message_descriptor.fields_by_name.get(name, None)\n        if not field and _VALID_EXTENSION_NAME.match(name):\n          if not message_descriptor.is_extendable:\n            raise ParseError(\n                'Message type {0} does not have extensions at {1}'.format(\n                    message_descriptor.full_name, path))\n          identifier = name[1:-1]  # strip [] brackets\n          # pylint: disable=protected-access\n          field = message.Extensions._FindExtensionByName(identifier)\n          # pylint: enable=protected-access\n          if not field:\n            # Try looking for extension by the message type name, dropping the\n            # field name following the final . separator in full_name.\n            identifier = '.'.join(identifier.split('.')[:-1])\n            # pylint: disable=protected-access\n            field = message.Extensions._FindExtensionByName(identifier)\n            # pylint: enable=protected-access\n        if not field:\n          if self.ignore_unknown_fields:\n            continue\n          raise ParseError(\n              ('Message type \"{0}\" has no field named \"{1}\" at \"{2}\".\\n'\n               ' Available Fields(except extensions): \"{3}\"').format(\n                   message_descriptor.full_name, name, path,\n                   [f.json_name for f in message_descriptor.fields]))\n        if name in names:\n          raise ParseError('Message type \"{0}\" should not have multiple '\n                           '\"{1}\" fields at \"{2}\".'.format(\n                               message.DESCRIPTOR.full_name, name, path))\n        names.append(name)\n        value = js[name]\n        # Check no other oneof field is parsed.\n        if field.containing_oneof is not None and value is not None:\n          oneof_name = field.containing_oneof.name\n          if oneof_name in names:\n            raise ParseError('Message type \"{0}\" should not have multiple '\n                             '\"{1}\" oneof fields at \"{2}\".'.format(\n                                 message.DESCRIPTOR.full_name, oneof_name,\n                                 path))\n          names.append(oneof_name)\n\n        if value is None:\n          if (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE\n              and field.message_type.full_name == 'google.protobuf.Value'):\n            sub_message = getattr(message, field.name)\n            sub_message.null_value = 0\n          elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM\n                and field.enum_type.full_name == 'google.protobuf.NullValue'):\n            setattr(message, field.name, 0)\n          else:\n            message.ClearField(field.name)\n          continue\n\n        # Parse field value.\n        if _IsMapEntry(field):\n          message.ClearField(field.name)\n          self._ConvertMapFieldValue(value, message, field,\n                                     '{0}.{1}'.format(path, name))\n        elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n          message.ClearField(field.name)\n          if not isinstance(value, list):\n            raise ParseError('repeated field {0} must be in [] which is '\n                             '{1} at {2}'.format(name, value, path))\n          if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n            # Repeated message field.\n            for index, item in enumerate(value):\n              sub_message = getattr(message, field.name).add()\n              # None is a null_value in Value.\n              if (item is None and\n                  sub_message.DESCRIPTOR.full_name != 'google.protobuf.Value'):\n                raise ParseError('null is not allowed to be used as an element'\n                                 ' in a repeated field at {0}.{1}[{2}]'.format(\n                                     path, name, index))\n              self.ConvertMessage(item, sub_message,\n                                  '{0}.{1}[{2}]'.format(path, name, index))\n          else:\n            # Repeated scalar field.\n            for index, item in enumerate(value):\n              if item is None:\n                raise ParseError('null is not allowed to be used as an element'\n                                 ' in a repeated field at {0}.{1}[{2}]'.format(\n                                     path, name, index))\n              getattr(message, field.name).append(\n                  _ConvertScalarFieldValue(\n                      item, field, '{0}.{1}[{2}]'.format(path, name, index)))\n        elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n          if field.is_extension:\n            sub_message = message.Extensions[field]\n          else:\n            sub_message = getattr(message, field.name)\n          sub_message.SetInParent()\n          self.ConvertMessage(value, sub_message, '{0}.{1}'.format(path, name))\n        else:\n          if field.is_extension:\n            message.Extensions[field] = _ConvertScalarFieldValue(\n                value, field, '{0}.{1}'.format(path, name))\n          else:\n            setattr(\n                message, field.name,\n                _ConvertScalarFieldValue(value, field,\n                                         '{0}.{1}'.format(path, name)))\n      except ParseError as e:\n        if field and field.containing_oneof is None:\n          raise ParseError('Failed to parse {0} field: {1}.'.format(name, e))\n        else:\n          raise ParseError(str(e))\n      except ValueError as e:\n        raise ParseError('Failed to parse {0} field: {1}.'.format(name, e))\n      except TypeError as e:\n        raise ParseError('Failed to parse {0} field: {1}.'.format(name, e))\n\n  def _ConvertAnyMessage(self, value, message, path):\n    \"\"\"Convert a JSON representation into Any message.\"\"\"\n    if isinstance(value, dict) and not value:\n      return\n    try:\n      type_url = value['@type']\n    except KeyError:\n      raise ParseError(\n          '@type is missing when parsing any message at {0}'.format(path))\n\n    try:\n      sub_message = _CreateMessageFromTypeUrl(type_url, self.descriptor_pool)\n    except TypeError as e:\n      raise ParseError('{0} at {1}'.format(e, path))\n    message_descriptor = sub_message.DESCRIPTOR\n    full_name = message_descriptor.full_name\n    if _IsWrapperMessage(message_descriptor):\n      self._ConvertWrapperMessage(value['value'], sub_message,\n                                  '{0}.value'.format(path))\n    elif full_name in _WKTJSONMETHODS:\n      methodcaller(_WKTJSONMETHODS[full_name][1], value['value'], sub_message,\n                   '{0}.value'.format(path))(\n                       self)\n    else:\n      del value['@type']\n      self._ConvertFieldValuePair(value, sub_message, path)\n      value['@type'] = type_url\n    # Sets Any message\n    message.value = sub_message.SerializeToString()\n    message.type_url = type_url\n\n  def _ConvertGenericMessage(self, value, message, path):\n    \"\"\"Convert a JSON representation into message with FromJsonString.\"\"\"\n    # Duration, Timestamp, FieldMask have a FromJsonString method to do the\n    # conversion. Users can also call the method directly.\n    try:\n      message.FromJsonString(value)\n    except ValueError as e:\n      raise ParseError('{0} at {1}'.format(e, path))\n\n  def _ConvertValueMessage(self, value, message, path):\n    \"\"\"Convert a JSON representation into Value message.\"\"\"\n    if isinstance(value, dict):\n      self._ConvertStructMessage(value, message.struct_value, path)\n    elif isinstance(value, list):\n      self._ConvertListValueMessage(value, message.list_value, path)\n    elif value is None:\n      message.null_value = 0\n    elif isinstance(value, bool):\n      message.bool_value = value\n    elif isinstance(value, str):\n      message.string_value = value\n    elif isinstance(value, _INT_OR_FLOAT):\n      message.number_value = value\n    else:\n      raise ParseError('Value {0} has unexpected type {1} at {2}'.format(\n          value, type(value), path))\n\n  def _ConvertListValueMessage(self, value, message, path):\n    \"\"\"Convert a JSON representation into ListValue message.\"\"\"\n    if not isinstance(value, list):\n      raise ParseError('ListValue must be in [] which is {0} at {1}'.format(\n          value, path))\n    message.ClearField('values')\n    for index, item in enumerate(value):\n      self._ConvertValueMessage(item, message.values.add(),\n                                '{0}[{1}]'.format(path, index))\n\n  def _ConvertStructMessage(self, value, message, path):\n    \"\"\"Convert a JSON representation into Struct message.\"\"\"\n    if not isinstance(value, dict):\n      raise ParseError('Struct must be in a dict which is {0} at {1}'.format(\n          value, path))\n    # Clear will mark the struct as modified so it will be created even if\n    # there are no values.\n    message.Clear()\n    for key in value:\n      self._ConvertValueMessage(value[key], message.fields[key],\n                                '{0}.{1}'.format(path, key))\n    return\n\n  def _ConvertWrapperMessage(self, value, message, path):\n    \"\"\"Convert a JSON representation into Wrapper message.\"\"\"\n    field = message.DESCRIPTOR.fields_by_name['value']\n    setattr(\n        message, 'value',\n        _ConvertScalarFieldValue(value, field, path='{0}.value'.format(path)))\n\n  def _ConvertMapFieldValue(self, value, message, field, path):\n    \"\"\"Convert map field value for a message map field.\n\n    Args:\n      value: A JSON object to convert the map field value.\n      message: A protocol message to record the converted data.\n      field: The descriptor of the map field to be converted.\n      path: parent path to log parse error info.\n\n    Raises:\n      ParseError: In case of convert problems.\n    \"\"\"\n    if not isinstance(value, dict):\n      raise ParseError(\n          'Map field {0} must be in a dict which is {1} at {2}'.format(\n              field.name, value, path))\n    key_field = field.message_type.fields_by_name['key']\n    value_field = field.message_type.fields_by_name['value']\n    for key in value:\n      key_value = _ConvertScalarFieldValue(key, key_field,\n                                           '{0}.key'.format(path), True)\n      if value_field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n        self.ConvertMessage(value[key],\n                            getattr(message, field.name)[key_value],\n                            '{0}[{1}]'.format(path, key_value))\n      else:\n        getattr(message, field.name)[key_value] = _ConvertScalarFieldValue(\n            value[key], value_field, path='{0}[{1}]'.format(path, key_value))\n\n\ndef _ConvertScalarFieldValue(value, field, path, require_str=False):\n  \"\"\"Convert a single scalar field value.\n\n  Args:\n    value: A scalar value to convert the scalar field value.\n    field: The descriptor of the field to convert.\n    path: parent path to log parse error info.\n    require_str: If True, the field value must be a str.\n\n  Returns:\n    The converted scalar field value\n\n  Raises:\n    ParseError: In case of convert problems.\n  \"\"\"\n  try:\n    if field.cpp_type in _INT_TYPES:\n      return _ConvertInteger(value)\n    elif field.cpp_type in _FLOAT_TYPES:\n      return _ConvertFloat(value, field)\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL:\n      return _ConvertBool(value, require_str)\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING:\n      if field.type == descriptor.FieldDescriptor.TYPE_BYTES:\n        if isinstance(value, str):\n          encoded = value.encode('utf-8')\n        else:\n          encoded = value\n        # Add extra padding '='\n        padded_value = encoded + b'=' * (4 - len(encoded) % 4)\n        return base64.urlsafe_b64decode(padded_value)\n      else:\n        # Checking for unpaired surrogates appears to be unreliable,\n        # depending on the specific Python version, so we check manually.\n        if _UNPAIRED_SURROGATE_PATTERN.search(value):\n          raise ParseError('Unpaired surrogate')\n        return value\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM:\n      # Convert an enum value.\n      enum_value = field.enum_type.values_by_name.get(value, None)\n      if enum_value is None:\n        try:\n          number = int(value)\n          enum_value = field.enum_type.values_by_number.get(number, None)\n        except ValueError:\n          raise ParseError('Invalid enum value {0} for enum type {1}'.format(\n              value, field.enum_type.full_name))\n        if enum_value is None:\n          if field.file.syntax == 'proto3':\n            # Proto3 accepts unknown enums.\n            return number\n          raise ParseError('Invalid enum value {0} for enum type {1}'.format(\n              value, field.enum_type.full_name))\n      return enum_value.number\n  except ParseError as e:\n    raise ParseError('{0} at {1}'.format(e, path))\n\n\ndef _ConvertInteger(value):\n  \"\"\"Convert an integer.\n\n  Args:\n    value: A scalar value to convert.\n\n  Returns:\n    The integer value.\n\n  Raises:\n    ParseError: If an integer couldn't be consumed.\n  \"\"\"\n  if isinstance(value, float) and not value.is_integer():\n    raise ParseError('Couldn\\'t parse integer: {0}'.format(value))\n\n  if isinstance(value, str) and value.find(' ') != -1:\n    raise ParseError('Couldn\\'t parse integer: \"{0}\"'.format(value))\n\n  if isinstance(value, bool):\n    raise ParseError('Bool value {0} is not acceptable for '\n                     'integer field'.format(value))\n\n  return int(value)\n\n\ndef _ConvertFloat(value, field):\n  \"\"\"Convert an floating point number.\"\"\"\n  if isinstance(value, float):\n    if math.isnan(value):\n      raise ParseError('Couldn\\'t parse NaN, use quoted \"NaN\" instead')\n    if math.isinf(value):\n      if value > 0:\n        raise ParseError('Couldn\\'t parse Infinity or value too large, '\n                         'use quoted \"Infinity\" instead')\n      else:\n        raise ParseError('Couldn\\'t parse -Infinity or value too small, '\n                         'use quoted \"-Infinity\" instead')\n    if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT:\n      # pylint: disable=protected-access\n      if value > type_checkers._FLOAT_MAX:\n        raise ParseError('Float value too large')\n      # pylint: disable=protected-access\n      if value < type_checkers._FLOAT_MIN:\n        raise ParseError('Float value too small')\n  if value == 'nan':\n    raise ParseError('Couldn\\'t parse float \"nan\", use \"NaN\" instead')\n  try:\n    # Assume Python compatible syntax.\n    return float(value)\n  except ValueError:\n    # Check alternative spellings.\n    if value == _NEG_INFINITY:\n      return float('-inf')\n    elif value == _INFINITY:\n      return float('inf')\n    elif value == _NAN:\n      return float('nan')\n    else:\n      raise ParseError('Couldn\\'t parse float: {0}'.format(value))\n\n\ndef _ConvertBool(value, require_str):\n  \"\"\"Convert a boolean value.\n\n  Args:\n    value: A scalar value to convert.\n    require_str: If True, value must be a str.\n\n  Returns:\n    The bool parsed.\n\n  Raises:\n    ParseError: If a boolean value couldn't be consumed.\n  \"\"\"\n  if require_str:\n    if value == 'true':\n      return True\n    elif value == 'false':\n      return False\n    else:\n      raise ParseError('Expected \"true\" or \"false\", not {0}'.format(value))\n\n  if not isinstance(value, bool):\n    raise ParseError('Expected true or false without quotes')\n  return value\n\n_WKTJSONMETHODS = {\n    'google.protobuf.Any': ['_AnyMessageToJsonObject',\n                            '_ConvertAnyMessage'],\n    'google.protobuf.Duration': ['_GenericMessageToJsonObject',\n                                 '_ConvertGenericMessage'],\n    'google.protobuf.FieldMask': ['_GenericMessageToJsonObject',\n                                  '_ConvertGenericMessage'],\n    'google.protobuf.ListValue': ['_ListValueMessageToJsonObject',\n                                  '_ConvertListValueMessage'],\n    'google.protobuf.Struct': ['_StructMessageToJsonObject',\n                               '_ConvertStructMessage'],\n    'google.protobuf.Timestamp': ['_GenericMessageToJsonObject',\n                                  '_ConvertGenericMessage'],\n    'google.protobuf.Value': ['_ValueMessageToJsonObject',\n                              '_ConvertValueMessage']\n}\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/message.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n# TODO(robinson): We should just make these methods all \"pure-virtual\" and move\n# all implementation out, into reflection.py for now.\n\n\n\"\"\"Contains an abstract base class for protocol messages.\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\nclass Error(Exception):\n  \"\"\"Base error type for this module.\"\"\"\n  pass\n\n\nclass DecodeError(Error):\n  \"\"\"Exception raised when deserializing messages.\"\"\"\n  pass\n\n\nclass EncodeError(Error):\n  \"\"\"Exception raised when serializing messages.\"\"\"\n  pass\n\n\nclass Message(object):\n\n  \"\"\"Abstract base class for protocol messages.\n\n  Protocol message classes are almost always generated by the protocol\n  compiler.  These generated types subclass Message and implement the methods\n  shown below.\n  \"\"\"\n\n  # TODO(robinson): Link to an HTML document here.\n\n  # TODO(robinson): Document that instances of this class will also\n  # have an Extensions attribute with __getitem__ and __setitem__.\n  # Again, not sure how to best convey this.\n\n  # TODO(robinson): Document that the class must also have a static\n  #   RegisterExtension(extension_field) method.\n  #   Not sure how to best express at this point.\n\n  # TODO(robinson): Document these fields and methods.\n\n  __slots__ = []\n\n  #: The :class:`google.protobuf.descriptor.Descriptor` for this message type.\n  DESCRIPTOR = None\n\n  def __deepcopy__(self, memo=None):\n    clone = type(self)()\n    clone.MergeFrom(self)\n    return clone\n\n  def __eq__(self, other_msg):\n    \"\"\"Recursively compares two messages by value and structure.\"\"\"\n    raise NotImplementedError\n\n  def __ne__(self, other_msg):\n    # Can't just say self != other_msg, since that would infinitely recurse. :)\n    return not self == other_msg\n\n  def __hash__(self):\n    raise TypeError('unhashable object')\n\n  def __str__(self):\n    \"\"\"Outputs a human-readable representation of the message.\"\"\"\n    raise NotImplementedError\n\n  def __unicode__(self):\n    \"\"\"Outputs a human-readable representation of the message.\"\"\"\n    raise NotImplementedError\n\n  def MergeFrom(self, other_msg):\n    \"\"\"Merges the contents of the specified message into current message.\n\n    This method merges the contents of the specified message into the current\n    message. Singular fields that are set in the specified message overwrite\n    the corresponding fields in the current message. Repeated fields are\n    appended. Singular sub-messages and groups are recursively merged.\n\n    Args:\n      other_msg (Message): A message to merge into the current message.\n    \"\"\"\n    raise NotImplementedError\n\n  def CopyFrom(self, other_msg):\n    \"\"\"Copies the content of the specified message into the current message.\n\n    The method clears the current message and then merges the specified\n    message using MergeFrom.\n\n    Args:\n      other_msg (Message): A message to copy into the current one.\n    \"\"\"\n    if self is other_msg:\n      return\n    self.Clear()\n    self.MergeFrom(other_msg)\n\n  def Clear(self):\n    \"\"\"Clears all data that was set in the message.\"\"\"\n    raise NotImplementedError\n\n  def SetInParent(self):\n    \"\"\"Mark this as present in the parent.\n\n    This normally happens automatically when you assign a field of a\n    sub-message, but sometimes you want to make the sub-message\n    present while keeping it empty.  If you find yourself using this,\n    you may want to reconsider your design.\n    \"\"\"\n    raise NotImplementedError\n\n  def IsInitialized(self):\n    \"\"\"Checks if the message is initialized.\n\n    Returns:\n      bool: The method returns True if the message is initialized (i.e. all of\n      its required fields are set).\n    \"\"\"\n    raise NotImplementedError\n\n  # TODO(robinson): MergeFromString() should probably return None and be\n  # implemented in terms of a helper that returns the # of bytes read.  Our\n  # deserialization routines would use the helper when recursively\n  # deserializing, but the end user would almost always just want the no-return\n  # MergeFromString().\n\n  def MergeFromString(self, serialized):\n    \"\"\"Merges serialized protocol buffer data into this message.\n\n    When we find a field in `serialized` that is already present\n    in this message:\n\n    -   If it's a \"repeated\" field, we append to the end of our list.\n    -   Else, if it's a scalar, we overwrite our field.\n    -   Else, (it's a nonrepeated composite), we recursively merge\n        into the existing composite.\n\n    Args:\n      serialized (bytes): Any object that allows us to call\n        ``memoryview(serialized)`` to access a string of bytes using the\n        buffer interface.\n\n    Returns:\n      int: The number of bytes read from `serialized`.\n      For non-group messages, this will always be `len(serialized)`,\n      but for messages which are actually groups, this will\n      generally be less than `len(serialized)`, since we must\n      stop when we reach an ``END_GROUP`` tag.  Note that if\n      we *do* stop because of an ``END_GROUP`` tag, the number\n      of bytes returned does not include the bytes\n      for the ``END_GROUP`` tag information.\n\n    Raises:\n      DecodeError: if the input cannot be parsed.\n    \"\"\"\n    # TODO(robinson): Document handling of unknown fields.\n    # TODO(robinson): When we switch to a helper, this will return None.\n    raise NotImplementedError\n\n  def ParseFromString(self, serialized):\n    \"\"\"Parse serialized protocol buffer data into this message.\n\n    Like :func:`MergeFromString()`, except we clear the object first.\n\n    Raises:\n      message.DecodeError if the input cannot be parsed.\n    \"\"\"\n    self.Clear()\n    return self.MergeFromString(serialized)\n\n  def SerializeToString(self, **kwargs):\n    \"\"\"Serializes the protocol message to a binary string.\n\n    Keyword Args:\n      deterministic (bool): If true, requests deterministic serialization\n        of the protobuf, with predictable ordering of map keys.\n\n    Returns:\n      A binary string representation of the message if all of the required\n      fields in the message are set (i.e. the message is initialized).\n\n    Raises:\n      EncodeError: if the message isn't initialized (see :func:`IsInitialized`).\n    \"\"\"\n    raise NotImplementedError\n\n  def SerializePartialToString(self, **kwargs):\n    \"\"\"Serializes the protocol message to a binary string.\n\n    This method is similar to SerializeToString but doesn't check if the\n    message is initialized.\n\n    Keyword Args:\n      deterministic (bool): If true, requests deterministic serialization\n        of the protobuf, with predictable ordering of map keys.\n\n    Returns:\n      bytes: A serialized representation of the partial message.\n    \"\"\"\n    raise NotImplementedError\n\n  # TODO(robinson): Decide whether we like these better\n  # than auto-generated has_foo() and clear_foo() methods\n  # on the instances themselves.  This way is less consistent\n  # with C++, but it makes reflection-type access easier and\n  # reduces the number of magically autogenerated things.\n  #\n  # TODO(robinson): Be sure to document (and test) exactly\n  # which field names are accepted here.  Are we case-sensitive?\n  # What do we do with fields that share names with Python keywords\n  # like 'lambda' and 'yield'?\n  #\n  # nnorwitz says:\n  # \"\"\"\n  # Typically (in python), an underscore is appended to names that are\n  # keywords. So they would become lambda_ or yield_.\n  # \"\"\"\n  def ListFields(self):\n    \"\"\"Returns a list of (FieldDescriptor, value) tuples for present fields.\n\n    A message field is non-empty if HasField() would return true. A singular\n    primitive field is non-empty if HasField() would return true in proto2 or it\n    is non zero in proto3. A repeated field is non-empty if it contains at least\n    one element. The fields are ordered by field number.\n\n    Returns:\n      list[tuple(FieldDescriptor, value)]: field descriptors and values\n      for all fields in the message which are not empty. The values vary by\n      field type.\n    \"\"\"\n    raise NotImplementedError\n\n  def HasField(self, field_name):\n    \"\"\"Checks if a certain field is set for the message.\n\n    For a oneof group, checks if any field inside is set. Note that if the\n    field_name is not defined in the message descriptor, :exc:`ValueError` will\n    be raised.\n\n    Args:\n      field_name (str): The name of the field to check for presence.\n\n    Returns:\n      bool: Whether a value has been set for the named field.\n\n    Raises:\n      ValueError: if the `field_name` is not a member of this message.\n    \"\"\"\n    raise NotImplementedError\n\n  def ClearField(self, field_name):\n    \"\"\"Clears the contents of a given field.\n\n    Inside a oneof group, clears the field set. If the name neither refers to a\n    defined field or oneof group, :exc:`ValueError` is raised.\n\n    Args:\n      field_name (str): The name of the field to check for presence.\n\n    Raises:\n      ValueError: if the `field_name` is not a member of this message.\n    \"\"\"\n    raise NotImplementedError\n\n  def WhichOneof(self, oneof_group):\n    \"\"\"Returns the name of the field that is set inside a oneof group.\n\n    If no field is set, returns None.\n\n    Args:\n      oneof_group (str): the name of the oneof group to check.\n\n    Returns:\n      str or None: The name of the group that is set, or None.\n\n    Raises:\n      ValueError: no group with the given name exists\n    \"\"\"\n    raise NotImplementedError\n\n  def HasExtension(self, extension_handle):\n    \"\"\"Checks if a certain extension is present for this message.\n\n    Extensions are retrieved using the :attr:`Extensions` mapping (if present).\n\n    Args:\n      extension_handle: The handle for the extension to check.\n\n    Returns:\n      bool: Whether the extension is present for this message.\n\n    Raises:\n      KeyError: if the extension is repeated. Similar to repeated fields,\n        there is no separate notion of presence: a \"not present\" repeated\n        extension is an empty list.\n    \"\"\"\n    raise NotImplementedError\n\n  def ClearExtension(self, extension_handle):\n    \"\"\"Clears the contents of a given extension.\n\n    Args:\n      extension_handle: The handle for the extension to clear.\n    \"\"\"\n    raise NotImplementedError\n\n  def UnknownFields(self):\n    \"\"\"Returns the UnknownFieldSet.\n\n    Returns:\n      UnknownFieldSet: The unknown fields stored in this message.\n    \"\"\"\n    raise NotImplementedError\n\n  def DiscardUnknownFields(self):\n    \"\"\"Clears all fields in the :class:`UnknownFieldSet`.\n\n    This operation is recursive for nested message.\n    \"\"\"\n    raise NotImplementedError\n\n  def ByteSize(self):\n    \"\"\"Returns the serialized size of this message.\n\n    Recursively calls ByteSize() on all contained messages.\n\n    Returns:\n      int: The number of bytes required to serialize this message.\n    \"\"\"\n    raise NotImplementedError\n\n  @classmethod\n  def FromString(cls, s):\n    raise NotImplementedError\n\n  @staticmethod\n  def RegisterExtension(extension_handle):\n    raise NotImplementedError\n\n  def _SetListener(self, message_listener):\n    \"\"\"Internal method used by the protocol message implementation.\n    Clients should not call this directly.\n\n    Sets a listener that this message will call on certain state transitions.\n\n    The purpose of this method is to register back-edges from children to\n    parents at runtime, for the purpose of setting \"has\" bits and\n    byte-size-dirty bits in the parent and ancestor objects whenever a child or\n    descendant object is modified.\n\n    If the client wants to disconnect this Message from the object tree, she\n    explicitly sets callback to None.\n\n    If message_listener is None, unregisters any existing listener.  Otherwise,\n    message_listener must implement the MessageListener interface in\n    internal/message_listener.py, and we discard any listener registered\n    via a previous _SetListener() call.\n    \"\"\"\n    raise NotImplementedError\n\n  def __getstate__(self):\n    \"\"\"Support the pickle protocol.\"\"\"\n    return dict(serialized=self.SerializePartialToString())\n\n  def __setstate__(self, state):\n    \"\"\"Support the pickle protocol.\"\"\"\n    self.__init__()\n    serialized = state['serialized']\n    # On Python 3, using encoding='latin1' is required for unpickling\n    # protos pickled by Python 2.\n    if not isinstance(serialized, bytes):\n      serialized = serialized.encode('latin1')\n    self.ParseFromString(serialized)\n\n  def __reduce__(self):\n    message_descriptor = self.DESCRIPTOR\n    if message_descriptor.containing_type is None:\n      return type(self), (), self.__getstate__()\n    # the message type must be nested.\n    # Python does not pickle nested classes; use the symbol_database on the\n    # receiving end.\n    container = message_descriptor\n    return (_InternalConstructMessage, (container.full_name,),\n            self.__getstate__())\n\n\ndef _InternalConstructMessage(full_name):\n  \"\"\"Constructs a nested message.\"\"\"\n  from google.protobuf import symbol_database  # pylint:disable=g-import-not-at-top\n\n  return symbol_database.Default().GetSymbol(full_name)()\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/message_factory.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Provides a factory class for generating dynamic messages.\n\nThe easiest way to use this class is if you have access to the FileDescriptor\nprotos containing the messages you want to create you can just do the following:\n\nmessage_classes = message_factory.GetMessages(iterable_of_file_descriptors)\nmy_proto_instance = message_classes['some.proto.package.MessageName']()\n\"\"\"\n\n__author__ = 'matthewtoia@google.com (Matt Toia)'\n\nfrom google.protobuf.internal import api_implementation\nfrom google.protobuf import descriptor_pool\nfrom google.protobuf import message\n\nif api_implementation.Type() == 'cpp':\n  from google.protobuf.pyext import cpp_message as message_impl\nelse:\n  from google.protobuf.internal import python_message as message_impl\n\n\n# The type of all Message classes.\n_GENERATED_PROTOCOL_MESSAGE_TYPE = message_impl.GeneratedProtocolMessageType\n\n\nclass MessageFactory(object):\n  \"\"\"Factory for creating Proto2 messages from descriptors in a pool.\"\"\"\n\n  def __init__(self, pool=None):\n    \"\"\"Initializes a new factory.\"\"\"\n    self.pool = pool or descriptor_pool.DescriptorPool()\n\n    # local cache of all classes built from protobuf descriptors\n    self._classes = {}\n\n  def GetPrototype(self, descriptor):\n    \"\"\"Obtains a proto2 message class based on the passed in descriptor.\n\n    Passing a descriptor with a fully qualified name matching a previous\n    invocation will cause the same class to be returned.\n\n    Args:\n      descriptor: The descriptor to build from.\n\n    Returns:\n      A class describing the passed in descriptor.\n    \"\"\"\n    if descriptor not in self._classes:\n      result_class = self.CreatePrototype(descriptor)\n      # The assignment to _classes is redundant for the base implementation, but\n      # might avoid confusion in cases where CreatePrototype gets overridden and\n      # does not call the base implementation.\n      self._classes[descriptor] = result_class\n      return result_class\n    return self._classes[descriptor]\n\n  def CreatePrototype(self, descriptor):\n    \"\"\"Builds a proto2 message class based on the passed in descriptor.\n\n    Don't call this function directly, it always creates a new class. Call\n    GetPrototype() instead. This method is meant to be overridden in subblasses\n    to perform additional operations on the newly constructed class.\n\n    Args:\n      descriptor: The descriptor to build from.\n\n    Returns:\n      A class describing the passed in descriptor.\n    \"\"\"\n    descriptor_name = descriptor.name\n    result_class = _GENERATED_PROTOCOL_MESSAGE_TYPE(\n        descriptor_name,\n        (message.Message,),\n        {\n            'DESCRIPTOR': descriptor,\n            # If module not set, it wrongly points to message_factory module.\n            '__module__': None,\n        })\n    result_class._FACTORY = self  # pylint: disable=protected-access\n    # Assign in _classes before doing recursive calls to avoid infinite\n    # recursion.\n    self._classes[descriptor] = result_class\n    for field in descriptor.fields:\n      if field.message_type:\n        self.GetPrototype(field.message_type)\n    for extension in result_class.DESCRIPTOR.extensions:\n      if extension.containing_type not in self._classes:\n        self.GetPrototype(extension.containing_type)\n      extended_class = self._classes[extension.containing_type]\n      extended_class.RegisterExtension(extension)\n    return result_class\n\n  def GetMessages(self, files):\n    \"\"\"Gets all the messages from a specified file.\n\n    This will find and resolve dependencies, failing if the descriptor\n    pool cannot satisfy them.\n\n    Args:\n      files: The file names to extract messages from.\n\n    Returns:\n      A dictionary mapping proto names to the message classes. This will include\n      any dependent messages as well as any messages defined in the same file as\n      a specified message.\n    \"\"\"\n    result = {}\n    for file_name in files:\n      file_desc = self.pool.FindFileByName(file_name)\n      for desc in file_desc.message_types_by_name.values():\n        result[desc.full_name] = self.GetPrototype(desc)\n\n      # While the extension FieldDescriptors are created by the descriptor pool,\n      # the python classes created in the factory need them to be registered\n      # explicitly, which is done below.\n      #\n      # The call to RegisterExtension will specifically check if the\n      # extension was already registered on the object and either\n      # ignore the registration if the original was the same, or raise\n      # an error if they were different.\n\n      for extension in file_desc.extensions_by_name.values():\n        if extension.containing_type not in self._classes:\n          self.GetPrototype(extension.containing_type)\n        extended_class = self._classes[extension.containing_type]\n        extended_class.RegisterExtension(extension)\n    return result\n\n\n_FACTORY = MessageFactory()\n\n\ndef GetMessages(file_protos):\n  \"\"\"Builds a dictionary of all the messages available in a set of files.\n\n  Args:\n    file_protos: Iterable of FileDescriptorProto to build messages out of.\n\n  Returns:\n    A dictionary mapping proto names to the message classes. This will include\n    any dependent messages as well as any messages defined in the same file as\n    a specified message.\n  \"\"\"\n  # The cpp implementation of the protocol buffer library requires to add the\n  # message in topological order of the dependency graph.\n  file_by_name = {file_proto.name: file_proto for file_proto in file_protos}\n  def _AddFile(file_proto):\n    for dependency in file_proto.dependency:\n      if dependency in file_by_name:\n        # Remove from elements to be visited, in order to cut cycles.\n        _AddFile(file_by_name.pop(dependency))\n    _FACTORY.pool.Add(file_proto)\n  while file_by_name:\n    _AddFile(file_by_name.popitem()[1])\n  return _FACTORY.GetMessages([file_proto.name for file_proto in file_protos])\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/proto_builder.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Dynamic Protobuf class creator.\"\"\"\n\nfrom collections import OrderedDict\nimport hashlib\nimport os\n\nfrom google.protobuf import descriptor_pb2\nfrom google.protobuf import descriptor\nfrom google.protobuf import message_factory\n\n\ndef _GetMessageFromFactory(factory, full_name):\n  \"\"\"Get a proto class from the MessageFactory by name.\n\n  Args:\n    factory: a MessageFactory instance.\n    full_name: str, the fully qualified name of the proto type.\n  Returns:\n    A class, for the type identified by full_name.\n  Raises:\n    KeyError, if the proto is not found in the factory's descriptor pool.\n  \"\"\"\n  proto_descriptor = factory.pool.FindMessageTypeByName(full_name)\n  proto_cls = factory.GetPrototype(proto_descriptor)\n  return proto_cls\n\n\ndef MakeSimpleProtoClass(fields, full_name=None, pool=None):\n  \"\"\"Create a Protobuf class whose fields are basic types.\n\n  Note: this doesn't validate field names!\n\n  Args:\n    fields: dict of {name: field_type} mappings for each field in the proto. If\n        this is an OrderedDict the order will be maintained, otherwise the\n        fields will be sorted by name.\n    full_name: optional str, the fully-qualified name of the proto type.\n    pool: optional DescriptorPool instance.\n  Returns:\n    a class, the new protobuf class with a FileDescriptor.\n  \"\"\"\n  factory = message_factory.MessageFactory(pool=pool)\n\n  if full_name is not None:\n    try:\n      proto_cls = _GetMessageFromFactory(factory, full_name)\n      return proto_cls\n    except KeyError:\n      # The factory's DescriptorPool doesn't know about this class yet.\n      pass\n\n  # Get a list of (name, field_type) tuples from the fields dict. If fields was\n  # an OrderedDict we keep the order, but otherwise we sort the field to ensure\n  # consistent ordering.\n  field_items = fields.items()\n  if not isinstance(fields, OrderedDict):\n    field_items = sorted(field_items)\n\n  # Use a consistent file name that is unlikely to conflict with any imported\n  # proto files.\n  fields_hash = hashlib.sha1()\n  for f_name, f_type in field_items:\n    fields_hash.update(f_name.encode('utf-8'))\n    fields_hash.update(str(f_type).encode('utf-8'))\n  proto_file_name = fields_hash.hexdigest() + '.proto'\n\n  # If the proto is anonymous, use the same hash to name it.\n  if full_name is None:\n    full_name = ('net.proto2.python.public.proto_builder.AnonymousProto_' +\n                 fields_hash.hexdigest())\n    try:\n      proto_cls = _GetMessageFromFactory(factory, full_name)\n      return proto_cls\n    except KeyError:\n      # The factory's DescriptorPool doesn't know about this class yet.\n      pass\n\n  # This is the first time we see this proto: add a new descriptor to the pool.\n  factory.pool.Add(\n      _MakeFileDescriptorProto(proto_file_name, full_name, field_items))\n  return _GetMessageFromFactory(factory, full_name)\n\n\ndef _MakeFileDescriptorProto(proto_file_name, full_name, field_items):\n  \"\"\"Populate FileDescriptorProto for MessageFactory's DescriptorPool.\"\"\"\n  package, name = full_name.rsplit('.', 1)\n  file_proto = descriptor_pb2.FileDescriptorProto()\n  file_proto.name = os.path.join(package.replace('.', '/'), proto_file_name)\n  file_proto.package = package\n  desc_proto = file_proto.message_type.add()\n  desc_proto.name = name\n  for f_number, (f_name, f_type) in enumerate(field_items, 1):\n    field_proto = desc_proto.field.add()\n    field_proto.name = f_name\n    # # If the number falls in the reserved range, reassign it to the correct\n    # # number after the range.\n    if f_number >= descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER:\n      f_number += (\n          descriptor.FieldDescriptor.LAST_RESERVED_FIELD_NUMBER -\n          descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER + 1)\n    field_proto.number = f_number\n    field_proto.label = descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL\n    field_proto.type = f_type\n  return file_proto\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/pyext/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/pyext/cpp_message.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Protocol message implementation hooks for C++ implementation.\n\nContains helper functions used to create protocol message classes from\nDescriptor objects at runtime backed by the protocol buffer C++ API.\n\"\"\"\n\n__author__ = 'tibell@google.com (Johan Tibell)'\n\nfrom google.protobuf.pyext import _message\n\n\nclass GeneratedProtocolMessageType(_message.MessageMeta):\n\n  \"\"\"Metaclass for protocol message classes created at runtime from Descriptors.\n\n  The protocol compiler currently uses this metaclass to create protocol\n  message classes at runtime.  Clients can also manually create their own\n  classes at runtime, as in this example:\n\n  mydescriptor = Descriptor(.....)\n  factory = symbol_database.Default()\n  factory.pool.AddDescriptor(mydescriptor)\n  MyProtoClass = factory.GetPrototype(mydescriptor)\n  myproto_instance = MyProtoClass()\n  myproto.foo_field = 23\n  ...\n\n  The above example will not work for nested types. If you wish to include them,\n  use reflection.MakeClass() instead of manually instantiating the class in\n  order to create the appropriate class structure.\n  \"\"\"\n\n  # Must be consistent with the protocol-compiler code in\n  # proto2/compiler/internal/generator.*.\n  _DESCRIPTOR_KEY = 'DESCRIPTOR'\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/pyext/python_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/pyext/python.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\\"google/protobuf/pyext/python.proto\\x12\\x1fgoogle.protobuf.python.internal\\\"\\xbc\\x02\\n\\x0cTestAllTypes\\x12\\\\\\n\\x17repeated_nested_message\\x18\\x01 \\x03(\\x0b\\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\\x12\\\\\\n\\x17optional_nested_message\\x18\\x02 \\x01(\\x0b\\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\\x12\\x16\\n\\x0eoptional_int32\\x18\\x03 \\x01(\\x05\\x1aX\\n\\rNestedMessage\\x12\\n\\n\\x02\\x62\\x62\\x18\\x01 \\x01(\\x05\\x12;\\n\\x02\\x63\\x63\\x18\\x02 \\x01(\\x0b\\x32/.google.protobuf.python.internal.ForeignMessage\\\"&\\n\\x0e\\x46oreignMessage\\x12\\t\\n\\x01\\x63\\x18\\x01 \\x01(\\x05\\x12\\t\\n\\x01\\x64\\x18\\x02 \\x03(\\x05\\\"\\x1d\\n\\x11TestAllExtensions*\\x08\\x08\\x01\\x10\\x80\\x80\\x80\\x80\\x02:\\x9a\\x01\\n!optional_nested_message_extension\\x12\\x32.google.protobuf.python.internal.TestAllExtensions\\x18\\x01 \\x01(\\x0b\\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage:\\x9a\\x01\\n!repeated_nested_message_extension\\x12\\x32.google.protobuf.python.internal.TestAllExtensions\\x18\\x02 \\x03(\\x0b\\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessageB\\x02H\\x01')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.pyext.python_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n  TestAllExtensions.RegisterExtension(optional_nested_message_extension)\n  TestAllExtensions.RegisterExtension(repeated_nested_message_extension)\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'H\\001'\n  _TESTALLTYPES._serialized_start=72\n  _TESTALLTYPES._serialized_end=388\n  _TESTALLTYPES_NESTEDMESSAGE._serialized_start=300\n  _TESTALLTYPES_NESTEDMESSAGE._serialized_end=388\n  _FOREIGNMESSAGE._serialized_start=390\n  _FOREIGNMESSAGE._serialized_end=428\n  _TESTALLEXTENSIONS._serialized_start=430\n  _TESTALLEXTENSIONS._serialized_end=459\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/reflection.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n# This code is meant to work on Python 2.4 and above only.\n\n\"\"\"Contains a metaclass and helper functions used to create\nprotocol message classes from Descriptor objects at runtime.\n\nRecall that a metaclass is the \"type\" of a class.\n(A class is to a metaclass what an instance is to a class.)\n\nIn this case, we use the GeneratedProtocolMessageType metaclass\nto inject all the useful functionality into the classes\noutput by the protocol compiler at compile-time.\n\nThe upshot of all this is that the real implementation\ndetails for ALL pure-Python protocol buffers are *here in\nthis file*.\n\"\"\"\n\n__author__ = 'robinson@google.com (Will Robinson)'\n\n\nfrom google.protobuf import message_factory\nfrom google.protobuf import symbol_database\n\n# The type of all Message classes.\n# Part of the public interface, but normally only used by message factories.\nGeneratedProtocolMessageType = message_factory._GENERATED_PROTOCOL_MESSAGE_TYPE\n\nMESSAGE_CLASS_CACHE = {}\n\n\n# Deprecated. Please NEVER use reflection.ParseMessage().\ndef ParseMessage(descriptor, byte_str):\n  \"\"\"Generate a new Message instance from this Descriptor and a byte string.\n\n  DEPRECATED: ParseMessage is deprecated because it is using MakeClass().\n  Please use MessageFactory.GetPrototype() instead.\n\n  Args:\n    descriptor: Protobuf Descriptor object\n    byte_str: Serialized protocol buffer byte string\n\n  Returns:\n    Newly created protobuf Message object.\n  \"\"\"\n  result_class = MakeClass(descriptor)\n  new_msg = result_class()\n  new_msg.ParseFromString(byte_str)\n  return new_msg\n\n\n# Deprecated. Please NEVER use reflection.MakeClass().\ndef MakeClass(descriptor):\n  \"\"\"Construct a class object for a protobuf described by descriptor.\n\n  DEPRECATED: use MessageFactory.GetPrototype() instead.\n\n  Args:\n    descriptor: A descriptor.Descriptor object describing the protobuf.\n  Returns:\n    The Message class object described by the descriptor.\n  \"\"\"\n  # Original implementation leads to duplicate message classes, which won't play\n  # well with extensions. Message factory info is also missing.\n  # Redirect to message_factory.\n  return symbol_database.Default().GetPrototype(descriptor)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/service.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"DEPRECATED:  Declares the RPC service interfaces.\n\nThis module declares the abstract interfaces underlying proto2 RPC\nservices.  These are intended to be independent of any particular RPC\nimplementation, so that proto2 services can be used on top of a variety\nof implementations.  Starting with version 2.3.0, RPC implementations should\nnot try to build on these, but should instead provide code generator plugins\nwhich generate code specific to the particular RPC implementation.  This way\nthe generated code can be more appropriate for the implementation in use\nand can avoid unnecessary layers of indirection.\n\"\"\"\n\n__author__ = 'petar@google.com (Petar Petrov)'\n\n\nclass RpcException(Exception):\n  \"\"\"Exception raised on failed blocking RPC method call.\"\"\"\n  pass\n\n\nclass Service(object):\n\n  \"\"\"Abstract base interface for protocol-buffer-based RPC services.\n\n  Services themselves are abstract classes (implemented either by servers or as\n  stubs), but they subclass this base interface. The methods of this\n  interface can be used to call the methods of the service without knowing\n  its exact type at compile time (analogous to the Message interface).\n  \"\"\"\n\n  def GetDescriptor():\n    \"\"\"Retrieves this service's descriptor.\"\"\"\n    raise NotImplementedError\n\n  def CallMethod(self, method_descriptor, rpc_controller,\n                 request, done):\n    \"\"\"Calls a method of the service specified by method_descriptor.\n\n    If \"done\" is None then the call is blocking and the response\n    message will be returned directly.  Otherwise the call is asynchronous\n    and \"done\" will later be called with the response value.\n\n    In the blocking case, RpcException will be raised on error.\n\n    Preconditions:\n\n    * method_descriptor.service == GetDescriptor\n    * request is of the exact same classes as returned by\n      GetRequestClass(method).\n    * After the call has started, the request must not be modified.\n    * \"rpc_controller\" is of the correct type for the RPC implementation being\n      used by this Service.  For stubs, the \"correct type\" depends on the\n      RpcChannel which the stub is using.\n\n    Postconditions:\n\n    * \"done\" will be called when the method is complete.  This may be\n      before CallMethod() returns or it may be at some point in the future.\n    * If the RPC failed, the response value passed to \"done\" will be None.\n      Further details about the failure can be found by querying the\n      RpcController.\n    \"\"\"\n    raise NotImplementedError\n\n  def GetRequestClass(self, method_descriptor):\n    \"\"\"Returns the class of the request message for the specified method.\n\n    CallMethod() requires that the request is of a particular subclass of\n    Message. GetRequestClass() gets the default instance of this required\n    type.\n\n    Example:\n      method = service.GetDescriptor().FindMethodByName(\"Foo\")\n      request = stub.GetRequestClass(method)()\n      request.ParseFromString(input)\n      service.CallMethod(method, request, callback)\n    \"\"\"\n    raise NotImplementedError\n\n  def GetResponseClass(self, method_descriptor):\n    \"\"\"Returns the class of the response message for the specified method.\n\n    This method isn't really needed, as the RpcChannel's CallMethod constructs\n    the response protocol message. It's provided anyway in case it is useful\n    for the caller to know the response type in advance.\n    \"\"\"\n    raise NotImplementedError\n\n\nclass RpcController(object):\n\n  \"\"\"An RpcController mediates a single method call.\n\n  The primary purpose of the controller is to provide a way to manipulate\n  settings specific to the RPC implementation and to find out about RPC-level\n  errors. The methods provided by the RpcController interface are intended\n  to be a \"least common denominator\" set of features which we expect all\n  implementations to support.  Specific implementations may provide more\n  advanced features (e.g. deadline propagation).\n  \"\"\"\n\n  # Client-side methods below\n\n  def Reset(self):\n    \"\"\"Resets the RpcController to its initial state.\n\n    After the RpcController has been reset, it may be reused in\n    a new call. Must not be called while an RPC is in progress.\n    \"\"\"\n    raise NotImplementedError\n\n  def Failed(self):\n    \"\"\"Returns true if the call failed.\n\n    After a call has finished, returns true if the call failed.  The possible\n    reasons for failure depend on the RPC implementation.  Failed() must not\n    be called before a call has finished.  If Failed() returns true, the\n    contents of the response message are undefined.\n    \"\"\"\n    raise NotImplementedError\n\n  def ErrorText(self):\n    \"\"\"If Failed is true, returns a human-readable description of the error.\"\"\"\n    raise NotImplementedError\n\n  def StartCancel(self):\n    \"\"\"Initiate cancellation.\n\n    Advises the RPC system that the caller desires that the RPC call be\n    canceled.  The RPC system may cancel it immediately, may wait awhile and\n    then cancel it, or may not even cancel the call at all.  If the call is\n    canceled, the \"done\" callback will still be called and the RpcController\n    will indicate that the call failed at that time.\n    \"\"\"\n    raise NotImplementedError\n\n  # Server-side methods below\n\n  def SetFailed(self, reason):\n    \"\"\"Sets a failure reason.\n\n    Causes Failed() to return true on the client side.  \"reason\" will be\n    incorporated into the message returned by ErrorText().  If you find\n    you need to return machine-readable information about failures, you\n    should incorporate it into your response protocol buffer and should\n    NOT call SetFailed().\n    \"\"\"\n    raise NotImplementedError\n\n  def IsCanceled(self):\n    \"\"\"Checks if the client cancelled the RPC.\n\n    If true, indicates that the client canceled the RPC, so the server may\n    as well give up on replying to it.  The server should still call the\n    final \"done\" callback.\n    \"\"\"\n    raise NotImplementedError\n\n  def NotifyOnCancel(self, callback):\n    \"\"\"Sets a callback to invoke on cancel.\n\n    Asks that the given callback be called when the RPC is canceled.  The\n    callback will always be called exactly once.  If the RPC completes without\n    being canceled, the callback will be called after completion.  If the RPC\n    has already been canceled when NotifyOnCancel() is called, the callback\n    will be called immediately.\n\n    NotifyOnCancel() must be called no more than once per request.\n    \"\"\"\n    raise NotImplementedError\n\n\nclass RpcChannel(object):\n\n  \"\"\"Abstract interface for an RPC channel.\n\n  An RpcChannel represents a communication line to a service which can be used\n  to call that service's methods.  The service may be running on another\n  machine. Normally, you should not use an RpcChannel directly, but instead\n  construct a stub {@link Service} wrapping it.  Example:\n\n  Example:\n    RpcChannel channel = rpcImpl.Channel(\"remotehost.example.com:1234\")\n    RpcController controller = rpcImpl.Controller()\n    MyService service = MyService_Stub(channel)\n    service.MyMethod(controller, request, callback)\n  \"\"\"\n\n  def CallMethod(self, method_descriptor, rpc_controller,\n                 request, response_class, done):\n    \"\"\"Calls the method identified by the descriptor.\n\n    Call the given method of the remote service.  The signature of this\n    procedure looks the same as Service.CallMethod(), but the requirements\n    are less strict in one important way:  the request object doesn't have to\n    be of any specific class as long as its descriptor is method.input_type.\n    \"\"\"\n    raise NotImplementedError\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/service_reflection.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Contains metaclasses used to create protocol service and service stub\nclasses from ServiceDescriptor objects at runtime.\n\nThe GeneratedServiceType and GeneratedServiceStubType metaclasses are used to\ninject all useful functionality into the classes output by the protocol\ncompiler at compile-time.\n\"\"\"\n\n__author__ = 'petar@google.com (Petar Petrov)'\n\n\nclass GeneratedServiceType(type):\n\n  \"\"\"Metaclass for service classes created at runtime from ServiceDescriptors.\n\n  Implementations for all methods described in the Service class are added here\n  by this class. We also create properties to allow getting/setting all fields\n  in the protocol message.\n\n  The protocol compiler currently uses this metaclass to create protocol service\n  classes at runtime. Clients can also manually create their own classes at\n  runtime, as in this example::\n\n    mydescriptor = ServiceDescriptor(.....)\n    class MyProtoService(service.Service):\n      __metaclass__ = GeneratedServiceType\n      DESCRIPTOR = mydescriptor\n    myservice_instance = MyProtoService()\n    # ...\n  \"\"\"\n\n  _DESCRIPTOR_KEY = 'DESCRIPTOR'\n\n  def __init__(cls, name, bases, dictionary):\n    \"\"\"Creates a message service class.\n\n    Args:\n      name: Name of the class (ignored, but required by the metaclass\n        protocol).\n      bases: Base classes of the class being constructed.\n      dictionary: The class dictionary of the class being constructed.\n        dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object\n        describing this protocol service type.\n    \"\"\"\n    # Don't do anything if this class doesn't have a descriptor. This happens\n    # when a service class is subclassed.\n    if GeneratedServiceType._DESCRIPTOR_KEY not in dictionary:\n      return\n\n    descriptor = dictionary[GeneratedServiceType._DESCRIPTOR_KEY]\n    service_builder = _ServiceBuilder(descriptor)\n    service_builder.BuildService(cls)\n    cls.DESCRIPTOR = descriptor\n\n\nclass GeneratedServiceStubType(GeneratedServiceType):\n\n  \"\"\"Metaclass for service stubs created at runtime from ServiceDescriptors.\n\n  This class has similar responsibilities as GeneratedServiceType, except that\n  it creates the service stub classes.\n  \"\"\"\n\n  _DESCRIPTOR_KEY = 'DESCRIPTOR'\n\n  def __init__(cls, name, bases, dictionary):\n    \"\"\"Creates a message service stub class.\n\n    Args:\n      name: Name of the class (ignored, here).\n      bases: Base classes of the class being constructed.\n      dictionary: The class dictionary of the class being constructed.\n        dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object\n        describing this protocol service type.\n    \"\"\"\n    super(GeneratedServiceStubType, cls).__init__(name, bases, dictionary)\n    # Don't do anything if this class doesn't have a descriptor. This happens\n    # when a service stub is subclassed.\n    if GeneratedServiceStubType._DESCRIPTOR_KEY not in dictionary:\n      return\n\n    descriptor = dictionary[GeneratedServiceStubType._DESCRIPTOR_KEY]\n    service_stub_builder = _ServiceStubBuilder(descriptor)\n    service_stub_builder.BuildServiceStub(cls)\n\n\nclass _ServiceBuilder(object):\n\n  \"\"\"This class constructs a protocol service class using a service descriptor.\n\n  Given a service descriptor, this class constructs a class that represents\n  the specified service descriptor. One service builder instance constructs\n  exactly one service class. That means all instances of that class share the\n  same builder.\n  \"\"\"\n\n  def __init__(self, service_descriptor):\n    \"\"\"Initializes an instance of the service class builder.\n\n    Args:\n      service_descriptor: ServiceDescriptor to use when constructing the\n        service class.\n    \"\"\"\n    self.descriptor = service_descriptor\n\n  def BuildService(builder, cls):\n    \"\"\"Constructs the service class.\n\n    Args:\n      cls: The class that will be constructed.\n    \"\"\"\n\n    # CallMethod needs to operate with an instance of the Service class. This\n    # internal wrapper function exists only to be able to pass the service\n    # instance to the method that does the real CallMethod work.\n    # Making sure to use exact argument names from the abstract interface in\n    # service.py to match the type signature\n    def _WrapCallMethod(self, method_descriptor, rpc_controller, request, done):\n      return builder._CallMethod(self, method_descriptor, rpc_controller,\n                                 request, done)\n\n    def _WrapGetRequestClass(self, method_descriptor):\n      return builder._GetRequestClass(method_descriptor)\n\n    def _WrapGetResponseClass(self, method_descriptor):\n      return builder._GetResponseClass(method_descriptor)\n\n    builder.cls = cls\n    cls.CallMethod = _WrapCallMethod\n    cls.GetDescriptor = staticmethod(lambda: builder.descriptor)\n    cls.GetDescriptor.__doc__ = 'Returns the service descriptor.'\n    cls.GetRequestClass = _WrapGetRequestClass\n    cls.GetResponseClass = _WrapGetResponseClass\n    for method in builder.descriptor.methods:\n      setattr(cls, method.name, builder._GenerateNonImplementedMethod(method))\n\n  def _CallMethod(self, srvc, method_descriptor,\n                  rpc_controller, request, callback):\n    \"\"\"Calls the method described by a given method descriptor.\n\n    Args:\n      srvc: Instance of the service for which this method is called.\n      method_descriptor: Descriptor that represent the method to call.\n      rpc_controller: RPC controller to use for this method's execution.\n      request: Request protocol message.\n      callback: A callback to invoke after the method has completed.\n    \"\"\"\n    if method_descriptor.containing_service != self.descriptor:\n      raise RuntimeError(\n          'CallMethod() given method descriptor for wrong service type.')\n    method = getattr(srvc, method_descriptor.name)\n    return method(rpc_controller, request, callback)\n\n  def _GetRequestClass(self, method_descriptor):\n    \"\"\"Returns the class of the request protocol message.\n\n    Args:\n      method_descriptor: Descriptor of the method for which to return the\n        request protocol message class.\n\n    Returns:\n      A class that represents the input protocol message of the specified\n      method.\n    \"\"\"\n    if method_descriptor.containing_service != self.descriptor:\n      raise RuntimeError(\n          'GetRequestClass() given method descriptor for wrong service type.')\n    return method_descriptor.input_type._concrete_class\n\n  def _GetResponseClass(self, method_descriptor):\n    \"\"\"Returns the class of the response protocol message.\n\n    Args:\n      method_descriptor: Descriptor of the method for which to return the\n        response protocol message class.\n\n    Returns:\n      A class that represents the output protocol message of the specified\n      method.\n    \"\"\"\n    if method_descriptor.containing_service != self.descriptor:\n      raise RuntimeError(\n          'GetResponseClass() given method descriptor for wrong service type.')\n    return method_descriptor.output_type._concrete_class\n\n  def _GenerateNonImplementedMethod(self, method):\n    \"\"\"Generates and returns a method that can be set for a service methods.\n\n    Args:\n      method: Descriptor of the service method for which a method is to be\n        generated.\n\n    Returns:\n      A method that can be added to the service class.\n    \"\"\"\n    return lambda inst, rpc_controller, request, callback: (\n        self._NonImplementedMethod(method.name, rpc_controller, callback))\n\n  def _NonImplementedMethod(self, method_name, rpc_controller, callback):\n    \"\"\"The body of all methods in the generated service class.\n\n    Args:\n      method_name: Name of the method being executed.\n      rpc_controller: RPC controller used to execute this method.\n      callback: A callback which will be invoked when the method finishes.\n    \"\"\"\n    rpc_controller.SetFailed('Method %s not implemented.' % method_name)\n    callback(None)\n\n\nclass _ServiceStubBuilder(object):\n\n  \"\"\"Constructs a protocol service stub class using a service descriptor.\n\n  Given a service descriptor, this class constructs a suitable stub class.\n  A stub is just a type-safe wrapper around an RpcChannel which emulates a\n  local implementation of the service.\n\n  One service stub builder instance constructs exactly one class. It means all\n  instances of that class share the same service stub builder.\n  \"\"\"\n\n  def __init__(self, service_descriptor):\n    \"\"\"Initializes an instance of the service stub class builder.\n\n    Args:\n      service_descriptor: ServiceDescriptor to use when constructing the\n        stub class.\n    \"\"\"\n    self.descriptor = service_descriptor\n\n  def BuildServiceStub(self, cls):\n    \"\"\"Constructs the stub class.\n\n    Args:\n      cls: The class that will be constructed.\n    \"\"\"\n\n    def _ServiceStubInit(stub, rpc_channel):\n      stub.rpc_channel = rpc_channel\n    self.cls = cls\n    cls.__init__ = _ServiceStubInit\n    for method in self.descriptor.methods:\n      setattr(cls, method.name, self._GenerateStubMethod(method))\n\n  def _GenerateStubMethod(self, method):\n    return (lambda inst, rpc_controller, request, callback=None:\n        self._StubMethod(inst, method, rpc_controller, request, callback))\n\n  def _StubMethod(self, stub, method_descriptor,\n                  rpc_controller, request, callback):\n    \"\"\"The body of all service methods in the generated stub class.\n\n    Args:\n      stub: Stub instance.\n      method_descriptor: Descriptor of the invoked method.\n      rpc_controller: Rpc controller to execute the method.\n      request: Request protocol message.\n      callback: A callback to execute when the method finishes.\n    Returns:\n      Response message (in case of blocking call).\n    \"\"\"\n    return stub.rpc_channel.CallMethod(\n        method_descriptor, rpc_controller, request,\n        method_descriptor.output_type._concrete_class, callback)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/source_context_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/source_context.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n$google/protobuf/source_context.proto\\x12\\x0fgoogle.protobuf\\\"\\\"\\n\\rSourceContext\\x12\\x11\\n\\tfile_name\\x18\\x01 \\x01(\\tB\\x8a\\x01\\n\\x13\\x63om.google.protobufB\\x12SourceContextProtoP\\x01Z6google.golang.org/protobuf/types/known/sourcecontextpb\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.source_context_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\022SourceContextProtoP\\001Z6google.golang.org/protobuf/types/known/sourcecontextpb\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _SOURCECONTEXT._serialized_start=57\n  _SOURCECONTEXT._serialized_end=91\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/struct_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/struct.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x1cgoogle/protobuf/struct.proto\\x12\\x0fgoogle.protobuf\\\"\\x84\\x01\\n\\x06Struct\\x12\\x33\\n\\x06\\x66ields\\x18\\x01 \\x03(\\x0b\\x32#.google.protobuf.Struct.FieldsEntry\\x1a\\x45\\n\\x0b\\x46ieldsEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12%\\n\\x05value\\x18\\x02 \\x01(\\x0b\\x32\\x16.google.protobuf.Value:\\x02\\x38\\x01\\\"\\xea\\x01\\n\\x05Value\\x12\\x30\\n\\nnull_value\\x18\\x01 \\x01(\\x0e\\x32\\x1a.google.protobuf.NullValueH\\x00\\x12\\x16\\n\\x0cnumber_value\\x18\\x02 \\x01(\\x01H\\x00\\x12\\x16\\n\\x0cstring_value\\x18\\x03 \\x01(\\tH\\x00\\x12\\x14\\n\\nbool_value\\x18\\x04 \\x01(\\x08H\\x00\\x12/\\n\\x0cstruct_value\\x18\\x05 \\x01(\\x0b\\x32\\x17.google.protobuf.StructH\\x00\\x12\\x30\\n\\nlist_value\\x18\\x06 \\x01(\\x0b\\x32\\x1a.google.protobuf.ListValueH\\x00\\x42\\x06\\n\\x04kind\\\"3\\n\\tListValue\\x12&\\n\\x06values\\x18\\x01 \\x03(\\x0b\\x32\\x16.google.protobuf.Value*\\x1b\\n\\tNullValue\\x12\\x0e\\n\\nNULL_VALUE\\x10\\x00\\x42\\x7f\\n\\x13\\x63om.google.protobufB\\x0bStructProtoP\\x01Z/google.golang.org/protobuf/types/known/structpb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.struct_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\013StructProtoP\\001Z/google.golang.org/protobuf/types/known/structpb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _STRUCT_FIELDSENTRY._options = None\n  _STRUCT_FIELDSENTRY._serialized_options = b'8\\001'\n  _NULLVALUE._serialized_start=474\n  _NULLVALUE._serialized_end=501\n  _STRUCT._serialized_start=50\n  _STRUCT._serialized_end=182\n  _STRUCT_FIELDSENTRY._serialized_start=113\n  _STRUCT_FIELDSENTRY._serialized_end=182\n  _VALUE._serialized_start=185\n  _VALUE._serialized_end=419\n  _LISTVALUE._serialized_start=421\n  _LISTVALUE._serialized_end=472\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/symbol_database.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"A database of Python protocol buffer generated symbols.\n\nSymbolDatabase is the MessageFactory for messages generated at compile time,\nand makes it easy to create new instances of a registered type, given only the\ntype's protocol buffer symbol name.\n\nExample usage::\n\n  db = symbol_database.SymbolDatabase()\n\n  # Register symbols of interest, from one or multiple files.\n  db.RegisterFileDescriptor(my_proto_pb2.DESCRIPTOR)\n  db.RegisterMessage(my_proto_pb2.MyMessage)\n  db.RegisterEnumDescriptor(my_proto_pb2.MyEnum.DESCRIPTOR)\n\n  # The database can be used as a MessageFactory, to generate types based on\n  # their name:\n  types = db.GetMessages(['my_proto.proto'])\n  my_message_instance = types['MyMessage']()\n\n  # The database's underlying descriptor pool can be queried, so it's not\n  # necessary to know a type's filename to be able to generate it:\n  filename = db.pool.FindFileContainingSymbol('MyMessage')\n  my_message_instance = db.GetMessages([filename])['MyMessage']()\n\n  # This functionality is also provided directly via a convenience method:\n  my_message_instance = db.GetSymbol('MyMessage')()\n\"\"\"\n\n\nfrom google.protobuf.internal import api_implementation\nfrom google.protobuf import descriptor_pool\nfrom google.protobuf import message_factory\n\n\nclass SymbolDatabase(message_factory.MessageFactory):\n  \"\"\"A database of Python generated symbols.\"\"\"\n\n  def RegisterMessage(self, message):\n    \"\"\"Registers the given message type in the local database.\n\n    Calls to GetSymbol() and GetMessages() will return messages registered here.\n\n    Args:\n      message: A :class:`google.protobuf.message.Message` subclass (or\n        instance); its descriptor will be registered.\n\n    Returns:\n      The provided message.\n    \"\"\"\n\n    desc = message.DESCRIPTOR\n    self._classes[desc] = message\n    self.RegisterMessageDescriptor(desc)\n    return message\n\n  def RegisterMessageDescriptor(self, message_descriptor):\n    \"\"\"Registers the given message descriptor in the local database.\n\n    Args:\n      message_descriptor (Descriptor): the message descriptor to add.\n    \"\"\"\n    if api_implementation.Type() == 'python':\n      # pylint: disable=protected-access\n      self.pool._AddDescriptor(message_descriptor)\n\n  def RegisterEnumDescriptor(self, enum_descriptor):\n    \"\"\"Registers the given enum descriptor in the local database.\n\n    Args:\n      enum_descriptor (EnumDescriptor): The enum descriptor to register.\n\n    Returns:\n      EnumDescriptor: The provided descriptor.\n    \"\"\"\n    if api_implementation.Type() == 'python':\n      # pylint: disable=protected-access\n      self.pool._AddEnumDescriptor(enum_descriptor)\n    return enum_descriptor\n\n  def RegisterServiceDescriptor(self, service_descriptor):\n    \"\"\"Registers the given service descriptor in the local database.\n\n    Args:\n      service_descriptor (ServiceDescriptor): the service descriptor to\n        register.\n    \"\"\"\n    if api_implementation.Type() == 'python':\n      # pylint: disable=protected-access\n      self.pool._AddServiceDescriptor(service_descriptor)\n\n  def RegisterFileDescriptor(self, file_descriptor):\n    \"\"\"Registers the given file descriptor in the local database.\n\n    Args:\n      file_descriptor (FileDescriptor): The file descriptor to register.\n    \"\"\"\n    if api_implementation.Type() == 'python':\n      # pylint: disable=protected-access\n      self.pool._InternalAddFileDescriptor(file_descriptor)\n\n  def GetSymbol(self, symbol):\n    \"\"\"Tries to find a symbol in the local database.\n\n    Currently, this method only returns message.Message instances, however, if\n    may be extended in future to support other symbol types.\n\n    Args:\n      symbol (str): a protocol buffer symbol.\n\n    Returns:\n      A Python class corresponding to the symbol.\n\n    Raises:\n      KeyError: if the symbol could not be found.\n    \"\"\"\n\n    return self._classes[self.pool.FindMessageTypeByName(symbol)]\n\n  def GetMessages(self, files):\n    # TODO(amauryfa): Fix the differences with MessageFactory.\n    \"\"\"Gets all registered messages from a specified file.\n\n    Only messages already created and registered will be returned; (this is the\n    case for imported _pb2 modules)\n    But unlike MessageFactory, this version also returns already defined nested\n    messages, but does not register any message extensions.\n\n    Args:\n      files (list[str]): The file names to extract messages from.\n\n    Returns:\n      A dictionary mapping proto names to the message classes.\n\n    Raises:\n      KeyError: if a file could not be found.\n    \"\"\"\n\n    def _GetAllMessages(desc):\n      \"\"\"Walk a message Descriptor and recursively yields all message names.\"\"\"\n      yield desc\n      for msg_desc in desc.nested_types:\n        for nested_desc in _GetAllMessages(msg_desc):\n          yield nested_desc\n\n    result = {}\n    for file_name in files:\n      file_desc = self.pool.FindFileByName(file_name)\n      for msg_desc in file_desc.message_types_by_name.values():\n        for desc in _GetAllMessages(msg_desc):\n          try:\n            result[desc.full_name] = self._classes[desc]\n          except KeyError:\n            # This descriptor has no registered class, skip it.\n            pass\n    return result\n\n\n_DEFAULT = SymbolDatabase(pool=descriptor_pool.Default())\n\n\ndef Default():\n  \"\"\"Returns the default SymbolDatabase.\"\"\"\n  return _DEFAULT\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/text_encoding.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Encoding related utilities.\"\"\"\nimport re\n\n_cescape_chr_to_symbol_map = {}\n_cescape_chr_to_symbol_map[9] = r'\\t'  # optional escape\n_cescape_chr_to_symbol_map[10] = r'\\n'  # optional escape\n_cescape_chr_to_symbol_map[13] = r'\\r'  # optional escape\n_cescape_chr_to_symbol_map[34] = r'\\\"'  # necessary escape\n_cescape_chr_to_symbol_map[39] = r\"\\'\"  # optional escape\n_cescape_chr_to_symbol_map[92] = r'\\\\'  # necessary escape\n\n# Lookup table for unicode\n_cescape_unicode_to_str = [chr(i) for i in range(0, 256)]\nfor byte, string in _cescape_chr_to_symbol_map.items():\n  _cescape_unicode_to_str[byte] = string\n\n# Lookup table for non-utf8, with necessary escapes at (o >= 127 or o < 32)\n_cescape_byte_to_str = ([r'\\%03o' % i for i in range(0, 32)] +\n                        [chr(i) for i in range(32, 127)] +\n                        [r'\\%03o' % i for i in range(127, 256)])\nfor byte, string in _cescape_chr_to_symbol_map.items():\n  _cescape_byte_to_str[byte] = string\ndel byte, string\n\n\ndef CEscape(text, as_utf8):\n  # type: (...) -> str\n  \"\"\"Escape a bytes string for use in an text protocol buffer.\n\n  Args:\n    text: A byte string to be escaped.\n    as_utf8: Specifies if result may contain non-ASCII characters.\n        In Python 3 this allows unescaped non-ASCII Unicode characters.\n        In Python 2 the return value will be valid UTF-8 rather than only ASCII.\n  Returns:\n    Escaped string (str).\n  \"\"\"\n  # Python's text.encode() 'string_escape' or 'unicode_escape' codecs do not\n  # satisfy our needs; they encodes unprintable characters using two-digit hex\n  # escapes whereas our C++ unescaping function allows hex escapes to be any\n  # length.  So, \"\\0011\".encode('string_escape') ends up being \"\\\\x011\", which\n  # will be decoded in C++ as a single-character string with char code 0x11.\n  text_is_unicode = isinstance(text, str)\n  if as_utf8 and text_is_unicode:\n    # We're already unicode, no processing beyond control char escapes.\n    return text.translate(_cescape_chr_to_symbol_map)\n  ord_ = ord if text_is_unicode else lambda x: x  # bytes iterate as ints.\n  if as_utf8:\n    return ''.join(_cescape_unicode_to_str[ord_(c)] for c in text)\n  return ''.join(_cescape_byte_to_str[ord_(c)] for c in text)\n\n\n_CUNESCAPE_HEX = re.compile(r'(\\\\+)x([0-9a-fA-F])(?![0-9a-fA-F])')\n\n\ndef CUnescape(text):\n  # type: (str) -> bytes\n  \"\"\"Unescape a text string with C-style escape sequences to UTF-8 bytes.\n\n  Args:\n    text: The data to parse in a str.\n  Returns:\n    A byte string.\n  \"\"\"\n\n  def ReplaceHex(m):\n    # Only replace the match if the number of leading back slashes is odd. i.e.\n    # the slash itself is not escaped.\n    if len(m.group(1)) & 1:\n      return m.group(1) + 'x0' + m.group(2)\n    return m.group(0)\n\n  # This is required because the 'string_escape' encoding doesn't\n  # allow single-digit hex escapes (like '\\xf').\n  result = _CUNESCAPE_HEX.sub(ReplaceHex, text)\n\n  return (result.encode('utf-8')  # Make it bytes to allow decode.\n          .decode('unicode_escape')\n          # Make it bytes again to return the proper type.\n          .encode('raw_unicode_escape'))\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/text_format.py",
    "content": "# Protocol Buffers - Google's data interchange format\n# Copyright 2008 Google Inc.  All rights reserved.\n# https://developers.google.com/protocol-buffers/\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#     * Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#     * Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following disclaimer\n# in the documentation and/or other materials provided with the\n# distribution.\n#     * Neither the name of Google Inc. nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Contains routines for printing protocol messages in text format.\n\nSimple usage example::\n\n  # Create a proto object and serialize it to a text proto string.\n  message = my_proto_pb2.MyMessage(foo='bar')\n  text_proto = text_format.MessageToString(message)\n\n  # Parse a text proto string.\n  message = text_format.Parse(text_proto, my_proto_pb2.MyMessage())\n\"\"\"\n\n__author__ = 'kenton@google.com (Kenton Varda)'\n\n# TODO(b/129989314) Import thread contention leads to test failures.\nimport encodings.raw_unicode_escape  # pylint: disable=unused-import\nimport encodings.unicode_escape  # pylint: disable=unused-import\nimport io\nimport math\nimport re\n\nfrom google.protobuf.internal import decoder\nfrom google.protobuf.internal import type_checkers\nfrom google.protobuf import descriptor\nfrom google.protobuf import text_encoding\n\n# pylint: disable=g-import-not-at-top\n__all__ = ['MessageToString', 'Parse', 'PrintMessage', 'PrintField',\n           'PrintFieldValue', 'Merge', 'MessageToBytes']\n\n_INTEGER_CHECKERS = (type_checkers.Uint32ValueChecker(),\n                     type_checkers.Int32ValueChecker(),\n                     type_checkers.Uint64ValueChecker(),\n                     type_checkers.Int64ValueChecker())\n_FLOAT_INFINITY = re.compile('-?inf(?:inity)?f?$', re.IGNORECASE)\n_FLOAT_NAN = re.compile('nanf?$', re.IGNORECASE)\n_QUOTES = frozenset((\"'\", '\"'))\n_ANY_FULL_TYPE_NAME = 'google.protobuf.Any'\n\n\nclass Error(Exception):\n  \"\"\"Top-level module error for text_format.\"\"\"\n\n\nclass ParseError(Error):\n  \"\"\"Thrown in case of text parsing or tokenizing error.\"\"\"\n\n  def __init__(self, message=None, line=None, column=None):\n    if message is not None and line is not None:\n      loc = str(line)\n      if column is not None:\n        loc += ':{0}'.format(column)\n      message = '{0} : {1}'.format(loc, message)\n    if message is not None:\n      super(ParseError, self).__init__(message)\n    else:\n      super(ParseError, self).__init__()\n    self._line = line\n    self._column = column\n\n  def GetLine(self):\n    return self._line\n\n  def GetColumn(self):\n    return self._column\n\n\nclass TextWriter(object):\n\n  def __init__(self, as_utf8):\n    self._writer = io.StringIO()\n\n  def write(self, val):\n    return self._writer.write(val)\n\n  def close(self):\n    return self._writer.close()\n\n  def getvalue(self):\n    return self._writer.getvalue()\n\n\ndef MessageToString(\n    message,\n    as_utf8=False,\n    as_one_line=False,\n    use_short_repeated_primitives=False,\n    pointy_brackets=False,\n    use_index_order=False,\n    float_format=None,\n    double_format=None,\n    use_field_number=False,\n    descriptor_pool=None,\n    indent=0,\n    message_formatter=None,\n    print_unknown_fields=False,\n    force_colon=False):\n  # type: (...) -> str\n  \"\"\"Convert protobuf message to text format.\n\n  Double values can be formatted compactly with 15 digits of\n  precision (which is the most that IEEE 754 \"double\" can guarantee)\n  using double_format='.15g'. To ensure that converting to text and back to a\n  proto will result in an identical value, double_format='.17g' should be used.\n\n  Args:\n    message: The protocol buffers message.\n    as_utf8: Return unescaped Unicode for non-ASCII characters.\n        In Python 3 actual Unicode characters may appear as is in strings.\n        In Python 2 the return value will be valid UTF-8 rather than only ASCII.\n    as_one_line: Don't introduce newlines between fields.\n    use_short_repeated_primitives: Use short repeated format for primitives.\n    pointy_brackets: If True, use angle brackets instead of curly braces for\n      nesting.\n    use_index_order: If True, fields of a proto message will be printed using\n      the order defined in source code instead of the field number, extensions\n      will be printed at the end of the message and their relative order is\n      determined by the extension number. By default, use the field number\n      order.\n    float_format (str): If set, use this to specify float field formatting\n      (per the \"Format Specification Mini-Language\"); otherwise, shortest float\n      that has same value in wire will be printed. Also affect double field\n      if double_format is not set but float_format is set.\n    double_format (str): If set, use this to specify double field formatting\n      (per the \"Format Specification Mini-Language\"); if it is not set but\n      float_format is set, use float_format. Otherwise, use ``str()``\n    use_field_number: If True, print field numbers instead of names.\n    descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types.\n    indent (int): The initial indent level, in terms of spaces, for pretty\n      print.\n    message_formatter (function(message, indent, as_one_line) -> unicode|None):\n      Custom formatter for selected sub-messages (usually based on message\n      type). Use to pretty print parts of the protobuf for easier diffing.\n    print_unknown_fields: If True, unknown fields will be printed.\n    force_colon: If set, a colon will be added after the field name even if the\n      field is a proto message.\n\n  Returns:\n    str: A string of the text formatted protocol buffer message.\n  \"\"\"\n  out = TextWriter(as_utf8)\n  printer = _Printer(\n      out,\n      indent,\n      as_utf8,\n      as_one_line,\n      use_short_repeated_primitives,\n      pointy_brackets,\n      use_index_order,\n      float_format,\n      double_format,\n      use_field_number,\n      descriptor_pool,\n      message_formatter,\n      print_unknown_fields=print_unknown_fields,\n      force_colon=force_colon)\n  printer.PrintMessage(message)\n  result = out.getvalue()\n  out.close()\n  if as_one_line:\n    return result.rstrip()\n  return result\n\n\ndef MessageToBytes(message, **kwargs):\n  # type: (...) -> bytes\n  \"\"\"Convert protobuf message to encoded text format.  See MessageToString.\"\"\"\n  text = MessageToString(message, **kwargs)\n  if isinstance(text, bytes):\n    return text\n  codec = 'utf-8' if kwargs.get('as_utf8') else 'ascii'\n  return text.encode(codec)\n\n\ndef _IsMapEntry(field):\n  return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and\n          field.message_type.has_options and\n          field.message_type.GetOptions().map_entry)\n\n\ndef PrintMessage(message,\n                 out,\n                 indent=0,\n                 as_utf8=False,\n                 as_one_line=False,\n                 use_short_repeated_primitives=False,\n                 pointy_brackets=False,\n                 use_index_order=False,\n                 float_format=None,\n                 double_format=None,\n                 use_field_number=False,\n                 descriptor_pool=None,\n                 message_formatter=None,\n                 print_unknown_fields=False,\n                 force_colon=False):\n  printer = _Printer(\n      out=out, indent=indent, as_utf8=as_utf8,\n      as_one_line=as_one_line,\n      use_short_repeated_primitives=use_short_repeated_primitives,\n      pointy_brackets=pointy_brackets,\n      use_index_order=use_index_order,\n      float_format=float_format,\n      double_format=double_format,\n      use_field_number=use_field_number,\n      descriptor_pool=descriptor_pool,\n      message_formatter=message_formatter,\n      print_unknown_fields=print_unknown_fields,\n      force_colon=force_colon)\n  printer.PrintMessage(message)\n\n\ndef PrintField(field,\n               value,\n               out,\n               indent=0,\n               as_utf8=False,\n               as_one_line=False,\n               use_short_repeated_primitives=False,\n               pointy_brackets=False,\n               use_index_order=False,\n               float_format=None,\n               double_format=None,\n               message_formatter=None,\n               print_unknown_fields=False,\n               force_colon=False):\n  \"\"\"Print a single field name/value pair.\"\"\"\n  printer = _Printer(out, indent, as_utf8, as_one_line,\n                     use_short_repeated_primitives, pointy_brackets,\n                     use_index_order, float_format, double_format,\n                     message_formatter=message_formatter,\n                     print_unknown_fields=print_unknown_fields,\n                     force_colon=force_colon)\n  printer.PrintField(field, value)\n\n\ndef PrintFieldValue(field,\n                    value,\n                    out,\n                    indent=0,\n                    as_utf8=False,\n                    as_one_line=False,\n                    use_short_repeated_primitives=False,\n                    pointy_brackets=False,\n                    use_index_order=False,\n                    float_format=None,\n                    double_format=None,\n                    message_formatter=None,\n                    print_unknown_fields=False,\n                    force_colon=False):\n  \"\"\"Print a single field value (not including name).\"\"\"\n  printer = _Printer(out, indent, as_utf8, as_one_line,\n                     use_short_repeated_primitives, pointy_brackets,\n                     use_index_order, float_format, double_format,\n                     message_formatter=message_formatter,\n                     print_unknown_fields=print_unknown_fields,\n                     force_colon=force_colon)\n  printer.PrintFieldValue(field, value)\n\n\ndef _BuildMessageFromTypeName(type_name, descriptor_pool):\n  \"\"\"Returns a protobuf message instance.\n\n  Args:\n    type_name: Fully-qualified protobuf  message type name string.\n    descriptor_pool: DescriptorPool instance.\n\n  Returns:\n    A Message instance of type matching type_name, or None if the a Descriptor\n    wasn't found matching type_name.\n  \"\"\"\n  # pylint: disable=g-import-not-at-top\n  if descriptor_pool is None:\n    from google.protobuf import descriptor_pool as pool_mod\n    descriptor_pool = pool_mod.Default()\n  from google.protobuf import symbol_database\n  database = symbol_database.Default()\n  try:\n    message_descriptor = descriptor_pool.FindMessageTypeByName(type_name)\n  except KeyError:\n    return None\n  message_type = database.GetPrototype(message_descriptor)\n  return message_type()\n\n\n# These values must match WireType enum in google/protobuf/wire_format.h.\nWIRETYPE_LENGTH_DELIMITED = 2\nWIRETYPE_START_GROUP = 3\n\n\nclass _Printer(object):\n  \"\"\"Text format printer for protocol message.\"\"\"\n\n  def __init__(\n      self,\n      out,\n      indent=0,\n      as_utf8=False,\n      as_one_line=False,\n      use_short_repeated_primitives=False,\n      pointy_brackets=False,\n      use_index_order=False,\n      float_format=None,\n      double_format=None,\n      use_field_number=False,\n      descriptor_pool=None,\n      message_formatter=None,\n      print_unknown_fields=False,\n      force_colon=False):\n    \"\"\"Initialize the Printer.\n\n    Double values can be formatted compactly with 15 digits of precision\n    (which is the most that IEEE 754 \"double\" can guarantee) using\n    double_format='.15g'. To ensure that converting to text and back to a proto\n    will result in an identical value, double_format='.17g' should be used.\n\n    Args:\n      out: To record the text format result.\n      indent: The initial indent level for pretty print.\n      as_utf8: Return unescaped Unicode for non-ASCII characters.\n          In Python 3 actual Unicode characters may appear as is in strings.\n          In Python 2 the return value will be valid UTF-8 rather than ASCII.\n      as_one_line: Don't introduce newlines between fields.\n      use_short_repeated_primitives: Use short repeated format for primitives.\n      pointy_brackets: If True, use angle brackets instead of curly braces for\n        nesting.\n      use_index_order: If True, print fields of a proto message using the order\n        defined in source code instead of the field number. By default, use the\n        field number order.\n      float_format: If set, use this to specify float field formatting\n        (per the \"Format Specification Mini-Language\"); otherwise, shortest\n        float that has same value in wire will be printed. Also affect double\n        field if double_format is not set but float_format is set.\n      double_format: If set, use this to specify double field formatting\n        (per the \"Format Specification Mini-Language\"); if it is not set but\n        float_format is set, use float_format. Otherwise, str() is used.\n      use_field_number: If True, print field numbers instead of names.\n      descriptor_pool: A DescriptorPool used to resolve Any types.\n      message_formatter: A function(message, indent, as_one_line): unicode|None\n        to custom format selected sub-messages (usually based on message type).\n        Use to pretty print parts of the protobuf for easier diffing.\n      print_unknown_fields: If True, unknown fields will be printed.\n      force_colon: If set, a colon will be added after the field name even if\n        the field is a proto message.\n    \"\"\"\n    self.out = out\n    self.indent = indent\n    self.as_utf8 = as_utf8\n    self.as_one_line = as_one_line\n    self.use_short_repeated_primitives = use_short_repeated_primitives\n    self.pointy_brackets = pointy_brackets\n    self.use_index_order = use_index_order\n    self.float_format = float_format\n    if double_format is not None:\n      self.double_format = double_format\n    else:\n      self.double_format = float_format\n    self.use_field_number = use_field_number\n    self.descriptor_pool = descriptor_pool\n    self.message_formatter = message_formatter\n    self.print_unknown_fields = print_unknown_fields\n    self.force_colon = force_colon\n\n  def _TryPrintAsAnyMessage(self, message):\n    \"\"\"Serializes if message is a google.protobuf.Any field.\"\"\"\n    if '/' not in message.type_url:\n      return False\n    packed_message = _BuildMessageFromTypeName(message.TypeName(),\n                                               self.descriptor_pool)\n    if packed_message:\n      packed_message.MergeFromString(message.value)\n      colon = ':' if self.force_colon else ''\n      self.out.write('%s[%s]%s ' % (self.indent * ' ', message.type_url, colon))\n      self._PrintMessageFieldValue(packed_message)\n      self.out.write(' ' if self.as_one_line else '\\n')\n      return True\n    else:\n      return False\n\n  def _TryCustomFormatMessage(self, message):\n    formatted = self.message_formatter(message, self.indent, self.as_one_line)\n    if formatted is None:\n      return False\n\n    out = self.out\n    out.write(' ' * self.indent)\n    out.write(formatted)\n    out.write(' ' if self.as_one_line else '\\n')\n    return True\n\n  def PrintMessage(self, message):\n    \"\"\"Convert protobuf message to text format.\n\n    Args:\n      message: The protocol buffers message.\n    \"\"\"\n    if self.message_formatter and self._TryCustomFormatMessage(message):\n      return\n    if (message.DESCRIPTOR.full_name == _ANY_FULL_TYPE_NAME and\n        self._TryPrintAsAnyMessage(message)):\n      return\n    fields = message.ListFields()\n    if self.use_index_order:\n      fields.sort(\n          key=lambda x: x[0].number if x[0].is_extension else x[0].index)\n    for field, value in fields:\n      if _IsMapEntry(field):\n        for key in sorted(value):\n          # This is slow for maps with submessage entries because it copies the\n          # entire tree.  Unfortunately this would take significant refactoring\n          # of this file to work around.\n          #\n          # TODO(haberman): refactor and optimize if this becomes an issue.\n          entry_submsg = value.GetEntryClass()(key=key, value=value[key])\n          self.PrintField(field, entry_submsg)\n      elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n        if (self.use_short_repeated_primitives\n            and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE\n            and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_STRING):\n          self._PrintShortRepeatedPrimitivesValue(field, value)\n        else:\n          for element in value:\n            self.PrintField(field, element)\n      else:\n        self.PrintField(field, value)\n\n    if self.print_unknown_fields:\n      self._PrintUnknownFields(message.UnknownFields())\n\n  def _PrintUnknownFields(self, unknown_fields):\n    \"\"\"Print unknown fields.\"\"\"\n    out = self.out\n    for field in unknown_fields:\n      out.write(' ' * self.indent)\n      out.write(str(field.field_number))\n      if field.wire_type == WIRETYPE_START_GROUP:\n        if self.as_one_line:\n          out.write(' { ')\n        else:\n          out.write(' {\\n')\n          self.indent += 2\n\n        self._PrintUnknownFields(field.data)\n\n        if self.as_one_line:\n          out.write('} ')\n        else:\n          self.indent -= 2\n          out.write(' ' * self.indent + '}\\n')\n      elif field.wire_type == WIRETYPE_LENGTH_DELIMITED:\n        try:\n          # If this field is parseable as a Message, it is probably\n          # an embedded message.\n          # pylint: disable=protected-access\n          (embedded_unknown_message, pos) = decoder._DecodeUnknownFieldSet(\n              memoryview(field.data), 0, len(field.data))\n        except Exception:    # pylint: disable=broad-except\n          pos = 0\n\n        if pos == len(field.data):\n          if self.as_one_line:\n            out.write(' { ')\n          else:\n            out.write(' {\\n')\n            self.indent += 2\n\n          self._PrintUnknownFields(embedded_unknown_message)\n\n          if self.as_one_line:\n            out.write('} ')\n          else:\n            self.indent -= 2\n            out.write(' ' * self.indent + '}\\n')\n        else:\n          # A string or bytes field. self.as_utf8 may not work.\n          out.write(': \\\"')\n          out.write(text_encoding.CEscape(field.data, False))\n          out.write('\\\" ' if self.as_one_line else '\\\"\\n')\n      else:\n        # varint, fixed32, fixed64\n        out.write(': ')\n        out.write(str(field.data))\n        out.write(' ' if self.as_one_line else '\\n')\n\n  def _PrintFieldName(self, field):\n    \"\"\"Print field name.\"\"\"\n    out = self.out\n    out.write(' ' * self.indent)\n    if self.use_field_number:\n      out.write(str(field.number))\n    else:\n      if field.is_extension:\n        out.write('[')\n        if (field.containing_type.GetOptions().message_set_wire_format and\n            field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and\n            field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL):\n          out.write(field.message_type.full_name)\n        else:\n          out.write(field.full_name)\n        out.write(']')\n      elif field.type == descriptor.FieldDescriptor.TYPE_GROUP:\n        # For groups, use the capitalized name.\n        out.write(field.message_type.name)\n      else:\n          out.write(field.name)\n\n    if (self.force_colon or\n        field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE):\n      # The colon is optional in this case, but our cross-language golden files\n      # don't include it. Here, the colon is only included if force_colon is\n      # set to True\n      out.write(':')\n\n  def PrintField(self, field, value):\n    \"\"\"Print a single field name/value pair.\"\"\"\n    self._PrintFieldName(field)\n    self.out.write(' ')\n    self.PrintFieldValue(field, value)\n    self.out.write(' ' if self.as_one_line else '\\n')\n\n  def _PrintShortRepeatedPrimitivesValue(self, field, value):\n    \"\"\"\"Prints short repeated primitives value.\"\"\"\n    # Note: this is called only when value has at least one element.\n    self._PrintFieldName(field)\n    self.out.write(' [')\n    for i in range(len(value) - 1):\n      self.PrintFieldValue(field, value[i])\n      self.out.write(', ')\n    self.PrintFieldValue(field, value[-1])\n    self.out.write(']')\n    self.out.write(' ' if self.as_one_line else '\\n')\n\n  def _PrintMessageFieldValue(self, value):\n    if self.pointy_brackets:\n      openb = '<'\n      closeb = '>'\n    else:\n      openb = '{'\n      closeb = '}'\n\n    if self.as_one_line:\n      self.out.write('%s ' % openb)\n      self.PrintMessage(value)\n      self.out.write(closeb)\n    else:\n      self.out.write('%s\\n' % openb)\n      self.indent += 2\n      self.PrintMessage(value)\n      self.indent -= 2\n      self.out.write(' ' * self.indent + closeb)\n\n  def PrintFieldValue(self, field, value):\n    \"\"\"Print a single field value (not including name).\n\n    For repeated fields, the value should be a single element.\n\n    Args:\n      field: The descriptor of the field to be printed.\n      value: The value of the field.\n    \"\"\"\n    out = self.out\n    if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n      self._PrintMessageFieldValue(value)\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM:\n      enum_value = field.enum_type.values_by_number.get(value, None)\n      if enum_value is not None:\n        out.write(enum_value.name)\n      else:\n        out.write(str(value))\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING:\n      out.write('\\\"')\n      if isinstance(value, str) and not self.as_utf8:\n        out_value = value.encode('utf-8')\n      else:\n        out_value = value\n      if field.type == descriptor.FieldDescriptor.TYPE_BYTES:\n        # We always need to escape all binary data in TYPE_BYTES fields.\n        out_as_utf8 = False\n      else:\n        out_as_utf8 = self.as_utf8\n      out.write(text_encoding.CEscape(out_value, out_as_utf8))\n      out.write('\\\"')\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL:\n      if value:\n        out.write('true')\n      else:\n        out.write('false')\n    elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT:\n      if self.float_format is not None:\n        out.write('{1:{0}}'.format(self.float_format, value))\n      else:\n        if math.isnan(value):\n          out.write(str(value))\n        else:\n          out.write(str(type_checkers.ToShortestFloat(value)))\n    elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_DOUBLE and\n          self.double_format is not None):\n      out.write('{1:{0}}'.format(self.double_format, value))\n    else:\n      out.write(str(value))\n\n\ndef Parse(text,\n          message,\n          allow_unknown_extension=False,\n          allow_field_number=False,\n          descriptor_pool=None,\n          allow_unknown_field=False):\n  \"\"\"Parses a text representation of a protocol message into a message.\n\n  NOTE: for historical reasons this function does not clear the input\n  message. This is different from what the binary msg.ParseFrom(...) does.\n  If text contains a field already set in message, the value is appended if the\n  field is repeated. Otherwise, an error is raised.\n\n  Example::\n\n    a = MyProto()\n    a.repeated_field.append('test')\n    b = MyProto()\n\n    # Repeated fields are combined\n    text_format.Parse(repr(a), b)\n    text_format.Parse(repr(a), b) # repeated_field contains [\"test\", \"test\"]\n\n    # Non-repeated fields cannot be overwritten\n    a.singular_field = 1\n    b.singular_field = 2\n    text_format.Parse(repr(a), b) # ParseError\n\n    # Binary version:\n    b.ParseFromString(a.SerializeToString()) # repeated_field is now \"test\"\n\n  Caller is responsible for clearing the message as needed.\n\n  Args:\n    text (str): Message text representation.\n    message (Message): A protocol buffer message to merge into.\n    allow_unknown_extension: if True, skip over missing extensions and keep\n      parsing\n    allow_field_number: if True, both field number and field name are allowed.\n    descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types.\n    allow_unknown_field: if True, skip over unknown field and keep\n      parsing. Avoid to use this option if possible. It may hide some\n      errors (e.g. spelling error on field name)\n\n  Returns:\n    Message: The same message passed as argument.\n\n  Raises:\n    ParseError: On text parsing problems.\n  \"\"\"\n  return ParseLines(text.split(b'\\n' if isinstance(text, bytes) else u'\\n'),\n                    message,\n                    allow_unknown_extension,\n                    allow_field_number,\n                    descriptor_pool=descriptor_pool,\n                    allow_unknown_field=allow_unknown_field)\n\n\ndef Merge(text,\n          message,\n          allow_unknown_extension=False,\n          allow_field_number=False,\n          descriptor_pool=None,\n          allow_unknown_field=False):\n  \"\"\"Parses a text representation of a protocol message into a message.\n\n  Like Parse(), but allows repeated values for a non-repeated field, and uses\n  the last one. This means any non-repeated, top-level fields specified in text\n  replace those in the message.\n\n  Args:\n    text (str): Message text representation.\n    message (Message): A protocol buffer message to merge into.\n    allow_unknown_extension: if True, skip over missing extensions and keep\n      parsing\n    allow_field_number: if True, both field number and field name are allowed.\n    descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types.\n    allow_unknown_field: if True, skip over unknown field and keep\n      parsing. Avoid to use this option if possible. It may hide some\n      errors (e.g. spelling error on field name)\n\n  Returns:\n    Message: The same message passed as argument.\n\n  Raises:\n    ParseError: On text parsing problems.\n  \"\"\"\n  return MergeLines(\n      text.split(b'\\n' if isinstance(text, bytes) else u'\\n'),\n      message,\n      allow_unknown_extension,\n      allow_field_number,\n      descriptor_pool=descriptor_pool,\n      allow_unknown_field=allow_unknown_field)\n\n\ndef ParseLines(lines,\n               message,\n               allow_unknown_extension=False,\n               allow_field_number=False,\n               descriptor_pool=None,\n               allow_unknown_field=False):\n  \"\"\"Parses a text representation of a protocol message into a message.\n\n  See Parse() for caveats.\n\n  Args:\n    lines: An iterable of lines of a message's text representation.\n    message: A protocol buffer message to merge into.\n    allow_unknown_extension: if True, skip over missing extensions and keep\n      parsing\n    allow_field_number: if True, both field number and field name are allowed.\n    descriptor_pool: A DescriptorPool used to resolve Any types.\n    allow_unknown_field: if True, skip over unknown field and keep\n      parsing. Avoid to use this option if possible. It may hide some\n      errors (e.g. spelling error on field name)\n\n  Returns:\n    The same message passed as argument.\n\n  Raises:\n    ParseError: On text parsing problems.\n  \"\"\"\n  parser = _Parser(allow_unknown_extension,\n                   allow_field_number,\n                   descriptor_pool=descriptor_pool,\n                   allow_unknown_field=allow_unknown_field)\n  return parser.ParseLines(lines, message)\n\n\ndef MergeLines(lines,\n               message,\n               allow_unknown_extension=False,\n               allow_field_number=False,\n               descriptor_pool=None,\n               allow_unknown_field=False):\n  \"\"\"Parses a text representation of a protocol message into a message.\n\n  See Merge() for more details.\n\n  Args:\n    lines: An iterable of lines of a message's text representation.\n    message: A protocol buffer message to merge into.\n    allow_unknown_extension: if True, skip over missing extensions and keep\n      parsing\n    allow_field_number: if True, both field number and field name are allowed.\n    descriptor_pool: A DescriptorPool used to resolve Any types.\n    allow_unknown_field: if True, skip over unknown field and keep\n      parsing. Avoid to use this option if possible. It may hide some\n      errors (e.g. spelling error on field name)\n\n  Returns:\n    The same message passed as argument.\n\n  Raises:\n    ParseError: On text parsing problems.\n  \"\"\"\n  parser = _Parser(allow_unknown_extension,\n                   allow_field_number,\n                   descriptor_pool=descriptor_pool,\n                   allow_unknown_field=allow_unknown_field)\n  return parser.MergeLines(lines, message)\n\n\nclass _Parser(object):\n  \"\"\"Text format parser for protocol message.\"\"\"\n\n  def __init__(self,\n               allow_unknown_extension=False,\n               allow_field_number=False,\n               descriptor_pool=None,\n               allow_unknown_field=False):\n    self.allow_unknown_extension = allow_unknown_extension\n    self.allow_field_number = allow_field_number\n    self.descriptor_pool = descriptor_pool\n    self.allow_unknown_field = allow_unknown_field\n\n  def ParseLines(self, lines, message):\n    \"\"\"Parses a text representation of a protocol message into a message.\"\"\"\n    self._allow_multiple_scalars = False\n    self._ParseOrMerge(lines, message)\n    return message\n\n  def MergeLines(self, lines, message):\n    \"\"\"Merges a text representation of a protocol message into a message.\"\"\"\n    self._allow_multiple_scalars = True\n    self._ParseOrMerge(lines, message)\n    return message\n\n  def _ParseOrMerge(self, lines, message):\n    \"\"\"Converts a text representation of a protocol message into a message.\n\n    Args:\n      lines: Lines of a message's text representation.\n      message: A protocol buffer message to merge into.\n\n    Raises:\n      ParseError: On text parsing problems.\n    \"\"\"\n    # Tokenize expects native str lines.\n    str_lines = (\n        line if isinstance(line, str) else line.decode('utf-8')\n        for line in lines)\n    tokenizer = Tokenizer(str_lines)\n    while not tokenizer.AtEnd():\n      self._MergeField(tokenizer, message)\n\n  def _MergeField(self, tokenizer, message):\n    \"\"\"Merges a single protocol message field into a message.\n\n    Args:\n      tokenizer: A tokenizer to parse the field name and values.\n      message: A protocol message to record the data.\n\n    Raises:\n      ParseError: In case of text parsing problems.\n    \"\"\"\n    message_descriptor = message.DESCRIPTOR\n    if (message_descriptor.full_name == _ANY_FULL_TYPE_NAME and\n        tokenizer.TryConsume('[')):\n      type_url_prefix, packed_type_name = self._ConsumeAnyTypeUrl(tokenizer)\n      tokenizer.Consume(']')\n      tokenizer.TryConsume(':')\n      if tokenizer.TryConsume('<'):\n        expanded_any_end_token = '>'\n      else:\n        tokenizer.Consume('{')\n        expanded_any_end_token = '}'\n      expanded_any_sub_message = _BuildMessageFromTypeName(packed_type_name,\n                                                           self.descriptor_pool)\n      if not expanded_any_sub_message:\n        raise ParseError('Type %s not found in descriptor pool' %\n                         packed_type_name)\n      while not tokenizer.TryConsume(expanded_any_end_token):\n        if tokenizer.AtEnd():\n          raise tokenizer.ParseErrorPreviousToken('Expected \"%s\".' %\n                                                  (expanded_any_end_token,))\n        self._MergeField(tokenizer, expanded_any_sub_message)\n      deterministic = False\n\n      message.Pack(expanded_any_sub_message,\n                   type_url_prefix=type_url_prefix,\n                   deterministic=deterministic)\n      return\n\n    if tokenizer.TryConsume('['):\n      name = [tokenizer.ConsumeIdentifier()]\n      while tokenizer.TryConsume('.'):\n        name.append(tokenizer.ConsumeIdentifier())\n      name = '.'.join(name)\n\n      if not message_descriptor.is_extendable:\n        raise tokenizer.ParseErrorPreviousToken(\n            'Message type \"%s\" does not have extensions.' %\n            message_descriptor.full_name)\n      # pylint: disable=protected-access\n      field = message.Extensions._FindExtensionByName(name)\n      # pylint: enable=protected-access\n\n\n      if not field:\n        if self.allow_unknown_extension:\n          field = None\n        else:\n          raise tokenizer.ParseErrorPreviousToken(\n              'Extension \"%s\" not registered. '\n              'Did you import the _pb2 module which defines it? '\n              'If you are trying to place the extension in the MessageSet '\n              'field of another message that is in an Any or MessageSet field, '\n              'that message\\'s _pb2 module must be imported as well' % name)\n      elif message_descriptor != field.containing_type:\n        raise tokenizer.ParseErrorPreviousToken(\n            'Extension \"%s\" does not extend message type \"%s\".' %\n            (name, message_descriptor.full_name))\n\n      tokenizer.Consume(']')\n\n    else:\n      name = tokenizer.ConsumeIdentifierOrNumber()\n      if self.allow_field_number and name.isdigit():\n        number = ParseInteger(name, True, True)\n        field = message_descriptor.fields_by_number.get(number, None)\n        if not field and message_descriptor.is_extendable:\n          field = message.Extensions._FindExtensionByNumber(number)\n      else:\n        field = message_descriptor.fields_by_name.get(name, None)\n\n        # Group names are expected to be capitalized as they appear in the\n        # .proto file, which actually matches their type names, not their field\n        # names.\n        if not field:\n          field = message_descriptor.fields_by_name.get(name.lower(), None)\n          if field and field.type != descriptor.FieldDescriptor.TYPE_GROUP:\n            field = None\n\n        if (field and field.type == descriptor.FieldDescriptor.TYPE_GROUP and\n            field.message_type.name != name):\n          field = None\n\n      if not field and not self.allow_unknown_field:\n        raise tokenizer.ParseErrorPreviousToken(\n            'Message type \"%s\" has no field named \"%s\".' %\n            (message_descriptor.full_name, name))\n\n    if field:\n      if not self._allow_multiple_scalars and field.containing_oneof:\n        # Check if there's a different field set in this oneof.\n        # Note that we ignore the case if the same field was set before, and we\n        # apply _allow_multiple_scalars to non-scalar fields as well.\n        which_oneof = message.WhichOneof(field.containing_oneof.name)\n        if which_oneof is not None and which_oneof != field.name:\n          raise tokenizer.ParseErrorPreviousToken(\n              'Field \"%s\" is specified along with field \"%s\", another member '\n              'of oneof \"%s\" for message type \"%s\".' %\n              (field.name, which_oneof, field.containing_oneof.name,\n               message_descriptor.full_name))\n\n      if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n        tokenizer.TryConsume(':')\n        merger = self._MergeMessageField\n      else:\n        tokenizer.Consume(':')\n        merger = self._MergeScalarField\n\n      if (field.label == descriptor.FieldDescriptor.LABEL_REPEATED and\n          tokenizer.TryConsume('[')):\n        # Short repeated format, e.g. \"foo: [1, 2, 3]\"\n        if not tokenizer.TryConsume(']'):\n          while True:\n            merger(tokenizer, message, field)\n            if tokenizer.TryConsume(']'):\n              break\n            tokenizer.Consume(',')\n\n      else:\n        merger(tokenizer, message, field)\n\n    else:  # Proto field is unknown.\n      assert (self.allow_unknown_extension or self.allow_unknown_field)\n      _SkipFieldContents(tokenizer)\n\n    # For historical reasons, fields may optionally be separated by commas or\n    # semicolons.\n    if not tokenizer.TryConsume(','):\n      tokenizer.TryConsume(';')\n\n\n  def _ConsumeAnyTypeUrl(self, tokenizer):\n    \"\"\"Consumes a google.protobuf.Any type URL and returns the type name.\"\"\"\n    # Consume \"type.googleapis.com/\".\n    prefix = [tokenizer.ConsumeIdentifier()]\n    tokenizer.Consume('.')\n    prefix.append(tokenizer.ConsumeIdentifier())\n    tokenizer.Consume('.')\n    prefix.append(tokenizer.ConsumeIdentifier())\n    tokenizer.Consume('/')\n    # Consume the fully-qualified type name.\n    name = [tokenizer.ConsumeIdentifier()]\n    while tokenizer.TryConsume('.'):\n      name.append(tokenizer.ConsumeIdentifier())\n    return '.'.join(prefix), '.'.join(name)\n\n  def _MergeMessageField(self, tokenizer, message, field):\n    \"\"\"Merges a single scalar field into a message.\n\n    Args:\n      tokenizer: A tokenizer to parse the field value.\n      message: The message of which field is a member.\n      field: The descriptor of the field to be merged.\n\n    Raises:\n      ParseError: In case of text parsing problems.\n    \"\"\"\n    is_map_entry = _IsMapEntry(field)\n\n    if tokenizer.TryConsume('<'):\n      end_token = '>'\n    else:\n      tokenizer.Consume('{')\n      end_token = '}'\n\n    if field.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n      if field.is_extension:\n        sub_message = message.Extensions[field].add()\n      elif is_map_entry:\n        sub_message = getattr(message, field.name).GetEntryClass()()\n      else:\n        sub_message = getattr(message, field.name).add()\n    else:\n      if field.is_extension:\n        if (not self._allow_multiple_scalars and\n            message.HasExtension(field)):\n          raise tokenizer.ParseErrorPreviousToken(\n              'Message type \"%s\" should not have multiple \"%s\" extensions.' %\n              (message.DESCRIPTOR.full_name, field.full_name))\n        sub_message = message.Extensions[field]\n      else:\n        # Also apply _allow_multiple_scalars to message field.\n        # TODO(jieluo): Change to _allow_singular_overwrites.\n        if (not self._allow_multiple_scalars and\n            message.HasField(field.name)):\n          raise tokenizer.ParseErrorPreviousToken(\n              'Message type \"%s\" should not have multiple \"%s\" fields.' %\n              (message.DESCRIPTOR.full_name, field.name))\n        sub_message = getattr(message, field.name)\n      sub_message.SetInParent()\n\n    while not tokenizer.TryConsume(end_token):\n      if tokenizer.AtEnd():\n        raise tokenizer.ParseErrorPreviousToken('Expected \"%s\".' % (end_token,))\n      self._MergeField(tokenizer, sub_message)\n\n    if is_map_entry:\n      value_cpptype = field.message_type.fields_by_name['value'].cpp_type\n      if value_cpptype == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:\n        value = getattr(message, field.name)[sub_message.key]\n        value.CopyFrom(sub_message.value)\n      else:\n        getattr(message, field.name)[sub_message.key] = sub_message.value\n\n  @staticmethod\n  def _IsProto3Syntax(message):\n    message_descriptor = message.DESCRIPTOR\n    return (hasattr(message_descriptor, 'syntax') and\n            message_descriptor.syntax == 'proto3')\n\n  def _MergeScalarField(self, tokenizer, message, field):\n    \"\"\"Merges a single scalar field into a message.\n\n    Args:\n      tokenizer: A tokenizer to parse the field value.\n      message: A protocol message to record the data.\n      field: The descriptor of the field to be merged.\n\n    Raises:\n      ParseError: In case of text parsing problems.\n      RuntimeError: On runtime errors.\n    \"\"\"\n    _ = self.allow_unknown_extension\n    value = None\n\n    if field.type in (descriptor.FieldDescriptor.TYPE_INT32,\n                      descriptor.FieldDescriptor.TYPE_SINT32,\n                      descriptor.FieldDescriptor.TYPE_SFIXED32):\n      value = _ConsumeInt32(tokenizer)\n    elif field.type in (descriptor.FieldDescriptor.TYPE_INT64,\n                        descriptor.FieldDescriptor.TYPE_SINT64,\n                        descriptor.FieldDescriptor.TYPE_SFIXED64):\n      value = _ConsumeInt64(tokenizer)\n    elif field.type in (descriptor.FieldDescriptor.TYPE_UINT32,\n                        descriptor.FieldDescriptor.TYPE_FIXED32):\n      value = _ConsumeUint32(tokenizer)\n    elif field.type in (descriptor.FieldDescriptor.TYPE_UINT64,\n                        descriptor.FieldDescriptor.TYPE_FIXED64):\n      value = _ConsumeUint64(tokenizer)\n    elif field.type in (descriptor.FieldDescriptor.TYPE_FLOAT,\n                        descriptor.FieldDescriptor.TYPE_DOUBLE):\n      value = tokenizer.ConsumeFloat()\n    elif field.type == descriptor.FieldDescriptor.TYPE_BOOL:\n      value = tokenizer.ConsumeBool()\n    elif field.type == descriptor.FieldDescriptor.TYPE_STRING:\n      value = tokenizer.ConsumeString()\n    elif field.type == descriptor.FieldDescriptor.TYPE_BYTES:\n      value = tokenizer.ConsumeByteString()\n    elif field.type == descriptor.FieldDescriptor.TYPE_ENUM:\n      value = tokenizer.ConsumeEnum(field)\n    else:\n      raise RuntimeError('Unknown field type %d' % field.type)\n\n    if field.label == descriptor.FieldDescriptor.LABEL_REPEATED:\n      if field.is_extension:\n        message.Extensions[field].append(value)\n      else:\n        getattr(message, field.name).append(value)\n    else:\n      if field.is_extension:\n        if (not self._allow_multiple_scalars and\n            not self._IsProto3Syntax(message) and\n            message.HasExtension(field)):\n          raise tokenizer.ParseErrorPreviousToken(\n              'Message type \"%s\" should not have multiple \"%s\" extensions.' %\n              (message.DESCRIPTOR.full_name, field.full_name))\n        else:\n          message.Extensions[field] = value\n      else:\n        duplicate_error = False\n        if not self._allow_multiple_scalars:\n          if self._IsProto3Syntax(message):\n            # Proto3 doesn't represent presence so we try best effort to check\n            # multiple scalars by compare to default values.\n            duplicate_error = bool(getattr(message, field.name))\n          else:\n            duplicate_error = message.HasField(field.name)\n\n        if duplicate_error:\n          raise tokenizer.ParseErrorPreviousToken(\n              'Message type \"%s\" should not have multiple \"%s\" fields.' %\n              (message.DESCRIPTOR.full_name, field.name))\n        else:\n          setattr(message, field.name, value)\n\n\ndef _SkipFieldContents(tokenizer):\n  \"\"\"Skips over contents (value or message) of a field.\n\n  Args:\n    tokenizer: A tokenizer to parse the field name and values.\n  \"\"\"\n  # Try to guess the type of this field.\n  # If this field is not a message, there should be a \":\" between the\n  # field name and the field value and also the field value should not\n  # start with \"{\" or \"<\" which indicates the beginning of a message body.\n  # If there is no \":\" or there is a \"{\" or \"<\" after \":\", this field has\n  # to be a message or the input is ill-formed.\n  if tokenizer.TryConsume(':') and not tokenizer.LookingAt(\n      '{') and not tokenizer.LookingAt('<'):\n    _SkipFieldValue(tokenizer)\n  else:\n    _SkipFieldMessage(tokenizer)\n\n\ndef _SkipField(tokenizer):\n  \"\"\"Skips over a complete field (name and value/message).\n\n  Args:\n    tokenizer: A tokenizer to parse the field name and values.\n  \"\"\"\n  if tokenizer.TryConsume('['):\n    # Consume extension name.\n    tokenizer.ConsumeIdentifier()\n    while tokenizer.TryConsume('.'):\n      tokenizer.ConsumeIdentifier()\n    tokenizer.Consume(']')\n  else:\n    tokenizer.ConsumeIdentifierOrNumber()\n\n  _SkipFieldContents(tokenizer)\n\n  # For historical reasons, fields may optionally be separated by commas or\n  # semicolons.\n  if not tokenizer.TryConsume(','):\n    tokenizer.TryConsume(';')\n\n\ndef _SkipFieldMessage(tokenizer):\n  \"\"\"Skips over a field message.\n\n  Args:\n    tokenizer: A tokenizer to parse the field name and values.\n  \"\"\"\n\n  if tokenizer.TryConsume('<'):\n    delimiter = '>'\n  else:\n    tokenizer.Consume('{')\n    delimiter = '}'\n\n  while not tokenizer.LookingAt('>') and not tokenizer.LookingAt('}'):\n    _SkipField(tokenizer)\n\n  tokenizer.Consume(delimiter)\n\n\ndef _SkipFieldValue(tokenizer):\n  \"\"\"Skips over a field value.\n\n  Args:\n    tokenizer: A tokenizer to parse the field name and values.\n\n  Raises:\n    ParseError: In case an invalid field value is found.\n  \"\"\"\n  # String/bytes tokens can come in multiple adjacent string literals.\n  # If we can consume one, consume as many as we can.\n  if tokenizer.TryConsumeByteString():\n    while tokenizer.TryConsumeByteString():\n      pass\n    return\n\n  if (not tokenizer.TryConsumeIdentifier() and\n      not _TryConsumeInt64(tokenizer) and not _TryConsumeUint64(tokenizer) and\n      not tokenizer.TryConsumeFloat()):\n    raise ParseError('Invalid field value: ' + tokenizer.token)\n\n\nclass Tokenizer(object):\n  \"\"\"Protocol buffer text representation tokenizer.\n\n  This class handles the lower level string parsing by splitting it into\n  meaningful tokens.\n\n  It was directly ported from the Java protocol buffer API.\n  \"\"\"\n\n  _WHITESPACE = re.compile(r'\\s+')\n  _COMMENT = re.compile(r'(\\s*#.*$)', re.MULTILINE)\n  _WHITESPACE_OR_COMMENT = re.compile(r'(\\s|(#.*$))+', re.MULTILINE)\n  _TOKEN = re.compile('|'.join([\n      r'[a-zA-Z_][0-9a-zA-Z_+-]*',  # an identifier\n      r'([0-9+-]|(\\.[0-9]))[0-9a-zA-Z_.+-]*',  # a number\n  ] + [  # quoted str for each quote mark\n      # Avoid backtracking! https://stackoverflow.com/a/844267\n      r'{qt}[^{qt}\\n\\\\]*((\\\\.)+[^{qt}\\n\\\\]*)*({qt}|\\\\?$)'.format(qt=mark)\n      for mark in _QUOTES\n  ]))\n\n  _IDENTIFIER = re.compile(r'[^\\d\\W]\\w*')\n  _IDENTIFIER_OR_NUMBER = re.compile(r'\\w+')\n\n  def __init__(self, lines, skip_comments=True):\n    self._position = 0\n    self._line = -1\n    self._column = 0\n    self._token_start = None\n    self.token = ''\n    self._lines = iter(lines)\n    self._current_line = ''\n    self._previous_line = 0\n    self._previous_column = 0\n    self._more_lines = True\n    self._skip_comments = skip_comments\n    self._whitespace_pattern = (skip_comments and self._WHITESPACE_OR_COMMENT\n                                or self._WHITESPACE)\n    self._SkipWhitespace()\n    self.NextToken()\n\n  def LookingAt(self, token):\n    return self.token == token\n\n  def AtEnd(self):\n    \"\"\"Checks the end of the text was reached.\n\n    Returns:\n      True iff the end was reached.\n    \"\"\"\n    return not self.token\n\n  def _PopLine(self):\n    while len(self._current_line) <= self._column:\n      try:\n        self._current_line = next(self._lines)\n      except StopIteration:\n        self._current_line = ''\n        self._more_lines = False\n        return\n      else:\n        self._line += 1\n        self._column = 0\n\n  def _SkipWhitespace(self):\n    while True:\n      self._PopLine()\n      match = self._whitespace_pattern.match(self._current_line, self._column)\n      if not match:\n        break\n      length = len(match.group(0))\n      self._column += length\n\n  def TryConsume(self, token):\n    \"\"\"Tries to consume a given piece of text.\n\n    Args:\n      token: Text to consume.\n\n    Returns:\n      True iff the text was consumed.\n    \"\"\"\n    if self.token == token:\n      self.NextToken()\n      return True\n    return False\n\n  def Consume(self, token):\n    \"\"\"Consumes a piece of text.\n\n    Args:\n      token: Text to consume.\n\n    Raises:\n      ParseError: If the text couldn't be consumed.\n    \"\"\"\n    if not self.TryConsume(token):\n      raise self.ParseError('Expected \"%s\".' % token)\n\n  def ConsumeComment(self):\n    result = self.token\n    if not self._COMMENT.match(result):\n      raise self.ParseError('Expected comment.')\n    self.NextToken()\n    return result\n\n  def ConsumeCommentOrTrailingComment(self):\n    \"\"\"Consumes a comment, returns a 2-tuple (trailing bool, comment str).\"\"\"\n\n    # Tokenizer initializes _previous_line and _previous_column to 0. As the\n    # tokenizer starts, it looks like there is a previous token on the line.\n    just_started = self._line == 0 and self._column == 0\n\n    before_parsing = self._previous_line\n    comment = self.ConsumeComment()\n\n    # A trailing comment is a comment on the same line than the previous token.\n    trailing = (self._previous_line == before_parsing\n                and not just_started)\n\n    return trailing, comment\n\n  def TryConsumeIdentifier(self):\n    try:\n      self.ConsumeIdentifier()\n      return True\n    except ParseError:\n      return False\n\n  def ConsumeIdentifier(self):\n    \"\"\"Consumes protocol message field identifier.\n\n    Returns:\n      Identifier string.\n\n    Raises:\n      ParseError: If an identifier couldn't be consumed.\n    \"\"\"\n    result = self.token\n    if not self._IDENTIFIER.match(result):\n      raise self.ParseError('Expected identifier.')\n    self.NextToken()\n    return result\n\n  def TryConsumeIdentifierOrNumber(self):\n    try:\n      self.ConsumeIdentifierOrNumber()\n      return True\n    except ParseError:\n      return False\n\n  def ConsumeIdentifierOrNumber(self):\n    \"\"\"Consumes protocol message field identifier.\n\n    Returns:\n      Identifier string.\n\n    Raises:\n      ParseError: If an identifier couldn't be consumed.\n    \"\"\"\n    result = self.token\n    if not self._IDENTIFIER_OR_NUMBER.match(result):\n      raise self.ParseError('Expected identifier or number, got %s.' % result)\n    self.NextToken()\n    return result\n\n  def TryConsumeInteger(self):\n    try:\n      self.ConsumeInteger()\n      return True\n    except ParseError:\n      return False\n\n  def ConsumeInteger(self):\n    \"\"\"Consumes an integer number.\n\n    Returns:\n      The integer parsed.\n\n    Raises:\n      ParseError: If an integer couldn't be consumed.\n    \"\"\"\n    try:\n      result = _ParseAbstractInteger(self.token)\n    except ValueError as e:\n      raise self.ParseError(str(e))\n    self.NextToken()\n    return result\n\n  def TryConsumeFloat(self):\n    try:\n      self.ConsumeFloat()\n      return True\n    except ParseError:\n      return False\n\n  def ConsumeFloat(self):\n    \"\"\"Consumes an floating point number.\n\n    Returns:\n      The number parsed.\n\n    Raises:\n      ParseError: If a floating point number couldn't be consumed.\n    \"\"\"\n    try:\n      result = ParseFloat(self.token)\n    except ValueError as e:\n      raise self.ParseError(str(e))\n    self.NextToken()\n    return result\n\n  def ConsumeBool(self):\n    \"\"\"Consumes a boolean value.\n\n    Returns:\n      The bool parsed.\n\n    Raises:\n      ParseError: If a boolean value couldn't be consumed.\n    \"\"\"\n    try:\n      result = ParseBool(self.token)\n    except ValueError as e:\n      raise self.ParseError(str(e))\n    self.NextToken()\n    return result\n\n  def TryConsumeByteString(self):\n    try:\n      self.ConsumeByteString()\n      return True\n    except ParseError:\n      return False\n\n  def ConsumeString(self):\n    \"\"\"Consumes a string value.\n\n    Returns:\n      The string parsed.\n\n    Raises:\n      ParseError: If a string value couldn't be consumed.\n    \"\"\"\n    the_bytes = self.ConsumeByteString()\n    try:\n      return str(the_bytes, 'utf-8')\n    except UnicodeDecodeError as e:\n      raise self._StringParseError(e)\n\n  def ConsumeByteString(self):\n    \"\"\"Consumes a byte array value.\n\n    Returns:\n      The array parsed (as a string).\n\n    Raises:\n      ParseError: If a byte array value couldn't be consumed.\n    \"\"\"\n    the_list = [self._ConsumeSingleByteString()]\n    while self.token and self.token[0] in _QUOTES:\n      the_list.append(self._ConsumeSingleByteString())\n    return b''.join(the_list)\n\n  def _ConsumeSingleByteString(self):\n    \"\"\"Consume one token of a string literal.\n\n    String literals (whether bytes or text) can come in multiple adjacent\n    tokens which are automatically concatenated, like in C or Python.  This\n    method only consumes one token.\n\n    Returns:\n      The token parsed.\n    Raises:\n      ParseError: When the wrong format data is found.\n    \"\"\"\n    text = self.token\n    if len(text) < 1 or text[0] not in _QUOTES:\n      raise self.ParseError('Expected string but found: %r' % (text,))\n\n    if len(text) < 2 or text[-1] != text[0]:\n      raise self.ParseError('String missing ending quote: %r' % (text,))\n\n    try:\n      result = text_encoding.CUnescape(text[1:-1])\n    except ValueError as e:\n      raise self.ParseError(str(e))\n    self.NextToken()\n    return result\n\n  def ConsumeEnum(self, field):\n    try:\n      result = ParseEnum(field, self.token)\n    except ValueError as e:\n      raise self.ParseError(str(e))\n    self.NextToken()\n    return result\n\n  def ParseErrorPreviousToken(self, message):\n    \"\"\"Creates and *returns* a ParseError for the previously read token.\n\n    Args:\n      message: A message to set for the exception.\n\n    Returns:\n      A ParseError instance.\n    \"\"\"\n    return ParseError(message, self._previous_line + 1,\n                      self._previous_column + 1)\n\n  def ParseError(self, message):\n    \"\"\"Creates and *returns* a ParseError for the current token.\"\"\"\n    return ParseError('\\'' + self._current_line + '\\': ' + message,\n                      self._line + 1, self._column + 1)\n\n  def _StringParseError(self, e):\n    return self.ParseError('Couldn\\'t parse string: ' + str(e))\n\n  def NextToken(self):\n    \"\"\"Reads the next meaningful token.\"\"\"\n    self._previous_line = self._line\n    self._previous_column = self._column\n\n    self._column += len(self.token)\n    self._SkipWhitespace()\n\n    if not self._more_lines:\n      self.token = ''\n      return\n\n    match = self._TOKEN.match(self._current_line, self._column)\n    if not match and not self._skip_comments:\n      match = self._COMMENT.match(self._current_line, self._column)\n    if match:\n      token = match.group(0)\n      self.token = token\n    else:\n      self.token = self._current_line[self._column]\n\n# Aliased so it can still be accessed by current visibility violators.\n# TODO(dbarnett): Migrate violators to textformat_tokenizer.\n_Tokenizer = Tokenizer  # pylint: disable=invalid-name\n\n\ndef _ConsumeInt32(tokenizer):\n  \"\"\"Consumes a signed 32bit integer number from tokenizer.\n\n  Args:\n    tokenizer: A tokenizer used to parse the number.\n\n  Returns:\n    The integer parsed.\n\n  Raises:\n    ParseError: If a signed 32bit integer couldn't be consumed.\n  \"\"\"\n  return _ConsumeInteger(tokenizer, is_signed=True, is_long=False)\n\n\ndef _ConsumeUint32(tokenizer):\n  \"\"\"Consumes an unsigned 32bit integer number from tokenizer.\n\n  Args:\n    tokenizer: A tokenizer used to parse the number.\n\n  Returns:\n    The integer parsed.\n\n  Raises:\n    ParseError: If an unsigned 32bit integer couldn't be consumed.\n  \"\"\"\n  return _ConsumeInteger(tokenizer, is_signed=False, is_long=False)\n\n\ndef _TryConsumeInt64(tokenizer):\n  try:\n    _ConsumeInt64(tokenizer)\n    return True\n  except ParseError:\n    return False\n\n\ndef _ConsumeInt64(tokenizer):\n  \"\"\"Consumes a signed 32bit integer number from tokenizer.\n\n  Args:\n    tokenizer: A tokenizer used to parse the number.\n\n  Returns:\n    The integer parsed.\n\n  Raises:\n    ParseError: If a signed 32bit integer couldn't be consumed.\n  \"\"\"\n  return _ConsumeInteger(tokenizer, is_signed=True, is_long=True)\n\n\ndef _TryConsumeUint64(tokenizer):\n  try:\n    _ConsumeUint64(tokenizer)\n    return True\n  except ParseError:\n    return False\n\n\ndef _ConsumeUint64(tokenizer):\n  \"\"\"Consumes an unsigned 64bit integer number from tokenizer.\n\n  Args:\n    tokenizer: A tokenizer used to parse the number.\n\n  Returns:\n    The integer parsed.\n\n  Raises:\n    ParseError: If an unsigned 64bit integer couldn't be consumed.\n  \"\"\"\n  return _ConsumeInteger(tokenizer, is_signed=False, is_long=True)\n\n\ndef _ConsumeInteger(tokenizer, is_signed=False, is_long=False):\n  \"\"\"Consumes an integer number from tokenizer.\n\n  Args:\n    tokenizer: A tokenizer used to parse the number.\n    is_signed: True if a signed integer must be parsed.\n    is_long: True if a long integer must be parsed.\n\n  Returns:\n    The integer parsed.\n\n  Raises:\n    ParseError: If an integer with given characteristics couldn't be consumed.\n  \"\"\"\n  try:\n    result = ParseInteger(tokenizer.token, is_signed=is_signed, is_long=is_long)\n  except ValueError as e:\n    raise tokenizer.ParseError(str(e))\n  tokenizer.NextToken()\n  return result\n\n\ndef ParseInteger(text, is_signed=False, is_long=False):\n  \"\"\"Parses an integer.\n\n  Args:\n    text: The text to parse.\n    is_signed: True if a signed integer must be parsed.\n    is_long: True if a long integer must be parsed.\n\n  Returns:\n    The integer value.\n\n  Raises:\n    ValueError: Thrown Iff the text is not a valid integer.\n  \"\"\"\n  # Do the actual parsing. Exception handling is propagated to caller.\n  result = _ParseAbstractInteger(text)\n\n  # Check if the integer is sane. Exceptions handled by callers.\n  checker = _INTEGER_CHECKERS[2 * int(is_long) + int(is_signed)]\n  checker.CheckValue(result)\n  return result\n\n\ndef _ParseAbstractInteger(text):\n  \"\"\"Parses an integer without checking size/signedness.\n\n  Args:\n    text: The text to parse.\n\n  Returns:\n    The integer value.\n\n  Raises:\n    ValueError: Thrown Iff the text is not a valid integer.\n  \"\"\"\n  # Do the actual parsing. Exception handling is propagated to caller.\n  orig_text = text\n  c_octal_match = re.match(r'(-?)0(\\d+)$', text)\n  if c_octal_match:\n    # Python 3 no longer supports 0755 octal syntax without the 'o', so\n    # we always use the '0o' prefix for multi-digit numbers starting with 0.\n    text = c_octal_match.group(1) + '0o' + c_octal_match.group(2)\n  try:\n    return int(text, 0)\n  except ValueError:\n    raise ValueError('Couldn\\'t parse integer: %s' % orig_text)\n\n\ndef ParseFloat(text):\n  \"\"\"Parse a floating point number.\n\n  Args:\n    text: Text to parse.\n\n  Returns:\n    The number parsed.\n\n  Raises:\n    ValueError: If a floating point number couldn't be parsed.\n  \"\"\"\n  try:\n    # Assume Python compatible syntax.\n    return float(text)\n  except ValueError:\n    # Check alternative spellings.\n    if _FLOAT_INFINITY.match(text):\n      if text[0] == '-':\n        return float('-inf')\n      else:\n        return float('inf')\n    elif _FLOAT_NAN.match(text):\n      return float('nan')\n    else:\n      # assume '1.0f' format\n      try:\n        return float(text.rstrip('f'))\n      except ValueError:\n        raise ValueError('Couldn\\'t parse float: %s' % text)\n\n\ndef ParseBool(text):\n  \"\"\"Parse a boolean value.\n\n  Args:\n    text: Text to parse.\n\n  Returns:\n    Boolean values parsed\n\n  Raises:\n    ValueError: If text is not a valid boolean.\n  \"\"\"\n  if text in ('true', 't', '1', 'True'):\n    return True\n  elif text in ('false', 'f', '0', 'False'):\n    return False\n  else:\n    raise ValueError('Expected \"true\" or \"false\".')\n\n\ndef ParseEnum(field, value):\n  \"\"\"Parse an enum value.\n\n  The value can be specified by a number (the enum value), or by\n  a string literal (the enum name).\n\n  Args:\n    field: Enum field descriptor.\n    value: String value.\n\n  Returns:\n    Enum value number.\n\n  Raises:\n    ValueError: If the enum value could not be parsed.\n  \"\"\"\n  enum_descriptor = field.enum_type\n  try:\n    number = int(value, 0)\n  except ValueError:\n    # Identifier.\n    enum_value = enum_descriptor.values_by_name.get(value, None)\n    if enum_value is None:\n      raise ValueError('Enum type \"%s\" has no value named %s.' %\n                       (enum_descriptor.full_name, value))\n  else:\n    # Numeric value.\n    if hasattr(field.file, 'syntax'):\n      # Attribute is checked for compatibility.\n      if field.file.syntax == 'proto3':\n        # Proto3 accept numeric unknown enums.\n        return number\n    enum_value = enum_descriptor.values_by_number.get(number, None)\n    if enum_value is None:\n      raise ValueError('Enum type \"%s\" has no value with number %d.' %\n                       (enum_descriptor.full_name, number))\n  return enum_value.number\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/timestamp_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/timestamp.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x1fgoogle/protobuf/timestamp.proto\\x12\\x0fgoogle.protobuf\\\"+\\n\\tTimestamp\\x12\\x0f\\n\\x07seconds\\x18\\x01 \\x01(\\x03\\x12\\r\\n\\x05nanos\\x18\\x02 \\x01(\\x05\\x42\\x85\\x01\\n\\x13\\x63om.google.protobufB\\x0eTimestampProtoP\\x01Z2google.golang.org/protobuf/types/known/timestamppb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.timestamp_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\016TimestampProtoP\\001Z2google.golang.org/protobuf/types/known/timestamppb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _TIMESTAMP._serialized_start=52\n  _TIMESTAMP._serialized_end=95\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/type_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/type.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2\nfrom google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x1agoogle/protobuf/type.proto\\x12\\x0fgoogle.protobuf\\x1a\\x19google/protobuf/any.proto\\x1a$google/protobuf/source_context.proto\\\"\\xd7\\x01\\n\\x04Type\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12&\\n\\x06\\x66ields\\x18\\x02 \\x03(\\x0b\\x32\\x16.google.protobuf.Field\\x12\\x0e\\n\\x06oneofs\\x18\\x03 \\x03(\\t\\x12(\\n\\x07options\\x18\\x04 \\x03(\\x0b\\x32\\x17.google.protobuf.Option\\x12\\x36\\n\\x0esource_context\\x18\\x05 \\x01(\\x0b\\x32\\x1e.google.protobuf.SourceContext\\x12\\'\\n\\x06syntax\\x18\\x06 \\x01(\\x0e\\x32\\x17.google.protobuf.Syntax\\\"\\xd5\\x05\\n\\x05\\x46ield\\x12)\\n\\x04kind\\x18\\x01 \\x01(\\x0e\\x32\\x1b.google.protobuf.Field.Kind\\x12\\x37\\n\\x0b\\x63\\x61rdinality\\x18\\x02 \\x01(\\x0e\\x32\\\".google.protobuf.Field.Cardinality\\x12\\x0e\\n\\x06number\\x18\\x03 \\x01(\\x05\\x12\\x0c\\n\\x04name\\x18\\x04 \\x01(\\t\\x12\\x10\\n\\x08type_url\\x18\\x06 \\x01(\\t\\x12\\x13\\n\\x0boneof_index\\x18\\x07 \\x01(\\x05\\x12\\x0e\\n\\x06packed\\x18\\x08 \\x01(\\x08\\x12(\\n\\x07options\\x18\\t \\x03(\\x0b\\x32\\x17.google.protobuf.Option\\x12\\x11\\n\\tjson_name\\x18\\n \\x01(\\t\\x12\\x15\\n\\rdefault_value\\x18\\x0b \\x01(\\t\\\"\\xc8\\x02\\n\\x04Kind\\x12\\x10\\n\\x0cTYPE_UNKNOWN\\x10\\x00\\x12\\x0f\\n\\x0bTYPE_DOUBLE\\x10\\x01\\x12\\x0e\\n\\nTYPE_FLOAT\\x10\\x02\\x12\\x0e\\n\\nTYPE_INT64\\x10\\x03\\x12\\x0f\\n\\x0bTYPE_UINT64\\x10\\x04\\x12\\x0e\\n\\nTYPE_INT32\\x10\\x05\\x12\\x10\\n\\x0cTYPE_FIXED64\\x10\\x06\\x12\\x10\\n\\x0cTYPE_FIXED32\\x10\\x07\\x12\\r\\n\\tTYPE_BOOL\\x10\\x08\\x12\\x0f\\n\\x0bTYPE_STRING\\x10\\t\\x12\\x0e\\n\\nTYPE_GROUP\\x10\\n\\x12\\x10\\n\\x0cTYPE_MESSAGE\\x10\\x0b\\x12\\x0e\\n\\nTYPE_BYTES\\x10\\x0c\\x12\\x0f\\n\\x0bTYPE_UINT32\\x10\\r\\x12\\r\\n\\tTYPE_ENUM\\x10\\x0e\\x12\\x11\\n\\rTYPE_SFIXED32\\x10\\x0f\\x12\\x11\\n\\rTYPE_SFIXED64\\x10\\x10\\x12\\x0f\\n\\x0bTYPE_SINT32\\x10\\x11\\x12\\x0f\\n\\x0bTYPE_SINT64\\x10\\x12\\\"t\\n\\x0b\\x43\\x61rdinality\\x12\\x17\\n\\x13\\x43\\x41RDINALITY_UNKNOWN\\x10\\x00\\x12\\x18\\n\\x14\\x43\\x41RDINALITY_OPTIONAL\\x10\\x01\\x12\\x18\\n\\x14\\x43\\x41RDINALITY_REQUIRED\\x10\\x02\\x12\\x18\\n\\x14\\x43\\x41RDINALITY_REPEATED\\x10\\x03\\\"\\xce\\x01\\n\\x04\\x45num\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12-\\n\\tenumvalue\\x18\\x02 \\x03(\\x0b\\x32\\x1a.google.protobuf.EnumValue\\x12(\\n\\x07options\\x18\\x03 \\x03(\\x0b\\x32\\x17.google.protobuf.Option\\x12\\x36\\n\\x0esource_context\\x18\\x04 \\x01(\\x0b\\x32\\x1e.google.protobuf.SourceContext\\x12\\'\\n\\x06syntax\\x18\\x05 \\x01(\\x0e\\x32\\x17.google.protobuf.Syntax\\\"S\\n\\tEnumValue\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x0e\\n\\x06number\\x18\\x02 \\x01(\\x05\\x12(\\n\\x07options\\x18\\x03 \\x03(\\x0b\\x32\\x17.google.protobuf.Option\\\";\\n\\x06Option\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12#\\n\\x05value\\x18\\x02 \\x01(\\x0b\\x32\\x14.google.protobuf.Any*.\\n\\x06Syntax\\x12\\x11\\n\\rSYNTAX_PROTO2\\x10\\x00\\x12\\x11\\n\\rSYNTAX_PROTO3\\x10\\x01\\x42{\\n\\x13\\x63om.google.protobufB\\tTypeProtoP\\x01Z-google.golang.org/protobuf/types/known/typepb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.type_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\tTypeProtoP\\001Z-google.golang.org/protobuf/types/known/typepb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _SYNTAX._serialized_start=1413\n  _SYNTAX._serialized_end=1459\n  _TYPE._serialized_start=113\n  _TYPE._serialized_end=328\n  _FIELD._serialized_start=331\n  _FIELD._serialized_end=1056\n  _FIELD_KIND._serialized_start=610\n  _FIELD_KIND._serialized_end=938\n  _FIELD_CARDINALITY._serialized_start=940\n  _FIELD_CARDINALITY._serialized_end=1056\n  _ENUM._serialized_start=1059\n  _ENUM._serialized_end=1265\n  _ENUMVALUE._serialized_start=1267\n  _ENUMVALUE._serialized_end=1350\n  _OPTION._serialized_start=1352\n  _OPTION._serialized_end=1411\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/util/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/util/json_format_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/util/json_format.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n&google/protobuf/util/json_format.proto\\x12\\x11protobuf_unittest\\\"\\x89\\x01\\n\\x13TestFlagsAndStrings\\x12\\t\\n\\x01\\x41\\x18\\x01 \\x02(\\x05\\x12K\\n\\rrepeatedgroup\\x18\\x02 \\x03(\\n24.protobuf_unittest.TestFlagsAndStrings.RepeatedGroup\\x1a\\x1a\\n\\rRepeatedGroup\\x12\\t\\n\\x01\\x66\\x18\\x03 \\x02(\\t\\\"!\\n\\x14TestBase64ByteArrays\\x12\\t\\n\\x01\\x61\\x18\\x01 \\x02(\\x0c\\\"G\\n\\x12TestJavaScriptJSON\\x12\\t\\n\\x01\\x61\\x18\\x01 \\x01(\\x05\\x12\\r\\n\\x05\\x66inal\\x18\\x02 \\x01(\\x02\\x12\\n\\n\\x02in\\x18\\x03 \\x01(\\t\\x12\\x0b\\n\\x03Var\\x18\\x04 \\x01(\\t\\\"Q\\n\\x18TestJavaScriptOrderJSON1\\x12\\t\\n\\x01\\x64\\x18\\x01 \\x01(\\x05\\x12\\t\\n\\x01\\x63\\x18\\x02 \\x01(\\x05\\x12\\t\\n\\x01x\\x18\\x03 \\x01(\\x08\\x12\\t\\n\\x01\\x62\\x18\\x04 \\x01(\\x05\\x12\\t\\n\\x01\\x61\\x18\\x05 \\x01(\\x05\\\"\\x89\\x01\\n\\x18TestJavaScriptOrderJSON2\\x12\\t\\n\\x01\\x64\\x18\\x01 \\x01(\\x05\\x12\\t\\n\\x01\\x63\\x18\\x02 \\x01(\\x05\\x12\\t\\n\\x01x\\x18\\x03 \\x01(\\x08\\x12\\t\\n\\x01\\x62\\x18\\x04 \\x01(\\x05\\x12\\t\\n\\x01\\x61\\x18\\x05 \\x01(\\x05\\x12\\x36\\n\\x01z\\x18\\x06 \\x03(\\x0b\\x32+.protobuf_unittest.TestJavaScriptOrderJSON1\\\"$\\n\\x0cTestLargeInt\\x12\\t\\n\\x01\\x61\\x18\\x01 \\x02(\\x03\\x12\\t\\n\\x01\\x62\\x18\\x02 \\x02(\\x04\\\"\\xa0\\x01\\n\\x0bTestNumbers\\x12\\x30\\n\\x01\\x61\\x18\\x01 \\x01(\\x0e\\x32%.protobuf_unittest.TestNumbers.MyType\\x12\\t\\n\\x01\\x62\\x18\\x02 \\x01(\\x05\\x12\\t\\n\\x01\\x63\\x18\\x03 \\x01(\\x02\\x12\\t\\n\\x01\\x64\\x18\\x04 \\x01(\\x08\\x12\\t\\n\\x01\\x65\\x18\\x05 \\x01(\\x01\\x12\\t\\n\\x01\\x66\\x18\\x06 \\x01(\\r\\\"(\\n\\x06MyType\\x12\\x06\\n\\x02OK\\x10\\x00\\x12\\x0b\\n\\x07WARNING\\x10\\x01\\x12\\t\\n\\x05\\x45RROR\\x10\\x02\\\"T\\n\\rTestCamelCase\\x12\\x14\\n\\x0cnormal_field\\x18\\x01 \\x01(\\t\\x12\\x15\\n\\rCAPITAL_FIELD\\x18\\x02 \\x01(\\x05\\x12\\x16\\n\\x0e\\x43\\x61melCaseField\\x18\\x03 \\x01(\\x05\\\"|\\n\\x0bTestBoolMap\\x12=\\n\\x08\\x62ool_map\\x18\\x01 \\x03(\\x0b\\x32+.protobuf_unittest.TestBoolMap.BoolMapEntry\\x1a.\\n\\x0c\\x42oolMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x08\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\\"O\\n\\rTestRecursion\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x05\\x12/\\n\\x05\\x63hild\\x18\\x02 \\x01(\\x0b\\x32 .protobuf_unittest.TestRecursion\\\"\\x86\\x01\\n\\rTestStringMap\\x12\\x43\\n\\nstring_map\\x18\\x01 \\x03(\\x0b\\x32/.protobuf_unittest.TestStringMap.StringMapEntry\\x1a\\x30\\n\\x0eStringMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\t:\\x02\\x38\\x01\\\"\\xc4\\x01\\n\\x14TestStringSerializer\\x12\\x15\\n\\rscalar_string\\x18\\x01 \\x01(\\t\\x12\\x17\\n\\x0frepeated_string\\x18\\x02 \\x03(\\t\\x12J\\n\\nstring_map\\x18\\x03 \\x03(\\x0b\\x32\\x36.protobuf_unittest.TestStringSerializer.StringMapEntry\\x1a\\x30\\n\\x0eStringMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\t:\\x02\\x38\\x01\\\"$\\n\\x18TestMessageWithExtension*\\x08\\x08\\x64\\x10\\x80\\x80\\x80\\x80\\x02\\\"z\\n\\rTestExtension\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\t2Z\\n\\x03\\x65xt\\x12+.protobuf_unittest.TestMessageWithExtension\\x18\\x64 \\x01(\\x0b\\x32 .protobuf_unittest.TestExtension\\\"Q\\n\\x14TestDefaultEnumValue\\x12\\x39\\n\\nenum_value\\x18\\x01 \\x01(\\x0e\\x32\\x1c.protobuf_unittest.EnumValue:\\x07\\x44\\x45\\x46\\x41ULT*2\\n\\tEnumValue\\x12\\x0c\\n\\x08PROTOCOL\\x10\\x00\\x12\\n\\n\\x06\\x42UFFER\\x10\\x01\\x12\\x0b\\n\\x07\\x44\\x45\\x46\\x41ULT\\x10\\x02')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n  TestMessageWithExtension.RegisterExtension(_TESTEXTENSION.extensions_by_name['ext'])\n\n  DESCRIPTOR._options = None\n  _TESTBOOLMAP_BOOLMAPENTRY._options = None\n  _TESTBOOLMAP_BOOLMAPENTRY._serialized_options = b'8\\001'\n  _TESTSTRINGMAP_STRINGMAPENTRY._options = None\n  _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\\001'\n  _TESTSTRINGSERIALIZER_STRINGMAPENTRY._options = None\n  _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_options = b'8\\001'\n  _ENUMVALUE._serialized_start=1607\n  _ENUMVALUE._serialized_end=1657\n  _TESTFLAGSANDSTRINGS._serialized_start=62\n  _TESTFLAGSANDSTRINGS._serialized_end=199\n  _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_start=173\n  _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_end=199\n  _TESTBASE64BYTEARRAYS._serialized_start=201\n  _TESTBASE64BYTEARRAYS._serialized_end=234\n  _TESTJAVASCRIPTJSON._serialized_start=236\n  _TESTJAVASCRIPTJSON._serialized_end=307\n  _TESTJAVASCRIPTORDERJSON1._serialized_start=309\n  _TESTJAVASCRIPTORDERJSON1._serialized_end=390\n  _TESTJAVASCRIPTORDERJSON2._serialized_start=393\n  _TESTJAVASCRIPTORDERJSON2._serialized_end=530\n  _TESTLARGEINT._serialized_start=532\n  _TESTLARGEINT._serialized_end=568\n  _TESTNUMBERS._serialized_start=571\n  _TESTNUMBERS._serialized_end=731\n  _TESTNUMBERS_MYTYPE._serialized_start=691\n  _TESTNUMBERS_MYTYPE._serialized_end=731\n  _TESTCAMELCASE._serialized_start=733\n  _TESTCAMELCASE._serialized_end=817\n  _TESTBOOLMAP._serialized_start=819\n  _TESTBOOLMAP._serialized_end=943\n  _TESTBOOLMAP_BOOLMAPENTRY._serialized_start=897\n  _TESTBOOLMAP_BOOLMAPENTRY._serialized_end=943\n  _TESTRECURSION._serialized_start=945\n  _TESTRECURSION._serialized_end=1024\n  _TESTSTRINGMAP._serialized_start=1027\n  _TESTSTRINGMAP._serialized_end=1161\n  _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=1113\n  _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=1161\n  _TESTSTRINGSERIALIZER._serialized_start=1164\n  _TESTSTRINGSERIALIZER._serialized_end=1360\n  _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_start=1113\n  _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_end=1161\n  _TESTMESSAGEWITHEXTENSION._serialized_start=1362\n  _TESTMESSAGEWITHEXTENSION._serialized_end=1398\n  _TESTEXTENSION._serialized_start=1400\n  _TESTEXTENSION._serialized_end=1522\n  _TESTDEFAULTENUMVALUE._serialized_start=1524\n  _TESTDEFAULTENUMVALUE._serialized_end=1605\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/util/json_format_proto3_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/util/json_format_proto3.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2\nfrom google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2\nfrom google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2\nfrom google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2\nfrom google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2\nfrom google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2\nfrom google.protobuf import unittest_pb2 as google_dot_protobuf_dot_unittest__pb2\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n-google/protobuf/util/json_format_proto3.proto\\x12\\x06proto3\\x1a\\x19google/protobuf/any.proto\\x1a\\x1egoogle/protobuf/duration.proto\\x1a google/protobuf/field_mask.proto\\x1a\\x1cgoogle/protobuf/struct.proto\\x1a\\x1fgoogle/protobuf/timestamp.proto\\x1a\\x1egoogle/protobuf/wrappers.proto\\x1a\\x1egoogle/protobuf/unittest.proto\\\"\\x1c\\n\\x0bMessageType\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x05\\\"\\x94\\x05\\n\\x0bTestMessage\\x12\\x12\\n\\nbool_value\\x18\\x01 \\x01(\\x08\\x12\\x13\\n\\x0bint32_value\\x18\\x02 \\x01(\\x05\\x12\\x13\\n\\x0bint64_value\\x18\\x03 \\x01(\\x03\\x12\\x14\\n\\x0cuint32_value\\x18\\x04 \\x01(\\r\\x12\\x14\\n\\x0cuint64_value\\x18\\x05 \\x01(\\x04\\x12\\x13\\n\\x0b\\x66loat_value\\x18\\x06 \\x01(\\x02\\x12\\x14\\n\\x0c\\x64ouble_value\\x18\\x07 \\x01(\\x01\\x12\\x14\\n\\x0cstring_value\\x18\\x08 \\x01(\\t\\x12\\x13\\n\\x0b\\x62ytes_value\\x18\\t \\x01(\\x0c\\x12$\\n\\nenum_value\\x18\\n \\x01(\\x0e\\x32\\x10.proto3.EnumType\\x12*\\n\\rmessage_value\\x18\\x0b \\x01(\\x0b\\x32\\x13.proto3.MessageType\\x12\\x1b\\n\\x13repeated_bool_value\\x18\\x15 \\x03(\\x08\\x12\\x1c\\n\\x14repeated_int32_value\\x18\\x16 \\x03(\\x05\\x12\\x1c\\n\\x14repeated_int64_value\\x18\\x17 \\x03(\\x03\\x12\\x1d\\n\\x15repeated_uint32_value\\x18\\x18 \\x03(\\r\\x12\\x1d\\n\\x15repeated_uint64_value\\x18\\x19 \\x03(\\x04\\x12\\x1c\\n\\x14repeated_float_value\\x18\\x1a \\x03(\\x02\\x12\\x1d\\n\\x15repeated_double_value\\x18\\x1b \\x03(\\x01\\x12\\x1d\\n\\x15repeated_string_value\\x18\\x1c \\x03(\\t\\x12\\x1c\\n\\x14repeated_bytes_value\\x18\\x1d \\x03(\\x0c\\x12-\\n\\x13repeated_enum_value\\x18\\x1e \\x03(\\x0e\\x32\\x10.proto3.EnumType\\x12\\x33\\n\\x16repeated_message_value\\x18\\x1f \\x03(\\x0b\\x32\\x13.proto3.MessageType\\\"\\x8c\\x02\\n\\tTestOneof\\x12\\x1b\\n\\x11oneof_int32_value\\x18\\x01 \\x01(\\x05H\\x00\\x12\\x1c\\n\\x12oneof_string_value\\x18\\x02 \\x01(\\tH\\x00\\x12\\x1b\\n\\x11oneof_bytes_value\\x18\\x03 \\x01(\\x0cH\\x00\\x12,\\n\\x10oneof_enum_value\\x18\\x04 \\x01(\\x0e\\x32\\x10.proto3.EnumTypeH\\x00\\x12\\x32\\n\\x13oneof_message_value\\x18\\x05 \\x01(\\x0b\\x32\\x13.proto3.MessageTypeH\\x00\\x12\\x36\\n\\x10oneof_null_value\\x18\\x06 \\x01(\\x0e\\x32\\x1a.google.protobuf.NullValueH\\x00\\x42\\r\\n\\x0boneof_value\\\"\\xe1\\x04\\n\\x07TestMap\\x12.\\n\\x08\\x62ool_map\\x18\\x01 \\x03(\\x0b\\x32\\x1c.proto3.TestMap.BoolMapEntry\\x12\\x30\\n\\tint32_map\\x18\\x02 \\x03(\\x0b\\x32\\x1d.proto3.TestMap.Int32MapEntry\\x12\\x30\\n\\tint64_map\\x18\\x03 \\x03(\\x0b\\x32\\x1d.proto3.TestMap.Int64MapEntry\\x12\\x32\\n\\nuint32_map\\x18\\x04 \\x03(\\x0b\\x32\\x1e.proto3.TestMap.Uint32MapEntry\\x12\\x32\\n\\nuint64_map\\x18\\x05 \\x03(\\x0b\\x32\\x1e.proto3.TestMap.Uint64MapEntry\\x12\\x32\\n\\nstring_map\\x18\\x06 \\x03(\\x0b\\x32\\x1e.proto3.TestMap.StringMapEntry\\x1a.\\n\\x0c\\x42oolMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x08\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a/\\n\\rInt32MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x05\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a/\\n\\rInt64MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x03\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x30\\n\\x0eUint32MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\r\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x30\\n\\x0eUint64MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x04\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x30\\n\\x0eStringMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\\"\\x85\\x06\\n\\rTestNestedMap\\x12\\x34\\n\\x08\\x62ool_map\\x18\\x01 \\x03(\\x0b\\x32\\\".proto3.TestNestedMap.BoolMapEntry\\x12\\x36\\n\\tint32_map\\x18\\x02 \\x03(\\x0b\\x32#.proto3.TestNestedMap.Int32MapEntry\\x12\\x36\\n\\tint64_map\\x18\\x03 \\x03(\\x0b\\x32#.proto3.TestNestedMap.Int64MapEntry\\x12\\x38\\n\\nuint32_map\\x18\\x04 \\x03(\\x0b\\x32$.proto3.TestNestedMap.Uint32MapEntry\\x12\\x38\\n\\nuint64_map\\x18\\x05 \\x03(\\x0b\\x32$.proto3.TestNestedMap.Uint64MapEntry\\x12\\x38\\n\\nstring_map\\x18\\x06 \\x03(\\x0b\\x32$.proto3.TestNestedMap.StringMapEntry\\x12\\x32\\n\\x07map_map\\x18\\x07 \\x03(\\x0b\\x32!.proto3.TestNestedMap.MapMapEntry\\x1a.\\n\\x0c\\x42oolMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x08\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a/\\n\\rInt32MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x05\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a/\\n\\rInt64MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x03\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x30\\n\\x0eUint32MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\r\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x30\\n\\x0eUint64MapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x04\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x30\\n\\x0eStringMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\x1a\\x44\\n\\x0bMapMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12$\\n\\x05value\\x18\\x02 \\x01(\\x0b\\x32\\x15.proto3.TestNestedMap:\\x02\\x38\\x01\\\"{\\n\\rTestStringMap\\x12\\x38\\n\\nstring_map\\x18\\x01 \\x03(\\x0b\\x32$.proto3.TestStringMap.StringMapEntry\\x1a\\x30\\n\\x0eStringMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\t:\\x02\\x38\\x01\\\"\\xee\\x07\\n\\x0bTestWrapper\\x12.\\n\\nbool_value\\x18\\x01 \\x01(\\x0b\\x32\\x1a.google.protobuf.BoolValue\\x12\\x30\\n\\x0bint32_value\\x18\\x02 \\x01(\\x0b\\x32\\x1b.google.protobuf.Int32Value\\x12\\x30\\n\\x0bint64_value\\x18\\x03 \\x01(\\x0b\\x32\\x1b.google.protobuf.Int64Value\\x12\\x32\\n\\x0cuint32_value\\x18\\x04 \\x01(\\x0b\\x32\\x1c.google.protobuf.UInt32Value\\x12\\x32\\n\\x0cuint64_value\\x18\\x05 \\x01(\\x0b\\x32\\x1c.google.protobuf.UInt64Value\\x12\\x30\\n\\x0b\\x66loat_value\\x18\\x06 \\x01(\\x0b\\x32\\x1b.google.protobuf.FloatValue\\x12\\x32\\n\\x0c\\x64ouble_value\\x18\\x07 \\x01(\\x0b\\x32\\x1c.google.protobuf.DoubleValue\\x12\\x32\\n\\x0cstring_value\\x18\\x08 \\x01(\\x0b\\x32\\x1c.google.protobuf.StringValue\\x12\\x30\\n\\x0b\\x62ytes_value\\x18\\t \\x01(\\x0b\\x32\\x1b.google.protobuf.BytesValue\\x12\\x37\\n\\x13repeated_bool_value\\x18\\x0b \\x03(\\x0b\\x32\\x1a.google.protobuf.BoolValue\\x12\\x39\\n\\x14repeated_int32_value\\x18\\x0c \\x03(\\x0b\\x32\\x1b.google.protobuf.Int32Value\\x12\\x39\\n\\x14repeated_int64_value\\x18\\r \\x03(\\x0b\\x32\\x1b.google.protobuf.Int64Value\\x12;\\n\\x15repeated_uint32_value\\x18\\x0e \\x03(\\x0b\\x32\\x1c.google.protobuf.UInt32Value\\x12;\\n\\x15repeated_uint64_value\\x18\\x0f \\x03(\\x0b\\x32\\x1c.google.protobuf.UInt64Value\\x12\\x39\\n\\x14repeated_float_value\\x18\\x10 \\x03(\\x0b\\x32\\x1b.google.protobuf.FloatValue\\x12;\\n\\x15repeated_double_value\\x18\\x11 \\x03(\\x0b\\x32\\x1c.google.protobuf.DoubleValue\\x12;\\n\\x15repeated_string_value\\x18\\x12 \\x03(\\x0b\\x32\\x1c.google.protobuf.StringValue\\x12\\x39\\n\\x14repeated_bytes_value\\x18\\x13 \\x03(\\x0b\\x32\\x1b.google.protobuf.BytesValue\\\"n\\n\\rTestTimestamp\\x12)\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x1a.google.protobuf.Timestamp\\x12\\x32\\n\\x0erepeated_value\\x18\\x02 \\x03(\\x0b\\x32\\x1a.google.protobuf.Timestamp\\\"k\\n\\x0cTestDuration\\x12(\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x19.google.protobuf.Duration\\x12\\x31\\n\\x0erepeated_value\\x18\\x02 \\x03(\\x0b\\x32\\x19.google.protobuf.Duration\\\":\\n\\rTestFieldMask\\x12)\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x1a.google.protobuf.FieldMask\\\"e\\n\\nTestStruct\\x12&\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x17.google.protobuf.Struct\\x12/\\n\\x0erepeated_value\\x18\\x02 \\x03(\\x0b\\x32\\x17.google.protobuf.Struct\\\"\\\\\\n\\x07TestAny\\x12#\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x14.google.protobuf.Any\\x12,\\n\\x0erepeated_value\\x18\\x02 \\x03(\\x0b\\x32\\x14.google.protobuf.Any\\\"b\\n\\tTestValue\\x12%\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x16.google.protobuf.Value\\x12.\\n\\x0erepeated_value\\x18\\x02 \\x03(\\x0b\\x32\\x16.google.protobuf.Value\\\"n\\n\\rTestListValue\\x12)\\n\\x05value\\x18\\x01 \\x01(\\x0b\\x32\\x1a.google.protobuf.ListValue\\x12\\x32\\n\\x0erepeated_value\\x18\\x02 \\x03(\\x0b\\x32\\x1a.google.protobuf.ListValue\\\"\\x89\\x01\\n\\rTestBoolValue\\x12\\x12\\n\\nbool_value\\x18\\x01 \\x01(\\x08\\x12\\x34\\n\\x08\\x62ool_map\\x18\\x02 \\x03(\\x0b\\x32\\\".proto3.TestBoolValue.BoolMapEntry\\x1a.\\n\\x0c\\x42oolMapEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\x08\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\x05:\\x02\\x38\\x01\\\"+\\n\\x12TestCustomJsonName\\x12\\x15\\n\\x05value\\x18\\x01 \\x01(\\x05R\\x06@value\\\"J\\n\\x0eTestExtensions\\x12\\x38\\n\\nextensions\\x18\\x01 \\x01(\\x0b\\x32$.protobuf_unittest.TestAllExtensions\\\"\\x84\\x01\\n\\rTestEnumValue\\x12%\\n\\x0b\\x65num_value1\\x18\\x01 \\x01(\\x0e\\x32\\x10.proto3.EnumType\\x12%\\n\\x0b\\x65num_value2\\x18\\x02 \\x01(\\x0e\\x32\\x10.proto3.EnumType\\x12%\\n\\x0b\\x65num_value3\\x18\\x03 \\x01(\\x0e\\x32\\x10.proto3.EnumType*\\x1c\\n\\x08\\x45numType\\x12\\x07\\n\\x03\\x46OO\\x10\\x00\\x12\\x07\\n\\x03\\x42\\x41R\\x10\\x01\\x42,\\n\\x18\\x63om.google.protobuf.utilB\\x10JsonFormatProto3b\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_proto3_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\030com.google.protobuf.utilB\\020JsonFormatProto3'\n  _TESTMAP_BOOLMAPENTRY._options = None\n  _TESTMAP_BOOLMAPENTRY._serialized_options = b'8\\001'\n  _TESTMAP_INT32MAPENTRY._options = None\n  _TESTMAP_INT32MAPENTRY._serialized_options = b'8\\001'\n  _TESTMAP_INT64MAPENTRY._options = None\n  _TESTMAP_INT64MAPENTRY._serialized_options = b'8\\001'\n  _TESTMAP_UINT32MAPENTRY._options = None\n  _TESTMAP_UINT32MAPENTRY._serialized_options = b'8\\001'\n  _TESTMAP_UINT64MAPENTRY._options = None\n  _TESTMAP_UINT64MAPENTRY._serialized_options = b'8\\001'\n  _TESTMAP_STRINGMAPENTRY._options = None\n  _TESTMAP_STRINGMAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_BOOLMAPENTRY._options = None\n  _TESTNESTEDMAP_BOOLMAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_INT32MAPENTRY._options = None\n  _TESTNESTEDMAP_INT32MAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_INT64MAPENTRY._options = None\n  _TESTNESTEDMAP_INT64MAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_UINT32MAPENTRY._options = None\n  _TESTNESTEDMAP_UINT32MAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_UINT64MAPENTRY._options = None\n  _TESTNESTEDMAP_UINT64MAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_STRINGMAPENTRY._options = None\n  _TESTNESTEDMAP_STRINGMAPENTRY._serialized_options = b'8\\001'\n  _TESTNESTEDMAP_MAPMAPENTRY._options = None\n  _TESTNESTEDMAP_MAPMAPENTRY._serialized_options = b'8\\001'\n  _TESTSTRINGMAP_STRINGMAPENTRY._options = None\n  _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\\001'\n  _TESTBOOLVALUE_BOOLMAPENTRY._options = None\n  _TESTBOOLVALUE_BOOLMAPENTRY._serialized_options = b'8\\001'\n  _ENUMTYPE._serialized_start=4849\n  _ENUMTYPE._serialized_end=4877\n  _MESSAGETYPE._serialized_start=277\n  _MESSAGETYPE._serialized_end=305\n  _TESTMESSAGE._serialized_start=308\n  _TESTMESSAGE._serialized_end=968\n  _TESTONEOF._serialized_start=971\n  _TESTONEOF._serialized_end=1239\n  _TESTMAP._serialized_start=1242\n  _TESTMAP._serialized_end=1851\n  _TESTMAP_BOOLMAPENTRY._serialized_start=1557\n  _TESTMAP_BOOLMAPENTRY._serialized_end=1603\n  _TESTMAP_INT32MAPENTRY._serialized_start=1605\n  _TESTMAP_INT32MAPENTRY._serialized_end=1652\n  _TESTMAP_INT64MAPENTRY._serialized_start=1654\n  _TESTMAP_INT64MAPENTRY._serialized_end=1701\n  _TESTMAP_UINT32MAPENTRY._serialized_start=1703\n  _TESTMAP_UINT32MAPENTRY._serialized_end=1751\n  _TESTMAP_UINT64MAPENTRY._serialized_start=1753\n  _TESTMAP_UINT64MAPENTRY._serialized_end=1801\n  _TESTMAP_STRINGMAPENTRY._serialized_start=1803\n  _TESTMAP_STRINGMAPENTRY._serialized_end=1851\n  _TESTNESTEDMAP._serialized_start=1854\n  _TESTNESTEDMAP._serialized_end=2627\n  _TESTNESTEDMAP_BOOLMAPENTRY._serialized_start=1557\n  _TESTNESTEDMAP_BOOLMAPENTRY._serialized_end=1603\n  _TESTNESTEDMAP_INT32MAPENTRY._serialized_start=1605\n  _TESTNESTEDMAP_INT32MAPENTRY._serialized_end=1652\n  _TESTNESTEDMAP_INT64MAPENTRY._serialized_start=1654\n  _TESTNESTEDMAP_INT64MAPENTRY._serialized_end=1701\n  _TESTNESTEDMAP_UINT32MAPENTRY._serialized_start=1703\n  _TESTNESTEDMAP_UINT32MAPENTRY._serialized_end=1751\n  _TESTNESTEDMAP_UINT64MAPENTRY._serialized_start=1753\n  _TESTNESTEDMAP_UINT64MAPENTRY._serialized_end=1801\n  _TESTNESTEDMAP_STRINGMAPENTRY._serialized_start=1803\n  _TESTNESTEDMAP_STRINGMAPENTRY._serialized_end=1851\n  _TESTNESTEDMAP_MAPMAPENTRY._serialized_start=2559\n  _TESTNESTEDMAP_MAPMAPENTRY._serialized_end=2627\n  _TESTSTRINGMAP._serialized_start=2629\n  _TESTSTRINGMAP._serialized_end=2752\n  _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=2704\n  _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=2752\n  _TESTWRAPPER._serialized_start=2755\n  _TESTWRAPPER._serialized_end=3761\n  _TESTTIMESTAMP._serialized_start=3763\n  _TESTTIMESTAMP._serialized_end=3873\n  _TESTDURATION._serialized_start=3875\n  _TESTDURATION._serialized_end=3982\n  _TESTFIELDMASK._serialized_start=3984\n  _TESTFIELDMASK._serialized_end=4042\n  _TESTSTRUCT._serialized_start=4044\n  _TESTSTRUCT._serialized_end=4145\n  _TESTANY._serialized_start=4147\n  _TESTANY._serialized_end=4239\n  _TESTVALUE._serialized_start=4241\n  _TESTVALUE._serialized_end=4339\n  _TESTLISTVALUE._serialized_start=4341\n  _TESTLISTVALUE._serialized_end=4451\n  _TESTBOOLVALUE._serialized_start=4454\n  _TESTBOOLVALUE._serialized_end=4591\n  _TESTBOOLVALUE_BOOLMAPENTRY._serialized_start=1557\n  _TESTBOOLVALUE_BOOLMAPENTRY._serialized_end=1603\n  _TESTCUSTOMJSONNAME._serialized_start=4593\n  _TESTCUSTOMJSONNAME._serialized_end=4636\n  _TESTEXTENSIONS._serialized_start=4638\n  _TESTEXTENSIONS._serialized_end=4712\n  _TESTENUMVALUE._serialized_start=4715\n  _TESTENUMVALUE._serialized_end=4847\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/nuke/vendor/google/protobuf/wrappers_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: google/protobuf/wrappers.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x1egoogle/protobuf/wrappers.proto\\x12\\x0fgoogle.protobuf\\\"\\x1c\\n\\x0b\\x44oubleValue\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x01\\\"\\x1b\\n\\nFloatValue\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x02\\\"\\x1b\\n\\nInt64Value\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x03\\\"\\x1c\\n\\x0bUInt64Value\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x04\\\"\\x1b\\n\\nInt32Value\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x05\\\"\\x1c\\n\\x0bUInt32Value\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\r\\\"\\x1a\\n\\tBoolValue\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x08\\\"\\x1c\\n\\x0bStringValue\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\t\\\"\\x1b\\n\\nBytesValue\\x12\\r\\n\\x05value\\x18\\x01 \\x01(\\x0c\\x42\\x83\\x01\\n\\x13\\x63om.google.protobufB\\rWrappersProtoP\\x01Z1google.golang.org/protobuf/types/known/wrapperspb\\xf8\\x01\\x01\\xa2\\x02\\x03GPB\\xaa\\x02\\x1eGoogle.Protobuf.WellKnownTypesb\\x06proto3')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.wrappers_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\023com.google.protobufB\\rWrappersProtoP\\001Z1google.golang.org/protobuf/types/known/wrapperspb\\370\\001\\001\\242\\002\\003GPB\\252\\002\\036Google.Protobuf.WellKnownTypes'\n  _DOUBLEVALUE._serialized_start=51\n  _DOUBLEVALUE._serialized_end=79\n  _FLOATVALUE._serialized_start=81\n  _FLOATVALUE._serialized_end=108\n  _INT64VALUE._serialized_start=110\n  _INT64VALUE._serialized_end=137\n  _UINT64VALUE._serialized_start=139\n  _UINT64VALUE._serialized_end=167\n  _INT32VALUE._serialized_start=169\n  _INT32VALUE._serialized_end=196\n  _UINT32VALUE._serialized_start=198\n  _UINT32VALUE._serialized_end=226\n  _BOOLVALUE._serialized_start=228\n  _BOOLVALUE._serialized_end=254\n  _STRINGVALUE._serialized_start=256\n  _STRINGVALUE._serialized_end=284\n  _BYTESVALUE._serialized_start=286\n  _BYTESVALUE._serialized_end=313\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "openpype/hosts/photoshop/__init__.py",
    "content": "from .addon import (\n    PhotoshopAddon,\n    PHOTOSHOP_HOST_DIR,\n)\n\n\n__all__ = (\n    \"PhotoshopAddon\",\n    \"PHOTOSHOP_HOST_DIR\",\n)\n"
  },
  {
    "path": "openpype/hosts/photoshop/addon.py",
    "content": "import os\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nPHOTOSHOP_HOST_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass PhotoshopAddon(OpenPypeModule, IHostAddon):\n    name = \"photoshop\"\n    host_name = \"photoshop\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        \"\"\"Modify environments to contain all required for implementation.\"\"\"\n        defaults = {\n            \"OPENPYPE_LOG_NO_COLORS\": \"True\",\n            \"WEBSOCKET_URL\": \"ws://localhost:8099/ws/\"\n        }\n        for key, value in defaults.items():\n            if not env.get(key):\n                env[key] = value\n\n    def get_workfile_extensions(self):\n        return [\".psd\", \".psb\"]\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/README.md",
    "content": "# Photoshop Integration\n\n## Setup\n\nThe Photoshop integration requires two components to work; `extension` and `server`.\n\n### Extension\n\nTo install the extension download [Extension Manager Command Line tool (ExManCmd)](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#option-2---exmancmd).\n\n```\nExManCmd /install {path to addon}/api/extension.zxp\n```\n\n### Server\n\nThe easiest way to get the server and Photoshop launch is with:\n\n```\npython -c ^\"import openpype.hosts.photoshop;openpype.hosts.photoshop.launch(\"\"C:\\Program Files\\Adobe\\Adobe Photoshop 2020\\Photoshop.exe\"\")^\"\n```\n\n`avalon.photoshop.launch` launches the application and server, and also closes the server when Photoshop exists.\n\n## Usage\n\nThe Photoshop extension can be found under `Window > Extensions > Ayon`. Once launched you should be presented with a panel like this:\n\n![Ayon Panel](panel.png \"AYON Panel\")\n\n\n## Developing\n\n### Extension\nWhen developing the extension you can load it [unsigned](https://github.com/Adobe-CEP/CEP-Resources/blob/master/CEP_9.x/Documentation/CEP%209.0%20HTML%20Extension%20Cookbook.md#debugging-unsigned-extensions).\n\nWhen signing the extension you can use this [guide](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#package-distribute-install-guide).\n\n```\nZXPSignCmd -selfSignedCert NA NA Ayon Ayon-Photoshop Ayon extension.p12\nZXPSignCmd -sign {path to avalon-core}\\avalon\\photoshop\\extension {path to avalon-core}\\avalon\\photoshop\\extension.zxp extension.p12 avalon\n```\n\n### Plugin Examples\n\nThese plugins were made with the [polly config](https://github.com/mindbender-studio/config). To fully integrate and load, you will have to use this config and add `image` to the [integration plugin](https://github.com/mindbender-studio/config/blob/master/polly/plugins/publish/integrate_asset.py).\n\n#### Creator Plugin\n```python\nfrom avalon import photoshop\n\n\nclass CreateImage(photoshop.Creator):\n    \"\"\"Image folder for publish.\"\"\"\n\n    name = \"imageDefault\"\n    label = \"Image\"\n    family = \"image\"\n\n    def __init__(self, *args, **kwargs):\n        super(CreateImage, self).__init__(*args, **kwargs)\n```\n\n#### Collector Plugin\n```python\nimport pythoncom\n\nimport pyblish.api\n\n\nclass CollectInstances(pyblish.api.ContextPlugin):\n    \"\"\"Gather instances by LayerSet and file metadata\n\n    This collector takes into account assets that are associated with\n    an LayerSet and marked with a unique identifier;\n\n    Identifier:\n        id (str): \"pyblish.avalon.instance\"\n    \"\"\"\n\n    label = \"Instances\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"photoshop\"]\n    families_mapping = {\n        \"image\": []\n    }\n\n    def process(self, context):\n        # Necessary call when running in a different thread which pyblish-qml\n        # can be.\n        pythoncom.CoInitialize()\n\n        photoshop_client = PhotoshopClientStub()\n        layers = photoshop_client.get_layers()\n        layers_meta = photoshop_client.get_layers_metadata()\n        for layer in layers:\n            layer_data = photoshop_client.read(layer, layers_meta)\n\n            # Skip layers without metadata.\n            if layer_data is None:\n                continue\n\n            # Skip containers.\n            if \"container\" in layer_data[\"id\"]:\n                continue\n\n            # child_layers = [*layer.Layers]\n            # self.log.debug(\"child_layers {}\".format(child_layers))\n            # if not child_layers:\n            #     self.log.info(\"%s skipped, it was empty.\" % layer.Name)\n            #     continue\n\n            instance = context.create_instance(layer.name)\n            instance.append(layer)\n            instance.data.update(layer_data)\n            instance.data[\"families\"] = self.families_mapping[\n                layer_data[\"family\"]\n            ]\n            instance.data[\"publish\"] = layer.visible\n\n            # Produce diagnostic message for any graphical\n            # user interface interested in visualising it.\n            self.log.info(\"Found: \\\"%s\\\" \" % instance.data[\"name\"])\n```\n\n#### Extractor Plugin\n```python\nimport os\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.photoshop import api as photoshop\n\n\nclass ExtractImage(publish.Extractor):\n    \"\"\"Produce a flattened image file from instance\n\n    This plug-in takes into account only the layers in the group.\n    \"\"\"\n\n    label = \"Extract Image\"\n    hosts = [\"photoshop\"]\n    families = [\"image\"]\n    formats = [\"png\", \"jpg\"]\n\n    def process(self, instance):\n\n        staging_dir = self.staging_dir(instance)\n        self.log.info(\"Outputting image to {}\".format(staging_dir))\n\n        # Perform extraction\n        stub = photoshop.stub()\n        files = {}\n        with photoshop.maintained_selection():\n            self.log.info(\"Extracting %s\" % str(list(instance)))\n            with photoshop.maintained_visibility():\n                # Hide all other layers.\n                extract_ids = set([ll.id for ll in stub.\n                                   get_layers_in_layers([instance[0]])])\n\n                for layer in stub.get_layers():\n                    # limit unnecessary calls to client\n                    if layer.visible and layer.id not in extract_ids:\n                        stub.set_visible(layer.id, False)\n\n                save_options = []\n                if \"png\" in self.formats:\n                    save_options.append('png')\n                if \"jpg\" in self.formats:\n                    save_options.append('jpg')\n\n                file_basename = os.path.splitext(\n                    stub.get_active_document_name()\n                )[0]\n                for extension in save_options:\n                    _filename = \"{}.{}\".format(file_basename, extension)\n                    files[extension] = _filename\n\n                    full_filename = os.path.join(staging_dir, _filename)\n                    stub.saveAs(full_filename, extension, True)\n\n        representations = []\n        for extension, filename in files.items():\n            representations.append({\n                \"name\": extension,\n                \"ext\": extension,\n                \"files\": filename,\n                \"stagingDir\": staging_dir\n            })\n        instance.data[\"representations\"] = representations\n        instance.data[\"stagingDir\"] = staging_dir\n\n        self.log.info(f\"Extracted {instance} to {staging_dir}\")\n```\n\n#### Loader Plugin\n```python\nfrom avalon import api, photoshop\nfrom openpype.pipeline import load, get_representation_path\n\nstub = photoshop.stub()\n\n\nclass ImageLoader(load.LoaderPlugin):\n    \"\"\"Load images\n\n    Stores the imported asset in a container named after the asset.\n    \"\"\"\n\n    families = [\"image\"]\n    representations = [\"*\"]\n\n    def load(self, context, name=None, namespace=None, data=None):\n        path = self.filepath_from_context(context)\n        with photoshop.maintained_selection():\n            layer = stub.import_smart_object(path)\n\n        self[:] = [layer]\n\n        return photoshop.containerise(\n            name,\n            namespace,\n            layer,\n            context,\n            self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n        layer = container.pop(\"layer\")\n\n        with photoshop.maintained_selection():\n            stub.replace_smart_object(\n                layer, get_representation_path(representation)\n            )\n\n        stub.imprint(\n            layer, {\"representation\": str(representation[\"_id\"])}\n        )\n\n    def remove(self, container):\n        container[\"layer\"].Delete()\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n```\nFor easier debugging of Javascript:\nhttps://community.adobe.com/t5/download-install/adobe-extension-debuger-problem/td-p/10911704?page=1\nAdd --enable-blink-features=ShadowDOMV0,CustomElementsV0 when starting Chrome\nthen localhost:8078 (port set in `photoshop\\extension\\.debug`)\n\nOr use Visual Studio Code https://medium.com/adobetech/extendscript-debugger-for-visual-studio-code-public-release-a2ff6161fa01\n\nOr install CEF client from https://github.com/Adobe-CEP/CEP-Resources/tree/master/CEP_9.x\n## Resources\n  - https://github.com/lohriialo/photoshop-scripting-python\n  - https://www.adobe.com/devnet/photoshop/scripting.html\n  - https://github.com/Adobe-CEP/Getting-Started-guides\n  - https://github.com/Adobe-CEP/CEP-Resources\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/__init__.py",
    "content": "\"\"\"Public API\n\nAnything that isn't defined here is INTERNAL and unreliable for external use.\n\n\"\"\"\n\nfrom .launch_logic import stub\n\nfrom .pipeline import (\n    PhotoshopHost,\n    ls,\n    containerise\n)\nfrom .plugin import (\n    PhotoshopLoader,\n    get_unique_layer_name\n)\n\n\nfrom .lib import (\n    maintained_selection,\n    maintained_visibility\n)\n\n__all__ = [\n    # launch_logic\n    \"stub\",\n\n    # pipeline\n    \"PhotoshopHost\",\n    \"ls\",\n    \"containerise\",\n\n    # Plugin\n    \"PhotoshopLoader\",\n    \"get_unique_layer_name\",\n\n    # lib\n    \"maintained_selection\",\n    \"maintained_visibility\",\n]\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/extension/.debug",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ExtensionList>\n    <Extension Id=\"io.ynput.PS.panel\">\n        <HostList>\n            <Host Name=\"PHXS\" Port=\"8078\"/>\n            <Host Name=\"FLPR\" Port=\"8078\"/>\n        </HostList>\n    </Extension>\n</ExtensionList>\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/extension/CSXS/manifest.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<ExtensionManifest ExtensionBundleId=\"io.ynput.PS.panel\" ExtensionBundleVersion=\"1.1.0\" Version=\"7.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n  <ExtensionList>\n    <Extension Id=\"io.ynput.PS.panel\" Version=\"1.0.1\" />\n  </ExtensionList>\n  <ExecutionEnvironment>\n    <HostList>\n      <Host Name=\"PHSP\" Version=\"19\" />\n      <Host Name=\"PHXS\" Version=\"19\" />\n    </HostList>\n    <LocaleList>\n      <Locale Code=\"All\" />\n    </LocaleList>\n    <RequiredRuntimeList>\n      <RequiredRuntime Name=\"CSXS\" Version=\"7.0\" />\n    </RequiredRuntimeList>\n  </ExecutionEnvironment>\n  <DispatchInfoList>\n    <Extension Id=\"io.ynput.PS.panel\">\n      <DispatchInfo>\n        <Resources>\n          <MainPath>./index.html</MainPath>\n          <CEFCommandLine />\n        </Resources>\n        <Lifecycle>\n\t\t  <AutoVisible>true</AutoVisible>\n          <StartOn>\n            <!-- Photoshop dispatches this event on startup -->\n            <Event>applicationActivate</Event>\n            <Event>com.adobe.csxs.events.ApplicationInitialized</Event>\n          </StartOn>\n\t\t</Lifecycle>\n        <UI>\n          <Type>Panel</Type>\n          <Menu>AYON</Menu>\n          <Geometry>\n            <Size>\n              <Width>300</Width>\n              <Height>140</Height>\n            </Size>\n            <MaxSize>\n              <Width>400</Width>\n              <Height>200</Height>\n            </MaxSize>\n          </Geometry>\n          <Icons>\n            <Icon Type=\"Normal\">./icons/ayon_logo.png</Icon>\n          </Icons>\n        </UI>\n      </DispatchInfo>\n    </Extension>\n  </DispatchInfoList>\n</ExtensionManifest>\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/extension/client/CSInterface.js",
    "content": "/**************************************************************************************************\n*\n* ADOBE SYSTEMS INCORPORATED\n* Copyright 2013 Adobe Systems Incorporated\n* All Rights Reserved.\n*\n* NOTICE:  Adobe permits you to use, modify, and distribute this file in accordance with the\n* terms of the Adobe license agreement accompanying it.  If you have received this file from a\n* source other than Adobe, then your use, modification, or distribution of it requires the prior\n* written permission of Adobe.\n*\n**************************************************************************************************/\n\n/** CSInterface - v8.0.0 */\n\n/**\n * Stores constants for the window types supported by the CSXS infrastructure.\n */\nfunction CSXSWindowType()\n{\n}\n\n/** Constant for the CSXS window type Panel. */\nCSXSWindowType._PANEL = \"Panel\";\n\n/** Constant for the CSXS window type Modeless. */\nCSXSWindowType._MODELESS = \"Modeless\";\n\n/** Constant for the CSXS window type ModalDialog. */\nCSXSWindowType._MODAL_DIALOG = \"ModalDialog\";\n\n/** EvalScript error message */\nEvalScript_ErrMessage = \"EvalScript error.\";\n\n/**\n * @class Version\n * Defines a version number with major, minor, micro, and special\n * components. The major, minor and micro values are numeric; the special\n * value can be any string.\n *\n * @param major   The major version component, a positive integer up to nine digits long.\n * @param minor   The minor version component, a positive integer up to nine digits long.\n * @param micro   The micro version component, a positive integer up to nine digits long.\n * @param special The special version component, an arbitrary string.\n *\n * @return A new \\c Version object.\n */\nfunction Version(major, minor, micro, special)\n{\n    this.major = major;\n    this.minor = minor;\n    this.micro = micro;\n    this.special = special;\n}\n\n/**\n * The maximum value allowed for a numeric version component.\n * This reflects the maximum value allowed in PlugPlug and the manifest schema.\n */\nVersion.MAX_NUM = 999999999;\n\n/**\n * @class VersionBound\n * Defines a boundary for a version range, which associates a \\c Version object\n * with a flag for whether it is an inclusive or exclusive boundary.\n *\n * @param version   The \\c #Version object.\n * @param inclusive True if this boundary is inclusive, false if it is exclusive.\n *\n * @return A new \\c VersionBound object.\n */\nfunction VersionBound(version, inclusive)\n{\n    this.version = version;\n    this.inclusive = inclusive;\n}\n\n/**\n * @class VersionRange\n * Defines a range of versions using a lower boundary and optional upper boundary.\n *\n * @param lowerBound The \\c #VersionBound object.\n * @param upperBound The \\c #VersionBound object, or null for a range with no upper boundary.\n *\n * @return A new \\c VersionRange object.\n */\nfunction VersionRange(lowerBound, upperBound)\n{\n    this.lowerBound = lowerBound;\n    this.upperBound = upperBound;\n}\n\n/**\n * @class Runtime\n * Represents a runtime related to the CEP infrastructure.\n * Extensions can declare dependencies on particular\n * CEP runtime versions in the extension manifest.\n *\n * @param name    The runtime name.\n * @param version A \\c #VersionRange object that defines a range of valid versions.\n *\n * @return A new \\c Runtime object.\n */\nfunction Runtime(name, versionRange)\n{\n    this.name = name;\n    this.versionRange = versionRange;\n}\n\n/**\n* @class Extension\n* Encapsulates a CEP-based extension to an Adobe application.\n*\n* @param id              The unique identifier of this extension.\n* @param name            The localizable display name of this extension.\n* @param mainPath        The path of the \"index.html\" file.\n* @param basePath        The base path of this extension.\n* @param windowType          The window type of the main window of this extension.\n                 Valid values are defined by \\c #CSXSWindowType.\n* @param width           The default width in pixels of the main window of this extension.\n* @param height          The default height in pixels of the main window of this extension.\n* @param minWidth        The minimum width in pixels of the main window of this extension.\n* @param minHeight       The minimum height in pixels of the main window of this extension.\n* @param maxWidth        The maximum width in pixels of the main window of this extension.\n* @param maxHeight       The maximum height in pixels of the main window of this extension.\n* @param defaultExtensionDataXml The extension data contained in the default \\c ExtensionDispatchInfo section of the extension manifest.\n* @param specialExtensionDataXml The extension data contained in the application-specific \\c ExtensionDispatchInfo section of the extension manifest.\n* @param requiredRuntimeList     An array of \\c Runtime objects for runtimes required by this extension.\n* @param isAutoVisible       True if this extension is visible on loading.\n* @param isPluginExtension   True if this extension has been deployed in the Plugins folder of the host application.\n*\n* @return A new \\c Extension object.\n*/\nfunction Extension(id, name, mainPath, basePath, windowType, width, height, minWidth, minHeight, maxWidth, maxHeight,\n                   defaultExtensionDataXml, specialExtensionDataXml, requiredRuntimeList, isAutoVisible, isPluginExtension)\n{\n    this.id = id;\n    this.name = name;\n    this.mainPath = mainPath;\n    this.basePath = basePath;\n    this.windowType = windowType;\n    this.width = width;\n    this.height = height;\n    this.minWidth = minWidth;\n    this.minHeight = minHeight;\n    this.maxWidth = maxWidth;\n    this.maxHeight = maxHeight;\n    this.defaultExtensionDataXml = defaultExtensionDataXml;\n    this.specialExtensionDataXml = specialExtensionDataXml;\n    this.requiredRuntimeList = requiredRuntimeList;\n    this.isAutoVisible = isAutoVisible;\n    this.isPluginExtension = isPluginExtension;\n}\n\n/**\n * @class CSEvent\n * A standard JavaScript event, the base class for CEP events.\n *\n * @param type        The name of the event type.\n * @param scope       The scope of event, can be \"GLOBAL\" or \"APPLICATION\".\n * @param appId       The unique identifier of the application that generated the event.\n * @param extensionId     The unique identifier of the extension that generated the event.\n *\n * @return A new \\c CSEvent object\n */\nfunction CSEvent(type, scope, appId, extensionId)\n{\n    this.type = type;\n    this.scope = scope;\n    this.appId = appId;\n    this.extensionId = extensionId;\n}\n\n/** Event-specific data. */\nCSEvent.prototype.data = \"\";\n\n/**\n * @class SystemPath\n * Stores operating-system-specific location constants for use in the\n * \\c #CSInterface.getSystemPath() method.\n * @return A new \\c SystemPath object.\n */\nfunction SystemPath()\n{\n}\n\n/** The path to user data.  */\nSystemPath.USER_DATA = \"userData\";\n\n/** The path to common files for Adobe applications.  */\nSystemPath.COMMON_FILES = \"commonFiles\";\n\n/** The path to the user's default document folder.  */\nSystemPath.MY_DOCUMENTS = \"myDocuments\";\n\n/** @deprecated. Use \\c #SystemPath.Extension.  */\nSystemPath.APPLICATION = \"application\";\n\n/** The path to current extension.  */\nSystemPath.EXTENSION = \"extension\";\n\n/** The path to hosting application's executable.  */\nSystemPath.HOST_APPLICATION = \"hostApplication\";\n\n/**\n * @class ColorType\n * Stores color-type constants.\n */\nfunction ColorType()\n{\n}\n\n/** RGB color type. */\nColorType.RGB = \"rgb\";\n\n/** Gradient color type. */\nColorType.GRADIENT = \"gradient\";\n\n/** Null color type. */\nColorType.NONE = \"none\";\n\n/**\n * @class RGBColor\n * Stores an RGB color with red, green, blue, and alpha values.\n * All values are in the range [0.0 to 255.0]. Invalid numeric values are\n * converted to numbers within this range.\n *\n * @param red   The red value, in the range [0.0 to 255.0].\n * @param green The green value, in the range [0.0 to 255.0].\n * @param blue  The blue value, in the range [0.0 to 255.0].\n * @param alpha The alpha (transparency) value, in the range [0.0 to 255.0].\n *      The default, 255.0, means that the color is fully opaque.\n *\n * @return A new RGBColor object.\n */\nfunction RGBColor(red, green, blue, alpha)\n{\n    this.red = red;\n    this.green = green;\n    this.blue = blue;\n    this.alpha = alpha;\n}\n\n/**\n * @class Direction\n * A point value  in which the y component is 0 and the x component\n * is positive or negative for a right or left direction,\n * or the x component is 0 and the y component is positive or negative for\n * an up or down direction.\n *\n * @param x     The horizontal component of the point.\n * @param y     The vertical component of the point.\n *\n * @return A new \\c Direction object.\n */\nfunction Direction(x, y)\n{\n    this.x = x;\n    this.y = y;\n}\n\n/**\n * @class GradientStop\n * Stores gradient stop information.\n *\n * @param offset   The offset of the gradient stop, in the range [0.0 to 1.0].\n * @param rgbColor The color of the gradient at this point, an \\c #RGBColor object.\n *\n * @return GradientStop object.\n */\nfunction GradientStop(offset, rgbColor)\n{\n    this.offset = offset;\n    this.rgbColor = rgbColor;\n}\n\n/**\n * @class GradientColor\n * Stores gradient color information.\n *\n * @param type          The gradient type, must be \"linear\".\n * @param direction     A \\c #Direction object for the direction of the gradient\n                (up, down, right, or left).\n * @param numStops          The number of stops in the gradient.\n * @param gradientStopList  An array of \\c #GradientStop objects.\n *\n * @return A new \\c GradientColor object.\n */\nfunction GradientColor(type, direction, numStops, arrGradientStop)\n{\n    this.type = type;\n    this.direction = direction;\n    this.numStops = numStops;\n    this.arrGradientStop = arrGradientStop;\n}\n\n/**\n * @class UIColor\n * Stores color information, including the type, anti-alias level, and specific color\n * values in a color object of an appropriate type.\n *\n * @param type          The color type, 1 for \"rgb\" and 2 for \"gradient\".\n                The supplied color object must correspond to this type.\n * @param antialiasLevel    The anti-alias level constant.\n * @param color         A \\c #RGBColor or \\c #GradientColor object containing specific color information.\n *\n * @return A new \\c UIColor object.\n */\nfunction UIColor(type, antialiasLevel, color)\n{\n    this.type = type;\n    this.antialiasLevel = antialiasLevel;\n    this.color = color;\n}\n\n/**\n * @class AppSkinInfo\n * Stores window-skin properties, such as color and font. All color parameter values are \\c #UIColor objects except that systemHighlightColor is \\c #RGBColor object.\n *\n * @param baseFontFamily        The base font family of the application.\n * @param baseFontSize          The base font size of the application.\n * @param appBarBackgroundColor     The application bar background color.\n * @param panelBackgroundColor      The background color of the extension panel.\n * @param appBarBackgroundColorSRGB     The application bar background color, as sRGB.\n * @param panelBackgroundColorSRGB      The background color of the extension panel, as sRGB.\n * @param systemHighlightColor          The highlight color of the extension panel, if provided by the host application. Otherwise, the operating-system highlight color. \n *\n * @return AppSkinInfo object.\n */\nfunction AppSkinInfo(baseFontFamily, baseFontSize, appBarBackgroundColor, panelBackgroundColor, appBarBackgroundColorSRGB, panelBackgroundColorSRGB, systemHighlightColor)\n{\n    this.baseFontFamily = baseFontFamily;\n    this.baseFontSize = baseFontSize;\n    this.appBarBackgroundColor = appBarBackgroundColor;\n    this.panelBackgroundColor = panelBackgroundColor;\n    this.appBarBackgroundColorSRGB = appBarBackgroundColorSRGB;\n    this.panelBackgroundColorSRGB = panelBackgroundColorSRGB;\n    this.systemHighlightColor = systemHighlightColor;\n}\n\n/**\n * @class HostEnvironment\n * Stores information about the environment in which the extension is loaded.\n *\n * @param appName   The application's name.\n * @param appVersion    The application's version.\n * @param appLocale The application's current license locale.\n * @param appUILocale   The application's current UI locale.\n * @param appId     The application's unique identifier.\n * @param isAppOnline  True if the application is currently online.\n * @param appSkinInfo   An \\c #AppSkinInfo object containing the application's default color and font styles.\n *\n * @return A new \\c HostEnvironment object.\n */\nfunction HostEnvironment(appName, appVersion, appLocale, appUILocale, appId, isAppOnline, appSkinInfo)\n{\n    this.appName = appName;\n    this.appVersion = appVersion;\n    this.appLocale = appLocale;\n    this.appUILocale = appUILocale;\n    this.appId = appId;\n    this.isAppOnline = isAppOnline;\n    this.appSkinInfo = appSkinInfo;\n}\n\n/**\n * @class HostCapabilities\n * Stores information about the host capabilities.\n *\n * @param EXTENDED_PANEL_MENU True if the application supports panel menu.\n * @param EXTENDED_PANEL_ICONS True if the application supports panel icon.\n * @param DELEGATE_APE_ENGINE True if the application supports delegated APE engine.\n * @param SUPPORT_HTML_EXTENSIONS True if the application supports HTML extensions.\n * @param DISABLE_FLASH_EXTENSIONS True if the application disables FLASH extensions.\n *\n * @return A new \\c HostCapabilities object.\n */\nfunction HostCapabilities(EXTENDED_PANEL_MENU, EXTENDED_PANEL_ICONS, DELEGATE_APE_ENGINE, SUPPORT_HTML_EXTENSIONS, DISABLE_FLASH_EXTENSIONS)\n{\n    this.EXTENDED_PANEL_MENU = EXTENDED_PANEL_MENU;\n    this.EXTENDED_PANEL_ICONS = EXTENDED_PANEL_ICONS;\n    this.DELEGATE_APE_ENGINE = DELEGATE_APE_ENGINE;\n    this.SUPPORT_HTML_EXTENSIONS = SUPPORT_HTML_EXTENSIONS;\n\tthis.DISABLE_FLASH_EXTENSIONS = DISABLE_FLASH_EXTENSIONS; // Since 5.0.0\n}\n\n/**\n * @class ApiVersion\n * Stores current api version.\n *\n * Since 4.2.0\n *\n * @param major  The major version\n * @param minor  The minor version.\n * @param micro  The micro version.\n *\n * @return ApiVersion object.\n */\nfunction ApiVersion(major, minor, micro)\n{\n    this.major = major;\n    this.minor = minor;\n    this.micro = micro;\n}\n\n/**\n * @class MenuItemStatus\n * Stores flyout menu item status\n *\n * Since 5.2.0\n *\n * @param menuItemLabel  The menu item label.\n * @param enabled  \t\t True if user wants to enable the menu item.\n * @param checked  \t\t True if user wants to check the menu item.\n *\n * @return MenuItemStatus object.\n */\nfunction MenuItemStatus(menuItemLabel, enabled, checked)\n{\n\tthis.menuItemLabel = menuItemLabel;\n\tthis.enabled = enabled;\n\tthis.checked = checked;\n}\n\n/**\n * @class ContextMenuItemStatus\n * Stores the status of the context menu item.\n *\n * Since 5.2.0\n *\n * @param menuItemID     The menu item id.\n * @param enabled  \t\t True if user wants to enable the menu item.\n * @param checked  \t\t True if user wants to check the menu item.\n *\n * @return MenuItemStatus object.\n */\nfunction ContextMenuItemStatus(menuItemID, enabled, checked)\n{\n\tthis.menuItemID = menuItemID;\n\tthis.enabled = enabled;\n\tthis.checked = checked;\n}\n//------------------------------ CSInterface ----------------------------------\n\n/**\n * @class CSInterface\n * This is the entry point to the CEP extensibility infrastructure.\n * Instantiate this object and use it to:\n * <ul>\n * <li>Access information about the host application in which an extension is running</li>\n * <li>Launch an extension</li>\n * <li>Register interest in event notifications, and dispatch events</li>\n * </ul>\n *\n * @return A new \\c CSInterface object\n */\nfunction CSInterface()\n{\n}\n\n/**\n * User can add this event listener to handle native application theme color changes.\n * Callback function gives extensions ability to fine-tune their theme color after the\n * global theme color has been changed.\n * The callback function should be like below:\n *\n * @example\n * // event is a CSEvent object, but user can ignore it.\n * function OnAppThemeColorChanged(event)\n * {\n *    // Should get a latest HostEnvironment object from application.\n *    var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo;\n *    // Gets the style information such as color info from the skinInfo,\n *    // and redraw all UI controls of your extension according to the style info.\n * }\n */\nCSInterface.THEME_COLOR_CHANGED_EVENT = \"com.adobe.csxs.events.ThemeColorChanged\";\n\n/** The host environment data object. */\nCSInterface.prototype.hostEnvironment = window.__adobe_cep__ ? JSON.parse(window.__adobe_cep__.getHostEnvironment()) : null;\n\n/** Retrieves information about the host environment in which the\n *  extension is currently running.\n *\n *   @return A \\c #HostEnvironment object.\n */\nCSInterface.prototype.getHostEnvironment = function()\n{\n    this.hostEnvironment = JSON.parse(window.__adobe_cep__.getHostEnvironment());\n    return this.hostEnvironment;\n};\n\n/** Closes this extension. */\nCSInterface.prototype.closeExtension = function()\n{\n    window.__adobe_cep__.closeExtension();\n};\n\n/**\n * Retrieves a path for which a constant is defined in the system.\n *\n * @param pathType The path-type constant defined in \\c #SystemPath ,\n *\n * @return The platform-specific system path string.\n */\nCSInterface.prototype.getSystemPath = function(pathType)\n{\n    var path = decodeURI(window.__adobe_cep__.getSystemPath(pathType));\n    var OSVersion = this.getOSInformation();\n    if (OSVersion.indexOf(\"Windows\") >= 0)\n    {\n      path = path.replace(\"file:///\", \"\");\n    }\n    else if (OSVersion.indexOf(\"Mac\") >= 0)\n    {\n      path = path.replace(\"file://\", \"\");\n    }\n    return path;\n};\n\n/**\n * Evaluates a JavaScript script, which can use the JavaScript DOM\n * of the host application.\n *\n * @param script    The JavaScript script.\n * @param callback  Optional. A callback function that receives the result of execution.\n *          If execution fails, the callback function receives the error message \\c EvalScript_ErrMessage.\n */\nCSInterface.prototype.evalScript = function(script, callback)\n{\n    if(callback === null || callback === undefined)\n    {\n        callback = function(result){};\n    }\n    window.__adobe_cep__.evalScript(script, callback);\n};\n\n/**\n * Retrieves the unique identifier of the application.\n * in which the extension is currently running.\n *\n * @return The unique ID string.\n */\nCSInterface.prototype.getApplicationID = function()\n{\n    var appId = this.hostEnvironment.appId;\n    return appId;\n};\n\n/**\n * Retrieves host capability information for the application\n * in which the extension is currently running.\n *\n * @return A \\c #HostCapabilities object.\n */\nCSInterface.prototype.getHostCapabilities = function()\n{\n    var hostCapabilities = JSON.parse(window.__adobe_cep__.getHostCapabilities() );\n    return hostCapabilities;\n};\n\n/**\n * Triggers a CEP event programmatically. Yoy can use it to dispatch\n * an event of a predefined type, or of a type you have defined.\n *\n * @param event A \\c CSEvent object.\n */\nCSInterface.prototype.dispatchEvent = function(event)\n{\n    if (typeof event.data == \"object\")\n    {\n        event.data = JSON.stringify(event.data);\n    }\n\n    window.__adobe_cep__.dispatchEvent(event);\n};\n\n/**\n * Registers an interest in a CEP event of a particular type, and\n * assigns an event handler.\n * The event infrastructure notifies your extension when events of this type occur,\n * passing the event object to the registered handler function.\n *\n * @param type     The name of the event type of interest.\n * @param listener The JavaScript handler function or method.\n * @param obj      Optional, the object containing the handler method, if any.\n *         Default is null.\n */\nCSInterface.prototype.addEventListener = function(type, listener, obj)\n{\n    window.__adobe_cep__.addEventListener(type, listener, obj);\n};\n\n/**\n * Removes a registered event listener.\n *\n * @param type      The name of the event type of interest.\n * @param listener  The JavaScript handler function or method that was registered.\n * @param obj       Optional, the object containing the handler method, if any.\n *          Default is null.\n */\nCSInterface.prototype.removeEventListener = function(type, listener, obj)\n{\n    window.__adobe_cep__.removeEventListener(type, listener, obj);\n};\n\n/**\n * Loads and launches another extension, or activates the extension if it is already loaded.\n *\n * @param extensionId       The extension's unique identifier.\n * @param startupParams     Not currently used, pass \"\".\n *\n * @example\n * To launch the extension \"help\" with ID \"HLP\" from this extension, call:\n * <code>requestOpenExtension(\"HLP\", \"\"); </code>\n *\n */\nCSInterface.prototype.requestOpenExtension = function(extensionId, params)\n{\n    window.__adobe_cep__.requestOpenExtension(extensionId, params);\n};\n\n/**\n * Retrieves the list of extensions currently loaded in the current host application.\n * The extension list is initialized once, and remains the same during the lifetime\n * of the CEP session.\n *\n * @param extensionIds  Optional, an array of unique identifiers for extensions of interest.\n *          If omitted, retrieves data for all extensions.\n *\n * @return Zero or more \\c #Extension objects.\n */\nCSInterface.prototype.getExtensions = function(extensionIds)\n{\n    var extensionIdsStr = JSON.stringify(extensionIds);\n    var extensionsStr = window.__adobe_cep__.getExtensions(extensionIdsStr);\n\n    var extensions = JSON.parse(extensionsStr);\n    return extensions;\n};\n\n/**\n * Retrieves network-related preferences.\n *\n * @return A JavaScript object containing network preferences.\n */\nCSInterface.prototype.getNetworkPreferences = function()\n{\n    var result = window.__adobe_cep__.getNetworkPreferences();\n    var networkPre = JSON.parse(result);\n\n    return networkPre;\n};\n\n/**\n * Initializes the resource bundle for this extension with property values\n * for the current application and locale.\n * To support multiple locales, you must define a property file for each locale,\n * containing keyed display-string values for that locale.\n * See localization documentation for Extension Builder and related products.\n *\n * Keys can be in the\n * form <code>key.value=\"localized string\"</code>, for use in HTML text elements.\n * For example, in this input element, the localized \\c key.value string is displayed\n * instead of the empty \\c value string:\n *\n * <code><input type=\"submit\" value=\"\" data-locale=\"key\"/></code>\n *\n * @return An object containing the resource bundle information.\n */\nCSInterface.prototype.initResourceBundle = function()\n{\n    var resourceBundle = JSON.parse(window.__adobe_cep__.initResourceBundle());\n    var resElms = document.querySelectorAll('[data-locale]');\n    for (var n = 0; n < resElms.length; n++)\n    {\n       var resEl = resElms[n];\n       // Get the resource key from the element.\n       var resKey = resEl.getAttribute('data-locale');\n       if (resKey)\n       {\n           // Get all the resources that start with the key.\n           for (var key in resourceBundle)\n           {\n               if (key.indexOf(resKey) === 0)\n               {\n                   var resValue = resourceBundle[key];\n                   if (key.length == resKey.length)\n                   {\n                        resEl.innerHTML = resValue;\n                   }\n                   else if ('.' == key.charAt(resKey.length))\n                   {\n                        var attrKey = key.substring(resKey.length + 1);\n                        resEl[attrKey] = resValue;\n                   }\n               }\n           }\n       }\n    }\n    return resourceBundle;\n};\n\n/**\n * Writes installation information to a file.\n *\n * @return The file path.\n */\nCSInterface.prototype.dumpInstallationInfo = function()\n{\n    return window.__adobe_cep__.dumpInstallationInfo();\n};\n\n/**\n * Retrieves version information for the current Operating System,\n * See http://www.useragentstring.com/pages/Chrome/ for Chrome \\c navigator.userAgent values.\n *\n * @return A string containing the OS version, or \"unknown Operation System\".\n * If user customizes the User Agent by setting CEF command parameter \"--user-agent\", only\n * \"Mac OS X\" or \"Windows\" will be returned. \n */\nCSInterface.prototype.getOSInformation = function()\n{\n    var userAgent = navigator.userAgent;\n\n    if ((navigator.platform == \"Win32\") || (navigator.platform == \"Windows\"))\n    {\n        var winVersion = \"Windows\";\n        var winBit = \"\";\n        if (userAgent.indexOf(\"Windows\") > -1)\n        {\n            if (userAgent.indexOf(\"Windows NT 5.0\") > -1)\n            {\n                winVersion = \"Windows 2000\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 5.1\") > -1)\n            {\n                winVersion = \"Windows XP\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 5.2\") > -1)\n            {\n                winVersion = \"Windows Server 2003\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 6.0\") > -1)\n            {\n                winVersion = \"Windows Vista\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 6.1\") > -1)\n            {\n                winVersion = \"Windows 7\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 6.2\") > -1)\n            {\n                winVersion = \"Windows 8\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 6.3\") > -1)\n            {\n                winVersion = \"Windows 8.1\";\n            }\n            else if (userAgent.indexOf(\"Windows NT 10\") > -1)\n            {\n                winVersion = \"Windows 10\";\n            }\n\n            if (userAgent.indexOf(\"WOW64\") > -1 || userAgent.indexOf(\"Win64\") > -1)\n            {\n                winBit = \" 64-bit\";\n            }\n            else\n            {\n                winBit = \" 32-bit\";\t\t\t\n            }\n        }\n\n        return winVersion + winBit;\n    }\n    else if ((navigator.platform == \"MacIntel\") || (navigator.platform == \"Macintosh\"))\n    {        \n        var result = \"Mac OS X\";\n\n        if (userAgent.indexOf(\"Mac OS X\") > -1)\n        {\n            result = userAgent.substring(userAgent.indexOf(\"Mac OS X\"), userAgent.indexOf(\")\"));\n            result = result.replace(/_/g, \".\");\n        }\n\n        return result;        \n    }\n\n    return \"Unknown Operation System\";\n};\n\n/**\n * Opens a page in the default system browser.\n *\n * Since 4.2.0\n *\n * @param url  The URL of the page/file to open, or the email address.\n * Must use HTTP/HTTPS/file/mailto protocol. For example:\n *   \"http://www.adobe.com\"\n *   \"https://github.com\"\n *   \"file:///C:/log.txt\"\n *   \"mailto:test@adobe.com\"\n *\n * @return One of these error codes:\\n\n *      <ul>\\n\n *          <li>NO_ERROR - 0</li>\\n\n *          <li>ERR_UNKNOWN - 1</li>\\n\n *          <li>ERR_INVALID_PARAMS - 2</li>\\n\n *          <li>ERR_INVALID_URL - 201</li>\\n\n *      </ul>\\n\n */\nCSInterface.prototype.openURLInDefaultBrowser = function(url)\n{\n    return cep.util.openURLInDefaultBrowser(url);\n};\n\n/**\n * Retrieves extension ID.\n *\n * Since 4.2.0\n *\n * @return extension ID.\n */\nCSInterface.prototype.getExtensionID = function()\n{\n     return window.__adobe_cep__.getExtensionId();\n};\n\n/**\n * Retrieves the scale factor of screen. \n * On Windows platform, the value of scale factor might be different from operating system's scale factor,\n * since host application may use its self-defined scale factor.\n *\n * Since 4.2.0\n *\n * @return One of the following float number.\n *      <ul>\\n\n *          <li> -1.0 when error occurs </li>\\n\n *          <li> 1.0 means normal screen </li>\\n\n *          <li> >1.0 means HiDPI screen </li>\\n\n *      </ul>\\n\n */\nCSInterface.prototype.getScaleFactor = function()\n{\n    return window.__adobe_cep__.getScaleFactor();\n};\n\n/**\n * Set a handler to detect any changes of scale factor. This only works on Mac.\n *\n * Since 4.2.0\n *\n * @param handler   The function to be called when scale factor is changed.\n *\n */\nCSInterface.prototype.setScaleFactorChangedHandler = function(handler)\n{\n    window.__adobe_cep__.setScaleFactorChangedHandler(handler);\n};\n\n/**\n * Retrieves current API version.\n *\n * Since 4.2.0\n *\n * @return ApiVersion object.\n *\n */\nCSInterface.prototype.getCurrentApiVersion = function()\n{\n    var apiVersion = JSON.parse(window.__adobe_cep__.getCurrentApiVersion());\n    return apiVersion;\n};\n\n/**\n * Set panel flyout menu by an XML.\n *\n * Since 5.2.0\n *\n * Register a callback function for \"com.adobe.csxs.events.flyoutMenuClicked\" to get notified when a \n * menu item is clicked.\n * The \"data\" attribute of event is an object which contains \"menuId\" and \"menuName\" attributes. \n *\n * Register callback functions for \"com.adobe.csxs.events.flyoutMenuOpened\" and \"com.adobe.csxs.events.flyoutMenuClosed\"\n * respectively to get notified when flyout menu is opened or closed.\n *\n * @param menu     A XML string which describes menu structure.\n * An example menu XML:\n * <Menu>\n *   <MenuItem Id=\"menuItemId1\" Label=\"TestExample1\" Enabled=\"true\" Checked=\"false\"/>\n *   <MenuItem Label=\"TestExample2\">\n *     <MenuItem Label=\"TestExample2-1\" >\n *       <MenuItem Label=\"TestExample2-1-1\" Enabled=\"false\" Checked=\"true\"/>\n *     </MenuItem>\n *     <MenuItem Label=\"TestExample2-2\" Enabled=\"true\" Checked=\"true\"/>\n *   </MenuItem>\n *   <MenuItem Label=\"---\" />\n *   <MenuItem Label=\"TestExample3\" Enabled=\"false\" Checked=\"false\"/>\n * </Menu>\n *\n */\nCSInterface.prototype.setPanelFlyoutMenu = function(menu)\n{\n    if (\"string\" != typeof menu)\n    {\n        return;\t\n    }\n\n\twindow.__adobe_cep__.invokeSync(\"setPanelFlyoutMenu\", menu);\n};\n\n/**\n * Updates a menu item in the extension window's flyout menu, by setting the enabled\n * and selection status.\n *  \n * Since 5.2.0\n *\n * @param menuItemLabel\tThe menu item label. \n * @param enabled\t\tTrue to enable the item, false to disable it (gray it out).\n * @param checked\t\tTrue to select the item, false to deselect it.\n *\n * @return false when the host application does not support this functionality (HostCapabilities.EXTENDED_PANEL_MENU is false). \n *         Fails silently if menu label is invalid.\n *\n * @see HostCapabilities.EXTENDED_PANEL_MENU\n */\nCSInterface.prototype.updatePanelMenuItem = function(menuItemLabel, enabled, checked)\n{\n\tvar ret = false;\n\tif (this.getHostCapabilities().EXTENDED_PANEL_MENU) \n\t{\n\t\tvar itemStatus = new MenuItemStatus(menuItemLabel, enabled, checked);\n\t\tret = window.__adobe_cep__.invokeSync(\"updatePanelMenuItem\", JSON.stringify(itemStatus));\n\t}\n\treturn ret;\n};\n\n\n/**\n * Set context menu by XML string.\n *\n * Since 5.2.0\n *\n * There are a number of conventions used to communicate what type of menu item to create and how it should be handled.\n * - an item without menu ID or menu name is disabled and is not shown.\n * - if the item name is \"---\" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL.\n * - Checkable attribute takes precedence over Checked attribute.\n * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. \n     The Chrome extension contextMenus API was taken as a reference. \n     https://developer.chrome.com/extensions/contextMenus\n * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter.\n *\n * @param menu      A XML string which describes menu structure.\n * @param callback  The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item.\n *\n * @description An example menu XML:\n * <Menu>\n *   <MenuItem Id=\"menuItemId1\" Label=\"TestExample1\" Enabled=\"true\" Checkable=\"true\" Checked=\"false\" Icon=\"./image/small_16X16.png\"/>\n *   <MenuItem Id=\"menuItemId2\" Label=\"TestExample2\">\n *     <MenuItem Id=\"menuItemId2-1\" Label=\"TestExample2-1\" >\n *       <MenuItem Id=\"menuItemId2-1-1\" Label=\"TestExample2-1-1\" Enabled=\"false\" Checkable=\"true\" Checked=\"true\"/>\n *     </MenuItem>\n *     <MenuItem Id=\"menuItemId2-2\" Label=\"TestExample2-2\" Enabled=\"true\" Checkable=\"true\" Checked=\"true\"/>\n *   </MenuItem>\n *   <MenuItem Label=\"---\" />\n *   <MenuItem Id=\"menuItemId3\" Label=\"TestExample3\" Enabled=\"false\" Checkable=\"true\" Checked=\"false\"/>\n * </Menu>\n */\nCSInterface.prototype.setContextMenu = function(menu, callback)\n{\n    if (\"string\" != typeof menu)\n    {\n        return;\n    }\n    \n\twindow.__adobe_cep__.invokeAsync(\"setContextMenu\", menu, callback);\n};\n\n/**\n * Set context menu by JSON string.\n *\n * Since 6.0.0\n *\n * There are a number of conventions used to communicate what type of menu item to create and how it should be handled.\n * - an item without menu ID or menu name is disabled and is not shown.\n * - if the item label is \"---\" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL.\n * - Checkable attribute takes precedence over Checked attribute.\n * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. \n     The Chrome extension contextMenus API was taken as a reference.\n * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter.\n     https://developer.chrome.com/extensions/contextMenus\n *\n * @param menu      A JSON string which describes menu structure.\n * @param callback  The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item.\n *\n * @description An example menu JSON:\n *\n * { \n *      \"menu\": [\n *          {\n *              \"id\": \"menuItemId1\",\n *              \"label\": \"testExample1\",\n *              \"enabled\": true,\n *              \"checkable\": true,\n *              \"checked\": false,\n *              \"icon\": \"./image/small_16X16.png\"\n *          },\n *          {\n *              \"id\": \"menuItemId2\",\n *              \"label\": \"testExample2\",\n *              \"menu\": [\n *                  {\n *                      \"id\": \"menuItemId2-1\",\n *                      \"label\": \"testExample2-1\",\n *                      \"menu\": [\n *                          {\n *                              \"id\": \"menuItemId2-1-1\",\n *                              \"label\": \"testExample2-1-1\",\n *                              \"enabled\": false,\n *                              \"checkable\": true,\n *                              \"checked\": true\n *                          }\n *                      ]\n *                  },\n *                  {\n *                      \"id\": \"menuItemId2-2\",\n *                      \"label\": \"testExample2-2\",\n *                      \"enabled\": true,\n *                      \"checkable\": true,\n *                      \"checked\": true\n *                  }\n *              ]\n *          },\n *          {\n *              \"label\": \"---\"\n *          },\n *          {\n *              \"id\": \"menuItemId3\",\n *              \"label\": \"testExample3\",\n *              \"enabled\": false,\n *              \"checkable\": true,\n *              \"checked\": false\n *          }\n *      ]\n *  }\n *\n */\nCSInterface.prototype.setContextMenuByJSON = function(menu, callback)\n{\n    if (\"string\" != typeof menu)\n    {\n        return;\t\n    }\n    \n\twindow.__adobe_cep__.invokeAsync(\"setContextMenuByJSON\", menu, callback);\n};\n\n/**\n * Updates a context menu item by setting the enabled and selection status.\n *  \n * Since 5.2.0\n *\n * @param menuItemID\tThe menu item ID. \n * @param enabled\t\tTrue to enable the item, false to disable it (gray it out).\n * @param checked\t\tTrue to select the item, false to deselect it.\n */\nCSInterface.prototype.updateContextMenuItem = function(menuItemID, enabled, checked)\n{\n\tvar itemStatus = new ContextMenuItemStatus(menuItemID, enabled, checked);\n\tret = window.__adobe_cep__.invokeSync(\"updateContextMenuItem\", JSON.stringify(itemStatus));\n};\n\n/**\n * Get the visibility status of an extension window. \n *  \n * Since 6.0.0\n *\n * @return true if the extension window is visible; false if the extension window is hidden.\n */\nCSInterface.prototype.isWindowVisible = function()\n{\n\treturn window.__adobe_cep__.invokeSync(\"isWindowVisible\", \"\");\n};\n\n/**\n * Resize extension's content to the specified dimensions.\n * 1. Works with modal and modeless extensions in all Adobe products.\n * 2. Extension's manifest min/max size constraints apply and take precedence. \n * 3. For panel extensions\n *    3.1 This works in all Adobe products except:\n *        * Premiere Pro\n *        * Prelude\n *        * After Effects\n *    3.2 When the panel is in certain states (especially when being docked),\n *        it will not change to the desired dimensions even when the\n *        specified size satisfies min/max constraints.\n *\n * Since 6.0.0\n *\n * @param width  The new width\n * @param height The new height\n */\nCSInterface.prototype.resizeContent = function(width, height)\n{\n    window.__adobe_cep__.resizeContent(width, height);\n};\n\n/**\n * Register the invalid certificate callback for an extension. \n * This callback will be triggered when the extension tries to access the web site that contains the invalid certificate on the main frame.\n * But if the extension does not call this function and tries to access the web site containing the invalid certificate, a default error page will be shown.\n *  \n * Since 6.1.0\n *\n * @param callback the callback function\n */\nCSInterface.prototype.registerInvalidCertificateCallback = function(callback)\n{\n    return window.__adobe_cep__.registerInvalidCertificateCallback(callback);\n};\n\n/**\n * Register an interest in some key events to prevent them from being sent to the host application.\n *\n * This function works with modeless extensions and panel extensions. \n * Generally all the key events will be sent to the host application for these two extensions if the current focused element\n * is not text input or dropdown,\n * If you want to intercept some key events and want them to be handled in the extension, please call this function\n * in advance to prevent them being sent to the host application.\n *\n * Since 6.1.0\n *\n * @param keyEventsInterest      A JSON string describing those key events you are interested in. A null object or\n                                 an empty string will lead to removing the interest\n *\n * This JSON string should be an array, each object has following keys:\n *\n * keyCode:  [Required] represents an OS system dependent virtual key code identifying\n *           the unmodified value of the pressed key.\n * ctrlKey:  [optional] a Boolean that indicates if the control key was pressed (true) or not (false) when the event occurred.\n * altKey:   [optional] a Boolean that indicates if the alt key was pressed (true) or not (false) when the event occurred.\n * shiftKey: [optional] a Boolean that indicates if the shift key was pressed (true) or not (false) when the event occurred.\n * metaKey:  [optional] (Mac Only) a Boolean that indicates if the Meta key was pressed (true) or not (false) when the event occurred.\n *                      On Macintosh keyboards, this is the command key. To detect Windows key on Windows, please use keyCode instead.\n * An example JSON string:\n *\n * [\n *     {\n *         \"keyCode\": 48\n *     },\n *     {\n *         \"keyCode\": 123,\n *         \"ctrlKey\": true\n *     },\n *     {\n *         \"keyCode\": 123,\n *         \"ctrlKey\": true,\n *         \"metaKey\": true\n *     }\n * ]\n *\n */\nCSInterface.prototype.registerKeyEventsInterest = function(keyEventsInterest)\n{\n    return window.__adobe_cep__.registerKeyEventsInterest(keyEventsInterest);\n};\n\n/**\n * Set the title of the extension window. \n * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver.\n *\n * Since 6.1.0\n *\n * @param title The window title.\n */\nCSInterface.prototype.setWindowTitle = function(title)\n{\n    window.__adobe_cep__.invokeSync(\"setWindowTitle\", title);\n};\n\n/**\n * Get the title of the extension window. \n * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver.\n *\n * Since 6.1.0\n *\n * @return The window title.\n */\nCSInterface.prototype.getWindowTitle = function()\n{\n    return window.__adobe_cep__.invokeSync(\"getWindowTitle\", \"\");\n};\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/extension/client/client.js",
    "content": "    // client facing part of extension, creates WSRPC client (jsx cannot \n    // do that)\n    // consumes RPC calls from server (OpenPype) calls ./host/index.jsx and\n    // returns values back (in json format)\n    \n    var logReturn = function(result){ log.warn('Result: ' + result);};\n    \n    var csInterface = new CSInterface();\n    \n    log.warn(\"script start\");\n\n    WSRPC.DEBUG = false;\n    WSRPC.TRACE = false;\n       \n    function myCallBack(){\n        log.warn(\"Triggered index.jsx\");\n    }\n    // importing through manifest.xml isn't working because relative paths\n    // possibly TODO\n    jsx.evalFile('./host/index.jsx', myCallBack);\n    \n    function runEvalScript(script) {\n        // because of asynchronous nature of functions in jsx\n        // this waits for response\n        return new Promise(function(resolve, reject){\n            csInterface.evalScript(script, resolve);\n        });\n    }\n    \n    /** main entry point **/\n    startUp(\"WEBSOCKET_URL\");\n\n    // get websocket server url from environment value\n    async function startUp(url){\n        log.warn(\"url\", url);  \n        promis = runEvalScript(\"getEnv('\" + url + \"')\");\n        \n        var res = await promis; \n        // run rest only after resolved promise\n        main(res);\n    }\n\n    function get_extension_version(){\n        /** Returns version number from extension manifest.xml **/\n        log.debug(\"get_extension_version\")\n        var path = csInterface.getSystemPath(SystemPath.EXTENSION);\n        log.debug(\"extension path \" + path);\n    \n        var result = window.cep.fs.readFile(path + \"/CSXS/manifest.xml\");\n        var version = undefined;\n        if(result.err === 0){\n            if (window.DOMParser) {\n                const parser = new DOMParser();\n                const xmlDoc = parser.parseFromString(result.data.toString(), 'text/xml');\n                const children = xmlDoc.children;\n        \n                for (let i = 0; i <= children.length; i++) {\n                    if (children[i] && children[i].getAttribute('ExtensionBundleVersion')) {\n                        version = children[i].getAttribute('ExtensionBundleVersion');\n                    }\n                }\n            }\n        }\n        return version\n    }\n             \n    function main(websocket_url){\n      // creates connection to 'websocket_url', registers routes    \n      log.warn(\"websocket_url\", websocket_url);   \n      var default_url = 'ws://localhost:8099/ws/';\n      \n      if  (websocket_url == ''){\n           websocket_url = default_url;\n      }\n      log.warn(\"connecting to:\", websocket_url);  \n      RPC = new WSRPC(websocket_url, 5000); // spin connection\n  \n      RPC.connect();\n  \n      log.warn(\"connected\"); \n      \n      function EscapeStringForJSX(str){\n      // Replaces:\n      //  \\ with \\\\\n      //  ' with \\'\n      //  \" with \\\"\n      // See: https://stackoverflow.com/a/3967927/5285364\n          return str.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\").replace(/\"/g, '\\\\\"');\n      }\n      \n      RPC.addRoute('Photoshop.open', function (data) {\n              log.warn('Server called client route \"open\":', data);\n              var escapedPath = EscapeStringForJSX(data.path);\n              return runEvalScript(\"fileOpen('\" + escapedPath +\"')\")\n                  .then(function(result){\n                      log.warn(\"open: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.read', function (data) {\n              log.warn('Server called client route \"read\":', data);\n              return runEvalScript(\"getHeadline()\")\n                  .then(function(result){\n                      log.warn(\"getHeadline: \" + result);\n                      return result;\n                  });\n      });\n  \n      RPC.addRoute('Photoshop.get_layers', function (data) {\n              log.warn('Server called client route \"get_layers\":', data);\n              return runEvalScript(\"getLayers()\")\n                  .then(function(result){\n                      log.warn(\"getLayers: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.set_visible', function (data) {\n              log.warn('Server called client route \"set_visible\":', data);\n              return runEvalScript(\"setVisible(\" + data.layer_id + \", \" +\n                                   data.visibility + \")\")\n                  .then(function(result){\n                      log.warn(\"setVisible: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.get_active_document_name', function (data) {\n              log.warn('Server called client route \"get_active_document_name\":', \n                        data);\n              return runEvalScript(\"getActiveDocumentName()\")\n                  .then(function(result){\n                      log.warn(\"save: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.get_active_document_full_name', function (data) {\n              log.warn('Server called client route ' +\n                       '\"get_active_document_full_name\":', data);\n              return runEvalScript(\"getActiveDocumentFullName()\")\n                  .then(function(result){\n                      log.warn(\"save: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.save', function (data) {\n              log.warn('Server called client route \"save\":', data);\n              \n              return runEvalScript(\"save()\")\n                  .then(function(result){\n                      log.warn(\"save: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.get_selected_layers', function (data) {\n              log.warn('Server called client route \"get_selected_layers\":', data);\n              \n              return runEvalScript(\"getSelectedLayers()\")\n                  .then(function(result){\n                      log.warn(\"get_selected_layers: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.create_group', function (data) {\n              log.warn('Server called client route \"create_group\":', data);\n              \n              return runEvalScript(\"createGroup('\" + data.name + \"')\")\n                  .then(function(result){\n                      log.warn(\"createGroup: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.group_selected_layers', function (data) {\n              log.warn('Server called client route \"group_selected_layers\":', \n                       data);\n              \n              return runEvalScript(\"groupSelectedLayers(null, \"+\n                                   \"'\" + data.name +\"')\")\n                  .then(function(result){\n                      log.warn(\"group_selected_layers: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.import_smart_object', function (data) {\n              log.warn('Server called client \"import_smart_object\":', data);\n              var escapedPath = EscapeStringForJSX(data.path);\n              return runEvalScript(\"importSmartObject('\" + escapedPath +\"', \" +\n                                                      \"'\"+ data.name +\"',\"+\n                                                      + data.as_reference +\")\")\n                  .then(function(result){\n                      log.warn(\"import_smart_object: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.replace_smart_object', function (data) {\n              log.warn('Server called route \"replace_smart_object\":', data);\n              var escapedPath = EscapeStringForJSX(data.path);\n              return runEvalScript(\"replaceSmartObjects(\"+data.layer_id+\",\" +\n                                                        \"'\" + escapedPath +\"',\"+\n                                                        \"'\"+ data.name +\"')\")\n                  .then(function(result){\n                      log.warn(\"replaceSmartObjects: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.delete_layer', function (data) {\n              log.warn('Server called route \"delete_layer\":', data);\n              return runEvalScript(\"deleteLayer(\"+data.layer_id+\")\")\n                  .then(function(result){\n                      log.warn(\"delete_layer: \" + result);\n                      return result;\n                  });\n      });\n\n      RPC.addRoute('Photoshop.rename_layer', function (data) {\n        log.warn('Server called route \"rename_layer\":', data);\n        return runEvalScript(\"renameLayer(\"+data.layer_id+\", \" +\n                                          \"'\"+ data.name +\"')\")\n            .then(function(result){\n                log.warn(\"rename_layer: \" + result);\n                return result;\n            });\n});\n       \n      RPC.addRoute('Photoshop.select_layers', function (data) {\n              log.warn('Server called client route \"select_layers\":', data);\n              \n              return runEvalScript(\"selectLayers('\" + data.layers +\"')\")\n                  .then(function(result){\n                      log.warn(\"select_layers: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.is_saved', function (data) {\n              log.warn('Server called client route \"is_saved\":', data);\n              \n              return runEvalScript(\"isSaved()\")\n                  .then(function(result){\n                      log.warn(\"is_saved: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.saveAs', function (data) {\n              log.warn('Server called client route \"saveAsJPEG\":', data);\n              var escapedPath = EscapeStringForJSX(data.image_path);\n              return runEvalScript(\"saveAs('\" + escapedPath + \"', \" +\n                                           \"'\" + data.ext + \"', \" + \n                                           data.as_copy + \")\")\n                  .then(function(result){\n                      log.warn(\"save: \" + result);\n                      return result;\n                  });\n      });\n      \n      RPC.addRoute('Photoshop.imprint', function (data) {\n              log.warn('Server called client route \"imprint\":', data);\n              var escaped = data.payload.replace(/\\n/g, \"\\\\n\");\n              return runEvalScript(\"imprint('\" + escaped + \"')\")\n                  .then(function(result){\n                      log.warn(\"imprint: \" + result);\n                      return result;\n                  });\n      });\n\n      RPC.addRoute('Photoshop.get_extension_version', function (data) {\n        log.warn('Server called client route \"get_extension_version\":', data);\n        return get_extension_version();\n      });\n\n      RPC.addRoute('Photoshop.close', function (data) {\n        log.warn('Server called client route \"close\":', data);\n        return runEvalScript(\"close()\");\n      });\n        \n      RPC.call('Photoshop.ping').then(function (data) {\n          log.warn('Result for calling server route \"ping\": ', data);\n          return runEvalScript(\"ping()\")\n                  .then(function(result){\n                      log.warn(\"ping: \" + result);\n                      return result;\n                  });\n                \n      }, function (error) {\n          log.warn(error);\n      });\n    \n    }\n    \n    log.warn(\"end script\");\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/extension/client/wsrpc.js",
    "content": "(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n  typeof define === 'function' && define.amd ? define(factory) :\n  (global = global || self, global.WSRPC = factory());\n}(this, function () { 'use strict';\n\n  function _classCallCheck(instance, Constructor) {\n    if (!(instance instanceof Constructor)) {\n      throw new TypeError(\"Cannot call a class as a function\");\n    }\n  }\n\n  var Deferred = function Deferred() {\n    _classCallCheck(this, Deferred);\n\n    var self = this;\n    self.resolve = null;\n    self.reject = null;\n    self.done = false;\n\n    function wrapper(func) {\n      return function () {\n        if (self.done) throw new Error('Promise already done');\n        self.done = true;\n        return func.apply(this, arguments);\n      };\n    }\n\n    self.promise = new Promise(function (resolve, reject) {\n      self.resolve = wrapper(resolve);\n      self.reject = wrapper(reject);\n    });\n\n    self.promise.isPending = function () {\n      return !self.done;\n    };\n\n    return self;\n  };\n\n  function logGroup(group, level, args) {\n    console.group(group);\n    console[level].apply(this, args);\n    console.groupEnd();\n  }\n\n  function log() {\n    if (!WSRPC.DEBUG) return;\n    logGroup('WSRPC.DEBUG', 'trace', arguments);\n  }\n\n  function trace(msg) {\n    if (!WSRPC.TRACE) return;\n    var payload = msg;\n    if ('data' in msg) payload = JSON.parse(msg.data);\n    logGroup(\"WSRPC.TRACE\", 'trace', [payload]);\n  }\n\n  function getAbsoluteWsUrl(url) {\n    if (/^\\w+:\\/\\//.test(url)) return url;\n    if (typeof window == 'undefined' && window.location.host.length < 1) throw new Error(\"Can not construct absolute URL from \".concat(window.location));\n    var scheme = window.location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n    var port = window.location.port === '' ? \":\".concat(window.location.port) : '';\n    var host = window.location.host;\n    var path = url.replace(/^\\/+/gm, '');\n    return \"\".concat(scheme, \"//\").concat(host).concat(port, \"/\").concat(path);\n  }\n\n  var readyState = Object.freeze({\n    0: 'CONNECTING',\n    1: 'OPEN',\n    2: 'CLOSING',\n    3: 'CLOSED'\n  });\n\n  var WSRPC = function WSRPC(URL) {\n    var reconnectTimeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1000;\n\n    _classCallCheck(this, WSRPC);\n\n    var self = this;\n    URL = getAbsoluteWsUrl(URL);\n    self.id = 1;\n    self.eventId = 0;\n    self.socketStarted = false;\n    self.eventStore = {\n      onconnect: {},\n      onerror: {},\n      onclose: {},\n      onchange: {}\n    };\n    self.connectionNumber = 0;\n    self.oneTimeEventStore = {\n      onconnect: [],\n      onerror: [],\n      onclose: [],\n      onchange: []\n    };\n    self.callQueue = [];\n\n    function createSocket() {\n      var ws = new WebSocket(URL);\n\n      var rejectQueue = function rejectQueue() {\n        self.connectionNumber++; // rejects incoming calls\n\n        var deferred; //reject all pending calls\n\n        while (0 < self.callQueue.length) {\n          var callObj = self.callQueue.shift();\n          deferred = self.store[callObj.id];\n          delete self.store[callObj.id];\n\n          if (deferred && deferred.promise.isPending()) {\n            deferred.reject('WebSocket error occurred');\n          }\n        } // reject all from the store\n\n\n        for (var key in self.store) {\n          if (!self.store.hasOwnProperty(key)) continue;\n          deferred = self.store[key];\n\n          if (deferred && deferred.promise.isPending()) {\n            deferred.reject('WebSocket error occurred');\n          }\n        }\n      };\n\n      function reconnect(callEvents) {\n        setTimeout(function () {\n          try {\n            self.socket = createSocket();\n            self.id = 1;\n          } catch (exc) {\n            callEvents('onerror', exc);\n            delete self.socket;\n            console.error(exc);\n          }\n        }, reconnectTimeout);\n      }\n\n      ws.onclose = function (err) {\n        log('ONCLOSE CALLED', 'STATE', self.public.state());\n        trace(err);\n\n        for (var serial in self.store) {\n          if (!self.store.hasOwnProperty(serial)) continue;\n\n          if (self.store[serial].hasOwnProperty('reject')) {\n            self.store[serial].reject('Connection closed');\n          }\n        }\n\n        rejectQueue();\n        callEvents('onclose', err);\n        callEvents('onchange', err);\n        reconnect(callEvents);\n      };\n\n      ws.onerror = function (err) {\n        log('ONERROR CALLED', 'STATE', self.public.state());\n        trace(err);\n        rejectQueue();\n        callEvents('onerror', err);\n        callEvents('onchange', err);\n        log('WebSocket has been closed by error: ', err);\n      };\n\n      function tryCallEvent(func, event) {\n        try {\n          return func(event);\n        } catch (e) {\n          if (e.hasOwnProperty('stack')) {\n            log(e.stack);\n          } else {\n            log('Event function', func, 'raised unknown error:', e);\n          }\n\n          console.error(e);\n        }\n      }\n\n      function callEvents(evName, event) {\n        while (0 < self.oneTimeEventStore[evName].length) {\n          var deferred = self.oneTimeEventStore[evName].shift();\n          if (deferred.hasOwnProperty('resolve') && deferred.promise.isPending()) deferred.resolve();\n        }\n\n        for (var i in self.eventStore[evName]) {\n          if (!self.eventStore[evName].hasOwnProperty(i)) continue;\n          var cur = self.eventStore[evName][i];\n          tryCallEvent(cur, event);\n        }\n      }\n\n      ws.onopen = function (ev) {\n        log('ONOPEN CALLED', 'STATE', self.public.state());\n        trace(ev);\n\n        while (0 < self.callQueue.length) {\n          // noinspection JSUnresolvedFunction\n          self.socket.send(JSON.stringify(self.callQueue.shift(), 0, 1));\n        }\n\n        callEvents('onconnect', ev);\n        callEvents('onchange', ev);\n      };\n\n      function handleCall(self, data) {\n        if (!self.routes.hasOwnProperty(data.method)) throw new Error('Route not found');\n        var connectionNumber = self.connectionNumber;\n        var deferred = new Deferred();\n        deferred.promise.then(function (result) {\n          if (connectionNumber !== self.connectionNumber) return;\n          self.socket.send(JSON.stringify({\n            id: data.id,\n            result: result\n          }));\n        }, function (error) {\n          if (connectionNumber !== self.connectionNumber) return;\n          self.socket.send(JSON.stringify({\n            id: data.id,\n            error: error\n          }));\n        });\n        var func = self.routes[data.method];\n        if (self.asyncRoutes[data.method]) return func.apply(deferred, [data.params]);\n\n        function badPromise() {\n          throw new Error(\"You should register route with async flag.\");\n        }\n\n        var promiseMock = {\n          resolve: badPromise,\n          reject: badPromise\n        };\n\n        try {\n          deferred.resolve(func.apply(promiseMock, [data.params]));\n        } catch (e) {\n          deferred.reject(e);\n          console.error(e);\n        }\n      }\n\n      function handleError(self, data) {\n        if (!self.store.hasOwnProperty(data.id)) return log('Unknown callback');\n        var deferred = self.store[data.id];\n        if (typeof deferred === 'undefined') return log('Confirmation without handler');\n        delete self.store[data.id];\n        log('REJECTING', data.error);\n        deferred.reject(data.error);\n      }\n\n      function handleResult(self, data) {\n        var deferred = self.store[data.id];\n        if (typeof deferred === 'undefined') return log('Confirmation without handler');\n        delete self.store[data.id];\n\n        if (data.hasOwnProperty('result')) {\n          return deferred.resolve(data.result);\n        }\n\n        return deferred.reject(data.error);\n      }\n\n      ws.onmessage = function (message) {\n        log('ONMESSAGE CALLED', 'STATE', self.public.state());\n        trace(message);\n        if (message.type !== 'message') return;\n        var data;\n\n        try {\n          data = JSON.parse(message.data);\n          log(data);\n\n          if (data.hasOwnProperty('method')) {\n            return handleCall(self, data);\n          } else if (data.hasOwnProperty('error') && data.error === null) {\n            return handleError(self, data);\n          } else {\n            return handleResult(self, data);\n          }\n        } catch (exception) {\n          var err = {\n            error: exception.message,\n            result: null,\n            id: data ? data.id : null\n          };\n          self.socket.send(JSON.stringify(err));\n          console.error(exception);\n        }\n      };\n\n      return ws;\n    }\n\n    function makeCall(func, args, params) {\n      self.id += 2;\n      var deferred = new Deferred();\n      var callObj = Object.freeze({\n        id: self.id,\n        method: func,\n        params: args\n      });\n      var state = self.public.state();\n\n      if (state === 'OPEN') {\n        self.store[self.id] = deferred;\n        self.socket.send(JSON.stringify(callObj));\n      } else if (state === 'CONNECTING') {\n        log('SOCKET IS', state);\n        self.store[self.id] = deferred;\n        self.callQueue.push(callObj);\n      } else {\n        log('SOCKET IS', state);\n\n        if (params && params['noWait']) {\n          deferred.reject(\"Socket is: \".concat(state));\n        } else {\n          self.store[self.id] = deferred;\n          self.callQueue.push(callObj);\n        }\n      }\n\n      return deferred.promise;\n    }\n\n    self.asyncRoutes = {};\n    self.routes = {};\n    self.store = {};\n    self.public = Object.freeze({\n      call: function call(func, args, params) {\n        return makeCall(func, args, params);\n      },\n      addRoute: function addRoute(route, callback, isAsync) {\n        self.asyncRoutes[route] = isAsync || false;\n        self.routes[route] = callback;\n      },\n      deleteRoute: function deleteRoute(route) {\n        delete self.asyncRoutes[route];\n        return delete self.routes[route];\n      },\n      addEventListener: function addEventListener(event, func) {\n        var eventId = self.eventId++;\n        self.eventStore[event][eventId] = func;\n        return eventId;\n      },\n      removeEventListener: function removeEventListener(event, index) {\n        if (self.eventStore[event].hasOwnProperty(index)) {\n          delete self.eventStore[event][index];\n          return true;\n        } else {\n          return false;\n        }\n      },\n      onEvent: function onEvent(event) {\n        var deferred = new Deferred();\n        self.oneTimeEventStore[event].push(deferred);\n        return deferred.promise;\n      },\n      destroy: function destroy() {\n        return self.socket.close();\n      },\n      state: function state() {\n        return readyState[this.stateCode()];\n      },\n      stateCode: function stateCode() {\n        if (self.socketStarted && self.socket) return self.socket.readyState;\n        return 3;\n      },\n      connect: function connect() {\n        self.socketStarted = true;\n        self.socket = createSocket();\n      }\n    });\n    self.public.addRoute('log', function (argsObj) {\n      //console.info(\"Websocket sent: \".concat(argsObj));\n    });\n    self.public.addRoute('ping', function (data) {\n      return data;\n    });\n    return self.public;\n  };\n\n  WSRPC.DEBUG = false;\n  WSRPC.TRACE = false;\n\n  return WSRPC;\n\n}));\n//# sourceMappingURL=wsrpc.js.map\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/extension/host/JSX.js",
    "content": "/*\n         _ ______  __   _\n        | / ___\\ \\/ /  (_)___\n     _  | \\___ \\\\  /   | / __|\n    | |_| |___) /  \\ _ | \\__ \\\n     \\___/|____/_/\\_(_)/ |___/\n                     |__/\n                        _               ____\n    /\\   /\\___ _ __ ___(_) ___  _ __   |___ \\\n    \\ \\ / / _ \\ '__/ __| |/ _ \\| '_ \\    __) |\n     \\ V /  __/ |  \\__ \\ | (_) | | | |  / __/\n      \\_/ \\___|_|  |___/_|\\___/|_| |_| |_____|\n*/\n\n\n//////////////////////////////////////////////////////////////////////////////////\n// JSX.js  and writtent by Trevor https://creative-scripts.com/jsx-js           //\n// If you turn over is less the $50,000,000 then you don't have to pay anything //\n// License MIT, don't complain, don't sue NO MATTER WHAT                        //\n// If you turn over is more the $50,000,000 then you DO have to pay             //\n// Contact me https://creative-scripts.com/contact for pricing and licensing     //\n// Don't remove these commented lines                                           //\n// For simple and effective calling of jsx from the js engine                   //\n// Version 2 last modified April 18 2018                                        //\n//////////////////////////////////////////////////////////////////////////////////\n\n///////////////////////////////////////////////////////////////////////////////////////////////////////////\n// Change log:                                                                                           //\n// JSX.js V2 is now independent of NodeJS and CSInterface.js <span class=\"wp-font-emots-emo-happy\"></span>                                         //\n// forceEval is now by default true                                                                      //\n// It wraps the scripts in a try catch and an eval providing useful error handling                       //\n// One can set in the jsx engine $.includeStack = true to return the call stack in the event of an error //\n///////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n///////////////////////////////////////////////////////////////////////////////////////////////////////////\n// JSX.js for calling jsx code from the js engine                                                        //\n// 2 methods included                                                                                    //\n// 1) jsx.evalScript AKA jsx.eval                                                                        //\n// 2) jsx.evalFile AKA jsx.file                                                                          //\n// Special features                                                                                      //\n// 1) Allows all changes in your jsx code to be reloaded into your extension at the click of a button    //\n// 2) Can enable the $.fileName property to work and provides a $.__fileName() method as an alternative  //\n// 3) Can force a callBack result from InDesign                                                          //\n// 4) No more csInterface.evalScript('alert(\"hello \"' + title + \" \" + name + '\");')                      //\n//    use jsx.evalScript('alert(\"hello __title__ __name__\");', {title: title, name: name});              //\n// 5) execute jsx files from your jsx folder like this jsx.evalFile('myFabJsxScript.jsx');               //\n//    or from a relative path jsx.evalFile('../myFabScripts/myFabJsxScript.jsx');                        //\n//    or from an absolute url jsx.evalFile('/Path/to/my/FabJsxScript.jsx'); (mac)                        //\n//    or from an absolute url jsx.evalFile('C:Path/to/my/FabJsxScript.jsx'); (windows)                   //\n// 6) Parameter can be entered in the from of a parameter list which can be in any order or as an object //\n// 7) Not camelCase sensitive (very useful for the illiterate)                                           //\n// <span class=\"wp-font-emots-emo-sunglasses\"></span> Dead easy to use BUT SPEND THE 3 TO 5 MINUTES IT SHOULD TAKE TO READ THE INSTRUCTIONS              //\n///////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n/* jshint undef:true, unused:true, esversion:6 */\n\n//////////////////////////////////////\n// jsx is the interface for the API //\n//////////////////////////////////////\n\nvar jsx;\n\n// Wrap everything in an anonymous function to prevent leeks\n(function() {\n    /////////////////////////////////////////////////////////////////////\n    // Substitute some CSInterface functions to avoid dependency on it //\n    /////////////////////////////////////////////////////////////////////\n\n    var __dirname = (function() {\n        var path, isMac;\n        path = decodeURI(window.__adobe_cep__.getSystemPath('extension'));\n        isMac = navigator.platform[0] === 'M'; // [M]ac\n        path = path.replace('file://' + (isMac ? '' : '/'), '');\n        return path;\n    })();\n\n    var evalScript = function(script, callback) {\n        callback = callback || function() {};\n        window.__adobe_cep__.evalScript(script, callback);\n    };\n\n\n    ////////////////////////////////////////////\n    // In place of using the node path module //\n    ////////////////////////////////////////////\n\n    // jshint undef: true, unused: true\n\n    // A very minified version of the NodeJs Path module!!\n    // For use outside of NodeJs\n    // Majorly nicked by Trevor from Joyent\n    var path = (function() {\n\n        var isString = function(arg) {\n            return typeof arg === 'string';\n        };\n\n        // var isObject = function(arg) {\n        //     return typeof arg === 'object' && arg !== null;\n        // };\n\n        var basename = function(path) {\n            if (!isString(path)) {\n                throw new TypeError('Argument to path.basename must be a string');\n            }\n            var bits = path.split(/[\\/\\\\]/g);\n            return bits[bits.length - 1];\n        };\n\n        // jshint undef: true\n        // Regex to split a windows path into three parts: [*, device, slash,\n        // tail] windows-only\n        var splitDeviceRe =\n            /^([a-zA-Z]:|[\\\\\\/]{2}[^\\\\\\/]+[\\\\\\/]+[^\\\\\\/]+)?([\\\\\\/])?([\\s\\S]*?)$/;\n\n        // Regex to split the tail part of the above into [*, dir, basename, ext]\n        // var splitTailRe =\n        //     /^([\\s\\S]*?)((?:\\.{1,2}|[^\\\\\\/]+?|)(\\.[^.\\/\\\\]*|))(?:[\\\\\\/]*)$/;\n\n        var win32 = {};\n        // Function to split a filename into [root, dir, basename, ext]\n        // var win32SplitPath = function(filename) {\n        //     // Separate device+slash from tail\n        //     var result = splitDeviceRe.exec(filename),\n        //         device = (result[1] || '') + (result[2] || ''),\n        //         tail = result[3] || '';\n        //     // Split the tail into dir, basename and extension\n        //     var result2 = splitTailRe.exec(tail),\n        //         dir = result2[1],\n        //         basename = result2[2],\n        //         ext = result2[3];\n        //     return [device, dir, basename, ext];\n        // };\n\n        var win32StatPath = function(path) {\n            var result = splitDeviceRe.exec(path),\n                device = result[1] || '',\n                isUnc = !!device && device[1] !== ':';\n            return {\n                device: device,\n                isUnc: isUnc,\n                isAbsolute: isUnc || !!result[2], // UNC paths are always absolute\n                tail: result[3]\n            };\n        };\n\n        var normalizeUNCRoot = function(device) {\n            return '\\\\\\\\' + device.replace(/^[\\\\\\/]+/, '').replace(/[\\\\\\/]+/g, '\\\\');\n        };\n\n        var normalizeArray = function(parts, allowAboveRoot) {\n            var res = [];\n            for (var i = 0; i < parts.length; i++) {\n                var p = parts[i];\n\n                // ignore empty parts\n                if (!p || p === '.')\n                    continue;\n\n                if (p === '..') {\n                    if (res.length && res[res.length - 1] !== '..') {\n                        res.pop();\n                    } else if (allowAboveRoot) {\n                        res.push('..');\n                    }\n                } else {\n                    res.push(p);\n                }\n            }\n\n            return res;\n        };\n\n        win32.normalize = function(path) {\n            var result = win32StatPath(path),\n                device = result.device,\n                isUnc = result.isUnc,\n                isAbsolute = result.isAbsolute,\n                tail = result.tail,\n                trailingSlash = /[\\\\\\/]$/.test(tail);\n\n            // Normalize the tail path\n            tail = normalizeArray(tail.split(/[\\\\\\/]+/), !isAbsolute).join('\\\\');\n\n            if (!tail && !isAbsolute) {\n                tail = '.';\n            }\n            if (tail && trailingSlash) {\n                tail += '\\\\';\n            }\n\n            // Convert slashes to backslashes when `device` points to an UNC root.\n            // Also squash multiple slashes into a single one where appropriate.\n            if (isUnc) {\n                device = normalizeUNCRoot(device);\n            }\n\n            return device + (isAbsolute ? '\\\\' : '') + tail;\n        };\n        win32.join = function() {\n            var paths = [];\n            for (var i = 0; i < arguments.length; i++) {\n                var arg = arguments[i];\n                if (!isString(arg)) {\n                    throw new TypeError('Arguments to path.join must be strings');\n                }\n                if (arg) {\n                    paths.push(arg);\n                }\n            }\n\n            var joined = paths.join('\\\\');\n\n            // Make sure that the joined path doesn't start with two slashes, because\n            // normalize() will mistake it for an UNC path then.\n            //\n            // This step is skipped when it is very clear that the user actually\n            // intended to point at an UNC path. This is assumed when the first\n            // non-empty string arguments starts with exactly two slashes followed by\n            // at least one more non-slash character.\n            //\n            // Note that for normalize() to treat a path as an UNC path it needs to\n            // have at least 2 components, so we don't filter for that here.\n            // This means that the user can use join to construct UNC paths from\n            // a server name and a share name; for example:\n            //   path.join('//server', 'share') -> '\\\\\\\\server\\\\share\\')\n            if (!/^[\\\\\\/]{2}[^\\\\\\/]/.test(paths[0])) {\n                joined = joined.replace(/^[\\\\\\/]{2,}/, '\\\\');\n            }\n            return win32.normalize(joined);\n        };\n\n        var posix = {};\n\n        // posix version\n        posix.join = function() {\n            var path = '';\n            for (var i = 0; i < arguments.length; i++) {\n                var segment = arguments[i];\n                if (!isString(segment)) {\n                    throw new TypeError('Arguments to path.join must be strings');\n                }\n                if (segment) {\n                    if (!path) {\n                        path += segment;\n                    } else {\n                        path += '/' + segment;\n                    }\n                }\n            }\n            return posix.normalize(path);\n        };\n\n        // path.normalize(path)\n        // posix version\n        posix.normalize = function(path) {\n            var isAbsolute = path.charAt(0) === '/',\n                trailingSlash = path && path[path.length - 1] === '/';\n\n            // Normalize the path\n            path = normalizeArray(path.split('/'), !isAbsolute).join('/');\n\n            if (!path && !isAbsolute) {\n                path = '.';\n            }\n            if (path && trailingSlash) {\n                path += '/';\n            }\n\n            return (isAbsolute ? '/' : '') + path;\n        };\n\n        win32.basename = posix.basename = basename;\n\n        this.win32 = win32;\n        this.posix = posix;\n        return (navigator.platform[0] === 'M') ? posix : win32;\n    })();\n\n    ////////////////////////////////////////////////////////////////////////////////////////////////////////\n    // The is the  \"main\" function which is to be prototyped                                              //\n    // It run a small snippet in the jsx engine that                                                      //\n    // 1) Assigns $.__dirname with the value of the extensions __dirname base path                        //\n    // 2) Sets up a method $.__fileName() for retrieving from within the jsx script it's $.fileName value //\n    //    more on that method later                                                                       //\n    // At the end of the script the global declaration jsx = new Jsx(); has been made.                    //\n    // If you like you can remove that and include in your relevant functions                             //\n    // var jsx = new Jsx(); You would never call the Jsx function without the \"new\" declaration           //\n    ////////////////////////////////////////////////////////////////////////////////////////////////////////\n    var Jsx = function() {\n        var jsxScript;\n        // Setup jsx function to enable the jsx scripts to easily retrieve their file location\n        jsxScript = [\n            '$.level = 0;',\n            'if(!$.__fileNames){',\n            '    $.__fileNames = {};',\n            '    $.__dirname = \"__dirname__\";'.replace('__dirname__', __dirname),\n            '    $.__fileName = function(name){',\n            '        name = name || $.fileName;',\n            '        return ($.__fileNames && $.__fileNames[name]) || $.fileName;',\n            '    };',\n            '}'\n        ].join('');\n        evalScript(jsxScript);\n        return this;\n    };\n\n    /**\n     * [evalScript] For calling jsx scripts from the js engine\n     *\n     *         The jsx.evalScript method is used for calling jsx scripts directly from the js engine\n     *         Allows for easy replacement i.e. variable insertions and for forcing eval.\n     *         For convenience jsx.eval or jsx.script or jsx.evalscript can be used instead of calling jsx.evalScript\n     *\n     * @param  {String} jsxScript\n     *                            The string that makes up the jsx script\n     *                            it can contain a simple template like syntax for replacements\n     *                            'alert(\"__foo__\");'\n     *                            the __foo__ will be replaced as per the replacements parameter\n     *\n     * @param  {Function} callback\n     *                            The callback function you want the jsx script to trigger on completion\n     *                            The result of the jsx script is passed as the argument to that function\n     *                            The function can exist in some other file.\n     *                            Note that InDesign does not automatically pass the callBack as a string.\n     *                            Either write your InDesign in a way that it returns a sting the form of\n     *                            return 'this is my result surrounded by quotes'\n     *                            or use the force eval option\n     *                            [Optional DEFAULT no callBack]\n     *\n     * @param  {Object} replacements\n     *                            The replacements to make on the jsx script\n     *                            given the following script (template)\n     *                            'alert(\"__message__: \" + __val__);'\n     *                            and we want to change the script to\n     *                            'alert(\"I was born in the year: \" + 1234);'\n     *                            we would pass the following object\n     *                            {\"message\": 'I was born in the year', \"val\": 1234}\n     *                            or if not using reserved words like do we can leave out the key quotes\n     *                            {message: 'I was born in the year', val: 1234}\n     *                            [Optional DEFAULT no replacements]\n     *\n     * @param  {Bolean} forceEval\n     *                             If the script should be wrapped in an eval and try catch\n     *                             This will 1) provide useful error feedback if heaven forbid it is needed\n     *                             2) The result will be a string which is required for callback results in InDesign\n     *                             [Optional DEFAULT true]\n     *\n     * Note 1) The order of the parameters is irrelevant\n     * Note 2) One can pass the arguments as an object if desired\n     *         jsx.evalScript(myCallBackFunction, 'alert(\"__myMessage__\");', true);\n     *         is the same as\n     *         jsx.evalScript({\n     *             script: 'alert(\"__myMessage__\");',\n     *             replacements: {myMessage: 'Hi there'},\n     *             callBack: myCallBackFunction,\n     *             eval: true\n     *         });\n     *         note that either lower or camelCase key names are valid\n     *         i.e. both callback or callBack will work\n     *\n     *      The following keys are the same jsx || script || jsxScript || jsxscript || file\n     *      The following keys are the same callBack || callback\n     *      The following keys are the same replacements || replace\n     *      The following keys are the same eval || forceEval || forceeval\n     *      The following keys are the same forceEvalScript || forceevalscript || evalScript || evalscript;\n     *\n     * @return {Boolean} if the jsxScript was executed or not\n     */\n\n    Jsx.prototype.evalScript = function() {\n        var arg, i, key, replaceThis, withThis, args, callback, forceEval, replacements, jsxScript, isBin;\n\n        //////////////////////////////////////////////////////////////////////////////////////\n        // sort out order which arguments into jsxScript, callback, replacements, forceEval //\n        //////////////////////////////////////////////////////////////////////////////////////\n\n        args = arguments;\n\n        // Detect if the parameters were passed as an object and if so allow for various keys\n        if (args.length === 1 && (arg = args[0]) instanceof Object) {\n            jsxScript = arg.jsxScript || arg.jsx || arg.script || arg.file || arg.jsxscript;\n            callback = arg.callBack || arg.callback;\n            replacements = arg.replacements || arg.replace;\n            forceEval = arg.eval || arg.forceEval || arg.forceeval;\n        } else {\n            for (i = 0; i < 4; i++) {\n                arg = args[i];\n                if (arg === undefined) {\n                    continue;\n                }\n                if (arg.constructor === String) {\n                    jsxScript = arg;\n                    continue;\n                }\n                if (arg.constructor === Object) {\n                    replacements = arg;\n                    continue;\n                }\n                if (arg.constructor === Function) {\n                    callback = arg;\n                    continue;\n                }\n                if (arg === false) {\n                    forceEval = false;\n                }\n            }\n        }\n\n        // If no script provide then not too much to do!\n        if (!jsxScript) {\n            return false;\n        }\n\n        // Have changed the forceEval default to be true as I prefer the error handling\n        if (forceEval !== false) {\n            forceEval = true;\n        }\n\n        //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n        // On Illustrator and other apps the result of the jsx script is automatically passed as a string                                       //\n        // if you have a \"script\" containing the single number 1 and nothing else then the callBack will register as \"1\"                        //\n        // On InDesign that same script will provide a blank callBack                                                                           //\n        // Let's say we have a callBack function var callBack = function(result){alert(result);}                                                //\n        // On Ai your see the 1 in the alert                                                                                                    //\n        // On ID your just see a blank alert                                                                                                    //\n        // To see the 1 in the alert you need to convert the result to a string and then it will show                                           //\n        // So if we rewrite out 1 byte script to '1' i.e. surround the 1 in quotes then the call back alert will show 1                         //\n        // If the scripts planed one can make sure that the results always passed as a string (including errors)                                //\n        // otherwise one can wrap the script in an eval and then have the result passed as a string                                             //\n        // I have not gone through all the apps but can say                                                                                     //\n        // for Ai you never need to set the forceEval to true                                                                                   //\n        // for ID you if you have not coded your script appropriately and your want to send a result to the callBack then set forceEval to true //\n        // I changed this that even on Illustrator it applies the try catch, Note the try catch will fail if $.level is set to 1                //\n        //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n        if (forceEval) {\n\n            isBin = (jsxScript.substring(0, 10) === '@JSXBIN@ES') ? '' : '\\n';\n            jsxScript = (\n                // \"\\n''') + '';} catch(e){(function(e){var n, a=[]; for (n in e){a.push(n + ': ' + e[n])}; return a.join('\\n')})(e)}\");\n                // \"\\n''') + '';} catch(e){e + (e.line ? ('\\\\nLine ' + (+e.line - 1)) : '')}\");\n                [\n                    \"$.level = 0;\",\n                    \"try{eval('''\" + isBin, // need to add an extra line otherwise #targetengine doesn't work ;-]\n                    jsxScript.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\").replace(/\"/g, '\\\\\"') + \"\\n''') + '';\",\n                    \"} catch (e) {\",\n                    \"    (function(e) {\",\n                    \"        var line, sourceLine, name, description, ErrorMessage, fileName, start, end, bug;\",\n                    \"        line = +e.line\" + (isBin === '' ? ';' : ' - 1;'), // To take into account the extra line added\n                    \"        fileName = File(e.fileName).fsName;\",\n                    \"        sourceLine = line && e.source.split(/[\\\\r\\\\n]/)[line];\",\n                    \"        name = e.name;\",\n                    \"        description = e.description;\",\n                    \"        ErrorMessage = name + ' ' + e.number + ': ' + description;\",\n                    \"        if (fileName.length && !(/[\\\\/\\\\\\\\]\\\\d+$/.test(fileName))) {\",\n                    \"           ErrorMessage += '\\\\nFile: ' + fileName;\",\n                    \"           line++;\",\n                    \"        }\",\n                    \"        if (line){\",\n                    \"           ErrorMessage += '\\\\nLine: ' + line +\",\n                    \"               '-> ' + ((sourceLine.length < 300) ? sourceLine : sourceLine.substring(0,300) + '...');\",\n                    \"        }\",\n                    \"        if (e.start) {ErrorMessage += '\\\\nBug: ' + e.source.substring(e.start - 1, e.end)}\",\n                    \"        if ($.includeStack) {ErrorMessage += '\\\\nStack:' + $.stack;}\",\n                    \"        return ErrorMessage;\",\n                    \"    })(e);\",\n                    \"}\"\n                ].join('')\n            );\n\n        }\n\n        /////////////////////////////////////////////////////////////\n        // deal with the replacements                              //\n        // Note it's probably better to use ${template} `literals` //\n        /////////////////////////////////////////////////////////////\n\n        if (replacements) {\n            for (key in replacements) {\n                if (replacements.hasOwnProperty(key)) {\n                    replaceThis = new RegExp('__' + key + '__', 'g');\n                    withThis = replacements[key];\n                    jsxScript = jsxScript.replace(replaceThis, withThis + '');\n                }\n            }\n        }\n\n\n        try {\n            evalScript(jsxScript, callback);\n            return true;\n        } catch (err) {\n            ////////////////////////////////////////////////\n            // Do whatever error handling you want here ! //\n            ////////////////////////////////////////////////\n            var newErr;\n            newErr = new Error(err);\n            alert('Error Eek: ' + newErr.stack);\n            return false;\n        }\n\n    };\n\n\n    /**\n     * [evalFile] For calling jsx scripts from the js engine\n     *\n     *         The jsx.evalFiles method is used for executing saved jsx scripts\n     *         where the jsxScript parameter is a string of the jsx scripts file location.\n     *         For convenience jsx.file or jsx.evalfile can be used instead of jsx.evalFile\n     *\n     * @param  {String} file\n     *                            The path to jsx script\n     *                            If only the base name is provided then the path will be presumed to be the\n     *                            To execute files stored in the jsx folder located in the __dirname folder use\n     *                            jsx.evalFile('myFabJsxScript.jsx');\n     *                            To execute files stored in the a folder myFabScripts located in the __dirname folder use\n     *                            jsx.evalFile('./myFabScripts/myFabJsxScript.jsx');\n     *                            To execute files stored in the a folder myFabScripts located at an absolute url use\n     *                            jsx.evalFile('/Path/to/my/FabJsxScript.jsx'); (mac)\n     *                            or jsx.evalFile('C:Path/to/my/FabJsxScript.jsx'); (windows)\n     *\n     * @param  {Function} callback\n     *                            The callback function you want the jsx script to trigger on completion\n     *                            The result of the jsx script is passed as the argument to that function\n     *                            The function can exist in some other file.\n     *                            Note that InDesign does not automatically pass the callBack as a string.\n     *                            Either write your InDesign in a way that it returns a sting the form of\n     *                            return 'this is my result surrounded by quotes'\n     *                            or use the force eval option\n     *                            [Optional DEFAULT no callBack]\n     *\n     * @param  {Object} replacements\n     *                            The replacements to make on the jsx script\n     *                            give the following script (template)\n     *                            'alert(\"__message__: \" + __val__);'\n     *                            and we want to change the script to\n     *                            'alert(\"I was born in the year: \" + 1234);'\n     *                            we would pass the following object\n     *                            {\"message\": 'I was born in the year', \"val\": 1234}\n     *                            or if not using reserved words like do we can leave out the key quotes\n     *                            {message: 'I was born in the year', val: 1234}\n     *                            By default when possible the forceEvalScript will be set to true\n     *                            The forceEvalScript option cannot be true when there are replacements\n     *                            To force the forceEvalScript to be false you can send a blank set of replacements\n     *                            jsx.evalFile('myFabScript.jsx', {}); Will NOT be executed using the $.evalScript method\n     *                            jsx.evalFile('myFabScript.jsx'); Will YES be executed using the $.evalScript method\n     *                            see the forceEvalScript parameter for details on this\n     *                            [Optional DEFAULT no replacements]\n     *\n     * @param  {Bolean} forceEval\n     *                             If the script should be wrapped in an eval and try catch\n     *                             This will 1) provide useful error feedback if heaven forbid it is needed\n     *                             2) The result will be a string which is required for callback results in InDesign\n     *                             [Optional DEFAULT true]\n     *\n     *                             If no replacements are needed then the jsx script is be executed by using the $.evalFile method\n     *                             This exposes the true value of the $.fileName property <span class=\"wp-font-emots-emo-sunglasses\"></span>\n     *                             In such a case it's best to avoid using the $.__fileName() with no base name as it won't work\n     *                             BUT one can still use the $.__fileName('baseName') method which is more accurate than the standard $.fileName property <span class=\"wp-font-emots-emo-happy\"></span>\n     *                             Let's say you have a Drive called \"Graphics\" AND YOU HAVE a root folder on your \"main\" drive called \"Graphics\"\n     *                             You call a script jsx.evalFile('/Volumes/Graphics/myFabScript.jsx');\n     *                             $.fileName will give you '/Graphics/myFabScript.jsx' which is wrong\n     *                             $.__fileName('myFabScript.jsx') will give you '/Volumes/Graphics/myFabScript.jsx' which is correct\n     *                             $.__fileName() will not give you a reliable result\n     *                             Note that if your calling multiple versions of myFabScript.jsx stored in multiple folders then you can get stuffed!\n     *                             i.e. if the fileName is important to you then don't do that.\n     *                             It also will force the result of the jsx file as a string which is particularly useful for InDesign callBacks\n     *\n     * Note 1) The order of the parameters is irrelevant\n     * Note 2) One can pass the arguments as an object if desired\n     *         jsx.evalScript(myCallBackFunction, 'alert(\"__myMessage__\");', true);\n     *         is the same as\n     *         jsx.evalScript({\n     *             script: 'alert(\"__myMessage__\");',\n     *             replacements: {myMessage: 'Hi there'},\n     *             callBack: myCallBackFunction,\n     *             eval: false,\n     *         });\n     *         note that either lower or camelCase key names or valid\n     *         i.e. both callback or callBack will work\n     *\n     *      The following keys are the same file || jsx || script || jsxScript || jsxscript\n     *      The following keys are the same callBack || callback\n     *      The following keys are the same replacements || replace\n     *      The following keys are the same eval || forceEval || forceeval\n     *\n     * @return {Boolean} if the jsxScript was executed or not\n     */\n\n    Jsx.prototype.evalFile = function() {\n        var arg, args, callback, fileName, fileNameScript, forceEval, forceEvalScript,\n            i, jsxFolder, jsxScript, newLine, replacements, success;\n\n        success = true; // optimistic <span class=\"wp-font-emots-emo-happy\"></span>\n        args = arguments;\n\n        jsxFolder = path.join(__dirname, 'jsx');\n        //////////////////////////////////////////////////////////////////////////////////////////////////////////\n        // $.fileName does not return it's correct path in the jsx engine for files called from the js engine   //\n        // In Illustrator it returns an integer in InDesign it returns an empty string                          //\n        // This script injection allows for the script to know it's path by calling                             //\n        // $.__fileName();                                                                                      //\n        // on Illustrator this works pretty well                                                                //\n        // on InDesign it's best to use with a bit of care                                                      //\n        // If the a second script has been called the InDesing will \"forget\" the path to the first script       //\n        // 2 work-arounds for this                                                                              //\n        // 1) at the beginning of your script add var thePathToMeIs = $.fileName();                             //\n        //    thePathToMeIs will not be forgotten after running the second script                               //\n        // 2) $.__fileName('myBaseName.jsx');                                                                   //\n        //    for example you have file with the following path                                                 //\n        //    /path/to/me.jsx                                                                                   //\n        //    Call $.__fileName('me.jsx') and you will get /path/to/me.jsx even after executing a second script //\n        // Note When the forceEvalScript option is used then you just use the regular $.fileName property       //\n        //////////////////////////////////////////////////////////////////////////////////////////////////////////\n        fileNameScript = [\n            // The if statement should not normally be executed\n            'if(!$.__fileNames){',\n            '    $.__fileNames = {};',\n            '    $.__dirname = \"__dirname__\";'.replace('__dirname__', __dirname),\n            '    $.__fileName = function(name){',\n            '        name = name || $.fileName;',\n            '        return ($.__fileNames && $.__fileNames[name]) || $.fileName;',\n            '    };',\n            '}',\n            '$.__fileNames[\"__basename__\"] = $.__fileNames[\"\" + $.fileName] = \"__fileName__\";'\n        ].join('');\n\n        //////////////////////////////////////////////////////////////////////////////////////\n        // sort out order which arguments into jsxScript, callback, replacements, forceEval //\n        //////////////////////////////////////////////////////////////////////////////////////\n\n\n        // Detect if the parameters were passed as an object and if so allow for various keys\n        if (args.length === 1 && (arg = args[0]) instanceof Object) {\n            jsxScript = arg.jsxScript || arg.jsx || arg.script || arg.file || arg.jsxscript;\n            callback = arg.callBack || arg.callback;\n            replacements = arg.replacements || arg.replace;\n            forceEval = arg.eval || arg.forceEval || arg.forceeval;\n        } else {\n            for (i = 0; i < 5; i++) {\n                arg = args[i];\n                if (arg === undefined) {\n                    continue;\n                }\n                if (arg.constructor.name === 'String') {\n                    jsxScript = arg;\n                    continue;\n                }\n                if (arg.constructor.name === 'Object') {\n                    //////////////////////////////////////////////////////////////////////////////////////////////////////////////\n                    // If no replacements are provided then the $.evalScript method will be used                                //\n                    // This will allow directly for the $.fileName property to be used                                          //\n                    // If one does not want the $.evalScript method to be used then                                             //\n                    // either send a blank object as the replacements {}                                                        //\n                    // or explicitly set the forceEvalScript option to false                                                    //\n                    // This can only be done if the parameters are passed as an object                                          //\n                    // i.e. jsx.evalFile({file:'myFabScript.jsx', forceEvalScript: false});                                     //\n                    // if the file was called using                                                                             //\n                    // i.e. jsx.evalFile('myFabScript.jsx');                                                                    //\n                    // then the following jsx code is called $.evalFile(new File('Path/to/myFabScript.jsx', 10000000000)) + ''; //\n                    // forceEval is never needed if the forceEvalScript is triggered                                            //\n                    //////////////////////////////////////////////////////////////////////////////////////////////////////////////\n                    replacements = arg;\n                    continue;\n                }\n                if (arg.constructor === Function) {\n                    callback = arg;\n                    continue;\n                }\n                if (arg === false) {\n                    forceEval = false;\n                }\n            }\n        }\n\n        // If no script provide then not too much to do!\n        if (!jsxScript) {\n            return false;\n        }\n\n        forceEvalScript = !replacements;\n\n\n        //////////////////////////////////////////////////////\n        // Get path of script                               //\n        // Check if it's literal, relative or in jsx folder //\n        //////////////////////////////////////////////////////\n\n        if (/^\\/|[a-zA-Z]+:/.test(jsxScript)) { // absolute path Mac  | Windows\n            jsxScript = path.normalize(jsxScript);\n        } else if (/^\\.+\\//.test(jsxScript)) {\n            jsxScript = path.join(__dirname, jsxScript); // relative path\n        } else {\n            jsxScript = path.join(jsxFolder, jsxScript); // files in the jsxFolder\n        }\n\n        if (forceEvalScript) {\n            jsxScript = jsxScript.replace(/\"/g, '\\\\\"');\n            // Check that the path exist, should change this to asynchronous at some point\n            if (!window.cep.fs.stat(jsxScript).err) {\n                jsxScript = fileNameScript.replace(/__fileName__/, jsxScript).replace(/__basename__/, path.basename(jsxScript)) +\n                    '$.evalFile(new File(\"' + jsxScript.replace(/\\\\/g, '\\\\\\\\') + '\")) + \"\";';\n                return this.evalScript(jsxScript, callback, forceEval);\n            } else {\n            throw new Error(`The file: {jsxScript} could not be found / read`);\n            }\n        }\n\n        ////////////////////////////////////////////////////////////////////////////////////////////////\n        // Replacements made so we can't use $.evalFile and need to read the jsx script for ourselves //\n        ////////////////////////////////////////////////////////////////////////////////////////////////\n\n        fileName = jsxScript.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n        try {\n            jsxScript = window.cep.fs.readFile(jsxScript).data;\n        } catch (er) {\n            throw new Error(`The file: ${fileName} could not be read`);\n        }\n        // It is desirable that the injected fileNameScript is on the same line as the 1st line of the script\n        // This is so that the $.line or error.line returns the same value as the actual file\n        // However if the 1st line contains a # directive then we need to insert a new line and stuff the above problem\n        // When possible i.e.  when there's no replacements then $.evalFile will be used and then the whole issue is avoided\n        newLine = /^\\s*#/.test(jsxScript) ? '\\n' : '';\n        jsxScript = fileNameScript.replace(/__fileName__/, fileName).replace(/__basename__/, path.basename(fileName)) + newLine + jsxScript;\n\n        try {\n            // evalScript(jsxScript, callback);\n            return this.evalScript(jsxScript, callback, replacements, forceEval);\n        } catch (err) {\n            ////////////////////////////////////////////////\n            // Do whatever error handling you want here ! //\n            ////////////////////////////////////////////////\n            var newErr;\n            newErr = new Error(err);\n            alert('Error Eek: ' + newErr.stack);\n            return false;\n        }\n\n        return success; // success should be an array but for now it's a Boolean\n    };\n\n\n    ////////////////////////////////////\n    // Setup alternative method names //\n    ////////////////////////////////////\n    Jsx.prototype.eval = Jsx.prototype.script = Jsx.prototype.evalscript = Jsx.prototype.evalScript;\n    Jsx.prototype.file = Jsx.prototype.evalfile = Jsx.prototype.evalFile;\n\n    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n    // Examples                                                                                                                      //\n    // jsx.evalScript('alert(\"foo\");');                                                                                              //\n    // jsx.evalFile('foo.jsx'); // where foo.jsx is stored in the jsx folder at the base of the extensions directory                 //\n    // jsx.evalFile('../myFolder/foo.jsx'); // where a relative or absolute file path is given                                       //\n    //                                                                                                                               //\n    // using conventional methods one would use in the case were the values to swap were supplied by variables                       //\n    // csInterface.evalScript('var q = \"' + name + '\"; alert(\"' + myString + '\" ' + myOp + ' q);q;', callback);                      //\n    // Using all the '' + foo + '' is very error prone                                                                               //\n    // jsx.evalScript('var q = \"__name__\"; alert(__string__ __opp__ q);q;',{'name':'Fred', 'string':'Hello ', 'opp':'+'}, callBack); //\n    // is much simpler and less error prone                                                                                          //\n    //                                                                                                                               //\n    // more readable to use object                                                                                                   //\n    // jsx.evalFile({                                                                                                                //\n    //      file: 'yetAnotherFabScript.jsx',                                                                                         //\n    //      replacements: {\"this\": foo, That: bar, and: \"&&\", the: foo2, other: bar2},                                               //\n    //      eval: true                                                                                                               //\n    // })                                                                                                                            //\n    // Enjoy <span class=\"wp-font-emots-emo-happy\"></span>                                                                                                                     //\n    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n\n    jsx = new Jsx();\n})();\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/extension/host/index.jsx",
    "content": "﻿#include \"json.js\";\n#target photoshop\n\nvar LogFactory=function(file,write,store,level,defaultStatus,continuing){if(file&&(file.constructor===String||file.constructor===File)){file={file:file};}else if(!file)file={file:{}};write=(file.write!==undefined)?file.write:write;if(write===undefined){write=true;}store=(file.store!==undefined)?file.store||false:store||false;level=(file.level!==undefined)?file.level:level;defaultStatus=(file.defaultStatus!==undefined)?file.defaultStatus:defaultStatus;if(defaultStatus===undefined){defaultStatus='LOG';}continuing=(file.continuing!==undefined)?file.continuing:continuing||false;file=file.file||{};var stack,times,logTime,logPoint,icons,statuses,LOG_LEVEL,LOG_STATUS;stack=[];times=[];logTime=new Date();logPoint='Log Factory Start';icons={\"1\":\"\\ud83d\\udd50\",\"130\":\"\\ud83d\\udd5c\",\"2\":\"\\ud83d\\udd51\",\"230\":\"\\ud83d\\udd5d\",\"3\":\"\\ud83d\\udd52\",\"330\":\"\\ud83d\\udd5e\",\"4\":\"\\ud83d\\udd53\",\"430\":\"\\ud83d\\udd5f\",\"5\":\"\\ud83d\\udd54\",\"530\":\"\\ud83d\\udd60\",\"6\":\"\\ud83d\\udd55\",\"630\":\"\\ud83d\\udd61\",\"7\":\"\\ud83d\\udd56\",\"730\":\"\\ud83d\\udd62\",\"8\":\"\\ud83d\\udd57\",\"830\":\"\\ud83d\\udd63\",\"9\":\"\\ud83d\\udd58\",\"930\":\"\\ud83d\\udd64\",\"10\":\"\\ud83d\\udd59\",\"1030\":\"\\ud83d\\udd65\",\"11\":\"\\ud83d\\udd5a\",\"1130\":\"\\ud83d\\udd66\",\"12\":\"\\ud83d\\udd5b\",\"1230\":\"\\ud83d\\udd67\",\"AIRPLANE\":\"\\ud83d\\udee9\",\"ALARM\":\"\\u23f0\",\"AMBULANCE\":\"\\ud83d\\ude91\",\"ANCHOR\":\"\\u2693\",\"ANGRY\":\"\\ud83d\\ude20\",\"ANGUISHED\":\"\\ud83d\\ude27\",\"ANT\":\"\\ud83d\\udc1c\",\"ANTENNA\":\"\\ud83d\\udce1\",\"APPLE\":\"\\ud83c\\udf4f\",\"APPLE2\":\"\\ud83c\\udf4e\",\"ATM\":\"\\ud83c\\udfe7\",\"ATOM\":\"\\u269b\",\"BABYBOTTLE\":\"\\ud83c\\udf7c\",\"BAD:\":\"\\ud83d\\udc4e\",\"BANANA\":\"\\ud83c\\udf4c\",\"BANDAGE\":\"\\ud83e\\udd15\",\"BANK\":\"\\ud83c\\udfe6\",\"BATTERY\":\"\\ud83d\\udd0b\",\"BED\":\"\\ud83d\\udecf\",\"BEE\":\"\\ud83d\\udc1d\",\"BEER\":\"\\ud83c\\udf7a\",\"BELL\":\"\\ud83d\\udd14\",\"BELLOFF\":\"\\ud83d\\udd15\",\"BIRD\":\"\\ud83d\\udc26\",\"BLACKFLAG\":\"\\ud83c\\udff4\",\"BLUSH\":\"\\ud83d\\ude0a\",\"BOMB\":\"\\ud83d\\udca3\",\"BOOK\":\"\\ud83d\\udcd5\",\"BOOKMARK\":\"\\ud83d\\udd16\",\"BOOKS\":\"\\ud83d\\udcda\",\"BOW\":\"\\ud83c\\udff9\",\"BOWLING\":\"\\ud83c\\udfb3\",\"BRIEFCASE\":\"\\ud83d\\udcbc\",\"BROKEN\":\"\\ud83d\\udc94\",\"BUG\":\"\\ud83d\\udc1b\",\"BUILDING\":\"\\ud83c\\udfdb\",\"BUILDINGS\":\"\\ud83c\\udfd8\",\"BULB\":\"\\ud83d\\udca1\",\"BUS\":\"\\ud83d\\ude8c\",\"CACTUS\":\"\\ud83c\\udf35\",\"CALENDAR\":\"\\ud83d\\udcc5\",\"CAMEL\":\"\\ud83d\\udc2a\",\"CAMERA\":\"\\ud83d\\udcf7\",\"CANDLE\":\"\\ud83d\\udd6f\",\"CAR\":\"\\ud83d\\ude98\",\"CAROUSEL\":\"\\ud83c\\udfa0\",\"CASTLE\":\"\\ud83c\\udff0\",\"CATEYES\":\"\\ud83d\\ude3b\",\"CATJOY\":\"\\ud83d\\ude39\",\"CATMOUTH\":\"\\ud83d\\ude3a\",\"CATSMILE\":\"\\ud83d\\ude3c\",\"CD\":\"\\ud83d\\udcbf\",\"CHECK\":\"\\u2714\",\"CHEQFLAG\":\"\\ud83c\\udfc1\",\"CHICK\":\"\\ud83d\\udc25\",\"CHICKEN\":\"\\ud83d\\udc14\",\"CHICKHEAD\":\"\\ud83d\\udc24\",\"CIRCLEBLACK\":\"\\u26ab\",\"CIRCLEBLUE\":\"\\ud83d\\udd35\",\"CIRCLERED\":\"\\ud83d\\udd34\",\"CIRCLEWHITE\":\"\\u26aa\",\"CIRCUS\":\"\\ud83c\\udfaa\",\"CLAPPER\":\"\\ud83c\\udfac\",\"CLAPPING\":\"\\ud83d\\udc4f\",\"CLIP\":\"\\ud83d\\udcce\",\"CLIPBOARD\":\"\\ud83d\\udccb\",\"CLOUD\":\"\\ud83c\\udf28\",\"CLOVER\":\"\\ud83c\\udf40\",\"CLOWN\":\"\\ud83e\\udd21\",\"COLDSWEAT\":\"\\ud83d\\ude13\",\"COLDSWEAT2\":\"\\ud83d\\ude30\",\"COMPRESS\":\"\\ud83d\\udddc\",\"CONFOUNDED\":\"\\ud83d\\ude16\",\"CONFUSED\":\"\\ud83d\\ude15\",\"CONSTRUCTION\":\"\\ud83d\\udea7\",\"CONTROL\":\"\\ud83c\\udf9b\",\"COOKIE\":\"\\ud83c\\udf6a\",\"COOKING\":\"\\ud83c\\udf73\",\"COOL\":\"\\ud83d\\ude0e\",\"COOLBOX\":\"\\ud83c\\udd92\",\"COPYRIGHT\":\"\\u00a9\",\"CRANE\":\"\\ud83c\\udfd7\",\"CRAYON\":\"\\ud83d\\udd8d\",\"CREDITCARD\":\"\\ud83d\\udcb3\",\"CROSS\":\"\\u2716\",\"CROSSBOX:\":\"\\u274e\",\"CRY\":\"\\ud83d\\ude22\",\"CRYCAT\":\"\\ud83d\\ude3f\",\"CRYSTALBALL\":\"\\ud83d\\udd2e\",\"CUSTOMS\":\"\\ud83d\\udec3\",\"DELICIOUS\":\"\\ud83d\\ude0b\",\"DERELICT\":\"\\ud83c\\udfda\",\"DESKTOP\":\"\\ud83d\\udda5\",\"DIAMONDLB\":\"\\ud83d\\udd37\",\"DIAMONDLO\":\"\\ud83d\\udd36\",\"DIAMONDSB\":\"\\ud83d\\udd39\",\"DIAMONDSO\":\"\\ud83d\\udd38\",\"DICE\":\"\\ud83c\\udfb2\",\"DISAPPOINTED\":\"\\ud83d\\ude1e\",\"CRY2\":\"\\ud83d\\ude25\",\"DIVISION\":\"\\u2797\",\"DIZZY\":\"\\ud83d\\ude35\",\"DOLLAR\":\"\\ud83d\\udcb5\",\"DOLLAR2\":\"\\ud83d\\udcb2\",\"DOWNARROW\":\"\\u2b07\",\"DVD\":\"\\ud83d\\udcc0\",\"EJECT\":\"\\u23cf\",\"ELEPHANT\":\"\\ud83d\\udc18\",\"EMAIL\":\"\\ud83d\\udce7\",\"ENVELOPE\":\"\\ud83d\\udce8\",\"ENVELOPE2\":\"\\u2709\",\"ENVELOPE_DOWN\":\"\\ud83d\\udce9\",\"EURO\":\"\\ud83d\\udcb6\",\"EVIL\":\"\\ud83d\\ude08\",\"EXPRESSIONLESS\":\"\\ud83d\\ude11\",\"EYES\":\"\\ud83d\\udc40\",\"FACTORY\":\"\\ud83c\\udfed\",\"FAX\":\"\\ud83d\\udce0\",\"FEARFUL\":\"\\ud83d\\ude28\",\"FILEBOX\":\"\\ud83d\\uddc3\",\"FILECABINET\":\"\\ud83d\\uddc4\",\"FIRE\":\"\\ud83d\\udd25\",\"FIREENGINE\":\"\\ud83d\\ude92\",\"FIST\":\"\\ud83d\\udc4a\",\"FLOWER\":\"\\ud83c\\udf37\",\"FLOWER2\":\"\\ud83c\\udf38\",\"FLUSHED\":\"\\ud83d\\ude33\",\"FOLDER\":\"\\ud83d\\udcc1\",\"FOLDER2\":\"\\ud83d\\udcc2\",\"FREE\":\"\\ud83c\\udd93\",\"FROG\":\"\\ud83d\\udc38\",\"FROWN\":\"\\ud83d\\ude41\",\"GEAR\":\"\\u2699\",\"GLOBE\":\"\\ud83c\\udf0d\",\"GLOWINGSTAR\":\"\\ud83c\\udf1f\",\"GOOD:\":\"\\ud83d\\udc4d\",\"GRIMACING\":\"\\ud83d\\ude2c\",\"GRIN\":\"\\ud83d\\ude00\",\"GRINNINGCAT\":\"\\ud83d\\ude38\",\"HALO\":\"\\ud83d\\ude07\",\"HAMMER\":\"\\ud83d\\udd28\",\"HAMSTER\":\"\\ud83d\\udc39\",\"HAND\":\"\\u270b\",\"HANDDOWN\":\"\\ud83d\\udc47\",\"HANDLEFT\":\"\\ud83d\\udc48\",\"HANDRIGHT\":\"\\ud83d\\udc49\",\"HANDUP\":\"\\ud83d\\udc46\",\"HATCHING\":\"\\ud83d\\udc23\",\"HAZARD\":\"\\u2623\",\"HEADPHONE\":\"\\ud83c\\udfa7\",\"HEARNOEVIL\":\"\\ud83d\\ude49\",\"HEARTBLUE\":\"\\ud83d\\udc99\",\"HEARTEYES\":\"\\ud83d\\ude0d\",\"HEARTGREEN\":\"\\ud83d\\udc9a\",\"HEARTYELLOW\":\"\\ud83d\\udc9b\",\"HELICOPTER\":\"\\ud83d\\ude81\",\"HERB\":\"\\ud83c\\udf3f\",\"HIGH_BRIGHTNESS\":\"\\ud83d\\udd06\",\"HIGHVOLTAGE\":\"\\u26a1\",\"HIT\":\"\\ud83c\\udfaf\",\"HONEY\":\"\\ud83c\\udf6f\",\"HOT\":\"\\ud83c\\udf36\",\"HOURGLASS\":\"\\u23f3\",\"HOUSE\":\"\\ud83c\\udfe0\",\"HUGGINGFACE\":\"\\ud83e\\udd17\",\"HUNDRED\":\"\\ud83d\\udcaf\",\"HUSHED\":\"\\ud83d\\ude2f\",\"ID\":\"\\ud83c\\udd94\",\"INBOX\":\"\\ud83d\\udce5\",\"INDEX\":\"\\ud83d\\uddc2\",\"JOY\":\"\\ud83d\\ude02\",\"KEY\":\"\\ud83d\\udd11\",\"KISS\":\"\\ud83d\\ude18\",\"KISS2\":\"\\ud83d\\ude17\",\"KISS3\":\"\\ud83d\\ude19\",\"KISS4\":\"\\ud83d\\ude1a\",\"KISSINGCAT\":\"\\ud83d\\ude3d\",\"KNIFE\":\"\\ud83d\\udd2a\",\"LABEL\":\"\\ud83c\\udff7\",\"LADYBIRD\":\"\\ud83d\\udc1e\",\"LANDING\":\"\\ud83d\\udeec\",\"LAPTOP\":\"\\ud83d\\udcbb\",\"LEFTARROW\":\"\\u2b05\",\"LEMON\":\"\\ud83c\\udf4b\",\"LIGHTNINGCLOUD\":\"\\ud83c\\udf29\",\"LINK\":\"\\ud83d\\udd17\",\"LITTER\":\"\\ud83d\\udeae\",\"LOCK\":\"\\ud83d\\udd12\",\"LOLLIPOP\":\"\\ud83c\\udf6d\",\"LOUDSPEAKER\":\"\\ud83d\\udce2\",\"LOW_BRIGHTNESS\":\"\\ud83d\\udd05\",\"MAD\":\"\\ud83d\\ude1c\",\"MAGNIFYING_GLASS\":\"\\ud83d\\udd0d\",\"MASK\":\"\\ud83d\\ude37\",\"MEDAL\":\"\\ud83c\\udf96\",\"MEMO\":\"\\ud83d\\udcdd\",\"MIC\":\"\\ud83c\\udfa4\",\"MICROSCOPE\":\"\\ud83d\\udd2c\",\"MINUS\":\"\\u2796\",\"MOBILE\":\"\\ud83d\\udcf1\",\"MONEY\":\"\\ud83d\\udcb0\",\"MONEYMOUTH\":\"\\ud83e\\udd11\",\"MONKEY\":\"\\ud83d\\udc35\",\"MOUSE\":\"\\ud83d\\udc2d\",\"MOUSE2\":\"\\ud83d\\udc01\",\"MOUTHLESS\":\"\\ud83d\\ude36\",\"MOVIE\":\"\\ud83c\\udfa5\",\"MUGS\":\"\\ud83c\\udf7b\",\"NERD\":\"\\ud83e\\udd13\",\"NEUTRAL\":\"\\ud83d\\ude10\",\"NEW\":\"\\ud83c\\udd95\",\"NOENTRY\":\"\\ud83d\\udeab\",\"NOTEBOOK\":\"\\ud83d\\udcd4\",\"NOTEPAD\":\"\\ud83d\\uddd2\",\"NUTANDBOLT\":\"\\ud83d\\udd29\",\"O\":\"\\u2b55\",\"OFFICE\":\"\\ud83c\\udfe2\",\"OK\":\"\\ud83c\\udd97\",\"OKHAND\":\"\\ud83d\\udc4c\",\"OLDKEY\":\"\\ud83d\\udddd\",\"OPENLOCK\":\"\\ud83d\\udd13\",\"OPENMOUTH\":\"\\ud83d\\ude2e\",\"OUTBOX\":\"\\ud83d\\udce4\",\"PACKAGE\":\"\\ud83d\\udce6\",\"PAGE\":\"\\ud83d\\udcc4\",\"PAINTBRUSH\":\"\\ud83d\\udd8c\",\"PALETTE\":\"\\ud83c\\udfa8\",\"PANDA\":\"\\ud83d\\udc3c\",\"PASSPORT\":\"\\ud83d\\udec2\",\"PAWS\":\"\\ud83d\\udc3e\",\"PEN\":\"\\ud83d\\udd8a\",\"PEN2\":\"\\ud83d\\udd8b\",\"PENSIVE\":\"\\ud83d\\ude14\",\"PERFORMING\":\"\\ud83c\\udfad\",\"PHONE\":\"\\ud83d\\udcde\",\"PILL\":\"\\ud83d\\udc8a\",\"PING\":\"\\u2757\",\"PLATE\":\"\\ud83c\\udf7d\",\"PLUG\":\"\\ud83d\\udd0c\",\"PLUS\":\"\\u2795\",\"POLICE\":\"\\ud83d\\ude93\",\"POLICELIGHT\":\"\\ud83d\\udea8\",\"POSTOFFICE\":\"\\ud83c\\udfe4\",\"POUND\":\"\\ud83d\\udcb7\",\"POUTING\":\"\\ud83d\\ude21\",\"POUTINGCAT\":\"\\ud83d\\ude3e\",\"PRESENT\":\"\\ud83c\\udf81\",\"PRINTER\":\"\\ud83d\\udda8\",\"PROJECTOR\":\"\\ud83d\\udcfd\",\"PUSHPIN\":\"\\ud83d\\udccc\",\"QUESTION\":\"\\u2753\",\"RABBIT\":\"\\ud83d\\udc30\",\"RADIOACTIVE\":\"\\u2622\",\"RADIOBUTTON\":\"\\ud83d\\udd18\",\"RAINCLOUD\":\"\\ud83c\\udf27\",\"RAT\":\"\\ud83d\\udc00\",\"RECYCLE\":\"\\u267b\",\"REGISTERED\":\"\\u00ae\",\"RELIEVED\":\"\\ud83d\\ude0c\",\"ROBOT\":\"\\ud83e\\udd16\",\"ROCKET\":\"\\ud83d\\ude80\",\"ROLLING\":\"\\ud83d\\ude44\",\"ROOSTER\":\"\\ud83d\\udc13\",\"RULER\":\"\\ud83d\\udccf\",\"SATELLITE\":\"\\ud83d\\udef0\",\"SAVE\":\"\\ud83d\\udcbe\",\"SCHOOL\":\"\\ud83c\\udfeb\",\"SCISSORS\":\"\\u2702\",\"SCREAMING\":\"\\ud83d\\ude31\",\"SCROLL\":\"\\ud83d\\udcdc\",\"SEAT\":\"\\ud83d\\udcba\",\"SEEDLING\":\"\\ud83c\\udf31\",\"SEENOEVIL\":\"\\ud83d\\ude48\",\"SHIELD\":\"\\ud83d\\udee1\",\"SHIP\":\"\\ud83d\\udea2\",\"SHOCKED\":\"\\ud83d\\ude32\",\"SHOWER\":\"\\ud83d\\udebf\",\"SLEEPING\":\"\\ud83d\\ude34\",\"SLEEPY\":\"\\ud83d\\ude2a\",\"SLIDER\":\"\\ud83c\\udf9a\",\"SLOT\":\"\\ud83c\\udfb0\",\"SMILE\":\"\\ud83d\\ude42\",\"SMILING\":\"\\ud83d\\ude03\",\"SMILINGCLOSEDEYES\":\"\\ud83d\\ude06\",\"SMILINGEYES\":\"\\ud83d\\ude04\",\"SMILINGSWEAT\":\"\\ud83d\\ude05\",\"SMIRK\":\"\\ud83d\\ude0f\",\"SNAIL\":\"\\ud83d\\udc0c\",\"SNAKE\":\"\\ud83d\\udc0d\",\"SOCCER\":\"\\u26bd\",\"SOS\":\"\\ud83c\\udd98\",\"SPEAKER\":\"\\ud83d\\udd08\",\"SPEAKEROFF\":\"\\ud83d\\udd07\",\"SPEAKNOEVIL\":\"\\ud83d\\ude4a\",\"SPIDER\":\"\\ud83d\\udd77\",\"SPIDERWEB\":\"\\ud83d\\udd78\",\"STAR\":\"\\u2b50\",\"STOP\":\"\\u26d4\",\"STOPWATCH\":\"\\u23f1\",\"SULK\":\"\\ud83d\\ude26\",\"SUNFLOWER\":\"\\ud83c\\udf3b\",\"SUNGLASSES\":\"\\ud83d\\udd76\",\"SYRINGE\":\"\\ud83d\\udc89\",\"TAKEOFF\":\"\\ud83d\\udeeb\",\"TAXI\":\"\\ud83d\\ude95\",\"TELESCOPE\":\"\\ud83d\\udd2d\",\"TEMPORATURE\":\"\\ud83e\\udd12\",\"TENNIS\":\"\\ud83c\\udfbe\",\"THERMOMETER\":\"\\ud83c\\udf21\",\"THINKING\":\"\\ud83e\\udd14\",\"THUNDERCLOUD\":\"\\u26c8\",\"TICKBOX\":\"\\u2705\",\"TICKET\":\"\\ud83c\\udf9f\",\"TIRED\":\"\\ud83d\\ude2b\",\"TOILET\":\"\\ud83d\\udebd\",\"TOMATO\":\"\\ud83c\\udf45\",\"TONGUE\":\"\\ud83d\\ude1b\",\"TOOLS\":\"\\ud83d\\udee0\",\"TORCH\":\"\\ud83d\\udd26\",\"TORNADO\":\"\\ud83c\\udf2a\",\"TOUNG2\":\"\\ud83d\\ude1d\",\"TRADEMARK\":\"\\u2122\",\"TRAFFICLIGHT\":\"\\ud83d\\udea6\",\"TRASH\":\"\\ud83d\\uddd1\",\"TREE\":\"\\ud83c\\udf32\",\"TRIANGLE_LEFT\":\"\\u25c0\",\"TRIANGLE_RIGHT\":\"\\u25b6\",\"TRIANGLEDOWN\":\"\\ud83d\\udd3b\",\"TRIANGLEUP\":\"\\ud83d\\udd3a\",\"TRIANGULARFLAG\":\"\\ud83d\\udea9\",\"TROPHY\":\"\\ud83c\\udfc6\",\"TRUCK\":\"\\ud83d\\ude9a\",\"TRUMPET\":\"\\ud83c\\udfba\",\"TURKEY\":\"\\ud83e\\udd83\",\"TURTLE\":\"\\ud83d\\udc22\",\"UMBRELLA\":\"\\u26f1\",\"UNAMUSED\":\"\\ud83d\\ude12\",\"UPARROW\":\"\\u2b06\",\"UPSIDEDOWN\":\"\\ud83d\\ude43\",\"WARNING\":\"\\u26a0\",\"WATCH\":\"\\u231a\",\"WAVING\":\"\\ud83d\\udc4b\",\"WEARY\":\"\\ud83d\\ude29\",\"WEARYCAT\":\"\\ud83d\\ude40\",\"WHITEFLAG\":\"\\ud83c\\udff3\",\"WINEGLASS\":\"\\ud83c\\udf77\",\"WINK\":\"\\ud83d\\ude09\",\"WORRIED\":\"\\ud83d\\ude1f\",\"WRENCH\":\"\\ud83d\\udd27\",\"X\":\"\\u274c\",\"YEN\":\"\\ud83d\\udcb4\",\"ZIPPERFACE\":\"\\ud83e\\udd10\",\"UNDEFINED\":\"\",\"\":\"\"};statuses={F:'FATAL',B:'BUG',C:'CRITICAL',E:'ERROR',W:'WARNING',I:'INFO',IM:'IMPORTANT',D:'DEBUG',L:'LOG',CO:'CONSTANT',FU:'FUNCTION',R:'RETURN',V:'VARIABLE',S:'STACK',RE:'RESULT',ST:'STOPPER',TI:'TIMER',T:'TRACE'};LOG_LEVEL={NONE:7,OFF:7,FATAL:6,ERROR:5,WARN:4,INFO:3,UNDEFINED:2,'':2,DEFAULT:2,DEBUG:2,TRACE:1,ON:0,ALL:0,};LOG_STATUS={OFF:LOG_LEVEL.OFF,NONE:LOG_LEVEL.OFF,NO:LOG_LEVEL.OFF,NOPE:LOG_LEVEL.OFF,FALSE:LOG_LEVEL.OFF,FATAL:LOG_LEVEL.FATAL,BUG:LOG_LEVEL.ERROR,CRITICAL:LOG_LEVEL.ERROR,ERROR:LOG_LEVEL.ERROR,WARNING:LOG_LEVEL.WARN,INFO:LOG_LEVEL.INFO,IMPORTANT:LOG_LEVEL.INFO,DEBUG:LOG_LEVEL.DEBUG,LOG:LOG_LEVEL.DEBUG,STACK:LOG_LEVEL.DEBUG,CONSTANT:LOG_LEVEL.DEBUG,FUNCTION:LOG_LEVEL.DEBUG,VARIABLE:LOG_LEVEL.DEBUG,RETURN:LOG_LEVEL.DEBUG,RESULT:LOG_LEVEL.TRACE,STOPPER:LOG_LEVEL.TRACE,TIMER:LOG_LEVEL.TRACE,TRACE:LOG_LEVEL.TRACE,ALL:LOG_LEVEL.ALL,YES:LOG_LEVEL.ALL,YEP:LOG_LEVEL.ALL,TRUE:LOG_LEVEL.ALL};var logFile,logFolder;var LOG=function(message,status,icon){if(LOG.level!==LOG_LEVEL.OFF&&(LOG.write||LOG.store)&&LOG.arguments.length)return LOG.addMessage(message,status,icon);};LOG.logDecodeLevel=function(level){if(level==~~level)return Math.abs(level);var lev;level+='';level=level.toUpperCase();if(level in statuses){level=statuses[level];}lev=LOG_LEVEL[level];if(lev!==undefined)return lev;lev=LOG_STATUS[level];if(lev!==undefined)return lev;return LOG_LEVEL.DEFAULT;};LOG.write=write;LOG.store=store;LOG.level=LOG.logDecodeLevel(level);LOG.status=defaultStatus;LOG.addMessage=function(message,status,icon){var date=new Date(),count,bool,logStatus;if(status&&status.constructor.name==='String'){status=status.toUpperCase();status=statuses[status]||status;}else status=LOG.status;logStatus=LOG_STATUS[status]||LOG_STATUS.ALL;if(logStatus<LOG.level)return;date=' \\t['+date+' '+date.getMilliseconds()+'ms]';status='['+status+'] ';if(status.length<11)status=(status+'           ').substr(0,11);if(icon){icon=(''+icon).toUpperCase();icon=(icon in icons&&icons[icon])||'';}else{icon='';}if(LOG.count!==~~LOG.count){LOG.count=1;}count=(LOG.count>999)?'['+LOG.count+'] ':('   ['+LOG.count+'] ').slice(-7);message=count+status+icon+(message instanceof Object?message.toSource():message)+date;if(LOG.store){stack.push(message);}if(LOG.write){bool=file&&file.writable&&logFile.writeln(message);if(!bool){file.writable=true;LOG.setFile(logFile);logFile.writeln(message);}}LOG.count++;return true;};var logNewFile=function(file,isCookie,overwrite){file.encoding='UTF-8';file.lineFeed=($.os[0]=='M')?'Macintosh':' Windows';if(isCookie)return file.open(overwrite?'w':'e')&&file;file.writable=LOG.write;logFile=file;logFolder=file.parent;if(continuing){LOG.count=LOG.setCount(file);}return(!LOG.write&&file||(file.open('a')&&file));};LOG.setFile=function(file,isCookie,overwrite){var bool,folder,fileName,suffix,newFileName,f,d,safeFileName;d=new Date();f=$.stack.split(\"\\n\")[0].replace(/^\\[\\(?/,'').replace(/\\)?\\]$/,'');if(f==~~f){f=$.fileName.replace(/[^\\/]+\\//g,'');}safeFileName=File.encode((isCookie?'/COOKIE_':'/LOG_')+f.replace(/^\\//,'')+'_'+(1900+d.getYear())+(''+d).replace(/...(...)(..).+/,'_$1_$2')+(isCookie?'.txt':'.log'));if(file&&file.constructor.name=='String'){file=(file.match('/'))?new File(file):new File((logFolder||Folder.temp)+'/'+file);}if(file instanceof File){folder=file.parent;bool=folder.exists||folder.create();if(!bool)folder=Folder.temp;fileName=File.decode(file.name);suffix=fileName.match(/\\.[^.]+$/);suffix=suffix?suffix[0]:'';fileName='/'+fileName;newFileName=fileName.replace(/\\.[^.]+$/,'')+'_'+(+(new Date())+suffix);f=logNewFile(file,isCookie,overwrite);if(f)return f;f=logNewFile(new File(folder+newFileName),isCookie,overwrite);if(f)return f;f=logNewFile(new File(folder+safeFileName),isCookie,overwrite);if(f)return f;if(folder!=Folder.temp){f=logNewFile(new File(Folder.temp+fileName),isCookie,overwrite);if(f)return f;f=logNewFile(new File(Folder.temp+safeFileName),isCookie,overwrite);return f||new File(Folder.temp+safeFileName);}}return LOG.setFile(((logFile&&!isCookie)?new File(logFile):new File(Folder.temp+safeFileName)),isCookie,overwrite );};LOG.setCount=function(file){if(~~file===file){LOG.count=file;return LOG.count;}if(file===undefined){file=logFile;}if(file&&file.constructor===String){file=new File(file);}var logNumbers,contents;if(!file.length||!file.exists){LOG.count=1;return 1;}file.open('r');file.encoding='utf-8';file.seek(10000,2);contents='\\n'+file.read();logNumbers=contents.match(/\\n{0,3}\\[\\d+\\] \\[\\w+\\]+/g);if(logNumbers){logNumbers=+logNumbers[logNumbers.length-1].match(/\\d+/)+1;file.close();LOG.count=logNumbers;return logNumbers;}if(file.length<10001){file.close();LOG.count=1;return 1;}file.seek(10000000,2);contents='\\n'+file.read();logNumbers=contents.match(/\\n{0,3}\\[\\d+\\] \\[\\w+\\]+/g);if(logNumbers){logNumbers=+logNumbers[logNumbers.length-1].match(/\\d+/)+1;file.close();LOG.count=logNumbers;return logNumbers;}file.close();LOG.count=1;return 1;};LOG.setLevel=function(level){LOG.level=LOG.logDecodeLevel(level);return LOG.level;};LOG.setStatus=function(status){status=(''+status).toUpperCase();LOG.status=statuses[status]||status;return LOG.status;};LOG.cookie=function(file,level,overwrite,setLevel){var log,cookie;if(!file){file={file:file};}if(file&&(file.constructor===String||file.constructor===File)){file={file:file};}log=file;if(log.level===undefined){log.level=(level!==undefined)?level:'NONE';}if(log.overwrite===undefined){log.overwrite=(overwrite!==undefined)?overwrite:false;}if(log.setLevel===undefined){log.setLevel=(setLevel!==undefined)?setLevel:true;}setLevel=log.setLevel;overwrite=log.overwrite;level=log.level;file=log.file;file=LOG.setFile(file,true,overwrite);if(overwrite){file.write(level);}else{cookie=file.read();if(cookie.length){level=cookie;}else{file.write(level);}}file.close();if(setLevel){LOG.setLevel(level);}return{path:file,level:level};};LOG.args=function(args,funct,line){if(LOG.level>LOG_STATUS.FUNCTION)return;if(!(args&&(''+args.constructor).replace(/\\s+/g,'')==='functionObject(){[nativecode]}'))return;if(!LOG.args.STRIP_COMMENTS){LOG.args.STRIP_COMMENTS=/((\\/.*$)|(\\/\\*[\\s\\S]*?\\*\\/))/mg;}if(!LOG.args.ARGUMENT_NAMES){LOG.args.ARGUMENT_NAMES=/([^\\s,]+)/g;}if(!LOG.args.OUTER_BRACKETS){LOG.args.OUTER_BRACKETS=/^\\((.+)?\\)$/;}if(!LOG.args.NEW_SOMETHING){LOG.args.NEW_SOMETHING=/^new \\w+\\((.+)?\\)$/;}var functionString,argumentNames,stackInfo,report,functionName,arg,argsL,n,argName,argValue,argsTotal;if(funct===~~funct){line=funct;}if(!(funct instanceof Function)){funct=args.callee;}if(!(funct instanceof Function))return;functionName=funct.name;functionString=(''+funct).replace(LOG.args.STRIP_COMMENTS,'');argumentNames=functionString.slice(functionString.indexOf('(')+1,functionString.indexOf(')')).match(LOG.args.ARGUMENT_NAMES);argumentNames=argumentNames||[];report=[];report.push('--------------');report.push('Function Data:');report.push('--------------');report.push('Function Name:'+functionName);argsL=args.length;stackInfo=$.stack.split(/[\\n\\r]/);stackInfo.pop();stackInfo=stackInfo.join('\\n                              ');report.push('Call stack:'+stackInfo);if(line){report.push('Function Line around:'+line);}report.push('Arguments Provided:'+argsL);report.push('Named Arguments:'+argumentNames.length);if(argumentNames.length){report.push('Arguments Names:'+argumentNames.join(','));}if(argsL){report.push('----------------');report.push('Argument Values:');report.push('----------------');}argsTotal=Math.max(argsL,argumentNames.length);for(n=0;n<argsTotal;n++){argName=argumentNames[n];arg=args[n];if(n>=argsL){argValue='NO VALUE PROVIDED';}else if(arg===undefined){argValue='undefined';}else if(arg===null){argValue='null';}else{argValue=arg.toSource().replace(LOG.args.OUTER_BRACKETS,'$1').replace(LOG.args.NEW_SOMETHING,'$1');}report.push((argName?argName:'arguments['+n+']')+':'+argValue);}report.push('');report=report.join('\\n                  ');LOG(report,'f');return report;};LOG.stack=function(reverse){var st=$.stack.split('\\n');st.pop();st.pop();if(reverse){st.reverse();}return LOG(st.join('\\n                  '),'s');};LOG.values=function(values){var n,value,map=[];if(!(values instanceof Object||values instanceof Array)){return;}if(!LOG.values.OUTER_BRACKETS){LOG.values.OUTER_BRACKETS=/^\\((.+)?\\)$/;}if(!LOG.values.NEW_SOMETHING){LOG.values.NEW_SOMETHING=/^new \\w+\\((.+)?\\)$/;}for(n in values){try{value=values[n];if(value===undefined){value='undefined';}else if(value===null){value='null';}else{value=value.toSource().replace(LOG.values.OUTER_BRACKETS,'$1').replace(LOG.values.NEW_SOMETHING,'$1');}}catch(e){value='\\uD83D\\uDEAB '+e;}map.push(n+':'+value);}if(map.length){map=map.join('\\n                  ')+'\\n                  ';return LOG(map,'v');}};LOG.reset=function(all){stack.length=0;LOG.count=1;if(all!==false){if(logFile instanceof File){logFile.close();}logFile=LOG.store=LOG.writeToFile=undefined;LOG.write=true;logFolder=Folder.temp;logTime=new Date();logPoint='After Log Reset';}};LOG.stopper=function(message){var newLogTime,t,m,newLogPoint;newLogTime=new Date();newLogPoint=(LOG.count!==undefined)?'LOG#'+LOG.count:'BEFORE LOG#1';LOG.time=t=newLogTime-logTime;if(message===false){return;}message=message||'Stopper start point';t=LOG.prettyTime(t);m=message+'\\n                  '+'From '+logPoint+' to '+newLogPoint+' took '+t+' Starting '+logTime+' '+logTime.getMilliseconds()+'ms'+' Ending '+newLogTime+' '+newLogTime.getMilliseconds()+'ms';LOG(m,'st');logPoint=newLogPoint;logTime=newLogTime;return m;};LOG.start=function(message){var t=new Date();times.push([t,(message!==undefined)?message+'':'']);};LOG.stop=function(message){if(!times.length)return;message=(message)?message+' ':'';var nt,startLog,ot,om,td,m;nt=new Date();startLog=times.pop();ot=startLog[0];om=startLog[1];td=nt-ot;if(om.length){om+=' ';}m=om+'STARTED ['+ot+' '+ot.getMilliseconds()+'ms]\\n                  '+message+'FINISHED ['+nt+' '+nt.getMilliseconds()+'ms]\\n                  TOTAL TIME ['+LOG.prettyTime(td)+']';LOG(m,'ti');return m;};LOG.prettyTime=function(t){var h,m,s,ms;h=Math.floor(t / 3600000);m=Math.floor((t % 3600000)/ 60000);s=Math.floor((t % 60000)/ 1000);ms=t % 1000;t=(!t)?'<1ms':((h)?h+' hours ':'')+((m)?m+' minutes ':'')+((s)?s+' seconds ':'')+((ms&&(h||m||s))?'&':'')+((ms)?ms+'ms':'');return t;};LOG.get=function(){if(!stack.length)return 'THE LOG IS NOT SET TO STORE';var a=fetchLogLines(arguments);return a?'\\n'+a.join('\\n'):'NO LOGS AVAILABLE';};var fetchLogLines=function(){var args=arguments[0];if(!args.length)return stack;var c,n,l,a=[],ln,start,end,j,sl;l=args.length;sl=stack.length-1;n=0;for(c=0;c<l;c++){ln=args[c];if(~~ln===ln){ln=(0>ln)?sl+ln+1:ln-1;if(ln>=0&&ln<=sl)a[n++]=stack[ln];}else if(ln instanceof Array&&ln.length===2){start=ln[0];end=ln[1];if(!(~~start===start&&~~end===end))continue;start=(0>start)?sl+start+1:start-1;end=(0>end)?sl+end+1:end-1;start=Math.max(Math.min(sl,start),0);end=Math.min(Math.max(end,0),sl);if(start<=end)for(j=start;j<=end;j++)a[n++]=stack[j];else for(j=start;j>=end;j--)a[n++]=stack[j];}}return(n)?a:false;};LOG.file=function(){return logFile;};LOG.openFolder=function(){if(logFolder)return logFolder.execute();};LOG.show=LOG.execute=function(){if(logFile)return logFile.execute();};LOG.close=function(){if(logFile)return logFile.close();};LOG.setFile(file);if(!$.summary.difference){$.summary.difference=function(){return $.summary().replace(/ *([0-9]+)([^ ]+)(\\n?)/g,$.summary.updateSnapshot );};}if(!$.summary.updateSnapshot){$.summary.updateSnapshot=function(full,count,name,lf){var snapshot=$.summary.snapshot;count=Number(count);var prev=snapshot[name]?snapshot[name]:0;snapshot[name]=count;var diff=count-prev;if(diff===0)return \"\";return \"     \".substring(String(diff).length)+diff+\" \"+name+lf;};}if(!$.summary.snapshot){$.summary.snapshot=[];$.summary.difference();}$.gc();$.gc();$.summary.difference();LOG.sumDiff=function(message){$.gc();$.gc();var diff=$.summary.difference();if(diff.length<8){diff=' - NONE -';}if(message===undefined){message='';}message+=diff;return LOG('$.summary.difference():'+message,'v');};return LOG;};\n\nvar log = new LogFactory('myLog.log'); // =>; creates the new log factory - put full path where\n\nfunction getEnv(variable){\n    return $.getenv(variable);\n}\n\nfunction fileOpen(path){\n    return app.open(new File(path));\n}\n\nfunction getLayerTypeWithName(layerName) {\n    var type = 'NA';\n    var nameParts = layerName.split('_');\n    var namePrefix = nameParts[0];\n    namePrefix = namePrefix.toLowerCase();\n    switch (namePrefix) {\n        case 'guide':\n        case 'tl':\n        case 'tr':\n        case 'bl':\n        case 'br':\n            type = 'GUIDE';\n            break;\n        case 'fg':\n            type = 'FG';\n            break;\n        case 'bg':\n            type = 'BG';\n            break;\n        case 'obj':\n        default:\n            type = 'OBJ';\n            break;\n    }\n\n    return type;\n}\n\nfunction getLayers() {\n    /**\n     * Get json representation of list of layers. \n     * Much faster this way than in DOM traversal (2s vs 45s on same file)\n     * \n     * Format of single layer info:\n     *      id :    number\n     *      name:   string\n     *      group:  boolean - true if layer is a group\n     *      parents:array - list of ids of parent groups, useful for selection \n     *          all children layers from parent layerSet (eg. group)\n     *      type:   string - type of layer guessed from its name\n     *      visible:boolean - true if visible\n     **/\n    if (documents.length == 0){\n        return '[]';\n    }\n    var ref1 = new ActionReference();\n    ref1.putEnumerated(charIDToTypeID('Dcmn'), charIDToTypeID('Ordn'), \n                       charIDToTypeID('Trgt'));\n    var count = executeActionGet(ref1).getInteger(charIDToTypeID('NmbL'));\n\n    // get all layer names\n    var layers = [];\n    var layer = {};\n    \n    var parents = [];\n    for (var i = count; i >= 1; i--) {\n      var layer = {};\n      var ref2 = new ActionReference();\n      ref2.putIndex(charIDToTypeID('Lyr '), i);\n\n      var desc = executeActionGet(ref2);  // Access layer index #i\n      var layerSection = typeIDToStringID(desc.getEnumerationValue(\n                                          stringIDToTypeID('layerSection')));\n      \n      layer.id = desc.getInteger(stringIDToTypeID(\"layerID\")); \n      layer.name = desc.getString(stringIDToTypeID(\"name\"));\n      layer.color_code = typeIDToStringID(desc.getEnumerationValue(stringIDToTypeID('color')));\n      layer.group = false;\n      layer.parents = parents.slice();\n      layer.type = getLayerTypeWithName(layer.name);\n      layer.visible = desc.getBoolean(stringIDToTypeID(\"visible\"));\n      //log(\" name: \" + layer.name + \" groupId \" + layer.groupId + \n      //\" group \" + layer.group);\n      if (layerSection == 'layerSectionStart') { // Group start and end\n        parents.push(layer.id); \n        layer.group = true;                    \n      }\n      if (layerSection == 'layerSectionEnd') {\n        parents.pop();\n        continue;\n      } \n      layers.push(JSON.stringify(layer));\n    }\n     try{\n        var bck = activeDocument.backgroundLayer;\n        layer.id = bck.id;\n        layer.name = bck.name;\n        layer.group = false;\n        layer.parents = [];\n        layer.type = 'background';\n        layer.visible = bck.visible;\n        layers.push(JSON.stringify(layer));\n    }catch(e){\n        // do nothing, no background layer\n    };\n    //log(\"layers \" + layers);\n    return '[' + layers + ']';\n}\n\nfunction setVisible(layer_id, visibility){\n    /**\n     * Sets particular 'layer_id'<int> to 'visibility' if true > show\n     **/\n    var desc = new ActionDescriptor();\n    var ref = new ActionReference();\n    ref.putIdentifier(stringIDToTypeID(\"layer\"), layer_id);\n    desc.putReference(stringIDToTypeID(\"null\"), ref);\n    \n    executeAction(visibility?stringIDToTypeID(\"show\"):stringIDToTypeID(\"hide\"), \n                  desc, DialogModes.NO);\n    \n}\n\nfunction getHeadline(){\n    /**\n     *  Returns headline of current document with metadata \n     * \n     **/\n    if (documents.length == 0){\n        return '';\n    }\n    var headline = app.activeDocument.info.headline;\n    \n    return headline;\n}\n\nfunction isSaved(){\n    return app.activeDocument.saved;\n}\n\nfunction save(){\n    /** Saves active document **/        \n    return app.activeDocument.save();\n}\n\nfunction saveAs(output_path, ext, as_copy){\n    /** Exports scene to various formats\n     * \n     * Currently implemented: 'jpg', 'png', 'psd'\n     * \n     * output_path - escaped file path on local system\n     * ext - extension for export\n     * as_copy - create copy, do not overwrite\n     * \n     * */\n    var saveName = output_path;\n    var saveOptions;\n    if (ext == 'jpg'){\n      saveOptions = new JPEGSaveOptions();\n      saveOptions.quality = 12;\n      saveOptions.embedColorProfile = true;\n      saveOptions.formatOptions = FormatOptions.PROGRESSIVE;\n      if(saveOptions.formatOptions == FormatOptions.PROGRESSIVE){\n      saveOptions.scans = 5};\n      saveOptions.matte = MatteType.NONE;\n    }\n    if (ext == 'png'){\n      saveOptions = new PNGSaveOptions();\n      saveOptions.interlaced = true;\n      saveOptions.transparency = true;\n    }\n    if (ext == 'psd'){\n        saveOptions = null;\n        return app.activeDocument.saveAs(new File(saveName));       \n    }\n    if (ext == 'psb'){\n        return savePSB(output_path);\n    }\n\n    return app.activeDocument.saveAs(new File(saveName), saveOptions, as_copy);   \n    \n}\n\nfunction getActiveDocumentName(){\n    /**\n     *   Returns file name of active document\n     * */\n    if (documents.length == 0){\n        return null;\n    }\n    return app.activeDocument.name;\n}\n\nfunction getActiveDocumentFullName(){\n    /**\n     *   Returns file name of active document with file path.\n     *   activeDocument.fullName returns path in URI (eg /c/.. instead of c:/)\n     * */\n    if (documents.length == 0){\n        return null;\n    }\n    var f = new File(app.activeDocument.fullName);\n    var path = f.fsName;\n    f.close();\n    return path;\n}\n\nfunction imprint(payload){\n    /**\n     *  Sets headline content of current document with metadata. Stores\n     *  information about assets created through Avalon.\n     *  Content accessible in PS through File > File Info\n     * \n     **/\n    app.activeDocument.info.headline = payload;\n}\n\nfunction getSelectedLayers(doc) {\n    /**\n     * Returns json representation of currently selected layers.\n     * Works in three steps - 1) creates new group with selected layers\n     *                        2) traverses this group\n     *                        3) deletes newly created group, not needed\n     * Bit weird, but Adobe..\n     **/\n    if (doc == null){\n        doc = app.activeDocument;\n    }\n        \n    var selLayers = [];\n    _grp = groupSelectedLayers(doc);\n  \n    var group = doc.activeLayer;\n    var layers = group.layers;\n\n    // // group is fake at this point\n    // var itself_name = '';\n    // if (layers){\n    //     itself_name = layers[0].name;\n    // }\n    \n    \n    for (var i = 0; i < layers.length; i++) {\n        var layer = {};\n        layer.id = layers[i].id;\n        layer.name = layers[i].name;\n        long_names =_get_parents_names(group.parent, layers[i].name);\n        var t = layers[i].kind;\n        if ((typeof t !== 'undefined') && \n            (layers[i].kind.toString() == 'LayerKind.NORMAL')){\n            layer.group = false;\n        }else{\n            layer.group = true;\n        }\n        layer.long_name = long_names;\n        \n        selLayers.push(layer);\n    }\n  \n    _undo();\n  \n    return JSON.stringify(selLayers);\n};\n\nfunction selectLayers(selectedLayers){\n    /**\n     *  Selects layers from list of ids\n     **/\n    selectedLayers = JSON.parse(selectedLayers);\n    var layers = new Array();\n    var id54 = charIDToTypeID( \"slct\" );\n    var desc12 = new ActionDescriptor();\n    var id55 = charIDToTypeID( \"null\" );\n    var ref9 = new ActionReference();\n    \n    var existing_layers = JSON.parse(getLayers());\n    var existing_ids = [];\n    for (var y = 0; y < existing_layers.length; y++){\n          existing_ids.push(existing_layers[y][\"id\"]);\n    }\n    for (var i = 0; i < selectedLayers.length; i++) {\n       // a check to see if the id still exists\n       var id = selectedLayers[i];\n       if(existing_ids.toString().indexOf(id)>=0){\n           layers[i] = charIDToTypeID( \"Lyr \" );\n           ref9.putIdentifier(layers[i], id);\n       }       \n    }\n    desc12.putReference( id55, ref9 );\n    var id58 = charIDToTypeID( \"MkVs\" );\n    desc12.putBoolean( id58, false );\n    executeAction( id54, desc12, DialogModes.NO );\n}\n\nfunction groupSelectedLayers(doc, name) {\n    /**\n     * Groups selected layers into new group.\n     * Returns json representation of Layer for server to consume\n     * \n     * Args:\n     *     doc(activeDocument)\n     *     name (str): new name of created group\n     **/\n    if (doc == null){\n        doc = app.activeDocument;\n    }\n    \n    var desc = new ActionDescriptor();\n    var ref = new ActionReference();\n    ref.putClass( stringIDToTypeID('layerSection') );\n    desc.putReference( charIDToTypeID('null'), ref );\n    var lref = new ActionReference();\n    lref.putEnumerated( charIDToTypeID('Lyr '), charIDToTypeID('Ordn'), \n                        charIDToTypeID('Trgt') );\n    desc.putReference( charIDToTypeID('From'), lref);\n    executeAction( charIDToTypeID('Mk  '), desc, DialogModes.NO );\n    \n    var group = doc.activeLayer;\n    if (name){\n        // Add special character to highlight group that will be published\n        group.name = name;\n    }   \n    var layer = {};\n    layer.id = group.id;\n    layer.name = name; // keep name clean\n    layer.group = true; \n\n    layer.long_name = _get_parents_names(group, name);\n\n    return JSON.stringify(layer);        \n};\n\nfunction importSmartObject(path, name, link){\n    /**\n     *  Creates new layer with an image from 'path'\n     *  \n     *      path: absolute path to loaded file\n     *      name: sets name of newly created laye\n     *  \n     **/\n    var desc1 = new ActionDescriptor();\n    desc1.putPath( app.charIDToTypeID(\"null\"), new File(path) );\n    link = link || false;\n    if (link) {\n        desc1.putBoolean( app.charIDToTypeID('Lnkd'), true );  \n    }\n\n    desc1.putEnumerated(app.charIDToTypeID(\"FTcs\"), app.charIDToTypeID(\"QCSt\"), \n                       app.charIDToTypeID(\"Qcsa\"));                     \n    var desc2 = new ActionDescriptor();\n    desc2.putUnitDouble(app.charIDToTypeID(\"Hrzn\"), \n                        app.charIDToTypeID(\"#Pxl\"), 0.0);\n    desc2.putUnitDouble(app.charIDToTypeID(\"Vrtc\"), \n                        app.charIDToTypeID(\"#Pxl\"), 0.0);\n    \n    desc1.putObject(charIDToTypeID(\"Ofst\"), charIDToTypeID(\"Ofst\"), desc2);\n    executeAction(charIDToTypeID(\"Plc \" ), desc1, DialogModes.NO);\n\n    var docRef =  app.activeDocument\n    var currentActivelayer = app.activeDocument.activeLayer;\n    if (name){\n        currentActivelayer.name = name;\n    }\n    var layer = {}\n    layer.id = currentActivelayer.id;\n    layer.name = currentActivelayer.name;                        \n    return JSON.stringify(layer);     \n}\n\nfunction replaceSmartObjects(layer_id, path, name){\n    /**\n     *  Updates content of 'layer' with an image from 'path'\n     *  \n     **/\n\n    var desc = new ActionDescriptor();\n    var ref = new ActionReference();\n    ref.putIdentifier(stringIDToTypeID(\"layer\"), layer_id);\n    desc.putReference(stringIDToTypeID(\"null\"), ref);\n\n    desc.putPath(charIDToTypeID('null'), new File(path) );\n    desc.putInteger(charIDToTypeID(\"PgNm\"), 1);\n\n    executeAction(stringIDToTypeID('placedLayerReplaceContents'), \n                  desc, DialogModes.NO );       \n    var currentActivelayer = app.activeDocument.activeLayer;\n    if (name){\n        currentActivelayer.name = name;\n    }                  \n}\n\nfunction createGroup(name){\n    /**\n     * Creates new group with a 'name'\n     * Because of asynchronous nature, only group.id is available\n     **/\n    group = app.activeDocument.layerSets.add();\n    // Add special character to highlight group that will be published\n    group.name = name;\n       \n    return group.id;  // only id available at this time :|\n}\n\nfunction deleteLayer(layer_id){          \n    /***\n     * Deletes layer by its layer_id\n     * \n     * layer_id (int)\n     **/\n    var d = new ActionDescriptor();\n    var r = new ActionReference();\n\n    r.putIdentifier(stringIDToTypeID(\"layer\"), layer_id);\n    d.putReference(stringIDToTypeID(\"null\"), r);\n    executeAction(stringIDToTypeID(\"delete\"), d, DialogModes.NO);\n}\n\nfunction _undo() {\n    executeAction(charIDToTypeID(\"undo\", undefined, DialogModes.NO));\n};\n\nfunction savePSB(output_path){\n    /***\n     * Saves file as .psb to 'output_path'\n     * \n     * output_path (str)\n     **/\n    var desc1 = new ActionDescriptor(); \n    var desc2 = new ActionDescriptor(); \n    desc2.putBoolean( stringIDToTypeID('maximizeCompatibility'), true );        \n    desc1.putObject( charIDToTypeID('As  '), charIDToTypeID('Pht8'), desc2 );        \n    desc1.putPath( charIDToTypeID('In  '), new File(output_path) );       \n    desc1.putBoolean( charIDToTypeID('LwCs'), true );        \n    executeAction( charIDToTypeID('save'), desc1, DialogModes.NO );      \n}\n\nfunction close(){\n    executeAction(stringIDToTypeID(\"quit\"), undefined, DialogModes.NO );\n}\n\nfunction renameLayer(layer_id, new_name){\n    /***\n     * Renames 'layer_id' to 'new_name'\n     * \n     * Via Action (fast)\n     * \n     * Args:\n     *    layer_id(int)\n     *    new_name(str)\n     * \n     * output_path (str)\n     **/\n    doc = app.activeDocument;\n    selectLayers('['+layer_id+']');\n\n    doc.activeLayer.name = new_name;\n}\n\nfunction _get_parents_names(layer, itself_name){\n    var long_names = [itself_name];\n    while (layer.parent){\n        if (layer.typename != \"LayerSet\"){\n            break;\n        }\n        long_names.push(layer.name);\n        layer = layer.parent;\n    }\n    return long_names;\n}\n\n// triggers when panel is opened, good for debugging \n//log(getActiveDocumentName()); \n// log.show();\n// var a = app.activeDocument.activeLayer;\n// log(a);\n//getSelectedLayers();\n// importSmartObject(\"c:/projects/test.jpg\", \"a aaNewLayer\", true);\n// log(\"dpc\");\n// replaceSmartObjects(153, \"▼Jungle_imageTest_001\", \"c:/projects/test_project_test_asset_TestTask_v001.png\");"
  },
  {
    "path": "openpype/hosts/photoshop/api/extension/host/json.js",
    "content": "//  json2.js\n//  2017-06-12\n//  Public Domain.\n//  NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.\n\n//  USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO\n//  NOT CONTROL.\n\n//  This file creates a global JSON object containing two methods: stringify\n//  and parse. This file provides the ES5 JSON capability to ES3 systems.\n//  If a project might run on IE8 or earlier, then this file should be included.\n//  This file does nothing on ES5 systems.\n\n//      JSON.stringify(value, replacer, space)\n//          value       any JavaScript value, usually an object or array.\n//          replacer    an optional parameter that determines how object\n//                      values are stringified for objects. It can be a\n//                      function or an array of strings.\n//          space       an optional parameter that specifies the indentation\n//                      of nested structures. If it is omitted, the text will\n//                      be packed without extra whitespace. If it is a number,\n//                      it will specify the number of spaces to indent at each\n//                      level. If it is a string (such as \"\\t\" or \"&nbsp;\"),\n//                      it contains the characters used to indent at each level.\n//          This method produces a JSON text from a JavaScript value.\n//          When an object value is found, if the object contains a toJSON\n//          method, its toJSON method will be called and the result will be\n//          stringified. A toJSON method does not serialize: it returns the\n//          value represented by the name/value pair that should be serialized,\n//          or undefined if nothing should be serialized. The toJSON method\n//          will be passed the key associated with the value, and this will be\n//          bound to the value.\n\n//          For example, this would serialize Dates as ISO strings.\n\n//              Date.prototype.toJSON = function (key) {\n//                  function f(n) {\n//                      // Format integers to have at least two digits.\n//                      return (n < 10)\n//                          ? \"0\" + n\n//                          : n;\n//                  }\n//                  return this.getUTCFullYear()   + \"-\" +\n//                       f(this.getUTCMonth() + 1) + \"-\" +\n//                       f(this.getUTCDate())      + \"T\" +\n//                       f(this.getUTCHours())     + \":\" +\n//                       f(this.getUTCMinutes())   + \":\" +\n//                       f(this.getUTCSeconds())   + \"Z\";\n//              };\n\n//          You can provide an optional replacer method. It will be passed the\n//          key and value of each member, with this bound to the containing\n//          object. The value that is returned from your method will be\n//          serialized. If your method returns undefined, then the member will\n//          be excluded from the serialization.\n\n//          If the replacer parameter is an array of strings, then it will be\n//          used to select the members to be serialized. It filters the results\n//          such that only members with keys listed in the replacer array are\n//          stringified.\n\n//          Values that do not have JSON representations, such as undefined or\n//          functions, will not be serialized. Such values in objects will be\n//          dropped; in arrays they will be replaced with null. You can use\n//          a replacer function to replace those with JSON values.\n\n//          JSON.stringify(undefined) returns undefined.\n\n//          The optional space parameter produces a stringification of the\n//          value that is filled with line breaks and indentation to make it\n//          easier to read.\n\n//          If the space parameter is a non-empty string, then that string will\n//          be used for indentation. If the space parameter is a number, then\n//          the indentation will be that many spaces.\n\n//          Example:\n\n//          text = JSON.stringify([\"e\", {pluribus: \"unum\"}]);\n//          // text is '[\"e\",{\"pluribus\":\"unum\"}]'\n\n//          text = JSON.stringify([\"e\", {pluribus: \"unum\"}], null, \"\\t\");\n//          // text is '[\\n\\t\"e\",\\n\\t{\\n\\t\\t\"pluribus\": \"unum\"\\n\\t}\\n]'\n\n//          text = JSON.stringify([new Date()], function (key, value) {\n//              return this[key] instanceof Date\n//                  ? \"Date(\" + this[key] + \")\"\n//                  : value;\n//          });\n//          // text is '[\"Date(---current time---)\"]'\n\n//      JSON.parse(text, reviver)\n//          This method parses a JSON text to produce an object or array.\n//          It can throw a SyntaxError exception.\n\n//          The optional reviver parameter is a function that can filter and\n//          transform the results. It receives each of the keys and values,\n//          and its return value is used instead of the original value.\n//          If it returns what it received, then the structure is not modified.\n//          If it returns undefined then the member is deleted.\n\n//          Example:\n\n//          // Parse the text. Values that look like ISO date strings will\n//          // be converted to Date objects.\n\n//          myData = JSON.parse(text, function (key, value) {\n//              var a;\n//              if (typeof value === \"string\") {\n//                  a =\n//   /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2}(?:\\.\\d*)?)Z$/.exec(value);\n//                  if (a) {\n//                      return new Date(Date.UTC(\n//                         +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]\n//                      ));\n//                  }\n//                  return value;\n//              }\n//          });\n\n//          myData = JSON.parse(\n//              \"[\\\"Date(09/09/2001)\\\"]\",\n//              function (key, value) {\n//                  var d;\n//                  if (\n//                      typeof value === \"string\"\n//                      && value.slice(0, 5) === \"Date(\"\n//                      && value.slice(-1) === \")\"\n//                  ) {\n//                      d = new Date(value.slice(5, -1));\n//                      if (d) {\n//                          return d;\n//                      }\n//                  }\n//                  return value;\n//              }\n//          );\n\n//  This is a reference implementation. You are free to copy, modify, or\n//  redistribute.\n\n/*jslint\n    eval, for, this\n*/\n\n/*property\n    JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,\n    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,\n    lastIndex, length, parse, prototype, push, replace, slice, stringify,\n    test, toJSON, toString, valueOf\n*/\n\n\n// Create a JSON object only if one does not already exist. We create the\n// methods in a closure to avoid creating global variables.\n\nif (typeof JSON !== \"object\") {\n    JSON = {};\n}\n\n(function () {\n    \"use strict\";\n\n    var rx_one = /^[\\],:{}\\s]*$/;\n    var rx_two = /\\\\(?:[\"\\\\\\/bfnrt]|u[0-9a-fA-F]{4})/g;\n    var rx_three = /\"[^\"\\\\\\n\\r]*\"|true|false|null|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?/g;\n    var rx_four = /(?:^|:|,)(?:\\s*\\[)+/g;\n    var rx_escapable = /[\\\\\"\\u0000-\\u001f\\u007f-\\u009f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g;\n    var rx_dangerous = /[\\u0000\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g;\n\n    function f(n) {\n        // Format integers to have at least two digits.\n        return (n < 10)\n            ? \"0\" + n\n            : n;\n    }\n\n    function this_value() {\n        return this.valueOf();\n    }\n\n    if (typeof Date.prototype.toJSON !== \"function\") {\n\n        Date.prototype.toJSON = function () {\n\n            return isFinite(this.valueOf())\n                ? (\n                    this.getUTCFullYear()\n                    + \"-\"\n                    + f(this.getUTCMonth() + 1)\n                    + \"-\"\n                    + f(this.getUTCDate())\n                    + \"T\"\n                    + f(this.getUTCHours())\n                    + \":\"\n                    + f(this.getUTCMinutes())\n                    + \":\"\n                    + f(this.getUTCSeconds())\n                    + \"Z\"\n                )\n                : null;\n        };\n\n        Boolean.prototype.toJSON = this_value;\n        Number.prototype.toJSON = this_value;\n        String.prototype.toJSON = this_value;\n    }\n\n    var gap;\n    var indent;\n    var meta;\n    var rep;\n\n\n    function quote(string) {\n\n// If the string contains no control characters, no quote characters, and no\n// backslash characters, then we can safely slap some quotes around it.\n// Otherwise we must also replace the offending characters with safe escape\n// sequences.\n\n        rx_escapable.lastIndex = 0;\n        return rx_escapable.test(string)\n            ? \"\\\"\" + string.replace(rx_escapable, function (a) {\n                var c = meta[a];\n                return typeof c === \"string\"\n                    ? c\n                    : \"\\\\u\" + (\"0000\" + a.charCodeAt(0).toString(16)).slice(-4);\n            }) + \"\\\"\"\n            : \"\\\"\" + string + \"\\\"\";\n    }\n\n\n    function str(key, holder) {\n\n// Produce a string from holder[key].\n\n        var i;          // The loop counter.\n        var k;          // The member key.\n        var v;          // The member value.\n        var length;\n        var mind = gap;\n        var partial;\n        var value = holder[key];\n\n// If the value has a toJSON method, call it to obtain a replacement value.\n\n        if (\n            value\n            && typeof value === \"object\"\n            && typeof value.toJSON === \"function\"\n        ) {\n            value = value.toJSON(key);\n        }\n\n// If we were called with a replacer function, then call the replacer to\n// obtain a replacement value.\n\n        if (typeof rep === \"function\") {\n            value = rep.call(holder, key, value);\n        }\n\n// What happens next depends on the value's type.\n\n        switch (typeof value) {\n        case \"string\":\n            return quote(value);\n\n        case \"number\":\n\n// JSON numbers must be finite. Encode non-finite numbers as null.\n\n            return (isFinite(value))\n                ? String(value)\n                : \"null\";\n\n        case \"boolean\":\n        case \"null\":\n\n// If the value is a boolean or null, convert it to a string. Note:\n// typeof null does not produce \"null\". The case is included here in\n// the remote chance that this gets fixed someday.\n\n            return String(value);\n\n// If the type is \"object\", we might be dealing with an object or an array or\n// null.\n\n        case \"object\":\n\n// Due to a specification blunder in ECMAScript, typeof null is \"object\",\n// so watch out for that case.\n\n            if (!value) {\n                return \"null\";\n            }\n\n// Make an array to hold the partial results of stringifying this object value.\n\n            gap += indent;\n            partial = [];\n\n// Is the value an array?\n\n            if (Object.prototype.toString.apply(value) === \"[object Array]\") {\n\n// The value is an array. Stringify every element. Use null as a placeholder\n// for non-JSON values.\n\n                length = value.length;\n                for (i = 0; i < length; i += 1) {\n                    partial[i] = str(i, value) || \"null\";\n                }\n\n// Join all of the elements together, separated with commas, and wrap them in\n// brackets.\n\n                v = partial.length === 0\n                    ? \"[]\"\n                    : gap\n                        ? (\n                            \"[\\n\"\n                            + gap\n                            + partial.join(\",\\n\" + gap)\n                            + \"\\n\"\n                            + mind\n                            + \"]\"\n                        )\n                        : \"[\" + partial.join(\",\") + \"]\";\n                gap = mind;\n                return v;\n            }\n\n// If the replacer is an array, use it to select the members to be stringified.\n\n            if (rep && typeof rep === \"object\") {\n                length = rep.length;\n                for (i = 0; i < length; i += 1) {\n                    if (typeof rep[i] === \"string\") {\n                        k = rep[i];\n                        v = str(k, value);\n                        if (v) {\n                            partial.push(quote(k) + (\n                                (gap)\n                                    ? \": \"\n                                    : \":\"\n                            ) + v);\n                        }\n                    }\n                }\n            } else {\n\n// Otherwise, iterate through all of the keys in the object.\n\n                for (k in value) {\n                    if (Object.prototype.hasOwnProperty.call(value, k)) {\n                        v = str(k, value);\n                        if (v) {\n                            partial.push(quote(k) + (\n                                (gap)\n                                    ? \": \"\n                                    : \":\"\n                            ) + v);\n                        }\n                    }\n                }\n            }\n\n// Join all of the member texts together, separated with commas,\n// and wrap them in braces.\n\n            v = partial.length === 0\n                ? \"{}\"\n                : gap\n                    ? \"{\\n\" + gap + partial.join(\",\\n\" + gap) + \"\\n\" + mind + \"}\"\n                    : \"{\" + partial.join(\",\") + \"}\";\n            gap = mind;\n            return v;\n        }\n    }\n\n// If the JSON object does not yet have a stringify method, give it one.\n\n    if (typeof JSON.stringify !== \"function\") {\n        meta = {    // table of character substitutions\n            \"\\b\": \"\\\\b\",\n            \"\\t\": \"\\\\t\",\n            \"\\n\": \"\\\\n\",\n            \"\\f\": \"\\\\f\",\n            \"\\r\": \"\\\\r\",\n            \"\\\"\": \"\\\\\\\"\",\n            \"\\\\\": \"\\\\\\\\\"\n        };\n        JSON.stringify = function (value, replacer, space) {\n\n// The stringify method takes a value and an optional replacer, and an optional\n// space parameter, and returns a JSON text. The replacer can be a function\n// that can replace values, or an array of strings that will select the keys.\n// A default replacer method can be provided. Use of the space parameter can\n// produce text that is more easily readable.\n\n            var i;\n            gap = \"\";\n            indent = \"\";\n\n// If the space parameter is a number, make an indent string containing that\n// many spaces.\n\n            if (typeof space === \"number\") {\n                for (i = 0; i < space; i += 1) {\n                    indent += \" \";\n                }\n\n// If the space parameter is a string, it will be used as the indent string.\n\n            } else if (typeof space === \"string\") {\n                indent = space;\n            }\n\n// If there is a replacer, it must be a function or an array.\n// Otherwise, throw an error.\n\n            rep = replacer;\n            if (replacer && typeof replacer !== \"function\" && (\n                typeof replacer !== \"object\"\n                || typeof replacer.length !== \"number\"\n            )) {\n                throw new Error(\"JSON.stringify\");\n            }\n\n// Make a fake root object containing our value under the key of \"\".\n// Return the result of stringifying the value.\n\n            return str(\"\", {\"\": value});\n        };\n    }\n\n\n// If the JSON object does not yet have a parse method, give it one.\n\n    if (typeof JSON.parse !== \"function\") {\n        JSON.parse = function (text, reviver) {\n\n// The parse method takes a text and an optional reviver function, and returns\n// a JavaScript value if the text is a valid JSON text.\n\n            var j;\n\n            function walk(holder, key) {\n\n// The walk method is used to recursively walk the resulting structure so\n// that modifications can be made.\n\n                var k;\n                var v;\n                var value = holder[key];\n                if (value && typeof value === \"object\") {\n                    for (k in value) {\n                        if (Object.prototype.hasOwnProperty.call(value, k)) {\n                            v = walk(value, k);\n                            if (v !== undefined) {\n                                value[k] = v;\n                            } else {\n                                delete value[k];\n                            }\n                        }\n                    }\n                }\n                return reviver.call(holder, key, value);\n            }\n\n\n// Parsing happens in four stages. In the first stage, we replace certain\n// Unicode characters with escape sequences. JavaScript handles many characters\n// incorrectly, either silently deleting them, or treating them as line endings.\n\n            text = String(text);\n            rx_dangerous.lastIndex = 0;\n            if (rx_dangerous.test(text)) {\n                text = text.replace(rx_dangerous, function (a) {\n                    return (\n                        \"\\\\u\"\n                        + (\"0000\" + a.charCodeAt(0).toString(16)).slice(-4)\n                    );\n                });\n            }\n\n// In the second stage, we run the text against regular expressions that look\n// for non-JSON patterns. We are especially concerned with \"()\" and \"new\"\n// because they can cause invocation, and \"=\" because it can cause mutation.\n// But just to be safe, we want to reject all unexpected forms.\n\n// We split the second stage into 4 regexp operations in order to work around\n// crippling inefficiencies in IE's and Safari's regexp engines. First we\n// replace the JSON backslash pairs with \"@\" (a non-JSON character). Second, we\n// replace all simple value tokens with \"]\" characters. Third, we delete all\n// open brackets that follow a colon or comma or that begin the text. Finally,\n// we look to see that the remaining characters are only whitespace or \"]\" or\n// \",\" or \":\" or \"{\" or \"}\". If that is so, then the text is safe for eval.\n\n            if (\n                rx_one.test(\n                    text\n                        .replace(rx_two, \"@\")\n                        .replace(rx_three, \"]\")\n                        .replace(rx_four, \"\")\n                )\n            ) {\n\n// In the third stage we use the eval function to compile the text into a\n// JavaScript structure. The \"{\" operator is subject to a syntactic ambiguity\n// in JavaScript: it can begin a block or an object literal. We wrap the text\n// in parens to eliminate the ambiguity.\n\n                j = eval(\"(\" + text + \")\");\n\n// In the optional fourth stage, we recursively walk the new structure, passing\n// each name/value pair to a reviver function for possible transformation.\n\n                return (typeof reviver === \"function\")\n                    ? walk({\"\": j}, \"\")\n                    : j;\n            }\n\n// If the text is not JSON parseable, then a SyntaxError is thrown.\n\n            throw new SyntaxError(\"JSON.parse\");\n        };\n    }\n}());"
  },
  {
    "path": "openpype/hosts/photoshop/api/extension/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <style type=\"text/css\">\n      html, body, iframe {\n        width: 100%;\n        height: 100%;\n        border: 0px;\n        margin: 0px;\n        overflow: hidden;\n        background-color: #424242;\n      }\n      button {width: 100%;}\n    </style>\n    \n    <style>\n      button {width: 100%;}\n      body {margin:0; padding:0; height: 100%;}\n      html {height: 100%;}\n    </style>\n    <script src=\"http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\">\n    </script>\n    \n    <script type=text/javascript>\n            $(function() {\n              $(\"a#workfiles-button\").bind(\"click\", function() {\n                RPC.call('Photoshop.workfiles_route').then(function (data) {\n                }, function (error) {\n                    alert(error);\n                });\n              });\n            });\n    </script>\n    \n    <script type=text/javascript>\n            $(function() {\n              $(\"a#loader-button\").bind(\"click\", function() {\n                RPC.call('Photoshop.loader_route').then(function (data) {\n                }, function (error) {\n                    alert(error);\n                });\n              });\n            });\n    </script>\n    \n    <script type=text/javascript>\n            $(function() {\n              $(\"a#publish-button\").bind(\"click\", function() {\n                RPC.call('Photoshop.publish_route').then(function (data) {\n                }, function (error) {\n                    alert(error);\n                });\n              });\n            });\n    </script>\n    \n    <script type=text/javascript>\n            $(function() {\n              $(\"a#sceneinventory-button\").bind(\"click\", function() {\n                RPC.call('Photoshop.sceneinventory_route').then(function (data) {\n                }, function (error) {\n                    alert(error);\n                });\n              });\n            });\n    </script>\n    \n    <script type=text/javascript>\n            $(function() {\n              $(\"a#experimental-button\").bind(\"click\", function() {\n                RPC.call('Photoshop.experimental_tools_route').then(function (data) {\n                }, function (error) {\n                    alert(error);\n                });\n              });\n            });\n    </script>\n</head>\n<body>\n  <script type=\"text/javascript\" src=\"./client/wsrpc.js\"></script>\n  <script type=\"text/javascript\" src=\"./client/CSInterface.js\"></script>\n  <script type=\"text/javascript\" src=\"./client/loglevel.min.js\"></script>\n  \n  <!-- helper library for better debugging of .jsx check its license! -->\n  <script type=\"text/javascript\" src=\"./host/JSX.js\"></script>\n  \n  <script type=\"text/javascript\" src=\"./client/client.js\"></script>\n  \n    <a href=# id=workfiles-button><button>Workfiles...</button></a>\n    <a href=# id=loader-button><button>Load...</button></a>\n    <a href=# id=publish-button><button>Publish...</button></a>\n    <a href=# id=sceneinventory-button><button>Manage...</button></a>\n    <a href=# id=experimental-button><button>Experimental Tools...</button></a>\n</body>\n</html>\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/launch_logic.py",
    "content": "import os\nimport subprocess\nimport collections\nimport asyncio\n\nfrom wsrpc_aiohttp import (\n    WebSocketRoute,\n    WebSocketAsync\n)\n\nfrom qtpy import QtCore\n\nfrom openpype.lib import Logger, StringTemplate\nfrom openpype.pipeline import (\n    registered_host,\n    Anatomy,\n)\nfrom openpype.pipeline.workfile import (\n    get_workfile_template_key_from_context,\n    get_last_workfile,\n)\nfrom openpype.pipeline.template_data import get_template_data_with_names\nfrom openpype.tools.utils import host_tools\nfrom openpype.tools.adobe_webserver.app import WebServerTool\nfrom openpype.pipeline.context_tools import change_current_context\nfrom openpype.client import get_asset_by_name\n\nfrom .ws_stub import PhotoshopServerStub\n\nlog = Logger.get_logger(__name__)\n\n\nclass ConnectionNotEstablishedYet(Exception):\n    pass\n\n\nclass MainThreadItem:\n    \"\"\"Structure to store information about callback in main thread.\n\n    Item should be used to execute callback in main thread which may be needed\n    for execution of Qt objects.\n\n    Item store callback (callable variable), arguments and keyword arguments\n    for the callback. Item hold information about it's process.\n    \"\"\"\n    not_set = object()\n\n    def __init__(self, callback, *args, **kwargs):\n        self._done = False\n        self._exception = self.not_set\n        self._result = self.not_set\n        self._callback = callback\n        self._args = args\n        self._kwargs = kwargs\n\n    @property\n    def done(self):\n        return self._done\n\n    @property\n    def exception(self):\n        return self._exception\n\n    @property\n    def result(self):\n        return self._result\n\n    def execute(self):\n        \"\"\"Execute callback and store its result.\n\n        Method must be called from main thread. Item is marked as `done`\n        when callback execution finished. Store output of callback of exception\n        information when callback raises one.\n        \"\"\"\n        log.debug(\"Executing process in main thread\")\n        if self.done:\n            log.warning(\"- item is already processed\")\n            return\n\n        log.info(\"Running callback: {}\".format(str(self._callback)))\n        try:\n            result = self._callback(*self._args, **self._kwargs)\n            self._result = result\n\n        except Exception as exc:\n            self._exception = exc\n\n        finally:\n            self._done = True\n\n\ndef stub():\n    \"\"\"\n        Convenience function to get server RPC stub to call methods directed\n        for host (Photoshop).\n        It expects already created connection, started from client.\n        Currently created when panel is opened (PS: Window>Extensions>Avalon)\n    :return: <PhotoshopClientStub> where functions could be called from\n    \"\"\"\n    ps_stub = PhotoshopServerStub()\n    if not ps_stub.client:\n        raise ConnectionNotEstablishedYet(\"Connection is not created yet\")\n\n    return ps_stub\n\n\ndef show_tool_by_name(tool_name):\n    kwargs = {}\n    if tool_name == \"loader\":\n        kwargs[\"use_context\"] = True\n\n    host_tools.show_tool_by_name(tool_name, **kwargs)\n\n\nclass ProcessLauncher(QtCore.QObject):\n    route_name = \"Photoshop\"\n    _main_thread_callbacks = collections.deque()\n\n    def __init__(self, subprocess_args):\n        self._subprocess_args = subprocess_args\n        self._log = None\n\n        super(ProcessLauncher, self).__init__()\n\n        # Keep track if launcher was already started\n        self._started = False\n\n        self._process = None\n        self._websocket_server = None\n\n        start_process_timer = QtCore.QTimer()\n        start_process_timer.setInterval(100)\n\n        loop_timer = QtCore.QTimer()\n        loop_timer.setInterval(200)\n\n        start_process_timer.timeout.connect(self._on_start_process_timer)\n        loop_timer.timeout.connect(self._on_loop_timer)\n\n        self._start_process_timer = start_process_timer\n        self._loop_timer = loop_timer\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(\n                \"{}-launcher\".format(self.route_name)\n            )\n        return self._log\n\n    @property\n    def websocket_server_is_running(self):\n        if self._websocket_server is not None:\n            return self._websocket_server.is_running\n        return False\n\n    @property\n    def is_process_running(self):\n        if self._process is not None:\n            return self._process.poll() is None\n        return False\n\n    @property\n    def is_host_connected(self):\n        \"\"\"Returns True if connected, False if app is not running at all.\"\"\"\n        if not self.is_process_running:\n            return False\n\n        try:\n            _stub = stub()\n            if _stub:\n                return True\n        except Exception:\n            pass\n\n        return None\n\n    @classmethod\n    def execute_in_main_thread(cls, callback, *args, **kwargs):\n        item = MainThreadItem(callback, *args, **kwargs)\n        cls._main_thread_callbacks.append(item)\n        return item\n\n    def start(self):\n        if self._started:\n            return\n        self.log.info(\"Started launch logic of Photoshop\")\n        self._started = True\n        self._start_process_timer.start()\n\n    def exit(self):\n        \"\"\" Exit whole application. \"\"\"\n        if self._start_process_timer.isActive():\n            self._start_process_timer.stop()\n        if self._loop_timer.isActive():\n            self._loop_timer.stop()\n\n        if self._websocket_server is not None:\n            self._websocket_server.stop()\n\n        if self._process:\n            self._process.kill()\n            self._process.wait()\n\n        QtCore.QCoreApplication.exit()\n\n    def _on_loop_timer(self):\n        # TODO find better way and catch errors\n        # Run only callbacks that are in queue at the moment\n        cls = self.__class__\n        for _ in range(len(cls._main_thread_callbacks)):\n            if cls._main_thread_callbacks:\n                item = cls._main_thread_callbacks.popleft()\n                item.execute()\n\n        if not self.is_process_running:\n            self.log.info(\"Host process is not running. Closing\")\n            self.exit()\n\n        elif not self.websocket_server_is_running:\n            self.log.info(\"Websocket server is not running. Closing\")\n            self.exit()\n\n    def _on_start_process_timer(self):\n        # TODO add try except validations for each part in this method\n        # Start server as first thing\n        if self._websocket_server is None:\n            self._init_server()\n            return\n\n        # TODO add waiting time\n        # Wait for webserver\n        if not self.websocket_server_is_running:\n            return\n\n        # Start application process\n        if self._process is None:\n            self._start_process()\n            self.log.info(\"Waiting for host to connect\")\n            return\n\n        # TODO add waiting time\n        # Wait until host is connected\n        if self.is_host_connected:\n            self._start_process_timer.stop()\n            self._loop_timer.start()\n        elif (\n            not self.is_process_running\n            or not self.websocket_server_is_running\n        ):\n            self.exit()\n\n    def _init_server(self):\n        if self._websocket_server is not None:\n            return\n\n        self.log.debug(\n            \"Initialization of websocket server for host communication\"\n        )\n\n        self._websocket_server = websocket_server = WebServerTool()\n        if websocket_server.port_occupied(\n            websocket_server.host_name,\n            websocket_server.port\n        ):\n            self.log.info(\n                \"Server already running, sending actual context and exit.\"\n            )\n            asyncio.run(websocket_server.send_context_change(self.route_name))\n            self.exit()\n            return\n\n        # Add Websocket route\n        websocket_server.add_route(\"*\", \"/ws/\", WebSocketAsync)\n        # Add after effects route to websocket handler\n\n        print(\"Adding {} route\".format(self.route_name))\n        WebSocketAsync.add_route(\n            self.route_name, PhotoshopRoute\n        )\n        self.log.info(\"Starting websocket server for host communication\")\n        websocket_server.start_server()\n\n    def _start_process(self):\n        if self._process is not None:\n            return\n        self.log.info(\"Starting host process\")\n        try:\n            self._process = subprocess.Popen(\n                self._subprocess_args,\n                stdout=subprocess.DEVNULL,\n                stderr=subprocess.DEVNULL\n            )\n        except Exception:\n            self.log.info(\"exce\", exc_info=True)\n            self.exit()\n\n\nclass PhotoshopRoute(WebSocketRoute):\n    \"\"\"\n        One route, mimicking external application (like Harmony, etc).\n        All functions could be called from client.\n        'do_notify' function calls function on the client - mimicking\n            notification after long running job on the server or similar\n    \"\"\"\n    instance = None\n\n    def init(self, **kwargs):\n        # Python __init__ must be return \"self\".\n        # This method might return anything.\n        log.debug(\"someone called Photoshop route\")\n        self.instance = self\n        return kwargs\n\n    # server functions\n    async def ping(self):\n        log.debug(\"someone called Photoshop route ping\")\n\n    # This method calls function on the client side\n    # client functions\n    async def set_context(self, project, asset, task):\n        \"\"\"\n            Sets 'project' and 'asset' to envs, eg. setting context.\n\n        Opens last workile from that context if exists.\n\n        Args:\n            project (str)\n            asset (str)\n            task (str\n        \"\"\"\n        log.info(\"Setting context change\")\n        log.info(f\"project {project} asset {asset} task {task}\")\n\n        asset_doc = get_asset_by_name(project, asset)\n        change_current_context(asset_doc, task)\n\n        last_workfile_path = self._get_last_workfile_path(project,\n                                                          asset,\n                                                          task)\n        if last_workfile_path and os.path.exists(last_workfile_path):\n            ProcessLauncher.execute_in_main_thread(\n                lambda: stub().open(last_workfile_path))\n\n\n    async def read(self):\n        log.debug(\"photoshop.read client calls server server calls \"\n                  \"photoshop client\")\n        return await self.socket.call('photoshop.read')\n\n    # panel routes for tools\n    async def workfiles_route(self):\n        self._tool_route(\"workfiles\")\n\n    async def loader_route(self):\n        self._tool_route(\"loader\")\n\n    async def publish_route(self):\n        self._tool_route(\"publisher\")\n\n    async def sceneinventory_route(self):\n        self._tool_route(\"sceneinventory\")\n\n    async def experimental_tools_route(self):\n        self._tool_route(\"experimental_tools\")\n\n    def _tool_route(self, _tool_name):\n        \"\"\"The address accessed when clicking on the buttons.\"\"\"\n\n        ProcessLauncher.execute_in_main_thread(show_tool_by_name, _tool_name)\n\n        # Required return statement.\n        return \"nothing\"\n\n    def _get_last_workfile_path(self, project_name, asset_name, task_name):\n        \"\"\"Returns last workfile path if exists\"\"\"\n        host = registered_host()\n        host_name = \"photoshop\"\n        template_key = get_workfile_template_key_from_context(\n            asset_name,\n            task_name,\n            host_name,\n            project_name=project_name\n        )\n        anatomy = Anatomy(project_name)\n\n        data = get_template_data_with_names(\n            project_name, asset_name, task_name, host_name\n        )\n        data[\"root\"] = anatomy.roots\n\n        file_template = anatomy.templates[template_key][\"file\"]\n\n        # Define saving file extension\n        extensions = host.get_workfile_extensions()\n\n        folder_template = anatomy.templates[template_key][\"folder\"]\n        work_root = StringTemplate.format_strict_template(\n            folder_template, data\n        )\n        last_workfile_path = get_last_workfile(\n            work_root, file_template, data, extensions, True\n        )\n\n        return last_workfile_path\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/lib.py",
    "content": "import os\nimport sys\nimport contextlib\nimport traceback\n\nfrom openpype.lib import env_value_to_bool, Logger\nfrom openpype.modules import ModulesManager\nfrom openpype.pipeline import install_host\nfrom openpype.tools.utils import host_tools\nfrom openpype.tools.utils import get_openpype_qt_app\nfrom openpype.tests.lib import is_in_tests\n\nfrom .launch_logic import ProcessLauncher, stub\n\nlog = Logger.get_logger(__name__)\n\n\ndef safe_excepthook(*args):\n    traceback.print_exception(*args)\n\n\ndef main(*subprocess_args):\n    from openpype.hosts.photoshop.api import PhotoshopHost\n\n    host = PhotoshopHost()\n    install_host(host)\n\n    sys.excepthook = safe_excepthook\n\n    # coloring in StdOutBroker\n    os.environ[\"OPENPYPE_LOG_NO_COLORS\"] = \"False\"\n    app = get_openpype_qt_app()\n    app.setQuitOnLastWindowClosed(False)\n\n    launcher = ProcessLauncher(subprocess_args)\n    launcher.start()\n\n    if env_value_to_bool(\"HEADLESS_PUBLISH\"):\n        manager = ModulesManager()\n        webpublisher_addon = manager[\"webpublisher\"]\n        launcher.execute_in_main_thread(\n            webpublisher_addon.headless_publish,\n            log,\n            \"ClosePS\",\n            is_in_tests()\n        )\n    elif env_value_to_bool(\"AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH\",\n                           default=True):\n\n        launcher.execute_in_main_thread(\n            host_tools.show_workfiles,\n            save=env_value_to_bool(\"WORKFILES_SAVE_AS\")\n        )\n\n    sys.exit(app.exec_())\n\n\n@contextlib.contextmanager\ndef maintained_selection():\n    \"\"\"Maintain selection during context.\"\"\"\n    selection = stub().get_selected_layers()\n    try:\n        yield selection\n    finally:\n        stub().select_layers(selection)\n\n\n@contextlib.contextmanager\ndef maintained_visibility(layers=None):\n    \"\"\"Maintain visibility during context.\n\n    Args:\n        layers (list) of PSItem (used for caching)\n    \"\"\"\n    visibility = {}\n    if not layers:\n        layers = stub().get_layers()\n    for layer in layers:\n        visibility[layer.id] = layer.visible\n    try:\n        yield\n    finally:\n        for layer in layers:\n            stub().set_visible(layer.id, visibility[layer.id])\n            pass\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/pipeline.py",
    "content": "import os\n\nfrom qtpy import QtWidgets\n\nimport pyblish.api\n\nfrom openpype.lib import register_event_callback, Logger\nfrom openpype.pipeline import (\n    register_loader_plugin_path,\n    register_creator_plugin_path,\n    AVALON_CONTAINER_ID,\n)\n\nfrom openpype.host import (\n    HostBase,\n    IWorkfileHost,\n    ILoadHost,\n    IPublishHost\n)\n\nfrom openpype.pipeline.load import any_outdated_containers\nfrom openpype.hosts.photoshop import PHOTOSHOP_HOST_DIR\nfrom openpype.tools.utils import get_openpype_qt_app\n\nfrom . import lib\n\nlog = Logger.get_logger(__name__)\n\nPLUGINS_DIR = os.path.join(PHOTOSHOP_HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\nINVENTORY_PATH = os.path.join(PLUGINS_DIR, \"inventory\")\n\n\nclass PhotoshopHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):\n    name = \"photoshop\"\n\n    def install(self):\n        \"\"\"Install Photoshop-specific functionality needed for integration.\n\n        This function is called automatically on calling\n        `api.install(photoshop)`.\n        \"\"\"\n        log.info(\"Installing OpenPype Photoshop...\")\n        pyblish.api.register_host(\"photoshop\")\n\n        pyblish.api.register_plugin_path(PUBLISH_PATH)\n        register_loader_plugin_path(LOAD_PATH)\n        register_creator_plugin_path(CREATE_PATH)\n\n        register_event_callback(\"application.launched\", on_application_launch)\n\n    def current_file(self):\n        try:\n            full_name = lib.stub().get_active_document_full_name()\n            if full_name and full_name != \"null\":\n                return os.path.normpath(full_name).replace(\"\\\\\", \"/\")\n        except Exception:\n            pass\n\n        return None\n\n    def work_root(self, session):\n        return os.path.normpath(session[\"AVALON_WORKDIR\"]).replace(\"\\\\\", \"/\")\n\n    def open_workfile(self, filepath):\n        lib.stub().open(filepath)\n\n        return True\n\n    def save_workfile(self, filepath=None):\n        _, ext = os.path.splitext(filepath)\n        lib.stub().saveAs(filepath, ext[1:], True)\n\n    def get_current_workfile(self):\n        return self.current_file()\n\n    def workfile_has_unsaved_changes(self):\n        if self.current_file():\n            return not lib.stub().is_saved()\n\n        return False\n\n    def get_workfile_extensions(self):\n        return [\".psd\", \".psb\"]\n\n    def get_containers(self):\n        return ls()\n\n    def get_context_data(self):\n        \"\"\"Get stored values for context (validation enable/disable etc)\"\"\"\n        meta = _get_stub().get_layers_metadata()\n        for item in meta:\n            if item.get(\"id\") == \"publish_context\":\n                item.pop(\"id\")\n                return item\n\n        return {}\n\n    def update_context_data(self, data, changes):\n        \"\"\"Store value needed for context\"\"\"\n        item = data\n        item[\"id\"] = \"publish_context\"\n        _get_stub().imprint(item[\"id\"], item)\n\n    def list_instances(self):\n        \"\"\"List all created instances to publish from current workfile.\n\n        Pulls from File > File Info\n\n        Returns:\n            (list) of dictionaries matching instances format\n        \"\"\"\n        stub = _get_stub()\n\n        if not stub:\n            return []\n\n        instances = []\n        layers_meta = stub.get_layers_metadata()\n        if layers_meta:\n            for instance in layers_meta:\n                if instance.get(\"id\") == \"pyblish.avalon.instance\":\n                    instances.append(instance)\n\n        return instances\n\n    def remove_instance(self, instance):\n        \"\"\"Remove instance from current workfile metadata.\n\n        Updates metadata of current file in File > File Info and removes\n        icon highlight on group layer.\n\n        Args:\n            instance (dict): instance representation from subsetmanager model\n        \"\"\"\n        stub = _get_stub()\n\n        if not stub:\n            return\n\n        inst_id = instance.get(\"instance_id\") or instance.get(\"uuid\")  # legacy\n        if not inst_id:\n            log.warning(\"No instance identifier for {}\".format(instance))\n            return\n\n        stub.remove_instance(inst_id)\n\n        if instance.get(\"members\"):\n            item = stub.get_layer(instance[\"members\"][0])\n            if item:\n                stub.rename_layer(item.id,\n                                  item.name.replace(stub.PUBLISH_ICON, ''))\n\n\ndef check_inventory():\n    if not any_outdated_containers():\n        return\n\n    # Warn about outdated containers.\n    _app = get_openpype_qt_app()\n\n    message_box = QtWidgets.QMessageBox()\n    message_box.setIcon(QtWidgets.QMessageBox.Warning)\n    msg = \"There are outdated containers in the scene.\"\n    message_box.setText(msg)\n    message_box.exec_()\n\n\ndef on_application_launch():\n    check_inventory()\n\n\ndef ls():\n    \"\"\"Yields containers from active Photoshop document\n\n    This is the host-equivalent of api.ls(), but instead of listing\n    assets on disk, it lists assets already loaded in Photoshop; once loaded\n    they are called 'containers'\n\n    Yields:\n        dict: container\n\n    \"\"\"\n    try:\n        stub = lib.stub()  # only after Photoshop is up\n    except lib.ConnectionNotEstablishedYet:\n        print(\"Not connected yet, ignoring\")\n        return\n\n    if not stub.get_active_document_name():\n        return\n\n    layers_meta = stub.get_layers_metadata()  # minimalize calls to PS\n    for layer in stub.get_layers():\n        data = stub.read(layer, layers_meta)\n\n        # Skip non-tagged layers.\n        if not data:\n            continue\n\n        # Filter to only containers.\n        if \"container\" not in data[\"id\"]:\n            continue\n\n        # Append transient data\n        data[\"objectName\"] = layer.name.replace(stub.LOADED_ICON, '')\n        data[\"layer\"] = layer\n\n        yield data\n\n\ndef _get_stub():\n    \"\"\"Handle pulling stub from PS to run operations on host\n\n    Returns:\n        (PhotoshopServerStub) or None\n    \"\"\"\n    try:\n        stub = lib.stub()  # only after Photoshop is up\n    except lib.ConnectionNotEstablishedYet:\n        print(\"Not connected yet, ignoring\")\n        return\n\n    if not stub.get_active_document_name():\n        return\n\n    return stub\n\n\ndef containerise(\n    name, namespace, layer, context, loader=None, suffix=\"_CON\"\n):\n    \"\"\"Imprint layer with metadata\n\n    Containerisation enables a tracking of version, author and origin\n    for loaded assets.\n\n    Arguments:\n        name (str): Name of resulting assembly\n        namespace (str): Namespace under which to host container\n        layer (PSItem): Layer to containerise\n        context (dict): Asset information\n        loader (str, optional): Name of loader used to produce this container.\n        suffix (str, optional): Suffix of container, defaults to `_CON`.\n\n    Returns:\n        container (str): Name of container assembly\n    \"\"\"\n    layer.name = name + suffix\n\n    data = {\n        \"schema\": \"openpype:container-2.0\",\n        \"id\": AVALON_CONTAINER_ID,\n        \"name\": name,\n        \"namespace\": namespace,\n        \"loader\": str(loader),\n        \"representation\": str(context[\"representation\"][\"_id\"]),\n        \"members\": [str(layer.id)]\n    }\n    stub = lib.stub()\n    stub.imprint(layer.id, data)\n\n    return layer\n\n\ndef cache_and_get_instances(creator):\n    \"\"\"Cache instances in shared data.\n\n    Storing all instances as a list as legacy instances might be still present.\n    Args:\n        creator (Creator): Plugin which would like to get instances from host.\n    Returns:\n        List[]: list of all instances stored in metadata\n    \"\"\"\n    shared_key = \"openpype.photoshop.instances\"\n    if shared_key not in creator.collection_shared_data:\n        creator.collection_shared_data[shared_key] = \\\n            creator.host.list_instances()\n    return creator.collection_shared_data[shared_key]\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/plugin.py",
    "content": "import re\n\nfrom openpype.pipeline import LoaderPlugin\nfrom .launch_logic import stub\n\n\ndef get_unique_layer_name(layers, asset_name, subset_name):\n    \"\"\"\n        Gets all layer names and if 'asset_name_subset_name' is present, it\n        increases suffix by 1 (eg. creates unique layer name - for Loader)\n    Args:\n        layers (list) of dict with layers info (name, id etc.)\n        asset_name (string):\n        subset_name (string):\n\n    Returns:\n        (string): name_00X (without version)\n    \"\"\"\n    name = \"{}_{}\".format(asset_name, subset_name)\n    names = {}\n    for layer in layers:\n        layer_name = re.sub(r'_\\d{3}$', '', layer.name)\n        if layer_name in names.keys():\n            names[layer_name] = names[layer_name] + 1\n        else:\n            names[layer_name] = 1\n    occurrences = names.get(name, 0)\n\n    return \"{}_{:0>3d}\".format(name, occurrences + 1)\n\n\nclass PhotoshopLoader(LoaderPlugin):\n    @staticmethod\n    def get_stub():\n        return stub()\n"
  },
  {
    "path": "openpype/hosts/photoshop/api/ws_stub.py",
    "content": "\"\"\"\n    Stub handling connection from server to client.\n    Used anywhere solution is calling client methods.\n\"\"\"\nimport json\nimport attr\nfrom wsrpc_aiohttp import WebSocketAsync\n\nfrom openpype.tools.adobe_webserver.app import WebServerTool\n\n\n@attr.s\nclass PSItem(object):\n    \"\"\"\n        Object denoting layer or group item in PS. Each item is created in\n        PS by any Loader, but contains same fields, which are being used\n        in later processing.\n    \"\"\"\n    # metadata\n    id = attr.ib()  # id created by AE, could be used for querying\n    name = attr.ib()  # name of item\n    group = attr.ib(default=None)  # item type (footage, folder, comp)\n    parents = attr.ib(factory=list)\n    visible = attr.ib(default=True)\n    type = attr.ib(default=None)\n    # all imported elements, single for\n    members = attr.ib(factory=list)\n    long_name = attr.ib(default=None)\n    color_code = attr.ib(default=None)  # color code of layer\n    instance_id = attr.ib(default=None)\n\n    @property\n    def clean_name(self):\n        \"\"\"Returns layer name without publish icon highlight\n\n        Returns:\n            (str)\n        \"\"\"\n        return (self.name.replace(PhotoshopServerStub.PUBLISH_ICON, '')\n                         .replace(PhotoshopServerStub.LOADED_ICON, ''))\n\n\nclass PhotoshopServerStub:\n    \"\"\"\n        Stub for calling function on client (Photoshop js) side.\n        Expects that client is already connected (started when avalon menu\n        is opened).\n        'self.websocketserver.call' is used as async wrapper\n    \"\"\"\n    PUBLISH_ICON = '\\u2117 '\n    LOADED_ICON = '\\u25bc'\n\n    def __init__(self):\n        self.websocketserver = WebServerTool.get_instance()\n        self.client = self.get_client()\n\n    @staticmethod\n    def get_client():\n        \"\"\"\n            Return first connected client to WebSocket\n            TODO implement selection by Route\n        :return: <WebSocketAsync> client\n        \"\"\"\n        clients = WebSocketAsync.get_clients()\n        client = None\n        if len(clients) > 0:\n            key = list(clients.keys())[0]\n            client = clients.get(key)\n\n        return client\n\n    def open(self, path):\n        \"\"\"Open file located at 'path' (local).\n\n        Args:\n            path(string): file path locally\n        Returns: None\n        \"\"\"\n        self.websocketserver.call(\n            self.client.call('Photoshop.open', path=path)\n        )\n\n    def read(self, layer, layers_meta=None):\n        \"\"\"Parses layer metadata from Headline field of active document.\n\n        Args:\n            layer: (PSItem)\n            layers_meta: full list from Headline (for performance in loops)\n        Returns:\n            (dict) of layer metadata stored in PS file\n\n        Example:\n            {\n                'id': 'pyblish.avalon.container',\n                'loader': 'ImageLoader',\n                'members': ['64'],\n                'name': 'imageMainMiddle',\n                'namespace': 'Hero_imageMainMiddle_001',\n                'representation': '6203dc91e80934d9f6ee7d96',\n                'schema': 'openpype:container-2.0'\n            }\n        \"\"\"\n        if layers_meta is None:\n            layers_meta = self.get_layers_metadata()\n\n        for layer_meta in layers_meta:\n            layer_id = layer_meta.get(\"uuid\")  # legacy\n            if layer_meta.get(\"members\"):\n                layer_id = layer_meta[\"members\"][0]\n            if str(layer.id) == str(layer_id):\n                return layer_meta\n        print(\"Unable to find layer metadata for {}\".format(layer.id))\n\n    def imprint(self, item_id, data, all_layers=None, items_meta=None):\n        \"\"\"Save layer metadata to Headline field of active document\n\n        Stores metadata in format:\n        [{\n            \"active\":true,\n            \"subset\":\"imageBG\",\n            \"family\":\"image\",\n            \"id\":\"pyblish.avalon.instance\",\n            \"asset\":\"Town\",\n            \"uuid\": \"8\"\n        }] - for created instances\n        OR\n        [{\n            \"schema\": \"openpype:container-2.0\",\n            \"id\": \"pyblish.avalon.instance\",\n            \"name\": \"imageMG\",\n            \"namespace\": \"Jungle_imageMG_001\",\n            \"loader\": \"ImageLoader\",\n            \"representation\": \"5fbfc0ee30a946093c6ff18a\",\n            \"members\": [\n                \"40\"\n            ]\n        }] - for loaded instances\n\n        Args:\n            item_id (str):\n            data(string): json representation for single layer\n            all_layers (list of PSItem): for performance, could be\n                injected for usage in loop, if not, single call will be\n                triggered\n            items_meta(string): json representation from Headline\n                           (for performance - provide only if imprint is in\n                           loop - value should be same)\n        Returns: None\n        \"\"\"\n        if not items_meta:\n            items_meta = self.get_layers_metadata()\n\n        # json.dumps writes integer values in a dictionary to string, so\n        # anticipating it here.\n        item_id = str(item_id)\n        is_new = True\n        result_meta = []\n        for item_meta in items_meta:\n            if ((item_meta.get('members') and\n                 item_id == str(item_meta.get('members')[0])) or\n                    item_meta.get(\"instance_id\") == item_id):\n                is_new = False\n                if data:\n                    item_meta.update(data)\n                    result_meta.append(item_meta)\n            else:\n                result_meta.append(item_meta)\n\n        if is_new:\n            result_meta.append(data)\n\n        # Ensure only valid ids are stored.\n        if not all_layers:\n            all_layers = self.get_layers()\n        layer_ids = [layer.id for layer in all_layers]\n        cleaned_data = []\n\n        for item in result_meta:\n            if item.get(\"members\"):\n                if int(item[\"members\"][0]) not in layer_ids:\n                    continue\n\n            cleaned_data.append(item)\n\n        payload = json.dumps(cleaned_data, indent=4)\n        self.websocketserver.call(\n            self.client.call('Photoshop.imprint', payload=payload)\n        )\n\n    def get_layers(self):\n        \"\"\"Returns JSON document with all(?) layers in active document.\n\n        Returns: <list of PSItem>\n                    Format of tuple: { 'id':'123',\n                                     'name': 'My Layer 1',\n                                     'type': 'GUIDE'|'FG'|'BG'|'OBJ'\n                                     'visible': 'true'|'false'\n        \"\"\"\n        res = self.websocketserver.call(\n            self.client.call('Photoshop.get_layers')\n        )\n\n        return self._to_records(res)\n\n    def get_layer(self, layer_id):\n        \"\"\"\n            Returns PSItem for specific 'layer_id' or None if not found\n        Args:\n            layer_id (string): unique layer id, stored in 'uuid' field\n\n        Returns:\n            (PSItem) or None\n        \"\"\"\n        layers = self.get_layers()\n        for layer in layers:\n            if str(layer.id) == str(layer_id):\n                return layer\n\n    def get_layers_in_layers(self, layers):\n        \"\"\"Return all layers that belong to layers (might be groups).\n\n        Args:\n            layers <list of PSItem>:\n\n        Returns:\n            <list of PSItem>\n        \"\"\"\n        parent_ids = set([lay.id for lay in layers])\n\n        return self._get_layers_in_layers(parent_ids)\n\n    def get_layers_in_layers_ids(self, layers_ids, layers=None):\n        \"\"\"Return all layers that belong to layers (might be groups).\n\n        Args:\n            layers_ids <list of Int>\n            layers <list of PSItem>:\n\n        Returns:\n            <list of PSItem>\n        \"\"\"\n        parent_ids = set(layers_ids)\n\n        return self._get_layers_in_layers(parent_ids, layers)\n\n    def _get_layers_in_layers(self, parent_ids, layers=None):\n        if not layers:\n            layers = self.get_layers()\n\n        all_layers = layers\n        ret = []\n\n        for layer in all_layers:\n            parents = set(layer.parents)\n            if len(parent_ids & parents) > 0:\n                ret.append(layer)\n            if layer.id in parent_ids:\n                ret.append(layer)\n\n        return ret\n\n    def create_group(self, name):\n        \"\"\"Create new group (eg. LayerSet)\n\n        Returns:\n            <PSItem>\n        \"\"\"\n        enhanced_name = self.PUBLISH_ICON + name\n        ret = self.websocketserver.call(\n            self.client.call('Photoshop.create_group', name=enhanced_name)\n        )\n        # create group on PS is asynchronous, returns only id\n        return PSItem(id=ret, name=name, group=True)\n\n    def group_selected_layers(self, name):\n        \"\"\"Group selected layers into new LayerSet (eg. group)\n\n        Returns:\n            (Layer)\n        \"\"\"\n        enhanced_name = self.PUBLISH_ICON + name\n        res = self.websocketserver.call(\n            self.client.call(\n                'Photoshop.group_selected_layers', name=enhanced_name\n            )\n        )\n        res = self._to_records(res)\n        if res:\n            rec = res.pop()\n            rec.name = rec.name.replace(self.PUBLISH_ICON, '')\n            return rec\n        raise ValueError(\"No group record returned\")\n\n    def get_selected_layers(self):\n        \"\"\"Get a list of actually selected layers.\n\n        Returns: <list of Layer('id':XX, 'name':\"YYY\")>\n        \"\"\"\n        res = self.websocketserver.call(\n            self.client.call('Photoshop.get_selected_layers')\n        )\n        return self._to_records(res)\n\n    def select_layers(self, layers):\n        \"\"\"Selects specified layers in Photoshop by its ids.\n\n        Args:\n            layers: <list of Layer('id':XX, 'name':\"YYY\")>\n        \"\"\"\n        layers_id = [str(lay.id) for lay in layers]\n        self.websocketserver.call(\n            self.client.call(\n                'Photoshop.select_layers',\n                layers=json.dumps(layers_id)\n            )\n        )\n\n    def get_active_document_full_name(self):\n        \"\"\"Returns full name with path of active document via ws call\n\n        Returns(string):\n            full path with name\n        \"\"\"\n        res = self.websocketserver.call(\n            self.client.call('Photoshop.get_active_document_full_name')\n        )\n\n        return res\n\n    def get_active_document_name(self):\n        \"\"\"Returns just a name of active document via ws call\n\n        Returns(string):\n            file name\n        \"\"\"\n        return self.websocketserver.call(\n            self.client.call('Photoshop.get_active_document_name')\n        )\n\n    def is_saved(self):\n        \"\"\"Returns true if no changes in active document\n\n        Returns:\n            <boolean>\n        \"\"\"\n        return self.websocketserver.call(\n            self.client.call('Photoshop.is_saved')\n        )\n\n    def save(self):\n        \"\"\"Saves active document\"\"\"\n        self.websocketserver.call(\n            self.client.call('Photoshop.save')\n        )\n\n    def saveAs(self, image_path, ext, as_copy):\n        \"\"\"Saves active document to psd (copy) or png or jpg\n\n        Args:\n            image_path(string): full local path\n            ext: <string psd|jpg|png>\n            as_copy: <boolean>\n        Returns: None\n        \"\"\"\n        self.websocketserver.call(\n            self.client.call(\n                'Photoshop.saveAs',\n                image_path=image_path,\n                ext=ext,\n                as_copy=as_copy\n            )\n        )\n\n    def set_visible(self, layer_id, visibility):\n        \"\"\"Set layer with 'layer_id' to 'visibility'\n\n        Args:\n            layer_id: <int>\n            visibility: <true - set visible, false - hide>\n        Returns: None\n        \"\"\"\n        self.websocketserver.call(\n            self.client.call(\n                'Photoshop.set_visible',\n                layer_id=layer_id,\n                visibility=visibility\n            )\n        )\n\n    def hide_all_others_layers(self, layers):\n        \"\"\"hides all layers that are not part of the list or that are not\n        children of this list\n\n        Args:\n            layers (list): list of PSItem - highest hierarchy\n        \"\"\"\n        extract_ids = set([ll.id for ll in self.get_layers_in_layers(layers)])\n\n        self.hide_all_others_layers_ids(extract_ids)\n\n    def hide_all_others_layers_ids(self, extract_ids, layers=None):\n        \"\"\"hides all layers that are not part of the list or that are not\n        children of this list\n\n        Args:\n            extract_ids (list): list of integer that should be visible\n            layers (list) of PSItem (used for caching)\n        \"\"\"\n        if not layers:\n            layers = self.get_layers()\n        for layer in layers:\n            if layer.visible and layer.id not in extract_ids:\n                self.set_visible(layer.id, False)\n\n    def get_layers_metadata(self):\n        \"\"\"Reads layers metadata from Headline from active document in PS.\n        (Headline accessible by File > File Info)\n\n        Returns:\n            (list)\n            example:\n                {\"8\":{\"active\":true,\"subset\":\"imageBG\",\n                      \"family\":\"image\",\"id\":\"pyblish.avalon.instance\",\n                      \"asset\":\"Town\"}}\n                8 is layer(group) id - used for deletion, update etc.\n        \"\"\"\n        res = self.websocketserver.call(self.client.call('Photoshop.read'))\n        layers_data = []\n        try:\n            if res:\n                layers_data = json.loads(res)\n        except json.decoder.JSONDecodeError:\n            raise ValueError(\"{} cannot be parsed, recreate meta\".format(res))\n        # format of metadata changed from {} to [] because of standardization\n        # keep current implementation logic as its working\n        if isinstance(layers_data, dict):\n            for layer_id, layer_meta in layers_data.items():\n                if layer_meta.get(\"schema\") != \"openpype:container-2.0\":\n                    layer_meta[\"members\"] = [str(layer_id)]\n            layers_data = list(layers_data.values())\n        return layers_data\n\n    def import_smart_object(self, path, layer_name, as_reference=False):\n        \"\"\"Import the file at `path` as a smart object to active document.\n\n        Args:\n            path (str): File path to import.\n            layer_name (str): Unique layer name to differentiate how many times\n                same smart object was loaded\n            as_reference (bool): pull in content or reference\n        \"\"\"\n        enhanced_name = self.LOADED_ICON + layer_name\n        res = self.websocketserver.call(\n            self.client.call(\n                'Photoshop.import_smart_object',\n                path=path,\n                name=enhanced_name,\n                as_reference=as_reference\n            )\n        )\n        rec = self._to_records(res).pop()\n        if rec:\n            rec.name = rec.name.replace(self.LOADED_ICON, '')\n        return rec\n\n    def replace_smart_object(self, layer, path, layer_name):\n        \"\"\"Replace the smart object `layer` with file at `path`\n\n        Args:\n            layer (PSItem):\n            path (str): File to import.\n            layer_name (str): Unique layer name to differentiate how many times\n                same smart object was loaded\n        \"\"\"\n        enhanced_name = self.LOADED_ICON + layer_name\n        self.websocketserver.call(\n            self.client.call(\n                'Photoshop.replace_smart_object',\n                layer_id=layer.id,\n                path=path,\n                name=enhanced_name\n            )\n        )\n\n    def delete_layer(self, layer_id):\n        \"\"\"Deletes specific layer by it's id.\n\n        Args:\n            layer_id (int): id of layer to delete\n        \"\"\"\n        self.websocketserver.call(\n            self.client.call('Photoshop.delete_layer', layer_id=layer_id)\n        )\n\n    def rename_layer(self, layer_id, name):\n        \"\"\"Renames specific layer by it's id.\n\n        Args:\n            layer_id (int): id of layer to delete\n            name (str): new name\n        \"\"\"\n        self.websocketserver.call(\n            self.client.call(\n                'Photoshop.rename_layer',\n                layer_id=layer_id,\n                name=name\n            )\n        )\n\n    def remove_instance(self, instance_id):\n        cleaned_data = []\n\n        for item in self.get_layers_metadata():\n            inst_id = item.get(\"instance_id\") or item.get(\"uuid\")\n            if inst_id != instance_id:\n                cleaned_data.append(item)\n\n        payload = json.dumps(cleaned_data, indent=4)\n\n        self.websocketserver.call(\n            self.client.call('Photoshop.imprint', payload=payload)\n        )\n\n    def get_extension_version(self):\n        \"\"\"Returns version number of installed extension.\"\"\"\n        return self.websocketserver.call(\n            self.client.call('Photoshop.get_extension_version')\n        )\n\n    def close(self):\n        \"\"\"Shutting down PS and process too.\n\n            For webpublishing only.\n        \"\"\"\n        # TODO change client.call to method with checks for client\n        self.websocketserver.call(self.client.call('Photoshop.close'))\n\n    def _to_records(self, res):\n        \"\"\"Converts string json representation into list of PSItem for\n        dot notation access to work.\n\n        Args:\n            res (string): valid json\n\n        Returns:\n            <list of PSItem>\n        \"\"\"\n        try:\n            layers_data = json.loads(res)\n        except json.decoder.JSONDecodeError:\n            raise ValueError(\"Received broken JSON {}\".format(res))\n        ret = []\n\n        # convert to AEItem to use dot donation\n        if isinstance(layers_data, dict):\n            layers_data = [layers_data]\n        for d in layers_data:\n            # currently implemented and expected fields\n            ret.append(PSItem(\n                d.get('id'),\n                d.get('name'),\n                d.get('group'),\n                d.get('parents'),\n                d.get('visible'),\n                d.get('type'),\n                d.get('members'),\n                d.get('long_name'),\n                d.get(\"color_code\"),\n                d.get(\"instance_id\")\n            ))\n        return ret\n"
  },
  {
    "path": "openpype/hosts/photoshop/lib.py",
    "content": "import re\n\nfrom openpype import AYON_SERVER_ENABLED\nimport openpype.hosts.photoshop.api as api\nfrom openpype.client import get_asset_by_name\nfrom openpype.lib import prepare_template_data\nfrom openpype.pipeline import (\n    AutoCreator,\n    CreatedInstance\n)\nfrom openpype.hosts.photoshop.api.pipeline import cache_and_get_instances\n\n\nclass PSAutoCreator(AutoCreator):\n    \"\"\"Generic autocreator to extend.\"\"\"\n    def get_instance_attr_defs(self):\n        return []\n\n    def collect_instances(self):\n        for instance_data in cache_and_get_instances(self):\n            creator_id = instance_data.get(\"creator_identifier\")\n\n            if creator_id == self.identifier:\n                instance = CreatedInstance.from_existing(\n                    instance_data, self\n                )\n                self._add_instance_to_context(instance)\n\n    def update_instances(self, update_list):\n        self.log.debug(\"update_list:: {}\".format(update_list))\n        for created_inst, _changes in update_list:\n            api.stub().imprint(created_inst.get(\"instance_id\"),\n                               created_inst.data_to_store())\n\n    def create(self, options=None):\n        existing_instance = None\n        for instance in self.create_context.instances:\n            if instance.family == self.family:\n                existing_instance = instance\n                break\n\n        context = self.create_context\n        project_name = context.get_current_project_name()\n        asset_name = context.get_current_asset_name()\n        task_name = context.get_current_task_name()\n        host_name = context.host_name\n\n        if existing_instance is None:\n            existing_instance_asset = None\n        elif AYON_SERVER_ENABLED:\n            existing_instance_asset = existing_instance[\"folderPath\"]\n        else:\n            existing_instance_asset = existing_instance[\"asset\"]\n\n        if existing_instance is None:\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                self.default_variant, task_name, asset_doc,\n                project_name, host_name\n            )\n            data = {\n                \"task\": task_name,\n                \"variant\": self.default_variant\n            }\n            if AYON_SERVER_ENABLED:\n                data[\"folderPath\"] = asset_name\n            else:\n                data[\"asset\"] = asset_name\n            data.update(self.get_dynamic_data(\n                self.default_variant, task_name, asset_doc,\n                project_name, host_name, None\n            ))\n\n            if not self.active_on_create:\n                data[\"active\"] = False\n\n            new_instance = CreatedInstance(\n                self.family, subset_name, data, self\n            )\n            self._add_instance_to_context(new_instance)\n            api.stub().imprint(new_instance.get(\"instance_id\"),\n                               new_instance.data_to_store())\n\n        elif (\n            existing_instance_asset != asset_name\n            or existing_instance[\"task\"] != task_name\n        ):\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                self.default_variant, task_name, asset_doc,\n                project_name, host_name\n            )\n            if AYON_SERVER_ENABLED:\n                existing_instance[\"folderPath\"] = asset_name\n            else:\n                existing_instance[\"asset\"] = asset_name\n            existing_instance[\"task\"] = task_name\n            existing_instance[\"subset\"] = subset_name\n\n\ndef clean_subset_name(subset_name):\n    \"\"\"Clean all variants leftover {layer} from subset name.\"\"\"\n    dynamic_data = prepare_template_data({\"layer\": \"{layer}\"})\n    for value in dynamic_data.values():\n        if value in subset_name:\n            subset_name = (subset_name.replace(value, \"\")\n                                      .replace(\"__\", \"_\")\n                                      .replace(\"..\", \".\"))\n    # clean trailing separator as Main_\n    pattern = r'[\\W_]+$'\n    replacement = ''\n    return re.sub(pattern, replacement, subset_name)\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/create/create_flatten_image.py",
    "content": "from openpype.pipeline import CreatedInstance\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.lib import BoolDef\nimport openpype.hosts.photoshop.api as api\nfrom openpype.hosts.photoshop.lib import PSAutoCreator, clean_subset_name\nfrom openpype.pipeline.create import get_subset_name\nfrom openpype.lib import prepare_template_data\nfrom openpype.client import get_asset_by_name\n\n\nclass AutoImageCreator(PSAutoCreator):\n    \"\"\"Creates flatten image from all visible layers.\n\n    Used in simplified publishing as auto created instance.\n    Must be enabled in Setting and template for subset name provided\n    \"\"\"\n    identifier = \"auto_image\"\n    family = \"image\"\n\n    # Settings\n    default_variant = \"\"\n    # - Mark by default instance for review\n    mark_for_review = True\n    active_on_create = True\n\n    def create(self, options=None):\n        existing_instance = None\n        for instance in self.create_context.instances:\n            if instance.creator_identifier == self.identifier:\n                existing_instance = instance\n                break\n\n        context = self.create_context\n        project_name = context.get_current_project_name()\n        asset_name = context.get_current_asset_name()\n        task_name = context.get_current_task_name()\n        host_name = context.host_name\n        asset_doc = get_asset_by_name(project_name, asset_name)\n\n        if existing_instance is None:\n            existing_instance_asset = None\n        elif AYON_SERVER_ENABLED:\n            existing_instance_asset = existing_instance[\"folderPath\"]\n        else:\n            existing_instance_asset = existing_instance[\"asset\"]\n\n        if existing_instance is None:\n            subset_name = self.get_subset_name(\n                self.default_variant, task_name, asset_doc,\n                project_name, host_name\n            )\n\n            data = {\n                \"task\": task_name,\n            }\n            if AYON_SERVER_ENABLED:\n                data[\"folderPath\"] = asset_name\n            else:\n                data[\"asset\"] = asset_name\n\n            if not self.active_on_create:\n                data[\"active\"] = False\n\n            creator_attributes = {\"mark_for_review\": self.mark_for_review}\n            data.update({\"creator_attributes\": creator_attributes})\n\n            new_instance = CreatedInstance(\n                self.family, subset_name, data, self\n            )\n            self._add_instance_to_context(new_instance)\n            api.stub().imprint(new_instance.get(\"instance_id\"),\n                               new_instance.data_to_store())\n\n        elif (  # existing instance from different context\n            existing_instance_asset != asset_name\n            or existing_instance[\"task\"] != task_name\n        ):\n            subset_name = self.get_subset_name(\n                self.default_variant, task_name, asset_doc,\n                project_name, host_name\n            )\n            if AYON_SERVER_ENABLED:\n                existing_instance[\"folderPath\"] = asset_name\n            else:\n                existing_instance[\"asset\"] = asset_name\n            existing_instance[\"task\"] = task_name\n            existing_instance[\"subset\"] = subset_name\n\n            api.stub().imprint(existing_instance.get(\"instance_id\"),\n                               existing_instance.data_to_store())\n\n    def get_pre_create_attr_defs(self):\n        return [\n            BoolDef(\n                \"mark_for_review\",\n                label=\"Review\",\n                default=self.mark_for_review\n            )\n        ]\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\n                \"mark_for_review\",\n                label=\"Review\"\n            )\n        ]\n\n    def apply_settings(self, project_settings):\n        plugin_settings = (\n            project_settings[\"photoshop\"][\"create\"][\"AutoImageCreator\"]\n        )\n\n        self.active_on_create = plugin_settings[\"active_on_create\"]\n        self.default_variant = plugin_settings[\"default_variant\"]\n        self.mark_for_review = plugin_settings[\"mark_for_review\"]\n        self.enabled = plugin_settings[\"enabled\"]\n\n    def get_detail_description(self):\n        return \"\"\"Creator for flatten image.\n\n        Studio might configure simple publishing workflow. In that case\n        `image` instance is automatically created which will publish flat\n        image from all visible layers.\n\n        Artist might disable this instance from publishing or from creating\n        review for it though.\n        \"\"\"\n\n    def get_subset_name(\n            self,\n            variant,\n            task_name,\n            asset_doc,\n            project_name,\n            host_name=None,\n            instance=None\n    ):\n        dynamic_data = prepare_template_data({\"layer\": \"{layer}\"})\n        subset_name = get_subset_name(\n            self.family, variant, task_name, asset_doc,\n            project_name, host_name, dynamic_data=dynamic_data\n        )\n        return clean_subset_name(subset_name)\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/create/create_image.py",
    "content": "import re\n\nfrom openpype.hosts.photoshop import api\nfrom openpype.lib import BoolDef\nfrom openpype.pipeline import (\n    Creator,\n    CreatedInstance,\n    CreatorError\n)\nfrom openpype.lib import prepare_template_data\nfrom openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS\nfrom openpype.hosts.photoshop.api.pipeline import cache_and_get_instances\nfrom openpype.hosts.photoshop.lib import clean_subset_name\n\n\nclass ImageCreator(Creator):\n    \"\"\"Creates image instance for publishing.\n\n    Result of 'image' instance is image of all visible layers, or image(s) of\n    selected layers.\n    \"\"\"\n    identifier = \"image\"\n    label = \"Image\"\n    family = \"image\"\n    description = \"Image creator\"\n\n    # Settings\n    default_variants = \"\"\n    mark_for_review = False\n    active_on_create = True\n\n    def create(self, subset_name_from_ui, data, pre_create_data):\n        groups_to_create = []\n        top_layers_to_wrap = []\n        create_empty_group = False\n\n        stub = api.stub()  # only after PS is up\n        top_level_selected_items = stub.get_selected_layers()\n        if pre_create_data.get(\"use_selection\"):\n            only_single_item_selected = len(top_level_selected_items) == 1\n            if (\n                    only_single_item_selected or\n                    pre_create_data.get(\"create_multiple\")):\n                for selected_item in top_level_selected_items:\n                    if selected_item.group:\n                        groups_to_create.append(selected_item)\n                    else:\n                        top_layers_to_wrap.append(selected_item)\n            else:\n                group = stub.group_selected_layers(subset_name_from_ui)\n                groups_to_create.append(group)\n        else:\n            stub.select_layers(stub.get_layers())\n            try:\n                group = stub.group_selected_layers(subset_name_from_ui)\n            except:\n                raise CreatorError(\"Cannot group locked Background layer!\")\n            groups_to_create.append(group)\n\n        # create empty group if nothing selected\n        if not groups_to_create and not top_layers_to_wrap:\n            group = stub.create_group(subset_name_from_ui)\n            groups_to_create.append(group)\n\n        # wrap each top level layer into separate new group\n        for layer in top_layers_to_wrap:\n            stub.select_layers([layer])\n            group = stub.group_selected_layers(layer.name)\n            groups_to_create.append(group)\n\n        layer_name = ''\n        # use artist chosen option OR force layer if more subsets are created\n        # to differentiate them\n        use_layer_name = (pre_create_data.get(\"use_layer_name\") or\n                          len(groups_to_create) > 1)\n        for group in groups_to_create:\n            subset_name = subset_name_from_ui  # reset to name from creator UI\n            layer_names_in_hierarchy = []\n            created_group_name = self._clean_highlights(stub, group.name)\n\n            if use_layer_name:\n                layer_name = re.sub(\n                    \"[^{}]+\".format(SUBSET_NAME_ALLOWED_SYMBOLS),\n                    \"\",\n                    group.name\n                )\n                if \"{layer}\" not in subset_name.lower():\n                    subset_name += \"{Layer}\"\n\n            layer_fill = prepare_template_data({\"layer\": layer_name})\n            subset_name = subset_name.format(**layer_fill)\n            subset_name = clean_subset_name(subset_name)\n\n            if group.long_name:\n                for directory in group.long_name[::-1]:\n                    name = self._clean_highlights(stub, directory)\n                    layer_names_in_hierarchy.append(name)\n\n            data_update = {\n                \"subset\": subset_name,\n                \"members\": [str(group.id)],\n                \"layer_name\": layer_name,\n                \"long_name\": \"_\".join(layer_names_in_hierarchy)\n            }\n            data.update(data_update)\n\n            mark_for_review = (pre_create_data.get(\"mark_for_review\") or\n                               self.mark_for_review)\n            creator_attributes = {\"mark_for_review\": mark_for_review}\n            data.update({\"creator_attributes\": creator_attributes})\n\n            if not self.active_on_create:\n                data[\"active\"] = False\n\n            new_instance = CreatedInstance(self.family, subset_name, data,\n                                           self)\n\n            stub.imprint(new_instance.get(\"instance_id\"),\n                         new_instance.data_to_store())\n            self._add_instance_to_context(new_instance)\n            # reusing existing group, need to rename afterwards\n            if not create_empty_group:\n                stub.rename_layer(group.id,\n                                  stub.PUBLISH_ICON + created_group_name)\n\n    def collect_instances(self):\n        for instance_data in cache_and_get_instances(self):\n            # legacy instances have family=='image'\n            creator_id = (instance_data.get(\"creator_identifier\") or\n                          instance_data.get(\"family\"))\n\n            if creator_id == self.identifier:\n                instance_data = self._handle_legacy(instance_data)\n                instance = CreatedInstance.from_existing(\n                    instance_data, self\n                )\n                self._add_instance_to_context(instance)\n\n    def update_instances(self, update_list):\n        self.log.debug(\"update_list:: {}\".format(update_list))\n        for created_inst, _changes in update_list:\n            if created_inst.get(\"layer\"):\n                # not storing PSItem layer to metadata\n                created_inst.pop(\"layer\")\n            api.stub().imprint(created_inst.get(\"instance_id\"),\n                               created_inst.data_to_store())\n\n    def remove_instances(self, instances):\n        for instance in instances:\n            self.host.remove_instance(instance)\n            self._remove_instance_from_context(instance)\n\n    def get_pre_create_attr_defs(self):\n        output = [\n            BoolDef(\"use_selection\", default=True,\n                    label=\"Create only for selected\"),\n            BoolDef(\"create_multiple\",\n                    default=True,\n                    label=\"Create separate instance for each selected\"),\n            BoolDef(\"use_layer_name\",\n                    default=False,\n                    label=\"Use layer name in subset\"),\n            BoolDef(\n                \"mark_for_review\",\n                label=\"Create separate review\",\n                default=False\n            )\n        ]\n        return output\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\n                \"mark_for_review\",\n                label=\"Review\"\n            )\n        ]\n\n    def apply_settings(self, project_settings):\n        plugin_settings = (\n            project_settings[\"photoshop\"][\"create\"][\"ImageCreator\"]\n        )\n\n        self.active_on_create = plugin_settings[\"active_on_create\"]\n        self.default_variants = plugin_settings[\"default_variants\"]\n        self.mark_for_review = plugin_settings[\"mark_for_review\"]\n        self.enabled = plugin_settings[\"enabled\"]\n\n    def get_detail_description(self):\n        return \"\"\"Creator for Image instances\n\n        Main publishable item in Photoshop will be of `image` family. Result of\n        this item (instance) is picture that could be loaded and used\n        in another DCCs (for example as single layer in composition in\n        AfterEffects, reference in Maya etc).\n\n        There are couple of options what to publish:\n        - separate image per selected layer (or group of layers)\n        - one image for all selected layers\n        - all visible layers (groups) flattened into single image\n\n        In most cases you would like to keep `Create only for selected`\n        toggled on and select what you would like to publish.\n        Toggling this option off will allow you to create instance for all\n        visible layers without a need to select them explicitly.\n\n        Use 'Create separate instance for each selected' to create separate\n        images per selected layer (group of layers).\n\n        'Use layer name in subset' will explicitly add layer name into subset\n        name. Position of this name is configurable in\n        `project_settings/global/tools/creator/subset_name_profiles`.\n        If layer placeholder ({layer}) is not used in `subset_name_profiles`\n        but layer name should be used (set explicitly in UI or implicitly if\n        multiple images should be created), it is added in capitalized form\n        as a suffix to subset name.\n\n        Each image could have its separate review created if necessary via\n        `Create separate review` toggle.\n        But more use case is to use separate `review` instance to create review\n        from all published items.\n        \"\"\"\n\n    def _handle_legacy(self, instance_data):\n        \"\"\"Converts old instances to new format.\"\"\"\n        if not instance_data.get(\"members\"):\n            instance_data[\"members\"] = [instance_data.get(\"uuid\")]\n\n        if instance_data.get(\"uuid\"):\n            # uuid not needed, replaced with unique instance_id\n            api.stub().remove_instance(instance_data.get(\"uuid\"))\n            instance_data.pop(\"uuid\")\n\n        if not instance_data.get(\"task\"):\n            instance_data[\"task\"] = self.create_context.get_current_task_name()\n\n        if not instance_data.get(\"variant\"):\n            instance_data[\"variant\"] = ''\n\n        return instance_data\n\n    def _clean_highlights(self, stub, item):\n        return item.replace(stub.PUBLISH_ICON, '').replace(stub.LOADED_ICON,\n                                                           '')\n\n    def get_dynamic_data(self, variant, task_name, asset_doc,\n                         project_name, host_name, instance):\n        if instance is not None:\n            layer_name = instance.get(\"layer_name\")\n            if layer_name:\n                return {\"layer\": layer_name}\n        return {\"layer\": \"{layer}\"}\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/create/create_review.py",
    "content": "from openpype.hosts.photoshop.lib import PSAutoCreator\n\n\nclass ReviewCreator(PSAutoCreator):\n    \"\"\"Creates review instance which might be disabled from publishing.\"\"\"\n    identifier = \"review\"\n    family = \"review\"\n\n    default_variant = \"Main\"\n\n    def get_detail_description(self):\n        return \"\"\"Auto creator for review.\n\n        Photoshop review is created from all published images or from all\n        visible layers if no `image` instances got created.\n\n        Review might be disabled by an artist (instance shouldn't be deleted as\n        it will get recreated in next publish either way).\n        \"\"\"\n\n    def apply_settings(self, project_settings):\n        plugin_settings = (\n            project_settings[\"photoshop\"][\"create\"][\"ReviewCreator\"]\n        )\n\n        self.default_variant = plugin_settings[\"default_variant\"]\n        self.active_on_create = plugin_settings[\"active_on_create\"]\n        self.enabled = plugin_settings[\"enabled\"]\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/create/create_workfile.py",
    "content": "from openpype.hosts.photoshop.lib import PSAutoCreator\n\n\nclass WorkfileCreator(PSAutoCreator):\n    identifier = \"workfile\"\n    family = \"workfile\"\n\n    default_variant = \"Main\"\n\n    def get_detail_description(self):\n        return \"\"\"Auto creator for workfile.\n\n        It is expected that each publish will also publish its source workfile\n        for safekeeping. This creator triggers automatically without need for\n        an artist to remember and trigger it explicitly.\n\n        Workfile instance could be disabled if it is not required to publish\n        workfile. (Instance shouldn't be deleted though as it will be recreated\n        in next publish automatically).\n        \"\"\"\n\n    def apply_settings(self, project_settings):\n        plugin_settings = (\n            project_settings[\"photoshop\"][\"create\"][\"WorkfileCreator\"]\n        )\n\n        self.active_on_create = plugin_settings[\"active_on_create\"]\n        self.enabled = plugin_settings[\"enabled\"]\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/load/load_image.py",
    "content": "import re\n\nfrom openpype.pipeline import get_representation_path\nfrom openpype.hosts.photoshop import api as photoshop\nfrom openpype.hosts.photoshop.api import get_unique_layer_name\n\n\nclass ImageLoader(photoshop.PhotoshopLoader):\n    \"\"\"Load images\n\n    Stores the imported asset in a container named after the asset.\n    \"\"\"\n\n    families = [\"image\", \"render\"]\n    representations = [\"*\"]\n\n    def load(self, context, name=None, namespace=None, data=None):\n        stub = self.get_stub()\n        layer_name = get_unique_layer_name(\n            stub.get_layers(),\n            context[\"asset\"][\"name\"],\n            name\n        )\n        with photoshop.maintained_selection():\n            path = self.filepath_from_context(context)\n            layer = self.import_layer(path, layer_name, stub)\n\n        self[:] = [layer]\n        namespace = namespace or layer_name\n\n        return photoshop.containerise(\n            name,\n            namespace,\n            layer,\n            context,\n            self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n        \"\"\" Switch asset or change version \"\"\"\n        stub = self.get_stub()\n\n        layer = container.pop(\"layer\")\n\n        context = representation.get(\"context\", {})\n\n        namespace_from_container = re.sub(r'_\\d{3}$', '',\n                                          container[\"namespace\"])\n        layer_name = \"{}_{}\".format(context[\"asset\"], context[\"subset\"])\n        # switching assets\n        if namespace_from_container != layer_name:\n            layer_name = get_unique_layer_name(\n                stub.get_layers(), context[\"asset\"], context[\"subset\"]\n            )\n        else:  # switching version - keep same name\n            layer_name = container[\"namespace\"]\n\n        path = get_representation_path(representation)\n        with photoshop.maintained_selection():\n            stub.replace_smart_object(\n                layer, path, layer_name\n            )\n\n        stub.imprint(\n            layer.id, {\"representation\": str(representation[\"_id\"])}\n        )\n\n    def remove(self, container):\n        \"\"\"\n            Removes element from scene: deletes layer + removes from Headline\n        Args:\n            container (dict): container to be removed - used to get layer_id\n        \"\"\"\n        stub = self.get_stub()\n\n        layer = container.pop(\"layer\")\n        stub.imprint(layer.id, {})\n        stub.delete_layer(layer.id)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def import_layer(self, file_name, layer_name, stub):\n        return stub.import_smart_object(file_name, layer_name)\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py",
    "content": "import os\n\nimport qargparse\n\nfrom openpype.hosts.photoshop import api as photoshop\nfrom openpype.hosts.photoshop.api import get_unique_layer_name\n\n\nclass ImageFromSequenceLoader(photoshop.PhotoshopLoader):\n    \"\"\" Load specific image from sequence\n\n        Used only as quick load of reference file from a sequence.\n\n        Plain ImageLoader picks first frame from sequence.\n\n        Loads only existing files - currently not possible to limit loaders\n        to single select - multiselect. If user selects multiple repres, list\n        for all of them is provided, but selection is only single file.\n        This loader will be triggered multiple times, but selected name will\n        match only to proper path.\n\n        Loader doesnt do containerization as there is currently no data model\n        of 'frame of rendered files' (only rendered sequence), update would be\n        difficult.\n    \"\"\"\n\n    families = [\"render\"]\n    representations = [\"*\"]\n    options = []\n\n    def load(self, context, name=None, namespace=None, data=None):\n\n        path = self.filepath_from_context(context)\n        if data.get(\"frame\"):\n            path = os.path.join(\n                os.path.dirname(path), data[\"frame\"]\n            )\n            if not os.path.exists(path):\n                return\n\n        stub = self.get_stub()\n        layer_name = get_unique_layer_name(\n            stub.get_layers(), context[\"asset\"][\"name\"], name\n        )\n\n        with photoshop.maintained_selection():\n            layer = stub.import_smart_object(path, layer_name)\n\n        self[:] = [layer]\n        namespace = namespace or layer_name\n\n        return namespace\n\n    @classmethod\n    def get_options(cls, repre_contexts):\n        \"\"\"\n            Returns list of files for selected 'repre_contexts'.\n\n            It returns only files with same extension as in context as it is\n            expected that context points to sequence of frames.\n\n            Returns:\n                (list) of qargparse.Choice\n        \"\"\"\n        files = []\n        for context in repre_contexts:\n            fname = cls.filepath_from_context(context)\n            _, file_extension = os.path.splitext(fname)\n\n            for file_name in os.listdir(os.path.dirname(fname)):\n                if not file_name.endswith(file_extension):\n                    continue\n                files.append(file_name)\n\n        # return selection only if there is something\n        if not files or len(files) <= 1:\n            return []\n\n        return [\n            qargparse.Choice(\n                \"frame\",\n                label=\"Select specific file\",\n                items=files,\n                default=0,\n                help=\"Which frame should be loaded?\"\n            )\n        ]\n\n    def update(self, container, representation):\n        \"\"\"No update possible, not containerized.\"\"\"\n        pass\n\n    def remove(self, container):\n        \"\"\"No update possible, not containerized.\"\"\"\n        pass\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/load/load_reference.py",
    "content": "import re\n\nfrom openpype.pipeline import get_representation_path\nfrom openpype.hosts.photoshop import api as photoshop\nfrom openpype.hosts.photoshop.api import get_unique_layer_name\n\n\nclass ReferenceLoader(photoshop.PhotoshopLoader):\n    \"\"\"Load reference images\n\n    Stores the imported asset in a container named after the asset.\n\n    Inheriting from 'load_image' didn't work because of\n    \"Cannot write to closing transport\", possible refactor.\n    \"\"\"\n\n    families = [\"image\", \"render\"]\n    representations = [\"*\"]\n\n    def load(self, context, name=None, namespace=None, data=None):\n        stub = self.get_stub()\n        layer_name = get_unique_layer_name(\n            stub.get_layers(), context[\"asset\"][\"name\"], name\n        )\n        with photoshop.maintained_selection():\n            path = self.filepath_from_context(context)\n            layer = self.import_layer(path, layer_name, stub)\n\n        self[:] = [layer]\n        namespace = namespace or layer_name\n\n        return photoshop.containerise(\n            name,\n            namespace,\n            layer,\n            context,\n            self.__class__.__name__\n        )\n\n    def update(self, container, representation):\n        \"\"\" Switch asset or change version \"\"\"\n        stub = self.get_stub()\n        layer = container.pop(\"layer\")\n\n        context = representation.get(\"context\", {})\n\n        namespace_from_container = re.sub(r'_\\d{3}$', '',\n                                          container[\"namespace\"])\n        layer_name = \"{}_{}\".format(context[\"asset\"], context[\"subset\"])\n        # switching assets\n        if namespace_from_container != layer_name:\n            layer_name = get_unique_layer_name(\n                stub.get_layers(), context[\"asset\"], context[\"subset\"]\n            )\n        else:  # switching version - keep same name\n            layer_name = container[\"namespace\"]\n\n        path = get_representation_path(representation)\n        with photoshop.maintained_selection():\n            stub.replace_smart_object(\n                layer, path, layer_name\n            )\n\n        stub.imprint(\n            layer.id, {\"representation\": str(representation[\"_id\"])}\n        )\n\n    def remove(self, container):\n        \"\"\"Removes element from scene: deletes layer + removes from Headline\n\n        Args:\n            container (dict): container to be removed - used to get layer_id\n        \"\"\"\n        stub = self.get_stub()\n        layer = container.pop(\"layer\")\n        stub.imprint(layer.id, {})\n        stub.delete_layer(layer.id)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def import_layer(self, file_name, layer_name, stub):\n        return stub.import_smart_object(\n            file_name, layer_name, as_reference=True\n        )\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/closePS.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Close PS after publish. For Webpublishing only.\"\"\"\nimport os\n\nimport pyblish.api\n\nfrom openpype.hosts.photoshop import api as photoshop\n\n\nclass ClosePS(pyblish.api.ContextPlugin):\n    \"\"\"Close PS after publish. For Webpublishing only.\n    \"\"\"\n\n    order = pyblish.api.IntegratorOrder + 14\n    label = \"Close PS\"\n    optional = True\n    active = True\n\n    hosts = [\"photoshop\"]\n    targets = [\"automated\"]\n\n    def process(self, context):\n        self.log.info(\"ClosePS\")\n\n        stub = photoshop.stub()\n        self.log.info(\"Shutting down PS\")\n        stub.save()\n        stub.close()\n        self.log.info(\"PS closed\")\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_auto_image.py",
    "content": "import pyblish.api\n\nfrom openpype.client import get_asset_name_identifier\nfrom openpype.hosts.photoshop import api as photoshop\nfrom openpype.pipeline.create import get_subset_name\n\n\nclass CollectAutoImage(pyblish.api.ContextPlugin):\n    \"\"\"Creates auto image in non artist based publishes (Webpublisher).\n    \"\"\"\n\n    label = \"Collect Auto Image\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"photoshop\"]\n    order = pyblish.api.CollectorOrder + 0.2\n\n    targets = [\"automated\"]\n\n    def process(self, context):\n        for instance in context:\n            creator_identifier = instance.data.get(\"creator_identifier\")\n            if creator_identifier and creator_identifier == \"auto_image\":\n                self.log.debug(\"Auto image instance found, won't create new\")\n                return\n\n        project_name = context.data[\"projectName\"]\n        proj_settings = context.data[\"project_settings\"]\n        task_name = context.data[\"task\"]\n        host_name = context.data[\"hostName\"]\n        asset_doc = context.data[\"assetEntity\"]\n        asset_name = get_asset_name_identifier(asset_doc)\n\n        auto_creator = proj_settings.get(\n            \"photoshop\", {}).get(\n            \"create\", {}).get(\n            \"AutoImageCreator\", {})\n\n        if not auto_creator or not auto_creator[\"enabled\"]:\n            self.log.debug(\"Auto image creator disabled, won't create new\")\n            return\n\n        stub = photoshop.stub()\n        stored_items = stub.get_layers_metadata()\n        for item in stored_items:\n            if item.get(\"creator_identifier\") == \"auto_image\":\n                if not item.get(\"active\"):\n                    self.log.debug(\"Auto_image instance disabled\")\n                    return\n\n        layer_items = stub.get_layers()\n\n        publishable_ids = [layer.id for layer in layer_items\n                           if layer.visible]\n\n        # collect stored image instances\n        instance_names = []\n        for layer_item in layer_items:\n            layer_meta_data = stub.read(layer_item, stored_items)\n\n            # Skip layers without metadata.\n            if layer_meta_data is None:\n                continue\n\n            # Skip containers.\n            if \"container\" in layer_meta_data[\"id\"]:\n                continue\n\n            # active might not be in legacy meta\n            if layer_meta_data.get(\"active\", True) and layer_item.visible:\n                instance_names.append(layer_meta_data[\"subset\"])\n\n        if len(instance_names) == 0:\n            variants = proj_settings.get(\n                \"photoshop\", {}).get(\n                \"create\", {}).get(\n                \"CreateImage\", {}).get(\n                \"default_variants\", [''])\n            family = \"image\"\n\n            variant = context.data.get(\"variant\") or variants[0]\n\n            subset_name = get_subset_name(\n                family, variant, task_name, asset_doc,\n                project_name, host_name\n            )\n\n            instance = context.create_instance(subset_name)\n            instance.data[\"family\"] = family\n            instance.data[\"asset\"] = asset_name\n            instance.data[\"subset\"] = subset_name\n            instance.data[\"ids\"] = publishable_ids\n            instance.data[\"publish\"] = True\n            instance.data[\"creator_identifier\"] = \"auto_image\"\n\n            if auto_creator[\"mark_for_review\"]:\n                instance.data[\"creator_attributes\"] = {\"mark_for_review\": True}\n                instance.data[\"families\"] = [\"review\"]\n\n            self.log.info(\"auto image instance: {} \".format(instance.data))\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py",
    "content": "import pyblish.api\n\nfrom openpype.hosts.photoshop import api as photoshop\n\n\nclass CollectAutoImageRefresh(pyblish.api.ContextPlugin):\n    \"\"\"Refreshes auto_image instance with currently visible layers..\n    \"\"\"\n\n    label = \"Collect Auto Image Refresh\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"photoshop\"]\n    order = pyblish.api.CollectorOrder + 0.2\n\n    def process(self, context):\n        for instance in context:\n            creator_identifier = instance.data.get(\"creator_identifier\")\n            if creator_identifier and creator_identifier == \"auto_image\":\n                self.log.debug(\"Auto image instance found, won't create new\")\n                # refresh existing auto image instance with current visible\n                publishable_ids = [layer.id for layer in photoshop.stub().get_layers()  # noqa\n                                   if layer.visible]\n                instance.data[\"ids\"] = publishable_ids\n                return\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_auto_review.py",
    "content": "\"\"\"\nRequires:\n    None\n\nProvides:\n    instance     -> family (\"review\")\n\"\"\"\nimport pyblish.api\n\nfrom openpype.client import get_asset_name_identifier\nfrom openpype.hosts.photoshop import api as photoshop\nfrom openpype.pipeline.create import get_subset_name\n\n\nclass CollectAutoReview(pyblish.api.ContextPlugin):\n    \"\"\"Create review instance in non artist based workflow.\n\n    Called only if PS is triggered in Webpublisher or in tests.\n    \"\"\"\n\n    label = \"Collect Auto Review\"\n    hosts = [\"photoshop\"]\n    order = pyblish.api.CollectorOrder + 0.2\n    targets = [\"automated\"]\n\n    publish = True\n\n    def process(self, context):\n        family = \"review\"\n        has_review = False\n        for instance in context:\n            if instance.data[\"family\"] == family:\n                self.log.debug(\"Review instance found, won't create new\")\n                has_review = True\n\n            creator_attributes = instance.data.get(\"creator_attributes\", {})\n            if (creator_attributes.get(\"mark_for_review\") and\n                    \"review\" not in instance.data[\"families\"]):\n                instance.data[\"families\"].append(\"review\")\n\n        if has_review:\n            return\n\n        stub = photoshop.stub()\n        stored_items = stub.get_layers_metadata()\n        for item in stored_items:\n            if item.get(\"creator_identifier\") == family:\n                if not item.get(\"active\"):\n                    self.log.debug(\"Review instance disabled\")\n                    return\n\n        auto_creator = context.data[\"project_settings\"].get(\n            \"photoshop\", {}).get(\n            \"create\", {}).get(\n            \"ReviewCreator\", {})\n\n        if not auto_creator or not auto_creator[\"enabled\"]:\n            self.log.debug(\"Review creator disabled, won't create new\")\n            return\n\n        variant = (context.data.get(\"variant\") or\n                   auto_creator[\"default_variant\"])\n\n        project_name = context.data[\"projectName\"]\n        proj_settings = context.data[\"project_settings\"]\n        task_name = context.data[\"task\"]\n        host_name = context.data[\"hostName\"]\n        asset_doc = context.data[\"assetEntity\"]\n\n        asset_name = get_asset_name_identifier(asset_doc)\n\n        subset_name = get_subset_name(\n            family,\n            variant,\n            task_name,\n            asset_doc,\n            project_name,\n            host_name=host_name,\n            project_settings=proj_settings\n        )\n\n        instance = context.create_instance(subset_name)\n        instance.data.update({\n            \"subset\": subset_name,\n            \"label\": subset_name,\n            \"name\": subset_name,\n            \"family\": family,\n            \"families\": [],\n            \"representations\": [],\n            \"asset\": asset_name,\n            \"publish\": self.publish\n        })\n\n        self.log.debug(\"auto review created::{}\".format(instance.data))\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py",
    "content": "import os\nimport pyblish.api\n\nfrom openpype.client import get_asset_name_identifier\nfrom openpype.hosts.photoshop import api as photoshop\nfrom openpype.pipeline.create import get_subset_name\n\n\nclass CollectAutoWorkfile(pyblish.api.ContextPlugin):\n    \"\"\"Collect current script for publish.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.2\n    label = \"Collect Workfile\"\n    hosts = [\"photoshop\"]\n\n    targets = [\"automated\"]\n\n    def process(self, context):\n        family = \"workfile\"\n        file_path = context.data[\"currentFile\"]\n        _, ext = os.path.splitext(file_path)\n        staging_dir = os.path.dirname(file_path)\n        base_name = os.path.basename(file_path)\n        workfile_representation = {\n            \"name\": ext[1:],\n            \"ext\": ext[1:],\n            \"files\": base_name,\n            \"stagingDir\": staging_dir,\n        }\n\n        for instance in context:\n            if instance.data[\"family\"] == family:\n                self.log.debug(\"Workfile instance found, won't create new\")\n                instance.data.update({\n                    \"label\": base_name,\n                    \"name\": base_name,\n                    \"representations\": [],\n                })\n\n                # creating representation\n                _, ext = os.path.splitext(file_path)\n                instance.data[\"representations\"].append(\n                    workfile_representation)\n\n                return\n\n        stub = photoshop.stub()\n        stored_items = stub.get_layers_metadata()\n        for item in stored_items:\n            if item.get(\"creator_identifier\") == family:\n                if not item.get(\"active\"):\n                    self.log.debug(\"Workfile instance disabled\")\n                    return\n\n        project_name = context.data[\"projectName\"]\n        proj_settings = context.data[\"project_settings\"]\n        auto_creator = proj_settings.get(\n            \"photoshop\", {}).get(\n            \"create\", {}).get(\n            \"WorkfileCreator\", {})\n\n        if not auto_creator or not auto_creator[\"enabled\"]:\n            self.log.debug(\"Workfile creator disabled, won't create new\")\n            return\n\n        # context.data[\"variant\"] might come only from collect_batch_data\n        variant = (context.data.get(\"variant\") or\n                   auto_creator[\"default_variant\"])\n\n        task_name = context.data[\"task\"]\n        host_name = context.data[\"hostName\"]\n        asset_doc = context.data[\"assetEntity\"]\n\n        asset_name = get_asset_name_identifier(asset_doc)\n        subset_name = get_subset_name(\n            family,\n            variant,\n            task_name,\n            asset_doc,\n            project_name,\n            host_name=host_name,\n            project_settings=proj_settings\n        )\n\n        # Create instance\n        instance = context.create_instance(subset_name)\n        instance.data.update({\n            \"subset\": subset_name,\n            \"label\": base_name,\n            \"name\": base_name,\n            \"family\": family,\n            \"families\": [],\n            \"representations\": [],\n            \"asset\": asset_name\n        })\n\n        # creating representation\n        instance.data[\"representations\"].append(workfile_representation)\n\n        self.log.debug(\"auto workfile review created:{}\".format(instance.data))\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_batch_data.py",
    "content": "\"\"\"Parses batch context from json and continues in publish process.\n\nProvides:\n    context -> Loaded batch file.\n        - asset\n        - task  (task name)\n        - taskType\n        - project_name\n        - variant\n\nCode is practically copy of `openype/hosts/webpublish/collect_batch_data` as\nwebpublisher should be eventually ejected as an addon, eg. mentioned plugin\nshouldn't be pushed into general publish plugins.\n\"\"\"\n\nimport os\n\nimport pyblish.api\n\nfrom openpype.pipeline import legacy_io\nfrom openpype_modules.webpublisher.lib import (\n    get_batch_asset_task_info,\n    parse_json\n)\nfrom openpype.tests.lib import is_in_tests\n\n\nclass CollectBatchData(pyblish.api.ContextPlugin):\n    \"\"\"Collect batch data from json stored in 'OPENPYPE_PUBLISH_DATA' env dir.\n\n    The directory must contain 'manifest.json' file where batch data should be\n    stored.\n    \"\"\"\n    # must be really early, context values are only in json file\n    order = pyblish.api.CollectorOrder - 0.495\n    label = \"Collect batch data\"\n    hosts = [\"photoshop\"]\n    targets = [\"webpublish\"]\n\n    def process(self, context):\n        self.log.info(\"CollectBatchData\")\n        batch_dir = os.environ.get(\"OPENPYPE_PUBLISH_DATA\")\n        if is_in_tests():\n            self.log.debug(\"Automatic testing, no batch data, skipping\")\n            return\n\n        assert batch_dir, (\n            \"Missing `OPENPYPE_PUBLISH_DATA`\")\n\n        assert os.path.exists(batch_dir), \\\n            \"Folder {} doesn't exist\".format(batch_dir)\n\n        project_name = os.environ.get(\"AVALON_PROJECT\")\n        if project_name is None:\n            raise AssertionError(\n                \"Environment `AVALON_PROJECT` was not found.\"\n                \"Could not set project `root` which may cause issues.\"\n            )\n\n        batch_data = parse_json(os.path.join(batch_dir, \"manifest.json\"))\n\n        context.data[\"batchDir\"] = batch_dir\n        context.data[\"batchData\"] = batch_data\n\n        asset_name, task_name, task_type = get_batch_asset_task_info(\n            batch_data[\"context\"]\n        )\n\n        os.environ[\"AVALON_ASSET\"] = asset_name\n        os.environ[\"AVALON_TASK\"] = task_name\n        legacy_io.Session[\"AVALON_ASSET\"] = asset_name\n        legacy_io.Session[\"AVALON_TASK\"] = task_name\n\n        context.data[\"asset\"] = asset_name\n        context.data[\"task\"] = task_name\n        context.data[\"taskType\"] = task_type\n        context.data[\"project_name\"] = project_name\n        context.data[\"variant\"] = batch_data[\"variant\"]\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py",
    "content": "import os\nimport re\n\nimport pyblish.api\n\nfrom openpype.lib import prepare_template_data\nfrom openpype.hosts.photoshop import api as photoshop\nfrom openpype.settings import get_project_settings\nfrom openpype.tests.lib import is_in_tests\n\n\nclass CollectColorCodedInstances(pyblish.api.ContextPlugin):\n    \"\"\"Creates instances for layers marked by configurable color.\n\n    Used in remote publishing when artists marks publishable layers by color-\n    coding. Top level layers (group) must be marked by specific color to be\n    published as an instance of 'image' family.\n\n    Can add group for all publishable layers to allow creation of flattened\n    image. (Cannot contain special background layer as it cannot be grouped!)\n\n    Based on value `create_flatten_image` from Settings:\n    - \"yes\": create flattened 'image' subset of all publishable layers + create\n        'image' subset per publishable layer\n    - \"only\": create ONLY flattened 'image' subset of all publishable layers\n    - \"no\": do not create flattened 'image' subset at all,\n        only separate subsets per marked layer.\n\n    Identifier:\n        id (str): \"pyblish.avalon.instance\"\n    \"\"\"\n    order = pyblish.api.CollectorOrder + 0.100\n\n    label = \"Instances\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"photoshop\"]\n    targets = [\"automated\"]\n\n    # configurable by Settings\n    color_code_mapping = []\n    # TODO check if could be set globally, probably doesn't make sense when\n    # flattened template cannot\n    subset_template_name = \"\"\n    create_flatten_image = \"no\"\n    flatten_subset_template = \"\"\n\n    def process(self, context):\n        self.log.info(\"CollectColorCodedInstances\")\n        batch_dir = os.environ.get(\"OPENPYPE_PUBLISH_DATA\")\n        if (is_in_tests() and\n                (not batch_dir or not os.path.exists(batch_dir))):\n            self.log.debug(\"Automatic testing, no batch data, skipping\")\n            return\n\n        existing_subset_names = self._get_existing_subset_names(context)\n\n        # from CollectBatchData\n        asset_name = context.data[\"asset\"]\n        task_name = context.data[\"task\"]\n        variant = context.data[\"variant\"]\n        project_name = context.data[\"projectEntity\"][\"name\"]\n\n        naming_conventions = get_project_settings(project_name).get(\n            \"photoshop\", {}).get(\n            \"publish\", {}).get(\n            \"ValidateNaming\", {})\n\n        stub = photoshop.stub()\n        layers = stub.get_layers()\n\n        publishable_layers = []\n        created_instances = []\n        family_from_settings = None\n        for layer in layers:\n            self.log.debug(\"Layer:: {}\".format(layer))\n            if layer.parents:\n                self.log.debug(\"!!! Not a top layer, skip\")\n                continue\n\n            if not layer.visible:\n                self.log.debug(\"Not visible, skip\")\n                continue\n\n            resolved_family, resolved_subset_template = self._resolve_mapping(\n                layer\n            )\n\n            if not resolved_subset_template or not resolved_family:\n                self.log.debug(\"!!! Not found family or template, skip\")\n                continue\n\n            if not family_from_settings:\n                family_from_settings = resolved_family\n\n            fill_pairs = {\n                \"variant\": variant,\n                \"family\": resolved_family,\n                \"task\": task_name,\n                \"layer\": layer.clean_name\n            }\n\n            subset = resolved_subset_template.format(\n                **prepare_template_data(fill_pairs))\n\n            subset = self._clean_subset_name(stub, naming_conventions,\n                                             subset, layer)\n\n            if subset in existing_subset_names:\n                self.log.info(\n                    \"Subset {} already created, skipping.\".format(subset))\n                continue\n\n            if self.create_flatten_image != \"flatten_only\":\n                instance = self._create_instance(context, layer,\n                                                 resolved_family,\n                                                 asset_name, subset, task_name)\n                created_instances.append(instance)\n\n            existing_subset_names.append(subset)\n            publishable_layers.append(layer)\n\n        if self.create_flatten_image != \"no\" and publishable_layers:\n            self.log.debug(\"create_flatten_image\")\n            if not self.flatten_subset_template:\n                self.log.warning(\"No template for flatten image\")\n                return\n\n            fill_pairs.pop(\"layer\")\n            subset = self.flatten_subset_template.format(\n                **prepare_template_data(fill_pairs))\n\n            first_layer = publishable_layers[0]  # dummy layer\n            first_layer.name = subset\n            family = family_from_settings  # inherit family\n            instance = self._create_instance(context, first_layer,\n                                             family,\n                                             asset_name, subset, task_name)\n            instance.data[\"ids\"] = [layer.id for layer in publishable_layers]\n            created_instances.append(instance)\n\n        for instance in created_instances:\n            # Produce diagnostic message for any graphical\n            # user interface interested in visualising it.\n            self.log.info(\"Found: \\\"%s\\\" \" % instance.data[\"name\"])\n            self.log.info(\"instance: {} \".format(instance.data))\n\n    def _get_existing_subset_names(self, context):\n        \"\"\"Collect manually created instances from workfile.\n\n        Shouldn't be any as Webpublisher bypass publishing via Openpype, but\n        might be some if workfile published through OP is reused.\n        \"\"\"\n        existing_subset_names = []\n        for instance in context:\n            if instance.data.get('publish'):\n                existing_subset_names.append(instance.data.get('subset'))\n\n        return existing_subset_names\n\n    def _create_instance(self, context, layer, family,\n                         asset, subset, task_name):\n        instance = context.create_instance(layer.name)\n        instance.data[\"family\"] = family\n        instance.data[\"publish\"] = True\n        instance.data[\"asset\"] = asset\n        instance.data[\"task\"] = task_name\n        instance.data[\"subset\"] = subset\n        instance.data[\"layer\"] = layer\n        instance.data[\"families\"] = []\n\n        return instance\n\n    def _resolve_mapping(self, layer):\n        \"\"\"Matches 'layer' color code and name to mapping.\n\n            If both color code AND name regex is configured, BOTH must be valid\n            If layer matches to multiple mappings, only first is used!\n        \"\"\"\n        family_list = []\n        family = None\n        subset_name_list = []\n        resolved_subset_template = None\n        for mapping in self.color_code_mapping:\n            if mapping[\"color_code\"] and \\\n                    layer.color_code not in mapping[\"color_code\"]:\n                continue\n\n            if mapping[\"layer_name_regex\"] and \\\n                    not any(re.search(pattern, layer.name)\n               for pattern in mapping[\"layer_name_regex\"]):\n                continue\n\n            family_list.append(mapping[\"family\"])\n            subset_name_list.append(mapping[\"subset_template_name\"])\n        if len(subset_name_list) > 1:\n            self.log.warning(\"Multiple mappings found for '{}'\".\n                             format(layer.name))\n            self.log.warning(\"Only first subset name template used!\")\n            subset_name_list[:] = subset_name_list[0]\n\n        if len(family_list) > 1:\n            self.log.warning(\"Multiple mappings found for '{}'\".\n                             format(layer.name))\n            self.log.warning(\"Only first family used!\")\n            family_list[:] = family_list[0]\n        if subset_name_list:\n            resolved_subset_template = subset_name_list.pop()\n        if family_list:\n            family = family_list.pop()\n\n        self.log.debug(\"resolved_family {}\".format(family))\n        self.log.debug(\"resolved_subset_template {}\".format(\n            resolved_subset_template))\n        return family, resolved_subset_template\n\n    def _clean_subset_name(self, stub, naming_conventions, subset, layer):\n        \"\"\"Cleans invalid characters from subset name and layer name.\"\"\"\n        if re.search(naming_conventions[\"invalid_chars\"], subset):\n            subset = re.sub(\n                naming_conventions[\"invalid_chars\"],\n                naming_conventions[\"replace_char\"],\n                subset\n            )\n            layer_name = re.sub(\n                naming_conventions[\"invalid_chars\"],\n                naming_conventions[\"replace_char\"],\n                layer.clean_name\n            )\n            layer.name = layer_name\n            stub.rename_layer(layer.id, layer_name)\n\n        return subset\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_current_file.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.hosts.photoshop import api as photoshop\n\n\nclass CollectCurrentFile(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current working file into context\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.49\n    label = \"Current File\"\n    hosts = [\"photoshop\"]\n\n    def process(self, context):\n        context.data[\"currentFile\"] = os.path.normpath(\n            photoshop.stub().get_active_document_full_name()\n        ).replace(\"\\\\\", \"/\")\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_extension_version.py",
    "content": "import os\nimport re\nimport pyblish.api\n\nfrom openpype.hosts.photoshop import api as photoshop\n\n\nclass CollectExtensionVersion(pyblish.api.ContextPlugin):\n    \"\"\" Pulls and compares version of installed extension.\n\n        It is recommended to use same extension as in provided Openpype code.\n\n        Please use Anastasiy’s Extension Manager or ZXPInstaller to update\n        extension in case of an error.\n\n        You can locate extension.zxp in your installed Openpype code in\n        `repos/avalon-core/avalon/photoshop`\n    \"\"\"\n    # This technically should be a validator, but other collectors might be\n    # impacted with usage of obsolete extension, so collector that runs first\n    # was chosen\n    order = pyblish.api.CollectorOrder - 0.5\n    label = \"Collect extension version\"\n    hosts = [\"photoshop\"]\n\n    optional = True\n    active = True\n\n    def process(self, context):\n        installed_version = photoshop.stub().get_extension_version()\n\n        if not installed_version:\n            raise ValueError(\"Unknown version, probably old extension\")\n\n        manifest_url = os.path.join(os.path.dirname(photoshop.__file__),\n                                    \"extension\", \"CSXS\", \"manifest.xml\")\n\n        if not os.path.exists(manifest_url):\n            self.log.debug(\"Unable to locate extension manifest, not checking\")\n            return\n\n        expected_version = None\n        with open(manifest_url) as fp:\n            content = fp.read()\n\n            found = re.findall(r'(ExtensionBundleVersion=\")([0-9\\.]+)(\")',\n                               content)\n            if found:\n                expected_version = found[0][1]\n\n        if expected_version != installed_version:\n            msg = \"Expected version '{}' found '{}'\\n\".format(\n                expected_version, installed_version)\n            msg += \"Please update your installed extension, it might not work \"\n            msg += \"properly.\"\n\n            raise ValueError(msg)\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_image.py",
    "content": "import pyblish.api\n\nfrom openpype.hosts.photoshop import api\n\n\nclass CollectImage(pyblish.api.InstancePlugin):\n    \"\"\"Collect layer metadata into a instance.\n\n    Used later in validation\n    \"\"\"\n    order = pyblish.api.CollectorOrder + 0.200\n    label = 'Collect Image'\n\n    hosts = [\"photoshop\"]\n    families = [\"image\"]\n\n    def process(self, instance):\n        if instance.data.get(\"members\"):\n            layer = api.stub().get_layer(instance.data[\"members\"][0])\n            instance.data[\"layer\"] = layer\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_published_version.py",
    "content": "\"\"\"Collects published version of workfile and increments it.\n\nFor synchronization of published image and workfile version it is required\nto store workfile version from workfile file name in context.data[\"version\"].\nIn remote publishing this name is unreliable (artist might not follow naming\nconvention etc.), last published workfile version for particular workfile\nsubset is used instead.\n\nThis plugin runs only in remote publishing (eg. Webpublisher).\n\nRequires:\n    context.data[\"assetEntity\"]\n\nProvides:\n    context[\"version\"] - incremented latest published workfile version\n\"\"\"\n\nimport pyblish.api\n\nfrom openpype.client import get_last_version_by_subset_name\nfrom openpype.pipeline.version_start import get_versioning_start\n\n\nclass CollectPublishedVersion(pyblish.api.ContextPlugin):\n    \"\"\"Collects published version of workfile and increments it.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.190\n    label = \"Collect published version\"\n    hosts = [\"photoshop\"]\n    targets = [\"automated\"]\n\n    def process(self, context):\n        workfile_subset_name = None\n        for instance in context:\n            if instance.data[\"family\"] == \"workfile\":\n                workfile_subset_name = instance.data[\"subset\"]\n                break\n\n        if not workfile_subset_name:\n            self.log.warning(\"No workfile instance found, \"\n                             \"synchronization of version will not work.\")\n            return\n\n        project_name = context.data[\"projectName\"]\n        asset_doc = context.data[\"assetEntity\"]\n        asset_id = asset_doc[\"_id\"]\n\n        version_doc = get_last_version_by_subset_name(project_name,\n                                                      workfile_subset_name,\n                                                      asset_id)\n\n        if version_doc:\n            version_int = int(version_doc[\"name\"]) + 1\n        else:\n            version_int = get_versioning_start(\n                project_name,\n                \"photoshop\",\n                task_name=context.data[\"task\"],\n                task_type=context.data[\"taskType\"],\n                project_settings=context.data[\"project_settings\"]\n            )\n\n        self.log.debug(f\"Setting {version_int} to context.\")\n        context.data[\"version\"] = version_int\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_review.py",
    "content": "\"\"\"\nRequires:\n    None\n\nProvides:\n    instance     -> family (\"review\")\n\"\"\"\n\nimport os\n\nimport pyblish.api\n\nfrom openpype.pipeline.create import get_subset_name\n\n\nclass CollectReview(pyblish.api.ContextPlugin):\n    \"\"\"Adds review to families for instances marked to be reviewable.\n    \"\"\"\n\n    label = \"Collect Review\"\n    label = \"Review\"\n    hosts = [\"photoshop\"]\n    order = pyblish.api.CollectorOrder + 0.1\n\n    publish = True\n\n    def process(self, context):\n        for instance in context:\n            creator_attributes = instance.data[\"creator_attributes\"]\n            if (creator_attributes.get(\"mark_for_review\") and\n                    \"review\" not in instance.data[\"families\"]):\n                instance.data[\"families\"].append(\"review\")\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_version.py",
    "content": "import pyblish.api\n\n\nclass CollectVersion(pyblish.api.InstancePlugin):\n    \"\"\"Collect version for publishable instances.\n\n    Used to synchronize version from workfile to all publishable instances:\n        - image (manually created or color coded)\n        - review\n        - workfile\n\n    Dev comment:\n    Explicit collector created to control this from single place and not from\n    3 different.\n\n    Workfile set here explicitly as version might to be forced from latest + 1\n    because of Webpublisher.\n    (This plugin must run after CollectPublishedVersion!)\n    \"\"\"\n    order = pyblish.api.CollectorOrder + 0.200\n    label = 'Collect Version'\n\n    hosts = [\"photoshop\"]\n    families = [\"image\", \"review\", \"workfile\"]\n\n    def process(self, instance):\n        workfile_version = instance.context.data[\"version\"]\n        self.log.debug(f\"Applying version {workfile_version}\")\n        instance.data[\"version\"] = workfile_version\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/collect_workfile.py",
    "content": "import os\nimport pyblish.api\n\nfrom openpype.pipeline.create import get_subset_name\n\n\nclass CollectWorkfile(pyblish.api.ContextPlugin):\n    \"\"\"Collect current script for publish.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.1\n    label = \"Collect Workfile\"\n    hosts = [\"photoshop\"]\n\n    default_variant = \"Main\"\n\n    def process(self, context):\n        for instance in context:\n            if instance.data[\"family\"] == \"workfile\":\n                file_path = context.data[\"currentFile\"]\n                _, ext = os.path.splitext(file_path)\n                staging_dir = os.path.dirname(file_path)\n                base_name = os.path.basename(file_path)\n\n                # creating representation\n                _, ext = os.path.splitext(file_path)\n                instance.data[\"representations\"].append({\n                    \"name\": ext[1:],\n                    \"ext\": ext[1:],\n                    \"files\": base_name,\n                    \"stagingDir\": staging_dir,\n                })\n                return\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/extract_image.py",
    "content": "import os\n\nimport pyblish.api\nfrom openpype.pipeline import publish\nfrom openpype.hosts.photoshop import api as photoshop\n\n\nclass ExtractImage(pyblish.api.ContextPlugin):\n    \"\"\"Extract all layers (groups) marked for publish.\n\n    Usually publishable instance is created as a wrapper of layer(s). For each\n    publishable instance so many images as there is 'formats' is created.\n\n    Logic tries to hide/unhide layers minimum times.\n\n    Called once for all publishable instances.\n    \"\"\"\n\n    order = publish.Extractor.order - 0.48\n    label = \"Extract Image\"\n    hosts = [\"photoshop\"]\n\n    families = [\"image\", \"background\"]\n    formats = [\"png\", \"jpg\"]\n\n    def process(self, context):\n        stub = photoshop.stub()\n        hidden_layer_ids = set()\n\n        all_layers = stub.get_layers()\n        for layer in all_layers:\n            if not layer.visible:\n                hidden_layer_ids.add(layer.id)\n        stub.hide_all_others_layers_ids([], layers=all_layers)\n\n        with photoshop.maintained_selection():\n            with photoshop.maintained_visibility(layers=all_layers):\n                for instance in context:\n                    if instance.data[\"family\"] not in self.families:\n                        continue\n\n                    staging_dir = self.staging_dir(instance)\n                    self.log.info(\"Outputting image to {}\".format(staging_dir))\n\n                    # Perform extraction\n                    files = {}\n                    ids = set()\n                    # real layers and groups\n                    members = instance.data(\"members\")\n                    if members:\n                        ids.update(set([int(member) for member in members]))\n                    # virtual groups collected by color coding or auto_image\n                    add_ids = instance.data.pop(\"ids\", None)\n                    if add_ids:\n                        ids.update(set(add_ids))\n                    extract_ids = set([ll.id for ll in stub.\n                                      get_layers_in_layers_ids(ids, all_layers)\n                                       if ll.id not in hidden_layer_ids])\n\n                    for extracted_id in extract_ids:\n                        stub.set_visible(extracted_id, True)\n\n                    file_basename = os.path.splitext(\n                        stub.get_active_document_name()\n                    )[0]\n                    for extension in self.formats:\n                        _filename = \"{}.{}\".format(file_basename,\n                                                   extension)\n                        files[extension] = _filename\n\n                        full_filename = os.path.join(staging_dir,\n                                                     _filename)\n                        stub.saveAs(full_filename, extension, True)\n                        self.log.info(f\"Extracted: {extension}\")\n\n                    representations = []\n                    for extension, filename in files.items():\n                        representations.append({\n                            \"name\": extension,\n                            \"ext\": extension,\n                            \"files\": filename,\n                            \"stagingDir\": staging_dir\n                        })\n                    instance.data[\"representations\"] = representations\n                    instance.data[\"stagingDir\"] = staging_dir\n\n                    self.log.info(f\"Extracted {instance} to {staging_dir}\")\n\n                    for extracted_id in extract_ids:\n                        stub.set_visible(extracted_id, False)\n\n    def staging_dir(self, instance):\n        \"\"\"Provide a temporary directory in which to store extracted files\n\n        Upon calling this method the staging directory is stored inside\n        the instance.data['stagingDir']\n        \"\"\"\n\n        from openpype.pipeline.publish import get_instance_staging_dir\n\n        return get_instance_staging_dir(instance)\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/extract_review.py",
    "content": "import os\nimport shutil\nfrom PIL import Image\n\nfrom openpype.lib import (\n    run_subprocess,\n    get_ffmpeg_tool_args,\n)\nfrom openpype.pipeline import publish\nfrom openpype.hosts.photoshop import api as photoshop\n\n\nclass ExtractReview(publish.Extractor):\n    \"\"\"\n        Produce a flattened or sequence image files from all 'image' instances.\n\n        If no 'image' instance is created, it produces flattened image from\n        all visible layers.\n\n        It creates review, thumbnail and mov representations.\n\n        'review' family could be used in other steps as a reference, as it\n        contains flattened image by default. (Eg. artist could load this\n        review as a single item and see full image. In most cases 'image'\n        family is separated by layers to better usage in animation or comp.)\n    \"\"\"\n\n    label = \"Extract Review\"\n    hosts = [\"photoshop\"]\n    families = [\"review\"]\n\n    # Extract Options\n    jpg_options = None\n    mov_options = None\n    make_image_sequence = None\n    max_downscale_size = 8192\n\n    def process(self, instance):\n        staging_dir = self.staging_dir(instance)\n        self.log.info(\"Outputting image to {}\".format(staging_dir))\n\n        fps = instance.data.get(\"fps\", 25)\n        stub = photoshop.stub()\n        self.output_seq_filename = os.path.splitext(\n            stub.get_active_document_name())[0] + \".%04d.jpg\"\n\n        layers = self._get_layers_from_image_instances(instance)\n        self.log.info(\"Layers image instance found: {}\".format(layers))\n\n        repre_name = \"jpg\"\n        repre_skeleton = {\n            \"name\": repre_name,\n            \"ext\": \"jpg\",\n            \"stagingDir\": staging_dir,\n            \"tags\": self.jpg_options['tags'],\n        }\n\n        if instance.data[\"family\"] != \"review\":\n            self.log.debug(\"Existing extracted file from image family used.\")\n            # enable creation of review, without this jpg review would clash\n            # with jpg of the image family\n            output_name = repre_name\n            repre_name = \"{}_{}\".format(repre_name, output_name)\n            repre_skeleton.update({\"name\": repre_name,\n                                   \"outputName\": output_name})\n\n            img_file = self.output_seq_filename % 0\n            self._prepare_file_for_image_family(img_file, instance,\n                                                staging_dir)\n            repre_skeleton.update({\n                \"files\": img_file,\n            })\n            processed_img_names = [img_file]\n        elif self.make_image_sequence and len(layers) > 1:\n            self.log.debug(\"Extract layers to image sequence.\")\n            img_list = self._save_sequence_images(staging_dir, layers)\n\n            repre_skeleton.update({\n                \"frameStart\": 0,\n                \"frameEnd\": len(img_list),\n                \"fps\": fps,\n                \"files\": img_list,\n            })\n            processed_img_names = img_list\n        else:\n            self.log.debug(\"Extract layers to flatten image.\")\n            img_file = self._save_flatten_image(staging_dir, layers)\n\n            repre_skeleton.update({\n                \"files\": img_file,\n            })\n            processed_img_names = [img_file]\n\n        instance.data[\"representations\"].append(repre_skeleton)\n\n        ffmpeg_args = get_ffmpeg_tool_args(\"ffmpeg\")\n\n        instance.data[\"stagingDir\"] = staging_dir\n\n        source_files_pattern = os.path.join(staging_dir,\n                                            self.output_seq_filename)\n        source_files_pattern = self._check_and_resize(processed_img_names,\n                                                      source_files_pattern,\n                                                      staging_dir)\n        self._generate_thumbnail(\n            list(ffmpeg_args),\n            instance,\n            source_files_pattern,\n            staging_dir)\n\n        no_of_frames = len(processed_img_names)\n        if no_of_frames > 1:\n            self._generate_mov(\n                list(ffmpeg_args),\n                instance,\n                fps,\n                no_of_frames,\n                source_files_pattern,\n                staging_dir)\n\n        self.log.info(f\"Extracted {instance} to {staging_dir}\")\n\n    def _prepare_file_for_image_family(self, img_file, instance, staging_dir):\n        \"\"\"Converts existing file for image family to .jpg\n\n        Image instance could have its own separate review (instance per layer\n        for example). This uses extracted file instead of extracting again.\n        Args:\n            img_file (str): name of output file (with 0000 value for ffmpeg\n                later)\n            instance:\n            staging_dir (str): temporary folder where extracted file is located\n        \"\"\"\n        repre_file = instance.data[\"representations\"][0]\n        source_file_path = os.path.join(repre_file[\"stagingDir\"],\n                                        repre_file[\"files\"])\n        if not os.path.exists(source_file_path):\n            raise RuntimeError(f\"{source_file_path} doesn't exist for \"\n                               \"review to create from\")\n        _, ext = os.path.splitext(repre_file[\"files\"])\n        if ext != \".jpg\":\n            im = Image.open(source_file_path)\n            if (im.mode in ('RGBA', 'LA') or (\n                    im.mode == 'P' and 'transparency' in im.info)):\n                # without this it produces messy low quality jpg\n                rgb_im = Image.new(\"RGBA\", (im.width, im.height), \"#ffffff\")\n                rgb_im.alpha_composite(im)\n                rgb_im.convert(\"RGB\").save(os.path.join(staging_dir, img_file))\n            else:\n                im.save(os.path.join(staging_dir, img_file))\n        else:\n            # handles already .jpg\n            shutil.copy(source_file_path,\n                        os.path.join(staging_dir, img_file))\n\n    def _generate_mov(self, ffmpeg_path, instance, fps, no_of_frames,\n                      source_files_pattern, staging_dir):\n        \"\"\"Generates .mov to upload to Ftrack.\n\n        Args:\n            ffmpeg_path (str): path to ffmpeg\n            instance (Pyblish Instance)\n            fps (str)\n            no_of_frames (int):\n            source_files_pattern (str): name of source file\n            staging_dir (str): temporary location to store thumbnail\n        Updates:\n            instance - adds representation portion\n        \"\"\"\n        # Generate mov.\n        mov_path = os.path.join(staging_dir, \"review.mov\")\n        self.log.info(f\"Generate mov review: {mov_path}\")\n        args = ffmpeg_path + [\n            \"-y\",\n            \"-i\", source_files_pattern,\n            \"-vf\", \"pad=ceil(iw/2)*2:ceil(ih/2)*2\",\n            \"-vframes\", str(no_of_frames),\n            mov_path\n        ]\n        self.log.debug(\"mov args:: {}\".format(args))\n        _output = run_subprocess(args)\n        instance.data[\"representations\"].append({\n            \"name\": \"mov\",\n            \"ext\": \"mov\",\n            \"files\": os.path.basename(mov_path),\n            \"stagingDir\": staging_dir,\n            \"frameStart\": 1,\n            \"frameEnd\": no_of_frames,\n            \"fps\": fps,\n            \"tags\": self.mov_options['tags']\n        })\n\n    def _generate_thumbnail(\n        self, ffmpeg_args, instance, source_files_pattern, staging_dir\n    ):\n        \"\"\"Generates scaled down thumbnail and adds it as representation.\n\n        Args:\n            ffmpeg_path (str): path to ffmpeg\n            instance (Pyblish Instance)\n            source_files_pattern (str): name of source file\n            staging_dir (str): temporary location to store thumbnail\n        Updates:\n            instance - adds representation portion\n        \"\"\"\n        # Generate thumbnail\n        thumbnail_path = os.path.join(staging_dir, \"thumbnail.jpg\")\n        self.log.info(f\"Generate thumbnail {thumbnail_path}\")\n        args = ffmpeg_args + [\n            \"-y\",\n            \"-i\", source_files_pattern,\n            \"-vf\", \"scale=300:-1\",\n            \"-vframes\", \"1\",\n            thumbnail_path\n        ]\n        self.log.debug(\"thumbnail args:: {}\".format(args))\n        _output = run_subprocess(args)\n        instance.data[\"representations\"].append({\n            \"name\": \"thumbnail\",\n            \"ext\": \"jpg\",\n            \"outputName\": \"thumb\",\n            \"files\": os.path.basename(thumbnail_path),\n            \"stagingDir\": staging_dir,\n            \"tags\": [\"thumbnail\", \"delete\"]\n        })\n        instance.data[\"thumbnailPath\"] = thumbnail_path\n\n    def _check_and_resize(self, processed_img_names, source_files_pattern,\n                          staging_dir):\n        \"\"\"Check if saved image could be used in ffmpeg.\n\n        Ffmpeg has max size 16384x16384. Saved image(s) must be resized to be\n        used as a source for thumbnail or review mov.\n        \"\"\"\n        Image.MAX_IMAGE_PIXELS = None\n        first_url = os.path.join(staging_dir, processed_img_names[0])\n        with Image.open(first_url) as im:\n            width, height = im.size\n\n        if width > self.max_downscale_size or height > self.max_downscale_size:\n            resized_dir = os.path.join(staging_dir, \"resized\")\n            os.mkdir(resized_dir)\n            source_files_pattern = os.path.join(resized_dir,\n                                                self.output_seq_filename)\n            for file_name in processed_img_names:\n                source_url = os.path.join(staging_dir, file_name)\n                with Image.open(source_url) as res_img:\n                    # 'thumbnail' automatically keeps aspect ratio\n                    res_img.thumbnail((self.max_downscale_size,\n                                       self.max_downscale_size),\n                                      Image.ANTIALIAS)\n                    res_img.save(os.path.join(resized_dir, file_name))\n\n        return source_files_pattern\n\n    def _get_layers_from_image_instances(self, instance):\n        \"\"\"Collect all layers from 'instance'.\n\n        Returns:\n            (list) of PSItem\n        \"\"\"\n        layers = []\n        # creating review for existing 'image' instance\n        if instance.data[\"family\"] == \"image\" and instance.data.get(\"layer\"):\n            layers.append(instance.data[\"layer\"])\n            return layers\n\n        for image_instance in instance.context:\n            if image_instance.data[\"family\"] != \"image\":\n                continue\n            if not image_instance.data.get(\"layer\"):\n                # dummy instance for flatten image\n                continue\n            layers.append(image_instance.data.get(\"layer\"))\n\n        return sorted(layers)\n\n    def _save_flatten_image(self, staging_dir, layers):\n        \"\"\"Creates flat image from 'layers' into 'staging_dir'.\n\n        Returns:\n            (str): path to new image\n        \"\"\"\n        img_filename = self.output_seq_filename % 0\n        output_image_path = os.path.join(staging_dir, img_filename)\n        stub = photoshop.stub()\n\n        with photoshop.maintained_visibility():\n            self.log.info(\"Extracting {}\".format(layers))\n            if layers:\n                stub.hide_all_others_layers(layers)\n\n            stub.saveAs(output_image_path, 'jpg', True)\n\n        return img_filename\n\n    def _save_sequence_images(self, staging_dir, layers):\n        \"\"\"Creates separate flat images from 'layers' into 'staging_dir'.\n\n        Used as source for multi frames .mov to review at once.\n        Returns:\n            (list): paths to new images\n        \"\"\"\n        stub = photoshop.stub()\n\n        list_img_filename = []\n        with photoshop.maintained_visibility():\n            for i, layer in enumerate(layers):\n                self.log.info(\"Extracting {}\".format(layer))\n\n                img_filename = self.output_seq_filename % i\n                output_image_path = os.path.join(staging_dir, img_filename)\n                list_img_filename.append(img_filename)\n\n                with photoshop.maintained_visibility():\n                    stub.hide_all_others_layers([layer])\n                    stub.saveAs(output_image_path, 'jpg', True)\n\n        return list_img_filename\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/extract_save_scene.py",
    "content": "from openpype.pipeline import publish\nfrom openpype.hosts.photoshop import api as photoshop\n\n\nclass ExtractSaveScene(publish.Extractor):\n    \"\"\"Save scene before extraction.\"\"\"\n\n    order = publish.Extractor.order - 0.49\n    label = \"Extract Save Scene\"\n    hosts = [\"photoshop\"]\n    families = [\"workfile\"]\n\n    def process(self, instance):\n        photoshop.stub().save()\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Asset does not match</title>\n<description>\n## Collected asset name is not same as in context\n\n    {msg}\n### How to repair?\n    {repair_msg}\n    Refresh Publish afterwards (circle arrow at the bottom right).\n\n    If that's not correct value, close workfile and reopen via Workfiles to get\n    proper context asset name OR disable this validator and publish again\n    if you are publishing to different context deliberately.\n\n    (Context means combination of project, asset name and task name.)\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Subset name</title>\n<description>\n## Invalid subset or layer name\n\nSubset or layer name cannot contain specific characters (spaces etc) which could cause issue when subset name is used in a published file name.\n    {msg}\n\n### How to repair?\n\nYou can fix this with \"repair\" button on the right and press Refresh publishing button at the bottom right.\n</description>\n<detail>\n### __Detailed Info__ (optional)\n\nNot all characters are available in a file names on all OS. Wrong characters could be configured in Settings.\n</detail>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/increment_workfile.py",
    "content": "import os\nimport pyblish.api\nfrom openpype.pipeline.publish import get_errored_plugins_from_context\nfrom openpype.lib import version_up\n\nfrom openpype.hosts.photoshop import api as photoshop\n\n\nclass IncrementWorkfile(pyblish.api.InstancePlugin):\n    \"\"\"Increment the current workfile.\n\n    Saves the current scene with an increased version number.\n    \"\"\"\n\n    label = \"Increment Workfile\"\n    order = pyblish.api.IntegratorOrder + 9.0\n    hosts = [\"photoshop\"]\n    families = [\"workfile\"]\n    optional = True\n\n    def process(self, instance):\n        errored_plugins = get_errored_plugins_from_context(instance.context)\n        if errored_plugins:\n            raise RuntimeError(\n                \"Skipping incrementing current file because publishing failed.\"\n            )\n\n        scene_path = version_up(instance.context.data[\"currentFile\"])\n        _, ext = os.path.splitext(scene_path)\n        photoshop.stub().saveAs(scene_path, ext[1:], True)\n\n        self.log.info(\"Incremented workfile to: {}\".format(scene_path))\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import get_current_asset_name\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.hosts.photoshop import api as photoshop\n\n\nclass ValidateInstanceAssetRepair(pyblish.api.Action):\n    \"\"\"Repair the instance asset.\"\"\"\n\n    label = \"Repair\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n\n        # Get the errored instances\n        failed = []\n        for result in context.data[\"results\"]:\n            if (result[\"error\"] is not None and result[\"instance\"] is not None\n                    and result[\"instance\"] not in failed):\n                failed.append(result[\"instance\"])\n\n        # Apply pyblish.logic to get the instances for the plug-in\n        instances = pyblish.api.instances_by_plugin(failed, plugin)\n        stub = photoshop.stub()\n        current_asset_name = get_current_asset_name()\n        for instance in instances:\n            data = stub.read(instance[0])\n            data[\"asset\"] = current_asset_name\n            stub.imprint(instance[0], data)\n\n\nclass ValidateInstanceAsset(OptionalPyblishPluginMixin,\n                            pyblish.api.InstancePlugin):\n    \"\"\"Validate the instance asset is the current selected context asset.\n\n    As it might happen that multiple worfiles are opened, switching\n    between them would mess with selected context.\n    In that case outputs might be output under wrong asset!\n\n    Repair action will use Context asset value (from Workfiles or Launcher)\n    Closing and reopening with Workfiles will refresh  Context value.\n    \"\"\"\n\n    label = \"Validate Instance Asset\"\n    hosts = [\"photoshop\"]\n    optional = True\n    actions = [ValidateInstanceAssetRepair]\n    order = ValidateContentsOrder\n\n    def process(self, instance):\n        instance_asset = instance.data[\"asset\"]\n        current_asset = get_current_asset_name()\n\n        if instance_asset != current_asset:\n            msg = (\n                f\"Instance asset {instance_asset} is not the same \"\n                f\"as current context {current_asset}.\"\n\n            )\n            repair_msg = (\n                f\"Repair with 'Repair' button to use '{current_asset}'.\\n\"\n            )\n            formatting_data = {\"msg\": msg,\n                               \"repair_msg\": repair_msg}\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n"
  },
  {
    "path": "openpype/hosts/photoshop/plugins/publish/validate_naming.py",
    "content": "import re\n\nimport pyblish.api\n\nfrom openpype.hosts.photoshop import api as photoshop\nfrom openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n)\n\n\nclass ValidateNamingRepair(pyblish.api.Action):\n    \"\"\"Repair the instance asset.\"\"\"\n\n    label = \"Repair\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n\n        # Get the errored instances\n        failed = []\n        for result in context.data[\"results\"]:\n            if (result[\"error\"] is not None and result[\"instance\"] is not None\n                    and result[\"instance\"] not in failed):\n                failed.append(result[\"instance\"])\n\n        invalid_chars, replace_char = plugin.get_replace_chars()\n        self.log.debug(\"{} --- {}\".format(invalid_chars, replace_char))\n\n        # Apply pyblish.logic to get the instances for the plug-in\n        instances = pyblish.api.instances_by_plugin(failed, plugin)\n        stub = photoshop.stub()\n        for instance in instances:\n            self.log.debug(\"validate_naming instance {}\".format(instance))\n            current_layer_state = stub.get_layer(instance.data[\"layer\"].id)\n            self.log.debug(\"current_layer{}\".format(current_layer_state))\n\n            layer_meta = stub.read(current_layer_state)\n            instance_id = (layer_meta.get(\"instance_id\") or\n                           layer_meta.get(\"uuid\"))\n            if not instance_id:\n                self.log.warning(\"Unable to repair, cannot find layer\")\n                continue\n\n            layer_name = re.sub(invalid_chars,\n                                replace_char,\n                                current_layer_state.clean_name)\n            layer_name = stub.PUBLISH_ICON + layer_name\n\n            stub.rename_layer(current_layer_state.id, layer_name)\n\n            subset_name = re.sub(invalid_chars, replace_char,\n                                 instance.data[\"subset\"])\n\n            # format from Tool Creator\n            subset_name = re.sub(\n                \"[^{}]+\".format(SUBSET_NAME_ALLOWED_SYMBOLS),\n                \"\",\n                subset_name\n            )\n\n            layer_meta[\"subset\"] = subset_name\n            stub.imprint(instance_id, layer_meta)\n\n        return True\n\n\nclass ValidateNaming(pyblish.api.InstancePlugin):\n    \"\"\"Validate the instance name.\n\n    Spaces in names are not allowed. Will be replace with underscores.\n    \"\"\"\n\n    label = \"Validate Naming\"\n    hosts = [\"photoshop\"]\n    order = ValidateContentsOrder\n    families = [\"image\"]\n    actions = [ValidateNamingRepair]\n\n    # configured by Settings\n    invalid_chars = ''\n    replace_char = ''\n\n    def process(self, instance):\n        help_msg = ' Use Repair button to fix it and then refresh publish.'\n\n        layer = instance.data.get(\"layer\")\n        if layer:\n            msg = \"Name \\\"{}\\\" is not allowed.{}\".format(layer.clean_name,\n                                                         help_msg)\n\n            formatting_data = {\"msg\": msg}\n            if re.search(self.invalid_chars, layer.clean_name):\n                raise PublishXmlValidationError(self, msg,\n                                                formatting_data=formatting_data\n                                                )\n\n        msg = \"Subset \\\"{}\\\" is not allowed.{}\".format(instance.data[\"subset\"],\n                                                       help_msg)\n        formatting_data = {\"msg\": msg}\n        if re.search(self.invalid_chars, instance.data[\"subset\"]):\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n\n    @classmethod\n    def get_replace_chars(cls):\n        \"\"\"Pass values configured in Settings for Repair.\"\"\"\n        return cls.invalid_chars, cls.replace_char\n"
  },
  {
    "path": "openpype/hosts/resolve/README.markdown",
    "content": "## Basic setup\n\n-   Actually supported version is up to v18\n-   install Python 3.6.2 (latest tested v17) or up to 3.9.13 (latest tested on v18)\n-   pip install PySide2:\n    -   Python 3.9.*: open terminal and go to python.exe directory, then `python -m pip install PySide2`\n-   pip install OpenTimelineIO:\n    -   Python 3.9.*: open terminal and go to python.exe directory, then  `python -m pip install OpenTimelineIO`\n    -   Python 3.6: open terminal and go to python.exe directory, then `python -m pip install git+https://github.com/PixarAnimationStudios/OpenTimelineIO.git@5aa24fbe89d615448876948fe4b4900455c9a3e8` and move built files from `./Lib/site-packages/opentimelineio/cxx-libs/bin and lib` to `./Lib/site-packages/opentimelineio/`. I was building it on Win10 machine with Visual Studio Community 2019 and\n    ![image](https://user-images.githubusercontent.com/40640033/102792588-ffcb1c80-43a8-11eb-9c6b-bf2114ed578e.png) with installed CMake in PATH.\n-   make sure Resolve Fusion (Fusion Tab/menu/Fusion/Fusion Settings) is set to Python 3.6\n    ![image](https://user-images.githubusercontent.com/40640033/102631545-280b0f00-414e-11eb-89fc-98ac268d209d.png)\n-   Open OpenPype **Tray/Admin/Studio settings** > `applications/resolve/environment` and add Python3 path to `RESOLVE_PYTHON3_HOME` platform related.\n\n## Editorial setup\n\nThis is how it looks on my testing project timeline\n![image](https://user-images.githubusercontent.com/40640033/102637638-96ec6600-4156-11eb-9656-6e8e3ce4baf8.png)\nNotice I had renamed tracks to `main` (holding metadata markers) and `review` used for generating review data with ffmpeg confersion to jpg sequence.\n\n1.  you need to start OpenPype menu from Resolve/EditTab/Menu/Workspace/Scripts/Comp/**__OpenPype_Menu__**\n2.  then select any clips in `main` track and change their color to `Chocolate`\n3.  in OpenPype Menu select `Create`\n4.  in Creator select `Create Publishable Clip [New]` (temporary name)\n5.  set `Rename clips` to True, Master Track to `main` and Use review track to `review` as in picture\n    ![image](https://user-images.githubusercontent.com/40640033/102643773-0d419600-4160-11eb-919e-9c2be0aecab8.png)\n6.  after you hit `ok` all clips are colored to `ping` and marked with openpype metadata tag\n7.  git `Publish` on openpype menu and see that all had been collected correctly. That is the last step for now as rest is Work in progress. Next steps will follow.\n"
  },
  {
    "path": "openpype/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt",
    "content": "Updated as of 26 May 2023\n----------------------------\nIn this package, you will find a brief introduction to the Scripting API for DaVinci Resolve Studio. Apart from this README.txt file, this package contains folders containing the basic import\nmodules for scripting access (DaVinciResolve.py) and some representative examples.\n\nFrom v16.2.0 onwards, the nodeIndex parameters accepted by SetLUT() and SetCDL() are 1-based instead of 0-based, i.e. 1 <= nodeIndex <= total number of nodes.\n\n\nOverview\n--------\nAs with Blackmagic Design Fusion scripts, user scripts written in Lua and Python programming languages are supported. By default, scripts can be invoked from the Console window in the Fusion page,\nor via command line. This permission can be changed in Resolve Preferences, to be only from Console, or to be invoked from the local network. Please be aware of the security implications when\nallowing scripting access from outside of the Resolve application.\n\n\nPrerequisites\n-------------\nDaVinci Resolve scripting requires one of the following to be installed (for all users):\n\n    Lua 5.1\n    Python 2.7 64-bit\n    Python >= 3.6 64-bit\n\n\nUsing a script\n--------------\nDaVinci Resolve needs to be running for a script to be invoked.\n\nFor a Resolve script to be executed from an external folder, the script needs to know of the API location.\nYou may need to set the these environment variables to allow for your Python installation to pick up the appropriate dependencies as shown below:\n\n    Mac OS X:\n    RESOLVE_SCRIPT_API=\"/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting\"\n    RESOLVE_SCRIPT_LIB=\"/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so\"\n    PYTHONPATH=\"$PYTHONPATH:$RESOLVE_SCRIPT_API/Modules/\"\n\n    Windows:\n    RESOLVE_SCRIPT_API=\"%PROGRAMDATA%\\Blackmagic Design\\DaVinci Resolve\\Support\\Developer\\Scripting\"\n    RESOLVE_SCRIPT_LIB=\"C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\fusionscript.dll\"\n    PYTHONPATH=\"%PYTHONPATH%;%RESOLVE_SCRIPT_API%\\Modules\\\"\n\n    Linux:\n    RESOLVE_SCRIPT_API=\"/opt/resolve/Developer/Scripting\"\n    RESOLVE_SCRIPT_LIB=\"/opt/resolve/libs/Fusion/fusionscript.so\"\n    PYTHONPATH=\"$PYTHONPATH:$RESOLVE_SCRIPT_API/Modules/\"\n    (Note: For standard ISO Linux installations, the path above may need to be modified to refer to /home/resolve instead of /opt/resolve)\n\nAs with Fusion scripts, Resolve scripts can also be invoked via the menu and the Console.\n\nOn startup, DaVinci Resolve scans the subfolders in the directories shown below and enumerates the scripts found in the Workspace application menu under Scripts.\nPlace your script under Utility to be listed in all pages, under Comp or Tool to be available in the Fusion page or under folders for individual pages (Edit, Color or Deliver). Scripts under Deliver are additionally listed under render jobs.\nPlacing your script here and invoking it from the menu is the easiest way to use scripts.\n    Mac OS X:\n      - All users: /Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts\n      - Specific user:  /Users/<UserName>/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts\n    Windows:\n      - All users: %PROGRAMDATA%\\Blackmagic Design\\DaVinci Resolve\\Fusion\\Scripts\n      - Specific user: %APPDATA%\\Roaming\\Blackmagic Design\\DaVinci Resolve\\Support\\Fusion\\Scripts\n    Linux:\n      - All users: /opt/resolve/Fusion/Scripts  (or /home/resolve/Fusion/Scripts/ depending on installation)\n      - Specific user: $HOME/.local/share/DaVinciResolve/Fusion/Scripts\n\nThe interactive Console window allows for an easy way to execute simple scripting commands, to query or modify properties, and to test scripts. The console accepts commands in Python 2.7, Python 3.6\nand Lua and evaluates and executes them immediately. For more information on how to use the Console, please refer to the DaVinci Resolve User Manual.\n\nThis example Python script creates a simple project:\n    #!/usr/bin/env python\n    import DaVinciResolveScript as dvr_script\n    resolve = dvr_script.scriptapp(\"Resolve\")\n    fusion = resolve.Fusion()\n    projectManager = resolve.GetProjectManager()\n    projectManager.CreateProject(\"Hello World\")\n\nThe resolve object is the fundamental starting point for scripting via Resolve. As a native object, it can be inspected for further scriptable properties - using table iteration and \"getmetatable\"\nin Lua and dir, help etc in Python (among other methods). A notable scriptable object above is fusion - it allows access to all existing Fusion scripting functionality.\n\n\nRunning DaVinci Resolve in headless mode\n----------------------------------------\nDaVinci Resolve can be launched in a headless mode without the user interface using the -nogui command line option. When DaVinci Resolve is launched using this option, the user interface is disabled.\nHowever, the various scripting APIs will continue to work as expected.\n\n\nBasic Resolve API\n-----------------\nSome commonly used API functions are described below (*). As with the resolve object, each object is inspectable for properties and functions.\n\nResolve\n  Fusion()                                        --> Fusion             # Returns the Fusion object. Starting point for Fusion scripts.\n  GetMediaStorage()                               --> MediaStorage       # Returns the media storage object to query and act on media locations.\n  GetProjectManager()                             --> ProjectManager     # Returns the project manager object for currently open database.\n  OpenPage(pageName)                              --> Bool               # Switches to indicated page in DaVinci Resolve. Input can be one of (\"media\", \"cut\", \"edit\", \"fusion\", \"color\", \"fairlight\", \"deliver\").\n  GetCurrentPage()                                --> String             # Returns the page currently displayed in the main window. Returned value can be one of (\"media\", \"cut\", \"edit\", \"fusion\", \"color\", \"fairlight\", \"deliver\", None).\n  GetProductName()                                --> string             # Returns product name.\n  GetVersion()                                    --> [version fields]   # Returns list of product version fields in [major, minor, patch, build, suffix] format.\n  GetVersionString()                              --> string             # Returns product version in \"major.minor.patch[suffix].build\" format.\n  LoadLayoutPreset(presetName)                    --> Bool               # Loads UI layout from saved preset named 'presetName'.\n  UpdateLayoutPreset(presetName)                  --> Bool               # Overwrites preset named 'presetName' with current UI layout.\n  ExportLayoutPreset(presetName, presetFilePath)  --> Bool               # Exports preset named 'presetName' to path 'presetFilePath'.\n  DeleteLayoutPreset(presetName)                  --> Bool               # Deletes preset named 'presetName'.\n  SaveLayoutPreset(presetName)                    --> Bool               # Saves current UI layout as a preset named 'presetName'.\n  ImportLayoutPreset(presetFilePath, presetName)  --> Bool               # Imports preset from path 'presetFilePath'. The optional argument 'presetName' specifies how the preset shall be named. If not specified, the preset is named based on the filename.\n  Quit()                                          --> None               # Quits the Resolve App.\n\nProjectManager\n  ArchiveProject(projectName,\n                 filePath,\n                 isArchiveSrcMedia=True,\n                 isArchiveRenderCache=True,\n                 isArchiveProxyMedia=False)       --> Bool               # Archives project to provided file path with the configuration as provided by the optional arguments\n  CreateProject(projectName)                      --> Project            # Creates and returns a project if projectName (string) is unique, and None if it is not.\n  DeleteProject(projectName)                      --> Bool               # Delete project in the current folder if not currently loaded\n  LoadProject(projectName)                        --> Project            # Loads and returns the project with name = projectName (string) if there is a match found, and None if there is no matching Project.\n  GetCurrentProject()                             --> Project            # Returns the currently loaded Resolve project.\n  SaveProject()                                   --> Bool               # Saves the currently loaded project with its own name. Returns True if successful.\n  CloseProject(project)                           --> Bool               # Closes the specified project without saving.\n  CreateFolder(folderName)                        --> Bool               # Creates a folder if folderName (string) is unique.\n  DeleteFolder(folderName)                        --> Bool               # Deletes the specified folder if it exists. Returns True in case of success.\n  GetProjectListInCurrentFolder()                 --> [project names...] # Returns a list of project names in current folder.\n  GetFolderListInCurrentFolder()                  --> [folder names...]  # Returns a list of folder names in current folder.\n  GotoRootFolder()                                --> Bool               # Opens root folder in database.\n  GotoParentFolder()                              --> Bool               # Opens parent folder of current folder in database if current folder has parent.\n  GetCurrentFolder()                              --> string             # Returns the current folder name.\n  OpenFolder(folderName)                          --> Bool               # Opens folder under given name.\n  ImportProject(filePath, projectName=None)       --> Bool               # Imports a project from the file path provided with given project name, if any. Returns True if successful.\n  ExportProject(projectName, filePath, withStillsAndLUTs=True) --> Bool  # Exports project to provided file path, including stills and LUTs if withStillsAndLUTs is True (enabled by default). Returns True in case of success.\n  RestoreProject(filePath, projectName=None)      --> Bool               # Restores a project from the file path provided with given project name, if any. Returns True if successful.\n  GetCurrentDatabase()                            --> {dbInfo}           # Returns a dictionary (with keys 'DbType', 'DbName' and optional 'IpAddress') corresponding to the current database connection\n  GetDatabaseList()                               --> [{dbInfo}]         # Returns a list of dictionary items (with keys 'DbType', 'DbName' and optional 'IpAddress') corresponding to all the databases added to Resolve\n  SetCurrentDatabase({dbInfo})                    --> Bool               # Switches current database connection to the database specified by the keys below, and closes any open project.\n                                                                         # 'DbType': 'Disk' or 'PostgreSQL' (string)\n                                                                         # 'DbName': database name (string)\n                                                                         # 'IpAddress': IP address of the PostgreSQL server (string, optional key - defaults to '127.0.0.1')\n\nProject\n  GetMediaPool()                                  --> MediaPool          # Returns the Media Pool object.\n  GetTimelineCount()                              --> int                # Returns the number of timelines currently present in the project.\n  GetTimelineByIndex(idx)                         --> Timeline           # Returns timeline at the given index, 1 <= idx <= project.GetTimelineCount()\n  GetCurrentTimeline()                            --> Timeline           # Returns the currently loaded timeline.\n  SetCurrentTimeline(timeline)                    --> Bool               # Sets given timeline as current timeline for the project. Returns True if successful.\n  GetGallery()                                    --> Gallery            # Returns the Gallery object.\n  GetName()                                       --> string             # Returns project name.\n  SetName(projectName)                            --> Bool               # Sets project name if given projectName (string) is unique.\n  GetPresetList()                                 --> [presets...]       # Returns a list of presets and their information.\n  SetPreset(presetName)                           --> Bool               # Sets preset by given presetName (string) into project.\n  AddRenderJob()                                  --> string             # Adds a render job based on current render settings to the render queue. Returns a unique job id (string) for the new render job.\n  DeleteRenderJob(jobId)                          --> Bool               # Deletes render job for input job id (string).\n  DeleteAllRenderJobs()                           --> Bool               # Deletes all render jobs in the queue.\n  GetRenderJobList()                              --> [render jobs...]   # Returns a list of render jobs and their information.\n  GetRenderPresetList()                           --> [presets...]       # Returns a list of render presets and their information.\n  StartRendering(jobId1, jobId2, ...)             --> Bool               # Starts rendering jobs indicated by the input job ids.\n  StartRendering([jobIds...], isInteractiveMode=False)    --> Bool       # Starts rendering jobs indicated by the input job ids.\n                                                                         # The optional \"isInteractiveMode\", when set, enables error feedback in the UI during rendering.\n  StartRendering(isInteractiveMode=False)                 --> Bool       # Starts rendering all queued render jobs.\n                                                                         # The optional \"isInteractiveMode\", when set, enables error feedback in the UI during rendering.\n  StopRendering()                                 --> None               # Stops any current render processes.\n  IsRenderingInProgress()                         --> Bool               # Returns True if rendering is in progress.\n  LoadRenderPreset(presetName)                    --> Bool               # Sets a preset as current preset for rendering if presetName (string) exists.\n  SaveAsNewRenderPreset(presetName)               --> Bool               # Creates new render preset by given name if presetName(string) is unique.\n  SetRenderSettings({settings})                   --> Bool               # Sets given settings for rendering. Settings is a dict, with support for the keys:\n                                                                         # Refer to \"Looking up render settings\" section for information for supported settings\n  GetRenderJobStatus(jobId)                       --> {status info}      # Returns a dict with job status and completion percentage of the job by given jobId (string).\n  GetSetting(settingName)                         --> string             # Returns value of project setting (indicated by settingName, string). Check the section below for more information.\n  SetSetting(settingName, settingValue)           --> Bool               # Sets the project setting (indicated by settingName, string) to the value (settingValue, string). Check the section below for more information.\n  GetRenderFormats()                              --> {render formats..} # Returns a dict (format -> file extension) of available render formats.\n  GetRenderCodecs(renderFormat)                   --> {render codecs...} # Returns a dict (codec description -> codec name) of available codecs for given render format (string).\n  GetCurrentRenderFormatAndCodec()                --> {format, codec}    # Returns a dict with currently selected format 'format' and render codec 'codec'.\n  SetCurrentRenderFormatAndCodec(format, codec)   --> Bool               # Sets given render format (string) and render codec (string) as options for rendering.\n  GetCurrentRenderMode()                          --> int                # Returns the render mode: 0 - Individual clips, 1 - Single clip.\n  SetCurrentRenderMode(renderMode)                --> Bool               # Sets the render mode. Specify renderMode = 0 for Individual clips, 1 for Single clip.\n  GetRenderResolutions(format, codec)             --> [{Resolution}]     # Returns list of resolutions applicable for the given render format (string) and render codec (string). Returns full list of resolutions if no argument is provided. Each element in the list is a dictionary with 2 keys \"Width\" and \"Height\".\n  RefreshLUTList()                                --> Bool               # Refreshes LUT List\n  GetUniqueId()                                   --> string             # Returns a unique ID for the project item\n  InsertAudioToCurrentTrackAtPlayhead(mediaPath,  --> Bool               # Inserts the media specified by mediaPath (string) with startOffsetInSamples (int) and durationInSamples (int) at the playhead on a selected track on the Fairlight page. Returns True if successful, otherwise False.\n          startOffsetInSamples, durationInSamples)\n  LoadBurnInPreset(presetName)                    --> Bool               # Loads user defined data burn in preset for project when supplied presetName (string). Returns true if successful.\n  ExportCurrentFrameAsStill(filePath)             --> Bool               # Exports current frame as still to supplied filePath. filePath must end in valid export file format. Returns True if succssful, False otherwise.\n\nMediaStorage\n  GetMountedVolumeList()                          --> [paths...]         # Returns list of folder paths corresponding to mounted volumes displayed in Resolve’s Media Storage.\n  GetSubFolderList(folderPath)                    --> [paths...]         # Returns list of folder paths in the given absolute folder path.\n  GetFileList(folderPath)                         --> [paths...]         # Returns list of media and file listings in the given absolute folder path. Note that media listings may be logically consolidated entries.\n  RevealInStorage(path)                           --> Bool               # Expands and displays given file/folder path in Resolve’s Media Storage.\n  AddItemListToMediaPool(item1, item2, ...)       --> [clips...]         # Adds specified file/folder paths from Media Storage into current Media Pool folder. Input is one or more file/folder paths. Returns a list of the MediaPoolItems created.\n  AddItemListToMediaPool([items...])              --> [clips...]         # Adds specified file/folder paths from Media Storage into current Media Pool folder. Input is an array of file/folder paths. Returns a list of the MediaPoolItems created.\n  AddItemListToMediaPool([{itemInfo}, ...])       --> [clips...]         # Adds list of itemInfos specified as dict of \"media\", \"startFrame\" (int), \"endFrame\" (int) from Media Storage into current Media Pool folder. Returns a list of the MediaPoolItems created.\n  AddClipMattesToMediaPool(MediaPoolItem, [paths], stereoEye) --> Bool   # Adds specified media files as mattes for the specified MediaPoolItem. StereoEye is an optional argument for specifying which eye to add the matte to for stereo clips (\"left\" or \"right\"). Returns True if successful.\n  AddTimelineMattesToMediaPool([paths])           --> [MediaPoolItems]   # Adds specified media files as timeline mattes in current media pool folder. Returns a list of created MediaPoolItems.\n\nMediaPool\n  GetRootFolder()                                 --> Folder             # Returns root Folder of Media Pool\n  AddSubFolder(folder, name)                      --> Folder             # Adds new subfolder under specified Folder object with the given name.\n  RefreshFolders()                                --> Bool               # Updates the folders in collaboration mode\n  CreateEmptyTimeline(name)                       --> Timeline           # Adds new timeline with given name.\n  AppendToTimeline(clip1, clip2, ...)             --> [TimelineItem]     # Appends specified MediaPoolItem objects in the current timeline. Returns the list of appended timelineItems.\n  AppendToTimeline([clips])                       --> [TimelineItem]     # Appends specified MediaPoolItem objects in the current timeline. Returns the list of appended timelineItems.\n  AppendToTimeline([{clipInfo}, ...])             --> [TimelineItem]     # Appends list of clipInfos specified as dict of \"mediaPoolItem\", \"startFrame\" (int), \"endFrame\" (int), (optional) \"mediaType\" (int; 1 - Video only, 2 - Audio only), \"trackIndex\" (int) and \"recordFrame\" (int). Returns the list of appended timelineItems.\n  CreateTimelineFromClips(name, clip1, clip2,...) --> Timeline           # Creates new timeline with specified name, and appends the specified MediaPoolItem objects.\n  CreateTimelineFromClips(name, [clips])          --> Timeline           # Creates new timeline with specified name, and appends the specified MediaPoolItem objects.\n  CreateTimelineFromClips(name, [{clipInfo}])     --> Timeline           # Creates new timeline with specified name, appending the list of clipInfos specified as a dict of \"mediaPoolItem\", \"startFrame\" (int), \"endFrame\" (int), \"recordFrame\" (int).\n  ImportTimelineFromFile(filePath, {importOptions}) --> Timeline         # Creates timeline based on parameters within given file (AAF/EDL/XML/FCPXML/DRT/ADL) and optional importOptions dict, with support for the keys:\n                                                                         # \"timelineName\": string, specifies the name of the timeline to be created. Not valid for DRT import\n                                                                         # \"importSourceClips\": Bool, specifies whether source clips should be imported, True by default. Not valid for DRT import\n                                                                         # \"sourceClipsPath\": string, specifies a filesystem path to search for source clips if the media is inaccessible in their original path and if \"importSourceClips\" is True\n                                                                         # \"sourceClipsFolders\": List of Media Pool folder objects to search for source clips if the media is not present in current folder and if \"importSourceClips\" is False. Not valid for DRT import\n                                                                         # \"interlaceProcessing\": Bool, specifies whether to enable interlace processing on the imported timeline being created. valid only for AAF import\n  DeleteTimelines([timeline])                     --> Bool               # Deletes specified timelines in the media pool.\n  GetCurrentFolder()                              --> Folder             # Returns currently selected Folder.\n  SetCurrentFolder(Folder)                        --> Bool               # Sets current folder by given Folder.\n  DeleteClips([clips])                            --> Bool               # Deletes specified clips or timeline mattes in the media pool\n  ImportFolderFromFile(filePath, sourceClipsPath=\"\") --> Bool            # Returns true if import from given DRB filePath is successful, false otherwise\n                                                                         # sourceClipsPath is a string that specifies a filesystem path to search for source clips if the media is inaccessible in their original path, empty by default\n  DeleteFolders([subfolders])                     --> Bool               # Deletes specified subfolders in the media pool\n  MoveClips([clips], targetFolder)                --> Bool               # Moves specified clips to target folder.\n  MoveFolders([folders], targetFolder)            --> Bool               # Moves specified folders to target folder.\n  GetClipMatteList(MediaPoolItem)                 --> [paths]            # Get mattes for specified MediaPoolItem, as a list of paths to the matte files.\n  GetTimelineMatteList(Folder)                    --> [MediaPoolItems]   # Get mattes in specified Folder, as list of MediaPoolItems.\n  DeleteClipMattes(MediaPoolItem, [paths])        --> Bool               # Delete mattes based on their file paths, for specified MediaPoolItem. Returns True on success.\n  RelinkClips([MediaPoolItem], folderPath)        --> Bool               # Update the folder location of specified media pool clips with the specified folder path.\n  UnlinkClips([MediaPoolItem])                    --> Bool               # Unlink specified media pool clips.\n  ImportMedia([items...])                         --> [MediaPoolItems]   # Imports specified file/folder paths into current Media Pool folder. Input is an array of file/folder paths. Returns a list of the MediaPoolItems created.\n  ImportMedia([{clipInfo}])                       --> [MediaPoolItems]   # Imports file path(s) into current Media Pool folder as specified in list of clipInfo dict. Returns a list of the MediaPoolItems created.\n                                                                         # Each clipInfo gets imported as one MediaPoolItem unless 'Show Individual Frames' is turned on.\n                                                                         # Example: ImportMedia([{\"FilePath\":\"file_%03d.dpx\", \"StartIndex\":1, \"EndIndex\":100}]) would import clip \"file_[001-100].dpx\".\n  ExportMetadata(fileName, [clips])               --> Bool               # Exports metadata of specified clips to 'fileName' in CSV format.\n                                                                         # If no clips are specified, all clips from media pool will be used.\n  GetUniqueId()                                   --> string             # Returns a unique ID for the media pool\n\nFolder\n  GetClipList()                                   --> [clips...]         # Returns a list of clips (items) within the folder.\n  GetName()                                       --> string             # Returns the media folder name.\n  GetSubFolderList()                              --> [folders...]       # Returns a list of subfolders in the folder.\n  GetIsFolderStale()                              --> bool               # Returns true if folder is stale in collaboration mode, false otherwise\n  GetUniqueId()                                   --> string             # Returns a unique ID for the media pool folder\n  Export(filePath)                                --> bool               # Returns true if export of DRB folder to filePath is successful, false otherwise\n\nMediaPoolItem\n  GetName()                                       --> string             # Returns the clip name.\n  GetMetadata(metadataType=None)                  --> string|dict        # Returns the metadata value for the key 'metadataType'.\n                                                                         # If no argument is specified, a dict of all set metadata properties is returned.\n  SetMetadata(metadataType, metadataValue)        --> Bool               # Sets the given metadata to metadataValue (string). Returns True if successful.\n  SetMetadata({metadata})                         --> Bool               # Sets the item metadata with specified 'metadata' dict. Returns True if successful.\n  GetMediaId()                                    --> string             # Returns the unique ID for the MediaPoolItem.\n  AddMarker(frameId, color, name, note, duration, --> Bool               # Creates a new marker at given frameId position and with given marker information. 'customData' is optional and helps to attach user specific data to the marker.\n            customData)\n  GetMarkers()                                    --> {markers...}       # Returns a dict (frameId -> {information}) of all markers and dicts with their information.\n                                                                         # Example of output format: {96.0: {'color': 'Green', 'duration': 1.0, 'note': '', 'name': 'Marker 1', 'customData': ''}, ...}\n                                                                         # In the above example - there is one 'Green' marker at offset 96 (position of the marker)\n  GetMarkerByCustomData(customData)               --> {markers...}       # Returns marker {information} for the first matching marker with specified customData.\n  UpdateMarkerCustomData(frameId, customData)     --> Bool               # Updates customData (string) for the marker at given frameId position. CustomData is not exposed via UI and is useful for scripting developer to attach any user specific data to markers.\n  GetMarkerCustomData(frameId)                    --> string             # Returns customData string for the marker at given frameId position.\n  DeleteMarkersByColor(color)                     --> Bool               # Delete all markers of the specified color from the media pool item. \"All\" as argument deletes all color markers.\n  DeleteMarkerAtFrame(frameNum)                   --> Bool               # Delete marker at frame number from the media pool item.\n  DeleteMarkerByCustomData(customData)            --> Bool               # Delete first matching marker with specified customData.\n  AddFlag(color)                                  --> Bool               # Adds a flag with given color (string).\n  GetFlagList()                                   --> [colors...]        # Returns a list of flag colors assigned to the item.\n  ClearFlags(color)                               --> Bool               # Clears the flag of the given color if one exists. An \"All\" argument is supported and clears all flags.\n  GetClipColor()                                  --> string             # Returns the item color as a string.\n  SetClipColor(colorName)                         --> Bool               # Sets the item color based on the colorName (string).\n  ClearClipColor()                                --> Bool               # Clears the item color.\n  GetClipProperty(propertyName=None)              --> string|dict        # Returns the property value for the key 'propertyName'.\n                                                                         # If no argument is specified, a dict of all clip properties is returned. Check the section below for more information.\n  SetClipProperty(propertyName, propertyValue)    --> Bool               # Sets the given property to propertyValue (string). Check the section below for more information.\n  LinkProxyMedia(proxyMediaFilePath)              --> Bool               # Links proxy media located at path specified by arg 'proxyMediaFilePath' with the current clip. 'proxyMediaFilePath' should be absolute clip path.\n  UnlinkProxyMedia()                              --> Bool               # Unlinks any proxy media associated with clip.\n  ReplaceClip(filePath)                           --> Bool               # Replaces the underlying asset and metadata of MediaPoolItem with the specified absolute clip path.\n  GetUniqueId()                                   --> string             # Returns a unique ID for the media pool item\n  TranscribeAudio()                               --> Bool               # Transcribes audio of the MediaPoolItem. Returns True if successful; False otherwise\n  ClearTranscription()                            --> Bool               # Clears audio transcription of the MediaPoolItem. Returns True if successful; False otherwise.\n\nTimeline\n  GetName()                                       --> string             # Returns the timeline name.\n  SetName(timelineName)                           --> Bool               # Sets the timeline name if timelineName (string) is unique. Returns True if successful.\n  GetStartFrame()                                 --> int                # Returns the frame number at the start of timeline.\n  GetEndFrame()                                   --> int                # Returns the frame number at the end of timeline.\n  SetStartTimecode(timecode)                      --> Bool               # Set the start timecode of the timeline to the string 'timecode'. Returns true when the change is successful, false otherwise.\n  GetStartTimecode()                              --> string             # Returns the start timecode for the timeline.\n  GetTrackCount(trackType)                        --> int                # Returns the number of tracks for the given track type (\"audio\", \"video\" or \"subtitle\").\n  AddTrack(trackType, optionalSubTrackType)       --> Bool               # Adds track of trackType (\"video\", \"subtitle\", \"audio\"). Second argument optionalSubTrackType is required for \"audio\"\n                                                                         # optionalSubTrackType can be one of {\"mono\", \"stereo\", \"5.1\", \"5.1film\", \"7.1\", \"7.1film\", \"adaptive1\", ... , \"adaptive24\"}\n  DeleteTrack(trackType, trackIndex)              --> Bool               # Deletes track of trackType (\"video\", \"subtitle\", \"audio\") and given trackIndex. 1 <= trackIndex <= GetTrackCount(trackType).\n  SetTrackEnable(trackType, trackIndex, Bool)     --> Bool               # Enables/Disables track with given trackType and trackIndex\n                                                                         # trackType is one of {\"audio\", \"video\", \"subtitle\"}\n                                                                         # 1 <= trackIndex <= GetTrackCount(trackType).\n  GetIsTrackEnabled(trackType, trackIndex)        --> Bool               # Returns True if track with given trackType and trackIndex is enabled and False otherwise.\n                                                                         # trackType is one of {\"audio\", \"video\", \"subtitle\"}\n                                                                         # 1 <= trackIndex <= GetTrackCount(trackType).\n  SetTrackLock(trackType, trackIndex, Bool)       --> Bool               # Locks/Unlocks track with given trackType and trackIndex\n                                                                         # trackType is one of {\"audio\", \"video\", \"subtitle\"}\n                                                                         # 1 <= trackIndex <= GetTrackCount(trackType).\n  GetIsTrackLocked(trackType, trackIndex)         --> Bool               # Returns True if track with given trackType and trackIndex is locked and False otherwise.\n                                                                         # trackType is one of {\"audio\", \"video\", \"subtitle\"}\n                                                                         # 1 <= trackIndex <= GetTrackCount(trackType).\n  DeleteClips([timelineItems], Bool)              --> Bool               # Deletes specified TimelineItems from the timeline, performing ripple delete if the second argument is True. Second argument is optional (The default for this is False)\n  SetClipsLinked([timelineItems], Bool)           --> Bool               # Links or unlinks the specified TimelineItems depending on second argument.\n  GetItemListInTrack(trackType, index)            --> [items...]         # Returns a list of timeline items on that track (based on trackType and index). 1 <= index <= GetTrackCount(trackType).\n  AddMarker(frameId, color, name, note, duration, --> Bool               # Creates a new marker at given frameId position and with given marker information. 'customData' is optional and helps to attach user specific data to the marker.\n            customData)\n  GetMarkers()                                    --> {markers...}       # Returns a dict (frameId -> {information}) of all markers and dicts with their information.\n                                                                         # Example: a value of {96.0: {'color': 'Green', 'duration': 1.0, 'note': '', 'name': 'Marker 1', 'customData': ''}, ...} indicates a single green marker at timeline offset 96\n  GetMarkerByCustomData(customData)               --> {markers...}       # Returns marker {information} for the first matching marker with specified customData.\n  UpdateMarkerCustomData(frameId, customData)     --> Bool               # Updates customData (string) for the marker at given frameId position. CustomData is not exposed via UI and is useful for scripting developer to attach any user specific data to markers.\n  GetMarkerCustomData(frameId)                    --> string             # Returns customData string for the marker at given frameId position.\n  DeleteMarkersByColor(color)                     --> Bool               # Deletes all timeline markers of the specified color. An \"All\" argument is supported and deletes all timeline markers.\n  DeleteMarkerAtFrame(frameNum)                   --> Bool               # Deletes the timeline marker at the given frame number.\n  DeleteMarkerByCustomData(customData)            --> Bool               # Delete first matching marker with specified customData.\n  ApplyGradeFromDRX(path, gradeMode, item1, item2, ...)--> Bool          # Loads a still from given file path (string) and applies grade to Timeline Items with gradeMode (int): 0 - \"No keyframes\", 1 - \"Source Timecode aligned\", 2 - \"Start Frames aligned\".\n  ApplyGradeFromDRX(path, gradeMode, [items])     --> Bool               # Loads a still from given file path (string) and applies grade to Timeline Items with gradeMode (int): 0 - \"No keyframes\", 1 - \"Source Timecode aligned\", 2 - \"Start Frames aligned\".\n  GetCurrentTimecode()                            --> string             # Returns a string timecode representation for the current playhead position, while on Cut, Edit, Color, Fairlight and Deliver pages.\n  SetCurrentTimecode(timecode)                    --> Bool               # Sets current playhead position from input timecode for Cut, Edit, Color, Fairlight and Deliver pages.\n  GetCurrentVideoItem()                           --> item               # Returns the current video timeline item.\n  GetCurrentClipThumbnailImage()                  --> {thumbnailData}    # Returns a dict (keys \"width\", \"height\", \"format\" and \"data\") with data containing raw thumbnail image data (RGB 8-bit image data encoded in base64 format) for current media in the Color Page.\n                                                                         # An example of how to retrieve and interpret thumbnails is provided in 6_get_current_media_thumbnail.py in the Examples folder.\n  GetTrackName(trackType, trackIndex)             --> string             # Returns the track name for track indicated by trackType (\"audio\", \"video\" or \"subtitle\") and index. 1 <= trackIndex <= GetTrackCount(trackType).\n  SetTrackName(trackType, trackIndex, name)       --> Bool               # Sets the track name (string) for track indicated by trackType (\"audio\", \"video\" or \"subtitle\") and index. 1 <= trackIndex <= GetTrackCount(trackType).\n  DuplicateTimeline(timelineName)                 --> timeline           # Duplicates the timeline and returns the created timeline, with the (optional) timelineName, on success.\n  CreateCompoundClip([timelineItems], {clipInfo}) --> timelineItem       # Creates a compound clip of input timeline items with an optional clipInfo map: {\"startTimecode\" : \"00:00:00:00\", \"name\" : \"Compound Clip 1\"}. It returns the created timeline item.\n  CreateFusionClip([timelineItems])               --> timelineItem       # Creates a Fusion clip of input timeline items. It returns the created timeline item.\n  ImportIntoTimeline(filePath, {importOptions})   --> Bool               # Imports timeline items from an AAF file and optional importOptions dict into the timeline, with support for the keys:\n                                                                         # \"autoImportSourceClipsIntoMediaPool\": Bool, specifies if source clips should be imported into media pool, True by default\n                                                                         # \"ignoreFileExtensionsWhenMatching\": Bool, specifies if file extensions should be ignored when matching, False by default\n                                                                         # \"linkToSourceCameraFiles\": Bool, specifies if link to source camera files should be enabled, False by default\n                                                                         # \"useSizingInfo\": Bool, specifies if sizing information should be used, False by default\n                                                                         # \"importMultiChannelAudioTracksAsLinkedGroups\": Bool, specifies if multi-channel audio tracks should be imported as linked groups, False by default\n                                                                         # \"insertAdditionalTracks\": Bool, specifies if additional tracks should be inserted, True by default\n                                                                         # \"insertWithOffset\": string, specifies insert with offset value in timecode format - defaults to \"00:00:00:00\", applicable if \"insertAdditionalTracks\" is False\n                                                                         # \"sourceClipsPath\": string, specifies a filesystem path to search for source clips if the media is inaccessible in their original path and if \"ignoreFileExtensionsWhenMatching\" is True\n                                                                         # \"sourceClipsFolders\": string, list of Media Pool folder objects to search for source clips if the media is not present in current folder\n\n  Export(fileName, exportType, exportSubtype)     --> Bool               # Exports timeline to 'fileName' as per input exportType & exportSubtype format.\n                                                                         # Refer to section \"Looking up timeline export properties\" for information on the parameters.\n  GetSetting(settingName)                         --> string             # Returns value of timeline setting (indicated by settingName : string). Check the section below for more information.\n  SetSetting(settingName, settingValue)           --> Bool               # Sets timeline setting (indicated by settingName : string) to the value (settingValue : string). Check the section below for more information.\n  InsertGeneratorIntoTimeline(generatorName)      --> TimelineItem       # Inserts a generator (indicated by generatorName : string) into the timeline.\n  InsertFusionGeneratorIntoTimeline(generatorName) --> TimelineItem      # Inserts a Fusion generator (indicated by generatorName : string) into the timeline.\n  InsertFusionCompositionIntoTimeline()           --> TimelineItem       # Inserts a Fusion composition into the timeline.\n  InsertOFXGeneratorIntoTimeline(generatorName)   --> TimelineItem       # Inserts an OFX generator (indicated by generatorName : string) into the timeline.\n  InsertTitleIntoTimeline(titleName)              --> TimelineItem       # Inserts a title (indicated by titleName : string) into the timeline.\n  InsertFusionTitleIntoTimeline(titleName)        --> TimelineItem       # Inserts a Fusion title (indicated by titleName : string) into the timeline.\n  GrabStill()                                     --> galleryStill       # Grabs still from the current video clip. Returns a GalleryStill object.\n  GrabAllStills(stillFrameSource)                 --> [galleryStill]     # Grabs stills from all the clips of the timeline at 'stillFrameSource' (1 - First frame, 2 - Middle frame). Returns the list of GalleryStill objects.\n  GetUniqueId()                                   --> string             # Returns a unique ID for the timeline\n  CreateSubtitlesFromAudio()                      --> Bool               # Creates subtitles from audio for the timeline. Returns True on success, False otherwise.\n  DetectSceneCuts()                               --> Bool               # Detects and makes scene cuts along the timeline. Returns True if successful, False otherwise.\n\nTimelineItem\n  GetName()                                       --> string             # Returns the item name.\n  GetDuration()                                   --> int                # Returns the item duration.\n  GetEnd()                                        --> int                # Returns the end frame position on the timeline.\n  GetFusionCompCount()                            --> int                # Returns number of Fusion compositions associated with the timeline item.\n  GetFusionCompByIndex(compIndex)                 --> fusionComp         # Returns the Fusion composition object based on given index. 1 <= compIndex <= timelineItem.GetFusionCompCount()\n  GetFusionCompNameList()                         --> [names...]         # Returns a list of Fusion composition names associated with the timeline item.\n  GetFusionCompByName(compName)                   --> fusionComp         # Returns the Fusion composition object based on given name.\n  GetLeftOffset()                                 --> int                # Returns the maximum extension by frame for clip from left side.\n  GetRightOffset()                                --> int                # Returns the maximum extension by frame for clip from right side.\n  GetStart()                                      --> int                # Returns the start frame position on the timeline.\n  SetProperty(propertyKey, propertyValue)         --> Bool               # Sets the value of property \"propertyKey\" to value \"propertyValue\"\n                                                                         # Refer to \"Looking up Timeline item properties\" for more information\n  GetProperty(propertyKey)                        --> int/[key:value]    # returns the value of the specified key\n                                                                         # if no key is specified, the method returns a dictionary(python) or table(lua) for all supported keys\n  AddMarker(frameId, color, name, note, duration, --> Bool               # Creates a new marker at given frameId position and with given marker information. 'customData' is optional and helps to attach user specific data to the marker.\n            customData)\n  GetMarkers()                                    --> {markers...}       # Returns a dict (frameId -> {information}) of all markers and dicts with their information.\n                                                                         # Example: a value of {96.0: {'color': 'Green', 'duration': 1.0, 'note': '', 'name': 'Marker 1', 'customData': ''}, ...} indicates a single green marker at clip offset 96\n  GetMarkerByCustomData(customData)               --> {markers...}       # Returns marker {information} for the first matching marker with specified customData.\n  UpdateMarkerCustomData(frameId, customData)     --> Bool               # Updates customData (string) for the marker at given frameId position. CustomData is not exposed via UI and is useful for scripting developer to attach any user specific data to markers.\n  GetMarkerCustomData(frameId)                    --> string             # Returns customData string for the marker at given frameId position.\n  DeleteMarkersByColor(color)                     --> Bool               # Delete all markers of the specified color from the timeline item. \"All\" as argument deletes all color markers.\n  DeleteMarkerAtFrame(frameNum)                   --> Bool               # Delete marker at frame number from the timeline item.\n  DeleteMarkerByCustomData(customData)            --> Bool               # Delete first matching marker with specified customData.\n  AddFlag(color)                                  --> Bool               # Adds a flag with given color (string).\n  GetFlagList()                                   --> [colors...]        # Returns a list of flag colors assigned to the item.\n  ClearFlags(color)                               --> Bool               # Clear flags of the specified color. An \"All\" argument is supported to clear all flags.\n  GetClipColor()                                  --> string             # Returns the item color as a string.\n  SetClipColor(colorName)                         --> Bool               # Sets the item color based on the colorName (string).\n  ClearClipColor()                                --> Bool               # Clears the item color.\n  AddFusionComp()                                 --> fusionComp         # Adds a new Fusion composition associated with the timeline item.\n  ImportFusionComp(path)                          --> fusionComp         # Imports a Fusion composition from given file path by creating and adding a new composition for the item.\n  ExportFusionComp(path, compIndex)               --> Bool               # Exports the Fusion composition based on given index to the path provided.\n  DeleteFusionCompByName(compName)                --> Bool               # Deletes the named Fusion composition.\n  LoadFusionCompByName(compName)                  --> fusionComp         # Loads the named Fusion composition as the active composition.\n  RenameFusionCompByName(oldName, newName)        --> Bool               # Renames the Fusion composition identified by oldName.\n  AddVersion(versionName, versionType)            --> Bool               # Adds a new color version for a video clip based on versionType (0 - local, 1 - remote).\n  GetCurrentVersion()                             --> {versionName...}   # Returns the current version of the video clip. The returned value will have the keys versionName and versionType(0 - local, 1 - remote).\n  DeleteVersionByName(versionName, versionType)   --> Bool               # Deletes a color version by name and versionType (0 - local, 1 - remote).\n  LoadVersionByName(versionName, versionType)     --> Bool               # Loads a named color version as the active version. versionType: 0 - local, 1 - remote.\n  RenameVersionByName(oldName, newName, versionType)--> Bool             # Renames the color version identified by oldName and versionType (0 - local, 1 - remote).\n  GetVersionNameList(versionType)                 --> [names...]         # Returns a list of all color versions for the given versionType (0 - local, 1 - remote).\n  GetMediaPoolItem()                              --> MediaPoolItem      # Returns the media pool item corresponding to the timeline item if one exists.\n  GetStereoConvergenceValues()                    --> {keyframes...}     # Returns a dict (offset -> value) of keyframe offsets and respective convergence values.\n  GetStereoLeftFloatingWindowParams()             --> {keyframes...}     # For the LEFT eye -> returns a dict (offset -> dict) of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values.\n  GetStereoRightFloatingWindowParams()            --> {keyframes...}     # For the RIGHT eye -> returns a dict (offset -> dict) of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values.\n  GetNumNodes()                                   --> int                # Returns the number of nodes in the current graph for the timeline item\n  ApplyArriCdlLut()                               --> Bool               # Applies ARRI CDL and LUT. Returns True if successful, False otherwise.\n  SetLUT(nodeIndex, lutPath)                      --> Bool               # Sets LUT on the node mapping the node index provided, 1 <= nodeIndex <= total number of nodes.\n                                                                         # The lutPath can be an absolute path, or a relative path (based off custom LUT paths or the master LUT path).\n                                                                         # The operation is successful for valid lut paths that Resolve has already discovered (see Project.RefreshLUTList).\n  GetLUT(nodeIndex)                               --> String             # Gets relative LUT path based on the node index provided, 1 <= nodeIndex <= total number of nodes.\n  SetCDL([CDL map])                               --> Bool               # Keys of map are: \"NodeIndex\", \"Slope\", \"Offset\", \"Power\", \"Saturation\", where 1 <= NodeIndex <= total number of nodes.\n                                                                         # Example python code - SetCDL({\"NodeIndex\" : \"1\", \"Slope\" : \"0.5 0.4 0.2\", \"Offset\" : \"0.4 0.3 0.2\", \"Power\" : \"0.6 0.7 0.8\", \"Saturation\" : \"0.65\"})\n  AddTake(mediaPoolItem, startFrame, endFrame)    --> Bool               # Adds mediaPoolItem as a new take. Initializes a take selector for the timeline item if needed. By default, the full clip extents is added. startFrame (int) and endFrame (int) are optional arguments used to specify the extents.\n  GetSelectedTakeIndex()                          --> int                # Returns the index of the currently selected take, or 0 if the clip is not a take selector.\n  GetTakesCount()                                 --> int                # Returns the number of takes in take selector, or 0 if the clip is not a take selector.\n  GetTakeByIndex(idx)                             --> {takeInfo...}      # Returns a dict (keys \"startFrame\", \"endFrame\" and \"mediaPoolItem\") with take info for specified index.\n  DeleteTakeByIndex(idx)                          --> Bool               # Deletes a take by index, 1 <= idx <= number of takes.\n  SelectTakeByIndex(idx)                          --> Bool               # Selects a take by index, 1 <= idx <= number of takes.\n  FinalizeTake()                                  --> Bool               # Finalizes take selection.\n  CopyGrades([tgtTimelineItems])                  --> Bool               # Copies the current grade to all the items in tgtTimelineItems list. Returns True on success and False if any error occurred.\n  SetClipEnabled(Bool)                            --> Bool               # Sets clip enabled based on argument.\n  GetClipEnabled()                                --> Bool               # Gets clip enabled status.\n  UpdateSidecar()                                 --> Bool               # Updates sidecar file for BRAW clips or RMD file for R3D clips.\n  GetUniqueId()                                   --> string             # Returns a unique ID for the timeline item\n  LoadBurnInPreset(presetName)                    --> Bool               # Loads user defined data burn in preset for clip when supplied presetName (string). Returns true if successful.\n  GetNodeLabel(nodeIndex)                         --> string             # Returns the label of the node at nodeIndex.\n  CreateMagicMask(mode)                           --> Bool               # Returns True if magic mask was created successfully, False otherwise. mode can \"F\" (forward), \"B\" (backward), or \"BI\" (bidirection)\n  RegenerateMagicMask()                           --> Bool               # Returns True if magic mask was regenerated successfully, False otherwise.\n  Stabilize()                                     --> Bool               # Returns True if stabilization was successful, False otherwise\n  SmartReframe()                                  --> Bool               # Performs Smart Reframe. Returns True if successful, False otherwise.\n\nGallery\n  GetAlbumName(galleryStillAlbum)                 --> string             # Returns the name of the GalleryStillAlbum object 'galleryStillAlbum'.\n  SetAlbumName(galleryStillAlbum, albumName)      --> Bool               # Sets the name of the GalleryStillAlbum object 'galleryStillAlbum' to 'albumName'.\n  GetCurrentStillAlbum()                          --> galleryStillAlbum  # Returns current album as a GalleryStillAlbum object.\n  SetCurrentStillAlbum(galleryStillAlbum)         --> Bool               # Sets current album to GalleryStillAlbum object 'galleryStillAlbum'.\n  GetGalleryStillAlbums()                         --> [galleryStillAlbum] # Returns the gallery albums as a list of GalleryStillAlbum objects.\n\nGalleryStillAlbum\n  GetStills()                                     --> [galleryStill]     # Returns the list of GalleryStill objects in the album.\n  GetLabel(galleryStill)                          --> string             # Returns the label of the galleryStill.\n  SetLabel(galleryStill, label)                   --> Bool               # Sets the new 'label' to GalleryStill object 'galleryStill'.\n  ExportStills([galleryStill], folderPath, filePrefix, format) --> Bool  # Exports list of GalleryStill objects '[galleryStill]' to directory 'folderPath', with filename prefix 'filePrefix', using file format 'format' (supported formats: dpx, cin, tif, jpg, png, ppm, bmp, xpm).\n  DeleteStills([galleryStill])                    --> Bool               # Deletes specified list of GalleryStill objects '[galleryStill]'.\n\nGalleryStill                                                             # This class does not provide any API functions but the object type is used by functions in other classes.\n\nList and Dict Data Structures\n-----------------------------\nBeside primitive data types, Resolve's Python API mainly uses list and dict data structures. Lists are denoted by [ ... ] and dicts are denoted by { ... } above.\nAs Lua does not support list and dict data structures, the Lua API implements \"list\" as a table with indices, e.g. { [1] = listValue1, [2] = listValue2, ... }.\nSimilarly the Lua API implements \"dict\" as a table with the dictionary key as first element, e.g. { [dictKey1] = dictValue1, [dictKey2] = dictValue2, ... }.\n\nLooking up Project and Clip properties\n--------------------------------------\nThis section covers additional notes for the functions \"Project:GetSetting\", \"Project:SetSetting\", \"Timeline:GetSetting\", \"Timeline:SetSetting\", \"MediaPoolItem:GetClipProperty\" and\n\"MediaPoolItem:SetClipProperty\". These functions are used to get and set properties otherwise available to the user through the Project Settings and the Clip Attributes dialogs.\n\nThe functions follow a key-value pair format, where each property is identified by a key (the settingName or propertyName parameter) and possesses a value (typically a text value). Keys and values are\ndesigned to be easily correlated with parameter names and values in the Resolve UI. Explicitly enumerated values for some parameters are listed below.\n\nSome properties may be read only - these include intrinsic clip properties like date created or sample rate, and properties that can be disabled in specific application contexts (e.g. custom colorspaces\nin an ACES workflow, or output sizing parameters when behavior is set to match timeline)\n\nGetting values:\nInvoke \"Project:GetSetting\", \"Timeline:GetSetting\" or \"MediaPoolItem:GetClipProperty\" with the appropriate property key. To get a snapshot of all queryable properties (keys and values), you can call\n\"Project:GetSetting\", \"Timeline:GetSetting\" or \"MediaPoolItem:GetClipProperty\" without parameters (or with a NoneType or a blank property key). Using specific keys to query individual properties will\nbe faster. Note that getting a property using an invalid key will return a trivial result.\n\nSetting values:\nInvoke \"Project:SetSetting\", \"Timeline:SetSetting\" or \"MediaPoolItem:SetClipProperty\" with the appropriate property key and a valid value. When setting a parameter, please check the return value to\nensure the success of the operation. You can troubleshoot the validity of keys and values by setting the desired result from the UI and checking property snapshots before and after the change.\n\nThe following Project properties have specifically enumerated values:\n\"superScale\" - the property value is an enumerated integer between 0 and 4 with these meanings: 0=Auto, 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x.\n               for super scale multiplier '2x Enhanced', exactly 4 arguments must be passed as outlined below. If less than 4 arguments are passed, it will default to 2x.\nAffects:\n• x = Project:GetSetting('superScale') and Project:SetSetting('superScale', x)\n• for '2x Enhanced' --> Project:SetSetting('superScale', 2, sharpnessValue, noiseReductionValue), where sharpnessValue is a float in the range [0.0, 1.0] and noiseReductionValue is a float in the range [0.0, 1.0]\n\n\"timelineFrameRate\" - the property value is one of the frame rates available to the user in project settings under \"Timeline frame rate\" option. Drop Frame can be configured for supported frame rates\n                      by appending the frame rate with \"DF\", e.g. \"29.97 DF\" will enable drop frame and \"29.97\" will disable drop frame\nAffects:\n• x = Project:GetSetting('timelineFrameRate') and Project:SetSetting('timelineFrameRate', x)\n\nThe following Clip properties have specifically enumerated values:\n\"Super Scale\" - the property value is an enumerated integer between 1 and 4 with these meanings: 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x.\n                for super scale multiplier '2x Enhanced', exactly 4 arguments must be passed as outlined below. If less than 4 arguments are passed, it will default to 2x.\nAffects:\n• x = MediaPoolItem:GetClipProperty('Super Scale') and MediaPoolItem:SetClipProperty('Super Scale', x)\n• for '2x Enhanced' --> MediaPoolItem:SetClipProperty('Super Scale', 2, sharpnessValue, noiseReductionValue), where sharpnessValue is a float in the range [0.0, 1.0] and noiseReductionValue is a float in the range [0.0, 1.0]\n\n\nLooking up Render Settings\n--------------------------\nThis section covers the supported settings for the method SetRenderSettings({settings})\n\nThe parameter setting is a dictionary containing the following keys:\n    - \"SelectAllFrames\": Bool (when set True, the settings MarkIn and MarkOut are ignored)\n    - \"MarkIn\": int\n    - \"MarkOut\": int\n    - \"TargetDir\": string\n    - \"CustomName\": string\n    - \"UniqueFilenameStyle\": 0 - Prefix, 1 - Suffix.\n    - \"ExportVideo\": Bool\n    - \"ExportAudio\": Bool\n    - \"FormatWidth\": int\n    - \"FormatHeight\": int\n    - \"FrameRate\": float (examples: 23.976, 24)\n    - \"PixelAspectRatio\": string (for SD resolution: \"16_9\" or \"4_3\") (other resolutions: \"square\" or \"cinemascope\")\n    - \"VideoQuality\" possible values for current codec (if applicable):\n    -    0 (int) - will set quality to automatic\n    -    [1 -> MAX] (int) - will set input bit rate\n    -    [\"Least\", \"Low\", \"Medium\", \"High\", \"Best\"] (String) - will set input quality level\n    - \"AudioCodec\": string (example: \"aac\")\n    - \"AudioBitDepth\": int\n    - \"AudioSampleRate\": int\n    - \"ColorSpaceTag\" : string (example: \"Same as Project\", \"AstroDesign\")\n    - \"GammaTag\" : string (example: \"Same as Project\", \"ACEScct\")\n    - \"ExportAlpha\": Bool\n    - \"EncodingProfile\": string (example: \"Main10\"). Can only be set for H.264 and H.265.\n    - \"MultiPassEncode\": Bool. Can only be set for H.264.\n    - \"AlphaMode\": 0 - Premultiplied, 1 - Straight. Can only be set if \"ExportAlpha\" is true.\n    - \"NetworkOptimization\": Bool. Only supported by QuickTime and MP4 formats.\n\nLooking up timeline export properties\n-------------------------------------\nThis section covers the parameters for the argument Export(fileName, exportType, exportSubtype).\n\nexportType can be one of the following constants:\n    - resolve.EXPORT_AAF\n    - resolve.EXPORT_DRT\n    - resolve.EXPORT_EDL\n    - resolve.EXPORT_FCP_7_XML\n    - resolve.EXPORT_FCPXML_1_8\n    - resolve.EXPORT_FCPXML_1_9\n    - resolve.EXPORT_FCPXML_1_10\n    - resolve.EXPORT_HDR_10_PROFILE_A\n    - resolve.EXPORT_HDR_10_PROFILE_B\n    - resolve.EXPORT_TEXT_CSV\n    - resolve.EXPORT_TEXT_TAB\n    - resolve.EXPORT_DOLBY_VISION_VER_2_9\n    - resolve.EXPORT_DOLBY_VISION_VER_4_0\n    - resolve.EXPORT_DOLBY_VISION_VER_5_1\n    - resolve.EXPORT_OTIO\nexportSubtype can be one of the following enums:\n    - resolve.EXPORT_NONE\n    - resolve.EXPORT_AAF_NEW\n    - resolve.EXPORT_AAF_EXISTING\n    - resolve.EXPORT_CDL\n    - resolve.EXPORT_SDL\n    - resolve.EXPORT_MISSING_CLIPS\nPlease note that exportSubType is a required parameter for resolve.EXPORT_AAF and resolve.EXPORT_EDL. For rest of the exportType, exportSubtype is ignored.\nWhen exportType is resolve.EXPORT_AAF, valid exportSubtype values are resolve.EXPORT_AAF_NEW and resolve.EXPORT_AAF_EXISTING.\nWhen exportType is resolve.EXPORT_EDL, valid exportSubtype values are resolve.EXPORT_CDL, resolve.EXPORT_SDL, resolve.EXPORT_MISSING_CLIPS and resolve.EXPORT_NONE.\nNote: Replace 'resolve.' when using the constants above, if a different Resolve class instance name is used.\n\nUnsupported exportType types\n---------------------------------\nStarting with DaVinci Resolve 18.1, the following export types are not supported:\n    - resolve.EXPORT_FCPXML_1_3\n    - resolve.EXPORT_FCPXML_1_4\n    - resolve.EXPORT_FCPXML_1_5\n    - resolve.EXPORT_FCPXML_1_6\n    - resolve.EXPORT_FCPXML_1_7\n\n\nLooking up Timeline item properties\n-----------------------------------\nThis section covers additional notes for the function \"TimelineItem:SetProperty\" and \"TimelineItem:GetProperty\". These functions are used to get and set properties mentioned.\n\nThe supported keys with their accepted values are:\n  \"Pan\" : floating point values from -4.0*width to 4.0*width\n  \"Tilt\" : floating point values from -4.0*height to 4.0*height\n  \"ZoomX\" : floating point values from 0.0 to 100.0\n  \"ZoomY\" : floating point values from 0.0 to 100.0\n  \"ZoomGang\" : a boolean value\n  \"RotationAngle\" : floating point values from -360.0 to 360.0\n  \"AnchorPointX\" : floating point values from -4.0*width to 4.0*width\n  \"AnchorPointY\" : floating point values from -4.0*height to 4.0*height\n  \"Pitch\" : floating point values from -1.5 to 1.5\n  \"Yaw\" : floating point values from -1.5 to 1.5\n  \"FlipX\" : boolean value for flipping horizontally\n  \"FlipY\" : boolean value for flipping vertically\n  \"CropLeft\" : floating point values from 0.0 to width\n  \"CropRight\" : floating point values from 0.0 to width\n  \"CropTop\" : floating point values from 0.0 to height\n  \"CropBottom\" : floating point values from 0.0 to height\n  \"CropSoftness\" : floating point values from -100.0 to 100.0\n  \"CropRetain\" : boolean value for \"Retain Image Position\" checkbox\n  \"DynamicZoomEase\" : A value from the following constants\n     - DYNAMIC_ZOOM_EASE_LINEAR = 0\n     - DYNAMIC_ZOOM_EASE_IN\n     - DYNAMIC_ZOOM_EASE_OUT\n     - DYNAMIC_ZOOM_EASE_IN_AND_OUT\n  \"CompositeMode\" : A value from the following constants\n     - COMPOSITE_NORMAL = 0\n     - COMPOSITE_ADD\n     - COMPOSITE_SUBTRACT\n     - COMPOSITE_DIFF\n     - COMPOSITE_MULTIPLY\n     - COMPOSITE_SCREEN\n     - COMPOSITE_OVERLAY\n     - COMPOSITE_HARDLIGHT\n     - COMPOSITE_SOFTLIGHT\n     - COMPOSITE_DARKEN\n     - COMPOSITE_LIGHTEN\n     - COMPOSITE_COLOR_DODGE\n     - COMPOSITE_COLOR_BURN\n     - COMPOSITE_EXCLUSION\n     - COMPOSITE_HUE\n     - COMPOSITE_SATURATE\n     - COMPOSITE_COLORIZE\n     - COMPOSITE_LUMA_MASK\n     - COMPOSITE_DIVIDE\n     - COMPOSITE_LINEAR_DODGE\n     - COMPOSITE_LINEAR_BURN\n     - COMPOSITE_LINEAR_LIGHT\n     - COMPOSITE_VIVID_LIGHT\n     - COMPOSITE_PIN_LIGHT\n     - COMPOSITE_HARD_MIX\n     - COMPOSITE_LIGHTER_COLOR\n     - COMPOSITE_DARKER_COLOR\n     - COMPOSITE_FOREGROUND\n     - COMPOSITE_ALPHA\n     - COMPOSITE_INVERTED_ALPHA\n     - COMPOSITE_LUM\n     - COMPOSITE_INVERTED_LUM\n  \"Opacity\" : floating point value from 0.0 to 100.0\n  \"Distortion\" : floating point value from -1.0 to 1.0\n  \"RetimeProcess\" : A value from the following constants\n     - RETIME_USE_PROJECT = 0\n     - RETIME_NEAREST\n     - RETIME_FRAME_BLEND\n     - RETIME_OPTICAL_FLOW\n  \"MotionEstimation\" : A value from the following constants\n     - MOTION_EST_USE_PROJECT = 0\n     - MOTION_EST_STANDARD_FASTER\n     - MOTION_EST_STANDARD_BETTER\n     - MOTION_EST_ENHANCED_FASTER\n     - MOTION_EST_ENHANCED_BETTER\n     - MOTION_EST_SPEED_WRAP\n  \"Scaling\" : A value from the following constants\n     - SCALE_USE_PROJECT = 0\n     - SCALE_CROP\n     - SCALE_FIT\n     - SCALE_FILL\n     - SCALE_STRETCH\n  \"ResizeFilter\" : A value from the following constants\n     - RESIZE_FILTER_USE_PROJECT = 0\n     - RESIZE_FILTER_SHARPER\n     - RESIZE_FILTER_SMOOTHER\n     - RESIZE_FILTER_BICUBIC\n     - RESIZE_FILTER_BILINEAR\n     - RESIZE_FILTER_BESSEL\n     - RESIZE_FILTER_BOX\n     - RESIZE_FILTER_CATMULL_ROM\n     - RESIZE_FILTER_CUBIC\n     - RESIZE_FILTER_GAUSSIAN\n     - RESIZE_FILTER_LANCZOS\n     - RESIZE_FILTER_MITCHELL\n     - RESIZE_FILTER_NEAREST_NEIGHBOR\n     - RESIZE_FILTER_QUADRATIC\n     - RESIZE_FILTER_SINC\n     - RESIZE_FILTER_LINEAR\nValues beyond the range will be clipped\nwidth and height are same as the UI max limits\n\nThe arguments can be passed as a key and value pair or they can be grouped together into a dictionary (for python) or table (for lua) and passed\nas a single argument.\n\nGetting the values for the keys that uses constants will return the number which is in the constant\n\nDeprecated Resolve API Functions\n--------------------------------\nThe following API functions are deprecated.\n\nProjectManager\n  GetProjectsInCurrentFolder()                    --> {project names...} # Returns a dict of project names in current folder.\n  GetFoldersInCurrentFolder()                     --> {folder names...}  # Returns a dict of folder names in current folder.\n\nProject\n  GetPresets()                                    --> {presets...}       # Returns a dict of presets and their information.\n  GetRenderJobs()                                 --> {render jobs...}   # Returns a dict of render jobs and their information.\n  GetRenderPresets()                              --> {presets...}       # Returns a dict of render presets and their information.\n\nMediaStorage\n  GetMountedVolumes()                             --> {paths...}         # Returns a dict of folder paths corresponding to mounted volumes displayed in Resolve’s Media Storage.\n  GetSubFolders(folderPath)                       --> {paths...}         # Returns a dict of folder paths in the given absolute folder path.\n  GetFiles(folderPath)                            --> {paths...}         # Returns a dict of media and file listings in the given absolute folder path. Note that media listings may be logically consolidated entries.\n  AddItemsToMediaPool(item1, item2, ...)          --> {clips...}         # Adds specified file/folder paths from Media Storage into current Media Pool folder. Input is one or more file/folder paths. Returns a dict of the MediaPoolItems created.\n  AddItemsToMediaPool([items...])                 --> {clips...}         # Adds specified file/folder paths from Media Storage into current Media Pool folder. Input is an array of file/folder paths. Returns a dict of the MediaPoolItems created.\n\nFolder\n  GetClips()                                      --> {clips...}         # Returns a dict of clips (items) within the folder.\n  GetSubFolders()                                 --> {folders...}       # Returns a dict of subfolders in the folder.\n\nMediaPoolItem\n  GetFlags()                                      --> {colors...}        # Returns a dict of flag colors assigned to the item.\n\nTimeline\n  GetItemsInTrack(trackType, index)               --> {items...}         # Returns a dict of Timeline items on the video or audio track (based on trackType) at specified\n\nTimelineItem\n  GetFusionCompNames()                            --> {names...}         # Returns a dict of Fusion composition names associated with the timeline item.\n  GetFlags()                                      --> {colors...}        # Returns a dict of flag colors assigned to the item.\n  GetVersionNames(versionType)                    --> {names...}         # Returns a dict of version names by provided versionType: 0 - local, 1 - remote.\n\n\nUnsupported Resolve API Functions\n---------------------------------\nThe following API (functions and parameters) are no longer supported. Use job IDs instead of indices.\n\nProject\n  StartRendering(index1, index2, ...)             --> Bool               # Please use unique job ids (string) instead of indices.\n  StartRendering([idxs...])                       --> Bool               # Please use unique job ids (string) instead of indices.\n  DeleteRenderJobByIndex(idx)                     --> Bool               # Please use unique job ids (string) instead of indices.\n  GetRenderJobStatus(idx)                         --> {status info}      # Please use unique job ids (string) instead of indices.\n  GetSetting and SetSetting                       --> {}                 # settingName videoMonitorUseRec601For422SDI is now replaced with videoMonitorUseMatrixOverrideFor422SDI and videoMonitorMatrixOverrideFor422SDI.\n                                                                         # settingName perfProxyMediaOn is now replaced with perfProxyMediaMode which takes values 0 - disabled, 1 - when available, 2 - when source not available.\n"
  },
  {
    "path": "openpype/hosts/resolve/__init__.py",
    "content": "from .addon import ResolveAddon\n\n\n__all__ = (\n    \"ResolveAddon\",\n)\n"
  },
  {
    "path": "openpype/hosts/resolve/addon.py",
    "content": "import os\n\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nfrom .utils import RESOLVE_ROOT_DIR\n\n\nclass ResolveAddon(OpenPypeModule, IHostAddon):\n    name = \"resolve\"\n    host_name = \"resolve\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [\n            os.path.join(RESOLVE_ROOT_DIR, \"hooks\")\n        ]\n\n    def get_workfile_extensions(self):\n        return [\".drp\"]\n"
  },
  {
    "path": "openpype/hosts/resolve/api/__init__.py",
    "content": "\"\"\"\nresolve api\n\"\"\"\nfrom .utils import (\n    get_resolve_module\n)\n\nfrom .pipeline import (\n    ResolveHost,\n    ls,\n    containerise,\n    update_container,\n    maintained_selection,\n    remove_instance,\n    list_instances\n)\n\nfrom .lib import (\n    maintain_current_timeline,\n    publish_clip_color,\n    get_project_manager,\n    get_current_project,\n    get_current_timeline,\n    get_any_timeline,\n    get_new_timeline,\n    create_bin,\n    get_media_pool_item,\n    create_media_pool_item,\n    create_timeline_item,\n    get_timeline_item,\n    get_video_track_names,\n    get_current_timeline_items,\n    get_pype_timeline_item_by_name,\n    get_timeline_item_pype_tag,\n    set_timeline_item_pype_tag,\n    imprint,\n    set_publish_attribute,\n    get_publish_attribute,\n    create_compound_clip,\n    swap_clips,\n    get_pype_clip_metadata,\n    set_project_manager_to_folder_name,\n    get_otio_clip_instance_data,\n    get_reformated_path\n)\n\nfrom .menu import launch_pype_menu\n\nfrom .plugin import (\n    ClipLoader,\n    TimelineItemLoader,\n    Creator,\n    PublishClip\n)\n\nfrom .workio import (\n    open_file,\n    save_file,\n    current_file,\n    has_unsaved_changes,\n    file_extensions,\n    work_root\n)\n\nfrom .testing_utils import TestGUI\n\n\nbmdvr = None\nbmdvf = None\n\n__all__ = [\n    \"bmdvr\",\n    \"bmdvf\",\n\n    # pipeline\n    \"ResolveHost\",\n    \"ls\",\n    \"containerise\",\n    \"update_container\",\n    \"maintained_selection\",\n    \"remove_instance\",\n    \"list_instances\",\n\n    # utils\n    \"get_resolve_module\",\n\n    # lib\n    \"maintain_current_timeline\",\n    \"publish_clip_color\",\n    \"get_project_manager\",\n    \"get_current_project\",\n    \"get_current_timeline\",\n    \"get_any_timeline\",\n    \"get_new_timeline\",\n    \"create_bin\",\n    \"get_media_pool_item\",\n    \"create_media_pool_item\",\n    \"create_timeline_item\",\n    \"get_timeline_item\",\n    \"get_video_track_names\",\n    \"get_current_timeline_items\",\n    \"get_pype_timeline_item_by_name\",\n    \"get_timeline_item_pype_tag\",\n    \"set_timeline_item_pype_tag\",\n    \"imprint\",\n    \"set_publish_attribute\",\n    \"get_publish_attribute\",\n    \"create_compound_clip\",\n    \"swap_clips\",\n    \"get_pype_clip_metadata\",\n    \"set_project_manager_to_folder_name\",\n    \"get_otio_clip_instance_data\",\n    \"get_reformated_path\",\n\n    # menu\n    \"launch_pype_menu\",\n\n    # plugin\n    \"ClipLoader\",\n    \"TimelineItemLoader\",\n    \"Creator\",\n    \"PublishClip\",\n\n    # workio\n    \"open_file\",\n    \"save_file\",\n    \"current_file\",\n    \"has_unsaved_changes\",\n    \"file_extensions\",\n    \"work_root\",\n\n    \"TestGUI\"\n]\n"
  },
  {
    "path": "openpype/hosts/resolve/api/action.py",
    "content": "# absolute_import is needed to counter the `module has no cmds error` in Maya\nfrom __future__ import absolute_import\n\nimport pyblish.api\n\n\nfrom openpype.pipeline.publish import get_errored_instances_from_context\n\n\nclass SelectInvalidAction(pyblish.api.Action):\n    \"\"\"Select invalid clips in Resolve timeline when plug-in failed.\n\n    To retrieve the invalid nodes this assumes a static `get_invalid()`\n    method is available on the plugin.\n\n    \"\"\"\n    label = \"Select invalid\"\n    on = \"failed\"  # This action is only available on a failed plug-in\n    icon = \"search\"  # Icon from Awesome Icon\n\n    def process(self, context, plugin):\n\n        try:\n            from .lib import get_project_manager\n            pm = get_project_manager()\n            self.log.debug(pm)\n        except ImportError:\n            raise ImportError(\"Current host is not Resolve\")\n\n        errored_instances = get_errored_instances_from_context(context,\n                                                               plugin=plugin)\n\n        # Get the invalid nodes for the plug-ins\n        self.log.info(\"Finding invalid clips..\")\n        invalid = list()\n        for instance in errored_instances:\n            invalid_nodes = plugin.get_invalid(instance)\n            if invalid_nodes:\n                if isinstance(invalid_nodes, (list, tuple)):\n                    invalid.extend(invalid_nodes)\n                else:\n                    self.log.warning(\"Plug-in returned to be invalid, \"\n                                     \"but has no selectable nodes.\")\n\n        # Ensure unique (process each node only once)\n        invalid = list(set(invalid))\n\n        if invalid:\n            self.log.info(\"Selecting invalid nodes: %s\" % \", \".join(invalid))\n            # TODO: select resolve timeline track items in current timeline\n        else:\n            self.log.info(\"No invalid nodes found.\")\n"
  },
  {
    "path": "openpype/hosts/resolve/api/lib.py",
    "content": "import sys\nimport json\nimport re\nimport os\nimport contextlib\nfrom opentimelineio import opentime\n\nfrom openpype.lib import Logger\nfrom openpype.pipeline.editorial import (\n    is_overlapping_otio_ranges,\n    frames_to_timecode\n)\n\nfrom ..otio import davinci_export as otio_export\n\nlog = Logger.get_logger(__name__)\n\nself = sys.modules[__name__]\nself.project_manager = None\nself.media_storage = None\n\n# OpenPype sequential rename variables\nself.rename_index = 0\nself.rename_add = 0\n\nself.publish_clip_color = \"Pink\"\nself.pype_marker_workflow = True\n\n# OpenPype compound clip workflow variable\nself.pype_tag_name = \"VFX Notes\"\n\n# OpenPype marker workflow variables\nself.pype_marker_name = \"OpenPypeData\"\nself.pype_marker_duration = 1\nself.pype_marker_color = \"Mint\"\nself.temp_marker_frame = None\n\n# OpenPype default timeline\nself.pype_timeline_name = \"OpenPypeTimeline\"\n\n\n@contextlib.contextmanager\ndef maintain_current_timeline(to_timeline: object,\n                              from_timeline: object = None):\n    \"\"\"Maintain current timeline selection during context\n\n    Attributes:\n        from_timeline (resolve.Timeline)[optional]:\n    Example:\n        >>> print(from_timeline.GetName())\n        timeline1\n        >>> print(to_timeline.GetName())\n        timeline2\n\n        >>> with maintain_current_timeline(to_timeline):\n        ...     print(get_current_timeline().GetName())\n        timeline2\n\n        >>> print(get_current_timeline().GetName())\n        timeline1\n    \"\"\"\n    project = get_current_project()\n    working_timeline = from_timeline or project.GetCurrentTimeline()\n\n    # switch to the input timeline\n    project.SetCurrentTimeline(to_timeline)\n\n    try:\n        # do a work\n        yield\n    finally:\n        # put the original working timeline to context\n        project.SetCurrentTimeline(working_timeline)\n\n\ndef get_project_manager():\n    from . import bmdvr\n    if not self.project_manager:\n        self.project_manager = bmdvr.GetProjectManager()\n    return self.project_manager\n\n\ndef get_media_storage():\n    from . import bmdvr\n    if not self.media_storage:\n        self.media_storage = bmdvr.GetMediaStorage()\n    return self.media_storage\n\n\ndef get_current_project():\n    \"\"\"Get current project object.\n    \"\"\"\n    return get_project_manager().GetCurrentProject()\n\n\ndef get_current_timeline(new=False):\n    \"\"\"Get current timeline object.\n\n    Args:\n        new (bool)[optional]: [DEPRECATED] if True it will create\n            new timeline if none exists\n\n    Returns:\n        TODO: will need to reflect future `None`\n        object: resolve.Timeline\n    \"\"\"\n    project = get_current_project()\n    timeline = project.GetCurrentTimeline()\n\n    # return current timeline if any\n    if timeline:\n        return timeline\n\n    # TODO: [deprecated] and will be removed in future\n    if new:\n        return get_new_timeline()\n\n\ndef get_any_timeline():\n    \"\"\"Get any timeline object.\n\n    Returns:\n        object | None: resolve.Timeline\n    \"\"\"\n    project = get_current_project()\n    timeline_count = project.GetTimelineCount()\n    if timeline_count > 0:\n        return project.GetTimelineByIndex(1)\n\n\ndef get_new_timeline(timeline_name: str = None):\n    \"\"\"Get new timeline object.\n\n    Arguments:\n        timeline_name (str): New timeline name.\n\n    Returns:\n        object: resolve.Timeline\n    \"\"\"\n    project = get_current_project()\n    media_pool = project.GetMediaPool()\n    new_timeline = media_pool.CreateEmptyTimeline(\n        timeline_name or self.pype_timeline_name)\n    project.SetCurrentTimeline(new_timeline)\n    return new_timeline\n\n\ndef create_bin(name: str, root: object = None) -> object:\n    \"\"\"\n    Create media pool's folder.\n\n    Return folder object and if the name does not exist it will create a new.\n    If the input name is with forward or backward slashes then it will create\n    all parents and return the last child bin object\n\n    Args:\n        name (str): name of folder / bin, or hierarchycal name \"parent/name\"\n        root (resolve.Folder)[optional]: root folder / bin object\n\n    Returns:\n        object: resolve.Folder\n    \"\"\"\n    # get all variables\n    media_pool = get_current_project().GetMediaPool()\n    root_bin = root or media_pool.GetRootFolder()\n\n    # create hierarchy of bins in case there is slash in name\n    if \"/\" in name.replace(\"\\\\\", \"/\"):\n        child_bin = None\n        for bname in name.split(\"/\"):\n            child_bin = create_bin(bname, child_bin or root_bin)\n        if child_bin:\n            return child_bin\n    else:\n        created_bin = None\n        for subfolder in root_bin.GetSubFolderList():\n            if subfolder.GetName() in name:\n                created_bin = subfolder\n\n        if not created_bin:\n            new_folder = media_pool.AddSubFolder(root_bin, name)\n            media_pool.SetCurrentFolder(new_folder)\n        else:\n            media_pool.SetCurrentFolder(created_bin)\n\n        return media_pool.GetCurrentFolder()\n\n\ndef remove_media_pool_item(media_pool_item: object) -> bool:\n    media_pool = get_current_project().GetMediaPool()\n    return media_pool.DeleteClips([media_pool_item])\n\n\ndef create_media_pool_item(\n        files: list,\n        root: object = None,\n) -> object:\n    \"\"\"\n    Create media pool item.\n\n    Args:\n        files (list[str]): list of absolute paths to files\n        root (resolve.Folder)[optional]: root folder / bin object\n\n    Returns:\n        object: resolve.MediaPoolItem\n    \"\"\"\n    # get all variables\n    media_pool = get_current_project().GetMediaPool()\n    root_bin = root or media_pool.GetRootFolder()\n\n    # make sure files list is not empty and first available file exists\n    filepath = next((f for f in files if os.path.isfile(f)), None)\n    if not filepath:\n        raise FileNotFoundError(\"No file found in input files list\")\n\n    # try to search in bin if the clip does not exist\n    existing_mpi = get_media_pool_item(filepath, root_bin)\n\n    if existing_mpi:\n        return existing_mpi\n\n    # add all data in folder to media pool\n    media_pool_items = media_pool.ImportMedia(files)\n\n    return media_pool_items.pop() if media_pool_items else False\n\n\ndef get_media_pool_item(filepath, root: object = None) -> object:\n    \"\"\"\n    Return clip if found in folder with use of input file path.\n\n    Args:\n        filepath (str): absolute path to a file\n        root (resolve.Folder)[optional]: root folder / bin object\n\n    Returns:\n        object: resolve.MediaPoolItem\n    \"\"\"\n    media_pool = get_current_project().GetMediaPool()\n    root = root or media_pool.GetRootFolder()\n    fname = os.path.basename(filepath)\n\n    for _mpi in root.GetClipList():\n        _mpi_name = _mpi.GetClipProperty(\"File Name\")\n        _mpi_name = get_reformated_path(_mpi_name, first=True)\n        if fname in _mpi_name:\n            return _mpi\n    return None\n\n\ndef create_timeline_item(\n        media_pool_item: object,\n        timeline: object = None,\n        timeline_in: int = None,\n        source_start: int = None,\n        source_end: int = None,\n) -> object:\n    \"\"\"\n    Add media pool item to current or defined timeline.\n\n    Args:\n        media_pool_item (resolve.MediaPoolItem): resolve's object\n        timeline (Optional[resolve.Timeline]): resolve's object\n        timeline_in (Optional[int]): timeline input frame (sequence frame)\n        source_start (Optional[int]): media source input frame (sequence frame)\n        source_end (Optional[int]): media source output frame (sequence frame)\n\n    Returns:\n        object: resolve.TimelineItem\n    \"\"\"\n    # get all variables\n    project = get_current_project()\n    media_pool = project.GetMediaPool()\n    _clip_property = media_pool_item.GetClipProperty\n    clip_name = _clip_property(\"File Name\")\n    timeline = timeline or get_current_timeline()\n\n    # timing variables\n    if all([timeline_in, source_start, source_end]):\n        fps = timeline.GetSetting(\"timelineFrameRate\")\n        duration = source_end - source_start\n        timecode_in = frames_to_timecode(timeline_in, fps)\n        timecode_out = frames_to_timecode(timeline_in + duration, fps)\n    else:\n        timecode_in = None\n        timecode_out = None\n\n    # if timeline was used then switch it to current timeline\n    with maintain_current_timeline(timeline):\n        # Add input mediaPoolItem to clip data\n        clip_data = {\n            \"mediaPoolItem\": media_pool_item,\n        }\n\n        if source_start:\n            clip_data[\"startFrame\"] = source_start\n        if source_end:\n            clip_data[\"endFrame\"] = source_end\n        if timecode_in:\n            clip_data[\"recordFrame\"] = timeline_in\n\n        # add to timeline\n        media_pool.AppendToTimeline([clip_data])\n\n        output_timeline_item = get_timeline_item(\n            media_pool_item, timeline)\n\n    assert output_timeline_item, AssertionError((\n        \"Clip name '{}' was't created on the timeline: '{}' \\n\\n\"\n        \"Please check if correct track position is activated, \\n\"\n        \"or if a clip is not already at the timeline in \\n\"\n        \"position: '{}' out: '{}'. \\n\\n\"\n        \"Clip data: {}\"\n    ).format(\n        clip_name, timeline.GetName(), timecode_in, timecode_out, clip_data\n    ))\n    return output_timeline_item\n\n\ndef get_timeline_item(media_pool_item: object,\n                      timeline: object = None) -> object:\n    \"\"\"\n    Returns clips related to input mediaPoolItem.\n\n    Args:\n        media_pool_item (resolve.MediaPoolItem): resolve's object\n        timeline (resolve.Timeline)[optional]: resolve's object\n\n    Returns:\n        object: resolve.TimelineItem\n    \"\"\"\n    _clip_property = media_pool_item.GetClipProperty\n    clip_name = _clip_property(\"File Name\")\n    output_timeline_item = None\n    timeline = timeline or get_current_timeline()\n\n    with maintain_current_timeline(timeline):\n        # search the timeline for the added clip\n\n        for _ti_data in get_current_timeline_items():\n            _ti_clip = _ti_data[\"clip\"][\"item\"]\n            _ti_clip_property = _ti_clip.GetMediaPoolItem().GetClipProperty\n            if clip_name in _ti_clip_property(\"File Name\"):\n                output_timeline_item = _ti_clip\n\n    return output_timeline_item\n\n\ndef get_video_track_names() -> list:\n    tracks = list()\n    track_type = \"video\"\n    timeline = get_current_timeline()\n\n    # get all tracks count filtered by track type\n    selected_track_count = timeline.GetTrackCount(track_type)\n\n    # loop all tracks and get items\n    track_index: int\n    for track_index in range(1, (int(selected_track_count) + 1)):\n        track_name = timeline.GetTrackName(\"video\", track_index)\n        tracks.append(track_name)\n\n    return tracks\n\n\ndef get_current_timeline_items(\n        filter: bool = False,\n        track_type: str = None,\n        track_name: str = None,\n        selecting_color: str = None) -> list:\n    \"\"\" Gets all available current timeline track items\n    \"\"\"\n    track_type = track_type or \"video\"\n    selecting_color = selecting_color or \"Chocolate\"\n    project = get_current_project()\n\n    # get timeline anyhow\n    timeline = (\n        get_current_timeline() or\n        get_any_timeline() or\n        get_new_timeline()\n    )\n    selected_clips = []\n\n    # get all tracks count filtered by track type\n    selected_track_count = timeline.GetTrackCount(track_type)\n\n    # loop all tracks and get items\n    _clips = {}\n    for track_index in range(1, (int(selected_track_count) + 1)):\n        _track_name = timeline.GetTrackName(track_type, track_index)\n\n        # filter out all unmathed track names\n        if track_name and _track_name not in track_name:\n            continue\n\n        timeline_items = timeline.GetItemListInTrack(\n            track_type, track_index)\n        _clips[track_index] = timeline_items\n\n        _data = {\n            \"project\": project,\n            \"timeline\": timeline,\n            \"track\": {\n                \"name\": _track_name,\n                \"index\": track_index,\n                \"type\": track_type}\n        }\n        # get track item object and its color\n        for clip_index, ti in enumerate(_clips[track_index]):\n            data = _data.copy()\n            data[\"clip\"] = {\n                \"item\": ti,\n                \"index\": clip_index\n            }\n            ti_color = ti.GetClipColor()\n            if filter and selecting_color in ti_color or not filter:\n                selected_clips.append(data)\n    return selected_clips\n\n\ndef get_pype_timeline_item_by_name(name: str) -> object:\n    \"\"\"Get timeline item by name.\n\n    Args:\n        name (str): name of timeline item\n\n    Returns:\n        object: resolve.TimelineItem\n    \"\"\"\n    for _ti_data in get_current_timeline_items():\n        _ti_clip = _ti_data[\"clip\"][\"item\"]\n        tag_data = get_timeline_item_pype_tag(_ti_clip)\n        tag_name = tag_data.get(\"namespace\")\n        if not tag_name:\n            continue\n        if tag_name in name:\n            return _ti_clip\n    return None\n\n\ndef get_timeline_item_pype_tag(timeline_item):\n    \"\"\"\n    Get openpype track item tag created by creator or loader plugin.\n\n    Attributes:\n        trackItem (resolve.TimelineItem): resolve object\n\n    Returns:\n        dict: openpype tag data\n    \"\"\"\n    return_tag = None\n\n    if self.pype_marker_workflow:\n        return_tag = get_pype_marker(timeline_item)\n    else:\n        media_pool_item = timeline_item.GetMediaPoolItem()\n\n        # get all tags from track item\n        _tags = media_pool_item.GetMetadata()\n        if not _tags:\n            return None\n        for key, data in _tags.items():\n            # return only correct tag defined by global name\n            if key in self.pype_tag_name:\n                return_tag = json.loads(data)\n\n    return return_tag\n\n\ndef set_timeline_item_pype_tag(timeline_item, data=None):\n    \"\"\"\n    Set openpype track item tag to input timeline_item.\n\n    Attributes:\n        trackItem (resolve.TimelineItem): resolve api object\n\n    Returns:\n        dict: json loaded data\n    \"\"\"\n    data = data or dict()\n\n    # get available openpype tag if any\n    tag_data = get_timeline_item_pype_tag(timeline_item)\n\n    if self.pype_marker_workflow:\n        # delete tag as it is not updatable\n        if tag_data:\n            delete_pype_marker(timeline_item)\n\n        tag_data.update(data)\n        set_pype_marker(timeline_item, tag_data)\n    else:\n        if tag_data:\n            media_pool_item = timeline_item.GetMediaPoolItem()\n            # it not tag then create one\n            tag_data.update(data)\n            media_pool_item.SetMetadata(\n                self.pype_tag_name, json.dumps(tag_data))\n        else:\n            tag_data = data\n            # if openpype tag available then update with input data\n            # add it to the input track item\n            timeline_item.SetMetadata(self.pype_tag_name, json.dumps(tag_data))\n\n    return tag_data\n\n\ndef imprint(timeline_item, data=None):\n    \"\"\"\n    Adding `Avalon data` into a hiero track item tag.\n\n    Also including publish attribute into tag.\n\n    Arguments:\n        timeline_item (hiero.core.TrackItem): hiero track item object\n        data (dict): Any data which needs to be imprinted\n\n    Examples:\n        data = {\n            'asset': 'sq020sh0280',\n            'family': 'render',\n            'subset': 'subsetMain'\n        }\n    \"\"\"\n    data = data or {}\n\n    set_timeline_item_pype_tag(timeline_item, data)\n\n    # add publish attribute\n    set_publish_attribute(timeline_item, True)\n\n\ndef set_publish_attribute(timeline_item, value):\n    \"\"\" Set Publish attribute in input Tag object\n\n    Attribute:\n        tag (hiero.core.Tag): a tag object\n        value (bool): True or False\n    \"\"\"\n    tag_data = get_timeline_item_pype_tag(timeline_item)\n    tag_data[\"publish\"] = value\n    # set data to the publish attribute\n    set_timeline_item_pype_tag(timeline_item, tag_data)\n\n\ndef get_publish_attribute(timeline_item):\n    \"\"\" Get Publish attribute from input Tag object\n\n    Attribute:\n        tag (hiero.core.Tag): a tag object\n        value (bool): True or False\n    \"\"\"\n    tag_data = get_timeline_item_pype_tag(timeline_item)\n    return tag_data[\"publish\"]\n\n\ndef set_pype_marker(timeline_item, tag_data):\n    source_start = timeline_item.GetLeftOffset()\n    item_duration = timeline_item.GetDuration()\n    frame = int(source_start + (item_duration / 2))\n\n    # marker attributes\n    frameId = (frame / 10) * 10\n    color = self.pype_marker_color\n    name = self.pype_marker_name\n    note = json.dumps(tag_data)\n    duration = (self.pype_marker_duration / 10) * 10\n\n    timeline_item.AddMarker(\n        frameId,\n        color,\n        name,\n        note,\n        duration\n    )\n\n\ndef get_pype_marker(timeline_item):\n    timeline_item_markers = timeline_item.GetMarkers()\n    for marker_frame, marker in timeline_item_markers.items():\n        color = marker[\"color\"]\n        name = marker[\"name\"]\n        if name == self.pype_marker_name and color == self.pype_marker_color:\n            note = marker[\"note\"]\n            self.temp_marker_frame = marker_frame\n            return json.loads(note)\n\n    return dict()\n\n\ndef delete_pype_marker(timeline_item):\n    timeline_item.DeleteMarkerAtFrame(self.temp_marker_frame)\n    self.temp_marker_frame = None\n\n\ndef create_compound_clip(clip_data, name, folder):\n    \"\"\"\n    Convert timeline object into nested timeline object\n\n    Args:\n        clip_data (dict): timeline item object packed into dict\n                          with project, timeline (sequence)\n        folder (resolve.MediaPool.Folder): media pool folder object,\n        name (str): name for compound clip\n\n    Returns:\n        resolve.MediaPoolItem: media pool item with compound clip timeline(cct)\n    \"\"\"\n    # get basic objects form data\n    project = clip_data[\"project\"]\n    timeline = clip_data[\"timeline\"]\n    clip = clip_data[\"clip\"]\n\n    # get details of objects\n    clip_item = clip[\"item\"]\n\n    mp = project.GetMediaPool()\n\n    # get clip attributes\n    clip_attributes = get_clip_attributes(clip_item)\n\n    mp_item = clip_item.GetMediaPoolItem()\n    _mp_props = mp_item.GetClipProperty\n\n    mp_first_frame = int(_mp_props(\"Start\"))\n    mp_last_frame = int(_mp_props(\"End\"))\n\n    # initialize basic source timing for otio\n    ci_l_offset = clip_item.GetLeftOffset()\n    ci_duration = clip_item.GetDuration()\n    rate = float(_mp_props(\"FPS\"))\n\n    # source rational times\n    mp_in_rc = opentime.RationalTime((ci_l_offset), rate)\n    mp_out_rc = opentime.RationalTime((ci_l_offset + ci_duration - 1), rate)\n\n    # get frame in and out for clip swapping\n    in_frame = opentime.to_frames(mp_in_rc)\n    out_frame = opentime.to_frames(mp_out_rc)\n\n    # keep original sequence\n    tl_origin = timeline\n\n    # Set current folder to input media_pool_folder:\n    mp.SetCurrentFolder(folder)\n\n    # check if clip doesn't exist already:\n    clips = folder.GetClipList()\n    cct = next((c for c in clips\n                if c.GetName() in name), None)\n\n    if cct:\n        print(f\"Compound clip exists: {cct}\")\n    else:\n        # Create empty timeline in current folder and give name:\n        cct = mp.CreateEmptyTimeline(name)\n\n        # check if clip doesn't exist already:\n        clips = folder.GetClipList()\n        cct = next((c for c in clips\n                    if c.GetName() in name), None)\n        print(f\"Compound clip created: {cct}\")\n\n        with maintain_current_timeline(cct, tl_origin):\n            # Add input clip to the current timeline:\n            mp.AppendToTimeline([{\n                \"mediaPoolItem\": mp_item,\n                \"startFrame\": mp_first_frame,\n                \"endFrame\": mp_last_frame\n            }])\n\n    # Add collected metadata and attributes to the comound clip:\n    if mp_item.GetMetadata(self.pype_tag_name):\n        clip_attributes[self.pype_tag_name] = mp_item.GetMetadata(\n            self.pype_tag_name)[self.pype_tag_name]\n\n    # stringify\n    clip_attributes = json.dumps(clip_attributes)\n\n    # add attributes to metadata\n    for k, v in mp_item.GetMetadata().items():\n        cct.SetMetadata(k, v)\n\n    # add metadata to cct\n    cct.SetMetadata(self.pype_tag_name, clip_attributes)\n\n    # reset start timecode of the compound clip\n    cct.SetClipProperty(\"Start TC\", _mp_props(\"Start TC\"))\n\n    # swap clips on timeline\n    swap_clips(clip_item, cct, in_frame, out_frame)\n\n    cct.SetClipColor(\"Pink\")\n    return cct\n\n\ndef swap_clips(from_clip, to_clip, to_in_frame, to_out_frame):\n    \"\"\"\n    Swapping clips on timeline in timelineItem\n\n    It will add take and activate it to the frame range which is inputted\n\n    Args:\n        from_clip (resolve.TimelineItem)\n        to_clip (resolve.mediaPoolItem)\n        to_clip_name (str): name of to_clip\n        to_in_frame (float): cut in frame, usually `GetLeftOffset()`\n        to_out_frame (float): cut out frame, usually left offset plus duration\n\n    Returns:\n        bool: True if successfully replaced\n\n    \"\"\"\n    # copy ACES input transform from timeline clip to new media item\n    mediapool_item_from_timeline = from_clip.GetMediaPoolItem()\n    _idt = mediapool_item_from_timeline.GetClipProperty('IDT')\n    to_clip.SetClipProperty('IDT', _idt)\n\n    _clip_prop = to_clip.GetClipProperty\n    to_clip_name = _clip_prop(\"File Name\")\n    # add clip item as take to timeline\n    take = from_clip.AddTake(\n        to_clip,\n        float(to_in_frame),\n        float(to_out_frame)\n    )\n\n    if not take:\n        return False\n\n    for take_index in range(1, (int(from_clip.GetTakesCount()) + 1)):\n        take_item = from_clip.GetTakeByIndex(take_index)\n        take_mp_item = take_item[\"mediaPoolItem\"]\n        if to_clip_name in take_mp_item.GetName():\n            from_clip.SelectTakeByIndex(take_index)\n            from_clip.FinalizeTake()\n            return True\n    return False\n\n\ndef _validate_tc(x):\n    # Validate and reformat timecode string\n\n    if len(x) != 11:\n        print('Invalid timecode. Try again.')\n\n    c = ':'\n    colonized = x[:2] + c + x[3:5] + c + x[6:8] + c + x[9:]\n\n    if colonized.replace(':', '').isdigit():\n        print(f\"_ colonized: {colonized}\")\n        return colonized\n    else:\n        print('Invalid timecode. Try again.')\n\n\ndef get_pype_clip_metadata(clip):\n    \"\"\"\n    Get openpype metadata created by creator plugin\n\n    Attributes:\n        clip (resolve.TimelineItem): resolve's object\n\n    Returns:\n        dict: hierarchy, orig clip attributes\n    \"\"\"\n    mp_item = clip.GetMediaPoolItem()\n    metadata = mp_item.GetMetadata()\n\n    return metadata.get(self.pype_tag_name)\n\n\ndef get_clip_attributes(clip):\n    \"\"\"\n    Collect basic attributes from resolve timeline item\n\n    Args:\n        clip (resolve.TimelineItem): timeline item object\n\n    Returns:\n        dict: all collected attributres as key: values\n    \"\"\"\n    mp_item = clip.GetMediaPoolItem()\n\n    return {\n        \"clipIn\": clip.GetStart(),\n        \"clipOut\": clip.GetEnd(),\n        \"clipLeftOffset\": clip.GetLeftOffset(),\n        \"clipRightOffset\": clip.GetRightOffset(),\n        \"clipMarkers\": clip.GetMarkers(),\n        \"clipFlags\": clip.GetFlagList(),\n        \"sourceId\": mp_item.GetMediaId(),\n        \"sourceProperties\": mp_item.GetClipProperty()\n    }\n\n\ndef set_project_manager_to_folder_name(folder_name):\n    \"\"\"\n    Sets context of Project manager to given folder by name.\n\n    Searching for folder by given name from root folder to nested.\n    If no existing folder by name it will create one in root folder.\n\n    Args:\n        folder_name (str): name of searched folder\n\n    Returns:\n        bool: True if success\n\n    Raises:\n        Exception: Cannot create folder in root\n\n    \"\"\"\n    # initialize project manager\n    get_project_manager()\n\n    set_folder = False\n\n    # go back to root folder\n    if self.project_manager.GotoRootFolder():\n        log.info(f\"Testing existing folder: {folder_name}\")\n        folders = _convert_resolve_list_type(\n            self.project_manager.GetFoldersInCurrentFolder())\n        log.info(f\"Testing existing folders: {folders}\")\n        # get me first available folder object\n        # with the same name as in `folder_name` else return False\n        if next((f for f in folders if f in folder_name), False):\n            log.info(f\"Found existing folder: {folder_name}\")\n            set_folder = self.project_manager.OpenFolder(folder_name)\n\n    if set_folder:\n        return True\n\n    # if folder by name is not existent then create one\n    # go back to root folder\n    log.info(f\"Folder `{folder_name}` not found and will be created\")\n    if self.project_manager.GotoRootFolder():\n        try:\n            # create folder by given name\n            self.project_manager.CreateFolder(folder_name)\n            self.project_manager.OpenFolder(folder_name)\n            return True\n        except NameError as e:\n            log.error((f\"Folder with name `{folder_name}` cannot be created!\"\n                       f\"Error: {e}\"))\n            return False\n\n\ndef _convert_resolve_list_type(resolve_list):\n    \"\"\" Resolve is using indexed dictionary as list type.\n    `{1.0: 'vaule'}`\n    This will convert it to normal list class\n    \"\"\"\n    assert isinstance(resolve_list, dict), (\n        \"Input argument should be dict() type\")\n\n    return [resolve_list[i] for i in sorted(resolve_list.keys())]\n\n\ndef create_otio_time_range_from_timeline_item_data(timeline_item_data):\n    timeline_item = timeline_item_data[\"clip\"][\"item\"]\n    project = timeline_item_data[\"project\"]\n    timeline = timeline_item_data[\"timeline\"]\n    timeline_start = timeline.GetStartFrame()\n\n    frame_start = int(timeline_item.GetStart() - timeline_start)\n    frame_duration = int(timeline_item.GetDuration())\n    fps = project.GetSetting(\"timelineFrameRate\")\n\n    return otio_export.create_otio_time_range(\n        frame_start, frame_duration, fps)\n\n\ndef get_otio_clip_instance_data(otio_timeline, timeline_item_data):\n    \"\"\"\n    Return otio objects for timeline, track and clip\n\n    Args:\n        timeline_item_data (dict): timeline_item_data from list returned by\n                                resolve.get_current_timeline_items()\n        otio_timeline (otio.schema.Timeline): otio object\n\n    Returns:\n        dict: otio clip object\n\n    \"\"\"\n\n    timeline_item = timeline_item_data[\"clip\"][\"item\"]\n    track_name = timeline_item_data[\"track\"][\"name\"]\n    timeline_range = create_otio_time_range_from_timeline_item_data(\n        timeline_item_data)\n\n    for otio_clip in otio_timeline.each_clip():\n        track_name = otio_clip.parent().name\n        parent_range = otio_clip.range_in_parent()\n        if track_name not in track_name:\n            continue\n        if otio_clip.name not in timeline_item.GetName():\n            continue\n        if is_overlapping_otio_ranges(\n                parent_range, timeline_range, strict=True):\n\n            # add pypedata marker to otio_clip metadata\n            for marker in otio_clip.markers:\n                if self.pype_marker_name in marker.name:\n                    otio_clip.metadata.update(marker.metadata)\n            return {\"otioClip\": otio_clip}\n\n    return None\n\n\ndef get_reformated_path(path, padded=False, first=False):\n    \"\"\"\n    Return fixed python expression path\n\n    Args:\n        path (str): path url or simple file name\n\n    Returns:\n        type: string with reformated path\n\n    Example:\n        get_reformated_path(\"plate.[0001-1008].exr\") > plate.%04d.exr\n\n    \"\"\"\n    first_frame_pattern = re.compile(r\"\\[(\\d+)\\-\\d+\\]\")\n\n    if \"[\" in path:\n        padding_pattern = r\"(\\d+)(?=-)\"\n        padding = len(re.findall(padding_pattern, path).pop())\n        num_pattern = r\"(\\[\\d+\\-\\d+\\])\"\n        if padded:\n            path = re.sub(num_pattern, f\"%0{padding}d\", path)\n        elif first:\n            first_frame = re.findall(first_frame_pattern, path, flags=0)\n            if len(first_frame) >= 1:\n                first_frame = first_frame[0]\n            path = re.sub(num_pattern, first_frame, path)\n        else:\n            path = re.sub(num_pattern, \"%d\", path)\n    return path\n"
  },
  {
    "path": "openpype/hosts/resolve/api/menu.py",
    "content": "import os\nimport sys\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.tools.utils import host_tools\nfrom openpype.pipeline import registered_host\n\n\nMENU_LABEL = os.environ[\"AVALON_LABEL\"]\n\n\ndef load_stylesheet():\n    path = os.path.join(os.path.dirname(__file__), \"menu_style.qss\")\n    if not os.path.exists(path):\n        print(\"Unable to load stylesheet, file not found in resources\")\n        return \"\"\n\n    with open(path, \"r\") as file_stream:\n        stylesheet = file_stream.read()\n    return stylesheet\n\n\nclass Spacer(QtWidgets.QWidget):\n    def __init__(self, height, *args, **kwargs):\n        super(Spacer, self).__init__(*args, **kwargs)\n\n        self.setFixedHeight(height)\n\n        real_spacer = QtWidgets.QWidget(self)\n        real_spacer.setObjectName(\"Spacer\")\n        real_spacer.setFixedHeight(height)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(real_spacer)\n\n        self.setLayout(layout)\n\n\nclass OpenPypeMenu(QtWidgets.QWidget):\n    def __init__(self, *args, **kwargs):\n        super(OpenPypeMenu, self).__init__(*args, **kwargs)\n\n        self.setObjectName(f\"{MENU_LABEL}Menu\")\n\n        self.setWindowFlags(\n            QtCore.Qt.Window\n            | QtCore.Qt.CustomizeWindowHint\n            | QtCore.Qt.WindowTitleHint\n            | QtCore.Qt.WindowCloseButtonHint\n            | QtCore.Qt.WindowStaysOnTopHint\n        )\n\n        self.setWindowTitle(f\"{MENU_LABEL}\")\n        save_current_btn = QtWidgets.QPushButton(\"Save current file\", self)\n        workfiles_btn = QtWidgets.QPushButton(\"Workfiles ...\", self)\n        create_btn = QtWidgets.QPushButton(\"Create ...\", self)\n        publish_btn = QtWidgets.QPushButton(\"Publish ...\", self)\n        load_btn = QtWidgets.QPushButton(\"Load ...\", self)\n        inventory_btn = QtWidgets.QPushButton(\"Manager ...\", self)\n        subsetm_btn = QtWidgets.QPushButton(\"Subset Manager ...\", self)\n        libload_btn = QtWidgets.QPushButton(\"Library ...\", self)\n        experimental_btn = QtWidgets.QPushButton(\n            \"Experimental tools ...\", self\n        )\n        # rename_btn = QtWidgets.QPushButton(\"Rename\", self)\n        # set_colorspace_btn = QtWidgets.QPushButton(\n        #     \"Set colorspace from presets\", self\n        # )\n        # reset_resolution_btn = QtWidgets.QPushButton(\n        #     \"Set Resolution from presets\", self\n        # )\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(10, 20, 10, 20)\n\n        layout.addWidget(save_current_btn)\n\n        layout.addWidget(Spacer(15, self))\n\n        layout.addWidget(workfiles_btn)\n        layout.addWidget(create_btn)\n        layout.addWidget(publish_btn)\n        layout.addWidget(load_btn)\n        layout.addWidget(inventory_btn)\n        layout.addWidget(subsetm_btn)\n\n        layout.addWidget(Spacer(15, self))\n\n        layout.addWidget(libload_btn)\n\n        # layout.addWidget(Spacer(15, self))\n\n        # layout.addWidget(rename_btn)\n\n        # layout.addWidget(Spacer(15, self))\n\n        # layout.addWidget(set_colorspace_btn)\n        # layout.addWidget(reset_resolution_btn)\n        layout.addWidget(Spacer(15, self))\n        layout.addWidget(experimental_btn)\n\n        self.setLayout(layout)\n\n        save_current_btn.clicked.connect(self.on_save_current_clicked)\n        save_current_btn.setShortcut(QtGui.QKeySequence.Save)\n        workfiles_btn.clicked.connect(self.on_workfile_clicked)\n        create_btn.clicked.connect(self.on_create_clicked)\n        publish_btn.clicked.connect(self.on_publish_clicked)\n        load_btn.clicked.connect(self.on_load_clicked)\n        inventory_btn.clicked.connect(self.on_inventory_clicked)\n        subsetm_btn.clicked.connect(self.on_subsetm_clicked)\n        libload_btn.clicked.connect(self.on_libload_clicked)\n        # rename_btn.clicked.connect(self.on_rename_clicked)\n        # set_colorspace_btn.clicked.connect(self.on_set_colorspace_clicked)\n        # reset_resolution_btn.clicked.connect(self.on_set_resolution_clicked)\n        experimental_btn.clicked.connect(self.on_experimental_clicked)\n\n    def on_save_current_clicked(self):\n        host = registered_host()\n        current_file = host.get_current_workfile()\n        if not current_file:\n            print(\"Current project is not saved. \"\n                  \"Please save once first via workfiles tool.\")\n            host_tools.show_workfiles()\n            return\n\n        print(f\"Saving current file to: {current_file}\")\n        host.save_workfile(current_file)\n\n    def on_workfile_clicked(self):\n        print(\"Clicked Workfile\")\n        host_tools.show_workfiles()\n\n    def on_create_clicked(self):\n        print(\"Clicked Create\")\n        host_tools.show_creator()\n\n    def on_publish_clicked(self):\n        print(\"Clicked Publish\")\n        host_tools.show_publish(parent=None)\n\n    def on_load_clicked(self):\n        print(\"Clicked Load\")\n        host_tools.show_loader(use_context=True)\n\n    def on_inventory_clicked(self):\n        print(\"Clicked Inventory\")\n        host_tools.show_scene_inventory()\n\n    def on_subsetm_clicked(self):\n        print(\"Clicked Subset Manager\")\n        host_tools.show_subset_manager()\n\n    def on_libload_clicked(self):\n        print(\"Clicked Library\")\n        host_tools.show_library_loader()\n\n    def on_rename_clicked(self):\n        print(\"Clicked Rename\")\n\n    def on_set_colorspace_clicked(self):\n        print(\"Clicked Set Colorspace\")\n\n    def on_set_resolution_clicked(self):\n        print(\"Clicked Set Resolution\")\n\n    def on_experimental_clicked(self):\n        host_tools.show_experimental_tools_dialog()\n\n\ndef launch_pype_menu():\n    app = QtWidgets.QApplication(sys.argv)\n\n    pype_menu = OpenPypeMenu()\n\n    stylesheet = load_stylesheet()\n    pype_menu.setStyleSheet(stylesheet)\n\n    pype_menu.show()\n\n    sys.exit(app.exec_())\n"
  },
  {
    "path": "openpype/hosts/resolve/api/menu_style.qss",
    "content": "QWidget {\n    background-color: #282828;\n    border-radius: 3;\n    font-size: 13px;\n}\n\nQComboBox {\n  border: 1px solid #090909;\n  background-color: #201f1f;\n  color: #ffffff;\n}\n\nQComboBox QAbstractItemView\n{\n    color: white;\n}\n\nQPushButton {\n    border: 1px solid #090909;\n    background-color: #201f1f;\n    color: #ffffff;\n    padding: 5;\n}\n\nQPushButton:focus {\n    background-color: \"#171717\";\n    color: #d0d0d0;\n}\n\nQPushButton:hover {\n    background-color: \"#171717\";\n    color: #e64b3d;\n}\n\nQSpinBox {\n  border: 1px solid #090909;\n  background-color: #201f1f;\n  color: #ffffff;\n  padding: 2;\n  max-width: 8em;\n  qproperty-alignment: AlignCenter;\n}\n\nQLineEdit {\n  border: 1px solid #090909;\n  border-radius: 3px;\n  background-color: #201f1f;\n  color: #ffffff;\n  padding: 2;\n  min-width: 10em;\n  qproperty-alignment: AlignCenter;\n}\n\n#OpenPypeMenu {\n    qproperty-alignment: AlignLeft;\n    min-width: 10em;\n    border: 1px solid #fef9ef;\n}\n\nQVBoxLayout {\n    background-color: #282828;\n}\n\n#Divider {\n    border: 1px solid #090909;\n    background-color: #585858;\n}\n\nQLabel {\n    color: #77776b;\n}\n"
  },
  {
    "path": "openpype/hosts/resolve/api/pipeline.py",
    "content": "\"\"\"\nBasic avalon integration\n\"\"\"\nimport os\nimport contextlib\nfrom collections import OrderedDict\n\nfrom pyblish import api as pyblish\n\nfrom openpype.lib import Logger\nfrom openpype.pipeline import (\n    schema,\n    register_loader_plugin_path,\n    register_creator_plugin_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.host import (\n    HostBase,\n    IWorkfileHost,\n    ILoadHost\n)\n\nfrom . import lib\nfrom .utils import get_resolve_module\nfrom .workio import (\n    open_file,\n    save_file,\n    file_extensions,\n    has_unsaved_changes,\n    work_root,\n    current_file\n)\n\nlog = Logger.get_logger(__name__)\n\nHOST_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))\nPLUGINS_DIR = os.path.join(HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\n\nAVALON_CONTAINERS = \":AVALON_CONTAINERS\"\n\n\nclass ResolveHost(HostBase, IWorkfileHost, ILoadHost):\n    name = \"resolve\"\n\n    def install(self):\n        \"\"\"Install resolve-specific functionality of avalon-core.\n\n        This is where you install menus and register families, data\n        and loaders into resolve.\n\n        It is called automatically when installing via `api.install(resolve)`.\n\n        See the Maya equivalent for inspiration on how to implement this.\n\n        \"\"\"\n\n        log.info(\"openpype.hosts.resolve installed\")\n\n        pyblish.register_host(self.name)\n        pyblish.register_plugin_path(PUBLISH_PATH)\n        print(\"Registering DaVinci Resolve plug-ins..\")\n\n        register_loader_plugin_path(LOAD_PATH)\n        register_creator_plugin_path(CREATE_PATH)\n\n        # register callback for switching publishable\n        pyblish.register_callback(\"instanceToggled\",\n                                  on_pyblish_instance_toggled)\n\n        get_resolve_module()\n\n    def open_workfile(self, filepath):\n        return open_file(filepath)\n\n    def save_workfile(self, filepath=None):\n        return save_file(filepath)\n\n    def work_root(self, session):\n        return work_root(session)\n\n    def get_current_workfile(self):\n        return current_file()\n\n    def workfile_has_unsaved_changes(self):\n        return has_unsaved_changes()\n\n    def get_workfile_extensions(self):\n        return file_extensions()\n\n    def get_containers(self):\n        return ls()\n\n\ndef containerise(timeline_item,\n                 name,\n                 namespace,\n                 context,\n                 loader=None,\n                 data=None):\n    \"\"\"Bundle Hiero's object into an assembly and imprint it with metadata\n\n    Containerisation enables a tracking of version, author and origin\n    for loaded assets.\n\n    Arguments:\n        timeline_item (hiero.core.TrackItem): object to imprint as container\n        name (str): Name of resulting assembly\n        namespace (str): Namespace under which to host container\n        context (dict): Asset information\n        loader (str, optional): Name of node used to produce this container.\n\n    Returns:\n        timeline_item (hiero.core.TrackItem): containerised object\n\n    \"\"\"\n\n    data_imprint = OrderedDict({\n        \"schema\": \"openpype:container-2.0\",\n        \"id\": AVALON_CONTAINER_ID,\n        \"name\": str(name),\n        \"namespace\": str(namespace),\n        \"loader\": str(loader),\n        \"representation\": str(context[\"representation\"][\"_id\"]),\n    })\n\n    if data:\n        data_imprint.update(data)\n\n    lib.set_timeline_item_pype_tag(timeline_item, data_imprint)\n\n    return timeline_item\n\n\ndef ls():\n    \"\"\"List available containers.\n\n    This function is used by the Container Manager in Nuke. You'll\n    need to implement a for-loop that then *yields* one Container at\n    a time.\n\n    See the `container.json` schema for details on how it should look,\n    and the Maya equivalent, which is in `avalon.maya.pipeline`\n    \"\"\"\n\n    # get all track items from current timeline\n    all_timeline_items = lib.get_current_timeline_items(filter=False)\n\n    for timeline_item_data in all_timeline_items:\n        timeline_item = timeline_item_data[\"clip\"][\"item\"]\n        container = parse_container(timeline_item)\n        if container:\n            yield container\n\n\ndef parse_container(timeline_item, validate=True):\n    \"\"\"Return container data from timeline_item's openpype tag.\n\n    Args:\n        timeline_item (hiero.core.TrackItem): A containerised track item.\n        validate (bool)[optional]: validating with avalon scheme\n\n    Returns:\n        dict: The container schema data for input containerized track item.\n\n    \"\"\"\n    # convert tag metadata to normal keys names\n    data = lib.get_timeline_item_pype_tag(timeline_item)\n\n    if validate and data and data.get(\"schema\"):\n        schema.validate(data)\n\n    if not isinstance(data, dict):\n        return\n\n    # If not all required data return the empty container\n    required = ['schema', 'id', 'name',\n                'namespace', 'loader', 'representation']\n\n    if not all(key in data for key in required):\n        return\n\n    container = {key: data[key] for key in required}\n\n    container[\"objectName\"] = timeline_item.GetName()\n\n    # Store reference to the node object\n    container[\"_timeline_item\"] = timeline_item\n\n    return container\n\n\ndef update_container(timeline_item, data=None):\n    \"\"\"Update container data to input timeline_item's openpype tag.\n\n    Args:\n        timeline_item (hiero.core.TrackItem): A containerised track item.\n        data (dict)[optional]: dictionery with data to be updated\n\n    Returns:\n        bool: True if container was updated correctly\n\n    \"\"\"\n    data = data or dict()\n\n    container = lib.get_timeline_item_pype_tag(timeline_item)\n\n    for _key, _value in container.items():\n        try:\n            container[_key] = data[_key]\n        except KeyError:\n            pass\n\n    log.info(\"Updating container: `{}`\".format(timeline_item))\n    return bool(lib.set_timeline_item_pype_tag(timeline_item, container))\n\n\n@contextlib.contextmanager\ndef maintained_selection():\n    \"\"\"Maintain selection during context\n\n    Example:\n        >>> with maintained_selection():\n        ...     node['selected'].setValue(True)\n        >>> print(node['selected'].value())\n        False\n    \"\"\"\n    try:\n        # do the operation\n        yield\n    finally:\n        pass\n\n\ndef reset_selection():\n    \"\"\"Deselect all selected nodes\n    \"\"\"\n    pass\n\n\ndef on_pyblish_instance_toggled(instance, old_value, new_value):\n    \"\"\"Toggle node passthrough states on instance toggles.\"\"\"\n\n    log.info(\"instance toggle: {}, old_value: {}, new_value:{} \".format(\n        instance, old_value, new_value))\n\n    from openpype.hosts.resolve.api import (\n        set_publish_attribute\n    )\n\n    # Whether instances should be passthrough based on new value\n    timeline_item = instance.data[\"item\"]\n    set_publish_attribute(timeline_item, new_value)\n\n\ndef remove_instance(instance):\n    \"\"\"Remove instance marker from track item.\"\"\"\n    instance_id = instance.get(\"uuid\")\n\n    selected_timeline_items = lib.get_current_timeline_items(\n        filter=True, selecting_color=lib.publish_clip_color)\n\n    found_ti = None\n    for timeline_item_data in selected_timeline_items:\n        timeline_item = timeline_item_data[\"clip\"][\"item\"]\n\n        # get openpype tag data\n        tag_data = lib.get_timeline_item_pype_tag(timeline_item)\n        _ti_id = tag_data.get(\"uuid\")\n        if _ti_id == instance_id:\n            found_ti = timeline_item\n            break\n\n    if found_ti is None:\n        return\n\n    # removing instance by marker color\n    print(f\"Removing instance: {found_ti.GetName()}\")\n    found_ti.DeleteMarkersByColor(lib.pype_marker_color)\n\n\ndef list_instances():\n    \"\"\"List all created instances from current workfile.\"\"\"\n    listed_instances = []\n    selected_timeline_items = lib.get_current_timeline_items(\n        filter=True, selecting_color=lib.publish_clip_color)\n\n    for timeline_item_data in selected_timeline_items:\n        timeline_item = timeline_item_data[\"clip\"][\"item\"]\n        ti_name = timeline_item.GetName().split(\".\")[0]\n\n        # get openpype tag data\n        tag_data = lib.get_timeline_item_pype_tag(timeline_item)\n\n        if tag_data:\n            asset = tag_data.get(\"asset\")\n            subset = tag_data.get(\"subset\")\n            tag_data[\"label\"] = f\"{ti_name} [{asset}-{subset}]\"\n            listed_instances.append(tag_data)\n\n    return listed_instances\n"
  },
  {
    "path": "openpype/hosts/resolve/api/plugin.py",
    "content": "import re\nimport uuid\nimport copy\n\nimport qargparse\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.settings import get_current_project_settings\nfrom openpype.pipeline import (\n    LegacyCreator,\n    LoaderPlugin,\n    Anatomy\n)\n\nfrom . import lib\nfrom .menu import load_stylesheet\n\n\nclass CreatorWidget(QtWidgets.QDialog):\n\n    # output items\n    items = {}\n\n    def __init__(self, name, info, ui_inputs, parent=None):\n        super(CreatorWidget, self).__init__(parent)\n\n        self.setObjectName(name)\n\n        self.setWindowFlags(\n            QtCore.Qt.Window\n            | QtCore.Qt.CustomizeWindowHint\n            | QtCore.Qt.WindowTitleHint\n            | QtCore.Qt.WindowCloseButtonHint\n            | QtCore.Qt.WindowStaysOnTopHint\n        )\n        self.setWindowTitle(name or \"OpenPype Creator Input\")\n        self.resize(500, 700)\n\n        # Where inputs and labels are set\n        self.content_widget = [QtWidgets.QWidget(self)]\n        top_layout = QtWidgets.QFormLayout(self.content_widget[0])\n        top_layout.setObjectName(\"ContentLayout\")\n        top_layout.addWidget(Spacer(5, self))\n\n        # first add widget tag line\n        top_layout.addWidget(QtWidgets.QLabel(info))\n\n        # main dynamic layout\n        self.scroll_area = QtWidgets.QScrollArea(self, widgetResizable=True)\n        self.scroll_area.setVerticalScrollBarPolicy(\n            QtCore.Qt.ScrollBarAsNeeded)\n        self.scroll_area.setVerticalScrollBarPolicy(\n            QtCore.Qt.ScrollBarAlwaysOn)\n        self.scroll_area.setHorizontalScrollBarPolicy(\n            QtCore.Qt.ScrollBarAlwaysOff)\n        self.scroll_area.setWidgetResizable(True)\n\n        self.content_widget.append(self.scroll_area)\n\n        scroll_widget = QtWidgets.QWidget(self)\n        in_scroll_area = QtWidgets.QVBoxLayout(scroll_widget)\n        self.content_layout = [in_scroll_area]\n\n        # add preset data into input widget layout\n        self.items = self.populate_widgets(ui_inputs)\n        self.scroll_area.setWidget(scroll_widget)\n\n        # Confirmation buttons\n        btns_widget = QtWidgets.QWidget(self)\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\")\n        btns_layout.addWidget(cancel_btn)\n\n        ok_btn = QtWidgets.QPushButton(\"Ok\")\n        btns_layout.addWidget(ok_btn)\n\n        # Main layout of the dialog\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(10, 10, 10, 10)\n        main_layout.setSpacing(0)\n\n        # adding content widget\n        for w in self.content_widget:\n            main_layout.addWidget(w)\n\n        main_layout.addWidget(btns_widget)\n\n        ok_btn.clicked.connect(self._on_ok_clicked)\n        cancel_btn.clicked.connect(self._on_cancel_clicked)\n\n        stylesheet = load_stylesheet()\n        self.setStyleSheet(stylesheet)\n\n    def _on_ok_clicked(self):\n        self.result = self.value(self.items)\n        self.close()\n\n    def _on_cancel_clicked(self):\n        self.result = None\n        self.close()\n\n    def value(self, data, new_data=None):\n        new_data = new_data or {}\n        for k, v in data.items():\n            new_data[k] = {\n                \"target\": None,\n                \"value\": None\n            }\n            if v[\"type\"] == \"dict\":\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = self.value(v[\"value\"])\n            if v[\"type\"] == \"section\":\n                new_data.pop(k)\n                new_data = self.value(v[\"value\"], new_data)\n            elif getattr(v[\"value\"], \"currentText\", None):\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = v[\"value\"].currentText()\n            elif getattr(v[\"value\"], \"isChecked\", None):\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = v[\"value\"].isChecked()\n            elif getattr(v[\"value\"], \"value\", None):\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = v[\"value\"].value()\n            elif getattr(v[\"value\"], \"text\", None):\n                new_data[k][\"target\"] = v[\"target\"]\n                new_data[k][\"value\"] = v[\"value\"].text()\n\n        return new_data\n\n    def camel_case_split(self, text):\n        matches = re.finditer(\n            '.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', text)\n        return \" \".join([str(m.group(0)).capitalize() for m in matches])\n\n    def create_row(self, layout, type, text, **kwargs):\n        # get type attribute from qwidgets\n        attr = getattr(QtWidgets, type)\n\n        # convert label text to normal capitalized text with spaces\n        label_text = self.camel_case_split(text)\n\n        # assign the new text to label widget\n        label = QtWidgets.QLabel(label_text)\n        label.setObjectName(\"LineLabel\")\n\n        # create attribute name text strip of spaces\n        attr_name = text.replace(\" \", \"\")\n\n        # create attribute and assign default values\n        setattr(\n            self,\n            attr_name,\n            attr(parent=self))\n\n        # assign the created attribute to variable\n        item = getattr(self, attr_name)\n        for func, val in kwargs.items():\n            if getattr(item, func):\n                func_attr = getattr(item, func)\n                if isinstance(val, tuple):\n                    func_attr(*val)\n                else:\n                    func_attr(val)\n\n        # add to layout\n        layout.addRow(label, item)\n\n        return item\n\n    def populate_widgets(self, data, content_layout=None):\n        \"\"\"\n        Populate widget from input dict.\n\n        Each plugin has its own set of widget rows defined in dictionary\n        each row values should have following keys: `type`, `target`,\n        `label`, `order`, `value` and optionally also `toolTip`.\n\n        Args:\n            data (dict): widget rows or organized groups defined\n                         by types `dict` or `section`\n            content_layout (QtWidgets.QFormLayout)[optional]: used when nesting\n\n        Returns:\n            dict: redefined data dict updated with created widgets\n\n        \"\"\"\n\n        content_layout = content_layout or self.content_layout[-1]\n        # fix order of process by defined order value\n        ordered_keys = list(data.keys())\n        for k, v in data.items():\n            try:\n                # try removing a key from index which should\n                # be filled with new\n                ordered_keys.pop(v[\"order\"])\n            except IndexError:\n                pass\n            # add key into correct order\n            ordered_keys.insert(v[\"order\"], k)\n\n        # process ordered\n        for k in ordered_keys:\n            v = data[k]\n            tool_tip = v.get(\"toolTip\", \"\")\n            if v[\"type\"] == \"dict\":\n                # adding spacer between sections\n                self.content_layout.append(QtWidgets.QWidget(self))\n                content_layout.addWidget(self.content_layout[-1])\n                self.content_layout[-1].setObjectName(\"sectionHeadline\")\n\n                headline = QtWidgets.QVBoxLayout(self.content_layout[-1])\n                headline.addWidget(Spacer(20, self))\n                headline.addWidget(QtWidgets.QLabel(v[\"label\"]))\n\n                # adding nested layout with label\n                self.content_layout.append(QtWidgets.QWidget(self))\n                self.content_layout[-1].setObjectName(\"sectionContent\")\n\n                nested_content_layout = QtWidgets.QFormLayout(\n                    self.content_layout[-1])\n                nested_content_layout.setObjectName(\"NestedContentLayout\")\n                content_layout.addWidget(self.content_layout[-1])\n\n                # add nested key as label\n                data[k][\"value\"] = self.populate_widgets(\n                    v[\"value\"], nested_content_layout)\n\n            if v[\"type\"] == \"section\":\n                # adding spacer between sections\n                self.content_layout.append(QtWidgets.QWidget(self))\n                content_layout.addWidget(self.content_layout[-1])\n                self.content_layout[-1].setObjectName(\"sectionHeadline\")\n\n                headline = QtWidgets.QVBoxLayout(self.content_layout[-1])\n                headline.addWidget(Spacer(20, self))\n                headline.addWidget(QtWidgets.QLabel(v[\"label\"]))\n\n                # adding nested layout with label\n                self.content_layout.append(QtWidgets.QWidget(self))\n                self.content_layout[-1].setObjectName(\"sectionContent\")\n\n                nested_content_layout = QtWidgets.QFormLayout(\n                    self.content_layout[-1])\n                nested_content_layout.setObjectName(\"NestedContentLayout\")\n                content_layout.addWidget(self.content_layout[-1])\n\n                # add nested key as label\n                data[k][\"value\"] = self.populate_widgets(\n                    v[\"value\"], nested_content_layout)\n\n            elif v[\"type\"] == \"QLineEdit\":\n                data[k][\"value\"] = self.create_row(\n                    content_layout, \"QLineEdit\", v[\"label\"],\n                    setText=v[\"value\"], setToolTip=tool_tip)\n            elif v[\"type\"] == \"QComboBox\":\n                data[k][\"value\"] = self.create_row(\n                    content_layout, \"QComboBox\", v[\"label\"],\n                    addItems=v[\"value\"], setToolTip=tool_tip)\n            elif v[\"type\"] == \"QCheckBox\":\n                data[k][\"value\"] = self.create_row(\n                    content_layout, \"QCheckBox\", v[\"label\"],\n                    setChecked=v[\"value\"], setToolTip=tool_tip)\n            elif v[\"type\"] == \"QSpinBox\":\n                data[k][\"value\"] = self.create_row(\n                    content_layout, \"QSpinBox\", v[\"label\"],\n                    setRange=(0, 99999),\n                    setValue=v[\"value\"],\n                    setToolTip=tool_tip)\n        return data\n\n\nclass Spacer(QtWidgets.QWidget):\n    def __init__(self, height, *args, **kwargs):\n        super(self.__class__, self).__init__(*args, **kwargs)\n\n        self.setFixedHeight(height)\n\n        real_spacer = QtWidgets.QWidget(self)\n        real_spacer.setObjectName(\"Spacer\")\n        real_spacer.setFixedHeight(height)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(real_spacer)\n\n        self.setLayout(layout)\n\n\nclass ClipLoader:\n\n    active_bin = None\n    data = {}\n\n    def __init__(self, loader_obj, context, **options):\n        \"\"\" Initialize object\n\n        Arguments:\n            loader_obj (openpype.pipeline.load.LoaderPlugin): plugin object\n            context (dict): loader plugin context\n            options (dict)[optional]: possible keys:\n                projectBinPath: \"path/to/binItem\"\n\n        \"\"\"\n        self.__dict__.update(loader_obj.__dict__)\n        self.context = context\n        self.active_project = lib.get_current_project()\n\n        # try to get value from options or evaluate key value for `handles`\n        self.with_handles = options.get(\"handles\") is True\n\n        # try to get value from options or evaluate key value for `load_to`\n        self.new_timeline = (\n            options.get(\"newTimeline\") or\n            options.get(\"load_to\") == \"New timeline\"\n        )\n        # try to get value from options or evaluate key value for `load_how`\n        self.sequential_load = (\n            options.get(\"sequentially\") or\n            options.get(\"load_how\") == \"Sequentially in order\"\n        )\n\n        assert self._populate_data(), str(\n            \"Cannot Load selected data, look into database \"\n            \"or call your supervisor\")\n\n        # inject asset data to representation dict\n        self._get_asset_data()\n\n        # add active components to class\n        if self.new_timeline:\n            loader_cls = loader_obj.__class__\n            if loader_cls.timeline:\n                # if multiselection is set then use options sequence\n                self.active_timeline = loader_cls.timeline\n            else:\n                # create new sequence\n                self.active_timeline = lib.get_new_timeline(\n                    \"{}_{}\".format(\n                        self.data[\"timeline_basename\"],\n                        str(uuid.uuid4())[:8]\n                    )\n                )\n                loader_cls.timeline = self.active_timeline\n\n        else:\n            self.active_timeline = lib.get_current_timeline()\n\n    def _populate_data(self):\n        \"\"\" Gets context and convert it to self.data\n        data structure:\n            {\n                \"name\": \"assetName_subsetName_representationName\"\n                \"binPath\": \"projectBinPath\",\n            }\n        \"\"\"\n        # create name\n        representation = self.context[\"representation\"]\n        representation_context = representation[\"context\"]\n        asset = str(representation_context[\"asset\"])\n        subset = str(representation_context[\"subset\"])\n        representation_name = str(representation_context[\"representation\"])\n        self.data[\"clip_name\"] = \"_\".join([\n            asset,\n            subset,\n            representation_name\n        ])\n        self.data[\"versionData\"] = self.context[\"version\"][\"data\"]\n\n        self.data[\"timeline_basename\"] = \"timeline_{}_{}\".format(\n            subset, representation_name)\n\n        # solve project bin structure path\n        hierarchy = str(\"/\".join((\n            \"Loader\",\n            representation_context[\"hierarchy\"].replace(\"\\\\\", \"/\"),\n            asset\n        )))\n\n        self.data[\"binPath\"] = hierarchy\n\n        return True\n\n    def _get_asset_data(self):\n        \"\"\" Get all available asset data\n\n        joint `data` key with asset.data dict into the representation\n\n        \"\"\"\n\n        self.data[\"assetData\"] = copy.deepcopy(self.context[\"asset\"][\"data\"])\n\n    def load(self, files):\n        \"\"\"Load clip into timeline\n\n        Arguments:\n            files (list[str]): list of files to load into timeline\n        \"\"\"\n        # create project bin for the media to be imported into\n        self.active_bin = lib.create_bin(self.data[\"binPath\"])\n\n        # create mediaItem in active project bin\n        # create clip media\n        media_pool_item = lib.create_media_pool_item(\n            files,\n            self.active_bin\n        )\n        _clip_property = media_pool_item.GetClipProperty\n        source_in = int(_clip_property(\"Start\"))\n        source_out = int(_clip_property(\"End\"))\n        source_duration = int(_clip_property(\"Frames\"))\n        # Trim clip start if slate is present\n        if \"slate\" in self.data[\"versionData\"][\"families\"]:\n            source_in += 1\n            source_duration = source_out - source_in + 1\n\n        if not self.with_handles:\n            # Load file without the handles of the source media\n            # We remove the handles from the source in and source out\n            # so that the handles are excluded in the timeline\n            handle_start = 0\n            handle_end = 0\n\n            # get version data frame data from db\n            version_data = self.data[\"versionData\"]\n            frame_start = version_data.get(\"frameStart\")\n            frame_end = version_data.get(\"frameEnd\")\n\n            # The version data usually stored the frame range + handles of the\n            # media however certain representations may be shorter because they\n            # exclude those handles intentionally. Unfortunately the\n            # representation does not store that in the database currently;\n            # so we should compensate for those cases. If the media is shorter\n            # than the frame range specified in the database we assume it is\n            # without handles and thus we do not need to remove the handles\n            # from source and out\n            if frame_start is not None and frame_end is not None:\n                # Version has frame range data, so we can compare media length\n                handle_start = version_data.get(\"handleStart\", 0)\n                handle_end = version_data.get(\"handleEnd\", 0)\n                frame_start_handle = frame_start - handle_start\n                frame_end_handle = frame_end + handle_end\n                database_frame_duration = int(\n                    frame_end_handle - frame_start_handle + 1\n                )\n                if source_duration >= database_frame_duration:\n                    source_in += handle_start\n                    source_out -= handle_end\n\n        # get timeline in\n        timeline_start = self.active_timeline.GetStartFrame()\n        if self.sequential_load:\n            # set timeline start frame\n            timeline_in = int(timeline_start)\n        else:\n            # set timeline start frame + original clip in frame\n            timeline_in = int(\n                timeline_start + self.data[\"assetData\"][\"clipIn\"])\n\n        # make track item from source in bin as item\n        timeline_item = lib.create_timeline_item(\n            media_pool_item,\n            self.active_timeline,\n            timeline_in,\n            source_in,\n            source_out,\n        )\n\n        print(\"Loading clips: `{}`\".format(self.data[\"clip_name\"]))\n        return timeline_item\n\n    def update(self, timeline_item, files):\n        # create project bin for the media to be imported into\n        self.active_bin = lib.create_bin(self.data[\"binPath\"])\n\n        # create mediaItem in active project bin\n        # create clip media\n        media_pool_item = lib.create_media_pool_item(\n            files,\n            self.active_bin\n        )\n        _clip_property = media_pool_item.GetClipProperty\n\n        # Read trimming from timeline item\n        timeline_item_in = timeline_item.GetLeftOffset()\n        timeline_item_len = timeline_item.GetDuration()\n        timeline_item_out = timeline_item_in + timeline_item_len\n\n        lib.swap_clips(\n            timeline_item,\n            media_pool_item,\n            timeline_item_in,\n            timeline_item_out\n        )\n\n        print(\"Loading clips: `{}`\".format(self.data[\"clip_name\"]))\n        return timeline_item\n\n\nclass TimelineItemLoader(LoaderPlugin):\n    \"\"\"A basic SequenceLoader for Resolve\n\n    This will implement the basic behavior for a loader to inherit from that\n    will containerize the reference and will implement the `remove` and\n    `update` logic.\n\n    \"\"\"\n\n    options = [\n        qargparse.Boolean(\n            \"handles\",\n            label=\"Include handles\",\n            default=0,\n            help=\"Load with handles or without?\"\n        ),\n        qargparse.Choice(\n            \"load_to\",\n            label=\"Where to load clips\",\n            items=[\n                \"Current timeline\",\n                \"New timeline\"\n            ],\n            default=0,\n            help=\"Where do you want clips to be loaded?\"\n        ),\n        qargparse.Choice(\n            \"load_how\",\n            label=\"How to load clips\",\n            items=[\n                \"Original timing\",\n                \"Sequentially in order\"\n            ],\n            default=\"Original timing\",\n            help=\"Would you like to place it at original timing?\"\n        )\n    ]\n\n    def load(\n        self,\n        context,\n        name=None,\n        namespace=None,\n        options=None\n    ):\n        pass\n\n    def update(self, container, representation):\n        \"\"\"Update an existing `container`\n        \"\"\"\n        pass\n\n    def remove(self, container):\n        \"\"\"Remove an existing `container`\n        \"\"\"\n        pass\n\n\nclass Creator(LegacyCreator):\n    \"\"\"Creator class wrapper\n    \"\"\"\n    marker_color = \"Purple\"\n\n    def __init__(self, *args, **kwargs):\n        super(Creator, self).__init__(*args, **kwargs)\n\n        resolve_p_settings = get_current_project_settings().get(\"resolve\")\n        self.presets = {}\n        if resolve_p_settings:\n            self.presets = resolve_p_settings[\"create\"].get(\n                self.__class__.__name__, {})\n\n        # adding basic current context resolve objects\n        self.project = lib.get_current_project()\n        self.timeline = lib.get_current_timeline()\n\n        if (self.options or {}).get(\"useSelection\"):\n            self.selected = lib.get_current_timeline_items(filter=True)\n        else:\n            self.selected = lib.get_current_timeline_items(filter=False)\n\n        self.widget = CreatorWidget\n\n\nclass PublishClip:\n    \"\"\"\n    Convert a track item to publishable instance\n\n    Args:\n        timeline_item (hiero.core.TrackItem): hiero track item object\n        kwargs (optional): additional data needed for rename=True (presets)\n\n    Returns:\n        hiero.core.TrackItem: hiero track item object with openpype tag\n    \"\"\"\n    vertical_clip_match = {}\n    tag_data = {}\n    types = {\n        \"shot\": \"shot\",\n        \"folder\": \"folder\",\n        \"episode\": \"episode\",\n        \"sequence\": \"sequence\",\n        \"track\": \"sequence\",\n    }\n\n    # parents search pattern\n    parents_search_pattern = r\"\\{([a-z]*?)\\}\"\n\n    # default templates for non-ui use\n    rename_default = False\n    hierarchy_default = \"{_folder_}/{_sequence_}/{_track_}\"\n    clip_name_default = \"shot_{_trackIndex_:0>3}_{_clipIndex_:0>4}\"\n    subset_name_default = \"<track_name>\"\n    review_track_default = \"< none >\"\n    subset_family_default = \"plate\"\n    count_from_default = 10\n    count_steps_default = 10\n    vertical_sync_default = False\n    driving_layer_default = \"\"\n\n    def __init__(self, cls, timeline_item_data, **kwargs):\n        # populate input cls attribute onto self.[attr]\n        self.__dict__.update(cls.__dict__)\n\n        # get main parent objects\n        self.timeline_item_data = timeline_item_data\n        self.timeline_item = timeline_item_data[\"clip\"][\"item\"]\n        timeline_name = timeline_item_data[\"timeline\"].GetName()\n        self.timeline_name = str(timeline_name).replace(\" \", \"_\")\n\n        # track item (clip) main attributes\n        self.ti_name = self.timeline_item.GetName()\n        self.ti_index = int(timeline_item_data[\"clip\"][\"index\"])\n\n        # get track name and index\n        track_name = timeline_item_data[\"track\"][\"name\"]\n        self.track_name = str(track_name).replace(\" \", \"_\")\n        self.track_index = int(timeline_item_data[\"track\"][\"index\"])\n\n        # adding tag.family into tag\n        if kwargs.get(\"avalon\"):\n            self.tag_data.update(kwargs[\"avalon\"])\n\n        # adding ui inputs if any\n        self.ui_inputs = kwargs.get(\"ui_inputs\", {})\n\n        # adding media pool folder if any\n        self.mp_folder = kwargs.get(\"mp_folder\")\n\n        # populate default data before we get other attributes\n        self._populate_timeline_item_default_data()\n\n        # use all populated default data to create all important attributes\n        self._populate_attributes()\n\n        # create parents with correct types\n        self._create_parents()\n\n    def convert(self):\n        # solve track item data and add them to tag data\n        self._convert_to_tag_data()\n\n        # if track name is in review track name and also if driving track name\n        # is not in review track name: skip tag creation\n        if (self.track_name in self.review_layer) and (\n                self.driving_layer not in self.review_layer):\n            return\n\n        # deal with clip name\n        new_name = self.tag_data.pop(\"newClipName\")\n\n        if self.rename:\n            self.tag_data[\"asset_name\"] = new_name\n        else:\n            self.tag_data[\"asset_name\"] = self.ti_name\n\n        # AYON unique identifier\n        folder_path = \"/{}/{}\".format(\n            self.tag_data[\"hierarchy\"],\n            self.tag_data[\"asset_name\"]\n        )\n        self.tag_data[\"folder_path\"] = folder_path\n\n        # create new name for track item\n        if not lib.pype_marker_workflow:\n            # create compound clip workflow\n            lib.create_compound_clip(\n                self.timeline_item_data,\n                self.tag_data[\"asset_name\"],\n                self.mp_folder\n            )\n\n            # add timeline_item_data selection to tag\n            self.tag_data.update({\n                \"track_data\": self.timeline_item_data[\"track\"]\n            })\n\n        # create openpype tag on timeline_item and add data\n        lib.imprint(self.timeline_item, self.tag_data)\n\n        return self.timeline_item\n\n    def _populate_timeline_item_default_data(self):\n        \"\"\" Populate default formatting data from track item. \"\"\"\n\n        self.timeline_item_default_data = {\n            \"_folder_\": \"shots\",\n            \"_sequence_\": self.timeline_name,\n            \"_track_\": self.track_name,\n            \"_clip_\": self.ti_name,\n            \"_trackIndex_\": self.track_index,\n            \"_clipIndex_\": self.ti_index\n        }\n\n    def _populate_attributes(self):\n        \"\"\" Populate main object attributes. \"\"\"\n        # track item frame range and parent track name for vertical sync check\n        self.clip_in = int(self.timeline_item.GetStart())\n        self.clip_out = int(self.timeline_item.GetEnd())\n\n        # define ui inputs if non gui mode was used\n        self.shot_num = self.ti_index\n\n        # ui_inputs data or default values if gui was not used\n        self.rename = self.ui_inputs.get(\n            \"clipRename\", {}).get(\"value\") or self.rename_default\n        self.clip_name = self.ui_inputs.get(\n            \"clipName\", {}).get(\"value\") or self.clip_name_default\n        self.hierarchy = self.ui_inputs.get(\n            \"hierarchy\", {}).get(\"value\") or self.hierarchy_default\n        self.hierarchy_data = self.ui_inputs.get(\n            \"hierarchyData\", {}).get(\"value\") or \\\n            self.timeline_item_default_data.copy()\n        self.count_from = self.ui_inputs.get(\n            \"countFrom\", {}).get(\"value\") or self.count_from_default\n        self.count_steps = self.ui_inputs.get(\n            \"countSteps\", {}).get(\"value\") or self.count_steps_default\n        self.subset_name = self.ui_inputs.get(\n            \"subsetName\", {}).get(\"value\") or self.subset_name_default\n        self.subset_family = self.ui_inputs.get(\n            \"subsetFamily\", {}).get(\"value\") or self.subset_family_default\n        self.vertical_sync = self.ui_inputs.get(\n            \"vSyncOn\", {}).get(\"value\") or self.vertical_sync_default\n        self.driving_layer = self.ui_inputs.get(\n            \"vSyncTrack\", {}).get(\"value\") or self.driving_layer_default\n        self.review_track = self.ui_inputs.get(\n            \"reviewTrack\", {}).get(\"value\") or self.review_track_default\n\n        # build subset name from layer name\n        if self.subset_name == \"<track_name>\":\n            self.subset_name = self.track_name\n\n        # create subset for publishing\n        self.subset = self.subset_family + self.subset_name.capitalize()\n\n    def _replace_hash_to_expression(self, name, text):\n        \"\"\" Replace hash with number in correct padding. \"\"\"\n        _spl = text.split(\"#\")\n        _len = (len(_spl) - 1)\n        _repl = \"{{{0}:0>{1}}}\".format(name, _len)\n        new_text = text.replace((\"#\" * _len), _repl)\n        return new_text\n\n    def _convert_to_tag_data(self):\n        \"\"\" Convert internal data to tag data.\n\n        Populating the tag data into internal variable self.tag_data\n        \"\"\"\n        # define vertical sync attributes\n        hero_track = True\n        self.review_layer = \"\"\n        if self.vertical_sync:\n            # check if track name is not in driving layer\n            if self.track_name not in self.driving_layer:\n                # if it is not then define vertical sync as None\n                hero_track = False\n\n        # increasing steps by index of rename iteration\n        self.count_steps *= self.rename_index\n\n        hierarchy_formatting_data = {}\n        _data = self.timeline_item_default_data.copy()\n        if self.ui_inputs:\n            # adding tag metadata from ui\n            for _k, _v in self.ui_inputs.items():\n                if _v[\"target\"] == \"tag\":\n                    self.tag_data[_k] = _v[\"value\"]\n\n            # driving layer is set as positive match\n            if hero_track or self.vertical_sync:\n                # mark review layer\n                if self.review_track and (\n                        self.review_track not in self.review_track_default):\n                    # if review layer is defined and not the same as default\n                    self.review_layer = self.review_track\n                # shot num calculate\n                if self.rename_index == 0:\n                    self.shot_num = self.count_from\n                else:\n                    self.shot_num = self.count_from + self.count_steps\n\n            # clip name sequence number\n            _data.update({\"shot\": self.shot_num})\n\n            # solve # in test to pythonic expression\n            for _k, _v in self.hierarchy_data.items():\n                if \"#\" not in _v[\"value\"]:\n                    continue\n                self.hierarchy_data[\n                    _k][\"value\"] = self._replace_hash_to_expression(\n                        _k, _v[\"value\"])\n\n            # fill up pythonic expresisons in hierarchy data\n            for k, _v in self.hierarchy_data.items():\n                hierarchy_formatting_data[k] = _v[\"value\"].format(**_data)\n        else:\n            # if no gui mode then just pass default data\n            hierarchy_formatting_data = self.hierarchy_data\n\n        tag_hierarchy_data = self._solve_tag_hierarchy_data(\n            hierarchy_formatting_data\n        )\n\n        tag_hierarchy_data.update({\"heroTrack\": True})\n        if hero_track and self.vertical_sync:\n            self.vertical_clip_match.update({\n                (self.clip_in, self.clip_out): tag_hierarchy_data\n            })\n\n        if not hero_track and self.vertical_sync:\n            # driving layer is set as negative match\n            for (_in, _out), hero_data in self.vertical_clip_match.items():\n                hero_data.update({\"heroTrack\": False})\n                if _in == self.clip_in and _out == self.clip_out:\n                    data_subset = hero_data[\"subset\"]\n                    # add track index in case duplicity of names in hero data\n                    if self.subset in data_subset:\n                        hero_data[\"subset\"] = self.subset + str(\n                            self.track_index)\n                    # in case track name and subset name is the same then add\n                    if self.subset_name == self.track_name:\n                        hero_data[\"subset\"] = self.subset\n                    # assign data to return hierarchy data to tag\n                    tag_hierarchy_data = hero_data\n\n        # add data to return data dict\n        self.tag_data.update(tag_hierarchy_data)\n\n        # add uuid to tag data\n        self.tag_data[\"uuid\"] = str(uuid.uuid4())\n\n        # add review track only to hero track\n        if hero_track and self.review_layer:\n            self.tag_data.update({\"reviewTrack\": self.review_layer})\n        else:\n            self.tag_data.update({\"reviewTrack\": None})\n\n    def _solve_tag_hierarchy_data(self, hierarchy_formatting_data):\n        \"\"\" Solve tag data from hierarchy data and templates. \"\"\"\n        # fill up clip name and hierarchy keys\n        hierarchy_filled = self.hierarchy.format(**hierarchy_formatting_data)\n        clip_name_filled = self.clip_name.format(**hierarchy_formatting_data)\n\n        return {\n            \"newClipName\": clip_name_filled,\n            \"hierarchy\": hierarchy_filled,\n            \"parents\": self.parents,\n            \"hierarchyData\": hierarchy_formatting_data,\n            \"subset\": self.subset,\n            \"family\": self.subset_family\n        }\n\n    def _convert_to_entity(self, key):\n        \"\"\" Converting input key to key with type. \"\"\"\n        # convert to entity type\n        entity_type = self.types.get(key)\n\n        assert entity_type, \"Missing entity type for `{}`\".format(\n            key\n        )\n\n        return {\n            \"entity_type\": entity_type,\n            \"entity_name\": self.hierarchy_data[key][\"value\"].format(\n                **self.timeline_item_default_data\n            )\n        }\n\n    def _create_parents(self):\n        \"\"\" Create parents and return it in list. \"\"\"\n        self.parents = []\n\n        pattern = re.compile(self.parents_search_pattern)\n        par_split = [pattern.findall(t).pop()\n                     for t in self.hierarchy.split(\"/\")]\n\n        for key in par_split:\n            parent = self._convert_to_entity(key)\n            self.parents.append(parent)\n\n\ndef get_representation_files(representation):\n    anatomy = Anatomy()\n    files = []\n    for file_data in representation[\"files\"]:\n        path = anatomy.fill_root(file_data[\"path\"])\n        files.append(path)\n    return files\n"
  },
  {
    "path": "openpype/hosts/resolve/api/testing_utils.py",
    "content": "#! python3\n\n\nclass TestGUI:\n    def __init__(self):\n        resolve = bmd.scriptapp(\"Resolve\")  # noqa\n        self.fu = resolve.Fusion()\n        ui = self.fu.UIManager\n        self.disp = bmd.UIDispatcher(self.fu.UIManager)  # noqa\n        self.title_font = ui.Font({\"PixelSize\": 18})\n        self._dialogue = self.disp.AddWindow(\n            {\n                \"WindowTitle\": \"Get Testing folder\",\n                \"ID\": \"TestingWin\",\n                \"Geometry\": [250, 250, 250, 100],\n                \"Spacing\": 0,\n                \"Margin\": 10\n            },\n            [\n                ui.VGroup(\n                    {\n                        \"Spacing\": 2\n                    },\n                    [\n                        ui.Button(\n                            {\n                                \"ID\": \"inputTestSourcesFolder\",\n                                \"Text\": \"Select folder with testing media\",\n                                \"Weight\": 1.25,\n                                \"ToolTip\": (\n                                    \"Chose folder with videos, sequences, \"\n                                    \"single images, nested folders with \"\n                                    \"media\"\n                                ),\n                                \"Flat\": False\n                            }\n                        ),\n                        ui.VGap(),\n                        ui.Button(\n                            {\n                                \"ID\": \"openButton\",\n                                \"Text\": \"Process Test\",\n                                \"Weight\": 2,\n                                \"ToolTip\": \"Run the test...\",\n                                \"Flat\": False\n                            }\n                        )\n                    ]\n                )\n            ]\n        )\n        self._widgets = self._dialogue.GetItems()\n        self._dialogue.On.TestingWin.Close = self._close_window\n        self._dialogue.On.inputTestSourcesFolder.Clicked = self._open_dir_button_pressed  # noqa\n        self._dialogue.On.openButton.Clicked = self.process\n\n    def _close_window(self, event):\n        self.disp.ExitLoop()\n\n    def process(self, event):\n        # placeholder function this supposed to be run from child class\n        pass\n\n    def _open_dir_button_pressed(self, event):\n        # placeholder function this supposed to be run from child class\n        pass\n\n    def show_gui(self):\n        self._dialogue.Show()\n        self.disp.RunLoop()\n        self._dialogue.Hide()\n"
  },
  {
    "path": "openpype/hosts/resolve/api/todo-rendering.py",
    "content": "#!/usr/bin/env python\n# TODO: convert this script to be usable with OpenPype\n\"\"\"\nExample DaVinci Resolve script:\nLoad a still from DRX file, apply the still to all clips in all timelines.\nSet render format and codec, add render jobs for all timelines, render\nto specified path and wait for rendering completion.\nOnce render is complete, delete all jobs\n\"\"\"\n# clonned from: https://github.com/survos/transcribe/blob/fe3cf51eb95b82dabcf21fbe5f89bfb3d8bb6ce2/python/3_grade_and_render_all_timelines.py  # noqa\n\nfrom python_get_resolve import GetResolve\nimport sys\nimport time\n\n\ndef AddTimelineToRender(project, timeline, presetName,\n                        targetDirectory, renderFormat, renderCodec):\n    project.SetCurrentTimeline(timeline)\n    project.LoadRenderPreset(presetName)\n\n    if not project.SetCurrentRenderFormatAndCodec(renderFormat, renderCodec):\n        return False\n\n    project.SetRenderSettings(\n        {\"SelectAllFrames\": 1, \"TargetDir\": targetDirectory})\n    return project.AddRenderJob()\n\n\ndef RenderAllTimelines(resolve, presetName, targetDirectory,\n                       renderFormat, renderCodec):\n    projectManager = resolve.GetProjectManager()\n    project = projectManager.GetCurrentProject()\n    if not project:\n        return False\n\n    resolve.OpenPage(\"Deliver\")\n    timelineCount = project.GetTimelineCount()\n\n    for index in range(0, int(timelineCount)):\n        if not AddTimelineToRender(\n                project,\n                project.GetTimelineByIndex(index + 1),\n                presetName,\n                targetDirectory,\n                renderFormat,\n                renderCodec):\n            return False\n    return project.StartRendering()\n\n\ndef IsRenderingInProgress(resolve):\n    projectManager = resolve.GetProjectManager()\n    project = projectManager.GetCurrentProject()\n    if not project:\n        return False\n\n    return project.IsRenderingInProgress()\n\n\ndef WaitForRenderingCompletion(resolve):\n    while IsRenderingInProgress(resolve):\n        time.sleep(1)\n    return\n\n\ndef ApplyDRXToAllTimelineClips(timeline, path, gradeMode=0):\n    trackCount = timeline.GetTrackCount(\"video\")\n\n    clips = {}\n    for index in range(1, int(trackCount) + 1):\n        clips.update(timeline.GetItemsInTrack(\"video\", index))\n    return timeline.ApplyGradeFromDRX(path, int(gradeMode), clips)\n\n\ndef ApplyDRXToAllTimelines(resolve, path, gradeMode=0):\n    projectManager = resolve.GetProjectManager()\n    project = projectManager.GetCurrentProject()\n    if not project:\n        return False\n    timelineCount = project.GetTimelineCount()\n\n    for index in range(0, int(timelineCount)):\n        timeline = project.GetTimelineByIndex(index + 1)\n        project.SetCurrentTimeline(timeline)\n        if not ApplyDRXToAllTimelineClips(timeline, path, gradeMode):\n            return False\n    return True\n\n\ndef DeleteAllRenderJobs(resolve):\n    projectManager = resolve.GetProjectManager()\n    project = projectManager.GetCurrentProject()\n    project.DeleteAllRenderJobs()\n    return\n\n\n# Inputs:\n# - DRX file to import grade still and apply it for clips\n# - grade mode (0, 1 or 2)\n# - preset name for rendering\n# - render path\n# - render format\n# - render codec\nif len(sys.argv) < 7:\n    print(\n        \"input parameters for scripts are [drx file path] [grade mode] \"\n        \"[render preset name] [render path] [render format] [render codec]\")\n    sys.exit()\n\ndrxPath = sys.argv[1]\ngradeMode = sys.argv[2]\nrenderPresetName = sys.argv[3]\nrenderPath = sys.argv[4]\nrenderFormat = sys.argv[5]\nrenderCodec = sys.argv[6]\n\n# Get currently open project\nresolve = GetResolve()\n\nif not ApplyDRXToAllTimelines(resolve, drxPath, gradeMode):\n    print(\"Unable to apply a still from drx file to all timelines\")\n    sys.exit()\n\nif not RenderAllTimelines(resolve, renderPresetName, renderPath,\n                          renderFormat, renderCodec):\n    print(\"Unable to set all timelines for rendering\")\n    sys.exit()\n\nWaitForRenderingCompletion(resolve)\n\nDeleteAllRenderJobs(resolve)\n\nprint(\"Rendering is completed.\")\n"
  },
  {
    "path": "openpype/hosts/resolve/api/utils.py",
    "content": "#! python3\n\n\"\"\"\nResolve's tools for setting environment\n\"\"\"\n\nimport os\nimport sys\n\nfrom openpype.lib import Logger\n\nlog = Logger.get_logger(__name__)\n\n\ndef get_resolve_module():\n    from openpype.hosts.resolve import api\n    # dont run if already loaded\n    if api.bmdvr:\n        log.info((\"resolve module is assigned to \"\n                  f\"`openpype.hosts.resolve.api.bmdvr`: {api.bmdvr}\"))\n        return api.bmdvr\n    try:\n        \"\"\"\n        The PYTHONPATH needs to be set correctly for this import\n        statement to work. An alternative is to import the\n        DaVinciResolveScript by specifying absolute path\n        (see ExceptionHandler logic)\n        \"\"\"\n        import DaVinciResolveScript as bmd\n    except ImportError:\n        if sys.platform.startswith(\"darwin\"):\n            expected_path = (\"/Library/Application Support/Blackmagic Design\"\n                             \"/DaVinci Resolve/Developer/Scripting/Modules\")\n        elif sys.platform.startswith(\"win\") \\\n                or sys.platform.startswith(\"cygwin\"):\n            expected_path = os.path.normpath(\n                os.getenv('PROGRAMDATA') + (\n                    \"/Blackmagic Design/DaVinci Resolve/Support/Developer\"\n                    \"/Scripting/Modules\"\n                )\n            )\n        elif sys.platform.startswith(\"linux\"):\n            expected_path = \"/opt/resolve/libs/Fusion/Modules\"\n        else:\n            raise NotImplementedError(\n                \"Unsupported platform: {}\".format(sys.platform)\n            )\n\n        # check if the default path has it...\n        print((\"Unable to find module DaVinciResolveScript from \"\n               \"$PYTHONPATH - trying default locations\"))\n\n        module_path = os.path.normpath(\n            os.path.join(\n                expected_path,\n                \"DaVinciResolveScript.py\"\n            )\n        )\n\n        try:\n            import imp\n            bmd = imp.load_source('DaVinciResolveScript', module_path)\n        except ImportError:\n            # No fallbacks ... report error:\n            log.error(\n                (\"Unable to find module DaVinciResolveScript - please \"\n                 \"ensure that the module DaVinciResolveScript is \"\n                 \"discoverable by python\")\n            )\n            log.error(\n                (\"For a default DaVinci Resolve installation, the \"\n                 f\"module is expected to be located in: {expected_path}\")\n            )\n            sys.exit()\n    # assign global var and return\n    bmdvr = bmd.scriptapp(\"Resolve\")\n    bmdvf = bmd.scriptapp(\"Fusion\")\n    api.bmdvr = bmdvr\n    api.bmdvf = bmdvf\n    log.info((\"Assigning resolve module to \"\n              f\"`openpype.hosts.resolve.api.bmdvr`: {api.bmdvr}\"))\n    log.info((\"Assigning resolve module to \"\n              f\"`openpype.hosts.resolve.api.bmdvf`: {api.bmdvf}\"))\n"
  },
  {
    "path": "openpype/hosts/resolve/api/workio.py",
    "content": "\"\"\"Host API required Work Files tool\"\"\"\n\nimport os\nfrom openpype.lib import Logger\nfrom .lib import (\n    get_project_manager,\n    get_current_project\n)\n\n\nlog = Logger.get_logger(__name__)\n\n\ndef file_extensions():\n    return [\".drp\"]\n\n\ndef has_unsaved_changes():\n    get_project_manager().SaveProject()\n    return False\n\n\ndef save_file(filepath):\n    pm = get_project_manager()\n    file = os.path.basename(filepath)\n    fname, _ = os.path.splitext(file)\n    project = get_current_project()\n    name = project.GetName()\n\n    response = False\n    if name == \"Untitled Project\":\n        response = pm.CreateProject(fname)\n        log.info(\"New project created: {}\".format(response))\n        pm.SaveProject()\n    elif name != fname:\n        response = project.SetName(fname)\n        log.info(\"Project renamed: {}\".format(response))\n\n    exported = pm.ExportProject(fname, filepath)\n    log.info(\"Project exported: {}\".format(exported))\n\n\ndef open_file(filepath):\n    \"\"\"\n    Loading project\n    \"\"\"\n\n    from . import bmdvr\n\n    pm = get_project_manager()\n    page = bmdvr.GetCurrentPage()\n    if page is not None:\n        # Save current project only if Resolve has an active page, otherwise\n        # we consider Resolve being in a pre-launch state (no open UI yet)\n        project = pm.GetCurrentProject()\n        print(f\"Saving current project: {project}\")\n        pm.SaveProject()\n\n    file = os.path.basename(filepath)\n    fname, _ = os.path.splitext(file)\n\n    try:\n        # load project from input path\n        project = pm.LoadProject(fname)\n        log.info(f\"Project {project.GetName()} opened...\")\n\n    except AttributeError:\n        log.warning((f\"Project with name `{fname}` does not exist! It will \"\n                     f\"be imported from {filepath} and then loaded...\"))\n        if pm.ImportProject(filepath):\n            # load project from input path\n            project = pm.LoadProject(fname)\n            log.info(f\"Project imported/loaded {project.GetName()}...\")\n            return True\n        return False\n    return True\n\n\ndef current_file():\n    pm = get_project_manager()\n    file_ext = file_extensions()[0]\n    workdir_path = os.getenv(\"AVALON_WORKDIR\")\n    project = pm.GetCurrentProject()\n    project_name = project.GetName()\n    file_name = project_name + file_ext\n\n    # create current file path\n    current_file_path = os.path.join(workdir_path, file_name)\n\n    # return current file path if it exists\n    if os.path.exists(current_file_path):\n        return os.path.normpath(current_file_path)\n\n\ndef work_root(session):\n    return os.path.normpath(session[\"AVALON_WORKDIR\"]).replace(\"\\\\\", \"/\")\n"
  },
  {
    "path": "openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py",
    "content": "import os\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass PreLaunchResolveLastWorkfile(PreLaunchHook):\n    \"\"\"Special hook to open last workfile for Resolve.\n\n    Checks 'start_last_workfile', if set to False, it will not open last\n    workfile. This property is set explicitly in Launcher.\n    \"\"\"\n    order = 10\n    app_groups = {\"resolve\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        if not self.data.get(\"start_last_workfile\"):\n            self.log.info(\"It is set to not start last workfile on start.\")\n            return\n\n        last_workfile = self.data.get(\"last_workfile_path\")\n        if not last_workfile:\n            self.log.warning(\"Last workfile was not collected.\")\n            return\n\n        if not os.path.exists(last_workfile):\n            self.log.info(\"Current context does not have any workfile yet.\")\n            return\n\n        # Add path to launch environment for the startup script to pick up\n        self.log.info(\n            \"Setting OPENPYPE_RESOLVE_OPEN_ON_LAUNCH to launch \"\n            f\"last workfile: {last_workfile}\"\n        )\n        key = \"OPENPYPE_RESOLVE_OPEN_ON_LAUNCH\"\n        self.launch_context.env[key] = last_workfile\n"
  },
  {
    "path": "openpype/hosts/resolve/hooks/pre_resolve_setup.py",
    "content": "import os\nfrom pathlib import Path\nimport platform\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\nfrom openpype.hosts.resolve.utils import setup\n\n\nclass PreLaunchResolveSetup(PreLaunchHook):\n    \"\"\"\n    This hook will set up the Resolve scripting environment as described in\n    Resolve's documentation found with the installed application at\n    {resolve}/Support/Developer/Scripting/README.txt\n\n    Prepares the following environment variables:\n    - `RESOLVE_SCRIPT_API`\n    - `RESOLVE_SCRIPT_LIB`\n\n    It adds $RESOLVE_SCRIPT_API/Modules to PYTHONPATH.\n\n    Additionally it sets up the Python home for Python 3 based on the\n    RESOLVE_PYTHON3_HOME in the environment (usually defined in OpenPype's\n    Application environment for Resolve by the admin). For this it sets\n    PYTHONHOME and PATH variables.\n\n    It also defines:\n    - `RESOLVE_UTILITY_SCRIPTS_DIR`: Destination directory for OpenPype\n        Fusion scripts to be copied to for Resolve to pick them up.\n    - `OPENPYPE_LOG_NO_COLORS` to True to ensure OP doesn't try to\n        use logging with terminal colors as it fails in Resolve.\n\n    \"\"\"\n\n    app_groups = {\"resolve\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        current_platform = platform.system().lower()\n\n        programdata = self.launch_context.env.get(\"PROGRAMDATA\", \"\")\n        resolve_script_api_locations = {\n            \"windows\": (\n                f\"{programdata}/Blackmagic Design/\"\n                \"DaVinci Resolve/Support/Developer/Scripting\"\n            ),\n            \"darwin\": (\n                \"/Library/Application Support/Blackmagic Design\"\n                \"/DaVinci Resolve/Developer/Scripting\"\n            ),\n            \"linux\": \"/opt/resolve/Developer/Scripting\",\n        }\n        resolve_script_api = Path(\n            resolve_script_api_locations[current_platform]\n        )\n        self.log.info(\n            f\"setting RESOLVE_SCRIPT_API variable to {resolve_script_api}\"\n        )\n        self.launch_context.env[\n            \"RESOLVE_SCRIPT_API\"\n        ] = resolve_script_api.as_posix()\n\n        resolve_script_lib_dirs = {\n            \"windows\": (\n                \"C:/Program Files/Blackmagic Design\"\n                \"/DaVinci Resolve/fusionscript.dll\"\n            ),\n            \"darwin\": (\n                \"/Applications/DaVinci Resolve/DaVinci Resolve.app\"\n                \"/Contents/Libraries/Fusion/fusionscript.so\"\n            ),\n            \"linux\": \"/opt/resolve/libs/Fusion/fusionscript.so\",\n        }\n        resolve_script_lib = Path(resolve_script_lib_dirs[current_platform])\n        self.launch_context.env[\n            \"RESOLVE_SCRIPT_LIB\"\n        ] = resolve_script_lib.as_posix()\n        self.log.info(\n            f\"setting RESOLVE_SCRIPT_LIB variable to {resolve_script_lib}\"\n        )\n\n        # TODO: add OTIO installation from `openpype/requirements.py`\n        # making sure python <3.9.* is installed at provided path\n        python3_home = Path(\n            self.launch_context.env.get(\"RESOLVE_PYTHON3_HOME\", \"\")\n        )\n\n        assert python3_home.is_dir(), (\n            \"Python 3 is not installed at the provided folder path. Either \"\n            \"make sure the `environments\\resolve.json` is having correctly \"\n            \"set `RESOLVE_PYTHON3_HOME` or make sure Python 3 is installed \"\n            f\"in given path. \\nRESOLVE_PYTHON3_HOME: `{python3_home}`\"\n        )\n        python3_home_str = python3_home.as_posix()\n        self.launch_context.env[\"PYTHONHOME\"] = python3_home_str\n        self.log.info(f\"Path to Resolve Python folder: `{python3_home_str}`\")\n\n        # add to the PYTHONPATH\n        env_pythonpath = self.launch_context.env[\"PYTHONPATH\"]\n        modules_path = Path(resolve_script_api, \"Modules\").as_posix()\n        self.launch_context.env[\n            \"PYTHONPATH\"\n        ] = f\"{modules_path}{os.pathsep}{env_pythonpath}\"\n\n        self.log.debug(f\"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}\")\n\n        # add the pythonhome folder to PATH because on Windows\n        # this is needed for Py3 to be correctly detected within Resolve\n        env_path = self.launch_context.env[\"PATH\"]\n        self.log.info(f\"Adding `{python3_home_str}` to the PATH variable\")\n        self.launch_context.env[\n            \"PATH\"\n        ] = f\"{python3_home_str}{os.pathsep}{env_path}\"\n\n        self.log.debug(f\"PATH: {self.launch_context.env['PATH']}\")\n\n        resolve_utility_scripts_dirs = {\n            \"windows\": (\n                f\"{programdata}/Blackmagic Design\"\n                \"/DaVinci Resolve/Fusion/Scripts/Comp\"\n            ),\n            \"darwin\": (\n                \"/Library/Application Support/Blackmagic Design\"\n                \"/DaVinci Resolve/Fusion/Scripts/Comp\"\n            ),\n            \"linux\": \"/opt/resolve/Fusion/Scripts/Comp\",\n        }\n        resolve_utility_scripts_dir = Path(\n            resolve_utility_scripts_dirs[current_platform]\n        )\n        # setting utility scripts dir for scripts syncing\n        self.launch_context.env[\n            \"RESOLVE_UTILITY_SCRIPTS_DIR\"\n        ] = resolve_utility_scripts_dir.as_posix()\n\n        # remove terminal coloring tags\n        self.launch_context.env[\"OPENPYPE_LOG_NO_COLORS\"] = \"True\"\n\n        # Resolve Setup integration\n        setup(self.launch_context.env)\n"
  },
  {
    "path": "openpype/hosts/resolve/hooks/pre_resolve_startup.py",
    "content": "import os\n\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\nimport openpype.hosts.resolve\n\n\nclass PreLaunchResolveStartup(PreLaunchHook):\n    \"\"\"Special hook to configure startup script.\n\n    \"\"\"\n    order = 11\n    app_groups = {\"resolve\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        # Set the openpype prelaunch startup script path for easy access\n        # in the LUA .scriptlib code\n        op_resolve_root = os.path.dirname(openpype.hosts.resolve.__file__)\n        script_path = os.path.join(op_resolve_root, \"startup.py\")\n        key = \"OPENPYPE_RESOLVE_STARTUP_SCRIPT\"\n        self.launch_context.env[key] = script_path\n\n        self.log.info(\n            f\"Setting OPENPYPE_RESOLVE_STARTUP_SCRIPT to: {script_path}\"\n        )\n"
  },
  {
    "path": "openpype/hosts/resolve/otio/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/resolve/otio/davinci_export.py",
    "content": "\"\"\" compatibility OpenTimelineIO 0.12.0 and older\n\"\"\"\n\nimport os\nimport re\nimport sys\nimport json\nimport opentimelineio as otio\nfrom . import utils\nimport clique\n\nself = sys.modules[__name__]\nself.track_types = {\n    \"video\": otio.schema.TrackKind.Video,\n    \"audio\": otio.schema.TrackKind.Audio\n}\nself.project_fps = None\n\n\ndef create_otio_rational_time(frame, fps):\n    return otio.opentime.RationalTime(\n        float(frame),\n        float(fps)\n    )\n\n\ndef create_otio_time_range(start_frame, frame_duration, fps):\n    return otio.opentime.TimeRange(\n        start_time=create_otio_rational_time(start_frame, fps),\n        duration=create_otio_rational_time(frame_duration, fps)\n    )\n\n\ndef create_otio_reference(media_pool_item):\n    metadata = _get_metadata_media_pool_item(media_pool_item)\n    print(\"media pool item: {}\".format(media_pool_item.GetName()))\n\n    _mp_clip_property = media_pool_item.GetClipProperty\n\n    path = _mp_clip_property(\"File Path\")\n    reformat_path = utils.get_reformated_path(path, padded=True)\n    padding = utils.get_padding_from_path(path)\n\n    if padding:\n        metadata.update({\n            \"isSequence\": True,\n            \"padding\": padding\n        })\n\n    # get clip property regarding to type\n    fps = float(_mp_clip_property(\"FPS\"))\n    if _mp_clip_property(\"Type\") == \"Video\":\n        frame_start = int(_mp_clip_property(\"Start\"))\n        frame_duration = int(_mp_clip_property(\"Frames\"))\n    else:\n        audio_duration = str(_mp_clip_property(\"Duration\"))\n        frame_start = 0\n        frame_duration = int(utils.timecode_to_frames(\n            audio_duration, float(fps)))\n\n    otio_ex_ref_item = None\n\n    if padding:\n        # if it is file sequence try to create `ImageSequenceReference`\n        # the OTIO might not be compatible so return nothing and do it old way\n        try:\n            dirname, filename = os.path.split(path)\n            collection = clique.parse(filename, '{head}[{ranges}]{tail}')\n            padding_num = len(re.findall(\"(\\\\d+)(?=-)\", filename).pop())\n            otio_ex_ref_item = otio.schema.ImageSequenceReference(\n                target_url_base=dirname + os.sep,\n                name_prefix=collection.format(\"{head}\"),\n                name_suffix=collection.format(\"{tail}\"),\n                start_frame=frame_start,\n                frame_zero_padding=padding_num,\n                rate=fps,\n                available_range=create_otio_time_range(\n                    frame_start,\n                    frame_duration,\n                    fps\n                )\n            )\n        except AttributeError:\n            pass\n\n    if not otio_ex_ref_item:\n        # in case old OTIO or video file create `ExternalReference`\n        otio_ex_ref_item = otio.schema.ExternalReference(\n            target_url=reformat_path,\n            available_range=create_otio_time_range(\n                frame_start,\n                frame_duration,\n                fps\n            )\n        )\n\n    # add metadata to otio item\n    add_otio_metadata(otio_ex_ref_item, media_pool_item, **metadata)\n\n    return otio_ex_ref_item\n\n\ndef create_otio_markers(track_item, fps):\n    track_item_markers = track_item.GetMarkers()\n    markers = []\n    for marker_frame in track_item_markers:\n        note = track_item_markers[marker_frame][\"note\"]\n        if \"{\" in note and \"}\" in note:\n            metadata = json.loads(note)\n        else:\n            metadata = {\"note\": note}\n        markers.append(\n            otio.schema.Marker(\n                name=track_item_markers[marker_frame][\"name\"],\n                marked_range=create_otio_time_range(\n                    marker_frame,\n                    track_item_markers[marker_frame][\"duration\"],\n                    fps\n                ),\n                color=track_item_markers[marker_frame][\"color\"].upper(),\n                metadata=metadata\n            )\n        )\n    return markers\n\n\ndef create_otio_clip(track_item):\n    media_pool_item = track_item.GetMediaPoolItem()\n    _mp_clip_property = media_pool_item.GetClipProperty\n\n    if not self.project_fps:\n        fps = float(_mp_clip_property(\"FPS\"))\n    else:\n        fps = self.project_fps\n\n    name = track_item.GetName()\n\n    media_reference = create_otio_reference(media_pool_item)\n    source_range = create_otio_time_range(\n        int(track_item.GetLeftOffset()),\n        int(track_item.GetDuration()),\n        fps\n    )\n\n    if _mp_clip_property(\"Type\") == \"Audio\":\n        return_clips = list()\n        audio_chanels = _mp_clip_property(\"Audio Ch\")\n        for channel in range(0, int(audio_chanels)):\n            clip = otio.schema.Clip(\n                name=f\"{name}_{channel}\",\n                source_range=source_range,\n                media_reference=media_reference\n            )\n            for marker in create_otio_markers(track_item, fps):\n                clip.markers.append(marker)\n            return_clips.append(clip)\n        return return_clips\n    else:\n        clip = otio.schema.Clip(\n            name=name,\n            source_range=source_range,\n            media_reference=media_reference\n        )\n        for marker in create_otio_markers(track_item, fps):\n            clip.markers.append(marker)\n\n        return clip\n\n\ndef create_otio_gap(gap_start, clip_start, tl_start_frame, fps):\n    return otio.schema.Gap(\n        source_range=create_otio_time_range(\n            gap_start,\n            (clip_start - tl_start_frame) - gap_start,\n            fps\n        )\n    )\n\n\ndef _create_otio_timeline(project, timeline, fps):\n    metadata = _get_timeline_metadata(project, timeline)\n    start_time = create_otio_rational_time(\n        timeline.GetStartFrame(), fps)\n    otio_timeline = otio.schema.Timeline(\n        name=timeline.GetName(),\n        global_start_time=start_time,\n        metadata=metadata\n    )\n    return otio_timeline\n\n\ndef _get_timeline_metadata(project, timeline):\n    media_pool = project.GetMediaPool()\n    root_folder = media_pool.GetRootFolder()\n    ls_folder = root_folder.GetClipList()\n    timeline = project.GetCurrentTimeline()\n    timeline_name = timeline.GetName()\n    for tl in ls_folder:\n        if tl.GetName() not in timeline_name:\n            continue\n        return _get_metadata_media_pool_item(tl)\n\n\ndef _get_metadata_media_pool_item(media_pool_item):\n    data = dict()\n    data.update({k: v for k, v in media_pool_item.GetMetadata().items()})\n    property = media_pool_item.GetClipProperty() or {}\n    for name, value in property.items():\n        if \"Resolution\" in name and \"\" != value:\n            width, height = value.split(\"x\")\n            data.update({\n                \"width\": int(width),\n                \"height\": int(height)\n            })\n        if \"PAR\" in name and \"\" != value:\n            try:\n                data.update({\"pixelAspect\": float(value)})\n            except ValueError:\n                if \"Square\" in value:\n                    data.update({\"pixelAspect\": float(1)})\n                else:\n                    data.update({\"pixelAspect\": float(1)})\n\n    return data\n\n\ndef create_otio_track(track_type, track_name):\n    return otio.schema.Track(\n        name=track_name,\n        kind=self.track_types[track_type]\n    )\n\n\ndef add_otio_gap(clip_start, otio_track, track_item, timeline):\n    # if gap between track start and clip start\n    if clip_start > otio_track.available_range().duration.value:\n        # create gap and add it to track\n        otio_track.append(\n            create_otio_gap(\n                otio_track.available_range().duration.value,\n                track_item.GetStart(),\n                timeline.GetStartFrame(),\n                self.project_fps\n            )\n        )\n\n\ndef add_otio_metadata(otio_item, media_pool_item, **kwargs):\n    mp_metadata = media_pool_item.GetMetadata()\n    # add additional metadata from kwargs\n    if kwargs:\n        mp_metadata.update(kwargs)\n\n    # add metadata to otio item metadata\n    for key, value in mp_metadata.items():\n        otio_item.metadata.update({key: value})\n\n\ndef create_otio_timeline(resolve_project):\n\n    # get current timeline\n    self.project_fps = resolve_project.GetSetting(\"timelineFrameRate\")\n    timeline = resolve_project.GetCurrentTimeline()\n\n    # convert timeline to otio\n    otio_timeline = _create_otio_timeline(\n        resolve_project, timeline, self.project_fps)\n\n    # loop all defined track types\n    for track_type in list(self.track_types.keys()):\n        # get total track count\n        track_count = timeline.GetTrackCount(track_type)\n\n        # loop all tracks by track indexes\n        for track_index in range(1, int(track_count) + 1):\n            # get current track name\n            track_name = timeline.GetTrackName(track_type, track_index)\n\n            # convert track to otio\n            otio_track = create_otio_track(\n                track_type, track_name)\n\n            # get all track items in current track\n            current_track_items = timeline.GetItemListInTrack(\n                track_type, track_index)\n\n            # loop available track items in current track items\n            for track_item in current_track_items:\n                # skip offline track items\n                if track_item.GetMediaPoolItem() is None:\n                    continue\n\n                # calculate real clip start\n                clip_start = track_item.GetStart() - timeline.GetStartFrame()\n\n                add_otio_gap(\n                    clip_start, otio_track, track_item, timeline)\n\n                # create otio clip and add it to track\n                otio_clip = create_otio_clip(track_item)\n\n                if not isinstance(otio_clip, list):\n                    otio_track.append(otio_clip)\n                else:\n                    for index, clip in enumerate(otio_clip):\n                        if index == 0:\n                            otio_track.append(clip)\n                        else:\n                            # add previous otio track to timeline\n                            otio_timeline.tracks.append(otio_track)\n                            # convert track to otio\n                            otio_track = create_otio_track(\n                                track_type, track_name)\n                            add_otio_gap(\n                                clip_start, otio_track,\n                                track_item, timeline)\n                            otio_track.append(clip)\n\n            # add track to otio timeline\n            otio_timeline.tracks.append(otio_track)\n\n    return otio_timeline\n\n\ndef write_to_file(otio_timeline, path):\n    otio.adapters.write_to_file(otio_timeline, path)\n"
  },
  {
    "path": "openpype/hosts/resolve/otio/davinci_import.py",
    "content": "import sys\nimport json\nimport DaVinciResolveScript\nimport opentimelineio as otio\n\n\nself = sys.modules[__name__]\nself.resolve = DaVinciResolveScript.scriptapp('Resolve')\nself.fusion = DaVinciResolveScript.scriptapp('Fusion')\nself.project_manager = self.resolve.GetProjectManager()\nself.current_project = self.project_manager.GetCurrentProject()\nself.media_pool = self.current_project.GetMediaPool()\nself.track_types = {\n    \"video\": otio.schema.TrackKind.Video,\n    \"audio\": otio.schema.TrackKind.Audio\n}\nself.project_fps = None\n\n\ndef build_timeline(otio_timeline):\n    # TODO: build timeline in mediapool `otioImport` folder\n    # TODO: loop otio tracks and build them in the new timeline\n    for clip in otio_timeline.each_clip():\n        # TODO: create track item\n        print(clip.name)\n        print(clip.parent().name)\n        print(clip.range_in_parent())\n\n\ndef _build_track(otio_track):\n    # TODO: _build_track\n    pass\n\n\ndef _build_media_pool_item(otio_media_reference):\n    # TODO: _build_media_pool_item\n    pass\n\n\ndef _build_track_item(otio_clip):\n    # TODO: _build_track_item\n    pass\n\n\ndef _build_gap(otio_clip):\n    # TODO: _build_gap\n    pass\n\n\ndef _build_marker(track_item, otio_marker):\n    frame_start = otio_marker.marked_range.start_time.value\n    frame_duration = otio_marker.marked_range.duration.value\n\n    # marker attributes\n    frameId = (frame_start / 10) * 10\n    color = otio_marker.color\n    name = otio_marker.name\n    note = otio_marker.metadata.get(\"note\") or json.dumps(otio_marker.metadata)\n    duration = (frame_duration / 10) * 10\n\n    track_item.AddMarker(\n        frameId,\n        color,\n        name,\n        note,\n        duration\n    )\n\n\ndef _build_media_pool_folder(name):\n    \"\"\"\n    Returns folder with input name and sets it as current folder.\n\n    It will create new media bin if none is found in root media bin\n\n    Args:\n        name (str): name of bin\n\n    Returns:\n        resolve.api.MediaPool.Folder: description\n\n    \"\"\"\n\n    root_folder = self.media_pool.GetRootFolder()\n    sub_folders = root_folder.GetSubFolderList()\n    testing_names = list()\n\n    for subfolder in sub_folders:\n        subf_name = subfolder.GetName()\n        if name in subf_name:\n            testing_names.append(subfolder)\n        else:\n            testing_names.append(False)\n\n    matching = next((f for f in testing_names if f is not False), None)\n\n    if not matching:\n        new_folder = self.media_pool.AddSubFolder(root_folder, name)\n        self.media_pool.SetCurrentFolder(new_folder)\n    else:\n        self.media_pool.SetCurrentFolder(matching)\n\n    return self.media_pool.GetCurrentFolder()\n\n\ndef read_from_file(otio_file):\n    otio_timeline = otio.adapters.read_from_file(otio_file)\n    build_timeline(otio_timeline)\n"
  },
  {
    "path": "openpype/hosts/resolve/otio/utils.py",
    "content": "import re\nimport opentimelineio as otio\n\n\ndef timecode_to_frames(timecode, framerate):\n    rt = otio.opentime.from_timecode(timecode, 24)\n    return int(otio.opentime.to_frames(rt))\n\n\ndef frames_to_timecode(frames, framerate):\n    rt = otio.opentime.from_frames(frames, framerate)\n    return otio.opentime.to_timecode(rt)\n\n\ndef frames_to_secons(frames, framerate):\n    rt = otio.opentime.from_frames(frames, framerate)\n    return otio.opentime.to_seconds(rt)\n\n\ndef get_reformated_path(path, padded=True, first=False):\n    \"\"\"\n    Return fixed python expression path\n\n    Args:\n        path (str): path url or simple file name\n\n    Returns:\n        type: string with reformated path\n\n    Example:\n        get_reformated_path(\"plate.[0001-1008].exr\") > plate.%04d.exr\n\n    \"\"\"\n    num_pattern = r\"(\\[\\d+\\-\\d+\\])\"\n    padding_pattern = r\"(\\d+)(?=-)\"\n    first_frame_pattern = re.compile(r\"\\[(\\d+)\\-\\d+\\]\")\n\n    if \"[\" in path:\n        padding = len(re.findall(padding_pattern, path).pop())\n        if padded:\n            path = re.sub(num_pattern, f\"%0{padding}d\", path)\n        elif first:\n            first_frame = re.findall(first_frame_pattern, path, flags=0)\n            if len(first_frame) >= 1:\n                first_frame = first_frame[0]\n            path = re.sub(num_pattern, first_frame, path)\n        else:\n            path = re.sub(num_pattern, \"%d\", path)\n    return path\n\n\ndef get_padding_from_path(path):\n    \"\"\"\n    Return padding number from DaVinci Resolve sequence path style\n\n    Args:\n        path (str): path url or simple file name\n\n    Returns:\n        int: padding number\n\n    Example:\n        get_padding_from_path(\"plate.[0001-1008].exr\") > 4\n\n    \"\"\"\n    padding_pattern = \"(\\\\d+)(?=-)\"\n    if \"[\" in path:\n        return len(re.findall(padding_pattern, path).pop())\n\n    return None\n"
  },
  {
    "path": "openpype/hosts/resolve/plugins/create/create_shot_clip.py",
    "content": "# from pprint import pformat\nfrom openpype.hosts.resolve.api import plugin, lib\nfrom openpype.hosts.resolve.api.lib import (\n    get_video_track_names,\n    create_bin,\n)\n\n\nclass CreateShotClip(plugin.Creator):\n    \"\"\"Publishable clip\"\"\"\n\n    label = \"Create Publishable Clip\"\n    family = \"clip\"\n    icon = \"film\"\n    defaults = [\"Main\"]\n\n    gui_tracks = get_video_track_names()\n    gui_name = \"OpenPype publish attributes creator\"\n    gui_info = \"Define sequential rename and fill hierarchy data.\"\n    gui_inputs = {\n        \"renameHierarchy\": {\n            \"type\": \"section\",\n            \"label\": \"Shot Hierarchy And Rename Settings\",\n            \"target\": \"ui\",\n            \"order\": 0,\n            \"value\": {\n                \"hierarchy\": {\n                    \"value\": \"{folder}/{sequence}\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"Shot Parent Hierarchy\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Parents folder for shot root folder, Template filled with `Hierarchy Data` section\",  # noqa\n                    \"order\": 0},\n                \"clipRename\": {\n                    \"value\": False,\n                    \"type\": \"QCheckBox\",\n                    \"label\": \"Rename clips\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"Renaming selected clips on fly\",  # noqa\n                    \"order\": 1},\n                \"clipName\": {\n                    \"value\": \"{sequence}{shot}\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"Clip Name Template\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"template for creating shot namespaused for renaming (use rename: on)\",  # noqa\n                    \"order\": 2},\n                \"countFrom\": {\n                    \"value\": 10,\n                    \"type\": \"QSpinBox\",\n                    \"label\": \"Count sequence from\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"Set when the sequence number stafrom\",  # noqa\n                    \"order\": 3},\n                \"countSteps\": {\n                    \"value\": 10,\n                    \"type\": \"QSpinBox\",\n                    \"label\": \"Stepping number\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"What number is adding every new step\",  # noqa\n                    \"order\": 4},\n            }\n        },\n        \"hierarchyData\": {\n            \"type\": \"dict\",\n            \"label\": \"Shot Template Keywords\",\n            \"target\": \"tag\",\n            \"order\": 1,\n            \"value\": {\n                \"folder\": {\n                    \"value\": \"shots\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"{folder}\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Name of folder used for root of generated shots.\\nUsable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                    \"order\": 0},\n                \"episode\": {\n                    \"value\": \"ep01\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"{episode}\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Name of episode.\\nUsable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                    \"order\": 1},\n                \"sequence\": {\n                    \"value\": \"sq01\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"{sequence}\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Name of sequence of shots.\\nUsable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                    \"order\": 2},\n                \"track\": {\n                    \"value\": \"{_track_}\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"{track}\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Name of sequence of shots.\\nUsable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                    \"order\": 3},\n                \"shot\": {\n                    \"value\": \"sh###\",\n                    \"type\": \"QLineEdit\",\n                    \"label\": \"{shot}\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Name of shot. `#` is converted to paded number. \\nAlso could be used with usable tokens:\\n\\t{_clip_}: name of used clip\\n\\t{_track_}: name of parent track layer\\n\\t{_sequence_}: name of parent sequence (timeline)\",  # noqa\n                    \"order\": 4}\n            }\n        },\n        \"verticalSync\": {\n            \"type\": \"section\",\n            \"label\": \"Vertical Synchronization Of Attributes\",\n            \"target\": \"ui\",\n            \"order\": 2,\n            \"value\": {\n                \"vSyncOn\": {\n                    \"value\": True,\n                    \"type\": \"QCheckBox\",\n                    \"label\": \"Enable Vertical Sync\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"Switch on if you want clips above each other to share its attributes\",  # noqa\n                    \"order\": 0},\n                \"vSyncTrack\": {\n                    \"value\": gui_tracks,  # noqa\n                    \"type\": \"QComboBox\",\n                    \"label\": \"Hero track\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"Select driving track name which should be mastering all others\",  # noqa\n                    \"order\": 1\n                }\n            }\n        },\n        \"publishSettings\": {\n            \"type\": \"section\",\n            \"label\": \"Publish Settings\",\n            \"target\": \"ui\",\n            \"order\": 3,\n            \"value\": {\n                \"subsetName\": {\n                    \"value\": [\"<track_name>\", \"main\", \"bg\", \"fg\", \"bg\",\n                              \"animatic\"],\n                    \"type\": \"QComboBox\",\n                    \"label\": \"Subset Name\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"chose subset name pattern, if <track_name> is selected, name of track layer will be used\",  # noqa\n                    \"order\": 0},\n                \"subsetFamily\": {\n                    \"value\": [\"plate\", \"take\"],\n                    \"type\": \"QComboBox\",\n                    \"label\": \"Subset Family\",\n                    \"target\": \"ui\", \"toolTip\": \"What use of this subset is for\",  # noqa\n                    \"order\": 1},\n                \"reviewTrack\": {\n                    \"value\": [\"< none >\"] + gui_tracks,\n                    \"type\": \"QComboBox\",\n                    \"label\": \"Use Review Track\",\n                    \"target\": \"ui\",\n                    \"toolTip\": \"Generate preview videos on fly, if `< none >` is defined nothing will be generated.\",  # noqa\n                    \"order\": 2},\n                \"audio\": {\n                    \"value\": False,\n                    \"type\": \"QCheckBox\",\n                    \"label\": \"Include audio\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Process subsets with corresponding audio\",  # noqa\n                    \"order\": 3},\n                \"sourceResolution\": {\n                    \"value\": False,\n                    \"type\": \"QCheckBox\",\n                    \"label\": \"Source resolution\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Is resloution taken from timeline or source?\",  # noqa\n                    \"order\": 4},\n            }\n        },\n        \"shotAttr\": {\n            \"type\": \"section\",\n            \"label\": \"Shot Attributes\",\n            \"target\": \"ui\",\n            \"order\": 4,\n            \"value\": {\n                \"workfileFrameStart\": {\n                    \"value\": 1001,\n                    \"type\": \"QSpinBox\",\n                    \"label\": \"Workfiles Start Frame\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Set workfile starting frame number\",  # noqa\n                    \"order\": 0\n                },\n                \"handleStart\": {\n                    \"value\": 0,\n                    \"type\": \"QSpinBox\",\n                    \"label\": \"Handle start (head)\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Handle at start of clip\",  # noqa\n                    \"order\": 1\n                },\n                \"handleEnd\": {\n                    \"value\": 0,\n                    \"type\": \"QSpinBox\",\n                    \"label\": \"Handle end (tail)\",\n                    \"target\": \"tag\",\n                    \"toolTip\": \"Handle at end of clip\",  # noqa\n                    \"order\": 2\n                }\n            }\n        }\n    }\n\n    presets = None\n\n    def process(self):\n        # get key pares from presets and match it on ui inputs\n        for k, v in self.gui_inputs.items():\n            if v[\"type\"] in (\"dict\", \"section\"):\n                # nested dictionary (only one level allowed\n                # for sections and dict)\n                for _k, _v in v[\"value\"].items():\n                    if self.presets.get(_k) is not None:\n                        self.gui_inputs[k][\n                            \"value\"][_k][\"value\"] = self.presets[_k]\n            if self.presets.get(k):\n                self.gui_inputs[k][\"value\"] = self.presets[k]\n\n        # open widget for plugins inputs\n        widget = self.widget(self.gui_name, self.gui_info, self.gui_inputs)\n        widget.exec_()\n\n        if len(self.selected) < 1:\n            return\n\n        if not widget.result:\n            print(\"Operation aborted\")\n            return\n\n        self.rename_add = 0\n\n        # get ui output for track name for vertical sync\n        v_sync_track = widget.result[\"vSyncTrack\"][\"value\"]\n\n        # sort selected trackItems by\n        sorted_selected_track_items = []\n        unsorted_selected_track_items = []\n        print(\"_____ selected ______\")\n        print(self.selected)\n        for track_item_data in self.selected:\n            if track_item_data[\"track\"][\"name\"] in v_sync_track:\n                sorted_selected_track_items.append(track_item_data)\n            else:\n                unsorted_selected_track_items.append(track_item_data)\n\n        sorted_selected_track_items.extend(unsorted_selected_track_items)\n\n        # sequence attrs\n        sq_frame_start = self.timeline.GetStartFrame()\n        sq_markers = self.timeline.GetMarkers()\n\n        # create media bin for compound clips (trackItems)\n        mp_folder = create_bin(self.timeline.GetName())\n\n        kwargs = {\n            \"ui_inputs\": widget.result,\n            \"avalon\": self.data,\n            \"mp_folder\": mp_folder,\n            \"sq_frame_start\": sq_frame_start,\n            \"sq_markers\": sq_markers\n        }\n        print(kwargs)\n        for i, track_item_data in enumerate(sorted_selected_track_items):\n            self.rename_index = i\n            self.log.info(track_item_data)\n            # convert track item to timeline media pool item\n            track_item = plugin.PublishClip(\n                self, track_item_data, **kwargs).convert()\n            track_item.SetClipColor(lib.publish_clip_color)\n"
  },
  {
    "path": "openpype/hosts/resolve/plugins/load/load_clip.py",
    "content": "from openpype.client import get_last_version_by_subset_id\nfrom openpype.pipeline import (\n    get_representation_context,\n    get_current_project_name\n)\nfrom openpype.hosts.resolve.api import lib, plugin\nfrom openpype.hosts.resolve.api.pipeline import (\n    containerise,\n    update_container,\n)\nfrom openpype.lib.transcoding import (\n    VIDEO_EXTENSIONS,\n    IMAGE_EXTENSIONS\n)\n\n\nclass LoadClip(plugin.TimelineItemLoader):\n    \"\"\"Load a subset to timeline as clip\n\n    Place clip to timeline on its asset origin timings collected\n    during conforming to project\n    \"\"\"\n\n    families = [\"render2d\", \"source\", \"plate\", \"render\", \"review\"]\n\n    representations = [\"*\"]\n    extensions = set(\n        ext.lstrip(\".\") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)\n    )\n\n    label = \"Load as clip\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    # for loader multiselection\n    timeline = None\n\n    # presets\n    clip_color_last = \"Olive\"\n    clip_color = \"Orange\"\n\n    def load(self, context, name, namespace, options):\n\n        # load clip to timeline and get main variables\n        files = plugin.get_representation_files(context[\"representation\"])\n\n        timeline_item = plugin.ClipLoader(\n            self, context, **options).load(files)\n        namespace = namespace or timeline_item.GetName()\n\n        # update color of clip regarding the version order\n        self.set_item_color(timeline_item, version=context[\"version\"])\n\n        data_imprint = self.get_tag_data(context, name, namespace)\n        return containerise(\n            timeline_item,\n            name, namespace, context,\n            self.__class__.__name__,\n            data_imprint)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n        \"\"\" Updating previously loaded clips\n        \"\"\"\n\n        context = get_representation_context(representation)\n        name = container['name']\n        namespace = container['namespace']\n        timeline_item = container[\"_timeline_item\"]\n\n        media_pool_item = timeline_item.GetMediaPoolItem()\n\n        files = plugin.get_representation_files(representation)\n\n        loader = plugin.ClipLoader(self, context)\n        timeline_item = loader.update(timeline_item, files)\n\n        # update color of clip regarding the version order\n        self.set_item_color(timeline_item, version=context[\"version\"])\n\n        # if original media pool item has no remaining usages left\n        # remove it from the media pool\n        if int(media_pool_item.GetClipProperty(\"Usage\")) == 0:\n            lib.remove_media_pool_item(media_pool_item)\n\n        data_imprint = self.get_tag_data(context, name, namespace)\n        return update_container(timeline_item, data_imprint)\n\n    def get_tag_data(self, context, name, namespace):\n        \"\"\"Return data to be imprinted on the timeline item marker\"\"\"\n\n        representation = context[\"representation\"]\n        version = context['version']\n        version_data = version.get(\"data\", {})\n        version_name = version.get(\"name\", None)\n        colorspace = version_data.get(\"colorspace\", None)\n        object_name = \"{}_{}\".format(name, namespace)\n\n        # add additional metadata from the version to imprint Avalon knob\n        # move all version data keys to tag data\n        add_version_data_keys = [\n            \"frameStart\", \"frameEnd\", \"source\", \"author\",\n            \"fps\", \"handleStart\", \"handleEnd\"\n        ]\n        data = {\n            key: version_data.get(key, \"None\") for key in add_version_data_keys\n        }\n\n        # add variables related to version context\n        data.update({\n            \"representation\": str(representation[\"_id\"]),\n            \"version\": version_name,\n            \"colorspace\": colorspace,\n            \"objectName\": object_name\n        })\n        return data\n\n    @classmethod\n    def set_item_color(cls, timeline_item, version):\n        \"\"\"Color timeline item based on whether it is outdated or latest\"\"\"\n        # define version name\n        version_name = version.get(\"name\", None)\n        # get all versions in list\n        project_name = get_current_project_name()\n        last_version_doc = get_last_version_by_subset_id(\n            project_name,\n            version[\"parent\"],\n            fields=[\"name\"]\n        )\n        if last_version_doc:\n            last_version = last_version_doc[\"name\"]\n        else:\n            last_version = None\n\n        # set clip colour\n        if version_name == last_version:\n            timeline_item.SetClipColor(cls.clip_color_last)\n        else:\n            timeline_item.SetClipColor(cls.clip_color)\n\n    def remove(self, container):\n        timeline_item = container[\"_timeline_item\"]\n        media_pool_item = timeline_item.GetMediaPoolItem()\n        timeline = lib.get_current_timeline()\n\n        # DeleteClips function was added in Resolve 18.5+\n        # by checking None we can detect whether the\n        # function exists in Resolve\n        if timeline.DeleteClips is not None:\n            timeline.DeleteClips([timeline_item])\n        else:\n            # Resolve versions older than 18.5 can't delete clips via API\n            # so all we can do is just remove the pype marker to 'untag' it\n            if lib.get_pype_marker(timeline_item):\n                # Note: We must call `get_pype_marker` because\n                # `delete_pype_marker` uses a global variable set by\n                # `get_pype_marker` to delete the right marker\n                # TODO: Improve code to avoid the global `temp_marker_frame`\n                lib.delete_pype_marker(timeline_item)\n\n        # if media pool item has no remaining usages left\n        # remove it from the media pool\n        if int(media_pool_item.GetClipProperty(\"Usage\")) == 0:\n            lib.remove_media_pool_item(media_pool_item)\n"
  },
  {
    "path": "openpype/hosts/resolve/plugins/publish/extract_workfile.py",
    "content": "import os\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.resolve.api.lib import get_project_manager\n\n\nclass ExtractWorkfile(publish.Extractor):\n    \"\"\"\n    Extractor export DRP workfile file representation\n    \"\"\"\n\n    label = \"Extract Workfile\"\n    order = pyblish.api.ExtractorOrder\n    families = [\"workfile\"]\n    hosts = [\"resolve\"]\n\n    def process(self, instance):\n        # create representation data\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        name = instance.data[\"name\"]\n        project = instance.context.data[\"activeProject\"]\n        staging_dir = self.staging_dir(instance)\n\n        resolve_workfile_ext = \".drp\"\n        drp_file_name = name + resolve_workfile_ext\n\n        drp_file_path = os.path.normpath(\n            os.path.join(staging_dir, drp_file_name))\n\n        # write out the drp workfile\n        get_project_manager().ExportProject(\n            project.GetName(), drp_file_path)\n\n        # create drp workfile representation\n        representation_drp = {\n            'name': resolve_workfile_ext[1:],\n            'ext': resolve_workfile_ext[1:],\n            'files': drp_file_name,\n            \"stagingDir\": staging_dir,\n        }\n\n        instance.data[\"representations\"].append(representation_drp)\n\n        # add sourcePath attribute to instance\n        if not instance.data.get(\"sourcePath\"):\n            instance.data[\"sourcePath\"] = drp_file_path\n\n        self.log.info(\"Added Resolve file representation: {}\".format(\n            representation_drp))\n"
  },
  {
    "path": "openpype/hosts/resolve/plugins/publish/precollect_instances.py",
    "content": "from pprint import pformat\n\nimport pyblish\n\nfrom openpype.hosts.resolve.api.lib import (\n    get_current_timeline_items,\n    get_timeline_item_pype_tag,\n    publish_clip_color,\n    get_publish_attribute,\n    get_otio_clip_instance_data,\n)\nfrom openpype import AYON_SERVER_ENABLED\n\n\nclass PrecollectInstances(pyblish.api.ContextPlugin):\n    \"\"\"Collect all Track items selection.\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.49\n    label = \"Precollect Instances\"\n    hosts = [\"resolve\"]\n\n    def process(self, context):\n        otio_timeline = context.data[\"otioTimeline\"]\n        selected_timeline_items = get_current_timeline_items(\n            filter=True, selecting_color=publish_clip_color)\n\n        self.log.info(\n            \"Processing enabled track items: {}\".format(\n                len(selected_timeline_items)))\n\n        for timeline_item_data in selected_timeline_items:\n\n            data = {}\n            timeline_item = timeline_item_data[\"clip\"][\"item\"]\n\n            # get pype tag data\n            tag_data = get_timeline_item_pype_tag(timeline_item)\n            self.log.debug(f\"__ tag_data: {pformat(tag_data)}\")\n\n            if not tag_data:\n                continue\n\n            if tag_data.get(\"id\") != \"pyblish.avalon.instance\":\n                continue\n\n            media_pool_item = timeline_item.GetMediaPoolItem()\n            source_duration = int(media_pool_item.GetClipProperty(\"Frames\"))\n\n            # solve handles length\n            handle_start = min(\n                tag_data[\"handleStart\"], int(timeline_item.GetLeftOffset()))\n            handle_end = min(\n                tag_data[\"handleEnd\"], int(\n                    source_duration - timeline_item.GetRightOffset()))\n\n            self.log.debug(\"Handles: <{}, {}>\".format(handle_start, handle_end))\n\n            # add tag data to instance data\n            data.update({\n                k: v for k, v in tag_data.items()\n                if k not in (\"id\", \"applieswhole\", \"label\")\n            })\n\n            if AYON_SERVER_ENABLED:\n                asset = tag_data[\"folder_path\"]\n            else:\n                asset = tag_data[\"asset_name\"]\n\n            subset = tag_data[\"subset\"]\n\n            data.update({\n                \"name\": \"{}_{}\".format(asset, subset),\n                \"label\": \"{} {}\".format(asset, subset),\n                \"asset\": asset,\n                \"item\": timeline_item,\n                \"publish\": get_publish_attribute(timeline_item),\n                \"fps\": context.data[\"fps\"],\n                \"handleStart\": handle_start,\n                \"handleEnd\": handle_end,\n                \"newAssetPublishing\": True,\n                \"families\": [\"clip\"],\n            })\n\n            # otio clip data\n            otio_data = get_otio_clip_instance_data(\n                otio_timeline, timeline_item_data) or {}\n            data.update(otio_data)\n\n            # add resolution\n            self.get_resolution_to_data(data, context)\n\n            # create instance\n            instance = context.create_instance(**data)\n\n            # create shot instance for shot attributes create/update\n            self.create_shot_instance(context, timeline_item, **data)\n\n            self.log.info(\"Creating instance: {}\".format(instance))\n            self.log.debug(\n                \"_ instance.data: {}\".format(pformat(instance.data)))\n\n    def get_resolution_to_data(self, data, context):\n        assert data.get(\"otioClip\"), \"Missing `otioClip` data\"\n\n        # solve source resolution option\n        if data.get(\"sourceResolution\", None):\n            otio_clip_metadata = data[\n                \"otioClip\"].media_reference.metadata\n            data.update({\n                \"resolutionWidth\": otio_clip_metadata[\"width\"],\n                \"resolutionHeight\": otio_clip_metadata[\"height\"],\n                \"pixelAspect\": otio_clip_metadata[\"pixelAspect\"]\n            })\n        else:\n            otio_tl_metadata = context.data[\"otioTimeline\"].metadata\n            data.update({\n                \"resolutionWidth\": otio_tl_metadata[\"width\"],\n                \"resolutionHeight\": otio_tl_metadata[\"height\"],\n                \"pixelAspect\": otio_tl_metadata[\"pixelAspect\"]\n            })\n\n    def create_shot_instance(self, context, timeline_item, **data):\n        hero_track = data.get(\"heroTrack\")\n        hierarchy_data = data.get(\"hierarchyData\")\n\n        if not hero_track:\n            return\n\n        if not hierarchy_data:\n            return\n\n        asset = data[\"asset\"]\n        subset = \"shotMain\"\n\n        # insert family into families\n        family = \"shot\"\n\n        data.update({\n            \"name\": \"{}_{}\".format(asset, subset),\n            \"label\": \"{} {}\".format(asset, subset),\n            \"subset\": subset,\n            \"asset\": asset,\n            \"family\": family,\n            \"families\": [],\n            \"publish\": get_publish_attribute(timeline_item)\n        })\n\n        context.create_instance(**data)\n"
  },
  {
    "path": "openpype/hosts/resolve/plugins/publish/precollect_workfile.py",
    "content": "import pyblish.api\nfrom pprint import pformat\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import get_current_asset_name\n\nfrom openpype.hosts.resolve import api as rapi\nfrom openpype.hosts.resolve.otio import davinci_export\n\n\nclass PrecollectWorkfile(pyblish.api.ContextPlugin):\n    \"\"\"Precollect the current working file into context\"\"\"\n\n    label = \"Precollect Workfile\"\n    order = pyblish.api.CollectorOrder - 0.5\n\n    def process(self, context):\n        current_asset_name = asset_name = get_current_asset_name()\n\n        if AYON_SERVER_ENABLED:\n            asset_name = current_asset_name.split(\"/\")[-1]\n\n        subset = \"workfileMain\"\n        project = rapi.get_current_project()\n        fps = project.GetSetting(\"timelineFrameRate\")\n        video_tracks = rapi.get_video_track_names()\n\n        # adding otio timeline to context\n        otio_timeline = davinci_export.create_otio_timeline(project)\n\n        instance_data = {\n            \"name\": \"{}_{}\".format(asset_name, subset),\n            \"label\": \"{} {}\".format(current_asset_name, subset),\n            \"asset\": current_asset_name,\n            \"subset\": subset,\n            \"item\": project,\n            \"family\": \"workfile\",\n            \"families\": []\n        }\n\n        # create instance with workfile\n        instance = context.create_instance(**instance_data)\n\n        # update context with main project attributes\n        context_data = {\n            \"activeProject\": project,\n            \"otioTimeline\": otio_timeline,\n            \"videoTracks\": video_tracks,\n            \"currentFile\": project.GetName(),\n            \"fps\": fps,\n        }\n        context.data.update(context_data)\n\n        self.log.info(\"Creating instance: {}\".format(instance))\n        self.log.debug(\"__ instance.data: {}\".format(pformat(instance.data)))\n        self.log.debug(\"__ context_data: {}\".format(pformat(context_data)))\n"
  },
  {
    "path": "openpype/hosts/resolve/startup.py",
    "content": "\"\"\"This script is used as a startup script in Resolve through a .scriptlib file\n\nIt triggers directly after the launch of Resolve and it's recommended to keep\nit optimized for fast performance since the Resolve UI is actually interactive\nwhile this is running. As such, there's nothing ensuring the user isn't\ncontinuing manually before any of the logic here runs. As such we also try\nto delay any imports as much as possible.\n\nThis code runs in a separate process to the main Resolve process.\n\n\"\"\"\nimport os\nfrom openpype.lib import Logger\nimport openpype.hosts.resolve.api\n\nlog = Logger.get_logger(__name__)\n\n\ndef ensure_installed_host():\n    \"\"\"Install resolve host with openpype and return the registered host.\n\n    This function can be called multiple times without triggering an\n    additional install.\n    \"\"\"\n    from openpype.pipeline import install_host, registered_host\n    host = registered_host()\n    if host:\n        return host\n\n    host = openpype.hosts.resolve.api.ResolveHost()\n    install_host(host)\n    return registered_host()\n\n\ndef launch_menu():\n    print(\"Launching Resolve OpenPype menu..\")\n    ensure_installed_host()\n    openpype.hosts.resolve.api.launch_pype_menu()\n\n\ndef open_workfile(path):\n    # Avoid the need to \"install\" the host\n    host = ensure_installed_host()\n    host.open_workfile(path)\n\n\ndef main():\n    # Open last workfile\n    workfile_path = os.environ.get(\"OPENPYPE_RESOLVE_OPEN_ON_LAUNCH\")\n\n    if workfile_path and os.path.exists(workfile_path):\n        log.info(f\"Opening last workfile: {workfile_path}\")\n        open_workfile(workfile_path)\n    else:\n        log.info(\"No last workfile set to open. Skipping..\")\n\n    # Launch OpenPype menu\n    from openpype.settings import get_project_settings\n    from openpype.pipeline.context_tools import get_current_project_name\n    project_name = get_current_project_name()\n    log.info(f\"Current project name in context: {project_name}\")\n\n    settings = get_project_settings(project_name)\n    if settings.get(\"resolve\", {}).get(\"launch_openpype_menu_on_start\", True):\n        log.info(\"Launching OpenPype menu..\")\n        launch_menu()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "openpype/hosts/resolve/utility_scripts/AYON__Menu.py",
    "content": "import os\nimport sys\n\nfrom openpype.pipeline import install_host\nfrom openpype.lib import Logger\n\nlog = Logger.get_logger(__name__)\n\n\ndef main(env):\n    from openpype.hosts.resolve.api import ResolveHost, launch_pype_menu\n\n    # activate resolve from openpype\n    host = ResolveHost()\n    install_host(host)\n\n    launch_pype_menu()\n\n\nif __name__ == \"__main__\":\n    result = main(os.environ)\n    sys.exit(not bool(result))\n"
  },
  {
    "path": "openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py",
    "content": "import os\nimport sys\n\nfrom openpype.pipeline import install_host\nfrom openpype.lib import Logger\n\nlog = Logger.get_logger(__name__)\n\n\ndef main(env):\n    from openpype.hosts.resolve.api import ResolveHost, launch_pype_menu\n\n    # activate resolve from openpype\n    host = ResolveHost()\n    install_host(host)\n\n    launch_pype_menu()\n\n\nif __name__ == \"__main__\":\n    result = main(os.environ)\n    sys.exit(not bool(result))\n"
  },
  {
    "path": "openpype/hosts/resolve/utility_scripts/develop/OTIO_export.py",
    "content": "#!/usr/bin/env python\nimport os\nfrom openpype.hosts.resolve.otio import davinci_export as otio_export\n\nresolve = bmd.scriptapp(\"Resolve\")  # noqa\nfu = resolve.Fusion()\n\nui = fu.UIManager\ndisp = bmd.UIDispatcher(fu.UIManager)  # noqa\n\n\ntitle_font = ui.Font({\"PixelSize\": 18})\ndlg = disp.AddWindow(\n    {\n        \"WindowTitle\": \"Export OTIO\",\n        \"ID\": \"OTIOwin\",\n        \"Geometry\": [250, 250, 250, 100],\n        \"Spacing\": 0,\n        \"Margin\": 10\n    },\n    [\n        ui.VGroup(\n            {\n                \"Spacing\": 2\n            },\n            [\n                ui.Button(\n                    {\n                        \"ID\": \"exportfilebttn\",\n                        \"Text\": \"Select Destination\",\n                        \"Weight\": 1.25,\n                        \"ToolTip\": \"Choose where to save the otio\",\n                        \"Flat\": False\n                    }\n                ),\n                ui.VGap(),\n                ui.Button(\n                    {\n                        \"ID\": \"exportbttn\",\n                        \"Text\": \"Export\",\n                        \"Weight\": 2,\n                        \"ToolTip\": \"Export the current timeline\",\n                        \"Flat\": False\n                    }\n                )\n            ]\n        )\n    ]\n)\n\nitm = dlg.GetItems()\n\n\ndef _close_window(event):\n    disp.ExitLoop()\n\n\ndef _export_button(event):\n    pm = resolve.GetProjectManager()\n    project = pm.GetCurrentProject()\n    timeline = project.GetCurrentTimeline()\n    otio_timeline = otio_export.create_otio_timeline(project)\n    otio_path = os.path.join(\n        itm[\"exportfilebttn\"].Text,\n        timeline.GetName() + \".otio\")\n    print(otio_path)\n    otio_export.write_to_file(\n        otio_timeline,\n        otio_path)\n    _close_window(None)\n\n\ndef _export_file_pressed(event):\n    selectedPath = fu.RequestDir(os.path.expanduser(\"~/Documents\"))\n    itm[\"exportfilebttn\"].Text = selectedPath\n\n\ndlg.On.OTIOwin.Close = _close_window\ndlg.On.exportfilebttn.Clicked = _export_file_pressed\ndlg.On.exportbttn.Clicked = _export_button\ndlg.Show()\ndisp.RunLoop()\ndlg.Hide()\n"
  },
  {
    "path": "openpype/hosts/resolve/utility_scripts/develop/OTIO_import.py",
    "content": "#!/usr/bin/env python\nimport os\nfrom openpype.hosts.resolve.otio import davinci_import as otio_import\n\nresolve = bmd.scriptapp(\"Resolve\")  # noqa\nfu = resolve.Fusion()\nui = fu.UIManager\ndisp = bmd.UIDispatcher(fu.UIManager)  # noqa\n\n\ntitle_font = ui.Font({\"PixelSize\": 18})\ndlg = disp.AddWindow(\n    {\n        \"WindowTitle\": \"Import OTIO\",\n        \"ID\": \"OTIOwin\",\n        \"Geometry\": [250, 250, 250, 100],\n        \"Spacing\": 0,\n        \"Margin\": 10\n    },\n    [\n        ui.VGroup(\n            {\n                \"Spacing\": 2\n            },\n            [\n                ui.Button(\n                    {\n                        \"ID\": \"importOTIOfileButton\",\n                        \"Text\": \"Select OTIO File Path\",\n                        \"Weight\": 1.25,\n                        \"ToolTip\": \"Choose otio file to import from\",\n                        \"Flat\": False\n                    }\n                ),\n                ui.VGap(),\n                ui.Button(\n                    {\n                        \"ID\": \"importButton\",\n                        \"Text\": \"Import\",\n                        \"Weight\": 2,\n                        \"ToolTip\": \"Import otio to new timeline\",\n                        \"Flat\": False\n                    }\n                )\n            ]\n        )\n    ]\n)\n\nitm = dlg.GetItems()\n\n\ndef _close_window(event):\n    disp.ExitLoop()\n\n\ndef _import_button(event):\n    otio_import.read_from_file(itm[\"importOTIOfileButton\"].Text)\n    _close_window(None)\n\n\ndef _import_file_pressed(event):\n    selected_path = fu.RequestFile(os.path.expanduser(\"~/Documents\"))\n    itm[\"importOTIOfileButton\"].Text = selected_path\n\n\ndlg.On.OTIOwin.Close = _close_window\ndlg.On.importOTIOfileButton.Clicked = _import_file_pressed\ndlg.On.importButton.Clicked = _import_button\ndlg.Show()\ndisp.RunLoop()\ndlg.Hide()\n"
  },
  {
    "path": "openpype/hosts/resolve/utility_scripts/develop/OpenPype_sync_util_scripts.py",
    "content": "#!/usr/bin/env python\nimport os\nimport sys\n\nfrom openpype.pipeline import install_host\n\n\ndef main(env):\n    from openpype.hosts.resolve.utils import setup\n    import openpype.hosts.resolve.api as bmdvr\n    # Registers openpype's Global pyblish plugins\n    install_host(bmdvr)\n    setup(env)\n\n\nif __name__ == \"__main__\":\n    result = main(os.environ)\n    sys.exit(not bool(result))\n"
  },
  {
    "path": "openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib",
    "content": "-- Run OpenPype's Python launch script for resolve\nfunction file_exists(name)\n   local f = io.open(name, \"r\")\n   return f ~= nil and io.close(f)\nend\n\n\nopenpype_startup_script = os.getenv(\"OPENPYPE_RESOLVE_STARTUP_SCRIPT\")\nif openpype_startup_script ~= nil then\n    script = fusion:MapPath(openpype_startup_script)\n\n    if file_exists(script) then\n        -- We must use RunScript to ensure it runs in a separate\n        -- process to Resolve itself to avoid a deadlock for\n        -- certain imports of OpenPype libraries or Qt\n        print(\"Running launch script: \" .. script)\n        fusion:RunScript(script)\n    else\n        print(\"Launch script not found at: \" .. script)\n    end\nend"
  },
  {
    "path": "openpype/hosts/resolve/utils.py",
    "content": "import os\nimport shutil\nfrom openpype.lib import Logger, is_running_from_build\n\nfrom openpype import AYON_SERVER_ENABLED\nRESOLVE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\ndef setup(env):\n    log = Logger.get_logger(\"ResolveSetup\")\n    scripts = {}\n    util_scripts_env = env.get(\"RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR\")\n    util_scripts_dir = env[\"RESOLVE_UTILITY_SCRIPTS_DIR\"]\n\n    util_scripts_paths = [os.path.join(\n        RESOLVE_ROOT_DIR,\n        \"utility_scripts\"\n    )]\n\n    # collect script dirs\n    if util_scripts_env:\n        log.info(\"Utility Scripts Env: `{}`\".format(util_scripts_env))\n        util_scripts_paths = util_scripts_env.split(\n            os.pathsep) + util_scripts_paths\n\n    # collect scripts from dirs\n    for path in util_scripts_paths:\n        scripts.update({path: os.listdir(path)})\n\n    log.info(\"Utility Scripts Dir: `{}`\".format(util_scripts_paths))\n    log.info(\"Utility Scripts: `{}`\".format(scripts))\n\n    # Make sure scripts dir exists\n    os.makedirs(util_scripts_dir, exist_ok=True)\n\n    # make sure no script file is in folder\n    for script in os.listdir(util_scripts_dir):\n        path = os.path.join(util_scripts_dir, script)\n        log.info(\"Removing `{}`...\".format(path))\n        if os.path.isdir(path):\n            shutil.rmtree(path, onerror=None)\n        else:\n            os.remove(path)\n\n    # copy scripts into Resolve's utility scripts dir\n    for directory, scripts in scripts.items():\n        for script in scripts:\n            if (\n                is_running_from_build() and\n                script in [\"tests\", \"develop\"]\n            ):\n                # only copy those if started from build\n                continue\n\n            src = os.path.join(directory, script)\n            dst = os.path.join(util_scripts_dir, script)\n\n            # TODO: remove this once we have a proper solution\n            if AYON_SERVER_ENABLED:\n                if \"OpenPype__Menu.py\" == script:\n                    continue\n            else:\n                if \"AYON__Menu.py\" == script:\n                    continue\n\n            # TODO: Make this a less hacky workaround\n            if script == \"openpype_startup.scriptlib\":\n                # Handle special case for scriptlib that needs to be a folder\n                # up from the Comp folder in the Fusion scripts\n                dst = os.path.join(os.path.dirname(util_scripts_dir),\n                                   script)\n\n            log.info(\"Copying `{}` to `{}`...\".format(src, dst))\n            if os.path.isdir(src):\n                shutil.copytree(\n                    src, dst, symlinks=False,\n                    ignore=None, ignore_dangling_symlinks=False\n                )\n            else:\n                shutil.copy2(src, dst)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/__init__.py",
    "content": "from .addon import StandAlonePublishAddon\n\n\n__all__ = (\n    \"StandAlonePublishAddon\",\n)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/addon.py",
    "content": "import os\n\nfrom openpype.lib import get_openpype_execute_args\nfrom openpype.lib.execute import run_detached_process\nfrom openpype.modules import (\n    click_wrap,\n    OpenPypeModule,\n    ITrayAction,\n    IHostAddon,\n)\n\nSTANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass StandAlonePublishAddon(OpenPypeModule, ITrayAction, IHostAddon):\n    label = \"Publisher (legacy)\"\n    name = \"standalonepublisher\"\n    host_name = \"standalonepublisher\"\n\n    def initialize(self, modules_settings):\n        self.enabled = modules_settings[\"standalonepublish_tool\"][\"enabled\"]\n        self.publish_paths = [\n            os.path.join(STANDALONEPUBLISH_ROOT_DIR, \"plugins\", \"publish\")\n        ]\n\n    def tray_init(self):\n        return\n\n    def on_action_trigger(self):\n        self.run_standalone_publisher()\n\n    def connect_with_modules(self, enabled_modules):\n        \"\"\"Collect publish paths from other modules.\"\"\"\n\n        publish_paths = self.manager.collect_plugin_paths()[\"publish\"]\n        self.publish_paths.extend(publish_paths)\n\n    def run_standalone_publisher(self):\n        args = get_openpype_execute_args(\"module\", self.name, \"launch\")\n        run_detached_process(args)\n\n    def cli(self, click_group):\n        click_group.add_command(cli_main.to_click_obj())\n\n\n@click_wrap.group(\n    StandAlonePublishAddon.name,\n    help=\"StandalonePublisher related commands.\")\ndef cli_main():\n    pass\n\n\n@cli_main.command()\ndef launch():\n    \"\"\"Launch StandalonePublisher tool UI.\"\"\"\n\n    from openpype.tools import standalonepublish\n\n    standalonepublish.main()\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_app_name.py",
    "content": "import pyblish.api\n\n\nclass CollectSAAppName(pyblish.api.ContextPlugin):\n    \"\"\"Collect app name and label.\"\"\"\n\n    label = \"Collect App Name/Label\"\n    order = pyblish.api.CollectorOrder - 0.5\n    hosts = [\"standalonepublisher\"]\n\n    def process(self, context):\n        context.data[\"appName\"] = \"standalone publisher\"\n        context.data[\"appLabel\"] = \"Standalone publisher\"\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py",
    "content": "import copy\nimport json\nimport pyblish.api\n\nfrom openpype.client import get_asset_by_name\nfrom openpype.pipeline.create import get_subset_name\n\n\nclass CollectBulkMovInstances(pyblish.api.InstancePlugin):\n    \"\"\"Collect all available instances for batch publish.\"\"\"\n\n    label = \"Collect Bulk Mov Instances\"\n    order = pyblish.api.CollectorOrder + 0.489\n    hosts = [\"standalonepublisher\"]\n    families = [\"render_mov_batch\"]\n\n    new_instance_family = \"render\"\n    instance_task_names = [\n        \"compositing\",\n        \"comp\"\n    ]\n    default_task_name = \"compositing\"\n    subset_name_variant = \"Default\"\n\n    def process(self, instance):\n        context = instance.context\n        project_name = context.data[\"projectEntity\"][\"name\"]\n        asset_name = instance.data[\"asset\"]\n        asset_doc = get_asset_by_name(project_name, asset_name)\n        if not asset_doc:\n            raise AssertionError((\n                \"Couldn't find Asset document with name \\\"{}\\\"\"\n            ).format(asset_name))\n\n        available_task_names = {}\n        asset_tasks = asset_doc.get(\"data\", {}).get(\"tasks\") or {}\n        for task_name in asset_tasks.keys():\n            available_task_names[task_name.lower()] = task_name\n\n        task_name = self.default_task_name\n        for _task_name in self.instance_task_names:\n            _task_name_low = _task_name.lower()\n            if _task_name_low in available_task_names:\n                task_name = available_task_names[_task_name_low]\n                break\n\n        subset_name = get_subset_name(\n            self.new_instance_family,\n            self.subset_name_variant,\n            task_name,\n            asset_doc,\n            project_name,\n            host_name=context.data[\"hostName\"],\n            project_settings=context.data[\"project_settings\"]\n        )\n        instance_name = f\"{asset_name}_{subset_name}\"\n\n        # create new instance\n        new_instance = context.create_instance(instance_name)\n        new_instance_data = {\n            \"name\": instance_name,\n            \"label\": instance_name,\n            \"family\": self.new_instance_family,\n            \"subset\": subset_name,\n            \"task\": task_name\n        }\n        new_instance.data.update(new_instance_data)\n        # add original instance data except name key\n        for key, value in instance.data.items():\n            if key in new_instance_data:\n                continue\n            # Make sure value is copy since value may be object which\n            # can be shared across all new created objects\n            new_instance.data[key] = copy.deepcopy(value)\n\n        # Add `render_mov_batch` for specific validators\n        if \"families\" not in new_instance.data:\n            new_instance.data[\"families\"] = []\n        new_instance.data[\"families\"].append(\"render_mov_batch\")\n\n        # delete original instance\n        context.remove(instance)\n\n        self.log.info(f\"Created new instance: {instance_name}\")\n\n        def converter(value):\n            return str(value)\n\n        self.log.debug(\"Instance data: {}\".format(\n            json.dumps(new_instance.data, indent=4, default=converter)\n        ))\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_context.py",
    "content": "\"\"\"\nRequires:\n    environment     -> SAPUBLISH_INPATH\n    environment     -> SAPUBLISH_OUTPATH\n\nProvides:\n    context         -> returnJsonPath (str)\n    context         -> project\n    context         -> asset\n    instance        -> destination_list (list)\n    instance        -> representations (list)\n    instance        -> source (list)\n    instance        -> representations\n\"\"\"\n\nimport os\nimport json\nimport copy\nfrom pprint import pformat\nimport clique\nimport pyblish.api\n\nfrom openpype.pipeline import legacy_io\n\n\nclass CollectContextDataSAPublish(pyblish.api.ContextPlugin):\n    \"\"\"\n    Collecting temp json data sent from a host context\n    and path for returning json data back to hostself.\n    \"\"\"\n\n    label = \"Collect Context - SA Publish\"\n    order = pyblish.api.CollectorOrder - 0.49\n    hosts = [\"standalonepublisher\"]\n\n    # presets\n    batch_extensions = [\"edl\", \"xml\", \"psd\"]\n\n    def process(self, context):\n        # get json paths from os and load them\n        legacy_io.install()\n\n        # get json file context\n        input_json_path = os.environ.get(\"SAPUBLISH_INPATH\")\n\n        with open(input_json_path, \"r\") as f:\n            in_data = json.load(f)\n        self.log.debug(f\"_ in_data: {pformat(in_data)}\")\n\n        self.add_files_to_ignore_cleanup(in_data, context)\n        # exception for editorial\n        if in_data[\"family\"] == \"render_mov_batch\":\n            in_data_list = self.prepare_mov_batch_instances(in_data)\n\n        elif in_data[\"family\"] in [\"editorial\", \"background_batch\"]:\n            in_data_list = self.multiple_instances(context, in_data)\n\n        else:\n            in_data_list = [in_data]\n\n        self.log.debug(f\"_ in_data_list: {pformat(in_data_list)}\")\n\n        for in_data in in_data_list:\n            # create instance\n            self.create_instance(context, in_data)\n\n    def add_files_to_ignore_cleanup(self, in_data, context):\n        all_filepaths = context.data.get(\"skipCleanupFilepaths\") or []\n        for repre in in_data[\"representations\"]:\n            files = repre[\"files\"]\n            if not isinstance(files, list):\n                files = [files]\n\n            dirpath = repre[\"stagingDir\"]\n            for filename in files:\n                filepath = os.path.normpath(os.path.join(dirpath, filename))\n                if filepath not in all_filepaths:\n                    all_filepaths.append(filepath)\n\n        context.data[\"skipCleanupFilepaths\"] = all_filepaths\n\n    def multiple_instances(self, context, in_data):\n        # avoid subset name duplicity\n        if not context.data.get(\"subsetNamesCheck\"):\n            context.data[\"subsetNamesCheck\"] = list()\n\n        in_data_list = list()\n        representations = in_data.pop(\"representations\")\n        for repr in representations:\n            in_data_copy = copy.deepcopy(in_data)\n            ext = repr[\"ext\"][1:]\n            subset = in_data_copy[\"subset\"]\n            # filter out non editorial files\n            if ext not in self.batch_extensions:\n                in_data_copy[\"representations\"] = [repr]\n                in_data_copy[\"subset\"] = f\"{ext}{subset}\"\n                in_data_list.append(in_data_copy)\n\n            files = repr.get(\"files\")\n\n            # delete unneeded keys\n            delete_repr_keys = [\"frameStart\", \"frameEnd\"]\n            for k in delete_repr_keys:\n                if repr.get(k):\n                    repr.pop(k)\n\n            # convert files to list if it isn't\n            if not isinstance(files, (tuple, list)):\n                files = [files]\n\n            self.log.debug(f\"_ files: {files}\")\n            for index, f in enumerate(files):\n                index += 1\n                # copy dictionaries\n                in_data_copy = copy.deepcopy(in_data_copy)\n                repr_new = copy.deepcopy(repr)\n\n                repr_new[\"files\"] = f\n                repr_new[\"name\"] = ext\n                in_data_copy[\"representations\"] = [repr_new]\n\n                # create subset Name\n                new_subset = f\"{ext}{index}{subset}\"\n                while new_subset in context.data[\"subsetNamesCheck\"]:\n                    index += 1\n                    new_subset = f\"{ext}{index}{subset}\"\n\n                context.data[\"subsetNamesCheck\"].append(new_subset)\n                in_data_copy[\"subset\"] = new_subset\n                in_data_list.append(in_data_copy)\n                self.log.info(f\"Creating subset: {ext}{index}{subset}\")\n\n        return in_data_list\n\n    def prepare_mov_batch_instances(self, in_data):\n        \"\"\"Copy of `multiple_instances` method.\n\n        Method was copied because `batch_extensions` is used in\n        `multiple_instances` but without any family filtering. Since usage\n        of the filtering is unknown and modification of that part may break\n        editorial or PSD batch publishing it was decided to create a copy with\n        this family specific filtering. Also \"frameStart\" and \"frameEnd\" keys\n        are removed from instance which is needed for this processing.\n\n        Instance data will also care about families.\n\n        TODO:\n        - Merge possible logic with `multiple_instances` method.\n        \"\"\"\n        self.log.info(\"Preparing data for mov batch processing.\")\n        in_data_list = []\n\n        representations = in_data.pop(\"representations\")\n        for repre in representations:\n            self.log.debug(\"Processing representation with files {}\".format(\n                str(repre[\"files\"])\n            ))\n            ext = repre[\"ext\"][1:]\n\n            # Rename representation name\n            repre_name = repre[\"name\"]\n            if repre_name.startswith(ext + \"_\"):\n                repre[\"name\"] = ext\n            # Skip files that are not available for mov batch publishing\n            # TODO add dynamic expected extensions by family from `in_data`\n            #   - with this modification it would be possible to use only\n            #     `multiple_instances` method\n            expected_exts = [\"mov\"]\n            if ext not in expected_exts:\n                self.log.warning((\n                    \"Skipping representation.\"\n                    \" Does not match expected extensions <{}>. {}\"\n                ).format(\", \".join(expected_exts), str(repre)))\n                continue\n\n            files = repre[\"files\"]\n            # Convert files to list if it isn't\n            if not isinstance(files, (tuple, list)):\n                files = [files]\n\n            # Loop through files and create new instance per each file\n            for filename in files:\n                # Create copy of representation and change it's files and name\n                new_repre = copy.deepcopy(repre)\n                new_repre[\"files\"] = filename\n                new_repre[\"name\"] = ext\n                new_repre[\"thumbnail\"] = True\n\n                if \"tags\" not in new_repre:\n                    new_repre[\"tags\"] = []\n                new_repre[\"tags\"].append(\"review\")\n\n                # Prepare new subset name (temporary name)\n                # - subset name will be changed in batch specific plugins\n                new_subset_name = \"{}{}\".format(\n                    in_data[\"subset\"],\n                    os.path.basename(filename)\n                )\n                # Create copy of instance data as new instance and pass in new\n                #   representation\n                in_data_copy = copy.deepcopy(in_data)\n                in_data_copy[\"representations\"] = [new_repre]\n                in_data_copy[\"subset\"] = new_subset_name\n                if \"families\" not in in_data_copy:\n                    in_data_copy[\"families\"] = []\n                in_data_copy[\"families\"].append(\"review\")\n\n                in_data_list.append(in_data_copy)\n\n        return in_data_list\n\n    def create_instance(self, context, in_data):\n        subset = in_data[\"subset\"]\n        # If instance data already contain families then use it\n        instance_families = in_data.get(\"families\") or []\n\n        instance = context.create_instance(subset)\n        instance.data.update(\n            {\n                \"subset\": subset,\n                \"asset\": in_data[\"asset\"],\n                \"label\": subset,\n                \"name\": subset,\n                \"family\": in_data[\"family\"],\n                \"frameStart\": in_data.get(\"representations\", [None])[0].get(\n                    \"frameStart\", None\n                ),\n                \"frameEnd\": in_data.get(\"representations\", [None])[0].get(\n                    \"frameEnd\", None\n                ),\n                \"families\": instance_families\n            }\n        )\n        # Fill version only if 'use_next_available_version' is disabled\n        #   and version is filled in instance data\n        version = in_data.get(\"version\")\n        use_next_available_version = in_data.get(\n            \"use_next_available_version\", True)\n        if not use_next_available_version and version is not None:\n            instance.data[\"version\"] = version\n\n        self.log.info(\"collected instance: {}\".format(pformat(instance.data)))\n        self.log.info(\"parsing data: {}\".format(pformat(in_data)))\n\n        instance.data[\"destination_list\"] = list()\n        instance.data[\"representations\"] = list()\n        instance.data[\"source\"] = \"standalone publisher\"\n\n        for component in in_data[\"representations\"]:\n            component[\"destination\"] = component[\"files\"]\n            component[\"stagingDir\"] = component[\"stagingDir\"]\n\n            if isinstance(component[\"files\"], list):\n                collections, _remainder = clique.assemble(component[\"files\"])\n                self.log.debug(\"collecting sequence: {}\".format(collections))\n                instance.data[\"frameStart\"] = int(component[\"frameStart\"])\n                instance.data[\"frameEnd\"] = int(component[\"frameEnd\"])\n                if component.get(\"fps\"):\n                    instance.data[\"fps\"] = int(component[\"fps\"])\n\n            ext = component[\"ext\"]\n            if ext.startswith(\".\"):\n                component[\"ext\"] = ext[1:]\n\n            # Remove 'preview' key from representation data\n            preview = component.pop(\"preview\")\n            if preview:\n                instance.data[\"families\"].append(\"review\")\n                component[\"tags\"] = [\"review\"]\n                self.log.debug(\"Adding review family\")\n\n            if \"psd\" in component[\"name\"]:\n                instance.data[\"source\"] = component[\"files\"]\n                self.log.debug(\"Adding image:background_batch family\")\n\n            instance.data[\"representations\"].append(component)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py",
    "content": "\"\"\"\nOptional:\n    presets     -> extensions (\n        example of use:\n            [\"mov\", \"mp4\"]\n    )\n    presets     -> source_dir (\n        example of use:\n            \"C:/pathToFolder\"\n            \"{root}/{project[name]}/inputs\"\n            \"{root[work]}/{project[name]}/inputs\"\n            \"./input\"\n            \"../input\"\n            \"\"\n    )\n\"\"\"\n\nimport os\nimport opentimelineio as otio\nimport pyblish.api\nfrom openpype import lib as plib\nfrom openpype.pipeline.context_tools import get_current_project_asset\n\n\nclass OTIO_View(pyblish.api.Action):\n    \"\"\"Currently disabled because OTIO requires PySide2. Issue on Qt.py:\n    https://github.com/PixarAnimationStudios/OpenTimelineIO/issues/289\n    \"\"\"\n\n    label = \"OTIO View\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n        instance = context[0]\n        representation = instance.data[\"representations\"][0]\n        file_path = os.path.join(\n            representation[\"stagingDir\"], representation[\"files\"]\n        )\n        plib.run_subprocess([\"otioview\", file_path])\n\n\nclass CollectEditorial(pyblish.api.InstancePlugin):\n    \"\"\"Collect Editorial OTIO timeline\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = \"Collect Editorial\"\n    hosts = [\"standalonepublisher\"]\n    families = [\"editorial\"]\n    actions = []\n\n    # presets\n    extensions = [\"mov\", \"mp4\"]\n    source_dir = None\n\n    def process(self, instance):\n        root_dir = None\n        # remove context test attribute\n        if instance.context.data.get(\"subsetNamesCheck\"):\n            instance.context.data.pop(\"subsetNamesCheck\")\n\n        self.log.debug(f\"__ instance: `{instance}`\")\n        # get representation with editorial file\n        for representation in instance.data[\"representations\"]:\n            self.log.debug(f\"__ representation: `{representation}`\")\n            # make editorial sequence file path\n            staging_dir = representation[\"stagingDir\"]\n            file_path = os.path.join(\n                staging_dir, str(representation[\"files\"])\n            )\n            instance.context.data[\"currentFile\"] = file_path\n\n            # get video file path\n            video_path = None\n            basename = os.path.splitext(os.path.basename(file_path))[0]\n\n            if self.source_dir != \"\":\n                source_dir = self.source_dir.replace(\"\\\\\", \"/\")\n                if (\"./\" in source_dir) or (\"../\" in source_dir):\n                    # get current working dir\n                    cwd = os.getcwd()\n                    # set cwd to staging dir for absolute path solving\n                    os.chdir(staging_dir)\n                    root_dir = os.path.abspath(source_dir)\n                    # set back original cwd\n                    os.chdir(cwd)\n                elif \"{\" in source_dir:\n                    root_dir = source_dir\n                else:\n                    root_dir = os.path.normpath(source_dir)\n\n            if root_dir:\n                # search for source data will need to be done\n                instance.data[\"editorialSourceRoot\"] = root_dir\n                instance.data[\"editorialSourcePath\"] = None\n            else:\n                # source data are already found\n                for f in os.listdir(staging_dir):\n                    # filter out by not sharing the same name\n                    if os.path.splitext(f)[0] not in basename:\n                        continue\n                    # filter out by respected extensions\n                    if os.path.splitext(f)[1][1:] not in self.extensions:\n                        continue\n                    video_path = os.path.join(\n                        staging_dir, f\n                    )\n                    self.log.debug(f\"__ video_path: `{video_path}`\")\n                instance.data[\"editorialSourceRoot\"] = staging_dir\n                instance.data[\"editorialSourcePath\"] = video_path\n\n            instance.data[\"stagingDir\"] = staging_dir\n\n            # get editorial sequence file into otio timeline object\n            extension = os.path.splitext(file_path)[1]\n            kwargs = {}\n            if extension == \".edl\":\n                # EDL has no frame rate embedded so needs explicit\n                # frame rate else 24 is assumed.\n                kwargs[\"rate\"] = get_current_project_asset()[\"data\"][\"fps\"]\n\n            instance.data[\"otio_timeline\"] = otio.adapters.read_from_file(\n                file_path, **kwargs)\n\n            self.log.info(f\"Added OTIO timeline from: `{file_path}`\")\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py",
    "content": "import os\nfrom copy import deepcopy\n\nimport opentimelineio as otio\nimport pyblish.api\n\nfrom openpype import lib as plib\nfrom openpype.pipeline.context_tools import get_current_project_asset\n\n\nclass CollectInstances(pyblish.api.InstancePlugin):\n    \"\"\"Collect instances from editorial's OTIO sequence\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.01\n    label = \"Collect Editorial Instances\"\n    hosts = [\"standalonepublisher\"]\n    families = [\"editorial\"]\n\n    # presets\n    subsets = {\n        \"referenceMain\": {\n            \"family\": \"review\",\n            \"families\": [\"clip\"],\n            \"extensions\": [\"mp4\"]\n        },\n        \"audioMain\": {\n            \"family\": \"audio\",\n            \"families\": [\"clip\"],\n            \"extensions\": [\"wav\"],\n        }\n    }\n    timeline_frame_start = 900000  # starndard edl default (10:00:00:00)\n    timeline_frame_offset = None\n    custom_start_frame = None\n\n    def process(self, instance):\n        # get context\n        context = instance.context\n\n        instance_data_filter = [\n            \"editorialSourceRoot\",\n            \"editorialSourcePath\"\n        ]\n\n        # attribute for checking duplicity during creation\n        if not context.data.get(\"assetNameCheck\"):\n            context.data[\"assetNameCheck\"] = list()\n\n        # create asset_names conversion table\n        if not context.data.get(\"assetsShared\"):\n            context.data[\"assetsShared\"] = dict()\n\n        # get timeline otio data\n        timeline = instance.data[\"otio_timeline\"]\n        fps = get_current_project_asset()[\"data\"][\"fps\"]\n\n        tracks = timeline.each_child(\n            descended_from_type=otio.schema.Track\n        )\n\n        # get data from avalon\n        asset_entity = instance.context.data[\"assetEntity\"]\n        asset_data = asset_entity[\"data\"]\n        asset_name = asset_entity[\"name\"]\n\n        # Timeline data.\n        handle_start = int(asset_data[\"handleStart\"])\n        handle_end = int(asset_data[\"handleEnd\"])\n\n        for track in tracks:\n            self.log.debug(f\"track.name: {track.name}\")\n            try:\n                track_start_frame = (\n                    abs(track.source_range.start_time.value)\n                )\n                self.log.debug(f\"track_start_frame: {track_start_frame}\")\n                track_start_frame -= self.timeline_frame_start\n            except AttributeError:\n                track_start_frame = 0\n\n            self.log.debug(f\"track_start_frame: {track_start_frame}\")\n\n            for clip in track.each_child():\n                if clip.name is None:\n                    continue\n\n                if isinstance(clip, otio.schema.Gap):\n                    continue\n\n                # skip all generators like black empty\n                if isinstance(\n                    clip.media_reference,\n                        otio.schema.GeneratorReference):\n                    continue\n\n                # Transitions are ignored, because Clips have the full frame\n                # range.\n                if isinstance(clip, otio.schema.Transition):\n                    continue\n\n                # basic unique asset name\n                clip_name = os.path.splitext(clip.name)[0].lower()\n                name = f\"{asset_name.split('_')[0]}_{clip_name}\"\n\n                if name not in context.data[\"assetNameCheck\"]:\n                    context.data[\"assetNameCheck\"].append(name)\n                else:\n                    self.log.warning(f\"duplicate shot name: {name}\")\n\n                # frame ranges data\n                clip_in = clip.range_in_parent().start_time.value\n                clip_in += track_start_frame\n                clip_out = clip.range_in_parent().end_time_inclusive().value\n                clip_out += track_start_frame\n                self.log.info(f\"clip_in: {clip_in} | clip_out: {clip_out}\")\n\n                # add offset in case there is any\n                if self.timeline_frame_offset:\n                    clip_in += self.timeline_frame_offset\n                    clip_out += self.timeline_frame_offset\n\n                clip_duration = clip.duration().value\n                self.log.info(f\"clip duration: {clip_duration}\")\n\n                source_in = clip.trimmed_range().start_time.value\n                source_out = source_in + clip_duration\n                source_in_h = source_in - handle_start\n                source_out_h = source_out + handle_end\n\n                clip_in_h = clip_in - handle_start\n                clip_out_h = clip_out + handle_end\n\n                # define starting frame for future shot\n                if self.custom_start_frame is not None:\n                    frame_start = self.custom_start_frame\n                else:\n                    frame_start = clip_in\n\n                frame_end = frame_start + (clip_duration - 1)\n\n                # create shared new instance data\n                instance_data = {\n                    # shared attributes\n                    \"asset\": name,\n                    \"assetShareName\": name,\n                    \"item\": clip,\n                    \"clipName\": clip_name,\n\n                    # parent time properties\n                    \"trackStartFrame\": track_start_frame,\n                    \"handleStart\": handle_start,\n                    \"handleEnd\": handle_end,\n                    \"fps\": fps,\n\n                    # media source\n                    \"sourceIn\": source_in,\n                    \"sourceOut\": source_out,\n                    \"sourceInH\": source_in_h,\n                    \"sourceOutH\": source_out_h,\n\n                    # timeline\n                    \"clipIn\": clip_in,\n                    \"clipOut\": clip_out,\n                    \"clipDuration\": clip_duration,\n                    \"clipInH\": clip_in_h,\n                    \"clipOutH\": clip_out_h,\n                    \"clipDurationH\": clip_duration + handle_start + handle_end,\n\n                    # task\n                    \"frameStart\": frame_start,\n                    \"frameEnd\": frame_end,\n                    \"frameStartH\": frame_start - handle_start,\n                    \"frameEndH\": frame_end + handle_end,\n                    \"newAssetPublishing\": True\n                }\n\n                for data_key in instance_data_filter:\n                    instance_data.update({\n                        data_key: instance.data.get(data_key)})\n\n                # adding subsets to context as instances\n                self.subsets.update({\n                    \"shotMain\": {\n                        \"family\": \"shot\",\n                        \"families\": []\n                    }\n                })\n                for subset, properties in self.subsets.items():\n                    version = properties.get(\"version\")\n                    if version == 0:\n                        properties.pop(\"version\")\n\n                    # adding Review-able instance\n                    subset_instance_data = deepcopy(instance_data)\n                    subset_instance_data.update(deepcopy(properties))\n                    subset_instance_data.update({\n                        # unique attributes\n                        \"name\": f\"{name}_{subset}\",\n                        \"label\": f\"{name} {subset} ({clip_in}-{clip_out})\",\n                        \"subset\": subset\n                    })\n                    # create new instance\n                    _instance = instance.context.create_instance(\n                        **subset_instance_data)\n                    self.log.debug(\n                        f\"Instance: `{_instance}` | \"\n                        f\"families: `{subset_instance_data['families']}`\")\n\n                context.data[\"assetsShared\"][name] = {\n                    \"_clipIn\": clip_in,\n                    \"_clipOut\": clip_out\n                }\n\n                self.log.debug(\"Instance: `{}` | families: `{}`\")\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py",
    "content": "import os\nimport re\nimport tempfile\nimport pyblish.api\nfrom copy import deepcopy\nimport clique\n\n\nclass CollectInstanceResources(pyblish.api.InstancePlugin):\n    \"\"\"Collect instance's resources\"\"\"\n\n    # must be after `CollectInstances`\n    order = pyblish.api.CollectorOrder + 0.011\n    label = \"Collect Editorial Resources\"\n    hosts = [\"standalonepublisher\"]\n    families = [\"clip\"]\n\n    def process(self, instance):\n        self.context = instance.context\n        self.log.info(f\"Processing instance: {instance}\")\n        self.new_instances = []\n        subset_files = dict()\n        subset_dirs = list()\n        anatomy = self.context.data[\"anatomy\"]\n        anatomy_data = deepcopy(self.context.data[\"anatomyData\"])\n        anatomy_data.update({\"root\": anatomy.roots})\n\n        subset = instance.data[\"subset\"]\n        clip_name = instance.data[\"clipName\"]\n\n        editorial_source_root = instance.data[\"editorialSourceRoot\"]\n        editorial_source_path = instance.data[\"editorialSourcePath\"]\n\n        # if `editorial_source_path` then loop through\n        if editorial_source_path:\n            # add family if mov or mp4 found which is longer for\n            # cutting `trimming` to enable `ExtractTrimmingVideoAudio` plugin\n            staging_dir = os.path.normpath(\n                tempfile.mkdtemp(prefix=\"pyblish_tmp_\")\n            )\n            instance.data[\"stagingDir\"] = staging_dir\n            instance.data[\"families\"] += [\"trimming\"]\n            return\n\n        # if template pattern in path then fill it with `anatomy_data`\n        if \"{\" in editorial_source_root:\n            editorial_source_root = editorial_source_root.format(\n                **anatomy_data)\n\n        self.log.debug(f\"root: {editorial_source_root}\")\n        # loop `editorial_source_root` and find clip name in folders\n        # and look for any subset name alternatives\n        for root, dirs, _files in os.walk(editorial_source_root):\n            # search only for directories related to clip name\n            correct_clip_dir = None\n            for _d_search in dirs:\n                # avoid all non clip dirs\n                if _d_search not in clip_name:\n                    continue\n                # found correct dir for clip\n                correct_clip_dir = _d_search\n\n            # continue if clip dir was not found\n            if not correct_clip_dir:\n                continue\n\n            clip_dir_path = os.path.join(root, correct_clip_dir)\n            subset_files_items = list()\n            # list content of clip dir and search for subset items\n            for subset_item in os.listdir(clip_dir_path):\n                # avoid all items which are not defined as subsets by name\n                if subset not in subset_item:\n                    continue\n\n                subset_item_path = os.path.join(\n                    clip_dir_path, subset_item)\n                # if it is dir store it to `subset_dirs` list\n                if os.path.isdir(subset_item_path):\n                    subset_dirs.append(subset_item_path)\n\n                # if it is file then store it to `subset_files` list\n                if os.path.isfile(subset_item_path):\n                    subset_files_items.append(subset_item_path)\n\n            if subset_files_items:\n                subset_files.update({clip_dir_path: subset_files_items})\n\n            # break the loop if correct_clip_dir was captured\n            # no need to cary on if correct folder was found\n            if correct_clip_dir:\n                break\n\n        if subset_dirs:\n            # look all dirs and check for subset name alternatives\n            for _dir in subset_dirs:\n                instance_data = deepcopy(\n                    {k: v for k, v in instance.data.items()})\n                sub_dir = os.path.basename(_dir)\n                # if subset name is only alternative then create new instance\n                if sub_dir != subset:\n                    instance_data = self.duplicate_instance(\n                        instance_data, subset, sub_dir)\n\n                # create all representations\n                self.create_representations(\n                    os.listdir(_dir), instance_data, _dir)\n\n                if sub_dir == subset:\n                    self.new_instances.append(instance_data)\n                    # instance.data.update(instance_data)\n\n        if subset_files:\n            unique_subset_names = list()\n            root_dir = list(subset_files.keys()).pop()\n            files_list = subset_files[root_dir]\n            search_pattern = f\"({subset}[A-Za-z0-9]+)(?=[\\\\._\\\\s])\"\n            for _file in files_list:\n                pattern = re.compile(search_pattern)\n                match = pattern.findall(_file)\n                if not match:\n                    continue\n                match_subset = match.pop()\n                if match_subset in unique_subset_names:\n                    continue\n                unique_subset_names.append(match_subset)\n\n            self.log.debug(f\"unique_subset_names: {unique_subset_names}\")\n\n            for _un_subs in unique_subset_names:\n                instance_data = self.duplicate_instance(\n                    instance.data, subset, _un_subs)\n\n                # create all representations\n                self.create_representations(\n                    [os.path.basename(f) for f in files_list\n                     if _un_subs in f],\n                    instance_data, root_dir)\n\n        # remove the original instance as it had been used only\n        # as template and is duplicated\n        self.context.remove(instance)\n\n        # create all instances in self.new_instances into context\n        for new_instance in self.new_instances:\n            _new_instance = self.context.create_instance(\n                new_instance[\"name\"])\n            _new_instance.data.update(new_instance)\n\n    def duplicate_instance(self, instance_data, subset, new_subset):\n\n        new_instance_data = dict()\n        for _key, _value in instance_data.items():\n            new_instance_data[_key] = _value\n            if not isinstance(_value, str):\n                continue\n            if subset in _value:\n                new_instance_data[_key] = _value.replace(\n                    subset, new_subset)\n\n        self.log.info(f\"Creating new instance: {new_instance_data['name']}\")\n        self.new_instances.append(new_instance_data)\n        return new_instance_data\n\n    def create_representations(\n            self, files_list, instance_data, staging_dir):\n        \"\"\" Create representations from Collection object\n        \"\"\"\n        # collecting frames for later frame start/end reset\n        frames = list()\n        # break down Collection object to collections and reminders\n        collections, remainder = clique.assemble(files_list)\n        # add staging_dir to instance_data\n        instance_data[\"stagingDir\"] = staging_dir\n        # add representations to instance_data\n        instance_data[\"representations\"] = list()\n\n        collection_head_name = None\n        # loop through collections and create representations\n        for _collection in collections:\n            ext = _collection.tail[1:]\n            collection_head_name = _collection.head\n            frame_start = list(_collection.indexes)[0]\n            frame_end = list(_collection.indexes)[-1]\n            repre_data = {\n                \"frameStart\": frame_start,\n                \"frameEnd\": frame_end,\n                \"name\": ext,\n                \"ext\": ext,\n                \"files\": [item for item in _collection],\n                \"stagingDir\": staging_dir\n            }\n\n            if instance_data.get(\"keepSequence\"):\n                repre_data_keep = deepcopy(repre_data)\n                instance_data[\"representations\"].append(repre_data_keep)\n\n            if \"review\" in instance_data[\"families\"]:\n                repre_data.update({\n                    \"thumbnail\": True,\n                    \"frameStartFtrack\": frame_start,\n                    \"frameEndFtrack\": frame_end,\n                    \"step\": 1,\n                    \"fps\": self.context.data.get(\"fps\"),\n                    \"name\": \"review\",\n                    \"tags\": [\"review\", \"ftrackreview\", \"delete\"],\n                })\n            instance_data[\"representations\"].append(repre_data)\n\n            # add to frames for frame range reset\n            frames.append(frame_start)\n            frames.append(frame_end)\n\n        # loop through reminders and create representations\n        for _reminding_file in remainder:\n            ext = os.path.splitext(_reminding_file)[-1][1:]\n            if ext not in instance_data[\"extensions\"]:\n                continue\n            if collection_head_name and (\n                (collection_head_name + ext) not in _reminding_file\n            ) and (ext in [\"mp4\", \"mov\"]):\n                self.log.info(f\"Skipping file: {_reminding_file}\")\n                continue\n            frame_start = 1\n            frame_end = 1\n\n            repre_data = {\n                \"name\": ext,\n                \"ext\": ext,\n                \"files\": _reminding_file,\n                \"stagingDir\": staging_dir\n            }\n\n            # exception for thumbnail\n            if \"thumb\" in _reminding_file:\n                repre_data.update({\n                    'name': \"thumbnail\",\n                    'thumbnail': True\n                })\n\n            # exception for mp4 preview\n            if ext in [\"mp4\", \"mov\"]:\n                frame_start = 0\n                frame_end = (\n                    (instance_data[\"frameEnd\"] - instance_data[\"frameStart\"])\n                    + 1)\n                # add review ftrack family into families\n                for _family in [\"review\", \"ftrack\"]:\n                    if _family not in instance_data[\"families\"]:\n                        instance_data[\"families\"].append(_family)\n                repre_data.update({\n                    \"frameStart\": frame_start,\n                    \"frameEnd\": frame_end,\n                    \"frameStartFtrack\": frame_start,\n                    \"frameEndFtrack\": frame_end,\n                    \"step\": 1,\n                    \"fps\": self.context.data.get(\"fps\"),\n                    \"name\": \"review\",\n                    \"thumbnail\": True,\n                    \"tags\": [\"review\", \"ftrackreview\", \"delete\"],\n                })\n\n            # add to frames for frame range reset only if no collection\n            if not collections:\n                frames.append(frame_start)\n                frames.append(frame_end)\n\n            instance_data[\"representations\"].append(repre_data)\n\n        # reset frame start / end\n        instance_data[\"frameStart\"] = min(frames)\n        instance_data[\"frameEnd\"] = max(frames)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect Harmony scenes in Standalone Publisher.\"\"\"\nimport copy\nimport glob\nimport os\nfrom pprint import pformat\n\nimport pyblish.api\n\n\nclass CollectHarmonyScenes(pyblish.api.InstancePlugin):\n    \"\"\"Collect Harmony xstage files.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.498\n    label = \"Collect Harmony Scene\"\n    hosts = [\"standalonepublisher\"]\n    families = [\"harmony.scene\"]\n\n    # presets\n    ignored_instance_data_keys = (\"name\", \"label\", \"stagingDir\", \"version\")\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        context = instance.context\n        asset_data = instance.context.data[\"assetEntity\"]\n        asset_name = instance.data[\"asset\"]\n        subset_name = instance.data.get(\"subset\", \"sceneMain\")\n        anatomy_data = instance.context.data[\"anatomyData\"]\n        repres = instance.data[\"representations\"]\n        staging_dir = repres[0][\"stagingDir\"]\n        files = repres[0][\"files\"]\n\n        if not files.endswith(\".zip\"):\n            # A harmony project folder / .xstage was dropped\n            instance_name = f\"{asset_name}_{subset_name}\"\n            task = instance.data.get(\"task\", \"harmonyIngest\")\n\n            # create new instance\n            new_instance = context.create_instance(instance_name)\n\n            # add original instance data except name key\n            for key, value in instance.data.items():\n                # Make sure value is copy since value may be object which\n                # can be shared across all new created objects\n                if key not in self.ignored_instance_data_keys:\n                    new_instance.data[key] = copy.deepcopy(value)\n\n            self.log.info(\"Copied data: {}\".format(new_instance.data))\n\n            # fix anatomy data\n            anatomy_data_new = copy.deepcopy(anatomy_data)\n\n            project_entity = context.data[\"projectEntity\"]\n            asset_entity = context.data[\"assetEntity\"]\n\n            task_type = asset_entity[\"data\"][\"tasks\"].get(task, {}).get(\"type\")\n            project_task_types = project_entity[\"config\"][\"tasks\"]\n            task_code = project_task_types.get(task_type, {}).get(\"short_name\")\n\n            # updating hierarchy data\n            anatomy_data_new.update({\n                \"asset\": asset_data[\"name\"],\n                \"folder\": {\n                    \"name\": asset_data[\"name\"],\n                },\n                \"task\": {\n                    \"name\": task,\n                    \"type\": task_type,\n                    \"short\": task_code,\n                },\n                \"subset\": subset_name\n            })\n\n            new_instance.data[\"label\"] = f\"{instance_name}\"\n            new_instance.data[\"subset\"] = subset_name\n            new_instance.data[\"extension\"] = \".zip\"\n            new_instance.data[\"anatomyData\"] = anatomy_data_new\n            new_instance.data[\"publish\"] = True\n\n            # When a project folder was dropped vs. just an xstage file, find\n            # the latest file xstage version and update the instance\n            if not files.endswith(\".xstage\"):\n\n                source_dir = os.path.join(\n                    staging_dir, files\n                ).replace(\"\\\\\", \"/\")\n\n                latest_file = max(glob.iglob(source_dir + \"/*.xstage\"),\n                                  key=os.path.getctime).replace(\"\\\\\", \"/\")\n\n                new_instance.data[\"representations\"][0][\"stagingDir\"] = (\n                    source_dir\n                )\n                new_instance.data[\"representations\"][0][\"files\"] = (\n                    os.path.basename(latest_file)\n                )\n            self.log.info(f\"Created new instance: {instance_name}\")\n            self.log.debug(f\"_ inst_data: {pformat(new_instance.data)}\")\n\n        # set original instance for removal\n        self.log.info(\"Context data: {}\".format(context.data))\n        instance.data[\"remove\"] = True\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect zips as Harmony scene files.\"\"\"\nimport copy\nfrom pprint import pformat\n\nimport pyblish.api\n\n\nclass CollectHarmonyZips(pyblish.api.InstancePlugin):\n    \"\"\"Collect Harmony zipped projects.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.497\n    label = \"Collect Harmony Zipped Projects\"\n    hosts = [\"standalonepublisher\"]\n    families = [\"harmony.scene\"]\n    extensions = [\"zip\"]\n\n    # presets\n    ignored_instance_data_keys = (\"name\", \"label\", \"stagingDir\", \"version\")\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        context = instance.context\n        asset_data = instance.context.data[\"assetEntity\"]\n        asset_name = instance.data[\"asset\"]\n        subset_name = instance.data.get(\"subset\", \"sceneMain\")\n        anatomy_data = instance.context.data[\"anatomyData\"]\n        repres = instance.data[\"representations\"]\n        files = repres[0][\"files\"]\n        project_entity = context.data[\"projectEntity\"]\n\n        if files.endswith(\".zip\"):\n            # A zip file was dropped\n            instance_name = f\"{asset_name}_{subset_name}\"\n            task = instance.data.get(\"task\", \"harmonyIngest\")\n\n            # create new instance\n            new_instance = context.create_instance(instance_name)\n\n            # add original instance data except name key\n            for key, value in instance.data.items():\n                # Make sure value is copy since value may be object which\n                # can be shared across all new created objects\n                if key not in self.ignored_instance_data_keys:\n                    new_instance.data[key] = copy.deepcopy(value)\n\n            self.log.info(\"Copied data: {}\".format(new_instance.data))\n\n            task_type = asset_data[\"data\"][\"tasks\"].get(task, {}).get(\"type\")\n            project_task_types = project_entity[\"config\"][\"tasks\"]\n            task_code = project_task_types.get(task_type, {}).get(\"short_name\")\n\n            # fix anatomy data\n            anatomy_data_new = copy.deepcopy(anatomy_data)\n            # updating hierarchy data\n            anatomy_data_new.update(\n                {\n                    \"asset\": asset_data[\"name\"],\n                    \"folder\": {\n                        \"name\": asset_data[\"name\"],\n                    },\n                    \"task\": {\n                        \"name\": task,\n                        \"type\": task_type,\n                        \"short\": task_code,\n                    },\n                    \"subset\": subset_name\n                }\n            )\n\n            new_instance.data[\"label\"] = f\"{instance_name}\"\n            new_instance.data[\"subset\"] = subset_name\n            new_instance.data[\"extension\"] = \".zip\"\n            new_instance.data[\"anatomyData\"] = anatomy_data_new\n            new_instance.data[\"publish\"] = True\n\n            self.log.info(f\"Created new instance: {instance_name}\")\n            self.log.debug(f\"_ inst_data: {pformat(new_instance.data)}\")\n\n        # set original instance for removal\n        self.log.info(\"Context data: {}\".format(context.data))\n        instance.data[\"remove\"] = True\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py",
    "content": "import os\nfrom pprint import pformat\nimport re\nfrom copy import deepcopy\nimport pyblish.api\n\nfrom openpype.client import get_asset_by_id\n\n\nclass CollectHierarchyInstance(pyblish.api.ContextPlugin):\n    \"\"\"Collecting hierarchy context from `parents` and `hierarchy` data\n    present in `clip` family instances coming from the request json data file\n\n    It will add `hierarchical_context` into each instance for integrate\n    plugins to be able to create needed parents for the context if they\n    don't exist yet\n    \"\"\"\n\n    label = \"Collect Hierarchy Clip\"\n    order = pyblish.api.CollectorOrder + 0.101\n    hosts = [\"standalonepublisher\"]\n    families = [\"shot\"]\n\n    # presets\n    shot_rename = True\n    shot_rename_template = None\n    shot_rename_search_patterns = None\n    shot_add_hierarchy = None\n    shot_add_tasks = None\n\n    def convert_to_entity(self, key, value):\n        # ftrack compatible entity types\n        types = {\"shot\": \"Shot\",\n                 \"folder\": \"Folder\",\n                 \"episode\": \"Episode\",\n                 \"sequence\": \"Sequence\",\n                 \"track\": \"Sequence\",\n                 }\n        # convert to entity type\n        entity_type = types.get(key, None)\n\n        # return if any\n        if entity_type:\n            return {\"entity_type\": entity_type, \"entity_name\": value}\n\n    def rename_with_hierarchy(self, instance):\n        search_text = \"\"\n        parent_name = instance.context.data[\"assetEntity\"][\"name\"]\n        clip = instance.data[\"item\"]\n        clip_name = os.path.splitext(clip.name)[0].lower()\n        if self.shot_rename_search_patterns and self.shot_rename:\n            search_text += parent_name + clip_name\n            instance.data[\"anatomyData\"].update({\"clip_name\": clip_name})\n            for type, pattern in self.shot_rename_search_patterns.items():\n                p = re.compile(pattern)\n                match = p.findall(search_text)\n                if not match:\n                    continue\n                instance.data[\"anatomyData\"][type] = match[-1]\n\n            # format to new shot name\n            instance.data[\"asset\"] = self.shot_rename_template.format(\n                **instance.data[\"anatomyData\"])\n\n    def create_hierarchy(self, instance):\n        asset_doc = instance.context.data[\"assetEntity\"]\n        project_doc = instance.context.data[\"projectEntity\"]\n        project_name = project_doc[\"name\"]\n        visual_hierarchy = [asset_doc]\n        current_doc = asset_doc\n        while True:\n            visual_parent_id = current_doc[\"data\"][\"visualParent\"]\n            visual_parent = None\n            if visual_parent_id:\n                visual_parent = get_asset_by_id(project_name, visual_parent_id)\n\n            if not visual_parent:\n                visual_hierarchy.append(project_doc)\n                break\n            visual_hierarchy.append(visual_parent)\n            current_doc = visual_parent\n\n        # add current selection context hierarchy from standalonepublisher\n        parents = list()\n        for entity in reversed(visual_hierarchy):\n            parents.append({\n                \"entity_type\": entity[\"data\"][\"entityType\"],\n                \"entity_name\": entity[\"name\"]\n            })\n\n        hierarchy = list()\n        if self.shot_add_hierarchy.get(\"enabled\"):\n            parent_template_patern = re.compile(r\"\\{([a-z]*?)\\}\")\n            # fill the parents parts from presets\n            shot_add_hierarchy = self.shot_add_hierarchy.copy()\n            hierarchy_parents = shot_add_hierarchy[\"parents\"].copy()\n\n            # fill parent keys data template from anatomy data\n            for parent_key in hierarchy_parents:\n                hierarchy_parents[parent_key] = hierarchy_parents[\n                    parent_key].format(**instance.data[\"anatomyData\"])\n\n            for _index, _parent in enumerate(\n                    shot_add_hierarchy[\"parents_path\"].split(\"/\")):\n                parent_filled = _parent.format(**hierarchy_parents)\n                parent_key = parent_template_patern.findall(_parent).pop()\n\n                # in case SP context is set to the same folder\n                if (_index == 0) and (\"folder\" in parent_key) \\\n                        and (parents[-1][\"entity_name\"] == parent_filled):\n                    self.log.debug(f\" skipping : {parent_filled}\")\n                    continue\n\n                # in case first parent is project then start parents from start\n                if (_index == 0) and (\"project\" in parent_key):\n                    self.log.debug(\"rebuilding parents from scratch\")\n                    project_parent = parents[0]\n                    parents = [project_parent]\n                    self.log.debug(f\"project_parent: {project_parent}\")\n                    self.log.debug(f\"parents: {parents}\")\n                    continue\n\n                prnt = self.convert_to_entity(\n                    parent_key, parent_filled)\n                parents.append(prnt)\n                hierarchy.append(parent_filled)\n\n        # convert hierarchy to string\n        hierarchy = \"/\".join(hierarchy)\n\n        # assign to instance data\n        instance.data[\"hierarchy\"] = hierarchy\n        instance.data[\"parents\"] = parents\n\n        # print\n        self.log.warning(f\"Hierarchy: {hierarchy}\")\n        self.log.info(f\"parents: {parents}\")\n\n        tasks_to_add = dict()\n        if self.shot_add_tasks:\n            project_tasks = project_doc[\"config\"][\"tasks\"]\n            for task_name, task_data in self.shot_add_tasks.items():\n                _task_data = deepcopy(task_data)\n\n                # fixing enumerator from settings\n                _task_data[\"type\"] = task_data[\"type\"][0]\n\n                # check if task type in project task types\n                if _task_data[\"type\"] in project_tasks.keys():\n                    tasks_to_add.update({task_name: _task_data})\n                else:\n                    raise KeyError(\n                        \"Wrong FtrackTaskType `{}` for `{}` is not\"\n                        \" existing in `{}``\".format(\n                            _task_data[\"type\"],\n                            task_name,\n                            list(project_tasks.keys())))\n\n        instance.data[\"tasks\"] = tasks_to_add\n\n        # updating hierarchy data\n        instance.data[\"anatomyData\"].update({\n            \"asset\": instance.data[\"asset\"],\n            \"task\": \"conform\"\n        })\n\n    def process(self, context):\n        self.log.info(\"self.shot_add_hierarchy: {}\".format(\n            pformat(self.shot_add_hierarchy)\n        ))\n        for instance in context:\n            if instance.data[\"family\"] in self.families:\n                self.processing_instance(instance)\n\n    def processing_instance(self, instance):\n        self.log.info(f\"_ instance: {instance}\")\n        # adding anatomyData for burnins\n        instance.data[\"anatomyData\"] = deepcopy(\n            instance.context.data[\"anatomyData\"])\n\n        asset = instance.data[\"asset\"]\n        assets_shared = instance.context.data.get(\"assetsShared\")\n\n        frame_start = instance.data[\"frameStart\"]\n        frame_end = instance.data[\"frameEnd\"]\n\n        if self.shot_rename_template:\n            self.rename_with_hierarchy(instance)\n\n        self.create_hierarchy(instance)\n\n        shot_name = instance.data[\"asset\"]\n        self.log.debug(f\"Shot Name: {shot_name}\")\n\n        label = f\"{shot_name} ({frame_start}-{frame_end})\"\n        instance.data[\"label\"] = label\n\n        # dealing with shared attributes across instances\n        # with the same asset name\n        if assets_shared.get(asset):\n            asset_shared = assets_shared.get(asset)\n        else:\n            asset_shared = assets_shared[asset]\n\n        asset_shared.update({\n            \"asset\": instance.data[\"asset\"],\n            \"hierarchy\": instance.data[\"hierarchy\"],\n            \"parents\": instance.data[\"parents\"],\n            \"tasks\": instance.data[\"tasks\"],\n            \"anatomyData\": instance.data[\"anatomyData\"]\n        })\n\n\nclass CollectHierarchyContext(pyblish.api.ContextPlugin):\n    '''Collecting Hierarchy from instances and building\n    context hierarchy tree\n    '''\n\n    label = \"Collect Hierarchy Context\"\n    order = pyblish.api.CollectorOrder + 0.102\n    hosts = [\"standalonepublisher\"]\n    families = [\"shot\"]\n\n    def update_dict(self, ex_dict, new_dict):\n        for key in ex_dict:\n            if key in new_dict and isinstance(ex_dict[key], dict):\n                new_dict[key] = self.update_dict(ex_dict[key], new_dict[key])\n            else:\n                if ex_dict.get(key) and new_dict.get(key):\n                    continue\n                else:\n                    new_dict[key] = ex_dict[key]\n\n        return new_dict\n\n    def process(self, context):\n        instances = context\n        # create hierarchyContext attr if context has none\n        assets_shared = context.data.get(\"assetsShared\")\n        final_context = {}\n        for instance in instances:\n            if 'editorial' in instance.data.get('family', ''):\n                continue\n            # inject assetsShared to other instances with\n            # the same `assetShareName` attribute in data\n            asset_shared_name = instance.data.get(\"assetShareName\")\n\n            s_asset_data = assets_shared.get(asset_shared_name)\n            if s_asset_data:\n                instance.data[\"asset\"] = s_asset_data[\"asset\"]\n                instance.data[\"parents\"] = s_asset_data[\"parents\"]\n                instance.data[\"hierarchy\"] = s_asset_data[\"hierarchy\"]\n                instance.data[\"tasks\"] = s_asset_data[\"tasks\"]\n                instance.data[\"anatomyData\"] = s_asset_data[\"anatomyData\"]\n\n            # generate hierarchy data only on shot instances\n            if 'shot' not in instance.data.get('family', ''):\n                continue\n\n            # get handles\n            handle_start = int(instance.data[\"handleStart\"])\n            handle_end = int(instance.data[\"handleEnd\"])\n\n            in_info = {}\n\n            # suppose that all instances are Shots\n            in_info['entity_type'] = 'Shot'\n\n            # get custom attributes of the shot\n\n            in_info['custom_attributes'] = {\n                \"handleStart\": handle_start,\n                \"handleEnd\": handle_end,\n                \"frameStart\": instance.data[\"frameStart\"],\n                \"frameEnd\": instance.data[\"frameEnd\"],\n                \"clipIn\": instance.data[\"clipIn\"],\n                \"clipOut\": instance.data[\"clipOut\"],\n                'fps': instance.data[\"fps\"]\n            }\n\n            in_info['tasks'] = instance.data['tasks']\n\n            from pprint import pformat\n            parents = instance.data.get('parents', [])\n            self.log.debug(f\"parents: {pformat(parents)}\")\n\n            # Split by '/' for AYON where asset is a path\n            name = instance.data[\"asset\"].split(\"/\")[-1]\n            actual = {name: in_info}\n\n            for parent in reversed(parents):\n                next_dict = {}\n                parent_name = parent[\"entity_name\"]\n                next_dict[parent_name] = {}\n                next_dict[parent_name][\"entity_type\"] = parent[\"entity_type\"]\n                next_dict[parent_name][\"childs\"] = actual\n                actual = next_dict\n\n            final_context = self.update_dict(final_context, actual)\n\n        # adding hierarchy context to instance\n        context.data[\"hierarchyContext\"] = final_context\n        self.log.debug(f\"hierarchyContext: {pformat(final_context)}\")\n        self.log.info(\"Hierarchy instance collected\")\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_instance_data.py",
    "content": "\"\"\"\nRequires:\n    Nothing\n\nProvides:\n    Instance\n\"\"\"\n\nimport pyblish.api\nfrom pprint import pformat\n\n\nclass CollectInstanceData(pyblish.api.InstancePlugin):\n    \"\"\"\n    Collector with only one reason for its existence - remove 'ftrack'\n    family implicitly added by Standalone Publisher\n    \"\"\"\n\n    label = \"Collect instance data\"\n    order = pyblish.api.CollectorOrder + 0.49\n    families = [\"render\", \"plate\", \"review\"]\n    hosts = [\"standalonepublisher\"]\n\n    def process(self, instance):\n        fps = instance.context.data[\"fps\"]\n\n        instance.data.update({\n            \"fps\": fps\n        })\n        self.log.debug(f\"instance.data: {pformat(instance.data)}\")\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_matching_asset.py",
    "content": "import os\nimport re\nimport collections\nimport pyblish.api\nfrom pprint import pformat\n\nfrom openpype.client import get_assets\n\n\nclass CollectMatchingAssetToInstance(pyblish.api.InstancePlugin):\n    \"\"\"\n    Collecting temp json data sent from a host context\n    and path for returning json data back to hostself.\n    \"\"\"\n\n    label = \"Collect Matching Asset to Instance\"\n    order = pyblish.api.CollectorOrder - 0.05\n    hosts = [\"standalonepublisher\"]\n    families = [\"background_batch\", \"render_mov_batch\"]\n\n    # Version regex to parse asset name and version from filename\n    version_regex = re.compile(r\"^(.+)_v([0-9]+)$\")\n\n    def process(self, instance):\n        source_filename = self.get_source_filename(instance)\n        self.log.info(\"Looking for asset document for file \\\"{}\\\"\".format(\n            source_filename\n        ))\n        asset_name = os.path.splitext(source_filename)[0].lower()\n\n        asset_docs_by_name = self.selection_children_by_name(instance)\n\n        version_number = None\n        # Always first check if source filename is in assets\n        matching_asset_doc = asset_docs_by_name.get(asset_name)\n        if matching_asset_doc is None:\n            # Check if source file contain version in name\n            self.log.debug((\n                \"Asset doc by \\\"{}\\\" was not found trying version regex.\"\n            ).format(asset_name))\n            regex_result = self.version_regex.findall(asset_name)\n            if regex_result:\n                _asset_name, _version_number = regex_result[0]\n                matching_asset_doc = asset_docs_by_name.get(_asset_name)\n                if matching_asset_doc:\n                    version_number = int(_version_number)\n\n        if matching_asset_doc is None:\n            for asset_name_low, asset_doc in asset_docs_by_name.items():\n                if asset_name_low in asset_name:\n                    matching_asset_doc = asset_doc\n                    break\n\n        if not matching_asset_doc:\n            self.log.debug(\"Available asset names {}\".format(\n                str(list(asset_docs_by_name.keys()))\n            ))\n            # TODO better error message\n            raise AssertionError((\n                \"Filename \\\"{}\\\" does not match\"\n                \" any name of asset documents in database for your selection.\"\n            ).format(source_filename))\n\n        instance.data[\"asset\"] = matching_asset_doc[\"name\"]\n        instance.data[\"assetEntity\"] = matching_asset_doc\n        if version_number is not None:\n            instance.data[\"version\"] = version_number\n\n        self.log.info(\n            f\"Matching asset found: {pformat(matching_asset_doc)}\"\n        )\n\n    def get_source_filename(self, instance):\n        if instance.data[\"family\"] == \"background_batch\":\n            return os.path.basename(instance.data[\"source\"])\n\n        if len(instance.data[\"representations\"]) != 1:\n            raise ValueError((\n                \"Implementation bug: Instance data contain\"\n                \" more than one representation.\"\n            ))\n\n        repre = instance.data[\"representations\"][0]\n        repre_files = repre[\"files\"]\n        if not isinstance(repre_files, str):\n            raise ValueError((\n                \"Implementation bug: Instance's representation contain\"\n                \" unexpected value (expected single file). {}\"\n            ).format(str(repre_files)))\n        return repre_files\n\n    def selection_children_by_name(self, instance):\n        storing_key = \"childrenDocsForSelection\"\n\n        children_docs = instance.context.data.get(storing_key)\n        if children_docs is None:\n            top_asset_doc = instance.context.data[\"assetEntity\"]\n            assets_by_parent_id = self._asset_docs_by_parent_id(instance)\n            _children_docs = self._children_docs(\n                assets_by_parent_id, top_asset_doc\n            )\n            children_docs = {\n                children_doc[\"name\"].lower(): children_doc\n                for children_doc in _children_docs\n            }\n            instance.context.data[storing_key] = children_docs\n        return children_docs\n\n    def _children_docs(self, documents_by_parent_id, parent_doc):\n        # Find all children in reverse order, last children is at first place.\n        output = []\n        children = documents_by_parent_id.get(parent_doc[\"_id\"]) or tuple()\n        for child in children:\n            output.extend(\n                self._children_docs(documents_by_parent_id, child)\n            )\n        output.append(parent_doc)\n        return output\n\n    def _asset_docs_by_parent_id(self, instance):\n        # Query all assets for project and store them by parent's id to list\n        project_name = instance.context.data[\"projectEntity\"][\"name\"]\n        asset_docs_by_parent_id = collections.defaultdict(list)\n        for asset_doc in get_assets(project_name):\n            parent_id = asset_doc[\"data\"][\"visualParent\"]\n            asset_docs_by_parent_id[parent_id].append(asset_doc)\n        return asset_docs_by_parent_id\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_remove_marked.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect instances that are marked for removal and remove them.\"\"\"\nimport pyblish.api\n\n\nclass CollectRemoveMarked(pyblish.api.ContextPlugin):\n    \"\"\"Clean up instances marked for removal.\n\n    Note:\n        This is a workaround for race conditions and removing of instances\n        used to generate other instances.\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.499\n    label = 'Remove Marked Instances'\n\n    def process(self, context):\n        \"\"\"Plugin entry point.\"\"\"\n        for instance in context:\n            if instance.data.get('remove'):\n                context.remove(instance)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_representation_names.py",
    "content": "import re\nimport os\nimport pyblish.api\n\n\nclass CollectRepresentationNames(pyblish.api.InstancePlugin):\n    \"\"\"\n    Sets the representation names for given families based on RegEx filter\n    \"\"\"\n\n    label = \"Collect Representation Names\"\n    order = pyblish.api.CollectorOrder\n    families = []\n    hosts = [\"standalonepublisher\"]\n    name_filter = \"\"\n\n    def process(self, instance):\n        for repre in instance.data['representations']:\n            new_repre_name = None\n            if isinstance(repre['files'], list):\n                shortened_name = os.path.splitext(repre['files'][0])[0]\n                new_repre_name = re.search(self.name_filter,\n                                           shortened_name).group()\n            else:\n                new_repre_name = re.search(self.name_filter,\n                                           repre['files']).group()\n\n            if new_repre_name:\n                repre['name'] = new_repre_name\n\n            repre['outputName'] = repre['name']\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py",
    "content": "import os\nimport re\nimport pyblish.api\nimport json\n\nfrom openpype.lib import (\n    prepare_template_data,\n    StringTemplate,\n)\n\n\nclass CollectTextures(pyblish.api.ContextPlugin):\n    \"\"\"Collect workfile (and its resource_files) and textures.\n\n        Currently implements use case with Mari and Substance Painter, where\n        one workfile is main (.mra - Mari) with possible additional workfiles\n        (.spp - Substance)\n\n\n        Provides:\n            1 instance per workfile (with 'resources' filled if needed)\n                (workfile family)\n            1 instance per group of textures\n                (textures family)\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = \"Collect Textures\"\n    hosts = [\"standalonepublisher\"]\n    families = [\"texture_batch\"]\n    actions = []\n\n    # from presets\n    main_workfile_extensions = ['mra']\n    other_workfile_extensions = ['spp', 'psd']\n    texture_extensions = [\"exr\", \"dpx\", \"jpg\", \"jpeg\", \"png\", \"tiff\", \"tga\",\n                          \"gif\", \"svg\"]\n\n    # additional families (ftrack etc.)\n    workfile_families = []\n    textures_families = []\n\n    color_space = [\"linsRGB\", \"raw\", \"acesg\"]\n\n    # currently implemented placeholders [\"color_space\"]\n    # describing patterns in file names splitted by regex groups\n    input_naming_patterns = {\n        # workfile: corridorMain_v001.mra >\n        # texture: corridorMain_aluminiumID_v001_baseColor_linsRGB_1001.exr\n        \"workfile\": r'^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+',\n        \"textures\": r'^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+', # noqa\n    }\n    # matching regex group position to 'input_naming_patterns'\n    input_naming_groups = {\n        \"workfile\": ('asset', 'filler', 'version'),\n        \"textures\": ('asset', 'shader', 'version', 'channel', 'color_space',\n                     'udim')\n    }\n\n    workfile_subset_template = \"textures{Subset}Workfile\"\n    # implemented keys: [\"color_space\", \"channel\", \"subset\", \"shader\"]\n    texture_subset_template = \"textures{Subset}_{Shader}_{Channel}\"\n\n    def process(self, context):\n        self.context = context\n\n        resource_files = {}\n        workfile_files = {}\n        representations = {}\n        version_data = {}\n        asset_builds = set()\n        asset = None\n        for instance in context:\n            if not self.input_naming_patterns:\n                raise ValueError(\"Naming patterns are not configured. \\n\"\n                                 \"Ask admin to provide naming conventions \"\n                                 \"for workfiles and textures.\")\n\n            if not asset:\n                asset = instance.data[\"asset\"]  # selected from SP\n\n            parsed_subset = instance.data[\"subset\"].replace(\n                instance.data[\"family\"], '')\n\n            explicit_data = {\n                \"subset\": parsed_subset\n            }\n\n            processed_instance = False\n            for repre in instance.data[\"representations\"]:\n                ext = repre[\"ext\"].replace('.', '')\n                asset_build = version = None\n\n                if isinstance(repre[\"files\"], list):\n                    repre_file = repre[\"files\"][0]\n                else:\n                    repre_file = repre[\"files\"]\n\n                if ext in self.main_workfile_extensions or \\\n                        ext in self.other_workfile_extensions:\n\n                    formatting_data = self._get_parsed_groups(\n                        repre_file,\n                        self.input_naming_patterns[\"workfile\"],\n                        self.input_naming_groups[\"workfile\"],\n                        self.color_space\n                    )\n                    self.log.info(\"Parsed groups from workfile \"\n                                  \"name '{}': {}\".format(repre_file,\n                                                         formatting_data))\n\n                    formatting_data.update(explicit_data)\n                    fill_pairs = prepare_template_data(formatting_data)\n                    workfile_subset = StringTemplate.format_strict_template(\n                        self.workfile_subset_template, fill_pairs\n                    )\n\n                    asset_build = self._get_asset_build(\n                        repre_file,\n                        self.input_naming_patterns[\"workfile\"],\n                        self.input_naming_groups[\"workfile\"],\n                        self.color_space\n                    )\n                    version = self._get_version(\n                        repre_file,\n                        self.input_naming_patterns[\"workfile\"],\n                        self.input_naming_groups[\"workfile\"],\n                        self.color_space\n                    )\n                    asset_builds.add((asset_build, version,\n                                      workfile_subset, 'workfile'))\n                    processed_instance = True\n\n                    if not representations.get(workfile_subset):\n                        representations[workfile_subset] = []\n\n                if ext in self.main_workfile_extensions:\n                    # workfiles can have only single representation\n                    # currently OP is not supporting different extensions in\n                    # representation files\n                    representations[workfile_subset] = [repre]\n\n                    workfile_files[asset_build] = repre_file\n\n                if ext in self.other_workfile_extensions:\n                    # add only if not added already from main\n                    if not representations.get(workfile_subset):\n                        representations[workfile_subset] = [repre]\n\n                    # only overwrite if not present\n                    if not workfile_files.get(asset_build):\n                        workfile_files[asset_build] = repre_file\n\n                    if not resource_files.get(workfile_subset):\n                        resource_files[workfile_subset] = []\n                    item = {\n                        \"files\": [os.path.join(repre[\"stagingDir\"],\n                                               repre[\"files\"])],\n                        \"source\": \"standalone publisher\"\n                    }\n                    resource_files[workfile_subset].append(item)\n\n                if ext in self.texture_extensions:\n                    formatting_data = self._get_parsed_groups(\n                        repre_file,\n                        self.input_naming_patterns[\"textures\"],\n                        self.input_naming_groups[\"textures\"],\n                        self.color_space\n                    )\n\n                    self.log.info(\"Parsed groups from texture \"\n                                  \"name '{}': {}\".format(repre_file,\n                                                         formatting_data))\n\n                    c_space = self._get_color_space(\n                        repre_file,\n                        self.color_space\n                    )\n\n                    # optional value\n                    channel = self._get_channel_name(\n                        repre_file,\n                        self.input_naming_patterns[\"textures\"],\n                        self.input_naming_groups[\"textures\"],\n                        self.color_space\n                    )\n\n                    # optional value\n                    shader = self._get_shader_name(\n                        repre_file,\n                        self.input_naming_patterns[\"textures\"],\n                        self.input_naming_groups[\"textures\"],\n                        self.color_space\n                    )\n\n                    explicit_data = {\n                        \"color_space\": c_space or '',  # None throws exception\n                        \"channel\": channel or '',\n                        \"shader\": shader or '',\n                        \"subset\": parsed_subset or ''\n                    }\n\n                    formatting_data.update(explicit_data)\n\n                    fill_pairs = prepare_template_data(formatting_data)\n                    subset = StringTemplate.format_strict_template(\n                        self.texture_subset_template, fill_pairs\n                    )\n\n                    asset_build = self._get_asset_build(\n                        repre_file,\n                        self.input_naming_patterns[\"textures\"],\n                        self.input_naming_groups[\"textures\"],\n                        self.color_space\n                    )\n                    version = self._get_version(\n                        repre_file,\n                        self.input_naming_patterns[\"textures\"],\n                        self.input_naming_groups[\"textures\"],\n                        self.color_space\n                    )\n                    if not representations.get(subset):\n                        representations[subset] = []\n                    representations[subset].append(repre)\n\n                    ver_data = {\n                        \"color_space\": c_space or '',\n                        \"channel_name\": channel or '',\n                        \"shader_name\": shader or ''\n                    }\n                    version_data[subset] = ver_data\n\n                    asset_builds.add(\n                        (asset_build, version, subset, \"textures\"))\n                    processed_instance = True\n\n            if processed_instance:\n                self.context.remove(instance)\n\n        self._create_new_instances(context,\n                                   asset,\n                                   asset_builds,\n                                   resource_files,\n                                   representations,\n                                   version_data,\n                                   workfile_files)\n\n    def _create_new_instances(self, context, asset, asset_builds,\n                              resource_files, representations,\n                              version_data, workfile_files):\n        \"\"\"Prepare new instances from collected data.\n\n            Args:\n                context (ContextPlugin)\n                asset (string): selected asset from SP\n                asset_builds (set) of tuples\n                    (asset_build, version, subset, family)\n                resource_files (list) of resource dicts - to store additional\n                  files to main workfile\n                representations (list) of dicts - to store workfile info OR\n                   all collected texture files, key is asset_build\n                version_data (dict) - prepared to store into version doc in DB\n                workfile_files (dict) - to store workfile to add to textures\n                    key is asset_build\n        \"\"\"\n        # sort workfile first\n        asset_builds = sorted(asset_builds,\n                              key=lambda tup: tup[3], reverse=True)\n\n        # workfile must have version, textures might\n        main_version = None\n        for asset_build, version, subset, family in asset_builds:\n            if not main_version:\n                main_version = version\n\n            try:\n                version_int = int(version or main_version or 1)\n            except ValueError:\n                self.log.error(\"Parsed version {} is not \"\n                               \"an number\".format(version))\n\n            new_instance = context.create_instance(subset)\n            new_instance.data.update(\n                {\n                    \"subset\": subset,\n                    \"asset\": asset,\n                    \"label\": subset,\n                    \"name\": subset,\n                    \"family\": family,\n                    \"version\": version_int,\n                    \"asset_build\": asset_build  # remove in validator\n                }\n            )\n\n            workfile = workfile_files.get(asset_build)\n\n            if resource_files.get(subset):\n                # add resources only when workfile is main style\n                for ext in self.main_workfile_extensions:\n                    if ext in workfile:\n                        new_instance.data.update({\n                            \"resources\": resource_files.get(subset)\n                        })\n                        break\n\n            # store origin\n            if family == 'workfile':\n                families = self.workfile_families\n                families.append(\"texture_batch_workfile\")\n\n                new_instance.data[\"source\"] = \"standalone publisher\"\n            else:\n                families = self.textures_families\n\n                repre = representations.get(subset)[0]\n                new_instance.context.data[\"currentFile\"] = os.path.join(\n                    repre[\"stagingDir\"], workfile or 'dummy.txt')\n\n            new_instance.data[\"families\"] = families\n\n            # add data for version document\n            ver_data = version_data.get(subset)\n            if ver_data:\n                if workfile:\n                    ver_data['workfile'] = workfile\n\n                new_instance.data.update(\n                    {\"versionData\": ver_data}\n                )\n\n            upd_representations = representations.get(subset)\n            if upd_representations and family != 'workfile':\n                upd_representations = self._update_representations(\n                    upd_representations)\n\n            new_instance.data[\"representations\"] = upd_representations\n\n            self.log.debug(\"new instance - {}:: {}\".format(\n                family,\n                json.dumps(new_instance.data, indent=4)))\n\n    def _get_asset_build(self, name,\n                         input_naming_patterns, input_naming_groups,\n                         color_spaces):\n        \"\"\"Loops through configured workfile patterns to find asset name.\n\n            Asset name used to bind workfile and its textures.\n\n            Args:\n                name (str): workfile name\n                input_naming_patterns (list):\n                    [workfile_pattern] or [texture_pattern]\n                input_naming_groups (list)\n                    ordinal position of regex groups matching to input_naming..\n                color_spaces (list) - predefined color spaces\n        \"\"\"\n        asset_name = \"NOT_AVAIL\"\n\n        return (self._parse_key(name, input_naming_patterns,\n                                input_naming_groups, color_spaces, 'asset') or\n                asset_name)\n\n    def _get_version(self, name, input_naming_patterns, input_naming_groups,\n                     color_spaces):\n        found = self._parse_key(name, input_naming_patterns,\n                                input_naming_groups, color_spaces, 'version')\n\n        if found:\n            return found.replace('v', '')\n\n        self.log.info(\"No version found in the name {}\".format(name))\n\n    def _get_udim(self, name, input_naming_patterns, input_naming_groups,\n                  color_spaces):\n        \"\"\"Parses from 'name' udim value.\"\"\"\n        found = self._parse_key(name, input_naming_patterns,\n                                input_naming_groups, color_spaces, 'udim')\n        if found:\n            return found\n\n        self.log.warning(\"Didn't find UDIM in {}\".format(name))\n\n    def _get_color_space(self, name, color_spaces):\n        \"\"\"Looks for color_space from a list in a file name.\n\n            Color space seems not to be recognizable by regex pattern, set of\n            known space spaces must be provided.\n        \"\"\"\n        color_space = None\n        found = [cs for cs in color_spaces if\n                 re.search(\"_{}_\".format(cs), name)]\n\n        if not found:\n            self.log.warning(\"No color space found in {}\".format(name))\n        else:\n            if len(found) > 1:\n                msg = \"Multiple color spaces found in {}->{}\".format(name,\n                                                                     found)\n                self.log.warning(msg)\n\n            color_space = found[0]\n\n        return color_space\n\n    def _get_shader_name(self, name, input_naming_patterns,\n                         input_naming_groups, color_spaces):\n        \"\"\"Return parsed shader name.\n\n            Shader name is needed for overlapping udims (eg. udims might be\n            used for different materials, shader needed to not overwrite).\n\n            Unknown format of channel name and color spaces >> cs are known\n            list - 'color_space' used as a placeholder\n        \"\"\"\n        found = None\n        try:\n            found = self._parse_key(name, input_naming_patterns,\n                                    input_naming_groups, color_spaces,\n                                    'shader')\n        except ValueError:\n            self.log.warning(\"Didn't find shader in {}\".format(name))\n\n        return found\n\n    def _get_channel_name(self, name, input_naming_patterns,\n                          input_naming_groups, color_spaces):\n        \"\"\"Return parsed channel name.\n\n            Unknown format of channel name and color spaces >> cs are known\n            list - 'color_space' used as a placeholder\n        \"\"\"\n        found = None\n        try:\n            found = self._parse_key(name, input_naming_patterns,\n                                    input_naming_groups, color_spaces,\n                                    'channel')\n        except ValueError:\n            self.log.warning(\"Didn't find channel in {}\".format(name))\n\n        return found\n\n    def _parse_key(self, name, input_naming_patterns, input_naming_groups,\n                   color_spaces, key):\n        \"\"\"Universal way to parse 'name' with configurable regex groups.\n\n            Args:\n                name (str): workfile name\n                input_naming_patterns (list):\n                    [workfile_pattern] or [texture_pattern]\n                input_naming_groups (list)\n                    ordinal position of regex groups matching to input_naming..\n                color_spaces (list) - predefined color spaces\n\n            Raises:\n                ValueError - if broken 'input_naming_groups'\n        \"\"\"\n        parsed_groups = self._get_parsed_groups(name,\n                                                input_naming_patterns,\n                                                input_naming_groups,\n                                                color_spaces)\n\n        try:\n            parsed_value = parsed_groups[key]\n            return parsed_value\n        except (IndexError, KeyError):\n            msg = (\"'Textures group positions' must \" +\n                   \"have '{}' key\".format(key))\n            raise ValueError(msg)\n\n    def _get_parsed_groups(self, name, input_naming_patterns,\n                           input_naming_groups, color_spaces):\n        \"\"\"Universal way to parse 'name' with configurable regex groups.\n\n        Args:\n            name (str): workfile name or texture name\n            input_naming_patterns (list):\n                [workfile_pattern] or [texture_pattern]\n            input_naming_groups (list)\n                ordinal position of regex groups matching to input_naming..\n            color_spaces (list) - predefined color spaces\n\n        Returns:\n            (dict) {group_name:parsed_value}\n        \"\"\"\n        for input_pattern in input_naming_patterns:\n            for cs in color_spaces:\n                pattern = input_pattern.replace('{color_space}', cs)\n                regex_result = re.findall(pattern, name)\n                if regex_result:\n                    if len(regex_result[0]) == len(input_naming_groups):\n                        return dict(zip(input_naming_groups, regex_result[0]))\n                    else:\n                        self.log.warning(\"No of parsed groups doesn't match \"\n                                         \"no of group labels\")\n\n        raise ValueError(\"Name '{}' cannot be parsed by any \"\n                         \"'{}' patterns\".format(name, input_naming_patterns))\n\n    def _update_representations(self, upd_representations):\n        \"\"\"Frames dont have sense for textures, add collected udims instead.\"\"\"\n        udims = []\n        for repre in upd_representations:\n            repre.pop(\"frameStart\", None)\n            repre.pop(\"frameEnd\", None)\n            repre.pop(\"fps\", None)\n\n            # ignore unique name from SP, use extension instead\n            # SP enforces unique name, here different subsets >> unique repres\n            repre[\"name\"] = repre[\"ext\"].replace('.', '')\n\n            files = repre.get(\"files\", [])\n            if not isinstance(files, list):\n                files = [files]\n\n            for file_name in files:\n                udim = self._get_udim(file_name,\n                                      self.input_naming_patterns[\"textures\"],\n                                      self.input_naming_groups[\"textures\"],\n                                      self.color_space)\n                udims.append(udim)\n\n            repre[\"udim\"] = udims  # must be this way, used for filling path\n\n        return upd_representations\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/extract_resources.py",
    "content": "import os\nimport pyblish.api\n\n\nclass ExtractResources(pyblish.api.InstancePlugin):\n    \"\"\"\n        Extracts files from instance.data[\"resources\"].\n\n        These files are additional (textures etc.), currently not stored in\n        representations!\n\n        Expects collected 'resourcesDir'. (list of dicts with 'files' key and\n            list of source urls)\n\n        Provides filled 'transfers' (list of tuples (source_url, target_url))\n    \"\"\"\n\n    label = \"Extract Resources SP\"\n    hosts = [\"standalonepublisher\"]\n    order = pyblish.api.ExtractorOrder\n\n    families = [\"workfile\"]\n\n    def process(self, instance):\n        if not instance.data.get(\"resources\"):\n            self.log.info(\"No resources\")\n            return\n\n        if not instance.data.get(\"transfers\"):\n            instance.data[\"transfers\"] = []\n\n        publish_dir = instance.data[\"resourcesDir\"]\n\n        transfers = []\n        for resource in instance.data[\"resources\"]:\n            for file_url in resource.get(\"files\", []):\n                file_name = os.path.basename(file_url)\n                dest_url = os.path.join(publish_dir, file_name)\n                transfers.append((file_url, dest_url))\n\n        self.log.info(\"transfers:: {}\".format(transfers))\n        instance.data[\"transfers\"].extend(transfers)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py",
    "content": "import os\nimport subprocess\nimport tempfile\nimport pyblish.api\nfrom openpype.lib import (\n    get_ffmpeg_tool_args,\n    get_ffprobe_streams,\n    path_to_subprocess_arg,\n    run_subprocess,\n)\n\n\nclass ExtractThumbnailSP(pyblish.api.InstancePlugin):\n    \"\"\"Extract jpeg thumbnail from component input from standalone publisher\n\n    Uses jpeg file from component if possible (when single or multiple jpegs\n    are loaded to component selected as thumbnail) otherwise extracts from\n    input file/s single jpeg to temp.\n    \"\"\"\n\n    label = \"Extract Thumbnail SP\"\n    hosts = [\"standalonepublisher\"]\n    order = pyblish.api.ExtractorOrder\n\n    # Presetable attribute\n    ffmpeg_args = None\n\n    def process(self, instance):\n        repres = instance.data.get('representations')\n        if not repres:\n            return\n\n        thumbnail_repre = None\n        for repre in repres:\n            if repre.get(\"thumbnail\"):\n                thumbnail_repre = repre\n                break\n\n        if not thumbnail_repre:\n            return\n\n        thumbnail_repre.pop(\"thumbnail\")\n        files = thumbnail_repre.get(\"files\")\n        if not files:\n            return\n\n        if isinstance(files, list):\n            first_filename = str(files[0])\n        else:\n            first_filename = files\n\n        # Convert to jpeg if not yet\n        full_input_path = os.path.join(\n            thumbnail_repre[\"stagingDir\"], first_filename\n        )\n        self.log.info(\"input {}\".format(full_input_path))\n        with tempfile.NamedTemporaryFile(suffix=\".jpg\") as tmp:\n            full_thumbnail_path = tmp.name\n\n        self.log.info(\"output {}\".format(full_thumbnail_path))\n\n        instance.context.data[\"cleanupFullPaths\"].append(full_thumbnail_path)\n\n        ffmpeg_executable_args = get_ffmpeg_tool_args(\"ffmpeg\")\n\n        ffmpeg_args = self.ffmpeg_args or {}\n\n        jpeg_items = [\n            subprocess.list2cmdline(ffmpeg_executable_args),\n            # override file if already exists\n            \"-y\"\n        ]\n\n        # add input filters from peresets\n        jpeg_items.extend(ffmpeg_args.get(\"input\") or [])\n        # input file\n        jpeg_items.extend([\n            \"-i\", path_to_subprocess_arg(full_input_path),\n            # extract only single file\n            \"-frames:v\", \"1\",\n            # Add black background for transparent images\n            \"-filter_complex\", (\n                \"\\\"color=black,format=rgb24[c]\"\n                \";[c][0]scale2ref[c][i]\"\n                \";[c][i]overlay=format=auto:shortest=1,setsar=1\\\"\"\n            ),\n        ])\n\n        jpeg_items.extend(ffmpeg_args.get(\"output\") or [])\n\n        # output file\n        jpeg_items.append(path_to_subprocess_arg(full_thumbnail_path))\n\n        subprocess_jpeg = \" \".join(jpeg_items)\n\n        if os.getenv(\"SHELL\") in (\"/bin/bash\", \"/bin/sh\"):\n            # Escape parentheses for bash\n            subprocess_jpeg = (\n                subprocess_jpeg\n                .replace(\"(\", \"\\\\(\")\n                .replace(\")\", \"\\\\)\")\n            )\n\n        # run subprocess\n        self.log.debug(\"Executing: {}\".format(subprocess_jpeg))\n        run_subprocess(\n            subprocess_jpeg, shell=True, logger=self.log\n        )\n\n        # remove thumbnail key from origin repre\n        streams = get_ffprobe_streams(full_thumbnail_path)\n        width = height = None\n        for stream in streams:\n            if \"width\" in stream and \"height\" in stream:\n                width = stream[\"width\"]\n                height = stream[\"height\"]\n                break\n\n        staging_dir, filename = os.path.split(full_thumbnail_path)\n\n        # create new thumbnail representation\n        representation = {\n            'name': 'thumbnail',\n            'ext': 'jpg',\n            'files': filename,\n            \"stagingDir\": staging_dir,\n            \"tags\": [\"thumbnail\", \"delete\"],\n            \"thumbnail\": True\n        }\n        if width and height:\n            representation[\"width\"] = width\n            representation[\"height\"] = height\n\n        self.log.info(f\"New representation {representation}\")\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py",
    "content": "import os\nimport pyblish.api\n\n\nclass ExtractWorkfileUrl(pyblish.api.ContextPlugin):\n    \"\"\"\n        Modifies 'workfile' field to contain link to published workfile.\n\n        Expects that batch contains only single workfile and matching\n        (multiple) textures.\n    \"\"\"\n\n    label = \"Extract Workfile Url SP\"\n    hosts = [\"standalonepublisher\"]\n    order = pyblish.api.ExtractorOrder\n\n    families = [\"textures\"]\n\n    def process(self, context):\n        filepath = None\n\n        # first loop for workfile\n        for instance in context:\n            if instance.data[\"family\"] == 'workfile':\n                anatomy = context.data['anatomy']\n                template_data = instance.data.get(\"anatomyData\")\n                rep_name = instance.data.get(\"representations\")[0].get(\"name\")\n                template_data[\"representation\"] = rep_name\n                template_data[\"ext\"] = rep_name\n                template_obj = anatomy.templates_obj[\"publish\"][\"path\"]\n                template_filled = template_obj.format_strict(template_data)\n                filepath = os.path.normpath(template_filled)\n                self.log.info(\"Using published scene for render {}\".format(\n                    filepath))\n                break\n\n        if not filepath:\n            self.log.info(\"Texture batch doesn't contain workfile.\")\n            return\n\n        # then apply to all textures\n        for instance in context:\n            if instance.data[\"family\"] == 'textures':\n                instance.data[\"versionData\"][\"workfile\"] = filepath\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/help/validate_editorial_resources.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Missing source video file</title>\n<description>\n## No attached video file found\n\nProcess expects presence of source video file with same name prefix as an editorial file in same folder.\n(example `simple_editorial_setup_Layer1.edl` expects `simple_editorial_setup.mp4` in same folder)\n\n\n### How to repair?\n\nCopy source video file to the folder next to `.edl` file. (On a disk, do not put it into Standalone Publisher.)\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/help/validate_frame_ranges.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Invalid frame range</title>\n<description>\n## Invalid frame range\n\nExpected duration or '{duration}' frames set in database, workfile contains only '{found}' frames.\n\n### How to repair?\n\nModify configuration in the database or tweak frame range in the workfile.\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/help/validate_shot_duplicates.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Duplicate shots</title>\n<description>\n## Duplicate shot names\n\nProcess contains duplicated shot names '{duplicates_str}'.\n\n### How to repair?\n\nRemove shot duplicates.\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Invalid texture name</title>\n<description>\n## Invalid file name\n\nSubmitted file has invalid name:\n'{invalid_file}'\n\n### How to repair?\n\n    Texture file must adhere to naming conventions for Unreal:\n    T_{asset}_*.ext\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/help/validate_sources.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Files not found</title>\n<description>\n## Source files not found\n\nProcess contains duplicated shot names:\n'{files_not_found}'\n\n### How to repair?\n\nAdd missing files or run Publish again to collect new publishable files.\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/help/validate_task_existence.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Task not found</title>\n<description>\n## Task not found in database\n\nProcess contains tasks that don't exist in database:\n'{task_not_found}'\n\n### How to repair?\n\nRemove set task or add task into database into proper place.\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_batch.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>No texture files found</title>\n<description>\n## Batch doesn't contain texture files\n\nBatch must contain at least one texture file.\n\n### How to repair?\n\nAdd texture file to the batch or check name if it follows naming convention to match texture files to the batch.\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_has_workfile.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>No workfile found</title>\n<description>\n## Batch should contain workfile\n\nIt is expected that published contains workfile that served as a source for textures.\n\n### How to repair?\n\nAdd workfile to the batch, or disable this validator if you do not want workfile published.\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_name.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Asset name not found</title>\n<description>\n## Couldn't parse asset name from a file\n\nUnable to parse asset name from '{file_name}'. File name doesn't match configured naming convention.\n\n### How to repair?\n\nCheck Settings: project_settings/standalonepublisher/publish/CollectTextures for naming convention.\n</description>\n<detail>\n### __Detailed Info__ (optional)\n\nThis error happens when parsing cannot figure out name of asset texture files belong under.\n</detail>\n</error>\n<error id=\"missing_values\">\n<title>Missing keys</title>\n<description>\n## Texture file name is missing some required keys\n\nTexture '{file_name}' is missing values for {missing_str} keys.\n\n### How to repair?\n\nFix name of texture file and Publish again.\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_versions.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Texture version</title>\n<description>\n## Texture version mismatch with workfile\n\nWorkfile '{file_name}' version doesn't match with '{version}' of a texture.\n\n### How to repair?\n\nRename either workfile or texture to contain matching versions\n</description>\n<detail>\n### __Detailed Info__ (optional)\n\nThis might happen if you are trying to publish textures for older version of workfile (or the other way).\n(Eg. publishing 'workfile_v001' and 'texture_file_v002')\n</detail>\n</error>\n<error id=\"too_many\">\n<title>Too many versions</title>\n<description>\n## Too many versions published at same time\n\nIt is currently expected to publish only batch with single version.\n\nFound {found} versions.\n\n### How to repair?\n\nPlease remove files with different version and split publishing into multiple steps.\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_workfiles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>No secondary workfile</title>\n<description>\n## No secondary workfile found\n\nCurrent process expects that primary workfile (for example with a extension '{extension}') will contain also 'secondary' workfile.\n\nSecondary workfile for '{file_name}' wasn't found.\n\n### How to repair?\n\nAttach secondary workfile or disable this validator and Publish again.\n</description>\n<detail>\n### __Detailed Info__ (optional)\n\nThis process was implemented for a possible use case of first workfile coming from Mari, secondary workfile for textures from Substance.\nPublish should contain both if primary workfile is present.\n</detail>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/validate_editorial_resources.py",
    "content": "import pyblish.api\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n)\n\n\nclass ValidateEditorialResources(pyblish.api.InstancePlugin):\n    \"\"\"Validate there is a \"mov\" next to the editorial file.\"\"\"\n\n    label = \"Validate Editorial Resources\"\n    hosts = [\"standalonepublisher\"]\n    families = [\"clip\", \"trimming\"]\n\n    # make sure it is enabled only if at least both families are available\n    match = pyblish.api.Subset\n\n    order = ValidateContentsOrder\n\n    def process(self, instance):\n        self.log.debug(\n            f\"Instance: {instance}, Families: \"\n            f\"{[instance.data['family']] + instance.data['families']}\")\n        check_file = instance.data[\"editorialSourcePath\"]\n        msg = \"Missing source video file.\"\n\n        if not check_file:\n            raise PublishXmlValidationError(self, msg)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py",
    "content": "import re\n\nimport pyblish.api\n\nfrom openpype.pipeline.context_tools import get_current_project_asset\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n)\n\n\nclass ValidateFrameRange(pyblish.api.InstancePlugin):\n    \"\"\"Validating frame range of rendered files against state in DB.\"\"\"\n\n    label = \"Validate Frame Range\"\n    hosts = [\"standalonepublisher\"]\n    families = [\"render\"]\n    order = ValidateContentsOrder\n\n    optional = True\n    # published data might be sequence (.mov, .mp4) in that counting files\n    # doesnt make sense\n    check_extensions = [\"exr\", \"dpx\", \"jpg\", \"jpeg\", \"png\", \"tiff\", \"tga\",\n                        \"gif\", \"svg\"]\n    skip_timelines_check = []  # skip for specific task names (regex)\n\n    def process(self, instance):\n        if any(re.search(pattern, instance.data[\"task\"])\n               for pattern in self.skip_timelines_check):\n            self.log.info(\"Skipping for {} task\".format(instance.data[\"task\"]))\n\n        # TODO replace query with using 'instance.data[\"assetEntity\"]'\n        asset_data = get_current_project_asset(instance.data[\"asset\"])[\"data\"]\n        frame_start = asset_data[\"frameStart\"]\n        frame_end = asset_data[\"frameEnd\"]\n        handle_start = asset_data[\"handleStart\"]\n        handle_end = asset_data[\"handleEnd\"]\n        duration = (frame_end - frame_start + 1) + handle_start + handle_end\n\n        repre = instance.data.get(\"representations\", [None])\n        if not repre:\n            self.log.info(\"No representations, skipping.\")\n            return\n\n        ext = repre[0]['ext'].replace(\".\", '')\n\n        if not ext or ext.lower() not in self.check_extensions:\n            self.log.warning(\"Cannot check for extension {}\".format(ext))\n            return\n\n        files = instance.data.get(\"representations\", [None])[0][\"files\"]\n        if isinstance(files, str):\n            files = [files]\n        frames = len(files)\n\n        msg = \"Frame duration from DB:'{}' \". format(int(duration)) +\\\n              \" doesn't match number of files:'{}'\".format(frames) +\\\n              \" Please change frame range for Asset or limit no. of files\"\n\n        formatting_data = {\"duration\": duration,\n                           \"found\": frames}\n        if frames != duration:\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n\n        self.log.debug(\"Valid ranges expected '{}' - found '{}'\".\n                       format(int(duration), frames))\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/validate_shot_duplicates.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n)\n\n\nclass ValidateShotDuplicates(pyblish.api.ContextPlugin):\n    \"\"\"Validating no duplicate names are in context.\"\"\"\n\n    label = \"Validate Shot Duplicates\"\n    hosts = [\"standalonepublisher\"]\n    order = ValidateContentsOrder\n\n    def process(self, context):\n        shot_names = []\n        duplicate_names = []\n        for instance in context:\n            name = instance.data[\"name\"]\n            if name in shot_names:\n                duplicate_names.append(name)\n            else:\n                shot_names.append(name)\n\n        msg = \"There are duplicate shot names:\\n{}\".format(duplicate_names)\n\n        formatting_data = {\"duplicates_str\": ','.join(duplicate_names)}\n        if duplicate_names:\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n)\n\n\nclass ValidateSources(pyblish.api.InstancePlugin):\n    \"\"\"Validates source files.\n\n        Loops through all 'files' in 'stagingDir' if actually exist. They might\n        got deleted between starting of SP and now.\n\n    \"\"\"\n    order = ValidateContentsOrder\n    label = \"Check source files\"\n\n    optional = True  # only for unforeseeable cases\n\n    hosts = [\"standalonepublisher\"]\n\n    def process(self, instance):\n        self.log.info(\"instance {}\".format(instance.data))\n\n        missing_files = set()\n        for repre in instance.data.get(\"representations\") or []:\n            files = []\n            if isinstance(repre[\"files\"], str):\n                files.append(repre[\"files\"])\n            else:\n                files = list(repre[\"files\"])\n\n            for file_name in files:\n                source_file = os.path.join(repre[\"stagingDir\"],\n                                           file_name)\n\n                if not os.path.exists(source_file):\n                    missing_files.add(source_file)\n\n        msg = \"Files '{}' not found\".format(','.join(missing_files))\n        formatting_data = {\"files_not_found\": '    - {}'.join(missing_files)}\n        if missing_files:\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/validate_task_existence.py",
    "content": "import pyblish.api\n\nfrom openpype.client import get_assets\nfrom openpype.pipeline import PublishXmlValidationError\n\n\nclass ValidateTaskExistence(pyblish.api.ContextPlugin):\n    \"\"\"Validating tasks on instances are filled and existing.\"\"\"\n\n    label = \"Validate Task Existence\"\n    order = pyblish.api.ValidatorOrder\n\n    hosts = [\"standalonepublisher\"]\n    families = [\"render_mov_batch\"]\n\n    def process(self, context):\n        asset_names = set()\n        for instance in context:\n            asset_names.add(instance.data[\"asset\"])\n\n        project_name = context.data[\"projectEntity\"][\"name\"]\n        asset_docs = get_assets(\n            project_name,\n            asset_names=asset_names,\n            fields=[\"name\", \"data.tasks\"]\n        )\n        tasks_by_asset_names = {}\n        for asset_doc in asset_docs:\n            asset_name = asset_doc[\"name\"]\n            asset_tasks = asset_doc.get(\"data\", {}).get(\"tasks\") or {}\n            tasks_by_asset_names[asset_name] = list(asset_tasks.keys())\n\n        missing_tasks = []\n        for instance in context:\n            asset_name = instance.data[\"asset\"]\n            task_name = instance.data[\"task\"]\n            task_names = tasks_by_asset_names.get(asset_name) or []\n            if task_name and task_name in task_names:\n                continue\n            missing_tasks.append((asset_name, task_name))\n\n        # Everything is OK\n        if not missing_tasks:\n            return\n\n        # Raise an exception\n        msg = \"Couldn't find task name/s required for publishing.\\n{}\"\n        pair_msgs = []\n        for missing_pair in missing_tasks:\n            pair_msgs.append(\n                \"Asset: \\\"{}\\\" Task: \\\"{}\\\"\".format(*missing_pair)\n            )\n\n        msg = msg.format(\"\\n\".join(pair_msgs))\n\n        formatting_data = {\"task_not_found\": '    - {}'.join(pair_msgs)}\n        if pair_msgs:\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n)\n\n\nclass ValidateTextureBatch(pyblish.api.InstancePlugin):\n    \"\"\"Validates that some texture files are present.\"\"\"\n\n    label = \"Validate Texture Presence\"\n    hosts = [\"standalonepublisher\"]\n    order = ValidateContentsOrder\n    families = [\"texture_batch_workfile\"]\n    optional = False\n\n    def process(self, instance):\n        present = False\n        for instance in instance.context:\n            if instance.data[\"family\"] == \"textures\":\n                self.log.info(\"At least some textures present.\")\n\n                return\n\n        msg = \"No textures found in published batch!\"\n        if not present:\n            raise PublishXmlValidationError(self, msg)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n)\n\n\nclass ValidateTextureHasWorkfile(pyblish.api.InstancePlugin):\n    \"\"\"Validates that textures have appropriate workfile attached.\n\n        Workfile is optional, disable this Validator after Refresh if you are\n        sure it is not needed.\n    \"\"\"\n    label = \"Validate Texture Has Workfile\"\n    hosts = [\"standalonepublisher\"]\n    order = ValidateContentsOrder\n    families = [\"textures\"]\n    optional = True\n\n    def process(self, instance):\n        wfile = instance.data[\"versionData\"].get(\"workfile\")\n\n        msg = \"Textures are missing attached workfile\"\n        if not wfile:\n            raise PublishXmlValidationError(self, msg)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n)\n\nclass ValidateTextureBatchNaming(pyblish.api.InstancePlugin):\n    \"\"\"Validates that all instances had properly formatted name.\"\"\"\n\n    label = \"Validate Texture Batch Naming\"\n    hosts = [\"standalonepublisher\"]\n    order = ValidateContentsOrder\n    families = [\"texture_batch_workfile\", \"textures\"]\n    optional = False\n\n    def process(self, instance):\n        file_name = instance.data[\"representations\"][0][\"files\"]\n        if isinstance(file_name, list):\n            file_name = file_name[0]\n\n        msg = \"Couldn't find asset name in '{}'\\n\".format(file_name) + \\\n              \"File name doesn't follow configured pattern.\\n\" + \\\n              \"Please rename the file.\"\n\n        formatting_data = {\"file_name\": file_name}\n        if \"NOT_AVAIL\" in instance.data[\"asset_build\"]:\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n\n        instance.data.pop(\"asset_build\")  # not needed anymore\n\n        if instance.data[\"family\"] == \"textures\":\n            file_name = instance.data[\"representations\"][0][\"files\"][0]\n            self._check_proper_collected(instance.data[\"versionData\"],\n                                         file_name)\n\n    def _check_proper_collected(self, versionData, file_name):\n        \"\"\"\n            Loop through collected versionData to check if name parsing was OK.\n        Args:\n            versionData: (dict)\n\n        Returns:\n            raises AssertionException\n        \"\"\"\n        missing_key_values = []\n        for key, value in versionData.items():\n            if not value:\n                missing_key_values.append(key)\n\n        msg = \"Collected data {} doesn't contain values for {}\".format(\n            versionData, missing_key_values) + \"\\n\" + \\\n            \"Name of the texture file doesn't match expected pattern.\\n\" + \\\n            \"Please rename file(s) {}\".format(file_name)\n\n        missing_str = ','.join([\"'{}'\".format(key)\n                                for key in missing_key_values])\n        formatting_data = {\"file_name\": file_name,\n                           \"missing_str\": missing_str}\n        if missing_key_values:\n            raise PublishXmlValidationError(self, msg, key=\"missing_values\",\n                                            formatting_data=formatting_data)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n)\n\n\nclass ValidateTextureBatchVersions(pyblish.api.InstancePlugin):\n    \"\"\"Validates that versions match in workfile and textures.\n\n        Workfile is optional, so if you are sure, you can disable this\n        validator after Refresh.\n\n        Validates that only single version is published at a time.\n    \"\"\"\n    label = \"Validate Texture Batch Versions\"\n    hosts = [\"standalonepublisher\"]\n    order = ValidateContentsOrder\n    families = [\"textures\"]\n    optional = False\n\n    def process(self, instance):\n        wfile = instance.data[\"versionData\"].get(\"workfile\")\n\n        version_str = \"v{:03d}\".format(instance.data[\"version\"])\n\n        if not wfile:  # no matching workfile, do not check versions\n            self.log.info(\"No workfile present for textures\")\n            return\n\n        if version_str not in wfile:\n            msg = \"Not matching version: texture v{:03d} - workfile {}\"\n            msg.format(\n                instance.data[\"version\"], wfile\n            )\n            raise PublishXmlValidationError(self, msg)\n\n        present_versions = set()\n        for instance in instance.context:\n            present_versions.add(instance.data[\"version\"])\n\n        if len(present_versions) != 1:\n            msg = \"Too many versions in a batch!\"\n            found = ','.join([\"'{}'\".format(val) for val in present_versions])\n            formatting_data = {\"found\": found}\n\n            raise PublishXmlValidationError(self, msg, key=\"too_many\",\n                                            formatting_data=formatting_data)\n"
  },
  {
    "path": "openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n)\n\n\nclass ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin):\n    \"\"\"Validates that textures workfile has collected resources (optional).\n\n        Collected resources means secondary workfiles (in most cases).\n    \"\"\"\n\n    label = \"Validate Texture Workfile Has Resources\"\n    hosts = [\"standalonepublisher\"]\n    order = ValidateContentsOrder\n    families = [\"texture_batch_workfile\"]\n    optional = True\n\n    def process(self, instance):\n        if instance.data[\"family\"] != \"workfile\":\n            return\n\n        ext = instance.data[\"representations\"][0][\"ext\"]\n        main_workfile_extensions = self.get_main_workfile_extensions(\n            instance\n        )\n        if ext not in main_workfile_extensions:\n            self.log.warning(\"Only secondary workfile present!\")\n            return\n\n        if not instance.data.get(\"resources\"):\n            msg = \"No secondary workfile present for workfile '{}'\". \\\n                format(instance.data[\"name\"])\n            ext = main_workfile_extensions[0]\n            formatting_data = {\"file_name\": instance.data[\"name\"],\n                               \"extension\": ext}\n\n            raise PublishXmlValidationError(\n                self, msg, formatting_data=formatting_data)\n\n    @staticmethod\n    def get_main_workfile_extensions(instance):\n        project_settings = instance.context.data[\"project_settings\"]\n\n        try:\n            extensions = (project_settings[\"standalonepublisher\"]\n                                          [\"publish\"]\n                                          [\"CollectTextures\"]\n                                          [\"main_workfile_extensions\"])\n        except KeyError:\n            raise Exception(\"Setting 'Main workfile extensions' not found.\"\n                            \" The setting must be set for the\"\n                            \" 'Collect Texture' publish plugin of the\"\n                            \" 'Standalone Publish' tool.\")\n\n        return extensions\n"
  },
  {
    "path": "openpype/hosts/substancepainter/__init__.py",
    "content": "from .addon import (\n    SubstanceAddon,\n    SUBSTANCE_HOST_DIR,\n)\n\n\n__all__ = (\n    \"SubstanceAddon\",\n    \"SUBSTANCE_HOST_DIR\"\n)\n"
  },
  {
    "path": "openpype/hosts/substancepainter/addon.py",
    "content": "import os\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nSUBSTANCE_HOST_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass SubstanceAddon(OpenPypeModule, IHostAddon):\n    name = \"substancepainter\"\n    host_name = \"substancepainter\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        # Add requirements to SUBSTANCE_PAINTER_PLUGINS_PATH\n        plugin_path = os.path.join(SUBSTANCE_HOST_DIR, \"deploy\")\n        plugin_path = plugin_path.replace(\"\\\\\", \"/\")\n        if env.get(\"SUBSTANCE_PAINTER_PLUGINS_PATH\"):\n            plugin_path += os.pathsep + env[\"SUBSTANCE_PAINTER_PLUGINS_PATH\"]\n\n        env[\"SUBSTANCE_PAINTER_PLUGINS_PATH\"] = plugin_path\n\n        # Log in Substance Painter doesn't support custom terminal colors\n        env[\"OPENPYPE_LOG_NO_COLORS\"] = \"Yes\"\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [\n            os.path.join(SUBSTANCE_HOST_DIR, \"hooks\")\n        ]\n\n    def get_workfile_extensions(self):\n        return [\".spp\", \".toc\"]\n"
  },
  {
    "path": "openpype/hosts/substancepainter/api/__init__.py",
    "content": "from .pipeline import (\n    SubstanceHost,\n\n)\n\n__all__ = [\n    \"SubstanceHost\",\n]\n"
  },
  {
    "path": "openpype/hosts/substancepainter/api/colorspace.py",
    "content": "\"\"\"Substance Painter OCIO management\n\nAdobe Substance 3D Painter supports OCIO color management using a per project\nconfiguration. Output color spaces are defined at the project level\n\nMore information see:\n  - https://substance3d.adobe.com/documentation/spdoc/color-management-223053233.html  # noqa\n  - https://substance3d.adobe.com/documentation/spdoc/color-management-with-opencolorio-225969419.html  # noqa\n\n\"\"\"\nimport substance_painter.export\nimport substance_painter.js\nimport json\n\nfrom .lib import (\n    get_document_structure,\n    get_channel_format\n)\n\n\ndef _iter_document_stack_channels():\n    \"\"\"Yield all stack paths and channels project\"\"\"\n\n    for material in get_document_structure()[\"materials\"]:\n        material_name = material[\"name\"]\n        for stack in material[\"stacks\"]:\n            stack_name = stack[\"name\"]\n            if stack_name:\n                stack_path = [material_name, stack_name]\n            else:\n                stack_path = material_name\n            for channel in stack[\"channels\"]:\n                yield stack_path, channel\n\n\ndef _get_first_color_and_data_stack_and_channel():\n    \"\"\"Return first found color channel and data channel.\"\"\"\n    color_channel = None\n    data_channel = None\n    for stack_path, channel in _iter_document_stack_channels():\n        channel_format = get_channel_format(stack_path, channel)\n        if channel_format[\"color\"]:\n            color_channel = (stack_path, channel)\n        else:\n            data_channel = (stack_path, channel)\n\n        if color_channel and data_channel:\n            return color_channel, data_channel\n\n    return color_channel, data_channel\n\n\ndef get_project_channel_data():\n    \"\"\"Return colorSpace settings for the current substance painter project.\n\n    In Substance Painter only color channels have Color Management enabled\n    whereas data channels have no color management applied. This can't be\n    changed. The artist can only customize the export color space for color\n    channels per bit-depth for 8 bpc, 16 bpc and 32 bpc.\n\n    As such this returns the color space for 'data' and for per bit-depth\n    for color channels.\n\n    Example output:\n    {\n        \"data\": {'colorSpace': 'Utility - Raw'},\n        \"8\": {\"colorSpace\": \"ACES - AcesCG\"},\n        \"16\": {\"colorSpace\": \"ACES - AcesCG\"},\n        \"16f\": {\"colorSpace\": \"ACES - AcesCG\"},\n        \"32f\": {\"colorSpace\": \"ACES - AcesCG\"}\n    }\n\n    \"\"\"\n\n    keys = [\"colorSpace\"]\n    query = {key: f\"${key}\" for key in keys}\n\n    config = {\n        \"exportPath\": \"/\",\n        \"exportShaderParams\": False,\n        \"defaultExportPreset\": \"query_preset\",\n\n        \"exportPresets\": [{\n            \"name\": \"query_preset\",\n\n            # List of maps making up this export preset.\n            \"maps\": [{\n                \"fileName\": json.dumps(query),\n                # List of source/destination defining which channels will\n                # make up the texture file.\n                \"channels\": [],\n                \"parameters\": {\n                    \"fileFormat\": \"exr\",\n                    \"bitDepth\": \"32f\",\n                    \"dithering\": False,\n                    \"sizeLog2\": 4,\n                    \"paddingAlgorithm\": \"passthrough\",\n                    \"dilationDistance\": 16\n                }\n            }]\n        }],\n    }\n\n    def _get_query_output(config):\n        # Return the basename of the single output path we defined\n        result = substance_painter.export.list_project_textures(config)\n        path = next(iter(result.values()))[0]\n        # strip extension and slash since we know relevant json data starts\n        # and ends with { and } characters\n        path = path.strip(\"/\\\\.exr\")\n        return json.loads(path)\n\n    # Query for each type of channel (color and data)\n    color_channel, data_channel = _get_first_color_and_data_stack_and_channel()\n    colorspaces = {}\n    for key, channel_data in {\n        \"data\": data_channel,\n        \"color\": color_channel\n    }.items():\n        if channel_data is None:\n            # No channel of that datatype anywhere in the Stack. We're\n            # unable to identify the output color space of the project\n            colorspaces[key] = None\n            continue\n\n        stack, channel = channel_data\n\n        # Stack must be a string\n        if not isinstance(stack, str):\n            # Assume iterable\n            stack = \"/\".join(stack)\n\n        # Define the temp output config\n        config[\"exportList\"] = [{\"rootPath\": stack}]\n        config_map = config[\"exportPresets\"][0][\"maps\"][0]\n        config_map[\"channels\"] = [\n            {\n                \"destChannel\": x,\n                \"srcChannel\": x,\n                \"srcMapType\": \"documentMap\",\n                \"srcMapName\": channel\n            } for x in \"RGB\"\n        ]\n\n        if key == \"color\":\n            # Query for each bit depth\n            # Color space definition can have a different OCIO config set\n            # for 8-bit, 16-bit and 32-bit outputs so we need to check each\n            # bit depth\n            for depth in [\"8\", \"16\", \"16f\", \"32f\"]:\n                config_map[\"parameters\"][\"bitDepth\"] = depth  # noqa\n                colorspaces[key + depth] = _get_query_output(config)\n        else:\n            # Data channel (not color managed)\n            colorspaces[key] = _get_query_output(config)\n\n    return colorspaces\n"
  },
  {
    "path": "openpype/hosts/substancepainter/api/lib.py",
    "content": "import os\nimport re\nimport json\nfrom collections import defaultdict\n\nimport substance_painter.project\nimport substance_painter.resource\nimport substance_painter.js\nimport substance_painter.export\n\nfrom qtpy import QtGui, QtWidgets, QtCore\n\n\ndef get_export_presets():\n    \"\"\"Return Export Preset resource URLs for all available Export Presets.\n\n    Returns:\n        dict: {Resource url: GUI Label}\n\n    \"\"\"\n    # TODO: Find more optimal way to find all export templates\n\n    preset_resources = {}\n    for shelf in substance_painter.resource.Shelves.all():\n        shelf_path = os.path.normpath(shelf.path())\n\n        presets_path = os.path.join(shelf_path, \"export-presets\")\n        if not os.path.exists(presets_path):\n            continue\n\n        for filename in os.listdir(presets_path):\n            if filename.endswith(\".spexp\"):\n                template_name = os.path.splitext(filename)[0]\n\n                resource = substance_painter.resource.ResourceID(\n                    context=shelf.name(),\n                    name=template_name\n                )\n                resource_url = resource.url()\n\n                preset_resources[resource_url] = template_name\n\n    # Sort by template name\n    export_templates = dict(sorted(preset_resources.items(),\n                                   key=lambda x: x[1]))\n\n    # Add default built-ins at the start\n    # TODO: find the built-ins automatically; scraped with https://gist.github.com/BigRoy/97150c7c6f0a0c916418207b9a2bc8f1  # noqa\n    result = {\n        \"export-preset-generator://viewport2d\": \"2D View\",  # noqa\n        \"export-preset-generator://doc-channel-normal-no-alpha\": \"Document channels + Normal + AO (No Alpha)\",  # noqa\n        \"export-preset-generator://doc-channel-normal-with-alpha\": \"Document channels + Normal + AO (With Alpha)\",  # noqa\n        \"export-preset-generator://sketchfab\": \"Sketchfab\",  # noqa\n        \"export-preset-generator://adobe-standard-material\": \"Substance 3D Stager\",  # noqa\n        \"export-preset-generator://usd\": \"USD PBR Metal Roughness\",  # noqa\n        \"export-preset-generator://gltf\": \"glTF PBR Metal Roughness\",  # noqa\n        \"export-preset-generator://gltf-displacement\": \"glTF PBR Metal Roughness + Displacement texture (experimental)\"  # noqa\n    }\n    result.update(export_templates)\n    return result\n\n\ndef _convert_stack_path_to_cmd_str(stack_path):\n    \"\"\"Convert stack path `str` or `[str, str]` for javascript query\n\n    Example usage:\n        >>> stack_path = _convert_stack_path_to_cmd_str(stack_path)\n        >>> cmd = f\"alg.mapexport.channelIdentifiers({stack_path})\"\n        >>> substance_painter.js.evaluate(cmd)\n\n    Args:\n        stack_path (list or str): Path to the stack, could be\n            \"Texture set name\" or [\"Texture set name\", \"Stack name\"]\n\n    Returns:\n        str: Stack path usable as argument in javascript query.\n\n    \"\"\"\n    return json.dumps(stack_path)\n\n\ndef get_channel_identifiers(stack_path=None):\n    \"\"\"Return the list of channel identifiers.\n\n    If a context is passed (texture set/stack),\n    return only used channels with resolved user channels.\n\n    Channel identifiers are:\n        basecolor, height, specular, opacity, emissive, displacement,\n        glossiness, roughness, anisotropylevel, anisotropyangle, transmissive,\n        scattering, reflection, ior, metallic, normal, ambientOcclusion,\n        diffuse, specularlevel, blendingmask, [custom user names].\n\n    Args:\n        stack_path (list or str, Optional): Path to the stack, could be\n            \"Texture set name\" or [\"Texture set name\", \"Stack name\"]\n\n    Returns:\n        list: List of channel identifiers.\n\n    \"\"\"\n    if stack_path is None:\n        stack_path = \"\"\n    else:\n        stack_path = _convert_stack_path_to_cmd_str(stack_path)\n    cmd = f\"alg.mapexport.channelIdentifiers({stack_path})\"\n    return substance_painter.js.evaluate(cmd)\n\n\ndef get_channel_format(stack_path, channel):\n    \"\"\"Retrieve the channel format of a specific stack channel.\n\n    See `alg.mapexport.channelFormat` (javascript API) for more details.\n\n    The channel format data is:\n        \"label\" (str): The channel format label: could be one of\n            [sRGB8, L8, RGB8, L16, RGB16, L16F, RGB16F, L32F, RGB32F]\n        \"color\" (bool): True if the format is in color, False is grayscale\n        \"floating\" (bool): True if the format uses floating point\n            representation, false otherwise\n        \"bitDepth\" (int): Bit per color channel (could be 8, 16 or 32 bpc)\n\n    Arguments:\n        stack_path (list or str): Path to the stack, could be\n            \"Texture set name\" or [\"Texture set name\", \"Stack name\"]\n        channel (str): Identifier of the channel to export\n            (see `get_channel_identifiers`)\n\n    Returns:\n        dict: The channel format data.\n\n    \"\"\"\n    stack_path = _convert_stack_path_to_cmd_str(stack_path)\n    cmd = f\"alg.mapexport.channelFormat({stack_path}, '{channel}')\"\n    return substance_painter.js.evaluate(cmd)\n\n\ndef get_document_structure():\n    \"\"\"Dump the document structure.\n\n    See `alg.mapexport.documentStructure` (javascript API) for more details.\n\n    Returns:\n        dict: Document structure or None when no project is open\n\n    \"\"\"\n    return substance_painter.js.evaluate(\"alg.mapexport.documentStructure()\")\n\n\ndef get_export_templates(config, format=\"png\", strip_folder=True):\n    \"\"\"Return export config outputs.\n\n    This use the Javascript API `alg.mapexport.getPathsExportDocumentMaps`\n    which returns a different output than using the Python equivalent\n    `substance_painter.export.list_project_textures(config)`.\n\n    The nice thing about the Javascript API version is that it returns the\n    output textures grouped by filename template.\n\n    A downside is that it doesn't return all the UDIM tiles but per template\n    always returns a single file.\n\n    Note:\n        The file format needs to be explicitly passed to the Javascript API\n        but upon exporting through the Python API the file format can be based\n        on the output preset. So it's likely the file extension will mismatch\n\n    Warning:\n        Even though the function appears to solely get the expected outputs\n        the Javascript API will actually create the config's texture output\n        folder if it does not exist yet. As such, a valid path must be set.\n\n    Example output:\n    {\n        \"DefaultMaterial\": {\n            \"$textureSet_BaseColor(_$colorSpace)(.$udim)\": \"DefaultMaterial_BaseColor_ACES - ACEScg.1002.png\",   # noqa\n            \"$textureSet_Emissive(_$colorSpace)(.$udim)\": \"DefaultMaterial_Emissive_ACES - ACEScg.1002.png\",     # noqa\n            \"$textureSet_Height(_$colorSpace)(.$udim)\": \"DefaultMaterial_Height_Utility - Raw.1002.png\",         # noqa\n            \"$textureSet_Metallic(_$colorSpace)(.$udim)\": \"DefaultMaterial_Metallic_Utility - Raw.1002.png\",     # noqa\n            \"$textureSet_Normal(_$colorSpace)(.$udim)\": \"DefaultMaterial_Normal_Utility - Raw.1002.png\",         # noqa\n            \"$textureSet_Roughness(_$colorSpace)(.$udim)\": \"DefaultMaterial_Roughness_Utility - Raw.1002.png\"    # noqa\n        }\n    }\n\n    Arguments:\n        config (dict) Export config\n        format (str, Optional): Output format to write to, defaults to 'png'\n        strip_folder (bool, Optional): Whether to strip the output folder\n            from the output filenames.\n\n    Returns:\n        dict: The expected output maps.\n\n    \"\"\"\n    folder = config[\"exportPath\"].replace(\"\\\\\", \"/\")\n    preset = config[\"defaultExportPreset\"]\n    cmd = f'alg.mapexport.getPathsExportDocumentMaps(\"{preset}\", \"{folder}\", \"{format}\")'  # noqa\n    result = substance_painter.js.evaluate(cmd)\n\n    if strip_folder:\n        for _stack, maps in result.items():\n            for map_template, map_filepath in maps.items():\n                map_filepath = map_filepath.replace(\"\\\\\", \"/\")\n                assert map_filepath.startswith(folder)\n                map_filename = map_filepath[len(folder):].lstrip(\"/\")\n                maps[map_template] = map_filename\n\n    return result\n\n\ndef _templates_to_regex(templates,\n                        texture_set,\n                        colorspaces,\n                        project,\n                        mesh):\n    \"\"\"Return regex based on a Substance Painter expot filename template.\n\n    This converts Substance Painter export filename templates like\n    `$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)` into a regex\n    which can be used to query an output filename to help retrieve:\n\n        - Which template filename the file belongs to.\n        - Which color space the file is written with.\n        - Which udim tile it is exactly.\n\n    This is used by `get_parsed_export_maps` which tries to as explicitly\n    as possible match the filename pattern against the known possible outputs.\n    That's why Texture Set name, Color spaces, Project path and mesh path must\n    be provided. By doing so we get the best shot at correctly matching the\n    right template because otherwise $texture_set could basically be any string\n    and thus match even that of a color space or mesh.\n\n    Arguments:\n        templates (list): List of templates to convert to regex.\n        texture_set (str): The texture set to match against.\n        colorspaces (list): The colorspaces defined in the current project.\n        project (str): Filepath of current substance project.\n        mesh (str): Path to mesh file used in current project.\n\n    Returns:\n        dict: Template: Template regex pattern\n\n    \"\"\"\n    def _filename_no_ext(path):\n        return os.path.splitext(os.path.basename(path))[0]\n\n    if colorspaces and any(colorspaces):\n        colorspace_match = \"|\".join(re.escape(c) for c in set(colorspaces))\n        colorspace_match = f\"({colorspace_match})\"\n    else:\n        # No colorspace support enabled\n        colorspace_match = \"\"\n\n    # Key to regex valid search values\n    key_matches = {\n        \"$project\": re.escape(_filename_no_ext(project)),\n        \"$mesh\": re.escape(_filename_no_ext(mesh)),\n        \"$textureSet\": re.escape(texture_set),\n        \"$colorSpace\": colorspace_match,\n        \"$udim\": \"([0-9]{4})\"\n    }\n\n    # Turn the templates into regexes\n    regexes = {}\n    for template in templates:\n\n        # We need to tweak a temp\n        search_regex = re.escape(template)\n\n        # Let's assume that any ( and ) character in the file template was\n        # intended as an optional template key and do a simple `str.replace`\n        # Note: we are matching against re.escape(template) so will need to\n        #       search for the escaped brackets.\n        search_regex = search_regex.replace(re.escape(\"(\"), \"(\")\n        search_regex = search_regex.replace(re.escape(\")\"), \")?\")\n\n        # Substitute each key into a named group\n        for key, key_expected_regex in key_matches.items():\n\n            # We want to use the template as a regex basis in the end so will\n            # escape the whole thing first. Note that thus we'll need to\n            # search for the escaped versions of the keys too.\n            escaped_key = re.escape(key)\n            key_label = key[1:]  # key without $ prefix\n\n            key_expected_grp_regex = f\"(?P<{key_label}>{key_expected_regex})\"\n            search_regex = search_regex.replace(escaped_key,\n                                                key_expected_grp_regex)\n\n        # The filename templates don't include the extension so we add it\n        # to be able to match the out filename beginning to end\n        ext_regex = r\"(?P<ext>\\.[A-Za-z][A-Za-z0-9-]*)\"\n        search_regex = rf\"^{search_regex}{ext_regex}$\"\n\n        regexes[template] = search_regex\n\n    return regexes\n\n\ndef strip_template(template, strip=\"._ \"):\n    \"\"\"Return static characters in a substance painter filename template.\n\n    >>> strip_template(\"$textureSet_HELLO(.$udim)\")\n    # HELLO\n    >>> strip_template(\"$mesh_$textureSet_HELLO_WORLD_$colorSpace(.$udim)\")\n    # HELLO_WORLD\n    >>> strip_template(\"$textureSet_HELLO(.$udim)\", strip=None)\n    # _HELLO\n    >>> strip_template(\"$mesh_$textureSet_$colorSpace(.$udim)\", strip=None)\n    # _HELLO_\n    >>> strip_template(\"$textureSet_HELLO(.$udim)\")\n    # _HELLO\n\n    Arguments:\n        template (str): Filename template to strip.\n        strip (str, optional): Characters to strip from beginning and end\n            of the static string in template. Defaults to: `._ `.\n\n    Returns:\n        str: The static string in filename template.\n\n    \"\"\"\n    # Return only characters that were part of the template that were static.\n    # Remove all keys\n    keys = [\"$project\", \"$mesh\", \"$textureSet\", \"$udim\", \"$colorSpace\"]\n    stripped_template = template\n    for key in keys:\n        stripped_template = stripped_template.replace(key, \"\")\n\n    # Everything inside an optional bracket space is excluded since it's not\n    # static. We keep a counter to track whether we are currently iterating\n    # over parts of the template that are inside an 'optional' group or not.\n    counter = 0\n    result = \"\"\n    for char in stripped_template:\n        if char == \"(\":\n            counter += 1\n        elif char == \")\":\n            counter -= 1\n            if counter < 0:\n                counter = 0\n        else:\n            if counter == 0:\n                result += char\n\n    if strip:\n        # Strip of any trailing start/end characters. Technically these are\n        # static but usually start and end separators like space or underscore\n        # aren't wanted.\n        result = result.strip(strip)\n\n    return result\n\n\ndef get_parsed_export_maps(config):\n    \"\"\"Return Export Config's expected output textures with parsed data.\n\n    This tries to parse the texture outputs using a Python API export config.\n\n    Parses template keys: $project, $mesh, $textureSet, $colorSpace, $udim\n\n    Example:\n    {(\"DefaultMaterial\", \"\"): {\n        \"$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)\": [\n                {\n                    // OUTPUT DATA FOR FILE #1 OF THE TEMPLATE\n                },\n                {\n                    // OUTPUT DATA FOR FILE #2 OF THE TEMPLATE\n                },\n            ]\n        },\n    }}\n\n    File output data (all outputs are `str`).\n    1) Parsed tokens: These are parsed tokens from the template, they will\n        only exist if found in the filename template and output filename.\n\n        project: Workfile filename without extension\n        mesh: Filename of the loaded mesh without extension\n        textureSet: The texture set, e.g. \"DefaultMaterial\",\n        colorSpace: The color space, e.g. \"ACES - ACEScg\",\n        udim: The udim tile, e.g. \"1001\"\n\n    2) Template output and filepath\n\n        filepath: Full path to the resulting texture map, e.g.\n            \"/path/to/mesh_DefaultMaterial_BaseColor_ACES - ACEScg.1002.png\",\n        output: \"mesh_DefaultMaterial_BaseColor_ACES - ACEScg.1002.png\"\n            Note: if template had slashes (folders) then `output` will too.\n                  So `output` might include a folder.\n\n    Returns:\n        dict: [texture_set, stack]: {template: [file1_data, file2_data]}\n\n    \"\"\"\n    # Import is here to avoid recursive lib <-> colorspace imports\n    from .colorspace import get_project_channel_data\n\n    outputs = substance_painter.export.list_project_textures(config)\n    templates = get_export_templates(config, strip_folder=False)\n\n    # Get all color spaces set for the current project\n    project_colorspaces = set(\n        data[\"colorSpace\"] for data in get_project_channel_data().values()\n    )\n\n    # Get current project mesh path and project path to explicitly match\n    # the $mesh and $project tokens\n    project_mesh_path = substance_painter.project.last_imported_mesh_path()\n    project_path = substance_painter.project.file_path()\n\n    # Get the current export path to strip this of the beginning of filepath\n    # results, since filename templates don't have these we'll match without\n    # that part of the filename.\n    export_path = config[\"exportPath\"]\n    export_path = export_path.replace(\"\\\\\", \"/\")\n    if not export_path.endswith(\"/\"):\n        export_path += \"/\"\n\n    # Parse the outputs\n    result = {}\n    for key, filepaths in outputs.items():\n        texture_set, stack = key\n\n        if stack:\n            stack_path = f\"{texture_set}/{stack}\"\n        else:\n            stack_path = texture_set\n\n        stack_templates = list(templates[stack_path].keys())\n\n        template_regex = _templates_to_regex(stack_templates,\n                                             texture_set=texture_set,\n                                             colorspaces=project_colorspaces,\n                                             mesh=project_mesh_path,\n                                             project=project_path)\n\n        # Let's precompile the regexes\n        for template, regex in template_regex.items():\n            template_regex[template] = re.compile(regex)\n\n        stack_results = defaultdict(list)\n        for filepath in sorted(filepaths):\n            # We strip explicitly using the full parent export path instead of\n            # using `os.path.basename` because export template is allowed to\n            # have subfolders in its template which we want to match against\n            filepath = filepath.replace(\"\\\\\", \"/\")\n            assert filepath.startswith(export_path), (\n                f\"Filepath {filepath} must start with folder {export_path}\"\n            )\n            filename = filepath[len(export_path):]\n\n            for template, regex in template_regex.items():\n                match = regex.match(filename)\n                if match:\n                    parsed = match.groupdict(default={})\n\n                    # Include some special outputs for convenience\n                    parsed[\"filepath\"] = filepath\n                    parsed[\"output\"] = filename\n\n                    stack_results[template].append(parsed)\n                    break\n            else:\n                raise ValueError(f\"Unable to match {filename} against any \"\n                                 f\"template in: {list(template_regex.keys())}\")\n\n        result[key] = dict(stack_results)\n\n    return result\n\n\ndef load_shelf(path, name=None):\n    \"\"\"Add shelf to substance painter (for current application session)\n\n    This will dynamically add a Shelf for the current session. It's good\n    to note however that these will *not* persist on restart of the host.\n\n    Note:\n        Consider the loaded shelf a static library of resources.\n\n        The shelf will *not* be visible in application preferences in\n        Edit > Settings > Libraries.\n\n        The shelf will *not* show in the Assets browser if it has no existing\n        assets\n\n        The shelf will *not* be a selectable option for selecting it as a\n        destination to import resources too.\n\n    \"\"\"\n\n    # Ensure expanded path with forward slashes\n    path = os.path.expandvars(path)\n    path = os.path.abspath(path)\n    path = path.replace(\"\\\\\", \"/\")\n\n    # Path must exist\n    if not os.path.isdir(path):\n        raise ValueError(f\"Path is not an existing folder: {path}\")\n\n    # This name must be unique and must only contain lowercase letters,\n    # numbers, underscores or hyphens.\n    if name is None:\n        name = os.path.basename(path)\n\n    name = name.lower()\n    name = re.sub(r\"[^a-z0-9_\\-]\", \"_\", name)   # sanitize to underscores\n\n    if substance_painter.resource.Shelves.exists(name):\n        shelf = next(\n            shelf for shelf in substance_painter.resource.Shelves.all()\n            if shelf.name() == name\n        )\n        if os.path.normpath(shelf.path()) != os.path.normpath(path):\n            raise ValueError(f\"Shelf with name '{name}' already exists \"\n                             f\"for a different path: '{shelf.path()}\")\n\n        return\n\n    print(f\"Adding Shelf '{name}' to path: {path}\")\n    substance_painter.resource.Shelves.add(name, path)\n\n    return name\n\n\ndef _get_new_project_action():\n    \"\"\"Return QAction which triggers Substance Painter's new project dialog\"\"\"\n\n    main_window = substance_painter.ui.get_main_window()\n\n    # Find the file menu's New file action\n    menubar = main_window.menuBar()\n    new_action = None\n    for action in menubar.actions():\n        menu = action.menu()\n        if not menu:\n            continue\n\n        if menu.objectName() != \"file\":\n            continue\n\n        # Find the action with the CTRL+N key sequence\n        new_action = next(action for action in menu.actions()\n                          if action.shortcut() == QtGui.QKeySequence.New)\n        break\n\n    return new_action\n\n\ndef prompt_new_file_with_mesh(mesh_filepath):\n    \"\"\"Prompts the user for a new file using Substance Painter's own dialog.\n\n    This will set the mesh path to load to the given mesh and disables the\n    dialog box to disallow the user to change the path. This way we can allow\n    user configuration of a project but set the mesh path ourselves.\n\n    Warning:\n        This is very hacky and experimental.\n\n    Note:\n       If a project is currently open using the same mesh filepath it can't\n       accurately detect whether the user had actually accepted the new project\n       dialog or whether the project afterwards is still the original project,\n       for example when the user might have cancelled the operation.\n\n    \"\"\"\n\n    app = QtWidgets.QApplication.instance()\n    assert os.path.isfile(mesh_filepath), \\\n        f\"Mesh filepath does not exist: {mesh_filepath}\"\n\n    def _setup_file_dialog():\n        \"\"\"Set filepath in QFileDialog and trigger accept result\"\"\"\n        file_dialog = app.activeModalWidget()\n        assert isinstance(file_dialog, QtWidgets.QFileDialog)\n\n        # Quickly hide the dialog\n        file_dialog.hide()\n        app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 1000)\n\n        file_dialog.setDirectory(os.path.dirname(mesh_filepath))\n        url = QtCore.QUrl.fromLocalFile(os.path.basename(mesh_filepath))\n        file_dialog.selectUrl(url)\n        # TODO: find a way to improve the process event to\n        # load more complicated mesh\n        app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 3000)\n\n        file_dialog.done(file_dialog.Accepted)\n        app.processEvents(QtCore.QEventLoop.AllEvents)\n\n    def _setup_prompt():\n        app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents)\n        dialog = app.activeModalWidget()\n        assert dialog.objectName() == \"NewProjectDialog\"\n\n        # Set the window title\n        mesh = os.path.basename(mesh_filepath)\n        dialog.setWindowTitle(f\"New Project with mesh: {mesh}\")\n\n        # Get the select mesh file button\n        mesh_select = dialog.findChild(QtWidgets.QPushButton, \"meshSelect\")\n\n        # Hide the select mesh button to the user to block changing of mesh\n        mesh_select.setVisible(False)\n\n        # Ensure UI is visually up-to-date\n        app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 8000)\n\n        # Trigger the 'select file' dialog to set the path and have the\n        # new file dialog to use the path.\n        QtCore.QTimer.singleShot(10, _setup_file_dialog)\n        mesh_select.click()\n\n        app.processEvents(QtCore.QEventLoop.AllEvents, 5000)\n\n        mesh_filename = dialog.findChild(QtWidgets.QFrame, \"meshFileName\")\n        mesh_filename_label = mesh_filename.findChild(QtWidgets.QLabel)\n        if not mesh_filename_label.text():\n            dialog.close()\n            substance_painter.logging.warning(\n                \"Failed to set mesh path with the prompt dialog:\"\n                f\"{mesh_filepath}\\n\\n\"\n                \"Creating new project directly with the mesh path instead.\")\n\n    new_action = _get_new_project_action()\n    if not new_action:\n        raise RuntimeError(\"Unable to detect new file action..\")\n\n    QtCore.QTimer.singleShot(0, _setup_prompt)\n    new_action.trigger()\n    app.processEvents(QtCore.QEventLoop.AllEvents, 5000)\n\n    if not substance_painter.project.is_open():\n        return\n\n    # Confirm mesh was set as expected\n    project_mesh = substance_painter.project.last_imported_mesh_path()\n    if os.path.normpath(project_mesh) != os.path.normpath(mesh_filepath):\n        return\n\n    return project_mesh\n"
  },
  {
    "path": "openpype/hosts/substancepainter/api/pipeline.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Pipeline tools for OpenPype Substance Painter integration.\"\"\"\nimport os\nimport logging\nfrom functools import partial\n\n# Substance 3D Painter modules\nimport substance_painter.ui\nimport substance_painter.event\nimport substance_painter.project\n\nimport pyblish.api\n\nfrom openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost\nfrom openpype.settings import (\n    get_current_project_settings,\n    get_system_settings\n)\n\nfrom openpype.pipeline.template_data import get_template_data_with_names\nfrom openpype.pipeline import (\n    register_creator_plugin_path,\n    register_loader_plugin_path,\n    AVALON_CONTAINER_ID,\n    Anatomy\n)\nfrom openpype.lib import (\n    StringTemplate,\n    register_event_callback,\n    emit_event,\n)\nfrom openpype.pipeline.load import any_outdated_containers\nfrom openpype.hosts.substancepainter import SUBSTANCE_HOST_DIR\n\nfrom . import lib\n\nlog = logging.getLogger(\"openpype.hosts.substance\")\n\nPLUGINS_DIR = os.path.join(SUBSTANCE_HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\nINVENTORY_PATH = os.path.join(PLUGINS_DIR, \"inventory\")\n\nOPENPYPE_METADATA_KEY = \"OpenPype\"\nOPENPYPE_METADATA_CONTAINERS_KEY = \"containers\"  # child key\nOPENPYPE_METADATA_CONTEXT_KEY = \"context\"        # child key\nOPENPYPE_METADATA_INSTANCES_KEY = \"instances\"    # child key\n\n\nclass SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):\n    name = \"substancepainter\"\n\n    def __init__(self):\n        super(SubstanceHost, self).__init__()\n        self._has_been_setup = False\n        self.menu = None\n        self.callbacks = []\n        self.shelves = []\n\n    def install(self):\n        pyblish.api.register_host(\"substancepainter\")\n\n        pyblish.api.register_plugin_path(PUBLISH_PATH)\n        register_loader_plugin_path(LOAD_PATH)\n        register_creator_plugin_path(CREATE_PATH)\n\n        log.info(\"Installing callbacks ... \")\n        # register_event_callback(\"init\", on_init)\n        self._register_callbacks()\n        # register_event_callback(\"before.save\", before_save)\n        # register_event_callback(\"save\", on_save)\n        register_event_callback(\"open\", on_open)\n        # register_event_callback(\"new\", on_new)\n\n        log.info(\"Installing menu ... \")\n        self._install_menu()\n\n        project_settings = get_current_project_settings()\n        self._install_shelves(project_settings)\n\n        self._has_been_setup = True\n\n    def uninstall(self):\n        self._uninstall_shelves()\n        self._uninstall_menu()\n        self._deregister_callbacks()\n\n    def workfile_has_unsaved_changes(self):\n\n        if not substance_painter.project.is_open():\n            return False\n\n        return substance_painter.project.needs_saving()\n\n    def get_workfile_extensions(self):\n        return [\".spp\", \".toc\"]\n\n    def save_workfile(self, dst_path=None):\n\n        if not substance_painter.project.is_open():\n            return False\n\n        if not dst_path:\n            dst_path = self.get_current_workfile()\n\n        full_save_mode = substance_painter.project.ProjectSaveMode.Full\n        substance_painter.project.save_as(dst_path, full_save_mode)\n\n        return dst_path\n\n    def open_workfile(self, filepath):\n\n        if not os.path.exists(filepath):\n            raise RuntimeError(\"File does not exist: {}\".format(filepath))\n\n        # We must first explicitly close current project before opening another\n        if substance_painter.project.is_open():\n            substance_painter.project.close()\n\n        substance_painter.project.open(filepath)\n        return filepath\n\n    def get_current_workfile(self):\n        if not substance_painter.project.is_open():\n            return None\n\n        filepath = substance_painter.project.file_path()\n        if filepath and filepath.endswith(\".spt\"):\n            # When currently in a Substance Painter template assume our\n            # scene isn't saved. This can be the case directly after doing\n            # \"New project\", the path will then be the template used. This\n            # avoids Workfiles tool trying to save as .spt extension if the\n            # file hasn't been saved before.\n            return\n\n        return filepath\n\n    def get_containers(self):\n\n        if not substance_painter.project.is_open():\n            return\n\n        metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)\n        containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY)\n        if containers:\n            for key, container in containers.items():\n                container[\"objectName\"] = key\n                yield container\n\n    def update_context_data(self, data, changes):\n\n        if not substance_painter.project.is_open():\n            return\n\n        metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)\n        metadata.set(OPENPYPE_METADATA_CONTEXT_KEY, data)\n\n    def get_context_data(self):\n\n        if not substance_painter.project.is_open():\n            return\n\n        metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)\n        return metadata.get(OPENPYPE_METADATA_CONTEXT_KEY) or {}\n\n    def _install_menu(self):\n        from PySide2 import QtWidgets\n        from openpype.tools.utils import host_tools\n\n        parent = substance_painter.ui.get_main_window()\n\n        tab_menu_label = os.environ.get(\"AVALON_LABEL\") or \"AYON\"\n        menu = QtWidgets.QMenu(tab_menu_label)\n\n        action = menu.addAction(\"Create...\")\n        action.triggered.connect(\n            lambda: host_tools.show_publisher(parent=parent,\n                                              tab=\"create\")\n        )\n\n        action = menu.addAction(\"Load...\")\n        action.triggered.connect(\n            lambda: host_tools.show_loader(parent=parent, use_context=True)\n        )\n\n        action = menu.addAction(\"Publish...\")\n        action.triggered.connect(\n            lambda: host_tools.show_publisher(parent=parent,\n                                              tab=\"publish\")\n        )\n\n        action = menu.addAction(\"Manage...\")\n        action.triggered.connect(\n            lambda: host_tools.show_scene_inventory(parent=parent)\n        )\n\n        action = menu.addAction(\"Library...\")\n        action.triggered.connect(\n            lambda: host_tools.show_library_loader(parent=parent)\n        )\n\n        menu.addSeparator()\n        action = menu.addAction(\"Work Files...\")\n        action.triggered.connect(\n            lambda: host_tools.show_workfiles(parent=parent)\n        )\n\n        substance_painter.ui.add_menu(menu)\n\n        def on_menu_destroyed():\n            self.menu = None\n\n        menu.destroyed.connect(on_menu_destroyed)\n\n        self.menu = menu\n\n    def _uninstall_menu(self):\n        if self.menu:\n            self.menu.destroy()\n            self.menu = None\n\n    def _register_callbacks(self):\n        # Prepare emit event callbacks\n        open_callback = partial(emit_event, \"open\")\n\n        # Connect to the Substance Painter events\n        dispatcher = substance_painter.event.DISPATCHER\n        for event, callback in [\n            (substance_painter.event.ProjectOpened, open_callback)\n        ]:\n            dispatcher.connect(event, callback)\n            # Keep a reference so we can deregister if needed\n            self.callbacks.append((event, callback))\n\n    def _deregister_callbacks(self):\n        for event, callback in self.callbacks:\n            substance_painter.event.DISPATCHER.disconnect(event, callback)\n        self.callbacks.clear()\n\n    def _install_shelves(self, project_settings):\n\n        shelves = project_settings[\"substancepainter\"].get(\"shelves\", {})\n        if not shelves:\n            return\n\n        # Prepare formatting data if we detect any path which might have\n        # template tokens like {asset} in there.\n        formatting_data = {}\n        has_formatting_entries = any(\"{\" in path for path in shelves.values())\n        if has_formatting_entries:\n            project_name = self.get_current_project_name()\n            asset_name = self.get_current_asset_name()\n            task_name = self.get_current_asset_name()\n            system_settings = get_system_settings()\n            formatting_data = get_template_data_with_names(project_name,\n                                                           asset_name,\n                                                           task_name,\n                                                           system_settings)\n            anatomy = Anatomy(project_name)\n            formatting_data[\"root\"] = anatomy.roots\n\n        for name, path in shelves.items():\n            shelf_name = None\n\n            # Allow formatting with anatomy for the paths\n            if \"{\" in path:\n                path = StringTemplate.format_template(path, formatting_data)\n\n            try:\n                shelf_name = lib.load_shelf(path, name=name)\n            except ValueError as exc:\n                print(f\"Failed to load shelf -> {exc}\")\n\n            if shelf_name:\n                self.shelves.append(shelf_name)\n\n    def _uninstall_shelves(self):\n        for shelf_name in self.shelves:\n            substance_painter.resource.Shelves.remove(shelf_name)\n        self.shelves.clear()\n\n\ndef on_open():\n    log.info(\"Running callback on open..\")\n\n    if any_outdated_containers():\n        from openpype.widgets import popup\n\n        log.warning(\"Scene has outdated content.\")\n\n        # Get main window\n        parent = substance_painter.ui.get_main_window()\n        if parent is None:\n            log.info(\"Skipping outdated content pop-up \"\n                     \"because Substance window can't be found.\")\n        else:\n\n            # Show outdated pop-up\n            def _on_show_inventory():\n                from openpype.tools.utils import host_tools\n                host_tools.show_scene_inventory(parent=parent)\n\n            dialog = popup.Popup(parent=parent)\n            dialog.setWindowTitle(\"Substance scene has outdated content\")\n            dialog.setMessage(\"There are outdated containers in \"\n                              \"your Substance scene.\")\n            dialog.on_clicked.connect(_on_show_inventory)\n            dialog.show()\n\n\ndef imprint_container(container,\n                      name,\n                      namespace,\n                      context,\n                      loader):\n    \"\"\"Imprint a loaded container with metadata.\n\n    Containerisation enables a tracking of version, author and origin\n    for loaded assets.\n\n    Arguments:\n        container (dict): The (substance metadata) dictionary to imprint into.\n        name (str): Name of resulting assembly\n        namespace (str): Namespace under which to host container\n        context (dict): Asset information\n        loader (load.LoaderPlugin): loader instance used to produce container.\n\n    Returns:\n        None\n\n    \"\"\"\n\n    data = [\n        (\"schema\", \"openpype:container-2.0\"),\n        (\"id\", AVALON_CONTAINER_ID),\n        (\"name\", str(name)),\n        (\"namespace\", str(namespace) if namespace else None),\n        (\"loader\", str(loader.__class__.__name__)),\n        (\"representation\", str(context[\"representation\"][\"_id\"])),\n    ]\n    for key, value in data:\n        container[key] = value\n\n\ndef set_container_metadata(object_name, container_data, update=False):\n    \"\"\"Helper method to directly set the data for a specific container\n\n    Args:\n        object_name (str): The unique object name identifier for the container\n        container_data (dict): The data for the container.\n            Note 'objectName' data is derived from `object_name` and key in\n            `container_data` will be ignored.\n        update (bool): Whether to only update the dict data.\n\n    \"\"\"\n    # The objectName is derived from the key in the metadata so won't be stored\n    # in the metadata in the container's data.\n    container_data.pop(\"objectName\", None)\n\n    metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)\n    containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) or {}\n    if update:\n        existing_data = containers.setdefault(object_name, {})\n        existing_data.update(container_data)  # mutable dict, in-place update\n    else:\n        containers[object_name] = container_data\n    metadata.set(\"containers\", containers)\n\n\ndef remove_container_metadata(object_name):\n    \"\"\"Helper method to remove the data for a specific container\"\"\"\n    metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)\n    containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY)\n    if containers:\n        containers.pop(object_name, None)\n        metadata.set(\"containers\", containers)\n\n\ndef set_instance(instance_id, instance_data, update=False):\n    \"\"\"Helper method to directly set the data for a specific container\n\n    Args:\n        instance_id (str): Unique identifier for the instance\n        instance_data (dict): The instance data to store in the metaadata.\n    \"\"\"\n    set_instances({instance_id: instance_data}, update=update)\n\n\ndef set_instances(instance_data_by_id, update=False):\n    \"\"\"Store data for multiple instances at the same time.\n\n    This is more optimal than querying and setting them in the metadata one\n    by one.\n    \"\"\"\n    metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)\n    instances = metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {}\n\n    for instance_id, instance_data in instance_data_by_id.items():\n        if update:\n            existing_data = instances.get(instance_id, {})\n            existing_data.update(instance_data)\n        else:\n            instances[instance_id] = instance_data\n\n    metadata.set(\"instances\", instances)\n\n\ndef remove_instance(instance_id):\n    \"\"\"Helper method to remove the data for a specific container\"\"\"\n    metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)\n    instances = metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {}\n    instances.pop(instance_id, None)\n    metadata.set(\"instances\", instances)\n\n\ndef get_instances_by_id():\n    \"\"\"Return all instances stored in the project instances metadata\"\"\"\n    if not substance_painter.project.is_open():\n        return {}\n\n    metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)\n    return metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {}\n\n\ndef get_instances():\n    \"\"\"Return all instances stored in the project instances as a list\"\"\"\n    return list(get_instances_by_id().values())\n"
  },
  {
    "path": "openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py",
    "content": "\n\ndef cleanup_openpype_qt_widgets():\n    \"\"\"\n        Workaround for Substance failing to shut down correctly\n        when a Qt window was still open at the time of shutting down.\n\n        This seems to work sometimes, but not all the time.\n\n    \"\"\"\n    # TODO: Create a more reliable method to close down all OpenPype Qt widgets\n    from PySide2 import QtWidgets\n    import substance_painter.ui\n\n    # Kill OpenPype Qt widgets\n    print(\"Killing OpenPype Qt widgets..\")\n    for widget in QtWidgets.QApplication.topLevelWidgets():\n        if widget.__module__.startswith(\"openpype.\"):\n            print(f\"Deleting widget: {widget.__class__.__name__}\")\n            substance_painter.ui.delete_ui_element(widget)\n\n\ndef start_plugin():\n    from openpype.pipeline import install_host\n    from openpype.hosts.substancepainter.api import SubstanceHost\n    install_host(SubstanceHost())\n\n\ndef close_plugin():\n    from openpype.pipeline import uninstall_host\n    cleanup_openpype_qt_widgets()\n    uninstall_host()\n\n\nif __name__ == \"__main__\":\n    start_plugin()\n"
  },
  {
    "path": "openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py",
    "content": "\"\"\"Ease the OpenPype on-boarding process by loading the plug-in on first run\"\"\"\n\nOPENPYPE_PLUGIN_NAME = \"openpype_plugin\"\n\n\ndef start_plugin():\n    try:\n        # This isn't exposed in the official API so we keep it in a try-except\n        from painter_plugins_ui import (\n            get_settings,\n            LAUNCH_AT_START_KEY,\n            ON_STATE,\n            PLUGINS_MENU,\n            plugin_manager\n        )\n\n        # The `painter_plugins_ui` plug-in itself is also a startup plug-in\n        # we need to take into account that it could run either earlier or\n        # later than this startup script, we check whether its menu initialized\n        is_before_plugins_menu = PLUGINS_MENU is None\n\n        settings = get_settings(OPENPYPE_PLUGIN_NAME)\n        if settings.value(LAUNCH_AT_START_KEY, None) is None:\n            print(\"Initializing OpenPype plug-in on first run...\")\n            if is_before_plugins_menu:\n                print(\"- running before 'painter_plugins_ui'\")\n                # Delay the launch to the painter_plugins_ui initialization\n                settings.setValue(LAUNCH_AT_START_KEY, ON_STATE)\n            else:\n                # Launch now\n                print(\"- running after 'painter_plugins_ui'\")\n                plugin_manager(OPENPYPE_PLUGIN_NAME)(True)\n\n                # Set the checked state in the menu to avoid confusion\n                action = next(action for action in PLUGINS_MENU._menu.actions()\n                              if action.text() == OPENPYPE_PLUGIN_NAME)\n                if action is not None:\n                    action.blockSignals(True)\n                    action.setChecked(True)\n                    action.blockSignals(False)\n\n    except Exception as exc:\n        print(exc)\n"
  },
  {
    "path": "openpype/hosts/substancepainter/plugins/create/create_textures.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating textures.\"\"\"\n\nfrom openpype.pipeline import CreatedInstance, Creator, CreatorError\nfrom openpype.lib import (\n    EnumDef,\n    UILabelDef,\n    NumberDef,\n    BoolDef\n)\n\nfrom openpype.hosts.substancepainter.api.pipeline import (\n    get_instances,\n    set_instance,\n    set_instances,\n    remove_instance\n)\nfrom openpype.hosts.substancepainter.api.lib import get_export_presets\n\nimport substance_painter.project\n\n\nclass CreateTextures(Creator):\n    \"\"\"Create a texture set.\"\"\"\n    identifier = \"io.openpype.creators.substancepainter.textureset\"\n    label = \"Textures\"\n    family = \"textureSet\"\n    icon = \"picture-o\"\n\n    default_variant = \"Main\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n\n        if not substance_painter.project.is_open():\n            raise CreatorError(\"Can't create a Texture Set instance without \"\n                               \"an open project.\")\n        # Transfer settings from pre create to instance\n        creator_attributes = instance_data.setdefault(\n            \"creator_attributes\", dict())\n        for key in [\n            \"exportPresetUrl\",\n            \"exportFileFormat\",\n            \"exportSize\",\n            \"exportPadding\",\n            \"exportDilationDistance\"\n        ]:\n            if key in pre_create_data:\n                creator_attributes[key] = pre_create_data[key]\n\n        instance = self.create_instance_in_context(subset_name,\n                                                   instance_data)\n        set_instance(\n            instance_id=instance[\"instance_id\"],\n            instance_data=instance.data_to_store()\n        )\n\n    def collect_instances(self):\n        for instance in get_instances():\n            if (instance.get(\"creator_identifier\") == self.identifier or\n                    instance.get(\"family\") == self.family):\n                self.create_instance_in_context_from_existing(instance)\n\n    def update_instances(self, update_list):\n        instance_data_by_id = {}\n        for instance, _changes in update_list:\n            # Persist the data\n            instance_id = instance.get(\"instance_id\")\n            instance_data = instance.data_to_store()\n            instance_data_by_id[instance_id] = instance_data\n        set_instances(instance_data_by_id, update=True)\n\n    def remove_instances(self, instances):\n        for instance in instances:\n            remove_instance(instance[\"instance_id\"])\n            self._remove_instance_from_context(instance)\n\n    # Helper methods (this might get moved into Creator class)\n    def create_instance_in_context(self, subset_name, data):\n        instance = CreatedInstance(\n            self.family, subset_name, data, self\n        )\n        self.create_context.creator_adds_instance(instance)\n        return instance\n\n    def create_instance_in_context_from_existing(self, data):\n        instance = CreatedInstance.from_existing(data, self)\n        self.create_context.creator_adds_instance(instance)\n        return instance\n\n    def get_instance_attr_defs(self):\n\n        return [\n            EnumDef(\"exportPresetUrl\",\n                    items=get_export_presets(),\n                    label=\"Output Template\"),\n            BoolDef(\"allowSkippedMaps\",\n                    label=\"Allow Skipped Output Maps\",\n                    tooltip=\"When enabled this allows the publish to ignore \"\n                            \"output maps in the used output template if one \"\n                            \"or more maps are skipped due to the required \"\n                            \"channels not being present in the current file.\",\n                    default=True),\n            EnumDef(\"exportFileFormat\",\n                    items={\n                        None: \"Based on output template\",\n                        # TODO: Get available extensions from substance API\n                        \"bmp\": \"bmp\",\n                        \"ico\": \"ico\",\n                        \"jpeg\": \"jpeg\",\n                        \"jng\": \"jng\",\n                        \"pbm\": \"pbm\",\n                        \"pgm\": \"pgm\",\n                        \"png\": \"png\",\n                        \"ppm\": \"ppm\",\n                        \"tga\": \"targa\",\n                        \"tif\": \"tiff\",\n                        \"wap\": \"wap\",\n                        \"wbmp\": \"wbmp\",\n                        \"xpm\": \"xpm\",\n                        \"gif\": \"gif\",\n                        \"hdr\": \"hdr\",\n                        \"exr\": \"exr\",\n                        \"j2k\": \"j2k\",\n                        \"jp2\": \"jp2\",\n                        \"pfm\": \"pfm\",\n                        \"webp\": \"webp\",\n                        # TODO: Unsure why jxr format fails to export\n                        # \"jxr\": \"jpeg-xr\",\n                        # TODO: File formats that combine the exported textures\n                        #   like psd are not correctly supported due to\n                        #   publishing only a single file\n                        # \"psd\": \"psd\",\n                        # \"sbsar\": \"sbsar\",\n                    },\n                    default=None,\n                    label=\"File type\"),\n            EnumDef(\"exportSize\",\n                    items={\n                        None: \"Based on each Texture Set's size\",\n                        #  The key is size of the texture file in log2.\n                        #  (i.e. 10 means 2^10 = 1024)\n                        7: \"128\",\n                        8: \"256\",\n                        9: \"512\",\n                        10: \"1024\",\n                        11: \"2048\",\n                        12: \"4096\"\n                    },\n                    default=None,\n                    label=\"Size\"),\n\n            EnumDef(\"exportPadding\",\n                    items={\n                        \"passthrough\": \"No padding (passthrough)\",\n                        \"infinite\": \"Dilation infinite\",\n                        \"transparent\": \"Dilation + transparent\",\n                        \"color\": \"Dilation + default background color\",\n                        \"diffusion\": \"Dilation + diffusion\"\n                    },\n                    default=\"infinite\",\n                    label=\"Padding\"),\n            NumberDef(\"exportDilationDistance\",\n                      minimum=0,\n                      maximum=256,\n                      decimals=0,\n                      default=16,\n                      label=\"Dilation Distance\"),\n            UILabelDef(\"*only used with \"\n                       \"'Dilation + <x>' padding\"),\n        ]\n\n    def get_pre_create_attr_defs(self):\n        # Use same attributes as for instance attributes\n        return self.get_instance_attr_defs()\n"
  },
  {
    "path": "openpype/hosts/substancepainter/plugins/create/create_workfile.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator plugin for creating workfiles.\"\"\"\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import CreatedInstance, AutoCreator\nfrom openpype.client import get_asset_by_name\n\nfrom openpype.hosts.substancepainter.api.pipeline import (\n    set_instances,\n    set_instance,\n    get_instances\n)\n\nimport substance_painter.project\n\n\nclass CreateWorkfile(AutoCreator):\n    \"\"\"Workfile auto-creator.\"\"\"\n    identifier = \"io.openpype.creators.substancepainter.workfile\"\n    label = \"Workfile\"\n    family = \"workfile\"\n    icon = \"document\"\n\n    default_variant = \"Main\"\n\n    def create(self):\n\n        if not substance_painter.project.is_open():\n            return\n\n        variant = self.default_variant\n        project_name = self.project_name\n        asset_name = self.create_context.get_current_asset_name()\n        task_name = self.create_context.get_current_task_name()\n        host_name = self.create_context.host_name\n\n        # Workfile instance should always exist and must only exist once.\n        # As such we'll first check if it already exists and is collected.\n        current_instance = next(\n            (\n                instance for instance in self.create_context.instances\n                if instance.creator_identifier == self.identifier\n            ), None)\n\n        if current_instance is None:\n            current_instance_asset = None\n        elif AYON_SERVER_ENABLED:\n            current_instance_asset = current_instance[\"folderPath\"]\n        else:\n            current_instance_asset = current_instance[\"asset\"]\n\n        if current_instance is None:\n            self.log.info(\"Auto-creating workfile instance...\")\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                variant, task_name, asset_doc, project_name, host_name\n            )\n            data = {\n                \"task\": task_name,\n                \"variant\": variant\n            }\n            if AYON_SERVER_ENABLED:\n                data[\"folderPath\"] = asset_name\n            else:\n                data[\"asset\"] = asset_name\n            current_instance = self.create_instance_in_context(subset_name,\n                                                               data)\n        elif (\n            current_instance_asset != asset_name\n            or current_instance[\"task\"] != task_name\n        ):\n            # Update instance context if is not the same\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                variant, task_name, asset_doc, project_name, host_name\n            )\n            if AYON_SERVER_ENABLED:\n                current_instance[\"folderPath\"] = asset_name\n            else:\n                current_instance[\"asset\"] = asset_name\n            current_instance[\"task\"] = task_name\n            current_instance[\"subset\"] = subset_name\n\n        set_instance(\n            instance_id=current_instance.get(\"instance_id\"),\n            instance_data=current_instance.data_to_store()\n        )\n\n    def collect_instances(self):\n        for instance in get_instances():\n            if (instance.get(\"creator_identifier\") == self.identifier or\n                    instance.get(\"family\") == self.family):\n                self.create_instance_in_context_from_existing(instance)\n\n    def update_instances(self, update_list):\n        instance_data_by_id = {}\n        for instance, _changes in update_list:\n            # Persist the data\n            instance_id = instance.get(\"instance_id\")\n            instance_data = instance.data_to_store()\n            instance_data_by_id[instance_id] = instance_data\n        set_instances(instance_data_by_id, update=True)\n\n    # Helper methods (this might get moved into Creator class)\n    def create_instance_in_context(self, subset_name, data):\n        instance = CreatedInstance(\n            self.family, subset_name, data, self\n        )\n        self.create_context.creator_adds_instance(instance)\n        return instance\n\n    def create_instance_in_context_from_existing(self, data):\n        instance = CreatedInstance.from_existing(data, self)\n        self.create_context.creator_adds_instance(instance)\n        return instance\n"
  },
  {
    "path": "openpype/hosts/substancepainter/plugins/load/load_mesh.py",
    "content": "import copy\nfrom qtpy import QtWidgets, QtCore\nfrom openpype.pipeline import (\n    load,\n    get_representation_path,\n)\nfrom openpype.pipeline.load import LoadError\nfrom openpype.hosts.substancepainter.api.pipeline import (\n    imprint_container,\n    set_container_metadata,\n    remove_container_metadata\n)\nfrom openpype.hosts.substancepainter.api.lib import prompt_new_file_with_mesh\n\nimport substance_painter.project\n\n\ndef _convert(substance_attr):\n    \"\"\"Return Substance Painter Python API Project attribute from string.\n\n    This converts a string like \"ProjectWorkflow.Default\" to for example\n    the Substance Painter Python API equivalent object, like:\n        `substance_painter.project.ProjectWorkflow.Default`\n\n    Args:\n        substance_attr (str): The `substance_painter.project` attribute,\n            for example \"ProjectWorkflow.Default\"\n\n    Returns:\n        Any: Substance Python API object of the project attribute.\n\n    Raises:\n        ValueError: If attribute does not exist on the\n            `substance_painter.project` python api.\n    \"\"\"\n    root = substance_painter.project\n    for attr in substance_attr.split(\".\"):\n        root = getattr(root, attr, None)\n        if root is None:\n            raise ValueError(\n                \"Substance Painter project attribute\"\n                f\" does not exist: {substance_attr}\")\n\n    return root\n\n\ndef get_template_by_name(name: str, templates: list[dict]) -> dict:\n    return next(\n        template for template in templates\n        if template[\"name\"] == name\n    )\n\n\nclass SubstanceProjectConfigurationWindow(QtWidgets.QDialog):\n    \"\"\"The pop-up dialog allows users to choose material\n    duplicate options for importing Max objects when updating\n    or switching assets.\n    \"\"\"\n    def __init__(self, project_templates):\n        super(SubstanceProjectConfigurationWindow, self).__init__()\n        self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)\n\n        self.configuration = None\n        self.template_names = [template[\"name\"] for template\n                               in project_templates]\n        self.project_templates = project_templates\n\n        self.widgets = {\n            \"label\": QtWidgets.QLabel(\n                \"Select your template for project configuration\"),\n            \"template_options\": QtWidgets.QComboBox(),\n            \"import_cameras\": QtWidgets.QCheckBox(\"Import Cameras\"),\n            \"preserve_strokes\": QtWidgets.QCheckBox(\"Preserve Strokes\"),\n            \"clickbox\": QtWidgets.QWidget(),\n            \"combobox\": QtWidgets.QWidget(),\n            \"buttons\": QtWidgets.QDialogButtonBox(\n                QtWidgets.QDialogButtonBox.Ok\n                | QtWidgets.QDialogButtonBox.Cancel)\n        }\n\n        self.widgets[\"template_options\"].addItems(self.template_names)\n\n        template_name = self.widgets[\"template_options\"].currentText()\n        self._update_to_match_template(template_name)\n        # Build clickboxes\n        layout = QtWidgets.QHBoxLayout(self.widgets[\"clickbox\"])\n        layout.addWidget(self.widgets[\"import_cameras\"])\n        layout.addWidget(self.widgets[\"preserve_strokes\"])\n        # Build combobox\n        layout = QtWidgets.QHBoxLayout(self.widgets[\"combobox\"])\n        layout.addWidget(self.widgets[\"template_options\"])\n        # Build buttons\n        layout = QtWidgets.QHBoxLayout(self.widgets[\"buttons\"])\n        # Build layout.\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(self.widgets[\"label\"])\n        layout.addWidget(self.widgets[\"combobox\"])\n        layout.addWidget(self.widgets[\"clickbox\"])\n        layout.addWidget(self.widgets[\"buttons\"])\n\n        self.widgets[\"template_options\"].currentTextChanged.connect(\n            self._update_to_match_template)\n        self.widgets[\"buttons\"].accepted.connect(self.on_accept)\n        self.widgets[\"buttons\"].rejected.connect(self.on_reject)\n\n    def on_accept(self):\n        self.configuration = self.get_project_configuration()\n        self.close()\n\n    def on_reject(self):\n        self.close()\n\n    def _update_to_match_template(self, template_name):\n        template = get_template_by_name(template_name, self.project_templates)\n        self.widgets[\"import_cameras\"].setChecked(template[\"import_cameras\"])\n        self.widgets[\"preserve_strokes\"].setChecked(\n            template[\"preserve_strokes\"])\n\n    def get_project_configuration(self):\n        templates = self.project_templates\n        template_name = self.widgets[\"template_options\"].currentText()\n        template = get_template_by_name(template_name, templates)\n        template = copy.deepcopy(template)  # do not edit the original\n        template[\"import_cameras\"] = self.widgets[\"import_cameras\"].isChecked()\n        template[\"preserve_strokes\"] = (\n            self.widgets[\"preserve_strokes\"].isChecked()\n        )\n        for key in [\"normal_map_format\",\n                    \"project_workflow\",\n                    \"tangent_space_mode\"]:\n            template[key] = _convert(template[key])\n        return template\n\n    @classmethod\n    def prompt(cls, templates):\n        dialog = cls(templates)\n        dialog.exec_()\n        configuration = dialog.configuration\n        dialog.deleteLater()\n        return configuration\n\n\nclass SubstanceLoadProjectMesh(load.LoaderPlugin):\n    \"\"\"Load mesh for project\"\"\"\n\n    families = [\"*\"]\n    representations = [\"abc\", \"fbx\", \"obj\", \"gltf\"]\n\n    label = \"Load mesh\"\n    order = -10\n    icon = \"code-fork\"\n    color = \"orange\"\n\n    # Defined via settings\n    project_templates = []\n\n    def load(self, context, name, namespace, options=None):\n\n        # Get user inputs\n        result = SubstanceProjectConfigurationWindow.prompt(\n            self.project_templates)\n        if not result:\n            # cancelling loader action\n            return\n        sp_settings = substance_painter.project.Settings(\n            import_cameras=result[\"import_cameras\"],\n            normal_map_format=result[\"normal_map_format\"],\n            project_workflow=result[\"project_workflow\"],\n            tangent_space_mode=result[\"tangent_space_mode\"],\n            default_texture_resolution=result[\"default_texture_resolution\"]\n        )\n        if not substance_painter.project.is_open():\n            # Allow to 'initialize' a new project\n            path = self.filepath_from_context(context)\n            sp_settings = substance_painter.project.Settings(\n                import_cameras=result[\"import_cameras\"],\n                normal_map_format=result[\"normal_map_format\"],\n                project_workflow=result[\"project_workflow\"],\n                tangent_space_mode=result[\"tangent_space_mode\"],\n                default_texture_resolution=result[\"default_texture_resolution\"]\n            )\n            settings = substance_painter.project.create(\n                mesh_file_path=path, settings=sp_settings\n            )\n        else:\n            # Reload the mesh\n            settings = substance_painter.project.MeshReloadingSettings(\n                import_cameras=result[\"import_cameras\"],\n                preserve_strokes=result[\"preserve_strokes\"])\n\n            def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus):  # noqa\n                if status == substance_painter.project.ReloadMeshStatus.SUCCESS:  # noqa\n                    self.log.info(\"Reload succeeded\")\n                else:\n                    raise LoadError(\"Reload of mesh failed\")\n\n            path = self.filepath_from_context(context)\n            substance_painter.project.reload_mesh(path,\n                                                  settings,\n                                                  on_mesh_reload)\n\n        # Store container\n        container = {}\n        project_mesh_object_name = \"_ProjectMesh_\"\n        imprint_container(container,\n                          name=project_mesh_object_name,\n                          namespace=project_mesh_object_name,\n                          context=context,\n                          loader=self)\n\n        # We want store some options for updating to keep consistent behavior\n        # from the user's original choice. We don't store 'preserve_strokes'\n        # as we always preserve strokes on updates.\n        container[\"options\"] = {\n            \"import_cameras\": result[\"import_cameras\"],\n        }\n\n        set_container_metadata(project_mesh_object_name, container)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n\n        path = get_representation_path(representation)\n\n        # Reload the mesh\n        container_options = container.get(\"options\", {})\n        settings = substance_painter.project.MeshReloadingSettings(\n            import_cameras=container_options.get(\"import_cameras\", True),\n            preserve_strokes=True\n        )\n\n        def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus):\n            if status == substance_painter.project.ReloadMeshStatus.SUCCESS:\n                self.log.info(\"Reload succeeded\")\n            else:\n                raise LoadError(\"Reload of mesh failed\")\n\n        substance_painter.project.reload_mesh(path, settings, on_mesh_reload)\n\n        # Update container representation\n        object_name = container[\"objectName\"]\n        update_data = {\"representation\": str(representation[\"_id\"])}\n        set_container_metadata(object_name, update_data, update=True)\n\n    def remove(self, container):\n\n        # Remove OpenPype related settings about what model was loaded\n        # or close the project?\n        # TODO: This is likely best 'hidden' away to the user because\n        #       this will leave the project's mesh unmanaged.\n        remove_container_metadata(container[\"objectName\"])\n"
  },
  {
    "path": "openpype/hosts/substancepainter/plugins/publish/collect_current_file.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import registered_host\n\n\nclass CollectCurrentFile(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current working file into context\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.49\n    label = \"Current Workfile\"\n    hosts = [\"substancepainter\"]\n\n    def process(self, context):\n        host = registered_host()\n        path = host.get_current_workfile()\n        context.data[\"currentFile\"] = path\n        self.log.debug(f\"Current workfile: {path}\")\n"
  },
  {
    "path": "openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py",
    "content": "import os\nimport copy\nimport pyblish.api\n\nfrom openpype.pipeline import publish\n\nimport substance_painter.textureset\nfrom openpype.hosts.substancepainter.api.lib import (\n    get_parsed_export_maps,\n    strip_template\n)\nfrom openpype.pipeline.create import get_subset_name\nfrom openpype.client import get_asset_by_name\n\n\nclass CollectTextureSet(pyblish.api.InstancePlugin):\n    \"\"\"Extract Textures using an output template config\"\"\"\n    # TODO: Production-test usage of color spaces\n    # TODO: Detect what source data channels end up in each file\n\n    label = \"Collect Texture Set images\"\n    hosts = [\"substancepainter\"]\n    families = [\"textureSet\"]\n    order = pyblish.api.CollectorOrder\n\n    def process(self, instance):\n\n        config = self.get_export_config(instance)\n        asset_doc = get_asset_by_name(\n            project_name=instance.context.data[\"projectName\"],\n            asset_name=instance.data[\"asset\"]\n        )\n\n        instance.data[\"exportConfig\"] = config\n        maps = get_parsed_export_maps(config)\n\n        # Let's break the instance into multiple instances to integrate\n        # a subset per generated texture or texture UDIM sequence\n        for (texture_set_name, stack_name), template_maps in maps.items():\n            self.log.info(f\"Processing {texture_set_name}/{stack_name}\")\n            for template, outputs in template_maps.items():\n                self.log.info(f\"Processing {template}\")\n                self.create_image_instance(instance, template, outputs,\n                                           asset_doc=asset_doc,\n                                           texture_set_name=texture_set_name,\n                                           stack_name=stack_name)\n\n    def create_image_instance(self, instance, template, outputs,\n                              asset_doc, texture_set_name, stack_name):\n        \"\"\"Create a new instance per image or UDIM sequence.\n\n        The new instances will be of family `image`.\n\n        \"\"\"\n\n        context = instance.context\n        first_filepath = outputs[0][\"filepath\"]\n        fnames = [os.path.basename(output[\"filepath\"]) for output in outputs]\n        ext = os.path.splitext(first_filepath)[1]\n        assert ext.lstrip(\".\"), f\"No extension: {ext}\"\n\n        always_include_texture_set_name = False  # todo: make this configurable\n        all_texture_sets = substance_painter.textureset.all_texture_sets()\n        texture_set = substance_painter.textureset.TextureSet.from_name(\n            texture_set_name\n        )\n\n        # Define the suffix we want to give this particular texture\n        # set and set up a remapped subset naming for it.\n        suffix = \"\"\n        if always_include_texture_set_name or len(all_texture_sets) > 1:\n            # More than one texture set, include texture set name\n            suffix += f\".{texture_set_name}\"\n        if texture_set.is_layered_material() and stack_name:\n            # More than one stack, include stack name\n            suffix += f\".{stack_name}\"\n\n        # Always include the map identifier\n        map_identifier = strip_template(template)\n        suffix += f\".{map_identifier}\"\n\n        image_subset = get_subset_name(\n            # TODO: The family actually isn't 'texture' currently but for now\n            #       this is only done so the subset name starts with 'texture'\n            family=\"texture\",\n            variant=instance.data[\"variant\"] + suffix,\n            task_name=instance.data.get(\"task\"),\n            asset_doc=asset_doc,\n            project_name=context.data[\"projectName\"],\n            host_name=context.data[\"hostName\"],\n            project_settings=context.data[\"project_settings\"]\n        )\n\n        # Prepare representation\n        representation = {\n            \"name\": ext.lstrip(\".\"),\n            \"ext\": ext.lstrip(\".\"),\n            \"files\": fnames if len(fnames) > 1 else fnames[0],\n        }\n\n        # Mark as UDIM explicitly if it has UDIM tiles.\n        if bool(outputs[0].get(\"udim\")):\n            # The representation for a UDIM sequence should have a `udim` key\n            # that is a list of all udim tiles (str) like: [\"1001\", \"1002\"]\n            # strings. See CollectTextures plug-in and Integrators.\n            representation[\"udim\"] = [output[\"udim\"] for output in outputs]\n\n        # Set up the representation for thumbnail generation\n        # TODO: Simplify this once thumbnail extraction is refactored\n        staging_dir = os.path.dirname(first_filepath)\n        representation[\"tags\"] = [\"review\"]\n        representation[\"stagingDir\"] = staging_dir\n\n        # Clone the instance\n        image_instance = context.create_instance(image_subset)\n        image_instance[:] = instance[:]\n        image_instance.data.update(copy.deepcopy(dict(instance.data)))\n        image_instance.data[\"name\"] = image_subset\n        image_instance.data[\"label\"] = image_subset\n        image_instance.data[\"subset\"] = image_subset\n        image_instance.data[\"family\"] = \"image\"\n        image_instance.data[\"families\"] = [\"image\", \"textures\"]\n        image_instance.data[\"representations\"] = [representation]\n\n        # Group the textures together in the loader\n        image_instance.data[\"subsetGroup\"] = instance.data[\"subset\"]\n\n        # Store the texture set name and stack name on the instance\n        image_instance.data[\"textureSetName\"] = texture_set_name\n        image_instance.data[\"textureStackName\"] = stack_name\n\n        # Store color space with the instance\n        # Note: The extractor will assign it to the representation\n        colorspace = outputs[0].get(\"colorSpace\")\n        if colorspace:\n            self.log.debug(f\"{image_subset} colorspace: {colorspace}\")\n            image_instance.data[\"colorspace\"] = colorspace\n\n        # Store the instance in the original instance as a member\n        instance.append(image_instance)\n\n    def get_export_config(self, instance):\n        \"\"\"Return an export configuration dict for texture exports.\n\n        This config can be supplied to:\n            - `substance_painter.export.export_project_textures`\n            - `substance_painter.export.list_project_textures`\n\n        See documentation on substance_painter.export module about the\n        formatting of the configuration dictionary.\n\n        Args:\n            instance (pyblish.api.Instance): Texture Set instance to be\n                published.\n\n        Returns:\n            dict: Export config\n\n        \"\"\"\n\n        creator_attrs = instance.data[\"creator_attributes\"]\n        preset_url = creator_attrs[\"exportPresetUrl\"]\n        self.log.debug(f\"Exporting using preset: {preset_url}\")\n\n        # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export  # noqa\n        config = {  # noqa\n            \"exportShaderParams\": True,\n            \"exportPath\": publish.get_instance_staging_dir(instance),\n            \"defaultExportPreset\": preset_url,\n\n            # Custom overrides to the exporter\n            \"exportParameters\": [\n                {\n                    \"parameters\": {\n                        \"fileFormat\": creator_attrs[\"exportFileFormat\"],\n                        \"sizeLog2\": creator_attrs[\"exportSize\"],\n                        \"paddingAlgorithm\": creator_attrs[\"exportPadding\"],\n                        \"dilationDistance\": creator_attrs[\"exportDilationDistance\"]  # noqa\n                    }\n                }\n            ]\n        }\n\n        # Create the list of Texture Sets to export.\n        config[\"exportList\"] = []\n        for texture_set in substance_painter.textureset.all_texture_sets():\n            config[\"exportList\"].append({\"rootPath\": texture_set.name()})\n\n        # Consider None values from the creator attributes optionals\n        for override in config[\"exportParameters\"]:\n            parameters = override.get(\"parameters\")\n            for key, value in dict(parameters).items():\n                if value is None:\n                    parameters.pop(key)\n\n        return config\n"
  },
  {
    "path": "openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py",
    "content": "import os\nimport pyblish.api\n\n\nclass CollectWorkfileRepresentation(pyblish.api.InstancePlugin):\n    \"\"\"Create a publish representation for the current workfile instance.\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = \"Workfile representation\"\n    hosts = [\"substancepainter\"]\n    families = [\"workfile\"]\n\n    def process(self, instance):\n\n        context = instance.context\n        current_file = context.data[\"currentFile\"]\n\n        folder, file = os.path.split(current_file)\n        filename, ext = os.path.splitext(file)\n\n        instance.data[\"representations\"] = [{\n            \"name\": ext.lstrip(\".\"),\n            \"ext\": ext.lstrip(\".\"),\n            \"files\": file,\n            \"stagingDir\": folder,\n        }]\n"
  },
  {
    "path": "openpype/hosts/substancepainter/plugins/publish/extract_textures.py",
    "content": "import substance_painter.export\n\nfrom openpype.pipeline import KnownPublishError, publish\n\n\nclass ExtractTextures(publish.Extractor,\n                      publish.ColormanagedPyblishPluginMixin):\n    \"\"\"Extract Textures using an output template config.\n\n    Note:\n        This Extractor assumes that `collect_textureset_images` has prepared\n        the relevant export config and has also collected the individual image\n        instances for publishing including its representation. That is why this\n        particular Extractor doesn't specify representations to integrate.\n\n    \"\"\"\n\n    label = \"Extract Texture Set\"\n    hosts = [\"substancepainter\"]\n    families = [\"textureSet\"]\n\n    # Run before thumbnail extractors\n    order = publish.Extractor.order - 0.1\n\n    def process(self, instance):\n\n        config = instance.data[\"exportConfig\"]\n        result = substance_painter.export.export_project_textures(config)\n\n        if result.status != substance_painter.export.ExportStatus.Success:\n            raise KnownPublishError(\n                \"Failed to export texture set: {}\".format(result.message)\n            )\n\n        # Log what files we generated\n        for (texture_set_name, stack_name), maps in result.textures.items():\n            # Log our texture outputs\n            self.log.info(f\"Exported stack: {texture_set_name} {stack_name}\")\n            for texture_map in maps:\n                self.log.info(f\"Exported texture: {texture_map}\")\n\n        # We'll insert the color space data for each image instance that we\n        # added into this texture set. The collector couldn't do so because\n        # some anatomy and other instance data needs to be collected prior\n        context = instance.context\n        for image_instance in instance:\n            representation = next(iter(image_instance.data[\"representations\"]))\n\n            colorspace = image_instance.data.get(\"colorspace\")\n            if not colorspace:\n                self.log.debug(\"No color space data present for instance: \"\n                               f\"{image_instance}\")\n                continue\n\n            self.set_representation_colorspace(representation,\n                                               context=context,\n                                               colorspace=colorspace)\n\n        # The TextureSet instance should not be integrated. It generates no\n        # output data. Instead the separated texture instances are generated\n        # from it which themselves integrate into the database.\n        instance.data[\"integrate\"] = False\n"
  },
  {
    "path": "openpype/hosts/substancepainter/plugins/publish/increment_workfile.py",
    "content": "import pyblish.api\n\nfrom openpype.lib import version_up\nfrom openpype.pipeline import registered_host\n\n\nclass IncrementWorkfileVersion(pyblish.api.ContextPlugin):\n    \"\"\"Increment current workfile version.\"\"\"\n\n    order = pyblish.api.IntegratorOrder + 1\n    label = \"Increment Workfile Version\"\n    optional = True\n    hosts = [\"substancepainter\"]\n\n    def process(self, context):\n\n        assert all(result[\"success\"] for result in context.data[\"results\"]), (\n            \"Publishing not successful so version is not increased.\")\n\n        host = registered_host()\n        path = context.data[\"currentFile\"]\n        self.log.info(f\"Incrementing current workfile to: {path}\")\n        host.save_workfile(version_up(path))\n"
  },
  {
    "path": "openpype/hosts/substancepainter/plugins/publish/save_workfile.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import (\n    registered_host,\n    KnownPublishError\n)\n\n\nclass SaveCurrentWorkfile(pyblish.api.ContextPlugin):\n    \"\"\"Save current workfile\"\"\"\n\n    label = \"Save current workfile\"\n    order = pyblish.api.ExtractorOrder - 0.49\n    hosts = [\"substancepainter\"]\n\n    def process(self, context):\n\n        host = registered_host()\n        current = host.get_current_workfile()\n        if context.data[\"currentFile\"] != current:\n            raise KnownPublishError(\"Workfile has changed during publishing!\")\n\n        if host.workfile_has_unsaved_changes():\n            self.log.info(\"Saving current file: {}\".format(current))\n            host.save_workfile()\n        else:\n            self.log.debug(\"Skipping workfile save because there are no \"\n                           \"unsaved changes.\")\n"
  },
  {
    "path": "openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py",
    "content": "import copy\nimport os\n\nimport pyblish.api\n\nimport substance_painter.export\n\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateOutputMaps(pyblish.api.InstancePlugin):\n    \"\"\"Validate all output maps for Output Template are generated.\n\n    Output maps will be skipped by Substance Painter if it is an output\n    map in the Substance Output Template which uses channels that the current\n    substance painter project has not painted or generated.\n\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate output maps\"\n    hosts = [\"substancepainter\"]\n    families = [\"textureSet\"]\n\n    def process(self, instance):\n\n        config = instance.data[\"exportConfig\"]\n\n        # Substance Painter API does not allow to query the actual output maps\n        # it will generate without actually exporting the files. So we try to\n        # generate the smallest size / fastest export as possible\n        config = copy.deepcopy(config)\n        parameters = config[\"exportParameters\"][0][\"parameters\"]\n        parameters[\"sizeLog2\"] = [1, 1]     # output 2x2 images (smallest)\n        parameters[\"paddingAlgorithm\"] = \"passthrough\"  # no dilation (faster)\n        parameters[\"dithering\"] = False     # no dithering (faster)\n\n        result = substance_painter.export.export_project_textures(config)\n        if result.status != substance_painter.export.ExportStatus.Success:\n            raise PublishValidationError(\n                \"Failed to export texture set: {}\".format(result.message)\n            )\n\n        generated_files = set()\n        for texture_maps in result.textures.values():\n            for texture_map in texture_maps:\n                generated_files.add(os.path.normpath(texture_map))\n                # Directly clean up our temporary export\n                os.remove(texture_map)\n\n        creator_attributes = instance.data.get(\"creator_attributes\", {})\n        allow_skipped_maps = creator_attributes.get(\"allowSkippedMaps\", True)\n        error_report_missing = []\n        for image_instance in instance:\n\n            # Confirm whether the instance has its expected files generated.\n            # We assume there's just one representation and that it is\n            # the actual texture representation from the collector.\n            representation = next(iter(image_instance.data[\"representations\"]))\n            staging_dir = representation[\"stagingDir\"]\n            filenames = representation[\"files\"]\n            if not isinstance(filenames, (list, tuple)):\n                # Convert single file to list\n                filenames = [filenames]\n\n            missing = []\n            for filename in filenames:\n                filepath = os.path.join(staging_dir, filename)\n                filepath = os.path.normpath(filepath)\n                if filepath not in generated_files:\n                    self.log.warning(f\"Missing texture: {filepath}\")\n                    missing.append(filepath)\n\n            if not missing:\n                continue\n\n            if allow_skipped_maps:\n                # TODO: This is changing state on the instance's which\n                #   should not be done during validation.\n                self.log.warning(f\"Disabling texture instance: \"\n                                 f\"{image_instance}\")\n                image_instance.data[\"active\"] = False\n                image_instance.data[\"publish\"] = False\n                image_instance.data[\"integrate\"] = False\n                representation.setdefault(\"tags\", []).append(\"delete\")\n                continue\n            else:\n                error_report_missing.append((image_instance, missing))\n\n        if error_report_missing:\n\n            message = (\n                \"The Texture Set skipped exporting some output maps which are \"\n                \"defined in the Output Template. This happens if the Output \"\n                \"Templates exports maps from channels which you do not \"\n                \"have in your current Substance Painter project.\\n\\n\"\n                \"To allow this enable the *Allow Skipped Output Maps* setting \"\n                \"on the instance.\\n\\n\"\n                f\"Instance {instance} skipped exporting output maps:\\n\"\n                \"\"\n            )\n\n            for image_instance, missing in error_report_missing:\n                missing_str = \", \".join(missing)\n                message += f\"- **{image_instance}** skipped: {missing_str}\\n\"\n\n            raise PublishValidationError(\n                message=message,\n                title=\"Missing output maps\"\n            )\n"
  },
  {
    "path": "openpype/hosts/traypublisher/__init__.py",
    "content": "from .addon import TrayPublishAddon\n\n\n__all__ = (\n    \"TrayPublishAddon\",\n)\n"
  },
  {
    "path": "openpype/hosts/traypublisher/addon.py",
    "content": "import os\n\nfrom openpype.lib import get_openpype_execute_args\nfrom openpype.lib.execute import run_detached_process\nfrom openpype.modules import (\n    click_wrap,\n    OpenPypeModule,\n    ITrayAction,\n    IHostAddon,\n)\n\nTRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass TrayPublishAddon(OpenPypeModule, IHostAddon, ITrayAction):\n    label = \"Publisher\"\n    name = \"traypublisher\"\n    host_name = \"traypublisher\"\n\n    def initialize(self, modules_settings):\n        self.enabled = True\n        self.publish_paths = [\n            os.path.join(TRAYPUBLISH_ROOT_DIR, \"plugins\", \"publish\")\n        ]\n\n    def tray_init(self):\n        return\n\n    def on_action_trigger(self):\n        self.run_traypublisher()\n\n    def connect_with_modules(self, enabled_modules):\n        \"\"\"Collect publish paths from other modules.\"\"\"\n        publish_paths = self.manager.collect_plugin_paths()[\"publish\"]\n        self.publish_paths.extend(publish_paths)\n\n    def run_traypublisher(self):\n        args = get_openpype_execute_args(\n            \"module\", self.name, \"launch\"\n        )\n        run_detached_process(args)\n\n    def cli(self, click_group):\n        click_group.add_command(cli_main.to_click_obj())\n\n\n@click_wrap.group(\n    TrayPublishAddon.name,\n    help=\"TrayPublisher related commands.\")\ndef cli_main():\n    pass\n\n\n@cli_main.command()\ndef launch():\n    \"\"\"Launch TrayPublish tool UI.\"\"\"\n\n    from openpype.tools import traypublisher\n\n    traypublisher.main()\n"
  },
  {
    "path": "openpype/hosts/traypublisher/api/__init__.py",
    "content": "from .pipeline import (\n    TrayPublisherHost,\n)\n\n\n__all__ = (\n    \"TrayPublisherHost\",\n)\n"
  },
  {
    "path": "openpype/hosts/traypublisher/api/editorial.py",
    "content": "import re\nfrom copy import deepcopy\n\nfrom openpype.client import get_asset_by_id\nfrom openpype.pipeline.create import CreatorError\n\n\nclass ShotMetadataSolver:\n    \"\"\" Solving hierarchical metadata\n\n    Used during editorial publishing. Works with input\n    clip name and settings defining python formatable\n    template. Settings also define searching patterns\n    and its token keys used for formatting in templates.\n    \"\"\"\n\n    NO_DECOR_PATERN = re.compile(r\"\\{([a-z]*?)\\}\")\n\n    # presets\n    clip_name_tokenizer = None\n    shot_rename = True\n    shot_hierarchy = None\n    shot_add_tasks = None\n\n    def __init__(\n        self,\n        clip_name_tokenizer,\n        shot_rename,\n        shot_hierarchy,\n        shot_add_tasks,\n        logger\n    ):\n        self.clip_name_tokenizer = clip_name_tokenizer\n        self.shot_rename = shot_rename\n        self.shot_hierarchy = shot_hierarchy\n        self.shot_add_tasks = shot_add_tasks\n        self.log = logger\n\n    def _rename_template(self, data):\n        \"\"\"Shot renaming function\n\n        Args:\n            data (dict): formatting data\n\n        Raises:\n            CreatorError: If missing keys\n\n        Returns:\n            str: formatted new name\n        \"\"\"\n        shot_rename_template = self.shot_rename[\n            \"shot_rename_template\"]\n        try:\n            # format to new shot name\n            return shot_rename_template.format(**data)\n        except KeyError as _error:\n            raise CreatorError((\n                \"Make sure all keys in settings are correct:: \\n\\n\"\n                f\"From template string {shot_rename_template} > \"\n                f\"`{_error}` has no equivalent in \\n\"\n                f\"{list(data.keys())} input formatting keys!\"\n            ))\n\n    def _generate_tokens(self, clip_name, source_data):\n        \"\"\"Token generator\n\n        Settings defines token pairs key and regex expression.\n\n        Args:\n            clip_name (str): name of clip in editorial\n            source_data (dict): data for formatting\n\n        Raises:\n            CreatorError: if missing key\n\n        Returns:\n            dict: updated source_data\n        \"\"\"\n        output_data = deepcopy(source_data[\"anatomy_data\"])\n        output_data[\"clip_name\"] = clip_name\n\n        if not self.clip_name_tokenizer:\n            return output_data\n\n        parent_name = source_data[\"selected_asset_doc\"][\"name\"]\n\n        search_text = parent_name + clip_name\n\n        for token_key, pattern in self.clip_name_tokenizer.items():\n            p = re.compile(pattern)\n            match = p.findall(search_text)\n            if not match:\n                raise CreatorError((\n                    \"Make sure regex expression works with your data: \\n\\n\"\n                    f\"'{token_key}' with regex '{pattern}' in your settings\\n\"\n                    \"can't find any match in your clip name \"\n                    f\"'{search_text}'!\\n\\nLook to: \"\n                    \"'project_settings/traypublisher/editorial_creators\"\n                    \"/editorial_simple/clip_name_tokenizer'\\n\"\n                    \"at your project settings...\"\n                ))\n\n            #  QUESTION:how to refactor `match[-1]` to some better way?\n            output_data[token_key] = match[-1]\n\n        return output_data\n\n    def _create_parents_from_settings(self, parents, data):\n        \"\"\"formatting parent components.\n\n        Args:\n            parents (list): list of dict parent components\n            data (dict): formatting data\n\n        Raises:\n            CreatorError: missing formatting key\n            CreatorError: missing token key\n            KeyError: missing parent token\n\n        Returns:\n            list: list of dict of parent components\n        \"\"\"\n        # fill the parents parts from presets\n        shot_hierarchy = deepcopy(self.shot_hierarchy)\n        hierarchy_parents = shot_hierarchy[\"parents\"]\n\n        # fill parent keys data template from anatomy data\n        try:\n            _parent_tokens_formatting_data = {\n                parent_token[\"name\"]: parent_token[\"value\"].format(**data)\n                for parent_token in hierarchy_parents\n            }\n        except KeyError as _error:\n            raise CreatorError((\n                \"Make sure all keys in settings are correct : \\n\"\n                f\"`{_error}` has no equivalent in \\n{list(data.keys())}\"\n            ))\n\n        _parent_tokens_type = {\n            parent_token[\"name\"]: parent_token[\"type\"]\n            for parent_token in hierarchy_parents\n        }\n        for _index, _parent in enumerate(\n                shot_hierarchy[\"parents_path\"].split(\"/\")\n        ):\n            # format parent token with value which is formatted\n            try:\n                parent_name = _parent.format(\n                    **_parent_tokens_formatting_data)\n            except KeyError as _error:\n                raise CreatorError((\n                    \"Make sure all keys in settings are correct : \\n\\n\"\n                    f\"`{_error}` from template string \"\n                    f\"{shot_hierarchy['parents_path']}, \"\n                    f\" has no equivalent in \\n\"\n                    f\"{list(_parent_tokens_formatting_data.keys())} parents\"\n                ))\n\n            parent_token_name = (\n                self.NO_DECOR_PATERN.findall(_parent).pop())\n\n            if not parent_token_name:\n                raise KeyError(\n                    f\"Parent token is not found in: `{_parent}`\")\n\n            # find parent type\n            parent_token_type = _parent_tokens_type[parent_token_name]\n\n            # in case selected context is set to the same asset\n            if (\n                _index == 0\n                and parents[-1][\"entity_name\"] == parent_name\n            ):\n                continue\n\n            # in case first parent is project then start parents from start\n            if (\n                _index == 0\n                and parent_token_type == \"Project\"\n            ):\n                project_parent = parents[0]\n                parents = [project_parent]\n                continue\n\n            parents.append({\n                \"entity_type\": parent_token_type,\n                \"entity_name\": parent_name\n            })\n\n        return parents\n\n    def _create_hierarchy_path(self, parents):\n        \"\"\"Converting hierarchy path from parents\n\n        Args:\n            parents (list): list of dict parent components\n\n        Returns:\n            str: hierarchy path\n        \"\"\"\n        return \"/\".join(\n            [\n                p[\"entity_name\"] for p in parents\n                if p[\"entity_type\"] != \"Project\"\n            ]\n        ) if parents else \"\"\n\n    def _get_parents_from_selected_asset(\n        self,\n        asset_doc,\n        project_doc\n    ):\n        \"\"\"Returning parents from context on selected asset.\n\n        Context defined in Traypublisher project tree.\n\n        Args:\n            asset_doc (db obj): selected asset doc\n            project_doc (db obj): actual project doc\n\n        Returns:\n            list:  list of dict parent components\n        \"\"\"\n        project_name = project_doc[\"name\"]\n        visual_hierarchy = [asset_doc]\n        current_doc = asset_doc\n\n        # looping through all available visual parents\n        # if they are not available anymore than it breaks\n        while True:\n            visual_parent_id = current_doc[\"data\"][\"visualParent\"]\n            visual_parent = None\n            if visual_parent_id:\n                visual_parent = get_asset_by_id(project_name, visual_parent_id)\n\n            if not visual_parent:\n                visual_hierarchy.append(project_doc)\n                break\n            visual_hierarchy.append(visual_parent)\n            current_doc = visual_parent\n\n        # add current selection context hierarchy\n        return [\n            {\n                \"entity_type\": entity[\"data\"][\"entityType\"],\n                \"entity_name\": entity[\"name\"]\n            }\n            for entity in reversed(visual_hierarchy)\n        ]\n\n    def _generate_tasks_from_settings(self, project_doc):\n        \"\"\"Convert settings inputs to task data.\n\n        Args:\n            project_doc (db obj): actual project doc\n\n        Raises:\n            KeyError: Missing task type in project doc\n\n        Returns:\n            dict: tasks data\n        \"\"\"\n        tasks_to_add = {}\n\n        project_tasks = project_doc[\"config\"][\"tasks\"]\n        for task_name, task_data in self.shot_add_tasks.items():\n            _task_data = deepcopy(task_data)\n\n            # check if task type in project task types\n            if _task_data[\"type\"] in project_tasks.keys():\n                tasks_to_add[task_name] = _task_data\n            else:\n                raise KeyError(\n                    \"Missing task type `{}` for `{}` is not\"\n                    \" existing in `{}``\".format(\n                        _task_data[\"type\"],\n                        task_name,\n                        list(project_tasks.keys())\n                    )\n                )\n\n        return tasks_to_add\n\n    def generate_data(self, clip_name, source_data):\n        \"\"\"Metadata generator.\n\n        Converts input data to hierarchy mentadata.\n\n        Args:\n            clip_name (str): clip name\n            source_data (dict): formatting data\n\n        Returns:\n            (str, dict): shot name and hierarchy data\n        \"\"\"\n\n        tasks = {}\n        asset_doc = source_data[\"selected_asset_doc\"]\n        project_doc = source_data[\"project_doc\"]\n\n        # match clip to shot name at start\n        shot_name = clip_name\n\n        # parse all tokens and generate formatting data\n        formatting_data = self._generate_tokens(shot_name, source_data)\n\n        # generate parents from selected asset\n        parents = self._get_parents_from_selected_asset(asset_doc, project_doc)\n\n        if self.shot_rename[\"enabled\"]:\n            shot_name = self._rename_template(formatting_data)\n            self.log.info(f\"Renamed shot name: {shot_name}\")\n\n        if self.shot_hierarchy[\"enabled\"]:\n            parents = self._create_parents_from_settings(\n                parents, formatting_data)\n\n        if self.shot_add_tasks:\n            tasks = self._generate_tasks_from_settings(\n                project_doc)\n\n        # generate hierarchy path from parents\n        hierarchy_path = self._create_hierarchy_path(parents)\n        if hierarchy_path:\n            folder_path = f\"/{hierarchy_path}/{shot_name}\"\n        else:\n            folder_path = f\"/{shot_name}\"\n\n        return shot_name, {\n            \"hierarchy\": hierarchy_path,\n            \"folderPath\": folder_path,\n            \"parents\": parents,\n            \"tasks\": tasks\n        }\n"
  },
  {
    "path": "openpype/hosts/traypublisher/api/pipeline.py",
    "content": "import os\nimport json\nimport tempfile\nimport atexit\n\nimport pyblish.api\n\nfrom openpype.pipeline import (\n    register_creator_plugin_path,\n    legacy_io,\n)\nfrom openpype.host import HostBase, IPublishHost\n\n\nROOT_DIR = os.path.dirname(os.path.dirname(\n    os.path.abspath(__file__)\n))\nPUBLISH_PATH = os.path.join(ROOT_DIR, \"plugins\", \"publish\")\nCREATE_PATH = os.path.join(ROOT_DIR, \"plugins\", \"create\")\n\n\nclass TrayPublisherHost(HostBase, IPublishHost):\n    name = \"traypublisher\"\n\n    def install(self):\n        os.environ[\"AVALON_APP\"] = self.name\n        legacy_io.Session[\"AVALON_APP\"] = self.name\n\n        pyblish.api.register_host(\"traypublisher\")\n        pyblish.api.register_plugin_path(PUBLISH_PATH)\n        register_creator_plugin_path(CREATE_PATH)\n\n    def get_context_title(self):\n        return HostContext.get_project_name()\n\n    def get_context_data(self):\n        return HostContext.get_context_data()\n\n    def update_context_data(self, data, changes):\n        HostContext.save_context_data(data)\n\n    def set_project_name(self, project_name):\n        # TODO Deregister project specific plugins and register new project\n        #   plugins\n        os.environ[\"AVALON_PROJECT\"] = project_name\n        legacy_io.Session[\"AVALON_PROJECT\"] = project_name\n        legacy_io.install()\n        HostContext.set_project_name(project_name)\n\n\nclass HostContext:\n    _context_json_path = None\n\n    @staticmethod\n    def _on_exit():\n        if (\n            HostContext._context_json_path\n            and os.path.exists(HostContext._context_json_path)\n        ):\n            os.remove(HostContext._context_json_path)\n\n    @classmethod\n    def get_context_json_path(cls):\n        if cls._context_json_path is None:\n            output_file = tempfile.NamedTemporaryFile(\n                mode=\"w\", prefix=\"traypub_\", suffix=\".json\"\n            )\n            output_file.close()\n            cls._context_json_path = output_file.name\n            atexit.register(HostContext._on_exit)\n            print(cls._context_json_path)\n        return cls._context_json_path\n\n    @classmethod\n    def _get_data(cls, group=None):\n        json_path = cls.get_context_json_path()\n        data = {}\n        if not os.path.exists(json_path):\n            with open(json_path, \"w\") as json_stream:\n                json.dump(data, json_stream)\n        else:\n            with open(json_path, \"r\") as json_stream:\n                content = json_stream.read()\n            if content:\n                data = json.loads(content)\n        if group is None:\n            return data\n        return data.get(group)\n\n    @classmethod\n    def _save_data(cls, group, new_data):\n        json_path = cls.get_context_json_path()\n        data = cls._get_data()\n        data[group] = new_data\n        with open(json_path, \"w\") as json_stream:\n            json.dump(data, json_stream)\n\n    @classmethod\n    def add_instance(cls, instance):\n        instances = cls.get_instances()\n        instances.append(instance)\n        cls.save_instances(instances)\n\n    @classmethod\n    def get_instances(cls):\n        return cls._get_data(\"instances\") or []\n\n    @classmethod\n    def save_instances(cls, instances):\n        cls._save_data(\"instances\", instances)\n\n    @classmethod\n    def get_context_data(cls):\n        return cls._get_data(\"context\") or {}\n\n    @classmethod\n    def save_context_data(cls, data):\n        cls._save_data(\"context\", data)\n\n    @classmethod\n    def get_project_name(cls):\n        return cls._get_data(\"project_name\")\n\n    @classmethod\n    def set_project_name(cls, project_name):\n        cls._save_data(\"project_name\", project_name)\n\n    @classmethod\n    def get_data_to_store(cls):\n        return {\n            \"project_name\": cls.get_project_name(),\n            \"instances\": cls.get_instances(),\n            \"context\": cls.get_context_data(),\n        }\n\n\ndef list_instances():\n    return HostContext.get_instances()\n\n\ndef update_instances(update_list):\n    updated_instances = {}\n    for instance, _changes in update_list:\n        updated_instances[instance.id] = instance.data_to_store()\n\n    instances = HostContext.get_instances()\n    for instance_data in instances:\n        instance_id = instance_data[\"instance_id\"]\n        if instance_id in updated_instances:\n            new_instance_data = updated_instances[instance_id]\n            old_keys = set(instance_data.keys())\n            new_keys = set(new_instance_data.keys())\n            instance_data.update(new_instance_data)\n            for key in (old_keys - new_keys):\n                instance_data.pop(key)\n\n    HostContext.save_instances(instances)\n\n\ndef remove_instances(instances):\n    if not isinstance(instances, (tuple, list)):\n        instances = [instances]\n\n    current_instances = HostContext.get_instances()\n    for instance in instances:\n        instance_id = instance.data[\"instance_id\"]\n        found_idx = None\n        for idx, _instance in enumerate(current_instances):\n            if instance_id == _instance[\"instance_id\"]:\n                found_idx = idx\n                break\n\n        if found_idx is not None:\n            current_instances.pop(found_idx)\n    HostContext.save_instances(current_instances)\n\n\ndef get_context_data():\n    return HostContext.get_context_data()\n\n\ndef update_context_data(data, changes):\n    HostContext.save_context_data(data)\n"
  },
  {
    "path": "openpype/hosts/traypublisher/api/plugin.py",
    "content": "from openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_assets,\n    get_subsets,\n    get_last_versions,\n    get_asset_name_identifier,\n)\nfrom openpype.lib.attribute_definitions import (\n    FileDef,\n    BoolDef,\n    NumberDef,\n    UISeparatorDef,\n)\nfrom openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS\nfrom openpype.pipeline.create import (\n    Creator,\n    HiddenCreator,\n    CreatedInstance,\n    cache_and_get_instances,\n    PRE_CREATE_THUMBNAIL_KEY,\n)\nfrom .pipeline import (\n    list_instances,\n    update_instances,\n    remove_instances,\n    HostContext,\n)\n\nREVIEW_EXTENSIONS = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS)\nSHARED_DATA_KEY = \"openpype.traypublisher.instances\"\n\n\nclass HiddenTrayPublishCreator(HiddenCreator):\n    host_name = \"traypublisher\"\n    settings_category = \"traypublisher\"\n\n    def collect_instances(self):\n        instances_by_identifier = cache_and_get_instances(\n            self, SHARED_DATA_KEY, list_instances\n        )\n        for instance_data in instances_by_identifier[self.identifier]:\n            instance = CreatedInstance.from_existing(instance_data, self)\n            self._add_instance_to_context(instance)\n\n    def update_instances(self, update_list):\n        update_instances(update_list)\n\n    def remove_instances(self, instances):\n        remove_instances(instances)\n        for instance in instances:\n            self._remove_instance_from_context(instance)\n\n    def _store_new_instance(self, new_instance):\n        \"\"\"Tray publisher specific method to store instance.\n\n        Instance is stored into \"workfile\" of traypublisher and also add it\n        to CreateContext.\n\n        Args:\n            new_instance (CreatedInstance): Instance that should be stored.\n        \"\"\"\n\n        # Host implementation of storing metadata about instance\n        HostContext.add_instance(new_instance.data_to_store())\n        # Add instance to current context\n        self._add_instance_to_context(new_instance)\n\n\nclass TrayPublishCreator(Creator):\n    create_allow_context_change = True\n    host_name = \"traypublisher\"\n    settings_category = \"traypublisher\"\n\n    def collect_instances(self):\n        instances_by_identifier = cache_and_get_instances(\n            self, SHARED_DATA_KEY, list_instances\n        )\n        for instance_data in instances_by_identifier[self.identifier]:\n            instance = CreatedInstance.from_existing(instance_data, self)\n            self._add_instance_to_context(instance)\n\n    def update_instances(self, update_list):\n        update_instances(update_list)\n\n    def remove_instances(self, instances):\n        remove_instances(instances)\n        for instance in instances:\n            self._remove_instance_from_context(instance)\n\n    def _store_new_instance(self, new_instance):\n        \"\"\"Tray publisher specific method to store instance.\n\n        Instance is stored into \"workfile\" of traypublisher and also add it\n        to CreateContext.\n\n        Args:\n            new_instance (CreatedInstance): Instance that should be stored.\n        \"\"\"\n\n        # Host implementation of storing metadata about instance\n        HostContext.add_instance(new_instance.data_to_store())\n        new_instance.mark_as_stored()\n\n        # Add instance to current context\n        self._add_instance_to_context(new_instance)\n\n\nclass SettingsCreator(TrayPublishCreator):\n    create_allow_context_change = True\n    create_allow_thumbnail = True\n    allow_version_control = False\n\n    extensions = []\n\n    def create(self, subset_name, data, pre_create_data):\n        # Pass precreate data to creator attributes\n        thumbnail_path = pre_create_data.pop(PRE_CREATE_THUMBNAIL_KEY, None)\n\n        # Fill 'version_to_use' if version control is enabled\n        if self.allow_version_control:\n            if AYON_SERVER_ENABLED:\n                asset_name = data[\"folderPath\"]\n            else:\n                asset_name = data[\"asset\"]\n            subset_docs_by_asset_id = self._prepare_next_versions(\n                [asset_name], [subset_name])\n            version = subset_docs_by_asset_id[asset_name].get(subset_name)\n            pre_create_data[\"version_to_use\"] = version\n            data[\"_previous_last_version\"] = version\n\n        data[\"creator_attributes\"] = pre_create_data\n        data[\"settings_creator\"] = True\n\n        # Create new instance\n        new_instance = CreatedInstance(self.family, subset_name, data, self)\n\n        self._store_new_instance(new_instance)\n\n        if thumbnail_path:\n            self.set_instance_thumbnail_path(new_instance.id, thumbnail_path)\n\n    def _prepare_next_versions(self, asset_names, subset_names):\n        \"\"\"Prepare next versions for given asset and subset names.\n\n        Todos:\n            Expect combination of subset names by asset name to avoid\n                unnecessary server calls for unused subsets.\n\n        Args:\n            asset_names (Iterable[str]): Asset names.\n            subset_names (Iterable[str]): Subset names.\n\n        Returns:\n            dict[str, dict[str, int]]: Last versions by asset\n                and subset names.\n        \"\"\"\n\n        # Prepare all versions for all combinations to '1'\n        subset_docs_by_asset_id = {\n            asset_name: {\n                subset_name: 1\n                for subset_name in subset_names\n            }\n            for asset_name in asset_names\n        }\n        if not asset_names or not subset_names:\n            return subset_docs_by_asset_id\n\n        asset_docs = get_assets(\n            self.project_name,\n            asset_names=asset_names,\n            fields=[\"_id\", \"name\", \"data.parents\"]\n        )\n        asset_names_by_id = {\n            asset_doc[\"_id\"]: get_asset_name_identifier(asset_doc)\n            for asset_doc in asset_docs\n        }\n        subset_docs = list(get_subsets(\n            self.project_name,\n            asset_ids=asset_names_by_id.keys(),\n            subset_names=subset_names,\n            fields=[\"_id\", \"name\", \"parent\"]\n        ))\n\n        subset_ids = {subset_doc[\"_id\"] for subset_doc in subset_docs}\n        last_versions = get_last_versions(\n            self.project_name,\n            subset_ids,\n            fields=[\"name\", \"parent\"])\n\n        for subset_doc in subset_docs:\n            asset_id = subset_doc[\"parent\"]\n            asset_name = asset_names_by_id[asset_id]\n            subset_name = subset_doc[\"name\"]\n            subset_id = subset_doc[\"_id\"]\n            last_version = last_versions.get(subset_id)\n            version = 0\n            if last_version is not None:\n                version = last_version[\"name\"]\n            subset_docs_by_asset_id[asset_name][subset_name] += version\n        return subset_docs_by_asset_id\n\n    def _fill_next_versions(self, instances_data):\n        \"\"\"Fill next version for instances.\n\n        Instances have also stored previous next version to be able to\n        recognize if user did enter different version. If version was\n        not changed by user, or user set it to '0' the next version will be\n        updated by current database state.\n        \"\"\"\n\n        filtered_instance_data = []\n        for instance in instances_data:\n            previous_last_version = instance.get(\"_previous_last_version\")\n            creator_attributes = instance[\"creator_attributes\"]\n            use_next_version = creator_attributes.get(\n                \"use_next_version\", True)\n            version = creator_attributes.get(\"version_to_use\", 0)\n            if (\n                use_next_version\n                or version == 0\n                or version == previous_last_version\n            ):\n                filtered_instance_data.append(instance)\n\n        if AYON_SERVER_ENABLED:\n            asset_names = {\n                instance[\"folderPath\"]\n                for instance in filtered_instance_data\n            }\n        else:\n            asset_names = {\n                instance[\"asset\"]\n                for instance in filtered_instance_data\n            }\n        subset_names = {\n            instance[\"subset\"]\n            for instance in filtered_instance_data}\n        subset_docs_by_asset_id = self._prepare_next_versions(\n            asset_names, subset_names\n        )\n        for instance in filtered_instance_data:\n            if AYON_SERVER_ENABLED:\n                asset_name = instance[\"folderPath\"]\n            else:\n                asset_name = instance[\"asset\"]\n            subset_name = instance[\"subset\"]\n            version = subset_docs_by_asset_id[asset_name][subset_name]\n            instance[\"creator_attributes\"][\"version_to_use\"] = version\n            instance[\"_previous_last_version\"] = version\n\n    def collect_instances(self):\n        \"\"\"Collect instances from host.\n\n        Overriden to be able to manage version control attributes. If version\n        control is disabled, the attributes will be removed from instances,\n        and next versions are filled if is version control enabled.\n        \"\"\"\n\n        instances_by_identifier = cache_and_get_instances(\n            self, SHARED_DATA_KEY, list_instances\n        )\n        instances = instances_by_identifier[self.identifier]\n        if not instances:\n            return\n\n        if self.allow_version_control:\n            self._fill_next_versions(instances)\n\n        for instance_data in instances:\n            # Make sure that there are not data related to version control\n            #   if plugin does not support it\n            if not self.allow_version_control:\n                instance_data.pop(\"_previous_last_version\", None)\n                creator_attributes = instance_data[\"creator_attributes\"]\n                creator_attributes.pop(\"version_to_use\", None)\n                creator_attributes.pop(\"use_next_version\", None)\n\n            instance = CreatedInstance.from_existing(instance_data, self)\n            self._add_instance_to_context(instance)\n\n    def get_instance_attr_defs(self):\n        defs = self.get_pre_create_attr_defs()\n        if self.allow_version_control:\n            defs += [\n                UISeparatorDef(),\n                BoolDef(\n                    \"use_next_version\",\n                    default=True,\n                    label=\"Use next version\",\n                ),\n                NumberDef(\n                    \"version_to_use\",\n                    default=1,\n                    minimum=0,\n                    maximum=999,\n                    label=\"Version to use\",\n                )\n            ]\n        return defs\n\n    def get_pre_create_attr_defs(self):\n        # Use same attributes as for instance attributes\n        return [\n            FileDef(\n                \"representation_files\",\n                folders=False,\n                extensions=self.extensions,\n                allow_sequences=self.allow_sequences,\n                single_item=not self.allow_multiple_items,\n                label=\"Representations\",\n            ),\n            FileDef(\n                \"reviewable\",\n                folders=False,\n                extensions=REVIEW_EXTENSIONS,\n                allow_sequences=True,\n                single_item=True,\n                label=\"Reviewable representations\",\n                extensions_label=\"Single reviewable item\"\n            )\n        ]\n\n    @classmethod\n    def from_settings(cls, item_data):\n        identifier = item_data[\"identifier\"]\n        family = item_data[\"family\"]\n        if not identifier:\n            identifier = \"settings_{}\".format(family)\n        return type(\n            \"{}{}\".format(cls.__name__, identifier),\n            (cls, ),\n            {\n                \"family\": family,\n                \"identifier\": identifier,\n                \"label\": item_data[\"label\"].strip(),\n                \"icon\": item_data[\"icon\"],\n                \"description\": item_data[\"description\"],\n                \"detailed_description\": item_data[\"detailed_description\"],\n                \"extensions\": item_data[\"extensions\"],\n                \"allow_sequences\": item_data[\"allow_sequences\"],\n                \"allow_multiple_items\": item_data[\"allow_multiple_items\"],\n                \"allow_version_control\": item_data.get(\n                    \"allow_version_control\", False),\n                \"default_variants\": item_data[\"default_variants\"],\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/traypublisher/batch_parsing.py",
    "content": "\"\"\"Functions to parse asset names, versions from file names\"\"\"\nimport os\nimport re\n\nfrom openpype.lib import Logger\nfrom openpype.client import get_assets, get_asset_by_name\n\n\ndef get_asset_doc_from_file_name(source_filename, project_name,\n                                 version_regex, all_selected_asset_ids=None):\n    \"\"\"Try to parse out asset name from file name provided.\n\n    Artists might provide various file name formats.\n    Currently handled:\n        - chair.mov\n        - chair_v001.mov\n        - my_chair_to_upload.mov\n    \"\"\"\n    version = None\n    asset_name = os.path.splitext(source_filename)[0]\n    # Always first check if source filename is directly asset (eg. 'chair.mov')\n    matching_asset_doc = get_asset_by_name_case_not_sensitive(\n        project_name, asset_name, all_selected_asset_ids)\n\n    if matching_asset_doc is None:\n        # name contains also a version\n        matching_asset_doc, version = (\n            parse_with_version(project_name, asset_name, version_regex,\n                               all_selected_asset_ids))\n\n    if matching_asset_doc is None:\n        matching_asset_doc = parse_containing(project_name, asset_name,\n                                              all_selected_asset_ids)\n\n    return matching_asset_doc, version\n\n\ndef parse_with_version(project_name, asset_name, version_regex,\n                       all_selected_asset_ids=None, log=None):\n    \"\"\"Try to parse asset name from a file name containing version too\n\n    Eg. 'chair_v001.mov' >> 'chair', 1\n    \"\"\"\n    if not log:\n        log = Logger.get_logger(__name__)\n    log.debug(\n        (\"Asset doc by \\\"{}\\\" was not found, trying version regex.\".\n         format(asset_name)))\n\n    matching_asset_doc = version_number = None\n\n    regex_result = version_regex.findall(asset_name)\n    if regex_result:\n        _asset_name, _version_number = regex_result[0]\n        matching_asset_doc = get_asset_by_name_case_not_sensitive(\n            project_name, _asset_name,\n            all_selected_asset_ids=all_selected_asset_ids)\n        if matching_asset_doc:\n            version_number = int(_version_number)\n\n    return matching_asset_doc, version_number\n\n\ndef parse_containing(project_name, asset_name, all_selected_asset_ids=None):\n    \"\"\"Look if file name contains any existing asset name\"\"\"\n    for asset_doc in get_assets(project_name, asset_ids=all_selected_asset_ids,\n                                fields=[\"name\"]):\n        if asset_doc[\"name\"].lower() in asset_name.lower():\n            return get_asset_by_name(project_name, asset_doc[\"name\"])\n\n\ndef get_asset_by_name_case_not_sensitive(project_name, asset_name,\n                                         all_selected_asset_ids=None,\n                                         log=None):\n    \"\"\"Handle more cases in file names\"\"\"\n    if not log:\n        log = Logger.get_logger(__name__)\n    asset_name = re.compile(asset_name, re.IGNORECASE)\n\n    assets = list(get_assets(project_name, asset_ids=all_selected_asset_ids,\n                             asset_names=[asset_name]))\n    if assets:\n        if len(assets) > 1:\n            log.warning(\"Too many records found for {}\".format(\n                asset_name))\n            return\n\n        return assets.pop()\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator of colorspace look files.\n\nThis creator is used to publish colorspace look files thanks to\nproduction type `ociolook`. All files are published as representation.\n\"\"\"\nfrom pathlib import Path\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import get_asset_by_name\nfrom openpype.lib.attribute_definitions import (\n    FileDef, EnumDef, TextDef, UISeparatorDef\n)\nfrom openpype.pipeline import (\n    CreatedInstance,\n    CreatorError\n)\nfrom openpype.pipeline import colorspace\nfrom openpype.hosts.traypublisher.api.plugin import TrayPublishCreator\n\n\nclass CreateColorspaceLook(TrayPublishCreator):\n    \"\"\"Creates colorspace look files.\"\"\"\n\n    identifier = \"io.openpype.creators.traypublisher.colorspace_look\"\n    label = \"Colorspace Look\"\n    family = \"ociolook\"\n    description = \"Publishes color space look file.\"\n    extensions = [\".cc\", \".cube\", \".3dl\", \".spi1d\", \".spi3d\", \".csp\", \".lut\"]\n    enabled = False\n\n    colorspace_items = [\n        (None, \"Not set\")\n    ]\n    colorspace_attr_show = False\n    config_items = None\n    config_data = None\n\n    def get_detail_description(self):\n        return \"\"\"# Colorspace Look\n\nThis creator publishes color space look file (LUT).\n        \"\"\"\n\n    def get_icon(self):\n        return \"mdi.format-color-fill\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        repr_file = pre_create_data.get(\"luts_file\")\n        if not repr_file:\n            raise CreatorError(\"No files specified\")\n\n        files = repr_file.get(\"filenames\")\n        if not files:\n            # this should never happen\n            raise CreatorError(\"Missing files from representation\")\n\n        if AYON_SERVER_ENABLED:\n            asset_name = instance_data[\"folderPath\"]\n        else:\n            asset_name = instance_data[\"asset\"]\n        asset_doc = get_asset_by_name(\n            self.project_name, asset_name)\n\n        subset_name = self.get_subset_name(\n            variant=instance_data[\"variant\"],\n            task_name=instance_data[\"task\"] or \"Not set\",\n            project_name=self.project_name,\n            asset_doc=asset_doc,\n        )\n\n        instance_data[\"creator_attributes\"] = {\n            \"abs_lut_path\": (\n                Path(repr_file[\"directory\"]) / files[0]).as_posix()\n        }\n\n        # Create new instance\n        new_instance = CreatedInstance(self.family, subset_name,\n                                       instance_data, self)\n        new_instance.transient_data[\"config_items\"] = self.config_items\n        new_instance.transient_data[\"config_data\"] = self.config_data\n\n        self._store_new_instance(new_instance)\n\n    def collect_instances(self):\n        super().collect_instances()\n        for instance in self.create_context.instances:\n            if instance.creator_identifier == self.identifier:\n                instance.transient_data[\"config_items\"] = self.config_items\n                instance.transient_data[\"config_data\"] = self.config_data\n\n    def get_instance_attr_defs(self):\n        return [\n            EnumDef(\n                \"working_colorspace\",\n                self.colorspace_items,\n                default=\"Not set\",\n                label=\"Working Colorspace\",\n            ),\n            UISeparatorDef(\n                label=\"Advanced1\"\n            ),\n            TextDef(\n                \"abs_lut_path\",\n                label=\"LUT Path\",\n            ),\n            EnumDef(\n                \"input_colorspace\",\n                self.colorspace_items,\n                default=\"Not set\",\n                label=\"Input Colorspace\",\n            ),\n            EnumDef(\n                \"direction\",\n                [\n                    (None, \"Not set\"),\n                    (\"forward\", \"Forward\"),\n                    (\"inverse\", \"Inverse\")\n                ],\n                default=\"Not set\",\n                label=\"Direction\"\n            ),\n            EnumDef(\n                \"interpolation\",\n                [\n                    (None, \"Not set\"),\n                    (\"linear\", \"Linear\"),\n                    (\"tetrahedral\", \"Tetrahedral\"),\n                    (\"best\", \"Best\"),\n                    (\"nearest\", \"Nearest\")\n                ],\n                default=\"Not set\",\n                label=\"Interpolation\"\n            ),\n            EnumDef(\n                \"output_colorspace\",\n                self.colorspace_items,\n                default=\"Not set\",\n                label=\"Output Colorspace\",\n            ),\n        ]\n\n    def get_pre_create_attr_defs(self):\n        return [\n            FileDef(\n                \"luts_file\",\n                folders=False,\n                extensions=self.extensions,\n                allow_sequences=False,\n                single_item=True,\n                label=\"Look Files\",\n            )\n        ]\n\n    def apply_settings(self, project_settings, system_settings):\n        host = self.create_context.host\n        host_name = host.name\n        project_name = host.get_current_project_name()\n        config_data = colorspace.get_imageio_config(\n            project_name, host_name,\n            project_settings=project_settings\n        )\n\n        if not config_data:\n            self.enabled = False\n            return\n\n        filepath = config_data[\"path\"]\n        config_items = colorspace.get_ocio_config_colorspaces(filepath)\n        labeled_colorspaces = colorspace.get_colorspaces_enumerator_items(\n            config_items,\n            include_aliases=True,\n            include_roles=True\n        )\n        self.config_items = config_items\n        self.config_data = config_data\n        self.colorspace_items.extend(labeled_colorspaces)\n        self.enabled = True\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/create/create_editorial.py",
    "content": "import os\nfrom copy import deepcopy\nimport opentimelineio as otio\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_asset_by_name,\n    get_project\n)\nfrom openpype.hosts.traypublisher.api.plugin import (\n    TrayPublishCreator,\n    HiddenTrayPublishCreator\n)\nfrom openpype.hosts.traypublisher.api.editorial import (\n    ShotMetadataSolver\n)\nfrom openpype.pipeline import CreatedInstance\nfrom openpype.lib import (\n    get_ffprobe_data,\n    convert_ffprobe_fps_value,\n\n    FileDef,\n    TextDef,\n    NumberDef,\n    EnumDef,\n    BoolDef,\n    UISeparatorDef,\n    UILabelDef\n)\n\n\nCLIP_ATTR_DEFS = [\n    EnumDef(\n        \"fps\",\n        items=[\n            {\"value\": \"from_selection\", \"label\": \"From selection\"},\n            {\"value\": 23.997, \"label\": \"23.976\"},\n            {\"value\": 24, \"label\": \"24\"},\n            {\"value\": 25, \"label\": \"25\"},\n            {\"value\": 29.97, \"label\": \"29.97\"},\n            {\"value\": 30, \"label\": \"30\"}\n        ],\n        label=\"FPS\"\n    ),\n    NumberDef(\n        \"workfile_start_frame\",\n        default=1001,\n        label=\"Workfile start frame\"\n    ),\n    NumberDef(\n        \"handle_start\",\n        default=0,\n        label=\"Handle start\"\n    ),\n    NumberDef(\n        \"handle_end\",\n        default=0,\n        label=\"Handle end\"\n    )\n]\n\n\nclass EditorialClipInstanceCreatorBase(HiddenTrayPublishCreator):\n    \"\"\" Wrapper class for clip family creators\n\n    Args:\n        HiddenTrayPublishCreator (BaseCreator): hidden supporting class\n    \"\"\"\n    host_name = \"traypublisher\"\n\n    def create(self, instance_data, source_data=None):\n        subset_name = instance_data[\"subset\"]\n\n        # Create new instance\n        new_instance = CreatedInstance(\n            self.family, subset_name, instance_data, self\n        )\n\n        self._store_new_instance(new_instance)\n\n        return new_instance\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\n                \"add_review_family\",\n                default=True,\n                label=\"Review\"\n            )\n        ]\n\n\nclass EditorialShotInstanceCreator(EditorialClipInstanceCreatorBase):\n    \"\"\" Shot family class\n\n    The shot metadata instance carrier.\n\n    Args:\n        EditorialClipInstanceCreatorBase (BaseCreator): hidden supporting class\n    \"\"\"\n    identifier = \"editorial_shot\"\n    family = \"shot\"\n    label = \"Editorial Shot\"\n\n    def get_instance_attr_defs(self):\n        instance_attributes = []\n        if AYON_SERVER_ENABLED:\n            instance_attributes.append(\n                TextDef(\n                    \"folderPath\",\n                    label=\"Folder path\"\n                )\n            )\n        else:\n            instance_attributes.append(\n                TextDef(\n                    \"shotName\",\n                    label=\"Shot name\"\n                )\n            )\n        instance_attributes.extend(CLIP_ATTR_DEFS)\n        return instance_attributes\n\n\nclass EditorialPlateInstanceCreator(EditorialClipInstanceCreatorBase):\n    \"\"\" Plate family class\n\n    Plate representation instance.\n\n    Args:\n        EditorialClipInstanceCreatorBase (BaseCreator): hidden supporting class\n    \"\"\"\n    identifier = \"editorial_plate\"\n    family = \"plate\"\n    label = \"Editorial Plate\"\n\n\nclass EditorialAudioInstanceCreator(EditorialClipInstanceCreatorBase):\n    \"\"\" Audio family class\n\n    Audio representation instance.\n\n    Args:\n        EditorialClipInstanceCreatorBase (BaseCreator): hidden supporting class\n    \"\"\"\n    identifier = \"editorial_audio\"\n    family = \"audio\"\n    label = \"Editorial Audio\"\n\n\nclass EditorialReviewInstanceCreator(EditorialClipInstanceCreatorBase):\n    \"\"\" Review family class\n\n    Review representation instance.\n\n    Args:\n        EditorialClipInstanceCreatorBase (BaseCreator): hidden supporting class\n    \"\"\"\n    identifier = \"editorial_review\"\n    family = \"review\"\n    label = \"Editorial Review\"\n\n\nclass EditorialSimpleCreator(TrayPublishCreator):\n    \"\"\" Editorial creator class\n\n    Simple workflow creator. This creator only disecting input\n    video file into clip chunks and then converts each to\n    defined format defined Settings for each subset preset.\n\n    Args:\n        TrayPublishCreator (Creator): Tray publisher plugin class\n    \"\"\"\n\n    label = \"Editorial Simple\"\n    family = \"editorial\"\n    identifier = \"editorial_simple\"\n    default_variants = [\n        \"main\"\n    ]\n    description = \"Editorial files to generate shots.\"\n    detailed_description = \"\"\"\nSupporting publishing new shots to project\nor updating already created. Publishing will create OTIO file.\n\"\"\"\n    icon = \"fa.file\"\n\n    def __init__(\n        self, project_settings, *args, **kwargs\n    ):\n        super(EditorialSimpleCreator, self).__init__(\n            project_settings, *args, **kwargs\n        )\n        editorial_creators = deepcopy(\n            project_settings[\"traypublisher\"][\"editorial_creators\"]\n        )\n        # get this creator settings by identifier\n        self._creator_settings = editorial_creators.get(self.identifier)\n\n        clip_name_tokenizer = self._creator_settings[\"clip_name_tokenizer\"]\n        shot_rename = self._creator_settings[\"shot_rename\"]\n        shot_hierarchy = self._creator_settings[\"shot_hierarchy\"]\n        shot_add_tasks = self._creator_settings[\"shot_add_tasks\"]\n\n        self._shot_metadata_solver = ShotMetadataSolver(\n            clip_name_tokenizer,\n            shot_rename,\n            shot_hierarchy,\n            shot_add_tasks,\n            self.log\n        )\n\n        # try to set main attributes from settings\n        if self._creator_settings.get(\"default_variants\"):\n            self.default_variants = self._creator_settings[\"default_variants\"]\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        allowed_family_presets = self._get_allowed_family_presets(\n            pre_create_data)\n\n        clip_instance_properties = {\n            k: v for k, v in pre_create_data.items()\n            if k != \"sequence_filepath_data\"\n            if k not in [\n                i[\"family\"] for i in self._creator_settings[\"family_presets\"]\n            ]\n        }\n        if AYON_SERVER_ENABLED:\n            asset_name = instance_data[\"folderPath\"]\n        else:\n            asset_name = instance_data[\"asset\"]\n\n        asset_doc = get_asset_by_name(self.project_name, asset_name)\n\n        if pre_create_data[\"fps\"] == \"from_selection\":\n            # get asset doc data attributes\n            fps = asset_doc[\"data\"][\"fps\"]\n        else:\n            fps = float(pre_create_data[\"fps\"])\n\n        instance_data.update({\n            \"fps\": fps\n        })\n\n        # get path of sequence\n        sequence_path_data = pre_create_data[\"sequence_filepath_data\"]\n        media_path_data = pre_create_data[\"media_filepaths_data\"]\n\n        sequence_paths = self._get_path_from_file_data(\n            sequence_path_data, multi=True)\n        media_path = self._get_path_from_file_data(media_path_data)\n\n        first_otio_timeline = None\n        for seq_path in sequence_paths:\n            # get otio timeline\n            otio_timeline = self._create_otio_timeline(\n                seq_path, fps)\n\n            # Create all clip instances\n            clip_instance_properties.update({\n                \"fps\": fps,\n                \"parent_asset_name\": asset_name,\n                \"variant\": instance_data[\"variant\"]\n            })\n\n            # create clip instances\n            self._get_clip_instances(\n                otio_timeline,\n                media_path,\n                clip_instance_properties,\n                allowed_family_presets,\n                os.path.basename(seq_path),\n                first_otio_timeline\n            )\n\n            if not first_otio_timeline:\n                # assign otio timeline for multi file to layer\n                first_otio_timeline = otio_timeline\n\n        # create otio editorial instance\n        self._create_otio_instance(\n            subset_name,\n            instance_data,\n            seq_path, media_path,\n            first_otio_timeline\n        )\n\n    def _create_otio_instance(\n        self,\n        subset_name,\n        data,\n        sequence_path,\n        media_path,\n        otio_timeline\n    ):\n        \"\"\"Otio instance creating function\n\n        Args:\n            subset_name (str): name of subset\n            data (dict): instance data\n            sequence_path (str): path to sequence file\n            media_path (str): path to media file\n            otio_timeline (otio.Timeline): otio timeline object\n        \"\"\"\n        # Pass precreate data to creator attributes\n        data.update({\n            \"sequenceFilePath\": sequence_path,\n            \"editorialSourcePath\": media_path,\n            \"otioTimeline\": otio.adapters.write_to_string(otio_timeline)\n        })\n        new_instance = CreatedInstance(\n            self.family, subset_name, data, self\n        )\n        self._store_new_instance(new_instance)\n\n    def _create_otio_timeline(self, sequence_path, fps):\n        \"\"\"Creating otio timeline from sequence path\n\n        Args:\n            sequence_path (str): path to sequence file\n            fps (float): frame per second\n\n        Returns:\n            otio.Timeline: otio timeline object\n        \"\"\"\n        # get editorial sequence file into otio timeline object\n        extension = os.path.splitext(sequence_path)[1]\n\n        kwargs = {}\n        if extension == \".edl\":\n            # EDL has no frame rate embedded so needs explicit\n            # frame rate else 24 is assumed.\n            kwargs[\"rate\"] = fps\n            kwargs[\"ignore_timecode_mismatch\"] = True\n\n        return otio.adapters.read_from_file(sequence_path, **kwargs)\n\n    def _get_path_from_file_data(self, file_path_data, multi=False):\n        \"\"\"Converting creator path data to single path string\n\n        Args:\n            file_path_data (FileDefItem): creator path data inputs\n            multi (bool): switch to multiple files mode\n\n        Raises:\n            FileExistsError: in case nothing had been set\n\n        Returns:\n            str: path string\n        \"\"\"\n        return_path_list = []\n\n\n        if isinstance(file_path_data, list):\n            return_path_list = [\n                os.path.join(f[\"directory\"], f[\"filenames\"][0])\n                for f in file_path_data\n            ]\n\n        if not return_path_list:\n            raise FileExistsError(\n                f\"File path was not added: {file_path_data}\")\n\n        return return_path_list if multi else return_path_list[0]\n\n    def _get_clip_instances(\n        self,\n        otio_timeline,\n        media_path,\n        instance_data,\n        family_presets,\n        sequence_file_name,\n        first_otio_timeline=None\n    ):\n        \"\"\"Helping function for creating clip instance\n\n        Args:\n            otio_timeline (otio.Timeline): otio timeline object\n            media_path (str): media file path string\n            instance_data (dict): clip instance data\n            family_presets (list): list of dict settings subset presets\n        \"\"\"\n        self.asset_name_check = []\n\n        tracks = [\n            track for track in otio_timeline.each_child(\n                descended_from_type=otio.schema.Track)\n            if track.kind == \"Video\"\n        ]\n\n        # media data for audio stream and reference solving\n        media_data = self._get_media_source_metadata(media_path)\n\n        for track in tracks:\n            # set track name\n            track.name = f\"{sequence_file_name} - {otio_timeline.name}\"\n\n            try:\n                track_start_frame = (\n                    abs(track.source_range.start_time.value)\n                )\n                track_start_frame -= self.timeline_frame_start\n            except AttributeError:\n                track_start_frame = 0\n\n            for otio_clip in track.each_child():\n                if not self._validate_clip_for_processing(otio_clip):\n                    continue\n\n\n                # get available frames info to clip data\n                self._create_otio_reference(otio_clip, media_path, media_data)\n\n                # convert timeline range to source range\n                self._restore_otio_source_range(otio_clip)\n\n                base_instance_data = self._get_base_instance_data(\n                    otio_clip,\n                    instance_data,\n                    track_start_frame\n                )\n\n                parenting_data = {\n                    \"instance_label\": None,\n                    \"instance_id\": None\n                }\n\n                for _fpreset in family_presets:\n                    # exclude audio family if no audio stream\n                    if (\n                        _fpreset[\"family\"] == \"audio\"\n                        and not media_data.get(\"audio\")\n                    ):\n                        continue\n\n                    instance = self._make_subset_instance(\n                        otio_clip,\n                        _fpreset,\n                        deepcopy(base_instance_data),\n                        parenting_data\n                    )\n\n            # add track to first otioTimeline if it is in input args\n            if first_otio_timeline:\n                first_otio_timeline.tracks.append(deepcopy(track))\n\n    def _restore_otio_source_range(self, otio_clip):\n        \"\"\"Infusing source range.\n\n        Otio clip is missing proper source clip range so\n        here we add them from from parent timeline frame range.\n\n        Args:\n            otio_clip (otio.Clip): otio clip object\n        \"\"\"\n        otio_clip.source_range = otio_clip.range_in_parent()\n\n    def _create_otio_reference(\n        self,\n        otio_clip,\n        media_path,\n        media_data\n    ):\n        \"\"\"Creating otio reference at otio clip.\n\n        Args:\n            otio_clip (otio.Clip): otio clip object\n            media_path (str): media file path string\n            media_data (dict): media metadata\n        \"\"\"\n        start_frame = media_data[\"start_frame\"]\n        frame_duration = media_data[\"duration\"]\n        fps = media_data[\"fps\"]\n\n        available_range = otio.opentime.TimeRange(\n            start_time=otio.opentime.RationalTime(\n                start_frame, fps),\n            duration=otio.opentime.RationalTime(\n                frame_duration, fps)\n        )\n        # in case old OTIO or video file create `ExternalReference`\n        media_reference = otio.schema.ExternalReference(\n            target_url=media_path,\n            available_range=available_range\n        )\n        otio_clip.media_reference = media_reference\n\n    def _get_media_source_metadata(self, path):\n        \"\"\"Get all available metadata from file\n\n        Args:\n            path (str): media file path string\n\n        Raises:\n            AssertionError: ffprobe couldn't read metadata\n\n        Returns:\n            dict: media file metadata\n        \"\"\"\n        return_data = {}\n\n        try:\n            media_data = get_ffprobe_data(\n                path, self.log\n            )\n\n            # get video stream data\n            video_streams = []\n            audio_streams = []\n            for stream in media_data[\"streams\"]:\n                codec_type = stream.get(\"codec_type\")\n                if codec_type == \"audio\":\n                    audio_streams.append(stream)\n\n                elif codec_type == \"video\":\n                    video_streams.append(stream)\n\n            if not video_streams:\n                raise ValueError(\n                    \"Could not find video stream in source file.\"\n                )\n\n            video_stream = video_streams[0]\n            return_data = {\n                \"video\": True,\n                \"start_frame\": 0,\n                \"duration\": int(video_stream[\"nb_frames\"]),\n                \"fps\": float(\n                    convert_ffprobe_fps_value(\n                        video_stream[\"r_frame_rate\"]\n                    )\n                )\n            }\n\n            # get audio  streams data\n            if audio_streams:\n                return_data[\"audio\"] = True\n\n        except Exception as exc:\n            raise AssertionError((\n                \"FFprobe couldn't read information about input file: \"\n                f\"\\\"{path}\\\". Error message: {exc}\"\n            ))\n\n        return return_data\n\n    def _make_subset_instance(\n        self,\n        otio_clip,\n        preset,\n        instance_data,\n        parenting_data\n    ):\n        \"\"\"Making subset instance from input preset\n\n        Args:\n            otio_clip (otio.Clip): otio clip object\n            preset (dict): single family preset\n            instance_data (dict): instance data\n            parenting_data (dict): shot instance parent data\n\n        Returns:\n            CreatedInstance: creator instance object\n        \"\"\"\n        family = preset[\"family\"]\n        label = self._make_subset_naming(\n            preset,\n            instance_data\n        )\n        instance_data[\"label\"] = label\n\n        # add file extension filter only if it is not shot family\n        if family == \"shot\":\n            instance_data[\"otioClip\"] = (\n                otio.adapters.write_to_string(otio_clip))\n            c_instance = self.create_context.creators[\n                \"editorial_shot\"].create(\n                    instance_data)\n            parenting_data.update({\n                \"instance_label\": label,\n                \"instance_id\": c_instance.data[\"instance_id\"]\n            })\n        else:\n            # add review family if defined\n            instance_data.update({\n                \"outputFileType\": preset[\"output_file_type\"],\n                \"parent_instance_id\": parenting_data[\"instance_id\"],\n                \"creator_attributes\": {\n                    \"parent_instance\": parenting_data[\"instance_label\"],\n                    \"add_review_family\": preset.get(\"review\")\n                }\n            })\n\n            creator_identifier = f\"editorial_{family}\"\n            editorial_clip_creator = self.create_context.creators[\n                creator_identifier]\n            c_instance = editorial_clip_creator.create(\n                instance_data)\n\n        return c_instance\n\n    def _make_subset_naming(\n        self,\n        preset,\n        instance_data\n    ):\n        \"\"\" Subset name maker\n\n        Args:\n            preset (dict): single preset item\n            instance_data (dict): instance data\n\n        Returns:\n            str: label string\n        \"\"\"\n        if AYON_SERVER_ENABLED:\n            asset_name = instance_data[\"creator_attributes\"][\"folderPath\"]\n        else:\n            asset_name = instance_data[\"creator_attributes\"][\"shotName\"]\n\n        variant_name = instance_data[\"variant\"]\n        family = preset[\"family\"]\n\n        # get variant name from preset or from inheritance\n        _variant_name = preset.get(\"variant\") or variant_name\n\n        # subset name\n        subset_name = \"{}{}\".format(\n            family, _variant_name.capitalize()\n        )\n        label = \"{} {}\".format(\n            asset_name,\n            subset_name\n        )\n\n        instance_data.update({\n            \"family\": family,\n            \"label\": label,\n            \"variant\": _variant_name,\n            \"subset\": subset_name,\n        })\n\n        return label\n\n    def _get_base_instance_data(\n        self,\n        otio_clip,\n        instance_data,\n        track_start_frame,\n    ):\n        \"\"\" Factoring basic set of instance data.\n\n        Args:\n            otio_clip (otio.Clip): otio clip object\n            instance_data (dict): precreate instance data\n            track_start_frame (int): track start frame\n\n        Returns:\n            dict: instance data\n        \"\"\"\n        # get clip instance properties\n        parent_asset_name = instance_data[\"parent_asset_name\"]\n        handle_start = instance_data[\"handle_start\"]\n        handle_end = instance_data[\"handle_end\"]\n        timeline_offset = instance_data[\"timeline_offset\"]\n        workfile_start_frame = instance_data[\"workfile_start_frame\"]\n        fps = instance_data[\"fps\"]\n        variant_name = instance_data[\"variant\"]\n\n        # basic unique asset name\n        clip_name = os.path.splitext(otio_clip.name)[0]\n        project_doc = get_project(self.project_name)\n\n        shot_name, shot_metadata = self._shot_metadata_solver.generate_data(\n            clip_name,\n            {\n                \"anatomy_data\": {\n                    \"project\": {\n                        \"name\": self.project_name,\n                        \"code\": project_doc[\"data\"][\"code\"]\n                    },\n                    \"parent\": parent_asset_name,\n                    \"app\": self.host_name\n                },\n                \"selected_asset_doc\": get_asset_by_name(\n                    self.project_name, parent_asset_name),\n                \"project_doc\": project_doc\n            }\n        )\n\n        # It should be validated only in openpype since we are supporting\n        # publishing to AYON with folder path and uniqueness is not an issue\n        if not AYON_SERVER_ENABLED:\n            self._validate_name_uniqueness(shot_name)\n\n        timing_data = self._get_timing_data(\n            otio_clip,\n            timeline_offset,\n            track_start_frame,\n            workfile_start_frame\n        )\n\n        # create creator attributes\n        creator_attributes = {\n\n            \"workfile_start_frame\": workfile_start_frame,\n            \"fps\": fps,\n            \"handle_start\": int(handle_start),\n            \"handle_end\": int(handle_end)\n        }\n        # add timing data\n        creator_attributes.update(timing_data)\n\n        # create base instance data\n        base_instance_data = {\n            \"shotName\": shot_name,\n            \"variant\": variant_name,\n            \"task\": \"\",\n            \"newAssetPublishing\": True,\n            \"trackStartFrame\": track_start_frame,\n            \"timelineOffset\": timeline_offset,\n\n            # creator_attributes\n            \"creator_attributes\": creator_attributes\n        }\n        # update base instance data with context data\n        # and also update creator attributes with context data\n        if AYON_SERVER_ENABLED:\n            # TODO: this is here just to be able to publish\n            #   to AYON with folder path\n            creator_attributes[\"folderPath\"] = shot_metadata.pop(\"folderPath\")\n            base_instance_data[\"folderPath\"] = parent_asset_name\n        else:\n            creator_attributes.update({\n                \"shotName\": shot_name,\n                \"Parent hierarchy path\": shot_metadata[\"hierarchy\"]\n            })\n\n            base_instance_data[\"asset\"] = parent_asset_name\n        # add creator attributes to shared instance data\n        base_instance_data[\"creator_attributes\"] = creator_attributes\n        # add hierarchy shot metadata\n        base_instance_data.update(shot_metadata)\n\n        return base_instance_data\n\n    def _get_timing_data(\n        self,\n        otio_clip,\n        timeline_offset,\n        track_start_frame,\n        workfile_start_frame\n    ):\n        \"\"\"Returning available timing data\n\n        Args:\n            otio_clip (otio.Clip): otio clip object\n            timeline_offset (int): offset value\n            track_start_frame (int): starting frame input\n            workfile_start_frame (int): start frame for shot's workfiles\n\n        Returns:\n            dict: timing metadata\n        \"\"\"\n        # frame ranges data\n        clip_in = otio_clip.range_in_parent().start_time.value\n        clip_in += track_start_frame\n        clip_out = otio_clip.range_in_parent().end_time_inclusive().value\n        clip_out += track_start_frame\n\n        # add offset in case there is any\n        if timeline_offset:\n            clip_in += timeline_offset\n            clip_out += timeline_offset\n\n        clip_duration = otio_clip.duration().value\n        source_in = otio_clip.trimmed_range().start_time.value\n        source_out = source_in + clip_duration\n\n        # define starting frame for future shot\n        frame_start = (\n            clip_in if workfile_start_frame is None\n            else workfile_start_frame\n        )\n        frame_end = frame_start + (clip_duration - 1)\n\n        return {\n            \"frameStart\": int(frame_start),\n            \"frameEnd\": int(frame_end),\n            \"clipIn\": int(clip_in),\n            \"clipOut\": int(clip_out),\n            \"clipDuration\": int(otio_clip.duration().value),\n            \"sourceIn\": int(source_in),\n            \"sourceOut\": int(source_out)\n        }\n\n    def _get_allowed_family_presets(self, pre_create_data):\n        \"\"\" Filter out allowed family presets.\n\n        Args:\n            pre_create_data (dict): precreate attributes inputs\n\n        Returns:\n            list: lit of dict with preset items\n        \"\"\"\n        return [\n            {\"family\": \"shot\"},\n            *[\n                preset for preset in self._creator_settings[\"family_presets\"]\n                if pre_create_data[preset[\"family\"]]\n            ]\n        ]\n\n    def _validate_clip_for_processing(self, otio_clip):\n        \"\"\"Validate otio clip attributes\n\n        Args:\n            otio_clip (otio.Clip): otio clip object\n\n        Returns:\n            bool: True if all passing conditions\n        \"\"\"\n        if otio_clip.name is None:\n            return False\n\n        if isinstance(otio_clip, otio.schema.Gap):\n            return False\n\n        # skip all generators like black empty\n        if isinstance(\n            otio_clip.media_reference,\n                otio.schema.GeneratorReference):\n            return False\n\n        # Transitions are ignored, because Clips have the full frame\n        # range.\n        if isinstance(otio_clip, otio.schema.Transition):\n            return False\n\n        return True\n\n    def _validate_name_uniqueness(self, name):\n        \"\"\" Validating name uniqueness.\n\n        In context of other clip names in sequence file.\n\n        Args:\n            name (str): shot name string\n        \"\"\"\n        if name not in self.asset_name_check:\n            self.asset_name_check.append(name)\n        else:\n            self.log.warning(\n                f\"Duplicate shot name: {name}! \"\n                \"Please check names in the input sequence files.\"\n            )\n\n    def get_pre_create_attr_defs(self):\n        \"\"\" Creating pre-create attributes at creator plugin.\n\n        Returns:\n            list: list of attribute object instances\n        \"\"\"\n        # Use same attributes as for instance attrobites\n        attr_defs = [\n            FileDef(\n                \"sequence_filepath_data\",\n                folders=False,\n                extensions=[\n                    \".edl\",\n                    \".xml\",\n                    \".aaf\",\n                    \".fcpxml\"\n                ],\n                allow_sequences=False,\n                single_item=False,\n                label=\"Sequence file\",\n            ),\n            FileDef(\n                \"media_filepaths_data\",\n                folders=False,\n                extensions=[\n                    \".mov\",\n                    \".mp4\",\n                    \".wav\"\n                ],\n                allow_sequences=False,\n                single_item=False,\n                label=\"Media files\",\n            ),\n            # TODO: perhaps better would be timecode and fps input\n            NumberDef(\n                \"timeline_offset\",\n                default=0,\n                label=\"Timeline offset\"\n            ),\n            UISeparatorDef(),\n            UILabelDef(\"Clip instance attributes\"),\n            UISeparatorDef()\n        ]\n        # add variants swithers\n        attr_defs.extend(\n            BoolDef(_var[\"family\"], label=_var[\"family\"])\n            for _var in self._creator_settings[\"family_presets\"]\n        )\n        attr_defs.append(UISeparatorDef())\n\n        attr_defs.extend(CLIP_ATTR_DEFS)\n        return attr_defs\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/create/create_from_settings.py",
    "content": "import os\nfrom openpype.lib import Logger\nfrom openpype.settings import get_project_settings\n\nlog = Logger.get_logger(__name__)\n\n\ndef initialize():\n    from openpype.hosts.traypublisher.api.plugin import SettingsCreator\n\n    project_name = os.environ[\"AVALON_PROJECT\"]\n    project_settings = get_project_settings(project_name)\n\n    simple_creators = project_settings[\"traypublisher\"][\"simple_creators\"]\n\n    global_variables = globals()\n    for item in simple_creators:\n\n        dynamic_plugin = SettingsCreator.from_settings(item)\n        global_variables[dynamic_plugin.__name__] = dynamic_plugin\n\n\ninitialize()\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/create/create_movie_batch.py",
    "content": "import copy\nimport os\nimport re\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import get_asset_name_identifier\nfrom openpype.lib import (\n    FileDef,\n    BoolDef,\n)\nfrom openpype.pipeline import (\n    CreatedInstance,\n)\nfrom openpype.pipeline.create import (\n    get_subset_name,\n    TaskNotSetError,\n)\n\nfrom openpype.hosts.traypublisher.api.plugin import TrayPublishCreator\nfrom openpype.hosts.traypublisher.batch_parsing import (\n    get_asset_doc_from_file_name\n)\n\n\nclass BatchMovieCreator(TrayPublishCreator):\n    \"\"\"Creates instances from movie file(s).\n\n    Intended for .mov files, but should work for any video file.\n    Doesn't handle image sequences though.\n    \"\"\"\n    identifier = \"render_movie_batch\"\n    label = \"Batch Movies\"\n    family = \"render\"\n    description = \"Publish batch of video files\"\n\n    create_allow_context_change = False\n    version_regex = re.compile(r\"^(.+)_v([0-9]+)$\")\n    # Position batch creator after simple creators\n    order = 110\n\n    def apply_settings(self, project_settings):\n        creator_settings = (\n            project_settings[\"traypublisher\"][\"create\"][\"BatchMovieCreator\"]\n        )\n        self.default_variants = creator_settings[\"default_variants\"]\n        self.default_tasks = creator_settings[\"default_tasks\"]\n        self.extensions = creator_settings[\"extensions\"]\n\n    def get_icon(self):\n        return \"fa.file\"\n\n    def create(self, subset_name, data, pre_create_data):\n        file_paths = pre_create_data.get(\"filepath\")\n        if not file_paths:\n            return\n\n        for file_info in file_paths:\n            instance_data = copy.deepcopy(data)\n            file_name = file_info[\"filenames\"][0]\n            filepath = os.path.join(file_info[\"directory\"], file_name)\n            instance_data[\"creator_attributes\"] = {\"filepath\": filepath}\n\n            asset_doc, version = get_asset_doc_from_file_name(\n                file_name, self.project_name, self.version_regex)\n\n            subset_name, task_name = self._get_subset_and_task(\n                asset_doc, data[\"variant\"], self.project_name)\n\n            asset_name = get_asset_name_identifier(asset_doc)\n\n            instance_data[\"task\"] = task_name\n            if AYON_SERVER_ENABLED:\n                instance_data[\"folderPath\"] = asset_name\n            else:\n                instance_data[\"asset\"] = asset_name\n\n            # Create new instance\n            new_instance = CreatedInstance(self.family, subset_name,\n                                           instance_data, self)\n            self._store_new_instance(new_instance)\n\n    def _get_subset_and_task(self, asset_doc, variant, project_name):\n        \"\"\"Create subset name according to standard template process\"\"\"\n        task_name = self._get_task_name(asset_doc)\n\n        try:\n            subset_name = get_subset_name(\n                self.family,\n                variant,\n                task_name,\n                asset_doc,\n                project_name\n            )\n        except TaskNotSetError:\n            # Create instance with fake task\n            # - instance will be marked as invalid so it can't be published\n            #   but user have ability to change it\n            # NOTE: This expect that there is not task 'Undefined' on asset\n            task_name = \"Undefined\"\n            subset_name = get_subset_name(\n                self.family,\n                variant,\n                task_name,\n                asset_doc,\n                project_name\n            )\n\n        return subset_name, task_name\n\n    def _get_task_name(self, asset_doc):\n        \"\"\"Get applicable task from 'asset_doc' \"\"\"\n        available_task_names = {}\n        asset_tasks = asset_doc.get(\"data\", {}).get(\"tasks\") or {}\n        for task_name in asset_tasks.keys():\n            available_task_names[task_name.lower()] = task_name\n\n        task_name = None\n        for _task_name in self.default_tasks:\n            _task_name_low = _task_name.lower()\n            if _task_name_low in available_task_names:\n                task_name = available_task_names[_task_name_low]\n                break\n\n        return task_name\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\n                \"add_review_family\",\n                default=True,\n                label=\"Review\"\n            )\n        ]\n\n    def get_pre_create_attr_defs(self):\n        # Use same attributes as for instance attributes\n        return [\n            FileDef(\n                \"filepath\",\n                folders=False,\n                single_item=False,\n                extensions=self.extensions,\n                allow_sequences=False,\n                label=\"Filepath\"\n            ),\n            BoolDef(\n                \"add_review_family\",\n                default=True,\n                label=\"Review\"\n            )\n        ]\n\n    def get_detail_description(self):\n        return \"\"\"# Publish batch of .mov to multiple assets.\n\n        File names must then contain only asset name, or asset name + version.\n        (eg. 'chair.mov', 'chair_v001.mov', not really safe `my_chair_v001.mov`\n        \"\"\"\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/create/create_online.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Creator of online files.\n\nOnline file retain their original name and use it as subset name. To\navoid conflicts, this creator checks if subset with this name already\nexists under selected asset.\n\"\"\"\nfrom pathlib import Path\n\n# from openpype.client import get_subset_by_name, get_asset_by_name\nfrom openpype.lib.attribute_definitions import FileDef, BoolDef\nfrom openpype.pipeline import (\n    CreatedInstance,\n    CreatorError\n)\nfrom openpype.hosts.traypublisher.api.plugin import TrayPublishCreator\n\n\nclass OnlineCreator(TrayPublishCreator):\n    \"\"\"Creates instance from file and retains its original name.\"\"\"\n\n    identifier = \"io.openpype.creators.traypublisher.online\"\n    label = \"Online\"\n    family = \"online\"\n    description = \"Publish file retaining its original file name\"\n    extensions = [\".mov\", \".mp4\", \".mxf\", \".m4v\", \".mpg\", \".exr\",\n                  \".dpx\", \".tif\", \".png\", \".jpg\"]\n\n    def get_detail_description(self):\n        return \"\"\"# Create file retaining its original file name.\n\n        This will publish files using template helping to retain original\n        file name and that file name is used as subset name.\n\n        Bz default it tries to guard against multiple publishes of the same\n        file.\"\"\"\n\n    def get_icon(self):\n        return \"fa.file\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        repr_file = pre_create_data.get(\"representation_file\")\n        if not repr_file:\n            raise CreatorError(\"No files specified\")\n\n        files = repr_file.get(\"filenames\")\n        if not files:\n            # this should never happen\n            raise CreatorError(\"Missing files from representation\")\n\n        origin_basename = Path(files[0]).stem\n\n        # disable check for existing subset with the same name\n        \"\"\"\n        asset = get_asset_by_name(\n            self.project_name, instance_data[\"asset\"], fields=[\"_id\"])\n\n        if get_subset_by_name(\n                self.project_name, origin_basename, asset[\"_id\"],\n                fields=[\"_id\"]):\n            raise CreatorError(f\"subset with {origin_basename} already \"\n                               \"exists in selected asset\")\n        \"\"\"\n\n        instance_data[\"originalBasename\"] = origin_basename\n        subset_name = origin_basename\n\n        instance_data[\"creator_attributes\"] = {\n            \"path\": (Path(repr_file[\"directory\"]) / files[0]).as_posix()\n        }\n\n        # Create new instance\n        new_instance = CreatedInstance(self.family, subset_name,\n                                       instance_data, self)\n        self._store_new_instance(new_instance)\n\n    def get_instance_attr_defs(self):\n        return [\n            BoolDef(\n                \"add_review_family\",\n                default=True,\n                label=\"Review\"\n            )\n        ]\n\n    def get_pre_create_attr_defs(self):\n        return [\n            FileDef(\n                \"representation_file\",\n                folders=False,\n                extensions=self.extensions,\n                allow_sequences=True,\n                single_item=True,\n                label=\"Representation\",\n            ),\n            BoolDef(\n                \"add_review_family\",\n                default=True,\n                label=\"Review\"\n            )\n        ]\n\n    def get_subset_name(\n        self,\n        variant,\n        task_name,\n        asset_doc,\n        project_name,\n        host_name=None,\n        instance=None\n    ):\n        if instance is None:\n            return \"{originalBasename}\"\n\n        return instance.data[\"subset\"]\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_app_name.py",
    "content": "import pyblish.api\n\n\nclass CollectTrayPublisherAppName(pyblish.api.ContextPlugin):\n    \"\"\"Collect app name and label.\"\"\"\n\n    label = \"Collect App Name/Label\"\n    order = pyblish.api.CollectorOrder - 0.5\n    hosts = [\"traypublisher\"]\n\n    def process(self, context):\n        context.data[\"appName\"] = \"tray publisher\"\n        context.data[\"appLabel\"] = \"Tray publisher\"\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py",
    "content": "from pprint import pformat\nimport pyblish.api\n\n\nclass CollectClipInstance(pyblish.api.InstancePlugin):\n    \"\"\"Collect clip instances and resolve its parent\"\"\"\n\n    label = \"Collect Clip Instances\"\n    order = pyblish.api.CollectorOrder - 0.081\n\n    hosts = [\"traypublisher\"]\n    families = [\"plate\", \"review\", \"audio\"]\n\n    def process(self, instance):\n        creator_identifier = instance.data[\"creator_identifier\"]\n        if creator_identifier not in [\n            \"editorial_plate\",\n            \"editorial_audio\",\n            \"editorial_review\"\n        ]:\n            return\n\n        instance.data[\"families\"].append(\"clip\")\n\n        parent_instance_id = instance.data[\"parent_instance_id\"]\n        edit_shared_data = instance.context.data[\"editorialSharedData\"]\n        instance.data.update(\n            edit_shared_data[parent_instance_id]\n        )\n\n        if \"editorialSourcePath\" in instance.context.data.keys():\n            instance.data[\"editorialSourcePath\"] = (\n                instance.context.data[\"editorialSourcePath\"])\n            instance.data[\"families\"].append(\"trimming\")\n\n        self.log.debug(pformat(instance.data))\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py",
    "content": "import os\nfrom pprint import pformat\nimport pyblish.api\nfrom openpype.pipeline import publish\nfrom openpype.pipeline import colorspace\n\n\nclass CollectColorspaceLook(pyblish.api.InstancePlugin,\n                            publish.OpenPypePyblishPluginMixin):\n    \"\"\"Collect OCIO colorspace look from LUT file\n    \"\"\"\n\n    label = \"Collect Colorspace Look\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"traypublisher\"]\n    families = [\"ociolook\"]\n\n    def process(self, instance):\n        creator_attrs = instance.data[\"creator_attributes\"]\n\n        lut_repre_name = \"LUTfile\"\n        file_url = creator_attrs[\"abs_lut_path\"]\n        file_name = os.path.basename(file_url)\n        base_name, ext = os.path.splitext(file_name)\n\n        # set output name with base_name which was cleared\n        # of all symbols and all parts were capitalized\n        output_name = (base_name.replace(\"_\", \" \")\n                                .replace(\".\", \" \")\n                                .replace(\"-\", \" \")\n                                .title()\n                                .replace(\" \", \"\"))\n\n        # get config items\n        config_items = instance.data[\"transientData\"][\"config_items\"]\n        config_data = instance.data[\"transientData\"][\"config_data\"]\n\n        # get colorspace items\n        converted_color_data = {}\n        for colorspace_key in [\n            \"working_colorspace\",\n            \"input_colorspace\",\n            \"output_colorspace\"\n        ]:\n            if creator_attrs[colorspace_key]:\n                color_data = colorspace.convert_colorspace_enumerator_item(\n                    creator_attrs[colorspace_key], config_items)\n                converted_color_data[colorspace_key] = color_data\n            else:\n                converted_color_data[colorspace_key] = None\n\n        # add colorspace to config data\n        if converted_color_data[\"working_colorspace\"]:\n            config_data[\"colorspace\"] = (\n                converted_color_data[\"working_colorspace\"][\"name\"]\n            )\n\n        # create lut representation data\n        lut_repre = {\n            \"name\": lut_repre_name,\n            \"output\": output_name,\n            \"ext\": ext.lstrip(\".\"),\n            \"files\": file_name,\n            \"stagingDir\": os.path.dirname(file_url),\n            \"tags\": []\n        }\n        instance.data.update({\n            \"representations\": [lut_repre],\n            \"source\": file_url,\n            \"ocioLookWorkingSpace\": converted_color_data[\"working_colorspace\"],\n            \"ocioLookItems\": [\n                {\n                    \"name\": lut_repre_name,\n                    \"ext\": ext.lstrip(\".\"),\n                    \"input_colorspace\": converted_color_data[\n                        \"input_colorspace\"],\n                    \"output_colorspace\": converted_color_data[\n                        \"output_colorspace\"],\n                    \"direction\": creator_attrs[\"direction\"],\n                    \"interpolation\": creator_attrs[\"interpolation\"],\n                    \"config_data\": config_data\n                }\n            ],\n        })\n\n        self.log.debug(pformat(instance.data))\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py",
    "content": "import os\nfrom pprint import pformat\nimport pyblish.api\nimport opentimelineio as otio\n\n\nclass CollectEditorialInstance(pyblish.api.InstancePlugin):\n    \"\"\"Collect data for instances created by settings creators.\"\"\"\n\n    label = \"Collect Editorial Instances\"\n    order = pyblish.api.CollectorOrder - 0.1\n\n    hosts = [\"traypublisher\"]\n    families = [\"editorial\"]\n\n    def process(self, instance):\n\n        if \"families\" not in instance.data:\n            instance.data[\"families\"] = []\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        fpath = instance.data[\"sequenceFilePath\"]\n        otio_timeline_string = instance.data.pop(\"otioTimeline\")\n        otio_timeline = otio.adapters.read_from_string(\n            otio_timeline_string)\n\n        instance.context.data[\"otioTimeline\"] = otio_timeline\n        instance.context.data[\"editorialSourcePath\"] = (\n            instance.data[\"editorialSourcePath\"])\n\n        self.log.info(fpath)\n\n        instance.data[\"stagingDir\"] = os.path.dirname(fpath)\n\n        _, ext = os.path.splitext(fpath)\n\n        instance.data[\"representations\"].append({\n            \"ext\": ext[1:],\n            \"name\": ext[1:],\n            \"stagingDir\": instance.data[\"stagingDir\"],\n            \"files\": os.path.basename(fpath)\n        })\n\n        self.log.debug(\"Created Editorial Instance {}\".format(\n            pformat(instance.data)\n        ))\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py",
    "content": "import pyblish.api\n\n\nclass CollectEditorialReviewable(pyblish.api.InstancePlugin):\n    \"\"\" Collect review input from user.\n\n    Adds the input to instance data.\n    \"\"\"\n\n    label = \"Collect Editorial Reviewable\"\n    order = pyblish.api.CollectorOrder\n\n    families = [\"plate\", \"review\", \"audio\"]\n    hosts = [\"traypublisher\"]\n\n    def process(self, instance):\n        creator_identifier = instance.data[\"creator_identifier\"]\n        if creator_identifier not in [\n            \"editorial_plate\",\n            \"editorial_audio\",\n            \"editorial_review\"\n        ]:\n            return\n\n        creator_attributes = instance.data[\"creator_attributes\"]\n\n        if creator_attributes[\"add_review_family\"]:\n            instance.data[\"families\"].append(\"review\")\n\n        self.log.debug(\"instance.data {}\".format(instance.data))\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import (\n    publish,\n    registered_host\n)\nfrom openpype.lib import EnumDef\nfrom openpype.pipeline import colorspace\nfrom openpype.pipeline.publish import KnownPublishError\n\n\nclass CollectColorspace(pyblish.api.InstancePlugin,\n                        publish.OpenPypePyblishPluginMixin,\n                        publish.ColormanagedPyblishPluginMixin):\n    \"\"\"Collect explicit user defined representation colorspaces\"\"\"\n\n    label = \"Choose representation colorspace\"\n    order = pyblish.api.CollectorOrder + 0.49\n    hosts = [\"traypublisher\"]\n    families = [\"render\", \"plate\", \"reference\", \"image\", \"online\"]\n    enabled = False\n\n    colorspace_items = [\n        (None, \"Don't override\")\n    ]\n    colorspace_attr_show = False\n    config_items = None\n\n    def process(self, instance):\n        values = self.get_attr_values_from_data(instance.data)\n        colorspace_value = values.get(\"colorspace\", None)\n        if colorspace_value is None:\n            return\n\n        color_data = colorspace.convert_colorspace_enumerator_item(\n            colorspace_value, self.config_items)\n\n        colorspace_name = self._colorspace_name_by_type(color_data)\n        self.log.debug(\"Explicit colorspace name: {}\".format(colorspace_name))\n\n        context = instance.context\n        for repre in instance.data.get(\"representations\", {}):\n            self.set_representation_colorspace(\n                representation=repre,\n                context=context,\n                colorspace=colorspace_name\n            )\n\n    def _colorspace_name_by_type(self, colorspace_data):\n        \"\"\"\n        Returns colorspace name by type\n\n        Arguments:\n            colorspace_data (dict): colorspace data\n\n        Returns:\n            str: colorspace name\n        \"\"\"\n        if colorspace_data[\"type\"] == \"colorspaces\":\n            return colorspace_data[\"name\"]\n        elif colorspace_data[\"type\"] == \"roles\":\n            return colorspace_data[\"colorspace\"]\n        else:\n            raise KnownPublishError(\n                (\n                    \"Collecting of colorspace failed. used config is missing \"\n                    \"colorspace type: '{}' . Please contact your pipeline TD.\"\n                ).format(colorspace_data['type'])\n            )\n\n    @classmethod\n    def apply_settings(cls, project_settings):\n        host = registered_host()\n        host_name = host.name\n        project_name = host.get_current_project_name()\n        config_data = colorspace.get_imageio_config(\n            project_name, host_name,\n            project_settings=project_settings\n        )\n\n        if config_data:\n            filepath = config_data[\"path\"]\n            config_items = colorspace.get_ocio_config_colorspaces(filepath)\n            labeled_colorspaces = colorspace.get_colorspaces_enumerator_items(\n                config_items,\n                include_aliases=True,\n                include_roles=True\n            )\n            cls.config_items = config_items\n            cls.colorspace_items.extend(labeled_colorspaces)\n            cls.enabled = True\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            EnumDef(\n                \"colorspace\",\n                cls.colorspace_items,\n                default=\"Don't override\",\n                label=\"Override Colorspace\"\n            )\n        ]\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py",
    "content": "import pyblish.api\n\n\nclass CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin):\n    \"\"\"Collect Frame Data From AssetEntity found in context\n\n    Frame range data will only be collected if the keys\n    are not yet collected for the instance.\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.491\n    label = \"Collect Missing Frame Data From Asset\"\n    families = [\"plate\", \"pointcache\",\n                \"vdbcache\", \"online\",\n                \"render\"]\n    hosts = [\"traypublisher\"]\n\n    def process(self, instance):\n        missing_keys = []\n        for key in (\n            \"fps\",\n            \"frameStart\",\n            \"frameEnd\",\n            \"handleStart\",\n            \"handleEnd\"\n        ):\n            if key not in instance.data:\n                missing_keys.append(key)\n        keys_set = []\n        for key in missing_keys:\n            asset_data = instance.data[\"assetEntity\"][\"data\"]\n            if key in asset_data:\n                instance.data[key] = asset_data[key]\n                keys_set.append(key)\n        if keys_set:\n            self.log.debug(f\"Frame range data {keys_set} \"\n                           \"has been collected from asset entity.\")\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_movie_batch.py",
    "content": "import os\n\nimport pyblish.api\nfrom openpype.pipeline import OpenPypePyblishPluginMixin\n\n\nclass CollectMovieBatch(\n    pyblish.api.InstancePlugin, OpenPypePyblishPluginMixin\n):\n    \"\"\"Collect file url for batch movies and create representation.\n\n    Adds review on instance and to repre.tags based on value of toggle button\n    on creator.\n    \"\"\"\n\n    label = \"Collect Movie Batch Files\"\n    order = pyblish.api.CollectorOrder\n\n    hosts = [\"traypublisher\"]\n\n    def process(self, instance):\n        if instance.data.get(\"creator_identifier\") != \"render_movie_batch\":\n            return\n\n        creator_attributes = instance.data[\"creator_attributes\"]\n\n        file_url = creator_attributes[\"filepath\"]\n        file_name = os.path.basename(file_url)\n        _, ext = os.path.splitext(file_name)\n\n        repre = {\n            \"name\": ext[1:],\n            \"ext\": ext[1:],\n            \"files\": file_name,\n            \"stagingDir\": os.path.dirname(file_url),\n            \"tags\": []\n        }\n        instance.data[\"representations\"].append(repre)\n\n        if creator_attributes[\"add_review_family\"]:\n            repre[\"tags\"].append(\"review\")\n            instance.data[\"families\"].append(\"review\")\n            if not instance.data.get(\"thumbnailSource\"):\n                instance.data[\"thumbnailSource\"] = file_url\n\n        instance.data[\"source\"] = file_url\n\n        self.log.debug(\"instance.data {}\".format(instance.data))\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_online_file.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom pathlib import Path\n\n\nclass CollectOnlineFile(pyblish.api.InstancePlugin):\n    \"\"\"Collect online file and retain its file name.\"\"\"\n    label = \"Collect Online File\"\n    order = pyblish.api.CollectorOrder\n    families = [\"online\"]\n    hosts = [\"traypublisher\"]\n\n    def process(self, instance):\n        file = Path(instance.data[\"creator_attributes\"][\"path\"])\n        review = instance.data[\"creator_attributes\"][\"add_review_family\"]\n        instance.data[\"review\"] = review\n        if \"review\" not in instance.data[\"families\"]:\n            instance.data[\"families\"].append(\"review\")\n        self.log.info(f\"Adding review: {review}\")\n\n        instance.data[\"representations\"].append(\n            {\n                \"name\": file.suffix.lstrip(\".\"),\n                \"ext\": file.suffix.lstrip(\".\"),\n                \"files\": file.name,\n                \"stagingDir\": file.parent.as_posix(),\n                \"tags\": [\"review\"] if review else []\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_review_frames.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\n\n\nclass CollectReviewInfo(pyblish.api.InstancePlugin):\n    \"\"\"Collect data required for review instances.\n\n    ExtractReview plugin requires frame start/end, fps on instance data which\n    are missing on instances from TrayPublishes.\n\n    Warning:\n        This is temporary solution to \"make it work\". Contains removed changes\n            from https://github.com/ynput/OpenPype/pull/4383 reduced only for\n            review instances.\n    \"\"\"\n\n    label = \"Collect Review Info\"\n    order = pyblish.api.CollectorOrder + 0.491\n    families = [\"review\"]\n    hosts = [\"traypublisher\"]\n\n    def process(self, instance):\n        asset_entity = instance.data.get(\"assetEntity\")\n        if instance.data.get(\"frameStart\") is not None or not asset_entity:\n            self.log.debug(\"Missing required data on instance\")\n            return\n\n        asset_data = asset_entity[\"data\"]\n        # Store collected data for logging\n        collected_data = {}\n        for key in (\n            \"fps\",\n            \"frameStart\",\n            \"frameEnd\",\n            \"handleStart\",\n            \"handleEnd\",\n        ):\n            if key in instance.data or key not in asset_data:\n                continue\n            value = asset_data[key]\n            collected_data[key] = value\n            instance.data[key] = value\n        self.log.debug(\"Collected data: {}\".format(str(collected_data)))\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py",
    "content": "import pyblish.api\nimport clique\n\nfrom openpype.pipeline import OptionalPyblishPluginMixin\n\n\nclass CollectSequenceFrameData(\n    pyblish.api.InstancePlugin,\n    OptionalPyblishPluginMixin\n):\n    \"\"\"Collect Original Sequence Frame Data\n\n    If the representation includes files with frame numbers,\n    then set `frameStart` and `frameEnd` for the instance to the\n    start and end frame respectively\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.4905\n    label = \"Collect Original Sequence Frame Data\"\n    families = [\"plate\", \"pointcache\",\n                \"vdbcache\", \"online\",\n                \"render\"]\n    hosts = [\"traypublisher\"]\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        # editorial would fail since they might not be in database yet\n        new_asset_publishing = instance.data.get(\"newAssetPublishing\")\n        if new_asset_publishing:\n            self.log.debug(\"Instance is creating new asset. Skipping.\")\n            return\n\n        frame_data = self.get_frame_data_from_repre_sequence(instance)\n\n        if not frame_data:\n            # if no dict data skip collecting the frame range data\n            return\n\n        for key, value in frame_data.items():\n            instance.data[key] = value\n            self.log.debug(f\"Collected Frame range data '{key}':{value} \")\n\n\n    def get_frame_data_from_repre_sequence(self, instance):\n        repres = instance.data.get(\"representations\")\n        asset_data = instance.data[\"assetEntity\"][\"data\"]\n\n        if repres:\n            first_repre = repres[0]\n            if \"ext\" not in first_repre:\n                self.log.warning(\"Cannot find file extension\"\n                                 \" in representation data\")\n                return\n\n            files = first_repre[\"files\"]\n            collections, _ = clique.assemble(files)\n            if not collections:\n                # No sequences detected and we can't retrieve\n                # frame range\n                self.log.debug(\n                    \"No sequences detected in the representation data.\"\n                    \" Skipping collecting frame range data.\")\n                return\n            collection = collections[0]\n            repres_frames = list(collection.indexes)\n\n            return {\n                \"frameStart\": repres_frames[0],\n                \"frameEnd\": repres_frames[-1],\n                \"handleStart\": 0,\n                \"handleEnd\": 0,\n                \"fps\": asset_data[\"fps\"]\n            }\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py",
    "content": "from pprint import pformat\nimport pyblish.api\nimport opentimelineio as otio\n\nfrom openpype import AYON_SERVER_ENABLED\n\n\nclass CollectShotInstance(pyblish.api.InstancePlugin):\n    \"\"\" Collect shot instances\n\n    Resolving its user inputs from creator attributes\n    to instance data.\n    \"\"\"\n\n    label = \"Collect Shot Instances\"\n    order = pyblish.api.CollectorOrder - 0.09\n\n    hosts = [\"traypublisher\"]\n    families = [\"shot\"]\n\n    SHARED_KEYS = [\n        \"asset\",\n        \"fps\",\n        \"handleStart\",\n        \"handleEnd\",\n        \"frameStart\",\n        \"frameEnd\",\n        \"clipIn\",\n        \"clipOut\",\n        \"clipDuration\",\n        \"sourceIn\",\n        \"sourceOut\",\n        \"otioClip\",\n        \"workfileFrameStart\"\n    ]\n\n    def process(self, instance):\n        creator_identifier = instance.data[\"creator_identifier\"]\n        if \"editorial\" not in creator_identifier:\n            return\n\n        # get otio clip object\n        otio_clip = self._get_otio_clip(instance)\n        instance.data[\"otioClip\"] = otio_clip\n\n        # first solve the inputs from creator attr\n        data = self._solve_inputs_to_data(instance)\n        instance.data.update(data)\n\n        # distribute all shared keys to clips instances\n        self._distribute_shared_data(instance)\n        self._solve_hierarchy_context(instance)\n\n        self.log.debug(pformat(instance.data))\n\n    def _get_otio_clip(self, instance):\n        \"\"\" Converts otio string data.\n\n        Convert them to proper otio object\n        and finds its equivalent at otio timeline.\n        This process is a hack to support also\n        resolving parent range.\n\n        Args:\n            instance (obj): publishing instance\n\n        Returns:\n            otio.Clip: otio clip object\n        \"\"\"\n        context = instance.context\n        # convert otio clip from string to object\n        otio_clip_string = instance.data.pop(\"otioClip\")\n        otio_clip = otio.adapters.read_from_string(\n            otio_clip_string)\n\n        otio_timeline = context.data[\"otioTimeline\"]\n\n        clips = [\n            clip for clip in otio_timeline.each_child(\n                descended_from_type=otio.schema.Clip)\n            if clip.name == otio_clip.name\n            if clip.parent().kind == \"Video\"\n        ]\n\n        otio_clip = clips.pop()\n\n        return otio_clip\n\n    def _distribute_shared_data(self, instance):\n        \"\"\" Distribute all defined keys.\n\n        All data are shared between all related\n        instances in context.\n\n        Args:\n            instance (obj): publishing instance\n        \"\"\"\n        context = instance.context\n\n        instance_id = instance.data[\"instance_id\"]\n\n        if not context.data.get(\"editorialSharedData\"):\n            context.data[\"editorialSharedData\"] = {}\n\n        context.data[\"editorialSharedData\"][instance_id] = {\n            _k: _v for _k, _v in instance.data.items()\n            if _k in self.SHARED_KEYS\n        }\n\n    def _solve_inputs_to_data(self, instance):\n        \"\"\" Resolve all user inputs into instance data.\n\n        Args:\n            instance (obj): publishing instance\n\n        Returns:\n            dict: instance data updating data\n        \"\"\"\n        _cr_attrs = instance.data[\"creator_attributes\"]\n        workfile_start_frame = _cr_attrs[\"workfile_start_frame\"]\n        frame_start = _cr_attrs[\"frameStart\"]\n        frame_end = _cr_attrs[\"frameEnd\"]\n        frame_dur = frame_end - frame_start\n\n        data = {\n            \"fps\": float(_cr_attrs[\"fps\"]),\n            \"handleStart\": _cr_attrs[\"handle_start\"],\n            \"handleEnd\": _cr_attrs[\"handle_end\"],\n            \"frameStart\": workfile_start_frame,\n            \"frameEnd\": workfile_start_frame + frame_dur,\n            \"clipIn\": _cr_attrs[\"clipIn\"],\n            \"clipOut\": _cr_attrs[\"clipOut\"],\n            \"clipDuration\": _cr_attrs[\"clipDuration\"],\n            \"sourceIn\": _cr_attrs[\"sourceIn\"],\n            \"sourceOut\": _cr_attrs[\"sourceOut\"],\n            \"workfileFrameStart\": workfile_start_frame\n        }\n        if AYON_SERVER_ENABLED:\n            data[\"asset\"] = _cr_attrs[\"folderPath\"]\n        else:\n            data[\"asset\"] = _cr_attrs[\"shotName\"]\n\n        return data\n\n    def _solve_hierarchy_context(self, instance):\n        \"\"\" Adding hierarchy data to context shared data.\n\n        Args:\n            instance (obj): publishing instance\n        \"\"\"\n        context = instance.context\n\n        final_context = (\n            context.data[\"hierarchyContext\"]\n            if context.data.get(\"hierarchyContext\")\n            else {}\n        )\n\n        # get handles\n        handle_start = int(instance.data[\"handleStart\"])\n        handle_end = int(instance.data[\"handleEnd\"])\n\n        in_info = {\n            \"entity_type\": \"Shot\",\n            \"custom_attributes\": {\n                \"handleStart\": handle_start,\n                \"handleEnd\": handle_end,\n                \"frameStart\": instance.data[\"frameStart\"],\n                \"frameEnd\": instance.data[\"frameEnd\"],\n                \"clipIn\": instance.data[\"clipIn\"],\n                \"clipOut\": instance.data[\"clipOut\"],\n                \"fps\": instance.data[\"fps\"]\n            },\n            \"tasks\": instance.data[\"tasks\"]\n        }\n\n        parents = instance.data.get('parents', [])\n\n        # Split by '/' for AYON where asset is a path\n        asset_name = instance.data[\"asset\"].split(\"/\")[-1]\n        actual = {asset_name: in_info}\n\n        for parent in reversed(parents):\n            parent_name = parent[\"entity_name\"]\n            next_dict = {\n                parent_name: {\n                    \"entity_type\": parent[\"entity_type\"],\n                    \"childs\": actual\n                }\n            }\n            actual = next_dict\n\n        final_context = self._update_dict(final_context, actual)\n\n        # adding hierarchy context to instance\n        context.data[\"hierarchyContext\"] = final_context\n\n    def _update_dict(self, ex_dict, new_dict):\n        \"\"\" Recursion function\n\n        Updating nested data with another nested data.\n\n        Args:\n            ex_dict (dict): nested data\n            new_dict (dict): nested data\n\n        Returns:\n            dict: updated nested data\n        \"\"\"\n        for key in ex_dict:\n            if key in new_dict and isinstance(ex_dict[key], dict):\n                new_dict[key] = self._update_dict(ex_dict[key], new_dict[key])\n            elif not ex_dict.get(key) or not new_dict.get(key):\n                new_dict[key] = ex_dict[key]\n\n        return new_dict\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py",
    "content": "import os\nimport tempfile\nfrom pathlib import Path\n\nimport clique\nimport pyblish.api\n\n\nclass CollectSettingsSimpleInstances(pyblish.api.InstancePlugin):\n    \"\"\"Collect data for instances created by settings creators.\n\n    Plugin create representations for simple instances based\n    on 'representation_files' attribute stored on instance data.\n\n    There is also possibility to have reviewable representation which can be\n    stored under 'reviewable' attribute stored on instance data. If there was\n    already created representation with the same files as 'reviewable' contains\n\n    Representations can be marked for review and in that case is also added\n    'review' family to instance families. For review can be marked only one\n    representation so **first** representation that has extension available\n    in '_review_extensions' is used for review.\n\n    For instance 'source' is used path from last representation created\n    from 'representation_files'.\n\n    Set staging directory on instance. That is probably never used because\n    each created representation has it's own staging dir.\n    \"\"\"\n\n    label = \"Collect Settings Simple Instances\"\n    order = pyblish.api.CollectorOrder - 0.49\n\n    hosts = [\"traypublisher\"]\n\n    def process(self, instance):\n        if not instance.data.get(\"settings_creator\"):\n            return\n\n        instance_label = instance.data[\"name\"]\n        # Create instance's staging dir in temp\n        tmp_folder = tempfile.mkdtemp(prefix=\"traypublisher_\")\n        instance.data[\"stagingDir\"] = tmp_folder\n        instance.context.data[\"cleanupFullPaths\"].append(tmp_folder)\n\n        self.log.debug((\n            \"Created temp staging directory for instance {}. {}\"\n        ).format(instance_label, tmp_folder))\n\n        self._fill_version(instance, instance_label)\n\n        # Store filepaths for validation of their existence\n        source_filepaths = []\n        # Make sure there are no representations with same name\n        repre_names_counter = {}\n        # Store created names for logging\n        repre_names = []\n        # Store set of filepaths per each representation\n        representation_files_mapping = []\n        source = self._create_main_representations(\n            instance,\n            source_filepaths,\n            repre_names_counter,\n            repre_names,\n            representation_files_mapping\n        )\n\n        self._create_review_representation(\n            instance,\n            source_filepaths,\n            repre_names_counter,\n            repre_names,\n            representation_files_mapping\n        )\n        source_filepaths = list(set(source_filepaths))\n        instance.data[\"source\"] = source\n        instance.data[\"sourceFilepaths\"] = source_filepaths\n\n        # NOTE: Missing filepaths should not cause crashes (at least not here)\n        # - if filepaths are required they should crash on validation\n        if source_filepaths:\n            # NOTE: Original basename is not handling sequences\n            # - we should maybe not fill the key when sequence is used?\n            origin_basename = Path(source_filepaths[0]).stem\n            instance.data[\"originalBasename\"] = origin_basename\n\n        self.log.debug(\n            (\n                \"Created Simple Settings instance \\\"{}\\\"\"\n                \" with {} representations: {}\"\n            ).format(\n                instance_label,\n                len(instance.data[\"representations\"]),\n                \", \".join(repre_names)\n            )\n        )\n\n    def _fill_version(self, instance, instance_label):\n        \"\"\"Fill instance version under which will be instance integrated.\n\n        Instance must have set 'use_next_version' to 'False'\n        and 'version_to_use' to version to use.\n\n        Args:\n            instance (pyblish.api.Instance): Instance to fill version for.\n            instance_label (str): Label of instance to fill version for.\n        \"\"\"\n\n        creator_attributes = instance.data[\"creator_attributes\"]\n        use_next_version = creator_attributes.get(\"use_next_version\", True)\n        # If 'version_to_use' is '0' it means that next version should be used\n        version_to_use = creator_attributes.get(\"version_to_use\", 0)\n        if use_next_version or not version_to_use:\n            return\n        instance.data[\"version\"] = version_to_use\n        self.log.debug(\n            \"Version for instance \\\"{}\\\" was set to \\\"{}\\\"\".format(\n                instance_label, version_to_use))\n\n    def _create_main_representations(\n        self,\n        instance,\n        source_filepaths,\n        repre_names_counter,\n        repre_names,\n        representation_files_mapping\n    ):\n        creator_attributes = instance.data[\"creator_attributes\"]\n        filepath_items = creator_attributes[\"representation_files\"]\n        if not isinstance(filepath_items, list):\n            filepath_items = [filepath_items]\n\n        source = None\n        for filepath_item in filepath_items:\n            # Skip if filepath item does not have filenames\n            if not filepath_item[\"filenames\"]:\n                continue\n\n            filepaths = {\n                os.path.join(filepath_item[\"directory\"], filename)\n                for filename in filepath_item[\"filenames\"]\n            }\n            source_filepaths.extend(filepaths)\n\n            source = self._calculate_source(filepaths)\n            representation = self._create_representation_data(\n                filepath_item, repre_names_counter, repre_names\n            )\n            instance.data[\"representations\"].append(representation)\n            representation_files_mapping.append(\n                (filepaths, representation, source)\n            )\n        return source\n\n    def _create_review_representation(\n        self,\n        instance,\n        source_filepaths,\n        repre_names_counter,\n        repre_names,\n        representation_files_mapping\n    ):\n        # Skip review representation creation if there are no representations\n        #   created for \"main\" part\n        #   - review representation must not be created in that case so\n        #       validation can care about it\n        if not representation_files_mapping:\n            self.log.warning((\n                \"There are missing source representations.\"\n                \" Creation of review representation was skipped.\"\n            ))\n            return\n\n        creator_attributes = instance.data[\"creator_attributes\"]\n        review_file_item = creator_attributes[\"reviewable\"]\n        filenames = review_file_item.get(\"filenames\")\n        if not filenames:\n            self.log.debug((\n                \"Filepath for review is not defined.\"\n                \" Skipping review representation creation.\"\n            ))\n            return\n\n        item_dir = review_file_item[\"directory\"]\n        first_filepath = os.path.join(item_dir, filenames[0])\n\n        filepaths = {\n            os.path.join(item_dir, filename)\n            for filename in filenames\n        }\n        source_filepaths.extend(filepaths)\n        # First try to find out representation with same filepaths\n        #   so it's not needed to create new representation just for review\n        review_representation = None\n        # Review path (only for logging)\n        review_path = None\n        for item in representation_files_mapping:\n            _filepaths, representation, repre_path = item\n            if _filepaths == filepaths:\n                review_representation = representation\n                review_path = repre_path\n                break\n\n        if review_representation is None:\n            self.log.debug(\"Creating new review representation\")\n            review_path = self._calculate_source(filepaths)\n            review_representation = self._create_representation_data(\n                review_file_item, repre_names_counter, repre_names\n            )\n            instance.data[\"representations\"].append(review_representation)\n\n        if \"review\" not in instance.data[\"families\"]:\n            instance.data[\"families\"].append(\"review\")\n\n        if not instance.data.get(\"thumbnailSource\"):\n            instance.data[\"thumbnailSource\"] = first_filepath\n\n        review_representation[\"tags\"].append(\"review\")\n\n        # Adding \"review\" to representation name since it can clash with main\n        # representation if they share the same extension.\n        review_representation[\"outputName\"] = \"review\"\n\n        self.log.debug(\"Representation {} was marked for review. {}\".format(\n            review_representation[\"name\"], review_path\n        ))\n\n    def _create_representation_data(\n        self, filepath_item, repre_names_counter, repre_names\n    ):\n        \"\"\"Create new representation data based on file item.\n\n        Args:\n            filepath_item (Dict[str, Any]): Item with information about\n                representation paths.\n            repre_names_counter (Dict[str, int]): Store count of representation\n                names.\n            repre_names (List[str]): All used representation names. For\n                logging purposes.\n\n        Returns:\n            Dict: Prepared base representation data.\n        \"\"\"\n\n        filenames = filepath_item[\"filenames\"]\n        _, ext = os.path.splitext(filenames[0])\n        if len(filenames) == 1:\n            filenames = filenames[0]\n\n        repre_name = repre_ext = ext[1:]\n        if repre_name not in repre_names_counter:\n            repre_names_counter[repre_name] = 2\n        else:\n            counter = repre_names_counter[repre_name]\n            repre_names_counter[repre_name] += 1\n            repre_name = \"{}_{}\".format(repre_name, counter)\n        repre_names.append(repre_name)\n        return {\n            \"ext\": repre_ext,\n            \"name\": repre_name,\n            \"stagingDir\": filepath_item[\"directory\"],\n            \"files\": filenames,\n            \"tags\": []\n        }\n\n    def _calculate_source(self, filepaths):\n        cols, rems = clique.assemble(filepaths)\n        if cols:\n            source = cols[0].format(\"{head}{padding}{tail}\")\n        elif rems:\n            source = rems[0]\n        return source\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/collect_source.py",
    "content": "import pyblish.api\n\n\nclass CollectSource(pyblish.api.ContextPlugin):\n    \"\"\"Collecting instances from traypublisher host.\"\"\"\n\n    label = \"Collect source\"\n    order = pyblish.api.CollectorOrder - 0.49\n    hosts = [\"traypublisher\"]\n\n    def process(self, context):\n        # get json paths from os and load them\n        source_name = \"traypublisher\"\n        for instance in context:\n            source = instance.data.get(\"source\")\n            if not source:\n                instance.data[\"source\"] = source_name\n                self.log.info((\n                    \"Source of instance \\\"{}\\\" is changed to \\\"{}\\\"\"\n                ).format(instance.data[\"name\"], source_name))\n            else:\n                self.log.info((\n                    \"Source of instance \\\"{}\\\" was already set to \\\"{}\\\"\"\n                ).format(instance.data[\"name\"], source))\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/extract_colorspace_look.py",
    "content": "import os\nimport json\nimport pyblish.api\nfrom openpype.pipeline import publish\n\n\nclass ExtractColorspaceLook(publish.Extractor,\n                            publish.OpenPypePyblishPluginMixin):\n    \"\"\"Extract OCIO colorspace look from LUT file\n    \"\"\"\n\n    label = \"Extract Colorspace Look\"\n    order = pyblish.api.ExtractorOrder\n    hosts = [\"traypublisher\"]\n    families = [\"ociolook\"]\n\n    def process(self, instance):\n        ociolook_items = instance.data[\"ocioLookItems\"]\n        ociolook_working_color = instance.data[\"ocioLookWorkingSpace\"]\n        staging_dir = self.staging_dir(instance)\n\n        # create ociolook file attributes\n        ociolook_file_name = \"ocioLookFile.json\"\n        ociolook_file_content = {\n            \"version\": 1,\n            \"data\": {\n                \"ocioLookItems\": ociolook_items,\n                \"ocioLookWorkingSpace\": ociolook_working_color\n            }\n        }\n\n        # write ociolook content into json file saved in staging dir\n        file_url = os.path.join(staging_dir, ociolook_file_name)\n        with open(file_url, \"w\") as f_:\n            json.dump(ociolook_file_content, f_, indent=4)\n\n        # create lut representation data\n        ociolook_repre = {\n            \"name\": \"ocioLookFile\",\n            \"ext\": \"json\",\n            \"files\": ociolook_file_name,\n            \"stagingDir\": staging_dir,\n            \"tags\": []\n        }\n        instance.data[\"representations\"].append(ociolook_repre)\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/help/validate_existing_version.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Version already exists</title>\n<description>\n## Version already exists\n\nVersion {version} you have set on instance '{subset_name}' under '{asset_name}' already exists. This validation is enabled by default to prevent accidental override of existing versions.\n\n### How to repair?\n- Click on 'Repair' action -> this will change version to next available.\n- Disable validation on the instance if you are sure you want to override the version.\n- Reset publishing and manually change the version number.\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/help/validate_frame_ranges.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Invalid frame range</title>\n<description>\n## Invalid frame range\n\nExpected duration or '{duration}' frames set in database, workfile contains only '{found}' frames.\n\n### How to repair?\n\nModify configuration in the database or tweak frame range in the workfile.\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import (\n    publish,\n    PublishValidationError\n)\n\nfrom openpype.pipeline.colorspace import (\n    get_ocio_config_colorspaces\n)\n\n\nclass ValidateColorspace(pyblish.api.InstancePlugin,\n                         publish.OpenPypePyblishPluginMixin,\n                         publish.ColormanagedPyblishPluginMixin):\n    \"\"\"Validate representation colorspaces\"\"\"\n\n    label = \"Validate representation colorspace\"\n    order = pyblish.api.ValidatorOrder\n    hosts = [\"traypublisher\"]\n    families = [\"render\", \"plate\", \"reference\", \"image\", \"online\"]\n\n    def process(self, instance):\n\n        config_colorspaces = {}  # cache of colorspaces per config path\n        for repre in instance.data.get(\"representations\", {}):\n\n            colorspace_data = repre.get(\"colorspaceData\", {})\n            if not colorspace_data:\n                # Nothing to validate\n                continue\n\n            config_path = colorspace_data[\"config\"][\"path\"]\n            if config_path not in config_colorspaces:\n                colorspaces = get_ocio_config_colorspaces(config_path)\n                if not colorspaces.get(\"colorspaces\"):\n                    message = (\n                        f\"OCIO config '{config_path}' does not contain any \"\n                        \"colorspaces. This is an error in the OCIO config. \"\n                        \"Contact your pipeline TD.\",\n                    )\n                    raise PublishValidationError(\n                        title=\"Colorspace validation\",\n                        message=message,\n                        description=message\n                    )\n                config_colorspaces[config_path] = set(\n                    colorspaces[\"colorspaces\"])\n\n            colorspace = colorspace_data[\"colorspace\"]\n            self.log.debug(\n                f\"Validating representation '{repre['name']}' \"\n                f\"colorspace '{colorspace}'\"\n            )\n            if colorspace not in config_colorspaces[config_path]:\n                message = (\n                    f\"Representation '{repre['name']}' colorspace \"\n                    f\"'{colorspace}' does not exist in OCIO config: \"\n                    f\"{config_path}\"\n                )\n\n                raise PublishValidationError(\n                    title=\"Representation colorspace\",\n                    message=message,\n                    description=message\n                )\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import (\n    publish,\n    PublishValidationError\n)\n\n\nclass ValidateColorspaceLook(pyblish.api.InstancePlugin,\n                             publish.OpenPypePyblishPluginMixin):\n    \"\"\"Validate colorspace look attributes\"\"\"\n\n    label = \"Validate colorspace look attributes\"\n    order = pyblish.api.ValidatorOrder\n    hosts = [\"traypublisher\"]\n    families = [\"ociolook\"]\n\n    def process(self, instance):\n        create_context = instance.context.data[\"create_context\"]\n        created_instance = create_context.get_instance_by_id(\n            instance.data[\"instance_id\"])\n        creator_defs = created_instance.creator_attribute_defs\n\n        ociolook_working_color = instance.data.get(\"ocioLookWorkingSpace\")\n        ociolook_items = instance.data.get(\"ocioLookItems\", [])\n\n        creator_defs_by_key = {_def.key: _def.label for _def in creator_defs}\n\n        not_set_keys = {}\n        if not ociolook_working_color:\n            not_set_keys[\"working_colorspace\"] = creator_defs_by_key[\n                \"working_colorspace\"]\n\n        for ociolook_item in ociolook_items:\n            item_not_set_keys = self.validate_colorspace_set_attrs(\n                ociolook_item, creator_defs_by_key)\n            if item_not_set_keys:\n                not_set_keys[ociolook_item[\"name\"]] = item_not_set_keys\n\n        if not_set_keys:\n            message = (\n                \"Colorspace look attributes are not set: \\n\"\n            )\n            for key, value in not_set_keys.items():\n                if isinstance(value, list):\n                    values_string = \"\\n\\t- \".join(value)\n                    message += f\"\\n\\t{key}:\\n\\t- {values_string}\"\n                else:\n                    message += f\"\\n\\t{value}\"\n\n            raise PublishValidationError(\n                title=\"Colorspace Look attributes\",\n                message=message,\n                description=message\n            )\n\n    def validate_colorspace_set_attrs(\n        self,\n        ociolook_item,\n        creator_defs_by_key\n    ):\n        \"\"\"Validate colorspace look attributes\"\"\"\n\n        self.log.debug(f\"Validate colorspace look attributes: {ociolook_item}\")\n\n        check_keys = [\n            \"input_colorspace\",\n            \"output_colorspace\",\n            \"direction\",\n            \"interpolation\"\n        ]\n\n        not_set_keys = []\n        for key in check_keys:\n            if ociolook_item[key]:\n                # key is set and it is correct\n                continue\n\n            def_label = creator_defs_by_key.get(key)\n\n            if not def_label:\n                # raise since key is not recognized by creator defs\n                raise KeyError(\n                    f\"Colorspace look attribute '{key}' is not \"\n                    f\"recognized by creator attributes: {creator_defs_by_key}\"\n                )\n            not_set_keys.append(def_label)\n\n        return not_set_keys\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/validate_existing_version.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin,\n    RepairAction,\n)\n\n\nclass ValidateExistingVersion(\n    OptionalPyblishPluginMixin,\n    pyblish.api.InstancePlugin\n):\n    label = \"Validate Existing Version\"\n    order = ValidateContentsOrder\n\n    hosts = [\"traypublisher\"]\n\n    actions = [RepairAction]\n\n    settings_category = \"traypublisher\"\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        version = instance.data.get(\"version\")\n        if version is None:\n            return\n\n        last_version = instance.data.get(\"latestVersion\")\n        if last_version is None or last_version < version:\n            return\n\n        subset_name = instance.data[\"subset\"]\n        msg = \"Version {} already exists for subset {}.\".format(\n            version, subset_name)\n\n        formatting_data = {\n            \"subset_name\": subset_name,\n            \"asset_name\": instance.data[\"asset\"],\n            \"version\": version\n        }\n        raise PublishXmlValidationError(\n            self, msg, formatting_data=formatting_data)\n\n    @classmethod\n    def repair(cls, instance):\n        create_context = instance.context.data[\"create_context\"]\n        created_instance = create_context.get_instance_by_id(\n            instance.data[\"instance_id\"])\n        creator_attributes = created_instance[\"creator_attributes\"]\n        # Disable version override\n        creator_attributes[\"use_next_version\"] = True\n        create_context.save_changes()\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/validate_filepaths.py",
    "content": "import os\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateFilePath(pyblish.api.InstancePlugin):\n    \"\"\"Validate existence of source filepaths on instance.\n\n    Plugins looks into key 'sourceFilepaths' and validate if paths there\n    actually exist on disk.\n\n    Also validate if the key is filled but is empty. In that case also\n    crashes so do not fill the key if unfilled value should not cause error.\n\n    This is primarily created for Simple Creator instances.\n    \"\"\"\n\n    label = \"Validate Filepaths\"\n    order = pyblish.api.ValidatorOrder - 0.49\n\n    hosts = [\"traypublisher\"]\n\n    def process(self, instance):\n        if \"sourceFilepaths\" not in instance.data:\n            self.log.info((\n                \"Skipped validation of source filepaths existence.\"\n                \" Instance does not have collected 'sourceFilepaths'\"\n            ))\n            return\n\n        family = instance.data[\"family\"]\n        label = instance.data[\"name\"]\n        filepaths = instance.data[\"sourceFilepaths\"]\n        if not filepaths:\n            raise PublishValidationError(\n                (\n                    \"Source filepaths of '{}' instance \\\"{}\\\" are not filled\"\n                ).format(family, label),\n                \"File not filled\",\n                (\n                    \"## Files were not filled\"\n                    \"\\nThis mean that you didn't enter any files into required\"\n                    \" file input.\"\n                    \"\\n- Please refresh publishing and check instance\"\n                    \" <b>{}</b>\"\n                ).format(label)\n            )\n\n        not_found_files = [\n            filepath\n            for filepath in filepaths\n            if not os.path.exists(filepath)\n        ]\n        if not_found_files:\n            joined_paths = \"\\n\".join([\n                \"- {}\".format(filepath)\n                for filepath in not_found_files\n            ])\n            raise PublishValidationError(\n                (\n                    \"Filepath of '{}' instance \\\"{}\\\" does not exist:\\n{}\"\n                ).format(family, label, joined_paths),\n                \"File not found\",\n                (\n                    \"## Files were not found\\nFiles\\n{}\"\n                    \"\\n\\nCheck if the path is still available.\"\n                ).format(joined_paths)\n            )\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py",
    "content": "import re\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin,\n)\n\n\nclass ValidateFrameRange(OptionalPyblishPluginMixin,\n                         pyblish.api.InstancePlugin):\n    \"\"\"Validating frame range of rendered files against state in DB.\"\"\"\n\n    label = \"Validate Frame Range\"\n    hosts = [\"traypublisher\"]\n    families = [\"render\", \"plate\"]\n    order = ValidateContentsOrder\n\n    optional = True\n    # published data might be sequence (.mov, .mp4) in that counting files\n    # doesnt make sense\n    check_extensions = [\"exr\", \"dpx\", \"jpg\", \"jpeg\", \"png\", \"tiff\", \"tga\",\n                        \"gif\", \"svg\"]\n    skip_timelines_check = []  # skip for specific task names (regex)\n\n    def process(self, instance):\n        # Skip the instance if is not active by data on the instance\n        if not self.is_active(instance.data):\n            return\n\n        # editorial would fail since they might not be in database yet\n        new_asset_publishing = instance.data.get(\"newAssetPublishing\")\n        if new_asset_publishing:\n            self.log.debug(\"Instance is creating new asset. Skipping.\")\n            return\n\n        if (self.skip_timelines_check and\n            any(re.search(pattern, instance.data[\"task\"])\n                for pattern in self.skip_timelines_check)):\n            self.log.info(\"Skipping for {} task\".format(instance.data[\"task\"]))\n\n        asset_doc = instance.data[\"assetEntity\"]\n        asset_data = asset_doc[\"data\"]\n        frame_start = asset_data[\"frameStart\"]\n        frame_end = asset_data[\"frameEnd\"]\n        handle_start = asset_data[\"handleStart\"]\n        handle_end = asset_data[\"handleEnd\"]\n        duration = (frame_end - frame_start + 1) + handle_start + handle_end\n\n        repres = instance.data.get(\"representations\")\n        if not repres:\n            self.log.info(\"No representations, skipping.\")\n            return\n\n        first_repre = repres[0]\n        ext = first_repre['ext'].replace(\".\", '')\n\n        if not ext or ext.lower() not in self.check_extensions:\n            self.log.warning(\"Cannot check for extension {}\".format(ext))\n            return\n\n        files = first_repre[\"files\"]\n        if isinstance(files, str):\n            files = [files]\n        frames = len(files)\n\n        msg = (\n            \"Frame duration from DB:'{}' doesn't match number of files:'{}'\"\n            \" Please change frame range for Asset or limit no. of files\"\n        ). format(int(duration), frames)\n\n        formatting_data = {\"duration\": duration,\n                           \"found\": frames}\n        if frames != duration:\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n\n        self.log.debug(\"Valid ranges expected '{}' - found '{}'\".\n                       format(int(duration), frames))\n"
  },
  {
    "path": "openpype/hosts/traypublisher/plugins/publish/validate_online_file.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    ValidateContentsOrder,\n    PublishValidationError,\n    OptionalPyblishPluginMixin,\n)\nfrom openpype.client import get_subset_by_name\n\n\nclass ValidateOnlineFile(OptionalPyblishPluginMixin,\n                         pyblish.api.InstancePlugin):\n    \"\"\"Validate that subset doesn't exist yet.\"\"\"\n    label = \"Validate Existing Online Files\"\n    hosts = [\"traypublisher\"]\n    families = [\"online\"]\n    order = ValidateContentsOrder\n\n    optional = True\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n        project_name = instance.context.data[\"projectName\"]\n        asset_id = instance.data[\"assetEntity\"][\"_id\"]\n        subset = get_subset_by_name(\n            project_name, instance.data[\"subset\"], asset_id)\n\n        if subset:\n            raise PublishValidationError(\n                \"Subset to be published already exists.\",\n                title=self.label\n            )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/__init__.py",
    "content": "from .addon import (\n    get_launch_script_path,\n    TVPaintAddon,\n    TVPAINT_ROOT_DIR,\n)\n\n\n__all__ = (\n    \"get_launch_script_path\",\n    \"TVPaintAddon\",\n    \"TVPAINT_ROOT_DIR\",\n)\n"
  },
  {
    "path": "openpype/hosts/tvpaint/addon.py",
    "content": "import os\nfrom openpype.modules import OpenPypeModule, IHostAddon\n\nTVPAINT_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\ndef get_launch_script_path():\n    return os.path.join(\n        TVPAINT_ROOT_DIR,\n        \"api\",\n        \"launch_script.py\"\n    )\n\n\nclass TVPaintAddon(OpenPypeModule, IHostAddon):\n    name = \"tvpaint\"\n    host_name = \"tvpaint\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def add_implementation_envs(self, env, _app):\n        \"\"\"Modify environments to contain all required for implementation.\"\"\"\n\n        defaults = {\n            \"OPENPYPE_LOG_NO_COLORS\": \"True\"\n        }\n        for key, value in defaults.items():\n            if not env.get(key):\n                env[key] = value\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [\n            os.path.join(TVPAINT_ROOT_DIR, \"hooks\")\n        ]\n\n    def get_workfile_extensions(self):\n        return [\".tvpp\"]\n"
  },
  {
    "path": "openpype/hosts/tvpaint/api/__init__.py",
    "content": "from .communication_server import CommunicationWrapper\nfrom .pipeline import (\n    TVPaintHost,\n)\n\n\n__all__ = (\n    \"CommunicationWrapper\",\n\n    \"TVPaintHost\",\n)\n"
  },
  {
    "path": "openpype/hosts/tvpaint/api/communication_server.py",
    "content": "import os\nimport json\nimport time\nimport subprocess\nimport collections\nimport asyncio\nimport logging\nimport socket\nimport platform\nimport filecmp\nimport tempfile\nimport threading\nimport shutil\n\nfrom contextlib import closing\n\nfrom aiohttp import web\nfrom aiohttp_json_rpc import JsonRpc\nfrom aiohttp_json_rpc.protocol import (\n    encode_request, encode_error, decode_msg, JsonRpcMsgTyp\n)\nfrom aiohttp_json_rpc.exceptions import RpcError\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.lib import emit_event\nfrom openpype.hosts.tvpaint.tvpaint_plugin import get_plugin_files_path\n\nlog = logging.getLogger(__name__)\nlog.setLevel(logging.DEBUG)\n\n\nclass CommunicationWrapper:\n    # TODO add logs and exceptions\n    communicator = None\n\n    log = logging.getLogger(\"CommunicationWrapper\")\n\n    @classmethod\n    def create_qt_communicator(cls, *args, **kwargs):\n        \"\"\"Create communicator for Artist usage.\"\"\"\n        communicator = QtCommunicator(*args, **kwargs)\n        cls.set_communicator(communicator)\n        return communicator\n\n    @classmethod\n    def set_communicator(cls, communicator):\n        if not cls.communicator:\n            cls.communicator = communicator\n        else:\n            cls.log.warning(\"Communicator was set multiple times.\")\n\n    @classmethod\n    def client(cls):\n        if not cls.communicator:\n            return None\n        return cls.communicator.client()\n\n    @classmethod\n    def execute_george(cls, george_script):\n        \"\"\"Execute passed goerge script in TVPaint.\"\"\"\n        if not cls.communicator:\n            return\n        return cls.communicator.execute_george(george_script)\n\n\nclass WebSocketServer:\n    def __init__(self):\n        self.client = None\n\n        self.loop = asyncio.new_event_loop()\n        self.app = web.Application(loop=self.loop)\n        self.port = self.find_free_port()\n        self.websocket_thread = WebsocketServerThread(\n            self, self.port, loop=self.loop\n        )\n\n    @property\n    def server_is_running(self):\n        return self.websocket_thread.server_is_running\n\n    def add_route(self, *args, **kwargs):\n        self.app.router.add_route(*args, **kwargs)\n\n    @staticmethod\n    def find_free_port():\n        with closing(\n            socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n        ) as sock:\n            sock.bind((\"\", 0))\n            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n            port = sock.getsockname()[1]\n        return port\n\n    def start(self):\n        self.websocket_thread.start()\n\n    def stop(self):\n        try:\n            if self.websocket_thread.is_running:\n                log.debug(\"Stopping websocket server\")\n                self.websocket_thread.is_running = False\n                self.websocket_thread.stop()\n        except Exception:\n            log.warning(\n                \"Error has happened during Killing websocket server\",\n                exc_info=True\n            )\n\n\nclass WebsocketServerThread(threading.Thread):\n    \"\"\" Listener for websocket rpc requests.\n\n        It would be probably better to \"attach\" this to main thread (as for\n        example Harmony needs to run something on main thread), but currently\n        it creates separate thread and separate asyncio event loop\n    \"\"\"\n    def __init__(self, module, port, loop):\n        super(WebsocketServerThread, self).__init__()\n        self.is_running = False\n        self.server_is_running = False\n        self.port = port\n        self.module = module\n        self.loop = loop\n        self.runner = None\n        self.site = None\n        self.tasks = []\n\n    def run(self):\n        self.is_running = True\n\n        try:\n            log.debug(\"Starting websocket server\")\n\n            self.loop.run_until_complete(self.start_server())\n\n            log.info(\n                \"Running Websocket server on URL:\"\n                \" \\\"ws://localhost:{}\\\"\".format(self.port)\n            )\n\n            asyncio.ensure_future(self.check_shutdown(), loop=self.loop)\n\n            self.server_is_running = True\n            self.loop.run_forever()\n\n        except Exception:\n            log.warning(\n                \"Websocket Server service has failed\", exc_info=True\n            )\n        finally:\n            self.server_is_running = False\n            # optional\n            self.loop.close()\n\n        self.is_running = False\n        log.info(\"Websocket server stopped\")\n\n    async def start_server(self):\n        \"\"\" Starts runner and TCPsite \"\"\"\n        self.runner = web.AppRunner(self.module.app)\n        await self.runner.setup()\n        self.site = web.TCPSite(self.runner, \"localhost\", self.port)\n        await self.site.start()\n\n    def stop(self):\n        \"\"\"Sets is_running flag to false, 'check_shutdown' shuts server down\"\"\"\n        self.is_running = False\n\n    async def check_shutdown(self):\n        \"\"\" Future that is running and checks if server should be running\n            periodically.\n        \"\"\"\n        while self.is_running:\n            while self.tasks:\n                task = self.tasks.pop(0)\n                log.debug(\"waiting for task {}\".format(task))\n                await task\n                log.debug(\"returned value {}\".format(task.result))\n\n            await asyncio.sleep(0.5)\n\n        log.debug(\"## Server shutdown started\")\n\n        await self.site.stop()\n        log.debug(\"# Site stopped\")\n        await self.runner.cleanup()\n        log.debug(\"# Server runner stopped\")\n        tasks = [\n            task for task in asyncio.all_tasks()\n            if task is not asyncio.current_task()\n        ]\n        list(map(lambda task: task.cancel(), tasks))  # cancel all the tasks\n        results = await asyncio.gather(*tasks, return_exceptions=True)\n        log.debug(f\"Finished awaiting cancelled tasks, results: {results}...\")\n        await self.loop.shutdown_asyncgens()\n        # to really make sure everything else has time to stop\n        await asyncio.sleep(0.07)\n        self.loop.stop()\n\n\nclass BaseTVPaintRpc(JsonRpc):\n    def __init__(self, communication_obj, route_name=\"\", **kwargs):\n        super().__init__(**kwargs)\n        self.requests_ids = collections.defaultdict(lambda: 0)\n        self.waiting_requests = collections.defaultdict(list)\n        self.responses = collections.defaultdict(list)\n\n        self.route_name = route_name\n        self.communication_obj = communication_obj\n\n    async def _handle_rpc_msg(self, http_request, raw_msg):\n        # This is duplicated code from super but there is no way how to do it\n        # to be able handle server->client requests\n        host = http_request.host\n        if host in self.waiting_requests:\n            try:\n                _raw_message = raw_msg.data\n                msg = decode_msg(_raw_message)\n\n            except RpcError as error:\n                await self._ws_send_str(http_request, encode_error(error))\n                return\n\n            if msg.type in (JsonRpcMsgTyp.RESULT, JsonRpcMsgTyp.ERROR):\n                msg_data = json.loads(_raw_message)\n                if msg_data.get(\"id\") in self.waiting_requests[host]:\n                    self.responses[host].append(msg_data)\n                    return\n\n        return await super()._handle_rpc_msg(http_request, raw_msg)\n\n    def client_connected(self):\n        # TODO This is poor check. Add check it is client from TVPaint\n        if self.clients:\n            return True\n        return False\n\n    def send_notification(self, client, method, params=None):\n        if params is None:\n            params = []\n        asyncio.run_coroutine_threadsafe(\n            client.ws.send_str(encode_request(method, params=params)),\n            loop=self.loop\n        )\n\n    def send_request(self, client, method, params=None, timeout=0):\n        if params is None:\n            params = []\n\n        client_host = client.host\n\n        request_id = self.requests_ids[client_host]\n        self.requests_ids[client_host] += 1\n\n        self.waiting_requests[client_host].append(request_id)\n\n        log.debug(\"Sending request to client {} ({}, {}) id: {}\".format(\n            client_host, method, params, request_id\n        ))\n        future = asyncio.run_coroutine_threadsafe(\n            client.ws.send_str(encode_request(method, request_id, params)),\n            loop=self.loop\n        )\n        result = future.result()\n\n        not_found = object()\n        response = not_found\n        start = time.time()\n        while True:\n            if client.ws.closed:\n                return None\n\n            for _response in self.responses[client_host]:\n                _id = _response.get(\"id\")\n                if _id == request_id:\n                    response = _response\n                    break\n\n            if response is not not_found:\n                break\n\n            if timeout > 0 and (time.time() - start) > timeout:\n                raise Exception(\"Timeout passed\")\n                return\n\n            time.sleep(0.1)\n\n        if response is not_found:\n            raise Exception(\"Connection closed\")\n\n        self.responses[client_host].remove(response)\n\n        error = response.get(\"error\")\n        result = response.get(\"result\")\n        if error:\n            raise Exception(\"Error happened: {}\".format(error))\n        return result\n\n\nclass QtTVPaintRpc(BaseTVPaintRpc):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        from openpype.tools.utils import host_tools\n        self.tools_helper = host_tools.HostToolsHelper()\n\n        route_name = self.route_name\n\n        # Register methods\n        self.add_methods(\n            (route_name, self.workfiles_tool),\n            (route_name, self.loader_tool),\n            (route_name, self.publish_tool),\n            (route_name, self.scene_inventory_tool),\n            (route_name, self.library_loader_tool),\n            (route_name, self.experimental_tools)\n        )\n\n    # Panel routes for tools\n    async def workfiles_tool(self):\n        log.info(\"Triggering Workfile tool\")\n        item = MainThreadItem(self.tools_helper.show_workfiles)\n        self._execute_in_main_thread(item, wait=False)\n        return\n\n    async def loader_tool(self):\n        log.info(\"Triggering Loader tool\")\n        item = MainThreadItem(self.tools_helper.show_loader)\n        self._execute_in_main_thread(item, wait=False)\n        return\n\n    async def publish_tool(self):\n        log.info(\"Triggering Publish tool\")\n        item = MainThreadItem(self.tools_helper.show_publisher_tool)\n        self._execute_in_main_thread(item, wait=False)\n        return\n\n    async def scene_inventory_tool(self):\n        \"\"\"Open Scene Inventory tool.\n\n        Function can't confirm if tool was opened becauise one part of\n        SceneInventory initialization is calling websocket request to host but\n        host can't response because is waiting for response from this call.\n        \"\"\"\n        log.info(\"Triggering Scene inventory tool\")\n        item = MainThreadItem(self.tools_helper.show_scene_inventory)\n        # Do not wait for result of callback\n        self._execute_in_main_thread(item, wait=False)\n        return\n\n    async def library_loader_tool(self):\n        log.info(\"Triggering Library loader tool\")\n        item = MainThreadItem(self.tools_helper.show_library_loader)\n        self._execute_in_main_thread(item, wait=False)\n        return\n\n    async def experimental_tools(self):\n        log.info(\"Triggering Library loader tool\")\n        item = MainThreadItem(self.tools_helper.show_experimental_tools_dialog)\n        self._execute_in_main_thread(item, wait=False)\n        return\n\n    async def _async_execute_in_main_thread(self, item, **kwargs):\n        await self.communication_obj.async_execute_in_main_thread(\n            item, **kwargs\n        )\n\n    def _execute_in_main_thread(self, item, **kwargs):\n        return self.communication_obj.execute_in_main_thread(item, **kwargs)\n\n\nclass MainThreadItem:\n    \"\"\"Structure to store information about callback in main thread.\n\n    Item should be used to execute callback in main thread which may be needed\n    for execution of Qt objects.\n\n    Item store callback (callable variable), arguments and keyword arguments\n    for the callback. Item hold information about it's process.\n    \"\"\"\n    not_set = object()\n    sleep_time = 0.1\n\n    def __init__(self, callback, *args, **kwargs):\n        self.done = False\n        self.exception = self.not_set\n        self.result = self.not_set\n        self.callback = callback\n        self.args = args\n        self.kwargs = kwargs\n\n    def execute(self):\n        \"\"\"Execute callback and store its result.\n\n        Method must be called from main thread. Item is marked as `done`\n        when callback execution finished. Store output of callback of exception\n        information when callback raises one.\n        \"\"\"\n        log.debug(\"Executing process in main thread\")\n        if self.done:\n            log.warning(\"- item is already processed\")\n            return\n\n        callback = self.callback\n        args = self.args\n        kwargs = self.kwargs\n        log.info(\"Running callback: {}\".format(str(callback)))\n        try:\n            result = callback(*args, **kwargs)\n            self.result = result\n\n        except Exception as exc:\n            self.exception = exc\n\n        finally:\n            self.done = True\n\n    def wait(self):\n        \"\"\"Wait for result from main thread.\n\n        This method stops current thread until callback is executed.\n\n        Returns:\n            object: Output of callback. May be any type or object.\n\n        Raises:\n            Exception: Reraise any exception that happened during callback\n                execution.\n        \"\"\"\n        while not self.done:\n            time.sleep(self.sleep_time)\n\n        if self.exception is self.not_set:\n            return self.result\n        raise self.exception\n\n    async def async_wait(self):\n        \"\"\"Wait for result from main thread.\n\n        Returns:\n            object: Output of callback. May be any type or object.\n\n        Raises:\n            Exception: Reraise any exception that happened during callback\n                execution.\n        \"\"\"\n        while not self.done:\n            await asyncio.sleep(self.sleep_time)\n\n        if self.exception is self.not_set:\n            return self.result\n        raise self.exception\n\n\nclass BaseCommunicator:\n    def __init__(self):\n        self.process = None\n        self.websocket_server = None\n        self.websocket_rpc = None\n        self.exit_code = None\n        self._connected_client = None\n\n    @property\n    def server_is_running(self):\n        if self.websocket_server is None:\n            return False\n        return self.websocket_server.server_is_running\n\n    def _windows_file_process(self, src_dst_mapping, to_remove):\n        \"\"\"Windows specific file processing asking for admin permissions.\n\n        It is required to have administration permissions to modify plugin\n        files in TVPaint installation folder.\n\n        Method requires `pywin32` python module.\n\n        Args:\n            src_dst_mapping (list, tuple, set): Mapping of source file to\n                destination. Both must be full path. Each item must be iterable\n                of size 2 `(C:/src/file.dll, C:/dst/file.dll)`.\n            to_remove (list): Fullpath to files that should be removed.\n        \"\"\"\n\n        import pythoncom\n        from win32comext.shell import shell\n\n        # Create temp folder where plugin files are temporary copied\n        # - reason is that copy to TVPaint requires administartion permissions\n        #   but admin may not have access to source folder\n        tmp_dir = os.path.normpath(\n            tempfile.mkdtemp(prefix=\"tvpaint_copy_\")\n        )\n\n        # Copy source to temp folder and create new mapping\n        dst_folders = collections.defaultdict(list)\n        new_src_dst_mapping = []\n        for old_src, dst in src_dst_mapping:\n            new_src = os.path.join(tmp_dir, os.path.split(old_src)[1])\n            shutil.copy(old_src, new_src)\n            new_src_dst_mapping.append((new_src, dst))\n\n        for src, dst in new_src_dst_mapping:\n            src = os.path.normpath(src)\n            dst = os.path.normpath(dst)\n            dst_filename = os.path.basename(dst)\n            dst_folder_path = os.path.dirname(dst)\n            dst_folders[dst_folder_path].append((dst_filename, src))\n\n        # create an instance of IFileOperation\n        fo = pythoncom.CoCreateInstance(\n            shell.CLSID_FileOperation,\n            None,\n            pythoncom.CLSCTX_ALL,\n            shell.IID_IFileOperation\n        )\n        # Add delete command to file operation object\n        for filepath in to_remove:\n            item = shell.SHCreateItemFromParsingName(\n                filepath, None, shell.IID_IShellItem\n            )\n            fo.DeleteItem(item)\n\n        # here you can use SetOperationFlags, progress Sinks, etc.\n        for folder_path, items in dst_folders.items():\n            # create an instance of IShellItem for the target folder\n            folder_item = shell.SHCreateItemFromParsingName(\n                folder_path, None, shell.IID_IShellItem\n            )\n            for _dst_filename, source_file_path in items:\n                # create an instance of IShellItem for the source item\n                copy_item = shell.SHCreateItemFromParsingName(\n                    source_file_path, None, shell.IID_IShellItem\n                )\n                # queue the copy operation\n                fo.CopyItem(copy_item, folder_item, _dst_filename, None)\n\n        # commit\n        fo.PerformOperations()\n\n        # Remove temp folder\n        shutil.rmtree(tmp_dir)\n\n    def _prepare_windows_plugin(self, launch_args):\n        \"\"\"Copy plugin to TVPaint plugins and set PATH to dependencies.\n\n        Check if plugin in TVPaint's plugins exist and match to plugin\n        version to current implementation version. Based on 64-bit or 32-bit\n        version of the plugin. Path to libraries required for plugin is added\n        to PATH variable.\n        \"\"\"\n\n        host_executable = launch_args[0]\n        executable_file = os.path.basename(host_executable)\n        if \"64bit\" in executable_file:\n            subfolder = \"windows_x64\"\n        elif \"32bit\" in executable_file:\n            subfolder = \"windows_x86\"\n        else:\n            raise ValueError(\n                \"Can't determine if executable \"\n                \"leads to 32-bit or 64-bit TVPaint!\"\n            )\n\n        plugin_files_path = get_plugin_files_path()\n        # Folder for right windows plugin files\n        source_plugins_dir = os.path.join(plugin_files_path, subfolder)\n\n        # Path to libraries (.dll) required for plugin library\n        # - additional libraries can be copied to TVPaint installation folder\n        #   (next to executable) or added to PATH environment variable\n        additional_libs_folder = os.path.join(\n            source_plugins_dir,\n            \"additional_libraries\"\n        )\n        additional_libs_folder = additional_libs_folder.replace(\"\\\\\", \"/\")\n        if (\n            os.path.exists(additional_libs_folder)\n            and additional_libs_folder not in os.environ[\"PATH\"]\n        ):\n            os.environ[\"PATH\"] += (os.pathsep + additional_libs_folder)\n\n        # Path to TVPaint's plugins folder (where we want to add our plugin)\n        host_plugins_path = os.path.join(\n            os.path.dirname(host_executable),\n            \"plugins\"\n        )\n\n        # Files that must be copied to TVPaint's plugin folder\n        plugin_dir = os.path.join(source_plugins_dir, \"plugin\")\n\n        to_copy = []\n        to_remove = []\n        # Remove old plugin name\n        deprecated_filepath = os.path.join(\n            host_plugins_path, \"AvalonPlugin.dll\"\n        )\n        if os.path.exists(deprecated_filepath):\n            to_remove.append(deprecated_filepath)\n\n        for filename in os.listdir(plugin_dir):\n            src_full_path = os.path.join(plugin_dir, filename)\n            dst_full_path = os.path.join(host_plugins_path, filename)\n            if dst_full_path in to_remove:\n                to_remove.remove(dst_full_path)\n\n            if (\n                not os.path.exists(dst_full_path)\n                or not filecmp.cmp(src_full_path, dst_full_path)\n            ):\n                to_copy.append((src_full_path, dst_full_path))\n\n        # Skip copy if everything is done\n        if not to_copy and not to_remove:\n            return\n\n        # Try to copy\n        try:\n            self._windows_file_process(to_copy, to_remove)\n        except Exception:\n            log.error(\"Plugin copy failed\", exc_info=True)\n\n        # Validate copy was done\n        invalid_copy = []\n        for src, dst in to_copy:\n            if not os.path.exists(dst) or not filecmp.cmp(src, dst):\n                invalid_copy.append((src, dst))\n\n        # Validate delete was dones\n        invalid_remove = []\n        for filepath in to_remove:\n            if os.path.exists(filepath):\n                invalid_remove.append(filepath)\n\n        if not invalid_remove and not invalid_copy:\n            return\n\n        msg_parts = []\n        if invalid_remove:\n            msg_parts.append(\n                \"Failed to remove files: {}\".format(\", \".join(invalid_remove))\n            )\n\n        if invalid_copy:\n            _invalid = [\n                \"\\\"{}\\\" -> \\\"{}\\\"\".format(src, dst)\n                for src, dst in invalid_copy\n            ]\n            msg_parts.append(\n                \"Failed to copy files: {}\".format(\", \".join(_invalid))\n            )\n        raise RuntimeError(\" & \".join(msg_parts))\n\n    def _launch_tv_paint(self, launch_args):\n        flags = (\n            subprocess.DETACHED_PROCESS\n            | subprocess.CREATE_NEW_PROCESS_GROUP\n        )\n        env = os.environ.copy()\n        # Remove QuickTime from PATH on windows\n        # - quicktime overrides TVPaint's ffmpeg encode/decode which may\n        #   cause issues on loading\n        if platform.system().lower() == \"windows\":\n            new_path = []\n            for path in env[\"PATH\"].split(os.pathsep):\n                if path and \"quicktime\" not in path.lower():\n                    new_path.append(path)\n            env[\"PATH\"] = os.pathsep.join(new_path)\n\n        kwargs = {\n            \"env\": env,\n            \"creationflags\": flags\n        }\n        self.process = subprocess.Popen(launch_args, **kwargs)\n\n    def _create_routes(self):\n        self.websocket_rpc = BaseTVPaintRpc(\n            self, loop=self.websocket_server.loop\n        )\n        self.websocket_server.add_route(\n            \"*\", \"/\", self.websocket_rpc.handle_request\n        )\n\n    def _start_webserver(self):\n        self.websocket_server.start()\n        # Make sure RPC is using same loop as websocket server\n        while not self.websocket_server.server_is_running:\n            time.sleep(0.1)\n\n    def _stop_webserver(self):\n        self.websocket_server.stop()\n\n    def _exit(self, exit_code=None):\n        self._stop_webserver()\n        if exit_code is not None:\n            self.exit_code = exit_code\n\n        if self.exit_code is None:\n            self.exit_code = 0\n\n    def stop(self):\n        \"\"\"Stop communication and currently running python process.\"\"\"\n        log.info(\"Stopping communication\")\n        self._exit()\n\n    def launch(self, launch_args):\n        \"\"\"Prepare all required data and launch host.\n\n        First is prepared websocket server as communication point for host,\n        when server is ready to use host is launched as subprocess.\n        \"\"\"\n        if platform.system().lower() == \"windows\":\n            self._prepare_windows_plugin(launch_args)\n\n        # Launch TVPaint and the websocket server.\n        log.info(\"Launching TVPaint\")\n        self.websocket_server = WebSocketServer()\n\n        self._create_routes()\n\n        os.environ[\"WEBSOCKET_URL\"] = \"ws://localhost:{}\".format(\n            self.websocket_server.port\n        )\n\n        log.info(\"Added request handler for url: {}\".format(\n            os.environ[\"WEBSOCKET_URL\"]\n        ))\n\n        self._start_webserver()\n\n        # Start TVPaint when server is running\n        self._launch_tv_paint(launch_args)\n\n        log.info(\"Waiting for client connection\")\n        while True:\n            if self.process.poll() is not None:\n                log.debug(\"Host process is not alive. Exiting\")\n                self._exit(1)\n                return\n\n            if self.websocket_rpc.client_connected():\n                log.info(\"Client has connected\")\n                break\n            time.sleep(0.5)\n\n        self._on_client_connect()\n\n        emit_event(\"application.launched\")\n\n    def _on_client_connect(self):\n        self._initial_textfile_write()\n\n    def _initial_textfile_write(self):\n        \"\"\"Show popup about Write to file at start of TVPaint.\"\"\"\n        tmp_file = tempfile.NamedTemporaryFile(\n            mode=\"w\", prefix=\"a_tvp_\", suffix=\".txt\", delete=False\n        )\n        tmp_file.close()\n        tmp_filepath = tmp_file.name.replace(\"\\\\\", \"/\")\n        george_script = (\n            \"tv_writetextfile \\\"strict\\\" \\\"append\\\" \\\"{}\\\" \\\"empty\\\"\"\n        ).format(tmp_filepath)\n\n        result = CommunicationWrapper.execute_george(george_script)\n\n        # Remote the file\n        os.remove(tmp_filepath)\n\n        if result is None:\n            log.warning(\n                \"Host was probably closed before plugin was initialized.\"\n            )\n        elif result.lower() == \"forbidden\":\n            log.warning(\"User didn't confirm saving files.\")\n\n    def _client(self):\n        if not self.websocket_rpc:\n            log.warning(\"Communicator's server did not start yet.\")\n            return None\n\n        for client in self.websocket_rpc.clients:\n            if not client.ws.closed:\n                return client\n        log.warning(\"Client is not yet connected to Communicator.\")\n        return None\n\n    def client(self):\n        if not self._connected_client or self._connected_client.ws.closed:\n            self._connected_client = self._client()\n        return self._connected_client\n\n    def send_request(self, method, params=None):\n        client = self.client()\n        if not client:\n            return\n\n        return self.websocket_rpc.send_request(\n            client, method, params\n        )\n\n    def send_notification(self, method, params=None):\n        client = self.client()\n        if not client:\n            return\n\n        self.websocket_rpc.send_notification(\n            client, method, params\n        )\n\n    def execute_george(self, george_script):\n        \"\"\"Execute passed goerge script in TVPaint.\"\"\"\n        return self.send_request(\n            \"execute_george\", [george_script]\n        )\n\n    def execute_george_through_file(self, george_script):\n        \"\"\"Execute george script with temp file.\n\n        Allows to execute multiline george script without stopping websocket\n        client.\n\n        On windows make sure script does not contain paths with backwards\n        slashes in paths, TVPaint won't execute properly in that case.\n\n        Args:\n            george_script (str): George script to execute. May be multilined.\n        \"\"\"\n        temporary_file = tempfile.NamedTemporaryFile(\n            mode=\"w\", prefix=\"a_tvp_\", suffix=\".grg\", delete=False\n        )\n        temporary_file.write(george_script)\n        temporary_file.close()\n        temp_file_path = temporary_file.name.replace(\"\\\\\", \"/\")\n        self.execute_george(\"tv_runscript {}\".format(temp_file_path))\n        os.remove(temp_file_path)\n\n\nclass QtCommunicator(BaseCommunicator):\n    label = os.getenv(\"AVALON_LABEL\")\n    if not label:\n        label = \"AYON\" if AYON_SERVER_ENABLED else \"OpenPype\"\n    title = \"{} Tools\".format(label)\n    menu_definitions = {\n        \"title\": title,\n        \"menu_items\": [\n            {\n                \"callback\": \"workfiles_tool\",\n                \"label\": \"Workfiles\",\n                \"help\": \"Open workfiles tool\"\n            }, {\n                \"callback\": \"loader_tool\",\n                \"label\": \"Load\",\n                \"help\": \"Open loader tool\"\n            }, {\n                \"callback\": \"scene_inventory_tool\",\n                \"label\": \"Scene inventory\",\n                \"help\": \"Open scene inventory tool\"\n            }, {\n                \"callback\": \"publish_tool\",\n                \"label\": \"Publish\",\n                \"help\": \"Open publisher\"\n            }, {\n                \"callback\": \"library_loader_tool\",\n                \"label\": \"Library\",\n                \"help\": \"Open library loader tool\"\n            }, {\n                \"callback\": \"experimental_tools\",\n                \"label\": \"Experimental tools\",\n                \"help\": \"Open experimental tools dialog\"\n            }\n        ]\n    }\n\n    def __init__(self, qt_app):\n        super().__init__()\n        self.callback_queue = collections.deque()\n        self.qt_app = qt_app\n\n    def _create_routes(self):\n        self.websocket_rpc = QtTVPaintRpc(\n            self, loop=self.websocket_server.loop\n        )\n        self.websocket_server.add_route(\n            \"*\", \"/\", self.websocket_rpc.handle_request\n        )\n\n    def execute_in_main_thread(self, main_thread_item, wait=True):\n        \"\"\"Add `MainThreadItem` to callback queue and wait for result.\"\"\"\n        self.callback_queue.append(main_thread_item)\n        if wait:\n            return main_thread_item.wait()\n        return\n\n    async def async_execute_in_main_thread(self, main_thread_item, wait=True):\n        \"\"\"Add `MainThreadItem` to callback queue and wait for result.\"\"\"\n        self.callback_queue.append(main_thread_item)\n        if wait:\n            return await main_thread_item.async_wait()\n\n    def main_thread_listen(self):\n        \"\"\"Get last `MainThreadItem` from queue.\n\n        Must be called from main thread.\n\n        Method checks if host process is still running as it may cause\n        issues if not.\n        \"\"\"\n        # check if host still running\n        if self.process.poll() is not None:\n            self._exit()\n            return None\n\n        if self.callback_queue:\n            return self.callback_queue.popleft()\n        return None\n\n    def _on_client_connect(self):\n        super()._on_client_connect()\n        self._build_menu()\n\n    def _build_menu(self):\n        self.send_request(\n            \"define_menu\", [self.menu_definitions]\n        )\n\n    def _exit(self, *args, **kwargs):\n        super()._exit(*args, **kwargs)\n        emit_event(\"application.exit\")\n        self.qt_app.exit(self.exit_code)\n"
  },
  {
    "path": "openpype/hosts/tvpaint/api/launch_script.py",
    "content": "import os\nimport sys\nimport signal\nimport traceback\nimport ctypes\nimport platform\nimport logging\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import style\nfrom openpype.pipeline import install_host\nfrom openpype.hosts.tvpaint.api import (\n    TVPaintHost,\n    CommunicationWrapper,\n)\n\nlog = logging.getLogger(__name__)\n\n\ndef safe_excepthook(*args):\n    traceback.print_exception(*args)\n\n\ndef main(launch_args):\n    # Be sure server won't crash at any moment but just print traceback\n    sys.excepthook = safe_excepthook\n\n    # Create QtApplication for tools\n    # - QApplicaiton is also main thread/event loop of the server\n    qt_app = QtWidgets.QApplication([])\n\n    tvpaint_host = TVPaintHost()\n    # Execute pipeline installation\n    install_host(tvpaint_host)\n\n    # Create Communicator object and trigger launch\n    # - this must be done before anything is processed\n    communicator = CommunicationWrapper.create_qt_communicator(qt_app)\n    communicator.launch(launch_args)\n\n    def process_in_main_thread():\n        \"\"\"Execution of `MainThreadItem`.\"\"\"\n        item = communicator.main_thread_listen()\n        if item:\n            item.execute()\n\n    timer = QtCore.QTimer()\n    timer.setInterval(100)\n    timer.timeout.connect(process_in_main_thread)\n    timer.start()\n\n    # Register terminal signal handler\n    def signal_handler(*_args):\n        print(\"You pressed Ctrl+C. Process ended.\")\n        communicator.stop()\n\n    signal.signal(signal.SIGINT, signal_handler)\n    signal.signal(signal.SIGTERM, signal_handler)\n\n    qt_app.setQuitOnLastWindowClosed(False)\n    qt_app.setStyleSheet(style.load_stylesheet())\n\n    # Load avalon icon\n    icon_path = style.app_icon_path()\n    if icon_path:\n        icon = QtGui.QIcon(icon_path)\n        qt_app.setWindowIcon(icon)\n\n    # Set application name to be able show application icon in task bar\n    if platform.system().lower() == \"windows\":\n        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(\n            u\"WebsocketServer\"\n        )\n\n    # Run Qt application event processing\n    sys.exit(qt_app.exec_())\n\n\nif __name__ == \"__main__\":\n    args = list(sys.argv)\n    if os.path.abspath(__file__) == os.path.normpath(args[0]):\n        # Pop path to script\n        args.pop(0)\n    main(args)\n"
  },
  {
    "path": "openpype/hosts/tvpaint/api/lib.py",
    "content": "import os\nimport logging\nimport tempfile\n\nfrom .communication_server import CommunicationWrapper\n\nlog = logging.getLogger(__name__)\n\n\ndef execute_george(george_script, communicator=None):\n    if not communicator:\n        communicator = CommunicationWrapper.communicator\n    return communicator.execute_george(george_script)\n\n\ndef execute_george_through_file(george_script, communicator=None):\n    \"\"\"Execute george script with temp file.\n\n    Allows to execute multiline george script without stopping websocket\n    client.\n\n    On windows make sure script does not contain paths with backwards\n    slashes in paths, TVPaint won't execute properly in that case.\n\n    Args:\n        george_script (str): George script to execute. May be multilined.\n    \"\"\"\n    if not communicator:\n        communicator = CommunicationWrapper.communicator\n\n    return communicator.execute_george_through_file(george_script)\n\n\ndef parse_layers_data(data):\n    \"\"\"Parse layers data loaded in 'get_layers_data'.\"\"\"\n    layers = []\n    layers_raw = data.split(\"\\n\")\n    for layer_raw in layers_raw:\n        layer_raw = layer_raw.strip()\n        if not layer_raw:\n            continue\n        (\n            layer_id, group_id, visible, position, opacity, name,\n            layer_type,\n            frame_start, frame_end, prelighttable, postlighttable,\n            selected, editable, sencil_state, is_current\n        ) = layer_raw.split(\"|\")\n        layer = {\n            \"layer_id\": int(layer_id),\n            \"group_id\": int(group_id),\n            \"visible\": visible == \"ON\",\n            \"position\": int(position),\n            # Opacity from 'tv_layerinfo' is always set to '0' so it's unusable\n            # \"opacity\": int(opacity),\n            \"name\": name,\n            \"type\": layer_type,\n            \"frame_start\": int(frame_start),\n            \"frame_end\": int(frame_end),\n            \"prelighttable\": prelighttable == \"1\",\n            \"postlighttable\": postlighttable == \"1\",\n            \"selected\": selected == \"1\",\n            \"editable\": editable == \"1\",\n            \"sencil_state\": sencil_state,\n            \"is_current\": is_current == \"1\"\n        }\n        layers.append(layer)\n    return layers\n\n\ndef get_layers_data_george_script(output_filepath, layer_ids=None):\n    \"\"\"Prepare george script which will collect all layers from workfile.\"\"\"\n    output_filepath = output_filepath.replace(\"\\\\\", \"/\")\n    george_script_lines = [\n        # Variable containing full path to output file\n        \"output_path = \\\"{}\\\"\".format(output_filepath),\n        # Get Current Layer ID\n        \"tv_LayerCurrentID\",\n        \"current_layer_id = result\"\n    ]\n    # Script part for getting and storing layer information to temp\n    layer_data_getter = (\n        # Get information about layer's group\n        \"tv_layercolor \\\"get\\\" layer_id\",\n        \"group_id = result\",\n        \"tv_LayerInfo layer_id\",\n        (\n            \"PARSE result visible position opacity name\"\n            \" type startFrame endFrame prelighttable postlighttable\"\n            \" selected editable sencilState\"\n        ),\n        # Check if layer ID match `tv_LayerCurrentID`\n        \"is_current=0\",\n        \"IF CMP(current_layer_id, layer_id)==1\",\n        # - mark layer as selected if layer id match to current layer id\n        \"is_current=1\",\n        \"selected=1\",\n        \"END\",\n        # Prepare line with data separated by \"|\"\n        (\n            \"line = layer_id'|'group_id'|'visible'|'position'|'opacity'|'\"\n            \"name'|'type'|'startFrame'|'endFrame'|'prelighttable'|'\"\n            \"postlighttable'|'selected'|'editable'|'sencilState'|'is_current\"\n        ),\n        # Write data to output file\n        \"tv_writetextfile \\\"strict\\\" \\\"append\\\" '\\\"'output_path'\\\"' line\",\n    )\n\n    # Collect data for all layers if layers are not specified\n    if layer_ids is None:\n        george_script_lines.extend((\n            # Layer loop variables\n            \"loop = 1\",\n            \"idx = 0\",\n            # Layers loop\n            \"WHILE loop\",\n            \"tv_LayerGetID idx\",\n            \"layer_id = result\",\n            \"idx = idx + 1\",\n            # Stop loop if layer_id is \"NONE\"\n            \"IF CMP(layer_id, \\\"NONE\\\")==1\",\n            \"loop = 0\",\n            \"ELSE\",\n            *layer_data_getter,\n            \"END\",\n            \"END\"\n        ))\n    else:\n        for layer_id in layer_ids:\n            george_script_lines.append(\"layer_id = {}\".format(layer_id))\n            george_script_lines.extend(layer_data_getter)\n\n    return \"\\n\".join(george_script_lines)\n\n\ndef layers_data(layer_ids=None, communicator=None):\n    \"\"\"Backwards compatible function of 'get_layers_data'.\"\"\"\n    return get_layers_data(layer_ids, communicator)\n\n\ndef get_layers_data(layer_ids=None, communicator=None):\n    \"\"\"Collect all layers information from currently opened workfile.\"\"\"\n    output_file = tempfile.NamedTemporaryFile(\n        mode=\"w\", prefix=\"a_tvp_\", suffix=\".txt\", delete=False\n    )\n    output_file.close()\n    if layer_ids is not None and isinstance(layer_ids, int):\n        layer_ids = [layer_ids]\n\n    output_filepath = output_file.name\n\n    george_script = get_layers_data_george_script(output_filepath, layer_ids)\n\n    execute_george_through_file(george_script, communicator)\n\n    with open(output_filepath, \"r\") as stream:\n        data = stream.read()\n\n    output = parse_layers_data(data)\n    os.remove(output_filepath)\n    return output\n\n\ndef parse_group_data(data):\n    \"\"\"Parse group data collected in 'get_groups_data'.\"\"\"\n    output = []\n    groups_raw = data.split(\"\\n\")\n    for group_raw in groups_raw:\n        group_raw = group_raw.strip()\n        if not group_raw:\n            continue\n\n        parts = group_raw.split(\"|\")\n        # Check for length and concatenate 2 last items until length match\n        # - this happens if name contain spaces\n        while len(parts) > 6:\n            last_item = parts.pop(-1)\n            parts[-1] = \"|\".join([parts[-1], last_item])\n        clip_id, group_id, red, green, blue, name = parts\n\n        group = {\n            \"group_id\": int(group_id),\n            \"name\": name,\n            \"clip_id\": int(clip_id),\n            \"red\": int(red),\n            \"green\": int(green),\n            \"blue\": int(blue),\n        }\n        output.append(group)\n    return output\n\n\ndef groups_data(communicator=None):\n    \"\"\"Backwards compatible function of 'get_groups_data'.\"\"\"\n    return get_groups_data(communicator)\n\n\ndef get_groups_data(communicator=None):\n    \"\"\"Information about groups from current workfile.\"\"\"\n    output_file = tempfile.NamedTemporaryFile(\n        mode=\"w\", prefix=\"a_tvp_\", suffix=\".txt\", delete=False\n    )\n    output_file.close()\n\n    output_filepath = output_file.name.replace(\"\\\\\", \"/\")\n    george_script_lines = (\n        # Variable containing full path to output file\n        \"output_path = \\\"{}\\\"\".format(output_filepath),\n        \"empty = 0\",\n        # Loop over 26 groups which is ATM maximum possible (in 11.7)\n        # - ref: https://www.tvpaint.com/forum/viewtopic.php?t=13880\n        \"FOR idx = 1 TO 26\",\n        # Receive information about groups\n        \"tv_layercolor \\\"getcolor\\\" 0 idx\",\n        \"PARSE result clip_id group_index c_red c_green c_blue group_name\",\n        # Create and add line to output file\n        \"line = clip_id'|'group_index'|'c_red'|'c_green'|'c_blue'|'group_name\",\n        \"tv_writetextfile \\\"strict\\\" \\\"append\\\" '\\\"'output_path'\\\"' line\",\n        \"END\",\n    )\n    george_script = \"\\n\".join(george_script_lines)\n    execute_george_through_file(george_script, communicator)\n\n    with open(output_filepath, \"r\") as stream:\n        data = stream.read()\n\n    output = parse_group_data(data)\n    os.remove(output_filepath)\n    return output\n\n\ndef get_layers_pre_post_behavior(layer_ids, communicator=None):\n    \"\"\"Collect data about pre and post behavior of layer ids.\n\n    Pre and Post behaviors is enumerator of possible values:\n    - \"none\"\n    - \"repeat\"\n    - \"pingpong\"\n    - \"hold\"\n\n    Example output:\n    ```json\n    {\n        0: {\n            \"pre\": \"none\",\n            \"post\": \"repeat\"\n        }\n    }\n    ```\n\n    Returns:\n        dict: Key is layer id value is dictionary with \"pre\" and \"post\" keys.\n    \"\"\"\n    # Skip if is empty\n    if not layer_ids:\n        return {}\n\n    # Auto convert to list\n    if not isinstance(layer_ids, (list, set, tuple)):\n        layer_ids = [layer_ids]\n\n    # Prepare temp file\n    output_file = tempfile.NamedTemporaryFile(\n        mode=\"w\", prefix=\"a_tvp_\", suffix=\".txt\", delete=False\n    )\n    output_file.close()\n\n    output_filepath = output_file.name.replace(\"\\\\\", \"/\")\n    george_script_lines = [\n        # Variable containing full path to output file\n        \"output_path = \\\"{}\\\"\".format(output_filepath),\n    ]\n    for layer_id in layer_ids:\n        george_script_lines.extend([\n            \"layer_id = {}\".format(layer_id),\n            \"tv_layerprebehavior layer_id\",\n            \"pre_beh = result\",\n            \"tv_layerpostbehavior layer_id\",\n            \"post_beh = result\",\n            \"line = layer_id'|'pre_beh'|'post_beh\",\n            \"tv_writetextfile \\\"strict\\\" \\\"append\\\" '\\\"'output_path'\\\"' line\"\n        ])\n\n    george_script = \"\\n\".join(george_script_lines)\n    execute_george_through_file(george_script, communicator)\n\n    # Read data\n    with open(output_filepath, \"r\") as stream:\n        data = stream.read()\n\n    # Remove temp file\n    os.remove(output_filepath)\n\n    # Parse data\n    output = {}\n    raw_lines = data.split(\"\\n\")\n    for raw_line in raw_lines:\n        line = raw_line.strip()\n        if not line:\n            continue\n        parts = line.split(\"|\")\n        if len(parts) != 3:\n            continue\n        layer_id, pre_beh, post_beh = parts\n        output[int(layer_id)] = {\n            \"pre\": pre_beh.lower(),\n            \"post\": post_beh.lower()\n        }\n    return output\n\n\ndef get_layers_exposure_frames(layer_ids, layers_data=None, communicator=None):\n    \"\"\"Get exposure frames.\n\n    Easily said returns frames where keyframes are. Recognized with george\n    function `tv_exposureinfo` returning \"Head\".\n\n    Args:\n        layer_ids (list): Ids of a layers for which exposure frames should\n            look for.\n        layers_data (list): Precollected layers data. If are not passed then\n            'get_layers_data' is used.\n        communicator (BaseCommunicator): Communicator used for communication\n            with TVPaint.\n\n    Returns:\n        dict: Frames where exposure is set to \"Head\" by layer id.\n    \"\"\"\n\n    if layers_data is None:\n        layers_data = get_layers_data(layer_ids)\n    _layers_by_id = {\n        layer[\"layer_id\"]: layer\n        for layer in layers_data\n    }\n    layers_by_id = {\n        layer_id: _layers_by_id.get(layer_id)\n        for layer_id in layer_ids\n    }\n    tmp_file = tempfile.NamedTemporaryFile(\n        mode=\"w\", prefix=\"a_tvp_\", suffix=\".txt\", delete=False\n    )\n    tmp_file.close()\n    tmp_output_path = tmp_file.name.replace(\"\\\\\", \"/\")\n    george_script_lines = [\n        \"output_path = \\\"{}\\\"\".format(tmp_output_path)\n    ]\n\n    output = {}\n    layer_id_mapping = {}\n    for layer_id, layer_data in layers_by_id.items():\n        layer_id_mapping[str(layer_id)] = layer_id\n        output[layer_id] = []\n        if not layer_data:\n            continue\n        first_frame = layer_data[\"frame_start\"]\n        last_frame = layer_data[\"frame_end\"]\n        george_script_lines.extend([\n            \"line = \\\"\\\"\",\n            \"layer_id = {}\".format(layer_id),\n            \"line = line''layer_id\",\n            \"tv_layerset layer_id\",\n            \"frame = {}\".format(first_frame),\n            \"WHILE (frame <= {})\".format(last_frame),\n            \"tv_exposureinfo frame\",\n            \"exposure = result\",\n            \"IF (CMP(exposure, \\\"Head\\\") == 1)\",\n            \"line = line'|'frame\",\n            \"END\",\n            \"frame = frame + 1\",\n            \"END\",\n            \"tv_writetextfile \\\"strict\\\" \\\"append\\\" '\\\"'output_path'\\\"' line\"\n        ])\n\n    execute_george_through_file(\"\\n\".join(george_script_lines), communicator)\n\n    with open(tmp_output_path, \"r\") as stream:\n        data = stream.read()\n\n    os.remove(tmp_output_path)\n\n    lines = []\n    for line in data.split(\"\\n\"):\n        line = line.strip()\n        if line:\n            lines.append(line)\n\n    for line in lines:\n        line_items = list(line.split(\"|\"))\n        layer_id = line_items.pop(0)\n        _layer_id = layer_id_mapping[layer_id]\n        output[_layer_id] = [int(frame) for frame in line_items]\n\n    return output\n\n\ndef get_exposure_frames(\n    layer_id, first_frame=None, last_frame=None, communicator=None\n):\n    \"\"\"Get exposure frames.\n\n    Easily said returns frames where keyframes are. Recognized with george\n    function `tv_exposureinfo` returning \"Head\".\n\n    Args:\n        layer_id (int): Id of a layer for which exposure frames should\n            look for.\n        first_frame (int): From which frame will look for exposure frames.\n            Used layers first frame if not entered.\n        last_frame (int): Last frame where will look for exposure frames.\n            Used layers last frame if not entered.\n\n    Returns:\n        list: Frames where exposure is set to \"Head\".\n    \"\"\"\n    if first_frame is None or last_frame is None:\n        layer = layers_data(layer_id)[0]\n        if first_frame is None:\n            first_frame = layer[\"frame_start\"]\n        if last_frame is None:\n            last_frame = layer[\"frame_end\"]\n\n    tmp_file = tempfile.NamedTemporaryFile(\n        mode=\"w\", prefix=\"a_tvp_\", suffix=\".txt\", delete=False\n    )\n    tmp_file.close()\n    tmp_output_path = tmp_file.name.replace(\"\\\\\", \"/\")\n    george_script_lines = [\n        \"tv_layerset {}\".format(layer_id),\n        \"output_path = \\\"{}\\\"\".format(tmp_output_path),\n        \"output = \\\"\\\"\",\n        \"frame = {}\".format(first_frame),\n        \"WHILE (frame <= {})\".format(last_frame),\n        \"tv_exposureinfo frame\",\n        \"exposure = result\",\n        \"IF (CMP(exposure, \\\"Head\\\") == 1)\",\n        \"IF (CMP(output, \\\"\\\") == 1)\",\n        \"output = output''frame\",\n        \"ELSE\",\n        \"output = output'|'frame\",\n        \"END\",\n        \"END\",\n        \"frame = frame + 1\",\n        \"END\",\n        \"tv_writetextfile \\\"strict\\\" \\\"append\\\" '\\\"'output_path'\\\"' output\"\n    ]\n\n    execute_george_through_file(\"\\n\".join(george_script_lines), communicator)\n\n    with open(tmp_output_path, \"r\") as stream:\n        data = stream.read()\n\n    os.remove(tmp_output_path)\n\n    lines = []\n    for line in data.split(\"\\n\"):\n        line = line.strip()\n        if line:\n            lines.append(line)\n\n    exposure_frames = []\n    for line in lines:\n        for frame in line.split(\"|\"):\n            exposure_frames.append(int(frame))\n    return exposure_frames\n\n\ndef get_scene_data(communicator=None):\n    \"\"\"Scene data of currently opened scene.\n\n    Result contains resolution, pixel aspect, fps mark in/out with states,\n    frame start and background color.\n\n    Returns:\n        dict: Scene data collected in many ways.\n    \"\"\"\n    workfile_info = execute_george(\"tv_projectinfo\", communicator)\n    workfile_info_parts = workfile_info.split(\" \")\n\n    # Project frame start - not used\n    workfile_info_parts.pop(-1)\n    field_order = workfile_info_parts.pop(-1)\n    frame_rate = float(workfile_info_parts.pop(-1))\n    pixel_apsect = float(workfile_info_parts.pop(-1))\n    height = int(workfile_info_parts.pop(-1))\n    width = int(workfile_info_parts.pop(-1))\n\n    # Marks return as \"{frame - 1} {state} \", example \"0 set\".\n    result = execute_george(\"tv_markin\", communicator)\n    mark_in_frame, mark_in_state, _ = result.split(\" \")\n\n    result = execute_george(\"tv_markout\", communicator)\n    mark_out_frame, mark_out_state, _ = result.split(\" \")\n\n    start_frame = execute_george(\"tv_startframe\", communicator)\n    return {\n        \"width\": width,\n        \"height\": height,\n        \"pixel_aspect\": pixel_apsect,\n        \"fps\": frame_rate,\n        \"field_order\": field_order,\n        \"mark_in\": int(mark_in_frame),\n        \"mark_in_state\": mark_in_state,\n        \"mark_in_set\": mark_in_state == \"set\",\n        \"mark_out\": int(mark_out_frame),\n        \"mark_out_state\": mark_out_state,\n        \"mark_out_set\": mark_out_state == \"set\",\n        \"start_frame\": int(start_frame),\n        \"bg_color\": get_scene_bg_color(communicator)\n    }\n\n\ndef get_scene_bg_color(communicator=None):\n    \"\"\"Background color set on scene.\n\n    Is important for review exporting where scene bg color is used as\n    background.\n    \"\"\"\n    output_file = tempfile.NamedTemporaryFile(\n        mode=\"w\", prefix=\"a_tvp_\", suffix=\".txt\", delete=False\n    )\n    output_file.close()\n    output_filepath = output_file.name.replace(\"\\\\\", \"/\")\n    george_script_lines = [\n        # Variable containing full path to output file\n        \"output_path = \\\"{}\\\"\".format(output_filepath),\n        \"tv_background\",\n        \"bg_color = result\",\n        # Write data to output file\n        \"tv_writetextfile \\\"strict\\\" \\\"append\\\" '\\\"'output_path'\\\"' bg_color\"\n    ]\n\n    george_script = \"\\n\".join(george_script_lines)\n    execute_george_through_file(george_script, communicator)\n\n    with open(output_filepath, \"r\") as stream:\n        data = stream.read()\n\n    os.remove(output_filepath)\n    data = data.strip()\n    if not data:\n        return None\n    return data.split(\" \")\n"
  },
  {
    "path": "openpype/hosts/tvpaint/api/pipeline.py",
    "content": "import os\nimport json\nimport tempfile\nimport logging\n\nimport requests\n\nimport pyblish.api\n\nfrom openpype.client import get_asset_by_name\nfrom openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost\nfrom openpype.hosts.tvpaint import TVPAINT_ROOT_DIR\nfrom openpype.settings import get_current_project_settings\nfrom openpype.lib import register_event_callback\nfrom openpype.pipeline import (\n    legacy_io,\n    register_loader_plugin_path,\n    register_creator_plugin_path,\n    AVALON_CONTAINER_ID,\n)\nfrom openpype.pipeline.context_tools import get_global_context\n\nfrom .lib import (\n    execute_george,\n    execute_george_through_file\n)\n\nlog = logging.getLogger(__name__)\n\n\nMETADATA_SECTION = \"avalon\"\nSECTION_NAME_CONTEXT = \"context\"\nSECTION_NAME_CREATE_CONTEXT = \"create_context\"\nSECTION_NAME_INSTANCES = \"instances\"\nSECTION_NAME_CONTAINERS = \"containers\"\n# Maximum length of metadata chunk string\n# TODO find out the max (500 is safe enough)\nTVPAINT_CHUNK_LENGTH = 500\n\n\"\"\"TVPaint's Metadata\n\nMetadata are stored to TVPaint's workfile.\n\nWorkfile works similar to .ini file but has few limitation. Most important\nlimitation is that value under key has limited length. Due to this limitation\neach metadata section/key stores number of \"subkeys\" that are related to\nthe section.\n\nExample:\nMetadata key `\"instances\"` may have stored value \"2\". In that case it is\nexpected that there are also keys `[\"instances0\", \"instances1\"]`.\n\nWorkfile data looks like:\n```\n[avalon]\ninstances0=[{{__dq__}id{__dq__}: {__dq__}pyblish.avalon.instance{__dq__...\ninstances1=...more data...\ninstances=2\n```\n\"\"\"\n\n\nclass TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):\n    name = \"tvpaint\"\n\n    def install(self):\n        \"\"\"Install TVPaint-specific functionality.\"\"\"\n\n        log.info(\"OpenPype - Installing TVPaint integration\")\n        legacy_io.install()\n\n        # Create workdir folder if does not exist yet\n        workdir = legacy_io.Session[\"AVALON_WORKDIR\"]\n        if not os.path.exists(workdir):\n            os.makedirs(workdir)\n\n        plugins_dir = os.path.join(TVPAINT_ROOT_DIR, \"plugins\")\n        publish_dir = os.path.join(plugins_dir, \"publish\")\n        load_dir = os.path.join(plugins_dir, \"load\")\n        create_dir = os.path.join(plugins_dir, \"create\")\n\n        pyblish.api.register_host(\"tvpaint\")\n        pyblish.api.register_plugin_path(publish_dir)\n        register_loader_plugin_path(load_dir)\n        register_creator_plugin_path(create_dir)\n\n        register_event_callback(\"application.launched\", self.initial_launch)\n        register_event_callback(\"application.exit\", self.application_exit)\n\n    def get_current_project_name(self):\n        \"\"\"\n        Returns:\n            Union[str, None]: Current project name.\n        \"\"\"\n\n        return self.get_current_context().get(\"project_name\")\n\n    def get_current_asset_name(self):\n        \"\"\"\n        Returns:\n            Union[str, None]: Current asset name.\n        \"\"\"\n\n        return self.get_current_context().get(\"asset_name\")\n\n    def get_current_task_name(self):\n        \"\"\"\n        Returns:\n            Union[str, None]: Current task name.\n        \"\"\"\n\n        return self.get_current_context().get(\"task_name\")\n\n    def get_current_context(self):\n        context = get_current_workfile_context()\n        if not context:\n            return get_global_context()\n\n        if \"project_name\" in context:\n            return context\n        # This is legacy way how context was stored\n        return {\n            \"project_name\": context.get(\"project\"),\n            \"asset_name\": context.get(\"asset\"),\n            \"task_name\": context.get(\"task\")\n        }\n\n    # --- Create ---\n    def get_context_data(self):\n        return get_workfile_metadata(SECTION_NAME_CREATE_CONTEXT, {})\n\n    def update_context_data(self, data, changes):\n        return write_workfile_metadata(SECTION_NAME_CREATE_CONTEXT, data)\n\n    def list_instances(self):\n        \"\"\"List all created instances from current workfile.\"\"\"\n        return list_instances()\n\n    def write_instances(self, data):\n        return write_instances(data)\n\n    # --- Workfile ---\n    def open_workfile(self, filepath):\n        george_script = \"tv_LoadProject '\\\"'\\\"{}\\\"'\\\"'\".format(\n            filepath.replace(\"\\\\\", \"/\")\n        )\n        return execute_george_through_file(george_script)\n\n    def save_workfile(self, filepath=None):\n        if not filepath:\n            filepath = self.get_current_workfile()\n        context = get_global_context()\n        save_current_workfile_context(context)\n\n        # Execute george script to save workfile.\n        george_script = \"tv_SaveProject {}\".format(filepath.replace(\"\\\\\", \"/\"))\n        return execute_george(george_script)\n\n    def work_root(self, session):\n        return session[\"AVALON_WORKDIR\"]\n\n    def get_current_workfile(self):\n        return execute_george(\"tv_GetProjectName\")\n\n    def workfile_has_unsaved_changes(self):\n        return None\n\n    def get_workfile_extensions(self):\n        return [\".tvpp\"]\n\n    # --- Load ---\n    def get_containers(self):\n        return get_containers()\n\n    def initial_launch(self):\n        # Setup project settings if its the template that's launched.\n        # TODO also check for template creation when it's possible to define\n        #   templates\n        last_workfile = os.environ.get(\"AVALON_LAST_WORKFILE\")\n        if not last_workfile or os.path.exists(last_workfile):\n            return\n\n        log.info(\"Setting up project...\")\n        global_context = get_global_context()\n        project_name = global_context.get(\"project_name\")\n        asset_name = global_context.get(\"aset_name\")\n        if not project_name or not asset_name:\n            return\n\n        asset_doc = get_asset_by_name(project_name, asset_name)\n\n        set_context_settings(project_name, asset_doc)\n\n    def application_exit(self):\n        \"\"\"Logic related to TimerManager.\n\n        Todo:\n            This should be handled out of TVPaint integration logic.\n        \"\"\"\n\n        data = get_current_project_settings()\n        stop_timer = data[\"tvpaint\"][\"stop_timer_on_application_exit\"]\n\n        if not stop_timer:\n            return\n\n        # Stop application timer.\n        webserver_url = os.environ.get(\"OPENPYPE_WEBSERVER_URL\")\n        rest_api_url = \"{}/timers_manager/stop_timer\".format(webserver_url)\n        requests.post(rest_api_url)\n\n\ndef containerise(\n    name, namespace, members, context, loader, current_containers=None\n):\n    \"\"\"Add new container to metadata.\n\n    Args:\n        name (str): Container name.\n        namespace (str): Container namespace.\n        members (list): List of members that were loaded and belongs\n            to the container (layer names).\n        current_containers (list): Preloaded containers. Should be used only\n            on update/switch when containers were modified during the process.\n\n    Returns:\n        dict: Container data stored to workfile metadata.\n    \"\"\"\n\n    container_data = {\n        \"schema\": \"openpype:container-2.0\",\n        \"id\": AVALON_CONTAINER_ID,\n        \"members\": members,\n        \"name\": name,\n        \"namespace\": namespace,\n        \"loader\": str(loader),\n        \"representation\": str(context[\"representation\"][\"_id\"])\n    }\n    if current_containers is None:\n        current_containers = get_containers()\n\n    # Add container to containers list\n    current_containers.append(container_data)\n\n    # Store data to metadata\n    write_workfile_metadata(SECTION_NAME_CONTAINERS, current_containers)\n\n    return container_data\n\n\ndef split_metadata_string(text, chunk_length=None):\n    \"\"\"Split string by length.\n\n    Split text to chunks by entered length.\n    Example:\n        ```python\n        text = \"ABCDEFGHIJKLM\"\n        result = split_metadata_string(text, 3)\n        print(result)\n        >>> ['ABC', 'DEF', 'GHI', 'JKL']\n        ```\n\n    Args:\n        text (str): Text that will be split into chunks.\n        chunk_length (int): Single chunk size. Default chunk_length is\n            set to global variable `TVPAINT_CHUNK_LENGTH`.\n\n    Returns:\n        list: List of strings with at least one item.\n    \"\"\"\n    if chunk_length is None:\n        chunk_length = TVPAINT_CHUNK_LENGTH\n    chunks = []\n    for idx in range(chunk_length, len(text) + chunk_length, chunk_length):\n        start_idx = idx - chunk_length\n        chunks.append(text[start_idx:idx])\n    return chunks\n\n\ndef get_workfile_metadata_string_for_keys(metadata_keys):\n    \"\"\"Read metadata for specific keys from current project workfile.\n\n    All values from entered keys are stored to single string without separator.\n\n    Function is designed to help get all values for one metadata key at once.\n    So order of passed keys matteres.\n\n    Args:\n        metadata_keys (list, str): Metadata keys for which data should be\n            retrieved. Order of keys matters! It is possible to enter only\n            single key as string.\n    \"\"\"\n    # Add ability to pass only single key\n    if isinstance(metadata_keys, str):\n        metadata_keys = [metadata_keys]\n\n    output_file = tempfile.NamedTemporaryFile(\n        mode=\"w\", prefix=\"a_tvp_\", suffix=\".txt\", delete=False\n    )\n    output_file.close()\n    output_filepath = output_file.name.replace(\"\\\\\", \"/\")\n\n    george_script_parts = []\n    george_script_parts.append(\n        \"output_path = \\\"{}\\\"\".format(output_filepath)\n    )\n    # Store data for each index of metadata key\n    for metadata_key in metadata_keys:\n        george_script_parts.append(\n            \"tv_readprojectstring \\\"{}\\\" \\\"{}\\\" \\\"\\\"\".format(\n                METADATA_SECTION, metadata_key\n            )\n        )\n        george_script_parts.append(\n            \"tv_writetextfile \\\"strict\\\" \\\"append\\\" '\\\"'output_path'\\\"' result\"\n        )\n\n    # Execute the script\n    george_script = \"\\n\".join(george_script_parts)\n    execute_george_through_file(george_script)\n\n    # Load data from temp file\n    with open(output_filepath, \"r\") as stream:\n        file_content = stream.read()\n\n    # Remove `\\n` from content\n    output_string = file_content.replace(\"\\n\", \"\")\n\n    # Delete temp file\n    os.remove(output_filepath)\n\n    return output_string\n\n\ndef get_workfile_metadata_string(metadata_key):\n    \"\"\"Read metadata for specific key from current project workfile.\"\"\"\n    result = get_workfile_metadata_string_for_keys([metadata_key])\n    if not result:\n        return None\n\n    stripped_result = result.strip()\n    if not stripped_result:\n        return None\n\n    # NOTE Backwards compatibility when metadata key did not store range of key\n    #   indexes but the value itself\n    # NOTE We don't have to care about negative values with `isdecimal` check\n    if not stripped_result.isdecimal():\n        metadata_string = result\n    else:\n        keys = []\n        for idx in range(int(stripped_result)):\n            keys.append(\"{}{}\".format(metadata_key, idx))\n        metadata_string = get_workfile_metadata_string_for_keys(keys)\n\n    # Replace quotes plaholders with their values\n    metadata_string = (\n        metadata_string\n        .replace(\"{__sq__}\", \"'\")\n        .replace(\"{__dq__}\", \"\\\"\")\n    )\n    return metadata_string\n\n\ndef get_workfile_metadata(metadata_key, default=None):\n    \"\"\"Read and parse metadata for specific key from current project workfile.\n\n    Pipeline use function to store loaded and created instances within keys\n    stored in `SECTION_NAME_INSTANCES` and `SECTION_NAME_CONTAINERS`\n    constants.\n\n    Args:\n        metadata_key (str): Key defying which key should read. It is expected\n            value contain json serializable string.\n    \"\"\"\n    if default is None:\n        default = []\n\n    json_string = get_workfile_metadata_string(metadata_key)\n    if json_string:\n        try:\n            return json.loads(json_string)\n        except json.decoder.JSONDecodeError:\n            # TODO remove when backwards compatibility of storing metadata\n            # will be removed\n            print((\n                \"Fixed invalid metadata in workfile.\"\n                \" Not serializable string was: {}\"\n            ).format(json_string))\n            write_workfile_metadata(metadata_key, default)\n    return default\n\n\ndef write_workfile_metadata(metadata_key, value):\n    \"\"\"Write metadata for specific key into current project workfile.\n\n    George script has specific way how to work with quotes which should be\n    solved automatically with this function.\n\n    Args:\n        metadata_key (str): Key defying under which key value will be stored.\n        value (dict,list,str): Data to store they must be json serializable.\n    \"\"\"\n    if isinstance(value, (dict, list)):\n        value = json.dumps(value)\n\n    if not value:\n        value = \"\"\n\n    # Handle quotes in dumped json string\n    # - replace single and double quotes with placeholders\n    value = (\n        value\n        .replace(\"'\", \"{__sq__}\")\n        .replace(\"\\\"\", \"{__dq__}\")\n    )\n    chunks = split_metadata_string(value)\n    chunks_len = len(chunks)\n\n    write_template = \"tv_writeprojectstring \\\"{}\\\" \\\"{}\\\" \\\"{}\\\"\"\n    george_script_parts = []\n    # Add information about chunks length to metadata key itself\n    george_script_parts.append(\n        write_template.format(METADATA_SECTION, metadata_key, chunks_len)\n    )\n    # Add chunk values to indexed metadata keys\n    for idx, chunk_value in enumerate(chunks):\n        sub_key = \"{}{}\".format(metadata_key, idx)\n        george_script_parts.append(\n            write_template.format(METADATA_SECTION, sub_key, chunk_value)\n        )\n\n    george_script = \"\\n\".join(george_script_parts)\n\n    return execute_george_through_file(george_script)\n\n\ndef get_current_workfile_context():\n    \"\"\"Return context in which was workfile saved.\"\"\"\n    return get_workfile_metadata(SECTION_NAME_CONTEXT, {})\n\n\ndef save_current_workfile_context(context):\n    \"\"\"Save context which was used to create a workfile.\"\"\"\n    return write_workfile_metadata(SECTION_NAME_CONTEXT, context)\n\n\ndef list_instances():\n    \"\"\"List all created instances from current workfile.\"\"\"\n    return get_workfile_metadata(SECTION_NAME_INSTANCES)\n\n\ndef write_instances(data):\n    return write_workfile_metadata(SECTION_NAME_INSTANCES, data)\n\n\ndef get_containers():\n    output = get_workfile_metadata(SECTION_NAME_CONTAINERS)\n    if output:\n        for item in output:\n            if \"objectName\" not in item and \"members\" in item:\n                members = item[\"members\"]\n                if isinstance(members, list):\n                    members = \"|\".join([str(member) for member in members])\n                item[\"objectName\"] = members\n    return output\n\n\ndef set_context_settings(project_name, asset_doc):\n    \"\"\"Set workfile settings by asset document data.\n\n    Change fps, resolution and frame start/end.\n    \"\"\"\n\n    width_key = \"resolutionWidth\"\n    height_key = \"resolutionHeight\"\n\n    width = asset_doc[\"data\"].get(width_key)\n    height = asset_doc[\"data\"].get(height_key)\n    if width is None or height is None:\n        print(\"Resolution was not found!\")\n    else:\n        execute_george(\n            \"tv_resizepage {} {} 0\".format(width, height)\n        )\n\n    framerate = asset_doc[\"data\"].get(\"fps\")\n\n    if framerate is not None:\n        execute_george(\n            \"tv_framerate {} \\\"timestretch\\\"\".format(framerate)\n        )\n    else:\n        print(\"Framerate was not found!\")\n\n    frame_start = asset_doc[\"data\"].get(\"frameStart\")\n    frame_end = asset_doc[\"data\"].get(\"frameEnd\")\n\n    if frame_start is None or frame_end is None:\n        print(\"Frame range was not found!\")\n        return\n\n    handle_start = asset_doc[\"data\"].get(\"handleStart\")\n    handle_end = asset_doc[\"data\"].get(\"handleEnd\")\n\n    # Always start from 0 Mark In and set only Mark Out\n    mark_in = 0\n    mark_out = mark_in + (frame_end - frame_start) + handle_start + handle_end\n\n    execute_george(\"tv_markin {} set\".format(mark_in))\n    execute_george(\"tv_markout {} set\".format(mark_out))\n"
  },
  {
    "path": "openpype/hosts/tvpaint/api/plugin.py",
    "content": "import re\n\nfrom openpype.pipeline import LoaderPlugin\nfrom openpype.pipeline.create import (\n    CreatedInstance,\n    get_subset_name,\n    AutoCreator,\n    Creator,\n)\nfrom openpype.pipeline.create.creator_plugins import cache_and_get_instances\n\nfrom .lib import get_layers_data\n\n\nSHARED_DATA_KEY = \"openpype.tvpaint.instances\"\n\n\nclass TVPaintCreatorCommon:\n    @property\n    def subset_template_family_filter(self):\n        return self.family\n\n    def _cache_and_get_instances(self):\n        return cache_and_get_instances(\n            self, SHARED_DATA_KEY, self.host.list_instances\n        )\n\n    def _collect_create_instances(self):\n        instances_by_identifier = self._cache_and_get_instances()\n        for instance_data in instances_by_identifier[self.identifier]:\n            instance = CreatedInstance.from_existing(instance_data, self)\n            self._add_instance_to_context(instance)\n\n    def _update_create_instances(self, update_list):\n        if not update_list:\n            return\n\n        cur_instances = self.host.list_instances()\n        cur_instances_by_id = {}\n        for instance_data in cur_instances:\n            instance_id = instance_data.get(\"instance_id\")\n            if instance_id:\n                cur_instances_by_id[instance_id] = instance_data\n\n        for instance, changes in update_list:\n            instance_data = changes.new_value\n            cur_instance_data = cur_instances_by_id.get(instance.id)\n            if cur_instance_data is None:\n                cur_instances.append(instance_data)\n                continue\n            for key in set(cur_instance_data) - set(instance_data):\n                cur_instance_data.pop(key)\n            cur_instance_data.update(instance_data)\n        self.host.write_instances(cur_instances)\n\n    def _custom_get_subset_name(\n        self,\n        variant,\n        task_name,\n        asset_doc,\n        project_name,\n        host_name=None,\n        instance=None\n    ):\n        dynamic_data = self.get_dynamic_data(\n            variant, task_name, asset_doc, project_name, host_name, instance\n        )\n\n        return get_subset_name(\n            self.family,\n            variant,\n            task_name,\n            asset_doc,\n            project_name,\n            host_name,\n            dynamic_data=dynamic_data,\n            project_settings=self.project_settings,\n            family_filter=self.subset_template_family_filter\n        )\n\n\nclass TVPaintCreator(Creator, TVPaintCreatorCommon):\n    def collect_instances(self):\n        self._collect_create_instances()\n\n    def update_instances(self, update_list):\n        self._update_create_instances(update_list)\n\n    def remove_instances(self, instances):\n        ids_to_remove = {\n            instance.id\n            for instance in instances\n        }\n        cur_instances = self.host.list_instances()\n        changed = False\n        new_instances = []\n        for instance_data in cur_instances:\n            if instance_data.get(\"instance_id\") in ids_to_remove:\n                changed = True\n            else:\n                new_instances.append(instance_data)\n\n        if changed:\n            self.host.write_instances(new_instances)\n\n        for instance in instances:\n            self._remove_instance_from_context(instance)\n\n    def get_dynamic_data(self, *args, **kwargs):\n        # Change asset and name by current workfile context\n        create_context = self.create_context\n        asset_name = create_context.get_current_asset_name()\n        task_name = create_context.get_current_task_name()\n        output = {}\n        if asset_name:\n            output[\"asset\"] = asset_name\n            if task_name:\n                output[\"task\"] = task_name\n        return output\n\n    def get_subset_name(self, *args, **kwargs):\n        return self._custom_get_subset_name(*args, **kwargs)\n\n    def _store_new_instance(self, new_instance):\n        instances_data = self.host.list_instances()\n        instances_data.append(new_instance.data_to_store())\n        self.host.write_instances(instances_data)\n        self._add_instance_to_context(new_instance)\n\n\nclass TVPaintAutoCreator(AutoCreator, TVPaintCreatorCommon):\n    def collect_instances(self):\n        self._collect_create_instances()\n\n    def update_instances(self, update_list):\n        self._update_create_instances(update_list)\n\n    def get_subset_name(self, *args, **kwargs):\n        return self._custom_get_subset_name(*args, **kwargs)\n\n\nclass Loader(LoaderPlugin):\n    hosts = [\"tvpaint\"]\n\n    @staticmethod\n    def get_members_from_container(container):\n        if \"members\" not in container and \"objectName\" in container:\n            # Backwards compatibility\n            layer_ids_str = container.get(\"objectName\")\n            return [\n                int(layer_id) for layer_id in layer_ids_str.split(\"|\")\n            ]\n        return container[\"members\"]\n\n    def get_unique_layer_name(self, asset_name, name):\n        \"\"\"Layer name with counter as suffix.\n\n        Find higher 3 digit suffix from all layer names in scene matching regex\n        `{asset_name}_{name}_{suffix}`. Higher 3 digit suffix is used\n        as base for next number if scene does not contain layer matching regex\n        `0` is used ase base.\n\n        Args:\n            asset_name (str): Name of subset's parent asset document.\n            name (str): Name of loaded subset.\n\n        Returns:\n            (str): `{asset_name}_{name}_{higher suffix + 1}`\n        \"\"\"\n        layer_name_base = \"{}_{}\".format(asset_name, name)\n\n        counter_regex = re.compile(r\"_(\\d{3})$\")\n\n        higher_counter = 0\n        for layer in get_layers_data():\n            layer_name = layer[\"name\"]\n            if not layer_name.startswith(layer_name_base):\n                continue\n            number_subpart = layer_name[len(layer_name_base):]\n            groups = counter_regex.findall(number_subpart)\n            if len(groups) != 1:\n                continue\n\n            counter = int(groups[0])\n            if counter > higher_counter:\n                higher_counter = counter\n                continue\n\n        return \"{}_{:0>3d}\".format(layer_name_base, higher_counter + 1)\n"
  },
  {
    "path": "openpype/hosts/tvpaint/hooks/pre_launch_args.py",
    "content": "from openpype.lib import get_openpype_execute_args\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\n\n\nclass TvpaintPrelaunchHook(PreLaunchHook):\n    \"\"\"Launch arguments preparation.\n\n    Hook add python executable and script path to tvpaint implementation before\n    tvpaint executable and add last workfile path to launch arguments.\n\n    Existence of last workfile is checked. If workfile does not exists tries\n    to copy templated workfile from predefined path.\n    \"\"\"\n    app_groups = {\"tvpaint\"}\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        # Pop tvpaint executable\n        executable_path = self.launch_context.launch_args.pop(0)\n\n        # Pop rest of launch arguments - There should not be other arguments!\n        remainders = []\n        while self.launch_context.launch_args:\n            remainders.append(self.launch_context.launch_args.pop(0))\n\n        new_launch_args = get_openpype_execute_args(\n            \"run\", self.launch_script_path(), executable_path\n        )\n\n        # Append as whole list as these areguments should not be separated\n        self.launch_context.launch_args.append(new_launch_args)\n\n        if remainders:\n            self.log.warning((\n                \"There are unexpected launch arguments in TVPaint launch. {}\"\n            ).format(str(remainders)))\n            self.launch_context.launch_args.extend(remainders)\n\n    def launch_script_path(self):\n        from openpype.hosts.tvpaint import get_launch_script_path\n\n        return get_launch_script_path()\n"
  },
  {
    "path": "openpype/hosts/tvpaint/lib.py",
    "content": "import os\nimport shutil\nimport collections\nfrom PIL import Image, ImageDraw\n\n\ndef backwards_id_conversion(data_by_layer_id):\n    \"\"\"Convert layer ids to strings from integers.\"\"\"\n    for key in tuple(data_by_layer_id.keys()):\n        if not isinstance(key, str):\n            data_by_layer_id[str(key)] = data_by_layer_id.pop(key)\n\n\ndef get_frame_filename_template(frame_end, filename_prefix=None, ext=None):\n    \"\"\"Get file template with frame key for rendered files.\n\n    This is simple template contains `{frame}{ext}` for sequential outputs\n    and `single_file{ext}` for single file output. Output is rendered to\n    temporary folder so filename should not matter as integrator change\n    them.\n    \"\"\"\n    frame_padding = 4\n    frame_end_str_len = len(str(frame_end))\n    if frame_end_str_len > frame_padding:\n        frame_padding = frame_end_str_len\n\n    ext = ext or \".png\"\n    filename_prefix = filename_prefix or \"\"\n\n    return \"{}{{frame:0>{}}}{}\".format(filename_prefix, frame_padding, ext)\n\n\ndef get_layer_pos_filename_template(range_end, filename_prefix=None, ext=None):\n    filename_prefix = filename_prefix or \"\"\n    new_filename_prefix = filename_prefix + \"pos_{pos}.\"\n    return get_frame_filename_template(range_end, new_filename_prefix, ext)\n\n\ndef _calculate_pre_behavior_copy(\n    range_start, exposure_frames, pre_beh,\n    layer_frame_start, layer_frame_end,\n    output_idx_by_frame_idx\n):\n    \"\"\"Calculate frames before first exposure frame based on pre behavior.\n\n    Function may skip whole processing if first exposure frame is before\n    layer's first frame. In that case pre behavior does not make sense.\n\n    Args:\n        range_start(int): First frame of range which should be rendered.\n        exposure_frames(list): List of all exposure frames on layer.\n        pre_beh(str): Pre behavior of layer (enum of 4 strings).\n        layer_frame_start(int): First frame of layer.\n        layer_frame_end(int): Last frame of layer.\n        output_idx_by_frame_idx(dict): References to already prepared frames\n            and where result will be stored.\n    \"\"\"\n    # Check if last layer frame is after range end\n    if layer_frame_start < range_start:\n        return\n\n    first_exposure_frame = min(exposure_frames)\n    # Skip if last exposure frame is after range end\n    if first_exposure_frame < range_start:\n        return\n\n    # Calculate frame count of layer\n    frame_count = layer_frame_end - layer_frame_start + 1\n\n    if pre_beh == \"none\":\n        # Just fill all frames from last exposure frame to range end with None\n        for frame_idx in range(range_start, layer_frame_start):\n            output_idx_by_frame_idx[frame_idx] = None\n\n    elif pre_beh == \"hold\":\n        # Keep first frame for whole time\n        for frame_idx in range(range_start, layer_frame_start):\n            output_idx_by_frame_idx[frame_idx] = first_exposure_frame\n\n    elif pre_beh == \"repeat\":\n        # Loop backwards from last frame of layer\n        for frame_idx in reversed(range(range_start, layer_frame_start)):\n            eq_frame_idx_offset = (\n                (layer_frame_end - frame_idx) % frame_count\n            )\n            eq_frame_idx = layer_frame_start + (\n                layer_frame_end - eq_frame_idx_offset\n            )\n            output_idx_by_frame_idx[frame_idx] = eq_frame_idx\n\n    elif pre_beh == \"pingpong\":\n        half_seq_len = frame_count - 1\n        seq_len = half_seq_len * 2\n        for frame_idx in reversed(range(range_start, layer_frame_start)):\n            eq_frame_idx_offset = (layer_frame_start - frame_idx) % seq_len\n            if eq_frame_idx_offset > half_seq_len:\n                eq_frame_idx_offset = (seq_len - eq_frame_idx_offset)\n            eq_frame_idx = layer_frame_start + eq_frame_idx_offset\n            output_idx_by_frame_idx[frame_idx] = eq_frame_idx\n\n\ndef _calculate_post_behavior_copy(\n    range_end, exposure_frames, post_beh,\n    layer_frame_start, layer_frame_end,\n    output_idx_by_frame_idx\n):\n    \"\"\"Calculate frames after last frame of layer based on post behavior.\n\n    Function may skip whole processing if last layer frame is after range_end.\n    In that case post behavior does not make sense.\n\n    Args:\n        range_end(int): Last frame of range which should be rendered.\n        exposure_frames(list): List of all exposure frames on layer.\n        post_beh(str): Post behavior of layer (enum of 4 strings).\n        layer_frame_start(int): First frame of layer.\n        layer_frame_end(int): Last frame of layer.\n        output_idx_by_frame_idx(dict): References to already prepared frames\n            and where result will be stored.\n    \"\"\"\n    # Check if last layer frame is after range end\n    if layer_frame_end >= range_end:\n        return\n\n    last_exposure_frame = max(exposure_frames)\n    # Skip if last exposure frame is after range end\n    #   - this is probably irrelevant with layer frame end check?\n    if last_exposure_frame >= range_end:\n        return\n\n    # Calculate frame count of layer\n    frame_count = layer_frame_end - layer_frame_start + 1\n\n    if post_beh == \"none\":\n        # Just fill all frames from last exposure frame to range end with None\n        for frame_idx in range(layer_frame_end + 1, range_end + 1):\n            output_idx_by_frame_idx[frame_idx] = None\n\n    elif post_beh == \"hold\":\n        # Keep last exposure frame to the end\n        for frame_idx in range(layer_frame_end + 1, range_end + 1):\n            output_idx_by_frame_idx[frame_idx] = last_exposure_frame\n\n    elif post_beh == \"repeat\":\n        # Loop backwards from last frame of layer\n        for frame_idx in range(layer_frame_end + 1, range_end + 1):\n            eq_frame_idx = layer_frame_start + (frame_idx % frame_count)\n            output_idx_by_frame_idx[frame_idx] = eq_frame_idx\n\n    elif post_beh == \"pingpong\":\n        half_seq_len = frame_count - 1\n        seq_len = half_seq_len * 2\n        for frame_idx in range(layer_frame_end + 1, range_end + 1):\n            eq_frame_idx_offset = (frame_idx - layer_frame_end) % seq_len\n            if eq_frame_idx_offset > half_seq_len:\n                eq_frame_idx_offset = seq_len - eq_frame_idx_offset\n            eq_frame_idx = layer_frame_end - eq_frame_idx_offset\n            output_idx_by_frame_idx[frame_idx] = eq_frame_idx\n\n\ndef _calculate_in_range_frames(\n    range_start, range_end,\n    exposure_frames, layer_frame_end,\n    output_idx_by_frame_idx\n):\n    \"\"\"Calculate frame references in defined range.\n\n    Function may skip whole processing if last layer frame is after range_end.\n    In that case post behavior does not make sense.\n\n    Args:\n        range_start(int): First frame of range which should be rendered.\n        range_end(int): Last frame of range which should be rendered.\n        exposure_frames(list): List of all exposure frames on layer.\n        layer_frame_end(int): Last frame of layer.\n        output_idx_by_frame_idx(dict): References to already prepared frames\n            and where result will be stored.\n    \"\"\"\n    # Calculate in range frames\n    in_range_frames = []\n    for frame_idx in exposure_frames:\n        if range_start <= frame_idx <= range_end:\n            output_idx_by_frame_idx[frame_idx] = frame_idx\n            in_range_frames.append(frame_idx)\n\n    if in_range_frames:\n        first_in_range_frame = min(in_range_frames)\n        # Calculate frames from first exposure frames to range end or last\n        #   frame of layer (post behavior should be calculated since that time)\n        previous_exposure = first_in_range_frame\n        for frame_idx in range(first_in_range_frame, range_end + 1):\n            if frame_idx > layer_frame_end:\n                break\n\n            if frame_idx in exposure_frames:\n                previous_exposure = frame_idx\n            else:\n                output_idx_by_frame_idx[frame_idx] = previous_exposure\n\n    # There can be frames before first exposure frame in range\n    # First check if we don't alreade have first range frame filled\n    if range_start in output_idx_by_frame_idx:\n        return\n\n    first_exposure_frame = max(exposure_frames)\n    last_exposure_frame = max(exposure_frames)\n    # Check if is first exposure frame smaller than defined range\n    #   if not then skip\n    if first_exposure_frame >= range_start:\n        return\n\n    # Check is if last exposure frame is also before range start\n    #   in that case we can't use fill frames before out range\n    if last_exposure_frame < range_start:\n        return\n\n    closest_exposure_frame = first_exposure_frame\n    for frame_idx in exposure_frames:\n        if frame_idx >= range_start:\n            break\n        if frame_idx > closest_exposure_frame:\n            closest_exposure_frame = frame_idx\n\n    output_idx_by_frame_idx[closest_exposure_frame] = closest_exposure_frame\n    for frame_idx in range(range_start, range_end + 1):\n        if frame_idx in output_idx_by_frame_idx:\n            break\n        output_idx_by_frame_idx[frame_idx] = closest_exposure_frame\n\n\ndef _cleanup_frame_references(output_idx_by_frame_idx):\n    \"\"\"Cleanup frame references to frame reference.\n\n    Cleanup not direct references to rendered frame.\n    ```\n    // Example input\n    {\n        1: 1,\n        2: 1,\n        3: 2\n    }\n    // Result\n    {\n        1: 1,\n        2: 1,\n        3: 1 // Changed reference to final rendered frame\n    }\n    ```\n    Result is dictionary where keys leads to frame that should be rendered.\n    \"\"\"\n    for frame_idx in tuple(output_idx_by_frame_idx.keys()):\n        reference_idx = output_idx_by_frame_idx[frame_idx]\n        # Skip transparent frames\n        if reference_idx is None or reference_idx == frame_idx:\n            continue\n\n        real_reference_idx = reference_idx\n        _tmp_reference_idx = reference_idx\n        while True:\n            _temp = output_idx_by_frame_idx[_tmp_reference_idx]\n            if _temp == _tmp_reference_idx:\n                real_reference_idx = _tmp_reference_idx\n                break\n            _tmp_reference_idx = _temp\n\n        if real_reference_idx != reference_idx:\n            output_idx_by_frame_idx[frame_idx] = real_reference_idx\n\n\ndef _cleanup_out_range_frames(output_idx_by_frame_idx, range_start, range_end):\n    \"\"\"Cleanup frame references to frames out of passed range.\n\n    First available frame in range is used\n    ```\n    // Example input. Range 2-3\n    {\n        1: 1,\n        2: 1,\n        3: 1\n    }\n    // Result\n    {\n        2: 2, // Redirect to self as is first that reference out range\n        3: 2 // Redirect to first redirected frame\n    }\n    ```\n    Result is dictionary where keys leads to frame that should be rendered.\n    \"\"\"\n    in_range_frames_by_out_frames = collections.defaultdict(set)\n    out_range_frames = set()\n    for frame_idx in tuple(output_idx_by_frame_idx.keys()):\n        # Skip frames that are already out of range\n        if frame_idx < range_start or frame_idx > range_end:\n            out_range_frames.add(frame_idx)\n            continue\n\n        reference_idx = output_idx_by_frame_idx[frame_idx]\n        # Skip transparent frames\n        if reference_idx is None:\n            continue\n\n        # Skip references in range\n        if reference_idx < range_start or reference_idx > range_end:\n            in_range_frames_by_out_frames[reference_idx].add(frame_idx)\n\n    for reference_idx in tuple(in_range_frames_by_out_frames.keys()):\n        frame_indexes = in_range_frames_by_out_frames.pop(reference_idx)\n        new_reference = None\n        for frame_idx in frame_indexes:\n            if new_reference is None:\n                new_reference = frame_idx\n            output_idx_by_frame_idx[frame_idx] = new_reference\n\n    # Finally remove out of range frames\n    for frame_idx in out_range_frames:\n        output_idx_by_frame_idx.pop(frame_idx)\n\n\ndef calculate_layer_frame_references(\n    range_start, range_end,\n    layer_frame_start,\n    layer_frame_end,\n    exposure_frames,\n    pre_beh, post_beh\n):\n    \"\"\"Calculate frame references for one layer based on it's data.\n\n    Output is dictionary where key is frame index referencing to rendered frame\n    index. If frame index should be rendered then is referencing to self.\n\n    ```\n    // Example output\n    {\n        1: 1, // Reference to self - will be rendered\n        2: 1, // Reference to frame 1 - will be copied\n        3: 1, // Reference to frame 1 - will be copied\n        4: 4, // Reference to self - will be rendered\n        ...\n        20: 4 // Reference to frame 4 - will be copied\n        21: None // Has reference to None - transparent image\n    }\n    ```\n\n    Args:\n        range_start(int): First frame of range which should be rendered.\n        range_end(int): Last frame of range which should be rendered.\n        layer_frame_start(int)L First frame of layer.\n        layer_frame_end(int): Last frame of layer.\n        exposure_frames(list): List of all exposure frames on layer.\n        pre_beh(str): Pre behavior of layer (enum of 4 strings).\n        post_beh(str): Post behavior of layer (enum of 4 strings).\n    \"\"\"\n    # Output variable\n    output_idx_by_frame_idx = {}\n    # Skip if layer does not have any exposure frames\n    if not exposure_frames:\n        return output_idx_by_frame_idx\n\n    # First calculate in range frames\n    _calculate_in_range_frames(\n        range_start, range_end,\n        exposure_frames, layer_frame_end,\n        output_idx_by_frame_idx\n    )\n    # Calculate frames by pre behavior of layer\n    _calculate_pre_behavior_copy(\n        range_start, exposure_frames, pre_beh,\n        layer_frame_start, layer_frame_end,\n        output_idx_by_frame_idx\n    )\n    # Calculate frames by post behavior of layer\n    _calculate_post_behavior_copy(\n        range_end, exposure_frames, post_beh,\n        layer_frame_start, layer_frame_end,\n        output_idx_by_frame_idx\n    )\n    # Cleanup of referenced frames\n    _cleanup_frame_references(output_idx_by_frame_idx)\n\n    # Remove frames out of range\n    _cleanup_out_range_frames(output_idx_by_frame_idx, range_start, range_end)\n\n    return output_idx_by_frame_idx\n\n\ndef calculate_layers_extraction_data(\n    layers_data,\n    exposure_frames_by_layer_id,\n    behavior_by_layer_id,\n    range_start,\n    range_end,\n    skip_not_visible=True,\n    filename_prefix=None,\n    ext=None\n):\n    \"\"\"Calculate extraction data for passed layers data.\n\n    ```\n    {\n        <layer_id>: {\n            \"frame_references\": {...},\n            \"filenames_by_frame_index\": {...}\n        },\n        ...\n    }\n    ```\n\n    Frame references contains frame index reference to rendered frame index.\n\n    Filename by frame index represents filename under which should be frame\n    stored. Directory is not handled here because each usage may need different\n    approach.\n\n    Args:\n        layers_data(list): Layers data loaded from TVPaint.\n        exposure_frames_by_layer_id(dict): Exposure frames of layers stored by\n            layer id.\n        behavior_by_layer_id(dict): Pre and Post behavior of layers stored by\n            layer id.\n        range_start(int): First frame of rendered range.\n        range_end(int): Last frame of rendered range.\n        skip_not_visible(bool): Skip calculations for hidden layers (Skipped\n            by default).\n        filename_prefix(str): Prefix before filename.\n        ext(str): Extension which filenames will have ('.png' is default).\n\n    Returns:\n        dict: Prepared data for rendering by layer position.\n    \"\"\"\n    # Make sure layer ids are strings\n    #   backwards compatibility when layer ids were integers\n    backwards_id_conversion(exposure_frames_by_layer_id)\n    backwards_id_conversion(behavior_by_layer_id)\n\n    layer_template = get_layer_pos_filename_template(\n        range_end, filename_prefix, ext\n    )\n    output = {}\n    for layer_data in layers_data:\n        if skip_not_visible and not layer_data[\"visible\"]:\n            continue\n\n        orig_layer_id = layer_data[\"layer_id\"]\n        layer_id = str(orig_layer_id)\n\n        # Skip if does not have any exposure frames (empty layer)\n        exposure_frames = exposure_frames_by_layer_id[layer_id]\n        if not exposure_frames:\n            continue\n\n        layer_position = layer_data[\"position\"]\n        layer_frame_start = layer_data[\"frame_start\"]\n        layer_frame_end = layer_data[\"frame_end\"]\n\n        layer_behavior = behavior_by_layer_id[layer_id]\n\n        pre_behavior = layer_behavior[\"pre\"]\n        post_behavior = layer_behavior[\"post\"]\n\n        frame_references = calculate_layer_frame_references(\n            range_start, range_end,\n            layer_frame_start,\n            layer_frame_end,\n            exposure_frames,\n            pre_behavior, post_behavior\n        )\n        # All values in 'frame_references' reference to a frame that must be\n        #   rendered out\n        frames_to_render = set(frame_references.values())\n        # Remove 'None' reference (transparent image)\n        if None in frames_to_render:\n            frames_to_render.remove(None)\n\n        # Skip layer if has nothing to render\n        if not frames_to_render:\n            continue\n\n        # All filenames that should be as output (not final output)\n        filename_frames = (\n            set(range(range_start, range_end + 1))\n            | frames_to_render\n        )\n        filenames_by_frame_index = {}\n        for frame_idx in filename_frames:\n            filenames_by_frame_index[frame_idx] = layer_template.format(\n                pos=layer_position,\n                frame=frame_idx\n            )\n\n        # Store objects under the layer id\n        output[orig_layer_id] = {\n            \"frame_references\": frame_references,\n            \"filenames_by_frame_index\": filenames_by_frame_index\n        }\n    return output\n\n\ndef create_transparent_image_from_source(src_filepath, dst_filepath):\n    \"\"\"Create transparent image of same type and size as source image.\"\"\"\n    img_obj = Image.open(src_filepath)\n    painter = ImageDraw.Draw(img_obj)\n    painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0))\n    img_obj.save(dst_filepath)\n\n\ndef fill_reference_frames(frame_references, filepaths_by_frame):\n    # Store path to first transparent image if there is any\n    for frame_idx, ref_idx in frame_references.items():\n        # Frame referencing to self should be rendered and used as source\n        #   and reference indexes with None can't be filled\n        if ref_idx is None or frame_idx == ref_idx:\n            continue\n\n        # Get destination filepath\n        src_filepath = filepaths_by_frame[ref_idx]\n        dst_filepath = filepaths_by_frame[frame_idx]\n\n        if hasattr(os, \"link\"):\n            os.link(src_filepath, dst_filepath)\n        else:\n            shutil.copy(src_filepath, dst_filepath)\n\n\ndef copy_render_file(src_path, dst_path):\n    \"\"\"Create copy file of an image.\"\"\"\n    if hasattr(os, \"link\"):\n        os.link(src_path, dst_path)\n    else:\n        shutil.copy(src_path, dst_path)\n\n\ndef cleanup_rendered_layers(filepaths_by_layer_id):\n    \"\"\"Delete all files for each individual layer files after compositing.\"\"\"\n    # Collect all filepaths from data\n    all_filepaths = []\n    for filepaths_by_frame in filepaths_by_layer_id.values():\n        all_filepaths.extend(filepaths_by_frame.values())\n\n    # Loop over loop\n    for filepath in set(all_filepaths):\n        if filepath is not None and os.path.exists(filepath):\n            os.remove(filepath)\n\n\ndef composite_rendered_layers(\n    layers_data, filepaths_by_layer_id,\n    range_start, range_end,\n    dst_filepaths_by_frame, cleanup=True\n):\n    \"\"\"Composite multiple rendered layers by their position.\n\n    Result is single frame sequence with transparency matching content\n    created in TVPaint. Missing source filepaths are replaced with transparent\n    images but at least one image must be rendered and exist.\n\n    Function can be used even if single layer was created to fill transparent\n    filepaths.\n\n    Args:\n        layers_data(list): Layers data loaded from TVPaint.\n        filepaths_by_layer_id(dict): Rendered filepaths stored by frame index\n            per layer id. Used as source for compositing.\n        range_start(int): First frame of rendered range.\n        range_end(int): Last frame of rendered range.\n        dst_filepaths_by_frame(dict): Output filepaths by frame where final\n            image after compositing will be stored. Path must not clash with\n            source filepaths.\n        cleanup(bool): Remove all source filepaths when done with compositing.\n    \"\"\"\n    # Prepare layers by their position\n    #   - position tells in which order will compositing happen\n    layer_ids_by_position = {}\n    for layer in layers_data:\n        layer_position = layer[\"position\"]\n        layer_ids_by_position[layer_position] = layer[\"layer_id\"]\n\n    # Sort layer positions\n    sorted_positions = tuple(reversed(sorted(layer_ids_by_position.keys())))\n    # Prepare variable where filepaths without any rendered content\n    #   - transparent will be created\n    transparent_filepaths = set()\n    # Store first final filepath\n    first_dst_filepath = None\n    for frame_idx in range(range_start, range_end + 1):\n        dst_filepath = dst_filepaths_by_frame[frame_idx]\n        src_filepaths = []\n        for layer_position in sorted_positions:\n            layer_id = layer_ids_by_position[layer_position]\n            filepaths_by_frame = filepaths_by_layer_id[layer_id]\n            src_filepath = filepaths_by_frame.get(frame_idx)\n            if src_filepath is not None:\n                src_filepaths.append(src_filepath)\n\n        if not src_filepaths:\n            transparent_filepaths.add(dst_filepath)\n            continue\n\n        # Store first destination filepath to be used for transparent images\n        if first_dst_filepath is None:\n            first_dst_filepath = dst_filepath\n\n        if len(src_filepaths) == 1:\n            src_filepath = src_filepaths[0]\n            if cleanup:\n                os.rename(src_filepath, dst_filepath)\n            else:\n                copy_render_file(src_filepath, dst_filepath)\n\n        else:\n            composite_images(src_filepaths, dst_filepath)\n\n    # Store first transparent filepath to be able copy it\n    transparent_filepath = None\n    for dst_filepath in transparent_filepaths:\n        if transparent_filepath is None:\n            create_transparent_image_from_source(\n                first_dst_filepath, dst_filepath\n            )\n            transparent_filepath = dst_filepath\n        else:\n            copy_render_file(transparent_filepath, dst_filepath)\n\n    # Remove all files that were used as source for compositing\n    if cleanup:\n        cleanup_rendered_layers(filepaths_by_layer_id)\n\n\ndef composite_images(input_image_paths, output_filepath):\n    \"\"\"Composite images in order from passed list.\n\n    Raises:\n        ValueError: When entered list is empty.\n    \"\"\"\n    if not input_image_paths:\n        raise ValueError(\"Nothing to composite.\")\n\n    img_obj = None\n    for image_filepath in input_image_paths:\n        _img_obj = Image.open(image_filepath)\n        if img_obj is None:\n            img_obj = _img_obj\n        else:\n            img_obj.alpha_composite(_img_obj)\n    img_obj.save(output_filepath)\n\n\ndef rename_filepaths_by_frame_start(\n    filepaths_by_frame, range_start, range_end, new_frame_start\n):\n    \"\"\"Change frames in filenames of finished images to new frame start.\"\"\"\n\n    # Calculate frame end\n    new_frame_end = range_end + (new_frame_start - range_start)\n    # Create filename template\n    filename_template = get_frame_filename_template(\n        max(range_end, new_frame_end)\n    )\n\n    # Use different ranges based on Mark In and output Frame Start values\n    # - this is to make sure that filename renaming won't affect files that\n    #   are not renamed yet\n    if range_start < new_frame_start:\n        source_range = range(range_end, range_start - 1, -1)\n        output_range = range(new_frame_end, new_frame_start - 1, -1)\n    else:\n        # This is less possible situation as frame start will be in most\n        #   cases higher than Mark In.\n        source_range = range(range_start, range_end + 1)\n        output_range = range(new_frame_start, new_frame_end + 1)\n\n    # Skip if source first frame is same as destination first frame\n    new_dst_filepaths = {}\n    for src_frame, dst_frame in zip(source_range, output_range):\n        src_filepath = os.path.normpath(filepaths_by_frame[src_frame])\n        dirpath, src_filename = os.path.split(src_filepath)\n        dst_filename = filename_template.format(frame=dst_frame)\n        dst_filepath = os.path.join(dirpath, dst_filename)\n\n        if src_filename != dst_filename:\n            os.rename(src_filepath, dst_filepath)\n\n        new_dst_filepaths[dst_frame] = dst_filepath\n\n    return new_dst_filepaths\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/create/convert_legacy.py",
    "content": "import collections\n\nfrom openpype.pipeline.create.creator_plugins import (\n    SubsetConvertorPlugin,\n    cache_and_get_instances,\n)\nfrom openpype.hosts.tvpaint.api.plugin import SHARED_DATA_KEY\nfrom openpype.hosts.tvpaint.api.lib import get_groups_data\n\n\nclass TVPaintLegacyConverted(SubsetConvertorPlugin):\n    \"\"\"Conversion of legacy instances in scene to new creators.\n\n    This convertor handles only instances created by core creators.\n\n    All instances that would be created using auto-creators are removed as at\n    the moment of finding them would there already be existing instances.\n    \"\"\"\n\n    identifier = \"tvpaint.legacy.converter\"\n\n    def find_instances(self):\n        instances_by_identifier = cache_and_get_instances(\n            self, SHARED_DATA_KEY, self.host.list_instances\n        )\n        if instances_by_identifier[None]:\n            self.add_convertor_item(\"Convert legacy instances\")\n\n    def convert(self):\n        current_instances = self.host.list_instances()\n        to_convert = collections.defaultdict(list)\n        converted = False\n        for instance in current_instances:\n            if instance.get(\"creator_identifier\") is not None:\n                continue\n            converted = True\n\n            family = instance.get(\"family\")\n            if family in (\n                \"renderLayer\",\n                \"renderPass\",\n                \"renderScene\",\n                \"review\",\n                \"workfile\",\n            ):\n                to_convert[family].append(instance)\n            else:\n                instance[\"keep\"] = False\n\n        # Skip if nothing was changed\n        if not converted:\n            self.remove_convertor_item()\n            return\n\n        self._convert_render_layers(\n            to_convert[\"renderLayer\"], current_instances)\n        self._convert_render_passes(\n            to_convert[\"renderPass\"], current_instances)\n        self._convert_render_scenes(\n            to_convert[\"renderScene\"], current_instances)\n        self._convert_workfiles(\n            to_convert[\"workfile\"], current_instances)\n        self._convert_reviews(\n            to_convert[\"review\"], current_instances)\n\n        new_instances = [\n            instance\n            for instance in current_instances\n            if instance.get(\"keep\") is not False\n        ]\n        self.host.write_instances(new_instances)\n        # remove legacy item if all is fine\n        self.remove_convertor_item()\n\n    def _convert_render_layers(self, render_layers, current_instances):\n        if not render_layers:\n            return\n\n        # Look for possible existing render layers in scene\n        render_layers_by_group_id = {}\n        for instance in current_instances:\n            if instance.get(\"creator_identifier\") == \"render.layer\":\n                group_id = instance[\"creator_identifier\"][\"group_id\"]\n                render_layers_by_group_id[group_id] = instance\n\n        groups_by_id = {\n            group[\"group_id\"]: group\n            for group in get_groups_data()\n        }\n        for render_layer in render_layers:\n            group_id = render_layer.pop(\"group_id\")\n            # Just remove legacy instance if group is already occupied\n            if group_id in render_layers_by_group_id:\n                render_layer[\"keep\"] = False\n                continue\n            # Add identifier\n            render_layer[\"creator_identifier\"] = \"render.layer\"\n            # Change 'uuid' to 'instance_id'\n            render_layer[\"instance_id\"] = render_layer.pop(\"uuid\")\n            # Fill creator attributes\n            render_layer[\"creator_attributes\"] = {\n                \"group_id\": group_id\n            }\n            render_layer[\"family\"] = \"render\"\n            group = groups_by_id[group_id]\n            # Use group name for variant\n            group[\"variant\"] = group[\"name\"]\n\n    def _convert_render_passes(self, render_passes, current_instances):\n        if not render_passes:\n            return\n\n        # Render passes must have available render layers so we look for render\n        #   layers first\n        # - '_convert_render_layers' must be called before this method\n        render_layers_by_group_id = {}\n        for instance in current_instances:\n            if instance.get(\"creator_identifier\") == \"render.layer\":\n                group_id = instance[\"creator_attributes\"][\"group_id\"]\n                render_layers_by_group_id[group_id] = instance\n\n        for render_pass in render_passes:\n            group_id = render_pass.pop(\"group_id\")\n            render_layer = render_layers_by_group_id.get(group_id)\n            if not render_layer:\n                render_pass[\"keep\"] = False\n                continue\n\n            render_pass[\"creator_identifier\"] = \"render.pass\"\n            render_pass[\"instance_id\"] = render_pass.pop(\"uuid\")\n            render_pass[\"family\"] = \"render\"\n\n            render_pass[\"creator_attributes\"] = {\n                \"render_layer_instance_id\": render_layer[\"instance_id\"]\n            }\n            render_pass[\"variant\"] = render_pass.pop(\"pass\")\n            render_pass.pop(\"renderlayer\")\n\n    # Rest of instances are just marked for deletion\n    def _convert_render_scenes(self, render_scenes, current_instances):\n        for render_scene in render_scenes:\n            render_scene[\"keep\"] = False\n\n    def _convert_workfiles(self, workfiles, current_instances):\n        for render_scene in workfiles:\n            render_scene[\"keep\"] = False\n\n    def _convert_reviews(self, reviews, current_instances):\n        for render_scene in reviews:\n            render_scene[\"keep\"] = False\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/create/create_render.py",
    "content": "\"\"\"Render Layer and Passes creators.\n\nRender layer is main part which is represented by group in TVPaint. All TVPaint\nlayers marked with that group color are part of the render layer. To be more\nspecific about some parts of layer it is possible to create sub-sets of layer\nwhich are named passes. Render pass consist of layers in same color group as\nrender layer but define more specific part.\n\nFor example render layer could be 'Bob' which consist of 5 TVPaint layers.\n- Bob has 'head' which consist of 2 TVPaint layers -> Render pass 'head'\n- Bob has 'body' which consist of 1 TVPaint layer -> Render pass 'body'\n- Bob has 'arm' which consist of 1 TVPaint layer -> Render pass 'arm'\n- Last layer does not belong to render pass at all\n\nBob will be rendered as 'beauty' of bob (all visible layers in group).\nHis head will be rendered too but without any other parts. The same for body\nand arm.\n\nWhat is this good for? Compositing has more power how the renders are used.\nCan do transforms on each render pass without need to modify a re-render them\nusing TVPaint.\n\nThe workflow may hit issues when there are used other blending modes than\ndefault 'color' blend more. In that case it is not recommended to use this\nworkflow at all as other blend modes may affect all layers in clip which can't\nbe done.\n\nThere is special case for simple publishing of scene which is called\n'render.scene'. That will use all visible layers and render them as one big\nsequence.\n\nTodos:\n    Add option to extract marked layers and passes as json output format for\n        AfterEffects.\n\"\"\"\n\nimport collections\nfrom typing import Any, Optional, Union\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import get_asset_by_name, get_asset_name_identifier\nfrom openpype.lib import (\n    prepare_template_data,\n    AbstractAttrDef,\n    UILabelDef,\n    UISeparatorDef,\n    EnumDef,\n    TextDef,\n    BoolDef,\n)\nfrom openpype.pipeline.create import (\n    CreatedInstance,\n    CreatorError,\n)\nfrom openpype.hosts.tvpaint.api.plugin import (\n    TVPaintCreator,\n    TVPaintAutoCreator,\n)\nfrom openpype.hosts.tvpaint.api.lib import (\n    get_layers_data,\n    get_groups_data,\n    execute_george_through_file,\n)\n\nRENDER_LAYER_DETAILED_DESCRIPTIONS = (\n    \"\"\"Render Layer is \"a group of TVPaint layers\"\n\nBe aware Render Layer <b>is not</b> TVPaint layer.\n\nAll TVPaint layers in the scene with the color group id are rendered in the\nbeauty pass. To create sub passes use Render Pass creator which is\ndependent on existence of render layer instance.\n\nThe group can represent an asset (tree) or different part of scene that consist\nof one or more TVPaint layers that can be used as single item during\ncompositing (for example).\n\nIn some cases may be needed to have sub parts of the layer. For example 'Bob'\ncould be Render Layer which has 'Arm', 'Head' and 'Body' as Render Passes.\n\"\"\"\n)\n\n\nRENDER_PASS_DETAILED_DESCRIPTIONS = (\n    \"\"\"Render Pass is sub part of Render Layer.\n\nRender Pass can consist of one or more TVPaint layers. Render Pass must\nbelong to a Render Layer. Marked TVPaint layers will change it's group color\nto match group color of Render Layer.\n\"\"\"\n)\n\n\nAUTODETECT_RENDER_DETAILED_DESCRIPTION = (\n    \"\"\"Semi-automated Render Layer and Render Pass creation.\n\nBased on information in TVPaint scene will be created Render Layers and Render\nPasses. All color groups used in scene will be used for Render Layer creation.\nName of the group is used as a variant.\n\nAll TVPaint layers under the color group will be created as Render Pass where\nlayer name is used as variant.\n\nThe plugin will use all used color groups and layers, or can skip those that\nare not visible.\n\nThere is option to auto-rename color groups before Render Layer creation. That\nis based on settings template where is filled index of used group from bottom\nto top.\n\"\"\"\n)\n\nclass CreateRenderlayer(TVPaintCreator):\n    \"\"\"Mark layer group as Render layer instance.\n\n    All TVPaint layers in the scene with the color group id are rendered in the\n    beauty pass. To create sub passes use Render Layer creator which is\n    dependent on existence of render layer instance.\n    \"\"\"\n\n    label = \"Render Layer\"\n    family = \"render\"\n    subset_template_family_filter = \"renderLayer\"\n    identifier = \"render.layer\"\n    icon = \"fa5.images\"\n\n    # George script to change color group\n    rename_script_template = (\n        \"tv_layercolor \\\"setcolor\\\"\"\n        \" {clip_id} {group_id} {r} {g} {b} \\\"{name}\\\"\"\n    )\n    # Order to be executed before Render Pass creator\n    order = 90\n    description = \"Mark TVPaint color group as one Render Layer.\"\n    detailed_description = RENDER_LAYER_DETAILED_DESCRIPTIONS\n\n    # Settings\n    # - Default render pass name for beauty\n    default_pass_name = \"beauty\"\n    # - Mark by default instance for review\n    mark_for_review = True\n\n    def apply_settings(self, project_settings):\n        plugin_settings = (\n            project_settings[\"tvpaint\"][\"create\"][\"create_render_layer\"]\n        )\n        self.default_variant = plugin_settings[\"default_variant\"]\n        self.default_variants = plugin_settings[\"default_variants\"]\n        self.default_pass_name = plugin_settings[\"default_pass_name\"]\n        self.mark_for_review = plugin_settings[\"mark_for_review\"]\n\n    def get_dynamic_data(\n        self, variant, task_name, asset_doc, project_name, host_name, instance\n    ):\n        dynamic_data = super().get_dynamic_data(\n            variant, task_name, asset_doc, project_name, host_name, instance\n        )\n        dynamic_data[\"renderpass\"] = self.default_pass_name\n        dynamic_data[\"renderlayer\"] = variant\n        return dynamic_data\n\n    def _get_selected_group_ids(self):\n        return {\n            layer[\"group_id\"]\n            for layer in get_layers_data()\n            if layer[\"selected\"]\n        }\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        self.log.debug(\"Query data from workfile.\")\n\n        group_name = instance_data[\"variant\"]\n        group_id = pre_create_data.get(\"group_id\")\n        # This creator should run only on one group\n        if group_id is None or group_id == -1:\n            selected_groups = self._get_selected_group_ids()\n            selected_groups.discard(0)\n            if len(selected_groups) > 1:\n                raise CreatorError(\"You have selected more than one group\")\n\n            if len(selected_groups) == 0:\n                raise CreatorError(\"You don't have selected any group\")\n            group_id = tuple(selected_groups)[0]\n\n        self.log.debug(\"Querying groups data from workfile.\")\n        groups_data = get_groups_data()\n        group_item = None\n        for group_data in groups_data:\n            if group_data[\"group_id\"] == group_id:\n                group_item = group_data\n\n        for instance in self.create_context.instances:\n            if (\n                instance.creator_identifier == self.identifier\n                and instance[\"creator_attributes\"][\"group_id\"] == group_id\n            ):\n                raise CreatorError((\n                    f\"Group \\\"{group_item.get('name')}\\\" is already used\"\n                    f\" by another render layer \\\"{instance['subset']}\\\"\"\n                ))\n\n        self.log.debug(f\"Selected group id is \\\"{group_id}\\\".\")\n        if \"creator_attributes\" not in instance_data:\n            instance_data[\"creator_attributes\"] = {}\n        creator_attributes = instance_data[\"creator_attributes\"]\n        mark_for_review = pre_create_data.get(\"mark_for_review\")\n        if mark_for_review is None:\n            mark_for_review = self.mark_for_review\n        creator_attributes[\"group_id\"] = group_id\n        creator_attributes[\"mark_for_review\"] = mark_for_review\n\n        self.log.info(f\"Subset name is {subset_name}\")\n        new_instance = CreatedInstance(\n            self.family,\n            subset_name,\n            instance_data,\n            self\n        )\n        self._store_new_instance(new_instance)\n\n        if not group_id or group_item[\"name\"] == group_name:\n            return new_instance\n\n        self.log.debug(\"Changing name of the group.\")\n        # Rename TVPaint group (keep color same)\n        # - groups can't contain spaces\n        rename_script = self.rename_script_template.format(\n            clip_id=group_item[\"clip_id\"],\n            group_id=group_item[\"group_id\"],\n            r=group_item[\"red\"],\n            g=group_item[\"green\"],\n            b=group_item[\"blue\"],\n            name=group_name\n        )\n        execute_george_through_file(rename_script)\n\n        self.log.info((\n            f\"Name of group with index {group_id}\"\n            f\" was changed to \\\"{group_name}\\\".\"\n        ))\n        return new_instance\n\n    def _get_groups_enum(self):\n        groups_enum = []\n        empty_groups = []\n        for group in get_groups_data():\n            group_name = group[\"name\"]\n            item = {\n                \"label\": group_name,\n                \"value\": group[\"group_id\"]\n            }\n            # TVPaint have defined how many color groups is available, but\n            #   the count is not consistent across versions. It is not possible\n            #   to know how many groups there is.\n            #\n            if group_name and group_name != \"0\":\n                if empty_groups:\n                    groups_enum.extend(empty_groups)\n                    empty_groups = []\n                groups_enum.append(item)\n            else:\n                empty_groups.append(item)\n        return groups_enum\n\n    def get_pre_create_attr_defs(self):\n        groups_enum = self._get_groups_enum()\n        groups_enum.insert(0, {\"label\": \"<Use selection>\", \"value\": -1})\n\n        return [\n            EnumDef(\n                \"group_id\",\n                label=\"Group\",\n                items=groups_enum\n            ),\n            BoolDef(\n                \"mark_for_review\",\n                label=\"Review\",\n                default=self.mark_for_review\n            )\n        ]\n\n    def get_instance_attr_defs(self):\n        groups_enum = self._get_groups_enum()\n        return [\n            EnumDef(\n                \"group_id\",\n                label=\"Group\",\n                items=groups_enum\n            ),\n            BoolDef(\n                \"mark_for_review\",\n                label=\"Review\",\n                default=self.mark_for_review\n            )\n        ]\n\n    def update_instances(self, update_list):\n        self._update_color_groups()\n        self._update_renderpass_groups()\n\n        super().update_instances(update_list)\n\n    def _update_color_groups(self):\n        render_layer_instances = []\n        for instance in self.create_context.instances:\n            if instance.creator_identifier == self.identifier:\n                render_layer_instances.append(instance)\n\n        if not render_layer_instances:\n            return\n\n        groups_by_id = {\n            group[\"group_id\"]: group\n            for group in get_groups_data()\n        }\n        grg_script_lines = []\n        for instance in render_layer_instances:\n            group_id = instance[\"creator_attributes\"][\"group_id\"]\n            variant = instance[\"variant\"]\n            group = groups_by_id[group_id]\n            if group[\"name\"] == variant:\n                continue\n\n            grg_script_lines.append(self.rename_script_template.format(\n                clip_id=group[\"clip_id\"],\n                group_id=group[\"group_id\"],\n                r=group[\"red\"],\n                g=group[\"green\"],\n                b=group[\"blue\"],\n                name=variant\n            ))\n\n        if grg_script_lines:\n            execute_george_through_file(\"\\n\".join(grg_script_lines))\n\n    def _update_renderpass_groups(self):\n        render_layer_instances = {}\n        render_pass_instances = collections.defaultdict(list)\n\n        for instance in self.create_context.instances:\n            if instance.creator_identifier == CreateRenderPass.identifier:\n                render_layer_id = (\n                    instance[\"creator_attributes\"][\"render_layer_instance_id\"]\n                )\n                render_pass_instances[render_layer_id].append(instance)\n            elif instance.creator_identifier == self.identifier:\n                render_layer_instances[instance.id] = instance\n\n        if not render_pass_instances or not render_layer_instances:\n            return\n\n        layers_data = get_layers_data()\n        layers_by_name = collections.defaultdict(list)\n        for layer in layers_data:\n            layers_by_name[layer[\"name\"]].append(layer)\n\n        george_lines = []\n        for render_layer_id, instances in render_pass_instances.items():\n            render_layer_inst = render_layer_instances.get(render_layer_id)\n            if render_layer_inst is None:\n                continue\n            group_id = render_layer_inst[\"creator_attributes\"][\"group_id\"]\n            layer_names = set()\n            for instance in instances:\n                layer_names |= set(instance[\"layer_names\"])\n\n            for layer_name in layer_names:\n                george_lines.extend(\n                    f\"tv_layercolor \\\"set\\\" {layer['layer_id']} {group_id}\"\n                    for layer in layers_by_name[layer_name]\n                    if layer[\"group_id\"] != group_id\n                )\n        if george_lines:\n            execute_george_through_file(\"\\n\".join(george_lines))\n\n\nclass CreateRenderPass(TVPaintCreator):\n    family = \"render\"\n    subset_template_family_filter = \"renderPass\"\n    identifier = \"render.pass\"\n    label = \"Render Pass\"\n    icon = \"fa5.image\"\n    description = \"Mark selected TVPaint layers as pass of Render Layer.\"\n    detailed_description = RENDER_PASS_DETAILED_DESCRIPTIONS\n\n    order = CreateRenderlayer.order + 10\n\n    # Settings\n    mark_for_review = True\n\n    def apply_settings(self, project_settings):\n        plugin_settings = (\n            project_settings[\"tvpaint\"][\"create\"][\"create_render_pass\"]\n        )\n        self.default_variant = plugin_settings[\"default_variant\"]\n        self.default_variants = plugin_settings[\"default_variants\"]\n        self.mark_for_review = plugin_settings[\"mark_for_review\"]\n\n    def collect_instances(self):\n        instances_by_identifier = self._cache_and_get_instances()\n        render_layers = {\n            instance_data[\"instance_id\"]: {\n                \"variant\": instance_data[\"variant\"],\n                \"template_data\": prepare_template_data({\n                    \"renderlayer\": instance_data[\"variant\"]\n                })\n            }\n            for instance_data in (\n                instances_by_identifier[CreateRenderlayer.identifier]\n            )\n        }\n\n        for instance_data in instances_by_identifier[self.identifier]:\n            render_layer_instance_id = (\n                instance_data\n                .get(\"creator_attributes\", {})\n                .get(\"render_layer_instance_id\")\n            )\n            render_layer_info = render_layers.get(render_layer_instance_id, {})\n            self.update_instance_labels(\n                instance_data,\n                render_layer_info.get(\"variant\"),\n                render_layer_info.get(\"template_data\")\n            )\n            instance = CreatedInstance.from_existing(instance_data, self)\n            self._add_instance_to_context(instance)\n\n    def get_dynamic_data(\n        self, variant, task_name, asset_doc, project_name, host_name, instance\n    ):\n        dynamic_data = super().get_dynamic_data(\n            variant, task_name, asset_doc, project_name, host_name, instance\n        )\n        dynamic_data[\"renderpass\"] = variant\n        dynamic_data[\"renderlayer\"] = \"{renderlayer}\"\n        return dynamic_data\n\n    def update_instance_labels(\n        self, instance, render_layer_variant, render_layer_data=None\n    ):\n        old_label = instance.get(\"label\")\n        old_group = instance.get(\"group\")\n        new_label = None\n        new_group = None\n        if render_layer_variant is not None:\n            if render_layer_data is None:\n                render_layer_data = prepare_template_data({\n                    \"renderlayer\": render_layer_variant\n                })\n            try:\n                new_label = instance[\"subset\"].format(**render_layer_data)\n            except (KeyError, ValueError):\n                pass\n\n            new_group = f\"{self.get_group_label()} ({render_layer_variant})\"\n\n        instance[\"label\"] = new_label\n        instance[\"group\"] = new_group\n        return old_group != new_group or old_label != new_label\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        render_layer_instance_id = pre_create_data.get(\n            \"render_layer_instance_id\"\n        )\n        if not render_layer_instance_id:\n            raise CreatorError((\n                \"You cannot create a Render Pass without a Render Layer.\"\n                \" Please select one first\"\n            ))\n\n        render_layer_instance = self.create_context.instances_by_id.get(\n            render_layer_instance_id\n        )\n        if render_layer_instance is None:\n            raise CreatorError((\n                \"RenderLayer instance was not found\"\n                f\" by id \\\"{render_layer_instance_id}\\\"\"\n            ))\n\n        group_id = render_layer_instance[\"creator_attributes\"][\"group_id\"]\n        self.log.debug(\"Query data from workfile.\")\n        layers_data = get_layers_data()\n\n        self.log.debug(\"Checking selection.\")\n        # Get all selected layers and their group ids\n        marked_layer_names = pre_create_data.get(\"layer_names\")\n        if marked_layer_names is not None:\n            layers_by_name = {layer[\"name\"]: layer for layer in layers_data}\n            marked_layers = []\n            for layer_name in marked_layer_names:\n                layer = layers_by_name.get(layer_name)\n                if layer is None:\n                    raise CreatorError(\n                        f\"Layer with name \\\"{layer_name}\\\" was not found\")\n                marked_layers.append(layer)\n\n        else:\n            marked_layers = [\n                layer\n                for layer in layers_data\n                if layer[\"selected\"]\n            ]\n\n            # Raise if nothing is selected\n            if not marked_layers:\n                raise CreatorError(\n                    \"Nothing is selected. Please select layers.\")\n\n            marked_layer_names = {layer[\"name\"] for layer in marked_layers}\n\n        marked_layer_names = set(marked_layer_names)\n\n        instances_to_remove = []\n        for instance in self.create_context.instances:\n            if instance.creator_identifier != self.identifier:\n                continue\n            cur_layer_names = set(instance[\"layer_names\"])\n            if not cur_layer_names.intersection(marked_layer_names):\n                continue\n            new_layer_names = cur_layer_names - marked_layer_names\n            if new_layer_names:\n                instance[\"layer_names\"] = list(new_layer_names)\n            else:\n                instances_to_remove.append(instance)\n\n        render_layer = render_layer_instance[\"variant\"]\n        subset_name_fill_data = {\"renderlayer\": render_layer}\n\n        # Format dynamic keys in subset name\n        label = subset_name\n        try:\n            label = label.format(\n                **prepare_template_data(subset_name_fill_data)\n            )\n        except (KeyError, ValueError):\n            pass\n\n        self.log.info(f\"New subset name is \\\"{label}\\\".\")\n        instance_data[\"label\"] = label\n        instance_data[\"group\"] = f\"{self.get_group_label()} ({render_layer})\"\n        instance_data[\"layer_names\"] = list(marked_layer_names)\n        if \"creator_attributes\" not in instance_data:\n            instance_data[\"creator_attributes\"] = {}\n\n        creator_attributes = instance_data[\"creator_attributes\"]\n        mark_for_review = pre_create_data.get(\"mark_for_review\")\n        if mark_for_review is None:\n            mark_for_review = self.mark_for_review\n        creator_attributes[\"mark_for_review\"] = mark_for_review\n        creator_attributes[\"render_layer_instance_id\"] = (\n            render_layer_instance_id\n        )\n\n        new_instance = CreatedInstance(\n            self.family,\n            subset_name,\n            instance_data,\n            self\n        )\n        instances_data = self._remove_and_filter_instances(\n            instances_to_remove\n        )\n        instances_data.append(new_instance.data_to_store())\n\n        self.host.write_instances(instances_data)\n        self._add_instance_to_context(new_instance)\n        self._change_layers_group(marked_layers, group_id)\n\n        return new_instance\n\n    def _change_layers_group(self, layers, group_id):\n        filtered_layers = [\n            layer\n            for layer in layers\n            if layer[\"group_id\"] != group_id\n        ]\n        if filtered_layers:\n            self.log.info((\n                \"Changing group of \"\n                f\"{','.join([l['name'] for l in filtered_layers])}\"\n                f\" to {group_id}\"\n            ))\n            george_lines = [\n                f\"tv_layercolor \\\"set\\\" {layer['layer_id']} {group_id}\"\n                for layer in filtered_layers\n            ]\n            execute_george_through_file(\"\\n\".join(george_lines))\n\n    def _remove_and_filter_instances(self, instances_to_remove):\n        instances_data = self.host.list_instances()\n        if not instances_to_remove:\n            return instances_data\n\n        removed_ids = set()\n        for instance in instances_to_remove:\n            removed_ids.add(instance.id)\n            self._remove_instance_from_context(instance)\n\n        return [\n            instance_data\n            for instance_data in instances_data\n            if instance_data.get(\"instance_id\") not in removed_ids\n        ]\n\n    def get_pre_create_attr_defs(self):\n        # Find available Render Layers\n        # - instances are created after creators reset\n        current_instances = self.host.list_instances()\n        render_layers = [\n            {\n                \"value\": inst[\"instance_id\"],\n                \"label\": inst[\"subset\"]\n            }\n            for inst in current_instances\n            if inst.get(\"creator_identifier\") == CreateRenderlayer.identifier\n        ]\n        if not render_layers:\n            render_layers.append({\"value\": None, \"label\": \"N/A\"})\n\n        return [\n            EnumDef(\n                \"render_layer_instance_id\",\n                label=\"Render Layer\",\n                items=render_layers\n            ),\n            UILabelDef(\n                \"NOTE: Try to hit refresh if you don't see a Render Layer\"\n            ),\n            BoolDef(\n                \"mark_for_review\",\n                label=\"Review\",\n                default=self.mark_for_review\n            )\n        ]\n\n    def get_instance_attr_defs(self):\n        # Find available Render Layers\n        current_instances = self.create_context.instances\n        render_layers = [\n            {\n                \"value\": instance.id,\n                \"label\": instance.label\n            }\n            for instance in current_instances\n            if instance.creator_identifier == CreateRenderlayer.identifier\n        ]\n        if not render_layers:\n            render_layers.append({\"value\": None, \"label\": \"N/A\"})\n\n        return [\n            EnumDef(\n                \"render_layer_instance_id\",\n                label=\"Render Layer\",\n                items=render_layers\n            ),\n            UILabelDef(\n                \"NOTE: Try to hit refresh if you don't see a Render Layer\"\n            ),\n            BoolDef(\n                \"mark_for_review\",\n                label=\"Review\",\n                default=self.mark_for_review\n            )\n        ]\n\n\nclass TVPaintAutoDetectRenderCreator(TVPaintCreator):\n    \"\"\"Create Render Layer and Render Pass instances based on scene data.\n\n    This is auto-detection creator which can be triggered by user to create\n    instances based on information in scene. Each used color group in scene\n    will be created as Render Layer where group name is used as variant and\n    each TVPaint layer as Render Pass where layer name is used as variant.\n\n    Never will have any instances, all instances belong to different creators.\n    \"\"\"\n\n    family = \"render\"\n    label = \"Render Layer/Passes\"\n    identifier = \"render.auto.detect.creator\"\n    order = CreateRenderPass.order + 10\n    description = (\n        \"Create Render Layers and Render Passes based on scene setup\"\n    )\n    detailed_description = AUTODETECT_RENDER_DETAILED_DESCRIPTION\n\n    # Settings\n    enabled = False\n    allow_group_rename = True\n    group_name_template = \"L{group_index}\"\n    group_idx_offset = 10\n    group_idx_padding = 3\n\n    def apply_settings(self, project_settings):\n        plugin_settings = (\n            project_settings\n            [\"tvpaint\"]\n            [\"create\"]\n            [\"auto_detect_render\"]\n        )\n        self.enabled = plugin_settings.get(\"enabled\", False)\n        self.allow_group_rename = plugin_settings[\"allow_group_rename\"]\n        self.group_name_template = plugin_settings[\"group_name_template\"]\n        self.group_idx_offset = plugin_settings[\"group_idx_offset\"]\n        self.group_idx_padding = plugin_settings[\"group_idx_padding\"]\n\n    def _rename_groups(\n        self,\n        groups_order: list[int],\n        scene_groups: list[dict[str, Any]]\n    ):\n        new_group_name_by_id: dict[int, str] = {}\n        groups_by_id: dict[int, dict[str, Any]] = {\n            group[\"group_id\"]: group\n            for group in scene_groups\n        }\n        # Count only renamed groups\n        for idx, group_id in enumerate(groups_order):\n            group_index_value: str = (\n                \"{{:0>{}}}\"\n                .format(self.group_idx_padding)\n                .format((idx + 1) * self.group_idx_offset)\n            )\n            group_name_fill_values: dict[str, str] = {\n                \"groupIdx\": group_index_value,\n                \"groupidx\": group_index_value,\n                \"group_idx\": group_index_value,\n                \"group_index\": group_index_value,\n            }\n\n            group_name: str = self.group_name_template.format(\n                **group_name_fill_values\n            )\n            group: dict[str, Any] = groups_by_id[group_id]\n            if group[\"name\"] != group_name:\n                new_group_name_by_id[group_id] = group_name\n\n        grg_lines: list[str] = []\n        for group_id, group_name in new_group_name_by_id.items():\n            group: dict[str, Any] = groups_by_id[group_id]\n            grg_line: str = \"tv_layercolor \\\"setcolor\\\" {} {} {} {} {}\".format(\n                group[\"clip_id\"],\n                group_id,\n                group[\"red\"],\n                group[\"green\"],\n                group[\"blue\"],\n                group_name\n            )\n            grg_lines.append(grg_line)\n            group[\"name\"] = group_name\n\n        if grg_lines:\n            execute_george_through_file(\"\\n\".join(grg_lines))\n\n    def _prepare_render_layer(\n        self,\n        project_name: str,\n        asset_doc: dict[str, Any],\n        task_name: str,\n        group_id: int,\n        groups: list[dict[str, Any]],\n        mark_for_review: bool,\n        existing_instance: Optional[CreatedInstance] = None,\n    ) -> Union[CreatedInstance, None]:\n        match_group: Union[dict[str, Any], None] = next(\n            (\n                group\n                for group in groups\n                if group[\"group_id\"] == group_id\n            ),\n            None\n        )\n        if not match_group:\n            return None\n\n        variant: str = match_group[\"name\"]\n        creator: CreateRenderlayer = (\n            self.create_context.creators[CreateRenderlayer.identifier]\n        )\n\n        subset_name: str = creator.get_subset_name(\n            variant,\n            task_name,\n            asset_doc,\n            project_name,\n            host_name=self.create_context.host_name,\n        )\n        asset_name = get_asset_name_identifier(asset_doc)\n        if existing_instance is not None:\n            if AYON_SERVER_ENABLED:\n                existing_instance[\"folderPath\"] = asset_name\n            else:\n                existing_instance[\"asset\"] = asset_name\n            existing_instance[\"task\"] = task_name\n            existing_instance[\"subset\"] = subset_name\n            return existing_instance\n\n        instance_data: dict[str, str] = {\n            \"task\": task_name,\n            \"family\": creator.family,\n            \"variant\": variant\n        }\n        if AYON_SERVER_ENABLED:\n            instance_data[\"folderPath\"] = asset_name\n        else:\n            instance_data[\"asset\"] = asset_name\n        pre_create_data: dict[str, str] = {\n            \"group_id\": group_id,\n            \"mark_for_review\": mark_for_review\n        }\n        return creator.create(subset_name, instance_data, pre_create_data)\n\n    def _prepare_render_passes(\n        self,\n        project_name: str,\n        asset_doc: dict[str, Any],\n        task_name: str,\n        render_layer_instance: CreatedInstance,\n        layers: list[dict[str, Any]],\n        mark_for_review: bool,\n        existing_render_passes: list[CreatedInstance]\n    ):\n        creator: CreateRenderPass = (\n            self.create_context.creators[CreateRenderPass.identifier]\n        )\n        render_pass_by_layer_name = {}\n        for render_pass in existing_render_passes:\n            for layer_name in render_pass[\"layer_names\"]:\n                render_pass_by_layer_name[layer_name] = render_pass\n\n        asset_name = get_asset_name_identifier(asset_doc)\n\n        for layer in layers:\n            layer_name = layer[\"name\"]\n            variant = layer_name\n            render_pass = render_pass_by_layer_name.get(layer_name)\n            if render_pass is not None:\n                if (render_pass[\"layer_names\"]) > 1:\n                    variant = render_pass[\"variant\"]\n\n            subset_name = creator.get_subset_name(\n                variant,\n                task_name,\n                asset_doc,\n                project_name,\n                host_name=self.create_context.host_name,\n                instance=render_pass\n            )\n\n            if render_pass is not None:\n                if AYON_SERVER_ENABLED:\n                    render_pass[\"folderPath\"] = asset_name\n                else:\n                    render_pass[\"asset\"] = asset_name\n\n                render_pass[\"task\"] = task_name\n                render_pass[\"subset\"] = subset_name\n                continue\n\n            instance_data: dict[str, str] = {\n                \"task\": task_name,\n                \"family\": creator.family,\n                \"variant\": variant\n            }\n            if AYON_SERVER_ENABLED:\n                instance_data[\"folderPath\"] = asset_name\n            else:\n                instance_data[\"asset\"] = asset_name\n\n            pre_create_data: dict[str, Any] = {\n                \"render_layer_instance_id\": render_layer_instance.id,\n                \"layer_names\": [layer_name],\n                \"mark_for_review\": mark_for_review\n            }\n            creator.create(subset_name, instance_data, pre_create_data)\n\n    def _filter_groups(\n        self,\n        layers_by_group_id,\n        groups_order,\n        only_visible_groups\n    ):\n        new_groups_order = []\n        for group_id in groups_order:\n            layers: list[dict[str, Any]] = layers_by_group_id[group_id]\n            if not layers:\n                continue\n\n            if (\n                only_visible_groups\n                and not any(\n                    layer\n                    for layer in layers\n                    if layer[\"visible\"]\n                )\n            ):\n                continue\n            new_groups_order.append(group_id)\n        return new_groups_order\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        project_name: str = self.create_context.get_current_project_name()\n        if AYON_SERVER_ENABLED:\n            asset_name: str = instance_data[\"folderPath\"]\n        else:\n            asset_name: str = instance_data[\"asset\"]\n        task_name: str = instance_data[\"task\"]\n        asset_doc: dict[str, Any] = get_asset_by_name(\n            project_name, asset_name)\n\n        render_layers_by_group_id: dict[int, CreatedInstance] = {}\n        render_passes_by_render_layer_id: dict[int, list[CreatedInstance]] = (\n            collections.defaultdict(list)\n        )\n        for instance in self.create_context.instances:\n            if instance.creator_identifier == CreateRenderlayer.identifier:\n                group_id = instance[\"creator_attributes\"][\"group_id\"]\n                render_layers_by_group_id[group_id] = instance\n            elif instance.creator_identifier == CreateRenderPass.identifier:\n                render_layer_id = (\n                    instance\n                    [\"creator_attributes\"]\n                    [\"render_layer_instance_id\"]\n                )\n                render_passes_by_render_layer_id[render_layer_id].append(\n                    instance\n                )\n\n        layers_by_group_id: dict[int, list[dict[str, Any]]] = (\n            collections.defaultdict(list)\n        )\n        scene_layers: list[dict[str, Any]] = get_layers_data()\n        scene_groups: list[dict[str, Any]] = get_groups_data()\n        groups_order: list[int] = []\n        for layer in scene_layers:\n            group_id: int = layer[\"group_id\"]\n            # Skip 'default' group\n            if group_id == 0:\n                continue\n\n            layers_by_group_id[group_id].append(layer)\n            if group_id not in groups_order:\n                groups_order.append(group_id)\n\n        groups_order.reverse()\n\n        mark_layers_for_review = pre_create_data.get(\n            \"mark_layers_for_review\", False\n        )\n        mark_passes_for_review = pre_create_data.get(\n            \"mark_passes_for_review\", False\n        )\n        rename_groups = pre_create_data.get(\"rename_groups\", False)\n        only_visible_groups = pre_create_data.get(\"only_visible_groups\", False)\n        groups_order = self._filter_groups(\n            layers_by_group_id,\n            groups_order,\n            only_visible_groups\n        )\n        if not groups_order:\n            return\n\n        if rename_groups:\n            self._rename_groups(groups_order, scene_groups)\n\n        # Make sure  all render layers are created\n        for group_id in groups_order:\n            instance: Union[CreatedInstance, None] = (\n                self._prepare_render_layer(\n                    project_name,\n                    asset_doc,\n                    task_name,\n                    group_id,\n                    scene_groups,\n                    mark_layers_for_review,\n                    render_layers_by_group_id.get(group_id),\n                )\n            )\n            if instance is not None:\n                render_layers_by_group_id[group_id] = instance\n\n        for group_id in groups_order:\n            layers: list[dict[str, Any]] = layers_by_group_id[group_id]\n            render_layer_instance: Union[CreatedInstance, None] = (\n                render_layers_by_group_id.get(group_id)\n            )\n            if not layers or render_layer_instance is None:\n                continue\n\n            self._prepare_render_passes(\n                project_name,\n                asset_doc,\n                task_name,\n                render_layer_instance,\n                layers,\n                mark_passes_for_review,\n                render_passes_by_render_layer_id[render_layer_instance.id]\n            )\n\n    def get_pre_create_attr_defs(self) -> list[AbstractAttrDef]:\n        render_layer_creator: CreateRenderlayer = (\n            self.create_context.creators[CreateRenderlayer.identifier]\n        )\n        render_pass_creator: CreateRenderPass = (\n            self.create_context.creators[CreateRenderPass.identifier]\n        )\n        output = []\n        if self.allow_group_rename:\n            output.extend([\n                BoolDef(\n                    \"rename_groups\",\n                    label=\"Rename color groups\",\n                    tooltip=\"Will rename color groups using studio template\",\n                    default=True\n                ),\n                BoolDef(\n                    \"only_visible_groups\",\n                    label=\"Only visible color groups\",\n                    tooltip=(\n                        \"Render Layers and rename will happen only on color\"\n                        \" groups with visible layers.\"\n                    ),\n                    default=True\n                ),\n                UISeparatorDef()\n            ])\n        output.extend([\n            BoolDef(\n                \"mark_layers_for_review\",\n                label=\"Mark RenderLayers for review\",\n                default=render_layer_creator.mark_for_review\n            ),\n            BoolDef(\n                \"mark_passes_for_review\",\n                label=\"Mark RenderPasses for review\",\n                default=render_pass_creator.mark_for_review\n            )\n        ])\n        return output\n\n\nclass TVPaintSceneRenderCreator(TVPaintAutoCreator):\n    family = \"render\"\n    subset_template_family_filter = \"renderScene\"\n    identifier = \"render.scene\"\n    label = \"Scene Render\"\n    icon = \"fa.file-image-o\"\n\n    # Settings\n    default_pass_name = \"beauty\"\n    mark_for_review = True\n    active_on_create = False\n\n    def apply_settings(self, project_settings):\n        plugin_settings = (\n            project_settings[\"tvpaint\"][\"create\"][\"create_render_scene\"]\n        )\n        self.default_variant = plugin_settings[\"default_variant\"]\n        self.default_variants = plugin_settings[\"default_variants\"]\n        self.mark_for_review = plugin_settings[\"mark_for_review\"]\n        self.active_on_create = plugin_settings[\"active_on_create\"]\n        self.default_pass_name = plugin_settings[\"default_pass_name\"]\n\n    def get_dynamic_data(self, variant, *args, **kwargs):\n        dynamic_data = super().get_dynamic_data(variant, *args, **kwargs)\n        dynamic_data[\"renderpass\"] = \"{renderpass}\"\n        dynamic_data[\"renderlayer\"] = variant\n        return dynamic_data\n\n    def _create_new_instance(self):\n        create_context = self.create_context\n        host_name = create_context.host_name\n        project_name = create_context.get_current_project_name()\n        asset_name = create_context.get_current_asset_name()\n        task_name = create_context.get_current_task_name()\n\n        asset_doc = get_asset_by_name(project_name, asset_name)\n        subset_name = self.get_subset_name(\n            self.default_variant,\n            task_name,\n            asset_doc,\n            project_name,\n            host_name\n        )\n        data = {\n            \"task\": task_name,\n            \"variant\": self.default_variant,\n            \"creator_attributes\": {\n                \"render_pass_name\": self.default_pass_name,\n                \"mark_for_review\": True\n            },\n            \"label\": self._get_label(\n                subset_name,\n                self.default_pass_name\n            )\n        }\n        if AYON_SERVER_ENABLED:\n            data[\"folderPath\"] = asset_name\n        else:\n            data[\"asset\"] = asset_name\n        if not self.active_on_create:\n            data[\"active\"] = False\n\n        new_instance = CreatedInstance(\n            self.family, subset_name, data, self\n        )\n        instances_data = self.host.list_instances()\n        instances_data.append(new_instance.data_to_store())\n        self.host.write_instances(instances_data)\n        self._add_instance_to_context(new_instance)\n        return new_instance\n\n    def create(self):\n        existing_instance = None\n        for instance in self.create_context.instances:\n            if instance.creator_identifier == self.identifier:\n                existing_instance = instance\n                break\n\n        if existing_instance is None:\n            return self._create_new_instance()\n\n        create_context = self.create_context\n        host_name = create_context.host_name\n        project_name = create_context.get_current_project_name()\n        asset_name = create_context.get_current_asset_name()\n        task_name = create_context.get_current_task_name()\n\n        existing_name = None\n        if AYON_SERVER_ENABLED:\n            existing_name = existing_instance.get(\"folderPath\")\n        if existing_name is None:\n            existing_name = existing_instance[\"asset\"]\n\n        if (\n            existing_name != asset_name\n            or existing_instance[\"task\"] != task_name\n        ):\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                existing_instance[\"variant\"],\n                task_name,\n                asset_doc,\n                project_name,\n                host_name,\n                existing_instance\n            )\n            if AYON_SERVER_ENABLED:\n                existing_instance[\"folderPath\"] = asset_name\n            else:\n                existing_instance[\"asset\"] = asset_name\n            existing_instance[\"task\"] = task_name\n            existing_instance[\"subset\"] = subset_name\n\n        existing_instance[\"label\"] = self._get_label(\n            existing_instance[\"subset\"],\n            existing_instance[\"creator_attributes\"][\"render_pass_name\"]\n        )\n\n    def _get_label(self, subset_name, render_pass_name):\n        try:\n            subset_name = subset_name.format(**prepare_template_data({\n                \"renderpass\": render_pass_name\n            }))\n        except (KeyError, ValueError):\n            pass\n\n        return subset_name\n\n    def get_instance_attr_defs(self):\n        return [\n            TextDef(\n                \"render_pass_name\",\n                label=\"Pass Name\",\n                default=self.default_pass_name,\n                tooltip=(\n                    \"Value is calculated during publishing and UI will update\"\n                    \" label after refresh.\"\n                )\n            ),\n            BoolDef(\n                \"mark_for_review\",\n                label=\"Review\",\n                default=self.mark_for_review\n            )\n        ]\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/create/create_review.py",
    "content": "from openpype import AYON_SERVER_ENABLED\nfrom openpype.client import get_asset_by_name\nfrom openpype.pipeline import CreatedInstance\nfrom openpype.hosts.tvpaint.api.plugin import TVPaintAutoCreator\n\n\nclass TVPaintReviewCreator(TVPaintAutoCreator):\n    family = \"review\"\n    identifier = \"scene.review\"\n    label = \"Review\"\n    icon = \"ei.video\"\n\n    # Settings\n    active_on_create = True\n\n    def apply_settings(self, project_settings):\n        plugin_settings = (\n            project_settings[\"tvpaint\"][\"create\"][\"create_review\"]\n        )\n        self.default_variant = plugin_settings[\"default_variant\"]\n        self.default_variants = plugin_settings[\"default_variants\"]\n        self.active_on_create = plugin_settings[\"active_on_create\"]\n\n    def create(self):\n        existing_instance = None\n        for instance in self.create_context.instances:\n            if instance.creator_identifier == self.identifier:\n                existing_instance = instance\n                break\n\n        create_context = self.create_context\n        host_name = create_context.host_name\n        project_name = create_context.get_current_project_name()\n        asset_name = create_context.get_current_asset_name()\n        task_name = create_context.get_current_task_name()\n\n        if existing_instance is None:\n            existing_asset_name = None\n        elif AYON_SERVER_ENABLED:\n            existing_asset_name = existing_instance[\"folderPath\"]\n        else:\n            existing_asset_name = existing_instance[\"asset\"]\n\n        if existing_instance is None:\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                self.default_variant,\n                task_name,\n                asset_doc,\n                project_name,\n                host_name\n            )\n            data = {\n                \"task\": task_name,\n                \"variant\": self.default_variant\n            }\n            if AYON_SERVER_ENABLED:\n                data[\"folderPath\"] = asset_name\n            else:\n                data[\"asset\"] = asset_name\n\n            if not self.active_on_create:\n                data[\"active\"] = False\n\n            new_instance = CreatedInstance(\n                self.family, subset_name, data, self\n            )\n            instances_data = self.host.list_instances()\n            instances_data.append(new_instance.data_to_store())\n            self.host.write_instances(instances_data)\n            self._add_instance_to_context(new_instance)\n\n        elif (\n            existing_asset_name != asset_name\n            or existing_instance[\"task\"] != task_name\n        ):\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                existing_instance[\"variant\"],\n                task_name,\n                asset_doc,\n                project_name,\n                host_name,\n                existing_instance\n            )\n            if AYON_SERVER_ENABLED:\n                existing_instance[\"folderPath\"] = asset_name\n            else:\n                existing_instance[\"asset\"] = asset_name\n            existing_instance[\"task\"] = task_name\n            existing_instance[\"subset\"] = subset_name\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/create/create_workfile.py",
    "content": "from openpype import AYON_SERVER_ENABLED\nfrom openpype.client import get_asset_by_name\nfrom openpype.pipeline import CreatedInstance\nfrom openpype.hosts.tvpaint.api.plugin import TVPaintAutoCreator\n\n\nclass TVPaintWorkfileCreator(TVPaintAutoCreator):\n    family = \"workfile\"\n    identifier = \"workfile\"\n    label = \"Workfile\"\n    icon = \"fa.file-o\"\n\n    def apply_settings(self, project_settings):\n        plugin_settings = (\n            project_settings[\"tvpaint\"][\"create\"][\"create_workfile\"]\n        )\n        self.default_variant = plugin_settings[\"default_variant\"]\n        self.default_variants = plugin_settings[\"default_variants\"]\n\n    def create(self):\n        existing_instance = None\n        for instance in self.create_context.instances:\n            if instance.creator_identifier == self.identifier:\n                existing_instance = instance\n                break\n\n        create_context = self.create_context\n        host_name = create_context.host_name\n        project_name = create_context.get_current_project_name()\n        asset_name = create_context.get_current_asset_name()\n        task_name = create_context.get_current_task_name()\n\n        if existing_instance is None:\n            existing_asset_name = None\n        elif AYON_SERVER_ENABLED:\n            existing_asset_name = existing_instance[\"folderPath\"]\n        else:\n            existing_asset_name = existing_instance[\"asset\"]\n\n        if existing_instance is None:\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                self.default_variant,\n                task_name,\n                asset_doc,\n                project_name,\n                host_name\n            )\n            data = {\n                \"task\": task_name,\n                \"variant\": self.default_variant\n            }\n            if AYON_SERVER_ENABLED:\n                data[\"folderPath\"] = asset_name\n            else:\n                data[\"asset\"] = asset_name\n\n            new_instance = CreatedInstance(\n                self.family, subset_name, data, self\n            )\n            instances_data = self.host.list_instances()\n            instances_data.append(new_instance.data_to_store())\n            self.host.write_instances(instances_data)\n            self._add_instance_to_context(new_instance)\n\n        elif (\n            existing_asset_name != asset_name\n            or existing_instance[\"task\"] != task_name\n        ):\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            subset_name = self.get_subset_name(\n                existing_instance[\"variant\"],\n                task_name,\n                asset_doc,\n                project_name,\n                host_name,\n                existing_instance\n            )\n            if AYON_SERVER_ENABLED:\n                existing_instance[\"folderPath\"] = asset_name\n            else:\n                existing_instance[\"asset\"] = asset_name\n            existing_instance[\"task\"] = task_name\n            existing_instance[\"subset\"] = subset_name\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/load/load_image.py",
    "content": "from openpype.lib.attribute_definitions import BoolDef\nfrom openpype.hosts.tvpaint.api import plugin\nfrom openpype.hosts.tvpaint.api.lib import execute_george_through_file\n\n\nclass ImportImage(plugin.Loader):\n    \"\"\"Load image or image sequence to TVPaint as new layer.\"\"\"\n\n    families = [\"render\", \"image\", \"background\", \"plate\", \"review\"]\n    representations = [\"*\"]\n\n    label = \"Import Image\"\n    order = 1\n    icon = \"image\"\n    color = \"white\"\n\n    import_script = (\n        \"filepath = \\\"{}\\\"\\n\"\n        \"layer_name = \\\"{}\\\"\\n\"\n        \"tv_loadsequence filepath {}PARSE layer_id\\n\"\n        \"tv_layerrename layer_id layer_name\"\n    )\n\n    defaults = {\n        \"stretch\": True,\n        \"timestretch\": True,\n        \"preload\": True\n    }\n\n    @classmethod\n    def get_options(cls, contexts):\n        return [\n            BoolDef(\n                \"stretch\",\n                label=\"Stretch to project size\",\n                default=cls.defaults[\"stretch\"],\n                tooltip=\"Stretch loaded image/s to project resolution?\"\n            ),\n            BoolDef(\n                \"timestretch\",\n                label=\"Stretch to timeline length\",\n                default=cls.defaults[\"timestretch\"],\n                tooltip=\"Clip loaded image/s to timeline length?\"\n            ),\n            BoolDef(\n                \"preload\",\n                label=\"Preload loaded image/s\",\n                default=cls.defaults[\"preload\"],\n                tooltip=\"Preload image/s?\"\n            )\n        ]\n\n    def load(self, context, name, namespace, options):\n        stretch = options.get(\"stretch\", self.defaults[\"stretch\"])\n        timestretch = options.get(\"timestretch\", self.defaults[\"timestretch\"])\n        preload = options.get(\"preload\", self.defaults[\"preload\"])\n\n        load_options = []\n        if stretch:\n            load_options.append(\"\\\"STRETCH\\\"\")\n        if timestretch:\n            load_options.append(\"\\\"TIMESTRETCH\\\"\")\n        if preload:\n            load_options.append(\"\\\"PRELOAD\\\"\")\n\n        load_options_str = \"\"\n        for load_option in load_options:\n            load_options_str += (load_option + \" \")\n\n        # Prepare layer name\n        asset_name = context[\"asset\"][\"name\"]\n        version_name = context[\"version\"][\"name\"]\n        layer_name = \"{}_{}_v{:0>3}\".format(\n            asset_name,\n            name,\n            version_name\n        )\n        # Fill import script with filename and layer name\n        # - filename mus not contain backwards slashes\n        path = self.filepath_from_context(context).replace(\"\\\\\", \"/\")\n        george_script = self.import_script.format(\n            path,\n            layer_name,\n            load_options_str\n        )\n        return execute_george_through_file(george_script)\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/load/load_reference_image.py",
    "content": "import collections\n\nfrom openpype.lib.attribute_definitions import BoolDef\nfrom openpype.pipeline import (\n    get_representation_context,\n    register_host,\n)\nfrom openpype.hosts.tvpaint.api import plugin\nfrom openpype.hosts.tvpaint.api.lib import (\n    get_layers_data,\n    execute_george_through_file,\n)\nfrom openpype.hosts.tvpaint.api.pipeline import (\n    write_workfile_metadata,\n    SECTION_NAME_CONTAINERS,\n    containerise,\n)\n\n\nclass LoadImage(plugin.Loader):\n    \"\"\"Load image or image sequence to TVPaint as new layer.\"\"\"\n\n    families = [\"render\", \"image\", \"background\", \"plate\", \"review\"]\n    representations = [\"*\"]\n\n    label = \"Load Image\"\n    order = 1\n    icon = \"image\"\n    color = \"white\"\n\n    import_script = (\n        \"filepath = '\\\"'\\\"{}\\\"'\\\"'\\n\"\n        \"layer_name = \\\"{}\\\"\\n\"\n        \"tv_loadsequence filepath {}PARSE layer_id\\n\"\n        \"tv_layerrename layer_id layer_name\"\n    )\n\n    defaults = {\n        \"stretch\": True,\n        \"timestretch\": True,\n        \"preload\": True\n    }\n\n    @classmethod\n    def get_options(cls, contexts):\n        return [\n            BoolDef(\n                \"stretch\",\n                label=\"Stretch to project size\",\n                default=cls.defaults[\"stretch\"],\n                tooltip=\"Stretch loaded image/s to project resolution?\"\n            ),\n            BoolDef(\n                \"timestretch\",\n                label=\"Stretch to timeline length\",\n                default=cls.defaults[\"timestretch\"],\n                tooltip=\"Clip loaded image/s to timeline length?\"\n            ),\n            BoolDef(\n                \"preload\",\n                label=\"Preload loaded image/s\",\n                default=cls.defaults[\"preload\"],\n                tooltip=\"Preload image/s?\"\n            )\n        ]\n\n    def load(self, context, name, namespace, options):\n        stretch = options.get(\"stretch\", self.defaults[\"stretch\"])\n        timestretch = options.get(\"timestretch\", self.defaults[\"timestretch\"])\n        preload = options.get(\"preload\", self.defaults[\"preload\"])\n\n        load_options = []\n        if stretch:\n            load_options.append(\"\\\"STRETCH\\\"\")\n        if timestretch:\n            load_options.append(\"\\\"TIMESTRETCH\\\"\")\n        if preload:\n            load_options.append(\"\\\"PRELOAD\\\"\")\n\n        load_options_str = \"\"\n        for load_option in load_options:\n            load_options_str += (load_option + \" \")\n\n        # Prepare layer name\n        asset_name = context[\"asset\"][\"name\"]\n        subset_name = context[\"subset\"][\"name\"]\n        layer_name = self.get_unique_layer_name(asset_name, subset_name)\n\n        path = self.filepath_from_context(context)\n\n        # Fill import script with filename and layer name\n        # - filename mus not contain backwards slashes\n        george_script = self.import_script.format(\n            path.replace(\"\\\\\", \"/\"),\n            layer_name,\n            load_options_str\n        )\n\n        execute_george_through_file(george_script)\n\n        loaded_layer = None\n        layers = get_layers_data()\n        for layer in layers:\n            if layer[\"name\"] == layer_name:\n                loaded_layer = layer\n                break\n\n        if loaded_layer is None:\n            raise AssertionError(\n                \"Loading probably failed during execution of george script.\"\n            )\n\n        layer_names = [loaded_layer[\"name\"]]\n        namespace = namespace or layer_name\n        return containerise(\n            name=name,\n            namespace=namespace,\n            members=layer_names,\n            context=context,\n            loader=self.__class__.__name__\n        )\n\n    def _remove_layers(self, layer_names=None, layer_ids=None, layers=None):\n        if not layer_names and not layer_ids:\n            self.log.warning(\"Got empty layer names list.\")\n            return\n\n        if layers is None:\n            layers = get_layers_data()\n\n        available_ids = set(layer[\"layer_id\"] for layer in layers)\n\n        if layer_ids is None:\n            # Backwards compatibility (layer ids were stored instead of names)\n            layer_names_are_ids = True\n            for layer_name in layer_names:\n                if (\n                    not isinstance(layer_name, int)\n                    and not layer_name.isnumeric()\n                ):\n                    layer_names_are_ids = False\n                    break\n\n            if layer_names_are_ids:\n                layer_ids = layer_names\n\n        layer_ids_to_remove = []\n        if layer_ids is not None:\n            for layer_id in layer_ids:\n                if layer_id in available_ids:\n                    layer_ids_to_remove.append(layer_id)\n\n        else:\n            layers_by_name = collections.defaultdict(list)\n            for layer in layers:\n                layers_by_name[layer[\"name\"]].append(layer)\n\n            for layer_name in layer_names:\n                layers = layers_by_name[layer_name]\n                if len(layers) == 1:\n                    layer_ids_to_remove.append(layers[0][\"layer_id\"])\n\n        if not layer_ids_to_remove:\n            self.log.warning(\"No layers to delete.\")\n            return\n\n        george_script_lines = []\n        for layer_id in layer_ids_to_remove:\n            line = \"tv_layerkill {}\".format(layer_id)\n            george_script_lines.append(line)\n        george_script = \"\\n\".join(george_script_lines)\n        execute_george_through_file(george_script)\n\n    def _remove_container(self, container):\n        if not container:\n            return\n        representation = container[\"representation\"]\n        members = self.get_members_from_container(container)\n        host = register_host()\n        current_containers = host.get_containers()\n        pop_idx = None\n        for idx, cur_con in enumerate(current_containers):\n            cur_members = self.get_members_from_container(cur_con)\n            if (\n                cur_members == members\n                and cur_con[\"representation\"] == representation\n            ):\n                pop_idx = idx\n                break\n\n        if pop_idx is None:\n            self.log.warning(\n                \"Didn't find container in workfile containers. {}\".format(\n                    container\n                )\n            )\n            return\n\n        current_containers.pop(pop_idx)\n        write_workfile_metadata(\n            SECTION_NAME_CONTAINERS, current_containers\n        )\n\n    def remove(self, container):\n        members = self.get_members_from_container(container)\n        self.log.warning(\"Layers to delete {}\".format(members))\n        self._remove_layers(members)\n        self._remove_container(container)\n\n    def switch(self, container, representation):\n        self.update(container, representation)\n\n    def update(self, container, representation):\n        \"\"\"Replace container with different version.\n\n        New layers are loaded as first step. Then is tried to change data in\n        new layers with data from old layers. When that is done old layers are\n        removed.\n        \"\"\"\n        # Create new containers first\n        context = get_representation_context(representation)\n\n        # Get layer ids from previous container\n        old_layer_names = self.get_members_from_container(container)\n\n        # Backwards compatibility (layer ids were stored instead of names)\n        old_layers_are_ids = True\n        for name in old_layer_names:\n            if isinstance(name, int) or name.isnumeric():\n                continue\n            old_layers_are_ids = False\n            break\n\n        old_layers = []\n        layers = get_layers_data()\n        previous_layer_ids = set(layer[\"layer_id\"] for layer in layers)\n        if old_layers_are_ids:\n            for layer in layers:\n                if layer[\"layer_id\"] in old_layer_names:\n                    old_layers.append(layer)\n        else:\n            layers_by_name = collections.defaultdict(list)\n            for layer in layers:\n                layers_by_name[layer[\"name\"]].append(layer)\n\n            for layer_name in old_layer_names:\n                layers = layers_by_name[layer_name]\n                if len(layers) == 1:\n                    old_layers.append(layers[0])\n\n        # Prepare few data\n        new_start_position = None\n        new_group_id = None\n        layer_ids_to_remove = set()\n        for layer in old_layers:\n            layer_ids_to_remove.add(layer[\"layer_id\"])\n            position = layer[\"position\"]\n            group_id = layer[\"group_id\"]\n            if new_start_position is None:\n                new_start_position = position\n            elif new_start_position > position:\n                new_start_position = position\n\n            if new_group_id is None:\n                new_group_id = group_id\n            elif new_group_id < 0:\n                continue\n            elif new_group_id != group_id:\n                new_group_id = -1\n\n        # Remove old container\n        self._remove_container(container)\n        # Remove old layers\n        self._remove_layers(layer_ids=layer_ids_to_remove)\n\n        name = container[\"name\"]\n        namespace = container[\"namespace\"]\n        new_container = self.load(context, name, namespace, {})\n        new_layer_names = self.get_members_from_container(new_container)\n\n        layers = get_layers_data()\n\n        new_layers = []\n        for layer in layers:\n            if layer[\"layer_id\"] in previous_layer_ids:\n                continue\n            if layer[\"name\"] in new_layer_names:\n                new_layers.append(layer)\n\n        george_script_lines = []\n        # Group new layers to same group as previous container layers had\n        # - all old layers must be under same group\n        if new_group_id is not None and new_group_id > 0:\n            for layer in new_layers:\n                line = \"tv_layercolor \\\"set\\\" {} {}\".format(\n                    layer[\"layer_id\"], new_group_id\n                )\n                george_script_lines.append(line)\n\n        # Rename new layer to have same name\n        # - only if both old and new have one layer\n        if len(old_layers) == 1 and len(new_layers) == 1:\n            layer_name = old_layers[0][\"name\"]\n            george_script_lines.append(\n                \"tv_layerrename {} \\\"{}\\\"\".format(\n                    new_layers[0][\"layer_id\"], layer_name\n                )\n            )\n\n        # Change position of new layer\n        # - this must be done before remove old layers\n        if len(new_layers) == 1 and new_start_position is not None:\n            new_layer = new_layers[0]\n            george_script_lines.extend([\n                \"tv_layerset {}\".format(new_layer[\"layer_id\"]),\n                \"tv_layermove {}\".format(new_start_position)\n            ])\n\n        # Execute george scripts if there are any\n        if george_script_lines:\n            george_script = \"\\n\".join(george_script_lines)\n            execute_george_through_file(george_script)\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/load/load_sound.py",
    "content": "import os\nimport tempfile\nfrom openpype.hosts.tvpaint.api import plugin\nfrom openpype.hosts.tvpaint.api.lib import (\n    execute_george_through_file,\n)\n\n\nclass ImportSound(plugin.Loader):\n    \"\"\"Load sound to TVPaint.\n\n    Sound layers does not have ids but only position index so we can't\n    reference them as we can't say which is which input.\n\n    We might do that (in future) by input path. Which may be identifier if\n    we'll allow only one loaded instance of the representation as an audio.\n\n    This plugin does not work for all version of TVPaint. Known working\n    version is TVPaint 11.0.10 .\n\n    It is allowed to load video files as sound but it does not check if video\n    file contain any audio.\n    \"\"\"\n\n    families = [\"audio\", \"review\", \"plate\"]\n    representations = [\"*\"]\n\n    label = \"Import Sound\"\n    order = 1\n    icon = \"image\"\n    color = \"white\"\n\n    import_script_lines = (\n        \"sound_path = '\\\"'\\\"{}\\\"'\\\"'\",\n        \"output_path = \\\"{}\\\"\",\n        # Try to get sound clip info to check if we are in TVPaint that can\n        # load sound\n        \"tv_clipcurrentid\",\n        \"clip_id = result\",\n        \"tv_soundclipinfo clip_id 0\",\n        \"IF CMP(result,\\\"\\\")==1\",\n        (\n            \"tv_writetextfile \\\"strict\\\" \\\"append\\\" '\\\"'output_path'\\\"'\"\n            \" 'success|'\"\n        ),\n        \"EXIT\",\n        \"END\",\n\n        \"tv_soundclipnew sound_path\",\n        \"line = 'success|'result\",\n        \"tv_writetextfile \\\"strict\\\" \\\"append\\\" '\\\"'output_path'\\\"' line\"\n    )\n\n    def load(self, context, name, namespace, options):\n        # Create temp file for output\n        output_file = tempfile.NamedTemporaryFile(\n            mode=\"w\", prefix=\"pype_tvp_\", suffix=\".txt\", delete=False\n        )\n        output_file.close()\n        output_filepath = output_file.name.replace(\"\\\\\", \"/\")\n\n        # Prepare george script\n        path = self.filepath_from_context(context).replace(\"\\\\\", \"/\")\n        import_script = \"\\n\".join(self.import_script_lines)\n        george_script = import_script.format(\n            path,\n            output_filepath\n        )\n        self.log.info(\"*** George script:\\n{}\\n***\".format(george_script))\n        # Execute geoge script\n        execute_george_through_file(george_script)\n\n        # Read output file\n        lines = []\n        with open(output_filepath, \"r\") as file_stream:\n            for line in file_stream:\n                line = line.rstrip()\n                if line:\n                    lines.append(line)\n\n        # Clean up temp file\n        os.remove(output_filepath)\n\n        output = {}\n        for line in lines:\n            key, value = line.split(\"|\")\n            output[key] = value\n\n        success = output.get(\"success\")\n        # Successfully loaded sound\n        if success == \"0\":\n            return\n\n        if success == \"\":\n            raise ValueError(\n                \"Your TVPaint version does not support loading of\"\n                \" sound through George script. Please use manual load.\"\n            )\n\n        if success is None:\n            raise ValueError(\n                \"Unknown error happened during load.\"\n                \" Please report and try to use manual load.\"\n            )\n\n        # Possible errors by TVPaint documentation\n        # https://www.tvpaint.com/doc/tvpaint-animation-11/george-commands#tv_soundclipnew\n        if success == \"-1\":\n            raise ValueError(\n                \"BUG: George command did not get enough arguments.\"\n            )\n\n        if success == \"-2\":\n            # Who know what does that mean?\n            raise ValueError(\"No current clip without mixer.\")\n\n        if success == \"-3\":\n            raise ValueError(\"TVPaint couldn't read the file.\")\n\n        if success == \"-4\":\n            raise ValueError(\"TVPaint couldn't add the track.\")\n\n        raise ValueError(\"BUG: Unknown success value {}.\".format(success))\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/load/load_workfile.py",
    "content": "import os\n\nfrom openpype.lib import StringTemplate\nfrom openpype.pipeline import (\n    registered_host,\n    get_current_context,\n    Anatomy,\n)\nfrom openpype.pipeline.workfile import (\n    get_workfile_template_key_from_context,\n    get_last_workfile_with_version,\n)\nfrom openpype.pipeline.template_data import get_template_data_with_names\nfrom openpype.hosts.tvpaint.api import plugin\nfrom openpype.hosts.tvpaint.api.lib import (\n    execute_george_through_file,\n)\nfrom openpype.hosts.tvpaint.api.pipeline import (\n    get_current_workfile_context,\n)\nfrom openpype.pipeline.version_start import get_versioning_start\n\n\nclass LoadWorkfile(plugin.Loader):\n    \"\"\"Load workfile.\"\"\"\n\n    families = [\"workfile\"]\n    representations = [\"tvpp\"]\n\n    label = \"Load Workfile\"\n\n    def load(self, context, name, namespace, options):\n        # Load context of current workfile as first thing\n        #   - which context and extension has\n        filepath = self.filepath_from_context(context)\n        filepath = filepath.replace(\"\\\\\", \"/\")\n\n        if not os.path.exists(filepath):\n            raise FileExistsError(\n                \"The loaded file does not exist. Try downloading it first.\"\n            )\n\n        host = registered_host()\n        current_file = host.get_current_workfile()\n        work_context = get_current_workfile_context()\n\n        george_script = \"tv_LoadProject '\\\"'\\\"{}\\\"'\\\"'\".format(\n            filepath\n        )\n        execute_george_through_file(george_script)\n\n        # Save workfile.\n        host_name = \"tvpaint\"\n        project_name = work_context.get(\"project\")\n        asset_name = work_context.get(\"asset\")\n        task_name = work_context.get(\"task\")\n        # Far cases when there is workfile without work_context\n        if not asset_name:\n            context = get_current_context()\n            project_name = context[\"project_name\"]\n            asset_name = context[\"asset_name\"]\n            task_name = context[\"task_name\"]\n\n        template_key = get_workfile_template_key_from_context(\n            asset_name,\n            task_name,\n            host_name,\n            project_name=project_name\n        )\n        anatomy = Anatomy(project_name)\n\n        data = get_template_data_with_names(\n            project_name, asset_name, task_name, host_name\n        )\n        data[\"root\"] = anatomy.roots\n\n        file_template = anatomy.templates[template_key][\"file\"]\n\n        # Define saving file extension\n        extensions = host.get_workfile_extensions()\n        if current_file:\n            # Match the extension of current file\n            _, extension = os.path.splitext(current_file)\n        else:\n            # Fall back to the first extension supported for this host.\n            extension = extensions[0]\n\n        data[\"ext\"] = extension\n\n        folder_template = anatomy.templates[template_key][\"folder\"]\n        work_root = StringTemplate.format_strict_template(\n            folder_template, data\n        )\n        version = get_last_workfile_with_version(\n            work_root, file_template, data, extensions\n        )[1]\n\n        if version is None:\n            version = get_versioning_start(\n                project_name,\n                \"tvpaint\",\n                task_name=task_name,\n                task_type=data[\"task\"][\"type\"],\n                family=\"workfile\"\n            )\n        else:\n            version += 1\n\n        data[\"version\"] = version\n\n        filename = StringTemplate.format_strict_template(\n            file_template, data\n        )\n        path = os.path.join(work_root, filename)\n        host.save_workfile(path)\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py",
    "content": "import pyblish.api\n\n\nclass CollectOutputFrameRange(pyblish.api.InstancePlugin):\n    \"\"\"Collect frame start/end from context.\n\n    When instances are collected context does not contain `frameStart` and\n    `frameEnd` keys yet. They are collected in global plugin\n    `CollectContextEntities`.\n    \"\"\"\n\n    label = \"Collect output frame range\"\n    order = pyblish.api.CollectorOrder + 0.4999\n    hosts = [\"tvpaint\"]\n    families = [\"review\", \"render\"]\n\n    def process(self, instance):\n        asset_doc = instance.data.get(\"assetEntity\")\n        if not asset_doc:\n            return\n\n        context = instance.context\n\n        frame_start = asset_doc[\"data\"][\"frameStart\"]\n        fps = asset_doc[\"data\"][\"fps\"]\n        frame_end = frame_start + (\n            context.data[\"sceneMarkOut\"] - context.data[\"sceneMarkIn\"]\n        )\n        instance.data[\"fps\"] = fps\n        instance.data[\"frameStart\"] = frame_start\n        instance.data[\"frameEnd\"] = frame_end\n        self.log.info(\n            \"Set frames {}-{} on instance {} \".format(\n                frame_start, frame_end, instance.data[\"subset\"]\n            )\n        )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py",
    "content": "import copy\nimport pyblish.api\nfrom openpype.lib import prepare_template_data\n\n\nclass CollectRenderInstances(pyblish.api.InstancePlugin):\n    label = \"Collect Render Instances\"\n    order = pyblish.api.CollectorOrder - 0.4\n    hosts = [\"tvpaint\"]\n    families = [\"render\", \"review\"]\n\n    ignore_render_pass_transparency = False\n\n    def process(self, instance):\n        context = instance.context\n        creator_identifier = instance.data[\"creator_identifier\"]\n        if creator_identifier == \"render.layer\":\n            self._collect_data_for_render_layer(instance)\n\n        elif creator_identifier == \"render.pass\":\n            self._collect_data_for_render_pass(instance)\n\n        elif creator_identifier == \"render.scene\":\n            self._collect_data_for_render_scene(instance)\n\n        else:\n            if creator_identifier == \"scene.review\":\n                self._collect_data_for_review(instance)\n            return\n\n        subset_name = instance.data[\"subset\"]\n        instance.data[\"name\"] = subset_name\n        instance.data[\"label\"] = \"{} [{}-{}]\".format(\n            subset_name,\n            context.data[\"sceneMarkIn\"] + 1,\n            context.data[\"sceneMarkOut\"] + 1\n        )\n\n    def _collect_data_for_render_layer(self, instance):\n        instance.data[\"families\"].append(\"renderLayer\")\n        creator_attributes = instance.data[\"creator_attributes\"]\n        group_id = creator_attributes[\"group_id\"]\n        if creator_attributes[\"mark_for_review\"]:\n            instance.data[\"families\"].append(\"review\")\n\n        layers_data = instance.context.data[\"layersData\"]\n        instance.data[\"layers\"] = [\n            copy.deepcopy(layer)\n            for layer in layers_data\n            if layer[\"group_id\"] == group_id\n        ]\n\n    def _collect_data_for_render_pass(self, instance):\n        instance.data[\"families\"].append(\"renderPass\")\n\n        layer_names = set(instance.data[\"layer_names\"])\n        layers_data = instance.context.data[\"layersData\"]\n\n        creator_attributes = instance.data[\"creator_attributes\"]\n        if creator_attributes[\"mark_for_review\"]:\n            instance.data[\"families\"].append(\"review\")\n\n        instance.data[\"layers\"] = [\n            copy.deepcopy(layer)\n            for layer in layers_data\n            if layer[\"name\"] in layer_names\n        ]\n        instance.data[\"ignoreLayersTransparency\"] = (\n            self.ignore_render_pass_transparency\n        )\n\n        render_layer_data = None\n        render_layer_id = creator_attributes[\"render_layer_instance_id\"]\n        for in_data in instance.context.data[\"workfileInstances\"]:\n            if (\n                in_data.get(\"creator_identifier\") == \"render.layer\"\n                and in_data[\"instance_id\"] == render_layer_id\n            ):\n                render_layer_data = in_data\n                break\n\n        instance.data[\"renderLayerData\"] = copy.deepcopy(render_layer_data)\n        # Invalid state\n        if render_layer_data is None:\n            return\n        render_layer_name = render_layer_data[\"variant\"]\n        subset_name = instance.data[\"subset\"]\n        instance.data[\"subset\"] = subset_name.format(\n            **prepare_template_data({\"renderlayer\": render_layer_name})\n        )\n\n    def _collect_data_for_render_scene(self, instance):\n        instance.data[\"families\"].append(\"renderScene\")\n\n        creator_attributes = instance.data[\"creator_attributes\"]\n        if creator_attributes[\"mark_for_review\"]:\n            instance.data[\"families\"].append(\"review\")\n\n        instance.data[\"layers\"] = copy.deepcopy(\n            instance.context.data[\"layersData\"]\n        )\n\n        render_pass_name = (\n            instance.data[\"creator_attributes\"][\"render_pass_name\"]\n        )\n        subset_name = instance.data[\"subset\"]\n        instance.data[\"subset\"] = subset_name.format(\n            **prepare_template_data({\"renderpass\": render_pass_name})\n        )\n\n    def _collect_data_for_review(self, instance):\n        instance.data[\"layers\"] = copy.deepcopy(\n            instance.context.data[\"layersData\"]\n        )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/collect_workfile.py",
    "content": "import os\nimport json\nimport pyblish.api\n\n\nclass CollectWorkfile(pyblish.api.InstancePlugin):\n    label = \"Collect Workfile\"\n    order = pyblish.api.CollectorOrder - 0.4\n    hosts = [\"tvpaint\"]\n    families = [\"workfile\"]\n\n    def process(self, instance):\n        context = instance.context\n        current_file = context.data[\"currentFile\"]\n\n        self.log.info(\n            \"Workfile path used for workfile family: {}\".format(current_file)\n        )\n\n        dirpath, filename = os.path.split(current_file)\n        basename, ext = os.path.splitext(filename)\n\n        instance.data[\"representations\"].append({\n            \"name\": ext.lstrip(\".\"),\n            \"ext\": ext.lstrip(\".\"),\n            \"files\": filename,\n            \"stagingDir\": dirpath\n        })\n\n        self.log.info(\"Collected workfile instance: {}\".format(\n            json.dumps(instance.data, indent=4)\n        ))\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py",
    "content": "import os\nimport json\nimport tempfile\n\nimport pyblish.api\n\nfrom openpype.pipeline import legacy_io\nfrom openpype.hosts.tvpaint.api.lib import (\n    execute_george,\n    execute_george_through_file,\n    get_layers_data,\n    get_groups_data,\n)\nfrom openpype.hosts.tvpaint.api.pipeline import (\n    SECTION_NAME_CONTEXT,\n    SECTION_NAME_INSTANCES,\n    SECTION_NAME_CONTAINERS,\n\n    get_workfile_metadata_string,\n    write_workfile_metadata,\n    get_current_workfile_context,\n    list_instances,\n)\n\n\nclass ResetTVPaintWorkfileMetadata(pyblish.api.Action):\n    \"\"\"Fix invalid metadata in workfile.\"\"\"\n    label = \"Reset invalid workfile metadata\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n        metadata_keys = {\n            SECTION_NAME_CONTEXT: {},\n            SECTION_NAME_INSTANCES: [],\n            SECTION_NAME_CONTAINERS: []\n        }\n        for metadata_key, default in metadata_keys.items():\n            json_string = get_workfile_metadata_string(metadata_key)\n            if not json_string:\n                continue\n\n            try:\n                return json.loads(json_string)\n            except Exception:\n                self.log.warning(\n                    (\n                        \"Couldn't parse metadata from key \\\"{}\\\".\"\n                        \" Will reset to default value \\\"{}\\\".\"\n                        \" Loaded value was: {}\"\n                    ).format(metadata_key, default, json_string),\n                    exc_info=True\n                )\n                write_workfile_metadata(metadata_key, default)\n\n\nclass CollectWorkfileData(pyblish.api.ContextPlugin):\n    label = \"Collect Workfile Data\"\n    order = pyblish.api.CollectorOrder - 0.45\n    hosts = [\"tvpaint\"]\n    actions = [ResetTVPaintWorkfileMetadata]\n\n    def process(self, context):\n        current_project_id = execute_george(\"tv_projectcurrentid\")\n        execute_george(\"tv_projectselect {}\".format(current_project_id))\n\n        # Collect and store current context to have reference\n        current_context = {\n            \"project_name\": context.data[\"projectName\"],\n            \"asset_name\": context.data[\"asset\"],\n            \"task_name\": context.data[\"task\"]\n        }\n        self.log.debug(\"Current context is: {}\".format(current_context))\n\n        # Collect context from workfile metadata\n        self.log.info(\"Collecting workfile context\")\n\n        workfile_context = get_current_workfile_context()\n        if \"project\" in workfile_context:\n            workfile_context = {\n                \"project_name\": workfile_context.get(\"project\"),\n                \"asset_name\": workfile_context.get(\"asset\"),\n                \"task_name\": workfile_context.get(\"task\"),\n            }\n        # Store workfile context to pyblish context\n        context.data[\"workfile_context\"] = workfile_context\n        if workfile_context:\n            # Change current context with context from workfile\n            key_map = (\n                (\"AVALON_ASSET\", \"asset_name\"),\n                (\"AVALON_TASK\", \"task_name\")\n            )\n            for env_key, key in key_map:\n                legacy_io.Session[env_key] = workfile_context[key]\n                os.environ[env_key] = workfile_context[key]\n            self.log.info(\"Context changed to: {}\".format(workfile_context))\n\n            asset_name = workfile_context[\"asset_name\"]\n            task_name = workfile_context[\"task_name\"]\n\n        else:\n            asset_name = current_context[\"asset_name\"]\n            task_name = current_context[\"task_name\"]\n            # Handle older workfiles or workfiles without metadata\n            self.log.warning((\n                \"Workfile does not contain information about context.\"\n                \" Using current Session context.\"\n            ))\n\n        # Store context asset name\n        context.data[\"asset\"] = asset_name\n        context.data[\"task\"] = task_name\n        self.log.info(\n            \"Context is set to Asset: \\\"{}\\\" and Task: \\\"{}\\\"\".format(\n                asset_name, task_name\n            )\n        )\n\n        # Collect instances\n        self.log.info(\"Collecting instance data from workfile\")\n        instance_data = list_instances()\n        context.data[\"workfileInstances\"] = instance_data\n        self.log.debug(\n            \"Instance data:\\\"{}\".format(json.dumps(instance_data, indent=4))\n        )\n\n        # Collect information about layers\n        self.log.info(\"Collecting layers data from workfile\")\n        layers_data = get_layers_data()\n        layers_by_name = {}\n        for layer in layers_data:\n            layer_name = layer[\"name\"]\n            if layer_name not in layers_by_name:\n                layers_by_name[layer_name] = []\n            layers_by_name[layer_name].append(layer)\n        context.data[\"layersData\"] = layers_data\n        context.data[\"layersByName\"] = layers_by_name\n\n        self.log.debug(\n            \"Layers data:\\\"{}\".format(json.dumps(layers_data, indent=4))\n        )\n\n        # Collect information about groups\n        self.log.info(\"Collecting groups data from workfile\")\n        group_data = get_groups_data()\n        context.data[\"groupsData\"] = group_data\n        self.log.debug(\n            \"Group data:\\\"{}\".format(json.dumps(group_data, indent=4))\n        )\n\n        self.log.info(\"Collecting scene data from workfile\")\n        workfile_info_parts = execute_george(\"tv_projectinfo\").split(\" \")\n\n        # Project frame start - not used\n        workfile_info_parts.pop(-1)\n        field_order = workfile_info_parts.pop(-1)\n        frame_rate = float(workfile_info_parts.pop(-1))\n        pixel_apsect = float(workfile_info_parts.pop(-1))\n        height = int(workfile_info_parts.pop(-1))\n        width = int(workfile_info_parts.pop(-1))\n        workfile_path = \" \".join(workfile_info_parts).replace(\"\\\"\", \"\")\n\n        # Marks return as \"{frame - 1} {state} \", example \"0 set\".\n        result = execute_george(\"tv_markin\")\n        mark_in_frame, mark_in_state, _ = result.split(\" \")\n\n        result = execute_george(\"tv_markout\")\n        mark_out_frame, mark_out_state, _ = result.split(\" \")\n\n        scene_data = {\n            \"currentFile\": workfile_path,\n            \"sceneWidth\": width,\n            \"sceneHeight\": height,\n            \"scenePixelAspect\": pixel_apsect,\n            \"sceneFps\": frame_rate,\n            \"sceneFieldOrder\": field_order,\n            \"sceneMarkIn\": int(mark_in_frame),\n            \"sceneMarkInState\": mark_in_state == \"set\",\n            \"sceneMarkOut\": int(mark_out_frame),\n            \"sceneMarkOutState\": mark_out_state == \"set\",\n            \"sceneStartFrame\": int(execute_george(\"tv_startframe\")),\n            \"sceneBgColor\": self._get_bg_color()\n        }\n        self.log.debug(\n            \"Scene data: {}\".format(json.dumps(scene_data, indent=4))\n        )\n        context.data.update(scene_data)\n\n    def _get_bg_color(self):\n        \"\"\"Background color set on scene.\n\n        Is important for review exporting where scene bg color is used as\n        background.\n        \"\"\"\n        output_file = tempfile.NamedTemporaryFile(\n            mode=\"w\", prefix=\"a_tvp_\", suffix=\".txt\", delete=False\n        )\n        output_file.close()\n        output_filepath = output_file.name.replace(\"\\\\\", \"/\")\n        george_script_lines = [\n            # Variable containing full path to output file\n            \"output_path = \\\"{}\\\"\".format(output_filepath),\n            \"tv_background\",\n            \"bg_color = result\",\n            # Write data to output file\n            (\n                \"tv_writetextfile\"\n                \" \\\"strict\\\" \\\"append\\\" '\\\"'output_path'\\\"' bg_color\"\n            )\n        ]\n\n        george_script = \"\\n\".join(george_script_lines)\n        execute_george_through_file(george_script)\n\n        with open(output_filepath, \"r\") as stream:\n            data = stream.read()\n\n        os.remove(output_filepath)\n        data = data.strip()\n        if not data:\n            return None\n        return data.split(\" \")\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py",
    "content": "\"\"\"Plugin converting png files from ExtractSequence into exrs.\n\nRequires:\n    ExtractSequence - source of PNG\n    ExtractReview - review was already created so we can convert to any exr\n\"\"\"\nimport os\nimport json\n\nimport pyblish.api\nfrom openpype.lib import (\n    get_oiio_tool_args,\n    ToolNotFoundError,\n    run_subprocess,\n)\nfrom openpype.pipeline import KnownPublishError\n\n\nclass ExtractConvertToEXR(pyblish.api.InstancePlugin):\n    # Offset to get after ExtractSequence plugin.\n    order = pyblish.api.ExtractorOrder + 0.1\n    label = \"Extract Sequence EXR\"\n    hosts = [\"tvpaint\"]\n    families = [\"render\"]\n\n    enabled = False\n\n    # Replace source PNG files or just add\n    replace_pngs = True\n    # EXR compression\n    exr_compression = \"ZIP\"\n\n    def process(self, instance):\n        repres = instance.data.get(\"representations\")\n        if not repres:\n            return\n\n        try:\n            oiio_args = get_oiio_tool_args(\"oiiotool\")\n        except ToolNotFoundError:\n            # Raise an exception when oiiotool is not available\n            # - this can currently happen on MacOS machines\n            raise KnownPublishError(\n                \"OpenImageIO tool is not available on this machine.\"\n            )\n\n        new_repres = []\n        for repre in repres:\n            if repre[\"name\"] != \"png\":\n                continue\n\n            self.log.info(\n                \"Processing representation: {}\".format(\n                    json.dumps(repre, sort_keys=True, indent=4)\n                )\n            )\n\n            src_filepaths = set()\n            new_filenames = []\n            for src_filename in repre[\"files\"]:\n                dst_filename = os.path.splitext(src_filename)[0] + \".exr\"\n                new_filenames.append(dst_filename)\n\n                src_filepath = os.path.join(repre[\"stagingDir\"], src_filename)\n                dst_filepath = os.path.join(repre[\"stagingDir\"], dst_filename)\n\n                src_filepaths.add(src_filepath)\n\n                args = oiio_args + [\n                    src_filepath,\n                    \"--compression\", self.exr_compression,\n                    # TODO how to define color conversion?\n                    \"--colorconvert\", \"sRGB\", \"linear\",\n                    \"-o\", dst_filepath\n                ]\n                run_subprocess(args)\n\n            new_repres.append(\n                {\n                    \"name\": \"exr\",\n                    \"ext\": \"exr\",\n                    \"files\": new_filenames,\n                    \"stagingDir\": repre[\"stagingDir\"],\n                    \"tags\": list(repre[\"tags\"])\n                }\n            )\n\n            if self.replace_pngs:\n                instance.data[\"representations\"].remove(repre)\n\n                for filepath in src_filepaths:\n                    instance.context.data[\"cleanupFullPaths\"].append(filepath)\n\n        instance.data[\"representations\"].extend(new_repres)\n        self.log.info(\n            \"Representations: {}\".format(\n                json.dumps(\n                    instance.data[\"representations\"], sort_keys=True, indent=4\n                )\n            )\n        )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/extract_sequence.py",
    "content": "import os\nimport copy\nimport tempfile\n\nfrom PIL import Image\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    KnownPublishError,\n    get_publish_instance_families,\n)\nfrom openpype.hosts.tvpaint.api.lib import (\n    execute_george,\n    execute_george_through_file,\n    get_layers_pre_post_behavior,\n    get_layers_exposure_frames,\n)\nfrom openpype.hosts.tvpaint.lib import (\n    calculate_layers_extraction_data,\n    get_frame_filename_template,\n    fill_reference_frames,\n    composite_rendered_layers,\n    rename_filepaths_by_frame_start,\n)\n\n\nclass ExtractSequence(pyblish.api.Extractor):\n    label = \"Extract Sequence\"\n    hosts = [\"tvpaint\"]\n    families = [\"review\", \"render\"]\n\n    # Modifiable with settings\n    review_bg = [255, 255, 255, 255]\n\n    def process(self, instance):\n        self.log.info(\n            \"* Processing instance \\\"{}\\\"\".format(instance.data[\"label\"])\n        )\n\n        # Get all layers and filter out not visible\n        layers = instance.data[\"layers\"]\n        filtered_layers = [\n            layer\n            for layer in layers\n            if layer[\"visible\"]\n        ]\n        layer_names = [str(layer[\"name\"]) for layer in filtered_layers]\n        if not layer_names:\n            self.log.info(\n                \"None of the layers from the instance\"\n                \" are visible. Extraction skipped.\"\n            )\n            return\n\n        joined_layer_names = \", \".join(\n            [\"\\\"{}\\\"\".format(name) for name in layer_names]\n        )\n        self.log.debug(\n            \"Instance has {} layers with names: {}\".format(\n                len(layer_names), joined_layer_names\n            )\n        )\n\n        ignore_layers_transparency = instance.data.get(\n            \"ignoreLayersTransparency\", False\n        )\n\n        mark_in = instance.context.data[\"sceneMarkIn\"]\n        mark_out = instance.context.data[\"sceneMarkOut\"]\n\n        # Change scene Start Frame to 0 to prevent frame index issues\n        #   - issue is that TVPaint versions deal with frame indexes in a\n        #     different way when Start Frame is not `0`\n        # NOTE It will be set back after rendering\n        scene_start_frame = instance.context.data[\"sceneStartFrame\"]\n        execute_george(\"tv_startframe 0\")\n\n        # Frame start/end may be stored as float\n        frame_start = int(instance.data[\"frameStart\"])\n\n        # Handles are not stored per instance but on Context\n        handle_start = instance.context.data[\"handleStart\"]\n\n        scene_bg_color = instance.context.data[\"sceneBgColor\"]\n\n        # Prepare output frames\n        output_frame_start = frame_start - handle_start\n\n        # Change output frame start to 0 if handles cause it's negative number\n        if output_frame_start < 0:\n            self.log.warning((\n                \"Frame start with handles has negative value.\"\n                \" Changed to \\\"0\\\". Frames start: {}, Handle Start: {}\"\n            ).format(frame_start, handle_start))\n            output_frame_start = 0\n\n        # Calculate frame end\n        output_frame_end = output_frame_start + (mark_out - mark_in)\n\n        # Save to staging dir\n        output_dir = instance.data.get(\"stagingDir\")\n        if not output_dir:\n            # Create temp folder if staging dir is not set\n            output_dir = (\n                tempfile.mkdtemp(prefix=\"tvpaint_render_\")\n            ).replace(\"\\\\\", \"/\")\n            instance.data[\"stagingDir\"] = output_dir\n\n        self.log.debug(\n            \"Files will be rendered to folder: {}\".format(output_dir)\n        )\n\n        if instance.data[\"family\"] == \"review\":\n            result = self.render_review(\n                output_dir, mark_in, mark_out, scene_bg_color\n            )\n        else:\n            # Render output\n            result = self.render(\n                output_dir,\n                mark_in,\n                mark_out,\n                filtered_layers,\n                ignore_layers_transparency\n            )\n\n        output_filepaths_by_frame_idx, thumbnail_fullpath = result\n\n        # Change scene frame Start back to previous value\n        execute_george(\"tv_startframe {}\".format(scene_start_frame))\n\n        # Sequence of one frame\n        if not output_filepaths_by_frame_idx:\n            self.log.warning(\"Extractor did not create any output.\")\n            return\n\n        repre_files = self._rename_output_files(\n            output_filepaths_by_frame_idx,\n            mark_in,\n            mark_out,\n            output_frame_start\n        )\n\n        # Fill tags and new families from project settings\n        instance_families = get_publish_instance_families(instance)\n        tags = []\n        if \"review\" in instance_families:\n            tags.append(\"review\")\n\n        # Sequence of one frame\n        single_file = len(repre_files) == 1\n        if single_file:\n            repre_files = repre_files[0]\n\n        # Extension is hardcoded\n        #   - changing extension would require change code\n        new_repre = {\n            \"name\": \"png\",\n            \"ext\": \"png\",\n            \"files\": repre_files,\n            \"stagingDir\": output_dir,\n            \"tags\": tags\n        }\n\n        if not single_file:\n            new_repre[\"frameStart\"] = output_frame_start\n            new_repre[\"frameEnd\"] = output_frame_end\n\n        self.log.debug(\"Creating new representation: {}\".format(new_repre))\n\n        instance.data[\"representations\"].append(new_repre)\n\n        if not thumbnail_fullpath:\n            return\n\n        thumbnail_ext = os.path.splitext(\n            thumbnail_fullpath\n        )[1].replace(\".\", \"\")\n        # Create thumbnail representation\n        thumbnail_repre = {\n            \"name\": \"thumbnail\",\n            \"ext\": thumbnail_ext,\n            \"outputName\": \"thumb\",\n            \"files\": os.path.basename(thumbnail_fullpath),\n            \"stagingDir\": output_dir,\n            \"tags\": [\"thumbnail\"]\n        }\n        instance.data[\"representations\"].append(thumbnail_repre)\n\n    def _rename_output_files(\n        self, filepaths_by_frame, mark_in, mark_out, output_frame_start\n    ):\n        new_filepaths_by_frame = rename_filepaths_by_frame_start(\n            filepaths_by_frame, mark_in, mark_out, output_frame_start\n        )\n\n        repre_filenames = []\n        for filepath in new_filepaths_by_frame.values():\n            repre_filenames.append(os.path.basename(filepath))\n\n        if mark_in < output_frame_start:\n            repre_filenames = list(reversed(repre_filenames))\n\n        return repre_filenames\n\n    def render_review(\n        self, output_dir, mark_in, mark_out, scene_bg_color\n    ):\n        \"\"\" Export images from TVPaint using `tv_savesequence` command.\n\n        Args:\n            output_dir (str): Directory where files will be stored.\n            mark_in (int): Starting frame index from which export will begin.\n            mark_out (int): On which frame index export will end.\n            scene_bg_color (list): Bg color set in scene. Result of george\n                script command `tv_background`.\n\n        Returns:\n            tuple: With 2 items first is list of filenames second is path to\n                thumbnail.\n        \"\"\"\n        filename_template = get_frame_filename_template(mark_out)\n\n        self.log.debug(\"Preparing data for rendering.\")\n        first_frame_filepath = os.path.join(\n            output_dir,\n            filename_template.format(frame=mark_in)\n        )\n\n        bg_color = self._get_review_bg_color()\n\n        george_script_lines = [\n            # Change bg color to color from settings\n            \"tv_background \\\"color\\\" {} {} {}\".format(*bg_color),\n            \"tv_SaveMode \\\"PNG\\\"\",\n            \"export_path = \\\"{}\\\"\".format(\n                first_frame_filepath.replace(\"\\\\\", \"/\")\n            ),\n            \"tv_savesequence '\\\"'export_path'\\\"' {} {}\".format(\n                mark_in, mark_out\n            )\n        ]\n        if scene_bg_color:\n            # Change bg color back to previous scene bg color\n            _scene_bg_color = copy.deepcopy(scene_bg_color)\n            bg_type = _scene_bg_color.pop(0)\n            orig_color_command = [\n                \"tv_background\",\n                \"\\\"{}\\\"\".format(bg_type)\n            ]\n            orig_color_command.extend(_scene_bg_color)\n\n            george_script_lines.append(\" \".join(orig_color_command))\n\n        execute_george_through_file(\"\\n\".join(george_script_lines))\n\n        first_frame_filepath = None\n        output_filepaths_by_frame_idx = {}\n        for frame_idx in range(mark_in, mark_out + 1):\n            filename = filename_template.format(frame=frame_idx)\n            filepath = os.path.join(output_dir, filename)\n\n            output_filepaths_by_frame_idx[frame_idx] = filepath\n\n            if not os.path.exists(filepath):\n                raise KnownPublishError(\n                    \"Output was not rendered. File was not found {}\".format(\n                        filepath\n                    )\n                )\n\n            if first_frame_filepath is None:\n                first_frame_filepath = filepath\n\n        thumbnail_filepath = None\n        if first_frame_filepath and os.path.exists(first_frame_filepath):\n            thumbnail_filepath = os.path.join(output_dir, \"thumbnail.jpg\")\n            source_img = Image.open(first_frame_filepath)\n            if source_img.mode.lower() != \"rgb\":\n                source_img = source_img.convert(\"RGB\")\n            source_img.save(thumbnail_filepath)\n\n        return output_filepaths_by_frame_idx, thumbnail_filepath\n\n    def render(\n        self, output_dir, mark_in, mark_out, layers, ignore_layer_opacity\n    ):\n        \"\"\" Export images from TVPaint.\n\n        Args:\n            output_dir (str): Directory where files will be stored.\n            mark_in (int): Starting frame index from which export will begin.\n            mark_out (int): On which frame index export will end.\n            layers (list): List of layers to be exported.\n            ignore_layer_opacity (bool): Layer's opacity will be ignored.\n\n        Returns:\n            tuple: With 2 items first is list of filenames second is path to\n                thumbnail.\n        \"\"\"\n        self.log.debug(\"Preparing data for rendering.\")\n\n        # Map layers by position\n        layers_by_position = {}\n        layers_by_id = {}\n        layer_ids = []\n        for layer in layers:\n            layer_id = layer[\"layer_id\"]\n            position = layer[\"position\"]\n            layers_by_position[position] = layer\n            layers_by_id[layer_id] = layer\n\n            layer_ids.append(layer_id)\n\n        # Sort layer positions in reverse order\n        sorted_positions = list(reversed(sorted(layers_by_position.keys())))\n        if not sorted_positions:\n            return [], None\n\n        self.log.debug(\"Collecting pre/post behavior of individual layers.\")\n        behavior_by_layer_id = get_layers_pre_post_behavior(layer_ids)\n        exposure_frames_by_layer_id = get_layers_exposure_frames(\n            layer_ids, layers\n        )\n        extraction_data_by_layer_id = calculate_layers_extraction_data(\n            layers,\n            exposure_frames_by_layer_id,\n            behavior_by_layer_id,\n            mark_in,\n            mark_out\n        )\n        # Render layers\n        filepaths_by_layer_id = {}\n        for layer_id, render_data in extraction_data_by_layer_id.items():\n            layer = layers_by_id[layer_id]\n            filepaths_by_layer_id[layer_id] = self._render_layer(\n                render_data, layer, output_dir, ignore_layer_opacity\n            )\n\n        # Prepare final filepaths where compositing should store result\n        output_filepaths_by_frame = {}\n        thumbnail_src_filepath = None\n        finale_template = get_frame_filename_template(mark_out)\n        for frame_idx in range(mark_in, mark_out + 1):\n            filename = finale_template.format(frame=frame_idx)\n\n            filepath = os.path.join(output_dir, filename)\n            output_filepaths_by_frame[frame_idx] = filepath\n\n            if thumbnail_src_filepath is None:\n                thumbnail_src_filepath = filepath\n\n        self.log.info(\"Started compositing of layer frames.\")\n        composite_rendered_layers(\n            layers, filepaths_by_layer_id,\n            mark_in, mark_out,\n            output_filepaths_by_frame\n        )\n\n        self.log.info(\"Compositing finished\")\n        thumbnail_filepath = None\n        if thumbnail_src_filepath and os.path.exists(thumbnail_src_filepath):\n            source_img = Image.open(thumbnail_src_filepath)\n            thumbnail_filepath = os.path.join(output_dir, \"thumbnail.jpg\")\n            # Composite background only on rgba images\n            # - just making sure\n            if source_img.mode.lower() == \"rgba\":\n                bg_color = self._get_review_bg_color()\n                self.log.debug(\"Adding thumbnail background color {}.\".format(\n                    \" \".join([str(val) for val in bg_color])\n                ))\n                bg_image = Image.new(\"RGBA\", source_img.size, bg_color)\n                thumbnail_obj = Image.alpha_composite(bg_image, source_img)\n                thumbnail_obj.convert(\"RGB\").save(thumbnail_filepath)\n\n            else:\n                self.log.info((\n                    \"Source for thumbnail has mode \\\"{}\\\" (Expected: RGBA).\"\n                    \" Can't use thubmanail background color.\"\n                ).format(source_img.mode))\n                source_img.save(thumbnail_filepath)\n\n        return output_filepaths_by_frame, thumbnail_filepath\n\n    def _get_review_bg_color(self):\n        red = green = blue = 255\n        if self.review_bg:\n            if len(self.review_bg) == 4:\n                red, green, blue, _ = self.review_bg\n            elif len(self.review_bg) == 3:\n                red, green, blue = self.review_bg\n        return (red, green, blue)\n\n    def _render_layer(\n        self, render_data, layer, output_dir, ignore_layer_opacity\n    ):\n        frame_references = render_data[\"frame_references\"]\n        filenames_by_frame_index = render_data[\"filenames_by_frame_index\"]\n\n        layer_id = layer[\"layer_id\"]\n        george_script_lines = [\n            \"tv_layerset {}\".format(layer_id),\n            \"tv_SaveMode \\\"PNG\\\"\"\n        ]\n        # Set density to 100 and store previous opacity\n        if ignore_layer_opacity:\n            george_script_lines.extend([\n                \"tv_layerdensity 100\",\n                \"orig_opacity = result\",\n            ])\n\n        filepaths_by_frame = {}\n        frames_to_render = []\n        for frame_idx, ref_idx in frame_references.items():\n            # None reference is skipped because does not have source\n            if ref_idx is None:\n                filepaths_by_frame[frame_idx] = None\n                continue\n            filename = filenames_by_frame_index[frame_idx]\n            dst_path = \"/\".join([output_dir, filename])\n            filepaths_by_frame[frame_idx] = dst_path\n            if frame_idx != ref_idx:\n                continue\n\n            frames_to_render.append(str(frame_idx))\n            # Go to frame\n            george_script_lines.append(\"tv_layerImage {}\".format(frame_idx))\n            # Store image to output\n            george_script_lines.append(\"tv_saveimage \\\"{}\\\"\".format(dst_path))\n\n        # Set density back to origin opacity\n        if ignore_layer_opacity:\n            george_script_lines.append(\"tv_layerdensity orig_opacity\")\n\n        self.log.debug(\"Rendering Exposure frames {} of layer {} ({})\".format(\n            \",\".join(frames_to_render), layer_id, layer[\"name\"]\n        ))\n        # Let TVPaint render layer's image\n        execute_george_through_file(\"\\n\".join(george_script_lines))\n\n        # Fill frames between `frame_start_index` and `frame_end_index`\n        self.log.debug(\"Filling frames not rendered frames.\")\n        fill_reference_frames(frame_references, filepaths_by_frame)\n\n        return filepaths_by_frame\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/help/validate_asset_name.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Subset context</title>\n<description>## Invalid subset context\n\nContext of the given subset doesn't match your current scene.\n\n### How to repair?\n\nYout can fix this with \"Repair\" button on the right. This will use '{expected_asset}' asset name and overwrite '{found_asset}' asset name in scene metadata.\n\nAfter that restart publishing with Reload button.\n</description>\n<detail>\n### How could this happen?\n\nThe subset was created in different scene with different context\nor the scene file was copy pasted from different context.\n</detail>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/help/validate_duplicated_layer_names.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Layer names</title>\n<description>## Duplicated layer names\n\nCan't determine which layers should be published because there are duplicated layer names in the scene.\n\n### Duplicated layer names\n\n{layer_names}\n\n*Check layer names for all subsets in list on left side.*\n\n### How to repair?\n\nHide/rename/remove layers that should not be published.\n\nIf all of them should be published then you have duplicated subset names in the scene. In that case you have to recrete them and use different variant name.\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/help/validate_layers_visibility.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Layers visibility</title>\n<description>## All layers are not visible\n\nLayers visibility was changed during publishing which caused that all layers for subset \"{instance_name}\" are hidden.\n\n### Layer names for **{instance_name}**\n\n{layer_names}\n\n*Check layer names for all subsets in the list on the left side.*\n\n### How to repair?\n\nReset publishing and do not change visibility of layers after hitting publish button.\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/help/validate_marks.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Frame range</title>\n<description>## Invalid render frame range\n\nScene frame range which will be rendered is defined by MarkIn and MarkOut. Expected frame range is {expected_frame_range} and current frame range is {current_frame_range}.\n\nIt is also required that MarkIn and MarkOut are enabled in the scene. Their color is highlighted on timeline when are enabled.\n\n- MarkIn is {mark_in_enable_state}\n- MarkOut is {mark_out_enable_state}\n\n### How to repair?\n\nYout can fix this with \"Repair\" button on the right. That will change MarkOut to {expected_mark_out}.\n\nOr you can manually modify MarkIn and MarkOut in the scene timeline.\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/help/validate_missing_layer_names.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Missing layers</title>\n<description>## Missing layers for render pass\n\nRender pass subset \"{instance_name}\" has stored layer names that belong to it's rendering scope but layers were not found in scene.\n\n### Missing layer names\n\n{layer_names}\n\n### How to repair?\n\nFind layers that belong to subset {instance_name} and rename them back to expected layer names or remove the subset and create new with right layers.\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/help/validate_render_layer_group.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Overused Color group</title>\n<description>## One Color group is used by multiple Render Layers\n\nSingle color group used by multiple Render Layers would cause clashes of rendered TVPaint layers. The same layers would be used for output files of both groups.\n\n### Missing layer names\n\n{groups_information}\n\n### How to repair?\n\nRefresh, go to 'Publish' tab and go through Render Layers and change their groups to not clash each other. If you reach limit of TVPaint color groups there is nothing you can do about it to fix the issue.\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/help/validate_render_pass_group.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Render pass group</title>\n<description>## Invalid group of Render Pass layers\n\nLayers of Render Pass {instance_name} belong to Render Group which is defined by TVPaint color group {expected_group}. But the layers are not in the group.\n\n### How to repair?\n\nChange the color group to {expected_group} on layers {layer_names}.\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/help/validate_scene_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Scene settings</title>\n<description>## Invalid scene settings\n\nScene settings do not match to expected values.\n\n**FPS**\n- Expected value: {expected_fps}\n- Current value: {current_fps}\n\n**Resolution**\n- Expected value: {expected_width}x{expected_height}\n- Current value: {current_width}x{current_height}\n\n**Pixel ratio**\n- Expected value: {expected_pixel_ratio}\n- Current value: {current_pixel_ratio}\n\n### How to repair?\n\nFPS and Pixel ratio can be modified in scene setting. Wrong resolution can be fixed with changing resolution of scene but due to TVPaint limitations it is possible that you will need to create new scene.\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/help/validate_start_frame.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>First frame</title>\n<description>## MarkIn is not set to 0\n\nMarkIn in your scene must start from 0 fram index but MarkIn is set to {current_start_frame}.\n\n### How to repair?\n\nYou can modify MarkIn manually or hit the \"Repair\" button on the right which will change MarkIn to 0 (does not change MarkOut).\n</description>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_metadata.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Missing metadata</title>\n<description>## Your scene miss context metadata\n\nYour scene does not contain metadata about {missing_metadata}.\n\n### How to repair?\n\nResave the scene using Workfiles tool or hit the \"Repair\" button on the right.\n</description>\n<detail>\n### How this could happen?\n\nYou're using scene file that was not created using Workfiles tool.\n</detail>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_project_name.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Project name</title>\n<description>## Your scene is from different project\n\nIt is not possible to publish into project \"{workfile_project_name}\" when TVPaint was opened with project \"{env_project_name}\" in context.\n\n### How to repair?\n\nIf the workfile belongs to project \"{env_project_name}\" then use Workfiles tool to resave it.\n\nOtherwise close TVPaint and launch it again from project you want to publish in.\n</description>\n<detail>\n### How this could happen?\n\nYou've opened workfile from different project. You've opened TVPaint on a task from \"{env_project_name}\" then you've opened TVPaint again on task from \"{workfile_project_name}\" without closing the TVPaint. Because TVPaint can run only once the project didn't change.\n\n### Why it is important?\nBecause project may affect how TVPaint works or change publishing behavior it is dangerous to allow change project context in many ways. For example publishing will not run as expected.\n</detail>\n</error>\n</root>\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py",
    "content": "import pyblish.api\n\nfrom openpype.lib import version_up\nfrom openpype.pipeline import registered_host\n\n\nclass IncrementWorkfileVersion(pyblish.api.ContextPlugin):\n    \"\"\"Increment current workfile version.\"\"\"\n\n    order = pyblish.api.IntegratorOrder + 1\n    label = \"Increment Workfile Version\"\n    optional = True\n    hosts = [\"tvpaint\"]\n\n    def process(self, context):\n\n        assert all(result[\"success\"] for result in context.data[\"results\"]), (\n            \"Publishing not successful so version is not increased.\")\n\n        host = registered_host()\n        path = context.data[\"currentFile\"]\n        host.save_workfile(version_up(path))\n        self.log.info('Incrementing workfile version')\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py",
    "content": "import pyblish.api\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import (\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin,\n)\nfrom openpype.hosts.tvpaint.api.pipeline import (\n    list_instances,\n    write_instances,\n)\n\n\nclass FixAssetNames(pyblish.api.Action):\n    \"\"\"Repair the asset names.\n\n    Change instanace metadata in the workfile.\n    \"\"\"\n\n    label = \"Repair\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n        context_asset_name = context.data[\"asset\"]\n        old_instance_items = list_instances()\n        new_instance_items = []\n        for instance_item in old_instance_items:\n            if AYON_SERVER_ENABLED:\n                instance_asset_name = instance_item.get(\"folderPath\")\n            else:\n                instance_asset_name = instance_item.get(\"asset\")\n\n            if (\n                instance_asset_name\n                and instance_asset_name != context_asset_name\n            ):\n                if AYON_SERVER_ENABLED:\n                    instance_item[\"folderPath\"] = context_asset_name\n                else:\n                    instance_item[\"asset\"] = context_asset_name\n            new_instance_items.append(instance_item)\n        write_instances(new_instance_items)\n\n\nclass ValidateAssetName(\n    OptionalPyblishPluginMixin,\n    pyblish.api.ContextPlugin\n):\n    \"\"\"Validate asset name present on instance.\n\n    Asset name on instance should be the same as context's.\n    \"\"\"\n\n    label = \"Validate Asset Names\"\n    order = pyblish.api.ValidatorOrder\n    hosts = [\"tvpaint\"]\n    actions = [FixAssetNames]\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            return\n        context_asset_name = context.data[\"asset\"]\n        for instance in context:\n            asset_name = instance.data.get(\"asset\")\n            if asset_name and asset_name == context_asset_name:\n                continue\n\n            instance_label = (\n                instance.data.get(\"label\") or instance.data[\"name\"]\n            )\n\n            raise PublishXmlValidationError(\n                self,\n                (\n                    \"Different asset name on instance then context's.\"\n                    \" Instance \\\"{}\\\" has asset name: \\\"{}\\\"\"\n                    \" Context asset name is: \\\"{}\\\"\"\n                ).format(\n                    instance_label, asset_name, context_asset_name\n                ),\n                formatting_data={\n                    \"expected_asset\": context_asset_name,\n                    \"found_asset\": asset_name\n                }\n            )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/validate_duplicated_layer_names.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import PublishXmlValidationError\n\n\nclass ValidateLayersGroup(pyblish.api.InstancePlugin):\n    \"\"\"Validate layer names for publishing are unique for whole workfile.\"\"\"\n\n    label = \"Validate Duplicated Layers Names\"\n    order = pyblish.api.ValidatorOrder\n    families = [\"renderPass\"]\n\n    def process(self, instance):\n        # Prepare layers\n        layers_by_name = instance.context.data[\"layersByName\"]\n\n        # Layers ids of an instance\n        layer_names = instance.data[\"layer_names\"]\n\n        # Check if all layers from render pass are in right group\n        duplicated_layer_names = []\n        for layer_name in layer_names:\n            layers = layers_by_name.get(layer_name)\n            # It is not job of this validator to handle missing layers\n            if layers is None:\n                continue\n            if len(layers) > 1:\n                duplicated_layer_names.append(layer_name)\n\n        # Everything is OK and skip exception\n        if not duplicated_layer_names:\n            return\n\n        layers_msg = \", \".join([\n            \"\\\"{}\\\"\".format(layer_name)\n            for layer_name in duplicated_layer_names\n        ])\n        detail_lines = [\n            \"- {}\".format(layer_name)\n            for layer_name in set(duplicated_layer_names)\n        ]\n        raise PublishXmlValidationError(\n            self,\n            (\n                \"Layers have duplicated names for instance {}.\"\n                # Description what's wrong\n                \" There are layers with same name and one of them is marked\"\n                \" for publishing so it is not possible to know which should\"\n                \" be published. Please look for layers with names: {}\"\n            ).format(instance.data[\"label\"], layers_msg),\n            formatting_data={\n                \"layer_names\": \"<br/>\".join(detail_lines)\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import PublishXmlValidationError\n\n\n# TODO @iLLiCiTiT add repair action to disable instances?\nclass ValidateLayersVisiblity(pyblish.api.InstancePlugin):\n    \"\"\"Validate existence of renderPass layers.\"\"\"\n\n    label = \"Validate Layers Visibility\"\n    order = pyblish.api.ValidatorOrder\n    families = [\"review\", \"render\"]\n\n    def process(self, instance):\n        layers = instance.data.get(\"layers\")\n        # Instance have empty layers\n        # - it is not job of this validator to check that\n        if not layers:\n            return\n        layer_names = set()\n        for layer in layers:\n            layer_names.add(layer[\"name\"])\n            if layer[\"visible\"]:\n                return\n\n        instance_label = (\n            instance.data.get(\"label\") or instance.data[\"name\"]\n        )\n\n        raise PublishXmlValidationError(\n            self,\n            \"All layers of instance \\\"{}\\\" are not visible.\".format(\n                instance_label\n            ),\n            formatting_data={\n                \"instance_name\": instance_label,\n                \"layer_names\": \"<br/>\".join([\n                    \"- {}\".format(layer_name)\n                    for layer_name in layer_names\n                ])\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/validate_marks.py",
    "content": "import json\n\nimport pyblish.api\nfrom openpype.pipeline import (\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin,\n)\nfrom openpype.hosts.tvpaint.api.lib import execute_george\n\n\nclass ValidateMarksRepair(pyblish.api.Action):\n    \"\"\"Repair the marks.\"\"\"\n\n    label = \"Repair\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n        expected_data = ValidateMarks.get_expected_data(context)\n\n        execute_george(\n            \"tv_markin {} set\".format(expected_data[\"markIn\"])\n        )\n        execute_george(\n            \"tv_markout {} set\".format(expected_data[\"markOut\"])\n        )\n\n\nclass ValidateMarks(\n    OptionalPyblishPluginMixin,\n    pyblish.api.ContextPlugin\n):\n    \"\"\"Validate mark in and out are enabled and it's duration.\n\n    Mark In/Out does not have to match frameStart and frameEnd but duration is\n    important.\n    \"\"\"\n\n    label = \"Validate Mark In/Out\"\n    order = pyblish.api.ValidatorOrder\n    optional = True\n    actions = [ValidateMarksRepair]\n\n    @staticmethod\n    def get_expected_data(context):\n        scene_mark_in = context.data[\"sceneMarkIn\"]\n\n        # Data collected in `CollectContextEntities`\n        frame_end = context.data[\"frameEnd\"]\n        frame_start = context.data[\"frameStart\"]\n        handle_start = context.data[\"handleStart\"]\n        handle_end = context.data[\"handleEnd\"]\n\n        # Calculate expected Mark out (Mark In + duration - 1)\n        expected_mark_out = (\n            scene_mark_in\n            + (frame_end - frame_start)\n            + handle_start + handle_end\n        )\n        return {\n            \"markIn\": scene_mark_in,\n            \"markInState\": True,\n            \"markOut\": expected_mark_out,\n            \"markOutState\": True\n        }\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            return\n\n        current_data = {\n            \"markIn\": context.data[\"sceneMarkIn\"],\n            \"markInState\": context.data[\"sceneMarkInState\"],\n            \"markOut\": context.data[\"sceneMarkOut\"],\n            \"markOutState\": context.data[\"sceneMarkOutState\"]\n        }\n        expected_data = self.get_expected_data(context)\n        invalid = {}\n        for k in current_data.keys():\n            if current_data[k] != expected_data[k]:\n                invalid[k] = {\n                    \"current\": current_data[k],\n                    \"expected\": expected_data[k]\n                }\n\n        # Validation ends\n        if not invalid:\n            return\n\n        current_frame_range = (\n            (current_data[\"markOut\"] - current_data[\"markIn\"]) + 1\n        )\n        expected_frame_range = (\n            (expected_data[\"markOut\"] - expected_data[\"markIn\"]) + 1\n        )\n        mark_in_enable_state = \"disabled\"\n        if current_data[\"markInState\"]:\n            mark_in_enable_state = \"enabled\"\n\n        mark_out_enable_state = \"disabled\"\n        if current_data[\"markOutState\"]:\n            mark_out_enable_state = \"enabled\"\n\n        raise PublishXmlValidationError(\n            self,\n            \"Marks does not match database:\\n{}\".format(\n                json.dumps(invalid, sort_keys=True, indent=4)\n            ),\n            formatting_data={\n                \"current_frame_range\": str(current_frame_range),\n                \"expected_frame_range\": str(expected_frame_range),\n                \"mark_in_enable_state\": mark_in_enable_state,\n                \"mark_out_enable_state\": mark_out_enable_state,\n                \"expected_mark_out\": expected_data[\"markOut\"]\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/validate_missing_layer_names.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import PublishXmlValidationError\n\n\nclass ValidateMissingLayers(pyblish.api.InstancePlugin):\n    \"\"\"Validate existence of renderPass layers.\"\"\"\n\n    label = \"Validate Missing Layers Names\"\n    order = pyblish.api.ValidatorOrder\n    families = [\"renderPass\"]\n\n    def process(self, instance):\n        # Prepare layers\n        layers_by_name = instance.context.data[\"layersByName\"]\n\n        # Layers ids of an instance\n        layer_names = instance.data[\"layer_names\"]\n\n        # Check if all layers from render pass are in right group\n        missing_layer_names = []\n        for layer_name in layer_names:\n            layers = layers_by_name.get(layer_name)\n            if not layers:\n                missing_layer_names.append(layer_name)\n\n        # Everything is OK and skip exception\n        if not missing_layer_names:\n            return\n\n        layers_msg = \", \".join([\n            \"\\\"{}\\\"\".format(layer_name)\n            for layer_name in missing_layer_names\n        ])\n        instance_label = (\n            instance.data.get(\"label\") or instance.data[\"name\"]\n        )\n        description_layer_names = \"<br/>\".join([\n            \"- {}\".format(layer_name)\n            for layer_name in missing_layer_names\n        ])\n\n        # Raise an error\n        raise PublishXmlValidationError(\n            self,\n            (\n                \"Layers were not found by name for instance \\\"{}\\\".\"\n                # Description what's wrong\n                \" Layer names marked for publishing are not available\"\n                \" in layers list. Missing layer names: {}\"\n            ).format(instance.data[\"label\"], layers_msg),\n            formatting_data={\n                \"instance_name\": instance_label,\n                \"layer_names\": description_layer_names\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/validate_render_layer_group.py",
    "content": "import collections\nimport pyblish.api\nfrom openpype.pipeline import PublishXmlValidationError\n\n\nclass ValidateRenderLayerGroups(pyblish.api.ContextPlugin):\n    \"\"\"Validate group ids of renderLayer subsets.\n\n    Validate that there are not 2 render layers using the same group.\n    \"\"\"\n\n    label = \"Validate Render Layers Group\"\n    order = pyblish.api.ValidatorOrder + 0.1\n\n    def process(self, context):\n        # Prepare layers\n        render_layers_by_group_id = collections.defaultdict(list)\n        for instance in context:\n            families = instance.data.get(\"families\")\n            if not families or \"renderLayer\" not in families:\n                continue\n\n            group_id = instance.data[\"creator_attributes\"][\"group_id\"]\n            render_layers_by_group_id[group_id].append(instance)\n\n        duplicated_instances = []\n        for group_id, instances in render_layers_by_group_id.items():\n            if len(instances) > 1:\n                duplicated_instances.append((group_id, instances))\n\n        if not duplicated_instances:\n            return\n\n        # Exception message preparations\n        groups_data = context.data[\"groupsData\"]\n        groups_by_id = {\n            group[\"group_id\"]: group\n            for group in groups_data\n        }\n\n        per_group_msgs = []\n        groups_information_lines = []\n        for group_id, instances in duplicated_instances:\n            group = groups_by_id[group_id]\n            group_label = \"Group \\\"{}\\\" ({})\".format(\n                group[\"name\"],\n                group[\"group_id\"],\n            )\n            line_join_subset_names = \"\\n\".join([\n                f\"    - {instance['subset']}\"\n                for instance in instances\n            ])\n            joined_subset_names = \", \".join([\n                f\"\\\"{instance['subset']}\\\"\"\n                for instance in instances\n            ])\n            per_group_msgs.append(\n                \"{} < {} >\".format(group_label, joined_subset_names)\n            )\n            groups_information_lines.append(\n                \"<b>{}</b>\\n{}\".format(group_label, line_join_subset_names)\n            )\n\n        # Raise an error\n        raise PublishXmlValidationError(\n            self,\n            (\n                \"More than one Render Layer is using the same TVPaint\"\n                \" group color. {}\"\n            ).format(\" | \".join(per_group_msgs)),\n            formatting_data={\n                \"groups_information\": \"\\n\".join(groups_information_lines)\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/validate_render_pass_group.py",
    "content": "import collections\nimport pyblish.api\nfrom openpype.pipeline import PublishXmlValidationError\n\n\nclass ValidateLayersGroup(pyblish.api.InstancePlugin):\n    \"\"\"Validate group ids of renderPass layers.\n\n    Validates that all layers are in same group as they were during creation.\n    \"\"\"\n\n    label = \"Validate Layers Group\"\n    order = pyblish.api.ValidatorOrder + 0.1\n    families = [\"renderPass\"]\n\n    def process(self, instance):\n        # Prepare layers\n        layers_data = instance.context.data[\"layersData\"]\n        layers_by_name = {\n            layer[\"name\"]: layer\n            for layer in layers_data\n        }\n\n        # Expected group id for instance layers\n        group_id = instance.data[\"group_id\"]\n        # Layers ids of an instance\n        layer_names = instance.data[\"layer_names\"]\n        # Check if all layers from render pass are in right group\n        invalid_layers_by_group_id = collections.defaultdict(list)\n        invalid_layer_names = set()\n        for layer_name in layer_names:\n            layer = layers_by_name.get(layer_name)\n            _group_id = layer[\"group_id\"]\n            if _group_id != group_id:\n                invalid_layers_by_group_id[_group_id].append(layer)\n                invalid_layer_names.add(layer_name)\n\n        # Everything is OK and skip exception\n        if not invalid_layers_by_group_id:\n            return\n\n        # Exception message preparations\n        groups_data = instance.context.data[\"groupsData\"]\n        groups_by_id = {\n            group[\"group_id\"]: group\n            for group in groups_data\n        }\n        correct_group = groups_by_id[group_id]\n\n        per_group_msgs = []\n        for _group_id, layers in invalid_layers_by_group_id.items():\n            _group = groups_by_id[_group_id]\n            layers_msgs = []\n            for layer in layers:\n                layers_msgs.append(\n                    \"\\\"{}\\\" (id: {})\".format(layer[\"name\"], layer[\"layer_id\"])\n                )\n            per_group_msgs.append(\n                \"Group \\\"{}\\\" (id: {}) < {} >\".format(\n                    _group[\"name\"],\n                    _group[\"group_id\"],\n                    \", \".join(layers_msgs)\n                )\n            )\n\n        # Raise an error\n        raise PublishXmlValidationError(\n            self,\n            (\n                # Short message\n                \"Layers in wrong group.\"\n                # Description what's wrong\n                \" Layers from render pass \\\"{}\\\" must be in group {} (id: {}).\"\n                # Detailed message\n                \" Layers in wrong group: {}\"\n            ).format(\n                instance.data[\"label\"],\n                correct_group[\"name\"],\n                correct_group[\"group_id\"],\n                \" | \".join(per_group_msgs)\n            ),\n            formatting_data={\n                \"instance_name\": (\n                    instance.data.get(\"label\") or instance.data[\"name\"]\n                ),\n                \"expected_group\": correct_group[\"name\"],\n                \"layer_names\": \", \".join(invalid_layer_names)\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py",
    "content": "import json\n\nimport pyblish.api\nfrom openpype.pipeline import (\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin,\n)\n\n\n# TODO @iLliCiTiT add fix action for fps\nclass ValidateProjectSettings(\n    OptionalPyblishPluginMixin,\n    pyblish.api.ContextPlugin\n):\n    \"\"\"Validate scene settings against database.\"\"\"\n\n    label = \"Validate Scene Settings\"\n    order = pyblish.api.ValidatorOrder\n    optional = True\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            return\n\n        expected_data = context.data[\"assetEntity\"][\"data\"]\n        scene_data = {\n            \"fps\": context.data.get(\"sceneFps\"),\n            \"resolutionWidth\": context.data.get(\"sceneWidth\"),\n            \"resolutionHeight\": context.data.get(\"sceneHeight\"),\n            \"pixelAspect\": context.data.get(\"scenePixelAspect\")\n        }\n        invalid = {}\n        for k in scene_data.keys():\n            expected_value = expected_data[k]\n            if scene_data[k] != expected_value:\n                invalid[k] = {\n                    \"current\": scene_data[k], \"expected\": expected_value\n                }\n\n        if not invalid:\n            return\n\n        raise PublishXmlValidationError(\n            self,\n            \"Scene settings does not match database:\\n{}\".format(\n                json.dumps(invalid, sort_keys=True, indent=4)\n            ),\n            formatting_data={\n                \"expected_fps\": expected_data[\"fps\"],\n                \"current_fps\": scene_data[\"fps\"],\n                \"expected_width\": expected_data[\"resolutionWidth\"],\n                \"expected_height\": expected_data[\"resolutionHeight\"],\n                \"current_width\": scene_data[\"resolutionWidth\"],\n                \"current_height\": scene_data[\"resolutionHeight\"],\n                \"expected_pixel_ratio\": expected_data[\"pixelAspect\"],\n                \"current_pixel_ratio\": scene_data[\"pixelAspect\"]\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import (\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin,\n)\nfrom openpype.hosts.tvpaint.api.lib import execute_george\n\n\nclass RepairStartFrame(pyblish.api.Action):\n    \"\"\"Repair start frame.\"\"\"\n\n    label = \"Repair\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n        execute_george(\"tv_startframe 0\")\n\n\nclass ValidateStartFrame(\n    OptionalPyblishPluginMixin,\n    pyblish.api.ContextPlugin\n):\n    \"\"\"Validate start frame being at frame 0.\"\"\"\n\n    label = \"Validate Start Frame\"\n    order = pyblish.api.ValidatorOrder\n    hosts = [\"tvpaint\"]\n    actions = [RepairStartFrame]\n    optional = True\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            return\n\n        start_frame = execute_george(\"tv_startframe\")\n        if start_frame == 0:\n            return\n\n        raise PublishXmlValidationError(\n            self,\n            \"Start frame has to be frame 0.\",\n            formatting_data={\n                \"current_start_frame\": start_frame\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import (\n    PublishXmlValidationError,\n    PublishValidationError,\n    registered_host,\n)\n\n\nclass ValidateWorkfileMetadataRepair(pyblish.api.Action):\n    \"\"\"Store current context into workfile metadata.\"\"\"\n\n    label = \"Use current context\"\n    icon = \"wrench\"\n    on = \"failed\"\n\n    def process(self, context, _plugin):\n        \"\"\"Save current workfile which should trigger storing of metadata.\"\"\"\n        current_file = context.data[\"currentFile\"]\n        host = registered_host()\n        # Save file should trigger\n        host.save_workfile(current_file)\n\n\nclass ValidateWorkfileMetadata(pyblish.api.ContextPlugin):\n    \"\"\"Validate if wokrfile contain required metadata for publising.\"\"\"\n\n    label = \"Validate Workfile Metadata\"\n    order = pyblish.api.ValidatorOrder\n\n    families = [\"workfile\"]\n\n    actions = [ValidateWorkfileMetadataRepair]\n\n    required_keys = {\"project_name\", \"asset_name\", \"task_name\"}\n\n    def process(self, context):\n        workfile_context = context.data[\"workfile_context\"]\n        if not workfile_context:\n            raise PublishValidationError(\n                \"Current workfile is missing whole metadata about context.\",\n                \"Missing context\",\n                (\n                    \"Current workfile is missing metadata about task.\"\n                    \" To fix this issue save the file using Workfiles tool.\"\n                )\n            )\n\n        missing_keys = []\n        for key in self.required_keys:\n            value = workfile_context.get(key)\n            if not value:\n                missing_keys.append(key)\n\n        if missing_keys:\n            raise PublishXmlValidationError(\n                self,\n                \"Current workfile is missing metadata about {}.\".format(\n                    \", \".join(missing_keys)\n                ),\n                formatting_data={\n                    \"missing_metadata\": \", \".join(missing_keys)\n                }\n            )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/plugins/publish/validate_workfile_project_name.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import PublishXmlValidationError\n\n\nclass ValidateWorkfileProjectName(pyblish.api.ContextPlugin):\n    \"\"\"Validate project name stored in workfile metadata.\n\n    It is not possible to publish from different project than is set in\n    environment variable \"AVALON_PROJECT\".\n    \"\"\"\n\n    label = \"Validate Workfile Project Name\"\n    order = pyblish.api.ValidatorOrder\n\n    def process(self, context):\n        workfile_context = context.data.get(\"workfile_context\")\n        # If workfile context is missing than project is matching to\n        #   global project\n        if not workfile_context:\n            self.log.info(\n                \"Workfile context (\\\"workfile_context\\\") is not filled.\"\n            )\n            return\n\n        workfile_project_name = workfile_context[\"project_name\"]\n        env_project_name = context.data[\"projectName\"]\n        if workfile_project_name == env_project_name:\n            self.log.info((\n                \"Both workfile project and environment project are same. {}\"\n            ).format(env_project_name))\n            return\n\n        # Raise an error\n        raise PublishXmlValidationError(\n            self,\n            (\n                # Short message\n                \"Workfile from different Project ({}).\"\n                # Description what's wrong\n                \" It is not possible to publish when TVPaint was launched in\"\n                \"context of different project. Current context project is\"\n                \" \\\"{}\\\". Launch TVPaint in context of project \\\"{}\\\"\"\n                \" and then publish.\"\n            ).format(\n                workfile_project_name,\n                env_project_name,\n                workfile_project_name,\n            ),\n            formatting_data={\n                \"workfile_project_name\": workfile_project_name,\n                \"expected_project_name\": env_project_name\n            }\n        )\n"
  },
  {
    "path": "openpype/hosts/tvpaint/tvpaint_plugin/__init__.py",
    "content": "import os\n\n\ndef get_plugin_files_path():\n    current_dir = os.path.dirname(os.path.abspath(__file__))\n    return os.path.join(current_dir, \"plugin_files\")\n"
  },
  {
    "path": "openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.17)\nproject(OpenPypePlugin C CXX)\n\nset(CMAKE_CXX_STANDARD 17)\nset(CMAKE_CXX_EXTENSIONS OFF)\n\nset(IP_ENABLE_UNICODE OFF)\nset(IP_ENABLE_DOCTEST OFF)\n\nif(MSVC)\n    set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)\n    add_definitions(-D_CRT_SECURE_NO_WARNINGS)\n    # Define WIN64 or WIN32 for TVPaint SDK\n    if(CMAKE_SIZEOF_VOID_P EQUAL 8)\n        message(\"64bit\")\n        add_definitions(-DWIN64)\n    elseif(CMAKE_SIZEOF_VOID_P EQUAL 4)\n        message(\"32bit\")\n        add_definitions(-DWIN32)\n    endif()\nendif()\n\n# TODO better options\noption(BOOST_ROOT \"Path to root of Boost\" \"\")\n\noption(OPENSSL_INCLUDE \"OpenSSL include path\" \"\")\noption(OPENSSL_LIB_DIR \"OpenSSL lib path\" \"\")\n\noption(WEBSOCKETPP_INCLUDE \"Websocketpp include path\" \"\")\n\noption(JSONRPCPP_INCLUDE \"Jsonrpcpp include path\" \"\")\n\n# Use static boost libraries\nset(Boost_USE_STATIC_LIBS ON)\n\nfind_package(Boost COMPONENTS random chrono date_time regex REQUIRED)\n\ninclude_directories(\n        \"${TVPAINT_SDK_INCLUDE}\"\n        \"${OPENSSL_INCLUDE}\"\n        \"${WEBSOCKETPP_INCLUDE}\"\n        \"${JSONRPCPP_INCLUDE}\"\n        \"${Boost_INCLUDE_DIRS}\"\n)\n\nlink_directories(\n        \"${OPENSSL_LIB_DIR}\"\n        \"${Boost_LIBRARY_DIRS}\"\n)\n\nadd_library(jsonrpcpp INTERFACE)\n\nadd_library(${PROJECT_NAME} SHARED library.cpp library.def \"${TVPAINT_SDK_LIB}/dllx.c\")\n\ntarget_link_libraries(${PROJECT_NAME} ${Boost_LIBRARIES})\ntarget_link_libraries(${PROJECT_NAME} jsonrpcpp)\n"
  },
  {
    "path": "openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/README.md",
    "content": "README for TVPaint Avalon plugin\n================================\nIntroduction\n------------\nThis project is dedicated to integrate Avalon functionality to TVPaint.\nThis implementation is using TVPaint plugin (C/C++) which can communicate with python process. The communication should allow to trigger tools or pipeline functions from TVPaint and accept requests from python process at the same time.\n\nCurrent implementation is based on websocket protocol, using json-rpc communication (specification 2.0). Project is in beta stage, tested only on Windows.\n\nTo be able to load plugin, environment variable `WEBSOCKET_URL` must be set otherwise plugin won't load at all. Plugin should not affect TVPaint if python server crash, but buttons won't work.\n\n## Requirements - Python server\n- python >= 3.6\n- aiohttp\n- aiohttp-json-rpc\n\n### Windows\n- pywin32 - required only for plugin installation\n\n## Requirements - Plugin compilation\n- TVPaint SDK - Ask for SDK on TVPaint support.\n- Boost 1.72.0 - Boost is used across other plugins (Should be possible to use different version with CMakeLists modification)\n- Websocket++/Websocketpp - Websocket library (https://github.com/zaphoyd/websocketpp)\n- OpenSSL library - Required by Websocketpp\n- jsonrpcpp - C++ library handling json-rpc 2.0 (https://github.com/badaix/jsonrpcpp)\n- nlohmann/json - Required for jsonrpcpp (https://github.com/nlohmann/json)\n\n### jsonrpcpp\nThis library has `nlohmann/json` as it's part, but current `master` has old version which has bug and probably won't be possible to use library on windows without using last `nlohmann/json`.\n\n## TODO\n- modify code and CMake to be able to compile on MacOS/Linux\n- separate websocket logic from plugin logic\n- hide buttons and show error message if server is closed\n"
  },
  {
    "path": "openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp",
    "content": "#ifdef _WIN32\n// Include <winsock2.h> before <windows.h>\n#include <winsock2.h>\n#endif\n\n#include <cstdio>\n#include <cstdlib>\n#include <iostream>\n#include <cstring>\n#include <map>\n#include <string>\n#include <queue>\n\n#include \"plugdllx.h\"\n\n#include <boost/chrono.hpp>\n\n#include <websocketpp/config/asio_no_tls_client.hpp>\n#include <websocketpp/client.hpp>\n\n#include \"json.hpp\"\n#include \"jsonrpcpp.hpp\"\n\n\n// All functions not exported should be static.\n// All global variables should be static.\n\n// mReq Identification of the requester.  (=0 closed, !=0 requester ID)\nstatic struct {\n    bool firstParams;\n    DWORD mReq;\n    void* mLocalFile;\n    PIFilter *current_filter;\n    // Id counter for client requests\n    int client_request_id;\n    // There are new menu items\n    bool newMenuItems;\n    // Menu item definitions received from connection\n    nlohmann::json menuItems;\n    // Menu items used in requester by their ID\n    nlohmann::json menuItemsById;\n    std::list<int> menuItemsIds;\n    // Messages from server before processing.\n    // - messages can't be process at the moment of receive as client is running in thread\n    std::queue<std::string> messages;\n    // Responses to requests mapped by request id\n    std::map<int, jsonrpcpp::Response> responses;\n\n} Data = {\n    true,\n    0,\n    nullptr,\n    nullptr,\n    1,\n    false,\n    nlohmann::json::object(),\n    nlohmann::json::object()\n};\n\n// Json rpc 2.0 parser - for handling messages and callbacks\njsonrpcpp::Parser parser;\ntypedef websocketpp::client<websocketpp::config::asio_client> client;\n\n\nclass connection_metadata {\nprivate:\n    websocketpp::connection_hdl m_hdl;\n    client *m_endpoint;\n    std::string m_status;\npublic:\n    typedef websocketpp::lib::shared_ptr<connection_metadata> ptr;\n\n    connection_metadata(websocketpp::connection_hdl hdl, client *endpoint)\n            : m_hdl(hdl), m_status(\"Connecting\") {\n        m_endpoint = endpoint;\n    }\n\n    void on_open(client *c, websocketpp::connection_hdl hdl) {\n        m_status = \"Open\";\n    }\n\n    void on_fail(client *c, websocketpp::connection_hdl hdl) {\n        m_status = \"Failed\";\n    }\n\n    void on_close(client *c, websocketpp::connection_hdl hdl) {\n        m_status = \"Closed\";\n    }\n\n    void on_message(websocketpp::connection_hdl, client::message_ptr msg) {\n        std::string json_str;\n        if (msg->get_opcode() == websocketpp::frame::opcode::text) {\n            json_str = msg->get_payload();\n        } else {\n            json_str = websocketpp::utility::to_hex(msg->get_payload());\n        }\n        process_message(json_str);\n    }\n\n    void process_message(std::string msg) {\n        std::cout << \"--> \" << msg << \"\\n\";\n        try {\n            jsonrpcpp::entity_ptr entity = parser.do_parse(msg);\n            if (!entity) {\n                // Return error code?\n\n            } else if (entity->is_response()) {\n                jsonrpcpp::Response response = jsonrpcpp::Response(entity->to_json());\n                Data.responses[response.id().int_id()] = response;\n\n            } else if (entity->is_request() || entity->is_notification()) {\n                Data.messages.push(msg);\n            }\n        }\n        catch (const jsonrpcpp::RequestException &e) {\n            std::string message = e.to_json().dump();\n            std::cout << \"<-- \" << e.to_json().dump() << \"\\n\";\n            send(message);\n        }\n        catch (const jsonrpcpp::ParseErrorException &e) {\n            std::string message = e.to_json().dump();\n            std::cout << \"<-- \" << message << \"\\n\";\n            send(message);\n        }\n        catch (const jsonrpcpp::RpcException &e) {\n            std::cerr << \"RpcException: \" << e.what() << \"\\n\";\n            std::string message = jsonrpcpp::ParseErrorException(e.what()).to_json().dump();\n            std::cout << \"<-- \" << message << \"\\n\";\n            send(message);\n        }\n        catch (const std::exception &e) {\n            std::cerr << \"Exception: \" << e.what() << \"\\n\";\n        }\n    }\n\n    void send(std::string message) {\n        if (get_status() != \"Open\") {\n            return;\n        }\n        websocketpp::lib::error_code ec;\n\n        m_endpoint->send(m_hdl, message, websocketpp::frame::opcode::text, ec);\n        if (ec) {\n            std::cout << \"> Error sending message: \" << ec.message() << std::endl;\n            return;\n        }\n    }\n\n    void send_notification(jsonrpcpp::Notification *notification) {\n        send(notification->to_json().dump());\n    }\n\n    void send_response(jsonrpcpp::Response *response) {\n        send(response->to_json().dump());\n    }\n\n    void send_request(jsonrpcpp::Request *request) {\n        send(request->to_json().dump());\n    }\n\n    websocketpp::connection_hdl get_hdl() const {\n        return m_hdl;\n    }\n\n    std::string get_status() const {\n        return m_status;\n    }\n};\n\n\nclass websocket_endpoint {\nprivate:\n    client m_endpoint;\n    connection_metadata::ptr client_metadata;\n    websocketpp::lib::shared_ptr<websocketpp::lib::thread> m_thread;\n    bool thread_is_running = false;\n\npublic:\n    websocket_endpoint() {\n        m_endpoint.clear_access_channels(websocketpp::log::alevel::all);\n        m_endpoint.clear_error_channels(websocketpp::log::elevel::all);\n    }\n\n    ~websocket_endpoint() {\n        close_connection();\n    }\n\n    void close_connection() {\n        m_endpoint.stop_perpetual();\n        if (connected())\n        {\n            // Close client\n            close(websocketpp::close::status::normal, \"\");\n        }\n        if (thread_is_running) {\n            // Join thread\n            m_thread->join();\n            thread_is_running = false;\n        }\n    }\n\n    bool connected()\n    {\n        return (client_metadata && client_metadata->get_status() == \"Open\");\n    }\n    int connect(std::string const &uri) {\n        if (client_metadata && client_metadata->get_status() == \"Open\") {\n            std::cout << \"> Already connected\" << std::endl;\n            return 0;\n        }\n\n        m_endpoint.init_asio();\n        m_endpoint.start_perpetual();\n\n        m_thread.reset(new websocketpp::lib::thread(&client::run, &m_endpoint));\n        thread_is_running = true;\n\n        websocketpp::lib::error_code ec;\n\n        client::connection_ptr con = m_endpoint.get_connection(uri, ec);\n\n        if (ec) {\n            std::cout << \"> Connect initialization error: \" << ec.message() << std::endl;\n            return -1;\n        }\n\n        client_metadata = websocketpp::lib::make_shared<connection_metadata>(con->get_handle(), &m_endpoint);\n\n        con->set_open_handler(websocketpp::lib::bind(\n                &connection_metadata::on_open,\n                client_metadata,\n                &m_endpoint,\n                websocketpp::lib::placeholders::_1\n        ));\n        con->set_fail_handler(websocketpp::lib::bind(\n                &connection_metadata::on_fail,\n                client_metadata,\n                &m_endpoint,\n                websocketpp::lib::placeholders::_1\n        ));\n        con->set_close_handler(websocketpp::lib::bind(\n                &connection_metadata::on_close,\n                client_metadata,\n                &m_endpoint,\n                websocketpp::lib::placeholders::_1\n        ));\n        con->set_message_handler(websocketpp::lib::bind(\n                &connection_metadata::on_message,\n                client_metadata,\n                websocketpp::lib::placeholders::_1,\n                websocketpp::lib::placeholders::_2\n        ));\n\n        m_endpoint.connect(con);\n\n        return 1;\n    }\n\n    void close(websocketpp::close::status::value code, std::string reason) {\n        if (!client_metadata || client_metadata->get_status() != \"Open\") {\n            std::cout << \"> Not connected yet\" << std::endl;\n            return;\n        }\n\n        websocketpp::lib::error_code ec;\n\n        m_endpoint.close(client_metadata->get_hdl(), code, reason, ec);\n        if (ec) {\n            std::cout << \"> Error initiating close: \" << ec.message() << std::endl;\n        }\n    }\n\n    void send(std::string message) {\n        if (!client_metadata || client_metadata->get_status() != \"Open\") {\n            std::cout << \"> Not connected yet\" << std::endl;\n            return;\n        }\n\n        client_metadata->send(message);\n    }\n\n    void send_notification(jsonrpcpp::Notification *notification) {\n        client_metadata->send_notification(notification);\n    }\n\n    void send_response(jsonrpcpp::Response *response) {\n        client_metadata->send(response->to_json().dump());\n    }\n\n    void send_response(std::shared_ptr<jsonrpcpp::Entity> response) {\n        client_metadata->send(response->to_json().dump());\n    }\n\n    void send_request(jsonrpcpp::Request *request) {\n        client_metadata->send_request(request);\n    }\n};\n\nclass Communicator {\nprivate:\n    // URL to websocket server\n    std::string websocket_url;\n    // Should be avalon plugin available?\n    // - this may change during processing if websocketet url is not set or server is down\n    bool server_available;\npublic:\n    Communicator(std::string url);\n    Communicator();\n    websocket_endpoint endpoint;\n    bool is_connected();\n    bool is_usable();\n    void connect();\n    void process_requests();\n    jsonrpcpp::Response call_method(std::string method_name, nlohmann::json params);\n    void call_notification(std::string method_name, nlohmann::json params);\n};\n\n\nCommunicator::Communicator(std::string url) {\n    // URL to websocket server\n    websocket_url = url;\n    // Should be avalon plugin available?\n    // - this may change during processing if websocketet url is not set or server is down\n    if (url == \"\") {\n        server_available = false;\n    } else {\n        server_available = true;\n    }\n}\n\n\nbool Communicator::is_connected(){\n    return endpoint.connected();\n}\n\nbool Communicator::is_usable(){\n    return server_available;\n}\n\nvoid Communicator::connect()\n{\n    if (!server_available) {\n        return;\n    }\n    int con_result;\n    con_result = endpoint.connect(websocket_url);\n    if (con_result == -1)\n    {\n        server_available = false;\n    } else {\n        server_available = true;\n    }\n}\n\nvoid Communicator::call_notification(std::string method_name, nlohmann::json params) {\n    if (!server_available || !is_connected()) {return;}\n\n    jsonrpcpp::Notification notification = {method_name, params};\n    endpoint.send_notification(&notification);\n}\n\njsonrpcpp::Response Communicator::call_method(std::string method_name, nlohmann::json params) {\n    jsonrpcpp::Response response;\n    if (!server_available || !is_connected())\n    {\n        return response;\n    }\n    int request_id = Data.client_request_id++;\n    jsonrpcpp::Request request = {request_id, method_name, params};\n    endpoint.send_request(&request);\n\n    bool found = false;\n    while (!found) {\n        std::map<int, jsonrpcpp::Response>::iterator iter = Data.responses.find(request_id);\n        if (iter != Data.responses.end()) {\n            //element found == was found response\n            response = iter->second;\n            Data.responses.erase(request_id);\n            found = true;\n        } else {\n            std::this_thread::sleep_for(std::chrono::milliseconds(100));\n        }\n    }\n    return response;\n}\n\nvoid Communicator::process_requests() {\n    if (!server_available || !is_connected() || Data.messages.empty()) {return;}\n\n    std::string msg = Data.messages.front();\n    Data.messages.pop();\n    std::cout << \"Parsing: \" << msg << std::endl;\n    // TODO: add try->except block\n    auto response = parser.parse(msg);\n    if (response->is_response()) {\n        endpoint.send_response(response);\n    } else {\n        jsonrpcpp::request_ptr request = std::dynamic_pointer_cast<jsonrpcpp::Request>(response);\n        jsonrpcpp::Error error(\"Method \\\"\" + request->method() + \"\\\" not found\", -32601);\n        jsonrpcpp::Response _response(request->id(), error);\n        endpoint.send_response(&_response);\n    }\n}\n\njsonrpcpp::response_ptr define_menu(const jsonrpcpp::Id &id, const jsonrpcpp::Parameter &params) {\n    /* Define plugin menu.\n\n    Menu is defined with json with \"title\" and \"menu_items\".\n    Each item in \"menu_items\" must have keys:\n    - \"callback\" - callback called with RPC when button is clicked\n    - \"label\" - label of button\n    - \"help\" - tooltip of button\n    ```\n    {\n        \"title\": \"< Menu title>\",\n        \"menu_items\": [\n            {\n                \"callback\": \"workfiles_tool\",\n                \"label\": \"Workfiles\",\n                \"help\": \"Open workfiles tool\"\n            },\n            ...\n        ]\n    }\n    ```\n    */\n    Data.menuItems = params.to_json()[0];\n    Data.newMenuItems = true;\n\n    std::string output;\n\n    return std::make_shared<jsonrpcpp::Response>(id, output);\n}\n\njsonrpcpp::response_ptr execute_george(const jsonrpcpp::Id &id, const jsonrpcpp::Parameter &params) {\n    const char *george_script;\n    char cmd_output[1024] = {0};\n    char empty_char = {0};\n    std::string std_george_script;\n    std::string output;\n\n    nlohmann::json json_params = params.to_json();\n    std_george_script = json_params[0];\n    george_script = std_george_script.c_str();\n\n    // Result of `TVSendCmd` is int with length of output string\n    TVSendCmd(Data.current_filter, george_script, cmd_output);\n\n    for (int i = 0; i < sizeof(cmd_output); i++)\n    {\n        if (cmd_output[i] == empty_char){\n            break;\n        }\n        output += cmd_output[i];\n    }\n    return std::make_shared<jsonrpcpp::Response>(id, output);\n}\n\nvoid register_callbacks(){\n    parser.register_request_callback(\"define_menu\", define_menu);\n    parser.register_request_callback(\"execute_george\", execute_george);\n}\n\nCommunicator* communication = nullptr;\n\n////////////////////////////////////////////////////////////////////////////////////////\n\nstatic char* GetLocalString( PIFilter* iFilter, int iNum, char* iDefault )\n{\n    char*  str;\n\n    if( Data.mLocalFile == NULL )\n        return  iDefault;\n\n    str = TVGetLocalString( iFilter, Data.mLocalFile, iNum );\n    if( str == NULL  ||  strlen( str ) == 0 )\n        return  iDefault;\n\n    return  str;\n}\n\n/**************************************************************************************/\n//  Localisation\n\n// numbers (like 10011) are IDs in the localized file.\n// strings are the default values to use when the ID is not found\n// in the localized file (or the localized file doesn't exist).\nstd::string label_from_evn()\n{\n    std::string _plugin_label = \"OpenPype\";\n    if (std::getenv(\"AVALON_LABEL\") && std::getenv(\"AVALON_LABEL\") != \"\")\n    {\n        _plugin_label = std::getenv(\"AVALON_LABEL\");\n    }\n    return _plugin_label;\n}\nstd::string plugin_label = label_from_evn();\n\n#define TXT_REQUESTER               GetLocalString( iFilter, 100, \"OpenPype Tools\" )\n\n#define TXT_REQUESTER_ERROR         GetLocalString( iFilter, 30001, \"Can't Open Requester !\" )\n\n////////////////////////////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////////////////////////////\n\n// The functions directly called by Aura through the plugin interface\n\n\n\n/**************************************************************************************/\n// \"About\" function.\n\n\nvoid FAR PASCAL PI_About( PIFilter* iFilter )\n{\n    char  text[256];\n\n    sprintf( text, \"%s %d,%d\", iFilter->PIName, iFilter->PIVersion, iFilter->PIRevision );\n\n    // Just open a warning popup with the filter name and version.\n    // You can open a much nicer requester if you want.\n    TVWarning( iFilter, text );\n}\n\n\n/**************************************************************************************/\n// Function called at Aura startup, when the filter is loaded.\n// Should do as little as possible to keep Aura's startup time small.\n\nint FAR PASCAL PI_Open( PIFilter* iFilter )\n{\n    Data.current_filter = iFilter;\n    char  tmp[256];\n\n    strcpy( iFilter->PIName, plugin_label.c_str() );\n    iFilter->PIVersion = 1;\n    iFilter->PIRevision = 0;\n\n    // If this plugin was the one open at Aura shutdown, re-open it\n    TVReadUserString( iFilter, iFilter->PIName, \"Open\", tmp, \"0\", 255 );\n    if( atoi( tmp ) )\n    {\n        PI_Parameters( iFilter, NULL ); // NULL as iArg means \"open the requester\"\n    }\n    char *env_value = std::getenv(\"WEBSOCKET_URL\");\n    if (env_value != NULL) {\n        communication = new Communicator(env_value);\n        communication->connect();\n        register_callbacks();\n    }\n    return  1; // OK\n}\n\n\n/**************************************************************************************/\n// Aura shutdown: we make all the necessary cleanup\n\nvoid FAR PASCAL PI_Close( PIFilter* iFilter )\n{\n    if( Data.mLocalFile )\n    {\n        TVCloseLocalFile( iFilter, Data.mLocalFile );\n    }\n    if( Data.mReq )\n    {\n        TVCloseReq( iFilter, Data.mReq );\n    }\n    if (communication != nullptr) {\n        communication->endpoint.close_connection();\n        delete communication;\n    }\n}\n\n\nint newMenuItemsProcess(PIFilter* iFilter) {\n    // Menu items defined with `define_menu` should be propagated.\n\n    // Change flag that there are new menu items (avoid infinite loop)\n    Data.newMenuItems = false;\n    // Skip if requester does not exists\n    if (Data.mReq == 0) {\n        return 0;\n    }\n    // Remove all previous menu items\n    for (int menu_id : Data.menuItemsIds)\n    {\n        TVRemoveButtonReq(iFilter, Data.mReq, menu_id);\n    }\n    // Clear caches\n    Data.menuItemsById.clear();\n    Data.menuItemsIds.clear();\n\n    // We use a variable to contains the vertical position of the buttons.\n    // Each time we create a button, we add its size to this variable.\n    // This makes it very easy to add/remove/displace buttons in a requester.\n    int x_pos = 9;\n    int y_pos = 5;\n\n    // Menu width\n    int menu_width = 185;\n    // Single menu item width\n    int btn_width = menu_width - 19;\n    // Single row height (btn height is 18)\n    int row_height = 20;\n    // Additional height to menu\n    int height_offset = 5;\n\n    // This is a very simple requester, so we create it's content right here instead\n    // of waiting for the PICBREQ_OPEN message...\n    // Not recommended for more complex requesters. (see the other examples)\n\n    const char *menu_title = TXT_REQUESTER;\n    if (Data.menuItems.contains(\"title\"))\n    {\n        menu_title = Data.menuItems[\"title\"].get<nlohmann::json::string_t*>()->c_str();\n    }\n    // Sets the title of the requester.\n    TVSetReqTitle( iFilter, Data.mReq, menu_title );\n\n    // Resize menu\n    // First get current position and sizes (we only need the position)\n    int current_x = 0;\n    int current_y = 0;\n    int current_width = 0;\n    int current_height = 0;\n    TVInfoReq(iFilter, Data.mReq, &current_x, &current_y, &current_width, &current_height);\n\n    // Calculate new height\n    int menu_height = (row_height * Data.menuItems[\"menu_items\"].size()) + height_offset;\n    // Resize\n    TVResizeReq(iFilter, Data.mReq, current_x, current_y, menu_width, menu_height);\n\n    // Add menu items\n    int item_counter = 1;\n    for (auto& item : Data.menuItems[\"menu_items\"].items())\n    {\n        int item_id = item_counter * 10;\n        item_counter ++;\n        std::string item_id_str = std::to_string(item_id);\n        nlohmann::json item_data = item.value();\n        const char *item_label = item_data[\"label\"].get<nlohmann::json::string_t*>()->c_str();\n        const char *help_text = item_data[\"help\"].get<nlohmann::json::string_t*>()->c_str();\n        std::string item_callback = item_data[\"callback\"].get<std::string>();\n        TVAddButtonReq(iFilter, Data.mReq, x_pos, y_pos, btn_width, 0, item_id, PIRBF_BUTTON_NORMAL|PIRBF_BUTTON_ACTION, item_label);\n        TVSetButtonInfoText( iFilter, Data.mReq, item_id, help_text );\n        y_pos += row_height;\n\n        Data.menuItemsById[std::to_string(item_id)] = item_callback;\n        Data.menuItemsIds.push_back(item_id);\n    }\n\n    return 1;\n}\n\n/**************************************************************************************/\n// we have something to do !\n\nint FAR PASCAL PI_Parameters( PIFilter* iFilter, char* iArg )\n{\n    if( !iArg )\n    {\n\n        // If the requester is not open, we open it.\n        if( Data.mReq == 0)\n        {\n            // Create empty requester because menu items are defined with\n            //  `define_menu` callback\n            DWORD  req = TVOpenFilterReqEx(\n                    iFilter,\n                    185,\n                    20,\n                    NULL,\n                    NULL,\n                    PIRF_STANDARD_REQ | PIRF_COLLAPSABLE_REQ,\n                    FILTERREQ_NO_TBAR\n            );\n            if( req == 0 )\n            {\n                TVWarning( iFilter, TXT_REQUESTER_ERROR );\n                return  0;\n            }\n\n            Data.mReq = req;\n\n            // This is a very simple requester, so we create it's content right here instead\n            // of waiting for the PICBREQ_OPEN message...\n            // Not recommended for more complex requesters. (see the other examples)\n\n            // Sets the title of the requester.\n            TVSetReqTitle( iFilter, Data.mReq, TXT_REQUESTER );\n            // Request to listen to ticks\n            TVGrabTicks(iFilter, req, PITICKS_FLAG_ON);\n\n            if ( Data.firstParams == true ) {\n                Data.firstParams = false;\n            } else {\n                newMenuItemsProcess(iFilter);\n            }\n        }\n        else\n        {\n            // If it is already open, we just put it on front of all other requesters.\n            TVReqToFront( iFilter, Data.mReq );\n        }\n    }\n\n    return  1;\n}\n\n/**************************************************************************************/\n// something happened that needs our attention.\n// Global variable where current button up data are stored\nstd::string button_up_item_id_str;\nint FAR PASCAL PI_Msg( PIFilter* iFilter, INTPTR iEvent, INTPTR iReq, INTPTR* iArgs )\n{\n    Data.current_filter = iFilter;\n    // what did happen ?\n    switch( iEvent )\n    {\n        // The user just 'clicked' on a normal button\n        case PICBREQ_BUTTON_UP:\n            button_up_item_id_str = std::to_string(iArgs[0]);\n            if (Data.menuItemsById.contains(button_up_item_id_str))\n            {\n                std::string callback_name = Data.menuItemsById[button_up_item_id_str].get<std::string>();\n                communication->call_method(callback_name, nlohmann::json::array());\n            }\n            TVExecute( iFilter );\n            break;\n\n            // The requester was just closed.\n        case PICBREQ_CLOSE:\n            // requester doesn't exists anymore\n            Data.mReq = 0;\n\n            char  tmp[256];\n            // Save the requester state (opened or closed)\n            // iArgs[4] contains a flag which tells us if the requester\n            // has been closed by the user (flag=0) or by Aura's shutdown (flag=1).\n            // If it was by Aura's shutdown, that means this requester was the\n            // last one open, so we should reopen this one the next time Aura\n            // is started.  Else we won't open it next time.\n            sprintf( tmp, \"%d\", (int)(iArgs[4]) );\n\n            // Save it in Aura's init file.\n            TVWriteUserString( iFilter, iFilter->PIName, \"Open\", tmp );\n            break;\n\n        case PICBREQ_TICKS:\n            if (Data.newMenuItems)\n            {\n                newMenuItemsProcess(iFilter);\n            }\n            if (communication != nullptr) {\n                communication->process_requests();\n            }\n    }\n\n    return  1;\n}\n\n\n/**************************************************************************************/\n// Start of the 'execution' of the filter for a new sequence.\n// - iNumImages contains the total number of frames to be processed.\n// Here you should allocate memory that is used for all frames,\n// and precompute all the stuff that doesn't change from frame to frame.\n\n\nint FAR PASCAL PI_SequenceStart( PIFilter* iFilter, int iNumImages )\n{\n    // In this simple example we don't have anything to allocate/precompute.\n\n    // 1 means 'continue', 0 means 'error, abort' (like 'not enough memory')\n    return  1;\n}\n\n\n// Here you should cleanup what you've done in PI_SequenceStart\n\nvoid FAR PASCAL PI_SequenceFinish( PIFilter* iFilter )\n{}\n\n\n/**************************************************************************************/\n// This is called before each frame.\n// Here you should allocate memory and precompute all the stuff you can.\n\nint FAR PASCAL PI_Start( PIFilter* iFilter, double iPos, double iSize )\n{\n    return  1;\n}\n\n\nvoid FAR PASCAL PI_Finish( PIFilter* iFilter )\n{\n    // nothing special to cleanup\n}\n\n\n/**************************************************************************************/\n// 'Execution' of the filter.\nint FAR PASCAL PI_Work( PIFilter* iFilter )\n{\n    return  1;\n}\n"
  },
  {
    "path": "openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.def",
    "content": "LIBRARY      Avalonplugin\nEXPORTS\n  PI_Msg\n  PI_Open\n  PI_About\n  PI_Parameters\n  PI_Start\n  PI_Work\n  PI_Finish\n  PI_Close\n"
  },
  {
    "path": "openpype/hosts/tvpaint/worker/__init__.py",
    "content": "from .worker_job import (\n    JobFailed,\n    ExecuteSimpleGeorgeScript,\n    ExecuteGeorgeScript,\n    CollectSceneData,\n    SenderTVPaintCommands,\n    ProcessTVPaintCommands\n)\n\nfrom .worker import main\n\n__all__ = (\n    \"JobFailed\",\n    \"ExecuteSimpleGeorgeScript\",\n    \"ExecuteGeorgeScript\",\n    \"CollectSceneData\",\n    \"SenderTVPaintCommands\",\n    \"ProcessTVPaintCommands\",\n\n    \"main\"\n)\n"
  },
  {
    "path": "openpype/hosts/tvpaint/worker/worker.py",
    "content": "import os\nimport signal\nimport time\nimport tempfile\nimport shutil\nimport asyncio\n\nfrom openpype.hosts.tvpaint.api.communication_server import (\n    BaseCommunicator,\n    CommunicationWrapper\n)\nfrom openpype_modules.job_queue.job_workers import WorkerJobsConnection\n\nfrom .worker_job import ProcessTVPaintCommands\n\n\nclass TVPaintWorkerCommunicator(BaseCommunicator):\n    \"\"\"Modified commuicator which cares about processing jobs.\n\n    Received jobs are send to TVPaint by parsing 'ProcessTVPaintCommands'.\n    \"\"\"\n    def __init__(self, server_url):\n        super().__init__()\n\n        self.return_code = 1\n        self._server_url = server_url\n        self._worker_connection = None\n\n    def _start_webserver(self):\n        \"\"\"Create connection to workers server before TVPaint server.\"\"\"\n        loop = self.websocket_server.loop\n        self._worker_connection = WorkerJobsConnection(\n            self._server_url, \"tvpaint\", loop\n        )\n        asyncio.ensure_future(\n            self._worker_connection.main_loop(register_worker=False),\n            loop=loop\n        )\n\n        super()._start_webserver()\n\n    def _open_init_file(self):\n        \"\"\"Open init TVPaint file.\n\n        File triggers dialog missing path to audio file which must be closed\n        once and is ignored for rest of running process.\n        \"\"\"\n        current_dir = os.path.dirname(os.path.abspath(__file__))\n        init_filepath = os.path.join(current_dir, \"init_file.tvpp\")\n        with tempfile.NamedTemporaryFile(\n            mode=\"w\", prefix=\"a_tvp_\", suffix=\".tvpp\"\n        ) as tmp_file:\n            tmp_filepath = tmp_file.name.replace(\"\\\\\", \"/\")\n\n        shutil.copy(init_filepath, tmp_filepath)\n        george_script = \"tv_LoadProject '\\\"'\\\"{}\\\"'\\\"'\".format(tmp_filepath)\n        self.execute_george_through_file(george_script)\n        self.execute_george(\"tv_projectclose\")\n        os.remove(tmp_filepath)\n\n    def _on_client_connect(self, *args, **kwargs):\n        super()._on_client_connect(*args, **kwargs)\n        self._open_init_file()\n        # Register as \"ready to work\" worker\n        self._worker_connection.register_as_worker()\n\n    def stop(self):\n        \"\"\"Stop worker connection and TVPaint server.\"\"\"\n        self._worker_connection.stop()\n        self.return_code = 0\n        super().stop()\n\n    @property\n    def current_job(self):\n        \"\"\"Retrieve job which should be processed.\"\"\"\n        if self._worker_connection:\n            return self._worker_connection.current_job\n        return None\n\n    def _check_process(self):\n        if self.process is None:\n            return True\n\n        if self.process.poll() is not None:\n            asyncio.ensure_future(\n                self._worker_connection.disconnect(),\n                loop=self.websocket_server.loop\n            )\n            self._exit()\n            return False\n        return True\n\n    def _process_job(self):\n        job = self.current_job\n        if job is None:\n            return\n\n        # Prepare variables used for sendig\n        success = False\n        message = \"Unknown function\"\n        data = None\n        job_data = job[\"data\"]\n        workfile = job_data[\"workfile\"]\n        # Currently can process only \"commands\" function\n        if job_data.get(\"function\") == \"commands\":\n            try:\n                commands = ProcessTVPaintCommands(\n                    workfile, job_data[\"commands\"], self\n                )\n                commands.execute()\n                data = commands.response_data()\n                success = True\n                message = \"Executed\"\n\n            except Exception as exc:\n                message = \"Error on worker: {}\".format(str(exc))\n\n        self._worker_connection.finish_job(success, message, data)\n\n    def main_loop(self):\n        \"\"\"Main loop where jobs are processed.\n\n        Server is stopped by killing this process or TVPaint process.\n        \"\"\"\n        while self.server_is_running:\n            if self._check_process():\n                self._process_job()\n            time.sleep(1)\n\n        return self.return_code\n\n\ndef _start_tvpaint(tvpaint_executable_path, server_url):\n    communicator = TVPaintWorkerCommunicator(server_url)\n    CommunicationWrapper.set_communicator(communicator)\n    communicator.launch([tvpaint_executable_path])\n\n\ndef main(tvpaint_executable_path, server_url):\n    # Register terminal signal handler\n    def signal_handler(*_args):\n        print(\"Termination signal received. Stopping.\")\n        if CommunicationWrapper.communicator is not None:\n            CommunicationWrapper.communicator.stop()\n\n    signal.signal(signal.SIGINT, signal_handler)\n    signal.signal(signal.SIGTERM, signal_handler)\n\n    _start_tvpaint(tvpaint_executable_path, server_url)\n\n    communicator = CommunicationWrapper.communicator\n    if communicator is None:\n        print(\"Communicator is not set\")\n        return 1\n\n    return communicator.main_loop()\n"
  },
  {
    "path": "openpype/hosts/tvpaint/worker/worker_job.py",
    "content": "import os\nimport tempfile\nimport inspect\nimport copy\nimport json\nimport time\nfrom uuid import uuid4\nfrom abc import ABCMeta, abstractmethod, abstractproperty\n\nimport six\n\nfrom openpype.lib import Logger\nfrom openpype.modules import ModulesManager\n\n\nTMP_FILE_PREFIX = \"opw_tvp_\"\n\n\nclass JobFailed(Exception):\n    \"\"\"Raised when job was sent and finished unsuccessfully.\"\"\"\n    def __init__(self, job_status):\n        job_state = job_status[\"state\"]\n        job_message = job_status[\"message\"] or \"Unknown issue\"\n        error_msg = (\n            \"Job didn't finish properly.\"\n            \" Job state: \\\"{}\\\" | Job message: \\\"{}\\\"\"\n        ).format(job_state, job_message)\n\n        self.job_status = job_status\n\n        super().__init__(error_msg)\n\n\n@six.add_metaclass(ABCMeta)\nclass BaseCommand:\n    \"\"\"Abstract TVPaint command which can be executed through worker.\n\n    Each command must have unique name and implemented 'execute' and\n    'from_existing' methods.\n\n    Command also have id which is created on command creation.\n\n    The idea is that command is just a data container on sender side send\n    through server to a worker where is replicated one by one, executed and\n    result sent back to sender through server.\n    \"\"\"\n    @abstractproperty\n    def name(self):\n        \"\"\"Command name (must be unique).\"\"\"\n        pass\n\n    def __init__(self, data=None):\n        if data is None:\n            data = {}\n        else:\n            data = copy.deepcopy(data)\n\n        # Use 'id' from data when replicating on process side\n        command_id = data.get(\"id\")\n        if command_id is None:\n            command_id = str(uuid4())\n        data[\"id\"] = command_id\n        data[\"command\"] = self.name\n\n        self._parent = None\n        self._result = None\n        self._command_data = data\n        self._done = False\n\n    def job_queue_root(self):\n        \"\"\"Access to job queue root.\n\n        Job queue root is shared access point to files shared across senders\n        and workers.\n        \"\"\"\n        if self._parent is None:\n            return None\n        return self._parent.job_queue_root()\n\n    def set_parent(self, parent):\n        self._parent = parent\n\n    @property\n    def id(self):\n        \"\"\"Command id.\"\"\"\n        return self._command_data[\"id\"]\n\n    @property\n    def parent(self):\n        \"\"\"Parent of command expected type of 'TVPaintCommands'.\"\"\"\n        return self._parent\n\n    @property\n    def communicator(self):\n        \"\"\"TVPaint communicator.\n\n        Available only on worker side.\n        \"\"\"\n        return self._parent.communicator\n\n    @property\n    def done(self):\n        \"\"\"Is command done.\"\"\"\n        return self._done\n\n    def set_done(self):\n        \"\"\"Change state of done.\"\"\"\n        self._done = True\n\n    def set_result(self, result):\n        \"\"\"Set result of executed command.\"\"\"\n        self._result = result\n\n    def result(self):\n        \"\"\"Result of command.\"\"\"\n        return copy.deepcopy(self._result)\n\n    def response_data(self):\n        \"\"\"Data send as response to sender.\"\"\"\n        return {\n            \"id\": self.id,\n            \"result\": self._result,\n            \"done\": self._done\n        }\n\n    def command_data(self):\n        \"\"\"Raw command data.\"\"\"\n        return copy.deepcopy(self._command_data)\n\n    @abstractmethod\n    def execute(self):\n        \"\"\"Execute command on worker side.\"\"\"\n        pass\n\n    @classmethod\n    @abstractmethod\n    def from_existing(cls, data):\n        \"\"\"Recreate object based on passed data.\"\"\"\n        pass\n\n    def execute_george(self, george_script):\n        \"\"\"Execute george script in TVPaint.\"\"\"\n        return self.parent.execute_george(george_script)\n\n    def execute_george_through_file(self, george_script):\n        \"\"\"Execute george script through temp file in TVPaint.\"\"\"\n        return self.parent.execute_george_through_file(george_script)\n\n\nclass ExecuteSimpleGeorgeScript(BaseCommand):\n    \"\"\"Execute simple george script in TVPaint.\n\n    Args:\n        script(str): Script that will be executed.\n    \"\"\"\n    name = \"execute_george_simple\"\n\n    def __init__(self, script, data=None):\n        data = data or {}\n        data[\"script\"] = script\n        self._script = script\n        super().__init__(data)\n\n    def execute(self):\n        self._result = self.execute_george(self._script)\n\n    @classmethod\n    def from_existing(cls, data):\n        script = data.pop(\"script\")\n        return cls(script, data)\n\n\nclass ExecuteGeorgeScript(BaseCommand):\n    \"\"\"Execute multiline george script in TVPaint.\n\n    Args:\n        script_lines(list): Lines that will be executed in george script\n            through temp george file.\n        tmp_file_keys(list): List of formatting keys in george script that\n            require replacement with path to a temp file where result will be\n            stored. The content of file is stored to result by the key.\n        root_dir_key(str): Formatting key that will be replaced in george\n            script with job queue root which can be different on worker side.\n        data(dict): Raw data about command.\n    \"\"\"\n    name = \"execute_george_through_file\"\n\n    def __init__(\n        self, script_lines, tmp_file_keys=None, root_dir_key=None, data=None\n    ):\n        data = data or {}\n        if not tmp_file_keys:\n            tmp_file_keys = data.get(\"tmp_file_keys\") or []\n\n        data[\"script_lines\"] = script_lines\n        data[\"tmp_file_keys\"] = tmp_file_keys\n        data[\"root_dir_key\"] = root_dir_key\n        self._script_lines = script_lines\n        self._tmp_file_keys = tmp_file_keys\n        self._root_dir_key = root_dir_key\n        super().__init__(data)\n\n    def execute(self):\n        filepath_by_key = {}\n        script = self._script_lines\n        if isinstance(script, list):\n            script = \"\\n\".join(script)\n\n        # Replace temporary files in george script\n        for key in self._tmp_file_keys:\n            output_file = tempfile.NamedTemporaryFile(\n                mode=\"w\", prefix=TMP_FILE_PREFIX, suffix=\".txt\", delete=False\n            )\n            output_file.close()\n            format_key = \"{\" + key + \"}\"\n            output_path = output_file.name.replace(\"\\\\\", \"/\")\n            script = script.replace(format_key, output_path)\n            filepath_by_key[key] = output_path\n\n        # Replace job queue root in script\n        if self._root_dir_key:\n            job_queue_root = self.job_queue_root()\n            format_key = \"{\" + self._root_dir_key + \"}\"\n            script = script.replace(\n                format_key, job_queue_root.replace(\"\\\\\", \"/\")\n            )\n\n        # Execute the script\n        self.execute_george_through_file(script)\n\n        # Store result of temporary files\n        result = {}\n        for key, filepath in filepath_by_key.items():\n            with open(filepath, \"r\") as stream:\n                data = stream.read()\n            result[key] = data\n            os.remove(filepath)\n\n        self._result = result\n\n    @classmethod\n    def from_existing(cls, data):\n        \"\"\"Recreate the object from data.\"\"\"\n        script_lines = data.pop(\"script_lines\")\n        tmp_file_keys = data.pop(\"tmp_file_keys\", None)\n        root_dir_key = data.pop(\"root_dir_key\", None)\n        return cls(script_lines, tmp_file_keys, root_dir_key, data)\n\n\nclass CollectSceneData(BaseCommand):\n    \"\"\"Helper command which will collect all useful info about workfile.\n\n    Result is dictionary with all layers data, exposure frames by layer ids\n    pre/post behavior of layers by their ids, group information and scene data.\n    \"\"\"\n    name = \"collect_scene_data\"\n\n    def execute(self):\n        from openpype.hosts.tvpaint.api.lib import (\n            get_layers_data,\n            get_groups_data,\n            get_layers_pre_post_behavior,\n            get_layers_exposure_frames,\n            get_scene_data\n        )\n\n        groups_data = get_groups_data(communicator=self.communicator)\n        layers_data = get_layers_data(communicator=self.communicator)\n        layer_ids = [\n            layer_data[\"layer_id\"]\n            for layer_data in layers_data\n        ]\n        pre_post_beh_by_layer_id = get_layers_pre_post_behavior(\n            layer_ids, communicator=self.communicator\n        )\n        exposure_frames_by_layer_id = get_layers_exposure_frames(\n            layer_ids, layers_data, communicator=self.communicator\n        )\n\n        self._result = {\n            \"layers_data\": layers_data,\n            \"exposure_frames_by_layer_id\": exposure_frames_by_layer_id,\n            \"pre_post_beh_by_layer_id\": pre_post_beh_by_layer_id,\n            \"groups_data\": groups_data,\n            \"scene_data\": get_scene_data(self.communicator)\n        }\n\n    @classmethod\n    def from_existing(cls, data):\n        return cls(data)\n\n\n@six.add_metaclass(ABCMeta)\nclass TVPaintCommands:\n    \"\"\"Wrapper around TVPaint commands to be able send multiple commands.\n\n    Commands may send one or multiple commands at once. Also gives api access\n    for commands info.\n\n    Base for sender and receiver which are extending the logic for their\n    purposes. One of differences is preparation of workfile path.\n\n    Args:\n        workfile(str): Path to workfile.\n        job_queue_module(JobQueueModule): Object of OpenPype module JobQueue.\n    \"\"\"\n    def __init__(self, workfile, job_queue_module=None):\n        self._log = None\n        self._commands = []\n        self._command_classes_by_name = None\n        if job_queue_module is None:\n            manager = ModulesManager()\n            job_queue_module = manager.modules_by_name[\"job_queue\"]\n        self._job_queue_module = job_queue_module\n\n        self._workfile = self._prepare_workfile(workfile)\n\n    @abstractmethod\n    def _prepare_workfile(self, workfile):\n        \"\"\"Modification of workfile path on initialization to match platorm.\"\"\"\n        pass\n\n    def job_queue_root(self):\n        \"\"\"Job queue root for current platform using current settings.\"\"\"\n        return self._job_queue_module.get_jobs_root_from_settings()\n\n    @property\n    def log(self):\n        \"\"\"Access to logger object.\"\"\"\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    @property\n    def classes_by_name(self):\n        \"\"\"Prepare commands classes for validation and recreation of commands.\n\n        It is expected that all commands are defined in this python file so\n        we're looking for all implementation of BaseCommand in globals.\n        \"\"\"\n        if self._command_classes_by_name is None:\n            command_classes_by_name = {}\n            for attr in globals().values():\n                if (\n                    not inspect.isclass(attr)\n                    or not issubclass(attr, BaseCommand)\n                    or attr is BaseCommand\n                ):\n                    continue\n\n                if inspect.isabstract(attr):\n                    self.log.debug(\n                        \"Skipping abstract class {}\".format(attr.__name__)\n                    )\n                command_classes_by_name[attr.name] = attr\n            self._command_classes_by_name = command_classes_by_name\n\n        return self._command_classes_by_name\n\n    def add_command(self, command):\n        \"\"\"Add command to process.\"\"\"\n        command.set_parent(self)\n        self._commands.append(command)\n\n    def result(self):\n        \"\"\"Result of commands in list in which they were processed.\"\"\"\n        return [\n            command.result()\n            for command in self._commands\n        ]\n\n    def response_data(self):\n        \"\"\"Data which should be send from worker.\"\"\"\n        return [\n            command.response_data()\n            for command in self._commands\n        ]\n\n\nclass SenderTVPaintCommands(TVPaintCommands):\n    \"\"\"Sender implementation of TVPaint Commands.\"\"\"\n    def _prepare_workfile(self, workfile):\n        \"\"\"Remove job queue root from workfile path.\n\n        It is expected that worker will add it's root before passed workfile.\n        \"\"\"\n        new_workfile = workfile.replace(\"\\\\\", \"/\")\n        job_queue_root = self.job_queue_root().replace(\"\\\\\", \"/\")\n        if job_queue_root not in new_workfile:\n            raise ValueError((\n                \"Workfile is not located in JobQueue root.\"\n                \" Workfile path: \\\"{}\\\". JobQueue root: \\\"{}\\\"\"\n            ).format(workfile, job_queue_root))\n        return new_workfile.replace(job_queue_root, \"\")\n\n    def commands_data(self):\n        \"\"\"Commands data to be able recreate them.\"\"\"\n        return [\n            command.command_data()\n            for command in self._commands\n        ]\n\n    def to_job_data(self):\n        \"\"\"Convert commands to job data before sending to workers server.\"\"\"\n        return {\n            \"workfile\": self._workfile,\n            \"function\": \"commands\",\n            \"commands\": self.commands_data()\n        }\n\n    def set_result(self, result):\n        commands_by_id = {\n            command.id: command\n            for command in self._commands\n        }\n\n        for item in result:\n            command = commands_by_id[item[\"id\"]]\n            command.set_result(item[\"result\"])\n            command.set_done()\n\n    def _send_job(self):\n        \"\"\"Send job to a workers server.\"\"\"\n        # Send job data to job queue server\n        job_data = self.to_job_data()\n        self.log.debug(\"Sending job to JobQueue server.\\n{}\".format(\n            json.dumps(job_data, indent=4)\n        ))\n        job_id = self._job_queue_module.send_job(\"tvpaint\", job_data)\n        self.log.info((\n            \"Job sent to JobQueue server and got id \\\"{}\\\".\"\n            \" Waiting for finishing the job.\"\n        ).format(job_id))\n\n        return job_id\n\n    def send_job_and_wait(self):\n        \"\"\"Send job to workers server and wait for response.\n\n        Result of job is stored into the object.\n\n        Raises:\n            JobFailed: When job was finished but not successfully.\n        \"\"\"\n        job_id = self._send_job()\n        while True:\n            job_status = self._job_queue_module.get_job_status(job_id)\n            if job_status[\"done\"]:\n                break\n            time.sleep(1)\n\n        # Check if job state is done\n        if job_status[\"state\"] != \"done\":\n            raise JobFailed(job_status)\n\n        self.set_result(job_status[\"result\"])\n\n        self.log.debug(\"Job is done and result is stored.\")\n\n\nclass ProcessTVPaintCommands(TVPaintCommands):\n    \"\"\"Worker side of TVPaint Commands.\n\n    It is expected this object is created only on worker's side from existing\n    data loaded from job.\n\n    Workfile path logic is based on 'SenderTVPaintCommands'.\n    \"\"\"\n    def __init__(self, workfile, commands, communicator):\n        super(ProcessTVPaintCommands, self).__init__(workfile)\n\n        self._communicator = communicator\n\n        self.commands_from_data(commands)\n\n    def _prepare_workfile(self, workfile):\n        \"\"\"Preprend job queue root before passed workfile.\"\"\"\n        workfile = workfile.replace(\"\\\\\", \"/\")\n        job_queue_root = self.job_queue_root().replace(\"\\\\\", \"/\")\n        new_workfile = \"/\".join([job_queue_root, workfile])\n        while \"//\" in new_workfile:\n            new_workfile = new_workfile.replace(\"//\", \"/\")\n        return os.path.normpath(new_workfile)\n\n    @property\n    def communicator(self):\n        \"\"\"Access to TVPaint communicator.\"\"\"\n        return self._communicator\n\n    def commands_from_data(self, commands_data):\n        \"\"\"Recreate command from passed data.\"\"\"\n        for command_data in commands_data:\n            command_name = command_data[\"command\"]\n\n            klass = self.classes_by_name[command_name]\n            command = klass.from_existing(command_data)\n            self.add_command(command)\n\n    def execute_george(self, george_script):\n        \"\"\"Helper method to execute george script.\"\"\"\n        return self.communicator.execute_george(george_script)\n\n    def execute_george_through_file(self, george_script):\n        \"\"\"Helper method to execute george script through temp file.\"\"\"\n        temporary_file = tempfile.NamedTemporaryFile(\n            mode=\"w\", prefix=TMP_FILE_PREFIX, suffix=\".grg\", delete=False\n        )\n        temporary_file.write(george_script)\n        temporary_file.close()\n        temp_file_path = temporary_file.name.replace(\"\\\\\", \"/\")\n        self.execute_george(\"tv_runscript {}\".format(temp_file_path))\n        os.remove(temp_file_path)\n\n    def _open_workfile(self):\n        \"\"\"Open workfile in TVPaint.\"\"\"\n        workfile = self._workfile\n        print(\"Opening workfile {}\".format(workfile))\n        george_script = \"tv_LoadProject '\\\"'\\\"{}\\\"'\\\"'\".format(workfile)\n        self.execute_george_through_file(george_script)\n\n    def _close_workfile(self):\n        \"\"\"Close workfile in TVPaint.\"\"\"\n        print(\"Closing workfile\")\n        self.execute_george_through_file(\"tv_projectclose\")\n\n    def execute(self):\n        \"\"\"Execute commands.\"\"\"\n        # First open the workfile\n        self._open_workfile()\n        # Execute commands one by one\n        # TODO maybe stop processing when command fails?\n        print(\"Commands execution started ({})\".format(len(self._commands)))\n        for command in self._commands:\n            command.execute()\n            command.set_done()\n        # Finally close workfile\n        self._close_workfile()\n"
  },
  {
    "path": "openpype/hosts/unreal/README.md",
    "content": "## Unreal Integration\n\nSupported Unreal Engine version is 4.26+ (mainly because of major Python changes done there).\n\n### Project naming\nUnreal doesn't support project names starting with non-alphabetic character. So names like `123_myProject` are\ninvalid. If Ayon detects such name it automatically prepends letter **P** to make it valid name, so `123_myProject`\nwill become `P123_myProject`. There is also soft-limit on project name length to be shorter than 20 characters.\nLonger names will issue warning in Unreal Editor that there might be possible side effects.\n"
  },
  {
    "path": "openpype/hosts/unreal/__init__.py",
    "content": "from .addon import UnrealAddon\n\n\n__all__ = (\n    \"UnrealAddon\",\n)\n"
  },
  {
    "path": "openpype/hosts/unreal/addon.py",
    "content": "import os\nimport re\nfrom openpype.modules import IHostAddon, OpenPypeModule\n\nUNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass UnrealAddon(OpenPypeModule, IHostAddon):\n    name = \"unreal\"\n    host_name = \"unreal\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def get_global_environments(self):\n        return {\n            \"AYON_UNREAL_ROOT\": UNREAL_ROOT_DIR,\n        }\n\n    def add_implementation_envs(self, env, app):\n        \"\"\"Modify environments to contain all required for implementation.\"\"\"\n        # Set AYON_UNREAL_PLUGIN required for Unreal implementation\n        # Imports are in this method for Python 2 compatiblity of an addon\n        from pathlib import Path\n\n        from .lib import get_compatible_integration\n\n        from openpype.widgets.message_window import Window\n\n        pattern = re.compile(r'^\\d+-\\d+$')\n\n        if not pattern.match(app.name):\n            msg = (\n                \"Unreal application key in the settings must be in format\"\n                \"'5-0' or '5-1'\"\n            )\n            Window(\n                parent=None,\n                title=\"Unreal application name format\",\n                message=msg,\n                level=\"critical\")\n            raise ValueError(msg)\n\n        ue_version = app.name.replace(\"-\", \".\")\n        unreal_plugin_path = os.path.join(\n            UNREAL_ROOT_DIR, \"integration\", \"UE_{}\".format(ue_version), \"Ayon\"\n        )\n        if not Path(unreal_plugin_path).exists():\n            compatible_versions = get_compatible_integration(\n                ue_version, Path(UNREAL_ROOT_DIR) / \"integration\"\n            )\n            if compatible_versions:\n                unreal_plugin_path = compatible_versions[-1] / \"Ayon\"\n                unreal_plugin_path = unreal_plugin_path.as_posix()\n\n        if not env.get(\"AYON_UNREAL_PLUGIN\") or \\\n                env.get(\"AYON_UNREAL_PLUGIN\") != unreal_plugin_path:\n            env[\"AYON_UNREAL_PLUGIN\"] = unreal_plugin_path\n\n        # Set default environments if are not set via settings\n        defaults = {\n            \"OPENPYPE_LOG_NO_COLORS\": \"True\",\n            \"UE_PYTHONPATH\": os.environ.get(\"PYTHONPATH\", \"\"),\n        }\n        for key, value in defaults.items():\n            if not env.get(key):\n                env[key] = value\n\n    def get_launch_hook_paths(self, app):\n        if app.host_name != self.host_name:\n            return []\n        return [\n            os.path.join(UNREAL_ROOT_DIR, \"hooks\")\n        ]\n\n    def get_workfile_extensions(self):\n        return [\".uproject\"]\n"
  },
  {
    "path": "openpype/hosts/unreal/api/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unreal Editor Ayon host API.\"\"\"\n\nfrom .plugin import (\n    UnrealActorCreator,\n    UnrealAssetCreator,\n    Loader\n)\n\nfrom .pipeline import (\n    install,\n    uninstall,\n    ls,\n    publish,\n    containerise,\n    show_creator,\n    show_loader,\n    show_publisher,\n    show_manager,\n    show_experimental_tools,\n    show_tools_dialog,\n    show_tools_popup,\n    instantiate,\n    UnrealHost,\n    set_sequence_hierarchy,\n    generate_sequence,\n    maintained_selection\n)\n\n__all__ = [\n    \"install\",\n    \"uninstall\",\n    \"Loader\",\n    \"ls\",\n    \"publish\",\n    \"containerise\",\n    \"show_creator\",\n    \"show_loader\",\n    \"show_publisher\",\n    \"show_manager\",\n    \"show_experimental_tools\",\n    \"show_tools_dialog\",\n    \"show_tools_popup\",\n    \"instantiate\",\n    \"UnrealHost\",\n    \"set_sequence_hierarchy\",\n    \"generate_sequence\",\n    \"maintained_selection\"\n]\n"
  },
  {
    "path": "openpype/hosts/unreal/api/helpers.py",
    "content": "# -*- coding: utf-8 -*-\nimport unreal  # noqa\n\n\nclass AyonUnrealException(Exception):\n    pass\n\n\n@unreal.uclass()\nclass AyonHelpers(unreal.AyonLib):\n    \"\"\"Class wrapping some useful functions for Ayon.\n\n    This class is extending native BP class in Ayon Integration Plugin.\n\n    \"\"\"\n\n    @unreal.ufunction(params=[str, unreal.LinearColor, bool])\n    def set_folder_color(self, path: str, color: unreal.LinearColor) -> None:\n        \"\"\"Set color on folder in Content Browser.\n\n        This method sets color on folder in Content Browser. Unfortunately\n        there is no way to refresh Content Browser so new color isn't applied\n        immediately. They are saved to config file and appears correctly\n        only after Editor is restarted.\n\n        Args:\n            path (str): Path to folder\n            color (:class:`unreal.LinearColor`): Color of the folder\n\n        Example:\n\n            AyonHelpers().set_folder_color(\n                \"/Game/Path\", unreal.LinearColor(a=1.0, r=1.0, g=0.5, b=0)\n            )\n\n        Note:\n            This will take effect only after Editor is restarted. I couldn't\n            find a way to refresh it. Also, this saves the color definition\n            into the project config, binding this path with color. So if you\n            delete this path and later re-create, it will set this color\n            again.\n\n        \"\"\"\n        self.c_set_folder_color(path, color, False)\n"
  },
  {
    "path": "openpype/hosts/unreal/api/pipeline.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nimport json\nimport logging\nfrom typing import List\nfrom contextlib import contextmanager\nimport semver\nimport time\n\nimport pyblish.api\n\nfrom openpype.client import get_asset_by_name, get_assets\nfrom openpype.pipeline import (\n    register_loader_plugin_path,\n    register_creator_plugin_path,\n    register_inventory_action_path,\n    deregister_loader_plugin_path,\n    deregister_creator_plugin_path,\n    deregister_inventory_action_path,\n    AYON_CONTAINER_ID,\n    legacy_io,\n)\nfrom openpype.tools.utils import host_tools\nimport openpype.hosts.unreal\nfrom openpype.host import HostBase, ILoadHost, IPublishHost\n\nimport unreal  # noqa\n\n# Rename to Ayon once parent module renames\nlogger = logging.getLogger(\"openpype.hosts.unreal\")\n\nAYON_CONTAINERS = \"AyonContainers\"\nAYON_ASSET_DIR = \"/Game/Ayon/Assets\"\nCONTEXT_CONTAINER = \"Ayon/context.json\"\nUNREAL_VERSION = semver.VersionInfo(\n    *os.getenv(\"AYON_UNREAL_VERSION\").split(\".\")\n)\n\nHOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.unreal.__file__))\nPLUGINS_DIR = os.path.join(HOST_DIR, \"plugins\")\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nCREATE_PATH = os.path.join(PLUGINS_DIR, \"create\")\nINVENTORY_PATH = os.path.join(PLUGINS_DIR, \"inventory\")\n\n\nclass UnrealHost(HostBase, ILoadHost, IPublishHost):\n    \"\"\"Unreal host implementation.\n\n    For some time this class will re-use functions from module based\n    implementation for backwards compatibility of older unreal projects.\n    \"\"\"\n\n    name = \"unreal\"\n\n    def install(self):\n        install()\n\n    def get_containers(self):\n        return ls()\n\n    @staticmethod\n    def show_tools_popup():\n        \"\"\"Show tools popup with actions leading to show other tools.\"\"\"\n        show_tools_popup()\n\n    @staticmethod\n    def show_tools_dialog():\n        \"\"\"Show tools dialog with actions leading to show other tools.\"\"\"\n        show_tools_dialog()\n\n    def update_context_data(self, data, changes):\n        content_path = unreal.Paths.project_content_dir()\n        op_ctx = content_path + CONTEXT_CONTAINER\n        attempts = 3\n        for i in range(attempts):\n            try:\n                with open(op_ctx, \"w+\") as f:\n                    json.dump(data, f)\n                break\n            except IOError as e:\n                if i == attempts - 1:\n                    raise Exception(\n                        \"Failed to write context data. Aborting.\") from e\n                unreal.log_warning(\"Failed to write context data. Retrying...\")\n                i += 1\n                time.sleep(3)\n                continue\n\n    def get_context_data(self):\n        content_path = unreal.Paths.project_content_dir()\n        op_ctx = content_path + CONTEXT_CONTAINER\n        if not os.path.isfile(op_ctx):\n            return {}\n        with open(op_ctx, \"r\") as fp:\n            data = json.load(fp)\n        return data\n\n\ndef install():\n    \"\"\"Install Unreal configuration for OpenPype.\"\"\"\n    print(\"-=\" * 40)\n    logo = '''.\n.\n                    ·\n                    │\n                   ·∙/\n                 ·-∙•∙-·\n              / \\\\  /∙·  / \\\\\n             ∙   \\\\  │  /   ∙\n              \\\\   \\\\ · /   /\n              \\\\\\\\   ∙ ∙  //\n                \\\\\\\\/   \\\\//\n                   ___\n                  │   │\n                  │   │\n                  │   │\n                  │___│\n                    -·\n\n         ·-─═─-∙ A Y O N ∙-─═─-·\n                by  YNPUT\n.\n'''\n    print(logo)\n    print(\"installing Ayon for Unreal ...\")\n    print(\"-=\" * 40)\n    logger.info(\"installing Ayon for Unreal\")\n    pyblish.api.register_host(\"unreal\")\n    pyblish.api.register_plugin_path(str(PUBLISH_PATH))\n    register_loader_plugin_path(str(LOAD_PATH))\n    register_creator_plugin_path(str(CREATE_PATH))\n    register_inventory_action_path(str(INVENTORY_PATH))\n    _register_callbacks()\n    _register_events()\n\n\ndef uninstall():\n    \"\"\"Uninstall Unreal configuration for Ayon.\"\"\"\n    pyblish.api.deregister_plugin_path(str(PUBLISH_PATH))\n    deregister_loader_plugin_path(str(LOAD_PATH))\n    deregister_creator_plugin_path(str(CREATE_PATH))\n    deregister_inventory_action_path(str(INVENTORY_PATH))\n\n\ndef _register_callbacks():\n    \"\"\"\n    TODO: Implement callbacks if supported by UE\n    \"\"\"\n    pass\n\n\ndef _register_events():\n    \"\"\"\n    TODO: Implement callbacks if supported by UE\n    \"\"\"\n    pass\n\n\ndef ls():\n    \"\"\"List all containers.\n\n    List all found in *Content Manager* of Unreal and return\n    metadata from them. Adding `objectName` to set.\n\n    \"\"\"\n    ar = unreal.AssetRegistryHelpers.get_asset_registry()\n    # UE 5.1 changed how class name is specified\n    class_name = [\"/Script/Ayon\", \"AyonAssetContainer\"] if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor > 0 else \"AyonAssetContainer\"  # noqa\n    ayon_containers = ar.get_assets_by_class(class_name, True)\n\n    # get_asset_by_class returns AssetData. To get all metadata we need to\n    # load asset. get_tag_values() work only on metadata registered in\n    # Asset Registry Project settings (and there is no way to set it with\n    # python short of editing ini configuration file).\n    for asset_data in ayon_containers:\n        asset = asset_data.get_asset()\n        data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset)\n        data[\"objectName\"] = asset_data.asset_name\n        yield cast_map_to_str_dict(data)\n\n\ndef ls_inst():\n    ar = unreal.AssetRegistryHelpers.get_asset_registry()\n    # UE 5.1 changed how class name is specified\n    class_name = [\n        \"/Script/Ayon\",\n        \"AyonPublishInstance\"\n    ] if (\n            UNREAL_VERSION.major == 5\n            and UNREAL_VERSION.minor > 0\n    ) else \"AyonPublishInstance\"  # noqa\n    instances = ar.get_assets_by_class(class_name, True)\n\n    # get_asset_by_class returns AssetData. To get all metadata we need to\n    # load asset. get_tag_values() work only on metadata registered in\n    # Asset Registry Project settings (and there is no way to set it with\n    # python short of editing ini configuration file).\n    for asset_data in instances:\n        asset = asset_data.get_asset()\n        data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset)\n        data[\"objectName\"] = asset_data.asset_name\n        yield cast_map_to_str_dict(data)\n\n\ndef parse_container(container):\n    \"\"\"To get data from container, AyonAssetContainer must be loaded.\n\n    Args:\n        container(str): path to container\n\n    Returns:\n        dict: metadata stored on container\n    \"\"\"\n    asset = unreal.EditorAssetLibrary.load_asset(container)\n    data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset)\n    data[\"objectName\"] = asset.get_name()\n    data = cast_map_to_str_dict(data)\n\n    return data\n\n\ndef publish():\n    \"\"\"Shorthand to publish from within host.\"\"\"\n    import pyblish.util\n\n    return pyblish.util.publish()\n\n\ndef containerise(name, namespace, nodes, context, loader=None, suffix=\"_CON\"):\n    \"\"\"Bundles *nodes* (assets) into a *container* and add metadata to it.\n\n    Unreal doesn't support *groups* of assets that you can add metadata to.\n    But it does support folders that helps to organize asset. Unfortunately\n    those folders are just that - you cannot add any additional information\n    to them. Ayon Integration Plugin is providing way out - Implementing\n    `AssetContainer` Blueprint class. This class when added to folder can\n    handle metadata on it using standard\n    :func:`unreal.EditorAssetLibrary.set_metadata_tag()` and\n    :func:`unreal.EditorAssetLibrary.get_metadata_tag_values()`. It also\n    stores and monitor all changes in assets in path where it resides. List of\n    those assets is available as `assets` property.\n\n    This is list of strings starting with asset type and ending with its path:\n    `Material /Game/Ayon/Test/TestMaterial.TestMaterial`\n\n    \"\"\"\n    # 1 - create directory for container\n    root = \"/Game\"\n    container_name = f\"{name}{suffix}\"\n    new_name = move_assets_to_path(root, container_name, nodes)\n\n    # 2 - create Asset Container there\n    path = f\"{root}/{new_name}\"\n    create_container(container=container_name, path=path)\n\n    namespace = path\n\n    data = {\n        \"schema\": \"ayon:container-2.0\",\n        \"id\": AYON_CONTAINER_ID,\n        \"name\": new_name,\n        \"namespace\": namespace,\n        \"loader\": str(loader),\n        \"representation\": context[\"representation\"][\"_id\"],\n    }\n    # 3 - imprint data\n    imprint(f\"{path}/{container_name}\", data)\n    return path\n\n\ndef instantiate(root, name, data, assets=None, suffix=\"_INS\"):\n    \"\"\"Bundles *nodes* into *container*.\n\n    Marking it with metadata as publishable instance. If assets are provided,\n    they are moved to new path where `AyonPublishInstance` class asset is\n    created and imprinted with metadata.\n\n    This can then be collected for publishing by Pyblish for example.\n\n    Args:\n        root (str): root path where to create instance container\n        name (str): name of the container\n        data (dict): data to imprint on container\n        assets (list of str): list of asset paths to include in publish\n                              instance\n        suffix (str): suffix string to append to instance name\n\n    \"\"\"\n    container_name = f\"{name}{suffix}\"\n\n    # if we specify assets, create new folder and move them there. If not,\n    # just create empty folder\n    if assets:\n        new_name = move_assets_to_path(root, container_name, assets)\n    else:\n        new_name = create_folder(root, name)\n\n    path = f\"{root}/{new_name}\"\n    create_publish_instance(instance=container_name, path=path)\n\n    imprint(f\"{path}/{container_name}\", data)\n\n\ndef imprint(node, data):\n    loaded_asset = unreal.EditorAssetLibrary.load_asset(node)\n    for key, value in data.items():\n        # Support values evaluated at imprint\n        if callable(value):\n            value = value()\n        # Unreal doesn't support NoneType in metadata values\n        if value is None:\n            value = \"\"\n        unreal.EditorAssetLibrary.set_metadata_tag(\n            loaded_asset, key, str(value)\n        )\n\n    with unreal.ScopedEditorTransaction(\"Ayon containerising\"):\n        unreal.EditorAssetLibrary.save_asset(node)\n\n\ndef show_tools_popup():\n    \"\"\"Show popup with tools.\n\n    Popup will disappear on click or losing focus.\n    \"\"\"\n    from openpype.hosts.unreal.api import tools_ui\n\n    tools_ui.show_tools_popup()\n\n\ndef show_tools_dialog():\n    \"\"\"Show dialog with tools.\n\n    Dialog will stay visible.\n    \"\"\"\n    from openpype.hosts.unreal.api import tools_ui\n\n    tools_ui.show_tools_dialog()\n\n\ndef show_creator():\n    host_tools.show_creator()\n\n\ndef show_loader():\n    host_tools.show_loader(use_context=True)\n\n\ndef show_publisher():\n    host_tools.show_publish()\n\n\ndef show_manager():\n    host_tools.show_scene_inventory()\n\n\ndef show_experimental_tools():\n    host_tools.show_experimental_tools_dialog()\n\n\ndef create_folder(root: str, name: str) -> str:\n    \"\"\"Create new folder.\n\n    If folder exists, append number at the end and try again, incrementing\n    if needed.\n\n    Args:\n        root (str): path root\n        name (str): folder name\n\n    Returns:\n        str: folder name\n\n    Example:\n        >>> create_folder(\"/Game/Foo\")\n        /Game/Foo\n        >>> create_folder(\"/Game/Foo\")\n        /Game/Foo1\n\n    \"\"\"\n    eal = unreal.EditorAssetLibrary\n    index = 1\n    while True:\n        if eal.does_directory_exist(f\"{root}/{name}\"):\n            name = f\"{name}{index}\"\n            index += 1\n        else:\n            eal.make_directory(f\"{root}/{name}\")\n            break\n\n    return name\n\n\ndef move_assets_to_path(root: str, name: str, assets: List[str]) -> str:\n    \"\"\"Moving (renaming) list of asset paths to new destination.\n\n    Args:\n        root (str): root of the path (eg. `/Game`)\n        name (str): name of destination directory (eg. `Foo` )\n        assets (list of str): list of asset paths\n\n    Returns:\n        str: folder name\n\n    Example:\n        This will get paths of all assets under `/Game/Test` and move them\n        to `/Game/NewTest`. If `/Game/NewTest` already exists, then resulting\n        path will be `/Game/NewTest1`\n\n        >>> assets = unreal.EditorAssetLibrary.list_assets(\"/Game/Test\")\n        >>> move_assets_to_path(\"/Game\", \"NewTest\", assets)\n        NewTest\n\n    \"\"\"\n    eal = unreal.EditorAssetLibrary\n    name = create_folder(root, name)\n\n    unreal.log(assets)\n    for asset in assets:\n        loaded = eal.load_asset(asset)\n        eal.rename_asset(asset, f\"{root}/{name}/{loaded.get_name()}\")\n\n    return name\n\n\ndef create_container(container: str, path: str) -> unreal.Object:\n    \"\"\"Helper function to create Asset Container class on given path.\n\n    This Asset Class helps to mark given path as Container\n    and enable asset version control on it.\n\n    Args:\n        container (str): Asset Container name\n        path (str): Path where to create Asset Container. This path should\n            point into container folder\n\n    Returns:\n        :class:`unreal.Object`: instance of created asset\n\n    Example:\n\n        create_container(\n            \"/Game/modelingFooCharacter_CON\",\n            \"modelingFooCharacter_CON\"\n        )\n\n    \"\"\"\n    factory = unreal.AyonAssetContainerFactory()\n    tools = unreal.AssetToolsHelpers().get_asset_tools()\n\n    return tools.create_asset(container, path, None, factory)\n\n\ndef create_publish_instance(instance: str, path: str) -> unreal.Object:\n    \"\"\"Helper function to create Ayon Publish Instance on given path.\n\n    This behaves similarly as :func:`create_ayon_container`.\n\n    Args:\n        path (str): Path where to create Publish Instance.\n            This path should point into container folder\n        instance (str): Publish Instance name\n\n    Returns:\n        :class:`unreal.Object`: instance of created asset\n\n    Example:\n\n        create_publish_instance(\n            \"/Game/modelingFooCharacter_INST\",\n            \"modelingFooCharacter_INST\"\n        )\n\n    \"\"\"\n    factory = unreal.AyonPublishInstanceFactory()\n    tools = unreal.AssetToolsHelpers().get_asset_tools()\n    return tools.create_asset(instance, path, None, factory)\n\n\ndef cast_map_to_str_dict(umap) -> dict:\n    \"\"\"Cast Unreal Map to dict.\n\n    Helper function to cast Unreal Map object to plain old python\n    dict. This will also cast values and keys to str. Useful for\n    metadata dicts.\n\n    Args:\n        umap: Unreal Map object\n\n    Returns:\n        dict\n\n    \"\"\"\n    return {str(key): str(value) for (key, value) in umap.items()}\n\n\ndef get_subsequences(sequence: unreal.LevelSequence):\n    \"\"\"Get list of subsequences from sequence.\n\n    Args:\n        sequence (unreal.LevelSequence): Sequence\n\n    Returns:\n        list(unreal.LevelSequence): List of subsequences\n\n    \"\"\"\n    tracks = sequence.get_master_tracks()\n    subscene_track = next(\n        (\n            t\n            for t in tracks\n            if t.get_class() == unreal.MovieSceneSubTrack.static_class()\n        ),\n        None,\n    )\n    if subscene_track is not None and subscene_track.get_sections():\n        return subscene_track.get_sections()\n    return []\n\n\ndef set_sequence_hierarchy(\n    seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths\n):\n    # Get existing sequencer tracks or create them if they don't exist\n    tracks = seq_i.get_master_tracks()\n    subscene_track = None\n    visibility_track = None\n    for t in tracks:\n        if t.get_class() == unreal.MovieSceneSubTrack.static_class():\n            subscene_track = t\n        if (t.get_class() ==\n                unreal.MovieSceneLevelVisibilityTrack.static_class()):\n            visibility_track = t\n    if not subscene_track:\n        subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack)\n    if not visibility_track:\n        visibility_track = seq_i.add_master_track(\n            unreal.MovieSceneLevelVisibilityTrack)\n\n    # Create the sub-scene section\n    subscenes = subscene_track.get_sections()\n    subscene = None\n    for s in subscenes:\n        if s.get_editor_property('sub_sequence') == seq_j:\n            subscene = s\n            break\n    if not subscene:\n        subscene = subscene_track.add_section()\n        subscene.set_row_index(len(subscene_track.get_sections()))\n        subscene.set_editor_property('sub_sequence', seq_j)\n        subscene.set_range(\n            min_frame_j,\n            max_frame_j + 1)\n\n    # Create the visibility section\n    ar = unreal.AssetRegistryHelpers.get_asset_registry()\n    maps = []\n    for m in map_paths:\n        # Unreal requires to load the level to get the map name\n        unreal.EditorLevelLibrary.save_all_dirty_levels()\n        unreal.EditorLevelLibrary.load_level(m)\n        maps.append(str(ar.get_asset_by_object_path(m).asset_name))\n\n    vis_section = visibility_track.add_section()\n    index = len(visibility_track.get_sections())\n\n    vis_section.set_range(\n        min_frame_j,\n        max_frame_j + 1)\n    vis_section.set_visibility(unreal.LevelVisibility.VISIBLE)\n    vis_section.set_row_index(index)\n    vis_section.set_level_names(maps)\n\n    if min_frame_j > 1:\n        hid_section = visibility_track.add_section()\n        hid_section.set_range(\n            1,\n            min_frame_j)\n        hid_section.set_visibility(unreal.LevelVisibility.HIDDEN)\n        hid_section.set_row_index(index)\n        hid_section.set_level_names(maps)\n    if max_frame_j < max_frame_i:\n        hid_section = visibility_track.add_section()\n        hid_section.set_range(\n            max_frame_j + 1,\n            max_frame_i + 1)\n        hid_section.set_visibility(unreal.LevelVisibility.HIDDEN)\n        hid_section.set_row_index(index)\n        hid_section.set_level_names(maps)\n\n\ndef generate_sequence(h, h_dir):\n    tools = unreal.AssetToolsHelpers().get_asset_tools()\n\n    sequence = tools.create_asset(\n        asset_name=h,\n        package_path=h_dir,\n        asset_class=unreal.LevelSequence,\n        factory=unreal.LevelSequenceFactoryNew()\n    )\n\n    project_name = legacy_io.active_project()\n    asset_data = get_asset_by_name(\n        project_name,\n        h_dir.split('/')[-1],\n        fields=[\"_id\", \"data.fps\"]\n    )\n\n    start_frames = []\n    end_frames = []\n\n    elements = list(get_assets(\n        project_name,\n        parent_ids=[asset_data[\"_id\"]],\n        fields=[\"_id\", \"data.clipIn\", \"data.clipOut\"]\n    ))\n    for e in elements:\n        start_frames.append(e.get('data').get('clipIn'))\n        end_frames.append(e.get('data').get('clipOut'))\n\n        elements.extend(get_assets(\n            project_name,\n            parent_ids=[e[\"_id\"]],\n            fields=[\"_id\", \"data.clipIn\", \"data.clipOut\"]\n        ))\n\n    min_frame = min(start_frames)\n    max_frame = max(end_frames)\n\n    fps = asset_data.get('data').get(\"fps\")\n\n    sequence.set_display_rate(\n        unreal.FrameRate(fps, 1.0))\n    sequence.set_playback_start(min_frame)\n    sequence.set_playback_end(max_frame)\n\n    sequence.set_work_range_start(min_frame / fps)\n    sequence.set_work_range_end(max_frame / fps)\n    sequence.set_view_range_start(min_frame / fps)\n    sequence.set_view_range_end(max_frame / fps)\n\n    tracks = sequence.get_master_tracks()\n    track = None\n    for t in tracks:\n        if (t.get_class() ==\n                unreal.MovieSceneCameraCutTrack.static_class()):\n            track = t\n            break\n    if not track:\n        track = sequence.add_master_track(\n            unreal.MovieSceneCameraCutTrack)\n\n    return sequence, (min_frame, max_frame)\n\n\ndef _get_comps_and_assets(\n    component_class, asset_class, old_assets, new_assets, selected\n):\n    eas = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\n\n    components = []\n    if selected:\n        sel_actors = eas.get_selected_level_actors()\n        for actor in sel_actors:\n            comps = actor.get_components_by_class(component_class)\n            components.extend(comps)\n    else:\n        comps = eas.get_all_level_actors_components()\n        components = [\n            c for c in comps if isinstance(c, component_class)\n        ]\n\n    # Get all the static meshes among the old assets in a dictionary with\n    # the name as key\n    selected_old_assets = {}\n    for a in old_assets:\n        asset = unreal.EditorAssetLibrary.load_asset(a)\n        if isinstance(asset, asset_class):\n            selected_old_assets[asset.get_name()] = asset\n\n    # Get all the static meshes among the new assets in a dictionary with\n    # the name as key\n    selected_new_assets = {}\n    for a in new_assets:\n        asset = unreal.EditorAssetLibrary.load_asset(a)\n        if isinstance(asset, asset_class):\n            selected_new_assets[asset.get_name()] = asset\n\n    return components, selected_old_assets, selected_new_assets\n\n\ndef replace_static_mesh_actors(old_assets, new_assets, selected):\n    smes = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem)\n\n    static_mesh_comps, old_meshes, new_meshes = _get_comps_and_assets(\n        unreal.StaticMeshComponent,\n        unreal.StaticMesh,\n        old_assets,\n        new_assets,\n        selected\n    )\n\n    for old_name, old_mesh in old_meshes.items():\n        new_mesh = new_meshes.get(old_name)\n\n        if not new_mesh:\n            continue\n\n        smes.replace_mesh_components_meshes(\n            static_mesh_comps, old_mesh, new_mesh)\n\n\ndef replace_skeletal_mesh_actors(old_assets, new_assets, selected):\n    skeletal_mesh_comps, old_meshes, new_meshes = _get_comps_and_assets(\n        unreal.SkeletalMeshComponent,\n        unreal.SkeletalMesh,\n        old_assets,\n        new_assets,\n        selected\n    )\n\n    for old_name, old_mesh in old_meshes.items():\n        new_mesh = new_meshes.get(old_name)\n\n        if not new_mesh:\n            continue\n\n        for comp in skeletal_mesh_comps:\n            if comp.get_skeletal_mesh_asset() == old_mesh:\n                comp.set_skeletal_mesh_asset(new_mesh)\n\n\ndef replace_geometry_cache_actors(old_assets, new_assets, selected):\n    geometry_cache_comps, old_caches, new_caches = _get_comps_and_assets(\n        unreal.GeometryCacheComponent,\n        unreal.GeometryCache,\n        old_assets,\n        new_assets,\n        selected\n    )\n\n    for old_name, old_mesh in old_caches.items():\n        new_mesh = new_caches.get(old_name)\n\n        if not new_mesh:\n            continue\n\n        for comp in geometry_cache_comps:\n            if comp.get_editor_property(\"geometry_cache\") == old_mesh:\n                comp.set_geometry_cache(new_mesh)\n\n\ndef delete_asset_if_unused(container, asset_content):\n    ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n    references = set()\n\n    for asset_path in asset_content:\n        asset = ar.get_asset_by_object_path(asset_path)\n        refs = ar.get_referencers(\n            asset.package_name,\n            unreal.AssetRegistryDependencyOptions(\n                include_soft_package_references=False,\n                include_hard_package_references=True,\n                include_searchable_names=False,\n                include_soft_management_references=False,\n                include_hard_management_references=False\n            ))\n        if not refs:\n            continue\n        references = references.union(set(refs))\n\n    # Filter out references that are in the Temp folder\n    cleaned_references = {\n        ref for ref in references if not str(ref).startswith(\"/Temp/\")}\n\n    # Check which of the references are Levels\n    for ref in cleaned_references:\n        loaded_asset = unreal.EditorAssetLibrary.load_asset(ref)\n        if isinstance(loaded_asset, unreal.World):\n            # If there is at least a level, we can stop, we don't want to\n            # delete the container\n            return\n\n    unreal.log(\"Previous version unused, deleting...\")\n\n    # No levels, delete the asset\n    unreal.EditorAssetLibrary.delete_directory(container[\"namespace\"])\n\n\n@contextmanager\ndef maintained_selection():\n    \"\"\"Stub to be either implemented or replaced.\n\n    This is needed for old publisher implementation, but\n    it is not supported (yet) in UE.\n    \"\"\"\n    try:\n        yield\n    finally:\n        pass\n"
  },
  {
    "path": "openpype/hosts/unreal/api/plugin.py",
    "content": "# -*- coding: utf-8 -*-\nimport ast\nimport collections\nimport sys\nimport six\nfrom abc import (\n    ABC,\n    ABCMeta,\n)\n\nimport unreal\n\nfrom .pipeline import (\n    create_publish_instance,\n    imprint,\n    ls_inst,\n    UNREAL_VERSION\n)\nfrom openpype.lib import (\n    BoolDef,\n    UILabelDef\n)\nfrom openpype.pipeline import (\n    Creator,\n    LoaderPlugin,\n    CreatorError,\n    CreatedInstance\n)\n\n\n@six.add_metaclass(ABCMeta)\nclass UnrealBaseCreator(Creator):\n    \"\"\"Base class for Unreal creator plugins.\"\"\"\n    root = \"/Game/Ayon/AyonPublishInstances\"\n    suffix = \"_INS\"\n\n    @staticmethod\n    def cache_subsets(shared_data):\n        \"\"\"Cache instances for Creators to shared data.\n\n        Create `unreal_cached_subsets` key when needed in shared data and\n        fill it with all collected instances from the scene under its\n        respective creator identifiers.\n\n        If legacy instances are detected in the scene, create\n        `unreal_cached_legacy_subsets` there and fill it with\n        all legacy subsets under family as a key.\n\n        Args:\n            Dict[str, Any]: Shared data.\n\n        Return:\n            Dict[str, Any]: Shared data dictionary.\n\n        \"\"\"\n        if shared_data.get(\"unreal_cached_subsets\") is None:\n            unreal_cached_subsets = collections.defaultdict(list)\n            unreal_cached_legacy_subsets = collections.defaultdict(list)\n            for instance in ls_inst():\n                creator_id = instance.get(\"creator_identifier\")\n                if creator_id:\n                    unreal_cached_subsets[creator_id].append(instance)\n                else:\n                    family = instance.get(\"family\")\n                    unreal_cached_legacy_subsets[family].append(instance)\n\n            shared_data[\"unreal_cached_subsets\"] = unreal_cached_subsets\n            shared_data[\"unreal_cached_legacy_subsets\"] = (\n                unreal_cached_legacy_subsets\n            )\n        return shared_data\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        try:\n            instance_name = f\"{subset_name}{self.suffix}\"\n            pub_instance = create_publish_instance(instance_name, self.root)\n\n            instance_data[\"subset\"] = subset_name\n            instance_data[\"instance_path\"] = f\"{self.root}/{instance_name}\"\n\n            instance = CreatedInstance(\n                self.family,\n                subset_name,\n                instance_data,\n                self)\n            self._add_instance_to_context(instance)\n\n            pub_instance.set_editor_property('add_external_assets', True)\n            assets = pub_instance.get_editor_property('asset_data_external')\n\n            ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n            for member in pre_create_data.get(\"members\", []):\n                obj = ar.get_asset_by_object_path(member).get_asset()\n                assets.add(obj)\n\n            imprint(f\"{self.root}/{instance_name}\", instance.data_to_store())\n\n            return instance\n\n        except Exception as er:\n            six.reraise(\n                CreatorError,\n                CreatorError(f\"Creator error: {er}\"),\n                sys.exc_info()[2])\n\n    def collect_instances(self):\n        # cache instances if missing\n        self.cache_subsets(self.collection_shared_data)\n        for instance in self.collection_shared_data[\n                \"unreal_cached_subsets\"].get(self.identifier, []):\n            # Unreal saves metadata as string, so we need to convert it back\n            instance['creator_attributes'] = ast.literal_eval(\n                instance.get('creator_attributes', '{}'))\n            instance['publish_attributes'] = ast.literal_eval(\n                instance.get('publish_attributes', '{}'))\n            created_instance = CreatedInstance.from_existing(instance, self)\n            self._add_instance_to_context(created_instance)\n\n    def update_instances(self, update_list):\n        for created_inst, changes in update_list:\n            instance_node = created_inst.get(\"instance_path\", \"\")\n\n            if not instance_node:\n                unreal.log_warning(\n                    f\"Instance node not found for {created_inst}\")\n                continue\n\n            new_values = {\n                key: changes[key].new_value\n                for key in changes.changed_keys\n            }\n            imprint(\n                instance_node,\n                new_values\n            )\n\n    def remove_instances(self, instances):\n        for instance in instances:\n            instance_node = instance.data.get(\"instance_path\", \"\")\n            if instance_node:\n                unreal.EditorAssetLibrary.delete_asset(instance_node)\n\n            self._remove_instance_from_context(instance)\n\n\n@six.add_metaclass(ABCMeta)\nclass UnrealAssetCreator(UnrealBaseCreator):\n    \"\"\"Base class for Unreal creator plugins based on assets.\"\"\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        \"\"\"Create instance of the asset.\n\n        Args:\n            subset_name (str): Name of the subset.\n            instance_data (dict): Data for the instance.\n            pre_create_data (dict): Data for the instance.\n\n        Returns:\n            CreatedInstance: Created instance.\n        \"\"\"\n        try:\n            # Check if instance data has members, filled by the plugin.\n            # If not, use selection.\n            if not pre_create_data.get(\"members\"):\n                pre_create_data[\"members\"] = []\n\n                if pre_create_data.get(\"use_selection\"):\n                    utilib = unreal.EditorUtilityLibrary\n                    sel_objects = utilib.get_selected_assets()\n                    pre_create_data[\"members\"] = [\n                        a.get_path_name() for a in sel_objects]\n\n            super(UnrealAssetCreator, self).create(\n                subset_name,\n                instance_data,\n                pre_create_data)\n\n        except Exception as er:\n            six.reraise(\n                CreatorError,\n                CreatorError(f\"Creator error: {er}\"),\n                sys.exc_info()[2])\n\n    def get_pre_create_attr_defs(self):\n        return [\n            BoolDef(\"use_selection\", label=\"Use selection\", default=True)\n        ]\n\n\n@six.add_metaclass(ABCMeta)\nclass UnrealActorCreator(UnrealBaseCreator):\n    \"\"\"Base class for Unreal creator plugins based on actors.\"\"\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        \"\"\"Create instance of the asset.\n\n        Args:\n            subset_name (str): Name of the subset.\n            instance_data (dict): Data for the instance.\n            pre_create_data (dict): Data for the instance.\n\n        Returns:\n            CreatedInstance: Created instance.\n        \"\"\"\n        try:\n            if UNREAL_VERSION.major == 5:\n                world = unreal.UnrealEditorSubsystem().get_editor_world()\n            else:\n                world = unreal.EditorLevelLibrary.get_editor_world()\n\n            # Check if the level is saved\n            if world.get_path_name().startswith(\"/Temp/\"):\n                raise CreatorError(\n                    \"Level must be saved before creating instances.\")\n\n            # Check if instance data has members, filled by the plugin.\n            # If not, use selection.\n            if not instance_data.get(\"members\"):\n                actor_subsystem = unreal.EditorActorSubsystem()\n                sel_actors = actor_subsystem.get_selected_level_actors()\n                selection = [a.get_path_name() for a in sel_actors]\n\n                instance_data[\"members\"] = selection\n\n            instance_data[\"level\"] = world.get_path_name()\n\n            super(UnrealActorCreator, self).create(\n                subset_name,\n                instance_data,\n                pre_create_data)\n\n        except Exception as er:\n            six.reraise(\n                CreatorError,\n                CreatorError(f\"Creator error: {er}\"),\n                sys.exc_info()[2])\n\n    def get_pre_create_attr_defs(self):\n        return [\n            UILabelDef(\"Select actors to create instance from them.\")\n        ]\n\n\nclass Loader(LoaderPlugin, ABC):\n    \"\"\"This serves as skeleton for future Ayon specific functionality\"\"\"\n    pass\n"
  },
  {
    "path": "openpype/hosts/unreal/api/rendering.py",
    "content": "import os\n\nimport unreal\n\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import Anatomy\nfrom openpype.hosts.unreal.api import pipeline\nfrom openpype.widgets.message_window import Window\n\n\nqueue = None\nexecutor = None\n\n\ndef _queue_finish_callback(exec, success):\n    unreal.log(\"Render completed. Success: \" + str(success))\n\n    # Delete our reference so we don't keep it alive.\n    global executor\n    global queue\n    del executor\n    del queue\n\n\ndef _job_finish_callback(job, success):\n    # You can make any edits you want to the editor world here, and the world\n    # will be duplicated when the next render happens. Make sure you undo your\n    # edits in OnQueueFinishedCallback if you don't want to leak state changes\n    # into the editor world.\n    unreal.log(\"Individual job completed.\")\n\n\ndef start_rendering():\n    \"\"\"\n    Start the rendering process.\n    \"\"\"\n    unreal.log(\"Starting rendering...\")\n\n    # Get selected sequences\n    assets = unreal.EditorUtilityLibrary.get_selected_assets()\n\n    if not assets:\n        Window(\n            parent=None,\n            title=\"No assets selected\",\n            message=\"No assets selected. Select a render instance.\",\n            level=\"warning\")\n        raise RuntimeError(\n            \"No assets selected. You need to select a render instance.\")\n\n    # instances = pipeline.ls_inst()\n    instances = [\n        a for a in assets\n        if a.get_class().get_name() == \"AyonPublishInstance\"]\n\n    inst_data = []\n\n    for i in instances:\n        data = pipeline.parse_container(i.get_path_name())\n        if data[\"family\"] == \"render\":\n            inst_data.append(data)\n\n    try:\n        project = os.environ.get(\"AVALON_PROJECT\")\n        anatomy = Anatomy(project)\n        root = anatomy.roots['renders']\n    except Exception as e:\n        raise Exception(\n            \"Could not find render root in anatomy settings.\") from e\n\n    render_dir = f\"{root}/{project}\"\n\n    # subsystem = unreal.get_editor_subsystem(\n    #     unreal.MoviePipelineQueueSubsystem)\n    # queue = subsystem.get_queue()\n    global queue\n    queue = unreal.MoviePipelineQueue()\n\n    ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n    data = get_project_settings(project)\n    config = None\n    config_path = str(data.get(\"unreal\").get(\"render_config_path\"))\n    if config_path and unreal.EditorAssetLibrary.does_asset_exist(config_path):\n        unreal.log(\"Found saved render configuration\")\n        config = ar.get_asset_by_object_path(config_path).get_asset()\n\n    for i in inst_data:\n        sequence = ar.get_asset_by_object_path(i[\"sequence\"]).get_asset()\n\n        sequences = [{\n            \"sequence\": sequence,\n            \"output\": f\"{i['output']}\",\n            \"frame_range\": (\n                int(float(i[\"frameStart\"])),\n                int(float(i[\"frameEnd\"])) + 1)\n        }]\n        render_list = []\n\n        # Get all the sequences to render. If there are subsequences,\n        # add them and their frame ranges to the render list. We also\n        # use the names for the output paths.\n        for seq in sequences:\n            subscenes = pipeline.get_subsequences(seq.get('sequence'))\n\n            if subscenes:\n                for sub_seq in subscenes:\n                    sequences.append({\n                        \"sequence\": sub_seq.get_sequence(),\n                        \"output\": (f\"{seq.get('output')}/\"\n                                   f\"{sub_seq.get_sequence().get_name()}\"),\n                        \"frame_range\": (\n                            sub_seq.get_start_frame(), sub_seq.get_end_frame())\n                    })\n            else:\n                # Avoid rendering camera sequences\n                if \"_camera\" not in seq.get('sequence').get_name():\n                    render_list.append(seq)\n\n        # Create the rendering jobs and add them to the queue.\n        for render_setting in render_list:\n            job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob)\n            job.sequence = unreal.SoftObjectPath(i[\"master_sequence\"])\n            job.map = unreal.SoftObjectPath(i[\"master_level\"])\n            job.author = \"Ayon\"\n\n            # If we have a saved configuration, copy it to the job.\n            if config:\n                job.get_configuration().copy_from(config)\n\n            # User data could be used to pass data to the job, that can be\n            # read in the job's OnJobFinished callback. We could,\n            # for instance, pass the AyonPublishInstance's path to the job.\n            # job.user_data = \"\"\n\n            output_dir = render_setting.get('output')\n            shot_name = render_setting.get('sequence').get_name()\n\n            settings = job.get_configuration().find_or_add_setting_by_class(\n                unreal.MoviePipelineOutputSetting)\n            settings.output_resolution = unreal.IntPoint(1920, 1080)\n            settings.custom_start_frame = render_setting.get(\"frame_range\")[0]\n            settings.custom_end_frame = render_setting.get(\"frame_range\")[1]\n            settings.use_custom_playback_range = True\n            settings.file_name_format = f\"{shot_name}\" + \".{frame_number}\"\n            settings.output_directory.path = f\"{render_dir}/{output_dir}\"\n\n            job.get_configuration().find_or_add_setting_by_class(\n                unreal.MoviePipelineDeferredPassBase)\n\n            render_format = data.get(\"unreal\").get(\"render_format\", \"png\")\n\n            if render_format == \"png\":\n                job.get_configuration().find_or_add_setting_by_class(\n                    unreal.MoviePipelineImageSequenceOutput_PNG)\n            elif render_format == \"exr\":\n                job.get_configuration().find_or_add_setting_by_class(\n                    unreal.MoviePipelineImageSequenceOutput_EXR)\n            elif render_format == \"jpg\":\n                job.get_configuration().find_or_add_setting_by_class(\n                    unreal.MoviePipelineImageSequenceOutput_JPG)\n            elif render_format == \"bmp\":\n                job.get_configuration().find_or_add_setting_by_class(\n                    unreal.MoviePipelineImageSequenceOutput_BMP)\n\n    # If there are jobs in the queue, start the rendering process.\n    if queue.get_jobs():\n        global executor\n        executor = unreal.MoviePipelinePIEExecutor()\n\n        preroll_frames = data.get(\"unreal\").get(\"preroll_frames\", 0)\n\n        settings = unreal.MoviePipelinePIEExecutorSettings()\n        settings.set_editor_property(\n            \"initial_delay_frame_count\", preroll_frames)\n\n        executor.on_executor_finished_delegate.add_callable_unique(\n            _queue_finish_callback)\n        executor.on_individual_job_finished_delegate.add_callable_unique(\n            _job_finish_callback)  # Only available on PIE Executor\n        executor.execute(queue)\n"
  },
  {
    "path": "openpype/hosts/unreal/api/tools_ui.py",
    "content": "import sys\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import (\n    resources,\n    style\n)\nfrom openpype.tools.utils import host_tools\nfrom openpype.tools.utils.lib import qt_app_context\nfrom openpype.hosts.unreal.api import rendering\n\n\nclass ToolsBtnsWidget(QtWidgets.QWidget):\n    \"\"\"Widget containing buttons which are clickable.\"\"\"\n    tool_required = QtCore.Signal(str)\n\n    def __init__(self, parent=None):\n        super(ToolsBtnsWidget, self).__init__(parent)\n\n        load_btn = QtWidgets.QPushButton(\"Load...\", self)\n        publish_btn = QtWidgets.QPushButton(\"Publisher...\", self)\n        manage_btn = QtWidgets.QPushButton(\"Manage...\", self)\n        render_btn = QtWidgets.QPushButton(\"Render...\", self)\n        experimental_tools_btn = QtWidgets.QPushButton(\n            \"Experimental tools...\", self\n        )\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(load_btn, 0)\n        layout.addWidget(publish_btn, 0)\n        layout.addWidget(manage_btn, 0)\n        layout.addWidget(render_btn, 0)\n        layout.addWidget(experimental_tools_btn, 0)\n        layout.addStretch(1)\n\n        load_btn.clicked.connect(self._on_load)\n        publish_btn.clicked.connect(self._on_publish)\n        manage_btn.clicked.connect(self._on_manage)\n        render_btn.clicked.connect(self._on_render)\n        experimental_tools_btn.clicked.connect(self._on_experimental)\n\n    def _on_create(self):\n        self.tool_required.emit(\"creator\")\n\n    def _on_load(self):\n        self.tool_required.emit(\"loader\")\n\n    def _on_publish(self):\n        self.tool_required.emit(\"publisher\")\n\n    def _on_manage(self):\n        self.tool_required.emit(\"sceneinventory\")\n\n    def _on_render(self):\n        rendering.start_rendering()\n\n    def _on_experimental(self):\n        self.tool_required.emit(\"experimental_tools\")\n\n\nclass ToolsDialog(QtWidgets.QDialog):\n    \"\"\"Dialog with tool buttons that will stay opened until user close it.\"\"\"\n    def __init__(self, *args, **kwargs):\n        super(ToolsDialog, self).__init__(*args, **kwargs)\n\n        self.setWindowTitle(\"Ayon tools\")\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n\n        self.setWindowFlags(\n            QtCore.Qt.Window\n            | QtCore.Qt.WindowStaysOnTopHint\n        )\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        tools_widget = ToolsBtnsWidget(self)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(tools_widget)\n\n        tools_widget.tool_required.connect(self._on_tool_require)\n        self._tools_widget = tools_widget\n\n        self._first_show = True\n\n    def sizeHint(self):\n        result = super(ToolsDialog, self).sizeHint()\n        result.setWidth(result.width() * 2)\n        return result\n\n    def showEvent(self, event):\n        super(ToolsDialog, self).showEvent(event)\n        if self._first_show:\n            self.setStyleSheet(style.load_stylesheet())\n            self._first_show = False\n\n    def _on_tool_require(self, tool_name):\n        host_tools.show_tool_by_name(tool_name, parent=self)\n\n\nclass ToolsPopup(ToolsDialog):\n    \"\"\"Popup with tool buttons that will close when loose focus.\"\"\"\n    def __init__(self, *args, **kwargs):\n        super(ToolsPopup, self).__init__(*args, **kwargs)\n\n        self.setWindowFlags(\n            QtCore.Qt.FramelessWindowHint\n            | QtCore.Qt.Popup\n        )\n\n    def showEvent(self, event):\n        super(ToolsPopup, self).showEvent(event)\n        app = QtWidgets.QApplication.instance()\n        app.processEvents()\n        pos = QtGui.QCursor.pos()\n        self.move(pos)\n\n\nclass WindowCache:\n    \"\"\"Cached objects and methods to be used in global scope.\"\"\"\n    _dialog = None\n    _popup = None\n    _first_show = True\n\n    @classmethod\n    def _before_show(cls):\n        \"\"\"Create QApplication if does not exists yet.\"\"\"\n        if not cls._first_show:\n            return\n\n        cls._first_show = False\n        if not QtWidgets.QApplication.instance():\n            QtWidgets.QApplication(sys.argv)\n\n    @classmethod\n    def show_popup(cls):\n        cls._before_show()\n        with qt_app_context():\n            if cls._popup is None:\n                cls._popup = ToolsPopup()\n\n            cls._popup.show()\n\n    @classmethod\n    def show_dialog(cls):\n        cls._before_show()\n        with qt_app_context():\n            if cls._dialog is None:\n                cls._dialog = ToolsDialog()\n\n            cls._dialog.show()\n            cls._dialog.raise_()\n            cls._dialog.activateWindow()\n\n\ndef show_tools_popup():\n    WindowCache.show_popup()\n\n\ndef show_tools_dialog():\n    WindowCache.show_dialog()\n"
  },
  {
    "path": "openpype/hosts/unreal/hooks/pre_workfile_preparation.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Hook to launch Unreal and prepare projects.\"\"\"\nimport os\nimport copy\nimport shutil\nimport tempfile\nfrom pathlib import Path\n\nfrom qtpy import QtCore\n\nfrom openpype import resources\nfrom openpype.lib.applications import (\n    PreLaunchHook,\n    ApplicationLaunchFailed,\n    LaunchTypes,\n)\nfrom openpype.pipeline.workfile import get_workfile_template_key\nimport openpype.hosts.unreal.lib as unreal_lib\nfrom openpype.hosts.unreal.ue_workers import (\n    UEProjectGenerationWorker,\n    UEPluginInstallWorker\n)\nfrom openpype.hosts.unreal.ui import SplashScreen\n\n\nclass UnrealPrelaunchHook(PreLaunchHook):\n    \"\"\"Hook to handle launching Unreal.\n\n    This hook will check if current workfile path has Unreal\n    project inside. IF not, it initializes it, and finally it pass\n    path to the project by environment variable to Unreal launcher\n    shell script.\n\n    \"\"\"\n    app_groups = {\"unreal\"}\n    launch_types = {LaunchTypes.local}\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        self.signature = f\"( {self.__class__.__name__} )\"\n\n    def _get_work_filename(self):\n        # Use last workfile if was found\n        if self.data.get(\"last_workfile_path\"):\n            last_workfile = Path(self.data.get(\"last_workfile_path\"))\n            if last_workfile and last_workfile.exists():\n                return last_workfile.name\n\n        # Prepare data for fill data and for getting workfile template key\n        anatomy = self.data[\"anatomy\"]\n        project_doc = self.data[\"project_doc\"]\n\n        # Use already prepared workdir data\n        workdir_data = copy.deepcopy(self.data[\"workdir_data\"])\n        task_type = workdir_data.get(\"task\", {}).get(\"type\")\n\n        # QUESTION raise exception if version is part of filename template?\n        workdir_data[\"version\"] = 1\n        workdir_data[\"ext\"] = \"uproject\"\n\n        # Get workfile template key for current context\n        workfile_template_key = get_workfile_template_key(\n            task_type,\n            self.host_name,\n            project_name=project_doc[\"name\"]\n        )\n        # Fill templates\n        template_obj = anatomy.templates_obj[workfile_template_key][\"file\"]\n\n        # Return filename\n        return template_obj.format_strict(workdir_data)\n\n    def exec_plugin_install(self, engine_path: Path, env: dict = None):\n        # set up the QThread and worker with necessary signals\n        env = env or os.environ\n        q_thread = QtCore.QThread()\n        ue_plugin_worker = UEPluginInstallWorker()\n\n        q_thread.started.connect(ue_plugin_worker.run)\n        ue_plugin_worker.setup(engine_path, env)\n        ue_plugin_worker.moveToThread(q_thread)\n\n        splash_screen = SplashScreen(\n            \"Installing plugin\",\n            resources.get_resource(\"app_icons\", \"ue4.png\")\n        )\n\n        # set up the splash screen with necessary triggers\n        ue_plugin_worker.installing.connect(\n            splash_screen.update_top_label_text\n        )\n        ue_plugin_worker.progress.connect(splash_screen.update_progress)\n        ue_plugin_worker.log.connect(splash_screen.append_log)\n        ue_plugin_worker.finished.connect(splash_screen.quit_and_close)\n        ue_plugin_worker.failed.connect(splash_screen.fail)\n\n        splash_screen.start_thread(q_thread)\n        splash_screen.show_ui()\n\n        if not splash_screen.was_proc_successful():\n            raise ApplicationLaunchFailed(\"Couldn't run the application! \"\n                                          \"Plugin failed to install!\")\n\n    def exec_ue_project_gen(self,\n                            engine_version: str,\n                            unreal_project_name: str,\n                            engine_path: Path,\n                            project_dir: Path):\n        self.log.info((\n            f\"{self.signature} Creating unreal \"\n            f\"project [ {unreal_project_name} ]\"\n        ))\n\n        q_thread = QtCore.QThread()\n        ue_project_worker = UEProjectGenerationWorker()\n        ue_project_worker.setup(\n            engine_version,\n            self.data[\"project_name\"],\n            unreal_project_name,\n            engine_path,\n            project_dir\n        )\n        ue_project_worker.moveToThread(q_thread)\n        q_thread.started.connect(ue_project_worker.run)\n\n        splash_screen = SplashScreen(\n            \"Initializing UE project\",\n            resources.get_resource(\"app_icons\", \"ue4.png\")\n        )\n\n        ue_project_worker.stage_begin.connect(\n            splash_screen.update_top_label_text\n        )\n        ue_project_worker.progress.connect(splash_screen.update_progress)\n        ue_project_worker.log.connect(splash_screen.append_log)\n        ue_project_worker.finished.connect(splash_screen.quit_and_close)\n        ue_project_worker.failed.connect(splash_screen.fail)\n\n        splash_screen.start_thread(q_thread)\n        splash_screen.show_ui()\n\n        if not splash_screen.was_proc_successful():\n            raise ApplicationLaunchFailed(\"Couldn't run the application! \"\n                                          \"Failed to generate the project!\")\n\n    def execute(self):\n        \"\"\"Hook entry method.\"\"\"\n        workdir = self.launch_context.env[\"AVALON_WORKDIR\"]\n        executable = str(self.launch_context.executable)\n        engine_version = self.app_name.split(\"/\")[-1].replace(\"-\", \".\")\n        try:\n            if int(engine_version.split(\".\")[0]) < 4 and \\\n                    int(engine_version.split(\".\")[1]) < 26:\n                raise ApplicationLaunchFailed((\n                    f\"{self.signature} Old unsupported version of UE \"\n                    f\"detected - {engine_version}\"))\n        except ValueError:\n            # there can be string in minor version and in that case\n            # int cast is failing. This probably happens only with\n            # early access versions and is of no concert for this check\n            # so let's keep it quiet.\n            ...\n\n        unreal_project_filename = self._get_work_filename()\n        unreal_project_name = os.path.splitext(unreal_project_filename)[0]\n        # Unreal is sensitive about project names longer then 20 chars\n        if len(unreal_project_name) > 20:\n            raise ApplicationLaunchFailed(\n                f\"Project name exceeds 20 characters ({unreal_project_name})!\"\n            )\n\n        # Unreal doesn't accept non alphabet characters at the start\n        # of the project name. This is because project name is then used\n        # in various places inside c++ code and there variable names cannot\n        # start with non-alpha. We append 'P' before project name to solve it.\n        # 😱\n        if not unreal_project_name[:1].isalpha():\n            self.log.warning((\n                \"Project name doesn't start with alphabet \"\n                f\"character ({unreal_project_name}). Appending 'P'\"\n            ))\n            unreal_project_name = f\"P{unreal_project_name}\"\n            unreal_project_filename = f'{unreal_project_name}.uproject'\n\n        project_path = Path(os.path.join(workdir, unreal_project_name))\n\n        self.log.info((\n            f\"{self.signature} requested UE version: \"\n            f\"[ {engine_version} ]\"\n        ))\n\n        project_path.mkdir(parents=True, exist_ok=True)\n\n        # engine_path points to the specific Unreal Engine root\n        # so, we are going up from the executable itself 3 levels.\n        engine_path: Path = Path(executable).parents[3]\n\n        # Check if new env variable exists, and if it does, if the path\n        # actually contains the plugin. If not, install it.\n\n        built_plugin_path = self.launch_context.env.get(\n            \"AYON_BUILT_UNREAL_PLUGIN\", None)\n\n        if unreal_lib.check_built_plugin_existance(built_plugin_path):\n            self.log.info((\n                f\"{self.signature} using existing built Ayon plugin from \"\n                f\"{built_plugin_path}\"\n            ))\n            unreal_lib.copy_built_plugin(engine_path, Path(built_plugin_path))\n        else:\n            # Set \"AYON_UNREAL_PLUGIN\" to current process environment for\n            # execution of `create_unreal_project`\n            env_key = \"AYON_UNREAL_PLUGIN\"\n            if self.launch_context.env.get(env_key):\n                self.log.info((\n                    f\"{self.signature} using Ayon plugin from \"\n                    f\"{self.launch_context.env.get(env_key)}\"\n                ))\n            if self.launch_context.env.get(env_key):\n                os.environ[env_key] = self.launch_context.env[env_key]\n\n            if not unreal_lib.check_plugin_existence(engine_path):\n                self.exec_plugin_install(engine_path)\n\n        project_file = project_path / unreal_project_filename\n\n        if not project_file.is_file():\n            with tempfile.TemporaryDirectory() as temp_dir:\n                self.exec_ue_project_gen(engine_version,\n                                         unreal_project_name,\n                                         engine_path,\n                                         Path(temp_dir))\n                try:\n                    self.log.info((\n                        f\"Moving from {temp_dir} to \"\n                        f\"{project_path.as_posix()}\"\n                    ))\n                    shutil.copytree(\n                        temp_dir, project_path, dirs_exist_ok=True)\n\n                except shutil.Error as e:\n                    raise ApplicationLaunchFailed((\n                        f\"{self.signature} Cannot copy directory {temp_dir} \"\n                        f\"to {project_path.as_posix()} - {e}\"\n                    )) from e\n\n        self.launch_context.env[\"AYON_UNREAL_VERSION\"] = engine_version\n        # Append project file to launch arguments\n        self.launch_context.launch_args.append(\n            f\"\\\"{project_file.as_posix()}\\\"\")\n"
  },
  {
    "path": "openpype/hosts/unreal/lib.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unreal launching and project tools.\"\"\"\n\nimport json\nimport os\nimport platform\nimport re\nimport subprocess\nfrom collections import OrderedDict\nfrom distutils import dir_util\nfrom pathlib import Path\nfrom typing import List\n\nfrom openpype.settings import get_project_settings\n\n\ndef get_engine_versions(env=None):\n    \"\"\"Detect Unreal Engine versions.\n\n    This will try to detect location and versions of installed Unreal Engine.\n    Location can be overridden by `UNREAL_ENGINE_LOCATION` environment\n    variable.\n\n    .. deprecated:: 3.15.4\n\n    Args:\n        env (dict, optional): Environment to use.\n\n    Returns:\n        OrderedDict: dictionary with version as a key and dir as value.\n            so the highest version is first.\n\n    Example:\n        >>> get_engine_versions()\n        {\n            \"4.23\": \"C:/Epic Games/UE_4.23\",\n            \"4.24\": \"C:/Epic Games/UE_4.24\"\n        }\n\n    \"\"\"\n    env = env or os.environ\n    engine_locations = {}\n    try:\n        root, dirs, _ = next(os.walk(env[\"UNREAL_ENGINE_LOCATION\"]))\n\n        for directory in dirs:\n            if directory.startswith(\"UE\"):\n                try:\n                    ver = re.split(r\"[-_]\", directory)[1]\n                except IndexError:\n                    continue\n                engine_locations[ver] = os.path.join(root, directory)\n    except KeyError:\n        # environment variable not set\n        pass\n    except OSError:\n        # specified directory doesn't exist\n        pass\n    except StopIteration:\n        # specified directory doesn't exist\n        pass\n\n    # if we've got something, terminate auto-detection process\n    if engine_locations:\n        return OrderedDict(sorted(engine_locations.items()))\n\n    # else kick in platform specific detection\n    if platform.system().lower() == \"windows\":\n        return OrderedDict(sorted(_win_get_engine_versions().items()))\n    if platform.system().lower() == \"linux\":\n        # on linux, there is no installation and getting Unreal Engine involves\n        # git clone. So we'll probably depend on `UNREAL_ENGINE_LOCATION`.\n        pass\n    if platform.system().lower() == \"darwin\":\n        return OrderedDict(sorted(_darwin_get_engine_version().items()))\n\n    return OrderedDict()\n\n\ndef get_editor_exe_path(engine_path: Path, engine_version: str) -> Path:\n    \"\"\"Get UE Editor executable path.\"\"\"\n    ue_path = engine_path / \"Engine/Binaries\"\n    if platform.system().lower() == \"windows\":\n        if engine_version.split(\".\")[0] == \"4\":\n            ue_path /= \"Win64/UE4Editor.exe\"\n        elif engine_version.split(\".\")[0] == \"5\":\n            ue_path /= \"Win64/UnrealEditor.exe\"\n\n    elif platform.system().lower() == \"linux\":\n        ue_path /= \"Linux/UE4Editor\"\n\n    elif platform.system().lower() == \"darwin\":\n        ue_path /= \"Mac/UE4Editor\"\n\n    return ue_path\n\n\ndef _win_get_engine_versions():\n    \"\"\"Get Unreal Engine versions on Windows.\n\n    If engines are installed via Epic Games Launcher then there is:\n    `%PROGRAMDATA%/Epic/UnrealEngineLauncher/LauncherInstalled.dat`\n    This file is JSON file listing installed stuff, Unreal engines\n    are marked with `\"AppName\" = \"UE_X.XX\"`` like `UE_4.24`\n\n    .. deprecated:: 3.15.4\n\n    Returns:\n        dict: version as a key and path as a value.\n\n    \"\"\"\n    install_json_path = os.path.join(\n        os.getenv(\"PROGRAMDATA\"),\n        \"Epic\",\n        \"UnrealEngineLauncher\",\n        \"LauncherInstalled.dat\",\n    )\n\n    return _parse_launcher_locations(install_json_path)\n\n\ndef _darwin_get_engine_version() -> dict:\n    \"\"\"Get Unreal Engine versions on MacOS.\n\n    It works the same as on Windows, just JSON file location is different.\n\n    .. deprecated:: 3.15.4\n\n    Returns:\n        dict: version as a key and path as a value.\n\n    See Also:\n        :func:`_win_get_engine_versions`.\n\n    \"\"\"\n    install_json_path = os.path.join(\n        os.getenv(\"HOME\"),\n        \"Library\",\n        \"Application Support\",\n        \"Epic\",\n        \"UnrealEngineLauncher\",\n        \"LauncherInstalled.dat\",\n    )\n\n    return _parse_launcher_locations(install_json_path)\n\n\ndef _parse_launcher_locations(install_json_path: str) -> dict:\n    \"\"\"This will parse locations from json file.\n\n    .. deprecated:: 3.15.4\n\n    Args:\n        install_json_path (str): Path to `LauncherInstalled.dat`.\n\n    Returns:\n        dict: with unreal engine versions as keys and\n            paths to those engine installations as value.\n\n    \"\"\"\n    engine_locations = {}\n    if os.path.isfile(install_json_path):\n        with open(install_json_path, \"r\") as ilf:\n            try:\n                install_data = json.load(ilf)\n            except json.JSONDecodeError as e:\n                raise Exception(\n                    \"Invalid `LauncherInstalled.dat file. `\"\n                    \"Cannot determine Unreal Engine location.\"\n                ) from e\n\n        for installation in install_data.get(\"InstallationList\", []):\n            if installation.get(\"AppName\").startswith(\"UE_\"):\n                ver = installation.get(\"AppName\").split(\"_\")[1]\n                engine_locations[ver] = installation.get(\"InstallLocation\")\n\n    return engine_locations\n\n\ndef create_unreal_project(project_name: str,\n                          unreal_project_name: str,\n                          ue_version: str,\n                          pr_dir: Path,\n                          engine_path: Path,\n                          dev_mode: bool = False,\n                          env: dict = None) -> None:\n    \"\"\"This will create `.uproject` file at specified location.\n\n    As there is no way I know to create a project via command line, this is\n    easiest option. Unreal project file is basically a JSON file. If we find\n    the `AYON_UNREAL_PLUGIN` environment variable we assume this is the\n    location of the Integration Plugin and we copy its content to the project\n    folder and enable this plugin.\n\n    Args:\n        project_name (str): Name of the project in AYON.\n        unreal_project_name (str): Name of the project in Unreal.\n        ue_version (str): Unreal engine version (like 4.23).\n        pr_dir (Path): Path to directory where project will be created.\n        engine_path (Path): Path to Unreal Engine installation.\n        dev_mode (bool, optional): Flag to trigger C++ style Unreal project\n            needing Visual Studio and other tools to compile plugins from\n            sources. This will trigger automatically if `Binaries`\n            directory is not found in plugin folders as this indicates\n            this is only source distribution of the plugin. Dev mode\n            is also set in Settings.\n        env (dict, optional): Environment to use. If not set, `os.environ`.\n\n    Throws:\n        NotImplementedError: For unsupported platforms.\n\n    Returns:\n        None\n\n    Deprecated:\n        since 3.16.0\n\n    \"\"\"\n    env = env or os.environ\n\n    preset = get_project_settings(project_name)[\"unreal\"][\"project_setup\"]\n    ue_id = \".\".join(ue_version.split(\".\")[:2])\n    # get unreal engine identifier\n    # -------------------------------------------------------------------------\n    # FIXME (antirotor): As of 4.26 this is problem with UE4 built from\n    # sources. In that case Engine ID is calculated per machine/user and not\n    # from Engine files as this code then reads. This then prevents UE4\n    # to directly open project as it will complain about project being\n    # created in different UE4 version. When user convert such project\n    # to his UE4 version, Engine ID is replaced in uproject file. If some\n    # other user tries to open it, it will present him with similar error.\n\n    # engine_path should be the location of UE_X.X folder\n\n    ue_editor_exe: Path = get_editor_exe_path(engine_path, ue_version)\n    cmdlet_project: Path = get_path_to_cmdlet_project(ue_version)\n\n    project_file = pr_dir / f\"{unreal_project_name}.uproject\"\n\n    print(\"--- Generating a new project ...\")\n    commandlet_cmd = [f'{ue_editor_exe.as_posix()}',\n                      f'{cmdlet_project.as_posix()}',\n                      f'-run=AyonGenerateProject',\n                      f'{project_file.resolve().as_posix()}']\n\n    if dev_mode or preset[\"dev_mode\"]:\n        commandlet_cmd.append('-GenerateCode')\n\n    gen_process = subprocess.Popen(commandlet_cmd,\n                                   stdout=subprocess.PIPE,\n                                   stderr=subprocess.PIPE)\n\n    for line in gen_process.stdout:\n        print(line.decode(), end='')\n    gen_process.stdout.close()\n    return_code = gen_process.wait()\n\n    if return_code and return_code != 0:\n        raise RuntimeError(\n            (f\"Failed to generate '{unreal_project_name}' project! \"\n             f\"Exited with return code {return_code}\"))\n\n    print(\"--- Project has been generated successfully.\")\n\n    with open(project_file.as_posix(), mode=\"r+\") as pf:\n        pf_json = json.load(pf)\n        pf_json[\"EngineAssociation\"] = get_build_id(engine_path, ue_version)\n        pf.seek(0)\n        json.dump(pf_json, pf, indent=4)\n        pf.truncate()\n        print(f'--- Engine ID has been written into the project file')\n\n    if dev_mode or preset[\"dev_mode\"]:\n        u_build_tool = get_path_to_ubt(engine_path, ue_version)\n\n        arch = \"Win64\"\n        if platform.system().lower() == \"windows\":\n            arch = \"Win64\"\n        elif platform.system().lower() == \"linux\":\n            arch = \"Linux\"\n        elif platform.system().lower() == \"darwin\":\n            # we need to test this out\n            arch = \"Mac\"\n\n        command1 = [u_build_tool.as_posix(), \"-projectfiles\",\n                    f\"-project={project_file}\", \"-progress\"]\n\n        subprocess.run(command1)\n\n        command2 = [u_build_tool.as_posix(),\n                    f\"-ModuleWithSuffix={unreal_project_name},3555\", arch,\n                    \"Development\", \"-TargetType=Editor\",\n                    f'-Project={project_file}',\n                    f'{project_file}',\n                    \"-IgnoreJunk\"]\n\n        subprocess.run(command2)\n\n    # ensure we have PySide2 installed in engine\n    python_path = None\n    if platform.system().lower() == \"windows\":\n        python_path = engine_path / (\"Engine/Binaries/ThirdParty/\"\n                                     \"Python3/Win64/python.exe\")\n\n    if platform.system().lower() == \"linux\":\n        python_path = engine_path / (\"Engine/Binaries/ThirdParty/\"\n                                     \"Python3/Linux/bin/python3\")\n\n    if platform.system().lower() == \"darwin\":\n        python_path = engine_path / (\"Engine/Binaries/ThirdParty/\"\n                                     \"Python3/Mac/bin/python3\")\n\n    if not python_path:\n        raise NotImplementedError(\"Unsupported platform\")\n    if not python_path.exists():\n        raise RuntimeError(f\"Unreal Python not found at {python_path}\")\n    subprocess.check_call(\n        [python_path.as_posix(), \"-m\", \"pip\", \"install\", \"pyside2\"])\n\n\ndef get_path_to_uat(engine_path: Path) -> Path:\n    if platform.system().lower() == \"windows\":\n        return engine_path / \"Engine/Build/BatchFiles/RunUAT.bat\"\n\n    if platform.system().lower() in [\"linux\", \"darwin\"]:\n        return engine_path / \"Engine/Build/BatchFiles/RunUAT.sh\"\n\n\ndef get_compatible_integration(\n        ue_version: str, integration_root: Path) -> List[Path]:\n    \"\"\"Get path to compatible version of integration plugin.\n\n    This will try to get the closest compatible versions to the one\n    specified in sorted list.\n\n    Args:\n        ue_version (str): version of the current Unreal Engine.\n        integration_root (Path): path to built-in integration plugins.\n\n    Returns:\n        list of Path: Sorted list of paths closest to the specified\n            version.\n\n    \"\"\"\n    major, minor = ue_version.split(\".\")\n    integration_paths = [p for p in integration_root.iterdir()\n                         if p.is_dir()]\n\n    compatible_versions = []\n    for i in integration_paths:\n        # parse version from path\n        try:\n            i_major, i_minor = re.search(\n                r\"(?P<major>\\d+).(?P<minor>\\d+)$\", i.name).groups()\n        except AttributeError:\n            # in case there is no match, just skip to next\n            continue\n\n        # consider versions with different major so different that they\n        # are incompatible\n        if int(major) != int(i_major):\n            continue\n\n        compatible_versions.append(i)\n\n    sorted(set(compatible_versions))\n    return compatible_versions\n\n\ndef get_path_to_cmdlet_project(ue_version: str) -> Path:\n    cmd_project = Path(\n        os.path.dirname(os.path.abspath(__file__)))\n\n    # For now, only tested on Windows (For Linux and Mac\n    # it has to be implemented)\n    cmd_project /= f\"integration/UE_{ue_version}\"\n\n    # if the integration doesn't exist for current engine version\n    # try to find the closest to it.\n    if cmd_project.exists():\n        return cmd_project / \"CommandletProject/CommandletProject.uproject\"\n\n    if compatible_versions := get_compatible_integration(\n        ue_version, cmd_project.parent\n    ):\n        return compatible_versions[-1] / \"CommandletProject/CommandletProject.uproject\"  # noqa: E501\n    else:\n        raise RuntimeError(\n            (\"There are no compatible versions of Unreal \"\n             \"integration plugin compatible with running version \"\n             f\"of Unreal Engine {ue_version}\"))\n\n\ndef get_path_to_ubt(engine_path: Path, ue_version: str) -> Path:\n    u_build_tool_path = engine_path / \"Engine/Binaries/DotNET\"\n\n    if ue_version.split(\".\")[0] == \"4\":\n        u_build_tool_path /= \"UnrealBuildTool.exe\"\n    elif ue_version.split(\".\")[0] == \"5\":\n        u_build_tool_path /= \"UnrealBuildTool/UnrealBuildTool.exe\"\n\n    return Path(u_build_tool_path)\n\n\ndef get_build_id(engine_path: Path, ue_version: str) -> str:\n    ue_modules = Path()\n    if platform.system().lower() == \"windows\":\n        ue_modules_path = engine_path / \"Engine/Binaries/Win64\"\n        if ue_version.split(\".\")[0] == \"4\":\n            ue_modules_path /= \"UE4Editor.modules\"\n        elif ue_version.split(\".\")[0] == \"5\":\n            ue_modules_path /= \"UnrealEditor.modules\"\n        ue_modules = Path(ue_modules_path)\n\n    if platform.system().lower() == \"linux\":\n        ue_modules = Path(os.path.join(engine_path, \"Engine\", \"Binaries\",\n                                       \"Linux\", \"UE4Editor.modules\"))\n\n    if platform.system().lower() == \"darwin\":\n        ue_modules = Path(os.path.join(engine_path, \"Engine\", \"Binaries\",\n                                       \"Mac\", \"UE4Editor.modules\"))\n\n    if ue_modules.exists():\n        print(\"--- Loading Engine ID from modules file ...\")\n        with open(ue_modules, \"r\") as mp:\n            loaded_modules = json.load(mp)\n\n        if loaded_modules.get(\"BuildId\"):\n            return \"{\" + loaded_modules.get(\"BuildId\") + \"}\"\n\n\ndef check_built_plugin_existance(plugin_path) -> bool:\n    if not plugin_path:\n        return False\n\n    integration_plugin_path = Path(plugin_path)\n\n    if not integration_plugin_path.is_dir():\n        raise RuntimeError(\"Path to the integration plugin is null!\")\n\n    if not (integration_plugin_path / \"Binaries\").is_dir() \\\n            or not (integration_plugin_path / \"Intermediate\").is_dir():\n        return False\n\n    return True\n\n\ndef copy_built_plugin(engine_path: Path, plugin_path: Path) -> None:\n    ayon_plugin_path: Path = engine_path / \"Engine/Plugins/Marketplace/Ayon\"\n\n    if not ayon_plugin_path.is_dir():\n        ayon_plugin_path.mkdir(parents=True, exist_ok=True)\n\n        engine_plugin_config_path: Path = ayon_plugin_path / \"Config\"\n        engine_plugin_config_path.mkdir(exist_ok=True)\n\n        dir_util._path_created = {}\n\n    dir_util.copy_tree(plugin_path.as_posix(), ayon_plugin_path.as_posix())\n\n\ndef check_plugin_existence(engine_path: Path, env: dict = None) -> bool:\n    env = env or os.environ\n    integration_plugin_path: Path = Path(env.get(\"AYON_UNREAL_PLUGIN\", \"\"))\n\n    if not os.path.isdir(integration_plugin_path):\n        raise RuntimeError(\"Path to the integration plugin is null!\")\n\n    # Create a path to the plugin in the engine\n    op_plugin_path: Path = engine_path / \"Engine/Plugins/Marketplace/Ayon\"\n\n    if not op_plugin_path.is_dir():\n        return False\n\n    if not (op_plugin_path / \"Binaries\").is_dir() \\\n            or not (op_plugin_path / \"Intermediate\").is_dir():\n        return False\n\n    return True\n\n\ndef try_installing_plugin(engine_path: Path, env: dict = None) -> None:\n    env = env or os.environ\n\n    integration_plugin_path: Path = Path(env.get(\"AYON_UNREAL_PLUGIN\", \"\"))\n\n    if not os.path.isdir(integration_plugin_path):\n        raise RuntimeError(\"Path to the integration plugin is null!\")\n\n    # Create a path to the plugin in the engine\n    op_plugin_path: Path = engine_path / \"Engine/Plugins/Marketplace/Ayon\"\n\n    if not op_plugin_path.is_dir():\n        op_plugin_path.mkdir(parents=True, exist_ok=True)\n\n        engine_plugin_config_path: Path = op_plugin_path / \"Config\"\n        engine_plugin_config_path.mkdir(exist_ok=True)\n\n        dir_util._path_created = {}\n\n    if not (op_plugin_path / \"Binaries\").is_dir() \\\n            or not (op_plugin_path / \"Intermediate\").is_dir():\n        _build_and_move_plugin(engine_path, op_plugin_path, env)\n\n\ndef _build_and_move_plugin(engine_path: Path,\n                           plugin_build_path: Path,\n                           env: dict = None) -> None:\n    uat_path: Path = get_path_to_uat(engine_path)\n\n    env = env or os.environ\n    integration_plugin_path: Path = Path(env.get(\"AYON_UNREAL_PLUGIN\", \"\"))\n\n    if uat_path.is_file():\n        temp_dir: Path = integration_plugin_path.parent / \"Temp\"\n        temp_dir.mkdir(exist_ok=True)\n        uplugin_path: Path = integration_plugin_path / \"Ayon.uplugin\"\n\n        # in order to successfully build the plugin,\n        # It must be built outside the Engine directory and then moved\n        build_plugin_cmd: List[str] = [f'{uat_path.as_posix()}',\n                                       'BuildPlugin',\n                                       f'-Plugin={uplugin_path.as_posix()}',\n                                       f'-Package={temp_dir.as_posix()}']\n        subprocess.run(build_plugin_cmd)\n\n        # Copy the contents of the 'Temp' dir into the\n        # 'Ayon' directory in the engine\n        dir_util.copy_tree(temp_dir.as_posix(), plugin_build_path.as_posix())\n\n        # We need to also copy the config folder.\n        # The UAT doesn't include the Config folder in the build\n        plugin_install_config_path: Path = plugin_build_path / \"Config\"\n        integration_plugin_config_path = integration_plugin_path / \"Config\"\n\n        dir_util.copy_tree(integration_plugin_config_path.as_posix(),\n                           plugin_install_config_path.as_posix())\n\n        dir_util.remove_tree(temp_dir.as_posix())\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/hosts/unreal/plugins/create/create_camera.py",
    "content": "# -*- coding: utf-8 -*-\nimport unreal\n\nfrom openpype.pipeline import CreatorError\nfrom openpype.hosts.unreal.api.pipeline import UNREAL_VERSION\nfrom openpype.hosts.unreal.api.plugin import (\n    UnrealAssetCreator,\n)\n\n\nclass CreateCamera(UnrealAssetCreator):\n    \"\"\"Create Camera.\"\"\"\n\n    identifier = \"io.ayon.creators.unreal.camera\"\n    label = \"Camera\"\n    family = \"camera\"\n    icon = \"fa.camera\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        if pre_create_data.get(\"use_selection\"):\n            sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()\n            selection = [a.get_path_name() for a in sel_objects]\n\n            if len(selection) != 1:\n                raise CreatorError(\"Please select only one object.\")\n\n        # Add the current level path to the metadata\n        if UNREAL_VERSION.major == 5:\n            world = unreal.UnrealEditorSubsystem().get_editor_world()\n        else:\n            world = unreal.EditorLevelLibrary.get_editor_world()\n\n        instance_data[\"level\"] = world.get_path_name()\n\n        super(CreateCamera, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/create/create_layout.py",
    "content": "# -*- coding: utf-8 -*-\nfrom openpype.hosts.unreal.api.plugin import (\n    UnrealActorCreator,\n)\n\n\nclass CreateLayout(UnrealActorCreator):\n    \"\"\"Layout output for character rigs.\"\"\"\n\n    identifier = \"io.ayon.creators.unreal.layout\"\n    label = \"Layout\"\n    family = \"layout\"\n    icon = \"cubes\"\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/create/create_look.py",
    "content": "# -*- coding: utf-8 -*-\nimport unreal\n\nfrom openpype.pipeline import CreatorError\nfrom openpype.hosts.unreal.api.pipeline import (\n    create_folder\n)\nfrom openpype.hosts.unreal.api.plugin import (\n    UnrealAssetCreator\n)\nfrom openpype.lib import UILabelDef\n\n\nclass CreateLook(UnrealAssetCreator):\n    \"\"\"Shader connections defining shape look.\"\"\"\n\n    identifier = \"io.ayon.creators.unreal.look\"\n    label = \"Look\"\n    family = \"look\"\n    icon = \"paint-brush\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # We need to set this to True for the parent class to work\n        pre_create_data[\"use_selection\"] = True\n        sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()\n        selection = [a.get_path_name() for a in sel_objects]\n\n        if len(selection) != 1:\n            raise CreatorError(\"Please select only one asset.\")\n\n        selected_asset = selection[0]\n\n        look_directory = \"/Game/Ayon/Looks\"\n\n        # Create the folder\n        folder_name = create_folder(look_directory, subset_name)\n        path = f\"{look_directory}/{folder_name}\"\n\n        instance_data[\"look\"] = path\n\n        # Create a new cube static mesh\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n        cube = ar.get_asset_by_object_path(\"/Engine/BasicShapes/Cube.Cube\")\n\n        # Get the mesh of the selected object\n        original_mesh = ar.get_asset_by_object_path(selected_asset).get_asset()\n        materials = original_mesh.get_editor_property('static_materials')\n\n        pre_create_data[\"members\"] = []\n\n        # Add the materials to the cube\n        for material in materials:\n            mat_name = material.get_editor_property('material_slot_name')\n            object_path = f\"{path}/{mat_name}.{mat_name}\"\n            unreal_object = unreal.EditorAssetLibrary.duplicate_loaded_asset(\n                cube.get_asset(), object_path\n            )\n\n            # Remove the default material of the cube object\n            unreal_object.get_editor_property('static_materials').pop()\n\n            unreal_object.add_material(\n                material.get_editor_property('material_interface'))\n\n            pre_create_data[\"members\"].append(object_path)\n\n            unreal.EditorAssetLibrary.save_asset(object_path)\n\n        super(CreateLook, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)\n\n    def get_pre_create_attr_defs(self):\n        return [\n            UILabelDef(\"Select the asset from which to create the look.\")\n        ]\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/create/create_render.py",
    "content": "# -*- coding: utf-8 -*-\nfrom pathlib import Path\n\nimport unreal\n\nfrom openpype.hosts.unreal.api.pipeline import (\n    UNREAL_VERSION,\n    create_folder,\n    get_subsequences,\n)\nfrom openpype.hosts.unreal.api.plugin import (\n    UnrealAssetCreator\n)\nfrom openpype.lib import (\n    UILabelDef,\n    UISeparatorDef,\n    BoolDef,\n    NumberDef\n)\n\n\nclass CreateRender(UnrealAssetCreator):\n    \"\"\"Create instance for sequence for rendering\"\"\"\n\n    identifier = \"io.ayon.creators.unreal.render\"\n    label = \"Render\"\n    family = \"render\"\n    icon = \"eye\"\n\n    def create_instance(\n            self, instance_data, subset_name, pre_create_data,\n            selected_asset_path, master_seq, master_lvl, seq_data\n    ):\n        instance_data[\"members\"] = [selected_asset_path]\n        instance_data[\"sequence\"] = selected_asset_path\n        instance_data[\"master_sequence\"] = master_seq\n        instance_data[\"master_level\"] = master_lvl\n        instance_data[\"output\"] = seq_data.get('output')\n        instance_data[\"frameStart\"] = seq_data.get('frame_range')[0]\n        instance_data[\"frameEnd\"] = seq_data.get('frame_range')[1]\n\n        super(CreateRender, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)\n\n    def create_with_new_sequence(\n            self, subset_name, instance_data, pre_create_data\n    ):\n        # If the option to create a new level sequence is selected,\n        # create a new level sequence and a master level.\n\n        root = f\"/Game/Ayon/Sequences\"\n\n        # Create a new folder for the sequence in root\n        sequence_dir_name = create_folder(root, subset_name)\n        sequence_dir = f\"{root}/{sequence_dir_name}\"\n\n        unreal.log_warning(f\"sequence_dir: {sequence_dir}\")\n\n        # Create the level sequence\n        asset_tools = unreal.AssetToolsHelpers.get_asset_tools()\n        seq = asset_tools.create_asset(\n            asset_name=subset_name,\n            package_path=sequence_dir,\n            asset_class=unreal.LevelSequence,\n            factory=unreal.LevelSequenceFactoryNew())\n\n        seq.set_playback_start(pre_create_data.get(\"start_frame\"))\n        seq.set_playback_end(pre_create_data.get(\"end_frame\"))\n\n        pre_create_data[\"members\"] = [seq.get_path_name()]\n\n        unreal.EditorAssetLibrary.save_asset(seq.get_path_name())\n\n        # Create the master level\n        if UNREAL_VERSION.major >= 5:\n            curr_level = unreal.LevelEditorSubsystem().get_current_level()\n        else:\n            world = unreal.EditorLevelLibrary.get_editor_world()\n            levels = unreal.EditorLevelUtils.get_levels(world)\n            curr_level = levels[0] if len(levels) else None\n            if not curr_level:\n                raise RuntimeError(\"No level loaded.\")\n        curr_level_path = curr_level.get_outer().get_path_name()\n\n        # If the level path does not start with \"/Game/\", the current\n        # level is a temporary, unsaved level.\n        if curr_level_path.startswith(\"/Game/\"):\n            if UNREAL_VERSION.major >= 5:\n                unreal.LevelEditorSubsystem().save_current_level()\n            else:\n                unreal.EditorLevelLibrary.save_current_level()\n\n        ml_path = f\"{sequence_dir}/{subset_name}_MasterLevel\"\n\n        if UNREAL_VERSION.major >= 5:\n            unreal.LevelEditorSubsystem().new_level(ml_path)\n        else:\n            unreal.EditorLevelLibrary.new_level(ml_path)\n\n        seq_data = {\n            \"sequence\": seq,\n            \"output\": f\"{seq.get_name()}\",\n            \"frame_range\": (\n                seq.get_playback_start(),\n                seq.get_playback_end())}\n\n        self.create_instance(\n            instance_data, subset_name, pre_create_data,\n            seq.get_path_name(), seq.get_path_name(), ml_path, seq_data)\n\n    def create_from_existing_sequence(\n            self, subset_name, instance_data, pre_create_data\n    ):\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()\n        selection = [\n            a.get_path_name() for a in sel_objects\n            if a.get_class().get_name() == \"LevelSequence\"]\n\n        if len(selection) == 0:\n            raise RuntimeError(\"Please select at least one Level Sequence.\")\n\n        seq_data = None\n\n        for sel in selection:\n            selected_asset = ar.get_asset_by_object_path(sel).get_asset()\n            selected_asset_path = selected_asset.get_path_name()\n\n            # Check if the selected asset is a level sequence asset.\n            if selected_asset.get_class().get_name() != \"LevelSequence\":\n                unreal.log_warning(\n                    f\"Skipping {selected_asset.get_name()}. It isn't a Level \"\n                    \"Sequence.\")\n\n            if pre_create_data.get(\"use_hierarchy\"):\n                # The asset name is the the third element of the path which\n                # contains the map.\n                # To take the asset name, we remove from the path the prefix\n                # \"/Game/OpenPype/\" and then we split the path by \"/\".\n                sel_path = selected_asset_path\n                asset_name = sel_path.replace(\n                    \"/Game/Ayon/\", \"\").split(\"/\")[0]\n\n                search_path = f\"/Game/Ayon/{asset_name}\"\n            else:\n                search_path = Path(selected_asset_path).parent.as_posix()\n\n            # Get the master sequence and the master level.\n            # There should be only one sequence and one level in the directory.\n            try:\n                ar_filter = unreal.ARFilter(\n                    class_names=[\"LevelSequence\"],\n                    package_paths=[search_path],\n                    recursive_paths=False)\n                sequences = ar.get_assets(ar_filter)\n                master_seq = sequences[0].get_asset().get_path_name()\n                master_seq_obj = sequences[0].get_asset()\n                ar_filter = unreal.ARFilter(\n                    class_names=[\"World\"],\n                    package_paths=[search_path],\n                    recursive_paths=False)\n                levels = ar.get_assets(ar_filter)\n                master_lvl = levels[0].get_asset().get_path_name()\n            except IndexError:\n                raise RuntimeError(\n                    f\"Could not find the hierarchy for the selected sequence.\")\n\n            # If the selected asset is the master sequence, we get its data\n            # and then we create the instance for the master sequence.\n            # Otherwise, we cycle from the master sequence to find the selected\n            # sequence and we get its data. This data will be used to create\n            # the instance for the selected sequence. In particular,\n            # we get the frame range of the selected sequence and its final\n            # output path.\n            master_seq_data = {\n                \"sequence\": master_seq_obj,\n                \"output\": f\"{master_seq_obj.get_name()}\",\n                \"frame_range\": (\n                    master_seq_obj.get_playback_start(),\n                    master_seq_obj.get_playback_end())}\n\n            if (selected_asset_path == master_seq or\n                    pre_create_data.get(\"use_hierarchy\")):\n                seq_data = master_seq_data\n            else:\n                seq_data_list = [master_seq_data]\n\n                for seq in seq_data_list:\n                    subscenes = get_subsequences(seq.get('sequence'))\n\n                    for sub_seq in subscenes:\n                        sub_seq_obj = sub_seq.get_sequence()\n                        curr_data = {\n                            \"sequence\": sub_seq_obj,\n                            \"output\": (f\"{seq.get('output')}/\"\n                                       f\"{sub_seq_obj.get_name()}\"),\n                            \"frame_range\": (\n                                sub_seq.get_start_frame(),\n                                sub_seq.get_end_frame() - 1)}\n\n                        # If the selected asset is the current sub-sequence,\n                        # we get its data and we break the loop.\n                        # Otherwise, we add the current sub-sequence data to\n                        # the list of sequences to check.\n                        if sub_seq_obj.get_path_name() == selected_asset_path:\n                            seq_data = curr_data\n                            break\n\n                        seq_data_list.append(curr_data)\n\n                    # If we found the selected asset, we break the loop.\n                    if seq_data is not None:\n                        break\n\n            # If we didn't find the selected asset, we don't create the\n            # instance.\n            if not seq_data:\n                unreal.log_warning(\n                    f\"Skipping {selected_asset.get_name()}. It isn't a \"\n                    \"sub-sequence of the master sequence.\")\n                continue\n\n            self.create_instance(\n                instance_data, subset_name, pre_create_data,\n                selected_asset_path, master_seq, master_lvl, seq_data)\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        if pre_create_data.get(\"create_seq\"):\n            self.create_with_new_sequence(\n                subset_name, instance_data, pre_create_data)\n        else:\n            self.create_from_existing_sequence(\n                subset_name, instance_data, pre_create_data)\n\n    def get_pre_create_attr_defs(self):\n        return [\n            UILabelDef(\n                \"Select a Level Sequence to render or create a new one.\"\n            ),\n            BoolDef(\n                \"create_seq\",\n                label=\"Create a new Level Sequence\",\n                default=False\n            ),\n            UILabelDef(\n                \"WARNING: If you create a new Level Sequence, the current\\n\"\n                \"level will be saved and a new Master Level will be created.\"\n            ),\n            NumberDef(\n                \"start_frame\",\n                label=\"Start Frame\",\n                default=0,\n                minimum=-999999,\n                maximum=999999\n            ),\n            NumberDef(\n                \"end_frame\",\n                label=\"Start Frame\",\n                default=150,\n                minimum=-999999,\n                maximum=999999\n            ),\n            UISeparatorDef(),\n            UILabelDef(\n                \"The following settings are valid only if you are not\\n\"\n                \"creating a new sequence.\"\n            ),\n            BoolDef(\n                \"use_hierarchy\",\n                label=\"Use Hierarchy\",\n                default=False\n            ),\n        ]\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py",
    "content": "# -*- coding: utf-8 -*-\nfrom openpype.hosts.unreal.api.plugin import (\n    UnrealAssetCreator,\n)\n\n\nclass CreateStaticMeshFBX(UnrealAssetCreator):\n    \"\"\"Create Static Meshes as FBX geometry.\"\"\"\n\n    identifier = \"io.ayon.creators.unreal.staticmeshfbx\"\n    label = \"Static Mesh (FBX)\"\n    family = \"unrealStaticMesh\"\n    icon = \"cube\"\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/create/create_uasset.py",
    "content": "# -*- coding: utf-8 -*-\nfrom pathlib import Path\n\nimport unreal\n\nfrom openpype.pipeline import CreatorError\nfrom openpype.hosts.unreal.api.plugin import (\n    UnrealAssetCreator,\n)\n\n\nclass CreateUAsset(UnrealAssetCreator):\n    \"\"\"Create UAsset.\"\"\"\n\n    identifier = \"io.ayon.creators.unreal.uasset\"\n    label = \"UAsset\"\n    family = \"uasset\"\n    icon = \"cube\"\n\n    extension = \".uasset\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        if pre_create_data.get(\"use_selection\"):\n            ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n            sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()\n            selection = [a.get_path_name() for a in sel_objects]\n\n            if len(selection) != 1:\n                raise CreatorError(\"Please select only one object.\")\n\n            obj = selection[0]\n\n            asset = ar.get_asset_by_object_path(obj).get_asset()\n            sys_path = unreal.SystemLibrary.get_system_path(asset)\n\n            if not sys_path:\n                raise CreatorError(\n                    f\"{Path(obj).name} is not on the disk. Likely it needs to\"\n                    \"be saved first.\")\n\n            if Path(sys_path).suffix != self.extension:\n                raise CreatorError(\n                    f\"{Path(sys_path).name} is not a {self.label}.\")\n\n        super(CreateUAsset, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)\n\n\nclass CreateUMap(CreateUAsset):\n    \"\"\"Create Level.\"\"\"\n\n    identifier = \"io.ayon.creators.unreal.umap\"\n    label = \"Level\"\n    family = \"uasset\"\n    extension = \".umap\"\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        instance_data[\"families\"] = [\"umap\"]\n\n        super(CreateUMap, self).create(\n            subset_name,\n            instance_data,\n            pre_create_data)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py",
    "content": "import unreal\n\nfrom openpype.hosts.unreal.api.tools_ui import qt_app_context\nfrom openpype.hosts.unreal.api.pipeline import delete_asset_if_unused\nfrom openpype.pipeline import InventoryAction\n\n\nclass DeleteUnusedAssets(InventoryAction):\n    \"\"\"Delete all the assets that are not used in any level.\n    \"\"\"\n\n    label = \"Delete Unused Assets\"\n    icon = \"trash\"\n    color = \"red\"\n    order = 1\n\n    dialog = None\n\n    def _delete_unused_assets(self, containers):\n        allowed_families = [\"model\", \"rig\"]\n\n        for container in containers:\n            container_dir = container.get(\"namespace\")\n            if container.get(\"family\") not in allowed_families:\n                unreal.log_warning(\n                    f\"Container {container_dir} is not supported.\")\n                continue\n\n            asset_content = unreal.EditorAssetLibrary.list_assets(\n                container_dir, recursive=True, include_folder=False\n            )\n\n            delete_asset_if_unused(container, asset_content)\n\n    def _show_confirmation_dialog(self, containers):\n        from qtpy import QtCore\n        from openpype.widgets import popup\n        from openpype.style import load_stylesheet\n\n        dialog = popup.Popup()\n        dialog.setWindowFlags(\n            QtCore.Qt.Window\n            | QtCore.Qt.WindowStaysOnTopHint\n        )\n        dialog.setFocusPolicy(QtCore.Qt.StrongFocus)\n        dialog.setWindowTitle(\"Delete all unused assets\")\n        dialog.setMessage(\n            \"You are about to delete all the assets in the project that \\n\"\n            \"are not used in any level. Are you sure you want to continue?\"\n        )\n        dialog.setButtonText(\"Delete\")\n\n        dialog.on_clicked.connect(\n            lambda: self._delete_unused_assets(containers)\n        )\n\n        dialog.show()\n        dialog.raise_()\n        dialog.activateWindow()\n        dialog.setStyleSheet(load_stylesheet())\n\n        self.dialog = dialog\n\n    def process(self, containers):\n        with qt_app_context():\n            self._show_confirmation_dialog(containers)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/inventory/update_actors.py",
    "content": "import unreal\n\nfrom openpype.hosts.unreal.api.pipeline import (\n    ls,\n    replace_static_mesh_actors,\n    replace_skeletal_mesh_actors,\n    replace_geometry_cache_actors,\n)\nfrom openpype.pipeline import InventoryAction\n\n\ndef update_assets(containers, selected):\n    allowed_families = [\"model\", \"rig\"]\n\n    # Get all the containers in the Unreal Project\n    all_containers = ls()\n\n    for container in containers:\n        container_dir = container.get(\"namespace\")\n        if container.get(\"family\") not in allowed_families:\n            unreal.log_warning(\n                f\"Container {container_dir} is not supported.\")\n            continue\n\n        # Get all containers with same asset_name but different objectName.\n        # These are the containers that need to be updated in the level.\n        sa_containers = [\n            i\n            for i in all_containers\n            if (\n                i.get(\"asset_name\") == container.get(\"asset_name\") and\n                i.get(\"objectName\") != container.get(\"objectName\")\n            )\n        ]\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            container_dir, recursive=True, include_folder=False\n        )\n\n        # Update all actors in level\n        for sa_cont in sa_containers:\n            sa_dir = sa_cont.get(\"namespace\")\n            old_content = unreal.EditorAssetLibrary.list_assets(\n                sa_dir, recursive=True, include_folder=False\n            )\n\n            if container.get(\"family\") == \"rig\":\n                replace_skeletal_mesh_actors(\n                    old_content, asset_content, selected)\n                replace_static_mesh_actors(\n                    old_content, asset_content, selected)\n            elif container.get(\"family\") == \"model\":\n                if container.get(\"loader\") == \"PointCacheAlembicLoader\":\n                    replace_geometry_cache_actors(\n                        old_content, asset_content, selected)\n                else:\n                    replace_static_mesh_actors(\n                        old_content, asset_content, selected)\n\n            unreal.EditorLevelLibrary.save_current_level()\n\n\nclass UpdateAllActors(InventoryAction):\n    \"\"\"Update all the Actors in the current level to the version of the asset\n    selected in the scene manager.\n    \"\"\"\n\n    label = \"Replace all Actors in level to this version\"\n    icon = \"arrow-up\"\n\n    def process(self, containers):\n        update_assets(containers, False)\n\n\nclass UpdateSelectedActors(InventoryAction):\n    \"\"\"Update only the selected Actors in the current level to the version\n    of the asset selected in the scene manager.\n    \"\"\"\n\n    label = \"Replace selected Actors in level to this version\"\n    icon = \"arrow-up\"\n\n    def process(self, containers):\n        update_assets(containers, True)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/load/load_alembic_animation.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Load Alembic Animation.\"\"\"\nimport os\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AYON_CONTAINER_ID\n)\nfrom openpype.hosts.unreal.api import plugin\nfrom openpype.hosts.unreal.api import pipeline as unreal_pipeline\nimport unreal  # noqa\n\n\nclass AnimationAlembicLoader(plugin.Loader):\n    \"\"\"Load Unreal SkeletalMesh from Alembic\"\"\"\n\n    families = [\"animation\"]\n    label = \"Import Alembic Animation\"\n    representations = [\"abc\"]\n    icon = \"cube\"\n    color = \"orange\"\n\n    def get_task(self, filename, asset_dir, asset_name, replace):\n        task = unreal.AssetImportTask()\n        options = unreal.AbcImportSettings()\n        sm_settings = unreal.AbcStaticMeshSettings()\n        conversion_settings = unreal.AbcConversionSettings(\n            preset=unreal.AbcConversionPreset.CUSTOM,\n            flip_u=False, flip_v=False,\n            rotation=[0.0, 0.0, 0.0],\n            scale=[1.0, 1.0, -1.0])\n\n        task.set_editor_property('filename', filename)\n        task.set_editor_property('destination_path', asset_dir)\n        task.set_editor_property('destination_name', asset_name)\n        task.set_editor_property('replace_existing', replace)\n        task.set_editor_property('automated', True)\n        task.set_editor_property('save', True)\n\n        options.set_editor_property(\n            'import_type', unreal.AlembicImportType.SKELETAL)\n\n        options.static_mesh_settings = sm_settings\n        options.conversion_settings = conversion_settings\n        task.options = options\n\n        return task\n\n    def load(self, context, name, namespace, data):\n        \"\"\"Load and containerise representation into Content Browser.\n\n        This is two step process. First, import FBX to temporary path and\n        then call `containerise()` on it - this moves all content to new\n        directory and then it will create AssetContainer there and imprint it\n        with metadata. This will mark this path as container.\n\n        Args:\n            context (dict): application context\n            name (str): subset name\n            namespace (str): in Unreal this is basically path to container.\n                             This is not passed here, so namespace is set\n                             by `containerise()` because only then we know\n                             real path.\n            data (dict): Those would be data to be imprinted. This is not used\n                         now, data are imprinted by `containerise()`.\n\n        Returns:\n            list(str): list of container content\n        \"\"\"\n\n        # Create directory for asset and ayon container\n        root = unreal_pipeline.AYON_ASSET_DIR\n        asset = context.get('asset').get('name')\n        suffix = \"_CON\"\n        if asset:\n            asset_name = \"{}_{}\".format(asset, name)\n        else:\n            asset_name = \"{}\".format(name)\n        version = context.get('version')\n        # Check if version is hero version and use different name\n        if not version.get(\"name\") and version.get('type') == \"hero_version\":\n            name_version = f\"{name}_hero\"\n        else:\n            name_version = f\"{name}_v{version.get('name'):03d}\"\n\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{root}/{asset}/{name_version}\", suffix=\"\")\n\n        container_name += suffix\n\n        if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):\n            unreal.EditorAssetLibrary.make_directory(asset_dir)\n\n            path = self.filepath_from_context(context)\n            task = self.get_task(path, asset_dir, asset_name, False)\n\n            asset_tools = unreal.AssetToolsHelpers.get_asset_tools()\n            asset_tools.import_asset_tasks([task])\n\n            # Create Asset Container\n            unreal_pipeline.create_container(\n                container=container_name, path=asset_dir)\n\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": asset_dir,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": context[\"representation\"][\"_id\"],\n            \"parent\": context[\"representation\"][\"parent\"],\n            \"family\": context[\"representation\"][\"context\"][\"family\"]\n        }\n        unreal_pipeline.imprint(\n            f\"{asset_dir}/{container_name}\", data)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=True\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n        return asset_content\n\n    def update(self, container, representation):\n        name = container[\"asset_name\"]\n        source_path = get_representation_path(representation)\n        destination_path = container[\"namespace\"]\n\n        task = self.get_task(source_path, destination_path, name, True)\n\n        # do import fbx and replace existing data\n        asset_tools = unreal.AssetToolsHelpers.get_asset_tools()\n        asset_tools.import_asset_tasks([task])\n\n        container_path = f\"{container['namespace']}/{container['objectName']}\"\n\n        # update metadata\n        unreal_pipeline.imprint(\n            container_path,\n            {\n                \"representation\": str(representation[\"_id\"]),\n                \"parent\": str(representation[\"parent\"])\n            })\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            destination_path, recursive=True, include_folder=True\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n    def remove(self, container):\n        path = container[\"namespace\"]\n        parent_path = os.path.dirname(path)\n\n        unreal.EditorAssetLibrary.delete_directory(path)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            parent_path, recursive=False\n        )\n\n        if len(asset_content) == 0:\n            unreal.EditorAssetLibrary.delete_directory(parent_path)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/load/load_animation.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Load FBX with animations.\"\"\"\nimport os\nimport json\n\nimport unreal\nfrom unreal import EditorAssetLibrary\nfrom unreal import MovieSceneSkeletalAnimationTrack\nfrom unreal import MovieSceneSkeletalAnimationSection\n\nfrom openpype.pipeline.context_tools import get_current_project_asset\nfrom openpype.pipeline import (\n    get_representation_path,\n    AYON_CONTAINER_ID\n)\nfrom openpype.hosts.unreal.api import plugin\nfrom openpype.hosts.unreal.api import pipeline as unreal_pipeline\n\n\nclass AnimationFBXLoader(plugin.Loader):\n    \"\"\"Load Unreal SkeletalMesh from FBX.\"\"\"\n\n    families = [\"animation\"]\n    label = \"Import FBX Animation\"\n    representations = [\"fbx\"]\n    icon = \"cube\"\n    color = \"orange\"\n\n    def _process(self, path, asset_dir, asset_name, instance_name):\n        automated = False\n        actor = None\n\n        task = unreal.AssetImportTask()\n        task.options = unreal.FbxImportUI()\n\n        if instance_name:\n            automated = True\n            # Old method to get the actor\n            # actor_name = 'PersistentLevel.' + instance_name\n            # actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name)\n            actors = unreal.EditorLevelLibrary.get_all_level_actors()\n            for a in actors:\n                if a.get_class().get_name() != \"SkeletalMeshActor\":\n                    continue\n                if a.get_actor_label() == instance_name:\n                    actor = a\n                    break\n            if not actor:\n                raise Exception(f\"Could not find actor {instance_name}\")\n            skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton\n            task.options.set_editor_property('skeleton', skeleton)\n\n        if not actor:\n            return None\n\n        asset_doc = get_current_project_asset(fields=[\"data.fps\"])\n\n        task.set_editor_property('filename', path)\n        task.set_editor_property('destination_path', asset_dir)\n        task.set_editor_property('destination_name', asset_name)\n        task.set_editor_property('replace_existing', False)\n        task.set_editor_property('automated', automated)\n        task.set_editor_property('save', False)\n\n        # set import options here\n        task.options.set_editor_property(\n            'automated_import_should_detect_type', False)\n        task.options.set_editor_property(\n            'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH)\n        task.options.set_editor_property(\n            'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION)\n        task.options.set_editor_property('import_mesh', False)\n        task.options.set_editor_property('import_animations', True)\n        task.options.set_editor_property('override_full_name', True)\n\n        task.options.anim_sequence_import_data.set_editor_property(\n            'animation_length',\n            unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME\n        )\n        task.options.anim_sequence_import_data.set_editor_property(\n            'import_meshes_in_bone_hierarchy', False)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'use_default_sample_rate', False)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'custom_sample_rate', asset_doc.get(\"data\", {}).get(\"fps\"))\n        task.options.anim_sequence_import_data.set_editor_property(\n            'import_custom_attribute', True)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'import_bone_tracks', True)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'remove_redundant_keys', False)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'convert_scene', True)\n\n        unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])\n\n        asset_content = EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=True\n        )\n\n        animation = None\n\n        for a in asset_content:\n            imported_asset_data = EditorAssetLibrary.find_asset_data(a)\n            imported_asset = unreal.AssetRegistryHelpers.get_asset(\n                imported_asset_data)\n            if imported_asset.__class__ == unreal.AnimSequence:\n                animation = imported_asset\n                break\n\n        if animation:\n            animation.set_editor_property('enable_root_motion', True)\n            actor.skeletal_mesh_component.set_editor_property(\n                'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE)\n            actor.skeletal_mesh_component.animation_data.set_editor_property(\n                'anim_to_play', animation)\n\n        return animation\n\n    def load(self, context, name, namespace, options=None):\n        \"\"\"\n        Load and containerise representation into Content Browser.\n\n        This is two step process. First, import FBX to temporary path and\n        then call `containerise()` on it - this moves all content to new\n        directory and then it will create AssetContainer there and imprint it\n        with metadata. This will mark this path as container.\n\n        Args:\n            context (dict): application context\n            name (str): subset name\n            namespace (str): in Unreal this is basically path to container.\n                             This is not passed here, so namespace is set\n                             by `containerise()` because only then we know\n                             real path.\n            data (dict): Those would be data to be imprinted. This is not used\n                         now, data are imprinted by `containerise()`.\n\n        Returns:\n            list(str): list of container content\n        \"\"\"\n        # Create directory for asset and Ayon container\n        hierarchy = context.get('asset').get('data').get('parents')\n        root = \"/Game/Ayon\"\n        asset = context.get('asset').get('name')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{root}/Animations/{asset}/{name}\", suffix=\"\")\n\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        _filter = unreal.ARFilter(\n            class_names=[\"World\"],\n            package_paths=[f\"{root}/{hierarchy[0]}\"],\n            recursive_paths=False)\n        levels = ar.get_assets(_filter)\n        master_level = levels[0].get_asset().get_path_name()\n\n        hierarchy_dir = root\n        for h in hierarchy:\n            hierarchy_dir = f\"{hierarchy_dir}/{h}\"\n        hierarchy_dir = f\"{hierarchy_dir}/{asset}\"\n\n        _filter = unreal.ARFilter(\n            class_names=[\"World\"],\n            package_paths=[f\"{hierarchy_dir}/\"],\n            recursive_paths=True)\n        levels = ar.get_assets(_filter)\n        level = levels[0].get_asset().get_path_name()\n\n        unreal.EditorLevelLibrary.save_all_dirty_levels()\n        unreal.EditorLevelLibrary.load_level(level)\n\n        container_name += suffix\n\n        EditorAssetLibrary.make_directory(asset_dir)\n\n        path = self.filepath_from_context(context)\n        libpath = path.replace(\".fbx\", \".json\")\n\n        with open(libpath, \"r\") as fp:\n            data = json.load(fp)\n\n        instance_name = data.get(\"instance_name\")\n\n        animation = self._process(path, asset_dir, asset_name, instance_name)\n\n        asset_content = EditorAssetLibrary.list_assets(\n            hierarchy_dir, recursive=True, include_folder=False)\n\n        # Get the sequence for the layout, excluding the camera one.\n        sequences = [a for a in asset_content\n                     if (EditorAssetLibrary.find_asset_data(a).get_class() ==\n                         unreal.LevelSequence.static_class() and\n                         \"_camera\" not in a.split(\"/\")[-1])]\n\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        for s in sequences:\n            sequence = ar.get_asset_by_object_path(s).get_asset()\n            possessables = [\n                p for p in sequence.get_possessables()\n                if p.get_display_name() == instance_name]\n\n            for p in possessables:\n                tracks = [\n                    t for t in p.get_tracks()\n                    if (t.get_class() ==\n                        MovieSceneSkeletalAnimationTrack.static_class())]\n\n                for t in tracks:\n                    sections = [\n                        s for s in t.get_sections()\n                        if (s.get_class() ==\n                            MovieSceneSkeletalAnimationSection.static_class())]\n\n                    for s in sections:\n                        s.params.set_editor_property('animation', animation)\n\n        # Create Asset Container\n        unreal_pipeline.create_container(\n            container=container_name, path=asset_dir)\n\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": asset_dir,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": context[\"representation\"][\"_id\"],\n            \"parent\": context[\"representation\"][\"parent\"],\n            \"family\": context[\"representation\"][\"context\"][\"family\"]\n        }\n        unreal_pipeline.imprint(f\"{asset_dir}/{container_name}\", data)\n\n        imported_content = EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=False)\n\n        for a in imported_content:\n            EditorAssetLibrary.save_asset(a)\n\n        unreal.EditorLevelLibrary.save_current_level()\n        unreal.EditorLevelLibrary.load_level(master_level)\n\n    def update(self, container, representation):\n        name = container[\"asset_name\"]\n        source_path = get_representation_path(representation)\n        asset_doc = get_current_project_asset(fields=[\"data.fps\"])\n        destination_path = container[\"namespace\"]\n\n        task = unreal.AssetImportTask()\n        task.options = unreal.FbxImportUI()\n\n        task.set_editor_property('filename', source_path)\n        task.set_editor_property('destination_path', destination_path)\n        # strip suffix\n        task.set_editor_property('destination_name', name)\n        task.set_editor_property('replace_existing', True)\n        task.set_editor_property('automated', True)\n        task.set_editor_property('save', True)\n\n        # set import options here\n        task.options.set_editor_property(\n            'automated_import_should_detect_type', False)\n        task.options.set_editor_property(\n            'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH)\n        task.options.set_editor_property(\n            'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION)\n        task.options.set_editor_property('import_mesh', False)\n        task.options.set_editor_property('import_animations', True)\n        task.options.set_editor_property('override_full_name', True)\n\n        task.options.anim_sequence_import_data.set_editor_property(\n            'animation_length',\n            unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME\n        )\n        task.options.anim_sequence_import_data.set_editor_property(\n            'import_meshes_in_bone_hierarchy', False)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'use_default_sample_rate', False)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'custom_sample_rate', asset_doc.get(\"data\", {}).get(\"fps\"))\n        task.options.anim_sequence_import_data.set_editor_property(\n            'import_custom_attribute', True)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'import_bone_tracks', True)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'remove_redundant_keys', False)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'convert_scene', True)\n\n        skeletal_mesh = EditorAssetLibrary.load_asset(\n            container.get('namespace') + \"/\" + container.get('asset_name'))\n        skeleton = skeletal_mesh.get_editor_property('skeleton')\n        task.options.set_editor_property('skeleton', skeleton)\n\n        # do import fbx and replace existing data\n        unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])\n        container_path = f'{container[\"namespace\"]}/{container[\"objectName\"]}'\n        # update metadata\n        unreal_pipeline.imprint(\n            container_path,\n            {\n                \"representation\": str(representation[\"_id\"]),\n                \"parent\": str(representation[\"parent\"])\n            })\n\n        asset_content = EditorAssetLibrary.list_assets(\n            destination_path, recursive=True, include_folder=True\n        )\n\n        for a in asset_content:\n            EditorAssetLibrary.save_asset(a)\n\n    def remove(self, container):\n        path = container[\"namespace\"]\n        parent_path = os.path.dirname(path)\n\n        EditorAssetLibrary.delete_directory(path)\n\n        asset_content = EditorAssetLibrary.list_assets(\n            parent_path, recursive=False, include_folder=True\n        )\n\n        if len(asset_content) == 0:\n            EditorAssetLibrary.delete_directory(parent_path)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/load/load_camera.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Load camera from FBX.\"\"\"\nfrom pathlib import Path\n\nimport unreal\nfrom unreal import (\n    EditorAssetLibrary,\n    EditorLevelLibrary,\n    EditorLevelUtils,\n    LevelSequenceEditorBlueprintLibrary as LevelSequenceLib,\n)\nfrom openpype.client import get_asset_by_name\nfrom openpype.pipeline import (\n    AYON_CONTAINER_ID,\n    get_current_project_name,\n)\nfrom openpype.hosts.unreal.api import plugin\nfrom openpype.hosts.unreal.api.pipeline import (\n    generate_sequence,\n    set_sequence_hierarchy,\n    create_container,\n    imprint,\n)\n\n\nclass CameraLoader(plugin.Loader):\n    \"\"\"Load Unreal StaticMesh from FBX\"\"\"\n\n    families = [\"camera\"]\n    label = \"Load Camera\"\n    representations = [\"fbx\"]\n    icon = \"cube\"\n    color = \"orange\"\n\n    def _import_camera(\n        self, world, sequence, bindings, import_fbx_settings, import_filename\n    ):\n        ue_version = unreal.SystemLibrary.get_engine_version().split('.')\n        ue_major = int(ue_version[0])\n        ue_minor = int(ue_version[1])\n\n        if ue_major == 4 and ue_minor <= 26:\n            unreal.SequencerTools.import_fbx(\n                world,\n                sequence,\n                bindings,\n                import_fbx_settings,\n                import_filename\n            )\n        elif (ue_major == 4 and ue_minor >= 27) or ue_major == 5:\n            unreal.SequencerTools.import_level_sequence_fbx(\n                world,\n                sequence,\n                bindings,\n                import_fbx_settings,\n                import_filename\n            )\n        else:\n            raise NotImplementedError(\n                f\"Unreal version {ue_major} not supported\")\n\n    def load(self, context, name, namespace, data):\n        \"\"\"\n        Load and containerise representation into Content Browser.\n\n        This is two step process. First, import FBX to temporary path and\n        then call `containerise()` on it - this moves all content to new\n        directory and then it will create AssetContainer there and imprint it\n        with metadata. This will mark this path as container.\n\n        Args:\n            context (dict): application context\n            name (str): subset name\n            namespace (str): in Unreal this is basically path to container.\n                             This is not passed here, so namespace is set\n                             by `containerise()` because only then we know\n                             real path.\n            data (dict): Those would be data to be imprinted. This is not used\n                         now, data are imprinted by `containerise()`.\n\n        Returns:\n            list(str): list of container content\n        \"\"\"\n\n        # Create directory for asset and Ayon container\n        hierarchy = context.get('asset').get('data').get('parents')\n        root = \"/Game/Ayon\"\n        hierarchy_dir = root\n        hierarchy_dir_list = []\n        for h in hierarchy:\n            hierarchy_dir = f\"{hierarchy_dir}/{h}\"\n            hierarchy_dir_list.append(hierarchy_dir)\n        asset = context.get('asset').get('name')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n\n        # Create a unique name for the camera directory\n        unique_number = 1\n        if EditorAssetLibrary.does_directory_exist(f\"{hierarchy_dir}/{asset}\"):\n            asset_content = EditorAssetLibrary.list_assets(\n                f\"{root}/{asset}\", recursive=False, include_folder=True\n            )\n\n            # Get highest number to make a unique name\n            folders = [a for a in asset_content\n                       if a[-1] == \"/\" and f\"{name}_\" in a]\n            # Get number from folder name. Splits the string by \"_\" and\n            # removes the last element (which is a \"/\").\n            f_numbers = [int(f.split(\"_\")[-1][:-1]) for f in folders]\n            f_numbers.sort()\n            unique_number = f_numbers[-1] + 1 if f_numbers else 1\n\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}\", suffix=\"\")\n\n        container_name += suffix\n\n        EditorAssetLibrary.make_directory(asset_dir)\n\n        # Create map for the shot, and create hierarchy of map. If the maps\n        # already exist, we will use them.\n        h_dir = hierarchy_dir_list[0]\n        h_asset = hierarchy[0]\n        master_level = f\"{h_dir}/{h_asset}_map.{h_asset}_map\"\n        if not EditorAssetLibrary.does_asset_exist(master_level):\n            EditorLevelLibrary.new_level(f\"{h_dir}/{h_asset}_map\")\n\n        level = f\"{asset_dir}/{asset}_map_camera.{asset}_map_camera\"\n        if not EditorAssetLibrary.does_asset_exist(level):\n            EditorLevelLibrary.new_level(f\"{asset_dir}/{asset}_map_camera\")\n\n            EditorLevelLibrary.load_level(master_level)\n            EditorLevelUtils.add_level_to_world(\n                EditorLevelLibrary.get_editor_world(),\n                level,\n                unreal.LevelStreamingDynamic\n            )\n        EditorLevelLibrary.save_all_dirty_levels()\n        EditorLevelLibrary.load_level(level)\n\n        # Get all the sequences in the hierarchy. It will create them, if\n        # they don't exist.\n        frame_ranges = []\n        sequences = []\n        for (h_dir, h) in zip(hierarchy_dir_list, hierarchy):\n            root_content = EditorAssetLibrary.list_assets(\n                h_dir, recursive=False, include_folder=False)\n\n            existing_sequences = [\n                EditorAssetLibrary.find_asset_data(asset)\n                for asset in root_content\n                if EditorAssetLibrary.find_asset_data(\n                    asset).get_class().get_name() == 'LevelSequence'\n            ]\n\n            if existing_sequences:\n                for seq in existing_sequences:\n                    sequences.append(seq.get_asset())\n                    frame_ranges.append((\n                        seq.get_asset().get_playback_start(),\n                        seq.get_asset().get_playback_end()))\n            else:\n                sequence, frame_range = generate_sequence(h, h_dir)\n\n                sequences.append(sequence)\n                frame_ranges.append(frame_range)\n\n        EditorAssetLibrary.make_directory(asset_dir)\n\n        cam_seq = tools.create_asset(\n            asset_name=f\"{asset}_camera\",\n            package_path=asset_dir,\n            asset_class=unreal.LevelSequence,\n            factory=unreal.LevelSequenceFactoryNew()\n        )\n\n        # Add sequences data to hierarchy\n        for i in range(len(sequences) - 1):\n            set_sequence_hierarchy(\n                sequences[i], sequences[i + 1],\n                frame_ranges[i][1],\n                frame_ranges[i + 1][0], frame_ranges[i + 1][1],\n                [level])\n\n        project_name = get_current_project_name()\n        data = get_asset_by_name(project_name, asset)[\"data\"]\n        cam_seq.set_display_rate(\n            unreal.FrameRate(data.get(\"fps\"), 1.0))\n        cam_seq.set_playback_start(data.get('clipIn'))\n        cam_seq.set_playback_end(data.get('clipOut') + 1)\n        set_sequence_hierarchy(\n            sequences[-1], cam_seq,\n            frame_ranges[-1][1],\n            data.get('clipIn'), data.get('clipOut'),\n            [level])\n\n        settings = unreal.MovieSceneUserImportFBXSettings()\n        settings.set_editor_property('reduce_keys', False)\n\n        if cam_seq:\n            path = self.filepath_from_context(context)\n            self._import_camera(\n                EditorLevelLibrary.get_editor_world(),\n                cam_seq,\n                cam_seq.get_bindings(),\n                settings,\n                path\n            )\n\n        # Set range of all sections\n        # Changing the range of the section is not enough. We need to change\n        # the frame of all the keys in the section.\n        for possessable in cam_seq.get_possessables():\n            for tracks in possessable.get_tracks():\n                for section in tracks.get_sections():\n                    section.set_range(\n                        data.get('clipIn'),\n                        data.get('clipOut') + 1)\n                    for channel in section.get_all_channels():\n                        for key in channel.get_keys():\n                            old_time = key.get_time().get_editor_property(\n                                'frame_number')\n                            old_time_value = old_time.get_editor_property(\n                                'value')\n                            new_time = old_time_value + (\n                                data.get('clipIn') - data.get('frameStart')\n                            )\n                            key.set_time(unreal.FrameNumber(value=new_time))\n\n        # Create Asset Container\n        create_container(\n            container=container_name, path=asset_dir)\n\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": asset_dir,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": context[\"representation\"][\"_id\"],\n            \"parent\": context[\"representation\"][\"parent\"],\n            \"family\": context[\"representation\"][\"context\"][\"family\"]\n        }\n        imprint(f\"{asset_dir}/{container_name}\", data)\n\n        EditorLevelLibrary.save_all_dirty_levels()\n        EditorLevelLibrary.load_level(master_level)\n\n        # Save all assets in the hierarchy\n        asset_content = EditorAssetLibrary.list_assets(\n            hierarchy_dir_list[0], recursive=True, include_folder=False\n        )\n\n        for a in asset_content:\n            EditorAssetLibrary.save_asset(a)\n\n        return asset_content\n\n    def update(self, container, representation):\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        curr_level_sequence = LevelSequenceLib.get_current_level_sequence()\n        curr_time = LevelSequenceLib.get_current_time()\n        is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport()\n\n        editor_subsystem = unreal.UnrealEditorSubsystem()\n        vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info()\n\n        asset_dir = container.get('namespace')\n\n        EditorLevelLibrary.save_current_level()\n\n        _filter = unreal.ARFilter(\n            class_names=[\"LevelSequence\"],\n            package_paths=[asset_dir],\n            recursive_paths=False)\n        sequences = ar.get_assets(_filter)\n        _filter = unreal.ARFilter(\n            class_names=[\"World\"],\n            package_paths=[asset_dir],\n            recursive_paths=True)\n        maps = ar.get_assets(_filter)\n\n        # There should be only one map in the list\n        EditorLevelLibrary.load_level(maps[0].get_asset().get_path_name())\n\n        level_sequence = sequences[0].get_asset()\n\n        display_rate = level_sequence.get_display_rate()\n        playback_start = level_sequence.get_playback_start()\n        playback_end = level_sequence.get_playback_end()\n\n        sequence_name = f\"{container.get('asset')}_camera\"\n\n        # Get the actors in the level sequence.\n        objs = unreal.SequencerTools.get_bound_objects(\n            unreal.EditorLevelLibrary.get_editor_world(),\n            level_sequence,\n            level_sequence.get_bindings(),\n            unreal.SequencerScriptingRange(\n                has_start_value=True,\n                has_end_value=True,\n                inclusive_start=level_sequence.get_playback_start(),\n                exclusive_end=level_sequence.get_playback_end()\n            )\n        )\n\n        # Delete actors from the map\n        for o in objs:\n            if o.bound_objects[0].get_class().get_name() == \"CineCameraActor\":\n                actor_path = o.bound_objects[0].get_path_name().split(\":\")[-1]\n                actor = EditorLevelLibrary.get_actor_reference(actor_path)\n                EditorLevelLibrary.destroy_actor(actor)\n\n        # Remove the Level Sequence from the parent.\n        # We need to traverse the hierarchy from the master sequence to find\n        # the level sequence.\n        root = \"/Game/Ayon\"\n        namespace = container.get('namespace').replace(f\"{root}/\", \"\")\n        ms_asset = namespace.split('/')[0]\n        _filter = unreal.ARFilter(\n            class_names=[\"LevelSequence\"],\n            package_paths=[f\"{root}/{ms_asset}\"],\n            recursive_paths=False)\n        sequences = ar.get_assets(_filter)\n        master_sequence = sequences[0].get_asset()\n        _filter = unreal.ARFilter(\n            class_names=[\"World\"],\n            package_paths=[f\"{root}/{ms_asset}\"],\n            recursive_paths=False)\n        levels = ar.get_assets(_filter)\n        master_level = levels[0].get_asset().get_path_name()\n\n        sequences = [master_sequence]\n\n        parent = None\n        sub_scene = None\n        for s in sequences:\n            tracks = s.get_master_tracks()\n            subscene_track = None\n            for t in tracks:\n                if t.get_class() == unreal.MovieSceneSubTrack.static_class():\n                    subscene_track = t\n            if subscene_track:\n                sections = subscene_track.get_sections()\n                for ss in sections:\n                    if ss.get_sequence().get_name() == sequence_name:\n                        parent = s\n                        sub_scene = ss\n                        break\n                    sequences.append(ss.get_sequence())\n                for i, ss in enumerate(sections):\n                    ss.set_row_index(i)\n            if parent:\n                break\n\n            assert parent, \"Could not find the parent sequence\"\n\n        EditorAssetLibrary.delete_asset(level_sequence.get_path_name())\n\n        settings = unreal.MovieSceneUserImportFBXSettings()\n        settings.set_editor_property('reduce_keys', False)\n\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        new_sequence = tools.create_asset(\n            asset_name=sequence_name,\n            package_path=asset_dir,\n            asset_class=unreal.LevelSequence,\n            factory=unreal.LevelSequenceFactoryNew()\n        )\n\n        new_sequence.set_display_rate(display_rate)\n        new_sequence.set_playback_start(playback_start)\n        new_sequence.set_playback_end(playback_end)\n\n        sub_scene.set_sequence(new_sequence)\n\n        self._import_camera(\n            EditorLevelLibrary.get_editor_world(),\n            new_sequence,\n            new_sequence.get_bindings(),\n            settings,\n            str(representation[\"data\"][\"path\"])\n        )\n\n        # Set range of all sections\n        # Changing the range of the section is not enough. We need to change\n        # the frame of all the keys in the section.\n        project_name = get_current_project_name()\n        asset = container.get('asset')\n        data = get_asset_by_name(project_name, asset)[\"data\"]\n\n        for possessable in new_sequence.get_possessables():\n            for tracks in possessable.get_tracks():\n                for section in tracks.get_sections():\n                    section.set_range(\n                        data.get('clipIn'),\n                        data.get('clipOut') + 1)\n                    for channel in section.get_all_channels():\n                        for key in channel.get_keys():\n                            old_time = key.get_time().get_editor_property(\n                                'frame_number')\n                            old_time_value = old_time.get_editor_property(\n                                'value')\n                            new_time = old_time_value + (\n                                data.get('clipIn') - data.get('frameStart')\n                            )\n                            key.set_time(unreal.FrameNumber(value=new_time))\n\n        data = {\n            \"representation\": str(representation[\"_id\"]),\n            \"parent\": str(representation[\"parent\"])\n        }\n        imprint(f\"{asset_dir}/{container.get('container_name')}\", data)\n\n        EditorLevelLibrary.save_current_level()\n\n        asset_content = EditorAssetLibrary.list_assets(\n            f\"{root}/{ms_asset}\", recursive=True, include_folder=False)\n\n        for a in asset_content:\n            EditorAssetLibrary.save_asset(a)\n\n        EditorLevelLibrary.load_level(master_level)\n\n        if curr_level_sequence:\n            LevelSequenceLib.open_level_sequence(curr_level_sequence)\n            LevelSequenceLib.set_current_time(curr_time)\n            LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock)\n\n        editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot)\n\n    def remove(self, container):\n        asset_dir = container.get('namespace')\n        path = Path(asset_dir)\n\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n        _filter = unreal.ARFilter(\n            class_names=[\"LevelSequence\"],\n            package_paths=[asset_dir],\n            recursive_paths=False)\n        sequences = ar.get_assets(_filter)\n\n        if not sequences:\n            raise Exception(\"Could not find sequence.\")\n\n        world = ar.get_asset_by_object_path(\n            EditorLevelLibrary.get_editor_world().get_path_name())\n\n        _filter = unreal.ARFilter(\n            class_names=[\"World\"],\n            package_paths=[asset_dir],\n            recursive_paths=True)\n        maps = ar.get_assets(_filter)\n\n        # There should be only one map in the list\n        if not maps:\n            raise Exception(\"Could not find map.\")\n\n        map = maps[0]\n\n        EditorLevelLibrary.save_all_dirty_levels()\n        EditorLevelLibrary.load_level(map.get_asset().get_path_name())\n\n        # Remove the camera from the level.\n        actors = EditorLevelLibrary.get_all_level_actors()\n\n        for a in actors:\n            if a.__class__ == unreal.CineCameraActor:\n                EditorLevelLibrary.destroy_actor(a)\n\n        EditorLevelLibrary.save_all_dirty_levels()\n        EditorLevelLibrary.load_level(world.get_asset().get_path_name())\n\n        # There should be only one sequence in the path.\n        sequence_name = sequences[0].asset_name\n\n        # Remove the Level Sequence from the parent.\n        # We need to traverse the hierarchy from the master sequence to find\n        # the level sequence.\n        root = \"/Game/Ayon\"\n        namespace = container.get('namespace').replace(f\"{root}/\", \"\")\n        ms_asset = namespace.split('/')[0]\n        _filter = unreal.ARFilter(\n            class_names=[\"LevelSequence\"],\n            package_paths=[f\"{root}/{ms_asset}\"],\n            recursive_paths=False)\n        sequences = ar.get_assets(_filter)\n        master_sequence = sequences[0].get_asset()\n        _filter = unreal.ARFilter(\n            class_names=[\"World\"],\n            package_paths=[f\"{root}/{ms_asset}\"],\n            recursive_paths=False)\n        levels = ar.get_assets(_filter)\n        master_level = levels[0].get_full_name()\n\n        sequences = [master_sequence]\n\n        parent = None\n        for s in sequences:\n            tracks = s.get_master_tracks()\n            subscene_track = None\n            visibility_track = None\n            for t in tracks:\n                if t.get_class() == unreal.MovieSceneSubTrack.static_class():\n                    subscene_track = t\n                if (t.get_class() ==\n                        unreal.MovieSceneLevelVisibilityTrack.static_class()):\n                    visibility_track = t\n            if subscene_track:\n                sections = subscene_track.get_sections()\n                for ss in sections:\n                    if ss.get_sequence().get_name() == sequence_name:\n                        parent = s\n                        subscene_track.remove_section(ss)\n                        break\n                    sequences.append(ss.get_sequence())\n                # Update subscenes indexes.\n                for i, ss in enumerate(sections):\n                    ss.set_row_index(i)\n\n            if visibility_track:\n                sections = visibility_track.get_sections()\n                for ss in sections:\n                    if (unreal.Name(f\"{container.get('asset')}_map_camera\")\n                            in ss.get_level_names()):\n                        visibility_track.remove_section(ss)\n                # Update visibility sections indexes.\n                i = -1\n                prev_name = []\n                for ss in sections:\n                    if prev_name != ss.get_level_names():\n                        i += 1\n                    ss.set_row_index(i)\n                    prev_name = ss.get_level_names()\n            if parent:\n                break\n\n        assert parent, \"Could not find the parent sequence\"\n\n        # Create a temporary level to delete the layout level.\n        EditorLevelLibrary.save_all_dirty_levels()\n        EditorAssetLibrary.make_directory(f\"{root}/tmp\")\n        tmp_level = f\"{root}/tmp/temp_map\"\n        if not EditorAssetLibrary.does_asset_exist(f\"{tmp_level}.temp_map\"):\n            EditorLevelLibrary.new_level(tmp_level)\n        else:\n            EditorLevelLibrary.load_level(tmp_level)\n\n        # Delete the layout directory.\n        EditorAssetLibrary.delete_directory(asset_dir)\n\n        EditorLevelLibrary.load_level(master_level)\n        EditorAssetLibrary.delete_directory(f\"{root}/tmp\")\n\n        # Check if there isn't any more assets in the parent folder, and\n        # delete it if not.\n        asset_content = EditorAssetLibrary.list_assets(\n            path.parent.as_posix(), recursive=False, include_folder=True\n        )\n\n        if len(asset_content) == 0:\n            EditorAssetLibrary.delete_directory(path.parent.as_posix())\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Loader for published alembics.\"\"\"\nimport os\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AYON_CONTAINER_ID\n)\nfrom openpype.hosts.unreal.api import plugin\nfrom openpype.hosts.unreal.api.pipeline import (\n    AYON_ASSET_DIR,\n    create_container,\n    imprint,\n)\n\nimport unreal  # noqa\n\n\nclass PointCacheAlembicLoader(plugin.Loader):\n    \"\"\"Load Point Cache from Alembic\"\"\"\n\n    families = [\"model\", \"pointcache\"]\n    label = \"Import Alembic Point Cache\"\n    representations = [\"abc\"]\n    icon = \"cube\"\n    color = \"orange\"\n\n    root = AYON_ASSET_DIR\n\n    @staticmethod\n    def get_task(\n        filename, asset_dir, asset_name, replace,\n        frame_start=None, frame_end=None\n    ):\n        task = unreal.AssetImportTask()\n        options = unreal.AbcImportSettings()\n        gc_settings = unreal.AbcGeometryCacheSettings()\n        conversion_settings = unreal.AbcConversionSettings()\n        sampling_settings = unreal.AbcSamplingSettings()\n\n        task.set_editor_property('filename', filename)\n        task.set_editor_property('destination_path', asset_dir)\n        task.set_editor_property('destination_name', asset_name)\n        task.set_editor_property('replace_existing', replace)\n        task.set_editor_property('automated', True)\n        task.set_editor_property('save', True)\n\n        options.set_editor_property(\n            'import_type', unreal.AlembicImportType.GEOMETRY_CACHE)\n\n        gc_settings.set_editor_property('flatten_tracks', False)\n\n        conversion_settings.set_editor_property('flip_u', False)\n        conversion_settings.set_editor_property('flip_v', True)\n        conversion_settings.set_editor_property(\n            'scale', unreal.Vector(x=100.0, y=100.0, z=100.0))\n        conversion_settings.set_editor_property(\n            'rotation', unreal.Vector(x=-90.0, y=0.0, z=180.0))\n\n        if frame_start is not None:\n            sampling_settings.set_editor_property('frame_start', frame_start)\n        if frame_end is not None:\n            sampling_settings.set_editor_property('frame_end', frame_end)\n\n        options.geometry_cache_settings = gc_settings\n        options.conversion_settings = conversion_settings\n        options.sampling_settings = sampling_settings\n        task.options = options\n\n        return task\n\n    def import_and_containerize(\n        self, filepath, asset_dir, asset_name, container_name,\n        frame_start, frame_end\n    ):\n        unreal.EditorAssetLibrary.make_directory(asset_dir)\n\n        task = self.get_task(\n            filepath, asset_dir, asset_name, False, frame_start, frame_end)\n\n        unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])\n\n        # Create Asset Container\n        create_container(container=container_name, path=asset_dir)\n\n    def imprint(\n        self, asset, asset_dir, container_name, asset_name, representation,\n        frame_start, frame_end\n    ):\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": asset_dir,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": representation[\"_id\"],\n            \"parent\": representation[\"parent\"],\n            \"family\": representation[\"context\"][\"family\"],\n            \"frame_start\": frame_start,\n            \"frame_end\": frame_end\n        }\n        imprint(f\"{asset_dir}/{container_name}\", data)\n\n    def load(self, context, name, namespace, options):\n        \"\"\"Load and containerise representation into Content Browser.\n\n        Args:\n            context (dict): application context\n            name (str): subset name\n            namespace (str): in Unreal this is basically path to container.\n                             This is not passed here, so namespace is set\n                             by `containerise()` because only then we know\n                             real path.\n            data (dict): Those would be data to be imprinted.\n\n        Returns:\n            list(str): list of container content\n        \"\"\"\n        # Create directory for asset and Ayon container\n        asset = context.get('asset').get('name')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n        version = context.get('version')\n        # Check if version is hero version and use different name\n        if not version.get(\"name\") and version.get('type') == \"hero_version\":\n            name_version = f\"{name}_hero\"\n        else:\n            name_version = f\"{name}_v{version.get('name'):03d}\"\n\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{self.root}/{asset}/{name_version}\", suffix=\"\")\n\n        container_name += suffix\n\n        frame_start = context.get('asset').get('data').get('frameStart')\n        frame_end = context.get('asset').get('data').get('frameEnd')\n\n        # If frame start and end are the same, we increase the end frame by\n        # one, otherwise Unreal will not import it\n        if frame_start == frame_end:\n            frame_end += 1\n\n        if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):\n            path = self.filepath_from_context(context)\n\n            self.import_and_containerize(\n                path, asset_dir, asset_name, container_name,\n                frame_start, frame_end)\n\n        self.imprint(\n            asset, asset_dir, container_name, asset_name,\n            context[\"representation\"], frame_start, frame_end)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=True\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n        return asset_content\n\n    def update(self, container, representation):\n        context = representation.get(\"context\", {})\n\n        unreal.log_warning(context)\n\n        if not context:\n            raise RuntimeError(\"No context found in representation\")\n\n        # Create directory for asset and Ayon container\n        asset = context.get('asset')\n        name = context.get('subset')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n        version = context.get('version')\n        # Check if version is hero version and use different name\n        name_version = f\"{name}_v{version:03d}\" if version else f\"{name}_hero\"\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{self.root}/{asset}/{name_version}\", suffix=\"\")\n\n        container_name += suffix\n\n        frame_start = int(container.get(\"frame_start\"))\n        frame_end = int(container.get(\"frame_end\"))\n\n        if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):\n            path = get_representation_path(representation)\n\n            self.import_and_containerize(\n                path, asset_dir, asset_name, container_name,\n                frame_start, frame_end)\n\n        self.imprint(\n            asset, asset_dir, container_name, asset_name, representation,\n            frame_start, frame_end)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=False\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n    def remove(self, container):\n        path = container[\"namespace\"]\n        parent_path = os.path.dirname(path)\n\n        unreal.EditorAssetLibrary.delete_directory(path)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            parent_path, recursive=False\n        )\n\n        if len(asset_content) == 0:\n            unreal.EditorAssetLibrary.delete_directory(parent_path)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/load/load_layout.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Loader for layouts.\"\"\"\nimport json\nimport collections\nfrom pathlib import Path\n\nimport unreal\nfrom unreal import (\n    EditorAssetLibrary,\n    EditorLevelLibrary,\n    EditorLevelUtils,\n    AssetToolsHelpers,\n    FBXImportType,\n    MovieSceneLevelVisibilityTrack,\n    MovieSceneSubTrack,\n    LevelSequenceEditorBlueprintLibrary as LevelSequenceLib,\n)\n\nfrom openpype.client import get_asset_by_name, get_representations\nfrom openpype.pipeline import (\n    discover_loader_plugins,\n    loaders_from_representation,\n    load_container,\n    get_representation_path,\n    AYON_CONTAINER_ID,\n    get_current_project_name,\n)\nfrom openpype.pipeline.context_tools import get_current_project_asset\nfrom openpype.settings import get_current_project_settings\nfrom openpype.hosts.unreal.api import plugin\nfrom openpype.hosts.unreal.api.pipeline import (\n    generate_sequence,\n    set_sequence_hierarchy,\n    create_container,\n    imprint,\n    ls,\n)\n\n\nclass LayoutLoader(plugin.Loader):\n    \"\"\"Load Layout from a JSON file\"\"\"\n\n    families = [\"layout\"]\n    representations = [\"json\"]\n\n    label = \"Load Layout\"\n    icon = \"code-fork\"\n    color = \"orange\"\n    ASSET_ROOT = \"/Game/Ayon\"\n\n    def _get_asset_containers(self, path):\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        asset_content = EditorAssetLibrary.list_assets(\n            path, recursive=True)\n\n        asset_containers = []\n\n        # Get all the asset containers\n        for a in asset_content:\n            obj = ar.get_asset_by_object_path(a)\n            if obj.get_asset().get_class().get_name() == 'AyonAssetContainer':\n                asset_containers.append(obj)\n\n        return asset_containers\n\n    @staticmethod\n    def _get_fbx_loader(loaders, family):\n        name = \"\"\n        if family == 'rig':\n            name = \"SkeletalMeshFBXLoader\"\n        elif family == 'model':\n            name = \"StaticMeshFBXLoader\"\n        elif family == 'camera':\n            name = \"CameraLoader\"\n\n        if name == \"\":\n            return None\n\n        for loader in loaders:\n            if loader.__name__ == name:\n                return loader\n\n        return None\n\n    @staticmethod\n    def _get_abc_loader(loaders, family):\n        name = \"\"\n        if family == 'rig':\n            name = \"SkeletalMeshAlembicLoader\"\n        elif family == 'model':\n            name = \"StaticMeshAlembicLoader\"\n\n        if name == \"\":\n            return None\n\n        for loader in loaders:\n            if loader.__name__ == name:\n                return loader\n\n        return None\n\n    def _transform_from_basis(self, transform, basis):\n        \"\"\"Transform a transform from a basis to a new basis.\"\"\"\n        # Get the basis matrix\n        basis_matrix = unreal.Matrix(\n            basis[0],\n            basis[1],\n            basis[2],\n            basis[3]\n        )\n        transform_matrix = unreal.Matrix(\n            transform[0],\n            transform[1],\n            transform[2],\n            transform[3]\n        )\n\n        new_transform = (\n            basis_matrix.get_inverse() * transform_matrix * basis_matrix)\n\n        return new_transform.transform()\n\n    def _process_family(\n        self, assets, class_name, transform, basis, sequence, inst_name=None\n    ):\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        actors = []\n        bindings = []\n\n        for asset in assets:\n            obj = ar.get_asset_by_object_path(asset).get_asset()\n            if obj.get_class().get_name() == class_name:\n                t = self._transform_from_basis(transform, basis)\n                actor = EditorLevelLibrary.spawn_actor_from_object(\n                    obj, t.translation\n                )\n                actor.set_actor_rotation(t.rotation.rotator(), False)\n                actor.set_actor_scale3d(t.scale3d)\n\n                if class_name == 'SkeletalMesh':\n                    skm_comp = actor.get_editor_property(\n                        'skeletal_mesh_component')\n                    skm_comp.set_bounds_scale(10.0)\n\n                actors.append(actor)\n\n                if sequence:\n                    binding = None\n                    for p in sequence.get_possessables():\n                        if p.get_name() == actor.get_name():\n                            binding = p\n                            break\n\n                    if not binding:\n                        binding = sequence.add_possessable(actor)\n\n                    bindings.append(binding)\n\n        return actors, bindings\n\n    def _import_animation(\n        self, asset_dir, path, instance_name, skeleton, actors_dict,\n        animation_file, bindings_dict, sequence\n    ):\n        anim_file = Path(animation_file)\n        anim_file_name = anim_file.with_suffix('')\n\n        anim_path = f\"{asset_dir}/animations/{anim_file_name}\"\n\n        asset_doc = get_current_project_asset()\n        # Import animation\n        task = unreal.AssetImportTask()\n        task.options = unreal.FbxImportUI()\n\n        task.set_editor_property(\n            'filename', str(path.with_suffix(f\".{animation_file}\")))\n        task.set_editor_property('destination_path', anim_path)\n        task.set_editor_property(\n            'destination_name', f\"{instance_name}_animation\")\n        task.set_editor_property('replace_existing', False)\n        task.set_editor_property('automated', True)\n        task.set_editor_property('save', False)\n\n        # set import options here\n        task.options.set_editor_property(\n            'automated_import_should_detect_type', False)\n        task.options.set_editor_property(\n            'original_import_type', FBXImportType.FBXIT_SKELETAL_MESH)\n        task.options.set_editor_property(\n            'mesh_type_to_import', FBXImportType.FBXIT_ANIMATION)\n        task.options.set_editor_property('import_mesh', False)\n        task.options.set_editor_property('import_animations', True)\n        task.options.set_editor_property('override_full_name', True)\n        task.options.set_editor_property('skeleton', skeleton)\n\n        task.options.anim_sequence_import_data.set_editor_property(\n            'animation_length',\n            unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME\n        )\n        task.options.anim_sequence_import_data.set_editor_property(\n            'import_meshes_in_bone_hierarchy', False)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'use_default_sample_rate', False)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'custom_sample_rate', asset_doc.get(\"data\", {}).get(\"fps\"))\n        task.options.anim_sequence_import_data.set_editor_property(\n            'import_custom_attribute', True)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'import_bone_tracks', True)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'remove_redundant_keys', False)\n        task.options.anim_sequence_import_data.set_editor_property(\n            'convert_scene', True)\n\n        AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            anim_path, recursive=False, include_folder=False\n        )\n\n        animation = None\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n            imported_asset_data = unreal.EditorAssetLibrary.find_asset_data(a)\n            imported_asset = unreal.AssetRegistryHelpers.get_asset(\n                imported_asset_data)\n            if imported_asset.__class__ == unreal.AnimSequence:\n                animation = imported_asset\n                break\n\n        if animation:\n            actor = None\n            if actors_dict.get(instance_name):\n                for a in actors_dict.get(instance_name):\n                    if a.get_class().get_name() == 'SkeletalMeshActor':\n                        actor = a\n                        break\n\n            animation.set_editor_property('enable_root_motion', True)\n            actor.skeletal_mesh_component.set_editor_property(\n                'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE)\n            actor.skeletal_mesh_component.animation_data.set_editor_property(\n                'anim_to_play', animation)\n\n            if sequence:\n                # Add animation to the sequencer\n                bindings = bindings_dict.get(instance_name)\n\n                ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n                for binding in bindings:\n                    tracks = binding.get_tracks()\n                    track = None\n                    track = tracks[0] if tracks else binding.add_track(\n                        unreal.MovieSceneSkeletalAnimationTrack)\n\n                    sections = track.get_sections()\n                    section = None\n                    if not sections:\n                        section = track.add_section()\n                    else:\n                        section = sections[0]\n\n                        sec_params = section.get_editor_property('params')\n                        curr_anim = sec_params.get_editor_property('animation')\n\n                        if curr_anim:\n                            # Checks if the animation path has a container.\n                            # If it does, it means that the animation is\n                            # already in the sequencer.\n                            anim_path = str(Path(\n                                curr_anim.get_path_name()).parent\n                            ).replace('\\\\', '/')\n\n                            _filter = unreal.ARFilter(\n                                class_names=[\"AyonAssetContainer\"],\n                                package_paths=[anim_path],\n                                recursive_paths=False)\n                            containers = ar.get_assets(_filter)\n\n                            if len(containers) > 0:\n                                return\n\n                    section.set_range(\n                        sequence.get_playback_start(),\n                        sequence.get_playback_end())\n                    sec_params = section.get_editor_property('params')\n                    sec_params.set_editor_property('animation', animation)\n\n    def _get_repre_docs_by_version_id(self, data):\n        version_ids = {\n            element.get(\"version\")\n            for element in data\n            if element.get(\"representation\")\n        }\n        version_ids.discard(None)\n\n        output = collections.defaultdict(list)\n        if not version_ids:\n            return output\n\n        project_name = get_current_project_name()\n        repre_docs = get_representations(\n            project_name,\n            representation_names=[\"fbx\", \"abc\"],\n            version_ids=version_ids,\n            fields=[\"_id\", \"parent\", \"name\"]\n        )\n        for repre_doc in repre_docs:\n            version_id = str(repre_doc[\"parent\"])\n            output[version_id].append(repre_doc)\n        return output\n\n    def _process(self, lib_path, asset_dir, sequence, repr_loaded=None):\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        with open(lib_path, \"r\") as fp:\n            data = json.load(fp)\n\n        all_loaders = discover_loader_plugins()\n\n        if not repr_loaded:\n            repr_loaded = []\n\n        path = Path(lib_path)\n\n        skeleton_dict = {}\n        actors_dict = {}\n        bindings_dict = {}\n\n        loaded_assets = []\n\n        repre_docs_by_version_id = self._get_repre_docs_by_version_id(data)\n        for element in data:\n            representation = None\n            repr_format = None\n            if element.get('representation'):\n                repre_docs = repre_docs_by_version_id[element.get(\"version\")]\n                if not repre_docs:\n                    self.log.error(\n                        f\"No valid representation found for version \"\n                        f\"{element.get('version')}\")\n                    continue\n                repre_doc = repre_docs[0]\n                representation = str(repre_doc[\"_id\"])\n                repr_format = repre_doc[\"name\"]\n\n            # This is to keep compatibility with old versions of the\n            # json format.\n            elif element.get('reference_fbx'):\n                representation = element.get('reference_fbx')\n                repr_format = 'fbx'\n            elif element.get('reference_abc'):\n                representation = element.get('reference_abc')\n                repr_format = 'abc'\n\n            # If reference is None, this element is skipped, as it cannot be\n            # imported in Unreal\n            if not representation:\n                continue\n\n            instance_name = element.get('instance_name')\n\n            skeleton = None\n\n            if representation not in repr_loaded:\n                repr_loaded.append(representation)\n\n                family = element.get('family')\n                loaders = loaders_from_representation(\n                    all_loaders, representation)\n\n                loader = None\n\n                if repr_format == 'fbx':\n                    loader = self._get_fbx_loader(loaders, family)\n                elif repr_format == 'abc':\n                    loader = self._get_abc_loader(loaders, family)\n\n                if not loader:\n                    self.log.error(\n                        f\"No valid loader found for {representation}\")\n                    continue\n\n                options = {\n                    # \"asset_dir\": asset_dir\n                }\n\n                assets = load_container(\n                    loader,\n                    representation,\n                    namespace=instance_name,\n                    options=options\n                )\n\n                container = None\n\n                for asset in assets:\n                    obj = ar.get_asset_by_object_path(asset).get_asset()\n                    if obj.get_class().get_name() == 'AyonAssetContainer':\n                        container = obj\n                    if obj.get_class().get_name() == 'Skeleton':\n                        skeleton = obj\n\n                loaded_assets.append(container.get_path_name())\n\n                instances = [\n                    item for item in data\n                    if ((item.get('version') and\n                        item.get('version') == element.get('version')) or\n                        item.get('reference_fbx') == representation or\n                        item.get('reference_abc') == representation)]\n\n                for instance in instances:\n                    # transform = instance.get('transform')\n                    transform = instance.get('transform_matrix')\n                    basis = instance.get('basis')\n                    inst = instance.get('instance_name')\n\n                    actors = []\n\n                    if family == 'model':\n                        actors, _ = self._process_family(\n                            assets, 'StaticMesh', transform, basis,\n                            sequence, inst\n                        )\n                    elif family == 'rig':\n                        actors, bindings = self._process_family(\n                            assets, 'SkeletalMesh', transform, basis,\n                            sequence, inst\n                        )\n                        actors_dict[inst] = actors\n                        bindings_dict[inst] = bindings\n\n                if skeleton:\n                    skeleton_dict[representation] = skeleton\n            else:\n                skeleton = skeleton_dict.get(representation)\n\n            animation_file = element.get('animation')\n\n            if animation_file and skeleton:\n                self._import_animation(\n                    asset_dir, path, instance_name, skeleton, actors_dict,\n                    animation_file, bindings_dict, sequence)\n\n        return loaded_assets\n\n    @staticmethod\n    def _remove_family(assets, components, class_name, prop_name):\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        objects = []\n        for a in assets:\n            obj = ar.get_asset_by_object_path(a)\n            if obj.get_asset().get_class().get_name() == class_name:\n                objects.append(obj)\n        for obj in objects:\n            for comp in components:\n                if comp.get_editor_property(prop_name) == obj.get_asset():\n                    comp.get_owner().destroy_actor()\n\n    def _remove_actors(self, path):\n        asset_containers = self._get_asset_containers(path)\n\n        # Get all the static and skeletal meshes components in the level\n        components = EditorLevelLibrary.get_all_level_actors_components()\n        static_meshes_comp = [\n            c for c in components\n            if c.get_class().get_name() == 'StaticMeshComponent']\n        skel_meshes_comp = [\n            c for c in components\n            if c.get_class().get_name() == 'SkeletalMeshComponent']\n\n        # For all the asset containers, get the static and skeletal meshes.\n        # Then, check the components in the level and destroy the matching\n        # actors.\n        for asset_container in asset_containers:\n            package_path = asset_container.get_editor_property('package_path')\n            family = EditorAssetLibrary.get_metadata_tag(\n                asset_container.get_asset(), 'family')\n            assets = EditorAssetLibrary.list_assets(\n                str(package_path), recursive=False)\n            if family == 'model':\n                self._remove_family(\n                    assets, static_meshes_comp, 'StaticMesh', 'static_mesh')\n            elif family == 'rig':\n                self._remove_family(\n                    assets, skel_meshes_comp, 'SkeletalMesh', 'skeletal_mesh')\n\n    def load(self, context, name, namespace, options):\n        \"\"\"Load and containerise representation into Content Browser.\n\n        This is two step process. First, import FBX to temporary path and\n        then call `containerise()` on it - this moves all content to new\n        directory and then it will create AssetContainer there and imprint it\n        with metadata. This will mark this path as container.\n\n        Args:\n            context (dict): application context\n            name (str): subset name\n            namespace (str): in Unreal this is basically path to container.\n                             This is not passed here, so namespace is set\n                             by `containerise()` because only then we know\n                             real path.\n            options (dict): Those would be data to be imprinted. This is not\n                used now, data are imprinted by `containerise()`.\n\n        Returns:\n            list(str): list of container content\n        \"\"\"\n        data = get_current_project_settings()\n        create_sequences = data[\"unreal\"][\"level_sequences_for_layouts\"]\n\n        # Create directory for asset and Ayon container\n        hierarchy = context.get('asset').get('data').get('parents')\n        root = self.ASSET_ROOT\n        hierarchy_dir = root\n        hierarchy_dir_list = []\n        for h in hierarchy:\n            hierarchy_dir = f\"{hierarchy_dir}/{h}\"\n            hierarchy_dir_list.append(hierarchy_dir)\n        asset = context.get('asset').get('name')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else name\n\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            \"{}/{}/{}\".format(hierarchy_dir, asset, name), suffix=\"\")\n\n        container_name += suffix\n\n        EditorAssetLibrary.make_directory(asset_dir)\n\n        master_level = None\n        shot = None\n        sequences = []\n\n        level = f\"{asset_dir}/{asset}_map.{asset}_map\"\n        EditorLevelLibrary.new_level(f\"{asset_dir}/{asset}_map\")\n\n        if create_sequences:\n            # Create map for the shot, and create hierarchy of map. If the\n            # maps already exist, we will use them.\n            if hierarchy:\n                h_dir = hierarchy_dir_list[0]\n                h_asset = hierarchy[0]\n                master_level = f\"{h_dir}/{h_asset}_map.{h_asset}_map\"\n                if not EditorAssetLibrary.does_asset_exist(master_level):\n                    EditorLevelLibrary.new_level(f\"{h_dir}/{h_asset}_map\")\n\n            if master_level:\n                EditorLevelLibrary.load_level(master_level)\n                EditorLevelUtils.add_level_to_world(\n                    EditorLevelLibrary.get_editor_world(),\n                    level,\n                    unreal.LevelStreamingDynamic\n                )\n                EditorLevelLibrary.save_all_dirty_levels()\n                EditorLevelLibrary.load_level(level)\n\n            # Get all the sequences in the hierarchy. It will create them, if\n            # they don't exist.\n            frame_ranges = []\n            for (h_dir, h) in zip(hierarchy_dir_list, hierarchy):\n                root_content = EditorAssetLibrary.list_assets(\n                    h_dir, recursive=False, include_folder=False)\n\n                existing_sequences = [\n                    EditorAssetLibrary.find_asset_data(asset)\n                    for asset in root_content\n                    if EditorAssetLibrary.find_asset_data(\n                        asset).get_class().get_name() == 'LevelSequence'\n                ]\n\n                if not existing_sequences:\n                    sequence, frame_range = generate_sequence(h, h_dir)\n\n                    sequences.append(sequence)\n                    frame_ranges.append(frame_range)\n                else:\n                    for e in existing_sequences:\n                        sequences.append(e.get_asset())\n                        frame_ranges.append((\n                            e.get_asset().get_playback_start(),\n                            e.get_asset().get_playback_end()))\n\n            shot = tools.create_asset(\n                asset_name=asset,\n                package_path=asset_dir,\n                asset_class=unreal.LevelSequence,\n                factory=unreal.LevelSequenceFactoryNew()\n            )\n\n            # sequences and frame_ranges have the same length\n            for i in range(0, len(sequences) - 1):\n                set_sequence_hierarchy(\n                    sequences[i], sequences[i + 1],\n                    frame_ranges[i][1],\n                    frame_ranges[i + 1][0], frame_ranges[i + 1][1],\n                    [level])\n\n            project_name = get_current_project_name()\n            data = get_asset_by_name(project_name, asset)[\"data\"]\n            shot.set_display_rate(\n                unreal.FrameRate(data.get(\"fps\"), 1.0))\n            shot.set_playback_start(0)\n            shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1)\n            if sequences:\n                set_sequence_hierarchy(\n                    sequences[-1], shot,\n                    frame_ranges[-1][1],\n                    data.get('clipIn'), data.get('clipOut'),\n                    [level])\n\n            EditorLevelLibrary.load_level(level)\n\n        path = self.filepath_from_context(context)\n        loaded_assets = self._process(path, asset_dir, shot)\n\n        for s in sequences:\n            EditorAssetLibrary.save_asset(s.get_path_name())\n\n        EditorLevelLibrary.save_current_level()\n\n        # Create Asset Container\n        create_container(\n            container=container_name, path=asset_dir)\n\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": asset_dir,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": context[\"representation\"][\"_id\"],\n            \"parent\": context[\"representation\"][\"parent\"],\n            \"family\": context[\"representation\"][\"context\"][\"family\"],\n            \"loaded_assets\": loaded_assets\n        }\n        imprint(\n            \"{}/{}\".format(asset_dir, container_name), data)\n\n        save_dir = hierarchy_dir_list[0] if create_sequences else asset_dir\n\n        asset_content = EditorAssetLibrary.list_assets(\n            save_dir, recursive=True, include_folder=False)\n\n        for a in asset_content:\n            EditorAssetLibrary.save_asset(a)\n\n        if master_level:\n            EditorLevelLibrary.load_level(master_level)\n\n        return asset_content\n\n    def update(self, container, representation):\n        data = get_current_project_settings()\n        create_sequences = data[\"unreal\"][\"level_sequences_for_layouts\"]\n\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        curr_level_sequence = LevelSequenceLib.get_current_level_sequence()\n        curr_time = LevelSequenceLib.get_current_time()\n        is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport()\n\n        editor_subsystem = unreal.UnrealEditorSubsystem()\n        vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info()\n\n        root = \"/Game/Ayon\"\n\n        asset_dir = container.get('namespace')\n        context = representation.get(\"context\")\n\n        hierarchy = context.get('hierarchy').split(\"/\")\n\n        sequence = None\n        master_level = None\n\n        if create_sequences:\n            h_dir = f\"{root}/{hierarchy[0]}\"\n            h_asset = hierarchy[0]\n            master_level = f\"{h_dir}/{h_asset}_map.{h_asset}_map\"\n\n            filter = unreal.ARFilter(\n                class_names=[\"LevelSequence\"],\n                package_paths=[asset_dir],\n                recursive_paths=False)\n            sequences = ar.get_assets(filter)\n            sequence = sequences[0].get_asset()\n\n        prev_level = None\n\n        if not master_level:\n            curr_level = unreal.LevelEditorSubsystem().get_current_level()\n            curr_level_path = curr_level.get_outer().get_path_name()\n            # If the level path does not start with \"/Game/\", the current\n            # level is a temporary, unsaved level.\n            if curr_level_path.startswith(\"/Game/\"):\n                prev_level = curr_level_path\n\n        # Get layout level\n        filter = unreal.ARFilter(\n            class_names=[\"World\"],\n            package_paths=[asset_dir],\n            recursive_paths=False)\n        levels = ar.get_assets(filter)\n\n        layout_level = levels[0].get_asset().get_path_name()\n\n        EditorLevelLibrary.save_all_dirty_levels()\n        EditorLevelLibrary.load_level(layout_level)\n\n        # Delete all the actors in the level\n        actors = unreal.EditorLevelLibrary.get_all_level_actors()\n        for actor in actors:\n            unreal.EditorLevelLibrary.destroy_actor(actor)\n\n        if create_sequences:\n            EditorLevelLibrary.save_current_level()\n\n        EditorAssetLibrary.delete_directory(f\"{asset_dir}/animations/\")\n\n        source_path = get_representation_path(representation)\n\n        loaded_assets = self._process(source_path, asset_dir, sequence)\n\n        data = {\n            \"representation\": str(representation[\"_id\"]),\n            \"parent\": str(representation[\"parent\"]),\n            \"loaded_assets\": loaded_assets\n        }\n        imprint(\n            \"{}/{}\".format(asset_dir, container.get('container_name')), data)\n\n        EditorLevelLibrary.save_current_level()\n\n        save_dir = f\"{root}/{hierarchy[0]}\" if create_sequences else asset_dir\n\n        asset_content = EditorAssetLibrary.list_assets(\n            save_dir, recursive=True, include_folder=False)\n\n        for a in asset_content:\n            EditorAssetLibrary.save_asset(a)\n\n        if master_level:\n            EditorLevelLibrary.load_level(master_level)\n        elif prev_level:\n            EditorLevelLibrary.load_level(prev_level)\n\n        if curr_level_sequence:\n            LevelSequenceLib.open_level_sequence(curr_level_sequence)\n            LevelSequenceLib.set_current_time(curr_time)\n            LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock)\n\n        editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot)\n\n    def remove(self, container):\n        \"\"\"\n        Delete the layout. First, check if the assets loaded with the layout\n        are used by other layouts. If not, delete the assets.\n        \"\"\"\n        data = get_current_project_settings()\n        create_sequences = data[\"unreal\"][\"level_sequences_for_layouts\"]\n\n        root = \"/Game/Ayon\"\n        path = Path(container.get(\"namespace\"))\n\n        containers = ls()\n        layout_containers = [\n            c for c in containers\n            if (c.get('asset_name') != container.get('asset_name') and\n                c.get('family') == \"layout\")]\n\n        # Check if the assets have been loaded by other layouts, and deletes\n        # them if they haven't.\n        for asset in eval(container.get('loaded_assets')):\n            layouts = [\n                lc for lc in layout_containers\n                if asset in lc.get('loaded_assets')]\n\n            if not layouts:\n                EditorAssetLibrary.delete_directory(str(Path(asset).parent))\n\n                # Delete the parent folder if there aren't any more\n                # layouts in it.\n                asset_content = EditorAssetLibrary.list_assets(\n                    str(Path(asset).parent.parent), recursive=False,\n                    include_folder=True\n                )\n\n                if len(asset_content) == 0:\n                    EditorAssetLibrary.delete_directory(\n                        str(Path(asset).parent.parent))\n\n        master_sequence = None\n        master_level = None\n        sequences = []\n\n        if create_sequences:\n            # Remove the Level Sequence from the parent.\n            # We need to traverse the hierarchy from the master sequence to\n            # find the level sequence.\n            namespace = container.get('namespace').replace(f\"{root}/\", \"\")\n            ms_asset = namespace.split('/')[0]\n            ar = unreal.AssetRegistryHelpers.get_asset_registry()\n            _filter = unreal.ARFilter(\n                class_names=[\"LevelSequence\"],\n                package_paths=[f\"{root}/{ms_asset}\"],\n                recursive_paths=False)\n            sequences = ar.get_assets(_filter)\n            master_sequence = sequences[0].get_asset()\n            _filter = unreal.ARFilter(\n                class_names=[\"World\"],\n                package_paths=[f\"{root}/{ms_asset}\"],\n                recursive_paths=False)\n            levels = ar.get_assets(_filter)\n            master_level = levels[0].get_asset().get_path_name()\n\n            sequences = [master_sequence]\n\n            parent = None\n            for s in sequences:\n                tracks = s.get_master_tracks()\n                subscene_track = None\n                visibility_track = None\n                for t in tracks:\n                    if t.get_class() == MovieSceneSubTrack.static_class():\n                        subscene_track = t\n                    if (t.get_class() ==\n                            MovieSceneLevelVisibilityTrack.static_class()):\n                        visibility_track = t\n                if subscene_track:\n                    sections = subscene_track.get_sections()\n                    for ss in sections:\n                        if (ss.get_sequence().get_name() ==\n                                container.get('asset')):\n                            parent = s\n                            subscene_track.remove_section(ss)\n                            break\n                        sequences.append(ss.get_sequence())\n                    # Update subscenes indexes.\n                    i = 0\n                    for ss in sections:\n                        ss.set_row_index(i)\n                        i += 1\n\n                if visibility_track:\n                    sections = visibility_track.get_sections()\n                    for ss in sections:\n                        if (unreal.Name(f\"{container.get('asset')}_map\")\n                                in ss.get_level_names()):\n                            visibility_track.remove_section(ss)\n                    # Update visibility sections indexes.\n                    i = -1\n                    prev_name = []\n                    for ss in sections:\n                        if prev_name != ss.get_level_names():\n                            i += 1\n                        ss.set_row_index(i)\n                        prev_name = ss.get_level_names()\n                if parent:\n                    break\n\n            assert parent, \"Could not find the parent sequence\"\n\n        # Create a temporary level to delete the layout level.\n        EditorLevelLibrary.save_all_dirty_levels()\n        EditorAssetLibrary.make_directory(f\"{root}/tmp\")\n        tmp_level = f\"{root}/tmp/temp_map\"\n        if not EditorAssetLibrary.does_asset_exist(f\"{tmp_level}.temp_map\"):\n            EditorLevelLibrary.new_level(tmp_level)\n        else:\n            EditorLevelLibrary.load_level(tmp_level)\n\n        # Delete the layout directory.\n        EditorAssetLibrary.delete_directory(str(path))\n\n        if create_sequences:\n            EditorLevelLibrary.load_level(master_level)\n            EditorAssetLibrary.delete_directory(f\"{root}/tmp\")\n\n        # Delete the parent folder if there aren't any more layouts in it.\n        asset_content = EditorAssetLibrary.list_assets(\n            str(path.parent), recursive=False, include_folder=True\n        )\n\n        if len(asset_content) == 0:\n            EditorAssetLibrary.delete_directory(str(path.parent))\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/load/load_layout_existing.py",
    "content": "import json\nfrom pathlib import Path\n\nimport unreal\nfrom unreal import EditorLevelLibrary\n\nfrom openpype.client import get_representations\nfrom openpype.pipeline import (\n    discover_loader_plugins,\n    loaders_from_representation,\n    load_container,\n    get_representation_path,\n    AYON_CONTAINER_ID,\n    get_current_project_name,\n)\nfrom openpype.hosts.unreal.api import plugin\nfrom openpype.hosts.unreal.api import pipeline as upipeline\n\n\nclass ExistingLayoutLoader(plugin.Loader):\n    \"\"\"\n    Load Layout for an existing scene, and match the existing assets.\n    \"\"\"\n\n    families = [\"layout\"]\n    representations = [\"json\"]\n\n    label = \"Load Layout on Existing Scene\"\n    icon = \"code-fork\"\n    color = \"orange\"\n    ASSET_ROOT = \"/Game/Ayon\"\n\n    delete_unmatched_assets = True\n\n    @classmethod\n    def apply_settings(cls, project_settings, *args, **kwargs):\n        super(ExistingLayoutLoader, cls).apply_settings(\n            project_settings, *args, **kwargs\n        )\n        cls.delete_unmatched_assets = (\n            project_settings[\"unreal\"][\"delete_unmatched_assets\"]\n        )\n\n    @staticmethod\n    def _create_container(\n        asset_name, asset_dir, asset, representation, parent, family\n    ):\n        container_name = f\"{asset_name}_CON\"\n\n        container = None\n        if not unreal.EditorAssetLibrary.does_asset_exist(\n            f\"{asset_dir}/{container_name}\"\n        ):\n            container = upipeline.create_container(container_name, asset_dir)\n        else:\n            ar = unreal.AssetRegistryHelpers.get_asset_registry()\n            obj = ar.get_asset_by_object_path(\n                f\"{asset_dir}/{container_name}.{container_name}\")\n            container = obj.get_asset()\n\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": asset_dir,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            # \"loader\": str(self.__class__.__name__),\n            \"representation\": representation,\n            \"parent\": parent,\n            \"family\": family\n        }\n\n        upipeline.imprint(\n            \"{}/{}\".format(asset_dir, container_name), data)\n\n        return container.get_path_name()\n\n    @staticmethod\n    def _get_current_level():\n        ue_version = unreal.SystemLibrary.get_engine_version().split('.')\n        ue_major = ue_version[0]\n\n        if ue_major == '4':\n            return EditorLevelLibrary.get_editor_world()\n        elif ue_major == '5':\n            return unreal.LevelEditorSubsystem().get_current_level()\n\n        raise NotImplementedError(\n            f\"Unreal version {ue_major} not supported\")\n\n    def _transform_from_basis(self, transform, basis):\n        \"\"\"Transform a transform from a basis to a new basis.\"\"\"\n        # Get the basis matrix\n        basis_matrix = unreal.Matrix(\n            basis[0],\n            basis[1],\n            basis[2],\n            basis[3]\n        )\n        transform_matrix = unreal.Matrix(\n            transform[0],\n            transform[1],\n            transform[2],\n            transform[3]\n        )\n\n        new_transform = (\n            basis_matrix.get_inverse() * transform_matrix * basis_matrix)\n\n        return new_transform.transform()\n\n    def _spawn_actor(self, obj, lasset):\n        actor = EditorLevelLibrary.spawn_actor_from_object(\n            obj, unreal.Vector(0.0, 0.0, 0.0)\n        )\n\n        actor.set_actor_label(lasset.get('instance_name'))\n\n        transform = lasset.get('transform_matrix')\n        basis = lasset.get('basis')\n\n        computed_transform = self._transform_from_basis(transform, basis)\n\n        actor.set_actor_transform(computed_transform, False, True)\n\n    @staticmethod\n    def _get_fbx_loader(loaders, family):\n        name = \"\"\n        if family == 'rig':\n            name = \"SkeletalMeshFBXLoader\"\n        elif family == 'model' or family == 'staticMesh':\n            name = \"StaticMeshFBXLoader\"\n        elif family == 'camera':\n            name = \"CameraLoader\"\n\n        if name == \"\":\n            return None\n\n        for loader in loaders:\n            if loader.__name__ == name:\n                return loader\n\n        return None\n\n    @staticmethod\n    def _get_abc_loader(loaders, family):\n        name = \"\"\n        if family == 'rig':\n            name = \"SkeletalMeshAlembicLoader\"\n        elif family == 'model':\n            name = \"StaticMeshAlembicLoader\"\n\n        if name == \"\":\n            return None\n\n        for loader in loaders:\n            if loader.__name__ == name:\n                return loader\n\n        return None\n\n    def _load_asset(self, repr_data, representation, instance_name, family):\n        repr_format = repr_data.get('name')\n\n        all_loaders = discover_loader_plugins()\n        loaders = loaders_from_representation(\n            all_loaders, representation)\n\n        loader = None\n\n        if repr_format == 'fbx':\n            loader = self._get_fbx_loader(loaders, family)\n        elif repr_format == 'abc':\n            loader = self._get_abc_loader(loaders, family)\n\n        if not loader:\n            self.log.error(f\"No valid loader found for {representation}\")\n            return []\n\n        # This option is necessary to avoid importing the assets with a\n        # different conversion compared to the other assets. For ABC files,\n        # it is in fact impossible to access the conversion settings. So,\n        # we must assume that the Maya conversion settings have been applied.\n        options = {\n            \"default_conversion\": True\n        }\n\n        assets = load_container(\n            loader,\n            representation,\n            namespace=instance_name,\n            options=options\n        )\n\n        return assets\n\n    def _get_valid_repre_docs(self, project_name, version_ids):\n        valid_formats = ['fbx', 'abc']\n\n        repre_docs = list(get_representations(\n            project_name,\n            representation_names=valid_formats,\n            version_ids=version_ids\n        ))\n        repre_doc_by_version_id = {}\n        for repre_doc in repre_docs:\n            version_id = str(repre_doc[\"parent\"])\n            repre_doc_by_version_id[version_id] = repre_doc\n        return repre_doc_by_version_id\n\n    def _process(self, lib_path, project_name):\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        actors = EditorLevelLibrary.get_all_level_actors()\n\n        with open(lib_path, \"r\") as fp:\n            data = json.load(fp)\n\n        elements = []\n        repre_ids = set()\n        # Get all the representations in the JSON from the database.\n        for element in data:\n            repre_id = element.get('representation')\n            if repre_id:\n                repre_ids.add(repre_id)\n                elements.append(element)\n\n        repre_docs = get_representations(\n            project_name, representation_ids=repre_ids\n        )\n        repre_docs_by_id = {\n            str(repre_doc[\"_id\"]): repre_doc\n            for repre_doc in repre_docs\n        }\n        layout_data = []\n        version_ids = set()\n        for element in elements:\n            repre_id = element.get(\"representation\")\n            repre_doc = repre_docs_by_id.get(repre_id)\n            if not repre_doc:\n                raise AssertionError(\"Representation not found\")\n            if not (repre_doc.get('data') or repre_doc['data'].get('path')):\n                raise AssertionError(\"Representation does not have path\")\n            if not repre_doc.get('context'):\n                raise AssertionError(\"Representation does not have context\")\n\n            layout_data.append((repre_doc, element))\n            version_ids.add(repre_doc[\"parent\"])\n\n        # Prequery valid repre documents for all elements at once\n        valid_repre_doc_by_version_id = self._get_valid_repre_docs(\n            project_name, version_ids)\n        containers = []\n        actors_matched = []\n\n        for (repr_data, lasset) in layout_data:\n            # For every actor in the scene, check if it has a representation in\n            # those we got from the JSON. If so, create a container for it.\n            # Otherwise, remove it from the scene.\n            found = False\n\n            for actor in actors:\n                if not actor.get_class().get_name() == 'StaticMeshActor':\n                    continue\n                if actor in actors_matched:\n                    continue\n\n                # Get the original path of the file from which the asset has\n                # been imported.\n                smc = actor.get_editor_property('static_mesh_component')\n                mesh = smc.get_editor_property('static_mesh')\n                import_data = mesh.get_editor_property('asset_import_data')\n                filename = import_data.get_first_filename()\n                path = Path(filename)\n\n                if (not path.name or\n                        path.name not in repr_data.get('data').get('path')):\n                    continue\n\n                actor.set_actor_label(lasset.get('instance_name'))\n\n                mesh_path = Path(mesh.get_path_name()).parent.as_posix()\n\n                # Create the container for the asset.\n                asset = repr_data.get('context').get('asset')\n                subset = repr_data.get('context').get('subset')\n                container = self._create_container(\n                    f\"{asset}_{subset}\", mesh_path, asset,\n                    repr_data.get('_id'), repr_data.get('parent'),\n                    repr_data.get('context').get('family')\n                )\n                containers.append(container)\n\n                # Set the transform for the actor.\n                transform = lasset.get('transform_matrix')\n                basis = lasset.get('basis')\n\n                computed_transform = self._transform_from_basis(\n                    transform, basis)\n                actor.set_actor_transform(computed_transform, False, True)\n\n                actors_matched.append(actor)\n                found = True\n                break\n\n            # If an actor has not been found for this representation,\n            # we check if it has been loaded already by checking all the\n            # loaded containers. If so, we add it to the scene. Otherwise,\n            # we load it.\n            if found:\n                continue\n\n            all_containers = upipeline.ls()\n\n            loaded = False\n\n            for container in all_containers:\n                repr = container.get('representation')\n\n                if not repr == str(repr_data.get('_id')):\n                    continue\n\n                asset_dir = container.get('namespace')\n\n                filter = unreal.ARFilter(\n                    class_names=[\"StaticMesh\"],\n                    package_paths=[asset_dir],\n                    recursive_paths=False)\n                assets = ar.get_assets(filter)\n\n                for asset in assets:\n                    obj = asset.get_asset()\n                    self._spawn_actor(obj, lasset)\n\n                loaded = True\n                break\n\n            # If the asset has not been loaded yet, we load it.\n            if loaded:\n                continue\n\n            assets = self._load_asset(\n                valid_repre_doc_by_version_id.get(lasset.get('version')),\n                lasset.get('representation'),\n                lasset.get('instance_name'),\n                lasset.get('family')\n            )\n\n            for asset in assets:\n                obj = ar.get_asset_by_object_path(asset).get_asset()\n                if not obj.get_class().get_name() == 'StaticMesh':\n                    continue\n                self._spawn_actor(obj, lasset)\n\n                break\n\n        # Check if an actor was not matched to a representation.\n        # If so, remove it from the scene.\n        for actor in actors:\n            if not actor.get_class().get_name() == 'StaticMeshActor':\n                continue\n            if actor not in actors_matched:\n                self.log.warning(f\"Actor {actor.get_name()} not matched.\")\n                if self.delete_unmatched_assets:\n                    EditorLevelLibrary.destroy_actor(actor)\n\n        return containers\n\n    def load(self, context, name, namespace, options):\n        print(\"Loading Layout and Match Assets\")\n\n        asset = context.get('asset').get('name')\n        asset_name = f\"{asset}_{name}\" if asset else name\n        container_name = f\"{asset}_{name}_CON\"\n\n        curr_level = self._get_current_level()\n\n        if not curr_level:\n            raise AssertionError(\"Current level not saved\")\n\n        project_name = context[\"project\"][\"name\"]\n        path = self.filepath_from_context(context)\n        containers = self._process(path, project_name)\n\n        curr_level_path = Path(\n            curr_level.get_outer().get_path_name()).parent.as_posix()\n\n        if not unreal.EditorAssetLibrary.does_asset_exist(\n            f\"{curr_level_path}/{container_name}\"\n        ):\n            upipeline.create_container(\n                container=container_name, path=curr_level_path)\n\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": curr_level_path,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": context[\"representation\"][\"_id\"],\n            \"parent\": context[\"representation\"][\"parent\"],\n            \"family\": context[\"representation\"][\"context\"][\"family\"],\n            \"loaded_assets\": containers\n        }\n        upipeline.imprint(f\"{curr_level_path}/{container_name}\", data)\n\n    def update(self, container, representation):\n        asset_dir = container.get('namespace')\n\n        source_path = get_representation_path(representation)\n        project_name = get_current_project_name()\n        containers = self._process(source_path, project_name)\n\n        data = {\n            \"representation\": str(representation[\"_id\"]),\n            \"parent\": str(representation[\"parent\"]),\n            \"loaded_assets\": containers\n        }\n        upipeline.imprint(\n            \"{}/{}\".format(asset_dir, container.get('container_name')), data)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Load Skeletal Mesh alembics.\"\"\"\nimport os\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AYON_CONTAINER_ID\n)\nfrom openpype.hosts.unreal.api import plugin\nfrom openpype.hosts.unreal.api.pipeline import (\n    AYON_ASSET_DIR,\n    create_container,\n    imprint,\n)\nimport unreal  # noqa\n\n\nclass SkeletalMeshAlembicLoader(plugin.Loader):\n    \"\"\"Load Unreal SkeletalMesh from Alembic\"\"\"\n\n    families = [\"pointcache\", \"skeletalMesh\"]\n    label = \"Import Alembic Skeletal Mesh\"\n    representations = [\"abc\"]\n    icon = \"cube\"\n    color = \"orange\"\n\n    root = AYON_ASSET_DIR\n\n    @staticmethod\n    def get_task(filename, asset_dir, asset_name, replace, default_conversion):\n        task = unreal.AssetImportTask()\n        options = unreal.AbcImportSettings()\n        conversion_settings = unreal.AbcConversionSettings(\n            preset=unreal.AbcConversionPreset.CUSTOM,\n            flip_u=False, flip_v=False,\n            rotation=[0.0, 0.0, 0.0],\n            scale=[1.0, 1.0, 1.0])\n\n        task.set_editor_property('filename', filename)\n        task.set_editor_property('destination_path', asset_dir)\n        task.set_editor_property('destination_name', asset_name)\n        task.set_editor_property('replace_existing', replace)\n        task.set_editor_property('automated', True)\n        task.set_editor_property('save', True)\n\n        options.set_editor_property(\n            'import_type', unreal.AlembicImportType.SKELETAL)\n\n        if not default_conversion:\n            conversion_settings = unreal.AbcConversionSettings(\n                preset=unreal.AbcConversionPreset.CUSTOM,\n                flip_u=False, flip_v=False,\n                rotation=[0.0, 0.0, 0.0],\n                scale=[1.0, 1.0, 1.0])\n            options.conversion_settings = conversion_settings\n\n        task.options = options\n\n        return task\n\n    def import_and_containerize(\n        self, filepath, asset_dir, asset_name, container_name,\n        default_conversion=False\n    ):\n        unreal.EditorAssetLibrary.make_directory(asset_dir)\n\n        task = self.get_task(\n            filepath, asset_dir, asset_name, False, default_conversion)\n\n        unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])\n\n        # Create Asset Container\n        create_container(container=container_name, path=asset_dir)\n\n    def imprint(\n        self, asset, asset_dir, container_name, asset_name, representation\n    ):\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": asset_dir,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": representation[\"_id\"],\n            \"parent\": representation[\"parent\"],\n            \"family\": representation[\"context\"][\"family\"]\n        }\n        imprint(f\"{asset_dir}/{container_name}\", data)\n\n    def load(self, context, name, namespace, options):\n        \"\"\"Load and containerise representation into Content Browser.\n\n        Args:\n            context (dict): application context\n            name (str): subset name\n            namespace (str): in Unreal this is basically path to container.\n                             This is not passed here, so namespace is set\n                             by `containerise()` because only then we know\n                             real path.\n            data (dict): Those would be data to be imprinted.\n\n        Returns:\n            list(str): list of container content\n        \"\"\"\n        # Create directory for asset and ayon container\n        asset = context.get('asset').get('name')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n        version = context.get('version')\n        # Check if version is hero version and use different name\n        if not version.get(\"name\") and version.get('type') == \"hero_version\":\n            name_version = f\"{name}_hero\"\n        else:\n            name_version = f\"{name}_v{version.get('name'):03d}\"\n\n        default_conversion = False\n        if options.get(\"default_conversion\"):\n            default_conversion = options.get(\"default_conversion\")\n\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{self.root}/{asset}/{name_version}\", suffix=\"\")\n\n        container_name += suffix\n\n        if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):\n            path = self.filepath_from_context(context)\n\n            self.import_and_containerize(path, asset_dir, asset_name,\n                                         container_name, default_conversion)\n\n        self.imprint(\n            asset, asset_dir, container_name, asset_name,\n            context[\"representation\"])\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=True\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n        return asset_content\n\n    def update(self, container, representation):\n        context = representation.get(\"context\", {})\n\n        if not context:\n            raise RuntimeError(\"No context found in representation\")\n\n        # Create directory for asset and Ayon container\n        asset = context.get('asset')\n        name = context.get('subset')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n        version = context.get('version')\n        # Check if version is hero version and use different name\n        name_version = f\"{name}_v{version:03d}\" if version else f\"{name}_hero\"\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{self.root}/{asset}/{name_version}\", suffix=\"\")\n\n        container_name += suffix\n\n        if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):\n            path = get_representation_path(representation)\n\n            self.import_and_containerize(path, asset_dir, asset_name,\n                                         container_name)\n\n        self.imprint(\n            asset, asset_dir, container_name, asset_name, representation)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=False\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n    def remove(self, container):\n        path = container[\"namespace\"]\n        parent_path = os.path.dirname(path)\n\n        unreal.EditorAssetLibrary.delete_directory(path)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            parent_path, recursive=False\n        )\n\n        if len(asset_content) == 0:\n            unreal.EditorAssetLibrary.delete_directory(parent_path)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Load Skeletal Meshes form FBX.\"\"\"\nimport os\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AYON_CONTAINER_ID\n)\nfrom openpype.hosts.unreal.api import plugin\nfrom openpype.hosts.unreal.api.pipeline import (\n    AYON_ASSET_DIR,\n    create_container,\n    imprint,\n)\nimport unreal  # noqa\n\n\nclass SkeletalMeshFBXLoader(plugin.Loader):\n    \"\"\"Load Unreal SkeletalMesh from FBX.\"\"\"\n\n    families = [\"rig\", \"skeletalMesh\"]\n    label = \"Import FBX Skeletal Mesh\"\n    representations = [\"fbx\"]\n    icon = \"cube\"\n    color = \"orange\"\n\n    root = AYON_ASSET_DIR\n\n    @staticmethod\n    def get_task(filename, asset_dir, asset_name, replace):\n        task = unreal.AssetImportTask()\n        options = unreal.FbxImportUI()\n\n        task.set_editor_property('filename', filename)\n        task.set_editor_property('destination_path', asset_dir)\n        task.set_editor_property('destination_name', asset_name)\n        task.set_editor_property('replace_existing', replace)\n        task.set_editor_property('automated', True)\n        task.set_editor_property('save', True)\n\n        options.set_editor_property(\n            'automated_import_should_detect_type', False)\n        options.set_editor_property('import_as_skeletal', True)\n        options.set_editor_property('import_animations', False)\n        options.set_editor_property('import_mesh', True)\n        options.set_editor_property('import_materials', False)\n        options.set_editor_property('import_textures', False)\n        options.set_editor_property('skeleton', None)\n        options.set_editor_property('create_physics_asset', False)\n\n        options.set_editor_property(\n            'mesh_type_to_import',\n            unreal.FBXImportType.FBXIT_SKELETAL_MESH)\n\n        options.skeletal_mesh_import_data.set_editor_property(\n            'import_content_type',\n            unreal.FBXImportContentType.FBXICT_ALL)\n\n        options.skeletal_mesh_import_data.set_editor_property(\n            'normal_import_method',\n            unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS)\n\n        task.options = options\n\n        return task\n\n    def import_and_containerize(\n        self, filepath, asset_dir, asset_name, container_name\n    ):\n        unreal.EditorAssetLibrary.make_directory(asset_dir)\n\n        task = self.get_task(\n            filepath, asset_dir, asset_name, False)\n\n        unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])\n\n        # Create Asset Container\n        create_container(container=container_name, path=asset_dir)\n\n    def imprint(\n        self, asset, asset_dir, container_name, asset_name, representation\n    ):\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": asset_dir,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": representation[\"_id\"],\n            \"parent\": representation[\"parent\"],\n            \"family\": representation[\"context\"][\"family\"]\n        }\n        imprint(f\"{asset_dir}/{container_name}\", data)\n\n    def load(self, context, name, namespace, options):\n        \"\"\"Load and containerise representation into Content Browser.\n\n        Args:\n            context (dict): application context\n            name (str): subset name\n            namespace (str): in Unreal this is basically path to container.\n                             This is not passed here, so namespace is set\n                             by `containerise()` because only then we know\n                             real path.\n            data (dict): Those would be data to be imprinted.\n\n        Returns:\n            list(str): list of container content\n        \"\"\"\n        # Create directory for asset and Ayon container\n        asset = context.get('asset').get('name')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n        version = context.get('version')\n        # Check if version is hero version and use different name\n        if not version.get(\"name\") and version.get('type') == \"hero_version\":\n            name_version = f\"{name}_hero\"\n        else:\n            name_version = f\"{name}_v{version.get('name'):03d}\"\n\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{self.root}/{asset}/{name_version}\", suffix=\"\"\n        )\n\n        container_name += suffix\n\n        if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):\n            path = self.filepath_from_context(context)\n\n            self.import_and_containerize(\n                path, asset_dir, asset_name, container_name)\n\n        self.imprint(\n            asset, asset_dir, container_name, asset_name,\n            context[\"representation\"])\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=True\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n        return asset_content\n\n    def update(self, container, representation):\n        context = representation.get(\"context\", {})\n\n        if not context:\n            raise RuntimeError(\"No context found in representation\")\n\n        # Create directory for asset and Ayon container\n        asset = context.get('asset')\n        name = context.get('subset')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n        version = context.get('version')\n        # Check if version is hero version and use different name\n        name_version = f\"{name}_v{version:03d}\" if version else f\"{name}_hero\"\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{self.root}/{asset}/{name_version}\", suffix=\"\")\n\n        container_name += suffix\n\n        if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):\n            path = get_representation_path(representation)\n\n            self.import_and_containerize(\n                path, asset_dir, asset_name, container_name)\n\n        self.imprint(\n            asset, asset_dir, container_name, asset_name, representation)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=False\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n    def remove(self, container):\n        path = container[\"namespace\"]\n        parent_path = os.path.dirname(path)\n\n        unreal.EditorAssetLibrary.delete_directory(path)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            parent_path, recursive=False\n        )\n\n        if len(asset_content) == 0:\n            unreal.EditorAssetLibrary.delete_directory(parent_path)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Loader for Static Mesh alembics.\"\"\"\nimport os\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AYON_CONTAINER_ID\n)\nfrom openpype.hosts.unreal.api import plugin\nfrom openpype.hosts.unreal.api.pipeline import (\n    AYON_ASSET_DIR,\n    create_container,\n    imprint,\n)\nimport unreal  # noqa\n\n\nclass StaticMeshAlembicLoader(plugin.Loader):\n    \"\"\"Load Unreal StaticMesh from Alembic\"\"\"\n\n    families = [\"model\", \"staticMesh\"]\n    label = \"Import Alembic Static Mesh\"\n    representations = [\"abc\"]\n    icon = \"cube\"\n    color = \"orange\"\n\n    root = AYON_ASSET_DIR\n\n    @staticmethod\n    def get_task(filename, asset_dir, asset_name, replace, default_conversion):\n        task = unreal.AssetImportTask()\n        options = unreal.AbcImportSettings()\n        sm_settings = unreal.AbcStaticMeshSettings()\n\n        task.set_editor_property('filename', filename)\n        task.set_editor_property('destination_path', asset_dir)\n        task.set_editor_property('destination_name', asset_name)\n        task.set_editor_property('replace_existing', replace)\n        task.set_editor_property('automated', True)\n        task.set_editor_property('save', True)\n\n        # set import options here\n        # Unreal 4.24 ignores the settings. It works with Unreal 4.26\n        options.set_editor_property(\n            'import_type', unreal.AlembicImportType.STATIC_MESH)\n\n        sm_settings.set_editor_property('merge_meshes', True)\n\n        if not default_conversion:\n            conversion_settings = unreal.AbcConversionSettings(\n                preset=unreal.AbcConversionPreset.CUSTOM,\n                flip_u=False, flip_v=False,\n                rotation=[0.0, 0.0, 0.0],\n                scale=[1.0, 1.0, 1.0])\n            options.conversion_settings = conversion_settings\n\n        options.static_mesh_settings = sm_settings\n        task.options = options\n\n        return task\n\n    def import_and_containerize(\n        self, filepath, asset_dir, asset_name, container_name,\n        default_conversion=False\n    ):\n        unreal.EditorAssetLibrary.make_directory(asset_dir)\n\n        task = self.get_task(\n            filepath, asset_dir, asset_name, False, default_conversion)\n\n        unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])\n\n        # Create Asset Container\n        create_container(container=container_name, path=asset_dir)\n\n    def imprint(\n        self, asset, asset_dir, container_name, asset_name, representation\n    ):\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": asset_dir,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": representation[\"_id\"],\n            \"parent\": representation[\"parent\"],\n            \"family\": representation[\"context\"][\"family\"]\n        }\n        imprint(f\"{asset_dir}/{container_name}\", data)\n\n    def load(self, context, name, namespace, options):\n        \"\"\"Load and containerise representation into Content Browser.\n\n        Args:\n            context (dict): application context\n            name (str): subset name\n            namespace (str): in Unreal this is basically path to container.\n                             This is not passed here, so namespace is set\n                             by `containerise()` because only then we know\n                             real path.\n            data (dict): Those would be data to be imprinted.\n\n        Returns:\n            list(str): list of container content\n        \"\"\"\n        # Create directory for asset and Ayon container\n        asset = context.get('asset').get('name')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n        version = context.get('version')\n        # Check if version is hero version and use different name\n        if not version.get(\"name\") and version.get('type') == \"hero_version\":\n            name_version = f\"{name}_hero\"\n        else:\n            name_version = f\"{name}_v{version.get('name'):03d}\"\n\n        default_conversion = False\n        if options.get(\"default_conversion\"):\n            default_conversion = options.get(\"default_conversion\")\n\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{self.root}/{asset}/{name_version}\", suffix=\"\")\n\n        container_name += suffix\n\n        if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):\n            path = self.filepath_from_context(context)\n\n            self.import_and_containerize(path, asset_dir, asset_name,\n                                         container_name, default_conversion)\n\n        self.imprint(\n            asset, asset_dir, container_name, asset_name,\n            context[\"representation\"])\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=False\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n        return asset_content\n\n    def update(self, container, representation):\n        context = representation.get(\"context\", {})\n\n        if not context:\n            raise RuntimeError(\"No context found in representation\")\n\n        # Create directory for asset and Ayon container\n        asset = context.get('asset')\n        name = context.get('subset')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n        version = context.get('version')\n        # Check if version is hero version and use different name\n        name_version = f\"{name}_v{version:03d}\" if version else f\"{name}_hero\"\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{self.root}/{asset}/{name_version}\", suffix=\"\")\n\n        container_name += suffix\n\n        if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):\n            path = get_representation_path(representation)\n\n            self.import_and_containerize(path, asset_dir, asset_name,\n                                         container_name)\n\n        self.imprint(\n            asset, asset_dir, container_name, asset_name, representation)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=False\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n    def remove(self, container):\n        path = container[\"namespace\"]\n        parent_path = os.path.dirname(path)\n\n        unreal.EditorAssetLibrary.delete_directory(path)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            parent_path, recursive=False\n        )\n\n        if len(asset_content) == 0:\n            unreal.EditorAssetLibrary.delete_directory(parent_path)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Load Static meshes form FBX.\"\"\"\nimport os\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AYON_CONTAINER_ID\n)\nfrom openpype.hosts.unreal.api import plugin\nfrom openpype.hosts.unreal.api.pipeline import (\n    AYON_ASSET_DIR,\n    create_container,\n    imprint,\n)\nimport unreal  # noqa\n\n\nclass StaticMeshFBXLoader(plugin.Loader):\n    \"\"\"Load Unreal StaticMesh from FBX.\"\"\"\n\n    families = [\"model\", \"staticMesh\"]\n    label = \"Import FBX Static Mesh\"\n    representations = [\"fbx\"]\n    icon = \"cube\"\n    color = \"orange\"\n\n    root = AYON_ASSET_DIR\n\n    @staticmethod\n    def get_task(filename, asset_dir, asset_name, replace):\n        task = unreal.AssetImportTask()\n        options = unreal.FbxImportUI()\n        import_data = unreal.FbxStaticMeshImportData()\n\n        task.set_editor_property('filename', filename)\n        task.set_editor_property('destination_path', asset_dir)\n        task.set_editor_property('destination_name', asset_name)\n        task.set_editor_property('replace_existing', replace)\n        task.set_editor_property('automated', True)\n        task.set_editor_property('save', True)\n\n        # set import options here\n        options.set_editor_property(\n            'automated_import_should_detect_type', False)\n        options.set_editor_property('import_animations', False)\n\n        import_data.set_editor_property('combine_meshes', True)\n        import_data.set_editor_property('remove_degenerates', False)\n\n        options.static_mesh_import_data = import_data\n        task.options = options\n\n        return task\n\n    def import_and_containerize(\n        self, filepath, asset_dir, asset_name, container_name\n    ):\n        unreal.EditorAssetLibrary.make_directory(asset_dir)\n\n        task = self.get_task(\n            filepath, asset_dir, asset_name, False)\n\n        unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])\n\n        # Create Asset Container\n        create_container(container=container_name, path=asset_dir)\n\n    def imprint(\n        self, asset, asset_dir, container_name, asset_name, representation\n    ):\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": asset_dir,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": representation[\"_id\"],\n            \"parent\": representation[\"parent\"],\n            \"family\": representation[\"context\"][\"family\"]\n        }\n        imprint(f\"{asset_dir}/{container_name}\", data)\n\n    def load(self, context, name, namespace, options):\n        \"\"\"Load and containerise representation into Content Browser.\n\n        Args:\n            context (dict): application context\n            name (str): subset name\n            namespace (str): in Unreal this is basically path to container.\n                             This is not passed here, so namespace is set\n                             by `containerise()` because only then we know\n                             real path.\n            options (dict): Those would be data to be imprinted.\n\n        Returns:\n            list(str): list of container content\n        \"\"\"\n        # Create directory for asset and Ayon container\n        asset = context.get('asset').get('name')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n        version = context.get('version')\n        # Check if version is hero version and use different name\n        if not version.get(\"name\") and version.get('type') == \"hero_version\":\n            name_version = f\"{name}_hero\"\n        else:\n            name_version = f\"{name}_v{version.get('name'):03d}\"\n\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{self.root}/{asset}/{name_version}\", suffix=\"\"\n        )\n\n        container_name += suffix\n\n        if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):\n            path = self.filepath_from_context(context)\n\n            self.import_and_containerize(\n                path, asset_dir, asset_name, container_name)\n\n        self.imprint(\n            asset, asset_dir, container_name, asset_name,\n            context[\"representation\"])\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=True\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n        return asset_content\n\n    def update(self, container, representation):\n        context = representation.get(\"context\", {})\n\n        if not context:\n            raise RuntimeError(\"No context found in representation\")\n\n        # Create directory for asset and Ayon container\n        asset = context.get('asset')\n        name = context.get('subset')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n        version = context.get('version')\n        # Check if version is hero version and use different name\n        name_version = f\"{name}_v{version:03d}\" if version else f\"{name}_hero\"\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{self.root}/{asset}/{name_version}\", suffix=\"\")\n\n        container_name += suffix\n\n        if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):\n            path = get_representation_path(representation)\n\n            self.import_and_containerize(\n                path, asset_dir, asset_name, container_name)\n\n        self.imprint(\n            asset, asset_dir, container_name, asset_name, representation)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=False\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n    def remove(self, container):\n        path = container[\"namespace\"]\n        parent_path = os.path.dirname(path)\n\n        unreal.EditorAssetLibrary.delete_directory(path)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            parent_path, recursive=False\n        )\n\n        if len(asset_content) == 0:\n            unreal.EditorAssetLibrary.delete_directory(parent_path)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/load/load_uasset.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Load UAsset.\"\"\"\nfrom pathlib import Path\nimport shutil\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AYON_CONTAINER_ID\n)\nfrom openpype.hosts.unreal.api import plugin\nfrom openpype.hosts.unreal.api import pipeline as unreal_pipeline\nimport unreal  # noqa\n\n\nclass UAssetLoader(plugin.Loader):\n    \"\"\"Load UAsset.\"\"\"\n\n    families = [\"uasset\"]\n    label = \"Load UAsset\"\n    representations = [\"uasset\"]\n    icon = \"cube\"\n    color = \"orange\"\n\n    extension = \"uasset\"\n\n    def load(self, context, name, namespace, options):\n        \"\"\"Load and containerise representation into Content Browser.\n\n        Args:\n            context (dict): application context\n            name (str): subset name\n            namespace (str): in Unreal this is basically path to container.\n                             This is not passed here, so namespace is set\n                             by `containerise()` because only then we know\n                             real path.\n            options (dict): Those would be data to be imprinted. This is not\n                used now, data are imprinted by `containerise()`.\n\n        Returns:\n            list(str): list of container content\n        \"\"\"\n\n        # Create directory for asset and Ayon container\n        root = unreal_pipeline.AYON_ASSET_DIR\n        asset = context.get('asset').get('name')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{root}/{asset}/{name}\", suffix=\"\"\n        )\n\n        unique_number = 1\n        while unreal.EditorAssetLibrary.does_directory_exist(\n            f\"{asset_dir}_{unique_number:02}\"\n        ):\n            unique_number += 1\n\n        asset_dir = f\"{asset_dir}_{unique_number:02}\"\n        container_name = f\"{container_name}_{unique_number:02}{suffix}\"\n\n        unreal.EditorAssetLibrary.make_directory(asset_dir)\n\n        destination_path = asset_dir.replace(\n            \"/Game\", Path(unreal.Paths.project_content_dir()).as_posix(), 1)\n\n        path = self.filepath_from_context(context)\n        shutil.copy(\n            path,\n            f\"{destination_path}/{name}_{unique_number:02}.{self.extension}\")\n\n        # Create Asset Container\n        unreal_pipeline.create_container(\n            container=container_name, path=asset_dir)\n\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": asset_dir,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": context[\"representation\"][\"_id\"],\n            \"parent\": context[\"representation\"][\"parent\"],\n            \"family\": context[\"representation\"][\"context\"][\"family\"],\n        }\n        unreal_pipeline.imprint(f\"{asset_dir}/{container_name}\", data)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=True\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n        return asset_content\n\n    def update(self, container, representation):\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        asset_dir = container[\"namespace\"]\n        name = representation[\"context\"][\"subset\"]\n\n        unique_number = container[\"container_name\"].split(\"_\")[-2]\n\n        destination_path = asset_dir.replace(\n            \"/Game\", Path(unreal.Paths.project_content_dir()).as_posix(), 1)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=False, include_folder=True\n        )\n\n        for asset in asset_content:\n            obj = ar.get_asset_by_object_path(asset).get_asset()\n            if obj.get_class().get_name() != \"AyonAssetContainer\":\n                unreal.EditorAssetLibrary.delete_asset(asset)\n\n        update_filepath = get_representation_path(representation)\n\n        shutil.copy(\n            update_filepath,\n            f\"{destination_path}/{name}_{unique_number}.{self.extension}\")\n\n        container_path = f'{container[\"namespace\"]}/{container[\"objectName\"]}'\n        # update metadata\n        unreal_pipeline.imprint(\n            container_path,\n            {\n                \"representation\": str(representation[\"_id\"]),\n                \"parent\": str(representation[\"parent\"]),\n            }\n        )\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=True\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n    def remove(self, container):\n        path = container[\"namespace\"]\n        parent_path = Path(path).parent.as_posix()\n\n        unreal.EditorAssetLibrary.delete_directory(path)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            parent_path, recursive=False\n        )\n\n        if len(asset_content) == 0:\n            unreal.EditorAssetLibrary.delete_directory(parent_path)\n\n\nclass UMapLoader(UAssetLoader):\n    \"\"\"Load Level.\"\"\"\n\n    families = [\"uasset\"]\n    label = \"Load Level\"\n    representations = [\"umap\"]\n\n    extension = \"umap\"\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/load/load_yeticache.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Loader for Yeti Cache.\"\"\"\nimport os\nimport json\n\nfrom openpype.pipeline import (\n    get_representation_path,\n    AYON_CONTAINER_ID\n)\nfrom openpype.hosts.unreal.api import plugin\nfrom openpype.hosts.unreal.api import pipeline as unreal_pipeline\nimport unreal  # noqa\n\n\nclass YetiLoader(plugin.Loader):\n    \"\"\"Load Yeti Cache\"\"\"\n\n    families = [\"yeticacheUE\"]\n    label = \"Import Yeti\"\n    representations = [\"abc\"]\n    icon = \"pagelines\"\n    color = \"orange\"\n\n    @staticmethod\n    def get_task(filename, asset_dir, asset_name, replace):\n        task = unreal.AssetImportTask()\n        options = unreal.AbcImportSettings()\n\n        task.set_editor_property('filename', filename)\n        task.set_editor_property('destination_path', asset_dir)\n        task.set_editor_property('destination_name', asset_name)\n        task.set_editor_property('replace_existing', replace)\n        task.set_editor_property('automated', True)\n        task.set_editor_property('save', True)\n\n        task.options = options\n\n        return task\n\n    @staticmethod\n    def is_groom_module_active():\n        \"\"\"\n        Check if Groom plugin is active.\n\n        This is a workaround, because the Unreal python API don't have\n        any method to check if plugin is active.\n        \"\"\"\n        prj_file = unreal.Paths.get_project_file_path()\n\n        with open(prj_file, \"r\") as fp:\n            data = json.load(fp)\n\n        plugins = data.get(\"Plugins\")\n\n        if not plugins:\n            return False\n\n        plugin_names = [p.get(\"Name\") for p in plugins]\n\n        return \"HairStrands\" in plugin_names\n\n    def load(self, context, name, namespace, options):\n        \"\"\"Load and containerise representation into Content Browser.\n\n        This is two step process. First, import FBX to temporary path and\n        then call `containerise()` on it - this moves all content to new\n        directory and then it will create AssetContainer there and imprint it\n        with metadata. This will mark this path as container.\n\n        Args:\n            context (dict): application context\n            name (str): subset name\n            namespace (str): in Unreal this is basically path to container.\n                             This is not passed here, so namespace is set\n                             by `containerise()` because only then we know\n                             real path.\n            data (dict): Those would be data to be imprinted. This is not used\n                         now, data are imprinted by `containerise()`.\n\n        Returns:\n            list(str): list of container content\n\n        \"\"\"\n        # Check if Groom plugin is active\n        if not self.is_groom_module_active():\n            raise RuntimeError(\"Groom plugin is not activated.\")\n\n        # Create directory for asset and Ayon container\n        root = unreal_pipeline.AYON_ASSET_DIR\n        asset = context.get('asset').get('name')\n        suffix = \"_CON\"\n        asset_name = f\"{asset}_{name}\" if asset else f\"{name}\"\n\n        tools = unreal.AssetToolsHelpers().get_asset_tools()\n        asset_dir, container_name = tools.create_unique_asset_name(\n            f\"{root}/{asset}/{name}\", suffix=\"\")\n\n        unique_number = 1\n        while unreal.EditorAssetLibrary.does_directory_exist(\n            f\"{asset_dir}_{unique_number:02}\"\n        ):\n            unique_number += 1\n\n        asset_dir = f\"{asset_dir}_{unique_number:02}\"\n        container_name = f\"{container_name}_{unique_number:02}{suffix}\"\n\n        if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):\n            unreal.EditorAssetLibrary.make_directory(asset_dir)\n\n            path = self.filepath_from_context(context)\n            task = self.get_task(path, asset_dir, asset_name, False)\n\n            unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])  # noqa: E501\n\n            # Create Asset Container\n            unreal_pipeline.create_container(\n                container=container_name, path=asset_dir)\n\n        data = {\n            \"schema\": \"ayon:container-2.0\",\n            \"id\": AYON_CONTAINER_ID,\n            \"asset\": asset,\n            \"namespace\": asset_dir,\n            \"container_name\": container_name,\n            \"asset_name\": asset_name,\n            \"loader\": str(self.__class__.__name__),\n            \"representation\": context[\"representation\"][\"_id\"],\n            \"parent\": context[\"representation\"][\"parent\"],\n            \"family\": context[\"representation\"][\"context\"][\"family\"]\n        }\n        unreal_pipeline.imprint(f\"{asset_dir}/{container_name}\", data)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            asset_dir, recursive=True, include_folder=True\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n        return asset_content\n\n    def update(self, container, representation):\n        name = container[\"asset_name\"]\n        source_path = get_representation_path(representation)\n        destination_path = container[\"namespace\"]\n\n        task = self.get_task(source_path, destination_path, name, True)\n\n        # do import fbx and replace existing data\n        unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])\n\n        container_path = f'{container[\"namespace\"]}/{container[\"objectName\"]}'\n        # update metadata\n        unreal_pipeline.imprint(\n            container_path,\n            {\n                \"representation\": str(representation[\"_id\"]),\n                \"parent\": str(representation[\"parent\"])\n            })\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            destination_path, recursive=True, include_folder=True\n        )\n\n        for a in asset_content:\n            unreal.EditorAssetLibrary.save_asset(a)\n\n    def remove(self, container):\n        path = container[\"namespace\"]\n        parent_path = os.path.dirname(path)\n\n        unreal.EditorAssetLibrary.delete_directory(path)\n\n        asset_content = unreal.EditorAssetLibrary.list_assets(\n            parent_path, recursive=False\n        )\n\n        if len(asset_content) == 0:\n            unreal.EditorAssetLibrary.delete_directory(parent_path)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/publish/collect_current_file.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect current project path.\"\"\"\nimport unreal  # noqa\nimport pyblish.api\n\n\nclass CollectUnrealCurrentFile(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current working file into context.\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.5\n    label = \"Unreal Current File\"\n    hosts = ['unreal']\n\n    def process(self, context):\n        \"\"\"Inject the current working file.\"\"\"\n        current_file = unreal.Paths.get_project_file_path()\n        context.data['currentFile'] = current_file\n\n        assert current_file != '', \"Current file is empty. \" \\\n            \"Save the file before continuing.\"\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/publish/collect_instance_members.py",
    "content": "import unreal\n\nimport pyblish.api\n\n\nclass CollectInstanceMembers(pyblish.api.InstancePlugin):\n    \"\"\"\n    Collect members of instance.\n\n    This collector will collect the assets for the families that support to\n    have them included as External Data, and will add them to the instance\n    as members.\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.1\n    hosts = [\"unreal\"]\n    families = [\"camera\", \"look\", \"unrealStaticMesh\", \"uasset\"]\n    label = \"Collect Instance Members\"\n\n    def process(self, instance):\n        \"\"\"Collect members of instance.\"\"\"\n        self.log.info(\"Collecting instance members\")\n\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        inst_path = instance.data.get('instance_path')\n        inst_name = inst_path.split('/')[-1]\n\n        pub_instance = ar.get_asset_by_object_path(\n            f\"{inst_path}.{inst_name}\").get_asset()\n\n        if not pub_instance:\n            self.log.error(f\"{inst_path}.{inst_name}\")\n            raise RuntimeError(f\"Instance {instance} not found.\")\n\n        if not pub_instance.get_editor_property(\"add_external_assets\"):\n            # No external assets in the instance\n            return\n\n        assets = pub_instance.get_editor_property('asset_data_external')\n\n        members = [asset.get_path_name() for asset in assets]\n\n        self.log.debug(f\"Members: {members}\")\n\n        instance.data[\"members\"] = members\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/publish/collect_remove_marked.py",
    "content": "import pyblish.api\n\n\nclass CollectRemoveMarked(pyblish.api.ContextPlugin):\n    \"\"\"Remove marked data\n\n    Remove instances that have 'remove' in their instance.data\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.499\n    label = 'Remove Marked Instances'\n\n    def process(self, context):\n\n        self.log.debug(context)\n        # make ftrack publishable\n        instances_to_remove = []\n        for instance in context:\n            if instance.data.get('remove'):\n                instances_to_remove.append(instance)\n\n        for instance in instances_to_remove:\n            context.remove(instance)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/publish/collect_render_instances.py",
    "content": "import os\nfrom pathlib import Path\n\nimport unreal\n\nfrom openpype.pipeline import get_current_project_name\nfrom openpype.pipeline import Anatomy\nfrom openpype.hosts.unreal.api import pipeline\nimport pyblish.api\n\n\nclass CollectRenderInstances(pyblish.api.InstancePlugin):\n    \"\"\" This collector will try to find all the rendered frames.\n\n    \"\"\"\n    order = pyblish.api.CollectorOrder\n    hosts = [\"unreal\"]\n    families = [\"render\"]\n    label = \"Collect Render Instances\"\n\n    def process(self, instance):\n        self.log.debug(\"Preparing Rendering Instances\")\n\n        context = instance.context\n\n        data = instance.data\n        data['remove'] = True\n\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        sequence = ar.get_asset_by_object_path(\n            data.get('sequence')).get_asset()\n\n        sequences = [{\n            \"sequence\": sequence,\n            \"output\": data.get('output'),\n            \"frame_range\": (\n                data.get('frameStart'), data.get('frameEnd'))\n        }]\n\n        for s in sequences:\n            self.log.debug(f\"Processing: {s.get('sequence').get_name()}\")\n            subscenes = pipeline.get_subsequences(s.get('sequence'))\n\n            if subscenes:\n                for ss in subscenes:\n                    sequences.append({\n                        \"sequence\": ss.get_sequence(),\n                        \"output\": (f\"{s.get('output')}/\"\n                                   f\"{ss.get_sequence().get_name()}\"),\n                        \"frame_range\": (\n                            ss.get_start_frame(), ss.get_end_frame() - 1)\n                    })\n            else:\n                # Avoid creating instances for camera sequences\n                if \"_camera\" not in s.get('sequence').get_name():\n                    seq = s.get('sequence')\n                    seq_name = seq.get_name()\n\n                    new_instance = context.create_instance(\n                        f\"{data.get('subset')}_\"\n                        f\"{seq_name}\")\n                    new_instance[:] = seq_name\n\n                    new_data = new_instance.data\n\n                    new_data[\"asset\"] = f\"/{s.get('output')}\"\n                    new_data[\"setMembers\"] = seq_name\n                    new_data[\"family\"] = \"render\"\n                    new_data[\"families\"] = [\"render\", \"review\"]\n                    new_data[\"parent\"] = data.get(\"parent\")\n                    new_data[\"subset\"] = f\"{data.get('subset')}_{seq_name}\"\n                    new_data[\"level\"] = data.get(\"level\")\n                    new_data[\"output\"] = s.get('output')\n                    new_data[\"fps\"] = seq.get_display_rate().numerator\n                    new_data[\"frameStart\"] = int(s.get('frame_range')[0])\n                    new_data[\"frameEnd\"] = int(s.get('frame_range')[1])\n                    new_data[\"sequence\"] = seq.get_path_name()\n                    new_data[\"master_sequence\"] = data[\"master_sequence\"]\n                    new_data[\"master_level\"] = data[\"master_level\"]\n\n                    self.log.debug(f\"new instance data: {new_data}\")\n\n                    try:\n                        project = get_current_project_name()\n                        anatomy = Anatomy(project)\n                        root = anatomy.roots['renders']\n                    except Exception as e:\n                        raise Exception((\n                            \"Could not find render root \"\n                            \"in anatomy settings.\")) from e\n\n                    render_dir = f\"{root}/{project}/{s.get('output')}\"\n                    render_path = Path(render_dir)\n\n                    frames = []\n\n                    for x in render_path.iterdir():\n                        if x.is_file() and x.suffix == '.png':\n                            frames.append(str(x.name))\n\n                    if \"representations\" not in new_instance.data:\n                        new_instance.data[\"representations\"] = []\n\n                    repr = {\n                        'frameStart': instance.data[\"frameStart\"],\n                        'frameEnd': instance.data[\"frameEnd\"],\n                        'name': 'png',\n                        'ext': 'png',\n                        'files': frames,\n                        'stagingDir': render_dir,\n                        'tags': ['review']\n                    }\n                    new_instance.data[\"representations\"].append(repr)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/publish/extract_camera.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Extract camera from Unreal.\"\"\"\nimport os\n\nimport unreal\n\nfrom openpype.pipeline import publish\nfrom openpype.hosts.unreal.api.pipeline import UNREAL_VERSION\n\n\nclass ExtractCamera(publish.Extractor):\n    \"\"\"Extract a camera.\"\"\"\n\n    label = \"Extract Camera\"\n    hosts = [\"unreal\"]\n    families = [\"camera\"]\n    optional = True\n\n    def process(self, instance):\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        # Define extract output file path\n        staging_dir = self.staging_dir(instance)\n        fbx_filename = \"{}.fbx\".format(instance.name)\n\n        # Perform extraction\n        self.log.info(\"Performing extraction..\")\n\n        # Check if the loaded level is the same of the instance\n        if UNREAL_VERSION.major == 5:\n            world = unreal.UnrealEditorSubsystem().get_editor_world()\n        else:\n            world = unreal.EditorLevelLibrary.get_editor_world()\n        current_level = world.get_path_name()\n        assert current_level == instance.data.get(\"level\"), \\\n            \"Wrong level loaded\"\n\n        for member in instance.data.get('members'):\n            data = ar.get_asset_by_object_path(member)\n            if UNREAL_VERSION.major == 5:\n                is_level_sequence = (\n                    data.asset_class_path.asset_name == \"LevelSequence\")\n            else:\n                is_level_sequence = (data.asset_class == \"LevelSequence\")\n\n            if is_level_sequence:\n                sequence = data.get_asset()\n                if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor >= 1:\n                    params = unreal.SequencerExportFBXParams(\n                        world=world,\n                        root_sequence=sequence,\n                        sequence=sequence,\n                        bindings=sequence.get_bindings(),\n                        master_tracks=sequence.get_master_tracks(),\n                        fbx_file_name=os.path.join(staging_dir, fbx_filename)\n                    )\n                    unreal.SequencerTools.export_level_sequence_fbx(params)\n                elif UNREAL_VERSION.major == 4 and UNREAL_VERSION.minor == 26:\n                    unreal.SequencerTools.export_fbx(\n                        world,\n                        sequence,\n                        sequence.get_bindings(),\n                        unreal.FbxExportOption(),\n                        os.path.join(staging_dir, fbx_filename)\n                    )\n                else:\n                    # Unreal 5.0 or 4.27\n                    unreal.SequencerTools.export_level_sequence_fbx(\n                        world,\n                        sequence,\n                        sequence.get_bindings(),\n                        unreal.FbxExportOption(),\n                        os.path.join(staging_dir, fbx_filename)\n                    )\n\n                if not os.path.isfile(os.path.join(staging_dir, fbx_filename)):\n                    raise RuntimeError(\"Failed to extract camera\")\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        fbx_representation = {\n            'name': 'fbx',\n            'ext': 'fbx',\n            'files': fbx_filename,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(fbx_representation)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/publish/extract_layout.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nimport json\nimport math\n\nimport unreal\nfrom unreal import EditorLevelLibrary as ell\nfrom unreal import EditorAssetLibrary as eal\n\nfrom openpype.client import get_representation_by_name\nfrom openpype.pipeline import publish\n\n\nclass ExtractLayout(publish.Extractor):\n    \"\"\"Extract a layout.\"\"\"\n\n    label = \"Extract Layout\"\n    hosts = [\"unreal\"]\n    families = [\"layout\"]\n    optional = True\n\n    def process(self, instance):\n        # Define extract output file path\n        staging_dir = self.staging_dir(instance)\n\n        # Perform extraction\n        self.log.info(\"Performing extraction..\")\n\n        # Check if the loaded level is the same of the instance\n        current_level = ell.get_editor_world().get_path_name()\n        assert current_level == instance.data.get(\"level\"), \\\n            \"Wrong level loaded\"\n\n        json_data = []\n        project_name = instance.context.data[\"projectName\"]\n\n        for member in instance[:]:\n            actor = ell.get_actor_reference(member)\n            mesh = None\n\n            # Check type the type of mesh\n            if actor.get_class().get_name() == 'SkeletalMeshActor':\n                mesh = actor.skeletal_mesh_component.skeletal_mesh\n            elif actor.get_class().get_name() == 'StaticMeshActor':\n                mesh = actor.static_mesh_component.static_mesh\n\n            if mesh:\n                # Search the reference to the Asset Container for the object\n                path = unreal.Paths.get_path(mesh.get_path_name())\n                filter = unreal.ARFilter(\n                    class_names=[\"AyonAssetContainer\"], package_paths=[path])\n                ar = unreal.AssetRegistryHelpers.get_asset_registry()\n                try:\n                    asset_container = ar.get_assets(filter)[0].get_asset()\n                except IndexError:\n                    self.log.error(\"AssetContainer not found.\")\n                    return\n\n                parent_id = eal.get_metadata_tag(asset_container, \"parent\")\n                family = eal.get_metadata_tag(asset_container, \"family\")\n\n                self.log.info(\"Parent: {}\".format(parent_id))\n                blend = get_representation_by_name(\n                    project_name, \"blend\", parent_id, fields=[\"_id\"]\n                )\n                blend_id = blend[\"_id\"]\n\n                json_element = {}\n                json_element[\"reference\"] = str(blend_id)\n                json_element[\"family\"] = family\n                json_element[\"instance_name\"] = actor.get_name()\n                json_element[\"asset_name\"] = mesh.get_name()\n                import_data = mesh.get_editor_property(\"asset_import_data\")\n                json_element[\"file_path\"] = import_data.get_first_filename()\n                transform = actor.get_actor_transform()\n\n                json_element[\"transform\"] = {\n                    \"translation\": {\n                        \"x\": -transform.translation.x,\n                        \"y\": transform.translation.y,\n                        \"z\": transform.translation.z\n                    },\n                    \"rotation\": {\n                        \"x\": math.radians(transform.rotation.euler().x),\n                        \"y\": math.radians(transform.rotation.euler().y),\n                        \"z\": math.radians(180.0 - transform.rotation.euler().z)\n                    },\n                    \"scale\": {\n                        \"x\": transform.scale3d.x,\n                        \"y\": transform.scale3d.y,\n                        \"z\": transform.scale3d.z\n                    }\n                }\n                json_data.append(json_element)\n\n        json_filename = \"{}.json\".format(instance.name)\n        json_path = os.path.join(staging_dir, json_filename)\n\n        with open(json_path, \"w+\") as file:\n            json.dump(json_data, fp=file, indent=2)\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        json_representation = {\n            'name': 'json',\n            'ext': 'json',\n            'files': json_filename,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(json_representation)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/publish/extract_look.py",
    "content": "# -*- coding: utf-8 -*-\nimport json\nimport os\n\nimport unreal\nfrom unreal import MaterialEditingLibrary as mat_lib\n\nfrom openpype.pipeline import publish\n\n\nclass ExtractLook(publish.Extractor):\n    \"\"\"Extract look.\"\"\"\n\n    label = \"Extract Look\"\n    hosts = [\"unreal\"]\n    families = [\"look\"]\n    optional = True\n\n    def process(self, instance):\n        # Define extract output file path\n        staging_dir = self.staging_dir(instance)\n        resources_dir = instance.data[\"resourcesDir\"]\n\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        transfers = []\n\n        json_data = []\n\n        for member in instance:\n            asset = ar.get_asset_by_object_path(member)\n            obj = asset.get_asset()\n\n            name = asset.get_editor_property('asset_name')\n\n            json_element = {'material': str(name)}\n\n            material_obj = obj.get_editor_property('static_materials')[0]\n            material = material_obj.material_interface\n\n            base_color = mat_lib.get_material_property_input_node(\n                material, unreal.MaterialProperty.MP_BASE_COLOR)\n\n            base_color_name = base_color.get_editor_property('parameter_name')\n\n            texture = mat_lib.get_material_default_texture_parameter_value(\n                material, base_color_name)\n\n            if texture:\n                # Export Texture\n                tga_filename = f\"{instance.name}_{name}_texture.tga\"\n\n                tga_exporter = unreal.TextureExporterTGA()\n\n                tga_export_task = unreal.AssetExportTask()\n\n                tga_export_task.set_editor_property('exporter', tga_exporter)\n                tga_export_task.set_editor_property('automated', True)\n                tga_export_task.set_editor_property('object', texture)\n                tga_export_task.set_editor_property(\n                    'filename', f\"{staging_dir}/{tga_filename}\")\n                tga_export_task.set_editor_property('prompt', False)\n                tga_export_task.set_editor_property('selected', False)\n\n                unreal.Exporter.run_asset_export_task(tga_export_task)\n\n                json_element['tga_filename'] = tga_filename\n\n                transfers.append((\n                    f\"{staging_dir}/{tga_filename}\",\n                    f\"{resources_dir}/{tga_filename}\"))\n\n            fbx_filename = f\"{instance.name}_{name}.fbx\"\n\n            fbx_exporter = unreal.StaticMeshExporterFBX()\n            fbx_exporter.set_editor_property('text', False)\n\n            options = unreal.FbxExportOption()\n            options.set_editor_property('ascii', False)\n            options.set_editor_property('collision', False)\n\n            task = unreal.AssetExportTask()\n            task.set_editor_property('exporter', fbx_exporter)\n            task.set_editor_property('options', options)\n            task.set_editor_property('automated', True)\n            task.set_editor_property('object', object)\n            task.set_editor_property(\n                'filename', f\"{staging_dir}/{fbx_filename}\")\n            task.set_editor_property('prompt', False)\n            task.set_editor_property('selected', False)\n\n            unreal.Exporter.run_asset_export_task(task)\n\n            json_element['fbx_filename'] = fbx_filename\n\n            transfers.append((\n                f\"{staging_dir}/{fbx_filename}\",\n                f\"{resources_dir}/{fbx_filename}\"))\n\n            json_data.append(json_element)\n\n        json_filename = f\"{instance.name}.json\"\n        json_path = os.path.join(staging_dir, json_filename)\n\n        with open(json_path, \"w+\") as file:\n            json.dump(json_data, fp=file, indent=2)\n\n        if \"transfers\" not in instance.data:\n            instance.data[\"transfers\"] = []\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        json_representation = {\n            'name': 'json',\n            'ext': 'json',\n            'files': json_filename,\n            \"stagingDir\": staging_dir,\n        }\n\n        instance.data[\"representations\"].append(json_representation)\n        instance.data[\"transfers\"].extend(transfers)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/publish/extract_uasset.py",
    "content": "from pathlib import Path\nimport shutil\n\nimport unreal\n\nfrom openpype.pipeline import publish\n\n\nclass ExtractUAsset(publish.Extractor):\n    \"\"\"Extract a UAsset.\"\"\"\n\n    label = \"Extract UAsset\"\n    hosts = [\"unreal\"]\n    families = [\"uasset\", \"umap\"]\n    optional = True\n\n    def process(self, instance):\n        extension = (\n            \"umap\" if \"umap\" in instance.data.get(\"families\") else \"uasset\")\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n\n        self.log.debug(\"Performing extraction..\")\n        staging_dir = self.staging_dir(instance)\n\n        members = instance.data.get(\"members\", [])\n\n        if not members:\n            raise RuntimeError(\"No members found in instance.\")\n\n        # UAsset publishing supports only one member\n        obj = members[0]\n\n        asset = ar.get_asset_by_object_path(obj).get_asset()\n        sys_path = unreal.SystemLibrary.get_system_path(asset)\n        filename = Path(sys_path).name\n\n        shutil.copy(sys_path, staging_dir)\n\n        self.log.info(f\"instance.data: {instance.data}\")\n\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        representation = {\n            \"name\": extension,\n            \"ext\": extension,\n            \"files\": filename,\n            \"stagingDir\": staging_dir,\n        }\n        instance.data[\"representations\"].append(representation)\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py",
    "content": "import unreal\n\nimport pyblish.api\n\n\nclass ValidateNoDependencies(pyblish.api.InstancePlugin):\n    \"\"\"Ensure that the uasset has no dependencies\n\n    The uasset is checked for dependencies. If there are any, the instance\n    cannot be published.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Check no dependencies\"\n    families = [\"uasset\"]\n    hosts = [\"unreal\"]\n    optional = True\n\n    def process(self, instance):\n        ar = unreal.AssetRegistryHelpers.get_asset_registry()\n        all_dependencies = []\n\n        for obj in instance[:]:\n            asset = ar.get_asset_by_object_path(obj)\n            dependencies = ar.get_dependencies(\n                asset.package_name,\n                unreal.AssetRegistryDependencyOptions(\n                    include_soft_package_references=False,\n                    include_hard_package_references=True,\n                    include_searchable_names=False,\n                    include_soft_management_references=False,\n                    include_hard_management_references=False\n                ))\n            if dependencies:\n                for dep in dependencies:\n                    if str(dep).startswith(\"/Game/\"):\n                        all_dependencies.append(str(dep))\n\n        if all_dependencies:\n            raise RuntimeError(\n                f\"Dependencies found: {all_dependencies}\")\n"
  },
  {
    "path": "openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py",
    "content": "import clique\nimport os\nimport re\n\nimport pyblish.api\nfrom openpype.pipeline.publish import PublishValidationError\n\n\nclass ValidateSequenceFrames(pyblish.api.InstancePlugin):\n    \"\"\"Ensure the sequence of frames is complete\n\n    The files found in the folder are checked against the frameStart and\n    frameEnd of the instance. If the first or last file is not\n    corresponding with the first or last frame it is flagged as invalid.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Sequence Frames\"\n    families = [\"render\"]\n    hosts = [\"unreal\"]\n    optional = True\n\n    def process(self, instance):\n        representations = instance.data.get(\"representations\")\n        for repr in representations:\n            data = instance.data.get(\"assetEntity\", {}).get(\"data\", {})\n            repr_files = repr[\"files\"]\n            if isinstance(repr_files, str):\n                continue\n\n            ext = repr.get(\"ext\")\n            if not ext:\n                _, ext = os.path.splitext(repr_files[0])\n            elif not ext.startswith(\".\"):\n                ext = \".{}\".format(ext)\n            pattern = r\"\\D?(?P<index>(?P<padding>0*)\\d+){}$\".format(\n                re.escape(ext))\n            patterns = [pattern]\n\n            collections, remainder = clique.assemble(\n                repr[\"files\"], minimum_items=1, patterns=patterns)\n\n            if remainder:\n                raise PublishValidationError(\n                    \"Some files have been found outside a sequence. \"\n                    f\"Invalid files: {remainder}\")\n            if not collections:\n                raise PublishValidationError(\n                    \"We have been unable to find a sequence in the \"\n                    \"files. Please ensure the files are named \"\n                    \"appropriately. \"\n                    f\"Files: {repr_files}\")\n            if len(collections) > 1:\n                raise PublishValidationError(\n                    \"Multiple collections detected. There should be a single \"\n                    \"collection per representation. \"\n                    f\"Collections identified: {collections}\")\n\n            collection = collections[0]\n            frames = list(collection.indexes)\n\n            if instance.data.get(\"slate\"):\n                # Slate is not part of the frame range\n                frames = frames[1:]\n\n            current_range = (frames[0], frames[-1])\n            required_range = (data[\"clipIn\"],\n                              data[\"clipOut\"])\n\n            if current_range != required_range:\n                raise PublishValidationError(\n                    f\"Invalid frame range: {current_range} - \"\n                    f\"expected: {required_range}\")\n\n            missing = collection.holes().indexes\n            if missing:\n                raise PublishValidationError(\n                    \"Missing frames have been detected. \"\n                    f\"Missing frames: {missing}\")\n"
  },
  {
    "path": "openpype/hosts/unreal/ue_workers.py",
    "content": "import json\nimport os\nimport platform\nimport re\nimport subprocess\nimport tempfile\nfrom distutils import dir_util\nfrom distutils.dir_util import copy_tree\nfrom pathlib import Path\nfrom typing import List, Union\n\nfrom qtpy import QtCore\n\nimport openpype.hosts.unreal.lib as ue_lib\nfrom openpype.settings import get_project_settings\n\n\ndef parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)):\n    match = re.search(r\"\\[[1-9]+/[0-9]+]\", line)\n    if match is not None:\n        split: list[str] = match.group().split(\"/\")\n        curr: float = float(split[0][1:])\n        total: float = float(split[1][:-1])\n        progress_signal.emit(int((curr / total) * 100.0))\n\n\ndef parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)):\n    match = re.search(\"@progress\", line)\n    if match is not None:\n        percent_match = re.search(r\"\\d{1,3}\", line)\n        progress_signal.emit(int(percent_match.group()))\n\n\ndef retrieve_exit_code(line: str):\n    match = re.search(r\"ExitCode=\\d+\", line)\n    if match is not None:\n        split: list[str] = match.group().split(\"=\")\n        return int(split[1])\n\n    return None\n\n\nclass UEWorker(QtCore.QObject):\n    finished = QtCore.Signal(str)\n    failed = QtCore.Signal(str, int)\n    progress = QtCore.Signal(int)\n    log = QtCore.Signal(str)\n\n    engine_path: Path = None\n    env = None\n\n    def execute(self):\n        raise NotImplementedError(\"Please implement this method!\")\n\n    def run(self):\n        try:\n            self.execute()\n        except Exception as e:\n            import traceback\n            self.log.emit(str(e))\n            self.log.emit(traceback.format_exc())\n            self.failed.emit(str(e), 1)\n            raise e\n\n\nclass UEProjectGenerationWorker(UEWorker):\n    stage_begin = QtCore.Signal(str)\n\n    ue_version: str = None\n    project_name: str = None\n    project_dir: Path = None\n    dev_mode = False\n\n    def setup(self, ue_version: str,\n              project_name: str,\n              unreal_project_name,\n              engine_path: Path,\n              project_dir: Path,\n              dev_mode: bool = False,\n              env: dict = None):\n        \"\"\"Set the worker with necessary parameters.\n\n        Args:\n            ue_version (str): Unreal Engine version.\n            project_name (str): Name of the project in AYON.\n            unreal_project_name (str): Name of the project in Unreal.\n            engine_path (Path): Path to the Unreal Engine.\n            project_dir (Path): Path to the project directory.\n            dev_mode (bool, optional): Whether to run the project in dev mode.\n                Defaults to False.\n            env (dict, optional): Environment variables. Defaults to None.\n\n        \"\"\"\n\n        self.ue_version = ue_version\n        self.project_dir = project_dir\n        self.env = env or os.environ\n\n        preset = get_project_settings(project_name)[\"unreal\"][\"project_setup\"]\n\n        if dev_mode or preset[\"dev_mode\"]:\n            self.dev_mode = True\n\n        self.project_name = unreal_project_name\n        self.engine_path = engine_path\n\n    def execute(self):\n        # engine_path should be the location of UE_X.X folder\n\n        ue_editor_exe = ue_lib.get_editor_exe_path(self.engine_path,\n                                                   self.ue_version)\n        cmdlet_project = ue_lib.get_path_to_cmdlet_project(self.ue_version)\n        project_file = self.project_dir / f\"{self.project_name}.uproject\"\n\n        print(\"--- Generating a new project ...\")\n        # 1st stage\n        stage_count = 2\n        if self.dev_mode:\n            stage_count = 4\n\n        self.stage_begin.emit(\n            (\"Generating a new UE project ... 1 out of \"\n             f\"{stage_count}\"))\n\n        # Need to copy the commandlet project to a temporary folder where\n        # users don't need admin rights to write to.\n        cmdlet_tmp = tempfile.TemporaryDirectory()\n        cmdlet_filename = cmdlet_project.name\n        cmdlet_dir = cmdlet_project.parent.as_posix()\n        cmdlet_tmp_name = Path(cmdlet_tmp.name)\n        cmdlet_tmp_file = cmdlet_tmp_name.joinpath(cmdlet_filename)\n        copy_tree(\n            cmdlet_dir,\n            cmdlet_tmp_name.as_posix())\n\n        commandlet_cmd = [\n            f\"{ue_editor_exe.as_posix()}\",\n            f\"{cmdlet_tmp_file.as_posix()}\",\n            \"-run=AyonGenerateProject\",\n            f\"{project_file.resolve().as_posix()}\",\n        ]\n\n        if self.dev_mode:\n            commandlet_cmd.append(\"-GenerateCode\")\n\n        gen_process = subprocess.Popen(commandlet_cmd,\n                                       stdout=subprocess.PIPE,\n                                       stderr=subprocess.PIPE)\n\n        for line in gen_process.stdout:\n            decoded_line = line.decode(errors=\"replace\")\n            print(decoded_line, end=\"\")\n            self.log.emit(decoded_line)\n        gen_process.stdout.close()\n        return_code = gen_process.wait()\n\n        cmdlet_tmp.cleanup()\n\n        if return_code and return_code != 0:\n            msg = (\n                f\"Failed to generate {self.project_name} \"\n                f\"project! Exited with return code {return_code}\"\n            )\n            self.failed.emit(msg, return_code)\n            raise RuntimeError(msg)\n\n        print(\"--- Project has been generated successfully.\")\n        self.stage_begin.emit(\n            (f\"Writing the Engine ID of the build UE ... 1\"\n             f\" out of {stage_count}\"))\n\n        if not project_file.is_file():\n            msg = (\"Failed to write the Engine ID into .uproject file! Can \"\n                   \"not read!\")\n            self.failed.emit(msg)\n            raise RuntimeError(msg)\n\n        with open(project_file.as_posix(), mode=\"r+\") as pf:\n            pf_json = json.load(pf)\n            pf_json[\"EngineAssociation\"] = ue_lib.get_build_id(\n                self.engine_path,\n                self.ue_version\n            )\n            print(pf_json[\"EngineAssociation\"])\n            pf.seek(0)\n            json.dump(pf_json, pf, indent=4)\n            pf.truncate()\n            print(\"--- Engine ID has been written into the project file\")\n\n        self.progress.emit(90)\n        if self.dev_mode:\n            # 2nd stage\n            self.stage_begin.emit(\n                (f\"Generating project files ... 2 out of \"\n                 f\"{stage_count}\"))\n\n            self.progress.emit(0)\n            ubt_path = ue_lib.get_path_to_ubt(self.engine_path,\n                                              self.ue_version)\n\n            arch = \"Win64\"\n            if platform.system().lower() == \"windows\":\n                arch = \"Win64\"\n            elif platform.system().lower() == \"linux\":\n                arch = \"Linux\"\n            elif platform.system().lower() == \"darwin\":\n                # we need to test this out\n                arch = \"Mac\"\n\n            gen_prj_files_cmd = [ubt_path.as_posix(),\n                                 \"-projectfiles\",\n                                 f\"-project={project_file}\",\n                                 \"-progress\"]\n            gen_proc = subprocess.Popen(gen_prj_files_cmd,\n                                        stdout=subprocess.PIPE,\n                                        stderr=subprocess.PIPE)\n            for line in gen_proc.stdout:\n                decoded_line: str = line.decode(errors=\"replace\")\n                print(decoded_line, end=\"\")\n                self.log.emit(decoded_line)\n                parse_prj_progress(decoded_line, self.progress)\n\n            gen_proc.stdout.close()\n            return_code = gen_proc.wait()\n\n            if return_code and return_code != 0:\n                msg = (\"Failed to generate project files! \"\n                       f\"Exited with return code {return_code}\")\n                self.failed.emit(msg, return_code)\n                raise RuntimeError(msg)\n\n            self.stage_begin.emit(\n                f\"Building the project ... 3 out of {stage_count}\")\n            self.progress.emit(0)\n            # 3rd stage\n            build_prj_cmd = [ubt_path.as_posix(),\n                             f\"-ModuleWithSuffix={self.project_name},3555\",\n                             arch,\n                             \"Development\",\n                             \"-TargetType=Editor\",\n                             f\"-Project={project_file}\",\n                             f\"{project_file}\",\n                             \"-IgnoreJunk\"]\n\n            build_prj_proc = subprocess.Popen(build_prj_cmd,\n                                              stdout=subprocess.PIPE,\n                                              stderr=subprocess.PIPE)\n            for line in build_prj_proc.stdout:\n                decoded_line: str = line.decode(errors=\"replace\")\n                print(decoded_line, end=\"\")\n                self.log.emit(decoded_line)\n                parse_comp_progress(decoded_line, self.progress)\n\n            build_prj_proc.stdout.close()\n            return_code = build_prj_proc.wait()\n\n            if return_code and return_code != 0:\n                msg = (\"Failed to build project! \"\n                       f\"Exited with return code {return_code}\")\n                self.failed.emit(msg, return_code)\n                raise RuntimeError(msg)\n\n        # ensure we have PySide2 installed in engine\n\n        self.progress.emit(0)\n        self.stage_begin.emit(\n            (f\"Checking PySide2 installation... {stage_count} \"\n             f\" out of {stage_count}\"))\n        python_path = None\n        if platform.system().lower() == \"windows\":\n            python_path = self.engine_path / (\"Engine/Binaries/ThirdParty/\"\n                                              \"Python3/Win64/python.exe\")\n\n        if platform.system().lower() == \"linux\":\n            python_path = self.engine_path / (\"Engine/Binaries/ThirdParty/\"\n                                              \"Python3/Linux/bin/python3\")\n\n        if platform.system().lower() == \"darwin\":\n            python_path = self.engine_path / (\"Engine/Binaries/ThirdParty/\"\n                                              \"Python3/Mac/bin/python3\")\n\n        if not python_path:\n            msg = \"Unsupported platform\"\n            self.failed.emit(msg, 1)\n            raise NotImplementedError(msg)\n        if not python_path.exists():\n            msg = f\"Unreal Python not found at {python_path}\"\n            self.failed.emit(msg, 1)\n            raise RuntimeError(msg)\n        pyside_cmd = [python_path.as_posix(),\n                      \"-m\",\n                      \"pip\",\n                      \"install\",\n                      \"pyside2\"]\n\n        pyside_install = subprocess.Popen(pyside_cmd,\n                                          stdout=subprocess.PIPE,\n                                          stderr=subprocess.PIPE)\n\n        for line in pyside_install.stdout:\n            decoded_line: str = line.decode(errors=\"replace\")\n            print(decoded_line, end=\"\")\n            self.log.emit(decoded_line)\n\n        pyside_install.stdout.close()\n        return_code = pyside_install.wait()\n\n        if return_code and return_code != 0:\n            msg = (\"Failed to create the project! \"\n                   \"The installation of PySide2 has failed!\")\n            self.failed.emit(msg, return_code)\n            raise RuntimeError(msg)\n\n        self.progress.emit(100)\n        self.finished.emit(\"Project successfully built!\")\n\n\nclass UEPluginInstallWorker(UEWorker):\n    installing = QtCore.Signal(str)\n\n    def setup(self, engine_path: Path, env: dict = None, ):\n        self.engine_path = engine_path\n        self.env = env or os.environ\n\n    def _build_and_move_plugin(self, plugin_build_path: Path):\n        uat_path: Path = ue_lib.get_path_to_uat(self.engine_path)\n        src_plugin_dir = Path(self.env.get(\"AYON_UNREAL_PLUGIN\", \"\"))\n\n        if not os.path.isdir(src_plugin_dir):\n            msg = \"Path to the integration plugin is null!\"\n            self.failed.emit(msg, 1)\n            raise RuntimeError(msg)\n\n        if not uat_path.is_file():\n            msg = \"Building failed! Path to UAT is invalid!\"\n            self.failed.emit(msg, 1)\n            raise RuntimeError(msg)\n\n        temp_dir: Path = src_plugin_dir.parent / \"Temp\"\n        temp_dir.mkdir(exist_ok=True)\n        uplugin_path: Path = src_plugin_dir / \"Ayon.uplugin\"\n\n        # in order to successfully build the plugin,\n        # It must be built outside the Engine directory and then moved\n        build_plugin_cmd: List[str] = [f\"{uat_path.as_posix()}\",\n                                       \"BuildPlugin\",\n                                       f\"-Plugin={uplugin_path.as_posix()}\",\n                                       f\"-Package={temp_dir.as_posix()}\"]\n\n        build_proc = subprocess.Popen(build_plugin_cmd,\n                                      stdout=subprocess.PIPE,\n                                      stderr=subprocess.PIPE)\n        return_code: Union[None, int] = None\n        for line in build_proc.stdout:\n            decoded_line: str = line.decode(errors=\"replace\")\n            print(decoded_line, end=\"\")\n            self.log.emit(decoded_line)\n            if return_code is None:\n                return_code = retrieve_exit_code(decoded_line)\n            parse_comp_progress(decoded_line, self.progress)\n\n        build_proc.stdout.close()\n        build_proc.wait()\n\n        if return_code and return_code != 0:\n            msg = (\"Failed to build plugin\"\n                   f\" project! Exited with return code {return_code}\")\n            dir_util.remove_tree(temp_dir.as_posix())\n            self.failed.emit(msg, return_code)\n            raise RuntimeError(msg)\n\n        # Copy the contents of the 'Temp' dir into the\n        # 'Ayon' directory in the engine\n        dir_util.copy_tree(temp_dir.as_posix(),\n                           plugin_build_path.as_posix())\n\n        # We need to also copy the config folder.\n        # The UAT doesn't include the Config folder in the build\n        plugin_install_config_path: Path = plugin_build_path / \"Config\"\n        src_plugin_config_path = src_plugin_dir / \"Config\"\n\n        dir_util.copy_tree(src_plugin_config_path.as_posix(),\n                           plugin_install_config_path.as_posix())\n\n        dir_util.remove_tree(temp_dir.as_posix())\n\n    def execute(self):\n        src_plugin_dir = Path(self.env.get(\"AYON_UNREAL_PLUGIN\", \"\"))\n\n        if not os.path.isdir(src_plugin_dir):\n            msg = \"Path to the integration plugin is null!\"\n            self.failed.emit(msg, 1)\n            raise RuntimeError(msg)\n\n        # Create a path to the plugin in the engine\n        op_plugin_path = self.engine_path / \"Engine/Plugins/Marketplace\" \\\n                                            \"/Ayon\"\n\n        if not op_plugin_path.is_dir():\n            self.installing.emit(\"Installing and building the plugin ...\")\n            op_plugin_path.mkdir(parents=True, exist_ok=True)\n\n            engine_plugin_config_path = op_plugin_path / \"Config\"\n            engine_plugin_config_path.mkdir(exist_ok=True)\n\n            dir_util._path_created = {}\n\n        if not (op_plugin_path / \"Binaries\").is_dir() \\\n                or not (op_plugin_path / \"Intermediate\").is_dir():\n            self.installing.emit(\"Building the plugin ...\")\n            print(\"--- Building the plugin...\")\n\n            self._build_and_move_plugin(op_plugin_path)\n\n        self.finished.emit(\"Plugin successfully installed\")\n"
  },
  {
    "path": "openpype/hosts/unreal/ui/__init__.py",
    "content": "from .splash_screen import SplashScreen\n\n__all__ = (\n    \"SplashScreen\",\n)\n"
  },
  {
    "path": "openpype/hosts/unreal/ui/splash_screen.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\nfrom openpype import style, resources\n\n\nclass SplashScreen(QtWidgets.QDialog):\n    \"\"\"Splash screen for executing a process on another thread. It is able\n    to inform about the progress of the process and log given information.\n    \"\"\"\n\n    splash_icon = None\n    top_label = None\n    show_log_btn: QtWidgets.QLabel = None\n    progress_bar = None\n    log_text: QtWidgets.QLabel = None\n    scroll_area: QtWidgets.QScrollArea = None\n    close_btn: QtWidgets.QPushButton = None\n    scroll_bar: QtWidgets.QScrollBar = None\n\n    is_log_visible = False\n    is_scroll_auto = True\n\n    thread_return_code = None\n    q_thread: QtCore.QThread = None\n\n    def __init__(self,\n                 window_title: str,\n                 splash_icon=None,\n                 window_icon=None):\n        \"\"\"\n        Args:\n            window_title (str): String which sets the window title\n            splash_icon (str | bytes | None): A resource (pic) which is used\n                for the splash icon\n            window_icon (str | bytes | None: A resource (pic) which is used for\n                the window's icon\n        \"\"\"\n        super(SplashScreen, self).__init__()\n\n        if splash_icon is None:\n            splash_icon = resources.get_openpype_icon_filepath()\n\n        if window_icon is None:\n            window_icon = resources.get_openpype_icon_filepath()\n\n        self.splash_icon = splash_icon\n        self.setWindowIcon(QtGui.QIcon(window_icon))\n        self.setWindowTitle(window_title)\n        self.init_ui()\n\n    def was_proc_successful(self) -> bool:\n        return self.thread_return_code == 0\n\n    def start_thread(self, q_thread: QtCore.QThread):\n        \"\"\"Saves the reference to this thread and starts it.\n\n        Args:\n            q_thread (QtCore.QThread): A QThread containing a given worker\n                (QtCore.QObject)\n\n        Returns:\n            None\n        \"\"\"\n        if not q_thread:\n            raise RuntimeError(\"Failed to run a worker thread! \"\n                               \"The thread is null!\")\n\n        self.q_thread = q_thread\n        self.q_thread.start()\n\n    @QtCore.Slot()\n    def quit_and_close(self):\n        \"\"\"Quits the thread and closes the splash screen. Note that this means\n        the thread has exited with the return code 0!\n\n        Returns:\n            None\n        \"\"\"\n        self.thread_return_code = 0\n        self.q_thread.quit()\n\n        if not self.q_thread.wait(5000):\n            raise RuntimeError(\"Failed to quit the QThread! \"\n                               \"The deadline has been reached! The thread \"\n                               \"has not finished it's execution!.\")\n        self.close()\n\n\n    @QtCore.Slot()\n    def toggle_log(self):\n        if self.is_log_visible:\n            self.scroll_area.hide()\n            width = self.width()\n            self.adjustSize()\n            self.resize(width, self.height())\n        else:\n            self.scroll_area.show()\n            self.scroll_bar.setValue(self.scroll_bar.maximum())\n            self.resize(self.width(), 300)\n\n        self.is_log_visible = not self.is_log_visible\n\n    def show_ui(self):\n        \"\"\"Shows the splash screen. BEWARE THAT THIS FUNCTION IS BLOCKING\n        (The execution of code can not proceed further beyond this function\n        until the splash screen is closed!)\n\n        Returns:\n            None\n        \"\"\"\n        self.show()\n        self.exec_()\n\n    def init_ui(self):\n        self.resize(450, 100)\n        self.setMinimumWidth(250)\n        self.setStyleSheet(style.load_stylesheet())\n\n        # Top Section\n        self.top_label = QtWidgets.QLabel(self)\n        self.top_label.setText(\"Starting process ...\")\n        self.top_label.setWordWrap(True)\n\n        icon = QtWidgets.QLabel(self)\n        icon.setPixmap(QtGui.QPixmap(self.splash_icon))\n        icon.setFixedHeight(45)\n        icon.setFixedWidth(45)\n        icon.setScaledContents(True)\n\n        self.close_btn = QtWidgets.QPushButton(self)\n        self.close_btn.setText(\"Quit\")\n        self.close_btn.clicked.connect(self.close)\n        self.close_btn.setFixedWidth(80)\n        self.close_btn.hide()\n\n        self.show_log_btn = QtWidgets.QPushButton(self)\n        self.show_log_btn.setText(\"Show log\")\n        self.show_log_btn.setFixedWidth(80)\n        self.show_log_btn.clicked.connect(self.toggle_log)\n\n        button_layout = QtWidgets.QVBoxLayout()\n        button_layout.addWidget(self.show_log_btn)\n        button_layout.addWidget(self.close_btn)\n\n        # Progress Bar\n        self.progress_bar = QtWidgets.QProgressBar()\n        self.progress_bar.setValue(0)\n        self.progress_bar.setAlignment(QtCore.Qt.AlignTop)\n\n        # Log Content\n        self.scroll_area = QtWidgets.QScrollArea(self)\n        self.scroll_area.hide()\n        log_widget = QtWidgets.QWidget(self.scroll_area)\n        self.scroll_area.setWidgetResizable(True)\n        self.scroll_area.setHorizontalScrollBarPolicy(\n            QtCore.Qt.ScrollBarAlwaysOn\n        )\n        self.scroll_area.setVerticalScrollBarPolicy(\n            QtCore.Qt.ScrollBarAlwaysOn\n        )\n        self.scroll_area.setWidget(log_widget)\n\n        self.scroll_bar = self.scroll_area.verticalScrollBar()\n        self.scroll_bar.sliderMoved.connect(self.on_scroll)\n\n        self.log_text = QtWidgets.QLabel(self)\n        self.log_text.setText('')\n        self.log_text.setAlignment(QtCore.Qt.AlignTop)\n\n        log_layout = QtWidgets.QVBoxLayout(log_widget)\n        log_layout.addWidget(self.log_text)\n\n        top_layout = QtWidgets.QHBoxLayout()\n        top_layout.setAlignment(QtCore.Qt.AlignTop)\n        top_layout.addWidget(icon)\n        top_layout.addSpacing(10)\n        top_layout.addWidget(self.top_label)\n        top_layout.addSpacing(10)\n        top_layout.addLayout(button_layout)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addLayout(top_layout)\n        main_layout.addSpacing(10)\n        main_layout.addWidget(self.progress_bar)\n        main_layout.addSpacing(10)\n        main_layout.addWidget(self.scroll_area)\n\n        self.setWindowFlags(\n            QtCore.Qt.Window\n            | QtCore.Qt.CustomizeWindowHint\n            | QtCore.Qt.WindowTitleHint\n            | QtCore.Qt.WindowMinimizeButtonHint\n        )\n\n        desktop_rect = QtWidgets.QApplication.desktop().availableGeometry(self)\n        center = desktop_rect.center()\n        self.move(\n            center.x() - (self.width() * 0.5),\n            center.y() - (self.height() * 0.5)\n        )\n\n    @QtCore.Slot(int)\n    def update_progress(self, value: int):\n        self.progress_bar.setValue(value)\n\n    @QtCore.Slot(str)\n    def update_top_label_text(self, text: str):\n        self.top_label.setText(text)\n\n    @QtCore.Slot(str, str)\n    def append_log(self, text: str, end: str = ''):\n        \"\"\"A slot used for receiving log info and appending it to scroll area's\n            content.\n        Args:\n            text (str): A log text that will append to the current one in the\n                scroll area.\n            end (str): end string which can be appended to the end of the given\n                line (for ex. a line break).\n\n        Returns:\n            None\n        \"\"\"\n        self.log_text.setText(self.log_text.text() + text + end)\n        if self.is_scroll_auto:\n            self.scroll_bar.setValue(self.scroll_bar.maximum())\n\n    @QtCore.Slot(int)\n    def on_scroll(self, position: int):\n        \"\"\"\n        A slot for the vertical scroll bar's movement. This ensures the\n        auto-scrolling feature of the scroll area when the scroll bar is at its\n        maximum value.\n\n        Args:\n            position (int): Position value of the scroll bar.\n\n        Returns:\n             None\n        \"\"\"\n        if self.scroll_bar.maximum() == position:\n            self.is_scroll_auto = True\n            return\n\n        self.is_scroll_auto = False\n\n    @QtCore.Slot(str, int)\n    def fail(self, text: str, return_code: int = 1):\n        \"\"\"\n        A slot used for signals which can emit when a worker (process) has\n        failed. at this moment the splash screen doesn't close by itself.\n        it has to be closed by the user.\n\n        Args:\n            text (str): A text which can be set to the top label.\n\n        Returns:\n            return_code (int): Return code of the thread's code\n        \"\"\"\n        self.top_label.setText(text)\n        self.close_btn.show()\n        self.thread_return_code = return_code\n        self.q_thread.exit(return_code)\n        self.q_thread.wait()\n"
  },
  {
    "path": "openpype/hosts/webpublisher/README.md",
    "content": "Webpublisher\n-------------\n\nPlugins meant for processing of Webpublisher.\n\nGets triggered by calling `openpype_console modules webpublisher publish` with appropriate arguments.\n"
  },
  {
    "path": "openpype/hosts/webpublisher/__init__.py",
    "content": "from .addon import (\n    WebpublisherAddon,\n    WEBPUBLISHER_ROOT_DIR,\n)\n\n\n__all__ = (\n    \"WebpublisherAddon\",\n    \"WEBPUBLISHER_ROOT_DIR\",\n)\n"
  },
  {
    "path": "openpype/hosts/webpublisher/addon.py",
    "content": "import os\n\nfrom openpype.modules import click_wrap, OpenPypeModule, IHostAddon\n\nWEBPUBLISHER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass WebpublisherAddon(OpenPypeModule, IHostAddon):\n    name = \"webpublisher\"\n    host_name = \"webpublisher\"\n\n    def initialize(self, module_settings):\n        self.enabled = True\n\n    def headless_publish(self, log, close_plugin_name=None, is_test=False):\n        \"\"\"Runs publish in a opened host with a context.\n\n        Close Python process at the end.\n        \"\"\"\n\n        from .lib import get_webpublish_conn, publish_and_log, publish_in_test\n\n        if is_test:\n            publish_in_test(log, close_plugin_name)\n            return\n\n        dbcon = get_webpublish_conn()\n        _id = os.environ.get(\"BATCH_LOG_ID\")\n        if not _id:\n            log.warning(\"Unable to store log records, \"\n                        \"batch will be unfinished!\")\n            return\n\n        publish_and_log(\n            dbcon, _id, log, close_plugin_name=close_plugin_name\n        )\n\n    def cli(self, click_group):\n        click_group.add_command(cli_main.to_click_obj())\n\n\n@click_wrap.group(\n    WebpublisherAddon.name,\n    help=\"Webpublisher related commands.\")\ndef cli_main():\n    pass\n\n\n@cli_main.command()\n@click_wrap.argument(\"path\")\n@click_wrap.option(\"-u\", \"--user\", help=\"User email address\")\n@click_wrap.option(\"-p\", \"--project\", help=\"Project\")\n@click_wrap.option(\"-t\", \"--targets\", help=\"Targets\", default=None,\n              multiple=True)\ndef publish(project, path, user=None, targets=None):\n    \"\"\"Start publishing (Inner command).\n\n    Publish collects json from paths provided as an argument.\n    More than one path is allowed.\n    \"\"\"\n\n    from .publish_functions import cli_publish\n\n    cli_publish(project, path, user, targets)\n\n\n@cli_main.command()\n@click_wrap.argument(\"path\")\n@click_wrap.option(\"-p\", \"--project\", help=\"Project\")\n@click_wrap.option(\"-h\", \"--host\", help=\"Host\")\n@click_wrap.option(\"-u\", \"--user\", help=\"User email address\")\n@click_wrap.option(\"-t\", \"--targets\", help=\"Targets\", default=None,\n              multiple=True)\ndef publishfromapp(project, path, host, user=None, targets=None):\n    \"\"\"Start publishing through application (Inner command).\n\n    Publish collects json from paths provided as an argument.\n    More than one path is allowed.\n    \"\"\"\n\n    from .publish_functions import cli_publish_from_app\n\n    cli_publish_from_app(project, path, host, user, targets)\n\n\n@cli_main.command()\n@click_wrap.option(\"-e\", \"--executable\", help=\"Executable\")\n@click_wrap.option(\"-u\", \"--upload_dir\", help=\"Upload dir\")\n@click_wrap.option(\"-h\", \"--host\", help=\"Host\", default=None)\n@click_wrap.option(\"-p\", \"--port\", help=\"Port\", default=None)\ndef webserver(executable, upload_dir, host=None, port=None):\n    \"\"\"Start service for communication with Webpublish Front end.\n\n        OP must be congigured on a machine, eg. OPENPYPE_MONGO filled AND\n        FTRACK_BOT_API_KEY provided with api key from Ftrack.\n\n        Expect \"pype.club\" user created on Ftrack.\n    \"\"\"\n\n    from .webserver_service import run_webserver\n\n    run_webserver(executable, upload_dir, host, port)\n"
  },
  {
    "path": "openpype/hosts/webpublisher/api/__init__.py",
    "content": "import os\nimport logging\n\nimport pyblish.api\n\nfrom openpype.host import HostBase\nfrom openpype.hosts.webpublisher import WEBPUBLISHER_ROOT_DIR\n\nlog = logging.getLogger(\"openpype.hosts.webpublisher\")\n\n\nclass WebpublisherHost(HostBase):\n    name = \"webpublisher\"\n\n    def install(self):\n        print(\"Installing Pype config...\")\n        pyblish.api.register_host(self.name)\n\n        publish_plugin_dir = os.path.join(\n            WEBPUBLISHER_ROOT_DIR, \"plugins\", \"publish\"\n        )\n        pyblish.api.register_plugin_path(publish_plugin_dir)\n        self.log.info(publish_plugin_dir)\n"
  },
  {
    "path": "openpype/hosts/webpublisher/lib.py",
    "content": "import os\nfrom datetime import datetime\nimport collections\nimport json\n\nfrom bson.objectid import ObjectId\n\nimport pyblish.util\nimport pyblish.api\n\nfrom openpype.client.mongo import OpenPypeMongoConnection\nfrom openpype.settings import get_project_settings\nfrom openpype.lib import Logger\nfrom openpype.lib.profiles_filtering import filter_profiles\n\nERROR_STATUS = \"error\"\nIN_PROGRESS_STATUS = \"in_progress\"\nREPROCESS_STATUS = \"reprocess\"\nSENT_REPROCESSING_STATUS = \"sent_for_reprocessing\"\nFINISHED_REPROCESS_STATUS = \"republishing_finished\"\nFINISHED_OK_STATUS = \"finished_ok\"\n\nlog = Logger.get_logger(__name__)\n\n\ndef parse_json(path):\n    \"\"\"Parses json file at 'path' location\n\n        Returns:\n            (dict) or None if unparsable\n        Raises:\n            AssertionError if 'path' doesn't exist\n    \"\"\"\n    path = path.strip('\\\"')\n    assert os.path.isfile(path), (\n        \"Path to json file doesn't exist. \\\"{}\\\"\".format(path)\n    )\n    data = None\n    with open(path, \"r\") as json_file:\n        try:\n            data = json.load(json_file)\n        except Exception as exc:\n            log.error(\n                \"Error loading json: {} - Exception: {}\".format(path, exc)\n            )\n    return data\n\n\ndef get_batch_asset_task_info(ctx):\n    \"\"\"Parses context data from webpublisher's batch metadata\n\n        Returns:\n            (tuple): asset, task_name (Optional), task_type\n    \"\"\"\n    task_type = \"default_task_type\"\n    task_name = None\n    asset = None\n\n    if ctx[\"type\"] == \"task\":\n        items = ctx[\"path\"].split('/')\n        asset = items[-2]\n        task_name = ctx[\"name\"]\n        task_type = ctx[\"attributes\"][\"type\"]\n    else:\n        asset = ctx[\"name\"]\n\n    return asset, task_name, task_type\n\n\ndef find_close_plugin(close_plugin_name, log):\n    if close_plugin_name:\n        plugins = pyblish.api.discover()\n        for plugin in plugins:\n            if plugin.__name__ == close_plugin_name:\n                return plugin\n\n    log.debug(\"Close plugin not found, app might not close.\")\n\n\ndef publish_in_test(log, close_plugin_name=None):\n    \"\"\"Loops through all plugins, logs to console. Used for tests.\n\n    Args:\n        log (Logger)\n        close_plugin_name (Optional[str]): Name of plugin with responsibility\n            to close application.\n    \"\"\"\n\n    # Error exit as soon as any error occurs.\n    error_format = \"Failed {plugin.__name__}: {error} -- {error.traceback}\"\n\n    close_plugin = find_close_plugin(close_plugin_name, log)\n\n    for result in pyblish.util.publish_iter():\n        for record in result[\"records\"]:\n            # Why do we log again? pyblish logger is logging to stdout...\n            log.info(\"{}: {}\".format(result[\"plugin\"].label, record.msg))\n\n        if not result[\"error\"]:\n            continue\n\n        # QUESTION We don't break on error?\n        error_message = error_format.format(**result)\n        log.error(error_message)\n        if close_plugin:  # close host app explicitly after error\n            context = pyblish.api.Context()\n            close_plugin().process(context)\n\n\ndef get_webpublish_conn():\n    \"\"\"Get connection to OP 'webpublishes' collection.\"\"\"\n    mongo_client = OpenPypeMongoConnection.get_mongo_client()\n    database_name = os.environ[\"OPENPYPE_DATABASE_NAME\"]\n    return mongo_client[database_name][\"webpublishes\"]\n\n\ndef start_webpublish_log(dbcon, batch_id, user):\n    \"\"\"Start new log record for 'batch_id'\n\n        Args:\n            dbcon (OpenPypeMongoConnection)\n            batch_id (str)\n            user (str)\n        Returns\n            (ObjectId) from DB\n    \"\"\"\n    return dbcon.insert_one({\n        \"batch_id\": batch_id,\n        \"start_date\": datetime.now(),\n        \"user\": user,\n        \"status\": IN_PROGRESS_STATUS,\n        \"progress\": 0  # integer 0-100, percentage\n    }).inserted_id\n\n\ndef publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None):\n    \"\"\"Loops through all plugins, logs ok and fails into OP DB.\n\n        Args:\n            dbcon (OpenPypeMongoConnection)\n            _id (str) - id of current job in DB\n            log (openpype.lib.Logger)\n            batch_id (str) - id sent from frontend\n            close_plugin_name (str): name of plugin with responsibility to\n                close host app\n    \"\"\"\n    # Error exit as soon as any error occurs.\n    error_format = \"Failed {plugin.__name__}: {error} -- {error.traceback}\\n\"\n    error_format += \"-\" * 80 + \"\\n\"\n\n    close_plugin = find_close_plugin(close_plugin_name, log)\n\n    if isinstance(_id, str):\n        _id = ObjectId(_id)\n\n    log_lines = []\n    processed = 0\n    log_every = 5\n    for result in pyblish.util.publish_iter():\n        for record in result[\"records\"]:\n            log_lines.append(\"{}: {}\".format(\n                result[\"plugin\"].label, record.msg))\n        processed += 1\n\n        if result[\"error\"]:\n            log.error(error_format.format(**result))\n            log_lines = [error_format.format(**result)] + log_lines\n            dbcon.update_one(\n                {\"_id\": _id},\n                {\"$set\":\n                    {\n                        \"finish_date\": datetime.now(),\n                        \"status\": ERROR_STATUS,\n                        \"log\": os.linesep.join(log_lines)\n\n                    }}\n            )\n            if close_plugin:  # close host app explicitly after error\n                context = pyblish.api.Context()\n                close_plugin().process(context)\n            return\n        elif processed % log_every == 0:\n            # pyblish returns progress in 0.0 - 2.0\n            progress = min(round(result[\"progress\"] / 2 * 100), 99)\n            dbcon.update_one(\n                {\"_id\": _id},\n                {\"$set\":\n                    {\n                        \"progress\": progress,\n                        \"log\": os.linesep.join(log_lines)\n                    }}\n            )\n\n    # final update\n    if batch_id:\n        dbcon.update_many(\n            {\"batch_id\": batch_id, \"status\": SENT_REPROCESSING_STATUS},\n            {\n                \"$set\":\n                    {\n                        \"finish_date\": datetime.now(),\n                        \"status\": FINISHED_REPROCESS_STATUS,\n                    }\n            }\n        )\n\n    dbcon.update_one(\n        {\"_id\": _id},\n        {\n            \"$set\":\n                {\n                    \"finish_date\": datetime.now(),\n                    \"status\": FINISHED_OK_STATUS,\n                    \"progress\": 100,\n                    \"log\": os.linesep.join(log_lines)\n                }\n        }\n    )\n\n\ndef fail_batch(_id, dbcon, msg):\n    \"\"\"Set current batch as failed as there is some problem.\n\n    Raises:\n        ValueError\n    \"\"\"\n    dbcon.update_one(\n        {\"_id\": _id},\n        {\"$set\":\n            {\n                \"finish_date\": datetime.now(),\n                \"status\": ERROR_STATUS,\n                \"log\": msg\n\n            }}\n    )\n    raise ValueError(msg)\n\n\ndef find_variant_key(application_manager, host):\n    \"\"\"Searches for latest installed variant for 'host'\n\n        Args:\n            application_manager (ApplicationManager)\n            host (str)\n        Returns\n            (string) (optional)\n        Raises:\n            (ValueError) if no variant found\n    \"\"\"\n    app_group = application_manager.app_groups.get(host)\n    if not app_group or not app_group.enabled:\n        raise ValueError(\"No application {} configured\".format(host))\n\n    found_variant_key = None\n    # finds most up-to-date variant if any installed\n    sorted_variants = collections.OrderedDict(\n        sorted(app_group.variants.items()))\n    for variant_key, variant in sorted_variants.items():\n        for executable in variant.executables:\n            if executable.exists():\n                found_variant_key = variant_key\n\n    if not found_variant_key:\n        raise ValueError(\"No executable for {} found\".format(host))\n\n    return found_variant_key\n\n\ndef get_task_data(batch_dir):\n    \"\"\"Return parsed data from first task manifest.json\n\n        Used for `publishfromapp` command where batch contains only\n        single task with publishable workfile.\n\n        Returns:\n            (dict)\n        Throws:\n            (ValueError) if batch or task manifest not found or broken\n    \"\"\"\n    batch_data = parse_json(os.path.join(batch_dir, \"manifest.json\"))\n    if not batch_data:\n        raise ValueError(\n            \"Cannot parse batch meta in {} folder\".format(batch_dir))\n    task_dir_name = batch_data[\"tasks\"][0]\n    task_data = parse_json(os.path.join(batch_dir, task_dir_name,\n                                        \"manifest.json\"))\n    if not task_data:\n        raise ValueError(\n            \"Cannot parse batch meta in {} folder\".format(task_data))\n\n    return task_data\n\n\ndef get_timeout(project_name, host_name, task_type):\n    \"\"\"Returns timeout(seconds) from Setting profile.\"\"\"\n    filter_data = {\n        \"task_types\": task_type,\n        \"hosts\": host_name\n    }\n    timeout_profiles = (get_project_settings(project_name)[\"webpublisher\"]\n                                                          [\"timeout_profiles\"])\n    matching_item = filter_profiles(timeout_profiles, filter_data)\n    timeout = 3600\n    if matching_item:\n        timeout = matching_item[\"timeout\"]\n\n    return timeout\n"
  },
  {
    "path": "openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py",
    "content": "\"\"\"Parses batch context from json and continues in publish process.\n\nProvides:\n    context -> Loaded batch file.\n        - asset\n        - task  (task name)\n        - taskType\n        - project_name\n        - variant\n\"\"\"\n\nimport os\n\nimport pyblish.api\n\nfrom openpype.pipeline import legacy_io\nfrom openpype_modules.webpublisher.lib import (\n    parse_json,\n    get_batch_asset_task_info,\n    get_webpublish_conn,\n    IN_PROGRESS_STATUS\n)\n\n\nclass CollectBatchData(pyblish.api.ContextPlugin):\n    \"\"\"Collect batch data from json stored in 'OPENPYPE_PUBLISH_DATA' env dir.\n\n    The directory must contain 'manifest.json' file where batch data should be\n    stored.\n    \"\"\"\n    # must be really early, context values are only in json file\n    order = pyblish.api.CollectorOrder - 0.495\n    label = \"Collect batch data\"\n    hosts = [\"webpublisher\"]\n\n    def process(self, context):\n        batch_dir = os.environ.get(\"OPENPYPE_PUBLISH_DATA\")\n\n        assert batch_dir, (\n            \"Missing `OPENPYPE_PUBLISH_DATA`\")\n\n        assert os.path.exists(batch_dir), \\\n            \"Folder {} doesn't exist\".format(batch_dir)\n\n        project_name = os.environ.get(\"AVALON_PROJECT\")\n        if project_name is None:\n            raise AssertionError(\n                \"Environment `AVALON_PROJECT` was not found.\"\n                \"Could not set project `root` which may cause issues.\"\n            )\n\n        batch_data = parse_json(os.path.join(batch_dir, \"manifest.json\"))\n\n        context.data[\"batchDir\"] = batch_dir\n        context.data[\"batchData\"] = batch_data\n\n        asset_name, task_name, task_type = get_batch_asset_task_info(\n            batch_data[\"context\"]\n        )\n\n        os.environ[\"AVALON_ASSET\"] = asset_name\n        legacy_io.Session[\"AVALON_ASSET\"] = asset_name\n        os.environ[\"AVALON_TASK\"] = task_name\n        legacy_io.Session[\"AVALON_TASK\"] = task_name\n\n        context.data[\"asset\"] = asset_name\n        context.data[\"task\"] = task_name\n        context.data[\"taskType\"] = task_type\n        context.data[\"project_name\"] = project_name\n        context.data[\"variant\"] = batch_data[\"variant\"]\n\n        self._set_ctx_path(batch_data)\n\n    def _set_ctx_path(self, batch_data):\n        dbcon = get_webpublish_conn()\n\n        batch_id = batch_data[\"batch\"]\n        ctx_path = batch_data[\"context\"][\"path\"]\n        self.log.info(\"ctx_path: {}\".format(ctx_path))\n        self.log.info(\"batch_id: {}\".format(batch_id))\n        if ctx_path and batch_id:\n            self.log.info(\"Updating log record\")\n            dbcon.update_one(\n                {\n                    \"batch_id\": batch_id,\n                    \"status\": IN_PROGRESS_STATUS\n                },\n                {\n                    \"$set\": {\n                        \"path\": ctx_path\n                    }\n                }\n            )\n"
  },
  {
    "path": "openpype/hosts/webpublisher/plugins/publish/collect_fps.py",
    "content": "\"\"\"\nRequires:\n    Nothing\n\nProvides:\n    Instance\n\"\"\"\n\nimport pyblish.api\nfrom pprint import pformat\n\n\nclass CollectFPS(pyblish.api.InstancePlugin):\n    \"\"\"\n        Adds fps from context to instance because of ExtractReview\n    \"\"\"\n\n    label = \"Collect fps\"\n    order = pyblish.api.CollectorOrder + 0.49\n    hosts = [\"webpublisher\"]\n\n    def process(self, instance):\n        instance_fps = instance.data.get(\"fps\")\n        if instance_fps is None:\n            instance.data[\"fps\"] = instance.context.data[\"fps\"]\n\n        self.log.debug(f\"instance.data: {pformat(instance.data)}\")\n"
  },
  {
    "path": "openpype/hosts/webpublisher/plugins/publish/collect_published_files.py",
    "content": "\"\"\"Create instances from batch data and continues in publish process.\n\nRequires:\n    CollectBatchData\n\nProvides:\n    context, instances -> All data from previous publishing process.\n\"\"\"\n\nimport os\nimport clique\nimport tempfile\nimport math\n\nimport pyblish.api\n\nfrom openpype.client import (\n    get_asset_by_name,\n    get_last_version_by_subset_name\n)\nfrom openpype.lib import (\n    prepare_template_data,\n    get_ffprobe_streams,\n    convert_ffprobe_fps_value,\n)\nfrom openpype.pipeline.create import get_subset_name\nfrom openpype_modules.webpublisher.lib import parse_json\nfrom openpype.pipeline.version_start import get_versioning_start\n\n\nclass CollectPublishedFiles(pyblish.api.ContextPlugin):\n    \"\"\"\n    This collector will try to find json files in provided\n    `OPENPYPE_PUBLISH_DATA`. Those files _MUST_ share same context.\n\n    This covers 'basic' webpublishes, eg artists uses Standalone Publisher to\n    publish rendered frames or assets.\n\n    This is not applicable for 'studio' processing where host application is\n    called to process uploaded workfile and render frames itself.\n\n    For each task configure what properties should resulting instance have\n    based on uploaded files:\n    - uploading sequence of 'png' >> create instance of 'render' family,\n    by adding 'review' to 'Families' and 'Create review' to Tags it will\n    produce review.\n\n    There might be difference between single(>>image) and sequence(>>render)\n    uploaded files.\n    \"\"\"\n    # must be really early, context values are only in json file\n    order = pyblish.api.CollectorOrder - 0.490\n    label = \"Collect rendered frames\"\n    hosts = [\"webpublisher\"]\n    targets = [\"filespublish\"]\n\n    # from Settings\n    task_type_to_family = []\n    sync_next_version = False  # find max version to be published, use for all\n\n    def process(self, context):\n        batch_dir = context.data[\"batchDir\"]\n        task_subfolders = []\n        for folder_name in os.listdir(batch_dir):\n            full_path = os.path.join(batch_dir, folder_name)\n            if os.path.isdir(full_path):\n                task_subfolders.append(full_path)\n\n        self.log.info(\"task_sub:: {}\".format(task_subfolders))\n\n        project_name = context.data[\"project_name\"]\n        asset_name = context.data[\"asset\"]\n        asset_doc = get_asset_by_name(project_name, asset_name)\n        task_name = context.data[\"task\"]\n        task_type = context.data[\"taskType\"]\n        project_name = context.data[\"project_name\"]\n        variant = context.data[\"variant\"]\n\n        next_versions = []\n        instances = []\n        for task_dir in task_subfolders:\n            task_data = parse_json(os.path.join(task_dir,\n                                                \"manifest.json\"))\n            self.log.info(\"task_data:: {}\".format(task_data))\n\n            is_sequence = len(task_data[\"files\"]) > 1\n            first_file = task_data[\"files\"][0]\n\n            _, extension = os.path.splitext(first_file)\n            extension = extension.lower()\n            family, families, tags = self._get_family(\n                self.task_type_to_family,\n                task_type,\n                is_sequence,\n                extension.replace(\".\", ''))\n\n            subset_name = get_subset_name(\n                family,\n                variant,\n                task_name,\n                asset_doc,\n                project_name=project_name,\n                host_name=\"webpublisher\",\n                project_settings=context.data[\"project_settings\"]\n            )\n            version = self._get_next_version(\n                project_name,\n                asset_doc,\n                task_name,\n                task_type,\n                family,\n                subset_name,\n                context\n            )\n            next_versions.append(version)\n\n            instance = context.create_instance(subset_name)\n            instance.data[\"asset\"] = asset_name\n            instance.data[\"subset\"] = subset_name\n            # set configurable result family\n            instance.data[\"family\"] = family\n            # set configurable additional families\n            instance.data[\"families\"] = families\n            instance.data[\"version\"] = version\n            instance.data[\"stagingDir\"] = tempfile.mkdtemp()\n            instance.data[\"source\"] = \"webpublisher\"\n\n            # to convert from email provided into Ftrack username\n            instance.data[\"user_email\"] = task_data[\"user\"]\n\n            if is_sequence:\n                instance.data[\"representations\"] = self._process_sequence(\n                    task_data[\"files\"], task_dir, tags\n                )\n                instance.data[\"frameStart\"] = \\\n                    instance.data[\"representations\"][0][\"frameStart\"]\n                instance.data[\"frameEnd\"] = \\\n                    instance.data[\"representations\"][0][\"frameEnd\"]\n            else:\n                frame_start = asset_doc[\"data\"][\"frameStart\"]\n                instance.data[\"frameStart\"] = frame_start\n                instance.data[\"frameEnd\"] = asset_doc[\"data\"][\"frameEnd\"]\n                instance.data[\"representations\"] = self._get_single_repre(\n                    task_dir, task_data[\"files\"], tags\n                )\n                if family != 'workfile':\n                    file_url = os.path.join(task_dir, task_data[\"files\"][0])\n                    try:\n                        no_of_frames = self._get_number_of_frames(file_url)\n                        if no_of_frames:\n                            frame_end = (\n                                int(frame_start) + math.ceil(no_of_frames)\n                            )\n                            frame_end = math.ceil(frame_end) - 1\n                            instance.data[\"frameEnd\"] = frame_end\n                            self.log.debug(\"frameEnd:: {}\".format(\n                                instance.data[\"frameEnd\"]))\n                    except Exception:\n                        self.log.warning(\"Unable to count frames duration.\")\n\n            instance.data[\"handleStart\"] = asset_doc[\"data\"][\"handleStart\"]\n            instance.data[\"handleEnd\"] = asset_doc[\"data\"][\"handleEnd\"]\n\n            if \"review\" in tags:\n                first_file_path = os.path.join(task_dir, first_file)\n                instance.data[\"thumbnailSource\"] = first_file_path\n\n            instances.append(instance)\n            self.log.info(\"instance.data:: {}\".format(instance.data))\n\n        if not self.sync_next_version:\n            return\n\n        # overwrite specific version with same version for all\n        max_next_version = max(next_versions)\n        for inst in instances:\n            inst.data[\"version\"] = max_next_version\n            self.log.debug(\"overwritten version:: {}\".format(max_next_version))\n\n    def _get_subset_name(self, family, subset_template, task_name, variant):\n        fill_pairs = {\n            \"variant\": variant,\n            \"family\": family,\n            \"task\": task_name\n        }\n        subset = subset_template.format(**prepare_template_data(fill_pairs))\n        return subset\n\n    def _get_single_repre(self, task_dir, files, tags):\n        _, ext = os.path.splitext(files[0])\n        ext = ext.lower()\n        repre_data = {\n            \"name\": ext[1:],\n            \"ext\": ext[1:],\n            \"files\": files[0],\n            \"stagingDir\": task_dir,\n            \"tags\": tags\n        }\n        self.log.info(\"single file repre_data.data:: {}\".format(repre_data))\n        return [repre_data]\n\n    def _process_sequence(self, files, task_dir, tags):\n        \"\"\"Prepare representation for sequence of files.\"\"\"\n        collections, remainder = clique.assemble(files)\n        assert len(collections) == 1, \\\n            \"Too many collections in {}\".format(files)\n\n        frame_start = list(collections[0].indexes)[0]\n        frame_end = list(collections[0].indexes)[-1]\n        ext = collections[0].tail\n        ext = ext.lower()\n        repre_data = {\n            \"frameStart\": frame_start,\n            \"frameEnd\": frame_end,\n            \"name\": ext[1:],\n            \"ext\": ext[1:],\n            \"files\": files,\n            \"stagingDir\": task_dir,\n            \"tags\": tags  # configurable tags from Settings\n        }\n        self.log.info(\"sequences repre_data.data:: {}\".format(repre_data))\n        return [repre_data]\n\n    def _get_family(self, settings, task_type, is_sequence, extension):\n        \"\"\"Guess family based on input data.\n\n            Args:\n                settings (dict): configuration per task_type\n                task_type (str): Animation|Art etc\n                is_sequence (bool): single file or sequence\n                extension (str): without '.'\n\n            Returns:\n                (family, [families], tags) tuple\n                AssertionError if not matching family found\n        \"\"\"\n        task_type = task_type.lower()\n        lower_cased_task_types = {}\n        for t_type, task in settings.items():\n            lower_cased_task_types[t_type.lower()] = task\n        task_obj = lower_cased_task_types.get(task_type)\n        assert task_obj, \"No family configuration for '{}'\".format(task_type)\n\n        found_family = None\n        families_config = []\n        # backward compatibility, should be removed pretty soon\n        if isinstance(task_obj, dict):\n            for family, config in task_obj:\n                config[\"result_family\"] = family\n                families_config.append(config)\n        else:\n            families_config = task_obj\n\n        for config in families_config:\n            if is_sequence != config[\"is_sequence\"]:\n                continue\n            extensions = config.get(\"extensions\") or []\n            lower_extensions = set()\n            for ext in extensions:\n                if ext:\n                    ext = ext.lower()\n                    if ext.startswith(\".\"):\n                        ext = ext[1:]\n                    lower_extensions.add(ext)\n\n            # all extensions setting\n            if not lower_extensions or extension in lower_extensions:\n                found_family = config[\"result_family\"]\n                break\n\n        msg = \"No family found for combination of \" +\\\n              \"task_type: {}, is_sequence:{}, extension: {}\".format(\n                  task_type, is_sequence, extension)\n        assert found_family, msg\n\n        return (found_family,\n                config[\"families\"],\n                config[\"tags\"])\n\n    def _get_next_version(\n        self,\n        project_name,\n        asset_doc,\n        task_name,\n        task_type,\n        family,\n        subset_name,\n        context\n    ):\n        \"\"\"Returns version number or 1 for 'asset' and 'subset'\"\"\"\n\n        version_doc = get_last_version_by_subset_name(\n            project_name,\n            subset_name,\n            asset_doc[\"_id\"],\n            fields=[\"name\"]\n        )\n        if version_doc:\n            version = int(version_doc[\"name\"]) + 1\n        else:\n            version = get_versioning_start(\n                project_name,\n                \"webpublisher\",\n                task_name=task_name,\n                task_type=task_type,\n                family=family,\n                subset=subset_name,\n                project_settings=context.data[\"project_settings\"]\n            )\n\n        return version\n\n    def _get_number_of_frames(self, file_url):\n        \"\"\"Return duration in frames\"\"\"\n        try:\n            streams = get_ffprobe_streams(file_url, self.log)\n        except Exception as exc:\n            raise AssertionError((\n                \"FFprobe couldn't read information about input file: \\\"{}\\\".\"\n                \" Error message: {}\"\n            ).format(file_url, str(exc)))\n\n        first_video_stream = None\n        for stream in streams:\n            if \"width\" in stream and \"height\" in stream:\n                first_video_stream = stream\n                break\n\n        if first_video_stream:\n            nb_frames = stream.get(\"nb_frames\")\n            if nb_frames:\n                try:\n                    return int(nb_frames)\n                except ValueError:\n                    self.log.warning(\n                        \"nb_frames {} not convertible\".format(nb_frames))\n\n                    duration = stream.get(\"duration\")\n                    frame_rate = convert_ffprobe_fps_value(\n                        stream.get(\"r_frame_rate\", '0/0')\n                    )\n                    self.log.debug(\"duration:: {} frame_rate:: {}\".format(\n                        duration, frame_rate))\n                    try:\n                        return float(duration) * float(frame_rate)\n                    except ValueError:\n                        self.log.warning(\n                            \"{} or {} cannot be converted\".format(duration,\n                                                                  frame_rate))\n\n        self.log.warning(\"Cannot get number of frames\")\n"
  },
  {
    "path": "openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py",
    "content": "\"\"\"\nRequires:\n    CollectTVPaintWorkfileData\n\nProvides:\n    Instances\n\"\"\"\nimport os\nimport re\nimport copy\nimport pyblish.api\n\nfrom openpype.pipeline.create import get_subset_name\n\n\nclass CollectTVPaintInstances(pyblish.api.ContextPlugin):\n    label = \"Collect TVPaint Instances\"\n    order = pyblish.api.CollectorOrder + 0.2\n    hosts = [\"webpublisher\"]\n    targets = [\"tvpaint_worker\"]\n\n    workfile_family = \"workfile\"\n    workfile_variant = \"\"\n    review_family = \"review\"\n    review_variant = \"Main\"\n    render_pass_family = \"renderPass\"\n    render_layer_family = \"renderLayer\"\n    render_layer_pass_name = \"beauty\"\n\n    # Set by settings\n    # Regex must contain 'layer' and 'variant' groups which are extracted from\n    #   name when instances are created\n    layer_name_regex = r\"(?P<layer>L[0-9]{3}_\\w+)_(?P<pass>.+)\"\n\n    def process(self, context):\n        # Prepare compiled regex\n        layer_name_regex = re.compile(self.layer_name_regex)\n\n        layers_data = context.data[\"layersData\"]\n\n        host_name = \"tvpaint\"\n        task_name = context.data.get(\"task\")\n        asset_doc = context.data[\"assetEntity\"]\n        project_doc = context.data[\"projectEntity\"]\n        project_name = project_doc[\"name\"]\n\n        new_instances = []\n\n        # Workfile instance\n        workfile_subset_name = get_subset_name(\n            self.workfile_family,\n            self.workfile_variant,\n            task_name,\n            asset_doc,\n            project_name,\n            host_name,\n            project_settings=context.data[\"project_settings\"]\n        )\n        workfile_instance = self._create_workfile_instance(\n            context, workfile_subset_name\n        )\n        new_instances.append(workfile_instance)\n\n        # Review instance\n        review_subset_name = get_subset_name(\n            self.review_family,\n            self.review_variant,\n            task_name,\n            asset_doc,\n            project_name,\n            host_name,\n            project_settings=context.data[\"project_settings\"]\n        )\n        review_instance = self._create_review_instance(\n            context, review_subset_name\n        )\n        new_instances.append(review_instance)\n\n        # Get render layers and passes from TVPaint layers\n        #   - it's based on regex extraction\n        layers_by_layer_and_pass = {}\n        for layer in layers_data:\n            # Filter only visible layers\n            if not layer[\"visible\"]:\n                continue\n\n            result = layer_name_regex.search(layer[\"name\"])\n            # Layer name not matching layer name regex\n            #   should raise an exception?\n            if result is None:\n                continue\n            render_layer = result.group(\"layer\")\n            render_pass = result.group(\"pass\")\n\n            render_pass_maping = layers_by_layer_and_pass.get(\n                render_layer\n            )\n            if render_pass_maping is None:\n                render_pass_maping = {}\n                layers_by_layer_and_pass[render_layer] = render_pass_maping\n\n            if render_pass not in render_pass_maping:\n                render_pass_maping[render_pass] = []\n            render_pass_maping[render_pass].append(copy.deepcopy(layer))\n\n        layers_by_render_layer = {}\n        for render_layer, render_passes in layers_by_layer_and_pass.items():\n            render_layer_layers = []\n            layers_by_render_layer[render_layer] = render_layer_layers\n            for render_pass, layers in render_passes.items():\n                render_layer_layers.extend(copy.deepcopy(layers))\n                dynamic_data = {\n                    \"render_pass\": render_pass,\n                    \"render_layer\": render_layer,\n                    # Override family for subset name\n                    \"family\": \"render\"\n                }\n\n                subset_name = get_subset_name(\n                    self.render_pass_family,\n                    render_pass,\n                    task_name,\n                    asset_doc,\n                    project_name,\n                    host_name,\n                    dynamic_data=dynamic_data,\n                    project_settings=context.data[\"project_settings\"]\n                )\n\n                instance = self._create_render_pass_instance(\n                    context, layers, subset_name\n                )\n                new_instances.append(instance)\n\n        for render_layer, layers in layers_by_render_layer.items():\n            variant = render_layer\n            dynamic_data = {\n                \"render_pass\": self.render_layer_pass_name,\n                \"render_layer\": render_layer,\n                # Override family for subset name\n                \"family\": \"render\"\n            }\n            subset_name = get_subset_name(\n                self.render_layer_family,\n                variant,\n                task_name,\n                asset_doc,\n                project_name,\n                host_name,\n                dynamic_data=dynamic_data,\n                project_settings=context.data[\"project_settings\"]\n            )\n            instance = self._create_render_layer_instance(\n                context, layers, subset_name\n            )\n            new_instances.append(instance)\n\n        # Set data same for all instances\n        frame_start = context.data.get(\"frameStart\")\n        frame_end = context.data.get(\"frameEnd\")\n\n        for instance in new_instances:\n            if (\n                instance.data.get(\"frameStart\") is None\n                or instance.data.get(\"frameEnd\") is None\n            ):\n                instance.data[\"frameStart\"] = frame_start\n                instance.data[\"frameEnd\"] = frame_end\n\n            if instance.data.get(\"asset\") is None:\n                instance.data[\"asset\"] = asset_doc[\"name\"]\n\n            if instance.data.get(\"task\") is None:\n                instance.data[\"task\"] = task_name\n\n            if \"representations\" not in instance.data:\n                instance.data[\"representations\"] = []\n\n            if \"source\" not in instance.data:\n                instance.data[\"source\"] = \"webpublisher\"\n\n    def _create_workfile_instance(self, context, subset_name):\n        workfile_path = context.data[\"workfilePath\"]\n        staging_dir = os.path.dirname(workfile_path)\n        filename = os.path.basename(workfile_path)\n        ext = os.path.splitext(filename)[-1]\n\n        return context.create_instance(**{\n            \"name\": subset_name,\n            \"label\": subset_name,\n            \"subset\": subset_name,\n            \"family\": self.workfile_family,\n            \"families\": [],\n            \"stagingDir\": staging_dir,\n            \"representations\": [{\n                \"name\": ext.lstrip(\".\"),\n                \"ext\": ext.lstrip(\".\"),\n                \"files\": filename,\n                \"stagingDir\": staging_dir\n            }]\n        })\n\n    def _create_review_instance(self, context, subset_name):\n        staging_dir = self._create_staging_dir(context, subset_name)\n        layers_data = context.data[\"layersData\"]\n        # Filter hidden layers\n        filtered_layers_data = [\n            copy.deepcopy(layer)\n            for layer in layers_data\n            if layer[\"visible\"]\n        ]\n        return context.create_instance(**{\n            \"name\": subset_name,\n            \"label\": subset_name,\n            \"subset\": subset_name,\n            \"family\": self.review_family,\n            \"families\": [],\n            \"layers\": filtered_layers_data,\n            \"stagingDir\": staging_dir\n        })\n\n    def _create_render_pass_instance(self, context, layers, subset_name):\n        staging_dir = self._create_staging_dir(context, subset_name)\n        # Global instance data modifications\n        # Fill families\n        return context.create_instance(**{\n            \"name\": subset_name,\n            \"subset\": subset_name,\n            \"label\": subset_name,\n            \"family\": \"render\",\n            # Add `review` family for thumbnail integration\n            \"families\": [self.render_pass_family, \"review\"],\n            \"representations\": [],\n            \"layers\": layers,\n            \"stagingDir\": staging_dir\n        })\n\n    def _create_render_layer_instance(self, context, layers, subset_name):\n        staging_dir = self._create_staging_dir(context, subset_name)\n        # Global instance data modifications\n        # Fill families\n        return context.create_instance(**{\n            \"name\": subset_name,\n            \"subset\": subset_name,\n            \"label\": subset_name,\n            \"family\": \"render\",\n            # Add `review` family for thumbnail integration\n            \"families\": [self.render_layer_family, \"review\"],\n            \"representations\": [],\n            \"layers\": layers,\n            \"stagingDir\": staging_dir\n        })\n\n    def _create_staging_dir(self, context, subset_name):\n        context_staging_dir = context.data[\"contextStagingDir\"]\n        staging_dir = os.path.join(context_staging_dir, subset_name)\n        if not os.path.exists(staging_dir):\n            os.makedirs(staging_dir)\n        return staging_dir\n"
  },
  {
    "path": "openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_workfile_data.py",
    "content": "\"\"\"\nRequires:\n    CollectPublishedFiles\n    CollectModules\n\nProvides:\n    workfilePath - Path to tvpaint workfile\n    sceneData - Scene data loaded from the workfile\n    groupsData -\n    layersData\n    layersExposureFrames\n    layersPrePostBehavior\n\"\"\"\nimport os\nimport uuid\nimport json\nimport shutil\nimport pyblish.api\nfrom openpype.hosts.tvpaint.worker import (\n    SenderTVPaintCommands,\n    CollectSceneData\n)\nfrom openpype_modules.webpublisher.lib import parse_json\n\n\nclass CollectTVPaintWorkfileData(pyblish.api.ContextPlugin):\n    label = \"Collect TVPaint Workfile data\"\n    order = pyblish.api.CollectorOrder - 0.4\n    hosts = [\"webpublisher\"]\n    targets = [\"tvpaint_worker\"]\n\n    def process(self, context):\n        # Get JobQueue module\n        modules = context.data[\"openPypeModules\"]\n        job_queue_module = modules[\"job_queue\"]\n        jobs_root = job_queue_module.get_jobs_root()\n        if not jobs_root:\n            raise ValueError(\"Job Queue root is not set.\")\n\n        context.data[\"jobsRoot\"] = jobs_root\n\n        context_staging_dir = self._create_context_staging_dir(jobs_root)\n        workfile_path = self._extract_workfile_path(\n            context, context_staging_dir\n        )\n        context.data[\"contextStagingDir\"] = context_staging_dir\n        context.data[\"workfilePath\"] = workfile_path\n\n        # Prepare tvpaint command\n        collect_scene_data_command = CollectSceneData()\n        # Create TVPaint sender commands\n        commands = SenderTVPaintCommands(workfile_path, job_queue_module)\n        commands.add_command(collect_scene_data_command)\n\n        # Send job and wait for answer\n        commands.send_job_and_wait()\n\n        collected_data = collect_scene_data_command.result()\n        layers_data = collected_data[\"layers_data\"]\n        groups_data = collected_data[\"groups_data\"]\n        scene_data = collected_data[\"scene_data\"]\n        exposure_frames_by_layer_id = (\n            collected_data[\"exposure_frames_by_layer_id\"]\n        )\n        pre_post_beh_by_layer_id = (\n            collected_data[\"pre_post_beh_by_layer_id\"]\n        )\n\n        # Store results\n        # scene data store the same way as TVPaint collector\n        scene_data = {\n            \"sceneWidth\": scene_data[\"width\"],\n            \"sceneHeight\": scene_data[\"height\"],\n            \"scenePixelAspect\": scene_data[\"pixel_aspect\"],\n            \"sceneFps\": scene_data[\"fps\"],\n            \"sceneFieldOrder\": scene_data[\"field_order\"],\n            \"sceneMarkIn\": scene_data[\"mark_in\"],\n            # scene_data[\"mark_in_state\"],\n            \"sceneMarkInState\": scene_data[\"mark_in_set\"],\n            \"sceneMarkOut\": scene_data[\"mark_out\"],\n            # scene_data[\"mark_out_state\"],\n            \"sceneMarkOutState\": scene_data[\"mark_out_set\"],\n            \"sceneStartFrame\": scene_data[\"start_frame\"],\n            \"sceneBgColor\": scene_data[\"bg_color\"]\n        }\n        context.data[\"sceneData\"] = scene_data\n        # Store only raw data\n        context.data[\"groupsData\"] = groups_data\n        context.data[\"layersData\"] = layers_data\n        context.data[\"layersExposureFrames\"] = exposure_frames_by_layer_id\n        context.data[\"layersPrePostBehavior\"] = pre_post_beh_by_layer_id\n\n        self.log.debug(\n            (\n                \"Collected data\"\n                \"\\nScene data: {}\"\n                \"\\nLayers data: {}\"\n                \"\\nExposure frames: {}\"\n                \"\\nPre/Post behavior: {}\"\n            ).format(\n                json.dumps(scene_data, indent=4),\n                json.dumps(layers_data, indent=4),\n                json.dumps(exposure_frames_by_layer_id, indent=4),\n                json.dumps(pre_post_beh_by_layer_id, indent=4)\n            )\n        )\n\n    def _create_context_staging_dir(self, jobs_root):\n        if not os.path.exists(jobs_root):\n            os.makedirs(jobs_root)\n\n        random_folder_name = str(uuid.uuid4())\n        full_path = os.path.join(jobs_root, random_folder_name)\n        if not os.path.exists(full_path):\n            os.makedirs(full_path)\n        return full_path\n\n    def _extract_workfile_path(self, context, context_staging_dir):\n        \"\"\"Find first TVPaint file in tasks and use it.\"\"\"\n        batch_dir = context.data[\"batchDir\"]\n        batch_data = context.data[\"batchData\"]\n        src_workfile_path = None\n        for task_id in batch_data[\"tasks\"]:\n            if src_workfile_path is not None:\n                break\n            task_dir = os.path.join(batch_dir, task_id)\n            task_manifest_path = os.path.join(task_dir, \"manifest.json\")\n            task_data = parse_json(task_manifest_path)\n            task_files = task_data[\"files\"]\n            for filename in task_files:\n                _, ext = os.path.splitext(filename)\n                if ext.lower() == \".tvpp\":\n                    src_workfile_path = os.path.join(task_dir, filename)\n                    break\n\n        # Copy workfile to job queue work root\n        new_workfile_path = os.path.join(\n            context_staging_dir, os.path.basename(src_workfile_path)\n        )\n        shutil.copy(src_workfile_path, new_workfile_path)\n\n        return new_workfile_path\n"
  },
  {
    "path": "openpype/hosts/webpublisher/plugins/publish/extract_tvpaint_workfile.py",
    "content": "import os\nimport copy\n\nfrom openpype.hosts.tvpaint.worker import (\n    SenderTVPaintCommands,\n    ExecuteSimpleGeorgeScript,\n    ExecuteGeorgeScript\n)\n\nimport pyblish.api\nfrom openpype.hosts.tvpaint.lib import (\n    calculate_layers_extraction_data,\n    get_frame_filename_template,\n    fill_reference_frames,\n    composite_rendered_layers,\n    rename_filepaths_by_frame_start\n)\nfrom PIL import Image\n\n\nclass ExtractTVPaintSequences(pyblish.api.Extractor):\n    label = \"Extract TVPaint Sequences\"\n    hosts = [\"webpublisher\"]\n    targets = [\"tvpaint_worker\"]\n\n    # Context plugin does not have families filtering\n    families_filter = [\"review\", \"renderPass\", \"renderLayer\"]\n\n    job_queue_root_key = \"jobs_root\"\n\n    # Modifiable with settings\n    review_bg = [255, 255, 255, 255]\n\n    def process(self, context):\n        # Get workfle path\n        workfile_path = context.data[\"workfilePath\"]\n        jobs_root = context.data[\"jobsRoot\"]\n        jobs_root_slashed = jobs_root.replace(\"\\\\\", \"/\")\n\n        # Prepare scene data\n        scene_data = context.data[\"sceneData\"]\n        scene_mark_in = scene_data[\"sceneMarkIn\"]\n        scene_mark_out = scene_data[\"sceneMarkOut\"]\n        scene_start_frame = scene_data[\"sceneStartFrame\"]\n        scene_bg_color = scene_data[\"sceneBgColor\"]\n\n        # Prepare layers behavior\n        behavior_by_layer_id = context.data[\"layersPrePostBehavior\"]\n        exposure_frames_by_layer_id = context.data[\"layersExposureFrames\"]\n\n        # Handles are not stored per instance but on Context\n        handle_start = context.data[\"handleStart\"]\n        handle_end = context.data[\"handleEnd\"]\n\n        # Get JobQueue module\n        modules = context.data[\"openPypeModules\"]\n        job_queue_module = modules[\"job_queue\"]\n\n        tvpaint_commands = SenderTVPaintCommands(\n            workfile_path, job_queue_module\n        )\n\n        # Change scene Start Frame to 0 to prevent frame index issues\n        #   - issue is that TVPaint versions deal with frame indexes in a\n        #     different way when Start Frame is not `0`\n        # NOTE It will be set back after rendering\n        tvpaint_commands.add_command(\n            ExecuteSimpleGeorgeScript(\"tv_startframe 0\")\n        )\n\n        root_key_replacement = \"{\" + self.job_queue_root_key + \"}\"\n        after_render_instances = []\n        for instance in context:\n            instance_families = set(instance.data.get(\"families\", []))\n            instance_families.add(instance.data[\"family\"])\n            valid = False\n            for family in instance_families:\n                if family in self.families_filter:\n                    valid = True\n                    break\n\n            if not valid:\n                continue\n\n            self.log.info(\"* Preparing commands for instance \\\"{}\\\"\".format(\n                instance.data[\"label\"]\n            ))\n            # Get all layers and filter out not visible\n            layers = instance.data[\"layers\"]\n            filtered_layers = [layer for layer in layers if layer[\"visible\"]]\n            if not filtered_layers:\n                self.log.info(\n                    \"None of the layers from the instance\"\n                    \" are visible. Extraction skipped.\"\n                )\n                continue\n\n            joined_layer_names = \", \".join([\n                \"\\\"{}\\\"\".format(str(layer[\"name\"]))\n                for layer in filtered_layers\n            ])\n            self.log.debug(\n                \"Instance has {} layers with names: {}\".format(\n                    len(filtered_layers), joined_layer_names\n                )\n            )\n\n            # Staging dir must be created during collection\n            staging_dir = instance.data[\"stagingDir\"].replace(\"\\\\\", \"/\")\n\n            job_root_template = staging_dir.replace(\n                jobs_root_slashed, root_key_replacement\n            )\n\n            # Frame start/end may be stored as float\n            frame_start = int(instance.data[\"frameStart\"])\n            frame_end = int(instance.data[\"frameEnd\"])\n\n            # Prepare output frames\n            output_frame_start = frame_start - handle_start\n            output_frame_end = frame_end + handle_end\n\n            # Change output frame start to 0 if handles cause it's negative\n            #   number\n            if output_frame_start < 0:\n                self.log.warning((\n                    \"Frame start with handles has negative value.\"\n                    \" Changed to \\\"0\\\". Frames start: {}, Handle Start: {}\"\n                ).format(frame_start, handle_start))\n                output_frame_start = 0\n\n            # Create copy of scene Mark In/Out\n            mark_in, mark_out = scene_mark_in, scene_mark_out\n\n            # Fix possible changes of output frame\n            mark_out, output_frame_end = self._fix_range_changes(\n                mark_in, mark_out, output_frame_start, output_frame_end\n            )\n            filename_template = get_frame_filename_template(\n                max(scene_mark_out, output_frame_end)\n            )\n\n            # -----------------------------------------------------------------\n            self.log.debug(\n                \"Files will be rendered to folder: {}\".format(staging_dir)\n            )\n\n            output_filepaths_by_frame_idx = {}\n            for frame_idx in range(mark_in, mark_out + 1):\n                filename = filename_template.format(frame=frame_idx)\n                filepath = os.path.join(staging_dir, filename)\n                output_filepaths_by_frame_idx[frame_idx] = filepath\n\n            # Prepare data for post render processing\n            post_render_data = {\n                \"output_dir\": staging_dir,\n                \"layers\": filtered_layers,\n                \"output_filepaths_by_frame_idx\": output_filepaths_by_frame_idx,\n                \"instance\": instance,\n                \"is_layers_render\": False,\n                \"output_frame_start\": output_frame_start,\n                \"output_frame_end\": output_frame_end\n            }\n            # Store them to list\n            after_render_instances.append(post_render_data)\n\n            # Review rendering\n            if instance.data[\"family\"] == \"review\":\n                self.add_render_review_command(\n                    tvpaint_commands, mark_in, mark_out, scene_bg_color,\n                    job_root_template, filename_template\n                )\n                continue\n\n            # Layers rendering\n            extraction_data_by_layer_id = calculate_layers_extraction_data(\n                filtered_layers,\n                exposure_frames_by_layer_id,\n                behavior_by_layer_id,\n                mark_in,\n                mark_out\n            )\n            filepaths_by_layer_id = self.add_render_command(\n                tvpaint_commands,\n                job_root_template,\n                staging_dir,\n                filtered_layers,\n                extraction_data_by_layer_id\n            )\n            # Add more data to post render processing\n            post_render_data.update({\n                \"is_layers_render\": True,\n                \"extraction_data_by_layer_id\": extraction_data_by_layer_id,\n                \"filepaths_by_layer_id\": filepaths_by_layer_id\n            })\n\n        # Change scene frame Start back to previous value\n        tvpaint_commands.add_command(\n            ExecuteSimpleGeorgeScript(\n                \"tv_startframe {}\".format(scene_start_frame)\n            )\n        )\n        self.log.info(\"Sending the job and waiting for response...\")\n        tvpaint_commands.send_job_and_wait()\n        self.log.info(\"Render job finished\")\n\n        for post_render_data in after_render_instances:\n            self._post_render_processing(post_render_data, mark_in, mark_out)\n\n    def _fix_range_changes(\n        self, mark_in, mark_out, output_frame_start, output_frame_end\n    ):\n        # Check Marks range and output range\n        output_range = output_frame_end - output_frame_start\n        marks_range = mark_out - mark_in\n\n        # Lower Mark Out if mark range is bigger than output\n        # - do not rendered not used frames\n        if output_range < marks_range:\n            new_mark_out = mark_out - (marks_range - output_range)\n            self.log.warning((\n                \"Lowering render range to {} frames. Changed Mark Out {} -> {}\"\n            ).format(marks_range + 1, mark_out, new_mark_out))\n            # Assign new mark out to variable\n            mark_out = new_mark_out\n\n        # Lower output frame end so representation has right `frameEnd` value\n        elif output_range > marks_range:\n            new_output_frame_end = (\n                output_frame_end - (output_range - marks_range)\n            )\n            self.log.warning((\n                \"Lowering representation range to {} frames.\"\n                \" Changed frame end {} -> {}\"\n            ).format(output_range + 1, mark_out, new_output_frame_end))\n            output_frame_end = new_output_frame_end\n        return mark_out, output_frame_end\n\n    def _post_render_processing(self, post_render_data, mark_in, mark_out):\n        # Unpack values\n        instance = post_render_data[\"instance\"]\n        output_filepaths_by_frame_idx = (\n            post_render_data[\"output_filepaths_by_frame_idx\"]\n        )\n        is_layers_render = post_render_data[\"is_layers_render\"]\n        output_dir = post_render_data[\"output_dir\"]\n        layers = post_render_data[\"layers\"]\n        output_frame_start = post_render_data[\"output_frame_start\"]\n        output_frame_end = post_render_data[\"output_frame_end\"]\n\n        # Trigger post processing of layers rendering\n        #   - only few frames were rendered this will complete the sequence\n        #   - multiple layers can be in single instance they must be composite\n        #       over each other\n        if is_layers_render:\n            self._finish_layer_render(\n                layers,\n                post_render_data[\"extraction_data_by_layer_id\"],\n                post_render_data[\"filepaths_by_layer_id\"],\n                mark_in,\n                mark_out,\n                output_filepaths_by_frame_idx\n            )\n\n        # Create thumbnail\n        thumbnail_filepath = os.path.join(output_dir, \"thumbnail.jpg\")\n        thumbnail_src_path = output_filepaths_by_frame_idx[mark_in]\n        self._create_thumbnail(thumbnail_src_path, thumbnail_filepath)\n\n        # Rename filepaths to final frames\n        repre_files = self._rename_output_files(\n            output_filepaths_by_frame_idx,\n            mark_in,\n            mark_out,\n            output_frame_start\n        )\n\n        # Fill tags and new families\n        family_lowered = instance.data[\"family\"].lower()\n        tags = []\n        if family_lowered in (\"review\", \"renderlayer\"):\n            tags.append(\"review\")\n\n        # Sequence of one frame\n        single_file = len(repre_files) == 1\n        if single_file:\n            repre_files = repre_files[0]\n\n        # Extension is hardcoded\n        #   - changing extension would require change code\n        new_repre = {\n            \"name\": \"png\",\n            \"ext\": \"png\",\n            \"files\": repre_files,\n            \"stagingDir\": output_dir,\n            \"tags\": tags\n        }\n\n        if not single_file:\n            new_repre[\"frameStart\"] = output_frame_start\n            new_repre[\"frameEnd\"] = output_frame_end\n\n        self.log.debug(\"Creating new representation: {}\".format(new_repre))\n\n        instance.data[\"representations\"].append(new_repre)\n\n        if family_lowered in (\"renderpass\", \"renderlayer\"):\n            # Change family to render\n            instance.data[\"family\"] = \"render\"\n\n        thumbnail_ext = os.path.splitext(thumbnail_filepath)[1]\n        # Create thumbnail representation\n        thumbnail_repre = {\n            \"name\": \"thumbnail\",\n            \"ext\": thumbnail_ext.replace(\".\", \"\"),\n            \"outputName\": \"thumb\",\n            \"files\": os.path.basename(thumbnail_filepath),\n            \"stagingDir\": output_dir,\n            \"tags\": [\"thumbnail\"]\n        }\n        instance.data[\"representations\"].append(thumbnail_repre)\n\n    def _rename_output_files(\n        self, filepaths_by_frame, mark_in, mark_out, output_frame_start\n    ):\n        new_filepaths_by_frame = rename_filepaths_by_frame_start(\n            filepaths_by_frame, mark_in, mark_out, output_frame_start\n        )\n\n        repre_filenames = []\n        for filepath in new_filepaths_by_frame.values():\n            repre_filenames.append(os.path.basename(filepath))\n\n        if mark_in < output_frame_start:\n            repre_filenames = list(reversed(repre_filenames))\n\n        return repre_filenames\n\n    def add_render_review_command(\n        self,\n        tvpaint_commands,\n        mark_in,\n        mark_out,\n        scene_bg_color,\n        job_root_template,\n        filename_template\n    ):\n        \"\"\" Export images from TVPaint using `tv_savesequence` command.\n\n        Args:\n            output_dir (str): Directory where files will be stored.\n            mark_in (int): Starting frame index from which export will begin.\n            mark_out (int): On which frame index export will end.\n            scene_bg_color (list): Bg color set in scene. Result of george\n                script command `tv_background`.\n        \"\"\"\n        self.log.debug(\"Preparing data for rendering.\")\n        bg_color = self._get_review_bg_color()\n        first_frame_filepath = \"/\".join([\n            job_root_template,\n            filename_template.format(frame=mark_in)\n        ])\n\n        george_script_lines = [\n            # Change bg color to color from settings\n            \"tv_background \\\"color\\\" {} {} {}\".format(*bg_color),\n            \"tv_SaveMode \\\"PNG\\\"\",\n            \"export_path = \\\"{}\\\"\".format(\n                first_frame_filepath.replace(\"\\\\\", \"/\")\n            ),\n            \"tv_savesequence '\\\"'export_path'\\\"' {} {}\".format(\n                mark_in, mark_out\n            )\n        ]\n        if scene_bg_color:\n            # Change bg color back to previous scene bg color\n            _scene_bg_color = copy.deepcopy(scene_bg_color)\n            bg_type = _scene_bg_color.pop(0)\n            orig_color_command = [\n                \"tv_background\",\n                \"\\\"{}\\\"\".format(bg_type)\n            ]\n            orig_color_command.extend(_scene_bg_color)\n\n            george_script_lines.append(\" \".join(orig_color_command))\n\n        tvpaint_commands.add_command(\n            ExecuteGeorgeScript(\n                george_script_lines,\n                root_dir_key=self.job_queue_root_key\n            )\n        )\n\n    def add_render_command(\n        self,\n        tvpaint_commands,\n        job_root_template,\n        staging_dir,\n        layers,\n        extraction_data_by_layer_id\n    ):\n        \"\"\" Export images from TVPaint.\n\n        Args:\n            output_dir (str): Directory where files will be stored.\n            mark_in (int): Starting frame index from which export will begin.\n            mark_out (int): On which frame index export will end.\n            layers (list): List of layers to be exported.\n\n        Returns:\n            tuple: With 2 items first is list of filenames second is path to\n                thumbnail.\n        \"\"\"\n        # Map layers by position\n        layers_by_id = {\n            layer[\"layer_id\"]: layer\n            for layer in layers\n        }\n\n        # Render layers\n        filepaths_by_layer_id = {}\n        for layer_id, render_data in extraction_data_by_layer_id.items():\n            layer = layers_by_id[layer_id]\n            frame_references = render_data[\"frame_references\"]\n            filenames_by_frame_index = render_data[\"filenames_by_frame_index\"]\n\n            filepaths_by_frame = {}\n            command_filepath_by_frame = {}\n            for frame_idx, ref_idx in frame_references.items():\n                # None reference is skipped because does not have source\n                if ref_idx is None:\n                    filepaths_by_frame[frame_idx] = None\n                    continue\n                filename = filenames_by_frame_index[frame_idx]\n\n                filepaths_by_frame[frame_idx] = os.path.join(\n                    staging_dir, filename\n                )\n                if frame_idx == ref_idx:\n                    command_filepath_by_frame[frame_idx] = \"/\".join(\n                        [job_root_template, filename]\n                    )\n\n            self._add_render_layer_command(\n                tvpaint_commands, layer, command_filepath_by_frame\n            )\n            filepaths_by_layer_id[layer_id] = filepaths_by_frame\n\n        return filepaths_by_layer_id\n\n    def _add_render_layer_command(\n        self, tvpaint_commands, layer, filepaths_by_frame\n    ):\n        george_script_lines = [\n            # Set current layer by position\n            \"tv_layergetid {}\".format(layer[\"position\"]),\n            \"layer_id = result\",\n            \"tv_layerset layer_id\",\n            \"tv_SaveMode \\\"PNG\\\"\"\n        ]\n\n        for frame_idx, filepath in filepaths_by_frame.items():\n            if filepath is None:\n                continue\n\n            # Go to frame\n            george_script_lines.append(\"tv_layerImage {}\".format(frame_idx))\n            # Store image to output\n            george_script_lines.append(\n                \"tv_saveimage \\\"{}\\\"\".format(filepath.replace(\"\\\\\", \"/\"))\n            )\n\n        tvpaint_commands.add_command(\n            ExecuteGeorgeScript(\n                george_script_lines,\n                root_dir_key=self.job_queue_root_key\n            )\n        )\n\n    def _finish_layer_render(\n        self,\n        layers,\n        extraction_data_by_layer_id,\n        filepaths_by_layer_id,\n        mark_in,\n        mark_out,\n        output_filepaths_by_frame_idx\n    ):\n        # Fill frames between `frame_start_index` and `frame_end_index`\n        self.log.debug(\"Filling frames not rendered frames.\")\n        for layer_id, render_data in extraction_data_by_layer_id.items():\n            frame_references = render_data[\"frame_references\"]\n            filepaths_by_frame = filepaths_by_layer_id[layer_id]\n            fill_reference_frames(frame_references, filepaths_by_frame)\n\n        # Prepare final filepaths where compositing should store result\n        self.log.info(\"Started compositing of layer frames.\")\n        composite_rendered_layers(\n            layers, filepaths_by_layer_id,\n            mark_in, mark_out,\n            output_filepaths_by_frame_idx\n        )\n\n    def _create_thumbnail(self, thumbnail_src_path, thumbnail_filepath):\n        if not os.path.exists(thumbnail_src_path):\n            return\n\n        source_img = Image.open(thumbnail_src_path)\n\n        # Composite background only on rgba images\n        # - just making sure\n        if source_img.mode.lower() == \"rgba\":\n            bg_color = self._get_review_bg_color()\n            self.log.debug(\"Adding thumbnail background color {}.\".format(\n                \" \".join([str(val) for val in bg_color])\n            ))\n            bg_image = Image.new(\"RGBA\", source_img.size, bg_color)\n            thumbnail_obj = Image.alpha_composite(bg_image, source_img)\n            thumbnail_obj.convert(\"RGB\").save(thumbnail_filepath)\n\n        else:\n            self.log.info((\n                \"Source for thumbnail has mode \\\"{}\\\" (Expected: RGBA).\"\n                \" Can't use thubmanail background color.\"\n            ).format(source_img.mode))\n            source_img.save(thumbnail_filepath)\n\n    def _get_review_bg_color(self):\n        red = green = blue = 255\n        if self.review_bg:\n            if len(self.review_bg) == 4:\n                red, green, blue, _ = self.review_bg\n            elif len(self.review_bg) == 3:\n                red, green, blue = self.review_bg\n        return (red, green, blue)\n"
  },
  {
    "path": "openpype/hosts/webpublisher/plugins/publish/others_cleanup_job_root.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Cleanup leftover files from publish.\"\"\"\nimport os\nimport shutil\nimport pyblish.api\n\n\nclass CleanUpJobRoot(pyblish.api.ContextPlugin):\n    \"\"\"Cleans up the job root directory after a successful publish.\n\n    Remove all files in job root as all of them should be published.\n    \"\"\"\n\n    order = pyblish.api.IntegratorOrder + 1\n    label = \"Clean Up Job Root\"\n    optional = True\n    active = True\n\n    def process(self, context):\n        context_staging_dir = context.data.get(\"contextStagingDir\")\n        if not context_staging_dir:\n            self.log.info(\"Key 'contextStagingDir' is empty.\")\n\n        elif not os.path.exists(context_staging_dir):\n            self.log.info((\n                \"Job root directory for this publish does not\"\n                \" exists anymore \\\"{}\\\".\"\n            ).format(context_staging_dir))\n        else:\n            self.log.info(\"Deleting job root with all files.\")\n            shutil.rmtree(context_staging_dir)\n"
  },
  {
    "path": "openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py",
    "content": "import pyblish.api\n\n\nclass ValidateWorkfileData(pyblish.api.ContextPlugin):\n    \"\"\"Validate mark in and out are enabled and it's duration.\n\n    Mark In/Out does not have to match frameStart and frameEnd but duration is\n    important.\n    \"\"\"\n\n    label = \"Validate Workfile Data\"\n    order = pyblish.api.ValidatorOrder\n    targets = [\"tvpaint_worker\"]\n\n    def process(self, context):\n        # Data collected in `CollectContextEntities`\n        frame_start = context.data[\"frameStart\"]\n        frame_end = context.data[\"frameEnd\"]\n        handle_start = context.data[\"handleStart\"]\n        handle_end = context.data[\"handleEnd\"]\n\n        scene_data = context.data[\"sceneData\"]\n        scene_mark_in = scene_data[\"sceneMarkIn\"]\n        scene_mark_out = scene_data[\"sceneMarkOut\"]\n\n        expected_range = (\n            (frame_end - frame_start + 1)\n            + handle_start\n            + handle_end\n        )\n        marks_range = scene_mark_out - scene_mark_in + 1\n        if expected_range != marks_range:\n            raise AssertionError((\n                \"Wrong Mark In/Out range.\"\n                \" Expected range is {} frames got {} frames\"\n            ).format(expected_range, marks_range))\n"
  },
  {
    "path": "openpype/hosts/webpublisher/publish_functions.py",
    "content": "import os\nimport time\nimport pyblish.api\nimport pyblish.util\n\nfrom openpype.lib import Logger\nfrom openpype.lib.applications import (\n    ApplicationManager,\n    LaunchTypes,\n)\nfrom openpype.pipeline import install_host\nfrom openpype.hosts.webpublisher.api import WebpublisherHost\n\nfrom .lib import (\n    get_batch_asset_task_info,\n    get_webpublish_conn,\n    start_webpublish_log,\n    publish_and_log,\n    fail_batch,\n    find_variant_key,\n    get_task_data,\n    get_timeout,\n    IN_PROGRESS_STATUS\n)\n\n\ndef cli_publish(project_name, batch_path, user_email, targets):\n    \"\"\"Start headless publishing.\n\n    Used to publish rendered assets, workfiles etc via Webpublisher.\n    Eventually should be yanked out to Webpublisher cli.\n\n    Publish use json from passed paths argument.\n\n    Args:\n        project_name (str): project to publish (only single context is\n            expected per call of 'publish')\n        batch_path (str): Path batch folder. Contains subfolders with\n            resources (workfile, another subfolder 'renders' etc.)\n        user_email (string): email address for webpublisher - used to\n            find Ftrack user with same email\n        targets (list): Pyblish targets\n            (to choose validator for example)\n\n    Raises:\n        RuntimeError: When there is no path to process.\n    \"\"\"\n\n    if not batch_path:\n        raise RuntimeError(\"No publish paths specified\")\n\n    log = Logger.get_logger(\"Webpublish\")\n    log.info(\"Webpublish command\")\n\n    # Register target and host\n    webpublisher_host = WebpublisherHost()\n\n    os.environ[\"OPENPYPE_PUBLISH_DATA\"] = batch_path\n    os.environ[\"AVALON_PROJECT\"] = project_name\n    os.environ[\"AVALON_APP\"] = webpublisher_host.name\n    os.environ[\"USER_EMAIL\"] = user_email\n    os.environ[\"HEADLESS_PUBLISH\"] = 'true'  # to use in app lib\n\n    if targets:\n        if isinstance(targets, str):\n            targets = [targets]\n        for target in targets:\n            pyblish.api.register_target(target)\n\n    install_host(webpublisher_host)\n\n    log.info(\"Running publish ...\")\n\n    _, batch_id = os.path.split(batch_path)\n    dbcon = get_webpublish_conn()\n    _id = start_webpublish_log(dbcon, batch_id, user_email)\n\n    task_data = get_task_data(batch_path)\n    if not task_data[\"context\"]:\n        msg = \"Batch manifest must contain context data\"\n        msg += \"Create new batch and set context properly.\"\n        fail_batch(_id, dbcon, msg)\n\n    publish_and_log(dbcon, _id, log, batch_id=batch_id)\n\n    log.info(\"Publish finished.\")\n\n\ndef cli_publish_from_app(\n    project_name, batch_path, host_name, user_email, targets\n):\n    \"\"\"Opens installed variant of 'host' and run remote publish there.\n\n    Eventually should be yanked out to Webpublisher cli.\n\n    Currently implemented and tested for Photoshop where customer\n    wants to process uploaded .psd file and publish collected layers\n    from there. Triggered by Webpublisher.\n\n    Checks if no other batches are running (status =='in_progress). If\n    so, it sleeps for SLEEP (this is separate process),\n    waits for WAIT_FOR seconds altogether.\n\n    Requires installed host application on the machine.\n\n    Runs publish process as user would, in automatic fashion.\n\n    Args:\n        project_name (str): project to publish (only single context is\n            expected per call of publish\n        batch_path (str): Path batch folder. Contains subfolders with\n            resources (workfile, another subfolder 'renders' etc.)\n        host_name (str): 'photoshop'\n        user_email (string): email address for webpublisher - used to\n            find Ftrack user with same email\n        targets (list): Pyblish targets\n            (to choose validator for example)\n    \"\"\"\n\n    log = Logger.get_logger(\"PublishFromApp\")\n\n    log.info(\"Webpublish photoshop command\")\n\n    task_data = get_task_data(batch_path)\n\n    workfile_path = os.path.join(batch_path,\n                                 task_data[\"task\"],\n                                 task_data[\"files\"][0])\n\n    print(\"workfile_path {}\".format(workfile_path))\n\n    batch_id = task_data[\"batch\"]\n    dbcon = get_webpublish_conn()\n    # safer to start logging here, launch might be broken altogether\n    _id = start_webpublish_log(dbcon, batch_id, user_email)\n\n    batches_in_progress = list(dbcon.find({\"status\": IN_PROGRESS_STATUS}))\n    if len(batches_in_progress) > 1:\n        running_batches = [str(batch[\"_id\"])\n                           for batch in batches_in_progress\n                           if batch[\"_id\"] != _id]\n        msg = \"There are still running batches {}\\n\". \\\n            format(\"\\n\".join(running_batches))\n        msg += \"Ask admin to check them and reprocess current batch\"\n        fail_batch(_id, dbcon, msg)\n\n    if not task_data[\"context\"]:\n        msg = \"Batch manifest must contain context data\"\n        msg += \"Create new batch and set context properly.\"\n        fail_batch(_id, dbcon, msg)\n\n    asset_name, task_name, task_type = get_batch_asset_task_info(\n        task_data[\"context\"])\n\n    application_manager = ApplicationManager()\n    found_variant_key = find_variant_key(application_manager, host_name)\n    app_name = \"{}/{}\".format(host_name, found_variant_key)\n\n    data = {\n        \"last_workfile_path\": workfile_path,\n        \"start_last_workfile\": True,\n        \"project_name\": project_name,\n        \"asset_name\": asset_name,\n        \"task_name\": task_name,\n        \"launch_type\": LaunchTypes.automated,\n    }\n    launch_context = application_manager.create_launch_context(\n        app_name, **data)\n    launch_context.run_prelaunch_hooks()\n\n    # must have for proper launch of app\n    env = launch_context.env\n    print(\"env:: {}\".format(env))\n    env[\"OPENPYPE_PUBLISH_DATA\"] = batch_path\n    # must pass identifier to update log lines for a batch\n    env[\"BATCH_LOG_ID\"] = str(_id)\n    env[\"HEADLESS_PUBLISH\"] = 'true'  # to use in app lib\n    env[\"USER_EMAIL\"] = user_email\n\n    os.environ.update(env)\n\n    # Why is this here? Registered host in this process does not affect\n    #   regitered host in launched process.\n    pyblish.api.register_host(host_name)\n    if targets:\n        if isinstance(targets, str):\n            targets = [targets]\n        current_targets = os.environ.get(\"PYBLISH_TARGETS\", \"\").split(\n            os.pathsep)\n        for target in targets:\n            current_targets.append(target)\n\n        os.environ[\"PYBLISH_TARGETS\"] = os.pathsep.join(\n            set(current_targets))\n\n    launched_app = application_manager.launch_with_context(launch_context)\n\n    timeout = get_timeout(project_name, host_name, task_type)\n\n    time_start = time.time()\n    while launched_app.poll() is None:\n        time.sleep(0.5)\n        if time.time() - time_start > timeout:\n            launched_app.terminate()\n            msg = \"Timeout reached\"\n            fail_batch(_id, dbcon, msg)\n"
  },
  {
    "path": "openpype/hosts/webpublisher/webserver_service/__init__.py",
    "content": "from .webserver import run_webserver\n\n\n__all__ = (\n    \"run_webserver\",\n)\n"
  },
  {
    "path": "openpype/hosts/webpublisher/webserver_service/webpublish_routes.py",
    "content": "\"\"\"Routes and etc. for webpublisher API.\"\"\"\nimport os\nimport json\nimport datetime\nimport collections\nimport subprocess\nfrom bson.objectid import ObjectId\nfrom aiohttp.web_response import Response\n\nfrom openpype.client import (\n    get_projects,\n    get_assets,\n)\nfrom openpype.lib import Logger\nfrom openpype.settings import get_project_settings\nfrom openpype_modules.webserver.base_routes import RestApiEndpoint\nfrom openpype_modules.webpublisher import WebpublisherAddon\nfrom openpype_modules.webpublisher.lib import (\n    get_webpublish_conn,\n    get_task_data,\n    ERROR_STATUS,\n    REPROCESS_STATUS\n)\n\nlog = Logger.get_logger(\"WebpublishRoutes\")\n\n\nclass ResourceRestApiEndpoint(RestApiEndpoint):\n    def __init__(self, resource):\n        self.resource = resource\n        super(ResourceRestApiEndpoint, self).__init__()\n\n\nclass WebpublishApiEndpoint(ResourceRestApiEndpoint):\n    @property\n    def dbcon(self):\n        return self.resource.dbcon\n\n\nclass JsonApiResource:\n    \"\"\"Resource for json manipulation.\n\n    All resources handling sending output to REST should inherit from\n    \"\"\"\n    @staticmethod\n    def json_dump_handler(value):\n        if isinstance(value, datetime.datetime):\n            return value.isoformat()\n        if isinstance(value, ObjectId):\n            return str(value)\n        if isinstance(value, set):\n            return list(value)\n        raise TypeError(value)\n\n    @classmethod\n    def encode(cls, data):\n        return json.dumps(\n            data,\n            indent=4,\n            default=cls.json_dump_handler\n        ).encode(\"utf-8\")\n\n\nclass RestApiResource(JsonApiResource):\n    \"\"\"Resource carrying needed info and Avalon DB connection for publish.\"\"\"\n    def __init__(self, server_manager, executable, upload_dir,\n                 studio_task_queue=None):\n        self.server_manager = server_manager\n        self.upload_dir = upload_dir\n        self.executable = executable\n\n        if studio_task_queue is None:\n            studio_task_queue = collections.deque().dequeu\n        self.studio_task_queue = studio_task_queue\n\n\nclass WebpublishRestApiResource(JsonApiResource):\n    \"\"\"Resource carrying OP DB connection for storing batch info into DB.\"\"\"\n\n    def __init__(self):\n        self.dbcon = get_webpublish_conn()\n\n\nclass ProjectsEndpoint(ResourceRestApiEndpoint):\n    \"\"\"Returns list of dict with project info (id, name).\"\"\"\n    async def get(self) -> Response:\n        output = []\n        for project_doc in get_projects():\n            ret_val = {\n                \"id\": project_doc[\"_id\"],\n                \"name\": project_doc[\"name\"]\n            }\n            output.append(ret_val)\n        return Response(\n            status=200,\n            body=self.resource.encode(output),\n            content_type=\"application/json\"\n        )\n\n\nclass HiearchyEndpoint(ResourceRestApiEndpoint):\n    \"\"\"Returns dictionary with context tree from assets.\"\"\"\n    async def get(self, project_name) -> Response:\n        query_projection = {\n            \"_id\": 1,\n            \"data.tasks\": 1,\n            \"data.visualParent\": 1,\n            \"data.entityType\": 1,\n            \"name\": 1,\n            \"type\": 1,\n        }\n\n        asset_docs = get_assets(project_name, fields=query_projection.keys())\n        asset_docs_by_id = {\n            asset_doc[\"_id\"]: asset_doc\n            for asset_doc in asset_docs\n        }\n\n        asset_docs_by_parent_id = collections.defaultdict(list)\n        for asset_doc in asset_docs_by_id.values():\n            parent_id = asset_doc[\"data\"].get(\"visualParent\")\n            asset_docs_by_parent_id[parent_id].append(asset_doc)\n\n        assets = collections.defaultdict(list)\n\n        for parent_id, children in asset_docs_by_parent_id.items():\n            for child in children:\n                node = assets.get(child[\"_id\"])\n                if not node:\n                    node = Node(child[\"_id\"],\n                                child[\"data\"].get(\"entityType\", \"Folder\"),\n                                child[\"name\"])\n                    assets[child[\"_id\"]] = node\n\n                    tasks = child[\"data\"].get(\"tasks\", {})\n                    for t_name, t_con in tasks.items():\n                        task_node = TaskNode(\"task\", t_name)\n                        task_node[\"attributes\"][\"type\"] = t_con.get(\"type\")\n\n                        task_node.parent = node\n\n                parent_node = assets.get(parent_id)\n                if not parent_node:\n                    asset_doc = asset_docs_by_id.get(parent_id)\n                    if asset_doc:  # regular node\n                        parent_node = Node(parent_id,\n                                           asset_doc[\"data\"].get(\"entityType\",\n                                                                 \"Folder\"),\n                                           asset_doc[\"name\"])\n                    else:  # root\n                        parent_node = Node(parent_id,\n                                           \"project\",\n                                           project_name)\n                    assets[parent_id] = parent_node\n                node.parent = parent_node\n\n        roots = [x for x in assets.values() if x.parent is None]\n\n        return Response(\n            status=200,\n            body=self.resource.encode(roots[0]),\n            content_type=\"application/json\"\n        )\n\n\nclass Node(dict):\n    \"\"\"Node element in context tree.\"\"\"\n\n    def __init__(self, uid, node_type, name):\n        self._parent = None  # pointer to parent Node\n        self[\"type\"] = node_type\n        self[\"name\"] = name\n        self['id'] = uid  # keep reference to id #\n        self['children'] = []  # collection of pointers to child Nodes\n\n    @property\n    def parent(self):\n        return self._parent  # simply return the object at the _parent pointer\n\n    @parent.setter\n    def parent(self, node):\n        self._parent = node\n        # add this node to parent's list of children\n        node['children'].append(self)\n\n\nclass TaskNode(Node):\n    \"\"\"Special node type only for Tasks.\"\"\"\n\n    def __init__(self, node_type, name):\n        self._parent = None\n        self[\"type\"] = node_type\n        self[\"name\"] = name\n        self[\"attributes\"] = {}\n\n\nclass BatchPublishEndpoint(WebpublishApiEndpoint):\n    \"\"\"Triggers headless publishing of batch.\"\"\"\n    async def post(self, request) -> Response:\n        # Validate existence of openpype executable\n        openpype_app = self.resource.executable\n        if not openpype_app or not os.path.exists(openpype_app):\n            msg = \"Non existent OpenPype executable {}\".format(openpype_app)\n            raise RuntimeError(msg)\n\n        log.info(\"BatchPublishEndpoint called\")\n        content = await request.json()\n\n        # Each filter have extensions which are checked on first task item\n        #   - first filter with extensions that are on first task is used\n        #   - filter defines command and can extend arguments dictionary\n        # This is used only if 'studio_processing' is enabled on batch\n        studio_processing_filters = [\n            # TVPaint filter\n            {\n                \"extensions\": [\".tvpp\"],\n                \"command\": \"publish\",\n                \"arguments\": {\n                    \"targets\": [\"tvpaint_worker\", \"webpublish\"]\n                },\n                \"add_to_queue\": False\n            },\n            # Photoshop filter\n            {\n                \"extensions\": [\".psd\", \".psb\"],\n                \"command\": \"publishfromapp\",\n                \"arguments\": {\n                    # Command 'publishfromapp' requires --host argument\n                    \"host\": \"photoshop\",\n                    # Make sure targets are set to None for cases that default\n                    #   would change\n                    # - targets argument is not used in 'publishfromapp'\n                    \"targets\": [\"automated\", \"webpublish\"]\n                },\n                # does publish need to be handled by a queue, eg. only\n                # single process running concurrently?\n                \"add_to_queue\": True\n            }\n        ]\n\n        batch_dir = os.path.join(self.resource.upload_dir, content[\"batch\"])\n\n        # Default command and arguments\n        command = \"publish\"\n        add_args = {\n            # All commands need 'project' and 'user'\n            \"project\": content[\"project_name\"],\n            \"user\": content[\"user\"],\n\n            \"targets\": [\"filespublish\", \"webpublish\"]\n        }\n\n        add_to_queue = False\n        if content.get(\"studio_processing\"):\n            log.info(\"Post processing called for {}\".format(batch_dir))\n\n            task_data = get_task_data(batch_dir)\n\n            for process_filter in studio_processing_filters:\n                filter_extensions = process_filter.get(\"extensions\") or []\n                for file_name in task_data[\"files\"]:\n                    file_ext = os.path.splitext(file_name)[-1].lower()\n                    if file_ext in filter_extensions:\n                        # Change command\n                        command = process_filter[\"command\"]\n                        # Update arguments\n                        add_args.update(\n                            process_filter.get(\"arguments\") or {}\n                        )\n                        add_to_queue = process_filter[\"add_to_queue\"]\n                        break\n\n        args = [\n            openpype_app,\n            \"module\",\n            WebpublisherAddon.name,\n            command,\n            batch_dir\n        ]\n\n        for key, value in add_args.items():\n            # Skip key values where value is None\n            if value is None:\n                continue\n            arg_key = \"--{}\".format(key)\n            if not isinstance(value, (tuple, list)):\n                value = [value]\n\n            for item in value:\n                args += [arg_key, item]\n\n        log.info(\"args:: {}\".format(args))\n        if add_to_queue:\n            log.debug(\"Adding to queue\")\n            self.resource.studio_task_queue.append(args)\n        else:\n            subprocess.Popen(args)\n\n        return Response(\n            status=200,\n            content_type=\"application/json\"\n        )\n\n\nclass TaskPublishEndpoint(WebpublishApiEndpoint):\n    \"\"\"Prepared endpoint triggered after each task - for future development.\"\"\"\n    async def post(self, request) -> Response:\n        return Response(\n            status=200,\n            body=self.resource.encode([]),\n            content_type=\"application/json\"\n        )\n\n\nclass BatchStatusEndpoint(WebpublishApiEndpoint):\n    \"\"\"Returns dict with info for batch_id.\n\n    Uses 'WebpublishRestApiResource'.\n    \"\"\"\n\n    async def get(self, batch_id) -> Response:\n        output = self.dbcon.find_one({\"batch_id\": batch_id})\n\n        if output:\n            status = 200\n        else:\n            output = {\"msg\": \"Batch id {} not found\".format(batch_id),\n                      \"status\": \"queued\",\n                      \"progress\": 0}\n            status = 404\n        body = self.resource.encode(output)\n        return Response(\n            status=status,\n            body=body,\n            content_type=\"application/json\"\n        )\n\n\nclass UserReportEndpoint(WebpublishApiEndpoint):\n    \"\"\"Returns list of dict with batch info for user (email address).\n\n    Uses 'WebpublishRestApiResource'.\n    \"\"\"\n\n    async def get(self, user) -> Response:\n        output = list(self.dbcon.find({\"user\": user},\n                                      projection={\"log\": False}))\n\n        if output:\n            status = 200\n        else:\n            output = {\"msg\": \"User {} not found\".format(user)}\n            status = 404\n        body = self.resource.encode(output)\n\n        return Response(\n            status=status,\n            body=body,\n            content_type=\"application/json\"\n        )\n\n\nclass ConfiguredExtensionsEndpoint(WebpublishApiEndpoint):\n    \"\"\"Returns dict of extensions which have mapping to family.\n\n        Returns:\n        {\n            \"file_exts\": [],\n            \"sequence_exts\": []\n        }\n    \"\"\"\n    async def get(self, project_name=None) -> Response:\n        sett = get_project_settings(project_name)\n\n        configured = {\n            \"file_exts\": set(),\n            \"sequence_exts\": set(),\n            # workfiles that could have \"Studio Processing\" hardcoded for now\n            \"studio_exts\": set([\"psd\", \"psb\", \"tvpp\", \"tvp\"])\n        }\n        collect_conf = sett[\"webpublisher\"][\"publish\"][\"CollectPublishedFiles\"]\n        configs = collect_conf.get(\"task_type_to_family\", [])\n        mappings = []\n        for _, conf_mappings in configs.items():\n            if isinstance(conf_mappings, dict):\n                conf_mappings = conf_mappings.values()\n            for conf_mapping in conf_mappings:\n                mappings.append(conf_mapping)\n\n        for mapping in mappings:\n            if mapping[\"is_sequence\"]:\n                configured[\"sequence_exts\"].update(mapping[\"extensions\"])\n            else:\n                configured[\"file_exts\"].update(mapping[\"extensions\"])\n\n        return Response(\n            status=200,\n            body=self.resource.encode(dict(configured)),\n            content_type=\"application/json\"\n        )\n\n\nclass BatchReprocessEndpoint(WebpublishApiEndpoint):\n    \"\"\"Marks latest 'batch_id' for reprocessing, returns 404 if not found.\n\n    Uses 'WebpublishRestApiResource'.\n    \"\"\"\n\n    async def post(self, batch_id) -> Response:\n        batches = self.dbcon.find({\"batch_id\": batch_id,\n                                   \"status\": ERROR_STATUS}).sort(\"_id\", -1)\n\n        if batches:\n            self.dbcon.update_one(\n                {\"_id\": batches[0][\"_id\"]},\n                {\"$set\": {\"status\": REPROCESS_STATUS}}\n            )\n            output = [{\"msg\": \"Batch id {} set to reprocess\".format(batch_id)}]\n            status = 200\n        else:\n            output = [{\"msg\": \"Batch id {} not found\".format(batch_id)}]\n            status = 404\n        body = self.resource.encode(output)\n\n        return Response(\n            status=status,\n            body=body,\n            content_type=\"application/json\"\n        )\n"
  },
  {
    "path": "openpype/hosts/webpublisher/webserver_service/webserver.py",
    "content": "import collections\nimport time\nimport os\nfrom datetime import datetime\nimport requests\nimport json\nimport subprocess\n\nfrom openpype.client import OpenPypeMongoConnection\nfrom openpype.modules import ModulesManager\nfrom openpype.lib import Logger\n\nfrom openpype_modules.webpublisher.lib import (\n    ERROR_STATUS,\n    REPROCESS_STATUS,\n    SENT_REPROCESSING_STATUS\n)\n\nfrom .webpublish_routes import (\n    RestApiResource,\n    WebpublishRestApiResource,\n    HiearchyEndpoint,\n    ProjectsEndpoint,\n    ConfiguredExtensionsEndpoint,\n    BatchPublishEndpoint,\n    BatchReprocessEndpoint,\n    BatchStatusEndpoint,\n    TaskPublishEndpoint,\n    UserReportEndpoint\n)\n\nlog = Logger.get_logger(\"webserver_gui\")\n\n\ndef run_webserver(executable, upload_dir, host=None, port=None):\n    \"\"\"Runs webserver in command line, adds routes.\"\"\"\n\n    if not host:\n        host = \"localhost\"\n    if not port:\n        port = 8079\n\n    manager = ModulesManager()\n    webserver_module = manager.modules_by_name[\"webserver\"]\n\n    server_manager = webserver_module.create_new_server_manager(port, host)\n    webserver_url = server_manager.url\n    # queue for publishfromapp tasks\n    studio_task_queue = collections.deque()\n\n    resource = RestApiResource(server_manager,\n                               upload_dir=upload_dir,\n                               executable=executable,\n                               studio_task_queue=studio_task_queue)\n    projects_endpoint = ProjectsEndpoint(resource)\n    server_manager.add_route(\n        \"GET\",\n        \"/api/projects\",\n        projects_endpoint.dispatch\n    )\n\n    hiearchy_endpoint = HiearchyEndpoint(resource)\n    server_manager.add_route(\n        \"GET\",\n        \"/api/hierarchy/{project_name}\",\n        hiearchy_endpoint.dispatch\n    )\n\n    configured_ext_endpoint = ConfiguredExtensionsEndpoint(resource)\n    server_manager.add_route(\n        \"GET\",\n        \"/api/webpublish/configured_ext/{project_name}\",\n        configured_ext_endpoint.dispatch\n    )\n\n    # triggers publish\n    webpublisher_task_publish_endpoint = BatchPublishEndpoint(resource)\n    server_manager.add_route(\n        \"POST\",\n        \"/api/webpublish/batch\",\n        webpublisher_task_publish_endpoint.dispatch\n    )\n\n    webpublisher_batch_publish_endpoint = TaskPublishEndpoint(resource)\n    server_manager.add_route(\n        \"POST\",\n        \"/api/webpublish/task\",\n        webpublisher_batch_publish_endpoint.dispatch\n    )\n\n    # reporting\n    webpublish_resource = WebpublishRestApiResource()\n    batch_status_endpoint = BatchStatusEndpoint(webpublish_resource)\n    server_manager.add_route(\n        \"GET\",\n        \"/api/batch_status/{batch_id}\",\n        batch_status_endpoint.dispatch\n    )\n\n    user_status_endpoint = UserReportEndpoint(webpublish_resource)\n    server_manager.add_route(\n        \"GET\",\n        \"/api/publishes/{user}\",\n        user_status_endpoint.dispatch\n    )\n\n    batch_reprocess_endpoint = BatchReprocessEndpoint(webpublish_resource)\n    server_manager.add_route(\n        \"POST\",\n        \"/api/webpublish/reprocess/{batch_id}\",\n        batch_reprocess_endpoint.dispatch\n    )\n\n    server_manager.start_server()\n    last_reprocessed = time.time()\n    while True:\n        if time.time() - last_reprocessed > 20:\n            reprocess_failed(upload_dir, webserver_url)\n            last_reprocessed = time.time()\n        if studio_task_queue:\n            args = studio_task_queue.popleft()\n            subprocess.call(args)  # blocking call\n\n        time.sleep(1.0)\n\n\ndef reprocess_failed(upload_dir, webserver_url):\n    # log.info(\"check_reprocesable_records\")\n    mongo_client = OpenPypeMongoConnection.get_mongo_client()\n    database_name = os.environ[\"OPENPYPE_DATABASE_NAME\"]\n    dbcon = mongo_client[database_name][\"webpublishes\"]\n\n    results = dbcon.find({\"status\": REPROCESS_STATUS})\n    reprocessed_batches = set()\n    for batch in results:\n        if batch[\"batch_id\"] in reprocessed_batches:\n            continue\n\n        batch_url = os.path.join(upload_dir,\n                                 batch[\"batch_id\"],\n                                 \"manifest.json\")\n        log.info(\"batch:: {} {}\".format(os.path.exists(batch_url), batch_url))\n        if not os.path.exists(batch_url):\n            msg = \"Manifest {} not found\".format(batch_url)\n            print(msg)\n            dbcon.update_one(\n                {\"_id\": batch[\"_id\"]},\n                {\"$set\":\n                    {\n                        \"finish_date\": datetime.now(),\n                        \"status\": ERROR_STATUS,\n                        \"progress\": 100,\n                        \"log\": batch.get(\"log\") + msg\n                    }}\n            )\n            continue\n        server_url = \"{}/api/webpublish/batch\".format(webserver_url)\n\n        with open(batch_url) as f:\n            data = json.loads(f.read())\n\n        dbcon.update_many(\n            {\n                \"batch_id\": batch[\"batch_id\"],\n                \"status\": {\"$in\": [ERROR_STATUS, REPROCESS_STATUS]}\n            },\n            {\n                \"$set\": {\n                    \"finish_date\": datetime.now(),\n                    \"status\": SENT_REPROCESSING_STATUS,\n                    \"progress\": 100\n                }\n            }\n        )\n\n        try:\n            r = requests.post(server_url, json=data)\n            log.info(\"response{}\".format(r))\n        except Exception:\n            log.info(\"exception\", exc_info=True)\n\n        reprocessed_batches.add(batch[\"batch_id\"])\n"
  },
  {
    "path": "openpype/lib/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n# flake8: noqa E402\n\"\"\"OpenPype lib functions.\"\"\"\n# add vendor to sys path based on Python version\nimport sys\nimport os\nimport site\nfrom openpype import PACKAGE_DIR\n\n# Add Python version specific vendor folder\npython_version_dir = os.path.join(\n    PACKAGE_DIR, \"vendor\", \"python\", \"python_{}\".format(sys.version[0])\n)\n# Prepend path in sys paths\nsys.path.insert(0, python_version_dir)\nsite.addsitedir(python_version_dir)\n\n\nfrom .events import (\n    emit_event,\n    register_event_callback\n)\n\nfrom .vendor_bin_utils import (\n    ToolNotFoundError,\n    find_executable,\n    get_vendor_bin_path,\n    get_oiio_tools_path,\n    get_oiio_tool_args,\n    get_ffmpeg_tool_path,\n    get_ffmpeg_tool_args,\n    is_oiio_supported,\n)\n\nfrom .attribute_definitions import (\n    AbstractAttrDef,\n\n    UIDef,\n    UISeparatorDef,\n    UILabelDef,\n\n    UnknownDef,\n    NumberDef,\n    TextDef,\n    EnumDef,\n    BoolDef,\n    FileDef,\n    FileDefItem,\n)\n\nfrom .env_tools import (\n    env_value_to_bool,\n    get_paths_from_environ,\n)\n\nfrom .terminal import Terminal\nfrom .execute import (\n    get_ayon_launcher_args,\n    get_openpype_execute_args,\n    get_linux_launcher_args,\n    execute,\n    run_subprocess,\n    run_detached_process,\n    run_ayon_launcher_process,\n    run_openpype_process,\n    clean_envs_for_openpype_process,\n    path_to_subprocess_arg,\n    CREATE_NO_WINDOW\n)\nfrom .log import (\n    Logger,\n)\n\nfrom .path_templates import (\n    merge_dict,\n    TemplateMissingKey,\n    TemplateUnsolved,\n    StringTemplate,\n    TemplatesDict,\n    FormatObject,\n)\n\nfrom .dateutils import (\n    get_datetime_data,\n    get_timestamp,\n    get_formatted_current_time\n)\n\nfrom .python_module_tools import (\n    import_filepath,\n    modules_from_path,\n    recursive_bases_from_class,\n    classes_from_module,\n    import_module_from_dirpath,\n    is_func_signature_supported,\n)\n\nfrom .profiles_filtering import (\n    compile_list_of_regexes,\n    filter_profiles\n)\n\nfrom .transcoding import (\n    get_transcode_temp_directory,\n    should_convert_for_ffmpeg,\n    convert_for_ffmpeg,\n    convert_input_paths_for_ffmpeg,\n    get_ffprobe_data,\n    get_ffprobe_streams,\n    get_ffmpeg_codec_args,\n    get_ffmpeg_format_args,\n    convert_ffprobe_fps_value,\n    convert_ffprobe_fps_to_float,\n    get_rescaled_command_arguments,\n)\n\nfrom .local_settings import (\n    IniSettingRegistry,\n    JSONSettingRegistry,\n    OpenPypeSecureRegistry,\n    OpenPypeSettingsRegistry,\n    get_local_site_id,\n    change_openpype_mongo_url,\n    get_openpype_username,\n    is_admin_password_required\n)\n\nfrom .applications import (\n    ApplicationLaunchFailed,\n    ApplictionExecutableNotFound,\n    ApplicationNotFound,\n    ApplicationManager,\n\n    PreLaunchHook,\n    PostLaunchHook,\n\n    EnvironmentPrepData,\n    prepare_app_environments,\n    prepare_context_environments,\n    get_app_environments_for_context,\n    apply_project_environments_value\n)\n\nfrom .plugin_tools import (\n    prepare_template_data,\n    source_hash,\n)\n\nfrom .path_tools import (\n    format_file_size,\n    collect_frames,\n    create_hard_link,\n    version_up,\n    get_version_from_path,\n    get_last_version_from_path,\n)\n\nfrom .openpype_version import (\n    op_version_control_available,\n    get_openpype_version,\n    get_build_version,\n    get_expected_version,\n    is_running_from_build,\n    is_running_staging,\n    is_current_version_studio_latest,\n    is_current_version_higher_than_expected\n)\n\n\nfrom .connections import (\n    requests_get,\n    requests_post\n)\n\nterminal = Terminal\n\n__all__ = [\n    \"emit_event\",\n    \"register_event_callback\",\n\n    \"get_ayon_launcher_args\",\n    \"get_openpype_execute_args\",\n    \"get_linux_launcher_args\",\n    \"execute\",\n    \"run_subprocess\",\n    \"run_detached_process\",\n    \"run_ayon_launcher_process\",\n    \"run_openpype_process\",\n    \"clean_envs_for_openpype_process\",\n    \"path_to_subprocess_arg\",\n    \"CREATE_NO_WINDOW\",\n\n    \"env_value_to_bool\",\n    \"get_paths_from_environ\",\n\n    \"ToolNotFoundError\",\n    \"find_executable\",\n    \"get_vendor_bin_path\",\n    \"get_oiio_tools_path\",\n    \"get_oiio_tool_args\",\n    \"get_ffmpeg_tool_path\",\n    \"get_ffmpeg_tool_args\",\n    \"is_oiio_supported\",\n\n    \"AbstractAttrDef\",\n\n    \"UIDef\",\n    \"UISeparatorDef\",\n    \"UILabelDef\",\n\n    \"UnknownDef\",\n    \"NumberDef\",\n    \"TextDef\",\n    \"EnumDef\",\n    \"BoolDef\",\n    \"FileDef\",\n    \"FileDefItem\",\n\n    \"import_filepath\",\n    \"modules_from_path\",\n    \"recursive_bases_from_class\",\n    \"classes_from_module\",\n    \"import_module_from_dirpath\",\n    \"is_func_signature_supported\",\n\n    \"get_transcode_temp_directory\",\n    \"should_convert_for_ffmpeg\",\n    \"convert_for_ffmpeg\",\n    \"convert_input_paths_for_ffmpeg\",\n    \"get_ffprobe_data\",\n    \"get_ffprobe_streams\",\n    \"get_ffmpeg_codec_args\",\n    \"get_ffmpeg_format_args\",\n    \"convert_ffprobe_fps_value\",\n    \"convert_ffprobe_fps_to_float\",\n    \"get_rescaled_command_arguments\",\n\n    \"IniSettingRegistry\",\n    \"JSONSettingRegistry\",\n    \"OpenPypeSecureRegistry\",\n    \"OpenPypeSettingsRegistry\",\n    \"get_local_site_id\",\n    \"change_openpype_mongo_url\",\n    \"get_openpype_username\",\n    \"is_admin_password_required\",\n\n    \"ApplicationLaunchFailed\",\n    \"ApplictionExecutableNotFound\",\n    \"ApplicationNotFound\",\n    \"ApplicationManager\",\n    \"PreLaunchHook\",\n    \"PostLaunchHook\",\n    \"EnvironmentPrepData\",\n    \"prepare_app_environments\",\n    \"prepare_context_environments\",\n    \"get_app_environments_for_context\",\n    \"apply_project_environments_value\",\n\n    \"compile_list_of_regexes\",\n\n    \"filter_profiles\",\n\n    \"prepare_template_data\",\n    \"source_hash\",\n\n    \"format_file_size\",\n    \"collect_frames\",\n    \"create_hard_link\",\n    \"version_up\",\n    \"get_version_from_path\",\n    \"get_last_version_from_path\",\n\n    \"merge_dict\",\n    \"TemplateMissingKey\",\n    \"TemplateUnsolved\",\n    \"StringTemplate\",\n    \"TemplatesDict\",\n    \"FormatObject\",\n\n    \"terminal\",\n\n    \"get_datetime_data\",\n    \"get_formatted_current_time\",\n\n    \"Logger\",\n\n    \"op_version_control_available\",\n    \"get_openpype_version\",\n    \"get_build_version\",\n    \"get_expected_version\",\n    \"is_running_from_build\",\n    \"is_running_staging\",\n    \"is_current_version_studio_latest\",\n\n    \"requests_get\",\n    \"requests_post\"\n]\n"
  },
  {
    "path": "openpype/lib/applications.py",
    "content": "import os\nimport sys\nimport copy\nimport json\nimport tempfile\nimport platform\nimport collections\nimport inspect\nimport subprocess\nfrom abc import ABCMeta, abstractmethod\n\nimport six\n\nfrom openpype import AYON_SERVER_ENABLED, PACKAGE_DIR\nfrom openpype.client import get_asset_name_identifier\nfrom openpype.settings import (\n    get_system_settings,\n    get_project_settings,\n    get_local_settings\n)\nfrom openpype.settings.constants import (\n    METADATA_KEYS,\n    M_DYNAMIC_KEY_LABEL\n)\nfrom .log import Logger\nfrom .profiles_filtering import filter_profiles\nfrom .local_settings import get_openpype_username\n\nfrom .python_module_tools import (\n    modules_from_path,\n    classes_from_module\n)\nfrom .execute import (\n    find_executable,\n    get_linux_launcher_args\n)\n\n_logger = None\n\nPLATFORM_NAMES = {\"windows\", \"linux\", \"darwin\"}\nDEFAULT_ENV_SUBGROUP = \"standard\"\nCUSTOM_LAUNCH_APP_GROUPS = {\n    \"djvview\"\n}\n\n\nclass LaunchTypes:\n    \"\"\"Launch types are filters for pre/post-launch hooks.\n\n    Please use these variables in case they'll change values.\n    \"\"\"\n\n    # Local launch - application is launched on local machine\n    local = \"local\"\n    # Farm render job - application is on farm\n    farm_render = \"farm-render\"\n    # Farm publish job - integration post-render job\n    farm_publish = \"farm-publish\"\n    # Remote launch - application is launched on remote machine from which\n    #     can be started publishing\n    remote = \"remote\"\n    # Automated launch - application is launched with automated publishing\n    automated = \"automated\"\n\n\ndef parse_environments(env_data, env_group=None, platform_name=None):\n    \"\"\"Parse environment values from settings byt group and platform.\n\n    Data may contain up to 2 hierarchical levels of dictionaries. At the end\n    of the last level must be string or list. List is joined using platform\n    specific joiner (';' for windows and ':' for linux and mac).\n\n    Hierarchical levels can contain keys for subgroups and platform name.\n    Platform specific values must be always last level of dictionary. Platform\n    names are \"windows\" (MS Windows), \"linux\" (any linux distribution) and\n    \"darwin\" (any MacOS distribution).\n\n    Subgroups are helpers added mainly for standard and on farm usage. Farm\n    may require different environments for e.g. licence related values or\n    plugins. Default subgroup is \"standard\".\n\n    Examples:\n    ```\n    {\n        # Unchanged value\n        \"ENV_KEY1\": \"value\",\n        # Empty values are kept (unset environment variable)\n        \"ENV_KEY2\": \"\",\n\n        # Join list values with ':' or ';'\n        \"ENV_KEY3\": [\"value1\", \"value2\"],\n\n        # Environment groups\n        \"ENV_KEY4\": {\n            \"standard\": \"DEMO_SERVER_URL\",\n            \"farm\": \"LICENCE_SERVER_URL\"\n        },\n\n        # Platform specific (and only for windows and mac)\n        \"ENV_KEY5\": {\n            \"windows\": \"windows value\",\n            \"darwin\": [\"value 1\", \"value 2\"]\n        },\n\n        # Environment groups and platform combination\n        \"ENV_KEY6\": {\n            \"farm\": \"FARM_VALUE\",\n            \"standard\": {\n                \"windows\": [\"value1\", \"value2\"],\n                \"linux\": \"value1\",\n                \"darwin\": \"\"\n            }\n        }\n    }\n    ```\n    \"\"\"\n    output = {}\n    if not env_data:\n        return output\n\n    if not env_group:\n        env_group = DEFAULT_ENV_SUBGROUP\n\n    if not platform_name:\n        platform_name = platform.system().lower()\n\n    for key, value in env_data.items():\n        if isinstance(value, dict):\n            # Look if any key is platform key\n            #   - expect that represents environment group if does not contain\n            #   platform keys\n            if not PLATFORM_NAMES.intersection(set(value.keys())):\n                # Skip the key if group is not available\n                if env_group not in value:\n                    continue\n                value = value[env_group]\n\n        # Check again if value is dictionary\n        #   - this time there should be only platform keys\n        if isinstance(value, dict):\n            value = value.get(platform_name)\n\n        # Check if value is list and join it's values\n        # QUESTION Should empty values be skipped?\n        if isinstance(value, (list, tuple)):\n            value = os.pathsep.join(value)\n\n        # Set key to output if value is string\n        if isinstance(value, six.string_types):\n            output[key] = value\n    return output\n\n\ndef get_logger():\n    \"\"\"Global lib.applications logger getter.\"\"\"\n    global _logger\n    if _logger is None:\n        _logger = Logger.get_logger(__name__)\n    return _logger\n\n\nclass ApplicationNotFound(Exception):\n    \"\"\"Application was not found in ApplicationManager by name.\"\"\"\n\n    def __init__(self, app_name):\n        self.app_name = app_name\n        super(ApplicationNotFound, self).__init__(\n            \"Application \\\"{}\\\" was not found.\".format(app_name)\n        )\n\n\nclass ApplictionExecutableNotFound(Exception):\n    \"\"\"Defined executable paths are not available on the machine.\"\"\"\n\n    def __init__(self, application):\n        self.application = application\n        details = None\n        if not application.executables:\n            msg = (\n                \"Executable paths for application \\\"{}\\\"({}) are not set.\"\n            )\n        else:\n            msg = (\n                \"Defined executable paths for application \\\"{}\\\"({})\"\n                \" are not available on this machine.\"\n            )\n            details = \"Defined paths:\"\n            for executable in application.executables:\n                details += \"\\n- \" + executable.executable_path\n\n        self.msg = msg.format(application.full_label, application.full_name)\n        self.details = details\n\n        exc_mgs = str(self.msg)\n        if details:\n            # Is good idea to pass new line symbol to exception message?\n            exc_mgs += \"\\n\" + details\n        self.exc_msg = exc_mgs\n        super(ApplictionExecutableNotFound, self).__init__(exc_mgs)\n\n\nclass ApplicationLaunchFailed(Exception):\n    \"\"\"Application launch failed due to known reason.\n\n    Message should be self explanatory as traceback won't be shown.\n    \"\"\"\n    pass\n\n\nclass ApplicationGroup:\n    \"\"\"Hold information about application group.\n\n    Application group wraps different versions(variants) of application.\n    e.g. \"maya\" is group and \"maya_2020\" is variant.\n\n    Group hold `host_name` which is implementation name used in pype. Also\n    holds `enabled` if whole app group is enabled or `icon` for application\n    icon path in resources.\n\n    Group has also `environment` which hold same environments for all variants.\n\n    Args:\n        name (str): Groups' name.\n        data (dict): Group defying data loaded from settings.\n        manager (ApplicationManager): Manager that created the group.\n    \"\"\"\n\n    def __init__(self, name, data, manager):\n        self.name = name\n        self.manager = manager\n        self._data = data\n\n        self.enabled = data.get(\"enabled\", True)\n        self.label = data.get(\"label\") or None\n        self.icon = data.get(\"icon\") or None\n        self._environment = data.get(\"environment\") or {}\n\n        host_name = data.get(\"host_name\", None)\n        self.is_host = host_name is not None\n        self.host_name = host_name\n\n        variants = data.get(\"variants\") or {}\n        key_label_mapping = variants.pop(M_DYNAMIC_KEY_LABEL, {})\n        for variant_name, variant_data in variants.items():\n            if variant_name in METADATA_KEYS:\n                continue\n\n            if \"variant_label\" not in variant_data:\n                variant_label = key_label_mapping.get(variant_name)\n                if variant_label:\n                    variant_data[\"variant_label\"] = variant_label\n\n            variants[variant_name] = Application(\n                variant_name, variant_data, self\n            )\n\n        self.variants = variants\n\n    def __repr__(self):\n        return \"<{}> - {}\".format(self.__class__.__name__, self.name)\n\n    def __iter__(self):\n        for variant in self.variants.values():\n            yield variant\n\n    @property\n    def environment(self):\n        return copy.deepcopy(self._environment)\n\n\nclass Application:\n    \"\"\"Hold information about application.\n\n    Object by itself does nothing special.\n\n    Args:\n        name (str): Specific version (or variant) of application.\n            e.g. \"maya2020\", \"nuke11.3\", etc.\n        data (dict): Data for the version containing information about\n            executables, variant label or if is enabled.\n            Only required key is `executables`.\n        group (ApplicationGroup): App group object that created the application\n            and under which application belongs.\n    \"\"\"\n\n    def __init__(self, name, data, group):\n        self.name = name\n        self.group = group\n        self._data = data\n\n        enabled = False\n        if group.enabled:\n            enabled = data.get(\"enabled\", True)\n        self.enabled = enabled\n        self.use_python_2 = data.get(\"use_python_2\", False)\n\n        self.label = data.get(\"variant_label\") or name\n        self.full_name = \"/\".join((group.name, name))\n\n        if group.label:\n            full_label = \" \".join((group.label, self.label))\n        else:\n            full_label = self.label\n        self.full_label = full_label\n        self._environment = data.get(\"environment\") or {}\n\n        arguments = data.get(\"arguments\")\n        if isinstance(arguments, dict):\n            arguments = arguments.get(platform.system().lower())\n\n        if not arguments:\n            arguments = []\n        self.arguments = arguments\n\n        if \"executables\" not in data:\n            self.executables = [\n                UndefinedApplicationExecutable()\n            ]\n            return\n\n        _executables = data[\"executables\"]\n        if isinstance(_executables, dict):\n            _executables = _executables.get(platform.system().lower())\n\n        if not _executables:\n            _executables = []\n\n        executables = []\n        for executable in _executables:\n            executables.append(ApplicationExecutable(executable))\n\n        self.executables = executables\n\n    def __repr__(self):\n        return \"<{}> - {}\".format(self.__class__.__name__, self.full_name)\n\n    @property\n    def environment(self):\n        return copy.deepcopy(self._environment)\n\n    @property\n    def manager(self):\n        return self.group.manager\n\n    @property\n    def host_name(self):\n        return self.group.host_name\n\n    @property\n    def icon(self):\n        return self.group.icon\n\n    @property\n    def is_host(self):\n        return self.group.is_host\n\n    def find_executable(self):\n        \"\"\"Try to find existing executable for application.\n\n        Returns (str): Path to executable from `executables` or None if any\n            exists.\n        \"\"\"\n        for executable in self.executables:\n            if executable.exists():\n                return executable\n        return None\n\n    def launch(self, *args, **kwargs):\n        \"\"\"Launch the application.\n\n        For this purpose is used manager's launch method to keep logic at one\n        place.\n\n        Arguments must match with manager's launch method. That's why *args\n        **kwargs are used.\n\n        Returns:\n            subprocess.Popen: Return executed process as Popen object.\n        \"\"\"\n        return self.manager.launch(self.full_name, *args, **kwargs)\n\n\nclass ApplicationManager:\n    \"\"\"Load applications and tools and store them by their full name.\n\n    Args:\n        system_settings (dict): Preloaded system settings. When passed manager\n            will always use these values. Gives ability to create manager\n            using different settings.\n    \"\"\"\n\n    def __init__(self, system_settings=None):\n        self.log = Logger.get_logger(self.__class__.__name__)\n\n        self.app_groups = {}\n        self.applications = {}\n        self.tool_groups = {}\n        self.tools = {}\n\n        self._system_settings = system_settings\n\n        self.refresh()\n\n    def set_system_settings(self, system_settings):\n        \"\"\"Ability to change init system settings.\n\n        This will trigger refresh of manager.\n        \"\"\"\n        self._system_settings = system_settings\n\n        self.refresh()\n\n    def refresh(self):\n        \"\"\"Refresh applications from settings.\"\"\"\n        self.app_groups.clear()\n        self.applications.clear()\n        self.tool_groups.clear()\n        self.tools.clear()\n\n        if self._system_settings is not None:\n            settings = copy.deepcopy(self._system_settings)\n        else:\n            settings = get_system_settings(\n                clear_metadata=False, exclude_locals=False\n            )\n\n        all_app_defs = {}\n        # Prepare known applications\n        app_defs = settings[\"applications\"]\n        additional_apps = {}\n        for group_name, variant_defs in app_defs.items():\n            if group_name in METADATA_KEYS:\n                continue\n\n            if group_name == \"additional_apps\":\n                additional_apps = variant_defs\n            else:\n                all_app_defs[group_name] = variant_defs\n\n        # Prepare additional applications\n        # - First find dynamic keys that can be used as labels of group\n        dynamic_keys = {}\n        for group_name, variant_defs in additional_apps.items():\n            if group_name == M_DYNAMIC_KEY_LABEL:\n                dynamic_keys = variant_defs\n                break\n\n        # Add additional apps to known applications\n        for group_name, variant_defs in additional_apps.items():\n            if group_name in METADATA_KEYS:\n                continue\n\n            # Determine group label\n            label = variant_defs.get(\"label\")\n            if not label:\n                # Look for label set in dynamic labels\n                label = dynamic_keys.get(group_name)\n                if not label:\n                    label = group_name\n                variant_defs[\"label\"] = label\n\n            all_app_defs[group_name] = variant_defs\n\n        for group_name, variant_defs in all_app_defs.items():\n            if group_name in METADATA_KEYS:\n                continue\n\n            group = ApplicationGroup(group_name, variant_defs, self)\n            self.app_groups[group_name] = group\n            for app in group:\n                self.applications[app.full_name] = app\n\n        tools_definitions = settings[\"tools\"][\"tool_groups\"]\n        tool_label_mapping = tools_definitions.pop(M_DYNAMIC_KEY_LABEL, {})\n        for tool_group_name, tool_group_data in tools_definitions.items():\n            if not tool_group_name or tool_group_name in METADATA_KEYS:\n                continue\n\n            tool_group_label = (\n                tool_label_mapping.get(tool_group_name) or tool_group_name\n            )\n            group = EnvironmentToolGroup(\n                tool_group_name, tool_group_label, tool_group_data, self\n            )\n            self.tool_groups[tool_group_name] = group\n            for tool in group:\n                self.tools[tool.full_name] = tool\n\n    def find_latest_available_variant_for_group(self, group_name):\n        group = self.app_groups.get(group_name)\n        if group is None or not group.enabled:\n            return None\n\n        output = None\n        for _, variant in reversed(sorted(group.variants.items())):\n            executable = variant.find_executable()\n            if executable:\n                output = variant\n                break\n        return output\n\n    def create_launch_context(self, app_name, **data):\n        \"\"\"Prepare launch context for application.\n\n        Args:\n            app_name (str): Name of application that should be launched.\n            **data (Any): Any additional data. Data may be used during\n\n        Returns:\n            ApplicationLaunchContext: Launch context for application.\n\n        Raises:\n            ApplicationNotFound: Application was not found by entered name.\n        \"\"\"\n\n        app = self.applications.get(app_name)\n        if not app:\n            raise ApplicationNotFound(app_name)\n\n        executable = app.find_executable()\n\n        return ApplicationLaunchContext(\n            app, executable, **data\n        )\n\n    def launch_with_context(self, launch_context):\n        \"\"\"Launch application using existing launch context.\n\n        Args:\n            launch_context (ApplicationLaunchContext): Prepared launch\n                context.\n        \"\"\"\n\n        if not launch_context.executable:\n            raise ApplictionExecutableNotFound(launch_context.application)\n        return launch_context.launch()\n\n    def launch(self, app_name, **data):\n        \"\"\"Launch procedure.\n\n        For host application it's expected to contain \"project_name\",\n        \"asset_name\" and \"task_name\".\n\n        Args:\n            app_name (str): Name of application that should be launched.\n            **data (dict): Any additional data. Data may be used during\n                preparation to store objects usable in multiple places.\n\n        Raises:\n            ApplicationNotFound: Application was not found by entered\n                argument `app_name`.\n            ApplictionExecutableNotFound: Executables in application definition\n                were not found on this machine.\n            ApplicationLaunchFailed: Something important for application launch\n                failed. Exception should contain explanation message,\n                traceback should not be needed.\n        \"\"\"\n\n        context = self.create_launch_context(app_name, **data)\n        return self.launch_with_context(context)\n\n\n\nclass EnvironmentToolGroup:\n    \"\"\"Hold information about environment tool group.\n\n    Environment tool group may hold different variants of same tool and set\n    environments that are same for all of them.\n\n    e.g. \"mtoa\" may have different versions but all environments except one\n        are same.\n\n    Args:\n        name (str): Name of the tool group.\n        data (dict): Group's information with it's variants.\n        manager (ApplicationManager): Manager that creates the group.\n    \"\"\"\n\n    def __init__(self, name, label, data, manager):\n        self.name = name\n        self.label = label\n        self._data = data\n        self.manager = manager\n        self._environment = data[\"environment\"]\n\n        variants = data.get(\"variants\") or {}\n        label_by_key = variants.pop(M_DYNAMIC_KEY_LABEL, {})\n        variants_by_name = {}\n        for variant_name, variant_data in variants.items():\n            if variant_name in METADATA_KEYS:\n                continue\n\n            variant_label = label_by_key.get(variant_name) or variant_name\n            tool = EnvironmentTool(\n                variant_name, variant_label, variant_data, self\n            )\n            variants_by_name[variant_name] = tool\n        self.variants = variants_by_name\n\n    def __repr__(self):\n        return \"<{}> - {}\".format(self.__class__.__name__, self.name)\n\n    def __iter__(self):\n        for variant in self.variants.values():\n            yield variant\n\n    @property\n    def environment(self):\n        return copy.deepcopy(self._environment)\n\n\nclass EnvironmentTool:\n    \"\"\"Hold information about application tool.\n\n    Structure of tool information.\n\n    Args:\n        name (str): Name of the tool.\n        variant_data (dict): Variant data with environments and\n            host and app variant filters.\n        group (str): Name of group which wraps tool.\n    \"\"\"\n\n    def __init__(self, name, label, variant_data, group):\n        # Backwards compatibility 3.9.1 - 3.9.2\n        # - 'variant_data' contained only environments but contain also host\n        #   and application variant filters\n        host_names = variant_data.get(\"host_names\", [])\n        app_variants = variant_data.get(\"app_variants\", [])\n\n        if \"environment\" in variant_data:\n            environment = variant_data[\"environment\"]\n        else:\n            environment = variant_data\n\n        self.host_names = host_names\n        self.app_variants = app_variants\n        self.name = name\n        self.variant_label = label\n        self.label = \" \".join((group.label, label))\n        self.group = group\n\n        self._environment = environment\n        self.full_name = \"/\".join((group.name, name))\n\n    def __repr__(self):\n        return \"<{}> - {}\".format(self.__class__.__name__, self.full_name)\n\n    @property\n    def environment(self):\n        return copy.deepcopy(self._environment)\n\n    def is_valid_for_app(self, app):\n        \"\"\"Is tool valid for application.\n\n        Args:\n            app (Application): Application for which are prepared environments.\n        \"\"\"\n        if self.app_variants and app.full_name not in self.app_variants:\n            return False\n\n        if self.host_names and app.host_name not in self.host_names:\n            return False\n        return True\n\n\nclass ApplicationExecutable:\n    \"\"\"Representation of executable loaded from settings.\"\"\"\n\n    def __init__(self, executable):\n        # Try to format executable with environments\n        try:\n            executable = executable.format(**os.environ)\n        except Exception:\n            pass\n\n        # On MacOS check if exists path to executable when ends with `.app`\n        # - it is common that path will lead to \"/Applications/Blender\" but\n        #   real path is \"/Applications/Blender.app\"\n        if platform.system().lower() == \"darwin\":\n            executable = self.macos_executable_prep(executable)\n\n        self.executable_path = executable\n\n    def __str__(self):\n        return self.executable_path\n\n    def __repr__(self):\n        return \"<{}> {}\".format(self.__class__.__name__, self.executable_path)\n\n    @staticmethod\n    def macos_executable_prep(executable):\n        \"\"\"Try to find full path to executable file.\n\n        Real executable is stored in '*.app/Contents/MacOS/<executable>'.\n\n        Having path to '*.app' gives ability to read it's plist info and\n        use \"CFBundleExecutable\" key from plist to know what is \"executable.\"\n\n        Plist is stored in '*.app/Contents/Info.plist'.\n\n        This is because some '*.app' directories don't have same permissions\n        as real executable.\n        \"\"\"\n        # Try to find if there is `.app` file\n        if not os.path.exists(executable):\n            _executable = executable + \".app\"\n            if os.path.exists(_executable):\n                executable = _executable\n\n        # Try to find real executable if executable has `Contents` subfolder\n        contents_dir = os.path.join(executable, \"Contents\")\n        if os.path.exists(contents_dir):\n            executable_filename = None\n            # Load plist file and check for bundle executable\n            plist_filepath = os.path.join(contents_dir, \"Info.plist\")\n            if os.path.exists(plist_filepath):\n                import plistlib\n\n                if hasattr(plistlib, \"load\"):\n                    with open(plist_filepath, \"rb\") as stream:\n                        parsed_plist = plistlib.load(stream)\n                else:\n                    parsed_plist = plistlib.readPlist(plist_filepath)\n                executable_filename = parsed_plist.get(\"CFBundleExecutable\")\n\n            if executable_filename:\n                executable = os.path.join(\n                    contents_dir, \"MacOS\", executable_filename\n                )\n\n        return executable\n\n    def as_args(self):\n        return [self.executable_path]\n\n    def _realpath(self):\n        \"\"\"Check if path is valid executable path.\"\"\"\n        # Check for executable in PATH\n        result = find_executable(self.executable_path)\n        if result is not None:\n            return result\n\n        # This is not 100% validation but it is better than remove ability to\n        #   launch .bat, .sh or extentionless files\n        if os.path.exists(self.executable_path):\n            return self.executable_path\n        return None\n\n    def exists(self):\n        if not self.executable_path:\n            return False\n        return bool(self._realpath())\n\n\nclass UndefinedApplicationExecutable(ApplicationExecutable):\n    \"\"\"Some applications do not require executable path from settings.\n\n    In that case this class is used to \"fake\" existing executable.\n    \"\"\"\n    def __init__(self):\n        pass\n\n    def __str__(self):\n        return self.__class__.__name__\n\n    def __repr__(self):\n        return \"<{}>\".format(self.__class__.__name__)\n\n    def as_args(self):\n        return []\n\n    def exists(self):\n        return True\n\n\n@six.add_metaclass(ABCMeta)\nclass LaunchHook:\n    \"\"\"Abstract base class of launch hook.\"\"\"\n    # Order of prelaunch hook, will be executed as last if set to None.\n    order = None\n    # List of host implementations, skipped if empty.\n    hosts = set()\n    # Set of application groups\n    app_groups = set()\n    # Set of specific application names\n    app_names = set()\n    # Set of platform availability\n    platforms = set()\n    # Set of launch types for which is available\n    # - if empty then is available for all launch types\n    # - by default has 'local' which is most common reason for launc hooks\n    launch_types = {LaunchTypes.local}\n\n    def __init__(self, launch_context):\n        \"\"\"Constructor of launch hook.\n\n        Always should be called\n        \"\"\"\n        self.log = Logger.get_logger(self.__class__.__name__)\n\n        self.launch_context = launch_context\n\n        is_valid = self.class_validation(launch_context)\n        if is_valid:\n            is_valid = self.validate()\n\n        self.is_valid = is_valid\n\n    @classmethod\n    def class_validation(cls, launch_context):\n        \"\"\"Validation of class attributes by launch context.\n\n        Args:\n            launch_context (ApplicationLaunchContext): Context of launching\n                application.\n\n        Returns:\n            bool: Is launch hook valid for the context by class attributes.\n        \"\"\"\n        if cls.platforms:\n            low_platforms = tuple(\n                _platform.lower()\n                for _platform in cls.platforms\n            )\n            if platform.system().lower() not in low_platforms:\n                return False\n\n        if cls.hosts:\n            if launch_context.host_name not in cls.hosts:\n                return False\n\n        if cls.app_groups:\n            if launch_context.app_group.name not in cls.app_groups:\n                return False\n\n        if cls.app_names:\n            if launch_context.app_name not in cls.app_names:\n                return False\n\n        if cls.launch_types:\n            if launch_context.launch_type not in cls.launch_types:\n                return False\n\n        return True\n\n    @property\n    def data(self):\n        return self.launch_context.data\n\n    @property\n    def application(self):\n        return getattr(self.launch_context, \"application\", None)\n\n    @property\n    def manager(self):\n        return getattr(self.application, \"manager\", None)\n\n    @property\n    def host_name(self):\n        return getattr(self.application, \"host_name\", None)\n\n    @property\n    def app_group(self):\n        return getattr(self.application, \"group\", None)\n\n    @property\n    def app_name(self):\n        return getattr(self.application, \"full_name\", None)\n\n    @property\n    def modules_manager(self):\n        return getattr(self.launch_context, \"modules_manager\", None)\n\n    def validate(self):\n        \"\"\"Optional validation of launch hook on initialization.\n\n        Returns:\n            bool: Hook is valid (True) or invalid (False).\n        \"\"\"\n        # QUESTION Not sure if this method has any usable potential.\n        # - maybe result can be based on settings\n        return True\n\n    @abstractmethod\n    def execute(self, *args, **kwargs):\n        \"\"\"Abstract execute method where logic of hook is.\"\"\"\n        pass\n\n\nclass PreLaunchHook(LaunchHook):\n    \"\"\"Abstract class of prelaunch hook.\n\n    This launch hook will be processed before application is launched.\n\n    If any exception will happen during processing the application won't be\n    launched.\n    \"\"\"\n\n\nclass PostLaunchHook(LaunchHook):\n    \"\"\"Abstract class of postlaunch hook.\n\n    This launch hook will be processed after application is launched.\n\n    Nothing will happen if any exception will happen during processing. And\n    processing of other postlaunch hooks won't stop either.\n    \"\"\"\n\n\nclass ApplicationLaunchContext:\n    \"\"\"Context of launching application.\n\n    Main purpose of context is to prepare launch arguments and keyword\n    arguments for new process. Most important part of keyword arguments\n    preparations are environment variables.\n\n    During the whole process is possible to use `data` attribute to store\n    object usable in multiple places.\n\n    Launch arguments are strings in list. It is possible to \"chain\" argument\n    when order of them matters. That is possible to do with adding list where\n    order is right and should not change.\n    NOTE: This is recommendation, not requirement.\n    e.g.: `[\"nuke.exe\", \"--NukeX\"]` -> In this case any part of process may\n    insert argument between `nuke.exe` and `--NukeX`. To keep them together\n    it is better to wrap them in another list: `[[\"nuke.exe\", \"--NukeX\"]]`.\n\n    Notes:\n        It is possible to use launch context only to prepare environment\n            variables. In that case `executable` may be None and can be used\n            'run_prelaunch_hooks' method to run prelaunch hooks which prepare\n            them.\n\n    Args:\n        application (Application): Application definition.\n        executable (ApplicationExecutable): Object with path to executable.\n        env_group (Optional[str]): Environment variable group. If not set\n            'DEFAULT_ENV_SUBGROUP' is used.\n        launch_type (Optional[str]): Launch type. If not set 'local' is used.\n        **data (dict): Any additional data. Data may be used during\n            preparation to store objects usable in multiple places.\n    \"\"\"\n\n    def __init__(\n        self,\n        application,\n        executable,\n        env_group=None,\n        launch_type=None,\n        **data\n    ):\n        from openpype.modules import ModulesManager\n\n        # Application object\n        self.application = application\n\n        self.modules_manager = ModulesManager()\n\n        # Logger\n        logger_name = \"{}-{}\".format(self.__class__.__name__,\n                                     self.application.full_name)\n        self.log = Logger.get_logger(logger_name)\n\n        self.executable = executable\n\n        if launch_type is None:\n            launch_type = LaunchTypes.local\n        self.launch_type = launch_type\n\n        if env_group is None:\n            env_group = DEFAULT_ENV_SUBGROUP\n\n        self.env_group = env_group\n\n        self.data = dict(data)\n\n        launch_args = []\n        if executable is not None:\n            launch_args = executable.as_args()\n        # subprocess.Popen launch arguments (first argument in constructor)\n        self.launch_args = launch_args\n        self.launch_args.extend(application.arguments)\n        if self.data.get(\"app_args\"):\n            self.launch_args.extend(self.data.pop(\"app_args\"))\n\n        # Handle launch environemtns\n        src_env = self.data.pop(\"env\", None)\n        if src_env is not None and not isinstance(src_env, dict):\n            self.log.warning((\n                \"Passed `env` kwarg has invalid type: {}. Expected: `dict`.\"\n                \" Using `os.environ` instead.\"\n            ).format(str(type(src_env))))\n            src_env = None\n\n        if src_env is None:\n            src_env = os.environ\n\n        ignored_env = {\"QT_API\", }\n        env = {\n            key: str(value)\n            for key, value in src_env.items()\n            if key not in ignored_env\n        }\n        # subprocess.Popen keyword arguments\n        self.kwargs = {\"env\": env}\n\n        if platform.system().lower() == \"windows\":\n            # Detach new process from currently running process on Windows\n            flags = (\n                subprocess.CREATE_NEW_PROCESS_GROUP\n                | subprocess.DETACHED_PROCESS\n            )\n            self.kwargs[\"creationflags\"] = flags\n\n        if not sys.stdout:\n            self.kwargs[\"stdout\"] = subprocess.DEVNULL\n            self.kwargs[\"stderr\"] = subprocess.DEVNULL\n\n        self.prelaunch_hooks = None\n        self.postlaunch_hooks = None\n\n        self.process = None\n        self._prelaunch_hooks_executed = False\n\n    @property\n    def env(self):\n        if (\n            \"env\" not in self.kwargs\n            or self.kwargs[\"env\"] is None\n        ):\n            self.kwargs[\"env\"] = {}\n        return self.kwargs[\"env\"]\n\n    @env.setter\n    def env(self, value):\n        if not isinstance(value, dict):\n            raise ValueError(\n                \"'env' attribute expect 'dict' object. Got: {}\".format(\n                    str(type(value))\n                )\n            )\n        self.kwargs[\"env\"] = value\n\n    def _collect_addons_launch_hook_paths(self):\n        \"\"\"Helper to collect application launch hooks from addons.\n\n        Module have to have implemented 'get_launch_hook_paths' method which\n        can expect application as argument or nothing.\n\n        Returns:\n            List[str]: Paths to launch hook directories.\n        \"\"\"\n\n        expected_types = (list, tuple, set)\n\n        output = []\n        for module in self.modules_manager.get_enabled_modules():\n            # Skip module if does not have implemented 'get_launch_hook_paths'\n            func = getattr(module, \"get_launch_hook_paths\", None)\n            if func is None:\n                continue\n\n            func = module.get_launch_hook_paths\n            if hasattr(inspect, \"signature\"):\n                sig = inspect.signature(func)\n                expect_args = len(sig.parameters) > 0\n            else:\n                expect_args = len(inspect.getargspec(func)[0]) > 0\n\n            # Pass application argument if method expect it.\n            try:\n                if expect_args:\n                    hook_paths = func(self.application)\n                else:\n                    hook_paths = func()\n            except Exception:\n                self.log.warning(\n                    \"Failed to call 'get_launch_hook_paths'\",\n                    exc_info=True\n                )\n                continue\n\n            if not hook_paths:\n                continue\n\n            # Convert string to list\n            if isinstance(hook_paths, six.string_types):\n                hook_paths = [hook_paths]\n\n            # Skip invalid types\n            if not isinstance(hook_paths, expected_types):\n                self.log.warning((\n                    \"Result of `get_launch_hook_paths`\"\n                    \" has invalid type {}. Expected {}\"\n                ).format(type(hook_paths), expected_types))\n                continue\n\n            output.extend(hook_paths)\n        return output\n\n    def paths_to_launch_hooks(self):\n        \"\"\"Directory paths where to look for launch hooks.\"\"\"\n        # This method has potential to be part of application manager (maybe).\n        paths = []\n\n        # TODO load additional studio paths from settings\n        import openpype\n        openpype_dir = os.path.dirname(os.path.abspath(openpype.__file__))\n\n        global_hooks_dir = os.path.join(openpype_dir, \"hooks\")\n\n        hooks_dirs = [\n            global_hooks_dir\n        ]\n        if self.host_name:\n            # If host requires launch hooks and is module then launch hooks\n            #   should be collected using 'collect_launch_hook_paths'\n            #   - module have to implement 'get_launch_hook_paths'\n            host_module = self.modules_manager.get_host_module(self.host_name)\n            if not host_module:\n                hooks_dirs.append(os.path.join(\n                    openpype_dir, \"hosts\", self.host_name, \"hooks\"\n                ))\n\n        for path in hooks_dirs:\n            if (\n                os.path.exists(path)\n                and os.path.isdir(path)\n                and path not in paths\n            ):\n                paths.append(path)\n\n        # Load modules paths\n        paths.extend(self._collect_addons_launch_hook_paths())\n\n        return paths\n\n    def discover_launch_hooks(self, force=False):\n        \"\"\"Load and prepare launch hooks.\"\"\"\n        if (\n            self.prelaunch_hooks is not None\n            or self.postlaunch_hooks is not None\n        ):\n            if not force:\n                self.log.info(\"Launch hooks were already discovered.\")\n                return\n\n            self.prelaunch_hooks.clear()\n            self.postlaunch_hooks.clear()\n\n        self.log.debug(\"Discovery of launch hooks started.\")\n\n        paths = self.paths_to_launch_hooks()\n        self.log.debug(\"Paths searched for launch hooks:\\n{}\".format(\n            \"\\n\".join(\"- {}\".format(path) for path in paths)\n        ))\n\n        all_classes = {\n            \"pre\": [],\n            \"post\": []\n        }\n        for path in paths:\n            if not os.path.exists(path):\n                self.log.info(\n                    \"Path to launch hooks does not exist: \\\"{}\\\"\".format(path)\n                )\n                continue\n\n            modules, _crashed = modules_from_path(path)\n            for _filepath, module in modules:\n                all_classes[\"pre\"].extend(\n                    classes_from_module(PreLaunchHook, module)\n                )\n                all_classes[\"post\"].extend(\n                    classes_from_module(PostLaunchHook, module)\n                )\n\n        for launch_type, classes in all_classes.items():\n            hooks_with_order = []\n            hooks_without_order = []\n            for klass in classes:\n                try:\n                    hook = klass(self)\n                    if not hook.is_valid:\n                        self.log.debug(\n                            \"Skipped hook invalid for current launch context: \"\n                            \"{}\".format(klass.__name__)\n                        )\n                        continue\n\n                    if inspect.isabstract(hook):\n                        self.log.debug(\"Skipped abstract hook: {}\".format(\n                            klass.__name__\n                        ))\n                        continue\n\n                    # Separate hooks by pre/post class\n                    if hook.order is None:\n                        hooks_without_order.append(hook)\n                    else:\n                        hooks_with_order.append(hook)\n\n                except Exception:\n                    self.log.warning(\n                        \"Initialization of hook failed: \"\n                        \"{}\".format(klass.__name__),\n                        exc_info=True\n                    )\n\n            # Sort hooks with order by order\n            ordered_hooks = list(sorted(\n                hooks_with_order, key=lambda obj: obj.order\n            ))\n            # Extend ordered hooks with hooks without defined order\n            ordered_hooks.extend(hooks_without_order)\n\n            if launch_type == \"pre\":\n                self.prelaunch_hooks = ordered_hooks\n            else:\n                self.postlaunch_hooks = ordered_hooks\n\n        self.log.debug(\"Found {} prelaunch and {} postlaunch hooks.\".format(\n            len(self.prelaunch_hooks), len(self.postlaunch_hooks)\n        ))\n\n    @property\n    def app_name(self):\n        return self.application.name\n\n    @property\n    def host_name(self):\n        return self.application.host_name\n\n    @property\n    def app_group(self):\n        return self.application.group\n\n    @property\n    def manager(self):\n        return self.application.manager\n\n    def _run_process(self):\n        # Windows and MacOS have easier process start\n        low_platform = platform.system().lower()\n        if low_platform in (\"windows\", \"darwin\"):\n            return subprocess.Popen(self.launch_args, **self.kwargs)\n\n        # Linux uses mid process\n        # - it is possible that the mid process executable is not\n        #   available for this version of OpenPype in that case use standard\n        #   launch\n        launch_args = get_linux_launcher_args()\n        if launch_args is None:\n            return subprocess.Popen(self.launch_args, **self.kwargs)\n\n        # Prepare data that will be passed to midprocess\n        # - store arguments to a json and pass path to json as last argument\n        # - pass environments to set\n        app_env = self.kwargs.pop(\"env\", {})\n        json_data = {\n            \"args\": self.launch_args,\n            \"env\": app_env\n        }\n        if app_env:\n            # Filter environments of subprocess\n            self.kwargs[\"env\"] = {\n                key: value\n                for key, value in os.environ.items()\n                if key in app_env\n            }\n\n        # Create temp file\n        json_temp = tempfile.NamedTemporaryFile(\n            mode=\"w\", prefix=\"op_app_args\", suffix=\".json\", delete=False\n        )\n        json_temp.close()\n        json_temp_filpath = json_temp.name\n        with open(json_temp_filpath, \"w\") as stream:\n            json.dump(json_data, stream)\n\n        launch_args.append(json_temp_filpath)\n\n        # Create mid-process which will launch application\n        process = subprocess.Popen(launch_args, **self.kwargs)\n        # Wait until the process finishes\n        #   - This is important! The process would stay in \"open\" state.\n        process.wait()\n        # Remove the temp file\n        os.remove(json_temp_filpath)\n        # Return process which is already terminated\n        return process\n\n    def run_prelaunch_hooks(self):\n        \"\"\"Run prelaunch hooks.\n\n        This method will be executed only once, any future calls will skip\n            the processing.\n        \"\"\"\n\n        if self._prelaunch_hooks_executed:\n            self.log.warning(\"Prelaunch hooks were already executed.\")\n            return\n        # Discover launch hooks\n        self.discover_launch_hooks()\n\n        # Execute prelaunch hooks\n        for prelaunch_hook in self.prelaunch_hooks:\n            self.log.debug(\"Executing prelaunch hook: {}\".format(\n                str(prelaunch_hook.__class__.__name__)\n            ))\n            prelaunch_hook.execute()\n        self._prelaunch_hooks_executed = True\n\n    def launch(self):\n        \"\"\"Collect data for new process and then create it.\n\n        This method must not be executed more than once.\n\n        Returns:\n            subprocess.Popen: Created process as Popen object.\n        \"\"\"\n        if self.process is not None:\n            self.log.warning(\"Application was already launched.\")\n            return\n\n        if not self._prelaunch_hooks_executed:\n            self.run_prelaunch_hooks()\n\n        self.log.debug(\"All prelaunch hook executed. Starting new process.\")\n\n        # Prepare subprocess args\n        args_len_str = \"\"\n        if isinstance(self.launch_args, str):\n            args = self.launch_args\n        else:\n            args = self.clear_launch_args(self.launch_args)\n            args_len_str = \" ({})\".format(len(args))\n        self.log.info(\n            \"Launching \\\"{}\\\" with args{}: {}\".format(\n                self.application.full_name, args_len_str, args\n            )\n        )\n        self.launch_args = args\n\n        # Run process\n        self.process = self._run_process()\n\n        # Process post launch hooks\n        for postlaunch_hook in self.postlaunch_hooks:\n            self.log.debug(\"Executing postlaunch hook: {}\".format(\n                str(postlaunch_hook.__class__.__name__)\n            ))\n\n            # TODO how to handle errors?\n            # - store to variable to let them accessible?\n            try:\n                postlaunch_hook.execute()\n\n            except Exception:\n                self.log.warning(\n                    \"After launch procedures were not successful.\",\n                    exc_info=True\n                )\n\n        self.log.debug(\"Launch of {} finished.\".format(\n            self.application.full_name\n        ))\n\n        return self.process\n\n    @staticmethod\n    def clear_launch_args(args):\n        \"\"\"Collect launch arguments to final order.\n\n        Launch argument should be list that may contain another lists this\n        function will upack inner lists and keep ordering.\n\n        ```\n        # source\n        [ [ arg1, [ arg2, arg3 ] ], arg4, [arg5, arg6]]\n        # result\n        [ arg1, arg2, arg3, arg4, arg5, arg6]\n\n        Args:\n            args (list): Source arguments in list may contain inner lists.\n\n        Return:\n            list: Unpacked arguments.\n        \"\"\"\n        if isinstance(args, str):\n            return args\n        all_cleared = False\n        while not all_cleared:\n            all_cleared = True\n            new_args = []\n            for arg in args:\n                if isinstance(arg, (list, tuple, set)):\n                    all_cleared = False\n                    for _arg in arg:\n                        new_args.append(_arg)\n                else:\n                    new_args.append(arg)\n            args = new_args\n\n        return args\n\n\nclass MissingRequiredKey(KeyError):\n    pass\n\n\nclass EnvironmentPrepData(dict):\n    \"\"\"Helper dictionary for storin temp data during environment prep.\n\n    Args:\n        data (dict): Data must contain required keys.\n    \"\"\"\n    required_keys = (\n        \"project_doc\", \"asset_doc\", \"task_name\", \"app\", \"anatomy\"\n    )\n\n    def __init__(self, data):\n        for key in self.required_keys:\n            if key not in data:\n                raise MissingRequiredKey(key)\n\n        if not data.get(\"log\"):\n            data[\"log\"] = get_logger()\n\n        if data.get(\"env\") is None:\n            data[\"env\"] = os.environ.copy()\n\n        if \"system_settings\" not in data:\n            data[\"system_settings\"] = get_system_settings()\n\n        super(EnvironmentPrepData, self).__init__(data)\n\n\ndef get_app_environments_for_context(\n    project_name,\n    asset_name,\n    task_name,\n    app_name,\n    env_group=None,\n    launch_type=None,\n    env=None,\n    modules_manager=None\n):\n    \"\"\"Prepare environment variables by context.\n    Args:\n        project_name (str): Name of project.\n        asset_name (str): Name of asset.\n        task_name (str): Name of task.\n        app_name (str): Name of application that is launched and can be found\n            by ApplicationManager.\n        env_group (Optional[str]): Name of environment group. If not passed\n            default group is used.\n        launch_type (Optional[str]): Type for which prelaunch hooks are\n            executed.\n        env (Optional[dict[str, str]]): Initial environment variables.\n            `os.environ` is used when not passed.\n        modules_manager (Optional[ModulesManager]): Initialized modules\n            manager.\n\n    Returns:\n        dict: Environments for passed context and application.\n    \"\"\"\n\n    # Prepare app object which can be obtained only from ApplicationManager\n    app_manager = ApplicationManager()\n    context = app_manager.create_launch_context(\n        app_name,\n        project_name=project_name,\n        asset_name=asset_name,\n        task_name=task_name,\n        env_group=env_group,\n        launch_type=launch_type,\n        env=env,\n        modules_manager=modules_manager,\n    )\n    context.run_prelaunch_hooks()\n    return context.env\n\n\ndef _merge_env(env, current_env):\n    \"\"\"Modified function(merge) from acre module.\"\"\"\n    import acre\n\n    result = current_env.copy()\n    for key, value in env.items():\n        # Keep missing keys by not filling `missing` kwarg\n        value = acre.lib.partial_format(value, data=current_env)\n        result[key] = value\n    return result\n\n\ndef _add_python_version_paths(app, env, logger, modules_manager):\n    \"\"\"Add vendor packages specific for a Python version.\"\"\"\n\n    for module in modules_manager.get_enabled_modules():\n        module.modify_application_launch_arguments(app, env)\n\n    # Skip adding if host name is not set\n    if not app.host_name:\n        return\n\n    # Add Python 2/3 modules\n    python_vendor_dir = os.path.join(\n        PACKAGE_DIR,\n        \"vendor\",\n        \"python\"\n    )\n    if app.use_python_2:\n        pythonpath = os.path.join(python_vendor_dir, \"python_2\")\n    else:\n        pythonpath = os.path.join(python_vendor_dir, \"python_3\")\n\n    if not os.path.exists(pythonpath):\n        return\n\n    logger.debug(\"Adding Python version specific paths to PYTHONPATH\")\n    python_paths = [pythonpath]\n\n    # Load PYTHONPATH from current launch context\n    python_path = env.get(\"PYTHONPATH\")\n    if python_path:\n        python_paths.append(python_path)\n\n    # Set new PYTHONPATH to launch context environments\n    env[\"PYTHONPATH\"] = os.pathsep.join(python_paths)\n\n\ndef prepare_app_environments(\n    data, env_group=None, implementation_envs=True, modules_manager=None\n):\n    \"\"\"Modify launch environments based on launched app and context.\n\n    Args:\n        data (EnvironmentPrepData): Dictionary where result and intermediate\n            result will be stored.\n    \"\"\"\n    import acre\n\n    app = data[\"app\"]\n    log = data[\"log\"]\n    source_env = data[\"env\"].copy()\n\n    if modules_manager is None:\n        from openpype.modules import ModulesManager\n\n        modules_manager = ModulesManager()\n\n    _add_python_version_paths(app, source_env, log, modules_manager)\n\n    # Use environments from local settings\n    filtered_local_envs = {}\n    system_settings = data[\"system_settings\"]\n    whitelist_envs = system_settings[\"general\"].get(\"local_env_white_list\")\n    if whitelist_envs:\n        local_settings = get_local_settings()\n        local_envs = local_settings.get(\"environments\") or {}\n        filtered_local_envs = {\n            key: value\n            for key, value in local_envs.items()\n            if key in whitelist_envs\n        }\n\n    # Apply local environment variables for already existing values\n    for key, value in filtered_local_envs.items():\n        if key in source_env:\n            source_env[key] = value\n\n    # `app_and_tool_labels` has debug purpose\n    app_and_tool_labels = [app.full_name]\n    # Environments for application\n    environments = [\n        app.group.environment,\n        app.environment\n    ]\n\n    asset_doc = data.get(\"asset_doc\")\n    # Add tools environments\n    groups_by_name = {}\n    tool_by_group_name = collections.defaultdict(dict)\n    if asset_doc:\n        # Make sure each tool group can be added only once\n        for key in asset_doc[\"data\"].get(\"tools_env\") or []:\n            tool = app.manager.tools.get(key)\n            if not tool or not tool.is_valid_for_app(app):\n                continue\n            groups_by_name[tool.group.name] = tool.group\n            tool_by_group_name[tool.group.name][tool.name] = tool\n\n        for group_name in sorted(groups_by_name.keys()):\n            group = groups_by_name[group_name]\n            environments.append(group.environment)\n            for tool_name in sorted(tool_by_group_name[group_name].keys()):\n                tool = tool_by_group_name[group_name][tool_name]\n                environments.append(tool.environment)\n                app_and_tool_labels.append(tool.full_name)\n\n    log.debug(\n        \"Will add environments for apps and tools: {}\".format(\n            \", \".join(app_and_tool_labels)\n        )\n    )\n\n    env_values = {}\n    for _env_values in environments:\n        if not _env_values:\n            continue\n\n        # Choose right platform\n        tool_env = parse_environments(_env_values, env_group)\n\n        # Apply local environment variables\n        # - must happen between all values because they may be used during\n        #   merge\n        for key, value in filtered_local_envs.items():\n            if key in tool_env:\n                tool_env[key] = value\n\n        # Merge dictionaries\n        env_values = _merge_env(tool_env, env_values)\n\n    merged_env = _merge_env(env_values, source_env)\n\n    loaded_env = acre.compute(merged_env, cleanup=False)\n\n    final_env = None\n    # Add host specific environments\n    if app.host_name and implementation_envs:\n        host_module = modules_manager.get_host_module(app.host_name)\n        if not host_module:\n            module = __import__(\"openpype.hosts\", fromlist=[app.host_name])\n            host_module = getattr(module, app.host_name, None)\n        add_implementation_envs = None\n        if host_module:\n            add_implementation_envs = getattr(\n                host_module, \"add_implementation_envs\", None\n            )\n        if add_implementation_envs:\n            # Function may only modify passed dict without returning value\n            final_env = add_implementation_envs(loaded_env, app)\n\n    if final_env is None:\n        final_env = loaded_env\n\n    keys_to_remove = set(source_env.keys()) - set(final_env.keys())\n\n    # Update env\n    data[\"env\"].update(final_env)\n    for key in keys_to_remove:\n        data[\"env\"].pop(key, None)\n\n\ndef apply_project_environments_value(\n    project_name, env, project_settings=None, env_group=None\n):\n    \"\"\"Apply project specific environments on passed environments.\n\n    The environments are applied on passed `env` argument value so it is not\n    required to apply changes back.\n\n    Args:\n        project_name (str): Name of project for which environments should be\n            received.\n        env (dict): Environment values on which project specific environments\n            will be applied.\n        project_settings (dict): Project settings for passed project name.\n            Optional if project settings are already prepared.\n\n    Returns:\n        dict: Passed env values with applied project environments.\n\n    Raises:\n        KeyError: If project settings do not contain keys for project specific\n            environments.\n    \"\"\"\n    import acre\n\n    if project_settings is None:\n        project_settings = get_project_settings(project_name)\n\n    env_value = project_settings[\"global\"][\"project_environments\"]\n    if env_value:\n        parsed_value = parse_environments(env_value, env_group)\n        env.update(acre.compute(\n            _merge_env(parsed_value, env),\n            cleanup=False\n        ))\n    return env\n\n\ndef prepare_context_environments(data, env_group=None, modules_manager=None):\n    \"\"\"Modify launch environments with context data for launched host.\n\n    Args:\n        data (EnvironmentPrepData): Dictionary where result and intermediate\n            result will be stored.\n    \"\"\"\n\n    from openpype.pipeline.template_data import get_template_data\n\n    # Context environments\n    log = data[\"log\"]\n\n    project_doc = data[\"project_doc\"]\n    asset_doc = data[\"asset_doc\"]\n    task_name = data[\"task_name\"]\n    if not project_doc:\n        log.info(\n            \"Skipping context environments preparation.\"\n            \" Launch context does not contain required data.\"\n        )\n        return\n\n    # Load project specific environments\n    project_name = project_doc[\"name\"]\n    project_settings = get_project_settings(project_name)\n    system_settings = get_system_settings()\n    data[\"project_settings\"] = project_settings\n    data[\"system_settings\"] = system_settings\n\n    app = data[\"app\"]\n    context_env = {\n        \"AVALON_PROJECT\": project_doc[\"name\"],\n        \"AVALON_APP_NAME\": app.full_name\n    }\n    if asset_doc:\n        asset_name = get_asset_name_identifier(asset_doc)\n        context_env[\"AVALON_ASSET\"] = asset_name\n\n        if task_name:\n            context_env[\"AVALON_TASK\"] = task_name\n\n    log.debug(\n        \"Context environments set:\\n{}\".format(\n            json.dumps(context_env, indent=4)\n        )\n    )\n    data[\"env\"].update(context_env)\n\n    # Apply project specific environments on current env value\n    # - apply them once the context environments are set\n    apply_project_environments_value(\n        project_name, data[\"env\"], project_settings, env_group\n    )\n\n    if not app.is_host:\n        return\n\n    data[\"env\"][\"AVALON_APP\"] = app.host_name\n\n    if not asset_doc or not task_name:\n        # QUESTION replace with log.info and skip workfile discovery?\n        # - technically it should be possible to launch host without context\n        raise ApplicationLaunchFailed(\n            \"Host launch require asset and task context.\"\n        )\n\n    workdir_data = get_template_data(\n        project_doc, asset_doc, task_name, app.host_name, system_settings\n    )\n    data[\"workdir_data\"] = workdir_data\n\n    anatomy = data[\"anatomy\"]\n\n    task_type = workdir_data[\"task\"][\"type\"]\n    # Temp solution how to pass task type to `_prepare_last_workfile`\n    data[\"task_type\"] = task_type\n\n    try:\n        from openpype.pipeline.workfile import get_workdir_with_workdir_data\n\n        workdir = get_workdir_with_workdir_data(\n            workdir_data,\n            anatomy.project_name,\n            anatomy,\n            project_settings=project_settings\n        )\n\n    except Exception as exc:\n        raise ApplicationLaunchFailed(\n            \"Error in anatomy.format: {}\".format(str(exc))\n        )\n\n    if not os.path.exists(workdir):\n        log.debug(\n            \"Creating workdir folder: \\\"{}\\\"\".format(workdir)\n        )\n        try:\n            os.makedirs(workdir)\n        except Exception as exc:\n            raise ApplicationLaunchFailed(\n                \"Couldn't create workdir because: {}\".format(str(exc))\n            )\n\n    data[\"env\"][\"AVALON_WORKDIR\"] = workdir\n\n    _prepare_last_workfile(data, workdir, modules_manager)\n\n\ndef _prepare_last_workfile(data, workdir, modules_manager):\n    \"\"\"last workfile workflow preparation.\n\n    Function check if should care about last workfile workflow and tries\n    to find the last workfile. Both information are stored to `data` and\n    environments.\n\n    Last workfile is filled always (with version 1) even if any workfile\n    exists yet.\n\n    Args:\n        data (EnvironmentPrepData): Dictionary where result and intermediate\n            result will be stored.\n        workdir (str): Path to folder where workfiles should be stored.\n    \"\"\"\n\n    from openpype.modules import ModulesManager\n    from openpype.pipeline import HOST_WORKFILE_EXTENSIONS\n\n    if not modules_manager:\n        modules_manager = ModulesManager()\n\n    log = data[\"log\"]\n\n    _workdir_data = data.get(\"workdir_data\")\n    if not _workdir_data:\n        log.info(\n            \"Skipping last workfile preparation.\"\n            \" Key `workdir_data` not filled.\"\n        )\n        return\n\n    app = data[\"app\"]\n    workdir_data = copy.deepcopy(_workdir_data)\n    project_name = data[\"project_name\"]\n    task_name = data[\"task_name\"]\n    task_type = data[\"task_type\"]\n\n    start_last_workfile = data.get(\"start_last_workfile\")\n    if start_last_workfile is None:\n        start_last_workfile = should_start_last_workfile(\n            project_name, app.host_name, task_name, task_type\n        )\n    else:\n        log.info(\"Opening of last workfile was disabled by user\")\n\n    data[\"start_last_workfile\"] = start_last_workfile\n\n    workfile_startup = should_workfile_tool_start(\n        project_name, app.host_name, task_name, task_type\n    )\n    data[\"workfile_startup\"] = workfile_startup\n\n    # Store boolean as \"0\"(False) or \"1\"(True)\n    data[\"env\"][\"AVALON_OPEN_LAST_WORKFILE\"] = (\n        str(int(bool(start_last_workfile)))\n    )\n    data[\"env\"][\"OPENPYPE_WORKFILE_TOOL_ON_START\"] = (\n        str(int(bool(workfile_startup)))\n    )\n\n    _sub_msg = \"\" if start_last_workfile else \" not\"\n    log.debug(\n        \"Last workfile should{} be opened on start.\".format(_sub_msg)\n    )\n\n    # Last workfile path\n    last_workfile_path = data.get(\"last_workfile_path\") or \"\"\n    if not last_workfile_path:\n        host_module = modules_manager.get_host_module(app.host_name)\n        if host_module:\n            extensions = host_module.get_workfile_extensions()\n        else:\n            extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name)\n\n        if extensions:\n            from openpype.pipeline.workfile import (\n                get_workfile_template_key,\n                get_last_workfile\n            )\n\n            anatomy = data[\"anatomy\"]\n            project_settings = data[\"project_settings\"]\n            task_type = workdir_data[\"task\"][\"type\"]\n            template_key = get_workfile_template_key(\n                task_type,\n                app.host_name,\n                project_name,\n                project_settings=project_settings\n            )\n            # Find last workfile\n            file_template = str(anatomy.templates[template_key][\"file\"])\n\n            workdir_data.update({\n                \"version\": 1,\n                \"user\": get_openpype_username(),\n                \"ext\": extensions[0]\n            })\n\n            last_workfile_path = get_last_workfile(\n                workdir, file_template, workdir_data, extensions, True\n            )\n\n    if os.path.exists(last_workfile_path):\n        log.debug((\n            \"Workfiles for launch context does not exists\"\n            \" yet but path will be set.\"\n        ))\n    log.debug(\n        \"Setting last workfile path: {}\".format(last_workfile_path)\n    )\n\n    data[\"env\"][\"AVALON_LAST_WORKFILE\"] = last_workfile_path\n    data[\"last_workfile_path\"] = last_workfile_path\n\n\ndef should_start_last_workfile(\n    project_name, host_name, task_name, task_type, default_output=False\n):\n    \"\"\"Define if host should start last version workfile if possible.\n\n    Default output is `False`. Can be overridden with environment variable\n    `AVALON_OPEN_LAST_WORKFILE`, valid values without case sensitivity are\n    `\"0\", \"1\", \"true\", \"false\", \"yes\", \"no\"`.\n\n    Args:\n        project_name (str): Name of project.\n        host_name (str): Name of host which is launched. In avalon's\n            application context it's value stored in app definition under\n            key `\"application_dir\"`. Is not case sensitive.\n        task_name (str): Name of task which is used for launching the host.\n            Task name is not case sensitive.\n\n    Returns:\n        bool: True if host should start workfile.\n\n    \"\"\"\n\n    project_settings = get_project_settings(project_name)\n    profiles = (\n        project_settings\n        [\"global\"]\n        [\"tools\"]\n        [\"Workfiles\"]\n        [\"last_workfile_on_startup\"]\n    )\n\n    if not profiles:\n        return default_output\n\n    filter_data = {\n        \"tasks\": task_name,\n        \"task_types\": task_type,\n        \"hosts\": host_name\n    }\n    matching_item = filter_profiles(profiles, filter_data)\n\n    output = None\n    if matching_item:\n        output = matching_item.get(\"enabled\")\n\n    if output is None:\n        return default_output\n    return output\n\n\ndef should_workfile_tool_start(\n    project_name, host_name, task_name, task_type, default_output=False\n):\n    \"\"\"Define if host should start workfile tool at host launch.\n\n    Default output is `False`. Can be overridden with environment variable\n    `OPENPYPE_WORKFILE_TOOL_ON_START`, valid values without case sensitivity are\n    `\"0\", \"1\", \"true\", \"false\", \"yes\", \"no\"`.\n\n    Args:\n        project_name (str): Name of project.\n        host_name (str): Name of host which is launched. In avalon's\n            application context it's value stored in app definition under\n            key `\"application_dir\"`. Is not case sensitive.\n        task_name (str): Name of task which is used for launching the host.\n            Task name is not case sensitive.\n\n    Returns:\n        bool: True if host should start workfile.\n\n    \"\"\"\n\n    project_settings = get_project_settings(project_name)\n    profiles = (\n        project_settings\n        [\"global\"]\n        [\"tools\"]\n        [\"Workfiles\"]\n        [\"open_workfile_tool_on_startup\"]\n    )\n\n    if not profiles:\n        return default_output\n\n    filter_data = {\n        \"tasks\": task_name,\n        \"task_types\": task_type,\n        \"hosts\": host_name\n    }\n    matching_item = filter_profiles(profiles, filter_data)\n\n    output = None\n    if matching_item:\n        output = matching_item.get(\"enabled\")\n\n    if output is None:\n        return default_output\n    return output\n\n\ndef get_non_python_host_kwargs(kwargs, allow_console=True):\n    \"\"\"Explicit setting of kwargs for Popen for AE/PS/Harmony.\n\n    Expected behavior\n    - openpype_console opens window with logs\n    - openpype_gui has stdout/stderr available for capturing\n\n    Args:\n        kwargs (dict) or None\n        allow_console (bool): use False for inner Popen opening app itself or\n           it will open additional console (at least for Harmony)\n    \"\"\"\n\n    if kwargs is None:\n        kwargs = {}\n\n    if platform.system().lower() != \"windows\":\n        return kwargs\n\n    if AYON_SERVER_ENABLED:\n        executable_path = os.environ.get(\"AYON_EXECUTABLE\")\n    else:\n        executable_path = os.environ.get(\"OPENPYPE_EXECUTABLE\")\n\n    executable_filename = \"\"\n    if executable_path:\n        executable_filename = os.path.basename(executable_path)\n\n    if AYON_SERVER_ENABLED:\n        is_gui_executable = \"ayon_console\" not in executable_filename\n    else:\n        is_gui_executable = \"openpype_gui\" in executable_filename\n\n    if is_gui_executable:\n        kwargs.update({\n            \"creationflags\": subprocess.CREATE_NO_WINDOW,\n            \"stdout\": subprocess.DEVNULL,\n            \"stderr\": subprocess.DEVNULL\n        })\n    elif allow_console:\n        kwargs.update({\n            \"creationflags\": subprocess.CREATE_NEW_CONSOLE\n        })\n    return kwargs\n"
  },
  {
    "path": "openpype/lib/attribute_definitions.py",
    "content": "import os\nimport re\nimport collections\nimport uuid\nimport json\nimport copy\nfrom abc import ABCMeta, abstractmethod, abstractproperty\n\nimport six\nimport clique\n\n# Global variable which store attribute definitions by type\n#   - default types are registered on import\n_attr_defs_by_type = {}\n\n\ndef register_attr_def_class(cls):\n    \"\"\"Register attribute definition.\n\n    Currently are registered definitions used to deserialize data to objects.\n\n    Attrs:\n        cls (AbstractAttrDef): Non-abstract class to be registered with unique\n            'type' attribute.\n\n    Raises:\n        KeyError: When type was already registered.\n    \"\"\"\n\n    if cls.type in _attr_defs_by_type:\n        raise KeyError(\"Type \\\"{}\\\" was already registered\".format(cls.type))\n    _attr_defs_by_type[cls.type] = cls\n\n\ndef get_attributes_keys(attribute_definitions):\n    \"\"\"Collect keys from list of attribute definitions.\n\n    Args:\n        attribute_definitions (List[AbstractAttrDef]): Objects of attribute\n            definitions.\n\n    Returns:\n        Set[str]: Keys that will be created using passed attribute definitions.\n    \"\"\"\n\n    keys = set()\n    if not attribute_definitions:\n        return keys\n\n    for attribute_def in attribute_definitions:\n        if not isinstance(attribute_def, UIDef):\n            keys.add(attribute_def.key)\n    return keys\n\n\ndef get_default_values(attribute_definitions):\n    \"\"\"Receive default values for attribute definitions.\n\n    Args:\n        attribute_definitions (List[AbstractAttrDef]): Attribute definitions\n            for which default values should be collected.\n\n    Returns:\n        Dict[str, Any]: Default values for passet attribute definitions.\n    \"\"\"\n\n    output = {}\n    if not attribute_definitions:\n        return output\n\n    for attr_def in attribute_definitions:\n        # Skip UI definitions\n        if not isinstance(attr_def, UIDef):\n            output[attr_def.key] = attr_def.default\n    return output\n\n\nclass AbstractAttrDefMeta(ABCMeta):\n    \"\"\"Metaclass to validate existence of 'key' attribute.\n\n    Each object of `AbstractAttrDef` mus have defined 'key' attribute.\n    \"\"\"\n\n    def __call__(self, *args, **kwargs):\n        obj = super(AbstractAttrDefMeta, self).__call__(*args, **kwargs)\n        init_class = getattr(obj, \"__init__class__\", None)\n        if init_class is not AbstractAttrDef:\n            raise TypeError(\"{} super was not called in __init__.\".format(\n                type(obj)\n            ))\n        return obj\n\n\n@six.add_metaclass(AbstractAttrDefMeta)\nclass AbstractAttrDef(object):\n    \"\"\"Abstraction of attribute definition.\n\n    Each attribute definition must have implemented validation and\n    conversion method.\n\n    Attribute definition should have ability to return \"default\" value. That\n    can be based on passed data into `__init__` so is not abstracted to\n    attribute.\n\n    QUESTION:\n    How to force to set `key` attribute?\n\n    Args:\n        key (str): Under which key will be attribute value stored.\n        default (Any): Default value of an attribute.\n        label (str): Attribute label.\n        tooltip (str): Attribute tooltip.\n        is_label_horizontal (bool): UI specific argument. Specify if label is\n            next to value input or ahead.\n        hidden (bool): Will be item hidden (for UI purposes).\n        disabled (bool): Item will be visible but disabled (for UI purposes).\n    \"\"\"\n\n    type_attributes = []\n\n    is_value_def = True\n\n    def __init__(\n        self,\n        key,\n        default,\n        label=None,\n        tooltip=None,\n        is_label_horizontal=None,\n        hidden=False,\n        disabled=False\n    ):\n        if is_label_horizontal is None:\n            is_label_horizontal = True\n\n        if hidden is None:\n            hidden = False\n\n        self.key = key\n        self.label = label\n        self.tooltip = tooltip\n        self.default = default\n        self.is_label_horizontal = is_label_horizontal\n        self.hidden = hidden\n        self.disabled = disabled\n        self._id = uuid.uuid4().hex\n\n        self.__init__class__ = AbstractAttrDef\n\n    @property\n    def id(self):\n        return self._id\n\n    def __eq__(self, other):\n        if not isinstance(other, self.__class__):\n            return False\n        return (\n            self.key == other.key\n            and self.hidden == other.hidden\n            and self.default == other.default\n            and self.disabled == other.disabled\n        )\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    @abstractproperty\n    def type(self):\n        \"\"\"Attribute definition type also used as identifier of class.\n\n        Returns:\n            str: Type of attribute definition.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def convert_value(self, value):\n        \"\"\"Convert value to a valid one.\n\n        Convert passed value to a valid type. Use default if value can't be\n        converted.\n        \"\"\"\n\n        pass\n\n    def serialize(self):\n        \"\"\"Serialize object to data so it's possible to recreate it.\n\n        Returns:\n            Dict[str, Any]: Serialized object that can be passed to\n                'deserialize' method.\n        \"\"\"\n\n        data = {\n            \"type\": self.type,\n            \"key\": self.key,\n            \"label\": self.label,\n            \"tooltip\": self.tooltip,\n            \"default\": self.default,\n            \"is_label_horizontal\": self.is_label_horizontal,\n            \"hidden\": self.hidden,\n            \"disabled\": self.disabled\n        }\n        for attr in self.type_attributes:\n            data[attr] = getattr(self, attr)\n        return data\n\n    @classmethod\n    def deserialize(cls, data):\n        \"\"\"Recreate object from data.\n\n        Data can be received using 'serialize' method.\n        \"\"\"\n\n        return cls(**data)\n\n\n# -----------------------------------------\n# UI attribute definitoins won't hold value\n# -----------------------------------------\n\nclass UIDef(AbstractAttrDef):\n    is_value_def = False\n\n    def __init__(self, key=None, default=None, *args, **kwargs):\n        super(UIDef, self).__init__(key, default, *args, **kwargs)\n\n    def convert_value(self, value):\n        return value\n\n\nclass UISeparatorDef(UIDef):\n    type = \"separator\"\n\n\nclass UILabelDef(UIDef):\n    type = \"label\"\n\n    def __init__(self, label, key=None):\n        super(UILabelDef, self).__init__(label=label, key=key)\n\n    def __eq__(self, other):\n        if not super(UILabelDef, self).__eq__(other):\n            return False\n        return self.label == other.label\n\n\n# ---------------------------------------\n# Attribute defintioins should hold value\n# ---------------------------------------\n\nclass UnknownDef(AbstractAttrDef):\n    \"\"\"Definition is not known because definition is not available.\n\n    This attribute can be used to keep existing data unchanged but does not\n    have known definition of type.\n    \"\"\"\n\n    type = \"unknown\"\n\n    def __init__(self, key, default=None, **kwargs):\n        kwargs[\"default\"] = default\n        super(UnknownDef, self).__init__(key, **kwargs)\n\n    def convert_value(self, value):\n        return value\n\n\nclass HiddenDef(AbstractAttrDef):\n    \"\"\"Hidden value of Any type.\n\n    This attribute can be used for UI purposes to pass values related\n    to other attributes (e.g. in multi-page UIs).\n\n    Keep in mind the value should be possible to parse by json parser.\n    \"\"\"\n\n    type = \"hidden\"\n\n    def __init__(self, key, default=None, **kwargs):\n        kwargs[\"default\"] = default\n        kwargs[\"hidden\"] = True\n        super(UnknownDef, self).__init__(key, **kwargs)\n\n    def convert_value(self, value):\n        return value\n\n\nclass NumberDef(AbstractAttrDef):\n    \"\"\"Number definition.\n\n    Number can have defined minimum/maximum value and decimal points. Value\n    is integer if decimals are 0.\n\n    Args:\n        minimum(int, float): Minimum possible value.\n        maximum(int, float): Maximum possible value.\n        decimals(int): Maximum decimal points of value.\n        default(int, float): Default value for conversion.\n    \"\"\"\n\n    type = \"number\"\n    type_attributes = [\n        \"minimum\",\n        \"maximum\",\n        \"decimals\"\n    ]\n\n    def __init__(\n        self, key, minimum=None, maximum=None, decimals=None, default=None,\n        **kwargs\n    ):\n        minimum = 0 if minimum is None else minimum\n        maximum = 999999 if maximum is None else maximum\n        # Swap min/max when are passed in opposited order\n        if minimum > maximum:\n            maximum, minimum = minimum, maximum\n\n        if default is None:\n            default = 0\n\n        elif not isinstance(default, (int, float)):\n            raise TypeError((\n                \"'default' argument must be 'int' or 'float', not '{}'\"\n            ).format(type(default)))\n\n        # Fix default value by mim/max values\n        if default < minimum:\n            default = minimum\n\n        elif default > maximum:\n            default = maximum\n\n        super(NumberDef, self).__init__(key, default=default, **kwargs)\n\n        self.minimum = minimum\n        self.maximum = maximum\n        self.decimals = 0 if decimals is None else decimals\n\n    def __eq__(self, other):\n        if not super(NumberDef, self).__eq__(other):\n            return False\n\n        return (\n            self.decimals == other.decimals\n            and self.maximum == other.maximum\n            and self.maximum == other.maximum\n        )\n\n    def convert_value(self, value):\n        if isinstance(value, six.string_types):\n            try:\n                value = float(value)\n            except Exception:\n                pass\n\n        if not isinstance(value, (int, float)):\n            return self.default\n\n        if self.decimals == 0:\n            return int(value)\n        return round(float(value), self.decimals)\n\n\nclass TextDef(AbstractAttrDef):\n    \"\"\"Text definition.\n\n    Text can have multiline option so endline characters are allowed regex\n    validation can be applied placeholder for UI purposes and default value.\n\n    Regex validation is not part of attribute implemntentation.\n\n    Args:\n        multiline(bool): Text has single or multiline support.\n        regex(str, re.Pattern): Regex validation.\n        placeholder(str): UI placeholder for attribute.\n        default(str, None): Default value. Empty string used when not defined.\n    \"\"\"\n\n    type = \"text\"\n    type_attributes = [\n        \"multiline\",\n        \"placeholder\",\n    ]\n\n    def __init__(\n        self, key, multiline=None, regex=None, placeholder=None, default=None,\n        **kwargs\n    ):\n        if default is None:\n            default = \"\"\n\n        super(TextDef, self).__init__(key, default=default, **kwargs)\n\n        if multiline is None:\n            multiline = False\n\n        elif not isinstance(default, six.string_types):\n            raise TypeError((\n                \"'default' argument must be a {}, not '{}'\"\n            ).format(six.string_types, type(default)))\n\n        if isinstance(regex, six.string_types):\n            regex = re.compile(regex)\n\n        self.multiline = multiline\n        self.placeholder = placeholder\n        self.regex = regex\n\n    def __eq__(self, other):\n        if not super(TextDef, self).__eq__(other):\n            return False\n\n        return (\n            self.multiline == other.multiline\n            and self.regex == other.regex\n        )\n\n    def convert_value(self, value):\n        if isinstance(value, six.string_types):\n            return value\n        return self.default\n\n    def serialize(self):\n        data = super(TextDef, self).serialize()\n        data[\"regex\"] = self.regex.pattern\n        return data\n\n\nclass EnumDef(AbstractAttrDef):\n    \"\"\"Enumeration of items.\n\n    Enumeration of single item from items. Or list of items if multiselection\n    is enabled.\n\n    Args:\n        items (Union[list[str], list[dict[str, Any]]): Items definition that\n            can be converted using 'prepare_enum_items'.\n        default (Optional[Any]): Default value. Must be one key(value) from\n            passed items or list of values for multiselection.\n        multiselection (Optional[bool]): If True, multiselection is allowed.\n            Output is list of selected items.\n    \"\"\"\n\n    type = \"enum\"\n\n    def __init__(\n        self, key, items, default=None, multiselection=False, **kwargs\n    ):\n        if not items:\n            raise ValueError((\n                \"Empty 'items' value. {} must have\"\n                \" defined values on initialization.\"\n            ).format(self.__class__.__name__))\n\n        items = self.prepare_enum_items(items)\n        item_values = [item[\"value\"] for item in items]\n        item_values_set = set(item_values)\n        if multiselection:\n            if default is None:\n                default = []\n            default = list(item_values_set.intersection(default))\n\n        elif default not in item_values:\n            default = next(iter(item_values), None)\n\n        super(EnumDef, self).__init__(key, default=default, **kwargs)\n\n        self.items = items\n        self._item_values = item_values_set\n        self.multiselection = multiselection\n\n    def __eq__(self, other):\n        if not super(EnumDef, self).__eq__(other):\n            return False\n\n        return (\n            self.items == other.items\n            and self.multiselection == other.multiselection\n        )\n\n    def convert_value(self, value):\n        if not self.multiselection:\n            if value in self._item_values:\n                return value\n            return self.default\n\n        if value is None:\n            return copy.deepcopy(self.default)\n        return list(self._item_values.intersection(value))\n\n    def serialize(self):\n        data = super(EnumDef, self).serialize()\n        data[\"items\"] = copy.deepcopy(self.items)\n        data[\"multiselection\"] = self.multiselection\n        return data\n\n    @staticmethod\n    def prepare_enum_items(items):\n        \"\"\"Convert items to unified structure.\n\n        Output is a list where each item is dictionary with 'value'\n        and 'label'.\n\n        ```python\n        # Example output\n        [\n            {\"label\": \"Option 1\", \"value\": 1},\n            {\"label\": \"Option 2\", \"value\": 2},\n            {\"label\": \"Option 3\", \"value\": 3}\n        ]\n        ```\n\n        Args:\n            items (Union[Dict[str, Any], List[Any], List[Dict[str, Any]]): The\n                items to convert.\n\n        Returns:\n            List[Dict[str, Any]]: Unified structure of items.\n        \"\"\"\n\n        output = []\n        if isinstance(items, dict):\n            for value, label in items.items():\n                output.append({\"label\": label, \"value\": value})\n\n        elif isinstance(items, (tuple, list, set)):\n            for item in items:\n                if isinstance(item, dict):\n                    # Validate if 'value' is available\n                    if \"value\" not in item:\n                        raise KeyError(\"Item does not contain 'value' key.\")\n\n                    if \"label\" not in item:\n                        item[\"label\"] = str(item[\"value\"])\n                elif isinstance(item, (list, tuple)):\n                    if len(item) == 2:\n                        value, label = item\n                    elif len(item) == 1:\n                        value = item[0]\n                        label = str(value)\n                    else:\n                        raise ValueError((\n                            \"Invalid items count {}.\"\n                            \" Expected 1 or 2. Value: {}\"\n                        ).format(len(item), str(item)))\n\n                    item = {\"label\": label, \"value\": value}\n                else:\n                    item = {\"label\": str(item), \"value\": item}\n                output.append(item)\n\n        else:\n            raise TypeError(\n                \"Unknown type for enum items '{}'\".format(type(items))\n            )\n\n        return output\n\n\nclass BoolDef(AbstractAttrDef):\n    \"\"\"Boolean representation.\n\n    Args:\n        default(bool): Default value. Set to `False` if not defined.\n    \"\"\"\n\n    type = \"bool\"\n\n    def __init__(self, key, default=None, **kwargs):\n        if default is None:\n            default = False\n        super(BoolDef, self).__init__(key, default=default, **kwargs)\n\n    def convert_value(self, value):\n        if isinstance(value, bool):\n            return value\n        return self.default\n\n\nclass FileDefItem(object):\n    def __init__(\n        self, directory, filenames, frames=None, template=None\n    ):\n        self.directory = directory\n\n        self.filenames = []\n        self.is_sequence = False\n        self.template = None\n        self.frames = []\n        self.is_empty = True\n\n        self.set_filenames(filenames, frames, template)\n\n    def __str__(self):\n        return json.dumps(self.to_dict())\n\n    def __repr__(self):\n        if self.is_empty:\n            filename = \"< empty >\"\n        elif self.is_sequence:\n            filename = self.template\n        else:\n            filename = self.filenames[0]\n\n        return \"<{}: \\\"{}\\\">\".format(\n            self.__class__.__name__,\n            os.path.join(self.directory, filename)\n        )\n\n    @property\n    def label(self):\n        if self.is_empty:\n            return None\n\n        if not self.is_sequence:\n            return self.filenames[0]\n\n        frame_start = self.frames[0]\n        filename_template = os.path.basename(self.template)\n        if len(self.frames) == 1:\n            return \"{} [{}]\".format(filename_template, frame_start)\n\n        frame_end = self.frames[-1]\n        expected_len = (frame_end - frame_start) + 1\n        if expected_len == len(self.frames):\n            return \"{} [{}-{}]\".format(\n                filename_template, frame_start, frame_end\n            )\n\n        ranges = []\n        _frame_start = None\n        _frame_end = None\n        for frame in range(frame_start, frame_end + 1):\n            if frame not in self.frames:\n                add_to_ranges = _frame_start is not None\n            elif _frame_start is None:\n                _frame_start = _frame_end = frame\n                add_to_ranges = frame == frame_end\n            else:\n                _frame_end = frame\n                add_to_ranges = frame == frame_end\n\n            if add_to_ranges:\n                if _frame_start != _frame_end:\n                    _range = \"{}-{}\".format(_frame_start, _frame_end)\n                else:\n                    _range = str(_frame_start)\n                ranges.append(_range)\n                _frame_start = _frame_end = None\n        return \"{} [{}]\".format(\n            filename_template, \",\".join(ranges)\n        )\n\n    def split_sequence(self):\n        if not self.is_sequence:\n            raise ValueError(\"Cannot split single file item\")\n\n        paths = [\n            os.path.join(self.directory, filename)\n            for filename in self.filenames\n        ]\n        return self.from_paths(paths, False)\n\n    @property\n    def ext(self):\n        if self.is_empty:\n            return None\n        _, ext = os.path.splitext(self.filenames[0])\n        if ext:\n            return ext\n        return None\n\n    @property\n    def lower_ext(self):\n        ext = self.ext\n        if ext is not None:\n            return ext.lower()\n        return ext\n\n    @property\n    def is_dir(self):\n        if self.is_empty:\n            return False\n\n        # QUESTION a better way how to define folder (in init argument?)\n        if self.ext:\n            return False\n        return True\n\n    def set_directory(self, directory):\n        self.directory = directory\n\n    def set_filenames(self, filenames, frames=None, template=None):\n        if frames is None:\n            frames = []\n        is_sequence = False\n        if frames:\n            is_sequence = True\n\n        if is_sequence and not template:\n            raise ValueError(\"Missing template for sequence\")\n\n        self.is_empty = len(filenames) == 0\n        self.filenames = filenames\n        self.template = template\n        self.frames = frames\n        self.is_sequence = is_sequence\n\n    @classmethod\n    def create_empty_item(cls):\n        return cls(\"\", \"\")\n\n    @classmethod\n    def from_value(cls, value, allow_sequences):\n        \"\"\"Convert passed value to FileDefItem objects.\n\n        Returns:\n            list: Created FileDefItem objects.\n        \"\"\"\n\n        # Convert single item to iterable\n        if not isinstance(value, (list, tuple, set)):\n            value = [value]\n\n        output = []\n        str_filepaths = []\n        for item in value:\n            if isinstance(item, dict):\n                item = cls.from_dict(item)\n\n            if isinstance(item, FileDefItem):\n                if not allow_sequences and item.is_sequence:\n                    output.extend(item.split_sequence())\n                else:\n                    output.append(item)\n\n            elif isinstance(item, six.string_types):\n                str_filepaths.append(item)\n            else:\n                raise TypeError(\n                    \"Unknown type \\\"{}\\\". Can't convert to {}\".format(\n                        str(type(item)), cls.__name__\n                    )\n                )\n\n        if str_filepaths:\n            output.extend(cls.from_paths(str_filepaths, allow_sequences))\n\n        return output\n\n    @classmethod\n    def from_dict(cls, data):\n        return cls(\n            data[\"directory\"],\n            data[\"filenames\"],\n            data.get(\"frames\"),\n            data.get(\"template\")\n        )\n\n    @classmethod\n    def from_paths(cls, paths, allow_sequences):\n        filenames_by_dir = collections.defaultdict(list)\n        for path in paths:\n            normalized = os.path.normpath(path)\n            directory, filename = os.path.split(normalized)\n            filenames_by_dir[directory].append(filename)\n\n        output = []\n        for directory, filenames in filenames_by_dir.items():\n            if allow_sequences:\n                cols, remainders = clique.assemble(filenames)\n            else:\n                cols = []\n                remainders = filenames\n\n            for remainder in remainders:\n                output.append(cls(directory, [remainder]))\n\n            for col in cols:\n                frames = list(col.indexes)\n                paths = [filename for filename in col]\n                template = col.format(\"{head}{padding}{tail}\")\n\n                output.append(cls(\n                    directory, paths, frames, template\n                ))\n\n        return output\n\n    def to_dict(self):\n        output = {\n            \"is_sequence\": self.is_sequence,\n            \"directory\": self.directory,\n            \"filenames\": list(self.filenames),\n        }\n        if self.is_sequence:\n            output.update({\n                \"template\": self.template,\n                \"frames\": list(sorted(self.frames)),\n            })\n\n        return output\n\n\nclass FileDef(AbstractAttrDef):\n    \"\"\"File definition.\n    It is possible to define filters of allowed file extensions and if supports\n    folders.\n    Args:\n        single_item(bool): Allow only single path item.\n        folders(bool): Allow folder paths.\n        extensions(List[str]): Allow files with extensions. Empty list will\n            allow all extensions and None will disable files completely.\n        extensions_label(str): Custom label shown instead of extensions in UI.\n        default(str, List[str]): Default value.\n    \"\"\"\n\n    type = \"path\"\n    type_attributes = [\n        \"single_item\",\n        \"folders\",\n        \"extensions\",\n        \"allow_sequences\",\n        \"extensions_label\",\n    ]\n\n    def __init__(\n        self, key, single_item=True, folders=None, extensions=None,\n        allow_sequences=True, extensions_label=None, default=None, **kwargs\n    ):\n        if folders is None and extensions is None:\n            folders = True\n            extensions = []\n\n        if default is None:\n            if single_item:\n                default = FileDefItem.create_empty_item().to_dict()\n            else:\n                default = []\n        else:\n            if single_item:\n                if isinstance(default, dict):\n                    FileDefItem.from_dict(default)\n\n                elif isinstance(default, six.string_types):\n                    default = FileDefItem.from_paths([default.strip()])[0]\n\n                else:\n                    raise TypeError((\n                        \"'default' argument must be 'str' or 'dict' not '{}'\"\n                    ).format(type(default)))\n\n            else:\n                if not isinstance(default, (tuple, list, set)):\n                    raise TypeError((\n                        \"'default' argument must be 'list', 'tuple' or 'set'\"\n                        \", not '{}'\"\n                    ).format(type(default)))\n\n        # Change horizontal label\n        is_label_horizontal = kwargs.get(\"is_label_horizontal\")\n        if is_label_horizontal is None:\n            kwargs[\"is_label_horizontal\"] = False\n\n        self.single_item = single_item\n        self.folders = folders\n        self.extensions = set(extensions)\n        self.allow_sequences = allow_sequences\n        self.extensions_label = extensions_label\n        super(FileDef, self).__init__(key, default=default, **kwargs)\n\n    def __eq__(self, other):\n        if not super(FileDef, self).__eq__(other):\n            return False\n\n        return (\n            self.single_item == other.single_item\n            and self.folders == other.folders\n            and self.extensions == other.extensions\n            and self.allow_sequences == other.allow_sequences\n        )\n\n    def convert_value(self, value):\n        if isinstance(value, six.string_types) or isinstance(value, dict):\n            value = [value]\n\n        if isinstance(value, (tuple, list, set)):\n            string_paths = []\n            dict_items = []\n            for item in value:\n                if isinstance(item, six.string_types):\n                    string_paths.append(item.strip())\n                elif isinstance(item, dict):\n                    try:\n                        FileDefItem.from_dict(item)\n                        dict_items.append(item)\n                    except (ValueError, KeyError):\n                        pass\n\n            if string_paths:\n                file_items = FileDefItem.from_paths(string_paths)\n                dict_items.extend([\n                    file_item.to_dict()\n                    for file_item in file_items\n                ])\n\n            if not self.single_item:\n                return dict_items\n\n            if not dict_items:\n                return self.default\n            return dict_items[0]\n\n        if self.single_item:\n            return FileDefItem.create_empty_item().to_dict()\n        return []\n\n\ndef serialize_attr_def(attr_def):\n    \"\"\"Serialize attribute definition to data.\n\n    Args:\n        attr_def (AbstractAttrDef): Attribute definition to serialize.\n\n    Returns:\n        Dict[str, Any]: Serialized data.\n    \"\"\"\n\n    return attr_def.serialize()\n\n\ndef serialize_attr_defs(attr_defs):\n    \"\"\"Serialize attribute definitions to data.\n\n    Args:\n        attr_defs (List[AbstractAttrDef]): Attribute definitions to serialize.\n\n    Returns:\n        List[Dict[str, Any]]: Serialized data.\n    \"\"\"\n\n    return [\n        serialize_attr_def(attr_def)\n        for attr_def in attr_defs\n    ]\n\n\ndef deserialize_attr_def(attr_def_data):\n    \"\"\"Deserialize attribute definition from data.\n\n    Args:\n        attr_def (Dict[str, Any]): Attribute definition data to deserialize.\n    \"\"\"\n\n    attr_type = attr_def_data.pop(\"type\")\n    cls = _attr_defs_by_type[attr_type]\n    return cls.deserialize(attr_def_data)\n\n\ndef deserialize_attr_defs(attr_defs_data):\n    \"\"\"Deserialize attribute definitions.\n\n    Args:\n        List[Dict[str, Any]]: List of attribute definitions.\n    \"\"\"\n\n    return [\n        deserialize_attr_def(attr_def_data)\n        for attr_def_data in attr_defs_data\n    ]\n\n\n# Register attribute definitions\nfor _attr_class in (\n    UISeparatorDef,\n    UILabelDef,\n    UnknownDef,\n    NumberDef,\n    TextDef,\n    EnumDef,\n    BoolDef,\n    FileDef\n):\n    register_attr_def_class(_attr_class)\n"
  },
  {
    "path": "openpype/lib/connections.py",
    "content": "import requests\nimport os\n\n\ndef requests_post(*args, **kwargs):\n    \"\"\"Wrap request post method.\n\n    Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment\n    variable is found. This is useful when Deadline server is\n    running with self-signed certificates and its certificate is not\n    added to trusted certificates on client machines.\n\n    Warning:\n        Disabling SSL certificate validation is defeating one line\n        of defense SSL is providing, and it is not recommended.\n\n    \"\"\"\n    if \"verify\" not in kwargs:\n        kwargs[\"verify\"] = not os.getenv(\"OPENPYPE_DONT_VERIFY_SSL\", True)\n    return requests.post(*args, **kwargs)\n\n\ndef requests_get(*args, **kwargs):\n    \"\"\"Wrap request get method.\n\n    Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment\n    variable is found. This is useful when Deadline server is\n    running with self-signed certificates and its certificate is not\n    added to trusted certificates on client machines.\n\n    Warning:\n        Disabling SSL certificate validation is defeating one line\n        of defense SSL is providing, and it is not recommended.\n\n    \"\"\"\n    if \"verify\" not in kwargs:\n        kwargs[\"verify\"] = not os.getenv(\"OPENPYPE_DONT_VERIFY_SSL\", True)\n    return requests.get(*args, **kwargs)\n"
  },
  {
    "path": "openpype/lib/dateutils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Get configuration data.\"\"\"\nimport datetime\n\n\ndef get_datetime_data(datetime_obj=None):\n    \"\"\"Returns current datetime data as dictionary.\n\n    Args:\n        datetime_obj (datetime): Specific datetime object\n\n    Returns:\n        dict: prepared date & time data\n\n    Available keys:\n        \"d\" - <Day of month number> in shortest possible way.\n        \"dd\" - <Day of month number> with 2 digits.\n        \"ddd\" - <Week day name> shortened week day. e.g.: `Mon`, ...\n        \"dddd\" - <Week day name> full name of week day. e.g.: `Monday`, ...\n        \"m\" - <Month number> in shortest possible way. e.g.: `1` if January\n        \"mm\" - <Month number> with 2 digits.\n        \"mmm\" - <Month name> shortened month name. e.g.: `Jan`, ...\n        \"mmmm\" - <Month name> full month name. e.g.: `January`, ...\n        \"yy\" - <Year number> shortened year. e.g.: `19`, `20`, ...\n        \"yyyy\" - <Year number> full year. e.g.: `2019`, `2020`, ...\n        \"H\" - <Hours number 24-hour> shortened hours.\n        \"HH\" - <Hours number 24-hour> with 2 digits.\n        \"h\" - <Hours number 12-hour> shortened hours.\n        \"hh\" - <Hours number 12-hour> with 2 digits.\n        \"ht\" - <Midday type> AM or PM.\n        \"M\" - <Minutes number> shortened minutes.\n        \"MM\" - <Minutes number> with 2 digits.\n        \"S\" - <Seconds number> shortened seconds.\n        \"SS\" - <Seconds number> with 2 digits.\n    \"\"\"\n\n    if not datetime_obj:\n        datetime_obj = datetime.datetime.now()\n\n    year = datetime_obj.strftime(\"%Y\")\n\n    month = datetime_obj.strftime(\"%m\")\n    month_name_full = datetime_obj.strftime(\"%B\")\n    month_name_short = datetime_obj.strftime(\"%b\")\n    day = datetime_obj.strftime(\"%d\")\n\n    weekday_full = datetime_obj.strftime(\"%A\")\n    weekday_short = datetime_obj.strftime(\"%a\")\n\n    hours = datetime_obj.strftime(\"%H\")\n    hours_midday = datetime_obj.strftime(\"%I\")\n    hour_midday_type = datetime_obj.strftime(\"%p\")\n    minutes = datetime_obj.strftime(\"%M\")\n    seconds = datetime_obj.strftime(\"%S\")\n\n    return {\n        \"d\": str(int(day)),\n        \"dd\": str(day),\n        \"ddd\": weekday_short,\n        \"dddd\": weekday_full,\n        \"m\": str(int(month)),\n        \"mm\": str(month),\n        \"mmm\": month_name_short,\n        \"mmmm\": month_name_full,\n        \"yy\": str(year[2:]),\n        \"yyyy\": str(year),\n        \"H\": str(int(hours)),\n        \"HH\": str(hours),\n        \"h\": str(int(hours_midday)),\n        \"hh\": str(hours_midday),\n        \"ht\": hour_midday_type,\n        \"M\": str(int(minutes)),\n        \"MM\": str(minutes),\n        \"S\": str(int(seconds)),\n        \"SS\": str(seconds),\n    }\n\n\ndef get_timestamp(datetime_obj=None):\n    \"\"\"Get standardized timestamp from datetime object.\n\n    Args:\n        datetime_obj (datetime.datetime): Object of datetime. Current time\n            is used if not passed.\n    \"\"\"\n\n    if datetime_obj is None:\n        datetime_obj = datetime.datetime.now()\n    return datetime_obj.strftime(\n        \"%Y%m%dT%H%M%SZ\"\n    )\n\n\ndef get_formatted_current_time():\n    return get_timestamp()\n"
  },
  {
    "path": "openpype/lib/env_tools.py",
    "content": "import os\n\n\ndef env_value_to_bool(env_key=None, value=None, default=False):\n    \"\"\"Convert environment variable value to boolean.\n\n    Function is based on value of the environemt variable. Value is lowered\n    so function is not case sensitive.\n\n    Returns:\n        bool: If value match to one of [\"true\", \"yes\", \"1\"] result if True\n            but if value match to [\"false\", \"no\", \"0\"] result is False else\n            default value is returned.\n    \"\"\"\n    if value is None and env_key is None:\n        return default\n\n    if value is None:\n        value = os.environ.get(env_key)\n\n    if value is not None:\n        value = str(value).lower()\n        if value in (\"true\", \"yes\", \"1\", \"on\"):\n            return True\n        elif value in (\"false\", \"no\", \"0\", \"off\"):\n            return False\n    return default\n\n\ndef get_paths_from_environ(env_key=None, env_value=None, return_first=False):\n    \"\"\"Return existing paths from specific environment variable.\n\n    Args:\n        env_key (str): Environment key where should look for paths.\n        env_value (str): Value of environment variable. Argument `env_key` is\n            skipped if this argument is entered.\n        return_first (bool): Return first found value or return list of found\n            paths. `None` or empty list returned if nothing found.\n\n    Returns:\n        str, list, None: Result of found path/s.\n    \"\"\"\n    existing_paths = []\n    if not env_key and not env_value:\n        if return_first:\n            return None\n        return existing_paths\n\n    if env_value is None:\n        env_value = os.environ.get(env_key) or \"\"\n\n    path_items = env_value.split(os.pathsep)\n    for path in path_items:\n        # Skip empty string\n        if not path:\n            continue\n        # Normalize path\n        path = os.path.normpath(path)\n        # Check if path exists\n        if os.path.exists(path):\n            # Return path if `return_first` is set to True\n            if return_first:\n                return path\n            # Store path\n            existing_paths.append(path)\n\n    # Return None if none of paths exists\n    if return_first:\n        return None\n    # Return all existing paths from environment variable\n    return existing_paths\n"
  },
  {
    "path": "openpype/lib/events.py",
    "content": "\"\"\"Events holding data about specific event.\"\"\"\nimport os\nimport re\nimport copy\nimport inspect\nimport collections\nimport logging\nimport weakref\nfrom uuid import uuid4\n\nfrom .python_2_comp import WeakMethod\nfrom .python_module_tools import is_func_signature_supported\n\n\nclass MissingEventSystem(Exception):\n    pass\n\n\ndef _get_func_ref(func):\n    if inspect.ismethod(func):\n        return WeakMethod(func)\n    return weakref.ref(func)\n\n\ndef _get_func_info(func):\n    path = \"<unknown path>\"\n    if func is None:\n        return \"<unknown>\", path\n\n    if hasattr(func, \"__name__\"):\n        name = func.__name__\n    else:\n        name = str(func)\n\n    # Get path to file and fallback to '<unknown path>' if fails\n    # NOTE This was added because of 'partial' functions which is handled,\n    #   but who knows what else can cause this to fail?\n    try:\n        path = os.path.abspath(inspect.getfile(func))\n    except TypeError:\n        pass\n\n    return name, path\n\n\nclass weakref_partial:\n    \"\"\"Partial function with weak reference to the wrapped function.\n\n    Can be used as 'functools.partial' but it will store weak reference to\n        function. That means that the function must be reference counted\n        to avoid garbage collecting the function itself.\n\n        When the referenced functions is garbage collected then calling the\n        weakref partial (no matter the args/kwargs passed) will do nothing.\n        It will fail silently, returning `None`. The `is_valid()` method can\n        be used to detect whether the reference is still valid.\n\n    Is useful for object methods. In that case the callback is\n        deregistered when object is destroyed.\n\n    Warnings:\n        Values passed as *args and **kwargs are stored strongly in memory.\n            That may \"keep alive\" objects that should be already destroyed.\n            It is recommended to pass only immutable objects like 'str',\n            'bool', 'int' etc.\n\n    Args:\n        func (Callable): Function to wrap.\n        *args: Arguments passed to the wrapped function.\n        **kwargs: Keyword arguments passed to the wrapped function.\n    \"\"\"\n\n    def __init__(self, func, *args, **kwargs):\n        self._func_ref = _get_func_ref(func)\n        self._args = args\n        self._kwargs = kwargs\n\n    def __call__(self, *args, **kwargs):\n        func = self._func_ref()\n        if func is None:\n            return\n\n        new_args = tuple(list(self._args) + list(args))\n        new_kwargs = dict(self._kwargs)\n        new_kwargs.update(kwargs)\n        return func(*new_args, **new_kwargs)\n\n    def get_func(self):\n        \"\"\"Get wrapped function.\n\n        Returns:\n            Union[Callable, None]: Wrapped function or None if it was\n                destroyed.\n        \"\"\"\n\n        return self._func_ref()\n\n    def is_valid(self):\n        \"\"\"Check if wrapped function is still valid.\n\n        Returns:\n            bool: Is wrapped function still valid.\n        \"\"\"\n\n        return self._func_ref() is not None\n\n    def validate_signature(self, *args, **kwargs):\n        \"\"\"Validate if passed arguments are supported by wrapped function.\n\n        Returns:\n            bool: Are passed arguments supported by wrapped function.\n        \"\"\"\n\n        func = self._func_ref()\n        if func is None:\n            return False\n\n        new_args = tuple(list(self._args) + list(args))\n        new_kwargs = dict(self._kwargs)\n        new_kwargs.update(kwargs)\n        return is_func_signature_supported(\n            func, *new_args, **new_kwargs\n        )\n\n\nclass EventCallback(object):\n    \"\"\"Callback registered to a topic.\n\n    The callback function is registered to a topic. Topic is a string which\n    may contain '*' that will be handled as \"any characters\".\n\n    # Examples:\n    - \"workfile.save\"   Callback will be triggered if the event topic is\n                        exactly \"workfile.save\" .\n    - \"workfile.*\"      Callback will be triggered an event topic starts with\n                        \"workfile.\" so \"workfile.save\" and \"workfile.open\"\n                        will trigger the callback.\n    - \"*\"               Callback will listen to all events.\n\n    Callback can be function or method. In both cases it should expect one\n    or none arguments. When 1 argument is expected then the processed 'Event'\n    object is passed in.\n\n    The callbacks are validated against their reference counter, that is\n        achieved using 'weakref' module. That means that the callback must\n        be stored in memory somewhere. e.g. lambda functions are not\n        supported as valid callback.\n\n    You can use 'weakref_partial' functions. In that case is partial object\n        stored in the callback object and reference counter is checked for\n        the wrapped function.\n\n    Args:\n        topic (str): Topic which will be listened.\n        func (Callable): Callback to a topic.\n        order (Union[int, None]): Order of callback. Lower number means higher\n            priority.\n\n    Raises:\n        TypeError: When passed function is not a callable object.\n    \"\"\"\n\n    def __init__(self, topic, func, order):\n        if not callable(func):\n            raise TypeError((\n                \"Registered callback is not callable. \\\"{}\\\"\"\n            ).format(str(func)))\n\n        self._validate_order(order)\n\n        self._log = None\n        self._topic = topic\n        self._order = order\n        self._enabled = True\n        # Replace '*' with any character regex and escape rest of text\n        #   - when callback is registered for '*' topic it will receive all\n        #       events\n        #   - it is possible to register to a partial topis 'my.event.*'\n        #       - it will receive all matching event topics\n        #           e.g. 'my.event.start' and 'my.event.end'\n        topic_regex_str = \"^{}$\".format(\n            \".+\".join(\n                re.escape(part)\n                for part in topic.split(\"*\")\n            )\n        )\n        topic_regex = re.compile(topic_regex_str)\n        self._topic_regex = topic_regex\n\n        # Callback function prep\n        if isinstance(func, weakref_partial):\n            partial_func = func\n            (name, path) = _get_func_info(func.get_func())\n            func_ref = None\n            expect_args = partial_func.validate_signature(\"fake\")\n            expect_kwargs = partial_func.validate_signature(event=\"fake\")\n\n        else:\n            partial_func = None\n            (name, path) = _get_func_info(func)\n            # Convert callback into references\n            #   - deleted functions won't cause crashes\n            func_ref = _get_func_ref(func)\n\n            # Get expected arguments from function spec\n            # - positional arguments are always preferred\n            expect_args = is_func_signature_supported(func, \"fake\")\n            expect_kwargs = is_func_signature_supported(func, event=\"fake\")\n\n        self._func_ref = func_ref\n        self._partial_func = partial_func\n        self._ref_is_valid = True\n        self._expect_args = expect_args\n        self._expect_kwargs = expect_kwargs\n\n        self._name = name\n        self._path = path\n\n    def __repr__(self):\n        return \"< {} - {} > {}\".format(\n            self.__class__.__name__, self._name, self._path\n        )\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = logging.getLogger(self.__class__.__name__)\n        return self._log\n\n    @property\n    def is_ref_valid(self):\n        \"\"\"\n\n        Returns:\n            bool: Is reference to callback valid.\n        \"\"\"\n\n        self._validate_ref()\n        return self._ref_is_valid\n\n    def validate_ref(self):\n        \"\"\"Validate if reference to callback is valid.\n\n        Deprecated:\n            Reference is always live checkd with 'is_ref_valid'.\n        \"\"\"\n\n        # Trigger validate by getting 'is_valid'\n        _ = self.is_ref_valid\n\n    @property\n    def enabled(self):\n        \"\"\"Is callback enabled.\n\n        Returns:\n            bool: Is callback enabled.\n        \"\"\"\n\n        return self._enabled\n\n    def set_enabled(self, enabled):\n        \"\"\"Change if callback is enabled.\n\n        Args:\n            enabled (bool): Change enabled state of the callback.\n        \"\"\"\n\n        self._enabled = enabled\n\n    def deregister(self):\n        \"\"\"Calling this function will cause that callback will be removed.\"\"\"\n\n        self._ref_is_valid = False\n        self._partial_func = None\n        self._func_ref = None\n\n    def get_order(self):\n        \"\"\"Get callback order.\n\n        Returns:\n            Union[int, None]: Callback order.\n        \"\"\"\n\n        return self._order\n\n    def set_order(self, order):\n        \"\"\"Change callback order.\n\n        Args:\n            order (Union[int, None]): Order of callback. Lower number means\n                higher priority.\n        \"\"\"\n\n        self._validate_order(order)\n        self._order = order\n\n    order = property(get_order, set_order)\n\n    def topic_matches(self, topic):\n        \"\"\"Check if event topic matches callback's topic.\n\n        Args:\n            topic (str): Topic name.\n\n        Returns:\n            bool: Topic matches callback's topic.\n        \"\"\"\n\n        return self._topic_regex.match(topic)\n\n    def process_event(self, event):\n        \"\"\"Process event.\n\n        Args:\n            event(Event): Event that was triggered.\n        \"\"\"\n\n        # Skip if callback is not enabled\n        if not self._enabled:\n            return\n\n        # Get reference and skip if is not available\n        callback = self._get_callback()\n        if callback is None:\n            return\n\n        if not self.topic_matches(event.topic):\n            return\n\n        # Try to execute callback\n        try:\n            if self._expect_args:\n                callback(event)\n\n            elif self._expect_kwargs:\n                callback(event=event)\n\n            else:\n                callback()\n\n        except Exception:\n            self.log.warning(\n                \"Failed to execute event callback {}\".format(\n                    str(repr(self))\n                ),\n                exc_info=True\n            )\n\n    def _validate_order(self, order):\n        if isinstance(order, int):\n            return\n\n        raise TypeError(\n            \"Expected type 'int' got '{}'.\".format(str(type(order)))\n        )\n\n    def _get_callback(self):\n        if self._partial_func is not None:\n            return self._partial_func\n\n        if self._func_ref is not None:\n            return self._func_ref()\n        return None\n\n    def _validate_ref(self):\n        if self._ref_is_valid is False:\n            return\n\n        if self._func_ref is not None:\n            self._ref_is_valid = self._func_ref() is not None\n\n        elif self._partial_func is not None:\n            self._ref_is_valid = self._partial_func.is_valid()\n\n        else:\n            self._ref_is_valid = False\n\n        if not self._ref_is_valid:\n            self._func_ref = None\n            self._partial_func = None\n\n\n# Inherit from 'object' for Python 2 hosts\nclass Event(object):\n    \"\"\"Base event object.\n\n    Can be used for any event because is not specific. Only required argument\n    is topic which defines why event is happening and may be used for\n    filtering.\n\n    Arg:\n        topic (str): Identifier of event.\n        data (Any): Data specific for event. Dictionary is recommended.\n        source (str): Identifier of source.\n        event_system (EventSystem): Event system in which can be event\n            triggered.\n    \"\"\"\n\n    _data = {}\n\n    def __init__(self, topic, data=None, source=None, event_system=None):\n        self._id = str(uuid4())\n        self._topic = topic\n        if data is None:\n            data = {}\n        self._data = data\n        self._source = source\n        self._event_system = event_system\n\n    def __getitem__(self, key):\n        return self._data[key]\n\n    def get(self, key, *args, **kwargs):\n        return self._data.get(key, *args, **kwargs)\n\n    @property\n    def id(self):\n        return self._id\n\n    @property\n    def source(self):\n        \"\"\"Event's source used for triggering callbacks.\n\n        Returns:\n            Union[str, None]: Source string or None. Source is optional.\n        \"\"\"\n\n        return self._source\n\n    @property\n    def data(self):\n        return self._data\n\n    @property\n    def topic(self):\n        \"\"\"Event's topic used for triggering callbacks.\n\n        Returns:\n            str: Topic string.\n        \"\"\"\n\n        return self._topic\n\n    def emit(self):\n        \"\"\"Emit event and trigger callbacks.\"\"\"\n        if self._event_system is None:\n            raise MissingEventSystem(\n                \"Can't emit event {}. Does not have set event system.\".format(\n                    str(repr(self))\n                )\n            )\n        self._event_system.emit_event(self)\n\n    def to_data(self):\n        \"\"\"Convert Event object to data.\n\n        Returns:\n            Dict[str, Any]: Event data.\n        \"\"\"\n\n        return {\n            \"id\": self.id,\n            \"topic\": self.topic,\n            \"source\": self.source,\n            \"data\": copy.deepcopy(self.data)\n        }\n\n    @classmethod\n    def from_data(cls, event_data, event_system=None):\n        \"\"\"Create event from data.\n\n        Args:\n            event_data (Dict[str, Any]): Event data with defined keys. Can be\n                created using 'to_data' method.\n            event_system (EventSystem): System to which the event belongs.\n\n        Returns:\n            Event: Event with attributes from passed data.\n        \"\"\"\n\n        obj = cls(\n            event_data[\"topic\"],\n            event_data[\"data\"],\n            event_data[\"source\"],\n            event_system\n        )\n        obj._id = event_data[\"id\"]\n        return obj\n\n\nclass EventSystem(object):\n    \"\"\"Encapsulate event handling into an object.\n\n    System wraps registered callbacks and triggered events into single object,\n    so it is possible to create multiple independent systems that have their\n    topics and callbacks.\n\n    Callbacks are stored by order of their registration, but it is possible to\n    manually define order of callbacks using 'order' argument within\n    'add_callback'.\n    \"\"\"\n\n    default_order = 100\n\n    def __init__(self):\n        self._registered_callbacks = []\n\n    def add_callback(self, topic, callback, order=None):\n        \"\"\"Register callback in event system.\n\n        Args:\n            topic (str): Topic for EventCallback.\n            callback (Union[Callable, weakref_partial]): Function or method\n                that will be called when topic is triggered.\n            order (Optional[int]): Order of callback. Lower number means\n                higher priority.\n\n        Returns:\n            EventCallback: Created callback object which can be used to\n                stop listening.\n        \"\"\"\n\n        if order is None:\n            order = self.default_order\n\n        callback = EventCallback(topic, callback, order)\n        self._registered_callbacks.append(callback)\n        return callback\n\n    def create_event(self, topic, data, source):\n        \"\"\"Create new event which is bound to event system.\n\n        Args:\n            topic (str): Event topic.\n            data (dict): Data related to event.\n            source (str): Source of event.\n\n        Returns:\n            Event: Object of event.\n        \"\"\"\n\n        return Event(topic, data, source, self)\n\n    def emit(self, topic, data, source):\n        \"\"\"Create event based on passed data and emit it.\n\n        This is easiest way how to trigger event in an event system.\n\n        Args:\n            topic (str): Event topic.\n            data (dict): Data related to event.\n            source (str): Source of event.\n\n        Returns:\n            Event: Created and emitted event.\n        \"\"\"\n\n        event = self.create_event(topic, data, source)\n        event.emit()\n        return event\n\n    def emit_event(self, event):\n        \"\"\"Emit event object.\n\n        Args:\n            event (Event): Prepared event with topic and data.\n        \"\"\"\n\n        self._process_event(event)\n\n    def _process_event(self, event):\n        \"\"\"Process event topic and trigger callbacks.\n\n        Args:\n            event (Event): Prepared event with topic and data.\n        \"\"\"\n\n        callbacks = tuple(sorted(\n            self._registered_callbacks, key=lambda x: x.order\n        ))\n        for callback in callbacks:\n            callback.process_event(event)\n            if not callback.is_ref_valid:\n                self._registered_callbacks.remove(callback)\n\n\nclass QueuedEventSystem(EventSystem):\n    \"\"\"Events are automatically processed in queue.\n\n    If callback triggers another event, the event is not processed until\n    all callbacks of previous event are processed.\n\n    Allows to implement custom event process loop by changing 'auto_execute'.\n\n    Note:\n        This probably should be default behavior of 'EventSystem'. Changing it\n            now could cause problems in existing code.\n\n    Args:\n        auto_execute (Optional[bool]): If 'True', events are processed\n            automatically. Custom loop calling 'process_next_event'\n            must be implemented when set to 'False'.\n    \"\"\"\n\n    def __init__(self, auto_execute=True):\n        super(QueuedEventSystem, self).__init__()\n        self._event_queue = collections.deque()\n        self._current_event = None\n        self._auto_execute = auto_execute\n\n    def __len__(self):\n        return self.count()\n\n    def count(self):\n        \"\"\"Get number of events in queue.\n\n        Returns:\n            int: Number of events in queue.\n        \"\"\"\n\n        return len(self._event_queue)\n\n    def process_next_event(self):\n        \"\"\"Process next event in queue.\n\n        Should be used only if 'auto_execute' is set to 'False'. Only single\n            event is processed.\n\n        Returns:\n            Union[Event, None]: Processed event.\n        \"\"\"\n\n        if self._current_event is not None:\n            raise ValueError(\"An event is already in progress.\")\n\n        if not self._event_queue:\n            return None\n        event = self._event_queue.popleft()\n        self._current_event = event\n        self._process_event(event)\n        self._current_event = None\n        return event\n\n    def emit_event(self, event):\n        \"\"\"Emit event object.\n\n        Args:\n           event (Event): Prepared event with topic and data.\n        \"\"\"\n\n        if not self._auto_execute or self._current_event is not None:\n            self._event_queue.append(event)\n            return\n\n        self._event_queue.append(event)\n        while self._event_queue:\n            event = self._event_queue.popleft()\n            self._current_event = event\n            self._process_event(event)\n        self._current_event = None\n\n\nclass GlobalEventSystem:\n    \"\"\"Event system living in global scope of process.\n\n    This is primarily used in host implementation to trigger events\n    related to DCC changes or changes of context in the host implementation.\n    \"\"\"\n\n    _global_event_system = None\n\n    @classmethod\n    def get_global_event_system(cls):\n        if cls._global_event_system is None:\n            cls._global_event_system = EventSystem()\n        return cls._global_event_system\n\n    @classmethod\n    def add_callback(cls, topic, callback):\n        event_system = cls.get_global_event_system()\n        return event_system.add_callback(topic, callback)\n\n    @classmethod\n    def emit(cls, topic, data, source):\n        event_system = cls.get_global_event_system()\n        return event_system.emit(topic, data, source)\n\n\ndef register_event_callback(topic, callback):\n    \"\"\"Add callback that will be executed on specific topic.\n\n    Args:\n        topic(str): Topic on which will callback be triggered.\n        callback(function): Callback that will be triggered when a topic\n            is triggered. Callback should expect none or 1 argument where\n            `Event` object is passed.\n\n    Returns:\n        EventCallback: Object wrapping the callback. It can be used to\n            enable/disable listening to a topic or remove the callback from\n            the topic completely.\n    \"\"\"\n\n    return GlobalEventSystem.add_callback(topic, callback)\n\n\ndef emit_event(topic, data=None, source=None):\n    \"\"\"Emit event with topic and data.\n\n    Arg:\n        topic(str): Event's topic.\n        data(dict): Event's additional data. Optional.\n        source(str): Who emitted the topic. Optional.\n\n    Returns:\n        Event: Object of event that was emitted.\n    \"\"\"\n\n    return GlobalEventSystem.emit(topic, data, source)\n"
  },
  {
    "path": "openpype/lib/execute.py",
    "content": "import os\nimport sys\nimport subprocess\nimport platform\nimport json\nimport tempfile\n\nfrom openpype import AYON_SERVER_ENABLED\n\nfrom .log import Logger\nfrom .vendor_bin_utils import find_executable\n\nfrom .openpype_version import is_running_from_build\n\n# MSDN process creation flag (Windows only)\nCREATE_NO_WINDOW = 0x08000000\n\n\ndef execute(args,\n            silent=False,\n            cwd=None,\n            env=None,\n            shell=None):\n    \"\"\"Execute command as process.\n\n    This will execute given command as process, monitor its output\n    and log it appropriately.\n\n    .. seealso::\n\n        :mod:`subprocess` module in Python.\n\n    Args:\n        args (list): list of arguments passed to process.\n        silent (bool): control output of executed process.\n        cwd (str): current working directory for process.\n        env (dict): environment variables for process.\n        shell (bool): use shell to execute, default is no.\n\n    Returns:\n        int: return code of process\n\n    \"\"\"\n\n    log_levels = ['DEBUG:', 'INFO:', 'ERROR:', 'WARNING:', 'CRITICAL:']\n\n    log = Logger.get_logger('execute')\n    log.info(\"Executing ({})\".format(\" \".join(args)))\n    popen = subprocess.Popen(\n        args,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.STDOUT,\n        universal_newlines=True,\n        bufsize=1,\n        cwd=cwd,\n        env=env or os.environ,\n        shell=shell\n    )\n\n    # Blocks until finished\n    while True:\n        line = popen.stdout.readline()\n        if line == '':\n            break\n        if silent:\n            continue\n        line_test = False\n        for test_string in log_levels:\n            if line.startswith(test_string):\n                line_test = True\n                break\n        if not line_test:\n            print(line[:-1])\n\n    log.info(\"Execution is finishing up ...\")\n\n    popen.wait()\n    return popen.returncode\n\n\ndef run_subprocess(*args, **kwargs):\n    \"\"\"Convenience method for getting output errors for subprocess.\n\n    Output logged when process finish.\n\n    Entered arguments and keyword arguments are passed to subprocess Popen.\n\n    On windows are 'creationflags' filled with flags that should cause ignore\n    creation of new window.\n\n    Args:\n        *args: Variable length argument list passed to Popen.\n        **kwargs : Arbitrary keyword arguments passed to Popen. Is possible to\n            pass `logging.Logger` object under \"logger\" to use custom logger\n            for output.\n\n    Returns:\n        str: Full output of subprocess concatenated stdout and stderr.\n\n    Raises:\n        RuntimeError: Exception is raised if process finished with nonzero\n            return code.\n    \"\"\"\n\n    # Modify creation flags on windows to hide console window if in UI mode\n    if (\n        platform.system().lower() == \"windows\"\n        and \"creationflags\" not in kwargs\n        # shell=True already tries to hide the console window\n        # and passing these creationflags then shows the window again\n        # so we avoid it for shell=True cases\n        and kwargs.get(\"shell\") is not True\n    ):\n        kwargs[\"creationflags\"] = (\n            subprocess.CREATE_NEW_PROCESS_GROUP\n            | getattr(subprocess, \"DETACHED_PROCESS\", 0)\n            | getattr(subprocess, \"CREATE_NO_WINDOW\", 0)\n        )\n\n    # Get environents from kwarg or use current process environments if were\n    # not passed.\n    env = kwargs.get(\"env\") or os.environ\n    # Make sure environment contains only strings\n    filtered_env = {str(k): str(v) for k, v in env.items()}\n\n    # Use lib's logger if was not passed with kwargs.\n    logger = kwargs.pop(\"logger\", None)\n    if logger is None:\n        logger = Logger.get_logger(\"run_subprocess\")\n\n    # set overrides\n    kwargs[\"stdout\"] = kwargs.get(\"stdout\", subprocess.PIPE)\n    kwargs[\"stderr\"] = kwargs.get(\"stderr\", subprocess.PIPE)\n    kwargs[\"stdin\"] = kwargs.get(\"stdin\", subprocess.PIPE)\n    kwargs[\"env\"] = filtered_env\n\n    proc = subprocess.Popen(*args, **kwargs)\n\n    full_output = \"\"\n    _stdout, _stderr = proc.communicate()\n    if _stdout:\n        _stdout = _stdout.decode(\"utf-8\", errors=\"backslashreplace\")\n        full_output += _stdout\n        logger.debug(_stdout)\n\n    if _stderr:\n        _stderr = _stderr.decode(\"utf-8\", errors=\"backslashreplace\")\n        # Add additional line break if output already contains stdout\n        if full_output:\n            full_output += \"\\n\"\n        full_output += _stderr\n        logger.info(_stderr)\n\n    if proc.returncode != 0:\n        exc_msg = \"Executing arguments was not successful: \\\"{}\\\"\".format(args)\n        if _stdout:\n            exc_msg += \"\\n\\nOutput:\\n{}\".format(_stdout)\n\n        if _stderr:\n            exc_msg += \"Error:\\n{}\".format(_stderr)\n\n        raise RuntimeError(exc_msg)\n\n    return full_output\n\n\ndef clean_envs_for_ayon_process(env=None):\n    \"\"\"Modify environments that may affect ayon-launcher process.\n\n    Main reason to implement this function is to pop PYTHONPATH which may be\n    affected by in-host environments.\n\n    Args:\n        env (Optional[dict[str, str]]): Environment variables to modify.\n\n    Returns:\n        dict[str, str]: Environment variables for ayon process.\n    \"\"\"\n\n    if env is None:\n        env = os.environ\n\n    # Exclude some environment variables from a copy of the environment\n    env = env.copy()\n    for key in [\"PYTHONPATH\", \"PYTHONHOME\"]:\n        env.pop(key, None)\n\n    return env\n\n\ndef clean_envs_for_openpype_process(env=None):\n    \"\"\"Modify environments that may affect OpenPype process.\n\n    Main reason to implement this function is to pop PYTHONPATH which may be\n    affected by in-host environments.\n    \"\"\"\n\n    if AYON_SERVER_ENABLED:\n        return clean_envs_for_ayon_process(env=env)\n\n    if env is None:\n        env = os.environ\n\n    # Exclude some environment variables from a copy of the environment\n    env = env.copy()\n    for key in [\"PYTHONPATH\", \"PYTHONHOME\"]:\n        env.pop(key, None)\n\n    return env\n\n\ndef run_ayon_launcher_process(*args, **kwargs):\n    \"\"\"Execute OpenPype process with passed arguments and wait.\n\n    Wrapper for 'run_process' which prepends OpenPype executable arguments\n    before passed arguments and define environments if are not passed.\n\n    Values from 'os.environ' are used for environments if are not passed.\n    They are cleaned using 'clean_envs_for_openpype_process' function.\n\n    Example:\n    ```\n    run_ayon_process(\"run\", \"<path to .py script>\")\n    ```\n\n    Args:\n        *args (str): ayon-launcher cli arguments.\n        **kwargs (Any): Keyword arguments for subprocess.Popen.\n\n    Returns:\n        str: Full output of subprocess concatenated stdout and stderr.\n    \"\"\"\n\n    args = get_ayon_launcher_args(*args)\n    env = kwargs.pop(\"env\", None)\n    # Keep env untouched if are passed and not empty\n    if not env:\n        # Skip envs that can affect OpenPype process\n        # - fill more if you find more\n        env = clean_envs_for_openpype_process(os.environ)\n\n    # Only keep OpenPype version if we are running from build.\n    if not is_running_from_build():\n        env.pop(\"OPENPYPE_VERSION\", None)\n\n    return run_subprocess(args, env=env, **kwargs)\n\n\ndef run_openpype_process(*args, **kwargs):\n    \"\"\"Execute OpenPype process with passed arguments and wait.\n\n    Wrapper for 'run_process' which prepends OpenPype executable arguments\n    before passed arguments and define environments if are not passed.\n\n    Values from 'os.environ' are used for environments if are not passed.\n    They are cleaned using 'clean_envs_for_openpype_process' function.\n\n    Example:\n        >>> run_openpype_process(\"version\")\n\n    Args:\n        *args (tuple): OpenPype cli arguments.\n        **kwargs (dict): Keyword arguments for subprocess.Popen.\n    \"\"\"\n\n    if AYON_SERVER_ENABLED:\n        return run_ayon_launcher_process(*args, **kwargs)\n\n    args = get_openpype_execute_args(*args)\n    env = kwargs.pop(\"env\", None)\n    # Keep env untouched if are passed and not empty\n    if not env:\n        # Skip envs that can affect OpenPype process\n        # - fill more if you find more\n        env = clean_envs_for_openpype_process(os.environ)\n\n    # Only keep OpenPype version if we are running from build.\n    if not is_running_from_build():\n        env.pop(\"OPENPYPE_VERSION\", None)\n\n    return run_subprocess(args, env=env, **kwargs)\n\n\ndef run_detached_process(args, **kwargs):\n    \"\"\"Execute process with passed arguments as separated process.\n\n    Values from 'os.environ' are used for environments if are not passed.\n    They are cleaned using 'clean_envs_for_openpype_process' function.\n\n    Example:\n        >>> run_detached_process(\"run\", \"./path_to.py\")\n\n\n    Args:\n        *args (tuple): OpenPype cli arguments.\n        **kwargs (dict): Keyword arguments for subprocess.Popen.\n\n    Returns:\n        subprocess.Popen: Pointer to launched process but it is possible that\n            launched process is already killed (on linux).\n    \"\"\"\n\n    env = kwargs.pop(\"env\", None)\n    # Keep env untouched if are passed and not empty\n    if not env:\n        env = os.environ\n\n    # Create copy of passed env\n    kwargs[\"env\"] = {k: v for k, v in env.items()}\n\n    low_platform = platform.system().lower()\n    if low_platform == \"darwin\":\n        new_args = [\"open\", \"-na\", args.pop(0), \"--args\"]\n        new_args.extend(args)\n        args = new_args\n\n    elif low_platform == \"windows\":\n        flags = (\n            subprocess.CREATE_NEW_PROCESS_GROUP\n            | subprocess.DETACHED_PROCESS\n        )\n        kwargs[\"creationflags\"] = flags\n\n        if not sys.stdout:\n            kwargs[\"stdout\"] = subprocess.DEVNULL\n            kwargs[\"stderr\"] = subprocess.DEVNULL\n\n    elif low_platform == \"linux\" and get_linux_launcher_args() is not None:\n        json_data = {\n            \"args\": args,\n            \"env\": kwargs.pop(\"env\")\n        }\n        json_temp = tempfile.NamedTemporaryFile(\n            mode=\"w\", prefix=\"op_app_args\", suffix=\".json\", delete=False\n        )\n        json_temp.close()\n        json_temp_filpath = json_temp.name\n        with open(json_temp_filpath, \"w\") as stream:\n            json.dump(json_data, stream)\n\n        new_args = get_linux_launcher_args()\n        new_args.append(json_temp_filpath)\n\n        # Create mid-process which will launch application\n        process = subprocess.Popen(new_args, **kwargs)\n        # Wait until the process finishes\n        #   - This is important! The process would stay in \"open\" state.\n        process.wait()\n        # Remove the temp file\n        os.remove(json_temp_filpath)\n        # Return process which is already terminated\n        return process\n\n    process = subprocess.Popen(args, **kwargs)\n    return process\n\n\ndef path_to_subprocess_arg(path):\n    \"\"\"Prepare path for subprocess arguments.\n\n    Returned path can be wrapped with quotes or kept as is.\n    \"\"\"\n    return subprocess.list2cmdline([path])\n\n\ndef get_ayon_launcher_args(*args):\n    \"\"\"Arguments to run ayon-launcher process.\n\n    Arguments for subprocess when need to spawn new pype process. Which may be\n    needed when new python process for pype scripts must be executed in build\n    pype.\n\n    Reasons:\n        Ayon-launcher started from code has different executable set to\n            virtual env python and must have path to script as first argument\n            which is not needed for built application.\n\n    Args:\n        *args (str): Any arguments that will be added after executables.\n\n    Returns:\n        list[str]: List of arguments to run ayon-launcher process.\n    \"\"\"\n\n    executable = os.environ[\"AYON_EXECUTABLE\"]\n    launch_args = [executable]\n\n    executable_filename = os.path.basename(executable)\n    if \"python\" in executable_filename.lower():\n        filepath = os.path.join(os.environ[\"AYON_ROOT\"], \"start.py\")\n        launch_args.append(filepath)\n\n    if args:\n        launch_args.extend(args)\n\n    return launch_args\n\n\ndef get_openpype_execute_args(*args):\n    \"\"\"Arguments to run pype command.\n\n    Arguments for subprocess when need to spawn new pype process. Which may be\n    needed when new python process for pype scripts must be executed in build\n    pype.\n\n    ## Why is this needed?\n    Pype executed from code has different executable set to virtual env python\n    and must have path to script as first argument which is not needed for\n    build pype.\n\n    It is possible to pass any arguments that will be added after pype\n    executables.\n    \"\"\"\n\n    if AYON_SERVER_ENABLED:\n        return get_ayon_launcher_args(*args)\n\n    executable = os.environ[\"OPENPYPE_EXECUTABLE\"]\n    launch_args = [executable]\n\n    executable_filename = os.path.basename(executable)\n    if \"python\" in executable_filename.lower():\n        filepath = os.path.join(os.environ[\"OPENPYPE_ROOT\"], \"start.py\")\n        launch_args.append(filepath)\n\n    if args:\n        launch_args.extend(args)\n\n    return launch_args\n\n\ndef get_linux_launcher_args(*args):\n    \"\"\"Path to application mid process executable.\n\n    This function should be able as arguments are different when used\n    from code and build.\n\n    It is possible that this function is used in OpenPype build which does\n    not have yet the new executable. In that case 'None' is returned.\n\n    Todos:\n        Replace by script in scripts for ayon-launcher.\n\n    Args:\n        args (iterable): List of additional arguments added after executable\n            argument.\n\n    Returns:\n        list: Executables with possible positional argument to script when\n            called from code.\n    \"\"\"\n\n    filename = \"app_launcher\"\n    if AYON_SERVER_ENABLED:\n        executable = os.environ[\"AYON_EXECUTABLE\"]\n    else:\n        executable = os.environ[\"OPENPYPE_EXECUTABLE\"]\n\n    executable_filename = os.path.basename(executable)\n    if \"python\" in executable_filename.lower():\n        if AYON_SERVER_ENABLED:\n            root = os.environ[\"AYON_ROOT\"]\n        else:\n            root = os.environ[\"OPENPYPE_ROOT\"]\n        script_path = os.path.join(root, \"{}.py\".format(filename))\n        launch_args = [executable, script_path]\n    else:\n        new_executable = os.path.join(\n            os.path.dirname(executable),\n            filename\n        )\n        executable_path = find_executable(new_executable)\n        if executable_path is None:\n            return None\n        launch_args = [executable_path]\n\n    if args:\n        launch_args.extend(args)\n\n    return launch_args\n"
  },
  {
    "path": "openpype/lib/file_transaction.py",
    "content": "import os\nimport logging\nimport sys\nimport errno\nimport six\n\nfrom openpype.lib import create_hard_link\n\n# this is needed until speedcopy for linux is fixed\nif sys.platform == \"win32\":\n    from speedcopy import copyfile\nelse:\n    from shutil import copyfile\n\n\nclass DuplicateDestinationError(ValueError):\n    \"\"\"Error raised when transfer destination already exists in queue.\n\n    The error is only raised if `allow_queue_replacements` is False on the\n    FileTransaction instance and the added file to transfer is of a different\n    src file than the one already detected in the queue.\n\n    \"\"\"\n\n\nclass FileTransaction(object):\n    \"\"\"File transaction with rollback options.\n\n    The file transaction is a three-step process.\n\n    1) Rename any existing files to a \"temporary backup\" during `process()`\n    2) Copy the files to final destination during `process()`\n    3) Remove any backed up files (*no rollback possible!) during `finalize()`\n\n    Step 3 is done during `finalize()`. If not called the .bak files will\n    remain on disk.\n\n    These steps try to ensure that we don't overwrite half of any existing\n    files e.g. if they are currently in use.\n\n    Note:\n        A regular filesystem is *not* a transactional file system and even\n        though this implementation tries to produce a 'safe copy' with a\n        potential rollback do keep in mind that it's inherently unsafe due\n        to how filesystem works and a myriad of things could happen during\n        the transaction that break the logic. A file storage could go down,\n        permissions could be changed, other machines could be moving or writing\n        files. A lot can happen.\n\n    Warning:\n        Any folders created during the transfer will not be removed.\n    \"\"\"\n\n    MODE_COPY = 0\n    MODE_HARDLINK = 1\n\n    def __init__(self, log=None, allow_queue_replacements=False):\n        if log is None:\n            log = logging.getLogger(\"FileTransaction\")\n\n        self.log = log\n\n        # The transfer queue\n        # todo: make this an actual FIFO queue?\n        self._transfers = {}\n\n        # Destination file paths that a file was transferred to\n        self._transferred = []\n\n        # Backup file location mapping to original locations\n        self._backup_to_original = {}\n\n        self._allow_queue_replacements = allow_queue_replacements\n\n    def add(self, src, dst, mode=MODE_COPY):\n        \"\"\"Add a new file to transfer queue.\n\n        Args:\n            src (str): Source path.\n            dst (str): Destination path.\n            mode (MODE_COPY, MODE_HARDLINK): Transfer mode.\n        \"\"\"\n\n        opts = {\"mode\": mode}\n\n        src = os.path.normpath(os.path.abspath(src))\n        dst = os.path.normpath(os.path.abspath(dst))\n\n        if dst in self._transfers:\n            queued_src = self._transfers[dst][0]\n            if src == queued_src:\n                self.log.debug(\n                    \"File transfer was already in queue: {} -> {}\".format(\n                        src, dst))\n                return\n            else:\n                if not self._allow_queue_replacements:\n                    raise DuplicateDestinationError(\n                        \"Transfer to destination is already in queue: \"\n                        \"{} -> {}. It's not allowed to be replaced by \"\n                        \"a new transfer from {}\".format(\n                            queued_src, dst, src\n                        ))\n\n                self.log.warning(\"File transfer in queue replaced..\")\n                self.log.debug(\n                    \"Removed from queue: {} -> {} replaced by {} -> {}\".format(\n                        queued_src, dst, src, dst))\n\n        self._transfers[dst] = (src, opts)\n\n    def process(self):\n        # Backup any existing files\n        for dst, (src, _) in self._transfers.items():\n            self.log.debug(\"Checking file ... {} -> {}\".format(src, dst))\n            path_same = self._same_paths(src, dst)\n            if path_same or not os.path.exists(dst):\n                continue\n\n            # Backup original file\n            # todo: add timestamp or uuid to ensure unique\n            backup = dst + \".bak\"\n            self._backup_to_original[backup] = dst\n            self.log.debug(\n                \"Backup existing file: {} -> {}\".format(dst, backup))\n            os.rename(dst, backup)\n\n        # Copy the files to transfer\n        for dst, (src, opts) in self._transfers.items():\n            path_same = self._same_paths(src, dst)\n            if path_same:\n                self.log.debug(\n                    \"Source and destination are same files {} -> {}\".format(\n                        src, dst))\n                continue\n\n            self._create_folder_for_file(dst)\n\n            if opts[\"mode\"] == self.MODE_COPY:\n                self.log.debug(\"Copying file ... {} -> {}\".format(src, dst))\n                copyfile(src, dst)\n            elif opts[\"mode\"] == self.MODE_HARDLINK:\n                self.log.debug(\"Hardlinking file ... {} -> {}\".format(\n                    src, dst))\n                create_hard_link(src, dst)\n\n            self._transferred.append(dst)\n\n    def finalize(self):\n        # Delete any backed up files\n        for backup in self._backup_to_original.keys():\n            try:\n                os.remove(backup)\n            except OSError:\n                self.log.error(\n                    \"Failed to remove backup file: {}\".format(backup),\n                    exc_info=True)\n\n    def rollback(self):\n        errors = 0\n        # Rollback any transferred files\n        for path in self._transferred:\n            try:\n                os.remove(path)\n            except OSError:\n                errors += 1\n                self.log.error(\n                    \"Failed to rollback created file: {}\".format(path),\n                    exc_info=True)\n\n        # Rollback the backups\n        for backup, original in self._backup_to_original.items():\n            try:\n                os.rename(backup, original)\n            except OSError:\n                errors += 1\n                self.log.error(\n                    \"Failed to restore original file: {} -> {}\".format(\n                        backup, original),\n                    exc_info=True)\n\n        if errors:\n            self.log.error(\n                \"{} errors occurred during rollback.\".format(errors),\n                exc_info=True)\n            six.reraise(*sys.exc_info())\n\n    @property\n    def transferred(self):\n        \"\"\"Return the processed transfers destination paths\"\"\"\n        return list(self._transferred)\n\n    @property\n    def backups(self):\n        \"\"\"Return the backup file paths\"\"\"\n        return list(self._backup_to_original.keys())\n\n    def _create_folder_for_file(self, path):\n        dirname = os.path.dirname(path)\n        try:\n            os.makedirs(dirname)\n        except OSError as e:\n            if e.errno == errno.EEXIST:\n                pass\n            else:\n                self.log.critical(\"An unexpected error occurred.\")\n                six.reraise(*sys.exc_info())\n\n    def _same_paths(self, src, dst):\n        # handles same paths but with C:/project vs c:/project\n        if os.path.exists(src) and os.path.exists(dst):\n            return os.stat(src) == os.stat(dst)\n\n        return src == dst\n"
  },
  {
    "path": "openpype/lib/local_settings.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Package to deal with saving and retrieving user specific settings.\"\"\"\nimport os\nimport json\nimport getpass\nimport platform\nfrom datetime import datetime\nfrom abc import ABCMeta, abstractmethod\n\n# TODO Use pype igniter logic instead of using duplicated code\n# disable lru cache in Python 2\ntry:\n    from functools import lru_cache\nexcept ImportError:\n    def lru_cache(maxsize):\n        def max_size(func):\n            def wrapper(*args, **kwargs):\n                value = func(*args, **kwargs)\n                return value\n            return wrapper\n        return max_size\n\n# ConfigParser was renamed in python3 to configparser\ntry:\n    import configparser\nexcept ImportError:\n    import ConfigParser as configparser\n\nimport six\nimport appdirs\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.settings import (\n    get_local_settings,\n    get_system_settings\n)\n\nfrom openpype.client.mongo import validate_mongo_connection\nfrom openpype.client import get_ayon_server_api_connection\n\n_PLACEHOLDER = object()\n\n\nclass OpenPypeSecureRegistry:\n    \"\"\"Store information using keyring.\n\n    Registry should be used for private data that should be available only for\n    user.\n\n    All passed registry names will have added prefix `OpenPype/` to easier\n    identify which data were created by OpenPype.\n\n    Args:\n        name(str): Name of registry used as identifier for data.\n    \"\"\"\n    def __init__(self, name):\n        try:\n            import keyring\n\n        except Exception:\n            raise NotImplementedError(\n                \"Python module `keyring` is not available.\"\n            )\n\n        # hack for cx_freeze and Windows keyring backend\n        if platform.system().lower() == \"windows\":\n            from keyring.backends import Windows\n\n            keyring.set_keyring(Windows.WinVaultKeyring())\n\n        # Force \"OpenPype\" prefix\n        self._name = \"/\".join((\"OpenPype\", name))\n\n    def set_item(self, name, value):\n        # type: (str, str) -> None\n        \"\"\"Set sensitive item into system's keyring.\n\n        This uses `Keyring module`_ to save sensitive stuff into system's\n        keyring.\n\n        Args:\n            name (str): Name of the item.\n            value (str): Value of the item.\n\n        .. _Keyring module:\n            https://github.com/jaraco/keyring\n\n        \"\"\"\n        import keyring\n\n        keyring.set_password(self._name, name, value)\n\n    @lru_cache(maxsize=32)\n    def get_item(self, name, default=_PLACEHOLDER):\n        \"\"\"Get value of sensitive item from system's keyring.\n\n        See also `Keyring module`_\n\n        Args:\n            name (str): Name of the item.\n            default (Any): Default value if item is not available.\n\n        Returns:\n            value (str): Value of the item.\n\n        Raises:\n            ValueError: If item doesn't exist and default is not defined.\n\n        .. _Keyring module:\n            https://github.com/jaraco/keyring\n\n        \"\"\"\n        import keyring\n\n        value = keyring.get_password(self._name, name)\n        if value is not None:\n            return value\n\n        if default is not _PLACEHOLDER:\n            return default\n\n        # NOTE Should raise `KeyError`\n        raise ValueError(\n            \"Item {}:{} does not exist in keyring.\".format(self._name, name)\n        )\n\n    def delete_item(self, name):\n        # type: (str) -> None\n        \"\"\"Delete value stored in system's keyring.\n\n        See also `Keyring module`_\n\n        Args:\n            name (str): Name of the item to be deleted.\n\n        .. _Keyring module:\n            https://github.com/jaraco/keyring\n\n        \"\"\"\n        import keyring\n\n        self.get_item.cache_clear()\n        keyring.delete_password(self._name, name)\n\n\n@six.add_metaclass(ABCMeta)\nclass ASettingRegistry():\n    \"\"\"Abstract class defining structure of **SettingRegistry** class.\n\n    It is implementing methods to store secure items into keyring, otherwise\n    mechanism for storing common items must be implemented in abstract\n    methods.\n\n    Attributes:\n        _name (str): Registry names.\n\n    \"\"\"\n\n    def __init__(self, name):\n        # type: (str) -> ASettingRegistry\n        super(ASettingRegistry, self).__init__()\n\n        self._name = name\n        self._items = {}\n\n    def set_item(self, name, value):\n        # type: (str, str) -> None\n        \"\"\"Set item to settings registry.\n\n        Args:\n            name (str): Name of the item.\n            value (str): Value of the item.\n\n        \"\"\"\n        self._set_item(name, value)\n\n    @abstractmethod\n    def _set_item(self, name, value):\n        # type: (str, str) -> None\n        # Implement it\n        pass\n\n    def __setitem__(self, name, value):\n        self._items[name] = value\n        self._set_item(name, value)\n\n    def get_item(self, name):\n        # type: (str) -> str\n        \"\"\"Get item from settings registry.\n\n        Args:\n            name (str): Name of the item.\n\n        Returns:\n            value (str): Value of the item.\n\n        Raises:\n            ValueError: If item doesn't exist.\n\n        \"\"\"\n        return self._get_item(name)\n\n    @abstractmethod\n    def _get_item(self, name):\n        # type: (str) -> str\n        # Implement it\n        pass\n\n    def __getitem__(self, name):\n        return self._get_item(name)\n\n    def delete_item(self, name):\n        # type: (str) -> None\n        \"\"\"Delete item from settings registry.\n\n        Args:\n            name (str): Name of the item.\n\n        \"\"\"\n        self._delete_item(name)\n\n    @abstractmethod\n    def _delete_item(self, name):\n        # type: (str) -> None\n        \"\"\"Delete item from settings.\n\n        Note:\n            see :meth:`openpype.lib.user_settings.ARegistrySettings.delete_item`\n\n        \"\"\"\n        pass\n\n    def __delitem__(self, name):\n        del self._items[name]\n        self._delete_item(name)\n\n\nclass IniSettingRegistry(ASettingRegistry):\n    \"\"\"Class using :mod:`configparser`.\n\n    This class is using :mod:`configparser` (ini) files to store items.\n\n    \"\"\"\n\n    def __init__(self, name, path):\n        # type: (str, str) -> IniSettingRegistry\n        super(IniSettingRegistry, self).__init__(name)\n        # get registry file\n        version = os.getenv(\"OPENPYPE_VERSION\", \"N/A\")\n        self._registry_file = os.path.join(path, \"{}.ini\".format(name))\n        if not os.path.exists(self._registry_file):\n            with open(self._registry_file, mode=\"w\") as cfg:\n                print(\"# Settings registry\", cfg)\n                print(\"# Generated by OpenPype {}\".format(version), cfg)\n                now = datetime.now().strftime(\"%d/%m/%Y %H:%M:%S\")\n                print(\"# {}\".format(now), cfg)\n\n    def set_item_section(\n            self, section, name, value):\n        # type: (str, str, str) -> None\n        \"\"\"Set item to specific section of ini registry.\n\n        If section doesn't exists, it is created.\n\n        Args:\n            section (str): Name of section.\n            name (str): Name of the item.\n            value (str): Value of the item.\n\n        \"\"\"\n        value = str(value)\n        config = configparser.ConfigParser()\n\n        config.read(self._registry_file)\n        if not config.has_section(section):\n            config.add_section(section)\n        current = config[section]\n        current[name] = value\n\n        with open(self._registry_file, mode=\"w\") as cfg:\n            config.write(cfg)\n\n    def _set_item(self, name, value):\n        # type: (str, str) -> None\n        self.set_item_section(\"MAIN\", name, value)\n\n    def set_item(self, name, value):\n        # type: (str, str) -> None\n        \"\"\"Set item to settings ini file.\n\n        This saves item to ``DEFAULT`` section of ini as each item there\n        must reside in some section.\n\n        Args:\n            name (str): Name of the item.\n            value (str): Value of the item.\n\n        \"\"\"\n        # this does the some, overridden just for different docstring.\n        # we cast value to str as ini options values must be strings.\n        super(IniSettingRegistry, self).set_item(name, str(value))\n\n    def get_item(self, name):\n        # type: (str) -> str\n        \"\"\"Gets item from settings ini file.\n\n        This gets settings from ``DEFAULT`` section of ini file as each item\n        there must reside in some section.\n\n        Args:\n            name (str): Name of the item.\n\n        Returns:\n            str: Value of item.\n\n        Raises:\n            ValueError: If value doesn't exist.\n\n        \"\"\"\n        return super(IniSettingRegistry, self).get_item(name)\n\n    @lru_cache(maxsize=32)\n    def get_item_from_section(self, section, name):\n        # type: (str, str) -> str\n        \"\"\"Get item from section of ini file.\n\n        This will read ini file and try to get item value from specified\n        section. If that section or item doesn't exist, :exc:`ValueError`\n        is risen.\n\n        Args:\n            section (str): Name of ini section.\n            name (str): Name of the item.\n\n        Returns:\n            str: Item value.\n\n        Raises:\n            ValueError: If value doesn't exist.\n\n        \"\"\"\n        config = configparser.ConfigParser()\n        config.read(self._registry_file)\n        try:\n            value = config[section][name]\n        except KeyError:\n            raise ValueError(\n                \"Registry doesn't contain value {}:{}\".format(section, name))\n        return value\n\n    def _get_item(self, name):\n        # type: (str) -> str\n        return self.get_item_from_section(\"MAIN\", name)\n\n    def delete_item_from_section(self, section, name):\n        # type: (str, str) -> None\n        \"\"\"Delete item from section in ini file.\n\n        Args:\n            section (str): Section name.\n            name (str): Name of the item.\n\n        Raises:\n            ValueError: If item doesn't exist.\n\n        \"\"\"\n        self.get_item_from_section.cache_clear()\n        config = configparser.ConfigParser()\n        config.read(self._registry_file)\n        try:\n            _ = config[section][name]\n        except KeyError:\n            raise ValueError(\n                \"Registry doesn't contain value {}:{}\".format(section, name))\n        config.remove_option(section, name)\n\n        # if section is empty, delete it\n        if len(config[section].keys()) == 0:\n            config.remove_section(section)\n\n        with open(self._registry_file, mode=\"w\") as cfg:\n            config.write(cfg)\n\n    def _delete_item(self, name):\n        \"\"\"Delete item from default section.\n\n        Note:\n            See :meth:`~openpype.lib.IniSettingsRegistry.delete_item_from_section`\n\n        \"\"\"\n        self.delete_item_from_section(\"MAIN\", name)\n\n\nclass JSONSettingRegistry(ASettingRegistry):\n    \"\"\"Class using json file as storage.\"\"\"\n\n    def __init__(self, name, path):\n        # type: (str, str) -> JSONSettingRegistry\n        super(JSONSettingRegistry, self).__init__(name)\n        #: str: name of registry file\n        self._registry_file = os.path.join(path, \"{}.json\".format(name))\n        now = datetime.now().strftime(\"%d/%m/%Y %H:%M:%S\")\n        header = {\n            \"__metadata__\": {\n                \"openpype-version\": os.getenv(\"OPENPYPE_VERSION\", \"N/A\"),\n                \"generated\": now\n            },\n            \"registry\": {}\n        }\n\n        if not os.path.exists(os.path.dirname(self._registry_file)):\n            os.makedirs(os.path.dirname(self._registry_file), exist_ok=True)\n        if not os.path.exists(self._registry_file):\n            with open(self._registry_file, mode=\"w\") as cfg:\n                json.dump(header, cfg, indent=4)\n\n    @lru_cache(maxsize=32)\n    def _get_item(self, name):\n        # type: (str) -> object\n        \"\"\"Get item value from registry json.\n\n        Note:\n            See :meth:`openpype.lib.JSONSettingRegistry.get_item`\n\n        \"\"\"\n        with open(self._registry_file, mode=\"r\") as cfg:\n            data = json.load(cfg)\n            try:\n                value = data[\"registry\"][name]\n            except KeyError:\n                raise ValueError(\n                    \"Registry doesn't contain value {}\".format(name))\n        return value\n\n    def get_item(self, name):\n        # type: (str) -> object\n        \"\"\"Get item value from registry json.\n\n        Args:\n            name (str): Name of the item.\n\n        Returns:\n            value of the item\n\n        Raises:\n            ValueError: If item is not found in registry file.\n\n        \"\"\"\n        return self._get_item(name)\n\n    def _set_item(self, name, value):\n        # type: (str, object) -> None\n        \"\"\"Set item value to registry json.\n\n        Note:\n            See :meth:`openpype.lib.JSONSettingRegistry.set_item`\n\n        \"\"\"\n        with open(self._registry_file, \"r+\") as cfg:\n            data = json.load(cfg)\n            data[\"registry\"][name] = value\n            cfg.truncate(0)\n            cfg.seek(0)\n            json.dump(data, cfg, indent=4)\n\n    def set_item(self, name, value):\n        # type: (str, object) -> None\n        \"\"\"Set item and its value into json registry file.\n\n        Args:\n            name (str): name of the item.\n            value (Any): value of the item.\n\n        \"\"\"\n        self._set_item(name, value)\n\n    def _delete_item(self, name):\n        # type: (str) -> None\n        self._get_item.cache_clear()\n        with open(self._registry_file, \"r+\") as cfg:\n            data = json.load(cfg)\n            del data[\"registry\"][name]\n            cfg.truncate(0)\n            cfg.seek(0)\n            json.dump(data, cfg, indent=4)\n\n\nclass OpenPypeSettingsRegistry(JSONSettingRegistry):\n    \"\"\"Class handling OpenPype general settings registry.\n\n    Attributes:\n        vendor (str): Name used for path construction.\n        product (str): Additional name used for path construction.\n\n    \"\"\"\n\n    def __init__(self, name=None):\n        if AYON_SERVER_ENABLED:\n            vendor = \"Ynput\"\n            product = \"AYON\"\n            default_name = \"AYON_settings\"\n        else:\n            vendor = \"pypeclub\"\n            product = \"openpype\"\n            default_name = \"openpype_settings\"\n        self.vendor = vendor\n        self.product = product\n        if not name:\n            name = default_name\n        path = appdirs.user_data_dir(self.product, self.vendor)\n        super(OpenPypeSettingsRegistry, self).__init__(name, path)\n\n\ndef _create_local_site_id(registry=None):\n    \"\"\"Create a local site identifier.\"\"\"\n    from coolname import generate_slug\n\n    if registry is None:\n        registry = OpenPypeSettingsRegistry()\n\n    new_id = generate_slug(3)\n\n    print(\"Created local site id \\\"{}\\\"\".format(new_id))\n\n    registry.set_item(\"localId\", new_id)\n\n    return new_id\n\n\ndef get_ayon_appdirs(*args):\n    \"\"\"Local app data directory of AYON client.\n\n    Args:\n        *args (Iterable[str]): Subdirectories/files in local app data dir.\n\n    Returns:\n        str: Path to directory/file in local app data dir.\n    \"\"\"\n\n    return os.path.join(\n        appdirs.user_data_dir(\"AYON\", \"Ynput\"),\n        *args\n    )\n\n\ndef _get_ayon_local_site_id():\n    # used for background syncing\n    site_id = os.environ.get(\"AYON_SITE_ID\")\n    if site_id:\n        return site_id\n\n    site_id_path = get_ayon_appdirs(\"site_id\")\n    if os.path.exists(site_id_path):\n        with open(site_id_path, \"r\") as stream:\n            site_id = stream.read()\n\n    if site_id:\n        return site_id\n\n    try:\n        from ayon_common.utils import get_local_site_id as _get_local_site_id\n        site_id = _get_local_site_id()\n    except ImportError:\n        raise ValueError(\"Couldn't access local site id\")\n\n    return site_id\n\n\ndef get_local_site_id():\n    \"\"\"Get local site identifier.\n\n    Identifier is created if does not exists yet.\n    \"\"\"\n\n    if AYON_SERVER_ENABLED:\n        return _get_ayon_local_site_id()\n\n    # override local id from environment\n    # used for background syncing\n    if os.environ.get(\"OPENPYPE_LOCAL_ID\"):\n        return os.environ[\"OPENPYPE_LOCAL_ID\"]\n\n    registry = OpenPypeSettingsRegistry()\n    try:\n        return registry.get_item(\"localId\")\n    except ValueError:\n        return _create_local_site_id()\n\n\ndef change_openpype_mongo_url(new_mongo_url):\n    \"\"\"Change mongo url in pype registry.\n\n    Change of OpenPype mongo URL require restart of running pype processes or\n    processes using pype.\n    \"\"\"\n\n    validate_mongo_connection(new_mongo_url)\n    key = \"openPypeMongo\"\n    registry = OpenPypeSecureRegistry(\"mongodb\")\n    existing_value = registry.get_item(key, None)\n    if existing_value is not None:\n        registry.delete_item(key)\n    registry.set_item(key, new_mongo_url)\n\n\ndef get_openpype_username():\n    \"\"\"OpenPype username used for templates and publishing.\n\n    May be different than machine's username.\n\n    Always returns \"OPENPYPE_USERNAME\" environment if is set then tries local\n    settings and last option is to use `getpass.getuser()` which returns\n    machine username.\n    \"\"\"\n\n    if AYON_SERVER_ENABLED:\n        con = get_ayon_server_api_connection()\n        return con.get_user()[\"name\"]\n\n    username = os.environ.get(\"OPENPYPE_USERNAME\")\n    if not username:\n        local_settings = get_local_settings()\n        username = (\n            local_settings\n            .get(\"general\", {})\n            .get(\"username\")\n        )\n        if not username:\n            username = getpass.getuser()\n    return username\n\n\ndef is_admin_password_required():\n    system_settings = get_system_settings()\n    password = system_settings[\"general\"].get(\"admin_password\")\n    if not password:\n        return False\n\n    local_settings = get_local_settings()\n    is_admin = local_settings.get(\"general\", {}).get(\"is_admin\", False)\n    if is_admin:\n        return False\n    return True\n"
  },
  {
    "path": "openpype/lib/log.py",
    "content": "\"\"\"\nLogging to console and to mongo. For mongo logging, you need to set either\n``OPENPYPE_LOG_MONGO_URL`` to something like:\n\n.. example::\n   mongo://user:password@hostname:port/database/collection?authSource=avalon\n\nor set ``OPENPYPE_LOG_MONGO_HOST`` and other variables.\nSee :func:`_mongo_settings`\n\nBest place for it is in ``repos/pype-config/environments/global.json``\n\"\"\"\n\n\nimport datetime\nimport getpass\nimport logging\nimport os\nimport platform\nimport socket\nimport sys\nimport time\nimport traceback\nimport threading\nimport copy\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client.mongo import (\n    MongoEnvNotSet,\n    get_default_components,\n    OpenPypeMongoConnection,\n)\nfrom . import Terminal\n\ntry:\n    import log4mongo\n    from log4mongo.handlers import MongoHandler\nexcept ImportError:\n    log4mongo = None\n    MongoHandler = type(\"NOT_SET\", (), {})\n\n# Check for `unicode` in builtins\nUSE_UNICODE = hasattr(__builtins__, \"unicode\")\n\n\nclass LogStreamHandler(logging.StreamHandler):\n    \"\"\" StreamHandler class designed to handle utf errors in python 2.x hosts.\n\n    \"\"\"\n\n    def __init__(self, stream=None):\n        super(LogStreamHandler, self).__init__(stream)\n        self.enabled = True\n\n    def enable(self):\n        \"\"\" Enable StreamHandler\n\n            Used to silence output\n        \"\"\"\n        self.enabled = True\n\n    def disable(self):\n        \"\"\" Disable StreamHandler\n\n            Make StreamHandler output again\n        \"\"\"\n        self.enabled = False\n\n    def emit(self, record):\n        if not self.enable:\n            return\n        try:\n            msg = self.format(record)\n            msg = Terminal.log(msg)\n            stream = self.stream\n            if stream is None:\n                return\n            fs = \"%s\\n\"\n            # if no unicode support...\n            if not USE_UNICODE:\n                stream.write(fs % msg)\n            else:\n                try:\n                    if (isinstance(msg, unicode) and  # noqa: F821\n                            getattr(stream, 'encoding', None)):\n                        ufs = u'%s\\n'\n                        try:\n                            stream.write(ufs % msg)\n                        except UnicodeEncodeError:\n                            stream.write((ufs % msg).encode(stream.encoding))\n                    else:\n                        if (getattr(stream, 'encoding', 'utf-8')):\n                            ufs = u'%s\\n'\n                            stream.write(ufs % unicode(msg))  # noqa: F821\n                        else:\n                            stream.write(fs % msg)\n                except UnicodeError:\n                    stream.write(fs % msg.encode(\"UTF-8\"))\n            self.flush()\n        except (KeyboardInterrupt, SystemExit):\n            raise\n\n        except OSError:\n            self.handleError(record)\n        except ValueError:\n            # this is raised when logging during interpreter shutdown\n            # or it real edge cases where logging stream is already closed.\n            # In particular, it happens a lot in 3DEqualizer.\n            # TODO: remove this condition when the cause is found.\n            pass\n\n        except Exception:\n            print(repr(record))\n            self.handleError(record)\n\n\nclass LogFormatter(logging.Formatter):\n\n    DFT = '%(levelname)s >>> { %(name)s }: [ %(message)s ]'\n    default_formatter = logging.Formatter(DFT)\n\n    def __init__(self, formats):\n        super(LogFormatter, self).__init__()\n        self.formatters = {}\n        for loglevel in formats:\n            self.formatters[loglevel] = logging.Formatter(formats[loglevel])\n\n    def format(self, record):\n        formatter = self.formatters.get(record.levelno, self.default_formatter)\n\n        _exc_info = record.exc_info\n        record.exc_info = None\n\n        out = formatter.format(record)\n        record.exc_info = _exc_info\n\n        if record.exc_info is not None:\n            line_len = len(str(record.exc_info[1]))\n            if line_len > 30:\n                line_len = 30\n            out = \"{}\\n{}\\n{}\\n{}\\n{}\".format(\n                out,\n                line_len * \"=\",\n                str(record.exc_info[1]),\n                line_len * \"=\",\n                self.formatException(record.exc_info)\n            )\n        return out\n\n\nclass MongoFormatter(logging.Formatter):\n\n    DEFAULT_PROPERTIES = logging.LogRecord(\n        '', '', '', '', '', '', '', '').__dict__.keys()\n\n    def format(self, record):\n        \"\"\"Formats LogRecord into python dictionary.\"\"\"\n        # Standard document\n        document = {\n            'timestamp': datetime.datetime.now(),\n            'level': record.levelname,\n            'thread': record.thread,\n            'threadName': record.threadName,\n            'message': record.getMessage(),\n            'loggerName': record.name,\n            'fileName': record.pathname,\n            'module': record.module,\n            'method': record.funcName,\n            'lineNumber': record.lineno\n        }\n        document.update(Logger.get_process_data())\n\n        # Standard document decorated with exception info\n        if record.exc_info is not None:\n            document['exception'] = {\n                'message': str(record.exc_info[1]),\n                'code': 0,\n                'stackTrace': self.formatException(record.exc_info)\n            }\n\n        # Standard document decorated with extra contextual information\n        if len(self.DEFAULT_PROPERTIES) != len(record.__dict__):\n            contextual_extra = set(record.__dict__).difference(\n                set(self.DEFAULT_PROPERTIES))\n            if contextual_extra:\n                for key in contextual_extra:\n                    document[key] = record.__dict__[key]\n        return document\n\n\nclass Logger:\n    DFT = '%(levelname)s >>> { %(name)s }: [ %(message)s ] '\n    DBG = \"  - { %(name)s }: [ %(message)s ] \"\n    INF = \">>> [ %(message)s ] \"\n    WRN = \"*** WRN: >>> { %(name)s }: [ %(message)s ] \"\n    ERR = \"!!! ERR: %(asctime)s >>> { %(name)s }: [ %(message)s ] \"\n    CRI = \"!!! CRI: %(asctime)s >>> { %(name)s }: [ %(message)s ] \"\n\n    FORMAT_FILE = {\n        logging.INFO: INF,\n        logging.DEBUG: DBG,\n        logging.WARNING: WRN,\n        logging.ERROR: ERR,\n        logging.CRITICAL: CRI,\n    }\n\n    # Is static class initialized\n    bootstraped = False\n    initialized = False\n    _init_lock = threading.Lock()\n\n    # Defines if mongo logging should be used\n    use_mongo_logging = None\n    mongo_process_id = None\n\n    # Backwards compatibility - was used in start.py\n    # TODO remove when all old builds are replaced with new one\n    #   not using 'log_mongo_url_components'\n    log_mongo_url_components = None\n\n    # Database name in Mongo\n    log_database_name = os.environ.get(\"OPENPYPE_DATABASE_NAME\")\n    # Collection name under database in Mongo\n    log_collection_name = \"logs\"\n\n    # Logging level - OPENPYPE_LOG_LEVEL\n    log_level = None\n\n    # Data same for all record documents\n    process_data = None\n    # Cached process name or ability to set different process name\n    _process_name = None\n\n    @classmethod\n    def get_logger(cls, name=None, _host=None):\n        if not cls.initialized:\n            cls.initialize()\n\n        logger = logging.getLogger(name or \"__main__\")\n\n        logger.setLevel(cls.log_level)\n\n        add_mongo_handler = cls.use_mongo_logging\n        add_console_handler = True\n\n        for handler in logger.handlers:\n            if isinstance(handler, MongoHandler):\n                add_mongo_handler = False\n            elif isinstance(handler, LogStreamHandler):\n                add_console_handler = False\n\n        if add_console_handler:\n            logger.addHandler(cls._get_console_handler())\n\n        if add_mongo_handler:\n            try:\n                handler = cls._get_mongo_handler()\n                if handler:\n                    logger.addHandler(handler)\n\n            except MongoEnvNotSet:\n                # Skip if mongo environments are not set yet\n                cls.use_mongo_logging = False\n\n            except Exception:\n                lines = traceback.format_exception(*sys.exc_info())\n                for line in lines:\n                    if line.endswith(\"\\n\"):\n                        line = line[:-1]\n                    Terminal.echo(line)\n                cls.use_mongo_logging = False\n\n        # Do not propagate logs to root logger\n        logger.propagate = False\n\n        if _host is not None:\n            # Warn about deprecated argument\n            # TODO remove backwards compatibility of host argument which is\n            # not used for more than a year\n            logger.warning(\n                \"Logger \\\"{}\\\" is using argument `host` on `get_logger`\"\n                \" which is deprecated. Please remove as backwards\"\n                \" compatibility will be removed soon.\"\n            )\n        return logger\n\n    @classmethod\n    def _get_mongo_handler(cls):\n        cls.bootstrap_mongo_log()\n\n        if not cls.use_mongo_logging:\n            return\n\n        components = get_default_components()\n        kwargs = {\n            \"host\": components[\"host\"],\n            \"database_name\": cls.log_database_name,\n            \"collection\": cls.log_collection_name,\n            \"username\": components[\"username\"],\n            \"password\": components[\"password\"],\n            \"capped\": True,\n            \"formatter\": MongoFormatter()\n        }\n        if components[\"port\"] is not None:\n            kwargs[\"port\"] = int(components[\"port\"])\n        if components[\"auth_db\"]:\n            kwargs[\"authentication_db\"] = components[\"auth_db\"]\n\n        return MongoHandler(**kwargs)\n\n    @classmethod\n    def _get_console_handler(cls):\n        formatter = LogFormatter(cls.FORMAT_FILE)\n        console_handler = LogStreamHandler()\n\n        console_handler.set_name(\"LogStreamHandler\")\n        console_handler.setFormatter(formatter)\n        return console_handler\n\n    @classmethod\n    def initialize(cls):\n        # TODO update already created loggers on re-initialization\n        if not cls._init_lock.locked():\n            with cls._init_lock:\n                cls._initialize()\n        else:\n            # If lock is locked wait until is finished\n            while cls._init_lock.locked():\n                time.sleep(0.1)\n\n    @classmethod\n    def _initialize(cls):\n        # Change initialization state to prevent runtime changes\n        # if is executed during runtime\n        cls.initialized = False\n        if not AYON_SERVER_ENABLED:\n            cls.log_mongo_url_components = get_default_components()\n\n        # Define if should logging to mongo be used\n        if AYON_SERVER_ENABLED:\n            use_mongo_logging = False\n        else:\n            use_mongo_logging = (\n                log4mongo is not None\n                and os.environ.get(\"OPENPYPE_LOG_TO_SERVER\") == \"1\"\n            )\n\n        # Set mongo id for process (ONLY ONCE)\n        if use_mongo_logging and cls.mongo_process_id is None:\n            try:\n                from bson.objectid import ObjectId\n            except Exception:\n                use_mongo_logging = False\n\n            # Check if mongo id was passed with environments and pop it\n            # - This is for subprocesses that are part of another process\n            #   like Ftrack event server has 3 other subprocesses that should\n            #   use same mongo id\n            if use_mongo_logging:\n                mongo_id = os.environ.pop(\"OPENPYPE_PROCESS_MONGO_ID\", None)\n                if not mongo_id:\n                    # Create new object id\n                    mongo_id = ObjectId()\n                else:\n                    # Convert string to ObjectId object\n                    mongo_id = ObjectId(mongo_id)\n                cls.mongo_process_id = mongo_id\n\n        # Store result to class definition\n        cls.use_mongo_logging = use_mongo_logging\n\n        # Define what is logging level\n        log_level = os.getenv(\"OPENPYPE_LOG_LEVEL\")\n        if not log_level:\n            # Check OPENPYPE_DEBUG for backwards compatibility\n            op_debug = os.getenv(\"OPENPYPE_DEBUG\")\n            if op_debug and int(op_debug) > 0:\n                log_level = 10\n            else:\n                log_level = 20\n        cls.log_level = int(log_level)\n\n        if not os.environ.get(\"OPENPYPE_MONGO\"):\n            cls.use_mongo_logging = False\n\n        # Mark as initialized\n        cls.initialized = True\n\n    @classmethod\n    def get_process_data(cls):\n        \"\"\"Data about current process which should be same for all records.\n\n        Process data are used for each record sent to mongo database.\n        \"\"\"\n        if cls.process_data is not None:\n            return copy.deepcopy(cls.process_data)\n\n        if not cls.initialized:\n            cls.initialize()\n\n        host_name = socket.gethostname()\n        try:\n            host_ip = socket.gethostbyname(host_name)\n        except socket.gaierror:\n            host_ip = \"127.0.0.1\"\n\n        process_name = cls.get_process_name()\n\n        cls.process_data = {\n            \"process_id\": cls.mongo_process_id,\n            \"hostname\": host_name,\n            \"hostip\": host_ip,\n            \"username\": getpass.getuser(),\n            \"system_name\": platform.system(),\n            \"process_name\": process_name\n        }\n        return copy.deepcopy(cls.process_data)\n\n    @classmethod\n    def set_process_name(cls, process_name):\n        \"\"\"Set process name for mongo logs.\"\"\"\n        # Just change the attribute\n        cls._process_name = process_name\n        # Update process data if are already set\n        if cls.process_data is not None:\n            cls.process_data[\"process_name\"] = process_name\n\n    @classmethod\n    def get_process_name(cls):\n        \"\"\"Process name that is like \"label\" of a process.\n\n        OpenPype's logging can be used from OpenPyppe itself of from hosts.\n        Even in OpenPype process it's good to know if logs are from tray or\n        from other cli commands. This should help to identify that information.\n        \"\"\"\n        if cls._process_name is not None:\n            return cls._process_name\n\n        # Get process name\n        process_name = os.environ.get(\"AVALON_APP_NAME\")\n        if not process_name:\n            try:\n                import psutil\n                process = psutil.Process(os.getpid())\n                process_name = process.name()\n\n            except ImportError:\n                pass\n\n        if not process_name:\n            process_name = os.path.basename(sys.executable)\n\n        cls._process_name = process_name\n        return cls._process_name\n\n    @classmethod\n    def bootstrap_mongo_log(cls):\n        \"\"\"Prepare mongo logging.\"\"\"\n        if cls.bootstraped:\n            return\n\n        if not cls.initialized:\n            cls.initialize()\n\n        if not cls.use_mongo_logging:\n            return\n\n        if not cls.log_database_name:\n            raise ValueError(\"Database name for logs is not set\")\n\n        client = log4mongo.handlers._connection\n        if not client:\n            client = cls.get_log_mongo_connection()\n            # Set the client inside log4mongo handlers to not create another\n            # mongo db connection.\n            log4mongo.handlers._connection = client\n\n        logdb = client[cls.log_database_name]\n\n        collist = logdb.list_collection_names()\n        if cls.log_collection_name not in collist:\n            logdb.create_collection(\n                cls.log_collection_name,\n                capped=True,\n                max=5000,\n                size=1073741824\n            )\n        cls.bootstraped = True\n\n    @classmethod\n    def get_log_mongo_connection(cls):\n        \"\"\"Mongo connection that allows to get to log collection.\n\n        This is implemented to prevent multiple connections to mongo from same\n        process.\n        \"\"\"\n        if not cls.initialized:\n            cls.initialize()\n\n        return OpenPypeMongoConnection.get_mongo_client()\n"
  },
  {
    "path": "openpype/lib/openpype_version.py",
    "content": "\"\"\"Lib access to OpenPypeVersion from igniter.\n\nAccess to logic from igniter is available only for OpenPype processes.\nIs meant to be able check OpenPype versions for studio. The logic is dependent\non igniter's inner logic of versions.\n\nKeep in mind that all functions except 'get_installed_version' does not return\nOpenPype version located in build but versions available in remote versions\nrepository or locally available.\n\"\"\"\n\nimport os\nimport sys\n\nimport openpype.version\nfrom openpype import AYON_SERVER_ENABLED\n\nfrom .python_module_tools import import_filepath\n\n\n# ----------------------------------------\n# Functions independent on OpenPypeVersion\n# ----------------------------------------\ndef get_openpype_version():\n    \"\"\"Version of pype that is currently used.\"\"\"\n    return openpype.version.__version__\n\n\ndef get_ayon_launcher_version():\n    version_filepath = os.path.join(\n        os.environ[\"AYON_ROOT\"],\n        \"version.py\"\n    )\n    if not os.path.exists(version_filepath):\n        return None\n    content = {}\n    with open(version_filepath, \"r\") as stream:\n        exec(stream.read(), content)\n    return content[\"__version__\"]\n\n\ndef get_build_version():\n    \"\"\"OpenPype version of build.\"\"\"\n\n    if AYON_SERVER_ENABLED:\n        return get_ayon_launcher_version()\n\n    # Return OpenPype version if is running from code\n    if not is_running_from_build():\n        return get_openpype_version()\n\n    # Import `version.py` from build directory\n    version_filepath = os.path.join(\n        os.environ[\"OPENPYPE_ROOT\"],\n        \"openpype\",\n        \"version.py\"\n    )\n    if not os.path.exists(version_filepath):\n        return None\n\n    module = import_filepath(version_filepath, \"openpype_build_version\")\n    return getattr(module, \"__version__\", None)\n\n\ndef is_running_from_build():\n    \"\"\"Determine if current process is running from build or code.\n\n    Returns:\n        bool: True if running from build.\n    \"\"\"\n\n    if AYON_SERVER_ENABLED:\n        executable_path = os.environ[\"AYON_EXECUTABLE\"]\n    else:\n        executable_path = os.environ[\"OPENPYPE_EXECUTABLE\"]\n    executable_filename = os.path.basename(executable_path)\n    if \"python\" in executable_filename.lower():\n        return False\n    return True\n\n\ndef is_staging_enabled():\n    if AYON_SERVER_ENABLED:\n        return os.getenv(\"AYON_USE_STAGING\") == \"1\"\n    return os.environ.get(\"OPENPYPE_USE_STAGING\") == \"1\"\n\n\ndef is_running_staging():\n    \"\"\"Currently used OpenPype is staging version.\n\n    This function is not 100% proper check of staging version. It is possible\n    to have enabled to use staging version but be in different one.\n\n    The function is based on 4 factors:\n    - env 'OPENPYPE_IS_STAGING' is set\n    - current production version\n    - current staging version\n    - use staging is enabled\n\n    First checks for 'OPENPYPE_IS_STAGING' environment which can be set to '1'.\n    The value should be set only when a process without access to\n    OpenPypeVersion is launched (e.g. in DCCs). If current version is same\n    as production version it is expected that it is not staging, and it\n    doesn't matter what would 'is_staging_enabled' return. If current version\n    is same as staging version it is expected we're in staging. In all other\n    cases 'is_staging_enabled' is used as source of outpu value.\n\n    The function is used to decide which icon is used. To check e.g. updates\n    the output should be combined with other functions from this file.\n\n    Returns:\n        bool: Using staging version or not.\n    \"\"\"\n\n    if AYON_SERVER_ENABLED:\n        return is_staging_enabled()\n\n    if os.environ.get(\"OPENPYPE_IS_STAGING\") == \"1\":\n        return True\n\n    if not op_version_control_available():\n        return False\n\n    from openpype.settings import get_global_settings\n\n    global_settings = get_global_settings()\n    production_version = global_settings[\"production_version\"]\n    latest_version = None\n    if not production_version or production_version == \"latest\":\n        latest_version = get_latest_version(local=False, remote=True)\n        production_version = latest_version\n\n    current_version = get_openpype_version()\n    if current_version == production_version:\n        return False\n\n    staging_version = global_settings[\"staging_version\"]\n    if not staging_version or staging_version == \"latest\":\n        if latest_version is None:\n            latest_version = get_latest_version(local=False, remote=True)\n        staging_version = latest_version\n\n    if current_version == staging_version:\n        return True\n\n    return is_staging_enabled()\n\n\n# ----------------------------------------\n# Functions dependent on OpenPypeVersion\n#   - Make sense to call only in OpenPype process\n# ----------------------------------------\ndef get_OpenPypeVersion():\n    \"\"\"Access to OpenPypeVersion class stored in sys modules.\"\"\"\n    return sys.modules.get(\"OpenPypeVersion\")\n\n\ndef op_version_control_available():\n    \"\"\"Check if current process has access to OpenPypeVersion.\"\"\"\n    if get_OpenPypeVersion() is None:\n        return False\n    return True\n\n\ndef get_installed_version():\n    \"\"\"Get OpenPype version inside build.\n\n    This version is not returned by any other functions here.\n    \"\"\"\n    if op_version_control_available():\n        return get_OpenPypeVersion().get_installed_version()\n    return None\n\n\ndef get_available_versions(*args, **kwargs):\n    \"\"\"Get list of available versions.\"\"\"\n    if op_version_control_available():\n        return get_OpenPypeVersion().get_available_versions(\n            *args, **kwargs\n        )\n    return None\n\n\ndef openpype_path_is_set():\n    \"\"\"OpenPype repository path is set in settings.\"\"\"\n    if op_version_control_available():\n        return get_OpenPypeVersion().openpype_path_is_set()\n    return None\n\n\ndef openpype_path_is_accessible():\n    \"\"\"OpenPype version repository path can be accessed.\"\"\"\n    if op_version_control_available():\n        return get_OpenPypeVersion().openpype_path_is_accessible()\n    return None\n\n\ndef get_local_versions(*args, **kwargs):\n    \"\"\"OpenPype versions available on this workstation.\"\"\"\n    if op_version_control_available():\n        return get_OpenPypeVersion().get_local_versions(*args, **kwargs)\n    return None\n\n\ndef get_remote_versions(*args, **kwargs):\n    \"\"\"OpenPype versions in repository path.\"\"\"\n    if op_version_control_available():\n        return get_OpenPypeVersion().get_remote_versions(*args, **kwargs)\n    return None\n\n\ndef get_latest_version(local=None, remote=None):\n    \"\"\"Get latest version from repository path.\"\"\"\n\n    if op_version_control_available():\n        return get_OpenPypeVersion().get_latest_version(\n            local=local,\n            remote=remote\n        )\n    return None\n\n\ndef get_expected_studio_version(staging=None):\n    \"\"\"Expected production or staging version in studio.\"\"\"\n    if op_version_control_available():\n        if staging is None:\n            staging = is_staging_enabled()\n        return get_OpenPypeVersion().get_expected_studio_version(staging)\n    return None\n\n\ndef get_expected_version(staging=None):\n    expected_version = get_expected_studio_version(staging)\n    if expected_version is None:\n        # Look for latest if expected version is not set in settings\n        expected_version = get_latest_version(\n            local=False,\n            remote=True\n        )\n    return expected_version\n\n\ndef is_current_version_studio_latest():\n    \"\"\"Is currently running OpenPype version which is defined by studio.\n\n    It is not recommended to ask in each process as there may be situations\n    when older OpenPype should be used. For example on farm. But it does make\n    sense in processes that can run for a long time.\n\n    Returns:\n        None: Can't determine. e.g. when running from code or the build is\n            too old.\n        bool: True when is using studio\n    \"\"\"\n    output = None\n    # Skip if is not running from build or build does not support version\n    #   control or path to folder with zip files is not accessible\n    if (\n        not is_running_from_build()\n        or not op_version_control_available()\n        or not openpype_path_is_accessible()\n    ):\n        return output\n\n    # Get OpenPypeVersion class\n    OpenPypeVersion = get_OpenPypeVersion()\n    # Convert current version to OpenPypeVersion object\n    current_version = OpenPypeVersion(version=get_openpype_version())\n\n    # Get expected version (from settings)\n    expected_version = get_expected_version()\n    # Check if current version is expected version\n    return current_version == expected_version\n\n\ndef is_current_version_higher_than_expected():\n    \"\"\"Is current OpenPype version higher than version defined by studio.\n\n    Returns:\n        None: Can't determine. e.g. when running from code or the build is\n            too old.\n        bool: True when is higher than studio version.\n    \"\"\"\n    output = None\n    # Skip if is not running from build or build does not support version\n    #   control or path to folder with zip files is not accessible\n    if (\n        not is_running_from_build()\n        or not op_version_control_available()\n        or not openpype_path_is_accessible()\n    ):\n        return output\n\n    # Get OpenPypeVersion class\n    OpenPypeVersion = get_OpenPypeVersion()\n    # Convert current version to OpenPypeVersion object\n    current_version = OpenPypeVersion(version=get_openpype_version())\n\n    # Get expected version (from settings)\n    expected_version = get_expected_version()\n    # Check if current version is expected version\n    return current_version > expected_version\n"
  },
  {
    "path": "openpype/lib/path_templates.py",
    "content": "import os\nimport re\nimport copy\nimport numbers\nimport collections\n\nimport six\n\nKEY_PATTERN = re.compile(r\"(\\{.*?[^{0]*\\})\")\nKEY_PADDING_PATTERN = re.compile(r\"([^:]+)\\S+[><]\\S+\")\nSUB_DICT_PATTERN = re.compile(r\"([^\\[\\]]+)\")\nOPTIONAL_PATTERN = re.compile(r\"(<.*?[^{0]*>)[^0-9]*?\")\n\n\ndef merge_dict(main_dict, enhance_dict):\n    \"\"\"Merges dictionaries by keys.\n\n    Function call itself if value on key is again dictionary.\n\n    Args:\n        main_dict (dict): First dict to merge second one into.\n        enhance_dict (dict): Second dict to be merged.\n\n    Returns:\n        dict: Merged result.\n\n    .. note:: does not overrides whole value on first found key\n              but only values differences from enhance_dict\n\n    \"\"\"\n    for key, value in enhance_dict.items():\n        if key not in main_dict:\n            main_dict[key] = value\n        elif isinstance(value, dict) and isinstance(main_dict[key], dict):\n            main_dict[key] = merge_dict(main_dict[key], value)\n        else:\n            main_dict[key] = value\n    return main_dict\n\n\nclass TemplateMissingKey(Exception):\n    \"\"\"Exception for cases when key does not exist in template.\"\"\"\n\n    msg = \"Template key does not exist: `{}`.\"\n\n    def __init__(self, parents):\n        parent_join = \"\".join([\"[\\\"{0}\\\"]\".format(key) for key in parents])\n        super(TemplateMissingKey, self).__init__(\n            self.msg.format(parent_join)\n        )\n\n\nclass TemplateUnsolved(Exception):\n    \"\"\"Exception for unsolved template when strict is set to True.\"\"\"\n\n    msg = \"Template \\\"{0}\\\" is unsolved.{1}{2}\"\n    invalid_types_msg = \" Keys with invalid DataType: `{0}`.\"\n    missing_keys_msg = \" Missing keys: \\\"{0}\\\".\"\n\n    def __init__(self, template, missing_keys, invalid_types):\n        invalid_type_items = []\n        for _key, _type in invalid_types.items():\n            invalid_type_items.append(\n                \"\\\"{0}\\\" {1}\".format(_key, str(_type))\n            )\n\n        invalid_types_msg = \"\"\n        if invalid_type_items:\n            invalid_types_msg = self.invalid_types_msg.format(\n                \", \".join(invalid_type_items)\n            )\n\n        missing_keys_msg = \"\"\n        if missing_keys:\n            missing_keys_msg = self.missing_keys_msg.format(\n                \", \".join(missing_keys)\n            )\n        super(TemplateUnsolved, self).__init__(\n            self.msg.format(template, missing_keys_msg, invalid_types_msg)\n        )\n\n\nclass StringTemplate(object):\n    \"\"\"String that can be formatted.\"\"\"\n    def __init__(self, template):\n        if not isinstance(template, six.string_types):\n            raise TypeError(\"<{}> argument must be a string, not {}.\".format(\n                self.__class__.__name__, str(type(template))\n            ))\n\n        self._template = template\n        parts = []\n        last_end_idx = 0\n        for item in KEY_PATTERN.finditer(template):\n            start, end = item.span()\n            if start > last_end_idx:\n                parts.append(template[last_end_idx:start])\n            parts.append(FormattingPart(template[start:end]))\n            last_end_idx = end\n\n        if last_end_idx < len(template):\n            parts.append(template[last_end_idx:len(template)])\n\n        new_parts = []\n        for part in parts:\n            if not isinstance(part, six.string_types):\n                new_parts.append(part)\n                continue\n\n            substr = \"\"\n            for char in part:\n                if char not in (\"<\", \">\"):\n                    substr += char\n                else:\n                    if substr:\n                        new_parts.append(substr)\n                    new_parts.append(char)\n                    substr = \"\"\n            if substr:\n                new_parts.append(substr)\n\n        self._parts = self.find_optional_parts(new_parts)\n\n    def __str__(self):\n        return self.template\n\n    def __repr__(self):\n        return \"<{}> {}\".format(self.__class__.__name__, self.template)\n\n    def __contains__(self, other):\n        return other in self.template\n\n    def replace(self, *args, **kwargs):\n        self._template = self.template.replace(*args, **kwargs)\n        return self\n\n    @property\n    def template(self):\n        return self._template\n\n    def format(self, data):\n        \"\"\" Figure out with whole formatting.\n\n        Separate advanced keys (*Like '{project[name]}') from string which must\n        be formatted separatelly in case of missing or incomplete keys in data.\n\n        Args:\n            data (dict): Containing keys to be filled into template.\n\n        Returns:\n            TemplateResult: Filled or partially filled template containing all\n                data needed or missing for filling template.\n        \"\"\"\n        result = TemplatePartResult()\n        for part in self._parts:\n            if isinstance(part, six.string_types):\n                result.add_output(part)\n            else:\n                part.format(data, result)\n\n        invalid_types = result.invalid_types\n        invalid_types.update(result.invalid_optional_types)\n        invalid_types = result.split_keys_to_subdicts(invalid_types)\n\n        missing_keys = result.missing_keys\n        missing_keys |= result.missing_optional_keys\n\n        solved = result.solved\n        used_values = result.get_clean_used_values()\n\n        return TemplateResult(\n            result.output,\n            self.template,\n            solved,\n            used_values,\n            missing_keys,\n            invalid_types\n        )\n\n    def format_strict(self, *args, **kwargs):\n        result = self.format(*args, **kwargs)\n        result.validate()\n        return result\n\n    @classmethod\n    def format_template(cls, template, data):\n        objected_template = cls(template)\n        return objected_template.format(data)\n\n    @classmethod\n    def format_strict_template(cls, template, data):\n        objected_template = cls(template)\n        return objected_template.format_strict(data)\n\n    @staticmethod\n    def find_optional_parts(parts):\n        new_parts = []\n        tmp_parts = {}\n        counted_symb = -1\n        for part in parts:\n            if part == \"<\":\n                counted_symb += 1\n                tmp_parts[counted_symb] = []\n\n            elif part == \">\":\n                if counted_symb > -1:\n                    parts = tmp_parts.pop(counted_symb)\n                    counted_symb -= 1\n                    # If part contains only single string keep value\n                    #   unchanged\n                    if parts:\n                        # Remove optional start char\n                        parts.pop(0)\n\n                    if not parts:\n                        value = \"<>\"\n                    elif (\n                        len(parts) == 1\n                        and isinstance(parts[0], six.string_types)\n                    ):\n                        value = \"<{}>\".format(parts[0])\n                    else:\n                        value = OptionalPart(parts)\n\n                    if counted_symb < 0:\n                        out_parts = new_parts\n                    else:\n                        out_parts = tmp_parts[counted_symb]\n                    # Store value\n                    out_parts.append(value)\n                    continue\n\n            if counted_symb < 0:\n                new_parts.append(part)\n            else:\n                tmp_parts[counted_symb].append(part)\n\n        if tmp_parts:\n            for idx in sorted(tmp_parts.keys()):\n                new_parts.extend(tmp_parts[idx])\n        return new_parts\n\n\nclass TemplatesDict(object):\n    def __init__(self, templates=None):\n        self._raw_templates = None\n        self._templates = None\n        self._objected_templates = None\n        self.set_templates(templates)\n\n    def set_templates(self, templates):\n        if templates is None:\n            self._raw_templates = None\n            self._templates = None\n            self._objected_templates = None\n        elif isinstance(templates, dict):\n            self._raw_templates = copy.deepcopy(templates)\n            self._templates = templates\n            self._objected_templates = self.create_objected_templates(\n                templates)\n        else:\n            raise TypeError(\"<{}> argument must be a dict, not {}.\".format(\n                self.__class__.__name__, str(type(templates))\n            ))\n\n    def __getitem__(self, key):\n        return self.objected_templates[key]\n\n    def get(self, key, *args, **kwargs):\n        return self.objected_templates.get(key, *args, **kwargs)\n\n    @property\n    def raw_templates(self):\n        return self._raw_templates\n\n    @property\n    def templates(self):\n        return self._templates\n\n    @property\n    def objected_templates(self):\n        return self._objected_templates\n\n    def _create_template_object(self, template):\n        \"\"\"Create template object from a template string.\n\n        Separated into method to give option change class of templates.\n\n        Args:\n            template (str): Template string.\n\n        Returns:\n            StringTemplate: Object of template.\n        \"\"\"\n\n        return StringTemplate(template)\n\n    def create_objected_templates(self, templates):\n        if not isinstance(templates, dict):\n            raise TypeError(\"Expected dict object, got {}\".format(\n                str(type(templates))\n            ))\n\n        objected_templates = copy.deepcopy(templates)\n        inner_queue = collections.deque()\n        inner_queue.append(objected_templates)\n        while inner_queue:\n            item = inner_queue.popleft()\n            if not isinstance(item, dict):\n                continue\n            for key in tuple(item.keys()):\n                value = item[key]\n                if isinstance(value, six.string_types):\n                    item[key] = self._create_template_object(value)\n                elif isinstance(value, dict):\n                    inner_queue.append(value)\n        return objected_templates\n\n    def _format_value(self, value, data):\n        if isinstance(value, StringTemplate):\n            return value.format(data)\n\n        if isinstance(value, dict):\n            return self._solve_dict(value, data)\n        return value\n\n    def _solve_dict(self, templates, data):\n        \"\"\" Solves templates with entered data.\n\n        Args:\n            templates (dict): All templates which will be formatted.\n            data (dict): Containing keys to be filled into template.\n\n        Returns:\n            dict: With `TemplateResult` in values containing filled or\n                partially filled templates.\n        \"\"\"\n        output = collections.defaultdict(dict)\n        for key, value in templates.items():\n            output[key] = self._format_value(value, data)\n\n        return output\n\n    def format(self, in_data, only_keys=True, strict=True):\n        \"\"\" Solves templates based on entered data.\n\n        Args:\n            data (dict): Containing keys to be filled into template.\n            only_keys (bool, optional): Decides if environ will be used to\n                fill templates or only keys in data.\n\n        Returns:\n            TemplatesResultDict: Output `TemplateResult` have `strict`\n                attribute set to True so accessing unfilled keys in templates\n                will raise exceptions with explaned error.\n        \"\"\"\n        # Create a copy of inserted data\n        data = copy.deepcopy(in_data)\n\n        # Add environment variable to data\n        if only_keys is False:\n            for key, val in os.environ.items():\n                env_key = \"$\" + key\n                if env_key not in data:\n                    data[env_key] = val\n\n        solved = self._solve_dict(self.objected_templates, data)\n\n        output = TemplatesResultDict(solved)\n        output.strict = strict\n        return output\n\n\nclass TemplateResult(str):\n    \"\"\"Result of template format with most of information in.\n\n    Args:\n        used_values (dict): Dictionary of template filling data with\n            only used keys.\n        solved (bool): For check if all required keys were filled.\n        template (str): Original template.\n        missing_keys (list): Missing keys that were not in the data. Include\n            missing optional keys.\n        invalid_types (dict): When key was found in data, but value had not\n            allowed DataType. Allowed data types are `numbers`,\n            `str`(`basestring`) and `dict`. Dictionary may cause invalid type\n            when value of key in data is dictionary but template expect string\n            of number.\n    \"\"\"\n\n    used_values = None\n    solved = None\n    template = None\n    missing_keys = None\n    invalid_types = None\n\n    def __new__(\n        cls, filled_template, template, solved,\n        used_values, missing_keys, invalid_types\n    ):\n        new_obj = super(TemplateResult, cls).__new__(cls, filled_template)\n        new_obj.used_values = used_values\n        new_obj.solved = solved\n        new_obj.template = template\n        new_obj.missing_keys = list(set(missing_keys))\n        new_obj.invalid_types = invalid_types\n        return new_obj\n\n    def __copy__(self, *args, **kwargs):\n        return self.copy()\n\n    def __deepcopy__(self, *args, **kwargs):\n        return self.copy()\n\n    def validate(self):\n        if not self.solved:\n            raise TemplateUnsolved(\n                self.template,\n                self.missing_keys,\n                self.invalid_types\n            )\n\n    def copy(self):\n        cls = self.__class__\n        return cls(\n            str(self),\n            self.template,\n            self.solved,\n            self.used_values,\n            self.missing_keys,\n            self.invalid_types\n        )\n\n    def normalized(self):\n        \"\"\"Convert to normalized path.\"\"\"\n\n        cls = self.__class__\n        return cls(\n            os.path.normpath(self.replace(\"\\\\\", \"/\")),\n            self.template,\n            self.solved,\n            self.used_values,\n            self.missing_keys,\n            self.invalid_types\n        )\n\n\nclass TemplatesResultDict(dict):\n    \"\"\"Holds and wrap TemplateResults for easy bug report.\"\"\"\n\n    def __init__(self, in_data, key=None, parent=None, strict=None):\n        super(TemplatesResultDict, self).__init__()\n        for _key, _value in in_data.items():\n            if isinstance(_value, dict):\n                _value = self.__class__(_value, _key, self)\n            self[_key] = _value\n\n        self.key = key\n        self.parent = parent\n        self.strict = strict\n        if self.parent is None and strict is None:\n            self.strict = True\n\n    def __getitem__(self, key):\n        if key not in self.keys():\n            hier = self.hierarchy()\n            hier.append(key)\n            raise TemplateMissingKey(hier)\n\n        value = super(TemplatesResultDict, self).__getitem__(key)\n        if isinstance(value, self.__class__):\n            return value\n\n        # Raise exception when expected solved templates and it is not.\n        if self.raise_on_unsolved and hasattr(value, \"validate\"):\n            value.validate()\n        return value\n\n    @property\n    def raise_on_unsolved(self):\n        \"\"\"To affect this change `strict` attribute.\"\"\"\n        if self.strict is not None:\n            return self.strict\n        return self.parent.raise_on_unsolved\n\n    def hierarchy(self):\n        \"\"\"Return dictionary keys one by one to root parent.\"\"\"\n        if self.parent is None:\n            return []\n\n        hier_keys = []\n        par_hier = self.parent.hierarchy()\n        if par_hier:\n            hier_keys.extend(par_hier)\n        hier_keys.append(self.key)\n\n        return hier_keys\n\n    @property\n    def missing_keys(self):\n        \"\"\"Return missing keys of all children templates.\"\"\"\n        missing_keys = set()\n        for value in self.values():\n            missing_keys |= value.missing_keys\n        return missing_keys\n\n    @property\n    def invalid_types(self):\n        \"\"\"Return invalid types of all children templates.\"\"\"\n        invalid_types = {}\n        for value in self.values():\n            invalid_types = merge_dict(invalid_types, value.invalid_types)\n        return invalid_types\n\n    @property\n    def used_values(self):\n        \"\"\"Return used values for all children templates.\"\"\"\n        used_values = {}\n        for value in self.values():\n            used_values = merge_dict(used_values, value.used_values)\n        return used_values\n\n    def get_solved(self):\n        \"\"\"Get only solved key from templates.\"\"\"\n        result = {}\n        for key, value in self.items():\n            if isinstance(value, self.__class__):\n                value = value.get_solved()\n                if not value:\n                    continue\n                result[key] = value\n\n            elif (\n                not hasattr(value, \"solved\") or\n                value.solved\n            ):\n                result[key] = value\n        return self.__class__(result, key=self.key, parent=self.parent)\n\n\nclass TemplatePartResult:\n    \"\"\"Result to store result of template parts.\"\"\"\n    def __init__(self, optional=False):\n        # Missing keys or invalid value types of required keys\n        self._missing_keys = set()\n        self._invalid_types = {}\n        # Missing keys or invalid value types of optional keys\n        self._missing_optional_keys = set()\n        self._invalid_optional_types = {}\n\n        # Used values stored by key with origin type\n        #   - key without any padding or key modifiers\n        #   - value from filling data\n        #   Example: {\"version\": 1}\n        self._used_values = {}\n        # Used values stored by key with all modifirs\n        #   - value is already formatted string\n        #   Example: {\"version:0>3\": \"001\"}\n        self._realy_used_values = {}\n        # Concatenated string output after formatting\n        self._output = \"\"\n        # Is this result from optional part\n        self._optional = True\n\n    def add_output(self, other):\n        if isinstance(other, six.string_types):\n            self._output += other\n\n        elif isinstance(other, TemplatePartResult):\n            self._output += other.output\n\n            self._missing_keys |= other.missing_keys\n            self._missing_optional_keys |= other.missing_optional_keys\n\n            self._invalid_types.update(other.invalid_types)\n            self._invalid_optional_types.update(other.invalid_optional_types)\n\n            if other.optional and not other.solved:\n                return\n            self._used_values.update(other.used_values)\n            self._realy_used_values.update(other.realy_used_values)\n\n        else:\n            raise TypeError(\"Cannot add data from \\\"{}\\\" to \\\"{}\\\"\".format(\n                str(type(other)), self.__class__.__name__)\n            )\n\n    @property\n    def solved(self):\n        if self.optional:\n            if (\n                len(self.missing_optional_keys) > 0\n                or len(self.invalid_optional_types) > 0\n            ):\n                return False\n        return (\n            len(self.missing_keys) == 0\n            and len(self.invalid_types) == 0\n        )\n\n    @property\n    def optional(self):\n        return self._optional\n\n    @property\n    def output(self):\n        return self._output\n\n    @property\n    def missing_keys(self):\n        return self._missing_keys\n\n    @property\n    def missing_optional_keys(self):\n        return self._missing_optional_keys\n\n    @property\n    def invalid_types(self):\n        return self._invalid_types\n\n    @property\n    def invalid_optional_types(self):\n        return self._invalid_optional_types\n\n    @property\n    def realy_used_values(self):\n        return self._realy_used_values\n\n    @property\n    def used_values(self):\n        return self._used_values\n\n    @staticmethod\n    def split_keys_to_subdicts(values):\n        output = {}\n        for key, value in values.items():\n            key_padding = list(KEY_PADDING_PATTERN.findall(key))\n            if key_padding:\n                key = key_padding[0]\n            key_subdict = list(SUB_DICT_PATTERN.findall(key))\n            data = output\n            last_key = key_subdict.pop(-1)\n            for subkey in key_subdict:\n                if subkey not in data:\n                    data[subkey] = {}\n                data = data[subkey]\n            data[last_key] = value\n        return output\n\n    def get_clean_used_values(self):\n        new_used_values = {}\n        for key, value in self.used_values.items():\n            if isinstance(value, FormatObject):\n                value = str(value)\n            new_used_values[key] = value\n\n        return self.split_keys_to_subdicts(new_used_values)\n\n    def add_realy_used_value(self, key, value):\n        self._realy_used_values[key] = value\n\n    def add_used_value(self, key, value):\n        self._used_values[key] = value\n\n    def add_missing_key(self, key):\n        if self._optional:\n            self._missing_optional_keys.add(key)\n        else:\n            self._missing_keys.add(key)\n\n    def add_invalid_type(self, key, value):\n        if self._optional:\n            self._invalid_optional_types[key] = type(value)\n        else:\n            self._invalid_types[key] = type(value)\n\n\nclass FormatObject(object):\n    \"\"\"Object that can be used for formatting.\n\n    This is base that is valid for to be used in 'StringTemplate' value.\n    \"\"\"\n    def __init__(self):\n        self.value = \"\"\n\n    def __format__(self, *args, **kwargs):\n        return self.value.__format__(*args, **kwargs)\n\n    def __str__(self):\n        return str(self.value)\n\n    def __repr__(self):\n        return self.__str__()\n\n\nclass FormattingPart:\n    \"\"\"String with formatting template.\n\n    Containt only single key to format e.g. \"{project[name]}\".\n\n    Args:\n        template(str): String containing the formatting key.\n    \"\"\"\n    def __init__(self, template):\n        self._template = template\n\n    @property\n    def template(self):\n        return self._template\n\n    def __repr__(self):\n        return \"<Format:{}>\".format(self._template)\n\n    def __str__(self):\n        return self._template\n\n    @staticmethod\n    def validate_value_type(value):\n        \"\"\"Check if value can be used for formatting of single key.\"\"\"\n        if isinstance(value, (numbers.Number, FormatObject)):\n            return True\n\n        for inh_class in type(value).mro():\n            if inh_class in six.string_types:\n                return True\n        return False\n\n    def format(self, data, result):\n        \"\"\"Format the formattings string.\n\n        Args:\n            data(dict): Data that should be used for formatting.\n            result(TemplatePartResult): Object where result is stored.\n        \"\"\"\n        key = self.template[1:-1]\n        if key in result.realy_used_values:\n            result.add_output(result.realy_used_values[key])\n            return result\n\n        # check if key expects subdictionary keys (e.g. project[name])\n        existence_check = key\n        key_padding = list(KEY_PADDING_PATTERN.findall(existence_check))\n        if key_padding:\n            existence_check = key_padding[0]\n        key_subdict = list(SUB_DICT_PATTERN.findall(existence_check))\n\n        value = data\n        missing_key = False\n        invalid_type = False\n        used_keys = []\n        for sub_key in key_subdict:\n            if (\n                value is None\n                or (hasattr(value, \"items\") and sub_key not in value)\n            ):\n                missing_key = True\n                used_keys.append(sub_key)\n                break\n\n            if not hasattr(value, \"items\"):\n                invalid_type = True\n                break\n\n            used_keys.append(sub_key)\n            value = value.get(sub_key)\n\n        if missing_key or invalid_type:\n            if len(used_keys) == 0:\n                invalid_key = key_subdict[0]\n            else:\n                invalid_key = used_keys[0]\n                for idx, sub_key in enumerate(used_keys):\n                    if idx == 0:\n                        continue\n                    invalid_key += \"[{0}]\".format(sub_key)\n\n            if missing_key:\n                result.add_missing_key(invalid_key)\n\n            elif invalid_type:\n                result.add_invalid_type(invalid_key, value)\n\n            result.add_output(self.template)\n            return result\n\n        if self.validate_value_type(value):\n            fill_data = {}\n            first_value = True\n            for used_key in reversed(used_keys):\n                if first_value:\n                    first_value = False\n                    fill_data[used_key] = value\n                else:\n                    _fill_data = {used_key: fill_data}\n                    fill_data = _fill_data\n\n            formatted_value = self.template.format(**fill_data)\n            result.add_realy_used_value(key, formatted_value)\n            result.add_used_value(existence_check, formatted_value)\n            result.add_output(formatted_value)\n            return result\n\n        result.add_invalid_type(key, value)\n        result.add_output(self.template)\n\n        return result\n\n\nclass OptionalPart:\n    \"\"\"Template part which contains optional formatting strings.\n\n    If this part can't be filled the result is empty string.\n\n    Args:\n        parts(list): Parts of template. Can contain 'str', 'OptionalPart' or\n            'FormattingPart'.\n    \"\"\"\n\n    def __init__(self, parts):\n        self._parts = parts\n\n    @property\n    def parts(self):\n        return self._parts\n\n    def __str__(self):\n        return \"<{}>\".format(\"\".join([str(p) for p in self._parts]))\n\n    def __repr__(self):\n        return \"<Optional:{}>\".format(\"\".join([str(p) for p in self._parts]))\n\n    def format(self, data, result):\n        new_result = TemplatePartResult(True)\n        for part in self._parts:\n            if isinstance(part, six.string_types):\n                new_result.add_output(part)\n            else:\n                part.format(data, new_result)\n\n        if new_result.solved:\n            result.add_output(new_result)\n        return result\n"
  },
  {
    "path": "openpype/lib/path_tools.py",
    "content": "import os\nimport re\nimport logging\nimport platform\n\nimport clique\n\nlog = logging.getLogger(__name__)\n\n\ndef format_file_size(file_size, suffix=None):\n    \"\"\"Returns formatted string with size in appropriate unit.\n\n    Args:\n        file_size (int): Size of file in bytes.\n        suffix (str): Suffix for formatted size. Default is 'B' (as bytes).\n\n    Returns:\n        str: Formatted size using proper unit and passed suffix (e.g. 7 MiB).\n    \"\"\"\n\n    if suffix is None:\n        suffix = \"B\"\n\n    for unit in [\"\", \"Ki\", \"Mi\", \"Gi\", \"Ti\", \"Pi\", \"Ei\", \"Zi\"]:\n        if abs(file_size) < 1024.0:\n            return \"%3.1f%s%s\" % (file_size, unit, suffix)\n        file_size /= 1024.0\n    return \"%.1f%s%s\" % (file_size, \"Yi\", suffix)\n\n\ndef create_hard_link(src_path, dst_path):\n    \"\"\"Create hardlink of file.\n\n    Args:\n        src_path(str): Full path to a file which is used as source for\n            hardlink.\n        dst_path(str): Full path to a file where a link of source will be\n            added.\n    \"\"\"\n    # Use `os.link` if is available\n    #   - should be for all platforms with newer python versions\n    if hasattr(os, \"link\"):\n        os.link(src_path, dst_path)\n        return\n\n    # Windows implementation of hardlinks\n    #   - used in Python 2\n    if platform.system().lower() == \"windows\":\n        import ctypes\n        from ctypes.wintypes import BOOL\n        CreateHardLink = ctypes.windll.kernel32.CreateHardLinkW\n        CreateHardLink.argtypes = [\n            ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_void_p\n        ]\n        CreateHardLink.restype = BOOL\n\n        res = CreateHardLink(dst_path, src_path, None)\n        if res == 0:\n            raise ctypes.WinError()\n        return\n    # Raises not implemented error if gets here\n    raise NotImplementedError(\n        \"Implementation of hardlink for current environment is missing.\"\n    )\n\n\ndef collect_frames(files):\n    \"\"\"Returns dict of source path and its frame, if from sequence\n\n    Uses clique as most precise solution, used when anatomy template that\n    created files is not known.\n\n    Assumption is that frames are separated by '.', negative frames are not\n    allowed.\n\n    Args:\n        files(list) or (set with single value): list of source paths\n\n    Returns:\n        (dict): {'/asset/subset_v001.0001.png': '0001', ....}\n    \"\"\"\n\n    patterns = [clique.PATTERNS[\"frames\"]]\n    collections, remainder = clique.assemble(\n        files, minimum_items=1, patterns=patterns)\n\n    sources_and_frames = {}\n    if collections:\n        for collection in collections:\n            src_head = collection.head\n            src_tail = collection.tail\n\n            for index in collection.indexes:\n                src_frame = collection.format(\"{padding}\") % index\n                src_file_name = \"{}{}{}\".format(\n                    src_head, src_frame, src_tail)\n                sources_and_frames[src_file_name] = src_frame\n    else:\n        sources_and_frames[remainder.pop()] = None\n\n    return sources_and_frames\n\n\ndef _rreplace(s, a, b, n=1):\n    \"\"\"Replace a with b in string s from right side n times.\"\"\"\n    return b.join(s.rsplit(a, n))\n\n\ndef version_up(filepath):\n    \"\"\"Version up filepath to a new non-existing version.\n\n    Parses for a version identifier like `_v001` or `.v001`\n    When no version present _v001 is appended as suffix.\n\n    Args:\n        filepath (str): full url\n\n    Returns:\n        (str): filepath with increased version number\n\n    \"\"\"\n    dirname = os.path.dirname(filepath)\n    basename, ext = os.path.splitext(os.path.basename(filepath))\n\n    regex = r\"[._]v\\d+\"\n    matches = re.findall(regex, str(basename), re.IGNORECASE)\n    if not matches:\n        log.info(\"Creating version...\")\n        new_label = \"_v{version:03d}\".format(version=1)\n        new_basename = \"{}{}\".format(basename, new_label)\n    else:\n        label = matches[-1]\n        version = re.search(r\"\\d+\", label).group()\n        padding = len(version)\n\n        new_version = int(version) + 1\n        new_version = '{version:0{padding}d}'.format(version=new_version,\n                                                     padding=padding)\n        new_label = label.replace(version, new_version, 1)\n        new_basename = _rreplace(basename, label, new_label)\n    new_filename = \"{}{}\".format(new_basename, ext)\n    new_filename = os.path.join(dirname, new_filename)\n    new_filename = os.path.normpath(new_filename)\n\n    if new_filename == filepath:\n        raise RuntimeError(\"Created path is the same as current file,\"\n                           \"this is a bug\")\n\n    # We check for version clashes against the current file for any file\n    # that matches completely in name up to the {version} label found. Thus\n    # if source file was test_v001_test.txt we want to also check clashes\n    # against test_v002.txt but do want to preserve the part after the version\n    # label for our new filename\n    clash_basename = new_basename\n    if not clash_basename.endswith(new_label):\n        index = (clash_basename.find(new_label))\n        index += len(new_label)\n        clash_basename = clash_basename[:index]\n\n    for file in os.listdir(dirname):\n        if file.endswith(ext) and file.startswith(clash_basename):\n            log.info(\"Skipping existing version %s\" % new_label)\n            return version_up(new_filename)\n\n    log.info(\"New version %s\" % new_label)\n    return new_filename\n\n\ndef get_version_from_path(file):\n    \"\"\"Find version number in file path string.\n\n    Args:\n        file (str): file path\n\n    Returns:\n        str: version number in string ('001')\n    \"\"\"\n\n    pattern = re.compile(r\"[\\._]v([0-9]+)\", re.IGNORECASE)\n    try:\n        return pattern.findall(file)[-1]\n    except IndexError:\n        log.error(\n            \"templates:get_version_from_workfile:\"\n            \"`{}` missing version string.\"\n            \"Example `v004`\".format(file)\n        )\n\n\ndef get_last_version_from_path(path_dir, filter):\n    \"\"\"Find last version of given directory content.\n\n    Args:\n        path_dir (str): directory path\n        filter (list): list of strings used as file name filter\n\n    Returns:\n        str: file name with last version\n\n    Example:\n        last_version_file = get_last_version_from_path(\n            \"/project/shots/shot01/work\", [\"shot01\", \"compositing\", \"nk\"])\n    \"\"\"\n\n    assert os.path.isdir(path_dir), \"`path_dir` argument needs to be directory\"\n    assert isinstance(filter, list) and (\n        len(filter) != 0), \"`filter` argument needs to be list and not empty\"\n\n    filtred_files = list()\n\n    # form regex for filtering\n    pattern = r\".*\".join(filter)\n\n    for file in os.listdir(path_dir):\n        if not re.findall(pattern, file):\n            continue\n        filtred_files.append(file)\n\n    if filtred_files:\n        sorted(filtred_files)\n        return filtred_files[-1]\n\n    return None\n"
  },
  {
    "path": "openpype/lib/plugin_tools.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Avalon/Pyblish plugin tools.\"\"\"\nimport os\nimport logging\nimport re\n\nlog = logging.getLogger(__name__)\n\n\ndef prepare_template_data(fill_pairs):\n    \"\"\"\n        Prepares formatted data for filling template.\n\n        It produces multiple variants of keys (key, Key, KEY) to control\n        format of filled template.\n\n        Args:\n            fill_pairs (iterable) of tuples (key, value)\n        Returns:\n            (dict)\n            ('host', 'maya') > {'host':'maya', 'Host': 'Maya', 'HOST': 'MAYA'}\n\n    \"\"\"\n    fill_data = {}\n    regex = re.compile(r\"[a-zA-Z0-9]\")\n    for key, value in dict(fill_pairs).items():\n        # Handle cases when value is `None` (standalone publisher)\n        if value is None:\n            continue\n        # Keep value as it is\n        fill_data[key] = value\n        # Both key and value are with upper case\n        fill_data[key.upper()] = value.upper()\n\n        # Capitalize only first char of value\n        # - conditions are because of possible index errors\n        # - regex is to skip symbols that are not chars or numbers\n        #   - e.g. \"{key}\" which starts with curly bracket\n        capitalized = \"\"\n        for idx in range(len(value or \"\")):\n            char = value[idx]\n            if not regex.match(char):\n                capitalized += char\n            else:\n                capitalized += char.upper()\n                capitalized += value[idx + 1:]\n                break\n\n        fill_data[key.capitalize()] = capitalized\n\n    return fill_data\n\n\ndef source_hash(filepath, *args):\n    \"\"\"Generate simple identifier for a source file.\n    This is used to identify whether a source file has previously been\n    processe into the pipeline, e.g. a texture.\n    The hash is based on source filepath, modification time and file size.\n    This is only used to identify whether a specific source file was already\n    published before from the same location with the same modification date.\n    We opt to do it this way as opposed to Avalanch C4 hash as this is much\n    faster and predictable enough for all our production use cases.\n    Args:\n        filepath (str): The source file path.\n    You can specify additional arguments in the function\n    to allow for specific 'processing' values to be included.\n    \"\"\"\n    # We replace dots with comma because . cannot be a key in a pymongo dict.\n    file_name = os.path.basename(filepath)\n    time = str(os.path.getmtime(filepath))\n    size = str(os.path.getsize(filepath))\n    return \"|\".join([file_name, time, size] + list(args)).replace(\".\", \",\")\n"
  },
  {
    "path": "openpype/lib/profiles_filtering.py",
    "content": "import re\nimport logging\n\nlog = logging.getLogger(__name__)\n\n\ndef compile_list_of_regexes(in_list):\n    \"\"\"Convert strings in entered list to compiled regex objects.\"\"\"\n    regexes = list()\n    if not in_list:\n        return regexes\n\n    for item in in_list:\n        if not item:\n            continue\n        try:\n            regexes.append(re.compile(item))\n        except TypeError:\n            print((\n                \"Invalid type \\\"{}\\\" value \\\"{}\\\".\"\n                \" Expected string based object. Skipping.\"\n            ).format(str(type(item)), str(item)))\n    return regexes\n\n\ndef _profile_exclusion(matching_profiles, logger):\n    \"\"\"Find out most matching profile byt host, task and family match.\n\n    Profiles are selectively filtered. Each item in passed argument must\n    contain tuple of (profile, profile's score) where score is list of\n    booleans. Each boolean represents existence of filter for specific key.\n    Profiles are looped in sequence. In each sequence are profiles split into\n    true_list and false_list. For next sequence loop are used profiles in\n    true_list if there are any profiles else false_list is used.\n\n    Filtering ends when only one profile left in true_list. Or when all\n    existence booleans loops passed, in that case first profile from remainded\n    profiles is returned.\n\n    Args:\n        matching_profiles (list): Profiles with same scores. Each item is tuple\n            with (profile, profile values)\n\n    Returns:\n        dict: Most matching profile.\n    \"\"\"\n    if not matching_profiles:\n        return None\n\n    if len(matching_profiles) == 1:\n        return matching_profiles[0][0]\n\n    scores_len = len(matching_profiles[0][1])\n    for idx in range(scores_len):\n        profiles_true = []\n        profiles_false = []\n        for profile, score in matching_profiles:\n            if score[idx]:\n                profiles_true.append((profile, score))\n            else:\n                profiles_false.append((profile, score))\n\n        if profiles_true:\n            matching_profiles = profiles_true\n        else:\n            matching_profiles = profiles_false\n\n        if len(matching_profiles) == 1:\n            return matching_profiles[0][0]\n\n    return matching_profiles[0][0]\n\n\ndef fullmatch(regex, string, flags=0):\n    \"\"\"Emulate python-3.4 re.fullmatch().\"\"\"\n    matched = re.match(regex, string, flags=flags)\n    if matched and matched.span()[1] == len(string):\n        return matched\n    return None\n\n\ndef validate_value_by_regexes(value, in_list):\n    \"\"\"Validates in any regex from list match entered value.\n\n    Args:\n        value (str): String where regexes is checked.\n        in_list (list): List with regexes.\n\n    Returns:\n        int: Returns `0` when list is not set, is empty or contain \"*\".\n            Returns `1` when any regex match value and returns `-1`\n            when none of regexes match entered value.\n    \"\"\"\n    if not in_list:\n        return 0\n\n    if not isinstance(in_list, (list, tuple, set)):\n        in_list = [in_list]\n\n    if \"*\" in in_list:\n        return 0\n\n    # If value is not set and in list has specific values then resolve value\n    #   as not matching.\n    if not value:\n        return -1\n\n    regexes = compile_list_of_regexes(in_list)\n    for regex in regexes:\n        if hasattr(regex, \"fullmatch\"):\n            result = regex.fullmatch(value)\n        else:\n            result = fullmatch(regex, value)\n        if result:\n            return 1\n    return -1\n\n\ndef filter_profiles(profiles_data, key_values, keys_order=None, logger=None):\n    \"\"\" Filter profiles by entered key -> values.\n\n    Profile if marked with score for each key/value from `key_values` with\n    points -1, 0 or 1.\n    - if profile contain the key and profile's value contain value from\n        `key_values` then profile gets 1 point\n    - if profile does not contain the key or profile's value is empty or\n        contain \"*\" then got 0 point\n    - if profile contain the key, profile's value is not empty and does not\n        contain \"*\" and value from `key_values` is not available in the value\n        then got -1 point\n\n    If profile gets -1 point at any time then is skipped and not used for\n    output. Profile with higher score is returned. If there are multiple\n    profiles with same score then first in order is used (order of profiles\n    matter).\n\n    Args:\n        profiles_data (list): Profile definitions as dictionaries.\n        key_values (dict): Mapping of Key <-> Value. Key is checked if is\n            available in profile and if Value is matching it's values.\n        keys_order (list, tuple): Order of keys from `key_values` which matters\n            only when multiple profiles have same score.\n        logger (logging.Logger): Optionally can be passed different logger.\n\n    Returns:\n        dict/None: Return most matching profile or None if none of profiles\n            match at least one criteria.\n    \"\"\"\n    if not profiles_data:\n        return None\n\n    if not logger:\n        logger = log\n\n    if not keys_order:\n        keys_order = tuple(key_values.keys())\n    else:\n        _keys_order = list(keys_order)\n        # Make all keys from `key_values` are passed\n        for key in key_values.keys():\n            if key not in _keys_order:\n                _keys_order.append(key)\n        keys_order = tuple(_keys_order)\n\n    log_parts = \" | \".join([\n        \"{}: \\\"{}\\\"\".format(*item)\n        for item in key_values.items()\n    ])\n\n    logger.debug(\n        \"Looking for matching profile for: {}\".format(log_parts)\n    )\n\n    matching_profiles = None\n    highest_profile_points = -1\n    # Each profile get 1 point for each matching filter. Profile with most\n    # points is returned. For cases when more than one profile will match\n    # are also stored ordered lists of matching values.\n    for profile in profiles_data:\n        profile_points = 0\n        profile_scores = []\n\n        for key in keys_order:\n            value = key_values[key]\n            match = validate_value_by_regexes(value, profile.get(key))\n            if match == -1:\n                profile_value = profile.get(key) or []\n                logger.debug(\n                    \"\\\"{}\\\" not found in \\\"{}\\\": {}\".format(value, key,\n                                                            profile_value)\n                )\n                profile_points = -1\n                break\n\n            profile_points += match\n            profile_scores.append(bool(match))\n\n        if (\n            profile_points < 0\n            or profile_points < highest_profile_points\n        ):\n            continue\n\n        if profile_points > highest_profile_points:\n            matching_profiles = []\n            highest_profile_points = profile_points\n\n        if profile_points == highest_profile_points:\n            matching_profiles.append((profile, profile_scores))\n\n    if not matching_profiles:\n        logger.debug(\n            \"None of profiles match your setup. {}\".format(log_parts)\n        )\n        return None\n\n    if len(matching_profiles) > 1:\n        logger.debug(\n            \"More than one profile match your setup. {}\".format(log_parts)\n        )\n\n    profile = _profile_exclusion(matching_profiles, logger)\n    if profile:\n        logger.debug(\n            \"Profile selected: {}\".format(profile)\n        )\n    return profile\n"
  },
  {
    "path": "openpype/lib/profiling.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Provide profiling decorator.\"\"\"\nimport os\nimport cProfile\n\n\ndef do_profile(fn, to_file=None):\n    \"\"\"Wraps function in profiler run and print stat after it is done.\n\n    Args:\n        to_file (str, optional): If specified, dumps stats into the file\n        instead of printing.\n\n    \"\"\"\n    if to_file:\n        to_file = to_file.format(pid=os.getpid())\n\n    def profiled(*args, **kwargs):\n        profiler = cProfile.Profile()\n        try:\n            profiler.enable()\n            res = fn(*args, **kwargs)\n            profiler.disable()\n            return res\n        finally:\n            if to_file:\n                profiler.dump_stats(to_file)\n            else:\n                profiler.print_stats()\n"
  },
  {
    "path": "openpype/lib/project_backpack.py",
    "content": "\"\"\"These lib functions are for development purposes.\n\nWARNING:\n    This is not meant for production data. Please don't write code which is\n        dependent on functionality here.\n\nGoal is to be able to create package of current state of project with related\ndocuments from mongo and files from disk to zip file and then be able\nto recreate the project based on the zip.\n\nThis gives ability to create project where a changes and tests can be done.\n\nKeep in mind that to be able to create a package of project has few\nrequirements. Possible requirement should be listed in 'pack_project' function.\n\"\"\"\n\nimport os\nimport json\nimport platform\nimport tempfile\nimport shutil\nimport datetime\n\nimport zipfile\nfrom openpype.client.mongo import (\n    load_json_file,\n    get_project_connection,\n    replace_project_documents,\n    store_project_documents,\n)\n\nDOCUMENTS_FILE_NAME = \"database\"\nMETADATA_FILE_NAME = \"metadata\"\nPROJECT_FILES_DIR = \"project_files\"\n\n\ndef add_timestamp(filepath):\n    \"\"\"Add timestamp string to a file.\"\"\"\n    base, ext = os.path.splitext(filepath)\n    timestamp = datetime.datetime.now().strftime(\"%y%m%d_%H%M%S\")\n    new_base = \"{}_{}\".format(base, timestamp)\n    return new_base + ext\n\n\ndef get_project_document(project_name, database_name=None):\n    \"\"\"Query project document.\n\n    Function 'get_project' from client api cannot be used as it does not allow\n    to change which 'database_name' is used.\n\n    Args:\n        project_name (str): Name of project.\n        database_name (Optional[str]): Name of mongo database where to look for\n            project.\n\n    Returns:\n        Union[dict[str, Any], None]: Project document or None.\n    \"\"\"\n\n    col = get_project_connection(project_name, database_name)\n    return col.find_one({\"type\": \"project\"})\n\n\ndef _pack_files_to_zip(zip_stream, source_path, root_path):\n    \"\"\"Pack files to a zip stream.\n\n    Args:\n        zip_stream (zipfile.ZipFile): Stream to a zipfile.\n        source_path (str): Path to a directory where files are.\n        root_path (str): Path to a directory which is used for calculation\n            of relative path.\n    \"\"\"\n\n    for root, _, filenames in os.walk(source_path):\n        for filename in filenames:\n            filepath = os.path.join(root, filename)\n            # TODO add one more folder\n            archive_name = os.path.join(\n                PROJECT_FILES_DIR,\n                os.path.relpath(filepath, root_path)\n            )\n            zip_stream.write(filepath, archive_name)\n\n\ndef pack_project(\n    project_name,\n    destination_dir=None,\n    only_documents=False,\n    database_name=None\n):\n    \"\"\"Make a package of a project with mongo documents and files.\n\n    This function has few restrictions:\n    - project must have only one root\n    - project must have all templates starting with\n        \"{root[...]}/{project[name]}\"\n\n    Args:\n        project_name (str): Project that should be packaged.\n        destination_dir (Optional[str]): Optional path where zip will be\n            stored. Project's root is used if not passed.\n        only_documents (Optional[bool]): Pack only Mongo documents and skip\n            files.\n        database_name (Optional[str]): Custom database name from which is\n            project queried.\n    \"\"\"\n\n    print(\"Creating package of project \\\"{}\\\"\".format(project_name))\n    # Validate existence of project\n    project_doc = get_project_document(project_name, database_name)\n    if not project_doc:\n        raise ValueError(\"Project \\\"{}\\\" was not found in database\".format(\n            project_name\n        ))\n\n    if only_documents and not destination_dir:\n        raise ValueError((\n            \"Destination directory must be defined\"\n            \" when only documents should be packed.\"\n        ))\n\n    root_path = None\n    source_root = {}\n    project_source_path = None\n    if not only_documents:\n        roots = project_doc[\"config\"][\"roots\"]\n        # Determine root directory of project\n        source_root = None\n        source_root_name = None\n        for root_name, root_value in roots.items():\n            if source_root is not None:\n                raise ValueError(\n                    \"Packaging is supported only for single root projects\"\n                )\n            source_root = root_value\n            source_root_name = root_name\n\n        root_path = source_root[platform.system().lower()]\n        print(\"Using root \\\"{}\\\" with path \\\"{}\\\"\".format(\n            source_root_name, root_path\n        ))\n\n        project_source_path = os.path.join(root_path, project_name)\n        if not os.path.exists(project_source_path):\n            raise ValueError(\"Didn't find source of project files\")\n\n    # Determine zip filepath where data will be stored\n    if not destination_dir:\n        destination_dir = root_path\n\n    if not destination_dir:\n        raise ValueError(\n            \"Project {} does not have any roots.\".format(project_name)\n        )\n\n    destination_dir = os.path.normpath(destination_dir)\n    if not os.path.exists(destination_dir):\n        os.makedirs(destination_dir)\n\n    zip_path = os.path.join(destination_dir, project_name + \".zip\")\n\n    print(\"Project will be packaged into \\\"{}\\\"\".format(zip_path))\n    # Rename already existing zip\n    if os.path.exists(zip_path):\n        dst_filepath = add_timestamp(zip_path)\n        os.rename(zip_path, dst_filepath)\n\n    # We can add more data\n    metadata = {\n        \"project_name\": project_name,\n        \"root\": source_root,\n        \"version\": 1\n    }\n    # Create temp json file where metadata are stored\n    with tempfile.NamedTemporaryFile(\"w\", suffix=\".json\", delete=False) as s:\n        temp_metadata_json = s.name\n\n    with open(temp_metadata_json, \"w\") as stream:\n        json.dump(metadata, stream)\n\n    # Create temp json file where database documents are stored\n    with tempfile.NamedTemporaryFile(\"w\", suffix=\".json\", delete=False) as s:\n        temp_docs_json = s.name\n\n    # Query all project documents and store them to temp json\n    store_project_documents(project_name, temp_docs_json, database_name)\n\n    print(\"Packing files into zip\")\n    # Write all to zip file\n    with zipfile.ZipFile(zip_path, \"w\", zipfile.ZIP_DEFLATED) as zip_stream:\n        # Add metadata file\n        zip_stream.write(temp_metadata_json, METADATA_FILE_NAME + \".json\")\n        # Add database documents\n        zip_stream.write(temp_docs_json, DOCUMENTS_FILE_NAME + \".json\")\n\n        # Add project files to zip\n        if not only_documents:\n            _pack_files_to_zip(zip_stream, project_source_path, root_path)\n\n    print(\"Cleaning up\")\n    # Cleanup\n    os.remove(temp_docs_json)\n    os.remove(temp_metadata_json)\n\n    print(\"*** Packing finished ***\")\n\n\ndef _unpack_project_files(unzip_dir, root_path, project_name):\n    \"\"\"Move project files from unarchived temp folder to new root.\n\n    Unpack is skipped if source files are not available in the zip. That can\n    happen if nothing was published yet or only documents were stored to\n    package.\n\n    Args:\n        unzip_dir (str): Location where zip was unzipped.\n        root_path (str): Path to new root.\n        project_name (str): Name of project.\n    \"\"\"\n\n    src_project_files_dir = os.path.join(\n        unzip_dir, PROJECT_FILES_DIR, project_name\n    )\n    # Skip if files are not in the zip\n    if not os.path.exists(src_project_files_dir):\n        return\n\n    # Make sure root path exists\n    if not os.path.exists(root_path):\n        os.makedirs(root_path)\n\n    dst_project_files_dir = os.path.normpath(\n        os.path.join(root_path, project_name)\n    )\n    if os.path.exists(dst_project_files_dir):\n        new_path = add_timestamp(dst_project_files_dir)\n        print(\"Project folder already exists. Renamed \\\"{}\\\" -> \\\"{}\\\"\".format(\n            dst_project_files_dir, new_path\n        ))\n        os.rename(dst_project_files_dir, new_path)\n\n    print(\"Moving project files from temp \\\"{}\\\" -> \\\"{}\\\"\".format(\n        src_project_files_dir, dst_project_files_dir\n    ))\n    shutil.move(src_project_files_dir, dst_project_files_dir)\n\n\ndef unpack_project(\n    path_to_zip, new_root=None, database_only=None, database_name=None\n):\n    \"\"\"Unpack project zip file to recreate project.\n\n    Args:\n        path_to_zip (str): Path to zip which was created using 'pack_project'\n            function.\n        new_root (str): Optional way how to set different root path for\n            unpacked project.\n        database_only (Optional[bool]): Unpack only database from zip.\n        database_name (str): Name of database where project will be recreated.\n    \"\"\"\n\n    if database_only is None:\n        database_only = False\n\n    print(\"Unpacking project from zip {}\".format(path_to_zip))\n    if not os.path.exists(path_to_zip):\n        print(\"Zip file does not exists: {}\".format(path_to_zip))\n        return\n\n    tmp_dir = tempfile.mkdtemp(prefix=\"unpack_\")\n    print(\"Zip is extracted to temp: {}\".format(tmp_dir))\n    with zipfile.ZipFile(path_to_zip, \"r\") as zip_stream:\n        if database_only:\n            for filename in (\n                \"{}.json\".format(METADATA_FILE_NAME),\n                \"{}.json\".format(DOCUMENTS_FILE_NAME),\n            ):\n                zip_stream.extract(filename, tmp_dir)\n        else:\n            zip_stream.extractall(tmp_dir)\n\n    metadata_json_path = os.path.join(tmp_dir, METADATA_FILE_NAME + \".json\")\n    with open(metadata_json_path, \"r\") as stream:\n        metadata = json.load(stream)\n\n    docs_json_path = os.path.join(tmp_dir, DOCUMENTS_FILE_NAME + \".json\")\n    docs = load_json_file(docs_json_path)\n\n    low_platform = platform.system().lower()\n    project_name = metadata[\"project_name\"]\n    root_path = metadata[\"root\"].get(low_platform)\n\n    # Drop existing collection\n    replace_project_documents(project_name, docs, database_name)\n    print(\"Creating project documents ({})\".format(len(docs)))\n\n    # Skip change of root if is the same as the one stored in metadata\n    if (\n        new_root\n        and (os.path.normpath(new_root) == os.path.normpath(root_path))\n    ):\n        new_root = None\n\n    if new_root:\n        print(\"Using different root path {}\".format(new_root))\n        root_path = new_root\n\n        project_doc = get_project_document(project_name)\n        roots = project_doc[\"config\"][\"roots\"]\n        key = tuple(roots.keys())[0]\n        update_key = \"config.roots.{}.{}\".format(key, low_platform)\n        collection = get_project_connection(project_name, database_name)\n        collection.update_one(\n            {\"_id\": project_doc[\"_id\"]},\n            {\"$set\": {\n                update_key: new_root\n            }}\n        )\n\n    _unpack_project_files(tmp_dir, root_path, project_name)\n\n    # CLeanup\n    print(\"Cleaning up\")\n    shutil.rmtree(tmp_dir)\n    print(\"*** Unpack finished ***\")\n"
  },
  {
    "path": "openpype/lib/pype_info.py",
    "content": "import os\nimport json\nimport datetime\nimport platform\nimport getpass\nimport socket\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.settings.lib import get_local_settings\nfrom .execute import get_openpype_execute_args\nfrom .local_settings import get_local_site_id\nfrom .openpype_version import (\n    is_running_from_build,\n    get_openpype_version,\n    get_build_version\n)\n\n\ndef get_openpype_info():\n    \"\"\"Information about currently used Pype process.\"\"\"\n    executable_args = get_openpype_execute_args()\n    if is_running_from_build():\n        version_type = \"build\"\n    else:\n        version_type = \"code\"\n\n    return {\n        \"build_verison\": get_build_version(),\n        \"version\": get_openpype_version(),\n        \"version_type\": version_type,\n        \"executable\": executable_args[-1],\n        \"pype_root\": os.environ[\"OPENPYPE_REPOS_ROOT\"],\n        \"mongo_url\": os.environ[\"OPENPYPE_MONGO\"]\n    }\n\n\ndef get_ayon_info():\n    executable_args = get_openpype_execute_args()\n    if is_running_from_build():\n        version_type = \"build\"\n    else:\n        version_type = \"code\"\n    return {\n        \"build_verison\": get_build_version(),\n        \"version_type\": version_type,\n        \"executable\": executable_args[-1],\n        \"ayon_root\": os.environ[\"AYON_ROOT\"],\n        \"server_url\": os.environ[\"AYON_SERVER_URL\"]\n    }\n\n\ndef get_workstation_info():\n    \"\"\"Basic information about workstation.\"\"\"\n    host_name = socket.gethostname()\n    try:\n        host_ip = socket.gethostbyname(host_name)\n    except socket.gaierror:\n        host_ip = \"127.0.0.1\"\n\n    return {\n        \"hostname\": host_name,\n        \"hostip\": host_ip,\n        \"username\": getpass.getuser(),\n        \"system_name\": platform.system(),\n        \"local_id\": get_local_site_id()\n    }\n\n\ndef get_all_current_info():\n    \"\"\"All information about current process in one dictionary.\"\"\"\n\n    output = {\n        \"workstation\": get_workstation_info(),\n        \"env\": os.environ.copy(),\n        \"local_settings\": get_local_settings()\n    }\n    if AYON_SERVER_ENABLED:\n        output[\"ayon\"] = get_ayon_info()\n    else:\n        output[\"openpype\"] = get_openpype_info()\n    return output\n\n\ndef extract_pype_info_to_file(dirpath):\n    \"\"\"Extract all current info to a file.\n\n    It is possible to define onpy directory path. Filename is concatenated with\n    pype version, workstation site id and timestamp.\n\n    Args:\n        dirpath (str): Path to directory where file will be stored.\n\n    Returns:\n        filepath (str): Full path to file where data were extracted.\n    \"\"\"\n    filename = \"{}_{}_{}.json\".format(\n        get_openpype_version(),\n        get_local_site_id(),\n        datetime.datetime.now().strftime(\"%y%m%d%H%M%S\")\n    )\n    filepath = os.path.join(dirpath, filename)\n    data = get_all_current_info()\n    if not os.path.exists(dirpath):\n        os.makedirs(dirpath)\n\n    with open(filepath, \"w\") as file_stream:\n        json.dump(data, file_stream, indent=4)\n    return filepath\n"
  },
  {
    "path": "openpype/lib/python_2_comp.py",
    "content": "import weakref\n\n\nWeakMethod = getattr(weakref, \"WeakMethod\", None)\n\nif WeakMethod is None:\n    class _WeakCallable:\n        def __init__(self, obj, func):\n            self.im_self = obj\n            self.im_func = func\n\n        def __call__(self, *args, **kws):\n            if self.im_self is None:\n                return self.im_func(*args, **kws)\n            else:\n                return self.im_func(self.im_self, *args, **kws)\n\n\n    class WeakMethod:\n        \"\"\" Wraps a function or, more importantly, a bound method in\n        a way that allows a bound method's object to be GCed, while\n        providing the same interface as a normal weak reference. \"\"\"\n\n        def __init__(self, fn):\n            try:\n                self._obj = weakref.ref(fn.im_self)\n                self._meth = fn.im_func\n            except AttributeError:\n                # It's not a bound method\n                self._obj = None\n                self._meth = fn\n\n        def __call__(self):\n            if self._dead():\n                return None\n            return _WeakCallable(self._getobj(), self._meth)\n\n        def _dead(self):\n            return self._obj is not None and self._obj() is None\n\n        def _getobj(self):\n            if self._obj is None:\n                return None\n            return self._obj()\n"
  },
  {
    "path": "openpype/lib/python_module_tools.py",
    "content": "import os\nimport sys\nimport types\nimport importlib\nimport inspect\nimport logging\n\nimport six\n\nlog = logging.getLogger(__name__)\n\n\ndef import_filepath(filepath, module_name=None):\n    \"\"\"Import python file as python module.\n\n    Python 2 and Python 3 compatibility.\n\n    Args:\n        filepath(str): Path to python file.\n        module_name(str): Name of loaded module. Only for Python 3. By default\n            is filled with filename of filepath.\n    \"\"\"\n    if module_name is None:\n        module_name = os.path.splitext(os.path.basename(filepath))[0]\n\n    # Make sure it is not 'unicode' in Python 2\n    module_name = str(module_name)\n\n    # Prepare module object where content of file will be parsed\n    module = types.ModuleType(module_name)\n    module.__file__ = filepath\n\n    if six.PY3:\n        # Use loader so module has full specs\n        module_loader = importlib.machinery.SourceFileLoader(\n            module_name, filepath\n        )\n        module_loader.exec_module(module)\n    else:\n        # Execute module code and store content to module\n        with open(filepath) as _stream:\n            # Execute content and store it to module object\n            six.exec_(_stream.read(), module.__dict__)\n\n    return module\n\n\ndef modules_from_path(folder_path):\n    \"\"\"Get python scripts as modules from a path.\n\n    Arguments:\n        path (str): Path to folder containing python scripts.\n\n    Returns:\n        tuple<list, list>: First list contains successfully imported modules\n            and second list contains tuples of path and exception.\n    \"\"\"\n    crashed = []\n    modules = []\n    output = (modules, crashed)\n    # Just skip and return empty list if path is not set\n    if not folder_path:\n        return output\n\n    # Do not allow relative imports\n    if folder_path.startswith(\".\"):\n        log.warning((\n            \"BUG: Relative paths are not allowed for security reasons. {}\"\n        ).format(folder_path))\n        return output\n\n    folder_path = os.path.normpath(folder_path)\n\n    if not os.path.isdir(folder_path):\n        log.warning(\"Not a directory path: {}\".format(folder_path))\n        return output\n\n    for filename in os.listdir(folder_path):\n        # Ignore files which start with underscore\n        if filename.startswith(\"_\"):\n            continue\n\n        mod_name, mod_ext = os.path.splitext(filename)\n        if not mod_ext == \".py\":\n            continue\n\n        full_path = os.path.join(folder_path, filename)\n        if not os.path.isfile(full_path):\n            continue\n\n        try:\n            module = import_filepath(full_path, mod_name)\n            modules.append((full_path, module))\n\n        except Exception:\n            crashed.append((full_path, sys.exc_info()))\n            log.warning(\n                \"Failed to load path: \\\"{0}\\\"\".format(full_path),\n                exc_info=True\n            )\n            continue\n\n    return output\n\n\ndef recursive_bases_from_class(klass):\n    \"\"\"Extract all bases from entered class.\"\"\"\n    result = []\n    bases = klass.__bases__\n    result.extend(bases)\n    for base in bases:\n        result.extend(recursive_bases_from_class(base))\n    return result\n\n\ndef classes_from_module(superclass, module):\n    \"\"\"Return plug-ins from module\n\n    Arguments:\n        superclass (superclass): Superclass of subclasses to look for\n        module (types.ModuleType): Imported module from which to\n            parse valid Avalon plug-ins.\n\n    Returns:\n        List of plug-ins, or empty list if none is found.\n\n    \"\"\"\n\n    classes = list()\n    for name in dir(module):\n        # It could be anything at this point\n        obj = getattr(module, name)\n        if not inspect.isclass(obj) or obj is superclass:\n            continue\n\n        if issubclass(obj, superclass):\n            classes.append(obj)\n\n    return classes\n\n\ndef _import_module_from_dirpath_py2(dirpath, module_name, dst_module_name):\n    \"\"\"Import passed dirpath as python module using `imp`.\"\"\"\n    if dst_module_name:\n        full_module_name = \"{}.{}\".format(dst_module_name, module_name)\n        dst_module = sys.modules[dst_module_name]\n    else:\n        full_module_name = module_name\n        dst_module = None\n\n    if full_module_name in sys.modules:\n        return sys.modules[full_module_name]\n\n    import imp\n\n    fp, pathname, description = imp.find_module(module_name, [dirpath])\n    module = imp.load_module(full_module_name, fp, pathname, description)\n    if dst_module is not None:\n        setattr(dst_module, module_name, module)\n\n    return module\n\n\ndef _import_module_from_dirpath_py3(dirpath, module_name, dst_module_name):\n    \"\"\"Import passed dirpath as python module using Python 3 modules.\"\"\"\n    if dst_module_name:\n        full_module_name = \"{}.{}\".format(dst_module_name, module_name)\n        dst_module = sys.modules[dst_module_name]\n    else:\n        full_module_name = module_name\n        dst_module = None\n\n    # Skip import if is already imported\n    if full_module_name in sys.modules:\n        return sys.modules[full_module_name]\n\n    import importlib.util\n    from importlib._bootstrap_external import PathFinder\n\n    # Find loader for passed path and name\n    loader = PathFinder.find_module(full_module_name, [dirpath])\n\n    # Load specs of module\n    spec = importlib.util.spec_from_loader(\n        full_module_name, loader, origin=dirpath\n    )\n\n    # Create module based on specs\n    module = importlib.util.module_from_spec(spec)\n\n    # Store module to destination module and `sys.modules`\n    # WARNING this mus be done before module execution\n    if dst_module is not None:\n        setattr(dst_module, module_name, module)\n\n    sys.modules[full_module_name] = module\n\n    # Execute module import\n    loader.exec_module(module)\n\n    return module\n\n\ndef import_module_from_dirpath(dirpath, folder_name, dst_module_name=None):\n    \"\"\"Import passed directory as a python module.\n\n    Python 2 and 3 compatible.\n\n    Imported module can be assigned as a child attribute of already loaded\n    module from `sys.modules` if has support of `setattr`. That is not default\n    behavior of python modules so parent module must be a custom module with\n    that ability.\n\n    It is not possible to reimport already cached module. If you need to\n    reimport module you have to remove it from caches manually.\n\n    Args:\n        dirpath(str): Parent directory path of loaded folder.\n        folder_name(str): Folder name which should be imported inside passed\n            directory.\n        dst_module_name(str): Parent module name under which can be loaded\n            module added.\n    \"\"\"\n    if six.PY3:\n        module = _import_module_from_dirpath_py3(\n            dirpath, folder_name, dst_module_name\n        )\n    else:\n        module = _import_module_from_dirpath_py2(\n            dirpath, folder_name, dst_module_name\n        )\n    return module\n\n\ndef is_func_signature_supported(func, *args, **kwargs):\n    \"\"\"Check if a function signature supports passed args and kwargs.\n\n    This check does not actually call the function, just look if function can\n    be called with the arguments.\n\n    Notes:\n        This does NOT check if the function would work with passed arguments\n            only if they can be passed in. If function have *args, **kwargs\n            in paramaters, this will always return 'True'.\n\n    Example:\n        >>> def my_function(my_number):\n        ...     return my_number + 1\n        ...\n        >>> is_func_signature_supported(my_function, 1)\n        True\n        >>> is_func_signature_supported(my_function, 1, 2)\n        False\n        >>> is_func_signature_supported(my_function, my_number=1)\n        True\n        >>> is_func_signature_supported(my_function, number=1)\n        False\n        >>> is_func_signature_supported(my_function, \"string\")\n        True\n        >>> def my_other_function(*args, **kwargs):\n        ...     my_function(*args, **kwargs)\n        ...\n        >>> is_func_signature_supported(\n        ...     my_other_function,\n        ...     \"string\",\n        ...     1,\n        ...     other=None\n        ... )\n        True\n\n    Args:\n        func (Callable): A function where the signature should be tested.\n        *args (Any): Positional arguments for function signature.\n        **kwargs (Any): Keyword arguments for function signature.\n\n    Returns:\n        bool: Function can pass in arguments.\n    \"\"\"\n\n    if hasattr(inspect, \"signature\"):\n        # Python 3 using 'Signature' object where we try to bind arg\n        #   or kwarg. Using signature is recommended approach based on\n        #   documentation.\n        sig = inspect.signature(func)\n        try:\n            sig.bind(*args, **kwargs)\n            return True\n        except TypeError:\n            pass\n\n    else:\n        # In Python 2 'signature' is not available so 'getcallargs' is used\n        # - 'getcallargs' is marked as deprecated since Python 3.0\n        try:\n            inspect.getcallargs(func, *args, **kwargs)\n            return True\n        except TypeError:\n            pass\n    return False\n"
  },
  {
    "path": "openpype/lib/terminal.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Package helping with colorizing and formatting terminal output.\"\"\"\n# ::\n#   //.  ...   ..      ///.     //.\n#  ///\\\\\\ \\\\\\   \\\\    ///\\\\\\   ///\n# ///  \\\\  \\\\\\   \\\\  ///  \\\\  /// //\n# \\\\\\  //   \\\\\\  //  \\\\\\  //  \\\\\\//  ./\n#  \\\\\\//     \\\\\\//    \\\\\\//    \\\\\\' //\n#   \\\\\\         \\\\\\    \\\\\\      \\\\\\//\n#    '''         '''    '''      '''\n#   ..---===[[ PyP3 Setup ]]===---...\n#\nimport re\nimport time\nimport threading\n\n\nclass Terminal:\n    \"\"\"Class formatting messages using colorama to specific visual tokens.\n\n    If :mod:`Colorama` is not found, it will still work, but without colors.\n\n    Depends on :mod:`Colorama`\n    Using **OPENPYPE_LOG_NO_COLORS** environment variable.\n    \"\"\"\n\n    # Is Terminal initialized\n    _initialized = False\n    # Thread lock for initialization to avoid race conditions\n    _init_lock = threading.Lock()\n    # Use colorized output\n    use_colors = True\n    # Output message replacements mapping - set on initialization\n    _sdict = {}\n\n    @staticmethod\n    def _initialize():\n        \"\"\"Initialize Terminal class as object.\n\n        First check if colorized output is disabled by environment variable\n        `OPENPYPE_LOG_NO_COLORS` value. By default is colorized output turned\n        on.\n\n        Then tries to import python module that do the colors magic and create\n        it's terminal object. Colorized output is not used if import of python\n        module or terminal object creation fails.\n\n        Set `_initialized` attribute to `True` when is done.\n        \"\"\"\n\n        from openpype.lib import env_value_to_bool\n        log_no_colors = env_value_to_bool(\n            \"OPENPYPE_LOG_NO_COLORS\", default=None\n        )\n        if log_no_colors is not None:\n            Terminal.use_colors = not log_no_colors\n\n        if not Terminal.use_colors:\n            Terminal._initialized = True\n            return\n\n        try:\n            # Try to import `blessed` module and create `Terminal` object\n            import blessed\n            term = blessed.Terminal()\n\n        except Exception:\n            # Do not use colors if crashed\n            Terminal.use_colors = False\n            print(\n                \"Module `blessed` failed on import or terminal creation.\"\n                \" Pype terminal won't use colors.\"\n            )\n            Terminal._initialized = True\n            return\n\n        # shortcuts for blessed codes\n        _SB = term.bold\n        _RST = \"\"\n        _LR = term.tomato2\n        _LG = term.aquamarine3\n        _LB = term.turquoise2\n        _LM = term.slateblue2\n        _LY = term.gold\n        _R = term.red\n        _G = term.green\n        _B = term.blue\n        _C = term.cyan\n        _Y = term.yellow\n        _W = term.white\n\n        # dictionary replacing string sequences with colorized one\n        Terminal._sdict = {\n            r\">>> \": _SB + _LG + r\">>> \" + _RST,\n            r\"!!!(?!\\sCRI|\\sERR)\": _SB + _R + r\"!!! \" + _RST,\n            r\"\\-\\-\\- \": _SB + _C + r\"--- \" + _RST,\n            r\"\\*\\*\\*(?!\\sWRN)\": _SB + _LY + r\"***\" + _RST,\n            r\"\\*\\*\\* WRN\": _SB + _LY + r\"*** WRN\" + _RST,\n            r\"  \\- \": _SB + _LY + r\"  - \" + _RST,\n            r\"\\[ \": _SB + _LG + r\"[ \" + _RST,\n            r\" \\]\": _SB + _LG + r\" ]\" + _RST,\n            r\"{\": _LG + r\"{\",\n            r\"}\": r\"}\" + _RST,\n            r\"\\(\": _LY + r\"(\",\n            r\"\\)\": r\")\" + _RST,\n            r\"^\\.\\.\\. \": _SB + _LR + r\"... \" + _RST,\n            r\"!!! ERR: \":\n                _SB + _LR + r\"!!! ERR: \" + _RST,\n            r\"!!! CRI: \":\n                _SB + _R + r\"!!! CRI: \" + _RST,\n            r\"(?i)failed\": _SB + _LR + \"FAILED\" + _RST,\n            r\"(?i)error\": _SB + _LR + \"ERROR\" + _RST\n        }\n\n        Terminal._SB = _SB\n        Terminal._RST = _RST\n        Terminal._LR = _LR\n        Terminal._LG = _LG\n        Terminal._LB = _LB\n        Terminal._LM = _LM\n        Terminal._LY = _LY\n        Terminal._R = _R\n        Terminal._G = _G\n        Terminal._B = _B\n        Terminal._C = _C\n        Terminal._Y = _Y\n        Terminal._W = _W\n\n        Terminal._initialized = True\n\n    @staticmethod\n    def _multiple_replace(text, adict):\n        \"\"\"Replace multiple tokens defined in dict.\n\n        Find and replace all occurrences of strings defined in dict is\n        supplied string.\n\n        Args:\n            text (str): string to be searched\n            adict (dict): dictionary with `{'search': 'replace'}`\n\n        Returns:\n            str: string with replaced tokens\n\n        \"\"\"\n        for r, v in adict.items():\n            text = re.sub(r, v, text)\n\n        return text\n\n    @staticmethod\n    def echo(message):\n        \"\"\"Print colorized message to stdout.\n\n        Args:\n            message (str): Message to be colorized.\n            debug (bool):\n\n        Returns:\n            str: Colorized message.\n\n        \"\"\"\n        colorized = Terminal.log(message)\n        print(colorized)\n\n        return colorized\n\n    @staticmethod\n    def log(message):\n        \"\"\"Return color formatted message.\n\n        If environment variable `OPENPYPE_LOG_NO_COLORS` is set to\n        whatever value, message will be formatted but not colorized.\n\n        Args:\n            message (str): Message to be colorized.\n\n        Returns:\n            str: Colorized message.\n\n        \"\"\"\n        T = Terminal\n        # Initialize if not yet initialized and use thread lock to avoid race\n        # condition issues\n        if not T._initialized:\n            # Check if lock is already locked to be sure `_initialize` is not\n            # executed multiple times\n            if not T._init_lock.locked():\n                with T._init_lock:\n                    T._initialize()\n            else:\n                # If lock is locked wait until is finished\n                while T._init_lock.locked():\n                    time.sleep(0.1)\n\n        # if we dont want colors, just print raw message\n        if not T.use_colors:\n            return message\n\n        message = re.sub(r'\\[(.*)\\]', '[ ' + T._SB + T._W +\n                         r'\\1' + T._RST + ' ]', message)\n        message = T._multiple_replace(message + T._RST, T._sdict)\n\n        return message\n"
  },
  {
    "path": "openpype/lib/transcoding.py",
    "content": "import os\nimport re\nimport logging\nimport json\nimport collections\nimport tempfile\nimport subprocess\nimport platform\n\nimport xml.etree.ElementTree\n\nfrom .execute import run_subprocess\nfrom .vendor_bin_utils import (\n    get_ffmpeg_tool_args,\n    get_oiio_tool_args,\n    is_oiio_supported,\n)\n\n# Max length of string that is supported by ffmpeg\nMAX_FFMPEG_STRING_LEN = 8196\n# Not allowed symbols in attributes for ffmpeg\nNOT_ALLOWED_FFMPEG_CHARS = (\"\\\"\", )\n\n# OIIO known xml tags\nSTRING_TAGS = {\n    \"format\"\n}\nINT_TAGS = {\n    \"x\", \"y\", \"z\",\n    \"width\", \"height\", \"depth\",\n    \"full_x\", \"full_y\", \"full_z\",\n    \"full_width\", \"full_height\", \"full_depth\",\n    \"tile_width\", \"tile_height\", \"tile_depth\",\n    \"nchannels\",\n    \"alpha_channel\",\n    \"z_channel\",\n    \"deep\",\n    \"subimages\",\n}\n\nXML_CHAR_REF_REGEX_HEX = re.compile(r\"&#x?[0-9a-fA-F]+;\")\n\n# Regex to parse array attributes\nARRAY_TYPE_REGEX = re.compile(r\"^(int|float|string)\\[\\d+\\]$\")\n\nIMAGE_EXTENSIONS = {\n    \".ani\", \".anim\", \".apng\", \".art\", \".bmp\", \".bpg\", \".bsave\",\n    \".cal\", \".cin\", \".cpc\", \".cpt\", \".dds\", \".dpx\", \".ecw\", \".exr\",\n    \".fits\", \".flic\", \".flif\", \".fpx\", \".gif\", \".hdri\", \".hevc\",\n    \".icer\", \".icns\", \".ico\", \".cur\", \".ics\", \".ilbm\", \".jbig\", \".jbig2\",\n    \".jng\", \".jpeg\", \".jpeg-ls\", \".jpeg-hdr\", \".2000\", \".jpg\",\n    \".kra\", \".logluv\", \".mng\", \".miff\", \".nrrd\", \".ora\",\n    \".pam\", \".pbm\", \".pgm\", \".ppm\", \".pnm\", \".pcx\", \".pgf\",\n    \".pictor\", \".png\", \".psd\", \".psb\", \".psp\", \".qtvr\",\n    \".ras\", \".rgbe\", \".sgi\", \".tga\",\n    \".tif\", \".tiff\", \".tiff/ep\", \".tiff/it\", \".ufo\", \".ufp\",\n    \".wbmp\", \".webp\", \".xr\", \".xt\", \".xbm\", \".xcf\", \".xpm\", \".xwd\"\n}\n\nVIDEO_EXTENSIONS = {\n    \".3g2\", \".3gp\", \".amv\", \".asf\", \".avi\", \".drc\", \".f4a\", \".f4b\",\n    \".f4p\", \".f4v\", \".flv\", \".gif\", \".gifv\", \".m2v\", \".m4p\", \".m4v\",\n    \".mkv\", \".mng\", \".mov\", \".mp2\", \".mp4\", \".mpe\", \".mpeg\", \".mpg\",\n    \".mpv\", \".mxf\", \".nsv\", \".ogg\", \".ogv\", \".qt\", \".rm\", \".rmvb\",\n    \".roq\", \".svi\", \".vob\", \".webm\", \".wmv\", \".yuv\"\n}\n\n\ndef get_transcode_temp_directory():\n    \"\"\"Creates temporary folder for transcoding.\n\n    Its local, in case of farm it is 'local' to the farm machine.\n\n    Should be much faster, needs to be cleaned up later.\n    \"\"\"\n    return os.path.normpath(\n        tempfile.mkdtemp(prefix=\"op_transcoding_\")\n    )\n\n\ndef get_oiio_info_for_input(filepath, logger=None, subimages=False):\n    \"\"\"Call oiiotool to get information about input and return stdout.\n\n    Stdout should contain xml format string.\n    \"\"\"\n    args = get_oiio_tool_args(\n        \"oiiotool\",\n        \"--info\",\n        \"-v\"\n    )\n    if subimages:\n        args.append(\"-a\")\n\n    args.extend([\"-i:infoformat=xml\", filepath])\n\n    output = run_subprocess(args, logger=logger)\n    output = output.replace(\"\\r\\n\", \"\\n\")\n\n    xml_started = False\n    subimages_lines = []\n    lines = []\n    for line in output.split(\"\\n\"):\n        if not xml_started:\n            if not line.startswith(\"<\"):\n                continue\n            xml_started = True\n\n        if xml_started:\n            lines.append(line)\n            if line == \"</ImageSpec>\":\n                subimages_lines.append(lines)\n                lines = []\n                xml_started = False\n\n    if not subimages_lines:\n        raise ValueError(\n            \"Failed to read input file \\\"{}\\\".\\nOutput:\\n{}\".format(\n                filepath, output\n            )\n        )\n\n    output = []\n    for subimage_lines in subimages_lines:\n        xml_text = \"\\n\".join(subimage_lines)\n        output.append(parse_oiio_xml_output(xml_text, logger=logger))\n\n    if subimages:\n        return output\n    return output[0]\n\n\nclass RationalToInt:\n    \"\"\"Rational value stored as division of 2 integers using string.\"\"\"\n\n    def __init__(self, string_value):\n        parts = string_value.split(\"/\")\n        top = float(parts[0])\n        bottom = 1.0\n        if len(parts) != 1:\n            bottom = float(parts[1])\n\n        self._value = float(top) / float(bottom)\n        self._string_value = string_value\n\n    @property\n    def value(self):\n        return self._value\n\n    @property\n    def string_value(self):\n        return self._string_value\n\n    def __format__(self, *args, **kwargs):\n        return self._string_value.__format__(*args, **kwargs)\n\n    def __float__(self):\n        return self._value\n\n    def __str__(self):\n        return self._string_value\n\n    def __repr__(self):\n        return \"<{}> {}\".format(self.__class__.__name__, self._string_value)\n\n\ndef convert_value_by_type_name(value_type, value, logger=None):\n    \"\"\"Convert value to proper type based on type name.\n\n    In some cases value types have custom python class.\n    \"\"\"\n    if logger is None:\n        logger = logging.getLogger(__name__)\n\n    # Simple types\n    if value_type == \"string\":\n        return value\n\n    if value_type == \"int\":\n        return int(value)\n\n    if value_type in (\"float\", \"double\"):\n        return float(value)\n\n    # Vectors will probably have more types\n    if value_type in (\"vec2f\", \"float2\", \"float2d\"):\n        return [float(item) for item in value.split(\",\")]\n\n    # Matrix should be always have square size of element 3x3, 4x4\n    # - are returned as list of lists\n    if value_type in (\"matrix\", \"matrixd\"):\n        output = []\n        current_index = -1\n        parts = value.split(\",\")\n        parts_len = len(parts)\n        if parts_len == 1:\n            divisor = 1\n        elif parts_len == 4:\n            divisor = 2\n        elif parts_len == 9:\n            divisor = 3\n        elif parts_len == 16:\n            divisor = 4\n        else:\n            logger.info(\"Unknown matrix resolution {}. Value: \\\"{}\\\"\".format(\n                parts_len, value\n            ))\n            for part in parts:\n                output.append(float(part))\n            return output\n\n        for idx, item in enumerate(parts):\n            list_index = idx % divisor\n            if list_index > current_index:\n                current_index = list_index\n                output.append([])\n            output[list_index].append(float(item))\n        return output\n\n    if value_type == \"rational2i\":\n        return RationalToInt(value)\n\n    if value_type in (\"vector\", \"vectord\"):\n        parts = [part.strip() for part in value.split(\",\")]\n        output = []\n        for part in parts:\n            if part == \"-nan\":\n                output.append(None)\n                continue\n            try:\n                part = float(part)\n            except ValueError:\n                pass\n            output.append(part)\n        return output\n\n    if value_type == \"timecode\":\n        return value\n\n    # Array of other types is converted to list\n    re_result = ARRAY_TYPE_REGEX.findall(value_type)\n    if re_result:\n        array_type = re_result[0]\n        output = []\n        for item in value.split(\",\"):\n            output.append(\n                convert_value_by_type_name(array_type, item, logger=logger)\n            )\n        return output\n\n    logger.debug((\n        \"Dev note (missing implementation):\"\n        \" Unknown attrib type \\\"{}\\\". Value: {}\"\n    ).format(value_type, value))\n    return value\n\n\ndef parse_oiio_xml_output(xml_string, logger=None):\n    \"\"\"Parse xml output from OIIO info command.\"\"\"\n    output = {}\n    if not xml_string:\n        return output\n\n    # Fix values with ampresand (lazy fix)\n    # - oiiotool exports invalid xml which ElementTree can't handle\n    #   e.g. \"&#01;\"\n    # WARNING: this will affect even valid character entities. If you need\n    #   those values correctly, this must take care of valid character ranges.\n    #   See https://github.com/pypeclub/OpenPype/pull/2729\n    matches = XML_CHAR_REF_REGEX_HEX.findall(xml_string)\n    for match in matches:\n        new_value = match.replace(\"&\", \"&amp;\")\n        xml_string = xml_string.replace(match, new_value)\n\n    if logger is None:\n        logger = logging.getLogger(\"OIIO-xml-parse\")\n\n    tree = xml.etree.ElementTree.fromstring(xml_string)\n    attribs = {}\n    output[\"attribs\"] = attribs\n    for child in tree:\n        tag_name = child.tag\n        if tag_name == \"attrib\":\n            attrib_def = child.attrib\n            value = convert_value_by_type_name(\n                attrib_def[\"type\"], child.text, logger=logger\n            )\n\n            attribs[attrib_def[\"name\"]] = value\n            continue\n\n        # Channels are stored as tex on each child\n        if tag_name == \"channelnames\":\n            value = []\n            for channel in child:\n                value.append(channel.text)\n\n        # Convert known integer type tags to int\n        elif tag_name in INT_TAGS:\n            value = int(child.text)\n\n        # Keep value of known string tags\n        elif tag_name in STRING_TAGS:\n            value = child.text\n\n        # Keep value as text for unknown tags\n        # - feel free to add more tags\n        else:\n            value = child.text\n            logger.debug((\n                \"Dev note (missing implementation):\"\n                \" Unknown tag \\\"{}\\\". Value \\\"{}\\\"\"\n            ).format(tag_name, value))\n\n        output[child.tag] = value\n\n    return output\n\n\ndef get_review_info_by_layer_name(channel_names):\n    \"\"\"Get channels info grouped by layer name.\n\n    Finds all layers in channel names and returns list of dictionaries with\n    information about channels in layer.\n    Example output (not real world example):\n        [\n            {\n                \"name\": \"Main\",\n                \"review_channels\": {\n                    \"R\": \"Main.red\",\n                    \"G\": \"Main.green\",\n                    \"B\": \"Main.blue\",\n                    \"A\": None,\n                }\n            },\n            {\n                \"name\": \"Composed\",\n                \"review_channels\": {\n                    \"R\": \"Composed.R\",\n                    \"G\": \"Composed.G\",\n                    \"B\": \"Composed.B\",\n                    \"A\": \"Composed.A\",\n                }\n            },\n            ...\n        ]\n\n    Args:\n        channel_names (list[str]): List of channel names.\n\n    Returns:\n        list[dict]: List of channels information.\n    \"\"\"\n\n    layer_names_order = []\n    rgba_by_layer_name = collections.defaultdict(dict)\n    channels_by_layer_name = collections.defaultdict(dict)\n\n    for channel_name in channel_names:\n        layer_name = \"\"\n        last_part = channel_name\n        if \".\" in channel_name:\n            layer_name, last_part = channel_name.rsplit(\".\", 1)\n\n        channels_by_layer_name[layer_name][channel_name] = last_part\n        if last_part.lower() not in {\n            \"r\", \"red\",\n            \"g\", \"green\",\n            \"b\", \"blue\",\n            \"a\", \"alpha\"\n        }:\n            continue\n\n        if layer_name not in layer_names_order:\n            layer_names_order.append(layer_name)\n        # R, G, B or A\n        channel = last_part[0].upper()\n        rgba_by_layer_name[layer_name][channel] = channel_name\n\n    # Put empty layer to the beginning of the list\n    # - if input has R, G, B, A channels they should be used for review\n    if \"\" in layer_names_order:\n        layer_names_order.remove(\"\")\n        layer_names_order.insert(0, \"\")\n\n    output = []\n    for layer_name in layer_names_order:\n        rgba_layer_info = rgba_by_layer_name[layer_name]\n        red = rgba_layer_info.get(\"R\")\n        green = rgba_layer_info.get(\"G\")\n        blue = rgba_layer_info.get(\"B\")\n        if not red or not green or not blue:\n            continue\n        output.append({\n            \"name\": layer_name,\n            \"review_channels\": {\n                \"R\": red,\n                \"G\": green,\n                \"B\": blue,\n                \"A\": rgba_layer_info.get(\"A\"),\n            }\n        })\n    return output\n\n\ndef get_convert_rgb_channels(channel_names):\n    \"\"\"Get first available RGB(A) group from channels info.\n\n    ## Examples\n    ```\n    # Ideal situation\n    channels_info: [\n        \"R\", \"G\", \"B\", \"A\"\n    ]\n    ```\n    Result will be `(\"R\", \"G\", \"B\", \"A\")`\n\n    ```\n    # Not ideal situation\n    channels_info: [\n        \"beauty.red\",\n        \"beauty.green\",\n        \"beauty.blue\",\n        \"depth.Z\"\n    ]\n    ```\n    Result will be `(\"beauty.red\", \"beauty.green\", \"beauty.blue\", None)`\n\n    Args:\n        channel_names (list[str]): List of channel names.\n\n    Returns:\n        Union[NoneType, tuple[str, str, str, Union[str, None]]]: Tuple of\n            4 channel names defying channel names for R, G, B, A or None\n            if there is not any layer with RGB combination.\n    \"\"\"\n\n    channels_info = get_review_info_by_layer_name(channel_names)\n    for item in channels_info:\n        review_channels = item[\"review_channels\"]\n        return (\n            review_channels[\"R\"],\n            review_channels[\"G\"],\n            review_channels[\"B\"],\n            review_channels[\"A\"]\n        )\n    return None\n\n\ndef get_review_layer_name(src_filepath):\n    \"\"\"Find layer name that could be used for review.\n\n    Args:\n        src_filepath (str): Path to input file.\n\n    Returns:\n        Union[str, None]: Layer name of None.\n    \"\"\"\n\n    ext = os.path.splitext(src_filepath)[-1].lower()\n    if ext != \".exr\":\n        return None\n\n    # Load info about file from oiio tool\n    input_info = get_oiio_info_for_input(src_filepath)\n    if not input_info:\n        return None\n\n    channel_names = input_info[\"channelnames\"]\n    channels_info = get_review_info_by_layer_name(channel_names)\n    for item in channels_info:\n        # Layer name can be '', when review channels are 'R', 'G', 'B'\n        #   without layer\n        return item[\"name\"] or None\n    return None\n\n\ndef should_convert_for_ffmpeg(src_filepath):\n    \"\"\"Find out if input should be converted for ffmpeg.\n\n    Currently cares only about exr inputs and is based on OpenImageIO.\n\n    Returns:\n        bool/NoneType: True if should be converted, False if should not and\n            None if can't determine.\n    \"\"\"\n    # Care only about exr at this moment\n    ext = os.path.splitext(src_filepath)[-1].lower()\n    if ext != \".exr\":\n        return False\n\n    # Can't determine if should convert or not without oiio_tool\n    if not is_oiio_supported():\n        return None\n\n    # Load info about file from oiio tool\n    input_info = get_oiio_info_for_input(src_filepath)\n    if not input_info:\n        return None\n\n    subimages = input_info.get(\"subimages\")\n    if subimages is not None and subimages > 1:\n        return True\n\n    # Check compression\n    compression = input_info[\"attribs\"].get(\"compression\")\n    if compression in (\"dwaa\", \"dwab\"):\n        return True\n\n    # Check channels\n    channel_names = input_info[\"channelnames\"]\n    review_channels = get_convert_rgb_channels(channel_names)\n    if review_channels is None:\n        return None\n\n    for attr_value in input_info[\"attribs\"].values():\n        if not isinstance(attr_value, str):\n            continue\n\n        if len(attr_value) > MAX_FFMPEG_STRING_LEN:\n            return True\n\n        for char in NOT_ALLOWED_FFMPEG_CHARS:\n            if char in attr_value:\n                return True\n    return False\n\n\n# Deprecated since 2022 4 20\n# - Reason - Doesn't convert sequences right way: Can't handle gaps, reuse\n#       first frame for all frames and changes filenames when input\n#       is sequence.\n# - use 'convert_input_paths_for_ffmpeg' instead\ndef convert_for_ffmpeg(\n    first_input_path,\n    output_dir,\n    input_frame_start=None,\n    input_frame_end=None,\n    logger=None\n):\n    \"\"\"Convert source file to format supported in ffmpeg.\n\n    Currently can convert only exrs.\n\n    Args:\n        first_input_path (str): Path to first file of a sequence or a single\n            file path for non-sequential input.\n        output_dir (str): Path to directory where output will be rendered.\n            Must not be same as input's directory.\n        input_frame_start (int): Frame start of input.\n        input_frame_end (int): Frame end of input.\n        logger (logging.Logger): Logger used for logging.\n\n    Raises:\n        ValueError: If input filepath has extension not supported by function.\n            Currently is supported only \".exr\" extension.\n    \"\"\"\n    if logger is None:\n        logger = logging.getLogger(__name__)\n\n    logger.warning((\n        \"DEPRECATED: 'openpype.lib.transcoding.convert_for_ffmpeg' is\"\n        \" deprecated function of conversion for FFMpeg. Please replace usage\"\n        \" with 'openpype.lib.transcoding.convert_input_paths_for_ffmpeg'\"\n    ))\n\n    ext = os.path.splitext(first_input_path)[1].lower()\n    if ext != \".exr\":\n        raise ValueError((\n            \"Function 'convert_for_ffmpeg' currently support only\"\n            \" \\\".exr\\\" extension. Got \\\"{}\\\".\"\n        ).format(ext))\n\n    is_sequence = False\n    if input_frame_start is not None and input_frame_end is not None:\n        is_sequence = int(input_frame_end) != int(input_frame_start)\n\n    input_info = get_oiio_info_for_input(first_input_path, logger=logger)\n\n    # Change compression only if source compression is \"dwaa\" or \"dwab\"\n    #   - they're not supported in ffmpeg\n    compression = input_info[\"attribs\"].get(\"compression\")\n    if compression in (\"dwaa\", \"dwab\"):\n        compression = \"none\"\n\n    # Prepare subprocess arguments\n    oiio_cmd = get_oiio_tool_args(\n        \"oiiotool\",\n        # Don't add any additional attributes\n        \"--nosoftwareattrib\",\n    )\n    # Add input compression if available\n    if compression:\n        oiio_cmd.extend([\"--compression\", compression])\n\n    # Collect channels to export\n    input_arg, channels_arg = get_oiio_input_and_channel_args(input_info)\n\n    oiio_cmd.extend([\n        input_arg, first_input_path,\n        # Tell oiiotool which channels should be put to top stack (and output)\n        \"--ch\", channels_arg,\n        # Use first subimage\n        \"--subimage\", \"0\"\n    ])\n\n    # Add frame definitions to arguments\n    if is_sequence:\n        oiio_cmd.extend([\n            \"--frames\", \"{}-{}\".format(input_frame_start, input_frame_end)\n        ])\n\n    for attr_name, attr_value in input_info[\"attribs\"].items():\n        if not isinstance(attr_value, str):\n            continue\n\n        # Remove attributes that have string value longer than allowed length\n        #   for ffmpeg or when contain prohibited symbols\n        erase_reason = \"Missing reason\"\n        erase_attribute = False\n        if len(attr_value) > MAX_FFMPEG_STRING_LEN:\n            erase_reason = \"has too long value ({} chars).\".format(\n                len(attr_value)\n            )\n            erase_attribute = True\n\n        if not erase_attribute:\n            for char in NOT_ALLOWED_FFMPEG_CHARS:\n                if char in attr_value:\n                    erase_attribute = True\n                    erase_reason = (\n                        \"contains unsupported character \\\"{}\\\".\"\n                    ).format(char)\n                    break\n\n        if erase_attribute:\n            # Set attribute to empty string\n            logger.info((\n                \"Removed attribute \\\"{}\\\" from metadata because {}.\"\n            ).format(attr_name, erase_reason))\n            oiio_cmd.extend([\"--eraseattrib\", attr_name])\n\n    # Add last argument - path to output\n    if is_sequence:\n        ext = os.path.splitext(first_input_path)[1]\n        base_filename = \"tmp.%{:0>2}d{}\".format(\n            len(str(input_frame_end)), ext\n        )\n    else:\n        base_filename = os.path.basename(first_input_path)\n    output_path = os.path.join(output_dir, base_filename)\n    oiio_cmd.extend([\n        \"-o\", output_path\n    ])\n\n    logger.debug(\"Conversion command: {}\".format(\" \".join(oiio_cmd)))\n    run_subprocess(oiio_cmd, logger=logger)\n\n\ndef convert_input_paths_for_ffmpeg(\n    input_paths,\n    output_dir,\n    logger=None\n):\n    \"\"\"Convert source file to format supported in ffmpeg.\n\n    Currently can convert only exrs. The input filepaths should be files\n    with same type. Information about input is loaded only from first found\n    file.\n\n    Filenames of input files are kept so make sure that output directory\n    is not the same directory as input files have.\n    - This way it can handle gaps and can keep input filenames without handling\n        frame template\n\n    Args:\n        input_paths (str): Paths that should be converted. It is expected that\n            contains single file or image sequence of same type.\n        output_dir (str): Path to directory where output will be rendered.\n            Must not be same as input's directory.\n        logger (logging.Logger): Logger used for logging.\n\n    Raises:\n        ValueError: If input filepath has extension not supported by function.\n            Currently is supported only \".exr\" extension.\n    \"\"\"\n    if logger is None:\n        logger = logging.getLogger(__name__)\n\n    first_input_path = input_paths[0]\n    ext = os.path.splitext(first_input_path)[1].lower()\n\n    if ext != \".exr\":\n        raise ValueError((\n            \"Function 'convert_for_ffmpeg' currently support only\"\n            \" \\\".exr\\\" extension. Got \\\"{}\\\".\"\n        ).format(ext))\n\n    input_info = get_oiio_info_for_input(first_input_path, logger=logger)\n\n    # Change compression only if source compression is \"dwaa\" or \"dwab\"\n    #   - they're not supported in ffmpeg\n    compression = input_info[\"attribs\"].get(\"compression\")\n    if compression in (\"dwaa\", \"dwab\"):\n        compression = \"none\"\n\n    # Collect channels to export\n    input_arg, channels_arg = get_oiio_input_and_channel_args(input_info)\n\n    for input_path in input_paths:\n        # Prepare subprocess arguments\n        oiio_cmd = get_oiio_tool_args(\n            \"oiiotool\",\n            # Don't add any additional attributes\n            \"--nosoftwareattrib\",\n        )\n        # Add input compression if available\n        if compression:\n            oiio_cmd.extend([\"--compression\", compression])\n\n        oiio_cmd.extend([\n            input_arg, input_path,\n            # Tell oiiotool which channels should be put to top stack\n            #   (and output)\n            \"--ch\", channels_arg,\n            # Use first subimage\n            \"--subimage\", \"0\"\n        ])\n\n        for attr_name, attr_value in input_info[\"attribs\"].items():\n            if not isinstance(attr_value, str):\n                continue\n\n            # Remove attributes that have string value longer than allowed\n            #   length for ffmpeg or when containing prohibited symbols\n            erase_reason = \"Missing reason\"\n            erase_attribute = False\n            if len(attr_value) > MAX_FFMPEG_STRING_LEN:\n                erase_reason = \"has too long value ({} chars).\".format(\n                    len(attr_value)\n                )\n                erase_attribute = True\n\n            if not erase_attribute:\n                for char in NOT_ALLOWED_FFMPEG_CHARS:\n                    if char in attr_value:\n                        erase_attribute = True\n                        erase_reason = (\n                            \"contains unsupported character \\\"{}\\\".\"\n                        ).format(char)\n                        break\n\n            if erase_attribute:\n                # Set attribute to empty string\n                logger.info((\n                    \"Removed attribute \\\"{}\\\" from metadata because {}.\"\n                ).format(attr_name, erase_reason))\n                oiio_cmd.extend([\"--eraseattrib\", attr_name])\n\n        # Add last argument - path to output\n        base_filename = os.path.basename(input_path)\n        output_path = os.path.join(output_dir, base_filename)\n        oiio_cmd.extend([\n            \"-o\", output_path\n        ])\n\n        logger.debug(\"Conversion command: {}\".format(\" \".join(oiio_cmd)))\n        run_subprocess(oiio_cmd, logger=logger)\n\n\n# FFMPEG functions\ndef get_ffprobe_data(path_to_file, logger=None):\n    \"\"\"Load data about entered filepath via ffprobe.\n\n    Args:\n        path_to_file (str): absolute path\n        logger (logging.Logger): injected logger, if empty new is created\n    \"\"\"\n    if not logger:\n        logger = logging.getLogger(__name__)\n    logger.debug(\n        \"Getting information about input \\\"{}\\\".\".format(path_to_file)\n    )\n    ffprobe_args = get_ffmpeg_tool_args(\"ffprobe\")\n    args = ffprobe_args + [\n        \"-hide_banner\",\n        \"-loglevel\", \"fatal\",\n        \"-show_error\",\n        \"-show_format\",\n        \"-show_streams\",\n        \"-show_programs\",\n        \"-show_chapters\",\n        \"-show_private_data\",\n        \"-print_format\", \"json\",\n        path_to_file\n    ]\n\n    logger.debug(\"FFprobe command: {}\".format(\n        subprocess.list2cmdline(args)\n    ))\n    kwargs = {\n        \"stdout\": subprocess.PIPE,\n        \"stderr\": subprocess.PIPE,\n    }\n    if platform.system().lower() == \"windows\":\n        kwargs[\"creationflags\"] = (\n            subprocess.CREATE_NEW_PROCESS_GROUP\n            | getattr(subprocess, \"DETACHED_PROCESS\", 0)\n            | getattr(subprocess, \"CREATE_NO_WINDOW\", 0)\n        )\n\n    popen = subprocess.Popen(args, **kwargs)\n\n    popen_stdout, popen_stderr = popen.communicate()\n    if popen_stdout:\n        logger.debug(\"FFprobe stdout:\\n{}\".format(\n            popen_stdout.decode(\"utf-8\")\n        ))\n\n    if popen_stderr:\n        logger.warning(\"FFprobe stderr:\\n{}\".format(\n            popen_stderr.decode(\"utf-8\")\n        ))\n\n    return json.loads(popen_stdout)\n\n\ndef get_ffprobe_streams(path_to_file, logger=None):\n    \"\"\"Load streams from entered filepath via ffprobe.\n\n    Args:\n        path_to_file (str): absolute path\n        logger (logging.Logger): injected logger, if empty new is created\n    \"\"\"\n    return get_ffprobe_data(path_to_file, logger)[\"streams\"]\n\n\ndef get_ffmpeg_format_args(ffprobe_data, source_ffmpeg_cmd=None):\n    \"\"\"Copy format from input metadata for output.\n\n    Args:\n        ffprobe_data(dict): Data received from ffprobe.\n        source_ffmpeg_cmd(str): Command that created input if available.\n    \"\"\"\n    input_format = ffprobe_data.get(\"format\") or {}\n    if input_format.get(\"format_name\") == \"mxf\":\n        return _ffmpeg_mxf_format_args(ffprobe_data, source_ffmpeg_cmd)\n    return []\n\n\ndef _ffmpeg_mxf_format_args(ffprobe_data, source_ffmpeg_cmd):\n    input_format = ffprobe_data[\"format\"]\n    format_tags = input_format.get(\"tags\") or {}\n    operational_pattern_ul = format_tags.get(\"operational_pattern_ul\") or \"\"\n    output = []\n    if operational_pattern_ul == \"060e2b34.04010102.0d010201.10030000\":\n        output.extend([\"-f\", \"mxf_opatom\"])\n    return output\n\n\ndef get_ffmpeg_codec_args(ffprobe_data, source_ffmpeg_cmd=None, logger=None):\n    \"\"\"Copy codec from input metadata for output.\n\n    Args:\n        ffprobe_data(dict): Data received from ffprobe.\n        source_ffmpeg_cmd(str): Command that created input if available.\n    \"\"\"\n    if logger is None:\n        logger = logging.getLogger(__name__)\n\n    video_stream = None\n    no_audio_stream = None\n    for stream in ffprobe_data[\"streams\"]:\n        codec_type = stream[\"codec_type\"]\n        if codec_type == \"video\":\n            video_stream = stream\n            break\n        elif no_audio_stream is None and codec_type != \"audio\":\n            no_audio_stream = stream\n\n    if video_stream is None:\n        if no_audio_stream is None:\n            logger.warning(\n                \"Couldn't find stream that is not an audio file.\"\n            )\n            return []\n        logger.info(\n            \"Didn't find video stream. Using first non audio stream.\"\n        )\n        video_stream = no_audio_stream\n\n    codec_name = video_stream.get(\"codec_name\")\n    # Codec \"prores\"\n    if codec_name == \"prores\":\n        return _ffmpeg_prores_codec_args(video_stream, source_ffmpeg_cmd)\n\n    # Codec \"h264\"\n    if codec_name == \"h264\":\n        return _ffmpeg_h264_codec_args(video_stream, source_ffmpeg_cmd)\n\n    # Coded DNxHD\n    if codec_name == \"dnxhd\":\n        return _ffmpeg_dnxhd_codec_args(video_stream, source_ffmpeg_cmd)\n\n    output = []\n    if codec_name:\n        output.extend([\"-codec:v\", codec_name])\n\n    bit_rate = video_stream.get(\"bit_rate\")\n    if bit_rate:\n        output.extend([\"-b:v\", bit_rate])\n\n    pix_fmt = video_stream.get(\"pix_fmt\")\n    if pix_fmt:\n        output.extend([\"-pix_fmt\", pix_fmt])\n\n    output.extend([\"-g\", \"1\"])\n\n    return output\n\n\ndef _ffmpeg_prores_codec_args(stream_data, source_ffmpeg_cmd):\n    output = []\n\n    tags = stream_data.get(\"tags\") or {}\n    encoder = tags.get(\"encoder\") or \"\"\n    if encoder.endswith(\"prores_ks\"):\n        codec_name = \"prores_ks\"\n\n    elif encoder.endswith(\"prores_aw\"):\n        codec_name = \"prores_aw\"\n\n    else:\n        codec_name = \"prores\"\n\n    output.extend([\"-codec:v\", codec_name])\n\n    pix_fmt = stream_data.get(\"pix_fmt\")\n    if pix_fmt:\n        output.extend([\"-pix_fmt\", pix_fmt])\n\n    # Rest of arguments is prores_kw specific\n    if codec_name == \"prores_ks\":\n        codec_tag_to_profile_map = {\n            \"apco\": \"proxy\",\n            \"apcs\": \"lt\",\n            \"apcn\": \"standard\",\n            \"apch\": \"hq\",\n            \"ap4h\": \"4444\",\n            \"ap4x\": \"4444xq\"\n        }\n        codec_tag_str = stream_data.get(\"codec_tag_string\")\n        if codec_tag_str:\n            profile = codec_tag_to_profile_map.get(codec_tag_str)\n            if profile:\n                output.extend([\"-profile:v\", profile])\n\n    return output\n\n\ndef _ffmpeg_h264_codec_args(stream_data, source_ffmpeg_cmd):\n    output = [\"-codec:v\", \"h264\"]\n\n    # Use arguments from source if are available source arguments\n    if source_ffmpeg_cmd:\n        copy_args = (\n            \"-crf\",\n            \"-b:v\", \"-vb\",\n            \"-minrate\", \"-minrate:\",\n            \"-maxrate\", \"-maxrate:\",\n            \"-bufsize\", \"-bufsize:\"\n        )\n        args = source_ffmpeg_cmd.split(\" \")\n        for idx, arg in enumerate(args):\n            if arg in copy_args:\n                output.extend([arg, args[idx + 1]])\n\n    pix_fmt = stream_data.get(\"pix_fmt\")\n    if pix_fmt:\n        output.extend([\"-pix_fmt\", pix_fmt])\n\n    output.extend([\"-intra\", \"-g\", \"1\"])\n    return output\n\n\ndef _ffmpeg_dnxhd_codec_args(stream_data, source_ffmpeg_cmd):\n    output = [\"-codec:v\", \"dnxhd\"]\n\n    # Use source profile (profiles in metadata are not usable in args directly)\n    profile = stream_data.get(\"profile\") or \"\"\n    # Lower profile and replace space with underscore\n    cleaned_profile = profile.lower().replace(\" \", \"_\")\n\n    # TODO validate this statement\n    # Looks like using 'dnxhd' profile must have set bit rate and in that case\n    #   should be used bitrate from source.\n    # - related attributes 'bit_rate_defined', 'bit_rate_must_be_defined'\n    bit_rate_must_be_defined = True\n    dnx_profiles = {\n        \"dnxhd\",\n        \"dnxhr_lb\",\n        \"dnxhr_sq\",\n        \"dnxhr_hq\",\n        \"dnxhr_hqx\",\n        \"dnxhr_444\"\n    }\n    if cleaned_profile in dnx_profiles:\n        if cleaned_profile != \"dnxhd\":\n            bit_rate_must_be_defined = False\n        output.extend([\"-profile:v\", cleaned_profile])\n\n    pix_fmt = stream_data.get(\"pix_fmt\")\n    if pix_fmt:\n        output.extend([\"-pix_fmt\", pix_fmt])\n\n    # Use arguments from source if are available source arguments\n    bit_rate_defined = False\n    if source_ffmpeg_cmd:\n        # Define bitrate arguments\n        bit_rate_args = (\"-b:v\", \"-vb\",)\n        # Separate the two variables in case something else should be copied\n        #   from source command\n        copy_args = []\n        copy_args.extend(bit_rate_args)\n\n        args = source_ffmpeg_cmd.split(\" \")\n        for idx, arg in enumerate(args):\n            if arg in copy_args:\n                if arg in bit_rate_args:\n                    bit_rate_defined = True\n                output.extend([arg, args[idx + 1]])\n\n    # Add bitrate if needed\n    if bit_rate_must_be_defined and not bit_rate_defined:\n        src_bit_rate = stream_data.get(\"bit_rate\")\n        if src_bit_rate:\n            output.extend([\"-b:v\", src_bit_rate])\n\n    output.extend([\"-g\", \"1\"])\n    return output\n\n\ndef convert_ffprobe_fps_value(str_value):\n    \"\"\"Returns (str) value of fps from ffprobe frame format (120/1)\"\"\"\n    if str_value == \"0/0\":\n        print(\"WARNING: Source has \\\"r_frame_rate\\\" value set to \\\"0/0\\\".\")\n        return \"Unknown\"\n\n    items = str_value.split(\"/\")\n    if len(items) == 1:\n        fps = float(items[0])\n\n    elif len(items) == 2:\n        fps = float(items[0]) / float(items[1])\n\n    # Check if fps is integer or float number\n    if int(fps) == fps:\n        fps = int(fps)\n\n    return str(fps)\n\n\ndef convert_ffprobe_fps_to_float(value):\n    \"\"\"Convert string value of frame rate to float.\n\n    Copy of 'convert_ffprobe_fps_value' which raises exceptions on invalid\n    value, does not convert value to string and does not return \"Unknown\"\n    string.\n\n    Args:\n        value (str): Value to be converted.\n\n    Returns:\n        Float: Converted frame rate in float. If divisor in value is '0' then\n            '0.0' is returned.\n\n    Raises:\n        ValueError: Passed value is invalid for conversion.\n    \"\"\"\n\n    if not value:\n        raise ValueError(\"Got empty value.\")\n\n    items = value.split(\"/\")\n    if len(items) == 1:\n        return float(items[0])\n\n    if len(items) > 2:\n        raise ValueError((\n            \"FPS expression contains multiple dividers \\\"{}\\\".\"\n        ).format(value))\n\n    dividend = float(items.pop(0))\n    divisor = float(items.pop(0))\n    if divisor == 0.0:\n        return 0.0\n    return dividend / divisor\n\n\ndef convert_colorspace(\n    input_path,\n    output_path,\n    config_path,\n    source_colorspace,\n    target_colorspace=None,\n    view=None,\n    display=None,\n    additional_command_args=None,\n    logger=None,\n):\n    \"\"\"Convert source file from one color space to another.\n\n    Args:\n        input_path (str): Path that should be converted. It is expected that\n            contains single file or image sequence of same type\n            (sequence in format 'file.FRAMESTART-FRAMEEND#.ext', see oiio docs,\n            eg `big.1-3#.tif`)\n        output_path (str): Path to output filename.\n            (must follow format of 'input_path', eg. single file or\n             sequence in 'file.FRAMESTART-FRAMEEND#.ext', `output.1-3#.tif`)\n        config_path (str): path to OCIO config file\n        source_colorspace (str): ocio valid color space of source files\n        target_colorspace (str): ocio valid target color space\n                    if filled, 'view' and 'display' must be empty\n        view (str): name for viewer space (ocio valid)\n            both 'view' and 'display' must be filled (if 'target_colorspace')\n        display (str): name for display-referred reference space (ocio valid)\n            both 'view' and 'display' must be filled (if 'target_colorspace')\n        additional_command_args (list): arguments for oiiotool (like binary\n            depth for .dpx)\n        logger (logging.Logger): Logger used for logging.\n    Raises:\n        ValueError: if misconfigured\n    \"\"\"\n    if logger is None:\n        logger = logging.getLogger(__name__)\n\n    input_info = get_oiio_info_for_input(input_path, logger=logger)\n\n    # Collect channels to export\n    input_arg, channels_arg = get_oiio_input_and_channel_args(input_info)\n\n    # Prepare subprocess arguments\n    oiio_cmd = get_oiio_tool_args(\n        \"oiiotool\",\n        # Don't add any additional attributes\n        \"--nosoftwareattrib\",\n        \"--colorconfig\", config_path\n    )\n\n    oiio_cmd.extend([\n        input_arg, input_path,\n        # Tell oiiotool which channels should be put to top stack\n        #   (and output)\n        \"--ch\", channels_arg,\n        # Use first subimage\n        \"--subimage\", \"0\"\n    ])\n\n    if all([target_colorspace, view, display]):\n        raise ValueError(\"Colorspace and both screen and display\"\n                         \" cannot be set together.\"\n                         \"Choose colorspace or screen and display\")\n    if not target_colorspace and not all([view, display]):\n        raise ValueError(\"Both screen and display must be set.\")\n\n    if additional_command_args:\n        oiio_cmd.extend(additional_command_args)\n\n    if target_colorspace:\n        oiio_cmd.extend([\"--colorconvert\",\n                         source_colorspace,\n                         target_colorspace])\n    if view and display:\n        oiio_cmd.extend([\"--iscolorspace\", source_colorspace])\n        oiio_cmd.extend([\"--ociodisplay\", display, view])\n\n    oiio_cmd.extend([\"-o\", output_path])\n\n    logger.debug(\"Conversion command: {}\".format(\" \".join(oiio_cmd)))\n    run_subprocess(oiio_cmd, logger=logger)\n\n\ndef split_cmd_args(in_args):\n    \"\"\"Makes sure all entered arguments are separated in individual items.\n\n    Split each argument string with \" -\" to identify if string contains\n    one or more arguments.\n    Args:\n        in_args (list): of arguments ['-n', '-d uint10']\n    Returns\n        (list): ['-n', '-d', 'unint10']\n    \"\"\"\n    splitted_args = []\n    for arg in in_args:\n        if not arg.strip():\n            continue\n        splitted_args.extend(arg.split(\" \"))\n    return splitted_args\n\n\ndef get_rescaled_command_arguments(\n        application,\n        input_path,\n        target_width,\n        target_height,\n        target_par=None,\n        bg_color=None,\n        log=None\n):\n    \"\"\"Get command arguments for rescaling input to target size.\n\n    Args:\n        application (str): Application for which command should be created.\n            Currently supported are \"ffmpeg\" and \"oiiotool\".\n        input_path (str): Path to input file.\n        target_width (int): Width of target.\n        target_height (int): Height of target.\n        target_par (Optional[float]): Pixel aspect ratio of target.\n        bg_color (Optional[list[int]]): List of 8bit int values for\n            background color. Should be in range 0 - 255.\n        log (Optional[logging.Logger]): Logger used for logging.\n\n    Returns:\n        list[str]: List of command arguments.\n    \"\"\"\n    command_args = []\n    target_par = target_par or 1.0\n    input_par = 1.0\n\n    input_height, input_width, stream_input_par = _get_image_dimensions(\n        application, input_path, log)\n    if stream_input_par:\n        input_par = (\n            float(stream_input_par.split(\":\")[0])\n            / float(stream_input_par.split(\":\")[1])\n        )\n    # recalculating input and target width\n    input_width = int(input_width * input_par)\n    target_width = int(target_width * target_par)\n\n    # calculate aspect ratios\n    target_aspect = float(target_width) / target_height\n    input_aspect = float(input_width) / input_height\n\n    # calculate scale size\n    scale_size = float(input_width) / target_width\n    if input_aspect < target_aspect:\n        scale_size = float(input_height) / target_height\n\n    # calculate rescaled width and height\n    rescaled_width = int(input_width / scale_size)\n    rescaled_height = int(input_height / scale_size)\n\n    # calculate width and height shift\n    rescaled_width_shift = int((target_width - rescaled_width) / 2)\n    rescaled_height_shift = int((target_height - rescaled_height) / 2)\n\n    if application == \"ffmpeg\":\n        # create scale command\n        scale = \"scale={0}:{1}\".format(input_width, input_height)\n        pad = \"pad={0}:{1}:({2}-iw)/2:({3}-ih)/2\".format(\n            target_width,\n            target_height,\n            target_width,\n            target_height\n        )\n        if input_width > target_width or input_height > target_height:\n            scale = \"scale={0}:{1}\".format(rescaled_width, rescaled_height)\n            pad = \"pad={0}:{1}:{2}:{3}\".format(\n                target_width,\n                target_height,\n                rescaled_width_shift,\n                rescaled_height_shift\n            )\n\n        if bg_color:\n            color = convert_color_values(application, bg_color)\n            pad += \":{0}\".format(color)\n        command_args.extend([\"-vf\", \"{0},{1}\".format(scale, pad)])\n\n    elif application == \"oiiotool\":\n        input_info = get_oiio_info_for_input(input_path, logger=log)\n        # Collect channels to export\n        _, channels_arg = get_oiio_input_and_channel_args(\n            input_info, alpha_default=1.0)\n\n        command_args.extend([\n            # Tell oiiotool which channels should be put to top stack\n            #   (and output)\n            \"--ch\", channels_arg,\n            # Use first subimage\n            \"--subimage\", \"0\"\n        ])\n\n        if input_par != 1.0:\n            command_args.extend([\"--pixelaspect\", \"1\"])\n\n        width_shift = int((target_width - input_width) / 2)\n        height_shift = int((target_height - input_height) / 2)\n\n        # default resample is not scaling source image\n        resample = [\n            \"--resize\",\n            \"{0}x{1}\".format(input_width, input_height),\n            \"--origin\",\n            \"+{0}+{1}\".format(width_shift, height_shift),\n        ]\n        # scaled source image to target size\n        if input_width > target_width or input_height > target_height:\n            # form resample command\n            resample = [\n                \"--resize:filter=lanczos3\",\n                \"{0}x{1}\".format(rescaled_width, rescaled_height),\n                \"--origin\",\n                \"+{0}+{1}\".format(rescaled_width_shift, rescaled_height_shift),\n            ]\n        command_args.extend(resample)\n\n        fullsize = [\n            \"--fullsize\",\n            \"{0}x{1}\".format(target_width, target_height)\n        ]\n        if bg_color:\n            color = convert_color_values(application, bg_color)\n\n            fullsize.extend([\n                \"--pattern\",\n                \"constant:color={0}\".format(color),\n                \"{0}x{1}\".format(target_width, target_height),\n                \"4\",  # 4 channels\n                \"--over\"\n            ])\n        command_args.extend(fullsize)\n\n    else:\n        raise ValueError(\n            \"\\\"application\\\" input argument should \"\n            \"be either \\\"ffmpeg\\\" or \\\"oiiotool\\\"\"\n        )\n\n    return command_args\n\n\ndef _get_image_dimensions(application, input_path, log):\n    \"\"\"Uses 'ffprobe' first and then 'oiiotool' if available to get dim.\n\n    Args:\n        application (str): \"oiiotool\"|\"ffmpeg\"\n        input_path (str): path to image file\n        log (Optional[logging.Logger]): Logger used for logging.\n    Returns:\n        (tuple) (int, int, dict) - (height, width, sample_aspect_ratio)\n    Raises:\n        RuntimeError if image dimensions couldn't be parsed out.\n    \"\"\"\n    # ffmpeg command\n    input_file_metadata = get_ffprobe_data(input_path, logger=log)\n    input_width = input_height = 0\n    stream = next(\n        (\n            s for s in input_file_metadata[\"streams\"]\n            if s.get(\"codec_type\") == \"video\"\n        ),\n        {}\n    )\n    if stream:\n        input_width = int(stream[\"width\"])\n        input_height = int(stream[\"height\"])\n\n    # fallback for weird files with width=0, height=0\n    if (input_width == 0 or input_height == 0) and application == \"oiiotool\":\n        # Load info about file from oiio tool\n        input_info = get_oiio_info_for_input(input_path, logger=log)\n        if input_info:\n            input_width = int(input_info[\"width\"])\n            input_height = int(input_info[\"height\"])\n\n    if input_width == 0 or input_height == 0:\n        raise RuntimeError(\"Couldn't read {} either \"\n                           \"with ffprobe or oiiotool\".format(input_path))\n\n    stream_input_par = stream.get(\"sample_aspect_ratio\")\n    return input_height, input_width, stream_input_par\n\n\ndef convert_color_values(application, color_value):\n    \"\"\"Get color mapping for ffmpeg and oiiotool.\n    Args:\n        application (str): Application for which command should be created.\n        color_value (list[int]): List of 8bit int values for RGBA.\n    Returns:\n        str: ffmpeg returns hex string, oiiotool is string with floats.\n    \"\"\"\n    red, green, blue, alpha = color_value\n\n    if application == \"ffmpeg\":\n        return \"{0:0>2X}{1:0>2X}{2:0>2X}@{3}\".format(\n            red, green, blue, (alpha / 255.0)\n        )\n    elif application == \"oiiotool\":\n        red = float(red / 255)\n        green = float(green / 255)\n        blue = float(blue / 255)\n        alpha = float(alpha / 255)\n\n        return \"{0:.3f},{1:.3f},{2:.3f},{3:.3f}\".format(\n            red, green, blue, alpha)\n    else:\n        raise ValueError(\n            \"\\\"application\\\" input argument should \"\n            \"be either \\\"ffmpeg\\\" or \\\"oiiotool\\\"\"\n        )\n\n\ndef get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None):\n    \"\"\"Get input and channel arguments for oiiotool.\n    Args:\n        oiio_input_info (dict): Information about input from oiio tool.\n            Should be output of function `get_oiio_info_for_input`.\n        alpha_default (float, optional): Default value for alpha channel.\n    Returns:\n        tuple[str, str]: Tuple of input and channel arguments.\n    \"\"\"\n    channel_names = oiio_input_info[\"channelnames\"]\n    review_channels = get_convert_rgb_channels(channel_names)\n\n    if review_channels is None:\n        raise ValueError(\n            \"Couldn't find channels that can be used for conversion.\"\n        )\n\n    red, green, blue, alpha = review_channels\n    input_channels = [red, green, blue]\n\n    channels_arg = \"R={0},G={1},B={2}\".format(red, green, blue)\n    if alpha is not None:\n        channels_arg += \",A={}\".format(alpha)\n        input_channels.append(alpha)\n    elif alpha_default:\n        channels_arg += \",A={}\".format(float(alpha_default))\n        input_channels.append(\"A\")\n\n    input_channels_str = \",\".join(input_channels)\n\n    subimages = oiio_input_info.get(\"subimages\")\n    input_arg = \"-i\"\n    if subimages is None or subimages == 1:\n        # Tell oiiotool which channels should be loaded\n        # - other channels are not loaded to memory so helps to avoid memory\n        #       leak issues\n        # - this option is crashing if used on multipart exrs\n        input_arg += \":ch={}\".format(input_channels_str)\n\n    return input_arg, channels_arg\n"
  },
  {
    "path": "openpype/lib/usdlib.py",
    "content": "import os\nimport re\nimport logging\n\ntry:\n    from pxr import Usd, UsdGeom, Sdf, Kind\nexcept ImportError:\n    # Allow to fall back on Multiverse 6.3.0+ pxr usd library\n    from mvpxr import Usd, UsdGeom, Sdf, Kind\n\nfrom openpype.client import get_project, get_asset_by_name\nfrom openpype.pipeline import Anatomy, get_current_project_name\n\nlog = logging.getLogger(__name__)\n\n\n# The predefined steps order used for bootstrapping USD Shots and Assets.\n# These are ordered in order from strongest to weakest opinions, like in USD.\nPIPELINE = {\n    \"shot\": [\n        \"usdLighting\",\n        \"usdFx\",\n        \"usdSimulation\",\n        \"usdAnimation\",\n        \"usdLayout\",\n    ],\n    \"asset\": [\"usdShade\", \"usdModel\"],\n}\n\n\ndef create_asset(\n    filepath, asset_name, reference_layers, kind=Kind.Tokens.component\n):\n    \"\"\"\n    Creates an asset file that consists of a top level layer and sublayers for\n    shading and geometry.\n\n    Args:\n        filepath (str): Filepath where the asset.usd file will be saved.\n        reference_layers (list): USD Files to reference in the asset.\n            Note that the bottom layer (first file, like a model) would\n            be last in the list. The strongest layer will be the first\n            index.\n        asset_name (str): The name for the Asset identifier and default prim.\n        kind (pxr.Kind): A USD Kind for the root asset.\n\n    \"\"\"\n    # Also see create_asset.py in PixarAnimationStudios/USD endToEnd example\n\n    log.info(\"Creating asset at %s\", filepath)\n\n    # Make the layer ascii - good for readability, plus the file is small\n    root_layer = Sdf.Layer.CreateNew(filepath, args={\"format\": \"usda\"})\n    stage = Usd.Stage.Open(root_layer)\n\n    # Define a prim for the asset and make it the default for the stage.\n    asset_prim = UsdGeom.Xform.Define(stage, \"/%s\" % asset_name).GetPrim()\n    stage.SetDefaultPrim(asset_prim)\n\n    # Let viewing applications know how to orient a free camera properly\n    UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.y)\n\n    # Usually we will \"loft up\" the kind authored into the exported geometry\n    # layer rather than re-stamping here; we'll leave that for a later\n    # tutorial, and just be explicit here.\n    model = Usd.ModelAPI(asset_prim)\n    if kind:\n        model.SetKind(kind)\n\n    model.SetAssetName(asset_name)\n    model.SetAssetIdentifier(\"%s/%s.usd\" % (asset_name, asset_name))\n\n    # Add references to the  asset prim\n    references = asset_prim.GetReferences()\n    for reference_filepath in reference_layers:\n        references.AddReference(reference_filepath)\n\n    stage.GetRootLayer().Save()\n\n\ndef create_shot(filepath, layers, create_layers=False):\n    \"\"\"Create a shot with separate layers for departments.\n\n    Args:\n        filepath (str): Filepath where the asset.usd file will be saved.\n        layers (str): When provided this will be added verbatim in the\n            subLayerPaths layers. When the provided layer paths do not exist\n            they are generated using  Sdf.Layer.CreateNew\n        create_layers (bool): Whether to create the stub layers on disk if\n            they do not exist yet.\n\n    Returns:\n        str: The saved shot file path\n\n    \"\"\"\n    # Also see create_shot.py in PixarAnimationStudios/USD endToEnd example\n\n    stage = Usd.Stage.CreateNew(filepath)\n    log.info(\"Creating shot at %s\" % filepath)\n\n    for layer_path in layers:\n        if create_layers and not os.path.exists(layer_path):\n            # We use the Sdf API here to quickly create layers.  Also, we're\n            # using it as a way to author the subLayerPaths as there is no\n            # way to do that directly in the Usd API.\n            layer_folder = os.path.dirname(layer_path)\n            if not os.path.exists(layer_folder):\n                os.makedirs(layer_folder)\n\n            Sdf.Layer.CreateNew(layer_path)\n\n        stage.GetRootLayer().subLayerPaths.append(layer_path)\n\n    # Lets viewing applications know how to orient a free camera properly\n    UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.y)\n    stage.GetRootLayer().Save()\n\n    return filepath\n\n\ndef create_model(filename, asset, variant_subsets):\n    \"\"\"Create a USD Model file.\n\n    For each of the variation paths it will payload the path and set its\n    relevant variation name.\n\n    \"\"\"\n\n    project_name = get_current_project_name()\n    asset_doc = get_asset_by_name(project_name, asset)\n    assert asset_doc, \"Asset not found: %s\" % asset\n\n    variants = []\n    for subset in variant_subsets:\n        prefix = \"usdModel\"\n        if subset.startswith(prefix):\n            # Strip off `usdModel_`\n            variant = subset[len(prefix):]\n        else:\n            raise ValueError(\n                \"Model subsets must start \" \"with usdModel: %s\" % subset\n            )\n\n        path = get_usd_master_path(\n            asset=asset_doc, subset=subset, representation=\"usd\"\n        )\n        variants.append((variant, path))\n\n    stage = _create_variants_file(\n        filename,\n        variants=variants,\n        variantset=\"model\",\n        variant_prim=\"/root\",\n        reference_prim=\"/root/geo\",\n        as_payload=True,\n    )\n\n    UsdGeom.SetStageMetersPerUnit(stage, 1)\n    UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.y)\n\n    # modelAPI = Usd.ModelAPI(root_prim)\n    # modelAPI.SetKind(Kind.Tokens.component)\n\n    # See http://openusd.org/docs/api/class_usd_model_a_p_i.html#details\n    # for more on assetInfo\n    # modelAPI.SetAssetName(asset)\n    # modelAPI.SetAssetIdentifier(asset)\n\n    stage.GetRootLayer().Save()\n\n\ndef create_shade(filename, asset, variant_subsets):\n    \"\"\"Create a master USD shade file for an asset.\n\n    For each available model variation this should generate a reference\n    to a `usdShade_{modelVariant}` subset.\n\n    \"\"\"\n\n    project_name = get_current_project_name()\n    asset_doc = get_asset_by_name(project_name, asset)\n    assert asset_doc, \"Asset not found: %s\" % asset\n\n    variants = []\n\n    for subset in variant_subsets:\n        prefix = \"usdModel\"\n        if subset.startswith(prefix):\n            # Strip off `usdModel_`\n            variant = subset[len(prefix):]\n        else:\n            raise ValueError(\n                \"Model subsets must start \" \"with usdModel: %s\" % subset\n            )\n\n        shade_subset = re.sub(\"^usdModel\", \"usdShade\", subset)\n        path = get_usd_master_path(\n            asset=asset_doc, subset=shade_subset, representation=\"usd\"\n        )\n        variants.append((variant, path))\n\n    stage = _create_variants_file(\n        filename, variants=variants, variantset=\"model\", variant_prim=\"/root\"\n    )\n\n    stage.GetRootLayer().Save()\n\n\ndef create_shade_variation(filename, asset, model_variant, shade_variants):\n    \"\"\"Create the master Shade file for a specific model variant.\n\n    This should reference all shade variants for the specific model variant.\n\n    \"\"\"\n\n    project_name = get_current_project_name()\n    asset_doc = get_asset_by_name(project_name, asset)\n    assert asset_doc, \"Asset not found: %s\" % asset\n\n    variants = []\n    for variant in shade_variants:\n        subset = \"usdShade_{model}_{shade}\".format(\n            model=model_variant, shade=variant\n        )\n        path = get_usd_master_path(\n            asset=asset_doc, subset=subset, representation=\"usd\"\n        )\n        variants.append((variant, path))\n\n    stage = _create_variants_file(\n        filename, variants=variants, variantset=\"shade\", variant_prim=\"/root\"\n    )\n\n    stage.GetRootLayer().Save()\n\n\ndef _create_variants_file(\n    filename,\n    variants,\n    variantset,\n    default_variant=None,\n    variant_prim=\"/root\",\n    reference_prim=None,\n    set_default_variant=True,\n    as_payload=False,\n    skip_variant_on_single_file=True,\n):\n\n    root_layer = Sdf.Layer.CreateNew(filename, args={\"format\": \"usda\"})\n    stage = Usd.Stage.Open(root_layer)\n\n    root_prim = stage.DefinePrim(variant_prim)\n    stage.SetDefaultPrim(root_prim)\n\n    def _reference(path):\n        \"\"\"Reference/Payload path depending on function arguments\"\"\"\n\n        if reference_prim:\n            prim = stage.DefinePrim(reference_prim)\n        else:\n            prim = root_prim\n\n        if as_payload:\n            # Payload\n            prim.GetPayloads().AddPayload(Sdf.Payload(path))\n        else:\n            # Reference\n            prim.GetReferences().AddReference(Sdf.Reference(path))\n\n    assert variants, \"Must have variants, got: %s\" % variants\n\n    log.info(filename)\n\n    if skip_variant_on_single_file and len(variants) == 1:\n        # Reference directly, no variants\n        variant_path = variants[0][1]\n        _reference(variant_path)\n\n        log.info(\"Non-variants..\")\n        log.info(\"Path: %s\" % variant_path)\n\n    else:\n        # Variants\n        append = Usd.ListPositionBackOfAppendList\n        variant_set = root_prim.GetVariantSets().AddVariantSet(\n            variantset, append\n        )\n\n        for variant, variant_path in variants:\n\n            if default_variant is None:\n                default_variant = variant\n\n            variant_set.AddVariant(variant, append)\n            variant_set.SetVariantSelection(variant)\n            with variant_set.GetVariantEditContext():\n                _reference(variant_path)\n\n                log.info(\"Variants..\")\n                log.info(\"Variant: %s\" % variant)\n                log.info(\"Path: %s\" % variant_path)\n\n        if set_default_variant:\n            variant_set.SetVariantSelection(default_variant)\n\n    return stage\n\n\ndef get_usd_master_path(asset, subset, representation):\n    \"\"\"Get the filepath for a .usd file of a subset.\n\n    This will return the path to an unversioned master file generated by\n    `usd_master_file.py`.\n\n    \"\"\"\n\n    project_name = get_current_project_name()\n    anatomy = Anatomy(project_name)\n    project_doc = get_project(\n        project_name,\n        fields=[\"name\", \"data.code\"]\n    )\n\n    if isinstance(asset, dict) and \"name\" in asset:\n        # Allow explicitly passing asset document\n        asset_doc = asset\n    else:\n        asset_doc = get_asset_by_name(project_name, asset, fields=[\"name\"])\n\n    template_obj = anatomy.templates_obj[\"publish\"][\"path\"]\n    path = template_obj.format_strict(\n        {\n            \"project\": {\n                \"name\": project_name,\n                \"code\": project_doc.get(\"data\", {}).get(\"code\")\n            },\n            \"folder\": {\n                \"name\": asset_doc[\"name\"],\n            },\n            \"asset\": asset_doc[\"name\"],\n            \"subset\": subset,\n            \"representation\": representation,\n            \"version\": 0,  # stub version zero\n        }\n    )\n\n    # Remove the version folder\n    subset_folder = os.path.dirname(os.path.dirname(path))\n    master_folder = os.path.join(subset_folder, \"master\")\n    fname = \"{0}.{1}\".format(subset, representation)\n\n    return os.path.join(master_folder, fname).replace(\"\\\\\", \"/\")\n\n\ndef parse_avalon_uri(uri):\n    # URI Pattern: avalon://{asset}/{subset}.{ext}\n    pattern = r\"avalon://(?P<asset>[^/.]*)/(?P<subset>[^/]*)\\.(?P<ext>.*)\"\n    if uri.startswith(\"avalon://\"):\n        match = re.match(pattern, uri)\n        if match:\n            return match.groupdict()\n"
  },
  {
    "path": "openpype/lib/vendor_bin_utils.py",
    "content": "import os\nimport logging\nimport platform\nimport subprocess\n\nfrom openpype import AYON_SERVER_ENABLED\n\nlog = logging.getLogger(\"Vendor utils\")\n\n\nclass ToolNotFoundError(Exception):\n    \"\"\"Raised when tool arguments are not found.\"\"\"\n\n\nclass CachedToolPaths:\n    \"\"\"Cache already used and discovered tools and their executables.\n\n    Discovering path can take some time and can trigger subprocesses so it's\n    better to cache the paths on first get.\n    \"\"\"\n\n    _cached_paths = {}\n\n    @classmethod\n    def is_tool_cached(cls, tool):\n        return tool in cls._cached_paths\n\n    @classmethod\n    def get_executable_path(cls, tool):\n        return cls._cached_paths.get(tool)\n\n    @classmethod\n    def cache_executable_path(cls, tool, path):\n        cls._cached_paths[tool] = path\n\n\ndef is_file_executable(filepath):\n    \"\"\"Filepath lead to executable file.\n\n    Args:\n        filepath(str): Full path to file.\n    \"\"\"\n    if not filepath:\n        return False\n\n    if os.path.isfile(filepath):\n        if os.access(filepath, os.X_OK):\n            return True\n\n        log.info(\n            \"Filepath is not available for execution \\\"{}\\\"\".format(filepath)\n        )\n    return False\n\n\ndef find_executable(executable):\n    \"\"\"Find full path to executable.\n\n    Also tries additional extensions if passed executable does not contain one.\n\n    Paths where it is looked for executable is defined by 'PATH' environment\n    variable, 'os.confstr(\"CS_PATH\")' or 'os.defpath'.\n\n    Args:\n        executable(str): Name of executable with or without extension. Can be\n            path to file.\n\n    Returns:\n        Union[str, None]: Full path to executable with extension which was\n            found otherwise None.\n    \"\"\"\n\n    # Skip if passed path is file\n    if is_file_executable(executable):\n        return executable\n\n    low_platform = platform.system().lower()\n    _, ext = os.path.splitext(executable)\n\n    # Prepare extensions to check\n    exts = set()\n    if ext:\n        exts.add(ext.lower())\n\n    else:\n        # Add other possible extension variants only if passed executable\n        #   does not have any\n        if low_platform == \"windows\":\n            exts |= {\".exe\", \".ps1\", \".bat\"}\n            for ext in os.getenv(\"PATHEXT\", \"\").split(os.pathsep):\n                exts.add(ext.lower())\n\n        else:\n            exts |= {\".sh\"}\n\n    # Executable is a path but there may be missing extension\n    #   - this can happen primarily on windows where\n    #       e.g. \"ffmpeg\" should be \"ffmpeg.exe\"\n    exe_dir, exe_filename = os.path.split(executable)\n    if exe_dir and os.path.isdir(exe_dir):\n        for filename in os.listdir(exe_dir):\n            filepath = os.path.join(exe_dir, filename)\n            basename, ext = os.path.splitext(filename)\n            if (\n                basename == exe_filename\n                and ext.lower() in exts\n                and is_file_executable(filepath)\n            ):\n                return filepath\n\n    # Get paths where to look for executable\n    path_str = os.environ.get(\"PATH\", None)\n    if path_str is None:\n        if hasattr(os, \"confstr\"):\n            path_str = os.confstr(\"CS_PATH\")\n        elif hasattr(os, \"defpath\"):\n            path_str = os.defpath\n\n    if not path_str:\n        return None\n\n    paths = path_str.split(os.pathsep)\n    for path in paths:\n        if not os.path.isdir(path):\n            continue\n        for filename in os.listdir(path):\n            filepath = os.path.abspath(os.path.join(path, filename))\n            # Filename matches executable exactly\n            if filename == executable and is_file_executable(filepath):\n                return filepath\n\n            basename, ext = os.path.splitext(filename)\n            if (\n                basename == executable\n                and ext.lower() in exts\n                and is_file_executable(filepath)\n            ):\n                return filepath\n\n    return None\n\n\ndef get_vendor_bin_path(bin_app):\n    \"\"\"Path to OpenPype vendorized binaries.\n\n    Vendorized executables are expected in specific hierarchy inside build or\n    in code source.\n\n    \"{OPENPYPE_ROOT}/vendor/bin/{name of vendorized app}/{platform}\"\n\n    Args:\n        bin_app (str): Name of vendorized application.\n\n    Returns:\n        str: Path to vendorized binaries folder.\n    \"\"\"\n\n    return os.path.join(\n        os.environ[\"OPENPYPE_ROOT\"],\n        \"vendor\",\n        \"bin\",\n        bin_app,\n        platform.system().lower()\n    )\n\n\ndef find_tool_in_custom_paths(paths, tool, validation_func=None):\n    \"\"\"Find a tool executable in custom paths.\n\n    Args:\n        paths (Iterable[str]): Iterable of paths where to look for tool.\n        tool (str): Name of tool (binary file) to find in passed paths.\n        validation_func (Function): Custom validation function of path.\n            Function must expect one argument which is path to executable.\n            If not passed only 'find_executable' is used to be able identify\n            if path is valid.\n\n    Reuturns:\n        Union[str, None]: Path to validated executable or None if was not\n            found.\n    \"\"\"\n\n    for path in paths:\n        # Skip empty strings\n        if not path:\n            continue\n\n        # Handle cases when path is just an executable\n        #   - it allows to use executable from PATH\n        #   - basename must match 'tool' value (without extension)\n        extless_path, ext = os.path.splitext(path)\n        if extless_path == tool:\n            executable_path = find_executable(tool)\n            if executable_path and (\n                validation_func is None\n                or validation_func(executable_path)\n            ):\n                return executable_path\n            continue\n\n        # Normalize path because it should be a path and check if exists\n        normalized = os.path.normpath(path)\n        if not os.path.exists(normalized):\n            continue\n\n        # Note: Path can be both file and directory\n\n        # If path is a file validate it\n        if os.path.isfile(normalized):\n            basename, ext = os.path.splitext(os.path.basename(path))\n            # Check if the filename has actually the sane bane as 'tool'\n            if basename == tool:\n                executable_path = find_executable(normalized)\n                if executable_path and (\n                    validation_func is None\n                    or validation_func(executable_path)\n                ):\n                    return executable_path\n\n        # Check if path is a directory and look for tool inside the dir\n        if os.path.isdir(normalized):\n            executable_path = find_executable(os.path.join(normalized, tool))\n            if executable_path and (\n                validation_func is None\n                or validation_func(executable_path)\n            ):\n                return executable_path\n    return None\n\n\ndef _check_args_returncode(args):\n    try:\n        kwargs = {}\n        if platform.system().lower() == \"windows\":\n            kwargs[\"creationflags\"] = (\n                subprocess.CREATE_NEW_PROCESS_GROUP\n                | getattr(subprocess, \"DETACHED_PROCESS\", 0)\n                | getattr(subprocess, \"CREATE_NO_WINDOW\", 0)\n            )\n\n        if hasattr(subprocess, \"DEVNULL\"):\n            proc = subprocess.Popen(\n                args,\n                stdout=subprocess.DEVNULL,\n                stderr=subprocess.DEVNULL,\n                **kwargs\n            )\n            proc.wait()\n        else:\n            with open(os.devnull, \"w\") as devnull:\n                proc = subprocess.Popen(\n                    args, stdout=devnull, stderr=devnull, **kwargs\n                )\n                proc.wait()\n\n    except Exception:\n        return False\n    return proc.returncode == 0\n\n\ndef _oiio_executable_validation(args):\n    \"\"\"Validate oiio tool executable if can be executed.\n\n    Validation has 2 steps. First is using 'find_executable' to fill possible\n    missing extension or fill directory then launch executable and validate\n    that it can be executed. For that is used '--help' argument which is fast\n    and does not need any other inputs.\n\n    Any possible crash of missing libraries or invalid build should be caught.\n\n    Main reason is to validate if executable can be executed on OS just running\n    which can be issue ob linux machines.\n\n    Note:\n        It does not validate if the executable is really a oiio tool which\n            should be used.\n\n    Args:\n        args (Union[str, list[str]]): Arguments to launch tool or\n            path to tool executable.\n\n    Returns:\n        bool: Filepath is valid executable.\n    \"\"\"\n\n    if not args:\n        return False\n\n    if not isinstance(args, list):\n        filepath = find_executable(args)\n        if not filepath:\n            return False\n        args = [filepath]\n    return _check_args_returncode(args + [\"--help\"])\n\n\ndef _get_ayon_oiio_tool_args(tool_name):\n    try:\n        # Use 'ayon-third-party' addon to get oiio arguments\n        from ayon_third_party import get_oiio_arguments\n    except Exception:\n        print(\"!!! Failed to import 'ayon_third_party' addon.\")\n        return None\n\n    try:\n        return get_oiio_arguments(tool_name)\n    except Exception as exc:\n        print(\"!!! Failed to get OpenImageIO args. Reason: {}\".format(exc))\n    return None\n\n\ndef get_oiio_tools_path(tool=\"oiiotool\"):\n    \"\"\"Path to OpenImageIO tool executables.\n\n    On Windows it adds .exe extension if missing from tool argument.\n\n    Args:\n        tool (string): Tool name 'oiiotool', 'maketx', etc.\n            Default is \"oiiotool\".\n    \"\"\"\n\n    if CachedToolPaths.is_tool_cached(tool):\n        return CachedToolPaths.get_executable_path(tool)\n\n    if AYON_SERVER_ENABLED:\n        args = _get_ayon_oiio_tool_args(tool)\n        if args:\n            if len(args) > 1:\n                raise ValueError(\n                    \"AYON oiio arguments consist of multiple arguments.\"\n                )\n            tool_executable_path = args[0]\n            CachedToolPaths.cache_executable_path(tool, tool_executable_path)\n            return tool_executable_path\n\n    custom_paths_str = os.environ.get(\"OPENPYPE_OIIO_PATHS\") or \"\"\n    tool_executable_path = find_tool_in_custom_paths(\n        custom_paths_str.split(os.pathsep),\n        tool,\n        _oiio_executable_validation\n    )\n\n    if not tool_executable_path:\n        oiio_dir = get_vendor_bin_path(\"oiio\")\n        if platform.system().lower() == \"linux\":\n            oiio_dir = os.path.join(oiio_dir, \"bin\")\n        default_path = find_executable(os.path.join(oiio_dir, tool))\n        if default_path and _oiio_executable_validation(default_path):\n            tool_executable_path = default_path\n\n    # Look to PATH for the tool\n    if not tool_executable_path:\n        from_path = find_executable(tool)\n        if from_path and _oiio_executable_validation(from_path):\n            tool_executable_path = from_path\n\n    CachedToolPaths.cache_executable_path(tool, tool_executable_path)\n    return tool_executable_path\n\n\ndef get_oiio_tool_args(tool_name, *extra_args):\n    \"\"\"Arguments to launch OpenImageIO tool.\n\n    Args:\n        tool_name (str): Tool name 'oiiotool', 'maketx', etc.\n        *extra_args (str): Extra arguments to add to after tool arguments.\n\n    Returns:\n        list[str]: List of arguments.\n    \"\"\"\n\n    extra_args = list(extra_args)\n\n    if AYON_SERVER_ENABLED:\n        args = _get_ayon_oiio_tool_args(tool_name)\n        if args:\n            return args + extra_args\n\n    path = get_oiio_tools_path(tool_name)\n    if path:\n        return [path] + extra_args\n    raise ToolNotFoundError(\n        \"OIIO '{}' tool not found.\".format(tool_name)\n    )\n\n\ndef _ffmpeg_executable_validation(args):\n    \"\"\"Validate ffmpeg tool executable if can be executed.\n\n    Validation has 2 steps. First is using 'find_executable' to fill possible\n    missing extension or fill directory then launch executable and validate\n    that it can be executed. For that is used '-version' argument which is fast\n    and does not need any other inputs.\n\n    Any possible crash of missing libraries or invalid build should be caught.\n\n    Main reason is to validate if executable can be executed on OS just running\n    which can be issue ob linux machines.\n\n    Note:\n        It does not validate if the executable is really a ffmpeg tool.\n\n    Args:\n        args (Union[str, list[str]]): Arguments to launch tool or\n            path to tool executable.\n\n    Returns:\n        bool: Filepath is valid executable.\n    \"\"\"\n\n    if not args:\n        return False\n\n    if not isinstance(args, list):\n        filepath = find_executable(args)\n        if not filepath:\n            return False\n        args = [filepath]\n    return _check_args_returncode(args + [\"--help\"])\n\n\ndef _get_ayon_ffmpeg_tool_args(tool_name):\n    try:\n        # Use 'ayon-third-party' addon to get ffmpeg arguments\n        from ayon_third_party import get_ffmpeg_arguments\n\n    except Exception:\n        print(\"!!! Failed to import 'ayon_third_party' addon.\")\n        return None\n\n    try:\n        return get_ffmpeg_arguments(tool_name)\n    except Exception as exc:\n        print(\"!!! Failed to get FFmpeg args. Reason: {}\".format(exc))\n    return None\n\n\ndef get_ffmpeg_tool_path(tool=\"ffmpeg\"):\n    \"\"\"Path to vendorized FFmpeg executable.\n\n    Args:\n        tool (str): Tool name 'ffmpeg', 'ffprobe', etc.\n            Default is \"ffmpeg\".\n\n    Returns:\n        str: Full path to ffmpeg executable.\n    \"\"\"\n\n    if CachedToolPaths.is_tool_cached(tool):\n        return CachedToolPaths.get_executable_path(tool)\n\n    if AYON_SERVER_ENABLED:\n        args = _get_ayon_ffmpeg_tool_args(tool)\n        if args is not None:\n            if len(args) > 1:\n                raise ValueError(\n                    \"AYON ffmpeg arguments consist of multiple arguments.\"\n                )\n            tool_executable_path = args[0]\n            CachedToolPaths.cache_executable_path(tool, tool_executable_path)\n            return tool_executable_path\n\n    custom_paths_str = os.environ.get(\"OPENPYPE_FFMPEG_PATHS\") or \"\"\n    tool_executable_path = find_tool_in_custom_paths(\n        custom_paths_str.split(os.pathsep),\n        tool,\n        _ffmpeg_executable_validation\n    )\n\n    if not tool_executable_path:\n        ffmpeg_dir = get_vendor_bin_path(\"ffmpeg\")\n        if platform.system().lower() == \"windows\":\n            ffmpeg_dir = os.path.join(ffmpeg_dir, \"bin\")\n        tool_path = find_executable(os.path.join(ffmpeg_dir, tool))\n        if tool_path and _ffmpeg_executable_validation(tool_path):\n            tool_executable_path = tool_path\n\n    # Look to PATH for the tool\n    if not tool_executable_path:\n        from_path = find_executable(tool)\n        if from_path and _ffmpeg_executable_validation(from_path):\n            tool_executable_path = from_path\n\n    CachedToolPaths.cache_executable_path(tool, tool_executable_path)\n    return tool_executable_path\n\n\ndef get_ffmpeg_tool_args(tool_name, *extra_args):\n    \"\"\"Arguments to launch FFmpeg tool.\n\n    Args:\n        tool_name (str): Tool name 'ffmpeg', 'ffprobe', exc.\n        *extra_args (str): Extra arguments to add to after tool arguments.\n\n    Returns:\n        list[str]: List of arguments.\n    \"\"\"\n\n    extra_args = list(extra_args)\n\n    if AYON_SERVER_ENABLED:\n        args = _get_ayon_ffmpeg_tool_args(tool_name)\n        if args:\n            return args + extra_args\n\n    executable_path = get_ffmpeg_tool_path(tool_name)\n    if executable_path:\n        return [executable_path] + extra_args\n    raise ToolNotFoundError(\n        \"FFmpeg '{}' tool not found.\".format(tool_name)\n    )\n\n\ndef is_oiio_supported():\n    \"\"\"Checks if oiiotool is configured for this platform.\n\n    Returns:\n        bool: OIIO tool executable is available.\n    \"\"\"\n\n    try:\n        args = get_oiio_tool_args(\"oiiotool\")\n    except ToolNotFoundError:\n        args = None\n    if not args:\n        log.debug(\"OIIOTool is not configured or not present.\")\n        return False\n    return _oiio_executable_validation(args)\n"
  },
  {
    "path": "openpype/modules/README.md",
    "content": "# OpenPype modules/addons\nOpenPype modules should contain separated logic of specific kind of implementation, such as Ftrack connection and its usage code, Deadline farm rendering or may contain only special plugins. Addons work the same way currently, there is no difference between module and addon functionality.\n\n## Modules concept\n- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the module located\n- modules or addons should never be imported directly, even if you know possible full import path\n - it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts\n\n### TODOs\n- add module/addon manifest\n - definition of module (not 100% defined content e.g. minimum required OpenPype version etc.)\n - defining a folder as a content of a module or an addon\n\n## Base class `OpenPypeModule`\n- abstract class as base for each module\n- implementation should contain module's api without GUI parts\n- may implement `get_global_environments` method which should return dictionary of environments that are globally applicable and value is the same for whole studio if launched at any workstation (except os specific paths)\n- abstract parts:\n - `name` attribute - name of a module\n - `initialize` method - method for own initialization of a module (should not override `__init__`)\n - `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules\n- `__init__` should not be overridden and `initialize` should not do time consuming part but only prepare base data about module\n - also keep in mind that they may be initialized in headless mode\n- connection with other modules is made with help of interfaces\n- `cli` method - add cli commands specific for the module\n    - command line arguments are handled using `click` python module\n    - `cli` method should expect single argument which is click group on which can be called any group specific methods (e.g. `add_command` to add another click group as children see `ExampleAddon`)\n    - it is possible to add trigger cli commands using `./openpype_console module <module_name> <command> *args`\n\n## Addon class `OpenPypeAddOn`\n- inherits from `OpenPypeModule` but is enabled by default and doesn't have to implement `initialize` and `connect_with_modules` methods\n - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...)\n\n## How to add addons/modules\n- in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder\n- for openpype example addons use `{OPENPYPE_REPOS_ROOT}/openpype/modules/example_addons`\n\n## Addon/module settings\n- addons/modules may have defined custom settings definitions with default values\n- it is based on settings type `dynamic_schema` which has `name`\n - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults\n - they can't be added to any schema hierarchy\n - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`)\n - addons may define it's dynamic schema items\n- they can be defined with class which inherits from `BaseModuleSettingsDef`\n - it is recommended to use pre implemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values\n - check it's docstring and check for `example_addon` in example addons\n- settings definition returns schemas by dynamic schemas names\n\n# Interfaces\n- interface is class that has defined abstract methods to implement and may contain pre implemented helper methods\n- module that inherit from an interface must implement those abstract methods otherwise won't be initialized\n- it is easy to find which module object inherited from which interfaces with 100% chance they have implemented required methods\n- interfaces can be defined in `interfaces.py` inside module directory\n - the file can't use relative imports or import anything from other parts\n of module itself at the header of file\n - this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation\n\n## Base class `OpenPypeInterface`\n- has nothing implemented\n- has ABCMeta as metaclass\n- is defined to be able find out classes which inherit from this base to be\n able tell this is an Interface\n\n## Global interfaces\n- few interfaces are implemented for global usage\n\n### IPluginPaths\n- module wants to add directory path/s to avalon or publish plugins\n- module must implement `get_plugin_paths` which must return dictionary with possible keys `\"publish\"`, `\"load\"`, `\"create\"` or `\"actions\"`\n - each key may contain list or string with a path to directory with plugins\n\n### ITrayModule\n- module has more logic when used in a tray\n - it is possible that module can be used only in the tray\n- abstract methods\n - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules`\n - `tray_menu` - add actions to tray widget's menu that represent the module\n - `tray_start` - start of module's login in tray\n - module is initialized and connected with other modules\n - `tray_exit` - module's cleanup like stop and join threads etc.\n - order of calling is based on implementation this order is how it works with `TrayModulesManager`\n - it is recommended to import and use GUI implementation only in these methods\n- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init`\n - if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations\n\n### ITrayService\n- inherits from `ITrayModule` and implements `tray_menu` method for you\n - adds action to submenu \"Services\" in tray widget menu with icon and label\n- abstract attribute `label`\n - label shown in menu\n- interface has pre implemented methods to change icon color\n - `set_service_running` - green icon\n - `set_service_failed` - red icon\n - `set_service_idle` - orange icon\n - these states must be set by module itself `set_service_running` is default state on initialization\n\n### ITrayAction\n- inherits from `ITrayModule` and implements `tray_menu` method for you\n - adds action to tray widget menu with label\n- abstract attribute `label`\n - label shown in menu\n- abstract method `on_action_trigger`\n - what should happen when an action is triggered\n- NOTE: It is a good idea to implement logic in `on_action_trigger` to the api method and trigger that method on callbacks. This gives ability to trigger that method outside tray\n\n## Modules interfaces\n- modules may have defined their own interfaces to be able to recognize other modules that would want to use their features\n\n### Example:\n- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which other modules want to add paths to server/user event handlers\n - Clockify module use `IFtrackEventHandlerPaths` and returns paths to clockify ftrack synchronizers\n\n- Clockify inherits from more interfaces. It's class definition looks like:\n```\nclass ClockifyModule(\n OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize.\n ITrayModule, # Says has special implementation when used in tray.\n IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher).\n IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server.\n ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module.\n):\n```\n\n### ModulesManager\n- collects module classes and tries to initialize them\n- important attributes\n - `modules` - list of available attributes\n - `modules_by_id` - dictionary of modules mapped by their ids\n - `modules_by_name` - dictionary of modules mapped by their names\n - all these attributes contain all found modules even if are not enabled\n- helper methods\n - `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them\n - `collect_plugin_paths` collects plugin paths from all enabled modules\n - output is always dictionary with all keys and values as an list\n ```\n {\n \"publish\": [],\n \"create\": [],\n \"load\": [],\n \"actions\": [],\n \"inventory\": []\n }\n ```\n\n### TrayModulesManager\n- inherits from `ModulesManager`\n- has specific implementation for Pype Tray tool and handle `ITrayModule` methods\n"
  },
  {
    "path": "openpype/modules/__init__.py",
    "content": "# -*- coding: utf-8 -*-\nfrom . import click_wrap\nfrom .interfaces import (\n    ILaunchHookPaths,\n    IPluginPaths,\n    ITrayModule,\n    ITrayAction,\n    ITrayService,\n    ISettingsChangeListener,\n    IHostAddon,\n)\n\nfrom .base import (\n    AYONAddon,\n    OpenPypeModule,\n    OpenPypeAddOn,\n\n    load_modules,\n\n    ModulesManager,\n    TrayModulesManager,\n\n    BaseModuleSettingsDef,\n    ModuleSettingsDef,\n    JsonFilesSettingsDef,\n\n    get_module_settings_defs\n)\n\n\n__all__ = (\n    \"click_wrap\",\n\n    \"ILaunchHookPaths\",\n    \"IPluginPaths\",\n    \"ITrayModule\",\n    \"ITrayAction\",\n    \"ITrayService\",\n    \"ISettingsChangeListener\",\n    \"IHostAddon\",\n\n    \"AYONAddon\",\n    \"OpenPypeModule\",\n    \"OpenPypeAddOn\",\n\n    \"load_modules\",\n\n    \"ModulesManager\",\n    \"TrayModulesManager\",\n\n    \"BaseModuleSettingsDef\",\n    \"ModuleSettingsDef\",\n    \"JsonFilesSettingsDef\",\n\n    \"get_module_settings_defs\"\n)\n"
  },
  {
    "path": "openpype/modules/asset_reporter/__init__.py",
    "content": "from .module import (\n    AssetReporterAction\n)\n\n\n__all__ = (\n    \"AssetReporterAction\",\n)\n"
  },
  {
    "path": "openpype/modules/asset_reporter/module.py",
    "content": "import os.path\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.modules import OpenPypeModule, ITrayAction\nfrom openpype.lib import run_detached_process, get_openpype_execute_args\n\n\nclass AssetReporterAction(OpenPypeModule, ITrayAction):\n\n    label = \"Asset Usage Report\"\n    name = \"asset_reporter\"\n\n    def tray_init(self):\n        pass\n\n    def initialize(self, modules_settings):\n        self.enabled = not AYON_SERVER_ENABLED\n\n    def on_action_trigger(self):\n        args = get_openpype_execute_args()\n        args += [\"run\",\n                 os.path.join(\n                     os.path.dirname(__file__),\n                     \"window.py\")]\n\n        print(\" \".join(args))\n        run_detached_process(args)\n"
  },
  {
    "path": "openpype/modules/asset_reporter/window.py",
    "content": "\"\"\"Tool for generating asset usage report.\n\nThis tool is used to generate asset usage report for a project.\nIt is using links between published version to find out where\nthe asset is used.\n\n\"\"\"\n\nimport csv\nimport time\n\nimport appdirs\nimport qtawesome\nfrom pymongo.collection import Collection\nfrom qtpy import QtCore, QtWidgets\nfrom qtpy.QtGui import QClipboard, QColor\n\nfrom openpype import style\nfrom openpype.client import OpenPypeMongoConnection\nfrom openpype.lib import JSONSettingRegistry\nfrom openpype.tools.utils import PlaceholderLineEdit, get_openpype_qt_app\nfrom openpype.tools.utils.constants import PROJECT_NAME_ROLE\nfrom openpype.tools.utils.models import ProjectModel, ProjectSortFilterProxy\n\n\nclass AssetReporterRegistry(JSONSettingRegistry):\n    \"\"\"Class handling OpenPype general settings registry.\n\n    This is used to store last selected project.\n\n    Attributes:\n        vendor (str): Name used for path construction.\n        product (str): Additional name used for path construction.\n\n    \"\"\"\n\n    def __init__(self):\n        self.vendor = \"ynput\"\n        self.product = \"openpype\"\n        name = \"asset_usage_reporter\"\n        path = appdirs.user_data_dir(self.product, self.vendor)\n        super(AssetReporterRegistry, self).__init__(name, path)\n\n\nclass OverlayWidget(QtWidgets.QFrame):\n    \"\"\"Overlay widget for choosing project.\n\n    This code is taken from the Tray Publisher tool.\n    \"\"\"\n    project_selected = QtCore.Signal(str)\n\n    def __init__(self, publisher_window):\n        super(OverlayWidget, self).__init__(publisher_window)\n        self.setObjectName(\"OverlayFrame\")\n\n        middle_frame = QtWidgets.QFrame(self)\n        middle_frame.setObjectName(\"ChooseProjectFrame\")\n\n        content_widget = QtWidgets.QWidget(middle_frame)\n\n        header_label = QtWidgets.QLabel(\"Choose project\", content_widget)\n        header_label.setObjectName(\"ChooseProjectLabel\")\n        # Create project models and view\n        projects_model = ProjectModel()\n        projects_proxy = ProjectSortFilterProxy()\n        projects_proxy.setSourceModel(projects_model)\n        projects_proxy.setFilterKeyColumn(0)\n\n        projects_view = QtWidgets.QListView(content_widget)\n        projects_view.setObjectName(\"ChooseProjectView\")\n        projects_view.setModel(projects_proxy)\n        projects_view.setEditTriggers(\n            QtWidgets.QAbstractItemView.NoEditTriggers\n        )\n\n        confirm_btn = QtWidgets.QPushButton(\"Confirm\", content_widget)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", content_widget)\n        cancel_btn.setVisible(False)\n        btns_layout = QtWidgets.QHBoxLayout()\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(cancel_btn, 0)\n        btns_layout.addWidget(confirm_btn, 0)\n\n        txt_filter = PlaceholderLineEdit(content_widget)\n        txt_filter.setPlaceholderText(\"Quick filter projects..\")\n        txt_filter.setClearButtonEnabled(True)\n        txt_filter.addAction(qtawesome.icon(\"fa.filter\", color=\"gray\"),\n                             QtWidgets.QLineEdit.LeadingPosition)\n\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.setContentsMargins(0, 0, 0, 0)\n        content_layout.setSpacing(20)\n        content_layout.addWidget(header_label, 0)\n        content_layout.addWidget(txt_filter, 0)\n        content_layout.addWidget(projects_view, 1)\n        content_layout.addLayout(btns_layout, 0)\n\n        middle_layout = QtWidgets.QHBoxLayout(middle_frame)\n        middle_layout.setContentsMargins(30, 30, 10, 10)\n        middle_layout.addWidget(content_widget)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(10, 10, 10, 10)\n        main_layout.addStretch(1)\n        main_layout.addWidget(middle_frame, 2)\n        main_layout.addStretch(1)\n\n        projects_view.doubleClicked.connect(self._on_double_click)\n        confirm_btn.clicked.connect(self._on_confirm_click)\n        cancel_btn.clicked.connect(self._on_cancel_click)\n        txt_filter.textChanged.connect(self._on_text_changed)\n\n        self._projects_view = projects_view\n        self._projects_model = projects_model\n        self._projects_proxy = projects_proxy\n        self._cancel_btn = cancel_btn\n        self._confirm_btn = confirm_btn\n        self._txt_filter = txt_filter\n\n        self._publisher_window = publisher_window\n        self._project_name = None\n\n    def showEvent(self, event):\n        self._projects_model.refresh()\n        # Sort projects after refresh\n        self._projects_proxy.sort(0)\n\n        setting_registry = AssetReporterRegistry()\n        try:\n            project_name = str(setting_registry.get_item(\"project_name\"))\n        except ValueError:\n            project_name = None\n\n        if project_name:\n            index = None\n            src_index = self._projects_model.find_project(project_name)\n            if src_index is not None:\n                index = self._projects_proxy.mapFromSource(src_index)\n\n            if index is not None:\n                selection_model = self._projects_view.selectionModel()\n                selection_model.select(\n                    index,\n                    QtCore.QItemSelectionModel.SelectCurrent\n                )\n                self._projects_view.setCurrentIndex(index)\n\n        self._cancel_btn.setVisible(self._project_name is not None)\n        super(OverlayWidget, self).showEvent(event)\n\n    def _on_double_click(self):\n        self.set_selected_project()\n\n    def _on_confirm_click(self):\n        self.set_selected_project()\n\n    def _on_cancel_click(self):\n        self._set_project(self._project_name)\n\n    def _on_text_changed(self):\n        self._projects_proxy.setFilterRegularExpression(\n            self._txt_filter.text())\n\n    def set_selected_project(self):\n        index = self._projects_view.currentIndex()\n\n        if project_name := index.data(PROJECT_NAME_ROLE):\n            self._set_project(project_name)\n\n    def _set_project(self, project_name):\n        self._project_name = project_name\n        self.setVisible(False)\n        self.project_selected.emit(project_name)\n\n        setting_registry = AssetReporterRegistry()\n        setting_registry.set_item(\"project_name\", project_name)\n\n\nclass AssetReporterWindow(QtWidgets.QDialog):\n    default_width = 1000\n    default_height = 800\n    _content = None\n\n    def __init__(self, parent=None, controller=None, reset_on_show=None):\n        super(AssetReporterWindow, self).__init__(parent)\n\n        self._result = {}\n        self.setObjectName(\"AssetReporterWindow\")\n\n        self.setWindowTitle(\"Asset Usage Reporter\")\n\n        if parent is None:\n            on_top_flag = QtCore.Qt.WindowStaysOnTopHint\n        else:\n            on_top_flag = QtCore.Qt.Dialog\n\n        self.setWindowFlags(\n            QtCore.Qt.WindowTitleHint\n            | QtCore.Qt.WindowMaximizeButtonHint\n            | QtCore.Qt.WindowMinimizeButtonHint\n            | QtCore.Qt.WindowCloseButtonHint\n            | on_top_flag\n        )\n        self.table = QtWidgets.QTableWidget(self)\n        self.table.setColumnCount(3)\n        self.table.setColumnWidth(0, 400)\n        self.table.setColumnWidth(1, 300)\n        self.table.setHorizontalHeaderLabels([\"Subset\", \"Used in\", \"Version\"])\n\n        # self.text_area = QtWidgets.QTextEdit(self)\n        self.copy_button = QtWidgets.QPushButton('Copy to Clipboard', self)\n        self.save_button = QtWidgets.QPushButton('Save to CSV File', self)\n\n        self.copy_button.clicked.connect(self.copy_to_clipboard)\n        self.save_button.clicked.connect(self.save_to_file)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(self.table)\n        # layout.addWidget(self.text_area)\n        layout.addWidget(self.copy_button)\n        layout.addWidget(self.save_button)\n\n        self.resize(self.default_width, self.default_height)\n        self.setStyleSheet(style.load_stylesheet())\n\n        overlay_widget = OverlayWidget(self)\n        overlay_widget.project_selected.connect(self._on_project_select)\n        self._overlay_widget = overlay_widget\n\n    def _on_project_select(self, project_name: str):\n        \"\"\"Generate table when project is selected.\n\n        This will generate the table and fill it with data.\n        Source data are held in memory in `_result` attribute that\n        is used to transform them into clipboard or csv file.\n        \"\"\"\n        self._project_name = project_name\n        self.process()\n        if not self._result:\n            self.set_content(\"no result generated\")\n            return\n\n        rows = sum(len(value) for key, value in self._result.items())\n        self.table.setRowCount(rows)\n\n        row = 0\n        content = []\n        for key, value in self._result.items():\n            item = QtWidgets.QTableWidgetItem(key)\n            # this doesn't work as it is probably overriden by stylesheet?\n            # item.setBackground(QColor(32, 32, 32))\n            self.table.setItem(row, 0, item)\n            for source in value:\n                self.table.setItem(\n                    row, 1, QtWidgets.QTableWidgetItem(source[\"name\"]))\n                self.table.setItem(\n                    row, 2, QtWidgets.QTableWidgetItem(\n                        str(source[\"version\"])))\n                row += 1\n\n            # generate clipboard content\n            content.append(key)\n            content.extend(\n                f\"\\t{source['name']} (v{source['version']})\" for source in value  # noqa: E501\n            )\n        self.set_content(\"\\n\".join(content))\n\n    def copy_to_clipboard(self):\n        clipboard = QtWidgets.QApplication.clipboard()\n        clipboard.setText(self._content, QClipboard.Clipboard)\n\n    def save_to_file(self):\n        file_name, _ = QtWidgets.QFileDialog.getSaveFileName(self, 'Save File')\n        if file_name:\n            self._write_csv(file_name)\n\n    def set_content(self, content):\n        self._content = content\n\n    def get_content(self):\n        return self._content\n\n    def _resize_overlay(self):\n        self._overlay_widget.resize(\n            self.width(),\n            self.height()\n        )\n\n    def resizeEvent(self, event):\n        super(AssetReporterWindow, self).resizeEvent(event)\n        self._resize_overlay()\n\n    def _get_subset(self, version_id, project: Collection):\n        pipeline = [\n            {\n                \"$match\": {\n                    \"_id\": version_id\n                },\n            }, {\n                \"$lookup\": {\n                    \"from\": project.name,\n                    \"localField\": \"parent\",\n                    \"foreignField\": \"_id\",\n                    \"as\": \"parents\"\n                }\n            }\n        ]\n\n        result = project.aggregate(pipeline)\n        doc = next(result)\n        # print(doc)\n        return {\n            \"name\": f'{\"/\".join(doc[\"parents\"][0][\"data\"][\"parents\"])}/{doc[\"parents\"][0][\"name\"]}/{doc[\"name\"]}',  # noqa: E501\n            \"family\": doc[\"data\"].get(\"family\") or doc[\"data\"].get(\"families\")[0]  # noqa: E501\n        }\n\n    def process(self):\n        \"\"\"Generate asset usage report data.\n\n        This is the main method of the tool. It is using MongoDB\n        aggregation pipeline to find all published versions that\n        are used as input for other published versions. Then it\n        generates a map of assets and their usage.\n\n        \"\"\"\n        start = time.perf_counter()\n        project = self._project_name\n\n        # get all versions of published workfiles that has non-empty\n        # inputLinks and connect it with their respective documents\n        # using ID.\n        pipeline = [\n            {\n                \"$match\": {\n                    \"data.inputLinks\": {\n                        \"$exists\": True,\n                        \"$ne\": []\n                    },\n                    \"data.families\": {\"$in\": [\"workfile\"]}\n                }\n            }, {\n                \"$lookup\": {\n                    \"from\": project,\n                    \"localField\": \"data.inputLinks.id\",\n                    \"foreignField\": \"_id\",\n                    \"as\": \"linked_docs\"\n                }\n            }\n        ]\n\n        client = OpenPypeMongoConnection.get_mongo_client()\n        db = client[\"avalon\"]\n\n        result = db[project].aggregate(pipeline)\n\n        asset_map = []\n        # this is creating the map - for every workfile and its linked\n        # documents, create a dictionary with \"source\" and \"refs\" keys\n        # and resolve the subset name and version from the document\n        for doc in result:\n            source = {\n                \"source\": self._get_subset(doc[\"parent\"], db[project]),\n            }\n            source[\"source\"].update({\"version\": doc[\"name\"]})\n            refs = []\n            version = '<unknown>'\n            for linked in doc[\"linked_docs\"]:\n                try:\n                    version = f'v{linked[\"name\"]}'\n                except KeyError:\n                    if linked[\"type\"] == \"hero_version\":\n                        version = \"hero\"\n                finally:\n                    refs.append({\n                        \"subset\": self._get_subset(\n                            linked[\"parent\"], db[project]),\n                        \"version\": version\n                    })\n\n            source[\"refs\"] = refs\n            asset_map.append(source)\n\n        grouped = {}\n\n        # this will group the assets by subset name and version\n        for asset in asset_map:\n            for ref in asset[\"refs\"]:\n                key = f'{ref[\"subset\"][\"name\"]} ({ref[\"version\"]})'\n                if key in grouped:\n                    grouped[key].append(asset[\"source\"])\n                else:\n                    grouped[key] = [asset[\"source\"]]\n        self._result = grouped\n\n        end = time.perf_counter()\n\n        print(f\"Finished in {end - start:0.4f} seconds\", 2)\n\n    def _write_csv(self, file_name: str) -> None:\n        \"\"\"Write CSV file with results.\"\"\"\n        with open(file_name, \"w\", newline=\"\") as csvfile:\n            writer = csv.writer(csvfile, delimiter=\";\")\n            writer.writerow([\"Subset\", \"Used in\", \"Version\"])\n            for key, value in self._result.items():\n                writer.writerow([key, \"\", \"\"])\n                for source in value:\n                    writer.writerow([\"\", source[\"name\"], source[\"version\"]])\n\n\ndef main():\n    app_instance = get_openpype_qt_app()\n    window = AssetReporterWindow()\n    window.show()\n    app_instance.exec_()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "openpype/modules/avalon_apps/__init__.py",
    "content": "from .avalon_app import AvalonModule\n\n\n__all__ = (\n    \"AvalonModule\",\n)\n"
  },
  {
    "path": "openpype/modules/avalon_apps/avalon_app.py",
    "content": "import os\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.modules import OpenPypeModule, ITrayModule\n\n\nclass AvalonModule(OpenPypeModule, ITrayModule):\n    name = \"avalon\"\n\n    def initialize(self, modules_settings):\n        # This module is always enabled\n        self.enabled = True\n\n        avalon_settings = modules_settings[self.name]\n\n        thumbnail_root = os.environ.get(\"AVALON_THUMBNAIL_ROOT\")\n        if not thumbnail_root:\n            thumbnail_root = avalon_settings[\"AVALON_THUMBNAIL_ROOT\"]\n\n        # Mongo timeout\n        avalon_mongo_timeout = os.environ.get(\"AVALON_TIMEOUT\")\n        if not avalon_mongo_timeout:\n            avalon_mongo_timeout = avalon_settings[\"AVALON_TIMEOUT\"]\n\n        self.thumbnail_root = thumbnail_root\n        self.avalon_mongo_timeout = avalon_mongo_timeout\n\n        # Tray attributes\n        self._library_loader_imported = None\n        self._library_loader_window = None\n        self.rest_api_obj = None\n\n    def get_global_environments(self):\n        \"\"\"Avalon global environments for pype implementation.\"\"\"\n        return {\n            # TODO thumbnails root should be multiplafrom\n            # - thumbnails root\n            \"AVALON_THUMBNAIL_ROOT\": self.thumbnail_root,\n            # - mongo timeout in ms\n            \"AVALON_TIMEOUT\": str(self.avalon_mongo_timeout),\n        }\n\n    def tray_init(self):\n        # Add library tool\n        self._library_loader_imported = False\n        try:\n            from openpype.tools.libraryloader import LibraryLoaderWindow\n\n            self._library_loader_imported = True\n        except Exception:\n            self.log.warning(\n                \"Couldn't load Library loader tool for tray.\",\n                exc_info=True\n            )\n\n    # Definition of Tray menu\n    def tray_menu(self, tray_menu):\n        if not self._library_loader_imported:\n            return\n\n        from qtpy import QtWidgets\n        # Actions\n        action_library_loader = QtWidgets.QAction(\n            \"Loader\", tray_menu\n        )\n\n        action_library_loader.triggered.connect(self.show_library_loader)\n\n        tray_menu.addAction(action_library_loader)\n\n    def tray_start(self, *_a, **_kw):\n        return\n\n    def tray_exit(self, *_a, **_kw):\n        return\n\n    def show_library_loader(self):\n        if self._library_loader_window is None:\n            from openpype.pipeline import install_openpype_plugins\n            if AYON_SERVER_ENABLED:\n                self._init_ayon_loader()\n            else:\n                self._init_library_loader()\n\n            install_openpype_plugins()\n\n        self._library_loader_window.show()\n\n        # Raise and activate the window\n        # for MacOS\n        self._library_loader_window.raise_()\n        # for Windows\n        self._library_loader_window.activateWindow()\n\n    # Webserver module implementation\n    def webserver_initialization(self, server_manager):\n        \"\"\"Add routes for webserver.\"\"\"\n        if self.tray_initialized:\n            from .rest_api import AvalonRestApiResource\n            self.rest_api_obj = AvalonRestApiResource(self, server_manager)\n\n    def _init_library_loader(self):\n        from qtpy import QtCore\n        from openpype.tools.libraryloader import LibraryLoaderWindow\n\n        libraryloader = LibraryLoaderWindow(\n            show_projects=True,\n            show_libraries=True\n        )\n        # Remove always on top flag for tray\n        window_flags = libraryloader.windowFlags()\n        if window_flags | QtCore.Qt.WindowStaysOnTopHint:\n            window_flags ^= QtCore.Qt.WindowStaysOnTopHint\n            libraryloader.setWindowFlags(window_flags)\n        self._library_loader_window = libraryloader\n\n    def _init_ayon_loader(self):\n        from openpype.tools.ayon_loader.ui import LoaderWindow\n\n        libraryloader = LoaderWindow()\n\n        self._library_loader_window = libraryloader\n"
  },
  {
    "path": "openpype/modules/avalon_apps/rest_api.py",
    "content": "import json\nimport datetime\n\nfrom bson.objectid import ObjectId\n\nfrom aiohttp.web_response import Response\n\nfrom openpype.client import (\n    get_projects,\n    get_project,\n    get_assets,\n    get_asset_by_name,\n)\nfrom openpype_modules.webserver.base_routes import RestApiEndpoint\n\n\nclass _RestApiEndpoint(RestApiEndpoint):\n    def __init__(self, resource):\n        self.resource = resource\n        super(_RestApiEndpoint, self).__init__()\n\n\nclass AvalonProjectsEndpoint(_RestApiEndpoint):\n    async def get(self) -> Response:\n        output = [\n            project_doc\n            for project_doc in get_projects()\n        ]\n        return Response(\n            status=200,\n            body=self.resource.encode(output),\n            content_type=\"application/json\"\n        )\n\n\nclass AvalonProjectEndpoint(_RestApiEndpoint):\n    async def get(self, project_name) -> Response:\n        project_doc = get_project(project_name)\n        if project_doc:\n            return Response(\n                status=200,\n                body=self.resource.encode(project_doc),\n                content_type=\"application/json\"\n            )\n        return Response(\n            status=404,\n            reason=\"Project name {} not found\".format(project_name)\n        )\n\n\nclass AvalonAssetsEndpoint(_RestApiEndpoint):\n    async def get(self, project_name) -> Response:\n        asset_docs = list(get_assets(project_name))\n        return Response(\n            status=200,\n            body=self.resource.encode(asset_docs),\n            content_type=\"application/json\"\n        )\n\n\nclass AvalonAssetEndpoint(_RestApiEndpoint):\n    async def get(self, project_name, asset_name) -> Response:\n        asset_doc = get_asset_by_name(project_name, asset_name)\n        if asset_doc:\n            return Response(\n                status=200,\n                body=self.resource.encode(asset_doc),\n                content_type=\"application/json\"\n            )\n        return Response(\n            status=404,\n            reason=\"Asset name {} not found in project {}\".format(\n                asset_name, project_name\n            )\n        )\n\n\nclass AvalonRestApiResource:\n    def __init__(self, avalon_module, server_manager):\n        self.module = avalon_module\n        self.server_manager = server_manager\n\n        self.prefix = \"/avalon\"\n\n        self.endpoint_defs = (\n            (\n                \"GET\",\n                \"/projects\",\n                AvalonProjectsEndpoint(self)\n            ),\n            (\n                \"GET\",\n                \"/projects/{project_name}\",\n                AvalonProjectEndpoint(self)\n            ),\n            (\n                \"GET\",\n                \"/projects/{project_name}/assets\",\n                AvalonAssetsEndpoint(self)\n            ),\n            (\n                \"GET\",\n                \"/projects/{project_name}/assets/{asset_name}\",\n                AvalonAssetEndpoint(self)\n            )\n        )\n\n        self.register()\n\n    def register(self):\n        for methods, url, endpoint in self.endpoint_defs:\n            final_url = self.prefix + url\n            self.server_manager.add_route(\n                methods, final_url, endpoint.dispatch\n            )\n\n    @staticmethod\n    def json_dump_handler(value):\n        if isinstance(value, datetime.datetime):\n            return value.isoformat()\n        if isinstance(value, ObjectId):\n            return str(value)\n        raise TypeError(value)\n\n    @classmethod\n    def encode(cls, data):\n        return json.dumps(\n            data,\n            indent=4,\n            default=cls.json_dump_handler\n        ).encode(\"utf-8\")\n"
  },
  {
    "path": "openpype/modules/base.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Base class for AYON addons.\"\"\"\nimport copy\nimport os\nimport sys\nimport json\nimport time\nimport inspect\nimport logging\nimport platform\nimport threading\nimport collections\nimport traceback\n\nfrom uuid import uuid4\nfrom abc import ABCMeta, abstractmethod\n\nimport six\nimport appdirs\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import get_ayon_server_api_connection\nfrom openpype.settings import (\n    get_system_settings,\n    SYSTEM_SETTINGS_KEY,\n    PROJECT_SETTINGS_KEY,\n    SCHEMA_KEY_SYSTEM_SETTINGS,\n    SCHEMA_KEY_PROJECT_SETTINGS\n)\n\nfrom openpype.settings.lib import (\n    get_studio_system_settings_overrides,\n    load_json_file,\n)\nfrom openpype.settings.ayon_settings import (\n    is_dev_mode_enabled,\n    get_ayon_settings,\n)\n\nfrom openpype.lib import (\n    Logger,\n    import_filepath,\n    import_module_from_dirpath,\n)\n\nfrom .interfaces import (\n    OpenPypeInterface,\n    IPluginPaths,\n    IHostAddon,\n    ITrayModule,\n    ITrayService\n)\n\n# Files that will be always ignored on addons import\nIGNORED_FILENAMES = (\n    \"__pycache__\",\n)\n# Files ignored on addons import from \"./openpype/modules\"\nIGNORED_DEFAULT_FILENAMES = (\n    \"__init__.py\",\n    \"base.py\",\n    \"interfaces.py\",\n    \"example_addons\",\n    \"default_modules\",\n)\n# Addons that won't be loaded in AYON mode from \"./openpype/modules\"\n# - the same addons are ignored in \"./server_addon/create_ayon_addons.py\"\nIGNORED_FILENAMES_IN_AYON = {\n    \"ftrack\",\n    \"shotgrid\",\n    \"sync_server\",\n    \"slack\",\n    \"kitsu\",\n}\nIGNORED_HOSTS_IN_AYON = {\n    \"flame\",\n    \"harmony\",\n}\n\n\n# Inherit from `object` for Python 2 hosts\nclass _ModuleClass(object):\n    \"\"\"Fake module class for storing OpenPype modules.\n\n    Object of this class can be stored to `sys.modules` and used for storing\n    dynamically imported modules.\n    \"\"\"\n\n    def __init__(self, name):\n        # Call setattr on super class\n        super(_ModuleClass, self).__setattr__(\"name\", name)\n        super(_ModuleClass, self).__setattr__(\"__name__\", name)\n\n        # Where modules and interfaces are stored\n        super(_ModuleClass, self).__setattr__(\"__attributes__\", dict())\n        super(_ModuleClass, self).__setattr__(\"__defaults__\", set())\n\n        super(_ModuleClass, self).__setattr__(\"_log\", None)\n\n    def __getattr__(self, attr_name):\n        if attr_name not in self.__attributes__:\n            if attr_name in (\"__path__\", \"__file__\"):\n                return None\n            raise AttributeError(\"'{}' has not attribute '{}'\".format(\n                self.name, attr_name\n            ))\n        return self.__attributes__[attr_name]\n\n    def __iter__(self):\n        for module in self.values():\n            yield module\n\n    def __setattr__(self, attr_name, value):\n        if attr_name in self.__attributes__:\n            self.log.warning(\n                \"Duplicated name \\\"{}\\\" in {}. Overriding.\".format(\n                    attr_name, self.name\n                )\n            )\n        self.__attributes__[attr_name] = value\n\n    def __setitem__(self, key, value):\n        self.__setattr__(key, value)\n\n    def __getitem__(self, key):\n        return getattr(self, key)\n\n    @property\n    def log(self):\n        if self._log is None:\n            super(_ModuleClass, self).__setattr__(\n                \"_log\", Logger.get_logger(self.name)\n            )\n        return self._log\n\n    def get(self, key, default=None):\n        return self.__attributes__.get(key, default)\n\n    def keys(self):\n        return self.__attributes__.keys()\n\n    def values(self):\n        return self.__attributes__.values()\n\n    def items(self):\n        return self.__attributes__.items()\n\n\nclass _InterfacesClass(_ModuleClass):\n    \"\"\"Fake module class for storing OpenPype interfaces.\n\n    MissingInterface object is returned if interfaces does not exists.\n    - this is because interfaces must be available even if are missing\n        implementation\n    \"\"\"\n\n    def __getattr__(self, attr_name):\n        if attr_name not in self.__attributes__:\n            if attr_name in (\"__path__\", \"__file__\"):\n                return None\n\n            raise AttributeError((\n                \"cannot import name '{}' from 'openpype_interfaces'\"\n            ).format(attr_name))\n\n        if _LoadCache.interfaces_loaded and attr_name != \"log\":\n            stack = list(traceback.extract_stack())\n            stack.pop(-1)\n            self.log.warning((\n                \"Using deprecated import of \\\"{}\\\" from 'openpype_interfaces'.\"\n                \" Please switch to use import\"\n                \" from 'openpype.modules.interfaces'\"\n                \" (will be removed after 3.16.x).{}\"\n            ).format(attr_name, \"\".join(traceback.format_list(stack))))\n        return self.__attributes__[attr_name]\n\n\nclass _LoadCache:\n    interfaces_lock = threading.Lock()\n    modules_lock = threading.Lock()\n    interfaces_loaded = False\n    modules_loaded = False\n\n\ndef get_default_modules_dir():\n    \"\"\"Path to default OpenPype modules.\"\"\"\n\n    current_dir = os.path.dirname(os.path.abspath(__file__))\n\n    output = []\n    for folder_name in (\"default_modules\", ):\n        path = os.path.join(current_dir, folder_name)\n        if os.path.exists(path) and os.path.isdir(path):\n            output.append(path)\n\n    return output\n\n\ndef get_dynamic_modules_dirs():\n    \"\"\"Possible paths to OpenPype Addons of Modules.\n\n    Paths are loaded from studio settings under:\n        `modules -> addon_paths -> {platform name}`\n\n    Path may contain environment variable as a formatting string.\n\n    They are not validated or checked their existence.\n\n    Returns:\n        list: Paths loaded from studio overrides.\n    \"\"\"\n\n    output = []\n    if AYON_SERVER_ENABLED:\n        return output\n\n    value = get_studio_system_settings_overrides()\n    for key in (\"modules\", \"addon_paths\", platform.system().lower()):\n        if key not in value:\n            return output\n        value = value[key]\n\n    for path in value:\n        if not path:\n            continue\n\n        try:\n            path = path.format(**os.environ)\n        except Exception:\n            pass\n        output.append(path)\n    return output\n\n\ndef get_module_dirs():\n    \"\"\"List of paths where OpenPype modules can be found.\"\"\"\n    _dirpaths = []\n    _dirpaths.extend(get_default_modules_dir())\n    _dirpaths.extend(get_dynamic_modules_dirs())\n\n    dirpaths = []\n    for path in _dirpaths:\n        if not path:\n            continue\n        normalized = os.path.normpath(path)\n        if normalized not in dirpaths:\n            dirpaths.append(normalized)\n    return dirpaths\n\n\ndef load_interfaces(force=False):\n    \"\"\"Load interfaces from modules into `openpype_interfaces`.\n\n    Only classes which inherit from `OpenPypeInterface` are loaded and stored.\n\n    Args:\n        force(bool): Force to load interfaces even if are already loaded.\n            This won't update already loaded and used (cached) interfaces.\n    \"\"\"\n\n    if _LoadCache.interfaces_loaded and not force:\n        return\n\n    if not _LoadCache.interfaces_lock.locked():\n        with _LoadCache.interfaces_lock:\n            _load_interfaces()\n            _LoadCache.interfaces_loaded = True\n    else:\n        # If lock is locked wait until is finished\n        while _LoadCache.interfaces_lock.locked():\n            time.sleep(0.1)\n\n\ndef _load_interfaces():\n    # Key under which will be modules imported in `sys.modules`\n    modules_key = \"openpype_interfaces\"\n\n    sys.modules[modules_key] = openpype_interfaces = (\n        _InterfacesClass(modules_key)\n    )\n\n    from . import interfaces\n\n    for attr_name in dir(interfaces):\n        attr = getattr(interfaces, attr_name)\n        if (\n            not inspect.isclass(attr)\n            or attr is OpenPypeInterface\n            or not issubclass(attr, OpenPypeInterface)\n        ):\n            continue\n        setattr(openpype_interfaces, attr_name, attr)\n\n\ndef load_modules(force=False):\n    \"\"\"Load OpenPype modules as python modules.\n\n    Modules does not load only classes (like in Interfaces) because there must\n    be ability to use inner code of module and be able to import it from one\n    defined place.\n\n    With this it is possible to import module's content from predefined module.\n\n    Function makes sure that `load_interfaces` was triggered. Modules import\n    has specific order which can't be changed.\n\n    Args:\n        force(bool): Force to load modules even if are already loaded.\n            This won't update already loaded and used (cached) modules.\n    \"\"\"\n\n    if _LoadCache.modules_loaded and not force:\n        return\n\n    # First load interfaces\n    # - modules must not be imported before interfaces\n    load_interfaces(force)\n\n    if not _LoadCache.modules_lock.locked():\n        with _LoadCache.modules_lock:\n            _load_modules()\n            _LoadCache.modules_loaded = True\n    else:\n        # If lock is locked wait until is finished\n        while _LoadCache.modules_lock.locked():\n            time.sleep(0.1)\n\n\ndef _get_ayon_bundle_data():\n    con = get_ayon_server_api_connection()\n    bundles = con.get_bundles()[\"bundles\"]\n\n    bundle_name = os.getenv(\"AYON_BUNDLE_NAME\")\n\n    return next(\n        (\n            bundle\n            for bundle in bundles\n            if bundle[\"name\"] == bundle_name\n        ),\n        None\n    )\n\n\ndef _get_ayon_addons_information(bundle_info):\n    \"\"\"Receive information about addons to use from server.\n\n    Todos:\n        Actually ask server for the information.\n        Allow project name as optional argument to be able to query information\n            about used addons for specific project.\n\n    Returns:\n        List[Dict[str, Any]]: List of addon information to use.\n    \"\"\"\n\n    output = []\n    bundle_addons = bundle_info[\"addons\"]\n    con = get_ayon_server_api_connection()\n    addons = con.get_addons_info()[\"addons\"]\n    for addon in addons:\n        name = addon[\"name\"]\n        versions = addon.get(\"versions\")\n        addon_version = bundle_addons.get(name)\n        if addon_version is None or not versions:\n            continue\n        version = versions.get(addon_version)\n        if version:\n            version = copy.deepcopy(version)\n            version[\"name\"] = name\n            version[\"version\"] = addon_version\n            output.append(version)\n    return output\n\n\ndef _load_ayon_addons(openpype_modules, modules_key, log):\n    \"\"\"Load AYON addons based on information from server.\n\n    This function should not trigger downloading of any addons but only use\n    what is already available on the machine (at least in first stages of\n    development).\n\n    Args:\n        openpype_modules (_ModuleClass): Module object where modules are\n            stored.\n        log (logging.Logger): Logger object.\n\n    Returns:\n        List[str]: List of v3 addons to skip to load because v4 alternative is\n            imported.\n    \"\"\"\n\n    v3_addons_to_skip = []\n\n    bundle_info = _get_ayon_bundle_data()\n    addons_info = _get_ayon_addons_information(bundle_info)\n    if not addons_info:\n        return v3_addons_to_skip\n\n    addons_dir = os.environ.get(\"AYON_ADDONS_DIR\")\n    if not addons_dir:\n        addons_dir = os.path.join(\n            appdirs.user_data_dir(\"AYON\", \"Ynput\"),\n            \"addons\"\n        )\n\n    dev_mode_enabled = is_dev_mode_enabled()\n    dev_addons_info = {}\n    if dev_mode_enabled:\n        # Get dev addons info only when dev mode is enabled\n        dev_addons_info = bundle_info.get(\"addonDevelopment\", dev_addons_info)\n\n    addons_dir_exists = os.path.exists(addons_dir)\n    if not addons_dir_exists:\n        log.warning(\"Addons directory does not exists. Path \\\"{}\\\"\".format(\n            addons_dir\n        ))\n\n    for addon_info in addons_info:\n        addon_name = addon_info[\"name\"]\n        addon_version = addon_info[\"version\"]\n\n        # OpenPype addon does not have any addon object\n        if addon_name == \"openpype\":\n            continue\n\n        dev_addon_info = dev_addons_info.get(addon_name, {})\n        use_dev_path = dev_addon_info.get(\"enabled\", False)\n\n        addon_dir = None\n        if use_dev_path:\n            addon_dir = dev_addon_info[\"path\"]\n            if not addon_dir or not os.path.exists(addon_dir):\n                log.warning((\n                    \"Dev addon {} {} path does not exists. Path \\\"{}\\\"\"\n                ).format(addon_name, addon_version, addon_dir))\n                continue\n\n        elif addons_dir_exists:\n            folder_name = \"{}_{}\".format(addon_name, addon_version)\n            addon_dir = os.path.join(addons_dir, folder_name)\n            if not os.path.exists(addon_dir):\n                log.debug((\n                    \"No localized client code found for addon {} {}.\"\n                ).format(addon_name, addon_version))\n                continue\n\n        if not addon_dir:\n            continue\n\n        sys.path.insert(0, addon_dir)\n        imported_modules = []\n        for name in os.listdir(addon_dir):\n            # Ignore of files is implemented to be able to run code from code\n            #   where usually is more files than just the addon\n            # Ignore start and setup scripts\n            if name in (\"setup.py\", \"start.py\", \"__pycache__\"):\n                continue\n\n            path = os.path.join(addon_dir, name)\n            basename, ext = os.path.splitext(name)\n            # Ignore folders/files with dot in name\n            #   - dot names cannot be imported in Python\n            if \".\" in basename:\n                continue\n            is_dir = os.path.isdir(path)\n            is_py_file = ext.lower() == \".py\"\n            if not is_py_file and not is_dir:\n                continue\n\n            try:\n                mod = __import__(basename, fromlist=(\"\",))\n                for attr_name in dir(mod):\n                    attr = getattr(mod, attr_name)\n                    if (\n                        inspect.isclass(attr)\n                        and issubclass(attr, AYONAddon)\n                    ):\n                        imported_modules.append(mod)\n                        break\n\n            except BaseException:\n                log.warning(\n                    \"Failed to import \\\"{}\\\"\".format(basename),\n                    exc_info=True\n                )\n\n        if not imported_modules:\n            log.warning(\"Addon {} {} has no content to import\".format(\n                addon_name, addon_version\n            ))\n            continue\n\n        if len(imported_modules) > 1:\n            log.warning((\n                \"Skipping addon '{}'.\"\n                \" Multiple modules were found ({}) in dir {}.\"\n            ).format(\n                addon_name,\n                \", \".join([m.__name__ for m in imported_modules]),\n                addon_dir,\n            ))\n            continue\n\n        mod = imported_modules[0]\n        addon_alias = getattr(mod, \"V3_ALIAS\", None)\n        if not addon_alias:\n            addon_alias = addon_name\n        v3_addons_to_skip.append(addon_alias)\n        new_import_str = \"{}.{}\".format(modules_key, addon_alias)\n\n        sys.modules[new_import_str] = mod\n        setattr(openpype_modules, addon_alias, mod)\n\n    return v3_addons_to_skip\n\n\ndef _load_modules():\n    # Key under which will be modules imported in `sys.modules`\n    modules_key = \"openpype_modules\"\n\n    # Change `sys.modules`\n    sys.modules[modules_key] = openpype_modules = _ModuleClass(modules_key)\n\n    log = Logger.get_logger(\"ModulesLoader\")\n\n    ignore_addon_names = []\n    if AYON_SERVER_ENABLED:\n        ignore_addon_names = _load_ayon_addons(\n            openpype_modules, modules_key, log\n        )\n\n    # Look for OpenPype modules in paths defined with `get_module_dirs`\n    #   - dynamically imported OpenPype modules and addons\n    module_dirs = get_module_dirs()\n\n    # Add current directory at first place\n    #   - has small differences in import logic\n    current_dir = os.path.abspath(os.path.dirname(__file__))\n    hosts_dir = os.path.join(os.path.dirname(current_dir), \"hosts\")\n    module_dirs.insert(0, hosts_dir)\n    module_dirs.insert(0, current_dir)\n\n    addons_dir = os.path.join(os.path.dirname(current_dir), \"addons\")\n    if os.path.exists(addons_dir):\n        module_dirs.append(addons_dir)\n\n    ignored_host_names = set(IGNORED_HOSTS_IN_AYON)\n    ignored_current_dir_filenames = set(IGNORED_DEFAULT_FILENAMES)\n    if AYON_SERVER_ENABLED:\n        ignored_current_dir_filenames |= IGNORED_FILENAMES_IN_AYON\n\n    processed_paths = set()\n    for dirpath in frozenset(module_dirs):\n        # Skip already processed paths\n        if dirpath in processed_paths:\n            continue\n        processed_paths.add(dirpath)\n\n        if not os.path.exists(dirpath):\n            log.warning((\n                \"Could not find path when loading OpenPype modules \\\"{}\\\"\"\n            ).format(dirpath))\n            continue\n\n        is_in_current_dir = dirpath == current_dir\n        is_in_host_dir = dirpath == hosts_dir\n\n        for filename in os.listdir(dirpath):\n            # Ignore filenames\n            if filename in IGNORED_FILENAMES:\n                continue\n\n            if (\n                is_in_current_dir\n                and filename in ignored_current_dir_filenames\n            ):\n                continue\n\n            if (\n                is_in_host_dir\n                and filename in ignored_host_names\n            ):\n                continue\n\n            fullpath = os.path.join(dirpath, filename)\n            basename, ext = os.path.splitext(filename)\n\n            if basename in ignore_addon_names:\n                continue\n\n            # Validations\n            if os.path.isdir(fullpath):\n                # Check existence of init file\n                init_path = os.path.join(fullpath, \"__init__.py\")\n                if not os.path.exists(init_path):\n                    log.debug((\n                        \"Module directory does not contain __init__.py\"\n                        \" file {}\"\n                    ).format(fullpath))\n                    continue\n\n            elif ext not in (\".py\", ):\n                continue\n\n            # TODO add more logic how to define if folder is module or not\n            # - check manifest and content of manifest\n            try:\n                # Don't import dynamically current directory modules\n                if is_in_current_dir:\n                    import_str = \"openpype.modules.{}\".format(basename)\n                    new_import_str = \"{}.{}\".format(modules_key, basename)\n                    default_module = __import__(import_str, fromlist=(\"\", ))\n                    sys.modules[new_import_str] = default_module\n                    setattr(openpype_modules, basename, default_module)\n\n                elif is_in_host_dir:\n                    import_str = \"openpype.hosts.{}\".format(basename)\n                    new_import_str = \"{}.{}\".format(modules_key, basename)\n                    # Until all hosts are converted to be able use them as\n                    #   modules is this error check needed\n                    try:\n                        default_module = __import__(\n                            import_str, fromlist=(\"\", )\n                        )\n                        sys.modules[new_import_str] = default_module\n                        setattr(openpype_modules, basename, default_module)\n\n                    except Exception:\n                        log.warning(\n                            \"Failed to import host folder {}\".format(basename),\n                            exc_info=True\n                        )\n\n                elif os.path.isdir(fullpath):\n                    import_module_from_dirpath(dirpath, filename, modules_key)\n\n                else:\n                    module = import_filepath(fullpath)\n                    setattr(openpype_modules, basename, module)\n\n            except Exception:\n                if is_in_current_dir:\n                    msg = \"Failed to import default module '{}'.\".format(\n                        basename\n                    )\n                else:\n                    msg = \"Failed to import module '{}'.\".format(fullpath)\n                log.error(msg, exc_info=True)\n\n\n@six.add_metaclass(ABCMeta)\nclass AYONAddon(object):\n    \"\"\"Base class of AYON addon.\n\n    Attributes:\n        id (UUID): Addon object id.\n        enabled (bool): Is addon enabled.\n        name (str): Addon name.\n\n    Args:\n        manager (ModulesManager): Manager object who discovered addon.\n        settings (dict[str, Any]): AYON settings.\n    \"\"\"\n\n    enabled = True\n    _id = None\n\n    def __init__(self, manager, settings):\n        self.manager = manager\n\n        self.log = Logger.get_logger(self.name)\n\n        self.initialize(settings)\n\n    @property\n    def id(self):\n        \"\"\"Random id of addon object.\n\n        Returns:\n            str: Object id.\n        \"\"\"\n\n        if self._id is None:\n            self._id = uuid4()\n        return self._id\n\n    @property\n    @abstractmethod\n    def name(self):\n        \"\"\"Addon name.\n\n        Returns:\n            str: Addon name.\n        \"\"\"\n\n        pass\n\n    def initialize(self, settings):\n        \"\"\"Initialization of module attributes.\n\n        It is not recommended to override __init__ that's why specific method\n        was implemented.\n\n        Args:\n            settings (dict[str, Any]): Settings.\n        \"\"\"\n\n        pass\n\n    def connect_with_modules(self, enabled_addons):\n        \"\"\"Connect with other enabled addons.\n\n        Args:\n            enabled_addons (list[AYONAddon]): Addons that are enabled.\n        \"\"\"\n\n        pass\n\n    def get_global_environments(self):\n        \"\"\"Get global environments values of module.\n\n        Environment variables that can be get only from system settings.\n\n        Returns:\n            dict[str, str]: Environment variables.\n        \"\"\"\n\n        return {}\n\n    def modify_application_launch_arguments(self, application, env):\n        \"\"\"Give option to modify launch environments before application launch.\n\n        Implementation is optional. To change environments modify passed\n        dictionary of environments.\n\n        Args:\n            application (Application): Application that is launched.\n            env (dict[str, str]): Current environment variables.\n        \"\"\"\n\n        pass\n\n    def on_host_install(self, host, host_name, project_name):\n        \"\"\"Host was installed which gives option to handle in-host logic.\n\n        It is a good option to register in-host event callbacks which are\n        specific for the module. The module is kept in memory for rest of\n        the process.\n\n        Arguments may change in future. E.g. 'host_name' should be possible\n        to receive from 'host' object.\n\n        Args:\n            host (Union[ModuleType, HostBase]): Access to installed/registered\n                host object.\n            host_name (str): Name of host.\n            project_name (str): Project name which is main part of host\n                context.\n        \"\"\"\n\n        pass\n\n    def cli(self, module_click_group):\n        \"\"\"Add commands to click group.\n\n        The best practise is to create click group for whole module which is\n        used to separate commands.\n\n        Example:\n            class MyPlugin(AYONAddon):\n                ...\n                def cli(self, module_click_group):\n                    module_click_group.add_command(cli_main)\n\n\n            @click.group(<module name>, help=\"<Any help shown in cmd>\")\n            def cli_main():\n                pass\n\n            @cli_main.command()\n            def mycommand():\n                print(\"my_command\")\n\n        Args:\n            module_click_group (click.Group): Group to which can be added\n                commands.\n        \"\"\"\n\n        pass\n\n\nclass OpenPypeModule(AYONAddon):\n    \"\"\"Base class of OpenPype module.\n\n    Instead of 'AYONAddon' are passed in module settings.\n\n    Args:\n        manager (ModulesManager): Manager object who discovered addon.\n        settings (dict[str, Any]): OpenPype settings.\n    \"\"\"\n\n    # Disable by default\n    enabled = False\n\n\nclass OpenPypeAddOn(OpenPypeModule):\n    # Enable Addon by default\n    enabled = True\n\n\nclass ModulesManager:\n    \"\"\"Manager of Pype modules helps to load and prepare them to work.\n\n    Args:\n        system_settings (Optional[dict[str, Any]]): OpenPype system settings.\n        ayon_settings (Optional[dict[str, Any]]): AYON studio settings.\n    \"\"\"\n\n    # Helper attributes for report\n    _report_total_key = \"Total\"\n    _system_settings = None\n    _ayon_settings = None\n\n    def __init__(self, system_settings=None, ayon_settings=None):\n        self.log = logging.getLogger(self.__class__.__name__)\n\n        self._system_settings = system_settings\n        self._ayon_settings = ayon_settings\n\n        self.modules = []\n        self.modules_by_id = {}\n        self.modules_by_name = {}\n        # For report of time consumption\n        self._report = {}\n\n        self.initialize_modules()\n        self.connect_modules()\n\n    def __getitem__(self, module_name):\n        return self.modules_by_name[module_name]\n\n    def get(self, module_name, default=None):\n        \"\"\"Access module by name.\n\n        Args:\n            module_name (str): Name of module which should be returned.\n            default (Any): Default output if module is not available.\n\n        Returns:\n            Union[AYONAddon, None]: Module found by name or None.\n        \"\"\"\n\n        return self.modules_by_name.get(module_name, default)\n\n    def get_enabled_module(self, module_name, default=None):\n        \"\"\"Fast access to enabled module.\n\n        If module is available but is not enabled default value is returned.\n\n        Args:\n            module_name (str): Name of module which should be returned.\n            default (Any): Default output if module is not available or is\n                not enabled.\n\n        Returns:\n            Union[AYONAddon, None]: Enabled module found by name or None.\n        \"\"\"\n\n        module = self.get(module_name)\n        if module is not None and module.enabled:\n            return module\n        return default\n\n    def initialize_modules(self):\n        \"\"\"Import and initialize modules.\"\"\"\n        # Make sure modules are loaded\n        load_modules()\n\n        import openpype_modules\n\n        self.log.debug(\"*** {} initialization.\".format(\n            \"AYON addons\"\n            if AYON_SERVER_ENABLED\n            else \"OpenPype modules\"\n        ))\n        # Prepare settings for modules\n        system_settings = self._system_settings\n        if system_settings is None:\n            system_settings = get_system_settings()\n\n        ayon_settings = self._ayon_settings\n        if AYON_SERVER_ENABLED and ayon_settings is None:\n            ayon_settings = get_ayon_settings()\n\n        modules_settings = system_settings[\"modules\"]\n\n        report = {}\n        time_start = time.time()\n        prev_start_time = time_start\n\n        module_classes = []\n        for module in openpype_modules:\n            # Go through globals in `pype.modules`\n            for name in dir(module):\n                modules_item = getattr(module, name, None)\n                # Filter globals that are not classes which inherit from\n                #   AYONAddon\n                if (\n                    not inspect.isclass(modules_item)\n                    or modules_item is AYONAddon\n                    or modules_item is OpenPypeModule\n                    or modules_item is OpenPypeAddOn\n                    or not issubclass(modules_item, AYONAddon)\n                ):\n                    continue\n\n                # Check if class is abstract (Developing purpose)\n                if inspect.isabstract(modules_item):\n                    # Find abstract attributes by convention on `abc` module\n                    not_implemented = []\n                    for attr_name in dir(modules_item):\n                        attr = getattr(modules_item, attr_name, None)\n                        abs_method = getattr(\n                            attr, \"__isabstractmethod__\", None\n                        )\n                        if attr and abs_method:\n                            not_implemented.append(attr_name)\n\n                    # Log missing implementations\n                    self.log.warning((\n                        \"Skipping abstract Class: {}.\"\n                        \" Missing implementations: {}\"\n                    ).format(name, \", \".join(not_implemented)))\n                    continue\n                module_classes.append(modules_item)\n\n        for modules_item in module_classes:\n            is_openpype_module = issubclass(modules_item, OpenPypeModule)\n            settings = (\n                modules_settings if is_openpype_module else ayon_settings\n            )\n            name = modules_item.__name__\n            try:\n                # Try initialize module\n                module = modules_item(self, settings)\n                # Store initialized object\n                self.modules.append(module)\n                self.modules_by_id[module.id] = module\n                self.modules_by_name[module.name] = module\n                enabled_str = \"X\"\n                if not module.enabled:\n                    enabled_str = \" \"\n                self.log.debug(\"[{}] {}\".format(enabled_str, name))\n\n                now = time.time()\n                report[module.__class__.__name__] = now - prev_start_time\n                prev_start_time = now\n\n            except Exception:\n                self.log.warning(\n                    \"Initialization of module {} failed.\".format(name),\n                    exc_info=True\n                )\n\n        if self._report is not None:\n            report[self._report_total_key] = time.time() - time_start\n            self._report[\"Initialization\"] = report\n\n    def connect_modules(self):\n        \"\"\"Trigger connection with other enabled modules.\n\n        Modules should handle their interfaces in `connect_with_modules`.\n        \"\"\"\n        report = {}\n        time_start = time.time()\n        prev_start_time = time_start\n        enabled_modules = self.get_enabled_modules()\n        self.log.debug(\"Has {} enabled modules.\".format(len(enabled_modules)))\n        for module in enabled_modules:\n            try:\n                module.connect_with_modules(enabled_modules)\n            except Exception:\n                self.log.error(\n                    \"BUG: Module failed on connection with other modules.\",\n                    exc_info=True\n                )\n\n            now = time.time()\n            report[module.__class__.__name__] = now - prev_start_time\n            prev_start_time = now\n\n        if self._report is not None:\n            report[self._report_total_key] = time.time() - time_start\n            self._report[\"Connect modules\"] = report\n\n    def get_enabled_modules(self):\n        \"\"\"Enabled modules initialized by the manager.\n\n        Returns:\n            list[AYONAddon]: Initialized and enabled modules.\n        \"\"\"\n\n        return [\n            module\n            for module in self.modules\n            if module.enabled\n        ]\n\n    def collect_global_environments(self):\n        \"\"\"Helper to collect global environment variabled from modules.\n\n        Returns:\n            dict: Global environment variables from enabled modules.\n\n        Raises:\n            AssertionError: Global environment variables must be unique for\n                all modules.\n        \"\"\"\n        module_envs = {}\n        for module in self.get_enabled_modules():\n            # Collect global module's global environments\n            _envs = module.get_global_environments()\n            for key, value in _envs.items():\n                if key in module_envs:\n                    # TODO better error message\n                    raise AssertionError(\n                        \"Duplicated environment key {}\".format(key)\n                    )\n                module_envs[key] = value\n        return module_envs\n\n    def collect_plugin_paths(self):\n        \"\"\"Helper to collect all plugins from modules inherited IPluginPaths.\n\n        Unknown keys are logged out.\n\n        Returns:\n            dict: Output is dictionary with keys \"publish\", \"create\", \"load\",\n                \"actions\" and \"inventory\" each containing list of paths.\n        \"\"\"\n        # Output structure\n        output = {\n            \"publish\": [],\n            \"create\": [],\n            \"load\": [],\n            \"actions\": [],\n            \"inventory\": []\n        }\n        unknown_keys_by_module = {}\n        for module in self.get_enabled_modules():\n            # Skip module that do not inherit from `IPluginPaths`\n            if not isinstance(module, IPluginPaths):\n                continue\n            plugin_paths = module.get_plugin_paths()\n            for key, value in plugin_paths.items():\n                # Filter unknown keys\n                if key not in output:\n                    if module.name not in unknown_keys_by_module:\n                        unknown_keys_by_module[module.name] = []\n                    unknown_keys_by_module[module.name].append(key)\n                    continue\n\n                # Skip if value is empty\n                if not value:\n                    continue\n\n                # Convert to list if value is not list\n                if not isinstance(value, (list, tuple, set)):\n                    value = [value]\n                output[key].extend(value)\n\n        # Report unknown keys (Developing purposes)\n        if unknown_keys_by_module:\n            expected_keys = \", \".join([\n                \"\\\"{}\\\"\".format(key) for key in output.keys()\n            ])\n            msg_template = \"Module: \\\"{}\\\" - got key {}\"\n            msg_items = []\n            for module_name, keys in unknown_keys_by_module.items():\n                joined_keys = \", \".join([\n                    \"\\\"{}\\\"\".format(key) for key in keys\n                ])\n                msg_items.append(msg_template.format(module_name, joined_keys))\n            self.log.warning((\n                \"Expected keys from `get_plugin_paths` are {}. {}\"\n            ).format(expected_keys, \" | \".join(msg_items)))\n        return output\n\n    def _collect_plugin_paths(self, method_name, *args, **kwargs):\n        output = []\n        for module in self.get_enabled_modules():\n            # Skip module that do not inherit from `IPluginPaths`\n            if not isinstance(module, IPluginPaths):\n                continue\n\n            method = getattr(module, method_name)\n            try:\n                paths = method(*args, **kwargs)\n            except Exception:\n                self.log.warning(\n                    (\n                        \"Failed to get plugin paths from module\"\n                        \" '{}' using '{}'.\"\n                    ).format(module.__class__.__name__, method_name),\n                    exc_info=True\n                )\n                continue\n\n            if paths:\n                # Convert to list if value is not list\n                if not isinstance(paths, (list, tuple, set)):\n                    paths = [paths]\n                output.extend(paths)\n        return output\n\n    def collect_create_plugin_paths(self, host_name):\n        \"\"\"Helper to collect creator plugin paths from modules.\n\n        Args:\n            host_name (str): For which host are creators meant.\n\n        Returns:\n            list: List of creator plugin paths.\n        \"\"\"\n\n        return self._collect_plugin_paths(\n            \"get_create_plugin_paths\",\n            host_name\n        )\n\n    collect_creator_plugin_paths = collect_create_plugin_paths\n\n    def collect_load_plugin_paths(self, host_name):\n        \"\"\"Helper to collect load plugin paths from modules.\n\n        Args:\n            host_name (str): For which host are load plugins meant.\n\n        Returns:\n            list: List of load plugin paths.\n        \"\"\"\n\n        return self._collect_plugin_paths(\n            \"get_load_plugin_paths\",\n            host_name\n        )\n\n    def collect_publish_plugin_paths(self, host_name):\n        \"\"\"Helper to collect load plugin paths from modules.\n\n        Args:\n            host_name (str): For which host are load plugins meant.\n\n        Returns:\n            list: List of pyblish plugin paths.\n        \"\"\"\n\n        return self._collect_plugin_paths(\n            \"get_publish_plugin_paths\",\n            host_name\n        )\n\n    def collect_inventory_action_paths(self, host_name):\n        \"\"\"Helper to collect load plugin paths from modules.\n\n        Args:\n            host_name (str): For which host are load plugins meant.\n\n        Returns:\n            list: List of pyblish plugin paths.\n        \"\"\"\n\n        return self._collect_plugin_paths(\n            \"get_inventory_action_paths\",\n            host_name\n        )\n\n    def get_host_module(self, host_name):\n        \"\"\"Find host module by host name.\n\n        Args:\n            host_name (str): Host name for which is found host module.\n\n        Returns:\n            AYONAddon: Found host module by name.\n            None: There was not found module inheriting IHostAddon which has\n                host name set to passed 'host_name'.\n        \"\"\"\n\n        for module in self.get_enabled_modules():\n            if (\n                isinstance(module, IHostAddon)\n                and module.host_name == host_name\n            ):\n                return module\n        return None\n\n    def get_host_names(self):\n        \"\"\"List of available host names based on host modules.\n\n        Returns:\n            Iterable[str]: All available host names based on enabled modules\n                inheriting 'IHostAddon'.\n        \"\"\"\n\n        return {\n            module.host_name\n            for module in self.get_enabled_modules()\n            if isinstance(module, IHostAddon)\n        }\n\n    def print_report(self):\n        \"\"\"Print out report of time spent on modules initialization parts.\n\n        Reporting is not automated must be implemented for each initialization\n        part separatelly. Reports must be stored to `_report` attribute.\n        Print is skipped if `_report` is empty.\n\n        Attribute `_report` is dictionary where key is \"label\" describing\n        the processed part and value is dictionary where key is module's\n        class name and value is time delta of it's processing.\n\n        It is good idea to add total time delta on processed part under key\n        which is defined in attribute `_report_total_key`. By default has value\n        `\"Total\"` but use the attribute please.\n\n        ```javascript\n        {\n            \"Initialization\": {\n                \"FtrackModule\": 0.003,\n                ...\n                \"Total\": 1.003,\n            },\n            ...\n        }\n        ```\n        \"\"\"\n        if not self._report:\n            return\n\n        available_col_names = set()\n        for module_names in self._report.values():\n            available_col_names |= set(module_names.keys())\n\n        # Prepare ordered dictionary for columns\n        cols = collections.OrderedDict()\n        # Add module names to first columnt\n        cols[\"Module name\"] = list(sorted(\n            module.__class__.__name__\n            for module in self.modules\n            if module.__class__.__name__ in available_col_names\n        ))\n        # Add total key (as last module)\n        cols[\"Module name\"].append(self._report_total_key)\n\n        # Add columns from report\n        for label in self._report.keys():\n            cols[label] = []\n\n        total_module_times = {}\n        for module_name in cols[\"Module name\"]:\n            total_module_times[module_name] = 0\n\n        for label, reported in self._report.items():\n            for module_name in cols[\"Module name\"]:\n                col_time = reported.get(module_name)\n                if col_time is None:\n                    cols[label].append(\"N/A\")\n                    continue\n                cols[label].append(\"{:.3f}\".format(col_time))\n                total_module_times[module_name] += col_time\n\n        # Add to also total column that should sum the row\n        cols[self._report_total_key] = []\n        for module_name in cols[\"Module name\"]:\n            cols[self._report_total_key].append(\n                \"{:.3f}\".format(total_module_times[module_name])\n            )\n\n        # Prepare column widths and total row count\n        # - column width is by\n        col_widths = {}\n        total_rows = None\n        for key, values in cols.items():\n            if total_rows is None:\n                total_rows = 1 + len(values)\n            max_width = len(key)\n            for value in values:\n                value_length = len(value)\n                if value_length > max_width:\n                    max_width = value_length\n            col_widths[key] = max_width\n\n        rows = []\n        for _idx in range(total_rows):\n            rows.append([])\n\n        for key, values in cols.items():\n            width = col_widths[key]\n            idx = 0\n            rows[idx].append(key.ljust(width))\n            for value in values:\n                idx += 1\n                rows[idx].append(value.ljust(width))\n\n        filler_parts = []\n        for width in col_widths.values():\n            filler_parts.append(width * \"-\")\n        filler = \"+\".join(filler_parts)\n\n        formatted_rows = [filler]\n        last_row_idx = len(rows) - 1\n        for idx, row in enumerate(rows):\n            # Add filler before last row\n            if idx == last_row_idx:\n                formatted_rows.append(filler)\n\n            formatted_rows.append(\"|\".join(row))\n\n            # Add filler after first row\n            if idx == 0:\n                formatted_rows.append(filler)\n\n        # Join rows with newline char and add new line at the end\n        output = \"\\n\".join(formatted_rows) + \"\\n\"\n        print(output)\n\n\nclass TrayModulesManager(ModulesManager):\n    # Define order of modules in menu\n    modules_menu_order = (\n        \"user\",\n        \"ftrack\",\n        \"kitsu\",\n        \"launcher_tool\",\n        \"avalon\",\n        \"clockify\",\n        \"standalonepublish_tool\",\n        \"traypublish_tool\",\n        \"log_viewer\",\n        \"local_settings\",\n        \"settings\"\n    )\n\n    def __init__(self):\n        self.log = Logger.get_logger(self.__class__.__name__)\n\n        self.modules = []\n        self.modules_by_id = {}\n        self.modules_by_name = {}\n        self._report = {}\n\n        self.tray_manager = None\n\n        self.doubleclick_callbacks = {}\n        self.doubleclick_callback = None\n\n    def add_doubleclick_callback(self, module, callback):\n        \"\"\"Register doubleclick callbacks on tray icon.\n\n        Currently there is no way how to determine which is launched. Name of\n        callback can be defined with `doubleclick_callback` attribute.\n\n        Missing feature how to define default callback.\n\n        Args:\n            addon (AYONAddon): Addon object.\n            callback (FunctionType): Function callback.\n        \"\"\"\n        callback_name = \"_\".join([module.name, callback.__name__])\n        if callback_name not in self.doubleclick_callbacks:\n            self.doubleclick_callbacks[callback_name] = callback\n            if self.doubleclick_callback is None:\n                self.doubleclick_callback = callback_name\n            return\n\n        self.log.warning((\n            \"Callback with name \\\"{}\\\" is already registered.\"\n        ).format(callback_name))\n\n    def initialize(self, tray_manager, tray_menu):\n        self.tray_manager = tray_manager\n        self.initialize_modules()\n        self.tray_init()\n        self.connect_modules()\n        self.tray_menu(tray_menu)\n\n    def get_enabled_tray_modules(self):\n        \"\"\"Enabled tray modules.\n\n        Returns:\n            list[AYONAddon]: Enabled addons that inherit from tray interface.\n        \"\"\"\n\n        return [\n            module\n            for module in self.modules\n            if module.enabled and isinstance(module, ITrayModule)\n        ]\n\n    def restart_tray(self):\n        if self.tray_manager:\n            self.tray_manager.restart()\n\n    def tray_init(self):\n        report = {}\n        time_start = time.time()\n        prev_start_time = time_start\n        for module in self.get_enabled_tray_modules():\n            try:\n                module._tray_manager = self.tray_manager\n                module.tray_init()\n                module.tray_initialized = True\n            except Exception:\n                self.log.warning(\n                    \"Module \\\"{}\\\" crashed on `tray_init`.\".format(\n                        module.name\n                    ),\n                    exc_info=True\n                )\n\n            now = time.time()\n            report[module.__class__.__name__] = now - prev_start_time\n            prev_start_time = now\n\n        if self._report is not None:\n            report[self._report_total_key] = time.time() - time_start\n            self._report[\"Tray init\"] = report\n\n    def tray_menu(self, tray_menu):\n        ordered_modules = []\n        enabled_by_name = {\n            module.name: module\n            for module in self.get_enabled_tray_modules()\n        }\n\n        for name in self.modules_menu_order:\n            module_by_name = enabled_by_name.pop(name, None)\n            if module_by_name:\n                ordered_modules.append(module_by_name)\n        ordered_modules.extend(enabled_by_name.values())\n\n        report = {}\n        time_start = time.time()\n        prev_start_time = time_start\n        for module in ordered_modules:\n            if not module.tray_initialized:\n                continue\n\n            try:\n                module.tray_menu(tray_menu)\n            except Exception:\n                # Unset initialized mark\n                module.tray_initialized = False\n                self.log.warning(\n                    \"Module \\\"{}\\\" crashed on `tray_menu`.\".format(\n                        module.name\n                    ),\n                    exc_info=True\n                )\n            now = time.time()\n            report[module.__class__.__name__] = now - prev_start_time\n            prev_start_time = now\n\n        if self._report is not None:\n            report[self._report_total_key] = time.time() - time_start\n            self._report[\"Tray menu\"] = report\n\n    def start_modules(self):\n        report = {}\n        time_start = time.time()\n        prev_start_time = time_start\n        for module in self.get_enabled_tray_modules():\n            if not module.tray_initialized:\n                if isinstance(module, ITrayService):\n                    module.set_service_failed_icon()\n                continue\n\n            try:\n                module.tray_start()\n            except Exception:\n                self.log.warning(\n                    \"Module \\\"{}\\\" crashed on `tray_start`.\".format(\n                        module.name\n                    ),\n                    exc_info=True\n                )\n            now = time.time()\n            report[module.__class__.__name__] = now - prev_start_time\n            prev_start_time = now\n\n        if self._report is not None:\n            report[self._report_total_key] = time.time() - time_start\n            self._report[\"Modules start\"] = report\n\n    def on_exit(self):\n        for module in self.get_enabled_tray_modules():\n            if module.tray_initialized:\n                try:\n                    module.tray_exit()\n                except Exception:\n                    self.log.warning(\n                        \"Module \\\"{}\\\" crashed on `tray_exit`.\".format(\n                            module.name\n                        ),\n                        exc_info=True\n                    )\n\n\ndef get_module_settings_defs():\n    \"\"\"Check loaded addons/modules for existence of their settings definition.\n\n    Check if OpenPype addon/module as python module has class that inherit\n    from `ModuleSettingsDef` in python module variables (imported\n    in `__init__py`).\n\n    Returns:\n        list: All valid and not abstract settings definitions from imported\n            openpype addons and modules.\n    \"\"\"\n    # Make sure modules are loaded\n    load_modules()\n\n    import openpype_modules\n\n    settings_defs = []\n\n    log = Logger.get_logger(\"ModuleSettingsLoad\")\n\n    for raw_module in openpype_modules:\n        for attr_name in dir(raw_module):\n            attr = getattr(raw_module, attr_name)\n            if (\n                not inspect.isclass(attr)\n                or attr is ModuleSettingsDef\n                or not issubclass(attr, ModuleSettingsDef)\n            ):\n                continue\n\n            if inspect.isabstract(attr):\n                # Find missing implementations by convention on `abc` module\n                not_implemented = []\n                for attr_name in dir(attr):\n                    attr = getattr(attr, attr_name, None)\n                    abs_method = getattr(\n                        attr, \"__isabstractmethod__\", None\n                    )\n                    if attr and abs_method:\n                        not_implemented.append(attr_name)\n\n                # Log missing implementations\n                log.warning((\n                    \"Skipping abstract Class: {} in module {}.\"\n                    \" Missing implementations: {}\"\n                ).format(\n                    attr_name, raw_module.__name__, \", \".join(not_implemented)\n                ))\n                continue\n\n            settings_defs.append(attr)\n\n    return settings_defs\n\n\n@six.add_metaclass(ABCMeta)\nclass BaseModuleSettingsDef:\n    \"\"\"Definition of settings for OpenPype module or AddOn.\"\"\"\n    _id = None\n\n    @property\n    def id(self):\n        \"\"\"ID created on initialization.\n\n        ID should be per created object. Helps to store objects.\n        \"\"\"\n        if self._id is None:\n            self._id = uuid4()\n        return self._id\n\n    @abstractmethod\n    def get_settings_schemas(self, schema_type):\n        \"\"\"Setting schemas for passed schema type.\n\n        These are main schemas by dynamic schema keys. If they're using\n        sub schemas or templates they should be loaded with\n        `get_dynamic_schemas`.\n\n        Returns:\n            dict: Schema by `dynamic_schema` keys.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_dynamic_schemas(self, schema_type):\n        \"\"\"Settings schemas and templates that can be used anywhere.\n\n        It is recommended to add prefix specific for addon/module to keys\n        (e.g. \"my_addon/real_schema_name\").\n\n        Returns:\n            dict: Schemas and templates by their keys.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_defaults(self, top_key):\n        \"\"\"Default values for passed top key.\n\n        Top keys are (currently) \"system_settings\" or \"project_settings\".\n\n        Should return exactly what was passed with `save_defaults`.\n\n        Returns:\n            dict: Default values by path to first key in OpenPype defaults.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def save_defaults(self, top_key, data):\n        \"\"\"Save default values for passed top key.\n\n        Top keys are (currently) \"system_settings\" or \"project_settings\".\n\n        Passed data are by path to first key defined in main schemas.\n        \"\"\"\n        pass\n\n\nclass ModuleSettingsDef(BaseModuleSettingsDef):\n    \"\"\"Settings definition with separated system and procect settings parts.\n\n    Reduce conditions that must be checked and adds predefined methods for\n    each case.\n    \"\"\"\n    def get_defaults(self, top_key):\n        \"\"\"Split method into 2 methods by top key.\"\"\"\n        if top_key == SYSTEM_SETTINGS_KEY:\n            return self.get_default_system_settings() or {}\n        elif top_key == PROJECT_SETTINGS_KEY:\n            return self.get_default_project_settings() or {}\n        return {}\n\n    def save_defaults(self, top_key, data):\n        \"\"\"Split method into 2 methods by top key.\"\"\"\n        if top_key == SYSTEM_SETTINGS_KEY:\n            self.save_system_defaults(data)\n        elif top_key == PROJECT_SETTINGS_KEY:\n            self.save_project_defaults(data)\n\n    def get_settings_schemas(self, schema_type):\n        \"\"\"Split method into 2 methods by schema type.\"\"\"\n        if schema_type == SCHEMA_KEY_SYSTEM_SETTINGS:\n            return self.get_system_settings_schemas() or {}\n        elif schema_type == SCHEMA_KEY_PROJECT_SETTINGS:\n            return self.get_project_settings_schemas() or {}\n        return {}\n\n    def get_dynamic_schemas(self, schema_type):\n        \"\"\"Split method into 2 methods by schema type.\"\"\"\n        if schema_type == SCHEMA_KEY_SYSTEM_SETTINGS:\n            return self.get_system_dynamic_schemas() or {}\n        elif schema_type == SCHEMA_KEY_PROJECT_SETTINGS:\n            return self.get_project_dynamic_schemas() or {}\n        return {}\n\n    @abstractmethod\n    def get_system_settings_schemas(self):\n        \"\"\"Schemas and templates usable in system settings schemas.\n\n        Returns:\n            dict: Schemas and templates by it's names. Names must be unique\n                across whole OpenPype.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_project_settings_schemas(self):\n        \"\"\"Schemas and templates usable in project settings schemas.\n\n        Returns:\n            dict: Schemas and templates by it's names. Names must be unique\n                across whole OpenPype.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_system_dynamic_schemas(self):\n        \"\"\"System schemas by dynamic schema name.\n\n        If dynamic schema name is not available in then schema will not used.\n\n        Returns:\n            dict: Schemas or list of schemas by dynamic schema name.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_project_dynamic_schemas(self):\n        \"\"\"Project schemas by dynamic schema name.\n\n        If dynamic schema name is not available in then schema will not used.\n\n        Returns:\n            dict: Schemas or list of schemas by dynamic schema name.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_default_system_settings(self):\n        \"\"\"Default system settings values.\n\n        Returns:\n            dict: Default values by path to first key.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_default_project_settings(self):\n        \"\"\"Default project settings values.\n\n        Returns:\n            dict: Default values by path to first key.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def save_system_defaults(self, data):\n        \"\"\"Save default system settings values.\n\n        Passed data are by path to first key defined in main schemas.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def save_project_defaults(self, data):\n        \"\"\"Save default project settings values.\n\n        Passed data are by path to first key defined in main schemas.\n        \"\"\"\n        pass\n\n\nclass JsonFilesSettingsDef(ModuleSettingsDef):\n    \"\"\"Preimplemented settings definition using json files and file structure.\n\n    Expected file structure:\n    ┕ root\n      │\n      │ # Default values\n      ┝ defaults\n      │ ┝ system_settings.json\n      │ ┕ project_settings.json\n      │\n      │ # Schemas for `dynamic_template` type\n      ┝ dynamic_schemas\n      │ ┝ system_dynamic_schemas.json\n      │ ┕ project_dynamic_schemas.json\n      │\n      │ # Schemas that can be used anywhere (enhancement for `dynamic_schemas`)\n      ┕ schemas\n        ┝ system_schemas\n        │ ┝ <system schema.json> # Any schema or template files\n        │ ┕ ...\n        ┕ project_schemas\n          ┝ <system schema.json> # Any schema or template files\n          ┕ ...\n\n    Schemas can be loaded with prefix to avoid duplicated schema/template names\n    across all OpenPype addons/modules. Prefix can be defined with class\n    attribute `schema_prefix`.\n\n    Only think which must be implemented in `get_settings_root_path` which\n    should return directory path to `root` (in structure graph above).\n    \"\"\"\n    # Possible way how to define `schemas` prefix\n    schema_prefix = \"\"\n\n    @abstractmethod\n    def get_settings_root_path(self):\n        \"\"\"Directory path where settings and it's schemas are located.\"\"\"\n        pass\n\n    def __init__(self):\n        settings_root_dir = self.get_settings_root_path()\n        defaults_dir = os.path.join(\n            settings_root_dir, \"defaults\"\n        )\n        dynamic_schemas_dir = os.path.join(\n            settings_root_dir, \"dynamic_schemas\"\n        )\n        schemas_dir = os.path.join(\n            settings_root_dir, \"schemas\"\n        )\n\n        self.system_defaults_filepath = os.path.join(\n            defaults_dir, \"system_settings.json\"\n        )\n        self.project_defaults_filepath = os.path.join(\n            defaults_dir, \"project_settings.json\"\n        )\n\n        self.system_dynamic_schemas_filepath = os.path.join(\n            dynamic_schemas_dir, \"system_dynamic_schemas.json\"\n        )\n        self.project_dynamic_schemas_filepath = os.path.join(\n            dynamic_schemas_dir, \"project_dynamic_schemas.json\"\n        )\n\n        self.system_schemas_dir = os.path.join(\n            schemas_dir, \"system_schemas\"\n        )\n        self.project_schemas_dir = os.path.join(\n            schemas_dir, \"project_schemas\"\n        )\n\n    def _load_json_file_data(self, path):\n        if os.path.exists(path):\n            return load_json_file(path)\n        return {}\n\n    def get_default_system_settings(self):\n        \"\"\"Default system settings values.\n\n        Returns:\n            dict: Default values by path to first key.\n        \"\"\"\n        return self._load_json_file_data(self.system_defaults_filepath)\n\n    def get_default_project_settings(self):\n        \"\"\"Default project settings values.\n\n        Returns:\n            dict: Default values by path to first key.\n        \"\"\"\n        return self._load_json_file_data(self.project_defaults_filepath)\n\n    def _save_data_to_filepath(self, path, data):\n        dirpath = os.path.dirname(path)\n        if not os.path.exists(dirpath):\n            os.makedirs(dirpath)\n\n        with open(path, \"w\") as file_stream:\n            json.dump(data, file_stream, indent=4)\n\n    def save_system_defaults(self, data):\n        \"\"\"Save default system settings values.\n\n        Passed data are by path to first key defined in main schemas.\n        \"\"\"\n        self._save_data_to_filepath(self.system_defaults_filepath, data)\n\n    def save_project_defaults(self, data):\n        \"\"\"Save default project settings values.\n\n        Passed data are by path to first key defined in main schemas.\n        \"\"\"\n        self._save_data_to_filepath(self.project_defaults_filepath, data)\n\n    def get_system_dynamic_schemas(self):\n        \"\"\"System schemas by dynamic schema name.\n\n        If dynamic schema name is not available in then schema will not used.\n\n        Returns:\n            dict: Schemas or list of schemas by dynamic schema name.\n        \"\"\"\n        return self._load_json_file_data(self.system_dynamic_schemas_filepath)\n\n    def get_project_dynamic_schemas(self):\n        \"\"\"Project schemas by dynamic schema name.\n\n        If dynamic schema name is not available in then schema will not used.\n\n        Returns:\n            dict: Schemas or list of schemas by dynamic schema name.\n        \"\"\"\n        return self._load_json_file_data(self.project_dynamic_schemas_filepath)\n\n    def _load_files_from_path(self, path):\n        output = {}\n        if not path or not os.path.exists(path):\n            return output\n\n        if os.path.isfile(path):\n            filename = os.path.basename(path)\n            basename, ext = os.path.splitext(filename)\n            if ext == \".json\":\n                if self.schema_prefix:\n                    key = \"{}/{}\".format(self.schema_prefix, basename)\n                else:\n                    key = basename\n                output[key] = self._load_json_file_data(path)\n            return output\n\n        path = os.path.normpath(path)\n        for root, _, files in os.walk(path, topdown=False):\n            for filename in files:\n                basename, ext = os.path.splitext(filename)\n                if ext != \".json\":\n                    continue\n\n                json_path = os.path.join(root, filename)\n                store_key = os.path.join(\n                    root.replace(path, \"\"), basename\n                ).replace(\"\\\\\", \"/\")\n                if self.schema_prefix:\n                    store_key = \"{}/{}\".format(self.schema_prefix, store_key)\n                output[store_key] = self._load_json_file_data(json_path)\n\n        return output\n\n    def get_system_settings_schemas(self):\n        \"\"\"Schemas and templates usable in system settings schemas.\n\n        Returns:\n            dict: Schemas and templates by it's names. Names must be unique\n                across whole OpenPype.\n        \"\"\"\n        return self._load_files_from_path(self.system_schemas_dir)\n\n    def get_project_settings_schemas(self):\n        \"\"\"Schemas and templates usable in project settings schemas.\n\n        Returns:\n            dict: Schemas and templates by it's names. Names must be unique\n                across whole OpenPype.\n        \"\"\"\n        return self._load_files_from_path(self.project_schemas_dir)\n"
  },
  {
    "path": "openpype/modules/click_wrap.py",
    "content": "\"\"\"Simplified wrapper for 'click' python module.\n\nModule 'click' is used as main cli handler in AYON/OpenPype. Addons can\nregister their own subcommands with options. This wrapper allows to define\ncommands and options as with 'click', but without any dependency.\n\nWhy not to use 'click' directly? Version of 'click' used in AYON/OpenPype\nis not compatible with 'click' version used in some DCCs (e.g. Houdini 20+).\nAnd updating 'click' would break other DCCs.\n\nHow to use it? If you already have cli commands defined in addon, just replace\n'click' with 'click_wrap' and it should work and modify your addon's cli\nmethod to convert 'click_wrap' object to 'click' object.\n\nBefore\n```python\nimport click\nfrom openpype.modules import OpenPypeModule\n\n\nclass ExampleAddon(OpenPypeModule):\n    name = \"example\"\n\n    def cli(self, click_group):\n        click_group.add_command(cli_main)\n\n\n@click.group(ExampleAddon.name, help=\"Example addon\")\ndef cli_main():\n    pass\n\n\n@cli_main.command(help=\"Example command\")\n@click.option(\"--arg1\", help=\"Example argument 1\", default=\"default1\")\n@click.option(\"--arg2\", help=\"Example argument 2\", is_flag=True)\ndef mycommand(arg1, arg2):\n    print(arg1, arg2)\n```\n\nNow\n```\nfrom openpype import click_wrap\nfrom openpype.modules import OpenPypeModule\n\n\nclass ExampleAddon(OpenPypeModule):\n    name = \"example\"\n\n    def cli(self, click_group):\n        click_group.add_command(cli_main.to_click_obj())\n\n\n@click_wrap.group(ExampleAddon.name, help=\"Example addon\")\ndef cli_main():\n    pass\n\n\n@cli_main.command(help=\"Example command\")\n@click_wrap.option(\"--arg1\", help=\"Example argument 1\", default=\"default1\")\n@click_wrap.option(\"--arg2\", help=\"Example argument 2\", is_flag=True)\ndef mycommand(arg1, arg2):\n    print(arg1, arg2)\n```\n\n\nAdded small enhancements:\n- most of the methods can be used as chained calls\n- functions/methods 'command' and 'group' can be used in a way that\n    first argument is callback function and the rest are arguments\n    for click\n\nExample:\n    ```python\n    from openpype import click_wrap\n    from openpype.modules import OpenPypeModule\n\n\n    class ExampleAddon(OpenPypeModule):\n        name = \"example\"\n\n        def cli(self, click_group):\n            # Define main command (name 'example')\n            main = click_wrap.group(\n                self._cli_main, name=self.name, help=\"Example addon\"\n            )\n            # Add subcommand (name 'mycommand')\n            (\n                main.command(\n                    self._cli_command, name=\"mycommand\", help=\"Example command\"\n                )\n                .option(\n                    \"--arg1\", help=\"Example argument 1\", default=\"default1\"\n                )\n                .option(\n                    \"--arg2\", help=\"Example argument 2\", is_flag=True,\n                )\n            )\n            # Convert main command to click object and add it to parent group\n            click_group.add_command(main.to_click_obj())\n\n        def _cli_main(self):\n            pass\n\n        def _cli_command(self, arg1, arg2):\n            print(arg1, arg2)\n    ```\n\n    ```shell\n    openpype_console addon example mycommand --arg1 value1 --arg2\n    ```\n\"\"\"\n\nimport collections\n\nFUNC_ATTR_NAME = \"__ayon_cli_options__\"\n\n\nclass Command(object):\n    def __init__(self, func, *args, **kwargs):\n        # Command function\n        self._func = func\n        # Command definition arguments\n        self._args = args\n        # Command definition kwargs\n        self._kwargs = kwargs\n        # Both 'options' and 'arguments' are stored to the same variable\n        #   - keep order of options and arguments\n        self._options = getattr(func, FUNC_ATTR_NAME, [])\n\n    def to_click_obj(self):\n        \"\"\"Converts this object to click object.\n\n        Returns:\n            click.Command: Click command object.\n        \"\"\"\n        return convert_to_click(self)\n\n    # --- Methods for 'convert_to_click' function ---\n    def get_args(self):\n        \"\"\"\n        Returns:\n            tuple: Command definition arguments.\n        \"\"\"\n        return self._args\n\n    def get_kwargs(self):\n        \"\"\"\n        Returns:\n            dict[str, Any]: Command definition kwargs.\n        \"\"\"\n        return self._kwargs\n\n    def get_func(self):\n        \"\"\"\n        Returns:\n            Function: Function to invoke on command trigger.\n        \"\"\"\n        return self._func\n\n    def iter_options(self):\n        \"\"\"\n        Yields:\n            tuple[str, tuple, dict]: Option type name with args and kwargs.\n        \"\"\"\n        for item in self._options:\n            yield item\n    # -----------------------------------------------\n\n    def add_option(self, *args, **kwargs):\n        return self.add_option_by_type(\"option\", *args, **kwargs)\n\n    def add_argument(self, *args, **kwargs):\n        return self.add_option_by_type(\"argument\", *args, **kwargs)\n\n    option = add_option\n    argument = add_argument\n\n    def add_option_by_type(self, option_name, *args, **kwargs):\n        self._options.append((option_name, args, kwargs))\n        return self\n\n\nclass Group(Command):\n    def __init__(self, func, *args, **kwargs):\n        super(Group, self).__init__(func, *args, **kwargs)\n        # Store sub-groupd and sub-commands to the same variable\n        self._commands = []\n\n    # --- Methods for 'convert_to_click' function ---\n    def iter_commands(self):\n        for command in self._commands:\n            yield command\n    # -----------------------------------------------\n\n    def add_command(self, command):\n        \"\"\"Add prepared command object as child.\n\n        Args:\n            command (Command): Prepared command object.\n        \"\"\"\n        if command not in self._commands:\n            self._commands.append(command)\n\n    def add_group(self, group):\n        \"\"\"Add prepared group object as child.\n\n        Args:\n            group (Group): Prepared group object.\n        \"\"\"\n        if group not in self._commands:\n            self._commands.append(group)\n\n    def command(self, *args, **kwargs):\n        \"\"\"Add child command.\n\n        Returns:\n            Union[Command, Function]: New command object, or wrapper function.\n        \"\"\"\n        return self._add_new(Command, *args, **kwargs)\n\n    def group(self, *args, **kwargs):\n        \"\"\"Add child group.\n\n        Returns:\n            Union[Group, Function]: New group object, or wrapper function.\n        \"\"\"\n        return self._add_new(Group, *args, **kwargs)\n\n    def _add_new(self, target_cls, *args, **kwargs):\n        func = None\n        if args and callable(args[0]):\n            args = list(args)\n            func = args.pop(0)\n            args = tuple(args)\n\n        def decorator(_func):\n            out = target_cls(_func, *args, **kwargs)\n            self._commands.append(out)\n            return out\n\n        if func is not None:\n            return decorator(func)\n        return decorator\n\n\ndef convert_to_click(obj_to_convert):\n    \"\"\"Convert wrapped object to click object.\n\n    Args:\n        obj_to_convert (Command): Object to convert to click object.\n\n    Returns:\n        click.Command: Click command object.\n    \"\"\"\n    import click\n\n    commands_queue = collections.deque()\n    commands_queue.append((obj_to_convert, None))\n    top_obj = None\n    while commands_queue:\n        item = commands_queue.popleft()\n        command_obj, parent_obj = item\n        if not isinstance(command_obj, Command):\n            raise TypeError(\n                \"Invalid type '{}' expected 'Command'\".format(\n                    type(command_obj)\n                )\n            )\n\n        if isinstance(command_obj, Group):\n            click_obj = (\n                click.group(\n                    *command_obj.get_args(),\n                    **command_obj.get_kwargs()\n                )(command_obj.get_func())\n            )\n\n        else:\n            click_obj = (\n                click.command(\n                    *command_obj.get_args(),\n                    **command_obj.get_kwargs()\n                )(command_obj.get_func())\n            )\n\n        for item in command_obj.iter_options():\n            option_name, args, kwargs = item\n            if option_name == \"option\":\n                click.option(*args, **kwargs)(click_obj)\n            elif option_name == \"argument\":\n                click.argument(*args, **kwargs)(click_obj)\n            else:\n                raise ValueError(\n                    \"Invalid option name '{}'\".format(option_name)\n                )\n\n        if top_obj is None:\n            top_obj = click_obj\n\n        if parent_obj is not None:\n            parent_obj.add_command(click_obj)\n\n        if isinstance(command_obj, Group):\n            for command in command_obj.iter_commands():\n                commands_queue.append((command, click_obj))\n\n    return top_obj\n\n\ndef group(*args, **kwargs):\n    func = None\n    if args and callable(args[0]):\n        args = list(args)\n        func = args.pop(0)\n        args = tuple(args)\n\n    def decorator(_func):\n        return Group(_func, *args, **kwargs)\n\n    if func is not None:\n        return decorator(func)\n    return decorator\n\n\ndef command(*args, **kwargs):\n    func = None\n    if args and callable(args[0]):\n        args = list(args)\n        func = args.pop(0)\n        args = tuple(args)\n\n    def decorator(_func):\n        return Command(_func, *args, **kwargs)\n\n    if func is not None:\n        return decorator(func)\n    return decorator\n\n\ndef argument(*args, **kwargs):\n    def decorator(func):\n        return _add_option_to_func(\n            func, \"argument\", *args, **kwargs\n        )\n    return decorator\n\n\ndef option(*args, **kwargs):\n    def decorator(func):\n        return _add_option_to_func(\n            func, \"option\", *args, **kwargs\n        )\n    return decorator\n\n\ndef _add_option_to_func(func, option_name, *args, **kwargs):\n    if isinstance(func, Command):\n        func.add_option_by_type(option_name, *args, **kwargs)\n        return func\n\n    if not hasattr(func, FUNC_ATTR_NAME):\n        setattr(func, FUNC_ATTR_NAME, [])\n    cli_options = getattr(func, FUNC_ATTR_NAME)\n    cli_options.append((option_name, args, kwargs))\n    return func\n"
  },
  {
    "path": "openpype/modules/clockify/__init__.py",
    "content": "from .clockify_module import ClockifyModule\n\n__all__ = (\n    \"ClockifyModule\",\n)\n"
  },
  {
    "path": "openpype/modules/clockify/clockify_api.py",
    "content": "import os\nimport re\nimport time\nimport json\nimport datetime\nimport requests\nfrom .constants import (\n    CLOCKIFY_ENDPOINT,\n    ADMIN_PERMISSION_NAMES,\n)\n\nfrom openpype.lib.local_settings import OpenPypeSecureRegistry\nfrom openpype.lib import Logger\n\n\nclass ClockifyAPI:\n    log = Logger.get_logger(__name__)\n\n    def __init__(self, api_key=None, master_parent=None):\n        self.workspace_name = None\n        self.master_parent = master_parent\n        self.api_key = api_key\n        self._workspace_id = None\n        self._user_id = None\n        self._secure_registry = None\n\n    @property\n    def secure_registry(self):\n        if self._secure_registry is None:\n            self._secure_registry = OpenPypeSecureRegistry(\"clockify\")\n        return self._secure_registry\n\n    @property\n    def headers(self):\n        return {\"x-api-key\": self.api_key}\n\n    @property\n    def workspace_id(self):\n        return self._workspace_id\n\n    @property\n    def user_id(self):\n        return self._user_id\n\n    def verify_api(self):\n        for key, value in self.headers.items():\n            if value is None or value.strip() == \"\":\n                return False\n        return True\n\n    def set_api(self, api_key=None):\n        if api_key is None:\n            api_key = self.get_api_key()\n\n        if api_key is not None and self.validate_api_key(api_key) is True:\n            self.api_key = api_key\n            self.set_workspace()\n            self.set_user_id()\n            if self.master_parent:\n                self.master_parent.signed_in()\n            return True\n        return False\n\n    def validate_api_key(self, api_key):\n        test_headers = {\"x-api-key\": api_key}\n        action_url = \"user\"\n        response = requests.get(\n            CLOCKIFY_ENDPOINT + action_url, headers=test_headers\n        )\n        if response.status_code != 200:\n            return False\n        return True\n\n    def validate_workspace_permissions(self, workspace_id=None, user_id=None):\n        if user_id is None:\n            self.log.info(\"No user_id found during validation\")\n            return False\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        action_url = f\"workspaces/{workspace_id}/users?includeRoles=1\"\n        response = requests.get(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers\n        )\n        data = response.json()\n        for user in data:\n            if user.get(\"id\") == user_id:\n                roles_data = user.get(\"roles\")\n        for entities in roles_data:\n            if entities.get(\"role\") in ADMIN_PERMISSION_NAMES:\n                return True\n        return False\n\n    def get_user_id(self):\n        action_url = \"user\"\n        response = requests.get(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers\n        )\n        result = response.json()\n        user_id = result.get(\"id\", None)\n\n        return user_id\n\n    def set_workspace(self, name=None):\n        if name is None:\n            name = os.environ.get(\"CLOCKIFY_WORKSPACE\", None)\n        self.workspace_name = name\n        if self.workspace_name is None:\n            return\n        try:\n            result = self.validate_workspace()\n        except Exception:\n            result = False\n        if result is not False:\n            self._workspace_id = result\n            if self.master_parent is not None:\n                self.master_parent.start_timer_check()\n            return True\n        return False\n\n    def validate_workspace(self, name=None):\n        if name is None:\n            name = self.workspace_name\n        all_workspaces = self.get_workspaces()\n        if name in all_workspaces:\n            return all_workspaces[name]\n        return False\n\n    def set_user_id(self):\n        try:\n            user_id = self.get_user_id()\n        except Exception:\n            user_id = None\n        if user_id is not None:\n            self._user_id = user_id\n\n    def get_api_key(self):\n        return self.secure_registry.get_item(\"api_key\", None)\n\n    def save_api_key(self, api_key):\n        self.secure_registry.set_item(\"api_key\", api_key)\n\n    def get_workspaces(self):\n        action_url = \"workspaces/\"\n        response = requests.get(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers\n        )\n        return {\n            workspace[\"name\"]: workspace[\"id\"] for workspace in response.json()\n        }\n\n    def get_projects(self, workspace_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        action_url = f\"workspaces/{workspace_id}/projects\"\n        response = requests.get(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers\n        )\n        if response.status_code != 403:\n            result = response.json()\n            return {project[\"name\"]: project[\"id\"] for project in result}\n\n    def get_project_by_id(self, project_id, workspace_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        action_url = \"workspaces/{}/projects/{}\".format(\n            workspace_id, project_id\n        )\n        response = requests.get(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers\n        )\n\n        return response.json()\n\n    def get_tags(self, workspace_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        action_url = \"workspaces/{}/tags\".format(workspace_id)\n        response = requests.get(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers\n        )\n\n        return {tag[\"name\"]: tag[\"id\"] for tag in response.json()}\n\n    def get_tasks(self, project_id, workspace_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        action_url = \"workspaces/{}/projects/{}/tasks\".format(\n            workspace_id, project_id\n        )\n        response = requests.get(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers\n        )\n\n        return {task[\"name\"]: task[\"id\"] for task in response.json()}\n\n    def get_workspace_id(self, workspace_name):\n        all_workspaces = self.get_workspaces()\n        if workspace_name not in all_workspaces:\n            return None\n        return all_workspaces[workspace_name]\n\n    def get_project_id(self, project_name, workspace_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        all_projects = self.get_projects(workspace_id)\n        if project_name not in all_projects:\n            return None\n        return all_projects[project_name]\n\n    def get_tag_id(self, tag_name, workspace_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        all_tasks = self.get_tags(workspace_id)\n        if tag_name not in all_tasks:\n            return None\n        return all_tasks[tag_name]\n\n    def get_task_id(self, task_name, project_id, workspace_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        all_tasks = self.get_tasks(project_id, workspace_id)\n        if task_name not in all_tasks:\n            return None\n        return all_tasks[task_name]\n\n    def get_current_time(self):\n        return str(datetime.datetime.utcnow().isoformat()) + \"Z\"\n\n    def start_time_entry(\n        self,\n        description,\n        project_id,\n        task_id=None,\n        tag_ids=None,\n        workspace_id=None,\n        user_id=None,\n        billable=True,\n    ):\n        # Workspace\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        # User ID\n        if user_id is None:\n            user_id = self._user_id\n\n        # get running timer to check if we need to start it\n        current_timer = self.get_in_progress()\n\n        # Check if is currently run another times and has same values\n        # DO not restart the timer, if it is already running for current task\n        if current_timer:\n            current_timer_hierarchy = current_timer.get(\"description\")\n            current_project_id = current_timer.get(\"projectId\")\n            current_task_id = current_timer.get(\"taskId\")\n            if (\n                description == current_timer_hierarchy\n                and project_id == current_project_id\n                and task_id == current_task_id\n            ):\n                self.log.info(\n                    \"Timer for the current project is already running\"\n                )\n                self.bool_timer_run = True\n                return self.bool_timer_run\n            self.finish_time_entry()\n\n        # Convert billable to strings\n        if billable:\n            billable = \"true\"\n        else:\n            billable = \"false\"\n        # Rest API Action\n        action_url = \"workspaces/{}/user/{}/time-entries\".format(\n            workspace_id, user_id\n        )\n        start = self.get_current_time()\n        body = {\n            \"start\": start,\n            \"billable\": billable,\n            \"description\": description,\n            \"projectId\": project_id,\n            \"taskId\": task_id,\n            \"tagIds\": tag_ids,\n        }\n        response = requests.post(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body\n        )\n        if response.status_code < 300:\n            return True\n        return False\n\n    def _get_current_timer_values(self, response):\n        if response is None:\n            return\n        try:\n            output = response.json()\n        except json.decoder.JSONDecodeError:\n            return None\n        if output and isinstance(output, list):\n            return output[0]\n        return None\n\n    def get_in_progress(self, user_id=None, workspace_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        if user_id is None:\n            user_id = self.user_id\n\n        action_url = (\n            f\"workspaces/{workspace_id}/user/\"\n            f\"{user_id}/time-entries?in-progress=1\"\n        )\n        response = requests.get(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers\n        )\n        return self._get_current_timer_values(response)\n\n    def finish_time_entry(self, workspace_id=None, user_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        if user_id is None:\n            user_id = self.user_id\n        current_timer = self.get_in_progress()\n        if not current_timer:\n            return\n        action_url = \"workspaces/{}/user/{}/time-entries\".format(\n            workspace_id, user_id\n        )\n        body = {\"end\": self.get_current_time()}\n        response = requests.patch(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body\n        )\n        return response.json()\n\n    def get_time_entries(self, workspace_id=None, user_id=None, quantity=10):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        if user_id is None:\n            user_id = self.user_id\n        action_url = \"workspaces/{}/user/{}/time-entries\".format(\n            workspace_id, user_id\n        )\n        response = requests.get(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers\n        )\n        return response.json()[:quantity]\n\n    def remove_time_entry(self, tid, workspace_id=None, user_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        action_url = \"workspaces/{}/user/{}/time-entries/{}\".format(\n            workspace_id, user_id, tid\n        )\n        response = requests.delete(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers\n        )\n        return response.json()\n\n    def add_project(self, name, workspace_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        action_url = \"workspaces/{}/projects\".format(workspace_id)\n        body = {\n            \"name\": name,\n            \"clientId\": \"\",\n            \"isPublic\": \"false\",\n            \"estimate\": {\"estimate\": 0, \"type\": \"AUTO\"},\n            \"color\": \"#f44336\",\n            \"billable\": \"true\",\n        }\n        response = requests.post(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body\n        )\n        return response.json()\n\n    def add_workspace(self, name):\n        action_url = \"workspaces/\"\n        body = {\"name\": name}\n        response = requests.post(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body\n        )\n        return response.json()\n\n    def add_task(self, name, project_id, workspace_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        action_url = \"workspaces/{}/projects/{}/tasks\".format(\n            workspace_id, project_id\n        )\n        body = {\"name\": name, \"projectId\": project_id}\n        response = requests.post(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body\n        )\n        return response.json()\n\n    def add_tag(self, name, workspace_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        action_url = \"workspaces/{}/tags\".format(workspace_id)\n        body = {\"name\": name}\n        response = requests.post(\n            CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body\n        )\n        return response.json()\n\n    def delete_project(self, project_id, workspace_id=None):\n        if workspace_id is None:\n            workspace_id = self.workspace_id\n        action_url = \"/workspaces/{}/projects/{}\".format(\n            workspace_id, project_id\n        )\n        response = requests.delete(\n            CLOCKIFY_ENDPOINT + action_url,\n            headers=self.headers,\n        )\n        return response.json()\n\n    def convert_input(\n        self, entity_id, entity_name, mode=\"Workspace\", project_id=None\n    ):\n        if entity_id is None:\n            error = False\n            error_msg = 'Missing information \"{}\"'\n            if mode.lower() == \"workspace\":\n                if entity_id is None and entity_name is None:\n                    if self.workspace_id is not None:\n                        entity_id = self.workspace_id\n                    else:\n                        error = True\n                else:\n                    entity_id = self.get_workspace_id(entity_name)\n            else:\n                if entity_id is None and entity_name is None:\n                    error = True\n                elif mode.lower() == \"project\":\n                    entity_id = self.get_project_id(entity_name)\n                elif mode.lower() == \"task\":\n                    entity_id = self.get_task_id(\n                        task_name=entity_name, project_id=project_id\n                    )\n                else:\n                    raise TypeError(\"Unknown type\")\n            # Raise error\n            if error:\n                raise ValueError(error_msg.format(mode))\n\n        return entity_id\n"
  },
  {
    "path": "openpype/modules/clockify/clockify_module.py",
    "content": "import os\nimport threading\nimport time\n\nfrom openpype.modules import OpenPypeModule, ITrayModule, IPluginPaths\nfrom openpype.client import get_asset_by_name\n\nfrom .constants import CLOCKIFY_FTRACK_USER_PATH, CLOCKIFY_FTRACK_SERVER_PATH\n\n\nclass ClockifyModule(OpenPypeModule, ITrayModule, IPluginPaths):\n    name = \"clockify\"\n\n    def initialize(self, modules_settings):\n        clockify_settings = modules_settings[self.name]\n        self.enabled = clockify_settings[\"enabled\"]\n        self.workspace_name = clockify_settings[\"workspace_name\"]\n\n        if self.enabled and not self.workspace_name:\n            raise Exception(\"Clockify Workspace is not set in settings.\")\n\n        self.timer_manager = None\n        self.MessageWidgetClass = None\n        self.message_widget = None\n        self._clockify_api = None\n\n        # TimersManager attributes\n        # - set `timers_manager_connector` only in `tray_init`\n        self.timers_manager_connector = None\n        self._timers_manager_module = None\n\n    @property\n    def clockify_api(self):\n        if self._clockify_api is None:\n            from .clockify_api import ClockifyAPI\n\n            self._clockify_api = ClockifyAPI(master_parent=self)\n        return self._clockify_api\n\n    def get_global_environments(self):\n        return {\"CLOCKIFY_WORKSPACE\": self.workspace_name}\n\n    def tray_init(self):\n        from .widgets import ClockifySettings, MessageWidget\n\n        self.MessageWidgetClass = MessageWidget\n\n        self.message_widget = None\n        self.widget_settings = ClockifySettings(self.clockify_api)\n        self.widget_settings_required = None\n\n        self.thread_timer_check = None\n        # Bools\n        self.bool_thread_check_running = False\n        self.bool_api_key_set = False\n        self.bool_workspace_set = False\n        self.bool_timer_run = False\n        self.bool_api_key_set = self.clockify_api.set_api()\n\n        # Define itself as TimersManager connector\n        self.timers_manager_connector = self\n\n    def tray_start(self):\n        if self.bool_api_key_set is False:\n            self.show_settings()\n            return\n\n        self.bool_workspace_set = self.clockify_api.workspace_id is not None\n        if self.bool_workspace_set is False:\n            return\n\n        self.start_timer_check()\n        self.set_menu_visibility()\n\n    def tray_exit(self, *_a, **_kw):\n        return\n\n    def get_plugin_paths(self):\n        \"\"\"Implementation of IPluginPaths to get plugin paths.\"\"\"\n        actions_path = os.path.join(\n            os.path.dirname(os.path.abspath(__file__)), \"launcher_actions\"\n        )\n        return {\"actions\": [actions_path]}\n\n    def get_ftrack_event_handler_paths(self):\n        \"\"\"Function for Ftrack module to add ftrack event handler paths.\"\"\"\n        return {\n            \"user\": [CLOCKIFY_FTRACK_USER_PATH],\n            \"server\": [CLOCKIFY_FTRACK_SERVER_PATH],\n        }\n\n    def clockify_timer_stopped(self):\n        self.bool_timer_run = False\n        self.timer_stopped()\n\n    def start_timer_check(self):\n        self.bool_thread_check_running = True\n        if self.thread_timer_check is None:\n            self.thread_timer_check = threading.Thread(\n                target=self.check_running\n            )\n            self.thread_timer_check.daemon = True\n            self.thread_timer_check.start()\n\n    def stop_timer_check(self):\n        self.bool_thread_check_running = True\n        if self.thread_timer_check is not None:\n            self.thread_timer_check.join()\n            self.thread_timer_check = None\n\n    def check_running(self):\n        while self.bool_thread_check_running is True:\n            bool_timer_run = False\n            if self.clockify_api.get_in_progress() is not None:\n                bool_timer_run = True\n\n            if self.bool_timer_run != bool_timer_run:\n                if self.bool_timer_run is True:\n                    self.clockify_timer_stopped()\n                elif self.bool_timer_run is False:\n                    current_timer = self.clockify_api.get_in_progress()\n                    if current_timer is None:\n                        continue\n                    current_proj_id = current_timer.get(\"projectId\")\n                    if not current_proj_id:\n                        continue\n\n                    project = self.clockify_api.get_project_by_id(\n                        current_proj_id\n                    )\n                    if project and project.get(\"code\") == 501:\n                        continue\n\n                    project_name = project.get(\"name\")\n\n                    current_timer_hierarchy = current_timer.get(\"description\")\n                    if not current_timer_hierarchy:\n                        continue\n                    hierarchy_items = current_timer_hierarchy.split(\"/\")\n                    # Each pype timer must have at least 2 items!\n                    if len(hierarchy_items) < 2:\n                        continue\n\n                    task_name = hierarchy_items[-1]\n                    hierarchy = hierarchy_items[:-1]\n\n                    data = {\n                        \"task_name\": task_name,\n                        \"hierarchy\": hierarchy,\n                        \"project_name\": project_name,\n                    }\n                    self.timer_started(data)\n\n                self.bool_timer_run = bool_timer_run\n                self.set_menu_visibility()\n            time.sleep(5)\n\n    def signed_in(self):\n        if not self.timer_manager:\n            return\n\n        if not self.timer_manager.last_task:\n            return\n\n        if self.timer_manager.is_running:\n            self.start_timer_manager(self.timer_manager.last_task)\n\n    def on_message_widget_close(self):\n        self.message_widget = None\n\n    # Definition of Tray menu\n    def tray_menu(self, parent_menu):\n        # Menu for Tray App\n        from qtpy import QtWidgets\n\n        menu = QtWidgets.QMenu(\"Clockify\", parent_menu)\n        menu.setProperty(\"submenu\", \"on\")\n\n        # Actions\n        action_show_settings = QtWidgets.QAction(\"Settings\", menu)\n        action_stop_timer = QtWidgets.QAction(\"Stop timer\", menu)\n\n        menu.addAction(action_show_settings)\n        menu.addAction(action_stop_timer)\n\n        action_show_settings.triggered.connect(self.show_settings)\n        action_stop_timer.triggered.connect(self.stop_timer)\n\n        self.action_stop_timer = action_stop_timer\n\n        self.set_menu_visibility()\n\n        parent_menu.addMenu(menu)\n\n    def show_settings(self):\n        self.widget_settings.input_api_key.setText(\n            self.clockify_api.get_api_key()\n        )\n        self.widget_settings.show()\n\n    def set_menu_visibility(self):\n        self.action_stop_timer.setVisible(self.bool_timer_run)\n\n    # --- TimersManager connection methods ---\n    def register_timers_manager(self, timer_manager_module):\n        \"\"\"Store TimersManager for future use.\"\"\"\n        self._timers_manager_module = timer_manager_module\n\n    def timer_started(self, data):\n        \"\"\"Tell TimersManager that timer started.\"\"\"\n        if self._timers_manager_module is not None:\n            self._timers_manager_module.timer_started(self.id, data)\n\n    def timer_stopped(self):\n        \"\"\"Tell TimersManager that timer stopped.\"\"\"\n        if self._timers_manager_module is not None:\n            self._timers_manager_module.timer_stopped(self.id)\n\n    def stop_timer(self):\n        \"\"\"Called from TimersManager to stop timer.\"\"\"\n        self.clockify_api.finish_time_entry()\n\n    def _verify_project_exists(self, project_name):\n        project_id = self.clockify_api.get_project_id(project_name)\n        if not project_id:\n            self.log.warning(\n                'Project \"{}\" was not found in Clockify. Timer won\\'t start.'\n            ).format(project_name)\n\n            if not self.MessageWidgetClass:\n                return\n\n            msg = (\n                'Project <b>\"{}\"</b> is not'\n                ' in Clockify Workspace <b>\"{}\"</b>.'\n                \"<br><br>Please inform your Project Manager.\"\n            ).format(project_name, str(self.clockify_api.workspace_name))\n\n            self.message_widget = self.MessageWidgetClass(\n                msg, \"Clockify - Info Message\"\n            )\n            self.message_widget.closed.connect(self.on_message_widget_close)\n            self.message_widget.show()\n            return False\n        return project_id\n\n    def start_timer(self, input_data):\n        \"\"\"Called from TimersManager to start timer.\"\"\"\n        # If not api key is not entered then skip\n        if not self.clockify_api.get_api_key():\n            return\n\n        task_name = input_data.get(\"task_name\")\n\n        # Concatenate hierarchy and task to get description\n        description_items = list(input_data.get(\"hierarchy\", []))\n        description_items.append(task_name)\n        description = \"/\".join(description_items)\n\n        # Check project existence\n        project_name = input_data.get(\"project_name\")\n        project_id = self._verify_project_exists(project_name)\n        if not project_id:\n            return\n\n        # Setup timer tags\n        tag_ids = []\n        tag_name = input_data.get(\"task_type\")\n        if not tag_name:\n            # no task_type found in the input data\n            # if the timer is restarted by idle time (bug?)\n            asset_name = input_data[\"hierarchy\"][-1]\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            task_info = asset_doc[\"data\"][\"tasks\"][task_name]\n            tag_name = task_info.get(\"type\", \"\")\n            if not tag_name:\n                self.log.info(\"No tag information found for the timer\")\n\n        task_tag_id = self.clockify_api.get_tag_id(tag_name)\n        if task_tag_id is not None:\n            tag_ids.append(task_tag_id)\n\n        # Start timer\n        self.clockify_api.start_time_entry(\n            description,\n            project_id,\n            tag_ids=tag_ids,\n            workspace_id=self.clockify_api.workspace_id,\n            user_id=self.clockify_api.user_id,\n        )\n"
  },
  {
    "path": "openpype/modules/clockify/constants.py",
    "content": "import os\n\n\nCLOCKIFY_FTRACK_SERVER_PATH = os.path.join(\n    os.path.dirname(os.path.abspath(__file__)), \"ftrack\", \"server\"\n)\nCLOCKIFY_FTRACK_USER_PATH = os.path.join(\n    os.path.dirname(os.path.abspath(__file__)), \"ftrack\", \"user\"\n)\n\nADMIN_PERMISSION_NAMES = [\"WORKSPACE_OWN\", \"WORKSPACE_ADMIN\"]\nCLOCKIFY_ENDPOINT = \"https://api.clockify.me/api/v1/\"\n"
  },
  {
    "path": "openpype/modules/clockify/ftrack/server/action_clockify_sync_server.py",
    "content": "import os\nimport json\nfrom openpype_modules.ftrack.lib import ServerAction\nfrom openpype_modules.clockify.clockify_api import ClockifyAPI\n\n\nclass SyncClockifyServer(ServerAction):\n    '''Synchronise project names and task types.'''\n\n    identifier = \"clockify.sync.server\"\n    label = \"Sync To Clockify (server)\"\n    description = \"Synchronise data to Clockify workspace\"\n\n    role_list = [\"Pypeclub\", \"Administrator\", \"project Manager\"]\n\n    def __init__(self, *args, **kwargs):\n        super(SyncClockifyServer, self).__init__(*args, **kwargs)\n\n        workspace_name = os.environ.get(\"CLOCKIFY_WORKSPACE\")\n        api_key = os.environ.get(\"CLOCKIFY_API_KEY\")\n        self.clockify_api = ClockifyAPI(api_key)\n        self.clockify_api.set_workspace(workspace_name)\n        if api_key is None:\n            modified_key = \"None\"\n        else:\n            str_len = int(len(api_key) / 2)\n            start_replace = int(len(api_key) / 4)\n            modified_key = \"\"\n            for idx in range(len(api_key)):\n                if idx >= start_replace and idx < start_replace + str_len:\n                    replacement = \"X\"\n                else:\n                    replacement = api_key[idx]\n                modified_key += replacement\n\n        self.log.info(\n            \"Clockify info. Workspace: \\\"{}\\\" API key: \\\"{}\\\"\".format(\n                str(workspace_name), str(modified_key)\n            )\n        )\n\n    def discover(self, session, entities, event):\n        if (\n            len(entities) != 1\n            or entities[0].entity_type.lower() != \"project\"\n        ):\n            return False\n        return True\n\n    def launch(self, session, entities, event):\n        self.clockify_api.set_api()\n        if self.clockify_api.workspace_id is None:\n            return {\n                \"success\": False,\n                \"message\": \"Clockify Workspace or API key are not set!\"\n            }\n\n        if not self.clockify_api.validate_workspace_permissions(\n            self.clockify_api.workspace_id, self.clockify_api.user_id\n        ):\n            return {\n                \"success\": False,\n                \"message\": \"Missing permissions for this action!\"\n            }\n\n        # JOB SETTINGS\n        user_id = event[\"source\"][\"user\"][\"id\"]\n        user = session.query(\"User where id is \" + user_id).one()\n\n        job = session.create(\"Job\", {\n            \"user\": user,\n            \"status\": \"running\",\n            \"data\": json.dumps({\"description\": \"Sync Ftrack to Clockify\"})\n        })\n        session.commit()\n\n        project_entity = entities[0]\n        if project_entity.entity_type.lower() != \"project\":\n            project_entity = self.get_project_from_entity(project_entity)\n\n        project_name = project_entity[\"full_name\"]\n        self.log.info(\n            \"Synchronization of project \\\"{}\\\" to clockify begins.\".format(\n                project_name\n            )\n        )\n        task_types = (\n            project_entity[\"project_schema\"][\"_task_type_schema\"][\"types\"]\n        )\n        task_type_names = [\n            task_type[\"name\"] for task_type in task_types\n        ]\n        try:\n            clockify_projects = self.clockify_api.get_projects()\n            if project_name not in clockify_projects:\n                response = self.clockify_api.add_project(project_name)\n                if \"id\" not in response:\n                    self.log.warning(\n                        \"Project \\\"{}\\\" can't be created. Response: {}\".format(\n                            project_name, response\n                        )\n                    )\n                    return {\n                        \"success\": False,\n                        \"message\": (\n                            \"Can't create clockify project \\\"{}\\\".\"\n                            \" Unexpected error.\"\n                        ).format(project_name)\n                    }\n\n            clockify_workspace_tags = self.clockify_api.get_tags()\n            for task_type_name in task_type_names:\n                if task_type_name in clockify_workspace_tags:\n                    self.log.debug(\n                        \"Task \\\"{}\\\" already exist\".format(task_type_name)\n                    )\n                    continue\n\n                response = self.clockify_api.add_tag(task_type_name)\n                if \"id\" not in response:\n                    self.log.warning(\n                        \"Task \\\"{}\\\" can't be created. Response: {}\".format(\n                            task_type_name, response\n                        )\n                    )\n\n            job[\"status\"] = \"done\"\n\n        except Exception:\n            self.log.warning(\n                \"Synchronization to clockify failed.\",\n                exc_info=True\n            )\n\n        finally:\n            if job[\"status\"] != \"done\":\n                job[\"status\"] = \"failed\"\n            session.commit()\n\n        return True\n\n\ndef register(session, **kw):\n    SyncClockifyServer(session).register()\n"
  },
  {
    "path": "openpype/modules/clockify/ftrack/user/action_clockify_sync_local.py",
    "content": "import json\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\nfrom openpype_modules.clockify.clockify_api import ClockifyAPI\n\n\nclass SyncClockifyLocal(BaseAction):\n    '''Synchronise project names and task types.'''\n\n    #: Action identifier.\n    identifier = 'clockify.sync.local'\n    #: Action label.\n    label = 'Sync To Clockify (local)'\n    #: Action description.\n    description = 'Synchronise data to Clockify workspace'\n    #: roles that are allowed to register this action\n    role_list = [\"Pypeclub\", \"Administrator\", \"project Manager\"]\n    #: icon\n    icon = statics_icon(\"app_icons\", \"clockify-white.png\")\n\n    def __init__(self, *args, **kwargs):\n        super(SyncClockifyLocal, self).__init__(*args, **kwargs)\n        #: CLockifyApi\n        self.clockify_api = ClockifyAPI()\n\n    def discover(self, session, entities, event):\n        if (\n            len(entities) == 1\n            and entities[0].entity_type.lower() == \"project\"\n        ):\n            return True\n        return False\n\n    def launch(self, session, entities, event):\n        self.clockify_api.set_api()\n        if self.clockify_api.workspace_id is None:\n            return {\n                \"success\": False,\n                \"message\": \"Clockify Workspace or API key are not set!\"\n            }\n\n        if (\n            self.clockify_api.validate_workspace_permissions(\n                self.clockify_api.workspace_id, self.clockify_api.user_id)\n            is False\n        ):\n            return {\n                \"success\": False,\n                \"message\": \"Missing permissions for this action!\"\n            }\n\n        # JOB SETTINGS\n        userId = event['source']['user']['id']\n        user = session.query('User where id is ' + userId).one()\n\n        job = session.create('Job', {\n            'user': user,\n            'status': 'running',\n            'data': json.dumps({\n                'description': 'Sync Ftrack to Clockify'\n            })\n        })\n        session.commit()\n\n        project_entity = entities[0]\n        if project_entity.entity_type.lower() != \"project\":\n            project_entity = self.get_project_from_entity(project_entity)\n\n        project_name = project_entity[\"full_name\"]\n        self.log.info(\n            \"Synchronization of project \\\"{}\\\" to clockify begins.\".format(\n                project_name\n            )\n        )\n        task_types = (\n            project_entity[\"project_schema\"][\"_task_type_schema\"][\"types\"]\n        )\n        task_type_names = [\n            task_type[\"name\"] for task_type in task_types\n        ]\n        try:\n            clockify_projects = self.clockify_api.get_projects()\n            if project_name not in clockify_projects:\n                response = self.clockify_api.add_project(project_name)\n                if \"id\" not in response:\n                    self.log.warning(\n                        \"Project \\\"{}\\\" can't be created. Response: {}\".format(\n                            project_name, response\n                        )\n                    )\n                    return {\n                        \"success\": False,\n                        \"message\": (\n                            \"Can't create clockify project \\\"{}\\\".\"\n                            \" Unexpected error.\"\n                        ).format(project_name)\n                    }\n\n            clockify_workspace_tags = self.clockify_api.get_tags()\n            for task_type_name in task_type_names:\n                if task_type_name in clockify_workspace_tags:\n                    self.log.debug(\n                        \"Task \\\"{}\\\" already exist\".format(task_type_name)\n                    )\n                    continue\n\n                response = self.clockify_api.add_tag(task_type_name)\n                if \"id\" not in response:\n                    self.log.warning(\n                        \"Task \\\"{}\\\" can't be created. Response: {}\".format(\n                            task_type_name, response\n                        )\n                    )\n\n            job[\"status\"] = \"done\"\n\n        except Exception:\n            pass\n\n        finally:\n            if job[\"status\"] != \"done\":\n                job[\"status\"] = \"failed\"\n            session.commit()\n\n        return True\n\n\ndef register(session, **kw):\n    SyncClockifyLocal(session).register()\n"
  },
  {
    "path": "openpype/modules/clockify/launcher_actions/ClockifyStart.py",
    "content": "from openpype.client import get_asset_by_name\nfrom openpype.pipeline import LauncherAction\nfrom openpype_modules.clockify.clockify_api import ClockifyAPI\n\n\nclass ClockifyStart(LauncherAction):\n    name = \"clockify_start_timer\"\n    label = \"Clockify - Start Timer\"\n    icon = \"app_icons/clockify.png\"\n    order = 500\n    clockify_api = ClockifyAPI()\n\n    def is_compatible(self, session):\n        \"\"\"Return whether the action is compatible with the session\"\"\"\n        if \"AVALON_TASK\" in session:\n            return True\n        return False\n\n    def process(self, session, **kwargs):\n        self.clockify_api.set_api()\n        user_id = self.clockify_api.user_id\n        workspace_id = self.clockify_api.workspace_id\n        project_name = session[\"AVALON_PROJECT\"]\n        asset_name = session[\"AVALON_ASSET\"]\n        task_name = session[\"AVALON_TASK\"]\n        description = asset_name\n\n        # fetch asset docs\n        asset_doc = get_asset_by_name(project_name, asset_name)\n\n        # get task type to fill the timer tag\n        task_info = asset_doc[\"data\"][\"tasks\"][task_name]\n        task_type = task_info[\"type\"]\n\n        # check if the task has hierarchy and fill the\n        parents_data = asset_doc[\"data\"]\n        if parents_data is not None:\n            description_items = parents_data.get(\"parents\", [])\n            description_items.append(asset_name)\n            description_items.append(task_name)\n            description = \"/\".join(description_items)\n\n        project_id = self.clockify_api.get_project_id(\n            project_name, workspace_id\n        )\n        tag_ids = []\n        tag_name = task_type\n        tag_ids.append(self.clockify_api.get_tag_id(tag_name, workspace_id))\n        self.clockify_api.start_time_entry(\n            description,\n            project_id,\n            tag_ids=tag_ids,\n            workspace_id=workspace_id,\n            user_id=user_id,\n        )\n"
  },
  {
    "path": "openpype/modules/clockify/launcher_actions/ClockifySync.py",
    "content": "from openpype.client import get_projects, get_project\nfrom openpype_modules.clockify.clockify_api import ClockifyAPI\nfrom openpype.pipeline import LauncherAction\n\n\nclass ClockifyPermissionsCheckFailed(Exception):\n    \"\"\"Timer start failed due to user permissions check.\n    Message should be self explanatory as traceback won't be shown.\n    \"\"\"\n\n    pass\n\n\nclass ClockifySync(LauncherAction):\n    name = \"sync_to_clockify\"\n    label = \"Sync to Clockify\"\n    icon = \"app_icons/clockify-white.png\"\n    order = 500\n    clockify_api = ClockifyAPI()\n\n    def is_compatible(self, session):\n        \"\"\"Check if there's some projects to sync\"\"\"\n        try:\n            next(get_projects())\n            return True\n        except StopIteration:\n            return False\n\n    def process(self, session, **kwargs):\n        self.clockify_api.set_api()\n        workspace_id = self.clockify_api.workspace_id\n        user_id = self.clockify_api.user_id\n        if not self.clockify_api.validate_workspace_permissions(\n            workspace_id, user_id\n        ):\n            raise ClockifyPermissionsCheckFailed(\n                \"Current CLockify user is missing permissions for this action!\"\n            )\n        project_name = session.get(\"AVALON_PROJECT\") or \"\"\n\n        projects_to_sync = []\n        if project_name.strip():\n            projects_to_sync = [get_project(project_name)]\n        else:\n            projects_to_sync = get_projects()\n\n        projects_info = {}\n        for project in projects_to_sync:\n            task_types = project[\"config\"][\"tasks\"].keys()\n            projects_info[project[\"name\"]] = task_types\n\n        clockify_projects = self.clockify_api.get_projects(workspace_id)\n        for project_name, task_types in projects_info.items():\n            if project_name in clockify_projects:\n                continue\n\n            response = self.clockify_api.add_project(\n                project_name, workspace_id\n            )\n            if \"id\" not in response:\n                self.log.error(\n                    \"Project {} can't be created\".format(project_name)\n                )\n                continue\n\n            clockify_workspace_tags = self.clockify_api.get_tags(workspace_id)\n            for task_type in task_types:\n                if task_type not in clockify_workspace_tags:\n                    response = self.clockify_api.add_tag(\n                        task_type, workspace_id\n                    )\n                    if \"id\" not in response:\n                        self.log.error(\n                            \"Task {} can't be created\".format(task_type)\n                        )\n                        continue\n"
  },
  {
    "path": "openpype/modules/clockify/widgets.py",
    "content": "from qtpy import QtCore, QtGui, QtWidgets\nfrom openpype import resources, style\n\n\nclass MessageWidget(QtWidgets.QWidget):\n\n    SIZE_W = 300\n    SIZE_H = 130\n\n    closed = QtCore.Signal()\n\n    def __init__(self, messages, title):\n        super(MessageWidget, self).__init__()\n\n        # Icon\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n\n        self.setWindowFlags(\n            QtCore.Qt.WindowCloseButtonHint |\n            QtCore.Qt.WindowMinimizeButtonHint\n        )\n\n        # Size setting\n        self.resize(self.SIZE_W, self.SIZE_H)\n        self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))\n        self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100))\n\n        # Style\n        self.setStyleSheet(style.load_stylesheet())\n\n        self.setLayout(self._ui_layout(messages))\n        self.setWindowTitle(title)\n\n    def _ui_layout(self, messages):\n        if not messages:\n            messages = [\"*Missing messages (This is a bug)*\", ]\n\n        elif not isinstance(messages, (tuple, list)):\n            messages = [messages, ]\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n\n        labels = []\n        for message in messages:\n            label = QtWidgets.QLabel(message)\n            label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))\n            label.setTextFormat(QtCore.Qt.RichText)\n            label.setWordWrap(True)\n\n            labels.append(label)\n            main_layout.addWidget(label)\n\n        btn_close = QtWidgets.QPushButton(\"Close\")\n        btn_close.setToolTip('Close this window')\n        btn_close.clicked.connect(self.on_close_clicked)\n\n        btn_group = QtWidgets.QHBoxLayout()\n        btn_group.addStretch(1)\n        btn_group.addWidget(btn_close)\n\n        main_layout.addLayout(btn_group)\n\n        self.labels = labels\n        self.btn_group = btn_group\n        self.btn_close = btn_close\n        self.main_layout = main_layout\n\n        return main_layout\n\n    def on_close_clicked(self):\n        self.close()\n\n    def close(self, *args, **kwargs):\n        self.closed.emit()\n        super(MessageWidget, self).close(*args, **kwargs)\n\n\nclass ClockifySettings(QtWidgets.QWidget):\n    SIZE_W = 500\n    SIZE_H = 130\n\n    loginSignal = QtCore.Signal(object, object, object)\n\n    def __init__(self, clockify_api, optional=True):\n        super(ClockifySettings, self).__init__()\n\n        self.clockify_api = clockify_api\n        self.optional = optional\n        self.validated = False\n\n        # Icon\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n\n        self.setWindowTitle(\"Clockify settings\")\n        self.setWindowFlags(\n            QtCore.Qt.WindowCloseButtonHint |\n            QtCore.Qt.WindowMinimizeButtonHint\n        )\n\n        # Size setting\n        self.resize(self.SIZE_W, self.SIZE_H)\n        self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))\n        self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100))\n        self.setStyleSheet(style.load_stylesheet())\n\n        self._ui_init()\n\n    def _ui_init(self):\n        label_api_key = QtWidgets.QLabel(\"Clockify API key:\")\n\n        input_api_key = QtWidgets.QLineEdit()\n        input_api_key.setFrame(True)\n        input_api_key.setPlaceholderText(\"e.g. XX1XxXX2x3x4xXxx\")\n\n        error_label = QtWidgets.QLabel(\"\")\n        error_label.setTextFormat(QtCore.Qt.RichText)\n        error_label.setWordWrap(True)\n        error_label.hide()\n\n        form_layout = QtWidgets.QFormLayout()\n        form_layout.setContentsMargins(10, 15, 10, 5)\n        form_layout.addRow(label_api_key, input_api_key)\n        form_layout.addRow(error_label)\n\n        btn_ok = QtWidgets.QPushButton(\"Ok\")\n        btn_ok.setToolTip('Sets Clockify API Key so can Start/Stop timer')\n\n        btn_cancel = QtWidgets.QPushButton(\"Cancel\")\n        cancel_tooltip = 'Application won\\'t start'\n        if self.optional:\n            cancel_tooltip = 'Close this window'\n        btn_cancel.setToolTip(cancel_tooltip)\n\n        btn_group = QtWidgets.QHBoxLayout()\n        btn_group.addStretch(1)\n        btn_group.addWidget(btn_ok)\n        btn_group.addWidget(btn_cancel)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addLayout(form_layout)\n        main_layout.addLayout(btn_group)\n\n        btn_ok.clicked.connect(self.click_ok)\n        btn_cancel.clicked.connect(self._close_widget)\n\n        self.label_api_key = label_api_key\n        self.input_api_key = input_api_key\n        self.error_label = error_label\n\n        self.btn_ok = btn_ok\n        self.btn_cancel = btn_cancel\n\n    def setError(self, msg):\n        self.error_label.setText(msg)\n        self.error_label.show()\n\n    def invalid_input(self, entity):\n        entity.setStyleSheet(\"border: 1px solid red;\")\n\n    def click_ok(self):\n        api_key = self.input_api_key.text().strip()\n        if self.optional is True and api_key == '':\n            self.clockify_api.save_api_key(None)\n            self.clockify_api.set_api(api_key)\n            self.validated = False\n            self._close_widget()\n            return\n\n        validation = self.clockify_api.validate_api_key(api_key)\n\n        if validation:\n            self.clockify_api.save_api_key(api_key)\n            self.clockify_api.set_api(api_key)\n            self.validated = True\n            self._close_widget()\n        else:\n            self.invalid_input(self.input_api_key)\n            self.validated = False\n            self.setError(\n                \"Entered invalid API key\"\n            )\n\n    def showEvent(self, event):\n        super(ClockifySettings, self).showEvent(event)\n\n        # Make btns same width\n        max_width = max(\n            self.btn_ok.sizeHint().width(),\n            self.btn_cancel.sizeHint().width()\n        )\n        self.btn_ok.setMinimumWidth(max_width)\n        self.btn_cancel.setMinimumWidth(max_width)\n\n    def closeEvent(self, event):\n        if self.optional is True:\n            event.ignore()\n            self._close_widget()\n        else:\n            self.validated = False\n\n    def _close_widget(self):\n        if self.optional is True:\n            self.hide()\n        else:\n            self.close()\n"
  },
  {
    "path": "openpype/modules/deadline/__init__.py",
    "content": "from .deadline_module import DeadlineModule\n\n\n__all__ = (\n    \"DeadlineModule\",\n)\n"
  },
  {
    "path": "openpype/modules/deadline/abstract_submit_deadline.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Abstract package for submitting jobs to Deadline.\n\nIt provides Deadline JobInfo data class.\n\n\"\"\"\nimport json.decoder\nimport os\nfrom abc import abstractmethod\nimport platform\nimport getpass\nfrom functools import partial\nfrom collections import OrderedDict\n\nimport six\nimport attr\nimport requests\n\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    AbstractMetaInstancePlugin,\n    KnownPublishError,\n    OpenPypePyblishPluginMixin\n)\nfrom openpype.pipeline.publish.lib import (\n    replace_with_published_scene_path\n)\nfrom openpype import AYON_SERVER_ENABLED\n\nJSONDecodeError = getattr(json.decoder, \"JSONDecodeError\", ValueError)\n\n\ndef requests_post(*args, **kwargs):\n    \"\"\"Wrap request post method.\n\n    Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment\n    variable is found. This is useful when Deadline server is\n    running with self-signed certificates and its certificate is not\n    added to trusted certificates on client machines.\n\n    Warning:\n        Disabling SSL certificate validation is defeating one line\n        of defense SSL is providing, and it is not recommended.\n\n    \"\"\"\n    if 'verify' not in kwargs:\n        kwargs['verify'] = False if os.getenv(\"OPENPYPE_DONT_VERIFY_SSL\",\n                                              True) else True  # noqa\n    # add 10sec timeout before bailing out\n    kwargs['timeout'] = 10\n    return requests.post(*args, **kwargs)\n\n\ndef requests_get(*args, **kwargs):\n    \"\"\"Wrap request get method.\n\n    Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment\n    variable is found. This is useful when Deadline server is\n    running with self-signed certificates and its certificate is not\n    added to trusted certificates on client machines.\n\n    Warning:\n        Disabling SSL certificate validation is defeating one line\n        of defense SSL is providing, and it is not recommended.\n\n    \"\"\"\n    if 'verify' not in kwargs:\n        kwargs['verify'] = False if os.getenv(\"OPENPYPE_DONT_VERIFY_SSL\",\n                                              True) else True  # noqa\n    # add 10sec timeout before bailing out\n    kwargs['timeout'] = 10\n    return requests.get(*args, **kwargs)\n\n\nclass DeadlineKeyValueVar(dict):\n    \"\"\"\n\n    Serializes dictionary key values as \"{key}={value}\" like Deadline uses\n    for EnvironmentKeyValue.\n\n    As an example:\n        EnvironmentKeyValue0=\"A_KEY=VALUE_A\"\n        EnvironmentKeyValue1=\"OTHER_KEY=VALUE_B\"\n\n    The keys are serialized in alphabetical order (sorted).\n\n    Example:\n        >>> var = DeadlineKeyValueVar(\"EnvironmentKeyValue\")\n        >>> var[\"my_var\"] = \"hello\"\n        >>> var[\"my_other_var\"] = \"hello2\"\n        >>> var.serialize()\n\n\n    \"\"\"\n    def __init__(self, key):\n        super(DeadlineKeyValueVar, self).__init__()\n        self.__key = key\n\n    def serialize(self):\n        key = self.__key\n\n        # Allow custom location for index in serialized string\n        if \"{}\" not in key:\n            key = key + \"{}\"\n\n        return {\n            key.format(index): \"{}={}\".format(var_key, var_value)\n            for index, (var_key, var_value) in enumerate(sorted(self.items()))\n        }\n\n\nclass DeadlineIndexedVar(dict):\n    \"\"\"\n\n    Allows to set and query values by integer indices:\n        Query: var[1] or var.get(1)\n        Set: var[1] = \"my_value\"\n        Append: var += \"value\"\n\n    Note: Iterating the instance is not guarantueed to be the order of the\n          indices. To do so iterate with `sorted()`\n\n    \"\"\"\n    def __init__(self, key):\n        super(DeadlineIndexedVar, self).__init__()\n        self.__key = key\n\n    def serialize(self):\n        key = self.__key\n\n        # Allow custom location for index in serialized string\n        if \"{}\" not in key:\n            key = key + \"{}\"\n\n        return {\n            key.format(index): value for index, value in sorted(self.items())\n        }\n\n    def next_available_index(self):\n        # Add as first unused entry\n        i = 0\n        while i in self.keys():\n            i += 1\n        return i\n\n    def update(self, data):\n        # Force the integer key check\n        for key, value in data.items():\n            self.__setitem__(key, value)\n\n    def __iadd__(self, other):\n        index = self.next_available_index()\n        self[index] = other\n        return self\n\n    def __setitem__(self, key, value):\n        if not isinstance(key, int):\n            raise TypeError(\"Key must be an integer: {}\".format(key))\n\n        if key < 0:\n            raise ValueError(\"Negative index can't be set: {}\".format(key))\n        dict.__setitem__(self, key, value)\n\n\n@attr.s\nclass DeadlineJobInfo(object):\n    \"\"\"Mapping of all Deadline *JobInfo* attributes.\n\n    This contains all JobInfo attributes plus their default values.\n    Those attributes set to `None` shouldn't be posted to Deadline as\n    the only required one is `Plugin`. Their default values used by Deadline\n    are stated in\n    comments.\n\n    ..seealso:\n        https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/manual-submission.html\n\n    \"\"\"\n\n    # Required\n    # ----------------------------------------------\n    Plugin = attr.ib()\n\n    # General\n    Frames = attr.ib(default=None)  # default: 0\n    Name = attr.ib(default=\"Untitled\")\n    Comment = attr.ib(default=None)  # default: empty\n    Department = attr.ib(default=None)  # default: empty\n    BatchName = attr.ib(default=None)  # default: empty\n    UserName = attr.ib(default=getpass.getuser())\n    MachineName = attr.ib(default=platform.node())\n    Pool = attr.ib(default=None)  # default: \"none\"\n    SecondaryPool = attr.ib(default=None)\n    Group = attr.ib(default=None)  # default: \"none\"\n    Priority = attr.ib(default=50)\n    ChunkSize = attr.ib(default=1)\n    ConcurrentTasks = attr.ib(default=1)\n    LimitConcurrentTasksToNumberOfCpus = attr.ib(\n        default=None)  # default: \"true\"\n    OnJobComplete = attr.ib(default=\"Nothing\")\n    SynchronizeAllAuxiliaryFiles = attr.ib(default=None)  # default: false\n    ForceReloadPlugin = attr.ib(default=None)  # default: false\n    Sequential = attr.ib(default=None)  # default: false\n    SuppressEvents = attr.ib(default=None)  # default: false\n    Protected = attr.ib(default=None)  # default: false\n    InitialStatus = attr.ib(default=\"Active\")\n    NetworkRoot = attr.ib(default=None)\n\n    # Timeouts\n    # ----------------------------------------------\n    MinRenderTimeSeconds = attr.ib(default=None)  # Default: 0\n    MinRenderTimeMinutes = attr.ib(default=None)  # Default: 0\n    TaskTimeoutSeconds = attr.ib(default=None)  # Default: 0\n    TaskTimeoutMinutes = attr.ib(default=None)  # Default: 0\n    StartJobTimeoutSeconds = attr.ib(default=None)  # Default: 0\n    StartJobTimeoutMinutes = attr.ib(default=None)  # Default: 0\n    InitializePluginTimeoutSeconds = attr.ib(default=None)  # Default: 0\n    # can be one of <Error/Notify/ErrorAndNotify/Complete>\n    OnTaskTimeout = attr.ib(default=None)  # Default: Error\n    EnableTimeoutsForScriptTasks = attr.ib(default=None)  # Default: false\n    EnableFrameTimeouts = attr.ib(default=None)  # Default: false\n    EnableAutoTimeout = attr.ib(default=None)  # Default: false\n\n    # Interruptible\n    # ----------------------------------------------\n    Interruptible = attr.ib(default=None)  # Default: false\n    InterruptiblePercentage = attr.ib(default=None)\n    RemTimeThreshold = attr.ib(default=None)\n\n    # Notifications\n    # ----------------------------------------------\n    # can be comma separated list of users\n    NotificationTargets = attr.ib(default=None)  # Default: blank\n    ClearNotificationTargets = attr.ib(default=None)  # Default: false\n    # A comma separated list of additional email addresses\n    NotificationEmails = attr.ib(default=None)  # Default: blank\n    OverrideNotificationMethod = attr.ib(default=None)  # Default: false\n    EmailNotification = attr.ib(default=None)  # Default: false\n    PopupNotification = attr.ib(default=None)  # Default: false\n    # String with `[EOL]` used for end of line\n    NotificationNote = attr.ib(default=None)  # Default: blank\n\n    # Machine Limit\n    # ----------------------------------------------\n    MachineLimit = attr.ib(default=None)  # Default: 0\n    MachineLimitProgress = attr.ib(default=None)  # Default: -1.0\n    Whitelist = attr.ib(default=None)  # Default: blank\n    Blacklist = attr.ib(default=None)  # Default: blank\n\n    # Limits\n    # ----------------------------------------------\n    # comma separated list of limit groups\n    LimitGroups = attr.ib(default=None)  # Default: blank\n\n    # Dependencies\n    # ----------------------------------------------\n    # comma separated list of job IDs\n    JobDependencies = attr.ib(default=None)  # Default: blank\n    JobDependencyPercentage = attr.ib(default=None)  # Default: -1\n    IsFrameDependent = attr.ib(default=None)  # Default: false\n    FrameDependencyOffsetStart = attr.ib(default=None)  # Default: 0\n    FrameDependencyOffsetEnd = attr.ib(default=None)  # Default: 0\n    ResumeOnCompleteDependencies = attr.ib(default=None)  # Default: true\n    ResumeOnDeletedDependencies = attr.ib(default=None)  # Default: false\n    ResumeOnFailedDependencies = attr.ib(default=None)  # Default: false\n    # comma separated list of asset paths\n    RequiredAssets = attr.ib(default=None)  # Default: blank\n    # comma separated list of script paths\n    ScriptDependencies = attr.ib(default=None)  # Default: blank\n\n    # Failure Detection\n    # ----------------------------------------------\n    OverrideJobFailureDetection = attr.ib(default=None)  # Default: false\n    FailureDetectionJobErrors = attr.ib(default=None)  # 0..x\n    OverrideTaskFailureDetection = attr.ib(default=None)  # Default: false\n    FailureDetectionTaskErrors = attr.ib(default=None)  # 0..x\n    IgnoreBadJobDetection = attr.ib(default=None)  # Default: false\n    SendJobErrorWarning = attr.ib(default=None)  # Default: false\n\n    # Cleanup\n    # ----------------------------------------------\n    DeleteOnComplete = attr.ib(default=None)  # Default: false\n    ArchiveOnComplete = attr.ib(default=None)  # Default: false\n    OverrideAutoJobCleanup = attr.ib(default=None)  # Default: false\n    OverrideJobCleanup = attr.ib(default=None)\n    JobCleanupDays = attr.ib(default=None)  # Default: false\n    # <ArchiveJobs/DeleteJobs>\n    OverrideJobCleanupType = attr.ib(default=None)\n\n    # Scheduling\n    # ----------------------------------------------\n    # <None/Once/Daily/Custom>\n    ScheduledType = attr.ib(default=None)  # Default: None\n    # <dd/MM/yyyy HH:mm>\n    ScheduledStartDateTime = attr.ib(default=None)\n    ScheduledDays = attr.ib(default=None)  # Default: 1\n    # <dd:hh:mm:ss>\n    JobDelay = attr.ib(default=None)\n    # <Day of the Week><Start/Stop>Time=<HH:mm:ss>\n    Scheduled = attr.ib(default=None)\n\n    # Scripts\n    # ----------------------------------------------\n    # all accept path to script\n    PreJobScript = attr.ib(default=None)  # Default: blank\n    PostJobScript = attr.ib(default=None)  # Default: blank\n    PreTaskScript = attr.ib(default=None)  # Default: blank\n    PostTaskScript = attr.ib(default=None)  # Default: blank\n\n    # Event Opt-Ins\n    # ----------------------------------------------\n    # comma separated list of plugins\n    EventOptIns = attr.ib(default=None)  # Default: blank\n\n    # Environment\n    # ----------------------------------------------\n    EnvironmentKeyValue = attr.ib(factory=partial(DeadlineKeyValueVar,\n                                                  \"EnvironmentKeyValue\"))\n\n    IncludeEnvironment = attr.ib(default=None)  # Default: false\n    UseJobEnvironmentOnly = attr.ib(default=None)  # Default: false\n    CustomPluginDirectory = attr.ib(default=None)  # Default: blank\n\n    # Job Extra Info\n    # ----------------------------------------------\n    ExtraInfo = attr.ib(factory=partial(DeadlineIndexedVar, \"ExtraInfo\"))\n    ExtraInfoKeyValue = attr.ib(factory=partial(DeadlineKeyValueVar,\n                                                \"ExtraInfoKeyValue\"))\n\n    # Task Extra Info Names\n    # ----------------------------------------------\n    OverrideTaskExtraInfoNames = attr.ib(default=None)  # Default: false\n    TaskExtraInfoName = attr.ib(factory=partial(DeadlineIndexedVar,\n                                                \"TaskExtraInfoName\"))\n\n    # Output\n    # ----------------------------------------------\n    OutputFilename = attr.ib(factory=partial(DeadlineIndexedVar,\n                                             \"OutputFilename\"))\n    OutputFilenameTile = attr.ib(factory=partial(DeadlineIndexedVar,\n                                                 \"OutputFilename{}Tile\"))\n    OutputDirectory = attr.ib(factory=partial(DeadlineIndexedVar,\n                                              \"OutputDirectory\"))\n\n    # Asset Dependency\n    # ----------------------------------------------\n    AssetDependency = attr.ib(factory=partial(DeadlineIndexedVar,\n                                              \"AssetDependency\"))\n\n    # Tile Job\n    # ----------------------------------------------\n    TileJob = attr.ib(default=None)  # Default: false\n    TileJobFrame = attr.ib(default=None)  # Default: 0\n    TileJobTilesInX = attr.ib(default=None)  # Default: 0\n    TileJobTilesInY = attr.ib(default=None)  # Default: 0\n    TileJobTileCount = attr.ib(default=None)  # Default: 0\n\n    # Maintenance Job\n    # ----------------------------------------------\n    MaintenanceJob = attr.ib(default=None)  # Default: false\n    MaintenanceJobStartFrame = attr.ib(default=None)  # Default: 0\n    MaintenanceJobEndFrame = attr.ib(default=None)  # Default: 0\n\n    def serialize(self):\n        \"\"\"Return all data serialized as dictionary.\n\n        Returns:\n            OrderedDict: all serialized data.\n\n        \"\"\"\n        def filter_data(a, v):\n            if isinstance(v, (DeadlineIndexedVar, DeadlineKeyValueVar)):\n                return False\n            if v is None:\n                return False\n            return True\n\n        serialized = attr.asdict(\n            self, dict_factory=OrderedDict, filter=filter_data)\n\n        # Custom serialize these attributes\n        for attribute in [\n            self.EnvironmentKeyValue,\n            self.ExtraInfo,\n            self.ExtraInfoKeyValue,\n            self.TaskExtraInfoName,\n            self.OutputFilename,\n            self.OutputFilenameTile,\n            self.OutputDirectory,\n            self.AssetDependency\n        ]:\n            serialized.update(attribute.serialize())\n\n        return serialized\n\n    def update(self, data):\n        \"\"\"Update instance with data dict\"\"\"\n        for key, value in data.items():\n            setattr(self, key, value)\n\n    def add_render_job_env_var(self):\n        \"\"\"Check if in OP or AYON mode and use appropriate env var.\"\"\"\n        if AYON_SERVER_ENABLED:\n            self.EnvironmentKeyValue[\"AYON_RENDER_JOB\"] = \"1\"\n            self.EnvironmentKeyValue[\"AYON_BUNDLE_NAME\"] = (\n                os.environ[\"AYON_BUNDLE_NAME\"])\n        else:\n            self.EnvironmentKeyValue[\"OPENPYPE_RENDER_JOB\"] = \"1\"\n\n\n@six.add_metaclass(AbstractMetaInstancePlugin)\nclass AbstractSubmitDeadline(pyblish.api.InstancePlugin,\n                             OpenPypePyblishPluginMixin):\n    \"\"\"Class abstracting access to Deadline.\"\"\"\n\n    label = \"Submit to Deadline\"\n    order = pyblish.api.IntegratorOrder + 0.1\n\n    import_reference = False\n    use_published = True\n    asset_dependencies = False\n    default_priority = 50\n\n    def __init__(self, *args, **kwargs):\n        super(AbstractSubmitDeadline, self).__init__(*args, **kwargs)\n        self._instance = None\n        self._deadline_url = None\n        self.scene_path = None\n        self.job_info = None\n        self.plugin_info = None\n        self.aux_files = None\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        self._instance = instance\n        context = instance.context\n        self._deadline_url = context.data.get(\"defaultDeadline\")\n        self._deadline_url = instance.data.get(\n            \"deadlineUrl\", self._deadline_url)\n\n        assert self._deadline_url, \"Requires Deadline Webservice URL\"\n\n        file_path = None\n        if self.use_published:\n            if not self.import_reference:\n                file_path = self.from_published_scene()\n            else:\n                self.log.info(\"use the scene with imported reference for rendering\") # noqa\n                file_path = context.data[\"currentFile\"]\n\n        # fallback if nothing was set\n        if not file_path:\n            self.log.warning(\"Falling back to workfile\")\n            file_path = context.data[\"currentFile\"]\n\n        self.scene_path = file_path\n        self.log.info(\"Using {} for render/export.\".format(file_path))\n\n        self.job_info = self.get_job_info()\n        self.plugin_info = self.get_plugin_info()\n        self.aux_files = self.get_aux_files()\n\n        job_id = self.process_submission()\n        self.log.info(\"Submitted job to Deadline: {}.\".format(job_id))\n\n        # TODO: Find a way that's more generic and not render type specific\n        if instance.data.get(\"splitRender\"):\n            self.log.info(\"Splitting export and render in two jobs\")\n            self.log.info(\"Export job id: %s\", job_id)\n            render_job_info = self.get_job_info(dependency_job_ids=[job_id])\n            render_plugin_info = self.get_plugin_info(job_type=\"render\")\n            payload = self.assemble_payload(\n                job_info=render_job_info,\n                plugin_info=render_plugin_info\n            )\n            render_job_id = self.submit(payload)\n            self.log.info(\"Render job id: %s\", render_job_id)\n\n    def process_submission(self):\n        \"\"\"Process data for submission.\n\n        This takes Deadline JobInfo, PluginInfo, AuxFile, creates payload\n        from them and submit it do Deadline.\n\n        Returns:\n            str: Deadline job ID\n\n        \"\"\"\n        payload = self.assemble_payload()\n        return self.submit(payload)\n\n    @abstractmethod\n    def get_job_info(self):\n        \"\"\"Return filled Deadline JobInfo.\n\n        This is host/plugin specific implementation of how to fill data in.\n\n        See:\n            :class:`DeadlineJobInfo`\n\n        Returns:\n            :class:`DeadlineJobInfo`: Filled Deadline JobInfo.\n\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_plugin_info(self):\n        \"\"\"Return filled Deadline PluginInfo.\n\n        This is host/plugin specific implementation of how to fill data in.\n\n        See:\n            :class:`DeadlineJobInfo`\n\n        Returns:\n            dict: Filled Deadline JobInfo.\n\n        \"\"\"\n        pass\n\n    def get_aux_files(self):\n        \"\"\"Return list of auxiliary files for Deadline job.\n\n        If needed this should be overridden, otherwise return empty list as\n        that field even empty must be present on Deadline submission.\n\n        Returns:\n            list: List of files.\n\n        \"\"\"\n        return []\n\n    def from_published_scene(self, replace_in_path=True):\n        \"\"\"Switch work scene for published scene.\n\n        If rendering/exporting from published scenes is enabled, this will\n        replace paths from working scene to published scene.\n\n        Args:\n            replace_in_path (bool): if True, it will try to find\n                old scene name in path of expected files and replace it\n                with name of published scene.\n\n        Returns:\n            str: Published scene path.\n            None: if no published scene is found.\n\n        Note:\n            Published scene path is actually determined from project Anatomy\n            as at the time this plugin is running scene can still no be\n            published.\n\n        \"\"\"\n        return replace_with_published_scene_path(\n            self._instance, replace_in_path=replace_in_path)\n\n    def assemble_payload(\n            self, job_info=None, plugin_info=None, aux_files=None):\n        \"\"\"Assemble payload data from its various parts.\n\n        Args:\n            job_info (DeadlineJobInfo): Deadline JobInfo. You can use\n                :class:`DeadlineJobInfo` for it.\n            plugin_info (dict): Deadline PluginInfo. Plugin specific options.\n            aux_files (list, optional): List of auxiliary file to submit with\n                the job.\n\n        Returns:\n            dict: Deadline Payload.\n\n        \"\"\"\n        job = job_info or self.job_info\n        return {\n            \"JobInfo\": job.serialize(),\n            \"PluginInfo\": plugin_info or self.plugin_info,\n            \"AuxFiles\": aux_files or self.aux_files\n        }\n\n    def submit(self, payload):\n        \"\"\"Submit payload to Deadline API end-point.\n\n        This takes payload in the form of JSON file and POST it to\n        Deadline jobs end-point.\n\n        Args:\n            payload (dict): dict to become json in deadline submission.\n\n        Returns:\n            str: resulting Deadline job id.\n\n        Throws:\n            KnownPublishError: if submission fails.\n\n        \"\"\"\n        url = \"{}/api/jobs\".format(self._deadline_url)\n        response = requests_post(url, json=payload)\n        if not response.ok:\n            self.log.error(\"Submission failed!\")\n            self.log.error(response.status_code)\n            self.log.error(response.content)\n            self.log.debug(payload)\n            raise KnownPublishError(response.text)\n\n        try:\n            result = response.json()\n        except JSONDecodeError:\n            msg = \"Broken response {}. \".format(response)\n            msg += \"Try restarting the Deadline Webservice.\"\n            self.log.warning(msg, exc_info=True)\n            raise KnownPublishError(\"Broken response from DL\")\n\n        # for submit publish job\n        self._instance.data[\"deadlineSubmissionJob\"] = result\n\n        return result[\"_id\"]\n"
  },
  {
    "path": "openpype/modules/deadline/deadline_module.py",
    "content": "import os\nimport requests\nimport six\nimport sys\n\nfrom openpype.lib import requests_get, Logger\nfrom openpype.modules import OpenPypeModule, IPluginPaths\n\n\nclass DeadlineWebserviceError(Exception):\n    \"\"\"\n    Exception to throw when connection to Deadline server fails.\n    \"\"\"\n\n\nclass DeadlineModule(OpenPypeModule, IPluginPaths):\n    name = \"deadline\"\n\n    def __init__(self, manager, settings):\n        self.deadline_urls = {}\n        super(DeadlineModule, self).__init__(manager, settings)\n\n    def initialize(self, modules_settings):\n        # This module is always enabled\n        deadline_settings = modules_settings[self.name]\n        self.enabled = deadline_settings[\"enabled\"]\n        deadline_url = deadline_settings.get(\"DEADLINE_REST_URL\")\n        if deadline_url:\n            self.deadline_urls = {\"default\": deadline_url}\n        else:\n            self.deadline_urls = deadline_settings.get(\"deadline_urls\")  # noqa: E501\n\n        if not self.deadline_urls:\n            self.enabled = False\n            self.log.warning((\"default Deadline Webservice URL \"\n                              \"not specified. Disabling module.\"))\n            return\n\n    def get_plugin_paths(self):\n        \"\"\"Deadline plugin paths.\"\"\"\n        current_dir = os.path.dirname(os.path.abspath(__file__))\n        return {\n            \"publish\": [os.path.join(current_dir, \"plugins\", \"publish\")]\n        }\n\n    @staticmethod\n    def get_deadline_pools(webservice, log=None):\n        # type: (str) -> list\n        \"\"\"Get pools from Deadline.\n        Args:\n            webservice (str): Server url.\n            log (Logger)\n        Returns:\n            list: Pools.\n        Throws:\n            RuntimeError: If deadline webservice is unreachable.\n\n        \"\"\"\n        if not log:\n            log = Logger.get_logger(__name__)\n\n        argument = \"{}/api/pools?NamesOnly=true\".format(webservice)\n        try:\n            response = requests_get(argument)\n        except requests.exceptions.ConnectionError as exc:\n            msg = 'Cannot connect to DL web service {}'.format(webservice)\n            log.error(msg)\n            six.reraise(\n                DeadlineWebserviceError,\n                DeadlineWebserviceError('{} - {}'.format(msg, exc)),\n                sys.exc_info()[2])\n        if not response.ok:\n            log.warning(\"No pools retrieved\")\n            return []\n\n        return response.json()\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect Deadline servers from instance.\n\nThis is resolving index of server lists stored in `deadlineServers` instance\nattribute or using default server if that attribute doesn't exists.\n\n\"\"\"\nimport pyblish.api\nfrom openpype.pipeline.publish import KnownPublishError\n\n\nclass CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin):\n    \"\"\"Collect Deadline Webservice URL from instance.\"\"\"\n\n    # Run before collect_render.\n    order = pyblish.api.CollectorOrder + 0.005\n    label = \"Deadline Webservice from the Instance\"\n    families = [\"rendering\", \"renderlayer\"]\n    hosts = [\"maya\"]\n\n    def process(self, instance):\n        instance.data[\"deadlineUrl\"] = self._collect_deadline_url(instance)\n        instance.data[\"deadlineUrl\"] = \\\n            instance.data[\"deadlineUrl\"].strip().rstrip(\"/\")\n        self.log.debug(\n            \"Using {} for submission.\".format(instance.data[\"deadlineUrl\"]))\n\n    def _collect_deadline_url(self, render_instance):\n        # type: (pyblish.api.Instance) -> str\n        \"\"\"Get Deadline Webservice URL from render instance.\n\n        This will get all configured Deadline Webservice URLs and create\n        subset of them based upon project configuration. It will then take\n        `deadlineServers` from render instance that is now basically `int`\n        index of that list.\n\n        Args:\n            render_instance (pyblish.api.Instance): Render instance created\n                by Creator in Maya.\n\n        Returns:\n            str: Selected Deadline Webservice URL.\n\n        \"\"\"\n        # Not all hosts can import this module.\n        from maya import cmds\n        deadline_settings = (\n            render_instance.context.data\n            [\"system_settings\"]\n            [\"modules\"]\n            [\"deadline\"]\n        )\n\n        default_server = render_instance.context.data[\"defaultDeadline\"]\n        instance_server = render_instance.data.get(\"deadlineServers\")\n        if not instance_server:\n            self.log.debug(\"Using default server.\")\n            return default_server\n\n        # Get instance server as sting.\n        if isinstance(instance_server, int):\n            instance_server = cmds.getAttr(\n                \"{}.deadlineServers\".format(render_instance.data[\"objset\"]),\n                asString=True\n            )\n\n        default_servers = deadline_settings[\"deadline_urls\"]\n        project_servers = (\n            render_instance.context.data\n            [\"project_settings\"]\n            [\"deadline\"]\n            [\"deadline_servers\"]\n        )\n        if not project_servers:\n            self.log.debug(\"Not project servers found. Using default servers.\")\n            return default_servers[instance_server]\n\n        project_enabled_servers = {\n            k: default_servers[k]\n            for k in project_servers\n            if k in default_servers\n        }\n\n        if instance_server not in project_enabled_servers:\n            msg = (\n                \"\\\"{}\\\" server on instance is not enabled in project settings.\"\n                \" Enabled project servers:\\n{}\".format(\n                    instance_server, project_enabled_servers\n                )\n            )\n            raise KnownPublishError(msg)\n\n        self.log.debug(\"Using project approved server.\")\n        return project_enabled_servers[instance_server]\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect default Deadline server.\"\"\"\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\n\n\nclass CollectDefaultDeadlineServer(pyblish.api.ContextPlugin):\n    \"\"\"Collect default Deadline Webservice URL.\n\n    DL webservice addresses must be configured first in System Settings for\n    project settings enum to work.\n\n    Default webservice could be overriden by\n    `project_settings/deadline/deadline_servers`. Currently only single url\n    is expected.\n\n    This url could be overriden by some hosts directly on instances with\n    `CollectDeadlineServerFromInstance`.\n    \"\"\"\n\n    # Run before collect_deadline_server_instance.\n    order = pyblish.api.CollectorOrder + 0.0025\n    label = \"Default Deadline Webservice\"\n\n    pass_mongo_url = False\n\n    def process(self, context):\n        try:\n            deadline_module = context.data.get(\"openPypeModules\")[\"deadline\"]\n        except AttributeError:\n            self.log.error(\"Cannot get OpenPype Deadline module.\")\n            raise AssertionError(\"OpenPype Deadline module not found.\")\n\n        deadline_settings = context.data[\"project_settings\"][\"deadline\"]\n        deadline_server_name = None\n        if AYON_SERVER_ENABLED:\n            deadline_server_name = deadline_settings[\"deadline_server\"]\n        else:\n            deadline_servers = deadline_settings[\"deadline_servers\"]\n            if deadline_servers:\n                deadline_server_name = deadline_servers[0]\n\n            context.data[\"deadlinePassMongoUrl\"] = self.pass_mongo_url\n\n        deadline_webservice = None\n        if deadline_server_name:\n            deadline_webservice = deadline_module.deadline_urls.get(\n                deadline_server_name)\n\n        default_deadline_webservice = deadline_module.deadline_urls[\"default\"]\n        deadline_webservice = (\n            deadline_webservice\n            or default_deadline_webservice\n        )\n\n        context.data[\"defaultDeadline\"] = deadline_webservice.strip().rstrip(\"/\")  # noqa\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/collect_pools.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\nfrom openpype.lib import TextDef\nfrom openpype.pipeline.publish import OpenPypePyblishPluginMixin\n\n\nclass CollectDeadlinePools(pyblish.api.InstancePlugin,\n                           OpenPypePyblishPluginMixin):\n    \"\"\"Collect pools from instance or Publisher attributes, from Setting\n    otherwise.\n\n    Pools are used to control which DL workers could render the job.\n\n    Pools might be set:\n    - directly on the instance (set directly in DCC)\n    - from Publisher attributes\n    - from defaults from Settings.\n\n    Publisher attributes could be shown even for instances that should be\n    rendered locally as visibility is driven by product type of the instance\n    (which will be `render` most likely).\n    (Might be resolved in the future and class attribute 'families' should\n    be cleaned up.)\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.420\n    label = \"Collect Deadline Pools\"\n    hosts = [\"aftereffects\",\n             \"fusion\",\n             \"harmony\"\n             \"nuke\",\n             \"maya\",\n             \"max\",\n             \"houdini\"]\n\n    families = [\"render\",\n                \"rendering\",\n                \"render.farm\",\n                \"renderFarm\",\n                \"renderlayer\",\n                \"maxrender\",\n                \"usdrender\",\n                \"redshift_rop\",\n                \"arnold_rop\",\n                \"mantra_rop\",\n                \"karma_rop\",\n                \"vray_rop\",\n                \"publish.hou\"]\n\n    primary_pool = None\n    secondary_pool = None\n\n    @classmethod\n    def apply_settings(cls, project_settings, system_settings):\n        # deadline.publish.CollectDeadlinePools\n        settings = project_settings[\"deadline\"][\"publish\"][\"CollectDeadlinePools\"]  # noqa\n        cls.primary_pool = settings.get(\"primary_pool\", None)\n        cls.secondary_pool = settings.get(\"secondary_pool\", None)\n\n    def process(self, instance):\n        attr_values = self.get_attr_values_from_data(instance.data)\n        if not instance.data.get(\"primaryPool\"):\n            instance.data[\"primaryPool\"] = (\n                attr_values.get(\"primaryPool\") or self.primary_pool or \"none\"\n            )\n        if instance.data[\"primaryPool\"] == \"-\":\n            instance.data[\"primaryPool\"] = None\n\n        if not instance.data.get(\"secondaryPool\"):\n            instance.data[\"secondaryPool\"] = (\n                attr_values.get(\"secondaryPool\") or self.secondary_pool or \"none\"  # noqa\n            )\n\n        if instance.data[\"secondaryPool\"] == \"-\":\n            instance.data[\"secondaryPool\"] = None\n\n    @classmethod\n    def get_attribute_defs(cls):\n        # TODO: Preferably this would be an enum for the user\n        #       but the Deadline server URL can be dynamic and\n        #       can be set per render instance. Since get_attribute_defs\n        #       can't be dynamic unfortunately EnumDef isn't possible (yet?)\n        # pool_names = self.deadline_module.get_deadline_pools(deadline_url,\n        #                                                      self.log)\n        # secondary_pool_names = [\"-\"] + pool_names\n\n        return [\n            TextDef(\"primaryPool\",\n                    label=\"Primary Pool\",\n                    default=cls.primary_pool,\n                    tooltip=\"Deadline primary pool, \"\n                            \"applicable for farm rendering\"),\n            TextDef(\"secondaryPool\",\n                    label=\"Secondary Pool\",\n                    default=cls.secondary_pool,\n                    tooltip=\"Deadline secondary pool, \"\n                            \"applicable for farm rendering\")\n        ]\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/collect_publishable_instances.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect instances that should be processed and published on DL.\n\n\"\"\"\nimport os\n\nimport pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\n\nclass CollectDeadlinePublishableInstances(pyblish.api.InstancePlugin):\n    \"\"\"Collect instances that should be processed and published on DL.\n\n    Some long running publishes (not just renders) could be offloaded to DL,\n    this plugin compares theirs name against env variable, marks only\n    publishable by farm.\n\n    Triggered only when running only in headless mode, eg on a farm.\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.499\n    label = \"Collect Deadline Publishable Instance\"\n    targets = [\"remote\"]\n\n    def process(self, instance):\n        self.log.debug(\"CollectDeadlinePublishableInstances\")\n        publish_inst = os.environ.get(\"OPENPYPE_PUBLISH_SUBSET\", '')\n        if not publish_inst:\n            raise PublishValidationError(\"OPENPYPE_PUBLISH_SUBSET env var \"\n                                         \"required for remote publishing\")\n\n        subset_name = instance.data[\"subset\"]\n        if subset_name == publish_inst:\n            self.log.debug(\"Publish {}\".format(subset_name))\n            instance.data[\"publish\"] = True\n            instance.data[\"farm\"] = False\n        else:\n            self.log.debug(\"Skipping {}\".format(subset_name))\n            instance.data[\"publish\"] = False\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n    <error id=\"main\">\n        <title>Deadline Pools</title>\n        <description>\n## Invalid Deadline pools found\n\nConfigured pools don't match available pools in Deadline.\n\n### How to repair?\n\nIf your instance had deadline pools set on creation, remove or\nchange them.\n\nIn other cases inform admin to change them in Settings.\n\nAvailable deadline pools:\n\n{pools_str}\n\n        </description>\n        <detail>\n### __Detailed Info__\n\nThis error is shown when a configured pool is not available on Deadline. It\ncan happen when publishing old workfiles which were created with previous\ndeadline pools, or someone changed the available pools in Deadline,\nbut didn't modify Openpype Settings to match the changes.\n        </detail>\n    </error>\n</root>"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py",
    "content": "import os\nimport attr\nimport getpass\nimport pyblish.api\nfrom datetime import datetime\n\nfrom openpype.lib import (\n    env_value_to_bool,\n    collect_frames,\n)\nfrom openpype.pipeline import legacy_io\nfrom openpype_modules.deadline import abstract_submit_deadline\nfrom openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo\nfrom openpype.tests.lib import is_in_tests\nfrom openpype.lib import is_running_from_build\n\n\n@attr.s\nclass DeadlinePluginInfo():\n    Comp = attr.ib(default=None)\n    SceneFile = attr.ib(default=None)\n    OutputFilePath = attr.ib(default=None)\n    Output = attr.ib(default=None)\n    StartupDirectory = attr.ib(default=None)\n    Arguments = attr.ib(default=None)\n    ProjectPath = attr.ib(default=None)\n    AWSAssetFile0 = attr.ib(default=None)\n    Version = attr.ib(default=None)\n    MultiProcess = attr.ib(default=None)\n\n\nclass AfterEffectsSubmitDeadline(\n    abstract_submit_deadline.AbstractSubmitDeadline\n):\n\n    label = \"Submit AE to Deadline\"\n    order = pyblish.api.IntegratorOrder + 0.1\n    hosts = [\"aftereffects\"]\n    families = [\"render.farm\"]  # cannot be \"render' as that is integrated\n    use_published = True\n    targets = [\"local\"]\n\n    priority = 50\n    chunk_size = 1000000\n    group = None\n    department = None\n    multiprocess = True\n\n    def get_job_info(self):\n        dln_job_info = DeadlineJobInfo(Plugin=\"AfterEffects\")\n\n        context = self._instance.context\n\n        batch_name = os.path.basename(self._instance.data[\"source\"])\n        if is_in_tests():\n            batch_name += datetime.now().strftime(\"%d%m%Y%H%M%S\")\n        dln_job_info.Name = self._instance.data[\"name\"]\n        dln_job_info.BatchName = batch_name\n        dln_job_info.Plugin = \"AfterEffects\"\n        dln_job_info.UserName = context.data.get(\n            \"deadlineUser\", getpass.getuser())\n        if self._instance.data[\"frameEnd\"] > self._instance.data[\"frameStart\"]:\n            # Deadline requires integers in frame range\n            frame_range = \"{}-{}\".format(\n                int(round(self._instance.data[\"frameStart\"])),\n                int(round(self._instance.data[\"frameEnd\"])))\n            dln_job_info.Frames = frame_range\n\n        dln_job_info.Priority = self.priority\n        dln_job_info.Pool = self._instance.data.get(\"primaryPool\")\n        dln_job_info.SecondaryPool = self._instance.data.get(\"secondaryPool\")\n        dln_job_info.Group = self.group\n        dln_job_info.Department = self.department\n        dln_job_info.ChunkSize = self.chunk_size\n        dln_job_info.OutputFilename += \\\n            os.path.basename(self._instance.data[\"expectedFiles\"][0])\n        dln_job_info.OutputDirectory += \\\n            os.path.dirname(self._instance.data[\"expectedFiles\"][0])\n        dln_job_info.JobDelay = \"00:00:00\"\n\n        keys = [\n            \"FTRACK_API_KEY\",\n            \"FTRACK_API_USER\",\n            \"FTRACK_SERVER\",\n            \"AVALON_DB\",\n            \"AVALON_PROJECT\",\n            \"AVALON_ASSET\",\n            \"AVALON_TASK\",\n            \"AVALON_APP_NAME\",\n            \"OPENPYPE_DEV\",\n            \"OPENPYPE_LOG_NO_COLORS\",\n            \"IS_TEST\"\n        ]\n\n        # Add OpenPype version if we are running from build.\n        if is_running_from_build():\n            keys.append(\"OPENPYPE_VERSION\")\n\n        # Add mongo url if it's enabled\n        if self._instance.context.data.get(\"deadlinePassMongoUrl\"):\n            keys.append(\"OPENPYPE_MONGO\")\n\n        environment = dict({key: os.environ[key] for key in keys\n                            if key in os.environ}, **legacy_io.Session)\n        for key in keys:\n            value = environment.get(key)\n            if value:\n                dln_job_info.EnvironmentKeyValue[key] = value\n\n        # to recognize render jobs\n        dln_job_info.add_render_job_env_var()\n\n        return dln_job_info\n\n    def get_plugin_info(self):\n        deadline_plugin_info = DeadlinePluginInfo()\n\n        render_path = self._instance.data[\"expectedFiles\"][0]\n\n        file_name, frame = list(collect_frames([render_path]).items())[0]\n        if frame:\n            # replace frame ('000001') with Deadline's required '[#######]'\n            # expects filename in format project_asset_subset_version.FRAME.ext\n            render_dir = os.path.dirname(render_path)\n            file_name = os.path.basename(render_path)\n            hashed = '[{}]'.format(len(frame) * \"#\")\n            file_name = file_name.replace(frame, hashed)\n            render_path = os.path.join(render_dir, file_name)\n\n        deadline_plugin_info.Comp = self._instance.data[\"comp_name\"]\n        deadline_plugin_info.Version = self._instance.data[\"app_version\"]\n        # must be here because of DL AE plugin\n        # added override of multiprocess by env var, if shouldn't be used for\n        # some app variant use MULTIPROCESS:false in Settings, default is True\n        env_multi = env_value_to_bool(\"MULTIPROCESS\", default=True)\n        deadline_plugin_info.MultiProcess = env_multi and self.multiprocess\n        deadline_plugin_info.SceneFile = self.scene_path\n        deadline_plugin_info.Output = render_path.replace(\"\\\\\", \"/\")\n\n        return attr.asdict(deadline_plugin_info)\n\n    def from_published_scene(self):\n        \"\"\" Do not overwrite expected files.\n\n            Use published is set to True, so rendering will be triggered\n            from published scene (in 'publish' folder). Default implementation\n            of abstract class renames expected (eg. rendered) files accordingly\n            which is not needed here.\n        \"\"\"\n        return super().from_published_scene(False)\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_blender_deadline.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Submitting render job to Deadline.\"\"\"\n\nimport os\nimport getpass\nimport attr\nfrom datetime import datetime\n\nfrom openpype.lib import (\n    is_running_from_build,\n    BoolDef,\n    NumberDef,\n    TextDef,\n)\nfrom openpype.pipeline import legacy_io\nfrom openpype.pipeline.publish import OpenPypePyblishPluginMixin\nfrom openpype.pipeline.farm.tools import iter_expected_files\nfrom openpype.tests.lib import is_in_tests\n\nfrom openpype_modules.deadline import abstract_submit_deadline\nfrom openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo\n\n\n@attr.s\nclass BlenderPluginInfo():\n    SceneFile = attr.ib(default=None)   # Input\n    Version = attr.ib(default=None)  # Mandatory for Deadline\n    SaveFile = attr.ib(default=True)\n\n\nclass BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,\n                            OpenPypePyblishPluginMixin):\n    label = \"Submit Render to Deadline\"\n    hosts = [\"blender\"]\n    families = [\"render\"]\n\n    use_published = True\n    priority = 50\n    chunk_size = 1\n    jobInfo = {}\n    pluginInfo = {}\n    group = None\n    job_delay = \"00:00:00:00\"\n\n    def get_job_info(self):\n        job_info = DeadlineJobInfo(Plugin=\"Blender\")\n\n        job_info.update(self.jobInfo)\n\n        instance = self._instance\n        context = instance.context\n\n        # Always use the original work file name for the Job name even when\n        # rendering is done from the published Work File. The original work\n        # file name is clearer because it can also have subversion strings,\n        # etc. which are stripped for the published file.\n        src_filepath = context.data[\"currentFile\"]\n        src_filename = os.path.basename(src_filepath)\n\n        if is_in_tests():\n            src_filename += datetime.now().strftime(\"%d%m%Y%H%M%S\")\n\n        job_info.Name = f\"{src_filename} - {instance.name}\"\n        job_info.BatchName = src_filename\n        instance.data.get(\"blenderRenderPlugin\", \"Blender\")\n        job_info.UserName = context.data.get(\"deadlineUser\", getpass.getuser())\n\n        # Deadline requires integers in frame range\n        frames = \"{start}-{end}x{step}\".format(\n            start=int(instance.data[\"frameStartHandle\"]),\n            end=int(instance.data[\"frameEndHandle\"]),\n            step=int(instance.data[\"byFrameStep\"]),\n        )\n        job_info.Frames = frames\n\n        job_info.Pool = instance.data.get(\"primaryPool\")\n        job_info.SecondaryPool = instance.data.get(\"secondaryPool\")\n        job_info.Comment = instance.data.get(\"comment\")\n\n        if self.group != \"none\" and self.group:\n            job_info.Group = self.group\n\n        attr_values = self.get_attr_values_from_data(instance.data)\n        render_globals = instance.data.setdefault(\"renderGlobals\", {})\n        machine_list = attr_values.get(\"machineList\", \"\")\n        if machine_list:\n            if attr_values.get(\"whitelist\", True):\n                machine_list_key = \"Whitelist\"\n            else:\n                machine_list_key = \"Blacklist\"\n            render_globals[machine_list_key] = machine_list\n\n        job_info.ChunkSize = attr_values.get(\"chunkSize\", self.chunk_size)\n        job_info.Priority = attr_values.get(\"priority\", self.priority)\n        job_info.ScheduledType = \"Once\"\n        job_info.JobDelay = attr_values.get(\"job_delay\", self.job_delay)\n\n        # Add options from RenderGlobals\n        render_globals = instance.data.get(\"renderGlobals\", {})\n        job_info.update(render_globals)\n\n        keys = [\n            \"FTRACK_API_KEY\",\n            \"FTRACK_API_USER\",\n            \"FTRACK_SERVER\",\n            \"OPENPYPE_SG_USER\",\n            \"AVALON_DB\",\n            \"AVALON_PROJECT\",\n            \"AVALON_ASSET\",\n            \"AVALON_TASK\",\n            \"AVALON_APP_NAME\",\n            \"OPENPYPE_DEV\"\n            \"IS_TEST\"\n        ]\n\n        # Add OpenPype version if we are running from build.\n        if is_running_from_build():\n            keys.append(\"OPENPYPE_VERSION\")\n\n        # Add mongo url if it's enabled\n        if self._instance.context.data.get(\"deadlinePassMongoUrl\"):\n            keys.append(\"OPENPYPE_MONGO\")\n\n        environment = dict({key: os.environ[key] for key in keys\n                            if key in os.environ}, **legacy_io.Session)\n\n        for key in keys:\n            value = environment.get(key)\n            if not value:\n                continue\n            job_info.EnvironmentKeyValue[key] = value\n\n        # to recognize job from PYPE for turning Event On/Off\n        job_info.add_render_job_env_var()\n        job_info.EnvironmentKeyValue[\"OPENPYPE_LOG_NO_COLORS\"] = \"1\"\n\n        # Adding file dependencies.\n        if self.asset_dependencies:\n            dependencies = instance.context.data[\"fileDependencies\"]\n            for dependency in dependencies:\n                job_info.AssetDependency += dependency\n\n        # Add list of expected files to job\n        # ---------------------------------\n        exp = instance.data.get(\"expectedFiles\")\n        for filepath in iter_expected_files(exp):\n            job_info.OutputDirectory += os.path.dirname(filepath)\n            job_info.OutputFilename += os.path.basename(filepath)\n\n        return job_info\n\n    def get_plugin_info(self):\n        # Not all hosts can import this module.\n        import bpy\n\n        plugin_info = BlenderPluginInfo(\n            SceneFile=self.scene_path,\n            Version=bpy.app.version_string,\n            SaveFile=True,\n        )\n\n        plugin_payload = attr.asdict(plugin_info)\n\n        # Patching with pluginInfo from settings\n        for key, value in self.pluginInfo.items():\n            plugin_payload[key] = value\n\n        return plugin_payload\n\n    def process_submission(self):\n        instance = self._instance\n\n        expected_files = instance.data[\"expectedFiles\"]\n        if not expected_files:\n            raise RuntimeError(\"No Render Elements found!\")\n\n        first_file = next(iter_expected_files(expected_files))\n        output_dir = os.path.dirname(first_file)\n        instance.data[\"outputDir\"] = output_dir\n        instance.data[\"toBeRenderedOn\"] = \"deadline\"\n\n        payload = self.assemble_payload()\n        return self.submit(payload)\n\n    def from_published_scene(self):\n        \"\"\"\n        This is needed to set the correct path for the json metadata. Because\n        the rendering path is set in the blend file during the collection,\n        and the path is adjusted to use the published scene, this ensures that\n        the metadata and the rendered files are in the same location.\n        \"\"\"\n        return super().from_published_scene(False)\n\n    @classmethod\n    def get_attribute_defs(cls):\n        defs = super(BlenderSubmitDeadline, cls).get_attribute_defs()\n        defs.extend([\n            BoolDef(\"use_published\",\n                    default=cls.use_published,\n                    label=\"Use Published Scene\"),\n\n            NumberDef(\"priority\",\n                      minimum=1,\n                      maximum=250,\n                      decimals=0,\n                      default=cls.priority,\n                      label=\"Priority\"),\n\n            NumberDef(\"chunkSize\",\n                      minimum=1,\n                      maximum=50,\n                      decimals=0,\n                      default=cls.chunk_size,\n                      label=\"Frame Per Task\"),\n\n            TextDef(\"group\",\n                    default=cls.group,\n                    label=\"Group Name\"),\n\n            TextDef(\"job_delay\",\n                    default=cls.job_delay,\n                    label=\"Job Delay\",\n                    placeholder=\"dd:hh:mm:ss\",\n                    tooltip=\"Delay the job by the specified amount of time. \"\n                            \"Timecode: dd:hh:mm:ss.\"),\n        ])\n\n        return defs\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py",
    "content": "import os\nimport re\nimport json\nimport getpass\nimport requests\nimport pyblish.api\n\n\nclass CelactionSubmitDeadline(pyblish.api.InstancePlugin):\n    \"\"\"Submit CelAction2D scene to Deadline\n\n    Renders are submitted to a Deadline Web Service.\n\n    \"\"\"\n\n    label = \"Submit CelAction to Deadline\"\n    order = pyblish.api.IntegratorOrder + 0.1\n    hosts = [\"celaction\"]\n    families = [\"render.farm\"]\n\n    deadline_department = \"\"\n    deadline_priority = 50\n    deadline_pool = \"\"\n    deadline_pool_secondary = \"\"\n    deadline_group = \"\"\n    deadline_chunk_size = 1\n    deadline_job_delay = \"00:00:08:00\"\n\n    def process(self, instance):\n\n        context = instance.context\n\n        # get default deadline webservice url from deadline module\n        deadline_url = instance.context.data[\"defaultDeadline\"]\n        # if custom one is set in instance, use that\n        if instance.data.get(\"deadlineUrl\"):\n            deadline_url = instance.data.get(\"deadlineUrl\")\n        assert deadline_url, \"Requires Deadline Webservice URL\"\n\n        self.deadline_url = \"{}/api/jobs\".format(deadline_url)\n        self._comment = instance.data[\"comment\"]\n        self._deadline_user = context.data.get(\n            \"deadlineUser\", getpass.getuser())\n        self._frame_start = int(instance.data[\"frameStart\"])\n        self._frame_end = int(instance.data[\"frameEnd\"])\n\n        # get output path\n        render_path = instance.data['path']\n        script_path = context.data[\"currentFile\"]\n\n        response = self.payload_submit(instance,\n                                       script_path,\n                                       render_path\n                                       )\n        # Store output dir for unified publisher (filesequence)\n        instance.data[\"deadlineSubmissionJob\"] = response.json()\n\n        instance.data[\"outputDir\"] = os.path.dirname(\n            render_path).replace(\"\\\\\", \"/\")\n\n        instance.data[\"publishJobState\"] = \"Suspended\"\n\n        # adding 2d render specific family for version identification in Loader\n        instance.data[\"families\"] = [\"render2d\"]\n\n    def payload_submit(self,\n                       instance,\n                       script_path,\n                       render_path\n                       ):\n        resolution_width = instance.data[\"resolutionWidth\"]\n        resolution_height = instance.data[\"resolutionHeight\"]\n        render_dir = os.path.normpath(os.path.dirname(render_path))\n        render_path = os.path.normpath(render_path)\n        script_name = os.path.basename(script_path)\n\n        for item in instance.context:\n            if \"workfile\" in item.data[\"family\"]:\n                msg = \"Workfile (scene) must be published along\"\n                assert item.data[\"publish\"] is True, msg\n\n                template_data = item.data.get(\"anatomyData\")\n                rep = item.data.get(\"representations\")[0].get(\"name\")\n                template_data[\"representation\"] = rep\n                template_data[\"ext\"] = rep\n                template_data[\"comment\"] = None\n                anatomy_filled = instance.context.data[\"anatomy\"].format(\n                    template_data)\n                template_filled = anatomy_filled[\"publish\"][\"path\"]\n                script_path = os.path.normpath(template_filled)\n\n                self.log.info(\n                    \"Using published scene for render {}\".format(script_path)\n                )\n\n        jobname = \"%s - %s\" % (script_name, instance.name)\n\n        output_filename_0 = self.preview_fname(render_path)\n\n        try:\n            # Ensure render folder exists\n            os.makedirs(render_dir)\n        except OSError:\n            pass\n\n        # define chunk and priority\n        chunk_size = instance.context.data.get(\"chunk\")\n        if not chunk_size:\n            chunk_size = self.deadline_chunk_size\n\n        # search for %02d pattern in name, and padding number\n        search_results = re.search(r\"(%0)(\\d)(d)[._]\", render_path).groups()\n        split_patern = \"\".join(search_results)\n        padding_number = int(search_results[1])\n\n        args = [\n            f\"<QUOTE>{script_path}<QUOTE>\",\n            \"-a\",\n            \"-16\",\n            \"-s <STARTFRAME>\",\n            \"-e <ENDFRAME>\",\n            f\"-d <QUOTE>{render_dir}<QUOTE>\",\n            f\"-x {resolution_width}\",\n            f\"-y {resolution_height}\",\n            f\"-r <QUOTE>{render_path.replace(split_patern, '')}<QUOTE>\",\n            f\"-= AbsoluteFrameNumber=on -= PadDigits={padding_number}\",\n            \"-= ClearAttachment=on\",\n        ]\n\n        payload = {\n            \"JobInfo\": {\n                # Job name, as seen in Monitor\n                \"Name\": jobname,\n\n                # plugin definition\n                \"Plugin\": \"CelAction\",\n\n                # Top-level group name\n                \"BatchName\": script_name,\n\n                # Arbitrary username, for visualisation in Monitor\n                \"UserName\": self._deadline_user,\n\n                \"Department\": self.deadline_department,\n                \"Priority\": self.deadline_priority,\n\n                \"Group\": self.deadline_group,\n                \"Pool\": self.deadline_pool,\n                \"SecondaryPool\": self.deadline_pool_secondary,\n                \"ChunkSize\": chunk_size,\n\n                \"Frames\": f\"{self._frame_start}-{self._frame_end}\",\n                \"Comment\": self._comment,\n\n                # Optional, enable double-click to preview rendered\n                # frames from Deadline Monitor\n                \"OutputFilename0\": output_filename_0.replace(\"\\\\\", \"/\"),\n\n                # # Asset dependency to wait for at least\n                # the scene file to sync.\n                # \"AssetDependency0\": script_path\n                \"ScheduledType\": \"Once\",\n                \"JobDelay\": self.deadline_job_delay\n            },\n            \"PluginInfo\": {\n                # Input\n                \"SceneFile\": script_path,\n\n                # Output directory\n                \"OutputFilePath\": render_dir.replace(\"\\\\\", \"/\"),\n\n                # Plugin attributes\n                \"StartupDirectory\": \"\",\n                \"Arguments\": \" \".join(args),\n\n                # Resolve relative references\n                \"ProjectPath\": script_path,\n                \"AWSAssetFile0\": render_path,\n            },\n\n            # Mandatory for Deadline, may be empty\n            \"AuxFiles\": []\n        }\n\n        plugin = payload[\"JobInfo\"][\"Plugin\"]\n        self.log.debug(\"using render plugin : {}\".format(plugin))\n\n        self.log.debug(\"Submitting..\")\n        self.log.debug(json.dumps(payload, indent=4, sort_keys=True))\n\n        # adding expectied files to instance.data\n        self.expected_files(instance, render_path)\n        self.log.debug(\"__ expectedFiles: `{}`\".format(\n            instance.data[\"expectedFiles\"]))\n\n        response = requests.post(self.deadline_url, json=payload)\n\n        if not response.ok:\n            self.log.error(\n                \"Submission failed! [{}] {}\".format(\n                    response.status_code, response.content))\n            self.log.debug(payload)\n            raise SystemExit(response.text)\n\n        return response\n\n    def preflight_check(self, instance):\n        \"\"\"Ensure the startFrame, endFrame and byFrameStep are integers\"\"\"\n\n        for key in (\"frameStart\", \"frameEnd\"):\n            value = instance.data[key]\n\n            if int(value) == value:\n                continue\n\n            self.log.warning(\n                \"%f=%d was rounded off to nearest integer\"\n                % (value, int(value))\n            )\n\n    def preview_fname(self, path):\n        \"\"\"Return output file path with #### for padding.\n\n        Deadline requires the path to be formatted with # in place of numbers.\n        For example `/path/to/render.####.png`\n\n        Args:\n            path (str): path to rendered images\n\n        Returns:\n            str\n\n        \"\"\"\n        self.log.debug(\"_ path: `{}`\".format(path))\n        if \"%\" in path:\n            search_results = re.search(r\"[._](%0)(\\d)(d)[._]\", path).groups()\n            split_patern = \"\".join(search_results)\n            split_path = path.split(split_patern)\n            hashes = \"#\" * int(search_results[1])\n            return \"\".join([split_path[0], hashes, split_path[-1]])\n\n        self.log.debug(\"_ path: `{}`\".format(path))\n        return path\n\n    def expected_files(self, instance, filepath):\n        \"\"\" Create expected files in instance data\n        \"\"\"\n        if not instance.data.get(\"expectedFiles\"):\n            instance.data[\"expectedFiles\"] = []\n\n        dirpath = os.path.dirname(filepath)\n        filename = os.path.basename(filepath)\n\n        if \"#\" in filename:\n            pparts = filename.split(\"#\")\n            padding = \"%0{}d\".format(len(pparts) - 1)\n            filename = pparts[0] + padding + pparts[-1]\n\n        if \"%\" not in filename:\n            instance.data[\"expectedFiles\"].append(filepath)\n            return\n\n        for i in range(self._frame_start, (self._frame_end + 1)):\n            instance.data[\"expectedFiles\"].append(\n                os.path.join(dirpath, (filename % i)).replace(\"\\\\\", \"/\")\n            )\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py",
    "content": "import os\nimport json\nimport getpass\n\nimport requests\n\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import legacy_io\nfrom openpype.pipeline.publish import (\n    OpenPypePyblishPluginMixin\n)\nfrom openpype.lib import (\n    BoolDef,\n    NumberDef,\n    is_running_from_build\n)\n\n\nclass FusionSubmitDeadline(\n    pyblish.api.InstancePlugin,\n    OpenPypePyblishPluginMixin\n):\n    \"\"\"Submit current Comp to Deadline\n\n    Renders are submitted to a Deadline Web Service as\n    supplied via settings key \"DEADLINE_REST_URL\".\n\n    \"\"\"\n\n    label = \"Submit Fusion to Deadline\"\n    order = pyblish.api.IntegratorOrder\n    hosts = [\"fusion\"]\n    families = [\"render\"]\n    targets = [\"local\"]\n\n    # presets\n    plugin = None\n\n    priority = 50\n    chunk_size = 1\n    concurrent_tasks = 1\n    group = \"\"\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            NumberDef(\n                \"priority\",\n                label=\"Priority\",\n                default=cls.priority,\n                decimals=0\n            ),\n            NumberDef(\n                \"chunk\",\n                label=\"Frames Per Task\",\n                default=cls.chunk_size,\n                decimals=0,\n                minimum=1,\n                maximum=1000\n            ),\n            NumberDef(\n                \"concurrency\",\n                label=\"Concurrency\",\n                default=cls.concurrent_tasks,\n                decimals=0,\n                minimum=1,\n                maximum=10\n            ),\n            BoolDef(\n                \"suspend_publish\",\n                default=False,\n                label=\"Suspend publish\"\n            )\n        ]\n\n    def process(self, instance):\n        if not instance.data.get(\"farm\"):\n            self.log.debug(\"Skipping local instance.\")\n            return\n\n        attribute_values = self.get_attr_values_from_data(\n            instance.data)\n\n        # add suspend_publish attributeValue to instance data\n        instance.data[\"suspend_publish\"] = attribute_values[\n            \"suspend_publish\"]\n\n        context = instance.context\n\n        key = \"__hasRun{}\".format(self.__class__.__name__)\n        if context.data.get(key, False):\n            return\n        else:\n            context.data[key] = True\n\n        from openpype.hosts.fusion.api.lib import get_frame_path\n\n        # get default deadline webservice url from deadline module\n        deadline_url = instance.context.data[\"defaultDeadline\"]\n        # if custom one is set in instance, use that\n        if instance.data.get(\"deadlineUrl\"):\n            deadline_url = instance.data.get(\"deadlineUrl\")\n        assert deadline_url, \"Requires Deadline Webservice URL\"\n\n        # Collect all saver instances in context that are to be rendered\n        saver_instances = []\n        for instance in context:\n            if instance.data[\"family\"] != \"render\":\n                # Allow only saver family instances\n                continue\n\n            if not instance.data.get(\"publish\", True):\n                # Skip inactive instances\n                continue\n\n            self.log.debug(instance.data[\"name\"])\n            saver_instances.append(instance)\n\n        if not saver_instances:\n            raise RuntimeError(\"No instances found for Deadline submission\")\n\n        comment = instance.data.get(\"comment\", \"\")\n        deadline_user = context.data.get(\"deadlineUser\", getpass.getuser())\n\n        script_path = context.data[\"currentFile\"]\n\n        for item in context:\n            if \"workfile\" in item.data[\"families\"]:\n                msg = \"Workfile (scene) must be published along\"\n                assert item.data[\"publish\"] is True, msg\n\n                template_data = item.data.get(\"anatomyData\")\n                rep = item.data.get(\"representations\")[0].get(\"name\")\n                template_data[\"representation\"] = rep\n                template_data[\"ext\"] = rep\n                template_data[\"comment\"] = None\n                anatomy_filled = context.data[\"anatomy\"].format(template_data)\n                template_filled = anatomy_filled[\"publish\"][\"path\"]\n                script_path = os.path.normpath(template_filled)\n\n                self.log.info(\n                    \"Using published scene for render {}\".format(script_path)\n                )\n\n        filename = os.path.basename(script_path)\n\n        # Documentation for keys available at:\n        # https://docs.thinkboxsoftware.com\n        #    /products/deadline/8.0/1_User%20Manual/manual\n        #    /manual-submission.html#job-info-file-options\n        payload = {\n            \"JobInfo\": {\n                # Top-level group name\n                \"BatchName\": filename,\n\n                # Asset dependency to wait for at least the scene file to sync.\n                \"AssetDependency0\": script_path,\n\n                # Job name, as seen in Monitor\n                \"Name\": filename,\n\n                \"Priority\": attribute_values.get(\n                    \"priority\", self.priority),\n                \"ChunkSize\": attribute_values.get(\n                    \"chunk\", self.chunk_size),\n                \"ConcurrentTasks\": attribute_values.get(\n                    \"concurrency\",\n                    self.concurrent_tasks\n                ),\n\n                # User, as seen in Monitor\n                \"UserName\": deadline_user,\n\n                \"Pool\": instance.data.get(\"primaryPool\"),\n                \"SecondaryPool\": instance.data.get(\"secondaryPool\"),\n                \"Group\": self.group,\n\n                \"Plugin\": self.plugin,\n                \"Frames\": \"{start}-{end}\".format(\n                    start=int(instance.data[\"frameStartHandle\"]),\n                    end=int(instance.data[\"frameEndHandle\"])\n                ),\n\n                \"Comment\": comment,\n            },\n            \"PluginInfo\": {\n                # Input\n                \"FlowFile\": script_path,\n\n                # Mandatory for Deadline\n                \"Version\": str(instance.data[\"app_version\"]),\n\n                # Render in high quality\n                \"HighQuality\": True,\n\n                # Whether saver output should be checked after rendering\n                # is complete\n                \"CheckOutput\": True,\n\n                # Proxy: higher numbers smaller images for faster test renders\n                # 1 = no proxy quality\n                \"Proxy\": 1\n            },\n\n            # Mandatory for Deadline, may be empty\n            \"AuxFiles\": []\n        }\n\n        # Enable going to rendered frames from Deadline Monitor\n        for index, instance in enumerate(saver_instances):\n            head, padding, tail = get_frame_path(\n                instance.data[\"expectedFiles\"][0]\n            )\n            path = \"{}{}{}\".format(head, \"#\" * padding, tail)\n            folder, filename = os.path.split(path)\n            payload[\"JobInfo\"][\"OutputDirectory%d\" % index] = folder\n            payload[\"JobInfo\"][\"OutputFilename%d\" % index] = filename\n\n        # Include critical variables with submission\n        keys = [\n            \"FTRACK_API_KEY\",\n            \"FTRACK_API_USER\",\n            \"FTRACK_SERVER\",\n            \"AVALON_DB\",\n            \"AVALON_PROJECT\",\n            \"AVALON_ASSET\",\n            \"AVALON_TASK\",\n            \"AVALON_APP_NAME\",\n            \"OPENPYPE_DEV\",\n            \"OPENPYPE_LOG_NO_COLORS\",\n            \"IS_TEST\"\n        ]\n\n        # Add OpenPype version if we are running from build.\n        if is_running_from_build():\n            keys.append(\"OPENPYPE_VERSION\")\n\n        environment = dict({key: os.environ[key] for key in keys\n                            if key in os.environ}, **legacy_io.Session)\n\n        # to recognize render jobs\n        if AYON_SERVER_ENABLED:\n            environment[\"AYON_BUNDLE_NAME\"] = os.environ[\"AYON_BUNDLE_NAME\"]\n            render_job_label = \"AYON_RENDER_JOB\"\n        else:\n            render_job_label = \"OPENPYPE_RENDER_JOB\"\n\n        environment[render_job_label] = \"1\"\n\n        payload[\"JobInfo\"].update({\n            \"EnvironmentKeyValue%d\" % index: \"{key}={value}\".format(\n                key=key,\n                value=environment[key]\n            ) for index, key in enumerate(environment)\n        })\n\n        self.log.debug(\"Submitting..\")\n        self.log.debug(json.dumps(payload, indent=4, sort_keys=True))\n\n        # E.g. http://192.168.0.1:8082/api/jobs\n        url = \"{}/api/jobs\".format(deadline_url)\n        response = requests.post(url, json=payload)\n        if not response.ok:\n            raise Exception(response.text)\n\n        # Store the response for dependent job submission plug-ins\n        for instance in saver_instances:\n            instance.data[\"deadlineSubmissionJob\"] = response.json()\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Submitting render job to Deadline.\"\"\"\nimport os\nfrom pathlib import Path\nfrom collections import OrderedDict\nfrom zipfile import ZipFile, is_zipfile\nimport re\nfrom datetime import datetime\n\nimport attr\nimport pyblish.api\n\nfrom openpype.pipeline import legacy_io\nfrom openpype_modules.deadline import abstract_submit_deadline\nfrom openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo\nfrom openpype.tests.lib import is_in_tests\nfrom openpype.lib import is_running_from_build\n\n\nclass _ZipFile(ZipFile):\n    \"\"\"Extended check for windows invalid characters.\"\"\"\n\n    # this is extending default zipfile table for few invalid characters\n    # that can come from Mac\n    _windows_illegal_characters = \":<>|\\\"?*\\r\\n\\x00\"\n    _windows_illegal_name_trans_table = str.maketrans(\n        _windows_illegal_characters,\n        \"_\" * len(_windows_illegal_characters)\n    )\n\n\n@attr.s\nclass PluginInfo(object):\n    \"\"\"Plugin info structure for Harmony Deadline plugin.\"\"\"\n\n    SceneFile = attr.ib()\n    # Harmony version\n    Version = attr.ib()\n\n    Camera = attr.ib(default=\"\")\n    FieldOfView = attr.ib(default=41.11)\n    IsDatabase = attr.ib(default=False)\n    ResolutionX = attr.ib(default=1920)\n    ResolutionY = attr.ib(default=1080)\n\n    # Resolution name preset, default\n    UsingResPreset = attr.ib(default=False)\n    ResolutionName = attr.ib(default=\"HDTV_1080p24\")\n\n    PreRenderInlineScript = attr.ib(default=None)\n\n    # --------------------------------------------------\n    _outputNode = attr.ib(factory=list)\n\n    @property\n    def OutputNode(self):  # noqa: N802\n        \"\"\"Return all output nodes formatted for Deadline.\n\n        Returns:\n            dict: as `{'Output0Node', 'Top/renderFarmDefault'}`\n\n        \"\"\"\n        out = {}\n        for index, v in enumerate(self._outputNode):\n            out[\"Output{}Node\".format(index)] = v\n        return out\n\n    @OutputNode.setter\n    def OutputNode(self, val):  # noqa: N802\n        self._outputNode.append(val)\n\n    # --------------------------------------------------\n    _outputType = attr.ib(factory=list)\n\n    @property\n    def OutputType(self):  # noqa: N802\n        \"\"\"Return output nodes type formatted for Deadline.\n\n        Returns:\n            dict: as `{'Output0Type', 'Image'}`\n\n        \"\"\"\n        out = {}\n        for index, v in enumerate(self._outputType):\n            out[\"Output{}Type\".format(index)] = v\n        return out\n\n    @OutputType.setter\n    def OutputType(self, val):  # noqa: N802\n        self._outputType.append(val)\n\n    # --------------------------------------------------\n    _outputLeadingZero = attr.ib(factory=list)\n\n    @property\n    def OutputLeadingZero(self):  # noqa: N802\n        \"\"\"Return output nodes type formatted for Deadline.\n\n        Returns:\n            dict: as `{'Output0LeadingZero', '3'}`\n\n        \"\"\"\n        out = {}\n        for index, v in enumerate(self._outputLeadingZero):\n            out[\"Output{}LeadingZero\".format(index)] = v\n        return out\n\n    @OutputLeadingZero.setter\n    def OutputLeadingZero(self, val):  # noqa: N802\n        self._outputLeadingZero.append(val)\n\n    # --------------------------------------------------\n    _outputFormat = attr.ib(factory=list)\n\n    @property\n    def OutputFormat(self):  # noqa: N802\n        \"\"\"Return output nodes format formatted for Deadline.\n\n        Returns:\n            dict: as `{'Output0Type', 'PNG4'}`\n\n        \"\"\"\n        out = {}\n        for index, v in enumerate(self._outputFormat):\n            out[\"Output{}Format\".format(index)] = v\n        return out\n\n    @OutputFormat.setter\n    def OutputFormat(self, val):  # noqa: N802\n        self._outputFormat.append(val)\n\n    # --------------------------------------------------\n    _outputStartFrame = attr.ib(factory=list)\n\n    @property\n    def OutputStartFrame(self):  # noqa: N802\n        \"\"\"Return start frame for output nodes formatted for Deadline.\n\n        Returns:\n            dict: as `{'Output0StartFrame', '1'}`\n\n        \"\"\"\n        out = {}\n        for index, v in enumerate(self._outputStartFrame):\n            out[\"Output{}StartFrame\".format(index)] = v\n        return out\n\n    @OutputStartFrame.setter\n    def OutputStartFrame(self, val):  # noqa: N802\n        self._outputStartFrame.append(val)\n\n    # --------------------------------------------------\n    _outputPath = attr.ib(factory=list)\n\n    @property\n    def OutputPath(self):  # noqa: N802\n        \"\"\"Return output paths for nodes formatted for Deadline.\n\n        Returns:\n            dict: as `{'Output0Path', '/output/path'}`\n\n        \"\"\"\n        out = {}\n        for index, v in enumerate(self._outputPath):\n            out[\"Output{}Path\".format(index)] = v\n        return out\n\n    @OutputPath.setter\n    def OutputPath(self, val):  # noqa: N802\n        self._outputPath.append(val)\n\n    def set_output(self, node, image_format, output,\n                   output_type=\"Image\", zeros=3, start_frame=1):\n        \"\"\"Helper to set output.\n\n        This should be used instead of setting properties individually\n        as so index remain consistent.\n\n        Args:\n            node (str): harmony write node name\n            image_format (str): format of output (PNG4, TIF, ...)\n            output (str): output path\n            output_type (str, optional): \"Image\" or \"Movie\" (not supported).\n            zeros (int, optional): Leading zeros (for 0001 = 3)\n            start_frame (int, optional): Sequence offset.\n\n        \"\"\"\n\n        self.OutputNode = node\n        self.OutputFormat = image_format\n        self.OutputPath = output\n        self.OutputType = output_type\n        self.OutputLeadingZero = zeros\n        self.OutputStartFrame = start_frame\n\n    def serialize(self):\n        \"\"\"Return all data serialized as dictionary.\n\n        Returns:\n            OrderedDict: all serialized data.\n\n        \"\"\"\n        def filter_data(a, v):\n            if a.name.startswith(\"_\"):\n                return False\n            if v is None:\n                return False\n            return True\n\n        serialized = attr.asdict(\n            self, dict_factory=OrderedDict, filter=filter_data)\n        serialized.update(self.OutputNode)\n        serialized.update(self.OutputFormat)\n        serialized.update(self.OutputPath)\n        serialized.update(self.OutputType)\n        serialized.update(self.OutputLeadingZero)\n        serialized.update(self.OutputStartFrame)\n\n        return serialized\n\n\nclass HarmonySubmitDeadline(\n    abstract_submit_deadline.AbstractSubmitDeadline\n):\n    \"\"\"Submit render write of Harmony scene to Deadline.\n\n    Renders are submitted to a Deadline Web Service as\n    supplied via the environment variable ``DEADLINE_REST_URL``.\n\n    Note:\n        If Deadline configuration is not detected, this plugin will\n        be disabled.\n\n    Attributes:\n        use_published (bool): Use published scene to render instead of the\n            one in work area.\n\n    \"\"\"\n\n    label = \"Submit to Deadline\"\n    order = pyblish.api.IntegratorOrder + 0.1\n    hosts = [\"harmony\"]\n    families = [\"render.farm\"]\n    targets = [\"local\"]\n\n    optional = True\n    use_published = False\n    priority = 50\n    chunk_size = 1000000\n    group = \"none\"\n    department = \"\"\n\n    def get_job_info(self):\n        job_info = DeadlineJobInfo(\"Harmony\")\n        job_info.Name = self._instance.data[\"name\"]\n        job_info.Plugin = \"HarmonyOpenPype\"\n        job_info.Frames = \"{}-{}\".format(\n            self._instance.data[\"frameStartHandle\"],\n            self._instance.data[\"frameEndHandle\"]\n        )\n        # for now, get those from presets. Later on it should be\n        # configurable in Harmony UI directly.\n        job_info.Priority = self.priority\n        job_info.Pool = self._instance.data.get(\"primaryPool\")\n        job_info.SecondaryPool = self._instance.data.get(\"secondaryPool\")\n        job_info.ChunkSize = self.chunk_size\n        batch_name = os.path.basename(self._instance.data[\"source\"])\n        if is_in_tests():\n            batch_name += datetime.now().strftime(\"%d%m%Y%H%M%S\")\n        job_info.BatchName = batch_name\n        job_info.Department = self.department\n        job_info.Group = self.group\n\n        keys = [\n            \"FTRACK_API_KEY\",\n            \"FTRACK_API_USER\",\n            \"FTRACK_SERVER\",\n            \"AVALON_DB\",\n            \"AVALON_PROJECT\",\n            \"AVALON_ASSET\",\n            \"AVALON_TASK\",\n            \"AVALON_APP_NAME\",\n            \"OPENPYPE_DEV\",\n            \"OPENPYPE_LOG_NO_COLORS\"\n            \"IS_TEST\"\n        ]\n\n        # Add OpenPype version if we are running from build.\n        if is_running_from_build():\n            keys.append(\"OPENPYPE_VERSION\")\n\n        # Add mongo url if it's enabled\n        if self._instance.context.data.get(\"deadlinePassMongoUrl\"):\n            keys.append(\"OPENPYPE_MONGO\")\n\n        environment = dict({key: os.environ[key] for key in keys\n                            if key in os.environ}, **legacy_io.Session)\n        for key in keys:\n            value = environment.get(key)\n            if value:\n                job_info.EnvironmentKeyValue[key] = value\n\n        # to recognize render jobs\n        job_info.add_render_job_env_var()\n\n        return job_info\n\n    def _unzip_scene_file(self, published_scene: Path) -> Path:\n        \"\"\"Unzip scene zip file to its directory.\n\n        Unzip scene file (if it is zip file) to its current directory and\n        return path to xstage file there. Xstage file is determined by its\n        name.\n\n        Args:\n            published_scene (Path): path to zip file.\n\n        Returns:\n            Path: The path to unzipped xstage.\n        \"\"\"\n        # if not zip, bail out.\n        if \"zip\" not in published_scene.suffix or not is_zipfile(\n            published_scene.as_posix()\n        ):\n            self.log.error(\"Published scene is not in zip.\")\n            self.log.error(published_scene)\n            raise AssertionError(\"invalid scene format\")\n\n        xstage_path = (\n            published_scene.parent\n            / published_scene.stem\n            / f\"{published_scene.stem}.xstage\"\n        )\n        unzip_dir = (published_scene.parent / published_scene.stem)\n        with _ZipFile(published_scene, \"r\") as zip_ref:\n            # UNC path (//?/) added to minimalize risk with extracting\n            # to large file paths\n            zip_ref.extractall(\"//?/\" + str(unzip_dir.as_posix()))\n\n        # find any xstage files in directory, prefer the one with the same name\n        # as directory (plus extension)\n        xstage_files = []\n        for scene in unzip_dir.iterdir():\n            if scene.suffix == \".xstage\":\n                xstage_files.append(scene)\n\n        # there must be at least one (but maybe not more?) xstage file\n        if not xstage_files:\n            self.log.error(\"No xstage files found in zip\")\n            raise AssertionError(\"Invalid scene archive\")\n\n        ideal_scene = False\n        # find the one with the same name as zip. In case there can be more\n        # then one xtage file.\n        for scene in xstage_files:\n            # if /foo/bar/baz.zip == /foo/bar/baz/baz.xstage\n            #             ^^^                     ^^^\n            if scene.stem == published_scene.stem:\n                xstage_path = scene\n                ideal_scene = True\n\n        # but sometimes xstage file has different name then zip - in that case\n        # use that one.\n        if not ideal_scene:\n            xstage_path = xstage_files[0]\n        return xstage_path\n\n    def get_plugin_info(self):\n        # this is path to published scene workfile _ZIP_. Before\n        # rendering, we need to unzip it.\n        published_scene = Path(\n            self.from_published_scene(False))\n        self.log.debug(f\"Processing {published_scene.as_posix()}\")\n        xstage_path = self._unzip_scene_file(published_scene)\n        render_path = xstage_path.parent / \"renders\"\n\n        # for submit_publish job to create .json file in\n        self._instance.data[\"outputDir\"] = render_path\n        new_expected_files = []\n        render_path_str = str(render_path.as_posix())\n        for file in self._instance.data[\"expectedFiles\"]:\n            _file = str(Path(file).as_posix())\n            expected_dir_str = os.path.dirname(_file)\n            new_expected_files.append(\n                _file.replace(expected_dir_str, render_path_str)\n            )\n        audio_file = self._instance.data.get(\"audioFile\")\n        if audio_file:\n            abs_path = xstage_path.parent / audio_file\n            self._instance.context.data[\"audioFile\"] = str(abs_path)\n\n        self._instance.data[\"source\"] = str(published_scene.as_posix())\n        self._instance.data[\"expectedFiles\"] = new_expected_files\n        harmony_plugin_info = PluginInfo(\n            SceneFile=xstage_path.as_posix(),\n            Version=(\n                self._instance.context.data[\"harmonyVersion\"].split(\".\")[0]),\n            FieldOfView=self._instance.context.data[\"FOV\"],\n            ResolutionX=self._instance.data[\"resolutionWidth\"],\n            ResolutionY=self._instance.data[\"resolutionHeight\"]\n        )\n\n        pattern = '[0]{' + str(self._instance.data[\"leadingZeros\"]) + \\\n                  '}1\\.[a-zA-Z]{3}'\n        render_prefix = re.sub(pattern, '',\n                               self._instance.data[\"expectedFiles\"][0])\n        harmony_plugin_info.set_output(\n            self._instance.data[\"setMembers\"][0],\n            self._instance.data[\"outputFormat\"],\n            render_prefix,\n            self._instance.data[\"outputType\"],\n            self._instance.data[\"leadingZeros\"],\n            self._instance.data[\"outputStartFrame\"]\n        )\n\n        all_write_nodes = self._instance.context.data[\"all_write_nodes\"]\n        disable_nodes = []\n        for node in all_write_nodes:\n            # disable all other write nodes\n            if node != self._instance.data[\"setMembers\"][0]:\n                disable_nodes.append(\"node.setEnable('{}', false)\"\n                                     .format(node))\n        harmony_plugin_info.PreRenderInlineScript = ';'.join(disable_nodes)\n\n        return harmony_plugin_info.serialize()\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py",
    "content": "import os\nimport getpass\nfrom datetime import datetime\n\nimport attr\nimport pyblish.api\nfrom openpype.lib import (\n    TextDef,\n    NumberDef,\n)\nfrom openpype.pipeline import (\n    legacy_io,\n    OpenPypePyblishPluginMixin\n)\nfrom openpype.tests.lib import is_in_tests\nfrom openpype.lib import is_running_from_build\nfrom openpype_modules.deadline import abstract_submit_deadline\nfrom openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo\n\n\n@attr.s\nclass HoudiniPluginInfo(object):\n    Build = attr.ib(default=None)\n    IgnoreInputs = attr.ib(default=True)\n    ScriptJob = attr.ib(default=True)\n    SceneFile = attr.ib(default=None)   # Input\n    SaveFile = attr.ib(default=True)\n    ScriptFilename = attr.ib(default=None)\n    OutputDriver = attr.ib(default=None)\n    Version = attr.ib(default=None)  # Mandatory for Deadline\n    ProjectPath = attr.ib(default=None)\n\n\nclass HoudiniCacheSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,   # noqa\n                                 OpenPypePyblishPluginMixin):\n    \"\"\"Submit Houdini scene to perform a local publish in Deadline.\n\n    Publishing in Deadline can be helpful for scenes that publish very slow.\n    This way it can process in the background on another machine without the\n    Artist having to wait for the publish to finish on their local machine.\n\n    Submission is done through the Deadline Web Service as\n    supplied via the environment variable AVALON_DEADLINE.\n\n    \"\"\"\n\n    label = \"Submit Scene to Deadline\"\n    order = pyblish.api.IntegratorOrder\n    hosts = [\"houdini\"]\n    families = [\"publish.hou\"]\n    targets = [\"local\"]\n\n    priority = 50\n    chunk_size = 999999\n    group = None\n    jobInfo = {}\n    pluginInfo = {}\n\n    def get_job_info(self):\n        job_info = DeadlineJobInfo(Plugin=\"Houdini\")\n\n        job_info.update(self.jobInfo)\n        instance = self._instance\n        context = instance.context\n        assert all(\n            result[\"success\"] for result in context.data[\"results\"]\n        ), \"Errors found, aborting integration..\"\n\n        # Deadline connection\n        AVALON_DEADLINE = legacy_io.Session.get(\n            \"AVALON_DEADLINE\", \"http://localhost:8082\"\n        )\n        assert AVALON_DEADLINE, \"Requires AVALON_DEADLINE\"\n\n        project_name = instance.context.data[\"projectName\"]\n        filepath = context.data[\"currentFile\"]\n        scenename = os.path.basename(filepath)\n        job_name = \"{scene} - {instance} [PUBLISH]\".format(\n            scene=scenename, instance=instance.name)\n        batch_name = \"{code} - {scene}\".format(code=project_name,\n                                               scene=scenename)\n        if is_in_tests():\n            batch_name += datetime.now().strftime(\"%d%m%Y%H%M%S\")\n\n        job_info.Name = job_name\n        job_info.BatchName = batch_name\n        job_info.Plugin = instance.data[\"plugin\"]\n        job_info.UserName = context.data.get(\"deadlineUser\", getpass.getuser())\n        rop_node = self.get_rop_node(instance)\n        if rop_node.type().name() != \"alembic\":\n            frames = \"{start}-{end}x{step}\".format(\n                start=int(instance.data[\"frameStart\"]),\n                end=int(instance.data[\"frameEnd\"]),\n                step=int(instance.data[\"byFrameStep\"]),\n            )\n\n            job_info.Frames = frames\n\n        job_info.Pool = instance.data.get(\"primaryPool\")\n        job_info.SecondaryPool = instance.data.get(\"secondaryPool\")\n\n        attr_values = self.get_attr_values_from_data(instance.data)\n\n        job_info.ChunkSize = instance.data.get(\"chunkSize\", self.chunk_size)\n        job_info.Comment = context.data.get(\"comment\")\n        job_info.Priority = attr_values.get(\"priority\", self.priority)\n        job_info.Group = attr_values.get(\"group\", self.group)\n\n        keys = [\n            \"FTRACK_API_KEY\",\n            \"FTRACK_API_USER\",\n            \"FTRACK_SERVER\",\n            \"OPENPYPE_SG_USER\",\n            \"AVALON_DB\",\n            \"AVALON_PROJECT\",\n            \"AVALON_ASSET\",\n            \"AVALON_TASK\",\n            \"AVALON_APP_NAME\",\n            \"OPENPYPE_DEV\",\n            \"OPENPYPE_LOG_NO_COLORS\",\n        ]\n\n        # Add OpenPype version if we are running from build.\n        if is_running_from_build():\n            keys.append(\"OPENPYPE_VERSION\")\n        # Add mongo url if it's enabled\n        if self._instance.context.data.get(\"deadlinePassMongoUrl\"):\n            keys.append(\"OPENPYPE_MONGO\")\n\n        environment = dict({key: os.environ[key] for key in keys\n                            if key in os.environ}, **legacy_io.Session)\n\n        for key in keys:\n            value = environment.get(key)\n            if not value:\n                continue\n            job_info.EnvironmentKeyValue[key] = value\n        # to recognize render jobs\n        job_info.add_render_job_env_var()\n\n        return job_info\n\n    def get_plugin_info(self):\n        # Not all hosts can import this module.\n        import hou\n\n        instance = self._instance\n        version = hou.applicationVersionString()\n        version = \".\".join(version.split(\".\")[:2])\n        rop = self.get_rop_node(instance)\n        plugin_info = HoudiniPluginInfo(\n            Build=None,\n            IgnoreInputs=True,\n            ScriptJob=True,\n            SceneFile=self.scene_path,\n            SaveFile=True,\n            OutputDriver=rop.path(),\n            Version=version,\n            ProjectPath=os.path.dirname(self.scene_path)\n        )\n\n        plugin_payload = attr.asdict(plugin_info)\n\n        return plugin_payload\n\n    def process(self, instance):\n        super(HoudiniCacheSubmitDeadline, self).process(instance)\n        output_dir = os.path.dirname(instance.data[\"files\"][0])\n        instance.data[\"outputDir\"] = output_dir\n        instance.data[\"toBeRenderedOn\"] = \"deadline\"\n\n    def get_rop_node(self, instance):\n        # Not all hosts can import this module.\n        import hou\n\n        rop = instance.data.get(\"instance_node\")\n        rop_node = hou.node(rop)\n\n        return rop_node\n\n    @classmethod\n    def get_attribute_defs(cls):\n        defs = super(HoudiniCacheSubmitDeadline, cls).get_attribute_defs()\n        defs.extend([\n            NumberDef(\"priority\",\n                      minimum=1,\n                      maximum=250,\n                      decimals=0,\n                      default=cls.priority,\n                      label=\"Priority\"),\n            TextDef(\"group\",\n                    default=cls.group,\n                    label=\"Group Name\"),\n        ])\n\n        return defs\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py",
    "content": "import os\nimport json\nfrom datetime import datetime\n\nimport requests\n\nimport pyblish.api\n\nfrom openpype.pipeline import legacy_io\nfrom openpype.tests.lib import is_in_tests\nfrom openpype.lib import is_running_from_build\n\n\nclass HoudiniSubmitPublishDeadline(pyblish.api.ContextPlugin):\n    \"\"\"Submit Houdini scene to perform a local publish in Deadline.\n\n    Publishing in Deadline can be helpful for scenes that publish very slow.\n    This way it can process in the background on another machine without the\n    Artist having to wait for the publish to finish on their local machine.\n\n    Submission is done through the Deadline Web Service as\n    supplied via the environment variable AVALON_DEADLINE.\n\n    \"\"\"\n\n    label = \"Submit Scene to Deadline\"\n    order = pyblish.api.IntegratorOrder\n    hosts = [\"houdini\"]\n    families = [\"*\"]\n    targets = [\"deadline\"]\n\n    def process(self, context):\n        # Not all hosts can import this module.\n        import hou\n\n        # Ensure no errors so far\n        assert all(\n            result[\"success\"] for result in context.data[\"results\"]\n        ), \"Errors found, aborting integration..\"\n\n        # Deadline connection\n        AVALON_DEADLINE = legacy_io.Session.get(\n            \"AVALON_DEADLINE\", \"http://localhost:8082\"\n        )\n        assert AVALON_DEADLINE, \"Requires AVALON_DEADLINE\"\n\n        # Note that `publish` data member might change in the future.\n        # See: https://github.com/pyblish/pyblish-base/issues/307\n        actives = [i for i in context if i.data[\"publish\"]]\n        instance_names = sorted(instance.name for instance in actives)\n\n        if not instance_names:\n            self.log.warning(\n                \"No active instances found. \" \"Skipping submission..\"\n            )\n            return\n\n        scene = context.data[\"currentFile\"]\n        scenename = os.path.basename(scene)\n\n        # Get project code\n        project = context.data[\"projectEntity\"]\n        code = project[\"data\"].get(\"code\", project[\"name\"])\n\n        job_name = \"{scene} [PUBLISH]\".format(scene=scenename)\n        batch_name = \"{code} - {scene}\".format(code=code, scene=scenename)\n        if is_in_tests():\n            batch_name += datetime.now().strftime(\"%d%m%Y%H%M%S\")\n        deadline_user = \"roy\"  # todo: get deadline user dynamically\n\n        # Get only major.minor version of Houdini, ignore patch version\n        version = hou.applicationVersionString()\n        version = \".\".join(version.split(\".\")[:2])\n\n        # Generate the payload for Deadline submission\n        payload = {\n            \"JobInfo\": {\n                \"Plugin\": \"Houdini\",\n                \"Pool\": \"houdini\",  # todo: remove hardcoded pool\n                \"BatchName\": batch_name,\n                \"Comment\": context.data.get(\"comment\", \"\"),\n                \"Priority\": 50,\n                \"Frames\": \"1-1\",  # Always trigger a single frame\n                \"IsFrameDependent\": False,\n                \"Name\": job_name,\n                \"UserName\": deadline_user,\n                # \"Comment\": instance.context.data.get(\"comment\", \"\"),\n                # \"InitialStatus\": state\n            },\n            \"PluginInfo\": {\n                \"Build\": None,  # Don't force build\n                \"IgnoreInputs\": True,\n                # Inputs\n                \"SceneFile\": scene,\n                \"OutputDriver\": \"/out/REMOTE_PUBLISH\",\n                # Mandatory for Deadline\n                \"Version\": version,\n            },\n            # Mandatory for Deadline, may be empty\n            \"AuxFiles\": [],\n        }\n\n        # Process submission per individual instance if the submission\n        # is set to publish each instance as a separate job. Else submit\n        # a single job to process all instances.\n        per_instance = context.data.get(\"separateJobPerInstance\", False)\n        if per_instance:\n            # Submit a job per instance\n            job_name = payload[\"JobInfo\"][\"Name\"]\n            for instance in instance_names:\n                # Clarify job name per submission (include instance name)\n                payload[\"JobInfo\"][\"Name\"] = job_name + \" - %s\" % instance\n                self.submit_job(\n                    context,\n                    payload,\n                    instances=[instance],\n                    deadline=AVALON_DEADLINE\n                )\n        else:\n            # Submit a single job\n            self.submit_job(\n                context,\n                payload,\n                instances=instance_names,\n                deadline=AVALON_DEADLINE\n            )\n\n    def submit_job(self, context, payload, instances, deadline):\n\n        # Ensure we operate on a copy, a shallow copy is fine.\n        payload = payload.copy()\n\n        # Include critical environment variables with submission + api.Session\n        keys = [\n            # Submit along the current Avalon tool setup that we launched\n            # this application with so the Render Slave can build its own\n            # similar environment using it, e.g. \"houdini17.5;pluginx2.3\"\n            \"AVALON_TOOLS\"\n        ]\n\n        # Add OpenPype version if we are running from build.\n        if is_running_from_build():\n            keys.append(\"OPENPYPE_VERSION\")\n\n        # Add mongo url if it's enabled\n        if context.data.get(\"deadlinePassMongoUrl\"):\n            keys.append(\"OPENPYPE_MONGO\")\n\n        environment = dict(\n            {key: os.environ[key] for key in keys if key in os.environ},\n            **legacy_io.Session\n        )\n        environment[\"PYBLISH_ACTIVE_INSTANCES\"] = \",\".join(instances)\n\n        payload[\"JobInfo\"].update(\n            {\n                \"EnvironmentKeyValue%d\"\n                % index: \"{key}={value}\".format(\n                    key=key, value=environment[key]\n                )\n                for index, key in enumerate(environment)\n            }\n        )\n\n        # Submit\n        self.log.debug(\"Submitting..\")\n        self.log.debug(json.dumps(payload, indent=4, sort_keys=True))\n\n        # E.g. http://192.168.0.1:8082/api/jobs\n        url = \"{}/api/jobs\".format(deadline)\n        response = requests.post(url, json=payload)\n        if not response.ok:\n            raise Exception(response.text)\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py",
    "content": "import os\nimport attr\nimport getpass\nfrom datetime import datetime\n\nimport pyblish.api\n\nfrom openpype.pipeline import legacy_io, OpenPypePyblishPluginMixin\nfrom openpype.tests.lib import is_in_tests\nfrom openpype_modules.deadline import abstract_submit_deadline\nfrom openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo\nfrom openpype.lib import (\n    is_running_from_build,\n    BoolDef,\n    TextDef,\n    NumberDef\n)\n\n\n@attr.s\nclass DeadlinePluginInfo():\n    SceneFile = attr.ib(default=None)\n    OutputDriver = attr.ib(default=None)\n    Version = attr.ib(default=None)\n    IgnoreInputs = attr.ib(default=True)\n\n\n@attr.s\nclass ArnoldRenderDeadlinePluginInfo():\n    InputFile = attr.ib(default=None)\n    Verbose = attr.ib(default=4)\n\n\n@attr.s\nclass MantraRenderDeadlinePluginInfo():\n    SceneFile = attr.ib(default=None)\n    Version = attr.ib(default=None)\n\n\n@attr.s\nclass VrayRenderPluginInfo():\n    InputFilename = attr.ib(default=None)\n    SeparateFilesPerFrame = attr.ib(default=True)\n\n\n@attr.s\nclass RedshiftRenderPluginInfo():\n    SceneFile = attr.ib(default=None)\n    Version = attr.ib(default=None)\n\n\nclass HoudiniSubmitDeadline(\n    abstract_submit_deadline.AbstractSubmitDeadline,\n    OpenPypePyblishPluginMixin\n):\n    \"\"\"Submit Render ROPs to Deadline.\n\n    Renders are submitted to a Deadline Web Service as\n    supplied via the environment variable AVALON_DEADLINE.\n\n    Target \"local\":\n        Even though this does *not* render locally this is seen as\n        a 'local' submission as it is the regular way of submitting\n        a Houdini render locally.\n\n    \"\"\"\n\n    label = \"Submit Render to Deadline\"\n    order = pyblish.api.IntegratorOrder\n    hosts = [\"houdini\"]\n    families = [\"usdrender\",\n                \"redshift_rop\",\n                \"arnold_rop\",\n                \"mantra_rop\",\n                \"karma_rop\",\n                \"vray_rop\"]\n    targets = [\"local\"]\n    use_published = True\n\n    # presets\n    export_priority = 50\n    export_chunk_size = 10\n    export_group = \"\"\n    priority = 50\n    chunk_size = 1\n    group = \"\"\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            BoolDef(\n                \"suspend_publish\",\n                default=False,\n                label=\"Suspend publish\"\n            ),\n            NumberDef(\n                \"priority\",\n                label=\"Priority\",\n                default=cls.priority,\n                decimals=0\n            ),\n            NumberDef(\n                \"chunk\",\n                label=\"Frames Per Task\",\n                default=cls.chunk_size,\n                decimals=0,\n                minimum=1,\n                maximum=1000\n            ),\n            TextDef(\n                \"group\",\n                default=cls.group,\n                label=\"Group Name\"\n            ),\n            NumberDef(\n                \"export_priority\",\n                label=\"Export Priority\",\n                default=cls.export_priority,\n                decimals=0\n            ),\n            NumberDef(\n                \"export_chunk\",\n                label=\"Export Frames Per Task\",\n                default=cls.export_chunk_size,\n                decimals=0,\n                minimum=1,\n                maximum=1000\n            ),\n            TextDef(\n                \"export_group\",\n                default=cls.export_group,\n                label=\"Export Group Name\"\n            ),\n            BoolDef(\n                \"suspend_publish\",\n                default=False,\n                label=\"Suspend publish\"\n            )\n        ]\n\n    def get_job_info(self, dependency_job_ids=None):\n\n        instance = self._instance\n        context = instance.context\n\n        attribute_values = self.get_attr_values_from_data(instance.data)\n\n        # Whether Deadline render submission is being split in two\n        # (extract + render)\n        split_render_job = instance.data.get(\"splitRender\")\n\n        # If there's some dependency job ids we can assume this is a render job\n        # and not an export job\n        is_export_job = True\n        if dependency_job_ids:\n            is_export_job = False\n\n        job_type = \"[RENDER]\"\n        if split_render_job and not is_export_job:\n            # Convert from family to Deadline plugin name\n            # i.e., arnold_rop -> Arnold\n            plugin = instance.data[\"family\"].replace(\"_rop\", \"\").capitalize()\n        else:\n            plugin = \"Houdini\"\n            if split_render_job:\n                job_type = \"[EXPORT IFD]\"\n\n        job_info = DeadlineJobInfo(Plugin=plugin)\n\n        filepath = context.data[\"currentFile\"]\n        filename = os.path.basename(filepath)\n        job_info.Name = \"{} - {} {}\".format(filename, instance.name, job_type)\n        job_info.BatchName = filename\n\n        job_info.UserName = context.data.get(\n            \"deadlineUser\", getpass.getuser())\n\n        if is_in_tests():\n            job_info.BatchName += datetime.now().strftime(\"%d%m%Y%H%M%S\")\n\n        # Deadline requires integers in frame range\n        start = instance.data[\"frameStartHandle\"]\n        end = instance.data[\"frameEndHandle\"]\n        frames = \"{start}-{end}x{step}\".format(\n            start=int(start),\n            end=int(end),\n            step=int(instance.data[\"byFrameStep\"]),\n        )\n        job_info.Frames = frames\n\n        # Make sure we make job frame dependent so render tasks pick up a soon\n        # as export tasks are done\n        if split_render_job and not is_export_job:\n            job_info.IsFrameDependent = True\n\n        job_info.Pool = instance.data.get(\"primaryPool\")\n        job_info.SecondaryPool = instance.data.get(\"secondaryPool\")\n\n        if split_render_job and is_export_job:\n            job_info.Priority = attribute_values.get(\n                \"export_priority\", self.export_priority\n            )\n            job_info.ChunkSize = attribute_values.get(\n                \"export_chunk\", self.export_chunk_size\n            )\n            job_info.Group = self.export_group\n        else:\n            job_info.Priority = attribute_values.get(\n                \"priority\", self.priority\n            )\n            job_info.ChunkSize = attribute_values.get(\n                \"chunk\", self.chunk_size\n            )\n            job_info.Group = self.group\n\n        job_info.Comment = context.data.get(\"comment\")\n\n        keys = [\n            \"FTRACK_API_KEY\",\n            \"FTRACK_API_USER\",\n            \"FTRACK_SERVER\",\n            \"OPENPYPE_SG_USER\",\n            \"AVALON_DB\",\n            \"AVALON_PROJECT\",\n            \"AVALON_ASSET\",\n            \"AVALON_TASK\",\n            \"AVALON_APP_NAME\",\n            \"OPENPYPE_DEV\",\n            \"OPENPYPE_LOG_NO_COLORS\",\n        ]\n\n        # Add OpenPype version if we are running from build.\n        if is_running_from_build():\n            keys.append(\"OPENPYPE_VERSION\")\n\n        # Add mongo url if it's enabled\n        if self._instance.context.data.get(\"deadlinePassMongoUrl\"):\n            keys.append(\"OPENPYPE_MONGO\")\n\n        environment = dict({key: os.environ[key] for key in keys\n                            if key in os.environ}, **legacy_io.Session)\n\n        for key in keys:\n            value = environment.get(key)\n            if value:\n                job_info.EnvironmentKeyValue[key] = value\n\n        # to recognize render jobs\n        job_info.add_render_job_env_var()\n\n        for i, filepath in enumerate(instance.data[\"files\"]):\n            dirname = os.path.dirname(filepath)\n            fname = os.path.basename(filepath)\n            job_info.OutputDirectory += dirname.replace(\"\\\\\", \"/\")\n            job_info.OutputFilename += fname\n\n        # Add dependencies if given\n        if dependency_job_ids:\n            job_info.JobDependencies = \",\".join(dependency_job_ids)\n\n        return job_info\n\n    def get_plugin_info(self, job_type=None):\n        # Not all hosts can import this module.\n        import hou\n\n        instance = self._instance\n        context = instance.context\n\n        hou_major_minor = hou.applicationVersionString().rsplit(\".\", 1)[0]\n\n        # Output driver to render\n        if job_type == \"render\":\n            family = instance.data.get(\"family\")\n            if family == \"arnold_rop\":\n                plugin_info = ArnoldRenderDeadlinePluginInfo(\n                    InputFile=instance.data[\"ifdFile\"]\n                )\n            elif family == \"mantra_rop\":\n                plugin_info = MantraRenderDeadlinePluginInfo(\n                    SceneFile=instance.data[\"ifdFile\"],\n                    Version=hou_major_minor,\n                )\n            elif family == \"vray_rop\":\n                plugin_info = VrayRenderPluginInfo(\n                    InputFilename=instance.data[\"ifdFile\"],\n                )\n            elif family == \"redshift_rop\":\n                plugin_info = RedshiftRenderPluginInfo(\n                    SceneFile=instance.data[\"ifdFile\"]\n                )\n                # Note: To use different versions of Redshift on Deadline\n                #       set the `REDSHIFT_VERSION` env variable in the Tools\n                #       settings in the AYON Application plugin. You will also\n                #       need to set that version in `Redshift.param` file\n                #       of the Redshift Deadline plugin:\n                #           [Redshift_Executable_*]\n                #           where * is the version number.\n                if os.getenv(\"REDSHIFT_VERSION\"):\n                    plugin_info.Version = os.getenv(\"REDSHIFT_VERSION\")\n                else:\n                    self.log.warning((\n                        \"REDSHIFT_VERSION env variable is not set\"\n                        \" - using version configured in Deadline\"\n                    ))\n\n            else:\n                self.log.error(\n                    \"Family '%s' not supported yet to split render job\",\n                    family\n                )\n                return\n        else:\n            driver = hou.node(instance.data[\"instance_node\"])\n            plugin_info = DeadlinePluginInfo(\n                SceneFile=context.data[\"currentFile\"],\n                OutputDriver=driver.path(),\n                Version=hou_major_minor,\n                IgnoreInputs=True\n            )\n\n        return attr.asdict(plugin_info)\n\n    def process(self, instance):\n        super(HoudiniSubmitDeadline, self).process(instance)\n\n        # TODO: Avoid the need for this logic here, needed for submit publish\n        # Store output dir for unified publisher (filesequence)\n        output_dir = os.path.dirname(instance.data[\"files\"][0])\n        instance.data[\"outputDir\"] = output_dir\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_max_deadline.py",
    "content": "import os\nimport getpass\nimport copy\nimport attr\n\nfrom openpype.lib import (\n    TextDef,\n    BoolDef,\n    NumberDef,\n)\nfrom openpype.pipeline import (\n    legacy_io,\n    OpenPypePyblishPluginMixin\n)\nfrom openpype.pipeline.publish.lib import (\n    replace_with_published_scene_path\n)\nfrom openpype.pipeline.publish import KnownPublishError\nfrom openpype_modules.deadline import abstract_submit_deadline\nfrom openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo\nfrom openpype.lib import is_running_from_build\n\n\n@attr.s\nclass MaxPluginInfo(object):\n    SceneFile = attr.ib(default=None)   # Input\n    Version = attr.ib(default=None)  # Mandatory for Deadline\n    SaveFile = attr.ib(default=True)\n    IgnoreInputs = attr.ib(default=True)\n\n\nclass MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,\n                        OpenPypePyblishPluginMixin):\n\n    label = \"Submit Render to Deadline\"\n    hosts = [\"max\"]\n    families = [\"maxrender\"]\n    targets = [\"local\"]\n\n    use_published = True\n    priority = 50\n    chunk_size = 1\n    jobInfo = {}\n    pluginInfo = {}\n    group = None\n\n    @classmethod\n    def apply_settings(cls, project_settings, system_settings):\n        settings = project_settings[\"deadline\"][\"publish\"][\"MaxSubmitDeadline\"]  # noqa\n\n        # Take some defaults from settings\n        cls.use_published = settings.get(\"use_published\",\n                                         cls.use_published)\n        cls.priority = settings.get(\"priority\",\n                                    cls.priority)\n        cls.chuck_size = settings.get(\"chunk_size\", cls.chunk_size)\n        cls.group = settings.get(\"group\", cls.group)\n    # TODO: multiple camera instance, separate job infos\n    def get_job_info(self):\n        job_info = DeadlineJobInfo(Plugin=\"3dsmax\")\n\n        # todo: test whether this works for existing production cases\n        #       where custom jobInfo was stored in the project settings\n        job_info.update(self.jobInfo)\n\n        instance = self._instance\n        context = instance.context\n        # Always use the original work file name for the Job name even when\n        # rendering is done from the published Work File. The original work\n        # file name is clearer because it can also have subversion strings,\n        # etc. which are stripped for the published file.\n\n        src_filepath = context.data[\"currentFile\"]\n        src_filename = os.path.basename(src_filepath)\n        job_info.Name = \"%s - %s\" % (src_filename, instance.name)\n        job_info.BatchName = src_filename\n        job_info.Plugin = instance.data[\"plugin\"]\n        job_info.UserName = context.data.get(\"deadlineUser\", getpass.getuser())\n        job_info.EnableAutoTimeout = True\n        # Deadline requires integers in frame range\n        frames = \"{start}-{end}\".format(\n            start=int(instance.data[\"frameStart\"]),\n            end=int(instance.data[\"frameEnd\"])\n        )\n        job_info.Frames = frames\n\n        job_info.Pool = instance.data.get(\"primaryPool\")\n        job_info.SecondaryPool = instance.data.get(\"secondaryPool\")\n\n        attr_values = self.get_attr_values_from_data(instance.data)\n\n        job_info.ChunkSize = attr_values.get(\"chunkSize\", 1)\n        job_info.Comment = context.data.get(\"comment\")\n        job_info.Priority = attr_values.get(\"priority\", self.priority)\n        job_info.Group = attr_values.get(\"group\", self.group)\n\n        # Add options from RenderGlobals\n        render_globals = instance.data.get(\"renderGlobals\", {})\n        job_info.update(render_globals)\n\n        keys = [\n            \"FTRACK_API_KEY\",\n            \"FTRACK_API_USER\",\n            \"FTRACK_SERVER\",\n            \"OPENPYPE_SG_USER\",\n            \"AVALON_DB\",\n            \"AVALON_PROJECT\",\n            \"AVALON_ASSET\",\n            \"AVALON_TASK\",\n            \"AVALON_APP_NAME\",\n            \"OPENPYPE_DEV\",\n            \"IS_TEST\"\n        ]\n\n        # Add OpenPype version if we are running from build.\n        if is_running_from_build():\n            keys.append(\"OPENPYPE_VERSION\")\n\n        # Add mongo url if it's enabled\n        if self._instance.context.data.get(\"deadlinePassMongoUrl\"):\n            keys.append(\"OPENPYPE_MONGO\")\n\n        environment = dict({key: os.environ[key] for key in keys\n                            if key in os.environ}, **legacy_io.Session)\n\n        for key in keys:\n            value = environment.get(key)\n            if not value:\n                continue\n            job_info.EnvironmentKeyValue[key] = value\n\n        # to recognize render jobs\n        job_info.add_render_job_env_var()\n        job_info.EnvironmentKeyValue[\"OPENPYPE_LOG_NO_COLORS\"] = \"1\"\n\n        # Add list of expected files to job\n        # ---------------------------------\n        if not instance.data.get(\"multiCamera\"):\n            exp = instance.data.get(\"expectedFiles\")\n            for filepath in self._iter_expected_files(exp):\n                job_info.OutputDirectory += os.path.dirname(filepath)\n                job_info.OutputFilename += os.path.basename(filepath)\n\n        return job_info\n\n    def get_plugin_info(self):\n        instance = self._instance\n\n        plugin_info = MaxPluginInfo(\n            SceneFile=self.scene_path,\n            Version=instance.data[\"maxversion\"],\n            SaveFile=True,\n            IgnoreInputs=True\n        )\n\n        plugin_payload = attr.asdict(plugin_info)\n\n        # Patching with pluginInfo from settings\n        for key, value in self.pluginInfo.items():\n            plugin_payload[key] = value\n\n        return plugin_payload\n\n    def process_submission(self):\n\n        instance = self._instance\n        filepath = instance.context.data[\"currentFile\"]\n\n        files = instance.data[\"expectedFiles\"]\n        if not files:\n            raise KnownPublishError(\"No Render Elements found!\")\n        first_file = next(self._iter_expected_files(files))\n        output_dir = os.path.dirname(first_file)\n        instance.data[\"outputDir\"] = output_dir\n\n        filename = os.path.basename(filepath)\n\n        payload_data = {\n            \"filename\": filename,\n            \"dirname\": output_dir\n        }\n\n        self.log.debug(\"Submitting 3dsMax render..\")\n        project_settings = instance.context.data[\"project_settings\"]\n        if instance.data.get(\"multiCamera\"):\n            self.log.debug(\"Submitting jobs for multiple cameras..\")\n            payload = self._use_published_name_for_multiples(\n                payload_data, project_settings)\n            job_infos, plugin_infos = payload\n            for job_info, plugin_info in zip(job_infos, plugin_infos):\n                self.submit(self.assemble_payload(job_info, plugin_info))\n        else:\n            payload = self._use_published_name(payload_data, project_settings)\n            job_info, plugin_info = payload\n            self.submit(self.assemble_payload(job_info, plugin_info))\n\n    def _use_published_name(self, data, project_settings):\n        # Not all hosts can import these modules.\n        from openpype.hosts.max.api.lib import (\n            get_current_renderer,\n            get_multipass_setting\n        )\n        from openpype.hosts.max.api.lib_rendersettings import RenderSettings\n\n        instance = self._instance\n        job_info = copy.deepcopy(self.job_info)\n        plugin_info = copy.deepcopy(self.plugin_info)\n        plugin_data = {}\n\n        multipass = get_multipass_setting(project_settings)\n        if multipass:\n            plugin_data[\"DisableMultipass\"] = 0\n        else:\n            plugin_data[\"DisableMultipass\"] = 1\n\n        files = instance.data.get(\"expectedFiles\")\n        if not files:\n            raise KnownPublishError(\"No render elements found\")\n        first_file = next(self._iter_expected_files(files))\n        old_output_dir = os.path.dirname(first_file)\n        output_beauty = RenderSettings().get_render_output(instance.name,\n                                                           old_output_dir)\n        rgb_bname = os.path.basename(output_beauty)\n        dir = os.path.dirname(first_file)\n        beauty_name = f\"{dir}/{rgb_bname}\"\n        beauty_name = beauty_name.replace(\"\\\\\", \"/\")\n        plugin_data[\"RenderOutput\"] = beauty_name\n        # as 3dsmax has version with different languages\n        plugin_data[\"Language\"] = \"ENU\"\n\n        renderer_class = get_current_renderer()\n\n        renderer = str(renderer_class).split(\":\")[0]\n        if renderer in [\n            \"ART_Renderer\",\n            \"Redshift_Renderer\",\n            \"V_Ray_6_Hotfix_3\",\n            \"V_Ray_GPU_6_Hotfix_3\",\n            \"Default_Scanline_Renderer\",\n            \"Quicksilver_Hardware_Renderer\",\n        ]:\n            render_elem_list = RenderSettings().get_render_element()\n            for i, element in enumerate(render_elem_list):\n                elem_bname = os.path.basename(element)\n                new_elem = f\"{dir}/{elem_bname}\"\n                new_elem = new_elem.replace(\"/\", \"\\\\\")\n                plugin_data[\"RenderElementOutputFilename%d\" % i] = new_elem   # noqa\n\n        if renderer == \"Redshift_Renderer\":\n            plugin_data[\"redshift_SeparateAovFiles\"] = instance.data.get(\n                \"separateAovFiles\")\n        if instance.data[\"cameras\"]:\n            camera = instance.data[\"cameras\"][0]\n            plugin_info[\"Camera0\"] = camera\n            plugin_info[\"Camera\"] = camera\n            plugin_info[\"Camera1\"] = camera\n        self.log.debug(\"plugin data:{}\".format(plugin_data))\n        plugin_info.update(plugin_data)\n\n        return job_info, plugin_info\n\n    def get_job_info_through_camera(self, camera):\n        \"\"\"Get the job parameters for deadline submission when\n        multi-camera is enabled.\n        Args:\n            infos(dict): a dictionary with job info.\n        \"\"\"\n        instance = self._instance\n        context = instance.context\n        job_info = copy.deepcopy(self.job_info)\n        exp = instance.data.get(\"expectedFiles\")\n\n        src_filepath = context.data[\"currentFile\"]\n        src_filename = os.path.basename(src_filepath)\n        job_info.Name = \"%s - %s - %s\" % (\n            src_filename, instance.name, camera)\n        for filepath in self._iter_expected_files(exp):\n            if camera not in filepath:\n                continue\n            job_info.OutputDirectory += os.path.dirname(filepath)\n            job_info.OutputFilename += os.path.basename(filepath)\n\n        return job_info\n        # set the output filepath with the relative camera\n\n    def get_plugin_info_through_camera(self, camera):\n        \"\"\"Get the plugin parameters for deadline submission when\n        multi-camera is enabled.\n        Args:\n            infos(dict): a dictionary with plugin info.\n        \"\"\"\n        from openpype.hosts.max.api.lib import get_current_renderer\n        from openpype.hosts.max.api.lib_rendersettings import RenderSettings\n\n        instance = self._instance\n        # set the target camera\n        plugin_info = copy.deepcopy(self.plugin_info)\n\n        plugin_data = {}\n        # set the output filepath with the relative camera\n        if instance.data.get(\"multiCamera\"):\n            scene_filepath = instance.context.data[\"currentFile\"]\n            scene_filename = os.path.basename(scene_filepath)\n            scene_directory = os.path.dirname(scene_filepath)\n            current_filename, ext = os.path.splitext(scene_filename)\n            camera_scene_name = f\"{current_filename}_{camera}{ext}\"\n            camera_scene_filepath = os.path.join(\n                scene_directory, f\"_{current_filename}\", camera_scene_name)\n            plugin_data[\"SceneFile\"] = camera_scene_filepath\n\n        files = instance.data.get(\"expectedFiles\")\n        if not files:\n            raise KnownPublishError(\"No render elements found\")\n        first_file = next(self._iter_expected_files(files))\n        old_output_dir = os.path.dirname(first_file)\n        rgb_output = RenderSettings().get_batch_render_output(camera)       # noqa\n        rgb_bname = os.path.basename(rgb_output)\n        dir = os.path.dirname(first_file)\n        beauty_name = f\"{dir}/{rgb_bname}\"\n        beauty_name = beauty_name.replace(\"\\\\\", \"/\")\n        plugin_info[\"RenderOutput\"] = beauty_name\n        renderer_class = get_current_renderer()\n\n        renderer = str(renderer_class).split(\":\")[0]\n        if renderer in [\n            \"ART_Renderer\",\n            \"Redshift_Renderer\",\n            \"V_Ray_6_Hotfix_3\",\n            \"V_Ray_GPU_6_Hotfix_3\",\n            \"Default_Scanline_Renderer\",\n            \"Quicksilver_Hardware_Renderer\",\n        ]:\n            render_elem_list = RenderSettings().get_batch_render_elements(\n                instance.name, old_output_dir, camera\n            )\n            for i, element in enumerate(render_elem_list):\n                if camera in element:\n                    elem_bname = os.path.basename(element)\n                    new_elem = f\"{dir}/{elem_bname}\"\n                    new_elem = new_elem.replace(\"/\", \"\\\\\")\n                    plugin_info[\"RenderElementOutputFilename%d\" % i] = new_elem   # noqa\n\n        if camera:\n            # set the default camera and target camera\n            # (weird parameters from max)\n            plugin_data[\"Camera\"] = camera\n            plugin_data[\"Camera1\"] = camera\n            plugin_data[\"Camera0\"] = None\n\n        plugin_info.update(plugin_data)\n        return plugin_info\n\n    def _use_published_name_for_multiples(self, data, project_settings):\n        \"\"\"Process the parameters submission for deadline when\n            user enables multi-cameras option.\n        Args:\n            job_info_list (list): A list of multiple job infos\n            plugin_info_list (list): A list of multiple plugin infos\n        \"\"\"\n        from openpype.hosts.max.api.lib import get_multipass_setting\n\n        job_info_list = []\n        plugin_info_list = []\n        instance = self._instance\n        cameras = instance.data.get(\"cameras\", [])\n        plugin_data = {}\n        multipass = get_multipass_setting(project_settings)\n        if multipass:\n            plugin_data[\"DisableMultipass\"] = 0\n        else:\n            plugin_data[\"DisableMultipass\"] = 1\n        for cam in cameras:\n            job_info = self.get_job_info_through_camera(cam)\n            plugin_info = self.get_plugin_info_through_camera(cam)\n            plugin_info.update(plugin_data)\n            job_info_list.append(job_info)\n            plugin_info_list.append(plugin_info)\n\n        return job_info_list, plugin_info_list\n\n    def from_published_scene(self, replace_in_path=True):\n        instance = self._instance\n        if instance.data[\"renderer\"] == \"Redshift_Renderer\":\n            self.log.debug(\"Using Redshift...published scene wont be used..\")\n            replace_in_path = False\n        return replace_with_published_scene_path(\n            instance, replace_in_path)\n\n    @staticmethod\n    def _iter_expected_files(exp):\n        if isinstance(exp[0], dict):\n            for _aov, files in exp[0].items():\n                for file in files:\n                    yield file\n        else:\n            for file in exp:\n                yield file\n\n    @classmethod\n    def get_attribute_defs(cls):\n        defs = super(MaxSubmitDeadline, cls).get_attribute_defs()\n        defs.extend([\n            BoolDef(\"use_published\",\n                    default=cls.use_published,\n                    label=\"Use Published Scene\"),\n\n            NumberDef(\"priority\",\n                      minimum=1,\n                      maximum=250,\n                      decimals=0,\n                      default=cls.priority,\n                      label=\"Priority\"),\n\n            NumberDef(\"chunkSize\",\n                      minimum=1,\n                      maximum=50,\n                      decimals=0,\n                      default=cls.chunk_size,\n                      label=\"Frame Per Task\"),\n\n            TextDef(\"group\",\n                    default=cls.group,\n                    label=\"Group Name\"),\n        ])\n\n        return defs\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_maya_deadline.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Submitting render job to Deadline.\n\nThis module is taking care of submitting job from Maya to Deadline. It\ncreates job and set correct environments. Its behavior is controlled by\n``DEADLINE_REST_URL`` environment variable - pointing to Deadline Web Service\nand :data:`MayaSubmitDeadline.use_published` property telling Deadline to\nuse published scene workfile or not.\n\nIf ``vrscene`` or ``assscene`` are detected in families, it will first\nsubmit job to export these files and then dependent job to render them.\n\nAttributes:\n    payload_skeleton (dict): Skeleton payload data sent as job to Deadline.\n        Default values are for ``MayaBatch`` plugin.\n\n\"\"\"\n\nfrom __future__ import print_function\nimport os\nimport getpass\nimport copy\nimport re\nimport hashlib\nfrom datetime import datetime\nimport itertools\nfrom collections import OrderedDict\n\nimport attr\n\nfrom openpype.pipeline import (\n    legacy_io,\n    OpenPypePyblishPluginMixin\n)\nfrom openpype.lib import (\n    BoolDef,\n    NumberDef,\n    TextDef,\n    EnumDef\n)\nfrom openpype.hosts.maya.api.lib_rendersettings import RenderSettings\nfrom openpype.hosts.maya.api.lib import get_attr_in_layer\n\nfrom openpype_modules.deadline import abstract_submit_deadline\nfrom openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo\nfrom openpype.tests.lib import is_in_tests\nfrom openpype.lib import is_running_from_build\nfrom openpype.pipeline.farm.tools import iter_expected_files\n\n\ndef _validate_deadline_bool_value(instance, attribute, value):\n    if not isinstance(value, (str, bool)):\n        raise TypeError(\n            \"Attribute {} must be str or bool.\".format(attribute))\n    if value not in {\"1\", \"0\", True, False}:\n        raise ValueError(\n            (\"Value of {} must be one of \"\n             \"'0', '1', True, False\").format(attribute)\n        )\n\n\n@attr.s\nclass MayaPluginInfo(object):\n    SceneFile = attr.ib(default=None)   # Input\n    OutputFilePath = attr.ib(default=None)  # Output directory and filename\n    OutputFilePrefix = attr.ib(default=None)\n    Version = attr.ib(default=None)  # Mandatory for Deadline\n    UsingRenderLayers = attr.ib(default=True)\n    RenderLayer = attr.ib(default=None)  # Render only this layer\n    Renderer = attr.ib(default=None)\n    ProjectPath = attr.ib(default=None)  # Resolve relative references\n    # Include all lights flag\n    RenderSetupIncludeLights = attr.ib(\n        default=\"1\", validator=_validate_deadline_bool_value)\n    StrictErrorChecking = attr.ib(default=True)\n\n\n@attr.s\nclass PythonPluginInfo(object):\n    ScriptFile = attr.ib()\n    Version = attr.ib(default=\"3.6\")\n    Arguments = attr.ib(default=None)\n    SingleFrameOnly = attr.ib(default=None)\n\n\n@attr.s\nclass VRayPluginInfo(object):\n    InputFilename = attr.ib(default=None)   # Input\n    SeparateFilesPerFrame = attr.ib(default=None)\n    VRayEngine = attr.ib(default=\"V-Ray\")\n    Width = attr.ib(default=None)\n    Height = attr.ib(default=None)  # Mandatory for Deadline\n    OutputFilePath = attr.ib(default=True)\n    OutputFileName = attr.ib(default=None)  # Render only this layer\n\n\n@attr.s\nclass ArnoldPluginInfo(object):\n    ArnoldFile = attr.ib(default=None)\n\n\nclass MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,\n                         OpenPypePyblishPluginMixin):\n\n    label = \"Submit Render to Deadline\"\n    hosts = [\"maya\"]\n    families = [\"renderlayer\"]\n    targets = [\"local\"]\n\n    tile_assembler_plugin = \"OpenPypeTileAssembler\"\n    priority = 50\n    tile_priority = 50\n    limit = []  # limit groups\n    jobInfo = {}\n    pluginInfo = {}\n    group = \"none\"\n    strict_error_checking = True\n\n    @classmethod\n    def apply_settings(cls, project_settings, system_settings):\n        settings = project_settings[\"deadline\"][\"publish\"][\"MayaSubmitDeadline\"]  # noqa\n\n        # Take some defaults from settings\n        cls.asset_dependencies = settings.get(\"asset_dependencies\",\n                                              cls.asset_dependencies)\n        cls.import_reference = settings.get(\"import_reference\",\n                                            cls.import_reference)\n        cls.use_published = settings.get(\"use_published\", cls.use_published)\n        cls.priority = settings.get(\"priority\", cls.priority)\n        cls.tile_priority = settings.get(\"tile_priority\", cls.tile_priority)\n        cls.limit = settings.get(\"limit\", cls.limit)\n        cls.group = settings.get(\"group\", cls.group)\n        cls.strict_error_checking = settings.get(\"strict_error_checking\",\n                                                 cls.strict_error_checking)\n        cls.jobInfo = settings.get(\"jobInfo\", cls.jobInfo)\n        cls.pluginInfo = settings.get(\"pluginInfo\", cls.pluginInfo)\n\n    def get_job_info(self):\n        job_info = DeadlineJobInfo(Plugin=\"MayaBatch\")\n\n        # todo: test whether this works for existing production cases\n        #       where custom jobInfo was stored in the project settings\n        job_info.update(self.jobInfo)\n\n        instance = self._instance\n        context = instance.context\n\n        # Always use the original work file name for the Job name even when\n        # rendering is done from the published Work File. The original work\n        # file name is clearer because it can also have subversion strings,\n        # etc. which are stripped for the published file.\n        src_filepath = context.data[\"currentFile\"]\n        src_filename = os.path.basename(src_filepath)\n\n        if is_in_tests():\n            src_filename += datetime.now().strftime(\"%d%m%Y%H%M%S\")\n\n        job_info.Name = \"%s - %s\" % (src_filename, instance.name)\n        job_info.BatchName = src_filename\n        job_info.Plugin = instance.data.get(\"mayaRenderPlugin\", \"MayaBatch\")\n        job_info.UserName = context.data.get(\"deadlineUser\", getpass.getuser())\n\n        # Deadline requires integers in frame range\n        frames = \"{start}-{end}x{step}\".format(\n            start=int(instance.data[\"frameStartHandle\"]),\n            end=int(instance.data[\"frameEndHandle\"]),\n            step=int(instance.data[\"byFrameStep\"]),\n        )\n        job_info.Frames = frames\n\n        job_info.Pool = instance.data.get(\"primaryPool\")\n        job_info.SecondaryPool = instance.data.get(\"secondaryPool\")\n        job_info.Comment = context.data.get(\"comment\")\n        job_info.Priority = instance.data.get(\"priority\", self.priority)\n\n        if self.group != \"none\" and self.group:\n            job_info.Group = self.group\n\n        if self.limit:\n            job_info.LimitGroups = \",\".join(self.limit)\n\n        attr_values = self.get_attr_values_from_data(instance.data)\n        render_globals = instance.data.setdefault(\"renderGlobals\", dict())\n        machine_list = attr_values.get(\"machineList\", \"\")\n        if machine_list:\n            if attr_values.get(\"whitelist\", True):\n                machine_list_key = \"Whitelist\"\n            else:\n                machine_list_key = \"Blacklist\"\n            render_globals[machine_list_key] = machine_list\n\n        job_info.Priority = attr_values.get(\"priority\")\n        job_info.ChunkSize = attr_values.get(\"chunkSize\")\n\n        # Add options from RenderGlobals\n        render_globals = instance.data.get(\"renderGlobals\", {})\n        job_info.update(render_globals)\n\n        keys = [\n            \"FTRACK_API_KEY\",\n            \"FTRACK_API_USER\",\n            \"FTRACK_SERVER\",\n            \"OPENPYPE_SG_USER\",\n            \"AVALON_DB\",\n            \"AVALON_PROJECT\",\n            \"AVALON_ASSET\",\n            \"AVALON_TASK\",\n            \"AVALON_APP_NAME\",\n            \"OPENPYPE_DEV\"\n            \"IS_TEST\"\n        ]\n\n        # Add OpenPype version if we are running from build.\n        if is_running_from_build():\n            keys.append(\"OPENPYPE_VERSION\")\n\n        # Add mongo url if it's enabled\n        if self._instance.context.data.get(\"deadlinePassMongoUrl\"):\n            keys.append(\"OPENPYPE_MONGO\")\n\n        environment = dict({key: os.environ[key] for key in keys\n                            if key in os.environ}, **legacy_io.Session)\n\n        for key in keys:\n            value = environment.get(key)\n            if not value:\n                continue\n            job_info.EnvironmentKeyValue[key] = value\n\n        # to recognize render jobs\n        job_info.add_render_job_env_var()\n        job_info.EnvironmentKeyValue[\"OPENPYPE_LOG_NO_COLORS\"] = \"1\"\n\n        # Adding file dependencies.\n        if not bool(os.environ.get(\"IS_TEST\")) and self.asset_dependencies:\n            dependencies = instance.context.data[\"fileDependencies\"]\n            for dependency in dependencies:\n                job_info.AssetDependency += dependency\n\n        # Add list of expected files to job\n        # ---------------------------------\n        exp = instance.data.get(\"expectedFiles\")\n        for filepath in iter_expected_files(exp):\n            job_info.OutputDirectory += os.path.dirname(filepath)\n            job_info.OutputFilename += os.path.basename(filepath)\n\n        return job_info\n\n    def get_plugin_info(self):\n        # Not all hosts can import this module.\n        from maya import cmds\n\n        instance = self._instance\n        context = instance.context\n\n        # Set it to default Maya behaviour if it cannot be determined\n        # from instance (but it should be, by the Collector).\n\n        default_rs_include_lights = (\n            instance.context.data['project_settings']\n                                 ['maya']\n                                 ['RenderSettings']\n                                 ['enable_all_lights']\n        )\n\n        rs_include_lights = instance.data.get(\n            \"renderSetupIncludeLights\", default_rs_include_lights)\n        if rs_include_lights not in {\"1\", \"0\", True, False}:\n            rs_include_lights = default_rs_include_lights\n\n        attr_values = self.get_attr_values_from_data(instance.data)\n        strict_error_checking = attr_values.get(\"strict_error_checking\",\n                                                self.strict_error_checking)\n        plugin_info = MayaPluginInfo(\n            SceneFile=self.scene_path,\n            Version=cmds.about(version=True),\n            RenderLayer=instance.data['setMembers'],\n            Renderer=instance.data[\"renderer\"],\n            RenderSetupIncludeLights=rs_include_lights,  # noqa\n            ProjectPath=context.data[\"workspaceDir\"],\n            UsingRenderLayers=True,\n            StrictErrorChecking=strict_error_checking\n        )\n\n        plugin_payload = attr.asdict(plugin_info)\n\n        # Patching with pluginInfo from settings\n        for key, value in self.pluginInfo.items():\n            plugin_payload[key] = value\n\n        return plugin_payload\n\n    def process_submission(self):\n        from maya import cmds\n        instance = self._instance\n\n        filepath = self.scene_path  # publish if `use_publish` else workfile\n\n        # TODO: Avoid the need for this logic here, needed for submit publish\n        # Store output dir for unified publisher (filesequence)\n        expected_files = instance.data[\"expectedFiles\"]\n        first_file = next(iter_expected_files(expected_files))\n        output_dir = os.path.dirname(first_file)\n        instance.data[\"outputDir\"] = output_dir\n\n        # Patch workfile (only when use_published is enabled)\n        if self.use_published:\n            self._patch_workfile()\n\n        # Gather needed data ------------------------------------------------\n        filename = os.path.basename(filepath)\n        dirname = os.path.join(\n            cmds.workspace(query=True, rootDirectory=True),\n            cmds.workspace(fileRuleEntry=\"images\")\n        )\n\n        # Fill in common data to payload ------------------------------------\n        # TODO: Replace these with collected data from CollectRender\n        payload_data = {\n            \"filename\": filename,\n            \"dirname\": dirname,\n        }\n\n        # Submit preceding export jobs -------------------------------------\n        export_job = None\n        assert not all(x in instance.data[\"families\"]\n                       for x in ['vrayscene', 'assscene']), (\n            \"Vray Scene and Ass Scene options are mutually exclusive\")\n\n        if \"vrayscene\" in instance.data[\"families\"]:\n            self.log.debug(\"Submitting V-Ray scene render..\")\n            vray_export_payload = self._get_vray_export_payload(payload_data)\n            export_job = self.submit(vray_export_payload)\n\n            payload = self._get_vray_render_payload(payload_data)\n\n        else:\n            self.log.debug(\"Submitting MayaBatch render..\")\n            payload = self._get_maya_payload(payload_data)\n\n        # Add export job as dependency --------------------------------------\n        if export_job:\n            job_info, _ = payload\n            job_info.JobDependencies = export_job\n\n        if instance.data.get(\"tileRendering\"):\n            # Prepare tiles data\n            self._tile_render(payload)\n        else:\n            # Submit main render job\n            job_info, plugin_info = payload\n            self.submit(self.assemble_payload(job_info, plugin_info))\n\n    def _tile_render(self, payload):\n        \"\"\"Submit as tile render per frame with dependent assembly jobs.\"\"\"\n\n        # As collected by super process()\n        instance = self._instance\n\n        payload_job_info, payload_plugin_info = payload\n        job_info = copy.deepcopy(payload_job_info)\n        plugin_info = copy.deepcopy(payload_plugin_info)\n\n        # Force plugin reload for vray cause the region does not get flushed\n        # between tile renders.\n        if plugin_info[\"Renderer\"] == \"vray\":\n            job_info.ForceReloadPlugin = True\n\n        # if we have sequence of files, we need to create tile job for\n        # every frame\n        job_info.TileJob = True\n        job_info.TileJobTilesInX = instance.data.get(\"tilesX\")\n        job_info.TileJobTilesInY = instance.data.get(\"tilesY\")\n\n        tiles_count = job_info.TileJobTilesInX * job_info.TileJobTilesInY\n\n        plugin_info[\"ImageHeight\"] = instance.data.get(\"resolutionHeight\")\n        plugin_info[\"ImageWidth\"] = instance.data.get(\"resolutionWidth\")\n        plugin_info[\"RegionRendering\"] = True\n\n        R_FRAME_NUMBER = re.compile(\n            r\".+\\.(?P<frame>[0-9]+)\\..+\")  # noqa: N806, E501\n        REPL_FRAME_NUMBER = re.compile(\n            r\"(.+\\.)([0-9]+)(\\..+)\")  # noqa: N806, E501\n\n        exp = instance.data[\"expectedFiles\"]\n        if isinstance(exp[0], dict):\n            # we have aovs and we need to iterate over them\n            # get files from `beauty`\n            files = exp[0].get(\"beauty\")\n            # assembly files are used for assembly jobs as we need to put\n            # together all AOVs\n            assembly_files = list(\n                itertools.chain.from_iterable(\n                    [f for _, f in exp[0].items()]))\n            if not files:\n                # if beauty doesn't exist, use first aov we found\n                files = exp[0].get(list(exp[0].keys())[0])\n        else:\n            files = exp\n            assembly_files = files\n\n        # Define frame tile jobs\n        frame_file_hash = {}\n        frame_payloads = {}\n        file_index = 1\n        for file in files:\n            frame = re.search(R_FRAME_NUMBER, file).group(\"frame\")\n\n            new_job_info = copy.deepcopy(job_info)\n            new_job_info.Name += \" (Frame {} - {} tiles)\".format(frame,\n                                                                 tiles_count)\n            new_job_info.TileJobFrame = frame\n\n            new_plugin_info = copy.deepcopy(plugin_info)\n\n            # Add tile data into job info and plugin info\n            tiles_data = _format_tiles(\n                file, 0,\n                instance.data.get(\"tilesX\"),\n                instance.data.get(\"tilesY\"),\n                instance.data.get(\"resolutionWidth\"),\n                instance.data.get(\"resolutionHeight\"),\n                payload_plugin_info[\"OutputFilePrefix\"]\n            )[0]\n\n            new_job_info.update(tiles_data[\"JobInfo\"])\n            new_plugin_info.update(tiles_data[\"PluginInfo\"])\n\n            self.log.debug(\"hashing {} - {}\".format(file_index, file))\n            job_hash = hashlib.sha256(\n                (\"{}_{}\".format(file_index, file)).encode(\"utf-8\"))\n\n            file_hash = job_hash.hexdigest()\n            frame_file_hash[frame] = file_hash\n\n            new_job_info.ExtraInfo[0] = file_hash\n            new_job_info.ExtraInfo[1] = file\n\n            frame_payloads[frame] = self.assemble_payload(\n                job_info=new_job_info,\n                plugin_info=new_plugin_info\n            )\n            file_index += 1\n\n        self.log.debug(\n            \"Submitting tile job(s) [{}] ...\".format(len(frame_payloads)))\n\n        # Submit frame tile jobs\n        frame_tile_job_id = {}\n        for frame, tile_job_payload in frame_payloads.items():\n            job_id = self.submit(tile_job_payload)\n            frame_tile_job_id[frame] = job_id\n\n        # Define assembly payloads\n        assembly_job_info = copy.deepcopy(job_info)\n        assembly_job_info.Plugin = self.tile_assembler_plugin\n        assembly_job_info.Name += \" - Tile Assembly Job\"\n        assembly_job_info.Frames = 1\n        assembly_job_info.MachineLimit = 1\n\n        attr_values = self.get_attr_values_from_data(instance.data)\n        assembly_job_info.Priority = attr_values.get(\"tile_priority\",\n                                                     self.tile_priority)\n        assembly_job_info.TileJob = False\n\n        # TODO: This should be a new publisher attribute definition\n        pool = instance.context.data[\"project_settings\"][\"deadline\"]\n        pool = pool[\"publish\"][\"ProcessSubmittedJobOnFarm\"][\"deadline_pool\"]\n        assembly_job_info.Pool = pool or instance.data.get(\"primaryPool\", \"\")\n\n        assembly_plugin_info = {\n            \"CleanupTiles\": 1,\n            \"ErrorOnMissing\": True,\n            \"Renderer\": self._instance.data[\"renderer\"]\n        }\n\n        assembly_payloads = []\n        output_dir = self.job_info.OutputDirectory[0]\n        config_files = []\n        for file in assembly_files:\n            frame = re.search(R_FRAME_NUMBER, file).group(\"frame\")\n\n            frame_assembly_job_info = copy.deepcopy(assembly_job_info)\n            frame_assembly_job_info.Name += \" (Frame {})\".format(frame)\n            frame_assembly_job_info.OutputFilename[0] = re.sub(\n                REPL_FRAME_NUMBER,\n                \"\\\\1{}\\\\3\".format(\"#\" * len(frame)), file)\n\n            file_hash = frame_file_hash[frame]\n            tile_job_id = frame_tile_job_id[frame]\n\n            frame_assembly_job_info.ExtraInfo[0] = file_hash\n            frame_assembly_job_info.ExtraInfo[1] = file\n            frame_assembly_job_info.JobDependencies = tile_job_id\n            frame_assembly_job_info.Frames = frame\n\n            # write assembly job config files\n            config_file = os.path.join(\n                output_dir,\n                \"{}_config_{}.txt\".format(\n                    os.path.splitext(file)[0],\n                    datetime.now().strftime(\"%Y_%m_%d_%H_%M_%S\")\n                )\n            )\n            config_files.append(config_file)\n            try:\n                if not os.path.isdir(output_dir):\n                    os.makedirs(output_dir)\n            except OSError:\n                # directory is not available\n                self.log.warning(\"Path is unreachable: \"\n                                 \"`{}`\".format(output_dir))\n\n            with open(config_file, \"w\") as cf:\n                print(\"TileCount={}\".format(tiles_count), file=cf)\n                print(\"ImageFileName={}\".format(file), file=cf)\n                print(\"ImageWidth={}\".format(\n                    instance.data.get(\"resolutionWidth\")), file=cf)\n                print(\"ImageHeight={}\".format(\n                    instance.data.get(\"resolutionHeight\")), file=cf)\n\n            reversed_y = False\n            if plugin_info[\"Renderer\"] == \"arnold\":\n                reversed_y = True\n\n            with open(config_file, \"a\") as cf:\n                # Need to reverse the order of the y tiles, because image\n                # coordinates are calculated from bottom left corner.\n                tiles = _format_tiles(\n                    file, 0,\n                    instance.data.get(\"tilesX\"),\n                    instance.data.get(\"tilesY\"),\n                    instance.data.get(\"resolutionWidth\"),\n                    instance.data.get(\"resolutionHeight\"),\n                    payload_plugin_info[\"OutputFilePrefix\"],\n                    reversed_y=reversed_y\n                )[1]\n                for k, v in sorted(tiles.items()):\n                    print(\"{}={}\".format(k, v), file=cf)\n\n            assembly_payloads.append(\n                self.assemble_payload(\n                    job_info=frame_assembly_job_info,\n                    plugin_info=assembly_plugin_info.copy(),\n                    # This would fail if the client machine and webserice are\n                    # using different storage paths.\n                    aux_files=[config_file]\n                )\n            )\n\n        # Submit assembly jobs\n        assembly_job_ids = []\n        num_assemblies = len(assembly_payloads)\n        for i, payload in enumerate(assembly_payloads):\n            self.log.debug(\n                \"submitting assembly job {} of {}\".format(i + 1,\n                                                          num_assemblies)\n            )\n            assembly_job_id = self.submit(payload)\n            assembly_job_ids.append(assembly_job_id)\n\n        instance.data[\"assemblySubmissionJobs\"] = assembly_job_ids\n\n        # Remove config files to avoid confusion about where data is coming\n        # from in Deadline.\n        for config_file in config_files:\n            os.remove(config_file)\n\n    def _get_maya_payload(self, data):\n\n        job_info = copy.deepcopy(self.job_info)\n\n        if not bool(os.environ.get(\"IS_TEST\")) and self.asset_dependencies:\n            # Asset dependency to wait for at least the scene file to sync.\n            job_info.AssetDependency += self.scene_path\n\n        # Get layer prefix\n        renderlayer = self._instance.data[\"setMembers\"]\n        renderer = self._instance.data[\"renderer\"]\n        layer_prefix_attr = RenderSettings.get_image_prefix_attr(renderer)\n        layer_prefix = get_attr_in_layer(layer_prefix_attr, layer=renderlayer)\n\n        plugin_info = copy.deepcopy(self.plugin_info)\n        plugin_info.update({\n            # Output directory and filename\n            \"OutputFilePath\": data[\"dirname\"].replace(\"\\\\\", \"/\"),\n            \"OutputFilePrefix\": layer_prefix,\n        })\n\n        # This hack is here because of how Deadline handles Renderman version.\n        # it considers everything with `renderman` set as version older than\n        # Renderman 22, and so if we are using renderman > 21 we need to set\n        # renderer string on the job to `renderman22`. We will have to change\n        # this when Deadline releases new version handling this.\n        renderer = self._instance.data[\"renderer\"]\n        if renderer == \"renderman\":\n            try:\n                from rfm2.config import cfg  # noqa\n            except ImportError:\n                raise Exception(\"Cannot determine renderman version\")\n\n            rman_version = cfg().build_info.version()  # type: str\n            if int(rman_version.split(\".\")[0]) > 22:\n                renderer = \"renderman22\"\n\n            plugin_info[\"Renderer\"] = renderer\n\n            # this is needed because renderman plugin in Deadline\n            # handles directory and file prefixes separately\n            plugin_info[\"OutputFilePath\"] = job_info.OutputDirectory[0]\n\n        return job_info, plugin_info\n\n    def _get_vray_export_payload(self, data):\n\n        job_info = copy.deepcopy(self.job_info)\n        job_info.Name = self._job_info_label(\"Export\")\n\n        # Get V-Ray settings info to compute output path\n        vray_scene = self.format_vray_output_filename()\n\n        plugin_info = {\n            \"Renderer\": \"vray\",\n            \"SkipExistingFrames\": True,\n            \"UseLegacyRenderLayers\": True,\n            \"OutputFilePath\": os.path.dirname(vray_scene)\n        }\n\n        return job_info, attr.asdict(plugin_info)\n\n    def _get_vray_render_payload(self, data):\n\n        # Job Info\n        job_info = copy.deepcopy(self.job_info)\n        job_info.Name = self._job_info_label(\"Render\")\n        job_info.Plugin = \"Vray\"\n        job_info.OverrideTaskExtraInfoNames = False\n\n        # Plugin Info\n        plugin_info = VRayPluginInfo(\n            InputFilename=self.format_vray_output_filename(),\n            SeparateFilesPerFrame=False,\n            VRayEngine=\"V-Ray\",\n            Width=self._instance.data[\"resolutionWidth\"],\n            Height=self._instance.data[\"resolutionHeight\"],\n            OutputFilePath=job_info.OutputDirectory[0],\n            OutputFileName=job_info.OutputFilename[0]\n        )\n\n        return job_info, attr.asdict(plugin_info)\n\n    def _get_arnold_render_payload(self, data):\n        from maya import cmds\n        # Job Info\n        job_info = copy.deepcopy(self.job_info)\n        job_info.Name = self._job_info_label(\"Render\")\n        job_info.Plugin = \"Arnold\"\n        job_info.OverrideTaskExtraInfoNames = False\n\n        # Plugin Info\n        ass_file, _ = os.path.splitext(data[\"output_filename_0\"])\n        ass_filepath = ass_file + \".ass\"\n\n        plugin_info = ArnoldPluginInfo(\n            ArnoldFile=ass_filepath\n        )\n\n        return job_info, attr.asdict(plugin_info)\n\n    def format_vray_output_filename(self):\n        \"\"\"Format the expected output file of the Export job.\n\n        Example:\n            <Scene>/<Scene>_<Layer>/<Layer>\n            \"shot010_v006/shot010_v006_CHARS/CHARS_0001.vrscene\"\n        Returns:\n            str\n\n        \"\"\"\n        from maya import cmds\n        # \"vrayscene/<Scene>/<Scene>_<Layer>/<Layer>\"\n        vray_settings = cmds.ls(type=\"VRaySettingsNode\")\n        node = vray_settings[0]\n        template = cmds.getAttr(\"{}.vrscene_filename\".format(node))\n        scene, _ = os.path.splitext(self.scene_path)\n\n        def smart_replace(string, key_values):\n            new_string = string\n            for key, value in key_values.items():\n                new_string = new_string.replace(key, value)\n            return new_string\n\n        # Get workfile scene path without extension to format vrscene_filename\n        scene_filename = os.path.basename(self.scene_path)\n        scene_filename_no_ext, _ = os.path.splitext(scene_filename)\n\n        layer = self._instance.data['setMembers']\n\n        # Reformat without tokens\n        output_path = smart_replace(\n            template,\n            {\"<Scene>\": scene_filename_no_ext,\n             \"<Layer>\": layer})\n\n        start_frame = int(self._instance.data[\"frameStartHandle\"])\n        workspace = self._instance.context.data[\"workspace\"]\n        filename_zero = \"{}_{:04d}.vrscene\".format(output_path, start_frame)\n        filepath_zero = os.path.join(workspace, filename_zero)\n\n        return filepath_zero.replace(\"\\\\\", \"/\")\n\n    def _patch_workfile(self):\n        \"\"\"Patch Maya scene.\n\n        This will take list of patches (lines to add) and apply them to\n        *published* Maya  scene file (that is used later for rendering).\n\n        Patches are dict with following structure::\n            {\n                \"name\": \"Name of patch\",\n                \"regex\": \"regex of line before patch\",\n                \"line\": \"line to insert\"\n            }\n\n        \"\"\"\n        project_settings = self._instance.context.data[\"project_settings\"]\n        patches = (\n            project_settings.get(\n                \"deadline\", {}).get(\n                \"publish\", {}).get(\n                \"MayaSubmitDeadline\", {}).get(\n                \"scene_patches\", {})\n        )\n        if not patches:\n            return\n\n        if not os.path.splitext(self.scene_path)[1].lower() != \".ma\":\n            self.log.debug(\"Skipping workfile patch since workfile is not \"\n                           \".ma file\")\n            return\n\n        compiled_regex = [re.compile(p[\"regex\"]) for p in patches]\n        with open(self.scene_path, \"r+\") as pf:\n            scene_data = pf.readlines()\n            for ln, line in enumerate(scene_data):\n                for i, r in enumerate(compiled_regex):\n                    if re.match(r, line):\n                        scene_data.insert(ln + 1, patches[i][\"line\"])\n                        pf.seek(0)\n                        pf.writelines(scene_data)\n                        pf.truncate()\n                        self.log.info(\"Applied {} patch to scene.\".format(\n                            patches[i][\"name\"]\n                        ))\n\n    def _job_info_label(self, label):\n        return \"{label} {job.Name} [{start}-{end}]\".format(\n            label=label,\n            job=self.job_info,\n            start=int(self._instance.data[\"frameStartHandle\"]),\n            end=int(self._instance.data[\"frameEndHandle\"]),\n        )\n\n    @classmethod\n    def get_attribute_defs(cls):\n        defs = super(MayaSubmitDeadline, cls).get_attribute_defs()\n\n        defs.extend([\n            NumberDef(\"priority\",\n                      label=\"Priority\",\n                      default=cls.default_priority,\n                      decimals=0),\n            NumberDef(\"chunkSize\",\n                      label=\"Frames Per Task\",\n                      default=1,\n                      decimals=0,\n                      minimum=1,\n                      maximum=1000),\n            TextDef(\"machineList\",\n                    label=\"Machine List\",\n                    default=\"\",\n                    placeholder=\"machine1,machine2\"),\n            EnumDef(\"whitelist\",\n                    label=\"Machine List (Allow/Deny)\",\n                    items={\n                        True: \"Allow List\",\n                        False: \"Deny List\",\n                    },\n                    default=False),\n            NumberDef(\"tile_priority\",\n                      label=\"Tile Assembler Priority\",\n                      decimals=0,\n                      default=cls.tile_priority),\n            BoolDef(\"strict_error_checking\",\n                    label=\"Strict Error Checking\",\n                    default=cls.strict_error_checking),\n\n        ])\n\n        return defs\n\ndef _format_tiles(\n        filename,\n        index,\n        tiles_x,\n        tiles_y,\n        width,\n        height,\n        prefix,\n        reversed_y=False\n):\n    \"\"\"Generate tile entries for Deadline tile job.\n\n    Returns two dictionaries - one that can be directly used in Deadline\n    job, second that can be used for Deadline Assembly job configuration\n    file.\n\n    This will format tile names:\n\n    Example::\n        {\n        \"OutputFilename0Tile0\": \"_tile_1x1_4x4_Main_beauty.1001.exr\",\n        \"OutputFilename0Tile1\": \"_tile_2x1_4x4_Main_beauty.1001.exr\"\n        }\n\n    And add tile prefixes like:\n\n    Example::\n        Image prefix is:\n        `<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>`\n\n        Result for tile 0 for 4x4 will be:\n        `<Scene>/<RenderLayer>/_tile_1x1_4x4_<RenderLayer>_<RenderPass>`\n\n        Calculating coordinates is tricky as in Job they are defined as top,\n    left, bottom, right with zero being in top-left corner. But Assembler\n    configuration file takes tile coordinates as X, Y, Width and Height and\n    zero is bottom left corner.\n\n    Args:\n        filename (str): Filename to process as tiles.\n        index (int): Index of that file if it is sequence.\n        tiles_x (int): Number of tiles in X.\n        tiles_y (int): Number of tiles in Y.\n        width (int): Width resolution of final image.\n        height (int):  Height resolution of final image.\n        prefix (str): Image prefix.\n        reversed_y (bool): Reverses the order of the y tiles.\n\n    Returns:\n        (dict, dict): Tuple of two dictionaries - first can be used to\n                      extend JobInfo, second has tiles x, y, width and height\n                      used for assembler configuration.\n\n    \"\"\"\n    # Math used requires integers for correct output - as such\n    # we ensure our inputs are correct.\n    assert type(tiles_x) is int, \"tiles_x must be an integer\"\n    assert type(tiles_y) is int, \"tiles_y must be an integer\"\n    assert type(width) is int, \"width must be an integer\"\n    assert type(height) is int, \"height must be an integer\"\n\n    out = {\"JobInfo\": {}, \"PluginInfo\": {}}\n    cfg = OrderedDict()\n    w_space = width // tiles_x\n    h_space = height // tiles_y\n\n    cfg[\"TilesCropped\"] = \"False\"\n\n    tile = 0\n    range_y = range(1, tiles_y + 1)\n    reversed_y_range = list(reversed(range_y))\n    for tile_x in range(1, tiles_x + 1):\n        for i, tile_y in enumerate(range_y):\n            tile_y_index = tile_y\n            if reversed_y:\n                tile_y_index = reversed_y_range[i]\n\n            tile_prefix = \"_tile_{}x{}_{}x{}_\".format(\n                tile_x, tile_y_index, tiles_x, tiles_y\n            )\n\n            new_filename = \"{}/{}{}\".format(\n                os.path.dirname(filename),\n                tile_prefix,\n                os.path.basename(filename)\n            )\n\n            top = height - (tile_y * h_space)\n            bottom = height - ((tile_y - 1) * h_space) - 1\n            left = (tile_x - 1) * w_space\n            right = (tile_x * w_space) - 1\n\n            # Job info\n            key = \"OutputFilename{}\".format(index)\n            out[\"JobInfo\"][key] = new_filename\n\n            # Plugin Info\n            key = \"RegionPrefix{}\".format(str(tile))\n            out[\"PluginInfo\"][key] = \"/{}\".format(\n                tile_prefix\n            ).join(prefix.rsplit(\"/\", 1))\n            out[\"PluginInfo\"][\"RegionTop{}\".format(tile)] = top\n            out[\"PluginInfo\"][\"RegionBottom{}\".format(tile)] = bottom\n            out[\"PluginInfo\"][\"RegionLeft{}\".format(tile)] = left\n            out[\"PluginInfo\"][\"RegionRight{}\".format(tile)] = right\n\n            # Tile config\n            cfg[\"Tile{}FileName\".format(tile)] = new_filename\n            cfg[\"Tile{}X\".format(tile)] = left\n            cfg[\"Tile{}Y\".format(tile)] = top\n            cfg[\"Tile{}Width\".format(tile)] = w_space\n            cfg[\"Tile{}Height\".format(tile)] = h_space\n\n            tile += 1\n\n    return out, cfg\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py",
    "content": "import os\nimport attr\nfrom datetime import datetime\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import legacy_io, PublishXmlValidationError\nfrom openpype.tests.lib import is_in_tests\nfrom openpype.lib import is_running_from_build\nfrom openpype_modules.deadline import abstract_submit_deadline\nfrom openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo\n\nimport pyblish.api\n\n\n@attr.s\nclass MayaPluginInfo(object):\n    Build = attr.ib(default=None)  # Don't force build\n    StrictErrorChecking = attr.ib(default=True)\n\n    SceneFile = attr.ib(default=None)  # Input scene\n    Version = attr.ib(default=None)  # Mandatory for Deadline\n    ProjectPath = attr.ib(default=None)\n\n    ScriptJob = attr.ib(default=True)\n    ScriptFilename = attr.ib(default=None)\n\n\nclass MayaSubmitRemotePublishDeadline(\n        abstract_submit_deadline.AbstractSubmitDeadline):\n    \"\"\"Submit Maya scene to perform a local publish in Deadline.\n\n    Publishing in Deadline can be helpful for scenes that publish very slow.\n    This way it can process in the background on another machine without the\n    Artist having to wait for the publish to finish on their local machine.\n\n    Submission is done through the Deadline Web Service. DL then triggers\n    `openpype/scripts/remote_publish.py`.\n\n    Each publishable instance creates its own full publish job.\n\n    Different from `ProcessSubmittedJobOnFarm` which creates publish job\n    depending on metadata json containing context and instance data of\n    rendered files.\n    \"\"\"\n\n    label = \"Submit Scene to Deadline\"\n    order = pyblish.api.IntegratorOrder\n    hosts = [\"maya\"]\n    families = [\"publish.farm\"]\n    targets = [\"local\"]\n\n    def process(self, instance):\n\n        # Ensure no errors so far\n        if not (all(result[\"success\"]\n                for result in instance.context.data[\"results\"])):\n            raise PublishXmlValidationError(\"Publish process has errors\")\n\n        if not instance.data[\"publish\"]:\n            self.log.warning(\"No active instances found. \"\n                             \"Skipping submission..\")\n            return\n\n        super(MayaSubmitRemotePublishDeadline, self).process(instance)\n\n    def get_job_info(self):\n        instance = self._instance\n        context = instance.context\n\n        project_name = instance.context.data[\"projectName\"]\n        scene = instance.context.data[\"currentFile\"]\n        scenename = os.path.basename(scene)\n\n        job_name = \"{scene} [PUBLISH]\".format(scene=scenename)\n        batch_name = \"{code} - {scene}\".format(code=project_name,\n                                               scene=scenename)\n\n        if is_in_tests():\n            batch_name += datetime.now().strftime(\"%d%m%Y%H%M%S\")\n\n        job_info = DeadlineJobInfo(Plugin=\"MayaBatch\")\n        job_info.BatchName = batch_name\n        job_info.Name = job_name\n        job_info.UserName = context.data.get(\"user\")\n        job_info.Comment = context.data.get(\"comment\", \"\")\n\n        # use setting for publish job on farm, no reason to have it separately\n        project_settings = context.data[\"project_settings\"]\n        deadline_publish_job_sett = project_settings[\"deadline\"][\"publish\"][\"ProcessSubmittedJobOnFarm\"]  # noqa\n        job_info.Department = deadline_publish_job_sett[\"deadline_department\"]\n        job_info.ChunkSize = deadline_publish_job_sett[\"deadline_chunk_size\"]\n        job_info.Priority = deadline_publish_job_sett[\"deadline_priority\"]\n        job_info.Group = deadline_publish_job_sett[\"deadline_group\"]\n        job_info.Pool = deadline_publish_job_sett[\"deadline_pool\"]\n\n        # Include critical environment variables with submission + Session\n        keys = [\n            \"FTRACK_API_USER\",\n            \"FTRACK_API_KEY\",\n            \"FTRACK_SERVER\"\n        ]\n\n        # Add OpenPype version if we are running from build.\n        if is_running_from_build():\n            keys.append(\"OPENPYPE_VERSION\")\n\n        environment = dict({key: os.environ[key] for key in keys\n                            if key in os.environ}, **legacy_io.Session)\n\n        # TODO replace legacy_io with context.data\n        environment[\"AVALON_PROJECT\"] = project_name\n        environment[\"AVALON_ASSET\"] = instance.context.data[\"asset\"]\n        environment[\"AVALON_TASK\"] = instance.context.data[\"task\"]\n        environment[\"AVALON_APP_NAME\"] = os.environ.get(\"AVALON_APP_NAME\")\n        environment[\"OPENPYPE_LOG_NO_COLORS\"] = \"1\"\n        environment[\"OPENPYPE_USERNAME\"] = instance.context.data[\"user\"]\n        environment[\"OPENPYPE_PUBLISH_SUBSET\"] = instance.data[\"subset\"]\n        environment[\"OPENPYPE_REMOTE_PUBLISH\"] = \"1\"\n\n        if AYON_SERVER_ENABLED:\n            environment[\"AYON_REMOTE_PUBLISH\"] = \"1\"\n        else:\n            environment[\"OPENPYPE_REMOTE_PUBLISH\"] = \"1\"\n            environment[\"AVALON_DB\"] = os.environ.get(\"AVALON_DB\")\n        for key, value in environment.items():\n            job_info.EnvironmentKeyValue[key] = value\n\n    def get_plugin_info(self):\n        # Not all hosts can import this module.\n        from maya import cmds\n        scene = self._instance.context.data[\"currentFile\"]\n\n        plugin_info = MayaPluginInfo()\n        plugin_info.SceneFile = scene\n        plugin_info.ScriptFilename = \"{OPENPYPE_REPOS_ROOT}/openpype/scripts/remote_publish.py\"  # noqa\n        plugin_info.Version = cmds.about(version=True)\n        plugin_info.ProjectPath = cmds.workspace(query=True,\n                                                 rootDirectory=True)\n\n        return attr.asdict(plugin_info)\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py",
    "content": "import os\nimport re\nimport json\nimport getpass\nfrom datetime import datetime\n\nimport requests\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import legacy_io\nfrom openpype.pipeline.publish import (\n    OpenPypePyblishPluginMixin\n)\nfrom openpype.tests.lib import is_in_tests\nfrom openpype.lib import (\n    is_running_from_build,\n    BoolDef,\n    NumberDef\n)\n\n\nclass NukeSubmitDeadline(pyblish.api.InstancePlugin,\n                         OpenPypePyblishPluginMixin):\n    \"\"\"Submit write to Deadline\n\n    Renders are submitted to a Deadline Web Service as\n    supplied via settings key \"DEADLINE_REST_URL\".\n\n    \"\"\"\n\n    label = \"Submit Nuke to Deadline\"\n    order = pyblish.api.IntegratorOrder + 0.1\n    hosts = [\"nuke\"]\n    families = [\"render\", \"prerender\"]\n    optional = True\n    targets = [\"local\"]\n\n    # presets\n    priority = 50\n    chunk_size = 1\n    concurrent_tasks = 1\n    group = \"\"\n    department = \"\"\n    limit_groups = {}\n    use_gpu = False\n    env_allowed_keys = []\n    env_search_replace_values = {}\n    workfile_dependency = True\n    use_published_workfile = True\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            NumberDef(\n                \"priority\",\n                label=\"Priority\",\n                default=cls.priority,\n                decimals=0\n            ),\n            NumberDef(\n                \"chunk\",\n                label=\"Frames Per Task\",\n                default=cls.chunk_size,\n                decimals=0,\n                minimum=1,\n                maximum=1000\n            ),\n            NumberDef(\n                \"concurrency\",\n                label=\"Concurrency\",\n                default=cls.concurrent_tasks,\n                decimals=0,\n                minimum=1,\n                maximum=10\n            ),\n            BoolDef(\n                \"use_gpu\",\n                default=cls.use_gpu,\n                label=\"Use GPU\"\n            ),\n            BoolDef(\n                \"suspend_publish\",\n                default=False,\n                label=\"Suspend publish\"\n            ),\n            BoolDef(\n                \"workfile_dependency\",\n                default=cls.workfile_dependency,\n                label=\"Workfile Dependency\"\n            ),\n            BoolDef(\n                \"use_published_workfile\",\n                default=cls.use_published_workfile,\n                label=\"Use Published Workfile\"\n            )\n        ]\n\n    def process(self, instance):\n        if not instance.data.get(\"farm\"):\n            self.log.debug(\"Skipping local instance.\")\n            return\n        instance.data[\"attributeValues\"] = self.get_attr_values_from_data(\n            instance.data)\n\n        # add suspend_publish attributeValue to instance data\n        instance.data[\"suspend_publish\"] = instance.data[\"attributeValues\"][\n            \"suspend_publish\"]\n\n        families = instance.data[\"families\"]\n\n        node = instance.data[\"transientData\"][\"node\"]\n        context = instance.context\n\n        # get default deadline webservice url from deadline module\n        deadline_url = instance.context.data[\"defaultDeadline\"]\n        # if custom one is set in instance, use that\n        if instance.data.get(\"deadlineUrl\"):\n            deadline_url = instance.data.get(\"deadlineUrl\")\n        assert deadline_url, \"Requires Deadline Webservice URL\"\n\n        self.deadline_url = \"{}/api/jobs\".format(deadline_url)\n        self._comment = context.data.get(\"comment\", \"\")\n        self._ver = re.search(r\"\\d+\\.\\d+\", context.data.get(\"hostVersion\"))\n        self._deadline_user = context.data.get(\n            \"deadlineUser\", getpass.getuser())\n        submit_frame_start = int(instance.data[\"frameStartHandle\"])\n        submit_frame_end = int(instance.data[\"frameEndHandle\"])\n\n        # get output path\n        render_path = instance.data['path']\n        script_path = context.data[\"currentFile\"]\n\n        use_published_workfile = instance.data[\"attributeValues\"].get(\n            \"use_published_workfile\", self.use_published_workfile\n        )\n        if use_published_workfile:\n            script_path = self._get_published_workfile_path(context)\n\n        # only add main rendering job if target is not frames_farm\n        r_job_response_json = None\n        if instance.data[\"render_target\"] != \"frames_farm\":\n            r_job_response = self.payload_submit(\n                instance,\n                script_path,\n                render_path,\n                node.name(),\n                submit_frame_start,\n                submit_frame_end\n            )\n            r_job_response_json = r_job_response.json()\n            instance.data[\"deadlineSubmissionJob\"] = r_job_response_json\n\n            # Store output dir for unified publisher (filesequence)\n            instance.data[\"outputDir\"] = os.path.dirname(\n                render_path).replace(\"\\\\\", \"/\")\n            instance.data[\"publishJobState\"] = \"Suspended\"\n\n        if instance.data.get(\"bakingNukeScripts\"):\n            for baking_script in instance.data[\"bakingNukeScripts\"]:\n                render_path = baking_script[\"bakeRenderPath\"]\n                script_path = baking_script[\"bakeScriptPath\"]\n                exe_node_name = baking_script[\"bakeWriteNodeName\"]\n\n                b_job_response = self.payload_submit(\n                    instance,\n                    script_path,\n                    render_path,\n                    exe_node_name,\n                    submit_frame_start,\n                    submit_frame_end,\n                    r_job_response_json,\n                    baking_submission=True\n                )\n\n                # Store output dir for unified publisher (filesequence)\n                instance.data[\"deadlineSubmissionJob\"] = b_job_response.json()\n\n                instance.data[\"publishJobState\"] = \"Suspended\"\n\n                # add to list of job Id\n                if not instance.data.get(\"bakingSubmissionJobs\"):\n                    instance.data[\"bakingSubmissionJobs\"] = []\n\n                instance.data[\"bakingSubmissionJobs\"].append(\n                    b_job_response.json()[\"_id\"])\n\n        # redefinition of families\n        if \"render\" in instance.data[\"family\"]:\n            instance.data['family'] = 'write'\n            families.insert(0, \"render2d\")\n        elif \"prerender\" in instance.data[\"family\"]:\n            instance.data['family'] = 'write'\n            families.insert(0, \"prerender\")\n        instance.data[\"families\"] = families\n\n    def _get_published_workfile_path(self, context):\n        \"\"\"This method is temporary while the class is not inherited from\n        AbstractSubmitDeadline\"\"\"\n        for instance in context:\n            if (\n                instance.data[\"family\"] != \"workfile\"\n                # Disabled instances won't be integrated\n                or instance.data(\"publish\") is False\n            ):\n                continue\n            template_data = instance.data[\"anatomyData\"]\n            # Expect workfile instance has only one representation\n            representation = instance.data[\"representations\"][0]\n            # Get workfile extension\n            repre_file = representation[\"files\"]\n            self.log.info(repre_file)\n            ext = os.path.splitext(repre_file)[1].lstrip(\".\")\n\n            # Fill template data\n            template_data[\"representation\"] = representation[\"name\"]\n            template_data[\"ext\"] = ext\n            template_data[\"comment\"] = None\n\n            anatomy = context.data[\"anatomy\"]\n            # WARNING Hardcoded template name 'publish' > may not be used\n            template_obj = anatomy.templates_obj[\"publish\"][\"path\"]\n\n            template_filled = template_obj.format(template_data)\n            script_path = os.path.normpath(template_filled)\n            self.log.info(\n                \"Using published scene for render {}\".format(\n                    script_path\n                )\n            )\n            return script_path\n\n        return None\n\n    def payload_submit(\n        self,\n        instance,\n        script_path,\n        render_path,\n        exe_node_name,\n        start_frame,\n        end_frame,\n        response_data=None,\n        baking_submission=False,\n    ):\n        \"\"\"Submit payload to Deadline\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n            script_path (str): path to nuke script\n            render_path (str): path to rendered images\n            exe_node_name (str): name of the node to render\n            start_frame (int): start frame\n            end_frame (int): end frame\n            response_data Optional[dict]: response data from\n                                          previous submission\n            baking_submission Optional[bool]: if it's baking submission\n\n        Returns:\n            requests.Response\n        \"\"\"\n        render_dir = os.path.normpath(os.path.dirname(render_path))\n\n        # batch name\n        src_filepath = instance.context.data[\"currentFile\"]\n        batch_name = os.path.basename(src_filepath)\n        job_name = os.path.basename(render_path)\n\n        if is_in_tests():\n            batch_name += datetime.now().strftime(\"%d%m%Y%H%M%S\")\n\n        output_filename_0 = self.preview_fname(render_path)\n\n        if not response_data:\n            response_data = {}\n\n        try:\n            # Ensure render folder exists\n            os.makedirs(render_dir)\n        except OSError:\n            pass\n\n        # resolve any limit groups\n        limit_groups = self.get_limit_groups()\n        self.log.debug(\"Limit groups: `{}`\".format(limit_groups))\n\n        payload = {\n            \"JobInfo\": {\n                # Top-level group name\n                \"BatchName\": batch_name,\n\n                # Job name, as seen in Monitor\n                \"Name\": job_name,\n\n                # Arbitrary username, for visualisation in Monitor\n                \"UserName\": self._deadline_user,\n\n                \"Priority\": instance.data[\"attributeValues\"].get(\n                    \"priority\", self.priority),\n                \"ChunkSize\": instance.data[\"attributeValues\"].get(\n                    \"chunk\", self.chunk_size),\n                \"ConcurrentTasks\": instance.data[\"attributeValues\"].get(\n                    \"concurrency\",\n                    self.concurrent_tasks\n                ),\n\n                \"Department\": self.department,\n\n                \"Pool\": instance.data.get(\"primaryPool\"),\n                \"SecondaryPool\": instance.data.get(\"secondaryPool\"),\n                \"Group\": self.group,\n\n                \"Plugin\": \"Nuke\",\n                \"Frames\": \"{start}-{end}\".format(\n                    start=start_frame,\n                    end=end_frame\n                ),\n                \"Comment\": self._comment,\n\n                # Optional, enable double-click to preview rendered\n                # frames from Deadline Monitor\n                \"OutputFilename0\": output_filename_0.replace(\"\\\\\", \"/\"),\n\n                # limiting groups\n                \"LimitGroups\": \",\".join(limit_groups)\n\n            },\n            \"PluginInfo\": {\n                # Input\n                \"SceneFile\": script_path,\n\n                # Output directory and filename\n                \"OutputFilePath\": render_dir.replace(\"\\\\\", \"/\"),\n                # \"OutputFilePrefix\": render_variables[\"filename_prefix\"],\n\n                # Mandatory for Deadline\n                \"Version\": self._ver.group(),\n\n                # Resolve relative references\n                \"ProjectPath\": script_path,\n                \"AWSAssetFile0\": render_path,\n\n                # using GPU by default\n                \"UseGpu\": instance.data[\"attributeValues\"].get(\n                    \"use_gpu\", self.use_gpu),\n\n                # Only the specific write node is rendered.\n                \"WriteNode\": exe_node_name\n            },\n\n            # Mandatory for Deadline, may be empty\n            \"AuxFiles\": []\n        }\n\n        # Add workfile dependency.\n        workfile_dependency = instance.data[\"attributeValues\"].get(\n            \"workfile_dependency\", self.workfile_dependency\n        )\n        if workfile_dependency:\n            payload[\"JobInfo\"].update({\"AssetDependency0\": script_path})\n\n        # TODO: rewrite for baking with sequences\n        if baking_submission:\n            payload[\"JobInfo\"].update({\n                \"JobType\": \"Normal\",\n                \"ChunkSize\": 99999999\n            })\n\n        if response_data.get(\"_id\"):\n            payload[\"JobInfo\"].update({\n                \"BatchName\": response_data[\"Props\"][\"Batch\"],\n                \"JobDependency0\": response_data[\"_id\"],\n            })\n\n        # Include critical environment variables with submission\n        keys = [\n            \"PYTHONPATH\",\n            \"PATH\",\n            \"AVALON_DB\",\n            \"AVALON_PROJECT\",\n            \"AVALON_ASSET\",\n            \"AVALON_TASK\",\n            \"AVALON_APP_NAME\",\n            \"FTRACK_API_KEY\",\n            \"FTRACK_API_USER\",\n            \"FTRACK_SERVER\",\n            \"PYBLISHPLUGINPATH\",\n            \"NUKE_PATH\",\n            \"TOOL_ENV\",\n            \"FOUNDRY_LICENSE\",\n            \"OPENPYPE_SG_USER\",\n        ]\n\n        # Add OpenPype version if we are running from build.\n        if is_running_from_build():\n            keys.append(\"OPENPYPE_VERSION\")\n\n        # Add mongo url if it's enabled\n        if instance.context.data.get(\"deadlinePassMongoUrl\"):\n            keys.append(\"OPENPYPE_MONGO\")\n\n        # add allowed keys from preset if any\n        if self.env_allowed_keys:\n            keys += self.env_allowed_keys\n\n        environment = dict({key: os.environ[key] for key in keys\n                            if key in os.environ}, **legacy_io.Session)\n\n        # to recognize render jobs\n        if AYON_SERVER_ENABLED:\n            environment[\"AYON_BUNDLE_NAME\"] = os.environ[\"AYON_BUNDLE_NAME\"]\n            render_job_label = \"AYON_RENDER_JOB\"\n        else:\n            render_job_label = \"OPENPYPE_RENDER_JOB\"\n\n        environment[render_job_label] = \"1\"\n\n        # finally search replace in values of any key\n        if self.env_search_replace_values:\n            for key, value in environment.items():\n                for _k, _v in self.env_search_replace_values.items():\n                    environment[key] = value.replace(_k, _v)\n\n        payload[\"JobInfo\"].update({\n            \"EnvironmentKeyValue%d\" % index: \"{key}={value}\".format(\n                key=key,\n                value=environment[key]\n            ) for index, key in enumerate(environment)\n        })\n\n        plugin = payload[\"JobInfo\"][\"Plugin\"]\n        self.log.debug(\"using render plugin : {}\".format(plugin))\n\n        self.log.debug(\"Submitting..\")\n        self.log.debug(json.dumps(payload, indent=4, sort_keys=True))\n\n        # adding expected files to instance.data\n        self.expected_files(\n            instance,\n            render_path,\n            start_frame,\n            end_frame\n        )\n\n        self.log.debug(\"__ expectedFiles: `{}`\".format(\n            instance.data[\"expectedFiles\"]))\n        response = requests.post(self.deadline_url, json=payload, timeout=10)\n\n        if not response.ok:\n            raise Exception(response.text)\n\n        return response\n\n    def preflight_check(self, instance):\n        \"\"\"Ensure the startFrame, endFrame and byFrameStep are integers\"\"\"\n\n        for key in (\"frameStart\", \"frameEnd\"):\n            value = instance.data[key]\n\n            if int(value) == value:\n                continue\n\n            self.log.warning(\n                \"%f=%d was rounded off to nearest integer\"\n                % (value, int(value))\n            )\n\n    def preview_fname(self, path):\n        \"\"\"Return output file path with #### for padding.\n\n        Deadline requires the path to be formatted with # in place of numbers.\n        For example `/path/to/render.####.png`\n\n        Args:\n            path (str): path to rendered images\n\n        Returns:\n            str\n\n        \"\"\"\n        self.log.debug(\"_ path: `{}`\".format(path))\n        if \"%\" in path:\n            search_results = re.search(r\"(%0)(\\d)(d.)\", path).groups()\n            self.log.debug(\"_ search_results: `{}`\".format(search_results))\n            return int(search_results[1])\n        if \"#\" in path:\n            self.log.debug(\"_ path: `{}`\".format(path))\n        return path\n\n    def expected_files(\n        self,\n        instance,\n        filepath,\n        start_frame,\n        end_frame\n    ):\n        \"\"\" Create expected files in instance data\n        \"\"\"\n        if not instance.data.get(\"expectedFiles\"):\n            instance.data[\"expectedFiles\"] = []\n\n        dirname = os.path.dirname(filepath)\n        file = os.path.basename(filepath)\n\n        # since some files might be already tagged as publish_on_farm\n        # we need to avoid adding them to expected files since those would be\n        # duplicated into metadata.json file\n        representations = instance.data.get(\"representations\", [])\n        # check if file is not in representations with publish_on_farm tag\n        for repre in representations:\n            # Skip if 'publish_on_farm' not available\n            if \"publish_on_farm\" not in repre.get(\"tags\", []):\n                continue\n\n            # in case where single file (video, image) is already in\n            # representation file. Will be added to expected files via\n            # submit_publish_job.py\n            if file in repre.get(\"files\", []):\n                self.log.debug(\n                    \"Skipping expected file: {}\".format(filepath))\n                return\n\n        # in case path is hashed sequence expression\n        # (e.g. /path/to/file.####.png)\n        if \"#\" in file:\n            pparts = file.split(\"#\")\n            padding = \"%0{}d\".format(len(pparts) - 1)\n            file = pparts[0] + padding + pparts[-1]\n\n        # in case input path was single file (video or image)\n        if \"%\" not in file:\n            instance.data[\"expectedFiles\"].append(filepath)\n            return\n\n        # shift start frame by 1 if slate is present\n        if instance.data.get(\"slate\"):\n            start_frame -= 1\n\n        # add sequence files to expected files\n        for i in range(start_frame, (end_frame + 1)):\n            instance.data[\"expectedFiles\"].append(\n                os.path.join(dirname, (file % i)).replace(\"\\\\\", \"/\"))\n\n    def get_limit_groups(self):\n        \"\"\"Search for limit group nodes and return group name.\n        Limit groups will be defined as pairs in Nuke deadline submitter\n        presents where the key will be name of limit group and value will be\n        a list of plugin's node class names. Thus, when a plugin uses more\n        than one node, these will be captured and the triggered process\n        will add the appropriate limit group to the payload jobinfo attributes.\n        Returning:\n            list: captured groups list\n        \"\"\"\n        # Not all hosts can import this module.\n        import nuke\n\n        captured_groups = []\n        for lg_name, list_node_class in self.limit_groups.items():\n            for node_class in list_node_class:\n                for node in nuke.allNodes(recurseGroups=True):\n                    # ignore all nodes not member of defined class\n                    if node.Class() not in node_class:\n                        continue\n                    # ignore all disabled nodes\n                    if node[\"disable\"].value():\n                        continue\n                    # add group name if not already added\n                    if lg_name not in captured_groups:\n                        captured_groups.append(lg_name)\n        return captured_groups\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Submit publishing job to farm.\"\"\"\nimport os\nimport json\nimport re\nfrom copy import deepcopy\nimport requests\n\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_last_version_by_subset_name,\n)\nfrom openpype.pipeline import publish, legacy_io\nfrom openpype.lib import EnumDef, is_running_from_build\nfrom openpype.tests.lib import is_in_tests\nfrom openpype.pipeline.version_start import get_versioning_start\n\nfrom openpype.pipeline.farm.pyblish_functions import (\n    create_skeleton_instance_cache,\n    create_instances_for_cache,\n    attach_instances_to_subset,\n    prepare_cache_representations,\n    create_metadata_path\n)\n\n\nclass ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin,\n                                     publish.OpenPypePyblishPluginMixin,\n                                     publish.ColormanagedPyblishPluginMixin):\n    \"\"\"Process Cache Job submitted on farm\n    This is replicated version of submit publish job\n    specifically for cache(s).\n\n    These jobs are dependent on a deadline job\n    submission prior to this plug-in.\n\n    - In case of Deadline, it creates dependent job on farm publishing\n      rendered image sequence.\n\n    Options in instance.data:\n        - deadlineSubmissionJob (dict, Required): The returned .json\n          data from the job submission to deadline.\n\n        - outputDir (str, Required): The output directory where the metadata\n            file should be generated. It's assumed that this will also be\n            final folder containing the output files.\n\n        - ext (str, Optional): The extension (including `.`) that is required\n            in the output filename to be picked up for image sequence\n            publishing.\n\n        - expectedFiles (list or dict): explained below\n\n    \"\"\"\n\n    label = \"Submit cache jobs to Deadline\"\n    order = pyblish.api.IntegratorOrder + 0.2\n    icon = \"tractor\"\n\n    targets = [\"local\"]\n\n    hosts = [\"houdini\"]\n\n    families = [\"publish.hou\"]\n\n    environ_job_filter = [\n        \"OPENPYPE_METADATA_FILE\"\n    ]\n\n    environ_keys = [\n        \"FTRACK_API_USER\",\n        \"FTRACK_API_KEY\",\n        \"FTRACK_SERVER\",\n        \"AVALON_APP_NAME\",\n        \"OPENPYPE_USERNAME\",\n        \"OPENPYPE_SG_USER\",\n        \"KITSU_LOGIN\",\n        \"KITSU_PWD\"\n    ]\n\n    # custom deadline attributes\n    deadline_department = \"\"\n    deadline_pool = \"\"\n    deadline_pool_secondary = \"\"\n    deadline_group = \"\"\n    deadline_chunk_size = 1\n    deadline_priority = None\n\n    # regex for finding frame number in string\n    R_FRAME_NUMBER = re.compile(r'.+\\.(?P<frame>[0-9]+)\\..+')\n\n    plugin_pype_version = \"3.0\"\n\n    # script path for publish_filesequence.py\n    publishing_script = None\n\n    def _submit_deadline_post_job(self, instance, job):\n        \"\"\"Submit publish job to Deadline.\n\n        Returns:\n            (str): deadline_publish_job_id\n        \"\"\"\n        data = instance.data.copy()\n        subset = data[\"subset\"]\n        job_name = \"Publish - {subset}\".format(subset=subset)\n\n        anatomy = instance.context.data['anatomy']\n\n        # instance.data.get(\"subset\") != instances[0][\"subset\"]\n        # 'Main' vs 'renderMain'\n        override_version = None\n        instance_version = instance.data.get(\"version\")  # take this if exists\n        if instance_version != 1:\n            override_version = instance_version\n\n        output_dir = self._get_publish_folder(\n            anatomy,\n            deepcopy(instance.data[\"anatomyData\"]),\n            instance.data.get(\"asset\"),\n            instance.data[\"subset\"],\n            instance.context,\n            instance.data[\"family\"],\n            override_version\n        )\n\n        # Transfer the environment from the original job to this dependent\n        # job so they use the same environment\n        metadata_path, rootless_metadata_path = \\\n            create_metadata_path(instance, anatomy)\n\n        environment = {\n            \"AVALON_PROJECT\": instance.context.data[\"projectName\"],\n            \"AVALON_ASSET\": instance.context.data[\"asset\"],\n            \"AVALON_TASK\": instance.context.data[\"task\"],\n            \"OPENPYPE_USERNAME\": instance.context.data[\"user\"],\n            \"OPENPYPE_LOG_NO_COLORS\": \"1\",\n            \"IS_TEST\": str(int(is_in_tests()))\n        }\n\n        if AYON_SERVER_ENABLED:\n            environment[\"AYON_PUBLISH_JOB\"] = \"1\"\n            environment[\"AYON_RENDER_JOB\"] = \"0\"\n            environment[\"AYON_REMOTE_PUBLISH\"] = \"0\"\n            environment[\"AYON_BUNDLE_NAME\"] = os.environ[\"AYON_BUNDLE_NAME\"]\n            deadline_plugin = \"Ayon\"\n        else:\n            environment[\"AVALON_DB\"] = os.environ[\"AVALON_DB\"]\n            environment[\"OPENPYPE_PUBLISH_JOB\"] = \"1\"\n            environment[\"OPENPYPE_RENDER_JOB\"] = \"0\"\n            environment[\"OPENPYPE_REMOTE_PUBLISH\"] = \"0\"\n            deadline_plugin = \"OpenPype\"\n            # Add OpenPype version if we are running from build.\n            if is_running_from_build():\n                self.environ_keys.append(\"OPENPYPE_VERSION\")\n\n        # add environments from self.environ_keys\n        for env_key in self.environ_keys:\n            if os.getenv(env_key):\n                environment[env_key] = os.environ[env_key]\n\n        # pass environment keys from self.environ_job_filter\n        job_environ = job[\"Props\"].get(\"Env\", {})\n        for env_j_key in self.environ_job_filter:\n            if job_environ.get(env_j_key):\n                environment[env_j_key] = job_environ[env_j_key]\n\n        # Add mongo url if it's enabled\n        if instance.context.data.get(\"deadlinePassMongoUrl\"):\n            mongo_url = os.environ.get(\"OPENPYPE_MONGO\")\n            if mongo_url:\n                environment[\"OPENPYPE_MONGO\"] = mongo_url\n\n        priority = self.deadline_priority or instance.data.get(\"priority\", 50)\n\n        instance_settings = self.get_attr_values_from_data(instance.data)\n        initial_status = instance_settings.get(\"publishJobState\", \"Active\")\n        # TODO: Remove this backwards compatibility of `suspend_publish`\n        if instance.data.get(\"suspend_publish\"):\n            initial_status = \"Suspended\"\n\n        args = [\n            \"--headless\",\n            'publish',\n            '\"{}\"'.format(rootless_metadata_path),\n            \"--targets\", \"deadline\",\n            \"--targets\", \"farm\"\n        ]\n\n        if is_in_tests():\n            args.append(\"--automatic-tests\")\n\n        # Generate the payload for Deadline submission\n        secondary_pool = (\n            self.deadline_pool_secondary or instance.data.get(\"secondaryPool\")\n        )\n        payload = {\n            \"JobInfo\": {\n                \"Plugin\": deadline_plugin,\n                \"BatchName\": job[\"Props\"][\"Batch\"],\n                \"Name\": job_name,\n                \"UserName\": job[\"Props\"][\"User\"],\n                \"Comment\": instance.context.data.get(\"comment\", \"\"),\n\n                \"Department\": self.deadline_department,\n                \"ChunkSize\": self.deadline_chunk_size,\n                \"Priority\": priority,\n                \"InitialStatus\": initial_status,\n\n                \"Group\": self.deadline_group,\n                \"Pool\": self.deadline_pool or instance.data.get(\"primaryPool\"),\n                \"SecondaryPool\": secondary_pool,\n                # ensure the outputdirectory with correct slashes\n                \"OutputDirectory0\": output_dir.replace(\"\\\\\", \"/\")\n            },\n            \"PluginInfo\": {\n                \"Version\": self.plugin_pype_version,\n                \"Arguments\": \" \".join(args),\n                \"SingleFrameOnly\": \"True\",\n            },\n            # Mandatory for Deadline, may be empty\n            \"AuxFiles\": [],\n        }\n\n        if job.get(\"_id\"):\n            payload[\"JobInfo\"][\"JobDependency0\"] = job[\"_id\"]\n\n        for index, (key_, value_) in enumerate(environment.items()):\n            payload[\"JobInfo\"].update(\n                {\n                    \"EnvironmentKeyValue%d\"\n                    % index: \"{key}={value}\".format(\n                        key=key_, value=value_\n                    )\n                }\n            )\n        # remove secondary pool\n        payload[\"JobInfo\"].pop(\"SecondaryPool\", None)\n\n        self.log.debug(\"Submitting Deadline publish job ...\")\n\n        url = \"{}/api/jobs\".format(self.deadline_url)\n        response = requests.post(url, json=payload, timeout=10)\n        if not response.ok:\n            raise Exception(response.text)\n\n        deadline_publish_job_id = response.json()[\"_id\"]\n\n        return deadline_publish_job_id\n\n    def process(self, instance):\n        # type: (pyblish.api.Instance) -> None\n        \"\"\"Process plugin.\n\n        Detect type of render farm submission and create and post dependent\n        job in case of Deadline. It creates json file with metadata needed for\n        publishing in directory of render.\n\n        Args:\n            instance (pyblish.api.Instance): Instance data.\n\n        \"\"\"\n        if not instance.data.get(\"farm\"):\n            self.log.debug(\"Skipping local instance.\")\n            return\n\n        anatomy = instance.context.data[\"anatomy\"]\n\n        instance_skeleton_data = create_skeleton_instance_cache(instance)\n        \"\"\"\n        if content of `expectedFiles` list are dictionaries, we will handle\n        it as list of AOVs, creating instance for every one of them.\n\n        Example:\n        --------\n\n        expectedFiles = [\n            {\n                \"beauty\": [\n                    \"foo_v01.0001.exr\",\n                    \"foo_v01.0002.exr\"\n                ],\n\n                \"Z\": [\n                    \"boo_v01.0001.exr\",\n                    \"boo_v01.0002.exr\"\n                ]\n            }\n        ]\n\n        This will create instances for `beauty` and `Z` subset\n        adding those files to their respective representations.\n\n        If we have only list of files, we collect all file sequences.\n        More then one doesn't probably make sense, but we'll handle it\n        like creating one instance with multiple representations.\n\n        Example:\n        --------\n\n        expectedFiles = [\n            \"foo_v01.0001.exr\",\n            \"foo_v01.0002.exr\",\n            \"xxx_v01.0001.exr\",\n            \"xxx_v01.0002.exr\"\n        ]\n\n        This will result in one instance with two representations:\n        `foo` and `xxx`\n        \"\"\"\n\n        if isinstance(instance.data.get(\"expectedFiles\")[0], dict):\n            instances = create_instances_for_cache(\n                instance, instance_skeleton_data)\n        else:\n            representations = prepare_cache_representations(\n                instance_skeleton_data,\n                instance.data.get(\"expectedFiles\"),\n                anatomy\n            )\n\n            if \"representations\" not in instance_skeleton_data.keys():\n                instance_skeleton_data[\"representations\"] = []\n\n            # add representation\n            instance_skeleton_data[\"representations\"] += representations\n            instances = [instance_skeleton_data]\n\n        # attach instances to subset\n        if instance.data.get(\"attachTo\"):\n            instances = attach_instances_to_subset(\n                instance.data.get(\"attachTo\"), instances\n            )\n\n        r''' SUBMiT PUBLiSH JOB 2 D34DLiN3\n          ____\n        '     '            .---.  .---. .--. .---. .--..--..--..--. .---.\n        |     |   --= \\   |  .  \\/   _|/    \\|  .  \\  ||  ||   \\  |/   _|\n        | JOB |   --= /   |  |  ||  __|  ..  |  |  |  |;_ ||  \\   ||  __|\n        |     |           |____./ \\.__|._||_.|___./|_____|||__|\\__|\\.___|\n        ._____.\n\n        '''\n\n        render_job = None\n        submission_type = \"\"\n        if instance.data.get(\"toBeRenderedOn\") == \"deadline\":\n            render_job = instance.data.pop(\"deadlineSubmissionJob\", None)\n            submission_type = \"deadline\"\n\n        if not render_job:\n            import getpass\n\n            render_job = {}\n            self.log.debug(\"Faking job data ...\")\n            render_job[\"Props\"] = {}\n            # Render job doesn't exist because we do not have prior submission.\n            # We still use data from it so lets fake it.\n            #\n            # Batch name reflect original scene name\n\n            if instance.data.get(\"assemblySubmissionJobs\"):\n                render_job[\"Props\"][\"Batch\"] = instance.data.get(\n                    \"jobBatchName\")\n            else:\n                batch = os.path.splitext(os.path.basename(\n                    instance.context.data.get(\"currentFile\")))[0]\n                render_job[\"Props\"][\"Batch\"] = batch\n            # User is deadline user\n            render_job[\"Props\"][\"User\"] = instance.context.data.get(\n                \"deadlineUser\", getpass.getuser())\n\n        deadline_publish_job_id = None\n        if submission_type == \"deadline\":\n            # get default deadline webservice url from deadline module\n            self.deadline_url = instance.context.data[\"defaultDeadline\"]\n            # if custom one is set in instance, use that\n            if instance.data.get(\"deadlineUrl\"):\n                self.deadline_url = instance.data.get(\"deadlineUrl\")\n            assert self.deadline_url, \"Requires Deadline Webservice URL\"\n\n            deadline_publish_job_id = \\\n                self._submit_deadline_post_job(instance, render_job)\n\n            # Inject deadline url to instances.\n            for inst in instances:\n                inst[\"deadlineUrl\"] = self.deadline_url\n\n        # publish job file\n        publish_job = {\n            \"asset\": instance_skeleton_data[\"asset\"],\n            \"frameStart\": instance_skeleton_data[\"frameStart\"],\n            \"frameEnd\": instance_skeleton_data[\"frameEnd\"],\n            \"fps\": instance_skeleton_data[\"fps\"],\n            \"source\": instance_skeleton_data[\"source\"],\n            \"user\": instance.context.data[\"user\"],\n            \"version\": instance.context.data[\"version\"],  # workfile version\n            \"intent\": instance.context.data.get(\"intent\"),\n            \"comment\": instance.context.data.get(\"comment\"),\n            \"job\": render_job or None,\n            \"session\": legacy_io.Session.copy(),\n            \"instances\": instances\n        }\n\n        if deadline_publish_job_id:\n            publish_job[\"deadline_publish_job_id\"] = deadline_publish_job_id\n\n        metadata_path, rootless_metadata_path = \\\n            create_metadata_path(instance, anatomy)\n\n        with open(metadata_path, \"w\") as f:\n            json.dump(publish_job, f, indent=4, sort_keys=True)\n\n    def _get_publish_folder(self, anatomy, template_data,\n                            asset, subset, context,\n                            family, version=None):\n        \"\"\"\n            Extracted logic to pre-calculate real publish folder, which is\n            calculated in IntegrateNew inside of Deadline process.\n            This should match logic in:\n                'collect_anatomy_instance_data' - to\n                    get correct anatomy, family, version for subset and\n                'collect_resources_path'\n                    get publish_path\n\n        Args:\n            anatomy (openpype.pipeline.anatomy.Anatomy):\n            template_data (dict): pre-calculated collected data for process\n            asset (string): asset name\n            subset (string): subset name (actually group name of subset)\n            family (string): for current deadline process it's always 'render'\n                TODO - for generic use family needs to be dynamically\n                    calculated like IntegrateNew does\n            version (int): override version from instance if exists\n\n        Returns:\n            (string): publish folder where rendered and published files will\n                be stored\n                based on 'publish' template\n        \"\"\"\n\n        project_name = context.data[\"projectName\"]\n        if not version:\n            version = get_last_version_by_subset_name(\n                project_name,\n                subset,\n                asset_name=asset\n            )\n            if version:\n                version = int(version[\"name\"]) + 1\n            else:\n                version = get_versioning_start(\n                    project_name,\n                    template_data[\"app\"],\n                    task_name=template_data[\"task\"][\"name\"],\n                    task_type=template_data[\"task\"][\"type\"],\n                    family=\"render\",\n                    subset=subset,\n                    project_settings=context.data[\"project_settings\"]\n                )\n\n        host_name = context.data[\"hostName\"]\n        task_info = template_data.get(\"task\") or {}\n\n        template_name = publish.get_publish_template_name(\n            project_name,\n            host_name,\n            family,\n            task_info.get(\"name\"),\n            task_info.get(\"type\"),\n        )\n\n        template_data[\"subset\"] = subset\n        template_data[\"family\"] = family\n        template_data[\"version\"] = version\n\n        render_templates = anatomy.templates_obj[template_name]\n        if \"folder\" in render_templates:\n            publish_folder = render_templates[\"folder\"].format_strict(\n                template_data\n            )\n        else:\n            # solve deprecated situation when `folder` key is not underneath\n            # `publish` anatomy\n            self.log.warning((\n                \"Deprecation warning: Anatomy does not have set `folder`\"\n                \" key underneath `publish` (in global of for project `{}`).\"\n            ).format(project_name))\n\n            file_path = render_templates[\"path\"].format_strict(template_data)\n            publish_folder = os.path.dirname(file_path)\n\n        return publish_folder\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            EnumDef(\"publishJobState\",\n                    label=\"Publish Job State\",\n                    items=[\"Active\", \"Suspended\"],\n                    default=\"Active\")\n        ]\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/submit_publish_job.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Submit publishing job to farm.\"\"\"\nimport os\nimport json\nimport re\nfrom copy import deepcopy\nimport requests\nimport clique\n\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_last_version_by_subset_name,\n)\nfrom openpype.pipeline import publish, legacy_io\nfrom openpype.lib import EnumDef, is_running_from_build\nfrom openpype.tests.lib import is_in_tests\nfrom openpype.pipeline.version_start import get_versioning_start\n\nfrom openpype.pipeline.farm.pyblish_functions import (\n    create_skeleton_instance,\n    create_instances_for_aov,\n    attach_instances_to_subset,\n    prepare_representations,\n    create_metadata_path\n)\n\n\ndef get_resource_files(resources, frame_range=None):\n    \"\"\"Get resource files at given path.\n\n    If `frame_range` is specified those outside will be removed.\n\n    Arguments:\n        resources (list): List of resources\n        frame_range (list): Frame range to apply override\n\n    Returns:\n        list of str: list of collected resources\n\n    \"\"\"\n    res_collections, _ = clique.assemble(resources)\n    assert len(res_collections) == 1, \"Multiple collections found\"\n    res_collection = res_collections[0]\n\n    # Remove any frames\n    if frame_range is not None:\n        for frame in frame_range:\n            if frame not in res_collection.indexes:\n                continue\n            res_collection.indexes.remove(frame)\n\n    return list(res_collection)\n\n\nclass ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,\n                                publish.OpenPypePyblishPluginMixin,\n                                publish.ColormanagedPyblishPluginMixin):\n    \"\"\"Process Job submitted on farm.\n\n    These jobs are dependent on a deadline job\n    submission prior to this plug-in.\n\n    It creates dependent job on farm publishing rendered image sequence.\n\n    Options in instance.data:\n        - deadlineSubmissionJob (dict, Required): The returned .json\n          data from the job submission to deadline.\n\n        - outputDir (str, Required): The output directory where the metadata\n            file should be generated. It's assumed that this will also be\n            final folder containing the output files.\n\n        - ext (str, Optional): The extension (including `.`) that is required\n            in the output filename to be picked up for image sequence\n            publishing.\n\n        - publishJobState (str, Optional): \"Active\" or \"Suspended\"\n            This defaults to \"Suspended\"\n\n        - expectedFiles (list or dict): explained below\n\n    \"\"\"\n\n    label = \"Submit Image Publishing job to Deadline\"\n    order = pyblish.api.IntegratorOrder + 0.2\n    icon = \"tractor\"\n\n    targets = [\"local\"]\n\n    hosts = [\"fusion\", \"max\", \"maya\", \"nuke\", \"houdini\",\n             \"celaction\", \"aftereffects\", \"harmony\", \"blender\"]\n\n    families = [\"render.farm\", \"render.frames_farm\",\n                \"prerender.farm\", \"prerender.frames_farm\",\n                \"renderlayer\", \"imagesequence\",\n                \"vrayscene\", \"maxrender\",\n                \"arnold_rop\", \"mantra_rop\",\n                \"karma_rop\", \"vray_rop\",\n                \"redshift_rop\"]\n\n    aov_filter = {\"maya\": [r\".*([Bb]eauty).*\"],\n                  \"blender\": [r\".*([Bb]eauty).*\"],\n                  \"aftereffects\": [r\".*\"],  # for everything from AE\n                  \"harmony\": [r\".*\"],  # for everything from AE\n                  \"celaction\": [r\".*\"],\n                  \"max\": [r\".*\"]}\n\n    environ_job_filter = [\n        \"OPENPYPE_METADATA_FILE\"\n    ]\n\n    environ_keys = [\n        \"FTRACK_API_USER\",\n        \"FTRACK_API_KEY\",\n        \"FTRACK_SERVER\",\n        \"AVALON_APP_NAME\",\n        \"OPENPYPE_USERNAME\",\n        \"OPENPYPE_SG_USER\",\n        \"KITSU_LOGIN\",\n        \"KITSU_PWD\"\n    ]\n\n    # custom deadline attributes\n    deadline_department = \"\"\n    deadline_pool = \"\"\n    deadline_pool_secondary = \"\"\n    deadline_group = \"\"\n    deadline_chunk_size = 1\n    deadline_priority = None\n\n    # regex for finding frame number in string\n    R_FRAME_NUMBER = re.compile(r'.+\\.(?P<frame>[0-9]+)\\..+')\n\n    # mapping of instance properties to be transferred to new instance\n    #     for every specified family\n    instance_transfer = {\n        \"slate\": [\"slateFrames\", \"slate\"],\n        \"review\": [\"lutPath\"],\n        \"render2d\": [\"bakingNukeScripts\", \"version\"],\n        \"renderlayer\": [\"convertToScanline\"]\n    }\n\n    # list of family names to transfer to new family if present\n    families_transfer = [\"render3d\", \"render2d\", \"ftrack\", \"slate\"]\n    plugin_pype_version = \"3.0\"\n\n    # script path for publish_filesequence.py\n    publishing_script = None\n\n    # poor man exclusion\n    skip_integration_repre_list = []\n\n    def _submit_deadline_post_job(self, instance, job, instances):\n        \"\"\"Submit publish job to Deadline.\n\n        Returns:\n            (str): deadline_publish_job_id\n        \"\"\"\n        data = instance.data.copy()\n        subset = data[\"subset\"]\n        job_name = \"Publish - {subset}\".format(subset=subset)\n\n        anatomy = instance.context.data['anatomy']\n\n        # instance.data.get(\"subset\") != instances[0][\"subset\"]\n        # 'Main' vs 'renderMain'\n        override_version = None\n        instance_version = instance.data.get(\"version\")  # take this if exists\n        if instance_version != 1:\n            override_version = instance_version\n\n        output_dir = self._get_publish_folder(\n            anatomy,\n            deepcopy(instance.data[\"anatomyData\"]),\n            instance.data.get(\"asset\"),\n            instances[0][\"subset\"],\n            instance.context,\n            instances[0][\"family\"],\n            override_version\n        )\n\n        # Transfer the environment from the original job to this dependent\n        # job so they use the same environment\n        metadata_path, rootless_metadata_path = \\\n            create_metadata_path(instance, anatomy)\n\n        environment = {\n            \"AVALON_PROJECT\": instance.context.data[\"projectName\"],\n            \"AVALON_ASSET\": instance.context.data[\"asset\"],\n            \"AVALON_TASK\": instance.context.data[\"task\"],\n            \"OPENPYPE_USERNAME\": instance.context.data[\"user\"],\n            \"OPENPYPE_LOG_NO_COLORS\": \"1\",\n            \"IS_TEST\": str(int(is_in_tests()))\n        }\n\n        if AYON_SERVER_ENABLED:\n            environment[\"AYON_PUBLISH_JOB\"] = \"1\"\n            environment[\"AYON_RENDER_JOB\"] = \"0\"\n            environment[\"AYON_REMOTE_PUBLISH\"] = \"0\"\n            environment[\"AYON_BUNDLE_NAME\"] = os.environ[\"AYON_BUNDLE_NAME\"]\n            deadline_plugin = \"Ayon\"\n        else:\n            environment[\"AVALON_DB\"] = os.environ[\"AVALON_DB\"]\n            environment[\"OPENPYPE_PUBLISH_JOB\"] = \"1\"\n            environment[\"OPENPYPE_RENDER_JOB\"] = \"0\"\n            environment[\"OPENPYPE_REMOTE_PUBLISH\"] = \"0\"\n            deadline_plugin = \"OpenPype\"\n            # Add OpenPype version if we are running from build.\n            if is_running_from_build():\n                self.environ_keys.append(\"OPENPYPE_VERSION\")\n\n        # add environments from self.environ_keys\n        for env_key in self.environ_keys:\n            if os.getenv(env_key):\n                environment[env_key] = os.environ[env_key]\n\n        # pass environment keys from self.environ_job_filter\n        job_environ = job[\"Props\"].get(\"Env\", {})\n        for env_j_key in self.environ_job_filter:\n            if job_environ.get(env_j_key):\n                environment[env_j_key] = job_environ[env_j_key]\n\n        # Add mongo url if it's enabled\n        if instance.context.data.get(\"deadlinePassMongoUrl\"):\n            mongo_url = os.environ.get(\"OPENPYPE_MONGO\")\n            if mongo_url:\n                environment[\"OPENPYPE_MONGO\"] = mongo_url\n\n        priority = self.deadline_priority or instance.data.get(\"priority\", 50)\n\n        instance_settings = self.get_attr_values_from_data(instance.data)\n        initial_status = instance_settings.get(\"publishJobState\", \"Active\")\n        # TODO: Remove this backwards compatibility of `suspend_publish`\n        if instance.data.get(\"suspend_publish\"):\n            initial_status = \"Suspended\"\n\n        args = [\n            \"--headless\",\n            'publish',\n            '\"{}\"'.format(rootless_metadata_path),\n            \"--targets\", \"deadline\",\n            \"--targets\", \"farm\"\n        ]\n\n        if is_in_tests():\n            args.append(\"--automatic-tests\")\n\n        # Generate the payload for Deadline submission\n        secondary_pool = (\n            self.deadline_pool_secondary or instance.data.get(\"secondaryPool\")\n        )\n        payload = {\n            \"JobInfo\": {\n                \"Plugin\": deadline_plugin,\n                \"BatchName\": job[\"Props\"][\"Batch\"],\n                \"Name\": job_name,\n                \"UserName\": job[\"Props\"][\"User\"],\n                \"Comment\": instance.context.data.get(\"comment\", \"\"),\n\n                \"Department\": self.deadline_department,\n                \"ChunkSize\": self.deadline_chunk_size,\n                \"Priority\": priority,\n                \"InitialStatus\": initial_status,\n\n                \"Group\": self.deadline_group,\n                \"Pool\": self.deadline_pool or instance.data.get(\"primaryPool\"),\n                \"SecondaryPool\": secondary_pool,\n                # ensure the outputdirectory with correct slashes\n                \"OutputDirectory0\": output_dir.replace(\"\\\\\", \"/\")\n            },\n            \"PluginInfo\": {\n                \"Version\": self.plugin_pype_version,\n                \"Arguments\": \" \".join(args),\n                \"SingleFrameOnly\": \"True\",\n            },\n            # Mandatory for Deadline, may be empty\n            \"AuxFiles\": [],\n        }\n\n        # add assembly jobs as dependencies\n        if instance.data.get(\"tileRendering\"):\n            self.log.info(\"Adding tile assembly jobs as dependencies...\")\n            job_index = 0\n            for assembly_id in instance.data.get(\"assemblySubmissionJobs\"):\n                payload[\"JobInfo\"][\"JobDependency{}\".format(\n                    job_index)] = assembly_id  # noqa: E501\n                job_index += 1\n        elif instance.data.get(\"bakingSubmissionJobs\"):\n            self.log.info(\n                \"Adding baking submission jobs as dependencies...\"\n            )\n            job_index = 0\n            for assembly_id in instance.data[\"bakingSubmissionJobs\"]:\n                payload[\"JobInfo\"][\"JobDependency{}\".format(\n                    job_index)] = assembly_id  # noqa: E501\n                job_index += 1\n        elif job.get(\"_id\"):\n            payload[\"JobInfo\"][\"JobDependency0\"] = job[\"_id\"]\n\n        for index, (key_, value_) in enumerate(environment.items()):\n            payload[\"JobInfo\"].update(\n                {\n                    \"EnvironmentKeyValue%d\"\n                    % index: \"{key}={value}\".format(\n                        key=key_, value=value_\n                    )\n                }\n            )\n        # remove secondary pool\n        payload[\"JobInfo\"].pop(\"SecondaryPool\", None)\n\n        self.log.debug(\"Submitting Deadline publish job ...\")\n\n        url = \"{}/api/jobs\".format(self.deadline_url)\n        response = requests.post(url, json=payload, timeout=10)\n        if not response.ok:\n            raise Exception(response.text)\n\n        deadline_publish_job_id = response.json()[\"_id\"]\n\n        return deadline_publish_job_id\n\n    def process(self, instance):\n        # type: (pyblish.api.Instance) -> None\n        \"\"\"Process plugin.\n\n        Detect type of render farm submission and create and post dependent\n        job in case of Deadline. It creates json file with metadata needed for\n        publishing in directory of render.\n\n        Args:\n            instance (pyblish.api.Instance): Instance data.\n\n        \"\"\"\n        if not instance.data.get(\"farm\"):\n            self.log.debug(\"Skipping local instance.\")\n            return\n\n        anatomy = instance.context.data[\"anatomy\"]\n\n        instance_skeleton_data = create_skeleton_instance(\n            instance, families_transfer=self.families_transfer,\n            instance_transfer=self.instance_transfer)\n        \"\"\"\n        if content of `expectedFiles` list are dictionaries, we will handle\n        it as list of AOVs, creating instance for every one of them.\n\n        Example:\n        --------\n\n        expectedFiles = [\n            {\n                \"beauty\": [\n                    \"foo_v01.0001.exr\",\n                    \"foo_v01.0002.exr\"\n                ],\n\n                \"Z\": [\n                    \"boo_v01.0001.exr\",\n                    \"boo_v01.0002.exr\"\n                ]\n            }\n        ]\n\n        This will create instances for `beauty` and `Z` subset\n        adding those files to their respective representations.\n\n        If we have only list of files, we collect all file sequences.\n        More then one doesn't probably make sense, but we'll handle it\n        like creating one instance with multiple representations.\n\n        Example:\n        --------\n\n        expectedFiles = [\n            \"foo_v01.0001.exr\",\n            \"foo_v01.0002.exr\",\n            \"xxx_v01.0001.exr\",\n            \"xxx_v01.0002.exr\"\n        ]\n\n        This will result in one instance with two representations:\n        `foo` and `xxx`\n        \"\"\"\n        do_not_add_review = False\n        if instance.data.get(\"review\") is False:\n            self.log.debug(\"Instance has review explicitly disabled.\")\n            do_not_add_review = True\n\n        if isinstance(instance.data.get(\"expectedFiles\")[0], dict):\n            instances = create_instances_for_aov(\n                instance, instance_skeleton_data,\n                self.aov_filter, self.skip_integration_repre_list,\n                do_not_add_review)\n        else:\n            representations = prepare_representations(\n                instance_skeleton_data,\n                instance.data.get(\"expectedFiles\"),\n                anatomy,\n                self.aov_filter,\n                self.skip_integration_repre_list,\n                do_not_add_review,\n                instance.context,\n                self\n            )\n\n            if \"representations\" not in instance_skeleton_data.keys():\n                instance_skeleton_data[\"representations\"] = []\n\n            # add representation\n            instance_skeleton_data[\"representations\"] += representations\n            instances = [instance_skeleton_data]\n\n        # attach instances to subset\n        if instance.data.get(\"attachTo\"):\n            instances = attach_instances_to_subset(\n                instance.data.get(\"attachTo\"), instances\n            )\n\n        r''' SUBMiT PUBLiSH JOB 2 D34DLiN3\n          ____\n        '     '            .---.  .---. .--. .---. .--..--..--..--. .---.\n        |     |   --= \\   |  .  \\/   _|/    \\|  .  \\  ||  ||   \\  |/   _|\n        | JOB |   --= /   |  |  ||  __|  ..  |  |  |  |;_ ||  \\   ||  __|\n        |     |           |____./ \\.__|._||_.|___./|_____|||__|\\__|\\.___|\n        ._____.\n\n        '''\n\n        render_job = instance.data.pop(\"deadlineSubmissionJob\", None)\n        if not render_job and instance.data.get(\"tileRendering\") is False:\n            raise AssertionError((\"Cannot continue without valid \"\n                                  \"Deadline submission.\"))\n        if not render_job:\n            import getpass\n\n            render_job = {}\n            self.log.debug(\"Faking job data ...\")\n            render_job[\"Props\"] = {}\n            # Render job doesn't exist because we do not have prior submission.\n            # We still use data from it so lets fake it.\n            #\n            # Batch name reflect original scene name\n\n            if instance.data.get(\"assemblySubmissionJobs\"):\n                render_job[\"Props\"][\"Batch\"] = instance.data.get(\n                    \"jobBatchName\")\n            else:\n                batch = os.path.splitext(os.path.basename(\n                    instance.context.data.get(\"currentFile\")))[0]\n                render_job[\"Props\"][\"Batch\"] = batch\n            # User is deadline user\n            render_job[\"Props\"][\"User\"] = instance.context.data.get(\n                \"deadlineUser\", getpass.getuser())\n\n            render_job[\"Props\"][\"Env\"] = {\n                \"FTRACK_API_USER\": os.environ.get(\"FTRACK_API_USER\"),\n                \"FTRACK_API_KEY\": os.environ.get(\"FTRACK_API_KEY\"),\n                \"FTRACK_SERVER\": os.environ.get(\"FTRACK_SERVER\"),\n            }\n\n        # get default deadline webservice url from deadline module\n        self.deadline_url = instance.context.data[\"defaultDeadline\"]\n        # if custom one is set in instance, use that\n        if instance.data.get(\"deadlineUrl\"):\n            self.deadline_url = instance.data.get(\"deadlineUrl\")\n        assert self.deadline_url, \"Requires Deadline Webservice URL\"\n\n        deadline_publish_job_id = \\\n            self._submit_deadline_post_job(instance, render_job, instances)\n\n        # Inject deadline url to instances.\n        for inst in instances:\n            inst[\"deadlineUrl\"] = self.deadline_url\n\n        # publish job file\n        publish_job = {\n            \"asset\": instance_skeleton_data[\"asset\"],\n            \"frameStart\": instance_skeleton_data[\"frameStart\"],\n            \"frameEnd\": instance_skeleton_data[\"frameEnd\"],\n            \"fps\": instance_skeleton_data[\"fps\"],\n            \"source\": instance_skeleton_data[\"source\"],\n            \"user\": instance.context.data[\"user\"],\n            \"version\": instance.context.data[\"version\"],  # workfile version\n            \"intent\": instance.context.data.get(\"intent\"),\n            \"comment\": instance.context.data.get(\"comment\"),\n            \"job\": render_job or None,\n            \"session\": legacy_io.Session.copy(),\n            \"instances\": instances\n        }\n\n        if deadline_publish_job_id:\n            publish_job[\"deadline_publish_job_id\"] = deadline_publish_job_id\n\n        # add audio to metadata file if available\n        audio_file = instance.context.data.get(\"audioFile\")\n        if audio_file and os.path.isfile(audio_file):\n            publish_job.update({\"audio\": audio_file})\n\n        metadata_path, rootless_metadata_path = \\\n            create_metadata_path(instance, anatomy)\n\n        with open(metadata_path, \"w\") as f:\n            json.dump(publish_job, f, indent=4, sort_keys=True)\n\n    def _get_publish_folder(self, anatomy, template_data,\n                            asset, subset, context,\n                            family, version=None):\n        \"\"\"\n            Extracted logic to pre-calculate real publish folder, which is\n            calculated in IntegrateNew inside of Deadline process.\n            This should match logic in:\n                'collect_anatomy_instance_data' - to\n                    get correct anatomy, family, version for subset and\n                'collect_resources_path'\n                    get publish_path\n\n        Args:\n            anatomy (openpype.pipeline.anatomy.Anatomy):\n            template_data (dict): pre-calculated collected data for process\n            asset (string): asset name\n            subset (string): subset name (actually group name of subset)\n            family (string): for current deadline process it's always 'render'\n                TODO - for generic use family needs to be dynamically\n                    calculated like IntegrateNew does\n            version (int): override version from instance if exists\n\n        Returns:\n            (string): publish folder where rendered and published files will\n                be stored\n                based on 'publish' template\n        \"\"\"\n\n        project_name = context.data[\"projectName\"]\n        host_name = context.data[\"hostName\"]\n        if not version:\n            version = get_last_version_by_subset_name(\n                project_name,\n                subset,\n                asset_name=asset\n            )\n            if version:\n                version = int(version[\"name\"]) + 1\n            else:\n                version = get_versioning_start(\n                    project_name,\n                    host_name,\n                    task_name=template_data[\"task\"][\"name\"],\n                    task_type=template_data[\"task\"][\"type\"],\n                    family=\"render\",\n                    subset=subset,\n                    project_settings=context.data[\"project_settings\"]\n                )\n\n        host_name = context.data[\"hostName\"]\n        task_info = template_data.get(\"task\") or {}\n\n        template_name = publish.get_publish_template_name(\n            project_name,\n            host_name,\n            family,\n            task_info.get(\"name\"),\n            task_info.get(\"type\"),\n        )\n\n        template_data[\"subset\"] = subset\n        template_data[\"family\"] = family\n        template_data[\"version\"] = version\n\n        render_templates = anatomy.templates_obj[template_name]\n        if \"folder\" in render_templates:\n            publish_folder = render_templates[\"folder\"].format_strict(\n                template_data\n            )\n        else:\n            # solve deprecated situation when `folder` key is not underneath\n            # `publish` anatomy\n            self.log.warning((\n                \"Deprecation warning: Anatomy does not have set `folder`\"\n                \" key underneath `publish` (in global of for project `{}`).\"\n            ).format(project_name))\n\n            file_path = render_templates[\"path\"].format_strict(template_data)\n            publish_folder = os.path.dirname(file_path)\n\n        return publish_folder\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            EnumDef(\"publishJobState\",\n                    label=\"Publish Job State\",\n                    items=[\"Active\", \"Suspended\"],\n                    default=\"Active\")\n        ]\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/validate_deadline_connection.py",
    "content": "import pyblish.api\n\nfrom openpype_modules.deadline.abstract_submit_deadline import requests_get\n\n\nclass ValidateDeadlineConnection(pyblish.api.InstancePlugin):\n    \"\"\"Validate Deadline Web Service is running\"\"\"\n\n    label = \"Validate Deadline Web Service\"\n    order = pyblish.api.ValidatorOrder\n    hosts = [\"maya\", \"nuke\"]\n    families = [\"renderlayer\", \"render\"]\n\n    # cache\n    responses = {}\n\n    def process(self, instance):\n        # get default deadline webservice url from deadline module\n        deadline_url = instance.context.data[\"defaultDeadline\"]\n        # if custom one is set in instance, use that\n        if instance.data.get(\"deadlineUrl\"):\n            deadline_url = instance.data.get(\"deadlineUrl\")\n            self.log.debug(\n                \"We have deadline URL on instance {}\".format(deadline_url)\n            )\n        assert deadline_url, \"Requires Deadline Webservice URL\"\n\n        if deadline_url not in self.responses:\n            self.responses[deadline_url] = requests_get(deadline_url)\n\n        response = self.responses[deadline_url]\n        assert response.ok, \"Response must be ok\"\n        assert response.text.startswith(\"Deadline Web Service \"), (\n            \"Web service did not respond with 'Deadline Web Service'\"\n        )\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/validate_deadline_pools.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline import (\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin\n)\nfrom openpype.modules.deadline.deadline_module import DeadlineModule\n\n\nclass ValidateDeadlinePools(OptionalPyblishPluginMixin,\n                            pyblish.api.InstancePlugin):\n    \"\"\"Validate primaryPool and secondaryPool on instance.\n\n    Values are on instance based on value insertion when Creating instance or\n    by Settings in CollectDeadlinePools.\n    \"\"\"\n\n    label = \"Validate Deadline Pools\"\n    order = pyblish.api.ValidatorOrder\n    families = [\"rendering\",\n                \"render.farm\",\n                \"render.frames_farm\",\n                \"renderFarm\",\n                \"renderlayer\",\n                \"maxrender\",\n                \"publish.hou\"]\n    optional = True\n\n    # cache\n    pools_per_url = {}\n\n    def process(self, instance):\n        if not self.is_active(instance.data):\n            return\n\n        if not instance.data.get(\"farm\"):\n            self.log.debug(\"Skipping local instance.\")\n            return\n\n        deadline_url = self.get_deadline_url(instance)\n        pools = self.get_pools(deadline_url)\n\n        invalid_pools = {}\n        primary_pool = instance.data.get(\"primaryPool\")\n        if primary_pool and primary_pool not in pools:\n            invalid_pools[\"primary\"] = primary_pool\n\n        secondary_pool = instance.data.get(\"secondaryPool\")\n        if secondary_pool and secondary_pool not in pools:\n            invalid_pools[\"secondary\"] = secondary_pool\n\n        if invalid_pools:\n            message = \"\\n\".join(\n                \"{} pool '{}' not available on Deadline\".format(key.title(),\n                                                                pool)\n                for key, pool in invalid_pools.items()\n            )\n            raise PublishXmlValidationError(\n                plugin=self,\n                message=message,\n                formatting_data={\"pools_str\": \", \".join(pools)}\n            )\n\n    def get_deadline_url(self, instance):\n        # get default deadline webservice url from deadline module\n        deadline_url = instance.context.data[\"defaultDeadline\"]\n        if instance.data.get(\"deadlineUrl\"):\n            # if custom one is set in instance, use that\n            deadline_url = instance.data.get(\"deadlineUrl\")\n        return deadline_url\n\n    def get_pools(self, deadline_url):\n        if deadline_url not in self.pools_per_url:\n            self.log.debug(\n                \"Querying available pools for Deadline url: {}\".format(\n                    deadline_url)\n            )\n            pools = DeadlineModule.get_deadline_pools(deadline_url,\n                                                      log=self.log)\n            self.log.info(\"Available pools: {}\".format(pools))\n            self.pools_per_url[deadline_url] = pools\n\n        return self.pools_per_url[deadline_url]\n"
  },
  {
    "path": "openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py",
    "content": "import os\nimport requests\n\nimport pyblish.api\n\nfrom openpype.lib import collect_frames\nfrom openpype_modules.deadline.abstract_submit_deadline import requests_get\n\n\nclass ValidateExpectedFiles(pyblish.api.InstancePlugin):\n    \"\"\"Compare rendered and expected files\"\"\"\n\n    label = \"Validate rendered files from Deadline\"\n    order = pyblish.api.ValidatorOrder\n    families = [\"render\"]\n    targets = [\"deadline\"]\n\n    # check if actual frame range on render job wasn't different\n    # case when artists wants to render only subset of frames\n    allow_user_override = True\n\n    def process(self, instance):\n        \"\"\"Process all the nodes in the instance\"\"\"\n\n        # get dependency jobs ids for retrieving frame list\n        dependent_job_ids = self._get_dependent_job_ids(instance)\n\n        if not dependent_job_ids:\n            self.log.warning(\"No dependent jobs found for instance: {}\"\n                             \"\".format(instance))\n            return\n\n        # get list of frames from dependent jobs\n        frame_list = self._get_dependent_jobs_frames(\n            instance, dependent_job_ids)\n\n        for repre in instance.data[\"representations\"]:\n            expected_files = self._get_expected_files(repre)\n\n            staging_dir = repre[\"stagingDir\"]\n            existing_files = self._get_existing_files(staging_dir)\n\n            if self.allow_user_override:\n                # We always check for user override because the user might have\n                # also overridden the Job frame list to be longer than the\n                # originally submitted frame range\n                # todo: We should first check if Job frame range was overridden\n                #       at all so we don't unnecessarily override anything\n                file_name_template, frame_placeholder = \\\n                    self._get_file_name_template_and_placeholder(\n                        expected_files)\n\n                if not file_name_template:\n                    raise RuntimeError(\"Unable to retrieve file_name template\"\n                                       \"from files: {}\".format(expected_files))\n\n                job_expected_files = self._get_job_expected_files(\n                    file_name_template,\n                    frame_placeholder,\n                    frame_list)\n\n                job_files_diff = job_expected_files.difference(expected_files)\n                if job_files_diff:\n                    self.log.debug(\n                        \"Detected difference in expected output files from \"\n                        \"Deadline job. Assuming an updated frame list by the \"\n                        \"user. Difference: {}\".format(sorted(job_files_diff))\n                    )\n\n                    # Update the representation expected files\n                    self.log.info(\"Update range from actual job range \"\n                                  \"to frame list: {}\".format(frame_list))\n                    # single item files must be string not list\n                    repre[\"files\"] = (sorted(job_expected_files)\n                                      if len(job_expected_files) > 1 else\n                                      list(job_expected_files)[0])\n\n                    # Update the expected files\n                    expected_files = job_expected_files\n\n            # We don't use set.difference because we do allow other existing\n            # files to be in the folder that we might not want to use.\n            missing = expected_files - existing_files\n            if missing:\n                raise RuntimeError(\n                    \"Missing expected files: {}\\n\"\n                    \"Expected files: {}\\n\"\n                    \"Existing files: {}\".format(\n                        sorted(missing),\n                        sorted(expected_files),\n                        sorted(existing_files)\n                    )\n                )\n\n    def _get_dependent_job_ids(self, instance):\n        \"\"\"Returns list of dependent job ids from instance metadata.json\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n\n        Returns:\n            (list): list of dependent job ids\n\n        \"\"\"\n        dependent_job_ids = []\n\n        # job_id collected from metadata.json\n        original_job_id = instance.data[\"render_job_id\"]\n\n        dependent_job_ids_env = os.environ.get(\"RENDER_JOB_IDS\")\n        if dependent_job_ids_env:\n            dependent_job_ids = dependent_job_ids_env.split(',')\n        elif original_job_id:\n            dependent_job_ids = [original_job_id]\n\n        return dependent_job_ids\n\n    def _get_dependent_jobs_frames(self, instance, dependent_job_ids):\n        \"\"\"Returns list of frame ranges from all render job.\n\n        Render job might be re-submitted so job_id in metadata.json could be\n        invalid. GlobalJobPreload injects current job id to RENDER_JOB_IDS.\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n            dependent_job_ids (list): list of dependent job ids\n        Returns:\n            (list)\n        \"\"\"\n        all_frame_lists = []\n\n        for job_id in dependent_job_ids:\n            job_info = self._get_job_info(instance, job_id)\n            frame_list = job_info[\"Props\"].get(\"Frames\")\n            if frame_list:\n                all_frame_lists.extend(frame_list.split(','))\n\n        return all_frame_lists\n\n    def _get_job_expected_files(self,\n                                file_name_template,\n                                frame_placeholder,\n                                frame_list):\n        \"\"\"Calculates list of names of expected rendered files.\n\n        Might be different from expected files from submission if user\n        explicitly and manually changed the frame list on the Deadline job.\n\n        \"\"\"\n        # no frames in file name at all, eg 'renderCompositingMain.withLut.mov'\n        if not frame_placeholder:\n            return set([file_name_template])\n\n        real_expected_rendered = set()\n        src_padding_exp = \"%0{}d\".format(len(frame_placeholder))\n        for frames in frame_list:\n            if '-' not in frames:  # single frame\n                frames = \"{}-{}\".format(frames, frames)\n\n            start, end = frames.split('-')\n            for frame in range(int(start), int(end) + 1):\n                ren_name = file_name_template.replace(\n                    frame_placeholder, src_padding_exp % frame)\n                real_expected_rendered.add(ren_name)\n\n        return real_expected_rendered\n\n    def _get_file_name_template_and_placeholder(self, files):\n        \"\"\"Returns file name with frame replaced with # and this placeholder\"\"\"\n        sources_and_frames = collect_frames(files)\n\n        file_name_template = frame_placeholder = None\n        for file_name, frame in sources_and_frames.items():\n\n            # There might be cases where clique was unable to collect\n            # collections in `collect_frames` - thus we capture that case\n            if frame is not None:\n                frame_placeholder = \"#\" * len(frame)\n\n                file_name_template = os.path.basename(\n                    file_name.replace(frame, frame_placeholder))\n            else:\n                file_name_template = file_name\n            break\n\n        return file_name_template, frame_placeholder\n\n    def _get_job_info(self, instance, job_id):\n        \"\"\"Calls DL for actual job info for 'job_id'\n\n        Might be different than job info saved in metadata.json if user\n        manually changes job pre/during rendering.\n\n        Args:\n            instance (pyblish.api.Instance): pyblish instance\n            job_id (str): Deadline job id\n\n        Returns:\n            (dict): Job info from Deadline\n\n        \"\"\"\n        # get default deadline webservice url from deadline module\n        deadline_url = instance.context.data[\"defaultDeadline\"]\n        # if custom one is set in instance, use that\n        if instance.data.get(\"deadlineUrl\"):\n            deadline_url = instance.data.get(\"deadlineUrl\")\n        assert deadline_url, \"Requires Deadline Webservice URL\"\n\n        url = \"{}/api/jobs?JobID={}\".format(deadline_url, job_id)\n        try:\n            response = requests_get(url)\n        except requests.exceptions.ConnectionError:\n            self.log.error(\"Deadline is not accessible at \"\n                           \"{}\".format(deadline_url))\n            return {}\n\n        if not response.ok:\n            self.log.error(\"Submission failed!\")\n            self.log.error(response.status_code)\n            self.log.error(response.content)\n            raise RuntimeError(response.text)\n\n        json_content = response.json()\n        if json_content:\n            return json_content.pop()\n        return {}\n\n    def _get_existing_files(self, staging_dir):\n        \"\"\"Returns set of existing file names from 'staging_dir'\"\"\"\n        existing_files = set()\n        for file_name in os.listdir(staging_dir):\n            existing_files.add(file_name)\n        return existing_files\n\n    def _get_expected_files(self, repre):\n        \"\"\"Returns set of file names in representation['files']\n\n        The representations are collected from `CollectRenderedFiles` using\n        the metadata.json file submitted along with the render job.\n\n        Args:\n            repre (dict): The representation containing 'files'\n\n        Returns:\n            set: Set of expected file_names in the staging directory.\n\n        \"\"\"\n        expected_files = set()\n\n        files = repre[\"files\"]\n        if not isinstance(files, list):\n            files = [files]\n\n        for file_name in files:\n            expected_files.add(file_name)\n        return expected_files\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.options",
    "content": "[Arguments]\nType=string\nLabel=Arguments\nCategory=Python Options\nCategoryOrder=0\nIndex=1\nDescription=The arguments to pass to the script. If no arguments are required, leave this blank.\nRequired=false\nDisableIfBlank=true\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.param",
    "content": "[About]\nType=label\nLabel=About\nCategory=About Plugin\nCategoryOrder=-1\nIndex=0\nDefault=Ayon Plugin for Deadline\nDescription=Not configurable\n\n[AyonExecutable]\nType=multilinemultifilename\nLabel=Ayon Executable\nCategory=Ayon Executables\nCategoryOrder=1\nIndex=0\nDefault=\nDescription=The path to the Ayon executable. Enter alternative paths on separate lines.\n\n[AyonServerUrl]\nType=string\nLabel=Ayon Server Url\nCategory=Ayon Credentials\nCategoryOrder=2\nIndex=0\nDefault=\nDescription=Url to Ayon server\n\n[AyonApiKey]\nType=password\nLabel=Ayon API key\nCategory=Ayon Credentials\nCategoryOrder=2\nIndex=0\nDefault=\nDescription=API key for service account on Ayon Server\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py",
    "content": "#!/usr/bin/env python3\n\nfrom System.IO import Path\nfrom System.Text.RegularExpressions import Regex\n\nfrom Deadline.Plugins import PluginType, DeadlinePlugin\nfrom Deadline.Scripting import (\n    StringUtils,\n    FileUtils,\n    DirectoryUtils,\n    RepositoryUtils\n)\n\nimport re\nimport os\nimport platform\n\n\n######################################################################\n# This is the function that Deadline calls to get an instance of the\n# main DeadlinePlugin class.\n######################################################################\ndef GetDeadlinePlugin():\n    return AyonDeadlinePlugin()\n\n\ndef CleanupDeadlinePlugin(deadlinePlugin):\n    deadlinePlugin.Cleanup()\n\n\nclass AyonDeadlinePlugin(DeadlinePlugin):\n    \"\"\"\n        Standalone plugin for publishing from Ayon\n\n        Calls Ayonexecutable 'ayon_console' from first correctly found\n        file based on plugin configuration. Uses 'publish' command and passes\n        path to metadata json file, which contains all needed information\n        for publish process.\n    \"\"\"\n    def __init__(self):\n        super().__init__()\n        self.InitializeProcessCallback += self.InitializeProcess\n        self.RenderExecutableCallback += self.RenderExecutable\n        self.RenderArgumentCallback += self.RenderArgument\n\n    def Cleanup(self):\n        for stdoutHandler in self.StdoutHandlers:\n            del stdoutHandler.HandleCallback\n\n        del self.InitializeProcessCallback\n        del self.RenderExecutableCallback\n        del self.RenderArgumentCallback\n\n    def InitializeProcess(self):\n        self.PluginType = PluginType.Simple\n        self.StdoutHandling = True\n\n        self.SingleFramesOnly = self.GetBooleanPluginInfoEntryWithDefault(\n            \"SingleFramesOnly\", False)\n        self.LogInfo(\"Single Frames Only: %s\" % self.SingleFramesOnly)\n\n        self.AddStdoutHandlerCallback(\n            \".*Progress: (\\d+)%.*\").HandleCallback += self.HandleProgress\n\n    def RenderExecutable(self):\n        job = self.GetJob()\n\n        # set required env vars for Ayon\n        # cannot be in InitializeProcess as it is too soon\n        config = RepositoryUtils.GetPluginConfig(\"Ayon\")\n        ayon_server_url = (\n                job.GetJobEnvironmentKeyValue(\"AYON_SERVER_URL\") or\n                config.GetConfigEntryWithDefault(\"AyonServerUrl\", \"\")\n        )\n        ayon_api_key = (\n                job.GetJobEnvironmentKeyValue(\"AYON_API_KEY\") or\n                config.GetConfigEntryWithDefault(\"AyonApiKey\", \"\")\n        )\n        ayon_bundle_name = job.GetJobEnvironmentKeyValue(\"AYON_BUNDLE_NAME\")\n\n        environment = {\n            \"AYON_SERVER_URL\": ayon_server_url,\n            \"AYON_API_KEY\": ayon_api_key,\n            \"AYON_BUNDLE_NAME\": ayon_bundle_name,\n        }\n\n        for env, val in environment.items():\n            self.SetEnvironmentVariable(env, val)\n\n        exe_list = self.GetConfigEntry(\"AyonExecutable\")\n        # clean '\\ ' for MacOS pasting\n        if platform.system().lower() == \"darwin\":\n            exe_list = exe_list.replace(\"\\\\ \", \" \")\n\n        expanded_paths = []\n        for path in exe_list.split(\";\"):\n            if path.startswith(\"~\"):\n                path = os.path.expanduser(path)\n            expanded_paths.append(path)\n        exe = FileUtils.SearchFileList(\";\".join(expanded_paths))\n\n        if exe == \"\":\n            self.FailRender(\n                \"Ayon executable was not found in the semicolon separated \"\n                \"list: \\\"{}\\\". The path to the render executable can be \"\n                \"configured from the Plugin Configuration in the Deadline \"\n                \"Monitor.\".format(exe_list)\n            )\n        return exe\n\n    def RenderArgument(self):\n        arguments = str(self.GetPluginInfoEntryWithDefault(\"Arguments\", \"\"))\n        arguments = RepositoryUtils.CheckPathMapping(arguments)\n\n        arguments = re.sub(r\"<(?i)STARTFRAME>\", str(self.GetStartFrame()),\n                           arguments)\n        arguments = re.sub(r\"<(?i)ENDFRAME>\", str(self.GetEndFrame()),\n                           arguments)\n        arguments = re.sub(r\"<(?i)QUOTE>\", \"\\\"\", arguments)\n\n        arguments = self.ReplacePaddedFrame(arguments,\n                                            \"<(?i)STARTFRAME%([0-9]+)>\",\n                                            self.GetStartFrame())\n        arguments = self.ReplacePaddedFrame(arguments,\n                                            \"<(?i)ENDFRAME%([0-9]+)>\",\n                                            self.GetEndFrame())\n\n        count = 0\n        for filename in self.GetAuxiliaryFilenames():\n            localAuxFile = Path.Combine(self.GetJobsDataDirectory(), filename)\n            arguments = re.sub(r\"<(?i)AUXFILE\" + str(count) + r\">\",\n                               localAuxFile.replace(\"\\\\\", \"/\"), arguments)\n            count += 1\n\n        return arguments\n\n    def ReplacePaddedFrame(self, arguments, pattern, frame):\n        frameRegex = Regex(pattern)\n        while True:\n            frameMatch = frameRegex.Match(arguments)\n            if not frameMatch.Success:\n                break\n            paddingSize = int(frameMatch.Groups[1].Value)\n            if paddingSize > 0:\n                padding = StringUtils.ToZeroPaddedString(\n                    frame, paddingSize, False)\n            else:\n                padding = str(frame)\n            arguments = arguments.replace(\n                frameMatch.Groups[0].Value, padding)\n\n        return arguments\n\n    def HandleProgress(self):\n        progress = float(self.GetRegexMatch(1))\n        self.SetProgress(progress)\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/CelAction/CelAction.param",
    "content": "[About]\nType=label\nLabel=About\nCategory=About Plugin\nCategoryOrder=-1\nIndex=0\nDefault=Celaction Plugin for Deadline\nDescription=Not configurable\n\n[ConcurrentTasks]\nType=label\nLabel=ConcurrentTasks\nCategory=About Plugin\nCategoryOrder=-1\nIndex=0\nDefault=True\nDescription=Not configurable\n\n[Executable]\nType=filename\nLabel=Executable\nCategory=Config\nCategoryOrder=0\nCategoryIndex=0\nDescription=The command executable to run\nRequired=false\nDisableIfBlank=true\n\n[RenderNameSeparator]\nType=string\nLabel=RenderNameSeparator\nCategory=Config\nCategoryOrder=0\nCategoryIndex=1\nDescription=The separator to use for naming\nRequired=false\nDisableIfBlank=true\nDefault=.\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/CelAction/CelAction.py",
    "content": "from System.Text.RegularExpressions import *\n\nfrom Deadline.Plugins import *\nfrom Deadline.Scripting import *\n\nimport _winreg\n\n######################################################################\n# This is the function that Deadline calls to get an instance of the\n# main DeadlinePlugin class.\n######################################################################\n\n\ndef GetDeadlinePlugin():\n    return CelActionPlugin()\n\n\ndef CleanupDeadlinePlugin(deadlinePlugin):\n    deadlinePlugin.Cleanup()\n\n######################################################################\n# This is the main DeadlinePlugin class for the CelAction plugin.\n######################################################################\n\n\nclass CelActionPlugin(DeadlinePlugin):\n\n    def __init__(self):\n        self.InitializeProcessCallback += self.InitializeProcess\n        self.RenderExecutableCallback += self.RenderExecutable\n        self.RenderArgumentCallback += self.RenderArgument\n        self.StartupDirectoryCallback += self.StartupDirectory\n\n    def Cleanup(self):\n        for stdoutHandler in self.StdoutHandlers:\n            del stdoutHandler.HandleCallback\n\n        del self.InitializeProcessCallback\n        del self.RenderExecutableCallback\n        del self.RenderArgumentCallback\n        del self.StartupDirectoryCallback\n\n    def GetCelActionRegistryKey(self):\n        # Modify registry for frame separation\n        path = r'Software\\CelAction\\CelAction2D\\User Settings'\n        _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, path)\n        regKey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, path, 0,\n                                 _winreg.KEY_ALL_ACCESS)\n        return regKey\n\n    def GetSeparatorValue(self, regKey):\n        useSeparator, _ = _winreg.QueryValueEx(\n            regKey, 'RenderNameUseSeparator')\n        separator, _ = _winreg.QueryValueEx(regKey, 'RenderNameSeparator')\n\n        return useSeparator, separator\n\n    def SetSeparatorValue(self, regKey, useSeparator, separator):\n        _winreg.SetValueEx(regKey, 'RenderNameUseSeparator',\n                           0, _winreg.REG_DWORD, useSeparator)\n        _winreg.SetValueEx(regKey, 'RenderNameSeparator',\n                           0, _winreg.REG_SZ, separator)\n\n    def InitializeProcess(self):\n        # Set the plugin specific settings.\n        self.SingleFramesOnly = False\n\n        # Set the process specific settings.\n        self.StdoutHandling = True\n        self.PopupHandling = True\n\n        # Ignore 'celaction' Pop-up dialog\n        self.AddPopupIgnorer(\".*Rendering.*\")\n        self.AddPopupIgnorer(\".*AutoRender.*\")\n\n        # Ignore 'celaction' Pop-up dialog\n        self.AddPopupIgnorer(\".*Wait.*\")\n\n        # Ignore 'celaction' Pop-up dialog\n        self.AddPopupIgnorer(\".*Timeline Scrub.*\")\n\n        celActionRegKey = self.GetCelActionRegistryKey()\n\n        self.SetSeparatorValue(celActionRegKey, 1, self.GetConfigEntryWithDefault(\n            \"RenderNameSeparator\", \".\").strip())\n\n    def RenderExecutable(self):\n        return RepositoryUtils.CheckPathMapping(self.GetConfigEntry(\"Executable\").strip())\n\n    def RenderArgument(self):\n        arguments = RepositoryUtils.CheckPathMapping(\n            self.GetPluginInfoEntry(\"Arguments\").strip())\n        arguments = arguments.replace(\n            \"<STARTFRAME>\", str(self.GetStartFrame()))\n        arguments = arguments.replace(\"<ENDFRAME>\", str(self.GetEndFrame()))\n        arguments = self.ReplacePaddedFrame(\n            arguments, \"<STARTFRAME%([0-9]+)>\", self.GetStartFrame())\n        arguments = self.ReplacePaddedFrame(\n            arguments, \"<ENDFRAME%([0-9]+)>\", self.GetEndFrame())\n        arguments = arguments.replace(\"<QUOTE>\", \"\\\"\")\n        return arguments\n\n    def StartupDirectory(self):\n        return self.GetPluginInfoEntryWithDefault(\"StartupDirectory\", \"\").strip()\n\n    def ReplacePaddedFrame(self, arguments, pattern, frame):\n        frameRegex = Regex(pattern)\n        while True:\n            frameMatch = frameRegex.Match(arguments)\n            if frameMatch.Success:\n                paddingSize = int(frameMatch.Groups[1].Value)\n                if paddingSize > 0:\n                    padding = StringUtils.ToZeroPaddedString(\n                        frame, paddingSize, False)\n                else:\n                    padding = str(frame)\n                arguments = arguments.replace(\n                    frameMatch.Groups[0].Value, padding)\n            else:\n                break\n\n        return arguments\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py",
    "content": "# /usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport os\nimport tempfile\nfrom datetime import datetime\nimport subprocess\nimport json\nimport platform\nimport uuid\nimport re\nfrom Deadline.Scripting import (\n    RepositoryUtils,\n    FileUtils,\n    DirectoryUtils,\n    ProcessUtils,\n)\n\nVERSION_REGEX = re.compile(\n    r\"(?P<major>0|[1-9]\\d*)\"\n    r\"\\.(?P<minor>0|[1-9]\\d*)\"\n    r\"\\.(?P<patch>0|[1-9]\\d*)\"\n    r\"(?:-(?P<prerelease>[a-zA-Z\\d\\-.]*))?\"\n    r\"(?:\\+(?P<buildmetadata>[a-zA-Z\\d\\-.]*))?\"\n)\n\n\nclass OpenPypeVersion:\n    \"\"\"Fake semver version class for OpenPype version purposes.\n\n    The version\n    \"\"\"\n    def __init__(self, major, minor, patch, prerelease, origin=None):\n        self.major = major\n        self.minor = minor\n        self.patch = patch\n        self.prerelease = prerelease\n\n        is_valid = True\n        if major is None or minor is None or patch is None:\n            is_valid = False\n        self.is_valid = is_valid\n\n        if origin is None:\n            base = \"{}.{}.{}\".format(str(major), str(minor), str(patch))\n            if not prerelease:\n                origin = base\n            else:\n                origin = \"{}-{}\".format(base, str(prerelease))\n\n        self.origin = origin\n\n    @classmethod\n    def from_string(cls, version):\n        \"\"\"Create an object of version from string.\n\n        Args:\n            version (str): Version as a string.\n\n        Returns:\n            Union[OpenPypeVersion, None]: Version object if input is nonempty\n                string otherwise None.\n        \"\"\"\n\n        if not version:\n            return None\n        valid_parts = VERSION_REGEX.findall(version)\n        if len(valid_parts) != 1:\n            # Return invalid version with filled 'origin' attribute\n            return cls(None, None, None, None, origin=str(version))\n\n        # Unpack found version\n        major, minor, patch, pre, post = valid_parts[0]\n        prerelease = pre\n        # Post release is not important anymore and should be considered as\n        #   part of prerelease\n        # - comparison is implemented to find suitable build and builds should\n        #       never contain prerelease part so \"not proper\" parsing is\n        #       acceptable for this use case.\n        if post:\n            prerelease = \"{}+{}\".format(pre, post)\n\n        return cls(\n            int(major), int(minor), int(patch), prerelease, origin=version\n        )\n\n    def has_compatible_release(self, other):\n        \"\"\"Version has compatible release as other version.\n\n        Both major and minor versions must be exactly the same. In that case\n        a build can be considered as release compatible with any version.\n\n        Args:\n            other (OpenPypeVersion): Other version.\n\n        Returns:\n            bool: Version is release compatible with other version.\n        \"\"\"\n\n        if self.is_valid and other.is_valid:\n            return self.major == other.major and self.minor == other.minor\n        return False\n\n    def __bool__(self):\n        return self.is_valid\n\n    def __repr__(self):\n        return \"<{} {}>\".format(self.__class__.__name__, self.origin)\n\n    def __eq__(self, other):\n        if not isinstance(other, self.__class__):\n            return self.origin == other\n        return self.origin == other.origin\n\n    def __lt__(self, other):\n        if not isinstance(other, self.__class__):\n            return None\n\n        if not self.is_valid:\n            return True\n\n        if not other.is_valid:\n            return False\n\n        if self.origin == other.origin:\n            return None\n\n        same_major = self.major == other.major\n        if not same_major:\n            return self.major < other.major\n\n        same_minor = self.minor == other.minor\n        if not same_minor:\n            return self.minor < other.minor\n\n        same_patch = self.patch == other.patch\n        if not same_patch:\n            return self.patch < other.patch\n\n        if not self.prerelease:\n            return False\n\n        if not other.prerelease:\n            return True\n\n        pres = [self.prerelease, other.prerelease]\n        pres.sort()\n        return pres[0] == self.prerelease\n\n\ndef get_openpype_version_from_path(path, build=True):\n    \"\"\"Get OpenPype version from provided path.\n         path (str): Path to scan.\n         build (bool, optional): Get only builds, not sources\n\n    Returns:\n        Union[OpenPypeVersion, None]: version of OpenPype if found.\n    \"\"\"\n\n    # fix path for application bundle on macos\n    if platform.system().lower() == \"darwin\":\n        path = os.path.join(path, \"MacOS\")\n\n    version_file = os.path.join(path, \"openpype\", \"version.py\")\n    if not os.path.isfile(version_file):\n        return None\n\n    # skip if the version is not build\n    exe = os.path.join(path, \"openpype_console.exe\")\n    if platform.system().lower() in [\"linux\", \"darwin\"]:\n        exe = os.path.join(path, \"openpype_console\")\n\n    # if only builds are requested\n    if build and not os.path.isfile(exe):  # noqa: E501\n        print(\"   ! path is not a build: {}\".format(path))\n        return None\n\n    version = {}\n    with open(version_file, \"r\") as vf:\n        exec(vf.read(), version)\n\n    version_str = version.get(\"__version__\")\n    if version_str:\n        return OpenPypeVersion.from_string(version_str)\n    return None\n\n\ndef get_openpype_executable():\n    \"\"\"Return OpenPype Executable from Event Plug-in Settings\"\"\"\n    config = RepositoryUtils.GetPluginConfig(\"OpenPype\")\n    exe_list = config.GetConfigEntryWithDefault(\"OpenPypeExecutable\", \"\")\n    dir_list = config.GetConfigEntryWithDefault(\n        \"OpenPypeInstallationDirs\", \"\")\n\n    # clean '\\ ' for MacOS pasting\n    if platform.system().lower() == \"darwin\":\n        exe_list = exe_list.replace(\"\\\\ \", \" \")\n        dir_list = dir_list.replace(\"\\\\ \", \" \")\n    return exe_list, dir_list\n\n\ndef get_openpype_versions(dir_list):\n    print(\">>> Getting OpenPype executable ...\")\n    openpype_versions = []\n\n    # special case of multiple install dirs\n    for dir_list in dir_list.split(\",\"):\n        install_dir = DirectoryUtils.SearchDirectoryList(dir_list)\n        if install_dir:\n            print(\"--- Looking for OpenPype at: {}\".format(install_dir))\n            sub_dirs = [\n                f.path for f in os.scandir(install_dir)\n                if f.is_dir()\n            ]\n            for subdir in sub_dirs:\n                version = get_openpype_version_from_path(subdir)\n                if not version:\n                    continue\n                print(\"  - found: {} - {}\".format(version, subdir))\n                openpype_versions.append((version, subdir))\n    return openpype_versions\n\n\ndef get_requested_openpype_executable(\n    exe, dir_list, requested_version\n):\n    requested_version_obj = OpenPypeVersion.from_string(requested_version)\n    if not requested_version_obj:\n        print((\n            \">>> Requested version '{}' does not match version regex '{}'\"\n        ).format(requested_version, VERSION_REGEX))\n        return None\n\n    print((\n        \">>> Scanning for compatible requested version {}\"\n    ).format(requested_version))\n    openpype_versions = get_openpype_versions(dir_list)\n    if not openpype_versions:\n        return None\n\n    # if looking for requested compatible version,\n    # add the implicitly specified to the list too.\n    if exe:\n        exe_dir = os.path.dirname(exe)\n        print(\"Looking for OpenPype at: {}\".format(exe_dir))\n        version = get_openpype_version_from_path(exe_dir)\n        if version:\n            print(\"  - found: {} - {}\".format(version, exe_dir))\n            openpype_versions.append((version, exe_dir))\n\n    matching_item = None\n    compatible_versions = []\n    for version_item in openpype_versions:\n        version, version_dir = version_item\n        if requested_version_obj.has_compatible_release(version):\n            compatible_versions.append(version_item)\n            if version == requested_version_obj:\n                # Store version item if version match exactly\n                # - break if is found matching version\n                matching_item = version_item\n                break\n\n    if not compatible_versions:\n        return None\n\n    compatible_versions.sort(key=lambda item: item[0])\n    if matching_item:\n        version, version_dir = matching_item\n        print((\n            \"*** Found exact match build version {} in {}\"\n        ).format(version_dir, version))\n\n    else:\n        version, version_dir = compatible_versions[-1]\n\n        print((\n            \"*** Latest compatible version found is {} in {}\"\n        ).format(version_dir, version))\n\n    # create list of executables for different platform and let\n    # Deadline decide.\n    exe_list = [\n        os.path.join(version_dir, \"openpype_console.exe\"),\n        os.path.join(version_dir, \"openpype_console\"),\n        os.path.join(version_dir, \"MacOS\", \"openpype_console\")\n    ]\n    return FileUtils.SearchFileList(\";\".join(exe_list))\n\n\ndef inject_openpype_environment(deadlinePlugin):\n    \"\"\" Pull env vars from OpenPype and push them to rendering process.\n\n        Used for correct paths, configuration from OpenPype etc.\n    \"\"\"\n    job = deadlinePlugin.GetJob()\n\n    print(\">>> Injecting OpenPype environments ...\")\n    try:\n        exe_list, dir_list = get_openpype_executable()\n        exe = FileUtils.SearchFileList(exe_list)\n\n        requested_version = job.GetJobEnvironmentKeyValue(\"OPENPYPE_VERSION\")\n        if requested_version:\n            exe = get_requested_openpype_executable(\n                exe, dir_list, requested_version\n            )\n            if exe is None:\n                raise RuntimeError((\n                    \"Cannot find compatible version available for version {}\"\n                    \" requested by the job. Please add it through plugin\"\n                    \" configuration in Deadline or install it to configured\"\n                    \" directory.\"\n                ).format(requested_version))\n\n        if not exe:\n            raise RuntimeError((\n                \"OpenPype executable was not found in the semicolon \"\n                \"separated list \\\"{}\\\".\"\n                \"The path to the render executable can be configured\"\n                \" from the Plugin Configuration in the Deadline Monitor.\"\n            ).format(\";\".join(exe_list)))\n\n        print(\"--- OpenPype executable: {}\".format(exe))\n\n        # tempfile.TemporaryFile cannot be used because of locking\n        temp_file_name = \"{}_{}.json\".format(\n            datetime.utcnow().strftime('%Y%m%d%H%M%S%f'),\n            str(uuid.uuid1())\n        )\n        export_url = os.path.join(tempfile.gettempdir(), temp_file_name)\n        print(\">>> Temporary path: {}\".format(export_url))\n\n        args = [\n            \"--headless\",\n            \"extractenvironments\",\n            export_url\n        ]\n\n        add_kwargs = {\n            \"project\": job.GetJobEnvironmentKeyValue(\"AVALON_PROJECT\"),\n            \"asset\": job.GetJobEnvironmentKeyValue(\"AVALON_ASSET\"),\n            \"task\": job.GetJobEnvironmentKeyValue(\"AVALON_TASK\"),\n            \"app\": job.GetJobEnvironmentKeyValue(\"AVALON_APP_NAME\"),\n            \"envgroup\": \"farm\"\n        }\n\n        if job.GetJobEnvironmentKeyValue('IS_TEST'):\n            args.append(\"--automatic-tests\")\n\n        if all(add_kwargs.values()):\n            for key, value in add_kwargs.items():\n                args.extend([\"--{}\".format(key), value])\n        else:\n            raise RuntimeError((\n                \"Missing required env vars: AVALON_PROJECT, AVALON_ASSET,\"\n                \" AVALON_TASK, AVALON_APP_NAME\"\n            ))\n\n        openpype_mongo = job.GetJobEnvironmentKeyValue(\"OPENPYPE_MONGO\")\n        if openpype_mongo:\n            # inject env var for OP extractenvironments\n            # SetEnvironmentVariable is important, not SetProcessEnv...\n            deadlinePlugin.SetEnvironmentVariable(\"OPENPYPE_MONGO\",\n                                                  openpype_mongo)\n\n        if not os.environ.get(\"OPENPYPE_MONGO\"):\n            print(\">>> Missing OPENPYPE_MONGO env var, process won't work\")\n\n        os.environ[\"AVALON_TIMEOUT\"] = \"5000\"\n\n        args_str = subprocess.list2cmdline(args)\n        print(\">>> Executing: {} {}\".format(exe, args_str))\n        process_exitcode = deadlinePlugin.RunProcess(\n            exe, args_str, os.path.dirname(exe), -1\n        )\n\n        if process_exitcode != 0:\n            raise RuntimeError(\n                \"Failed to run OpenPype process to extract environments.\"\n            )\n\n        print(\">>> Loading file ...\")\n        with open(export_url) as fp:\n            contents = json.load(fp)\n\n        for key, value in contents.items():\n            deadlinePlugin.SetProcessEnvironmentVariable(key, value)\n\n        if \"PATH\" in contents:\n            # Set os.environ[PATH] so studio settings' path entries\n            # can be used to define search path for executables.\n            print(f\">>> Setting 'PATH' Environment to: {contents['PATH']}\")\n            os.environ[\"PATH\"] = contents[\"PATH\"]\n\n        script_url = job.GetJobPluginInfoKeyValue(\"ScriptFilename\")\n        if script_url:\n            script_url = script_url.format(**contents).replace(\"\\\\\", \"/\")\n            print(\">>> Setting script path {}\".format(script_url))\n            job.SetJobPluginInfoKeyValue(\"ScriptFilename\", script_url)\n\n        print(\">>> Removing temporary file\")\n        os.remove(export_url)\n\n        print(\">> Injection end.\")\n    except Exception as e:\n        if hasattr(e, \"output\"):\n            print(\">>> Exception {}\".format(e.output))\n        import traceback\n        print(traceback.format_exc())\n        print(\"!!! Injection failed.\")\n        RepositoryUtils.FailJob(job)\n        raise\n\n\ndef inject_ayon_environment(deadlinePlugin):\n    \"\"\" Pull env vars from Ayon and push them to rendering process.\n\n        Used for correct paths, configuration from OpenPype etc.\n    \"\"\"\n    job = deadlinePlugin.GetJob()\n\n    print(\">>> Injecting Ayon environments ...\")\n    try:\n        exe_list = get_ayon_executable()\n        exe = FileUtils.SearchFileList(exe_list)\n\n        if not exe:\n            raise RuntimeError((\n               \"Ayon executable was not found in the semicolon \"\n               \"separated list \\\"{}\\\".\"\n               \"The path to the render executable can be configured\"\n               \" from the Plugin Configuration in the Deadline Monitor.\"\n            ).format(exe_list))\n\n        print(\"--- Ayon executable: {}\".format(exe))\n\n        ayon_bundle_name = job.GetJobEnvironmentKeyValue(\"AYON_BUNDLE_NAME\")\n        if not ayon_bundle_name:\n            raise RuntimeError(\"Missing env var in job properties \"\n                               \"AYON_BUNDLE_NAME\")\n\n        config = RepositoryUtils.GetPluginConfig(\"Ayon\")\n        ayon_server_url = (\n                job.GetJobEnvironmentKeyValue(\"AYON_SERVER_URL\") or\n                config.GetConfigEntryWithDefault(\"AyonServerUrl\", \"\")\n        )\n        ayon_api_key = (\n                job.GetJobEnvironmentKeyValue(\"AYON_API_KEY\") or\n                config.GetConfigEntryWithDefault(\"AyonApiKey\", \"\")\n        )\n\n        if not all([ayon_server_url, ayon_api_key]):\n            raise RuntimeError((\n                \"Missing required values for server url and api key. \"\n                \"Please fill in Ayon Deadline plugin or provide by \"\n                \"AYON_SERVER_URL and AYON_API_KEY\"\n            ))\n\n        # tempfile.TemporaryFile cannot be used because of locking\n        temp_file_name = \"{}_{}.json\".format(\n            datetime.utcnow().strftime('%Y%m%d%H%M%S%f'),\n            str(uuid.uuid1())\n        )\n        export_url = os.path.join(tempfile.gettempdir(), temp_file_name)\n        print(\">>> Temporary path: {}\".format(export_url))\n\n        args = [\n            \"--headless\",\n            \"extractenvironments\",\n            export_url\n        ]\n\n        add_kwargs = {\n            \"project\": job.GetJobEnvironmentKeyValue(\"AVALON_PROJECT\"),\n            \"asset\": job.GetJobEnvironmentKeyValue(\"AVALON_ASSET\"),\n            \"task\": job.GetJobEnvironmentKeyValue(\"AVALON_TASK\"),\n            \"app\": job.GetJobEnvironmentKeyValue(\"AVALON_APP_NAME\"),\n            \"envgroup\": \"farm\",\n        }\n\n        if job.GetJobEnvironmentKeyValue('IS_TEST'):\n            args.append(\"--automatic-tests\")\n\n        if all(add_kwargs.values()):\n            for key, value in add_kwargs.items():\n                args.extend([\"--{}\".format(key), value])\n        else:\n            raise RuntimeError((\n                \"Missing required env vars: AVALON_PROJECT, AVALON_ASSET,\"\n                \" AVALON_TASK, AVALON_APP_NAME\"\n            ))\n\n        environment = {\n            \"AYON_SERVER_URL\": ayon_server_url,\n            \"AYON_API_KEY\": ayon_api_key,\n            \"AYON_BUNDLE_NAME\": ayon_bundle_name,\n        }\n        for env, val in environment.items():\n            # Add the env var for the Render Plugin that is about to render\n            deadlinePlugin.SetEnvironmentVariable(env, val)\n            # Add the env var for current calls to `DeadlinePlugin.RunProcess`\n            deadlinePlugin.SetProcessEnvironmentVariable(env, val)\n\n        args_str = subprocess.list2cmdline(args)\n        print(\">>> Executing: {} {}\".format(exe, args_str))\n        process_exitcode = deadlinePlugin.RunProcess(\n            exe, args_str, os.path.dirname(exe), -1\n        )\n\n        if process_exitcode != 0:\n            raise RuntimeError(\n                \"Failed to run Ayon process to extract environments.\"\n            )\n\n        print(\">>> Loading file ...\")\n        with open(export_url) as fp:\n            contents = json.load(fp)\n\n        for key, value in contents.items():\n            deadlinePlugin.SetProcessEnvironmentVariable(key, value)\n\n        if \"PATH\" in contents:\n            # Set os.environ[PATH] so studio settings' path entries\n            # can be used to define search path for executables.\n            print(f\">>> Setting 'PATH' Environment to: {contents['PATH']}\")\n            os.environ[\"PATH\"] = contents[\"PATH\"]\n\n        script_url = job.GetJobPluginInfoKeyValue(\"ScriptFilename\")\n        if script_url:\n            script_url = script_url.format(**contents).replace(\"\\\\\", \"/\")\n            print(\">>> Setting script path {}\".format(script_url))\n            job.SetJobPluginInfoKeyValue(\"ScriptFilename\", script_url)\n\n        print(\">>> Removing temporary file\")\n        os.remove(export_url)\n\n        print(\">> Injection end.\")\n    except Exception as e:\n        if hasattr(e, \"output\"):\n            print(\">>> Exception {}\".format(e.output))\n        import traceback\n        print(traceback.format_exc())\n        print(\"!!! Injection failed.\")\n        RepositoryUtils.FailJob(job)\n        raise\n\n\ndef get_ayon_executable():\n    \"\"\"Return OpenPype Executable from Event Plug-in Settings\n\n    Returns:\n            (list) of paths\n    Raises:\n        (RuntimeError) if no path configured at all\n    \"\"\"\n    config = RepositoryUtils.GetPluginConfig(\"Ayon\")\n    exe_list = config.GetConfigEntryWithDefault(\"AyonExecutable\", \"\")\n\n    if not exe_list:\n        raise RuntimeError(\"Path to Ayon executable not configured.\"\n                           \"Please set it in Ayon Deadline Plugin.\")\n\n    # clean '\\ ' for MacOS pasting\n    if platform.system().lower() == \"darwin\":\n        exe_list = exe_list.replace(\"\\\\ \", \" \")\n\n    # Expand user paths\n    expanded_paths = []\n    for path in exe_list.split(\";\"):\n        if path.startswith(\"~\"):\n            path = os.path.expanduser(path)\n        expanded_paths.append(path)\n    return \";\".join(expanded_paths)\n\n\ndef inject_render_job_id(deadlinePlugin):\n    \"\"\"Inject dependency ids to publish process as env var for validation.\"\"\"\n    print(\">>> Injecting render job id ...\")\n    job = deadlinePlugin.GetJob()\n\n    dependency_ids = job.JobDependencyIDs\n    print(\">>> Dependency IDs: {}\".format(dependency_ids))\n    render_job_ids = \",\".join(dependency_ids)\n\n    deadlinePlugin.SetProcessEnvironmentVariable(\"RENDER_JOB_IDS\",\n                                                 render_job_ids)\n    print(\">>> Injection end.\")\n\n\ndef __main__(deadlinePlugin):\n    print(\"*** GlobalJobPreload start ...\")\n    print(\">>> Getting job ...\")\n    job = deadlinePlugin.GetJob()\n\n    openpype_render_job = \\\n        job.GetJobEnvironmentKeyValue('OPENPYPE_RENDER_JOB') or '0'\n    openpype_publish_job = \\\n        job.GetJobEnvironmentKeyValue('OPENPYPE_PUBLISH_JOB') or '0'\n    openpype_remote_job = \\\n        job.GetJobEnvironmentKeyValue('OPENPYPE_REMOTE_PUBLISH') or '0'\n\n    if openpype_publish_job == '1' and openpype_render_job == '1':\n        raise RuntimeError(\"Misconfiguration. Job couldn't be both \" +\n                           \"render and publish.\")\n\n    if openpype_publish_job == '1':\n        inject_render_job_id(deadlinePlugin)\n    if openpype_render_job == '1' or openpype_remote_job == '1':\n        inject_openpype_environment(deadlinePlugin)\n\n    ayon_render_job = \\\n        job.GetJobEnvironmentKeyValue('AYON_RENDER_JOB') or '0'\n    ayon_publish_job = \\\n        job.GetJobEnvironmentKeyValue('AYON_PUBLISH_JOB') or '0'\n    ayon_remote_job = \\\n        job.GetJobEnvironmentKeyValue('AYON_REMOTE_PUBLISH') or '0'\n\n    if ayon_publish_job == '1' and ayon_render_job == '1':\n        raise RuntimeError(\"Misconfiguration. Job couldn't be both \" +\n                           \"render and publish.\")\n\n    if ayon_publish_job == '1':\n        inject_render_job_id(deadlinePlugin)\n    if ayon_render_job == '1' or ayon_remote_job == '1':\n        inject_ayon_environment(deadlinePlugin)\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.options",
    "content": "[SceneFile]\nType=filename\nLabel=Scene Filename\nCategory=Global Settings\nCategoryOrder=0\nIndex=0\nDescription=The scene filename as it exists on the network.\nRequired=false\nDisableIfBlank=true\n\n[Environment]\nType=filename\nLabel=Scene Environment\nCategory=Global Settings\nCategoryOrder=0\nIndex=1\nDescription=The Environment for the scene.\nRequired=false\nDisableIfBlank=true\n\n[Job]\nType=filename\nLabel=Scene Job\nCategory=Global Settings\nCategoryOrder=0\nIndex=2\nDescription=The Job that the scene belongs to.\nRequired=false\nDisableIfBlank=true\n\n[SceneName]\nType=filename\nLabel=Scene Name\nCategory=Global Settings\nCategoryOrder=0\nIndex=3\nDescription=The name of the scene to render\nRequired=false\nDisableIfBlank=true\n\n[SceneVersion]\nType=filename\nLabel=Scene Version\nCategory=Global Settings\nCategoryOrder=0\nIndex=4\nDescription=The version of the scene to render.\nRequired=false\nDisableIfBlank=true\n\n[Version]\nType=enum\nValues=10;11;12\nLabel=Harmony Version\nCategory=Global Settings\nCategoryOrder=0\nIndex=5\nDescription=The version of Harmony to use.\nRequired=false\nDisableIfBlank=true\n\n[IsDatabase]\nType=Boolean\nLabel=Is Database Scene\nCategory=Global Settings\nCategoryOrder=0\nIndex=6\nDescription=Whether or not the scene is in the database or not\nRequired=false\nDisableIfBlank=true\n\n[Camera]\nType=string\nLabel=Camera\nCategory=Render Settings\nCategoryOrder=1\nIndex=0\nDescription=Specifies the camera to use for rendering images. If Blank, the scene will be rendered with the current Camera.\nRequired=false\nDisableIfBlank=true\n\n[UsingResPreset]\nType=Boolean\nLabel=Use Resolution Preset\nCategory=Render Settings\nCategoryOrder=1\nIndex=1\nDescription=Whether or not you are using a resolution preset.\nRequired=false\nDisableIfBlank=true\n\n[ResolutionName]\nType=enum\nValues=HDTV_1080p24;HDTV_1080p25;HDTV_720p24;4K_UHD;8K_UHD;DCI_2K;DCI_4K;film-2K;film-4K;film-1.33_H;film-1.66_H;film-1.66_V;Cineon;NTSC;PAL;2160p;1440p;1080p;720p;480p;360p;240p;low;Web_Video;Game_512;Game_512_Ortho;WebCC_Preview;Custom\nLabel=Resolution Preset\nCategory=Render Settings\nCategoryOrder=1\nIndex=2\nDescription=The resolution preset to use.\nRequired=true\nDefault=HDTV_1080p24\n\n[PresetName]\nType=string\nLabel=Preset Name\nCategory=Render Settings\nCategoryOrder=1\nIndex=3\nDescription=Specify the custom resolution name.\nRequired=true\nDefault=\n\n[ResolutionX]\nType=integer\nLabel=Resolution X\nMinimum=0\nMaximum=1000000\nCategory=Render Settings\nCategoryOrder=1\nIndex=4\nDescription=Specifies the width of the rendered images. If 0, then the current resolution and Field of view will be used.\nRequired=true\nDefault=1920\n\n[ResolutionY]\nType=integer\nLabel=Resolution Y\nMinimum=0\nMaximum=1000000\nCategory=Render Settings\nCategoryOrder=1\nIndex=5\nDescription=Specifies the height of the rendered images. If 0, then the current resolution and Field of view will be used.\nRequired=true\nDefault=1080\n\n[FieldOfView]\nType=float\nLabel=Field Of View\nMinimum=0\nMaximum=89\nDecimalPlaces=2\nCategory=Render Settings\nCategoryOrder=1\nIndex=6\nDescription=Specifies the field of view of the rendered images. If 0, then the current resolution and Field of view will be used.\nRequired=true\nDefault=41.11\n\n[Output0Node]\nType=string\nLabel=Render Node 0 Name\nCategory=Output Settings\nCategoryOrder=2\nIndex=0\nDescription=The name of the render node.\nRequired=false\nDisableIfBlank=true\n\n[Output0Type]\nType=enum\nValues=Image;Movie\nLabel=Render Node 0 Type\nCategory=Output Settings\nCategoryOrder=2\nIndex=1\nDescription=The type of output that the render node is producing.\nRequired=false\nDisableIfBlank=true\n\n[Output0Path]\nType=string\nLabel=Render Node 0 Path\nCategory=Output Settings\nCategoryOrder=2\nIndex=2\nDescription=The output path and file name of the output files.\nRequired=false\nDisableIfBlank=true\n\n[Output0LeadingZero]\nType=integer\nLabel=Render Node 0 Leading Zeroes\nCategory=Output Settings\nCategoryOrder=2\nMinimum=0\nMaximum=5\nIndex=3\nDescription=The number of leading zeroes for a 1 digit frame number.  (1 less then the full padded length)\nRequired=false\nDisableIfBlank=true\n\n[Output0Format]\nType=string\nLabel=Render Node 0 Format\nCategory=Output Settings\nCategoryOrder=2\nIndex=4\nDescription=The format for the rendered output images.\nRequired=false\nDisableIfBlank=true\n\n[Output0StartFrame]\nType=integer\nLabel=Render Node 0 Start Frame\nCategory=Output Settings\nCategoryOrder=2\nMinimum=1\nIndex=5\nDescription=The frame that will correspond to frame one when numbering.  If this value is not 1 then the monitor's job output features will not work properly.\nRequired=false\nDisableIfBlank=true\n\n[Output1Node]\nType=string\nLabel=Render Node 1 Name\nCategory=Output Settings\nCategoryOrder=2\nIndex=6\nDescription=The name of the render node.\nRequired=false\nDisableIfBlank=true\n\n[Output1Type]\nType=enum\nValues=Image;Movie\nLabel=Render Node 1 Type\nCategory=Output Settings\nCategoryOrder=2\nIndex=7\nDescription=The type of output that the render node is producing.\nRequired=false\nDisableIfBlank=true\n\n[Output1Path]\nType=string\nLabel=Render Node 1 Path\nCategory=Output Settings\nCategoryOrder=2\nIndex=8\nDescription=The output path and file name of the output files.\nRequired=false\nDisableIfBlank=true\n\n[Output1LeadingZero]\nType=integer\nLabel=Render Node 1 Leading Zeroes\nCategory=Output Settings\nCategoryOrder=2\nMinimum=0\nMaximum=5\nIndex=9\nDescription=The number of leading zeroes for a 1 digit frame number.  (1 less then the full padded length)\nRequired=false\nDisableIfBlank=true\n\n[Output1Format]\nType=string\nLabel=Render Node 1 Format\nCategory=Output Settings\nCategoryOrder=2\nIndex=10\nDescription=The format for the rendered output images.\nRequired=false\nDisableIfBlank=true\n\n[Output1StartFrame]\nType=integer\nLabel=Render Node 1 Start Frame\nCategory=Output Settings\nCategoryOrder=2\nMinimum=1\nIndex=11\nDescription=The frame that will correspond to frame one when numbering.  If this value is not 1 then the monitor's job output features will not work properly.\nRequired=false\nDisableIfBlank=true\n\n[Output2Node]\nType=string\nLabel=Render Node 2 Name\nCategory=Output Settings\nCategoryOrder=2\nIndex=12\nDescription=The name of the render node.\nRequired=false\nDisableIfBlank=true\n\n[Output2Type]\nType=enum\nValues=Image;Movie\nLabel=Render Node 2 Type\nCategory=Output Settings\nCategoryOrder=2\nIndex=13\nDescription=The type of output that the render node is producing.\nRequired=false\nDisableIfBlank=true\n\n[Output2Path]\nType=string\nLabel=Render Node 2 Path\nCategory=Output Settings\nCategoryOrder=2\nIndex=14\nDescription=The output path and file name of the output files.\nRequired=false\nDisableIfBlank=true\n\n[Output2LeadingZero]\nType=integer\nLabel=Render Node 2 Leading Zeroes\nCategory=Output Settings\nCategoryOrder=2\nMinimum=0\nMaximum=5\nIndex=15\nDescription=The number of leading zeroes for a 1 digit frame number.  (1 less then the full padded length)\nRequired=false\nDisableIfBlank=true\n\n[Output2Format]\nType=string\nLabel=Render Node 2 Format\nCategory=Output Settings\nCategoryOrder=2\nIndex=16\nDescription=The format for the rendered output images.\nRequired=false\nDisableIfBlank=true\n\n[Output2StartFrame]\nType=integer\nLabel=Render Node 2 Start Frame\nCategory=Output Settings\nCategoryOrder=2\nMinimum=1\nIndex=17\nDescription=The frame that will correspond to frame one when numbering.  If this value is not 1 then the monitor's job output features will not work properly.\nRequired=false\nDisableIfBlank=true\n\n[Output3Node]\nType=string\nLabel=Render Node 3 Name\nCategory=Output Settings\nCategoryOrder=2\nIndex=18\nDescription=The name of the render node.\nRequired=false\nDisableIfBlank=true\n\n[Output3Type]\nType=enum\nValues=Image;Movie\nLabel=Render Node 3 Type\nCategory=Output Settings\nCategoryOrder=2\nIndex=19\nDescription=The type of output that the render node is producing.\nRequired=false\nDisableIfBlank=true\n\n[Output3Path]\nType=string\nLabel=Render Node 3 Path\nCategory=Output Settings\nCategoryOrder=2\nIndex=20\nDescription=The output path and file name of the output files.\nRequired=false\nDisableIfBlank=true\n\n[Output3LeadingZero]\nType=integer\nLabel=Render Node 3 Leading Zeroes\nCategory=Output Settings\nCategoryOrder=2\nMinimum=0\nMaximum=5\nIndex=21\nDescription=The number of leading zeroes for a 1 digit frame number.  (1 less then the full padded length)\nRequired=false\nDisableIfBlank=true\n\n[Output3Format]\nType=string\nLabel=Render Node 3 Format\nCategory=Output Settings\nCategoryOrder=2\nIndex=22\nDescription=The format for the rendered output images.\nRequired=false\nDisableIfBlank=true\n\n[Output3StartFrame]\nType=integer\nLabel=Render Node 3 Start Frame\nCategory=Output Settings\nCategoryOrder=2\nMinimum=1\nIndex=23\nDescription=The frame that will correspond to frame one when numbering.  If this value is not 1 then the monitor's job output features will not work properly.\nRequired=false\nDisableIfBlank=true\n\n[Output4Node]\nType=string\nLabel=Render Node 4 Name\nCategory=Output Settings\nCategoryOrder=2\nIndex=24\nDescription=The name of the render node.\nRequired=false\nDisableIfBlank=true\n\n[Output4Type]\nType=enum\nValues=Image;Movie\nLabel=Render Node 4 Type\nCategory=Output Settings\nCategoryOrder=2\nIndex=25\nDescription=The type of output that the render node is producing.\nRequired=false\nDisableIfBlank=true\n\n[Output4Path]\nType=string\nLabel=Render Node 4 Path\nCategory=Output Settings\nCategoryOrder=2\nIndex=26\nDescription=The output path and file name of the output files.\nRequired=false\nDisableIfBlank=true\n\n[Output4LeadingZero]\nType=integer\nLabel=Render Node 4 Leading Zeroes\nCategory=Output Settings\nCategoryOrder=2\nMinimum=0\nMaximum=5\nIndex=27\nDescription=The number of leading zeroes for a 1 digit frame number.  (1 less then the full padded length)\nRequired=false\nDisableIfBlank=true\n\n[Output4Format]\nType=string\nLabel=Render Node 4 Format\nCategory=Output Settings\nCategoryOrder=2\nIndex=28\nDescription=The format for the rendered output images.\nRequired=false\nDisableIfBlank=true\n\n[Output4StartFrame]\nType=integer\nLabel=Render Node 4 Start Frame\nCategory=Output Settings\nCategoryOrder=2\nMinimum=1\nIndex=29\nDescription=The frame that will correspond to frame one when numbering.  If this value is not 1 then the monitor's job output features will not work properly.\nRequired=false\nDisableIfBlank=true\n\n[Output5Node]\nType=string\nLabel=Render Node 5 Name\nCategory=Output Settings\nCategoryOrder=2\nIndex=30\nDescription=The name of the render node.\nRequired=false\nDisableIfBlank=true\n\n[Output5Type]\nType=enum\nValues=Image;Movie\nLabel=Render Node 5 Type\nCategory=Output Settings\nCategoryOrder=2\nIndex=31\nDescription=The type of output that the render node is producing.\nRequired=false\nDisableIfBlank=true\n\n[Output5Path]\nType=string\nLabel=Render Node 5 Path\nCategory=Output Settings\nCategoryOrder=2\nIndex=32\nDescription=The output path and file name of the output files.\nRequired=false\nDisableIfBlank=true\n\n[Output5LeadingZero]\nType=integer\nLabel=Render Node 5 Leading Zeroes\nCategory=Output Settings\nCategoryOrder=2\nMinimum=0\nMaximum=5\nIndex=33\nDescription=The number of leading zeroes for a 1 digit frame number.  (1 less then the full padded length)\nRequired=false\nDisableIfBlank=true\n\n[Output5Format]\nType=string\nLabel=Render Node 5 Format\nCategory=Output Settings\nCategoryOrder=2\nIndex=34\nDescription=The format for the rendered output images.\nRequired=false\nDisableIfBlank=true\n\n[Output5StartFrame]\nType=integer\nLabel=Render Node 5 Start Frame\nCategory=Output Settings\nCategoryOrder=2\nMinimum=1\nIndex=35\nDescription=The frame that will correspond to frame one when numbering.  If this value is not 1 then the monitor's job output features will not work properly.\nRequired=false\nDisableIfBlank=true"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.param",
    "content": "[About]\nType=label\nLabel=About\nCategory=About Plugin\nCategoryOrder=-1\nIndex=0\nDefault=Harmony Render Plugin for Deadline\nDescription=Not configurable\n\n[ConcurrentTasks]\nType=label\nLabel=ConcurrentTasks\nCategory=About Plugin\nCategoryOrder=-1\nIndex=0\nDefault=True\nDescription=Not configurable\n\n[Harmony_RenderExecutable_10]\nType=multilinemultifilename\nCategory=Render Executables\nCategoryOrder=0\nIndex=0\nLabel=Harmony 10 Render Executable\nDescription=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines.\nDefault=C:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 10.0\\win64\\bin\\Stage.exe\n\n[Harmony_RenderExecutable_11]\nType=multilinemultifilename\nCategory=Render Executables\nCategoryOrder=0\nIndex=1\nLabel=Harmony 11 Render Executable\nDescription=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines.\nDefault=C:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 11.0\\win64\\bin\\Stage.exe\n\n[Harmony_RenderExecutable_12]\nType=multilinemultifilename\nCategory=Render Executables\nCategoryOrder=0\nIndex=2\nLabel=Harmony 12 Render Executable\nDescription=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines.\nDefault=C:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 12.0 Premium\\win64\\bin\\HarmonyPremium.exe;/Applications/Toon Boom Harmony 12.0 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_12/lnx86_64/bin/HarmonyPremium\n\n[Harmony_RenderExecutable_14]\nType=multilinemultifilename\nCategory=Render Executables\nCategoryOrder=0\nIndex=3\nLabel=Harmony 14 Render Executable\nDescription=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines.\nDefault=C:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 14.0 Premium\\win64\\bin\\HarmonyPremium.exe;/Applications/Toon Boom Harmony 14.0 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_14/lnx86_64/bin/HarmonyPremium\n\n[Harmony_RenderExecutable_15]\nType=multilinemultifilename\nCategory=Render Executables\nCategoryOrder=0\nIndex=4\nLabel=Harmony 15 Render Executable\nDescription=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines.\nDefault=C:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 15.0 Premium\\win64\\bin\\HarmonyPremium.exe;/Applications/Toon Boom Harmony 15.0 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_15.0/lnx86_64/bin/HarmonyPremium\n\n[Harmony_RenderExecutable_17]\nType=multilinemultifilename\nCategory=Render Executables\nCategoryOrder=0\nIndex=4\nLabel=Harmony 17 Render Executable\nDescription=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines.\nDefault=c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 17 Premium\\win64\\bin\\HarmonyPremium.exe;/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_17/lnx86_64/bin/HarmonyPremium\n\n[Harmony_RenderExecutable_20]\nType=multilinemultifilename\nCategory=Render Executables\nCategoryOrder=0\nIndex=4\nLabel=Harmony 20 Render Executable\nDescription=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines.\nDefault=c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 20 Premium\\win64\\bin\\HarmonyPremium.exe;/Applications/Toon Boom Harmony 20 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_20/lnx86_64/bin/HarmonyPremium\n\n[Harmony_RenderExecutable_21]\nType=multilinemultifilename\nCategory=Render Executables\nCategoryOrder=0\nIndex=4\nLabel=Harmony 21 Render Executable\nDescription=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines.\nDefault=c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 21 Premium\\win64\\bin\\HarmonyPremium.exe;/Applications/Toon Boom Harmony 21 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_21/lnx86_64/bin/HarmonyPremium\n\n[Harmony_RenderExecutable_22]\nType=multilinemultifilename\nCategory=Render Executables\nCategoryOrder=0\nIndex=4\nLabel=Harmony 22 Render Executable\nDescription=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines.\nDefault=c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 22 Premium\\win64\\bin\\HarmonyPremium.exe;/Applications/Toon Boom Harmony 22 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_22/lnx86_64/bin/HarmonyPremium\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py",
    "content": "#!/usr/bin/env python3\nfrom System import *\nfrom System.Diagnostics import *\nfrom System.IO import *\nfrom System.Text import *\n\nfrom Deadline.Plugins import *\nfrom Deadline.Scripting import *\n\ndef GetDeadlinePlugin():\n    return HarmonyOpenPypePlugin()\n\ndef CleanupDeadlinePlugin( deadlinePlugin ):\n    deadlinePlugin.Cleanup()\n\nclass HarmonyOpenPypePlugin( DeadlinePlugin ):\n\n    def __init__( self ):\n        super().__init__()\n        self.InitializeProcessCallback += self.InitializeProcess\n        self.RenderExecutableCallback += self.RenderExecutable\n        self.RenderArgumentCallback += self.RenderArgument\n        self.CheckExitCodeCallback += self.CheckExitCode\n\n    def Cleanup( self ):\n        print(\"Cleanup\")\n        for stdoutHandler in self.StdoutHandlers:\n            del stdoutHandler.HandleCallback\n\n        del self.InitializeProcessCallback\n        del self.RenderExecutableCallback\n        del self.RenderArgumentCallback\n\n    def CheckExitCode( self, exitCode ):\n        print(\"check code\")\n        if exitCode != 0:\n            if exitCode == 100:\n                self.LogInfo( \"Renderer reported an error with error code 100. This will be ignored, since the option to ignore it is specified in the Job Properties.\" )\n            else:\n                self.FailRender( \"Renderer returned non-zero error code %d. Check the renderer's output.\" % exitCode )\n\n    def InitializeProcess( self ):\n        self.PluginType = PluginType.Simple\n        self.StdoutHandling = True\n        self.PopupHandling = True\n\n        self.AddStdoutHandlerCallback( \"Rendered frame ([0-9]+)\" ).HandleCallback += self.HandleStdoutProgress\n\n    def HandleStdoutProgress( self ):\n        startFrame = self.GetStartFrame()\n        endFrame = self.GetEndFrame()\n        if( endFrame - startFrame + 1 != 0 ):\n            self.SetProgress( 100 * ( int(self.GetRegexMatch(1)) - startFrame + 1 ) / ( endFrame - startFrame + 1 ) )\n\n    def RenderExecutable( self ):\n        version = int( self.GetPluginInfoEntry( \"Version\" ) )\n        exe = \"\"\n        exeList = self.GetConfigEntry( \"Harmony_RenderExecutable_\" + str(version) )\n        exe = FileUtils.SearchFileList( exeList )\n        if( exe == \"\" ):\n            self.FailRender( \"Harmony render executable was not found in the configured separated list \\\"\" + exeList + \"\\\". The path to the render executable can be configured from the Plugin Configuration in the Deadline Monitor.\" )\n        return exe\n\n    def RenderArgument( self ):\n        renderArguments = \"-batch\"\n\n        if self.GetBooleanPluginInfoEntryWithDefault( \"UsingResPreset\", False ):\n            resName = self.GetPluginInfoEntryWithDefault( \"ResolutionName\", \"HDTV_1080p24\" )\n            if resName == \"Custom\":\n                renderArguments += \" -res \" + self.GetPluginInfoEntryWithDefault( \"PresetName\", \"HDTV_1080p24\" )\n            else:\n                renderArguments += \" -res \" + resName\n        else:\n            resolutionX = self.GetIntegerPluginInfoEntryWithDefault( \"ResolutionX\", -1 )\n            resolutionY = self.GetIntegerPluginInfoEntryWithDefault( \"ResolutionY\", -1 )\n            fov = self.GetFloatPluginInfoEntryWithDefault( \"FieldOfView\", -1 )\n\n            if resolutionX > 0 and resolutionY > 0 and fov > 0:\n                renderArguments += \" -res \" + str( resolutionX ) + \" \" + str( resolutionY ) + \" \" + str( fov )\n\n        camera = self.GetPluginInfoEntryWithDefault( \"Camera\", \"\" )\n\n        if not camera == \"\":\n            renderArguments += \" -camera \" + camera\n\n        startFrame = str( self.GetStartFrame() )\n        endFrame = str( self.GetEndFrame() )\n\n        renderArguments += \" -frames \" + startFrame + \" \" + endFrame\n\n        if not self.GetBooleanPluginInfoEntryWithDefault( \"IsDatabase\", False ):\n            sceneFilename = self.GetPluginInfoEntryWithDefault( \"SceneFile\", self.GetDataFilename() )\n            sceneFilename = RepositoryUtils.CheckPathMapping( sceneFilename )\n            renderArguments += \" \\\"\" + sceneFilename + \"\\\"\"\n        else:\n            environment = self.GetPluginInfoEntryWithDefault( \"Environment\", \"\" )\n            renderArguments += \" -env \" + environment\n            job = self.GetPluginInfoEntryWithDefault( \"Job\", \"\" )\n            renderArguments += \" -job \" + job\n            scene = self.GetPluginInfoEntryWithDefault( \"SceneName\", \"\" )\n            renderArguments += \" -scene \" + scene\n            version = self.GetPluginInfoEntryWithDefault( \"SceneVersion\", \"\" )\n            renderArguments += \" -version \" + version\n\n        #tempSceneDirectory = self.CreateTempDirectory( \"thread\" + str(self.GetThreadNumber()) )\n        #preRenderScript =\n        rendernodeNum = 0\n        scriptBuilder = StringBuilder()\n\n        while True:\n            nodeName = self.GetPluginInfoEntryWithDefault( \"Output\" + str( rendernodeNum ) + \"Node\", \"\" )\n            if nodeName == \"\":\n                break\n            nodeType = self.GetPluginInfoEntryWithDefault( \"Output\" + str( rendernodeNum ) + \"Type\", \"Image\" )\n            if nodeType == \"Image\":\n                nodePath = self.GetPluginInfoEntryWithDefault( \"Output\" + str( rendernodeNum ) + \"Path\", \"\" )\n                nodeLeadingZero = self.GetPluginInfoEntryWithDefault( \"Output\" + str( rendernodeNum ) + \"LeadingZero\", \"\" )\n                nodeFormat = self.GetPluginInfoEntryWithDefault( \"Output\" + str( rendernodeNum ) + \"Format\", \"\" )\n                nodeStartFrame = self.GetPluginInfoEntryWithDefault( \"Output\" + str( rendernodeNum ) + \"StartFrame\", \"\" )\n\n                if not nodePath == \"\":\n                    scriptBuilder.AppendLine(\"node.setTextAttr( \\\"\" + nodeName + \"\\\", \\\"drawingName\\\", 1, \\\"\" + nodePath + \"\\\" );\")\n\n                if not nodeLeadingZero == \"\":\n                    scriptBuilder.AppendLine(\"node.setTextAttr( \\\"\" + nodeName + \"\\\", \\\"leadingZeros\\\", 1, \\\"\" + nodeLeadingZero + \"\\\" );\")\n\n                if not nodeFormat == \"\":\n                    scriptBuilder.AppendLine(\"node.setTextAttr( \\\"\" + nodeName + \"\\\", \\\"drawingType\\\", 1, \\\"\" + nodeFormat + \"\\\" );\")\n\n                if not nodeStartFrame == \"\":\n                    scriptBuilder.AppendLine(\"node.setTextAttr( \\\"\" + nodeName + \"\\\", \\\"start\\\", 1, \\\"\" + nodeStartFrame + \"\\\" );\")\n\n            if nodeType == \"Movie\":\n                nodePath = self.GetPluginInfoEntryWithDefault( \"Output\" + str( rendernodeNum ) + \"Path\", \"\" )\n                if not nodePath == \"\":\n                    scriptBuilder.AppendLine(\"node.setTextAttr( \\\"\" + nodeName + \"\\\", \\\"moviePath\\\", 1, \\\"\" + nodePath + \"\\\" );\")\n\n            rendernodeNum += 1\n\n        tempDirectory = self.CreateTempDirectory( \"thread\" + str(self.GetThreadNumber()) )\n        preRenderScriptName = Path.Combine( tempDirectory, \"preRenderScript.txt\" )\n\n        File.WriteAllText( preRenderScriptName, scriptBuilder.ToString() )\n\n        preRenderInlineScript = self.GetPluginInfoEntryWithDefault( \"PreRenderInlineScript\", \"\" )\n        if preRenderInlineScript:\n            renderArguments += \" -preRenderInlineScript \\\"\" + preRenderInlineScript +\"\\\"\"\n\n        renderArguments += \" -preRenderScript \\\"\" + preRenderScriptName +\"\\\"\"\n\n        return renderArguments\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.options",
    "content": "[Arguments]\nType=string\nLabel=Arguments\nCategory=Python Options\nCategoryOrder=0\nIndex=1\nDescription=The arguments to pass to the script. If no arguments are required, leave this blank.\nRequired=false\nDisableIfBlank=true\n\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.param",
    "content": "[About]\nType=label\nLabel=About\nCategory=About Plugin\nCategoryOrder=-1\nIndex=0\nDefault=OpenPype Plugin for Deadline\nDescription=Not configurable\n\n[OpenPypeInstallationDirs]\nType=multilinemultifolder\nLabel=Directories where OpenPype versions are installed\nCategory=OpenPype Installation Directories\nCategoryOrder=0\nIndex=0\nDefault=C:\\Program Files (x86)\\OpenPype\nDescription=Path or paths to directories where multiple versions of OpenPype might be installed. Enter every such path on separate lines.\n\n[OpenPypeExecutable]\nType=multilinemultifilename\nLabel=OpenPype Executable\nCategory=OpenPype Executables\nCategoryOrder=1\nIndex=0\nDefault=\nDescription=The path to the OpenPype executable. Enter alternative paths on separate lines.\n\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py",
    "content": "#!/usr/bin/env python3\n\nfrom System.IO import Path\nfrom System.Text.RegularExpressions import Regex\n\nfrom Deadline.Plugins import PluginType, DeadlinePlugin\nfrom Deadline.Scripting import (\n    StringUtils,\n    FileUtils,\n    DirectoryUtils,\n    RepositoryUtils\n)\n\nimport re\nimport os\nimport platform\n\n\n######################################################################\n# This is the function that Deadline calls to get an instance of the\n# main DeadlinePlugin class.\n######################################################################\ndef GetDeadlinePlugin():\n    return OpenPypeDeadlinePlugin()\n\n\ndef CleanupDeadlinePlugin(deadlinePlugin):\n    deadlinePlugin.Cleanup()\n\n\nclass OpenPypeDeadlinePlugin(DeadlinePlugin):\n    \"\"\"\n        Standalone plugin for publishing from OpenPype.\n\n        Calls OpenPype executable 'openpype_console' from first correctly found\n        file based on plugin configuration. Uses 'publish' command and passes\n        path to metadata json file, which contains all needed information\n        for publish process.\n    \"\"\"\n    def __init__(self):\n        super().__init__()\n        self.InitializeProcessCallback += self.InitializeProcess\n        self.RenderExecutableCallback += self.RenderExecutable\n        self.RenderArgumentCallback += self.RenderArgument\n\n    def Cleanup(self):\n        for stdoutHandler in self.StdoutHandlers:\n            del stdoutHandler.HandleCallback\n\n        del self.InitializeProcessCallback\n        del self.RenderExecutableCallback\n        del self.RenderArgumentCallback\n\n    def InitializeProcess(self):\n        self.PluginType = PluginType.Simple\n        self.StdoutHandling = True\n\n        self.SingleFramesOnly = self.GetBooleanPluginInfoEntryWithDefault(\n            \"SingleFramesOnly\", False)\n        self.LogInfo(\"Single Frames Only: %s\" % self.SingleFramesOnly)\n\n        self.AddStdoutHandlerCallback(\n            \".*Progress: (\\d+)%.*\").HandleCallback += self.HandleProgress\n\n    @staticmethod\n    def get_openpype_version_from_path(path, build=True):\n        \"\"\"Get OpenPype version from provided path.\n             path (str): Path to scan.\n             build (bool, optional): Get only builds, not sources\n\n        Returns:\n            str or None: version of OpenPype if found.\n\n        \"\"\"\n        # fix path for application bundle on macos\n        if platform.system().lower() == \"darwin\":\n            path = os.path.join(path, \"MacOS\")\n\n        version_file = os.path.join(path, \"openpype\", \"version.py\")\n        if not os.path.isfile(version_file):\n            return None\n\n        # skip if the version is not build\n        exe = os.path.join(path, \"openpype_console.exe\")\n        if platform.system().lower() in [\"linux\", \"darwin\"]:\n            exe = os.path.join(path, \"openpype_console\")\n\n        # if only builds are requested\n        if build and not os.path.isfile(exe):  # noqa: E501\n            print(f\"   ! path is not a build: {path}\")\n            return None\n\n        version = {}\n        with open(version_file, \"r\") as vf:\n            exec(vf.read(), version)\n\n        version_match = re.search(r\"(\\d+\\.\\d+.\\d+).*\", version[\"__version__\"])\n        return version_match[1]\n\n    def RenderExecutable(self):\n        job = self.GetJob()\n        openpype_versions = []\n        # if the job requires specific OpenPype version,\n        # lets go over all available and find compatible build.\n        requested_version = job.GetJobEnvironmentKeyValue(\"OPENPYPE_VERSION\")\n        if requested_version:\n            self.LogInfo((\n                \"Scanning for compatible requested \"\n                f\"version {requested_version}\"))\n            dir_list = self.GetConfigEntry(\"OpenPypeInstallationDirs\")\n\n            # clean '\\ ' for MacOS pasting\n            if platform.system().lower() == \"darwin\":\n                dir_list = dir_list.replace(\"\\\\ \", \" \")\n\n            for dir_list in dir_list.split(\",\"):\n                install_dir = DirectoryUtils.SearchDirectoryList(dir_list)\n                if install_dir:\n                    sub_dirs = [\n                        f.path for f in os.scandir(install_dir)\n                        if f.is_dir()\n                    ]\n                    for subdir in sub_dirs:\n                        version = self.get_openpype_version_from_path(subdir)\n                        if not version:\n                            continue\n                        openpype_versions.append((version, subdir))\n\n        exe_list = self.GetConfigEntry(\"OpenPypeExecutable\")\n        # clean '\\ ' for MacOS pasting\n        if platform.system().lower() == \"darwin\":\n            exe_list = exe_list.replace(\"\\\\ \", \" \")\n        exe = FileUtils.SearchFileList(exe_list)\n        if openpype_versions:\n            # if looking for requested compatible version,\n            # add the implicitly specified to the list too.\n            version = self.get_openpype_version_from_path(\n                os.path.dirname(exe))\n            if version:\n                openpype_versions.append((version, os.path.dirname(exe)))\n\n        if requested_version:\n            # sort detected versions\n            if openpype_versions:\n                openpype_versions.sort(\n                    key=lambda ver: [\n                        int(t) if t.isdigit() else t.lower()\n                        for t in re.split(r\"(\\d+)\", ver[0])\n                    ])\n            requested_major, requested_minor, _ = requested_version.split(\".\")[:3]  # noqa: E501\n            compatible_versions = []\n            for version in openpype_versions:\n                v = version[0].split(\".\")[:3]\n                if v[0] == requested_major and v[1] == requested_minor:\n                    compatible_versions.append(version)\n            if not compatible_versions:\n                self.FailRender((\"Cannot find compatible version available \"\n                                 \"for version {} requested by the job. \"\n                                 \"Please add it through plugin configuration \"\n                                 \"in Deadline or install it to configured \"\n                                 \"directory.\").format(requested_version))\n            # sort compatible versions nad pick the last one\n            compatible_versions.sort(\n                key=lambda ver: [\n                    int(t) if t.isdigit() else t.lower()\n                    for t in re.split(r\"(\\d+)\", ver[0])\n                ])\n            # create list of executables for different platform and let\n            # Deadline decide.\n            exe_list = [\n                os.path.join(\n                    compatible_versions[-1][1], \"openpype_console.exe\"),\n                os.path.join(\n                    compatible_versions[-1][1], \"openpype_console\"),\n                os.path.join(\n                    compatible_versions[-1][1], \"MacOS\", \"openpype_console\")\n            ]\n            exe = FileUtils.SearchFileList(\";\".join(exe_list))\n\n        if exe == \"\":\n            self.FailRender(\n                \"OpenPype executable was not found \" +\n                \"in the semicolon separated list \" +\n                \"\\\"\" + \";\".join(exe_list) + \"\\\". \" +\n                \"The path to the render executable can be configured \" +\n                \"from the Plugin Configuration in the Deadline Monitor.\")\n        return exe\n\n    def RenderArgument(self):\n        arguments = str(self.GetPluginInfoEntryWithDefault(\"Arguments\", \"\"))\n        arguments = RepositoryUtils.CheckPathMapping(arguments)\n\n        arguments = re.sub(r\"<(?i)STARTFRAME>\", str(self.GetStartFrame()),\n                           arguments)\n        arguments = re.sub(r\"<(?i)ENDFRAME>\", str(self.GetEndFrame()),\n                           arguments)\n        arguments = re.sub(r\"<(?i)QUOTE>\", \"\\\"\", arguments)\n\n        arguments = self.ReplacePaddedFrame(arguments,\n                                            \"<(?i)STARTFRAME%([0-9]+)>\",\n                                            self.GetStartFrame())\n        arguments = self.ReplacePaddedFrame(arguments,\n                                            \"<(?i)ENDFRAME%([0-9]+)>\",\n                                            self.GetEndFrame())\n\n        count = 0\n        for filename in self.GetAuxiliaryFilenames():\n            localAuxFile = Path.Combine(self.GetJobsDataDirectory(), filename)\n            arguments = re.sub(r\"<(?i)AUXFILE\" + str(count) + r\">\",\n                               localAuxFile.replace(\"\\\\\", \"/\"), arguments)\n            count += 1\n\n        return arguments\n\n    def ReplacePaddedFrame(self, arguments, pattern, frame):\n        frameRegex = Regex(pattern)\n        while True:\n            frameMatch = frameRegex.Match(arguments)\n            if frameMatch.Success:\n                paddingSize = int(frameMatch.Groups[1].Value)\n                if paddingSize > 0:\n                    padding = StringUtils.ToZeroPaddedString(frame,\n                                                             paddingSize,\n                                                             False)\n                else:\n                    padding = str(frame)\n                arguments = arguments.replace(frameMatch.Groups[0].Value,\n                                              padding)\n            else:\n                break\n\n        return arguments\n\n    def HandleProgress(self):\n        progress = float(self.GetRegexMatch(1))\n        self.SetProgress(progress)\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options",
    "content": "[OIIOToolPath]\nType=filename\nLabel=OIIO Tool location\nCategory=OIIO\nIndex=0\nDescription=OIIO Tool executable to use.\nRequired=false\nDisableIfBlank=true\n\n[OutputFile]\nType=filenamesave\nLabel=Output File\nCategory=Output\nIndex=0\nDescription=The scene filename as it exists on the network\nRequired=false\nDisableIfBlank=true\n\n[CleanupTiles]\nType=boolean\nCategory=Options\nIndex=0\nLabel=Cleanup Tiles\nRequired=false\nDisableIfBlank=true\nDescription=If enabled, the OpenPype Tile Assembler will cleanup all tiles after assembly.\n\n[Renderer]\nType=string\nLabel=Renderer\nCategory=Quicktime Info\nIndex=0\nDescription=Renderer name\nRequired=false\nDisableIfBlank=true\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param",
    "content": "[About]\nType=label\nLabel=About\nCategory=About Plugin\nCategoryOrder=-1\nIndex=0\nDefault=OpenPype Tile Assembler Plugin for Deadline\nDescription=Not configurable\n\n[OIIOTool_RenderExecutable]\nType=multilinemultifilename\nLabel=OIIO Tool Executable\nCategory=Render Executables\nCategoryOrder=0\nDefault=C:\\Program Files\\OIIO\\bin\\oiiotool.exe;/usr/bin/oiiotool\nDescription=The path to the Open Image IO Tool executable file used for rendering. Enter alternative paths on separate lines.\nW\n"
  },
  {
    "path": "openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tile Assembler Plugin using Open Image IO tool.\n\nTodo:\n    Currently we support only EXRs with their data window set.\n\"\"\"\nimport os\nimport re\nimport subprocess\nimport xml.etree.ElementTree\n\nfrom System.IO import Path\n\nfrom Deadline.Plugins import DeadlinePlugin\nfrom Deadline.Scripting import (\n    FileUtils, RepositoryUtils, SystemUtils)\n\n\nversion_major = 1\nversion_minor = 0\nversion_patch = 0\nversion_string = \"{}.{}.{}\".format(version_major, version_minor, version_patch)\nSTRING_TAGS = {\n    \"format\"\n}\nINT_TAGS = {\n    \"x\", \"y\", \"z\",\n    \"width\", \"height\", \"depth\",\n    \"full_x\", \"full_y\", \"full_z\",\n    \"full_width\", \"full_height\", \"full_depth\",\n    \"tile_width\", \"tile_height\", \"tile_depth\",\n    \"nchannels\",\n    \"alpha_channel\",\n    \"z_channel\",\n    \"deep\",\n    \"subimages\",\n}\n\n\nXML_CHAR_REF_REGEX_HEX = re.compile(r\"&#x?[0-9a-fA-F]+;\")\n\n# Regex to parse array attributes\nARRAY_TYPE_REGEX = re.compile(r\"^(int|float|string)\\[\\d+\\]$\")\n\n\ndef convert_value_by_type_name(value_type, value):\n    \"\"\"Convert value to proper type based on type name.\n\n    In some cases value types have custom python class.\n    \"\"\"\n\n    # Simple types\n    if value_type == \"string\":\n        return value\n\n    if value_type == \"int\":\n        return int(value)\n\n    if value_type == \"float\":\n        return float(value)\n\n    # Vectors will probably have more types\n    if value_type in (\"vec2f\", \"float2\"):\n        return [float(item) for item in value.split(\",\")]\n\n    # Matrix should be always have square size of element 3x3, 4x4\n    # - are returned as list of lists\n    if value_type == \"matrix\":\n        output = []\n        current_index = -1\n        parts = value.split(\",\")\n        parts_len = len(parts)\n        if parts_len == 1:\n            divisor = 1\n        elif parts_len == 4:\n            divisor = 2\n        elif parts_len == 9:\n            divisor = 3\n        elif parts_len == 16:\n            divisor = 4\n        else:\n            print(\"Unknown matrix resolution {}. Value: \\\"{}\\\"\".format(\n                parts_len, value\n            ))\n            for part in parts:\n                output.append(float(part))\n            return output\n\n        for idx, item in enumerate(parts):\n            list_index = idx % divisor\n            if list_index > current_index:\n                current_index = list_index\n                output.append([])\n            output[list_index].append(float(item))\n        return output\n\n    if value_type == \"rational2i\":\n        parts = value.split(\"/\")\n        top = float(parts[0])\n        bottom = 1.0\n        if len(parts) != 1:\n            bottom = float(parts[1])\n        return float(top) / float(bottom)\n\n    if value_type == \"vector\":\n        parts = [part.strip() for part in value.split(\",\")]\n        output = []\n        for part in parts:\n            if part == \"-nan\":\n                output.append(None)\n                continue\n            try:\n                part = float(part)\n            except ValueError:\n                pass\n            output.append(part)\n        return output\n\n    if value_type == \"timecode\":\n        return value\n\n    # Array of other types is converted to list\n    re_result = ARRAY_TYPE_REGEX.findall(value_type)\n    if re_result:\n        array_type = re_result[0]\n        output = []\n        for item in value.split(\",\"):\n            output.append(\n                convert_value_by_type_name(array_type, item)\n            )\n        return output\n\n    print((\n        \"Dev note (missing implementation):\"\n        \" Unknown attrib type \\\"{}\\\". Value: {}\"\n    ).format(value_type, value))\n    return value\n\n\ndef parse_oiio_xml_output(xml_string):\n    \"\"\"Parse xml output from OIIO info command.\"\"\"\n    output = {}\n    if not xml_string:\n        return output\n\n    # Fix values with ampresand (lazy fix)\n    # - oiiotool exports invalid xml which ElementTree can't handle\n    #   e.g. \"&#01;\"\n    # WARNING: this will affect even valid character entities. If you need\n    #   those values correctly, this must take care of valid character ranges.\n    #   See https://github.com/pypeclub/OpenPype/pull/2729\n    matches = XML_CHAR_REF_REGEX_HEX.findall(xml_string)\n    for match in matches:\n        new_value = match.replace(\"&\", \"&amp;\")\n        xml_string = xml_string.replace(match, new_value)\n\n    tree = xml.etree.ElementTree.fromstring(xml_string)\n    attribs = {}\n    output[\"attribs\"] = attribs\n    for child in tree:\n        tag_name = child.tag\n        if tag_name == \"attrib\":\n            attrib_def = child.attrib\n            value = convert_value_by_type_name(\n                attrib_def[\"type\"], child.text\n            )\n\n            attribs[attrib_def[\"name\"]] = value\n            continue\n\n        # Channels are stored as tex on each child\n        if tag_name == \"channelnames\":\n            value = []\n            for channel in child:\n                value.append(channel.text)\n\n        # Convert known integer type tags to int\n        elif tag_name in INT_TAGS:\n            value = int(child.text)\n\n        # Keep value of known string tags\n        elif tag_name in STRING_TAGS:\n            value = child.text\n\n        # Keep value as text for unknown tags\n        # - feel free to add more tags\n        else:\n            value = child.text\n            print((\n                \"Dev note (missing implementation):\"\n                \" Unknown tag \\\"{}\\\". Value \\\"{}\\\"\"\n            ).format(tag_name, value))\n\n        output[child.tag] = value\n\n    return output\n\n\ndef info_about_input(oiiotool_path, filepath):\n    args = [\n        oiiotool_path,\n        \"--info\",\n        \"-v\",\n        \"-i:infoformat=xml\",\n        filepath\n    ]\n    popen = subprocess.Popen(args, stdout=subprocess.PIPE)\n    _stdout, _stderr = popen.communicate()\n    output = \"\"\n    if _stdout:\n        output += _stdout.decode(\"utf-8\", errors=\"backslashreplace\")\n\n    if _stderr:\n        output += _stderr.decode(\"utf-8\", errors=\"backslashreplace\")\n\n    output = output.replace(\"\\r\\n\", \"\\n\")\n    xml_started = False\n    lines = []\n    for line in output.split(\"\\n\"):\n        if not xml_started:\n            if not line.startswith(\"<\"):\n                continue\n            xml_started = True\n        if xml_started:\n            lines.append(line)\n\n    if not xml_started:\n        raise ValueError(\n            \"Failed to read input file \\\"{}\\\".\\nOutput:\\n{}\".format(\n                filepath, output\n            )\n        )\n    xml_text = \"\\n\".join(lines)\n    return parse_oiio_xml_output(xml_text)\n\n\ndef GetDeadlinePlugin():  # noqa: N802\n    \"\"\"Helper.\"\"\"\n    return OpenPypeTileAssembler()\n\n\ndef CleanupDeadlinePlugin(deadlinePlugin):  # noqa: N802, N803\n    \"\"\"Helper.\"\"\"\n    deadlinePlugin.cleanup()\n\n\nclass OpenPypeTileAssembler(DeadlinePlugin):\n    \"\"\"Deadline plugin for assembling tiles using OIIO.\"\"\"\n\n    def __init__(self):\n        \"\"\"Init.\"\"\"\n        super().__init__()\n        self.InitializeProcessCallback += self.initialize_process\n        self.RenderExecutableCallback += self.render_executable\n        self.RenderArgumentCallback += self.render_argument\n        self.PreRenderTasksCallback += self.pre_render_tasks\n        self.PostRenderTasksCallback += self.post_render_tasks\n\n    def cleanup(self):\n        \"\"\"Cleanup function.\"\"\"\n        for stdoutHandler in self.StdoutHandlers:\n            del stdoutHandler.HandleCallback\n\n        del self.InitializeProcessCallback\n        del self.RenderExecutableCallback\n        del self.RenderArgumentCallback\n        del self.PreRenderTasksCallback\n        del self.PostRenderTasksCallback\n\n    def initialize_process(self):\n        \"\"\"Initialization.\"\"\"\n        self.LogInfo(\"Plugin version: {}\".format(version_string))\n        self.SingleFramesOnly = True\n        self.StdoutHandling = True\n        self.renderer = self.GetPluginInfoEntryWithDefault(\n            \"Renderer\", \"undefined\")\n        self.AddStdoutHandlerCallback(\n            \".*Error.*\").HandleCallback += self.handle_stdout_error\n\n    def render_executable(self):\n        \"\"\"Get render executable name.\n\n        Get paths from plugin configuration, find executable and return it.\n\n        Returns:\n            (str): Render executable.\n\n        \"\"\"\n        oiiotool_exe_list = self.GetConfigEntry(\"OIIOTool_RenderExecutable\")\n        oiiotool_exe = FileUtils.SearchFileList(oiiotool_exe_list)\n\n        if oiiotool_exe == \"\":\n            self.FailRender((\"No file found in the semicolon separated \"\n                             \"list \\\"{}\\\". The path to the render executable \"\n                             \"can be configured from the Plugin Configuration \"\n                             \"in the Deadline Monitor.\").format(\n                                oiiotool_exe_list))\n\n        return oiiotool_exe\n\n    def render_argument(self):\n        \"\"\"Generate command line arguments for render executable.\n\n        Returns:\n            (str): arguments to add to render executable.\n\n        \"\"\"\n        # Read tile config file. This file is in compatible format with\n        # Draft Tile Assembler\n        data = {}\n        with open(self.config_file, \"rU\") as f:\n            for text in f:\n                # Parsing key-value pair and removing white-space\n                # around the entries\n                info = [x.strip() for x in text.split(\"=\", 1)]\n\n                if len(info) > 1:\n                    try:\n                        data[str(info[0])] = info[1]\n                    except Exception as e:\n                        # should never be called\n                        self.FailRender(\n                            \"Cannot parse config file: {}\".format(e))\n\n        # Get output file. We support only EXRs now.\n        output_file = data[\"ImageFileName\"]\n        output_file = RepositoryUtils.CheckPathMapping(output_file)\n        output_file = self.process_path(output_file)\n\n        tile_info = []\n        for tile in range(int(data[\"TileCount\"])):\n            tile_info.append({\n                \"filepath\": data[\"Tile{}\".format(tile)],\n                \"pos_x\": int(data[\"Tile{}X\".format(tile)]),\n                \"pos_y\": int(data[\"Tile{}Y\".format(tile)]),\n                \"height\": int(data[\"Tile{}Height\".format(tile)]),\n                \"width\": int(data[\"Tile{}Width\".format(tile)])\n            })\n\n        arguments = self.tile_oiio_args(\n            int(data[\"ImageWidth\"]), int(data[\"ImageHeight\"]),\n            tile_info, output_file)\n        self.LogInfo(\n            \"Using arguments: {}\".format(\" \".join(arguments)))\n        self.tiles = tile_info\n        return \" \".join(arguments)\n\n    def process_path(self, filepath):\n        \"\"\"Handle slashes in file paths.\"\"\"\n        if SystemUtils.IsRunningOnWindows():\n            filepath = filepath.replace(\"/\", \"\\\\\")\n            if filepath.startswith(\"\\\\\") and not filepath.startswith(\"\\\\\\\\\"):\n                filepath = \"\\\\\" + filepath\n        else:\n            filepath = filepath.replace(\"\\\\\", \"/\")\n        return filepath\n\n    def pre_render_tasks(self):\n        \"\"\"Load config file and do remapping.\"\"\"\n        self.LogInfo(\"OpenPype Tile Assembler starting...\")\n        config_file = self.GetPluginInfoEntry(\"ConfigFile\")\n\n        temp_scene_directory = self.CreateTempDirectory(\n            \"thread\" + str(self.GetThreadNumber()))\n        temp_scene_filename = Path.GetFileName(config_file)\n        self.config_file = Path.Combine(\n            temp_scene_directory, temp_scene_filename)\n\n        if SystemUtils.IsRunningOnWindows():\n            RepositoryUtils.CheckPathMappingInFileAndReplaceSeparator(\n                config_file, self.config_file, \"/\", \"\\\\\")\n        else:\n            RepositoryUtils.CheckPathMappingInFileAndReplaceSeparator(\n                config_file, self.config_file, \"\\\\\", \"/\")\n            os.chmod(self.config_file, os.stat(self.config_file).st_mode)\n\n    def post_render_tasks(self):\n        \"\"\"Cleanup tiles if required.\"\"\"\n        if self.GetBooleanPluginInfoEntryWithDefault(\"CleanupTiles\", False):\n            self.LogInfo(\"Cleaning up Tiles...\")\n            for tile in self.tiles:\n                try:\n                    self.LogInfo(\"Deleting: {}\".format(tile[\"filepath\"]))\n                    os.remove(tile[\"filepath\"])\n                    # By this time we would have errored out\n                    # if error on missing was enabled\n                except KeyError:\n                    pass\n                except OSError:\n                    self.LogInfo(\"Failed to delete: {}\".format(\n                        tile[\"filepath\"]))\n                    pass\n\n        self.LogInfo(\"OpenPype Tile Assembler Job finished.\")\n\n    def handle_stdout_error(self):\n        \"\"\"Handle errors in stdout.\"\"\"\n        self.FailRender(self.GetRegexMatch(0))\n\n    def tile_oiio_args(\n            self, output_width, output_height, tile_info, output_path):\n        \"\"\"Generate oiio tool arguments for tile assembly.\n\n        Args:\n            output_width (int): Width of output image.\n            output_height (int): Height of output image.\n            tiles_info (list): List of tile items, each item must be\n                dictionary with `filepath`, `pos_x` and `pos_y` keys\n                representing path to file and x, y coordinates on output\n                image where top-left point of tile item should start.\n            output_path (str): Path to file where should be output stored.\n\n        Returns:\n            (list): oiio tools arguments.\n\n        \"\"\"\n        args = []\n\n        # Create new image with output resolution, and with same type and\n        # channels as input\n        oiiotool_path = self.render_executable()\n        first_tile_path = tile_info[0][\"filepath\"]\n        first_tile_info = info_about_input(oiiotool_path, first_tile_path)\n        create_arg_template = \"--create{} {}x{} {}\"\n\n        image_type = \"\"\n        image_format = first_tile_info.get(\"format\")\n        if image_format:\n            image_type = \":type={}\".format(image_format)\n\n        create_arg = create_arg_template.format(\n            image_type, output_width,\n            output_height, first_tile_info[\"nchannels\"]\n        )\n        args.append(create_arg)\n\n        for tile in tile_info:\n            path = tile[\"filepath\"]\n            pos_x = tile[\"pos_x\"]\n            tile_height = info_about_input(oiiotool_path, path)[\"height\"]\n            if self.renderer == \"vray\":\n                pos_y = tile[\"pos_y\"]\n            else:\n                pos_y = output_height - tile[\"pos_y\"] - tile_height\n\n            # Add input path and make sure inputs origin is 0, 0\n            args.append(path)\n            args.append(\"--origin +0+0\")\n            # Swap to have input as foreground\n            args.append(\"--swap\")\n            # Paste foreground to background\n            args.append(\"--paste {x:+d}{y:+d}\".format(x=pos_x, y=pos_y))\n\n        args.append(\"-o\")\n        args.append(output_path)\n\n        return args\n"
  },
  {
    "path": "openpype/modules/deadline/repository/readme.md",
    "content": "## OpenPype Deadline repository overlay\n\n This directory is an overlay for Deadline repository. \n It means that you can copy the whole hierarchy to Deadline repository and it \n should work.\n \n Logic:\n -----\n GlobalJobPreLoad\n ----- \n \nThe `GlobalJobPreLoad` will retrieve the OpenPype executable path from the\n`OpenPype` Deadline Plug-in's settings. Then it will call the executable to \nretrieve the environment variables needed for the Deadline Job.\nThese environment variables are injected into rendering process.\n\nDeadline triggers the `GlobalJobPreLoad.py` for each Worker as it starts the \nJob.  \n\n*Note*: It also contains backward compatible logic to preserve functionality \nfor old Pype2 and non-OpenPype triggered jobs.\n \n Plugin\n ------\n For each render and publishing job the `OpenPype` Deadline Plug-in is checked \n for the configured location of the OpenPype executable (needs to be configured \n in `Deadline's Configure Plugins > OpenPype`) through `GlobalJobPreLoad`.\n \n \n"
  },
  {
    "path": "openpype/modules/example_addons/example_addon/__init__.py",
    "content": "\"\"\" Addon class definition and Settings definition must be imported here.\n\nIf addon class or settings definition won't be here their definition won't\nbe found by OpenPype discovery.\n\"\"\"\n\nfrom .addon import (\n    AddonSettingsDef,\n    ExampleAddon\n)\n\n__all__ = (\n    \"AddonSettingsDef\",\n    \"ExampleAddon\"\n)\n"
  },
  {
    "path": "openpype/modules/example_addons/example_addon/addon.py",
    "content": "\"\"\"Addon definition is located here.\n\nImport of python packages that may not be available should not be imported\nin global space here until are required or used.\n- Qt related imports\n- imports of Python 3 packages\n    - we still support Python 2 hosts where addon definition should available\n\"\"\"\n\nimport os\n\nfrom openpype.modules import (\n    click_wrap,\n    JsonFilesSettingsDef,\n    OpenPypeAddOn,\n    ModulesManager,\n    IPluginPaths,\n    ITrayAction\n)\n\n\n# Settings definition of this addon using `JsonFilesSettingsDef`\n# - JsonFilesSettingsDef is prepared settings definition using json files\n#   to define settings and store default values\nclass AddonSettingsDef(JsonFilesSettingsDef):\n    # This will add prefixes to every schema and template from `schemas`\n    #   subfolder.\n    # - it is not required to fill the prefix but it is highly\n    #   recommended as schemas and templates may have name clashes across\n    #   multiple addons\n    # - it is also recommended that prefix has addon name in it\n    schema_prefix = \"example_addon\"\n\n    def get_settings_root_path(self):\n        \"\"\"Implemented abstract class of JsonFilesSettingsDef.\n\n        Return directory path where json files defying addon settings are\n        located.\n        \"\"\"\n        return os.path.join(\n            os.path.dirname(os.path.abspath(__file__)),\n            \"settings\"\n        )\n\n\nclass ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction):\n    \"\"\"This Addon has defined its settings and interface.\n\n    This example has system settings with an enabled option. And use\n    few other interfaces:\n    - `IPluginPaths` to define custom plugin paths\n    - `ITrayAction` to be shown in tray tool\n    \"\"\"\n    label = \"Example Addon\"\n    name = \"example_addon\"\n\n    def initialize(self, settings):\n        \"\"\"Initialization of addon.\"\"\"\n        module_settings = settings[self.name]\n        # Enabled by settings\n        self.enabled = module_settings.get(\"enabled\", False)\n\n        # Prepare variables that can be used or set afterwards\n        self._connected_modules = None\n        # UI which must not be created at this time\n        self._dialog = None\n\n    def tray_init(self):\n        \"\"\"Implementation of abstract method for `ITrayAction`.\n\n        We're definitely  in tray tool so we can pre create dialog.\n        \"\"\"\n\n        self._create_dialog()\n\n    def _create_dialog(self):\n        # Don't recreate dialog if already exists\n        if self._dialog is not None:\n            return\n\n        from .widgets import MyExampleDialog\n\n        self._dialog = MyExampleDialog()\n\n    def show_dialog(self):\n        \"\"\"Show dialog with connected modules.\n\n        This can be called from anywhere but can also crash in headless mode.\n        There is no way to prevent addon to do invalid operations if he's\n        not handling them.\n        \"\"\"\n        # Make sure dialog is created\n        self._create_dialog()\n        # Show dialog\n        self._dialog.open()\n\n    def get_connected_modules(self):\n        \"\"\"Custom implementation of addon.\"\"\"\n        names = set()\n        if self._connected_modules is not None:\n            for module in self._connected_modules:\n                names.add(module.name)\n        return names\n\n    def on_action_trigger(self):\n        \"\"\"Implementation of abstract method for `ITrayAction`.\"\"\"\n        self.show_dialog()\n\n    def get_plugin_paths(self):\n        \"\"\"Implementation of abstract method for `IPluginPaths`.\"\"\"\n        current_dir = os.path.dirname(os.path.abspath(__file__))\n\n        return {\n            \"publish\": [os.path.join(current_dir, \"plugins\", \"publish\")]\n        }\n\n    def cli(self, click_group):\n        click_group.add_command(cli_main.to_click_obj())\n\n\n@click_wrap.group(\n    ExampleAddon.name,\n    help=\"Example addon dynamic cli commands.\")\ndef cli_main():\n    pass\n\n\n@cli_main.command()\ndef nothing():\n    \"\"\"Does nothing but print a message.\"\"\"\n    print(\"You've triggered \\\"nothing\\\" command.\")\n\n\n@cli_main.command()\ndef show_dialog():\n    \"\"\"Show ExampleAddon dialog.\n\n    We don't have access to addon directly through cli so we have to create\n    it again.\n    \"\"\"\n    from openpype.tools.utils.lib import qt_app_context\n\n    manager = ModulesManager()\n    example_addon = manager.modules_by_name[ExampleAddon.name]\n    with qt_app_context():\n        example_addon.show_dialog()\n"
  },
  {
    "path": "openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py",
    "content": "import pyblish.api\n\n\nclass CollectExampleAddon(pyblish.api.ContextPlugin):\n    order = pyblish.api.CollectorOrder + 0.4\n    label = \"Collect Example Addon\"\n\n    def process(self, context):\n        self.log.info(\"I'm in example addon's plugin!\")\n"
  },
  {
    "path": "openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json",
    "content": "{\n    \"project_settings/example_addon\": {\n        \"number\": 0,\n        \"color_1\": [\n            0.0,\n            0.0,\n            0.0\n        ],\n        \"color_2\": [\n            0.0,\n            0.0,\n            0.0\n        ]\n    }\n}"
  },
  {
    "path": "openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json",
    "content": "{\n    \"modules/example_addon\": {\n        \"enabled\": true\n    }\n}"
  },
  {
    "path": "openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json",
    "content": "{\n    \"project_settings/global\": {\n        \"type\": \"schema\",\n        \"name\": \"example_addon/main\"\n    }\n}\n"
  },
  {
    "path": "openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json",
    "content": "{\n    \"system_settings/modules\": {\n        \"type\": \"schema\",\n        \"name\": \"example_addon/main\"\n    }\n}\n"
  },
  {
    "path": "openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"example_addon\",\n    \"label\": \"Example addon\",\n    \"collapsible\": true,\n    \"children\": [\n        {\n            \"type\": \"number\",\n            \"key\": \"number\",\n            \"label\": \"This is your lucky number:\",\n            \"minimum\": 7,\n            \"maximum\": 7,\n            \"decimals\": 0\n        },\n        {\n            \"type\": \"template\",\n            \"name\": \"example_addon/the_template\",\n            \"template_data\": [\n                {\n                    \"name\": \"color_1\",\n                    \"label\": \"Color 1\"\n                },\n                {\n                    \"name\": \"color_2\",\n                    \"label\": \"Color 2\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json",
    "content": "[\n    {\n        \"type\": \"list-strict\",\n        \"key\": \"{name}\",\n        \"label\": \"{label}\",\n        \"object_types\": [\n            {\n                \"label\": \"Red\",\n                \"type\": \"number\",\n                \"minimum\": 0,\n                \"maximum\": 1,\n                \"decimal\": 3\n            },\n            {\n                \"label\": \"Green\",\n                \"type\": \"number\",\n                \"minimum\": 0,\n                \"maximum\": 1,\n                \"decimal\": 3\n            },\n            {\n                \"label\": \"Blue\",\n                \"type\": \"number\",\n                \"minimum\": 0,\n                \"maximum\": 1,\n                \"decimal\": 3\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"example_addon\",\n    \"label\": \"Example addon\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/modules/example_addons/example_addon/widgets.py",
    "content": "from qtpy import QtWidgets\n\nfrom openpype.style import load_stylesheet\n\n\nclass MyExampleDialog(QtWidgets.QDialog):\n    def __init__(self, parent=None):\n        super(MyExampleDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"Connected modules\")\n\n        msg = \"This is example dialog of example addon.\"\n        label_widget = QtWidgets.QLabel(msg, self)\n\n        ok_btn = QtWidgets.QPushButton(\"OK\", self)\n        btns_layout = QtWidgets.QHBoxLayout()\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(ok_btn)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(label_widget)\n        layout.addLayout(btns_layout)\n\n        ok_btn.clicked.connect(self._on_ok_clicked)\n\n        self._label_widget = label_widget\n\n        self.setStyleSheet(load_stylesheet())\n\n    def _on_ok_clicked(self):\n        self.done(1)\n"
  },
  {
    "path": "openpype/modules/example_addons/tiny_addon.py",
    "content": "from openpype.modules import OpenPypeAddOn\n\n\nclass TinyAddon(OpenPypeAddOn):\n    \"\"\"This is tiniest possible addon.\n\n    This addon won't do much but will exist in OpenPype modules environment.\n    \"\"\"\n    name = \"tiniest_addon_ever\"\n"
  },
  {
    "path": "openpype/modules/ftrack/__init__.py",
    "content": "from .ftrack_module import (\n    FtrackModule,\n    FTRACK_MODULE_DIR,\n\n    resolve_ftrack_url,\n)\n\n__all__ = (\n    \"FtrackModule\",\n    \"FTRACK_MODULE_DIR\",\n\n    \"resolve_ftrack_url\",\n)\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py",
    "content": "import json\n\nfrom openpype_modules.ftrack.lib import ServerAction\n\n\ndef clone_review_session(session, entity):\n    # Create a client review with timestamp.\n    name = entity[\"name\"]\n    review_session = session.create(\n        \"ReviewSession\",\n        {\n            \"name\": f\"Clone of {name}\",\n            \"project\": entity[\"project\"]\n        }\n    )\n\n    # Add all invitees.\n    for invitee in entity[\"review_session_invitees\"]:\n        # Make sure email is not None but string\n        email = invitee[\"email\"] or \"\"\n        session.create(\n            \"ReviewSessionInvitee\",\n            {\n                \"name\": invitee[\"name\"],\n                \"email\": email,\n                \"review_session\": review_session\n            }\n        )\n\n    # Add all objects to new review session.\n    for obj in entity[\"review_session_objects\"]:\n        session.create(\n            \"ReviewSessionObject\",\n            {\n                \"name\": obj[\"name\"],\n                \"version\": obj[\"version\"],\n                \"review_session\": review_session,\n                \"asset_version\": obj[\"asset_version\"]\n            }\n        )\n\n    session.commit()\n\n\nclass CloneReviewSession(ServerAction):\n    '''Generate Client Review action\n    `label` a descriptive string identifying your action.\n    `varaint` To group actions together, give them the same\n    label and specify a unique variant per action.\n    `identifier` a unique identifier for your action.\n    `description` a verbose descriptive text for you action\n     '''\n    label = \"Clone Review Session\"\n    variant = None\n    identifier = \"clone-review-session\"\n    description = None\n    settings_key = \"clone_review_session\"\n\n    def discover(self, session, entities, event):\n        '''Return true if we can handle the selected entities.\n        *session* is a `ftrack_api.Session` instance\n        *entities* is a list of tuples each containing the entity type and the\n        entity id.\n        If the entity is a hierarchical you will always get the entity\n        type TypedContext, once retrieved through a get operation you\n        will have the \"real\" entity type ie. example Shot, Sequence\n        or Asset Build.\n        *event* the unmodified original event\n        '''\n        is_valid = (\n            len(entities) == 1\n            and entities[0].entity_type == \"ReviewSession\"\n        )\n        if is_valid:\n            is_valid = self.valid_roles(session, entities, event)\n        return is_valid\n\n    def launch(self, session, entities, event):\n        '''Callback method for the custom action.\n        return either a bool ( True if successful or False if the action\n        failed ) or a dictionary with they keys `message` and `success`, the\n        message should be a string and will be displayed as feedback to the\n        user, success should be a bool, True if successful or False if the\n        action failed.\n        *session* is a `ftrack_api.Session` instance\n        *entities* is a list of tuples each containing the entity type and the\n        entity id.\n        If the entity is a hierarchical you will always get the entity\n        type TypedContext, once retrieved through a get operation you\n        will have the \"real\" entity type ie. example Shot, Sequence\n        or Asset Build.\n        *event* the unmodified original event\n        '''\n        userId = event['source']['user']['id']\n        user = session.query('User where id is ' + userId).one()\n        job = session.create(\n            'Job',\n            {\n                'user': user,\n                'status': 'running',\n                'data': json.dumps({\n                    'description': 'Cloning Review Session.'\n                })\n            }\n        )\n        session.commit()\n\n        try:\n            clone_review_session(session, entities[0])\n\n            job['status'] = 'done'\n            session.commit()\n        except Exception:\n            session.rollback()\n            job[\"status\"] = \"failed\"\n            session.commit()\n            self.log.error(\n                \"Cloning review session failed ({})\", exc_info=True\n            )\n\n        return {\n            'success': True,\n            'message': 'Action completed successfully'\n        }\n\n\ndef register(session):\n    '''Register action. Called when used as an event plugin.'''\n\n    CloneReviewSession(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/action_create_review_session.py",
    "content": "import threading\nimport datetime\nimport copy\nimport collections\n\nimport ftrack_api\n\nfrom openpype.lib import get_datetime_data\nfrom openpype.settings.lib import (\n    get_project_settings,\n    get_default_project_settings\n)\nfrom openpype_modules.ftrack.lib import ServerAction\n\n\nclass CreateDailyReviewSessionServerAction(ServerAction):\n    \"\"\"Create daily review session object per project.\n\n    Action creates review sessions based on settings. Settings define if is\n    action enabled and what is a template for review session name. Logic works\n    in a way that if review session with the name already exists then skip\n    process. If review session for current day does not exist but yesterdays\n    review exists and is empty then yesterdays is renamed otherwise creates\n    new review session.\n\n    Also contains cycle creation of dailies which is triggered each morning.\n    This option must be enabled in project settings. Cycle creation is also\n    checked on registration of action.\n    \"\"\"\n\n    identifier = \"create.daily.review.session\"\n    #: Action label.\n    label = \"OpenPype Admin\"\n    variant = \"- Create Daily Review Session (Server)\"\n    #: Action description.\n    description = \"Manually create daily review session\"\n    role_list = {\"Pypeclub\", \"Administrator\", \"Project Manager\"}\n\n    settings_key = \"create_daily_review_session\"\n    default_template = \"{yy}{mm}{dd}\"\n\n    def __init__(self, *args, **kwargs):\n        super(CreateDailyReviewSessionServerAction, self).__init__(\n            *args, **kwargs\n        )\n\n        self._cycle_timer = None\n        self._last_cyle_time = None\n        self._day_delta = datetime.timedelta(days=1)\n\n    def discover(self, session, entities, event):\n        \"\"\"Show action only on AssetVersions.\"\"\"\n\n        valid_selection = False\n        for ent in event[\"data\"][\"selection\"]:\n            # Ignore entities that are not tasks or projects\n            if ent[\"entityType\"].lower() in (\n                \"show\", \"task\", \"reviewsession\", \"assetversion\"\n            ):\n                valid_selection = True\n                break\n\n        if not valid_selection:\n            return False\n        return self.valid_roles(session, entities, event)\n\n    def launch(self, session, entities, event):\n        project_entity = self.get_project_from_entity(entities[0], session)\n        project_name = project_entity[\"full_name\"]\n        project_settings = self.get_project_settings_from_event(\n            event, project_name\n        )\n        action_settings = self._extract_action_settings(project_settings)\n        project_name_by_id = {\n            project_entity[\"id\"]: project_name\n        }\n        settings_by_project_id = {\n            project_entity[\"id\"]: action_settings\n        }\n        self._process_review_session(\n            session, settings_by_project_id, project_name_by_id\n        )\n        return True\n\n    def _calculate_next_cycle_delta(self):\n        studio_default_settings = get_default_project_settings()\n        action_settings = (\n            studio_default_settings\n            [\"ftrack\"]\n            [self.settings_frack_subkey]\n            [self.settings_key]\n        )\n        cycle_hour_start = action_settings.get(\"cycle_hour_start\")\n        if not cycle_hour_start:\n            h = m = s = 0\n        else:\n            h, m, s = cycle_hour_start\n\n        # Create threading timer which will trigger creation of report\n        #   at the 00:00:01 of next day\n        # - callback will trigger another timer which will have 1 day offset\n        now = datetime.datetime.now()\n        # Create object of today morning\n        expected_next_trigger = datetime.datetime(\n            now.year, now.month, now.day, h, m, s\n        )\n        if expected_next_trigger > now:\n            seconds = (expected_next_trigger - now).total_seconds()\n        else:\n            expected_next_trigger += self._day_delta\n            seconds = (expected_next_trigger - now).total_seconds()\n        return seconds, expected_next_trigger\n\n    def register(self, *args, **kwargs):\n        \"\"\"Override register to be able trigger \"\"\"\n        # Register server action as would be normally\n        super(CreateDailyReviewSessionServerAction, self).register(\n            *args, **kwargs\n        )\n\n        seconds_delta, cycle_time = self._calculate_next_cycle_delta()\n\n        # Store cycle time which will be used to create next timer\n        self._last_cyle_time = cycle_time\n        # Create timer thread\n        self._cycle_timer = threading.Timer(\n            seconds_delta, self._timer_callback\n        )\n        self._cycle_timer.start()\n\n        self._check_review_session()\n\n    def _timer_callback(self):\n        if (\n            self._cycle_timer is not None\n            and self._last_cyle_time is not None\n        ):\n            seconds_delta, cycle_time = self._calculate_next_cycle_delta()\n            self._last_cyle_time = cycle_time\n\n            self._cycle_timer = threading.Timer(\n                seconds_delta, self._timer_callback\n            )\n            self._cycle_timer.start()\n        self._check_review_session()\n\n    def _check_review_session(self):\n        session = ftrack_api.Session(\n            server_url=self.session.server_url,\n            api_key=self.session.api_key,\n            api_user=self.session.api_user,\n            auto_connect_event_hub=False\n        )\n        project_entities = session.query(\n            \"select id, full_name from Project\"\n        ).all()\n        project_names_by_id = {\n            project_entity[\"id\"]: project_entity[\"full_name\"]\n            for project_entity in project_entities\n        }\n\n        action_settings_by_project_id = self._get_action_settings(\n            project_names_by_id\n        )\n        enabled_action_settings_by_project_id = {}\n        for item in action_settings_by_project_id.items():\n            project_id, action_settings = item\n            if action_settings.get(\"cycle_enabled\"):\n                enabled_action_settings_by_project_id[project_id] = (\n                    action_settings\n                )\n\n        if not enabled_action_settings_by_project_id:\n            self.log.info((\n                \"There are no projects that have enabled\"\n                \" cycle review sesison creation\"\n            ))\n\n        else:\n            self._process_review_session(\n                session,\n                enabled_action_settings_by_project_id,\n                project_names_by_id\n            )\n\n        session.close()\n\n    def _process_review_session(\n        self, session, settings_by_project_id, project_names_by_id\n    ):\n        review_sessions = session.query((\n            \"select id, name, project_id\"\n            \" from ReviewSession where project_id in ({})\"\n        ).format(self.join_query_keys(settings_by_project_id))).all()\n\n        review_sessions_by_project_id = collections.defaultdict(list)\n        for review_session in review_sessions:\n            project_id = review_session[\"project_id\"]\n            review_sessions_by_project_id[project_id].append(review_session)\n\n        # Prepare fill data for today's review sesison and yesterdays\n        now = datetime.datetime.now()\n        today_obj = datetime.datetime(\n            now.year, now.month, now.day, 0, 0, 0\n        )\n        yesterday_obj = today_obj - self._day_delta\n\n        today_fill_data = get_datetime_data(today_obj)\n        yesterday_fill_data = get_datetime_data(yesterday_obj)\n\n        # Loop through projects and try to create daily reviews\n        for project_id, action_settings in settings_by_project_id.items():\n            review_session_template = (\n                action_settings[\"review_session_template\"]\n            ).strip() or self.default_template\n\n            today_project_fill_data = copy.deepcopy(today_fill_data)\n            yesterday_project_fill_data = copy.deepcopy(yesterday_fill_data)\n            project_name = project_names_by_id[project_id]\n            today_project_fill_data[\"project_name\"] = project_name\n            yesterday_project_fill_data[\"project_name\"] = project_name\n\n            today_session_name = self._fill_review_template(\n                review_session_template, today_project_fill_data\n            )\n            yesterday_session_name = self._fill_review_template(\n                review_session_template, yesterday_project_fill_data\n            )\n            # Skip if today's session name could not be filled\n            if not today_session_name:\n                continue\n\n            # Find matching review session\n            project_review_sessions = review_sessions_by_project_id[project_id]\n            todays_session = None\n            yesterdays_session = None\n            for review_session in project_review_sessions:\n                session_name = review_session[\"name\"]\n                if session_name == today_session_name:\n                    todays_session = review_session\n                    break\n                elif session_name == yesterday_session_name:\n                    yesterdays_session = review_session\n\n            # Skip if today's session already exist\n            if todays_session is not None:\n                self.log.debug((\n                    \"Todays ReviewSession \\\"{}\\\"\"\n                    \" in project \\\"{}\\\" already exists\"\n                ).format(today_session_name, project_name))\n                continue\n\n            # Check if there is yesterday's session and is empty\n            # - in that case just rename it\n            if (\n                yesterdays_session is not None\n                and len(yesterdays_session[\"review_session_objects\"]) == 0\n            ):\n                self.log.debug((\n                    \"Renaming yesterdays empty review session \\\"{}\\\" to \\\"{}\\\"\"\n                    \" in project \\\"{}\\\"\"\n                ).format(\n                    yesterday_session_name, today_session_name, project_name\n                ))\n                yesterdays_session[\"name\"] = today_session_name\n                session.commit()\n                continue\n\n            # Create new review session with new name\n            self.log.debug((\n                \"Creating new review session \\\"{}\\\" in project \\\"{}\\\"\"\n            ).format(today_session_name, project_name))\n            session.create(\"ReviewSession\", {\n                \"project_id\": project_id,\n                \"name\": today_session_name\n            })\n            session.commit()\n\n    def _get_action_settings(self, project_names_by_id):\n        settings_by_project_id = {}\n        for project_id, project_name in project_names_by_id.items():\n            project_settings = get_project_settings(project_name)\n            action_settings = self._extract_action_settings(project_settings)\n            settings_by_project_id[project_id] = action_settings\n        return settings_by_project_id\n\n    def _extract_action_settings(self, project_settings):\n        return (\n            project_settings\n            .get(\"ftrack\", {})\n            .get(self.settings_frack_subkey, {})\n            .get(self.settings_key)\n        ) or {}\n\n    def _fill_review_template(self, template, data):\n        output = None\n        try:\n            output = template.format(**data)\n        except Exception:\n            self.log.warning(\n                (\n                    \"Failed to fill review session template {} with data {}\"\n                ).format(template, data),\n                exc_info=True\n            )\n        return output\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n    CreateDailyReviewSessionServerAction(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py",
    "content": "from openpype_modules.ftrack.lib import ServerAction\n\n\nclass MultipleNotesServer(ServerAction):\n    \"\"\"Action adds same note for muliple AssetVersions.\n\n    Note is added to selection of AssetVersions. Note is created with user\n    who triggered the action. It is possible to define note category of note.\n    \"\"\"\n\n    identifier = \"multiple.notes.server\"\n    label = \"Multiple Notes (Server)\"\n    description = \"Add same note to multiple Asset Versions\"\n\n    _none_category = \"__NONE__\"\n\n    def discover(self, session, entities, event):\n        \"\"\"Show action only on AssetVersions.\"\"\"\n        if not entities:\n            return False\n\n        for entity in entities:\n            if entity.entity_type.lower() != \"assetversion\":\n                return False\n        return True\n\n    def interface(self, session, entities, event):\n        event_source = event[\"source\"]\n        user_info = event_source.get(\"user\") or {}\n        user_id = user_info.get(\"id\")\n        if not user_id:\n            return None\n\n        values = event[\"data\"].get(\"values\")\n        if values:\n            return None\n\n        note_label = {\n            \"type\": \"label\",\n            \"value\": \"# Enter note: #\"\n        }\n\n        note_value = {\n            \"name\": \"note\",\n            \"type\": \"textarea\"\n        }\n\n        category_label = {\n            \"type\": \"label\",\n            \"value\": \"## Category: ##\"\n        }\n\n        category_data = []\n        category_data.append({\n            \"label\": \"- None -\",\n            \"value\": self._none_category\n        })\n        all_categories = session.query(\n            \"select id, name from NoteCategory\"\n        ).all()\n        for cat in all_categories:\n            category_data.append({\n                \"label\": cat[\"name\"],\n                \"value\": cat[\"id\"]\n            })\n        category_value = {\n            \"type\": \"enumerator\",\n            \"name\": \"category\",\n            \"data\": category_data,\n            \"value\": self._none_category\n        }\n\n        splitter = {\n            \"type\": \"label\",\n            \"value\": \"---\"\n        }\n\n        return [\n            note_label,\n            note_value,\n            splitter,\n            category_label,\n            category_value\n        ]\n\n    def launch(self, session, entities, event):\n        if \"values\" not in event[\"data\"]:\n            return None\n\n        values = event[\"data\"][\"values\"]\n        if len(values) <= 0 or \"note\" not in values:\n            return False\n\n        # Get Note text\n        note_value = values[\"note\"]\n        if note_value.lower().strip() == \"\":\n            return {\n                \"success\": True,\n                \"message\": \"Note was not entered. Skipping\"\n            }\n\n        # Get User\n        event_source = event[\"source\"]\n        user_info = event_source.get(\"user\") or {}\n        user_id = user_info.get(\"id\")\n        user = None\n        if user_id:\n            user = session.query(\n                'User where id is \"{}\"'.format(user_id)\n            ).first()\n\n        if not user:\n            return {\n                \"success\": False,\n                \"message\": \"Couldn't get user information.\"\n            }\n\n        # Logging message preparation\n        # - username\n        username = user.get(\"username\") or \"N/A\"\n\n        # - AssetVersion ids\n        asset_version_ids_str = \",\".join([entity[\"id\"] for entity in entities])\n\n        # Base note data\n        note_data = {\n            \"content\": note_value,\n            \"author\": user\n        }\n\n        # Get category\n        category_id = values[\"category\"]\n        if category_id == self._none_category:\n            category_id = None\n\n        category_name = None\n        if category_id is not None:\n            category = session.query(\n                \"select id, name from NoteCategory where id is \\\"{}\\\"\".format(\n                    category_id\n                )\n            ).first()\n            if category:\n                note_data[\"category\"] = category\n                category_name = category[\"name\"]\n\n        category_msg = \"\"\n        if category_name:\n            category_msg = \" with category: \\\"{}\\\"\".format(category_name)\n\n        self.log.warning((\n            \"Creating note{} as User \\\"{}\\\" on \"\n            \"AssetVersions: {} with value \\\"{}\\\"\"\n        ).format(category_msg, username, asset_version_ids_str, note_value))\n\n        # Create notes for entities\n        for entity in entities:\n            new_note = session.create(\"Note\", note_data)\n            entity[\"notes\"].append(new_note)\n            session.commit()\n        return True\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    MultipleNotesServer(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/action_prepare_project.py",
    "content": "import json\nimport copy\n\nfrom openpype.client import get_project, create_project\nfrom openpype.settings import ProjectSettings, SaveWarningExc\n\nfrom openpype_modules.ftrack.lib import (\n    ServerAction,\n    get_openpype_attr,\n    CUST_ATTR_AUTO_SYNC\n)\n\n\nclass PrepareProjectServer(ServerAction):\n    \"\"\"Prepare project attributes in Anatomy.\"\"\"\n\n    identifier = \"prepare.project.server\"\n    label = \"OpenPype Admin\"\n    variant = \"- Prepare Project (Server)\"\n    description = \"Set basic attributes on the project\"\n\n    settings_key = \"prepare_project\"\n\n    role_list = [\"Pypeclub\", \"Administrator\", \"Project Manager\"]\n\n    settings_key = \"prepare_project\"\n\n    item_splitter = {\"type\": \"label\", \"value\": \"---\"}\n    _keys_order = (\n        \"fps\",\n        \"frameStart\",\n        \"frameEnd\",\n        \"handleStart\",\n        \"handleEnd\",\n        \"clipIn\",\n        \"clipOut\",\n        \"resolutionHeight\",\n        \"resolutionWidth\",\n        \"pixelAspect\",\n        \"applications\",\n        \"tools_env\",\n        \"library_project\",\n    )\n\n    def discover(self, session, entities, event):\n        \"\"\"Show only on project.\"\"\"\n        if (\n            len(entities) != 1\n            or entities[0].entity_type.lower() != \"project\"\n        ):\n            return False\n\n        return self.valid_roles(session, entities, event)\n\n    def interface(self, session, entities, event):\n        if event['data'].get('values', {}):\n            return\n\n        # Inform user that this may take a while\n        self.show_message(event, \"Preparing data... Please wait\", True)\n        self.log.debug(\"Preparing data which will be shown\")\n\n        self.log.debug(\"Loading custom attributes\")\n\n        project_entity = entities[0]\n        project_name = project_entity[\"full_name\"]\n\n        project_settings = ProjectSettings(project_name)\n\n        project_anatom_settings = project_settings[\"project_anatomy\"]\n        root_items = self.prepare_root_items(project_anatom_settings)\n\n        ca_items, multiselect_enumerators = (\n            self.prepare_custom_attribute_items(project_anatom_settings)\n        )\n\n        self.log.debug(\"Heavy items are ready. Preparing last items group.\")\n\n        title = \"Prepare Project\"\n        items = []\n\n        # Add root items\n        items.extend(root_items)\n\n        items.append(self.item_splitter)\n        items.append({\n            \"type\": \"label\",\n            \"value\": \"<h3>Set basic Attributes:</h3>\"\n        })\n\n        items.extend(ca_items)\n\n        # This item will be last before enumerators\n        # Set value of auto synchronization\n        auto_sync_value = project_entity[\"custom_attributes\"].get(\n            CUST_ATTR_AUTO_SYNC, False\n        )\n        auto_sync_item = {\n            \"name\": CUST_ATTR_AUTO_SYNC,\n            \"type\": \"boolean\",\n            \"value\": auto_sync_value,\n            \"label\": \"AutoSync to Avalon\"\n        }\n        # Add autosync attribute\n        items.append(auto_sync_item)\n\n        # Add enumerator items at the end\n        for item in multiselect_enumerators:\n            items.append(item)\n\n        return {\n            \"items\": items,\n            \"title\": title\n        }\n\n    def prepare_root_items(self, project_anatom_settings):\n        self.log.debug(\"Root items preparation begins.\")\n\n        root_items = []\n        root_items.append({\n            \"type\": \"label\",\n            \"value\": \"<h3>Check your Project root settings</h3>\"\n        })\n        root_items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<p><i>NOTE: Roots are <b>crucial</b> for path filling\"\n                \" (and creating folder structure).</i></p>\"\n            )\n        })\n        root_items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<p><i>WARNING: Do not change roots on running project,\"\n                \" that <b>will cause workflow issues</b>.</i></p>\"\n            )\n        })\n\n        empty_text = \"Enter root path here...\"\n\n        roots_entity = project_anatom_settings[\"roots\"]\n        for root_name, root_entity in roots_entity.items():\n            root_items.append(self.item_splitter)\n            root_items.append({\n                \"type\": \"label\",\n                \"value\": \"Root: \\\"{}\\\"\".format(root_name)\n            })\n            for platform_name, value_entity in root_entity.items():\n                root_items.append({\n                    \"label\": platform_name,\n                    \"name\": \"__root__{}__{}\".format(root_name, platform_name),\n                    \"type\": \"text\",\n                    \"value\": value_entity.value,\n                    \"empty_text\": empty_text\n                })\n\n        root_items.append({\n            \"type\": \"hidden\",\n            \"name\": \"__rootnames__\",\n            \"value\": json.dumps(list(roots_entity.keys()))\n        })\n\n        self.log.debug(\"Root items preparation ended.\")\n        return root_items\n\n    def _attributes_to_set(self, project_anatom_settings):\n        attributes_to_set = {}\n\n        attribute_values_by_key = {}\n        for key, entity in project_anatom_settings[\"attributes\"].items():\n            attribute_values_by_key[key] = entity.value\n\n        cust_attrs, hier_cust_attrs = get_openpype_attr(self.session, True)\n\n        for attr in hier_cust_attrs:\n            key = attr[\"key\"]\n            if key.startswith(\"avalon_\"):\n                continue\n            attributes_to_set[key] = {\n                \"label\": attr[\"label\"],\n                \"object\": attr,\n                \"default\": attribute_values_by_key.get(key)\n            }\n\n        for attr in cust_attrs:\n            if attr[\"entity_type\"].lower() != \"show\":\n                continue\n            key = attr[\"key\"]\n            if key.startswith(\"avalon_\"):\n                continue\n            attributes_to_set[key] = {\n                \"label\": attr[\"label\"],\n                \"object\": attr,\n                \"default\": attribute_values_by_key.get(key)\n            }\n\n        # Sort by label\n        attributes_to_set = dict(sorted(\n            attributes_to_set.items(),\n            key=lambda x: x[1][\"label\"]\n        ))\n        return attributes_to_set\n\n    def prepare_custom_attribute_items(self, project_anatom_settings):\n        items = []\n        multiselect_enumerators = []\n        attributes_to_set = self._attributes_to_set(project_anatom_settings)\n\n        self.log.debug(\"Preparing interface for keys: \\\"{}\\\"\".format(\n            str([key for key in attributes_to_set])\n        ))\n\n        attribute_keys = set(attributes_to_set.keys())\n        keys_order = []\n        for key in self._keys_order:\n            if key in attribute_keys:\n                keys_order.append(key)\n\n        attribute_keys = attribute_keys - set(keys_order)\n        for key in sorted(attribute_keys):\n            keys_order.append(key)\n\n        for key in keys_order:\n            in_data = attributes_to_set[key]\n            attr = in_data[\"object\"]\n\n            # initial item definition\n            item = {\n                \"name\": key,\n                \"label\": in_data[\"label\"]\n            }\n\n            # cust attr type - may have different visualization\n            type_name = attr[\"type\"][\"name\"].lower()\n            easy_types = [\"text\", \"boolean\", \"date\", \"number\"]\n\n            easy_type = False\n            if type_name in easy_types:\n                easy_type = True\n\n            elif type_name == \"enumerator\":\n\n                attr_config = json.loads(attr[\"config\"])\n                attr_config_data = json.loads(attr_config[\"data\"])\n\n                if attr_config[\"multiSelect\"] is True:\n                    multiselect_enumerators.append(self.item_splitter)\n                    multiselect_enumerators.append({\n                        \"type\": \"label\",\n                        \"value\": \"<h3>{}</h3>\".format(in_data[\"label\"])\n                    })\n\n                    default = in_data[\"default\"]\n                    names = []\n                    for option in sorted(\n                        attr_config_data, key=lambda x: x[\"menu\"]\n                    ):\n                        name = option[\"value\"]\n                        new_name = \"__{}__{}\".format(key, name)\n                        names.append(new_name)\n                        item = {\n                            \"name\": new_name,\n                            \"type\": \"boolean\",\n                            \"label\": \"- {}\".format(option[\"menu\"])\n                        }\n                        if default:\n                            if isinstance(default, (list, tuple)):\n                                if name in default:\n                                    item[\"value\"] = True\n                            else:\n                                if name == default:\n                                    item[\"value\"] = True\n\n                        multiselect_enumerators.append(item)\n\n                    multiselect_enumerators.append({\n                        \"type\": \"hidden\",\n                        \"name\": \"__hidden__{}\".format(key),\n                        \"value\": json.dumps(names)\n                    })\n                else:\n                    easy_type = True\n                    item[\"data\"] = attr_config_data\n\n            else:\n                self.log.warning((\n                    \"Custom attribute \\\"{}\\\" has type \\\"{}\\\".\"\n                    \" I don't know how to handle\"\n                ).format(key, type_name))\n                items.append({\n                    \"type\": \"label\",\n                    \"value\": (\n                        \"!!! Can't handle Custom attritubte type \\\"{}\\\"\"\n                        \" (key: \\\"{}\\\")\"\n                    ).format(type_name, key)\n                })\n\n            if easy_type:\n                item[\"type\"] = type_name\n\n                # default value in interface\n                default = in_data[\"default\"]\n                if default is not None:\n                    item[\"value\"] = default\n\n                items.append(item)\n\n        return items, multiselect_enumerators\n\n    def launch(self, session, entities, event):\n        in_data = event[\"data\"].get(\"values\")\n        if not in_data:\n            return\n\n        root_values = {}\n        root_key = \"__root__\"\n        for key in tuple(in_data.keys()):\n            if key.startswith(root_key):\n                _key = key[len(root_key):]\n                root_values[_key] = in_data.pop(key)\n\n        root_names = in_data.pop(\"__rootnames__\", None)\n        root_data = {}\n        for root_name in json.loads(root_names):\n            root_data[root_name] = {}\n            for key, value in tuple(root_values.items()):\n                prefix = \"{}__\".format(root_name)\n                if not key.startswith(prefix):\n                    continue\n\n                _key = key[len(prefix):]\n                root_data[root_name][_key] = value\n\n        # Find hidden items for multiselect enumerators\n        keys_to_process = []\n        for key in in_data:\n            if key.startswith(\"__hidden__\"):\n                keys_to_process.append(key)\n\n        self.log.debug(\"Preparing data for Multiselect Enumerators\")\n        enumerators = {}\n        for key in keys_to_process:\n            new_key = key.replace(\"__hidden__\", \"\")\n            enumerator_items = in_data.pop(key)\n            enumerators[new_key] = json.loads(enumerator_items)\n\n        # find values set for multiselect enumerator\n        for key, enumerator_items in enumerators.items():\n            in_data[key] = []\n\n            name = \"__{}__\".format(key)\n\n            for item in enumerator_items:\n                value = in_data.pop(item)\n                if value is True:\n                    new_key = item.replace(name, \"\")\n                    in_data[key].append(new_key)\n\n        self.log.debug(\"Setting Custom Attribute values\")\n\n        project_entity = entities[0]\n        project_name = project_entity[\"full_name\"]\n\n        # Try to find project document\n        project_doc = get_project(project_name)\n\n        # Create project if is not available\n        # - creation is required to be able set project anatomy and attributes\n        if not project_doc:\n            project_code = project_entity[\"name\"]\n            self.log.info(\"Creating project \\\"{} [{}]\\\"\".format(\n                project_name, project_code\n            ))\n            create_project(project_name, project_code)\n            self.trigger_event(\n                \"openpype.project.created\",\n                {\"project_name\": project_name}\n            )\n\n        project_settings = ProjectSettings(project_name)\n        project_anatomy_settings = project_settings[\"project_anatomy\"]\n        project_anatomy_settings[\"roots\"] = root_data\n\n        custom_attribute_values = {}\n        attributes_entity = project_anatomy_settings[\"attributes\"]\n        for key, value in in_data.items():\n            if key not in attributes_entity:\n                custom_attribute_values[key] = value\n            else:\n                attributes_entity[key] = value\n\n        try:\n            project_settings.save()\n        except SaveWarningExc as exc:\n            self.log.info(\"Few warnings happened during settings save:\")\n            for warning in exc.warnings:\n                self.log.info(str(warning))\n\n        # Change custom attributes on project\n        if custom_attribute_values:\n            for key, value in custom_attribute_values.items():\n                project_entity[\"custom_attributes\"][key] = value\n                self.log.debug(\"- Key \\\"{}\\\" set to \\\"{}\\\"\".format(key, value))\n            session.commit()\n\n        event_data = copy.deepcopy(in_data)\n        event_data[\"project_name\"] = project_name\n        self.trigger_event(\"openpype.project.prepared\", event_data)\n\n        return True\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n    PrepareProjectServer(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/action_private_project_detection.py",
    "content": "from openpype_modules.ftrack.lib import ServerAction\n\n\nclass PrivateProjectDetectionAction(ServerAction):\n    \"\"\"Action helps to identify if does not have access to project.\"\"\"\n\n    identifier = \"server.missing.perm.private.project\"\n    label = \"Missing permissions\"\n    description = (\n        \"Main ftrack event server does not have access to this project.\"\n    )\n\n    def _discover(self, event):\n        \"\"\"Show action only if there is a selection in event data.\"\"\"\n        entities = self._translate_event(event)\n        if entities:\n            return None\n\n        selection = event[\"data\"].get(\"selection\")\n        if not selection:\n            return None\n\n        return {\n            \"items\": [{\n                \"label\": self.label,\n                \"variant\": self.variant,\n                \"description\": self.description,\n                \"actionIdentifier\": self.discover_identifier,\n                \"icon\": self.icon,\n            }]\n        }\n\n    def _launch(self, event):\n        # Ignore if there are values in event data\n        # - somebody clicked on submit button\n        values = event[\"data\"].get(\"values\")\n        if values:\n            return None\n\n        title = \"# Private project (missing permissions) #\"\n        msg = (\n            \"User ({}) or API Key used on Ftrack event server\"\n            \" does not have permissions to access this private project.\"\n        ).format(self.session.api_user)\n        return {\n            \"type\": \"form\",\n            \"title\": \"Missing permissions\",\n            \"items\": [\n                {\"type\": \"label\", \"value\": title},\n                {\"type\": \"label\", \"value\": msg},\n                # Add hidden to be able detect if was clicked on submit\n                {\"type\": \"hidden\", \"value\": \"1\", \"name\": \"hidden\"}\n            ],\n            \"submit_button_label\": \"Got it\"\n        }\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    PrivateProjectDetectionAction(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py",
    "content": "import sys\nimport json\nimport collections\nimport ftrack_api\nfrom openpype_modules.ftrack.lib import (\n    ServerAction,\n    query_custom_attributes\n)\n\n\nclass PushHierValuesToNonHier(ServerAction):\n    \"\"\"Action push hierarchical custom attribute values to non-hierarchical.\n\n    Hierarchical value is also pushed to their task entities.\n\n    Action has 3 configurable attributes:\n    - `role_list`: List of use roles that can discover the action.\n    - `interest_attributes`: Keys of custom attributes that will be looking\n        for to push values. Attribute key must have both custom attribute types\n        hierarchical and on specific object type (entity type).\n    - `interest_entity_types`: Entity types that will be in focus of pushing\n        hierarchical to object type's custom attribute.\n\n    EXAMPLE:\n    * Before action\n    |_ Project\n      |_ Shot1\n        - hierarchical custom attribute value: `frameStart`: 1001\n        - custom attribute for `Shot`: frameStart: 1\n        |_ Task1\n            - hierarchical custom attribute value: `frameStart`: 10\n            - custom attribute for `Task`: frameStart: 0\n\n    * After action\n    |_ Project\n      |_ Shot1\n        - hierarchical custom attribute value: `frameStart`: 1001\n        - custom attribute for `Shot`: frameStart: 1001\n        |_ Task1\n            - hierarchical custom attribute value: `frameStart`: 1001\n            - custom attribute for `Task`: frameStart: 1001\n    \"\"\"\n\n    identifier = \"admin.push_hier_values_to_non_hier\"\n    label = \"OpenPype Admin\"\n    variant = \"- Push Hierarchical values To Non-Hierarchical\"\n\n    entities_query_by_project = (\n        \"select id, parent_id, object_type_id from TypedContext\"\n        \" where project_id is \\\"{}\\\"\"\n    )\n    cust_attrs_query = (\n        \"select id, key, object_type_id, is_hierarchical, default\"\n        \" from CustomAttributeConfiguration\"\n        \" where key in ({})\"\n    )\n\n    # configurable\n    settings_key = \"sync_hier_entity_attributes\"\n    settings_enabled_key = \"action_enabled\"\n\n    def discover(self, session, entities, event):\n        \"\"\" Validation \"\"\"\n        # Check if selection is valid\n        is_valid = False\n        for ent in event[\"data\"][\"selection\"]:\n            # Ignore entities that are not tasks or projects\n            if ent[\"entityType\"].lower() in (\"task\", \"show\"):\n                is_valid = True\n                break\n\n        if is_valid:\n            is_valid = self.valid_roles(session, entities, event)\n        return is_valid\n\n    def launch(self, session, entities, event):\n        self.log.debug(\"{}: Creating job\".format(self.label))\n\n        user_entity = session.query(\n            \"User where id is {}\".format(event[\"source\"][\"user\"][\"id\"])\n        ).one()\n        job = session.create(\"Job\", {\n            \"user\": user_entity,\n            \"status\": \"running\",\n            \"data\": json.dumps({\n                \"description\": \"Propagation of Frame attribute values to task.\"\n            })\n        })\n        session.commit()\n\n        try:\n            result = self.propagate_values(session, event, entities)\n\n        except Exception as exc:\n            msg = \"Pushing Custom attribute values to task Failed\"\n\n            self.log.warning(msg, exc_info=True)\n\n            session.rollback()\n\n            description = \"{} (Download traceback)\".format(msg)\n            self.add_traceback_to_job(\n                job, session, sys.exc_info(), description\n            )\n\n            return {\n                \"success\": False,\n                \"message\": \"Error: {}\".format(str(exc))\n            }\n\n        job[\"status\"] = \"done\"\n        session.commit()\n\n        return result\n\n    def attrs_configurations(self, session, object_ids, interest_attributes):\n        attrs = session.query(self.cust_attrs_query.format(\n            self.join_query_keys(interest_attributes),\n            self.join_query_keys(object_ids)\n        )).all()\n\n        attrs_by_obj_id = collections.defaultdict(list)\n        hiearchical = []\n        for attr in attrs:\n            if attr[\"is_hierarchical\"]:\n                hiearchical.append(attr)\n                continue\n            obj_id = attr[\"object_type_id\"]\n            attrs_by_obj_id[obj_id].append(attr)\n        return attrs_by_obj_id, hiearchical\n\n    def query_attr_value(\n        self,\n        session,\n        hier_attrs,\n        attrs_by_obj_id,\n        dst_object_type_ids,\n        task_entity_ids,\n        non_task_entity_ids,\n        parent_id_by_entity_id\n    ):\n        all_non_task_ids_with_parents = set()\n        for entity_id in non_task_entity_ids:\n            all_non_task_ids_with_parents.add(entity_id)\n            _entity_id = entity_id\n            while True:\n                parent_id = parent_id_by_entity_id.get(_entity_id)\n                if (\n                    parent_id is None\n                    or parent_id in all_non_task_ids_with_parents\n                ):\n                    break\n                all_non_task_ids_with_parents.add(parent_id)\n                _entity_id = parent_id\n\n        all_entity_ids = (\n            set(all_non_task_ids_with_parents)\n            | set(task_entity_ids)\n        )\n        attr_ids = {attr[\"id\"] for attr in hier_attrs}\n        for obj_id in dst_object_type_ids:\n            attrs = attrs_by_obj_id.get(obj_id)\n            if attrs is not None:\n                for attr in attrs:\n                    attr_ids.add(attr[\"id\"])\n\n        real_values_by_entity_id = {\n            entity_id: {}\n            for entity_id in all_entity_ids\n        }\n\n        attr_values = query_custom_attributes(\n            session, attr_ids, all_entity_ids, True\n        )\n        for item in attr_values:\n            entity_id = item[\"entity_id\"]\n            attr_id = item[\"configuration_id\"]\n            real_values_by_entity_id[entity_id][attr_id] = item[\"value\"]\n\n        # Fill hierarchical values\n        hier_attrs_key_by_id = {\n            hier_attr[\"id\"]: hier_attr\n            for hier_attr in hier_attrs\n        }\n        hier_values_per_entity_id = {}\n        for entity_id in all_non_task_ids_with_parents:\n            real_values = real_values_by_entity_id[entity_id]\n            hier_values_per_entity_id[entity_id] = {}\n            for attr_id, attr in hier_attrs_key_by_id.items():\n                key = attr[\"key\"]\n                hier_values_per_entity_id[entity_id][key] = (\n                    real_values.get(attr_id)\n                )\n\n        output = {}\n        for entity_id in non_task_entity_ids:\n            output[entity_id] = {}\n            for attr in hier_attrs_key_by_id.values():\n                key = attr[\"key\"]\n                value = hier_values_per_entity_id[entity_id][key]\n                tried_ids = set()\n                if value is None:\n                    tried_ids.add(entity_id)\n                    _entity_id = entity_id\n                    while value is None:\n                        parent_id = parent_id_by_entity_id.get(_entity_id)\n                        if not parent_id:\n                            break\n                        value = hier_values_per_entity_id[parent_id][key]\n                        if value is not None:\n                            break\n                        _entity_id = parent_id\n                        tried_ids.add(parent_id)\n\n                if value is None:\n                    value = attr[\"default\"]\n\n                if value is not None:\n                    for ent_id in tried_ids:\n                        hier_values_per_entity_id[ent_id][key] = value\n\n                output[entity_id][key] = value\n\n        return real_values_by_entity_id, output\n\n    def propagate_values(self, session, event, selected_entities):\n        ftrack_settings = self.get_ftrack_settings(\n            session, event, selected_entities\n        )\n        action_settings = (\n            ftrack_settings[self.settings_frack_subkey][self.settings_key]\n        )\n\n        project_entity = self.get_project_from_entity(selected_entities[0])\n        selected_ids = [entity[\"id\"] for entity in selected_entities]\n\n        self.log.debug(\"Querying project's entities \\\"{}\\\".\".format(\n            project_entity[\"full_name\"]\n        ))\n        interest_entity_types = tuple(\n            ent_type.lower()\n            for ent_type in action_settings[\"interest_entity_types\"]\n        )\n        all_object_types = session.query(\"ObjectType\").all()\n        object_types_by_low_name = {\n            object_type[\"name\"].lower(): object_type\n            for object_type in all_object_types\n        }\n\n        task_object_type = object_types_by_low_name[\"task\"]\n        dst_object_type_ids = {task_object_type[\"id\"]}\n        for ent_type in interest_entity_types:\n            obj_type = object_types_by_low_name.get(ent_type)\n            if obj_type:\n                dst_object_type_ids.add(obj_type[\"id\"])\n\n        interest_attributes = action_settings[\"interest_attributes\"]\n        # Find custom attributes definitions\n        attrs_by_obj_id, hier_attrs = self.attrs_configurations(\n            session, dst_object_type_ids, interest_attributes\n        )\n        # Filter destination object types if they have any object specific\n        # custom attribute\n        for obj_id in tuple(dst_object_type_ids):\n            if obj_id not in attrs_by_obj_id:\n                dst_object_type_ids.remove(obj_id)\n\n        if not dst_object_type_ids:\n            # TODO report that there are not matching custom attributes\n            return {\n                \"success\": True,\n                \"message\": \"Nothing has changed.\"\n            }\n\n        (\n            parent_id_by_entity_id,\n            filtered_entities\n        ) = self.all_hierarchy_entities(\n            session,\n            selected_ids,\n            project_entity,\n            dst_object_type_ids\n        )\n\n        self.log.debug(\"Preparing whole project hierarchy by ids.\")\n\n        entities_by_obj_id = {\n            obj_id: []\n            for obj_id in dst_object_type_ids\n        }\n\n        self.log.debug(\"Filtering Task entities.\")\n        focus_entity_ids = []\n        non_task_entity_ids = []\n        task_entity_ids = []\n        for entity in filtered_entities:\n            entity_id = entity[\"id\"]\n            focus_entity_ids.append(entity_id)\n            if entity.entity_type.lower() == \"task\":\n                task_entity_ids.append(entity_id)\n            else:\n                non_task_entity_ids.append(entity_id)\n\n            obj_id = entity[\"object_type_id\"]\n            entities_by_obj_id[obj_id].append(entity_id)\n\n        if not non_task_entity_ids:\n            return {\n                \"success\": True,\n                \"message\": \"Nothing to do in your selection.\"\n            }\n\n        self.log.debug(\"Getting Custom attribute values.\")\n        (\n            real_values_by_entity_id,\n            hier_values_by_entity_id\n        ) = self.query_attr_value(\n            session,\n            hier_attrs,\n            attrs_by_obj_id,\n            dst_object_type_ids,\n            task_entity_ids,\n            non_task_entity_ids,\n            parent_id_by_entity_id\n        )\n\n        self.log.debug(\"Setting parents' values to task.\")\n        self.set_task_attr_values(\n            session,\n            hier_attrs,\n            task_entity_ids,\n            hier_values_by_entity_id,\n            parent_id_by_entity_id,\n            real_values_by_entity_id\n        )\n\n        self.log.debug(\"Setting values to entities themselves.\")\n        self.push_values_to_entities(\n            session,\n            entities_by_obj_id,\n            attrs_by_obj_id,\n            hier_values_by_entity_id,\n            real_values_by_entity_id\n        )\n\n        return True\n\n    def all_hierarchy_entities(\n        self,\n        session,\n        selected_ids,\n        project_entity,\n        destination_object_type_ids\n    ):\n        selected_ids = set(selected_ids)\n\n        filtered_entities = []\n        parent_id_by_entity_id = {}\n        # Query is simple if project is in selection\n        if project_entity[\"id\"] in selected_ids:\n            entities = session.query(\n                self.entities_query_by_project.format(project_entity[\"id\"])\n            ).all()\n\n            for entity in entities:\n                if entity[\"object_type_id\"] in destination_object_type_ids:\n                    filtered_entities.append(entity)\n                entity_id = entity[\"id\"]\n                parent_id_by_entity_id[entity_id] = entity[\"parent_id\"]\n            return parent_id_by_entity_id, filtered_entities\n\n        # Query selection and get it's link to be able calculate parentings\n        entities_with_link = session.query((\n            \"select id, parent_id, link, object_type_id\"\n            \" from TypedContext where id in ({})\"\n        ).format(self.join_query_keys(selected_ids))).all()\n\n        # Process and store queried entities and store all lower entities to\n        #   `bottom_ids`\n        # - bottom_ids should not contain 2 ids where one is parent of second\n        bottom_ids = set(selected_ids)\n        for entity in entities_with_link:\n            if entity[\"object_type_id\"] in destination_object_type_ids:\n                filtered_entities.append(entity)\n            children_id = None\n            for idx, item in enumerate(reversed(entity[\"link\"])):\n                item_id = item[\"id\"]\n                if idx > 0 and item_id in bottom_ids:\n                    bottom_ids.remove(item_id)\n\n                if children_id is not None:\n                    parent_id_by_entity_id[children_id] = item_id\n\n                children_id = item_id\n\n        # Query all children of selection per one hierarchy level and process\n        #   their data the same way as selection but parents are already known\n        chunk_size = 100\n        while bottom_ids:\n            child_entities = []\n            # Query entities in chunks\n            entity_ids = list(bottom_ids)\n            for idx in range(0, len(entity_ids), chunk_size):\n                _entity_ids = entity_ids[idx:idx + chunk_size]\n                child_entities.extend(session.query((\n                    \"select id, parent_id, object_type_id from\"\n                    \" TypedContext where parent_id in ({})\"\n                ).format(self.join_query_keys(_entity_ids))).all())\n\n            bottom_ids = set()\n            for entity in child_entities:\n                entity_id = entity[\"id\"]\n                parent_id_by_entity_id[entity_id] = entity[\"parent_id\"]\n                bottom_ids.add(entity_id)\n                if entity[\"object_type_id\"] in destination_object_type_ids:\n                    filtered_entities.append(entity)\n\n        return parent_id_by_entity_id, filtered_entities\n\n    def set_task_attr_values(\n        self,\n        session,\n        hier_attrs,\n        task_entity_ids,\n        hier_values_by_entity_id,\n        parent_id_by_entity_id,\n        real_values_by_entity_id\n    ):\n        hier_attr_id_by_key = {\n            attr[\"key\"]: attr[\"id\"]\n            for attr in hier_attrs\n        }\n        filtered_task_ids = set()\n        for task_id in task_entity_ids:\n            parent_id = parent_id_by_entity_id.get(task_id)\n            parent_values = hier_values_by_entity_id.get(parent_id)\n            if parent_values:\n                filtered_task_ids.add(task_id)\n\n        if not filtered_task_ids:\n            return\n\n        for task_id in filtered_task_ids:\n            parent_id = parent_id_by_entity_id[task_id]\n            parent_values = hier_values_by_entity_id[parent_id]\n            hier_values_by_entity_id[task_id] = {}\n            real_task_attr_values = real_values_by_entity_id[task_id]\n            for key, value in parent_values.items():\n                hier_values_by_entity_id[task_id][key] = value\n                if value is None:\n                    continue\n\n                configuration_id = hier_attr_id_by_key[key]\n                _entity_key = collections.OrderedDict([\n                    (\"configuration_id\", configuration_id),\n                    (\"entity_id\", task_id)\n                ])\n                op = None\n                if configuration_id not in real_task_attr_values:\n                    op = ftrack_api.operation.CreateEntityOperation(\n                        \"CustomAttributeValue\",\n                        _entity_key,\n                        {\"value\": value}\n                    )\n                elif real_task_attr_values[configuration_id] != value:\n                    op = ftrack_api.operation.UpdateEntityOperation(\n                        \"CustomAttributeValue\",\n                        _entity_key,\n                        \"value\",\n                        real_task_attr_values[configuration_id],\n                        value\n                    )\n\n                if op is not None:\n                    session.recorded_operations.push(op)\n                    if len(session.recorded_operations) > 100:\n                        session.commit()\n\n        session.commit()\n\n    def push_values_to_entities(\n        self,\n        session,\n        entities_by_obj_id,\n        attrs_by_obj_id,\n        hier_values_by_entity_id,\n        real_values_by_entity_id\n    ):\n        \"\"\"Push values from hierarchical custom attributes to non-hierarchical.\n\n        Args:\n            session (ftrack_api.Sessison): Session which queried entities,\n                values and which is used for change propagation.\n            entities_by_obj_id (dict[str, list[str]]): TypedContext\n                ftrack entity ids where the attributes are propagated by their\n                object ids.\n            attrs_by_obj_id (dict[str, ftrack_api.Entity]): Objects of\n                'CustomAttributeConfiguration' by their ids.\n            hier_values_by_entity_id (doc[str, dict[str, Any]]): Attribute\n                values by entity id and by their keys.\n            real_values_by_entity_id (doc[str, dict[str, Any]]): Real attribute\n                values of entities.\n        \"\"\"\n\n        for object_id, entity_ids in entities_by_obj_id.items():\n            attrs = attrs_by_obj_id.get(object_id)\n            if not attrs or not entity_ids:\n                continue\n\n            for entity_id in entity_ids:\n                real_values = real_values_by_entity_id.get(entity_id)\n                hier_values = hier_values_by_entity_id.get(entity_id)\n                if hier_values is None:\n                    continue\n\n                for attr in attrs:\n                    attr_id = attr[\"id\"]\n                    attr_key = attr[\"key\"]\n                    value = hier_values.get(attr_key)\n                    if value is None:\n                        continue\n\n                    _entity_key = collections.OrderedDict([\n                        (\"configuration_id\", attr_id),\n                        (\"entity_id\", entity_id)\n                    ])\n\n                    op = None\n                    if attr_id not in real_values:\n                        op = ftrack_api.operation.CreateEntityOperation(\n                            \"CustomAttributeValue\",\n                            _entity_key,\n                            {\"value\": value}\n                        )\n                    elif real_values[attr_id] != value:\n                        op = ftrack_api.operation.UpdateEntityOperation(\n                            \"CustomAttributeValue\",\n                            _entity_key,\n                            \"value\",\n                            real_values[attr_id],\n                            value\n                        )\n\n                    if op is not None:\n                        session.recorded_operations.push(op)\n                        if len(session.recorded_operations) > 100:\n                            session.commit()\n\n        session.commit()\n\n\ndef register(session):\n    PushHierValuesToNonHier(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/action_sync_to_avalon.py",
    "content": "import time\nimport sys\nimport json\n\nimport ftrack_api\n\nfrom openpype_modules.ftrack.lib import ServerAction\nfrom openpype_modules.ftrack.lib.avalon_sync import SyncEntitiesFactory\n\n\nclass SyncToAvalonServer(ServerAction):\n    \"\"\"\n    Synchronizing data action - from Ftrack to Avalon DB\n\n    Stores all information about entity.\n    - Name(string) - Most important information = identifier of entity\n    - Parent(ObjectId) - Avalon Project Id, if entity is not project itself\n    - Data(dictionary):\n        - VisualParent(ObjectId) - Avalon Id of parent asset\n        - Parents(array of string) - All parent names except project\n        - Tasks(dictionary of dictionaries) - Tasks on asset\n        - FtrackId(string)\n        - entityType(string) - entity's type on Ftrack\n        * All Custom attributes in group 'Avalon'\n            - custom attributes that start with 'avalon_' are skipped\n\n    * These information are stored for entities in whole project.\n\n    Avalon ID of asset is stored to Ftrack\n        - Custom attribute 'avalon_mongo_id'.\n    - action IS NOT creating this Custom attribute if doesn't exist\n        - run 'Create Custom Attributes' action\n        - or do it manually (Not recommended)\n    \"\"\"\n    #: Action identifier.\n    identifier = \"sync.to.avalon.server\"\n    #: Action label.\n    label = \"OpenPype Admin\"\n    variant = \"- Sync To Avalon (Server)\"\n    #: Action description.\n    description = \"Send data from Ftrack to Avalon\"\n    role_list = {\"Pypeclub\", \"Administrator\", \"Project Manager\"}\n    settings_key = \"sync_to_avalon\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.entities_factory = SyncEntitiesFactory(self.log, self.session)\n\n    def discover(self, session, entities, event):\n        \"\"\" Validation \"\"\"\n        # Check if selection is valid\n        is_valid = False\n        for ent in event[\"data\"][\"selection\"]:\n            # Ignore entities that are not tasks or projects\n            if ent[\"entityType\"].lower() in [\"show\", \"task\"]:\n                is_valid = True\n                break\n\n        if is_valid:\n            is_valid = self.valid_roles(session, entities, event)\n        return is_valid\n\n    def launch(self, session, in_entities, event):\n        self.log.debug(\"{}: Creating job\".format(self.label))\n\n        user_entity = session.query(\n            \"User where id is {}\".format(event[\"source\"][\"user\"][\"id\"])\n        ).one()\n        job_entity = session.create(\"Job\", {\n            \"user\": user_entity,\n            \"status\": \"running\",\n            \"data\": json.dumps({\n                \"description\": \"Sync to avalon is running...\"\n            })\n        })\n        session.commit()\n\n        project_entity = self.get_project_from_entity(in_entities[0])\n        project_name = project_entity[\"full_name\"]\n\n        try:\n            result = self.synchronization(event, project_name)\n\n        except Exception:\n            self.log.error(\n                \"Synchronization failed due to code error\", exc_info=True\n            )\n\n            description = \"Sync to avalon Crashed (Download traceback)\"\n            self.add_traceback_to_job(\n                job_entity, session, sys.exc_info(), description\n            )\n\n            msg = \"An error has happened during synchronization\"\n            title = \"Synchronization report ({}):\".format(project_name)\n            items = []\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"# {}\".format(msg)\n            })\n            items.append({\n                \"type\": \"label\",\n                \"value\": (\n                    \"<p>Download report from job for more information.</p>\"\n                )\n            })\n\n            report = {}\n            try:\n                report = self.entities_factory.report()\n            except Exception:\n                pass\n\n            _items = report.get(\"items\") or []\n            if _items:\n                items.append(self.entities_factory.report_splitter)\n                items.extend(_items)\n\n            self.show_interface(items, title, event, submit_btn_label=\"Ok\")\n\n            return {\"success\": True, \"message\": msg}\n\n        job_entity[\"status\"] = \"done\"\n        job_entity[\"data\"] = json.dumps({\n            \"description\": \"Sync to avalon finished.\"\n        })\n        session.commit()\n\n        return result\n\n    def synchronization(self, event, project_name):\n        time_start = time.time()\n\n        self.show_message(event, \"Synchronization - Preparing data\", True)\n\n        try:\n            output = self.entities_factory.launch_setup(project_name)\n            if output is not None:\n                return output\n\n            time_1 = time.time()\n\n            self.entities_factory.set_cutom_attributes()\n            time_2 = time.time()\n\n            # This must happen before all filtering!!!\n            self.entities_factory.prepare_avalon_entities(project_name)\n            time_3 = time.time()\n\n            self.entities_factory.filter_by_ignore_sync()\n            time_4 = time.time()\n\n            self.entities_factory.duplicity_regex_check()\n            time_5 = time.time()\n\n            self.entities_factory.prepare_ftrack_ent_data()\n            time_6 = time.time()\n\n            self.entities_factory.synchronize()\n            time_7 = time.time()\n\n            self.log.debug(\n                \"*** Synchronization finished ***\"\n            )\n            self.log.debug(\n                \"preparation <{}>\".format(time_1 - time_start)\n            )\n            self.log.debug(\n                \"set_cutom_attributes <{}>\".format(time_2 - time_1)\n            )\n            self.log.debug(\n                \"prepare_avalon_entities <{}>\".format(time_3 - time_2)\n            )\n            self.log.debug(\n                \"filter_by_ignore_sync <{}>\".format(time_4 - time_3)\n            )\n            self.log.debug(\n                \"duplicity_regex_check <{}>\".format(time_5 - time_4)\n            )\n            self.log.debug(\n                \"prepare_ftrack_ent_data <{}>\".format(time_6 - time_5)\n            )\n            self.log.debug(\n                \"synchronize <{}>\".format(time_7 - time_6)\n            )\n            self.log.debug(\n                \"* Total time: {}\".format(time_7 - time_start)\n            )\n\n            if self.entities_factory.project_created:\n                event = ftrack_api.event.base.Event(\n                    topic=\"openpype.project.created\",\n                    data={\"project_name\": project_name}\n                )\n                self.session.event_hub.publish(event)\n\n            report = self.entities_factory.report()\n            if report and report.get(\"items\"):\n                default_title = \"Synchronization report ({}):\".format(\n                    project_name\n                )\n                self.show_interface(\n                    items=report[\"items\"],\n                    title=report.get(\"title\", default_title),\n                    event=event\n                )\n            return {\n                \"success\": True,\n                \"message\": \"Synchronization Finished\"\n            }\n\n        finally:\n            try:\n                self.entities_factory.dbcon.uninstall()\n            except Exception:\n                pass\n\n            try:\n                self.entities_factory.session.close()\n            except Exception:\n                pass\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n    SyncToAvalonServer(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py",
    "content": "import copy\nimport json\nimport collections\n\nimport ftrack_api\n\nfrom openpype_modules.ftrack.lib import (\n    ServerAction,\n    statics_icon,\n)\nfrom openpype_modules.ftrack.lib.avalon_sync import create_chunks\n\n\nclass TransferHierarchicalValues(ServerAction):\n    \"\"\"Transfer values across hierarchical attributes.\n\n    Aalso gives ability to convert types meanwhile. That is limited to\n    conversions between numbers and strings\n    - int <-> float\n    - in, float -> string\n    \"\"\"\n\n    identifier = \"transfer.hierarchical.values\"\n    label = \"OpenPype Admin\"\n    variant = \"- Transfer values between 2 custom attributes\"\n    description = (\n        \"Move values from a hierarchical attribute to\"\n        \" second hierarchical attribute.\"\n    )\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"OpenPypeAdmin.svg\")\n\n    all_project_entities_query = (\n        \"select id, name, parent_id, link\"\n        \" from TypedContext where project_id is \\\"{}\\\"\"\n    )\n    cust_attr_query = (\n        \"select value, entity_id from CustomAttributeValue\"\n        \" where entity_id in ({}) and configuration_id is \\\"{}\\\"\"\n    )\n    settings_key = \"transfer_values_of_hierarchical_attributes\"\n\n    def discover(self, session, entities, event):\n        \"\"\"Show anywhere.\"\"\"\n\n        return self.valid_roles(session, entities, event)\n\n    def _selection_interface(self, session, event_values=None):\n        title = \"Transfer hierarchical values\"\n\n        attr_confs = session.query(\n            (\n                \"select id, key from CustomAttributeConfiguration\"\n                \" where is_hierarchical is true\"\n            )\n        ).all()\n        attr_items = []\n        for attr_conf in attr_confs:\n            attr_items.append({\n                \"value\": attr_conf[\"id\"],\n                \"label\": attr_conf[\"key\"]\n            })\n\n        if len(attr_items) < 2:\n            return {\n                \"title\": title,\n                \"items\": [{\n                    \"type\": \"label\",\n                    \"value\": (\n                        \"Didn't find custom attributes\"\n                        \" that can be transferred.\"\n                    )\n                }]\n            }\n\n        attr_items = sorted(attr_items, key=lambda item: item[\"label\"])\n        items = []\n        item_splitter = {\"type\": \"label\", \"value\": \"---\"}\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<h2>Please select source and destination\"\n                \" Custom attribute</h2>\"\n            )\n        })\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<b>WARNING:</b> This will take affect for all projects!\"\n            )\n        })\n        if event_values:\n            items.append({\n                \"type\": \"label\",\n                \"value\": (\n                    \"<b>Note:</b> Please select 2 different custom attributes.\"\n                )\n            })\n\n        items.append(item_splitter)\n\n        src_item = {\n            \"type\": \"enumerator\",\n            \"label\": \"Source\",\n            \"name\": \"src_attr_id\",\n            \"data\": copy.deepcopy(attr_items)\n        }\n        dst_item = {\n            \"type\": \"enumerator\",\n            \"label\": \"Destination\",\n            \"name\": \"dst_attr_id\",\n            \"data\": copy.deepcopy(attr_items)\n        }\n        delete_item = {\n            \"type\": \"boolean\",\n            \"name\": \"delete_dst_attr_first\",\n            \"label\": \"Delete first\",\n            \"value\": False\n        }\n        if event_values:\n            src_item[\"value\"] = event_values[\"src_attr_id\"]\n            dst_item[\"value\"] = event_values[\"dst_attr_id\"]\n            delete_item[\"value\"] = event_values[\"delete_dst_attr_first\"]\n\n        items.append(src_item)\n        items.append(dst_item)\n        items.append(item_splitter)\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<b>WARNING:</b> All values from destination\"\n                \" Custom Attribute will be removed if this is enabled.\"\n            )\n        })\n        items.append(delete_item)\n\n        return {\n            \"title\": title,\n            \"items\": items\n        }\n\n    def interface(self, session, entities, event):\n        if event[\"data\"].get(\"values\", {}):\n            return None\n\n        return self._selection_interface(session)\n\n    def launch(self, session, entities, event):\n        values = event[\"data\"].get(\"values\", {})\n        if not values:\n            return None\n        src_attr_id = values[\"src_attr_id\"]\n        dst_attr_id = values[\"dst_attr_id\"]\n        delete_dst_values = values[\"delete_dst_attr_first\"]\n\n        if not src_attr_id or not dst_attr_id:\n            self.log.info(\"Attributes were not filled. Nothing to do.\")\n            return {\n                \"success\": True,\n                \"message\": \"Nothing to do\"\n            }\n\n        if src_attr_id == dst_attr_id:\n            self.log.info((\n                \"Same attributes were selected {}, {}.\"\n                \" Showing interface again.\"\n            ).format(src_attr_id, dst_attr_id))\n            return self._selection_interface(session, values)\n\n        # Query custom attrbutes\n        src_conf = session.query((\n            \"select id from CustomAttributeConfiguration where id is {}\"\n        ).format(src_attr_id)).one()\n        dst_conf = session.query((\n            \"select id from CustomAttributeConfiguration where id is {}\"\n        ).format(dst_attr_id)).one()\n        src_type_name = src_conf[\"type\"][\"name\"]\n        dst_type_name = dst_conf[\"type\"][\"name\"]\n        # Limit conversion to\n        # - same type -> same type (there is no need to do conversion)\n        # - number <Any> -> number <Any> (int to float and back)\n        # - number <Any> -> str (any number can be converted to str)\n        src_type = None\n        dst_type = None\n        if src_type_name == \"number\" or src_type_name != dst_type_name:\n            src_type = self._get_attr_type(dst_conf)\n            dst_type = self._get_attr_type(dst_conf)\n            valid = False\n            # Can convert numbers\n            if src_type in (int, float) and dst_type in (int, float):\n                valid = True\n            # Can convert numbers to string\n            elif dst_type is str:\n                valid = True\n\n            if not valid:\n                self.log.info((\n                    \"Don't know how to properly convert\"\n                    \" custom attribute types {} > {}\"\n                ).format(src_type_name, dst_type_name))\n                return {\n                    \"message\": (\n                        \"Don't know how to properly convert\"\n                        \" custom attribute types {} > {}\"\n                    ).format(src_type_name, dst_type_name),\n                    \"success\": False\n                }\n\n        # Query source values\n        src_attr_values = session.query(\n            (\n                \"select value, entity_id\"\n                \" from CustomAttributeValue\"\n                \" where configuration_id is {}\"\n            ).format(src_attr_id)\n        ).all()\n\n        self.log.debug(\"Queried source values.\")\n        failed_entity_ids = []\n        if dst_type is not None:\n            self.log.debug(\"Converting source values to desctination type\")\n            value_by_id = {}\n            for attr_value in src_attr_values:\n                entity_id = attr_value[\"entity_id\"]\n                value = attr_value[\"value\"]\n                if value is not None:\n                    try:\n                        if dst_type is not None:\n                            value = dst_type(value)\n                        value_by_id[entity_id] = value\n                    except Exception:\n                        failed_entity_ids.append(entity_id)\n\n        if failed_entity_ids:\n            self.log.info(\n                \"Couldn't convert some values to destination attribute\"\n            )\n            return {\n                \"success\": False,\n                \"message\": (\n                    \"Couldn't convert some values to destination attribute\"\n                )\n            }\n\n        # Delete destination custom attributes first\n        if delete_dst_values:\n            self.log.info(\"Deleting destination custom attribute values first\")\n            self._delete_custom_attribute_values(session, dst_attr_id)\n\n        self.log.info(\"Applying source values on destination custom attribute\")\n        self._apply_values(session, value_by_id, dst_attr_id)\n        return True\n\n    def _delete_custom_attribute_values(self, session, dst_attr_id):\n        dst_attr_values = session.query(\n            (\n                \"select configuration_id, entity_id\"\n                \" from CustomAttributeValue\"\n                \" where configuration_id is {}\"\n            ).format(dst_attr_id)\n        ).all()\n        delete_operations = []\n        for attr_value in dst_attr_values:\n            entity_id = attr_value[\"entity_id\"]\n            configuration_id = attr_value[\"configuration_id\"]\n            entity_key = collections.OrderedDict((\n                (\"configuration_id\", configuration_id),\n                (\"entity_id\", entity_id)\n            ))\n            delete_operations.append(\n                ftrack_api.operation.DeleteEntityOperation(\n                    \"CustomAttributeValue\",\n                    entity_key\n                )\n            )\n\n        if not delete_operations:\n            return\n\n        for chunk in create_chunks(delete_operations, 500):\n            for operation in chunk:\n                session.recorded_operations.push(operation)\n            session.commit()\n\n    def _apply_values(self, session, value_by_id, dst_attr_id):\n        dst_attr_values = session.query(\n            (\n                \"select configuration_id, entity_id\"\n                \" from CustomAttributeValue\"\n                \" where configuration_id is {}\"\n            ).format(dst_attr_id)\n        ).all()\n\n        dst_entity_ids_with_value = {\n            item[\"entity_id\"]\n            for item in dst_attr_values\n        }\n        operations = []\n        for entity_id, value in value_by_id.items():\n            entity_key = collections.OrderedDict((\n                (\"configuration_id\", dst_attr_id),\n                (\"entity_id\", entity_id)\n            ))\n            if entity_id in dst_entity_ids_with_value:\n                operations.append(\n                    ftrack_api.operation.UpdateEntityOperation(\n                        \"CustomAttributeValue\",\n                        entity_key,\n                        \"value\",\n                        ftrack_api.symbol.NOT_SET,\n                        value\n                    )\n                )\n            else:\n                operations.append(\n                    ftrack_api.operation.CreateEntityOperation(\n                        \"CustomAttributeValue\",\n                        entity_key,\n                        {\"value\": value}\n                    )\n                )\n\n        if not operations:\n            return\n\n        for chunk in create_chunks(operations, 500):\n            for operation in chunk:\n                session.recorded_operations.push(operation)\n            session.commit()\n\n    def _get_attr_type(self, conf_def):\n        type_name = conf_def[\"type\"][\"name\"]\n        if type_name == \"text\":\n            return str\n\n        if type_name == \"number\":\n            config = json.loads(conf_def[\"config\"])\n            if config[\"isdecimal\"]:\n                return float\n            return int\n        return None\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    TransferHierarchicalValues(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/event_del_avalon_id_from_new.py",
    "content": "from openpype_modules.ftrack.lib import BaseEvent\nfrom openpype_modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY\nfrom openpype_modules.ftrack.event_handlers_server.event_sync_to_avalon import (\n    SyncToAvalonEvent\n)\n\n\nclass DelAvalonIdFromNew(BaseEvent):\n    '''\n    This event removes AvalonId from custom attributes of new entities\n    Result:\n    - 'Copy->Pasted' entities won't have same AvalonID as source entity\n\n    Priority of this event must be less than SyncToAvalon event\n    '''\n    priority = SyncToAvalonEvent.priority - 1\n    ignore_me = True\n\n    def launch(self, session, event):\n        created = []\n        entities = event['data']['entities']\n        for entity in entities:\n            try:\n                entity_id = entity['entityId']\n\n                if entity.get('action', None) == 'add':\n                    id_dict = entity['changes']['id']\n\n                    if id_dict['new'] is not None and id_dict['old'] is None:\n                        created.append(id_dict['new'])\n\n                elif (\n                    entity.get('action', None) == 'update' and\n                    CUST_ATTR_ID_KEY in entity['keys'] and\n                    entity_id in created\n                ):\n                    ftrack_entity = session.get(\n                        self._get_entity_type(entity),\n                        entity_id\n                    )\n\n                    cust_attrs = ftrack_entity[\"custom_attributes\"]\n                    if cust_attrs[CUST_ATTR_ID_KEY]:\n                        cust_attrs[CUST_ATTR_ID_KEY] = \"\"\n                        session.commit()\n\n            except Exception:\n                session.rollback()\n                continue\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n    DelAvalonIdFromNew(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/event_first_version_status.py",
    "content": "import collections\n\nfrom openpype.client import get_project\nfrom openpype_modules.ftrack.lib import BaseEvent\n\n\nclass FirstVersionStatus(BaseEvent):\n\n    # WARNING Priority MUST be higher\n    # than handler in `event_version_to_task_statuses.py`\n    priority = 200\n\n    keys_enum = [\"task\", \"task_type\"]\n    # This should be set with presets\n    task_status_map = []\n\n    # EXAMPLE of `task_status_map`\n    __example_status_map__ = [{\n        # `key` specify where to look for name (is enumerator of `keys_enum`)\n        # By default is set to \"task\"\n        \"key\": \"task\",\n        # speicification of name\n        \"name\": \"compositing\",\n        # Status to set to the asset version\n        \"status\": \"Blocking\"\n    }]\n\n    def register(self, *args, **kwargs):\n        result = super(FirstVersionStatus, self).register(*args, **kwargs)\n\n        valid_task_status_map = []\n        for item in self.task_status_map:\n            key = (item.get(\"key\") or \"task\").lower()\n            name = (item.get(\"name\") or \"\").lower()\n            status = (item.get(\"status\") or \"\").lower()\n            if not (key and name and status):\n                self.log.warning((\n                    \"Invalid item in Task -> Status mapping. {}\"\n                ).format(str(item)))\n                continue\n\n            if key not in self.keys_enum:\n                expected_msg = \"\"\n                last_key_idx = len(self.keys_enum) - 1\n                for idx, key in enumerate(self.keys_enum):\n                    if idx == 0:\n                        joining_part = \"`{}`\"\n                    elif idx == last_key_idx:\n                        joining_part = \"or `{}`\"\n                    else:\n                        joining_part = \", `{}`\"\n                    expected_msg += joining_part.format(key)\n\n                self.log.warning((\n                    \"Invalid key `{}`. Expected: {}.\"\n                ).format(key, expected_msg))\n                continue\n\n            valid_task_status_map.append({\n                \"key\": key,\n                \"name\": name,\n                \"status\": status\n            })\n\n        self.task_status_map = valid_task_status_map\n        if not self.task_status_map:\n            self.log.warning((\n                \"Event handler `{}` don't have set presets.\"\n            ).format(self.__class__.__name__))\n\n        return result\n\n    def launch(self, session, event):\n        \"\"\"Set task's status for first created Asset Version.\"\"\"\n\n        if not self.task_status_map:\n            return\n\n        filtered_entities_info = self.filter_entities_info(event)\n        if not filtered_entities_info:\n            return\n\n        for project_id, entities_info in filtered_entities_info.items():\n            self.process_by_project(session, event, project_id, entities_info)\n\n    def process_by_project(self, session, event, project_id, entities_info):\n        project_name = self.get_project_name_from_event(\n            session, event, project_id\n        )\n        if get_project(project_name) is None:\n            self.log.debug(\n                f\"Project '{project_name}' not found in OpenPype. Skipping\"\n            )\n            return\n\n        entity_ids = []\n        for entity_info in entities_info:\n            entity_ids.append(entity_info[\"entityId\"])\n\n        joined_entity_ids = \",\".join(\n            [\"\\\"{}\\\"\".format(entity_id) for entity_id in entity_ids]\n        )\n        asset_versions = session.query(\n            \"AssetVersion where id in ({})\".format(joined_entity_ids)\n        ).all()\n\n        asset_version_statuses = None\n\n        project_schema = None\n        for asset_version in asset_versions:\n            task_entity = asset_version[\"task\"]\n            found_item = None\n            for item in self.task_status_map:\n                if (\n                    item[\"key\"] == \"task\" and\n                    task_entity[\"name\"].lower() != item[\"name\"]\n                ):\n                    continue\n\n                elif (\n                    item[\"key\"] == \"task_type\" and\n                    task_entity[\"type\"][\"name\"].lower() != item[\"name\"]\n                ):\n                    continue\n\n                found_item = item\n                break\n\n            if not found_item:\n                continue\n\n            if project_schema is None:\n                project_schema = task_entity[\"project\"][\"project_schema\"]\n\n            # Get all available statuses for Task\n            if asset_version_statuses is None:\n                statuses = project_schema.get_statuses(\"AssetVersion\")\n\n                # map lowered status name with it's object\n                asset_version_statuses = {\n                    status[\"name\"].lower(): status for status in statuses\n                }\n\n            ent_path = \"/\".join(\n                [ent[\"name\"] for ent in task_entity[\"link\"]] +\n                [\n                    str(asset_version[\"asset\"][\"name\"]),\n                    str(asset_version[\"version\"])\n                ]\n            )\n\n            new_status = asset_version_statuses.get(found_item[\"status\"])\n            if not new_status:\n                self.log.warning((\n                    \"AssetVersion doesn't have status `{}`.\"\n                ).format(found_item[\"status\"]))\n                continue\n\n            try:\n                asset_version[\"status\"] = new_status\n                session.commit()\n                self.log.debug(\"[ {} ] Status updated to [ {} ]\".format(\n                    ent_path, new_status['name']\n                ))\n\n            except Exception:\n                session.rollback()\n                self.log.warning(\n                    \"[ {} ] Status couldn't be set.\".format(ent_path),\n                    exc_info=True\n                )\n\n    def filter_entities_info(self, event):\n        filtered_entities_info = collections.defaultdict(list)\n        for entity_info in event[\"data\"].get(\"entities\", []):\n            # Care only about add actions\n            if entity_info.get(\"action\") != \"add\":\n                continue\n\n            # Filter AssetVersions\n            if entity_info[\"entityType\"] != \"assetversion\":\n                continue\n\n            entity_changes = entity_info.get(\"changes\") or {}\n\n            # Check if version of Asset Version is `1`\n            version_num = entity_changes.get(\"version\", {}).get(\"new\")\n            if version_num != 1:\n                continue\n\n            # Skip in Asset Version don't have task\n            task_id = entity_changes.get(\"taskid\", {}).get(\"new\")\n            if not task_id:\n                continue\n\n            project_id = None\n            for parent_item in reversed(entity_info[\"parents\"]):\n                if parent_item[\"entityType\"] == \"show\":\n                    project_id = parent_item[\"entityId\"]\n                    break\n\n            if project_id is None:\n                continue\n\n            filtered_entities_info[project_id].append(entity_info)\n\n        return filtered_entities_info\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    FirstVersionStatus(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/event_next_task_update.py",
    "content": "import collections\n\nfrom openpype.client import get_project\nfrom openpype_modules.ftrack.lib import BaseEvent\n\n\nclass NextTaskUpdate(BaseEvent):\n    \"\"\"Change status on following Task.\n\n    Handler cares about changes of status id on Task entities. When new status\n    has state \"Done\" it will try to find following task and change it's status.\n    It is expected following task should be marked as \"Ready to work on\".\n\n    By default all tasks with same task type must have state \"Done\" to do any\n    changes. And when all tasks with same task type are \"done\" it will change\n    statuses on all tasks with next task type.\n\n    # Enable\n    Handler is based on settings, handler can be turned on/off with \"enabled\"\n    key.\n    ```\n    \"enabled\": True\n    ```\n\n    # Status mappings\n    Must have set mappings of new statuses:\n    ```\n    \"mapping\": {\n        # From -> To\n        \"Not Ready\": \"Ready\",\n        ...\n    }\n    ```\n\n    If current status name is not found then status change is skipped.\n\n    # Ignored statuses\n    These status names are skipping as they would be in \"Done\" state. Best\n    example is status \"Omitted\" which in most of cases is \"Blocked\" state but\n    it will never change.\n    ```\n    \"ignored_statuses\": [\n        \"Omitted\",\n        ...\n    ]\n    ```\n\n    # Change statuses sorted by task type and by name\n    Change behaviour of task type batching. Statuses are not checked and set\n    by batches of tasks by Task type but one by one. Tasks are sorted by\n    Task type and then by name if all previous tasks are \"Done\" the following\n    will change status.\n    ```\n    \"name_sorting\": True\n    ```\n    \"\"\"\n    settings_key = \"next_task_update\"\n\n    def launch(self, session, event):\n        '''Propagates status from version to task when changed'''\n\n        filtered_entities_info = self.filter_entities_info(event)\n        if not filtered_entities_info:\n            return\n\n        for project_id, entities_info in filtered_entities_info.items():\n            self.process_by_project(session, event, project_id, entities_info)\n\n    def filter_entities_info(self, event):\n        # Filter if event contain relevant data\n        entities_info = event[\"data\"].get(\"entities\")\n        if not entities_info:\n            return\n\n        filtered_entities_info = collections.defaultdict(list)\n        for entity_info in entities_info:\n            # Care only about Task `entity_type`\n            if entity_info.get(\"entity_type\") != \"Task\":\n                continue\n\n            # Care only about changes of status\n            changes = entity_info.get(\"changes\") or {}\n            statusid_changes = changes.get(\"statusid\") or {}\n            if (\n                statusid_changes.get(\"new\") is None\n                or statusid_changes.get(\"old\") is None\n            ):\n                continue\n\n            project_id = None\n            for parent_info in reversed(entity_info[\"parents\"]):\n                if parent_info[\"entityType\"] == \"show\":\n                    project_id = parent_info[\"entityId\"]\n                    break\n\n            if project_id:\n                filtered_entities_info[project_id].append(entity_info)\n        return filtered_entities_info\n\n    def process_by_project(self, session, event, project_id, _entities_info):\n        project_name = self.get_project_name_from_event(\n            session, event, project_id\n        )\n        if get_project(project_name) is None:\n            self.log.debug(\"Project not found in OpenPype. Skipping\")\n            return\n\n        # Load settings\n        project_settings = self.get_project_settings_from_event(\n            event, project_name\n        )\n\n        # Load status mapping from presets\n        event_settings = (\n            project_settings[\"ftrack\"][\"events\"][self.settings_key]\n        )\n        if not event_settings[\"enabled\"]:\n            self.log.debug(\"Project \\\"{}\\\" has disabled {}.\".format(\n                project_name, self.__class__.__name__\n            ))\n            return\n\n        statuses = session.query(\"Status\").all()\n\n        entities_info = self.filter_by_status_state(_entities_info, statuses)\n        if not entities_info:\n            return\n\n        parent_ids = set()\n        event_task_ids_by_parent_id = collections.defaultdict(list)\n        for entity_info in entities_info:\n            parent_id = entity_info[\"parentId\"]\n            entity_id = entity_info[\"entityId\"]\n            parent_ids.add(parent_id)\n            event_task_ids_by_parent_id[parent_id].append(entity_id)\n\n        # From now it doesn't matter what was in event data\n        task_entities = session.query(\n            (\n                \"select id, type_id, status_id, parent_id, link from Task\"\n                \" where parent_id in ({})\"\n            ).format(self.join_query_keys(parent_ids))\n        ).all()\n\n        tasks_by_parent_id = collections.defaultdict(list)\n        for task_entity in task_entities:\n            tasks_by_parent_id[task_entity[\"parent_id\"]].append(task_entity)\n\n        project_entity = session.get(\"Project\", project_id)\n        self.set_next_task_statuses(\n            session,\n            tasks_by_parent_id,\n            event_task_ids_by_parent_id,\n            statuses,\n            project_entity,\n            event_settings\n        )\n\n    def filter_by_status_state(self, entities_info, statuses):\n        statuses_by_id = {\n            status[\"id\"]: status\n            for status in statuses\n        }\n\n        # Care only about tasks having status with state `Done`\n        filtered_entities_info = []\n        for entity_info in entities_info:\n            status_id = entity_info[\"changes\"][\"statusid\"][\"new\"]\n            status_entity = statuses_by_id[status_id]\n            if status_entity[\"state\"][\"name\"].lower() == \"done\":\n                filtered_entities_info.append(entity_info)\n        return filtered_entities_info\n\n    def set_next_task_statuses(\n        self,\n        session,\n        tasks_by_parent_id,\n        event_task_ids_by_parent_id,\n        statuses,\n        project_entity,\n        event_settings\n    ):\n        statuses_by_id = {\n            status[\"id\"]: status\n            for status in statuses\n        }\n\n        # Lower ignored statuses\n        ignored_statuses = set(\n            status_name.lower()\n            for status_name in event_settings[\"ignored_statuses\"]\n        )\n        # Lower both key and value of mapped statuses\n        mapping = {\n            status_from.lower(): status_to.lower()\n            for status_from, status_to in event_settings[\"mapping\"].items()\n        }\n        # Should use name sorting or not\n        name_sorting = event_settings[\"name_sorting\"]\n\n        # Collect task type ids from changed entities\n        task_type_ids = set()\n        for task_entities in tasks_by_parent_id.values():\n            for task_entity in task_entities:\n                task_type_ids.add(task_entity[\"type_id\"])\n\n        statusese_by_obj_id = self.statuses_for_tasks(\n            task_type_ids, project_entity\n        )\n\n        sorted_task_type_ids = self.get_sorted_task_type_ids(session)\n\n        for parent_id, _task_entities in tasks_by_parent_id.items():\n            task_entities_by_type_id = collections.defaultdict(list)\n            for _task_entity in _task_entities:\n                type_id = _task_entity[\"type_id\"]\n                task_entities_by_type_id[type_id].append(_task_entity)\n\n            event_ids = set(event_task_ids_by_parent_id[parent_id])\n            if name_sorting:\n                # Sort entities by name\n                self.sort_by_name_task_entities_by_type(\n                    task_entities_by_type_id\n                )\n                # Sort entities by type id\n                sorted_task_entities = []\n                for type_id in sorted_task_type_ids:\n                    task_entities = task_entities_by_type_id.get(type_id)\n                    if task_entities:\n                        sorted_task_entities.extend(task_entities)\n\n                next_tasks = self.next_tasks_with_name_sorting(\n                    sorted_task_entities,\n                    event_ids,\n                    statuses_by_id,\n                    ignored_statuses\n                )\n\n            else:\n                next_tasks = self.next_tasks_with_type_sorting(\n                    task_entities_by_type_id,\n                    sorted_task_type_ids,\n                    event_ids,\n                    statuses_by_id,\n                    ignored_statuses\n                )\n\n        for task_entity in next_tasks:\n            if task_entity[\"status\"][\"state\"][\"name\"].lower() == \"done\":\n                continue\n\n            task_status = statuses_by_id[task_entity[\"status_id\"]]\n            old_status_name = task_status[\"name\"].lower()\n            if old_status_name in ignored_statuses:\n                continue\n\n            new_task_name = mapping.get(old_status_name)\n            if not new_task_name:\n                self.log.debug(\n                    \"Didn't find mapping for status \\\"{}\\\".\".format(\n                        task_status[\"name\"]\n                    )\n                )\n                continue\n\n            ent_path = \"/\".join(\n                [ent[\"name\"] for ent in task_entity[\"link\"]]\n            )\n            type_id = task_entity[\"type_id\"]\n            new_status = statusese_by_obj_id[type_id].get(new_task_name)\n            if new_status is None:\n                self.log.warning((\n                    \"\\\"{}\\\" does not have available status name \\\"{}\\\"\"\n                ).format(ent_path, new_task_name))\n                continue\n\n            try:\n                task_entity[\"status_id\"] = new_status[\"id\"]\n                session.commit()\n                self.log.info(\n                    \"\\\"{}\\\" updated status to \\\"{}\\\"\".format(\n                        ent_path, new_status[\"name\"]\n                    )\n                )\n            except Exception:\n                session.rollback()\n                self.log.warning(\n                    \"\\\"{}\\\" status couldn't be set to \\\"{}\\\"\".format(\n                        ent_path, new_status[\"name\"]\n                    ),\n                    exc_info=True\n                )\n\n    def next_tasks_with_name_sorting(\n        self,\n        sorted_task_entities,\n        event_ids,\n        statuses_by_id,\n        ignored_statuses,\n    ):\n        # Pre sort task entities by name\n        use_next_task = False\n        next_tasks = []\n        for task_entity in sorted_task_entities:\n            if task_entity[\"id\"] in event_ids:\n                event_ids.remove(task_entity[\"id\"])\n                use_next_task = True\n                continue\n\n            if not use_next_task:\n                continue\n\n            task_status = statuses_by_id[task_entity[\"status_id\"]]\n            low_status_name = task_status[\"name\"].lower()\n            if low_status_name in ignored_statuses:\n                continue\n\n            next_tasks.append(task_entity)\n            use_next_task = False\n            if not event_ids:\n                break\n\n        return next_tasks\n\n    def check_statuses_done(\n        self, task_entities, ignored_statuses, statuses_by_id\n    ):\n        all_are_done = True\n        for task_entity in task_entities:\n            task_status = statuses_by_id[task_entity[\"status_id\"]]\n            low_status_name = task_status[\"name\"].lower()\n            if low_status_name in ignored_statuses:\n                continue\n\n            low_state_name = task_status[\"state\"][\"name\"].lower()\n            if low_state_name != \"done\":\n                all_are_done = False\n                break\n        return all_are_done\n\n    def next_tasks_with_type_sorting(\n        self,\n        task_entities_by_type_id,\n        sorted_task_type_ids,\n        event_ids,\n        statuses_by_id,\n        ignored_statuses\n    ):\n        # `use_next_task` is used only if `name_sorting` is enabled!\n        next_tasks = []\n        use_next_tasks = False\n        for type_id in sorted_task_type_ids:\n            if type_id not in task_entities_by_type_id:\n                continue\n\n            task_entities = task_entities_by_type_id[type_id]\n\n            # Check if any task was in event\n            event_id_in_tasks = False\n            for task_entity in task_entities:\n                task_id = task_entity[\"id\"]\n                if task_id in event_ids:\n                    event_ids.remove(task_id)\n                    event_id_in_tasks = True\n\n            if use_next_tasks:\n                # Check if next tasks are not done already\n                all_in_type_done = self.check_statuses_done(\n                    task_entities, ignored_statuses, statuses_by_id\n                )\n                if all_in_type_done:\n                    continue\n\n                next_tasks.extend(task_entities)\n                use_next_tasks = False\n                if not event_ids:\n                    break\n\n            if not event_id_in_tasks:\n                continue\n\n            all_in_type_done = self.check_statuses_done(\n                task_entities, ignored_statuses, statuses_by_id\n            )\n            use_next_tasks = all_in_type_done\n            if all_in_type_done:\n                continue\n\n            if not event_ids:\n                break\n\n            use_next_tasks = False\n\n        return next_tasks\n\n    def statuses_for_tasks(self, task_type_ids, project_entity):\n        project_schema = project_entity[\"project_schema\"]\n        output = {}\n        for task_type_id in task_type_ids:\n            statuses = project_schema.get_statuses(\"Task\", task_type_id)\n            output[task_type_id] = {\n                status[\"name\"].lower(): status\n                for status in statuses\n            }\n\n        return output\n\n    def get_sorted_task_type_ids(self, session):\n        types_by_order = collections.defaultdict(list)\n        for _type in session.query(\"Type\").all():\n            sort_oder = _type.get(\"sort\")\n            if sort_oder is not None:\n                types_by_order[sort_oder].append(_type[\"id\"])\n\n        types = []\n        for sort_oder in sorted(types_by_order.keys()):\n            types.extend(types_by_order[sort_oder])\n        return types\n\n    @staticmethod\n    def sort_by_name_task_entities_by_type(task_entities_by_type_id):\n        _task_entities_by_type_id = {}\n        for type_id, task_entities in task_entities_by_type_id.items():\n            # Store tasks by name\n            task_entities_by_name = {}\n            for task_entity in task_entities:\n                task_name = task_entity[\"name\"]\n                task_entities_by_name[task_name] = task_entity\n\n            # Store task entities by sorted names\n            sorted_task_entities = []\n            for task_name in sorted(task_entities_by_name.keys()):\n                task_entity = task_entities_by_name[task_name]\n                sorted_task_entities.append(task_entity)\n            # Store result to temp dictionary\n            _task_entities_by_type_id[type_id] = sorted_task_entities\n\n        # Override values in source object\n        for type_id, value in _task_entities_by_type_id.items():\n            task_entities_by_type_id[type_id] = value\n\n\ndef register(session):\n    NextTaskUpdate(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py",
    "content": "import collections\nimport copy\nfrom typing import Any\n\nimport ftrack_api\n\nfrom openpype.client import get_project\nfrom openpype_modules.ftrack.lib import (\n    BaseEvent,\n    query_custom_attributes,\n)\n\n\nclass PushHierValuesToNonHierEvent(BaseEvent):\n    \"\"\"Push value changes between hierarchical and non-hierarchical attributes.\n\n    Changes of non-hierarchical attributes are pushed to hierarchical and back.\n    The attributes must have same definition of custom attribute.\n\n    Handler does not handle changes of hierarchical parents. So if entity does\n    not have explicitly set value of hierarchical attribute and any parent\n    would change it the change would not be propagated.\n\n    The handler also push the value to task entity on task creation\n        and movement. To push values between hierarchical & non-hierarchical\n        add 'Task' to entity types in settings.\n\n    Todos:\n        Task attribute values push on create/move should be possible to\n            enabled by settings.\n    \"\"\"\n\n    # Ignore event handler by default\n    cust_attrs_query = (\n        \"select id, key, object_type_id, is_hierarchical, default\"\n        \" from CustomAttributeConfiguration\"\n        \" where key in ({})\"\n    )\n\n    _cached_task_object_id = None\n    _cached_interest_object_ids = None\n    _cached_user_id = None\n    _cached_changes = []\n    _max_delta = 30\n\n    settings_key = \"sync_hier_entity_attributes\"\n\n    def filter_entities_info(\n        self, event: ftrack_api.event.base.Event\n    ) -> dict[str, list[dict[str, Any]]]:\n        \"\"\"Basic entities filter info we care about.\n\n        This filtering is first of many filters. This does not query anything\n        from ftrack nor use settings.\n\n        Args:\n            event (ftrack_api.event.base.Event): Ftrack event with update\n                information.\n\n        Returns:\n            dict[str, list[dict[str, Any]]]: Filtered entity changes by\n                project id.\n        \"\"\"\n\n        # Filter if event contain relevant data\n        entities_info = event[\"data\"].get(\"entities\")\n        if not entities_info:\n            return\n\n        entities_info_by_project_id = collections.defaultdict(list)\n        for entity_info in entities_info:\n            # Ignore removed entities\n            if entity_info.get(\"action\") == \"remove\":\n                continue\n\n            # Care only about information with changes of entities\n            changes = entity_info.get(\"changes\")\n            if not changes:\n                continue\n\n            # Get project id from entity info\n            project_id = None\n            for parent_item in reversed(entity_info[\"parents\"]):\n                if parent_item[\"entityType\"] == \"show\":\n                    project_id = parent_item[\"entityId\"]\n                    break\n\n            if project_id is None:\n                continue\n\n            entities_info_by_project_id[project_id].append(entity_info)\n\n        return entities_info_by_project_id\n\n    def _get_attrs_configurations(self, session, interest_attributes):\n        \"\"\"Get custom attribute configurations by name.\n\n        Args:\n            session (ftrack_api.Session): Ftrack sesson.\n            interest_attributes (list[str]): Names of custom attributes\n                that should be synchronized.\n\n        Returns:\n            tuple[dict[str, list], list]: Attributes by object id and\n                hierarchical attributes.\n        \"\"\"\n\n        attrs = session.query(self.cust_attrs_query.format(\n            self.join_query_keys(interest_attributes)\n        )).all()\n\n        attrs_by_obj_id = collections.defaultdict(list)\n        hier_attrs = []\n        for attr in attrs:\n            if attr[\"is_hierarchical\"]:\n                hier_attrs.append(attr)\n                continue\n            obj_id = attr[\"object_type_id\"]\n            attrs_by_obj_id[obj_id].append(attr)\n        return attrs_by_obj_id, hier_attrs\n\n    def _get_handler_project_settings(\n        self,\n        session: ftrack_api.Session,\n        event: ftrack_api.event.base.Event,\n        project_id: str\n    ) -> tuple[set[str], set[str]]:\n        \"\"\"Get handler settings based on the project.\n\n        Args:\n            session (ftrack_api.Session): Ftrack session.\n            event (ftrack_api.event.base.Event): Ftrack event which triggered\n                the changes.\n            project_id (str): Project id where the current changes are handled.\n\n        Returns:\n            tuple[set[str], set[str]]: Attribute names we care about and\n                entity types we care about.\n        \"\"\"\n\n        project_name: str = self.get_project_name_from_event(\n            session, event, project_id\n        )\n        if get_project(project_name) is None:\n            self.log.debug(\"Project not found in OpenPype. Skipping\")\n            return set(), set()\n\n        # Load settings\n        project_settings: dict[str, Any] = (\n            self.get_project_settings_from_event(event, project_name)\n        )\n        # Load status mapping from presets\n        event_settings: dict[str, Any] = (\n            project_settings\n            [\"ftrack\"]\n            [\"events\"]\n            [self.settings_key]\n        )\n        # Skip if event is not enabled\n        if not event_settings[\"enabled\"]:\n            self.log.debug(\"Project \\\"{}\\\" has disabled {}\".format(\n                project_name, self.__class__.__name__\n            ))\n            return set(), set()\n\n        interest_attributes: list[str] = event_settings[\"interest_attributes\"]\n        if not interest_attributes:\n            self.log.info((\n                \"Project \\\"{}\\\" does not have filled 'interest_attributes',\"\n                \" skipping.\"\n            ))\n\n        interest_entity_types: list[str] = (\n            event_settings[\"interest_entity_types\"])\n        if not interest_entity_types:\n            self.log.info((\n                \"Project \\\"{}\\\" does not have filled 'interest_entity_types',\"\n                \" skipping.\"\n            ))\n\n        # Unify possible issues from settings ('Asset Build' -> 'assetbuild')\n        interest_entity_types: set[str] = {\n            entity_type.replace(\" \", \"\").lower()\n            for entity_type in interest_entity_types\n        }\n        return set(interest_attributes), interest_entity_types\n\n    def _entities_filter_by_settings(\n        self,\n        entities_info: list[dict[str, Any]],\n        interest_attributes: set[str],\n        interest_entity_types: set[str]\n    ):\n        new_entities_info = []\n        for entity_info in entities_info:\n            entity_type_low = entity_info[\"entity_type\"].lower()\n\n            changes = entity_info[\"changes\"]\n            # SPECIAL CASE: Capture changes of task created/moved under\n            #   interested entity type\n            if (\n                entity_type_low == \"task\"\n                and \"parent_id\" in changes\n            ):\n                # Direct parent is always second item in 'parents' and 'Task'\n                #   must have at least one parent\n                parent_info = entity_info[\"parents\"][1]\n                parent_entity_type = (\n                    parent_info[\"entity_type\"]\n                    .replace(\" \", \"\")\n                    .lower()\n                )\n                if parent_entity_type in interest_entity_types:\n                    new_entities_info.append(entity_info)\n                    continue\n\n            # Skip if entity type is not enabled for attr value sync\n            if entity_type_low not in interest_entity_types:\n                continue\n\n            valid_attr_change = entity_info.get(\"action\") == \"add\"\n            for attr_key in interest_attributes:\n                if valid_attr_change:\n                    break\n\n                if attr_key not in changes:\n                    continue\n\n                if changes[attr_key][\"new\"] is not None:\n                    valid_attr_change = True\n\n            if not valid_attr_change:\n                continue\n\n            new_entities_info.append(entity_info)\n\n        return new_entities_info\n\n    def propagate_attribute_changes(\n        self,\n        session,\n        interest_attributes,\n        entities_info,\n        attrs_by_obj_id,\n        hier_attrs,\n        real_values_by_entity_id,\n        hier_values_by_entity_id,\n    ):\n        hier_attr_ids_by_key = {\n            attr[\"key\"]: attr[\"id\"]\n            for attr in hier_attrs\n        }\n        filtered_interest_attributes = {\n            attr_name\n            for attr_name in interest_attributes\n            if attr_name in hier_attr_ids_by_key\n        }\n        attrs_keys_by_obj_id = {}\n        for obj_id, attrs in attrs_by_obj_id.items():\n            attrs_keys_by_obj_id[obj_id] = {\n                attr[\"key\"]: attr[\"id\"]\n                for attr in attrs\n            }\n\n        op_changes = []\n        for entity_info in entities_info:\n            entity_id = entity_info[\"entityId\"]\n            obj_id = entity_info[\"objectTypeId\"]\n            # Skip attributes sync if does not have object specific custom\n            #   attribute\n            if obj_id not in attrs_keys_by_obj_id:\n                continue\n            attr_keys = attrs_keys_by_obj_id[obj_id]\n            real_values = real_values_by_entity_id[entity_id]\n            hier_values = hier_values_by_entity_id[entity_id]\n\n            changes = copy.deepcopy(entity_info[\"changes\"])\n            obj_id_attr_keys = {\n                attr_key\n                for attr_key in filtered_interest_attributes\n                if attr_key in attr_keys\n            }\n            if not obj_id_attr_keys:\n                continue\n\n            value_by_key = {}\n            is_new_entity = entity_info.get(\"action\") == \"add\"\n            for attr_key in obj_id_attr_keys:\n                if (\n                    attr_key in changes\n                    and changes[attr_key][\"new\"] is not None\n                ):\n                    value_by_key[attr_key] = changes[attr_key][\"new\"]\n\n                if not is_new_entity:\n                    continue\n\n                hier_attr_id = hier_attr_ids_by_key[attr_key]\n                attr_id = attr_keys[attr_key]\n                if hier_attr_id in real_values or attr_id in real_values:\n                    continue\n\n                value_by_key[attr_key] = hier_values[hier_attr_id]\n\n            for key, new_value in value_by_key.items():\n                if new_value is None:\n                    continue\n\n                hier_id = hier_attr_ids_by_key[key]\n                std_id = attr_keys[key]\n                real_hier_value = real_values.get(hier_id)\n                real_std_value = real_values.get(std_id)\n                hier_value = hier_values[hier_id]\n                # Get right type of value for conversion\n                #   - values in event are strings\n                type_value = real_hier_value\n                if type_value is None:\n                    type_value = real_std_value\n                    if type_value is None:\n                        type_value = hier_value\n                        # Skip if current values are not set\n                        if type_value is None:\n                            continue\n\n                try:\n                    new_value = type(type_value)(new_value)\n                except Exception:\n                    self.log.warning((\n                        \"Couldn't convert from {} to {}.\"\n                        \" Skipping update values.\"\n                    ).format(type(new_value), type(type_value)))\n                    continue\n\n                real_std_value_is_same = new_value == real_std_value\n                real_hier_value_is_same = new_value == real_hier_value\n                # New value does not match anything in current entity values\n                if (\n                    not is_new_entity\n                    and not real_std_value_is_same\n                    and not real_hier_value_is_same\n                ):\n                    continue\n\n                if not real_std_value_is_same:\n                    op_changes.append((\n                        std_id,\n                        entity_id,\n                        new_value,\n                        real_values.get(std_id),\n                        std_id in real_values\n                    ))\n\n                if not real_hier_value_is_same:\n                    op_changes.append((\n                        hier_id,\n                        entity_id,\n                        new_value,\n                        real_values.get(hier_id),\n                        hier_id in real_values\n                    ))\n\n        for change in op_changes:\n            (\n                attr_id,\n                entity_id,\n                new_value,\n                old_value,\n                do_update\n            ) = change\n\n            entity_key = collections.OrderedDict([\n                (\"configuration_id\", attr_id),\n                (\"entity_id\", entity_id)\n            ])\n            if do_update:\n                op = ftrack_api.operation.UpdateEntityOperation(\n                    \"CustomAttributeValue\",\n                    entity_key,\n                    \"value\",\n                    old_value,\n                    new_value\n                )\n\n            else:\n                op = ftrack_api.operation.CreateEntityOperation(\n                    \"CustomAttributeValue\",\n                    entity_key,\n                    {\"value\": new_value}\n                )\n\n            session.recorded_operations.push(op)\n            if len(session.recorded_operations) > 100:\n                session.commit()\n        session.commit()\n\n    def process_by_project(\n        self,\n        session: ftrack_api.Session,\n        event: ftrack_api.event.base.Event,\n        project_id: str,\n        entities_info: list[dict[str, Any]]\n    ):\n        \"\"\"Process changes in single project.\n\n        Args:\n            session (ftrack_api.Session): Ftrack session.\n            event (ftrack_api.event.base.Event): Event which has all changes\n                information.\n            project_id (str): Project id related to changes.\n            entities_info (list[dict[str, Any]]): Changes of entities.\n        \"\"\"\n\n        (\n            interest_attributes,\n            interest_entity_types\n        ) = self._get_handler_project_settings(session, event, project_id)\n        if not interest_attributes or not interest_entity_types:\n            return\n\n        entities_info: list[dict[str, Any]] = (\n            self._entities_filter_by_settings(\n                entities_info,\n                interest_attributes,\n                interest_entity_types\n            )\n        )\n        if not entities_info:\n            return\n\n        attrs_by_obj_id, hier_attrs = self._get_attrs_configurations(\n            session, interest_attributes\n        )\n        # Skip if attributes are not available\n        #   - there is nothing to sync\n        if not attrs_by_obj_id or not hier_attrs:\n            return\n\n        entity_ids_by_parent_id = collections.defaultdict(set)\n        all_entity_ids = set()\n        for entity_info in entities_info:\n            entity_id = None\n            for item in entity_info[\"parents\"]:\n                item_id = item[\"entityId\"]\n                all_entity_ids.add(item_id)\n                if entity_id is not None:\n                    entity_ids_by_parent_id[item_id].add(entity_id)\n                entity_id = item_id\n\n        attr_ids = {attr[\"id\"] for attr in hier_attrs}\n        for attrs in attrs_by_obj_id.values():\n            attr_ids |= {attr[\"id\"] for attr in attrs}\n\n        # Query real custom attribute values\n        #   - we have to know what are the real values, if are set and to what\n        #       value\n        value_items = query_custom_attributes(\n            session, attr_ids, all_entity_ids, True\n        )\n        real_values_by_entity_id = collections.defaultdict(dict)\n        for item in value_items:\n            entity_id = item[\"entity_id\"]\n            attr_id = item[\"configuration_id\"]\n            real_values_by_entity_id[entity_id][attr_id] = item[\"value\"]\n\n        hier_values_by_entity_id = {}\n        default_values = {\n            attr[\"id\"]: attr[\"default\"]\n            for attr in hier_attrs\n        }\n        hier_queue = collections.deque()\n        hier_queue.append((default_values, [project_id]))\n        while hier_queue:\n            parent_values, entity_ids = hier_queue.popleft()\n            for entity_id in entity_ids:\n                entity_values = copy.deepcopy(parent_values)\n                real_values = real_values_by_entity_id[entity_id]\n                for attr_id, value in real_values.items():\n                    entity_values[attr_id] = value\n                hier_values_by_entity_id[entity_id] = entity_values\n                hier_queue.append(\n                    (entity_values, entity_ids_by_parent_id[entity_id])\n                )\n\n        self.propagate_attribute_changes(\n            session,\n            interest_attributes,\n            entities_info,\n            attrs_by_obj_id,\n            hier_attrs,\n            real_values_by_entity_id,\n            hier_values_by_entity_id,\n        )\n\n    def launch(self, session, event):\n        filtered_entities_info = self.filter_entities_info(event)\n        if not filtered_entities_info:\n            return\n\n        for project_id, entities_info in filtered_entities_info.items():\n            self.process_by_project(session, event, project_id, entities_info)\n\n\ndef register(session):\n    PushHierValuesToNonHierEvent(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/event_radio_buttons.py",
    "content": "import ftrack_api\nfrom openpype_modules.ftrack.lib import BaseEvent\n\n\nclass RadioButtons(BaseEvent):\n\n    ignore_me = True\n\n    def launch(self, session, event):\n        '''Provides a radio button behaviour to any boolean attribute in\n           radio_button group.'''\n\n        # start of event procedure ----------------------------------\n        for entity in event['data'].get('entities', []):\n\n            if entity['entityType'] == 'assetversion':\n\n                query = 'CustomAttributeGroup where name is \"radio_button\"'\n                group = session.query(query).one()\n                radio_buttons = []\n                for g in group['custom_attribute_configurations']:\n                    radio_buttons.append(g['key'])\n\n                for key in entity['keys']:\n                    if (key in radio_buttons and entity['changes'] is not None):\n                        if entity['changes'][key]['new'] == '1':\n                            version = session.get('AssetVersion',\n                                                  entity['entityId'])\n                            asset = session.get('Asset', entity['parentId'])\n                            for v in asset['versions']:\n                                if version is not v:\n                                    v['custom_attributes'][key] = 0\n\n            session.commit()\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    RadioButtons(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/event_sync_links.py",
    "content": "from pymongo import UpdateOne\nfrom bson.objectid import ObjectId\n\nfrom openpype.pipeline import AvalonMongoDB\n\nfrom openpype_modules.ftrack.lib import (\n    CUST_ATTR_ID_KEY,\n    query_custom_attributes,\n\n    BaseEvent\n)\n\n\nclass SyncLinksToAvalon(BaseEvent):\n    \"\"\"Synchronize inpug linkts to avalon documents.\"\"\"\n    # Run after sync to avalon event handler\n    priority = 110\n\n    def __init__(self, session):\n        self.dbcon = AvalonMongoDB()\n\n        super(SyncLinksToAvalon, self).__init__(session)\n\n    def launch(self, session, event):\n        # Try to commit and if any error happen then recreate session\n        entities_info = event[\"data\"][\"entities\"]\n        dependency_changes = []\n        removed_entities = set()\n        for entity_info in entities_info:\n            action = entity_info.get(\"action\")\n            entityType = entity_info.get(\"entityType\")\n            if action not in (\"remove\", \"add\"):\n                continue\n\n            if entityType == \"task\":\n                removed_entities.add(entity_info[\"entityId\"])\n            elif entityType == \"dependency\":\n                dependency_changes.append(entity_info)\n\n        # Care only about dependency changes\n        if not dependency_changes:\n            return\n\n        project_id = None\n        for entity_info in dependency_changes:\n            for parent_info in entity_info[\"parents\"]:\n                if parent_info[\"entityType\"] == \"show\":\n                    project_id = parent_info[\"entityId\"]\n            if project_id is not None:\n                break\n\n        changed_to_ids = set()\n        for entity_info in dependency_changes:\n            to_id_change = entity_info[\"changes\"][\"to_id\"]\n            if to_id_change[\"new\"] is not None:\n                changed_to_ids.add(to_id_change[\"new\"])\n\n            if to_id_change[\"old\"] is not None:\n                changed_to_ids.add(to_id_change[\"old\"])\n\n        self._update_in_links(session, changed_to_ids, project_id)\n\n    def _update_in_links(self, session, ftrack_ids, project_id):\n        if not ftrack_ids or project_id is None:\n            return\n\n        attr_def = session.query((\n            \"select id from CustomAttributeConfiguration where key is \\\"{}\\\"\"\n        ).format(CUST_ATTR_ID_KEY)).first()\n        if attr_def is None:\n            return\n\n        project_entity = session.query((\n            \"select full_name from Project where id is \\\"{}\\\"\"\n        ).format(project_id)).first()\n        if not project_entity:\n            return\n\n        project_name = project_entity[\"full_name\"]\n        mongo_id_by_ftrack_id = self._get_mongo_ids_by_ftrack_ids(\n            session, attr_def[\"id\"], ftrack_ids\n        )\n\n        filtered_ftrack_ids = tuple(mongo_id_by_ftrack_id.keys())\n        context_links = session.query((\n            \"select from_id, to_id from TypedContextLink where to_id in ({})\"\n        ).format(self.join_query_keys(filtered_ftrack_ids))).all()\n\n        mapping_by_to_id = {\n            ftrack_id: set()\n            for ftrack_id in filtered_ftrack_ids\n        }\n        all_from_ids = set()\n        for context_link in context_links:\n            to_id = context_link[\"to_id\"]\n            from_id = context_link[\"from_id\"]\n            if from_id == to_id:\n                continue\n            all_from_ids.add(from_id)\n            mapping_by_to_id[to_id].add(from_id)\n\n        mongo_id_by_ftrack_id.update(self._get_mongo_ids_by_ftrack_ids(\n            session, attr_def[\"id\"], all_from_ids\n        ))\n        self.log.info(mongo_id_by_ftrack_id)\n        bulk_writes = []\n        for to_id, from_ids in mapping_by_to_id.items():\n            dst_mongo_id = mongo_id_by_ftrack_id[to_id]\n            links = []\n            for ftrack_id in from_ids:\n                link_mongo_id = mongo_id_by_ftrack_id.get(ftrack_id)\n                if link_mongo_id is None:\n                    continue\n\n                links.append({\n                    \"id\": ObjectId(link_mongo_id),\n                    \"linkedBy\": \"ftrack\",\n                    \"type\": \"breakdown\"\n                })\n\n            bulk_writes.append(UpdateOne(\n                {\"_id\": ObjectId(dst_mongo_id)},\n                {\"$set\": {\"data.inputLinks\": links}}\n            ))\n\n        if bulk_writes:\n            self.dbcon.database[project_name].bulk_write(bulk_writes)\n\n    def _get_mongo_ids_by_ftrack_ids(self, session, attr_id, ftrack_ids):\n        output = query_custom_attributes(\n            session, [attr_id], ftrack_ids, True\n        )\n        mongo_id_by_ftrack_id = {}\n        for item in output:\n            mongo_id = item[\"value\"]\n            if not mongo_id:\n                continue\n\n            ftrack_id = item[\"entity_id\"]\n\n            mongo_id_by_ftrack_id[ftrack_id] = mongo_id\n        return mongo_id_by_ftrack_id\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n    SyncLinksToAvalon(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py",
    "content": "import collections\nimport copy\nimport json\nimport time\nimport datetime\nimport atexit\nimport traceback\n\nfrom bson.objectid import ObjectId\nfrom pymongo import UpdateOne\n\nimport arrow\nimport ftrack_api\n\nfrom openpype.client import (\n    get_project,\n    get_assets,\n    get_archived_assets,\n    get_asset_ids_with_subsets\n)\nfrom openpype.client.operations import CURRENT_ASSET_DOC_SCHEMA\nfrom openpype.pipeline import AvalonMongoDB, schema\n\nfrom openpype_modules.ftrack.lib import (\n    get_openpype_attr,\n    query_custom_attributes,\n    CUST_ATTR_ID_KEY,\n    CUST_ATTR_AUTO_SYNC,\n    FPS_KEYS,\n\n    avalon_sync,\n\n    BaseEvent\n)\nfrom openpype_modules.ftrack.lib.avalon_sync import (\n    convert_to_fps,\n    InvalidFpsValue\n)\n\n\nclass SyncToAvalonEvent(BaseEvent):\n    interest_entTypes = [\"show\", \"task\"]\n    ignore_ent_types = [\"Milestone\"]\n    ignore_keys = [\"statusid\", \"thumbid\"]\n\n    cust_attr_query_keys = [\n        \"id\",\n        \"key\",\n        \"entity_type\",\n        \"object_type_id\",\n        \"is_hierarchical\",\n        \"config\",\n        \"default\"\n    ]\n    project_query = (\n        \"select full_name, name, custom_attributes\"\n        \", project_schema._task_type_schema.types.name\"\n        \" from Project where id is \\\"{}\\\"\"\n    )\n\n    entities_query_by_id = (\n        \"select id, name, parent_id, link, custom_attributes, description\"\n        \" from TypedContext where project_id is \\\"{}\\\" and id in ({})\"\n    )\n\n    # useful for getting all tasks for asset\n    task_entities_query_by_parent_id = (\n        \"select id, name, parent_id, type_id from Task\"\n        \" where project_id is \\\"{}\\\" and parent_id in ({})\"\n    )\n    task_types_query = (\n        \"select id, name from Type\"\n    )\n    entities_name_query_by_name = (\n        \"select id, name from TypedContext\"\n        \" where project_id is \\\"{}\\\" and name in ({})\"\n    )\n    created_entities = []\n    report_splitter = {\"type\": \"label\", \"value\": \"---\"}\n\n    def __init__(self, session):\n        '''Expects a ftrack_api.Session instance'''\n        # Debug settings\n        # - time expiration in seconds\n        self.debug_print_time_expiration = 5 * 60\n        # - store current time\n        self.debug_print_time = datetime.datetime.now()\n        # - store synchronize entity types to be able to use\n        #   only entityTypes in interest instead of filtering by ignored\n        self.debug_sync_types = collections.defaultdict(list)\n\n        self.dbcon = AvalonMongoDB()\n        # Set processing session to not use global\n        self.set_process_session(session)\n        super().__init__(session)\n\n    def debug_logs(self):\n        \"\"\"This is debug method for printing small debugs messages. \"\"\"\n        now_datetime = datetime.datetime.now()\n        delta = now_datetime - self.debug_print_time\n        if delta.total_seconds() < self.debug_print_time_expiration:\n            return\n\n        self.debug_print_time = now_datetime\n        known_types_items = []\n        for entityType, entity_type in self.debug_sync_types.items():\n            ent_types_msg = \", \".join(entity_type)\n            known_types_items.append(\n                \"<{}> ({})\".format(entityType, ent_types_msg)\n            )\n\n        known_entityTypes = \", \".join(known_types_items)\n        self.log.debug(\n            \"DEBUG MESSAGE: Known types {}\".format(known_entityTypes)\n        )\n\n    @property\n    def cur_project(self):\n        if self._cur_project is None:\n            found_id = None\n            for ent_info in self._cur_event[\"data\"][\"entities\"]:\n                if found_id is not None:\n                    break\n                parents = ent_info.get(\"parents\") or []\n                for parent in parents:\n                    if parent.get(\"entityType\") == \"show\":\n                        found_id = parent.get(\"entityId\")\n                        break\n            if found_id:\n                self._cur_project = self.process_session.query(\n                    self.project_query.format(found_id)\n                ).one()\n        return self._cur_project\n\n    @property\n    def avalon_cust_attrs(self):\n        if self._avalon_cust_attrs is None:\n            self._avalon_cust_attrs = get_openpype_attr(\n                self.process_session, query_keys=self.cust_attr_query_keys\n            )\n        return self._avalon_cust_attrs\n\n    @property\n    def cust_attr_types_by_id(self):\n        if self._cust_attr_types_by_id is None:\n            cust_attr_types = self.process_session.query(\n                \"select id, name from CustomAttributeType\"\n            ).all()\n            self._cust_attr_types_by_id = {\n                cust_attr_type[\"id\"]: cust_attr_type\n                for cust_attr_type in cust_attr_types\n            }\n        return self._cust_attr_types_by_id\n\n    @property\n    def avalon_entities(self):\n        if self._avalon_ents is None:\n            project_name = self.cur_project[\"full_name\"]\n            self.dbcon.install()\n            self.dbcon.Session[\"AVALON_PROJECT\"] = project_name\n            avalon_project = get_project(project_name)\n            avalon_entities = list(get_assets(project_name))\n            self._avalon_ents = (avalon_project, avalon_entities)\n        return self._avalon_ents\n\n    @property\n    def avalon_ents_by_name(self):\n        if self._avalon_ents_by_name is None:\n            self._avalon_ents_by_name = {}\n            proj, ents = self.avalon_entities\n            for ent in ents:\n                self._avalon_ents_by_name[ent[\"name\"]] = ent\n        return self._avalon_ents_by_name\n\n    @property\n    def avalon_ents_by_id(self):\n        if self._avalon_ents_by_id is None:\n            self._avalon_ents_by_id = {}\n            proj, ents = self.avalon_entities\n            if proj:\n                self._avalon_ents_by_id[proj[\"_id\"]] = proj\n                for ent in ents:\n                    self._avalon_ents_by_id[ent[\"_id\"]] = ent\n        return self._avalon_ents_by_id\n\n    @property\n    def avalon_ents_by_parent_id(self):\n        if self._avalon_ents_by_parent_id is None:\n            self._avalon_ents_by_parent_id = collections.defaultdict(list)\n            proj, ents = self.avalon_entities\n            for ent in ents:\n                vis_par = ent[\"data\"][\"visualParent\"]\n                if vis_par is None:\n                    vis_par = proj[\"_id\"]\n                self._avalon_ents_by_parent_id[vis_par].append(ent)\n        return self._avalon_ents_by_parent_id\n\n    @property\n    def avalon_ents_by_ftrack_id(self):\n        if self._avalon_ents_by_ftrack_id is None:\n            self._avalon_ents_by_ftrack_id = {}\n            proj, ents = self.avalon_entities\n            if proj:\n                ftrack_id = proj[\"data\"].get(\"ftrackId\")\n                if ftrack_id is None:\n                    self.handle_missing_ftrack_id(proj)\n                    ftrack_id = proj[\"data\"][\"ftrackId\"]\n                self._avalon_ents_by_ftrack_id[ftrack_id] = proj\n\n                self._avalon_ents_by_ftrack_id[ftrack_id] = proj\n                for ent in ents:\n                    ftrack_id = ent[\"data\"].get(\"ftrackId\")\n                    if ftrack_id is None:\n                        continue\n                    self._avalon_ents_by_ftrack_id[ftrack_id] = ent\n        return self._avalon_ents_by_ftrack_id\n\n    def handle_missing_ftrack_id(self, doc):\n        # TODO handling of missing ftrack id is primarily issue of editorial\n        #   publishing it would be better to find out what causes that\n        #   ftrack id is removed during the publishing\n        ftrack_id = doc[\"data\"].get(\"ftrackId\")\n        if ftrack_id is not None:\n            return\n\n        if doc[\"type\"] == \"project\":\n            ftrack_id = self.cur_project[\"id\"]\n\n            self.dbcon.update_one(\n                {\"type\": \"project\"},\n                {\"$set\": {\n                    \"data.ftrackId\": ftrack_id,\n                    \"data.entityType\": self.cur_project.entity_type\n                }}\n            )\n\n            doc[\"data\"][\"ftrackId\"] = ftrack_id\n            doc[\"data\"][\"entityType\"] = self.cur_project.entity_type\n            self.log.info(\"Updated ftrack id of project \\\"{}\\\"\".format(\n                self.cur_project[\"full_name\"]\n            ))\n            return\n\n        if doc[\"type\"] != \"asset\":\n            return\n\n        doc_parents = doc.get(\"data\", {}).get(\"parents\")\n        if doc_parents is None:\n            return\n\n        entities = self.process_session.query((\n            \"select id, link from TypedContext\"\n            \" where project_id is \\\"{}\\\" and name is \\\"{}\\\"\"\n        ).format(self.cur_project[\"id\"], doc[\"name\"])).all()\n        self.log.info(\"Entities: {}\".format(str(entities)))\n        matching_entity = None\n        for entity in entities:\n            parents = []\n            for item in entity[\"link\"]:\n                if item[\"id\"] == entity[\"id\"]:\n                    break\n                low_type = item[\"type\"].lower()\n                if low_type == \"typedcontext\":\n                    parents.append(item[\"name\"])\n            if doc_parents == parents:\n                matching_entity = entity\n                break\n\n        if matching_entity is None:\n            return\n\n        ftrack_id = matching_entity[\"id\"]\n        self.dbcon.update_one(\n            {\"_id\": doc[\"_id\"]},\n            {\"$set\": {\n                \"data.ftrackId\": ftrack_id,\n                \"data.entityType\": matching_entity.entity_type\n            }}\n        )\n        doc[\"data\"][\"ftrackId\"] = ftrack_id\n        doc[\"data\"][\"entityType\"] = matching_entity.entity_type\n\n        entity_path_items = []\n        for item in entity[\"link\"]:\n            entity_path_items.append(item[\"name\"])\n        self.log.info(\"Updated ftrack id of entity \\\"{}\\\"\".format(\n            \"/\".join(entity_path_items)\n        ))\n        self._avalon_ents_by_ftrack_id[ftrack_id] = doc\n\n    @property\n    def avalon_asset_ids_with_subsets(self):\n        if self._avalon_asset_ids_with_subsets is None:\n            project_name = self.cur_project[\"full_name\"]\n            self._avalon_asset_ids_with_subsets = get_asset_ids_with_subsets(\n                project_name\n            )\n\n        return self._avalon_asset_ids_with_subsets\n\n    @property\n    def avalon_archived_by_id(self):\n        if self._avalon_archived_by_id is None:\n            self._avalon_archived_by_id = {}\n            project_name = self.cur_project[\"full_name\"]\n            for asset in get_archived_assets(project_name):\n                self._avalon_archived_by_id[asset[\"_id\"]] = asset\n        return self._avalon_archived_by_id\n\n    @property\n    def avalon_archived_by_name(self):\n        if self._avalon_archived_by_name is None:\n            self._avalon_archived_by_name = {}\n            for asset in self.avalon_archived_by_id.values():\n                self._avalon_archived_by_name[asset[\"name\"]] = asset\n        return self._avalon_archived_by_name\n\n    @property\n    def changeability_by_mongo_id(self):\n        \"\"\"Return info about changeability of entity and it's parents.\"\"\"\n        if self._changeability_by_mongo_id is None:\n            self._changeability_by_mongo_id = collections.defaultdict(\n                lambda: True\n            )\n            avalon_project, avalon_entities = self.avalon_entities\n            self._changeability_by_mongo_id[avalon_project[\"_id\"]] = False\n            self._bubble_changeability(\n                list(self.avalon_asset_ids_with_subsets)\n            )\n\n        return self._changeability_by_mongo_id\n\n    def remove_cached_by_key(self, key, values):\n        if self._avalon_ents is None:\n            return\n\n        if not isinstance(values, (list, tuple)):\n            values = [values]\n\n        def get_found_data(entity):\n            if not entity:\n                return None\n            return {\n                \"ftrack_id\": entity[\"data\"][\"ftrackId\"],\n                \"parent_id\": entity[\"data\"][\"visualParent\"],\n                \"_id\": entity[\"_id\"],\n                \"name\": entity[\"name\"],\n                \"entity\": entity\n            }\n\n        if key == \"id\":\n            key = \"_id\"\n        elif key == \"ftrack_id\":\n            key = \"data.ftrackId\"\n\n        found_data = {}\n        project, entities = self._avalon_ents\n        key_items = key.split(\".\")\n        for value in values:\n            ent = None\n            if key == \"_id\":\n                if self._avalon_ents_by_id is not None:\n                    ent = self._avalon_ents_by_id.get(value)\n\n            elif key == \"name\":\n                if self._avalon_ents_by_name is not None:\n                    ent = self._avalon_ents_by_name.get(value)\n\n            elif key == \"data.ftrackId\":\n                if self._avalon_ents_by_ftrack_id is not None:\n                    ent = self._avalon_ents_by_ftrack_id.get(value)\n\n            if ent is None:\n                for _ent in entities:\n                    _temp = _ent\n                    for item in key_items:\n                        _temp = _temp[item]\n\n                    if _temp == value:\n                        ent = _ent\n                        break\n\n            found_data[value] = get_found_data(ent)\n\n        for value in values:\n            data = found_data[value]\n            if not data:\n                # TODO logging\n                self.log.warning(\n                    \"Didn't find entity by key/value \\\"{}\\\" / \\\"{}\\\"\".format(\n                        key, value\n                    )\n                )\n                continue\n\n            ftrack_id = data[\"ftrack_id\"]\n            parent_id = data[\"parent_id\"]\n            mongo_id = data[\"_id\"]\n            name = data[\"name\"]\n            entity = data[\"entity\"]\n\n            project, ents = self._avalon_ents\n            ents.remove(entity)\n            self._avalon_ents = project, ents\n\n            if self._avalon_ents_by_ftrack_id is not None:\n                self._avalon_ents_by_ftrack_id.pop(ftrack_id, None)\n\n            if self._avalon_ents_by_parent_id is not None:\n                self._avalon_ents_by_parent_id[parent_id].remove(entity)\n\n            if self._avalon_ents_by_id is not None:\n                self._avalon_ents_by_id.pop(mongo_id, None)\n\n            if self._avalon_ents_by_name is not None:\n                self._avalon_ents_by_name.pop(name, None)\n\n            if self._avalon_archived_by_id is not None:\n                self._avalon_archived_by_id[mongo_id] = entity\n\n    def _bubble_changeability(self, unchangeable_ids):\n        unchangeable_queue = collections.deque()\n        for entity_id in unchangeable_ids:\n            unchangeable_queue.append((entity_id, False))\n\n        processed_parents_ids = []\n        while unchangeable_queue:\n            entity_id, child_is_archived = unchangeable_queue.popleft()\n            # skip if already processed\n            if entity_id in processed_parents_ids:\n                continue\n\n            entity = self.avalon_ents_by_id.get(entity_id)\n            # if entity is not archived but unchageable child was then skip\n            # - archived entities should not affect not archived?\n            if entity and child_is_archived:\n                continue\n\n            # set changeability of current entity to False\n            self._changeability_by_mongo_id[entity_id] = False\n            processed_parents_ids.append(entity_id)\n            # if not entity then is probably archived\n            if not entity:\n                entity = self.avalon_archived_by_id.get(entity_id)\n                child_is_archived = True\n\n            if not entity:\n                # if entity is not found then it is subset without parent\n                if entity_id in unchangeable_ids:\n                    self.log.warning((\n                        \"Parent <{}> with subsets does not exist\"\n                    ).format(str(entity_id)))\n                else:\n                    self.log.warning((\n                        \"In avalon are entities without valid parents that\"\n                        \" lead to Project (should not cause errors)\"\n                        \" - MongoId <{}>\"\n                    ).format(str(entity_id)))\n                continue\n\n            # skip if parent is project\n            parent_id = entity[\"data\"][\"visualParent\"]\n            if parent_id is None:\n                continue\n            unchangeable_queue.append((parent_id, child_is_archived))\n\n    def reset_variables(self):\n        \"\"\"Reset variables so each event callback has clear env.\"\"\"\n        self._cur_project = None\n\n        self._avalon_cust_attrs = None\n        self._cust_attr_types_by_id = None\n\n        self._avalon_ents = None\n        self._avalon_ents_by_id = None\n        self._avalon_ents_by_parent_id = None\n        self._avalon_ents_by_ftrack_id = None\n        self._avalon_ents_by_name = None\n        self._avalon_asset_ids_with_subsets = None\n        self._changeability_by_mongo_id = None\n        self._avalon_archived_by_id = None\n        self._avalon_archived_by_name = None\n\n        self._ent_types_by_name = None\n\n        self.ftrack_ents_by_id = {}\n        self.obj_id_ent_type_map = {}\n        self.ftrack_recreated_mapping = {}\n\n        self.ftrack_added = {}\n        self.ftrack_moved = {}\n        self.ftrack_renamed = {}\n        self.ftrack_updated = {}\n        self.ftrack_removed = {}\n\n        # set of ftrack ids with modified tasks\n        # handled separately by full wipeout and replace from FTrack\n        self.modified_tasks_ftrackids = set()\n\n        self.moved_in_avalon = []\n        self.renamed_in_avalon = []\n        self.hier_cust_attrs_changes = collections.defaultdict(list)\n\n        self.duplicated = []\n        self.regex_failed = []\n\n        self.regex_schemas = {}\n        self.updates = collections.defaultdict(dict)\n\n        self.report_items = {\n            \"info\": collections.defaultdict(list),\n            \"warning\": collections.defaultdict(list),\n            \"error\": collections.defaultdict(list)\n        }\n\n    def set_process_session(self, session):\n        try:\n            self.process_session.close()\n        except Exception:\n            pass\n        self.process_session = ftrack_api.Session(\n            server_url=session.server_url,\n            api_key=session.api_key,\n            api_user=session.api_user,\n            auto_connect_event_hub=True\n        )\n        atexit.register(lambda: self.process_session.close())\n\n    def filter_updated(self, updates):\n        filtered_updates = {}\n        for ftrack_id, ent_info in updates.items():\n            changed_keys = [k for k in (ent_info.get(\"keys\") or [])]\n            changes = {\n                k: v for k, v in (ent_info.get(\"changes\") or {}).items()\n            }\n\n            entity_type = ent_info[\"entity_type\"]\n            if entity_type == \"Task\":\n                if \"name\" in changed_keys:\n                    ent_info[\"keys\"] = [\"name\"]\n                    ent_info[\"changes\"] = {\"name\": changes.pop(\"name\")}\n                    filtered_updates[ftrack_id] = ent_info\n                continue\n\n            for _key in self.ignore_keys:\n                if _key in changed_keys:\n                    changed_keys.remove(_key)\n                    changes.pop(_key, None)\n\n            if not changed_keys:\n                continue\n\n            # Remove custom attributes starting with `avalon_` from changes\n            # - these custom attributes are not synchronized\n            avalon_keys = []\n            for key in changes:\n                if key.startswith(\"avalon_\"):\n                    avalon_keys.append(key)\n\n            for _key in avalon_keys:\n                changed_keys.remove(_key)\n                changes.pop(_key, None)\n\n            if not changed_keys:\n                continue\n\n            ent_info[\"keys\"] = changed_keys\n            ent_info[\"changes\"] = changes\n            filtered_updates[ftrack_id] = ent_info\n\n        return filtered_updates\n\n    def get_ent_path(self, ftrack_id):\n        \"\"\"\n            Looks for entity in FTrack with 'ftrack_id'. If found returns\n            concatenated paths from its 'link' elemenent's names. Describes\n            location of entity in tree.\n        Args:\n            ftrack_id (string): entityId of FTrack entity\n\n        Returns:\n            (string) - example : \"/test_project/assets/my_asset\"\n        \"\"\"\n        entity = self.ftrack_ents_by_id.get(ftrack_id)\n        if not entity:\n            entity = self.process_session.query(\n                self.entities_query_by_id.format(\n                    self.cur_project[\"id\"], ftrack_id\n                )\n            ).first()\n            if entity:\n                self.ftrack_ents_by_id[ftrack_id] = entity\n            else:\n                return \"unknown hierarchy\"\n        return \"/\".join([ent[\"name\"] for ent in entity[\"link\"]])\n\n    def launch(self, session, event):\n        \"\"\"\n            Main entry port for synchronization.\n            Goes through event (can contain multiple changes) and decides if\n            the event is interesting for us (interest_entTypes).\n            It separates changes into add|remove|update.\n            All task changes are handled together by refresh from Ftrack.\n        Args:\n            session (object): session to Ftrack\n            event (dictionary): event content\n\n        Returns:\n            (boolean or None)\n        \"\"\"\n        # Try to commit and if any error happen then recreate session\n        try:\n            self.process_session.commit()\n        except Exception:\n            self.set_process_session(session)\n        # Reset object values for each launch\n        self.reset_variables()\n        self._cur_event = event\n\n        entities_by_action = {\n            \"remove\": {},\n            \"update\": {},\n            \"move\": {},\n            \"add\": {}\n        }\n\n        entities_info = event[\"data\"][\"entities\"]\n        found_actions = set()\n        for ent_info in entities_info:\n            entityType = ent_info[\"entityType\"]\n            if entityType not in self.interest_entTypes:\n                continue\n\n            entity_type = ent_info.get(\"entity_type\")\n            if not entity_type or entity_type in self.ignore_ent_types:\n                continue\n\n            if entity_type not in self.debug_sync_types[entityType]:\n                self.debug_sync_types[entityType].append(entity_type)\n\n            action = ent_info[\"action\"]\n            ftrack_id = ent_info[\"entityId\"]\n            if isinstance(ftrack_id, list):\n                self.log.warning((\n                    \"BUG REPORT: Entity info has `entityId` as `list` \\\"{}\\\"\"\n                ).format(ent_info))\n                if len(ftrack_id) == 0:\n                    continue\n                ftrack_id = ftrack_id[0]\n\n            # Skip deleted projects\n            if action == \"remove\" and entityType == \"show\":\n                return True\n\n            # task modified, collect parent id of task, handle separately\n            if entity_type.lower() == \"task\":\n                changes = ent_info.get(\"changes\") or {}\n                if action == \"move\":\n                    parent_changes = changes[\"parent_id\"]\n                    self.modified_tasks_ftrackids.add(parent_changes[\"new\"])\n                    self.modified_tasks_ftrackids.add(parent_changes[\"old\"])\n\n                elif \"typeid\" in changes or \"name\" in changes:\n                    self.modified_tasks_ftrackids.add(ent_info[\"parentId\"])\n                continue\n\n            if action == \"move\":\n                ent_keys = ent_info[\"keys\"]\n                # Separate update info from move action\n                if len(ent_keys) > 1:\n                    _ent_info = ent_info.copy()\n                    for ent_key in ent_keys:\n                        if ent_key == \"parent_id\":\n                            _ent_info[\"changes\"].pop(ent_key, None)\n                            _ent_info[\"keys\"].remove(ent_key)\n                        else:\n                            ent_info[\"changes\"].pop(ent_key, None)\n                            ent_info[\"keys\"].remove(ent_key)\n                    entities_by_action[\"update\"][ftrack_id] = _ent_info\n            # regular change process handles all other than Tasks\n            found_actions.add(action)\n            entities_by_action[action][ftrack_id] = ent_info\n\n        found_actions = list(found_actions)\n        if not found_actions and not self.modified_tasks_ftrackids:\n            return True\n\n        # Check if auto sync was turned on/off\n        updated = entities_by_action[\"update\"]\n        for ftrack_id, ent_info in updated.items():\n            # filter project\n            if ent_info[\"entityType\"] != \"show\":\n                continue\n\n            changes = ent_info[\"changes\"]\n            if CUST_ATTR_AUTO_SYNC not in changes:\n                continue\n\n            auto_sync = changes[CUST_ATTR_AUTO_SYNC][\"new\"]\n            turned_on = auto_sync == \"1\"\n            ft_project = self.cur_project\n            username = self._get_username(session, event)\n            message = (\n                \"Auto sync was turned {} for project \\\"{}\\\" by \\\"{}\\\".\"\n            ).format(\n                \"on\" if turned_on else \"off\",\n                ft_project[\"full_name\"],\n                username\n            )\n            if turned_on:\n                message += \" Triggering syncToAvalon action.\"\n            self.log.debug(message)\n\n            if turned_on:\n                # Trigger sync to avalon action if auto sync was turned on\n                selection = [{\n                    \"entityId\": ft_project[\"id\"],\n                    \"entityType\": \"show\"\n                }]\n                self.trigger_action(\n                    action_name=\"sync.to.avalon.server\",\n                    event=event,\n                    selection=selection\n                )\n            # Exit for both cases\n            return True\n\n        # Filter updated data by changed keys\n        updated = self.filter_updated(updated)\n\n        # skip most of events where nothing has changed for avalon\n        if (\n            len(found_actions) == 1\n            and found_actions[0] == \"update\"\n            and not updated\n            and not self.modified_tasks_ftrackids\n        ):\n            return True\n\n        ft_project = self.cur_project\n        # Check if auto-sync custom attribute exists\n        if CUST_ATTR_AUTO_SYNC not in ft_project[\"custom_attributes\"]:\n            # TODO should we sent message to someone?\n            self.log.error((\n                \"Custom attribute \\\"{}\\\" is not created or user \\\"{}\\\" used\"\n                \" for Event server don't have permissions to access it!\"\n            ).format(CUST_ATTR_AUTO_SYNC, self.session.api_user))\n            return True\n\n        # Skip if auto-sync is not set\n        auto_sync = ft_project[\"custom_attributes\"][CUST_ATTR_AUTO_SYNC]\n        if auto_sync is not True:\n            return True\n\n        debug_msg = \"Updated: {}\".format(len(updated))\n        debug_action_map = {\n            \"add\": \"Created\",\n            \"remove\": \"Removed\",\n            \"move\": \"Moved\"\n        }\n        for action, infos in entities_by_action.items():\n            if action == \"update\":\n                continue\n            _action = debug_action_map[action]\n            debug_msg += \"| {}: {}\".format(_action, len(infos))\n\n        self.log.debug(\"Project changes <{}>: {}\".format(\n            ft_project[\"full_name\"], debug_msg\n        ))\n        # Get ftrack entities - find all ftrack ids first\n        ftrack_ids = set(updated.keys())\n\n        for action, _ftrack_ids in entities_by_action.items():\n            # skip updated (already prepared) and removed (not exist in ftrack)\n            if action not in (\"remove\", \"update\"):\n                ftrack_ids |= set(_ftrack_ids)\n\n        # collect entity records data which might not be in event\n        if ftrack_ids:\n            joined_ids = \", \".join([\"\\\"{}\\\"\".format(id) for id in ftrack_ids])\n            ftrack_entities = self.process_session.query(\n                self.entities_query_by_id.format(ft_project[\"id\"], joined_ids)\n            ).all()\n            for entity in ftrack_entities:\n                self.ftrack_ents_by_id[entity[\"id\"]] = entity\n\n        # Filter updates where name is changing\n        for ftrack_id, ent_info in updated.items():\n            ent_keys = ent_info[\"keys\"]\n            # Separate update info from rename\n            if \"name\" not in ent_keys:\n                continue\n\n            _ent_info = copy.deepcopy(ent_info)\n            for ent_key in ent_keys:\n                if ent_key == \"name\":\n                    ent_info[\"changes\"].pop(ent_key, None)\n                    ent_info[\"keys\"].remove(ent_key)\n                else:\n                    _ent_info[\"changes\"].pop(ent_key, None)\n                    _ent_info[\"keys\"].remove(ent_key)\n\n            self.ftrack_renamed[ftrack_id] = _ent_info\n\n        self.ftrack_removed = entities_by_action[\"remove\"]\n        self.ftrack_moved = entities_by_action[\"move\"]\n        self.ftrack_added = entities_by_action[\"add\"]\n        self.ftrack_updated = updated\n\n        self.debug_logs()\n\n        self.log.debug(\"Synchronization begins\")\n        try:\n            time_1 = time.time()\n            # 1.) Process removed - may affect all other actions\n            self.process_removed()\n            time_2 = time.time()\n            # 2.) Process renamed - may affect added\n            self.process_renamed()\n            time_3 = time.time()\n            # 3.) Process added - moved entity may be moved to new entity\n            self.process_added()\n            time_4 = time.time()\n            # 4.) Process moved\n            self.process_moved()\n            time_5 = time.time()\n            # 5.) Process updated\n            self.process_updated()\n            time_6 = time.time()\n            # 6.) Process changes in hierarchy or hier custom attributes\n            self.process_hier_cleanup()\n            time_7 = time.time()\n            self.process_task_updates()\n            if self.updates:\n                self.update_entities()\n            time_8 = time.time()\n\n            time_removed = time_2 - time_1\n            time_renamed = time_3 - time_2\n            time_added = time_4 - time_3\n            time_moved = time_5 - time_4\n            time_updated = time_6 - time_5\n            time_cleanup = time_7 - time_6\n            time_task_updates = time_8 - time_7\n            time_total = time_8 - time_1\n            self.log.debug((\n                \"Process time: {:.2f} <{:.2f}, {:.2f}, {:.2f}, \"\n                \"{:.2f}, {:.2f}, {:.2f}, {:.2f}>\"\n            ).format(\n                time_total, time_removed, time_renamed, time_added,\n                time_moved, time_updated, time_cleanup, time_task_updates\n            ))\n\n        except Exception:\n            msg = \"An error has happened during synchronization\"\n            self.report_items[\"error\"][msg].append((\n                str(traceback.format_exc()).replace(\"\\n\", \"<br>\")\n            ).replace(\" \", \"&nbsp;\"))\n\n        self.report()\n        return True\n\n    def _get_username(self, session, event):\n        username = \"Unknown\"\n        event_source = event.get(\"source\")\n        if not event_source:\n            return username\n        user_info = event_source.get(\"user\")\n        if not user_info:\n            return username\n        user_id = user_info.get(\"id\")\n        if not user_id:\n            return username\n\n        user_entity = session.query(\n            \"User where id is {}\".format(user_id)\n        ).first()\n        if user_entity:\n            username = user_entity[\"username\"] or username\n        return username\n\n\n    def process_removed(self):\n        \"\"\"\n            Handles removed entities (not removed tasks - handle separately).\n        \"\"\"\n        if not self.ftrack_removed:\n            return\n        ent_infos = self.ftrack_removed\n        self.log.debug(\n            \"Processing removed entities: {}\".format(str(ent_infos))\n        )\n        removable_ids = []\n        recreate_ents = []\n        removed_names = []\n        for ftrack_id, removed in ent_infos.items():\n            entity_type = removed[\"entity_type\"]\n            if entity_type.lower() == \"task\":\n                continue\n\n            removed_name = removed[\"changes\"][\"name\"][\"old\"]\n\n            avalon_ent = self.avalon_ents_by_ftrack_id.get(ftrack_id)\n            if not avalon_ent:\n                continue\n            mongo_id = avalon_ent[\"_id\"]\n            if self.changeability_by_mongo_id[mongo_id]:\n                removable_ids.append(mongo_id)\n                removed_names.append(removed_name)\n            else:\n                recreate_ents.append(avalon_ent)\n\n        if removable_ids:\n            # TODO logging\n            self.log.debug(\"Assets marked as archived <{}>\".format(\n                \", \".join(removed_names)\n            ))\n            self.dbcon.update_many(\n                {\"_id\": {\"$in\": removable_ids}, \"type\": \"asset\"},\n                {\"$set\": {\"type\": \"archived_asset\"}}\n            )\n            self.remove_cached_by_key(\"id\", removable_ids)\n\n        if recreate_ents:\n            # sort removed entities by parents len\n            # - length of parents determine hierarchy level\n            recreate_ents = sorted(\n                recreate_ents,\n                key=(lambda item: len(\n                    (item.get(\"data\", {}).get(\"parents\") or [])\n                ))\n            )\n            # TODO logging\n            # TODO report\n            recreate_msg = (\n                \"Deleted entity was recreated||Entity was recreated because\"\n                \" it or its children contain published data\"\n            )\n            proj, ents = self.avalon_entities\n            for avalon_entity in recreate_ents:\n                old_ftrack_id = avalon_entity[\"data\"][\"ftrackId\"]\n                vis_par = avalon_entity[\"data\"][\"visualParent\"]\n                if vis_par is None:\n                    vis_par = proj[\"_id\"]\n                parent_ent = self.avalon_ents_by_id[vis_par]\n\n                parent_ftrack_id = parent_ent[\"data\"].get(\"ftrackId\")\n                if parent_ftrack_id is None:\n                    self.handle_missing_ftrack_id(parent_ent)\n                    parent_ftrack_id = parent_ent[\"data\"].get(\"ftrackId\")\n                    if parent_ftrack_id is None:\n                        continue\n\n                parent_ftrack_ent = self.ftrack_ents_by_id.get(\n                    parent_ftrack_id\n                )\n                if not parent_ftrack_ent:\n                    if parent_ent[\"type\"].lower() == \"project\":\n                        parent_ftrack_ent = self.cur_project\n                    else:\n                        parent_ftrack_ent = self.process_session.query(\n                            self.entities_query_by_id.format(\n                                self.cur_project[\"id\"], parent_ftrack_id\n                            )\n                        ).one()\n                entity_type = avalon_entity[\"data\"][\"entityType\"]\n                new_entity = self.process_session.create(entity_type, {\n                    \"name\": avalon_entity[\"name\"],\n                    \"parent\": parent_ftrack_ent\n                })\n                try:\n                    self.process_session.commit()\n                except Exception:\n                    # TODO logging\n                    # TODO report\n                    self.process_session.rollback()\n                    ent_path_items = [self.cur_project[\"full_name\"]]\n                    ent_path_items.extend([\n                        par for par in avalon_entity[\"data\"][\"parents\"]\n                    ])\n                    ent_path_items.append(avalon_entity[\"name\"])\n                    ent_path = \"/\".join(ent_path_items)\n\n                    error_msg = \"Couldn't recreate entity in Ftrack\"\n                    report_msg = (\n                        \"{}||Trying to recreate because it or its children\"\n                        \" contain published data\"\n                    ).format(error_msg)\n                    self.report_items[\"warning\"][report_msg].append(ent_path)\n                    self.log.warning(\n                        \"{}. Process session commit failed! <{}>\".format(\n                            error_msg, ent_path\n                        ),\n                        exc_info=True\n                    )\n                    continue\n\n                new_entity_id = new_entity[\"id\"]\n                avalon_entity[\"data\"][\"ftrackId\"] = new_entity_id\n\n                for key, val in avalon_entity[\"data\"].items():\n                    if not val:\n                        continue\n                    if key not in new_entity[\"custom_attributes\"]:\n                        continue\n\n                    new_entity[\"custom_attributes\"][key] = val\n\n                new_entity[\"custom_attributes\"][CUST_ATTR_ID_KEY] = (\n                    str(avalon_entity[\"_id\"])\n                )\n                ent_path = self.get_ent_path(new_entity_id)\n\n                try:\n                    self.process_session.commit()\n                except Exception:\n                    # TODO logging\n                    # TODO report\n                    self.process_session.rollback()\n                    error_msg = (\n                        \"Couldn't update custom attributes after recreation\"\n                        \" of entity in Ftrack\"\n                    )\n                    report_msg = (\n                        \"{}||Entity was recreated because it or its children\"\n                        \" contain published data\"\n                    ).format(error_msg)\n                    self.report_items[\"warning\"][report_msg].append(ent_path)\n                    self.log.warning(\n                        \"{}. Process session commit failed! <{}>\".format(\n                            error_msg, ent_path\n                        ),\n                        exc_info=True\n                    )\n                    continue\n\n                self.report_items[\"info\"][recreate_msg].append(ent_path)\n\n                self.ftrack_recreated_mapping[old_ftrack_id] = new_entity_id\n                self.process_session.commit()\n\n                found_idx = None\n                proj_doc, asset_docs = self._avalon_ents\n                for idx, asset_doc in enumerate(asset_docs):\n                    if asset_doc[\"_id\"] == avalon_entity[\"_id\"]:\n                        found_idx = idx\n                        break\n\n                if found_idx is None:\n                    continue\n\n                # Prepare updates dict for mongo update\n                if \"data\" not in self.updates[avalon_entity[\"_id\"]]:\n                    self.updates[avalon_entity[\"_id\"]][\"data\"] = {}\n\n                self.updates[avalon_entity[\"_id\"]][\"data\"][\"ftrackId\"] = (\n                    new_entity_id\n                )\n                # Update cached entities\n                asset_docs[found_idx] = avalon_entity\n                self._avalon_ents = proj_doc, asset_docs\n\n                if self._avalon_ents_by_id is not None:\n                    mongo_id = avalon_entity[\"_id\"]\n                    self._avalon_ents_by_id[mongo_id] = avalon_entity\n\n                if self._avalon_ents_by_parent_id is not None:\n                    vis_par = avalon_entity[\"data\"][\"visualParent\"]\n                    children = self._avalon_ents_by_parent_id[vis_par]\n                    found_idx = None\n                    for idx, _entity in enumerate(children):\n                        if _entity[\"_id\"] == avalon_entity[\"_id\"]:\n                            found_idx = idx\n                            break\n                    children[found_idx] = avalon_entity\n                    self._avalon_ents_by_parent_id[vis_par] = children\n\n                if self._avalon_ents_by_ftrack_id is not None:\n                    self._avalon_ents_by_ftrack_id.pop(old_ftrack_id)\n                    self._avalon_ents_by_ftrack_id[new_entity_id] = (\n                        avalon_entity\n                    )\n\n                if self._avalon_ents_by_name is not None:\n                    name = avalon_entity[\"name\"]\n                    self._avalon_ents_by_name[name] = avalon_entity\n\n        # Check if entities with same name can be synchronized\n        if not removed_names:\n            return\n\n        self.check_names_synchronizable(removed_names)\n\n    def check_names_synchronizable(self, names):\n        \"\"\"Check if entities with specific names are importable.\n\n        This check should happen after removing entity or renaming entity.\n        When entity was removed or renamed then it's name is possible to sync.\n        \"\"\"\n        joined_passed_names = \", \".join(\n            [\"\\\"{}\\\"\".format(name) for name in names]\n        )\n        same_name_entities = self.process_session.query(\n            self.entities_name_query_by_name.format(\n                self.cur_project[\"id\"], joined_passed_names\n            )\n        ).all()\n        if not same_name_entities:\n            return\n\n        entities_by_name = collections.defaultdict(list)\n        for entity in same_name_entities:\n            entities_by_name[entity[\"name\"]].append(entity)\n\n        synchronizable_ents = []\n        self.log.debug((\n            \"Deleting of entities should allow to synchronize another entities\"\n            \" with same name.\"\n        ))\n        for name, ents in entities_by_name.items():\n            if len(ents) != 1:\n                self.log.debug((\n                    \"Name \\\"{}\\\" still have more than one entity <{}>\"\n                ).format(\n                    name, \"| \".join(\n                        [self.get_ent_path(ent[\"id\"]) for ent in ents]\n                    )\n                ))\n                continue\n\n            entity = ents[0]\n            ent_path = self.get_ent_path(entity[\"id\"])\n            # TODO logging\n            self.log.debug(\n                \"Checking if can synchronize entity <{}>\".format(ent_path)\n            )\n            # skip if already synchronized\n            ftrack_id = entity[\"id\"]\n            if ftrack_id in self.avalon_ents_by_ftrack_id:\n                # TODO logging\n                self.log.debug(\n                    \"- Entity is already synchronized (skipping) <{}>\".format(\n                        ent_path\n                    )\n                )\n                continue\n\n            parent_id = entity[\"parent_id\"]\n            if parent_id not in self.avalon_ents_by_ftrack_id:\n                # TODO logging\n                self.log.debug((\n                    \"- Entity's parent entity doesn't seems to\"\n                    \" be synchronized (skipping) <{}>\"\n                ).format(ent_path))\n                continue\n\n            synchronizable_ents.append(entity)\n\n        if not synchronizable_ents:\n            return\n\n        synchronizable_ents = sorted(\n            synchronizable_ents,\n            key=(lambda entity: len(entity[\"link\"]))\n        )\n\n        children_queue = collections.deque()\n        for entity in synchronizable_ents:\n            parent_avalon_ent = self.avalon_ents_by_ftrack_id[\n                entity[\"parent_id\"]\n            ]\n            self.create_entity_in_avalon(entity, parent_avalon_ent)\n\n            for child in entity[\"children\"]:\n                if child.entity_type.lower() != \"task\":\n                    children_queue.append(child)\n\n        while children_queue:\n            entity = children_queue.popleft()\n            ftrack_id = entity[\"id\"]\n            name = entity[\"name\"]\n            ent_by_ftrack_id = self.avalon_ents_by_ftrack_id.get(ftrack_id)\n            if ent_by_ftrack_id:\n                raise Exception((\n                    \"This is bug, parent was just synchronized to avalon\"\n                    \" but entity is already in database {}\"\n                ).format(dict(entity)))\n\n            # Entity has duplicated name with another entity\n            # - may be renamed: in that case renaming method will handle that\n            duplicate_ent = self.avalon_ents_by_name.get(name)\n            if duplicate_ent:\n                continue\n\n            passed_regex = avalon_sync.check_regex(\n                name, \"asset\", schema_patterns=self.regex_schemas\n            )\n            if not passed_regex:\n                continue\n\n            parent_id = entity[\"parent_id\"]\n            parent_avalon_ent = self.avalon_ents_by_ftrack_id[parent_id]\n\n            self.create_entity_in_avalon(entity, parent_avalon_ent)\n\n            for child in entity[\"children\"]:\n                if child.entity_type.lower() == \"task\":\n                    continue\n                children_queue.append(child)\n\n    def create_entity_in_avalon(self, ftrack_ent, parent_avalon):\n        proj, ents = self.avalon_entities\n\n        # Parents, Hierarchy\n        ent_path_items = [ent[\"name\"] for ent in ftrack_ent[\"link\"]]\n        parents = ent_path_items[1:len(ent_path_items)-1:]\n\n        # TODO logging\n        self.log.debug(\n            \"Trying to synchronize entity <{}>\".format(\n                \"/\".join(ent_path_items)\n            )\n        )\n\n        # Add entity to modified so tasks are added at the end\n        self.modified_tasks_ftrackids.add(ftrack_ent[\"id\"])\n\n        # Visual Parent\n        vis_par = None\n        if parent_avalon[\"type\"].lower() != \"project\":\n            vis_par = parent_avalon[\"_id\"]\n\n        mongo_id = ObjectId()\n        name = ftrack_ent[\"name\"]\n        final_entity = {\n            \"_id\": mongo_id,\n            \"name\": name,\n            \"type\": \"asset\",\n            \"schema\": CURRENT_ASSET_DOC_SCHEMA,\n            \"parent\": proj[\"_id\"],\n            \"data\": {\n                \"ftrackId\": ftrack_ent[\"id\"],\n                \"entityType\": ftrack_ent.entity_type,\n                \"parents\": parents,\n                \"tasks\": {},\n                \"visualParent\": vis_par,\n                \"description\": ftrack_ent[\"description\"]\n            }\n        }\n        invalid_fps_items = []\n        cust_attrs = self.get_cust_attr_values(ftrack_ent)\n        for key, val in cust_attrs.items():\n            if key.startswith(\"avalon_\"):\n                continue\n\n            if key in FPS_KEYS:\n                try:\n                    val = convert_to_fps(val)\n                except InvalidFpsValue:\n                    invalid_fps_items.append((ftrack_ent[\"id\"], val))\n                    continue\n\n            final_entity[\"data\"][key] = val\n\n        if invalid_fps_items:\n            fps_msg = (\n                \"These entities have invalid fps value in custom attributes\"\n            )\n            items = []\n            for entity_id, value in invalid_fps_items:\n                ent_path = self.get_ent_path(entity_id)\n                items.append(\"{} - \\\"{}\\\"\".format(ent_path, value))\n            self.report_items[\"error\"][fps_msg] = items\n\n        _mongo_id_str = cust_attrs.get(CUST_ATTR_ID_KEY)\n        if _mongo_id_str:\n            try:\n                _mongo_id = ObjectId(_mongo_id_str)\n                if _mongo_id not in self.avalon_ents_by_id:\n                    mongo_id = _mongo_id\n                    final_entity[\"_id\"] = mongo_id\n\n            except Exception:\n                pass\n\n        ent_path_items = [self.cur_project[\"full_name\"]]\n        ent_path_items.extend([par for par in parents])\n        ent_path_items.append(name)\n        ent_path = \"/\".join(ent_path_items)\n\n        try:\n            schema.validate(final_entity)\n        except Exception:\n            # TODO logging\n            # TODO report\n            error_msg = (\n                \"Schema validation failed for new entity (This is a bug)\"\n            )\n            error_traceback = (\n                str(traceback.format_exc()).replace(\"\\n\", \"<br>\")\n            ).replace(\" \", \"&nbsp;\")\n\n            item_msg = ent_path + \"<br>\" + error_traceback\n            self.report_items[\"error\"][error_msg].append(item_msg)\n            self.log.error(\n                \"{}: \\\"{}\\\"\".format(error_msg, str(final_entity)),\n                exc_info=True\n            )\n            return None\n\n        replaced = False\n        archived = self.avalon_archived_by_name.get(name)\n        if archived:\n            archived_id = archived[\"_id\"]\n            if (\n                archived[\"data\"][\"parents\"] == parents or\n                self.changeability_by_mongo_id[archived_id]\n            ):\n                # TODO logging\n                self.log.debug(\n                    \"Entity was unarchived instead of creation <{}>\".format(\n                        ent_path\n                    )\n                )\n                mongo_id = archived_id\n                final_entity[\"_id\"] = mongo_id\n                self.dbcon.replace_one({\"_id\": mongo_id}, final_entity)\n                replaced = True\n\n        if not replaced:\n            self.dbcon.insert_one(final_entity)\n            # TODO logging\n            self.log.debug(\"Entity was synchronized <{}>\".format(ent_path))\n\n        mongo_id_str = str(mongo_id)\n        if mongo_id_str != ftrack_ent[\"custom_attributes\"][CUST_ATTR_ID_KEY]:\n            ftrack_ent[\"custom_attributes\"][CUST_ATTR_ID_KEY] = mongo_id_str\n            try:\n                self.process_session.commit()\n            except Exception:\n                self.process_session.rollback()\n                # TODO logging\n                # TODO report\n                error_msg = (\n                    \"Failed to store MongoID to entity's custom attribute\"\n                )\n                report_msg = (\n                    \"{}||SyncToAvalon action may solve this issue\"\n                ).format(error_msg)\n\n                self.report_items[\"warning\"][report_msg].append(ent_path)\n                self.log.error(\n                    \"{}: \\\"{}\\\"\".format(error_msg, ent_path),\n                    exc_info=True\n                )\n\n        # modify cached data\n        # Skip if self._avalon_ents is not set(maybe never happen)\n        if self._avalon_ents is None:\n            return final_entity\n\n        if self._avalon_ents is not None:\n            proj, ents = self._avalon_ents\n            ents.append(final_entity)\n            self._avalon_ents = (proj, ents)\n\n        if self._avalon_ents_by_id is not None:\n            self._avalon_ents_by_id[mongo_id] = final_entity\n\n        if self._avalon_ents_by_parent_id is not None:\n            self._avalon_ents_by_parent_id[vis_par].append(final_entity)\n\n        if self._avalon_ents_by_ftrack_id is not None:\n            self._avalon_ents_by_ftrack_id[ftrack_ent[\"id\"]] = final_entity\n\n        if self._avalon_ents_by_name is not None:\n            self._avalon_ents_by_name[ftrack_ent[\"name\"]] = final_entity\n\n        return final_entity\n\n    def get_cust_attr_values(self, entity):\n        output = {}\n        custom_attrs, hier_attrs = self.avalon_cust_attrs\n\n        # Notmal custom attributes\n        for attr in custom_attrs:\n            key = attr[\"key\"]\n            if key in entity[\"custom_attributes\"]:\n                output[key] = entity[\"custom_attributes\"][key]\n\n        hier_values = avalon_sync.get_hierarchical_attributes_values(\n            self.process_session,\n            entity,\n            hier_attrs,\n            self.cust_attr_types_by_id.values()\n        )\n        for key, val in hier_values.items():\n            output[key] = val\n\n        # Make sure mongo id is not set\n        output.pop(CUST_ATTR_ID_KEY, None)\n\n        return output\n\n    def process_renamed(self):\n        ent_infos = self.ftrack_renamed\n        if not ent_infos:\n            return\n\n        self.log.debug(\n            \"Processing renamed entities: {}\".format(str(ent_infos))\n        )\n\n        changeable_queue = collections.deque()\n        for ftrack_id, ent_info in ent_infos.items():\n            entity_type = ent_info[\"entity_type\"]\n            if entity_type == \"Task\":\n                continue\n\n            new_name = ent_info[\"changes\"][\"name\"][\"new\"]\n            old_name = ent_info[\"changes\"][\"name\"][\"old\"]\n\n            ent_path = self.get_ent_path(ftrack_id)\n            avalon_ent = self.avalon_ents_by_ftrack_id.get(ftrack_id)\n            if not avalon_ent:\n                # TODO logging\n                self.log.debug((\n                    \"Entity is not is avalon. Moving to \\\"add\\\" process. <{}>\"\n                ).format(ent_path))\n                self.ftrack_added[ftrack_id] = ent_info\n                continue\n\n            if new_name == avalon_ent[\"name\"]:\n                # TODO logging\n                self.log.debug((\n                    \"Avalon entity already has the same name <{}>\"\n                ).format(ent_path))\n                continue\n\n            mongo_id = avalon_ent[\"_id\"]\n            if self.changeability_by_mongo_id[mongo_id]:\n                changeable_queue.append((ftrack_id, avalon_ent, new_name))\n            else:\n                ftrack_ent = self.ftrack_ents_by_id[ftrack_id]\n                ftrack_ent[\"name\"] = avalon_ent[\"name\"]\n                try:\n                    self.process_session.commit()\n                    # TODO logging\n                    # TODO report\n                    error_msg = \"Entity renamed back\"\n                    report_msg = (\n                        \"{}||It is not possible to change\"\n                        \" the name of an entity or it's parents, \"\n                        \" if it already contained published data.\"\n                    ).format(error_msg)\n                    self.report_items[\"info\"][report_msg].append(ent_path)\n                    self.log.warning(\"{} <{}>\".format(error_msg, ent_path))\n\n                except Exception:\n                    self.process_session.rollback()\n                    # TODO report\n                    # TODO logging\n                    error_msg = (\n                        \"Couldn't rename the entity back to its original name\"\n                    )\n                    report_msg = (\n                        \"{}||Renamed because it is not possible to\"\n                        \" change the name of an entity or it's parents, \"\n                        \" if it already contained published data.\"\n                    ).format(error_msg)\n                    error_traceback = (\n                        str(traceback.format_exc()).replace(\"\\n\", \"<br>\")\n                    ).replace(\" \", \"&nbsp;\")\n\n                    item_msg = ent_path + \"<br>\" + error_traceback\n                    self.report_items[\"warning\"][report_msg].append(item_msg)\n                    self.log.warning(\n                        \"{}: \\\"{}\\\"\".format(error_msg, ent_path),\n                        exc_info=True\n                    )\n\n        old_names = []\n        # Process renaming in Avalon DB\n        while changeable_queue:\n            ftrack_id, avalon_ent, new_name = changeable_queue.popleft()\n            mongo_id = avalon_ent[\"_id\"]\n            old_name = avalon_ent[\"name\"]\n\n            _entity_type = \"asset\"\n            if entity_type == \"Project\":\n                _entity_type = \"project\"\n\n            passed_regex = avalon_sync.check_regex(\n                new_name, _entity_type, schema_patterns=self.regex_schemas\n            )\n            if not passed_regex:\n                self.regex_failed.append(ftrack_id)\n                continue\n\n            # if avalon does not have same name then can be changed\n            same_name_avalon_ent = self.avalon_ents_by_name.get(new_name)\n            if not same_name_avalon_ent:\n                old_val = self._avalon_ents_by_name.pop(old_name)\n                old_val[\"name\"] = new_name\n                self._avalon_ents_by_name[new_name] = old_val\n                self.updates[mongo_id] = {\"name\": new_name}\n                self.renamed_in_avalon.append(mongo_id)\n\n                old_names.append(old_name)\n                if new_name in old_names:\n                    old_names.remove(new_name)\n\n                # TODO logging\n                ent_path = self.get_ent_path(ftrack_id)\n                self.log.debug(\n                    \"Name of entity will be changed to \\\"{}\\\" <{}>\".format(\n                        new_name, ent_path\n                    )\n                )\n                continue\n\n            # Check if same name is in changable_queue\n            # - it's name may be changed in next iteration\n            same_name_ftrack_id = same_name_avalon_ent[\"data\"][\"ftrackId\"]\n            same_is_unprocessed = False\n            for item in changeable_queue:\n                if same_name_ftrack_id == item[0]:\n                    same_is_unprocessed = True\n                    break\n\n            if same_is_unprocessed:\n                changeable_queue.append((ftrack_id, avalon_ent, new_name))\n                continue\n\n            self.duplicated.append(ftrack_id)\n\n        if old_names:\n            self.check_names_synchronizable(old_names)\n\n        # not_found are not processed since all not found are\n        # not found because they are not synchronizable\n\n    def process_added(self):\n        ent_infos = self.ftrack_added\n        if not ent_infos:\n            return\n\n        self.log.debug(\n            \"Processing added entities: {}\".format(str(ent_infos))\n        )\n\n        cust_attrs, hier_attrs = self.avalon_cust_attrs\n        entity_type_conf_ids = {}\n        # Skip if already exit in avalon db or tasks entities\n        # - happen when was created by any sync event/action\n        pop_out_ents = []\n        for ftrack_id, ent_info in ent_infos.items():\n            if self.avalon_ents_by_ftrack_id.get(ftrack_id):\n                pop_out_ents.append(ftrack_id)\n                self.log.warning(\n                    \"Added entity is already synchronized <{}>\".format(\n                        self.get_ent_path(ftrack_id)\n                    )\n                )\n                continue\n\n            entity_type = ent_info[\"entity_type\"]\n            if entity_type == \"Task\":\n                continue\n\n            name = (\n                ent_info\n                .get(\"changes\", {})\n                .get(\"name\", {})\n                .get(\"new\")\n            )\n            avalon_ent_by_name = self.avalon_ents_by_name.get(name) or {}\n            avalon_ent_by_name_ftrack_id = (\n                avalon_ent_by_name\n                .get(\"data\", {})\n                .get(\"ftrackId\")\n            )\n            if avalon_ent_by_name and avalon_ent_by_name_ftrack_id is None:\n                ftrack_ent = self.ftrack_ents_by_id.get(ftrack_id)\n                if not ftrack_ent:\n                    ftrack_ent = self.process_session.query(\n                        self.entities_query_by_id.format(\n                            self.cur_project[\"id\"], ftrack_id\n                        )\n                    ).one()\n                    self.ftrack_ents_by_id[ftrack_id] = ftrack_ent\n\n                ent_path_items = [ent[\"name\"] for ent in ftrack_ent[\"link\"]]\n                parents = ent_path_items[1:len(ent_path_items)-1:]\n\n                avalon_ent_parents = (\n                    avalon_ent_by_name.get(\"data\", {}).get(\"parents\")\n                )\n                if parents == avalon_ent_parents:\n                    self.dbcon.update_one({\n                        \"_id\": avalon_ent_by_name[\"_id\"]\n                    }, {\n                        \"$set\": {\n                            \"data.ftrackId\": ftrack_id,\n                            \"data.entityType\": entity_type\n                        }\n                    })\n\n                    avalon_ent_by_name[\"data\"][\"ftrackId\"] = ftrack_id\n                    avalon_ent_by_name[\"data\"][\"entityType\"] = entity_type\n\n                    self._avalon_ents_by_ftrack_id[ftrack_id] = (\n                        avalon_ent_by_name\n                    )\n                    if self._avalon_ents_by_parent_id:\n                        found = None\n                        for _parent_id_, _entities_ in (\n                            self._avalon_ents_by_parent_id.items()\n                        ):\n                            for _idx_, entity in enumerate(_entities_):\n                                if entity[\"_id\"] == avalon_ent_by_name[\"_id\"]:\n                                    found = (_parent_id_, _idx_)\n                                    break\n\n                            if found:\n                                break\n\n                        if found:\n                            _parent_id_, _idx_ = found\n                            self._avalon_ents_by_parent_id[_parent_id_][\n                                _idx_] = avalon_ent_by_name\n\n                    if self._avalon_ents_by_id:\n                        self._avalon_ents_by_id[avalon_ent_by_name[\"_id\"]] = (\n                            avalon_ent_by_name\n                        )\n\n                    if self._avalon_ents_by_name:\n                        self._avalon_ents_by_name[name] = avalon_ent_by_name\n\n                    if self._avalon_ents:\n                        found = None\n                        project, entities = self._avalon_ents\n                        for _idx_, _ent_ in enumerate(entities):\n                            if _ent_[\"_id\"] != avalon_ent_by_name[\"_id\"]:\n                                continue\n                            found = _idx_\n                            break\n\n                        if found is not None:\n                            entities[found] = avalon_ent_by_name\n                            self._avalon_ents = project, entities\n\n                    pop_out_ents.append(ftrack_id)\n                    continue\n\n            mongo_id_configuration_id = self._mongo_id_configuration(\n                ent_info,\n                cust_attrs,\n                hier_attrs,\n                entity_type_conf_ids\n            )\n            if not mongo_id_configuration_id:\n                self.log.warning((\n                    \"BUG REPORT: Missing MongoID configuration for `{} < {} >`\"\n                ).format(entity_type, ent_info[\"entityType\"]))\n                continue\n\n            _entity_key = collections.OrderedDict()\n            _entity_key[\"configuration_id\"] = mongo_id_configuration_id\n            _entity_key[\"entity_id\"] = ftrack_id\n\n            self.process_session.recorded_operations.push(\n                ftrack_api.operation.UpdateEntityOperation(\n                    \"ContextCustomAttributeValue\",\n                    _entity_key,\n                    \"value\",\n                    ftrack_api.symbol.NOT_SET,\n                    \"\"\n                )\n            )\n\n        try:\n            # Commit changes of mongo_id to empty string\n            self.process_session.commit()\n            self.log.debug(\"Committing unsetting\")\n        except Exception:\n            self.process_session.rollback()\n            # TODO logging\n            msg = (\n                \"Could not set value of Custom attribute, where mongo id\"\n                \" is stored, to empty string. Ftrack ids: \\\"{}\\\"\"\n            ).format(\", \".join(ent_infos.keys()))\n            self.log.warning(msg, exc_info=True)\n\n        for ftrack_id in pop_out_ents:\n            ent_infos.pop(ftrack_id)\n\n        # sort by parents length (same as by hierarchy level)\n        _ent_infos = sorted(\n            ent_infos.values(),\n            key=(lambda ent_info: len(ent_info.get(\"parents\", [])))\n        )\n        to_sync_by_id = collections.OrderedDict()\n        for ent_info in _ent_infos:\n            ft_id = ent_info[\"entityId\"]\n            to_sync_by_id[ft_id] = self.ftrack_ents_by_id[ft_id]\n\n        # cache regex success (for tasks)\n        for ftrack_id, entity in to_sync_by_id.items():\n            if entity.entity_type.lower() == \"project\":\n                raise Exception((\n                    \"Project can't be created with event handler!\"\n                    \"This is a bug\"\n                ))\n            parent_id = entity[\"parent_id\"]\n            parent_avalon = self.avalon_ents_by_ftrack_id.get(parent_id)\n            if not parent_avalon:\n                # TODO logging\n                self.log.debug((\n                    \"Skipping synchronization of entity\"\n                    \" because parent was not found in Avalon DB <{}>\"\n                ).format(self.get_ent_path(ftrack_id)))\n                continue\n\n            is_synchonizable = True\n            name = entity[\"name\"]\n            passed_regex = avalon_sync.check_regex(\n                name, \"asset\", schema_patterns=self.regex_schemas\n            )\n            if not passed_regex:\n                self.regex_failed.append(ftrack_id)\n                is_synchonizable = False\n\n            if name in self.avalon_ents_by_name:\n                self.duplicated.append(ftrack_id)\n                is_synchonizable = False\n\n            if not is_synchonizable:\n                continue\n\n            self.create_entity_in_avalon(entity, parent_avalon)\n\n    def process_moved(self):\n        \"\"\"\n            Handles moved entities to different place in hierarchy.\n            (Not tasks - handled separately.)\n        \"\"\"\n        if not self.ftrack_moved:\n            return\n\n        self.log.debug(\n            \"Processing moved entities: {}\".format(str(self.ftrack_moved))\n        )\n\n        ftrack_moved = {k: v for k, v in sorted(\n            self.ftrack_moved.items(),\n            key=(lambda line: len(\n                (line[1].get(\"data\", {}).get(\"parents\") or [])\n            ))\n        )}\n\n        for ftrack_id, ent_info in ftrack_moved.items():\n            avalon_ent = self.avalon_ents_by_ftrack_id.get(ftrack_id)\n            if not avalon_ent:\n                continue\n\n            new_parent_id = ent_info[\"changes\"][\"parent_id\"][\"new\"]\n            old_parent_id = ent_info[\"changes\"][\"parent_id\"][\"old\"]\n\n            mongo_id = avalon_ent[\"_id\"]\n            if self.changeability_by_mongo_id[mongo_id]:\n                par_av_ent = self.avalon_ents_by_ftrack_id.get(new_parent_id)\n                if not par_av_ent:\n                    # TODO logging\n                    # TODO report\n                    ent_path_items = [self.cur_project[\"full_name\"]]\n                    ent_path_items.extend(avalon_ent[\"data\"][\"parents\"])\n                    ent_path_items.append(avalon_ent[\"name\"])\n                    ent_path = \"/\".join(ent_path_items)\n\n                    error_msg = (\n                        \"New parent of entity is not synchronized to avalon\"\n                    )\n                    report_msg = (\n                        \"{}||Parent in Avalon can't be changed. That\"\n                        \" may cause issues. Please fix parent or move entity\"\n                        \" under valid entity.\"\n                    ).format(error_msg)\n\n                    self.report_items[\"warning\"][report_msg].append(ent_path)\n                    self.log.warning(\"{} <{}>\".format(error_msg, ent_path))\n                    continue\n\n                # THIS MUST HAPPEN AFTER CREATING NEW ENTITIES !!!!\n                # - because may be moved to new created entity\n                if \"data\" not in self.updates[mongo_id]:\n                    self.updates[mongo_id][\"data\"] = {}\n\n                vis_par_id = None\n                ent_path_items = [self.cur_project[\"full_name\"]]\n                if par_av_ent[\"type\"].lower() != \"project\":\n                    vis_par_id = par_av_ent[\"_id\"]\n                    ent_path_items.extend(par_av_ent[\"data\"][\"parents\"])\n                    ent_path_items.append(par_av_ent[\"name\"])\n\n                self.updates[mongo_id][\"data\"][\"visualParent\"] = vis_par_id\n                self.moved_in_avalon.append(mongo_id)\n\n                ent_path_items.append(avalon_ent[\"name\"])\n                ent_path = \"/\".join(ent_path_items)\n                self.log.debug((\n                    \"Parent of entity ({}) was changed in avalon <{}>\"\n                    ).format(str(mongo_id), ent_path)\n                )\n\n            else:\n                avalon_ent = self.avalon_ents_by_id[mongo_id]\n                avalon_parent_id = avalon_ent[\"data\"][\"visualParent\"]\n                if avalon_parent_id is None:\n                    avalon_parent_id = avalon_ent[\"parent\"]\n\n                avalon_parent = self.avalon_ents_by_id[avalon_parent_id]\n                parent_id = avalon_parent[\"data\"][\"ftrackId\"]\n\n                # For cases when parent was deleted at the same time\n                if parent_id in self.ftrack_recreated_mapping:\n                    parent_id = (\n                        self.ftrack_recreated_mapping[parent_id]\n                    )\n\n                ftrack_ent = self.ftrack_ents_by_id.get(ftrack_id)\n                if not ftrack_ent:\n                    ftrack_ent = self.process_session.query(\n                        self.entities_query_by_id.format(\n                            self.cur_project[\"id\"], ftrack_id\n                        )\n                    ).one()\n                    self.ftrack_ents_by_id[ftrack_id] = ftrack_ent\n\n                if parent_id == ftrack_ent[\"parent_id\"]:\n                    continue\n\n                ftrack_ent[\"parent_id\"] = parent_id\n                try:\n                    self.process_session.commit()\n                    # TODO logging\n                    # TODO report\n                    msg = \"Entity was moved back\"\n                    report_msg = (\n                        \"{}||Entity can't be moved when\"\n                        \" it or its children contain published data\"\n                    ).format(msg)\n                    ent_path = self.get_ent_path(ftrack_id)\n                    self.report_items[\"info\"][report_msg].append(ent_path)\n                    self.log.warning(\"{} <{}>\".format(msg, ent_path))\n\n                except Exception:\n                    self.process_session.rollback()\n                    # TODO logging\n                    # TODO report\n                    error_msg = (\n                        \"Couldn't moved the entity back to its original parent\"\n                    )\n                    report_msg = (\n                        \"{}||Moved back because it is not possible to\"\n                        \" move with an entity or it's parents, \"\n                        \" if it already contained published data.\"\n                    ).format(error_msg)\n                    error_traceback = (\n                        str(traceback.format_exc()).replace(\"\\n\", \"<br>\")\n                    ).replace(\" \", \"&nbsp;\")\n\n                    item_msg = ent_path + \"<br>\" + error_traceback\n                    self.report_items[\"warning\"][report_msg].append(item_msg)\n                    self.log.warning(\n                        \"{}: \\\"{}\\\"\".format(error_msg, ent_path),\n                        exc_info=True\n                    )\n\n    def process_updated(self):\n        \"\"\"\n            Only custom attributes changes should get here\n        \"\"\"\n        if not self.ftrack_updated:\n            return\n\n        self.log.debug(\n            \"Processing updated entities: {}\".format(str(self.ftrack_updated))\n        )\n\n        ent_infos = self.ftrack_updated\n        ftrack_mongo_mapping = {}\n        not_found_ids = []\n        for ftrack_id, ent_info in ent_infos.items():\n            avalon_ent = self.avalon_ents_by_ftrack_id.get(ftrack_id)\n            if not avalon_ent:\n                not_found_ids.append(ftrack_id)\n                continue\n\n            ftrack_mongo_mapping[ftrack_id] = avalon_ent[\"_id\"]\n\n        for ftrack_id in not_found_ids:\n            ent_infos.pop(ftrack_id)\n\n        if not ent_infos:\n            return\n\n        cust_attrs, hier_attrs = self.avalon_cust_attrs\n        hier_attrs_by_key = {\n            attr[\"key\"]: attr\n            for attr in hier_attrs\n        }\n        cust_attrs_by_obj_id = collections.defaultdict(dict)\n        for cust_attr in cust_attrs:\n            key = cust_attr[\"key\"]\n            if key.startswith(\"avalon_\"):\n                continue\n\n            ca_ent_type = cust_attr[\"entity_type\"]\n\n            if ca_ent_type == \"show\":\n                cust_attrs_by_obj_id[ca_ent_type][key] = cust_attr\n\n            elif ca_ent_type == \"task\":\n                obj_id = cust_attr[\"object_type_id\"]\n                cust_attrs_by_obj_id[obj_id][key] = cust_attr\n\n        for ftrack_id, ent_info in ent_infos.items():\n            mongo_id = ftrack_mongo_mapping[ftrack_id]\n            entType = ent_info[\"entityType\"]\n            ent_path = self.get_ent_path(ftrack_id)\n            if entType == \"show\":\n                ent_cust_attrs = cust_attrs_by_obj_id.get(\"show\")\n            else:\n                obj_type_id = ent_info[\"objectTypeId\"]\n                ent_cust_attrs = cust_attrs_by_obj_id.get(obj_type_id)\n\n            # Ftrack's entity_type does not have defined custom attributes\n            if ent_cust_attrs is None:\n                ent_cust_attrs = {}\n\n            ent_changes = ent_info[\"changes\"]\n            if \"description\" in ent_changes:\n                if \"data\" not in self.updates[mongo_id]:\n                    self.updates[mongo_id][\"data\"] = {}\n                self.updates[mongo_id][\"data\"][\"description\"] = (\n                    ent_changes[\"description\"][\"new\"] or \"\"\n                )\n\n            for key, values in ent_changes.items():\n                if key in hier_attrs_by_key:\n                    self.hier_cust_attrs_changes[key].append(ftrack_id)\n                    continue\n\n                if key not in ent_cust_attrs:\n                    continue\n\n                value = values[\"new\"]\n                new_value = self.convert_value_by_cust_attr_conf(\n                    value, ent_cust_attrs[key]\n                )\n\n                if entType == \"show\" and key == \"applications\":\n                    # Store apps to project't config\n                    proj_apps, warnings = (\n                        avalon_sync.get_project_apps(new_value)\n                    )\n                    if \"config\" not in self.updates[mongo_id]:\n                        self.updates[mongo_id][\"config\"] = {}\n                    self.updates[mongo_id][\"config\"][\"apps\"] = proj_apps\n\n                    for msg, items in warnings.items():\n                        if not msg or not items:\n                            continue\n                        self.report_items[\"warning\"][msg] = items\n                    continue\n\n                if \"data\" not in self.updates[mongo_id]:\n                    self.updates[mongo_id][\"data\"] = {}\n                self.updates[mongo_id][\"data\"][key] = new_value\n                self.log.debug(\n                    \"Setting data value of \\\"{}\\\" to \\\"{}\\\" <{}>\".format(\n                        key, new_value, ent_path\n                    )\n                )\n\n    def convert_value_by_cust_attr_conf(self, value, cust_attr_conf):\n        type_id = cust_attr_conf[\"type_id\"]\n        cust_attr_type_name = self.cust_attr_types_by_id[type_id][\"name\"]\n        ignored = (\n            \"expression\", \"notificationtype\", \"dynamic enumerator\"\n        )\n        if cust_attr_type_name in ignored:\n            return None\n\n        if cust_attr_type_name == \"text\":\n            return value\n\n        if cust_attr_type_name == \"boolean\":\n            if value == \"1\":\n                return True\n            if value == \"0\":\n                return False\n            return bool(value)\n\n        if cust_attr_type_name == \"date\":\n            return arrow.get(value)\n\n        cust_attr_config = json.loads(cust_attr_conf[\"config\"])\n\n        if cust_attr_type_name == \"number\":\n            if cust_attr_config[\"isdecimal\"]:\n                return float(value)\n            return int(value)\n\n        if cust_attr_type_name == \"enumerator\":\n            if not cust_attr_config[\"multiSelect\"]:\n                return value\n            return value.split(\", \")\n        return value\n\n    def process_hier_cleanup(self):\n        if (\n            not self.moved_in_avalon and\n            not self.renamed_in_avalon and\n            not self.hier_cust_attrs_changes\n        ):\n            return\n\n        parent_changes = []\n        hier_cust_attrs_ids = []\n        hier_cust_attrs_keys = []\n        all_keys = False\n        for mongo_id in self.moved_in_avalon:\n            parent_changes.append(mongo_id)\n            hier_cust_attrs_ids.append(mongo_id)\n            all_keys = True\n\n        for mongo_id in self.renamed_in_avalon:\n            if mongo_id not in parent_changes:\n                parent_changes.append(mongo_id)\n\n        for key, ftrack_ids in self.hier_cust_attrs_changes.items():\n            if key.startswith(\"avalon_\"):\n                continue\n            for ftrack_id in ftrack_ids:\n                avalon_ent = self.avalon_ents_by_ftrack_id[ftrack_id]\n                mongo_id = avalon_ent[\"_id\"]\n                if mongo_id in hier_cust_attrs_ids:\n                    continue\n                hier_cust_attrs_ids.append(mongo_id)\n                if not all_keys and key not in hier_cust_attrs_keys:\n                    hier_cust_attrs_keys.append(key)\n\n        # Parents preparation ***\n        mongo_to_ftrack_parents = {}\n        missing_ftrack_ents = {}\n        for mongo_id in parent_changes:\n            avalon_ent = self.avalon_ents_by_id[mongo_id]\n            ftrack_id = avalon_ent[\"data\"][\"ftrackId\"]\n            if ftrack_id not in self.ftrack_ents_by_id:\n                missing_ftrack_ents[ftrack_id] = mongo_id\n                continue\n            ftrack_ent = self.ftrack_ents_by_id[ftrack_id]\n            mongo_to_ftrack_parents[mongo_id] = len(ftrack_ent[\"link\"])\n\n        if missing_ftrack_ents:\n            joine_ids = \", \".join(\n                [\"\\\"{}\\\"\".format(id) for id in missing_ftrack_ents.keys()]\n            )\n            entities = self.process_session.query(\n                self.entities_query_by_id.format(\n                    self.cur_project[\"id\"], joine_ids\n                )\n            ).all()\n            for entity in entities:\n                ftrack_id = entity[\"id\"]\n                self.ftrack_ents_by_id[ftrack_id] = entity\n                mongo_id = missing_ftrack_ents[ftrack_id]\n                mongo_to_ftrack_parents[mongo_id] = len(entity[\"link\"])\n\n        stored_parents_by_mongo = {}\n        # sort by hierarchy level\n        mongo_to_ftrack_parents = [k for k, v in sorted(\n            mongo_to_ftrack_parents.items(),\n            key=(lambda item: item[1])\n        )]\n        self.log.debug(\n            \"Updating parents and hieararchy because of name/parenting changes\"\n        )\n        for mongo_id in mongo_to_ftrack_parents:\n            avalon_ent = self.avalon_ents_by_id[mongo_id]\n            vis_par = avalon_ent[\"data\"][\"visualParent\"]\n            if vis_par in stored_parents_by_mongo:\n                parents = [par for par in stored_parents_by_mongo[vis_par]]\n                if vis_par is not None:\n                    parent_ent = self.avalon_ents_by_id[vis_par]\n                    parents.append(parent_ent[\"name\"])\n                stored_parents_by_mongo[mongo_id] = parents\n                continue\n\n            ftrack_id = avalon_ent[\"data\"][\"ftrackId\"]\n            ftrack_ent = self.ftrack_ents_by_id[ftrack_id]\n            ent_path_items = [ent[\"name\"] for ent in ftrack_ent[\"link\"]]\n            parents = ent_path_items[1:len(ent_path_items)-1:]\n            stored_parents_by_mongo[mongo_id] = parents\n\n        for mongo_id, parents in stored_parents_by_mongo.items():\n            avalon_ent = self.avalon_ents_by_id[mongo_id]\n            cur_par = avalon_ent[\"data\"][\"parents\"]\n            if cur_par == parents:\n                continue\n\n            if \"data\" not in self.updates[mongo_id]:\n                self.updates[mongo_id][\"data\"] = {}\n            self.updates[mongo_id][\"data\"][\"parents\"] = parents\n\n        # Skip custom attributes if didn't change\n        if not hier_cust_attrs_ids:\n            # TODO logging\n            self.log.debug(\n                \"Hierarchical attributes were not changed. Skipping\"\n            )\n            self.update_entities()\n            return\n\n        _, hier_attrs = self.avalon_cust_attrs\n\n        # Hierarchical custom attributes preparation ***\n        hier_attr_key_by_id = {\n            attr[\"id\"]: attr[\"key\"]\n            for attr in hier_attrs\n        }\n        hier_attr_id_by_key = {\n            key: attr_id\n            for attr_id, key in hier_attr_key_by_id.items()\n        }\n\n        if all_keys:\n            hier_cust_attrs_keys = [\n                key\n                for key in hier_attr_id_by_key.keys()\n                if not key.startswith(\"avalon_\")\n            ]\n\n        mongo_ftrack_mapping = {}\n        cust_attrs_ftrack_ids = []\n        # ftrack_parenting = collections.defaultdict(list)\n        entities_dict = collections.defaultdict(dict)\n\n        children_queue = collections.deque()\n        parent_queue = collections.deque()\n\n        for mongo_id in hier_cust_attrs_ids:\n            avalon_ent = self.avalon_ents_by_id[mongo_id]\n            parent_queue.append(avalon_ent)\n            ftrack_id = avalon_ent[\"data\"][\"ftrackId\"]\n            if ftrack_id not in entities_dict:\n                entities_dict[ftrack_id] = {\n                    \"children\": [],\n                    \"parent_id\": None,\n                    \"hier_attrs\": {}\n                }\n\n            mongo_ftrack_mapping[mongo_id] = ftrack_id\n            cust_attrs_ftrack_ids.append(ftrack_id)\n            children_ents = self.avalon_ents_by_parent_id.get(mongo_id) or []\n            for children_ent in children_ents:\n                _ftrack_id = children_ent[\"data\"][\"ftrackId\"]\n                if _ftrack_id in entities_dict:\n                    continue\n\n                entities_dict[_ftrack_id] = {\n                    \"children\": [],\n                    \"parent_id\": None,\n                    \"hier_attrs\": {}\n                }\n                # if _ftrack_id not in ftrack_parenting[ftrack_id]:\n                #     ftrack_parenting[ftrack_id].append(_ftrack_id)\n                entities_dict[_ftrack_id][\"parent_id\"] = ftrack_id\n                if _ftrack_id not in entities_dict[ftrack_id][\"children\"]:\n                    entities_dict[ftrack_id][\"children\"].append(_ftrack_id)\n                children_queue.append(children_ent)\n\n        while children_queue:\n            avalon_ent = children_queue.popleft()\n            mongo_id = avalon_ent[\"_id\"]\n            ftrack_id = avalon_ent[\"data\"][\"ftrackId\"]\n            if ftrack_id in cust_attrs_ftrack_ids:\n                continue\n\n            mongo_ftrack_mapping[mongo_id] = ftrack_id\n            cust_attrs_ftrack_ids.append(ftrack_id)\n\n            children_ents = self.avalon_ents_by_parent_id.get(mongo_id) or []\n            for children_ent in children_ents:\n                _ftrack_id = children_ent[\"data\"][\"ftrackId\"]\n                if _ftrack_id in entities_dict:\n                    continue\n\n                entities_dict[_ftrack_id] = {\n                    \"children\": [],\n                    \"parent_id\": None,\n                    \"hier_attrs\": {}\n                }\n                entities_dict[_ftrack_id][\"parent_id\"] = ftrack_id\n                if _ftrack_id not in entities_dict[ftrack_id][\"children\"]:\n                    entities_dict[ftrack_id][\"children\"].append(_ftrack_id)\n                children_queue.append(children_ent)\n\n        while parent_queue:\n            avalon_ent = parent_queue.popleft()\n            if avalon_ent[\"type\"].lower() == \"project\":\n                continue\n\n            ftrack_id = avalon_ent[\"data\"][\"ftrackId\"]\n\n            vis_par = avalon_ent[\"data\"][\"visualParent\"]\n            if vis_par is None:\n                vis_par = avalon_ent[\"parent\"]\n\n            parent_ent = self.avalon_ents_by_id[vis_par]\n            parent_ftrack_id = parent_ent[\"data\"].get(\"ftrackId\")\n            if parent_ftrack_id is None:\n                self.handle_missing_ftrack_id(parent_ent)\n                parent_ftrack_id = parent_ent[\"data\"].get(\"ftrackId\")\n                if parent_ftrack_id is None:\n                    continue\n\n            if parent_ftrack_id not in entities_dict:\n                entities_dict[parent_ftrack_id] = {\n                    \"children\": [],\n                    \"parent_id\": None,\n                    \"hier_attrs\": {}\n                }\n\n            if ftrack_id not in entities_dict[parent_ftrack_id][\"children\"]:\n                entities_dict[parent_ftrack_id][\"children\"].append(ftrack_id)\n\n            entities_dict[ftrack_id][\"parent_id\"] = parent_ftrack_id\n\n            if parent_ftrack_id in cust_attrs_ftrack_ids:\n                continue\n            mongo_ftrack_mapping[vis_par] = parent_ftrack_id\n            cust_attrs_ftrack_ids.append(parent_ftrack_id)\n            # if ftrack_id not in ftrack_parenting[parent_ftrack_id]:\n            #     ftrack_parenting[parent_ftrack_id].append(ftrack_id)\n\n            parent_queue.append(parent_ent)\n\n        # Prepare values to query\n        configuration_ids = set()\n        for key in hier_cust_attrs_keys:\n            configuration_ids.add(hier_attr_id_by_key[key])\n\n        values = query_custom_attributes(\n            self.process_session,\n            configuration_ids,\n            cust_attrs_ftrack_ids,\n            True\n        )\n\n        ftrack_project_id = self.cur_project[\"id\"]\n\n        attr_types_by_id = self.cust_attr_types_by_id\n        convert_types_by_id = {}\n        for attr in hier_attrs:\n            key = attr[\"key\"]\n            if key not in hier_cust_attrs_keys:\n                continue\n\n            type_id = attr[\"type_id\"]\n            attr_id = attr[\"id\"]\n            cust_attr_type_name = attr_types_by_id[type_id][\"name\"]\n            convert_type = avalon_sync.get_python_type_for_custom_attribute(\n                attr, cust_attr_type_name\n            )\n\n            convert_types_by_id[attr_id] = convert_type\n            default_value = attr[\"default\"]\n            if key in FPS_KEYS:\n                try:\n                    default_value = convert_to_fps(default_value)\n                except InvalidFpsValue:\n                    pass\n\n            entities_dict[ftrack_project_id][\"hier_attrs\"][key] = (\n                attr[\"default\"]\n            )\n\n        # PREPARE DATA BEFORE THIS\n        invalid_fps_items = []\n        avalon_hier = []\n        for item in values:\n            value = item[\"value\"]\n            if value is None:\n                continue\n            entity_id = item[\"entity_id\"]\n            configuration_id = item[\"configuration_id\"]\n\n            convert_type = convert_types_by_id[configuration_id]\n            key = hier_attr_key_by_id[configuration_id]\n\n            if convert_type:\n                value = convert_type(value)\n\n            if key in FPS_KEYS:\n                try:\n                    value = convert_to_fps(value)\n                except InvalidFpsValue:\n                    invalid_fps_items.append((entity_id, value))\n                    continue\n            entities_dict[entity_id][\"hier_attrs\"][key] = value\n\n        if invalid_fps_items:\n            fps_msg = (\n                \"These entities have invalid fps value in custom attributes\"\n            )\n            items = []\n            for entity_id, value in invalid_fps_items:\n                ent_path = self.get_ent_path(entity_id)\n                items.append(\"{} - \\\"{}\\\"\".format(ent_path, value))\n            self.report_items[\"error\"][fps_msg] = items\n\n        # Get dictionary with not None hierarchical values to pull to children\n        project_values = {}\n        for key, value in (\n            entities_dict[ftrack_project_id][\"hier_attrs\"].items()\n        ):\n            if value is not None:\n                project_values[key] = value\n\n        for key in avalon_hier:\n            value = entities_dict[ftrack_project_id][\"avalon_attrs\"][key]\n            if value is not None:\n                project_values[key] = value\n\n        hier_down_queue = collections.deque()\n        hier_down_queue.append(\n            (project_values, ftrack_project_id)\n        )\n\n        while hier_down_queue:\n            hier_values, parent_id = hier_down_queue.popleft()\n            for child_id in entities_dict[parent_id][\"children\"]:\n                _hier_values = hier_values.copy()\n                for name in hier_cust_attrs_keys:\n                    value = entities_dict[child_id][\"hier_attrs\"].get(name)\n                    if value is not None:\n                        _hier_values[name] = value\n\n                entities_dict[child_id][\"hier_attrs\"].update(_hier_values)\n                hier_down_queue.append((_hier_values, child_id))\n\n        ftrack_mongo_mapping = {}\n        for mongo_id, ftrack_id in mongo_ftrack_mapping.items():\n            ftrack_mongo_mapping[ftrack_id] = mongo_id\n\n        for ftrack_id, data in entities_dict.items():\n            mongo_id = ftrack_mongo_mapping[ftrack_id]\n            avalon_ent = self.avalon_ents_by_id[mongo_id]\n            ent_path = self.get_ent_path(ftrack_id)\n            # TODO logging\n            self.log.debug(\n                \"Updating hierarchical attributes <{}>\".format(ent_path)\n            )\n            for key, value in data[\"hier_attrs\"].items():\n                if (\n                    key in avalon_ent[\"data\"] and\n                    avalon_ent[\"data\"][key] == value\n                ):\n                    continue\n\n                self.log.debug(\"- {}: {}\".format(key, value))\n                if \"data\" not in self.updates[mongo_id]:\n                    self.updates[mongo_id][\"data\"] = {}\n\n                self.updates[mongo_id][\"data\"][key] = value\n\n        self.update_entities()\n\n    def process_task_updates(self):\n        \"\"\"\n            Pull task information for selected ftrack ids to replace stored\n            existing in Avalon.\n            Solves problem of changing type (even Status in the future) of\n            task without storing ftrack id for task in the DB. (Which doesn't\n            bring much advantage currently and it could be troublesome for\n            all hosts or plugins (for example Nuke) to collect and store.\n        Returns:\n            None\n        \"\"\"\n        self.log.debug(\n            \"Processing task changes for parents: {}\".format(\n                self.modified_tasks_ftrackids\n            )\n        )\n        if not self.modified_tasks_ftrackids:\n            return\n\n        joined_ids = \", \".join([\n            \"\\\"{}\\\"\".format(ftrack_id)\n            for ftrack_id in self.modified_tasks_ftrackids\n        ])\n        task_entities = self.process_session.query(\n            self.task_entities_query_by_parent_id.format(\n                self.cur_project[\"id\"], joined_ids\n            )\n        ).all()\n\n        ftrack_mongo_mapping_found = {}\n        not_found_ids = []\n        # Make sure all parents have updated tasks, as they may not have any\n        tasks_per_ftrack_id = {\n            ftrack_id: {}\n            for ftrack_id in self.modified_tasks_ftrackids\n        }\n\n        # Query all task types at once\n        task_types = self.process_session.query(self.task_types_query).all()\n        task_types_by_id = {\n            task_type[\"id\"]: task_type\n            for task_type in task_types\n        }\n\n        # prepare all tasks per parentId, eg. Avalon asset record\n        for task_entity in task_entities:\n            task_type = task_types_by_id[task_entity[\"type_id\"]]\n            ftrack_id = task_entity[\"parent_id\"]\n            if ftrack_id not in tasks_per_ftrack_id:\n                tasks_per_ftrack_id[ftrack_id] = {}\n\n            passed_regex = avalon_sync.check_regex(\n                task_entity[\"name\"], \"task\",\n                schema_patterns=self.regex_schemas\n            )\n            if not passed_regex:\n                self.regex_failed.append(task_entity[\"id\"])\n                continue\n\n            tasks_per_ftrack_id[ftrack_id][task_entity[\"name\"]] = {\n                \"type\": task_type[\"name\"]\n            }\n\n        # find avalon entity by parentId\n        # should be there as create was run first\n        for ftrack_id in tasks_per_ftrack_id.keys():\n            avalon_entity = self.avalon_ents_by_ftrack_id.get(ftrack_id)\n            if not avalon_entity:\n                not_found_ids.append(ftrack_id)\n                continue\n            ftrack_mongo_mapping_found[ftrack_id] = avalon_entity[\"_id\"]\n\n        self._update_avalon_tasks(\n            ftrack_mongo_mapping_found,\n            tasks_per_ftrack_id\n        )\n\n    def update_entities(self):\n        \"\"\"\n            Update Avalon entities by mongo bulk changes.\n            Expects self.updates which are transferred to $set part of update\n            command.\n            Resets self.updates afterwards.\n        \"\"\"\n        mongo_changes_bulk = []\n        for mongo_id, changes in self.updates.items():\n            avalon_ent = self.avalon_ents_by_id[mongo_id]\n            is_project = avalon_ent[\"type\"] == \"project\"\n            change_data = avalon_sync.from_dict_to_set(changes, is_project)\n            mongo_changes_bulk.append(\n                UpdateOne({\"_id\": mongo_id}, change_data)\n            )\n\n        if not mongo_changes_bulk:\n            return\n\n        self.dbcon.bulk_write(mongo_changes_bulk)\n        self.updates = collections.defaultdict(dict)\n\n    @property\n    def duplicated_report(self):\n        if not self.duplicated:\n            return []\n\n        ft_project = self.cur_project\n        duplicated_names = []\n        for ftrack_id in self.duplicated:\n            ftrack_ent = self.ftrack_ents_by_id.get(ftrack_id)\n            if not ftrack_ent:\n                ftrack_ent = self.process_session.query(\n                    self.entities_query_by_id.format(\n                        ft_project[\"id\"], ftrack_id\n                    )\n                ).one()\n                self.ftrack_ents_by_id[ftrack_id] = ftrack_ent\n            name = ftrack_ent[\"name\"]\n            if name not in duplicated_names:\n                duplicated_names.append(name)\n\n        joined_names = \", \".join(\n            [\"\\\"{}\\\"\".format(name) for name in duplicated_names]\n        )\n        ft_ents = self.process_session.query(\n            self.entities_name_query_by_name.format(\n                ft_project[\"id\"], joined_names\n            )\n        ).all()\n\n        ft_ents_by_name = collections.defaultdict(list)\n        for ft_ent in ft_ents:\n            name = ft_ent[\"name\"]\n            ft_ents_by_name[name].append(ft_ent)\n\n        if not ft_ents_by_name:\n            return []\n\n        subtitle = \"Duplicated entity names:\"\n        items = []\n        items.append({\n            \"type\": \"label\",\n            \"value\": \"# {}\".format(subtitle)\n        })\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<p><i>NOTE: It is not allowed to use the same name\"\n                \" for multiple entities in the same project</i></p>\"\n            )\n        })\n\n        for name, ents in ft_ents_by_name.items():\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"## {}\".format(name)\n            })\n            paths = []\n            for ent in ents:\n                ftrack_id = ent[\"id\"]\n                ent_path = \"/\".join([_ent[\"name\"] for _ent in ent[\"link\"]])\n                avalon_ent = self.avalon_ents_by_id.get(ftrack_id)\n\n                if avalon_ent:\n                    additional = \" (synchronized)\"\n                    if avalon_ent[\"name\"] != name:\n                        additional = \" (synchronized as {})\".format(\n                            avalon_ent[\"name\"]\n                        )\n                    ent_path += additional\n                paths.append(ent_path)\n\n            items.append({\n                \"type\": \"label\",\n                \"value\": '<p>{}</p>'.format(\"<br>\".join(paths))\n            })\n\n        return items\n\n    @property\n    def regex_report(self):\n        if not self.regex_failed:\n            return []\n\n        subtitle = \"Entity names contain prohibited symbols:\"\n        items = []\n        items.append({\n            \"type\": \"label\",\n            \"value\": \"# {}\".format(subtitle)\n        })\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<p><i>NOTE: You can use Letters( a-Z ),\"\n                \" Numbers( 0-9 ) and Underscore( _ )</i></p>\"\n            )\n        })\n\n        ft_project = self.cur_project\n        for ftrack_id in self.regex_failed:\n            ftrack_ent = self.ftrack_ents_by_id.get(ftrack_id)\n            if not ftrack_ent:\n                ftrack_ent = self.process_session.query(\n                    self.entities_query_by_id.format(\n                        ft_project[\"id\"], ftrack_id\n                    )\n                ).one()\n                self.ftrack_ents_by_id[ftrack_id] = ftrack_ent\n\n            name = ftrack_ent[\"name\"]\n            ent_path_items = [_ent[\"name\"] for _ent in ftrack_ent[\"link\"][:-1]]\n            ent_path_items.append(\"<strong>{}</strong>\".format(name))\n            ent_path = \"/\".join(ent_path_items)\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"<p>{} - {}</p>\".format(name, ent_path)\n            })\n\n        return items\n\n    def report(self):\n        msg_len = len(self.duplicated) + len(self.regex_failed)\n        for msgs in self.report_items.values():\n            msg_len += len(msgs)\n\n        if msg_len == 0:\n            return\n\n        items = []\n        project_name = self.cur_project[\"full_name\"]\n        title = \"Synchronization report ({}):\".format(project_name)\n\n        keys = [\"error\", \"warning\", \"info\"]\n        for key in keys:\n            subitems = []\n            if key == \"warning\":\n                subitems.extend(self.duplicated_report)\n                subitems.extend(self.regex_report)\n\n            for _msg, _items in self.report_items[key].items():\n                if not _items:\n                    continue\n\n                msg_items = _msg.split(\"||\")\n                msg = msg_items[0]\n                subitems.append({\n                    \"type\": \"label\",\n                    \"value\": \"# {}\".format(msg)\n                })\n\n                if len(msg_items) > 1:\n                    for note in msg_items[1:]:\n                        subitems.append({\n                            \"type\": \"label\",\n                            \"value\": \"<p><i>NOTE: {}</i></p>\".format(note)\n                        })\n\n                if isinstance(_items, str):\n                    _items = [_items]\n                subitems.append({\n                    \"type\": \"label\",\n                    \"value\": '<p>{}</p>'.format(\"<br>\".join(_items))\n                })\n\n            if items and subitems:\n                items.append(self.report_splitter)\n\n            items.extend(subitems)\n\n        self.show_interface(\n            items=items,\n            title=title,\n            event=self._cur_event\n        )\n        return True\n\n    def _update_avalon_tasks(\n        self, ftrack_mongo_mapping_found, tasks_per_ftrack_id\n    ):\n        \"\"\"\n            Prepare new \"tasks\" content for existing records in Avalon.\n        Args:\n            ftrack_mongo_mapping_found (dictionary): ftrack parentId to\n                Avalon _id mapping\n            tasks_per_ftrack_id (dictionary): task dictionaries per ftrack\n                parentId\n\n        Returns:\n            None\n        \"\"\"\n        mongo_changes_bulk = []\n        for ftrack_id, mongo_id in ftrack_mongo_mapping_found.items():\n            filter = {\"_id\": mongo_id}\n            change_data = {\"$set\": {}}\n            change_data[\"$set\"][\"data.tasks\"] = tasks_per_ftrack_id[ftrack_id]\n            mongo_changes_bulk.append(UpdateOne(filter, change_data))\n\n        if mongo_changes_bulk:\n            self.dbcon.bulk_write(mongo_changes_bulk)\n\n    def _mongo_id_configuration(\n        self,\n        ent_info,\n        cust_attrs,\n        hier_attrs,\n        temp_dict\n    ):\n        # Use hierarchical mongo id attribute if possible.\n        if \"_hierarchical\" not in temp_dict:\n            hier_mongo_id_configuration_id = None\n            for attr in hier_attrs:\n                if attr[\"key\"] == CUST_ATTR_ID_KEY:\n                    hier_mongo_id_configuration_id = attr[\"id\"]\n                    break\n            temp_dict[\"_hierarchical\"] = hier_mongo_id_configuration_id\n\n        hier_mongo_id_configuration_id = temp_dict.get(\"_hierarchical\")\n        if hier_mongo_id_configuration_id is not None:\n            return hier_mongo_id_configuration_id\n\n        # Legacy part for cases that MongoID attribute is per entity type.\n        entity_type = ent_info[\"entity_type\"]\n        mongo_id_configuration_id = temp_dict.get(entity_type)\n        if mongo_id_configuration_id is not None:\n            return mongo_id_configuration_id\n\n        for attr in cust_attrs:\n            key = attr[\"key\"]\n            if key != CUST_ATTR_ID_KEY:\n                continue\n\n            if attr[\"entity_type\"] != ent_info[\"entityType\"]:\n                continue\n\n            if (\n                ent_info[\"entityType\"] == \"task\" and\n                attr[\"object_type_id\"] != ent_info[\"objectTypeId\"]\n            ):\n                continue\n\n            mongo_id_configuration_id = attr[\"id\"]\n            break\n\n        temp_dict[entity_type] = mongo_id_configuration_id\n\n        return mongo_id_configuration_id\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n    SyncToAvalonEvent(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py",
    "content": "import collections\n\nfrom openpype.client import get_project\nfrom openpype_modules.ftrack.lib import BaseEvent\n\n\nclass TaskStatusToParent(BaseEvent):\n    settings_key = \"status_task_to_parent\"\n\n    def launch(self, session, event):\n        \"\"\"Propagates status from task to parent when changed.\"\"\"\n\n        filtered_entities_info = self.filter_entities_info(event)\n        if not filtered_entities_info:\n            return\n\n        for project_id, entities_info in filtered_entities_info.items():\n            self.process_by_project(session, event, project_id, entities_info)\n\n    def filter_entities_info(self, event):\n        # Filter if event contain relevant data\n        entities_info = event[\"data\"].get(\"entities\")\n        if not entities_info:\n            return\n\n        filtered_entity_info = collections.defaultdict(list)\n        status_ids = set()\n        for entity_info in entities_info:\n            # Care only about tasks\n            if entity_info.get(\"entityType\") != \"task\":\n                continue\n\n            # Care only about changes of status\n            changes = entity_info.get(\"changes\")\n            if not changes:\n                continue\n            statusid_changes = changes.get(\"statusid\")\n            if not statusid_changes:\n                continue\n\n            new_status_id = entity_info[\"changes\"][\"statusid\"][\"new\"]\n            if (\n                statusid_changes.get(\"old\") is None\n                or new_status_id is None\n            ):\n                continue\n\n            project_id = None\n            for parent_item in reversed(entity_info[\"parents\"]):\n                if parent_item[\"entityType\"] == \"show\":\n                    project_id = parent_item[\"entityId\"]\n                    break\n\n            if project_id:\n                filtered_entity_info[project_id].append(entity_info)\n                status_ids.add(new_status_id)\n\n        return filtered_entity_info\n\n    def process_by_project(self, session, event, project_id, entities_info):\n        # Get project name\n        project_name = self.get_project_name_from_event(\n            session, event, project_id\n        )\n        if get_project(project_name) is None:\n            self.log.debug(\"Project not found in OpenPype. Skipping\")\n            return\n\n        # Load settings\n        project_settings = self.get_project_settings_from_event(\n            event, project_name\n        )\n\n        # Prepare loaded settings and check if can be processed\n        result = self.prepare_settings(project_settings, project_name)\n        if not result:\n            return\n\n        # Unpack the result\n        parent_object_types, all_match, single_match = result\n\n        # Prepare valid object type ids for object types from settings\n        object_types = session.query(\"select id, name from ObjectType\").all()\n        object_type_id_by_low_name = {\n            object_type[\"name\"].lower(): object_type[\"id\"]\n            for object_type in object_types\n        }\n\n        valid_object_type_ids = set()\n        for object_type_name in parent_object_types:\n            if object_type_name in object_type_id_by_low_name:\n                valid_object_type_ids.add(\n                    object_type_id_by_low_name[object_type_name]\n                )\n            else:\n                self.log.warning(\n                    \"Unknown object type \\\"{}\\\" set on project \\\"{}\\\".\".format(\n                        object_type_name, project_name\n                    )\n                )\n\n        if not valid_object_type_ids:\n            return\n\n        # Prepare parent ids\n        parent_ids = set()\n        for entity_info in entities_info:\n            parent_id = entity_info[\"parentId\"]\n            if parent_id:\n                parent_ids.add(parent_id)\n\n        # Query parent ids by object type ids and parent ids\n        parent_entities = session.query(\n            (\n                \"select id, status_id, object_type_id, link from TypedContext\"\n                \" where id in ({}) and object_type_id in ({})\"\n            ).format(\n                self.join_query_keys(parent_ids),\n                self.join_query_keys(valid_object_type_ids)\n            )\n        ).all()\n        # Skip if none of parents match the filtering\n        if not parent_entities:\n            return\n\n        obj_ids = set()\n        for entity in parent_entities:\n            obj_ids.add(entity[\"object_type_id\"])\n\n        types_mapping = {\n            _type.lower(): _type\n            for _type in session.types\n        }\n        # Map object type id by lowered and modified object type name\n        object_type_name_by_id = {}\n        for object_type in object_types:\n            mapping_name = object_type[\"name\"].lower().replace(\" \", \"\")\n            obj_id = object_type[\"id\"]\n            object_type_name_by_id[obj_id] = types_mapping[mapping_name]\n\n        project_entity = session.get(\"Project\", project_id)\n        project_schema = project_entity[\"project_schema\"]\n        available_statuses_by_obj_id = {}\n        for obj_id in obj_ids:\n            obj_name = object_type_name_by_id[obj_id]\n            statuses = project_schema.get_statuses(obj_name)\n            statuses_by_low_name = {\n                status[\"name\"].lower(): status\n                for status in statuses\n            }\n            valid = False\n            for name in all_match.keys():\n                if name in statuses_by_low_name:\n                    valid = True\n                    break\n\n            if not valid:\n                for item in single_match:\n                    if item[\"new_status\"] in statuses_by_low_name:\n                        valid = True\n                        break\n            if valid:\n                available_statuses_by_obj_id[obj_id] = statuses_by_low_name\n\n        valid_parent_ids = set()\n        status_ids = set()\n        valid_parent_entities = []\n        for entity in parent_entities:\n            if entity[\"object_type_id\"] not in available_statuses_by_obj_id:\n                continue\n\n            valid_parent_entities.append(entity)\n            valid_parent_ids.add(entity[\"id\"])\n            status_ids.add(entity[\"status_id\"])\n\n        if not valid_parent_ids:\n            return\n\n        task_entities = session.query(\n            (\n                \"select id, parent_id, status_id from TypedContext\"\n                \" where parent_id in ({}) and object_type_id is \\\"{}\\\"\"\n            ).format(\n                self.join_query_keys(valid_parent_ids),\n                object_type_id_by_low_name[\"task\"]\n            )\n        ).all()\n\n        # This should not happen but it is safer\n        if not task_entities:\n            return\n\n        task_entities_by_parent_id = collections.defaultdict(list)\n        for task_entity in task_entities:\n            status_ids.add(task_entity[\"status_id\"])\n            parent_id = task_entity[\"parent_id\"]\n            task_entities_by_parent_id[parent_id].append(task_entity)\n\n        status_entities = session.query((\n            \"select id, name from Status where id in ({})\"\n        ).format(self.join_query_keys(status_ids))).all()\n\n        statuses_by_id = {\n            entity[\"id\"]: entity\n            for entity in status_entities\n        }\n\n        # New status determination logic\n        new_statuses_by_parent_id = self.new_status_by_all_task_statuses(\n            task_entities_by_parent_id, statuses_by_id, all_match\n        )\n\n        task_entities_by_id = {\n            task_entity[\"id\"]: task_entity\n            for task_entity in task_entities\n        }\n        # Check if there are remaining any parents that does not have\n        # determined new status yet\n        remainder_tasks_by_parent_id = collections.defaultdict(list)\n        for entity_info in entities_info:\n            entity_id = entity_info[\"entityId\"]\n            if entity_id not in task_entities_by_id:\n                continue\n            parent_id = entity_info[\"parentId\"]\n            if (\n                # Skip if already has determined new status\n                parent_id in new_statuses_by_parent_id\n                # Skip if parent is not in parent mapping\n                # - if was not found or parent type is not interesting\n                or parent_id not in task_entities_by_parent_id\n            ):\n                continue\n\n            remainder_tasks_by_parent_id[parent_id].append(\n                task_entities_by_id[entity_id]\n            )\n\n        # Try to find new status for remained parents\n        new_statuses_by_parent_id.update(\n            self.new_status_by_remainders(\n                remainder_tasks_by_parent_id,\n                statuses_by_id,\n                single_match\n            )\n        )\n\n        # If there are not new statuses then just skip\n        if not new_statuses_by_parent_id:\n            return\n\n        parent_entities_by_id = {\n            parent_entity[\"id\"]: parent_entity\n            for parent_entity in valid_parent_entities\n        }\n        for parent_id, new_status_name in new_statuses_by_parent_id.items():\n            if not new_status_name:\n                continue\n\n            parent_entity = parent_entities_by_id[parent_id]\n            ent_path = \"/\".join(\n                [ent[\"name\"] for ent in parent_entity[\"link\"]]\n            )\n\n            obj_id = parent_entity[\"object_type_id\"]\n            statuses_by_low_name = available_statuses_by_obj_id.get(obj_id)\n            if not statuses_by_low_name:\n                continue\n\n            new_status = statuses_by_low_name.get(new_status_name)\n            if not new_status:\n                self.log.warning((\n                    \"\\\"{}\\\" Couldn't change status to \\\"{}\\\".\"\n                    \" Status is not available for entity type \\\"{}\\\".\"\n                ).format(\n                    ent_path, new_status_name, parent_entity.entity_type\n                ))\n                continue\n\n            current_status = parent_entity[\"status\"]\n            # Do nothing if status is already set\n            if new_status[\"id\"] == current_status[\"id\"]:\n                self.log.debug(\n                    \"\\\"{}\\\" Status \\\"{}\\\" already set.\".format(\n                        ent_path, current_status[\"name\"]\n                    )\n                )\n                continue\n\n            try:\n                parent_entity[\"status_id\"] = new_status[\"id\"]\n                session.commit()\n                self.log.info(\n                    \"\\\"{}\\\" changed status to \\\"{}\\\"\".format(\n                        ent_path, new_status[\"name\"]\n                    )\n                )\n            except Exception:\n                session.rollback()\n                self.log.warning(\n                    \"\\\"{}\\\" status couldn't be set to \\\"{}\\\"\".format(\n                        ent_path, new_status[\"name\"]\n                    ),\n                    exc_info=True\n                )\n\n    def prepare_settings(self, project_settings, project_name):\n        event_settings = (\n            project_settings[\"ftrack\"][\"events\"][self.settings_key]\n        )\n\n        if not event_settings[\"enabled\"]:\n            self.log.debug(\"Project \\\"{}\\\" has disabled {}.\".format(\n                project_name, self.__class__.__name__\n            ))\n            return\n\n        _parent_object_types = event_settings[\"parent_object_types\"]\n        if not _parent_object_types:\n            self.log.debug((\n                \"Project \\\"{}\\\" does not have set\"\n                \" parent object types filtering.\"\n            ).format(project_name))\n            return\n\n        _all_match = (\n            event_settings[\"parent_status_match_all_task_statuses\"]\n        )\n        _single_match = (\n            event_settings[\"parent_status_by_task_status\"]\n        )\n\n        if not _all_match and not _single_match:\n            self.log.debug((\n                \"Project \\\"{}\\\" does not have set\"\n                \" parent status mappings.\"\n            ).format(project_name))\n            return\n\n        parent_object_types = [\n            item.lower()\n            for item in _parent_object_types\n        ]\n        all_match = {}\n        for new_status_name, task_statuses in _all_match.items():\n            all_match[new_status_name.lower()] = [\n                status_name.lower()\n                for status_name in task_statuses\n            ]\n\n        single_match = []\n        for item in _single_match:\n            single_match.append({\n                \"new_status\": item[\"new_status\"].lower(),\n                \"task_statuses\": [\n                    status_name.lower()\n                    for status_name in item[\"task_statuses\"]\n                ]\n            })\n        return parent_object_types, all_match, single_match\n\n    def new_status_by_all_task_statuses(\n        self, tasks_by_parent_id, statuses_by_id, all_match\n    ):\n        \"\"\"All statuses of parent entity must match specific status names.\n\n        Only if all task statuses match the condition parent's status name is\n        determined.\n        \"\"\"\n        output = {}\n        for parent_id, task_entities in tasks_by_parent_id.items():\n            task_statuses_lowered = set()\n            for task_entity in task_entities:\n                task_status = statuses_by_id[task_entity[\"status_id\"]]\n                low_status_name = task_status[\"name\"].lower()\n                task_statuses_lowered.add(low_status_name)\n\n            new_status = None\n            for _new_status, task_statuses in all_match.items():\n                valid_item = True\n                for status_name_low in task_statuses_lowered:\n                    if status_name_low not in task_statuses:\n                        valid_item = False\n                        break\n\n                if valid_item:\n                    new_status = _new_status\n                    break\n\n            if new_status is not None:\n                output[parent_id] = new_status\n\n        return output\n\n    def new_status_by_remainders(\n        self, remainder_tasks_by_parent_id, statuses_by_id, single_match\n    ):\n        \"\"\"By new task status can be determined new status of parent.\"\"\"\n        output = {}\n        if not remainder_tasks_by_parent_id:\n            return output\n\n        for parent_id, task_entities in remainder_tasks_by_parent_id.items():\n            if not task_entities:\n                continue\n\n            # For cases there are multiple tasks in changes\n            # - task status which match any new status item by order in the\n            #   list `single_match` is preferred\n            best_order = len(single_match)\n            best_order_status = None\n            for task_entity in task_entities:\n                task_status = statuses_by_id[task_entity[\"status_id\"]]\n                low_status_name = task_status[\"name\"].lower()\n                for order, item in enumerate(single_match):\n                    if order >= best_order:\n                        break\n\n                    if low_status_name in item[\"task_statuses\"]:\n                        best_order = order\n                        best_order_status = item[\"new_status\"]\n                        break\n\n            if best_order_status:\n                output[parent_id] = best_order_status\n        return output\n\n\ndef register(session):\n    TaskStatusToParent(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py",
    "content": "import collections\n\nfrom openpype.client import get_project\nfrom openpype_modules.ftrack.lib import BaseEvent\n\n\nclass TaskToVersionStatus(BaseEvent):\n    \"\"\"Changes status of task's latest AssetVersions on its status change.\"\"\"\n\n    settings_key = \"status_task_to_version\"\n\n    # Attribute for caching session user id\n    _cached_user_id = None\n\n    def is_event_invalid(self, session, event):\n        \"\"\"Skip task status changes for session user changes.\n\n        It is expected that there may be another event handler that set\n        version status to task in that case skip all events caused by same\n        user as session has to avoid infinite loop of status changes.\n        \"\"\"\n        # Cache user id of currently running session\n        if self._cached_user_id is None:\n            session_user_entity = session.query(\n                \"User where username is \\\"{}\\\"\".format(session.api_user)\n            ).first()\n            if not session_user_entity:\n                self.log.warning(\n                    \"Couldn't query Ftrack user with username \\\"{}\\\"\".format(\n                        session.api_user\n                    )\n                )\n                return False\n            self._cached_user_id = session_user_entity[\"id\"]\n\n        # Skip processing if current session user was the user who created\n        # the event\n        user_info = event[\"source\"].get(\"user\") or {}\n        user_id = user_info.get(\"id\")\n\n        # Mark as invalid if user is unknown\n        if user_id is None:\n            return True\n        return user_id == self._cached_user_id\n\n    def filter_event_entities(self, event):\n        \"\"\"Filter if event contain relevant data.\n\n        Event cares only about changes of `statusid` on `entity_type` \"Task\".\n        \"\"\"\n\n        entities_info = event[\"data\"].get(\"entities\")\n        if not entities_info:\n            return\n\n        filtered_entity_info = collections.defaultdict(list)\n        for entity_info in entities_info:\n            # Care only about tasks\n            if entity_info.get(\"entity_type\") != \"Task\":\n                continue\n\n            # Care only about changes of status\n            changes = entity_info.get(\"changes\") or {}\n            statusid_changes = changes.get(\"statusid\") or {}\n            if (\n                statusid_changes.get(\"new\") is None\n                or statusid_changes.get(\"old\") is None\n            ):\n                continue\n\n            # Get project id from entity info\n            project_id = None\n            for parent_item in reversed(entity_info[\"parents\"]):\n                if parent_item[\"entityType\"] == \"show\":\n                    project_id = parent_item[\"entityId\"]\n                    break\n\n            if project_id:\n                filtered_entity_info[project_id].append(entity_info)\n\n        return filtered_entity_info\n\n    def _get_ent_path(self, entity):\n        return \"/\".join(\n            [ent[\"name\"] for ent in entity[\"link\"]]\n        )\n\n    def launch(self, session, event):\n        '''Propagates status from version to task when changed'''\n        if self.is_event_invalid(session, event):\n            return\n\n        filtered_entity_infos = self.filter_event_entities(event)\n        if not filtered_entity_infos:\n            return\n\n        for project_id, entities_info in filtered_entity_infos.items():\n            self.process_by_project(session, event, project_id, entities_info)\n\n    def process_by_project(self, session, event, project_id, entities_info):\n        if not entities_info:\n            return\n\n        project_name = self.get_project_name_from_event(\n            session, event, project_id\n        )\n        if get_project(project_name) is None:\n            self.log.debug(\"Project not found in OpenPype. Skipping\")\n            return\n\n        # Load settings\n        project_settings = self.get_project_settings_from_event(\n            event, project_name\n        )\n\n        event_settings = (\n            project_settings[\"ftrack\"][\"events\"][self.settings_key]\n        )\n        _status_mapping = event_settings[\"mapping\"]\n        if not event_settings[\"enabled\"]:\n            self.log.debug(\"Project \\\"{}\\\" has disabled {}.\".format(\n                project_name, self.__class__.__name__\n            ))\n            return\n\n        if not _status_mapping:\n            self.log.debug((\n                \"Project \\\"{}\\\" does not have set status mapping for {}.\"\n            ).format(project_name, self.__class__.__name__))\n            return\n\n        status_mapping = {\n            key.lower(): value\n            for key, value in _status_mapping.items()\n        }\n\n        asset_types_filter = event_settings[\"asset_types_filter\"]\n\n        task_ids = [\n            entity_info[\"entityId\"]\n            for entity_info in entities_info\n        ]\n\n        last_asset_versions_by_task_id = (\n            self.find_last_asset_versions_for_task_ids(\n                session, task_ids, asset_types_filter\n            )\n        )\n\n        # Query Task entities for last asset versions\n        joined_filtered_ids = self.join_query_keys(\n            last_asset_versions_by_task_id.keys()\n        )\n        if not joined_filtered_ids:\n            return\n\n        task_entities = session.query(\n            \"select status_id, link from Task where id in ({})\".format(\n                joined_filtered_ids\n            )\n        ).all()\n        if not task_entities:\n            return\n\n        status_ids = set()\n        for task_entity in task_entities:\n            status_ids.add(task_entity[\"status_id\"])\n\n        task_status_entities = session.query(\n            \"select id, name from Status where id in ({})\".format(\n                self.join_query_keys(status_ids)\n            )\n        ).all()\n        task_status_name_by_id = {\n            status_entity[\"id\"]: status_entity[\"name\"]\n            for status_entity in task_status_entities\n        }\n\n        # Final process of changing statuses\n        project_entity = session.get(\"Project\", project_id)\n        av_statuses_by_low_name, av_statuses_by_id = (\n            self.get_asset_version_statuses(project_entity)\n        )\n\n        asset_ids = set()\n        for asset_versions in last_asset_versions_by_task_id.values():\n            for asset_version in asset_versions:\n                asset_ids.add(asset_version[\"asset_id\"])\n\n        asset_entities = session.query(\n            \"select name from Asset where id in ({})\".format(\n                self.join_query_keys(asset_ids)\n            )\n        ).all()\n        asset_names_by_id = {\n            asset_entity[\"id\"]: asset_entity[\"name\"]\n            for asset_entity in asset_entities\n        }\n        for task_entity in task_entities:\n            task_id = task_entity[\"id\"]\n            status_id = task_entity[\"status_id\"]\n            task_path = self._get_ent_path(task_entity)\n\n            task_status_name = task_status_name_by_id[status_id]\n            task_status_name_low = task_status_name.lower()\n\n            new_asset_version_status = None\n            mapped_status_names = status_mapping.get(task_status_name_low)\n            if mapped_status_names:\n                for status_name in mapped_status_names:\n                    _status = av_statuses_by_low_name.get(status_name.lower())\n                    if _status:\n                        new_asset_version_status = _status\n                        break\n\n            if not new_asset_version_status:\n                new_asset_version_status = av_statuses_by_low_name.get(\n                    task_status_name_low\n                )\n            # Skip if tasks status is not available to AssetVersion\n            if not new_asset_version_status:\n                self.log.debug((\n                    \"AssetVersion does not have matching status to \\\"{}\\\"\"\n                ).format(task_status_name))\n                continue\n\n            last_asset_versions = last_asset_versions_by_task_id[task_id]\n            for asset_version in last_asset_versions:\n                version = asset_version[\"version\"]\n                self.log.debug((\n                    \"Trying to change status of last AssetVersion {}\"\n                    \" for task \\\"{}\\\"\"\n                ).format(version, task_path))\n\n                asset_id = asset_version[\"asset_id\"]\n                asset_type_name = asset_names_by_id[asset_id]\n                av_ent_path = task_path + \" Asset {} AssetVersion {}\".format(\n                    asset_type_name,\n                    version\n                )\n\n                # Skip if current AssetVersion's status is same\n                status_id = asset_version[\"status_id\"]\n                current_status_name = av_statuses_by_id[status_id][\"name\"]\n                if current_status_name.lower() == task_status_name_low:\n                    self.log.debug((\n                        \"AssetVersion already has set status \\\"{}\\\". \\\"{}\\\"\"\n                    ).format(current_status_name, av_ent_path))\n                    continue\n\n                new_status_id = new_asset_version_status[\"id\"]\n                new_status_name = new_asset_version_status[\"name\"]\n                # Skip if status is already same\n                if asset_version[\"status_id\"] == new_status_id:\n                    continue\n\n                # Change the status\n                try:\n                    asset_version[\"status_id\"] = new_status_id\n                    session.commit()\n                    self.log.info(\"[ {} ] Status updated to [ {} ]\".format(\n                        av_ent_path, new_status_name\n                    ))\n                except Exception:\n                    session.rollback()\n                    self.log.warning(\n                        \"[ {} ]Status couldn't be set to \\\"{}\\\"\".format(\n                            av_ent_path, new_status_name\n                        ),\n                        exc_info=True\n                    )\n\n    def get_asset_version_statuses(self, project_entity):\n        \"\"\"Status entities for AssetVersion from project's schema.\n\n        Load statuses from project's schema and store them by id and name.\n\n        Args:\n            project_entity (ftrack_api.Entity): Entity of ftrack's project.\n\n        Returns:\n            tuple: 2 items are returned first are statuses by name\n                second are statuses by id.\n        \"\"\"\n        project_schema = project_entity[\"project_schema\"]\n        # Get all available statuses for Task\n        statuses = project_schema.get_statuses(\"AssetVersion\")\n        # map lowered status name with it's object\n        av_statuses_by_low_name = {}\n        av_statuses_by_id = {}\n        for status in statuses:\n            av_statuses_by_low_name[status[\"name\"].lower()] = status\n            av_statuses_by_id[status[\"id\"]] = status\n\n        return av_statuses_by_low_name, av_statuses_by_id\n\n    def find_last_asset_versions_for_task_ids(\n        self, session, task_ids, asset_types_filter\n    ):\n        \"\"\"Find latest AssetVersion entities for task.\n\n        Find first latest AssetVersion for task and all AssetVersions with\n        same version for the task.\n\n        Args:\n            asset_versions (list): AssetVersion entities sorted by \"version\".\n            task_ids (list): Task ids.\n            asset_types_filter (list): Asset types short names that will be\n                used to filter AssetVersions. Filtering is skipped if entered\n                value is empty list.\n        \"\"\"\n\n        # Allow event only on specific asset type names\n        asset_query_part = \"\"\n        if asset_types_filter:\n            # Query all AssetTypes\n            asset_types = session.query(\n                \"select id, short from AssetType\"\n            ).all()\n            # Store AssetTypes by id\n            asset_type_short_by_id = {\n                asset_type[\"id\"]: asset_type[\"short\"]\n                for asset_type in asset_types\n            }\n\n            # Lower asset types from settings\n            # WARNING: not sure if is good idea to lower names as Ftrack may\n            #   contain asset type with name \"Scene\" and \"scene\"!\n            asset_types_filter_low = set(\n                asset_types_name.lower()\n                for asset_types_name in asset_types_filter\n            )\n            asset_type_ids = []\n            for type_id, short in asset_type_short_by_id.items():\n                # TODO log if asset type name is not found\n                if short.lower() in asset_types_filter_low:\n                    asset_type_ids.append(type_id)\n\n            # TODO log that none of asset type names were found in ftrack\n            if asset_type_ids:\n                asset_query_part = \" and asset.type_id in ({})\".format(\n                    self.join_query_keys(asset_type_ids)\n                )\n\n        # Query tasks' AssetVersions\n        asset_versions = session.query((\n            \"select status_id, version, task_id, asset_id\"\n            \" from AssetVersion where task_id in ({}){}\"\n            \" order by version descending\"\n        ).format(self.join_query_keys(task_ids), asset_query_part)).all()\n\n        last_asset_versions_by_task_id = collections.defaultdict(list)\n        last_version_by_task_id = {}\n        not_finished_task_ids = set(task_ids)\n        for asset_version in asset_versions:\n            task_id = asset_version[\"task_id\"]\n            # Check if task id is still in `not_finished_task_ids`\n            if task_id not in not_finished_task_ids:\n                continue\n\n            version = asset_version[\"version\"]\n\n            # Find last version in `last_version_by_task_id`\n            last_version = last_version_by_task_id.get(task_id)\n            if last_version is None:\n                # If task id does not have version set yet then it's first\n                # AssetVersion for this task\n                last_version_by_task_id[task_id] = version\n\n            elif last_version > version:\n                # Skip processing if version is lower than last version\n                # and pop task id from `not_finished_task_ids`\n                not_finished_task_ids.remove(task_id)\n                continue\n\n            # Add AssetVersion entity to output dictionary\n            last_asset_versions_by_task_id[task_id].append(asset_version)\n\n        return last_asset_versions_by_task_id\n\n\ndef register(session):\n    TaskToVersionStatus(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py",
    "content": "import collections\n\nfrom openpype.client import get_project\nfrom openpype_modules.ftrack.lib import BaseEvent\n\n\nclass ThumbnailEvents(BaseEvent):\n    settings_key = \"thumbnail_updates\"\n\n    def launch(self, session, event):\n        \"\"\"Updates thumbnails of entities from new AssetVersion.\"\"\"\n        filtered_entities = self.filter_entities(event)\n        if not filtered_entities:\n            return\n\n        for project_id, entities_info in filtered_entities.items():\n            self.process_project_entities(\n                session, event, project_id, entities_info\n            )\n\n    def process_project_entities(\n        self, session, event, project_id, entities_info\n    ):\n        project_name = self.get_project_name_from_event(\n            session, event, project_id\n        )\n        if get_project(project_name) is None:\n            self.log.debug(\"Project not found in OpenPype. Skipping\")\n            return\n\n        # Load settings\n        project_settings = self.get_project_settings_from_event(\n            event, project_name\n        )\n\n        event_settings = (\n            project_settings\n            [\"ftrack\"]\n            [\"events\"]\n            [self.settings_key]\n        )\n        if not event_settings[\"enabled\"]:\n            self.log.debug(\"Project \\\"{}\\\" does not have activated {}.\".format(\n                project_name, self.__class__.__name__\n            ))\n            return\n\n        self.log.debug(\"Processing {} on project \\\"{}\\\".\".format(\n            self.__class__.__name__, project_name\n        ))\n\n        parent_levels = event_settings[\"levels\"]\n        if parent_levels < 1:\n            self.log.debug(\n                \"Project \\\"{}\\\" has parent levels set to {}. Skipping\".format(\n                    project_name, parent_levels\n                )\n            )\n            return\n\n        asset_version_ids = set()\n        for entity in entities_info:\n            asset_version_ids.add(entity[\"entityId\"])\n\n        # Do not use attribute `asset_version_entities` will be filtered\n        # to when `asset_versions_by_id` is filled\n        asset_version_entities = session.query((\n            \"select task_id, thumbnail_id from AssetVersion where id in ({})\"\n        ).format(self.join_query_keys(asset_version_ids))).all()\n\n        asset_versions_by_id = {}\n        for asset_version_entity in asset_version_entities:\n            if not asset_version_entity[\"thumbnail_id\"]:\n                continue\n            entity_id = asset_version_entity[\"id\"]\n            asset_versions_by_id[entity_id] = asset_version_entity\n\n        if not asset_versions_by_id:\n            self.log.debug(\"None of asset versions has set thumbnail id.\")\n            return\n\n        entity_ids_by_asset_version_id = collections.defaultdict(list)\n        hierarchy_ids = set()\n        for entity_info in entities_info:\n            entity_id = entity_info[\"entityId\"]\n            if entity_id not in asset_versions_by_id:\n                continue\n\n            parent_ids = []\n            counter = None\n            for parent_info in entity_info[\"parents\"]:\n                if counter is not None:\n                    if counter >= parent_levels:\n                        break\n                    parent_ids.append(parent_info[\"entityId\"])\n                    counter += 1\n\n                elif parent_info[\"entityType\"] == \"asset\":\n                    counter = 0\n\n            for parent_id in parent_ids:\n                hierarchy_ids.add(parent_id)\n                entity_ids_by_asset_version_id[entity_id].append(parent_id)\n\n        for asset_version_entity in asset_versions_by_id.values():\n            task_id = asset_version_entity[\"task_id\"]\n            if task_id:\n                hierarchy_ids.add(task_id)\n                asset_version_id = asset_version_entity[\"id\"]\n                entity_ids_by_asset_version_id[asset_version_id].append(\n                    task_id\n                )\n\n        entities = session.query((\n            \"select thumbnail_id, link from TypedContext where id in ({})\"\n        ).format(self.join_query_keys(hierarchy_ids))).all()\n        entities_by_id = {\n            entity[\"id\"]: entity\n            for entity in entities\n        }\n\n        for version_id, version_entity in asset_versions_by_id.items():\n            for entity_id in entity_ids_by_asset_version_id[version_id]:\n                entity = entities_by_id.get(entity_id)\n                if not entity:\n                    continue\n\n                entity[\"thumbnail_id\"] = version_entity[\"thumbnail_id\"]\n                self.log.info(\"Updating thumbnail for entity [ {} ]\".format(\n                    self.get_entity_path(entity)\n                ))\n\n            try:\n                session.commit()\n            except Exception:\n                session.rollback()\n\n    def filter_entities(self, event):\n        filtered_entities_info = {}\n        for entity_info in event[\"data\"].get(\"entities\", []):\n            action = entity_info.get(\"action\")\n            if not action:\n                continue\n\n            if (\n                action == \"remove\"\n                or entity_info[\"entityType\"].lower() != \"assetversion\"\n                or \"thumbid\" not in (entity_info.get(\"keys\") or [])\n            ):\n                continue\n\n            # Get project id from entity info\n            project_id = entity_info[\"parents\"][-1][\"entityId\"]\n            if project_id not in filtered_entities_info:\n                filtered_entities_info[project_id] = []\n            filtered_entities_info[project_id].append(entity_info)\n        return filtered_entities_info\n\n\ndef register(session):\n    ThumbnailEvents(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/event_user_assigment.py",
    "content": "import re\nimport subprocess\n\nfrom openpype.client import get_asset_by_id, get_asset_by_name\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import Anatomy\nfrom openpype_modules.ftrack.lib import BaseEvent\nfrom openpype_modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY\n\n\nclass UserAssigmentEvent(BaseEvent):\n    \"\"\"\n    This script will intercept user assignment / de-assignment event and\n    run shell script, providing as much context as possible.\n\n    It expects configuration file ``presets/ftrack/user_assigment_event.json``.\n    In it, you define paths to scripts to be run for user assignment event and\n    for user-deassigment::\n        {\n            \"add\": [\n                \"/path/to/script1\",\n                \"/path/to/script2\"\n            ],\n            \"remove\": [\n                \"/path/to/script3\",\n                \"/path/to/script4\"\n            ]\n        }\n\n    Those scripts are executed in shell. Three arguments will be passed to\n    to them:\n        1) user name of user (de)assigned\n        2) path to workfiles of task user was (de)assigned to\n        3) path to publish files of task user was (de)assigned to\n    \"\"\"\n\n    def error(self, *err):\n        for e in err:\n            self.log.error(e)\n\n    def _run_script(self, script, args):\n        \"\"\"\n        Run shell script with arguments as subprocess\n\n        :param script: script path\n        :type script: str\n        :param args: list of arguments passed to script\n        :type args: list\n        :returns: return code\n        :rtype: int\n        \"\"\"\n        p = subprocess.call([script, args], shell=True)\n        return p\n\n    def _get_task_and_user(self, session, action, changes):\n        \"\"\"\n        Get Task and User entities from Ftrack session\n\n        :param session: ftrack session\n        :type session: ftrack_api.session\n        :param action: event action\n        :type action: str\n        :param changes: what was changed by event\n        :type changes: dict\n        :returns: User and Task entities\n        :rtype: tuple\n        \"\"\"\n        if not changes:\n            return None, None\n\n        if action == 'add':\n            task_id = changes.get('context_id', {}).get('new')\n            user_id = changes.get('resource_id', {}).get('new')\n\n        elif action == 'remove':\n            task_id = changes.get('context_id', {}).get('old')\n            user_id = changes.get('resource_id', {}).get('old')\n\n        if not task_id:\n            return None, None\n\n        if not user_id:\n            return None, None\n\n        task = session.query('Task where id is \"{}\"'.format(task_id)).first()\n        user = session.query('User where id is \"{}\"'.format(user_id)).first()\n\n        return task, user\n\n    def _get_asset(self, task):\n        \"\"\"\n        Get asset from task entity\n\n        :param task: Task entity\n        :type task: dict\n        :returns: Asset entity\n        :rtype: dict\n        \"\"\"\n        parent = task['parent']\n        project_name = task[\"project\"][\"full_name\"]\n        avalon_entity = None\n        parent_id = parent['custom_attributes'].get(CUST_ATTR_ID_KEY)\n        if parent_id:\n            avalon_entity = get_asset_by_id(project_name, parent_id)\n\n        if not avalon_entity:\n            avalon_entity = get_asset_by_name(project_name, parent[\"name\"])\n\n        if not avalon_entity:\n            msg = 'Entity \"{}\" not found in avalon database'.format(\n                parent['name']\n            )\n            self.error(msg)\n            return {\n                'success': False,\n                'message': msg\n            }\n        return avalon_entity\n\n    def _get_hierarchy(self, asset):\n        \"\"\"\n        Get hierarchy from Asset entity\n\n        :param asset: Asset entity\n        :type asset: dict\n        :returns: hierarchy string\n        :rtype: str\n        \"\"\"\n        return asset['data']['hierarchy']\n\n    def _get_template_data(self, task):\n        \"\"\"\n        Get data to fill template from task\n\n        .. seealso:: :mod:`openpype.pipeline.Anatomy`\n\n        :param task: Task entity\n        :type task: dict\n        :returns: data for anatomy template\n        :rtype: dict\n        \"\"\"\n        project_name = task['project']['full_name']\n        project_code = task['project']['name']\n\n        # fill in template data\n        asset = self._get_asset(task)\n        t_data = {\n            'project': {\n                'name': project_name,\n                'code': project_code\n            },\n            'asset': asset['name'],\n            'task': task['name'],\n            'hierarchy': self._get_hierarchy(asset)\n        }\n\n        return t_data\n\n    def launch(self, session, event):\n        if not event.get(\"data\"):\n            return\n\n        entities_info = event[\"data\"].get(\"entities\")\n        if not entities_info:\n            return\n\n        # load shell scripts presets\n        tmp_by_project_name = {}\n        for entity_info in entities_info:\n            if entity_info.get('entity_type') != 'Appointment':\n                continue\n\n            task_entity, user_entity = self._get_task_and_user(\n                session,\n                entity_info.get('action'),\n                entity_info.get('changes')\n            )\n\n            if not task_entity or not user_entity:\n                self.log.error(\"Task or User was not found.\")\n                continue\n\n            # format directories to pass to shell script\n            project_name = task_entity[\"project\"][\"full_name\"]\n            project_data = tmp_by_project_name.get(project_name) or {}\n            if \"scripts_by_action\" not in project_data:\n                project_settings = get_project_settings(project_name)\n                _settings = (\n                    project_settings[\"ftrack\"][\"events\"][\"user_assignment\"]\n                )\n                project_data[\"scripts_by_action\"] = _settings.get(\"scripts\")\n                tmp_by_project_name[project_name] = project_data\n\n            scripts_by_action = project_data[\"scripts_by_action\"]\n            if not scripts_by_action:\n                continue\n\n            if \"anatomy\" not in project_data:\n                project_data[\"anatomy\"] = Anatomy(project_name)\n                tmp_by_project_name[project_name] = project_data\n\n            anatomy = project_data[\"anatomy\"]\n            data = self._get_template_data(task_entity)\n            anatomy_filled = anatomy.format(data)\n            # formatting work dir is easiest part as we can use whole path\n            work_dir = anatomy_filled[\"work\"][\"folder\"]\n            # we also need publish but not whole\n            anatomy_filled.strict = False\n            publish = anatomy_filled[\"publish\"][\"folder\"]\n\n            # now find path to {asset}\n            m = re.search(\n                \"(^.+?{})\".format(data[\"asset\"]),\n                publish\n            )\n\n            if not m:\n                msg = 'Cannot get part of publish path {}'.format(publish)\n                self.log.error(msg)\n                return {\n                    'success': False,\n                    'message': msg\n                }\n            publish_dir = m.group(1)\n\n            username = user_entity[\"username\"]\n            event_entity_action = entity_info[\"action\"]\n            for script in scripts_by_action.get(event_entity_action):\n                self.log.info((\n                    \"[{}] : running script for user {}\"\n                ).format(event_entity_action, username))\n                self._run_script(script, [username, work_dir, publish_dir])\n\n        return True\n\n\ndef register(session):\n    \"\"\"\n    Register plugin. Called when used as an plugin.\n    \"\"\"\n\n    UserAssigmentEvent(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py",
    "content": "from openpype.client import get_project\nfrom openpype_modules.ftrack.lib import BaseEvent\n\n\nclass VersionToTaskStatus(BaseEvent):\n    \"\"\"Propagates status from version to task when changed.\"\"\"\n    def launch(self, session, event):\n        # Filter event entities\n        # - output is dictionary where key is project id and event info in\n        #   value\n        filtered_entities_info = self.filter_entity_info(event)\n        if not filtered_entities_info:\n            return\n\n        for project_id, entities_info in filtered_entities_info.items():\n            self.process_by_project(session, event, project_id, entities_info)\n\n    def filter_entity_info(self, event):\n        filtered_entity_info = {}\n        for entity_info in event[\"data\"].get(\"entities\", []):\n            # Filter AssetVersions\n            if entity_info[\"entityType\"] != \"assetversion\":\n                continue\n\n            # Skip if statusid not in keys (in changes)\n            keys = entity_info.get(\"keys\")\n            if not keys or \"statusid\" not in keys:\n                continue\n\n            # Get new version task name\n            version_status_id = (\n                entity_info\n                .get(\"changes\", {})\n                .get(\"statusid\", {})\n                .get(\"new\", {})\n            )\n\n            # Just check that `new` is set to any value\n            if not version_status_id:\n                continue\n\n            # Get project id from entity info\n            project_id = entity_info[\"parents\"][-1][\"entityId\"]\n            if project_id not in filtered_entity_info:\n                filtered_entity_info[project_id] = []\n            filtered_entity_info[project_id].append(entity_info)\n        return filtered_entity_info\n\n    def process_by_project(self, session, event, project_id, entities_info):\n        # Check for project data if event is enabled for event handler\n        project_name = self.get_project_name_from_event(\n            session, event, project_id\n        )\n        if get_project(project_name) is None:\n            self.log.debug(\"Project not found in OpenPype. Skipping\")\n            return\n\n        # Load settings\n        project_settings = self.get_project_settings_from_event(\n            event, project_name\n        )\n\n        # Load status mapping from presets\n        event_settings = (\n            project_settings[\"ftrack\"][\"events\"][\"status_version_to_task\"]\n        )\n        # Skip if event is not enabled or status mapping is not set\n        if not event_settings[\"enabled\"]:\n            self.log.debug(\"Project \\\"{}\\\" has disabled {}\".format(\n                project_name, self.__class__.__name__\n            ))\n            return\n\n        _status_mapping = event_settings[\"mapping\"] or {}\n        status_mapping = {\n            key.lower(): value\n            for key, value in _status_mapping.items()\n        }\n\n        asset_types_to_skip = [\n            short_name.lower()\n            for short_name in event_settings[\"asset_types_to_skip\"]\n        ]\n\n        # Collect entity ids\n        asset_version_ids = set()\n        for entity_info in entities_info:\n            asset_version_ids.add(entity_info[\"entityId\"])\n\n        # Query tasks for AssetVersions\n        _asset_version_entities = session.query(\n            \"AssetVersion where task_id != none and id in ({})\".format(\n                self.join_query_keys(asset_version_ids)\n            )\n        ).all()\n        if not _asset_version_entities:\n            return\n\n        # Filter asset versions by asset type and store their task_ids\n        task_ids = set()\n        asset_version_entities = []\n        for asset_version in _asset_version_entities:\n            if asset_types_to_skip:\n                short_name = asset_version[\"asset\"][\"type\"][\"short\"].lower()\n                if short_name in asset_types_to_skip:\n                    continue\n            asset_version_entities.append(asset_version)\n            task_ids.add(asset_version[\"task_id\"])\n\n        # Skip if `task_ids` are empty\n        if not task_ids:\n            return\n\n        task_entities = session.query(\n            \"select link from Task where id in ({})\".format(\n                self.join_query_keys(task_ids)\n            )\n        ).all()\n        task_entities_by_id = {\n            task_entiy[\"id\"]: task_entiy\n            for task_entiy in task_entities\n        }\n\n        # Prepare asset version by their id\n        asset_versions_by_id = {\n            asset_version[\"id\"]: asset_version\n            for asset_version in asset_version_entities\n        }\n\n        # Query status entities\n        status_ids = set()\n        for entity_info in entities_info:\n            # Skip statuses of asset versions without task\n            if entity_info[\"entityId\"] not in asset_versions_by_id:\n                continue\n            status_ids.add(entity_info[\"changes\"][\"statusid\"][\"new\"])\n\n        version_status_entities = session.query(\n            \"select id, name from Status where id in ({})\".format(\n                self.join_query_keys(status_ids)\n            )\n        ).all()\n\n        # Qeury statuses\n        statusese_by_obj_id = self.statuses_for_tasks(\n            session, task_entities, project_id\n        )\n        # Prepare status names by their ids\n        status_name_by_id = {\n            status_entity[\"id\"]: status_entity[\"name\"]\n            for status_entity in version_status_entities\n        }\n        for entity_info in entities_info:\n            entity_id = entity_info[\"entityId\"]\n            status_id = entity_info[\"changes\"][\"statusid\"][\"new\"]\n            status_name = status_name_by_id.get(status_id)\n            if not status_name:\n                continue\n            status_name_low = status_name.lower()\n\n            # Lower version status name and check if has mapping\n            new_status_names = []\n            mapped = status_mapping.get(status_name_low)\n            if mapped:\n                new_status_names.extend(list(mapped))\n\n            new_status_names.append(status_name_low)\n\n            self.log.debug(\n                \"Processing AssetVersion status change: [ {} ]\".format(\n                    status_name\n                )\n            )\n\n            asset_version = asset_versions_by_id[entity_id]\n            task_entity = task_entities_by_id[asset_version[\"task_id\"]]\n            type_id = task_entity[\"type_id\"]\n\n            # Lower all names from presets\n            new_status_names = [name.lower() for name in new_status_names]\n            task_statuses_by_low_name = statusese_by_obj_id[type_id]\n\n            new_status = None\n            for status_name in new_status_names:\n                if status_name not in task_statuses_by_low_name:\n                    self.log.debug((\n                        \"Task does not have status name \\\"{}\\\" available.\"\n                    ).format(status_name))\n                    continue\n\n                # store object of found status\n                new_status = task_statuses_by_low_name[status_name]\n                self.log.debug(\"Status to set: [ {} ]\".format(\n                    new_status[\"name\"]\n                ))\n                break\n\n            # Skip if status names were not found for paticulat entity\n            if not new_status:\n                self.log.warning(\n                    \"Any of statuses from presets can be set: {}\".format(\n                        str(new_status_names)\n                    )\n                )\n                continue\n            # Get full path to task for logging\n            ent_path = \"/\".join([ent[\"name\"] for ent in task_entity[\"link\"]])\n\n            # Setting task status\n            try:\n                task_entity[\"status\"] = new_status\n                session.commit()\n                self.log.debug(\"[ {} ] Status updated to [ {} ]\".format(\n                    ent_path, new_status[\"name\"]\n                ))\n            except Exception:\n                session.rollback()\n                self.log.warning(\n                    \"[ {} ]Status couldn't be set\".format(ent_path),\n                    exc_info=True\n                )\n\n    def statuses_for_tasks(self, session, task_entities, project_id):\n        task_type_ids = set()\n        for task_entity in task_entities:\n            task_type_ids.add(task_entity[\"type_id\"])\n\n        project_entity = session.get(\"Project\", project_id)\n        project_schema = project_entity[\"project_schema\"]\n        output = {}\n        for task_type_id in task_type_ids:\n            statuses = project_schema.get_statuses(\"Task\", task_type_id)\n            output[task_type_id] = {\n                status[\"name\"].lower(): status\n                for status in statuses\n            }\n\n        return output\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    VersionToTaskStatus(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_applications.py",
    "content": "import os\n\nfrom openpype.client import get_project\nfrom openpype_modules.ftrack.lib import BaseAction\nfrom openpype.lib.applications import (\n    ApplicationManager,\n    ApplicationLaunchFailed,\n    ApplictionExecutableNotFound,\n    CUSTOM_LAUNCH_APP_GROUPS\n)\n\n\nclass AppplicationsAction(BaseAction):\n    \"\"\"Applications Action class.\"\"\"\n\n    type = \"Application\"\n    label = \"Application action\"\n\n    identifier = \"openpype_app\"\n    _launch_identifier_with_id = None\n\n    icon_url = os.environ.get(\"OPENPYPE_STATICS_SERVER\")\n\n    def __init__(self, *args, **kwargs):\n        super(AppplicationsAction, self).__init__(*args, **kwargs)\n\n        self.application_manager = ApplicationManager()\n\n    @property\n    def discover_identifier(self):\n        if self._discover_identifier is None:\n            self._discover_identifier = \"{}.{}\".format(\n                self.identifier, self.process_identifier()\n            )\n        return self._discover_identifier\n\n    @property\n    def launch_identifier(self):\n        if self._launch_identifier is None:\n            self._launch_identifier = \"{}.*\".format(self.identifier)\n        return self._launch_identifier\n\n    @property\n    def launch_identifier_with_id(self):\n        if self._launch_identifier_with_id is None:\n            self._launch_identifier_with_id = \"{}.{}\".format(\n                self.identifier, self.process_identifier()\n            )\n        return self._launch_identifier_with_id\n\n    def construct_requirements_validations(self):\n        # Override validation as this action does not need them\n        return\n\n    def register(self):\n        \"\"\"Registers the action, subscribing the discover and launch topics.\"\"\"\n\n        discovery_subscription = (\n            \"topic=ftrack.action.discover and source.user.username={0}\"\n        ).format(self.session.api_user)\n\n        self.session.event_hub.subscribe(\n            discovery_subscription,\n            self._discover,\n            priority=self.priority\n        )\n\n        launch_subscription = (\n            \"topic=ftrack.action.launch\"\n            \" and data.actionIdentifier={0}\"\n            \" and source.user.username={1}\"\n        ).format(\n            self.launch_identifier,\n            self.session.api_user\n        )\n        self.session.event_hub.subscribe(\n            launch_subscription,\n            self._launch\n        )\n\n    def _discover(self, event):\n        entities = self._translate_event(event)\n        items = self.discover(self.session, entities, event)\n        if items:\n            return {\"items\": items}\n\n    def discover(self, session, entities, event):\n        \"\"\"Return true if we can handle the selected entities.\n\n        Args:\n            session (ftrack_api.Session): Helps to query necessary data.\n            entities (list): Object of selected entities.\n            event (ftrack_api.Event): Ftrack event causing discover callback.\n        \"\"\"\n\n        if (\n            len(entities) != 1\n            or entities[0].entity_type.lower() != \"task\"\n        ):\n            return False\n\n        entity = entities[0]\n        if entity[\"parent\"].entity_type.lower() == \"project\":\n            return False\n\n        avalon_project_apps = event[\"data\"].get(\"avalon_project_apps\", None)\n        avalon_project_doc = event[\"data\"].get(\"avalon_project_doc\", None)\n        if avalon_project_apps is None:\n            if avalon_project_doc is None:\n                ft_project = self.get_project_from_entity(entity)\n                project_name = ft_project[\"full_name\"]\n                avalon_project_doc = get_project(project_name) or False\n                event[\"data\"][\"avalon_project_doc\"] = avalon_project_doc\n\n            if not avalon_project_doc:\n                return False\n\n            project_apps_config = avalon_project_doc[\"config\"].get(\"apps\", [])\n            avalon_project_apps = [\n                app[\"name\"] for app in project_apps_config\n            ] or False\n            event[\"data\"][\"avalon_project_apps\"] = avalon_project_apps\n\n        if not avalon_project_apps:\n            return False\n\n        settings = self.get_project_settings_from_event(\n            event, avalon_project_doc[\"name\"])\n\n        only_available = settings[\"applications\"][\"only_available\"]\n\n        items = []\n        for app_name in avalon_project_apps:\n            app = self.application_manager.applications.get(app_name)\n            if not app or not app.enabled:\n                continue\n\n            if app.group.name in CUSTOM_LAUNCH_APP_GROUPS:\n                continue\n\n            # Skip applications without valid executables\n            if only_available and not app.find_executable():\n                continue\n\n            app_icon = app.icon\n            if app_icon and self.icon_url:\n                try:\n                    app_icon = app_icon.format(self.icon_url)\n                except Exception:\n                    self.log.warning((\n                        \"Couldn't fill icon path. Icon template: \\\"{}\\\"\"\n                        \" --- Icon url: \\\"{}\\\"\"\n                    ).format(app_icon, self.icon_url))\n                    app_icon = None\n\n            items.append({\n                \"label\": app.group.label,\n                \"variant\": app.label,\n                \"description\": None,\n                \"actionIdentifier\": \"{}.{}\".format(\n                    self.launch_identifier_with_id, app_name\n                ),\n                \"icon\": app_icon\n            })\n\n        return items\n\n    def _launch(self, event):\n        event_identifier = event[\"data\"][\"actionIdentifier\"]\n        # Check if identifier is same\n        # - show message that acion may not be triggered on this machine\n        if event_identifier.startswith(self.launch_identifier_with_id):\n            return BaseAction._launch(self, event)\n\n        return {\n            \"success\": False,\n            \"message\": (\n                \"There are running more OpenPype processes\"\n                \" where Application can be launched.\"\n            )\n        }\n\n    def launch(self, session, entities, event):\n        \"\"\"Callback method for the custom action.\n\n        return either a bool (True if successful or False if the action failed)\n        or a dictionary with they keys `message` and `success`, the message\n        should be a string and will be displayed as feedback to the user,\n        success should be a bool, True if successful or False if the action\n        failed.\n\n        *session* is a `ftrack_api.Session` instance\n\n        *entities* is a list of tuples each containing the entity type and\n        the entity id. If the entity is a hierarchical you will always get\n        the entity type TypedContext, once retrieved through a get operation\n        you will have the \"real\" entity type ie. example Shot, Sequence\n        or Asset Build.\n\n        *event* the unmodified original event\n        \"\"\"\n        identifier = event[\"data\"][\"actionIdentifier\"]\n        id_identifier_len = len(self.launch_identifier_with_id) + 1\n        app_name = identifier[id_identifier_len:]\n\n        entity = entities[0]\n\n        task_name = entity[\"name\"]\n        asset_name = entity[\"parent\"][\"name\"]\n        project_name = entity[\"project\"][\"full_name\"]\n        self.log.info((\n            \"Ftrack launch app: \\\"{}\\\" on Project/Asset/Task: {}/{}/{}\"\n        ).format(app_name, project_name, asset_name, task_name))\n        try:\n            self.application_manager.launch(\n                app_name,\n                project_name=project_name,\n                asset_name=asset_name,\n                task_name=task_name\n            )\n\n        except ApplictionExecutableNotFound as exc:\n            self.log.warning(exc.exc_msg)\n            return {\n                \"success\": False,\n                \"message\": exc.msg\n            }\n\n        except ApplicationLaunchFailed as exc:\n            self.log.error(str(exc))\n            return {\n                \"success\": False,\n                \"message\": str(exc)\n            }\n\n        except Exception:\n            msg = \"Unexpected failure of application launch {}\".format(\n                self.label\n            )\n            self.log.error(msg, exc_info=True)\n            return {\n                \"success\": False,\n                \"message\": msg\n            }\n\n        return {\n            \"success\": True,\n            \"message\": \"Launching {0}\".format(self.label)\n        }\n\n\ndef register(session):\n    \"\"\"Register action. Called when used as an event plugin.\"\"\"\n    AppplicationsAction(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_batch_task_creation.py",
    "content": "\"\"\"\nTaken from https://github.com/tokejepsen/ftrack-hooks/tree/master/batch_tasks\n\"\"\"\n\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass BatchTasksAction(BaseAction):\n    '''Batch Tasks action\n    `label` a descriptive string identifying your action.\n    `varaint` To group actions together, give them the same\n    label and specify a unique variant per action.\n    `identifier` a unique identifier for your action.\n    `description` a verbose descriptive text for you action\n     '''\n    label = \"Batch Task Create\"\n    variant = None\n    identifier = \"batch-tasks\"\n    description = None\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"BatchTasks.svg\")\n\n    def discover(self, session, entities, event):\n        '''Return true if we can handle the selected entities.\n        *session* is a `ftrack_api.Session` instance\n        *entities* is a list of tuples each containing the entity type and the\n        entity id.\n        If the entity is a hierarchical you will always get the entity\n        type TypedContext, once retrieved through a get operation you\n        will have the \"real\" entity type ie. example Shot, Sequence\n        or Asset Build.\n        *event* the unmodified original event\n        '''\n\n        not_allowed = [\"assetversion\", \"project\", \"ReviewSession\"]\n        if entities[0].entity_type.lower() in not_allowed:\n            return False\n\n        return True\n\n\n    def get_task_form_items(self, session, number_of_tasks):\n        items = []\n\n        task_type_options = [\n            {'label': task_type[\"name\"], 'value': task_type[\"id\"]}\n            for task_type in session.query(\"Type\")\n        ]\n\n        for index in range(0, number_of_tasks):\n            items.extend(\n                [\n                    {\n                        'value': '##Template for Task{0}##'.format(\n                            index\n                        ),\n                        'type': 'label'\n                    },\n                    {\n                        'label': 'Type',\n                        'type': 'enumerator',\n                        'name': 'task_{0}_typeid'.format(index),\n                        'data': task_type_options\n                    },\n                    {\n                        'label': 'Name',\n                        'type': 'text',\n                        'name': 'task_{0}_name'.format(index)\n                    }\n                ]\n            )\n\n        return items\n\n    def ensure_task(self, session, name, task_type, parent):\n\n        # Query for existing task.\n        query = (\n            'Task where type.id is \"{0}\" and name is \"{1}\" '\n            'and parent.id is \"{2}\"'\n        )\n        task = session.query(\n            query.format(\n                task_type[\"id\"],\n                name,\n                parent[\"id\"]\n            )\n        ).first()\n\n        # Create task.\n        if not task:\n            session.create(\n                \"Task\",\n                {\n                    \"name\": name,\n                    \"type\": task_type,\n                    \"parent\": parent\n                }\n            )\n\n    def launch(self, session, entities, event):\n        '''Callback method for the custom action.\n        return either a bool ( True if successful or False if the action\n        failed ) or a dictionary with they keys `message` and `success`, the\n        message should be a string and will be displayed as feedback to the\n        user, success should be a bool, True if successful or False if the\n        action failed.\n        *session* is a `ftrack_api.Session` instance\n        *entities* is a list of tuples each containing the entity type and the\n        entity id.\n        If the entity is a hierarchical you will always get the entity\n        type TypedContext, once retrieved through a get operation you\n        will have the \"real\" entity type ie. example Shot, Sequence\n        or Asset Build.\n        *event* the unmodified original event\n        '''\n        if 'values' in event['data']:\n            values = event['data']['values']\n            if 'number_of_tasks' in values:\n                return {\n                    'success': True,\n                    'message': '',\n                    'items': self.get_task_form_items(\n                        session, int(values['number_of_tasks'])\n                    )\n                }\n            else:\n                # Create tasks on each entity\n                for entity in entities:\n                    for count in range(0, int(len(values.keys()) / 2)):\n                        task_type = session.query(\n                            'Type where id is \"{0}\"'.format(\n                                values[\"task_{0}_typeid\".format(count)]\n                            )\n                        ).one()\n\n                        # Get name, or assume task type in lower case as name.\n                        name = values[\"task_{0}_name\".format(count)]\n                        if not name:\n                            name = task_type[\"name\"].lower()\n\n                        self.ensure_task(session, name, task_type, entity)\n\n                session.commit()\n\n                return {\n                    'success': True,\n                    'message': 'Action completed successfully'\n                }\n\n        return {\n            'success': True,\n            'message': \"\",\n            'items': [\n                {\n                    'label': 'Number of tasks',\n                    'type': 'number',\n                    'name': 'number_of_tasks',\n                    'value': 2\n                }\n            ]\n        }\n\n\ndef register(session):\n    '''Register action. Called when used as an event plugin.'''\n\n    BatchTasksAction(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py",
    "content": "import collections\nimport ftrack_api\nfrom openpype_modules.ftrack.lib import (\n    BaseAction,\n    statics_icon,\n    get_openpype_attr\n)\n\n\nclass CleanHierarchicalAttrsAction(BaseAction):\n    identifier = \"clean.hierarchical.attr\"\n    label = \"OpenPype Admin\"\n    variant = \"- Clean hierarchical custom attributes\"\n    description = \"Unset empty hierarchical attribute values.\"\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"OpenPypeAdmin.svg\")\n\n    all_project_entities_query = (\n        \"select id, name, parent_id, link\"\n        \" from TypedContext where project_id is \\\"{}\\\"\"\n    )\n    cust_attr_query = (\n        \"select value, entity_id from CustomAttributeValue\"\n        \" where entity_id in ({}) and configuration_id is \\\"{}\\\"\"\n    )\n    settings_key = \"clean_hierarchical_attr\"\n\n    def discover(self, session, entities, event):\n        \"\"\"Show only on project entity.\"\"\"\n        if (\n            len(entities) != 1\n            or entities[0].entity_type.lower() != \"project\"\n        ):\n            return False\n\n        return self.valid_roles(session, entities, event)\n\n    def launch(self, session, entities, event):\n        project = entities[0]\n\n        user_message = \"This may take some time\"\n        self.show_message(event, user_message, result=True)\n        self.log.debug(\"Preparing entities for cleanup.\")\n\n        all_entities = session.query(\n            self.all_project_entities_query.format(project[\"id\"])\n        ).all()\n\n        all_entities_ids = [\n            \"\\\"{}\\\"\".format(entity[\"id\"])\n            for entity in all_entities\n            if entity.entity_type.lower() != \"task\"\n        ]\n        self.log.debug(\n            \"Collected {} entities to process.\".format(len(all_entities_ids))\n        )\n        entity_ids_joined = \", \".join(all_entities_ids)\n\n        attrs, hier_attrs = get_openpype_attr(session)\n\n        for attr in hier_attrs:\n            configuration_key = attr[\"key\"]\n            self.log.debug(\n                \"Looking for cleanup of custom attribute \\\"{}\\\"\".format(\n                    configuration_key\n                )\n            )\n            configuration_id = attr[\"id\"]\n            values = session.query(\n                self.cust_attr_query.format(\n                    entity_ids_joined, configuration_id\n                )\n            ).all()\n\n            data = {}\n            for item in values:\n                value = item[\"value\"]\n                if value is None:\n                    data[item[\"entity_id\"]] = value\n\n            if not data:\n                self.log.debug(\n                    \"Nothing to clean for \\\"{}\\\".\".format(configuration_key)\n                )\n                continue\n\n            self.log.debug(\"Cleaning up {} values for \\\"{}\\\".\".format(\n                len(data), configuration_key\n            ))\n            for entity_id, value in data.items():\n                entity_key = collections.OrderedDict((\n                    (\"configuration_id\", configuration_id),\n                    (\"entity_id\", entity_id)\n                ))\n                session.recorded_operations.push(\n                    ftrack_api.operation.DeleteEntityOperation(\n                        \"CustomAttributeValue\",\n                        entity_key\n                    )\n                )\n            session.commit()\n\n        return True\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    CleanHierarchicalAttrsAction(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_client_review_sort.py",
    "content": "from openpype_modules.ftrack.lib import BaseAction, statics_icon\ntry:\n    from functools import cmp_to_key\nexcept Exception:\n    cmp_to_key = None\n\n\ndef existence_comaprison(item_a, item_b):\n    if not item_a and not item_b:\n        return 0\n    if not item_a:\n        return 1\n    if not item_b:\n        return -1\n    return None\n\n\ndef task_name_sorter(item_a, item_b):\n    asset_version_a = item_a[\"asset_version\"]\n    asset_version_b = item_b[\"asset_version\"]\n    asset_version_comp = existence_comaprison(asset_version_a, asset_version_b)\n    if asset_version_comp is not None:\n        return asset_version_comp\n\n    task_a = asset_version_a[\"task\"]\n    task_b = asset_version_b[\"task\"]\n    task_comp = existence_comaprison(task_a, task_b)\n    if task_comp is not None:\n        return task_comp\n\n    if task_a[\"name\"] > task_b[\"name\"]:\n        return 1\n    if task_a[\"name\"] < task_b[\"name\"]:\n        return -1\n    return 0\n\n\nif cmp_to_key:\n    task_name_sorter = cmp_to_key(task_name_sorter)\ntask_name_kwarg_key = \"key\" if cmp_to_key else \"cmp\"\ntask_name_sort_kwargs = {task_name_kwarg_key: task_name_sorter}\n\n\nclass ClientReviewSort(BaseAction):\n    '''Custom action.'''\n\n    #: Action identifier.\n    identifier = 'client.review.sort'\n\n    #: Action label.\n    label = 'Sort Review'\n\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"SortReview.svg\")\n\n    def discover(self, session, entities, event):\n        ''' Validation '''\n\n        if (len(entities) == 0 or entities[0].entity_type != 'ReviewSession'):\n            return False\n\n        return True\n\n    def launch(self, session, entities, event):\n        entity = entities[0]\n\n        # Get all objects from Review Session and all 'sort order' possibilities\n        obj_list = []\n        sort_order_list = []\n        for obj in entity['review_session_objects']:\n            obj_list.append(obj)\n            sort_order_list.append(obj['sort_order'])\n\n        # Sort criteria\n        obj_list = sorted(obj_list, key=lambda k: k['version'])\n        obj_list.sort(**task_name_sort_kwargs)\n        obj_list = sorted(obj_list, key=lambda k: k['name'])\n        # Set 'sort order' to sorted list, so they are sorted in Ftrack also\n        for i in range(len(obj_list)):\n            obj_list[i]['sort_order'] = sort_order_list[i]\n\n        session.commit()\n\n        return {\n            'success': True,\n            'message': 'Client Review sorted!'\n        }\n\n\ndef register(session):\n    '''Register action. Called when used as an event plugin.'''\n\n    ClientReviewSort(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_component_open.py",
    "content": "import os\nimport sys\nimport subprocess\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass ComponentOpen(BaseAction):\n    '''Custom action.'''\n\n    # Action identifier\n    identifier = 'component.open'\n    # Action label\n    label = 'Open File'\n    # Action icon\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"ComponentOpen.svg\")\n\n    def discover(self, session, entities, event):\n        ''' Validation '''\n        if len(entities) != 1 or entities[0].entity_type != 'FileComponent':\n            return False\n\n        return True\n\n    def launch(self, session, entities, event):\n\n        entity = entities[0]\n\n        # Return error if component is on ftrack server\n        location_name = entity['component_locations'][0]['location']['name']\n        if location_name == 'ftrack.server':\n            return {\n                'success': False,\n                'message': \"This component is stored on ftrack server!\"\n            }\n\n        # Get component filepath\n        # TODO with locations it will be different???\n        fpath = entity['component_locations'][0]['resource_identifier']\n        fpath = os.path.normpath(os.path.dirname(fpath))\n\n        if os.path.isdir(fpath):\n            if 'win' in sys.platform:  # windows\n                subprocess.Popen('explorer \"%s\"' % fpath)\n            elif sys.platform == 'darwin':  # macOS\n                subprocess.Popen(['open', fpath])\n            else:  # linux\n                try:\n                    subprocess.Popen(['xdg-open', fpath])\n                except OSError:\n                    raise OSError('unsupported xdg-open call??')\n        else:\n            return {\n                'success': False,\n                'message': \"Didn't find file: \" + fpath\n            }\n\n        return {\n            'success': True,\n            'message': 'Component folder Opened'\n        }\n\n\ndef register(session):\n    '''Register action. Called when used as an event plugin.'''\n\n    ComponentOpen(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py",
    "content": "import collections\nimport json\nimport arrow\nimport ftrack_api\nfrom openpype_modules.ftrack.lib import (\n    BaseAction,\n    statics_icon,\n\n    CUST_ATTR_ID_KEY,\n    CUST_ATTR_GROUP,\n    CUST_ATTR_TOOLS,\n    CUST_ATTR_APPLICATIONS,\n    CUST_ATTR_INTENT,\n    FPS_KEYS,\n\n    default_custom_attributes_definition,\n    app_definitions_from_app_manager,\n    tool_definitions_from_app_manager\n)\n\nfrom openpype.settings import get_system_settings\nfrom openpype.lib import ApplicationManager\n\n\"\"\"\nThis action creates/updates custom attributes.\n## First part take care about special attributes\n    - `avalon_mongo_id` for storing Avalon MongoID\n    - `applications` based on applications usages\n    - `tools` based on tools usages\n\n## Second part is based on json file in ftrack module.\nFile location: `~/OpenPype/pype/modules/ftrack/ftrack_custom_attributes.json`\n\nData in json file is nested dictionary. Keys in first dictionary level\nrepresents Ftrack entity type (task, show, assetversion, user, list, asset)\nand dictionary value define attribute.\n\nThere is special key for hierchical attributes `is_hierarchical`.\n\nEntity types `task` requires to define task object type (Folder, Shot,\nSequence, Task, Library, Milestone, Episode, Asset Build, etc.) at second\ndictionary level, task's attributes are nested more.\n\n*** Not Changeable *********************************************************\n\ngroup (string)\n    - name of group\n    - based on attribute `openpype_modules.ftrack.lib.CUST_ATTR_GROUP`\n        - \"pype\" by default\n\n*** Required ***************************************************************\n\nlabel (string)\n    - label that will show in ftrack\n\nkey (string)\n    - must contain only chars [a-z0-9_]\n\ntype (string)\n    - type of custom attribute\n    - possibilities:\n        text, boolean, date, enumerator, dynamic enumerator, number\n\n*** Required with conditions ***********************************************\n\nconfig (dictionary)\n    - for each attribute type different requirements and possibilities:\n        - enumerator:\n            multiSelect = True/False(default: False)\n            data = {key_1:value_1,key_2:value_2,..,key_n:value_n}\n                - 'data' is Required value with enumerator\n                - 'key' must contain only chars [a-z0-9_]\n\n        - number:\n            isdecimal = True/False(default: False)\n\n        - text:\n            markdown = True/False(default: False)\n\n*** Presetable keys **********************************************************\n\nwrite_security_roles/read_security_roles (array of strings)\n    - default: [\"ALL\"]\n    - strings should be role names (e.g.: [\"API\", \"Administrator\"])\n    - if set to [\"ALL\"] - all roles will be available\n    - if first is 'except' - roles will be set to all except roles in array\n        - Warning: Be careful with except - roles can be different by company\n        - example:\n            write_security_roles = [\"except\", \"User\"]\n            read_security_roles = [\"ALL\"] # (User is can only read)\n\ndefault\n    - default: None\n    - sets default value for custom attribute:\n        - text -> string\n        - number -> integer\n        - enumerator -> array with string of key/s\n        - boolean -> bool true/false\n        - date -> string in format: 'YYYY.MM.DD' or 'YYYY.MM.DD HH:mm:ss'\n            - example: \"2018.12.24\" / \"2018.1.1 6:0:0\"\n        - dynamic enumerator -> DON'T HAVE DEFAULT VALUE!!!\n\nExample:\n```\n\"show\": {\n    \"avalon_auto_sync\": {\n      \"label\": \"Avalon auto-sync\",\n      \"type\": \"boolean\",\n      \"write_security_roles\": [\"API\", \"Administrator\"],\n      \"read_security_roles\": [\"API\", \"Administrator\"]\n    }\n},\n\"is_hierarchical\": {\n    \"fps\": {\n        \"label\": \"FPS\",\n        \"type\": \"number\",\n        \"config\": {\"isdecimal\": true}\n    }\n},\n\"task\": {\n    \"library\": {\n        \"my_attr_name\": {\n            \"label\": \"My Attr\",\n            \"type\": \"number\"\n        }\n    }\n}\n```\n\"\"\"\n\n\nclass CustAttrException(Exception):\n    pass\n\n\nclass CustomAttributes(BaseAction):\n    '''Edit meta data action.'''\n\n    #: Action identifier.\n    identifier = 'create.update.attributes'\n    #: Action label.\n    label = \"OpenPype Admin\"\n    variant = '- Create/Update Custom Attributes'\n    #: Action description.\n    description = 'Creates required custom attributes in ftrack'\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"OpenPypeAdmin.svg\")\n    settings_key = \"create_update_attributes\"\n\n    required_keys = (\"key\", \"label\", \"type\")\n\n    presetable_keys = (\n        \"default\",\n        \"write_security_roles\",\n        \"read_security_roles\"\n    )\n    hierarchical_key = \"is_hierarchical\"\n\n    type_posibilities = (\n        \"text\", \"boolean\", \"date\", \"enumerator\",\n        \"dynamic enumerator\", \"number\"\n    )\n\n    def discover(self, session, entities, event):\n        '''\n        Validation\n        - action is only for Administrators\n        '''\n        return self.valid_roles(session, entities, event)\n\n    def launch(self, session, entities, event):\n        # JOB SETTINGS\n        userId = event['source']['user']['id']\n        user = session.query('User where id is ' + userId).one()\n\n        job = session.create('Job', {\n            'user': user,\n            'status': 'running',\n            'data': json.dumps({\n                'description': 'Custom Attribute creation.'\n            })\n        })\n        session.commit()\n\n        self.app_manager = ApplicationManager()\n\n        try:\n            self.prepare_global_data(session)\n            self.avalon_mongo_id_attributes(session, event)\n            self.applications_attribute(event)\n            self.tools_attribute(event)\n            self.intent_attribute(event)\n            self.custom_attributes_from_file(event)\n\n            job['status'] = 'done'\n            session.commit()\n\n        except Exception:\n            session.rollback()\n            job[\"status\"] = \"failed\"\n            session.commit()\n            self.log.error(\n                \"Creating custom attributes failed ({})\", exc_info=True\n            )\n\n        return True\n\n    def prepare_global_data(self, session):\n        self.types_per_name = {\n            attr_type[\"name\"].lower(): attr_type\n            for attr_type in session.query(\"CustomAttributeType\").all()\n        }\n\n        self.security_roles = {\n            role[\"name\"].lower(): role\n            for role in session.query(\"SecurityRole\").all()\n        }\n\n        object_types = session.query(\"ObjectType\").all()\n        self.object_types_per_id = {\n            object_type[\"id\"]: object_type for object_type in object_types\n        }\n        self.object_types_per_name = {\n            object_type[\"name\"].lower(): object_type\n            for object_type in object_types\n        }\n\n        self.groups = {}\n\n        self.ftrack_settings = get_system_settings()[\"modules\"][\"ftrack\"]\n        self.attrs_settings = self.prepare_attribute_settings()\n\n    def prepare_attribute_settings(self):\n        output = {}\n        attr_settings = self.ftrack_settings[\"custom_attributes\"]\n        for entity_type, attr_data in attr_settings.items():\n            # Lower entity type\n            entity_type = entity_type.lower()\n            # Just store if entity type is not \"task\"\n            if entity_type != \"task\":\n                output[entity_type] = attr_data\n                continue\n\n            # Prepare empty dictionary for entity type if not set yet\n            if entity_type not in output:\n                output[entity_type] = {}\n\n            # Store presets per lowered object type\n            for obj_type, _preset in attr_data.items():\n                output[entity_type][obj_type.lower()] = _preset\n\n        return output\n\n    def avalon_mongo_id_attributes(self, session, event):\n        self.create_hierarchical_mongo_attr(session, event)\n\n        hierarchical_attr, object_type_attrs = (\n            self.mongo_id_custom_attributes(session)\n        )\n        if object_type_attrs:\n            self.convert_mongo_id_to_hierarchical(\n                hierarchical_attr, object_type_attrs, session, event\n            )\n\n    def mongo_id_custom_attributes(self, session):\n        cust_attrs_query = (\n            \"select id, entity_type, object_type_id, is_hierarchical, default\"\n            \" from CustomAttributeConfiguration\"\n            \" where key = \\\"{}\\\"\"\n        ).format(CUST_ATTR_ID_KEY)\n\n        mongo_id_avalon_attr = session.query(cust_attrs_query).all()\n        heirarchical_attr = None\n        object_type_attrs = []\n        for cust_attr in mongo_id_avalon_attr:\n            if cust_attr[\"is_hierarchical\"]:\n                heirarchical_attr = cust_attr\n\n            else:\n                object_type_attrs.append(cust_attr)\n\n        return heirarchical_attr, object_type_attrs\n\n    def create_hierarchical_mongo_attr(self, session, event):\n        # Set security roles for attribute\n        data = {\n            \"key\": CUST_ATTR_ID_KEY,\n            \"label\": \"Avalon/Mongo ID\",\n            \"type\": \"text\",\n            \"default\": \"\",\n            \"group\": CUST_ATTR_GROUP,\n            \"is_hierarchical\": True,\n            \"config\": {\"markdown\": False}\n        }\n\n        self.process_attr_data(data, event)\n\n    def convert_mongo_id_to_hierarchical(\n        self, hierarchical_attr, object_type_attrs, session, event\n    ):\n        user_msg = \"Converting old custom attributes. This may take some time.\"\n        self.show_message(event, user_msg, True)\n        self.log.info(user_msg)\n\n        object_types_per_id = {\n            object_type[\"id\"]: object_type\n            for object_type in session.query(\"ObjectType\").all()\n        }\n\n        cust_attr_query = (\n            \"select value, entity_id from CustomAttributeValue\"\n            \" where configuration_id is {}\"\n        )\n        for attr_def in object_type_attrs:\n            attr_ent_type = attr_def[\"entity_type\"]\n            if attr_ent_type == \"show\":\n                entity_type_label = \"Project\"\n            elif attr_ent_type == \"task\":\n                entity_type_label = (\n                    object_types_per_id[attr_def[\"object_type_id\"]][\"name\"]\n                )\n            else:\n                self.log.warning(\n                    \"Unsupported entity type: \\\"{}\\\". Skipping.\".format(\n                        attr_ent_type\n                    )\n                )\n                continue\n\n            self.log.debug((\n                \"Converting Avalon MongoID attr for Entity type \\\"{}\\\".\"\n            ).format(entity_type_label))\n            values = session.query(\n                cust_attr_query.format(attr_def[\"id\"])\n            ).all()\n            for value in values:\n                table_values = collections.OrderedDict([\n                    (\"configuration_id\", hierarchical_attr[\"id\"]),\n                    (\"entity_id\", value[\"entity_id\"])\n                ])\n\n                session.recorded_operations.push(\n                    ftrack_api.operation.UpdateEntityOperation(\n                        \"ContextCustomAttributeValue\",\n                        table_values,\n                        \"value\",\n                        ftrack_api.symbol.NOT_SET,\n                        value[\"value\"]\n                    )\n                )\n\n            try:\n                session.commit()\n\n            except Exception:\n                session.rollback()\n                self.log.warning(\n                    (\n                        \"Couldn't transfer Avalon Mongo ID\"\n                        \" attribute for entity type \\\"{}\\\".\"\n                    ).format(entity_type_label),\n                    exc_info=True\n                )\n\n            try:\n                session.delete(attr_def)\n                session.commit()\n\n            except Exception:\n                session.rollback()\n                self.log.warning(\n                    (\n                        \"Couldn't delete Avalon Mongo ID\"\n                        \" attribute for entity type \\\"{}\\\".\"\n                    ).format(entity_type_label),\n                    exc_info=True\n                )\n\n    def applications_attribute(self, event):\n        apps_data = app_definitions_from_app_manager(self.app_manager)\n\n        applications_custom_attr_data = {\n            \"label\": \"Applications\",\n            \"key\": CUST_ATTR_APPLICATIONS,\n            \"type\": \"enumerator\",\n            \"entity_type\": \"show\",\n            \"group\": CUST_ATTR_GROUP,\n            \"config\": {\n                \"multiselect\": True,\n                \"data\": apps_data\n            }\n        }\n        self.process_attr_data(applications_custom_attr_data, event)\n\n    def tools_attribute(self, event):\n        tools_data = tool_definitions_from_app_manager(self.app_manager)\n\n        tools_custom_attr_data = {\n            \"label\": \"Tools\",\n            \"key\": CUST_ATTR_TOOLS,\n            \"type\": \"enumerator\",\n            \"is_hierarchical\": True,\n            \"group\": CUST_ATTR_GROUP,\n            \"config\": {\n                \"multiselect\": True,\n                \"data\": tools_data\n            }\n        }\n        self.process_attr_data(tools_custom_attr_data, event)\n\n    def intent_attribute(self, event):\n        intent_key_values = self.ftrack_settings[\"intent\"][\"items\"]\n\n        intent_values = []\n        for key, label in intent_key_values.items():\n            if not key or not label:\n                self.log.info((\n                    \"Skipping intent row: {{\\\"{}\\\": \\\"{}\\\"}}\"\n                    \" because of empty key or label.\"\n                ).format(key, label))\n                continue\n\n            intent_values.append({key: label})\n\n        if not intent_values:\n            return\n\n        intent_custom_attr_data = {\n            \"label\": \"Intent\",\n            \"key\": CUST_ATTR_INTENT,\n            \"type\": \"enumerator\",\n            \"entity_type\": \"assetversion\",\n            \"group\": CUST_ATTR_GROUP,\n            \"config\": {\n                \"multiselect\": False,\n                \"data\": intent_values\n            }\n        }\n        self.process_attr_data(intent_custom_attr_data, event)\n\n    def custom_attributes_from_file(self, event):\n        # Load json with custom attributes configurations\n        cust_attr_def = default_custom_attributes_definition()\n        attrs_data = []\n\n        # Prepare data of hierarchical attributes\n        hierarchical_attrs = cust_attr_def.pop(self.hierarchical_key, {})\n        for key, cust_attr_data in hierarchical_attrs.items():\n            cust_attr_data[\"key\"] = key\n            cust_attr_data[\"is_hierarchical\"] = True\n            attrs_data.append(cust_attr_data)\n\n        # Prepare data of entity specific attributes\n        for entity_type, cust_attr_datas in cust_attr_def.items():\n            if entity_type.lower() != \"task\":\n                for key, cust_attr_data in cust_attr_datas.items():\n                    cust_attr_data[\"key\"] = key\n                    cust_attr_data[\"entity_type\"] = entity_type\n                    attrs_data.append(cust_attr_data)\n                continue\n\n            # Task should have nested level for object type\n            for object_type, _cust_attr_datas in cust_attr_datas.items():\n                for key, cust_attr_data in _cust_attr_datas.items():\n                    cust_attr_data[\"key\"] = key\n                    cust_attr_data[\"entity_type\"] = entity_type\n                    cust_attr_data[\"object_type\"] = object_type\n                    attrs_data.append(cust_attr_data)\n\n        # Process prepared data\n        for cust_attr_data in attrs_data:\n            # Add group\n            cust_attr_data[\"group\"] = CUST_ATTR_GROUP\n            self.process_attr_data(cust_attr_data, event)\n\n    def presets_for_attr_data(self, attr_data):\n        output = {}\n\n        attr_key = attr_data[\"key\"]\n        if attr_data.get(\"is_hierarchical\"):\n            entity_key = self.hierarchical_key\n        else:\n            entity_key = attr_data[\"entity_type\"]\n\n        entity_settings = self.attrs_settings.get(entity_key) or {}\n        if entity_key.lower() == \"task\":\n            object_type = attr_data[\"object_type\"]\n            entity_settings = entity_settings.get(object_type.lower()) or {}\n\n        key_settings = entity_settings.get(attr_key) or {}\n        for key, value in key_settings.items():\n            if key in self.presetable_keys and value:\n                output[key] = value\n        return output\n\n    def process_attr_data(self, cust_attr_data, event):\n        attr_settings = self.presets_for_attr_data(cust_attr_data)\n        cust_attr_data.update(attr_settings)\n\n        try:\n            data = {}\n            # Get key, label, type\n            data.update(self.get_required(cust_attr_data))\n            # Get hierarchical/ entity_type/ object_id\n            data.update(self.get_entity_type(cust_attr_data))\n            # Get group, default, security roles\n            data.update(self.get_optional(cust_attr_data))\n            # Process data\n            self.process_attribute(data)\n\n        except CustAttrException as cae:\n            cust_attr_name = cust_attr_data.get(\"label\", cust_attr_data[\"key\"])\n\n            if cust_attr_name:\n                msg = 'Custom attribute error \"{}\" - {}'.format(\n                    cust_attr_name, str(cae)\n                )\n            else:\n                msg = 'Custom attribute error - {}'.format(str(cae))\n            self.log.warning(msg, exc_info=True)\n            self.show_message(event, msg)\n\n    def process_attribute(self, data):\n        existing_attrs = self.session.query((\n            \"select is_hierarchical, key, type, entity_type, object_type_id\"\n            \" from CustomAttributeConfiguration\"\n        )).all()\n        matching = []\n        is_hierarchical = data.get(\"is_hierarchical\", False)\n        for attr in existing_attrs:\n            if (\n                is_hierarchical != attr[\"is_hierarchical\"]\n                or attr[\"key\"] != data[\"key\"]\n            ):\n                continue\n\n            if attr[\"type\"][\"name\"] != data[\"type\"][\"name\"]:\n                if data[\"key\"] in FPS_KEYS and attr[\"type\"][\"name\"] == \"text\":\n                    self.log.info(\"Kept 'fps' as text custom attribute.\")\n                    return\n                continue\n\n            if is_hierarchical:\n                matching.append(attr)\n\n            elif \"object_type_id\" in data:\n                if (\n                    attr[\"entity_type\"] == data[\"entity_type\"] and\n                    attr[\"object_type_id\"] == data[\"object_type_id\"]\n                ):\n                    matching.append(attr)\n            else:\n                if attr[\"entity_type\"] == data[\"entity_type\"]:\n                    matching.append(attr)\n\n        if len(matching) == 0:\n            self.session.create(\"CustomAttributeConfiguration\", data)\n            self.session.commit()\n            self.log.debug(\n                \"Custom attribute \\\"{}\\\" created\".format(data[\"label\"])\n            )\n\n        elif len(matching) == 1:\n            attr_update = matching[0]\n            for key in data:\n                if key not in (\n                    \"is_hierarchical\", \"entity_type\", \"object_type_id\"\n                ):\n                    attr_update[key] = data[key]\n\n            self.session.commit()\n            self.log.debug(\n                \"Custom attribute \\\"{}\\\" updated\".format(data[\"label\"])\n            )\n\n        else:\n            raise CustAttrException((\n                \"Custom attribute is duplicated. Key: \\\"{}\\\" Type: \\\"{}\\\"\"\n            ).format(data[\"key\"], data[\"type\"][\"name\"]))\n\n    def get_required(self, attr):\n        output = {}\n        for key in self.required_keys:\n            if key not in attr:\n                raise CustAttrException(\n                    \"BUG: Key \\\"{}\\\" is required\".format(key)\n                )\n\n        if attr['type'].lower() not in self.type_posibilities:\n            raise CustAttrException(\n                'Type {} is not valid'.format(attr['type'])\n            )\n\n        output['key'] = attr['key']\n        output['label'] = attr['label']\n\n        type_name = attr['type'].lower()\n        output['type'] = self.types_per_name[type_name]\n\n        config = None\n        if type_name == 'number':\n            config = self.get_number_config(attr)\n        elif type_name == 'text':\n            config = self.get_text_config(attr)\n        elif type_name == 'enumerator':\n            config = self.get_enumerator_config(attr)\n\n        if config is not None:\n            output['config'] = config\n\n        return output\n\n    def get_number_config(self, attr):\n        if 'config' in attr and 'isdecimal' in attr['config']:\n            isdecimal = attr['config']['isdecimal']\n        else:\n            isdecimal = False\n\n        config = json.dumps({'isdecimal': isdecimal})\n\n        return config\n\n    def get_text_config(self, attr):\n        if 'config' in attr and 'markdown' in attr['config']:\n            markdown = attr['config']['markdown']\n        else:\n            markdown = False\n        config = json.dumps({'markdown': markdown})\n\n        return config\n\n    def get_enumerator_config(self, attr):\n        if 'config' not in attr:\n            raise CustAttrException('Missing config with data')\n        if 'data' not in attr['config']:\n            raise CustAttrException('Missing data in config')\n\n        data = []\n        for item in attr['config']['data']:\n            item_data = {}\n            for key in item:\n                # TODO key check by regex\n                item_data['menu'] = item[key]\n                item_data['value'] = key\n                data.append(item_data)\n\n        multiSelect = False\n        for k in attr['config']:\n            if k.lower() == 'multiselect':\n                if isinstance(attr['config'][k], bool):\n                    multiSelect = attr['config'][k]\n                else:\n                    raise CustAttrException('Multiselect must be boolean')\n                break\n\n        config = json.dumps({\n            'multiSelect': multiSelect,\n            'data': json.dumps(data)\n        })\n\n        return config\n\n    def get_group(self, attr):\n        if isinstance(attr, dict):\n            group_name = attr['group'].lower()\n        else:\n            group_name = attr\n        if group_name in self.groups:\n            return self.groups[group_name]\n\n        query = 'CustomAttributeGroup where name is \"{}\"'.format(group_name)\n        groups = self.session.query(query).all()\n\n        if len(groups) == 1:\n            group = groups[0]\n            self.groups[group_name] = group\n\n            return group\n\n        elif len(groups) < 1:\n            group = self.session.create('CustomAttributeGroup', {\n                'name': group_name,\n            })\n            self.session.commit()\n\n            return group\n\n        else:\n            raise CustAttrException(\n                'Found more than one group \"{}\"'.format(group_name)\n            )\n\n    def get_security_roles(self, security_roles):\n        security_roles_lowered = tuple(name.lower() for name in security_roles)\n        if (\n            len(security_roles_lowered) == 0\n            or \"all\" in security_roles_lowered\n        ):\n            return list(self.security_roles.values())\n\n        output = []\n        if security_roles_lowered[0] == \"except\":\n            excepts = security_roles_lowered[1:]\n            for role_name, role in self.security_roles.items():\n                if role_name not in excepts:\n                    output.append(role)\n\n        else:\n            for role_name in security_roles_lowered:\n                if role_name in self.security_roles:\n                    output.append(self.security_roles[role_name])\n                else:\n                    raise CustAttrException((\n                        \"Securit role \\\"{}\\\" was not found in Ftrack.\"\n                    ).format(role_name))\n        return output\n\n    def get_default(self, attr):\n        type = attr['type']\n        default = attr['default']\n        if default is None:\n            return default\n        err_msg = 'Default value is not'\n        if type == 'number':\n            if isinstance(default, (str)) and default.isnumeric():\n                default = float(default)\n\n            if not isinstance(default, (float, int)):\n                raise CustAttrException('{} integer'.format(err_msg))\n        elif type == 'text':\n            if not isinstance(default, str):\n                raise CustAttrException('{} string'.format(err_msg))\n        elif type == 'boolean':\n            if not isinstance(default, bool):\n                raise CustAttrException('{} boolean'.format(err_msg))\n        elif type == 'enumerator':\n            if not isinstance(default, list):\n                raise CustAttrException(\n                    '{} array with strings'.format(err_msg)\n                )\n            # TODO check if multiSelect is available\n            # and if default is one of data menu\n            if not isinstance(default[0], str):\n                raise CustAttrException('{} array of strings'.format(err_msg))\n        elif type == 'date':\n            date_items = default.split(' ')\n            try:\n                if len(date_items) == 1:\n                    default = arrow.get(default, 'YY.M.D')\n                elif len(date_items) == 2:\n                    default = arrow.get(default, 'YY.M.D H:m:s')\n                else:\n                    raise Exception\n            except Exception:\n                raise CustAttrException('Date is not in proper format')\n        elif type == 'dynamic enumerator':\n            raise CustAttrException('Dynamic enumerator can\\'t have default')\n\n        return default\n\n    def get_optional(self, attr):\n        output = {}\n        if \"group\" in attr:\n            output[\"group\"] = self.get_group(attr)\n        if \"default\" in attr:\n            output[\"default\"] = self.get_default(attr)\n\n        roles_read = []\n        roles_write = []\n        if \"read_security_roles\" in attr:\n            roles_read = attr[\"read_security_roles\"]\n        if \"write_security_roles\" in attr:\n            roles_write = attr[\"write_security_roles\"]\n\n        output[\"read_security_roles\"] = self.get_security_roles(roles_read)\n        output[\"write_security_roles\"] = self.get_security_roles(roles_write)\n        return output\n\n    def get_entity_type(self, attr):\n        if attr.get(\"is_hierarchical\", False):\n            return {\n                \"is_hierarchical\": True,\n                \"entity_type\": attr.get(\"entity_type\") or \"show\"\n            }\n\n        if 'entity_type' not in attr:\n            raise CustAttrException('Missing entity_type')\n\n        if attr['entity_type'].lower() != 'task':\n            return {'entity_type': attr['entity_type']}\n\n        if 'object_type' not in attr:\n            raise CustAttrException('Missing object_type')\n\n        object_type_name = attr['object_type']\n        object_type_name_low = object_type_name.lower()\n        object_type = self.object_types_per_name.get(object_type_name_low)\n        if not object_type:\n            raise CustAttrException((\n                'Object type with name \"{}\" don\\'t exist'\n            ).format(object_type_name))\n\n        return {\n            'entity_type': attr['entity_type'],\n            'object_type_id': object_type[\"id\"]\n        }\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    CustomAttributes(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_create_folders.py",
    "content": "import os\nimport collections\nimport copy\nfrom openpype.pipeline import Anatomy\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass CreateFolders(BaseAction):\n    identifier = \"create.folders\"\n    label = \"Create Folders\"\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"CreateFolders.svg\")\n\n    def discover(self, session, entities, event):\n        for entity_item in event[\"data\"][\"selection\"]:\n            if entity_item.get(\"entityType\").lower() in (\"task\", \"show\"):\n                return True\n        return False\n\n    def interface(self, session, entities, event):\n        if event[\"data\"].get(\"values\", {}):\n            return\n\n        with_interface = False\n        for entity in entities:\n            if entity.entity_type.lower() != \"task\":\n                with_interface = True\n                break\n\n        if \"values\" not in event[\"data\"]:\n            event[\"data\"][\"values\"] = {}\n\n        event[\"data\"][\"values\"][\"with_interface\"] = with_interface\n        if not with_interface:\n            return\n\n        title = \"Create folders\"\n\n        entity_name = entity[\"name\"]\n        msg = (\n            \"<h2>Do you want create folders also\"\n            \" for all children of your selection?</h2>\"\n        )\n        if entity.entity_type.lower() == \"project\":\n            entity_name = entity[\"full_name\"]\n            msg = msg.replace(\" also\", \"\")\n            msg += \"<h3>(Project root won't be created if not checked)</h3>\"\n        items = [\n            {\n                \"type\": \"label\",\n                \"value\": msg.format(entity_name)\n            },\n            {\n                \"type\": \"label\",\n                \"value\": \"With all children entities\"\n            },\n            {\n                \"name\": \"children_included\",\n                \"type\": \"boolean\",\n                \"value\": False\n            },\n            {\n                \"type\": \"hidden\",\n                \"name\": \"with_interface\",\n                \"value\": with_interface\n            }\n        ]\n\n        return {\n            \"items\": items,\n            \"title\": title\n        }\n\n    def launch(self, session, entities, event):\n        '''Callback method for custom action.'''\n\n        if \"values\" not in event[\"data\"]:\n            return\n\n        with_interface = event[\"data\"][\"values\"][\"with_interface\"]\n        with_childrens = True\n        if with_interface:\n            with_childrens = event[\"data\"][\"values\"][\"children_included\"]\n\n        filtered_entities = []\n        for entity in entities:\n            low_context_type = entity[\"context_type\"].lower()\n            if low_context_type in (\"task\", \"show\"):\n                if not with_childrens and low_context_type == \"show\":\n                    continue\n                filtered_entities.append(entity)\n\n        if not filtered_entities:\n            return {\n                \"success\": True,\n                \"message\": 'Nothing was created'\n            }\n\n        project_entity = self.get_project_from_entity(filtered_entities[0])\n\n        project_name = project_entity[\"full_name\"]\n        project_code = project_entity[\"name\"]\n\n        task_entities = []\n        other_entities = []\n        self.get_all_entities(\n            session, entities, task_entities, other_entities\n        )\n        hierarchy = self.get_entities_hierarchy(\n            session, task_entities, other_entities\n        )\n        task_types = session.query(\"select id, name from Type\").all()\n        task_type_names_by_id = {\n            task_type[\"id\"]: task_type[\"name\"]\n            for task_type in task_types\n        }\n\n        anatomy = Anatomy(project_name)\n\n        work_keys = [\"work\", \"folder\"]\n        work_template = anatomy.templates\n        for key in work_keys:\n            work_template = work_template[key]\n\n        publish_keys = [\"publish\", \"folder\"]\n        publish_template = anatomy.templates\n        for key in publish_keys:\n            publish_template = publish_template[key]\n\n        project_data = {\n            \"project\": {\n                \"name\": project_name,\n                \"code\": project_code\n            }\n        }\n\n        collected_paths = []\n        for item in hierarchy:\n            parent_entity, task_entities = item\n\n            parent_data = copy.deepcopy(project_data)\n\n            parents = parent_entity[\"link\"][1:-1]\n            hierarchy_names = [p[\"name\"] for p in parents]\n            hierarchy = \"/\".join(hierarchy_names)\n\n            if hierarchy_names:\n                parent_name = hierarchy_names[-1]\n            else:\n                parent_name = project_name\n\n            parent_data.update({\n                \"asset\": parent_entity[\"name\"],\n                \"hierarchy\": hierarchy,\n                \"parent\": parent_name\n            })\n\n            if not task_entities:\n                # create path for entity\n                collected_paths.append(self.compute_template(\n                    anatomy, parent_data, work_keys\n                ))\n                collected_paths.append(self.compute_template(\n                    anatomy, parent_data, publish_keys\n                ))\n                continue\n\n            for task_entity in task_entities:\n                task_type_id = task_entity[\"type_id\"]\n                task_type_name = task_type_names_by_id[task_type_id]\n                task_data = copy.deepcopy(parent_data)\n                task_data[\"task\"] = {\n                    \"name\": task_entity[\"name\"],\n                    \"type\": task_type_name\n                }\n\n                # Template wok\n                collected_paths.append(self.compute_template(\n                    anatomy, task_data, work_keys\n                ))\n\n                # Template publish\n                collected_paths.append(self.compute_template(\n                    anatomy, task_data, publish_keys\n                ))\n\n        if len(collected_paths) == 0:\n            return {\n                \"success\": True,\n                \"message\": \"No project folders to create.\"\n            }\n\n        self.log.info(\"Creating folders:\")\n\n        for path in set(collected_paths):\n            self.log.info(path)\n            if not os.path.exists(path):\n                os.makedirs(path)\n\n        return {\n            \"success\": True,\n            \"message\": \"Successfully created project folders.\"\n        }\n\n    def get_all_entities(\n        self, session, entities, task_entities, other_entities\n    ):\n        if not entities:\n            return\n\n        no_task_entities = []\n        for entity in entities:\n            if entity.entity_type.lower() == \"task\":\n                task_entities.append(entity)\n            else:\n                no_task_entities.append(entity)\n\n        if not no_task_entities:\n            return task_entities\n\n        other_entities.extend(no_task_entities)\n\n        no_task_entity_ids = [entity[\"id\"] for entity in no_task_entities]\n        next_entities = session.query((\n            \"select id, parent_id\"\n            \" from TypedContext where parent_id in ({})\"\n        ).format(self.join_query_keys(no_task_entity_ids))).all()\n\n        self.get_all_entities(\n            session, next_entities, task_entities, other_entities\n        )\n\n    def get_entities_hierarchy(self, session, task_entities, other_entities):\n        task_entity_ids = [entity[\"id\"] for entity in task_entities]\n        full_task_entities = session.query((\n            \"select id, name, type_id, parent_id\"\n            \" from TypedContext where id in ({})\"\n        ).format(self.join_query_keys(task_entity_ids)))\n        task_entities_by_parent_id = collections.defaultdict(list)\n        for entity in full_task_entities:\n            parent_id = entity[\"parent_id\"]\n            task_entities_by_parent_id[parent_id].append(entity)\n\n        output = []\n        if not task_entities_by_parent_id:\n            return output\n\n        other_ids = set()\n        for entity in other_entities:\n            other_ids.add(entity[\"id\"])\n        other_ids |= set(task_entities_by_parent_id.keys())\n\n        parent_entities = session.query((\n            \"select id, name from TypedContext where id in ({})\"\n        ).format(self.join_query_keys(other_ids))).all()\n\n        for parent_entity in parent_entities:\n            parent_id = parent_entity[\"id\"]\n            output.append((\n                parent_entity,\n                task_entities_by_parent_id[parent_id]\n            ))\n\n        return output\n\n    def compute_template(self, anatomy, data, anatomy_keys):\n        filled_template = anatomy.format_all(data)\n        for key in anatomy_keys:\n            filled_template = filled_template[key]\n\n        if filled_template.solved:\n            return os.path.normpath(filled_template)\n\n        self.log.warning(\n            \"Template \\\"{}\\\" was not fully filled \\\"{}\\\"\".format(\n                filled_template.template, filled_template\n            )\n        )\n        return os.path.normpath(filled_template.split(\"{\")[0])\n\n\ndef register(session):\n    \"\"\"Register plugin. Called when used as an plugin.\"\"\"\n    CreateFolders(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py",
    "content": "import re\n\nfrom openpype.pipeline.project_folders import (\n    get_project_basic_paths,\n    create_project_folders,\n)\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass CreateProjectFolders(BaseAction):\n    \"\"\"Action create folder structure and may create hierarchy in Ftrack.\n\n    Creation of folder structure and hierarchy in Ftrack is based on presets.\n    These presets are located in:\n    `~/pype-config/presets/tools/project_folder_structure.json`\n\n    Example of content:\n    ```json\n    {\n        \"__project_root__\": {\n            \"prod\" : {},\n            \"resources\" : {\n              \"footage\": {\n                \"plates\": {},\n                \"offline\": {}\n              },\n              \"audio\": {},\n              \"art_dept\": {}\n            },\n            \"editorial\" : {},\n            \"assets[ftrack.Library]\": {\n              \"characters[ftrack]\": {},\n              \"locations[ftrack]\": {}\n            },\n            \"shots[ftrack.Sequence]\": {\n              \"scripts\": {},\n              \"editorial[ftrack.Folder]\": {}\n            }\n        }\n    }\n    ```\n    Key \"__project_root__\" indicates root folder (or entity). Each key in\n    dictionary represents folder name. Value may contain another dictionary\n    with subfolders.\n\n    Identifier `[ftrack]` in name says that this should be also created in\n    Ftrack hierarchy. It is possible to specify entity type of item with \".\" .\n    If key is `assets[ftrack.Library]` then in ftrack will be created entity\n    with name \"assets\" and entity type \"Library\". It is expected Library entity\n    type exist in Ftrack.\n    \"\"\"\n\n    identifier = \"create.project.structure\"\n    label = \"Create Project Structure\"\n    description = \"Creates folder structure\"\n    role_list = [\"Pypeclub\", \"Administrator\", \"Project Manager\"]\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"CreateProjectFolders.svg\")\n\n    pattern_array = re.compile(r\"\\[.*\\]\")\n    pattern_ftrack = re.compile(r\".*\\[[.]*ftrack[.]*\")\n    pattern_ent_ftrack = re.compile(r\"ftrack\\.[^.,\\],\\s,]*\")\n    project_root_key = \"__project_root__\"\n\n    def discover(self, session, entities, event):\n        if len(entities) != 1:\n            return False\n\n        if entities[0].entity_type.lower() != \"project\":\n            return False\n\n        return True\n\n    def launch(self, session, entities, event):\n        # Get project entity\n        project_entity = self.get_project_from_entity(entities[0])\n        project_name = project_entity[\"full_name\"]\n        try:\n            # Get paths based on presets\n            basic_paths = get_project_basic_paths(project_name)\n            if not basic_paths:\n                return {\n                    \"success\": False,\n                    \"message\": \"Project structure is not set.\"\n                }\n\n            # Invoking OpenPype API to create the project folders\n            create_project_folders(project_name, basic_paths)\n            self.create_ftrack_entities(basic_paths, project_entity)\n\n            self.trigger_event(\n                \"openpype.project.structure.created\",\n                {\"project_name\": project_name}\n            )\n\n        except Exception as exc:\n            self.log.warning(\"Creating of structure crashed.\", exc_info=True)\n            session.rollback()\n            return {\n                \"success\": False,\n                \"message\": str(exc)\n            }\n\n        return True\n\n    def get_ftrack_paths(self, paths_items):\n        all_ftrack_paths = []\n        for path_items in paths_items:\n            ftrack_path_items = []\n            is_ftrack = False\n            for item in reversed(path_items):\n                if item == self.project_root_key:\n                    continue\n                if is_ftrack:\n                    ftrack_path_items.append(item)\n                elif re.match(self.pattern_ftrack, item):\n                    ftrack_path_items.append(item)\n                    is_ftrack = True\n            ftrack_path_items = list(reversed(ftrack_path_items))\n            if ftrack_path_items:\n                all_ftrack_paths.append(ftrack_path_items)\n        return all_ftrack_paths\n\n    def compute_ftrack_items(self, in_list, keys):\n        if len(keys) == 0:\n            return in_list\n        key = keys[0]\n        exist = None\n        for index, subdict in enumerate(in_list):\n            if key in subdict:\n                exist = index\n                break\n        if exist is not None:\n            in_list[exist][key] = self.compute_ftrack_items(\n                in_list[exist][key], keys[1:]\n            )\n        else:\n            in_list.append({key: self.compute_ftrack_items([], keys[1:])})\n        return in_list\n\n    def translate_ftrack_items(self, paths_items):\n        main = []\n        for path_items in paths_items:\n            main = self.compute_ftrack_items(main, path_items)\n        return main\n\n    def create_ftrack_entities(self, basic_paths, project_ent):\n        only_ftrack_items = self.get_ftrack_paths(basic_paths)\n        ftrack_paths = self.translate_ftrack_items(only_ftrack_items)\n\n        for separation in ftrack_paths:\n            parent = project_ent\n            self.trigger_creation(separation, parent)\n\n    def trigger_creation(self, separation, parent):\n        for item, subvalues in separation.items():\n            matches = re.findall(self.pattern_array, item)\n            ent_type = \"Folder\"\n            if len(matches) == 0:\n                name = item\n            else:\n                match = matches[0]\n                name = item.replace(match, \"\")\n                ent_type_match = re.findall(self.pattern_ent_ftrack, match)\n                if len(ent_type_match) > 0:\n                    ent_type_split = ent_type_match[0].split(\".\")\n                    if len(ent_type_split) == 2:\n                        ent_type = ent_type_split[1]\n            new_parent = self.create_ftrack_entity(name, ent_type, parent)\n            if subvalues:\n                for subvalue in subvalues:\n                    self.trigger_creation(subvalue, new_parent)\n\n    def create_ftrack_entity(self, name, ent_type, parent):\n        for children in parent[\"children\"]:\n            if children[\"name\"] == name:\n                return children\n        data = {\n            \"name\": name,\n            \"parent_id\": parent[\"id\"]\n        }\n        if parent.entity_type.lower() == \"project\":\n            data[\"project_id\"] = parent[\"id\"]\n        else:\n            data[\"project_id\"] = parent[\"project\"][\"id\"]\n\n        existing_entity = self.session.query((\n            \"TypedContext where name is \\\"{}\\\" and \"\n            \"parent_id is \\\"{}\\\" and project_id is \\\"{}\\\"\"\n        ).format(name, data[\"parent_id\"], data[\"project_id\"])).first()\n        if existing_entity:\n            return existing_entity\n\n        new_ent = self.session.create(ent_type, data)\n        self.session.commit()\n        return new_ent\n\n\ndef register(session):\n    CreateProjectFolders(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_delete_asset.py",
    "content": "import collections\nimport uuid\nfrom datetime import datetime\n\nfrom bson.objectid import ObjectId\n\nfrom openpype.client import get_assets, get_subsets\nfrom openpype.pipeline import AvalonMongoDB\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\nfrom openpype_modules.ftrack.lib.avalon_sync import create_chunks\n\n\nclass DeleteAssetSubset(BaseAction):\n    '''Edit meta data action.'''\n\n    # Action identifier.\n    identifier = \"delete.asset.subset\"\n    # Action label.\n    label = \"Delete Asset/Subsets\"\n    # Action description.\n    description = \"Removes from Avalon with all children and asset from Ftrack\"\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"DeleteAsset.svg\")\n\n    settings_key = \"delete_asset_subset\"\n    # Db connection\n    dbcon = None\n\n    splitter = {\"type\": \"label\", \"value\": \"---\"}\n    action_data_by_id = {}\n    asset_prefix = \"asset:\"\n    subset_prefix = \"subset:\"\n\n    def __init__(self, *args, **kwargs):\n        self.dbcon = AvalonMongoDB()\n\n        super(DeleteAssetSubset, self).__init__(*args, **kwargs)\n\n    def discover(self, session, entities, event):\n        \"\"\" Validation \"\"\"\n        task_ids = []\n        for ent_info in event[\"data\"][\"selection\"]:\n            if ent_info.get(\"entityType\") == \"task\":\n                task_ids.append(ent_info[\"entityId\"])\n\n        is_valid = False\n        for entity in entities:\n            if (\n                entity[\"id\"] in task_ids\n                and entity.entity_type.lower() != \"task\"\n            ):\n                is_valid = True\n                break\n\n        if is_valid:\n            is_valid = self.valid_roles(session, entities, event)\n        return is_valid\n\n    def _launch(self, event):\n        try:\n            entities = self._translate_event(event)\n            if \"values\" not in event[\"data\"]:\n                self.dbcon.install()\n                return self._interface(self.session, entities, event)\n\n            confirmation = self.confirm_delete(entities, event)\n            if confirmation:\n                return confirmation\n\n            self.dbcon.install()\n            response = self.launch(\n                self.session, entities, event\n            )\n        finally:\n            self.dbcon.uninstall()\n\n        return self._handle_result(response)\n\n    def interface(self, session, entities, event):\n        self.show_message(event, \"Preparing data...\", True)\n        items = []\n        title = \"Choose items to delete\"\n\n        # Filter selection and get ftrack ids\n        selection = event[\"data\"].get(\"selection\") or []\n        ftrack_ids = []\n        project_in_selection = False\n        for entity in selection:\n            entity_type = (entity.get(\"entityType\") or \"\").lower()\n            if entity_type != \"task\":\n                if entity_type == \"show\":\n                    project_in_selection = True\n                continue\n\n            ftrack_id = entity.get(\"entityId\")\n            if ftrack_id:\n                ftrack_ids.append(ftrack_id)\n\n        if project_in_selection:\n            msg = \"It is not possible to use this action on project entity.\"\n            self.show_message(event, msg, True)\n\n        # Filter event even more (skip task entities)\n        # - task entities are not relevant for avalon\n        entity_mapping = {}\n        for entity in entities:\n            ftrack_id = entity[\"id\"]\n            if ftrack_id not in ftrack_ids:\n                continue\n\n            if entity.entity_type.lower() == \"task\":\n                ftrack_ids.remove(ftrack_id)\n\n            entity_mapping[ftrack_id] = entity\n\n        if not ftrack_ids:\n            # It is bug if this happens!\n            return {\n                \"success\": False,\n                \"message\": \"Invalid selection for this action (Bug)\"\n            }\n\n        project = self.get_project_from_entity(entities[0], session)\n        project_name = project[\"full_name\"]\n        self.dbcon.Session[\"AVALON_PROJECT\"] = project_name\n\n        asset_docs = list(get_assets(\n            project_name,\n            fields=[\"_id\", \"name\", \"data.ftrackId\", \"data.parents\"]\n        ))\n        selected_av_entities = []\n        found_ftrack_ids = set()\n        asset_docs_by_name = collections.defaultdict(list)\n        for asset_doc in asset_docs:\n            ftrack_id = asset_doc[\"data\"].get(\"ftrackId\")\n            if ftrack_id:\n                found_ftrack_ids.add(ftrack_id)\n                if ftrack_id in entity_mapping:\n                    selected_av_entities.append(asset_doc)\n\n            asset_name = asset_doc[\"name\"]\n            asset_docs_by_name[asset_name].append(asset_doc)\n\n        found_without_ftrack_id = {}\n        for ftrack_id, entity in entity_mapping.items():\n            if ftrack_id in found_ftrack_ids:\n                continue\n\n            av_ents_by_name = asset_docs_by_name[entity[\"name\"]]\n            if not av_ents_by_name:\n                continue\n\n            ent_path_items = [ent[\"name\"] for ent in entity[\"link\"]]\n            end_index = len(ent_path_items) - 1\n            parents = ent_path_items[1:end_index:]\n            # TODO we should say to user that\n            # few of them are missing in avalon\n            for av_ent in av_ents_by_name:\n                if av_ent[\"data\"][\"parents\"] != parents:\n                    continue\n\n                # TODO we should say to user that found entity\n                # with same name does not match same ftrack id?\n                if \"ftrackId\" not in av_ent[\"data\"]:\n                    selected_av_entities.append(av_ent)\n                    found_without_ftrack_id[str(av_ent[\"_id\"])] = ftrack_id\n                    break\n\n        if not selected_av_entities:\n            return {\n                \"success\": True,\n                \"message\": (\n                    \"Didn't find entities in avalon.\"\n                    \" You can use Ftrack's Delete button for the selection.\"\n                )\n            }\n\n        # Remove cached action older than 2 minutes\n        old_action_ids = []\n        for action_id, data in self.action_data_by_id.items():\n            created_at = data.get(\"created_at\")\n            if not created_at:\n                old_action_ids.append(action_id)\n                continue\n            cur_time = datetime.now()\n            existing_in_sec = (created_at - cur_time).total_seconds()\n            if existing_in_sec > 60 * 2:\n                old_action_ids.append(action_id)\n\n        for action_id in old_action_ids:\n            self.action_data_by_id.pop(action_id, None)\n\n        # Store data for action id\n        action_id = str(uuid.uuid1())\n        self.action_data_by_id[action_id] = {\n            \"attempt\": 1,\n            \"created_at\": datetime.now(),\n            \"project_name\": project_name,\n            \"subset_ids_by_name\": {},\n            \"subset_ids_by_parent\": {},\n            \"without_ftrack_id\": found_without_ftrack_id\n        }\n\n        id_item = {\n            \"type\": \"hidden\",\n            \"name\": \"action_id\",\n            \"value\": action_id\n        }\n\n        items.append(id_item)\n        asset_ids = [ent[\"_id\"] for ent in selected_av_entities]\n        subsets_for_selection = get_subsets(project_name, asset_ids=asset_ids)\n\n        asset_ending = \"\"\n        if len(selected_av_entities) > 1:\n            asset_ending = \"s\"\n\n        asset_title = {\n            \"type\": \"label\",\n            \"value\": \"# Delete asset{}:\".format(asset_ending)\n        }\n        asset_note = {\n            \"type\": \"label\",\n            \"value\": (\n                \"<p><i>NOTE: Action will delete checked entities\"\n                \" in Ftrack and Avalon with all children entities and\"\n                \" published content.</i></p>\"\n            )\n        }\n\n        items.append(asset_title)\n        items.append(asset_note)\n\n        asset_items = collections.defaultdict(list)\n        for asset in selected_av_entities:\n            ent_path_items = [project_name]\n            ent_path_items.extend(asset.get(\"data\", {}).get(\"parents\") or [])\n            ent_path_to_parent = \"/\".join(ent_path_items) + \"/\"\n            asset_items[ent_path_to_parent].append(asset)\n\n        for asset_parent_path, assets in sorted(asset_items.items()):\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"## <b>- {}</b>\".format(asset_parent_path)\n            })\n            for asset in assets:\n                items.append({\n                    \"label\": asset[\"name\"],\n                    \"name\": \"{}{}\".format(\n                        self.asset_prefix, str(asset[\"_id\"])\n                    ),\n                    \"type\": 'boolean',\n                    \"value\": False\n                })\n\n        subset_ids_by_name = collections.defaultdict(list)\n        subset_ids_by_parent = collections.defaultdict(list)\n        for subset in subsets_for_selection:\n            subset_id = subset[\"_id\"]\n            name = subset[\"name\"]\n            parent_id = subset[\"parent\"]\n            subset_ids_by_name[name].append(subset_id)\n            subset_ids_by_parent[parent_id].append(subset_id)\n\n        if not subset_ids_by_name:\n            return {\n                \"items\": items,\n                \"title\": title\n            }\n\n        subset_ending = \"\"\n        if len(subset_ids_by_name.keys()) > 1:\n            subset_ending = \"s\"\n\n        subset_title = {\n            \"type\": \"label\",\n            \"value\": \"# Subset{} to delete:\".format(subset_ending)\n        }\n        subset_note = {\n            \"type\": \"label\",\n            \"value\": (\n                \"<p><i>WARNING: Subset{} will be removed\"\n                \" for all <b>selected</b> entities.</i></p>\"\n            ).format(subset_ending)\n        }\n\n        items.append(self.splitter)\n        items.append(subset_title)\n        items.append(subset_note)\n\n        for name in subset_ids_by_name:\n            items.append({\n                \"label\": \"<b>{}</b>\".format(name),\n                \"name\": \"{}{}\".format(self.subset_prefix, name),\n                \"type\": \"boolean\",\n                \"value\": False\n            })\n\n        self.action_data_by_id[action_id][\"subset_ids_by_parent\"] = (\n            subset_ids_by_parent\n        )\n        self.action_data_by_id[action_id][\"subset_ids_by_name\"] = (\n            subset_ids_by_name\n        )\n\n        return {\n            \"items\": items,\n            \"title\": title\n        }\n\n    def confirm_delete(self, entities, event):\n        values = event[\"data\"][\"values\"]\n        action_id = values.get(\"action_id\")\n        spec_data = self.action_data_by_id.get(action_id)\n        if not spec_data:\n            # it is a bug if this happens!\n            return {\n                \"success\": False,\n                \"message\": \"Something bad has happened. Please try again.\"\n            }\n\n        # Process Delete confirmation\n        delete_key = values.get(\"delete_key\")\n        if delete_key:\n            delete_key = delete_key.lower().strip()\n            # Go to launch part if user entered `delete`\n            if delete_key == \"delete\":\n                return\n            # Skip whole process if user didn't enter any text\n            elif delete_key == \"\":\n                self.action_data_by_id.pop(action_id, None)\n                return {\n                    \"success\": True,\n                    \"message\": \"Deleting cancelled (delete entry was empty)\"\n                }\n            # Get data to show again\n            to_delete = spec_data[\"to_delete\"]\n\n        else:\n            to_delete = collections.defaultdict(list)\n            for key, value in values.items():\n                if not value:\n                    continue\n                if key.startswith(self.asset_prefix):\n                    _key = key.replace(self.asset_prefix, \"\")\n                    to_delete[\"assets\"].append(_key)\n\n                elif key.startswith(self.subset_prefix):\n                    _key = key.replace(self.subset_prefix, \"\")\n                    to_delete[\"subsets\"].append(_key)\n\n            self.action_data_by_id[action_id][\"to_delete\"] = to_delete\n\n        asset_to_delete = len(to_delete.get(\"assets\") or []) > 0\n        subset_to_delete = len(to_delete.get(\"subsets\") or []) > 0\n\n        if not asset_to_delete and not subset_to_delete:\n            self.action_data_by_id.pop(action_id, None)\n            return {\n                \"success\": True,\n                \"message\": \"Nothing was selected to delete\"\n            }\n\n        attempt = spec_data[\"attempt\"]\n        if attempt > 3:\n            self.action_data_by_id.pop(action_id, None)\n            return {\n                \"success\": False,\n                \"message\": \"You didn't enter \\\"DELETE\\\" properly 3 times!\"\n            }\n\n        self.action_data_by_id[action_id][\"attempt\"] += 1\n\n        title = \"Confirmation of deleting\"\n\n        if asset_to_delete:\n            asset_len = len(to_delete[\"assets\"])\n            asset_ending = \"\"\n            if asset_len > 1:\n                asset_ending = \"s\"\n            title += \" {} Asset{}\".format(asset_len, asset_ending)\n            if subset_to_delete:\n                title += \" and\"\n\n        if subset_to_delete:\n            sub_len = len(to_delete[\"subsets\"])\n            type_ending = \"\"\n            sub_ending = \"\"\n            if sub_len == 1:\n                subset_ids_by_name = spec_data[\"subset_ids_by_name\"]\n                if len(subset_ids_by_name[to_delete[\"subsets\"][0]]) > 1:\n                    sub_ending = \"s\"\n\n            elif sub_len > 1:\n                type_ending = \"s\"\n                sub_ending = \"s\"\n\n            title += \" {} type{} of subset{}\".format(\n                sub_len, type_ending, sub_ending\n            )\n\n        items = []\n\n        id_item = {\"type\": \"hidden\", \"name\": \"action_id\", \"value\": action_id}\n        delete_label = {\n            'type': 'label',\n            'value': '# Please enter \"DELETE\" to confirm #'\n        }\n        delete_item = {\n            \"name\": \"delete_key\",\n            \"type\": \"text\",\n            \"value\": \"\",\n            \"empty_text\": \"Type Delete here...\"\n        }\n\n        items.append(id_item)\n        items.append(delete_label)\n        items.append(delete_item)\n\n        return {\n            \"items\": items,\n            \"title\": title\n        }\n\n    def launch(self, session, entities, event):\n        self.show_message(event, \"Processing...\", True)\n        values = event[\"data\"][\"values\"]\n        action_id = values.get(\"action_id\")\n        spec_data = self.action_data_by_id.get(action_id)\n        if not spec_data:\n            # it is a bug if this happens!\n            return {\n                \"success\": False,\n                \"message\": \"Something bad has happened. Please try again.\"\n            }\n\n        report_messages = collections.defaultdict(list)\n\n        project_name = spec_data[\"project_name\"]\n        to_delete = spec_data[\"to_delete\"]\n        self.dbcon.Session[\"AVALON_PROJECT\"] = project_name\n\n        assets_to_delete = to_delete.get(\"assets\") or []\n        subsets_to_delete = to_delete.get(\"subsets\") or []\n\n        # Convert asset ids to ObjectId obj\n        assets_to_delete = [\n            ObjectId(asset_id)\n            for asset_id in assets_to_delete\n            if asset_id\n        ]\n\n        subset_ids_by_parent = spec_data[\"subset_ids_by_parent\"]\n        subset_ids_by_name = spec_data[\"subset_ids_by_name\"]\n\n        subset_ids_to_archive = []\n        asset_ids_to_archive = []\n        ftrack_ids_to_delete = []\n        if len(assets_to_delete) > 0:\n            map_av_ftrack_id = spec_data[\"without_ftrack_id\"]\n            # Prepare data when deleting whole avalon asset\n            avalon_assets = get_assets(\n                project_name,\n                fields=[\"_id\", \"data.visualParent\", \"data.ftrackId\"]\n            )\n            avalon_assets_by_parent = collections.defaultdict(list)\n            for asset in avalon_assets:\n                asset_id = asset[\"_id\"]\n                parent_id = asset[\"data\"][\"visualParent\"]\n                avalon_assets_by_parent[parent_id].append(asset)\n                if asset_id in assets_to_delete:\n                    ftrack_id = map_av_ftrack_id.get(str(asset_id))\n                    if not ftrack_id:\n                        ftrack_id = asset[\"data\"].get(\"ftrackId\")\n\n                    if ftrack_id:\n                        ftrack_ids_to_delete.append(ftrack_id)\n\n            children_queue = collections.deque()\n            for mongo_id in assets_to_delete:\n                children_queue.append(mongo_id)\n\n            while children_queue:\n                mongo_id = children_queue.popleft()\n                if mongo_id in asset_ids_to_archive:\n                    continue\n\n                asset_ids_to_archive.append(mongo_id)\n                for subset_id in subset_ids_by_parent.get(mongo_id, []):\n                    if subset_id not in subset_ids_to_archive:\n                        subset_ids_to_archive.append(subset_id)\n\n                children = avalon_assets_by_parent.get(mongo_id)\n                if not children:\n                    continue\n\n                for child in children:\n                    child_id = child[\"_id\"]\n                    if child_id not in asset_ids_to_archive:\n                        children_queue.append(child_id)\n\n        # Prepare names of assets in ftrack and ids of subsets in mongo\n        asset_names_to_delete = []\n        if len(subsets_to_delete) > 0:\n            for name in subsets_to_delete:\n                asset_names_to_delete.append(name)\n                for subset_id in subset_ids_by_name[name]:\n                    if subset_id in subset_ids_to_archive:\n                        continue\n                    subset_ids_to_archive.append(subset_id)\n\n        # Get ftrack ids of entities where will be delete only asset\n        not_deleted_entities_id = []\n        ftrack_id_name_map = {}\n        if asset_names_to_delete:\n            for entity in entities:\n                ftrack_id = entity[\"id\"]\n                ftrack_id_name_map[ftrack_id] = entity[\"name\"]\n                if ftrack_id not in ftrack_ids_to_delete:\n                    not_deleted_entities_id.append(ftrack_id)\n\n        mongo_proc_txt = \"MongoProcessing: \"\n        ftrack_proc_txt = \"Ftrack processing: \"\n        if asset_ids_to_archive:\n            self.log.debug(\"{}Archivation of assets <{}>\".format(\n                mongo_proc_txt,\n                \", \".join([str(id) for id in asset_ids_to_archive])\n            ))\n            self.dbcon.update_many(\n                {\n                    \"_id\": {\"$in\": asset_ids_to_archive},\n                    \"type\": \"asset\"\n                },\n                {\"$set\": {\"type\": \"archived_asset\"}}\n            )\n\n        if subset_ids_to_archive:\n            self.log.debug(\"{}Archivation of subsets <{}>\".format(\n                mongo_proc_txt,\n                \", \".join([str(id) for id in subset_ids_to_archive])\n            ))\n            self.dbcon.update_many(\n                {\n                    \"_id\": {\"$in\": subset_ids_to_archive},\n                    \"type\": \"subset\"\n                },\n                {\"$set\": {\"type\": \"archived_subset\"}}\n            )\n\n        if ftrack_ids_to_delete:\n            self.log.debug(\"{}Deleting Ftrack Entities <{}>\".format(\n                ftrack_proc_txt, \", \".join(ftrack_ids_to_delete)\n            ))\n\n            entities_by_link_len = self._prepare_entities_before_delete(\n                ftrack_ids_to_delete, session\n            )\n            for link_len in sorted(entities_by_link_len.keys(), reverse=True):\n                for entity in entities_by_link_len[link_len]:\n                    session.delete(entity)\n\n                try:\n                    session.commit()\n                except Exception:\n                    ent_path = \"/\".join(\n                        [ent[\"name\"] for ent in entity[\"link\"]]\n                    )\n                    msg = \"Failed to delete entity\"\n                    report_messages[msg].append(ent_path)\n                    session.rollback()\n                    self.log.warning(\n                        \"{} <{}>\".format(msg, ent_path),\n                        exc_info=True\n                    )\n\n        if not_deleted_entities_id and asset_names_to_delete:\n            joined_not_deleted = \",\".join([\n                \"\\\"{}\\\"\".format(ftrack_id)\n                for ftrack_id in not_deleted_entities_id\n            ])\n            joined_asset_names = \",\".join([\n                \"\\\"{}\\\"\".format(name)\n                for name in asset_names_to_delete\n            ])\n            # Find assets of selected entities with names of checked subsets\n            assets = session.query((\n                \"select id from Asset where\"\n                \" context_id in ({}) and name in ({})\"\n            ).format(joined_not_deleted, joined_asset_names)).all()\n\n            self.log.debug(\"{}Deleting Ftrack Assets <{}>\".format(\n                ftrack_proc_txt,\n                \", \".join([asset[\"id\"] for asset in assets])\n            ))\n            for asset in assets:\n                session.delete(asset)\n                try:\n                    session.commit()\n                except Exception:\n                    session.rollback()\n                    msg = \"Failed to delete asset\"\n                    report_messages[msg].append(asset[\"id\"])\n                    self.log.warning(\n                        \"Asset: {} <{}>\".format(asset[\"name\"], asset[\"id\"]),\n                        exc_info=True\n                    )\n\n        return self.report_handle(report_messages, project_name, event)\n\n    def _prepare_entities_before_delete(self, ftrack_ids_to_delete, session):\n        \"\"\"Filter children entities to avoid CircularDependencyError.\"\"\"\n        joined_ids_to_delete = \", \".join(\n            [\"\\\"{}\\\"\".format(id) for id in ftrack_ids_to_delete]\n        )\n        to_delete_entities = session.query(\n            \"select id, link from TypedContext where id in ({})\".format(\n                joined_ids_to_delete\n            )\n        ).all()\n        # Find all children entities and add them to list\n        # - Delete tasks first then their parents and continue\n        parent_ids_to_delete = [\n            entity[\"id\"]\n            for entity in to_delete_entities\n        ]\n        while parent_ids_to_delete:\n            joined_parent_ids_to_delete = \",\".join([\n                \"\\\"{}\\\"\".format(ftrack_id)\n                for ftrack_id in parent_ids_to_delete\n            ])\n            _to_delete = session.query((\n                \"select id, link from TypedContext where parent_id in ({})\"\n            ).format(joined_parent_ids_to_delete)).all()\n            parent_ids_to_delete = []\n            for entity in _to_delete:\n                parent_ids_to_delete.append(entity[\"id\"])\n                to_delete_entities.append(entity)\n\n        # Unset 'task_id' from AssetVersion entities\n        # - when task is deleted the asset version is not marked for deletion\n        task_ids = set(\n            entity[\"id\"]\n            for entity in to_delete_entities\n            if entity.entity_type.lower() == \"task\"\n        )\n        for chunk in create_chunks(task_ids):\n            asset_versions = session.query((\n                \"select id, task_id from AssetVersion where task_id in ({})\"\n            ).format(self.join_query_keys(chunk))).all()\n            for asset_version in asset_versions:\n                asset_version[\"task_id\"] = None\n            session.commit()\n\n        entities_by_link_len = collections.defaultdict(list)\n        for entity in to_delete_entities:\n            entities_by_link_len[len(entity[\"link\"])].append(entity)\n\n        return entities_by_link_len\n\n    def report_handle(self, report_messages, project_name, event):\n        if not report_messages:\n            return {\n                \"success\": True,\n                \"message\": \"Deletion was successful!\"\n            }\n\n        title = \"Delete report ({}):\".format(project_name)\n        items = []\n        items.append({\n            \"type\": \"label\",\n            \"value\": \"# Deleting was not completely successful\"\n        })\n        items.append({\n            \"type\": \"label\",\n            \"value\": \"<p><i>Check logs for more information</i></p>\"\n        })\n        for msg, _items in report_messages.items():\n            if not _items or not msg:\n                continue\n\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"# {}\".format(msg)\n            })\n\n            if isinstance(_items, str):\n                _items = [_items]\n            items.append({\n                \"type\": \"label\",\n                \"value\": '<p>{}</p>'.format(\"<br>\".join(_items))\n            })\n            items.append(self.splitter)\n\n        self.show_interface(items, title, event)\n\n        return {\n            \"success\": False,\n            \"message\": \"Deleting finished. Read report messages.\"\n        }\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    DeleteAssetSubset(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py",
    "content": "import os\nimport collections\nimport uuid\n\nimport clique\nfrom pymongo import UpdateOne\n\nfrom openpype.client import (\n    get_assets,\n    get_subsets,\n    get_versions,\n    get_representations\n)\nfrom openpype.lib import (\n    StringTemplate,\n    TemplateUnsolved,\n    format_file_size,\n)\nfrom openpype.pipeline import AvalonMongoDB, Anatomy\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass DeleteOldVersions(BaseAction):\n\n    identifier = \"delete.old.versions\"\n    label = \"OpenPype Admin\"\n    variant = \"- Delete old versions\"\n    description = (\n        \"Delete files from older publishes so project can be\"\n        \" archived with only latest versions.\"\n    )\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"OpenPypeAdmin.svg\")\n\n    settings_key = \"delete_old_versions\"\n\n    dbcon = AvalonMongoDB()\n\n    inteface_title = \"Choose your preferences\"\n    splitter_item = {\"type\": \"label\", \"value\": \"---\"}\n    sequence_splitter = \"__sequence_splitter__\"\n\n    def discover(self, session, entities, event):\n        \"\"\" Validation. \"\"\"\n        is_valid = False\n        for entity in entities:\n            if entity.entity_type.lower() == \"assetversion\":\n                is_valid = True\n                break\n\n        if is_valid:\n            is_valid = self.valid_roles(session, entities, event)\n        return is_valid\n\n    def interface(self, session, entities, event):\n        # TODO Add roots existence validation\n        items = []\n        values = event[\"data\"].get(\"values\")\n        if values:\n            versions_count = int(values[\"last_versions_count\"])\n            if versions_count >= 1:\n                return\n            items.append({\n                \"type\": \"label\",\n                \"value\": (\n                    \"# You have to keep at least 1 version!\"\n                )\n            })\n\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<i><b>WARNING:</b> This will remove published files of older\"\n                \" versions from disk so we don't recommend use\"\n                \" this action on \\\"live\\\" project.</i>\"\n            )\n        })\n\n        items.append(self.splitter_item)\n\n        # How many versions to keep\n        items.append({\n            \"type\": \"label\",\n            \"value\": \"## Choose how many versions you want to keep:\"\n        })\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<i><b>NOTE:</b> We do recommend to keep 2 versions.</i>\"\n            )\n        })\n        items.append({\n            \"type\": \"number\",\n            \"name\": \"last_versions_count\",\n            \"label\": \"Versions\",\n            \"value\": 2\n        })\n\n        items.append(self.splitter_item)\n\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"## Remove publish folder even if there\"\n                \" are other than published files:\"\n            )\n        })\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<i><b>WARNING:</b> This may remove more than you want.</i>\"\n            )\n        })\n        items.append({\n            \"type\": \"boolean\",\n            \"name\": \"force_delete_publish_folder\",\n            \"label\": \"Are You sure?\",\n            \"value\": False\n        })\n\n        items.append(self.splitter_item)\n\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<i>This will <b>NOT</b> delete any files and only return the \"\n                \"total size of the files.</i>\"\n            )\n        })\n        items.append({\n            \"type\": \"boolean\",\n            \"name\": \"only_calculate\",\n            \"label\": \"Only calculate size of files.\",\n            \"value\": False\n        })\n\n        return {\n            \"items\": items,\n            \"title\": self.inteface_title\n        }\n\n    def launch(self, session, entities, event):\n        values = event[\"data\"].get(\"values\")\n        if not values:\n            return\n\n        versions_count = int(values[\"last_versions_count\"])\n        force_to_remove = values[\"force_delete_publish_folder\"]\n        only_calculate = values[\"only_calculate\"]\n\n        _val1 = \"OFF\"\n        if force_to_remove:\n            _val1 = \"ON\"\n\n        _val3 = \"s\"\n        if versions_count == 1:\n            _val3 = \"\"\n\n        self.log.debug((\n            \"Process started. Force to delete publish folder is set to [{0}]\"\n            \" and will keep {1} latest version{2}.\"\n        ).format(_val1, versions_count, _val3))\n\n        self.dbcon.install()\n\n        project = None\n        avalon_asset_names = []\n        asset_versions_by_parent_id = collections.defaultdict(list)\n        subset_names_by_asset_name = collections.defaultdict(list)\n\n        ftrack_assets_by_name = {}\n        for entity in entities:\n            ftrack_asset = entity[\"asset\"]\n\n            parent_ent = ftrack_asset[\"parent\"]\n            parent_ftrack_id = parent_ent[\"id\"]\n            parent_name = parent_ent[\"name\"]\n\n            if parent_name not in avalon_asset_names:\n                avalon_asset_names.append(parent_name)\n\n            # Group asset versions by parent entity\n            asset_versions_by_parent_id[parent_ftrack_id].append(entity)\n\n            # Get project\n            if project is None:\n                project = parent_ent[\"project\"]\n\n            # Collect subset names per asset\n            subset_name = ftrack_asset[\"name\"]\n            subset_names_by_asset_name[parent_name].append(subset_name)\n\n            if subset_name not in ftrack_assets_by_name:\n                ftrack_assets_by_name[subset_name] = ftrack_asset\n\n        # Set Mongo collection\n        project_name = project[\"full_name\"]\n        anatomy = Anatomy(project_name)\n        self.dbcon.Session[\"AVALON_PROJECT\"] = project_name\n        self.log.debug(\"Project is set to {}\".format(project_name))\n\n        # Get Assets from avalon database\n        assets = list(\n            get_assets(project_name, asset_names=avalon_asset_names)\n        )\n        asset_id_to_name_map = {\n            asset[\"_id\"]: asset[\"name\"] for asset in assets\n        }\n        asset_ids = list(asset_id_to_name_map.keys())\n\n        self.log.debug(\"Collected assets ({})\".format(len(asset_ids)))\n\n        # Get Subsets\n        subsets = list(\n            get_subsets(project_name, asset_ids=asset_ids)\n        )\n        subsets_by_id = {}\n        subset_ids = []\n        for subset in subsets:\n            asset_id = subset[\"parent\"]\n            asset_name = asset_id_to_name_map[asset_id]\n            available_subsets = subset_names_by_asset_name[asset_name]\n\n            if subset[\"name\"] not in available_subsets:\n                continue\n\n            subset_ids.append(subset[\"_id\"])\n            subsets_by_id[subset[\"_id\"]] = subset\n\n        self.log.debug(\"Collected subsets ({})\".format(len(subset_ids)))\n\n        # Get Versions\n        versions = list(\n            get_versions(project_name, subset_ids=subset_ids)\n        )\n\n        versions_by_parent = collections.defaultdict(list)\n        for ent in versions:\n            versions_by_parent[ent[\"parent\"]].append(ent)\n\n        def sort_func(ent):\n            return int(ent[\"name\"])\n\n        all_last_versions = []\n        for parent_id, _versions in versions_by_parent.items():\n            for idx, version in enumerate(\n                sorted(_versions, key=sort_func, reverse=True)\n            ):\n                if idx >= versions_count:\n                    break\n                all_last_versions.append(version)\n\n        self.log.debug(\"Collected versions ({})\".format(len(versions)))\n\n        # Filter latest versions\n        for version in all_last_versions:\n            versions.remove(version)\n\n        # Update versions_by_parent without filtered versions\n        versions_by_parent = collections.defaultdict(list)\n        for ent in versions:\n            versions_by_parent[ent[\"parent\"]].append(ent)\n\n        # Filter already deleted versions\n        versions_to_pop = []\n        for version in versions:\n            version_tags = version[\"data\"].get(\"tags\")\n            if version_tags and \"deleted\" in version_tags:\n                versions_to_pop.append(version)\n\n        for version in versions_to_pop:\n            subset = subsets_by_id[version[\"parent\"]]\n            asset_id = subset[\"parent\"]\n            asset_name = asset_id_to_name_map[asset_id]\n            msg = \"Asset: \\\"{}\\\" | Subset: \\\"{}\\\" | Version: \\\"{}\\\"\".format(\n                asset_name, subset[\"name\"], version[\"name\"]\n            )\n            self.log.warning((\n                \"Skipping version. Already tagged as `deleted`. < {} >\"\n            ).format(msg))\n            versions.remove(version)\n\n        version_ids = [ent[\"_id\"] for ent in versions]\n\n        self.log.debug(\n            \"Filtered versions to delete ({})\".format(len(version_ids))\n        )\n\n        if not version_ids:\n            msg = \"Skipping processing. Nothing to delete.\"\n            self.log.debug(msg)\n            return {\n                \"success\": True,\n                \"message\": msg\n            }\n\n        repres = list(\n            get_representations(project_name, version_ids=version_ids)\n        )\n\n        self.log.debug(\n            \"Collected representations to remove ({})\".format(len(repres))\n        )\n\n        dir_paths = {}\n        file_paths_by_dir = collections.defaultdict(list)\n        for repre in repres:\n            file_path, seq_path = self.path_from_represenation(repre, anatomy)\n            if file_path is None:\n                self.log.warning((\n                    \"Could not format path for representation \\\"{}\\\"\"\n                ).format(str(repre)))\n                continue\n\n            dir_path = os.path.dirname(file_path)\n            dir_id = None\n            for _dir_id, _dir_path in dir_paths.items():\n                if _dir_path == dir_path:\n                    dir_id = _dir_id\n                    break\n\n            if dir_id is None:\n                dir_id = uuid.uuid4()\n                dir_paths[dir_id] = dir_path\n\n            file_paths_by_dir[dir_id].append([file_path, seq_path])\n\n        dir_ids_to_pop = []\n        for dir_id, dir_path in dir_paths.items():\n            if os.path.exists(dir_path):\n                continue\n\n            dir_ids_to_pop.append(dir_id)\n\n        # Pop dirs from both dictionaries\n        for dir_id in dir_ids_to_pop:\n            dir_paths.pop(dir_id)\n            paths = file_paths_by_dir.pop(dir_id)\n            # TODO report of missing directories?\n            paths_msg = \", \".join([\n                \"'{}'\".format(path[0].replace(\"\\\\\", \"/\")) for path in paths\n            ])\n            self.log.warning((\n                \"Folder does not exist. Deleting it's files skipped: {}\"\n            ).format(paths_msg))\n\n        # Size of files.\n        size = 0\n\n        if only_calculate:\n            if force_to_remove:\n                size = self.delete_whole_dir_paths(\n                    dir_paths.values(), delete=False\n                )\n            else:\n                size = self.delete_only_repre_files(\n                    dir_paths, file_paths_by_dir, delete=False\n                )\n\n            msg = \"Total size of files: {}\".format(format_file_size(size))\n\n            self.log.warning(msg)\n\n            return {\"success\": True, \"message\": msg}\n\n        if force_to_remove:\n            size = self.delete_whole_dir_paths(dir_paths.values())\n        else:\n            size = self.delete_only_repre_files(dir_paths, file_paths_by_dir)\n\n        mongo_changes_bulk = []\n        for version in versions:\n            orig_version_tags = version[\"data\"].get(\"tags\") or []\n            version_tags = [tag for tag in orig_version_tags]\n            if \"deleted\" not in version_tags:\n                version_tags.append(\"deleted\")\n\n            if version_tags == orig_version_tags:\n                continue\n\n            update_query = {\"_id\": version[\"_id\"]}\n            update_data = {\"$set\": {\"data.tags\": version_tags}}\n            mongo_changes_bulk.append(UpdateOne(update_query, update_data))\n\n        if mongo_changes_bulk:\n            self.dbcon.bulk_write(mongo_changes_bulk)\n\n        self.dbcon.uninstall()\n\n        # Set attribute `is_published` to `False` on ftrack AssetVersions\n        for subset_id, _versions in versions_by_parent.items():\n            subset_name = None\n            for subset in subsets:\n                if subset[\"_id\"] == subset_id:\n                    subset_name = subset[\"name\"]\n                    break\n\n            if subset_name is None:\n                self.log.warning(\n                    \"Subset with ID `{}` was not found.\".format(str(subset_id))\n                )\n                continue\n\n            ftrack_asset = ftrack_assets_by_name.get(subset_name)\n            if not ftrack_asset:\n                self.log.warning((\n                    \"Could not find Ftrack asset with name `{}`\"\n                ).format(subset_name))\n                continue\n\n            version_numbers = [int(ver[\"name\"]) for ver in _versions]\n            for version in ftrack_asset[\"versions\"]:\n                if int(version[\"version\"]) in version_numbers:\n                    version[\"is_published\"] = False\n\n        try:\n            session.commit()\n\n        except Exception:\n            msg = (\n                \"Could not set `is_published` attribute to `False`\"\n                \" for selected AssetVersions.\"\n            )\n            self.log.warning(msg, exc_info=True)\n\n            return {\n                \"success\": False,\n                \"message\": msg\n            }\n\n        msg = \"Total size of files deleted: {}\".format(format_file_size(size))\n\n        self.log.warning(msg)\n\n        return {\"success\": True, \"message\": msg}\n\n    def delete_whole_dir_paths(self, dir_paths, delete=True):\n        size = 0\n\n        for dir_path in dir_paths:\n            # Delete all files and fodlers in dir path\n            for root, dirs, files in os.walk(dir_path, topdown=False):\n                for name in files:\n                    file_path = os.path.join(root, name)\n                    size += os.path.getsize(file_path)\n                    if delete:\n                        os.remove(file_path)\n                        self.log.debug(\"Removed file: {}\".format(file_path))\n\n                for name in dirs:\n                    if delete:\n                        os.rmdir(os.path.join(root, name))\n\n            if not delete:\n                continue\n\n            # Delete even the folder and it's parents folders if they are empty\n            while True:\n                if not os.path.exists(dir_path):\n                    dir_path = os.path.dirname(dir_path)\n                    continue\n\n                if len(os.listdir(dir_path)) != 0:\n                    break\n\n                os.rmdir(os.path.join(dir_path))\n\n        return size\n\n    def delete_only_repre_files(self, dir_paths, file_paths, delete=True):\n        size = 0\n\n        for dir_id, dir_path in dir_paths.items():\n            dir_files = os.listdir(dir_path)\n            collections, remainders = clique.assemble(dir_files)\n            for file_path, seq_path in file_paths[dir_id]:\n                file_path_base = os.path.split(file_path)[1]\n                # Just remove file if `frame` key was not in context or\n                # filled path is in remainders (single file sequence)\n                if not seq_path or file_path_base in remainders:\n                    if not os.path.exists(file_path):\n                        self.log.warning(\n                            \"File was not found: {}\".format(file_path)\n                        )\n                        continue\n\n                    size += os.path.getsize(file_path)\n\n                    if delete:\n                        os.remove(file_path)\n                        self.log.debug(\"Removed file: {}\".format(file_path))\n\n                    if file_path_base in remainders:\n                        remainders.remove(file_path_base)\n                    continue\n\n                seq_path_base = os.path.split(seq_path)[1]\n                head, tail = seq_path_base.split(self.sequence_splitter)\n\n                final_col = None\n                for collection in collections:\n                    if head != collection.head or tail != collection.tail:\n                        continue\n                    final_col = collection\n                    break\n\n                if final_col is not None:\n                    # Fill full path to head\n                    final_col.head = os.path.join(dir_path, final_col.head)\n                    for _file_path in final_col:\n                        if os.path.exists(_file_path):\n\n                            size += os.path.getsize(_file_path)\n\n                            if delete:\n                                os.remove(_file_path)\n                                self.log.debug(\n                                    \"Removed file: {}\".format(_file_path)\n                                )\n\n                    _seq_path = final_col.format(\"{head}{padding}{tail}\")\n                    self.log.debug(\"Removed files: {}\".format(_seq_path))\n                    collections.remove(final_col)\n\n                elif os.path.exists(file_path):\n                    size += os.path.getsize(file_path)\n\n                    if delete:\n                        os.remove(file_path)\n                        self.log.debug(\"Removed file: {}\".format(file_path))\n                else:\n                    self.log.warning(\n                        \"File was not found: {}\".format(file_path)\n                    )\n\n        # Delete as much as possible parent folders\n        if not delete:\n            return size\n\n        for dir_path in dir_paths.values():\n            while True:\n                if not os.path.exists(dir_path):\n                    dir_path = os.path.dirname(dir_path)\n                    continue\n\n                if len(os.listdir(dir_path)) != 0:\n                    break\n\n                self.log.debug(\"Removed folder: {}\".format(dir_path))\n                os.rmdir(dir_path)\n\n        return size\n\n    def path_from_represenation(self, representation, anatomy):\n        try:\n            template = representation[\"data\"][\"template\"]\n\n        except KeyError:\n            return (None, None)\n\n        sequence_path = None\n        try:\n            context = representation[\"context\"]\n            context[\"root\"] = anatomy.roots\n            path = StringTemplate.format_strict_template(template, context)\n            if \"frame\" in context:\n                context[\"frame\"] = self.sequence_splitter\n                sequence_path = os.path.normpath(\n                    StringTemplate.format_strict_template(\n                        template, context\n                    )\n                )\n\n        except (KeyError, TemplateUnsolved):\n            # Template references unavailable data\n            return (None, None)\n\n        return (os.path.normpath(path), sequence_path)\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    DeleteOldVersions(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_delivery.py",
    "content": "import os\nimport copy\nimport json\nimport collections\n\nfrom openpype.client import (\n    get_project,\n    get_assets,\n    get_subsets,\n    get_versions,\n    get_representations\n)\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\nfrom openpype_modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY\nfrom openpype_modules.ftrack.lib.custom_attributes import (\n    query_custom_attributes\n)\nfrom openpype.lib.dateutils import get_datetime_data\nfrom openpype.pipeline import Anatomy\nfrom openpype.pipeline.load import get_representation_path_with_anatomy\nfrom openpype.pipeline.delivery import (\n    get_format_dict,\n    check_destination_path,\n    deliver_single_file,\n    deliver_sequence,\n)\n\n\nclass Delivery(BaseAction):\n    identifier = \"delivery.action\"\n    label = \"Delivery\"\n    description = \"Deliver data to client\"\n    role_list = [\"Pypeclub\", \"Administrator\", \"Project manager\"]\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"Delivery.svg\")\n    settings_key = \"delivery_action\"\n\n    def discover(self, session, entities, event):\n        is_valid = False\n        for entity in entities:\n            if entity.entity_type.lower() in (\"assetversion\", \"reviewsession\"):\n                is_valid = True\n                break\n\n        if is_valid:\n            is_valid = self.valid_roles(session, entities, event)\n        return is_valid\n\n    def interface(self, session, entities, event):\n        if event[\"data\"].get(\"values\", {}):\n            return\n\n        title = \"Delivery data to Client\"\n\n        items = []\n        item_splitter = {\"type\": \"label\", \"value\": \"---\"}\n\n        project_entity = self.get_project_from_entity(entities[0])\n        project_name = project_entity[\"full_name\"]\n        project_doc = get_project(project_name, fields=[\"name\"])\n        if not project_doc:\n            return {\n                \"success\": False,\n                \"message\": (\n                    \"Didn't find project \\\"{}\\\" in avalon.\"\n                ).format(project_name)\n            }\n\n        repre_names = self._get_repre_names(project_name, session, entities)\n\n        items.append({\n            \"type\": \"hidden\",\n            \"name\": \"__project_name__\",\n            \"value\": project_name\n        })\n\n        # Prepare anatomy data\n        anatomy = Anatomy(project_name)\n        new_anatomies = []\n        first = None\n        for key, template in (anatomy.templates.get(\"delivery\") or {}).items():\n            # Use only keys with `{root}` or `{root[*]}` in value\n            if isinstance(template, str) and \"{root\" in template:\n                new_anatomies.append({\n                    \"label\": key,\n                    \"value\": key\n                })\n                if first is None:\n                    first = key\n\n        skipped = False\n        # Add message if there are any common components\n        if not repre_names or not new_anatomies:\n            skipped = True\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"<h1>Something went wrong:</h1>\"\n            })\n\n        items.append({\n            \"type\": \"hidden\",\n            \"name\": \"__skipped__\",\n            \"value\": skipped\n        })\n\n        if not repre_names:\n            if len(entities) == 1:\n                items.append({\n                    \"type\": \"label\",\n                    \"value\": (\n                        \"- Selected entity doesn't have components to deliver.\"\n                    )\n                })\n            else:\n                items.append({\n                    \"type\": \"label\",\n                    \"value\": (\n                        \"- Selected entities don't have common components.\"\n                    )\n                })\n\n        # Add message if delivery anatomies are not set\n        if not new_anatomies:\n            items.append({\n                \"type\": \"label\",\n                \"value\": (\n                    \"- `\\\"delivery\\\"` anatomy key is not set in config.\"\n                )\n            })\n\n        # Skip if there are any data shortcomings\n        if skipped:\n            return {\n                \"items\": items,\n                \"title\": title\n            }\n\n        items.append({\n            \"value\": \"<h1>Choose Components to deliver</h1>\",\n            \"type\": \"label\"\n        })\n\n        for repre_name in repre_names:\n            items.append({\n                \"type\": \"boolean\",\n                \"value\": False,\n                \"label\": repre_name,\n                \"name\": repre_name\n            })\n\n        items.append(item_splitter)\n\n        items.append({\n            \"value\": \"<h2>Location for delivery</h2>\",\n            \"type\": \"label\"\n        })\n\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<i>NOTE: It is possible to replace `root` key in anatomy.</i>\"\n            )\n        })\n\n        items.append({\n            \"type\": \"text\",\n            \"name\": \"__location_path__\",\n            \"empty_text\": \"Type location path here...(Optional)\"\n        })\n\n        items.append(item_splitter)\n\n        items.append({\n            \"value\": \"<h2>Anatomy of delivery files</h2>\",\n            \"type\": \"label\"\n        })\n\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<p><i>NOTE: These can be set in Anatomy.yaml\"\n                \" within `delivery` key.</i></p>\"\n            )\n        })\n\n        items.append({\n            \"type\": \"enumerator\",\n            \"name\": \"__new_anatomies__\",\n            \"data\": new_anatomies,\n            \"value\": first\n        })\n\n        return {\n            \"items\": items,\n            \"title\": title\n        }\n\n    def _get_repre_names(self, project_name, session, entities):\n        version_ids = self._get_interest_version_ids(\n            project_name, session, entities\n        )\n        if not version_ids:\n            return []\n        repre_docs = get_representations(\n            project_name,\n            version_ids=version_ids,\n            fields=[\"name\"]\n        )\n        repre_names = {repre_doc[\"name\"] for repre_doc in repre_docs}\n        return list(sorted(repre_names))\n\n    def _get_interest_version_ids(self, project_name, session, entities):\n        # Extract AssetVersion entities\n        asset_versions = self._extract_asset_versions(session, entities)\n        # Prepare Asset ids\n        asset_ids = {\n            asset_version[\"asset_id\"]\n            for asset_version in asset_versions\n        }\n        # Query Asset entities\n        assets = session.query((\n            \"select id, name, context_id from Asset where id in ({})\"\n        ).format(self.join_query_keys(asset_ids))).all()\n        assets_by_id = {\n            asset[\"id\"]: asset\n            for asset in assets\n        }\n        parent_ids = set()\n        subset_names = set()\n        version_nums = set()\n        for asset_version in asset_versions:\n            asset_id = asset_version[\"asset_id\"]\n            asset = assets_by_id[asset_id]\n\n            parent_ids.add(asset[\"context_id\"])\n            subset_names.add(asset[\"name\"])\n            version_nums.add(asset_version[\"version\"])\n\n        asset_docs_by_ftrack_id = self._get_asset_docs(\n            project_name, session, parent_ids\n        )\n        subset_docs = self._get_subset_docs(\n            project_name,\n            asset_docs_by_ftrack_id,\n            subset_names,\n            asset_versions,\n            assets_by_id\n        )\n        version_docs = self._get_version_docs(\n            project_name,\n            asset_docs_by_ftrack_id,\n            subset_docs,\n            version_nums,\n            asset_versions,\n            assets_by_id\n        )\n\n        return [version_doc[\"_id\"] for version_doc in version_docs]\n\n    def _extract_asset_versions(self, session, entities):\n        asset_version_ids = set()\n        review_session_ids = set()\n        for entity in entities:\n            entity_type_low = entity.entity_type.lower()\n            if entity_type_low == \"assetversion\":\n                asset_version_ids.add(entity[\"id\"])\n            elif entity_type_low == \"reviewsession\":\n                review_session_ids.add(entity[\"id\"])\n\n        for version_id in self._get_asset_version_ids_from_review_sessions(\n            session, review_session_ids\n        ):\n            asset_version_ids.add(version_id)\n\n        asset_versions = session.query((\n            \"select id, version, asset_id from AssetVersion where id in ({})\"\n        ).format(self.join_query_keys(asset_version_ids))).all()\n\n        return asset_versions\n\n    def _get_asset_version_ids_from_review_sessions(\n        self, session, review_session_ids\n    ):\n        if not review_session_ids:\n            return set()\n        review_session_objects = session.query((\n            \"select version_id from ReviewSessionObject\"\n            \" where review_session_id in ({})\"\n        ).format(self.join_query_keys(review_session_ids))).all()\n\n        return {\n            review_session_object[\"version_id\"]\n            for review_session_object in review_session_objects\n        }\n\n    def _get_version_docs(\n        self,\n        project_name,\n        asset_docs_by_ftrack_id,\n        subset_docs,\n        version_nums,\n        asset_versions,\n        assets_by_id\n    ):\n        subset_docs_by_id = {\n            subset_doc[\"_id\"]: subset_doc\n            for subset_doc in subset_docs\n        }\n        version_docs = list(get_versions(\n            project_name,\n            subset_ids=subset_docs_by_id.keys(),\n            versions=version_nums\n        ))\n        version_docs_by_parent_id = collections.defaultdict(dict)\n        for version_doc in version_docs:\n            subset_doc = subset_docs_by_id[version_doc[\"parent\"]]\n\n            asset_id = subset_doc[\"parent\"]\n            subset_name = subset_doc[\"name\"]\n            version = version_doc[\"name\"]\n            if version_docs_by_parent_id[asset_id].get(subset_name) is None:\n                version_docs_by_parent_id[asset_id][subset_name] = {}\n\n            version_docs_by_parent_id[asset_id][subset_name][version] = (\n                version_doc\n            )\n\n        filtered_versions = []\n        for asset_version in asset_versions:\n            asset_id = asset_version[\"asset_id\"]\n            asset = assets_by_id[asset_id]\n            parent_id = asset[\"context_id\"]\n            asset_doc = asset_docs_by_ftrack_id.get(parent_id)\n            if not asset_doc:\n                continue\n\n            subsets_by_name = version_docs_by_parent_id.get(asset_doc[\"_id\"])\n            if not subsets_by_name:\n                continue\n\n            subset_name = asset[\"name\"]\n            version_docs_by_version = subsets_by_name.get(subset_name)\n            if not version_docs_by_version:\n                continue\n\n            version = asset_version[\"version\"]\n            version_doc = version_docs_by_version.get(version)\n            if version_doc:\n                filtered_versions.append(version_doc)\n        return filtered_versions\n\n    def _get_subset_docs(\n        self,\n        project_name,\n        asset_docs_by_ftrack_id,\n        subset_names,\n        asset_versions,\n        assets_by_id\n    ):\n        asset_doc_ids = [\n            asset_doc[\"_id\"]\n            for asset_doc in asset_docs_by_ftrack_id.values()\n        ]\n        subset_docs = list(get_subsets(\n            project_name,\n            asset_ids=asset_doc_ids,\n            subset_names=subset_names\n        ))\n        subset_docs_by_parent_id = collections.defaultdict(dict)\n        for subset_doc in subset_docs:\n            asset_id = subset_doc[\"parent\"]\n            subset_name = subset_doc[\"name\"]\n            subset_docs_by_parent_id[asset_id][subset_name] = subset_doc\n\n        filtered_subsets = []\n        for asset_version in asset_versions:\n            asset_id = asset_version[\"asset_id\"]\n            asset = assets_by_id[asset_id]\n\n            parent_id = asset[\"context_id\"]\n            asset_doc = asset_docs_by_ftrack_id.get(parent_id)\n            if not asset_doc:\n                continue\n\n            subsets_by_name = subset_docs_by_parent_id.get(asset_doc[\"_id\"])\n            if not subsets_by_name:\n                continue\n\n            subset_name = asset[\"name\"]\n            subset_doc = subsets_by_name.get(subset_name)\n            if subset_doc:\n                filtered_subsets.append(subset_doc)\n        return filtered_subsets\n\n    def _get_asset_docs(self, project_name, session, parent_ids):\n        asset_docs = list(get_assets(\n            project_name, fields=[\"_id\", \"name\", \"data.ftrackId\"]\n        ))\n\n        asset_docs_by_id = {}\n        asset_docs_by_name = {}\n        asset_docs_by_ftrack_id = {}\n        for asset_doc in asset_docs:\n            asset_id = str(asset_doc[\"_id\"])\n            asset_name = asset_doc[\"name\"]\n            ftrack_id = asset_doc[\"data\"].get(\"ftrackId\")\n\n            asset_docs_by_id[asset_id] = asset_doc\n            asset_docs_by_name[asset_name] = asset_doc\n            if ftrack_id:\n                asset_docs_by_ftrack_id[ftrack_id] = asset_doc\n\n        attr_def = session.query((\n            \"select id from CustomAttributeConfiguration where key is \\\"{}\\\"\"\n        ).format(CUST_ATTR_ID_KEY)).first()\n        if attr_def is None:\n            return asset_docs_by_ftrack_id\n\n        avalon_mongo_id_values = query_custom_attributes(\n            session, [attr_def[\"id\"]], parent_ids, True\n        )\n        missing_ids = set(parent_ids)\n        for item in avalon_mongo_id_values:\n            if not item[\"value\"]:\n                continue\n            asset_id = item[\"value\"]\n            entity_id = item[\"entity_id\"]\n            asset_doc = asset_docs_by_id.get(asset_id)\n            if asset_doc:\n                asset_docs_by_ftrack_id[entity_id] = asset_doc\n                missing_ids.remove(entity_id)\n\n        entity_ids_by_name = {}\n        if missing_ids:\n            not_found_entities = session.query((\n                \"select id, name from TypedContext where id in ({})\"\n            ).format(self.join_query_keys(missing_ids))).all()\n            entity_ids_by_name = {\n                entity[\"name\"]: entity[\"id\"]\n                for entity in not_found_entities\n            }\n\n        for asset_name, entity_id in entity_ids_by_name.items():\n            asset_doc = asset_docs_by_name.get(asset_name)\n            if asset_doc:\n                asset_docs_by_ftrack_id[entity_id] = asset_doc\n\n        return asset_docs_by_ftrack_id\n\n    def launch(self, session, entities, event):\n        if \"values\" not in event[\"data\"]:\n            return {\n                \"success\": True,\n                \"message\": \"Nothing to do\"\n            }\n\n        values = event[\"data\"][\"values\"]\n        skipped = values.pop(\"__skipped__\")\n        if skipped:\n            return {\n                \"success\": False,\n                \"message\": \"Action skipped\"\n            }\n\n        user_id = event[\"source\"][\"user\"][\"id\"]\n        user_entity = session.query(\n            \"User where id is {}\".format(user_id)\n        ).one()\n\n        job = session.create(\"Job\", {\n            \"user\": user_entity,\n            \"status\": \"running\",\n            \"data\": json.dumps({\n                \"description\": \"Delivery processing.\"\n            })\n        })\n        session.commit()\n\n        try:\n            report = self.real_launch(session, entities, event)\n\n        except Exception as exc:\n            report = {\n                \"success\": False,\n                \"title\": \"Delivery failed\",\n                \"items\": [{\n                    \"type\": \"label\",\n                    \"value\": (\n                        \"Error during delivery action process:<br>{}\"\n                        \"<br><br>Check logs for more information.\"\n                    ).format(str(exc))\n                }]\n            }\n            self.log.warning(\n                \"Failed during processing delivery action.\",\n                exc_info=True\n            )\n\n        finally:\n            if report[\"success\"]:\n                job[\"status\"] = \"done\"\n            else:\n                job[\"status\"] = \"failed\"\n            session.commit()\n\n        if not report[\"success\"]:\n            self.show_interface(\n                items=report[\"items\"],\n                title=report[\"title\"],\n                event=event\n            )\n            return {\n                \"success\": False,\n                \"message\": \"Errors during delivery process. See report.\"\n            }\n\n        return report\n\n    def real_launch(self, session, entities, event):\n        self.log.info(\"Delivery action just started.\")\n        report_items = collections.defaultdict(list)\n\n        values = event[\"data\"][\"values\"]\n\n        location_path = values.pop(\"__location_path__\")\n        anatomy_name = values.pop(\"__new_anatomies__\")\n        project_name = values.pop(\"__project_name__\")\n\n        repre_names = []\n        for key, value in values.items():\n            if value is True:\n                repre_names.append(key)\n\n        if not repre_names:\n            return {\n                \"success\": True,\n                \"message\": \"No selected components to deliver.\"\n            }\n\n        location_path = location_path.strip()\n        if location_path:\n            location_path = os.path.normpath(location_path)\n            if not os.path.exists(location_path):\n                os.makedirs(location_path)\n\n        self.log.debug(\"Collecting representations to process.\")\n        version_ids = self._get_interest_version_ids(\n            project_name, session, entities\n        )\n        repres_to_deliver = list(get_representations(\n            project_name,\n            representation_names=repre_names,\n            version_ids=version_ids\n        ))\n        anatomy = Anatomy(project_name)\n\n        format_dict = get_format_dict(anatomy, location_path)\n\n        datetime_data = get_datetime_data()\n        for repre in repres_to_deliver:\n            source_path = repre.get(\"data\", {}).get(\"path\")\n            debug_msg = \"Processing representation {}\".format(repre[\"_id\"])\n            if source_path:\n                debug_msg += \" with published path {}.\".format(source_path)\n            self.log.debug(debug_msg)\n\n            anatomy_data = copy.deepcopy(repre[\"context\"])\n            repre_report_items = check_destination_path(repre[\"_id\"],\n                                                        anatomy,\n                                                        anatomy_data,\n                                                        datetime_data,\n                                                        anatomy_name)\n\n            if repre_report_items:\n                report_items.update(repre_report_items)\n                continue\n\n            # Get source repre path\n            frame = repre['context'].get('frame')\n\n            if frame:\n                repre[\"context\"][\"frame\"] = len(str(frame)) * \"#\"\n\n            repre_path = get_representation_path_with_anatomy(repre, anatomy)\n            # TODO add backup solution where root of path from component\n            # is replaced with root\n            args = (\n                repre_path,\n                repre,\n                anatomy,\n                anatomy_name,\n                anatomy_data,\n                format_dict,\n                report_items,\n                self.log\n            )\n            if not frame:\n                deliver_single_file(*args)\n            else:\n                deliver_sequence(*args)\n\n        return self.report(report_items)\n\n    def report(self, report_items):\n        \"\"\"Returns dict with final status of delivery (success, fail etc.).\"\"\"\n        items = []\n\n        for msg, _items in report_items.items():\n            if not _items:\n                continue\n\n            if items:\n                items.append({\"type\": \"label\", \"value\": \"---\"})\n\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"# {}\".format(msg)\n            })\n            if not isinstance(_items, (list, tuple)):\n                _items = [_items]\n            __items = []\n            for item in _items:\n                __items.append(str(item))\n\n            items.append({\n                \"type\": \"label\",\n                \"value\": '<p>{}</p>'.format(\"<br>\".join(__items))\n            })\n\n        if not items:\n            return {\n                \"success\": True,\n                \"message\": \"Delivery Finished\"\n            }\n\n        return {\n            \"items\": items,\n            \"title\": \"Delivery report\",\n            \"success\": False\n        }\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    Delivery(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_djvview.py",
    "content": "import os\nimport time\nimport subprocess\nfrom operator import itemgetter\nfrom openpype.lib import ApplicationManager\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass DJVViewAction(BaseAction):\n    \"\"\"Launch DJVView action.\"\"\"\n    identifier = \"djvview-launch-action\"\n    label = \"DJV View\"\n    description = \"DJV View Launcher\"\n    icon = statics_icon(\"app_icons\", \"djvView.png\")\n\n    type = \"Application\"\n\n    allowed_types = [\n        \"cin\", \"dpx\", \"avi\", \"dv\", \"gif\", \"flv\", \"mkv\", \"mov\", \"mpg\", \"mpeg\",\n        \"mp4\", \"m4v\", \"mxf\", \"iff\", \"z\", \"ifl\", \"jpeg\", \"jpg\", \"jfif\", \"lut\",\n        \"1dl\", \"exr\", \"pic\", \"png\", \"ppm\", \"pnm\", \"pgm\", \"pbm\", \"rla\", \"rpf\",\n        \"sgi\", \"rgba\", \"rgb\", \"bw\", \"tga\", \"tiff\", \"tif\", \"img\"\n    ]\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        self.application_manager = ApplicationManager()\n        self._last_check = time.time()\n        self._check_interval = 10\n\n    def _get_djv_apps(self):\n        app_group = self.application_manager.app_groups[\"djvview\"]\n\n        output = []\n        for app in app_group:\n            executable = app.find_executable()\n            if executable is not None:\n                output.append(app)\n        return output\n\n    def get_djv_apps(self):\n        cur_time = time.time()\n        if (cur_time - self._last_check) > self._check_interval:\n            self.application_manager.refresh()\n        return self._get_djv_apps()\n\n    def discover(self, session, entities, event):\n        \"\"\"Return available actions based on *event*. \"\"\"\n        selection = event[\"data\"].get(\"selection\", [])\n        if len(selection) != 1:\n            return False\n\n        entityType = selection[0].get(\"entityType\", None)\n        if entityType not in [\"assetversion\", \"task\"]:\n            return False\n\n        if self.get_djv_apps():\n            return True\n        return False\n\n    def interface(self, session, entities, event):\n        if event[\"data\"].get(\"values\", {}):\n            return\n\n        entity = entities[0]\n        versions = []\n\n        entity_type = entity.entity_type.lower()\n        if entity_type == \"assetversion\":\n            if (\n                entity[\n                    \"components\"\n                ][0][\"file_type\"][1:] in self.allowed_types\n            ):\n                versions.append(entity)\n        else:\n            master_entity = entity\n            if entity_type == \"task\":\n                master_entity = entity[\"parent\"]\n\n            for asset in master_entity[\"assets\"]:\n                for version in asset[\"versions\"]:\n                    # Get only AssetVersion of selected task\n                    if (\n                        entity_type == \"task\" and\n                        version[\"task\"][\"id\"] != entity[\"id\"]\n                    ):\n                        continue\n                    # Get only components with allowed type\n                    filetype = version[\"components\"][0][\"file_type\"]\n                    if filetype[1:] in self.allowed_types:\n                        versions.append(version)\n\n        if len(versions) < 1:\n            return {\n                \"success\": False,\n                \"message\": \"There are no Asset Versions to open.\"\n            }\n\n        # TODO sort them (somehow?)\n        enum_items = []\n        first_value = None\n        for app in self.get_djv_apps():\n            if first_value is None:\n                first_value = app.full_name\n            enum_items.append({\n                \"value\": app.full_name,\n                \"label\": app.full_label\n            })\n\n        if not enum_items:\n            return {\n                \"success\": False,\n                \"message\": \"Couldn't find DJV executable.\"\n            }\n\n        items = [\n            {\n                \"type\": \"enumerator\",\n                \"label\": \"DJV version:\",\n                \"name\": \"djv_app_name\",\n                \"data\": enum_items,\n                \"value\": first_value\n            },\n            {\n                \"type\": \"label\",\n                \"value\": \"---\"\n            }\n        ]\n        version_items = []\n        base_label = \"v{0} - {1} - {2}\"\n        default_component = None\n        last_available = None\n        select_value = None\n        for version in versions:\n            for component in version[\"components\"]:\n                label = base_label.format(\n                    str(version[\"version\"]).zfill(3),\n                    version[\"asset\"][\"type\"][\"name\"],\n                    component[\"name\"]\n                )\n\n                try:\n                    location = component[\n                        \"component_locations\"\n                    ][0][\"location\"]\n                    file_path = location.get_filesystem_path(component)\n                except Exception:\n                    file_path = component[\n                        \"component_locations\"\n                    ][0][\"resource_identifier\"]\n\n                if os.path.isdir(os.path.dirname(file_path)):\n                    last_available = file_path\n                    if component[\"name\"] == default_component:\n                        select_value = file_path\n                    version_items.append(\n                        {\"label\": label, \"value\": file_path}\n                    )\n\n        if len(version_items) == 0:\n            return {\n                \"success\": False,\n                \"message\": (\n                    \"There are no Asset Versions with accessible path.\"\n                )\n            }\n\n        item = {\n            \"label\": \"Items to view\",\n            \"type\": \"enumerator\",\n            \"name\": \"path\",\n            \"data\": sorted(\n                version_items,\n                key=itemgetter(\"label\"),\n                reverse=True\n            )\n        }\n        if select_value is not None:\n            item[\"value\"] = select_value\n        else:\n            item[\"value\"] = last_available\n\n        items.append(item)\n\n        return {\"items\": items}\n\n    def launch(self, session, entities, event):\n        \"\"\"Callback method for DJVView action.\"\"\"\n\n        # Launching application\n        event_values = event[\"data\"].get(\"values\")\n        if not event_values:\n            return\n\n        djv_app_name = event_values[\"djv_app_name\"]\n        app = self.application_manager.applications.get(djv_app_name)\n        executable = None\n        if app is not None:\n            executable = app.find_executable()\n\n        if not executable:\n            return {\n                \"success\": False,\n                \"message\": \"Couldn't find DJV executable.\"\n            }\n\n        filpath = os.path.normpath(event_values[\"path\"])\n\n        cmd = [\n            # DJV path\n            str(executable),\n            # PATH TO COMPONENT\n            filpath\n        ]\n\n        try:\n            # Run DJV with these commands\n            _process = subprocess.Popen(cmd)\n            # Keep process in memory for some time\n            time.sleep(0.1)\n\n        except FileNotFoundError:\n            return {\n                \"success\": False,\n                \"message\": \"File \\\"{}\\\" was not found.\".format(\n                    os.path.basename(filpath)\n                )\n            }\n\n        return True\n\n\ndef register(session):\n    \"\"\"Register hooks.\"\"\"\n\n    DJVViewAction(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py",
    "content": "import os\nimport sys\nimport json\nimport collections\nimport tempfile\nimport datetime\n\nimport ftrack_api\n\nfrom openpype.client import (\n    get_project,\n    get_assets,\n)\nfrom openpype.settings import get_project_settings, get_system_settings\nfrom openpype.lib import StringTemplate\nfrom openpype.pipeline import Anatomy\nfrom openpype.pipeline.template_data import get_template_data\nfrom openpype.pipeline.workfile import get_workfile_template_key\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\nfrom openpype_modules.ftrack.lib.avalon_sync import create_chunks\n\nNOT_SYNCHRONIZED_TITLE = \"Not synchronized\"\n\n\nclass FillWorkfileAttributeAction(BaseAction):\n    \"\"\"Action fill work filename into custom attribute on tasks.\n\n    Prerequirements are that the project is synchronized so it is possible to\n    access project anatomy and project/asset documents. Tasks that are not\n    synchronized are skipped too.\n    \"\"\"\n\n    identifier = \"fill.workfile.attr\"\n    label = \"OpenPype Admin\"\n    variant = \"- Fill workfile attribute\"\n    description = \"Precalculate and fill workfile name into a custom attribute\"\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"OpenPypeAdmin.svg\")\n\n    settings_key = \"fill_workfile_attribute\"\n\n    def discover(self, session, entities, event):\n        \"\"\" Validate selection. \"\"\"\n        is_valid = False\n        for ent in event[\"data\"][\"selection\"]:\n            # Ignore entities that are not tasks or projects\n            if ent[\"entityType\"].lower() in [\"show\", \"task\"]:\n                is_valid = True\n                break\n\n        if is_valid:\n            is_valid = self.valid_roles(session, entities, event)\n        return is_valid\n\n    def launch(self, session, entities, event):\n        # Separate entities and get project entity\n        project_entity = None\n        for entity in entities:\n            if project_entity is None:\n                project_entity = self.get_project_from_entity(entity)\n                break\n\n        if not project_entity:\n            return {\n                \"message\": (\n                    \"Couldn't find project entity.\"\n                    \" Could be an issue with permissions.\"\n                ),\n                \"success\": False\n            }\n\n        # Get project settings and check if custom attribute where workfile\n        #   should be set is defined.\n        project_name = project_entity[\"full_name\"]\n        project_settings = get_project_settings(project_name)\n        custom_attribute_key = (\n            project_settings\n            .get(\"ftrack\", {})\n            .get(\"user_handlers\", {})\n            .get(self.settings_key, {})\n            .get(\"custom_attribute_key\")\n        )\n        if not custom_attribute_key:\n            return {\n                \"success\": False,\n                \"message\": \"Custom attribute key is not set in settings\"\n            }\n\n        # Try to find the custom attribute\n        # - get Task type object id\n        task_obj_type = session.query(\n            \"select id from ObjectType where name is \\\"Task\\\"\"\n        ).one()\n        # - get text custom attribute type\n        text_type = session.query(\n            \"select id from CustomAttributeType where name is \\\"text\\\"\"\n        ).one()\n        # - find the attribute\n        attr_conf = session.query(\n            (\n                \"select id, key from CustomAttributeConfiguration\"\n                \" where object_type_id is \\\"{}\\\"\"\n                \" and type_id is \\\"{}\\\"\"\n                \" and key is \\\"{}\\\"\"\n            ).format(\n                task_obj_type[\"id\"], text_type[\"id\"], custom_attribute_key\n            )\n        ).first()\n        if not attr_conf:\n            return {\n                \"success\": False,\n                \"message\": (\n                    \"Could not find Task (text) Custom attribute \\\"{}\\\"\"\n                ).format(custom_attribute_key)\n            }\n\n        # Store report information\n        report = collections.defaultdict(list)\n        user_entity = session.query(\n            \"User where id is {}\".format(event[\"source\"][\"user\"][\"id\"])\n        ).one()\n        job_entity = session.create(\"Job\", {\n            \"user\": user_entity,\n            \"status\": \"running\",\n            \"data\": json.dumps({\n                \"description\": \"(0/3) Fill of workfiles started\"\n            })\n        })\n        session.commit()\n\n        try:\n            self.in_job_process(\n                session,\n                entities,\n                job_entity,\n                project_entity,\n                project_settings,\n                attr_conf,\n                report\n            )\n        except Exception:\n            self.log.error(\n                \"Fill of workfiles to custom attribute failed\", exc_info=True\n            )\n            session.rollback()\n\n            description = \"Fill of workfiles Failed (Download traceback)\"\n            self.add_traceback_to_job(\n                job_entity, session, sys.exc_info(), description\n            )\n            return {\n                \"message\": (\n                    \"Fill of workfiles failed.\"\n                    \" Check job for more information\"\n                ),\n                \"success\": False\n            }\n\n        job_entity[\"status\"] = \"done\"\n        job_entity[\"data\"] = json.dumps({\n            \"description\": \"Fill of workfiles completed.\"\n        })\n        session.commit()\n        if report:\n            temp_obj = tempfile.NamedTemporaryFile(\n                mode=\"w\",\n                prefix=\"openpype_ftrack_\",\n                suffix=\".json\",\n                delete=False\n            )\n            temp_obj.close()\n            temp_filepath = temp_obj.name\n            with open(temp_filepath, \"w\") as temp_file:\n                json.dump(report, temp_file)\n\n            component_name = \"{}_{}\".format(\n                \"FillWorkfilesReport\",\n                datetime.datetime.now().strftime(\"%y-%m-%d-%H%M\")\n            )\n            self.add_file_component_to_job(\n                job_entity, session, temp_filepath, component_name\n            )\n            # Delete temp file\n            os.remove(temp_filepath)\n            self._show_report(event, report, project_name)\n            return {\n                \"message\": (\n                    \"Fill of workfiles finished with few issues.\"\n                    \" Check job for more information\"\n                ),\n                \"success\": True\n            }\n\n        return {\n            \"success\": True,\n            \"message\": \"Finished with filling of work filenames\"\n        }\n\n    def _show_report(self, event, report, project_name):\n        items = []\n        title = \"Fill workfiles report ({}):\".format(project_name)\n\n        for subtitle, lines in report.items():\n            if items:\n                items.append({\n                    \"type\": \"label\",\n                    \"value\": \"---\"\n                })\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"# {}\".format(subtitle)\n            })\n            items.append({\n                \"type\": \"label\",\n                \"value\": '<p>{}</p>'.format(\"<br>\".join(lines))\n            })\n\n        self.show_interface(\n            items=items,\n            title=title,\n            event=event\n        )\n\n    def in_job_process(\n        self,\n        session,\n        entities,\n        job_entity,\n        project_entity,\n        project_settings,\n        attr_conf,\n        report\n    ):\n        task_entities = []\n        other_entities = []\n        project_selected = False\n        for entity in entities:\n            ent_type_low = entity.entity_type.lower()\n            if ent_type_low == \"project\":\n                project_selected = True\n                break\n\n            elif ent_type_low == \"task\":\n                task_entities.append(entity)\n            else:\n                other_entities.append(entity)\n\n        project_name = project_entity[\"full_name\"]\n\n        # Find matching asset documents and map them by ftrack task entities\n        # - result stored to 'asset_docs_with_task_entities' is list with\n        #   tuple `(asset document, [task entitis, ...])`\n        # Quety all asset documents\n        asset_docs = list(get_assets(project_name))\n        job_entity[\"data\"] = json.dumps({\n            \"description\": \"(1/3) Asset documents queried.\"\n        })\n        session.commit()\n\n        # When project is selected then we can query whole project\n        if project_selected:\n            asset_docs_with_task_entities = self._get_asset_docs_for_project(\n                session, project_entity, asset_docs, report\n            )\n\n        else:\n            asset_docs_with_task_entities = self._get_tasks_for_selection(\n                session, other_entities, task_entities, asset_docs, report\n            )\n\n        job_entity[\"data\"] = json.dumps({\n            \"description\": \"(2/3) Queried related task entities.\"\n        })\n        session.commit()\n\n        # Keep placeholders in the template unfilled\n        host_name = \"{app}\"\n        extension = \"{ext}\"\n        project_doc = get_project(project_name)\n        project_settings = get_project_settings(project_name)\n        system_settings = get_system_settings()\n        anatomy = Anatomy(project_name)\n        templates_by_key = {}\n\n        operations = []\n        for asset_doc, task_entities in asset_docs_with_task_entities:\n            for task_entity in task_entities:\n                workfile_data = get_template_data(\n                    project_doc,\n                    asset_doc,\n                    task_entity[\"name\"],\n                    host_name,\n                    system_settings\n                )\n                # Use version 1 for each workfile\n                workfile_data[\"version\"] = 1\n                workfile_data[\"ext\"] = extension\n\n                task_type = workfile_data[\"task\"][\"type\"]\n                template_key = get_workfile_template_key(\n                    task_type,\n                    host_name,\n                    project_name,\n                    project_settings=project_settings\n                )\n                if template_key in templates_by_key:\n                    template = templates_by_key[template_key]\n                else:\n                    template = StringTemplate(\n                        anatomy.templates[template_key][\"file\"]\n                    )\n                    templates_by_key[template_key] = template\n\n                result = template.format(workfile_data)\n                if not result.solved:\n                    # TODO report\n                    pass\n                else:\n                    table_values = collections.OrderedDict((\n                        (\"configuration_id\", attr_conf[\"id\"]),\n                        (\"entity_id\", task_entity[\"id\"])\n                    ))\n                    operations.append(\n                        ftrack_api.operation.UpdateEntityOperation(\n                            \"ContextCustomAttributeValue\",\n                            table_values,\n                            \"value\",\n                            ftrack_api.symbol.NOT_SET,\n                            str(result)\n                        )\n                    )\n\n        if operations:\n            for sub_operations in create_chunks(operations, 50):\n                for op in sub_operations:\n                    session.recorded_operations.push(op)\n                session.commit()\n\n        job_entity[\"data\"] = json.dumps({\n            \"description\": \"(3/3) Set custom attribute values.\"\n        })\n        session.commit()\n\n    def _get_entity_path(self, entity):\n        path_items = []\n        for item in entity[\"link\"]:\n            if item[\"type\"].lower() != \"project\":\n                path_items.append(item[\"name\"])\n        return \"/\".join(path_items)\n\n    def _get_asset_docs_for_project(\n        self, session, project_entity, asset_docs, report\n    ):\n        asset_docs_task_names = {}\n\n        for asset_doc in asset_docs:\n            asset_data = asset_doc[\"data\"]\n            ftrack_id = asset_data.get(\"ftrackId\")\n            if not ftrack_id:\n                hierarchy = list(asset_data.get(\"parents\") or [])\n                hierarchy.append(asset_doc[\"name\"])\n                path = \"/\".join(hierarchy)\n                report[NOT_SYNCHRONIZED_TITLE].append(path)\n                continue\n\n            asset_tasks = asset_data.get(\"tasks\") or {}\n            asset_docs_task_names[ftrack_id] = (\n                asset_doc, list(asset_tasks.keys())\n            )\n\n        task_entities = session.query((\n            \"select id, name, parent_id, link from Task where project_id is {}\"\n        ).format(project_entity[\"id\"])).all()\n        task_entities_by_parent_id = collections.defaultdict(list)\n        for task_entity in task_entities:\n            parent_id = task_entity[\"parent_id\"]\n            task_entities_by_parent_id[parent_id].append(task_entity)\n\n        output = []\n        for ftrack_id, item in asset_docs_task_names.items():\n            asset_doc, task_names = item\n            valid_task_entities = []\n            for task_entity in task_entities_by_parent_id[ftrack_id]:\n                if task_entity[\"name\"] in task_names:\n                    valid_task_entities.append(task_entity)\n                else:\n                    path = self._get_entity_path(task_entity)\n                    report[NOT_SYNCHRONIZED_TITLE].append(path)\n\n            if valid_task_entities:\n                output.append((asset_doc, valid_task_entities))\n\n        return output\n\n    def _get_tasks_for_selection(\n        self, session, other_entities, task_entities, asset_docs, report\n    ):\n        all_tasks = object()\n        asset_docs_by_ftrack_id = {}\n        asset_docs_by_parent_id = collections.defaultdict(list)\n        for asset_doc in asset_docs:\n            asset_data = asset_doc[\"data\"]\n            ftrack_id = asset_data.get(\"ftrackId\")\n            parent_id = asset_data.get(\"visualParent\")\n            asset_docs_by_parent_id[parent_id].append(asset_doc)\n            if ftrack_id:\n                asset_docs_by_ftrack_id[ftrack_id] = asset_doc\n\n        missing_doc_ftrack_ids = {}\n        all_tasks_ids = set()\n        task_names_by_ftrack_id = collections.defaultdict(list)\n        for other_entity in other_entities:\n            ftrack_id = other_entity[\"id\"]\n            if ftrack_id not in asset_docs_by_ftrack_id:\n                missing_doc_ftrack_ids[ftrack_id] = None\n                continue\n            all_tasks_ids.add(ftrack_id)\n            task_names_by_ftrack_id[ftrack_id] = all_tasks\n\n        for task_entity in task_entities:\n            parent_id = task_entity[\"parent_id\"]\n            if parent_id not in asset_docs_by_ftrack_id:\n                missing_doc_ftrack_ids[parent_id] = None\n                continue\n\n            if all_tasks_ids not in all_tasks_ids:\n                task_names_by_ftrack_id[ftrack_id].append(task_entity[\"name\"])\n\n        ftrack_ids = set()\n        asset_doc_with_task_names_by_id = {}\n        for ftrack_id, task_names in task_names_by_ftrack_id.items():\n            asset_doc = asset_docs_by_ftrack_id[ftrack_id]\n            asset_data = asset_doc[\"data\"]\n            asset_tasks = asset_data.get(\"tasks\") or {}\n\n            if task_names is all_tasks:\n                task_names = list(asset_tasks.keys())\n            else:\n                new_task_names = []\n                for task_name in task_names:\n                    if task_name in asset_tasks:\n                        new_task_names.append(task_name)\n                        continue\n\n                    if ftrack_id not in missing_doc_ftrack_ids:\n                        missing_doc_ftrack_ids[ftrack_id] = []\n                    if missing_doc_ftrack_ids[ftrack_id] is not None:\n                        missing_doc_ftrack_ids[ftrack_id].append(task_name)\n\n                task_names = new_task_names\n\n            if task_names:\n                ftrack_ids.add(ftrack_id)\n                asset_doc_with_task_names_by_id[ftrack_id] = (\n                    asset_doc, task_names\n                )\n\n        task_entities = session.query((\n            \"select id, name, parent_id from Task where parent_id in ({})\"\n        ).format(self.join_query_keys(ftrack_ids))).all()\n        task_entitiy_by_parent_id = collections.defaultdict(list)\n        for task_entity in task_entities:\n            parent_id = task_entity[\"parent_id\"]\n            task_entitiy_by_parent_id[parent_id].append(task_entity)\n\n        output = []\n        for ftrack_id, item in asset_doc_with_task_names_by_id.items():\n            asset_doc, task_names = item\n            valid_task_entities = []\n            for task_entity in task_entitiy_by_parent_id[ftrack_id]:\n                if task_entity[\"name\"] in task_names:\n                    valid_task_entities.append(task_entity)\n                else:\n                    if ftrack_id not in missing_doc_ftrack_ids:\n                        missing_doc_ftrack_ids[ftrack_id] = []\n                    if missing_doc_ftrack_ids[ftrack_id] is not None:\n                        missing_doc_ftrack_ids[ftrack_id].append(task_name)\n            if valid_task_entities:\n                output.append((asset_doc, valid_task_entities))\n\n        # Store report information about not synchronized entities\n        if missing_doc_ftrack_ids:\n            missing_entities = session.query(\n                \"select id, link from TypedContext where id in ({})\".format(\n                    self.join_query_keys(missing_doc_ftrack_ids.keys())\n                )\n            ).all()\n            for missing_entity in missing_entities:\n                path = self._get_entity_path(missing_entity)\n                task_names = missing_doc_ftrack_ids[missing_entity[\"id\"]]\n                if task_names is None:\n                    report[NOT_SYNCHRONIZED_TITLE].append(path)\n                else:\n                    for task_name in task_names:\n                        task_path = \"/\".join([path, task_name])\n                        report[NOT_SYNCHRONIZED_TITLE].append(task_path)\n\n        return output\n\n\ndef register(session):\n    FillWorkfileAttributeAction(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_job_killer.py",
    "content": "import json\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass JobKiller(BaseAction):\n    \"\"\"Kill jobs that are marked as running.\"\"\"\n\n    identifier = \"job.killer\"\n    label = \"OpenPype Admin\"\n    variant = \"- Job Killer\"\n    description = \"Killing selected running jobs\"\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"OpenPypeAdmin.svg\")\n    settings_key = \"job_killer\"\n\n    def discover(self, session, entities, event):\n        \"\"\"Check if action is available for user role.\"\"\"\n        return self.valid_roles(session, entities, event)\n\n    def interface(self, session, entities, event):\n        if event[\"data\"].get(\"values\"):\n            return\n\n        title = \"Select jobs to kill\"\n\n        jobs = session.query(\n            \"select id, user_id, status, created_at, data from Job\"\n            \" where status in (\\\"queued\\\", \\\"running\\\")\"\n        ).all()\n        if not jobs:\n            return {\n                \"success\": True,\n                \"message\": \"Didn't find any running jobs\"\n            }\n\n        # Collect user ids from jobs\n        user_ids = set()\n        for job in jobs:\n            user_id = job[\"user_id\"]\n            if user_id:\n                user_ids.add(user_id)\n\n        # Store usernames by their ids\n        usernames_by_id = {}\n        if user_ids:\n            users = session.query(\n                \"select id, username from User where id in ({})\".format(\n                    self.join_query_keys(user_ids)\n                )\n            ).all()\n            for user in users:\n                usernames_by_id[user[\"id\"]] = user[\"username\"]\n\n        items = []\n        for job in jobs:\n            try:\n                data = json.loads(job[\"data\"])\n                description = data[\"description\"]\n            except Exception:\n                description = \"*No description*\"\n            user_id = job[\"user_id\"]\n            username = usernames_by_id.get(user_id) or \"Unknown user\"\n            created = job[\"created_at\"].strftime('%d.%m.%Y %H:%M:%S')\n            label = \"{} - {} - {}\".format(\n                username, description, created\n            )\n            item_label = {\n                \"type\": \"label\",\n                \"value\": label\n            }\n            item = {\n                \"name\": job[\"id\"],\n                \"type\": \"boolean\",\n                \"value\": False\n            }\n            if len(items) > 0:\n                items.append({\"type\": \"label\", \"value\": \"---\"})\n            items.append(item_label)\n            items.append(item)\n\n        return {\n            \"items\": items,\n            \"title\": title\n        }\n\n    def launch(self, session, entities, event):\n        if \"values\" not in event[\"data\"]:\n            return\n\n        values = event[\"data\"][\"values\"]\n        if len(values) < 1:\n            return {\n                \"success\": True,\n                \"message\": \"No jobs to kill!\"\n            }\n\n        job_ids = set()\n        for job_id, kill_job in values.items():\n            if kill_job:\n                job_ids.add(job_id)\n\n        jobs = session.query(\n            \"select id, status from Job where id in ({})\".format(\n                self.join_query_keys(job_ids)\n            )\n        ).all()\n\n        # Update all the queried jobs, setting the status to failed.\n        for job in jobs:\n            try:\n                origin_status = job[\"status\"]\n                self.log.debug((\n                    'Changing Job ({}) status: {} -> failed'\n                ).format(job[\"id\"], origin_status))\n\n                job[\"status\"] = \"failed\"\n                session.commit()\n\n            except Exception:\n                session.rollback()\n                self.log.warning((\n                    \"Changing Job ({}) has failed\"\n                ).format(job[\"id\"]))\n\n        self.log.info(\"All selected jobs were killed Successfully!\")\n        return {\n            \"success\": True,\n            \"message\": \"All selected jobs were killed Successfully!\"\n        }\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    JobKiller(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py",
    "content": "from openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass MultipleNotes(BaseAction):\n    '''Edit meta data action.'''\n\n    #: Action identifier.\n    identifier = 'multiple.notes'\n    #: Action label.\n    label = 'Multiple Notes'\n    #: Action description.\n    description = 'Add same note to multiple entities'\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"MultipleNotes.svg\")\n\n    def discover(self, session, entities, event):\n        ''' Validation '''\n        valid = True\n\n        # Check for multiple selection.\n        if len(entities) < 2:\n            valid = False\n\n        # Check for valid entities.\n        valid_entity_types = ['assetversion', 'task']\n        for entity in entities:\n            if entity.entity_type.lower() not in valid_entity_types:\n                valid = False\n                break\n\n        return valid\n\n    def interface(self, session, entities, event):\n        if not event['data'].get('values', {}):\n            note_label = {\n                'type': 'label',\n                'value': '# Enter note: #'\n            }\n\n            note_value = {\n                'name': 'note',\n                'type': 'textarea'\n            }\n\n            category_label = {\n                'type': 'label',\n                'value': '## Category: ##'\n            }\n\n            category_data = []\n            category_data.append({\n                'label': '- None -',\n                'value': 'none'\n            })\n            all_categories = session.query('NoteCategory').all()\n            for cat in all_categories:\n                category_data.append({\n                    'label': cat['name'],\n                    'value': cat['id']\n                })\n            category_value = {\n                'type': 'enumerator',\n                'name': 'category',\n                'data': category_data,\n                'value': 'none'\n            }\n\n            splitter = {\n                'type': 'label',\n                'value': '{}'.format(200 * \"-\")\n            }\n\n            items = []\n            items.append(note_label)\n            items.append(note_value)\n            items.append(splitter)\n            items.append(category_label)\n            items.append(category_value)\n            return items\n\n    def launch(self, session, entities, event):\n        if 'values' not in event['data']:\n            return\n\n        values = event['data']['values']\n        if len(values) <= 0 or 'note' not in values:\n            return False\n        # Get Note text\n        note_value = values['note']\n        if note_value.lower().strip() == '':\n            return False\n        # Get User\n        user = session.query(\n            'User where username is \"{}\"'.format(session.api_user)\n        ).one()\n        # Base note data\n        note_data = {\n            'content': note_value,\n            'author': user\n        }\n        # Get category\n        category_value = values['category']\n        if category_value != 'none':\n            category = session.query(\n                'NoteCategory where id is \"{}\"'.format(category_value)\n            ).one()\n            note_data['category'] = category\n        # Create notes for entities\n        for entity in entities:\n            new_note = session.create('Note', note_data)\n            entity['notes'].append(new_note)\n            session.commit()\n        return True\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    MultipleNotes(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_prepare_project.py",
    "content": "import json\nimport copy\n\nfrom openpype.client import get_project, create_project\nfrom openpype.settings import ProjectSettings, SaveWarningExc\n\nfrom openpype_modules.ftrack.lib import (\n    BaseAction,\n    statics_icon,\n    get_openpype_attr,\n    CUST_ATTR_AUTO_SYNC\n)\n\n\nclass PrepareProjectLocal(BaseAction):\n    \"\"\"Prepare project attributes in Anatomy.\"\"\"\n\n    identifier = \"prepare.project.local\"\n    label = \"Prepare Project\"\n    description = \"Set basic attributes on the project\"\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"PrepareProject.svg\")\n\n    role_list = [\"Pypeclub\", \"Administrator\", \"Project Manager\"]\n\n    settings_key = \"prepare_project\"\n\n    # Key to store info about triggering create folder structure\n    create_project_structure_key = \"create_folder_structure\"\n    create_project_structure_identifier = \"create.project.structure\"\n    item_splitter = {\"type\": \"label\", \"value\": \"---\"}\n    _keys_order = (\n        \"fps\",\n        \"frameStart\",\n        \"frameEnd\",\n        \"handleStart\",\n        \"handleEnd\",\n        \"clipIn\",\n        \"clipOut\",\n        \"resolutionHeight\",\n        \"resolutionWidth\",\n        \"pixelAspect\",\n        \"applications\",\n        \"tools_env\",\n        \"library_project\",\n    )\n\n    def discover(self, session, entities, event):\n        \"\"\"Show only on project.\"\"\"\n        if (\n            len(entities) != 1\n            or entities[0].entity_type.lower() != \"project\"\n        ):\n            return False\n\n        return self.valid_roles(session, entities, event)\n\n    def interface(self, session, entities, event):\n        if event['data'].get('values', {}):\n            return\n\n        # Inform user that this may take a while\n        self.show_message(event, \"Preparing data... Please wait\", True)\n        self.log.debug(\"Preparing data which will be shown\")\n\n        self.log.debug(\"Loading custom attributes\")\n\n        project_entity = entities[0]\n        project_name = project_entity[\"full_name\"]\n\n        project_settings = ProjectSettings(project_name)\n\n        project_anatom_settings = project_settings[\"project_anatomy\"]\n        root_items = self.prepare_root_items(project_anatom_settings)\n\n        ca_items, multiselect_enumerators = (\n            self.prepare_custom_attribute_items(project_anatom_settings)\n        )\n\n        self.log.debug(\"Heavy items are ready. Preparing last items group.\")\n\n        title = \"Prepare Project\"\n        items = []\n\n        # Add root items\n        items.extend(root_items)\n\n        items.append(self.item_splitter)\n        items.append({\n            \"type\": \"label\",\n            \"value\": \"<h3>Set basic Attributes:</h3>\"\n        })\n\n        items.extend(ca_items)\n\n        # Set value of auto synchronization\n        auto_sync_value = project_entity[\"custom_attributes\"].get(\n            CUST_ATTR_AUTO_SYNC, False\n        )\n        auto_sync_item = {\n            \"name\": CUST_ATTR_AUTO_SYNC,\n            \"type\": \"boolean\",\n            \"value\": auto_sync_value,\n            \"label\": \"AutoSync to Avalon\"\n        }\n        # Add autosync attribute\n        items.append(auto_sync_item)\n\n        # This item will be last before enumerators\n        # Ask if want to trigger Action Create Folder Structure\n        create_project_structure_checked = (\n            project_settings\n            [\"project_settings\"]\n            [\"ftrack\"]\n            [\"user_handlers\"]\n            [\"prepare_project\"]\n            [\"create_project_structure_checked\"]\n        ).value\n        items.append({\n            \"type\": \"label\",\n            \"value\": \"<h3>Want to create basic Folder Structure?</h3>\"\n        })\n        items.append({\n            \"name\": self.create_project_structure_key,\n            \"type\": \"boolean\",\n            \"value\": create_project_structure_checked,\n            \"label\": \"Check if Yes\"\n        })\n\n        # Add enumerator items at the end\n        for item in multiselect_enumerators:\n            items.append(item)\n\n        return {\n            \"items\": items,\n            \"title\": title\n        }\n\n    def prepare_root_items(self, project_anatom_settings):\n        self.log.debug(\"Root items preparation begins.\")\n\n        root_items = []\n        root_items.append({\n            \"type\": \"label\",\n            \"value\": \"<h3>Check your Project root settings</h3>\"\n        })\n        root_items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<p><i>NOTE: Roots are <b>crucial</b> for path filling\"\n                \" (and creating folder structure).</i></p>\"\n            )\n        })\n        root_items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<p><i>WARNING: Do not change roots on running project,\"\n                \" that <b>will cause workflow issues</b>.</i></p>\"\n            )\n        })\n\n        empty_text = \"Enter root path here...\"\n\n        roots_entity = project_anatom_settings[\"roots\"]\n        for root_name, root_entity in roots_entity.items():\n            root_items.append(self.item_splitter)\n            root_items.append({\n                \"type\": \"label\",\n                \"value\": \"Root: \\\"{}\\\"\".format(root_name)\n            })\n            for platform_name, value_entity in root_entity.items():\n                root_items.append({\n                    \"label\": platform_name,\n                    \"name\": \"__root__{}__{}\".format(root_name, platform_name),\n                    \"type\": \"text\",\n                    \"value\": value_entity.value,\n                    \"empty_text\": empty_text\n                })\n\n        root_items.append({\n            \"type\": \"hidden\",\n            \"name\": \"__rootnames__\",\n            \"value\": json.dumps(list(roots_entity.keys()))\n        })\n\n        self.log.debug(\"Root items preparation ended.\")\n        return root_items\n\n    def _attributes_to_set(self, project_anatom_settings):\n        attributes_to_set = {}\n\n        attribute_values_by_key = {}\n        for key, entity in project_anatom_settings[\"attributes\"].items():\n            attribute_values_by_key[key] = entity.value\n\n        cust_attrs, hier_cust_attrs = get_openpype_attr(self.session, True)\n\n        for attr in hier_cust_attrs:\n            key = attr[\"key\"]\n            if key.startswith(\"avalon_\"):\n                continue\n            attributes_to_set[key] = {\n                \"label\": attr[\"label\"],\n                \"object\": attr,\n                \"default\": attribute_values_by_key.get(key)\n            }\n\n        for attr in cust_attrs:\n            if attr[\"entity_type\"].lower() != \"show\":\n                continue\n            key = attr[\"key\"]\n            if key.startswith(\"avalon_\"):\n                continue\n            attributes_to_set[key] = {\n                \"label\": attr[\"label\"],\n                \"object\": attr,\n                \"default\": attribute_values_by_key.get(key)\n            }\n\n        # Sort by label\n        attributes_to_set = dict(sorted(\n            attributes_to_set.items(),\n            key=lambda x: x[1][\"label\"]\n        ))\n        return attributes_to_set\n\n    def prepare_custom_attribute_items(self, project_anatom_settings):\n        items = []\n        multiselect_enumerators = []\n        attributes_to_set = self._attributes_to_set(project_anatom_settings)\n\n        self.log.debug(\"Preparing interface for keys: \\\"{}\\\"\".format(\n            str([key for key in attributes_to_set])\n        ))\n\n        attribute_keys = set(attributes_to_set.keys())\n        keys_order = []\n        for key in self._keys_order:\n            if key in attribute_keys:\n                keys_order.append(key)\n\n        attribute_keys = attribute_keys - set(keys_order)\n        for key in sorted(attribute_keys):\n            keys_order.append(key)\n\n        for key in keys_order:\n            in_data = attributes_to_set[key]\n            attr = in_data[\"object\"]\n\n            # initial item definition\n            item = {\n                \"name\": key,\n                \"label\": in_data[\"label\"]\n            }\n\n            # cust attr type - may have different visualization\n            type_name = attr[\"type\"][\"name\"].lower()\n            easy_types = [\"text\", \"boolean\", \"date\", \"number\"]\n\n            easy_type = False\n            if type_name in easy_types:\n                easy_type = True\n\n            elif type_name == \"enumerator\":\n\n                attr_config = json.loads(attr[\"config\"])\n                attr_config_data = json.loads(attr_config[\"data\"])\n\n                if attr_config[\"multiSelect\"] is True:\n                    multiselect_enumerators.append(self.item_splitter)\n                    multiselect_enumerators.append({\n                        \"type\": \"label\",\n                        \"value\": \"<h3>{}</h3>\".format(in_data[\"label\"])\n                    })\n\n                    default = in_data[\"default\"]\n                    names = []\n                    for option in sorted(\n                        attr_config_data, key=lambda x: x[\"menu\"]\n                    ):\n                        name = option[\"value\"]\n                        new_name = \"__{}__{}\".format(key, name)\n                        names.append(new_name)\n                        item = {\n                            \"name\": new_name,\n                            \"type\": \"boolean\",\n                            \"label\": \"- {}\".format(option[\"menu\"])\n                        }\n                        if default:\n                            if isinstance(default, (list, tuple)):\n                                if name in default:\n                                    item[\"value\"] = True\n                            else:\n                                if name == default:\n                                    item[\"value\"] = True\n\n                        multiselect_enumerators.append(item)\n\n                    multiselect_enumerators.append({\n                        \"type\": \"hidden\",\n                        \"name\": \"__hidden__{}\".format(key),\n                        \"value\": json.dumps(names)\n                    })\n                else:\n                    easy_type = True\n                    item[\"data\"] = attr_config_data\n\n            else:\n                self.log.warning((\n                    \"Custom attribute \\\"{}\\\" has type \\\"{}\\\".\"\n                    \" I don't know how to handle\"\n                ).format(key, type_name))\n                items.append({\n                    \"type\": \"label\",\n                    \"value\": (\n                        \"!!! Can't handle Custom attritubte type \\\"{}\\\"\"\n                        \" (key: \\\"{}\\\")\"\n                    ).format(type_name, key)\n                })\n\n            if easy_type:\n                item[\"type\"] = type_name\n\n                # default value in interface\n                default = in_data[\"default\"]\n                if default is not None:\n                    item[\"value\"] = default\n\n                items.append(item)\n\n        return items, multiselect_enumerators\n\n    def launch(self, session, entities, event):\n        in_data = event[\"data\"].get(\"values\")\n        if not in_data:\n            return\n\n        create_project_structure_checked = in_data.pop(\n            self.create_project_structure_key\n        )\n\n        root_values = {}\n        root_key = \"__root__\"\n        for key in tuple(in_data.keys()):\n            if key.startswith(root_key):\n                _key = key[len(root_key):]\n                root_values[_key] = in_data.pop(key)\n\n        root_names = in_data.pop(\"__rootnames__\", None)\n        root_data = {}\n        for root_name in json.loads(root_names):\n            root_data[root_name] = {}\n            for key, value in tuple(root_values.items()):\n                prefix = \"{}__\".format(root_name)\n                if not key.startswith(prefix):\n                    continue\n\n                _key = key[len(prefix):]\n                root_data[root_name][_key] = value\n\n        # Find hidden items for multiselect enumerators\n        keys_to_process = []\n        for key in in_data:\n            if key.startswith(\"__hidden__\"):\n                keys_to_process.append(key)\n\n        self.log.debug(\"Preparing data for Multiselect Enumerators\")\n        enumerators = {}\n        for key in keys_to_process:\n            new_key = key.replace(\"__hidden__\", \"\")\n            enumerator_items = in_data.pop(key)\n            enumerators[new_key] = json.loads(enumerator_items)\n\n        # find values set for multiselect enumerator\n        for key, enumerator_items in enumerators.items():\n            in_data[key] = []\n\n            name = \"__{}__\".format(key)\n\n            for item in enumerator_items:\n                value = in_data.pop(item)\n                if value is True:\n                    new_key = item.replace(name, \"\")\n                    in_data[key].append(new_key)\n\n        self.log.debug(\"Setting Custom Attribute values\")\n\n        project_entity = entities[0]\n        project_name = project_entity[\"full_name\"]\n\n        # Try to find project document\n        project_doc = get_project(project_name)\n\n        # Create project if is not available\n        # - creation is required to be able set project anatomy and attributes\n        if not project_doc:\n            project_code = project_entity[\"name\"]\n            self.log.info(\"Creating project \\\"{} [{}]\\\"\".format(\n                project_name, project_code\n            ))\n            create_project(project_name, project_code)\n            self.trigger_event(\n                \"openpype.project.created\",\n                {\"project_name\": project_name}\n            )\n\n        project_settings = ProjectSettings(project_name)\n        project_anatomy_settings = project_settings[\"project_anatomy\"]\n        project_anatomy_settings[\"roots\"] = root_data\n\n        custom_attribute_values = {}\n        attributes_entity = project_anatomy_settings[\"attributes\"]\n        for key, value in in_data.items():\n            if key not in attributes_entity:\n                custom_attribute_values[key] = value\n            else:\n                attributes_entity[key] = value\n\n        try:\n            project_settings.save()\n        except SaveWarningExc as exc:\n            self.log.info(\"Few warnings happened during settings save:\")\n            for warning in exc.warnings:\n                self.log.info(str(warning))\n\n        # Change custom attributes on project\n        if custom_attribute_values:\n            for key, value in custom_attribute_values.items():\n                project_entity[\"custom_attributes\"][key] = value\n                self.log.debug(\"- Key \\\"{}\\\" set to \\\"{}\\\"\".format(key, value))\n            session.commit()\n\n        # Trigger create project structure action\n        if create_project_structure_checked:\n            trigger_identifier = \"{}.{}\".format(\n                self.create_project_structure_identifier,\n                self.process_identifier()\n            )\n            self.trigger_action(trigger_identifier, event)\n\n        event_data = copy.deepcopy(in_data)\n        event_data[\"project_name\"] = project_name\n        self.trigger_event(\"openpype.project.prepared\", event_data)\n        return True\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n    PrepareProjectLocal(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_rv.py",
    "content": "import os\nimport subprocess\nimport traceback\nimport json\n\nimport ftrack_api\n\nfrom openpype.client import (\n    get_asset_by_name,\n    get_subset_by_name,\n    get_version_by_name,\n    get_representation_by_name\n)\nfrom openpype.pipeline import (\n    get_representation_path,\n    AvalonMongoDB,\n    Anatomy,\n)\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass RVAction(BaseAction):\n    \"\"\" Launch RV action \"\"\"\n    identifier = \"rv.launch.action\"\n    label = \"rv\"\n    description = \"rv Launcher\"\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"RV.png\")\n\n    type = 'Application'\n\n    allowed_types = [\"img\", \"mov\", \"exr\", \"mp4\"]\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # QUESTION load RV application data from AppplicationManager?\n        rv_path = None\n\n        # RV_HOME should be set if properly installed\n        if os.environ.get('RV_HOME'):\n            rv_path = os.path.join(\n                os.environ.get('RV_HOME'),\n                'bin',\n                'rv'\n            )\n            if not os.path.exists(rv_path):\n                rv_path = None\n\n        if not rv_path:\n            self.log.info(\"RV path was not found.\")\n            self.ignore_me = True\n\n        self.rv_path = rv_path\n\n    def discover(self, session, entities, event):\n        \"\"\"Return available actions based on *event*. \"\"\"\n        return True\n\n    def preregister(self):\n        if self.rv_path is None:\n            return (\n                'RV is not installed or paths in presets are not set correctly'\n            )\n        return True\n\n    def get_components_from_entity(self, session, entity, components):\n        \"\"\"Get components from various entity types.\n\n        The components dictionary is modified in place, so nothing is returned.\n\n            Args:\n                entity (Ftrack entity)\n                components (dict)\n        \"\"\"\n\n        if entity.entity_type.lower() == \"assetversion\":\n            for component in entity[\"components\"]:\n                if component[\"file_type\"][1:] not in self.allowed_types:\n                    continue\n\n                try:\n                    components[entity[\"asset\"][\"parent\"][\"name\"]].append(\n                        component\n                    )\n                except KeyError:\n                    components[entity[\"asset\"][\"parent\"][\"name\"]] = [component]\n\n            return\n\n        if entity.entity_type.lower() == \"task\":\n            query = \"AssetVersion where task_id is '{0}'\".format(entity[\"id\"])\n            for assetversion in session.query(query):\n                self.get_components_from_entity(\n                    session, assetversion, components\n                )\n\n            return\n\n        if entity.entity_type.lower() == \"shot\":\n            query = \"AssetVersion where asset.parent.id is '{0}'\".format(\n                entity[\"id\"]\n            )\n            for assetversion in session.query(query):\n                self.get_components_from_entity(\n                    session, assetversion, components\n                )\n\n            return\n\n        raise NotImplementedError(\n            \"\\\"{}\\\" entity type is not implemented yet.\".format(\n                entity.entity_type\n            )\n        )\n\n    def interface(self, session, entities, event):\n        if event['data'].get('values', {}):\n            return\n\n        user = session.query(\n            \"User where username is '{0}'\".format(\n                os.environ[\"FTRACK_API_USER\"]\n            )\n        ).one()\n        job = session.create(\n            \"Job\",\n            {\n                \"user\": user,\n                \"status\": \"running\",\n                \"data\": json.dumps({\n                    \"description\": \"RV: Collecting components.\"\n                })\n            }\n        )\n        # Commit to feedback to user.\n        session.commit()\n\n        items = []\n        try:\n            items = self.get_interface_items(session, entities)\n        except Exception:\n            self.log.error(traceback.format_exc())\n            job[\"status\"] = \"failed\"\n        else:\n            job[\"status\"] = \"done\"\n\n        # Commit to end job.\n        session.commit()\n\n        return {\"items\": items}\n\n    def get_interface_items(self, session, entities):\n\n        components = {}\n        for entity in entities:\n            self.get_components_from_entity(session, entity, components)\n\n        # Sort by version\n        for parent_name, entities in components.items():\n            version_mapping = {}\n            for entity in entities:\n                try:\n                    version_mapping[entity[\"version\"][\"version\"]].append(\n                        entity\n                    )\n                except KeyError:\n                    version_mapping[entity[\"version\"][\"version\"]] = [entity]\n\n            # Sort same versions by date.\n            for version, entities in version_mapping.items():\n                version_mapping[version] = sorted(\n                    entities, key=lambda x: x[\"version\"][\"date\"], reverse=True\n                )\n\n            components[parent_name] = []\n            for version in reversed(sorted(version_mapping.keys())):\n                components[parent_name].extend(version_mapping[version])\n\n        # Items to present to user.\n        items = []\n        label = \"{} - v{} - {}\"\n        for parent_name, entities in components.items():\n            data = []\n            for entity in entities:\n                data.append(\n                    {\n                        \"label\": label.format(\n                            entity[\"version\"][\"asset\"][\"name\"],\n                            str(entity[\"version\"][\"version\"]).zfill(3),\n                            entity[\"file_type\"][1:]\n                        ),\n                        \"value\": entity[\"id\"]\n                    }\n                )\n\n            items.append(\n                {\n                    \"label\": parent_name,\n                    \"type\": \"enumerator\",\n                    \"name\": parent_name,\n                    \"data\": data,\n                    \"value\": data[0][\"value\"]\n                }\n            )\n\n        return items\n\n    def launch(self, session, entities, event):\n        \"\"\"Callback method for RV action.\"\"\"\n        # Launching application\n        if \"values\" not in event[\"data\"]:\n            return\n\n        user = session.query(\n            \"User where username is '{0}'\".format(\n                os.environ[\"FTRACK_API_USER\"]\n            )\n        ).one()\n        job = session.create(\n            \"Job\",\n            {\n                \"user\": user,\n                \"status\": \"running\",\n                \"data\": json.dumps({\n                    \"description\": \"RV: Collecting file paths.\"\n                })\n            }\n        )\n        # Commit to feedback to user.\n        session.commit()\n\n        paths = []\n        try:\n            paths = self.get_file_paths(session, event)\n        except Exception:\n            self.log.error(traceback.format_exc())\n            job[\"status\"] = \"failed\"\n        else:\n            job[\"status\"] = \"done\"\n\n        # Commit to end job.\n        session.commit()\n\n        args = [os.path.normpath(self.rv_path)]\n\n        fps = entities[0].get(\"custom_attributes\", {}).get(\"fps\", None)\n        if fps is not None:\n            args.extend([\"-fps\", str(fps)])\n\n        args.extend(paths)\n\n        self.log.info(\"Running rv: {}\".format(args))\n\n        subprocess.Popen(args)\n\n        return True\n\n    def get_file_paths(self, session, event):\n        \"\"\"Get file paths from selected components.\"\"\"\n\n        link = session.get(\n            \"Component\", list(event[\"data\"][\"values\"].values())[0]\n        )[\"version\"][\"asset\"][\"parent\"][\"link\"][0]\n        project = session.get(link[\"type\"], link[\"id\"])\n        project_name = project[\"full_name\"]\n        dbcon = AvalonMongoDB()\n        dbcon.Session[\"AVALON_PROJECT\"] = project_name\n        anatomy = Anatomy(project_name)\n\n        location = ftrack_api.Session().pick_location()\n\n        paths = []\n        for parent_name in sorted(event[\"data\"][\"values\"].keys()):\n            component = session.get(\n                \"Component\", event[\"data\"][\"values\"][parent_name]\n            )\n\n            # Newer publishes have the source referenced in Ftrack.\n            online_source = False\n            for neighbour_component in component[\"version\"][\"components\"]:\n                if neighbour_component[\"name\"] != \"ftrackreview-mp4_src\":\n                    continue\n\n                paths.append(\n                    location.get_filesystem_path(neighbour_component)\n                )\n                online_source = True\n\n            if online_source:\n                continue\n\n            subset_name = component[\"version\"][\"asset\"][\"name\"]\n            version_name = component[\"version\"][\"version\"]\n            representation_name = component[\"file_type\"][1:]\n\n            asset_doc = get_asset_by_name(\n                project_name, parent_name, fields=[\"_id\"]\n            )\n            subset_doc = get_subset_by_name(\n                project_name,\n                subset_name=subset_name,\n                asset_id=asset_doc[\"_id\"]\n            )\n            version_doc = get_version_by_name(\n                project_name,\n                version=version_name,\n                subset_id=subset_doc[\"_id\"]\n            )\n            repre_doc = get_representation_by_name(\n                project_name,\n                version_id=version_doc[\"_id\"],\n                representation_name=representation_name\n            )\n            if not repre_doc:\n                repre_doc = get_representation_by_name(\n                    project_name,\n                    version_id=version_doc[\"_id\"],\n                    representation_name=\"preview\"\n                )\n\n            paths.append(get_representation_path(\n                repre_doc, root=anatomy.roots, dbcon=dbcon\n            ))\n\n        return paths\n\n\ndef register(session):\n    \"\"\"Register hooks.\"\"\"\n\n    RVAction(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_seed.py",
    "content": "import os\nfrom operator import itemgetter\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass SeedDebugProject(BaseAction):\n    '''Edit meta data action.'''\n\n    #: Action identifier.\n    identifier = \"seed.debug.project\"\n    #: Action label.\n    label = \"Seed Debug Project\"\n    #: Action description.\n    description = \"Description\"\n    #: priority\n    priority = 100\n    #: roles that are allowed to register this action\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"SeedProject.svg\")\n\n    # Asset names which will be created in `Assets` entity\n    assets = [\n        \"Addax\", \"Alpaca\", \"Ant\", \"Antelope\", \"Aye\", \"Badger\", \"Bear\", \"Bee\",\n        \"Beetle\", \"Bluebird\", \"Bongo\", \"Bontebok\", \"Butterflie\", \"Caiman\",\n        \"Capuchin\", \"Capybara\", \"Cat\", \"Caterpillar\", \"Coyote\", \"Crocodile\",\n        \"Cuckoo\", \"Deer\", \"Dragonfly\", \"Duck\", \"Eagle\", \"Egret\", \"Elephant\",\n        \"Falcon\", \"Fossa\", \"Fox\", \"Gazelle\", \"Gecko\", \"Gerbil\",\n        \"GiantArmadillo\", \"Gibbon\", \"Giraffe\", \"Goose\", \"Gorilla\",\n        \"Grasshoper\", \"Hare\", \"Hawk\", \"Hedgehog\", \"Heron\", \"Hog\",\n        \"Hummingbird\", \"Hyena\", \"Chameleon\", \"Cheetah\", \"Iguana\", \"Jackal\",\n        \"Jaguar\", \"Kingfisher\", \"Kinglet\", \"Kite\", \"Komodo\", \"Lemur\",\n        \"Leopard\", \"Lion\", \"Lizard\", \"Macaw\", \"Malachite\", \"Mandrill\",\n        \"Mantis\", \"Marmoset\", \"Meadowlark\", \"Meerkat\", \"Mockingbird\",\n        \"Mongoose\", \"Monkey\", \"Nyal\", \"Ocelot\", \"Okapi\", \"Oribi\", \"Oriole\",\n        \"Otter\", \"Owl\", \"Panda\", \"Parrot\", \"Pelican\", \"Pig\", \"Porcupine\",\n        \"Reedbuck\", \"Rhinocero\", \"Sandpiper\", \"Servil\", \"Skink\", \"Sloth\",\n        \"Snake\", \"Spider\", \"Squirrel\", \"Sunbird\", \"Swallow\", \"Swift\", \"Tiger\",\n        \"Sylph\", \"Tanager\", \"Vulture\", \"Warthog\", \"Waterbuck\", \"Woodpecker\",\n        \"Zebra\"\n    ]\n\n    # Tasks which will be created for Assets\n    asset_tasks = [\n        \"Modeling\", \"Lookdev\", \"Rigging\"\n    ]\n    # Tasks which will be created for Shots\n    shot_tasks = [\n        \"Animation\", \"Lighting\", \"Compositing\", \"FX\"\n    ]\n\n    # Define how much sequences will be created\n    default_seq_count = 5\n    # Define how much shots will be created for each sequence\n    default_shots_count = 10\n\n    max_entities_created_at_one_commit = 50\n\n    existing_projects = None\n    new_project_item = \"< New Project >\"\n    current_project_item = \"< Current Project >\"\n    settings_key = \"seed_project\"\n\n    def discover(self, session, entities, event):\n        ''' Validation '''\n        if not self.valid_roles(session, entities, event):\n            return False\n        return True\n\n    def interface(self, session, entities, event):\n        if event[\"data\"].get(\"values\", {}):\n            return\n\n        title = \"Select Project where you want to create seed data\"\n\n        items = []\n        item_splitter = {\"type\": \"label\", \"value\": \"---\"}\n\n        description_label = {\n            \"type\": \"label\",\n            \"value\": (\n                \"WARNING: Action does NOT check if entities already exist !!!\"\n            )\n        }\n        items.append(description_label)\n\n        all_projects = session.query(\"select full_name from Project\").all()\n        self.existing_projects = [proj[\"full_name\"] for proj in all_projects]\n        projects_items = [\n            {\"label\": proj, \"value\": proj} for proj in self.existing_projects\n        ]\n\n        data_items = []\n\n        data_items.append({\n            \"label\": self.new_project_item,\n            \"value\": self.new_project_item\n        })\n\n        data_items.append({\n            \"label\": self.current_project_item,\n            \"value\": self.current_project_item\n        })\n\n        data_items.extend(sorted(\n            projects_items,\n            key=itemgetter(\"label\"),\n            reverse=False\n        ))\n        projects_item = {\n            \"label\": \"Choose Project\",\n            \"type\": \"enumerator\",\n            \"name\": \"project_name\",\n            \"data\": data_items,\n            \"value\": self.current_project_item\n        }\n        items.append(projects_item)\n        items.append(item_splitter)\n\n        items.append({\n            \"label\": \"Number of assets\",\n            \"type\": \"number\",\n            \"name\": \"asset_count\",\n            \"value\": len(self.assets)\n        })\n        items.append({\n            \"label\": \"Number of sequences\",\n            \"type\": \"number\",\n            \"name\": \"seq_count\",\n            \"value\": self.default_seq_count\n        })\n        items.append({\n            \"label\": \"Number of shots\",\n            \"type\": \"number\",\n            \"name\": \"shots_count\",\n            \"value\": self.default_shots_count\n        })\n        items.append(item_splitter)\n\n        note_label = {\n            \"type\": \"label\",\n            \"value\": (\n                \"<p><i>NOTE: Enter project name and choose schema if you \"\n                \"chose `\\\"< New Project >\\\"`(code is optional)</i><p>\"\n            )\n        }\n        items.append(note_label)\n        items.append({\n            \"label\": \"Project name\",\n            \"name\": \"new_project_name\",\n            \"type\": \"text\",\n            \"value\": \"\"\n        })\n\n        project_schemas = [\n            sch[\"name\"] for sch in self.session.query(\"ProjectSchema\").all()\n        ]\n        schemas_item = {\n            \"label\": \"Choose Schema\",\n            \"type\": \"enumerator\",\n            \"name\": \"new_schema_name\",\n            \"data\": [\n                {\"label\": sch, \"value\": sch} for sch in project_schemas\n            ],\n            \"value\": project_schemas[0]\n        }\n        items.append(schemas_item)\n\n        items.append({\n            \"label\": \"*Project code\",\n            \"name\": \"new_project_code\",\n            \"type\": \"text\",\n            \"value\": \"\",\n            \"empty_text\": \"Optional...\"\n        })\n\n        return {\n            \"items\": items,\n            \"title\": title\n        }\n\n    def launch(self, session, in_entities, event):\n        if \"values\" not in event[\"data\"]:\n            return\n\n        # THIS IS THE PROJECT PART\n        values = event[\"data\"][\"values\"]\n        selected_project = values[\"project_name\"]\n        if selected_project == self.new_project_item:\n            project_name = values[\"new_project_name\"]\n            if project_name in self.existing_projects:\n                msg = \"Project \\\"{}\\\" already exist\".format(project_name)\n                self.log.error(msg)\n                return {\"success\": False, \"message\": msg}\n\n            project_code = values[\"new_project_code\"]\n            project_schema_name = values[\"new_schema_name\"]\n            if not project_code:\n                project_code = project_name\n            project_code = project_code.lower().replace(\" \", \"_\").strip()\n            _project = session.query(\n                \"Project where name is \\\"{}\\\"\".format(project_code)\n            ).first()\n            if _project:\n                msg = \"Project with code \\\"{}\\\" already exist\".format(\n                    project_code\n                )\n                self.log.error(msg)\n                return {\"success\": False, \"message\": msg}\n\n            project_schema = session.query(\n                \"ProjectSchema where name is \\\"{}\\\"\".format(\n                    project_schema_name\n                )\n            ).one()\n            # Create the project with the chosen schema.\n            self.log.debug((\n                \"*** Creating Project: name <{}>, code <{}>, schema <{}>\"\n            ).format(project_name, project_code, project_schema_name))\n            project = session.create(\"Project\", {\n                \"name\": project_code,\n                \"full_name\": project_name,\n                \"project_schema\": project_schema\n            })\n            session.commit()\n\n        elif selected_project == self.current_project_item:\n            entity = in_entities[0]\n            if entity.entity_type.lower() == \"project\":\n                project = entity\n            else:\n                if \"project\" in entity:\n                    project = entity[\"project\"]\n                else:\n                    project = entity[\"parent\"][\"project\"]\n            project_schema = project[\"project_schema\"]\n            self.log.debug((\n                \"*** Using Project: name <{}>, code <{}>, schema <{}>\"\n            ).format(\n                project[\"full_name\"], project[\"name\"], project_schema[\"name\"]\n            ))\n        else:\n            project = session.query(\"Project where full_name is \\\"{}\\\"\".format(\n                selected_project\n            )).one()\n            project_schema = project[\"project_schema\"]\n            self.log.debug((\n                \"*** Using Project: name <{}>, code <{}>, schema <{}>\"\n            ).format(\n                project[\"full_name\"], project[\"name\"], project_schema[\"name\"]\n            ))\n\n        # THIS IS THE MAGIC PART\n        task_types = {}\n        for _type in project_schema[\"_task_type_schema\"][\"types\"]:\n            if _type[\"name\"] not in task_types:\n                task_types[_type[\"name\"]] = _type\n        self.task_types = task_types\n\n        asset_count = values.get(\"asset_count\") or len(self.assets)\n        seq_count = values.get(\"seq_count\") or self.default_seq_count\n        shots_count = values.get(\"shots_count\") or self.default_shots_count\n\n        self.create_assets(project, asset_count)\n        self.create_shots(project, seq_count, shots_count)\n\n        return True\n\n    def create_assets(self, project, asset_count):\n        self.log.debug(\"*** Creating assets:\")\n\n        try:\n            asset_count = int(asset_count)\n        except ValueError:\n            asset_count = 0\n\n        if asset_count <= 0:\n            self.log.debug(\"No assets to create\")\n            return\n\n        main_entity = self.session.create(\"Folder\", {\n            \"name\": \"Assets\",\n            \"parent\": project\n        })\n        self.log.debug(\"- Assets\")\n        available_assets = len(self.assets)\n        repetitive_times = (\n            int(asset_count / available_assets) +\n            (asset_count % available_assets > 0)\n        )\n\n        index = 0\n        created_entities = 0\n        to_create_length = asset_count + (asset_count * len(self.asset_tasks))\n        for _asset_name in self.assets:\n            if created_entities >= to_create_length:\n                break\n            for asset_num in range(1, repetitive_times + 1):\n                if created_entities >= asset_count:\n                    break\n                asset_name = \"%s_%02d\" % (_asset_name, asset_num)\n                asset = self.session.create(\"AssetBuild\", {\n                    \"name\": asset_name,\n                    \"parent\": main_entity\n                })\n                self.log.debug(\"- Assets/{}\".format(asset_name))\n\n                created_entities += 1\n                index += 1\n                if self.temp_commit(index, created_entities, to_create_length):\n                    index = 0\n\n                for task_name in self.asset_tasks:\n                    self.session.create(\"Task\", {\n                        \"name\": task_name,\n                        \"parent\": asset,\n                        \"type\": self.task_types[task_name]\n                    })\n                    self.log.debug(\"- Assets/{}/{}\".format(\n                        asset_name, task_name\n                    ))\n\n                    created_entities += 1\n                    index += 1\n                    if self.temp_commit(\n                        index, created_entities, to_create_length\n                    ):\n                        index = 0\n\n        self.log.debug(\"*** Committing Assets\")\n        self.log.debug(\"Committing entities. {}/{}\".format(\n            created_entities, to_create_length\n        ))\n        self.session.commit()\n\n    def create_shots(self, project, seq_count, shots_count):\n        self.log.debug(\"*** Creating shots:\")\n\n        # Convert counts to integers\n        try:\n            seq_count = int(seq_count)\n        except ValueError:\n            seq_count = 0\n\n        try:\n            shots_count = int(shots_count)\n        except ValueError:\n            shots_count = 0\n\n        # Check if both are higher than 0\n        missing = []\n        if seq_count <= 0:\n            missing.append(\"sequences\")\n\n        if shots_count <= 0:\n            missing.append(\"shots\")\n\n        if missing:\n            self.log.debug(\"No {} to create\".format(\" and \".join(missing)))\n            return\n\n        # Create Folder \"Shots\"\n        main_entity = self.session.create(\"Folder\", {\n            \"name\": \"Shots\",\n            \"parent\": project\n        })\n        self.log.debug(\"- Shots\")\n\n        index = 0\n        created_entities = 0\n        to_create_length = (\n            seq_count\n            + (seq_count * shots_count)\n            + (seq_count * shots_count * len(self.shot_tasks))\n        )\n        for seq_num in range(1, seq_count + 1):\n            seq_name = \"sq%03d\" % seq_num\n            seq = self.session.create(\"Sequence\", {\n                \"name\": seq_name,\n                \"parent\": main_entity\n            })\n            self.log.debug(\"- Shots/{}\".format(seq_name))\n\n            created_entities += 1\n            index += 1\n            if self.temp_commit(index, created_entities, to_create_length):\n                index = 0\n\n            for shot_num in range(1, shots_count + 1):\n                shot_name = \"%ssh%04d\" % (seq_name, (shot_num * 10))\n                shot = self.session.create(\"Shot\", {\n                    \"name\": shot_name,\n                    \"parent\": seq\n                })\n                self.log.debug(\"- Shots/{}/{}\".format(seq_name, shot_name))\n\n                created_entities += 1\n                index += 1\n                if self.temp_commit(index, created_entities, to_create_length):\n                    index = 0\n\n                for task_name in self.shot_tasks:\n                    self.session.create(\"Task\", {\n                        \"name\": task_name,\n                        \"parent\": shot,\n                        \"type\": self.task_types[task_name]\n                    })\n                    self.log.debug(\"- Shots/{}/{}/{}\".format(\n                        seq_name, shot_name, task_name\n                    ))\n\n                    created_entities += 1\n                    index += 1\n                    if self.temp_commit(\n                        index, created_entities, to_create_length\n                    ):\n                        index = 0\n\n        self.log.debug(\"*** Committing Shots\")\n        self.log.debug(\"Committing entities. {}/{}\".format(\n            created_entities, to_create_length\n        ))\n        self.session.commit()\n\n    def temp_commit(self, index, created_entities, to_create_length):\n        if index < self.max_entities_created_at_one_commit:\n            return False\n        self.log.debug(\"Committing {} entities. {}/{}\".format(\n            index, created_entities, to_create_length\n        ))\n        self.session.commit()\n        return True\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    SeedDebugProject(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py",
    "content": "import os\nimport errno\nimport json\nimport requests\n\nfrom bson.objectid import ObjectId\n\nfrom openpype.client import (\n    get_project,\n    get_asset_by_id,\n    get_assets,\n    get_subset_by_name,\n    get_version_by_name,\n    get_representations\n)\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\nfrom openpype.pipeline import AvalonMongoDB, Anatomy\n\nfrom openpype_modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY\n\n\nclass StoreThumbnailsToAvalon(BaseAction):\n    # Action identifier\n    identifier = \"store.thubmnail.to.avalon\"\n    # Action label\n    label = \"OpenPype Admin\"\n    # Action variant\n    variant = \"- Store Thumbnails to avalon\"\n    # Action description\n    description = 'Test action'\n    # roles that are allowed to register this action\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"OpenPypeAdmin.svg\")\n    settings_key = \"store_thubmnail_to_avalon\"\n\n    thumbnail_key = \"AVALON_THUMBNAIL_ROOT\"\n\n    def __init__(self, *args, **kwargs):\n        self.db_con = AvalonMongoDB()\n        super(StoreThumbnailsToAvalon, self).__init__(*args, **kwargs)\n\n    def discover(self, session, entities, event):\n        is_valid = False\n        for entity in entities:\n            if entity.entity_type.lower() == \"assetversion\":\n                is_valid = True\n                break\n\n        if is_valid:\n            is_valid = self.valid_roles(session, entities, event)\n        return is_valid\n\n    def launch(self, session, entities, event):\n        user = session.query(\n            \"User where username is '{0}'\".format(session.api_user)\n        ).one()\n        action_job = session.create(\"Job\", {\n            \"user\": user,\n            \"status\": \"running\",\n            \"data\": json.dumps({\n                \"description\": \"Storing thumbnails to avalon.\"\n            })\n        })\n        session.commit()\n\n        project = self.get_project_from_entity(entities[0])\n        project_name = project[\"full_name\"]\n        anatomy = Anatomy(project_name)\n\n        if \"publish\" not in anatomy.templates:\n            msg = \"Anatomy does not have set publish key!\"\n\n            action_job[\"status\"] = \"failed\"\n            session.commit()\n\n            self.log.warning(msg)\n\n            return {\n                \"success\": False,\n                \"message\": msg\n            }\n\n        if \"thumbnail\" not in anatomy.templates[\"publish\"]:\n            msg = (\n                \"There is not set \\\"thumbnail\\\"\"\n                \" template in Antomy for project \\\"{}\\\"\"\n            ).format(project_name)\n\n            action_job[\"status\"] = \"failed\"\n            session.commit()\n\n            self.log.warning(msg)\n\n            return {\n                \"success\": False,\n                \"message\": msg\n            }\n\n        thumbnail_roots = os.environ.get(self.thumbnail_key)\n        if (\n            \"{thumbnail_root}\" in anatomy.templates[\"publish\"][\"thumbnail\"]\n            and not thumbnail_roots\n        ):\n            msg = \"`{}` environment is not set\".format(self.thumbnail_key)\n\n            action_job[\"status\"] = \"failed\"\n            session.commit()\n\n            self.log.warning(msg)\n\n            return {\n                \"success\": False,\n                \"message\": msg\n            }\n\n        existing_thumbnail_root = None\n        for path in thumbnail_roots.split(os.pathsep):\n            if os.path.exists(path):\n                existing_thumbnail_root = path\n                break\n\n        if existing_thumbnail_root is None:\n            msg = (\n                \"Can't access paths, set in `{}` ({})\"\n            ).format(self.thumbnail_key, thumbnail_roots)\n\n            action_job[\"status\"] = \"failed\"\n            session.commit()\n\n            self.log.warning(msg)\n\n            return {\n                \"success\": False,\n                \"message\": msg\n            }\n\n        example_template_data = {\n            \"_id\": \"ID\",\n            \"thumbnail_root\": \"THUBMNAIL_ROOT\",\n            \"thumbnail_type\": \"THUMBNAIL_TYPE\",\n            \"ext\": \".EXT\",\n            \"project\": {\n                \"name\": \"PROJECT_NAME\",\n                \"code\": \"PROJECT_CODE\"\n            },\n            \"asset\": \"ASSET_NAME\",\n            \"subset\": \"SUBSET_NAME\",\n            \"version\": \"VERSION_NAME\",\n            \"hierarchy\": \"HIERARCHY\"\n        }\n        tmp_filled = anatomy.format_all(example_template_data)\n        thumbnail_result = tmp_filled[\"publish\"][\"thumbnail\"]\n        if not thumbnail_result.solved:\n            missing_keys = thumbnail_result.missing_keys\n            invalid_types = thumbnail_result.invalid_types\n            submsg = \"\"\n            if missing_keys:\n                submsg += \"Missing keys: {}\".format(\", \".join(\n                    [\"\\\"{}\\\"\".format(key) for key in missing_keys]\n                ))\n\n            if invalid_types:\n                items = []\n                for key, value in invalid_types.items():\n                    items.append(\"{}{}\".format(str(key), str(value)))\n                submsg += \"Invalid types: {}\".format(\", \".join(items))\n\n            msg = (\n                \"Thumbnail Anatomy template expects more keys than action\"\n                \" can offer. {}\"\n            ).format(submsg)\n\n            action_job[\"status\"] = \"failed\"\n            session.commit()\n\n            self.log.warning(msg)\n\n            return {\n                \"success\": False,\n                \"message\": msg\n            }\n\n        thumbnail_template = anatomy.templates[\"publish\"][\"thumbnail\"]\n\n        self.db_con.install()\n\n        for entity in entities:\n            # Skip if entity is not AssetVersion (should never happen, but..)\n            if entity.entity_type.lower() != \"assetversion\":\n                continue\n\n            # Skip if AssetVersion don't have thumbnail\n            thumbnail_ent = entity[\"thumbnail\"]\n            if thumbnail_ent is None:\n                self.log.debug((\n                    \"Skipping. AssetVersion don't \"\n                    \"have set thumbnail. {}\"\n                ).format(entity[\"id\"]))\n                continue\n\n            avalon_ents_result = self.get_avalon_entities_for_assetversion(\n                entity, self.db_con\n            )\n            version_full_path = (\n                \"Asset: \\\"{project_name}/{asset_path}\\\"\"\n                \" | Subset: \\\"{subset_name}\\\"\"\n                \" | Version: \\\"{version_name}\\\"\"\n            ).format(**avalon_ents_result)\n\n            version = avalon_ents_result[\"version\"]\n            if not version:\n                self.log.warning((\n                    \"AssetVersion does not have version in avalon. {}\"\n                ).format(version_full_path))\n                continue\n\n            thumbnail_id = version[\"data\"].get(\"thumbnail_id\")\n            if thumbnail_id:\n                self.log.info((\n                    \"AssetVersion skipped, already has thubmanil set. {}\"\n                ).format(version_full_path))\n                continue\n\n            # Get thumbnail extension\n            file_ext = thumbnail_ent[\"file_type\"]\n            if not file_ext.startswith(\".\"):\n                file_ext = \".{}\".format(file_ext)\n\n            avalon_project = avalon_ents_result[\"project\"]\n            avalon_asset = avalon_ents_result[\"asset\"]\n            hierarchy = \"\"\n            parents = avalon_asset[\"data\"].get(\"parents\") or []\n            if parents:\n                hierarchy = \"/\".join(parents)\n\n            # Prepare anatomy template fill data\n            # 1. Create new id for thumbnail entity\n            thumbnail_id = ObjectId()\n\n            template_data = {\n                \"_id\": str(thumbnail_id),\n                \"thumbnail_root\": existing_thumbnail_root,\n                \"thumbnail_type\": \"thumbnail\",\n                \"ext\": file_ext,\n                \"project\": {\n                    \"name\": avalon_project[\"name\"],\n                    \"code\": avalon_project[\"data\"].get(\"code\")\n                },\n                \"asset\": avalon_ents_result[\"asset_name\"],\n                \"subset\": avalon_ents_result[\"subset_name\"],\n                \"version\": avalon_ents_result[\"version_name\"],\n                \"hierarchy\": hierarchy\n            }\n\n            anatomy_filled = anatomy.format(template_data)\n            thumbnail_path = anatomy_filled[\"publish\"][\"thumbnail\"]\n            thumbnail_path = thumbnail_path.replace(\"..\", \".\")\n            thumbnail_path = os.path.normpath(thumbnail_path)\n\n            downloaded = False\n            for loc in (thumbnail_ent.get(\"component_locations\") or []):\n                res_id = loc.get(\"resource_identifier\")\n                if not res_id:\n                    continue\n\n                thubmnail_url = self.get_thumbnail_url(res_id)\n                if self.download_file(thubmnail_url, thumbnail_path):\n                    downloaded = True\n                    break\n\n            if not downloaded:\n                self.log.warning(\n                    \"Could not download thumbnail for {}\".format(\n                        version_full_path\n                    )\n                )\n                continue\n\n            # Clean template data from keys that are dynamic\n            template_data.pop(\"_id\")\n            template_data.pop(\"thumbnail_root\")\n\n            thumbnail_entity = {\n                \"_id\": thumbnail_id,\n                \"type\": \"thumbnail\",\n                \"schema\": \"openpype:thumbnail-1.0\",\n                \"data\": {\n                    \"template\": thumbnail_template,\n                    \"template_data\": template_data\n                }\n            }\n\n            # Create thumbnail entity\n            self.db_con.insert_one(thumbnail_entity)\n            self.log.debug(\n                \"Creating entity in database {}\".format(str(thumbnail_entity))\n            )\n\n            # Set thumbnail id for version\n            self.db_con.update_one(\n                {\"_id\": version[\"_id\"]},\n                {\"$set\": {\"data.thumbnail_id\": thumbnail_id}}\n            )\n\n            self.db_con.update_one(\n                {\"_id\": avalon_asset[\"_id\"]},\n                {\"$set\": {\"data.thumbnail_id\": thumbnail_id}}\n            )\n\n        action_job[\"status\"] = \"done\"\n        session.commit()\n\n        return True\n\n    def get_thumbnail_url(self, resource_identifier, size=None):\n        # TODO use ftrack_api method rather (find way how to use it)\n        url_string = (\n            u'{url}/component/thumbnail?id={id}&username={username}'\n            u'&apiKey={apiKey}'\n        )\n        url = url_string.format(\n            url=self.session.server_url,\n            id=resource_identifier,\n            username=self.session.api_user,\n            apiKey=self.session.api_key\n        )\n        if size:\n            url += u'&size={0}'.format(size)\n\n        return url\n\n    def download_file(self, source_url, dst_file_path):\n        dir_path = os.path.dirname(dst_file_path)\n        try:\n            os.makedirs(dir_path)\n        except OSError as exc:\n            if exc.errno != errno.EEXIST:\n                self.log.warning(\n                    \"Could not create folder: \\\"{}\\\"\".format(dir_path)\n                )\n                return False\n\n        self.log.debug(\n            \"Downloading file \\\"{}\\\" -> \\\"{}\\\"\".format(\n                source_url, dst_file_path\n            )\n        )\n        file_open = open(dst_file_path, \"wb\")\n        try:\n            file_open.write(requests.get(source_url).content)\n        except Exception:\n            self.log.warning(\n                \"Download of image `{}` failed.\".format(source_url)\n            )\n            return False\n        finally:\n            file_open.close()\n        return True\n\n    def get_avalon_entities_for_assetversion(self, asset_version, db_con):\n        output = {\n            \"success\": True,\n            \"message\": None,\n            \"project\": None,\n            \"project_name\": None,\n            \"asset\": None,\n            \"asset_name\": None,\n            \"asset_path\": None,\n            \"subset\": None,\n            \"subset_name\": None,\n            \"version\": None,\n            \"version_name\": None,\n            \"representations\": None\n        }\n\n        db_con.install()\n\n        ft_asset = asset_version[\"asset\"]\n        subset_name = ft_asset[\"name\"]\n        version = asset_version[\"version\"]\n        parent = ft_asset[\"parent\"]\n        ent_path = \"/\".join(\n            [ent[\"name\"] for ent in parent[\"link\"]]\n        )\n        project = self.get_project_from_entity(asset_version)\n        project_name = project[\"full_name\"]\n\n        output[\"project_name\"] = project_name\n        output[\"asset_name\"] = parent[\"name\"]\n        output[\"asset_path\"] = ent_path\n        output[\"subset_name\"] = subset_name\n        output[\"version_name\"] = version\n\n        db_con.Session[\"AVALON_PROJECT\"] = project_name\n\n        avalon_project = get_project(project_name)\n        output[\"project\"] = avalon_project\n\n        if not avalon_project:\n            output[\"success\"] = False\n            output[\"message\"] = (\n                \"Project not synchronized to avalon `{}`\".format(project_name)\n            )\n            return output\n\n        asset_ent = None\n        asset_mongo_id = parent[\"custom_attributes\"].get(CUST_ATTR_ID_KEY)\n        if asset_mongo_id:\n            try:\n                asset_ent = get_asset_by_id(project_name, asset_mongo_id)\n            except Exception:\n                pass\n\n        if not asset_ent:\n            asset_docs = get_assets(project_name, asset_names=[parent[\"name\"]])\n            for asset_doc in asset_docs:\n                ftrack_id = asset_doc.get(\"data\", {}).get(\"ftrackId\")\n                if ftrack_id == parent[\"id\"]:\n                    asset_ent = asset_doc\n                    break\n\n        output[\"asset\"] = asset_ent\n\n        if not asset_ent:\n            output[\"success\"] = False\n            output[\"message\"] = (\n                \"Not synchronized entity to avalon `{}`\".format(ent_path)\n            )\n            return output\n\n        subset_ent = get_subset_by_name(\n            project_name,\n            subset_name=subset_name,\n            asset_id=asset_ent[\"_id\"]\n        )\n\n        output[\"subset\"] = subset_ent\n\n        if not subset_ent:\n            output[\"success\"] = False\n            output[\"message\"] = (\n                \"Subset `{}` does not exist under Asset `{}`\"\n            ).format(subset_name, ent_path)\n            return output\n\n        version_ent = get_version_by_name(\n            project_name,\n            version,\n            subset_ent[\"_id\"]\n        )\n\n        output[\"version\"] = version_ent\n\n        if not version_ent:\n            output[\"success\"] = False\n            output[\"message\"] = (\n                \"Version `{}` does not exist under Subset `{}` | Asset `{}`\"\n            ).format(version, subset_name, ent_path)\n            return output\n\n        repre_ents = list(get_representations(\n            project_name,\n            version_ids=[version_ent[\"_id\"]]\n        ))\n\n        output[\"representations\"] = repre_ents\n        return output\n\n\ndef register(session):\n    StoreThumbnailsToAvalon(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_sync_to_avalon.py",
    "content": "import time\nimport sys\nimport json\n\nimport ftrack_api\n\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\nfrom openpype_modules.ftrack.lib.avalon_sync import SyncEntitiesFactory\n\n\nclass SyncToAvalonLocal(BaseAction):\n    \"\"\"\n    Synchronizing data action - from Ftrack to Avalon DB\n\n    Stores all information about entity.\n    - Name(string) - Most important information = identifier of entity\n    - Parent(ObjectId) - Avalon Project Id, if entity is not project itself\n    - Data(dictionary):\n        - VisualParent(ObjectId) - Avalon Id of parent asset\n        - Parents(array of string) - All parent names except project\n        - Tasks(array of string) - Tasks on asset\n        - FtrackId(string)\n        - entityType(string) - entity's type on Ftrack\n        * All Custom attributes in group 'Avalon'\n            - custom attributes that start with 'avalon_' are skipped\n\n    * These information are stored for entities in whole project.\n\n    Avalon ID of asset is stored to Ftrack\n        - Custom attribute 'avalon_mongo_id'.\n    - action IS NOT creating this Custom attribute if doesn't exist\n        - run 'Create Custom Attributes' action\n        - or do it manually (Not recommended)\n    \"\"\"\n\n    identifier = \"sync.to.avalon.local\"\n    label = \"OpenPype Admin\"\n    variant = \"- Sync To Avalon (Local)\"\n    priority = 200\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"OpenPypeAdmin.svg\")\n\n    settings_key = \"sync_to_avalon_local\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.entities_factory = SyncEntitiesFactory(self.log, self.session)\n\n    def discover(self, session, entities, event):\n        \"\"\" Validate selection. \"\"\"\n        is_valid = False\n        for ent in event[\"data\"][\"selection\"]:\n            # Ignore entities that are not tasks or projects\n            if ent[\"entityType\"].lower() in [\"show\", \"task\"]:\n                is_valid = True\n                break\n\n        if is_valid:\n            is_valid = self.valid_roles(session, entities, event)\n        return is_valid\n\n    def launch(self, session, in_entities, event):\n        self.log.debug(\"{}: Creating job\".format(self.label))\n\n        user_entity = session.query(\n            \"User where id is {}\".format(event[\"source\"][\"user\"][\"id\"])\n        ).one()\n        job_entity = session.create(\"Job\", {\n            \"user\": user_entity,\n            \"status\": \"running\",\n            \"data\": json.dumps({\n                \"description\": \"Sync to avalon is running...\"\n            })\n        })\n        session.commit()\n\n        project_entity = self.get_project_from_entity(in_entities[0])\n        project_name = project_entity[\"full_name\"]\n\n        try:\n            result = self.synchronization(event, project_name)\n\n        except Exception:\n            self.log.error(\n                \"Synchronization failed due to code error\", exc_info=True\n            )\n\n            description = \"Sync to avalon Crashed (Download traceback)\"\n            self.add_traceback_to_job(\n                job_entity, session, sys.exc_info(), description\n            )\n\n            msg = \"An error has happened during synchronization\"\n            title = \"Synchronization report ({}):\".format(project_name)\n            items = []\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"# {}\".format(msg)\n            })\n            items.append({\n                \"type\": \"label\",\n                \"value\": (\n                    \"<p>Download report from job for more information.</p>\"\n                )\n            })\n\n            report = {}\n            try:\n                report = self.entities_factory.report()\n            except Exception:\n                pass\n\n            _items = report.get(\"items\") or []\n            if _items:\n                items.append(self.entities_factory.report_splitter)\n                items.extend(_items)\n\n            self.show_interface(items, title, event, submit_btn_label=\"Ok\")\n\n            return {\"success\": True, \"message\": msg}\n\n        job_entity[\"status\"] = \"done\"\n        job_entity[\"data\"] = json.dumps({\n            \"description\": \"Sync to avalon finished.\"\n        })\n        session.commit()\n\n        return result\n\n    def synchronization(self, event, project_name):\n        time_start = time.time()\n\n        self.show_message(event, \"Synchronization - Preparing data\", True)\n\n        try:\n            output = self.entities_factory.launch_setup(project_name)\n            if output is not None:\n                return output\n\n            time_1 = time.time()\n\n            self.entities_factory.set_cutom_attributes()\n            time_2 = time.time()\n\n            # This must happen before all filtering!!!\n            self.entities_factory.prepare_avalon_entities(project_name)\n            time_3 = time.time()\n\n            self.entities_factory.filter_by_ignore_sync()\n            time_4 = time.time()\n\n            self.entities_factory.duplicity_regex_check()\n            time_5 = time.time()\n\n            self.entities_factory.prepare_ftrack_ent_data()\n            time_6 = time.time()\n\n            self.entities_factory.synchronize()\n            time_7 = time.time()\n\n            self.log.debug(\n                \"*** Synchronization finished ***\"\n            )\n            self.log.debug(\n                \"preparation <{}>\".format(time_1 - time_start)\n            )\n            self.log.debug(\n                \"set_cutom_attributes <{}>\".format(time_2 - time_1)\n            )\n            self.log.debug(\n                \"prepare_avalon_entities <{}>\".format(time_3 - time_2)\n            )\n            self.log.debug(\n                \"filter_by_ignore_sync <{}>\".format(time_4 - time_3)\n            )\n            self.log.debug(\n                \"duplicity_regex_check <{}>\".format(time_5 - time_4)\n            )\n            self.log.debug(\n                \"prepare_ftrack_ent_data <{}>\".format(time_6 - time_5)\n            )\n            self.log.debug(\n                \"synchronize <{}>\".format(time_7 - time_6)\n            )\n            self.log.debug(\n                \"* Total time: {}\".format(time_7 - time_start)\n            )\n\n            if self.entities_factory.project_created:\n                event = ftrack_api.event.base.Event(\n                    topic=\"openpype.project.created\",\n                    data={\"project_name\": project_name}\n                )\n                self.session.event_hub.publish(event)\n\n            report = self.entities_factory.report()\n            if report and report.get(\"items\"):\n                default_title = \"Synchronization report ({}):\".format(\n                    project_name\n                )\n                self.show_interface(\n                    items=report[\"items\"],\n                    title=report.get(\"title\", default_title),\n                    event=event\n                )\n            return {\n                \"success\": True,\n                \"message\": \"Synchronization Finished\"\n            }\n\n        finally:\n            try:\n                self.entities_factory.dbcon.uninstall()\n            except Exception:\n                pass\n\n            try:\n                self.entities_factory.session.close()\n            except Exception:\n                pass\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    SyncToAvalonLocal(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_test.py",
    "content": "from openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass TestAction(BaseAction):\n    \"\"\"Action for testing purpose or as base for new actions.\"\"\"\n\n    ignore_me = True\n\n    identifier = 'test.action'\n    label = 'Test action'\n    description = 'Test action'\n    priority = 10000\n    role_list = ['Pypeclub']\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"TestAction.svg\")\n\n    def discover(self, session, entities, event):\n        return True\n\n    def launch(self, session, entities, event):\n        self.log.info(event)\n\n        return True\n\n\ndef register(session):\n    TestAction(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_thumbnail_to_childern.py",
    "content": "import json\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass ThumbToChildren(BaseAction):\n    '''Custom action.'''\n\n    # Action identifier\n    identifier = 'thumb.to.children'\n    # Action label\n    label = 'Thumbnail'\n    # Action variant\n    variant = \" to Children\"\n    # Action icon\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"Thumbnail.svg\")\n\n    def discover(self, session, entities, event):\n        \"\"\"Show only on project.\"\"\"\n        if (len(entities) != 1 or entities[0].entity_type in [\"Project\"]):\n            return False\n        return True\n\n    def launch(self, session, entities, event):\n        '''Callback method for action.'''\n\n        userId = event['source']['user']['id']\n        user = session.query('User where id is ' + userId).one()\n\n        job = session.create('Job', {\n            'user': user,\n            'status': 'running',\n            'data': json.dumps({\n                'description': 'Push thumbnails to Childrens'\n            })\n        })\n        session.commit()\n        try:\n            for entity in entities:\n                thumbid = entity['thumbnail_id']\n                if thumbid:\n                    for child in entity['children']:\n                        child['thumbnail_id'] = thumbid\n\n            # inform the user that the job is done\n            job['status'] = 'done'\n        except Exception as exc:\n            session.rollback()\n            # fail the job if something goes wrong\n            job['status'] = 'failed'\n            raise exc\n        finally:\n            session.commit()\n\n        return {\n            'success': True,\n            'message': 'Created job for updating thumbnails!'\n        }\n\n\ndef register(session):\n    '''Register action. Called when used as an event plugin.'''\n\n    ThumbToChildren(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_thumbnail_to_parent.py",
    "content": "import json\nfrom openpype_modules.ftrack.lib import BaseAction, statics_icon\n\n\nclass ThumbToParent(BaseAction):\n    '''Custom action.'''\n\n    # Action identifier\n    identifier = 'thumb.to.parent'\n    # Action label\n    label = 'Thumbnail'\n    # Action variant\n    variant = \" to Parent\"\n    # Action icon\n    icon = statics_icon(\"ftrack\", \"action_icons\", \"Thumbnail.svg\")\n\n    def discover(self, session, entities, event):\n        '''Return action config if triggered on asset versions.'''\n\n        if len(entities) <= 0 or entities[0].entity_type in ['Project']:\n            return False\n\n        return True\n\n    def launch(self, session, entities, event):\n        '''Callback method for action.'''\n\n        userId = event['source']['user']['id']\n        user = session.query('User where id is ' + userId).one()\n\n        job = session.create('Job', {\n            'user': user,\n            'status': 'running',\n            'data': json.dumps({\n                'description': 'Push thumbnails to parents'\n            })\n        })\n        session.commit()\n        try:\n            for entity in entities:\n                parent = None\n                thumbid = None\n                if entity.entity_type.lower() == 'assetversion':\n                    parent = entity['task']\n\n                    if parent is None:\n                        par_ent = entity['link'][-2]\n                        parent = session.get(par_ent['type'], par_ent['id'])\n                else:\n                    try:\n                        parent = entity['parent']\n                    except Exception as e:\n                        msg = (\n                            \"During Action 'Thumb to Parent'\"\n                            \" went something wrong\"\n                        )\n                        self.log.error(msg)\n                        raise e\n                thumbid = entity['thumbnail_id']\n\n                if parent and thumbid:\n                    parent['thumbnail_id'] = thumbid\n                    status = 'done'\n                else:\n                    raise Exception(\n                        \"Parent or thumbnail id not found. Parent: {}. \"\n                        \"Thumbnail id: {}\".format(parent, thumbid)\n                    )\n\n            # inform the user that the job is done\n            job['status'] = status or 'done'\n\n        except Exception as exc:\n            session.rollback()\n            # fail the job if something goes wrong\n            job['status'] = 'failed'\n            raise exc\n\n        finally:\n            session.commit()\n\n        return {\n            'success': True,\n            'message': 'Created job for updating thumbnails!'\n        }\n\n\ndef register(session):\n    '''Register action. Called when used as an event plugin.'''\n\n    ThumbToParent(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/event_handlers_user/action_where_run_ask.py",
    "content": "import platform\nimport socket\nimport getpass\n\nfrom openpype_modules.ftrack.lib import BaseAction\nfrom openpype_modules.ftrack.ftrack_server.lib import get_host_ip\n\n\nclass ActionWhereIRun(BaseAction):\n    \"\"\"Show where same user has running OpenPype instances.\"\"\"\n\n    identifier = \"ask.where.i.run\"\n    show_identifier = \"show.where.i.run\"\n    label = \"OpenPype Admin\"\n    variant = \"- Where I run\"\n    description = \"Show PC info where user have running OpenPype\"\n\n    def _discover(self, _event):\n        return {\n            \"items\": [{\n                \"label\": self.label,\n                \"variant\": self.variant,\n                \"description\": self.description,\n                \"actionIdentifier\": self.discover_identifier,\n                \"icon\": self.icon,\n            }]\n        }\n\n    def _launch(self, event):\n        self.trigger_action(self.show_identifier, event)\n\n    def register(self):\n        # Register default action callbacks\n        super(ActionWhereIRun, self).register()\n\n        # Add show identifier\n        show_subscription = (\n            \"topic=ftrack.action.launch\"\n            \" and data.actionIdentifier={}\"\n            \" and source.user.username={}\"\n        ).format(\n            self.show_identifier,\n            self.session.api_user\n        )\n        self.session.event_hub.subscribe(\n            show_subscription,\n            self._show_info\n        )\n\n    def _show_info(self, event):\n        title = \"Where Do I Run?\"\n        msgs = {}\n        all_keys = [\"Hostname\", \"IP\", \"Username\", \"System name\", \"PC name\"]\n        try:\n            host_name = socket.gethostname()\n            msgs[\"Hostname\"] = host_name\n            msgs[\"IP\"] = get_host_ip() or \"N/A\"\n        except Exception:\n            pass\n\n        try:\n            system_name, pc_name, *_ = platform.uname()\n            msgs[\"System name\"] = system_name\n            msgs[\"PC name\"] = pc_name\n        except Exception:\n            pass\n\n        try:\n            msgs[\"Username\"] = getpass.getuser()\n        except Exception:\n            pass\n\n        for key in all_keys:\n            if not msgs.get(key):\n                msgs[key] = \"-Undefined-\"\n\n        items = []\n        first = True\n        separator = {\"type\": \"label\", \"value\": \"---\"}\n        for key, value in msgs.items():\n            if first:\n                first = False\n            else:\n                items.append(separator)\n            self.log.debug(\"{}: {}\".format(key, value))\n\n            subtitle = {\"type\": \"label\", \"value\": \"<h3>{}</h3>\".format(key)}\n            items.append(subtitle)\n            message = {\"type\": \"label\", \"value\": \"<p>{}</p>\".format(value)}\n            items.append(message)\n\n        self.show_interface(items, title, event=event)\n\n\ndef register(session):\n    '''Register plugin. Called when used as an plugin.'''\n\n    ActionWhereIRun(session).register()\n"
  },
  {
    "path": "openpype/modules/ftrack/ftrack_module.py",
    "content": "import os\nimport json\nimport collections\nimport platform\n\nfrom openpype.modules import (\n    click_wrap,\n    OpenPypeModule,\n    ITrayModule,\n    IPluginPaths,\n    ISettingsChangeListener\n)\nfrom openpype.settings import SaveWarningExc\nfrom openpype.lib import Logger\n\nFTRACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))\n_URL_NOT_SET = object()\n\n\nclass FtrackModule(\n    OpenPypeModule,\n    ITrayModule,\n    IPluginPaths,\n    ISettingsChangeListener\n):\n    name = \"ftrack\"\n\n    def initialize(self, settings):\n        ftrack_settings = settings[self.name]\n\n        self.enabled = ftrack_settings[\"enabled\"]\n        self._settings_ftrack_url = ftrack_settings[\"ftrack_server\"]\n        self._ftrack_url = _URL_NOT_SET\n\n        current_dir = os.path.dirname(os.path.abspath(__file__))\n        low_platform = platform.system().lower()\n\n        # Server event handler paths\n        server_event_handlers_paths = [\n            os.path.join(current_dir, \"event_handlers_server\")\n        ]\n        settings_server_paths = ftrack_settings[\"ftrack_events_path\"]\n        if isinstance(settings_server_paths, dict):\n            settings_server_paths = settings_server_paths[low_platform]\n        server_event_handlers_paths.extend(settings_server_paths)\n\n        # User event handler paths\n        user_event_handlers_paths = [\n            os.path.join(current_dir, \"event_handlers_user\")\n        ]\n        settings_action_paths = ftrack_settings[\"ftrack_actions_path\"]\n        if isinstance(settings_action_paths, dict):\n            settings_action_paths = settings_action_paths[low_platform]\n        user_event_handlers_paths.extend(settings_action_paths)\n\n        # Prepare attribute\n        self.server_event_handlers_paths = server_event_handlers_paths\n        self.user_event_handlers_paths = user_event_handlers_paths\n        self.tray_module = None\n\n        # TimersManager connection\n        self.timers_manager_connector = None\n        self._timers_manager_module = None\n\n    def get_ftrack_url(self):\n        \"\"\"Resolved ftrack url.\n\n        Resolving is trying to fill missing information in url and tried to\n        connect to the server.\n\n        Returns:\n            Union[str, None]: Final variant of url or None if url could not be\n                reached.\n        \"\"\"\n\n        if self._ftrack_url is _URL_NOT_SET:\n            self._ftrack_url = resolve_ftrack_url(\n                self._settings_ftrack_url,\n                logger=self.log\n            )\n        return self._ftrack_url\n\n    ftrack_url = property(get_ftrack_url)\n\n    @property\n    def settings_ftrack_url(self):\n        \"\"\"Ftrack url from settings in a format as it is.\n\n        Returns:\n            str: Ftrack url from settings.\n        \"\"\"\n\n        return self._settings_ftrack_url\n\n    def get_global_environments(self):\n        \"\"\"Ftrack's global environments.\"\"\"\n\n        return {\n            \"FTRACK_SERVER\": self.ftrack_url\n        }\n\n    def get_plugin_paths(self):\n        \"\"\"Ftrack plugin paths.\"\"\"\n        return {\n            \"publish\": [os.path.join(FTRACK_MODULE_DIR, \"plugins\", \"publish\")]\n        }\n\n    def get_launch_hook_paths(self):\n        \"\"\"Implementation for applications launch hooks.\"\"\"\n\n        return os.path.join(FTRACK_MODULE_DIR, \"launch_hooks\")\n\n    def modify_application_launch_arguments(self, application, env):\n        if not application.use_python_2:\n            return\n\n        self.log.info(\"Adding Ftrack Python 2 packages to PYTHONPATH.\")\n\n        # Prepare vendor dir path\n        python_2_vendor = os.path.join(FTRACK_MODULE_DIR, \"python2_vendor\")\n\n        # Add Python 2 modules\n        python_paths = [\n            # `python-ftrack-api`\n            os.path.join(python_2_vendor, \"ftrack-python-api\", \"source\")\n        ]\n\n        # Load PYTHONPATH from current launch context\n        python_path = env.get(\"PYTHONPATH\")\n        if python_path:\n            python_paths.append(python_path)\n\n        # Set new PYTHONPATH to launch context environments\n        env[\"PYTHONPATH\"] = os.pathsep.join(python_paths)\n\n    def connect_with_modules(self, enabled_modules):\n        for module in enabled_modules:\n            if not hasattr(module, \"get_ftrack_event_handler_paths\"):\n                continue\n\n            try:\n                paths_by_type = module.get_ftrack_event_handler_paths()\n            except Exception:\n                continue\n\n            if not isinstance(paths_by_type, dict):\n                continue\n\n            for key, value in paths_by_type.items():\n                if not value:\n                    continue\n\n                if key not in (\"server\", \"user\"):\n                    self.log.warning(\n                        \"Unknown event handlers key \\\"{}\\\" skipping.\".format(\n                            key\n                        )\n                    )\n                    continue\n\n                if not isinstance(value, (list, tuple, set)):\n                    value = [value]\n\n                if key == \"server\":\n                    self.server_event_handlers_paths.extend(value)\n                elif key == \"user\":\n                    self.user_event_handlers_paths.extend(value)\n\n    def on_system_settings_save(\n        self, old_value, new_value, changes, new_value_metadata\n    ):\n        \"\"\"Implementation of ISettingsChangeListener interface.\"\"\"\n        if not self.ftrack_url:\n            raise SaveWarningExc((\n                \"Ftrack URL is not set.\"\n                \" Can't propagate changes to Ftrack server.\"\n            ))\n\n        ftrack_changes = changes.get(\"modules\", {}).get(\"ftrack\", {})\n        url_change_msg = None\n        if \"ftrack_server\" in ftrack_changes:\n            url_change_msg = (\n                \"Ftrack URL was changed.\"\n                \" This change may need to restart OpenPype to take affect.\"\n            )\n\n        try:\n            session = self.create_ftrack_session()\n        except Exception:\n            self.log.warning(\"Couldn't create ftrack session.\", exc_info=True)\n\n            if url_change_msg:\n                raise SaveWarningExc(url_change_msg)\n\n            raise SaveWarningExc((\n                \"Saving of attributes to ftrack wasn't successful,\"\n                \" try running Create/Update Avalon Attributes in ftrack.\"\n            ))\n\n        from .lib import (\n            get_openpype_attr,\n            CUST_ATTR_APPLICATIONS,\n            CUST_ATTR_TOOLS,\n            app_definitions_from_app_manager,\n            tool_definitions_from_app_manager\n        )\n        from openpype.lib import ApplicationManager\n        query_keys = [\n            \"id\",\n            \"key\",\n            \"config\"\n        ]\n        custom_attributes = get_openpype_attr(\n            session,\n            split_hierarchical=False,\n            query_keys=query_keys\n        )\n        app_attribute = None\n        tool_attribute = None\n        for custom_attribute in custom_attributes:\n            key = custom_attribute[\"key\"]\n            if key == CUST_ATTR_APPLICATIONS:\n                app_attribute = custom_attribute\n            elif key == CUST_ATTR_TOOLS:\n                tool_attribute = custom_attribute\n\n        app_manager = ApplicationManager(new_value_metadata)\n        missing_attributes = []\n        if not app_attribute:\n            missing_attributes.append(CUST_ATTR_APPLICATIONS)\n        else:\n            config = json.loads(app_attribute[\"config\"])\n            new_data = app_definitions_from_app_manager(app_manager)\n            prepared_data = []\n            for item in new_data:\n                for key, label in item.items():\n                    prepared_data.append({\n                        \"menu\": label,\n                        \"value\": key\n                    })\n\n            config[\"data\"] = json.dumps(prepared_data)\n            app_attribute[\"config\"] = json.dumps(config)\n\n        if not tool_attribute:\n            missing_attributes.append(CUST_ATTR_TOOLS)\n        else:\n            config = json.loads(tool_attribute[\"config\"])\n            new_data = tool_definitions_from_app_manager(app_manager)\n            prepared_data = []\n            for item in new_data:\n                for key, label in item.items():\n                    prepared_data.append({\n                        \"menu\": label,\n                        \"value\": key\n                    })\n            config[\"data\"] = json.dumps(prepared_data)\n            tool_attribute[\"config\"] = json.dumps(config)\n\n        session.commit()\n\n        if missing_attributes:\n            raise SaveWarningExc((\n                \"Couldn't find custom attribute/s ({}) to update.\"\n                \" Try running Create/Update Avalon Attributes in ftrack.\"\n            ).format(\", \".join(missing_attributes)))\n\n        if url_change_msg:\n            raise SaveWarningExc(url_change_msg)\n\n    def on_project_settings_save(self, *_args, **_kwargs):\n        \"\"\"Implementation of ISettingsChangeListener interface.\"\"\"\n        # Ignore\n        return\n\n    def on_project_anatomy_save(\n        self, old_value, new_value, changes, project_name, new_value_metadata\n    ):\n        \"\"\"Implementation of ISettingsChangeListener interface.\"\"\"\n        if not project_name:\n            return\n\n        new_attr_values = new_value.get(\"attributes\")\n        if not new_attr_values:\n            return\n\n        import ftrack_api\n        from openpype_modules.ftrack.lib import (\n            get_openpype_attr,\n            default_custom_attributes_definition,\n            CUST_ATTR_TOOLS,\n            CUST_ATTR_APPLICATIONS,\n            CUST_ATTR_INTENT\n        )\n\n        try:\n            session = self.create_ftrack_session()\n        except Exception:\n            self.log.warning(\"Couldn't create ftrack session.\", exc_info=True)\n            raise SaveWarningExc((\n                \"Saving of attributes to ftrack wasn't successful,\"\n                \" try running Create/Update Avalon Attributes in ftrack.\"\n            ))\n\n        project_entity = session.query(\n            \"Project where full_name is \\\"{}\\\"\".format(project_name)\n        ).first()\n\n        if not project_entity:\n            msg = (\n                \"Ftrack project with name \\\"{}\\\" was not found in Ftrack.\"\n                \" Can't push attribute changes.\"\n            ).format(project_name)\n            self.log.warning(msg)\n            raise SaveWarningExc(msg)\n\n        project_id = project_entity[\"id\"]\n\n        ca_defs = default_custom_attributes_definition()\n        hierarchical_attrs = ca_defs.get(\"is_hierarchical\") or {}\n        project_attrs = ca_defs.get(\"show\") or {}\n        ca_keys = (\n            set(hierarchical_attrs.keys())\n            | set(project_attrs.keys())\n            | {CUST_ATTR_TOOLS, CUST_ATTR_APPLICATIONS, CUST_ATTR_INTENT}\n        )\n\n        cust_attr, hier_attr = get_openpype_attr(session)\n        cust_attr_by_key = {attr[\"key\"]: attr for attr in cust_attr}\n        hier_attrs_by_key = {attr[\"key\"]: attr for attr in hier_attr}\n\n        failed = {}\n        missing = {}\n        for key, value in new_attr_values.items():\n            if key not in ca_keys:\n                continue\n\n            configuration = hier_attrs_by_key.get(key)\n            if not configuration:\n                configuration = cust_attr_by_key.get(key)\n            if not configuration:\n                self.log.warning(\n                    \"Custom attribute \\\"{}\\\" was not found.\".format(key)\n                )\n                missing[key] = value\n                continue\n\n            # TODO add add permissions check\n            # TODO add value validations\n            # - value type and list items\n            entity_key = collections.OrderedDict([\n                (\"configuration_id\", configuration[\"id\"]),\n                (\"entity_id\", project_id)\n            ])\n\n            session.recorded_operations.push(\n                ftrack_api.operation.UpdateEntityOperation(\n                    \"ContextCustomAttributeValue\",\n                    entity_key,\n                    \"value\",\n                    ftrack_api.symbol.NOT_SET,\n                    value\n                )\n            )\n            try:\n                session.commit()\n                self.log.debug(\n                    \"Changed project custom attribute \\\"{}\\\" to \\\"{}\\\"\".format(\n                        key, value\n                    )\n                )\n            except Exception:\n                self.log.warning(\n                    \"Failed to set \\\"{}\\\" to \\\"{}\\\"\".format(key, value),\n                    exc_info=True\n                )\n                session.rollback()\n                failed[key] = value\n\n        if not failed and not missing:\n            return\n\n        error_msg = (\n            \"Values were not updated on Ftrack which may cause issues.\"\n            \" try running Create/Update Avalon Attributes in ftrack \"\n            \" and resave project settings.\"\n        )\n        if missing:\n            error_msg += \"\\nMissing Custom attributes on Ftrack: {}.\".format(\n                \", \".join([\n                    '\"{}\"'.format(key)\n                    for key in missing.keys()\n                ])\n            )\n        if failed:\n            joined_failed = \", \".join([\n                '\"{}\": \"{}\"'.format(key, value)\n                for key, value in failed.items()\n            ])\n            error_msg += \"\\nFailed to set: {}\".format(joined_failed)\n        raise SaveWarningExc(error_msg)\n\n    def create_ftrack_session(self, **session_kwargs):\n        import ftrack_api\n\n        if \"server_url\" not in session_kwargs:\n            session_kwargs[\"server_url\"] = self.ftrack_url\n\n        api_key = session_kwargs.get(\"api_key\")\n        api_user = session_kwargs.get(\"api_user\")\n        # First look into environments\n        # - both OpenPype tray and ftrack event server should have set them\n        # - ftrack event server may crash when credentials are tried to load\n        #   from keyring\n        if not api_key or not api_user:\n            api_key = os.environ.get(\"FTRACK_API_KEY\")\n            api_user = os.environ.get(\"FTRACK_API_USER\")\n\n        if not api_key or not api_user:\n            from .lib import credentials\n            cred = credentials.get_credentials()\n            api_user = cred.get(\"username\")\n            api_key = cred.get(\"api_key\")\n\n        session_kwargs[\"api_user\"] = api_user\n        session_kwargs[\"api_key\"] = api_key\n        return ftrack_api.Session(**session_kwargs)\n\n    def tray_init(self):\n        from .tray import FtrackTrayWrapper\n\n        self.tray_module = FtrackTrayWrapper(self)\n        # Module is it's own connector to TimersManager\n        self.timers_manager_connector = self\n\n    def tray_menu(self, parent_menu):\n        return self.tray_module.tray_menu(parent_menu)\n\n    def tray_start(self):\n        return self.tray_module.validate()\n\n    def tray_exit(self):\n        self.tray_module.tray_exit()\n\n    def set_credentials_to_env(self, username, api_key):\n        os.environ[\"FTRACK_API_USER\"] = username or \"\"\n        os.environ[\"FTRACK_API_KEY\"] = api_key or \"\"\n\n    # --- TimersManager connection methods ---\n    def start_timer(self, data):\n        if self.tray_module:\n            self.tray_module.start_timer_manager(data)\n\n    def stop_timer(self):\n        if self.tray_module:\n            self.tray_module.stop_timer_manager()\n\n    def register_timers_manager(self, timer_manager_module):\n        self._timers_manager_module = timer_manager_module\n\n    def timer_started(self, data):\n        if self._timers_manager_module is not None:\n            self._timers_manager_module.timer_started(self.id, data)\n\n    def timer_stopped(self):\n        if self._timers_manager_module is not None:\n            self._timers_manager_module.timer_stopped(self.id)\n\n    def get_task_time(self, project_name, asset_name, task_name):\n        session = self.create_ftrack_session()\n        query = (\n            'Task where name is \"{}\"'\n            ' and parent.name is \"{}\"'\n            ' and project.full_name is \"{}\"'\n        ).format(task_name, asset_name, project_name)\n        task_entity = session.query(query).first()\n        if not task_entity:\n            return 0\n        hours_logged = (task_entity[\"time_logged\"] / 60) / 60\n        return hours_logged\n\n    def get_credentials(self):\n        # type: () -> tuple\n        \"\"\"Get local Ftrack credentials.\"\"\"\n        from .lib import credentials\n\n        cred = credentials.get_credentials(self.ftrack_url)\n        return cred.get(\"username\"), cred.get(\"api_key\")\n\n    def cli(self, click_group):\n        click_group.add_command(cli_main.to_click_obj())\n\n\ndef _check_ftrack_url(url):\n    import requests\n\n    try:\n        result = requests.get(url, allow_redirects=False)\n    except requests.exceptions.RequestException:\n        return False\n\n    if (result.status_code != 200 or \"FTRACK_VERSION\" not in result.headers):\n        return False\n    return True\n\n\ndef resolve_ftrack_url(url, logger=None):\n    \"\"\"Checks if Ftrack server is responding.\"\"\"\n\n    if logger is None:\n        logger = Logger.get_logger(__name__)\n\n    url = url.strip(\"/ \")\n    if not url:\n        logger.error(\"Ftrack URL is not set!\")\n        return None\n\n    if not url.startswith(\"http\"):\n        url = \"https://\" + url\n\n    ftrack_url = None\n    if url and _check_ftrack_url(url):\n        ftrack_url = url\n\n    if not ftrack_url and not url.endswith(\"ftrackapp.com\"):\n        ftrackapp_url = url + \".ftrackapp.com\"\n        if _check_ftrack_url(ftrackapp_url):\n            ftrack_url = ftrackapp_url\n\n    if not ftrack_url and _check_ftrack_url(url):\n        ftrack_url = url\n\n    if ftrack_url:\n        logger.debug(\"Ftrack server \\\"{}\\\" is accessible.\".format(ftrack_url))\n\n    else:\n        logger.error(\"Ftrack server \\\"{}\\\" is not accessible!\".format(url))\n\n    return ftrack_url\n\n\n@click_wrap.group(FtrackModule.name, help=\"Ftrack module related commands.\")\ndef cli_main():\n    pass\n\n\n@cli_main.command()\n@click_wrap.option(\"-d\", \"--debug\", is_flag=True, help=\"Print debug messages\")\n@click_wrap.option(\"--ftrack-url\", envvar=\"FTRACK_SERVER\",\n              help=\"Ftrack server url\")\n@click_wrap.option(\"--ftrack-user\", envvar=\"FTRACK_API_USER\",\n              help=\"Ftrack api user\")\n@click_wrap.option(\"--ftrack-api-key\", envvar=\"FTRACK_API_KEY\",\n              help=\"Ftrack api key\")\n@click_wrap.option(\"--legacy\", is_flag=True,\n              help=\"run event server without mongo storing\")\n@click_wrap.option(\"--clockify-api-key\", envvar=\"CLOCKIFY_API_KEY\",\n              help=\"Clockify API key.\")\n@click_wrap.option(\"--clockify-workspace\", envvar=\"CLOCKIFY_WORKSPACE\",\n              help=\"Clockify workspace\")\ndef eventserver(\n    debug,\n    ftrack_url,\n    ftrack_user,\n    ftrack_api_key,\n    legacy,\n    clockify_api_key,\n    clockify_workspace\n):\n    \"\"\"Launch ftrack event server.\n\n    This should be ideally used by system service (such us systemd or upstart\n    on linux and window service).\n    \"\"\"\n    if debug:\n        os.environ[\"OPENPYPE_DEBUG\"] = \"3\"\n\n    from .ftrack_server.event_server_cli import run_event_server\n\n    return run_event_server(\n        ftrack_url,\n        ftrack_user,\n        ftrack_api_key,\n        legacy,\n        clockify_api_key,\n        clockify_workspace\n    )\n"
  },
  {
    "path": "openpype/modules/ftrack/ftrack_server/__init__.py",
    "content": "from .ftrack_server import FtrackServer\n\n\n__all__ = (\n    \"FtrackServer\",\n)\n"
  },
  {
    "path": "openpype/modules/ftrack/ftrack_server/event_server_cli.py",
    "content": "import os\nimport signal\nimport datetime\nimport subprocess\nimport socket\nimport json\nimport getpass\nimport atexit\nimport time\nimport uuid\n\nimport ftrack_api\nimport pymongo\nfrom openpype.client.mongo import (\n    OpenPypeMongoConnection,\n    validate_mongo_connection,\n)\nfrom openpype.lib import (\n    get_openpype_execute_args,\n    get_openpype_version,\n    get_build_version,\n)\nfrom openpype_modules.ftrack import (\n    FTRACK_MODULE_DIR,\n    resolve_ftrack_url,\n)\nfrom openpype_modules.ftrack.lib import credentials\nfrom openpype_modules.ftrack.ftrack_server import socket_thread\nfrom openpype_modules.ftrack.ftrack_server.lib import get_host_ip\n\n\nclass MongoPermissionsError(Exception):\n    \"\"\"Is used when is created multiple objects of same RestApi class.\"\"\"\n    def __init__(self, message=None):\n        if not message:\n            message = \"Exiting because have issue with access to MongoDB\"\n        super().__init__(message)\n\n\ndef check_mongo_url(mongo_uri, log_error=False):\n    \"\"\"Checks if mongo server is responding\"\"\"\n    try:\n        validate_mongo_connection(mongo_uri)\n\n    except pymongo.errors.InvalidURI as err:\n        if log_error:\n            print(\"Can't connect to MongoDB at {} because: {}\".format(\n                mongo_uri, err\n            ))\n        return False\n\n    except pymongo.errors.ServerSelectionTimeoutError as err:\n        if log_error:\n            print(\"Can't connect to MongoDB at {} because: {}\".format(\n                mongo_uri, err\n            ))\n        return False\n\n    return True\n\n\ndef validate_credentials(url, user, api):\n    first_validation = True\n    if not user:\n        print('- Ftrack Username is not set')\n        first_validation = False\n    if not api:\n        print('- Ftrack API key is not set')\n        first_validation = False\n    if not first_validation:\n        return False\n\n    try:\n        session = ftrack_api.Session(\n            server_url=url,\n            api_user=user,\n            api_key=api\n        )\n        session.close()\n    except Exception as e:\n        print(\"Can't log into Ftrack with used credentials:\")\n        ftrack_cred = {\n            \"Ftrack server\": str(url),\n            \"Username\": str(user),\n            \"API key\": str(api)\n        }\n        item_lens = [len(key) + 1 for key in ftrack_cred.keys()]\n        justify_len = max(*item_lens)\n        for key, value in ftrack_cred.items():\n            print(\"{} {}\".format(\n                (key + \":\").ljust(justify_len, \" \"),\n                value\n            ))\n        return False\n\n    print('DEBUG: Credentials Username: \"{}\", API key: \"{}\" are valid.'.format(\n        user, api\n    ))\n    return True\n\n\ndef legacy_server(ftrack_url):\n    # Current file\n    scripts_dir = os.path.join(FTRACK_MODULE_DIR, \"scripts\")\n\n    min_fail_seconds = 5\n    max_fail_count = 3\n    wait_time_after_max_fail = 10\n\n    subproc = None\n    subproc_path = \"{}/sub_legacy_server.py\".format(scripts_dir)\n    subproc_last_failed = datetime.datetime.now()\n    subproc_failed_count = 0\n\n    ftrack_accessible = False\n    printed_ftrack_error = False\n\n    while True:\n        if not ftrack_accessible:\n            ftrack_accessible = resolve_ftrack_url(ftrack_url)\n\n        # Run threads only if Ftrack is accessible\n        if not ftrack_accessible and not printed_ftrack_error:\n            print(\"Can't access Ftrack {} <{}>\".format(\n                ftrack_url, str(datetime.datetime.now())\n            ))\n            if subproc is not None:\n                if subproc.poll() is None:\n                    subproc.terminate()\n\n                subproc = None\n\n            printed_ftrack_error = True\n\n            time.sleep(1)\n            continue\n\n        printed_ftrack_error = False\n\n        if subproc is None:\n            if subproc_failed_count < max_fail_count:\n                args = get_openpype_execute_args(\"run\", subproc_path)\n                subproc = subprocess.Popen(\n                    args,\n                    stdout=subprocess.PIPE\n                )\n            elif subproc_failed_count == max_fail_count:\n                print((\n                    \"Storer failed {}times I'll try to run again {}s later\"\n                ).format(str(max_fail_count), str(wait_time_after_max_fail)))\n                subproc_failed_count += 1\n            elif ((\n                datetime.datetime.now() - subproc_last_failed\n            ).seconds > wait_time_after_max_fail):\n                subproc_failed_count = 0\n\n        # If thread failed test Ftrack and Mongo connection\n        elif subproc.poll() is not None:\n            subproc = None\n            ftrack_accessible = False\n\n            _subproc_last_failed = datetime.datetime.now()\n            delta_time = (_subproc_last_failed - subproc_last_failed).seconds\n            if delta_time < min_fail_seconds:\n                subproc_failed_count += 1\n            else:\n                subproc_failed_count = 0\n            subproc_last_failed = _subproc_last_failed\n\n        time.sleep(1)\n\n\ndef main_loop(ftrack_url):\n    \"\"\" This is main loop of event handling.\n\n    Loop is handling threads which handles subprocesses of event storer and\n    processor. When one of threads is stopped it is tested to connect to\n    ftrack and mongo server. Threads are not started when ftrack or mongo\n    server is not accessible. When threads are started it is checked for socket\n    signals as heartbeat. Heartbeat must become at least once per 30sec\n    otherwise thread will be killed.\n    \"\"\"\n\n    os.environ[\"FTRACK_EVENT_SUB_ID\"] = str(uuid.uuid1())\n\n    mongo_uri = OpenPypeMongoConnection.get_default_mongo_url()\n\n    # Current file\n    scripts_dir = os.path.join(FTRACK_MODULE_DIR, \"scripts\")\n\n    min_fail_seconds = 5\n    max_fail_count = 3\n    wait_time_after_max_fail = 10\n\n    # Threads data\n    storer_name = \"StorerThread\"\n    storer_port = 10001\n    storer_path = \"{}/sub_event_storer.py\".format(scripts_dir)\n    storer_thread = None\n    storer_last_failed = datetime.datetime.now()\n    storer_failed_count = 0\n\n    processor_name = \"ProcessorThread\"\n    processor_port = 10011\n    processor_path = \"{}/sub_event_processor.py\".format(scripts_dir)\n    processor_thread = None\n    processor_last_failed = datetime.datetime.now()\n    processor_failed_count = 0\n\n    statuser_name = \"StorerThread\"\n    statuser_port = 10021\n    statuser_path = \"{}/sub_event_status.py\".format(scripts_dir)\n    statuser_thread = None\n    statuser_last_failed = datetime.datetime.now()\n    statuser_failed_count = 0\n\n    ftrack_accessible = False\n    mongo_accessible = False\n\n    printed_ftrack_error = False\n    printed_mongo_error = False\n\n    # stop threads on exit\n    # TODO check if works and args have thread objects!\n    def on_exit(processor_thread, storer_thread, statuser_thread):\n        if processor_thread is not None:\n            processor_thread.stop()\n            processor_thread.join()\n            processor_thread = None\n\n        if storer_thread is not None:\n            storer_thread.stop()\n            storer_thread.join()\n            storer_thread = None\n\n        if statuser_thread is not None:\n            statuser_thread.stop()\n            statuser_thread.join()\n            statuser_thread = None\n\n    atexit.register(\n        on_exit,\n        processor_thread=processor_thread,\n        storer_thread=storer_thread,\n        statuser_thread=statuser_thread\n    )\n\n    host_name = socket.gethostname()\n    host_ip = get_host_ip()\n\n    main_info = [\n        [\"created_at\", datetime.datetime.now().strftime(\"%Y.%m.%d %H:%M:%S\")],\n        [\"Username\", getpass.getuser()],\n        [\"Host Name\", host_name],\n        [\"Host IP\", host_ip or \"N/A\"],\n        [\"OpenPype executable\", get_openpype_execute_args()[-1]],\n        [\"OpenPype version\", get_openpype_version() or \"N/A\"],\n        [\"OpenPype build version\", get_build_version() or \"N/A\"]\n    ]\n    main_info_str = json.dumps(main_info)\n    # Main loop\n    while True:\n        # Check if accessible Ftrack and Mongo url\n        if not ftrack_accessible:\n            ftrack_accessible = resolve_ftrack_url(ftrack_url)\n\n        if not mongo_accessible:\n            mongo_accessible = check_mongo_url(mongo_uri)\n\n        # Run threads only if Ftrack is accessible\n        if not ftrack_accessible or not mongo_accessible:\n            if not mongo_accessible and not printed_mongo_error:\n                print(\"Can't access Mongo {}\".format(mongo_uri))\n\n            if not ftrack_accessible and not printed_ftrack_error:\n                print(\"Can't access Ftrack {}\".format(ftrack_url))\n\n            if storer_thread is not None:\n                storer_thread.stop()\n                storer_thread.join()\n                storer_thread = None\n\n            if processor_thread is not None:\n                processor_thread.stop()\n                processor_thread.join()\n                processor_thread = None\n\n            printed_ftrack_error = True\n            printed_mongo_error = True\n\n            time.sleep(1)\n            continue\n\n        printed_ftrack_error = False\n        printed_mongo_error = False\n\n        # ====== STATUSER =======\n        if statuser_thread is None:\n            if statuser_failed_count < max_fail_count:\n                statuser_thread = socket_thread.StatusSocketThread(\n                    statuser_name, statuser_port, statuser_path,\n                    [main_info_str]\n                )\n                statuser_thread.start()\n\n            elif statuser_failed_count == max_fail_count:\n                print((\n                    \"Statuser failed {}times in row\"\n                    \" I'll try to run again {}s later\"\n                ).format(str(max_fail_count), str(wait_time_after_max_fail)))\n                statuser_failed_count += 1\n\n            elif ((\n                datetime.datetime.now() - statuser_last_failed\n            ).seconds > wait_time_after_max_fail):\n                statuser_failed_count = 0\n\n        # If thread failed test Ftrack and Mongo connection\n        elif not statuser_thread.is_alive():\n            statuser_thread.join()\n            statuser_thread = None\n            ftrack_accessible = False\n            mongo_accessible = False\n\n            _processor_last_failed = datetime.datetime.now()\n            delta_time = (\n                _processor_last_failed - statuser_last_failed\n            ).seconds\n\n            if delta_time < min_fail_seconds:\n                statuser_failed_count += 1\n            else:\n                statuser_failed_count = 0\n            statuser_last_failed = _processor_last_failed\n\n        elif statuser_thread.stop_subprocess:\n            print(\"Main process was stopped by action\")\n            on_exit(processor_thread, storer_thread, statuser_thread)\n            os.kill(os.getpid(), signal.SIGTERM)\n            return 1\n\n        # ====== STORER =======\n        # Run backup thread which does not require mongo to work\n        if storer_thread is None:\n            if storer_failed_count < max_fail_count:\n                storer_thread = socket_thread.SocketThread(\n                    storer_name, storer_port, storer_path\n                )\n                storer_thread.start()\n\n            elif storer_failed_count == max_fail_count:\n                print((\n                    \"Storer failed {}times I'll try to run again {}s later\"\n                ).format(str(max_fail_count), str(wait_time_after_max_fail)))\n                storer_failed_count += 1\n            elif ((\n                datetime.datetime.now() - storer_last_failed\n            ).seconds > wait_time_after_max_fail):\n                storer_failed_count = 0\n\n        # If thread failed test Ftrack and Mongo connection\n        elif not storer_thread.is_alive():\n            if storer_thread.mongo_error:\n                raise MongoPermissionsError()\n            storer_thread.join()\n            storer_thread = None\n            ftrack_accessible = False\n            mongo_accessible = False\n\n            _storer_last_failed = datetime.datetime.now()\n            delta_time = (_storer_last_failed - storer_last_failed).seconds\n            if delta_time < min_fail_seconds:\n                storer_failed_count += 1\n            else:\n                storer_failed_count = 0\n            storer_last_failed = _storer_last_failed\n\n        # ====== PROCESSOR =======\n        if processor_thread is None:\n            if processor_failed_count < max_fail_count:\n                processor_thread = socket_thread.SocketThread(\n                    processor_name, processor_port, processor_path\n                )\n                processor_thread.start()\n\n            elif processor_failed_count == max_fail_count:\n                print((\n                    \"Processor failed {}times in row\"\n                    \" I'll try to run again {}s later\"\n                ).format(str(max_fail_count), str(wait_time_after_max_fail)))\n                processor_failed_count += 1\n\n            elif ((\n                datetime.datetime.now() - processor_last_failed\n            ).seconds > wait_time_after_max_fail):\n                processor_failed_count = 0\n\n        # If thread failed test Ftrack and Mongo connection\n        elif not processor_thread.is_alive():\n            if processor_thread.mongo_error:\n                raise Exception(\n                    \"Exiting because have issue with access to MongoDB\"\n                )\n            processor_thread.join()\n            processor_thread = None\n            ftrack_accessible = False\n            mongo_accessible = False\n\n            _processor_last_failed = datetime.datetime.now()\n            delta_time = (\n                _processor_last_failed - processor_last_failed\n            ).seconds\n\n            if delta_time < min_fail_seconds:\n                processor_failed_count += 1\n            else:\n                processor_failed_count = 0\n            processor_last_failed = _processor_last_failed\n\n        if statuser_thread is not None:\n            statuser_thread.set_process(\"storer\", storer_thread)\n            statuser_thread.set_process(\"processor\", processor_thread)\n\n        time.sleep(1)\n\n\ndef run_event_server(\n    ftrack_url,\n    ftrack_user,\n    ftrack_api_key,\n    legacy,\n    clockify_api_key,\n    clockify_workspace\n):\n    if not ftrack_user or not ftrack_api_key:\n        print((\n            \"Ftrack user/api key were not passed.\"\n            \" Trying to use credentials from user keyring.\"\n        ))\n        cred = credentials.get_credentials(ftrack_url)\n        ftrack_user = cred.get(\"username\")\n        ftrack_api_key = cred.get(\"api_key\")\n\n    if clockify_workspace and clockify_api_key:\n        os.environ[\"CLOCKIFY_WORKSPACE\"] = clockify_workspace\n        os.environ[\"CLOCKIFY_API_KEY\"] = clockify_api_key\n\n    # Check url regex and accessibility\n    ftrack_url = resolve_ftrack_url(ftrack_url)\n    if not ftrack_url:\n        print('Exiting! < Please enter Ftrack server url >')\n        return 1\n\n    # Validate entered credentials\n    if not validate_credentials(ftrack_url, ftrack_user, ftrack_api_key):\n        print('Exiting! < Please enter valid credentials >')\n        return 1\n\n    # Set Ftrack environments\n    os.environ[\"FTRACK_SERVER\"] = ftrack_url\n    os.environ[\"FTRACK_API_USER\"] = ftrack_user\n    os.environ[\"FTRACK_API_KEY\"] = ftrack_api_key\n\n    if legacy:\n        return legacy_server(ftrack_url)\n\n    return main_loop(ftrack_url)\n"
  },
  {
    "path": "openpype/modules/ftrack/ftrack_server/ftrack_server.py",
    "content": "import os\nimport time\nimport types\nimport logging\nimport traceback\n\nimport ftrack_api\n\nfrom openpype.lib import (\n    Logger,\n    modules_from_path\n)\n\n\"\"\"\n# Required - Needed for connection to Ftrack\nFTRACK_SERVER # Ftrack server e.g. \"https://myFtrack.ftrackapp.com\"\nFTRACK_API_KEY # Ftrack user's API key \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\nFTRACK_API_USER # Ftrack username e.g. \"user.name\"\n\n# Required - Paths to folder with actions\nFTRACK_ACTIONS_PATH # Paths to folders where are located actions\n    - EXAMPLE: \"M:/FtrackApi/../actions/\"\nFTRACK_EVENTS_PATH # Paths to folders where are located actions\n    - EXAMPLE: \"M:/FtrackApi/../events/\"\n\n# Required - Needed for import included modules\nPYTHONPATH # Path to ftrack_api and paths to all modules used in actions\n    - path to ftrack_action_handler, etc.\n\"\"\"\n\n\nclass FtrackServer:\n    def __init__(self, handler_paths=None):\n        \"\"\"\n            - 'type' is by default set to 'action' - Runs Action server\n            - enter 'event' for Event server\n\n            EXAMPLE FOR EVENT SERVER:\n                ...\n                server = FtrackServer()\n                server.run_server()\n                ..\n        \"\"\"\n\n        # set Ftrack logging to Warning only - OPTIONAL\n        ftrack_log = logging.getLogger(\"ftrack_api\")\n        ftrack_log.setLevel(logging.WARNING)\n\n        self.log = Logger.get_logger(__name__)\n\n        self.stopped = True\n        self.is_running = False\n\n        self.handler_paths = handler_paths or []\n\n    def stop_session(self):\n        self.stopped = True\n        if self.session.event_hub.connected is True:\n            self.session.event_hub.disconnect()\n        self.session.close()\n        self.session = None\n\n    def set_files(self, paths):\n        # Iterate all paths\n        register_functions = []\n        for path in paths:\n            # Try to format path with environments\n            try:\n                path = path.format(**os.environ)\n            except BaseException:\n                pass\n\n            # Get all modules with functions\n            modules, crashed = modules_from_path(path)\n            for filepath, exc_info in crashed:\n                self.log.warning(\"Filepath load crashed {}.\\n{}\".format(\n                    filepath, traceback.format_exception(*exc_info)\n                ))\n\n            for filepath, module in modules:\n                register_function = None\n                for name, attr in module.__dict__.items():\n                    if (\n                        name == \"register\"\n                        and isinstance(attr, types.FunctionType)\n                    ):\n                        register_function = attr\n                        break\n\n                if not register_function:\n                    self.log.warning(\n                        \"\\\"{}\\\" - Missing register method\".format(filepath)\n                    )\n                    continue\n\n                register_functions.append(\n                    (filepath, register_function)\n                )\n\n        if not register_functions:\n            self.log.warning((\n                \"There are no events with `register` function\"\n                \" in registered paths: \\\"{}\\\"\"\n            ).format(\"| \".join(paths)))\n\n        for filepath, register_func in register_functions:\n            try:\n                register_func(self.session)\n            except Exception:\n                self.log.warning(\n                    \"\\\"{}\\\" - register was not successful\".format(filepath),\n                    exc_info=True\n                )\n\n    def set_handler_paths(self, paths):\n        self.handler_paths = paths\n        if self.is_running:\n            self.stop_session()\n            self.run_server()\n\n        elif not self.stopped:\n            self.run_server()\n\n    def run_server(self, session=None, load_files=True):\n        self.stopped = False\n        self.is_running = True\n        if not session:\n            session = ftrack_api.Session(auto_connect_event_hub=True)\n\n        # Wait until session has connected event hub\n        if session._auto_connect_event_hub_thread:\n            # Use timeout from session (since ftrack-api 2.1.0)\n            timeout = getattr(session, \"request_timeout\", 60)\n            started = time.time()\n            while not session.event_hub.connected:\n                if (time.time() - started) > timeout:\n                    raise RuntimeError((\n                        \"Connection to Ftrack was not created in {} seconds\"\n                    ).format(timeout))\n                time.sleep(0.1)\n\n        self.session = session\n        if load_files:\n            if not self.handler_paths:\n                self.log.warning((\n                    \"Paths to event handlers are not set.\"\n                    \" Ftrack server won't launch.\"\n                ))\n                self.is_running = False\n                return\n\n            self.set_files(self.handler_paths)\n\n            msg = \"Registration of event handlers has finished!\"\n            self.log.info(len(msg) * \"*\")\n            self.log.info(msg)\n\n        # keep event_hub on session running\n        self.session.event_hub.wait()\n        self.is_running = False\n"
  },
  {
    "path": "openpype/modules/ftrack/ftrack_server/lib.py",
    "content": "import os\nimport sys\nimport logging\nimport getpass\nimport atexit\nimport threading\nimport datetime\nimport time\nimport queue\nimport collections\nimport appdirs\nimport socket\n\nimport pymongo\nimport requests\nimport ftrack_api\nimport ftrack_api.session\nimport ftrack_api.cache\nimport ftrack_api.operation\nimport ftrack_api._centralized_storage_scenario\nimport ftrack_api.event\nfrom ftrack_api.logging import LazyLogMessage as L\ntry:\n    from weakref import WeakMethod\nexcept ImportError:\n    from ftrack_api._weakref import WeakMethod\nfrom openpype_modules.ftrack.lib import get_ftrack_event_mongo_info\n\nfrom openpype.client import OpenPypeMongoConnection\nfrom openpype.lib import Logger\n\nTOPIC_STATUS_SERVER = \"openpype.event.server.status\"\nTOPIC_STATUS_SERVER_RESULT = \"openpype.event.server.status.result\"\n\n\ndef get_host_ip():\n    host_name = socket.gethostname()\n    try:\n        return socket.gethostbyname(host_name)\n    except Exception:\n        pass\n\n    return None\n\n\nclass SocketBaseEventHub(ftrack_api.event.hub.EventHub):\n\n    hearbeat_msg = b\"hearbeat\"\n    heartbeat_callbacks = []\n\n    def __init__(self, *args, **kwargs):\n        self.sock = kwargs.pop(\"sock\")\n        super(SocketBaseEventHub, self).__init__(*args, **kwargs)\n\n    def _handle_packet(self, code, packet_identifier, path, data):\n        \"\"\"Override `_handle_packet` which extend heartbeat\"\"\"\n        code_name = self._code_name_mapping[code]\n        if code_name == \"heartbeat\":\n            # Reply with heartbeat.\n            for callback in self.heartbeat_callbacks:\n                callback()\n\n            self.sock.sendall(self.hearbeat_msg)\n            return self._send_packet(self._code_name_mapping[\"heartbeat\"])\n\n        return super(SocketBaseEventHub, self)._handle_packet(\n            code, packet_identifier, path, data\n        )\n\n\nclass StatusEventHub(SocketBaseEventHub):\n    def _handle_packet(self, code, packet_identifier, path, data):\n        \"\"\"Override `_handle_packet` which extend heartbeat\"\"\"\n        code_name = self._code_name_mapping[code]\n        if code_name == \"connect\":\n            event = ftrack_api.event.base.Event(\n                topic=\"openpype.status.started\",\n                data={},\n                source={\n                    \"id\": self.id,\n                    \"user\": {\"username\": self._api_user}\n                }\n            )\n            self._event_queue.put(event)\n\n        return super(StatusEventHub, self)._handle_packet(\n            code, packet_identifier, path, data\n        )\n\n\nclass StorerEventHub(SocketBaseEventHub):\n\n    hearbeat_msg = b\"storer\"\n\n    def _handle_packet(self, code, packet_identifier, path, data):\n        \"\"\"Override `_handle_packet` which extend heartbeat\"\"\"\n        code_name = self._code_name_mapping[code]\n        if code_name == \"connect\":\n            event = ftrack_api.event.base.Event(\n                topic=\"openpype.storer.started\",\n                data={},\n                source={\n                    \"id\": self.id,\n                    \"user\": {\"username\": self._api_user}\n                }\n            )\n            self._event_queue.put(event)\n\n        return super(StorerEventHub, self)._handle_packet(\n            code, packet_identifier, path, data\n        )\n\n\nclass ProcessEventHub(SocketBaseEventHub):\n    hearbeat_msg = b\"processor\"\n\n    is_collection_created = False\n    pypelog = Logger.get_logger(\"Session Processor\")\n\n    def __init__(self, *args, **kwargs):\n        self.mongo_url = None\n        self.dbcon = None\n\n        super(ProcessEventHub, self).__init__(*args, **kwargs)\n\n    def prepare_dbcon(self):\n        try:\n            database_name, collection_name = get_ftrack_event_mongo_info()\n            mongo_client = OpenPypeMongoConnection.get_mongo_client()\n            self.dbcon = mongo_client[database_name][collection_name]\n            self.mongo_client = mongo_client\n\n        except pymongo.errors.AutoReconnect:\n            self.pypelog.error((\n                \"Mongo server \\\"{}\\\" is not responding, exiting.\"\n            ).format(OpenPypeMongoConnection.get_default_mongo_url()))\n            sys.exit(0)\n\n        except pymongo.errors.OperationFailure:\n            self.pypelog.error((\n                \"Error with Mongo access, probably permissions.\"\n                \"Check if exist database with name \\\"{}\\\"\"\n                \" and collection \\\"{}\\\" inside.\"\n            ).format(self.database, self.collection_name))\n            self.sock.sendall(b\"MongoError\")\n            sys.exit(0)\n\n    def wait(self, duration=None):\n        \"\"\"Overridden wait\n        Event are loaded from Mongo DB when queue is empty. Handled event is\n        set as processed in Mongo DB.\n        \"\"\"\n        started = time.time()\n        self.prepare_dbcon()\n        while True:\n            try:\n                event = self._event_queue.get(timeout=0.1)\n            except queue.Empty:\n                if not self.load_events():\n                    time.sleep(0.5)\n            else:\n                try:\n                    self._handle(event)\n\n                    mongo_id = event[\"data\"].get(\"_event_mongo_id\")\n                    if mongo_id is None:\n                        continue\n\n                    self.dbcon.update_one(\n                        {\"_id\": mongo_id},\n                        {\"$set\": {\"pype_data.is_processed\": True}}\n                    )\n\n                except pymongo.errors.AutoReconnect:\n                    self.pypelog.error((\n                        \"Mongo server \\\"{}\\\" is not responding, exiting.\"\n                    ).format(os.environ[\"OPENPYPE_MONGO\"]))\n                    sys.exit(0)\n                # Additional special processing of events.\n                if event['topic'] == 'ftrack.meta.disconnected':\n                    break\n\n            if duration is not None:\n                if (time.time() - started) > duration:\n                    break\n\n    def load_events(self):\n        \"\"\"Load not processed events sorted by stored date\"\"\"\n        ago_date = datetime.datetime.now() - datetime.timedelta(days=3)\n        self.dbcon.delete_many({\n            \"pype_data.stored\": {\"$lte\": ago_date},\n            \"pype_data.is_processed\": True\n        })\n\n        not_processed_events = self.dbcon.find(\n            {\"pype_data.is_processed\": False}\n        ).sort(\n            [(\"pype_data.stored\", pymongo.ASCENDING)]\n        ).limit(100)\n\n        found = False\n        for event_data in not_processed_events:\n            new_event_data = {\n                k: v for k, v in event_data.items()\n                if k not in [\"_id\", \"pype_data\"]\n            }\n            try:\n                event = ftrack_api.event.base.Event(**new_event_data)\n                event[\"data\"][\"_event_mongo_id\"] = event_data[\"_id\"]\n            except Exception:\n                self.logger.exception(L(\n                    'Failed to convert payload into event: {0}',\n                    event_data\n                ))\n                continue\n            found = True\n            self._event_queue.put(event)\n\n        return found\n\n    def _handle_packet(self, code, packet_identifier, path, data):\n        \"\"\"Override `_handle_packet` which skip events and extend heartbeat\"\"\"\n        code_name = self._code_name_mapping[code]\n        if code_name == \"event\":\n            return\n\n        return super()._handle_packet(code, packet_identifier, path, data)\n\n\nclass CustomEventHubSession(ftrack_api.session.Session):\n    '''An isolated session for interaction with an ftrack server.'''\n    def __init__(\n        self, server_url=None, api_key=None, api_user=None, auto_populate=True,\n        plugin_paths=None, cache=None, cache_key_maker=None,\n        auto_connect_event_hub=False, schema_cache_path=None,\n        plugin_arguments=None, timeout=60, **kwargs\n    ):\n        self.kwargs = kwargs\n\n        super(ftrack_api.session.Session, self).__init__()\n        self.logger = logging.getLogger(\n            __name__ + '.' + self.__class__.__name__\n        )\n        self._closed = False\n\n        if server_url is None:\n            server_url = os.environ.get('FTRACK_SERVER')\n\n        if not server_url:\n            raise TypeError(\n                'Required \"server_url\" not specified. Pass as argument or set '\n                'in environment variable FTRACK_SERVER.'\n            )\n\n        self._server_url = server_url\n\n        if api_key is None:\n            api_key = os.environ.get(\n                'FTRACK_API_KEY',\n                # Backwards compatibility\n                os.environ.get('FTRACK_APIKEY')\n            )\n\n        if not api_key:\n            raise TypeError(\n                'Required \"api_key\" not specified. Pass as argument or set in '\n                'environment variable FTRACK_API_KEY.'\n            )\n\n        self._api_key = api_key\n\n        if api_user is None:\n            api_user = os.environ.get('FTRACK_API_USER')\n            if not api_user:\n                try:\n                    api_user = getpass.getuser()\n                except Exception:\n                    pass\n\n        if not api_user:\n            raise TypeError(\n                'Required \"api_user\" not specified. Pass as argument, set in '\n                'environment variable FTRACK_API_USER or one of the standard '\n                'environment variables used by Python\\'s getpass module.'\n            )\n\n        self._api_user = api_user\n\n        # Currently pending operations.\n        self.recorded_operations = ftrack_api.operation.Operations()\n\n        # OpenPype change - In new API are operations properties\n        new_api = hasattr(self.__class__, \"record_operations\")\n\n        if new_api:\n            self._record_operations = collections.defaultdict(\n                lambda: True\n            )\n            self._auto_populate = collections.defaultdict(\n                lambda: auto_populate\n            )\n        else:\n            self.record_operations = True\n            self.auto_populate = auto_populate\n\n        self.cache_key_maker = cache_key_maker\n        if self.cache_key_maker is None:\n            self.cache_key_maker = ftrack_api.cache.StringKeyMaker()\n\n        # Enforce always having a memory cache at top level so that the same\n        # in-memory instance is returned from session.\n        self.cache = ftrack_api.cache.LayeredCache([\n            ftrack_api.cache.MemoryCache()\n        ])\n\n        if cache is not None:\n            if callable(cache):\n                cache = cache(self)\n\n            if cache is not None:\n                self.cache.caches.append(cache)\n\n        if new_api:\n            self.merge_lock = threading.RLock()\n\n        self._managed_request = None\n        self._request = requests.Session()\n        self._request.auth = ftrack_api.session.SessionAuthentication(\n            self._api_key, self._api_user\n        )\n        self.request_timeout = timeout\n\n        # Fetch server information and in doing so also check credentials.\n        self._server_information = self._fetch_server_information()\n\n        # Now check compatibility of server based on retrieved information.\n        self.check_server_compatibility()\n\n        # Construct event hub and load plugins.\n        self._event_hub = self._create_event_hub()\n\n        self._auto_connect_event_hub_thread = None\n        if auto_connect_event_hub:\n            # Connect to event hub in background thread so as not to block main\n            # session usage waiting for event hub connection.\n            self._auto_connect_event_hub_thread = threading.Thread(\n                target=self._event_hub.connect\n            )\n            self._auto_connect_event_hub_thread.daemon = True\n            self._auto_connect_event_hub_thread.start()\n\n        # Register to auto-close session on exit.\n        atexit.register(WeakMethod(self.close))\n\n        self._plugin_paths = plugin_paths\n        if self._plugin_paths is None:\n            self._plugin_paths = os.environ.get(\n                'FTRACK_EVENT_PLUGIN_PATH', ''\n            ).split(os.pathsep)\n\n        self._discover_plugins(plugin_arguments=plugin_arguments)\n\n        # TODO: Make schemas read-only and non-mutable (or at least without\n        # rebuilding types)?\n        if schema_cache_path is not False:\n            if schema_cache_path is None:\n                schema_cache_path = appdirs.user_cache_dir()\n                schema_cache_path = os.environ.get(\n                    'FTRACK_API_SCHEMA_CACHE_PATH', schema_cache_path\n                )\n\n            schema_cache_path = os.path.join(\n                schema_cache_path, 'ftrack_api_schema_cache.json'\n            )\n\n        self.schemas = self._load_schemas(schema_cache_path)\n        self.types = self._build_entity_type_classes(self.schemas)\n\n        ftrack_api._centralized_storage_scenario.register(self)\n\n        self._configure_locations()\n        self.event_hub.publish(\n            ftrack_api.event.base.Event(\n                topic='ftrack.api.session.ready',\n                data=dict(\n                    session=self\n                )\n            ),\n            synchronous=True\n        )\n\n    def _create_event_hub(self):\n        return ftrack_api.event.hub.EventHub(\n            self._server_url,\n            self._api_user,\n            self._api_key\n        )\n\n\nclass SocketSession(CustomEventHubSession):\n    def _create_event_hub(self):\n        self.sock = self.kwargs[\"sock\"]\n        return self.kwargs[\"Eventhub\"](\n            self._server_url,\n            self._api_user,\n            self._api_key,\n            sock=self.sock\n        )\n"
  },
  {
    "path": "openpype/modules/ftrack/ftrack_server/socket_thread.py",
    "content": "import os\nimport sys\nimport time\nimport socket\nimport threading\nimport traceback\nimport subprocess\n\nfrom openpype.lib import get_openpype_execute_args, Logger\n\n\nclass SocketThread(threading.Thread):\n    \"\"\"Thread that checks suprocess of storer of processor of events\"\"\"\n\n    MAX_TIMEOUT = int(os.environ.get(\"OPENPYPE_FTRACK_SOCKET_TIMEOUT\", 45))\n\n    def __init__(self, name, port, filepath, additional_args=[]):\n        super(SocketThread, self).__init__()\n        self.log = Logger.get_logger(self.__class__.__name__)\n        self.setName(name)\n        self.name = name\n        self.port = port\n        self.filepath = filepath\n        self.additional_args = additional_args\n\n        self.sock = None\n        self.subproc = None\n        self.connection = None\n        self._is_running = False\n        self.finished = False\n\n        self.mongo_error = False\n\n        self._temp_data = {}\n\n    def stop(self):\n        self._is_running = False\n\n    def run(self):\n        self._is_running = True\n        time_socket = time.time()\n        # Create a TCP/IP socket\n        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n        self.sock = sock\n\n        # Bind the socket to the port - skip already used ports\n        while True:\n            try:\n                server_address = (\"localhost\", self.port)\n                sock.bind(server_address)\n                break\n            except OSError:\n                self.port += 1\n\n        self.log.debug(\n            \"Running Socked thread on {}:{}\".format(*server_address)\n        )\n\n        env = os.environ.copy()\n        env[\"OPENPYPE_PROCESS_MONGO_ID\"] = str(Logger.mongo_process_id)\n        # OpenPype executable (with path to start script if not build)\n        args = get_openpype_execute_args(\n            # Add `run` command\n            \"run\",\n            self.filepath,\n            *self.additional_args,\n            str(self.port)\n        )\n        kwargs = {\n            \"env\": env,\n            \"stdin\": subprocess.PIPE\n        }\n        if not sys.stdout:\n            # Redirect to devnull if stdout is None\n            kwargs[\"stdout\"] = subprocess.DEVNULL\n            kwargs[\"stderr\"] = subprocess.DEVNULL\n\n        self.subproc = subprocess.Popen(args, **kwargs)\n\n        # Listen for incoming connections\n        sock.listen(1)\n        sock.settimeout(1.0)\n        while True:\n            if not self._is_running:\n                break\n            try:\n                connection, client_address = sock.accept()\n                time_socket = time.time()\n                connection.settimeout(1.0)\n                self.connection = connection\n\n            except socket.timeout:\n                if (time.time() - time_socket) > self.MAX_TIMEOUT:\n                    self.log.error(\"Connection timeout passed. Terminating.\")\n                    self._is_running = False\n                    self.subproc.terminate()\n                    break\n                continue\n\n            try:\n                time_con = time.time()\n                # Receive the data in small chunks and retransmit it\n                while True:\n                    try:\n                        if not self._is_running:\n                            break\n                        data = None\n                        try:\n                            data = self.get_data_from_con(connection)\n                            time_con = time.time()\n\n                        except socket.timeout:\n                            if (time.time() - time_con) > self.MAX_TIMEOUT:\n                                self.log.error(\n                                    \"Connection timeout passed. Terminating.\"\n                                )\n                                self._is_running = False\n                                self.subproc.terminate()\n                                break\n                            continue\n\n                        except ConnectionResetError:\n                            self._is_running = False\n                            break\n\n                        self._handle_data(connection, data)\n\n                    except Exception as exc:\n                        self.log.error(\n                            \"Event server process failed\", exc_info=True\n                        )\n\n            finally:\n                # Clean up the connection\n                connection.close()\n                if self.subproc.poll() is None:\n                    self.subproc.terminate()\n\n                self.finished = True\n\n    def get_data_from_con(self, connection):\n        return connection.recv(16)\n\n    def _handle_data(self, connection, data):\n        if not data:\n            return\n\n        if data == b\"MongoError\":\n            self.mongo_error = True\n        connection.sendall(data)\n\n\nclass StatusSocketThread(SocketThread):\n    process_name_mapping = {\n        b\"RestartS\": \"storer\",\n        b\"RestartP\": \"processor\",\n        b\"RestartM\": \"main\"\n    }\n\n    def __init__(self, *args, **kwargs):\n        self.process_threads = {}\n        self.stop_subprocess = False\n        super(StatusSocketThread, self).__init__(*args, **kwargs)\n\n    def set_process(self, process_name, thread):\n        try:\n            if not self.subproc:\n                self.process_threads[process_name] = None\n                return\n\n            if (\n                process_name in self.process_threads and\n                self.process_threads[process_name] == thread\n            ):\n                return\n\n            self.process_threads[process_name] = thread\n            self.subproc.stdin.write(\n                str.encode(\"reset:{}\\r\\n\".format(process_name))\n            )\n            self.subproc.stdin.flush()\n\n        except Exception:\n            print(\"Could not set thread in StatusSocketThread\")\n            traceback.print_exception(*sys.exc_info())\n\n    def _handle_data(self, connection, data):\n        if not data:\n            return\n\n        process_name = self.process_name_mapping.get(data)\n        if process_name:\n            if process_name == \"main\":\n                self.stop_subprocess = True\n            else:\n                subp = self.process_threads.get(process_name)\n                if subp:\n                    subp.stop()\n        connection.sendall(data)\n"
  },
  {
    "path": "openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py",
    "content": "import os\n\nimport ftrack_api\nfrom openpype.settings import get_project_settings\nfrom openpype.lib.applications import PostLaunchHook, LaunchTypes\n\n\nclass PostFtrackHook(PostLaunchHook):\n    order = None\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        project_name = self.data.get(\"project_name\")\n        asset_name = self.data.get(\"asset_name\")\n        task_name = self.data.get(\"task_name\")\n\n        missing_context_keys = set()\n        if not project_name:\n            missing_context_keys.add(\"project_name\")\n        if not asset_name:\n            missing_context_keys.add(\"asset_name\")\n        if not task_name:\n            missing_context_keys.add(\"task_name\")\n\n        if missing_context_keys:\n            missing_keys_str = \", \".join([\n                \"\\\"{}\\\"\".format(key) for key in missing_context_keys\n            ])\n            self.log.debug(\"Hook {} skipped. Missing data keys: {}\".format(\n                self.__class__.__name__, missing_keys_str\n            ))\n            return\n\n        required_keys = (\"FTRACK_SERVER\", \"FTRACK_API_USER\", \"FTRACK_API_KEY\")\n        for key in required_keys:\n            if not os.environ.get(key):\n                self.log.debug((\n                    \"Missing required environment \\\"{}\\\"\"\n                    \" for Ftrack after launch procedure.\"\n                ).format(key))\n                return\n\n        try:\n            session = ftrack_api.Session(auto_connect_event_hub=True)\n            self.log.debug(\"Ftrack session created\")\n        except Exception:\n            self.log.warning(\"Couldn't create Ftrack session\")\n            return\n\n        try:\n            entity = self.find_ftrack_task_entity(\n                session, project_name, asset_name, task_name\n            )\n            if entity:\n                self.ftrack_status_change(session, entity, project_name)\n\n        except Exception:\n            self.log.warning(\n                \"Couldn't finish Ftrack procedure.\", exc_info=True\n            )\n            return\n\n        finally:\n            session.close()\n\n    def find_ftrack_task_entity(\n        self, session, project_name, asset_name, task_name\n    ):\n        project_entity = session.query(\n            \"Project where full_name is \\\"{}\\\"\".format(project_name)\n        ).first()\n        if not project_entity:\n            self.log.warning(\n                \"Couldn't find project \\\"{}\\\" in Ftrack.\".format(project_name)\n            )\n            return\n\n        potential_task_entities = session.query((\n            \"TypedContext where parent.name is \\\"{}\\\" and project_id is \\\"{}\\\"\"\n        ).format(asset_name, project_entity[\"id\"])).all()\n        filtered_entities = []\n        for _entity in potential_task_entities:\n            if (\n                _entity.entity_type.lower() == \"task\"\n                and _entity[\"name\"] == task_name\n            ):\n                filtered_entities.append(_entity)\n\n        if not filtered_entities:\n            self.log.warning((\n                \"Couldn't find task \\\"{}\\\" under parent \\\"{}\\\" in Ftrack.\"\n            ).format(task_name, asset_name))\n            return\n\n        if len(filtered_entities) > 1:\n            self.log.warning((\n                \"Found more than one task \\\"{}\\\"\"\n                \" under parent \\\"{}\\\" in Ftrack.\"\n            ).format(task_name, asset_name))\n            return\n\n        return filtered_entities[0]\n\n    def ftrack_status_change(self, session, entity, project_name):\n        project_settings = get_project_settings(project_name)\n        status_update = project_settings[\"ftrack\"][\"events\"][\"status_update\"]\n        if not status_update[\"enabled\"]:\n            self.log.debug(\n                \"Status changes are disabled for project \\\"{}\\\"\".format(\n                    project_name\n                )\n            )\n            return\n\n        status_mapping = status_update[\"mapping\"]\n        if not status_mapping:\n            self.log.warning(\n                \"Project \\\"{}\\\" does not have set status changes.\".format(\n                    project_name\n                )\n            )\n            return\n\n        actual_status = entity[\"status\"][\"name\"].lower()\n        already_tested = set()\n        ent_path = \"/\".join(\n            [ent[\"name\"] for ent in entity[\"link\"]]\n        )\n        while True:\n            next_status_name = None\n            for key, value in status_mapping.items():\n                if key in already_tested:\n                    continue\n\n                value = [i.lower() for i in value]\n                if actual_status in value or \"__any__\" in value:\n                    if key != \"__ignore__\":\n                        next_status_name = key\n                        already_tested.add(key)\n                    break\n                already_tested.add(key)\n\n            if next_status_name is None:\n                break\n\n            try:\n                query = \"Status where name is \\\"{}\\\"\".format(\n                    next_status_name\n                )\n                status = session.query(query).one()\n\n                entity[\"status\"] = status\n                session.commit()\n                self.log.debug(\"Changing status to \\\"{}\\\" <{}>\".format(\n                    next_status_name, ent_path\n                ))\n                break\n\n            except Exception:\n                session.rollback()\n                msg = (\n                    \"Status \\\"{}\\\" in presets wasn't found\"\n                    \" on Ftrack entity type \\\"{}\\\"\"\n                ).format(next_status_name, entity.entity_type)\n                self.log.warning(msg)\n"
  },
  {
    "path": "openpype/modules/ftrack/lib/__init__.py",
    "content": "from .constants import (\n    CUST_ATTR_ID_KEY,\n    CUST_ATTR_AUTO_SYNC,\n    CUST_ATTR_GROUP,\n    CUST_ATTR_TOOLS,\n    CUST_ATTR_APPLICATIONS,\n    CUST_ATTR_INTENT,\n    FPS_KEYS\n)\nfrom .settings import (\n    get_ftrack_event_mongo_info\n)\nfrom .custom_attributes import (\n    default_custom_attributes_definition,\n    app_definitions_from_app_manager,\n    tool_definitions_from_app_manager,\n    get_openpype_attr,\n    query_custom_attributes\n)\n\nfrom . import avalon_sync\nfrom . import credentials\nfrom .ftrack_base_handler import BaseHandler\nfrom .ftrack_event_handler import BaseEvent\nfrom .ftrack_action_handler import BaseAction, ServerAction, statics_icon\n\n\n__all__ = (\n    \"CUST_ATTR_ID_KEY\",\n    \"CUST_ATTR_AUTO_SYNC\",\n    \"CUST_ATTR_GROUP\",\n    \"CUST_ATTR_TOOLS\",\n    \"CUST_ATTR_APPLICATIONS\",\n    \"CUST_ATTR_INTENT\",\n    \"FPS_KEYS\",\n\n    \"get_ftrack_event_mongo_info\",\n\n    \"default_custom_attributes_definition\",\n    \"app_definitions_from_app_manager\",\n    \"tool_definitions_from_app_manager\",\n    \"get_openpype_attr\",\n    \"query_custom_attributes\",\n\n    \"avalon_sync\",\n\n    \"credentials\",\n\n    \"BaseHandler\",\n\n    \"BaseEvent\",\n\n    \"BaseAction\",\n    \"ServerAction\",\n    \"statics_icon\"\n)\n"
  },
  {
    "path": "openpype/modules/ftrack/lib/avalon_sync.py",
    "content": "import re\nimport json\nimport collections\nimport copy\nimport numbers\n\nimport six\n\nfrom openpype.client import (\n    get_project,\n    get_assets,\n    get_archived_assets,\n    get_subsets,\n    get_versions,\n    get_representations\n)\nfrom openpype.client.operations import (\n    CURRENT_ASSET_DOC_SCHEMA,\n    CURRENT_PROJECT_SCHEMA,\n    CURRENT_PROJECT_CONFIG_SCHEMA,\n)\nfrom openpype.settings import get_anatomy_settings\nfrom openpype.lib import ApplicationManager, Logger\nfrom openpype.pipeline import AvalonMongoDB, schema\n\nfrom .constants import CUST_ATTR_ID_KEY, FPS_KEYS\nfrom .custom_attributes import get_openpype_attr, query_custom_attributes\n\nfrom bson.objectid import ObjectId\nfrom bson.errors import InvalidId\nfrom pymongo import UpdateOne, ReplaceOne\nimport ftrack_api\n\nlog = Logger.get_logger(__name__)\n\n\nclass InvalidFpsValue(Exception):\n    pass\n\n\ndef is_string_number(value):\n    \"\"\"Can string value be converted to number (float).\"\"\"\n    if not isinstance(value, six.string_types):\n        raise TypeError(\"Expected {} got {}\".format(\n            \", \".join(str(t) for t in six.string_types), str(type(value))\n        ))\n    if value == \".\":\n        return False\n\n    if value.startswith(\".\"):\n        value = \"0\" + value\n    elif value.endswith(\".\"):\n        value = value + \"0\"\n\n    if re.match(r\"^\\d+(\\.\\d+)?$\", value) is None:\n        return False\n    return True\n\n\ndef convert_to_fps(source_value):\n    \"\"\"Convert value into fps value.\n\n    Non string values are kept untouched. String is tried to convert.\n    Valid values:\n    \"1000\"\n    \"1000.05\"\n    \"1000,05\"\n    \",05\"\n    \".05\"\n    \"1000,\"\n    \"1000.\"\n    \"1000/1000\"\n    \"1000.05/1000\"\n    \"1000/1000.05\"\n    \"1000.05/1000.05\"\n    \"1000,05/1000\"\n    \"1000/1000,05\"\n    \"1000,05/1000,05\"\n\n    Invalid values:\n    \"/\"\n    \"/1000\"\n    \"1000/\"\n    \",\"\n    \".\"\n    ...any other string\n\n    Returns:\n        float: Converted value.\n\n    Raises:\n        InvalidFpsValue: When value can't be converted to float.\n    \"\"\"\n    if not isinstance(source_value, six.string_types):\n        if isinstance(source_value, numbers.Number):\n            return float(source_value)\n        return source_value\n\n    value = source_value.strip().replace(\",\", \".\")\n    if not value:\n        raise InvalidFpsValue(\"Got empty value\")\n\n    subs = value.split(\"/\")\n    if len(subs) == 1:\n        str_value = subs[0]\n        if not is_string_number(str_value):\n            raise InvalidFpsValue(\n                \"Value \\\"{}\\\" can't be converted to number.\".format(value)\n            )\n        return float(str_value)\n\n    elif len(subs) == 2:\n        divident, divisor = subs\n        if not divident or not is_string_number(divident):\n            raise InvalidFpsValue(\n                \"Divident value \\\"{}\\\" can't be converted to number\".format(\n                    divident\n                )\n            )\n\n        if not divisor or not is_string_number(divisor):\n            raise InvalidFpsValue(\n                \"Divisor value \\\"{}\\\" can't be converted to number\".format(\n                    divident\n                )\n            )\n        divisor_float = float(divisor)\n        if divisor_float == 0.0:\n            raise InvalidFpsValue(\"Can't divide by zero\")\n        return float(divident) / divisor_float\n\n    raise InvalidFpsValue(\n        \"Value can't be converted to number \\\"{}\\\"\".format(source_value)\n    )\n\n\ndef create_chunks(iterable, chunk_size=None):\n    \"\"\"Separate iterable into multiple chunks by size.\n\n    Args:\n        iterable(list|tuple|set): Object that will be separated into chunks.\n        chunk_size(int): Size of one chunk. Default value is 200.\n\n    Returns:\n        list<list>: Chunked items.\n    \"\"\"\n    chunks = []\n\n    tupled_iterable = tuple(iterable)\n    if not tupled_iterable:\n        return chunks\n    iterable_size = len(tupled_iterable)\n    if chunk_size is None:\n        chunk_size = 200\n\n    if chunk_size < 1:\n        chunk_size = 1\n\n    for idx in range(0, iterable_size, chunk_size):\n        chunks.append(tupled_iterable[idx:idx + chunk_size])\n    return chunks\n\n\ndef check_regex(name, entity_type, in_schema=None, schema_patterns=None):\n    schema_name = \"asset-3.0\"\n    if in_schema:\n        schema_name = in_schema\n    elif entity_type == \"project\":\n        schema_name = \"project-2.1\"\n    elif entity_type == \"task\":\n        schema_name = \"task\"\n\n    name_pattern = None\n    if schema_patterns is not None:\n        name_pattern = schema_patterns.get(schema_name)\n\n    if not name_pattern:\n        default_pattern = \"^[a-zA-Z0-9_.]*$\"\n        schema_obj = schema._cache.get(schema_name + \".json\")\n        if not schema_obj:\n            name_pattern = default_pattern\n        else:\n            name_pattern = (\n                schema_obj\n                .get(\"properties\", {})\n                .get(\"name\", {})\n                .get(\"pattern\", default_pattern)\n            )\n        if schema_patterns is not None:\n            schema_patterns[schema_name] = name_pattern\n\n    if re.match(name_pattern, name):\n        return True\n    return False\n\n\ndef join_query_keys(keys):\n    return \",\".join([\"\\\"{}\\\"\".format(key) for key in keys])\n\n\ndef get_python_type_for_custom_attribute(cust_attr, cust_attr_type_name=None):\n    \"\"\"Python type that should value of custom attribute have.\n\n    This function is mainly for number type which is always float from ftrack.\n\n    Returns:\n        type: Python type which call be called on object to convert the object\n            to the type or None if can't figure out.\n    \"\"\"\n    if cust_attr_type_name is None:\n        cust_attr_type_name = cust_attr[\"type\"][\"name\"]\n\n    if cust_attr_type_name == \"text\":\n        return str\n\n    if cust_attr_type_name == \"boolean\":\n        return bool\n\n    if cust_attr_type_name in (\"number\", \"enumerator\"):\n        cust_attr_config = json.loads(cust_attr[\"config\"])\n        if cust_attr_type_name == \"number\":\n            if cust_attr_config[\"isdecimal\"]:\n                return float\n            return int\n\n        if cust_attr_type_name == \"enumerator\":\n            if cust_attr_config[\"multiSelect\"]:\n                return list\n            return str\n    # \"date\", \"expression\", \"notificationtype\", \"dynamic enumerator\"\n    return None\n\n\ndef from_dict_to_set(data, is_project):\n    \"\"\"\n        Converts 'data' into $set part of MongoDB update command.\n        Sets new or modified keys.\n        Tasks are updated completely, not per task. (Eg. change in any of the\n        tasks results in full update of \"tasks\" from Ftrack.\n    Args:\n        data (dictionary): up-to-date data from Ftrack\n        is_project (boolean): true for project\n\n    Returns:\n        (dictionary) - { \"$set\" : \"{..}\"}\n    \"\"\"\n    not_set = object()\n    task_changes = not_set\n    if (\n        is_project\n        and \"config\" in data\n        and \"tasks\" in data[\"config\"]\n    ):\n        task_changes = data[\"config\"].pop(\"tasks\")\n        task_changes_key = \"config.tasks\"\n        if not data[\"config\"]:\n            data.pop(\"config\")\n    elif (\n        not is_project\n        and \"data\" in data\n        and \"tasks\" in data[\"data\"]\n    ):\n        task_changes = data[\"data\"].pop(\"tasks\")\n        task_changes_key = \"data.tasks\"\n        if not data[\"data\"]:\n            data.pop(\"data\")\n\n    result = {\"$set\": {}}\n    dict_queue = collections.deque()\n    dict_queue.append((None, data))\n\n    while dict_queue:\n        _key, _data = dict_queue.popleft()\n        for key, value in _data.items():\n            new_key = key\n            if _key is not None:\n                new_key = \"{}.{}\".format(_key, key)\n\n            if not isinstance(value, dict) or \\\n                    (isinstance(value, dict) and not bool(value)):  # empty dic\n                result[\"$set\"][new_key] = value\n                continue\n            dict_queue.append((new_key, value))\n\n    if task_changes is not not_set and task_changes_key:\n        result[\"$set\"][task_changes_key] = task_changes\n    return result\n\n\ndef get_project_apps(in_app_list):\n    \"\"\" Application definitions for app name.\n\n    Args:\n        in_app_list: (list) - names of applications\n\n    Returns:\n        tuple (list, dictionary) - list of dictionaries with apps definitions\n            dictionary of warnings\n    \"\"\"\n    apps = []\n    warnings = collections.defaultdict(list)\n\n    if not in_app_list:\n        return apps, warnings\n\n    missing_app_msg = \"Missing definition of application\"\n    application_manager = ApplicationManager()\n    for app_name in in_app_list:\n        if application_manager.applications.get(app_name):\n            apps.append({\"name\": app_name})\n        else:\n            warnings[missing_app_msg].append(app_name)\n    return apps, warnings\n\n\ndef get_hierarchical_attributes_values(\n    session, entity, hier_attrs, cust_attr_types=None\n):\n    if not cust_attr_types:\n        cust_attr_types = session.query(\n            \"select id, name from CustomAttributeType\"\n        ).all()\n\n    cust_attr_name_by_id = {\n        cust_attr_type[\"id\"]: cust_attr_type[\"name\"]\n        for cust_attr_type in cust_attr_types\n    }\n    # Hierarchical cust attrs\n    attr_key_by_id = {}\n    convert_types_by_attr_id = {}\n    defaults = {}\n    for attr in hier_attrs:\n        attr_id = attr[\"id\"]\n        key = attr[\"key\"]\n        type_id = attr[\"type_id\"]\n\n        attr_key_by_id[attr_id] = key\n        defaults[key] = attr[\"default\"]\n\n        cust_attr_type_name = cust_attr_name_by_id[type_id]\n        convert_type = get_python_type_for_custom_attribute(\n            attr, cust_attr_type_name\n        )\n        convert_types_by_attr_id[attr_id] = convert_type\n\n    entity_ids = [item[\"id\"] for item in entity[\"link\"]]\n\n    values = query_custom_attributes(\n        session, list(attr_key_by_id.keys()), entity_ids, True\n    )\n\n    hier_values = {}\n    for key, val in defaults.items():\n        hier_values[key] = val\n\n    if not values:\n        return hier_values\n\n    values_by_entity_id = collections.defaultdict(dict)\n    for item in values:\n        value = item[\"value\"]\n        if value is None:\n            continue\n\n        attr_id = item[\"configuration_id\"]\n\n        convert_type = convert_types_by_attr_id[attr_id]\n        if convert_type:\n            value = convert_type(value)\n\n        key = attr_key_by_id[attr_id]\n        entity_id = item[\"entity_id\"]\n        values_by_entity_id[entity_id][key] = value\n\n    for entity_id in entity_ids:\n        for key in attr_key_by_id.values():\n            value = values_by_entity_id[entity_id].get(key)\n            if value is not None:\n                hier_values[key] = value\n\n    return hier_values\n\n\nclass SyncEntitiesFactory:\n    dbcon = AvalonMongoDB()\n\n    cust_attr_query_keys = [\n        \"id\",\n        \"key\",\n        \"entity_type\",\n        \"object_type_id\",\n        \"is_hierarchical\",\n        \"config\",\n        \"default\"\n    ]\n\n    project_query = (\n        \"select full_name, name, custom_attributes\"\n        \", project_schema._task_type_schema.types.name\"\n        \" from Project where full_name is \\\"{}\\\"\"\n    )\n    entities_query = (\n        \"select id, name, type_id, parent_id, link, description\"\n        \" from TypedContext where project_id is \\\"{}\\\"\"\n    )\n    ignore_custom_attr_key = \"avalon_ignore_sync\"\n    ignore_entity_types = [\"milestone\"]\n\n    report_splitter = {\"type\": \"label\", \"value\": \"---\"}\n\n    def __init__(self, log_obj, session):\n        self.log = log_obj\n        self._server_url = session.server_url\n        self._api_key = session.api_key\n        self._api_user = session.api_user\n\n    def launch_setup(self, project_full_name):\n        try:\n            self.session.close()\n        except Exception:\n            pass\n\n        self.session = ftrack_api.Session(\n            server_url=self._server_url,\n            api_key=self._api_key,\n            api_user=self._api_user,\n            auto_connect_event_hub=False\n        )\n\n        self.duplicates = {}\n        self.failed_regex = {}\n        self.tasks_failed_regex = collections.defaultdict(list)\n        self.report_items = {\n            \"info\": collections.defaultdict(list),\n            \"warning\": collections.defaultdict(list),\n            \"error\": collections.defaultdict(list)\n        }\n\n        self.create_list = []\n        self.project_created = False\n        self.unarchive_list = []\n        self.updates = collections.defaultdict(dict)\n\n        self.avalon_project = None\n        self.avalon_entities = None\n\n        self._avalon_ents_by_id = None\n        self._avalon_ents_by_ftrack_id = None\n        self._avalon_ents_by_name = None\n        self._avalon_ents_by_parent_id = None\n\n        self._avalon_archived_ents = None\n        self._avalon_archived_by_id = None\n        self._avalon_archived_by_parent_id = None\n        self._avalon_archived_by_name = None\n\n        self._subsets_by_parent_id = None\n        self._changeability_by_mongo_id = None\n\n        self._object_types_by_name = None\n\n        self.all_filtered_entities = {}\n        self.filtered_ids = []\n        self.not_selected_ids = []\n\n        self.hier_cust_attr_ids_by_key = {}\n\n        self._ent_paths_by_ftrack_id = {}\n\n        self.ftrack_avalon_mapper = None\n        self.avalon_ftrack_mapper = None\n        self.create_ftrack_ids = None\n        self.update_ftrack_ids = None\n        self.deleted_entities = None\n\n        # Get Ftrack project\n        ft_project = self.session.query(\n            self.project_query.format(project_full_name)\n        ).one()\n        ft_project_id = ft_project[\"id\"]\n\n        # Skip if project is ignored\n        if ft_project[\"custom_attributes\"].get(\n            self.ignore_custom_attr_key\n        ) is True:\n            msg = (\n                \"Project \\\"{}\\\" has set `Ignore Sync` custom attribute to True\"\n            ).format(project_full_name)\n            self.log.warning(msg)\n            return {\"success\": False, \"message\": msg}\n\n        self.log.debug((\n            \"*** Synchronization initialization started <{}>.\"\n        ).format(project_full_name))\n        # Check if `avalon_mongo_id` custom attribute exist or is accessible\n        if CUST_ATTR_ID_KEY not in ft_project[\"custom_attributes\"]:\n            items = []\n            items.append({\n                \"type\": \"label\",\n                \"value\": (\n                    \"# Can't access Custom attribute: <b>\\\"{}\\\"</b>\"\n                ).format(CUST_ATTR_ID_KEY)\n            })\n            items.append({\n                \"type\": \"label\",\n                \"value\": (\n                    \"<p>- Check if your User and API key has permissions\"\n                    \" to access the Custom attribute.\"\n                    \"<br>Username:\\\"{}\\\"\"\n                    \"<br>API key:\\\"{}\\\"</p>\"\n                ).format(self._api_user, self._api_key)\n            })\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"<p>- Check if the Custom attribute exist</p>\"\n            })\n            return {\n                \"items\": items,\n                \"title\": \"Synchronization failed\",\n                \"success\": False,\n                \"message\": \"Synchronization failed\"\n            }\n\n        # Store entities by `id` and `parent_id`\n        entities_dict = collections.defaultdict(lambda: {\n            \"children\": list(),\n            \"parent_id\": None,\n            \"entity\": None,\n            \"entity_type\": None,\n            \"name\": None,\n            \"custom_attributes\": {},\n            \"hier_attrs\": {},\n            \"avalon_attrs\": {},\n            \"tasks\": {}\n        })\n\n        # Find all entities in project\n        all_project_entities = self.session.query(\n            self.entities_query.format(ft_project_id)\n        ).all()\n        task_types = self.session.query(\"select id, name from Type\").all()\n        task_type_names_by_id = {\n            task_type[\"id\"]: task_type[\"name\"]\n            for task_type in task_types\n        }\n        for entity in all_project_entities:\n            parent_id = entity[\"parent_id\"]\n            entity_type = entity.entity_type\n            entity_type_low = entity_type.lower()\n            if entity_type_low in self.ignore_entity_types:\n                continue\n\n            elif entity_type_low == \"task\":\n                # enrich task info with additional metadata\n                task_type_name = task_type_names_by_id[entity[\"type_id\"]]\n                task = {\"type\": task_type_name}\n                entities_dict[parent_id][\"tasks\"][entity[\"name\"]] = task\n                continue\n\n            entity_id = entity[\"id\"]\n            entities_dict[entity_id].update({\n                \"entity\": entity,\n                \"parent_id\": parent_id,\n                \"entity_type\": entity_type_low,\n                \"entity_type_orig\": entity_type,\n                \"name\": entity[\"name\"]\n            })\n            entities_dict[parent_id][\"children\"].append(entity_id)\n\n        entities_dict[ft_project_id][\"entity\"] = ft_project\n        entities_dict[ft_project_id][\"entity_type\"] = (\n            ft_project.entity_type.lower()\n        )\n        entities_dict[ft_project_id][\"entity_type_orig\"] = (\n            ft_project.entity_type\n        )\n        entities_dict[ft_project_id][\"name\"] = ft_project[\"full_name\"]\n\n        self.ft_project_id = ft_project_id\n        self.entities_dict = entities_dict\n\n    @property\n    def project_name(self):\n        return self.entities_dict[self.ft_project_id][\"name\"]\n\n    @property\n    def avalon_ents_by_id(self):\n        \"\"\"\n            Returns dictionary of avalon tracked entities (assets stored in\n            MongoDB) accessible by its '_id'\n            (mongo intenal ID - example ObjectId(\"5f48de5830a9467b34b69798\"))\n        Returns:\n            (dictionary) - {\"(_id)\": whole entity asset}\n        \"\"\"\n        if self._avalon_ents_by_id is None:\n            self._avalon_ents_by_id = {}\n            for entity in self.avalon_entities:\n                self._avalon_ents_by_id[str(entity[\"_id\"])] = entity\n\n        return self._avalon_ents_by_id\n\n    @property\n    def avalon_ents_by_ftrack_id(self):\n        \"\"\"\n            Returns dictionary of Mongo ids of avalon tracked entities\n            (assets stored in MongoDB) accessible by its 'ftrackId'\n            (id from ftrack)\n            (example '431ee3f2-e91a-11ea-bfa4-92591a5b5e3e')\n            Returns:\n                (dictionary) - {\"(ftrackId)\": \"_id\"}\n        \"\"\"\n        if self._avalon_ents_by_ftrack_id is None:\n            self._avalon_ents_by_ftrack_id = {}\n            for entity in self.avalon_entities:\n                key = entity.get(\"data\", {}).get(\"ftrackId\")\n                if not key:\n                    continue\n                self._avalon_ents_by_ftrack_id[key] = str(entity[\"_id\"])\n\n        return self._avalon_ents_by_ftrack_id\n\n    @property\n    def avalon_ents_by_name(self):\n        \"\"\"\n            Returns dictionary of Mongo ids of avalon tracked entities\n            (assets stored in MongoDB) accessible by its 'name'\n            (example 'Hero')\n            Returns:\n                (dictionary) - {\"(name)\": \"_id\"}\n        \"\"\"\n        if self._avalon_ents_by_name is None:\n            self._avalon_ents_by_name = {}\n            for entity in self.avalon_entities:\n                self._avalon_ents_by_name[entity[\"name\"]] = str(entity[\"_id\"])\n\n        return self._avalon_ents_by_name\n\n    @property\n    def avalon_ents_by_parent_id(self):\n        \"\"\"\n            Returns dictionary of avalon tracked entities\n            (assets stored in MongoDB) accessible by its 'visualParent'\n            (example ObjectId(\"5f48de5830a9467b34b69798\"))\n\n            Fills 'self._avalon_archived_ents' for performance\n            Returns:\n                (dictionary) - {\"(_id)\": whole entity}\n        \"\"\"\n        if self._avalon_ents_by_parent_id is None:\n            self._avalon_ents_by_parent_id = collections.defaultdict(list)\n            for entity in self.avalon_entities:\n                parent_id = entity[\"data\"][\"visualParent\"]\n                if parent_id is not None:\n                    parent_id = str(parent_id)\n                self._avalon_ents_by_parent_id[parent_id].append(entity)\n\n        return self._avalon_ents_by_parent_id\n\n    @property\n    def avalon_archived_ents(self):\n        \"\"\"\n            Returns list of archived assets from DB\n            (their \"type\" == 'archived_asset')\n\n            Fills 'self._avalon_archived_ents' for performance\n        Returns:\n            (list) of assets\n        \"\"\"\n        if self._avalon_archived_ents is None:\n            self._avalon_archived_ents = list(\n                get_archived_assets(self.project_name)\n            )\n        return self._avalon_archived_ents\n\n    @property\n    def avalon_archived_by_name(self):\n        \"\"\"\n            Returns list of archived assets from DB\n            (their \"type\" == 'archived_asset')\n\n            Fills 'self._avalon_archived_by_name' for performance\n        Returns:\n            (dictionary of lists) of assets accessible by asset name\n        \"\"\"\n        if self._avalon_archived_by_name is None:\n            self._avalon_archived_by_name = collections.defaultdict(list)\n            for ent in self.avalon_archived_ents:\n                self._avalon_archived_by_name[ent[\"name\"]].append(ent)\n        return self._avalon_archived_by_name\n\n    @property\n    def avalon_archived_by_id(self):\n        \"\"\"\n            Returns dictionary of archived assets from DB\n            (their \"type\" == 'archived_asset')\n\n            Fills 'self._avalon_archived_by_id' for performance\n        Returns:\n            (dictionary) of assets accessible by asset mongo _id\n        \"\"\"\n        if self._avalon_archived_by_id is None:\n            self._avalon_archived_by_id = {\n                str(ent[\"_id\"]): ent for ent in self.avalon_archived_ents\n            }\n        return self._avalon_archived_by_id\n\n    @property\n    def avalon_archived_by_parent_id(self):\n        \"\"\"\n            Returns dictionary of archived assets from DB per their's parent\n            (their \"type\" == 'archived_asset')\n\n            Fills 'self._avalon_archived_by_parent_id' for performance\n        Returns:\n            (dictionary of lists) of assets accessible by asset parent\n                                     mongo _id\n        \"\"\"\n        if self._avalon_archived_by_parent_id is None:\n            self._avalon_archived_by_parent_id = collections.defaultdict(list)\n            for entity in self.avalon_archived_ents:\n                parent_id = entity[\"data\"][\"visualParent\"]\n                if parent_id is not None:\n                    parent_id = str(parent_id)\n                self._avalon_archived_by_parent_id[parent_id].append(entity)\n\n        return self._avalon_archived_by_parent_id\n\n    @property\n    def subsets_by_parent_id(self):\n        \"\"\"\n            Returns dictionary of subsets from Mongo (\"type\": \"subset\")\n            grouped by their parent.\n\n            Fills 'self._subsets_by_parent_id' for performance\n        Returns:\n            (dictionary of lists)\n        \"\"\"\n        if self._subsets_by_parent_id is None:\n            self._subsets_by_parent_id = collections.defaultdict(list)\n            for subset in get_subsets(self.project_name):\n                self._subsets_by_parent_id[str(subset[\"parent\"])].append(\n                    subset\n                )\n\n        return self._subsets_by_parent_id\n\n    @property\n    def changeability_by_mongo_id(self):\n        if self._changeability_by_mongo_id is None:\n            self._changeability_by_mongo_id = collections.defaultdict(\n                lambda: True\n            )\n            self._changeability_by_mongo_id[self.avalon_project_id] = False\n            self._bubble_changeability(list(self.subsets_by_parent_id.keys()))\n        return self._changeability_by_mongo_id\n\n    @property\n    def object_types_by_name(self):\n        if self._object_types_by_name is None:\n            object_types_by_name = self.session.query(\n                \"select id, name from ObjectType\"\n            ).all()\n            self._object_types_by_name = {\n                object_type[\"name\"]: object_type\n                for object_type in object_types_by_name\n            }\n        return self._object_types_by_name\n\n    @property\n    def all_ftrack_names(self):\n        \"\"\"\n            Returns lists of names of all entities in Ftrack\n        Returns:\n            (list)\n        \"\"\"\n        return [\n            ent_dict[\"name\"] for ent_dict in self.entities_dict.values() if (\n                ent_dict.get(\"name\")\n            )\n        ]\n\n    def duplicity_regex_check(self):\n        self.log.debug(\"* Checking duplicities and invalid symbols\")\n        # Duplicity and regex check\n        entity_ids_by_name = {}\n        duplicates = []\n        failed_regex = []\n        task_names = {}\n        _schema_patterns = {}\n        for ftrack_id, entity_dict in self.entities_dict.items():\n            regex_check = True\n            name = entity_dict[\"name\"]\n            entity_type = entity_dict[\"entity_type\"]\n            # Tasks must be checked too\n            for task in entity_dict[\"tasks\"].items():\n                task_name, task = task\n                passed = task_names.get(task_name)\n                if passed is None:\n                    passed = check_regex(\n                        task_name, \"task\", schema_patterns=_schema_patterns\n                    )\n                    task_names[task_name] = passed\n\n                if not passed:\n                    self.tasks_failed_regex[task_name].append(ftrack_id)\n\n            if name in entity_ids_by_name:\n                duplicates.append(name)\n            else:\n                entity_ids_by_name[name] = []\n                regex_check = check_regex(\n                    name, entity_type, schema_patterns=_schema_patterns\n                )\n\n            entity_ids_by_name[name].append(ftrack_id)\n            if not regex_check:\n                failed_regex.append(name)\n\n        for name in failed_regex:\n            self.failed_regex[name] = entity_ids_by_name[name]\n\n        for name in duplicates:\n            self.duplicates[name] = entity_ids_by_name[name]\n\n        self.filter_by_duplicate_regex()\n\n    def filter_by_duplicate_regex(self):\n        filter_queue = collections.deque()\n        failed_regex_msg = \"{} - Entity has invalid symbols in the name\"\n        duplicate_msg = \"There are multiple entities with the name: \\\"{}\\\":\"\n\n        for ids in self.failed_regex.values():\n            for id in ids:\n                ent_path = self.get_ent_path(id)\n                self.log.warning(failed_regex_msg.format(ent_path))\n                filter_queue.append(id)\n\n        for name, ids in self.duplicates.items():\n            self.log.warning(duplicate_msg.format(name))\n            for id in ids:\n                ent_path = self.get_ent_path(id)\n                self.log.warning(ent_path)\n                filter_queue.append(id)\n\n        filtered_ids = []\n        while filter_queue:\n            ftrack_id = filter_queue.popleft()\n            if ftrack_id in filtered_ids:\n                continue\n\n            entity_dict = self.entities_dict.pop(ftrack_id, {})\n            if not entity_dict:\n                continue\n\n            self.all_filtered_entities[ftrack_id] = entity_dict\n            parent_id = entity_dict.get(\"parent_id\")\n            if parent_id and parent_id in self.entities_dict:\n                if ftrack_id in self.entities_dict[parent_id][\"children\"]:\n                    self.entities_dict[parent_id][\"children\"].remove(ftrack_id)\n\n            filtered_ids.append(ftrack_id)\n            for child_id in entity_dict.get(\"children\", []):\n                filter_queue.append(child_id)\n\n        for name, ids in self.tasks_failed_regex.items():\n            for id in ids:\n                if id not in self.entities_dict:\n                    continue\n                self.entities_dict[id][\"tasks\"].pop(name)\n                ent_path = self.get_ent_path(id)\n                self.log.warning(failed_regex_msg.format(\n                    \"/\".join([ent_path, name])\n                ))\n\n    def filter_by_ignore_sync(self):\n        # skip filtering if `ignore_sync` attribute do not exist\n        if self.entities_dict[self.ft_project_id][\"avalon_attrs\"].get(\n            self.ignore_custom_attr_key, \"_notset_\"\n        ) == \"_notset_\":\n            return\n\n        filter_queue = collections.deque()\n        filter_queue.append((self.ft_project_id, False))\n        while filter_queue:\n            parent_id, remove = filter_queue.popleft()\n            if remove:\n                parent_dict = self.entities_dict.pop(parent_id, {})\n                self.all_filtered_entities[parent_id] = parent_dict\n                self.filtered_ids.append(parent_id)\n            else:\n                parent_dict = self.entities_dict.get(parent_id, {})\n\n            for child_id in list(parent_dict.get(\"children\", [])):\n                # keep original `remove` value for all children\n                _remove = (remove is True)\n                if not _remove:\n                    if self.entities_dict[child_id][\"avalon_attrs\"].get(\n                        self.ignore_custom_attr_key\n                    ):\n                        self.entities_dict[parent_id][\"children\"].remove(\n                            child_id\n                        )\n                        _remove = True\n                filter_queue.append((child_id, _remove))\n\n    def filter_by_selection(self, event):\n        # BUGGY!!!! cause that entities are in deleted list\n        # TODO may be working when filtering happen after preparations\n        # - But this part probably does not have any functional reason\n        #   - Time of synchronization probably won't be changed much\n        selected_ids = []\n        for entity in event[\"data\"][\"selection\"]:\n            # Skip if project is in selection\n            if entity[\"entityType\"] == \"show\":\n                return\n            selected_ids.append(entity[\"entityId\"])\n\n        sync_ids = [self.ft_project_id]\n        parents_queue = collections.deque()\n        children_queue = collections.deque()\n        for selected_id in selected_ids:\n            # skip if already filtered with ignore sync custom attribute\n            if selected_id in self.filtered_ids:\n                continue\n\n            parents_queue.append(selected_id)\n            children_queue.append(selected_id)\n\n        while parents_queue:\n            ftrack_id = parents_queue.popleft()\n            while True:\n                # Stops when parent is in sync_ids\n                if (\n                    ftrack_id in self.filtered_ids\n                    or ftrack_id in sync_ids\n                    or ftrack_id is None\n                ):\n                    break\n                sync_ids.append(ftrack_id)\n                ftrack_id = self.entities_dict[ftrack_id][\"parent_id\"]\n\n        while children_queue:\n            parent_id = children_queue.popleft()\n            for child_id in self.entities_dict[parent_id][\"children\"]:\n                if child_id in sync_ids or child_id in self.filtered_ids:\n                    continue\n                sync_ids.append(child_id)\n                children_queue.append(child_id)\n\n        # separate not selected and to process entities\n        for key, value in self.entities_dict.items():\n            if key not in sync_ids:\n                self.not_selected_ids.append(key)\n\n        for ftrack_id in self.not_selected_ids:\n            # pop from entities\n            value = self.entities_dict.pop(ftrack_id)\n            # remove entity from parent's children\n            parent_id = value[\"parent_id\"]\n            if parent_id not in sync_ids:\n                continue\n\n            self.entities_dict[parent_id][\"children\"].remove(ftrack_id)\n\n    def set_cutom_attributes(self):\n        self.log.debug(\"* Preparing custom attributes\")\n        # Get custom attributes and values\n        custom_attrs, hier_attrs = get_openpype_attr(\n            self.session, query_keys=self.cust_attr_query_keys\n        )\n        ent_types_by_name = self.object_types_by_name\n        # Custom attribute types\n        cust_attr_types = self.session.query(\n            \"select id, name from CustomAttributeType\"\n        ).all()\n        cust_attr_type_name_by_id = {\n            cust_attr_type[\"id\"]: cust_attr_type[\"name\"]\n            for cust_attr_type in cust_attr_types\n        }\n\n        # store default values per entity type\n        attrs_per_entity_type = collections.defaultdict(dict)\n        avalon_attrs = collections.defaultdict(dict)\n        # store also custom attribute configuration id for future use (create)\n        attrs_per_entity_type_ca_id = collections.defaultdict(dict)\n        avalon_attrs_ca_id = collections.defaultdict(dict)\n\n        attribute_key_by_id = {}\n        convert_types_by_attr_id = {}\n        for cust_attr in custom_attrs:\n            key = cust_attr[\"key\"]\n            attr_id = cust_attr[\"id\"]\n            type_id = cust_attr[\"type_id\"]\n\n            attribute_key_by_id[attr_id] = key\n            cust_attr_type_name = cust_attr_type_name_by_id[type_id]\n\n            convert_type = get_python_type_for_custom_attribute(\n                cust_attr, cust_attr_type_name\n            )\n            convert_types_by_attr_id[attr_id] = convert_type\n\n            ca_ent_type = cust_attr[\"entity_type\"]\n            if key.startswith(\"avalon_\"):\n                if ca_ent_type == \"show\":\n                    avalon_attrs[ca_ent_type][key] = cust_attr[\"default\"]\n                    avalon_attrs_ca_id[ca_ent_type][key] = cust_attr[\"id\"]\n                elif ca_ent_type == \"task\":\n                    obj_id = cust_attr[\"object_type_id\"]\n                    avalon_attrs[obj_id][key] = cust_attr[\"default\"]\n                    avalon_attrs_ca_id[obj_id][key] = cust_attr[\"id\"]\n                continue\n\n            if ca_ent_type == \"show\":\n                attrs_per_entity_type[ca_ent_type][key] = cust_attr[\"default\"]\n                attrs_per_entity_type_ca_id[ca_ent_type][key] = cust_attr[\"id\"]\n            elif ca_ent_type == \"task\":\n                obj_id = cust_attr[\"object_type_id\"]\n                attrs_per_entity_type[obj_id][key] = cust_attr[\"default\"]\n                attrs_per_entity_type_ca_id[obj_id][key] = cust_attr[\"id\"]\n\n        obj_id_ent_type_map = {}\n        sync_ids = []\n        for entity_id, entity_dict in self.entities_dict.items():\n            sync_ids.append(entity_id)\n            entity_type = entity_dict[\"entity_type\"]\n            entity_type_orig = entity_dict[\"entity_type_orig\"]\n\n            if entity_type == \"project\":\n                attr_key = \"show\"\n            else:\n                map_key = obj_id_ent_type_map.get(entity_type_orig)\n                if not map_key:\n                    # Put space between capitals\n                    # (e.g. 'AssetBuild' -> 'Asset Build')\n                    map_key = re.sub(\n                        r\"(\\w)([A-Z])\", r\"\\1 \\2\", entity_type_orig\n                    )\n                    obj_id_ent_type_map[entity_type_orig] = map_key\n\n                # Get object id of entity type\n                attr_key = ent_types_by_name.get(map_key)\n\n                # Backup soluction when id is not found by prequeried objects\n                if not attr_key:\n                    query = \"ObjectType where name is \\\"{}\\\"\".format(map_key)\n                    attr_key = self.session.query(query).one()[\"id\"]\n                    ent_types_by_name[map_key] = attr_key\n\n            prepared_attrs = attrs_per_entity_type.get(attr_key)\n            prepared_avalon_attr = avalon_attrs.get(attr_key)\n            prepared_attrs_ca_id = attrs_per_entity_type_ca_id.get(attr_key)\n            prepared_avalon_attr_ca_id = avalon_attrs_ca_id.get(attr_key)\n            if prepared_attrs:\n                self.entities_dict[entity_id][\"custom_attributes\"] = (\n                    copy.deepcopy(prepared_attrs)\n                )\n            if prepared_attrs_ca_id:\n                self.entities_dict[entity_id][\"custom_attributes_id\"] = (\n                    copy.deepcopy(prepared_attrs_ca_id)\n                )\n            if prepared_avalon_attr:\n                self.entities_dict[entity_id][\"avalon_attrs\"] = (\n                    copy.deepcopy(prepared_avalon_attr)\n                )\n            if prepared_avalon_attr_ca_id:\n                self.entities_dict[entity_id][\"avalon_attrs_id\"] = (\n                    copy.deepcopy(prepared_avalon_attr_ca_id)\n                )\n\n        items = query_custom_attributes(\n            self.session,\n            list(attribute_key_by_id.keys()),\n            sync_ids\n        )\n\n        invalid_fps_items = []\n        for item in items:\n            entity_id = item[\"entity_id\"]\n            attr_id = item[\"configuration_id\"]\n            key = attribute_key_by_id[attr_id]\n            store_key = \"custom_attributes\"\n            if key.startswith(\"avalon_\"):\n                store_key = \"avalon_attrs\"\n\n            convert_type = convert_types_by_attr_id[attr_id]\n            value = item[\"value\"]\n            if convert_type:\n                value = convert_type(value)\n\n            if key in FPS_KEYS:\n                try:\n                    value = convert_to_fps(value)\n                except InvalidFpsValue:\n                    invalid_fps_items.append((entity_id, value))\n            self.entities_dict[entity_id][store_key][key] = value\n\n        if invalid_fps_items:\n            fps_msg = (\n                \"These entities have invalid fps value in custom attributes\"\n            )\n            items = []\n            for entity_id, value in invalid_fps_items:\n                ent_path = self.get_ent_path(entity_id)\n                items.append(\"{} - \\\"{}\\\"\".format(ent_path, value))\n            self.report_items[\"error\"][fps_msg] = items\n\n        # process hierarchical attributes\n        self.set_hierarchical_attribute(\n            hier_attrs, sync_ids, cust_attr_type_name_by_id\n        )\n\n    def set_hierarchical_attribute(\n        self, hier_attrs, sync_ids, cust_attr_type_name_by_id\n    ):\n        # collect all hierarchical attribute keys\n        # and prepare default values to project\n        attributes_by_key = {}\n        attribute_key_by_id = {}\n        convert_types_by_attr_id = {}\n        for attr in hier_attrs:\n            key = attr[\"key\"]\n            attr_id = attr[\"id\"]\n            type_id = attr[\"type_id\"]\n            attribute_key_by_id[attr_id] = key\n            attributes_by_key[key] = attr\n\n            cust_attr_type_name = cust_attr_type_name_by_id[type_id]\n            convert_type = get_python_type_for_custom_attribute(\n                attr, cust_attr_type_name\n            )\n            convert_types_by_attr_id[attr_id] = convert_type\n\n            self.hier_cust_attr_ids_by_key[key] = attr[\"id\"]\n\n            store_key = \"hier_attrs\"\n            if key.startswith(\"avalon_\"):\n                store_key = \"avalon_attrs\"\n\n            default_value = attr[\"default\"]\n            if key in FPS_KEYS:\n                try:\n                    default_value = convert_to_fps(default_value)\n                except InvalidFpsValue:\n                    pass\n\n            self.entities_dict[self.ft_project_id][store_key][key] = (\n                default_value\n            )\n\n        # Add attribute ids to entities dictionary\n        avalon_attribute_id_by_key = {\n            attr_key: attr_id\n            for attr_id, attr_key in attribute_key_by_id.items()\n            if attr_key.startswith(\"avalon_\")\n        }\n        for entity_id in self.entities_dict.keys():\n            if \"avalon_attrs_id\" not in self.entities_dict[entity_id]:\n                self.entities_dict[entity_id][\"avalon_attrs_id\"] = {}\n\n            for attr_key, attr_id in avalon_attribute_id_by_key.items():\n                self.entities_dict[entity_id][\"avalon_attrs_id\"][attr_key] = (\n                    attr_id\n                )\n\n        # Prepare dict with all hier keys and None values\n        prepare_dict = {}\n        prepare_dict_avalon = {}\n        for key in attributes_by_key.keys():\n            if key.startswith(\"avalon_\"):\n                prepare_dict_avalon[key] = None\n            else:\n                prepare_dict[key] = None\n\n        for entity_dict in self.entities_dict.values():\n            # Skip project because has stored defaults at the moment\n            if entity_dict[\"entity_type\"] == \"project\":\n                continue\n            entity_dict[\"hier_attrs\"] = copy.deepcopy(prepare_dict)\n            for key, val in prepare_dict_avalon.items():\n                entity_dict[\"avalon_attrs\"][key] = val\n\n        items = query_custom_attributes(\n            self.session,\n            list(attribute_key_by_id.keys()),\n            sync_ids,\n            True\n        )\n\n        invalid_fps_items = []\n        avalon_hier = []\n        for item in items:\n            value = item[\"value\"]\n            # WARNING It is not possible to propagate enumerate hierarchical\n            # attributes with multiselection 100% right. Unsetting all values\n            # will cause inheritance from parent.\n            if (\n                value is None\n                or (isinstance(value, (tuple, list)) and not value)\n            ):\n                continue\n\n            attr_id = item[\"configuration_id\"]\n            convert_type = convert_types_by_attr_id[attr_id]\n            if convert_type:\n                value = convert_type(value)\n\n            entity_id = item[\"entity_id\"]\n            key = attribute_key_by_id[attr_id]\n            if key in FPS_KEYS:\n                try:\n                    value = convert_to_fps(value)\n                except InvalidFpsValue:\n                    invalid_fps_items.append((entity_id, value))\n                    continue\n\n            if key.startswith(\"avalon_\"):\n                store_key = \"avalon_attrs\"\n                avalon_hier.append(key)\n            else:\n                store_key = \"hier_attrs\"\n            self.entities_dict[entity_id][store_key][key] = value\n\n        if invalid_fps_items:\n            fps_msg = (\n                \"These entities have invalid fps value in custom attributes\"\n            )\n            items = []\n            for entity_id, value in invalid_fps_items:\n                ent_path = self.get_ent_path(entity_id)\n                items.append(\"{} - \\\"{}\\\"\".format(ent_path, value))\n            self.report_items[\"error\"][fps_msg] = items\n\n        # Get dictionary with not None hierarchical values to pull to children\n        top_id = self.ft_project_id\n        project_values = {}\n        for key, value in self.entities_dict[top_id][\"hier_attrs\"].items():\n            if value is not None:\n                project_values[key] = value\n\n        for key in avalon_hier:\n            if key == CUST_ATTR_ID_KEY:\n                continue\n            value = self.entities_dict[top_id][\"avalon_attrs\"][key]\n            if value is not None:\n                project_values[key] = value\n\n        hier_down_queue = collections.deque()\n        hier_down_queue.append((project_values, top_id))\n\n        while hier_down_queue:\n            hier_values, parent_id = hier_down_queue.popleft()\n            for child_id in self.entities_dict[parent_id][\"children\"]:\n                _hier_values = copy.deepcopy(hier_values)\n                for key in attributes_by_key.keys():\n                    if key.startswith(\"avalon_\"):\n                        store_key = \"avalon_attrs\"\n                    else:\n                        store_key = \"hier_attrs\"\n                    value = self.entities_dict[child_id][store_key][key]\n                    if value is not None:\n                        _hier_values[key] = value\n\n                self.entities_dict[child_id][\"hier_attrs\"].update(_hier_values)\n                hier_down_queue.append((_hier_values, child_id))\n\n    def remove_from_archived(self, mongo_id):\n        entity = self.avalon_archived_by_id.pop(mongo_id, None)\n        if not entity:\n            return\n\n        if self._avalon_archived_ents is not None:\n            if entity in self._avalon_archived_ents:\n                self._avalon_archived_ents.remove(entity)\n\n        if self._avalon_archived_by_name is not None:\n            name = entity[\"name\"]\n            if name in self._avalon_archived_by_name:\n                name_ents = self._avalon_archived_by_name[name]\n                if entity in name_ents:\n                    if len(name_ents) == 1:\n                        self._avalon_archived_by_name.pop(name)\n                    else:\n                        self._avalon_archived_by_name[name].remove(entity)\n\n        # TODO use custom None instead of __NOTSET__\n        if self._avalon_archived_by_parent_id is not None:\n            parent_id = entity.get(\"data\", {}).get(\n                \"visualParent\", \"__NOTSET__\"\n            )\n            if parent_id is not None:\n                parent_id = str(parent_id)\n\n            if parent_id in self._avalon_archived_by_parent_id:\n                parent_list = self._avalon_archived_by_parent_id[parent_id]\n                if entity not in parent_list:\n                    self._avalon_archived_by_parent_id[parent_id].remove(\n                        entity\n                    )\n\n    def _get_input_links(self, ftrack_ids):\n        tupled_ids = tuple(ftrack_ids)\n        mapping_by_to_id = {\n            ftrack_id: set()\n            for ftrack_id in tupled_ids\n        }\n        ids_len = len(tupled_ids)\n        chunk_size = int(5000 / ids_len)\n        all_links = []\n        for chunk in create_chunks(ftrack_ids, chunk_size):\n            entity_ids_joined = join_query_keys(chunk)\n\n            all_links.extend(self.session.query((\n                \"select from_id, to_id from\"\n                \" TypedContextLink where to_id in ({})\"\n            ).format(entity_ids_joined)).all())\n\n        for context_link in all_links:\n            to_id = context_link[\"to_id\"]\n            from_id = context_link[\"from_id\"]\n            if from_id == to_id:\n                continue\n            mapping_by_to_id[to_id].add(from_id)\n        return mapping_by_to_id\n\n    def prepare_ftrack_ent_data(self):\n        not_set_ids = []\n        for ftrack_id, entity_dict in self.entities_dict.items():\n            entity = entity_dict[\"entity\"]\n            if entity is None:\n                not_set_ids.append(ftrack_id)\n                continue\n\n            self.entities_dict[ftrack_id][\"final_entity\"] = {}\n            self.entities_dict[ftrack_id][\"final_entity\"][\"name\"] = (\n                entity_dict[\"name\"]\n            )\n            data = {}\n            data[\"ftrackId\"] = entity[\"id\"]\n            data[\"entityType\"] = entity_dict[\"entity_type_orig\"]\n\n            for key, val in entity_dict.get(\"custom_attributes\", []).items():\n                data[key] = val\n\n            for key, val in entity_dict.get(\"hier_attrs\", []).items():\n                data[key] = val\n\n            if ftrack_id != self.ft_project_id:\n                data[\"description\"] = entity[\"description\"]\n\n                ent_path_items = [ent[\"name\"] for ent in entity[\"link\"]]\n                parents = ent_path_items[1:len(ent_path_items) - 1:]\n\n                data[\"parents\"] = parents\n                data[\"tasks\"] = self.entities_dict[ftrack_id].pop(\"tasks\", {})\n                self.entities_dict[ftrack_id][\"final_entity\"][\"data\"] = data\n                self.entities_dict[ftrack_id][\"final_entity\"][\"type\"] = \"asset\"\n                continue\n            project_name = entity[\"full_name\"]\n            data[\"code\"] = entity[\"name\"]\n            self.entities_dict[ftrack_id][\"final_entity\"][\"data\"] = data\n            self.entities_dict[ftrack_id][\"final_entity\"][\"type\"] = (\n                \"project\"\n            )\n\n            proj_schema = entity[\"project_schema\"]\n            task_types = proj_schema[\"_task_type_schema\"][\"types\"]\n            proj_apps, warnings = get_project_apps(\n                data.pop(\"applications\", [])\n            )\n            for msg, items in warnings.items():\n                if not msg or not items:\n                    continue\n                self.report_items[\"warning\"][msg] = items\n\n            current_project_anatomy_data = get_anatomy_settings(\n                project_name, exclude_locals=True\n            )\n            anatomy_tasks = current_project_anatomy_data[\"tasks\"]\n            tasks = {}\n            default_type_data = {\n                \"short_name\": \"\"\n            }\n            for task_type in task_types:\n                task_type_name = task_type[\"name\"]\n                tasks[task_type_name] = copy.deepcopy(\n                    anatomy_tasks.get(task_type_name)\n                    or default_type_data\n                )\n\n            project_config = {\n                \"tasks\": tasks,\n                \"apps\": proj_apps\n            }\n            for key, value in current_project_anatomy_data.items():\n                if key in project_config or key == \"attributes\":\n                    continue\n                project_config[key] = value\n\n            self.entities_dict[ftrack_id][\"final_entity\"][\"config\"] = (\n                project_config\n            )\n\n        if not_set_ids:\n            self.log.debug((\n                \"- Debug information: Filtering bug, there are empty dicts\"\n                \"in entities dict (functionality should not be affected) <{}>\"\n            ).format(\"| \".join(not_set_ids)))\n            for id in not_set_ids:\n                self.entities_dict.pop(id)\n\n    def get_ent_path(self, ftrack_id):\n        ent_path = self._ent_paths_by_ftrack_id.get(ftrack_id)\n        if not ent_path:\n            entity = self.entities_dict[ftrack_id][\"entity\"]\n            ent_path = \"/\".join(\n                [ent[\"name\"] for ent in entity[\"link\"]]\n            )\n            self._ent_paths_by_ftrack_id[ftrack_id] = ent_path\n\n        return ent_path\n\n    def prepare_avalon_entities(self, ft_project_name):\n        self.log.debug((\n            \"* Preparing avalon entities \"\n            \"(separate to Create, Update and Deleted groups)\"\n        ))\n        # Avalon entities\n        self.dbcon.install()\n        self.dbcon.Session[\"AVALON_PROJECT\"] = ft_project_name\n        avalon_project = get_project(ft_project_name)\n        avalon_entities = get_assets(ft_project_name)\n        self.avalon_project = avalon_project\n        self.avalon_entities = avalon_entities\n\n        ftrack_avalon_mapper = {}\n        avalon_ftrack_mapper = {}\n        create_ftrack_ids = []\n        update_ftrack_ids = []\n\n        same_mongo_id = []\n        all_mongo_ids = {}\n        for ftrack_id, entity_dict in self.entities_dict.items():\n            mongo_id = entity_dict[\"avalon_attrs\"].get(CUST_ATTR_ID_KEY)\n            if not mongo_id:\n                continue\n            if mongo_id in all_mongo_ids:\n                same_mongo_id.append(mongo_id)\n            else:\n                all_mongo_ids[mongo_id] = []\n            all_mongo_ids[mongo_id].append(ftrack_id)\n\n        if avalon_project:\n            mongo_id = str(avalon_project[\"_id\"])\n            ftrack_avalon_mapper[self.ft_project_id] = mongo_id\n            avalon_ftrack_mapper[mongo_id] = self.ft_project_id\n            update_ftrack_ids.append(self.ft_project_id)\n        else:\n            create_ftrack_ids.append(self.ft_project_id)\n\n        # make it go hierarchically\n        prepare_queue = collections.deque()\n\n        for child_id in self.entities_dict[self.ft_project_id][\"children\"]:\n            prepare_queue.append(child_id)\n\n        while prepare_queue:\n            ftrack_id = prepare_queue.popleft()\n            for child_id in self.entities_dict[ftrack_id][\"children\"]:\n                prepare_queue.append(child_id)\n\n            entity_dict = self.entities_dict[ftrack_id]\n            ent_path = self.get_ent_path(ftrack_id)\n\n            mongo_id = entity_dict[\"avalon_attrs\"].get(CUST_ATTR_ID_KEY)\n            av_ent_by_mongo_id = self.avalon_ents_by_id.get(mongo_id)\n            if av_ent_by_mongo_id:\n                av_ent_ftrack_id = av_ent_by_mongo_id.get(\"data\", {}).get(\n                    \"ftrackId\"\n                )\n                is_right = False\n                else_match_better = False\n                if av_ent_ftrack_id and av_ent_ftrack_id == ftrack_id:\n                    is_right = True\n\n                elif mongo_id not in same_mongo_id:\n                    is_right = True\n\n                else:\n                    ftrack_ids_with_same_mongo = all_mongo_ids[mongo_id]\n                    for _ftrack_id in ftrack_ids_with_same_mongo:\n                        if _ftrack_id == av_ent_ftrack_id:\n                            continue\n\n                        _entity_dict = self.entities_dict[_ftrack_id]\n                        _mongo_id = (\n                            _entity_dict[\"avalon_attrs\"][CUST_ATTR_ID_KEY]\n                        )\n                        _av_ent_by_mongo_id = self.avalon_ents_by_id.get(\n                            _mongo_id\n                        )\n                        _av_ent_ftrack_id = _av_ent_by_mongo_id.get(\n                            \"data\", {}\n                        ).get(\"ftrackId\")\n                        if _av_ent_ftrack_id == ftrack_id:\n                            else_match_better = True\n                            break\n\n                if not is_right and not else_match_better:\n                    entity = entity_dict[\"entity\"]\n                    ent_path_items = [ent[\"name\"] for ent in entity[\"link\"]]\n                    parents = ent_path_items[1:len(ent_path_items) - 1:]\n                    av_parents = av_ent_by_mongo_id[\"data\"][\"parents\"]\n                    if av_parents == parents:\n                        is_right = True\n                    else:\n                        name = entity_dict[\"name\"]\n                        av_name = av_ent_by_mongo_id[\"name\"]\n                        if name == av_name:\n                            is_right = True\n\n                if is_right:\n                    self.log.debug(\n                        \"Existing (by MongoID) <{}>\".format(ent_path)\n                    )\n                    ftrack_avalon_mapper[ftrack_id] = mongo_id\n                    avalon_ftrack_mapper[mongo_id] = ftrack_id\n                    update_ftrack_ids.append(ftrack_id)\n                    continue\n\n            mongo_id = self.avalon_ents_by_ftrack_id.get(ftrack_id)\n            if not mongo_id:\n                mongo_id = self.avalon_ents_by_name.get(entity_dict[\"name\"])\n                if mongo_id:\n                    self.log.debug(\n                        \"Existing (by matching name) <{}>\".format(ent_path)\n                    )\n            else:\n                self.log.debug(\n                    \"Existing (by FtrackID in mongo) <{}>\".format(ent_path)\n                )\n\n            if mongo_id:\n                ftrack_avalon_mapper[ftrack_id] = mongo_id\n                avalon_ftrack_mapper[mongo_id] = ftrack_id\n                update_ftrack_ids.append(ftrack_id)\n                continue\n\n            self.log.debug(\"New <{}>\".format(ent_path))\n            create_ftrack_ids.append(ftrack_id)\n\n        deleted_entities = []\n        for mongo_id in self.avalon_ents_by_id:\n            if mongo_id in avalon_ftrack_mapper:\n                continue\n            deleted_entities.append(mongo_id)\n\n            av_ent = self.avalon_ents_by_id[mongo_id]\n            av_ent_path_items = list(av_ent[\"data\"][\"parents\"])\n            av_ent_path_items.append(av_ent[\"name\"])\n            self.log.debug(\"Deleted <{}>\".format(\"/\".join(av_ent_path_items)))\n\n        self.ftrack_avalon_mapper = ftrack_avalon_mapper\n        self.avalon_ftrack_mapper = avalon_ftrack_mapper\n        self.create_ftrack_ids = create_ftrack_ids\n        self.update_ftrack_ids = update_ftrack_ids\n        self.deleted_entities = deleted_entities\n\n        self.log.debug((\n            \"Ftrack -> Avalon comparison: New <{}> \"\n            \"| Existing <{}> | Deleted <{}>\"\n        ).format(\n            len(create_ftrack_ids),\n            len(update_ftrack_ids),\n            len(deleted_entities)\n        ))\n\n    def filter_with_children(self, ftrack_id):\n        if ftrack_id not in self.entities_dict:\n            return\n        ent_dict = self.entities_dict[ftrack_id]\n        parent_id = ent_dict[\"parent_id\"]\n        self.entities_dict[parent_id][\"children\"].remove(ftrack_id)\n\n        children_queue = collections.deque()\n        children_queue.append(ftrack_id)\n        while children_queue:\n            _ftrack_id = children_queue.popleft()\n            entity_dict = self.entities_dict.pop(_ftrack_id, {\"children\": []})\n            for child_id in entity_dict[\"children\"]:\n                children_queue.append(child_id)\n\n    def set_input_links(self):\n        ftrack_ids = set(self.create_ftrack_ids) | set(self.update_ftrack_ids)\n\n        input_links_by_ftrack_id = self._get_input_links(ftrack_ids)\n\n        for ftrack_id in ftrack_ids:\n            input_links = []\n            final_entity = self.entities_dict[ftrack_id][\"final_entity\"]\n            final_entity[\"data\"][\"inputLinks\"] = input_links\n            link_ids = input_links_by_ftrack_id[ftrack_id]\n            if not link_ids:\n                continue\n\n            for ftrack_link_id in link_ids:\n                mongo_id = self.ftrack_avalon_mapper.get(ftrack_link_id)\n                if mongo_id is not None:\n                    input_links.append({\n                        \"id\": ObjectId(mongo_id),\n                        \"linkedBy\": \"ftrack\",\n                        \"type\": \"breakdown\"\n                    })\n\n    def prepare_changes(self):\n        self.log.debug(\"* Preparing changes for avalon/ftrack\")\n        hierarchy_changing_ids = []\n        ignore_keys = collections.defaultdict(list)\n\n        update_queue = collections.deque()\n        for ftrack_id in self.update_ftrack_ids:\n            update_queue.append(ftrack_id)\n\n        while update_queue:\n            ftrack_id = update_queue.popleft()\n            if ftrack_id == self.ft_project_id:\n                changes = self.prepare_project_changes()\n                if changes:\n                    self.updates[self.avalon_project_id] = changes\n                continue\n\n            ftrack_ent_dict = self.entities_dict[ftrack_id]\n\n            # *** check parents\n            parent_check = False\n\n            ftrack_parent_id = ftrack_ent_dict[\"parent_id\"]\n            avalon_id = self.ftrack_avalon_mapper[ftrack_id]\n            avalon_entity = self.avalon_ents_by_id[avalon_id]\n            avalon_parent_id = avalon_entity[\"data\"][\"visualParent\"]\n            if avalon_parent_id is not None:\n                avalon_parent_id = str(avalon_parent_id)\n\n            ftrack_parent_mongo_id = self.ftrack_avalon_mapper[\n                ftrack_parent_id\n            ]\n\n            # if parent is project\n            if (ftrack_parent_mongo_id == avalon_parent_id) or (\n                ftrack_parent_id == self.ft_project_id and\n                avalon_parent_id is None\n            ):\n                parent_check = True\n\n            # check name\n            ftrack_name = ftrack_ent_dict[\"name\"]\n            avalon_name = avalon_entity[\"name\"]\n            name_check = ftrack_name == avalon_name\n\n            # IDEAL STATE: both parent and name check passed\n            if parent_check and name_check:\n                continue\n\n            # If entity is changeable then change values of parent or name\n            if self.changeability_by_mongo_id[avalon_id]:\n                # TODO logging\n                if not parent_check:\n                    if ftrack_parent_mongo_id == str(self.avalon_project_id):\n                        new_parent_name = self.entities_dict[\n                            self.ft_project_id][\"name\"]\n                        new_parent_id = None\n                    else:\n                        new_parent_name = self.avalon_ents_by_id[\n                            ftrack_parent_mongo_id][\"name\"]\n                        new_parent_id = ObjectId(ftrack_parent_mongo_id)\n\n                    if avalon_parent_id == str(self.avalon_project_id):\n                        old_parent_name = self.entities_dict[\n                            self.ft_project_id][\"name\"]\n                    else:\n                        old_parent_name = \"N/A\"\n                        if ftrack_parent_mongo_id in self.avalon_ents_by_id:\n                            old_parent_name = (\n                                self.avalon_ents_by_id\n                                [ftrack_parent_mongo_id]\n                                [\"name\"]\n                            )\n\n                    self.updates[avalon_id][\"data\"] = {\n                        \"visualParent\": new_parent_id\n                    }\n                    ignore_keys[ftrack_id].append(\"data.visualParent\")\n                    self.log.debug((\n                        \"Avalon entity \\\"{}\\\" changed parent \\\"{}\\\" -> \\\"{}\\\"\"\n                    ).format(avalon_name, old_parent_name, new_parent_name))\n\n                if not name_check:\n                    self.updates[avalon_id][\"name\"] = ftrack_name\n                    ignore_keys[ftrack_id].append(\"name\")\n                    self.log.debug(\n                        \"Avalon entity \\\"{}\\\" was renamed to \\\"{}\\\"\".format(\n                            avalon_name, ftrack_name\n                        )\n                    )\n                continue\n\n            # parents and hierarchy must be recalculated\n            hierarchy_changing_ids.append(ftrack_id)\n\n            # Parent is project if avalon_parent_id is set to None\n            if avalon_parent_id is None:\n                avalon_parent_id = str(self.avalon_project_id)\n\n            if not name_check:\n                ent_path = self.get_ent_path(ftrack_id)\n                # TODO report\n                # TODO logging\n                self.entities_dict[ftrack_id][\"name\"] = avalon_name\n                self.entities_dict[ftrack_id][\"entity\"][\"name\"] = (\n                    avalon_name\n                )\n                self.entities_dict[ftrack_id][\"final_entity\"][\"name\"] = (\n                    avalon_name\n                )\n                self.log.warning(\"Name was changed back to {} <{}>\".format(\n                    avalon_name, ent_path\n                ))\n                self._ent_paths_by_ftrack_id.pop(ftrack_id, None)\n                msg = (\n                    \"<Entity renamed back> It is not possible to change\"\n                    \" the name of an entity or it's parents, \"\n                    \" if it already contained published data.\"\n                )\n                self.report_items[\"warning\"][msg].append(ent_path)\n\n            # skip parent oricessing if hierarchy didn't change\n            if parent_check:\n                continue\n\n            # Logic when parenting(hierarchy) has changed and should not\n            old_ftrack_parent_id = self.avalon_ftrack_mapper.get(\n                avalon_parent_id\n            )\n\n            # If last ftrack parent id from mongo entity exist then just\n            # remap paren_id on entity\n            if old_ftrack_parent_id:\n                # TODO report\n                # TODO logging\n                ent_path = self.get_ent_path(ftrack_id)\n                msg = (\n                    \"<Entity moved back in hierarchy> It is not possible\"\n                    \" to change the hierarchy of an entity or it's parents,\"\n                    \" if it already contained published data.\"\n                )\n                self.report_items[\"warning\"][msg].append(ent_path)\n                self.log.warning((\n                    \" Entity contains published data so it was moved\"\n                    \" back to it's original hierarchy <{}>\"\n                ).format(ent_path))\n                self.entities_dict[ftrack_id][\"entity\"][\"parent_id\"] = (\n                    old_ftrack_parent_id\n                )\n                self.entities_dict[ftrack_id][\"parent_id\"] = (\n                    old_ftrack_parent_id\n                )\n                self.entities_dict[old_ftrack_parent_id][\n                    \"children\"\n                ].append(ftrack_id)\n\n                continue\n\n            old_parent_ent = self.avalon_ents_by_id.get(avalon_parent_id)\n            if not old_parent_ent:\n                old_parent_ent = self.avalon_archived_by_id.get(\n                    avalon_parent_id\n                )\n\n            # TODO report\n            # TODO logging\n            if not old_parent_ent:\n                self.log.warning((\n                    \"Parent entity was not found by id\"\n                    \" - Trying to find by parent name\"\n                ))\n                ent_path = self.get_ent_path(ftrack_id)\n\n                parents = avalon_entity[\"data\"][\"parents\"]\n                parent_name = parents[-1]\n                matching_entity_id = None\n                for id, entity_dict in self.entities_dict.items():\n                    if entity_dict[\"name\"] == parent_name:\n                        matching_entity_id = id\n                        break\n\n                if matching_entity_id is None:\n                    # TODO logging\n                    # TODO report (turn off auto-sync?)\n                    self.log.error((\n                        \"The entity contains published data but it was moved\"\n                        \" to a different place in the hierarchy and it's\"\n                        \" previous parent cannot be found.\"\n                        \" It's impossible to solve this programmatically <{}>\"\n                    ).format(ent_path))\n                    msg = (\n                        \"<Entity can't be synchronised> Hierarchy of an entity\"\n                        \" can't be changed due to published data and missing\"\n                        \" previous parent\"\n                    )\n                    self.report_items[\"error\"][msg].append(ent_path)\n                    self.filter_with_children(ftrack_id)\n                    continue\n\n                matching_ent_dict = self.entities_dict.get(matching_entity_id)\n                match_ent_parents = matching_ent_dict.get(\n                    \"final_entity\", {}).get(\n                    \"data\", {}).get(\n                    \"parents\", [\"__NOTSET__\"]\n                )\n                # TODO logging\n                # TODO report\n                if (\n                    len(match_ent_parents) >= len(parents) or\n                    match_ent_parents[:-1] != parents\n                ):\n                    ent_path = self.get_ent_path(ftrack_id)\n                    self.log.error((\n                        \"The entity contains published data but it was moved\"\n                        \" to a different place in the hierarchy and it's\"\n                        \" previous parents were moved too.\"\n                        \" It's impossible to solve this programmatically <{}>\"\n                    ).format(ent_path))\n                    msg = (\n                        \"<Entity not synchronizable> Hierarchy of an entity\"\n                        \" can't be changed due to published data and scrambled\"\n                        \"hierarchy\"\n                    )\n                    continue\n\n                old_parent_ent = matching_ent_dict[\"final_entity\"]\n\n            parent_id = self.ft_project_id\n            entities_to_create = []\n            # TODO logging\n            self.log.warning(\n                \"Ftrack entities must be recreated because they were deleted,\"\n                \" but they contain published data.\"\n            )\n\n            _avalon_ent = old_parent_ent\n\n            self.updates[avalon_parent_id] = {\"type\": \"asset\"}\n            success = True\n            while True:\n                _vis_par = _avalon_ent[\"data\"][\"visualParent\"]\n                _name = _avalon_ent[\"name\"]\n                if _name in self.all_ftrack_names:\n                    av_ent_path_items = list(_avalon_ent[\"data\"][\"parents\"])\n                    av_ent_path_items.append(_name)\n                    av_ent_path = \"/\".join(av_ent_path_items)\n                    # TODO report\n                    # TODO logging\n                    self.log.error((\n                        \"Can't recreate the entity in Ftrack because an entity\"\n                        \" with the same name already exists in a different\"\n                        \" place in the hierarchy <{}>\"\n                    ).format(av_ent_path))\n                    msg = (\n                        \"<Entity not synchronizable> Hierarchy of an entity\"\n                        \" can't be changed. I contains published data and it's\"\n                        \" previous parent had a name, that is duplicated at a \"\n                        \" different hierarchy level\"\n                    )\n                    self.report_items[\"error\"][msg].append(av_ent_path)\n                    self.filter_with_children(ftrack_id)\n                    success = False\n                    break\n\n                entities_to_create.append(_avalon_ent)\n                if _vis_par is None:\n                    break\n\n                _vis_par = str(_vis_par)\n                _mapped = self.avalon_ftrack_mapper.get(_vis_par)\n                if _mapped:\n                    parent_id = _mapped\n                    break\n\n                _avalon_ent = self.avalon_ents_by_id.get(_vis_par)\n                if not _avalon_ent:\n                    _avalon_ent = self.avalon_archived_by_id.get(_vis_par)\n\n            if success is False:\n                continue\n\n            new_entity_id = None\n            for av_entity in reversed(entities_to_create):\n                new_entity_id = self.create_ftrack_ent_from_avalon_ent(\n                    av_entity, parent_id\n                )\n                update_queue.append(new_entity_id)\n\n            if new_entity_id:\n                ftrack_ent_dict[\"entity\"][\"parent_id\"] = new_entity_id\n\n        if hierarchy_changing_ids:\n            self.reload_parents(hierarchy_changing_ids)\n\n        for ftrack_id in self.update_ftrack_ids:\n            if ftrack_id == self.ft_project_id:\n                continue\n\n            avalon_id = self.ftrack_avalon_mapper[ftrack_id]\n            avalon_entity = self.avalon_ents_by_id[avalon_id]\n\n            avalon_attrs = self.entities_dict[ftrack_id][\"avalon_attrs\"]\n            if (\n                CUST_ATTR_ID_KEY not in avalon_attrs or\n                avalon_attrs[CUST_ATTR_ID_KEY] != avalon_id\n            ):\n                configuration_id = self.entities_dict[ftrack_id][\n                    \"avalon_attrs_id\"][CUST_ATTR_ID_KEY]\n\n                _entity_key = collections.OrderedDict([\n                    (\"configuration_id\", configuration_id),\n                    (\"entity_id\", ftrack_id)\n                ])\n\n                self.session.recorded_operations.push(\n                    ftrack_api.operation.UpdateEntityOperation(\n                        \"ContextCustomAttributeValue\",\n                        _entity_key,\n                        \"value\",\n                        ftrack_api.symbol.NOT_SET,\n                        avalon_id\n                    )\n                )\n            # Prepare task changes as they have to be stored as one key\n            final_doc = self.entities_dict[ftrack_id][\"final_entity\"]\n            final_doc_tasks = final_doc[\"data\"].pop(\"tasks\", None) or {}\n            current_doc_tasks = avalon_entity[\"data\"].get(\"tasks\") or {}\n            if not final_doc_tasks:\n                update_tasks = True\n            else:\n                update_tasks = final_doc_tasks != current_doc_tasks\n\n            # check rest of data\n            data_changes = self.compare_dict(\n                final_doc,\n                avalon_entity,\n                ignore_keys[ftrack_id]\n            )\n            if data_changes:\n                self.updates[avalon_id] = self.merge_dicts(\n                    data_changes,\n                    self.updates[avalon_id]\n                )\n\n            # Add tasks back to final doc object\n            final_doc[\"data\"][\"tasks\"] = final_doc_tasks\n            # Add tasks to updates if there are different\n            if update_tasks:\n                if \"data\" not in self.updates[avalon_id]:\n                    self.updates[avalon_id][\"data\"] = {}\n                self.updates[avalon_id][\"data\"][\"tasks\"] = final_doc_tasks\n\n    def synchronize(self):\n        self.log.debug(\"* Synchronization begins\")\n        avalon_project_id = self.ftrack_avalon_mapper.get(self.ft_project_id)\n        if avalon_project_id:\n            self.avalon_project_id = ObjectId(avalon_project_id)\n\n        # remove filtered ftrack ids from create/update list\n        for ftrack_id in self.all_filtered_entities:\n            if ftrack_id in self.create_ftrack_ids:\n                self.create_ftrack_ids.remove(ftrack_id)\n            elif ftrack_id in self.update_ftrack_ids:\n                self.update_ftrack_ids.remove(ftrack_id)\n\n        self.log.debug(\"* Processing entities for archivation\")\n        self.delete_entities()\n\n        self.log.debug(\"* Processing new entities\")\n        # Create not created entities\n        for ftrack_id in self.create_ftrack_ids:\n            # CHECK it is possible that entity was already created\n            # because is parent of another entity which was processed first\n            if ftrack_id not in self.ftrack_avalon_mapper:\n                self.create_avalon_entity(ftrack_id)\n\n        self.set_input_links()\n\n        unarchive_writes = []\n        for item in self.unarchive_list:\n            mongo_id = item[\"_id\"]\n            unarchive_writes.append(ReplaceOne(\n                {\"_id\": mongo_id},\n                item\n            ))\n            av_ent_path_items = list(item[\"data\"][\"parents\"])\n            av_ent_path_items.append(item[\"name\"])\n            av_ent_path = \"/\".join(av_ent_path_items)\n            self.log.debug(\n                \"Entity was unarchived <{}>\".format(av_ent_path)\n            )\n            self.remove_from_archived(mongo_id)\n\n        if unarchive_writes:\n            self.dbcon.bulk_write(unarchive_writes)\n\n        if len(self.create_list) > 0:\n            self.dbcon.insert_many(self.create_list)\n\n        self.session.commit()\n\n        self.log.debug(\"* Processing entities for update\")\n        self.prepare_changes()\n        self.update_entities()\n        self.session.commit()\n\n    def create_avalon_entity(self, ftrack_id):\n        if ftrack_id == self.ft_project_id:\n            self.create_avalon_project()\n            return\n\n        entity_dict = self.entities_dict[ftrack_id]\n        parent_ftrack_id = entity_dict[\"parent_id\"]\n        avalon_parent = None\n        if parent_ftrack_id != self.ft_project_id:\n            avalon_parent = self.ftrack_avalon_mapper.get(parent_ftrack_id)\n            # if not avalon_parent:\n            #     self.create_avalon_entity(parent_ftrack_id)\n            #     avalon_parent = self.ftrack_avalon_mapper[parent_ftrack_id]\n            avalon_parent = ObjectId(avalon_parent)\n\n        # avalon_archived_by_id avalon_archived_by_name\n        current_id = (\n            entity_dict[\"avalon_attrs\"].get(CUST_ATTR_ID_KEY) or \"\"\n        ).strip()\n        mongo_id = current_id\n        name = entity_dict[\"name\"]\n\n        # Check if exist archived asset in mongo - by ID\n        unarchive = False\n        unarchive_id = self.check_unarchivation(ftrack_id, mongo_id, name)\n        if unarchive_id is not None:\n            unarchive = True\n            mongo_id = unarchive_id\n\n        item = entity_dict[\"final_entity\"]\n        try:\n            new_id = ObjectId(mongo_id)\n            if mongo_id in self.avalon_ftrack_mapper:\n                new_id = ObjectId()\n        except InvalidId:\n            new_id = ObjectId()\n\n        item[\"_id\"] = new_id\n        item[\"parent\"] = self.avalon_project_id\n        item[\"schema\"] = CURRENT_ASSET_DOC_SCHEMA\n        item[\"data\"][\"visualParent\"] = avalon_parent\n\n        new_id_str = str(new_id)\n        self.ftrack_avalon_mapper[ftrack_id] = new_id_str\n        self.avalon_ftrack_mapper[new_id_str] = ftrack_id\n\n        self._avalon_ents_by_id[new_id_str] = item\n        self._avalon_ents_by_ftrack_id[ftrack_id] = new_id_str\n        self._avalon_ents_by_name[item[\"name\"]] = new_id_str\n\n        if current_id != new_id_str:\n            # store mongo id to ftrack entity\n            configuration_id = self.hier_cust_attr_ids_by_key.get(\n                CUST_ATTR_ID_KEY\n            )\n            if not configuration_id:\n                # NOTE this is for cases when CUST_ATTR_ID_KEY key is not\n                # hierarchical custom attribute but per entity type\n                configuration_id = self.entities_dict[ftrack_id][\n                    \"avalon_attrs_id\"\n                ][CUST_ATTR_ID_KEY]\n\n            _entity_key = collections.OrderedDict({\n                \"configuration_id\": configuration_id,\n                \"entity_id\": ftrack_id\n            })\n\n            self.session.recorded_operations.push(\n                ftrack_api.operation.UpdateEntityOperation(\n                    \"ContextCustomAttributeValue\",\n                    _entity_key,\n                    \"value\",\n                    ftrack_api.symbol.NOT_SET,\n                    new_id_str\n                )\n            )\n\n        if unarchive is False:\n            self.create_list.append(item)\n        else:\n            self.unarchive_list.append(item)\n\n    def check_unarchivation(self, ftrack_id, mongo_id, name):\n        archived_by_id = self.avalon_archived_by_id.get(mongo_id)\n        archived_by_name = self.avalon_archived_by_name.get(name)\n\n        # if not found in archived then skip\n        if not archived_by_id and not archived_by_name:\n            return None\n\n        entity_dict = self.entities_dict[ftrack_id]\n\n        final_parents = entity_dict[\"final_entity\"][\"data\"][\"parents\"]\n        if archived_by_id:\n            # if is changeable then unarchive (nothing to check here)\n            if self.changeability_by_mongo_id[mongo_id]:\n                return mongo_id\n\n            # TODO replace `__NOTSET__` with custom None constant\n            archived_parent_id = archived_by_id[\"data\"].get(\n                \"visualParent\", \"__NOTSET__\"\n            )\n            archived_parents = archived_by_id[\"data\"].get(\"parents\")\n            archived_name = archived_by_id[\"name\"]\n\n            if (\n                archived_name != entity_dict[\"name\"]\n                or archived_parents != final_parents\n            ):\n                return None\n\n            return mongo_id\n\n        # First check if there is any that have same parents\n        for archived in archived_by_name:\n            mongo_id = str(archived[\"_id\"])\n            archived_parents = archived.get(\"data\", {}).get(\"parents\")\n            if archived_parents == final_parents:\n                return mongo_id\n\n        # Secondly try to find more close to current ftrack entity\n        first_changeable = None\n        for archived in archived_by_name:\n            mongo_id = str(archived[\"_id\"])\n            if not self.changeability_by_mongo_id[mongo_id]:\n                continue\n\n            if first_changeable is None:\n                first_changeable = mongo_id\n\n            ftrack_parent_id = entity_dict[\"parent_id\"]\n            map_ftrack_parent_id = self.ftrack_avalon_mapper.get(\n                ftrack_parent_id\n            )\n\n            # TODO replace `__NOTSET__` with custom None constant\n            archived_parent_id = archived.get(\"data\", {}).get(\n                \"visualParent\", \"__NOTSET__\"\n            )\n            if archived_parent_id is not None:\n                archived_parent_id = str(archived_parent_id)\n\n            # skip if parent is archived - How this should be possible?\n            parent_entity = self.avalon_ents_by_id.get(archived_parent_id)\n            if (\n                parent_entity and (\n                    map_ftrack_parent_id is not None and\n                    map_ftrack_parent_id == str(parent_entity[\"_id\"])\n                )\n            ):\n                return mongo_id\n        # Last return first changeable with same name (or None)\n        return first_changeable\n\n    def create_avalon_project(self):\n        project_item = self.entities_dict[self.ft_project_id][\"final_entity\"]\n        mongo_id = (\n            self.entities_dict[self.ft_project_id][\"avalon_attrs\"].get(\n                CUST_ATTR_ID_KEY\n            ) or \"\"\n        ).strip()\n\n        try:\n            new_id = ObjectId(mongo_id)\n        except InvalidId:\n            new_id = ObjectId()\n\n        project_item[\"_id\"] = new_id\n        project_item[\"parent\"] = None\n        project_item[\"schema\"] = CURRENT_PROJECT_SCHEMA\n        project_item[\"config\"][\"schema\"] = CURRENT_PROJECT_CONFIG_SCHEMA\n\n        self.ftrack_avalon_mapper[self.ft_project_id] = new_id\n        self.avalon_ftrack_mapper[new_id] = self.ft_project_id\n\n        self.avalon_project_id = new_id\n\n        self._avalon_ents_by_id[str(new_id)] = project_item\n        if self._avalon_ents_by_ftrack_id is None:\n            self._avalon_ents_by_ftrack_id = {}\n        self._avalon_ents_by_ftrack_id[self.ft_project_id] = str(new_id)\n        if self._avalon_ents_by_name is None:\n            self._avalon_ents_by_name = {}\n        self._avalon_ents_by_name[project_item[\"name\"]] = str(new_id)\n\n        self.create_list.append(project_item)\n        self.project_created = True\n\n        # store mongo id to ftrack entity\n        entity = self.entities_dict[self.ft_project_id][\"entity\"]\n        entity[\"custom_attributes\"][CUST_ATTR_ID_KEY] = str(new_id)\n\n    def _bubble_changeability(self, unchangeable_ids):\n        unchangeable_queue = collections.deque()\n        for entity_id in unchangeable_ids:\n            unchangeable_queue.append((entity_id, False))\n\n        processed_parents_ids = []\n        subsets_to_remove = []\n        while unchangeable_queue:\n            entity_id, child_is_archived = unchangeable_queue.popleft()\n            # skip if already processed\n            if entity_id in processed_parents_ids:\n                continue\n\n            entity = self.avalon_ents_by_id.get(entity_id)\n            # if entity is not archived but unchageable child was then skip\n            # - archived entities should not affect not archived?\n            if entity and child_is_archived:\n                continue\n\n            # set changeability of current entity to False\n            self._changeability_by_mongo_id[entity_id] = False\n            processed_parents_ids.append(entity_id)\n            # if not entity then is probably archived\n            if not entity:\n                entity = self.avalon_archived_by_id.get(entity_id)\n                child_is_archived = True\n\n            if not entity:\n                # if entity is not found then it is subset without parent\n                if entity_id in unchangeable_ids:\n                    subsets_to_remove.append(entity_id)\n                else:\n                    # TODO logging - What is happening here?\n                    self.log.warning((\n                        \"Avalon contains entities without valid parents that\"\n                        \" lead to Project (should not cause errors)\"\n                        \" - MongoId <{}>\"\n                    ).format(str(entity_id)))\n                continue\n\n            # skip if parent is project\n            parent_id = entity[\"data\"][\"visualParent\"]\n            if parent_id is None:\n                continue\n            unchangeable_queue.append(\n                (str(parent_id), child_is_archived)\n            )\n\n        self._delete_subsets_without_asset(subsets_to_remove)\n\n    def _delete_subsets_without_asset(self, not_existing_parents):\n        repre_ids = []\n        to_delete = []\n\n        subset_ids = []\n        for parent_id in not_existing_parents:\n            subsets = self.subsets_by_parent_id.get(parent_id)\n            if not subsets:\n                continue\n            for subset in subsets:\n                if subset.get(\"type\") == \"subset\":\n                    subset_ids.append(subset[\"_id\"])\n\n        db_versions = get_versions(\n            self.project_name,\n            subset_ids=subset_ids,\n            fields=[\"_id\"]\n        )\n        version_ids = [ver[\"_id\"] for ver in db_versions]\n        db_repres = get_representations(\n            self.project_name,\n            version_ids=version_ids,\n            fields=[\"_id\"]\n        )\n        repre_ids = [repre[\"_id\"] for repre in db_repres]\n\n        to_delete.extend(subset_ids)\n        to_delete.extend(version_ids)\n        to_delete.extend(repre_ids)\n\n        if to_delete:\n            self.dbcon.delete_many({\"_id\": {\"$in\": to_delete}})\n\n    # Probably deprecated\n    def _check_changeability(self, parent_id=None):\n        for entity in self.avalon_ents_by_parent_id[parent_id]:\n            mongo_id = str(entity[\"_id\"])\n            is_changeable = self._changeability_by_mongo_id.get(mongo_id)\n            if is_changeable is not None:\n                continue\n\n            self._check_changeability(mongo_id)\n            is_changeable = True\n            for child in self.avalon_ents_by_parent_id[parent_id]:\n                if not self._changeability_by_mongo_id[str(child[\"_id\"])]:\n                    is_changeable = False\n                    break\n\n            if is_changeable is True:\n                is_changeable = (mongo_id in self.subsets_by_parent_id)\n            self._changeability_by_mongo_id[mongo_id] = is_changeable\n\n    def update_entities(self):\n        \"\"\"\n            Runs changes converted to \"$set\" queries in bulk.\n        \"\"\"\n        mongo_changes_bulk = []\n        for mongo_id, changes in self.updates.items():\n            mongo_id = ObjectId(mongo_id)\n            is_project = mongo_id == self.avalon_project_id\n            change_data = from_dict_to_set(changes, is_project)\n\n            filter = {\"_id\": mongo_id}\n            mongo_changes_bulk.append(UpdateOne(filter, change_data))\n        if not mongo_changes_bulk:\n            # TODO LOG\n            return\n        self.dbcon.bulk_write(mongo_changes_bulk)\n\n    def reload_parents(self, hierarchy_changing_ids):\n        parents_queue = collections.deque()\n        parents_queue.append((self.ft_project_id, [], False))\n        while parents_queue:\n            ftrack_id, parent_parents, changed = parents_queue.popleft()\n            _parents = copy.deepcopy(parent_parents)\n            if ftrack_id not in hierarchy_changing_ids and not changed:\n                if ftrack_id != self.ft_project_id:\n                    _parents.append(self.entities_dict[ftrack_id][\"name\"])\n                for child_id in self.entities_dict[ftrack_id][\"children\"]:\n                    parents_queue.append(\n                        (child_id, _parents, changed)\n                    )\n                continue\n\n            changed = True\n            parents = list(_parents)\n            self.entities_dict[ftrack_id][\n                \"final_entity\"][\"data\"][\"parents\"] = parents\n\n            _parents.append(self.entities_dict[ftrack_id][\"name\"])\n            for child_id in self.entities_dict[ftrack_id][\"children\"]:\n                parents_queue.append(\n                    (child_id, _parents, changed)\n                )\n\n            if ftrack_id in self.create_ftrack_ids:\n                mongo_id = self.ftrack_avalon_mapper[ftrack_id]\n                if \"data\" not in self.updates[mongo_id]:\n                    self.updates[mongo_id][\"data\"] = {}\n                self.updates[mongo_id][\"data\"][\"parents\"] = parents\n\n    def prepare_project_changes(self):\n        ftrack_ent_dict = self.entities_dict[self.ft_project_id]\n        ftrack_entity = ftrack_ent_dict[\"entity\"]\n        avalon_code = self.avalon_project[\"data\"][\"code\"]\n        # TODO Is possible to sync if full name was changed?\n        # if ftrack_ent_dict[\"name\"] != self.avalon_project[\"name\"]:\n        #     ftrack_entity[\"full_name\"] = avalon_name\n        #     self.entities_dict[self.ft_project_id][\"name\"] = avalon_name\n        #     self.entities_dict[self.ft_project_id][\"final_entity\"][\n        #         \"name\"\n        #     ] = avalon_name\n\n        # TODO logging\n        # TODO report\n        # TODO May this happen? Is possible to change project code?\n        if ftrack_entity[\"name\"] != avalon_code:\n            ftrack_entity[\"name\"] = avalon_code\n            self.entities_dict[self.ft_project_id][\"final_entity\"][\"data\"][\n                \"code\"\n            ] = avalon_code\n            self.session.commit()\n            sub_msg = (\n                \"Project code was changed back to \\\"{}\\\"\".format(avalon_code)\n            )\n            msg = (\n                \"It is not possible to change\"\n                \" project code after synchronization\"\n            )\n            self.report_items[\"warning\"][msg] = sub_msg\n            self.log.warning(sub_msg)\n\n        # Compare tasks from current project schema and previous project schema\n        final_doc_data = self.entities_dict[self.ft_project_id][\"final_entity\"]\n        final_doc_tasks = final_doc_data[\"config\"].pop(\"tasks\")\n        current_doc_tasks = self.avalon_project.get(\"config\", {}).get(\"tasks\")\n        # Update project's task types\n        if not current_doc_tasks:\n            update_tasks = True\n        else:\n            # Check if task types are same\n            update_tasks = False\n            for task_type in final_doc_tasks:\n                if task_type not in current_doc_tasks:\n                    update_tasks = True\n                    break\n\n            # Update new task types\n            #   - but keep data about existing types and only add new one\n            if update_tasks:\n                for task_type, type_data in current_doc_tasks.items():\n                    final_doc_tasks[task_type] = type_data\n\n        changes = self.compare_dict(final_doc_data, self.avalon_project)\n\n        # Put back tasks data to final entity object\n        final_doc_data[\"config\"][\"tasks\"] = final_doc_tasks\n\n        # Add tasks updates if tasks changed\n        if update_tasks:\n            if \"config\" not in changes:\n                changes[\"config\"] = {}\n            changes[\"config\"][\"tasks\"] = final_doc_tasks\n        return changes\n\n    def compare_dict(self, dict_new, dict_old, _ignore_keys=[]):\n        \"\"\"\n            Recursively compares and list changes between dictionaries\n            'dict_new' and 'dict_old'.\n            Keys in '_ignore_keys' are skipped and not compared.\n        Args:\n            dict_new (dictionary):\n            dict_old (dictionary):\n            _ignore_keys (list):\n\n        Returns:\n            (dictionary) of new or updated keys and theirs values\n        \"\"\"\n        # _ignore_keys may be used for keys nested dict like\"data.visualParent\"\n        changes = {}\n        ignore_keys = []\n        for key_val in _ignore_keys:\n            key_items = key_val.split(\".\")\n            if len(key_items) == 1:\n                ignore_keys.append(key_items[0])\n\n        for key, value in dict_new.items():\n            if key in ignore_keys:\n                continue\n\n            if key not in dict_old:\n                changes[key] = value\n                continue\n\n            if isinstance(value, dict):\n                if not isinstance(dict_old[key], dict):\n                    changes[key] = value\n                    continue\n\n                _new_ignore_keys = []\n                for key_val in _ignore_keys:\n                    key_items = key_val.split(\".\")\n                    if len(key_items) <= 1:\n                        continue\n                    _new_ignore_keys.append(\".\".join(key_items[1:]))\n\n                _changes = self.compare_dict(\n                    value, dict_old[key], _new_ignore_keys\n                )\n                if _changes:\n                    changes[key] = _changes\n                continue\n\n            if value != dict_old[key]:\n                changes[key] = value\n\n        return changes\n\n    def merge_dicts(self, dict_new, dict_old):\n        \"\"\"\n            Apply all new or updated keys from 'dict_new' on 'dict_old'.\n            Recursively.\n            Doesn't recognise that 'dict_new' doesn't contain some keys\n            anymore.\n        Args:\n            dict_new (dictionary): from Ftrack most likely\n            dict_old (dictionary): current in DB\n\n        Returns:\n            (dictionary) of applied changes to original dictionary\n        \"\"\"\n        for key, value in dict_new.items():\n            if key not in dict_old:\n                dict_old[key] = value\n                continue\n\n            if isinstance(value, dict):\n                dict_old[key] = self.merge_dicts(value, dict_old[key])\n                continue\n\n            dict_old[key] = value\n\n        return dict_old\n\n    def delete_entities(self):\n        if not self.deleted_entities:\n            return\n        # Try to order so child is not processed before parent\n        deleted_entities = []\n        _deleted_entities = [id for id in self.deleted_entities]\n\n        while True:\n            if not _deleted_entities:\n                break\n            _ready = []\n            for mongo_id in _deleted_entities:\n                ent = self.avalon_ents_by_id[mongo_id]\n                vis_par = ent[\"data\"][\"visualParent\"]\n                if (\n                    vis_par is not None and\n                    str(vis_par) in _deleted_entities\n                ):\n                    continue\n                _ready.append(mongo_id)\n\n            for id in _ready:\n                deleted_entities.append(id)\n                _deleted_entities.remove(id)\n\n        delete_ids = []\n        for mongo_id in deleted_entities:\n            # delete if they are deletable\n            if self.changeability_by_mongo_id[mongo_id]:\n                delete_ids.append(ObjectId(mongo_id))\n                continue\n\n            # check if any new created entity match same entity\n            # - name and parents must match\n            deleted_entity = self.avalon_ents_by_id[mongo_id]\n            name = deleted_entity[\"name\"]\n            parents = deleted_entity[\"data\"][\"parents\"]\n            similar_ent_id = None\n            for ftrack_id in self.create_ftrack_ids:\n                _ent_final = self.entities_dict[ftrack_id][\"final_entity\"]\n                if _ent_final[\"name\"] != name:\n                    continue\n                if _ent_final[\"data\"][\"parents\"] != parents:\n                    continue\n\n                # If in create is \"same\" then we can \"archive\" current\n                # since will be unarchived in create method\n                similar_ent_id = ftrack_id\n                break\n\n            # If similar entity(same name and parents) is in create\n            # entities list then just change from create to update\n            if similar_ent_id is not None:\n                self.create_ftrack_ids.remove(similar_ent_id)\n                self.update_ftrack_ids.append(similar_ent_id)\n                self.avalon_ftrack_mapper[mongo_id] = similar_ent_id\n                self.ftrack_avalon_mapper[similar_ent_id] = mongo_id\n                continue\n\n            found_by_name_id = None\n            for ftrack_id, ent_dict in self.entities_dict.items():\n                if not ent_dict.get(\"name\"):\n                    continue\n\n                if name == ent_dict[\"name\"]:\n                    found_by_name_id = ftrack_id\n                    break\n\n            if found_by_name_id is not None:\n                # * THESE conditins are too complex to implement in first stage\n                # - probably not possible to solve if this happen\n                # if found_by_name_id in self.create_ftrack_ids:\n                #     # reparent entity of the new one create?\n                #     pass\n                #\n                # elif found_by_name_id in self.update_ftrack_ids:\n                #     found_mongo_id = self.ftrack_avalon_mapper[found_by_name_id]\n                #\n                # ent_dict = self.entities_dict[found_by_name_id]\n\n                # TODO report - CRITICAL entity with same name already exists\n                #     in different hierarchy - can't recreate entity\n                continue\n\n            _vis_parent = deleted_entity[\"data\"][\"visualParent\"]\n            if _vis_parent is None:\n                _vis_parent = self.avalon_project_id\n            _vis_parent = str(_vis_parent)\n            ftrack_parent_id = self.avalon_ftrack_mapper[_vis_parent]\n            self.create_ftrack_ent_from_avalon_ent(\n                deleted_entity, ftrack_parent_id\n            )\n\n        filter = {\"_id\": {\"$in\": delete_ids}, \"type\": \"asset\"}\n        self.dbcon.update_many(filter, {\"$set\": {\"type\": \"archived_asset\"}})\n\n    def create_ftrack_ent_from_avalon_ent(self, av_entity, parent_id):\n        new_entity = None\n        parent_entity = self.entities_dict[parent_id][\"entity\"]\n\n        _name = av_entity[\"name\"]\n        _type = av_entity[\"data\"].get(\"entityType\")\n        # Check existence of object type\n        if _type and _type not in self.object_types_by_name:\n            _type = None\n\n        if not _type:\n            _type = \"Folder\"\n\n        self.log.debug((\n            \"Re-ceating deleted entity {} <{}>\"\n        ).format(_name, _type))\n\n        new_entity = self.session.create(_type, {\n            \"name\": _name,\n            \"parent\": parent_entity\n        })\n        self.session.commit()\n\n        final_entity = {}\n        for k, v in av_entity.items():\n            final_entity[k] = v\n\n        if final_entity.get(\"type\") != \"asset\":\n            final_entity[\"type\"] = \"asset\"\n\n        new_entity_id = new_entity[\"id\"]\n        new_entity_data = {\n            \"entity\": new_entity,\n            \"parent_id\": parent_id,\n            \"entity_type\": _type.lower(),\n            \"entity_type_orig\": _type,\n            \"name\": _name,\n            \"final_entity\": final_entity\n        }\n        for k, v in new_entity_data.items():\n            self.entities_dict[new_entity_id][k] = v\n\n        p_chilren = self.entities_dict[parent_id][\"children\"]\n        if new_entity_id not in p_chilren:\n            self.entities_dict[parent_id][\"children\"].append(new_entity_id)\n\n        cust_attr, _ = get_openpype_attr(self.session)\n        for _attr in cust_attr:\n            key = _attr[\"key\"]\n            if key not in av_entity[\"data\"]:\n                continue\n\n            if key not in new_entity[\"custom_attributes\"]:\n                continue\n\n            value = av_entity[\"data\"][key]\n            if not value:\n                continue\n\n            new_entity[\"custom_attributes\"][key] = value\n\n        av_entity_id = str(av_entity[\"_id\"])\n        new_entity[\"custom_attributes\"][CUST_ATTR_ID_KEY] = av_entity_id\n\n        self.ftrack_avalon_mapper[new_entity_id] = av_entity_id\n        self.avalon_ftrack_mapper[av_entity_id] = new_entity_id\n\n        self.session.commit()\n\n        ent_path = self.get_ent_path(new_entity_id)\n        msg = (\n            \"Deleted entity was recreated because it or its children\"\n            \" contain published data\"\n        )\n\n        self.report_items[\"info\"][msg].append(ent_path)\n\n        return new_entity_id\n\n    def regex_duplicate_interface(self):\n        items = []\n        if self.failed_regex or self.tasks_failed_regex:\n            subtitle = \"Entity names contain prohibited symbols:\"\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"# {}\".format(subtitle)\n            })\n            items.append({\n                \"type\": \"label\",\n                \"value\": (\n                    \"<p><i>NOTE: You can use Letters( a-Z ),\"\n                    \" Numbers( 0-9 ) and Underscore( _ )</i></p>\"\n                )\n            })\n            log_msgs = []\n            for name, ids in self.failed_regex.items():\n                error_title = {\n                    \"type\": \"label\",\n                    \"value\": \"## {}\".format(name)\n                }\n                items.append(error_title)\n                paths = []\n                for entity_id in ids:\n                    ent_path = self.get_ent_path(entity_id)\n                    paths.append(ent_path)\n\n                error_message = {\n                    \"type\": \"label\",\n                    \"value\": '<p>{}</p>'.format(\"<br>\".join(paths))\n                }\n                items.append(error_message)\n                log_msgs.append(\"<{}> ({})\".format(name, \",\".join(paths)))\n\n            for name, ids in self.tasks_failed_regex.items():\n                error_title = {\n                    \"type\": \"label\",\n                    \"value\": \"## Task: {}\".format(name)\n                }\n                items.append(error_title)\n                paths = []\n                for entity_id in ids:\n                    ent_path = self.get_ent_path(entity_id)\n                    ent_path = \"/\".join([ent_path, name])\n                    paths.append(ent_path)\n\n                error_message = {\n                    \"type\": \"label\",\n                    \"value\": '<p>{}</p>'.format(\"<br>\".join(paths))\n                }\n                items.append(error_message)\n                log_msgs.append(\"<{}> ({})\".format(name, \",\".join(paths)))\n\n            self.log.warning(\"{}{}\".format(subtitle, \", \".join(log_msgs)))\n\n        if self.duplicates:\n            subtitle = \"Duplicated entity names:\"\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"# {}\".format(subtitle)\n            })\n            items.append({\n                \"type\": \"label\",\n                \"value\": (\n                    \"<p><i>NOTE: It is not allowed to use the same name\"\n                    \" for multiple entities in the same project</i></p>\"\n                )\n            })\n            log_msgs = []\n            for name, ids in self.duplicates.items():\n                error_title = {\n                    \"type\": \"label\",\n                    \"value\": \"## {}\".format(name)\n                }\n                items.append(error_title)\n                paths = []\n                for entity_id in ids:\n                    ent_path = self.get_ent_path(entity_id)\n                    paths.append(ent_path)\n\n                error_message = {\n                    \"type\": \"label\",\n                    \"value\": '<p>{}</p>'.format(\"<br>\".join(paths))\n                }\n                items.append(error_message)\n                log_msgs.append(\"<{}> ({})\".format(name, \", \".join(paths)))\n\n            self.log.warning(\"{}{}\".format(subtitle, \", \".join(log_msgs)))\n\n        return items\n\n    def report(self):\n        items = []\n        title = \"Synchronization report ({}):\".format(self.project_name)\n\n        keys = [\"error\", \"warning\", \"info\"]\n        for key in keys:\n            subitems = []\n            if key == \"warning\":\n                for _item in self.regex_duplicate_interface():\n                    subitems.append(_item)\n\n            for msg, _items in self.report_items[key].items():\n                if not _items:\n                    continue\n\n                subitems.append({\n                    \"type\": \"label\",\n                    \"value\": \"# {}\".format(msg)\n                })\n                if isinstance(_items, str):\n                    _items = [_items]\n                subitems.append({\n                    \"type\": \"label\",\n                    \"value\": '<p>{}</p>'.format(\"<br>\".join(_items))\n                })\n\n            if items and subitems:\n                items.append(self.report_splitter)\n\n            items.extend(subitems)\n\n        return {\n            \"items\": items,\n            \"title\": title,\n            \"success\": False,\n            \"message\": \"Synchronization Finished\"\n        }\n"
  },
  {
    "path": "openpype/modules/ftrack/lib/constants.py",
    "content": "# Group name of custom attributes\nCUST_ATTR_GROUP = \"openpype\"\n\n# name of Custom attribute that stores mongo_id from avalon db\nCUST_ATTR_ID_KEY = \"avalon_mongo_id\"\n# Auto sync of project\nCUST_ATTR_AUTO_SYNC = \"avalon_auto_sync\"\n\n# Applications custom attribute name\nCUST_ATTR_APPLICATIONS = \"applications\"\n# Environment tools custom attribute\nCUST_ATTR_TOOLS = \"tools_env\"\n# Intent custom attribute name\nCUST_ATTR_INTENT = \"intent\"\n\nFPS_KEYS = {\n    \"fps\",\n    # For development purposes\n    \"fps_string\"\n}\n"
  },
  {
    "path": "openpype/modules/ftrack/lib/credentials.py",
    "content": "import os\nimport ftrack_api\n\ntry:\n    from urllib.parse import urlparse\nexcept ImportError:\n    from urlparse import urlparse\n\n\nfrom openpype.lib import OpenPypeSecureRegistry\n\nUSERNAME_KEY = \"username\"\nAPI_KEY_KEY = \"api_key\"\n\n\ndef get_ftrack_hostname(ftrack_server=None):\n    if not ftrack_server:\n        ftrack_server = os.environ.get(\"FTRACK_SERVER\")\n\n    if not ftrack_server:\n        return None\n\n    if \"//\" not in ftrack_server:\n        ftrack_server = \"//\" + ftrack_server\n\n    return urlparse(ftrack_server).hostname\n\n\ndef _get_ftrack_secure_key(hostname, key):\n    \"\"\"Secure item key for entered hostname.\"\"\"\n    return \"/\".join((\"ftrack\", hostname, key))\n\n\ndef get_credentials(ftrack_server=None):\n    output = {\n        USERNAME_KEY: None,\n        API_KEY_KEY: None\n    }\n    hostname = get_ftrack_hostname(ftrack_server)\n    if not hostname:\n        return output\n\n    username_name = _get_ftrack_secure_key(hostname, USERNAME_KEY)\n    api_key_name = _get_ftrack_secure_key(hostname, API_KEY_KEY)\n\n    username_registry = OpenPypeSecureRegistry(username_name)\n    api_key_registry = OpenPypeSecureRegistry(api_key_name)\n\n    output[USERNAME_KEY] = username_registry.get_item(USERNAME_KEY, None)\n    output[API_KEY_KEY] = api_key_registry.get_item(API_KEY_KEY, None)\n\n    return output\n\n\ndef save_credentials(username, api_key, ftrack_server=None):\n    hostname = get_ftrack_hostname(ftrack_server)\n    username_name = _get_ftrack_secure_key(hostname, USERNAME_KEY)\n    api_key_name = _get_ftrack_secure_key(hostname, API_KEY_KEY)\n\n    # Clear credentials\n    clear_credentials(ftrack_server)\n\n    username_registry = OpenPypeSecureRegistry(username_name)\n    api_key_registry = OpenPypeSecureRegistry(api_key_name)\n\n    username_registry.set_item(USERNAME_KEY, username)\n    api_key_registry.set_item(API_KEY_KEY, api_key)\n\n\ndef clear_credentials(ftrack_server=None):\n    hostname = get_ftrack_hostname(ftrack_server)\n    username_name = _get_ftrack_secure_key(hostname, USERNAME_KEY)\n    api_key_name = _get_ftrack_secure_key(hostname, API_KEY_KEY)\n\n    username_registry = OpenPypeSecureRegistry(username_name)\n    api_key_registry = OpenPypeSecureRegistry(api_key_name)\n\n    current_username = username_registry.get_item(USERNAME_KEY, None)\n    current_api_key = api_key_registry.get_item(API_KEY_KEY, None)\n\n    if current_username is not None:\n        username_registry.delete_item(USERNAME_KEY)\n\n    if current_api_key is not None:\n        api_key_registry.delete_item(API_KEY_KEY)\n\n\ndef check_credentials(username, api_key, ftrack_server=None):\n    if not ftrack_server:\n        ftrack_server = os.environ.get(\"FTRACK_SERVER\")\n\n    if not ftrack_server or not username or not api_key:\n        return False\n\n    user_exists = False\n    try:\n        session = ftrack_api.Session(\n            server_url=ftrack_server,\n            api_key=api_key,\n            api_user=username\n        )\n        # Validated that the username actually exists\n        user = session.query(\"User where username is \\\"{}\\\"\".format(username))\n        user_exists = user is not None\n        session.close()\n\n    except Exception:\n        pass\n    return user_exists\n"
  },
  {
    "path": "openpype/modules/ftrack/lib/custom_attributes.json",
    "content": "{\n    \"show\": {\n        \"avalon_auto_sync\": {\n          \"label\": \"Avalon auto-sync\",\n          \"type\": \"boolean\"\n        },\n        \"library_project\": {\n          \"label\": \"Library Project\",\n          \"type\": \"boolean\"\n        }\n    },\n    \"is_hierarchical\": {\n        \"fps\": {\n            \"label\": \"FPS\",\n            \"type\": \"number\",\n            \"config\": {\"isdecimal\": true}\n        },\n        \"clipIn\": {\n            \"label\": \"Clip in\",\n            \"type\": \"number\"\n        },\n        \"clipOut\": {\n            \"label\": \"Clip out\",\n            \"type\": \"number\"\n        },\n        \"frameStart\": {\n            \"label\": \"Frame start\",\n            \"type\": \"number\"\n        },\n        \"frameEnd\": {\n            \"label\": \"Frame end\",\n            \"type\": \"number\"\n        },\n        \"resolutionWidth\": {\n            \"label\": \"Resolution Width\",\n            \"type\": \"number\"\n        },\n        \"resolutionHeight\": {\n            \"label\": \"Resolution Height\",\n            \"type\": \"number\"\n        },\n        \"pixelAspect\": {\n            \"label\": \"Pixel aspect\",\n            \"type\": \"number\",\n            \"config\": {\"isdecimal\": true}\n        },\n        \"handleStart\": {\n            \"label\": \"Frame handles start\",\n            \"type\": \"number\"\n        },\n        \"handleEnd\": {\n            \"label\": \"Frame handles end\",\n            \"type\": \"number\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/modules/ftrack/lib/custom_attributes.py",
    "content": "import os\nimport json\n\nfrom .constants import CUST_ATTR_GROUP\n\n\ndef default_custom_attributes_definition():\n    json_file_path = os.path.join(\n        os.path.dirname(os.path.abspath(__file__)),\n        \"custom_attributes.json\"\n    )\n    with open(json_file_path, \"r\") as json_stream:\n        data = json.load(json_stream)\n    return data\n\n\ndef app_definitions_from_app_manager(app_manager):\n    _app_definitions = []\n    for app_name, app in app_manager.applications.items():\n        if app.enabled:\n            _app_definitions.append(\n                (app_name, app.full_label)\n            )\n\n    # Sort items by label\n    app_definitions = []\n    for key, label in sorted(_app_definitions, key=lambda item: item[1]):\n        app_definitions.append({key: label})\n\n    if not app_definitions:\n        app_definitions.append({\"empty\": \"< Empty >\"})\n    return app_definitions\n\n\ndef tool_definitions_from_app_manager(app_manager):\n    _tools_data = []\n    for tool_name, tool in app_manager.tools.items():\n        _tools_data.append(\n            (tool_name, tool.label)\n        )\n\n    # Sort items by label\n    tools_data = []\n    for key, label in sorted(_tools_data, key=lambda item: item[1]):\n        tools_data.append({key: label})\n\n    # Make sure there is at least one item\n    if not tools_data:\n        tools_data.append({\"empty\": \"< Empty >\"})\n    return tools_data\n\n\ndef get_openpype_attr(session, split_hierarchical=True, query_keys=None):\n    custom_attributes = []\n    hier_custom_attributes = []\n    if not query_keys:\n        query_keys = [\n            \"id\",\n            \"entity_type\",\n            \"object_type_id\",\n            \"is_hierarchical\",\n            \"default\"\n        ]\n    # TODO remove deprecated \"pype\" group from query\n    cust_attrs_query = (\n        \"select {}\"\n        \" from CustomAttributeConfiguration\"\n        # Kept `pype` for Backwards Compatibility\n        \" where group.name in (\\\"pype\\\", \\\"ayon\\\", \\\"{}\\\")\"\n    ).format(\", \".join(query_keys), CUST_ATTR_GROUP)\n    all_avalon_attr = session.query(cust_attrs_query).all()\n    for cust_attr in all_avalon_attr:\n        if split_hierarchical and cust_attr[\"is_hierarchical\"]:\n            hier_custom_attributes.append(cust_attr)\n            continue\n\n        custom_attributes.append(cust_attr)\n\n    if split_hierarchical:\n        # return tuple\n        return custom_attributes, hier_custom_attributes\n\n    return custom_attributes\n\n\ndef join_query_keys(keys):\n    \"\"\"Helper to join keys to query.\"\"\"\n    return \",\".join([\"\\\"{}\\\"\".format(key) for key in keys])\n\n\ndef query_custom_attributes(\n    session, conf_ids, entity_ids, only_set_values=False\n):\n    \"\"\"Query custom attribute values from ftrack database.\n\n    Using ftrack call method result may differ based on used table name and\n    version of ftrack server.\n\n    For hierarchical attributes you shou always use `only_set_values=True`\n    otherwise result will be default value of custom attribute and it would not\n    be possible to differentiate if value is set on entity or default value is\n    used.\n\n    Args:\n        session(ftrack_api.Session): Connected ftrack session.\n        conf_id(list, set, tuple): Configuration(attribute) ids which are\n            queried.\n        entity_ids(list, set, tuple): Entity ids for which are values queried.\n        only_set_values(bool): Entities that don't have explicitly set\n            value won't return a value. If is set to False then default custom\n            attribute value is returned if value is not set.\n    \"\"\"\n    output = []\n    # Just skip\n    if not conf_ids or not entity_ids:\n        return output\n\n    if only_set_values:\n        table_name = \"CustomAttributeValue\"\n    else:\n        table_name = \"ContextCustomAttributeValue\"\n\n    # Prepare values to query\n    attributes_joined = join_query_keys(conf_ids)\n    attributes_len = len(conf_ids)\n\n    # Query values in chunks\n    chunk_size = int(5000 / attributes_len)\n    # Make sure entity_ids is `list` for chunk selection\n    entity_ids = list(entity_ids)\n    for idx in range(0, len(entity_ids), chunk_size):\n        entity_ids_joined = join_query_keys(\n            entity_ids[idx:idx + chunk_size]\n        )\n        output.extend(\n            session.query(\n                (\n                    \"select value, entity_id, configuration_id from {}\"\n                    \" where entity_id in ({}) and configuration_id in ({})\"\n                ).format(\n                    table_name,\n                    entity_ids_joined,\n                    attributes_joined\n                )\n            ).all()\n        )\n    return output\n"
  },
  {
    "path": "openpype/modules/ftrack/lib/ftrack_action_handler.py",
    "content": "import os\nfrom .ftrack_base_handler import BaseHandler\n\n\ndef statics_icon(*icon_statics_file_parts):\n    statics_server = os.environ.get(\"OPENPYPE_STATICS_SERVER\")\n    if not statics_server:\n        return None\n    return \"/\".join((statics_server, *icon_statics_file_parts))\n\n\nclass BaseAction(BaseHandler):\n    '''Custom Action base class\n\n    `label` a descriptive string identifying your action.\n\n    `varaint` To group actions together, give them the same\n    label and specify a unique variant per action.\n\n    `identifier` a unique identifier for your action.\n\n    `description` a verbose descriptive text for you action\n\n     '''\n    label = None\n    variant = None\n    identifier = None\n    description = None\n    icon = None\n    type = 'Action'\n\n    _discover_identifier = None\n    _launch_identifier = None\n\n    settings_frack_subkey = \"user_handlers\"\n    settings_enabled_key = \"enabled\"\n\n    def __init__(self, session):\n        '''Expects a ftrack_api.Session instance'''\n        if self.label is None:\n            raise ValueError('Action missing label.')\n\n        if self.identifier is None:\n            raise ValueError('Action missing identifier.')\n\n        super().__init__(session)\n\n    @property\n    def discover_identifier(self):\n        if self._discover_identifier is None:\n            self._discover_identifier = \"{}.{}\".format(\n                self.identifier, self.process_identifier()\n            )\n        return self._discover_identifier\n\n    @property\n    def launch_identifier(self):\n        if self._launch_identifier is None:\n            self._launch_identifier = \"{}.{}\".format(\n                self.identifier, self.process_identifier()\n            )\n        return self._launch_identifier\n\n    def register(self):\n        '''\n        Registers the action, subscribing the the discover and launch topics.\n        - highest priority event will show last\n        '''\n        self.session.event_hub.subscribe(\n            'topic=ftrack.action.discover and source.user.username={0}'.format(\n                self.session.api_user\n            ),\n            self._discover,\n            priority=self.priority\n        )\n\n        launch_subscription = (\n            'topic=ftrack.action.launch'\n            ' and data.actionIdentifier={0}'\n            ' and source.user.username={1}'\n        ).format(\n            self.launch_identifier,\n            self.session.api_user\n        )\n        self.session.event_hub.subscribe(\n            launch_subscription,\n            self._launch\n        )\n\n    def _discover(self, event):\n        entities = self._translate_event(event)\n        if not entities:\n            return\n\n        accepts = self.discover(self.session, entities, event)\n        if not accepts:\n            return\n\n        self.log.debug(u'Discovering action with selection: {0}'.format(\n            event['data'].get('selection', [])\n        ))\n\n        return {\n            'items': [{\n                'label': self.label,\n                'variant': self.variant,\n                'description': self.description,\n                'actionIdentifier': self.discover_identifier,\n                'icon': self.icon,\n            }]\n        }\n\n    def discover(self, session, entities, event):\n        '''Return true if we can handle the selected entities.\n\n        *session* is a `ftrack_api.Session` instance\n\n\n        *entities* is a list of tuples each containing the entity type and the\n        entity id. If the entity is a hierarchical you will always get the\n        entity type TypedContext, once retrieved through a get operation you\n        will have the \"real\" entity type ie. example Shot, Sequence\n        or Asset Build.\n\n        *event* the unmodified original event\n\n        '''\n\n        return False\n\n    def _interface(self, session, entities, event):\n        interface = self.interface(session, entities, event)\n        if not interface:\n            return\n\n        if isinstance(interface, (tuple, list)):\n            return {\"items\": interface}\n\n        if isinstance(interface, dict):\n            if (\n                \"items\" in interface\n                or (\"success\" in interface and \"message\" in interface)\n            ):\n                return interface\n\n            raise ValueError((\n                \"Invalid interface output expected key: \\\"items\\\" or keys:\"\n                \" \\\"success\\\" and \\\"message\\\". Got: \\\"{}\\\"\"\n            ).format(str(interface)))\n\n        raise ValueError(\n            \"Invalid interface output type \\\"{}\\\"\".format(\n                str(type(interface))\n            )\n        )\n\n    def interface(self, session, entities, event):\n        '''Return a interface if applicable or None\n\n        *session* is a `ftrack_api.Session` instance\n\n        *entities* is a list of tuples each containing the entity type and\n        the entity id. If the entity is a hierarchical you will always get the\n        entity type TypedContext, once retrieved through a get operation you\n        will have the \"real\" entity type ie. example Shot, Sequence\n        or Asset Build.\n\n        *event* the unmodified original event\n        '''\n        return None\n\n    def _launch(self, event):\n        entities = self._translate_event(event)\n        if not entities:\n            return\n\n        preactions_launched = self._handle_preactions(self.session, event)\n        if preactions_launched is False:\n            return\n\n        interface = self._interface(self.session, entities, event)\n        if interface:\n            return interface\n\n        response = self.launch(self.session, entities, event)\n\n        return self._handle_result(response)\n\n    def _handle_result(self, result):\n        '''Validate the returned result from the action callback'''\n        if isinstance(result, bool):\n            if result is True:\n                msg = 'Action {0} finished.'\n            else:\n                msg = 'Action {0} failed.'\n\n            return {\n                'success': result,\n                'message': msg.format(self.label)\n            }\n\n        if isinstance(result, dict):\n            if 'items' in result:\n                if not isinstance(result['items'], list):\n                    raise ValueError('Invalid items format, must be list!')\n\n            else:\n                for key in ('success', 'message'):\n                    if key not in result:\n                        raise KeyError(\n                            \"Missing required key: {0}.\".format(key)\n                        )\n            return result\n\n        self.log.warning((\n            'Invalid result type \\\"{}\\\" must be bool or dictionary!'\n        ).format(str(type(result))))\n\n        return result\n\n    @staticmethod\n    def roles_check(settings_roles, user_roles, default=True):\n        \"\"\"Compare roles from setting and user's roles.\n\n        Args:\n            settings_roles(list): List of role names from settings.\n            user_roles(list): User's lowered role names.\n            default(bool): If `settings_roles` is empty list.\n\n        Returns:\n            bool: `True` if user has at least one role from settings or\n                default if `settings_roles` is empty.\n        \"\"\"\n        if not settings_roles:\n            return default\n\n        user_roles = {\n            role_name.lower()\n            for role_name in user_roles\n        }\n        for role_name in settings_roles:\n            if role_name.lower() in user_roles:\n                return True\n        return False\n\n    @classmethod\n    def get_user_entity_from_event(cls, session, event):\n        \"\"\"Query user entity from event.\"\"\"\n        not_set = object()\n\n        # Check if user is already stored in event data\n        user_entity = event[\"data\"].get(\"user_entity\", not_set)\n        if user_entity is not_set:\n            # Query user entity from event\n            user_info = event.get(\"source\", {}).get(\"user\", {})\n            user_id = user_info.get(\"id\")\n            username = user_info.get(\"username\")\n            if user_id:\n                user_entity = session.query(\n                    \"User where id is {}\".format(user_id)\n                ).first()\n            if not user_entity and username:\n                user_entity = session.query(\n                    \"User where username is {}\".format(username)\n                ).first()\n            event[\"data\"][\"user_entity\"] = user_entity\n\n        return user_entity\n\n    @classmethod\n    def get_user_roles_from_event(cls, session, event, lower=True):\n        \"\"\"Get user roles based on data in event.\n\n        Args:\n            session (ftrack_api.Session): Prepared ftrack session.\n            event (ftrack_api.event.Event): Event which is processed.\n            lower (Optional[bool]): Lower the role names. Default 'True'.\n        \"\"\"\n\n        not_set = object()\n\n        user_roles = event[\"data\"].get(\"user_roles\", not_set)\n        if user_roles is not_set:\n            user_roles = []\n            user_entity = cls.get_user_entity_from_event(session, event)\n            for role in user_entity[\"user_security_roles\"]:\n                role_name = role[\"security_role\"][\"name\"]\n                if lower:\n                    role_name = role_name.lower()\n                user_roles.append(role_name)\n            event[\"data\"][\"user_roles\"] = user_roles\n        return user_roles\n\n    def get_project_name_from_event(self, session, event, entities):\n        \"\"\"Load or query and fill project entity from/to event data.\n\n        Project data are stored by ftrack id because in most cases it is\n        easier to access project id than project name.\n\n        Args:\n            session (ftrack_api.Session): Current session.\n            event (ftrack_api.Event): Processed event by session.\n            entities (list): Ftrack entities of selection.\n        \"\"\"\n\n        # Try to get project entity from event\n        project_name = event[\"data\"].get(\"project_name\")\n        if not project_name:\n            project_entity = self.get_project_from_entity(\n                entities[0], session\n            )\n            project_name = project_entity[\"full_name\"]\n\n            event[\"data\"][\"project_name\"] = project_name\n        return project_name\n\n    def get_ftrack_settings(self, session, event, entities):\n        project_name = self.get_project_name_from_event(\n            session, event, entities\n        )\n        project_settings = self.get_project_settings_from_event(\n            event, project_name\n        )\n        return project_settings[\"ftrack\"]\n\n    def valid_roles(self, session, entities, event):\n        \"\"\"Validate user roles by settings.\n\n        Method requires to have set `settings_key` attribute.\n        \"\"\"\n        ftrack_settings = self.get_ftrack_settings(session, event, entities)\n        settings = (\n            ftrack_settings[self.settings_frack_subkey][self.settings_key]\n        )\n        if self.settings_enabled_key:\n            if not settings.get(self.settings_enabled_key, True):\n                return False\n\n        user_role_list = self.get_user_roles_from_event(\n            session, event, lower=False)\n        if not self.roles_check(settings.get(\"role_list\"), user_role_list):\n            return False\n        return True\n\n\nclass LocalAction(BaseAction):\n    \"\"\"Action that warn user when more Processes with same action are running.\n\n    Action is launched all the time but if id does not match id of current\n    instanace then message is shown to user.\n\n    Handy for actions where matters if is executed on specific machine.\n    \"\"\"\n    _full_launch_identifier = None\n\n    @property\n    def discover_identifier(self):\n        if self._discover_identifier is None:\n            self._discover_identifier = \"{}.{}\".format(\n                self.identifier, self.process_identifier()\n            )\n        return self._discover_identifier\n\n    @property\n    def launch_identifier(self):\n        \"\"\"Catch all topics with same identifier.\"\"\"\n        if self._launch_identifier is None:\n            self._launch_identifier = \"{}.*\".format(self.identifier)\n        return self._launch_identifier\n\n    @property\n    def full_launch_identifier(self):\n        \"\"\"Catch all topics with same identifier.\"\"\"\n        if self._full_launch_identifier is None:\n            self._full_launch_identifier = \"{}.{}\".format(\n                self.identifier, self.process_identifier()\n            )\n        return self._full_launch_identifier\n\n    def _discover(self, event):\n        entities = self._translate_event(event)\n        if not entities:\n            return\n\n        accepts = self.discover(self.session, entities, event)\n        if not accepts:\n            return\n\n        self.log.debug(\"Discovering action with selection: {0}\".format(\n            event[\"data\"].get(\"selection\", [])\n        ))\n\n        return {\n            \"items\": [{\n                \"label\": self.label,\n                \"variant\": self.variant,\n                \"description\": self.description,\n                \"actionIdentifier\": self.discover_identifier,\n                \"icon\": self.icon,\n            }]\n        }\n\n    def _launch(self, event):\n        event_identifier = event[\"data\"][\"actionIdentifier\"]\n        # Check if identifier is same\n        # - show message that acion may not be triggered on this machine\n        if event_identifier != self.full_launch_identifier:\n            return {\n                \"success\": False,\n                \"message\": (\n                    \"There are running more OpenPype processes\"\n                    \" where this action could be launched.\"\n                )\n            }\n        return super(LocalAction, self)._launch(event)\n\n\nclass ServerAction(BaseAction):\n    \"\"\"Action class meant to be used on event server.\n\n    Unlike the `BaseAction` roles are not checked on register but on discover.\n    For the same reason register is modified to not filter topics by username.\n    \"\"\"\n\n    settings_frack_subkey = \"events\"\n\n    @property\n    def discover_identifier(self):\n        return self.identifier\n\n    @property\n    def launch_identifier(self):\n        return self.identifier\n\n    def register(self):\n        \"\"\"Register subcription to Ftrack event hub.\"\"\"\n        self.session.event_hub.subscribe(\n            \"topic=ftrack.action.discover\",\n            self._discover,\n            priority=self.priority\n        )\n\n        launch_subscription = (\n            \"topic=ftrack.action.launch and data.actionIdentifier={0}\"\n        ).format(self.launch_identifier)\n        self.session.event_hub.subscribe(launch_subscription, self._launch)\n"
  },
  {
    "path": "openpype/modules/ftrack/lib/ftrack_base_handler.py",
    "content": "import os\nimport tempfile\nimport json\nimport functools\nimport uuid\nimport datetime\nimport traceback\nimport time\nfrom openpype.lib import Logger\nfrom openpype.settings import get_project_settings\n\nimport ftrack_api\nfrom openpype_modules.ftrack import ftrack_server\n\n\nclass MissingPermision(Exception):\n    def __init__(self, message=None):\n        if message is None:\n            message = 'Ftrack'\n        super().__init__(message)\n\n\nclass PreregisterException(Exception):\n    def __init__(self, message=None):\n        if not message:\n            message = \"Pre-registration conditions were not met\"\n        super().__init__(message)\n\n\nclass BaseHandler(object):\n    '''Custom Action base class\n\n    <label> - a descriptive string identifying your action.\n    <varaint>   - To group actions together, give them the same\n                  label and specify a unique variant per action.\n    <identifier>  - a unique identifier for app.\n    <description>   - a verbose descriptive text for you action\n    <icon>  - icon in ftrack\n    '''\n    _process_id = None\n    # Default priority is 100\n    priority = 100\n    # Type is just for logging purpose (e.g.: Action, Event, Application,...)\n    type = 'No-type'\n    ignore_me = False\n    preactions = []\n\n    @staticmethod\n    def join_query_keys(keys):\n        \"\"\"Helper to join keys to query.\"\"\"\n        return \",\".join([\"\\\"{}\\\"\".format(key) for key in keys])\n\n    def __init__(self, session):\n        '''Expects a ftrack_api.Session instance'''\n        self.log = Logger.get_logger(self.__class__.__name__)\n        if not(\n            isinstance(session, ftrack_api.session.Session) or\n            isinstance(session, ftrack_server.lib.SocketSession)\n        ):\n            raise Exception((\n                \"Session object entered with args is instance of \\\"{}\\\"\"\n                \" but expected instances are \\\"{}\\\" and \\\"{}\\\"\"\n            ).format(\n                str(type(session)),\n                str(ftrack_api.session.Session),\n                str(ftrack_server.lib.SocketSession)\n            ))\n\n        self._session = session\n\n        # Using decorator\n        self.register = self.register_decorator(self.register)\n        self.launch = self.launch_log(self.launch)\n\n    @staticmethod\n    def process_identifier():\n        \"\"\"Helper property to have \"\"\"\n        if not BaseHandler._process_id:\n            BaseHandler._process_id = str(uuid.uuid4())\n        return BaseHandler._process_id\n\n    # Decorator\n    def register_decorator(self, func):\n        @functools.wraps(func)\n        def wrapper_register(*args, **kwargs):\n            if self.ignore_me:\n                return\n\n            label = getattr(self, \"label\", self.__class__.__name__)\n            variant = getattr(self, \"variant\", None)\n            if variant:\n                label = \"{} {}\".format(label, variant)\n\n            try:\n                self._preregister()\n\n                start_time = time.perf_counter()\n                func(*args, **kwargs)\n                end_time = time.perf_counter()\n                run_time = end_time - start_time\n                self.log.info((\n                    '{} \"{}\" - Registered successfully ({:.4f}sec)'\n                ).format(self.type, label, run_time))\n            except MissingPermision as MPE:\n                self.log.info((\n                    '!{} \"{}\" - You\\'re missing required {} permissions'\n                ).format(self.type, label, str(MPE)))\n            except AssertionError as ae:\n                self.log.warning((\n                    '!{} \"{}\" - {}'\n                ).format(self.type, label, str(ae)))\n            except NotImplementedError:\n                self.log.error((\n                    '{} \"{}\" - Register method is not implemented'\n                ).format(self.type, label))\n            except PreregisterException as exc:\n                self.log.warning((\n                    '{} \"{}\" - {}'\n                ).format(self.type, label, str(exc)))\n            except Exception as e:\n                self.log.error('{} \"{}\" - Registration failed ({})'.format(\n                    self.type, label, str(e))\n                )\n        return wrapper_register\n\n    # Decorator\n    def launch_log(self, func):\n        @functools.wraps(func)\n        def wrapper_launch(*args, **kwargs):\n            label = getattr(self, \"label\", self.__class__.__name__)\n            variant = getattr(self, \"variant\", None)\n            if variant:\n                label = \"{} {}\".format(label, variant)\n\n            self.log.info(('{} \"{}\": Launched').format(self.type, label))\n            try:\n                return func(*args, **kwargs)\n            except Exception as exc:\n                self.session.rollback()\n                msg = '{} \"{}\": Failed ({})'.format(self.type, label, str(exc))\n                self.log.error(msg, exc_info=True)\n                return {\n                    'success': False,\n                    'message': msg\n                }\n            finally:\n                self.log.info(('{} \"{}\": Finished').format(self.type, label))\n        return wrapper_launch\n\n    @property\n    def session(self):\n        '''Return current session.'''\n        return self._session\n\n    def reset_session(self):\n        self.session.reset()\n\n    def _preregister(self):\n        # Custom validations\n        result = self.preregister()\n        if result is None:\n            self.log.debug((\n                \"\\\"{}\\\" 'preregister' method returned 'None'. Expected it\"\n                \" didn't fail and continue as preregister returned True.\"\n            ).format(self.__class__.__name__))\n            return\n\n        if result is not True:\n            msg = None\n            if isinstance(result, str):\n                msg = result\n            raise PreregisterException(msg)\n\n    def preregister(self):\n        '''\n        Preregister conditions.\n        Registration continues if returns True.\n        '''\n        return True\n\n    def register(self):\n        '''\n        Registers the action, subscribing the discover and launch topics.\n        Is decorated by register_log\n        '''\n\n        raise NotImplementedError()\n\n    def _translate_event(self, event, session=None):\n        '''Return *event* translated structure to be used with the API.'''\n        if session is None:\n            session = self.session\n\n        _entities = event[\"data\"].get(\"entities_object\", None)\n        if _entities is not None and not _entities:\n            return _entities\n\n        if (\n            _entities is None\n            or _entities[0].get(\n                \"link\", None\n            ) == ftrack_api.symbol.NOT_SET\n        ):\n            _entities = [\n                item\n                for item in self._get_entities(event)\n                if item is not None\n            ]\n            event[\"data\"][\"entities_object\"] = _entities\n\n        return _entities\n\n    def _get_entities(self, event, session=None, ignore=None):\n        entities = []\n        selection = event['data'].get('selection')\n        if not selection:\n            return entities\n\n        if ignore is None:\n            ignore = []\n        elif isinstance(ignore, str):\n            ignore = [ignore]\n\n        filtered_selection = []\n        for entity in selection:\n            if entity['entityType'] not in ignore:\n                filtered_selection.append(entity)\n\n        if not filtered_selection:\n            return entities\n\n        if session is None:\n            session = self.session\n            session._local_cache.clear()\n\n        for entity in filtered_selection:\n            entities.append(session.get(\n                self._get_entity_type(entity, session),\n                entity.get('entityId')\n            ))\n\n        return entities\n\n    def _get_entity_type(self, entity, session=None):\n        '''Return translated entity type that can be used with API.'''\n        # Get entity type and make sure it is lower cased. Most places except\n        # the component tab in the Sidebar will use lower case notation.\n        entity_type = entity.get('entityType').replace('_', '').lower()\n\n        if session is None:\n            session = self.session\n\n        for schema in self.session.schemas:\n            alias_for = schema.get('alias_for')\n\n            if (\n                alias_for and isinstance(alias_for, str) and\n                alias_for.lower() == entity_type\n            ):\n                return schema['id']\n\n        for schema in self.session.schemas:\n            if schema['id'].lower() == entity_type:\n                return schema['id']\n\n        raise ValueError(\n            'Unable to translate entity type: {0}.'.format(entity_type)\n        )\n\n    def _launch(self, event):\n        self.session.rollback()\n        self.session._local_cache.clear()\n\n        self.launch(self.session, event)\n\n    def launch(self, session, event):\n        '''Callback method for the custom action.\n\n        return either a bool ( True if successful or False if the action failed )\n        or a dictionary with they keys `message` and `success`, the message should be a\n        string and will be displayed as feedback to the user, success should be a bool,\n        True if successful or False if the action failed.\n\n        *session* is a `ftrack_api.Session` instance\n\n        *entities* is a list of tuples each containing the entity type and the entity id.\n        If the entity is a hierarchical you will always get the entity\n        type TypedContext, once retrieved through a get operation you\n        will have the \"real\" entity type ie. example Shot, Sequence\n        or Asset Build.\n\n        *event* the unmodified original event\n\n        '''\n        raise NotImplementedError()\n\n    def _handle_preactions(self, session, event):\n        # If preactions are not set\n        if len(self.preactions) == 0:\n            return True\n        # If no selection\n        selection = event.get('data', {}).get('selection', None)\n        if (selection is None):\n            return False\n        # If preactions were already started\n        if event['data'].get('preactions_launched', None) is True:\n            return True\n\n        # Launch preactions\n        for preaction in self.preactions:\n            self.trigger_action(preaction, event)\n\n        # Relaunch this action\n        additional_data = {\"preactions_launched\": True}\n        self.trigger_action(\n            self.identifier, event, additional_event_data=additional_data\n        )\n\n        return False\n\n    def _handle_result(self, result):\n        '''Validate the returned result from the action callback'''\n        if isinstance(result, bool):\n            if result is True:\n                result = {\n                    'success': result,\n                    'message': (\n                        '{0} launched successfully.'.format(self.label)\n                    )\n                }\n            else:\n                result = {\n                    'success': result,\n                    'message': (\n                        '{0} launch failed.'.format(self.label)\n                    )\n                }\n\n        elif isinstance(result, dict):\n            items = 'items' in result\n            if items is False:\n                for key in ('success', 'message'):\n                    if key in result:\n                        continue\n\n                    raise KeyError(\n                        'Missing required key: {0}.'.format(key)\n                    )\n\n        return result\n\n    def show_message(self, event, input_message, result=False):\n        \"\"\"\n        Shows message to user who triggered event\n        - event - just source of user id\n        - input_message - message that is shown to user\n        - result - changes color of message (based on ftrack settings)\n            - True = Violet\n            - False = Red\n        \"\"\"\n        if not isinstance(result, bool):\n            result = False\n\n        try:\n            message = str(input_message)\n        except Exception:\n            return\n\n        user_id = event['source']['user']['id']\n        target = (\n            'applicationId=ftrack.client.web and user.id=\"{0}\"'\n        ).format(user_id)\n        self.session.event_hub.publish(\n            ftrack_api.event.base.Event(\n                topic='ftrack.action.trigger-user-interface',\n                data=dict(\n                    type='message',\n                    success=result,\n                    message=message\n                ),\n                target=target\n            ),\n            on_error='ignore'\n        )\n\n    def show_interface(\n        self, items, title=\"\", event=None, user=None,\n        username=None, user_id=None, submit_btn_label=None\n    ):\n        \"\"\"\n        Shows interface to user\n        - to identify user must be entered one of args:\n            event, user, username, user_id\n        - 'items' must be list containing Ftrack interface items\n        \"\"\"\n        if not any([event, user, username, user_id]):\n            raise TypeError((\n                'Missing argument `show_interface` requires one of args:'\n                ' event (ftrack_api Event object),'\n                ' user (ftrack_api User object)'\n                ' username (string) or user_id (string)'\n            ))\n\n        if event:\n            user_id = event['source']['user']['id']\n        elif user:\n            user_id = user['id']\n        else:\n            if user_id:\n                key = 'id'\n                value = user_id\n            else:\n                key = 'username'\n                value = username\n\n            user = self.session.query(\n                'User where {} is \"{}\"'.format(key, value)\n            ).first()\n\n            if not user:\n                raise TypeError((\n                    'Ftrack user with {} \"{}\" was not found!'\n                ).format(key, value))\n\n            user_id = user['id']\n\n        target = (\n            'applicationId=ftrack.client.web and user.id=\"{0}\"'\n        ).format(user_id)\n\n        event_data = {\n            \"type\": \"widget\",\n            \"items\": items,\n            \"title\": title\n        }\n        if submit_btn_label:\n            event_data[\"submit_button_label\"] = submit_btn_label\n\n        self.session.event_hub.publish(\n            ftrack_api.event.base.Event(\n                topic='ftrack.action.trigger-user-interface',\n                data=event_data,\n                target=target\n            ),\n            on_error='ignore'\n        )\n\n    def show_interface_from_dict(\n        self, messages, title=\"\", event=None,\n        user=None, username=None, user_id=None, submit_btn_label=None\n    ):\n        if not messages:\n            self.log.debug(\"No messages to show! (messages dict is empty)\")\n            return\n        items = []\n        splitter = {'type': 'label', 'value': '---'}\n        first = True\n        for key, value in messages.items():\n            if not first:\n                items.append(splitter)\n            else:\n                first = False\n\n            subtitle = {'type': 'label', 'value': '<h3>{}</h3>'.format(key)}\n            items.append(subtitle)\n            if isinstance(value, list):\n                for item in value:\n                    message = {\n                        'type': 'label', 'value': '<p>{}</p>'.format(item)\n                    }\n                    items.append(message)\n            else:\n                message = {'type': 'label', 'value': '<p>{}</p>'.format(value)}\n                items.append(message)\n\n        self.show_interface(\n            items, title, event, user, username, user_id, submit_btn_label\n        )\n\n    def trigger_action(\n        self, action_name, event=None, session=None,\n        selection=None, user_data=None,\n        topic=\"ftrack.action.launch\", additional_event_data={},\n        on_error=\"ignore\"\n    ):\n        self.log.debug(\"Triggering action \\\"{}\\\" Begins\".format(action_name))\n\n        if not session:\n            session = self.session\n\n        # Getting selection and user data\n        _selection = None\n        _user_data = None\n\n        if event:\n            _selection = event.get(\"data\", {}).get(\"selection\")\n            _user_data = event.get(\"source\", {}).get(\"user\")\n\n        if selection is not None:\n            _selection = selection\n\n        if user_data is not None:\n            _user_data = user_data\n\n        # Without selection and user data skip triggering\n        msg = \"Can't trigger \\\"{}\\\" action without {}.\"\n        if _selection is None:\n            self.log.error(msg.format(action_name, \"selection\"))\n            return\n\n        if _user_data is None:\n            self.log.error(msg.format(action_name, \"user data\"))\n            return\n\n        _event_data = {\n            \"actionIdentifier\": action_name,\n            \"selection\": _selection\n        }\n\n        # Add additional data\n        if additional_event_data:\n            _event_data.update(additional_event_data)\n\n        # Create and trigger event\n        session.event_hub.publish(\n            ftrack_api.event.base.Event(\n                topic=topic,\n                data=_event_data,\n                source=dict(user=_user_data)\n            ),\n            on_error=on_error\n        )\n        self.log.debug(\n            \"Action \\\"{}\\\" Triggered successfully\".format(action_name)\n        )\n\n    def trigger_event(\n        self, topic, event_data=None, session=None, source=None,\n        event=None, on_error=\"ignore\"\n    ):\n        if session is None:\n            session = self.session\n\n        if not source and event:\n            source = event.get(\"source\")\n\n        if event_data is None:\n            event_data = {}\n        # Create and trigger event\n        event = ftrack_api.event.base.Event(\n            topic=topic,\n            data=event_data,\n            source=source\n        )\n        session.event_hub.publish(event, on_error=on_error)\n\n        self.log.debug((\n            \"Publishing event: {}\"\n        ).format(str(event.__dict__)))\n\n    def get_project_from_entity(self, entity, session=None):\n        low_entity_type = entity.entity_type.lower()\n        if low_entity_type == \"project\":\n            return entity\n\n        if \"project\" in entity:\n            # reviewsession, task(Task, Shot, Sequence,...)\n            return entity[\"project\"]\n\n        if low_entity_type == \"filecomponent\":\n            entity = entity[\"version\"]\n            low_entity_type = entity.entity_type.lower()\n\n        if low_entity_type == \"assetversion\":\n            asset = entity[\"asset\"]\n            parent = None\n            if asset:\n                parent = asset[\"parent\"]\n\n            if parent:\n                if parent.entity_type.lower() == \"project\":\n                    return parent\n\n                if \"project\" in parent:\n                    return parent[\"project\"]\n\n        project_data = entity[\"link\"][0]\n\n        if session is None:\n            session = self.session\n        return session.query(\n            \"Project where id is {}\".format(project_data[\"id\"])\n        ).one()\n\n    def get_project_settings_from_event(self, event, project_name):\n        \"\"\"Load or fill OpenPype's project settings from event data.\n\n        Project data are stored by ftrack id because in most cases it is\n        easier to access project id than project name.\n\n        Args:\n            event (ftrack_api.Event): Processed event by session.\n            project_entity (ftrack_api.Entity): Project entity.\n        \"\"\"\n        project_settings_by_id = event[\"data\"].get(\"project_settings\")\n        if not project_settings_by_id:\n            project_settings_by_id = {}\n            event[\"data\"][\"project_settings\"] = project_settings_by_id\n\n        project_settings = project_settings_by_id.get(project_name)\n        if not project_settings:\n            project_settings = get_project_settings(project_name)\n            event[\"data\"][\"project_settings\"][project_name] = project_settings\n        return project_settings\n\n    @staticmethod\n    def get_entity_path(entity):\n        \"\"\"Return full hierarchical path to entity.\"\"\"\n        return \"/\".join(\n            [ent[\"name\"] for ent in entity[\"link\"]]\n        )\n\n    @classmethod\n    def add_traceback_to_job(\n        cls, job, session, exc_info,\n        description=None,\n        component_name=None,\n        job_status=None\n    ):\n        \"\"\"Add traceback file to a job.\n\n        Args:\n            job (JobEntity): Entity of job where file should be able to\n                download (Created or queried with passed session).\n            session (Session): Ftrack session which was used to query/create\n                entered job.\n            exc_info (tuple): Exception info (e.g. from `sys.exc_info()`).\n            description (str): Change job description to describe what\n                happened. Job description won't change if not passed.\n            component_name (str): Name of component and default name of\n                downloaded file. Class name and current date time are used if\n                not specified.\n            job_status (str): Status of job which will be set. By default is\n                set to 'failed'.\n        \"\"\"\n        if description:\n            job_data = {\n                \"description\": description\n            }\n            job[\"data\"] = json.dumps(job_data)\n\n        if not job_status:\n            job_status = \"failed\"\n\n        job[\"status\"] = job_status\n\n        # Create temp file where traceback will be stored\n        temp_obj = tempfile.NamedTemporaryFile(\n            mode=\"w\", prefix=\"openpype_ftrack_\", suffix=\".txt\", delete=False\n        )\n        temp_obj.close()\n        temp_filepath = temp_obj.name\n\n        # Store traceback to file\n        result = traceback.format_exception(*exc_info)\n        with open(temp_filepath, \"w\") as temp_file:\n            temp_file.write(\"\".join(result))\n\n        # Upload file with traceback to ftrack server and add it to job\n        if not component_name:\n            component_name = \"{}_{}\".format(\n                cls.__name__,\n                datetime.datetime.now().strftime(\"%y-%m-%d-%H%M\")\n            )\n        cls.add_file_component_to_job(\n            job, session, temp_filepath, component_name\n        )\n        # Delete temp file\n        os.remove(temp_filepath)\n\n    @staticmethod\n    def add_file_component_to_job(job, session, filepath, basename=None):\n        \"\"\"Add filepath as downloadable component to job.\n\n        Args:\n            job (JobEntity): Entity of job where file should be able to\n                download (Created or queried with passed session).\n            session (Session): Ftrack session which was used to query/create\n                entered job.\n            filepath (str): Path to file which should be added to job.\n            basename (str): Defines name of file which will be downloaded on\n                user's side. Must be without extension otherwise extension will\n                be duplicated in downloaded name. Basename from entered path\n                used when not entered.\n        \"\"\"\n        # Make sure session's locations are configured\n        # - they can be deconfigured e.g. using `rollback` method\n        session._configure_locations()\n\n        # Query `ftrack.server` location where component will be stored\n        location = session.query(\n            \"Location where name is \\\"ftrack.server\\\"\"\n        ).one()\n\n        # Use filename as basename if not entered (must be without extension)\n        if basename is None:\n            basename = os.path.splitext(\n                os.path.basename(filepath)\n            )[0]\n\n        component = session.create_component(\n            filepath,\n            data={\"name\": basename},\n            location=location\n        )\n        session.create(\n            \"JobComponent\",\n            {\n                \"component_id\": component[\"id\"],\n                \"job_id\": job[\"id\"]\n            }\n        )\n        session.commit()\n"
  },
  {
    "path": "openpype/modules/ftrack/lib/ftrack_event_handler.py",
    "content": "import functools\nfrom .ftrack_base_handler import BaseHandler\n\n\nclass BaseEvent(BaseHandler):\n    '''Custom Event base class\n\n    BaseEvent is based on ftrack.update event\n    - get entities from event\n\n    If want to use different event base\n    - override register and *optional _translate_event method\n\n    '''\n\n    type = 'Event'\n\n    # Decorator\n    def launch_log(self, func):\n        @functools.wraps(func)\n        def wrapper_launch(*args, **kwargs):\n            try:\n                func(*args, **kwargs)\n            except Exception as exc:\n                self.session.rollback()\n                self.log.error(\n                    'Event \"{}\" Failed: {}'.format(\n                        self.__class__.__name__, str(exc)\n                    ),\n                    exc_info=True\n                )\n        return wrapper_launch\n\n    def register(self):\n        '''Registers the event, subscribing the discover and launch topics.'''\n        self.session.event_hub.subscribe(\n            'topic=ftrack.update',\n            self._launch,\n            priority=self.priority\n        )\n\n    def _translate_event(self, event, session=None):\n        '''Return *event* translated structure to be used with the API.'''\n        return self._get_entities(\n            event,\n            session,\n            ignore=['socialfeed', 'socialnotification', 'team']\n        )\n\n    def get_project_name_from_event(self, session, event, project_id):\n        \"\"\"Load or query and fill project entity from/to event data.\n\n        Project data are stored by ftrack id because in most cases it is\n        easier to access project id than project name.\n\n        Args:\n            session (ftrack_api.Session): Current session.\n            event (ftrack_api.Event): Processed event by session.\n            project_id (str): Ftrack project id.\n        \"\"\"\n        if not project_id:\n            raise ValueError(\n                \"Entered `project_id` is not valid. {} ({})\".format(\n                    str(project_id), str(type(project_id))\n                )\n            )\n        # Try to get project entity from event\n        project_data = event[\"data\"].get(\"project_data\")\n        if not project_data:\n            project_data = {}\n            event[\"data\"][\"project_data\"] = project_data\n\n        project_name = project_data.get(project_id)\n        if not project_name:\n            # Get project entity from task and store to event\n            project_entity = session.get(\"Project\", project_id)\n            project_name = project_entity[\"full_name\"]\n            event[\"data\"][\"project_data\"][project_id] = project_name\n        return project_name\n"
  },
  {
    "path": "openpype/modules/ftrack/lib/settings.py",
    "content": "import os\n\n\ndef get_ftrack_event_mongo_info():\n    database_name = os.environ[\"OPENPYPE_DATABASE_NAME\"]\n    collection_name = \"ftrack_events\"\n    return database_name, collection_name\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/_unused_publish/integrate_ftrack_comments.py",
    "content": "import sys\nimport pyblish.api\nimport six\n\n\nclass IntegrateFtrackComments(pyblish.api.InstancePlugin):\n    \"\"\"Create comments in Ftrack.\"\"\"\n\n    order = pyblish.api.IntegratorOrder\n    label = \"Integrate Comments to Ftrack\"\n    families = [\"shot\"]\n    enabled = False\n\n    def process(self, instance):\n        session = instance.context.data[\"ftrackSession\"]\n\n        entity = session.query(\n            \"Shot where name is \\\"{}\\\"\".format(instance.data[\"item\"].name())\n        ).one()\n\n        notes = []\n        for comment in instance.data[\"comments\"]:\n            notes.append(session.create(\"Note\", {\"content\": comment}))\n\n        entity[\"notes\"].extend(notes)\n\n        try:\n            session.commit()\n        except Exception:\n            tp, value, tb = sys.exc_info()\n            session.rollback()\n            six.reraise(tp, value, tb)\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py",
    "content": "\"\"\"\nRequires:\n    context > ftrackSession\n    context > ftrackEntity\n    instance > ftrackEntity\n\nProvides:\n    instance > customData > ftrack\n\"\"\"\nimport copy\n\nimport pyblish.api\n\n\nclass CollectFtrackCustomAttributeData(pyblish.api.ContextPlugin):\n    \"\"\"Collect custom attribute values and store them to customData.\n\n    Data are stored into each instance in context under\n        instance.data[\"customData\"][\"ftrack\"].\n\n    Hierarchical attributes are not looked up properly for that functionality\n    custom attribute values lookup must be extended.\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.4992\n    label = \"Collect Ftrack Custom Attribute Data\"\n\n    # Name of custom attributes for which will be look for\n    custom_attribute_keys = []\n\n    def process(self, context):\n        if not self.custom_attribute_keys:\n            self.log.info(\"Custom attribute keys are not set. Skipping\")\n            return\n\n        ftrack_entities_by_id = {}\n        default_entity_id = None\n\n        context_entity = context.data.get(\"ftrackEntity\")\n        if context_entity:\n            entity_id = context_entity[\"id\"]\n            default_entity_id = entity_id\n            ftrack_entities_by_id[entity_id] = context_entity\n\n        instances_by_entity_id = {\n            default_entity_id: []\n        }\n        for instance in context:\n            entity = instance.data.get(\"ftrackEntity\")\n            if not entity:\n                instances_by_entity_id[default_entity_id].append(instance)\n                continue\n\n            entity_id = entity[\"id\"]\n            ftrack_entities_by_id[entity_id] = entity\n            if entity_id not in instances_by_entity_id:\n                instances_by_entity_id[entity_id] = []\n            instances_by_entity_id[entity_id].append(instance)\n\n        if not ftrack_entities_by_id:\n            self.log.info(\"Ftrack entities are not set. Skipping\")\n            return\n\n        session = context.data[\"ftrackSession\"]\n        custom_attr_key_by_id = self.query_attr_confs(session)\n        if not custom_attr_key_by_id:\n            self.log.info((\n                \"Didn't find any of defined custom attributes {}\"\n            ).format(\", \".join(self.custom_attribute_keys)))\n            return\n\n        entity_ids = list(instances_by_entity_id.keys())\n        values_by_entity_id = self.query_attr_values(\n            session, entity_ids, custom_attr_key_by_id\n        )\n\n        for entity_id, instances in instances_by_entity_id.items():\n            if entity_id not in values_by_entity_id:\n                # Use default empty values\n                entity_id = None\n\n            for instance in instances:\n                value = copy.deepcopy(values_by_entity_id[entity_id])\n                if \"customData\" not in instance.data:\n                    instance.data[\"customData\"] = {}\n                instance.data[\"customData\"][\"ftrack\"] = value\n                instance_label = (\n                    instance.data.get(\"label\") or instance.data[\"name\"]\n                )\n                self.log.debug((\n                    \"Added ftrack custom data to instance \\\"{}\\\": {}\"\n                ).format(instance_label, value))\n\n    def query_attr_values(self, session, entity_ids, custom_attr_key_by_id):\n        # Prepare values for query\n        entity_ids_joined = \",\".join([\n            '\"{}\"'.format(entity_id)\n            for entity_id in entity_ids\n        ])\n        conf_ids_joined = \",\".join([\n            '\"{}\"'.format(conf_id)\n            for conf_id in custom_attr_key_by_id.keys()\n        ])\n        # Query custom attribute values\n        value_items = session.query(\n            (\n                \"select value, entity_id, configuration_id\"\n                \" from CustomAttributeValue\"\n                \" where entity_id in ({}) and configuration_id in ({})\"\n            ).format(\n                entity_ids_joined,\n                conf_ids_joined\n            )\n        ).all()\n\n        # Prepare default value output per entity id\n        values_by_key = {\n            key: None for key in self.custom_attribute_keys\n        }\n        # Prepare all entity ids that were queried\n        values_by_entity_id = {\n            entity_id: copy.deepcopy(values_by_key)\n            for entity_id in entity_ids\n        }\n        # Add none entity id which is used as default value\n        values_by_entity_id[None] = copy.deepcopy(values_by_key)\n        # Go through queried data and store them\n        for item in value_items:\n            conf_id = item[\"configuration_id\"]\n            conf_key = custom_attr_key_by_id[conf_id]\n            entity_id = item[\"entity_id\"]\n            values_by_entity_id[entity_id][conf_key] = item[\"value\"]\n        return values_by_entity_id\n\n    def query_attr_confs(self, session):\n        custom_attributes = set(self.custom_attribute_keys)\n        cust_attrs_query = (\n            \"select id, key from CustomAttributeConfiguration\"\n            \" where key in ({})\"\n        ).format(\", \".join(\n            [\"\\\"{}\\\"\".format(attr_name) for attr_name in custom_attributes]\n        ))\n\n        custom_attr_confs = session.query(cust_attrs_query).all()\n        return {\n            conf[\"id\"]: conf[\"key\"]\n            for conf in custom_attr_confs\n        }\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py",
    "content": "import logging\nimport pyblish.api\n\n\nclass CollectFtrackApi(pyblish.api.ContextPlugin):\n    \"\"\" Collects an ftrack session and the current task id. \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.4991\n    label = \"Collect Ftrack Api\"\n\n    def process(self, context):\n        ftrack_log = logging.getLogger('ftrack_api')\n        ftrack_log.setLevel(logging.WARNING)\n        ftrack_log = logging.getLogger('ftrack_api_old')\n        ftrack_log.setLevel(logging.WARNING)\n\n        # Collect session\n        # NOTE Import python module here to know if import was successful\n        import ftrack_api\n\n        session = ftrack_api.Session(auto_connect_event_hub=False)\n        self.log.debug(\"Ftrack user: \\\"{0}\\\"\".format(session.api_user))\n\n        # Collect task\n        project_name = context.data[\"projectName\"]\n        asset_name = context.data[\"asset\"]\n        task_name = context.data[\"task\"]\n\n        # Find project entity\n        project_query = 'Project where full_name is \"{0}\"'.format(project_name)\n        self.log.debug(\"Project query: < {0} >\".format(project_query))\n        project_entities = list(session.query(project_query).all())\n        if len(project_entities) == 0:\n            raise AssertionError(\n                \"Project \\\"{0}\\\" not found in Ftrack.\".format(project_name)\n            )\n        # QUESTION Is possible to happen?\n        elif len(project_entities) > 1:\n            raise AssertionError((\n                \"Found more than one project with name \\\"{0}\\\" in Ftrack.\"\n            ).format(project_name))\n\n        project_entity = project_entities[0]\n\n        self.log.debug(\"Project found: {0}\".format(project_entity))\n\n        task_object_type = session.query(\n            \"ObjectType where name is 'Task'\").one()\n        task_object_type_id = task_object_type[\"id\"]\n        asset_entity = None\n        if asset_name:\n            # Find asset entity\n            entity_query = (\n                \"TypedContext where project_id is '{}'\"\n                \" and name is '{}'\"\n                \" and object_type_id != '{}'\"\n            ).format(\n                project_entity[\"id\"],\n                asset_name,\n                task_object_type_id\n            )\n            self.log.debug(\"Asset entity query: < {0} >\".format(entity_query))\n            asset_entities = []\n            for entity in session.query(entity_query).all():\n                asset_entities.append(entity)\n\n            if len(asset_entities) == 0:\n                raise AssertionError((\n                    \"Entity with name \\\"{0}\\\" not found\"\n                    \" in Ftrack project \\\"{1}\\\".\"\n                ).format(asset_name, project_name))\n\n            elif len(asset_entities) > 1:\n                raise AssertionError((\n                    \"Found more than one entity with name \\\"{0}\\\"\"\n                    \" in Ftrack project \\\"{1}\\\".\"\n                ).format(asset_name, project_name))\n\n            asset_entity = asset_entities[0]\n\n        self.log.debug(\"Asset found: {0}\".format(asset_entity))\n\n        task_entity = None\n        # Find task entity if task is set\n        if not asset_entity:\n            self.log.warning(\n                \"Asset entity is not set. Skipping query of task entity.\"\n            )\n        elif not task_name:\n            self.log.warning(\"Task name is not set.\")\n        else:\n            task_query = (\n                'Task where name is \"{0}\" and parent_id is \"{1}\"'\n            ).format(task_name, asset_entity[\"id\"])\n            self.log.debug(\"Task entity query: < {0} >\".format(task_query))\n            task_entity = session.query(task_query).first()\n            if not task_entity:\n                self.log.warning(\n                    \"Task entity with name \\\"{0}\\\" was not found.\".format(\n                        task_name\n                    )\n                )\n            else:\n                self.log.debug(\"Task entity found: {0}\".format(task_entity))\n\n        context.data[\"ftrackSession\"] = session\n        context.data[\"ftrackPythonModule\"] = ftrack_api\n        context.data[\"ftrackProject\"] = project_entity\n        context.data[\"ftrackEntity\"] = asset_entity\n        context.data[\"ftrackTask\"] = task_entity\n\n        self.per_instance_process(\n            context,\n            asset_entity,\n            task_entity,\n            task_object_type_id\n        )\n\n    def per_instance_process(\n        self,\n        context,\n        context_asset_entity,\n        context_task_entity,\n        task_object_type_id\n    ):\n        context_task_name = None\n        context_asset_name = None\n        if context_asset_entity:\n            context_asset_name = context_asset_entity[\"name\"]\n            if context_task_entity:\n                context_task_name = context_task_entity[\"name\"]\n        instance_by_asset_and_task = {}\n        for instance in context:\n            self.log.debug(\n                \"Checking entities of instance \\\"{}\\\"\".format(str(instance))\n            )\n            instance_asset_name = instance.data.get(\"asset\")\n            instance_task_name = instance.data.get(\"task\")\n\n            if not instance_asset_name and not instance_task_name:\n                self.log.debug(\"Instance does not have set context keys.\")\n                instance.data[\"ftrackEntity\"] = context_asset_entity\n                instance.data[\"ftrackTask\"] = context_task_entity\n                continue\n\n            elif instance_asset_name and instance_task_name:\n                if (\n                    instance_asset_name == context_asset_name\n                    and instance_task_name == context_task_name\n                ):\n                    self.log.debug((\n                        \"Instance's context is same as in publish context.\"\n                        \" Asset: {} | Task: {}\"\n                    ).format(context_asset_name, context_task_name))\n                    instance.data[\"ftrackEntity\"] = context_asset_entity\n                    instance.data[\"ftrackTask\"] = context_task_entity\n                    continue\n                asset_name = instance_asset_name\n                task_name = instance_task_name\n\n            elif instance_task_name:\n                if instance_task_name == context_task_name:\n                    self.log.debug((\n                        \"Instance's context task is same as in publish\"\n                        \" context. Task: {}\"\n                    ).format(context_task_name))\n                    instance.data[\"ftrackEntity\"] = context_asset_entity\n                    instance.data[\"ftrackTask\"] = context_task_entity\n                    continue\n\n                asset_name = context_asset_name\n                task_name = instance_task_name\n\n            elif instance_asset_name:\n                if instance_asset_name == context_asset_name:\n                    self.log.debug((\n                        \"Instance's context asset is same as in publish\"\n                        \" context. Asset: {}\"\n                    ).format(context_asset_name))\n                    instance.data[\"ftrackEntity\"] = context_asset_entity\n                    instance.data[\"ftrackTask\"] = context_task_entity\n                    continue\n\n                # Do not use context's task name\n                task_name = instance_task_name\n                asset_name = instance_asset_name\n\n            if asset_name not in instance_by_asset_and_task:\n                instance_by_asset_and_task[asset_name] = {}\n\n            if task_name not in instance_by_asset_and_task[asset_name]:\n                instance_by_asset_and_task[asset_name][task_name] = []\n            instance_by_asset_and_task[asset_name][task_name].append(instance)\n\n        if not instance_by_asset_and_task:\n            return\n\n        session = context.data[\"ftrackSession\"]\n        project_entity = context.data[\"ftrackProject\"]\n        asset_names = set(instance_by_asset_and_task.keys())\n\n        joined_asset_names = \",\".join([\n            \"\\\"{}\\\"\".format(name)\n            for name in asset_names\n        ])\n        entities = session.query(\n            (\n                \"TypedContext where project_id is \\\"{}\\\" and name in ({})\"\n                \" and object_type_id != '{}'\"\n            ).format(\n                project_entity[\"id\"],\n                joined_asset_names,\n                task_object_type_id\n            )\n        ).all()\n\n        entities_by_name = {\n            entity[\"name\"]: entity\n            for entity in entities\n        }\n        for asset_name, by_task_data in instance_by_asset_and_task.items():\n            entity = entities_by_name.get(asset_name)\n            task_entity_by_name = {}\n            if not entity:\n                self.log.warning((\n                    \"Didn't find entity with name \\\"{}\\\" in Project \\\"{}\\\"\"\n                ).format(asset_name, project_entity[\"full_name\"]))\n            else:\n                task_entities = session.query((\n                    \"select id, name from Task where parent_id is \\\"{}\\\"\"\n                ).format(entity[\"id\"])).all()\n                for task_entity in task_entities:\n                    task_name_low = task_entity[\"name\"].lower()\n                    task_entity_by_name[task_name_low] = task_entity\n\n            for task_name, instances in by_task_data.items():\n                task_entity = None\n                if task_name and entity:\n                    task_entity = task_entity_by_name.get(task_name.lower())\n\n                for instance in instances:\n                    instance.data[\"ftrackEntity\"] = entity\n                    instance.data[\"ftrackTask\"] = task_entity\n\n                    self.log.debug((\n                        \"Instance {} has own ftrack entities\"\n                        \" as has different context. TypedContext: {} Task: {}\"\n                    ).format(str(instance), str(entity), str(task_entity)))\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py",
    "content": "\"\"\"\nRequires:\n    none\n\nProvides:\n    instance     -> families ([])\n\"\"\"\nimport pyblish.api\n\nfrom openpype.lib import filter_profiles\n\n\nclass CollectFtrackFamily(pyblish.api.InstancePlugin):\n    \"\"\"Adds explicitly 'ftrack' to families to upload instance to FTrack.\n\n    Uses selection by combination of hosts/families/tasks names via\n    profiles resolution.\n\n    Triggered everywhere, checks instance against configured.\n\n    Checks advanced filtering which works on 'families' not on main\n    'family', as some variants dynamically resolves addition of ftrack\n    based on 'families' (editorial drives it by presence of 'review')\n    \"\"\"\n\n    label = \"Collect Ftrack Family\"\n    order = pyblish.api.CollectorOrder + 0.4990\n\n    profiles = None\n\n    def process(self, instance):\n        if not self.profiles:\n            self.log.warning(\"No profiles present for adding Ftrack family\")\n            return\n\n        host_name = instance.context.data[\"hostName\"]\n        family = instance.data[\"family\"]\n        task_name = instance.data.get(\"task\")\n\n        filtering_criteria = {\n            \"hosts\": host_name,\n            \"families\": family,\n            \"tasks\": task_name\n        }\n        profile = filter_profiles(\n            self.profiles,\n            filtering_criteria,\n            logger=self.log\n        )\n\n        add_ftrack_family = False\n        families = instance.data.setdefault(\"families\", [])\n\n        if profile:\n            add_ftrack_family = profile[\"add_ftrack_family\"]\n            additional_filters = profile.get(\"advanced_filtering\")\n            if additional_filters:\n                families_set = set(families) | {family}\n                self.log.info(\n                    \"'{}' families used for additional filtering\".format(\n                        families_set))\n                add_ftrack_family = self._get_add_ftrack_f_from_addit_filters(\n                    additional_filters,\n                    families_set,\n                    add_ftrack_family\n                )\n\n        result_str = \"Not adding\"\n        if add_ftrack_family:\n            result_str = \"Adding\"\n            if \"ftrack\" not in families:\n                families.append(\"ftrack\")\n\n        self.log.debug(\"{} 'ftrack' family for instance with '{}'\".format(\n            result_str, family\n        ))\n\n    def _get_add_ftrack_f_from_addit_filters(\n        self, additional_filters, families, add_ftrack_family\n    ):\n        \"\"\"Compares additional filters - working on instance's families.\n\n        Triggered for more detailed filtering when main family matches,\n        but content of 'families' actually matter.\n        (For example 'review' in 'families' should result in adding to\n        Ftrack)\n\n        Args:\n            additional_filters (dict) - from Setting\n            families (set[str]) - subfamilies\n            add_ftrack_family (bool) - add ftrack to families if True\n        \"\"\"\n\n        override_filter = None\n        override_filter_value = -1\n        for additional_filter in additional_filters:\n            filter_families = set(additional_filter[\"families\"])\n            valid = filter_families <= set(families)  # issubset\n            if not valid:\n                continue\n\n            value = len(filter_families)\n            if value > override_filter_value:\n                override_filter = additional_filter\n                override_filter_value = value\n\n        if override_filter:\n            add_ftrack_family = override_filter[\"add_ftrack_family\"]\n\n        return add_ftrack_family\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/collect_local_ftrack_creds.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect default Deadline server.\"\"\"\nimport pyblish.api\nimport os\n\n\nclass CollectLocalFtrackCreds(pyblish.api.ContextPlugin):\n    \"\"\"Collect default Royal Render path.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.01\n    label = \"Collect local ftrack credentials\"\n    targets = [\"rr_control\"]\n\n    def process(self, context):\n        if os.getenv(\"FTRACK_API_USER\") and os.getenv(\"FTRACK_API_KEY\") and \\\n                os.getenv(\"FTRACK_SERVER\"):\n            return\n        ftrack_module = context.data[\"openPypeModules\"][\"ftrack\"]\n        if ftrack_module.enabled:\n            creds = ftrack_module.get_credentials()\n            os.environ[\"FTRACK_API_USER\"] = creds[0]\n            os.environ[\"FTRACK_API_KEY\"] = creds[1]\n            os.environ[\"FTRACK_SERVER\"] = ftrack_module.ftrack_url\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/collect_username.py",
    "content": "\"\"\"Loads publishing context from json and continues in publish process.\n\nShould run before 'CollectAnatomyContextData' so the user on context is\nchanged before it's stored to context anatomy data or instance anatomy data.\n\nRequires:\n    anatomy -> context[\"anatomy\"] *(pyblish.api.CollectorOrder - 0.11)\n\nProvides:\n    context, instances -> All data from previous publishing process.\n\"\"\"\n\nimport ftrack_api\nimport os\n\nimport pyblish.api\n\n\nclass CollectUsernameForWebpublish(pyblish.api.ContextPlugin):\n    \"\"\"\n        Translates user email to Ftrack username.\n\n        Emails in Ftrack are same as company's Slack, username is needed to\n        load data to Ftrack.\n\n        Expects \"pype.club\" user created on Ftrack and FTRACK_BOT_API_KEY env\n        var set up.\n\n        Resets `context.data[\"user\"] to correctly populate `version.author` and\n        `representation.context.username`\n\n    \"\"\"\n    order = pyblish.api.CollectorOrder + 0.0015\n    label = \"Collect ftrack username\"\n    hosts = [\"webpublisher\", \"photoshop\"]\n    targets = [\"webpublish\"]\n\n    def process(self, context):\n        self.log.info(\"{}\".format(self.__class__.__name__))\n        os.environ[\"FTRACK_API_USER\"] = os.environ[\"FTRACK_BOT_API_USER\"]\n        os.environ[\"FTRACK_API_KEY\"] = os.environ[\"FTRACK_BOT_API_KEY\"]\n\n        # for publishes with studio processing\n        user_email = os.environ.get(\"USER_EMAIL\")\n        self.log.debug(\"Email from env:: {}\".format(user_email))\n        if not user_email:\n            # for basic webpublishes\n            for instance in context:\n                user_email = instance.data.get(\"user_email\")\n                self.log.debug(\"Email from instance:: {}\".format(user_email))\n                break\n\n        if not user_email:\n            self.log.info(\"No email found\")\n            return\n\n        session = ftrack_api.Session(auto_connect_event_hub=False)\n        user = session.query(\n            \"User where email like '{}'\".format(user_email)\n        ).first()\n\n        if not user:\n            raise ValueError(\n                \"Couldn't find user with {} email\".format(user_email))\n\n        username = user.get(\"username\")\n        self.log.debug(\"Resolved ftrack username:: {}\".format(username))\n        os.environ[\"FTRACK_API_USER\"] = username\n\n        burnin_name = username\n        if '@' in burnin_name:\n            burnin_name = burnin_name[:burnin_name.index('@')]\n        context.data[\"user\"] = burnin_name\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py",
    "content": "\"\"\"Integrate components into ftrack\n\nRequires:\n    context -> ftrackSession - connected ftrack.Session\n    instance -> ftrackComponentsList - list of components to integrate\n\nProvides:\n    instance -> ftrackIntegratedAssetVersionsData\n    # legacy\n    instance -> ftrackIntegratedAssetVersions\n\"\"\"\n\nimport os\nimport collections\n\nimport pyblish.api\nimport clique\n\n\nclass IntegrateFtrackApi(pyblish.api.InstancePlugin):\n    \"\"\" Commit components to server. \"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.499\n    label = \"Integrate Ftrack Api\"\n    families = [\"ftrack\"]\n\n    def process(self, instance):\n        component_list = instance.data.get(\"ftrackComponentsList\")\n        if not component_list:\n            self.log.debug(\n                \"Instance doesn't have components to integrate to Ftrack.\"\n                \" Skipping.\"\n            )\n            return\n\n        context = instance.context\n        task_entity, parent_entity = self.get_instance_entities(\n            instance, context)\n        if parent_entity is None:\n            self.log.debug((\n                \"Skipping ftrack integration. Instance \\\"{}\\\" does not\"\n                \" have specified ftrack entities.\"\n            ).format(str(instance)))\n            return\n\n        session = context.data[\"ftrackSession\"]\n        # Reset session operations and reconfigure locations\n        session.recorded_operations.clear()\n        session._configure_locations()\n\n        try:\n            self.integrate_to_ftrack(\n                session,\n                instance,\n                task_entity,\n                parent_entity,\n                component_list\n            )\n\n        except Exception:\n            session.reset()\n            raise\n\n    def get_instance_entities(self, instance, context):\n        parent_entity = None\n        # If instance has set \"ftrackEntity\" or \"ftrackTask\" then use them from\n        #   instance. Even if they are set to None. If they are set to None it\n        #   has a reason. (like has different context)\n        if \"ftrackEntity\" in instance.data or \"ftrackTask\" in instance.data:\n            task_entity = instance.data.get(\"ftrackTask\")\n            parent_entity = instance.data.get(\"ftrackEntity\")\n\n        elif \"ftrackEntity\" in context.data or \"ftrackTask\" in context.data:\n            task_entity = context.data.get(\"ftrackTask\")\n            parent_entity = context.data.get(\"ftrackEntity\")\n\n        if task_entity:\n            parent_entity = task_entity[\"parent\"]\n\n        return task_entity, parent_entity\n\n    def integrate_to_ftrack(\n        self,\n        session,\n        instance,\n        task_entity,\n        parent_entity,\n        component_list\n    ):\n        default_asset_name = None\n        if task_entity:\n            default_asset_name = task_entity[\"name\"]\n\n        if not default_asset_name:\n            default_asset_name = parent_entity[\"name\"]\n\n        # Change status on task\n        asset_version_status_ids_by_name = {}\n        project_entity = instance.context.data.get(\"ftrackProject\")\n        if project_entity:\n            project_schema = project_entity[\"project_schema\"]\n            asset_version_statuses = (\n                project_schema.get_statuses(\"AssetVersion\")\n            )\n            asset_version_status_ids_by_name = {\n                status[\"name\"].lower(): status[\"id\"]\n                for status in asset_version_statuses\n            }\n\n        # Prepare AssetTypes\n        asset_types_by_short = self._ensure_asset_types_exists(\n            session, component_list\n        )\n        self._fill_component_locations(session, component_list)\n\n        asset_versions_data_by_id = {}\n        used_asset_versions = []\n\n        # Iterate over components and publish\n        for data in component_list:\n            self.log.debug(\"data: {}\".format(data))\n\n            # AssetType\n            asset_type_short = data[\"assettype_data\"][\"short\"]\n            asset_type_entity = asset_types_by_short[asset_type_short]\n\n            # Asset\n            asset_data = data.get(\"asset_data\") or {}\n            if \"name\" not in asset_data:\n                asset_data[\"name\"] = default_asset_name\n            asset_entity = self._ensure_asset_exists(\n                session,\n                asset_data,\n                asset_type_entity[\"id\"],\n                parent_entity[\"id\"]\n            )\n\n            # Asset Version\n            asset_version_data = data.get(\"assetversion_data\") or {}\n            asset_version_entity = self._ensure_asset_version_exists(\n                session,\n                asset_version_data,\n                asset_entity[\"id\"],\n                task_entity,\n                asset_version_status_ids_by_name\n            )\n\n            # Store asset version and components items that were\n            version_id = asset_version_entity[\"id\"]\n            if version_id not in asset_versions_data_by_id:\n                asset_versions_data_by_id[version_id] = {\n                    \"asset_version\": asset_version_entity,\n                    \"component_items\": []\n                }\n\n            asset_versions_data_by_id[version_id][\"component_items\"].append(\n                data\n            )\n\n            # Backwards compatibility\n            if asset_version_entity not in used_asset_versions:\n                used_asset_versions.append(asset_version_entity)\n\n        self._create_components(session, asset_versions_data_by_id)\n\n        instance.data[\"ftrackIntegratedAssetVersionsData\"] = (\n            asset_versions_data_by_id\n        )\n\n        # Backwards compatibility\n        asset_versions_key = \"ftrackIntegratedAssetVersions\"\n        if asset_versions_key not in instance.data:\n            instance.data[asset_versions_key] = []\n\n        for asset_version in used_asset_versions:\n            if asset_version not in instance.data[asset_versions_key]:\n                instance.data[asset_versions_key].append(asset_version)\n\n    def _fill_component_locations(self, session, component_list):\n        components_by_location_name = collections.defaultdict(list)\n        components_by_location_id = collections.defaultdict(list)\n        for component_item in component_list:\n            # Location entity can be prefilled\n            # - this is not recommended as connection to ftrack server may\n            #   be lost and in that case the entity is not valid when gets\n            #   to this plugin\n            location = component_item.get(\"component_location\")\n            if location is not None:\n                continue\n\n            # Collect location id\n            location_id = component_item.get(\"component_location_id\")\n            if location_id:\n                components_by_location_id[location_id].append(\n                    component_item\n                )\n                continue\n\n            location_name = component_item.get(\"component_location_name\")\n            if location_name:\n                components_by_location_name[location_name].append(\n                    component_item\n                )\n                continue\n\n        # Skip if there is nothing to do\n        if not components_by_location_name and not components_by_location_id:\n            return\n\n        # Query locations\n        query_filters = []\n        if components_by_location_id:\n            joined_location_ids = \",\".join([\n                '\"{}\"'.format(location_id)\n                for location_id in components_by_location_id\n            ])\n            query_filters.append(\"id in ({})\".format(joined_location_ids))\n\n        if components_by_location_name:\n            joined_location_names = \",\".join([\n                '\"{}\"'.format(location_name)\n                for location_name in components_by_location_name\n            ])\n            query_filters.append(\"name in ({})\".format(joined_location_names))\n\n        locations = session.query(\n            \"select id, name from Location where {}\".format(\n                \" or \".join(query_filters)\n            )\n        ).all()\n        # Fill locations in components\n        for location in locations:\n            location_id = location[\"id\"]\n            location_name = location[\"name\"]\n            if location_id in components_by_location_id:\n                for component in components_by_location_id[location_id]:\n                    component[\"component_location\"] = location\n\n            if location_name in components_by_location_name:\n                for component in components_by_location_name[location_name]:\n                    component[\"component_location\"] = location\n\n    def _ensure_asset_types_exists(self, session, component_list):\n        \"\"\"Make sure that all AssetType entities exists for integration.\n\n        Returns:\n            dict: All asset types by short name.\n        \"\"\"\n        # Query existing asset types\n        asset_types = session.query(\"select id, short from AssetType\").all()\n        # Stpore all existing short names\n        asset_type_shorts = {asset_type[\"short\"] for asset_type in asset_types}\n        # Check which asset types are missing and store them\n        asset_type_names_by_missing_shorts = {}\n        default_short_name = \"upload\"\n        for data in component_list:\n            asset_type_data = data.get(\"assettype_data\") or {}\n            asset_type_short = asset_type_data.get(\"short\")\n            if not asset_type_short:\n                # Use default asset type name if not set and change the\n                #   input data\n                asset_type_short = default_short_name\n                asset_type_data[\"short\"] = asset_type_short\n                data[\"assettype_data\"] = asset_type_data\n\n            if (\n                # Skip if short name exists\n                asset_type_short in asset_type_shorts\n                # Skip if short name was already added to missing types\n                #   and asset type name is filled\n                # - if asset type name is missing then try use name from other\n                #   data\n                or asset_type_names_by_missing_shorts.get(asset_type_short)\n            ):\n                continue\n\n            asset_type_names_by_missing_shorts[asset_type_short] = (\n                asset_type_data.get(\"name\")\n            )\n\n        # Create missing asset types if there are any\n        if asset_type_names_by_missing_shorts:\n            self.log.info(\"Creating asset types with short names: {}\".format(\n                \", \".join(asset_type_names_by_missing_shorts.keys())\n            ))\n            for missing_short, type_name in (\n                asset_type_names_by_missing_shorts.items()\n            ):\n                # Use short for name if name is not defined\n                if not type_name:\n                    type_name = missing_short\n                # Use short name also for name\n                #   - there is not other source for 'name'\n                session.create(\n                    \"AssetType\",\n                    {\n                        \"short\": missing_short,\n                        \"name\": type_name\n                    }\n                )\n\n            # Commit creation\n            session.commit()\n            # Requery asset types\n            asset_types = session.query(\n                \"select id, short from AssetType\"\n            ).all()\n\n        return {asset_type[\"short\"]: asset_type for asset_type in asset_types}\n\n    def _ensure_asset_exists(\n        self, session, asset_data, asset_type_id, parent_id\n    ):\n        asset_name = asset_data[\"name\"]\n        asset_entity = self._query_asset(\n            session, asset_name, asset_type_id, parent_id\n        )\n        if asset_entity is not None:\n            return asset_entity\n\n        asset_data = {\n            \"name\": asset_name,\n            \"type_id\": asset_type_id,\n            \"context_id\": parent_id\n        }\n        self.log.debug(\"Created new Asset with data: {}.\".format(asset_data))\n        session.create(\"Asset\", asset_data)\n        session.commit()\n        return self._query_asset(session, asset_name, asset_type_id, parent_id)\n\n    def _query_asset(self, session, asset_name, asset_type_id, parent_id):\n        return session.query(\n            (\n                \"select id from Asset\"\n                \" where name is \\\"{}\\\"\"\n                \" and type_id is \\\"{}\\\"\"\n                \" and context_id is \\\"{}\\\"\"\n            ).format(asset_name, asset_type_id, parent_id)\n        ).first()\n\n    def _ensure_asset_version_exists(\n        self,\n        session,\n        asset_version_data,\n        asset_id,\n        task_entity,\n        status_ids_by_name\n    ):\n        task_id = None\n        if task_entity:\n            task_id = task_entity[\"id\"]\n\n        status_name = asset_version_data.pop(\"status_name\", None)\n\n        # Try query asset version by criteria (asset id and version)\n        version = asset_version_data.get(\"version\") or \"0\"\n        asset_version_entity = self._query_asset_version(\n            session, version, asset_id\n        )\n\n        # Prepare comment value\n        comment = asset_version_data.get(\"comment\") or \"\"\n        if asset_version_entity is not None:\n            changed = False\n            if comment != asset_version_entity[\"comment\"]:\n                asset_version_entity[\"comment\"] = comment\n                changed = True\n\n            if task_id != asset_version_entity[\"task_id\"]:\n                asset_version_entity[\"task_id\"] = task_id\n                changed = True\n\n            if changed:\n                session.commit()\n\n        else:\n            new_asset_version_data = {\n                \"version\": version,\n                \"asset_id\": asset_id\n            }\n            if task_id:\n                new_asset_version_data[\"task_id\"] = task_id\n\n            if comment:\n                new_asset_version_data[\"comment\"] = comment\n\n            self.log.debug(\"Created new AssetVersion with data {}\".format(\n                new_asset_version_data\n            ))\n            session.create(\"AssetVersion\", new_asset_version_data)\n            session.commit()\n            asset_version_entity = self._query_asset_version(\n                session, version, asset_id\n            )\n\n        if status_name:\n            status_id = status_ids_by_name.get(status_name.lower())\n            if not status_id:\n                self.log.info((\n                    \"Ftrack status with name \\\"{}\\\"\"\n                    \" for AssetVersion was not found.\"\n                ).format(status_name))\n\n            elif asset_version_entity[\"status_id\"] != status_id:\n                asset_version_entity[\"status_id\"] = status_id\n                session.commit()\n\n        # Set custom attributes if there were any set\n        custom_attrs = asset_version_data.get(\"custom_attributes\") or {}\n        for attr_key, attr_value in custom_attrs.items():\n            if attr_key in asset_version_entity[\"custom_attributes\"]:\n                try:\n                    asset_version_entity[\"custom_attributes\"][attr_key] = (\n                        attr_value\n                    )\n                    session.commit()\n                    continue\n                except Exception:\n                    session.rollback()\n                    session._configure_locations()\n\n            self.log.warning(\n                (\n                    \"Custom Attribute \\\"{0}\\\" is not available for\"\n                    \" AssetVersion <{1}>. Can't set it's value to: \\\"{2}\\\"\"\n                ).format(\n                    attr_key, asset_version_entity[\"id\"], str(attr_value)\n                )\n            )\n\n        return asset_version_entity\n\n    def _query_asset_version(self, session, version, asset_id):\n        return session.query(\n            (\n                \"select id, task_id, comment from AssetVersion\"\n                \" where version is \\\"{}\\\" and asset_id is \\\"{}\\\"\"\n            ).format(version, asset_id)\n        ).first()\n\n    def create_component(self, session, asset_version_entity, data):\n        component_data = data.get(\"component_data\") or {}\n\n        if not component_data.get(\"name\"):\n            component_data[\"name\"] = \"main\"\n\n        version_id = asset_version_entity[\"id\"]\n        component_data[\"version_id\"] = version_id\n        component_entity = session.query(\n            (\n                \"select id, name from Component where name is \\\"{}\\\"\"\n                \" and version_id is \\\"{}\\\"\"\n            ).format(component_data[\"name\"], version_id)\n        ).first()\n\n        component_overwrite = data.get(\"component_overwrite\", False)\n        location = data.get(\"component_location\", session.pick_location())\n\n        # Overwrite existing component data if requested.\n        if component_entity and component_overwrite:\n            origin_location = session.query(\n                \"Location where name is \\\"ftrack.origin\\\"\"\n            ).one()\n\n            # Removing existing members from location\n            components = list(component_entity.get(\"members\", []))\n            components += [component_entity]\n            for component in components:\n                for loc in component[\"component_locations\"]:\n                    if location[\"id\"] == loc[\"location_id\"]:\n                        location.remove_component(\n                            component, recursive=False\n                        )\n\n            # Deleting existing members on component entity\n            for member in component_entity.get(\"members\", []):\n                session.delete(member)\n                del(member)\n\n            session.commit()\n\n            # Reset members in memory\n            if \"members\" in component_entity.keys():\n                component_entity[\"members\"] = []\n\n            # Add components to origin location\n            try:\n                collection = clique.parse(data[\"component_path\"])\n            except ValueError:\n                # Assume its a single file\n                # Changing file type\n                name, ext = os.path.splitext(data[\"component_path\"])\n                component_entity[\"file_type\"] = ext\n\n                origin_location.add_component(\n                    component_entity, data[\"component_path\"]\n                )\n            else:\n                # Changing file type\n                component_entity[\"file_type\"] = collection.format(\"{tail}\")\n\n                # Create member components for sequence.\n                for member_path in collection:\n\n                    size = 0\n                    try:\n                        size = os.path.getsize(member_path)\n                    except OSError:\n                        pass\n\n                    name = collection.match(member_path).group(\"index\")\n\n                    member_data = {\n                        \"name\": name,\n                        \"container\": component_entity,\n                        \"size\": size,\n                        \"file_type\": os.path.splitext(member_path)[-1]\n                    }\n\n                    component = session.create(\n                        \"FileComponent\", member_data\n                    )\n                    origin_location.add_component(\n                        component, member_path, recursive=False\n                    )\n                    component_entity[\"members\"].append(component)\n\n            # Add components to location.\n            location.add_component(\n                component_entity, origin_location, recursive=True\n            )\n\n            data[\"component\"] = component_entity\n            self.log.info(\n                (\n                    \"Overwriting Component with path: {0}, data: {1},\"\n                    \" location: {2}\"\n                ).format(\n                    data[\"component_path\"],\n                    component_data,\n                    location\n                )\n            )\n\n        # Extracting metadata, and adding after entity creation. This is\n        # due to a ftrack_api bug where you can't add metadata on creation.\n        component_metadata = component_data.pop(\"metadata\", {})\n\n        # Create new component if none exists.\n        new_component = False\n        if not component_entity:\n            component_entity = asset_version_entity.create_component(\n                data[\"component_path\"],\n                data=component_data,\n                location=location\n            )\n            data[\"component\"] = component_entity\n            self.log.debug(\n                (\n                    \"Created new Component with path: {0}, data: {1},\"\n                    \" metadata: {2}, location: {3}\"\n                ).format(\n                    data[\"component_path\"],\n                    component_data,\n                    component_metadata,\n                    location\n                )\n            )\n            new_component = True\n\n        # Adding metadata\n        existing_component_metadata = component_entity[\"metadata\"]\n        existing_component_metadata.update(component_metadata)\n        component_entity[\"metadata\"] = existing_component_metadata\n\n        # if component_data['name'] = 'ftrackreview-mp4-mp4':\n        #     assetversion_entity[\"thumbnail_id\"]\n\n        # Setting assetversion thumbnail\n        if data.get(\"thumbnail\"):\n            asset_version_entity[\"thumbnail_id\"] = component_entity[\"id\"]\n\n        # Inform user about no changes to the database.\n        if (\n            component_entity\n            and not component_overwrite\n            and not new_component\n        ):\n            data[\"component\"] = component_entity\n            self.log.info(\n                \"Found existing component, and no request to overwrite. \"\n                \"Nothing has been changed.\"\n            )\n        else:\n            # Commit changes.\n            session.commit()\n\n    def _create_components(self, session, asset_versions_data_by_id):\n        for item in asset_versions_data_by_id.values():\n            asset_version_entity = item[\"asset_version\"]\n            component_items = item[\"component_items\"]\n\n            component_entities = session.query(\n                (\n                    \"select id, name from Component where version_id is \\\"{}\\\"\"\n                ).format(asset_version_entity[\"id\"])\n            ).all()\n\n            existing_component_names = {\n                component[\"name\"]\n                for component in component_entities\n            }\n\n            contain_review = \"ftrackreview-mp4\" in existing_component_names\n            thumbnail_component_item = None\n            for component_item in component_items:\n                component_data = component_item.get(\"component_data\") or {}\n                component_name = component_data.get(\"name\")\n                if component_name == \"ftrackreview-mp4\":\n                    contain_review = True\n                elif component_name == \"ftrackreview-image\":\n                    thumbnail_component_item = component_item\n\n            if contain_review and thumbnail_component_item:\n                thumbnail_component_item[\"component_data\"][\"name\"] = (\n                    \"thumbnail\"\n                )\n\n            # Component\n            for component_item in component_items:\n                self.create_component(\n                    session, asset_version_entity, component_item\n                )\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/integrate_ftrack_component_overwrite.py",
    "content": "import pyblish.api\n\n\nclass IntegrateFtrackComponentOverwrite(pyblish.api.InstancePlugin):\n    \"\"\"\n    Set `component_overwrite` to True on all instances `ftrackComponentsList`\n    \"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.49\n    label = 'Overwrite ftrack created versions'\n    families = [\"clip\"]\n    optional = True\n    active = False\n\n    def process(self, instance):\n        component_list = instance.data.get('ftrackComponentsList')\n        if not component_list:\n            self.log.info(\"No component to overwrite...\")\n            return\n\n        for cl in component_list:\n            cl['component_overwrite'] = True\n            self.log.debug('Component {} overwriting'.format(\n                cl['component_data']['name']))\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py",
    "content": "\"\"\"\nRequires:\n    context > comment\n    context > ftrackSession\n    instance > ftrackIntegratedAssetVersionsData\n\"\"\"\n\nimport sys\nimport json\n\nimport six\nimport pyblish.api\nfrom openpype.lib import StringTemplate\n\n\nclass IntegrateFtrackDescription(pyblish.api.InstancePlugin):\n    \"\"\"Add description to AssetVersions in Ftrack.\"\"\"\n\n    # Must be after integrate asset new\n    order = pyblish.api.IntegratorOrder + 0.4999\n    label = \"Integrate Ftrack description\"\n    families = [\"ftrack\"]\n    optional = True\n\n    # Can be set in settings:\n    # - Allows `intent` and `comment` keys\n    description_template = \"{comment}\"\n\n    def process(self, instance):\n        if not self.description_template:\n            self.log.info(\"Skipping. Description template is not set.\")\n            return\n\n        # Check if there are any integrated AssetVersion entities\n        asset_versions_key = \"ftrackIntegratedAssetVersionsData\"\n        asset_versions_data_by_id = instance.data.get(asset_versions_key)\n        if not asset_versions_data_by_id:\n            self.log.info(\"There are any integrated AssetVersions\")\n            return\n\n        comment = instance.data[\"comment\"]\n        if not comment:\n            self.log.debug(\"Comment is not set.\")\n        else:\n            self.log.debug(\"Comment is set to `{}`\".format(comment))\n\n        intent = instance.context.data.get(\"intent\")\n        if intent and \"{intent}\" in self.description_template:\n            value = intent.get(\"value\")\n            if value:\n                intent = intent.get(\"label\") or value\n\n        if not intent and not comment:\n            self.log.info(\"Skipping. Intent and comment are empty.\")\n            return\n\n        # if intent label is set then format comment\n        # - it is possible that intent_label is equal to \"\" (empty string)\n        if intent:\n            self.log.debug(\"Intent is set to `{}`.\".format(intent))\n        else:\n            self.log.debug(\"Intent is not set.\")\n\n        # If we would like to use more \"optional\" possibilities we would have\n        #   come up with some expressions in templates or speicifc templates\n        #   for all 3 possible combinations when comment and intent are\n        #   set or not (when both are not set then description does not\n        #   make sense).\n        fill_data = {}\n        if comment:\n            fill_data[\"comment\"] = comment\n        if intent:\n            fill_data[\"intent\"] = intent\n\n        description = StringTemplate.format_template(\n            self.description_template, fill_data\n        )\n        if not description.solved:\n            self.log.warning((\n                \"Couldn't solve template \\\"{}\\\" with data {}\"\n            ).format(\n                self.description_template, json.dumps(fill_data, indent=4)\n            ))\n            return\n\n        if not description:\n            self.log.debug((\n                \"Skipping. Result of template is empty string.\"\n                \" Template \\\"{}\\\" Fill data: {}\"\n            ).format(\n                self.description_template, json.dumps(fill_data, indent=4)\n            ))\n            return\n\n        session = instance.context.data[\"ftrackSession\"]\n        for asset_version_data in asset_versions_data_by_id.values():\n            asset_version = asset_version_data[\"asset_version\"]\n\n            # Backwards compatibility for older settings using\n            #   attribute 'note_with_intent_template'\n\n            asset_version[\"comment\"] = description\n\n            try:\n                session.commit()\n                self.log.debug(\"Comment added to AssetVersion \\\"{}\\\"\".format(\n                    str(asset_version)\n                ))\n            except Exception:\n                tp, value, tb = sys.exc_info()\n                session.rollback()\n                session._configure_locations()\n                six.reraise(tp, value, tb)\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py",
    "content": "import os\nimport json\nimport copy\nimport pyblish.api\n\nfrom openpype.pipeline.publish import get_publish_repre_path\nfrom openpype.lib.openpype_version import get_openpype_version\nfrom openpype.lib.transcoding import (\n    get_ffprobe_streams,\n    convert_ffprobe_fps_to_float,\n)\nfrom openpype.lib.profiles_filtering import filter_profiles\nfrom openpype.lib.transcoding import VIDEO_EXTENSIONS\n\n\nclass IntegrateFtrackInstance(pyblish.api.InstancePlugin):\n    \"\"\"Collect ftrack component data (not integrate yet).\n\n    Add ftrack component list to instance.\n    \"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.48\n    label = \"Integrate Ftrack Component\"\n    families = [\"ftrack\"]\n\n    metadata_keys_to_label = {\n        \"openpype_version\": \"OpenPype version\",\n        \"frame_start\": \"Frame start\",\n        \"frame_end\": \"Frame end\",\n        \"duration\": \"Duration\",\n        \"width\": \"Resolution width\",\n        \"height\": \"Resolution height\",\n        \"fps\": \"FPS\",\n        \"codec\": \"Codec\"\n    }\n\n    family_mapping = {\n        \"camera\": \"cam\",\n        \"look\": \"look\",\n        \"mayaAscii\": \"scene\",\n        \"model\": \"geo\",\n        \"rig\": \"rig\",\n        \"setdress\": \"setdress\",\n        \"pointcache\": \"cache\",\n        \"render\": \"render\",\n        \"prerender\": \"render\",\n        \"render2d\": \"render\",\n        \"nukescript\": \"comp\",\n        \"write\": \"render\",\n        \"review\": \"mov\",\n        \"plate\": \"img\",\n        \"audio\": \"audio\",\n        \"workfile\": \"scene\",\n        \"animation\": \"cache\",\n        \"image\": \"img\",\n        \"reference\": \"reference\"\n    }\n    keep_first_subset_name_for_review = True\n    upload_reviewable_with_origin_name = False\n    asset_versions_status_profiles = []\n    additional_metadata_keys = []\n\n    def process(self, instance):\n        # QUESTION: should this be operating even for `farm` target?\n        self.log.debug(\"instance {}\".format(instance))\n\n        instance_repres = instance.data.get(\"representations\")\n        if not instance_repres:\n            self.log.info((\n                \"Skipping instance. Does not have any representations {}\"\n            ).format(str(instance)))\n            return\n\n        instance_version = instance.data.get(\"version\")\n        if instance_version is None:\n            raise ValueError(\"Instance version not set\")\n\n        version_number = int(instance_version)\n\n        family = instance.data[\"family\"]\n\n        # Perform case-insensitive family mapping\n        family_low = family.lower()\n        asset_type = instance.data.get(\"ftrackFamily\")\n        if not asset_type:\n            for map_family, map_value in self.family_mapping.items():\n                if map_family.lower() == family_low:\n                    asset_type = map_value\n                    break\n\n        if not asset_type:\n            asset_type = \"upload\"\n\n        self.log.debug(\n            \"Family: {}\\nMapping: {}\".format(family_low, self.family_mapping)\n        )\n        status_name = self._get_asset_version_status_name(instance)\n\n        # Base of component item data\n        # - create a copy of this object when want to use it\n        base_component_item = {\n            \"assettype_data\": {\n                \"short\": asset_type,\n            },\n            \"asset_data\": {\n                \"name\": instance.data[\"subset\"],\n            },\n            \"assetversion_data\": {\n                \"version\": version_number,\n                \"comment\": instance.context.data.get(\"comment\") or \"\",\n                \"status_name\": status_name\n            },\n            \"component_overwrite\": False,\n            # This can be change optionally\n            \"thumbnail\": False,\n            # These must be changed for each component\n            \"component_data\": None,\n            \"component_path\": None,\n            \"component_location\": None,\n            \"component_location_name\": None,\n            \"additional_data\": {}\n        }\n\n        # Filter types of representations\n        review_representations = []\n        thumbnail_representations = []\n        other_representations = []\n        has_movie_review = False\n        for repre in instance_repres:\n            repre_tags = repre.get(\"tags\") or []\n            # exclude representations with are going to be published on farm\n            if \"publish_on_farm\" in repre_tags:\n                continue\n\n            self.log.debug(\"Representation {}\".format(repre))\n\n            # include only thumbnail representations\n            if repre.get(\"thumbnail\") or \"thumbnail\" in repre_tags:\n                thumbnail_representations.append(repre)\n\n            # include only review representations\n            elif \"ftrackreview\" in repre_tags:\n                review_representations.append(repre)\n                if self._is_repre_video(repre):\n                    has_movie_review = True\n\n            else:\n                # include all other representations\n                other_representations.append(repre)\n\n        # Prepare ftrack locations\n        unmanaged_location_name = \"ftrack.unmanaged\"\n        ftrack_server_location_name = \"ftrack.server\"\n\n        # check if any outputName keys are in review_representations\n        # also check if any outputName keys are in thumbnail_representations\n        synced_multiple_output_names = []\n        for review_repre in review_representations:\n            review_output_name = review_repre.get(\"outputName\")\n            if not review_output_name:\n                continue\n            for thumb_repre in thumbnail_representations:\n                thumb_output_name = thumb_repre.get(\"outputName\")\n                if not thumb_output_name:\n                    continue\n                if (\n                    thumb_output_name == review_output_name\n                    # output name can be added also as tags during intermediate\n                    # files creation\n                    or thumb_output_name in review_repre.get(\"tags\", [])\n                ):\n                    synced_multiple_output_names.append(\n                        thumb_repre[\"outputName\"])\n        self.log.debug(\"Multiple output names: {}\".format(\n            synced_multiple_output_names\n        ))\n        multiple_synced_thumbnails = len(synced_multiple_output_names) > 1\n\n        # Components data\n        component_list = []\n        thumbnail_data_items = []\n\n        # Create thumbnail components\n        for repre in thumbnail_representations:\n            # get repre path from representation\n            # and return published_path if available\n            # the path is validated and if it does not exists it returns None\n            repre_path = get_publish_repre_path(\n                instance,\n                repre,\n                only_published=False\n            )\n            if not repre_path:\n                self.log.warning(\n                    \"Published path is not set or source was removed.\"\n                )\n                continue\n\n            # Create copy of base comp item and append it\n            thumbnail_item = copy.deepcopy(base_component_item)\n            thumbnail_item.update({\n                \"component_path\": repre_path,\n                \"component_data\": {\n                    \"name\": (\n                        \"thumbnail\" if review_representations\n                        else \"ftrackreview-image\"\n                    ),\n                    \"metadata\": self._prepare_image_component_metadata(\n                        repre,\n                        repre_path\n                    )\n                },\n                \"thumbnail\": True,\n                \"component_location_name\": ftrack_server_location_name\n            })\n\n            # add thumbnail data to items for future synchronization\n            current_item_data = {\n                \"sync_key\": repre.get(\"outputName\"),\n                \"representation\": repre,\n                \"item\": thumbnail_item\n            }\n            # Create copy of item before setting location\n            if \"delete\" not in repre.get(\"tags\", []):\n                src_comp = self._create_src_component(\n                    instance,\n                    repre,\n                    copy.deepcopy(thumbnail_item),\n                    unmanaged_location_name\n                )\n                component_list.append(src_comp)\n\n                current_item_data[\"src_component\"] = src_comp\n\n            # Add item to component list\n            thumbnail_data_items.append(current_item_data)\n\n        # Create review components\n        # Change asset name of each new component for review\n        multiple_reviewable = len(review_representations) > 1\n        extended_asset_name = None\n        for index, repre in enumerate(review_representations):\n            if not self._is_repre_video(repre) and has_movie_review:\n                self.log.debug(\"Movie repre has priority \"\n                               \"from {}\".format(repre))\n                continue\n\n            repre_path = get_publish_repre_path(instance, repre, False)\n            if not repre_path:\n                self.log.warning(\n                    \"Published path is not set and source was removed.\"\n                )\n                continue\n\n            # Create copy of base comp item and append it\n            review_item = copy.deepcopy(base_component_item)\n\n            # get first or synchronize thumbnail item\n            sync_thumbnail_item = None\n            sync_thumbnail_item_src = None\n            sync_thumbnail_data = self._get_matching_thumbnail_item(\n                repre,\n                thumbnail_data_items,\n                multiple_synced_thumbnails\n            )\n            if sync_thumbnail_data:\n                sync_thumbnail_item = sync_thumbnail_data.get(\"item\")\n                sync_thumbnail_item_src = sync_thumbnail_data.get(\n                    \"src_component\")\n\n            \"\"\"\n            Renaming asset name only to those components which are explicitly\n            allowed in settings. Usually clients wanted to keep first component\n            as untouched product name with version and any other assetVersion\n            to be named with extended form. The renaming will only happen if\n            there is more than one reviewable component and extended name is\n            not empty.\n            \"\"\"\n            extended_asset_name = self._make_extended_component_name(\n                base_component_item, repre, index)\n\n            if multiple_reviewable and extended_asset_name:\n                review_item[\"asset_data\"][\"name\"] = extended_asset_name\n                # rename also thumbnail\n                if sync_thumbnail_item:\n                    sync_thumbnail_item[\"asset_data\"][\"name\"] = (\n                        extended_asset_name\n                    )\n                # rename also src_thumbnail\n                if sync_thumbnail_item_src:\n                    sync_thumbnail_item_src[\"asset_data\"][\"name\"] = (\n                        extended_asset_name\n                    )\n\n            # adding thumbnail component to component list\n            if sync_thumbnail_item:\n                component_list.append(copy.deepcopy(sync_thumbnail_item))\n            if sync_thumbnail_item_src:\n                component_list.append(copy.deepcopy(sync_thumbnail_item_src))\n\n            # add metadata to review component\n            if self._is_repre_video(repre):\n                component_name = \"ftrackreview-mp4\"\n                metadata = self._prepare_video_component_metadata(\n                    instance, repre, repre_path, True\n                )\n            else:\n                component_name = \"ftrackreview-image\"\n                metadata = self._prepare_image_component_metadata(\n                    repre, repre_path\n                )\n                review_item[\"thumbnail\"] = True\n\n            review_item.update({\n                \"component_path\": repre_path,\n                \"component_data\": {\n                    \"name\": component_name,\n                    \"metadata\": metadata\n                },\n                \"component_location_name\": ftrack_server_location_name\n            })\n\n            # Create copy of item before setting location\n            if \"delete\" not in repre.get(\"tags\", []):\n                src_comp = self._create_src_component(\n                    instance,\n                    repre,\n                    copy.deepcopy(review_item),\n                    unmanaged_location_name\n                )\n                component_list.append(src_comp)\n\n            # Add item to component list\n            component_list.append(review_item)\n\n\n            if self.upload_reviewable_with_origin_name:\n                origin_name_component = copy.deepcopy(review_item)\n                filename = os.path.basename(repre_path)\n                origin_name_component[\"component_data\"][\"name\"] = (\n                    os.path.splitext(filename)[0]\n                )\n                component_list.append(origin_name_component)\n\n        # Add others representations as component\n        for repre in other_representations:\n            published_path = get_publish_repre_path(instance, repre, True)\n            if not published_path:\n                continue\n            # Create copy of base comp item and append it\n            other_item = copy.deepcopy(base_component_item)\n\n            # add extended name if any\n            if (\n                multiple_reviewable\n                and not self.keep_first_subset_name_for_review\n                and extended_asset_name\n            ):\n                other_item[\"asset_data\"][\"name\"] = extended_asset_name\n\n            other_item.update({\n                \"component_path\": published_path,\n                \"component_data\": {\n                    \"name\": repre[\"name\"],\n                    \"metadata\": self._prepare_component_metadata(\n                        instance, repre, published_path, False\n                    )\n                },\n                \"component_location_name\": unmanaged_location_name,\n            })\n\n            component_list.append(other_item)\n\n        def json_obj_parser(obj):\n            return str(obj)\n\n        self.log.debug(\"Components list: {}\".format(\n            json.dumps(\n                component_list,\n                sort_keys=True,\n                indent=4,\n                default=json_obj_parser\n            )\n        ))\n        instance.data[\"ftrackComponentsList\"] = component_list\n\n    def _get_matching_thumbnail_item(\n        self,\n        review_representation,\n        thumbnail_data_items,\n        are_multiple_synced_thumbnails\n    ):\n        \"\"\"Return matching thumbnail item from list of thumbnail items.\n\n        If a thumbnail item already exists, this should return it.\n        The benefit is that if an `outputName` key is found in\n        representation and is also used as a `sync_key`  in a thumbnail\n        data item, it can sync with that item.\n\n        Args:\n            review_representation (dict): Review representation\n            thumbnail_data_items (list): List of thumbnail data items\n            are_multiple_synced_thumbnails (bool): If there are multiple synced\n                thumbnails\n\n        Returns:\n            dict: Thumbnail data item or empty dict\n        \"\"\"\n        output_name = review_representation.get(\"outputName\")\n        tags = review_representation.get(\"tags\", [])\n        matching_thumbnail_item = {}\n        for thumb_item in thumbnail_data_items:\n            if (\n                are_multiple_synced_thumbnails\n                and (\n                    thumb_item[\"sync_key\"] == output_name\n                    # intermediate files can have preset name in tags\n                    # this is usually aligned with `outputName` distributed\n                    # during thumbnail creation in `need_thumbnail` tagging\n                    # workflow\n                    or thumb_item[\"sync_key\"] in tags\n                )\n            ):\n                # return only synchronized thumbnail if multiple\n                matching_thumbnail_item = thumb_item\n                break\n            elif not are_multiple_synced_thumbnails:\n                # return any first found thumbnail since we need thumbnail\n                # but dont care which one\n                matching_thumbnail_item = thumb_item\n                break\n\n        if not matching_thumbnail_item:\n            # WARNING: this can only happen if multiple thumbnails\n            # workflow is broken, since it found multiple matching outputName\n            # in representation but they do not align with any thumbnail item\n            self.log.warning(\n                \"No matching thumbnail item found for output name \"\n                \"'{}'\".format(output_name)\n            )\n            if not thumbnail_data_items:\n                self.log.warning(\n                    \"No thumbnail data items found\"\n                )\n                return {}\n            # as fallback return first thumbnail item\n            return thumbnail_data_items[0]\n\n        return matching_thumbnail_item\n\n    def _make_extended_component_name(\n            self, component_item, repre, iteration_index):\n        \"\"\" Returns the extended component name\n\n        Name is based on the asset name and representation name.\n\n        Args:\n            component_item (dict): The component item dictionary.\n            repre (dict): The representation dictionary.\n            iteration_index (int): The index of the iteration.\n\n        Returns:\n            str: The extended component name.\n\n        \"\"\"\n        # reset extended if no need for extended asset name\n        if self.keep_first_subset_name_for_review and iteration_index == 0:\n            return\n\n        # get asset name and define extended name variant\n        asset_name = component_item[\"asset_data\"][\"name\"]\n        return \"_\".join(\n            (asset_name, repre[\"name\"])\n        )\n\n    def _create_src_component(\n            self, instance, repre, component_item, location):\n        \"\"\"Create src component for thumbnail.\n\n        This will replicate the input component and change its name to\n        have suffix \"_src\".\n\n        Args:\n            instance (pyblish.api.Instance): Instance\n            repre (dict): Representation\n            component_item (dict): Component item\n            location (str): Location name\n\n        Returns:\n            dict: Component item\n        \"\"\"\n        # Make sure thumbnail is disabled\n        component_item[\"thumbnail\"] = False\n        # Set location\n        component_item[\"component_location_name\"] = location\n        # Modify name of component to have suffix \"_src\"\n        component_data = component_item[\"component_data\"]\n        component_name = component_data[\"name\"]\n        component_data[\"name\"] = component_name + \"_src\"\n        component_data[\"metadata\"] = self._prepare_component_metadata(\n            instance, repre, component_item[\"component_path\"], False\n        )\n        return component_item\n\n    def _collect_additional_metadata(self, streams):\n        pass\n\n    def _get_asset_version_status_name(self, instance):\n        if not self.asset_versions_status_profiles:\n            return None\n\n        # Prepare filtering data for new asset version status\n        anatomy_data = instance.data[\"anatomyData\"]\n        task_type = anatomy_data.get(\"task\", {}).get(\"type\")\n        filtering_criteria = {\n            \"families\": instance.data[\"family\"],\n            \"hosts\": instance.context.data[\"hostName\"],\n            \"task_types\": task_type\n        }\n        matching_profile = filter_profiles(\n            self.asset_versions_status_profiles,\n            filtering_criteria\n        )\n        if not matching_profile:\n            return None\n\n        return matching_profile[\"status\"] or None\n\n    def _prepare_component_metadata(\n        self, instance, repre, component_path, is_review=None\n    ):\n        if self._is_repre_video(repre):\n            return self._prepare_video_component_metadata(instance, repre,\n                                                          component_path,\n                                                          is_review)\n        else:\n            return self._prepare_image_component_metadata(repre,\n                                                          component_path)\n\n    def _prepare_video_component_metadata(\n        self, instance, repre, component_path, is_review=None\n    ):\n        metadata = {}\n        if \"openpype_version\" in self.additional_metadata_keys:\n            label = self.metadata_keys_to_label[\"openpype_version\"]\n            metadata[label] = get_openpype_version()\n\n        extension = os.path.splitext(component_path)[-1]\n        streams = []\n        try:\n            streams = get_ffprobe_streams(component_path)\n        except Exception:\n            self.log.debug(\n                \"Failed to retrieve information about \"\n                \"input {}\".format(component_path))\n\n        # Find video streams\n        video_streams = [\n            stream\n            for stream in streams\n            if stream[\"codec_type\"] == \"video\"\n        ]\n        # Skip if there are not video streams\n        #   - exr is special case which can have issues with reading through\n        #       ffmpegh but we want to set fps for it\n        if not video_streams and extension not in [\".exr\"]:\n            return metadata\n\n        stream_width = None\n        stream_height = None\n        stream_fps = None\n        frame_out = None\n        codec_label = None\n        for video_stream in video_streams:\n            codec_label = video_stream.get(\"codec_long_name\")\n            if not codec_label:\n                codec_label = video_stream.get(\"codec\")\n\n            if codec_label:\n                pix_fmt = video_stream.get(\"pix_fmt\")\n                if pix_fmt:\n                    codec_label += \" ({})\".format(pix_fmt)\n\n            tmp_width = video_stream.get(\"width\")\n            tmp_height = video_stream.get(\"height\")\n            if tmp_width and tmp_height:\n                stream_width = tmp_width\n                stream_height = tmp_height\n\n            input_framerate = video_stream.get(\"r_frame_rate\")\n            stream_duration = video_stream.get(\"duration\")\n            if input_framerate is None or stream_duration is None:\n                continue\n            try:\n                stream_fps = convert_ffprobe_fps_to_float(\n                    input_framerate\n                )\n            except ValueError:\n                self.log.warning(\n                    \"Could not convert ffprobe \"\n                    \"fps to float \\\"{}\\\"\".format(input_framerate))\n                continue\n\n            stream_width = tmp_width\n            stream_height = tmp_height\n\n            frame_out = float(stream_duration) * stream_fps\n            break\n\n        # Prepare FPS\n        instance_fps = instance.data.get(\"fps\")\n        if instance_fps is None:\n            instance_fps = instance.context.data[\"fps\"]\n\n        repre_fps = repre.get(\"fps\")\n        if repre_fps is not None:\n            repre_fps = float(repre_fps)\n\n        fps = stream_fps or repre_fps or instance_fps\n\n        # Prepare frame ranges\n        frame_start = repre.get(\"frameStartFtrack\")\n        frame_end = repre.get(\"frameEndFtrack\")\n        if frame_start is None or frame_end is None:\n            frame_start = instance.data[\"frameStart\"]\n            frame_end = instance.data[\"frameEnd\"]\n        duration = (frame_end - frame_start) + 1\n\n        for key, value in [\n            (\"fps\", fps),\n            (\"frame_start\", frame_start),\n            (\"frame_end\", frame_end),\n            (\"duration\", duration),\n            (\"width\", stream_width),\n            (\"height\", stream_height),\n            (\"fps\", fps),\n            (\"codec\", codec_label)\n        ]:\n            if not value or key not in self.additional_metadata_keys:\n                continue\n            label = self.metadata_keys_to_label[key]\n            metadata[label] = value\n\n        if not is_review:\n            ftr_meta = {}\n            if fps:\n                ftr_meta[\"frameRate\"] = fps\n\n            if stream_width and stream_height:\n                ftr_meta[\"width\"] = int(stream_width)\n                ftr_meta[\"height\"] = int(stream_height)\n            metadata[\"ftr_meta\"] = json.dumps(ftr_meta)\n            return metadata\n\n        # Frame end of uploaded video file should be duration in frames\n        # - frame start is always 0\n        # - frame end is duration in frames\n        if not frame_out:\n            frame_out = duration\n\n        # Ftrack documentation says that it is required to have\n        #   'width' and 'height' in review component. But with those values\n        #   review video does not play.\n        metadata[\"ftr_meta\"] = json.dumps({\n            \"frameIn\": 0,\n            \"frameOut\": frame_out,\n            \"frameRate\": float(fps)\n        })\n        return metadata\n\n    def _prepare_image_component_metadata(self, repre, component_path):\n        width = repre.get(\"width\")\n        height = repre.get(\"height\")\n        if not width or not height:\n            streams = []\n            try:\n                streams = get_ffprobe_streams(component_path)\n            except Exception:\n                self.log.debug(\n                    \"Failed to retrieve information \"\n                    \"about input {}\".format(component_path))\n\n            for stream in streams:\n                if \"width\" in stream and \"height\" in stream:\n                    width = stream[\"width\"]\n                    height = stream[\"height\"]\n                    break\n\n        metadata = {}\n        if width and height:\n            metadata = {\n                \"ftr_meta\": json.dumps({\n                    \"width\": width,\n                    \"height\": height,\n                    \"format\": \"image\"\n                })\n            }\n\n        return metadata\n\n    def _is_repre_video(self, repre):\n        repre_ext = \".{}\".format(repre[\"ext\"])\n        return repre_ext in VIDEO_EXTENSIONS\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py",
    "content": "\"\"\"\nRequires:\n    context > hostName\n    context > appName\n    context > appLabel\n    context > comment\n    context > ftrackSession\n    instance > ftrackIntegratedAssetVersionsData\n\"\"\"\n\nimport sys\nimport copy\n\nimport six\nimport pyblish.api\nfrom openpype.lib import StringTemplate\n\n\nclass IntegrateFtrackNote(pyblish.api.InstancePlugin):\n    \"\"\"Create comments in Ftrack.\"\"\"\n\n    # Must be after integrate asset new\n    order = pyblish.api.IntegratorOrder + 0.4999\n    label = \"Integrate Ftrack note\"\n    families = [\"ftrack\"]\n    optional = True\n\n    # Can be set in presets:\n    # - Allows only `intent` and `comment` keys\n    note_template = None\n    # Backwards compatibility\n    note_with_intent_template = \"{intent}: {comment}\"\n    # - note label must exist in Ftrack\n    note_labels = []\n\n    def process(self, instance):\n        # Check if there are any integrated AssetVersion entities\n        asset_versions_key = \"ftrackIntegratedAssetVersionsData\"\n        asset_versions_data_by_id = instance.data.get(asset_versions_key)\n        if not asset_versions_data_by_id:\n            self.log.info(\"There are any integrated AssetVersions\")\n            return\n\n        context = instance.context\n        host_name = context.data[\"hostName\"]\n        app_name = context.data[\"appName\"]\n        app_label = context.data[\"appLabel\"]\n        comment = instance.data[\"comment\"]\n        if not comment:\n            self.log.debug(\"Comment is not set.\")\n        else:\n            self.log.debug(\"Comment is set to `{}`\".format(comment))\n\n        session = context.data[\"ftrackSession\"]\n\n        intent = instance.context.data.get(\"intent\")\n        intent_label = None\n        if intent:\n            value = intent[\"value\"]\n            if value:\n                intent_label = intent[\"label\"] or value\n\n        # if intent label is set then format comment\n        # - it is possible that intent_label is equal to \"\" (empty string)\n        if intent_label:\n            self.log.debug(\n                \"Intent label is set to `{}`.\".format(intent_label)\n            )\n\n        else:\n            self.log.debug(\"Intent is not set.\")\n\n        user = session.query(\n            \"User where username is \\\"{}\\\"\".format(session.api_user)\n        ).first()\n        if not user:\n            self.log.warning(\n                \"Was not able to query current User {}\".format(\n                    session.api_user\n                )\n            )\n\n        labels = []\n        if self.note_labels:\n            all_labels = session.query(\"select id, name from NoteLabel\").all()\n            labels_by_low_name = {lab[\"name\"].lower(): lab for lab in all_labels}\n            for _label in self.note_labels:\n                label = labels_by_low_name.get(_label.lower())\n                if not label:\n                    self.log.warning(\n                        \"Note Label `{}` was not found.\".format(_label)\n                    )\n                    continue\n\n                labels.append(label)\n\n        base_format_data = {\n            \"host_name\": host_name,\n            \"app_name\": app_name,\n            \"app_label\": app_label,\n            \"source\": instance.data.get(\"source\", '')\n        }\n        if comment:\n            base_format_data[\"comment\"] = comment\n        for asset_version_data in asset_versions_data_by_id.values():\n            asset_version = asset_version_data[\"asset_version\"]\n            component_items = asset_version_data[\"component_items\"]\n\n            published_paths = set()\n            for component_item in component_items:\n                published_paths.add(component_item[\"component_path\"])\n\n            # Backwards compatibility for older settings using\n            #   attribute 'note_with_intent_template'\n            template = self.note_template\n            if template is None:\n                template = self.note_with_intent_template\n            format_data = copy.deepcopy(base_format_data)\n            format_data[\"published_paths\"] = \"<br/>\".join(\n                sorted(published_paths)\n            )\n            if intent:\n                if \"{intent}\" in template:\n                    format_data[\"intent\"] = intent_label\n                else:\n                    format_data[\"intent\"] = intent\n\n            note_text = StringTemplate.format_template(template, format_data)\n            if not note_text.solved:\n                self.log.debug((\n                    \"Note template require more keys then can be provided.\"\n                    \"\\nTemplate: {}\\nMissing values for keys:{}\\nData: {}\"\n                ).format(template, note_text.missing_keys, format_data))\n                continue\n\n            if not note_text:\n                self.log.debug((\n                    \"Note for AssetVersion {} would be empty. Skipping.\"\n                    \"\\nTemplate: {}\\nData: {}\"\n                ).format(asset_version[\"id\"], template, format_data))\n                continue\n            asset_version.create_note(note_text, author=user, labels=labels)\n\n            try:\n                session.commit()\n                self.log.debug(\"Note added to AssetVersion \\\"{}\\\"\".format(\n                    str(asset_version)\n                ))\n            except Exception:\n                tp, value, tb = sys.exc_info()\n                session.rollback()\n                session._configure_locations()\n                six.reraise(tp, value, tb)\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/integrate_ftrack_status.py",
    "content": "import copy\n\nimport pyblish.api\nfrom openpype.lib import filter_profiles\n\n\ndef create_chunks(iterable, chunk_size=None):\n    \"\"\"Separate iterable into multiple chunks by size.\n\n    Args:\n        iterable(list|tuple|set): Object that will be separated into chunks.\n        chunk_size(int): Size of one chunk. Default value is 200.\n\n    Returns:\n        list<list>: Chunked items.\n    \"\"\"\n    chunks = []\n\n    tupled_iterable = tuple(iterable)\n    if not tupled_iterable:\n        return chunks\n    iterable_size = len(tupled_iterable)\n    if chunk_size is None:\n        chunk_size = 200\n\n    if chunk_size < 1:\n        chunk_size = 1\n\n    for idx in range(0, iterable_size, chunk_size):\n        chunks.append(tupled_iterable[idx:idx + chunk_size])\n    return chunks\n\n\nclass CollectFtrackTaskStatuses(pyblish.api.ContextPlugin):\n    \"\"\"Collect available task statuses on the project.\n\n    This is preparation for integration of task statuses.\n\n    Requirements:\n        ftrackSession (ftrack_api.Session): Prepared ftrack session.\n\n    Provides:\n        ftrackTaskStatuses (dict[str, list[Any]]): Dictionary of available\n            task statuses on project by task type id.\n        ftrackStatusByTaskId (dict[str, str]): Empty dictionary of task\n            statuses by task id. Status on task can be set only once.\n            Value should be a name of status.\n    \"\"\"\n\n    # After 'CollectFtrackApi'\n    order = pyblish.api.CollectorOrder + 0.4992\n    label = \"Collect Ftrack Task Statuses\"\n    settings_category = \"ftrack\"\n\n    def process(self, context):\n        ftrack_session = context.data(\"ftrackSession\")\n        if ftrack_session is None:\n            self.log.info(\"Ftrack session is not created.\")\n            return\n\n        # Prepare available task statuses on the project\n        project_name = context.data[\"projectName\"]\n        project_entity = ftrack_session.query((\n            \"select project_schema from Project where full_name is \\\"{}\\\"\"\n        ).format(project_name)).one()\n        project_schema = project_entity[\"project_schema\"]\n\n        task_type_ids = {\n            task_type[\"id\"]\n            for task_type in ftrack_session.query(\"select id from Type\").all()\n        }\n        task_statuses_by_type_id = {\n            task_type_id: project_schema.get_statuses(\"Task\", task_type_id)\n            for task_type_id in task_type_ids\n        }\n        context.data[\"ftrackTaskStatuses\"] = task_statuses_by_type_id\n        context.data[\"ftrackStatusByTaskId\"] = {}\n        self.log.debug(\"Collected ftrack task statuses.\")\n\n\nclass IntegrateFtrackStatusBase(pyblish.api.InstancePlugin):\n    \"\"\"Base plugin for status collection.\n\n    Requirements:\n        projectName (str): Name of the project.\n        hostName (str): Name of the host.\n        ftrackSession (ftrack_api.Session): Prepared ftrack session.\n        ftrackTaskStatuses (dict[str, list[Any]]): Dictionary of available\n            task statuses on project by task type id.\n        ftrackStatusByTaskId (dict[str, str]): Empty dictionary of task\n            statuses by task id. Status on task can be set only once.\n            Value should be a name of status.\n    \"\"\"\n\n    active = False\n    settings_key = None\n    status_profiles = []\n\n    @classmethod\n    def apply_settings(cls, project_settings):\n        settings_key = cls.settings_key\n        if settings_key is None:\n            settings_key = cls.__name__\n\n        try:\n            settings = project_settings[\"ftrack\"][\"publish\"][settings_key]\n        except KeyError:\n            return\n\n        for key, value in settings.items():\n            setattr(cls, key, value)\n\n    def process(self, instance):\n        context = instance.context\n        # No profiles -> skip\n        profiles = self.get_status_profiles()\n        if not profiles:\n            project_name = context.data[\"projectName\"]\n            self.log.debug((\n                \"Status profiles are not filled for project \\\"{}\\\". Skipping\"\n            ).format(project_name))\n            return\n\n        # Task statuses were not collected -> skip\n        task_statuses_by_type_id = context.data.get(\"ftrackTaskStatuses\")\n        if not task_statuses_by_type_id:\n            self.log.debug(\n                \"Ftrack task statuses are not collected. Skipping.\")\n            return\n\n        self.prepare_status_names(context, instance, profiles)\n\n    def get_status_profiles(self):\n        \"\"\"List of profiles to determine status name.\n\n        Example profile item:\n            {\n                \"host_names\": [\"nuke\"],\n                \"task_types\": [\"Compositing\"],\n                \"task_names\": [\"Comp\"],\n                \"families\": [\"render\"],\n                \"subset_names\": [\"renderComp\"],\n                \"status_name\": \"Rendering\",\n            }\n\n        Returns:\n            list[dict[str, Any]]: List of profiles.\n        \"\"\"\n\n        return self.status_profiles\n\n    def prepare_status_names(self, context, instance, profiles):\n        if not self.is_valid_instance(context, instance):\n            return\n\n        filter_data = self.get_profile_filter_data(context, instance)\n        status_profile = filter_profiles(\n            profiles,\n            filter_data,\n            logger=self.log\n        )\n        if not status_profile:\n            return\n\n        status_name = status_profile[\"status_name\"]\n        if status_name:\n            self.fill_status(context, instance, status_name)\n\n    def get_profile_filter_data(self, context, instance):\n        task_entity = instance.data[\"ftrackTask\"]\n        return {\n            \"host_names\": context.data[\"hostName\"],\n            \"task_types\": task_entity[\"type\"][\"name\"],\n            \"task_names\": task_entity[\"name\"],\n            \"families\": instance.data[\"family\"],\n            \"subset_names\": instance.data[\"subset\"],\n        }\n\n    def is_valid_instance(self, context, instance):\n        \"\"\"Filter instances that should be processed.\n\n        Ignore instances that are not enabled for publishing or don't have\n        filled task. Also skip instances with tasks that already have defined\n        status.\n\n        Plugin should do more filtering which is custom for plugin logic.\n\n        Args:\n            context (pyblish.api.Context): Pyblish context.\n            instance (pyblish.api.Instance): Instance to process.\n\n        Returns:\n            list[pyblish.api.Instance]: List of instances that should be\n                processed.\n        \"\"\"\n\n        ftrack_status_by_task_id = context.data[\"ftrackStatusByTaskId\"]\n        # Skip disabled instances\n        if instance.data.get(\"publish\") is False:\n            return False\n\n        task_entity = instance.data.get(\"ftrackTask\")\n        if not task_entity:\n            self.log.debug(\n                \"Skipping instance  Does not have filled task\".format(\n                    instance.data[\"subset\"]))\n            return False\n\n        task_id = task_entity[\"id\"]\n        if task_id in ftrack_status_by_task_id:\n            self.log.debug(\"Status for task {} was already defined\".format(\n                task_entity[\"name\"]\n            ))\n            return False\n\n        return True\n\n    def fill_status(self, context, instance, status_name):\n        \"\"\"Fill status for instance task.\n\n        If task already had set status, it will be skipped.\n\n        Args:\n            context (pyblish.api.Context): Pyblish context.\n            instance (pyblish.api.Instance): Pyblish instance.\n            status_name (str): Name of status to set.\n        \"\"\"\n\n        task_entity = instance.data[\"ftrackTask\"]\n        task_id = task_entity[\"id\"]\n        ftrack_status_by_task_id = context.data[\"ftrackStatusByTaskId\"]\n        if task_id in ftrack_status_by_task_id:\n            self.log.debug(\"Status for task {} was already defined\".format(\n                task_entity[\"name\"]\n            ))\n            return\n\n        ftrack_status_by_task_id[task_id] = status_name\n        self.log.info((\n            \"Task {} will be set to \\\"{}\\\" status.\"\n        ).format(task_entity[\"name\"], status_name))\n\n\nclass IntegrateFtrackFarmStatus(IntegrateFtrackStatusBase):\n    \"\"\"Collect task status names for instances that are sent to farm.\n\n    Instance which has set \"farm\" key in data to 'True' is considered as will\n    be rendered on farm thus it's status should be changed.\n\n    Requirements:\n        projectName (str): Name of the project.\n        hostName (str): Name of the host.\n        ftrackSession (ftrack_api.Session): Prepared ftrack session.\n        ftrackTaskStatuses (dict[str, list[Any]]): Dictionary of available\n            task statuses on project by task type id.\n        ftrackStatusByTaskId (dict[str, str]): Empty dictionary of task\n            statuses by task id. Status on task can be set only once.\n            Value should be a name of status.\n    \"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.48\n    label = \"Ftrack Task Status To Farm Status\"\n    active = True\n\n    farm_status_profiles = []\n    status_profiles = None\n\n    def is_valid_instance(self, context, instance):\n        if not instance.data.get(\"farm\"):\n            self.log.debug(\"{} Won't be rendered on farm.\".format(\n                instance.data[\"subset\"]\n            ))\n            return False\n        return super(IntegrateFtrackFarmStatus, self).is_valid_instance(\n            context, instance)\n\n    def get_status_profiles(self):\n        if self.status_profiles is None:\n            profiles = copy.deepcopy(self.farm_status_profiles)\n            for profile in profiles:\n                profile[\"host_names\"] = profile.pop(\"hosts\")\n                profile[\"subset_names\"] = profile.pop(\"subsets\")\n            self.status_profiles = profiles\n        return self.status_profiles\n\n\nclass IntegrateFtrackLocalStatus(IntegrateFtrackStatusBase):\n    \"\"\"Collect task status names for instances that are published locally.\n\n    Instance which has set \"farm\" key in data to 'True' is considered as will\n    be rendered on farm thus it's status should be changed.\n\n    Requirements:\n        projectName (str): Name of the project.\n        hostName (str): Name of the host.\n        ftrackSession (ftrack_api.Session): Prepared ftrack session.\n        ftrackTaskStatuses (dict[str, list[Any]]): Dictionary of available\n            task statuses on project by task type id.\n        ftrackStatusByTaskId (dict[str, str]): Empty dictionary of task\n            statuses by task id. Status on task can be set only once.\n            Value should be a name of status.\n    \"\"\"\n\n    order = IntegrateFtrackFarmStatus.order + 0.001\n    label = \"Ftrack Task Status Local Publish\"\n    active = True\n    targets = [\"local\"]\n    settings_key = \"ftrack_task_status_local_publish\"\n\n    def is_valid_instance(self, context, instance):\n        if instance.data.get(\"farm\"):\n            self.log.debug(\"{} Will be rendered on farm.\".format(\n                instance.data[\"subset\"]\n            ))\n            return False\n        return super(IntegrateFtrackLocalStatus, self).is_valid_instance(\n            context, instance)\n\n\nclass IntegrateFtrackOnFarmStatus(IntegrateFtrackStatusBase):\n    \"\"\"Collect task status names for instances that are published on farm.\n\n    Requirements:\n        projectName (str): Name of the project.\n        hostName (str): Name of the host.\n        ftrackSession (ftrack_api.Session): Prepared ftrack session.\n        ftrackTaskStatuses (dict[str, list[Any]]): Dictionary of available\n            task statuses on project by task type id.\n        ftrackStatusByTaskId (dict[str, str]): Empty dictionary of task\n            statuses by task id. Status on task can be set only once.\n            Value should be a name of status.\n    \"\"\"\n\n    order = IntegrateFtrackLocalStatus.order + 0.001\n    label = \"Ftrack Task Status On Farm Status\"\n    active = True\n    targets = [\"farm\"]\n    settings_key = \"ftrack_task_status_on_farm_publish\"\n\n\nclass IntegrateFtrackTaskStatus(pyblish.api.ContextPlugin):\n    # Use order of Integrate Ftrack Api plugin and offset it before or after\n    base_order = pyblish.api.IntegratorOrder + 0.499\n    # By default is after Integrate Ftrack Api\n    order = base_order + 0.0001\n    label = \"Integrate Ftrack Task Status\"\n\n    @classmethod\n    def apply_settings(cls, project_settings):\n        \"\"\"Apply project settings to plugin.\n\n        Args:\n            project_settings (dict[str, Any]): Project settings.\n        \"\"\"\n\n        settings = (\n            project_settings[\"ftrack\"][\"publish\"][\"IntegrateFtrackTaskStatus\"]\n        )\n        diff = 0.001\n        if not settings[\"after_version_statuses\"]:\n            diff = -diff\n        cls.order = cls.base_order + diff\n\n    def process(self, context):\n        task_statuses_by_type_id = context.data.get(\"ftrackTaskStatuses\")\n        if not task_statuses_by_type_id:\n            self.log.debug(\"Ftrack task statuses are not collected. Skipping.\")\n            return\n\n        status_by_task_id = self._get_status_by_task_id(context)\n        if not status_by_task_id:\n            self.log.debug(\"No statuses to set. Skipping.\")\n            return\n\n        ftrack_session = context.data[\"ftrackSession\"]\n\n        task_entities = self._get_task_entities(\n            ftrack_session, status_by_task_id)\n\n        for task_entity in task_entities:\n            task_path = \"/\".join([\n                item[\"name\"] for item in task_entity[\"link\"]\n            ])\n            task_id = task_entity[\"id\"]\n            type_id = task_entity[\"type_id\"]\n            new_status = None\n            status_name = status_by_task_id[task_id]\n            self.log.debug(\n                \"Status to set {} on task {}.\".format(status_name, task_path))\n            status_name_low = status_name.lower()\n            available_statuses = task_statuses_by_type_id[type_id]\n            for status in available_statuses:\n                if status[\"name\"].lower() == status_name_low:\n                    new_status = status\n                    break\n\n            if new_status is None:\n                joined_statuses = \", \".join([\n                    \"'{}'\".format(status[\"name\"])\n                    for status in available_statuses\n                ])\n                self.log.debug((\n                    \"Status '{}' was not found in available statuses: {}.\"\n                ).format(status_name, joined_statuses))\n                continue\n\n            if task_entity[\"status_id\"] != new_status[\"id\"]:\n                task_entity[\"status_id\"] = new_status[\"id\"]\n\n                self.log.debug(\"Changing status of task '{}' to '{}'\".format(\n                    task_path, status_name\n                ))\n                ftrack_session.commit()\n\n    def _get_status_by_task_id(self, context):\n        status_by_task_id = context.data[\"ftrackStatusByTaskId\"]\n        return {\n            task_id: status_name\n            for task_id, status_name in status_by_task_id.items()\n            if status_name\n        }\n\n    def _get_task_entities(self, ftrack_session, status_by_task_id):\n        task_entities = []\n        for chunk_ids in create_chunks(status_by_task_id.keys()):\n            joined_ids = \",\".join(\n                ['\"{}\"'.format(task_id) for task_id in chunk_ids]\n            )\n            task_entities.extend(ftrack_session.query((\n                \"select id, type_id, status_id, link from Task\"\n                \" where id in ({})\"\n            ).format(joined_ids)).all())\n        return task_entities\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py",
    "content": "import sys\nimport collections\nimport six\nfrom copy import deepcopy\n\nimport pyblish.api\n\nfrom openpype.client import get_asset_by_id\nfrom openpype.lib import filter_profiles\nfrom openpype.pipeline import KnownPublishError\n\nCUST_ATTR_GROUP = \"openpype\"\n\n\n# Copy of `get_pype_attr` from openpype_modules.ftrack.lib\n# TODO import from openpype's ftrack module when possible to not break Python 2\ndef get_pype_attr(session, split_hierarchical=True):\n    custom_attributes = []\n    hier_custom_attributes = []\n    cust_attrs_query = (\n        \"select id, entity_type, object_type_id, is_hierarchical, default\"\n        \" from CustomAttributeConfiguration\"\n        # Kept `pype` for Backwards Compatibility\n        \" where group.name in (\\\"pype\\\", \\\"ayon\\\", \\\"{}\\\")\"\n    ).format(CUST_ATTR_GROUP)\n    all_avalon_attr = session.query(cust_attrs_query).all()\n    for cust_attr in all_avalon_attr:\n        if split_hierarchical and cust_attr[\"is_hierarchical\"]:\n            hier_custom_attributes.append(cust_attr)\n            continue\n\n        custom_attributes.append(cust_attr)\n\n    if split_hierarchical:\n        # return tuple\n        return custom_attributes, hier_custom_attributes\n\n    return custom_attributes\n\n\nclass IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):\n    \"\"\"\n    Create entities in ftrack based on collected data from premiere\n    Example of entry data:\n    {\n        \"ProjectXS\": {\n            \"entity_type\": \"Project\",\n            \"custom_attributes\": {\n                \"fps\": 24,...\n            },\n            \"tasks\": [\n                \"Compositing\",\n                \"Lighting\",... *task must exist as task type in project schema*\n            ],\n            \"childs\": {\n                \"sq01\": {\n                    \"entity_type\": \"Sequence\",\n                    ...\n                }\n            }\n        }\n    }\n    \"\"\"\n\n    order = pyblish.api.IntegratorOrder - 0.04\n    label = \"Integrate Hierarchy To Ftrack\"\n    families = [\"shot\"]\n    hosts = [\n        \"hiero\",\n        \"resolve\",\n        \"standalonepublisher\",\n        \"flame\",\n        \"traypublisher\"\n    ]\n    optional = False\n    create_task_status_profiles = []\n\n    def process(self, context):\n        if \"hierarchyContext\" not in context.data:\n            return\n\n        hierarchy_context = self._get_active_assets(context)\n        self.log.debug(\"__ hierarchy_context: {}\".format(hierarchy_context))\n\n        session = context.data[\"ftrackSession\"]\n        project_name = context.data[\"projectName\"]\n        project = session.query(\n            'select id, full_name from Project where full_name is \"{}\"'.format(\n                project_name\n            )\n        ).first()\n        if not project:\n            raise KnownPublishError(\n                \"Project \\\"{}\\\" was not found on ftrack.\".format(project_name)\n            )\n\n        self.session = session\n        self.ft_project = project\n        self.task_types = self.get_all_task_types(project)\n        self.task_statuses = self.get_task_statuses(project)\n\n        # import ftrack hierarchy\n        self.import_to_ftrack(context, project_name, hierarchy_context)\n\n    def query_ftrack_entitites(self, session, ft_project):\n        project_id = ft_project[\"id\"]\n        entities = session.query((\n            \"select id, name, parent_id\"\n            \" from TypedContext where project_id is \\\"{}\\\"\"\n        ).format(project_id)).all()\n\n        entities_by_id = {}\n        entities_by_parent_id = collections.defaultdict(list)\n        for entity in entities:\n            entities_by_id[entity[\"id\"]] = entity\n            parent_id = entity[\"parent_id\"]\n            entities_by_parent_id[parent_id].append(entity)\n\n        ftrack_hierarchy = []\n        ftrack_id_queue = collections.deque()\n        ftrack_id_queue.append((project_id, ftrack_hierarchy))\n        while ftrack_id_queue:\n            item = ftrack_id_queue.popleft()\n            ftrack_id, parent_list = item\n            if ftrack_id == project_id:\n                entity = ft_project\n                name = entity[\"full_name\"]\n            else:\n                entity = entities_by_id[ftrack_id]\n                name = entity[\"name\"]\n\n            children = []\n            parent_list.append({\n                \"name\": name,\n                \"low_name\": name.lower(),\n                \"entity\": entity,\n                \"children\": children,\n            })\n            for child in entities_by_parent_id[ftrack_id]:\n                ftrack_id_queue.append((child[\"id\"], children))\n        return ftrack_hierarchy\n\n    def find_matching_ftrack_entities(\n        self, hierarchy_context, ftrack_hierarchy\n    ):\n        walk_queue = collections.deque()\n        for entity_name, entity_data in hierarchy_context.items():\n            walk_queue.append(\n                (entity_name, entity_data, ftrack_hierarchy)\n            )\n\n        matching_ftrack_entities = []\n        while walk_queue:\n            item = walk_queue.popleft()\n            entity_name, entity_data, ft_children = item\n            matching_ft_child = None\n            for ft_child in ft_children:\n                if ft_child[\"low_name\"] == entity_name.lower():\n                    matching_ft_child = ft_child\n                    break\n\n            if matching_ft_child is None:\n                continue\n\n            entity = matching_ft_child[\"entity\"]\n            entity_data[\"ft_entity\"] = entity\n            matching_ftrack_entities.append(entity)\n\n            hierarchy_children = entity_data.get(\"childs\")\n            if not hierarchy_children:\n                continue\n\n            for child_name, child_data in hierarchy_children.items():\n                walk_queue.append(\n                    (child_name, child_data, matching_ft_child[\"children\"])\n                )\n        return matching_ftrack_entities\n\n    def query_custom_attribute_values(self, session, entities, hier_attrs):\n        attr_ids = {\n            attr[\"id\"]\n            for attr in hier_attrs\n        }\n        entity_ids = {\n            entity[\"id\"]\n            for entity in entities\n        }\n        output = {\n            entity_id: {}\n            for entity_id in entity_ids\n        }\n        if not attr_ids or not entity_ids:\n            return {}\n\n        joined_attr_ids = \",\".join(\n            ['\"{}\"'.format(attr_id) for attr_id in attr_ids]\n        )\n\n        # Query values in chunks\n        chunk_size = int(5000 / len(attr_ids))\n        # Make sure entity_ids is `list` for chunk selection\n        entity_ids = list(entity_ids)\n        results = []\n        for idx in range(0, len(entity_ids), chunk_size):\n            joined_entity_ids = \",\".join([\n                '\"{}\"'.format(entity_id)\n                for entity_id in entity_ids[idx:idx + chunk_size]\n            ])\n            results.extend(\n                session.query(\n                    (\n                        \"select value, entity_id, configuration_id\"\n                        \" from CustomAttributeValue\"\n                        \" where entity_id in ({}) and configuration_id in ({})\"\n                    ).format(\n                        joined_entity_ids,\n                        joined_attr_ids\n                    )\n                ).all()\n            )\n\n        for result in results:\n            attr_id = result[\"configuration_id\"]\n            entity_id = result[\"entity_id\"]\n            output[entity_id][attr_id] = result[\"value\"]\n\n        return output\n\n    def import_to_ftrack(self, context, project_name, hierarchy_context):\n        # Prequery hiearchical custom attributes\n        hier_attrs = get_pype_attr(self.session)[1]\n        hier_attr_by_key = {\n            attr[\"key\"]: attr\n            for attr in hier_attrs\n        }\n        # Query user entity (for comments)\n        user = self.session.query(\n            \"User where username is \\\"{}\\\"\".format(self.session.api_user)\n        ).first()\n        if not user:\n            self.log.warning(\n                \"Was not able to query current User {}\".format(\n                    self.session.api_user\n                )\n            )\n\n        # Query ftrack hierarchy with parenting\n        ftrack_hierarchy = self.query_ftrack_entitites(\n            self.session, self.ft_project)\n\n        # Fill ftrack entities to hierarchy context\n        # - there is no need to query entities again\n        matching_entities = self.find_matching_ftrack_entities(\n            hierarchy_context, ftrack_hierarchy)\n        # Query custom attribute values of each entity\n        custom_attr_values_by_id = self.query_custom_attribute_values(\n            self.session, matching_entities, hier_attrs)\n\n        # Get ftrack api module (as they are different per python version)\n        ftrack_api = context.data[\"ftrackPythonModule\"]\n\n        # Use queue of hierarchy items to process\n        import_queue = collections.deque()\n        for entity_name, entity_data in hierarchy_context.items():\n            import_queue.append(\n                (entity_name, entity_data, None)\n            )\n\n        while import_queue:\n            item = import_queue.popleft()\n            entity_name, entity_data, parent = item\n\n            entity_type = entity_data['entity_type']\n            self.log.debug(entity_data)\n\n            entity = entity_data.get(\"ft_entity\")\n            if entity is None and entity_type.lower() == \"project\":\n                raise AssertionError(\n                    \"Collected items are not in right order!\"\n                )\n\n            # Create entity if not exists\n            if entity is None:\n                entity = self.session.create(entity_type, {\n                    \"name\": entity_name,\n                    \"parent\": parent\n                })\n                entity_data[\"ft_entity\"] = entity\n\n            # self.log.info('entity: {}'.format(dict(entity)))\n            # CUSTOM ATTRIBUTES\n            custom_attributes = entity_data.get('custom_attributes', {})\n            instances = []\n            for instance in context:\n                instance_asset_name = instance.data.get(\"asset\")\n                if (\n                    instance_asset_name\n                    and instance_asset_name.lower() == entity[\"name\"].lower()\n                ):\n                    instances.append(instance)\n\n            for instance in instances:\n                instance.data[\"ftrackEntity\"] = entity\n\n            for key, cust_attr_value in custom_attributes.items():\n                if cust_attr_value is None:\n                    continue\n\n                hier_attr = hier_attr_by_key.get(key)\n                # Use simple method if key is not hierarchical\n                if not hier_attr:\n                    if key not in entity[\"custom_attributes\"]:\n                        raise KnownPublishError((\n                            \"Missing custom attribute in ftrack with name '{}'\"\n                        ).format(key))\n\n                    entity['custom_attributes'][key] = cust_attr_value\n                    continue\n\n                attr_id = hier_attr[\"id\"]\n                entity_values = custom_attr_values_by_id.get(entity[\"id\"], {})\n                # New value is defined by having id in values\n                # - it can be set to 'None' (ftrack allows that using API)\n                is_new_value = attr_id not in entity_values\n                attr_value = entity_values.get(attr_id)\n\n                # Use ftrack operations method to set hiearchical\n                # attribute value.\n                # - this is because there may be non hiearchical custom\n                #   attributes with different properties\n                entity_key = collections.OrderedDict((\n                    (\"configuration_id\", hier_attr[\"id\"]),\n                    (\"entity_id\", entity[\"id\"])\n                ))\n                op = None\n                if is_new_value:\n                    op = ftrack_api.operation.CreateEntityOperation(\n                        \"CustomAttributeValue\",\n                        entity_key,\n                        {\"value\": cust_attr_value}\n                    )\n\n                elif attr_value != cust_attr_value:\n                    op = ftrack_api.operation.UpdateEntityOperation(\n                        \"CustomAttributeValue\",\n                        entity_key,\n                        \"value\",\n                        attr_value,\n                        cust_attr_value\n                    )\n\n                if op is not None:\n                    self.session.recorded_operations.push(op)\n\n            if self.session.recorded_operations:\n                try:\n                    self.session.commit()\n                except Exception:\n                    tp, value, tb = sys.exc_info()\n                    self.session.rollback()\n                    self.session._configure_locations()\n                    six.reraise(tp, value, tb)\n\n            # TASKS\n            instances_by_task_name = collections.defaultdict(list)\n            for instance in instances:\n                task_name = instance.data.get(\"task\")\n                if task_name:\n                    instances_by_task_name[task_name.lower()].append(instance)\n\n            ftrack_status_by_task_id = context.data[\"ftrackStatusByTaskId\"]\n            tasks = entity_data.get('tasks', [])\n            existing_tasks = []\n            tasks_to_create = []\n            for child in entity['children']:\n                if child.entity_type.lower() == \"task\":\n                    task_name_low = child[\"name\"].lower()\n                    existing_tasks.append(task_name_low)\n\n                    for instance in instances_by_task_name[task_name_low]:\n                        instance.data[\"ftrackTask\"] = child\n\n            for task_name in tasks:\n                task_type = tasks[task_name][\"type\"]\n                if task_name.lower() in existing_tasks:\n                    print(\"Task {} already exists\".format(task_name))\n                    continue\n                tasks_to_create.append((task_name, task_type))\n\n            for task_name, task_type in tasks_to_create:\n                task_entity = self.create_task(\n                    task_name,\n                    task_type,\n                    entity,\n                    ftrack_status_by_task_id\n                )\n                for instance in instances_by_task_name[task_name.lower()]:\n                    instance.data[\"ftrackTask\"] = task_entity\n\n            # Incoming links.\n            self.create_links(project_name, entity_data, entity)\n            try:\n                self.session.commit()\n            except Exception:\n                tp, value, tb = sys.exc_info()\n                self.session.rollback()\n                self.session._configure_locations()\n                six.reraise(tp, value, tb)\n\n            # Create notes.\n            entity_comments = entity_data.get(\"comments\")\n            if user and entity_comments:\n                for comment in entity_comments:\n                    entity.create_note(comment, user)\n\n                try:\n                    self.session.commit()\n                except Exception:\n                    tp, value, tb = sys.exc_info()\n                    self.session.rollback()\n                    self.session._configure_locations()\n                    six.reraise(tp, value, tb)\n\n            # Import children.\n            children = entity_data.get(\"childs\")\n            if not children:\n                continue\n\n            for entity_name, entity_data in children.items():\n                import_queue.append(\n                    (entity_name, entity_data, entity)\n                )\n\n    def create_links(self, project_name, entity_data, entity):\n        # Clear existing links.\n        for link in entity.get(\"incoming_links\", []):\n            self.session.delete(link)\n            try:\n                self.session.commit()\n            except Exception:\n                tp, value, tb = sys.exc_info()\n                self.session.rollback()\n                self.session._configure_locations()\n                six.reraise(tp, value, tb)\n\n        # Create new links.\n        for asset_id in entity_data.get(\"inputs\", []):\n            asset_doc = get_asset_by_id(project_name, asset_id)\n            ftrack_id = None\n            if asset_doc:\n                ftrack_id = asset_doc[\"data\"].get(\"ftrackId\")\n            if not ftrack_id:\n                continue\n\n            assetbuild = self.session.get(\"AssetBuild\", ftrack_id)\n            self.log.debug(\n                \"Creating link from {0} to {1}\".format(\n                    assetbuild[\"name\"], entity[\"name\"]\n                )\n            )\n            self.session.create(\n                \"TypedContextLink\", {\"from\": assetbuild, \"to\": entity}\n            )\n\n    def get_all_task_types(self, project):\n        tasks = {}\n        proj_template = project['project_schema']\n        temp_task_types = proj_template['_task_type_schema']['types']\n\n        for type in temp_task_types:\n            if type['name'] not in tasks:\n                tasks[type['name']] = type\n\n        return tasks\n\n    def get_task_statuses(self, project_entity):\n        project_schema = project_entity[\"project_schema\"]\n        task_workflow_statuses = project_schema[\"_task_workflow\"][\"statuses\"]\n        return {\n            status[\"id\"]: status\n            for status in task_workflow_statuses\n        }\n\n    def create_task(self, name, task_type, parent, ftrack_status_by_task_id):\n        filter_data = {\n            \"task_names\": name,\n            \"task_types\": task_type\n        }\n        profile = filter_profiles(\n            self.create_task_status_profiles,\n            filter_data\n        )\n        status_id = None\n        status_name = None\n        if profile:\n            status_name = profile[\"status_name\"]\n            status_name_low = status_name.lower()\n            for _status_id, status in self.task_statuses.items():\n                if status[\"name\"].lower() == status_name_low:\n                    status_id = _status_id\n                    status_name = status[\"name\"]\n                    break\n\n            if status_id is None:\n                self.log.warning(\n                    \"Task status \\\"{}\\\" was not found\".format(status_name)\n                )\n\n        task = self.session.create('Task', {\n            'name': name,\n            'parent': parent\n        })\n        # TODO not secured!!! - check if task_type exists\n        self.log.info(task_type)\n        self.log.info(self.task_types)\n        task['type'] = self.task_types[task_type]\n        if status_id is not None:\n            task[\"status_id\"] = status_id\n\n        try:\n            self.session.commit()\n        except Exception:\n            tp, value, tb = sys.exc_info()\n            self.session.rollback()\n            self.session._configure_locations()\n            six.reraise(tp, value, tb)\n\n        if status_id is not None:\n            ftrack_status_by_task_id[task[\"id\"]] = None\n        return task\n\n    def _get_active_assets(self, context):\n        \"\"\" Returns only asset dictionary.\n            Usually the last part of deep dictionary which\n            is not having any children\n        \"\"\"\n        def get_pure_hierarchy_data(input_dict):\n            input_dict_copy = deepcopy(input_dict)\n            for key in input_dict.keys():\n                self.log.debug(\"__ key: {}\".format(key))\n                # check if child key is available\n                if input_dict[key].get(\"childs\"):\n                    # loop deeper\n                    input_dict_copy[\n                        key][\"childs\"] = get_pure_hierarchy_data(\n                            input_dict[key][\"childs\"])\n                elif key not in active_assets:\n                    input_dict_copy.pop(key, None)\n            return input_dict_copy\n\n        hierarchy_context = context.data[\"hierarchyContext\"]\n\n        active_assets = set()\n        # filter only the active publishing insatnces\n        for instance in context:\n            if instance.data.get(\"publish\") is False:\n                continue\n\n            asset_name = instance.data.get(\"asset\")\n            if asset_name:\n                active_assets.add(asset_name)\n\n        # remove duplicity in list\n        self.log.debug(\"__ active_assets: {}\".format(list(active_assets)))\n\n        return get_pure_hierarchy_data(hierarchy_context)\n"
  },
  {
    "path": "openpype/modules/ftrack/plugins/publish/validate_custom_ftrack_attributes.py",
    "content": "import pyblish.api\nfrom openpype.pipeline.publish import ValidateContentsOrder\n\n\nclass ValidateFtrackAttributes(pyblish.api.InstancePlugin):\n    \"\"\"\n    This will validate attributes in ftrack against data in scene.\n\n    Attributes to be validated are specified in:\n\n        `$OPENPYPE_CONFIG/presets/<host>/ftrack_attributes.json`\n\n    This is array (list) of checks in format:\n    [\n        [<attribute>, <operator>, <expression>]\n    ]\n\n    Where <attribute> is name of ftrack attribute, <operator> is one of:\n\n        \"is\", is_not\", \"greater_than\", \"less_than\", \"contains\", \"not_contains\",\n        \"starts_with\", \"ends_with\"\n\n    <expression> is python code that is evaluated by validator. This allows\n    you to fetch whatever value in scene you want, for example in Maya:\n\n    [\n        \"fps\", \"is\",\n        \"from maya import mel; out = mel.eval('currentTimeUnitToFPS()')\"\n    ]\n\n    will test if ftrack fps attribute on current Task parent is same as fps\n    info we get from maya. Store the value you need to compare in\n    variable `out` in your expression.\n    \"\"\"\n\n    label = \"Validate Custom Ftrack Attributes\"\n    order = ValidateContentsOrder\n    families = [\"ftrack\"]\n    optional = True\n    # Ignore standalone host, because it does not have an Ftrack entity\n    # associated.\n    hosts = [\n        \"blender\",\n        \"fusion\",\n        \"harmony\",\n        \"houdini\",\n        \"maya\",\n        \"nuke\",\n        \"hiero\",\n        \"photoshop\",\n        \"premiere\",\n        \"resolve\",\n        \"unreal\"\n    ]\n\n    def process(self, instance):\n        context = instance.context\n        task = context.data.get('ftrackTask', False)\n        if not task:\n            self._raise(AttributeError,\n                        \"Missing FTrack Task entity in context\")\n\n        host = pyblish.api.current_host()\n        to_check = self.ftrack_custom_attributes.get(host, {})\n\n        if not to_check:\n            self.log.warning(\"ftrack_attributes preset not found\")\n            return\n\n        self.log.info(\"getting attributes from ftrack ...\")\n        # get parent of task\n        custom_attributes = {}\n        try:\n            parent = task[\"parent\"]\n            custom_attributes = parent[\"custom_attributes\"].items()\n        except KeyError:\n            self._raise(KeyError, \"missing `parent` or `attributes`\")\n\n        custom_attributes = dict(custom_attributes)\n\n        # get list of hierarchical attributes from ftrack\n        session = context.data[\"ftrackSession\"]\n\n        custom_hier_attributes = self._get_custom_hier_attrs(session)\n        custom_attributes = {}\n        _nonhier = {}\n        custom_hier_attributes = {k: None for k in custom_hier_attributes}\n\n        for key, value in dict(parent[\"custom_attributes\"]).items():\n            if key in custom_hier_attributes:\n                custom_hier_attributes[key] = value\n            else:\n                _nonhier[key] = value\n\n        custom_hier_values = self._get_hierarchical_values(\n            custom_hier_attributes, parent)\n\n        custom_hier_values.update(_nonhier)\n\n        errors = []\n        attribs = custom_hier_values\n        for check in to_check:\n            ev = {}\n            # WARNING(Ondrej Samohel): This is really not secure as we are\n            # basically executing user code. But there's no other way to make\n            # it flexible enough for users to get stuff from\n            exec(str(check[2]), {}, ev)\n            if not ev.get(\"out\"):\n                errors.append(\"{} code doesn't return 'out': '{}'\".format(\n                    check[0], check[2]))\n                continue\n            if check[0] in attribs:\n                if check[1] == \"is\":\n                    if attribs[check[0]] != ev[\"out\"]:\n                        errors.append(\"{}: {} is not {}\".format(\n                            check[0], attribs[check[0]], ev[\"out\"]))\n                elif check[1] == \"is_not\":\n                    if attribs[check[0]] == ev[\"out\"]:\n                        errors.append(\"{}: {} is {}\".format(\n                            check[0], attribs[check[0]], ev[\"out\"]))\n                elif check[1] == \"less_than\":\n                    if attribs[check[0]] < ev[\"out\"]:\n                        errors.append(\"{}: {} is greater {}\".format(\n                            check[0], attribs[check[0]], ev[\"out\"]))\n                elif check[1] == \"greater_than\":\n                    if attribs[check[0]] < ev[\"out\"]:\n                        errors.append(\"{}: {} is less {}\".format(\n                            check[0], attribs[check[0]], ev[\"out\"]))\n                elif check[1] == \"contains\":\n                    if attribs[check[0]] in ev[\"out\"]:\n                        errors.append(\"{}: {} does not contain {}\".format(\n                            check[0], attribs[check[0]], ev[\"out\"]))\n                elif check[1] == \"not_contains\":\n                    if attribs[check[0]] not in ev[\"out\"]:\n                        errors.append(\"{}: {} contains {}\".format(\n                            check[0], attribs[check[0]], ev[\"out\"]))\n                elif check[1] == \"starts_with\":\n                    if attribs[check[0]].startswith(ev[\"out\"]):\n                        errors.append(\"{}: {} does not starts with {}\".format(\n                            check[0], attribs[check[0]], ev[\"out\"]))\n                elif check[1] == \"ends_with\":\n                    if attribs[check[0]].endswith(ev[\"out\"]):\n                        errors.append(\"{}: {} does not end with {}\".format(\n                            check[0], attribs[check[0]], ev[\"out\"]))\n\n        if errors:\n            self.log.error('There are invalid values for attributes:')\n            for e in errors:\n                self.log.error(e)\n            raise ValueError(\"ftrack attributes doesn't match\")\n\n    def _get_custom_hier_attrs(self, session):\n        hier_custom_attributes = []\n        cust_attrs_query = (\n            \"select id, entity_type, object_type_id, is_hierarchical\"\n            \" from CustomAttributeConfiguration\"\n        )\n        all_avalon_attr = session.query(cust_attrs_query).all()\n        for cust_attr in all_avalon_attr:\n            if cust_attr[\"is_hierarchical\"]:\n                hier_custom_attributes.append(cust_attr[\"key\"])\n\n        return hier_custom_attributes\n\n    def _get_hierarchical_values(self, keys_dict, entity):\n        # check values already set\n        _set_keys = []\n        for key, value in keys_dict.items():\n            if value is not None:\n                _set_keys.append(key)\n\n        # pop set values from keys_dict\n        set_keys = {}\n        for key in _set_keys:\n            set_keys[key] = keys_dict.pop(key)\n\n        # find if entity has set values and pop them out\n        keys_to_pop = []\n        for key in keys_dict.keys():\n            _val = entity[\"custom_attributes\"][key]\n            if _val:\n                keys_to_pop.append(key)\n                set_keys[key] = _val\n\n        for key in keys_to_pop:\n            keys_dict.pop(key)\n\n        # if there are not keys to find value return found\n        if not keys_dict:\n            return set_keys\n\n        # end recursion if entity is project\n        if entity.entity_type.lower() == \"project\":\n            for key, value in keys_dict.items():\n                set_keys[key] = value\n\n        else:\n            result = self._get_hierarchical_values(keys_dict, entity[\"parent\"])\n            for key, value in result.items():\n                set_keys[key] = value\n\n        return set_keys\n\n    def _raise(self, exc, msg):\n        self.log.error(msg)\n        raise exc(msg)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/.gitignore",
    "content": "# General\n*.py[cod]\n\n# Packages\n*.egg\n*.egg-info\ndist\nbuild\n.eggs/\neggs\nparts\nbin\nvar\nsdist\ndevelop-eggs\n.installed.cfg\nlib\nlib64\n__pycache__\n\n# Installer logs\npip-log.txt\n\n# Unit test / coverage reports\n.coverage\n.tox\n\n# Caches\nThumbs.db\n\n# Development\n.project\n.pydevproject\n.settings\n.idea/\n.history/\n.vscode/\n\n# Testing\n.cache\ntest-reports/*\n.pytest_cache/*"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/LICENSE.python",
    "content": "A. HISTORY OF THE SOFTWARE\n==========================\n\nPython was created in the early 1990s by Guido van Rossum at Stichting\nMathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands\nas a successor of a language called ABC.  Guido remains Python's\nprincipal author, although it includes many contributions from others.\n\nIn 1995, Guido continued his work on Python at the Corporation for\nNational Research Initiatives (CNRI, see http://www.cnri.reston.va.us)\nin Reston, Virginia where he released several versions of the\nsoftware.\n\nIn May 2000, Guido and the Python core development team moved to\nBeOpen.com to form the BeOpen PythonLabs team.  In October of the same\nyear, the PythonLabs team moved to Digital Creations, which became\nZope Corporation.  In 2001, the Python Software Foundation (PSF, see\nhttps://www.python.org/psf/) was formed, a non-profit organization\ncreated specifically to own Python-related Intellectual Property.\nZope Corporation was a sponsoring member of the PSF.\n\nAll Python releases are Open Source (see http://www.opensource.org for\nthe Open Source Definition).  Historically, most, but not all, Python\nreleases have also been GPL-compatible; the table below summarizes\nthe various releases.\n\n    Release         Derived     Year        Owner       GPL-\n                    from                                compatible? (1)\n\n    0.9.0 thru 1.2              1991-1995   CWI         yes\n    1.3 thru 1.5.2  1.2         1995-1999   CNRI        yes\n    1.6             1.5.2       2000        CNRI        no\n    2.0             1.6         2000        BeOpen.com  no\n    1.6.1           1.6         2001        CNRI        yes (2)\n    2.1             2.0+1.6.1   2001        PSF         no\n    2.0.1           2.0+1.6.1   2001        PSF         yes\n    2.1.1           2.1+2.0.1   2001        PSF         yes\n    2.1.2           2.1.1       2002        PSF         yes\n    2.1.3           2.1.2       2002        PSF         yes\n    2.2 and above   2.1.1       2001-now    PSF         yes\n\nFootnotes:\n\n(1) GPL-compatible doesn't mean that we're distributing Python under\n    the GPL.  All Python licenses, unlike the GPL, let you distribute\n    a modified version without making your changes open source.  The\n    GPL-compatible licenses make it possible to combine Python with\n    other software that is released under the GPL; the others don't.\n\n(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,\n    because its license has a choice of law clause.  According to\n    CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1\n    is \"not incompatible\" with the GPL.\n\nThanks to the many outside volunteers who have worked under Guido's\ndirection to make these releases possible.\n\n\nB. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON\n===============================================================\n\nPYTHON SOFTWARE FOUNDATION LICENSE VERSION 2\n--------------------------------------------\n\n1. This LICENSE AGREEMENT is between the Python Software Foundation\n(\"PSF\"), and the Individual or Organization (\"Licensee\") accessing and\notherwise using this software (\"Python\") in source or binary form and\nits associated documentation.\n\n2. Subject to the terms and conditions of this License Agreement, PSF hereby\ngrants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,\nanalyze, test, perform and/or display publicly, prepare derivative works,\ndistribute, and otherwise use Python alone or in any derivative version,\nprovided, however, that PSF's License Agreement and PSF's notice of copyright,\ni.e., \"Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,\n2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 Python Software Foundation;\nAll Rights Reserved\" are retained in Python alone or in any derivative version\nprepared by Licensee.\n\n3. In the event Licensee prepares a derivative work that is based on\nor incorporates Python or any part thereof, and wants to make\nthe derivative work available to others as provided herein, then\nLicensee hereby agrees to include in any such work a brief summary of\nthe changes made to Python.\n\n4. PSF is making Python available to Licensee on an \"AS IS\"\nbasis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR\nIMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND\nDISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS\nFOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT\nINFRINGE ANY THIRD PARTY RIGHTS.\n\n5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON\nFOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS\nA RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,\nOR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.\n\n6. This License Agreement will automatically terminate upon a material\nbreach of its terms and conditions.\n\n7. Nothing in this License Agreement shall be deemed to create any\nrelationship of agency, partnership, or joint venture between PSF and\nLicensee.  This License Agreement does not grant permission to use PSF\ntrademarks or trade name in a trademark sense to endorse or promote\nproducts or services of Licensee, or any third party.\n\n8. By copying, installing or otherwise using Python, Licensee\nagrees to be bound by the terms and conditions of this License\nAgreement.\n\n\nBEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0\n-------------------------------------------\n\nBEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1\n\n1. This LICENSE AGREEMENT is between BeOpen.com (\"BeOpen\"), having an\noffice at 160 Saratoga Avenue, Santa Clara, CA 95051, and the\nIndividual or Organization (\"Licensee\") accessing and otherwise using\nthis software in source or binary form and its associated\ndocumentation (\"the Software\").\n\n2. Subject to the terms and conditions of this BeOpen Python License\nAgreement, BeOpen hereby grants Licensee a non-exclusive,\nroyalty-free, world-wide license to reproduce, analyze, test, perform\nand/or display publicly, prepare derivative works, distribute, and\notherwise use the Software alone or in any derivative version,\nprovided, however, that the BeOpen Python License is retained in the\nSoftware, alone or in any derivative version prepared by Licensee.\n\n3. BeOpen is making the Software available to Licensee on an \"AS IS\"\nbasis.  BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR\nIMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND\nDISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS\nFOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT\nINFRINGE ANY THIRD PARTY RIGHTS.\n\n4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE\nSOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS\nAS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY\nDERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.\n\n5. This License Agreement will automatically terminate upon a material\nbreach of its terms and conditions.\n\n6. This License Agreement shall be governed by and interpreted in all\nrespects by the law of the State of California, excluding conflict of\nlaw provisions.  Nothing in this License Agreement shall be deemed to\ncreate any relationship of agency, partnership, or joint venture\nbetween BeOpen and Licensee.  This License Agreement does not grant\npermission to use BeOpen trademarks or trade names in a trademark\nsense to endorse or promote products or services of Licensee, or any\nthird party.  As an exception, the \"BeOpen Python\" logos available at\nhttp://www.pythonlabs.com/logos.html may be used according to the\npermissions granted on that web page.\n\n7. By copying, installing or otherwise using the software, Licensee\nagrees to be bound by the terms and conditions of this License\nAgreement.\n\n\nCNRI LICENSE AGREEMENT FOR PYTHON 1.6.1\n---------------------------------------\n\n1. This LICENSE AGREEMENT is between the Corporation for National\nResearch Initiatives, having an office at 1895 Preston White Drive,\nReston, VA 20191 (\"CNRI\"), and the Individual or Organization\n(\"Licensee\") accessing and otherwise using Python 1.6.1 software in\nsource or binary form and its associated documentation.\n\n2. Subject to the terms and conditions of this License Agreement, CNRI\nhereby grants Licensee a nonexclusive, royalty-free, world-wide\nlicense to reproduce, analyze, test, perform and/or display publicly,\nprepare derivative works, distribute, and otherwise use Python 1.6.1\nalone or in any derivative version, provided, however, that CNRI's\nLicense Agreement and CNRI's notice of copyright, i.e., \"Copyright (c)\n1995-2001 Corporation for National Research Initiatives; All Rights\nReserved\" are retained in Python 1.6.1 alone or in any derivative\nversion prepared by Licensee.  Alternately, in lieu of CNRI's License\nAgreement, Licensee may substitute the following text (omitting the\nquotes): \"Python 1.6.1 is made available subject to the terms and\nconditions in CNRI's License Agreement.  This Agreement together with\nPython 1.6.1 may be located on the Internet using the following\nunique, persistent identifier (known as a handle): 1895.22/1013.  This\nAgreement may also be obtained from a proxy server on the Internet\nusing the following URL: http://hdl.handle.net/1895.22/1013\".\n\n3. In the event Licensee prepares a derivative work that is based on\nor incorporates Python 1.6.1 or any part thereof, and wants to make\nthe derivative work available to others as provided herein, then\nLicensee hereby agrees to include in any such work a brief summary of\nthe changes made to Python 1.6.1.\n\n4. CNRI is making Python 1.6.1 available to Licensee on an \"AS IS\"\nbasis.  CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR\nIMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND\nDISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS\nFOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT\nINFRINGE ANY THIRD PARTY RIGHTS.\n\n5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON\n1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS\nA RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,\nOR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.\n\n6. This License Agreement will automatically terminate upon a material\nbreach of its terms and conditions.\n\n7. This License Agreement shall be governed by the federal\nintellectual property law of the United States, including without\nlimitation the federal copyright law, and, to the extent such\nU.S. federal law does not apply, by the law of the Commonwealth of\nVirginia, excluding Virginia's conflict of law provisions.\nNotwithstanding the foregoing, with regard to derivative works based\non Python 1.6.1 that incorporate non-separable material that was\npreviously distributed under the GNU General Public License (GPL), the\nlaw of the Commonwealth of Virginia shall govern this License\nAgreement only as to issues arising under or with respect to\nParagraphs 4, 5, and 7 of this License Agreement.  Nothing in this\nLicense Agreement shall be deemed to create any relationship of\nagency, partnership, or joint venture between CNRI and Licensee.  This\nLicense Agreement does not grant permission to use CNRI trademarks or\ntrade name in a trademark sense to endorse or promote products or\nservices of Licensee, or any third party.\n\n8. By clicking on the \"ACCEPT\" button where indicated, or by copying,\ninstalling or otherwise using Python 1.6.1, Licensee agrees to be\nbound by the terms and conditions of this License Agreement.\n\n        ACCEPT\n\n\nCWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2\n--------------------------------------------------\n\nCopyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,\nThe Netherlands.  All rights reserved.\n\nPermission to use, copy, modify, and distribute this software and its\ndocumentation for any purpose and without fee is hereby granted,\nprovided that the above copyright notice appear in all copies and that\nboth that copyright notice and this permission notice appear in\nsupporting documentation, and that the name of Stichting Mathematisch\nCentrum or CWI not be used in advertising or publicity pertaining to\ndistribution of the software without specific, written prior\npermission.\n\nSTICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO\nTHIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE\nFOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\nOF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/LICENSE.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include resource *.py\nrecursive-include doc *.rst *.conf *.py *.png *.css\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/README.rst",
    "content": "#################\nftrack Python API\n#################\n\nPython API for ftrack.\n\n.. important::\n\n    This is the new Python client for the ftrack API. If you are migrating from\n    the old client then please read the dedicated `migration guide <http://ftrack-python-api.rtd.ftrack.com/en/stable/release/migrating_from_old_api.html>`_.\n\n*************\nDocumentation\n*************\n\nFull documentation, including installation and setup guides, can be found at\nhttp://ftrack-python-api.rtd.ftrack.com/en/stable/\n\n*********************\nCopyright and license\n*********************\n\nCopyright (c) 2014 ftrack\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use\nthis work except in compliance with the License. You may obtain a copy of the\nLicense in the LICENSE.txt file, or at:\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software distributed\nunder the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied. See the License for the\nspecific language governing permissions and limitations under the License."
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/bitbucket-pipelines.yml",
    "content": "# Test configuration for bitbucket pipelines.\noptions:\n  max-time: 20\ndefinitions:\n  services:\n    ftrack:\n      image:\n        name: ftrackdocker/test-server:latest\n        username: $DOCKER_HUB_USERNAME\n        password: $DOCKER_HUB_PASSWORD\n        email: $DOCKER_HUB_EMAIL\npipelines:\n  default:\n    - parallel:  \n      - step:\n          name: run tests against python 2.7.x\n          image: python:2.7\n          caches:\n            - pip\n          services:\n            - ftrack\n          script:\n            - bash -c 'while [[ \"$(curl -s -o /dev/null -w ''%{http_code}'' $FTRACK_SERVER)\" != \"200\" ]]; do sleep 1; done'\n            - python setup.py test"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/_static/ftrack.css",
    "content": "@import \"css/theme.css\";\n\n.domain-summary li {\n    float: left;\n    min-width: 12em;\n}\n\n.domain-summary ul:before, ul:after {\n    content: '';\n    clear: both;\n    display:block;\n}\n\n.rst-content table.docutils td:last-child {\n    white-space: normal;\n}\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/accessor/base.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n************************\nftrack_api.accessor.base\n************************\n\n.. automodule:: ftrack_api.accessor.base\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/accessor/disk.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n************************\nftrack_api.accessor.disk\n************************\n\n.. automodule:: ftrack_api.accessor.disk\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/accessor/index.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n*******************\nftrack_api.accessor\n*******************\n\n.. automodule:: ftrack_api.accessor\n\n.. toctree::\n    :maxdepth: 1\n    :glob:\n\n    *\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/accessor/server.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n************************\nftrack_api.accessor.server\n************************\n\n.. automodule:: ftrack_api.accessor.server\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/attribute.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n********************\nftrack_api.attribute\n********************\n\n.. automodule:: ftrack_api.attribute\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/cache.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n****************\nftrack_api.cache\n****************\n\n.. automodule:: ftrack_api.cache\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/collection.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n*********************\nftrack_api.collection\n*********************\n\n.. automodule:: ftrack_api.collection\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/entity/asset_version.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n*******************************\nftrack_api.entity.asset_version\n*******************************\n\n.. automodule:: ftrack_api.entity.asset_version\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/entity/base.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n**********************\nftrack_api.entity.base\n**********************\n\n.. automodule:: ftrack_api.entity.base\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/entity/component.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n***************************\nftrack_api.entity.component\n***************************\n\n.. automodule:: ftrack_api.entity.component\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/entity/factory.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n*************************\nftrack_api.entity.factory\n*************************\n\n.. automodule:: ftrack_api.entity.factory\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/entity/index.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n*****************\nftrack_api.entity\n*****************\n\n.. automodule:: ftrack_api.entity\n\n.. toctree::\n    :maxdepth: 1\n    :glob:\n\n    *\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/entity/job.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n*********************\nftrack_api.entity.job\n*********************\n\n.. automodule:: ftrack_api.entity.job\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/entity/location.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n**************************\nftrack_api.entity.location\n**************************\n\n.. automodule:: ftrack_api.entity.location\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/entity/note.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n**********************\nftrack_api.entity.note\n**********************\n\n.. automodule:: ftrack_api.entity.note\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/entity/project_schema.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n********************************\nftrack_api.entity.project_schema\n********************************\n\n.. automodule:: ftrack_api.entity.project_schema\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/entity/user.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n**********************\nftrack_api.entity.user\n**********************\n\n.. automodule:: ftrack_api.entity.user\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/event/base.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n*********************\nftrack_api.event.base\n*********************\n\n.. automodule:: ftrack_api.event.base\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/event/expression.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n***************************\nftrack_api.event.expression\n***************************\n\n.. automodule:: ftrack_api.event.expression\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/event/hub.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n********************\nftrack_api.event.hub\n********************\n\n.. automodule:: ftrack_api.event.hub\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/event/index.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n****************\nftrack_api.event\n****************\n\n.. automodule:: ftrack_api.event\n\n.. toctree::\n    :maxdepth: 1\n    :glob:\n\n    *\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/event/subscriber.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n***************************\nftrack_api.event.subscriber\n***************************\n\n.. automodule:: ftrack_api.event.subscriber\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/event/subscription.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n*****************************\nftrack_api.event.subscription\n*****************************\n\n.. automodule:: ftrack_api.event.subscription\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/exception.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n********************\nftrack_api.exception\n********************\n\n.. automodule:: ftrack_api.exception\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/formatter.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n********************\nftrack_api.formatter\n********************\n\n.. automodule:: ftrack_api.formatter\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/index.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _api_reference:\n\n*************\nAPI Reference\n*************\n\nftrack_api\n==========\n\n.. automodule:: ftrack_api\n\n.. toctree::\n    :maxdepth: 1\n    :glob:\n\n    */index\n    *\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/inspection.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n*********************\nftrack_api.inspection\n*********************\n\n.. automodule:: ftrack_api.inspection\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/logging.rst",
    "content": "..\n    :copyright: Copyright (c) 2016 ftrack\n\n******************\nftrack_api.logging\n******************\n\n.. automodule:: ftrack_api.logging\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/operation.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n********************\nftrack_api.operation\n********************\n\n.. automodule:: ftrack_api.operation\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/plugin.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n*****************\nftrack_api.plugin\n*****************\n\n.. automodule:: ftrack_api.plugin\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/query.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n****************\nftrack_api.query\n****************\n\n.. automodule:: ftrack_api.query\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/resource_identifier_transformer/base.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. _api_reference/resource_identifier_transformer.base:\n\n***********************************************\nftrack_api.resource_identifier_transformer.base\n***********************************************\n\n.. automodule:: ftrack_api.resource_identifier_transformer.base\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/resource_identifier_transformer/index.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. _api_reference/resource_identifier_transformer:\n\n******************************************\nftrack_api.resource_identifier_transformer\n******************************************\n\n.. automodule:: ftrack_api.resource_identifier_transformer\n\n.. toctree::\n    :maxdepth: 1\n    :glob:\n\n    *\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/session.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n******************\nftrack_api.session\n******************\n\n.. automodule:: ftrack_api.session\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/structure/base.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n*************************\nftrack_api.structure.base\n*************************\n\n.. automodule:: ftrack_api.structure.base\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/structure/id.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n***********************\nftrack_api.structure.id\n***********************\n\n.. automodule:: ftrack_api.structure.id\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/structure/index.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n********************\nftrack_api.structure\n********************\n\n.. automodule:: ftrack_api.structure\n\n.. toctree::\n    :maxdepth: 1\n    :glob:\n\n    *\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/structure/origin.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n***************************\nftrack_api.structure.origin\n***************************\n\n.. automodule:: ftrack_api.structure.origin\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/structure/standard.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n*****************************\nftrack_api.structure.standard\n*****************************\n\n.. automodule:: ftrack_api.structure.standard\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/api_reference/symbol.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n*****************\nftrack_api.symbol\n*****************\n\n.. automodule:: ftrack_api.symbol\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/caching.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n\n.. _caching:\n\n*******\nCaching\n*******\n\nThe API makes use of caching in order to provide more efficient retrieval of\ndata by reducing the number of calls to the remote server::\n\n    # First call to retrieve user performs a request to the server.\n    user = session.get('User', 'some-user-id')\n\n    # A later call in the same session to retrieve the same user just gets\n    # the existing instance from the cache without a request to the server.\n    user = session.get('User', 'some-user-id')\n\nIt also seamlessly merges related data together regardless of how it was\nretrieved::\n\n    >>> timelog = user['timelogs'][0]\n    >>> with session.auto_populating(False):\n    >>>     print timelog['comment']\n    NOT_SET\n    >>> session.query(\n    ...     'select comment from Timelog where id is \"{0}\"'\n    ...     .format(timelog['id'])\n    ... ).all()\n    >>> with session.auto_populating(False):\n    >>>     print timelog['comment']\n    'Some comment'\n\nBy default, each :class:`~ftrack_api.session.Session` is configured with a\nsimple :class:`~ftrack_api.cache.MemoryCache()` and the cache is lost as soon as\nthe session expires.\n\nConfiguring a session cache\n===========================\n\nIt is possible to configure the cache that a session uses. An example would be a\npersistent auto-populated cache that survives between sessions::\n\n    import os\n    import ftrack_api.cache\n\n    # Specify where the file based cache should be stored.\n    cache_path = os.path.join(tempfile.gettempdir(), 'ftrack_session_cache.dbm')\n\n\n    # Define a cache maker that returns a file based cache. Note that a\n    # function is used because the file based cache should use the session's\n    # encode and decode methods to serialise the entity data to a format that\n    # can be written to disk (JSON).\n    def cache_maker(session):\n        '''Return cache to use for *session*.'''\n        return ftrack_api.cache.SerialisedCache(\n            ftrack_api.cache.FileCache(cache_path),\n            encode=session.encode,\n            decode=session.decode\n        )\n\n    # Create the session using the cache maker.\n    session = ftrack_api.Session(cache=cache_maker)\n\n.. note::\n\n    There can be a performance penalty when using a more complex cache setup.\n    For example, serialising data and also writing and reading from disk can be\n    relatively slow operations.\n\nRegardless of the cache specified, the session will always construct a\n:class:`~ftrack_api.cache.LayeredCache` with a\n:class:`~ftrack_api.cache.MemoryCache` at the top level and then your cache at\nthe second level. This is to ensure consistency of instances returned by the\nsession.\n\nYou can check (or even modify) at any time what cache configuration a session is\nusing by accessing the `cache` attribute on a\n:class:`~ftrack_api.session.Session`::\n\n    >>> print session.cache\n    <ftrack_api.cache.LayeredCache object at 0x0000000002F64400>\n\nWriting a new cache interface\n=============================\n\nIf you have a custom cache backend you should be able to integrate it into the\nsystem by writing a cache interface that matches the one defined by\n:class:`ftrack_api.cache.Cache`. This typically involves a subclass and\noverriding the :meth:`~ftrack_api.cache.Cache.get`,\n:meth:`~ftrack_api.cache.Cache.set` and :meth:`~ftrack_api.cache.Cache.remove`\nmethods.\n\n\nManaging what gets cached\n=========================\n\nThe cache system is quite flexible when it comes to controlling what should be\ncached.\n\nConsider you have a layered cache where the bottom layer cache should be\npersisted between sessions. In this setup you probably don't want the persisted\ncache to hold non-persisted values, such as modified entity values or newly\ncreated entities not yet committed to the server. However, you might want the\ntop level memory cache to hold onto these values.\n\nHere is one way to set this up. First define a new proxy cache that is selective\nabout what it sets::\n\n    import ftrack_api.inspection\n\n\n    class SelectiveCache(ftrack_api.cache.ProxyCache):\n        '''Proxy cache that won't cache newly created entities.'''\n\n        def set(self, key, value):\n            '''Set *value* for *key*.'''\n            if isinstance(value, ftrack_api.entity.base.Entity):\n                if (\n                    ftrack_api.inspection.state(value)\n                    is ftrack_api.symbol.CREATED\n                ):\n                    return\n\n            super(SelectiveCache, self).set(key, value)\n\nNow use this custom cache to wrap the serialised cache in the setup above:\n\n.. code-block:: python\n    :emphasize-lines: 3, 9\n\n    def cache_maker(session):\n        '''Return cache to use for *session*.'''\n        return SelectiveCache(\n            ftrack_api.cache.SerialisedCache(\n                ftrack_api.cache.FileCache(cache_path),\n                encode=session.encode,\n                decode=session.decode\n            )\n        )\n\nNow to prevent modified attributes also being persisted, tweak the encode\nsettings for the file cache:\n\n.. code-block:: python\n    :emphasize-lines: 1, 9-12\n\n    import functools\n\n\n    def cache_maker(session):\n        '''Return cache to use for *session*.'''\n        return SelectiveCache(\n            ftrack_api.cache.SerialisedCache(\n                ftrack_api.cache.FileCache(cache_path),\n                encode=functools.partial(\n                    session.encode,\n                    entity_attribute_strategy='persisted_only'\n                ),\n                decode=session.decode\n            )\n        )\n\nAnd use the updated cache maker for your session::\n\n    session = ftrack_api.Session(cache=cache_maker)\n\n.. note::\n\n    For some type of attributes that are computed, long term caching is not\n    recommended and such values will not be encoded with the `persisted_only`\n    strategy.\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/conf.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\n'''ftrack Python API documentation build configuration file.'''\n\nimport os\nimport re\n\n# -- General ------------------------------------------------------------------\n\n# Extensions.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.extlinks',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.viewcode',\n    'lowdown'\n]\n\n\n# The suffix of source filenames.\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'ftrack Python API'\ncopyright = u'2014, ftrack'\n\n# Version\nwith open(\n    os.path.join(\n        os.path.dirname(__file__), '..', 'source',\n        'ftrack_api', '_version.py'\n    )\n) as _version_file:\n    _version = re.match(\n        r'.*__version__ = \\'(.*?)\\'', _version_file.read(), re.DOTALL\n    ).group(1)\n\nversion = _version\nrelease = _version\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\nexclude_patterns = ['_template']\n\n# A list of prefixes to ignore for module listings.\nmodindex_common_prefix = [\n    'ftrack_api.'\n]\n\n# -- HTML output --------------------------------------------------------------\n\nif not os.environ.get('READTHEDOCS', None) == 'True':\n    # Only import and set the theme if building locally.\n    import sphinx_rtd_theme\n    html_theme = 'sphinx_rtd_theme'\n    html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]\n\nhtml_static_path = ['_static']\nhtml_style = 'ftrack.css'\n\n# If True, copy source rst files to output for reference.\nhtml_copy_source = True\n\n\n# -- Autodoc ------------------------------------------------------------------\n\nautodoc_default_flags = ['members', 'undoc-members', 'inherited-members']\nautodoc_member_order = 'bysource'\n\n\ndef autodoc_skip(app, what, name, obj, skip, options):\n    '''Don't skip __init__ method for autodoc.'''\n    if name == '__init__':\n        return False\n\n    return skip\n\n\n# -- Intersphinx --------------------------------------------------------------\n\nintersphinx_mapping = {\n    'python': ('http://docs.python.org/', None),\n    'ftrack': (\n        'http://rtd.ftrack.com/docs/ftrack/en/stable/', None\n    )\n}\n\n\n# -- Todos ---------------------------------------------------------------------\n\ntodo_include_todos = os.environ.get('FTRACK_DOC_INCLUDE_TODOS', False) == 'True'\n\n\n# -- Setup --------------------------------------------------------------------\n\ndef setup(app):\n    app.connect('autodoc-skip-member', autodoc_skip)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/docutils.conf",
    "content": "[html4css1 writer]\nfield-name-limit:0"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/environment_variables.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _environment_variables:\n\n*********************\nEnvironment variables\n*********************\n\nThe following is a consolidated list of environment variables that this API\ncan reference:\n\n.. envvar:: FTRACK_SERVER\n\n    The full url of the ftrack server to connect to. For example\n    \"https://mycompany.ftrackapp.com\"\n\n.. envvar:: FTRACK_API_USER\n\n    The username of the ftrack user to act on behalf of when performing actions\n    in the system.\n\n    .. note::\n\n        When this environment variable is not set, the API will typically also\n        check other standard operating system variables that hold the username\n        of the current logged in user. To do this it uses\n        :func:`getpass.getuser`.\n\n.. envvar:: FTRACK_API_KEY\n\n    The API key to use when performing actions in the system. The API key is\n    used to determine the permissions that a script has in the system.\n\n.. envvar:: FTRACK_APIKEY\n\n    For backwards compatibility. See :envvar:`FTRACK_API_KEY`.\n\n.. envvar:: FTRACK_EVENT_PLUGIN_PATH\n\n    Paths to search recursively for plugins to load and use in a session.\n    Multiple paths can be specified by separating with the value of\n    :attr:`os.pathsep` (e.g. ':' or ';').\n\n.. envvar:: FTRACK_API_SCHEMA_CACHE_PATH\n\n    Path to a directory that will be used for storing and retrieving a cache of\n    the entity schemas fetched from the server.\n\n.. envvar:: http_proxy / https_proxy\n\n    If you need to use a proxy to connect to ftrack you can use the\n    \"standard\" :envvar:`http_proxy` and :envvar:`https_proxy`. Please note that they\n    are lowercase.\n\n    For example \"export https_proxy=http://proxy.mycompany.com:8080\""
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/event_list.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _event_list:\n\n**********\nEvent list\n**********\n\nThe following is a consolidated list of events published directly by this API.\n\nFor some events, a template plugin file is also listed for download\n(:guilabel:`Download template plugin`) to help get you started with writing your\nown plugin for a particular event.\n\n.. seealso::\n\n    * :ref:`handling_events`\n    * :ref:`ftrack server event list <ftrack:developing/events/list>`\n\n.. _event_list/ftrack.api.session.construct-entity-type:\n\nftrack.api.session.construct-entity-type\n========================================\n\n:download:`Download template plugin\n</../resource/plugin/construct_entity_type.py>`\n\n:ref:`Synchronous <handling_events/publishing/synchronously>`. Published by\nthe session to retrieve constructed class for specified schema::\n\n    Event(\n        topic='ftrack.api.session.construct-entity-type',\n        data=dict(\n            schema=schema,\n            schemas=schemas\n        )\n    )\n\nExpects returned data to be::\n\n    A Python class.\n\n.. seealso:: :ref:`working_with_entities/entity_types`.\n\n.. _event_list/ftrack.api.session.configure-location:\n\nftrack.api.session.configure-location\n=====================================\n\n:download:`Download template plugin\n</../resource/plugin/configure_locations.py>`\n\n:ref:`Synchronous <handling_events/publishing/synchronously>`. Published by\nthe session to allow configuring of location instances::\n\n    Event(\n        topic='ftrack.api.session.configure-location',\n        data=dict(\n            session=self\n        )\n    )\n\n.. seealso:: :ref:`Configuring locations <locations/configuring/automatically>`.\n\n.. _event_list/ftrack.location.component-added:\n\nftrack.location.component-added\n===============================\n\nPublished whenever a component is added to a location::\n\n    Event(\n        topic='ftrack.location.component-added',\n        data=dict(\n            component_id='e2dc0524-b576-11d3-9612-080027331d74',\n            location_id='07b82a97-8cf9-11e3-9383-20c9d081909b'\n        )\n    )\n\n.. _event_list/ftrack.location.component-removed:\n\nftrack.location.component-removed\n=================================\n\nPublished whenever a component is removed from a location::\n\n    Event(\n        topic='ftrack.location.component-removed',\n        data=dict(\n            component_id='e2dc0524-b576-11d3-9612-080027331d74',\n            location_id='07b82a97-8cf9-11e3-9383-20c9d081909b'\n        )\n    )\n\n.. _event_list/ftrack.api.session.ready:\n\nftrack.api.session.ready\n========================\n\n:ref:`Synchronous <handling_events/publishing/synchronously>`. Published after\na :class:`~ftrack_api.session.Session` has been initialized and\nis ready to be used::\n\n    Event(\n        topic='ftrack.api.session.ready',\n        data=dict(\n            session=<Session instance>,\n        )\n    )\n\n.. warning::\n\n    Since the event is synchronous and blocking, avoid doing any unnecessary\n    work as it will slow down session initialization.\n\n.. seealso::\n\n    Also see example usage in :download:`example_plugin_using_session.py\n    </resource/example_plugin_using_session.py>`.\n\n\n.. _event_list/ftrack.api.session.reset:\n\nftrack.api.session.reset\n========================\n\n:ref:`Synchronous <handling_events/publishing/synchronously>`. Published after\na :class:`~ftrack_api.session.Session` has been reset and is ready to be used\nagain::\n\n    Event(\n        topic='ftrack.api.session.reset',\n        data=dict(\n            session=<Session instance>,\n        )\n    )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/assignments_and_allocations.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. _example/assignments_and_allocations:\n\n****************************************\nWorking with assignments and allocations\n****************************************\n\n.. currentmodule:: ftrack_api.session\n\nThe API exposes `assignments` and `allocations` relationships on objects in\nthe project hierarchy. You can use these to retrieve the allocated or assigned\nresources, which can be either groups or users. \n\nAllocations can be used to allocate users or groups to a project team, while\nassignments are more explicit and is used to assign users to tasks. Both\nassignment and allocations are modelled as `Appointment` objects, with a\n`type` attribute indicating the type of the appoinment.\n\nThe following example retrieves all users part of the project team::\n\n    # Retrieve a project\n    project = session.query('Project').first()\n\n    # Set to hold all users part of the project team\n    project_team = set()\n\n    # Add all allocated groups and users\n    for allocation in project['allocations']:\n\n        # Resource may be either a group or a user\n        resource = allocation['resource']\n\n        # If the resource is a group, add its members\n        if isinstance(resource, session.types['Group']):\n            for membership in resource['memberships']:\n                user = membership['user']\n                project_team.add(user)\n\n        # The resource is a user, add it.\n        else:\n            user = resource\n            project_team.add(user)\n\nThe next example shows how to assign the current user to a task::\n\n    # Retrieve a task and the current user\n    task = session.query('Task').first()\n    current_user = session.query(\n        u'User where username is {0}'.format(session.api_user)\n    ).one()\n\n    # Create a new Appointment of type assignment.\n    session.create('Appointment', {\n        'context': task,\n        'resource': current_user,\n        'type': 'assignment'\n    })\n\n    # Finally, persist the new assignment\n    session.commit()\n\nTo list all users assigned to a task, see the following example::\n\n    task = session.query('Task').first()\n    users = session.query(\n        'select first_name, last_name from User '\n        'where assignments any (context_id = \"{0}\")'.format(task['id'])\n    )\n    for user in users:\n        print user['first_name'], user['last_name']\n\nTo list the current user's assigned tasks, see the example below::\n\n    assigned_tasks = session.query(\n        'select link from Task '\n        'where assignments any (resource.username = \"{0}\")'.format(session.api_user)\n    )\n    for task in assigned_tasks:\n        print u' / '.join(item['name'] for item in task['link'])\n\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/component.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _example/component:\n\n***********************\nWorking with components\n***********************\n\n.. currentmodule:: ftrack_api.session\n\nComponents can be created manually or using the provide helper methods on a\n:meth:`session <ftrack_api.session.Session.create_component>` or existing\n:meth:`asset version\n<ftrack_api.entity.asset_version.AssetVersion.create_component>`::\n\n    component = version.create_component('/path/to/file_or_sequence.jpg')\n    session.commit()\n\nWhen a component is created using the helpers it is automatically added to a\nlocation.\n\n.. seealso:: :ref:`Locations tutorial <locations/tutorial>`\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/custom_attribute.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. _example/custom_attribute:\n\n***********************\nUsing custom attributes\n***********************\n\n.. currentmodule:: ftrack_api.session\n\nCustom attributes can be written and read from entities using the\n``custom_attributes`` property.\n\nThe ``custom_attributes`` property provides a similar interface to a dictionary.\n\nKeys can be printed using the keys method::\n\n    >>> task['custom_attributes'].keys()\n    [u'my_text_field']\n\nor access keys and values as items::\n\n    >>> print task['custom_attributes'].items()\n    [(u'my_text_field', u'some text')]\n\nRead existing custom attribute values::\n\n    >>> print task['custom_attributes']['my_text_field']\n    'some text'\n\nUpdating a custom attributes can also be done similar to a dictionary::\n\n    task['custom_attributes']['my_text_field'] = 'foo'\n\nTo query for tasks with a custom attribute, ``my_text_field``, you can use the\nkey from the configuration::\n \n    for task in session.query(\n        'Task where custom_attributes any '\n        '(key is \"my_text_field\" and value is \"bar\")'\n    ):\n        print task['name']\n\nLimitations\n===========\n\nExpression attributes\n---------------------\n\nExpression attributes are not yet supported and the reported value will\nalways be the non-evaluated expression.\n\nHierarchical attributes\n-----------------------\n\nHierarchical attributes are not yet fully supported in the API. Hierarchical\nattributes support both read and write, but when read they are not calculated\nand instead the `raw` value is returned::\n\n    # The hierarchical attribute `my_attribute` is set on Shot but this will not\n    # be reflected on the children. Instead the raw value is returned.\n    print shot['custom_attributes']['my_attribute']\n    'foo'\n    print task['custom_attributes']['my_attribute']\n    None\n\nTo work around this limitation it is possible to use the legacy api for\nhierarchical attributes or to manually query the parents for values and use the\nfirst value that is set.\n\nValidation\n==========\n\nCustom attributes are validated on the ftrack server before persisted. The\nvalidation will check that the type of the data is correct for the custom\nattribute.\n\n    * number - :py:class:`int` or :py:class:`float`\n    * text - :py:class:`str` or :py:class:`unicode`\n    * enumerator - :py:class:`list`\n    * boolean - :py:class:`bool`\n    * date - :py:class:`datetime.datetime` or :py:class:`datetime.date`\n\nIf the value set is not valid a :py:exc:`ftrack_api.exception.ServerError` is\nraised with debug information::\n\n    shot['custom_attributes']['fstart'] = 'test'\n\n    Traceback (most recent call last):\n        ...\n    ftrack_api.exception.ServerError: Server reported error: \n    ValidationError(Custom attribute value for \"fstart\" must be of type number.\n    Got \"test\" of type <type 'unicode'>)"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/encode_media.rst",
    "content": "..\n    :copyright: Copyright (c) 2016 ftrack\n\n.. currentmodule:: ftrack_api.session\n\n.. _example/encode_media:\n\n**************\nEncoding media\n**************\n\nMedia such as images and video can be encoded by the ftrack server to allow\nplaying it in the ftrack web interface. Media can be encoded using\n:meth:`ftrack_api.session.Session.encode_media` which accepts a path to a file\nor an existing component in the ftrack.server location.\n\nHere is an example of how to encode a video and read the output::\n\n    job = session.encode_media('/PATH/TO/MEDIA')\n    job_data = json.loads(job['data'])\n\n    print 'Source component id', job_data['source_component_id']\n    print 'Keeping original component', job_data['keep_original']\n    for output in job_data['output']:\n        print u'Output component - id: {0}, format: {1}'.format(\n            output['component_id'], output['format']\n        )\n\nYou can also call the corresponding helper method on an :meth:`asset version\n<ftrack_api.entity.asset_version.AssetVersion.encode_media>`, to have the\nencoded components automatically associated with the version::\n\n    job = asset_version.encode_media('/PATH/TO/MEDIA')\n\nIt is also possible to get the URL to an encoded component once the job has\nfinished::\n\n    job = session.encode_media('/PATH/TO/MEDIA')\n\n    # Wait for job to finish.\n\n    location = session.query('Location where name is \"ftrack.server\"').one()\n    for component in job['job_components']:\n        print location.get_url(component)\n\nMedia can also be an existing component in another location. Before encoding it,\nthe component needs to be added to the ftrack.server location::\n\n    location = session.query('Location where name is \"ftrack.server\"').one()\n    location.add_component(component)\n    session.commit()\n\n    job = session.encode_media(component)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/entity_links.rst",
    "content": "..\n    :copyright: Copyright (c) 2016 ftrack\n\n.. _example/entity_links:\n\n******************\nUsing entity links\n******************\n\nA link can be used to represent a dependency or another relation between\ntwo entities in ftrack.\n\nThere are two types of entities that can be linked:\n\n*   Versions can be linked to other asset versions, where the link entity type\n    is `AssetVersionLink`.\n*   Objects like Task, Shot or Folder, where the link entity type is\n    `TypedContextLink`.\n\nBoth `AssetVersion` and `TypedContext` objects have the same relations\n`incoming_links` and `outgoing_links`. To list the incoming links to a Shot we\ncan use the relationship `incoming_links`::\n\n    for link in shot['incoming_links']:\n        print link['from'], link['to']\n\nIn the above example `link['to']` is the shot and `link['from']` could be an\nasset build or something else that is linked to the shot. There is an equivalent\n`outgoing_links` that can be used to access outgoing links on an object.\n\nTo create a new link between objects or asset versions create a new \n`TypedContextLink` or `AssetVersionLink` entity with the from and to properties\nset. In this example we will link two asset versions::\n\n    session.create('AssetVersionLink', {\n        'from': from_asset_version,\n        'to': to_asset_version\n    })\n    session.commit()\n\nUsing asset version link shortcut\n=================================\n\nLinks on asset version can also be created by the use of the `uses_versions` and\n`used_in_versions` relations::\n\n    rig_version['uses_versions'].append(model_version)\n    session.commit()\n\nThis has the same result as creating the `AssetVersionLink` entity as in the\nprevious section.\n\nWhich versions are using the model can be listed with::\n\n    for version in model_version['used_in_versions']:\n        print '{0} is using {1}'.format(version, model_version)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/index.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. currentmodule:: ftrack_api.session\n\n.. _example:\n\n**************\nUsage examples\n**************\n\nThe following examples show how to use the API to accomplish specific tasks\nusing the default configuration.\n\n.. note::\n\n    If you are using a server with a customised configuration you may need to\n    alter the examples slightly to make them work correctly.\n\nMost of the examples assume you have the *ftrack_api* package imported and have\nalready constructed a :class:`Session`::\n\n    import ftrack_api\n\n    session = ftrack_api.Session()\n\n\n.. toctree::\n\n    project\n    component\n    review_session\n    metadata\n    custom_attribute\n    manage_custom_attribute_configuration\n    link_attribute\n    scope\n    job\n    note\n    list\n    timer\n    assignments_and_allocations\n    thumbnail\n    encode_media\n    entity_links\n    web_review\n    publishing\n    security_roles\n    task_template\n    sync_ldap_users\n    invite_user\n\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/invite_user.rst",
    "content": "..\n    :copyright: Copyright (c) 2017 ftrack\n\n.. _example/invite_user:\n\n*********************\nInvite user\n*********************\n\nHere we create a new user and send them a invitation through mail\n\n\nCreate a new user::\n\n    user_email = 'artist@mail.vfx-company.com'\n\n    new_user = session.create(\n        'User', {\n            'username':user_email,\n            'email':user_email,\n            'is_active':True\n        }\n    )\n\n    session.commit()\n\n\nInvite our new user::\n\n    new_user.send_invite()\n\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/job.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _example/job:\n\n*************\nManaging jobs\n*************\n\n.. currentmodule:: ftrack_api.session\n\nJobs can be used to display feedback to users in the ftrack web interface when\nperforming long running tasks in the API.\n\nTo create a job use :meth:`Session.create`::\n\n    user = # Get a user from ftrack.\n\n    job = session.create('Job', {\n        'user': user,\n        'status': 'running'\n    })\n\nThe created job will appear as running in the :guilabel:`jobs` menu for the\nspecified user. To set a description on the job, add a dictionary containing\ndescription as the `data` key:\n\n.. note::\n\n    In the current version of the API the dictionary needs to be JSON\n    serialised.\n\n.. code-block:: python\n    \n    import json\n\n    job = session.create('Job', {\n        'user': user,\n        'status': 'running',\n        'data': json.dumps({\n            'description': 'My custom job description.'\n        })\n    })\n\nWhen the long running task has finished simply set the job as completed and\ncontinue with the next task.\n\n.. code-block:: python\n\n    job['status'] = 'done'\n    session.commit()\n\nAttachments\n===========\n\nJob attachments are files that are attached to a job. In the ftrack web\ninterface these attachments can be downloaded by clicking on a job in the `Jobs`\nmenu.\n\nTo get a job's attachments through the API you can use the `job_components`\nrelation and then use the ftrack server location to get the download URL::\n\n    server_location = session.query(\n        'Location where name is \"ftrack.server\"'\n    ).one()\n\n    for job_component in job['job_components']:\n        print 'Download URL: {0}'.format(\n            server_location.get_url(job_component['component'])   \n        )\n\nTo add an attachment to a job you have to add it to the ftrack server location\nand create a `jobComponent`::\n\n    server_location = session.query(\n        'Location where name is \"ftrack.server\"'\n    ).one()    \n\n    # Create component and name it \"My file\".\n    component = session.create_component(\n        '/path/to/file',\n        data={'name': 'My file'},\n        location=server_location\n    )\n\n    # Attach the component to the job.\n    session.create(\n        'JobComponent',\n        {'component_id': component['id'], 'job_id': job['id']}\n    )\n\n    session.commit()\n\n.. note::\n\n    The ftrack web interface does only support downloading one attachment so\n    attaching more than one will have limited support in the web interface.\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/link_attribute.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. _example/link_attribute:\n\n*********************\nUsing link attributes\n*********************\n\nThe `link` attribute can be used to retreive the ids and names of the parents of\nan object. It is particularly useful in cases where the path of an object must\nbe presented in a UI, but can also be used to speedup certain query patterns.\n\nYou can use the `link` attribute on any entity inheriting from a\n`Context` or `AssetVersion`. Here we use it on the `Task` entity::\n\n    task = session.query(\n        'select link from Task where name is \"myTask\"'\n    ).first()\n    print task['link']\n\nIt can also be used create a list of parent entities, including the task\nitself::\n\n    entities = []\n    for item in task['link']:\n        entities.append(session.get(item['type'], item['id']))\n\nThe `link` attribute is an ordered list of dictionaries containting data\nof the parents and the item itself. Each dictionary contains the following\nentries:\n\n    id\n        The id of the object and can be used to do a :meth:`Session.get`.\n    name\n        The name of the object.\n    type\n        The schema id of the object.\n\nA more advanced use-case is to get the parent names and ids of all timelogs for\na user::\n\n    for timelog in session.query(\n        'select context.link, start, duration from Timelog '\n        'where user.username is \"john.doe\"'\n    ):\n        print timelog['context']['link'], timelog['start'], timelog['duration']\n\nThe attribute is also available from the `AssetVersion` asset relation::\n\n    for asset_version in session.query(\n        'select link from AssetVersion '\n        'where user.username is \"john.doe\"'\n    ):\n        print asset_version['link']\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/list.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. _example/list:\n\n***********\nUsing lists\n***********\n\n.. currentmodule:: ftrack_api.session\n\nLists can be used to create a collection of asset versions or objects such as\ntasks. It could be a list of items that should be sent to client, be included in\ntodays review session or items that belong together in way that is different\nfrom the project hierarchy.\n\nThere are two types of lists, one for asset versions and one for other objects\nsuch as tasks.\n\nTo create a list use :meth:`Session.create`::\n\n    user = # Get a user from ftrack.\n    project = # Get a project from ftrack.\n    list_category = # Get a list category from ftrack.\n\n    asset_version_list = session.create('AssetVersionList', {\n        'owner': user,\n        'project': project,\n        'category': list_category\n    })\n\n    task_list = session.create('TypedContextList', {\n        'owner': user,\n        'project': project,\n        'category': list_category\n    })\n\nThen add items to the list like this::\n\n    asset_version_list['items'].append(asset_version)\n    task_list['items'].append(task)\n\nAnd remove items from the list like this::\n\n    asset_version_list['items'].remove(asset_version)\n    task_list['items'].remove(task)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/manage_custom_attribute_configuration.rst",
    "content": "..\n    :copyright: Copyright (c) 2017 ftrack\n\n.. _example/manage_custom_attribute_configuration:\n\n****************************************\nManaging custom attribute configurations\n****************************************\n\nFrom the API it is not only possible to\n:ref:`read and update custom attributes for entities <example/custom_attribute>`,\nbut also managing custom attribute configurations.\n\nExisting custom attribute configurations can be queried as ::\n\n    # Print all existing custom attribute configurations.\n    print session.query('CustomAttributeConfiguration').all()\n\nUse :meth:`Session.create` to create a new custom attribute configuration::\n\n    # Get the custom attribute type.\n    custom_attribute_type = session.query(\n        'CustomAttributeType where name is \"text\"'\n    ).one()\n\n    # Create a custom attribute configuration.\n    session.create('CustomAttributeConfiguration', {\n        'entity_type': 'assetversion',\n        'type': custom_attribute_type,\n        'label': 'Asset version text attribute',\n        'key': 'asset_version_text_attribute',\n        'default': 'bar',\n        'config': json.dumps({'markdown': False})\n    })\n\n    # Persist it to the ftrack instance.\n    session.commit()\n\n.. tip::\n\n    The example above does not add security roles. This can be done either\n    from System Settings in the ftrack web application, or by following the\n    :ref:`example/manage_custom_attribute_configuration/security_roles` example.\n\nGlobal or project specific\n==========================\n\nA custom attribute can be global or project specific depending on the\n`project_id` attribute::\n\n    # Create a custom attribute configuration.\n    session.create('CustomAttributeConfiguration', {\n        # Set the `project_id` and the custom attribute will only be available\n        # on `my_project`.\n        'project_id': my_project['id'],\n        'entity_type': 'assetversion',\n        'type': custom_attribute_type,\n        'label': 'Asset version text attribute',\n        'key': 'asset_version_text_attribute',\n        'default': 'bar',\n        'config': json.dumps({'markdown': False})\n    })\n    session.commit()\n\nA project specific custom attribute can be changed to a global::\n\n    custom_attribute_configuration['project_id'] = None\n    session.commit()\n\nChanging a global custom attribute configuration to a project specific is not\nallowed.\n\nEntity types\n============\n\nCustom attribute configuration entity types are using a legacy notation. A\nconfiguration can have one of the following as `entity_type`:\n\n:task:\n    Represents TypedContext (Folder, Shot, Sequence, Task, etc.) custom\n    attribute configurations. When setting this as entity_type the\n    object_type_id must be set as well.\n\n    Creating a text custom attribute for Folder::\n\n        custom_attribute_type = session.query(\n            'CustomAttributeType where name is \"text\"'\n        ).one()\n        object_type = session.query('ObjectType where name is \"Folder\"').one()\n        session.create('CustomAttributeConfiguration', {\n            'entity_type': 'task',\n            'object_type_id': object_type['id'],\n            'type': custom_attribute_type,\n            'label': 'Foo',\n            'key': 'foo',\n            'default': 'bar',\n        })\n        session.commit()\n\n    Can be associated with a `project_id`.\n\n:show:\n    Represents Projects custom attribute configurations.\n\n    Can be associated with a `project_id`.\n\n:assetversion:\n    Represents AssetVersion custom attribute configurations.\n\n    Can be associated with a `project_id`.\n\n:user:\n    Represents User custom attribute configurations.\n\n    Must be `global` and cannot be associated with a `project_id`.\n\n:list:\n    Represents List custom attribute configurations.\n\n    Can be associated with a `project_id`.\n\n:asset:\n    Represents Asset custom attribute configurations.\n\n    .. note::\n       \n        Asset custom attributes have limited support in the ftrack web\n        interface.\n\n    Can be associated with a `project_id`.\n\nIt is not possible to change type after a custom attribute configuration has\nbeen created.\n\nCustom attribute configuration types\n====================================\n\nCustom attributes can be of different data types depending on what type is set\nin the configuration. Some types requires an extra json encoded config to be\nset:\n\n:text:\n    A sting type custom attribute.\n\n    The `default` value must be either :py:class:`str` or :py:class:`unicode`.\n\n    Can be either presented as raw text or markdown formatted in applicaitons\n    which support it. This is configured through a markwdown key::\n\n        # Get the custom attribute type.\n        custom_attribute_type = session.query(\n            'CustomAttributeType where name is \"text\"'\n        ).one()\n\n        # Create a custom attribute configuration.\n        session.create('CustomAttributeConfiguration', {\n            'entity_type': 'assetversion',\n            'type': custom_attribute_type,\n            'label': 'Asset version text attribute',\n            'key': 'asset_version_text_attribute',\n            'default': 'bar',\n            'config': json.dumps({'markdown': False})\n        })\n\n        # Persist it to the ftrack instance.\n        session.commit()\n\n:boolean:\n\n    A boolean type custom attribute.\n\n    The `default` value must be a :py:class:`bool`.\n\n    No config is required.\n\n:date:\n    A date type custom attribute.\n\n    The `default` value must be an :term:`arrow` date - e.g.\n    arrow.Arrow(2017, 2, 8).\n\n    No config is required.\n\n:enumerator:\n    An enumerator type custom attribute.\n\n    The `default` value must be a list with either :py:class:`str` or\n    :py:class:`unicode`.\n\n    The enumerator can either be single or multi select. The config must a json\n    dump of a dictionary containing `multiSelect` and `data`. Where\n    `multiSelect` is True or False and data is a list of options. Each option\n    should be a dictionary containing `value` and `menu`, where `menu` is meant\n    to be used as label in a user interface.\n\n    Create a custom attribute enumerator::\n\n        custom_attribute_type = session.query(\n            'CustomAttributeType where name is \"enumerator\"'\n        ).first()\n        session.create('CustomAttributeConfiguration', {\n            'entity_type': 'assetversion',\n            'type': custom_attribute_type,\n            'label': 'Enumerator attribute',\n            'key': 'enumerator_attribute',\n            'default': ['bar'],\n            'config': json.dumps({\n                'multiSelect': True,\n                'data': json.dumps([\n                    {'menu': 'Foo', 'value': 'foo'},\n                    {'menu': 'Bar', 'value': 'bar'}\n                ])\n            })\n        })\n        session.commit()\n\n:dynamic enumerator:\n\n    An enumerator type where available options are fetched from remote. Created\n    in the same way as enumerator but without `data`.\n\n:number:\n\n    A number custom attribute can be either decimal or integer for presentation.\n\n    This can be configured through the `isdecimal` config option::\n\n        custom_attribute_type = session.query(\n            'CustomAttributeType where name is \"number\"'\n        ).first()\n        session.create('CustomAttributeConfiguration', {\n            'entity_type': 'assetversion',\n            'type': custom_attribute_type,\n            'label': 'Number attribute',\n            'key': 'number_attribute',\n            'default': 42,\n            'config': json.dumps({\n                'isdecimal': True\n            })\n        })\n        session.commit()\n\nChanging default\n================\n\nIt is possible to update the `default` value of a custom attribute\nconfiguration. This will not change the value of any existing custom\nattributes::\n\n    # Change the default value of custom attributes. This will only affect\n    # newly created entities.\n    custom_attribute_configuration['default'] = 43\n    session.commit()\n\n.. _example/manage_custom_attribute_configuration/security_roles:\n\nSecurity roles\n==============\n\nBy default new custom attribute configurations and the entity values are not\nreadable or writable by any security role.\n\nThis can be configured through the `read_security_roles` and `write_security_roles`\nattributes::\n\n    # Pick random security role.\n    security_role = session.query('SecurityRole').first()\n    custom_attribute_type = session.query(\n        'CustomAttributeType where name is \"date\"'\n    ).first()\n    session.create('CustomAttributeConfiguration', {\n        'entity_type': 'assetversion',\n        'type': custom_attribute_type,\n        'label': 'Date attribute',\n        'key': 'date_attribute',\n        'default': arrow.Arrow(2017, 2, 8),\n        'write_security_roles': [security_role],\n        'read_security_roles': [security_role]\n    })\n    session.commit()\n\n.. note::\n\n    Setting the correct security role is important and must be changed to\n    whatever security role is appropriate for your configuration and intended\n    purpose.\n\nCustom attribute groups\n=======================\n\nA custom attribute configuration can be categorized using a\n`CustomAttributeGroup`::\n\n    group = session.query('CustomAttributeGroup').first()\n    security_role = session.query('SecurityRole').first()\n    custom_attribute_type = session.query(\n        'CustomAttributeType where name is \"enumerator\"'\n    ).first()\n    session.create('CustomAttributeConfiguration', {\n        'entity_type': 'assetversion',\n        'type': custom_attribute_type,\n        'label': 'Enumerator attribute',\n        'key': 'enumerator_attribute',\n        'default': ['bar'],\n        'config': json.dumps({\n            'multiSelect': True,\n            'data': json.dumps([\n                {'menu': 'Foo', 'value': 'foo'},\n                {'menu': 'Bar', 'value': 'bar'}\n            ])\n        }),\n        'group': group,\n        'write_security_roles': [security_role],\n        'read_security_roles': [security_role]\n    })\n    session.commit()\n\n.. seealso::\n\n    :ref:`example/custom_attribute`\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/metadata.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _example/metadata:\n\n**************\nUsing metadata\n**************\n\n.. currentmodule:: ftrack_api.session\n\nKey/value metadata can be written to entities using the metadata property\nand also used to query entities.\n\nThe metadata property has a similar interface as a dictionary and keys can be\nprinted using the keys method::\n\n    >>> print new_sequence['metadata'].keys()\n    ['frame_padding', 'focal_length']\n\nor items::\n\n    >>> print new_sequence['metadata'].items()\n    [('frame_padding': '4'), ('focal_length': '70')]\n\nRead existing metadata::\n\n    >>> print new_sequence['metadata']['frame_padding']\n    '4'\n\nSetting metadata can be done in a few ways where that later one will replace\nany existing metadata::\n\n    new_sequence['metadata']['frame_padding'] = '5'\n    new_sequence['metadata'] = {\n        'frame_padding': '4'\n    }\n\nEntities can also be queried using metadata::\n\n    session.query(\n        'Sequence where metadata any (key is \"frame_padding\" and value is \"4\")'\n    )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/note.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. currentmodule:: ftrack_api.session\n\n.. _example/note:\n\n***********\nUsing notes\n***********\n\nNotes can be written on almost all levels in ftrack. To retrieve notes on an\nentity you can either query them or use the relation called `notes`::\n    \n    task = session.query('Task').first()\n    \n    # Retrieve notes using notes property.\n    notes_on_task = task['notes']\n\n    # Or query them.\n    notes_on_task = session.query('Note where parent_id is \"{}\"'.format(\n        task['id']\n    ))\n\n.. note::\n\n    It's currently not possible to use the `parent` property when querying\n    notes or to use the `parent` property on notes::\n\n        task = session.query('Task').first()\n\n        # This won't work in the current version of the API.\n        session.query('Note where parent.id is \"{}\"'.format(\n            task['id']\n        ))\n\n        # Neither will this.\n        parent_of_note = note['parent']\n\nTo create new notes you can either use the helper method called\n:meth:`~ftrack_api.entity.note.CreateNoteMixin.create_note` on any entity that\ncan have notes or use :meth:`Session.create` to create them manually::\n\n    user = session.query('User').first()\n\n    # Create note using the helper method.\n    note = task.create_note('My new note', author=user)\n\n    # Manually create a note\n    note = session.create('Note', {\n        'content': 'My new note',\n        'author': user\n    })\n    \n    task['notes'].append(note)\n\nReplying to an existing note can also be done with a helper method or by\nusing :meth:`Session.create`::\n\n    # Create using helper method.\n    first_note_on_task = task['notes'][0]\n    first_note_on_task.create_reply('My new reply on note', author=user)\n\n    # Create manually\n    reply = session.create('Note', {\n        'content': 'My new note',\n        'author': user\n    })\n    \n    first_note_on_task.replies.append(reply)\n\nNotes can have labels. Use the label argument to set labels on the\nnote using the helper method::\n\n    label = session.query(\n        'NoteLabel where name is \"External Note\"'\n    ).first()\n\n    note = task.create_note(\n        'New note with external category', author=user, labels=[label]\n    )\n\nOr add labels to notes when creating a note manually::\n\n    label = session.query(\n        'NoteLabel where name is \"External Note\"'\n    ).first()\n\n    note = session.create('Note', {\n        'content': 'New note with external category',\n        'author': user\n    })\n\n    session.create('NoteLabelLink', {\n        'note_id': note['id],\n        'label_id': label['id']\n    })\n\n    task['notes'].append(note)\n\n.. note::\n\n    Support for labels on notes was added in ftrack server version 4.3. For\n    older versions of the server, NoteCategory can be used instead.\n\nTo specify a category when creating a note simply pass a `NoteCategory` instance\nto the helper method::\n\n    category = session.query(\n        'NoteCategory where name is \"External Note\"'\n    ).first()\n\n    note = task.create_note(\n        'New note with external category', author=user, category=category\n    )\n\nWhen writing notes you might want to direct the note to someone. This is done\nby adding users as recipients. If a user is added as a recipient the user will\nreceive notifications and the note will be displayed in their inbox.\n\nTo add recipients pass a list of user or group instances to the helper method::\n\n    john = session.query('User where username is \"john\"').one()\n    animation_group = session.query('Group where name is \"Animation\"').first()\n\n    note = task.create_note(\n        'Note with recipients', author=user, recipients=[john, animation_group]\n    )\n\nAttachments\n===========\n\nNote attachments are files that are attached to a note. In the ftrack web\ninterface these attachments appears next to the note and can be downloaded by\nthe user.\n\nTo get a note's attachments through the API you can use the `note_components`\nrelation and then use the ftrack server location to get the download URL::\n\n    server_location = session.query(\n        'Location where name is \"ftrack.server\"'\n    ).one()\n\n    for note_component in note['note_components']:\n        print 'Download URL: {0}'.format(\n            server_location.get_url(note_component['component'])\n        )\n\nTo add an attachment to a note you have to add it to the ftrack server location\nand create a `NoteComponent`::\n\n    server_location = session.query(\n        'Location where name is \"ftrack.server\"'\n    ).one()    \n\n    # Create component and name it \"My file\".\n    component = session.create_component(\n        '/path/to/file',\n        data={'name': 'My file'},\n        location=server_location\n    )\n\n    # Attach the component to the note.\n    session.create(\n        'NoteComponent',\n        {'component_id': component['id'], 'note_id': note['id']}\n    )\n\n    session.commit()\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/project.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. _example/project:\n\n*********************\nWorking with projects\n*********************\n\n.. currentmodule:: ftrack_api.session\n\nCreating a project\n==================\n\nA project with sequences, shots and tasks can be created in one single\ntransaction. Tasks need to have a type and status set on creation based on the\nproject schema::\n\n    import uuid\n\n    # Create a unique name for the project.\n    name = 'projectname_{0}'.format(uuid.uuid1().hex)\n\n    # Naively pick the first project schema. For this example to work the\n    # schema must contain `Shot` and `Sequence` object types.\n    project_schema = session.query('ProjectSchema').first()\n\n    # Create the project with the chosen schema.\n    project = session.create('Project', {\n        'name': name,\n        'full_name': name + '_full',\n        'project_schema': project_schema\n    })\n\n    # Retrieve default types.\n    default_shot_status = project_schema.get_statuses('Shot')[0]\n    default_task_type = project_schema.get_types('Task')[0]\n    default_task_status = project_schema.get_statuses(\n        'Task', default_task_type['id']\n    )[0]\n\n    # Create sequences, shots and tasks.\n    for sequence_number in range(1, 5):\n        sequence = session.create('Sequence', {\n            'name': 'seq_{0}'.format(sequence_number),\n            'parent': project\n        })\n\n        for shot_number in range(1, 5):\n            shot = session.create('Shot', {\n                'name': '{0}0'.format(shot_number).zfill(3),\n                'parent': sequence,\n                'status': default_shot_status\n            })\n\n            for task_number in range(1, 5):\n                session.create('Task', {\n                    'name': 'task_{0}'.format(task_number),\n                    'parent': shot,\n                    'status': default_task_status,\n                    'type': default_task_type\n                })\n\n    # Commit all changes to the server.\n    session.commit()\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/publishing.rst",
    "content": "..\n    :copyright: Copyright (c) 2016 ftrack\n\n.. currentmodule:: ftrack_api.session\n\n.. _example/publishing:\n\n*******************\nPublishing versions\n*******************\n\nTo know more about publishing and the concepts around publishing, read the\n`ftrack article <http://ftrack.rtd.ftrack.com/en/stable/developing/publishing/index.html>`_\nabout publishing.\n\nTo publish an asset you first need to get the context where the asset should be\npublished::\n\n    # Get a task from a given id.\n    task = session.get('Task', '423ac382-e61d-4802-8914-dce20c92b740')\n\nAnd the parent of the task which will be used to publish the asset on::\n\n    asset_parent = task['parent']\n\nThen we create an asset and a version on the asset::\n\n    asset_type = session.query('AssetType where name is \"Geometry\"').one()\n    asset = session.create('Asset', {\n        'name': 'My asset',\n        'type': asset_type,\n        'parent': asset_parent\n    })\n    asset_version = session.create('AssetVersion', {\n        'asset': asset,\n        'task': task\n    })\n\n.. note::\n\n    The task is not used as the parent of the asset, instead the task is linked\n    directly to the AssetVersion.\n\nThen when we have a version where we can create the components::\n\n    asset_version.create_component(\n        '/path/to/a/file.mov', location='auto'\n    )\n    asset_version.create_component(\n        '/path/to/a/another-file.mov', location='auto'\n    )\n\n    session.commit()\n\nThis will automatically create a new component and add it to the location which\nhas been configured as the first in priority.\n\nComponents can also be named and added to a custom location like this::\n\n    location = session.query('Location where name is \"my-location\"')\n    asset_version.create_component(\n        '/path/to/a/file.mov',\n        data={\n            'name': 'foobar'\n        },\n        location=location\n    )\n\n.. seealso::\n\n    * :ref:`example/component`\n    * :ref:`example/web_review`\n    * :ref:`example/thumbnail`\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/review_session.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. _example/review_session:\n\n*********************\nUsing review sessions\n*********************\n\n.. currentmodule:: ftrack_api.session\n\nClient review sessions can either be queried manually or by using a project\ninstance.\n\n.. code-block:: python\n\n    review_sessions = session.query(\n        'ReviewSession where name is \"Weekly review\"'\n    )\n    \n    project_review_sessions = project['review_sessions']\n\nTo create a new review session on a specific project use :meth:`Session.create`.\n\n.. code-block:: python\n\n    review_session = session.create('ReviewSession', {\n        'name': 'Weekly review',\n        'description': 'See updates from last week.',\n        'project': project\n    })\n\nTo add objects to a review session create them using\n:meth:`Session.create` and reference a review session and an asset version.\n\n.. code-block:: python\n\n    review_session = session.create('ReviewSessionObject', {\n        'name': 'Compositing',\n        'description': 'Fixed shadows.',\n        'version': 'Version 3',\n        'review_session': review_session,\n        'asset_version': asset_version\n    })\n\nTo list all objects in a review session.\n\n.. code-block:: python\n\n    review_session_objects = review_session['review_session_objects']\n\nListing and adding collaborators to review session can be done using \n:meth:`Session.create` and the `review_session_invitees` relation on a \nreview session.\n\n.. code-block:: python\n\n    invitee = session.create('ReviewSessionInvitee', {\n        'name': 'John Doe',\n        'email': 'john.doe@example.com',\n        'review_session': review_session\n    })\n    \n    session.commit()\n    \n    invitees = review_session['review_session_invitees']\n\nTo remove a collaborator simply delete the object using\n:meth:`Session.delete`.\n\n.. code-block:: python\n\n    session.delete(invitee)\n\nTo send out an invite email to a signle collaborator use\n:meth:`Session.send_review_session_invite`.\n\n.. code-block:: python\n\n    session.send_review_session_invite(invitee)\n\nMultiple invitees can have emails sent to them in one batch using\n:meth:`Session.send_review_session_invites`.\n\n.. code-block:: python\n\n    session.send_review_session_invites(a_list_of_invitees)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/scope.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _example/scope:\n\n************\nUsing scopes\n************\n\n.. currentmodule:: ftrack_api.session\n\nEntities can be queried based on their scopes::\n\n    >>> tasks = session.query(\n    ...     'Task where scopes.name is \"London\"'\n    ... )\n\nScopes can be read and modified for entities::\n\n    >>> scope = session.query(\n    ...     'Scope where name is \"London\"'\n    ... )[0]\n    ...\n    ... if scope in task['scopes']:\n    ...     task['scopes'].remove(scope)\n    ... else:\n    ...     task['scopes'].append(scope)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/security_roles.rst",
    "content": "..\n    :copyright: Copyright (c) 2017 ftrack\n\n.. _example/security_roles:\n\n*********************************\nWorking with user security roles\n*********************************\n\n.. currentmodule:: ftrack_api.session\n\nThe API exposes `SecurityRole` and `UserSecurityRole` that can be used to\nspecify who should have access to certain data on different projects.\n\nList all available security roles like this::\n\n    security_roles = session.query(\n        'select name from SecurityRole where type is \"PROJECT\"'\n    )\n\n.. note::\n\n    We only query for project roles since those are the ones we can add to a\n    user for certain projects. Other types include API and ASSIGNED. Type API\n    can only be added to global API keys, which is currently not supported via\n    the api and type ASSIGNED only applies to assigned tasks.\n\nTo get all security roles from a user we can either use relations like this::\n\n    for user_security_role in user['user_security_roles']:\n        if user_security_role['is_all_projects']:\n            result_string = 'all projects'\n        else:\n            result_string = ', '.join(\n                [project['full_name'] for project in user_security_role['projects']]\n            )\n\n        print 'User has security role \"{0}\" which is valid on {1}.'.format(\n            user_security_role['security_role']['name'],\n            result_string\n        )\n\nor query them directly like this::\n\n    user_security_roles = session.query(\n        'UserSecurityRole where user.username is \"{0}\"'.format(session.api_user)\n    ).all()\n\nUser security roles can also be added to a user for all projects like this::\n\n    project_manager_role = session.query(\n        'SecurityRole where name is \"Project Manager\"'\n    ).one()\n\n    session.create('UserSecurityRole', {\n        'is_all_projects': True,\n        'user': user,\n        'security_role': project_manager_role\n    })\n    session.commit()\n\nor for certain projects only like this::\n\n    projects = session.query(\n        'Project where full_name is \"project1\" or full_name is \"project2\"'\n    ).all()[:]\n\n    session.create('UserSecurityRole', {\n        'user': user,\n        'security_role': project_manager_role,\n        'projects': projects\n    })\n    session.commit()\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/sync_ldap_users.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _example/sync_with_ldap:\n\n********************\nSync users with LDAP\n********************\n\n.. currentmodule:: ftrack_api.session\n\n\nIf ftrack is configured to connect to LDAP you may trigger a\nsynchronization through the api using the\n:meth:`ftrack_api.session.Session.call`::\n\n    result = session.call([\n        dict(\n            action='delayed_job',\n            job_type='SYNC_USERS_LDAP'\n        )\n    ])\n    job = result[0]['data]\n\nYou will get a `ftrack_api.entity.job.Job` instance back which can be used\nto check the success of the job::\n\n    if job.get('status') == 'failed':\n        # The job failed get the error.\n        logging.error(job.get('data'))\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/task_template.rst",
    "content": "..\n    :copyright: Copyright (c) 2017 ftrack\n\n.. _example/task_template:\n\n***************************\nWorking with Task Templates\n***************************\n\nTask templates can help you organize your workflows by building a collection\nof tasks to be applied for specific contexts. They can be applied to all `Context`\nobjects for example Project, Sequences, Shots, etc...\n\nQuery task templates\n=======================\n\nRetrive all task templates and there tasks for a project::\n\n    project = session.query('Project').first()\n\n    for task_template in project['project_schema']['task_templates']:\n        print('\\ntask template: {0}'.format(\n            task_template['name']\n        ))\n\n        for task_type in [t['task_type'] for t in task_template['items']]:\n            print('\\ttask type: {0}'.format(\n                task_type['name']\n            ))\n\n\n\n\"Apply\" a task template\n=======================\nCreate all tasks in a random task template directly under the project::\n\n\n    project = session.query('Project').first()\n\n    task_template = random.choice(\n        project['project_schema']['task_templates']\n    )\n\n    for task_type in [t['task_type'] for t in task_template['items']]:\n        session.create(\n            'Task', {\n                'name': task_type['name'],\n                'type': task_type,\n                'parent': project\n            }\n        )\n\n    session.commit()\n\n\n\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/thumbnail.rst",
    "content": "..\n    :copyright: Copyright (c) 2016 ftrack\n\n.. _example/thumbnail:\n\n***********************\nWorking with thumbnails\n***********************\n\nComponents can be used as thumbnails on various entities, including\n`Project`, `Task`, `AssetVersion` and `User`.  To create and set a thumbnail\nyou can use the helper method \n:meth:`~ftrack_api.entity.component.CreateThumbnailMixin.create_thumbnail` on\nany entity that can have a thumbnail::\n\n    task = session.get('Task', my_task_id)\n    thumbnail_component = task.create_thumbnail('/path/to/image.jpg')\n\nIt is also possible to set an entity thumbnail by setting its `thumbnail`\nrelation or `thumbnail_id` attribute to a component you would\nlike to use as a thumbnail. For a component to be usable as a thumbnail,\nit should\n\n    1. Be a FileComponent.\n    2. Exist in the *ftrack.server* :term:`location`.\n    3. Be of an appropriate resolution and valid file type.\n\nThe following example creates a new component in the server location, and\nuses that as a thumbnail for a task::\n\n    task = session.get('Task', my_task_id)\n    server_location = session.query(\n        'Location where name is \"ftrack.server\"'\n    ).one()\n\n    thumbnail_component = session.create_component(\n        '/path/to/image.jpg',\n        dict(name='thumbnail'),\n        location=server_location\n    )\n    task['thumbnail'] = thumbnail_component\n    session.commit()\n\nThe next example reuses a version's thumbnail for the asset parent thumbnail::\n\n    asset_version = session.get('AssetVersion', my_asset_version_id)\n    asset_parent = asset_version['asset']['parent']\n    asset_parent['thumbnail_id'] = asset_version['thumbnail_id']\n    session.commit()\n\n.. _example/thumbnail/url:\n\nRetrieving thumbnail URL\n========================\n\nTo get an URL to a thumbnail, `thumbnail_component`, which can be used used\nto download or display the image in an interface, use the following::\n\n    import ftrack_api.symbol\n    server_location = session.get('Location', ftrack_api.symbol.SERVER_LOCATION_ID)\n    thumbnail_url = server_location.get_thumbnail_url(thumbnail_component)\n    thumbnail_url_tiny = server_location.get_thumbnail_url(\n        thumbnail_component, size=100\n    )\n    thumbnail_url_large = server_location.get_thumbnail_url(\n        thumbnail_component, size=500\n    )\n\n.. seealso::\n\n    :ref:`example/component`\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/timer.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. _example/timer:\n\n************\nUsing timers\n************\n\n.. currentmodule:: ftrack_api.session\n\nTimers can be used to track how much time has been spend working on something.\n\nTo start a timer for a user::\n\n    user = # Get a user from ftrack.\n    task = # Get a task from ftrack.\n\n    user.start_timer(task)\n\nA timer has now been created for that user and should show up in the ftrack web\nUI.\n\nTo stop the currently running timer for a user and create a timelog from it::\n\n    user = # Get a user from ftrack.\n\n    timelog = user.stop_timer()\n\n.. note::\n\n    Starting a timer when a timer is already running will raise in an exception.\n    Use the force parameter to automatically stop the running timer first.\n\n    .. code-block:: python\n\n        user.start_timer(task, force=True)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/example/web_review.rst",
    "content": "..\n    :copyright: Copyright (c) 2016 ftrack\n\n.. currentmodule:: ftrack_api.session\n\n.. _example/web_review:\n\n*************************\nPublishing for web review\n*************************\n\nFollow the :ref:`example/encode_media` example if you want to\nupload and encode media using ftrack.\n\nIf you already have a file encoded in the correct format and want to bypass\nthe built-in encoding in ftrack, you can create the component manually\nand add it to the `ftrack.server` location::\n\n    # Retrieve or create version.\n    version = session.query('AssetVersion', 'SOME-ID')\n\n    server_location = session.query('Location where name is \"ftrack.server\"').one()\n    filepath = '/path/to/local/file.mp4'\n\n    component = version.create_component(\n        path=filepath,\n        data={\n            'name': 'ftrackreview-mp4'\n        },\n        location=server_location\n    )\n\n    # Meta data needs to contain *frameIn*, *frameOut* and *frameRate*.\n    component['metadata']['ftr_meta'] = json.dumps({\n        'frameIn': 0,\n        'frameOut': 150,\n        'frameRate': 25\n    })\n\n    component.session.commit()\n\nTo publish an image for review the steps are similar::\n\n    # Retrieve or create version.\n    version = session.query('AssetVersion', 'SOME-ID')\n\n    server_location = session.query('Location where name is \"ftrack.server\"').one()\n    filepath = '/path/to/image.jpg'\n\n    component = version.create_component(\n        path=filepath,\n        data={\n            'name': 'ftrackreview-image'\n        },\n        location=server_location\n    )\n\n    # Meta data needs to contain *format*.\n    component['metadata']['ftr_meta'] = json.dumps({\n        'format': 'image'\n    })\n\n    component.session.commit()\n\nHere is a list of components names and how they should be used:\n\n==================  =====================================\nComponent name      Use\n==================  =====================================\nftrackreview-image  Images reviewable in the browser\nftrackreview-mp4    H.264/mp4 video reviewable in browser\nftrackreview-webm   WebM video reviewable in browser\n==================  =====================================\n\n.. note::\n\n    Make sure to use the pre-defined component names and set the `ftr_meta` on\n    the components or review will not work.\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/glossary.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n********\nGlossary\n********\n\n.. glossary::\n\n    accessor\n        An implementation (typically a :term:`Python` plugin) for accessing\n        a particular type of storage using a specific protocol.\n\n        .. seealso:: :ref:`locations/overview/accessors`\n\n    action\n        Actions in ftrack provide a standardised way to integrate other tools,\n        either off-the-shelf or custom built, directly into your ftrack\n        workflow.\n\n        .. seealso:: :ref:`ftrack:using/actions`\n\n    api\n        Application programming interface.\n\n    arrow\n        A Python library that offers a sensible, human-friendly approach to \n        creating, manipulating, formatting and converting dates, times, and\n        timestamps. Read more at http://crsmithdev.com/arrow/\n\n    asset\n        A container for :term:`asset versions <asset version>`, typically\n        representing the output from an artist. For example, 'geometry'\n        from a modeling artist. Has an :term:`asset type` that categorises the\n        asset.\n\n    asset type\n        Category for a particular asset.\n\n    asset version\n        A specific version of data for an :term:`asset`. Can contain multiple\n        :term:`components <component>`.\n\n    component\n        A container to hold any type of data (such as a file or file sequence).\n        An :term:`asset version` can have any number of components, each with\n        a specific name. For example, a published version of geometry might\n        have two components containing the high and low resolution files, with\n        the component names as 'hires' and 'lowres' respectively.\n\n    PEP-8\n        Style guide for :term:`Python` code. Read the guide at \n        https://www.python.org/dev/peps/pep-0008/\n\n    plugin\n        :term:`Python` plugins are used by the API to extend it with new\n        functionality, such as :term:`locations <location>` or :term:`actions <action>`.\n\n        .. seealso:: :ref:`understanding_sessions/plugins`\n\n    python\n        A programming language that lets you work more quickly and integrate\n        your systems more effectively. Often used in creative industries. Visit\n        the language website at http://www.python.org\n\n    PyPi\n        :term:`Python` package index. The Python Package Index or PyPI is the\n        official third-party software repository for the Python programming\n        language. Visit the website at https://pypi.python.org/pypi\n\n    resource identifier\n        A string that is stored in ftrack as a reference to a resource (such as\n        a file) in a specific location. Used by :term:`accessors <accessor>` to\n        determine how to access data.\n\n        .. seealso:: :ref:`locations/overview/resource_identifiers`\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/handling_events.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _handling_events:\n\n***************\nHandling events\n***************\n\n.. currentmodule:: ftrack_api.event\n\nEvents are generated in ftrack when things happen such as a task being updated\nor a new version being published. Each :class:`~ftrack_api.session.Session`\nautomatically connects to the event server and can be used to subscribe to\nspecific events and perform an action as a result. That action could be updating\nanother related entity based on a status change or generating folders when a new\nshot is created for example.\n\nThe :class:`~hub.EventHub` for each :class:`~ftrack_api.session.Session` is\naccessible via :attr:`Session.event_hub\n<~ftrack_api.session.Session.event_hub>`.\n\n.. _handling_events/subscribing:\n\nSubscribing to events\n=====================\n\nTo listen to events, you register a function against a subscription using\n:meth:`Session.event_hub.subscribe <hub.EventHub.subscribe>`. The subscription\nuses the :ref:`expression <handling_events/expressions>` syntax and will filter\nagainst each :class:`~base.Event` instance to determine if the registered\nfunction should receive that event. If the subscription matches, the registered\nfunction will be called with the :class:`~base.Event` instance as its sole\nargument. The :class:`~base.Event` instance is a mapping like structure and can\nbe used like a normal dictionary.\n\nThe following example subscribes a function to receive all 'ftrack.update'\nevents and then print out the entities that were updated::\n\n    import ftrack_api\n\n\n    def my_callback(event):\n        '''Event callback printing all new or updated entities.'''\n        for entity in event['data'].get('entities', []):\n\n            # Print data for the entity.\n            print(entity)\n\n\n    # Subscribe to events with the update topic.\n    session = ftrack_api.Session()\n    session.event_hub.subscribe('topic=ftrack.update', my_callback)\n\nAt this point, if you run this, your code would exit almost immediately. This\nis because the event hub listens for events in a background thread. Typically,\nyou only want to stay connected whilst using the session, but in some cases you\nwill want to block and listen for events solely - a dedicated event processor.\nTo do this, use the :meth:`EventHub.wait <hub.EventHub.wait>` method::\n\n    # Wait for events to be received and handled.\n    session.event_hub.wait()\n\nYou cancel waiting for events by using a system interrupt (:kbd:`Ctrl-C`).\nAlternatively, you can specify a *duration* to process events for::\n\n    # Only wait and process events for 5 seconds.\n    session.event_hub.wait(duration=5)\n\n.. note::\n\n    Events are continually received and queued for processing in the background\n    as soon as the connection to the server is established. As a result you may\n    see a flurry of activity as soon as you call\n    :meth:`~hub.EventHub.wait` for the first time.\n\n.. _handling_events/subscribing/subscriber_information:\n\nSubscriber information\n----------------------\n\nWhen subscribing, you can also specify additional information about your\nsubscriber. This contextual information can be useful when routing events,\nparticularly when :ref:`targeting events\n<handling_events/publishing/targeting>`. By default, the\n:class:`~hub.EventHub` will set some default information, but it can be\nuseful to enhance this. To do so, simply pass in *subscriber* as a dictionary of\ndata to the :meth:`~hub.EventHub.subscribe` method::\n\n    session.event_hub.subscribe(\n        'topic=ftrack.update',\n        my_callback,\n        subscriber={\n            'id': 'my-unique-subscriber-id',\n            'applicationId': 'maya'\n        }\n    )\n\n.. _handling_events/subscribing/sending_replies:\n\nSending replies\n---------------\n\nWhen handling an event it is sometimes useful to be able to send information\nback to the source of the event. For example,\n:ref:`ftrack:developing/events/list/ftrack.location.request-resolve` would\nexpect a resolved path to be sent back.\n\nYou can craft a custom reply event if you want, but an easier way is just to\nreturn the appropriate data from your handler. Any non *None* value will be\nautomatically sent as a reply::\n\n    def on_event(event):\n        # Send following data in automatic reply.\n        return {'success': True, 'message': 'Cool!'}\n\n    session.event_hub.subscribe('topic=test-reply', on_event)\n\n.. seealso::\n\n    :ref:`handling_events/publishing/handling_replies`\n\n.. note::\n\n    Some events are published :ref:`synchronously\n    <handling_events/publishing/synchronously>`. In this case, any returned data\n    is passed back to the publisher directly.\n\n.. _handling_events/subscribing/stopping_events:\n\nStopping events\n---------------\n\nThe *event* instance passed to each event handler also provides a method for\nstopping the event, :meth:`Event.stop <base.Event.stop>`.\n\nOnce an event has been stopped, no further handlers for that specific event\nwill be called **locally**. Other handlers in other processes may still be\ncalled.\n\nCombining this with setting appropriate priorities when subscribing to a topic\nallows handlers to prevent lower priority handlers running when desired.\n\n    >>> import ftrack_api\n    >>> import ftrack_api.event.base\n    >>>\n    >>> def callback_a(event):\n    ...     '''Stop the event!'''\n    ...     print('Callback A')\n    ...     event.stop()\n    >>>\n    >>> def callback_b(event):\n    ...     '''Never run.'''\n    ...     print('Callback B')\n    >>>\n    >>> session = ftrack_api.Session()\n    >>> session.event_hub.subscribe(\n    ...     'topic=test-stop-event', callback_a, priority=10\n    ... )\n    >>> session.event_hub.subscribe(\n    ...     'topic=test-stop-event', callback_b, priority=20\n    ... )\n    >>> session.event_hub.publish(\n    ...     ftrack_api.event.base.Event(topic='test-stop-event')\n    ... )\n    >>> session.event_hub.wait(duration=5)\n    Callback A called.\n\n.. _handling_events/publishing:\n\nPublishing events\n=================\n\nSo far we have looked at listening to events coming from ftrack. However, you\nare also free to publish your own events (or even publish relevant ftrack\nevents).\n\nTo do this, simply construct an instance of :class:`ftrack_api.event.base.Event`\nand pass it to :meth:`EventHub.publish <hub.EventHub.publish>` via the session::\n\n    import ftrack_api.event.base\n\n    event = ftrack_api.event.base.Event(\n        topic='my-company.some-topic',\n        data={'key': 'value'}\n    )\n    session.event_hub.publish(event)\n\nThe event hub will automatically add some information to your event before it\ngets published, including the *source* of the event. By default the event source\nis just the event hub, but you can customise this to provide more relevant\ninformation if you want. For example, if you were publishing from within Maya::\n\n    session.event_hub.publish(ftrack_api.event.base.Event(\n        topic='my-company.some-topic',\n        data={'key': 'value'},\n        source={\n            'applicationId': 'maya'\n        }\n    ))\n\nRemember that all supplied information can be used by subscribers to filter\nevents so the more accurate the information the better.\n\n.. _handling_events/publishing/synchronously:\n\nPublish synchronously\n---------------------\n\nIt is also possible to call :meth:`~hub.EventHub.publish` synchronously by\npassing `synchronous=True`. In synchronous mode, only local handlers will be\ncalled. The result from each called handler is collected and all the results\nreturned together in a list::\n\n    >>> import ftrack_api\n    >>> import ftrack_api.event.base\n    >>>\n    >>> def callback_a(event):\n    ...     return 'A'\n    >>>\n    >>> def callback_b(event):\n    ...     return 'B'\n    >>>\n    >>> session = ftrack_api.Session()\n    >>> session.event_hub.subscribe(\n    ...     'topic=test-synchronous', callback_a, priority=10\n    ... )\n    >>> session.event_hub.subscribe(\n    ...     'topic=test-synchronous', callback_b, priority=20\n    ... )\n    >>> results = session.event_hub.publish(\n    ...     ftrack_api.event.base.Event(topic='test-synchronous'),\n    ...     synchronous=True\n    ... )\n    >>> print results\n    ['A', 'B']\n\n.. _handling_events/publishing/handling_replies:\n\nHandling replies\n----------------\n\nWhen publishing an event it is also possible to pass a callable that will be\ncalled with any :ref:`reply event <handling_events/subscribing/sending_replies>`\nreceived in response to the published event.\n\nTo do so, simply pass in a callable as the *on_reply* parameter::\n\n    def handle_reply(event):\n        print 'Got reply', event\n\n    session.event_hub.publish(\n        ftrack_api.event.base.Event(topic='test-reply'),\n        on_reply=handle_reply\n    )\n\n.. _handling_events/publishing/targeting:\n\nTargeting events\n----------------\n\nIn addition to subscribers filtering events to receive, it is also possible to\ngive an event a specific target to help route it to the right subscriber.\n\nTo do this, set the *target* value on the event to an :ref:`expression\n<handling_events/expressions>`. The expression will filter against registered\n:ref:`subscriber information\n<handling_events/subscribing/subscriber_information>`.\n\nFor example, if you have many subscribers listening for a event, but only want\none of those subscribers to get the event, you can target the event to the\nsubscriber using its registered subscriber id::\n\n    session.event_hub.publish(\n        ftrack_api.event.base.Event(\n            topic='my-company.topic',\n            data={'key': 'value'},\n            target='id=my-custom-subscriber-id'\n        )\n    )\n\n.. _handling_events/expressions:\n\nExpressions\n===========\n\nAn expression is used to filter against a data structure, returning whether the\nstructure fulfils the expression requirements. Expressions are currently used\nfor subscriptions when :ref:`subscribing to events\n<handling_events/subscribing>` and for targets when :ref:`publishing targeted\nevents <handling_events/publishing/targeting>`.\n\nThe form of the expression is loosely groupings of 'key=value' with conjunctions\nto join them.\n\nFor example, a common expression for subscriptions is to filter against an event\ntopic::\n\n    'topic=ftrack.location.component-added'\n\nHowever, you can also perform more complex filtering, including accessing\nnested parameters::\n\n    'topic=ftrack.location.component-added and data.locationId=london'\n\n.. note::\n\n    If the structure being tested does not have any value for the specified\n    key reference then it is treated as *not* matching.\n\nYou can also use a single wildcard '*' at the end of any value for matching\nmultiple values. For example, the following would match all events that have a\ntopic starting with 'ftrack.'::\n\n    'topic=ftrack.*'\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/index.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n#################\nftrack Python API\n#################\n\nWelcome to the ftrack :term:`Python` :term:`API` documentation.\n\n.. important::\n\n    This is the new :term:`Python` client for the ftrack :term:`API`. If you are\n    migrating from the old client then please read the dedicated\n    :ref:`migration guide <release/migrating_from_old_api>`.\n\n.. toctree::\n    :maxdepth: 1\n\n    introduction\n    installing\n    tutorial\n    understanding_sessions\n    working_with_entities\n    querying\n    handling_events\n    caching\n    locations/index\n    example/index\n    api_reference/index\n    event_list\n    environment_variables\n    security_and_authentication\n    release/index\n    glossary\n\n******************\nIndices and tables\n******************\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/installing.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _installing:\n\n**********\nInstalling\n**********\n\n.. highlight:: bash\n\nInstallation is simple with `pip <http://www.pip-installer.org/>`_::\n\n    pip install ftrack-python-api\n\nBuilding from source\n====================\n\nYou can also build manually from the source for more control. First obtain a\ncopy of the source by either downloading the\n`zipball <https://bitbucket.org/ftrack/ftrack-python-api/get/master.zip>`_ or\ncloning the public repository::\n\n    git clone git@bitbucket.org:ftrack/ftrack-python-api.git\n\nThen you can build and install the package into your current Python\nsite-packages folder::\n\n    python setup.py install\n\nAlternatively, just build locally and manage yourself::\n\n    python setup.py build\n\nBuilding documentation from source\n----------------------------------\n\nTo build the documentation from source::\n\n    python setup.py build_sphinx\n\nThen view in your browser::\n\n    file:///path/to/ftrack-python-api/build/doc/html/index.html\n\nRunning tests against the source\n--------------------------------\n\nWith a copy of the source it is also possible to run the unit tests::\n\n    python setup.py test\n\nDependencies\n============\n\n* `ftrack server <http://ftrack.rtd.ftrack.com/en/stable/>`_ >= 3.3.11\n* `Python <http://python.org>`_ >= 2.7, < 3\n* `Requests <http://docs.python-requests.org>`_ >= 2, <3,\n* `Arrow <http://crsmithdev.com/arrow/>`_ >= 0.4.4, < 1,\n* `termcolor <https://pypi.python.org/pypi/termcolor>`_ >= 1.1.0, < 2,\n* `pyparsing <http://pyparsing.wikispaces.com/>`_ >= 2.0, < 3,\n* `Clique <http://clique.readthedocs.org/>`_ >= 1.2.0, < 2,\n* `websocket-client <https://pypi.python.org/pypi/websocket-client>`_ >= 0.40.0, < 1\n\nAdditional For building\n-----------------------\n\n* `Sphinx <http://sphinx-doc.org/>`_ >= 1.2.2, < 2\n* `sphinx_rtd_theme <https://github.com/snide/sphinx_rtd_theme>`_ >= 0.1.6, < 1\n* `Lowdown <http://lowdown.rtd.ftrack.com/en/stable/>`_ >= 0.1.0, < 2\n\nAdditional For testing\n----------------------\n\n* `Pytest <http://pytest.org>`_  >= 2.3.5, < 3\n* `pytest-mock <https://pypi.python.org/pypi/pytest-mock/>`_ >= 0.4, < 1,\n* `pytest-catchlog <https://pypi.python.org/pypi/pytest-catchlog/>`_ >= 1, <=2"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/introduction.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _introduction:\n\n************\nIntroduction\n************\n\nThis API allows developers to write :term:`Python` scripts that talk directly\nwith an ftrack server. The scripts can perform operations against that server\ndepending on granted permissions.\n\nWith any API it is important to find the right balance between flexibility and\nusefulness. If an API is too low level then everyone ends up writing boilerplate\ncode for common problems and usually in an non-uniform way making it harder to\nshare scripts with others. It's also harder to get started with such an API.\nConversely, an API that attempts to be too smart can often become restrictive\nwhen trying to do more advanced functionality or optimise for performance.\n\nWith this API we have tried to strike the right balance between these two,\nproviding an API that should be simple to use out-of-the-box, but also expose\nmore flexibility and power when needed.\n\nNothing is perfect though, so please do provide feedback on ways that we can\ncontinue to improve this API for your specific needs.\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/locations/configuring.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _locations/configuring:\n\n*********************\nConfiguring locations\n*********************\n\nTo allow management of data by a location or retrieval of filesystem paths where\nsupported, a location instance needs to be configured in a session with an\n:term:`accessor` and :term:`structure`.\n\n.. note::\n\n    The standard builtin locations require no further setup or configuration\n    and it is not necessary to read the rest of this section to use them.\n\nBefore continuing, make sure that you are familiar with the general concepts\nof locations by reading the :ref:`locations/overview`.\n\n.. _locations/configuring/manually:\n\nConfiguring manually\n====================\n\nLocations can be configured manually when using a session by retrieving the\nlocation and setting the appropriate attributes::\n\n    location = session.query('Location where name is \"my.location\"').one()\n    location.structure = ftrack_api.structure.id.IdStructure()\n    location.priority = 50\n\n.. _locations/configuring/automatically:\n\nConfiguring automatically\n=========================\n\nOften the configuration of locations should be determined by developers\nlooking after the core pipeline and so ftrack provides a way for a plugin to\nbe registered to configure the necessary locations for each session. This can\nthen be managed centrally if desired.\n\nThe configuration is handled through the standard events system via a topic\n*ftrack.api.session.configure-location*. Set up an :ref:`event listener plugin\n<understanding_sessions/plugins>` as normal with a register function that\naccepts a :class:`~ftrack_api.session.Session` instance. Then register a\ncallback against the relevant topic to configure locations at the appropriate\ntime::\n\n    import ftrack_api\n    import ftrack_api.entity.location\n    import ftrack_api.accessor.disk\n    import ftrack_api.structure.id\n\n\n    def configure_locations(event):\n        '''Configure locations for session.'''\n        session = event['data']['session']\n\n        # Find location(s) and customise instances.\n        location = session.query('Location where name is \"my.location\"').one()\n        ftrack_api.mixin(location, ftrack_api.entity.location.UnmanagedLocationMixin)\n        location.accessor = ftrack_api.accessor.disk.DiskAccessor(prefix='')\n        location.structure = ftrack_api.structure.id.IdStructure()\n        location.priority = 50\n\n\n    def register(session):\n        '''Register plugin with *session*.'''\n        session.event_hub.subscribe(\n            'topic=ftrack.api.session.configure-location',\n            configure_locations\n        )\n\n.. note::\n\n    If you expect the plugin to also be evaluated by the legacy API, remember\n    to :ref:`validate the arguments <ftrack:release/migration/3.0.29/developer_notes/register_function>`.\n\nSo long as the directory containing the plugin exists on your\n:envvar:`FTRACK_EVENT_PLUGIN_PATH`, the plugin will run for each session\ncreated and any configured locations will then remain configured for the\nduration of that related session.\n\nBe aware that you can configure many locations in one plugin or have separate\nplugins for different locations - the choice is entirely up to you!\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/locations/index.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _developing/locations:\n\n*********\nLocations\n*********\n\nLearn how to access locations using the API and configure your own location\nplugins.\n\n.. toctree::\n    :maxdepth: 1\n\n    overview\n    tutorial\n    configuring\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/locations/overview.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _locations/overview:\n\n********\nOverview\n********\n\nLocations provides a way to easily track and manage data (files, image sequences\netc.) using ftrack.\n\nWith locations it is possible to see where published data is in the world and\nalso to transfer data automatically between different locations, even different\nstorage mechanisms, by defining a few simple :term:`Python` plugins. By keeping\ntrack of the size of the data it also helps manage storage capacity better. In\naddition, the intrinsic links to production information makes assigning work to\nothers and transferring only the relevant data much simpler as well as greatly\nreducing the burden on those responsible for archiving finished work.\n\nConcepts\n========\n\nThe system is implemented in layers using a few key concepts in order to provide\na balance between out of the box functionality and custom configuration.\n\n.. _locations/overview/locations:\n\nLocations\n---------\n\nData locations can be varied in scope and meaning - a facility, a laptop, a\nspecific drive. As such, rather than place a hard limit on what can be\nconsidered a location, ftrack simply requires that a location be identifiable by\na string and that string be unique to that location.\n\nA global company with facilities in many different parts of the world might\nfollow a location naming convention similar to the following:\n\n    * 'ftrack.london.server01'\n    * 'ftrack.london.server02'\n    * 'ftrack.nyc.server01'\n    * 'ftrack.amsterdam.server01'\n    * '<company>.<city>.<server#>'\n\nWhereas, for a looser setup, the following might suit better:\n\n    * 'bjorns-workstation'\n    * 'fredriks-mobile'\n    * 'martins-laptop'\n    * 'cloud-backup'\n\nAvailability\n------------\n\nWhen tracking data across several locations it is important to be able to\nquickly find out where data is available and where it is not. As such, ftrack\nprovides simple mechanisms for retrieving information on the availability of a\n:term:`component` in each location.\n\nFor a single file, the availability with be either 0% or 100%. For containers,\nsuch as file sequences, each file is tracked separately and the availability of\nthe container calculated as an overall percentage (e.g. 47%).\n\n.. _locations/overview/accessors:\n\nAccessors\n---------\n\nDue to the flexibility of what can be considered a location, the system must be\nable to cope with locations that represent different ways of storing data. For\nexample, data might be stored on a local hard drive, a cloud service or even in\na database.\n\nIn addition, the method of accessing that storage can change depending on\nperspective - local filesystem, FTP, S3 API etc.\n\nTo handle this, ftrack introduces the idea of an :term:`accessor` that provides\naccess to the data in a standard way. An accessor is implemented in\n:term:`Python` following a set interface and can be configured at runtime to\nprovide relevant access to a location.\n\nWith an accessor configured for a location, it becomes possible to not only\ntrack data, but also manage it through ftrack by using the accessor to add and\nremove data from the location.\n\nAt present, ftrack includes a :py:class:`disk accessor\n<ftrack_api.accessor.disk.DiskAccessor>` for local filesystem access. More will be\nadded over time and developers are encouraged to contribute their own.\n\n.. _locations/overview/structure:\n\nStructure\n---------\n\nAnother important consideration for locations is how data should be structured\nin the location (folder structure and naming conventions). For example,\ndifferent facilities may want to use different folder structures, or different\nstorage mechanisms may use different paths for the data.\n\nFor this, ftrack supports the use of a :term:`Python` structure plugin. This\nplugin is called when adding a :term:`component` to a location in order to\ndetermine the correct structure to use.\n\n.. note::\n\n    A structure plugin accepts an ftrack entity as its input and so can be\n    reused for generating general structures as well. For example, an action\n    callback could be implemented to create the base folder structure for some\n    selected shots by reusing a structure plugin.\n\n.. _locations/overview/resource_identifiers:\n\nResource identifiers\n--------------------\n\nWhen a :term:`component` can be linked to multiple locations it becomes\nnecessary to store information about the relationship on the link rather than\ndirectly on the :term:`component` itself. The most important information is the\npath to the data in that location.\n\nHowever, as seen above, not all locations may be filesystem based or accessed\nusing standard filesystem protocols. For this reason, and to help avoid\nconfusion, this *path* is referred to as a :term:`resource identifier` and no\nlimitations are placed on the format. Keep in mind though that accessors use\nthis information (retrieved from the database) in order to work out how to\naccess the data, so the format used must be compatible with all the accessors\nused for any one location. For this reason, most\n:term:`resource identifiers <resource identifier>` should ideally look like\nrelative filesystem paths.\n\n.. _locations/overview/resource_identifiers/transformer:\n\nTransformer\n^^^^^^^^^^^\n\nTo further support custom formats for\n:term:`resource identifiers <resource identifier>`, it is also possible to\nconfigure a resource identifier transformer plugin which will convert\nthe identifiers before they are stored centrally and after they are retrieved.\n\nA possible use case of this might be to store JSON encoded metadata about a path\nin the database and convert this to an actual filesystem path on retrieval.\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/locations/tutorial.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _locations/tutorial:\n\n********\nTutorial\n********\n\nThis tutorial is a walkthrough on how you interact with Locations using the \nftrack :term:`API`. Before you read this tutorial, make sure you familiarize\nyourself with the location concepts by reading the :ref:`locations/overview`.\n\nAll examples assume you are using Python 2.x, have the :mod:`ftrack_api`\nmodule imported and a :class:`session <ftrack_api.session.Session>` created.\n\n.. code-block:: python\n\n    import ftrack_api\n    session = ftrack_api.Session()\n\n.. _locations/creating-locations:\n\nCreating locations\n==================\n\nLocations can be created just like any other entity using\n:meth:`Session.create <ftrack_api.session.Session.create>`::\n\n    location = session.create('Location', dict(name='my.location'))\n    session.commit()\n\n.. note:: \n    Location names beginning with ``ftrack.`` are reserved for internal use. Do\n    not use this prefix for your location names.\n\nTo create a location only if it doesn't already exist use the convenience \nmethod :meth:`Session.ensure <ftrack_api.session.Session.ensure>`. This will return\neither an existing matching location or a newly created one.\n\nRetrieving locations\n====================\n\nYou can retrieve existing locations using the standard session\n:meth:`~ftrack_api.session.Session.get` and\n:meth:`~ftrack_api.session.Session.query` methods::\n\n    # Retrieve location by unique id.\n    location_by_id = session.get('Location', 'unique-id')\n\n    # Retrieve location by name.\n    location_by_name = session.query(\n        'Location where name is \"my.location\"'\n    ).one()\n\nTo retrieve all existing locations use a standard query::\n\n    all_locations = session.query('Location').all()\n    for existing_location in all_locations:\n        print existing_location['name']\n\nConfiguring locations\n=====================\n\nAt this point you have created a custom location \"my.location\" in the database\nand have an instance to reflect that. However, the location cannot be used in\nthis session to manage  data unless it has been configured. To configure a\nlocation for the session, set the appropriate attributes for accessor and\nstructure::\n\n    import tempfile\n    import ftrack_api.accessor.disk\n    import ftrack_api.structure.id\n\n    # Assign a disk accessor with *temporary* storage\n    location.accessor = ftrack_api.accessor.disk.DiskAccessor(\n        prefix=tempfile.mkdtemp()\n    )\n\n    # Assign using ID structure.\n    location.structure = ftrack_api.structure.id.IdStructure()\n\n    # Set a priority which will be used when automatically picking locations.\n    # Lower number is higher priority.\n    location.priority = 30\n\nTo learn more about how to configure locations automatically in a session, see\n:ref:`locations/configuring`.\n\n.. note::\n\n    If a location is not configured in a session it can still be used as a\n    standard entity and to find out availability of components\n\nUsing components with locations\n===============================\n\nThe Locations :term:`API` tries to use sane defaults to stay out of your way.\nWhen creating :term:`components <component>`, a location is automatically picked\nusing :meth:`Session.pick_location <ftrack_api.session.Session.pick_location>`::\n\n    (_, component_path) = tempfile.mkstemp(suffix='.txt')\n    component_a = session.create_component(path=component_path)\n\nTo override, specify a location explicitly::\n\n    (_, component_path) = tempfile.mkstemp(suffix='.txt')\n    component_b = session.create_component(\n        path=component_path, location=location\n    )\n\nIf you set the location to ``None``, the component will only be present in the\nspecial origin location for the duration of the session::\n\n    (_, component_path) = tempfile.mkstemp(suffix='.txt')\n    component_c = session.create_component(path=component_path, location=None)\n\nAfter creating a :term:`component` in a location, it can be added to another\nlocation by calling :meth:`Location.add_component\n<ftrack_api.entity.location.Location.add_component>` and passing the location to\nuse as the *source* location::\n\n    origin_location = session.query(\n        'Location where name is \"ftrack.origin\"'\n    ).one()\n    location.add_component(component_c, origin_location)\n\nTo remove a component from a location use :meth:`Location.remove_component\n<ftrack_api.entity.location.Location.remove_component>`::\n\n    location.remove_component(component_b)\n\nEach location specifies whether to automatically manage data when adding or\nremoving components. To ensure that a location does not manage data, mixin the\nrelevant location mixin class before use::\n\n    import ftrack_api\n    import ftrack_api.entity.location\n\n    ftrack_api.mixin(location, ftrack_api.entity.location.UnmanagedLocationMixin)\n\nAccessing paths\n===============\n\nThe locations system is designed to help avoid having to deal with filesystem\npaths directly. This is particularly important when you consider that a number\nof locations won't provide any direct filesystem access (such as cloud storage).\n\nHowever, it is useful to still be able to get a filesystem path from locations\nthat support them (typically those configured with a\n:class:`~ftrack_api.accessor.disk.DiskAccessor`). For example, you might need to\npass a filesystem path to another application or perform a copy using a faster\nprotocol.\n\nTo retrieve the path if available, use :meth:`Location.get_filesystem_path\n<ftrack_api.entity.location.Location.get_filesystem_path>`::\n\n    print location.get_filesystem_path(component_c)\n\nObtaining component availability\n================================\n\nComponents in locations have a notion of availability. For regular components,\nconsisting of a single file, the availability would be either 0 if the \ncomponent is unavailable or 100 percent if the component is available in the \nlocation. Composite components, like image sequences, have an availability \nwhich is proportional to the amount of child components that have been added to \nthe location. \n\nFor example, an image sequence might currently be in a state of being \ntransferred to :data:`test.location`. If half of the images are transferred,  it\nmight be possible to start working with the sequence. To check availability use\nthe helper :meth:`Session.get_component_availability\n<ftrack_api.session.Session.get_component_availability>` method::\n\n    print session.get_component_availability(component_c)\n\nThere are also convenience methods on both :meth:`components\n<ftrack_api.entity.component.Component.get_availability>` and :meth:`locations\n<ftrack_api.entity.location.Location.get_component_availability>` for\nretrieving availability as well::\n\n    print component_c.get_availability()\n    print location.get_component_availability(component_c)\n\nLocation events\n===============\n\nIf you want to receive event notifications when components are added to or \nremoved from locations, you can subscribe to the topics published,\n:data:`ftrack_api.symbol.COMPONENT_ADDED_TO_LOCATION_TOPIC` or\n:data:`ftrack_api.symbol.COMPONENT_REMOVED_FROM_LOCATION_TOPIC` and the callback\nyou want to be run.\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/querying.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _querying:\n\n********\nQuerying\n********\n\n.. currentmodule:: ftrack_api.session\n\nThe API provides a simple, but powerful query language in addition to iterating\ndirectly over entity attributes. Using queries can often substantially speed\nup your code as well as reduce the amount of code written.\n\nA query is issued using :meth:`Session.query` and returns a list of matching\nentities. The query always has a single *target* entity type that the query\nis built against. This means that you cannot currently retrieve back a list of\ndifferent entity types in one query, though using :ref:`projections\n<querying/projections>` does allow retrieving related entities of a different\ntype in one go.\n\nThe syntax for a query is:\n\n.. code-block:: none\n\n    select <projections> from <entity type> where <criteria>\n\nHowever, both the selection of projections and criteria are optional. This means\nthe most basic query is just to fetch all entities of a particular type, such as\nall projects in the system::\n\n    projects = session.query('Project')\n\nA query always returns a :class:`~ftrack_api.query.QueryResult` instance that\nacts like a list with some special behaviour. The main special behaviour is that\nthe actual query to the server is not issued until you iterate or index into the\nquery results::\n\n    for project in projects:\n        print project['name']\n\nYou can also explicitly call :meth:`~ftrack_api.query.QueryResult.all` on the\nresult set::\n\n    projects = session.query('Project').all()\n\n.. note::\n\n    This behaviour exists in order to make way for efficient *paging* and other\n    optimisations in future.\n\n.. _querying/criteria:\n\nUsing criteria to narrow results\n================================\n\nOften you will have some idea of the entities you want to retrieve. In this\ncase you can optimise your code by not fetching more data than you need. To do\nthis, add criteria to your query::\n\n    projects = session.query('Project where status is active')\n\nEach criteria follows the form:\n\n.. code-block:: none\n\n    <attribute> <operator> <value>\n\nYou can inspect the entity type or instance to find out which :ref:`attributes\n<working_with_entities/attributes>` are available to filter on for a particular\nentity type. The list of :ref:`operators <querying/criteria/operators>` that can\nbe applied and the types of values they expect is listed later on.\n\n.. _querying/criteria/combining:\n\nCombining criteria\n------------------\n\nMultiple criteria can be applied in a single expression by joining them with\neither ``and`` or ``or``::\n\n    projects = session.query(\n        'Project where status is active and name like \"%thrones\"'\n    )\n\nYou can use parenthesis to control the precedence when compound criteria are\nused (by default ``and`` takes precedence)::\n\n    projects = session.query(\n        'Project where status is active and '\n        '(name like \"%thrones\" or full_name like \"%thrones\")'\n    )\n\n.. _querying/criteria/relationships:\n\nFiltering on relationships\n--------------------------\n\nFiltering on relationships is also intuitively supported. Simply follow the\nrelationship using a dotted notation::\n\n    tasks_in_project = session.query(\n        'Task where project.id is \"{0}\"'.format(project['id'])\n    )\n\nThis works even for multiple strides across relationships (though do note that\nexcessive strides can affect performance)::\n\n    tasks_completed_in_project = session.query(\n        'Task where project.id is \"{0}\" and '\n        'status.type.name is \"Done\"'\n        .format(project['id'])\n    )\n\nThe same works for collections (where each entity in the collection is compared\nagainst the subsequent condition)::\n\n    import arrow\n\n    tasks_with_time_logged_today = session.query(\n        'Task where timelogs.start >= \"{0}\"'.format(arrow.now().floor('day'))\n    )\n\nIn the above query, each *Task* that has at least one *Timelog* with a *start*\ntime greater than the start of today is returned.\n\nWhen filtering on relationships, the conjunctions ``has`` and ``any`` can be\nused to specify how the criteria should be applied. This becomes important when\nquerying using multiple conditions on collection relationships. The relationship\ncondition can be written against the following form::\n\n    <not?> <relationship> <has|any> (<criteria>)\n\nFor optimal performance ``has`` should be used for scalar relationships when\nmultiple conditions are involved. For example, to find notes by a specific\nauthor when only name is known::\n\n    notes_written_by_jane_doe = session.query(\n        'Note where author has (first_name is \"Jane\" and last_name is \"Doe\")'\n    )\n\nThis query could be written without ``has``, giving the same results::\n\n    notes_written_by_jane_doe = session.query(\n        'Note where author.first_name is \"Jane\" and author.last_name is \"Doe\"'\n    )\n\n``any`` should be used for collection relationships. For example, to find all\nprojects that have at least one metadata instance that has `key=some_key` \nand `value=some_value` the query would be::\n\n    projects_where_some_key_is_some_value = session.query(\n        'Project where metadata any (key=some_key and value=some_value)'\n    )\n\nIf the query was written without ``any``, projects with one metadata matching \n*key* and another matching the *value* would be returned.\n\n``any`` can also be used to query for empty relationship collections::\n\n    users_without_timelogs = session.query(\n        'User where not timelogs any ()'\n    )\n\n.. _querying/criteria/operators:\n\nSupported operators\n-------------------\n\nThis is the list of currently supported operators:\n\n+--------------+----------------+----------------------------------------------+\n| Operators    | Description    | Example                                      |\n+==============+================+==============================================+\n| =            | Exactly equal. | name is \"martin\"                             |\n| is           |                |                                              |\n+--------------+----------------+----------------------------------------------+\n| !=           | Not exactly    | name is_not \"martin\"                         |\n| is_not       | equal.         |                                              |\n+--------------+----------------+----------------------------------------------+\n| >            | Greater than   | start after \"2015-06-01\"                     |\n| after        | exclusive.     |                                              |\n| greater_than |                |                                              |\n+--------------+----------------+----------------------------------------------+\n| <            | Less than      | end before \"2015-06-01\"                      |\n| before       | exclusive.     |                                              |\n| less_than    |                |                                              |\n+--------------+----------------+----------------------------------------------+\n| >=           | Greater than   | bid >= 10                                    |\n|              | inclusive.     |                                              |\n+--------------+----------------+----------------------------------------------+\n| <=           | Less than      | bid <= 10                                    |\n|              | inclusive.     |                                              |\n+--------------+----------------+----------------------------------------------+\n| in           | One of.        | status.type.name in (\"In Progress\", \"Done\")  |\n+--------------+----------------+----------------------------------------------+\n| not_in       | Not one of.    | status.name not_in (\"Omitted\", \"On Hold\")    |\n+--------------+----------------+----------------------------------------------+\n| like         | Matches        | name like \"%thrones\"                         |\n|              | pattern.       |                                              |\n+--------------+----------------+----------------------------------------------+\n| not_like     | Does not match | name not_like \"%thrones\"                     |\n|              | pattern.       |                                              |\n+--------------+----------------+----------------------------------------------+\n| has          | Test scalar    | author has (first_name is \"Jane\" and         |\n|              | relationship.  | last_name is \"Doe\")                          |\n+--------------+----------------+----------------------------------------------+\n| any          | Test collection| metadata any (key=some_key and               |\n|              | relationship.  | value=some_value)                            |\n+--------------+----------------+----------------------------------------------+\n\n.. _querying/projections:\n\nOptimising using projections\n============================\n\nIn :ref:`understanding_sessions` we mentioned :ref:`auto-population\n<understanding_sessions/auto_population>` of attribute values on access. This\nmeant that when iterating over a lot of entities and attributes a large number\nof queries were being sent to the server. Ultimately, this can cause your code\nto run slowly::\n\n    >>> projects = session.query('Project')\n    >>> for project in projects:\n    ...     print(\n    ...         # Multiple queries issued here for each attribute accessed for\n    ...         # each project in the loop!\n    ...         '{project[full_name]} - {project[status][name]})'\n    ...         .format(project=project)\n    ...     )\n\n\nFortunately, there is an easy way to optimise. If you know what attributes you\nare interested in ahead of time you can include them in your query string as\n*projections* in order to fetch them in one go::\n\n    >>> projects = session.query(\n    ...     'select full_name, status.name from Project'\n    ... )\n    >>> for project in projects:\n    ...     print(\n    ...         # No additional queries issued here as the values were already\n    ...         # loaded by the above query!\n    ...         '{project[full_name]} - {project[status][name]})'\n    ...         .format(project=project)\n    ...     )\n\nNotice how this works for related entities as well. In the example above, we\nalso fetched the name of each *Status* entity attached to a project in the same\nquery, which meant that no further queries had to be issued when accessing those\nnested attributes.\n\n.. note::\n\n    There are no arbitrary limits to the number (or depth) of projections, but\n    do be aware that excessive projections can ultimately result in poor\n    performance also. As always, it is about choosing the right tool for the\n    job.\n\nYou can also customise the\n:ref:`working_with_entities/entity_types/default_projections` to use for each\nentity type when none are specified in the query string.\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/release/index.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _release:\n\n***************************\nRelease and migration notes\n***************************\n\nFind out information about what has changed between versions and any important\nmigration notes to be aware of when switching to a new version.\n\n.. toctree::\n    :maxdepth: 1\n\n    release_notes\n    migration\n    migrating_from_old_api\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/release/migrating_from_old_api.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. _release/migrating_from_old_api:\n\n**********************\nMigrating from old API\n**********************\n\n.. currentmodule:: ftrack_api.session\n\nWhy a new API?\n==============\n\nWith the introduction of Workflows, ftrack is capable of supporting a greater\ndiversity of industries. We're enabling teams to closely align the system with\ntheir existing practices and naming conventions, resulting in a tool that feels\nmore natural and intuitive. The old API was locked to specific workflows, making\nit impractical to support this new feature naturally.\n\nWe also wanted this new flexibility to extend to developers, so we set about\nredesigning the API to fully leverage the power in the system. And while we had\nthe wrenches out, we figured why not go that extra mile and build in some of the\nfeatures that we see developers having to continually implement in-house across\ndifferent companies - features such as caching and support for custom pipeline\nextensions. In essence, we decided to build the API that, as pipeline\ndevelopers, we had always wanted from our production tracking and asset\nmanagement systems. We think we succeeded, and we hope you agree.\n\nInstalling\n==========\n\nBefore, you used to download the API package from your ftrack instance. With \neach release of the new API we make it available on :term:`PyPi`, and \ninstalling is super simple:\n\n.. code-block:: none\n\n    pip install ftrack-python-api\n\nBefore installing, it is always good to check the latest\n:ref:`release/release_notes`  to see which version of the ftrack server is\nrequired.\n\n.. seealso:: :ref:`installing`\n\nOverview\n========\n\nAn API needs to be approachable, so we built the new API to feel\nintuitive and familiar. We bundle all the core functionality into one place – a\nsession – with consistent methods for interacting with entities in the system::\n\n    import ftrack_api\n    session = ftrack_api.Session()\n\nThe session is responsible for loading plugins and communicating with the ftrack\nserver and allows you to use multiple simultaneous sessions. You will no longer\nneed to explicitly call :meth:`ftrack.setup` to load plugins.\n\nThe core methods are straightforward:\n\nSession.create\n    create a new entity, like a new version.\nSession.query\n    fetch entities from the server using a powerful query language.\nSession.delete\n    delete existing entities.\nSession.commit\n    commit all changes in one efficient call.\n\n.. note::\n\n    The new API batches create, update and delete operations by default for\n    efficiency. To synchronise local changes with the server you need to call\n    :meth:`Session.commit`.\n\nIn addition all entities in the API now act like simple Python dictionaries,\nwith some additional helper methods where appropriate. If you know a little\nPython (or even if you don't) getting up to speed should be a breeze::\n\n    >>> print user.keys()\n    ['first_name', 'last_name', 'email', ...]\n    >>> print user['email']\n    'old@example.com'\n    >>> user['email'] = 'new@example.com'\n\nAnd of course, relationships between entities are reflected in a natural way as\nwell::\n\n    new_timelog = session.create('Timelog', {...})\n    task['timelogs'].append(new_timelog)\n\n.. seealso :: :ref:`tutorial`\n\nThe new API also makes use of caching in order to provide more efficient\nretrieval of data by reducing the number of calls to the remote server.\n\n.. seealso:: :ref:`caching`\n\nOpen source and standard code style\n===================================\n\nThe new API is open source software and developed in public at\n`Bitbucket <https://bitbucket.org/ftrack/ftrack-python-api>`_. We welcome you\nto join us in the development and create pull requests there.\n\nIn the new API, we also follow the standard code style for Python,\n:term:`PEP-8`. This means that you will now find that methods and variables are\nwritten using  ``snake_case`` instead of ``camelCase``, amongst other things.\n\nPackage name\n============\n\nThe new package is named :mod:`ftrack_api`. By using a new package name, we\nenable you to use the old API and the new side-by-side in the same process.\n\nOld API::\n\n    import ftrack\n\nNew API::\n\n    import ftrack_api\n\nSpecifying your credentials\n===========================\n\nThe old API used three environment variables to authenticate with your ftrack\ninstance. While these continue to work as before, you now also have\nthe option to specify them when initializing the session::\n\n    >>> import ftrack_api\n    >>> session = ftrack_api.Session(\n    ...     server_url='https://mycompany.ftrackapp.com',\n    ...     api_key='7545384e-a653-11e1-a82c-f22c11dd25eq',\n    ...     api_user='martin'\n    ... )\n\nIn the examples below, will assume that you have imported the package and\ncreated a session.\n\n.. seealso:: \n\n    * :ref:`environment_variables`\n    * :ref:`tutorial`\n\n\nQuerying objects\n================\n\nThe old API relied on predefined methods for querying objects and constructors\nwhich enabled you to get an entity by it's id or name.\n\nOld API::\n\n    project = ftrack.getProject('dev_tutorial')\n    task = ftrack.Task('8923b7b3-4bf0-11e5-8811-3c0754289fd3')\n    user = ftrack.User('jane')\n\nNew API::\n\n    project = session.query('Project where name is \"dev_tutorial\"').one()\n    task = session.get('Task', '8923b7b3-4bf0-11e5-8811-3c0754289fd3')\n    user = session.query('User where username is \"jane\"').one()\n\nWhile the new API can be a bit more verbose for simple queries, it is much more\npowerful and allows you to filter on any field and preload related data::\n\n    tasks = session.query(\n        'select name, parent.name from Task '\n        'where project.full_name is \"My Project\" '\n        'and status.type.short is \"DONE\" '\n        'and not timelogs any ()'\n    ).all()\n\nThe above fetches all tasks for “My Project” that are done but have no timelogs.\nIt also pre-fetches related information about the tasks parent – all in one\nefficient query.\n\n.. seealso:: :ref:`querying`\n\nCreating objects\n================\n\nIn the old API, you create objects using specialized methods, such as \n:meth:`ftrack.createProject`, :meth:`Project.createSequence` and\n:meth:`Task.createShot`.\n\nIn the new API, you can create any object using :meth:`Session.create`. In\naddition, there are a few helper methods to reduce the amount of boilerplate\nnecessary to create certain objects. Don't forget to call :meth:`Session.commit`\nonce you have issued your create statements to commit your changes.\n\nAs an example, let's look at populating a project with a few entities.\n\nOld API::\n\n    project = ftrack.getProject('migration_test')\n\n    # Get default task type and status from project schema\n    taskType = project.getTaskTypes()[0]\n    taskStatus = project.getTaskStatuses(taskType)[0]\n\n    sequence = project.createSequence('001')\n\n    # Create five shots with one task each\n    for shot_number in xrange(10, 60, 10):\n        shot = sequence.createShot(\n            '{0:03d}'.format(shot_number)\n        )\n        shot.createTask(\n            'Task name',\n            taskType,\n            taskStatus\n        )\n\n\nNew API::\n\n    project = session.query('Project where name is \"migration_test\"').one()\n\n    # Get default task type and status from project schema\n    project_schema = project['project_schema']\n    default_shot_status = project_schema.get_statuses('Shot')[0]\n    default_task_type = project_schema.get_types('Task')[0]\n    default_task_status = project_schema.get_statuses(\n        'Task', default_task_type['id']\n    )[0]\n\n    # Create sequence\n    sequence = session.create('Sequence', {\n        'name': '001',\n        'parent': project\n    })\n\n    # Create five shots with one task each\n    for shot_number in xrange(10, 60, 10):\n        shot = session.create('Shot', {\n            'name': '{0:03d}'.format(shot_number),\n            'parent': sequence,\n            'status': default_shot_status\n        })\n        session.create('Task', {\n            'name': 'Task name',\n            'parent': shot,\n            'status': default_task_status,\n            'type': default_task_type\n        })\n\n    # Commit all changes to the server.\n    session.commit()\n\nIf you test the example above, one thing you might notice is that the new API\nis much more efficient. Thanks to the transaction-based architecture in the new\nAPI only a single call to the server is required to create all the objects.\n\n.. seealso:: :ref:`working_with_entities/creating`\n\nUpdating objects\n================\n\nUpdating objects in the new API works in a similar way to the old API. Instead\nof using the :meth:`set` method on objects, you simply set the key of the \nentity to the new value, and call :meth:`Session.commit` to persist the\nchanges to the database.\n\nThe following example adjusts the duration and comment of a timelog for a\nuser using the old and new API, respectively.\n\nOld API::\n\n    import ftrack\n\n    user = ftrack.User('john')\n    user.set('email', 'john@example.com')\n\nNew API::\n\n    import ftrack_api\n    session = ftrack_api.Session()\n\n    user = session.query('User where username is \"john\"').one()\n    user['email'] = 'john@example.com'\n    session.commit()\n\n.. seealso:: :ref:`working_with_entities/updating`\n\n\nDate and datetime attributes\n============================\n\nIn the old API, date and datetime attributes where represented using a standard\n:mod:`datetime` object. In the new API we have opted to use the :term:`arrow` \nlibrary instead. Datetime attributes are represented in the server timezone,\nbut with the timezone information stripped.\n\nOld API::\n\n    >>> import datetime\n\n    >>> task_old_api = ftrack.Task(task_id)\n    >>> task_old_api.get('startdate')\n    datetime.datetime(2015, 9, 2, 0, 0)\n\n    >>> # Updating a datetime attribute\n    >>> task_old_api.set('startdate', datetime.date.today())\n\nNew API::\n\n    >>> import arrow\n\n    >>> task_new_api = session.get('Task', task_id)\n    >>> task_new_api['start_date']\n    <Arrow [2015-09-02T00:00:00+00:00]>\n\n    >>> # In the new API, utilize the arrow library when updating a datetime.\n    >>> task_new_api['start_date'] = arrow.utcnow().floor('day')\n    >>> session.commit()\n\nCustom attributes\n=================\n\nIn the old API, custom attributes could be retrieved from an entity by using\nthe methods :meth:`get` and :meth:`set`, like standard attributes. In the new \nAPI, custom attributes can be written and read from entities using the \n``custom_attributes`` property, which provides a dictionary-like interface.\n\nOld API::\n\n    >>> task_old_api = ftrack.Task(task_id)\n    >>> task_old_api.get('my_custom_attribute')\n\n    >>> task_old_api.set('my_custom_attribute', 'My new value')\n\n\nNew API::\n\n    >>> task_new_api = session.get('Task', task_id)\n    >>> task_new_api['custom_attributes']['my_custom_attribute']\n\n\n    >>> task_new_api['custom_attributes']['my_custom_attribute'] = 'My new value'\n\nFor more information on working with custom attributes and existing\nlimitations, please see:\n\n.. seealso::\n\n    :ref:`example/custom_attribute`\n\n\nUsing both APIs side-by-side\n============================\n\nWith so many powerful new features and the necessary support for more flexible\nworkflows, we chose early on to not limit the new API design by necessitating\nbackwards compatibility. However, we also didn't want to force teams using the\nexisting API to make a costly all-or-nothing switchover. As such, we have made\nthe new API capable of coexisting in the same process as the old API::\n\n    import ftrack\n    import ftrack_api\n\nIn addition, the old API will continue to be supported for some time, but do\nnote that it will not support the new `Workflows\n<https://www.ftrack.com/workflows>`_ and will not have new features back ported\nto it.\n\nIn the first example, we obtain a task reference using the old API and\nthen use the new API to assign a user to it::\n\n    import ftrack\n    import ftrack_api\n\n    # Create session for new API, authenticating using envvars.\n    session = ftrack_api.Session()\n\n    # Obtain task id using old API\n    shot = ftrack.getShot(['migration_test', '001', '010'])\n    task = shot.getTasks()[0]\n    task_id = task.getId()\n\n    user = session.query(\n        'User where username is \"{0}\"'.format(session.api_user)\n    ).one()\n    session.create('Appointment', {\n        'resource': user,\n        'context_id': task_id,\n        'type': 'assignment'\n    })\n\nThe second example fetches a version using the new API and uploads and sets a\nthumbnail using the old API::\n\n    import arrow\n    import ftrack\n\n    # fetch a version published today\n    version = session.query(\n        'AssetVersion where date >= \"{0}\"'.format(\n            arrow.now().floor('day')\n        )\n    ).first()\n\n    # Create a thumbnail using the old api.\n    thumbnail_path = '/path/to/thumbnail.jpg'\n    version_old_api = ftrack.AssetVersion(version['id'])\n    thumbnail = version_old_api.createThumbnail(thumbnail_path)\n\n    # Also set the same thumbnail on the task linked to the version.\n    task_old_api = ftrack.Task(version['task_id'])\n    task_old_api.setThumbnail(thumbnail)\n\n.. note::\n\n    It is now possible to set thumbnails using the new API as well, for more\n    info see :ref:`example/thumbnail`.\n\nPlugin registration\n-------------------\n\nTo make event and location plugin register functions work with both old and new\nAPI the function should be updated to validate the input arguments. For old\nplugins the register method should validate that the first input is of type\n``ftrack.Registry``, and for the new API it should be of type \n:class:`ftrack_api.session.Session`.\n\nIf the input parameter is not validated, a plugin might be mistakenly\nregistered twice, since both the new and old API will look for plugins the\nsame directories.\n\n.. seealso::\n\n    :ref:`ftrack:release/migration/3.0.29/developer_notes/register_function`\n\n\nExample: publishing a new version\n=================================\n\nIn the following example, we look at migrating a script which publishes a new\nversion with two components.\n\nOld API::\n\n    # Query a shot and a task to create the asset against.\n    shot = ftrack.getShot(['dev_tutorial', '001', '010'])\n    task = shot.getTasks()[0]\n\n    # Create new asset.\n    asset = shot.createAsset(name='forest', assetType='geo')\n\n    # Create a new version for the asset.\n    version = asset.createVersion(\n        comment='Added more leaves.',\n        taskid=task.getId()\n    )\n\n    # Get the calculated version number.\n    print version.getVersion()\n\n    # Add some components.\n    previewPath = '/path/to/forest_preview.mov'\n    previewComponent = version.createComponent(path=previewPath)\n\n    modelPath = '/path/to/forest_mode.ma'\n    modelComponent = version.createComponent(name='model', path=modelPath)\n\n    # Publish.\n    asset.publish()\n\n    # Add thumbnail to version.\n    thumbnail = version.createThumbnail('/path/to/forest_thumbnail.jpg')\n\n    # Set thumbnail on other objects without duplicating it.\n    task.setThumbnail(thumbnail)\n\nNew API::\n\n    # Query a shot and a task to create the asset against.\n    shot = session.query(\n        'Shot where project.name is \"dev_tutorial\" '\n        'and parent.name is \"001\" and name is \"010\"'\n    ).one()\n    task = shot['children'][0]\n\n    # Create new asset.\n    asset_type = session.query('AssetType where short is \"geo\"').first()\n    asset = session.create('Asset', {\n        'parent': shot,\n        'name': 'forest',\n        'type': asset_type\n    })\n\n    # Create a new version for the asset.\n    status = session.query('Status where name is \"Pending\"').one()\n    version = session.create('AssetVersion', {\n        'asset': asset,\n        'status': status,\n        'comment': 'Added more leaves.',\n        'task': task\n    })\n\n    # In the new API, the version number is not set until we persist the changes\n    print 'Version number before commit: {0}'.format(version['version'])\n    session.commit()\n    print 'Version number after commit: {0}'.format(version['version'])\n\n    # Add some components.\n    preview_path = '/path/to/forest_preview.mov'\n    preview_component = version.create_component(preview_path, location='auto')\n\n    model_path = '/path/to/forest_mode.ma'\n    model_component = version.create_component(model_path, {\n        'name': 'model'\n    }, location='auto')\n\n    # Publish. Newly created version defaults to being published in the new api,\n    # but if set to false you can update it by setting the key on the version.\n    version['is_published'] = True\n\n    # Persist the changes \n    session.commit()\n\n    # Add thumbnail to version.\n    thumbnail = version.create_thumbnail(\n        '/path/to/forest_thumbnail.jpg'\n    )\n\n    # Set thumbnail on other objects without duplicating it.\n    task['thumbnail'] = thumbnail\n    session.commit()\n\n\nWorkarounds for missing convenience methods\n===========================================\n\nQuery object by path\n--------------------\n\nIn the old API, there existed a convenience methods to get an object by \nreferencing the path (i.e object and parent names).\n\nOld API::\n\n    shot = ftrack.getShot(['dev_tutorial', '001', '010'])\n\nNew API::\n\n    shot = session.query(\n        'Shot where project.name is \"dev_tutorial\" '\n        'and parent.name is \"001\" and name is \"010\"'\n    )\n\n\nRetrieving an object's parents\n------------------------------\n\nTo retrieve a list of an object's parents, you could call the method\n:meth:`getParents` in the old API. Currently, it is not possible to fetch this\nin a single call using the new API, so you will have to traverse the ancestors \none-by-one and fetch each object's parent.\n\nOld API::\n\n    parents = task.getParents()\n\nNew API::\n\n    parents = []\n    for item in task['link'][:-1]:\n        parents.append(session.get(item['type'], item['id']))\n\nNote that link includes the task itself so `[:-1]` is used to only retreive the\nparents. To learn more about the `link` attribute, see\n:ref:`Using link attributes example<example/link_attribute>`.\n\nLimitations in the current version of the API\n=============================================\n\nThe new API is still quite young and in active development and there are a few\nlimitations currently to keep in mind when using it.\n\nMissing schemas\n---------------\n\nThe following entities are as of the time of writing not currently available\nin the new API. Let us know if you depend on any of them.\n\n    * Booking\n    * Calendar and Calendar Type\n    * Dependency\n    * Manager and Manager Type\n    * Phase\n    * Role\n    * Task template\n    * Temp data\n\nAction base class\n-----------------\nThere is currently no helper class for creating actions using the new API. We\nwill add one in the near future.\n\nIn the meantime, it is still possible to create actions without the base class\nby listening and responding to the \n:ref:`ftrack:developing/events/list/ftrack.action.discover` and \n:ref:`ftrack:developing/events/list/ftrack.action.launch` events.\n\nLegacy location\n---------------\n\nThe ftrack legacy disk locations utilizing the \n:class:`InternalResourceIdentifierTransformer` has been deprecated.\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/release/migration.rst",
    "content": "..\n    :copyright: Copyright (c) 2015 ftrack\n\n.. _release/migration:\n\n***************\nMigration notes\n***************\n\n.. note::\n\n    Migrating from the old ftrack API? Read the dedicated :ref:`guide\n    <release/migrating_from_old_api>`.\n\nMigrate to upcoming 2.0.0\n=========================\n\n.. _release/migration/2.0.0/event_hub:\n\nDefault behavior for connecting to event hub\n--------------------------------------------\n\nThe default behavior for the `ftrack_api.Session` class will change\nfor the argument `auto_connect_event_hub`, the default value will\nswitch from True to False. In order for code relying on the event hub\nto continue functioning as expected you must modify your code\nto explicitly set the argument to True or that you manually call\n`session.event_hub.connect()`.\n\n.. note::\n    If you rely on the `ftrack.location.component-added` or\n    `ftrack.location.component-removed` events to further process created\n    or deleted components remember that your session must be connected\n    to the event hub for the events to be published.\n\n\nMigrate to 1.0.3\n================\n\n.. _release/migration/1.0.3/mutating_dictionary:\n\nMutating custom attribute dictionary\n------------------------------------\n\nCustom attributes can no longer be set by mutating entire dictionary::\n\n    # This will result in an error.\n    task['custom_attributes'] = dict(foo='baz', bar=2)\n    session.commit()\n\nInstead the individual values should be changed::\n\n    # This works better.\n    task['custom_attributes']['foo'] = 'baz'\n    task['custom_attributes']['bar'] = 2\n    session.commit()\n\nMigrate to 1.0.0\n================\n\n.. _release/migration/1.0.0/chunked_transfer:\n\nChunked accessor transfers\n--------------------------\n\nData transfers between accessors is now buffered using smaller chunks instead of\nall data at the same time. Included accessor file representations such as\n:class:`ftrack_api.data.File` and :class:`ftrack_api.accessor.server.ServerFile`\nare built to handle that. If you have written your own accessor and file\nrepresentation you may have to update it to support multiple reads using the\nlimit parameter and multiple writes.\n\nMigrate to 0.2.0\n================\n\n.. _release/migration/0.2.0/new_api_name:\n\nNew API name\n------------\n\nIn this release the API has been renamed from `ftrack` to `ftrack_api`. This is\nto allow both the old and new API to co-exist in the same environment without\nconfusion.\n\nAs such, any scripts using this new API need to be updated to import\n`ftrack_api` instead of `ftrack`. For example:\n\n**Previously**::\n\n    import ftrack\n    import ftrack.formatter\n    ...\n\n**Now**::\n\n    import ftrack_api\n    import ftrack_api.formatter\n    ...\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/release/release_notes.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _release/release_notes:\n\n*************\nRelease Notes\n*************\n\n.. currentmodule:: ftrack_api.session\n\n.. release:: 1.8.2\n    :date: 2020-01-14\n\n    .. change:: fixed\n        :tag: Test\n\n        test_ensure_entity_with_non_string_data_types test fails due to missing parents.\n\n    .. change:: changed\n        :tags: session\n\n        Use WeakMethod when registering atexit handler to prevent memory leak.\n\n.. release:: 1.8.1\n    :date: 2019-10-30\n\n    .. change:: changed\n        :tags: Location\n\n        Increase chunk size for file operations to 1 Megabyte.\n        This value can now also be set from the environment variable:\n\n        :envvar:`FTRACK_API_FILE_CHUNK_SIZE`\n\n    .. change:: new\n        :tag: setup\n\n        Add check for correct python version when installing with pip.\n\n    .. change:: new\n        :tags: Notes\n\n        Add support for note labels in create_note helper method.\n\n    .. change:: changed\n        :tags: session\n\n        Ensure errors from server are fully reported with stack trace.\n\n.. release:: 1.8.0\n    :date: 2019-02-21\n\n    .. change:: fixed\n        :tags: documentation\n\n        Event description component-removed report component-added event signature.\n\n    .. change:: new\n        :tags: session, attribute\n\n        Add new scalar type `object` to factory.\n\n    .. change:: new\n        :tags: session, attribute\n\n        Add support for list of `computed` attributes as part of schema\n        definition. A computed attribute is derived on the server side, and can\n        be time dependentant and differ between users. As such a computed\n        attribute is not suitable for long term encoding and will not be encoded\n        with the `persisted_only` stragey.\n\n    .. change:: changed\n\n        The `delayed_job` method has been deprecated in favour of a direct\n        `Session.call`. See :ref:`example/sync_with_ldap` for example\n        usage.\n\n    .. change:: changed\n\n        Private method :meth:`Session._call` has been converted to\n        a public method, :meth:`Session.call`.\n\n        The private method will continue to work, but a pending deprecation\n        warning will be issued when used. The private method will be removed\n        entirely in version 2.0.\n\n    .. change:: changed\n        :tags: session, events\n\n        Event server connection error is too generic,\n        the actual error is now reported to users.\n\n.. release:: 1.7.1\n    :date: 2018-11-13\n\n    .. change:: fixed\n        :tags: session, events\n\n        Meta events for event hub connect and disconnect does not include\n        source.\n\n    .. change:: fixed\n        :tags: session, location\n\n        Missing context argument to\n        :meth:`ResourceIdentifierTransformer.decode`\n        in :meth:`Location.get_resource_identifier`.\n\n.. release:: 1.7.0\n    :date: 2018-07-27\n\n    .. change:: new\n        :tags: session, events\n\n        Added new events :ref:`event_list/ftrack.api.session.ready` and\n        :ref:`event_list/ftrack.api.session.reset` which can be used to perform\n        operations after the session is ready or has been reset, respectively.\n\n    .. change:: changed\n\n        Private method :meth:`Session._entity_reference` has been converted to\n        a public method, :meth:`Session.entity_reference`.\n\n        The private method will continue to work, but a pending deprecation\n        warning will be issued when used. The private method will be removed\n        entirely in version 2.0.\n\n    .. change:: fixed\n        :tags: session, events\n\n        :meth:`Session.close` raises an exception if event hub was explicitly\n        connected after session initialization.\n\n.. release:: 1.6.0\n    :date: 2018-05-17\n\n    .. change:: new\n        :tags: depreciation, events\n\n        In version 2.0.0 of the `ftrack-python-api` the default behavior for\n        the :class:`Session` class will change for the argument\n        *auto_connect_event_hub*, the default value will switch from *True* to\n        *False*.\n\n        A warning will now be emitted if async events are published or\n        subscribed to without *auto_connect_event_hub* has not explicitly been\n        set to *True*.\n\n        .. seealso:: :ref:`release/migration/2.0.0/event_hub`.\n\n    .. change:: fixed\n        :tags: documentation\n\n        Event payload not same as what is being emitted for\n        :ref:`event_list/ftrack.location.component-added` and\n        :ref:`event_list/ftrack.location.component-removed`.\n\n    .. change:: fixed\n        :tags: events\n\n        Pyparsing is causing random errors in a threaded environment.\n\n.. release:: 1.5.0\n    :date: 2018-04-19\n\n    .. change:: fixed\n       :tags: session, cache\n\n       Cached entities not updated correctly when fetched in a nested\n       query.\n\n.. release:: 1.4.0\n    :date: 2018-02-05\n\n    .. change:: fixed\n        :tags: session, cache\n\n        Collection attributes not merged correctly when fetched from\n        server.\n\n    .. change:: new\n        :tags: session, user, api key\n\n        New function :meth:`ftrack_api.session.Session.reset_remote` allows\n        resetting of attributes to their default value. A convenience method\n        for resetting a users api key utalizing this was also added\n        :meth:`ftrack_api.entity.user.User.reset_api_key`.\n\n        .. seealso:: :ref:`working_with_entities/resetting`\n\n    .. change:: new\n\n       Add support for sending out invitation emails to users.\n       See :ref:`example/invite_user` for example usage.\n\n    .. change:: changed\n        :tags: cache, performance\n\n        Entities fetched from cache are now lazily merged. Improved\n        performance when dealing with highly populated caches.\n\n.. release:: 1.3.3\n    :date: 2017-11-16\n\n\n    .. change:: new\n        :tags: users, ldap\n\n        Add support for triggering a synchronization of\n        users between ldap and ftrack. See :ref:`example/sync_with_ldap`\n        for example usage.\n\n        .. note::\n\n            This requires that you run ftrack 3.5.10 or later.\n\n    .. change:: fixed\n        :tags: metadata\n\n        Not possible to set metadata on creation.\n\n.. release:: 1.3.2\n    :date: 2017-09-18\n\n\n    .. change:: new\n        :tags: task template\n\n        Added example for managing task templates through the API. See\n        :ref:`example/task_template` for example usage.\n\n    .. change:: fixed\n        :tags: custom attributes\n\n         Not possible to set hierarchical custom attributes on an entity that\n         has not been committed.\n\n    .. change:: fixed\n        :tags: custom attributes\n\n         Not possible to set custom attributes on an `Asset` that has not been\n         committed.\n\n    .. change:: fixed\n        :tags: metadata\n\n        Not possible to set metadata on creation.\n\n.. release:: 1.3.1\n    :date: 2017-07-21\n\n    .. change:: fixed\n        :tags: session, events\n\n        Calling disconnect on the event hub is slow.\n\n.. release:: 1.3.0\n    :date: 2017-07-17\n\n    .. change:: new\n        :tags: session\n\n        Support using a :class:`Session` as a context manager to aid closing of\n        session after use::\n\n            with ftrack_api.Session() as session:\n                # Perform operations with session.\n\n    .. change:: new\n        :tags: session\n\n        :meth:`Session.close` automatically called on Python exit if session not\n        already closed.\n\n    .. change:: new\n        :tags: session\n\n        Added :meth:`Session.close` to properly close a session's connections to\n        the server(s) as well as ensure event listeners are properly\n        unsubscribed.\n\n    .. change:: new\n\n        Added :exc:`ftrack_api.exception.ConnectionClosedError` to represent\n        error caused when trying to access servers over closed connection.\n\n.. release:: 1.2.0\n    :date: 2017-06-16\n\n    .. change:: changed\n        :tags: events\n\n        Updated the websocket-client dependency to version >= 0.40.0 to allow\n        for http proxies.\n\n    .. change:: fixed\n        :tags: documentation\n\n        The :ref:`example/publishing` example incorrectly stated that a\n        location would be automatically picked if the *location* keyword\n        argument was omitted.\n\n.. release:: 1.1.1\n    :date: 2017-04-27\n\n    .. change:: fixed\n        :tags: custom attributes\n\n        Cannot use custom attributes for `Asset` in ftrack versions prior to\n        `3.5.0`.\n\n    .. change:: fixed\n        :tags: documentation\n\n        The :ref:`example <example/manage_custom_attribute_configuration>`\n        section for managing `text` custom attributes is not correct.\n\n.. release:: 1.1.0\n    :date: 2017-03-08\n\n    .. change:: new\n        :tags: server location, thumbnail\n\n        Added method :meth:`get_thumbnail_url() <ftrack_api.entity.location.ServerLocationMixin.get_thumbnail_url>`\n        to server location, which can be used to retrieve a thumbnail URL.\n        See :ref:`example/thumbnail/url` for example usage.\n\n    .. change:: new\n        :tags: documentation\n\n        Added :ref:`example <example/entity_links>` on how to manage entity\n        links from the API.\n\n    .. change:: new\n        :tags: documentation\n\n        Added :ref:`example <example/manage_custom_attribute_configuration>` on\n        how to manage custom attribute configurations from the API.\n\n    .. change:: new\n        :tags: documentation\n\n        Added :ref:`example <example/security_roles>` on how to use\n        `SecurityRole` and `UserSecurityRole` to manage security roles for\n        users.\n\n    .. change:: new\n        :tags: documentation\n\n        Added :ref:`examples <example/assignments_and_allocations>` to show how\n        to list a user's assigned tasks and all users assigned to a task.\n\n    .. change:: changed\n        :tags: session, plugins\n\n        Added *plugin_arguments* to :class:`Session` to allow passing of\n        optional keyword arguments to discovered plugin register functions. Only\n        arguments defined in a plugin register function signature are passed so\n        existing plugin register functions do not need updating if the new\n        functionality is not desired.\n\n    .. change:: fixed\n        :tags: documentation\n\n        The :ref:`example/project` example can be confusing since the project\n        schema may not contain the necessary object types.\n\n    .. change:: fixed\n        :tags: documentation\n\n        Query tutorial article gives misleading information about the ``has``\n        operator.\n\n    .. change:: fixed\n        :tags: session\n\n        Size is not set on sequence components when using\n        :meth:`Session.create_component`.\n\n.. release:: 1.0.4\n    :date: 2017-01-13\n\n    .. change:: fixed\n        :tags: custom attributes\n\n        Custom attribute values cannot be set on entities that are not\n        persisted.\n\n    .. change:: fixed\n        :tags: events\n\n        `username` in published event's source data is set to the operating\n        system user and not the API user.\n\n.. release:: 1.0.3\n    :date: 2017-01-04\n\n    .. change:: changed\n        :tags: session, custom attributes\n\n        Increased performance of custom attributes and better support for\n        filtering when using a version of ftrack that supports non-sparse\n        attribute values.\n\n    .. change:: changed\n        :tags: session, custom attributes\n\n        Custom attributes can no longer be set by mutating entire dictionary.\n\n        .. seealso:: :ref:`release/migration/1.0.3/mutating_dictionary`.\n\n.. release:: 1.0.2\n    :date: 2016-11-17\n\n    .. change:: changed\n        :tags: session\n\n        Removed version restriction for higher server versions.\n\n.. release:: 1.0.1\n    :date: 2016-11-11\n\n    .. change:: fixed\n\n        :meth:`EventHub.publish <ftrack_api.event.hub.EventHub.publish>`\n        *on_reply* callback only called for first received reply. It should be\n        called for all relevant replies received.\n\n.. release:: 1.0.0\n    :date: 2016-10-28\n\n    .. change:: new\n        :tags: session\n\n        :meth:`Session.get_upload_metadata` has been added.\n\n    .. change:: changed\n        :tags: locations, backwards-incompatible\n\n        Data transfer between locations using accessors is now chunked to avoid\n        reading large files into memory.\n\n        .. seealso:: :ref:`release/migration/1.0.0/chunked_transfer`.\n\n    .. change:: changed\n        :tags: server accessor\n\n        :class:`ftrack_api.accessor.server.ServerFile` has been refactored to\n        work with large files more efficiently.\n\n    .. change:: changed\n        :tags: server accessor\n\n        :class:`ftrack_api.accessor.server.ServerFile` has been updated to use\n        the get_upload_metadata API endpoint instead of\n        /component/getPutMetadata.\n\n    .. change:: changed\n        :tags: locations\n\n        :class:`ftrack_api.data.String` is now using a temporary file instead of\n        StringIO to avoid reading large files into memory.\n\n    .. change:: fixed\n        :tags: session, locations\n\n        `ftrack.centralized-storage` does not properly validate location\n        selection during user configuration.\n\n.. release:: 0.16.0\n    :date: 2016-10-18\n\n    .. change:: new\n        :tags: session, encode media\n\n        :meth:`Session.encode_media` can now automatically associate the output\n        with a version by specifying a *version_id* keyword argument. A new\n        helper method on versions, :meth:`AssetVersion.encode_media\n        <ftrack_api.entity.asset_version.AssetVersion.encode_media>`, can be\n        used to make versions playable in a browser. A server version of 3.3.32\n        or higher is required for it to function properly.\n\n        .. seealso:: :ref:`example/encode_media`.\n\n    .. change:: changed\n        :tags: session, encode media\n\n        You can now decide if :meth:`Session.encode_media` should keep or\n        delete the original component by specifying the *keep_original*\n        keyword argument.\n\n    .. change:: changed\n        :tags: backwards-incompatible, collection\n\n        Collection mutation now stores collection instance in operations rather\n        than underlying data structure.\n\n    .. change:: changed\n        :tags: performance\n\n        Improve performance of commit operations by optimising encoding and\n        reducing payload sent to server.\n\n    .. change:: fixed\n        :tags: documentation\n\n        Asset parent variable is declared but never used in\n        :ref:`example/publishing`.\n\n    .. change:: fixed\n        :tags: documentation\n\n        Documentation of hierarchical attributes and their limitations are\n        misleading. See :ref:`example/custom_attribute`.\n\n.. release:: 0.15.5\n    :date: 2016-08-12\n\n    .. change:: new\n        :tags: documentation\n\n        Added two new examples for :ref:`example/publishing` and\n        :ref:`example/web_review`.\n\n    .. change:: fixed\n        :tags: session, availability\n\n        :meth:`Session.get_component_availabilities` ignores passed locations\n        shortlist and includes all locations in returned availability mapping.\n\n    .. change:: fixed\n        :tags: documentation\n\n        Source distribution of ftrack-python-api does not include ftrack.css\n        in the documentation.\n\n.. release:: 0.15.4\n    :date: 2016-07-12\n\n    .. change:: fixed\n        :tags: querying\n\n        Custom offset not respected by\n        :meth:`QueryResult.first <ftrack_api.query.QueryResult.first>`.\n\n    .. change:: changed\n        :tags: querying\n\n        Using a custom offset with :meth:`QueryResult.one\n        <ftrack_api.query.QueryResult.one>` helper method now raises an\n        exception as an offset is inappropriate when expecting to select a\n        single item.\n\n    .. change:: fixed\n        :tags: caching\n\n        :meth:`LayeredCache.remove <ftrack_api.cache.LayeredCache.remove>`\n        incorrectly raises :exc:`~exceptions.KeyError` if key only exists in\n        sub-layer cache.\n\n.. release:: 0.15.3\n    :date: 2016-06-30\n\n    .. change:: fixed\n        :tags: session, caching\n\n        A newly created entity now has the correct\n        :attr:`ftrack_api.symbol.CREATED` state when checked in caching layer.\n        Previously the state was :attr:`ftrack_api.symbol.NOT_SET`. Note that\n        this fix causes a change in logic and the stored\n        :class:`ftrack_api.operation.CreateEntityOperation` might hold data that\n        has not been fully :meth:`merged <Session.merge>`.\n\n    .. change:: fixed\n        :tags: documentation\n\n        The second example in the assignments article is not working.\n\n    .. change:: changed\n        :tags: session, caching\n\n        A callable cache maker can now return ``None`` to indicate that it could\n        not create a suitable cache, but :class:`Session` instantiation can\n        continue safely.\n\n.. release:: 0.15.2\n    :date: 2016-06-02\n\n    .. change:: new\n        :tags: documentation\n\n        Added an example on how to work with assignments and allocations\n        :ref:`example/assignments_and_allocations`.\n\n    .. change:: new\n        :tags: documentation\n\n        Added :ref:`example/entity_links` article with\n        examples of how to manage asset version dependencies.\n\n    .. change:: fixed\n        :tags: performance\n\n        Improve performance of large collection management.\n\n    .. change:: fixed\n\n        Entities are not hashable because\n        :meth:`ftrack_api.entity.base.Entity.__hash__` raises `TypeError`.\n\n.. release:: 0.15.1\n    :date: 2016-05-02\n\n    .. change:: fixed\n        :tags: collection, attribute, performance\n\n        Custom attribute configurations does not cache necessary keys, leading\n        to performance issues.\n\n    .. change:: fixed\n        :tags: locations, structure\n\n        Standard structure does not work if version relation is not set on\n        the `Component`.\n\n.. release:: 0.15.0\n    :date: 2016-04-04\n\n    .. change:: new\n        :tags: session, locations\n\n        `ftrack.centralized-storage` not working properly on Windows.\n\n.. release:: 0.14.0\n    :date: 2016-03-14\n\n    .. change:: changed\n        :tags: session, locations\n\n        The `ftrack.centralized-storage` configurator now validates that name,\n        label and description for new locations are filled in.\n\n    .. change:: new\n        :tags: session, client review\n\n        Added :meth:`Session.send_review_session_invite` and\n        :meth:`Session.send_review_session_invites` that can be used to inform\n        review session invitees about a review session.\n\n        .. seealso:: :ref:`Usage guide <example/review_session>`.\n\n    .. change:: new\n        :tags: session, locations\n\n        Added `ftrack.centralized-storage` configurator as a private module. It\n        implements a wizard like interface used to configure a centralised\n        storage scenario.\n\n    .. change:: new\n        :tags: session, locations\n\n        `ftrack.centralized-storage` storage scenario is automatically\n        configured based on information passed from the server with the\n        `query_server_information` action.\n\n    .. change:: new\n        :tags: structure\n\n        Added :class:`ftrack_api.structure.standard.StandardStructure` with\n        hierarchy based resource identifier generation.\n\n    .. change:: new\n        :tags: documentation\n\n        Added more information to the :ref:`understanding_sessions/plugins`\n        article.\n\n    .. change:: fixed\n\n        :meth:`~ftrack_api.entity.user.User.start_timer` arguments *comment*\n        and *name* are ignored.\n\n    .. change:: fixed\n\n        :meth:`~ftrack_api.entity.user.User.stop_timer` calculates the wrong\n        duration when the server is not running in UTC.\n\n        For the duration to be calculated correctly ftrack server version\n        >= 3.3.15 is required.\n\n.. release:: 0.13.0\n    :date: 2016-02-10\n\n    .. change:: new\n        :tags: component, thumbnail\n\n        Added improved support for handling thumbnails.\n\n        .. seealso:: :ref:`example/thumbnail`.\n\n    .. change:: new\n        :tags: session, encode media\n\n        Added :meth:`Session.encode_media` that can be used to encode\n        media to make it playable in a browser.\n\n        .. seealso:: :ref:`example/encode_media`.\n\n    .. change:: fixed\n\n        :meth:`Session.commit` fails when setting a custom attribute on an asset\n        version that has been created and committed in the same session.\n\n    .. change:: new\n        :tags: locations\n\n        Added :meth:`ftrack_api.entity.location.Location.get_url` to retrieve a\n        URL to a component in a location if supported by the\n        :class:`ftrack_api.accessor.base.Accessor`.\n\n    .. change:: new\n        :tags: documentation\n\n        Updated :ref:`example/note` and :ref:`example/job` articles with\n        examples of how to use note and job components.\n\n    .. change:: changed\n        :tags: logging, performance\n\n        Logged messages now evaluated lazily using\n        :class:`ftrack_api.logging.LazyLogMessage` as optimisation.\n\n    .. change:: changed\n        :tags: session, events\n\n        Auto connection of event hub for :class:`Session` now takes place in\n        background to improve session startup time.\n\n    .. change:: changed\n        :tags: session, events\n\n        Event hub connection timeout is now 60 seconds instead of 10.\n\n    .. change:: changed\n        :tags: server version\n\n        ftrack server version >= 3.3.11, < 3.4 required.\n\n    .. change:: changed\n        :tags: querying, performance\n\n        :class:`ftrack_api.query.QueryResult` now pages internally using a\n        specified page size in order to optimise record retrieval for large\n        query results. :meth:`Session.query` has also been updated to allow\n        passing a custom page size at runtime if desired.\n\n    .. change:: changed\n        :tags: querying, performance\n\n        Increased performance of :meth:`~ftrack_api.query.QueryResult.first` and\n        :meth:`~ftrack_api.query.QueryResult.one` by using new `limit` syntax.\n\n.. release:: 0.12.0\n    :date: 2015-12-17\n\n    .. change:: new\n        :tags: session, widget url\n\n        Added :meth:`ftrack_api.session.Session.get_widget_url` to retrieve an\n        authenticated URL to info or tasks widgets.\n\n.. release:: 0.11.0\n    :date: 2015-12-04\n\n    .. change:: new\n        :tags: documentation\n\n        Updated :ref:`release/migrating_from_old_api` with new link attribute\n        and added a :ref:`usage example <example/link_attribute>`.\n\n    .. change:: new\n        :tags: caching, schemas, performance\n\n        Caching of schemas for increased performance.\n        :meth:`ftrack_api.session.Session` now accepts `schema_cache_path`\n        argument to specify location of schema cache. If not set it will use a\n        temporary folder.\n\n.. release:: 0.10.0\n    :date: 2015-11-24\n\n    .. change:: changed\n        :tags: tests\n\n        Updated session test to use mocked schemas for encoding tests.\n\n    .. change:: fixed\n\n        Documentation specifies Python 2.6 instead of Python 2.7 as minimum\n        interpreter version.\n\n    .. change:: fixed\n\n        Documentation does not reflect current dependencies.\n\n    .. change:: changed\n        :tags: session, component, locations, performance\n\n        Improved performance of\n        :meth:`ftrack_api.entity.location.Location.add_components` by batching\n        database operations.\n\n        As a result it is no longer possible to determine progress of transfer\n        for container components in realtime as events will be emitted in batch\n        at end of operation.\n\n        In addition, it is now the callers responsibility to clean up any\n        transferred data should an error occur during either data transfer or\n        database registration.\n\n    .. change:: changed\n        :tags: exception, locations\n\n        :exc:`ftrack_api.exception.ComponentInLocationError` now accepts either\n        a single component or multiple components and makes them available as\n        *components* in its *details* parameter.\n\n    .. change:: changed\n        :tags: tests\n\n        Updated session test to not fail on the new private link attribute.\n\n    .. change:: changed\n        :tags: session\n\n        Internal method :py:meth:`_fetch_schemas` has beed renamed to\n        :py:meth:`Session._load_schemas` and now requires a `schema_cache_path`\n        argument.\n\n.. release:: 0.9.0\n    :date: 2015-10-30\n\n    .. change:: new\n        :tags: caching\n\n        Added :meth:`ftrack_api.cache.Cache.values` as helper for retrieving\n        all values in cache.\n\n    .. change:: fixed\n        :tags: session, caching\n\n        :meth:`Session.merge` redundantly attempts to expand entity references\n        that have already been expanded causing performance degradation.\n\n    .. change:: new\n        :tags: session\n\n        :meth:`Session.rollback` has been added to support cleanly reverting\n        session state to last good state following a failed commit.\n\n    .. change:: changed\n        :tags: events\n\n        Event hub will no longer allow unverified SSL connections.\n\n        .. seealso:: :ref:`security_and_authentication`.\n\n    .. change:: changed\n        :tags: session\n\n        :meth:`Session.reset` no longer resets the connection. It also clears\n        all local state and re-configures certain aspects that are cache\n        dependant, such as location plugins.\n\n    .. change:: fixed\n        :tags: factory\n\n        Debug logging messages using incorrect index for formatting leading to\n        misleading exception.\n\n.. release:: 0.8.4\n    :date: 2015-10-08\n\n    .. change:: new\n\n        Added initial support for custom attributes.\n\n        .. seealso:: :ref:`example/custom_attribute`.\n\n    .. change:: new\n        :tags: collection, attribute\n\n        Added :class:`ftrack_api.collection.CustomAttributeCollectionProxy` and\n        :class:`ftrack_api.attribute.CustomAttributeCollectionAttribute` to\n        handle custom attributes.\n\n    .. change:: changed\n        :tags: collection, attribute\n\n        ``ftrack_api.attribute.MappedCollectionAttribute`` renamed to\n        :class:`ftrack_api.attribute.KeyValueMappedCollectionAttribute` to more\n        closely reflect purpose.\n\n    .. change:: changed\n        :tags: collection\n\n        :class:`ftrack_api.collection.MappedCollectionProxy` has been refactored\n        as a generic base class with key, value specialisation handled in new\n        dedicated class\n        :class:`ftrack_api.collection.KeyValueMappedCollectionProxy`. This is\n        done to avoid confusion following introduction of new\n        :class:`ftrack_api.collection.CustomAttributeCollectionProxy` class.\n\n    .. change:: fixed\n        :tags: events\n\n        The event hub does not always reconnect after computer has come back\n        from sleep.\n\n.. release:: 0.8.3\n    :date: 2015-09-28\n\n    .. change:: changed\n        :tags: server version\n\n        ftrack server version >= 3.2.1, < 3.4 required.\n\n    .. change:: changed\n\n        Updated *ftrack.server* location implementation. A server version of 3.3\n        or higher is required for it to function properly.\n\n    .. change:: fixed\n\n        :meth:`ftrack_api.entity.factory.StandardFactory.create` not respecting\n        *bases* argument.\n\n.. release:: 0.8.2\n    :date: 2015-09-16\n\n    .. change:: fixed\n        :tags: session\n\n        Wrong file type set on component when publishing image sequence using\n        :meth:`Session.create_component`.\n\n.. release:: 0.8.1\n    :date: 2015-09-08\n\n    .. change:: fixed\n        :tags: session\n\n        :meth:`Session.ensure` not implemented.\n\n.. release:: 0.8.0\n    :date: 2015-08-28\n\n    .. change:: changed\n        :tags: server version\n\n        ftrack server version >= 3.2.1, < 3.3 required.\n\n    .. change:: new\n\n        Added lists example.\n\n            .. seealso:: :ref:`example/list`.\n\n    .. change:: new\n\n        Added convenience methods for handling timers\n        :class:`~ftrack_api.entity.user.User.start_timer` and\n        :class:`~ftrack_api.entity.user.User.stop_timer`.\n\n    .. change:: changed\n\n        The dynamic API classes Type, Status, Priority and\n        StatusType have been renamed to Type, Status, Priority and State.\n\n    .. change:: changed\n\n        :meth:`Session.reset` now also clears the top most level cache (by\n        default a :class:`~ftrack_api.cache.MemoryCache`).\n\n    .. change:: fixed\n\n        Some invalid server url formats not detected.\n\n    .. change:: fixed\n\n        Reply events not encoded correctly causing them to be misinterpreted by\n        the server.\n\n.. release:: 0.7.0\n    :date: 2015-08-24\n\n    .. change:: changed\n        :tags: server version\n\n        ftrack server version >= 3.2, < 3.3 required.\n\n    .. change:: changed\n\n        Removed automatic set of default statusid, priorityid and typeid on\n        objects as that is now either not mandatory or handled on server.\n\n    .. change:: changed\n\n        Updated :meth:`~ftrack_api.entity.project_schema.ProjectSchema.get_statuses`\n        and :meth:`~ftrack_api.entity.project_schema.ProjectSchema.get_types` to\n        handle custom objects.\n\n.. release:: 0.6.0\n    :date: 2015-08-19\n\n    .. change:: changed\n        :tags: server version\n\n        ftrack server version >= 3.1.8, < 3.2 required.\n\n    .. change:: changed\n        :tags: querying, documentation\n\n        Updated documentation with details on new operators ``has`` and ``any``\n        for querying relationships.\n\n        .. seealso:: :ref:`querying/criteria/operators`\n\n.. release:: 0.5.2\n    :date: 2015-07-29\n\n    .. change:: changed\n        :tags: server version\n\n        ftrack server version 3.1.5 or greater required.\n\n    .. change:: changed\n\n        Server reported errors are now more readable and are no longer sometimes\n        presented as an HTML page.\n\n.. release:: 0.5.1\n    :date: 2015-07-06\n\n    .. change:: changed\n\n        Defaults computed by :class:`~ftrack_api.entity.factory.StandardFactory`\n        are now memoised per session to improve performance.\n\n    .. change:: changed\n\n        :class:`~ftrack_api.cache.Memoiser` now supports a *return_copies*\n        parameter to control whether deep copies should be returned when a value\n        was retrieved from the cache.\n\n.. release:: 0.5.0\n    :date: 2015-07-02\n\n    .. change:: changed\n\n        Now checks for server compatibility and requires an ftrack server\n        version of 3.1 or greater.\n\n    .. change:: new\n\n        Added convenience methods to :class:`~ftrack_api.query.QueryResult` to\n        fetch :meth:`~ftrack_api.query.QueryResult.first` or exactly\n        :meth:`~ftrack_api.query.QueryResult.one` result.\n\n    .. change:: new\n        :tags: notes\n\n        Added support for handling notes.\n\n        .. seealso:: :ref:`example/note`.\n\n    .. change:: changed\n\n        Collection attributes generate empty collection on first access when no\n        remote value available. This allows interacting with a collection on a\n        newly created entity before committing.\n\n    .. change:: fixed\n        :tags: session\n\n        Ambiguous error raised when :class:`Session` is started with an invalid\n        user or key.\n\n    .. change:: fixed\n        :tags: caching, session\n\n        :meth:`Session.merge` fails against\n        :class:`~ftrack_api.cache.SerialisedCache` when circular reference\n        encountered due to entity identity not being prioritised in merge.\n\n.. release:: 0.4.3\n    :date: 2015-06-29\n\n    .. change:: fixed\n        :tags: plugins, session, entity types\n\n        Entity types not constructed following standard install.\n\n        This is because the discovery of the default plugins is unreliable\n        across Python installation processes (pip, wheel etc). Instead, the\n        default plugins have been added as templates to the :ref:`event_list`\n        documentation and the\n        :class:`~ftrack_api.entity.factory.StandardFactory` used to create any\n        missing classes on :class:`Session` startup.\n\n.. release:: 0.4.2\n    :date: 2015-06-26\n\n    .. change:: fixed\n        :tags: metadata\n\n        Setting exact same metadata twice can cause\n        :exc:`~ftrack_api.exception.ImmutableAttributeError` to be incorrectly\n        raised.\n\n    .. change:: fixed\n        :tags: session\n\n        Calling :meth:`Session.commit` does not clear locally set attribute\n        values leading to immutability checks being bypassed in certain cases.\n\n.. release:: 0.4.1\n    :date: 2015-06-25\n\n    .. change:: fixed\n        :tags: metadata\n\n        Setting metadata twice in one session causes `KeyError`.\n\n.. release:: 0.4.0\n    :date: 2015-06-22\n\n    .. change:: changed\n        :tags: documentation\n\n        Documentation extensively updated.\n\n    .. change:: new\n        :tags: Client review\n\n        Added support for handling review sessions.\n\n        .. seealso:: :ref:`Usage guide <example/review_session>`.\n\n    .. change:: fixed\n\n        Metadata property not working in line with rest of system, particularly\n        the caching framework.\n\n    .. change:: new\n        :tags: collection\n\n        Added :class:`ftrack_api.collection.MappedCollectionProxy` class for\n        providing a dictionary interface to a standard\n        :class:`ftrack_api.collection.Collection`.\n\n    .. change:: new\n        :tags: collection, attribute\n\n        Added :class:`ftrack_api.attribute.MappedCollectionAttribute` class for\n        describing an attribute that should use the\n        :class:`ftrack_api.collection.MappedCollectionProxy`.\n\n    .. change:: new\n\n        Entities that use composite primary keys are now fully supported in the\n        session, including for :meth:`Session.get` and :meth:`Session.populate`.\n\n    .. change:: change\n\n        Base :class:`ftrack_api.entity.factory.Factory` refactored to separate\n        out attribute instantiation into dedicated methods to make extending\n        simpler.\n\n    .. change:: change\n        :tags: collection, attribute\n\n        :class:`ftrack_api.attribute.DictionaryAttribute` and\n        :class:`ftrack_api.attribute.DictionaryAttributeCollection` removed.\n        They have been replaced by the new\n        :class:`ftrack_api.attribute.MappedCollectionAttribute` and\n        :class:`ftrack_api.collection.MappedCollectionProxy` respectively.\n\n    .. change:: new\n        :tags: events\n\n        :class:`Session` now supports an *auto_connect_event_hub* argument to\n        control whether the built in event hub should connect to the server on\n        session initialisation. This is useful for when only local events should\n        be supported or when the connection should be manually controlled.\n\n.. release:: 0.3.0\n    :date: 2015-06-14\n\n    .. change:: fixed\n\n        Session operations may be applied server side in invalid order resulting\n        in unexpected error.\n\n    .. change:: fixed\n\n        Creating and deleting an entity in single commit causes error as create\n        operation never persisted to server.\n\n        Now all operations for the entity are ignored on commit when this case\n        is detected.\n\n    .. change:: changed\n\n        Internally moved from differential state to operation tracking for\n        determining session changes when persisting.\n\n    .. change:: new\n\n        ``Session.recorded_operations`` attribute for examining current\n        pending operations on a :class:`Session`.\n\n    .. change:: new\n\n        :meth:`Session.operation_recording` context manager for suspending\n        recording operations temporarily. Can also manually control\n        ``Session.record_operations`` boolean.\n\n    .. change:: new\n\n        Operation classes to track individual operations occurring in session.\n\n    .. change:: new\n\n        Public :meth:`Session.merge` method for merging arbitrary values into\n        the session manually.\n\n    .. change:: changed\n\n        An entity's state is now computed from the operations performed on it\n        and is no longer manually settable.\n\n    .. change:: changed\n\n        ``Entity.state`` attribute removed. Instead use the new inspection\n        :func:`ftrack_api.inspection.state`.\n\n        Previously::\n\n            print entity.state\n\n        Now::\n\n            import ftrack_api.inspection\n            print ftrack_api.inspection.state(entity)\n\n        There is also an optimised inspection,\n        :func:`ftrack_api.inspection.states`. for determining state of many\n        entities at once.\n\n    .. change:: changed\n\n        Shallow copying a :class:`ftrack_api.symbol.Symbol` instance now\n        returns same instance.\n\n.. release:: 0.2.0\n    :date: 2015-06-04\n\n    .. change:: changed\n\n        Changed name of API from `ftrack` to `ftrack_api`.\n\n        .. seealso:: :ref:`release/migration/0.2.0/new_api_name`.\n\n    .. change:: new\n        :tags: caching\n\n        Configurable caching support in :class:`Session`, including the ability\n        to use an external persisted cache and new cache implementations.\n\n        .. seealso:: :ref:`caching`.\n\n    .. change:: new\n        :tags: caching\n\n        :meth:`Session.get` now tries to retrieve matching entity from\n        configured cache first.\n\n    .. change:: new\n        :tags: serialisation, caching\n\n        :meth:`Session.encode` supports a new mode *persisted_only* that will\n        only encode persisted attribute values.\n\n    .. change:: changed\n\n        Session.merge method is now private (:meth:`Session._merge`) until it is\n        qualified for general usage.\n\n    .. change:: changed\n        :tags: entity state\n\n        :class:`~ftrack_api.entity.base.Entity` state now managed on the entity\n        directly rather than stored separately in the :class:`Session`.\n\n        Previously::\n\n            session.set_state(entity, state)\n            print session.get_state(entity)\n\n        Now::\n\n            entity.state = state\n            print entity.state\n\n    .. change:: changed\n        :tags: entity state\n\n        Entity states are now :class:`ftrack_api.symbol.Symbol` instances rather\n        than strings.\n\n        Previously::\n\n            entity.state = 'created'\n\n        Now::\n\n            entity.state = ftrack_api.symbol.CREATED\n\n    .. change:: fixed\n        :tags: entity state\n\n        It is now valid to transition from most entity states to an\n        :attr:`ftrack_api.symbol.NOT_SET` state.\n\n    .. change:: changed\n        :tags: caching\n\n        :class:`~ftrack_api.cache.EntityKeyMaker` removed and replaced by\n        :class:`~ftrack_api.cache.StringKeyMaker`. Entity identity now\n        computed separately and passed to key maker to allow key maker to work\n        with non entity instances.\n\n    .. change:: fixed\n        :tags: entity\n\n        Internal data keys ignored when re/constructing entities reducing\n        distracting and irrelevant warnings in logs.\n\n    .. change:: fixed\n        :tags: entity\n\n        :class:`~ftrack_api.entity.base.Entity` equality test raises error when\n        other is not an entity instance.\n\n    .. change:: changed\n        :tags: entity, caching\n\n        :meth:`~ftrack_api.entity.base.Entity.merge` now also merges state and\n        local attributes. In addition, it ensures values being merged have also\n        been merged into the session and outputs more log messages.\n\n    .. change:: fixed\n        :tags: inspection\n\n        :func:`ftrack_api.inspection.identity` returns different result for same\n        entity depending on whether entity type is unicode or string.\n\n    .. change:: fixed\n\n        :func:`ftrack_api.mixin` causes method resolution failure when same\n        class mixed in multiple times.\n\n    .. change:: changed\n\n        Representations of objects now show plain id rather than converting to\n        hex.\n\n    .. change:: fixed\n        :tags: events\n\n        Event hub raises TypeError when listening to ftrack.update events.\n\n    .. change:: fixed\n        :tags: events\n\n        :meth:`ftrack_api.event.hub.EventHub.subscribe` fails when subscription\n        argument contains special characters such as `@` or `+`.\n\n    .. change:: fixed\n        :tags: collection\n\n        :meth:`ftrack_api.collection.Collection` incorrectly modifies entity\n        state on initialisation.\n\n.. release:: 0.1.0\n    :date: 2015-03-25\n\n    .. change:: changed\n\n        Moved standardised construct entity type logic to core package (as part\n        of the :class:`~ftrack_api.entity.factory.StandardFactory`) for easier\n        reuse and extension.\n\n.. release:: 0.1.0-beta.2\n    :date: 2015-03-17\n\n    .. change:: new\n        :tags: locations\n\n        Support for ftrack.server location. The corresponding server build is\n        required for it to function properly.\n\n    .. change:: new\n        :tags: locations\n\n        Support for managing components in locations has been added. Check out\n        the :ref:`dedicated tutorial <locations/tutorial>`.\n\n    .. change:: new\n\n        A new inspection API (:mod:`ftrack_api.inspection`) has been added for\n        extracting useful information from objects in the system, such as the\n        identity of an entity.\n\n    .. change:: changed\n\n        ``Entity.primary_key`` and ``Entity.identity`` have been removed.\n        Instead, use the new :func:`ftrack_api.inspection.primary_key` and\n        :func:`ftrack_api.inspection.identity` functions. This was done to make it\n        clearer the the extracted information is determined from the current\n        entity state and modifying the returned object will have no effect on\n        the entity instance itself.\n\n    .. change:: changed\n\n        :func:`ftrack_api.inspection.primary_key` now returns a mapping of the\n        attribute names and values that make up the primary key, rather than\n        the previous behaviour of returning a tuple of just the values. To\n        emulate previous behaviour do::\n\n            ftrack_api.inspection.primary_key(entity).values()\n\n    .. change:: changed\n\n        :meth:`Session.encode` now supports different strategies for encoding\n        entities via the entity_attribute_strategy* keyword argument. This makes\n        it possible to use this method for general serialisation of entity\n        instances.\n\n    .. change:: changed\n\n        Encoded referenced entities are now a mapping containing\n        *__entity_type__* and then each key, value pair that makes up the\n        entity's primary key. For example::\n\n            {\n                '__entity_type__': 'User',\n                'id': '8b90a444-4e65-11e1-a500-f23c91df25eb'\n            }\n\n    .. change:: changed\n\n        :meth:`Session.decode` no longer automatically adds decoded entities to\n        the :class:`Session` cache making it possible to use decode\n        independently.\n\n    .. change:: new\n\n        Added :meth:`Session.merge` for merging entities recursively into the\n        session cache.\n\n    .. change:: fixed\n\n        Replacing an entity in a :class:`ftrack_api.collection.Collection` with an\n        identical entity no longer raises\n        :exc:`ftrack_api.exception.DuplicateItemInCollectionError`.\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/resource/example_plugin.py",
    "content": "# :coding: utf-8\nimport logging\n\nimport ftrack_api.session\n\n\ndef register(session, **kw):\n    '''Register plugin. Called when used as an plugin.'''\n    logger = logging.getLogger('com.example.example-plugin')\n\n    # Validate that session is an instance of ftrack_api.Session. If not,\n    # assume that register is being called from an old or incompatible API and\n    # return without doing anything.\n    if not isinstance(session, ftrack_api.session.Session):\n        logger.debug(\n            'Not subscribing plugin as passed argument {0!r} is not an '\n            'ftrack_api.Session instance.'.format(session)\n        )\n        return\n\n    # Perform your logic here, such as subscribe to an event.\n    pass\n\n    logger.debug('Plugin registered')\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/resource/example_plugin_safe.py",
    "content": ""
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/resource/example_plugin_using_session.py",
    "content": "# :coding: utf-8\nimport logging\n\nimport ftrack_api.session\n\n\ndef register_with_session_ready(event):\n    '''Called when session is ready to be used.'''\n    logger = logging.getLogger('com.example.example-plugin')\n    logger.debug('Session ready.')\n    session = event['data']['session']\n\n    # Session is now ready and can be used to e.g. query objects.\n    task = session.query('Task').first()\n    print task['name']\n\n\ndef register(session, **kw):\n    '''Register plugin. Called when used as an plugin.'''\n    logger = logging.getLogger('com.example.example-plugin')\n\n    # Validate that session is an instance of ftrack_api.Session. If not,\n    # assume that register is being called from an old or incompatible API and\n    # return without doing anything.\n    if not isinstance(session, ftrack_api.session.Session):\n        logger.debug(\n            'Not subscribing plugin as passed argument {0!r} is not an '\n            'ftrack_api.Session instance.'.format(session)\n        )\n        return\n\n    session.event_hub.subscribe(\n        'topic=ftrack.api.session.ready',\n        register_with_session_ready\n    )\n\n    logger.debug('Plugin registered')\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/security_and_authentication.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _security_and_authentication:\n\n***************************\nSecurity and authentication\n***************************\n\nSelf signed SSL certificate\n===========================\n\nWhen using a self signed SSL certificate the API may fail to connect if it\ncannot verify the SSL certificate. Under the hood the\n`requests <http://docs.python-requests.org/en/latest/>`_ library is used and it\nmust be specified where the trusted certificate authority can be found using the\nenvironment variable ``REQUESTS_CA_BUNDLE``.\n\n.. seealso:: `SSL Cert Verification <http://docs.python-requests.org/en/latest/user/advanced/?highlight=requests_ca_bundle#ssl-cert-verification>`_\n\nInsecurePlatformWarning\n=======================\n\nWhen using this API you may sometimes see a warning::\n\n    InsecurePlatformWarning: A true SSLContext object is not available. This\n    prevents urllib3 from configuring SSL appropriately and may cause certain\n    SSL connections to fail.\n\nIf you encounter this warning, its recommended you upgrade to Python 2.7.9, or\nuse pyOpenSSL. To use pyOpenSSL simply::\n\n    pip install pyopenssl ndg-httpsclient pyasn1\n\nand the `requests <http://docs.python-requests.org/en/latest/>`_ library used by\nthis API will use pyOpenSSL instead.\n\n.. seealso:: `InsecurePlatformWarning <http://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning>`_\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/tutorial.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _tutorial:\n\n********\nTutorial\n********\n\n.. currentmodule:: ftrack_api.session\n\nThis tutorial provides a quick dive into using the API and the broad stroke\nconcepts involved.\n\nFirst make sure the ftrack Python API is :ref:`installed <installing>`.\n\nThen start a Python session and import the ftrack API::\n\n    >>> import ftrack_api\n\nThe API uses :ref:`sessions <understanding_sessions>` to manage communication\nwith an ftrack server. Create a session that connects to your ftrack server\n(changing the passed values as appropriate)::\n\n    >>> session = ftrack_api.Session(\n    ...     server_url='https://mycompany.ftrackapp.com',\n    ...     api_key='7545384e-a653-11e1-a82c-f22c11dd25eq',\n    ...     api_user='martin'\n    ... )\n\n.. note::\n\n    A session can use :ref:`environment variables\n    <understanding_sessions/connection>` to configure itself.\n\nNow print a list of the available entity types retrieved from the server::\n\n    >>> print session.types.keys()\n    [u'TypedContext', u'ObjectType', u'Priority', u'Project', u'Sequence',\n     u'Shot', u'Task', u'Status', u'Type', u'Timelog', u'User']\n\nNow the list of possible entity types is known, :ref:`query <querying>` the\nserver to retrieve entities of a particular type by using the\n:meth:`Session.query` method::\n\n    >>> projects = session.query('Project')\n\nEach project retrieved will be an :ref:`entity <working_with_entities>` instance\nthat behaves much like a standard Python dictionary. For example, to find out\nthe available keys for an entity, call the\n:meth:`~ftrack_api.entity.Entity.keys` method::\n\n    >>> print projects[0].keys()\n    [u'status', u'is_global', u'name', u'end_date', u'context_type',\n     u'id', u'full_name', u'root', u'start_date']\n\nNow, iterate over the retrieved entities and print each ones name::\n\n    >>> for project in projects:\n    ...     print project['name']\n    test\n    client_review\n    tdb\n    man_test\n    ftrack\n    bunny\n\n.. note::\n\n    Many attributes for retrieved entities are loaded on demand when the\n    attribute is first accessed. Doing this lots of times in a script can be\n    inefficient, so it is worth using :ref:`projections <querying/projections>`\n    in queries or :ref:`pre-populating <working_with_entities/populating>`\n    entities where appropriate. You can also :ref:`customise default projections\n    <working_with_entities/entity_types/default_projections>` to help others\n    pre-load common attributes.\n\nTo narrow a search, add :ref:`criteria <querying/criteria>` to the query::\n\n    >>> active_projects = session.query('Project where status is active')\n\nCombine criteria for more powerful queries::\n\n    >>> import arrow\n    >>>\n    >>> active_projects_ending_before_next_week = session.query(\n    ...     'Project where status is active and end_date before \"{0}\"'\n    ...     .format(arrow.now().replace(weeks=+1))\n    ... )\n\nSome attributes on an entity will refer to another entity or collection of\nentities, such as *children* on a *Project* being a collection of *Context*\nentities that have the project as their parent::\n\n    >>> project = session.query('Project').first()\n    >>> print project['children']\n    <ftrack_api.collection.Collection object at 0x00000000045B1438>\n\nAnd on each *Context* there is a corresponding *parent* attribute which is a\nlink back to the parent::\n\n    >>> child = project['children'][0]\n    >>> print child['parent'] is project\n    True\n\nThese relationships can also be used in the criteria for a query::\n\n    >>> results = session.query(\n    ...     'Context where parent.name like \"te%\"'\n    ... )\n\nTo create new entities in the system use :meth:`Session.create`::\n\n    >>> new_sequence = session.create('Sequence', {\n    ...     'name': 'Starlord Reveal'\n    ... })\n\nThe created entity is not yet persisted to the server, but it is still possible\nto modify it.\n\n    >>> new_sequence['description'] = 'First hero character reveal.'\n\nThe sequence also needs a parent. This can be done in one of two ways:\n\n* Set the parent attribute on the sequence::\n\n    >>> new_sequence['parent'] = project\n\n* Add the sequence to a parent's children attribute::\n\n    >>> project['children'].append(new_sequence)\n\nWhen ready, persist to the server using :meth:`Session.commit`::\n\n    >>> session.commit()\n\nWhen finished with a :class:`Session`, it is important to :meth:`~Session.close`\nit in order to release resources and properly unsubscribe any registered event\nlisteners. It is also possible to use the session as a context manager in order\nto have it closed automatically after use::\n\n    >>> with ftrack_api.Session() as session:\n    ...     print session.query('User').first()\n    <User(0154901c-eaf9-11e5-b165-00505681ec7a)>\n    >>> print session.closed\n    True\n\nOnce a :class:`Session` is closed, any operations that attempt to use the closed\nconnection to the ftrack server will fail::\n\n    >>> session.query('Project').first()\n    ConnectionClosedError: Connection closed.\n\nContinue to the next section to start learning more about the API in greater\ndepth or jump over to the :ref:`usage examples <example>` if you prefer to learn\nby example.\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/understanding_sessions.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _understanding_sessions:\n\n**********************\nUnderstanding sessions\n**********************\n\n.. currentmodule:: ftrack_api.session\n\nAll communication with an ftrack server takes place through a :class:`Session`.\nThis allows more opportunity for configuring the connection, plugins etc. and\nalso makes it possible to connect to multiple ftrack servers from within the\nsame Python process.\n\n.. _understanding_sessions/connection:\n\nConnection\n==========\n\nA session can be manually configured at runtime to connect to a server with\ncertain credentials::\n\n    >>> session = ftrack_api.Session(\n    ...     server_url='https://mycompany.ftrackapp.com',\n    ...     api_key='7545384e-a653-11e1-a82c-f22c11dd25eq',\n    ...     api_user='martin'\n    ... )\n\nAlternatively, a session can use the following environment variables to\nconfigure itself:\n\n    * :envvar:`FTRACK_SERVER`\n    * :envvar:`FTRACK_API_USER`\n    * :envvar:`FTRACK_API_KEY`\n\nWhen using environment variables, no server connection arguments need to be\npassed manually::\n\n    >>> session = ftrack_api.Session()\n\n.. _understanding_sessions/unit_of_work:\n\nUnit of work\n============\n\nEach session follows the unit of work pattern. This means that many of the\noperations performed using a session will happen locally and only be persisted\nto the server at certain times, notably when calling :meth:`Session.commit`.\nThis approach helps optimise calls to the server and also group related logic\ntogether in a transaction::\n\n    user = session.create('User', {})\n    user['username'] = 'martin'\n    other_user = session.create('User', {'username': 'bjorn'})\n    other_user['email'] = 'bjorn@example.com'\n\nBehind the scenes a series of :class:`operations\n<ftrack_api.operation.Operation>` are recorded reflecting the changes made. You\ncan take a peek at these operations if desired by examining the\n``Session.recorded_operations`` property::\n\n    >>> for operation in session.recorded_operations:\n    ...     print operation\n    <ftrack_api.operation.CreateEntityOperation object at 0x0000000003EC49B0>\n    <ftrack_api.operation.UpdateEntityOperation object at 0x0000000003E16898>\n    <ftrack_api.operation.CreateEntityOperation object at 0x0000000003E16240>\n    <ftrack_api.operation.UpdateEntityOperation object at 0x0000000003E16128>\n\nCalling :meth:`Session.commit` persists all recorded operations to the server\nand clears the operation log::\n\n    session.commit()\n\n.. note::\n\n    The commit call will optimise operations to be as efficient as possible\n    without breaking logical ordering. For example, a create followed by updates\n    on the same entity will be compressed into a single create.\n\nQueries are special and always issued on demand. As a result, a query may return\nunexpected results if the relevant local changes have not yet been sent to the\nserver::\n\n    >>> user = session.create('User', {'username': 'some_unique_username'})\n    >>> query = 'User where username is \"{0}\"'.format(user['username'])\n    >>> print len(session.query(query))\n    0\n    >>> session.commit()\n    >>> print len(session.query(query))\n    1\n\nWhere possible, query results are merged in with existing data transparently\nwith any local changes preserved::\n\n    >>> user = session.query('User').first()\n    >>> user['email'] = 'me@example.com'  # Not yet committed to server.\n    >>> retrieved = session.query(\n    ...     'User where id is \"{0}\"'.format(user['id'])\n    ... ).one()\n    >>> print retrieved['email']  # Displays locally set value.\n    'me@example.com'\n    >>> print retrieved is user\n    True\n\nThis is possible due to the smart :ref:`caching` layer in the session.\n\n.. _understanding_sessions/auto_population:\n\nAuto-population\n===============\n\nAnother important concept in a session is that of auto-population. By default a\nsession is configured to auto-populate missing attribute values on access. This\nmeans that the first time you access an attribute on an entity instance a query\nwill be sent to the server to fetch the value::\n\n    user = session.query('User').first()\n    # The next command will issue a request to the server to fetch the\n    # 'username' value on demand at this is the first time it is accessed.\n    print user['username']\n\nOnce a value has been retrieved it is :ref:`cached <caching>` locally in the\nsession and accessing it again will not issue more server calls::\n\n    # On second access no server call is made.\n    print user['username']\n\nYou can control the auto population behaviour of a session by either changing\nthe ``Session.auto_populate`` attribute on a session or using the provided\ncontext helper :meth:`Session.auto_populating` to temporarily change the\nsetting. When turned off you may see a special\n:attr:`~ftrack_api.symbol.NOT_SET` symbol that represents a value has not yet\nbeen fetched::\n\n    >>> with session.auto_populating(False):\n    ...     print user['email']\n    NOT_SET\n\nWhilst convenient for simple scripts, making many requests to the server for\neach attribute can slow execution of a script. To support optimisation the API\nincludes methods for batch fetching attributes. Read about them in\n:ref:`querying/projections` and :ref:`working_with_entities/populating`.\n\n.. _understanding_sessions/entity_types:\n\nEntity types\n============\n\nWhen a session has successfully connected to the server it will automatically\ndownload schema information and :ref:`create appropriate classes\n<working_with_entities/entity_types>` for use. This is important as different\nservers can support different entity types and configurations.\n\nThis information is readily available and useful if you need to check that the\nentity types you expect are present. Here's how to print a list of all entity\ntypes registered for use in the current API session::\n\n    >>> print session.types.keys()\n    [u'Task', u'Shot', u'TypedContext', u'Sequence', u'Priority',\n     u'Status', u'Project', u'User', u'Type', u'ObjectType']\n\nEach entity type is backed by a :ref:`customisable class\n<working_with_entities/entity_types>` that further describes the entity type and\nthe attributes that are available.\n\n.. hint::\n\n    If you need to use an :func:`isinstance` check, always go through the\n    session as the classes are built dynamically::\n\n        >>> isinstance(entity, session.types['Project'])\n\n.. _understanding_sessions/plugins:\n\nConfiguring plugins\n===================\n\nPlugins are used by the API to extend it with new functionality, such as \n:term:`locations <location>` or adding convenience methods to\n:ref:`understanding_sessions/entity_types`. In addition to new API\nfunctionality, event plugins may also be used for event processing by listening\nto :ref:`ftrack update events <handling_events>` or adding custom functionality to ftrack by registering\n:term:`actions <action>`.\n\n\nWhen starting a new :class:`Session` either pass the *plugins_paths* to search\nexplicitly or rely on the environment variable \n:envvar:`FTRACK_EVENT_PLUGIN_PATH`. As each session is independent of others,\nyou can configure plugins per session.\n\nThe paths will be searched for :term:`plugins <plugin>`, python files\nwhich expose a `register` function. These functions will be evaluated and can\nbe used extend the API with new functionality, such as locations or actions.\n\nIf you do not specify any override then the session will attempt to discover and\nuse the default plugins.\n\nPlugins are discovered using :func:`ftrack_api.plugin.discover` with the\nsession instance passed as the sole positional argument. Most plugins should\ntake the form of a mount function that then subscribes to specific :ref:`events\n<handling_events>` on the session::\n\n    def configure_locations(event):\n        '''Configure locations for session.'''\n        session = event['data']['session']\n        # Find location(s) and customise instances.\n\n    def register(session):\n        '''Register plugin with *session*.'''\n        session.event_hub.subscribe(\n            'topic=ftrack.api.session.configure-location',\n            configure_locations\n        )\n\nAdditional keyword arguments can be passed as *plugin_arguments* to the\n:class:`Session` on instantiation. These are passed to the plugin register\nfunction if its signature supports them::\n\n    # a_plugin.py\n    def register(session, reticulate_splines=False):\n        '''Register plugin with *session*.'''\n        ...\n\n    # main.py\n    session = ftrack_api.Session(\n        plugin_arguments={\n            'reticulate_splines': True,\n            'some_other_argument': 42\n        }\n    )\n\n.. seealso::\n\n    Lists of events which you can subscribe to in your plugins are available\n    both for :ref:`synchronous event published by the python API <event_list>`\n    and :ref:`asynchronous events published by the server <ftrack:developing/events/list>`\n\n\nQuick setup\n-----------\n\n1. Create a directory where plugins will be stored. Place any plugins you want\nloaded automatically in an API *session* here.\n\n.. image:: /image/configuring_plugins_directory.png\n\n2. Configure the :envvar:`FTRACK_EVENT_PLUGIN_PATH` to point to the directory.\n\n\nDetailed setup\n--------------\n\nStart out by creating a directory on your machine where you will store your\nplugins. Download :download:`example_plugin.py </resource/example_plugin.py>`\nand place it in the directory.\n\nOpen up a terminal window, and ensure that plugin is picked up when\ninstantiating the session and manually setting the *plugin_paths*::\n\n    >>>  # Set up basic logging\n    >>> import logging\n    >>> logging.basicConfig()\n    >>> plugin_logger = logging.getLogger('com.example.example-plugin')\n    >>> plugin_logger.setLevel(logging.DEBUG)\n    >>>\n    >>> # Configure the API, loading plugins in the specified paths.\n    >>> import ftrack_api\n    >>> plugin_paths = ['/path/to/plugins']\n    >>> session = ftrack_api.Session(plugin_paths=plugin_paths)\n\nIf everything is working as expected, you should see the following in the\noutput::\n\n    DEBUG:com.example.example-plugin:Plugin registered\n\nInstead of specifying the plugin paths when instantiating the session, you can\nalso specify the :envvar:`FTRACK_EVENT_PLUGIN_PATH` to point to the directory.\nTo specify multiple directories, use the path separator for your operating\nsystem."
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/doc/working_with_entities.rst",
    "content": "..\n    :copyright: Copyright (c) 2014 ftrack\n\n.. _working_with_entities:\n\n*********************\nWorking with entities\n*********************\n\n.. currentmodule:: ftrack_api.session\n\n:class:`Entity <ftrack_api.entity.base.Entity>` instances are Python dict-like\nobjects whose keys correspond to attributes for that type in the system. They\nmay also provide helper methods to perform common operations such as replying to\na note::\n\n    note = session.query('Note').first()\n    print note.keys()\n    print note['content']\n    note['content'] = 'A different message!'\n    reply = note.create_reply(...)\n\n.. _working_with_entities/attributes:\n\nAttributes\n==========\n\nEach entity instance is typed according to its underlying entity type on the\nserver and configured with appropriate attributes. For example, a *task* will be\nrepresented by a *Task* class and have corresponding attributes. You can\n:ref:`customise entity classes <working_with_entities/entity_types>` to alter\nattribute access or provide your own helper methods.\n\nTo see the available attribute names on an entity use the\n:meth:`~ftrack_api.entity.base.Entity.keys` method on the instance::\n\n    >>> task = session.query('Task').first()\n    >>> print task.keys()\n    ['id', 'name', ...]\n\nIf you need more information about the type of attribute, examine the\n``attributes`` property on the corresponding class::\n\n    >>> for attribute in type(task).attributes:\n    ...     print attribute\n    <ftrack_api.attribute.ScalarAttribute(id) object at 66701296>\n    <ftrack_api.attribute.ScalarAttribute(name) object at 66702192>\n    <ftrack_api.attribute.ReferenceAttribute(status) object at 66701240>\n    <ftrack_api.attribute.CollectionAttribute(timelogs) object at 66701184>\n    <ftrack_api.attribute.KeyValueMappedCollectionAttribute(metadata) object at 66701632>\n    ...\n\nNotice that there are different types of attribute such as\n:class:`~ftrack_api.attribute.ScalarAttribute` for plain values or\n:class:`~ftrack_api.attribute.ReferenceAttribute` for relationships. These\ndifferent types are reflected in the behaviour on the entity instance when\naccessing a particular attribute by key:\n\n    >>> # Scalar\n    >>> print task['name']\n    'model'\n    >>> task['name'] = 'comp'\n\n    >>> # Single reference\n    >>> print task['status']\n    <Status(e610b180-4e64-11e1-a500-f23c91df25eb)>\n    >>> new_status = session.query('Status').first()\n    >>> task['status'] = new_status\n\n    >>> # Collection\n    >>> print task['timelogs']\n    <ftrack_api.collection.Collection object at 0x00000000040D95C0>\n    >>> print task['timelogs'][:]\n    [<dynamic ftrack Timelog object 72322240>, ...]\n    >>> new_timelog = session.create('Timelog', {...})\n    >>> task['timelogs'].append(new_timelog)\n\n.. _working_with_entities/attributes/bidirectional:\n\nBi-directional relationships\n----------------------------\n\nSome attributes refer to different sides of a bi-directional relationship. In\nthe current version of the API bi-directional updates are not propagated\nautomatically to the other side of the relationship. For example, setting a\n*parent* will not update the parent entity's *children* collection locally.\nThere are plans to support this behaviour better in the future. For now, after\ncommit, :ref:`populate <working_with_entities/populating>` the reverse side\nattribute manually.\n\n.. _working_with_entities/creating:\n\nCreating entities\n=================\n\nIn order to create a new instance of an entity call :meth:`Session.create`\npassing in the entity type to create and any initial attribute values::\n\n    new_user = session.create('User', {'username': 'martin'})\n\nIf there are any default values that can be set client side then they will be\napplied at this point. Typically this will be the unique entity key::\n\n    >>> print new_user['id']\n    170f02a4-6656-4f15-a5cb-c4dd77ce0540\n\nAt this point no information has been sent to the server. However, you are free\nto continue :ref:`updating <working_with_entities/updating>` this object\nlocally until you are ready to persist the changes by calling\n:meth:`Session.commit`.\n\nIf you are wondering about what would happen if you accessed an unset attribute\non a newly created entity, go ahead and give it a go::\n\n    >>> print new_user['first_name']\n    NOT_SET\n\nThe session knows that it is a newly created entity that has not yet been\npersisted so it doesn't try to fetch any attributes on access even when\n``session.auto_populate`` is turned on.\n\n.. _working_with_entities/updating:\n\nUpdating entities\n=================\n\nUpdating an entity is as simple as modifying the values for specific keys on\nthe dict-like instance and calling :meth:`Session.commit` when ready. The entity\nto update can either be a new entity or a retrieved entity::\n\n    task = session.query('Task').first()\n    task['bid'] = 8\n\nRemember that, for existing entities, accessing an attribute will load it from\nthe server automatically. If you are interested in just setting values without\nfirst fetching them from the server, turn :ref:`auto-population\n<understanding_sessions/auto_population>` off temporarily::\n\n    >>> with session.auto_populating(False):\n    ...    task = session.query('Task').first()\n    ...    task['bid'] = 8\n\n\n.. _working_with_entities/resetting:\n\nServer side reset of entity attributes or settings.\n===========================\n\nSome entities support resetting of attributes, for example\nto reset a users api key::\n\n\n    session.reset_remote(\n        'api_key', entity=session.query('User where username is \"test_user\"').one()\n    )\n\n.. note::\n    Currently the only attribute possible to reset is 'api_key' on\n    the user entity type.\n\n\n.. _working_with_entities/deleting:\n\nDeleting entities\n=================\n\nTo delete an entity you need an instance of the entity in your session (either\nfrom having created one or retrieving one). Then call :meth:`Session.delete` on\nthe entity and :meth:`Session.commit` when ready::\n\n    task_to_delete = session.query('Task').first()\n    session.delete(task_to_delete)\n    ...\n    session.commit()\n\n.. note::\n\n    Even though the entity is deleted, you will still have access to the local\n    instance and any local data stored on that instance whilst that instance\n    remains in memory.\n\nKeep in mind that some deletions, when propagated to the server, will cause\nother entities to be deleted also, so you don't have to worry about deleting an\nentire hierarchy manually. For example, deleting a *Task* will also delete all\n*Notes* on that task.\n\n.. _working_with_entities/populating:\n\nPopulating entities\n===================\n\nWhen an entity is retrieved via :meth:`Session.query` or :meth:`Session.get` it\nwill have some attributes prepopulated. The rest are dynamically loaded when\nthey are accessed. If you need to access many attributes it can be more\nefficient to request all those attributes be loaded in one go. One way to do\nthis is to use a :ref:`projections <querying/projections>` in queries.\n\nHowever, if you have entities that have been passed to you from elsewhere you\ndon't have control over the query that was issued to get those entities. In this\ncase you can you can populate those entities in one go using\n:meth:`Session.populate` which works exactly like :ref:`projections\n<querying/projections>` in queries do, but operating against known entities::\n\n    >>> users = session.query('User')\n    >>> session.populate(users, 'first_name, last_name')\n    >>> with session.auto_populating(False):  # Turn off for example purpose.\n    ...     for user in users:\n    ...         print 'Name: {0}'.format(user['first_name'])\n    ...         print 'Email: {0}'.format(user['email'])\n    Name: Martin\n    Email: NOT_SET\n    ...\n\n.. note::\n\n    You can populate a single or many entities in one call so long as they are\n    all the same entity type.\n\n.. _working_with_entities/entity_states:\n\nEntity states\n=============\n\nOperations on entities are :ref:`recorded in the session\n<understanding_sessions/unit_of_work>` as they happen. At any time you can\ninspect an entity to determine its current state from those pending operations.\n\nTo do this, use :func:`ftrack_api.inspection.state`::\n\n    >>> import ftrack_api.inspection\n    >>> new_user = session.create('User', {})\n    >>> print ftrack_api.inspection.state(new_user)\n    CREATED\n    >>> existing_user = session.query('User').first()\n    >>> print ftrack_api.inspection.state(existing_user)\n    NOT_SET\n    >>> existing_user['email'] = 'martin@example.com'\n    >>> print ftrack_api.inspection.state(existing_user)\n    MODIFIED\n    >>> session.delete(new_user)\n    >>> print ftrack_api.inspection.state(new_user)\n    DELETED\n\n.. _working_with_entities/entity_types:\n\nCustomising entity types\n========================\n\nEach type of entity in the system is represented in the Python client by a\ndedicated class. However, because the types of entities can vary these classes\nare built on demand using schema information retrieved from the server.\n\nMany of the default classes provide additional helper methods which are mixed\ninto the generated class at runtime when a session is started.\n\nIn some cases it can be useful to tailor the custom classes to your own pipeline\nworkflows. Perhaps you want to add more helper functions, change attribute\naccess rules or even providing a layer of backwards compatibility for existing\ncode. The Python client was built with this in mind and makes such\ncustomisations as easy as possible.\n\nWhen a :class:`Session` is constructed it fetches schema details from the\nconnected server and then calls an :class:`Entity factory\n<ftrack_api.entity.factory.Factory>` to create classes from those schemas. It\ndoes this by emitting a synchronous event,\n*ftrack.api.session.construct-entity-type*, for  each schema and expecting a\n*class* object to be returned.\n\nIn the default setup, a :download:`construct_entity_type.py\n<../resource/plugin/construct_entity_type.py>` plugin is placed on the\n:envvar:`FTRACK_EVENT_PLUGIN_PATH`. This plugin will register a trivial subclass\nof :class:`ftrack_api.entity.factory.StandardFactory` to create the classes in\nresponse to the construct event. The simplest way to get started is to edit this\ndefault plugin as required.\n\n.. seealso:: :ref:`understanding_sessions/plugins`\n\n.. _working_with_entities/entity_types/default_projections:\n\nDefault projections\n-------------------\n\nWhen a :ref:`query <querying>` is issued without any :ref:`projections\n<querying/projections>`, the session will automatically add default projections\naccording to the type of the entity.\n\nFor example, the following shows that for a *User*, only *id* is fetched by\ndefault when no projections added to the query::\n\n    >>> user = session.query('User').first()\n    >>> with session.auto_populating(False):  # For demonstration purpose only.\n    ...     print user.items()\n    [\n        (u'id', u'59f0963a-15e2-11e1-a5f1-0019bb4983d8')\n        (u'username', Symbol(NOT_SET)),\n        (u'first_name', Symbol(NOT_SET)),\n        ...\n    ]\n\n.. note::\n\n    These default projections are also used when you access a relationship\n    attribute using the dictionary key syntax.\n\nIf you want to default to fetching *username* for a *Task* as well then you can\nchange the default_projections* in your class factory plugin::\n\n    class Factory(ftrack_api.entity.factory.StandardFactory):\n        '''Entity class factory.'''\n\n        def create(self, schema, bases=None):\n            '''Create and return entity class from *schema*.'''\n            cls = super(Factory, self).create(schema, bases=bases)\n\n            # Further customise cls before returning.\n            if schema['id'] == 'User':\n                cls.default_projections = ['id', 'username']\n\n            return cls\n\nNow a projection-less query will also query *username* by default:\n\n.. note::\n\n    You will need to start a new session to pick up the change you made::\n\n        session = ftrack_api.Session()\n\n.. code-block:: python\n\n    >>> user = session.query('User').first()\n    >>> with session.auto_populating(False):  # For demonstration purpose only.\n    ...     print user.items()\n    [\n        (u'id', u'59f0963a-15e2-11e1-a5f1-0019bb4983d8')\n        (u'username', u'martin'),\n        (u'first_name', Symbol(NOT_SET)),\n        ...\n    ]\n\nNote that if any specific projections are applied in a query, those override\nthe default projections entirely. This allows you to also *reduce* the data\nloaded on demand::\n\n    >>> session = ftrack_api.Session()  # Start new session to avoid cache.\n    >>> user = session.query('select id from User').first()\n    >>> with session.auto_populating(False):  # For demonstration purpose only.\n    ...     print user.items()\n    [\n        (u'id', u'59f0963a-15e2-11e1-a5f1-0019bb4983d8')\n        (u'username', Symbol(NOT_SET)),\n        (u'first_name', Symbol(NOT_SET)),\n        ...\n    ]\n\n.. _working_with_entities/entity_types/helper_methods:\n\nHelper methods\n--------------\n\nIf you want to add additional helper methods to the constructed classes to\nbetter support your pipeline logic, then you can simply patch the created\nclasses in your factory, much like with changing the default projections::\n\n    def get_full_name(self):\n        '''Return full name for user.'''\n        return '{0} {1}'.format(self['first_name'], self['last_name']).strip()\n\n    class Factory(ftrack_api.entity.factory.StandardFactory):\n        '''Entity class factory.'''\n\n        def create(self, schema, bases=None):\n            '''Create and return entity class from *schema*.'''\n            cls = super(Factory, self).create(schema, bases=bases)\n\n            # Further customise cls before returning.\n            if schema['id'] == 'User':\n                cls.get_full_name = get_full_name\n\n            return cls\n\nNow you have a new helper method *get_full_name* on your *User* entities::\n\n    >>> session = ftrack_api.Session()  # New session to pick up changes.\n    >>> user = session.query('User').first()\n    >>> print user.get_full_name()\n    Martin Pengelly-Phillips\n\nIf you'd rather not patch the existing classes, or perhaps have a lot of helpers\nto mixin, you can instead inject your own class as the base class. The only\nrequirement is that it has the base :class:`~ftrack_api.entity.base.Entity`\nclass in its ancestor classes::\n\n    import ftrack_api.entity.base\n\n\n    class CustomUser(ftrack_api.entity.base.Entity):\n        '''Represent user.'''\n\n        def get_full_name(self):\n            '''Return full name for user.'''\n            return '{0} {1}'.format(self['first_name'], self['last_name']).strip()\n\n\n    class Factory(ftrack_api.entity.factory.StandardFactory):\n        '''Entity class factory.'''\n\n        def create(self, schema, bases=None):\n            '''Create and return entity class from *schema*.'''\n            # Alter base class for constructed class.\n            if bases is None:\n                bases = [ftrack_api.entity.base.Entity]\n\n            if schema['id'] == 'User':\n                bases = [CustomUser]\n\n            cls = super(Factory, self).create(schema, bases=bases)\n            return cls\n\nThe resulting effect is the same::\n\n    >>> session = ftrack_api.Session()  # New session to pick up changes.\n    >>> user = session.query('User').first()\n    >>> print user.get_full_name()\n    Martin Pengelly-Phillips\n\n.. note::\n\n    Your custom class is not the leaf class which will still be a dynamically\n    generated class. Instead your custom class becomes the base for the leaf\n    class::\n\n        >>> print type(user).__mro__\n        (<dynamic ftrack class 'User'>, <dynamic ftrack class 'CustomUser'>, ...)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/pytest.ini",
    "content": "[pytest]\nminversion = 2.4.2\naddopts = -v -k-slow --junitxml=test-reports/junit.xml --cache-clear\nnorecursedirs = .* _*\npython_files = test_*.py\npython_functions = test_*\nmock_use_standalone_module = true"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/resource/plugin/configure_locations.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport logging\n\nimport ftrack_api\nimport ftrack_api.entity.location\nimport ftrack_api.accessor.disk\n\n\ndef configure_locations(event):\n    '''Configure locations for session.'''\n    session = event['data']['session']\n\n    # Find location(s) and customise instances.\n    #\n    # location = session.query('Location where name is \"my.location\"').one()\n    # ftrack_api.mixin(location, ftrack_api.entity.location.UnmanagedLocationMixin)\n    # location.accessor = ftrack_api.accessor.disk.DiskAccessor(prefix='')\n\n\ndef register(session):\n    '''Register plugin with *session*.'''\n    logger = logging.getLogger('ftrack_plugin:configure_locations.register')\n\n    # Validate that session is an instance of ftrack_api.Session. If not, assume\n    # that register is being called from an old or incompatible API and return\n    # without doing anything.\n    if not isinstance(session, ftrack_api.Session):\n        logger.debug(\n            'Not subscribing plugin as passed argument {0} is not an '\n            'ftrack_api.Session instance.'.format(session)\n        )\n        return\n\n    session.event_hub.subscribe(\n        'topic=ftrack.api.session.configure-location',\n        configure_locations\n    )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/resource/plugin/construct_entity_type.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport logging\n\nimport ftrack_api.entity.factory\n\n\nclass Factory(ftrack_api.entity.factory.StandardFactory):\n    '''Entity class factory.'''\n\n    def create(self, schema, bases=None):\n        '''Create and return entity class from *schema*.'''\n        # Optionally change bases for class to be generated.\n        cls = super(Factory, self).create(schema, bases=bases)\n\n        # Further customise cls before returning.\n\n        return cls\n\n\ndef register(session):\n    '''Register plugin with *session*.'''\n    logger = logging.getLogger('ftrack_plugin:construct_entity_type.register')\n\n    # Validate that session is an instance of ftrack_api.Session. If not, assume\n    # that register is being called from an old or incompatible API and return\n    # without doing anything.\n    if not isinstance(session, ftrack_api.Session):\n        logger.debug(\n            'Not subscribing plugin as passed argument {0!r} is not an '\n            'ftrack_api.Session instance.'.format(session)\n        )\n        return\n\n    factory = Factory()\n\n    def construct_entity_type(event):\n        '''Return class to represent entity type specified by *event*.'''\n        schema = event['data']['schema']\n        return factory.create(schema)\n\n    session.event_hub.subscribe(\n        'topic=ftrack.api.session.construct-entity-type',\n        construct_entity_type\n    )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/setup.cfg",
    "content": "[build_sphinx]\nconfig-dir = doc\nsource-dir = doc\nbuild-dir = build/doc\nbuilder = html\nall_files = 1\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/setup.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport os\nimport re\n\nfrom setuptools import setup, find_packages\nfrom setuptools.command.test import test as TestCommand\n\n\nROOT_PATH = os.path.dirname(os.path.realpath(__file__))\nRESOURCE_PATH = os.path.join(ROOT_PATH, 'resource')\nSOURCE_PATH = os.path.join(ROOT_PATH, 'source')\nREADME_PATH = os.path.join(ROOT_PATH, 'README.rst')\n\n\n# Read version from source.\nwith open(\n    os.path.join(SOURCE_PATH, 'ftrack_api', '_version.py')\n) as _version_file:\n    VERSION = re.match(\n        r'.*__version__ = \\'(.*?)\\'', _version_file.read(), re.DOTALL\n    ).group(1)\n\n\n# Custom commands.\nclass PyTest(TestCommand):\n    '''Pytest command.'''\n\n    def finalize_options(self):\n        '''Finalize options to be used.'''\n        TestCommand.finalize_options(self)\n        self.test_args = []\n        self.test_suite = True\n\n    def run_tests(self):\n        '''Import pytest and run.'''\n        import pytest\n        raise SystemExit(pytest.main(self.test_args))\n\n\n# Call main setup.\nsetup(\n    name='ftrack-python-api',\n    version=VERSION,\n    description='Python API for ftrack.',\n    long_description=open(README_PATH).read(),\n    keywords='ftrack, python, api',\n    url='https://bitbucket.org/ftrack/ftrack-python-api',\n    author='ftrack',\n    author_email='support@ftrack.com',\n    license='Apache License (2.0)',\n    packages=find_packages(SOURCE_PATH),\n    package_dir={\n        '': 'source'\n    },\n    setup_requires=[\n        'sphinx >= 1.2.2, < 2',\n        'sphinx_rtd_theme >= 0.1.6, < 1',\n        'lowdown >= 0.1.0, < 2'\n    ],\n    install_requires=[\n        'requests >= 2, <3',\n        'arrow >= 0.4.4, < 1',\n        'termcolor >= 1.1.0, < 2',\n        'pyparsing >= 2.0, < 3',\n        'clique >= 1.2.0, < 2',\n        'websocket-client >= 0.40.0, < 1'\n    ],\n    tests_require=[\n        'pytest >= 2.7, < 3',\n        'pytest-mock >= 0.4, < 1',\n        'pytest-catchlog >= 1, <=2'\n    ],\n    cmdclass={\n        'test': PyTest\n    },\n    zip_safe=False,\n    python_requires=\">=2.7.9, <3.0\"\n\n)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/__init__.py",
    "content": "from ftrack_api import *\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/__init__.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nfrom ._version import __version__\nfrom .session import Session\n\n\ndef mixin(instance, mixin_class, name=None):\n    '''Mixin *mixin_class* to *instance*.\n\n    *name* can be used to specify new class name. If not specified then one will\n    be generated.\n\n    '''\n    if name is None:\n        name = '{0}{1}'.format(\n            instance.__class__.__name__, mixin_class.__name__\n        )\n\n    # Check mixin class not already present in mro in order to avoid consistent\n    # method resolution failure.\n    if mixin_class in instance.__class__.mro():\n        return\n\n    instance.__class__ = type(\n        name,\n        (\n            mixin_class,\n            instance.__class__\n        ),\n        {}\n    )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/_centralized_storage_scenario.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2016 ftrack\n\nfrom __future__ import absolute_import\n\nimport logging\nimport json\nimport sys\nimport os\n\nimport ftrack_api\nimport ftrack_api.structure.standard as _standard\nfrom ftrack_api.logging import LazyLogMessage as L\n\n\nscenario_name = 'ftrack.centralized-storage'\n\n\nclass ConfigureCentralizedStorageScenario(object):\n    '''Configure a centralized storage scenario.'''\n\n    def __init__(self):\n        '''Instansiate centralized storage scenario.'''\n        self.logger = logging.getLogger(\n            __name__ + '.' + self.__class__.__name__\n        )\n\n    @property\n    def storage_scenario(self):\n        '''Return storage scenario setting.'''\n        return self.session.query(\n            'select value from Setting '\n            'where name is \"storage_scenario\" and group is \"STORAGE\"'\n        ).one()\n\n    @property\n    def existing_centralized_storage_configuration(self):\n        '''Return existing centralized storage configuration.'''\n        storage_scenario = self.storage_scenario\n\n        try:\n            configuration = json.loads(storage_scenario['value'])\n        except (ValueError, TypeError):\n            return None\n\n        if not isinstance(configuration, dict):\n            return None\n\n        if configuration.get('scenario') != scenario_name:\n            return None\n\n        return configuration.get('data', {})\n\n    def _get_confirmation_text(self, configuration):\n        '''Return confirmation text from *configuration*.'''\n        configure_location = configuration.get('configure_location')\n        select_location = configuration.get('select_location')\n        select_mount_point = configuration.get('select_mount_point')\n\n        if configure_location:\n            location_text = unicode(\n                'A new location will be created:\\n\\n'\n                '* Label: {location_label}\\n'\n                '* Name: {location_name}\\n'\n                '* Description: {location_description}\\n'\n            ).format(**configure_location)\n        else:\n            location = self.session.get(\n                'Location', select_location['location_id']\n            )\n            location_text = (\n                u'You have choosen to use an existing location: {0}'.format(\n                    location['label']\n                )\n            )\n\n        mount_points_text = unicode(\n            '* Linux: {linux}\\n'\n            '* OS X: {osx}\\n'\n            '* Windows: {windows}\\n\\n'\n        ).format(\n            linux=select_mount_point.get('linux_mount_point') or '*Not set*',\n            osx=select_mount_point.get('osx_mount_point') or '*Not set*',\n            windows=select_mount_point.get('windows_mount_point') or '*Not set*'\n        )\n\n        mount_points_not_set = []\n\n        if not select_mount_point.get('linux_mount_point'):\n            mount_points_not_set.append('Linux')\n\n        if not select_mount_point.get('osx_mount_point'):\n            mount_points_not_set.append('OS X')\n\n        if not select_mount_point.get('windows_mount_point'):\n            mount_points_not_set.append('Windows')\n\n        if mount_points_not_set:\n            mount_points_text += unicode(\n                'Please be aware that this location will not be working on '\n                '{missing} because the mount points are not set up.'\n            ).format(\n                missing=' and '.join(mount_points_not_set)\n            )\n\n        text = unicode(\n            '#Confirm storage setup#\\n\\n'\n            'Almost there! Please take a moment to verify the settings you '\n            'are about to save. You can always come back later and update the '\n            'configuration.\\n'\n            '##Location##\\n\\n'\n            '{location}\\n'\n            '##Mount points##\\n\\n'\n            '{mount_points}'\n        ).format(\n            location=location_text,\n            mount_points=mount_points_text\n        )\n\n        return text\n\n    def configure_scenario(self, event):\n        '''Configure scenario based on *event* and return form items.'''\n        steps = (\n            'select_scenario',\n            'select_location',\n            'configure_location',\n            'select_structure',\n            'select_mount_point',\n            'confirm_summary',\n            'save_configuration'\n        )\n\n        warning_message = ''\n        values = event['data'].get('values', {})\n\n        # Calculate previous step and the next.\n        previous_step = values.get('step', 'select_scenario')\n        next_step = steps[steps.index(previous_step) + 1]\n        state = 'configuring'\n\n        self.logger.info(L(\n            u'Configuring scenario, previous step: {0}, next step: {1}. '\n            u'Values {2!r}.',\n            previous_step, next_step, values\n        ))\n\n        if 'configuration' in values:\n            configuration = values.pop('configuration')\n        else:\n            configuration = {}\n\n        if values:\n            # Update configuration with values from the previous step.\n            configuration[previous_step] = values\n\n        if previous_step == 'select_location':\n            values = configuration['select_location']\n            if values.get('location_id') != 'create_new_location':\n                location_exists = self.session.query(\n                    'Location where id is \"{0}\"'.format(\n                        values.get('location_id')\n                    )\n                ).first()\n                if not location_exists:\n                    next_step = 'select_location'\n                    warning_message = (\n                        '**The selected location does not exist. Please choose '\n                        'one from the dropdown or create a new one.**'\n                    )\n\n        if next_step == 'select_location':\n            try:\n                location_id = (\n                    self.existing_centralized_storage_configuration['location_id']\n                )\n            except (KeyError, TypeError):\n                location_id = None\n\n            options = [{\n                'label': 'Create new location',\n                'value': 'create_new_location'\n            }]\n            for location in self.session.query(\n                'select name, label, description from Location'\n            ):\n                if location['name'] not in (\n                    'ftrack.origin', 'ftrack.unmanaged', 'ftrack.connect',\n                    'ftrack.server', 'ftrack.review'\n                ):\n                    options.append({\n                        'label': u'{label} ({name})'.format(\n                            label=location['label'], name=location['name']\n                        ),\n                        'description': location['description'],\n                        'value': location['id']\n                    })\n\n            warning = ''\n            if location_id is not None:\n                # If there is already a location configured we must make the\n                # user aware that changing the location may be problematic.\n                warning = (\n                    '\\n\\n**Be careful if you switch to another location '\n                    'for an existing storage scenario. Components that have '\n                    'already been published to the previous location will be '\n                    'made unavailable for common use.**'\n                )\n                default_value = location_id\n            elif location_id is None and len(options) == 1:\n                # No location configured and no existing locations to use.\n                default_value = 'create_new_location'\n            else:\n                # There are existing locations to choose from but non of them\n                # are currently active in the centralized storage scenario.\n                default_value = None\n\n            items = [{\n                'type': 'label',\n                'value': (\n                    '#Select location#\\n'\n                    'Choose an already existing location or create a new one '\n                    'to represent your centralized storage. {0}'.format(\n                        warning\n                    )\n                )\n            }, {\n                'type': 'enumerator',\n                'label': 'Location',\n                'name': 'location_id',\n                'value': default_value,\n                'data': options\n            }]\n\n        default_location_name = 'studio.central-storage-location'\n        default_location_label = 'Studio location'\n        default_location_description = (\n            'The studio central location where all components are '\n            'stored.'\n        )\n\n        if previous_step == 'configure_location':\n            configure_location = configuration.get(\n                'configure_location'\n            )\n\n            if configure_location:\n                try:\n                    existing_location = self.session.query(\n                        u'Location where name is \"{0}\"'.format(\n                            configure_location.get('location_name')\n                        )\n                    ).first()\n                except UnicodeEncodeError:                \n                    next_step = 'configure_location'\n                    warning_message += (\n                        '**The location name contains non-ascii characters. '\n                        'Please change the name and try again.**'\n                    )\n                    values = configuration['select_location']\n                else:\n                    if existing_location:\n                        next_step = 'configure_location'\n                        warning_message += (\n                            u'**There is already a location named {0}. '\n                            u'Please change the name and try again.**'.format(\n                                configure_location.get('location_name')\n                            )\n                        )\n                        values = configuration['select_location']\n\n                if (\n                    not configure_location.get('location_name') or\n                    not configure_location.get('location_label') or\n                    not configure_location.get('location_description')\n                ):\n                    next_step = 'configure_location'\n                    warning_message += (\n                        '**Location name, label and description cannot '\n                        'be empty.**'\n                    )\n                    values = configuration['select_location']\n\n            if next_step == 'configure_location':\n                # Populate form with previous configuration.\n                default_location_label = configure_location['location_label']\n                default_location_name = configure_location['location_name']\n                default_location_description = (\n                    configure_location['location_description']\n                )\n\n        if next_step == 'configure_location':\n\n            if values.get('location_id') == 'create_new_location':\n                # Add options to create a new location.\n                items = [{\n                    'type': 'label',\n                    'value': (\n                        '#Create location#\\n'\n                        'Here you will create a new location to be used '\n                        'with your new Storage scenario. For your '\n                        'convenience we have already filled in some default '\n                        'values. If this is the first time you are configuring '\n                        'a storage scenario in ftrack we recommend that you '\n                        'stick with these settings.'\n                    )\n                }, {\n                    'label': 'Label',\n                    'name': 'location_label',\n                    'value': default_location_label,\n                    'type': 'text'\n                }, {\n                    'label': 'Name',\n                    'name': 'location_name',\n                    'value': default_location_name,\n                    'type': 'text'\n                }, {\n                    'label': 'Description',\n                    'name': 'location_description',\n                    'value': default_location_description,\n                    'type': 'text'\n                }]\n\n            else:\n                # The user selected an existing location. Move on to next\n                # step.\n                next_step = 'select_mount_point'\n\n        if next_step == 'select_structure':\n            # There is only one structure to choose from, go to next step.\n            next_step = 'select_mount_point'\n            # items = [\n            #     {\n            #         'type': 'label',\n            #         'value': (\n            #             '#Select structure#\\n'\n            #             'Select which structure to use with your location. '\n            #             'The structure is used to generate the filesystem '\n            #             'path for components that are added to this location.'\n            #         )\n            #     },\n            #     {\n            #         'type': 'enumerator',\n            #         'label': 'Structure',\n            #         'name': 'structure_id',\n            #         'value': 'standard',\n            #         'data': [{\n            #             'label': 'Standard',\n            #             'value': 'standard',\n            #             'description': (\n            #                 'The Standard structure uses the names in your '\n            #                 'project structure to determine the path.'\n            #             )\n            #         }]\n            #     }\n            # ]\n\n        if next_step == 'select_mount_point':\n            try:\n                mount_points = (\n                    self.existing_centralized_storage_configuration['accessor']['mount_points']\n                )\n            except (KeyError, TypeError):\n                mount_points = dict()\n\n            items = [\n                {\n                    'value': (\n                        '#Mount points#\\n'\n                        'Set mount points for your centralized storage '\n                        'location. For the location to work as expected each '\n                        'platform that you intend to use must have the '\n                        'corresponding mount point set and the storage must '\n                        'be accessible. If not set correctly files will not be '\n                        'saved or read.'\n                    ),\n                    'type': 'label'\n                }, {\n                    'type': 'text',\n                    'label': 'Linux',\n                    'name': 'linux_mount_point',\n                    'empty_text': 'E.g. /usr/mnt/MyStorage ...',\n                    'value': mount_points.get('linux', '')\n                }, {\n                    'type': 'text',\n                    'label': 'OS X',\n                    'name': 'osx_mount_point',\n                    'empty_text': 'E.g. /Volumes/MyStorage ...',\n                    'value': mount_points.get('osx', '')\n                }, {\n                    'type': 'text',\n                    'label': 'Windows',\n                    'name': 'windows_mount_point',\n                    'empty_text': 'E.g. \\\\\\\\MyStorage ...',\n                    'value': mount_points.get('windows', '')\n                }\n            ]\n\n        if next_step == 'confirm_summary':\n            items = [{\n                'type': 'label',\n                'value': self._get_confirmation_text(configuration)\n            }]\n            state = 'confirm'\n\n        if next_step == 'save_configuration':\n            mount_points = configuration['select_mount_point']\n            select_location = configuration['select_location']\n\n            if select_location['location_id'] == 'create_new_location':\n                configure_location = configuration['configure_location']\n                location = self.session.create(\n                    'Location',\n                    {\n                        'name': configure_location['location_name'],\n                        'label': configure_location['location_label'],\n                        'description': (\n                            configure_location['location_description']\n                        )\n                    }\n                )\n\n            else:\n                location = self.session.query(\n                    'Location where id is \"{0}\"'.format(\n                        select_location['location_id']\n                    )\n                ).one()\n\n            setting_value = json.dumps({\n                'scenario': scenario_name,\n                'data': {\n                    'location_id': location['id'],\n                    'location_name': location['name'],\n                    'accessor': {\n                        'mount_points': {\n                            'linux': mount_points['linux_mount_point'],\n                            'osx': mount_points['osx_mount_point'],\n                            'windows': mount_points['windows_mount_point']\n                        }\n                    }\n                }\n            })\n\n            self.storage_scenario['value'] = setting_value\n            self.session.commit()\n\n            # Broadcast an event that storage scenario has been configured.\n            event = ftrack_api.event.base.Event(\n                topic='ftrack.storage-scenario.configure-done'\n            )\n            self.session.event_hub.publish(event)\n\n            items = [{\n                'type': 'label',\n                'value': (\n                    '#Done!#\\n'\n                    'Your storage scenario is now configured and ready '\n                    'to use. **Note that you may have to restart Connect and '\n                    'other applications to start using it.**'\n                )\n            }]\n            state = 'done'\n\n        if warning_message:\n            items.insert(0, {\n                'type': 'label',\n                'value': warning_message\n            })\n\n        items.append({\n            'type': 'hidden',\n            'value': configuration,\n            'name': 'configuration'\n        })\n        items.append({\n            'type': 'hidden',\n            'value': next_step,\n            'name': 'step'\n        })\n\n        return {\n            'items': items,\n            'state': state\n        }\n\n    def discover_centralized_scenario(self, event):\n        '''Return action discover dictionary for *event*.'''\n        return {\n            'id': scenario_name,\n            'name': 'Centralized storage scenario',\n            'description': (\n                '(Recommended) centralized storage scenario where all files '\n                'are kept on a storage that is mounted and available to '\n                'everyone in the studio.'\n            )\n        }\n\n    def register(self, session):\n        '''Subscribe to events on *session*.'''\n        self.session = session\n\n        #: TODO: Move these to a separate function.\n        session.event_hub.subscribe(\n            unicode(\n                'topic=ftrack.storage-scenario.discover '\n                'and source.user.username=\"{0}\"'\n            ).format(\n                session.api_user\n            ),\n            self.discover_centralized_scenario\n        )\n        session.event_hub.subscribe(\n            unicode(\n                'topic=ftrack.storage-scenario.configure '\n                'and data.scenario_id=\"{0}\" '\n                'and source.user.username=\"{1}\"'\n            ).format(\n                scenario_name,\n                session.api_user\n            ),\n            self.configure_scenario\n        )\n\n\nclass ActivateCentralizedStorageScenario(object):\n    '''Activate a centralized storage scenario.'''\n\n    def __init__(self):\n        '''Instansiate centralized storage scenario.'''\n        self.logger = logging.getLogger(\n            __name__ + '.' + self.__class__.__name__\n        )\n\n    def activate(self, event):\n        '''Activate scenario in *event*.'''\n        storage_scenario = event['data']['storage_scenario']\n\n        try:\n            location_data = storage_scenario['data']\n            location_name = location_data['location_name']\n            location_id = location_data['location_id']\n            mount_points = location_data['accessor']['mount_points']\n\n        except KeyError:\n            error_message = (\n                'Unable to read storage scenario data.'\n            )\n            self.logger.error(L(error_message))\n            raise ftrack_api.exception.LocationError(\n                'Unable to configure location based on scenario.'\n            )\n\n        else:\n            location = self.session.create(\n                'Location',\n                data=dict(\n                    name=location_name,\n                    id=location_id\n                ),\n                reconstructing=True\n            )\n\n            if sys.platform == 'darwin':\n                prefix = mount_points['osx']\n            elif sys.platform == 'linux2':\n                prefix = mount_points['linux']\n            elif sys.platform == 'win32':\n                prefix = mount_points['windows']\n            else:\n                raise ftrack_api.exception.LocationError(\n                    (\n                        'Unable to find accessor prefix for platform {0}.'\n                    ).format(sys.platform)\n                )\n\n            location.accessor = ftrack_api.accessor.disk.DiskAccessor(\n                prefix=prefix\n            )\n            location.structure = _standard.StandardStructure()\n            location.priority = 1\n            self.logger.info(L(\n                u'Storage scenario activated. Configured {0!r} from '\n                u'{1!r}',\n                location, storage_scenario\n            ))\n\n    def _verify_startup(self, event):\n        '''Verify the storage scenario configuration.'''\n        storage_scenario = event['data']['storage_scenario']\n        location_data = storage_scenario['data']\n        mount_points = location_data['accessor']['mount_points']\n\n        prefix = None\n        if sys.platform == 'darwin':\n            prefix = mount_points['osx']\n        elif sys.platform == 'linux2':\n            prefix = mount_points['linux']\n        elif sys.platform == 'win32':\n            prefix = mount_points['windows']\n\n        if not prefix:\n            return (\n                u'The storage scenario has not been configured for your '\n                u'operating system. ftrack may not be able to '\n                u'store and track files correctly.'\n            )\n\n        if not os.path.isdir(prefix):\n            return (\n                unicode(\n                    'The path {0} does not exist. ftrack may not be able to '\n                    'store and track files correctly. \\n\\nIf the storage is '\n                    'newly setup you may want to create necessary folder '\n                    'structures. If the storage is a network drive you should '\n                    'make sure that it is mounted correctly.'\n                ).format(prefix)\n            )\n\n    def register(self, session):\n        '''Subscribe to events on *session*.'''\n        self.session = session\n\n        session.event_hub.subscribe(\n            (\n                'topic=ftrack.storage-scenario.activate '\n                'and data.storage_scenario.scenario=\"{0}\"'.format(\n                    scenario_name\n                )\n            ),\n            self.activate\n        )\n\n        # Listen to verify startup event from ftrack connect to allow responding\n        # with a message if something is not working correctly with this\n        # scenario that the user should be notified about.\n        self.session.event_hub.subscribe(\n            (\n                'topic=ftrack.connect.verify-startup '\n                'and data.storage_scenario.scenario=\"{0}\"'.format(\n                    scenario_name\n                )\n            ),\n            self._verify_startup\n        )\n\ndef register(session):\n    '''Register storage scenario.'''\n    scenario = ActivateCentralizedStorageScenario()\n    scenario.register(session)\n\n\ndef register_configuration(session):\n    '''Register storage scenario.'''\n    scenario = ConfigureCentralizedStorageScenario()\n    scenario.register(session)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/_python_ntpath.py",
    "content": "# pragma: no cover\n# Module 'ntpath' -- common operations on WinNT/Win95 pathnames\n\"\"\"Common pathname manipulations, WindowsNT/95 version.\n\nInstead of importing this module directly, import os and refer to this\nmodule as os.path.\n\"\"\"\n\nimport os\nimport sys\nimport stat\nimport genericpath\nimport warnings\n\nfrom genericpath import *\n\n__all__ = [\"normcase\",\"isabs\",\"join\",\"splitdrive\",\"split\",\"splitext\",\n           \"basename\",\"dirname\",\"commonprefix\",\"getsize\",\"getmtime\",\n           \"getatime\",\"getctime\", \"islink\",\"exists\",\"lexists\",\"isdir\",\"isfile\",\n           \"ismount\",\"walk\",\"expanduser\",\"expandvars\",\"normpath\",\"abspath\",\n           \"splitunc\",\"curdir\",\"pardir\",\"sep\",\"pathsep\",\"defpath\",\"altsep\",\n           \"extsep\",\"devnull\",\"realpath\",\"supports_unicode_filenames\",\"relpath\"]\n\n# strings representing various path-related bits and pieces\ncurdir = '.'\npardir = '..'\nextsep = '.'\nsep = '\\\\'\npathsep = ';'\naltsep = '/'\ndefpath = '.;C:\\\\bin'\nif 'ce' in sys.builtin_module_names:\n    defpath = '\\\\Windows'\nelif 'os2' in sys.builtin_module_names:\n    # OS/2 w/ VACPP\n    altsep = '/'\ndevnull = 'nul'\n\n# Normalize the case of a pathname and map slashes to backslashes.\n# Other normalizations (such as optimizing '../' away) are not done\n# (this is done by normpath).\n\ndef normcase(s):\n    \"\"\"Normalize case of pathname.\n\n    Makes all characters lowercase and all slashes into backslashes.\"\"\"\n    return s.replace(\"/\", \"\\\\\").lower()\n\n\n# Return whether a path is absolute.\n# Trivial in Posix, harder on the Mac or MS-DOS.\n# For DOS it is absolute if it starts with a slash or backslash (current\n# volume), or if a pathname after the volume letter and colon / UNC resource\n# starts with a slash or backslash.\n\ndef isabs(s):\n    \"\"\"Test whether a path is absolute\"\"\"\n    s = splitdrive(s)[1]\n    return s != '' and s[:1] in '/\\\\'\n\n\n# Join two (or more) paths.\n\ndef join(a, *p):\n    \"\"\"Join two or more pathname components, inserting \"\\\\\" as needed.\n    If any component is an absolute path, all previous path components\n    will be discarded.\"\"\"\n    path = a\n    for b in p:\n        b_wins = 0  # set to 1 iff b makes path irrelevant\n        if path == \"\":\n            b_wins = 1\n\n        elif isabs(b):\n            # This probably wipes out path so far.  However, it's more\n            # complicated if path begins with a drive letter:\n            #     1. join('c:', '/a') == 'c:/a'\n            #     2. join('c:/', '/a') == 'c:/a'\n            # But\n            #     3. join('c:/a', '/b') == '/b'\n            #     4. join('c:', 'd:/') = 'd:/'\n            #     5. join('c:/', 'd:/') = 'd:/'\n            if path[1:2] != \":\" or b[1:2] == \":\":\n                # Path doesn't start with a drive letter, or cases 4 and 5.\n                b_wins = 1\n\n            # Else path has a drive letter, and b doesn't but is absolute.\n            elif len(path) > 3 or (len(path) == 3 and\n                                   path[-1] not in \"/\\\\\"):\n                # case 3\n                b_wins = 1\n\n        if b_wins:\n            path = b\n        else:\n            # Join, and ensure there's a separator.\n            assert len(path) > 0\n            if path[-1] in \"/\\\\\":\n                if b and b[0] in \"/\\\\\":\n                    path += b[1:]\n                else:\n                    path += b\n            elif path[-1] == \":\":\n                path += b\n            elif b:\n                if b[0] in \"/\\\\\":\n                    path += b\n                else:\n                    path += \"\\\\\" + b\n            else:\n                # path is not empty and does not end with a backslash,\n                # but b is empty; since, e.g., split('a/') produces\n                # ('a', ''), it's best if join() adds a backslash in\n                # this case.\n                path += '\\\\'\n\n    return path\n\n\n# Split a path in a drive specification (a drive letter followed by a\n# colon) and the path specification.\n# It is always true that drivespec + pathspec == p\ndef splitdrive(p):\n    \"\"\"Split a pathname into drive and path specifiers. Returns a 2-tuple\n\"(drive,path)\";  either part may be empty\"\"\"\n    if p[1:2] == ':':\n        return p[0:2], p[2:]\n    return '', p\n\n\n# Parse UNC paths\ndef splitunc(p):\n    \"\"\"Split a pathname into UNC mount point and relative path specifiers.\n\n    Return a 2-tuple (unc, rest); either part may be empty.\n    If unc is not empty, it has the form '//host/mount' (or similar\n    using backslashes).  unc+rest is always the input path.\n    Paths containing drive letters never have an UNC part.\n    \"\"\"\n    if p[1:2] == ':':\n        return '', p # Drive letter present\n    firstTwo = p[0:2]\n    if firstTwo == '//' or firstTwo == '\\\\\\\\':\n        # is a UNC path:\n        # vvvvvvvvvvvvvvvvvvvv equivalent to drive letter\n        # \\\\machine\\mountpoint\\directories...\n        #           directory ^^^^^^^^^^^^^^^\n        normp = normcase(p)\n        index = normp.find('\\\\', 2)\n        if index == -1:\n            ##raise RuntimeError, 'illegal UNC path: \"' + p + '\"'\n            return (\"\", p)\n        index = normp.find('\\\\', index + 1)\n        if index == -1:\n            index = len(p)\n        return p[:index], p[index:]\n    return '', p\n\n\n# Split a path in head (everything up to the last '/') and tail (the\n# rest).  After the trailing '/' is stripped, the invariant\n# join(head, tail) == p holds.\n# The resulting head won't end in '/' unless it is the root.\n\ndef split(p):\n    \"\"\"Split a pathname.\n\n    Return tuple (head, tail) where tail is everything after the final slash.\n    Either part may be empty.\"\"\"\n\n    d, p = splitdrive(p)\n    # set i to index beyond p's last slash\n    i = len(p)\n    while i and p[i-1] not in '/\\\\':\n        i = i - 1\n    head, tail = p[:i], p[i:]  # now tail has no slashes\n    # remove trailing slashes from head, unless it's all slashes\n    head2 = head\n    while head2 and head2[-1] in '/\\\\':\n        head2 = head2[:-1]\n    head = head2 or head\n    return d + head, tail\n\n\n# Split a path in root and extension.\n# The extension is everything starting at the last dot in the last\n# pathname component; the root is everything before that.\n# It is always true that root + ext == p.\n\ndef splitext(p):\n    return genericpath._splitext(p, sep, altsep, extsep)\nsplitext.__doc__ = genericpath._splitext.__doc__\n\n\n# Return the tail (basename) part of a path.\n\ndef basename(p):\n    \"\"\"Returns the final component of a pathname\"\"\"\n    return split(p)[1]\n\n\n# Return the head (dirname) part of a path.\n\ndef dirname(p):\n    \"\"\"Returns the directory component of a pathname\"\"\"\n    return split(p)[0]\n\n# Is a path a symbolic link?\n# This will always return false on systems where posix.lstat doesn't exist.\n\ndef islink(path):\n    \"\"\"Test for symbolic link.\n    On WindowsNT/95 and OS/2 always returns false\n    \"\"\"\n    return False\n\n# alias exists to lexists\nlexists = exists\n\n# Is a path a mount point?  Either a root (with or without drive letter)\n# or an UNC path with at most a / or \\ after the mount point.\n\ndef ismount(path):\n    \"\"\"Test whether a path is a mount point (defined as root of drive)\"\"\"\n    unc, rest = splitunc(path)\n    if unc:\n        return rest in (\"\", \"/\", \"\\\\\")\n    p = splitdrive(path)[1]\n    return len(p) == 1 and p[0] in '/\\\\'\n\n\n# Directory tree walk.\n# For each directory under top (including top itself, but excluding\n# '.' and '..'), func(arg, dirname, filenames) is called, where\n# dirname is the name of the directory and filenames is the list\n# of files (and subdirectories etc.) in the directory.\n# The func may modify the filenames list, to implement a filter,\n# or to impose a different order of visiting.\n\ndef walk(top, func, arg):\n    \"\"\"Directory tree walk with callback function.\n\n    For each directory in the directory tree rooted at top (including top\n    itself, but excluding '.' and '..'), call func(arg, dirname, fnames).\n    dirname is the name of the directory, and fnames a list of the names of\n    the files and subdirectories in dirname (excluding '.' and '..').  func\n    may modify the fnames list in-place (e.g. via del or slice assignment),\n    and walk will only recurse into the subdirectories whose names remain in\n    fnames; this can be used to implement a filter, or to impose a specific\n    order of visiting.  No semantics are defined for, or required of, arg,\n    beyond that arg is always passed to func.  It can be used, e.g., to pass\n    a filename pattern, or a mutable object designed to accumulate\n    statistics.  Passing None for arg is common.\"\"\"\n    warnings.warnpy3k(\"In 3.x, os.path.walk is removed in favor of os.walk.\",\n                      stacklevel=2)\n    try:\n        names = os.listdir(top)\n    except os.error:\n        return\n    func(arg, top, names)\n    for name in names:\n        name = join(top, name)\n        if isdir(name):\n            walk(name, func, arg)\n\n\n# Expand paths beginning with '~' or '~user'.\n# '~' means $HOME; '~user' means that user's home directory.\n# If the path doesn't begin with '~', or if the user or $HOME is unknown,\n# the path is returned unchanged (leaving error reporting to whatever\n# function is called with the expanded path as argument).\n# See also module 'glob' for expansion of *, ? and [...] in pathnames.\n# (A function should also be defined to do full *sh-style environment\n# variable expansion.)\n\ndef expanduser(path):\n    \"\"\"Expand ~ and ~user constructs.\n\n    If user or $HOME is unknown, do nothing.\"\"\"\n    if path[:1] != '~':\n        return path\n    i, n = 1, len(path)\n    while i < n and path[i] not in '/\\\\':\n        i = i + 1\n\n    if 'HOME' in os.environ:\n        userhome = os.environ['HOME']\n    elif 'USERPROFILE' in os.environ:\n        userhome = os.environ['USERPROFILE']\n    elif not 'HOMEPATH' in os.environ:\n        return path\n    else:\n        try:\n            drive = os.environ['HOMEDRIVE']\n        except KeyError:\n            drive = ''\n        userhome = join(drive, os.environ['HOMEPATH'])\n\n    if i != 1: #~user\n        userhome = join(dirname(userhome), path[1:i])\n\n    return userhome + path[i:]\n\n\n# Expand paths containing shell variable substitutions.\n# The following rules apply:\n#       - no expansion within single quotes\n#       - '$$' is translated into '$'\n#       - '%%' is translated into '%' if '%%' are not seen in %var1%%var2%\n#       - ${varname} is accepted.\n#       - $varname is accepted.\n#       - %varname% is accepted.\n#       - varnames can be made out of letters, digits and the characters '_-'\n#         (though is not verified in the ${varname} and %varname% cases)\n# XXX With COMMAND.COM you can use any characters in a variable name,\n# XXX except '^|<>='.\n\ndef expandvars(path):\n    \"\"\"Expand shell variables of the forms $var, ${var} and %var%.\n\n    Unknown variables are left unchanged.\"\"\"\n    if '$' not in path and '%' not in path:\n        return path\n    import string\n    varchars = string.ascii_letters + string.digits + '_-'\n    res = ''\n    index = 0\n    pathlen = len(path)\n    while index < pathlen:\n        c = path[index]\n        if c == '\\'':   # no expansion within single quotes\n            path = path[index + 1:]\n            pathlen = len(path)\n            try:\n                index = path.index('\\'')\n                res = res + '\\'' + path[:index + 1]\n            except ValueError:\n                res = res + path\n                index = pathlen - 1\n        elif c == '%':  # variable or '%'\n            if path[index + 1:index + 2] == '%':\n                res = res + c\n                index = index + 1\n            else:\n                path = path[index+1:]\n                pathlen = len(path)\n                try:\n                    index = path.index('%')\n                except ValueError:\n                    res = res + '%' + path\n                    index = pathlen - 1\n                else:\n                    var = path[:index]\n                    if var in os.environ:\n                        res = res + os.environ[var]\n                    else:\n                        res = res + '%' + var + '%'\n        elif c == '$':  # variable or '$$'\n            if path[index + 1:index + 2] == '$':\n                res = res + c\n                index = index + 1\n            elif path[index + 1:index + 2] == '{':\n                path = path[index+2:]\n                pathlen = len(path)\n                try:\n                    index = path.index('}')\n                    var = path[:index]\n                    if var in os.environ:\n                        res = res + os.environ[var]\n                    else:\n                        res = res + '${' + var + '}'\n                except ValueError:\n                    res = res + '${' + path\n                    index = pathlen - 1\n            else:\n                var = ''\n                index = index + 1\n                c = path[index:index + 1]\n                while c != '' and c in varchars:\n                    var = var + c\n                    index = index + 1\n                    c = path[index:index + 1]\n                if var in os.environ:\n                    res = res + os.environ[var]\n                else:\n                    res = res + '$' + var\n                if c != '':\n                    index = index - 1\n        else:\n            res = res + c\n        index = index + 1\n    return res\n\n\n# Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\\B.\n# Previously, this function also truncated pathnames to 8+3 format,\n# but as this module is called \"ntpath\", that's obviously wrong!\n\ndef normpath(path):\n    \"\"\"Normalize path, eliminating double slashes, etc.\"\"\"\n    # Preserve unicode (if path is unicode)\n    backslash, dot = (u'\\\\', u'.') if isinstance(path, unicode) else ('\\\\', '.')\n    if path.startswith(('\\\\\\\\.\\\\', '\\\\\\\\?\\\\')):\n        # in the case of paths with these prefixes:\n        # \\\\.\\ -> device names\n        # \\\\?\\ -> literal paths\n        # do not do any normalization, but return the path unchanged\n        return path\n    path = path.replace(\"/\", \"\\\\\")\n    prefix, path = splitdrive(path)\n    # We need to be careful here. If the prefix is empty, and the path starts\n    # with a backslash, it could either be an absolute path on the current\n    # drive (\\dir1\\dir2\\file) or a UNC filename (\\\\server\\mount\\dir1\\file). It\n    # is therefore imperative NOT to collapse multiple backslashes blindly in\n    # that case.\n    # The code below preserves multiple backslashes when there is no drive\n    # letter. This means that the invalid filename \\\\\\a\\b is preserved\n    # unchanged, where a\\\\\\b is normalised to a\\b. It's not clear that there\n    # is any better behaviour for such edge cases.\n    if prefix == '':\n        # No drive letter - preserve initial backslashes\n        while path[:1] == \"\\\\\":\n            prefix = prefix + backslash\n            path = path[1:]\n    else:\n        # We have a drive letter - collapse initial backslashes\n        if path.startswith(\"\\\\\"):\n            prefix = prefix + backslash\n            path = path.lstrip(\"\\\\\")\n    comps = path.split(\"\\\\\")\n    i = 0\n    while i < len(comps):\n        if comps[i] in ('.', ''):\n            del comps[i]\n        elif comps[i] == '..':\n            if i > 0 and comps[i-1] != '..':\n                del comps[i-1:i+1]\n                i -= 1\n            elif i == 0 and prefix.endswith(\"\\\\\"):\n                del comps[i]\n            else:\n                i += 1\n        else:\n            i += 1\n    # If the path is now empty, substitute '.'\n    if not prefix and not comps:\n        comps.append(dot)\n    return prefix + backslash.join(comps)\n\n\n# Return an absolute path.\ntry:\n    from nt import _getfullpathname\n\nexcept ImportError: # not running on Windows - mock up something sensible\n    def abspath(path):\n        \"\"\"Return the absolute version of a path.\"\"\"\n        if not isabs(path):\n            if isinstance(path, unicode):\n                cwd = os.getcwdu()\n            else:\n                cwd = os.getcwd()\n            path = join(cwd, path)\n        return normpath(path)\n\nelse:  # use native Windows method on Windows\n    def abspath(path):\n        \"\"\"Return the absolute version of a path.\"\"\"\n\n        if path: # Empty path must return current working directory.\n            try:\n                path = _getfullpathname(path)\n            except WindowsError:\n                pass # Bad path - return unchanged.\n        elif isinstance(path, unicode):\n            path = os.getcwdu()\n        else:\n            path = os.getcwd()\n        return normpath(path)\n\n# realpath is a no-op on systems without islink support\nrealpath = abspath\n# Win9x family and earlier have no Unicode filename support.\nsupports_unicode_filenames = (hasattr(sys, \"getwindowsversion\") and\n                              sys.getwindowsversion()[3] >= 2)\n\ndef _abspath_split(path):\n    abs = abspath(normpath(path))\n    prefix, rest = splitunc(abs)\n    is_unc = bool(prefix)\n    if not is_unc:\n        prefix, rest = splitdrive(abs)\n    return is_unc, prefix, [x for x in rest.split(sep) if x]\n\ndef relpath(path, start=curdir):\n    \"\"\"Return a relative version of a path\"\"\"\n\n    if not path:\n        raise ValueError(\"no path specified\")\n\n    start_is_unc, start_prefix, start_list = _abspath_split(start)\n    path_is_unc, path_prefix, path_list = _abspath_split(path)\n\n    if path_is_unc ^ start_is_unc:\n        raise ValueError(\"Cannot mix UNC and non-UNC paths (%s and %s)\"\n                                                            % (path, start))\n    if path_prefix.lower() != start_prefix.lower():\n        if path_is_unc:\n            raise ValueError(\"path is on UNC root %s, start on UNC root %s\"\n                                                % (path_prefix, start_prefix))\n        else:\n            raise ValueError(\"path is on drive %s, start on drive %s\"\n                                                % (path_prefix, start_prefix))\n    # Work out how much of the filepath is shared by start and path.\n    i = 0\n    for e1, e2 in zip(start_list, path_list):\n        if e1.lower() != e2.lower():\n            break\n        i += 1\n\n    rel_list = [pardir] * (len(start_list)-i) + path_list[i:]\n    if not rel_list:\n        return curdir\n    return join(*rel_list)\n\ntry:\n    # The genericpath.isdir implementation uses os.stat and checks the mode\n    # attribute to tell whether or not the path is a directory.\n    # This is overkill on Windows - just pass the path to GetFileAttributes\n    # and check the attribute from there.\n    from nt import _isdir as isdir\nexcept ImportError:\n    # Use genericpath.isdir as imported above.\n    pass\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/_version.py",
    "content": "__version__ = '1.8.2'\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/_weakref.py",
    "content": "\"\"\"\nYet another backport of WeakMethod for Python 2.7.\nChanges include removing exception chaining and adding args to super() calls.\n\nCopyright (c) 2001-2019 Python Software Foundation.All rights reserved.\n\nFull license available in LICENSE.python.\n\"\"\"\nfrom weakref import ref\n\n\nclass WeakMethod(ref):\n    \"\"\"\n    A custom `weakref.ref` subclass which simulates a weak reference to\n    a bound method, working around the lifetime problem of bound methods.\n    \"\"\"\n\n    __slots__ = \"_func_ref\", \"_meth_type\", \"_alive\", \"__weakref__\"\n\n    def __new__(cls, meth, callback=None):\n        try:\n            obj = meth.__self__\n            func = meth.__func__\n        except AttributeError:\n            raise TypeError(\n                \"argument should be a bound method, not {}\".format(type(meth))\n            )\n\n        def _cb(arg):\n            # The self-weakref trick is needed to avoid creating a reference\n            # cycle.\n            self = self_wr()\n            if self._alive:\n                self._alive = False\n                if callback is not None:\n                    callback(self)\n\n        self = ref.__new__(cls, obj, _cb)\n        self._func_ref = ref(func, _cb)\n        self._meth_type = type(meth)\n        self._alive = True\n        self_wr = ref(self)\n        return self\n\n    def __call__(self):\n        obj = super(WeakMethod, self).__call__()\n        func = self._func_ref()\n        if obj is None or func is None:\n            return None\n        return self._meth_type(func, obj)\n\n    def __eq__(self, other):\n        if isinstance(other, WeakMethod):\n            if not self._alive or not other._alive:\n                return self is other\n            return ref.__eq__(self, other) and self._func_ref == other._func_ref\n        return NotImplemented\n\n    def __ne__(self, other):\n        if isinstance(other, WeakMethod):\n            if not self._alive or not other._alive:\n                return self is not other\n            return ref.__ne__(self, other) or self._func_ref != other._func_ref\n        return NotImplemented\n\n    __hash__ = ref.__hash__\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/accessor/__init__.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/accessor/base.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2013 ftrack\n\nimport abc\n\nimport ftrack_api.exception\n\n\nclass Accessor(object):\n    '''Provide data access to a location.\n\n    A location represents a specific storage, but access to that storage may\n    vary. For example, both local filesystem and FTP access may be possible for\n    the same storage. An accessor implements these different ways of accessing\n    the same data location.\n\n    As different accessors may access the same location, only part of a data\n    path that is commonly understood may be stored in the database. The format\n    of this path should be a contract between the accessors that require access\n    to the same location and is left as an implementation detail. As such, this\n    system provides no guarantee that two different accessors can provide access\n    to the same location, though this is a clear goal. The path stored centrally\n    is referred to as the **resource identifier** and should be used when\n    calling any of the accessor methods that accept a *resource_identifier*\n    argument.\n\n    '''\n\n    __metaclass__ = abc.ABCMeta\n\n    def __init__(self):\n        '''Initialise location accessor.'''\n        super(Accessor, self).__init__()\n\n    @abc.abstractmethod\n    def list(self, resource_identifier):\n        '''Return list of entries in *resource_identifier* container.\n\n        Each entry in the returned list should be a valid resource identifier.\n\n        Raise :exc:`~ftrack_api.exception.AccessorResourceNotFoundError` if\n        *resource_identifier* does not exist or\n        :exc:`~ftrack_api.exception.AccessorResourceInvalidError` if\n        *resource_identifier* is not a container.\n\n        '''\n\n    @abc.abstractmethod\n    def exists(self, resource_identifier):\n        '''Return if *resource_identifier* is valid and exists in location.'''\n\n    @abc.abstractmethod\n    def is_file(self, resource_identifier):\n        '''Return whether *resource_identifier* refers to a file.'''\n\n    @abc.abstractmethod\n    def is_container(self, resource_identifier):\n        '''Return whether *resource_identifier* refers to a container.'''\n\n    @abc.abstractmethod\n    def is_sequence(self, resource_identifier):\n        '''Return whether *resource_identifier* refers to a file sequence.'''\n\n    @abc.abstractmethod\n    def open(self, resource_identifier, mode='rb'):\n        '''Return :class:`~ftrack_api.data.Data` for *resource_identifier*.'''\n\n    @abc.abstractmethod\n    def remove(self, resource_identifier):\n        '''Remove *resource_identifier*.\n\n        Raise :exc:`~ftrack_api.exception.AccessorResourceNotFoundError` if\n        *resource_identifier* does not exist.\n\n        '''\n\n    @abc.abstractmethod\n    def make_container(self, resource_identifier, recursive=True):\n        '''Make a container at *resource_identifier*.\n\n        If *recursive* is True, also make any intermediate containers.\n\n        Should silently ignore existing containers and not recreate them.\n\n        '''\n\n    @abc.abstractmethod\n    def get_container(self, resource_identifier):\n        '''Return resource_identifier of container for *resource_identifier*.\n\n        Raise :exc:`~ftrack_api.exception.AccessorParentResourceNotFoundError`\n        if container of *resource_identifier* could not be determined.\n\n        '''\n\n    def remove_container(self, resource_identifier):  # pragma: no cover\n        '''Remove container at *resource_identifier*.'''\n        return self.remove(resource_identifier)\n\n    def get_filesystem_path(self, resource_identifier):  # pragma: no cover\n        '''Return filesystem path for *resource_identifier*.\n\n        Raise :exc:`~ftrack_api.exception.AccessorFilesystemPathError` if\n        filesystem path could not be determined from *resource_identifier* or\n        :exc:`~ftrack_api.exception.AccessorUnsupportedOperationError` if\n        retrieving filesystem paths is not supported by this accessor.\n\n        '''\n        raise ftrack_api.exception.AccessorUnsupportedOperationError(\n            'get_filesystem_path', resource_identifier=resource_identifier\n        )\n\n    def get_url(self, resource_identifier):\n        '''Return URL for *resource_identifier*.\n\n        Raise :exc:`~ftrack_api.exception.AccessorFilesystemPathError` if\n        URL could not be determined from *resource_identifier* or\n        :exc:`~ftrack_api.exception.AccessorUnsupportedOperationError` if\n        retrieving URL is not supported by this accessor.\n\n        '''\n        raise ftrack_api.exception.AccessorUnsupportedOperationError(\n            'get_url', resource_identifier=resource_identifier\n        )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/accessor/disk.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2013 ftrack\n\nimport os\nimport sys\nimport errno\nimport contextlib\n\nimport ftrack_api._python_ntpath as ntpath\nimport ftrack_api.accessor.base\nimport ftrack_api.data\nfrom ftrack_api.exception import (\n    AccessorFilesystemPathError,\n    AccessorUnsupportedOperationError,\n    AccessorResourceNotFoundError,\n    AccessorOperationFailedError,\n    AccessorPermissionDeniedError,\n    AccessorResourceInvalidError,\n    AccessorContainerNotEmptyError,\n    AccessorParentResourceNotFoundError\n)\n\n\nclass DiskAccessor(ftrack_api.accessor.base.Accessor):\n    '''Provide disk access to a location.\n\n    Expect resource identifiers to refer to relative filesystem paths.\n\n    '''\n\n    def __init__(self, prefix, **kw):\n        '''Initialise location accessor.\n\n        *prefix* specifies the base folder for the disk based structure and\n        will be prepended to any path. It should be specified in the syntax of\n        the current OS.\n\n        '''\n        if prefix:\n            prefix = os.path.expanduser(os.path.expandvars(prefix))\n            prefix = os.path.abspath(prefix)\n        self.prefix = prefix\n\n        super(DiskAccessor, self).__init__(**kw)\n\n    def list(self, resource_identifier):\n        '''Return list of entries in *resource_identifier* container.\n\n        Each entry in the returned list should be a valid resource identifier.\n\n        Raise :exc:`~ftrack_api.exception.AccessorResourceNotFoundError` if\n        *resource_identifier* does not exist or\n        :exc:`~ftrack_api.exception.AccessorResourceInvalidError` if\n        *resource_identifier* is not a container.\n\n        '''\n        filesystem_path = self.get_filesystem_path(resource_identifier)\n\n        with error_handler(\n            operation='list', resource_identifier=resource_identifier\n        ):\n            listing = []\n            for entry in os.listdir(filesystem_path):\n                listing.append(os.path.join(resource_identifier, entry))\n\n        return listing\n\n    def exists(self, resource_identifier):\n        '''Return if *resource_identifier* is valid and exists in location.'''\n        filesystem_path = self.get_filesystem_path(resource_identifier)\n        return os.path.exists(filesystem_path)\n\n    def is_file(self, resource_identifier):\n        '''Return whether *resource_identifier* refers to a file.'''\n        filesystem_path = self.get_filesystem_path(resource_identifier)\n        return os.path.isfile(filesystem_path)\n\n    def is_container(self, resource_identifier):\n        '''Return whether *resource_identifier* refers to a container.'''\n        filesystem_path = self.get_filesystem_path(resource_identifier)\n        return os.path.isdir(filesystem_path)\n\n    def is_sequence(self, resource_identifier):\n        '''Return whether *resource_identifier* refers to a file sequence.'''\n        raise AccessorUnsupportedOperationError(operation='is_sequence')\n\n    def open(self, resource_identifier, mode='rb'):\n        '''Return :class:`~ftrack_api.Data` for *resource_identifier*.'''\n        filesystem_path = self.get_filesystem_path(resource_identifier)\n\n        with error_handler(\n            operation='open', resource_identifier=resource_identifier\n        ):\n            data = ftrack_api.data.File(filesystem_path, mode)\n\n        return data\n\n    def remove(self, resource_identifier):\n        '''Remove *resource_identifier*.\n\n        Raise :exc:`~ftrack_api.exception.AccessorResourceNotFoundError` if\n        *resource_identifier* does not exist.\n\n        '''\n        filesystem_path = self.get_filesystem_path(resource_identifier)\n\n        if self.is_file(resource_identifier):\n            with error_handler(\n                operation='remove', resource_identifier=resource_identifier\n            ):\n                os.remove(filesystem_path)\n\n        elif self.is_container(resource_identifier):\n            with error_handler(\n                operation='remove', resource_identifier=resource_identifier\n            ):\n                os.rmdir(filesystem_path)\n\n        else:\n            raise AccessorResourceNotFoundError(\n                resource_identifier=resource_identifier\n            )\n\n    def make_container(self, resource_identifier, recursive=True):\n        '''Make a container at *resource_identifier*.\n\n        If *recursive* is True, also make any intermediate containers.\n\n        '''\n        filesystem_path = self.get_filesystem_path(resource_identifier)\n\n        with error_handler(\n            operation='makeContainer', resource_identifier=resource_identifier\n        ):\n            try:\n                if recursive:\n                    os.makedirs(filesystem_path)\n                else:\n                    try:\n                        os.mkdir(filesystem_path)\n                    except OSError as error:\n                        if error.errno == errno.ENOENT:\n                            raise AccessorParentResourceNotFoundError(\n                                resource_identifier=resource_identifier\n                            )\n                        else:\n                            raise\n\n            except OSError, error:\n                if error.errno != errno.EEXIST:\n                    raise\n\n    def get_container(self, resource_identifier):\n        '''Return resource_identifier of container for *resource_identifier*.\n\n        Raise :exc:`~ftrack_api.exception.AccessorParentResourceNotFoundError` if\n        container of *resource_identifier* could not be determined.\n\n        '''\n        filesystem_path = self.get_filesystem_path(resource_identifier)\n\n        container = os.path.dirname(filesystem_path)\n\n        if self.prefix:\n            if not container.startswith(self.prefix):\n                raise AccessorParentResourceNotFoundError(\n                    resource_identifier=resource_identifier,\n                    message='Could not determine container for '\n                            '{resource_identifier} as container falls outside '\n                            'of configured prefix.'\n                )\n\n            # Convert container filesystem path into resource identifier.\n            container = container[len(self.prefix):]\n            if ntpath.isabs(container):\n                # Ensure that resulting path is relative by stripping any\n                # leftover prefixed slashes from string.\n                # E.g. If prefix was '/tmp' and path was '/tmp/foo/bar' the\n                # result will be 'foo/bar'.\n                container = container.lstrip('\\\\/')\n\n        return container\n\n    def get_filesystem_path(self, resource_identifier):\n        '''Return filesystem path for *resource_identifier*.\n\n        For example::\n\n            >>> accessor = DiskAccessor('my.location', '/mountpoint')\n            >>> print accessor.get_filesystem_path('test.txt')\n            /mountpoint/test.txt\n            >>> print accessor.get_filesystem_path('/mountpoint/test.txt')\n            /mountpoint/test.txt\n\n        Raise :exc:`ftrack_api.exception.AccessorFilesystemPathError` if filesystem\n        path could not be determined from *resource_identifier*.\n\n        '''\n        filesystem_path = resource_identifier\n        if filesystem_path:\n            filesystem_path = os.path.normpath(filesystem_path)\n\n        if self.prefix:\n            if not os.path.isabs(filesystem_path):\n                filesystem_path = os.path.normpath(\n                    os.path.join(self.prefix, filesystem_path)\n                )\n\n            if not filesystem_path.startswith(self.prefix):\n                raise AccessorFilesystemPathError(\n                    resource_identifier=resource_identifier,\n                    message='Could not determine access path for '\n                            'resource_identifier outside of configured prefix: '\n                            '{resource_identifier}.'\n                )\n\n        return filesystem_path\n\n\n@contextlib.contextmanager\ndef error_handler(**kw):\n    '''Conform raised OSError/IOError exception to appropriate FTrack error.'''\n    try:\n        yield\n\n    except (OSError, IOError) as error:\n        (exception_type, exception_value, traceback) = sys.exc_info()\n        kw.setdefault('error', error)\n\n        error_code = getattr(error, 'errno')\n        if not error_code:\n            raise AccessorOperationFailedError(**kw), None, traceback\n\n        if error_code == errno.ENOENT:\n            raise AccessorResourceNotFoundError(**kw), None, traceback\n\n        elif error_code == errno.EPERM:\n            raise AccessorPermissionDeniedError(**kw), None, traceback\n\n        elif error_code == errno.ENOTEMPTY:\n            raise AccessorContainerNotEmptyError(**kw), None, traceback\n\n        elif error_code in (errno.ENOTDIR, errno.EISDIR, errno.EINVAL):\n            raise AccessorResourceInvalidError(**kw), None, traceback\n\n        else:\n            raise AccessorOperationFailedError(**kw), None, traceback\n\n    except Exception:\n        raise\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/accessor/server.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport os\nimport hashlib\nimport base64\nimport json\n\nimport requests\n\nfrom .base import Accessor\nfrom ..data import String\nimport ftrack_api.exception\nimport ftrack_api.symbol\n\n\nclass ServerFile(String):\n    '''Representation of a server file.'''\n\n    def __init__(self, resource_identifier, session, mode='rb'):\n        '''Initialise file.'''\n        self.mode = mode\n        self.resource_identifier = resource_identifier\n        self._session = session\n        self._has_read = False\n\n        super(ServerFile, self).__init__()\n\n    def flush(self):\n        '''Flush all changes.'''\n        super(ServerFile, self).flush()\n\n        if self.mode == 'wb':\n            self._write()\n\n    def read(self, limit=None):\n        '''Read file.'''\n        if not self._has_read:\n            self._read()\n            self._has_read = True\n\n        return super(ServerFile, self).read(limit)\n\n    def _read(self):\n        '''Read all remote content from key into wrapped_file.'''\n        position = self.tell()\n        self.seek(0)\n\n        response = requests.get(\n            '{0}/component/get'.format(self._session.server_url),\n            params={\n                'id': self.resource_identifier,\n                'username': self._session.api_user,\n                'apiKey': self._session.api_key\n            },\n            stream=True\n        )\n\n        try:\n            response.raise_for_status()\n        except requests.exceptions.HTTPError as error:\n            raise ftrack_api.exception.AccessorOperationFailedError(\n                'Failed to read data: {0}.'.format(error)\n            )\n\n        for block in response.iter_content(ftrack_api.symbol.CHUNK_SIZE):\n            self.wrapped_file.write(block)\n\n        self.flush()\n        self.seek(position)\n\n    def _write(self):\n        '''Write current data to remote key.'''\n        position = self.tell()\n        self.seek(0)\n\n        # Retrieve component from cache to construct a filename.\n        component = self._session.get('FileComponent', self.resource_identifier)\n        if not component:\n            raise ftrack_api.exception.AccessorOperationFailedError(\n                'Unable to retrieve component with id: {0}.'.format(\n                    self.resource_identifier\n                )\n            )\n\n        # Construct a name from component name and file_type.\n        name = component['name']\n        if component['file_type']:\n            name = u'{0}.{1}'.format(\n                name,\n                component['file_type'].lstrip('.')\n            )\n\n        try:\n            metadata = self._session.get_upload_metadata(\n                component_id=self.resource_identifier,\n                file_name=name,\n                file_size=self._get_size(),\n                checksum=self._compute_checksum()\n            )\n        except Exception as error:\n            raise ftrack_api.exception.AccessorOperationFailedError(\n                'Failed to get put metadata: {0}.'.format(error)\n            )\n\n        # Ensure at beginning of file before put.\n        self.seek(0)\n\n        # Put the file based on the metadata.\n        response = requests.put(\n            metadata['url'],\n            data=self.wrapped_file,\n            headers=metadata['headers']\n        )\n\n        try:\n            response.raise_for_status()\n        except requests.exceptions.HTTPError as error:\n            raise ftrack_api.exception.AccessorOperationFailedError(\n                'Failed to put file to server: {0}.'.format(error)\n            )\n\n        self.seek(position)\n\n    def _get_size(self):\n        '''Return size of file in bytes.'''\n        position = self.tell()\n        self.seek(0, os.SEEK_END)\n        length = self.tell()\n        self.seek(position)\n        return length\n\n    def _compute_checksum(self):\n        '''Return checksum for file.'''\n        fp = self.wrapped_file\n        buf_size = ftrack_api.symbol.CHUNK_SIZE\n        hash_obj = hashlib.md5()\n        spos = fp.tell()\n\n        s = fp.read(buf_size)\n        while s:\n            hash_obj.update(s)\n            s = fp.read(buf_size)\n\n        base64_digest = base64.encodestring(hash_obj.digest())\n        if base64_digest[-1] == '\\n':\n            base64_digest = base64_digest[0:-1]\n\n        fp.seek(spos)\n        return base64_digest\n\n\nclass _ServerAccessor(Accessor):\n    '''Provide server location access.'''\n\n    def __init__(self, session, **kw):\n        '''Initialise location accessor.'''\n        super(_ServerAccessor, self).__init__(**kw)\n\n        self._session = session\n\n    def open(self, resource_identifier, mode='rb'):\n        '''Return :py:class:`~ftrack_api.Data` for *resource_identifier*.'''\n        return ServerFile(resource_identifier, session=self._session, mode=mode)\n\n    def remove(self, resourceIdentifier):\n        '''Remove *resourceIdentifier*.'''\n        response = requests.get(\n            '{0}/component/remove'.format(self._session.server_url),\n            params={\n                'id': resourceIdentifier,\n                'username': self._session.api_user,\n                'apiKey': self._session.api_key\n            }\n        )\n        if response.status_code != 200:\n            raise ftrack_api.exception.AccessorOperationFailedError(\n                'Failed to remove file.'\n            )\n\n    def get_container(self, resource_identifier):\n        '''Return resource_identifier of container for *resource_identifier*.'''\n        return None\n\n    def make_container(self, resource_identifier, recursive=True):\n        '''Make a container at *resource_identifier*.'''\n\n    def list(self, resource_identifier):\n        '''Return list of entries in *resource_identifier* container.'''\n        raise NotImplementedError()\n\n    def exists(self, resource_identifier):\n        '''Return if *resource_identifier* is valid and exists in location.'''\n        return False\n\n    def is_file(self, resource_identifier):\n        '''Return whether *resource_identifier* refers to a file.'''\n        raise NotImplementedError()\n\n    def is_container(self, resource_identifier):\n        '''Return whether *resource_identifier* refers to a container.'''\n        raise NotImplementedError()\n\n    def is_sequence(self, resource_identifier):\n        '''Return whether *resource_identifier* refers to a file sequence.'''\n        raise NotImplementedError()\n\n    def get_url(self, resource_identifier):\n        '''Return url for *resource_identifier*.'''\n        url_string = (\n            u'{url}/component/get?id={id}&username={username}'\n            u'&apiKey={apiKey}'\n        )\n        return url_string.format(\n            url=self._session.server_url,\n            id=resource_identifier,\n            username=self._session.api_user,\n            apiKey=self._session.api_key\n        )\n\n    def get_thumbnail_url(self, resource_identifier, size=None):\n        '''Return thumbnail url for *resource_identifier*.\n\n        Optionally, specify *size* to constrain the downscaled image to size\n        x size pixels.\n        '''\n        url_string = (\n            u'{url}/component/thumbnail?id={id}&username={username}'\n            u'&apiKey={apiKey}'\n        )\n        url = url_string.format(\n            url=self._session.server_url,\n            id=resource_identifier,\n            username=self._session.api_user,\n            apiKey=self._session.api_key\n        )\n        if size:\n            url += u'&size={0}'.format(size)\n\n        return url\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/attribute.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nfrom __future__ import absolute_import\n\nimport collections\nimport copy\nimport logging\nimport functools\n\nimport ftrack_api.symbol\nimport ftrack_api.exception\nimport ftrack_api.collection\nimport ftrack_api.inspection\nimport ftrack_api.operation\n\nlogger = logging.getLogger(\n    __name__\n)\n\n\ndef merge_references(function):\n    '''Decorator to handle merging of references / collections.'''\n\n    @functools.wraps(function)\n    def get_value(attribute, entity):\n        '''Merge the attribute with the local cache.'''\n\n        if attribute.name not in entity._inflated:\n            # Only merge on first access to avoid\n            # inflating them multiple times.\n\n            logger.debug(\n                'Merging potential new data into attached '\n                'entity for attribute {0}.'.format(\n                    attribute.name\n                )\n            )\n\n            # Local attributes.\n            local_value = attribute.get_local_value(entity)\n            if isinstance(\n                local_value,\n                (\n                    ftrack_api.entity.base.Entity,\n                    ftrack_api.collection.Collection,\n                    ftrack_api.collection.MappedCollectionProxy\n                )\n            ):\n                logger.debug(\n                    'Merging local value for attribute {0}.'.format(attribute)\n                )\n\n                merged_local_value = entity.session._merge(\n                    local_value, merged=dict()\n                )\n\n                if merged_local_value is not local_value:\n                    with entity.session.operation_recording(False):\n                        attribute.set_local_value(entity, merged_local_value)\n\n            # Remote attributes.\n            remote_value = attribute.get_remote_value(entity)\n            if isinstance(\n                remote_value,\n                (\n                    ftrack_api.entity.base.Entity,\n                    ftrack_api.collection.Collection,\n                    ftrack_api.collection.MappedCollectionProxy\n                )\n            ):\n                logger.debug(\n                    'Merging remote value for attribute {0}.'.format(attribute)\n                )\n\n                merged_remote_value = entity.session._merge(\n                    remote_value, merged=dict()\n                )\n\n                if merged_remote_value is not remote_value:\n                    attribute.set_remote_value(entity, merged_remote_value)\n\n            entity._inflated.add(\n                attribute.name\n            )\n\n        return function(\n            attribute, entity\n        )\n\n    return get_value\n\n\nclass Attributes(object):\n    '''Collection of properties accessible by name.'''\n\n    def __init__(self, attributes=None):\n        super(Attributes, self).__init__()\n        self._data = dict()\n        if attributes is not None:\n            for attribute in attributes:\n                self.add(attribute)\n\n    def add(self, attribute):\n        '''Add *attribute*.'''\n        existing = self._data.get(attribute.name, None)\n        if existing:\n            raise ftrack_api.exception.NotUniqueError(\n                'Attribute with name {0} already added as {1}'\n                .format(attribute.name, existing)\n            )\n\n        self._data[attribute.name] = attribute\n\n    def remove(self, attribute):\n        '''Remove attribute.'''\n        self._data.pop(attribute.name)\n\n    def get(self, name):\n        '''Return attribute by *name*.\n\n        If no attribute matches *name* then return None.\n\n        '''\n        return self._data.get(name, None)\n\n    def keys(self):\n        '''Return list of attribute names.'''\n        return self._data.keys()\n\n    def __contains__(self, item):\n        '''Return whether *item* present.'''\n        if not isinstance(item, Attribute):\n            return False\n\n        return item.name in self._data\n\n    def __iter__(self):\n        '''Return iterator over attributes.'''\n        return self._data.itervalues()\n\n    def __len__(self):\n        '''Return count of attributes.'''\n        return len(self._data)\n\n\nclass Attribute(object):\n    '''A name and value pair persisted remotely.'''\n\n    def __init__(\n        self, name, default_value=ftrack_api.symbol.NOT_SET, mutable=True,\n        computed=False\n    ):\n        '''Initialise attribute with *name*.\n\n        *default_value* represents the default value for the attribute. It may\n        be a callable. It is not used within the attribute when providing\n        values, but instead exists for other parts of the system to reference.\n\n        If *mutable* is set to False then the local value of the attribute on an\n        entity can only be set when both the existing local and remote values\n        are :attr:`ftrack_api.symbol.NOT_SET`. The exception to this is when the\n        target value is also :attr:`ftrack_api.symbol.NOT_SET`.\n\n        If *computed* is set to True the value is a remote side computed value\n        and should not be long-term cached.\n\n        '''\n        super(Attribute, self).__init__()\n        self._name = name\n        self._mutable = mutable\n        self._computed = computed\n        self.default_value = default_value\n\n        self._local_key = 'local'\n        self._remote_key = 'remote'\n\n    def __repr__(self):\n        '''Return representation of entity.'''\n        return '<{0}.{1}({2}) object at {3}>'.format(\n            self.__module__,\n            self.__class__.__name__,\n            self.name,\n            id(self)\n        )\n\n    def get_entity_storage(self, entity):\n        '''Return attribute storage on *entity* creating if missing.'''\n        storage_key = '_ftrack_attribute_storage'\n        storage = getattr(entity, storage_key, None)\n        if storage is None:\n            storage = collections.defaultdict(\n                lambda:\n                {\n                    self._local_key: ftrack_api.symbol.NOT_SET,\n                    self._remote_key: ftrack_api.symbol.NOT_SET\n                }\n            )\n            setattr(entity, storage_key, storage)\n\n        return storage\n\n    @property\n    def name(self):\n        '''Return name.'''\n        return self._name\n\n    @property\n    def mutable(self):\n        '''Return whether attribute is mutable.'''\n        return self._mutable\n\n    @property\n    def computed(self):\n        '''Return whether attribute is computed.'''\n        return self._computed\n\n    def get_value(self, entity):\n        '''Return current value for *entity*.\n\n        If a value was set locally then return it, otherwise return last known\n        remote value. If no remote value yet retrieved, make a request for it\n        via the session and block until available.\n\n        '''\n        value = self.get_local_value(entity)\n        if value is not ftrack_api.symbol.NOT_SET:\n            return value\n\n        value = self.get_remote_value(entity)\n        if value is not ftrack_api.symbol.NOT_SET:\n            return value\n\n        if not entity.session.auto_populate:\n            return value\n\n        self.populate_remote_value(entity)\n        return self.get_remote_value(entity)\n\n    def get_local_value(self, entity):\n        '''Return locally set value for *entity*.'''\n        storage = self.get_entity_storage(entity)\n        return storage[self.name][self._local_key]\n\n    def get_remote_value(self, entity):\n        '''Return remote value for *entity*.\n\n        .. note::\n\n            Only return locally stored remote value, do not fetch from remote.\n\n        '''\n        storage = self.get_entity_storage(entity)\n        return storage[self.name][self._remote_key]\n\n    def set_local_value(self, entity, value):\n        '''Set local *value* for *entity*.'''\n        if (\n            not self.mutable\n            and self.is_set(entity)\n            and value is not ftrack_api.symbol.NOT_SET\n        ):\n            raise ftrack_api.exception.ImmutableAttributeError(self)\n\n        old_value = self.get_local_value(entity)\n\n        storage = self.get_entity_storage(entity)\n        storage[self.name][self._local_key] = value\n\n        # Record operation.\n        if entity.session.record_operations:\n            entity.session.recorded_operations.push(\n                ftrack_api.operation.UpdateEntityOperation(\n                    entity.entity_type,\n                    ftrack_api.inspection.primary_key(entity),\n                    self.name,\n                    old_value,\n                    value\n                )\n            )\n\n    def set_remote_value(self, entity, value):\n        '''Set remote *value*.\n\n        .. note::\n\n            Only set locally stored remote value, do not persist to remote.\n\n        '''\n        storage = self.get_entity_storage(entity)\n        storage[self.name][self._remote_key] = value\n\n    def populate_remote_value(self, entity):\n        '''Populate remote value for *entity*.'''\n        entity.session.populate([entity], self.name)\n\n    def is_modified(self, entity):\n        '''Return whether local value set and differs from remote.\n\n        .. note::\n\n            Will not fetch remote value so may report True even when values\n            are the same on the remote.\n\n        '''\n        local_value = self.get_local_value(entity)\n        remote_value = self.get_remote_value(entity)\n        return (\n            local_value is not ftrack_api.symbol.NOT_SET\n            and local_value != remote_value\n        )\n\n    def is_set(self, entity):\n        '''Return whether a value is set for *entity*.'''\n        return any([\n            self.get_local_value(entity) is not ftrack_api.symbol.NOT_SET,\n            self.get_remote_value(entity) is not ftrack_api.symbol.NOT_SET\n        ])\n\n\nclass ScalarAttribute(Attribute):\n    '''Represent a scalar value.'''\n\n    def __init__(self, name, data_type, **kw):\n        '''Initialise property.'''\n        super(ScalarAttribute, self).__init__(name, **kw)\n        self.data_type = data_type\n\n\nclass ReferenceAttribute(Attribute):\n    '''Reference another entity.'''\n\n    def __init__(self, name, entity_type, **kw):\n        '''Initialise property.'''\n        super(ReferenceAttribute, self).__init__(name, **kw)\n        self.entity_type = entity_type\n\n    def populate_remote_value(self, entity):\n        '''Populate remote value for *entity*.\n\n        As attribute references another entity, use that entity's configured\n        default projections to auto populate useful attributes when loading.\n\n        '''\n        reference_entity_type = entity.session.types[self.entity_type]\n        default_projections = reference_entity_type.default_projections\n\n        projections = []\n        if default_projections:\n            for projection in default_projections:\n                projections.append('{0}.{1}'.format(self.name, projection))\n        else:\n            projections.append(self.name)\n\n        entity.session.populate([entity], ', '.join(projections))\n\n    def is_modified(self, entity):\n        '''Return whether a local value has been set and differs from remote.\n\n        .. note::\n\n            Will not fetch remote value so may report True even when values\n            are the same on the remote.\n\n        '''\n        local_value = self.get_local_value(entity)\n        remote_value = self.get_remote_value(entity)\n\n        if local_value is ftrack_api.symbol.NOT_SET:\n            return False\n\n        if remote_value is ftrack_api.symbol.NOT_SET:\n            return True\n\n        if (\n            ftrack_api.inspection.identity(local_value)\n            != ftrack_api.inspection.identity(remote_value)\n        ):\n            return True\n\n        return False\n\n\n    @merge_references\n    def get_value(self, entity):\n        return super(ReferenceAttribute, self).get_value(\n            entity\n        )\n\nclass AbstractCollectionAttribute(Attribute):\n    '''Base class for collection attributes.'''\n\n    #: Collection class used by attribute.\n    collection_class = None\n\n    @merge_references\n    def get_value(self, entity):\n        '''Return current value for *entity*.\n\n        If a value was set locally then return it, otherwise return last known\n        remote value. If no remote value yet retrieved, make a request for it\n        via the session and block until available.\n\n        .. note::\n\n            As value is a collection that is mutable, will transfer a remote\n            value into the local value on access if no local value currently\n            set.\n\n        '''\n        super(AbstractCollectionAttribute, self).get_value(entity)\n\n        # Conditionally, copy remote value into local value so that it can be\n        # mutated without side effects.\n        local_value = self.get_local_value(entity)\n        remote_value = self.get_remote_value(entity)\n        if (\n            local_value is ftrack_api.symbol.NOT_SET\n            and isinstance(remote_value, self.collection_class)\n        ):\n            try:\n                with entity.session.operation_recording(False):\n                    self.set_local_value(entity, copy.copy(remote_value))\n            except ftrack_api.exception.ImmutableAttributeError:\n                pass\n\n        value = self.get_local_value(entity)\n\n        # If the local value is still not set then attempt to set it with a\n        # suitable placeholder collection so that the caller can interact with\n        # the collection using its normal interface. This is required for a\n        # newly created entity for example. It *could* be done as a simple\n        # default value, but that would incur cost for every collection even\n        # when they are not modified before commit.\n        if value is ftrack_api.symbol.NOT_SET:\n            try:\n                with entity.session.operation_recording(False):\n                    self.set_local_value(\n                        entity,\n                        # None should be treated as empty collection.\n                        None\n                    )\n            except ftrack_api.exception.ImmutableAttributeError:\n                pass\n\n        return self.get_local_value(entity)\n\n    def set_local_value(self, entity, value):\n        '''Set local *value* for *entity*.'''\n        if value is not ftrack_api.symbol.NOT_SET:\n            value = self._adapt_to_collection(entity, value)\n            value.mutable = self.mutable\n\n        super(AbstractCollectionAttribute, self).set_local_value(entity, value)\n\n    def set_remote_value(self, entity, value):\n        '''Set remote *value*.\n\n        .. note::\n\n            Only set locally stored remote value, do not persist to remote.\n\n        '''\n        if value is not ftrack_api.symbol.NOT_SET:\n            value = self._adapt_to_collection(entity, value)\n            value.mutable = False\n\n        super(AbstractCollectionAttribute, self).set_remote_value(entity, value)\n\n    def _adapt_to_collection(self, entity, value):\n        '''Adapt *value* to appropriate collection instance for *entity*.\n\n        .. note::\n\n            If *value* is None then return a suitable empty collection.\n\n        '''\n        raise NotImplementedError()\n\n\nclass CollectionAttribute(AbstractCollectionAttribute):\n    '''Represent a collection of other entities.'''\n\n    #: Collection class used by attribute.\n    collection_class = ftrack_api.collection.Collection\n\n    def _adapt_to_collection(self, entity, value):\n        '''Adapt *value* to a Collection instance on *entity*.'''\n\n        if not isinstance(value, ftrack_api.collection.Collection):\n\n            if value is None:\n                value = ftrack_api.collection.Collection(entity, self)\n\n            elif isinstance(value, list):\n                value = ftrack_api.collection.Collection(\n                    entity, self, data=value\n                )\n\n            else:\n                raise NotImplementedError(\n                    'Cannot convert {0!r} to collection.'.format(value)\n                )\n\n        else:\n            if value.attribute is not self:\n                raise ftrack_api.exception.AttributeError(\n                    'Collection already bound to a different attribute'\n                )\n\n        return value\n\n\nclass KeyValueMappedCollectionAttribute(AbstractCollectionAttribute):\n    '''Represent a mapped key, value collection of entities.'''\n\n    #: Collection class used by attribute.\n    collection_class = ftrack_api.collection.KeyValueMappedCollectionProxy\n\n    def __init__(\n        self, name, creator, key_attribute, value_attribute, **kw\n    ):\n        '''Initialise attribute with *name*.\n\n        *creator* should be a function that accepts a dictionary of data and\n        is used by the referenced collection to create new entities in the\n        collection.\n\n        *key_attribute* should be the name of the attribute on an entity in\n        the collection that represents the value for 'key' of the dictionary.\n\n        *value_attribute* should be the name of the attribute on an entity in\n        the collection that represents the value for 'value' of the dictionary.\n\n        '''\n        self.creator = creator\n        self.key_attribute = key_attribute\n        self.value_attribute = value_attribute\n\n        super(KeyValueMappedCollectionAttribute, self).__init__(name, **kw)\n\n    def _adapt_to_collection(self, entity, value):\n        '''Adapt *value* to an *entity*.'''\n        if not isinstance(\n            value, ftrack_api.collection.KeyValueMappedCollectionProxy\n        ):\n\n            if value is None:\n                value = ftrack_api.collection.KeyValueMappedCollectionProxy(\n                    ftrack_api.collection.Collection(entity, self),\n                    self.creator, self.key_attribute,\n                    self.value_attribute\n                )\n\n            elif isinstance(value, (list, ftrack_api.collection.Collection)):\n\n                if isinstance(value, list):\n                    value = ftrack_api.collection.Collection(\n                        entity, self, data=value\n                    )\n\n                value = ftrack_api.collection.KeyValueMappedCollectionProxy(\n                    value, self.creator, self.key_attribute,\n                    self.value_attribute\n                )\n\n            elif isinstance(value, collections.Mapping):\n                # Convert mapping.\n                # TODO: When backend model improves, revisit this logic.\n                # First get existing value and delete all references. This is\n                # needed because otherwise they will not be automatically\n                # removed server side.\n                # The following should not cause recursion as the internal\n                # values should be mapped collections already.\n                current_value = self.get_value(entity)\n                if not isinstance(\n                    current_value,\n                    ftrack_api.collection.KeyValueMappedCollectionProxy\n                ):\n                    raise NotImplementedError(\n                        'Cannot adapt mapping to collection as current value '\n                        'type is not a KeyValueMappedCollectionProxy.'\n                    )\n\n                # Create the new collection using the existing collection as\n                # basis. Then update through proxy interface to ensure all\n                # internal operations called consistently (such as entity\n                # deletion for key removal).\n                collection = ftrack_api.collection.Collection(\n                    entity, self, data=current_value.collection[:]\n                )\n                collection_proxy = (\n                    ftrack_api.collection.KeyValueMappedCollectionProxy(\n                        collection, self.creator,\n                        self.key_attribute, self.value_attribute\n                    )\n                )\n\n                # Remove expired keys from collection.\n                expired_keys = set(current_value.keys()) - set(value.keys())\n                for key in expired_keys:\n                    del collection_proxy[key]\n\n                # Set new values for existing keys / add new keys.\n                for key, value in value.items():\n                    collection_proxy[key] = value\n\n                value = collection_proxy\n\n            else:\n                raise NotImplementedError(\n                    'Cannot convert {0!r} to collection.'.format(value)\n                )\n        else:\n            if value.attribute is not self:\n                raise ftrack_api.exception.AttributeError(\n                    'Collection already bound to a different attribute.'\n                )\n\n        return value\n\n\nclass CustomAttributeCollectionAttribute(AbstractCollectionAttribute):\n    '''Represent a mapped custom attribute collection of entities.'''\n\n    #: Collection class used by attribute.\n    collection_class = (\n        ftrack_api.collection.CustomAttributeCollectionProxy\n    )\n\n    def _adapt_to_collection(self, entity, value):\n        '''Adapt *value* to an *entity*.'''\n        if not isinstance(\n            value, ftrack_api.collection.CustomAttributeCollectionProxy\n        ):\n\n            if value is None:\n                value = ftrack_api.collection.CustomAttributeCollectionProxy(\n                    ftrack_api.collection.Collection(entity, self)\n                )\n\n            elif isinstance(value, (list, ftrack_api.collection.Collection)):\n\n                # Why are we creating a new if it is a list? This will cause\n                # any merge to create a new proxy and collection.\n                if isinstance(value, list):\n                    value = ftrack_api.collection.Collection(\n                        entity, self, data=value\n                    )\n\n                value = ftrack_api.collection.CustomAttributeCollectionProxy(\n                    value\n                )\n\n            elif isinstance(value, collections.Mapping):\n                # Convert mapping.\n                # TODO: When backend model improves, revisit this logic.\n                # First get existing value and delete all references. This is\n                # needed because otherwise they will not be automatically\n                # removed server side.\n                # The following should not cause recursion as the internal\n                # values should be mapped collections already.\n                current_value = self.get_value(entity)\n                if not isinstance(\n                    current_value,\n                    ftrack_api.collection.CustomAttributeCollectionProxy\n                ):\n                    raise NotImplementedError(\n                        'Cannot adapt mapping to collection as current value '\n                        'type is not a MappedCollectionProxy.'\n                    )\n\n                # Create the new collection using the existing collection as\n                # basis. Then update through proxy interface to ensure all\n                # internal operations called consistently (such as entity\n                # deletion for key removal).\n                collection = ftrack_api.collection.Collection(\n                    entity, self, data=current_value.collection[:]\n                )\n                collection_proxy = (\n                    ftrack_api.collection.CustomAttributeCollectionProxy(\n                        collection\n                    )\n                )\n\n                # Remove expired keys from collection.\n                expired_keys = set(current_value.keys()) - set(value.keys())\n                for key in expired_keys:\n                    del collection_proxy[key]\n\n                # Set new values for existing keys / add new keys.\n                for key, value in value.items():\n                    collection_proxy[key] = value\n\n                value = collection_proxy\n\n            else:\n                raise NotImplementedError(\n                    'Cannot convert {0!r} to collection.'.format(value)\n                )\n        else:\n            if value.attribute is not self:\n                raise ftrack_api.exception.AttributeError(\n                    'Collection already bound to a different attribute.'\n                )\n\n        return value\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/cache.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\n'''Caching framework.\n\nDefines a standardised :class:`Cache` interface for storing data against\nspecific keys. Key generation is also standardised using a :class:`KeyMaker`\ninterface.\n\nCombining a Cache and KeyMaker allows for memoisation of function calls with\nrespect to the arguments used by using a :class:`Memoiser`.\n\nAs a convenience a simple :func:`memoise` decorator is included for quick\nmemoisation of function using a global cache and standard key maker.\n\n'''\n\nimport collections\nimport functools\nimport abc\nimport copy\nimport inspect\nimport re\nimport anydbm\nimport contextlib\ntry:\n    import cPickle as pickle\nexcept ImportError:  # pragma: no cover\n    import pickle\n\nimport ftrack_api.inspection\nimport ftrack_api.symbol\n\n\nclass Cache(object):\n    '''Cache interface.\n\n    Derive from this to define concrete cache implementations. A cache is\n    centered around the concept of key:value pairings where the key is unique\n    across the cache.\n\n    '''\n\n    __metaclass__ = abc.ABCMeta\n\n    @abc.abstractmethod\n    def get(self, key):\n        '''Return value for *key*.\n\n        Raise :exc:`KeyError` if *key* not found.\n\n        '''\n\n    @abc.abstractmethod\n    def set(self, key, value):\n        '''Set *value* for *key*.'''\n\n    @abc.abstractmethod\n    def remove(self, key):\n        '''Remove *key* and return stored value.\n\n        Raise :exc:`KeyError` if *key* not found.\n\n        '''\n\n    def keys(self):\n        '''Return list of keys at this current time.\n\n        .. warning::\n\n            Actual keys may differ from those returned due to timing of access.\n\n        '''\n        raise NotImplementedError()  # pragma: no cover\n\n    def values(self):\n        '''Return values for current keys.'''\n        values = []\n        for key in self.keys():\n            try:\n                value = self.get(key)\n            except KeyError:\n                continue\n            else:\n                values.append(value)\n\n        return values\n\n    def clear(self, pattern=None):\n        '''Remove all keys matching *pattern*.\n\n        *pattern* should be a regular expression string.\n\n        If *pattern* is None then all keys will be removed.\n\n        '''\n        if pattern is not None:\n            pattern = re.compile(pattern)\n\n        for key in self.keys():\n            if pattern is not None:\n                if not pattern.search(key):\n                    continue\n\n            try:\n                self.remove(key)\n            except KeyError:\n                pass\n\n\nclass ProxyCache(Cache):\n    '''Proxy another cache.'''\n\n    def __init__(self, proxied):\n        '''Initialise cache with *proxied* cache instance.'''\n        self.proxied = proxied\n        super(ProxyCache, self).__init__()\n\n    def get(self, key):\n        '''Return value for *key*.\n\n        Raise :exc:`KeyError` if *key* not found.\n\n        '''\n        return self.proxied.get(key)\n\n    def set(self, key, value):\n        '''Set *value* for *key*.'''\n        return self.proxied.set(key, value)\n\n    def remove(self, key):\n        '''Remove *key* and return stored value.\n\n        Raise :exc:`KeyError` if *key* not found.\n\n        '''\n        return self.proxied.remove(key)\n\n    def keys(self):\n        '''Return list of keys at this current time.\n\n        .. warning::\n\n            Actual keys may differ from those returned due to timing of access.\n\n        '''\n        return self.proxied.keys()\n\n\nclass LayeredCache(Cache):\n    '''Layered cache.'''\n\n    def __init__(self, caches):\n        '''Initialise cache with *caches*.'''\n        super(LayeredCache, self).__init__()\n        self.caches = caches\n\n    def get(self, key):\n        '''Return value for *key*.\n\n        Raise :exc:`KeyError` if *key* not found.\n\n        Attempt to retrieve from cache layers in turn, starting with shallowest.\n        If value retrieved, then also set the value in each higher level cache\n        up from where retrieved.\n\n        '''\n        target_caches = []\n        value = ftrack_api.symbol.NOT_SET\n\n        for cache in self.caches:\n            try:\n                value = cache.get(key)\n            except KeyError:\n                target_caches.append(cache)\n                continue\n            else:\n                break\n\n        if value is ftrack_api.symbol.NOT_SET:\n            raise KeyError(key)\n\n        # Set value on all higher level caches.\n        for cache in target_caches:\n            cache.set(key, value)\n\n        return value\n\n    def set(self, key, value):\n        '''Set *value* for *key*.'''\n        for cache in self.caches:\n            cache.set(key, value)\n\n    def remove(self, key):\n        '''Remove *key*.\n\n        Raise :exc:`KeyError` if *key* not found in any layer.\n\n        '''\n        removed = False\n        for cache in self.caches:\n            try:\n                cache.remove(key)\n            except KeyError:\n                pass\n            else:\n                removed = True\n\n        if not removed:\n            raise KeyError(key)\n\n    def keys(self):\n        '''Return list of keys at this current time.\n\n        .. warning::\n\n            Actual keys may differ from those returned due to timing of access.\n\n        '''\n        keys = []\n        for cache in self.caches:\n            keys.extend(cache.keys())\n\n        return list(set(keys))\n\n\nclass MemoryCache(Cache):\n    '''Memory based cache.'''\n\n    def __init__(self):\n        '''Initialise cache.'''\n        self._cache = {}\n        super(MemoryCache, self).__init__()\n\n    def get(self, key):\n        '''Return value for *key*.\n\n        Raise :exc:`KeyError` if *key* not found.\n\n        '''\n        return self._cache[key]\n\n    def set(self, key, value):\n        '''Set *value* for *key*.'''\n        self._cache[key] = value\n\n    def remove(self, key):\n        '''Remove *key*.\n\n        Raise :exc:`KeyError` if *key* not found.\n\n        '''\n        del self._cache[key]\n\n    def keys(self):\n        '''Return list of keys at this current time.\n\n        .. warning::\n\n            Actual keys may differ from those returned due to timing of access.\n\n        '''\n        return self._cache.keys()\n\n\nclass FileCache(Cache):\n    '''File based cache that uses :mod:`anydbm` module.\n\n    .. note::\n\n        No locking of the underlying file is performed.\n\n    '''\n\n    def __init__(self, path):\n        '''Initialise cache at *path*.'''\n        self.path = path\n\n        # Initialise cache.\n        cache = anydbm.open(self.path, 'c')\n        cache.close()\n\n        super(FileCache, self).__init__()\n\n    @contextlib.contextmanager\n    def _database(self):\n        '''Yield opened database file.'''\n        cache = anydbm.open(self.path, 'w')\n        try:\n            yield cache\n        finally:\n            cache.close()\n\n    def get(self, key):\n        '''Return value for *key*.\n\n        Raise :exc:`KeyError` if *key* not found.\n\n        '''\n        with self._database() as cache:\n            return cache[key]\n\n    def set(self, key, value):\n        '''Set *value* for *key*.'''\n        with self._database() as cache:\n            cache[key] = value\n\n    def remove(self, key):\n        '''Remove *key*.\n\n        Raise :exc:`KeyError` if *key* not found.\n\n        '''\n        with self._database() as cache:\n            del cache[key]\n\n    def keys(self):\n        '''Return list of keys at this current time.\n\n        .. warning::\n\n            Actual keys may differ from those returned due to timing of access.\n\n        '''\n        with self._database() as cache:\n            return cache.keys()\n\n\nclass SerialisedCache(ProxyCache):\n    '''Proxied cache that stores values as serialised data.'''\n\n    def __init__(self, proxied, encode=None, decode=None):\n        '''Initialise cache with *encode* and *decode* callables.\n\n        *proxied* is the underlying cache to use for storage.\n\n        '''\n        self.encode = encode\n        self.decode = decode\n        super(SerialisedCache, self).__init__(proxied)\n\n    def get(self, key):\n        '''Return value for *key*.\n\n        Raise :exc:`KeyError` if *key* not found.\n\n        '''\n        value = super(SerialisedCache, self).get(key)\n        if self.decode:\n            value = self.decode(value)\n\n        return value\n\n    def set(self, key, value):\n        '''Set *value* for *key*.'''\n        if self.encode:\n            value = self.encode(value)\n\n        super(SerialisedCache, self).set(key, value)\n\n\nclass KeyMaker(object):\n    '''Generate unique keys.'''\n\n    __metaclass__ = abc.ABCMeta\n\n    def __init__(self):\n        '''Initialise key maker.'''\n        super(KeyMaker, self).__init__()\n        self.item_separator = ''\n\n    def key(self, *items):\n        '''Return key for *items*.'''\n        keys = []\n        for item in items:\n            keys.append(self._key(item))\n\n        return self.item_separator.join(keys)\n\n    @abc.abstractmethod\n    def _key(self, obj):\n        '''Return key for *obj*.'''\n\n\nclass StringKeyMaker(KeyMaker):\n    '''Generate string key.'''\n\n    def _key(self, obj):\n        '''Return key for *obj*.'''\n        return str(obj)\n\n\nclass ObjectKeyMaker(KeyMaker):\n    '''Generate unique keys for objects.'''\n\n    def __init__(self):\n        '''Initialise key maker.'''\n        super(ObjectKeyMaker, self).__init__()\n        self.item_separator = '\\0'\n        self.mapping_identifier = '\\1'\n        self.mapping_pair_separator = '\\2'\n        self.iterable_identifier = '\\3'\n        self.name_identifier = '\\4'\n\n    def _key(self, item):\n        '''Return key for *item*.\n\n        Returned key will be a pickle like string representing the *item*. This\n        allows for typically non-hashable objects to be used in key generation\n        (such as dictionaries).\n\n        If *item* is iterable then each item in it shall also be passed to this\n        method to ensure correct key generation.\n\n        Special markers are used to distinguish handling of specific cases in\n        order to ensure uniqueness of key corresponds directly to *item*.\n\n        Example::\n\n            >>> key_maker = ObjectKeyMaker()\n            >>> def add(x, y):\n            ...     \"Return sum of *x* and *y*.\"\n            ...     return x + y\n            ...\n            >>> key_maker.key(add, (1, 2))\n            '\\x04add\\x00__main__\\x00\\x03\\x80\\x02K\\x01.\\x00\\x80\\x02K\\x02.\\x03'\n            >>> key_maker.key(add, (1, 3))\n            '\\x04add\\x00__main__\\x00\\x03\\x80\\x02K\\x01.\\x00\\x80\\x02K\\x03.\\x03'\n\n        '''\n        # TODO: Consider using a more robust and comprehensive solution such as\n        # dill (https://github.com/uqfoundation/dill).\n        if isinstance(item, collections.Iterable):\n            if isinstance(item, basestring):\n                return pickle.dumps(item, pickle.HIGHEST_PROTOCOL)\n\n            if isinstance(item, collections.Mapping):\n                contents = self.item_separator.join([\n                    (\n                        self._key(key) +\n                        self.mapping_pair_separator +\n                        self._key(value)\n                    )\n                    for key, value in sorted(item.items())\n                ])\n                return (\n                    self.mapping_identifier +\n                    contents +\n                    self.mapping_identifier\n                )\n\n            else:\n                contents = self.item_separator.join([\n                    self._key(item) for item in item\n                ])\n                return (\n                    self.iterable_identifier +\n                    contents +\n                    self.iterable_identifier\n                )\n\n        elif inspect.ismethod(item):\n            return ''.join((\n                self.name_identifier,\n                item.__name__,\n                self.item_separator,\n                item.im_class.__name__,\n                self.item_separator,\n                item.__module__\n            ))\n\n        elif inspect.isfunction(item) or inspect.isclass(item):\n            return ''.join((\n                self.name_identifier,\n                item.__name__,\n                self.item_separator,\n                item.__module__\n            ))\n\n        elif inspect.isbuiltin(item):\n            return self.name_identifier + item.__name__\n\n        else:\n            return pickle.dumps(item, pickle.HIGHEST_PROTOCOL)\n\n\nclass Memoiser(object):\n    '''Memoise function calls using a :class:`KeyMaker` and :class:`Cache`.\n\n    Example::\n\n        >>> memoiser = Memoiser(MemoryCache(), ObjectKeyMaker())\n        >>> def add(x, y):\n        ...     \"Return sum of *x* and *y*.\"\n        ...     print 'Called'\n        ...     return x + y\n        ...\n        >>> memoiser.call(add, (1, 2), {})\n        Called\n        >>> memoiser.call(add, (1, 2), {})\n        >>> memoiser.call(add, (1, 3), {})\n        Called\n\n    '''\n\n    def __init__(self, cache=None, key_maker=None, return_copies=True):\n        '''Initialise with *cache* and *key_maker* to use.\n\n        If *cache* is not specified a default :class:`MemoryCache` will be\n        used. Similarly, if *key_maker* is not specified a default\n        :class:`ObjectKeyMaker` will be used.\n\n        If *return_copies* is True then all results returned from the cache will\n        be deep copies to avoid indirect mutation of cached values.\n\n        '''\n        self.cache = cache\n        if self.cache is None:\n            self.cache = MemoryCache()\n\n        self.key_maker = key_maker\n        if self.key_maker is None:\n            self.key_maker = ObjectKeyMaker()\n\n        self.return_copies = return_copies\n        super(Memoiser, self).__init__()\n\n    def call(self, function, args=None, kw=None):\n        '''Call *function* with *args* and *kw* and return result.\n\n        If *function* was previously called with exactly the same arguments\n        then return cached result if available.\n\n        Store result for call in cache.\n\n        '''\n        if args is None:\n            args = ()\n\n        if kw is None:\n            kw = {}\n\n        # Support arguments being passed as positionals or keywords.\n        arguments = inspect.getcallargs(function, *args, **kw)\n\n        key = self.key_maker.key(function, arguments)\n        try:\n            value = self.cache.get(key)\n\n        except KeyError:\n            value = function(*args, **kw)\n            self.cache.set(key, value)\n\n        # If requested, deep copy value to return in order to avoid cached value\n        # being inadvertently altered by the caller.\n        if self.return_copies:\n            value = copy.deepcopy(value)\n\n        return value\n\n\ndef memoise_decorator(memoiser):\n    '''Decorator to memoise function calls using *memoiser*.'''\n    def outer(function):\n\n        @functools.wraps(function)\n        def inner(*args, **kw):\n            return memoiser.call(function, args, kw)\n\n        return inner\n\n    return outer\n\n\n#: Default memoiser.\nmemoiser = Memoiser()\n\n#: Default memoise decorator using standard cache and key maker.\nmemoise = memoise_decorator(memoiser)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/collection.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nfrom __future__ import absolute_import\n\nimport logging\n\nimport collections\nimport copy\n\nimport ftrack_api.exception\nimport ftrack_api.inspection\nimport ftrack_api.symbol\nimport ftrack_api.operation\nimport ftrack_api.cache\nfrom ftrack_api.logging import LazyLogMessage as L\n\n\nclass Collection(collections.MutableSequence):\n    '''A collection of entities.'''\n\n    def __init__(self, entity, attribute, mutable=True, data=None):\n        '''Initialise collection.'''\n        self.entity = entity\n        self.attribute = attribute\n        self._data = []\n        self._identities = set()\n\n        # Set initial dataset.\n        # Note: For initialisation, immutability is deferred till after initial\n        # population as otherwise there would be no public way to initialise an\n        # immutable collection. The reason self._data is not just set directly\n        # is to ensure other logic can be applied without special handling.\n        self.mutable = True\n        try:\n            if data is None:\n                data = []\n\n            with self.entity.session.operation_recording(False):\n                self.extend(data)\n        finally:\n            self.mutable = mutable\n\n    def _identity_key(self, entity):\n        '''Return identity key for *entity*.'''\n        return str(ftrack_api.inspection.identity(entity))\n\n    def __copy__(self):\n        '''Return shallow copy.\n\n        .. note::\n\n            To maintain expectations on usage, the shallow copy will include a\n            shallow copy of the underlying data store.\n\n        '''\n        cls = self.__class__\n        copied_instance = cls.__new__(cls)\n        copied_instance.__dict__.update(self.__dict__)\n        copied_instance._data = copy.copy(self._data)\n        copied_instance._identities = copy.copy(self._identities)\n\n        return copied_instance\n\n    def _notify(self, old_value):\n        '''Notify about modification.'''\n        # Record operation.\n        if self.entity.session.record_operations:\n            self.entity.session.recorded_operations.push(\n                ftrack_api.operation.UpdateEntityOperation(\n                    self.entity.entity_type,\n                    ftrack_api.inspection.primary_key(self.entity),\n                    self.attribute.name,\n                    old_value,\n                    self\n                )\n            )\n\n    def insert(self, index, item):\n        '''Insert *item* at *index*.'''\n        if not self.mutable:\n            raise ftrack_api.exception.ImmutableCollectionError(self)\n\n        if item in self:\n            raise ftrack_api.exception.DuplicateItemInCollectionError(\n                item, self\n            )\n\n        old_value = copy.copy(self)\n        self._data.insert(index, item)\n        self._identities.add(self._identity_key(item))\n        self._notify(old_value)\n\n    def __contains__(self, value):\n        '''Return whether *value* present in collection.'''\n        return self._identity_key(value) in self._identities\n\n    def __getitem__(self, index):\n        '''Return item at *index*.'''\n        return self._data[index]\n\n    def __setitem__(self, index, item):\n        '''Set *item* against *index*.'''\n        if not self.mutable:\n            raise ftrack_api.exception.ImmutableCollectionError(self)\n\n        try:\n            existing_index = self.index(item)\n        except ValueError:\n            pass\n        else:\n            if index != existing_index:\n                raise ftrack_api.exception.DuplicateItemInCollectionError(\n                    item, self\n                )\n\n        old_value = copy.copy(self)\n        try:\n            existing_item = self._data[index]\n        except IndexError:\n            pass\n        else:\n            self._identities.remove(self._identity_key(existing_item))\n\n        self._data[index] = item\n        self._identities.add(self._identity_key(item))\n        self._notify(old_value)\n\n    def __delitem__(self, index):\n        '''Remove item at *index*.'''\n        if not self.mutable:\n            raise ftrack_api.exception.ImmutableCollectionError(self)\n\n        old_value = copy.copy(self)\n        item = self._data[index]\n        del self._data[index]\n        self._identities.remove(self._identity_key(item))\n        self._notify(old_value)\n\n    def __len__(self):\n        '''Return count of items.'''\n        return len(self._data)\n\n    def __eq__(self, other):\n        '''Return whether this collection is equal to *other*.'''\n        if not isinstance(other, Collection):\n            return False\n\n        return sorted(self._identities) == sorted(other._identities)\n\n    def __ne__(self, other):\n        '''Return whether this collection is not equal to *other*.'''\n        return not self == other\n\n\nclass MappedCollectionProxy(collections.MutableMapping):\n    '''Common base class for mapped collection of entities.'''\n\n    def __init__(self, collection):\n        '''Initialise proxy for *collection*.'''\n        self.logger = logging.getLogger(\n            __name__ + '.' + self.__class__.__name__\n        )\n        self.collection = collection\n        super(MappedCollectionProxy, self).__init__()\n\n    def __copy__(self):\n        '''Return shallow copy.\n\n        .. note::\n\n            To maintain expectations on usage, the shallow copy will include a\n            shallow copy of the underlying collection.\n\n        '''\n        cls = self.__class__\n        copied_instance = cls.__new__(cls)\n        copied_instance.__dict__.update(self.__dict__)\n        copied_instance.collection = copy.copy(self.collection)\n\n        return copied_instance\n\n    @property\n    def mutable(self):\n        '''Return whether collection is mutable.'''\n        return self.collection.mutable\n\n    @mutable.setter\n    def mutable(self, value):\n        '''Set whether collection is mutable to *value*.'''\n        self.collection.mutable = value\n\n    @property\n    def attribute(self):\n        '''Return attribute bound to.'''\n        return self.collection.attribute\n\n    @attribute.setter\n    def attribute(self, value):\n        '''Set bound attribute to *value*.'''\n        self.collection.attribute = value\n\n\nclass KeyValueMappedCollectionProxy(MappedCollectionProxy):\n    '''A mapped collection of key, value entities.\n\n    Proxy a standard :class:`Collection` as a mapping where certain attributes\n    from the entities in the collection are mapped to key, value pairs.\n\n    For example::\n\n        >>> collection = [Metadata(key='foo', value='bar'), ...]\n        >>> mapped = KeyValueMappedCollectionProxy(\n        ...     collection, create_metadata,\n        ...     key_attribute='key', value_attribute='value'\n        ... )\n        >>> print mapped['foo']\n        'bar'\n        >>> mapped['bam'] = 'biz'\n        >>> print mapped.collection[-1]\n        Metadata(key='bam', value='biz')\n\n    '''\n\n    def __init__(\n        self, collection, creator, key_attribute, value_attribute\n    ):\n        '''Initialise collection.'''\n        self.creator = creator\n        self.key_attribute = key_attribute\n        self.value_attribute = value_attribute\n        super(KeyValueMappedCollectionProxy, self).__init__(collection)\n\n    def _get_entity_by_key(self, key):\n        '''Return entity instance with matching *key* from collection.'''\n        for entity in self.collection:\n            if entity[self.key_attribute] == key:\n                return entity\n\n        raise KeyError(key)\n\n    def __getitem__(self, key):\n        '''Return value for *key*.'''\n        entity = self._get_entity_by_key(key)\n        return entity[self.value_attribute]\n\n    def __setitem__(self, key, value):\n        '''Set *value* for *key*.'''\n        try:\n            entity = self._get_entity_by_key(key)\n        except KeyError:\n            data = {\n                self.key_attribute: key,\n                self.value_attribute: value\n            }\n            entity = self.creator(self, data)\n\n            if (\n                ftrack_api.inspection.state(entity) is\n                ftrack_api.symbol.CREATED\n            ):\n                # Persisting this entity will be handled here, record the\n                # operation.\n                self.collection.append(entity)\n\n            else:\n                # The entity is created and persisted separately by the\n                # creator. Do not record this operation.\n                with self.collection.entity.session.operation_recording(False):\n                    # Do not record this operation since it will trigger\n                    # redudant and potentially failing operations.\n                    self.collection.append(entity)\n\n        else:\n            entity[self.value_attribute] = value\n\n    def __delitem__(self, key):\n        '''Remove and delete *key*.\n\n        .. note::\n\n            The associated entity will be deleted as well.\n\n        '''\n        for index, entity in enumerate(self.collection):\n            if entity[self.key_attribute] == key:\n                break\n        else:\n            raise KeyError(key)\n\n        del self.collection[index]\n        entity.session.delete(entity)\n\n    def __iter__(self):\n        '''Iterate over all keys.'''\n        keys = set()\n        for entity in self.collection:\n            keys.add(entity[self.key_attribute])\n\n        return iter(keys)\n\n    def __len__(self):\n        '''Return count of keys.'''\n        keys = set()\n        for entity in self.collection:\n            keys.add(entity[self.key_attribute])\n\n        return len(keys)\n\n\nclass PerSessionDefaultKeyMaker(ftrack_api.cache.KeyMaker):\n    '''Generate key for session.'''\n\n    def _key(self, obj):\n        '''Return key for *obj*.'''\n        if isinstance(obj, dict):\n            session = obj.get('session')\n            if session is not None:\n                # Key by session only.\n                return str(id(session))\n\n        return str(obj)\n\n\n#: Memoiser for use with callables that should be called once per session.\nmemoise_session = ftrack_api.cache.memoise_decorator(\n    ftrack_api.cache.Memoiser(\n        key_maker=PerSessionDefaultKeyMaker(), return_copies=False\n    )\n)\n\n\n@memoise_session\ndef _get_custom_attribute_configurations(session):\n    '''Return list of custom attribute configurations.\n\n    The configuration objects will have key, project_id, id and object_type_id\n    populated.\n\n    '''\n    return session.query(\n        'select key, project_id, id, object_type_id, entity_type from '\n        'CustomAttributeConfiguration'\n    ).all()\n\n\nclass CustomAttributeCollectionProxy(MappedCollectionProxy):\n    '''A mapped collection of custom attribute value entities.'''\n\n    def __init__(\n        self, collection\n    ):\n        '''Initialise collection.'''\n        self.key_attribute = 'configuration_id'\n        self.value_attribute = 'value'\n        super(CustomAttributeCollectionProxy, self).__init__(collection)\n\n    def _get_entity_configurations(self):\n        '''Return all configurations for current collection entity.'''\n        entity = self.collection.entity\n        entity_type = None\n        project_id = None\n        object_type_id = None\n\n        if 'object_type_id' in entity.keys():\n            project_id = entity['project_id']\n            entity_type = 'task'\n            object_type_id = entity['object_type_id']\n\n        if entity.entity_type == 'AssetVersion':\n            project_id = entity['asset']['parent']['project_id']\n            entity_type = 'assetversion'\n\n        if entity.entity_type == 'Asset':\n            project_id = entity['parent']['project_id']\n            entity_type = 'asset'\n\n        if entity.entity_type == 'Project':\n            project_id = entity['id']\n            entity_type = 'show'\n\n        if entity.entity_type == 'User':\n            entity_type = 'user'\n\n        if entity_type is None:\n            raise ValueError(\n                'Entity {!r} not supported.'.format(entity)\n            )\n\n        configurations = []\n        for configuration in _get_custom_attribute_configurations(\n            entity.session\n        ):\n            if (\n                configuration['entity_type'] == entity_type and\n                configuration['project_id'] in (project_id, None) and\n                configuration['object_type_id'] == object_type_id\n            ):\n                configurations.append(configuration)\n\n        # Return with global configurations at the end of the list. This is done\n        # so that global conigurations are shadowed by project specific if the\n        # configurations list is looped when looking for a matching `key`.\n        return sorted(\n            configurations, key=lambda item: item['project_id'] is None\n        )\n\n    def _get_keys(self):\n        '''Return a list of all keys.'''\n        keys = []\n        for configuration in self._get_entity_configurations():\n            keys.append(configuration['key'])\n\n        return keys\n\n    def _get_entity_by_key(self, key):\n        '''Return entity instance with matching *key* from collection.'''\n        configuration_id = self.get_configuration_id_from_key(key)\n        for entity in self.collection:\n            if entity[self.key_attribute] == configuration_id:\n                return entity\n\n        return None\n\n    def get_configuration_id_from_key(self, key):\n        '''Return id of configuration with matching *key*.\n\n        Raise :exc:`KeyError` if no configuration with matching *key* found.\n\n        '''\n        for configuration in self._get_entity_configurations():\n            if key == configuration['key']:\n                return configuration['id']\n\n        raise KeyError(key)\n\n    def __getitem__(self, key):\n        '''Return value for *key*.'''\n        entity = self._get_entity_by_key(key)\n\n        if entity:\n            return entity[self.value_attribute]\n\n        for configuration in self._get_entity_configurations():\n            if configuration['key'] == key:\n                return configuration['default']\n\n        raise KeyError(key)\n\n    def __setitem__(self, key, value):\n        '''Set *value* for *key*.'''\n        custom_attribute_value = self._get_entity_by_key(key)\n\n        if custom_attribute_value:\n            custom_attribute_value[self.value_attribute] = value\n        else:\n            entity = self.collection.entity\n            session = entity.session\n            data = {\n                self.key_attribute: self.get_configuration_id_from_key(key),\n                self.value_attribute: value,\n                'entity_id': entity['id']\n            }\n\n            # Make sure to use the currently active collection. This is\n            # necessary since a merge might have replaced the current one.\n            self.collection.entity['custom_attributes'].collection.append(\n                session.create('CustomAttributeValue', data)\n            )\n\n    def __delitem__(self, key):\n        '''Remove and delete *key*.\n\n        .. note::\n\n            The associated entity will be deleted as well.\n\n        '''\n        custom_attribute_value = self._get_entity_by_key(key)\n\n        if custom_attribute_value:\n            index = self.collection.index(custom_attribute_value)\n            del self.collection[index]\n\n            custom_attribute_value.session.delete(custom_attribute_value)\n        else:\n            self.logger.warning(L(\n                'Cannot delete {0!r} on {1!r}, no custom attribute value set.',\n                key, self.collection.entity\n            ))\n\n    def __eq__(self, collection):\n        '''Return True if *collection* equals proxy collection.'''\n        if collection is ftrack_api.symbol.NOT_SET:\n            return False\n\n        return collection.collection == self.collection\n\n    def __iter__(self):\n        '''Iterate over all keys.'''\n        keys = self._get_keys()\n        return iter(keys)\n\n    def __len__(self):\n        '''Return count of keys.'''\n        keys = self._get_keys()\n        return len(keys)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/data.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2013 ftrack\n\nimport os\nfrom abc import ABCMeta, abstractmethod\nimport tempfile\n\n\nclass Data(object):\n    '''File-like object for manipulating data.'''\n\n    __metaclass__ = ABCMeta\n\n    def __init__(self):\n        '''Initialise data access.'''\n        self.closed = False\n\n    @abstractmethod\n    def read(self, limit=None):\n        '''Return content from current position up to *limit*.'''\n\n    @abstractmethod\n    def write(self, content):\n        '''Write content at current position.'''\n\n    def flush(self):\n        '''Flush buffers ensuring data written.'''\n\n    def seek(self, offset, whence=os.SEEK_SET):\n        '''Move internal pointer by *offset*.\n\n        The *whence* argument is optional and defaults to os.SEEK_SET or 0\n        (absolute file positioning); other values are os.SEEK_CUR or 1\n        (seek relative to the current position) and os.SEEK_END or 2\n        (seek relative to the file's end).\n\n        '''\n        raise NotImplementedError('Seek not supported.')\n\n    def tell(self):\n        '''Return current position of internal pointer.'''\n        raise NotImplementedError('Tell not supported.')\n\n    def close(self):\n        '''Flush buffers and prevent further access.'''\n        self.flush()\n        self.closed = True\n\n\nclass FileWrapper(Data):\n    '''Data wrapper for Python file objects.'''\n\n    def __init__(self, wrapped_file):\n        '''Initialise access to *wrapped_file*.'''\n        self.wrapped_file = wrapped_file\n        self._read_since_last_write = False\n        super(FileWrapper, self).__init__()\n\n    def read(self, limit=None):\n        '''Return content from current position up to *limit*.'''\n        self._read_since_last_write = True\n\n        if limit is None:\n            limit = -1\n\n        return self.wrapped_file.read(limit)\n\n    def write(self, content):\n        '''Write content at current position.'''\n        if self._read_since_last_write:\n            # Windows requires a seek before switching from read to write.\n            self.seek(self.tell())\n\n        self.wrapped_file.write(content)\n        self._read_since_last_write = False\n\n    def flush(self):\n        '''Flush buffers ensuring data written.'''\n        super(FileWrapper, self).flush()\n        if hasattr(self.wrapped_file, 'flush'):\n            self.wrapped_file.flush()\n\n    def seek(self, offset, whence=os.SEEK_SET):\n        '''Move internal pointer by *offset*.'''\n        self.wrapped_file.seek(offset, whence)\n\n    def tell(self):\n        '''Return current position of internal pointer.'''\n        return self.wrapped_file.tell()\n\n    def close(self):\n        '''Flush buffers and prevent further access.'''\n        if not self.closed:\n            super(FileWrapper, self).close()\n            if hasattr(self.wrapped_file, 'close'):\n                self.wrapped_file.close()\n\n\nclass File(FileWrapper):\n    '''Data wrapper accepting filepath.'''\n\n    def __init__(self, path, mode='rb'):\n        '''Open file at *path* with *mode*.'''\n        file_object = open(path, mode)\n        super(File, self).__init__(file_object)\n\n\nclass String(FileWrapper):\n    '''Data wrapper using TemporaryFile instance.'''\n\n    def __init__(self, content=None):\n        '''Initialise data with *content*.'''\n        super(String, self).__init__(\n            tempfile.TemporaryFile()\n        )\n\n        if content is not None:\n            self.wrapped_file.write(content)\n            self.wrapped_file.seek(0)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/entity/__init__.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/entity/asset_version.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport ftrack_api.entity.base\n\n\nclass AssetVersion(ftrack_api.entity.base.Entity):\n    '''Represent asset version.'''\n\n    def create_component(\n        self, path, data=None, location=None\n    ):\n        '''Create a new component from *path* with additional *data*\n\n        .. note::\n\n            This is a helper method. To create components manually use the\n            standard :meth:`Session.create` method.\n\n        *path* can be a string representing a filesystem path to the data to\n        use for the component. The *path* can also be specified as a sequence\n        string, in which case a sequence component with child components for\n        each item in the sequence will be created automatically. The accepted\n        format for a sequence is '{head}{padding}{tail} [{ranges}]'. For\n        example::\n\n            '/path/to/file.%04d.ext [1-5, 7, 8, 10-20]'\n\n        .. seealso::\n\n            `Clique documentation <http://clique.readthedocs.org>`_\n\n        *data* should be a dictionary of any additional data to construct the\n        component with (as passed to :meth:`Session.create`). This version is\n        automatically set as the component's version.\n\n        If *location* is specified then automatically add component to that\n        location.\n\n        '''\n        if data is None:\n            data = {}\n\n        data.pop('version_id', None)\n        data['version'] = self\n\n        return self.session.create_component(path, data=data, location=location)\n\n    def encode_media(self, media, keep_original='auto'):\n        '''Return a new Job that encode *media* to make it playable in browsers.\n\n        *media* can be a path to a file or a FileComponent in the ftrack.server\n        location.\n\n        The job will encode *media* based on the file type and job data contains\n        information about encoding in the following format::\n\n            {\n                'output': [{\n                    'format': 'video/mp4',\n                    'component_id': 'e2dc0524-b576-11d3-9612-080027331d74'\n                }, {\n                    'format': 'image/jpeg',\n                    'component_id': '07b82a97-8cf9-11e3-9383-20c9d081909b'\n                }],\n                'source_component_id': 'e3791a09-7e11-4792-a398-3d9d4eefc294',\n                'keep_original': True\n            }\n\n        The output components are associated with the job via the job_components\n        relation.\n\n        An image component will always be generated if possible, and will be \n        set as the version's thumbnail.\n\n        The new components will automatically be associated with the version.\n        A server version of 3.3.32 or higher is required for this to function\n        properly.\n\n        If *media* is a file path, a new source component will be created and\n        added to the ftrack server location and a call to :meth:`commit` will be\n        issued. If *media* is a FileComponent, it will be assumed to be in\n        available in the ftrack.server location.\n\n        If *keep_original* is not set, the original media will be kept if it\n        is a FileComponent, and deleted if it is a file path. You can specify\n        True or False to change this behavior.\n        '''\n        return self.session.encode_media(\n            media, version_id=self['id'], keep_original=keep_original\n        )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/entity/base.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nfrom __future__ import absolute_import\n\nimport abc\nimport collections\nimport logging\n\nimport ftrack_api.symbol\nimport ftrack_api.attribute\nimport ftrack_api.inspection\nimport ftrack_api.exception\nimport ftrack_api.operation\nfrom ftrack_api.logging import LazyLogMessage as L\n\n\nclass DynamicEntityTypeMetaclass(abc.ABCMeta):\n    '''Custom metaclass to customise representation of dynamic classes.\n\n    .. note::\n\n        Derive from same metaclass as derived bases to avoid conflicts.\n\n    '''\n    def __repr__(self):\n        '''Return representation of class.'''\n        return '<dynamic ftrack class \\'{0}\\'>'.format(self.__name__)\n\n\nclass Entity(collections.MutableMapping):\n    '''Base class for all entities.'''\n\n    __metaclass__ = DynamicEntityTypeMetaclass\n\n    entity_type = 'Entity'\n    attributes = None\n    primary_key_attributes = None\n    default_projections = None\n\n    def __init__(self, session, data=None, reconstructing=False):\n        '''Initialise entity.\n\n        *session* is an instance of :class:`ftrack_api.session.Session` that\n        this entity instance is bound to.\n\n        *data* is a mapping of key, value pairs to apply as initial attribute\n        values.\n\n        *reconstructing* indicates whether this entity is being reconstructed,\n        such as from a query, and therefore should not have any special creation\n        logic applied, such as initialising defaults for missing data.\n\n        '''\n        super(Entity, self).__init__()\n        self.logger = logging.getLogger(\n            __name__ + '.' + self.__class__.__name__\n        )\n        self.session = session\n        self._inflated = set()\n\n        if data is None:\n            data = {}\n\n        self.logger.debug(L(\n            '{0} entity from {1!r}.',\n            ('Reconstructing' if reconstructing else 'Constructing'), data\n        ))\n\n        self._ignore_data_keys = ['__entity_type__']\n        if not reconstructing:\n            self._construct(data)\n        else:\n            self._reconstruct(data)\n\n    def _construct(self, data):\n        '''Construct from *data*.'''\n        # Suspend operation recording so that all modifications can be applied\n        # in single create operation. In addition, recording a modification\n        # operation requires a primary key which may not be available yet.\n\n        relational_attributes = dict()\n\n        with self.session.operation_recording(False):\n            # Set defaults for any unset local attributes.\n            for attribute in self.__class__.attributes:\n                if attribute.name not in data:\n                    default_value = attribute.default_value\n                    if callable(default_value):\n                        default_value = default_value(self)\n\n                    attribute.set_local_value(self, default_value)\n\n\n            # Data represents locally set values.\n            for key, value in data.items():\n                if key in self._ignore_data_keys:\n                    continue\n\n                attribute = self.__class__.attributes.get(key)\n                if attribute is None:\n                    self.logger.debug(L(\n                        'Cannot populate {0!r} attribute as no such '\n                        'attribute found on entity {1!r}.', key, self\n                    ))\n                    continue\n\n                if not isinstance(attribute, ftrack_api.attribute.ScalarAttribute):\n                    relational_attributes.setdefault(\n                        attribute, value\n                    )\n\n                else:\n                    attribute.set_local_value(self, value)\n\n        # Record create operation.\n        # Note: As this operation is recorded *before* any Session.merge takes\n        # place there is the possibility that the operation will hold references\n        # to outdated data in entity_data. However, this would be unusual in\n        # that it would mean the same new entity was created twice and only one\n        # altered. Conversely, if this operation were recorded *after*\n        # Session.merge took place, any cache would not be able to determine\n        # the status of the entity, which could be important if the cache should\n        # not store newly created entities that have not yet been persisted. Out\n        # of these two 'evils' this approach is deemed the lesser at this time.\n        # A third, more involved, approach to satisfy both might be to record\n        # the operation with a PENDING entity_data value and then update with\n        # merged values post merge.\n        if self.session.record_operations:\n            entity_data = {}\n\n            # Lower level API used here to avoid including any empty\n            # collections that are automatically generated on access.\n            for attribute in self.attributes:\n                value = attribute.get_local_value(self)\n                if value is not ftrack_api.symbol.NOT_SET:\n                    entity_data[attribute.name] = value\n\n            self.session.recorded_operations.push(\n                ftrack_api.operation.CreateEntityOperation(\n                    self.entity_type,\n                    ftrack_api.inspection.primary_key(self),\n                    entity_data\n                )\n            )\n\n        for attribute, value in relational_attributes.items():\n            # Finally we set values for \"relational\" attributes, we need\n            # to do this at the end in order to get the create operations\n            # in the correct order as the newly created attributes might\n            # contain references to the newly created entity.\n\n            attribute.set_local_value(\n                self, value\n            )\n\n    def _reconstruct(self, data):\n        '''Reconstruct from *data*.'''\n        # Data represents remote values.\n        for key, value in data.items():\n            if key in self._ignore_data_keys:\n                continue\n\n            attribute = self.__class__.attributes.get(key)\n            if attribute is None:\n                self.logger.debug(L(\n                    'Cannot populate {0!r} attribute as no such attribute '\n                    'found on entity {1!r}.', key, self\n                ))\n                continue\n\n            attribute.set_remote_value(self, value)\n\n    def __repr__(self):\n        '''Return representation of instance.'''\n        return '<dynamic ftrack {0} object {1}>'.format(\n            self.__class__.__name__, id(self)\n        )\n\n    def __str__(self):\n        '''Return string representation of instance.'''\n        with self.session.auto_populating(False):\n            primary_key = ['Unknown']\n            try:\n                primary_key = ftrack_api.inspection.primary_key(self).values()\n            except KeyError:\n                pass\n\n        return '<{0}({1})>'.format(\n            self.__class__.__name__, ', '.join(primary_key)\n        )\n\n    def __hash__(self):\n        '''Return hash representing instance.'''\n        return hash(str(ftrack_api.inspection.identity(self)))\n\n    def __eq__(self, other):\n        '''Return whether *other* is equal to this instance.\n\n        .. note::\n\n            Equality is determined by both instances having the same identity.\n            Values of attributes are not considered.\n\n        '''\n        try:\n            return (\n                ftrack_api.inspection.identity(other)\n                == ftrack_api.inspection.identity(self)\n            )\n        except (AttributeError, KeyError):\n            return False\n\n    def __getitem__(self, key):\n        '''Return attribute value for *key*.'''\n        attribute = self.__class__.attributes.get(key)\n        if attribute is None:\n            raise KeyError(key)\n\n        return attribute.get_value(self)\n\n    def __setitem__(self, key, value):\n        '''Set attribute *value* for *key*.'''\n        attribute = self.__class__.attributes.get(key)\n        if attribute is None:\n            raise KeyError(key)\n\n        attribute.set_local_value(self, value)\n\n    def __delitem__(self, key):\n        '''Clear attribute value for *key*.\n\n        .. note::\n\n            Will not remove the attribute, but instead clear any local value\n            and revert to the last known server value.\n\n        '''\n        attribute = self.__class__.attributes.get(key)\n        attribute.set_local_value(self, ftrack_api.symbol.NOT_SET)\n\n    def __iter__(self):\n        '''Iterate over all attributes keys.'''\n        for attribute in self.__class__.attributes:\n            yield attribute.name\n\n    def __len__(self):\n        '''Return count of attributes.'''\n        return len(self.__class__.attributes)\n\n    def values(self):\n        '''Return list of values.'''\n        if self.session.auto_populate:\n            self._populate_unset_scalar_attributes()\n\n        return super(Entity, self).values()\n\n    def items(self):\n        '''Return list of tuples of (key, value) pairs.\n\n        .. note::\n\n            Will fetch all values from the server if not already fetched or set\n            locally.\n\n        '''\n        if self.session.auto_populate:\n            self._populate_unset_scalar_attributes()\n\n        return super(Entity, self).items()\n\n    def clear(self):\n        '''Reset all locally modified attribute values.'''\n        for attribute in self:\n            del self[attribute]\n\n    def merge(self, entity, merged=None):\n        '''Merge *entity* attribute values and other data into this entity.\n\n        Only merge values from *entity* that are not\n        :attr:`ftrack_api.symbol.NOT_SET`.\n\n        Return a list of changes made with each change being a mapping with\n        the keys:\n\n            * type - Either 'remote_attribute', 'local_attribute' or 'property'.\n            * name - The name of the attribute / property modified.\n            * old_value - The previous value.\n            * new_value - The new merged value.\n\n        '''\n        log_debug = self.logger.isEnabledFor(logging.DEBUG)\n\n        if merged is None:\n            merged = {}\n\n        log_message = 'Merged {type} \"{name}\": {old_value!r} -> {new_value!r}'\n        changes = []\n\n        # Attributes.\n\n        # Prioritise by type so that scalar values are set first. This should\n        # guarantee that the attributes making up the identity of the entity\n        # are merged before merging any collections that may have references to\n        # this entity.\n        attributes = collections.deque()\n        for attribute in entity.attributes:\n            if isinstance(attribute, ftrack_api.attribute.ScalarAttribute):\n                attributes.appendleft(attribute)\n            else:\n                attributes.append(attribute)\n\n        for other_attribute in attributes:\n            attribute = self.attributes.get(other_attribute.name)\n\n            # Local attributes.\n            other_local_value = other_attribute.get_local_value(entity)\n            if other_local_value is not ftrack_api.symbol.NOT_SET:\n                local_value = attribute.get_local_value(self)\n                if local_value != other_local_value:\n                    merged_local_value = self.session.merge(\n                        other_local_value, merged=merged\n                    )\n\n                    attribute.set_local_value(self, merged_local_value)\n                    changes.append({\n                        'type': 'local_attribute',\n                        'name': attribute.name,\n                        'old_value': local_value,\n                        'new_value': merged_local_value\n                    })\n                    log_debug and self.logger.debug(\n                        log_message.format(**changes[-1])\n                    )\n\n            # Remote attributes.\n            other_remote_value = other_attribute.get_remote_value(entity)\n            if other_remote_value is not ftrack_api.symbol.NOT_SET:\n                remote_value = attribute.get_remote_value(self)\n                if remote_value != other_remote_value:\n                    merged_remote_value = self.session.merge(\n                        other_remote_value, merged=merged\n                    )\n\n                    attribute.set_remote_value(\n                        self, merged_remote_value\n                    )\n\n                    changes.append({\n                        'type': 'remote_attribute',\n                        'name': attribute.name,\n                        'old_value': remote_value,\n                        'new_value': merged_remote_value\n                    })\n\n                    log_debug and self.logger.debug(\n                        log_message.format(**changes[-1])\n                    )\n\n                    # We need to handle collections separately since\n                    # they may store a local copy of the remote attribute\n                    # even though it may not be modified.\n                    if not isinstance(\n                        attribute, ftrack_api.attribute.AbstractCollectionAttribute\n                    ):\n                        continue\n\n                    local_value = attribute.get_local_value(\n                        self\n                    )\n\n                    # Populated but not modified, update it.\n                    if (\n                        local_value is not ftrack_api.symbol.NOT_SET and\n                        local_value == remote_value\n                    ):\n                        attribute.set_local_value(\n                            self, merged_remote_value\n                        )\n                        changes.append({\n                            'type': 'local_attribute',\n                            'name': attribute.name,\n                            'old_value': local_value,\n                            'new_value': merged_remote_value\n                        })\n\n                        log_debug and self.logger.debug(\n                            log_message.format(**changes[-1])\n                        )\n\n        return changes\n\n    def _populate_unset_scalar_attributes(self):\n        '''Populate all unset scalar attributes in one query.'''\n        projections = []\n        for attribute in self.attributes:\n            if isinstance(attribute, ftrack_api.attribute.ScalarAttribute):\n                if attribute.get_remote_value(self) is ftrack_api.symbol.NOT_SET:\n                    projections.append(attribute.name)\n\n        if projections:\n            self.session.populate([self], ', '.join(projections))\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/entity/component.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport ftrack_api.entity.base\n\n\nclass Component(ftrack_api.entity.base.Entity):\n    '''Represent a component.'''\n\n    def get_availability(self, locations=None):\n        '''Return availability in *locations*.\n\n        If *locations* is None, all known locations will be checked.\n\n        Return a dictionary of {location_id:percentage_availability}\n\n        '''\n        return self.session.get_component_availability(\n            self, locations=locations\n        )\n\n\nclass CreateThumbnailMixin(object):\n    '''Mixin to add create_thumbnail method on entity class.'''\n\n    def create_thumbnail(self, path, data=None):\n        '''Set entity thumbnail from *path*.\n\n        Creates a thumbnail component using in the ftrack.server location \n        :meth:`Session.create_component \n        <ftrack_api.session.Session.create_component>` The thumbnail component\n        will be created using *data* if specified. If no component name is\n        given, `thumbnail` will be used.\n\n        The file is expected to be of an appropriate size and valid file\n        type.\n\n        .. note::\n\n            A :meth:`Session.commit<ftrack_api.session.Session.commit>` will be\n            automatically issued.\n\n        '''\n        if data is None:\n            data = {}\n        if not data.get('name'):\n            data['name'] = 'thumbnail'\n\n        thumbnail_component = self.session.create_component(\n            path, data, location=None\n        )\n\n        origin_location = self.session.get(\n            'Location', ftrack_api.symbol.ORIGIN_LOCATION_ID\n        )\n        server_location = self.session.get(\n            'Location', ftrack_api.symbol.SERVER_LOCATION_ID\n        )\n        server_location.add_component(thumbnail_component, [origin_location])\n\n        # TODO: This commit can be avoided by reordering the operations in \n        # this method so that the component is transferred to ftrack.server\n        # after the thumbnail has been set.\n        # \n        # There is currently a bug in the API backend, causing the operations\n        # to *some* times be ordered wrongly, where the update occurs before\n        # the component has been created, causing an integrity error.\n        # \n        # Once this issue has been resolved, this commit can be removed and\n        # and the update placed between component creation and registration. \n        self['thumbnail_id'] = thumbnail_component['id']\n        self.session.commit()\n\n        return thumbnail_component\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/entity/factory.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nfrom __future__ import absolute_import\n\nimport logging\nimport uuid\nimport functools\n\nimport ftrack_api.attribute\nimport ftrack_api.entity.base\nimport ftrack_api.entity.location\nimport ftrack_api.entity.component\nimport ftrack_api.entity.asset_version\nimport ftrack_api.entity.project_schema\nimport ftrack_api.entity.note\nimport ftrack_api.entity.job\nimport ftrack_api.entity.user\nimport ftrack_api.symbol\nimport ftrack_api.cache\nfrom ftrack_api.logging import LazyLogMessage as L\n\n\nclass Factory(object):\n    '''Entity class factory.'''\n\n    def __init__(self):\n        '''Initialise factory.'''\n        super(Factory, self).__init__()\n        self.logger = logging.getLogger(\n            __name__ + '.' + self.__class__.__name__\n        )\n\n    def create(self, schema, bases=None):\n        '''Create and return entity class from *schema*.\n\n        *bases* should be a list of bases to give the constructed class. If not\n        specified, default to :class:`ftrack_api.entity.base.Entity`.\n\n        '''\n        entity_type = schema['id']\n        class_name = entity_type\n\n        class_bases = bases\n        if class_bases is None:\n            class_bases = [ftrack_api.entity.base.Entity]\n\n        class_namespace = dict()\n\n        # Build attributes for class.\n        attributes = ftrack_api.attribute.Attributes()\n        immutable_properties = schema.get('immutable', [])\n        computed_properties = schema.get('computed', [])\n        for name, fragment in schema.get('properties', {}).items():\n            mutable = name not in immutable_properties\n            computed = name in computed_properties\n\n            default = fragment.get('default', ftrack_api.symbol.NOT_SET)\n            if default == '{uid}':\n                default = lambda instance: str(uuid.uuid4())\n\n            data_type = fragment.get('type', ftrack_api.symbol.NOT_SET)\n\n            if data_type is not ftrack_api.symbol.NOT_SET:\n\n                if data_type in (\n                    'string', 'boolean', 'integer', 'number', 'variable',\n                    'object'\n                ):\n                    # Basic scalar attribute.\n                    if data_type == 'number':\n                        data_type = 'float'\n\n                    if data_type == 'string':\n                        data_format = fragment.get('format')\n                        if data_format == 'date-time':\n                            data_type = 'datetime'\n\n                    attribute = self.create_scalar_attribute(\n                        class_name, name, mutable, computed, default, data_type\n                    )\n                    if attribute:\n                        attributes.add(attribute)\n\n                elif data_type == 'array':\n                    attribute = self.create_collection_attribute(\n                        class_name, name, mutable\n                    )\n                    if attribute:\n                        attributes.add(attribute)\n\n                elif data_type == 'mapped_array':\n                    reference = fragment.get('items', {}).get('$ref')\n                    if not reference:\n                        self.logger.debug(L(\n                            'Skipping {0}.{1} mapped_array attribute that does '\n                            'not define a schema reference.', class_name, name\n                        ))\n                        continue\n\n                    attribute = self.create_mapped_collection_attribute(\n                        class_name, name, mutable, reference\n                    )\n                    if attribute:\n                        attributes.add(attribute)\n\n                else:\n                    self.logger.debug(L(\n                        'Skipping {0}.{1} attribute with unrecognised data '\n                        'type {2}', class_name, name, data_type\n                    ))\n            else:\n                # Reference attribute.\n                reference = fragment.get('$ref', ftrack_api.symbol.NOT_SET)\n                if reference is ftrack_api.symbol.NOT_SET:\n                    self.logger.debug(L(\n                        'Skipping {0}.{1} mapped_array attribute that does '\n                        'not define a schema reference.', class_name, name\n                    ))\n                    continue\n\n                attribute = self.create_reference_attribute(\n                    class_name, name, mutable, reference\n                )\n                if attribute:\n                    attributes.add(attribute)\n\n        default_projections = schema.get('default_projections', [])\n\n        # Construct class.\n        class_namespace['entity_type'] = entity_type\n        class_namespace['attributes'] = attributes\n        class_namespace['primary_key_attributes'] = schema['primary_key'][:]\n        class_namespace['default_projections'] = default_projections\n\n        cls = type(\n            str(class_name),  # type doesn't accept unicode.\n            tuple(class_bases),\n            class_namespace\n        )\n\n        return cls\n\n    def create_scalar_attribute(\n        self, class_name, name, mutable, computed, default, data_type\n    ):\n        '''Return appropriate scalar attribute instance.'''\n        return ftrack_api.attribute.ScalarAttribute(\n            name, data_type=data_type, default_value=default, mutable=mutable,\n            computed=computed\n        )\n\n    def create_reference_attribute(self, class_name, name, mutable, reference):\n        '''Return appropriate reference attribute instance.'''\n        return ftrack_api.attribute.ReferenceAttribute(\n            name, reference, mutable=mutable\n        )\n\n    def create_collection_attribute(self, class_name, name, mutable):\n        '''Return appropriate collection attribute instance.'''\n        return ftrack_api.attribute.CollectionAttribute(\n            name, mutable=mutable\n        )\n\n    def create_mapped_collection_attribute(\n        self, class_name, name, mutable, reference\n    ):\n        '''Return appropriate mapped collection attribute instance.'''\n        self.logger.debug(L(\n            'Skipping {0}.{1} mapped_array attribute that has '\n            'no implementation defined for reference {2}.',\n            class_name, name, reference\n        ))\n\n\nclass PerSessionDefaultKeyMaker(ftrack_api.cache.KeyMaker):\n    '''Generate key for defaults.'''\n\n    def _key(self, obj):\n        '''Return key for *obj*.'''\n        if isinstance(obj, dict):\n            entity = obj.get('entity')\n            if entity is not None:\n                # Key by session only.\n                return str(id(entity.session))\n\n        return str(obj)\n\n\n#: Memoiser for use with default callables that should only be called once per\n# session.\nmemoise_defaults = ftrack_api.cache.memoise_decorator(\n    ftrack_api.cache.Memoiser(\n        key_maker=PerSessionDefaultKeyMaker(), return_copies=False\n    )\n)\n\n#: Memoiser for use with callables that should be called once per session.\nmemoise_session = ftrack_api.cache.memoise_decorator(\n    ftrack_api.cache.Memoiser(\n        key_maker=PerSessionDefaultKeyMaker(), return_copies=False\n    )\n)\n\n\n@memoise_session\ndef _get_custom_attribute_configurations(session):\n    '''Return list of custom attribute configurations.\n\n    The configuration objects will have key, project_id, id and object_type_id\n    populated.\n\n    '''\n    return session.query(\n        'select key, project_id, id, object_type_id, entity_type, '\n        'is_hierarchical from CustomAttributeConfiguration'\n    ).all()\n\n\ndef _get_entity_configurations(entity):\n    '''Return all configurations for current collection entity.'''\n    entity_type = None\n    project_id = None\n    object_type_id = None\n\n    if 'object_type_id' in entity.keys():\n        project_id = entity['project_id']\n        entity_type = 'task'\n        object_type_id = entity['object_type_id']\n\n    if entity.entity_type == 'AssetVersion':\n        project_id = entity['asset']['parent']['project_id']\n        entity_type = 'assetversion'\n\n    if entity.entity_type == 'Project':\n        project_id = entity['id']\n        entity_type = 'show'\n\n    if entity.entity_type == 'User':\n        entity_type = 'user'\n\n    if entity.entity_type == 'Asset':\n        entity_type = 'asset'\n\n    if entity.entity_type in ('TypedContextList', 'AssetVersionList'):\n        entity_type = 'list'\n\n    if entity_type is None:\n        raise ValueError(\n            'Entity {!r} not supported.'.format(entity)\n        )\n\n    configurations = []\n    for configuration in _get_custom_attribute_configurations(\n        entity.session\n    ):\n        if (\n            configuration['entity_type'] == entity_type and\n            configuration['project_id'] in (project_id, None) and\n            configuration['object_type_id'] == object_type_id\n        ):\n            # The custom attribute configuration is for the target entity type.\n            configurations.append(configuration)\n        elif (\n            entity_type in ('asset', 'assetversion', 'show', 'task') and\n            configuration['project_id'] in (project_id, None) and\n            configuration['is_hierarchical']\n        ):\n            # The target entity type allows hierarchical attributes.\n            configurations.append(configuration)\n\n    # Return with global configurations at the end of the list. This is done\n    # so that global conigurations are shadowed by project specific if the\n    # configurations list is looped when looking for a matching `key`.\n    return sorted(\n        configurations, key=lambda item: item['project_id'] is None\n    )\n\n\nclass StandardFactory(Factory):\n    '''Standard entity class factory.'''\n\n    def create(self, schema, bases=None):\n        '''Create and return entity class from *schema*.'''\n        if not bases:\n            bases = []\n\n        extra_bases = []\n        # Customise classes.\n        if schema['id'] == 'ProjectSchema':\n            extra_bases = [ftrack_api.entity.project_schema.ProjectSchema]\n\n        elif schema['id'] == 'Location':\n            extra_bases = [ftrack_api.entity.location.Location]\n\n        elif schema['id'] == 'AssetVersion':\n            extra_bases = [ftrack_api.entity.asset_version.AssetVersion]\n\n        elif schema['id'].endswith('Component'):\n            extra_bases = [ftrack_api.entity.component.Component]\n\n        elif schema['id'] == 'Note':\n            extra_bases = [ftrack_api.entity.note.Note]\n\n        elif schema['id'] == 'Job':\n            extra_bases = [ftrack_api.entity.job.Job]\n\n        elif schema['id'] == 'User':\n            extra_bases = [ftrack_api.entity.user.User]\n\n        bases = extra_bases + bases\n\n        # If bases does not contain any items, add the base entity class.\n        if not bases:\n            bases = [ftrack_api.entity.base.Entity]\n\n        # Add mixins.\n        if 'notes' in schema.get('properties', {}):\n            bases.append(\n                ftrack_api.entity.note.CreateNoteMixin\n            )\n\n        if 'thumbnail_id' in schema.get('properties', {}):\n            bases.append(\n                ftrack_api.entity.component.CreateThumbnailMixin\n            )\n\n        cls = super(StandardFactory, self).create(schema, bases=bases)\n\n        return cls\n\n    def create_mapped_collection_attribute(\n        self, class_name, name, mutable, reference\n    ):\n        '''Return appropriate mapped collection attribute instance.'''\n        if reference == 'Metadata':\n\n            def create_metadata(proxy, data, reference):\n                '''Return metadata for *data*.'''\n                entity = proxy.collection.entity\n                session = entity.session\n                data.update({\n                    'parent_id': entity['id'],\n                    'parent_type': entity.entity_type\n                })\n                return session.create(reference, data)\n\n            creator = functools.partial(\n                create_metadata, reference=reference\n            )\n            key_attribute = 'key'\n            value_attribute = 'value'\n\n            return ftrack_api.attribute.KeyValueMappedCollectionAttribute(\n                name, creator, key_attribute, value_attribute, mutable=mutable\n            )\n\n        elif reference == 'CustomAttributeValue':\n            return (\n                ftrack_api.attribute.CustomAttributeCollectionAttribute(\n                    name, mutable=mutable\n                )\n            )\n\n        elif reference.endswith('CustomAttributeValue'):\n            def creator(proxy, data):\n                '''Create a custom attribute based on *proxy* and *data*.\n\n                Raise :py:exc:`KeyError` if related entity is already presisted\n                to the server. The proxy represents dense custom attribute\n                values and should never create new custom attribute values\n                through the proxy if entity exists on the remote.\n\n                If the entity is not persisted the ususal\n                <entity_type>CustomAttributeValue items cannot be updated as\n                the related entity does not exist on remote and values not in\n                the proxy. Instead a <entity_type>CustomAttributeValue will\n                be reconstructed and an update operation will be recorded.\n\n                '''\n                entity = proxy.collection.entity\n                if (\n                    ftrack_api.inspection.state(entity) is not\n                    ftrack_api.symbol.CREATED\n                ):\n                    raise KeyError(\n                        'Custom attributes must be created explicitly for the '\n                        'given entity type before being set.'\n                    )\n\n                configuration = None\n                for candidate in _get_entity_configurations(entity):\n                    if candidate['key'] == data['key']:\n                        configuration = candidate\n                        break\n\n                if configuration is None:\n                    raise ValueError(\n                        u'No valid custom attribute for data {0!r} was found.'\n                        .format(data)\n                    )\n\n                create_data = dict(data.items())\n                create_data['configuration_id'] = configuration['id']\n                create_data['entity_id'] = entity['id']\n\n                session = entity.session\n\n                # Create custom attribute by reconstructing it and update the\n                # value. This will prevent a create operation to be sent to the\n                # remote, as create operations for this entity type is not\n                # allowed. Instead an update operation will be recorded.\n                value = create_data.pop('value')\n                item = session.create(\n                    reference,\n                    create_data,\n                    reconstructing=True\n                )\n\n                # Record update operation.\n                item['value'] = value\n\n                return item\n\n            key_attribute = 'key'\n            value_attribute = 'value'\n\n            return ftrack_api.attribute.KeyValueMappedCollectionAttribute(\n                name, creator, key_attribute, value_attribute, mutable=mutable\n            )\n\n        self.logger.debug(L(\n            'Skipping {0}.{1} mapped_array attribute that has no configuration '\n            'for reference {2}.', class_name, name, reference\n        ))\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/entity/job.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport ftrack_api.entity.base\n\n\nclass Job(ftrack_api.entity.base.Entity):\n    '''Represent job.'''\n\n    def __init__(self, session, data=None, reconstructing=False):\n        '''Initialise entity.\n\n        *session* is an instance of :class:`ftrack_api.session.Session` that\n        this entity instance is bound to.\n\n        *data* is a mapping of key, value pairs to apply as initial attribute\n        values.\n\n        To set a job `description` visible in the web interface, *data* can\n        contain a key called `data` which should be a JSON serialised\n        dictionary containing description::\n\n            data = {\n                'status': 'running',\n                'data': json.dumps(dict(description='My job description.')),\n                ...\n            }\n\n        Will raise a :py:exc:`ValueError` if *data* contains `type` and `type`\n        is set to something not equal to \"api_job\".\n\n        *reconstructing* indicates whether this entity is being reconstructed,\n        such as from a query, and therefore should not have any special creation\n        logic applied, such as initialising defaults for missing data.\n\n        '''\n\n        if not reconstructing:\n            if data.get('type') not in ('api_job', None):\n                raise ValueError(\n                    'Invalid job type \"{0}\". Must be \"api_job\"'.format(\n                        data.get('type')\n                    )\n                )\n\n        super(Job, self).__init__(\n            session, data=data, reconstructing=reconstructing\n        )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/entity/location.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport collections\nimport functools\n\nimport ftrack_api.entity.base\nimport ftrack_api.exception\nimport ftrack_api.event.base\nimport ftrack_api.symbol\nimport ftrack_api.inspection\nfrom ftrack_api.logging import LazyLogMessage as L\n\n\nclass Location(ftrack_api.entity.base.Entity):\n    '''Represent storage for components.'''\n\n    def __init__(self, session, data=None, reconstructing=False):\n        '''Initialise entity.\n\n        *session* is an instance of :class:`ftrack_api.session.Session` that\n        this entity instance is bound to.\n\n        *data* is a mapping of key, value pairs to apply as initial attribute\n        values.\n\n        *reconstructing* indicates whether this entity is being reconstructed,\n        such as from a query, and therefore should not have any special creation\n        logic applied, such as initialising defaults for missing data.\n\n        '''\n        self.accessor = ftrack_api.symbol.NOT_SET\n        self.structure = ftrack_api.symbol.NOT_SET\n        self.resource_identifier_transformer = ftrack_api.symbol.NOT_SET\n        self.priority = 95\n        super(Location, self).__init__(\n            session, data=data, reconstructing=reconstructing\n        )\n\n    def __str__(self):\n        '''Return string representation of instance.'''\n        representation = super(Location, self).__str__()\n\n        with self.session.auto_populating(False):\n            name = self['name']\n            if name is not ftrack_api.symbol.NOT_SET:\n                representation = representation.replace(\n                    '(', '(\"{0}\", '.format(name)\n                )\n\n        return representation\n\n    def add_component(self, component, source, recursive=True):\n        '''Add *component* to location.\n\n        *component* should be a single component instance.\n\n        *source* should be an instance of another location that acts as the\n        source.\n\n        Raise :exc:`ftrack_api.ComponentInLocationError` if the *component*\n        already exists in this location.\n\n        Raise :exc:`ftrack_api.LocationError` if managing data and the generated\n        target structure for the component already exists according to the\n        accessor. This helps prevent potential data loss by avoiding overwriting\n        existing data. Note that there is a race condition between the check and\n        the write so if another process creates data at the same target during\n        that period it will be overwritten.\n\n        .. note::\n\n            A :meth:`Session.commit<ftrack_api.session.Session.commit>` may be\n            automatically issued as part of the component registration.\n\n        '''\n        return self.add_components(\n            [component], sources=source, recursive=recursive\n        )\n\n    def add_components(self, components, sources, recursive=True, _depth=0):\n        '''Add *components* to location.\n\n        *components* should be a list of component instances.\n\n        *sources* may be either a single source or a list of sources. If a list\n        then each corresponding index in *sources* will be used for each\n        *component*. A source should be an instance of another location.\n\n        Raise :exc:`ftrack_api.exception.ComponentInLocationError` if any\n        component in *components* already exists in this location. In this case,\n        no changes will be made and no data transferred.\n\n        Raise :exc:`ftrack_api.exception.LocationError` if managing data and the\n        generated target structure for the component already exists according to\n        the accessor. This helps prevent potential data loss by avoiding\n        overwriting existing data. Note that there is a race condition between\n        the check and the write so if another process creates data at the same\n        target during that period it will be overwritten.\n\n        .. note::\n\n            A :meth:`Session.commit<ftrack_api.session.Session.commit>` may be\n            automatically issued as part of the components registration.\n\n        .. important::\n\n            If this location manages data then the *components* data is first\n            transferred to the target prescribed by the structure plugin, using\n            the configured accessor. If any component fails to transfer then\n            :exc:`ftrack_api.exception.LocationError` is raised and none of the\n            components are registered with the database. In this case it is left\n            up to the caller to decide and act on manually cleaning up any\n            transferred data using the 'transferred' detail in the raised error.\n\n            Likewise, after transfer, all components are registered with the\n            database in a batch call. If any component causes an error then all\n            components will remain unregistered and\n            :exc:`ftrack_api.exception.LocationError` will be raised detailing\n            issues and any transferred data under the 'transferred' detail key.\n\n        '''\n        if (\n            isinstance(sources, basestring)\n            or not isinstance(sources, collections.Sequence)\n        ):\n            sources = [sources]\n\n        sources_count = len(sources)\n        if sources_count not in (1, len(components)):\n            raise ValueError(\n                'sources must be either a single source or a sequence of '\n                'sources with indexes corresponding to passed components.'\n            )\n\n        if not self.structure:\n            raise ftrack_api.exception.LocationError(\n                'No structure defined for location {location}.',\n                details=dict(location=self)\n            )\n\n        if not components:\n            # Optimisation: Return early when no components to process, such as\n            # when called recursively on an empty sequence component.\n            return\n\n        indent = '    ' * (_depth + 1)\n\n        # Check that components not already added to location.\n        existing_components = []\n        try:\n            self.get_resource_identifiers(components)\n\n        except ftrack_api.exception.ComponentNotInLocationError as error:\n            missing_component_ids = [\n                missing_component['id']\n                for missing_component in error.details['components']\n            ]\n            for component in components:\n                if component['id'] not in missing_component_ids:\n                    existing_components.append(component)\n\n        else:\n            existing_components.extend(components)\n\n        if existing_components:\n            # Some of the components already present in location.\n            raise ftrack_api.exception.ComponentInLocationError(\n                existing_components, self\n            )\n\n        # Attempt to transfer each component's data to this location.\n        transferred = []\n\n        for index, component in enumerate(components):\n            try:\n                # Determine appropriate source.\n                if sources_count == 1:\n                    source = sources[0]\n                else:\n                    source = sources[index]\n\n                # Add members first for container components.\n                is_container = 'members' in component.keys()\n                if is_container and recursive:\n                    self.add_components(\n                        component['members'], source, recursive=recursive,\n                        _depth=(_depth + 1)\n                    )\n\n                # Add component to this location.\n                context = self._get_context(component, source)\n                resource_identifier = self.structure.get_resource_identifier(\n                    component, context\n                )\n\n                # Manage data transfer.\n                self._add_data(component, resource_identifier, source)\n\n            except Exception as error:\n                raise ftrack_api.exception.LocationError(\n                    'Failed to transfer component {component} data to location '\n                    '{location} due to error:\\n{indent}{error}\\n{indent}'\n                    'Transferred component data that may require cleanup: '\n                    '{transferred}',\n                    details=dict(\n                        indent=indent,\n                        component=component,\n                        location=self,\n                        error=error,\n                        transferred=transferred\n                    )\n                )\n\n            else:\n                transferred.append((component, resource_identifier))\n\n        # Register all successfully transferred components.\n        components_to_register = []\n        component_resource_identifiers = []\n\n        try:\n            for component, resource_identifier in transferred:\n                if self.resource_identifier_transformer:\n                    # Optionally encode resource identifier before storing.\n                    resource_identifier = (\n                        self.resource_identifier_transformer.encode(\n                            resource_identifier,\n                            context={'component': component}\n                        )\n                    )\n\n                components_to_register.append(component)\n                component_resource_identifiers.append(resource_identifier)\n\n            # Store component in location information.\n            self._register_components_in_location(\n                components, component_resource_identifiers\n            )\n\n        except Exception as error:\n            raise ftrack_api.exception.LocationError(\n                'Failed to register components with location {location} due to '\n                'error:\\n{indent}{error}\\n{indent}Transferred component data '\n                'that may require cleanup: {transferred}',\n                details=dict(\n                    indent=indent,\n                    location=self,\n                    error=error,\n                    transferred=transferred\n                )\n            )\n\n        # Publish events.\n        for component in components_to_register:\n\n            component_id = ftrack_api.inspection.primary_key(\n                component\n            ).values()[0]\n            location_id = ftrack_api.inspection.primary_key(self).values()[0]\n\n            self.session.event_hub.publish(\n                ftrack_api.event.base.Event(\n                    topic=ftrack_api.symbol.COMPONENT_ADDED_TO_LOCATION_TOPIC,\n                    data=dict(\n                        component_id=component_id,\n                        location_id=location_id\n                    ),\n                ),\n                on_error='ignore'\n            )\n\n    def _get_context(self, component, source):\n        '''Return context for *component* and *source*.'''\n        context = {}\n        if source:\n            try:\n                source_resource_identifier = source.get_resource_identifier(\n                    component\n                )\n            except ftrack_api.exception.ComponentNotInLocationError:\n                pass\n            else:\n                context.update(dict(\n                    source_resource_identifier=source_resource_identifier\n                ))\n\n        return context\n\n    def _add_data(self, component, resource_identifier, source):\n        '''Manage transfer of *component* data from *source*.\n\n        *resource_identifier* specifies the identifier to use with this\n        locations accessor.\n\n        '''\n        self.logger.debug(L(\n            'Adding data for component {0!r} from source {1!r} to location '\n            '{2!r} using resource identifier {3!r}.',\n            component, resource_identifier, source, self\n        ))\n\n        # Read data from source and write to this location.\n        if not source.accessor:\n            raise ftrack_api.exception.LocationError(\n                'No accessor defined for source location {location}.',\n                details=dict(location=source)\n            )\n\n        if not self.accessor:\n            raise ftrack_api.exception.LocationError(\n                'No accessor defined for target location {location}.',\n                details=dict(location=self)\n            )\n\n        is_container = 'members' in component.keys()\n        if is_container:\n            # TODO: Improve this check. Possibly introduce an inspection\n            # such as ftrack_api.inspection.is_sequence_component.\n            if component.entity_type != 'SequenceComponent':\n                self.accessor.make_container(resource_identifier)\n\n        else:\n            # Try to make container of component.\n            try:\n                container = self.accessor.get_container(\n                    resource_identifier\n                )\n\n            except ftrack_api.exception.AccessorParentResourceNotFoundError:\n                # Container could not be retrieved from\n                # resource_identifier. Assume that there is no need to\n                # make the container.\n                pass\n\n            else:\n                # No need for existence check as make_container does not\n                # recreate existing containers.\n                self.accessor.make_container(container)\n\n            if self.accessor.exists(resource_identifier):\n                # Note: There is a race condition here in that the\n                # data may be added externally between the check for\n                # existence and the actual write which would still\n                # result in potential data loss. However, there is no\n                # good cross platform, cross accessor solution for this\n                # at present.\n                raise ftrack_api.exception.LocationError(\n                    'Cannot add component as data already exists and '\n                    'overwriting could result in data loss. Computed '\n                    'target resource identifier was: {0}'\n                    .format(resource_identifier)\n                )\n\n            # Read and write data.\n            source_data = source.accessor.open(\n                source.get_resource_identifier(component), 'rb'\n            )\n            target_data = self.accessor.open(resource_identifier, 'wb')\n\n            # Read/write data in chunks to avoid reading all into memory at the\n            # same time.\n            chunked_read = functools.partial(\n                source_data.read, ftrack_api.symbol.CHUNK_SIZE\n            )\n            for chunk in iter(chunked_read, ''):\n                target_data.write(chunk)\n\n            target_data.close()\n            source_data.close()\n\n    def _register_component_in_location(self, component, resource_identifier):\n        '''Register *component* in location against *resource_identifier*.'''\n        return self._register_components_in_location(\n            [component], [resource_identifier]\n        )\n\n    def _register_components_in_location(\n        self, components, resource_identifiers\n    ):\n        '''Register *components* in location against *resource_identifiers*.\n\n        Indices of *components* and *resource_identifiers* should align.\n\n        '''\n        for component, resource_identifier in zip(\n            components, resource_identifiers\n        ):\n            self.session.create(\n                'ComponentLocation', data=dict(\n                    component=component,\n                    location=self,\n                    resource_identifier=resource_identifier\n                )\n            )\n\n        self.session.commit()\n\n    def remove_component(self, component, recursive=True):\n        '''Remove *component* from location.\n\n        .. note::\n\n            A :meth:`Session.commit<ftrack_api.session.Session.commit>` may be\n            automatically issued as part of the component deregistration.\n\n        '''\n        return self.remove_components([component], recursive=recursive)\n\n    def remove_components(self, components, recursive=True):\n        '''Remove *components* from location.\n\n        .. note::\n\n            A :meth:`Session.commit<ftrack_api.session.Session.commit>` may be\n            automatically issued as part of the components deregistration.\n\n        '''\n        for component in components:\n            # Check component is in this location\n            self.get_resource_identifier(component)\n\n            # Remove members first for container components.\n            is_container = 'members' in component.keys()\n            if is_container and recursive:\n                self.remove_components(\n                    component['members'], recursive=recursive\n                )\n\n            # Remove data.\n            self._remove_data(component)\n\n            # Remove metadata.\n            self._deregister_component_in_location(component)\n\n            # Emit event.\n            component_id = ftrack_api.inspection.primary_key(\n                component\n            ).values()[0]\n            location_id = ftrack_api.inspection.primary_key(self).values()[0]\n            self.session.event_hub.publish(\n                ftrack_api.event.base.Event(\n                    topic=ftrack_api.symbol.COMPONENT_REMOVED_FROM_LOCATION_TOPIC,\n                    data=dict(\n                        component_id=component_id,\n                        location_id=location_id\n                    )\n                ),\n                on_error='ignore'\n            )\n\n    def _remove_data(self, component):\n        '''Remove data associated with *component*.'''\n        if not self.accessor:\n            raise ftrack_api.exception.LocationError(\n                'No accessor defined for location {location}.',\n                details=dict(location=self)\n            )\n\n        try:\n            self.accessor.remove(\n                self.get_resource_identifier(component)\n            )\n        except ftrack_api.exception.AccessorResourceNotFoundError:\n            # If accessor does not support detecting sequence paths then an\n            # AccessorResourceNotFoundError is raised. For now, if the\n            # component type is 'SequenceComponent' assume success.\n            if not component.entity_type == 'SequenceComponent':\n                raise\n\n    def _deregister_component_in_location(self, component):\n        '''Deregister *component* from location.'''\n        component_id = ftrack_api.inspection.primary_key(component).values()[0]\n        location_id = ftrack_api.inspection.primary_key(self).values()[0]\n\n        # TODO: Use session.get for optimisation.\n        component_location = self.session.query(\n            'ComponentLocation where component_id is {0} and location_id is '\n            '{1}'.format(component_id, location_id)\n        )[0]\n\n        self.session.delete(component_location)\n\n        # TODO: Should auto-commit here be optional?\n        self.session.commit()\n\n    def get_component_availability(self, component):\n        '''Return availability of *component* in this location as a float.'''\n        return self.session.get_component_availability(\n            component, locations=[self]\n        )[self['id']]\n\n    def get_component_availabilities(self, components):\n        '''Return availabilities of *components* in this location.\n\n        Return list of float values corresponding to each component.\n\n        '''\n        return [\n            availability[self['id']] for availability in\n            self.session.get_component_availabilities(\n                components, locations=[self]\n            )\n        ]\n\n    def get_resource_identifier(self, component):\n        '''Return resource identifier for *component*.\n\n        Raise :exc:`ftrack_api.exception.ComponentNotInLocationError` if the\n        component is not present in this location.\n\n        '''\n        return self.get_resource_identifiers([component])[0]\n\n    def get_resource_identifiers(self, components):\n        '''Return resource identifiers for *components*.\n\n        Raise :exc:`ftrack_api.exception.ComponentNotInLocationError` if any\n        of the components are not present in this location.\n\n        '''\n        resource_identifiers = self._get_resource_identifiers(components)\n\n        # Optionally decode resource identifier.\n        if self.resource_identifier_transformer:\n            for index, resource_identifier in enumerate(resource_identifiers):\n                resource_identifiers[index] = (\n                    self.resource_identifier_transformer.decode(\n                        resource_identifier,\n                        context={'component': components[index]}\n                    )\n                )\n\n        return resource_identifiers\n\n    def _get_resource_identifiers(self, components):\n        '''Return resource identifiers for *components*.\n\n        Raise :exc:`ftrack_api.exception.ComponentNotInLocationError` if any\n        of the components are not present in this location.\n\n        '''\n        component_ids_mapping = collections.OrderedDict()\n        for component in components:\n            component_id = ftrack_api.inspection.primary_key(\n                component\n            ).values()[0]\n            component_ids_mapping[component_id] = component\n\n        component_locations = self.session.query(\n            'select component_id, resource_identifier from ComponentLocation '\n            'where location_id is {0} and component_id in ({1})'\n            .format(\n                ftrack_api.inspection.primary_key(self).values()[0],\n                ', '.join(component_ids_mapping.keys())\n            )\n        )\n\n        resource_identifiers_map = {}\n        for component_location in component_locations:\n            resource_identifiers_map[component_location['component_id']] = (\n                component_location['resource_identifier']\n            )\n\n        resource_identifiers = []\n        missing = []\n        for component_id, component in component_ids_mapping.items():\n            if component_id not in resource_identifiers_map:\n                missing.append(component)\n            else:\n                resource_identifiers.append(\n                    resource_identifiers_map[component_id]\n                )\n\n        if missing:\n            raise ftrack_api.exception.ComponentNotInLocationError(\n                missing, self\n            )\n\n        return resource_identifiers\n\n    def get_filesystem_path(self, component):\n        '''Return filesystem path for *component*.'''\n        return self.get_filesystem_paths([component])[0]\n\n    def get_filesystem_paths(self, components):\n        '''Return filesystem paths for *components*.'''\n        resource_identifiers = self.get_resource_identifiers(components)\n\n        filesystem_paths = []\n        for resource_identifier in resource_identifiers:\n            filesystem_paths.append(\n                self.accessor.get_filesystem_path(resource_identifier)\n            )\n\n        return filesystem_paths\n\n    def get_url(self, component):\n        '''Return url for *component*.\n\n        Raise :exc:`~ftrack_api.exception.AccessorFilesystemPathError` if\n        URL could not be determined from *component* or\n        :exc:`~ftrack_api.exception.AccessorUnsupportedOperationError` if\n        retrieving URL is not supported by the location's accessor.\n        '''\n        resource_identifier = self.get_resource_identifier(component)\n\n        return self.accessor.get_url(resource_identifier)\n\n\nclass MemoryLocationMixin(object):\n    '''Represent storage for components.\n\n    Unlike a standard location, only store metadata for components in this\n    location in memory rather than persisting to the database.\n\n    '''\n\n    @property\n    def _cache(self):\n        '''Return cache.'''\n        try:\n            cache = self.__cache\n        except AttributeError:\n            cache = self.__cache = {}\n\n        return cache\n\n    def _register_component_in_location(self, component, resource_identifier):\n        '''Register *component* in location with *resource_identifier*.'''\n        component_id = ftrack_api.inspection.primary_key(component).values()[0]\n        self._cache[component_id] = resource_identifier\n\n    def _register_components_in_location(\n        self, components, resource_identifiers\n    ):\n        '''Register *components* in location against *resource_identifiers*.\n\n        Indices of *components* and *resource_identifiers* should align.\n\n        '''\n        for component, resource_identifier in zip(\n            components, resource_identifiers\n        ):\n            self._register_component_in_location(component, resource_identifier)\n\n    def _deregister_component_in_location(self, component):\n        '''Deregister *component* in location.'''\n        component_id = ftrack_api.inspection.primary_key(component).values()[0]\n        self._cache.pop(component_id)\n\n    def _get_resource_identifiers(self, components):\n        '''Return resource identifiers for *components*.\n\n        Raise :exc:`ftrack_api.exception.ComponentNotInLocationError` if any\n        of the referenced components are not present in this location.\n\n        '''\n        resource_identifiers = []\n        missing = []\n        for component in components:\n            component_id = ftrack_api.inspection.primary_key(\n                component\n            ).values()[0]\n            resource_identifier = self._cache.get(component_id)\n            if resource_identifier is None:\n                missing.append(component)\n            else:\n                resource_identifiers.append(resource_identifier)\n\n        if missing:\n            raise ftrack_api.exception.ComponentNotInLocationError(\n                missing, self\n            )\n\n        return resource_identifiers\n\n\nclass UnmanagedLocationMixin(object):\n    '''Location that does not manage data.'''\n\n    def _add_data(self, component, resource_identifier, source):\n        '''Manage transfer of *component* data from *source*.\n\n        *resource_identifier* specifies the identifier to use with this\n        locations accessor.\n\n        Overridden to have no effect.\n\n        '''\n        return\n\n    def _remove_data(self, component):\n        '''Remove data associated with *component*.\n\n        Overridden to have no effect.\n\n        '''\n        return\n\n\nclass OriginLocationMixin(MemoryLocationMixin, UnmanagedLocationMixin):\n    '''Special origin location that expects sources as filepaths.'''\n\n    def _get_context(self, component, source):\n        '''Return context for *component* and *source*.'''\n        context = {}\n        if source:\n            context.update(dict(\n                source_resource_identifier=source\n            ))\n\n        return context\n\n\nclass ServerLocationMixin(object):\n    '''Location representing ftrack server.\n\n    Adds convenience methods to location, specific to ftrack server.\n    '''\n    def get_thumbnail_url(self, component, size=None):\n        '''Return thumbnail url for *component*.\n\n        Optionally, specify *size* to constrain the downscaled image to size\n        x size pixels.\n\n        Raise :exc:`~ftrack_api.exception.AccessorFilesystemPathError` if\n        URL could not be determined from *resource_identifier* or\n        :exc:`~ftrack_api.exception.AccessorUnsupportedOperationError` if\n        retrieving URL is not supported by the location's accessor.\n        '''\n        resource_identifier = self.get_resource_identifier(component)\n        return self.accessor.get_thumbnail_url(resource_identifier, size)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/entity/note.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport warnings\n\nimport ftrack_api.entity.base\n\n\nclass Note(ftrack_api.entity.base.Entity):\n    '''Represent a note.'''\n\n    def create_reply(\n        self, content, author\n    ):\n        '''Create a reply with *content* and *author*.\n\n        .. note::\n\n            This is a helper method. To create replies manually use the\n            standard :meth:`Session.create` method.\n\n        '''\n        reply = self.session.create(\n            'Note', {\n                'author': author,\n                'content': content\n            }\n        )\n\n        self['replies'].append(reply)\n\n        return reply\n\n\nclass CreateNoteMixin(object):\n    '''Mixin to add create_note method on entity class.'''\n\n    def create_note(\n        self, content, author, recipients=None, category=None, labels=None\n    ):\n        '''Create note with *content*, *author*.\n\n        NoteLabels can be set by including *labels*.\n\n        Note category can be set by including *category*.\n        \n        *recipients* can be specified as a list of user or group instances.\n\n        '''\n        note_label_support = 'NoteLabel' in self.session.types\n\n        if not labels:\n            labels = []\n\n        if labels and not note_label_support:\n            raise ValueError(\n                'NoteLabel is not supported by the current server version.'\n            )\n\n        if category and labels:\n            raise ValueError(\n                'Both category and labels cannot be set at the same time.'\n            )\n\n        if not recipients:\n            recipients = []\n\n        data = {\n            'content': content,\n            'author': author\n        }\n\n        if category:\n            if note_label_support:\n                labels = [category]\n                warnings.warn(\n                    'category argument will be removed in an upcoming version, '\n                    'please use labels instead.',\n                    PendingDeprecationWarning\n                )\n            else:\n                data['category_id'] = category['id']\n\n        note = self.session.create('Note', data)\n\n        self['notes'].append(note)\n\n        for resource in recipients:\n            recipient = self.session.create('Recipient', {\n                'note_id': note['id'],\n                'resource_id': resource['id']\n            })\n\n            note['recipients'].append(recipient)\n\n        for label in labels:\n            self.session.create(\n                'NoteLabelLink',\n                {\n                    'label_id': label['id'],\n                    'note_id': note['id']\n                }\n            )\n\n        return note\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/entity/project_schema.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport ftrack_api.entity.base\n\n\nclass ProjectSchema(ftrack_api.entity.base.Entity):\n    '''Class representing ProjectSchema.'''\n\n    def get_statuses(self, schema, type_id=None):\n        '''Return statuses for *schema* and optional *type_id*.\n\n        *type_id* is the id of the Type for a TypedContext and can be used to\n        get statuses where the workflow has been overridden.\n\n        '''\n        # Task has overrides and need to be handled separately.\n        if schema == 'Task':\n            if type_id is not None:\n                overrides = self['_overrides']\n                for override in overrides:\n                    if override['type_id'] == type_id:\n                        return override['workflow_schema']['statuses'][:]\n\n            return self['_task_workflow']['statuses'][:]\n\n        elif schema == 'AssetVersion':\n            return self['_version_workflow']['statuses'][:]\n\n        else:\n            try:\n                EntityTypeClass = self.session.types[schema]\n            except KeyError:\n                raise ValueError('Schema {0} does not exist.'.format(schema))\n\n            object_type_id_attribute = EntityTypeClass.attributes.get(\n                'object_type_id'\n            )\n\n            try:\n                object_type_id = object_type_id_attribute.default_value\n            except AttributeError:\n                raise ValueError(\n                    'Schema {0} does not have statuses.'.format(schema)\n                )\n\n            for _schema in self['_schemas']:\n                if _schema['type_id'] == object_type_id:\n                    result = self.session.query(\n                        'select task_status from SchemaStatus '\n                        'where schema_id is {0}'.format(_schema['id'])\n                    )\n                    return [\n                        schema_type['task_status'] for schema_type in result\n                    ]\n\n            raise ValueError(\n                'No valid statuses were found for schema {0}.'.format(schema)\n            )\n\n    def get_types(self, schema):\n        '''Return types for *schema*.'''\n        # Task need to be handled separately.\n        if schema == 'Task':\n            return self['_task_type_schema']['types'][:]\n\n        else:\n            try:\n                EntityTypeClass = self.session.types[schema]\n            except KeyError:\n                raise ValueError('Schema {0} does not exist.'.format(schema))\n\n            object_type_id_attribute = EntityTypeClass.attributes.get(\n                'object_type_id'\n            )\n\n            try:\n                object_type_id = object_type_id_attribute.default_value\n            except AttributeError:\n                raise ValueError(\n                    'Schema {0} does not have types.'.format(schema)\n                )\n\n            for _schema in self['_schemas']:\n                if _schema['type_id'] == object_type_id:\n                    result = self.session.query(\n                        'select task_type from SchemaType '\n                        'where schema_id is {0}'.format(_schema['id'])\n                    )\n                    return [schema_type['task_type'] for schema_type in result]\n\n            raise ValueError(\n                'No valid types were found for schema {0}.'.format(schema)\n            )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/entity/user.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport arrow\n\nimport ftrack_api.entity.base\nimport ftrack_api.exception\n\n\nclass User(ftrack_api.entity.base.Entity):\n    '''Represent a user.'''\n\n    def start_timer(self, context=None, comment='', name=None, force=False):\n        '''Start a timer for *context* and return it.\n\n        *force* can be used to automatically stop an existing timer and create a\n        timelog for it. If you need to get access to the created timelog, use\n        :func:`stop_timer` instead.\n\n        *comment* and *name* are optional but will be set on the timer.\n\n        .. note::\n\n            This method will automatically commit the changes and if *force* is\n            False then it will fail with a\n            :class:`ftrack_api.exception.NotUniqueError` exception if a\n            timer is already running.\n\n        '''\n        if force:\n            try:\n                self.stop_timer()\n            except ftrack_api.exception.NoResultFoundError:\n                self.logger.debug('Failed to stop existing timer.')\n\n        timer = self.session.create('Timer', {\n            'user': self,\n            'context': context,\n            'name': name,\n            'comment': comment\n        })\n\n        # Commit the new timer and try to catch any error that indicate another\n        # timelog already exists and inform the user about it.\n        try:\n            self.session.commit()\n        except ftrack_api.exception.ServerError as error:\n            if 'IntegrityError' in str(error):\n                raise ftrack_api.exception.NotUniqueError(\n                    ('Failed to start a timelog for user with id: {0}, it is '\n                     'likely that a timer is already running. Either use '\n                     'force=True or stop the timer first.').format(self['id'])\n                )\n            else:\n                # Reraise the error as it might be something unrelated.\n                raise\n\n        return timer\n\n    def stop_timer(self):\n        '''Stop the current timer and return a timelog created from it.\n\n        If a timer is not running, a\n        :exc:`ftrack_api.exception.NoResultFoundError` exception will be\n        raised.\n\n        .. note::\n\n            This method will automatically commit the changes.\n\n        '''\n        timer = self.session.query(\n            'Timer where user_id = \"{0}\"'.format(self['id'])\n        ).one()\n\n        # If the server is running in the same timezone as the local\n        # timezone, we remove the TZ offset to get the correct duration.\n        is_timezone_support_enabled = self.session.server_information.get(\n            'is_timezone_support_enabled', None\n        )\n        if is_timezone_support_enabled is None:\n            self.logger.warning(\n                'Could not identify if server has timezone support enabled. '\n                'Will assume server is running in UTC.'\n            )\n            is_timezone_support_enabled = True\n\n        if is_timezone_support_enabled:\n            now = arrow.now()\n        else:\n            now = arrow.now().replace(tzinfo='utc')\n\n        delta = now - timer['start']\n        duration = delta.days * 24 * 60 * 60 + delta.seconds\n\n        timelog = self.session.create('Timelog', {\n            'user_id': timer['user_id'],\n            'context_id': timer['context_id'],\n            'comment': timer['comment'],\n            'start': timer['start'],\n            'duration': duration,\n            'name': timer['name']\n        })\n\n        self.session.delete(timer)\n        self.session.commit()\n\n        return timelog\n\n    def send_invite(self):\n        '''Send a invation email to the user'''\n\n        self.session.send_user_invite(\n            self\n        )\n    def reset_api_key(self):\n        '''Reset the users api key.'''\n\n        response = self.session.reset_remote(\n            'api_key', entity=self\n        )\n\n        return response['api_key']\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/event/__init__.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/event/base.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport uuid\nimport collections\n\n\nclass Event(collections.MutableMapping):\n    '''Represent a single event.'''\n\n    def __init__(self, topic, id=None, data=None, sent=None,\n                 source=None, target='', in_reply_to_event=None):\n        '''Initialise event.\n\n        *topic* is the required topic for the event. It can use a dotted\n        notation to demarcate groupings. For example, 'ftrack.update'.\n\n        *id* is the unique id for this event instance. It is primarily used when\n        replying to an event. If not supplied a default uuid based value will\n        be used.\n\n        *data* refers to event specific data. It should be a mapping structure\n        and defaults to an empty dictionary if not supplied.\n\n        *sent* is the timestamp the event is sent. It will be set automatically\n        as send time unless specified here.\n\n        *source* is information about where the event originated. It should be\n        a mapping and include at least a unique id value under an 'id' key. If\n        not specified, senders usually populate the value automatically at\n        publish time.\n\n        *target* can be an expression that targets this event. For example,\n        a reply event would target the event to the sender of the source event.\n        The expression will be tested against subscriber information only.\n\n        *in_reply_to_event* is used when replying to an event and should contain\n        the unique id of the event being replied to.\n\n        '''\n        super(Event, self).__init__()\n        self._data = dict(\n            id=id or uuid.uuid4().hex,\n            data=data or {},\n            topic=topic,\n            sent=sent,\n            source=source or {},\n            target=target,\n            in_reply_to_event=in_reply_to_event\n        )\n        self._stopped = False\n\n    def stop(self):\n        '''Stop further processing of this event.'''\n        self._stopped = True\n\n    def is_stopped(self):\n        '''Return whether event has been stopped.'''\n        return self._stopped\n\n    def __str__(self):\n        '''Return string representation.'''\n        return '<{0} {1}>'.format(\n            self.__class__.__name__, str(self._data)\n        )\n\n    def __getitem__(self, key):\n        '''Return value for *key*.'''\n        return self._data[key]\n\n    def __setitem__(self, key, value):\n        '''Set *value* for *key*.'''\n        self._data[key] = value\n\n    def __delitem__(self, key):\n        '''Remove *key*.'''\n        del self._data[key]\n\n    def __iter__(self):\n        '''Iterate over all keys.'''\n        return iter(self._data)\n\n    def __len__(self):\n        '''Return count of keys.'''\n        return len(self._data)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/event/expression.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nfrom operator import eq, ne, ge, le, gt, lt\n\nfrom pyparsing import (Group, Word, CaselessKeyword, Forward,\n                       FollowedBy, Suppress, oneOf, OneOrMore, Optional,\n                       alphanums, quotedString, removeQuotes)\n\nimport ftrack_api.exception\n\n# Do not enable packrat since it is not thread-safe and will result in parsing\n# exceptions in a multi threaded environment.\n# ParserElement.enablePackrat()\n\n\nclass Parser(object):\n    '''Parse string based expression into :class:`Expression` instance.'''\n\n    def __init__(self):\n        '''Initialise parser.'''\n        self._operators = {\n            '=': eq,\n            '!=': ne,\n            '>=': ge,\n            '<=': le,\n            '>': gt,\n            '<': lt\n        }\n        self._parser = self._construct_parser()\n        super(Parser, self).__init__()\n\n    def _construct_parser(self):\n        '''Construct and return parser.'''\n        field = Word(alphanums + '_.')\n        operator = oneOf(self._operators.keys())\n        value = Word(alphanums + '-_,./*@+')\n        quoted_value = quotedString('quoted_value').setParseAction(removeQuotes)\n\n        condition = Group(\n            field + operator + (quoted_value | value)\n        )('condition')\n\n        not_ = Optional(Suppress(CaselessKeyword('not')))('not')\n        and_ = Suppress(CaselessKeyword('and'))('and')\n        or_ = Suppress(CaselessKeyword('or'))('or')\n\n        expression = Forward()\n        parenthesis = Suppress('(') + expression + Suppress(')')\n        previous = condition | parenthesis\n\n        for conjunction in (not_, and_, or_):\n            current = Forward()\n\n            if conjunction in (and_, or_):\n                conjunction_expression = (\n                    FollowedBy(previous + conjunction + previous)\n                    + Group(\n                        previous + OneOrMore(conjunction + previous)\n                    )(conjunction.resultsName)\n                )\n\n            elif conjunction in (not_, ):\n                conjunction_expression = (\n                    FollowedBy(conjunction.expr + current)\n                    + Group(conjunction + current)(conjunction.resultsName)\n                )\n\n            else:  # pragma: no cover\n                raise ValueError('Unrecognised conjunction.')\n\n            current <<= (conjunction_expression | previous)\n            previous = current\n\n        expression <<= previous\n        return expression('expression')\n\n    def parse(self, expression):\n        '''Parse string *expression* into :class:`Expression`.\n\n        Raise :exc:`ftrack_api.exception.ParseError` if *expression* could\n        not be parsed.\n\n        '''\n        result = None\n        expression = expression.strip()\n        if expression:\n            try:\n                result = self._parser.parseString(\n                    expression, parseAll=True\n                )\n            except Exception as error:\n                raise ftrack_api.exception.ParseError(\n                    'Failed to parse: {0}. {1}'.format(expression, error)\n                )\n\n        return self._process(result)\n\n    def _process(self, result):\n        '''Process *result* using appropriate method.\n\n        Method called is determined by the name of the result.\n\n        '''\n        method_name = '_process_{0}'.format(result.getName())\n        method = getattr(self, method_name)\n        return method(result)\n\n    def _process_expression(self, result):\n        '''Process *result* as expression.'''\n        return self._process(result[0])\n\n    def _process_not(self, result):\n        '''Process *result* as NOT operation.'''\n        return Not(self._process(result[0]))\n\n    def _process_and(self, result):\n        '''Process *result* as AND operation.'''\n        return All([self._process(entry) for entry in result])\n\n    def _process_or(self, result):\n        '''Process *result* as OR operation.'''\n        return Any([self._process(entry) for entry in result])\n\n    def _process_condition(self, result):\n        '''Process *result* as condition.'''\n        key, operator, value = result\n        return Condition(key, self._operators[operator], value)\n\n    def _process_quoted_value(self, result):\n        '''Process *result* as quoted value.'''\n        return result\n\n\nclass Expression(object):\n    '''Represent a structured expression to test candidates against.'''\n\n    def __str__(self):\n        '''Return string representation.'''\n        return '<{0}>'.format(self.__class__.__name__)\n\n    def match(self, candidate):\n        '''Return whether *candidate* satisfies this expression.'''\n        return True\n\n\nclass All(Expression):\n    '''Match candidate that matches all of the specified expressions.\n\n    .. note::\n\n        If no expressions are supplied then will always match.\n\n    '''\n\n    def __init__(self, expressions=None):\n        '''Initialise with list of *expressions* to match against.'''\n        self._expressions = expressions or []\n        super(All, self).__init__()\n\n    def __str__(self):\n        '''Return string representation.'''\n        return '<{0} [{1}]>'.format(\n            self.__class__.__name__,\n            ' '.join(map(str, self._expressions))\n        )\n\n    def match(self, candidate):\n        '''Return whether *candidate* satisfies this expression.'''\n        return all([\n            expression.match(candidate) for expression in self._expressions\n        ])\n\n\nclass Any(Expression):\n    '''Match candidate that matches any of the specified expressions.\n\n    .. note::\n\n        If no expressions are supplied then will never match.\n\n    '''\n\n    def __init__(self, expressions=None):\n        '''Initialise with list of *expressions* to match against.'''\n        self._expressions = expressions or []\n        super(Any, self).__init__()\n\n    def __str__(self):\n        '''Return string representation.'''\n        return '<{0} [{1}]>'.format(\n            self.__class__.__name__,\n            ' '.join(map(str, self._expressions))\n        )\n\n    def match(self, candidate):\n        '''Return whether *candidate* satisfies this expression.'''\n        return any([\n            expression.match(candidate) for expression in self._expressions\n        ])\n\n\nclass Not(Expression):\n    '''Negate expression.'''\n\n    def __init__(self, expression):\n        '''Initialise with *expression* to negate.'''\n        self._expression = expression\n        super(Not, self).__init__()\n\n    def __str__(self):\n        '''Return string representation.'''\n        return '<{0} {1}>'.format(\n            self.__class__.__name__,\n            self._expression\n        )\n\n    def match(self, candidate):\n        '''Return whether *candidate* satisfies this expression.'''\n        return not self._expression.match(candidate)\n\n\nclass Condition(Expression):\n    '''Represent condition.'''\n\n    def __init__(self, key, operator, value):\n        '''Initialise condition.\n\n        *key* is the key to check on the data when matching. It can be a nested\n        key represented by dots. For example, 'data.eventType' would attempt to\n        match candidate['data']['eventType']. If the candidate is missing any\n        of the requested keys then the match fails immediately.\n\n        *operator* is the operator function to use to perform the match between\n        the retrieved candidate value and the conditional *value*.\n\n        If *value* is a string, it can use a wildcard '*' at the end to denote\n        that any values matching the substring portion are valid when matching\n        equality only.\n\n        '''\n        self._key = key\n        self._operator = operator\n        self._value = value\n        self._wildcard = '*'\n        self._operatorMapping = {\n            eq: '=',\n            ne: '!=',\n            ge: '>=',\n            le: '<=',\n            gt: '>',\n            lt: '<'\n        }\n\n    def __str__(self):\n        '''Return string representation.'''\n        return '<{0} {1}{2}{3}>'.format(\n            self.__class__.__name__,\n            self._key,\n            self._operatorMapping.get(self._operator, self._operator),\n            self._value\n        )\n\n    def match(self, candidate):\n        '''Return whether *candidate* satisfies this expression.'''\n        key_parts = self._key.split('.')\n\n        try:\n            value = candidate\n            for keyPart in key_parts:\n                value = value[keyPart]\n        except (KeyError, TypeError):\n            return False\n\n        if (\n            self._operator is eq\n            and isinstance(self._value, basestring)\n            and self._value[-1] == self._wildcard\n        ):\n            return self._value[:-1] in value\n        else:\n            return self._operator(value, self._value)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/event/hub.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2013 ftrack\n\nfrom __future__ import absolute_import\n\nimport collections\nimport urlparse\nimport threading\nimport Queue as queue\nimport logging\nimport time\nimport uuid\nimport operator\nimport functools\nimport json\nimport socket\nimport warnings\n\nimport requests\nimport requests.exceptions\nimport websocket\n\nimport ftrack_api.exception\nimport ftrack_api.event.base\nimport ftrack_api.event.subscriber\nimport ftrack_api.event.expression\nfrom ftrack_api.logging import LazyLogMessage as L\n\n\nSocketIoSession = collections.namedtuple('SocketIoSession', [\n    'id',\n    'heartbeatTimeout',\n    'supportedTransports',\n])\n\n\nServerDetails = collections.namedtuple('ServerDetails', [\n    'scheme',\n    'hostname',\n    'port',\n])\n\n\n\n\nclass EventHub(object):\n    '''Manage routing of events.'''\n\n    _future_signature_warning = (\n        'When constructing your Session object you did not explicitly define '\n        'auto_connect_event_hub as True even though you appear to be publishing '\n        'and / or subscribing to asynchronous events. In version version 2.0 of '\n        'the ftrack-python-api the default behavior will change from True '\n        'to False. Please make sure to update your tools. You can read more at '\n        'http://ftrack-python-api.rtd.ftrack.com/en/stable/release/migration.html'\n    )\n\n    def __init__(self, server_url, api_user, api_key):\n        '''Initialise hub, connecting to ftrack *server_url*.\n\n        *api_user* is the user to authenticate as and *api_key* is the API key\n        to authenticate with.\n\n        '''\n        super(EventHub, self).__init__()\n        self.logger = logging.getLogger(\n            __name__ + '.' + self.__class__.__name__\n        )\n        self.id = uuid.uuid4().hex\n        self._connection = None\n\n        self._unique_packet_id = 0\n        self._packet_callbacks = {}\n        self._lock = threading.RLock()\n\n        self._wait_timeout = 4\n\n        self._subscribers = []\n        self._reply_callbacks = {}\n        self._intentional_disconnect = False\n\n        self._event_queue = queue.Queue()\n        self._event_namespace = 'ftrack.event'\n        self._expression_parser = ftrack_api.event.expression.Parser()\n\n        # Default values for auto reconnection timeout on unintentional\n        # disconnection. Equates to 5 minutes.\n        self._auto_reconnect_attempts = 30\n        self._auto_reconnect_delay = 10\n\n        self._deprecation_warning_auto_connect = False\n\n        # Mapping of Socket.IO codes to meaning.\n        self._code_name_mapping = {\n            '0': 'disconnect',\n            '1': 'connect',\n            '2': 'heartbeat',\n            '3': 'message',\n            '4': 'json',\n            '5': 'event',\n            '6': 'acknowledge',\n            '7': 'error'\n        }\n        self._code_name_mapping.update(\n            dict((name, code) for code, name in self._code_name_mapping.items())\n        )\n\n        self._server_url = server_url\n        self._api_user = api_user\n        self._api_key = api_key\n\n        # Parse server URL and store server details.\n        url_parse_result = urlparse.urlparse(self._server_url)\n        if not url_parse_result.scheme:\n            raise ValueError('Could not determine scheme from server url.')\n\n        if not url_parse_result.hostname:\n            raise ValueError('Could not determine hostname from server url.')\n\n        self.server = ServerDetails(\n            url_parse_result.scheme,\n            url_parse_result.hostname,\n            url_parse_result.port\n        )\n\n    def get_server_url(self):\n        '''Return URL to server.'''\n        return '{0}://{1}'.format(\n            self.server.scheme, self.get_network_location()\n        )\n\n    def get_network_location(self):\n        '''Return network location part of url (hostname with optional port).'''\n        if self.server.port:\n            return '{0}:{1}'.format(self.server.hostname, self.server.port)\n        else:\n            return self.server.hostname\n\n    @property\n    def secure(self):\n        '''Return whether secure connection used.'''\n        return self.server.scheme == 'https'\n\n    def connect(self):\n        '''Initialise connection to server.\n\n        Raise :exc:`ftrack_api.exception.EventHubConnectionError` if already\n        connected or connection fails.\n\n        '''\n\n        self._deprecation_warning_auto_connect = False\n\n        if self.connected:\n            raise ftrack_api.exception.EventHubConnectionError(\n                'Already connected.'\n            )\n\n        # Reset flag tracking whether disconnection was intentional.\n        self._intentional_disconnect = False\n\n        try:\n            # Connect to socket.io server using websocket transport.\n            session = self._get_socket_io_session()\n\n            if 'websocket' not in session.supportedTransports:\n                raise ValueError(\n                    'Server does not support websocket sessions.'\n                )\n\n            scheme = 'wss' if self.secure else 'ws'\n            url = '{0}://{1}/socket.io/1/websocket/{2}'.format(\n                scheme, self.get_network_location(), session.id\n            )\n\n            # timeout is set to 60 seconds to avoid the issue where the socket\n            # ends up in a bad state where it is reported as connected but the\n            # connection has been closed. The issue happens often when connected\n            # to a secure socket and the computer goes to sleep.\n            # More information on how the timeout works can be found here:\n            # https://docs.python.org/2/library/socket.html#socket.socket.setblocking\n            self._connection = websocket.create_connection(url, timeout=60)\n\n        except Exception as error:\n            error_message = (\n                'Failed to connect to event server at {server_url} with '\n                'error: \"{error}\".'\n            )\n\n            error_details = {\n                'error': unicode(error),\n                'server_url': self.get_server_url()\n            }\n\n            self.logger.debug(\n                L(\n                    error_message, **error_details\n                ),\n                exc_info=1\n            )\n            raise ftrack_api.exception.EventHubConnectionError(\n                error_message,\n                details=error_details\n            )\n\n        # Start background processing thread.\n        self._processor_thread = _ProcessorThread(self)\n        self._processor_thread.start()\n\n        # Subscribe to reply events if not already. Note: Only adding the\n        # subscriber locally as the following block will notify server of all\n        # existing subscribers, which would cause the server to report a\n        # duplicate subscriber error if EventHub.subscribe was called here.\n        try:\n            self._add_subscriber(\n                'topic=ftrack.meta.reply',\n                self._handle_reply,\n                subscriber=dict(\n                    id=self.id\n                )\n            )\n        except ftrack_api.exception.NotUniqueError:\n            pass\n\n        # Now resubscribe any existing stored subscribers. This can happen when\n        # reconnecting automatically for example.\n        for subscriber in self._subscribers[:]:\n            self._notify_server_about_subscriber(subscriber)\n\n    @property\n    def connected(self):\n        '''Return if connected.'''\n        return self._connection is not None and self._connection.connected\n\n    def disconnect(self, unsubscribe=True):\n        '''Disconnect from server.\n\n        Raise :exc:`ftrack_api.exception.EventHubConnectionError` if not\n        currently connected.\n\n        If *unsubscribe* is True then unsubscribe all current subscribers\n        automatically before disconnecting.\n\n        '''\n        if not self.connected:\n            raise ftrack_api.exception.EventHubConnectionError(\n                'Not currently connected.'\n            )\n\n        else:\n            # Set flag to indicate disconnection was intentional.\n            self._intentional_disconnect = True\n\n            # Set blocking to true on socket to make sure unsubscribe events\n            # are emitted before closing the connection.\n            self._connection.sock.setblocking(1)\n\n            # Unsubscribe all subscribers.\n            if unsubscribe:\n                for subscriber in self._subscribers[:]:\n                    self.unsubscribe(subscriber.metadata['id'])\n\n            # Now disconnect.\n            self._connection.close()\n            self._connection = None\n\n            # Shutdown background processing thread.\n            self._processor_thread.cancel()\n\n            # Join to it if it is not current thread to help ensure a clean\n            # shutdown.\n            if threading.current_thread() != self._processor_thread:\n                self._processor_thread.join(self._wait_timeout)\n\n    def reconnect(self, attempts=10, delay=5):\n        '''Reconnect to server.\n\n         Make *attempts* number of attempts with *delay* in seconds between each\n         attempt.\n\n        .. note::\n\n            All current subscribers will be automatically resubscribed after\n            successful reconnection.\n\n        Raise :exc:`ftrack_api.exception.EventHubConnectionError` if fail to\n        reconnect.\n\n        '''\n        try:\n            self.disconnect(unsubscribe=False)\n        except ftrack_api.exception.EventHubConnectionError:\n            pass\n\n        for attempt in range(attempts):\n            self.logger.debug(L(\n                'Reconnect attempt {0} of {1}', attempt, attempts\n            ))\n\n            # Silence logging temporarily to avoid lots of failed connection\n            # related information.\n            try:\n                logging.disable(logging.CRITICAL)\n\n                try:\n                    self.connect()\n                except ftrack_api.exception.EventHubConnectionError:\n                    time.sleep(delay)\n                else:\n                    break\n\n            finally:\n                logging.disable(logging.NOTSET)\n\n        if not self.connected:\n            raise ftrack_api.exception.EventHubConnectionError(\n                'Failed to reconnect to event server at {0} after {1} attempts.'\n                .format(self.get_server_url(), attempts)\n            )\n\n    def wait(self, duration=None):\n        '''Wait for events and handle as they arrive.\n\n        If *duration* is specified, then only process events until duration is\n        reached. *duration* is in seconds though float values can be used for\n        smaller values.\n\n        '''\n        started = time.time()\n\n        while True:\n            try:\n                event = self._event_queue.get(timeout=0.1)\n            except queue.Empty:\n                pass\n            else:\n                self._handle(event)\n\n                # Additional special processing of events.\n                if event['topic'] == 'ftrack.meta.disconnected':\n                    break\n\n            if duration is not None:\n                if (time.time() - started) > duration:\n                    break\n\n    def get_subscriber_by_identifier(self, identifier):\n        '''Return subscriber with matching *identifier*.\n\n        Return None if no subscriber with *identifier* found.\n\n        '''\n        for subscriber in self._subscribers[:]:\n            if subscriber.metadata.get('id') == identifier:\n                return subscriber\n\n        return None\n\n    def subscribe(self, subscription, callback, subscriber=None, priority=100):\n        '''Register *callback* for *subscription*.\n\n        A *subscription* is a string that can specify in detail which events the\n        callback should receive. The filtering is applied against each event\n        object. Nested references are supported using '.' separators.\n        For example, 'topic=foo and data.eventType=Shot' would match the\n        following event::\n\n            <Event {'topic': 'foo', 'data': {'eventType': 'Shot'}}>\n\n        The *callback* should accept an instance of\n        :class:`ftrack_api.event.base.Event` as its sole argument.\n\n        Callbacks are called in order of *priority*. The lower the priority\n        number the sooner it will be called, with 0 being the first. The\n        default priority is 100. Note that priority only applies against other\n        callbacks registered with this hub and not as a global priority.\n\n        An earlier callback can prevent processing of subsequent callbacks by\n        calling :meth:`Event.stop` on the passed `event` before\n        returning.\n\n        .. warning::\n\n            Handlers block processing of other received events. For long\n            running callbacks it is advisable to delegate the main work to\n            another process or thread.\n\n        A *callback* can be attached to *subscriber* information that details\n        the subscriber context. A subscriber context will be generated\n        automatically if not supplied.\n\n        .. note::\n\n            The subscription will be stored locally, but until the server\n            receives notification of the subscription it is possible the\n            callback will not be called.\n\n        Return subscriber identifier.\n\n        Raise :exc:`ftrack_api.exception.NotUniqueError` if a subscriber with\n        the same identifier already exists.\n\n        '''\n        # Add subscriber locally.\n        subscriber = self._add_subscriber(\n            subscription, callback, subscriber, priority\n        )\n\n        # Notify server now if possible.\n        try:\n            self._notify_server_about_subscriber(subscriber)\n        except ftrack_api.exception.EventHubConnectionError:\n            self.logger.debug(L(\n                'Failed to notify server about new subscriber {0} '\n                'as server not currently reachable.', subscriber.metadata['id']\n            ))\n\n        return subscriber.metadata['id']\n\n    def _add_subscriber(\n        self, subscription, callback, subscriber=None, priority=100\n    ):\n        '''Add subscriber locally.\n\n        See :meth:`subscribe` for argument descriptions.\n\n        Return :class:`ftrack_api.event.subscriber.Subscriber` instance.\n\n        Raise :exc:`ftrack_api.exception.NotUniqueError` if a subscriber with\n        the same identifier already exists.\n\n        '''\n        if subscriber is None:\n            subscriber = {}\n\n        subscriber.setdefault('id', uuid.uuid4().hex)\n\n        # Check subscriber not already subscribed.\n        existing_subscriber = self.get_subscriber_by_identifier(\n            subscriber['id']\n        )\n\n        if existing_subscriber is not None:\n            raise ftrack_api.exception.NotUniqueError(\n                'Subscriber with identifier {0} already exists.'\n                .format(subscriber['id'])\n            )\n\n        subscriber = ftrack_api.event.subscriber.Subscriber(\n            subscription=subscription,\n            callback=callback,\n            metadata=subscriber,\n            priority=priority\n        )\n\n        self._subscribers.append(subscriber)\n\n        return subscriber\n\n    def _notify_server_about_subscriber(self, subscriber):\n        '''Notify server of new *subscriber*.'''\n        subscribe_event = ftrack_api.event.base.Event(\n            topic='ftrack.meta.subscribe',\n            data=dict(\n                subscriber=subscriber.metadata,\n                subscription=str(subscriber.subscription)\n            )\n        )\n\n        self._publish(\n            subscribe_event,\n            callback=functools.partial(self._on_subscribed, subscriber)\n        )\n\n    def _on_subscribed(self, subscriber, response):\n        '''Handle acknowledgement of subscription.'''\n        if response.get('success') is False:\n            self.logger.warning(L(\n                'Server failed to subscribe subscriber {0}: {1}',\n                subscriber.metadata['id'], response.get('message')\n            ))\n\n    def unsubscribe(self, subscriber_identifier):\n        '''Unsubscribe subscriber with *subscriber_identifier*.\n\n        .. note::\n\n            If the server is not reachable then it won't be notified of the\n            unsubscription. However, the subscriber will be removed locally\n            regardless.\n\n        '''\n        subscriber = self.get_subscriber_by_identifier(subscriber_identifier)\n\n        if subscriber is None:\n            raise ftrack_api.exception.NotFoundError(\n                'Cannot unsubscribe missing subscriber with identifier {0}'\n                .format(subscriber_identifier)\n            )\n\n        self._subscribers.pop(self._subscribers.index(subscriber))\n\n        # Notify the server if possible.\n        unsubscribe_event = ftrack_api.event.base.Event(\n            topic='ftrack.meta.unsubscribe',\n            data=dict(subscriber=subscriber.metadata)\n        )\n\n        try:\n            self._publish(\n                unsubscribe_event,\n                callback=functools.partial(self._on_unsubscribed, subscriber)\n            )\n        except ftrack_api.exception.EventHubConnectionError:\n            self.logger.debug(L(\n                'Failed to notify server to unsubscribe subscriber {0} as '\n                'server not currently reachable.', subscriber.metadata['id']\n            ))\n\n    def _on_unsubscribed(self, subscriber, response):\n        '''Handle acknowledgement of unsubscribing *subscriber*.'''\n        if response.get('success') is not True:\n            self.logger.warning(L(\n                'Server failed to unsubscribe subscriber {0}: {1}',\n                subscriber.metadata['id'], response.get('message')\n            ))\n\n    def _prepare_event(self, event):\n        '''Prepare *event* for sending.'''\n        event['source'].setdefault('id', self.id)\n        event['source'].setdefault('user', {\n            'username': self._api_user\n        })\n\n    def _prepare_reply_event(self, event, source_event, source=None):\n        '''Prepare *event* as a reply to another *source_event*.\n\n        Modify *event*, setting appropriate values to target event correctly as\n        a reply.\n\n        '''\n        event['target'] = 'id={0}'.format(source_event['source']['id'])\n        event['in_reply_to_event'] = source_event['id']\n        if source is not None:\n            event['source'] = source\n\n    def publish(\n        self, event, synchronous=False, on_reply=None, on_error='raise'\n    ):\n        '''Publish *event*.\n\n        If *synchronous* is specified as True then this method will wait and\n        return a list of results from any called callbacks.\n\n        .. note::\n\n            Currently, if synchronous is True then only locally registered\n            callbacks will be called and no event will be sent to the server.\n            This may change in future.\n\n        *on_reply* is an optional callable to call with any reply event that is\n        received in response to the published *event*.\n\n        .. note::\n\n            Will not be called when *synchronous* is True.\n\n        If *on_error* is set to 'ignore' then errors raised during publish of\n        event will be caught by this method and ignored.\n\n        '''\n        if self._deprecation_warning_auto_connect and not synchronous:\n            warnings.warn(\n                self._future_signature_warning, FutureWarning\n            )\n\n        try:\n            return self._publish(\n                event, synchronous=synchronous, on_reply=on_reply\n            )\n        except Exception:\n            if on_error == 'ignore':\n                pass\n            else:\n                raise\n\n    def publish_reply(self, source_event, data, source=None):\n        '''Publish a reply event to *source_event* with supplied *data*.\n\n        If *source* is specified it will be used for the source value of the\n        sent event.\n\n        '''\n        reply_event = ftrack_api.event.base.Event(\n            'ftrack.meta.reply',\n            data=data\n        )\n        self._prepare_reply_event(reply_event, source_event, source=source)\n        self.publish(reply_event)\n\n    def _publish(self, event, synchronous=False, callback=None, on_reply=None):\n        '''Publish *event*.\n\n        If *synchronous* is specified as True then this method will wait and\n        return a list of results from any called callbacks.\n\n        .. note::\n\n            Currently, if synchronous is True then only locally registered\n            callbacks will be called and no event will be sent to the server.\n            This may change in future.\n\n        A *callback* can also be specified. This callback will be called once\n        the server acknowledges receipt of the sent event. A default callback\n        that checks for errors from the server will be used if not specified.\n\n        *on_reply* is an optional callable to call with any reply event that is\n        received in response to the published *event*. Note that there is no\n        guarantee that a reply will be sent.\n\n        Raise :exc:`ftrack_api.exception.EventHubConnectionError` if not\n        currently connected.\n\n        '''\n        # Prepare event adding any relevant additional information.\n        self._prepare_event(event)\n\n        if synchronous:\n            # Bypass emitting event to server and instead call locally\n            # registered handlers directly, collecting and returning results.\n            return self._handle(event, synchronous=synchronous)\n\n        if not self.connected:\n            raise ftrack_api.exception.EventHubConnectionError(\n                'Cannot publish event asynchronously as not connected to '\n                'server.'\n            )\n\n        # Use standard callback if none specified.\n        if callback is None:\n            callback = functools.partial(self._on_published, event)\n\n        # Emit event to central server for asynchronous processing.\n        try:\n            # Register on reply callback if specified.\n            if on_reply is not None:\n                # TODO: Add cleanup process that runs after a set duration to\n                # garbage collect old reply callbacks and prevent dictionary\n                # growing too large.\n                self._reply_callbacks[event['id']] = on_reply\n\n            try:\n                self._emit_event_packet(\n                    self._event_namespace, event, callback=callback\n                )\n            except ftrack_api.exception.EventHubConnectionError:\n                # Connection may have dropped temporarily. Wait a few moments to\n                # see if background thread reconnects automatically.\n                time.sleep(15)\n\n                self._emit_event_packet(\n                    self._event_namespace, event, callback=callback\n                )\n            except:\n                raise\n\n        except Exception:\n            # Failure to send event should not cause caller to fail.\n            # TODO: This behaviour is inconsistent with the failing earlier on\n            # lack of connection and also with the error handling parameter of\n            # EventHub.publish. Consider refactoring.\n            self.logger.exception(L('Error sending event {0}.', event))\n\n    def _on_published(self, event, response):\n        '''Handle acknowledgement of published event.'''\n        if response.get('success', False) is False:\n            self.logger.error(L(\n                'Server responded with error while publishing event {0}. '\n                'Error was: {1}', event, response.get('message')\n            ))\n\n    def _handle(self, event, synchronous=False):\n        '''Handle *event*.\n\n        If *synchronous* is True, do not send any automatic reply events.\n\n        '''\n        # Sort by priority, lower is higher.\n        # TODO: Use a sorted list to avoid sorting each time in order to improve\n        # performance.\n        subscribers = sorted(\n            self._subscribers, key=operator.attrgetter('priority')\n        )\n\n        results = []\n\n        target = event.get('target', None)\n        target_expression = None\n        if target:\n            try:\n                target_expression = self._expression_parser.parse(target)\n            except Exception:\n                self.logger.exception(L(\n                    'Cannot handle event as failed to parse event target '\n                    'information: {0}', event\n                ))\n                return\n\n        for subscriber in subscribers:\n            # Check if event is targeted to the subscriber.\n            if (\n                target_expression is not None\n                and not target_expression.match(subscriber.metadata)\n            ):\n                continue\n\n            # Check if subscriber interested in the event.\n            if not subscriber.interested_in(event):\n                continue\n\n            response = None\n\n            try:\n                response = subscriber.callback(event)\n                results.append(response)\n            except Exception:\n                self.logger.exception(L(\n                    'Error calling subscriber {0} for event {1}.',\n                    subscriber, event\n                ))\n\n            # Automatically publish a non None response as a reply when not in\n            # synchronous mode.\n            if not synchronous:\n                if self._deprecation_warning_auto_connect:\n                    warnings.warn(\n                        self._future_signature_warning, FutureWarning\n                    )\n\n                if response is not None:\n                    try:\n                        self.publish_reply(\n                            event, data=response, source=subscriber.metadata\n                        )\n\n                    except Exception:\n                        self.logger.exception(L(\n                            'Error publishing response {0} from subscriber {1} '\n                            'for event {2}.', response, subscriber, event\n                        ))\n\n            # Check whether to continue processing topic event.\n            if event.is_stopped():\n                self.logger.debug(L(\n                    'Subscriber {0} stopped event {1}. Will not process '\n                    'subsequent subscriber callbacks for this event.',\n                    subscriber, event\n                ))\n                break\n\n        return results\n\n    def _handle_reply(self, event):\n        '''Handle reply *event*, passing it to any registered callback.'''\n        callback = self._reply_callbacks.get(event['in_reply_to_event'], None)\n        if callback is not None:\n            callback(event)\n\n    def subscription(self, subscription, callback, subscriber=None,\n                     priority=100):\n        '''Return context manager with *callback* subscribed to *subscription*.\n\n        The subscribed callback will be automatically unsubscribed on exit\n        of the context manager.\n\n        '''\n        return _SubscriptionContext(\n            self, subscription, callback, subscriber=subscriber,\n            priority=priority,\n        )\n\n    # Socket.IO interface.\n    #\n\n    def _get_socket_io_session(self):\n        '''Connect to server and retrieve session information.'''\n        socket_io_url = (\n            '{0}://{1}/socket.io/1/?api_user={2}&api_key={3}'\n        ).format(\n            self.server.scheme,\n            self.get_network_location(),\n            self._api_user,\n            self._api_key\n        )\n        try:\n            response = requests.get(\n                socket_io_url,\n                timeout=60  # 60 seconds timeout to recieve errors faster.\n            )\n        except requests.exceptions.Timeout as error:\n            raise ftrack_api.exception.EventHubConnectionError(\n                'Timed out connecting to server: {0}.'.format(error)\n            )\n        except requests.exceptions.SSLError as error:\n            raise ftrack_api.exception.EventHubConnectionError(\n                'Failed to negotiate SSL with server: {0}.'.format(error)\n            )\n        except requests.exceptions.ConnectionError as error:\n            raise ftrack_api.exception.EventHubConnectionError(\n                'Failed to connect to server: {0}.'.format(error)\n            )\n        else:\n            status = response.status_code\n            if status != 200:\n                raise ftrack_api.exception.EventHubConnectionError(\n                    'Received unexpected status code {0}.'.format(status)\n                )\n\n        # Parse result and return session information.\n        parts = response.text.split(':')\n        return SocketIoSession(\n            parts[0],\n            parts[1],\n            parts[3].split(',')\n        )\n\n    def _add_packet_callback(self, callback):\n        '''Store callback against a new unique packet ID.\n\n        Return the unique packet ID.\n\n        '''\n        with self._lock:\n            self._unique_packet_id += 1\n            unique_identifier = self._unique_packet_id\n\n        self._packet_callbacks[unique_identifier] = callback\n\n        return '{0}+'.format(unique_identifier)\n\n    def _pop_packet_callback(self, packet_identifier):\n        '''Pop and return callback for *packet_identifier*.'''\n        return self._packet_callbacks.pop(packet_identifier)\n\n    def _emit_event_packet(self, namespace, event, callback):\n        '''Send *event* packet under *namespace*.'''\n        data = self._encode(\n            dict(name=namespace, args=[event])\n        )\n        self._send_packet(\n            self._code_name_mapping['event'], data=data, callback=callback\n        )\n\n    def _acknowledge_packet(self, packet_identifier, *args):\n        '''Send acknowledgement of packet with *packet_identifier*.'''\n        packet_identifier = packet_identifier.rstrip('+')\n        data = str(packet_identifier)\n        if args:\n            data += '+{1}'.format(self._encode(args))\n\n        self._send_packet(self._code_name_mapping['acknowledge'], data=data)\n\n    def _send_packet(self, code, data='', callback=None):\n        '''Send packet via connection.'''\n        path = ''\n        packet_identifier = (\n            self._add_packet_callback(callback) if callback else ''\n        )\n        packet_parts = (str(code), packet_identifier, path, data)\n        packet = ':'.join(packet_parts)\n\n        try:\n            self._connection.send(packet)\n            self.logger.debug(L(u'Sent packet: {0}', packet))\n        except socket.error as error:\n            raise ftrack_api.exception.EventHubConnectionError(\n                'Failed to send packet: {0}'.format(error)\n            )\n\n    def _receive_packet(self):\n        '''Receive and return packet via connection.'''\n        try:\n            packet = self._connection.recv()\n        except Exception as error:\n            raise ftrack_api.exception.EventHubConnectionError(\n                'Error receiving packet: {0}'.format(error)\n            )\n\n        try:\n            parts = packet.split(':', 3)\n        except AttributeError:\n            raise ftrack_api.exception.EventHubPacketError(\n                'Received invalid packet {0}'.format(packet)\n            )\n\n        code, packet_identifier, path, data = None, None, None, None\n\n        count = len(parts)\n        if count == 4:\n            code, packet_identifier, path, data = parts\n        elif count == 3:\n            code, packet_identifier, path = parts\n        elif count == 1:\n            code = parts[0]\n        else:\n            raise ftrack_api.exception.EventHubPacketError(\n                'Received invalid packet {0}'.format(packet)\n            )\n\n        self.logger.debug(L('Received packet: {0}', packet))\n        return code, packet_identifier, path, data\n\n    def _handle_packet(self, code, packet_identifier, path, data):\n        '''Handle packet received from server.'''\n        code_name = self._code_name_mapping[code]\n\n        if code_name == 'connect':\n            self.logger.debug('Connected to event server.')\n            event = ftrack_api.event.base.Event('ftrack.meta.connected')\n            self._prepare_event(event)\n            self._event_queue.put(event)\n\n        elif code_name == 'disconnect':\n            self.logger.debug('Disconnected from event server.')\n            if not self._intentional_disconnect:\n                self.logger.debug(\n                    'Disconnected unexpectedly. Attempting to reconnect.'\n                )\n                try:\n                    self.reconnect(\n                        attempts=self._auto_reconnect_attempts,\n                        delay=self._auto_reconnect_delay\n                    )\n                except ftrack_api.exception.EventHubConnectionError:\n                    self.logger.debug('Failed to reconnect automatically.')\n                else:\n                    self.logger.debug('Reconnected successfully.')\n\n            if not self.connected:\n                event = ftrack_api.event.base.Event('ftrack.meta.disconnected')\n                self._prepare_event(event)\n                self._event_queue.put(event)\n\n        elif code_name == 'heartbeat':\n            # Reply with heartbeat.\n            self._send_packet(self._code_name_mapping['heartbeat'])\n\n        elif code_name == 'message':\n            self.logger.debug(L('Message received: {0}', data))\n\n        elif code_name == 'event':\n            payload = self._decode(data)\n            args = payload.get('args', [])\n\n            if len(args) == 1:\n                event_payload = args[0]\n                if isinstance(event_payload, collections.Mapping):\n                    try:\n                        event = ftrack_api.event.base.Event(**event_payload)\n                    except Exception:\n                        self.logger.exception(L(\n                            'Failed to convert payload into event: {0}',\n                            event_payload\n                        ))\n                        return\n\n                    self._event_queue.put(event)\n\n        elif code_name == 'acknowledge':\n            parts = data.split('+', 1)\n            acknowledged_packet_identifier = int(parts[0])\n            args = []\n            if len(parts) == 2:\n                args = self._decode(parts[1])\n\n            try:\n                callback = self._pop_packet_callback(\n                    acknowledged_packet_identifier\n                )\n            except KeyError:\n                pass\n            else:\n                callback(*args)\n\n        elif code_name == 'error':\n            self.logger.error(L('Event server reported error: {0}.', data))\n\n        else:\n            self.logger.debug(L('{0}: {1}', code_name, data))\n\n    def _encode(self, data):\n        '''Return *data* encoded as JSON formatted string.'''\n        return json.dumps(\n            data,\n            default=self._encode_object_hook,\n            ensure_ascii=False\n        )\n\n    def _encode_object_hook(self, item):\n        '''Return *item* transformed for encoding.'''\n        if isinstance(item, ftrack_api.event.base.Event):\n            # Convert to dictionary for encoding.\n            item = dict(**item)\n\n            if 'in_reply_to_event' in item:\n                # Convert keys to server convention.\n                item['inReplyToEvent'] = item.pop('in_reply_to_event')\n\n            return item\n\n        raise TypeError('{0!r} is not JSON serializable'.format(item))\n\n    def _decode(self, string):\n        '''Return decoded JSON *string* as Python object.'''\n        return json.loads(string, object_hook=self._decode_object_hook)\n\n    def _decode_object_hook(self, item):\n        '''Return *item* transformed.'''\n        if isinstance(item, collections.Mapping):\n            if 'inReplyToEvent' in item:\n                item['in_reply_to_event'] = item.pop('inReplyToEvent')\n\n        return item\n\n\nclass _SubscriptionContext(object):\n    '''Context manager for a one-off subscription.'''\n\n    def __init__(self, hub, subscription, callback, subscriber, priority):\n        '''Initialise context.'''\n        self._hub = hub\n        self._subscription = subscription\n        self._callback = callback\n        self._subscriber = subscriber\n        self._priority = priority\n        self._subscriberIdentifier = None\n\n    def __enter__(self):\n        '''Enter context subscribing callback to topic.'''\n        self._subscriberIdentifier = self._hub.subscribe(\n            self._subscription, self._callback, subscriber=self._subscriber,\n            priority=self._priority\n        )\n\n    def __exit__(self, exception_type, exception_value, traceback):\n        '''Exit context unsubscribing callback from topic.'''\n        self._hub.unsubscribe(self._subscriberIdentifier)\n\n\nclass _ProcessorThread(threading.Thread):\n    '''Process messages from server.'''\n\n    daemon = True\n\n    def __init__(self, client):\n        '''Initialise thread with Socket.IO *client* instance.'''\n        super(_ProcessorThread, self).__init__()\n        self.logger = logging.getLogger(\n            __name__ + '.' + self.__class__.__name__\n        )\n        self.client = client\n        self.done = threading.Event()\n\n    def run(self):\n        '''Perform work in thread.'''\n        while not self.done.is_set():\n            try:\n                code, packet_identifier, path, data = self.client._receive_packet()\n                self.client._handle_packet(code, packet_identifier, path, data)\n\n            except ftrack_api.exception.EventHubPacketError as error:\n                self.logger.debug(L('Ignoring invalid packet: {0}', error))\n                continue\n\n            except ftrack_api.exception.EventHubConnectionError:\n                self.cancel()\n\n                # Fake a disconnection event in order to trigger reconnection\n                # when necessary.\n                self.client._handle_packet('0', '', '', '')\n\n                break\n\n            except Exception as error:\n                self.logger.debug(L('Aborting processor thread: {0}', error))\n                self.cancel()\n                break\n\n    def cancel(self):\n        '''Cancel work as soon as possible.'''\n        self.done.set()\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/event/subscriber.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport ftrack_api.event.subscription\n\n\nclass Subscriber(object):\n    '''Represent event subscriber.'''\n\n    def __init__(self, subscription, callback, metadata, priority):\n        '''Initialise subscriber.'''\n        self.subscription = ftrack_api.event.subscription.Subscription(\n            subscription\n        )\n        self.callback = callback\n        self.metadata = metadata\n        self.priority = priority\n\n    def __str__(self):\n        '''Return string representation.'''\n        return '<{0} metadata={1} subscription=\"{2}\">'.format(\n            self.__class__.__name__, self.metadata, self.subscription\n        )\n\n    def interested_in(self, event):\n        '''Return whether subscriber interested in *event*.'''\n        return self.subscription.includes(event)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/event/subscription.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport ftrack_api.event.expression\n\n\nclass Subscription(object):\n    '''Represent a subscription.'''\n\n    parser = ftrack_api.event.expression.Parser()\n\n    def __init__(self, subscription):\n        '''Initialise with *subscription*.'''\n        self._subscription = subscription\n        self._expression = self.parser.parse(subscription)\n\n    def __str__(self):\n        '''Return string representation.'''\n        return self._subscription\n\n    def includes(self, event):\n        '''Return whether subscription includes *event*.'''\n        return self._expression.match(event)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/exception.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport sys\nimport traceback\n\nimport ftrack_api.entity.base\n\n\nclass Error(Exception):\n    '''ftrack specific error.'''\n\n    default_message = 'Unspecified error occurred.'\n\n    def __init__(self, message=None, details=None):\n        '''Initialise exception with *message*.\n\n        If *message* is None, the class 'default_message' will be used.\n\n        *details* should be a mapping of extra information that can be used in\n        the message and also to provide more context.\n\n        '''\n        if message is None:\n            message = self.default_message\n\n        self.message = message\n        self.details = details\n        if self.details is None:\n            self.details = {}\n\n        self.traceback = traceback.format_exc()\n\n    def __str__(self):\n        '''Return string representation.'''\n        keys = {}\n        for key, value in self.details.iteritems():\n            if isinstance(value, unicode):\n                value = value.encode(sys.getfilesystemencoding())\n            keys[key] = value\n\n        return str(self.message.format(**keys))\n\n\nclass AuthenticationError(Error):\n    '''Raise when an authentication error occurs.'''\n\n    default_message = 'Authentication error.'\n\n\nclass ServerError(Error):\n    '''Raise when the server reports an error.'''\n\n    default_message = 'Server reported error processing request.'\n\n\nclass ServerCompatibilityError(ServerError):\n    '''Raise when server appears incompatible.'''\n\n    default_message = 'Server incompatible.'\n\n\nclass NotFoundError(Error):\n    '''Raise when something that should exist is not found.'''\n\n    default_message = 'Not found.'\n\n\nclass NotUniqueError(Error):\n    '''Raise when unique value required and duplicate detected.'''\n\n    default_message = 'Non-unique value detected.'\n\n\nclass IncorrectResultError(Error):\n    '''Raise when a result is incorrect.'''\n\n    default_message = 'Incorrect result detected.'\n\n\nclass NoResultFoundError(IncorrectResultError):\n    '''Raise when a result was expected but no result was found.'''\n\n    default_message = 'Expected result, but no result was found.'\n\n\nclass MultipleResultsFoundError(IncorrectResultError):\n    '''Raise when a single result expected, but multiple results found.'''\n\n    default_message = 'Expected single result, but received multiple results.'\n\n\nclass EntityTypeError(Error):\n    '''Raise when an entity type error occurs.'''\n\n    default_message = 'Entity type error.'\n\n\nclass UnrecognisedEntityTypeError(EntityTypeError):\n    '''Raise when an unrecognised entity type detected.'''\n\n    default_message = 'Entity type \"{entity_type}\" not recognised.'\n\n    def __init__(self, entity_type, **kw):\n        '''Initialise with *entity_type* that is unrecognised.'''\n        kw.setdefault('details', {}).update(dict(\n            entity_type=entity_type\n        ))\n        super(UnrecognisedEntityTypeError, self).__init__(**kw)\n\n\nclass OperationError(Error):\n    '''Raise when an operation error occurs.'''\n\n    default_message = 'Operation error.'\n\n\nclass InvalidStateError(Error):\n    '''Raise when an invalid state detected.'''\n\n    default_message = 'Invalid state.'\n\n\nclass InvalidStateTransitionError(InvalidStateError):\n    '''Raise when an invalid state transition detected.'''\n\n    default_message = (\n        'Invalid transition from {current_state!r} to {target_state!r} state '\n        'for entity {entity!r}'\n    )\n\n    def __init__(self, current_state, target_state, entity, **kw):\n        '''Initialise error.'''\n        kw.setdefault('details', {}).update(dict(\n            current_state=current_state,\n            target_state=target_state,\n            entity=entity\n        ))\n        super(InvalidStateTransitionError, self).__init__(**kw)\n\n\nclass AttributeError(Error):\n    '''Raise when an error related to an attribute occurs.'''\n\n    default_message = 'Attribute error.'\n\n\nclass ImmutableAttributeError(AttributeError):\n    '''Raise when modification of immutable attribute attempted.'''\n\n    default_message = (\n        'Cannot modify value of immutable {attribute.name!r} attribute.'\n    )\n\n    def __init__(self, attribute, **kw):\n        '''Initialise error.'''\n        kw.setdefault('details', {}).update(dict(\n            attribute=attribute\n        ))\n        super(ImmutableAttributeError, self).__init__(**kw)\n\n\nclass CollectionError(Error):\n    '''Raise when an error related to collections occurs.'''\n\n    default_message = 'Collection error.'\n\n    def __init__(self, collection, **kw):\n        '''Initialise error.'''\n        kw.setdefault('details', {}).update(dict(\n            collection=collection\n        ))\n        super(CollectionError, self).__init__(**kw)\n\n\nclass ImmutableCollectionError(CollectionError):\n    '''Raise when modification of immutable collection attempted.'''\n\n    default_message = (\n        'Cannot modify value of immutable collection {collection!r}.'\n    )\n\n\nclass DuplicateItemInCollectionError(CollectionError):\n    '''Raise when duplicate item in collection detected.'''\n\n    default_message = (\n        'Item {item!r} already exists in collection {collection!r}.'\n    )\n\n    def __init__(self, item, collection, **kw):\n        '''Initialise error.'''\n        kw.setdefault('details', {}).update(dict(\n            item=item\n        ))\n        super(DuplicateItemInCollectionError, self).__init__(collection, **kw)\n\n\nclass ParseError(Error):\n    '''Raise when a parsing error occurs.'''\n\n    default_message = 'Failed to parse.'\n\n\nclass EventHubError(Error):\n    '''Raise when issues related to event hub occur.'''\n\n    default_message = 'Event hub error occurred.'\n\n\nclass EventHubConnectionError(EventHubError):\n    '''Raise when event hub encounters connection problem.'''\n\n    default_message = 'Event hub is not connected.'\n\n\nclass EventHubPacketError(EventHubError):\n    '''Raise when event hub encounters an issue with a packet.'''\n\n    default_message = 'Invalid packet.'\n\n\nclass PermissionDeniedError(Error):\n    '''Raise when permission is denied.'''\n\n    default_message = 'Permission denied.'\n\n\nclass LocationError(Error):\n    '''Base for errors associated with locations.'''\n\n    default_message = 'Unspecified location error'\n\n\nclass ComponentNotInAnyLocationError(LocationError):\n    '''Raise when component not available in any location.'''\n\n    default_message = 'Component not available in any location.'\n\n\nclass ComponentNotInLocationError(LocationError):\n    '''Raise when component(s) not in location.'''\n\n    default_message = (\n        'Component(s) {formatted_components} not found in location {location}.'\n    )\n\n    def __init__(self, components, location, **kw):\n        '''Initialise with *components* and *location*.'''\n        if isinstance(components, ftrack_api.entity.base.Entity):\n            components = [components]\n\n        kw.setdefault('details', {}).update(dict(\n            components=components,\n            formatted_components=', '.join(\n                [str(component) for component in components]\n            ),\n            location=location\n        ))\n\n        super(ComponentNotInLocationError, self).__init__(**kw)\n\n\nclass ComponentInLocationError(LocationError):\n    '''Raise when component(s) already exists in location.'''\n\n    default_message = (\n        'Component(s) {formatted_components} already exist in location '\n        '{location}.'\n    )\n\n    def __init__(self, components, location, **kw):\n        '''Initialise with *components* and *location*.'''\n        if isinstance(components, ftrack_api.entity.base.Entity):\n            components = [components]\n\n        kw.setdefault('details', {}).update(dict(\n            components=components,\n            formatted_components=', '.join(\n                [str(component) for component in components]\n            ),\n            location=location\n        ))\n\n        super(ComponentInLocationError, self).__init__(**kw)\n\n\nclass AccessorError(Error):\n    '''Base for errors associated with accessors.'''\n\n    default_message = 'Unspecified accessor error'\n\n\nclass AccessorOperationFailedError(AccessorError):\n    '''Base for failed operations on accessors.'''\n\n    default_message = 'Operation {operation} failed: {error}'\n\n    def __init__(\n        self, operation='', resource_identifier=None, error=None, **kw\n    ):\n        kw.setdefault('details', {}).update(dict(\n            operation=operation,\n            resource_identifier=resource_identifier,\n            error=error\n        ))\n        super(AccessorOperationFailedError, self).__init__(**kw)\n\n\nclass AccessorUnsupportedOperationError(AccessorOperationFailedError):\n    '''Raise when operation is unsupported.'''\n\n    default_message = 'Operation {operation} unsupported.'\n\n\nclass AccessorPermissionDeniedError(AccessorOperationFailedError):\n    '''Raise when permission denied.'''\n\n    default_message = (\n        'Cannot {operation} {resource_identifier}. Permission denied.'\n    )\n\n\nclass AccessorResourceIdentifierError(AccessorError):\n    '''Raise when a error related to a resource_identifier occurs.'''\n\n    default_message = 'Resource identifier is invalid: {resource_identifier}.'\n\n    def __init__(self, resource_identifier, **kw):\n        kw.setdefault('details', {}).update(dict(\n            resource_identifier=resource_identifier\n        ))\n        super(AccessorResourceIdentifierError, self).__init__(**kw)\n\n\nclass AccessorFilesystemPathError(AccessorResourceIdentifierError):\n    '''Raise when a error related to an accessor filesystem path occurs.'''\n\n    default_message = (\n        'Could not determine filesystem path from resource identifier: '\n        '{resource_identifier}.'\n    )\n\n\nclass AccessorResourceError(AccessorError):\n    '''Base for errors associated with specific resource.'''\n\n    default_message = 'Unspecified resource error: {resource_identifier}'\n\n    def __init__(self, operation='', resource_identifier=None, error=None,\n                 **kw):\n        kw.setdefault('details', {}).update(dict(\n            operation=operation,\n            resource_identifier=resource_identifier\n        ))\n        super(AccessorResourceError, self).__init__(**kw)\n\n\nclass AccessorResourceNotFoundError(AccessorResourceError):\n    '''Raise when a required resource is not found.'''\n\n    default_message = 'Resource not found: {resource_identifier}'\n\n\nclass AccessorParentResourceNotFoundError(AccessorResourceError):\n    '''Raise when a parent resource (such as directory) is not found.'''\n\n    default_message = 'Parent resource is missing: {resource_identifier}'\n\n\nclass AccessorResourceInvalidError(AccessorResourceError):\n    '''Raise when a resource is not the right type.'''\n\n    default_message = 'Resource invalid: {resource_identifier}'\n\n\nclass AccessorContainerNotEmptyError(AccessorResourceError):\n    '''Raise when container is not empty.'''\n\n    default_message = 'Container is not empty: {resource_identifier}'\n\n\nclass StructureError(Error):\n    '''Base for errors associated with structures.'''\n\n    default_message = 'Unspecified structure error'\n\n\nclass ConnectionClosedError(Error):\n    '''Raise when attempt to use closed connection detected.'''\n\n    default_message = \"Connection closed.\"\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/formatter.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport termcolor\n\nimport ftrack_api.entity.base\nimport ftrack_api.collection\nimport ftrack_api.symbol\nimport ftrack_api.inspection\n\n\n#: Useful filters to pass to :func:`format`.`\nFILTER = {\n    'ignore_unset': (\n        lambda entity, name, value: value is not ftrack_api.symbol.NOT_SET\n    )\n}\n\n\ndef format(\n    entity, formatters=None, attribute_filter=None, recursive=False,\n    indent=0, indent_first_line=True, _seen=None\n):\n    '''Return formatted string representing *entity*.\n\n    *formatters* can be used to customise formatting of elements. It should be a\n    mapping with one or more of the following keys:\n\n        * header - Used to format entity type.\n        * label - Used to format attribute names.\n\n    Specify an *attribute_filter* to control which attributes to include. By\n    default all attributes are included. The *attribute_filter* should be a\n    callable that accepts `(entity, attribute_name, attribute_value)` and\n    returns True if the attribute should be included in the output. For example,\n    to filter out all unset values::\n\n        attribute_filter=ftrack_api.formatter.FILTER['ignore_unset']\n\n    If *recursive* is True then recurse into Collections and format each entity\n    present.\n\n    *indent* specifies the overall indentation in spaces of the formatted text,\n    whilst *indent_first_line* determines whether to apply that indent to the\n    first generated line.\n\n    .. warning::\n\n        Iterates over all *entity* attributes which may cause multiple queries\n        to the server. Turn off auto populating in the session to prevent this.\n\n    '''\n    # Initialise default formatters.\n    if formatters is None:\n        formatters = dict()\n\n    formatters.setdefault(\n        'header', lambda text: termcolor.colored(\n            text, 'white', 'on_blue', attrs=['bold']\n        )\n    )\n    formatters.setdefault(\n        'label', lambda text: termcolor.colored(\n            text, 'blue', attrs=['bold']\n        )\n    )\n\n    # Determine indents.\n    spacer = ' ' * indent\n    if indent_first_line:\n        first_line_spacer = spacer\n    else:\n        first_line_spacer = ''\n\n    # Avoid infinite recursion on circular references.\n    if _seen is None:\n        _seen = set()\n\n    identifier = str(ftrack_api.inspection.identity(entity))\n    if identifier in _seen:\n        return (\n            first_line_spacer +\n            formatters['header'](entity.entity_type) + '{...}'\n        )\n\n    _seen.add(identifier)\n    information = list()\n\n    information.append(\n        first_line_spacer + formatters['header'](entity.entity_type)\n    )\n    for key, value in sorted(entity.items()):\n        if attribute_filter is not None:\n            if not attribute_filter(entity, key, value):\n                continue\n\n        child_indent = indent + len(key) + 3\n\n        if isinstance(value, ftrack_api.entity.base.Entity):\n            value = format(\n                value,\n                formatters=formatters,\n                attribute_filter=attribute_filter,\n                recursive=recursive,\n                indent=child_indent,\n                indent_first_line=False,\n                _seen=_seen.copy()\n            )\n\n        if isinstance(value, ftrack_api.collection.Collection):\n            if recursive:\n                child_values = []\n                for index, child in enumerate(value):\n                    child_value = format(\n                        child,\n                        formatters=formatters,\n                        attribute_filter=attribute_filter,\n                        recursive=recursive,\n                        indent=child_indent,\n                        indent_first_line=index != 0,\n                        _seen=_seen.copy()\n                    )\n                    child_values.append(child_value)\n\n                value = '\\n'.join(child_values)\n\n        information.append(\n            spacer + u' {0}: {1}'.format(formatters['label'](key), value)\n        )\n\n    return '\\n'.join(information)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/inspection.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport collections\n\nimport ftrack_api.symbol\nimport ftrack_api.operation\n\n\ndef identity(entity):\n    '''Return unique identity of *entity*.'''\n    return (\n        str(entity.entity_type),\n        primary_key(entity).values()\n    )\n\n\ndef primary_key(entity):\n    '''Return primary key of *entity* as an ordered mapping of {field: value}.\n\n    To get just the primary key values::\n\n        primary_key(entity).values()\n\n    '''\n    primary_key = collections.OrderedDict()\n    for name in entity.primary_key_attributes:\n        value = entity[name]\n        if value is ftrack_api.symbol.NOT_SET:\n            raise KeyError(\n                'Missing required value for primary key attribute \"{0}\" on '\n                'entity {1!r}.'.format(name, entity)\n            )\n\n        primary_key[str(name)] = str(value)\n\n    return primary_key\n\n\ndef _state(operation, state):\n    '''Return state following *operation* against current *state*.'''\n    if (\n        isinstance(\n            operation, ftrack_api.operation.CreateEntityOperation\n        )\n        and state is ftrack_api.symbol.NOT_SET\n    ):\n        state = ftrack_api.symbol.CREATED\n\n    elif (\n        isinstance(\n            operation, ftrack_api.operation.UpdateEntityOperation\n        )\n        and state is ftrack_api.symbol.NOT_SET\n    ):\n        state = ftrack_api.symbol.MODIFIED\n\n    elif isinstance(\n        operation, ftrack_api.operation.DeleteEntityOperation\n    ):\n        state = ftrack_api.symbol.DELETED\n\n    return state\n\n\ndef state(entity):\n    '''Return current *entity* state.\n\n    .. seealso:: :func:`ftrack_api.inspection.states`.\n\n    '''\n    value = ftrack_api.symbol.NOT_SET\n\n    for operation in entity.session.recorded_operations:\n        # Determine if operation refers to an entity and whether that entity\n        # is *entity*.\n        if (\n            isinstance(\n                operation,\n                (\n                    ftrack_api.operation.CreateEntityOperation,\n                    ftrack_api.operation.UpdateEntityOperation,\n                    ftrack_api.operation.DeleteEntityOperation\n                )\n            )\n            and operation.entity_type == entity.entity_type\n            and operation.entity_key == primary_key(entity)\n        ):\n            value = _state(operation, value)\n\n    return value\n\n\ndef states(entities):\n    '''Return current states of *entities*.\n\n    An optimised function for determining states of multiple entities in one\n    go.\n\n    .. note::\n\n        All *entities* should belong to the same session.\n\n    .. seealso:: :func:`ftrack_api.inspection.state`.\n\n    '''\n    if not entities:\n        return []\n\n    session = entities[0].session\n\n    entities_by_identity = collections.OrderedDict()\n    for entity in entities:\n        key = (entity.entity_type, str(primary_key(entity).values()))\n        entities_by_identity[key] = ftrack_api.symbol.NOT_SET\n\n    for operation in session.recorded_operations:\n        if (\n            isinstance(\n                operation,\n                (\n                    ftrack_api.operation.CreateEntityOperation,\n                    ftrack_api.operation.UpdateEntityOperation,\n                    ftrack_api.operation.DeleteEntityOperation\n                )\n            )\n        ):\n            key = (operation.entity_type, str(operation.entity_key.values()))\n            if key not in entities_by_identity:\n                continue\n\n            value = _state(operation, entities_by_identity[key])\n            entities_by_identity[key] = value\n\n    return entities_by_identity.values()\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/logging.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2016 ftrack\n\nimport functools\nimport warnings\n\n\ndef deprecation_warning(message):\n    def decorator(function):\n        @functools.wraps(function)\n        def wrapper(*args, **kwargs):\n            warnings.warn(\n                message,\n                PendingDeprecationWarning\n            )\n            return function(*args, **kwargs)\n        return wrapper\n\n    return decorator\n\n\nclass LazyLogMessage(object):\n    '''A log message that can be evaluated lazily for improved performance.\n\n    Example::\n\n        # Formatting of string will not occur unless debug logging enabled.\n        logger.debug(LazyLogMessage(\n            'Hello {0}', 'world'\n        ))\n\n    '''\n\n    def __init__(self, message, *args, **kwargs):\n        '''Initialise with *message* format string and arguments.'''\n        self.message = message\n        self.args = args\n        self.kwargs = kwargs\n\n    def __str__(self):\n        '''Return string representation.'''\n        return self.message.format(*self.args, **self.kwargs)\n\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/operation.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport copy\n\n\nclass Operations(object):\n    '''Stack of operations.'''\n\n    def __init__(self):\n        '''Initialise stack.'''\n        self._stack = []\n        super(Operations, self).__init__()\n\n    def clear(self):\n        '''Clear all operations.'''\n        del self._stack[:]\n\n    def push(self, operation):\n        '''Push *operation* onto stack.'''\n        self._stack.append(operation)\n\n    def pop(self):\n        '''Pop and return most recent operation from stack.'''\n        return self._stack.pop()\n\n    def __len__(self):\n        '''Return count of operations.'''\n        return len(self._stack)\n\n    def __iter__(self):\n        '''Return iterator over operations.'''\n        return iter(self._stack)\n\n\nclass Operation(object):\n    '''Represent an operation.'''\n\n\nclass CreateEntityOperation(Operation):\n    '''Represent create entity operation.'''\n\n    def __init__(self, entity_type, entity_key, entity_data):\n        '''Initialise operation.\n\n        *entity_type* should be the type of entity in string form (as returned\n        from :attr:`ftrack_api.entity.base.Entity.entity_type`).\n\n        *entity_key* should be the unique key for the entity and should follow\n        the form returned from :func:`ftrack_api.inspection.primary_key`.\n\n        *entity_data* should be a mapping of the initial data to populate the\n        entity with when creating.\n\n        .. note::\n\n            Shallow copies will be made of each value in *entity_data*.\n\n        '''\n        super(CreateEntityOperation, self).__init__()\n        self.entity_type = entity_type\n        self.entity_key = entity_key\n        self.entity_data = {}\n        for key, value in entity_data.items():\n            self.entity_data[key] = copy.copy(value)\n\n\nclass UpdateEntityOperation(Operation):\n    '''Represent update entity operation.'''\n\n    def __init__(\n        self, entity_type, entity_key, attribute_name, old_value, new_value\n    ):\n        '''Initialise operation.\n\n        *entity_type* should be the type of entity in string form (as returned\n        from :attr:`ftrack_api.entity.base.Entity.entity_type`).\n\n        *entity_key* should be the unique key for the entity and should follow\n        the form returned from :func:`ftrack_api.inspection.primary_key`.\n\n        *attribute_name* should be the string name of the attribute being\n        modified and *old_value* and *new_value* should reflect the change in\n        value.\n\n        .. note::\n\n            Shallow copies will be made of both *old_value* and *new_value*.\n\n        '''\n        super(UpdateEntityOperation, self).__init__()\n        self.entity_type = entity_type\n        self.entity_key = entity_key\n        self.attribute_name = attribute_name\n        self.old_value = copy.copy(old_value)\n        self.new_value = copy.copy(new_value)\n\n\nclass DeleteEntityOperation(Operation):\n    '''Represent delete entity operation.'''\n\n    def __init__(self, entity_type, entity_key):\n        '''Initialise operation.\n\n        *entity_type* should be the type of entity in string form (as returned\n        from :attr:`ftrack_api.entity.base.Entity.entity_type`).\n\n        *entity_key* should be the unique key for the entity and should follow\n        the form returned from :func:`ftrack_api.inspection.primary_key`.\n\n        '''\n        super(DeleteEntityOperation, self).__init__()\n        self.entity_type = entity_type\n        self.entity_key = entity_key\n\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/plugin.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nfrom __future__ import absolute_import\n\nimport logging\nimport os\nimport uuid\nimport imp\nimport inspect\n\n\ndef discover(paths, positional_arguments=None, keyword_arguments=None):\n    '''Find and load plugins in search *paths*.\n\n    Each discovered module should implement a register function that accepts\n    *positional_arguments* and *keyword_arguments* as \\*args and \\*\\*kwargs\n    respectively.\n\n    If a register function does not accept variable arguments, then attempt to\n    only pass accepted arguments to the function by inspecting its signature.\n\n    '''\n    logger = logging.getLogger(__name__ + '.discover')\n\n    if positional_arguments is None:\n        positional_arguments = []\n\n    if keyword_arguments is None:\n        keyword_arguments = {}\n\n    for path in paths:\n        # Ignore empty paths that could resolve to current directory.\n        path = path.strip()\n        if not path:\n            continue\n\n        for base, directories, filenames in os.walk(path):\n            for filename in filenames:\n                name, extension = os.path.splitext(filename)\n                if extension != '.py':\n                    continue\n\n                module_path = os.path.join(base, filename)\n                unique_name = uuid.uuid4().hex\n\n                try:\n                    module = imp.load_source(unique_name, module_path)\n                except Exception as error:\n                    logger.warning(\n                        'Failed to load plugin from \"{0}\": {1}'\n                        .format(module_path, error)\n                    )\n                    continue\n\n                try:\n                    module.register\n                except AttributeError:\n                    logger.warning(\n                        'Failed to load plugin that did not define a '\n                        '\"register\" function at the module level: {0}'\n                        .format(module_path)\n                    )\n                else:\n                    # Attempt to only pass arguments that are accepted by the\n                    # register function.\n                    specification = inspect.getargspec(module.register)\n\n                    selected_positional_arguments = positional_arguments\n                    selected_keyword_arguments = keyword_arguments\n\n                    if (\n                        not specification.varargs and\n                        len(positional_arguments) > len(specification.args)\n                    ):\n                        logger.warning(\n                            'Culling passed arguments to match register '\n                            'function signature.'\n                        )\n\n                        selected_positional_arguments = positional_arguments[\n                            len(specification.args):\n                        ]\n                        selected_keyword_arguments = {}\n\n                    elif not specification.keywords:\n                        # Remove arguments that have been passed as positionals.\n                        remainder = specification.args[\n                            len(positional_arguments):\n                        ]\n\n                        # Determine remaining available keyword arguments.\n                        defined_keyword_arguments = []\n                        if specification.defaults:\n                            defined_keyword_arguments = specification.args[\n                                -len(specification.defaults):\n                            ]\n\n                        remaining_keyword_arguments = set([\n                            keyword_argument for keyword_argument\n                            in defined_keyword_arguments\n                            if keyword_argument in remainder\n                        ])\n\n                        if not set(keyword_arguments.keys()).issubset(\n                            remaining_keyword_arguments\n                        ):\n                            logger.warning(\n                                'Culling passed arguments to match register '\n                                'function signature.'\n                            )\n                            selected_keyword_arguments = {\n                                key: value\n                                for key, value in keyword_arguments.items()\n                                if key in remaining_keyword_arguments\n                            }\n\n                    module.register(\n                        *selected_positional_arguments,\n                        **selected_keyword_arguments\n                    )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/query.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport re\nimport collections\n\nimport ftrack_api.exception\n\n\nclass QueryResult(collections.Sequence):\n    '''Results from a query.'''\n\n    OFFSET_EXPRESSION = re.compile('(?P<offset>offset (?P<value>\\d+))')\n    LIMIT_EXPRESSION = re.compile('(?P<limit>limit (?P<value>\\d+))')\n\n    def __init__(self, session, expression, page_size=500):\n        '''Initialise result set.\n\n        *session* should be an instance of :class:`ftrack_api.session.Session`\n        that will be used for executing the query *expression*.\n\n        *page_size* should be an integer specifying the maximum number of\n        records to fetch in one request allowing the results to be fetched\n        incrementally in a transparent manner for optimal performance. Any\n        offset or limit specified in *expression* are honoured for final result\n        set, but intermediate queries may be issued with different offsets and\n        limits in order to fetch pages. When an embedded limit is smaller than\n        the given *page_size* it will be used instead and no paging will take\n        place.\n\n        .. warning::\n\n            Setting *page_size* to a very large amount may negatively impact\n            performance of not only the caller, but the server in general.\n\n        '''\n        super(QueryResult, self).__init__()\n        self._session = session\n        self._results = []\n\n        (\n            self._expression,\n            self._offset,\n            self._limit\n        ) = self._extract_offset_and_limit(expression)\n\n        self._page_size = page_size\n        if self._limit is not None and self._limit < self._page_size:\n            # Optimise case where embedded limit is less than fetching a\n            # single page.\n            self._page_size = self._limit\n\n        self._next_offset = self._offset\n        if self._next_offset is None:\n            # Initialise with zero offset.\n            self._next_offset = 0\n\n    def _extract_offset_and_limit(self, expression):\n        '''Process *expression* extracting offset and limit.\n\n        Return (expression, offset, limit).\n\n        '''\n        offset = None\n        match = self.OFFSET_EXPRESSION.search(expression)\n        if match:\n            offset = int(match.group('value'))\n            expression = (\n                expression[:match.start('offset')] +\n                expression[match.end('offset'):]\n            )\n\n        limit = None\n        match = self.LIMIT_EXPRESSION.search(expression)\n        if match:\n            limit = int(match.group('value'))\n            expression = (\n                expression[:match.start('limit')] +\n                expression[match.end('limit'):]\n            )\n\n        return expression.strip(), offset, limit\n\n    def __getitem__(self, index):\n        '''Return value at *index*.'''\n        while self._can_fetch_more() and index >= len(self._results):\n            self._fetch_more()\n\n        return self._results[index]\n\n    def __len__(self):\n        '''Return number of items.'''\n        while self._can_fetch_more():\n            self._fetch_more()\n\n        return len(self._results)\n\n    def _can_fetch_more(self):\n        '''Return whether more results are available to fetch.'''\n        return self._next_offset is not None\n\n    def _fetch_more(self):\n        '''Fetch next page of results if available.'''\n        if not self._can_fetch_more():\n            return\n\n        expression = '{0} offset {1} limit {2}'.format(\n            self._expression, self._next_offset, self._page_size\n        )\n        records, metadata = self._session._query(expression)\n        self._results.extend(records)\n\n        if self._limit is not None and (len(self._results) >= self._limit):\n            # Original limit reached.\n            self._next_offset = None\n            del self._results[self._limit:]\n        else:\n            # Retrieve next page offset from returned metadata.\n            self._next_offset = metadata.get('next', {}).get('offset', None)\n\n    def all(self):\n        '''Fetch and return all data.'''\n        return list(self)\n\n    def one(self):\n        '''Return exactly one single result from query by applying a limit.\n\n        Raise :exc:`ValueError` if an existing limit is already present in the\n        expression.\n\n        Raise :exc:`ValueError` if an existing offset is already present in the\n        expression as offset is inappropriate when expecting a single item.\n\n        Raise :exc:`~ftrack_api.exception.MultipleResultsFoundError` if more\n        than one result was available or\n        :exc:`~ftrack_api.exception.NoResultFoundError` if no results were\n        available.\n\n        .. note::\n\n            Both errors subclass\n            :exc:`~ftrack_api.exception.IncorrectResultError` if you want to\n            catch only one error type.\n\n        '''\n        expression = self._expression\n\n        if self._limit is not None:\n            raise ValueError(\n                'Expression already contains a limit clause.'\n            )\n\n        if self._offset is not None:\n            raise ValueError(\n                'Expression contains an offset clause which does not make '\n                'sense when selecting a single item.'\n            )\n\n        # Apply custom limit as optimisation. A limit of 2 is used rather than\n        # 1 so that it is possible to test for multiple matching entries\n        # case.\n        expression += ' limit 2'\n\n        results, metadata = self._session._query(expression)\n\n        if not results:\n            raise ftrack_api.exception.NoResultFoundError()\n\n        if len(results) != 1:\n            raise ftrack_api.exception.MultipleResultsFoundError()\n\n        return results[0]\n\n    def first(self):\n        '''Return first matching result from query by applying a limit.\n\n        Raise :exc:`ValueError` if an existing limit is already present in the\n        expression.\n\n        If no matching result available return None.\n\n        '''\n        expression = self._expression\n\n        if self._limit is not None:\n            raise ValueError(\n                'Expression already contains a limit clause.'\n            )\n\n        # Apply custom offset if present.\n        if self._offset is not None:\n            expression += ' offset {0}'.format(self._offset)\n\n        # Apply custom limit as optimisation.\n        expression += ' limit 1'\n\n        results, metadata = self._session._query(expression)\n\n        if results:\n            return results[0]\n\n        return None\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/resource_identifier_transformer/__init__.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/resource_identifier_transformer/base.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\n\nclass ResourceIdentifierTransformer(object):\n    '''Transform resource identifiers.\n\n    Provide ability to modify resource identifier before it is stored centrally\n    (:meth:`encode`), or after it has been retrieved, but before it is used\n    locally (:meth:`decode`).\n\n    For example, you might want to decompose paths into a set of key, value\n    pairs to store centrally and then compose a path from those values when\n    reading back.\n\n    .. note::\n\n        This is separate from any transformations an\n        :class:`ftrack_api.accessor.base.Accessor` may perform and is targeted\n        towards common transformations.\n\n    '''\n\n    def __init__(self, session):\n        '''Initialise resource identifier transformer.\n\n        *session* should be the :class:`ftrack_api.session.Session` instance\n        to use for communication with the server.\n\n        '''\n        self.session = session\n        super(ResourceIdentifierTransformer, self).__init__()\n\n    def encode(self, resource_identifier, context=None):\n        '''Return encoded *resource_identifier* for storing centrally.\n\n        A mapping of *context* values may be supplied to guide the\n        transformation.\n\n        '''\n        return resource_identifier\n\n    def decode(self, resource_identifier, context=None):\n        '''Return decoded *resource_identifier* for use locally.\n\n        A mapping of *context* values may be supplied to guide the\n        transformation.\n\n        '''\n        return resource_identifier\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/session.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nfrom __future__ import absolute_import\n\nimport json\nimport logging\nimport collections\nimport datetime\nimport os\nimport getpass\nimport functools\nimport itertools\nimport distutils.version\nimport hashlib\nimport appdirs\nimport threading\nimport atexit\n\nimport requests\nimport requests.auth\nimport arrow\nimport clique\n\nimport ftrack_api\nimport ftrack_api.exception\nimport ftrack_api.entity.factory\nimport ftrack_api.entity.base\nimport ftrack_api.entity.location\nimport ftrack_api.cache\nimport ftrack_api.symbol\nimport ftrack_api.query\nimport ftrack_api.attribute\nimport ftrack_api.collection\nimport ftrack_api.event.hub\nimport ftrack_api.event.base\nimport ftrack_api.plugin\nimport ftrack_api.inspection\nimport ftrack_api.operation\nimport ftrack_api.accessor.disk\nimport ftrack_api.structure.origin\nimport ftrack_api.structure.entity_id\nimport ftrack_api.accessor.server\nimport ftrack_api._centralized_storage_scenario\nimport ftrack_api.logging\nfrom ftrack_api.logging import LazyLogMessage as L\n\ntry:\n    from weakref import WeakMethod\nexcept ImportError:\n    from ftrack_api._weakref import WeakMethod\n\n\nclass SessionAuthentication(requests.auth.AuthBase):\n    '''Attach ftrack session authentication information to requests.'''\n\n    def __init__(self, api_key, api_user):\n        '''Initialise with *api_key* and *api_user*.'''\n        self.api_key = api_key\n        self.api_user = api_user\n        super(SessionAuthentication, self).__init__()\n\n    def __call__(self, request):\n        '''Modify *request* to have appropriate headers.'''\n        request.headers.update({\n            'ftrack-api-key': self.api_key,\n            'ftrack-user': self.api_user\n        })\n        return request\n\n\nclass Session(object):\n    '''An isolated session for interaction with an ftrack server.'''\n\n    def __init__(\n        self, server_url=None, api_key=None, api_user=None, auto_populate=True,\n        plugin_paths=None, cache=None, cache_key_maker=None,\n        auto_connect_event_hub=None, schema_cache_path=None,\n        plugin_arguments=None\n    ):\n        '''Initialise session.\n\n        *server_url* should be the URL of the ftrack server to connect to\n        including any port number. If not specified attempt to look up from\n        :envvar:`FTRACK_SERVER`.\n\n        *api_key* should be the API key to use for authentication whilst\n        *api_user* should be the username of the user in ftrack to record\n        operations against. If not specified, *api_key* should be retrieved\n        from :envvar:`FTRACK_API_KEY` and *api_user* from\n        :envvar:`FTRACK_API_USER`.\n\n        If *auto_populate* is True (the default), then accessing entity\n        attributes will cause them to be automatically fetched from the server\n        if they are not already. This flag can be changed on the session\n        directly at any time.\n\n        *plugin_paths* should be a list of paths to search for plugins. If not\n        specified, default to looking up :envvar:`FTRACK_EVENT_PLUGIN_PATH`.\n\n        *cache* should be an instance of a cache that fulfils the\n        :class:`ftrack_api.cache.Cache` interface and will be used as the cache\n        for the session. It can also be a callable that will be called with the\n        session instance as sole argument. The callable should return ``None``\n        if a suitable cache could not be configured, but session instantiation\n        can continue safely.\n\n        .. note::\n\n            The session will add the specified cache to a pre-configured layered\n            cache that specifies the top level cache as a\n            :class:`ftrack_api.cache.MemoryCache`. Therefore, it is unnecessary\n            to construct a separate memory cache for typical behaviour. Working\n            around this behaviour or removing the memory cache can lead to\n            unexpected behaviour.\n\n        *cache_key_maker* should be an instance of a key maker that fulfils the\n        :class:`ftrack_api.cache.KeyMaker` interface and will be used to\n        generate keys for objects being stored in the *cache*. If not specified,\n        a :class:`~ftrack_api.cache.StringKeyMaker` will be used.\n\n        If *auto_connect_event_hub* is True then embedded event hub will be\n        automatically connected to the event server and allow for publishing and\n        subscribing to **non-local** events. If False, then only publishing and\n        subscribing to **local** events will be possible until the hub is\n        manually connected using :meth:`EventHub.connect\n        <ftrack_api.event.hub.EventHub.connect>`.\n\n        .. note::\n\n            The event hub connection is performed in a background thread to\n            improve session startup time. If a registered plugin requires a\n            connected event hub then it should check the event hub connection\n            status explicitly. Subscribing to events does *not* require a\n            connected event hub.\n\n        Enable schema caching by setting *schema_cache_path* to a folder path.\n        If not set, :envvar:`FTRACK_API_SCHEMA_CACHE_PATH` will be used to\n        determine the path to store cache in. If the environment variable is\n        also not specified then a temporary directory will be used. Set to\n        `False` to disable schema caching entirely.\n\n        *plugin_arguments* should be an optional mapping (dict) of keyword\n        arguments to pass to plugin register functions upon discovery. If a\n        discovered plugin has a signature that is incompatible with the passed\n        arguments, the discovery mechanism will attempt to reduce the passed\n        arguments to only those that the plugin accepts. Note that a warning\n        will be logged in this case.\n\n        '''\n        super(Session, self).__init__()\n        self.logger = logging.getLogger(\n            __name__ + '.' + self.__class__.__name__\n        )\n        self._closed = False\n\n        if server_url is None:\n            server_url = os.environ.get('FTRACK_SERVER')\n\n        if not server_url:\n            raise TypeError(\n                'Required \"server_url\" not specified. Pass as argument or set '\n                'in environment variable FTRACK_SERVER.'\n            )\n\n        self._server_url = server_url\n\n        if api_key is None:\n            api_key = os.environ.get(\n                'FTRACK_API_KEY',\n                # Backwards compatibility\n                os.environ.get('FTRACK_APIKEY')\n            )\n\n        if not api_key:\n            raise TypeError(\n                'Required \"api_key\" not specified. Pass as argument or set in '\n                'environment variable FTRACK_API_KEY.'\n            )\n\n        self._api_key = api_key\n\n        if api_user is None:\n            api_user = os.environ.get('FTRACK_API_USER')\n            if not api_user:\n                try:\n                    api_user = getpass.getuser()\n                except Exception:\n                    pass\n\n        if not api_user:\n            raise TypeError(\n                'Required \"api_user\" not specified. Pass as argument, set in '\n                'environment variable FTRACK_API_USER or one of the standard '\n                'environment variables used by Python\\'s getpass module.'\n            )\n\n        self._api_user = api_user\n\n        # Currently pending operations.\n        self.recorded_operations = ftrack_api.operation.Operations()\n        self.record_operations = True\n\n        self.cache_key_maker = cache_key_maker\n        if self.cache_key_maker is None:\n            self.cache_key_maker = ftrack_api.cache.StringKeyMaker()\n\n        # Enforce always having a memory cache at top level so that the same\n        # in-memory instance is returned from session.\n        self.cache = ftrack_api.cache.LayeredCache([\n            ftrack_api.cache.MemoryCache()\n        ])\n\n        if cache is not None:\n            if callable(cache):\n                cache = cache(self)\n\n            if cache is not None:\n                self.cache.caches.append(cache)\n\n        self._managed_request = None\n        self._request = requests.Session()\n        self._request.auth = SessionAuthentication(\n            self._api_key, self._api_user\n        )\n\n        self.auto_populate = auto_populate\n\n        # Fetch server information and in doing so also check credentials.\n        self._server_information = self._fetch_server_information()\n\n        # Now check compatibility of server based on retrieved information.\n        self.check_server_compatibility()\n\n        # Construct event hub and load plugins.\n        self._event_hub = ftrack_api.event.hub.EventHub(\n            self._server_url,\n            self._api_user,\n            self._api_key,\n        )\n\n        self._auto_connect_event_hub_thread = None\n        if auto_connect_event_hub is True:\n            # Connect to event hub in background thread so as not to block main\n            # session usage waiting for event hub connection.\n            self._auto_connect_event_hub_thread = threading.Thread(\n                target=self._event_hub.connect\n            )\n            self._auto_connect_event_hub_thread.daemon = True\n            self._auto_connect_event_hub_thread.start()\n\n        # To help with migration from auto_connect_event_hub default changing\n        # from True to False.\n        self._event_hub._deprecation_warning_auto_connect = False\n\n        # Register to auto-close session on exit.\n        atexit.register(WeakMethod(self.close))\n\n        self._plugin_paths = plugin_paths\n        if self._plugin_paths is None:\n            self._plugin_paths = os.environ.get(\n                'FTRACK_EVENT_PLUGIN_PATH', ''\n            ).split(os.pathsep)\n\n        self._discover_plugins(plugin_arguments=plugin_arguments)\n\n        # TODO: Make schemas read-only and non-mutable (or at least without\n        # rebuilding types)?\n        if schema_cache_path is not False:\n            if schema_cache_path is None:\n                schema_cache_path = appdirs.user_cache_dir()\n                schema_cache_path = os.environ.get(\n                    'FTRACK_API_SCHEMA_CACHE_PATH', schema_cache_path\n                )\n\n            schema_cache_path = os.path.join(\n                schema_cache_path, 'ftrack_api_schema_cache.json'\n            )\n\n        self.schemas = self._load_schemas(schema_cache_path)\n        self.types = self._build_entity_type_classes(self.schemas)\n\n        ftrack_api._centralized_storage_scenario.register(self)\n\n        self._configure_locations()\n        self.event_hub.publish(\n            ftrack_api.event.base.Event(\n                topic='ftrack.api.session.ready',\n                data=dict(\n                    session=self\n                )\n            ),\n            synchronous=True\n        )\n\n    def __enter__(self):\n        '''Return session as context manager.'''\n        return self\n\n    def __exit__(self, exception_type, exception_value, traceback):\n        '''Exit session context, closing session in process.'''\n        self.close()\n\n    @property\n    def _request(self):\n        '''Return request session.\n\n        Raise :exc:`ftrack_api.exception.ConnectionClosedError` if session has\n        been closed and connection unavailable.\n\n        '''\n        if self._managed_request is None:\n            raise ftrack_api.exception.ConnectionClosedError()\n\n        return self._managed_request\n\n    @_request.setter\n    def _request(self, value):\n        '''Set request session to *value*.'''\n        self._managed_request = value\n\n    @property\n    def closed(self):\n        '''Return whether session has been closed.'''\n        return self._closed\n\n    @property\n    def server_information(self):\n        '''Return server information such as server version.'''\n        return self._server_information.copy()\n\n    @property\n    def server_url(self):\n        '''Return server ulr used for session.'''\n        return self._server_url\n\n    @property\n    def api_user(self):\n        '''Return username used for session.'''\n        return self._api_user\n\n    @property\n    def api_key(self):\n        '''Return API key used for session.'''\n        return self._api_key\n\n    @property\n    def event_hub(self):\n        '''Return event hub.'''\n        return self._event_hub\n\n    @property\n    def _local_cache(self):\n        '''Return top level memory cache.'''\n        return self.cache.caches[0]\n\n    def check_server_compatibility(self):\n        '''Check compatibility with connected server.'''\n        server_version = self.server_information.get('version')\n        if server_version is None:\n            raise ftrack_api.exception.ServerCompatibilityError(\n                'Could not determine server version.'\n            )\n\n        # Perform basic version check.\n        if server_version != 'dev':\n            min_server_version = '3.3.11'\n            if (\n                distutils.version.LooseVersion(min_server_version)\n                > distutils.version.LooseVersion(server_version)\n            ):\n                raise ftrack_api.exception.ServerCompatibilityError(\n                    'Server version {0} incompatible with this version of the '\n                    'API which requires a server version >= {1}'.format(\n                        server_version,\n                        min_server_version\n                    )\n                )\n\n    def close(self):\n        '''Close session.\n\n        Close connections to server. Clear any pending operations and local\n        cache.\n\n        Use this to ensure that session is cleaned up properly after use.\n\n        '''\n        if self.closed:\n            self.logger.debug('Session already closed.')\n            return\n\n        self._closed = True\n\n        self.logger.debug('Closing session.')\n        if self.recorded_operations:\n            self.logger.warning(\n                'Closing session with pending operations not persisted.'\n            )\n\n        # Clear pending operations.\n        self.recorded_operations.clear()\n\n        # Clear top level cache (expected to be enforced memory cache).\n        self._local_cache.clear()\n\n        # Close connections.\n        self._request.close()\n        self._request = None\n\n        try:\n            self.event_hub.disconnect()\n            if self._auto_connect_event_hub_thread:\n                self._auto_connect_event_hub_thread.join()\n        except ftrack_api.exception.EventHubConnectionError:\n            pass\n\n        self.logger.debug('Session closed.')\n\n    def reset(self):\n        '''Reset session clearing local state.\n\n        Clear all pending operations and expunge all entities from session.\n\n        Also clear the local cache. If the cache used by the session is a\n        :class:`~ftrack_api.cache.LayeredCache` then only clear top level cache.\n        Otherwise, clear the entire cache.\n\n        Plugins are not rediscovered or reinitialised, but certain plugin events\n        are re-emitted to properly configure session aspects that are dependant\n        on cache (such as location plugins).\n\n        .. warning::\n\n            Previously attached entities are not reset in memory and will retain\n            their state, but should not be used. Doing so will cause errors.\n\n        '''\n        if self.recorded_operations:\n            self.logger.warning(\n                'Resetting session with pending operations not persisted.'\n            )\n\n        # Clear pending operations.\n        self.recorded_operations.clear()\n\n        # Clear top level cache (expected to be enforced memory cache).\n        self._local_cache.clear()\n\n        # Re-configure certain session aspects that may be dependant on cache.\n        self._configure_locations()\n\n        self.event_hub.publish(\n            ftrack_api.event.base.Event(\n                topic='ftrack.api.session.reset',\n                data=dict(\n                    session=self\n                )\n            ),\n            synchronous=True\n        )\n\n    def auto_populating(self, auto_populate):\n        '''Temporarily set auto populate to *auto_populate*.\n\n        The current setting will be restored automatically when done.\n\n        Example::\n\n            with session.auto_populating(False):\n                print entity['name']\n\n        '''\n        return AutoPopulatingContext(self, auto_populate)\n\n    def operation_recording(self, record_operations):\n        '''Temporarily set operation recording to *record_operations*.\n\n        The current setting will be restored automatically when done.\n\n        Example::\n\n            with session.operation_recording(False):\n                entity['name'] = 'change_not_recorded'\n\n        '''\n        return OperationRecordingContext(self, record_operations)\n\n    @property\n    def created(self):\n        '''Return list of newly created entities.'''\n        entities = self._local_cache.values()\n        states = ftrack_api.inspection.states(entities)\n\n        return [\n            entity for (entity, state) in itertools.izip(entities, states)\n            if state is ftrack_api.symbol.CREATED\n        ]\n\n    @property\n    def modified(self):\n        '''Return list of locally modified entities.'''\n        entities = self._local_cache.values()\n        states = ftrack_api.inspection.states(entities)\n\n        return [\n            entity for (entity, state) in itertools.izip(entities, states)\n            if state is ftrack_api.symbol.MODIFIED\n        ]\n\n    @property\n    def deleted(self):\n        '''Return list of deleted entities.'''\n        entities = self._local_cache.values()\n        states = ftrack_api.inspection.states(entities)\n\n        return [\n            entity for (entity, state) in itertools.izip(entities, states)\n            if state is ftrack_api.symbol.DELETED\n        ]\n\n    def reset_remote(self, reset_type, entity=None):\n        '''Perform a server side reset.\n\n        *reset_type* is a server side supported reset type,\n        passing the optional *entity* to perform the option upon.\n\n        Please refer to ftrack documentation for a complete list of\n        supported server side reset types.\n        '''\n\n        payload = {\n            'action': 'reset_remote',\n            'reset_type': reset_type\n        }\n\n        if entity is not None:\n            payload.update({\n                'entity_type': entity.entity_type,\n                'entity_key': entity.get('id')\n            })\n\n        result = self.call(\n            [payload]\n        )\n\n        return result[0]['data']\n\n    def create(self, entity_type, data=None, reconstructing=False):\n        '''Create and return an entity of *entity_type* with initial *data*.\n\n        If specified, *data* should be a dictionary of key, value pairs that\n        should be used to populate attributes on the entity.\n\n        If *reconstructing* is False then create a new entity setting\n        appropriate defaults for missing data. If True then reconstruct an\n        existing entity.\n\n        Constructed entity will be automatically :meth:`merged <Session.merge>`\n        into the session.\n\n        '''\n        entity = self._create(entity_type, data, reconstructing=reconstructing)\n        entity = self.merge(entity)\n        return entity\n\n    def _create(self, entity_type, data, reconstructing):\n        '''Create and return an entity of *entity_type* with initial *data*.'''\n        try:\n            EntityTypeClass = self.types[entity_type]\n        except KeyError:\n            raise ftrack_api.exception.UnrecognisedEntityTypeError(entity_type)\n\n        return EntityTypeClass(self, data=data, reconstructing=reconstructing)\n\n    def ensure(self, entity_type, data, identifying_keys=None):\n        '''Retrieve entity of *entity_type* with *data*, creating if necessary.\n\n        *data* should be a dictionary of the same form passed to :meth:`create`.\n\n        By default, check for an entity that has matching *data*. If\n        *identifying_keys* is specified as a list of keys then only consider the\n        values from *data* for those keys when searching for existing entity. If\n        *data* is missing an identifying key then raise :exc:`KeyError`.\n\n        If no *identifying_keys* specified then use all of the keys from the\n        passed *data*. Raise :exc:`ValueError` if no *identifying_keys* can be\n        determined.\n\n        Each key should be a string.\n\n        .. note::\n\n            Currently only top level scalars supported. To ensure an entity by\n            looking at relationships, manually issue the :meth:`query` and\n            :meth:`create` calls.\n\n        If more than one entity matches the determined filter criteria then\n        raise :exc:`~ftrack_api.exception.MultipleResultsFoundError`.\n\n        If no matching entity found then create entity using supplied *data*.\n\n        If a matching entity is found, then update it if necessary with *data*.\n\n        .. note::\n\n            If entity created or updated then a :meth:`commit` will be issued\n            automatically. If this behaviour is undesired, perform the\n            :meth:`query` and :meth:`create` calls manually.\n\n        Return retrieved or created entity.\n\n        Example::\n\n            # First time, a new entity with `username=martin` is created.\n            entity = session.ensure('User', {'username': 'martin'})\n\n            # After that, the existing entity is retrieved.\n            entity = session.ensure('User', {'username': 'martin'})\n\n            # When existing entity retrieved, entity may also be updated to\n            # match supplied data.\n            entity = session.ensure(\n                'User', {'username': 'martin', 'email': 'martin@example.com'}\n            )\n\n        '''\n        if not identifying_keys:\n            identifying_keys = data.keys()\n\n        self.logger.debug(L(\n            'Ensuring entity {0!r} with data {1!r} using identifying keys '\n            '{2!r}', entity_type, data, identifying_keys\n        ))\n\n        if not identifying_keys:\n            raise ValueError(\n                'Could not determine any identifying data to check against '\n                'when ensuring {0!r} with data {1!r}. Identifying keys: {2!r}'\n                .format(entity_type, data, identifying_keys)\n            )\n\n        expression = '{0} where'.format(entity_type)\n        criteria = []\n        for identifying_key in identifying_keys:\n            value = data[identifying_key]\n\n            if isinstance(value, basestring):\n                value = '\"{0}\"'.format(value)\n\n            elif isinstance(\n                value, (arrow.Arrow, datetime.datetime, datetime.date)\n            ):\n                # Server does not store microsecond or timezone currently so\n                # need to strip from query.\n                # TODO: When datetime handling improved, update this logic.\n                value = (\n                    arrow.get(value).naive.replace(microsecond=0).isoformat()\n                )\n                value = '\"{0}\"'.format(value)\n\n            criteria.append('{0} is {1}'.format(identifying_key, value))\n\n        expression = '{0} {1}'.format(\n            expression, ' and '.join(criteria)\n        )\n\n        try:\n            entity = self.query(expression).one()\n\n        except ftrack_api.exception.NoResultFoundError:\n            self.logger.debug('Creating entity as did not already exist.')\n\n            # Create entity.\n            entity = self.create(entity_type, data)\n            self.commit()\n\n        else:\n            self.logger.debug('Retrieved matching existing entity.')\n\n            # Update entity if required.\n            updated = False\n            for key, target_value in data.items():\n                if entity[key] != target_value:\n                    entity[key] = target_value\n                    updated = True\n\n            if updated:\n                self.logger.debug('Updating existing entity to match new data.')\n                self.commit()\n\n        return entity\n\n    def delete(self, entity):\n        '''Mark *entity* for deletion.'''\n        if self.record_operations:\n            self.recorded_operations.push(\n                ftrack_api.operation.DeleteEntityOperation(\n                    entity.entity_type,\n                    ftrack_api.inspection.primary_key(entity)\n                )\n            )\n\n    def get(self, entity_type, entity_key):\n        '''Return entity of *entity_type* with unique *entity_key*.\n\n        First check for an existing entry in the configured cache, otherwise\n        issue a query to the server.\n\n        If no matching entity found, return None.\n\n        '''\n        self.logger.debug(L('Get {0} with key {1}', entity_type, entity_key))\n\n        primary_key_definition = self.types[entity_type].primary_key_attributes\n        if isinstance(entity_key, basestring):\n            entity_key = [entity_key]\n\n        if len(entity_key) != len(primary_key_definition):\n            raise ValueError(\n                'Incompatible entity_key {0!r} supplied. Entity type {1} '\n                'expects a primary key composed of {2} values ({3}).'\n                .format(\n                    entity_key, entity_type, len(primary_key_definition),\n                    ', '.join(primary_key_definition)\n                )\n            )\n\n        entity = None\n        try:\n            entity = self._get(entity_type, entity_key)\n\n\n        except KeyError:\n\n            # Query for matching entity.\n            self.logger.debug(\n                'Entity not present in cache. Issuing new query.'\n            )\n            condition = []\n            for key, value in zip(primary_key_definition, entity_key):\n                condition.append('{0} is \"{1}\"'.format(key, value))\n\n            expression = '{0} where ({1})'.format(\n                entity_type, ' and '.join(condition)\n            )\n\n            results = self.query(expression).all()\n            if results:\n                entity = results[0]\n\n        return entity\n\n    def _get(self, entity_type, entity_key):\n        '''Return cached entity of *entity_type* with unique *entity_key*.\n\n        Raise :exc:`KeyError` if no such entity in the cache.\n\n        '''\n        # Check cache for existing entity emulating\n        # ftrack_api.inspection.identity result object to pass to key maker.\n        cache_key = self.cache_key_maker.key(\n            (str(entity_type), map(str, entity_key))\n        )\n        self.logger.debug(L(\n            'Checking cache for entity with key {0}', cache_key\n        ))\n        entity = self.cache.get(cache_key)\n        self.logger.debug(L(\n            'Retrieved existing entity from cache: {0} at {1}',\n            entity, id(entity)\n        ))\n\n        return entity\n\n    def query(self, expression, page_size=500):\n        '''Query against remote data according to *expression*.\n\n        *expression* is not executed directly. Instead return an\n        :class:`ftrack_api.query.QueryResult` instance that will execute remote\n        call on access.\n\n        *page_size* specifies the maximum page size that the returned query\n        result object should be configured with.\n\n        .. seealso:: :ref:`querying`\n\n        '''\n        self.logger.debug(L('Query {0!r}', expression))\n\n        # Add in sensible projections if none specified. Note that this is\n        # done here rather than on the server to allow local modification of the\n        # schema setting to include commonly used custom attributes for example.\n        # TODO: Use a proper parser perhaps?\n        if not expression.startswith('select'):\n            entity_type = expression.split(' ', 1)[0]\n            EntityTypeClass = self.types[entity_type]\n            projections = EntityTypeClass.default_projections\n\n            expression = 'select {0} from {1}'.format(\n                ', '.join(projections),\n                expression\n            )\n\n        query_result = ftrack_api.query.QueryResult(\n            self, expression, page_size=page_size\n        )\n        return query_result\n\n    def _query(self, expression):\n        '''Execute *query* and return (records, metadata).\n\n        Records will be a list of entities retrieved via the query and metadata\n        a dictionary of accompanying information about the result set.\n\n        '''\n        # TODO: Actually support batching several queries together.\n        # TODO: Should batches have unique ids to match them up later.\n        batch = [{\n            'action': 'query',\n            'expression': expression\n        }]\n\n        # TODO: When should this execute? How to handle background=True?\n        results = self.call(batch)\n\n        # Merge entities into local cache and return merged entities.\n        data = []\n        merged = dict()\n        for entity in results[0]['data']:\n            data.append(self._merge_recursive(entity, merged))\n\n        return data, results[0]['metadata']\n\n    def merge(self, value, merged=None):\n        '''Merge *value* into session and return merged value.\n\n        *merged* should be a mapping to record merges during run and should be\n        used to avoid infinite recursion. If not set will default to a\n        dictionary.\n\n        '''\n        if merged is None:\n            merged = {}\n\n        with self.operation_recording(False):\n            return self._merge(value, merged)\n\n    def _merge(self, value, merged):\n        '''Return merged *value*.'''\n        log_debug = self.logger.isEnabledFor(logging.DEBUG)\n\n        if isinstance(value, ftrack_api.entity.base.Entity):\n            log_debug and self.logger.debug(\n                'Merging entity into session: {0} at {1}'\n                .format(value, id(value))\n            )\n\n            return self._merge_entity(value, merged=merged)\n\n        elif isinstance(value, ftrack_api.collection.Collection):\n            log_debug and self.logger.debug(\n                'Merging collection into session: {0!r} at {1}'\n                .format(value, id(value))\n            )\n\n            merged_collection = []\n            for entry in value:\n                merged_collection.append(\n                    self._merge(entry, merged=merged)\n                )\n\n            return merged_collection\n\n        elif isinstance(value, ftrack_api.collection.MappedCollectionProxy):\n            log_debug and self.logger.debug(\n                'Merging mapped collection into session: {0!r} at {1}'\n                .format(value, id(value))\n            )\n\n            merged_collection = []\n            for entry in value.collection:\n                merged_collection.append(\n                    self._merge(entry, merged=merged)\n                )\n\n            return merged_collection\n\n        else:\n            return value\n\n    def _merge_recursive(self, entity, merged=None):\n        '''Merge *entity* and all its attributes recursivly.'''\n        log_debug = self.logger.isEnabledFor(logging.DEBUG)\n\n        if merged is None:\n            merged = {}\n\n        attached = self.merge(entity, merged)\n\n        for attribute in entity.attributes:\n            # Remote attributes.\n            remote_value = attribute.get_remote_value(entity)\n\n            if isinstance(\n                remote_value,\n                (\n                    ftrack_api.entity.base.Entity,\n                    ftrack_api.collection.Collection,\n                    ftrack_api.collection.MappedCollectionProxy\n                )\n            ):\n                log_debug and self.logger.debug(\n                    'Merging remote value for attribute {0}.'.format(attribute)\n                )\n\n                if isinstance(remote_value, ftrack_api.entity.base.Entity):\n                    self._merge_recursive(remote_value, merged=merged)\n\n                elif isinstance(\n                    remote_value, ftrack_api.collection.Collection\n                ):\n                    for entry in remote_value:\n                        self._merge_recursive(entry, merged=merged)\n\n                elif isinstance(\n                    remote_value, ftrack_api.collection.MappedCollectionProxy\n                ):\n                    for entry in remote_value.collection:\n                        self._merge_recursive(entry, merged=merged)\n\n        return attached\n\n    def _merge_entity(self, entity, merged=None):\n        '''Merge *entity* into session returning merged entity.\n\n        Merge is recursive so any references to other entities will also be\n        merged.\n\n        *entity* will never be modified in place. Ensure that the returned\n        merged entity instance is used.\n\n        '''\n        log_debug = self.logger.isEnabledFor(logging.DEBUG)\n\n        if merged is None:\n            merged = {}\n\n        with self.auto_populating(False):\n            entity_key = self.cache_key_maker.key(\n                ftrack_api.inspection.identity(entity)\n            )\n\n            # Check whether this entity has already been processed.\n            attached_entity = merged.get(entity_key)\n            if attached_entity is not None:\n                log_debug and self.logger.debug(\n                    'Entity already processed for key {0} as {1} at {2}'\n                    .format(entity_key, attached_entity, id(attached_entity))\n                )\n\n                return attached_entity\n            else:\n                log_debug and self.logger.debug(\n                    'Entity not already processed for key {0}.'\n                    .format(entity_key)\n                )\n\n            # Check for existing instance of entity in cache.\n            log_debug and self.logger.debug(\n                'Checking for entity in cache with key {0}'.format(entity_key)\n            )\n            try:\n                attached_entity = self.cache.get(entity_key)\n\n                log_debug and self.logger.debug(\n                    'Retrieved existing entity from cache: {0} at {1}'\n                    .format(attached_entity, id(attached_entity))\n                )\n\n            except KeyError:\n                # Construct new minimal instance to store in cache.\n                attached_entity = self._create(\n                    entity.entity_type, {}, reconstructing=True\n                )\n\n                log_debug and self.logger.debug(\n                    'Entity not present in cache. Constructed new instance: '\n                    '{0} at {1}'.format(attached_entity, id(attached_entity))\n                )\n\n            # Mark entity as seen to avoid infinite loops.\n            merged[entity_key] = attached_entity\n\n            changes = attached_entity.merge(entity, merged=merged)\n            if changes:\n                self.cache.set(entity_key, attached_entity)\n                self.logger.debug('Cache updated with merged entity.')\n\n            else:\n                self.logger.debug(\n                    'Cache not updated with merged entity as no differences '\n                    'detected.'\n                )\n\n        return attached_entity\n\n    def populate(self, entities, projections):\n        '''Populate *entities* with attributes specified by *projections*.\n\n        Any locally set values included in the *projections* will not be\n        overwritten with the retrieved remote value. If this 'synchronise'\n        behaviour is required, first clear the relevant values on the entity by\n        setting them to :attr:`ftrack_api.symbol.NOT_SET`. Deleting the key will\n        have the same effect::\n\n            >>> print(user['username'])\n            martin\n            >>> del user['username']\n            >>> print(user['username'])\n            Symbol(NOT_SET)\n\n        .. note::\n\n            Entities that have been created and not yet persisted will be\n            skipped as they have no remote values to fetch.\n\n        '''\n        self.logger.debug(L(\n            'Populate {0!r} projections for {1}.', projections, entities\n        ))\n\n        if not isinstance(\n            entities, (list, tuple, ftrack_api.query.QueryResult)\n        ):\n            entities = [entities]\n\n        # TODO: How to handle a mixed collection of different entity types\n        # Should probably fail, but need to consider handling hierarchies such\n        # as User and Group both deriving from Resource. Actually, could just\n        # proceed and ignore projections that are not present in entity type.\n\n        entities_to_process = []\n\n        for entity in entities:\n            if ftrack_api.inspection.state(entity) is ftrack_api.symbol.CREATED:\n                # Created entities that are not yet persisted have no remote\n                # values. Don't raise an error here as it is reasonable to\n                # iterate over an entities properties and see that some of them\n                # are NOT_SET.\n                self.logger.debug(L(\n                    'Skipping newly created entity {0!r} for population as no '\n                    'data will exist in the remote for this entity yet.', entity\n                ))\n                continue\n\n            entities_to_process.append(entity)\n\n        if entities_to_process:\n            reference_entity = entities_to_process[0]\n            entity_type = reference_entity.entity_type\n            query = 'select {0} from {1}'.format(projections, entity_type)\n\n            primary_key_definition = reference_entity.primary_key_attributes\n            entity_keys = [\n                ftrack_api.inspection.primary_key(entity).values()\n                for entity in entities_to_process\n            ]\n\n            if len(primary_key_definition) > 1:\n                # Composite keys require full OR syntax unfortunately.\n                conditions = []\n                for entity_key in entity_keys:\n                    condition = []\n                    for key, value in zip(primary_key_definition, entity_key):\n                        condition.append('{0} is \"{1}\"'.format(key, value))\n\n                    conditions.append('({0})'.format('and '.join(condition)))\n\n                query = '{0} where {1}'.format(query, ' or '.join(conditions))\n\n            else:\n                primary_key = primary_key_definition[0]\n\n                if len(entity_keys) > 1:\n                    query = '{0} where {1} in ({2})'.format(\n                        query, primary_key,\n                        ','.join([\n                            str(entity_key[0]) for entity_key in entity_keys\n                        ])\n                    )\n                else:\n                    query = '{0} where {1} is {2}'.format(\n                        query, primary_key, str(entity_keys[0][0])\n                    )\n\n            result = self.query(query)\n\n            # Fetch all results now. Doing so will cause them to populate the\n            # relevant entities in the cache.\n            result.all()\n\n            # TODO: Should we check that all requested attributes were\n            # actually populated? If some weren't would we mark that to avoid\n            # repeated calls or perhaps raise an error?\n\n    # TODO: Make atomic.\n    def commit(self):\n        '''Commit all local changes to the server.'''\n        batch = []\n\n        with self.auto_populating(False):\n            for operation in self.recorded_operations:\n\n                # Convert operation to payload.\n                if isinstance(\n                    operation, ftrack_api.operation.CreateEntityOperation\n                ):\n                    # At present, data payload requires duplicating entity\n                    # type in data and also ensuring primary key added.\n                    entity_data = {\n                        '__entity_type__': operation.entity_type,\n                    }\n                    entity_data.update(operation.entity_key)\n                    entity_data.update(operation.entity_data)\n\n                    payload = OperationPayload({\n                        'action': 'create',\n                        'entity_type': operation.entity_type,\n                        'entity_key': operation.entity_key.values(),\n                        'entity_data': entity_data\n                    })\n\n                elif isinstance(\n                    operation, ftrack_api.operation.UpdateEntityOperation\n                ):\n                    entity_data = {\n                        # At present, data payload requires duplicating entity\n                        # type.\n                        '__entity_type__': operation.entity_type,\n                        operation.attribute_name: operation.new_value\n                    }\n\n                    payload = OperationPayload({\n                        'action': 'update',\n                        'entity_type': operation.entity_type,\n                        'entity_key': operation.entity_key.values(),\n                        'entity_data': entity_data\n                    })\n\n                elif isinstance(\n                    operation, ftrack_api.operation.DeleteEntityOperation\n                ):\n                    payload = OperationPayload({\n                        'action': 'delete',\n                        'entity_type': operation.entity_type,\n                        'entity_key': operation.entity_key.values()\n                    })\n\n                else:\n                    raise ValueError(\n                        'Cannot commit. Unrecognised operation type {0} '\n                        'detected.'.format(type(operation))\n                    )\n\n                batch.append(payload)\n\n        # Optimise batch.\n        # TODO: Might be better to perform these on the operations list instead\n        # so all operation contextual information available.\n\n        # If entity was created and deleted in one batch then remove all\n        # payloads for that entity.\n        created = set()\n        deleted = set()\n\n        for payload in batch:\n            if payload['action'] == 'create':\n                created.add(\n                    (payload['entity_type'], str(payload['entity_key']))\n                )\n\n            elif payload['action'] == 'delete':\n                deleted.add(\n                    (payload['entity_type'], str(payload['entity_key']))\n                )\n\n        created_then_deleted = deleted.intersection(created)\n        if created_then_deleted:\n            optimised_batch = []\n            for payload in batch:\n                entity_type = payload.get('entity_type')\n                entity_key = str(payload.get('entity_key'))\n\n                if (entity_type, entity_key) in created_then_deleted:\n                    continue\n\n                optimised_batch.append(payload)\n\n            batch = optimised_batch\n\n        # Remove early update operations so that only last operation on\n        # attribute is applied server side.\n        updates_map = set()\n        for payload in reversed(batch):\n            if payload['action'] in ('update', ):\n                for key, value in payload['entity_data'].items():\n                    if key == '__entity_type__':\n                        continue\n\n                    identity = (\n                        payload['entity_type'], str(payload['entity_key']), key\n                    )\n                    if identity in updates_map:\n                        del payload['entity_data'][key]\n                    else:\n                        updates_map.add(identity)\n\n        # Remove NOT_SET values from entity_data.\n        for payload in batch:\n            entity_data = payload.get('entity_data', {})\n            for key, value in entity_data.items():\n                if value is ftrack_api.symbol.NOT_SET:\n                    del entity_data[key]\n\n        # Remove payloads with redundant entity_data.\n        optimised_batch = []\n        for payload in batch:\n            entity_data = payload.get('entity_data')\n            if entity_data is not None:\n                keys = entity_data.keys()\n                if not keys or keys == ['__entity_type__']:\n                    continue\n\n            optimised_batch.append(payload)\n\n        batch = optimised_batch\n\n        # Collapse updates that are consecutive into one payload. Also, collapse\n        # updates that occur immediately after creation into the create payload.\n        optimised_batch = []\n        previous_payload = None\n\n        for payload in batch:\n            if (\n                previous_payload is not None\n                and payload['action'] == 'update'\n                and previous_payload['action'] in ('create', 'update')\n                and previous_payload['entity_type'] == payload['entity_type']\n                and previous_payload['entity_key'] == payload['entity_key']\n            ):\n                previous_payload['entity_data'].update(payload['entity_data'])\n                continue\n\n            else:\n                optimised_batch.append(payload)\n                previous_payload = payload\n\n        batch = optimised_batch\n\n        # Process batch.\n        if batch:\n            result = self.call(batch)\n\n            # Clear recorded operations.\n            self.recorded_operations.clear()\n\n            # As optimisation, clear local values which are not primary keys to\n            # avoid redundant merges when merging references. Note: primary keys\n            # remain as needed for cache retrieval on new entities.\n            with self.auto_populating(False):\n                with self.operation_recording(False):\n                    for entity in self._local_cache.values():\n                        for attribute in entity:\n                            if attribute not in entity.primary_key_attributes:\n                                del entity[attribute]\n\n            # Process results merging into cache relevant data.\n            for entry in result:\n\n                if entry['action'] in ('create', 'update'):\n                    # Merge returned entities into local cache.\n                    self.merge(entry['data'])\n\n                elif entry['action'] == 'delete':\n                    # TODO: Detach entity - need identity returned?\n                    # TODO: Expunge entity from cache.\n                    pass\n            # Clear remaining local state, including local values for primary\n            # keys on entities that were merged.\n            with self.auto_populating(False):\n                with self.operation_recording(False):\n                    for entity in self._local_cache.values():\n                        entity.clear()\n\n    def rollback(self):\n        '''Clear all recorded operations and local state.\n\n        Typically this would be used following a failed :meth:`commit` in order\n        to revert the session to a known good state.\n\n        Newly created entities not yet persisted will be detached from the\n        session / purged from cache and no longer contribute, but the actual\n        objects are not deleted from memory. They should no longer be used and\n        doing so could cause errors.\n\n        '''\n        with self.auto_populating(False):\n            with self.operation_recording(False):\n\n                # Detach all newly created entities and remove from cache. This\n                # is done because simply clearing the local values of newly\n                # created entities would result in entities with no identity as\n                # primary key was local while not persisted. In addition, it\n                # makes no sense for failed created entities to exist in session\n                # or cache.\n                for operation in self.recorded_operations:\n                    if isinstance(\n                        operation, ftrack_api.operation.CreateEntityOperation\n                    ):\n                        entity_key = str((\n                            str(operation.entity_type),\n                            operation.entity_key.values()\n                        ))\n                        try:\n                            self.cache.remove(entity_key)\n                        except KeyError:\n                            pass\n\n                # Clear locally stored modifications on remaining entities.\n                for entity in self._local_cache.values():\n                    entity.clear()\n\n        self.recorded_operations.clear()\n\n    def _fetch_server_information(self):\n        '''Return server information.'''\n        result = self.call([{'action': 'query_server_information'}])\n        return result[0]\n\n    def _discover_plugins(self, plugin_arguments=None):\n        '''Find and load plugins in search paths.\n\n        Each discovered module should implement a register function that\n        accepts this session as first argument. Typically the function should\n        register appropriate event listeners against the session's event hub.\n\n            def register(session):\n                session.event_hub.subscribe(\n                    'topic=ftrack.api.session.construct-entity-type',\n                    construct_entity_type\n                )\n\n        *plugin_arguments* should be an optional mapping of keyword arguments\n        and values to pass to plugin register functions upon discovery.\n\n        '''\n        plugin_arguments = plugin_arguments or {}\n        ftrack_api.plugin.discover(\n            self._plugin_paths, [self], plugin_arguments\n        )\n\n    def _read_schemas_from_cache(self, schema_cache_path):\n        '''Return schemas and schema hash from *schema_cache_path*.\n\n        *schema_cache_path* should be the path to the file containing the\n        schemas in JSON format.\n\n        '''\n        self.logger.debug(L(\n            'Reading schemas from cache {0!r}', schema_cache_path\n        ))\n\n        if not os.path.exists(schema_cache_path):\n            self.logger.info(L(\n                'Cache file not found at {0!r}.', schema_cache_path\n            ))\n\n            return [], None\n\n        with open(schema_cache_path, 'r') as schema_file:\n            schemas = json.load(schema_file)\n            hash_ = hashlib.md5(\n                json.dumps(schemas, sort_keys=True)\n            ).hexdigest()\n\n        return schemas, hash_\n\n    def _write_schemas_to_cache(self, schemas, schema_cache_path):\n        '''Write *schemas* to *schema_cache_path*.\n\n        *schema_cache_path* should be a path to a file that the schemas can be\n        written to in JSON format.\n\n        '''\n        self.logger.debug(L(\n            'Updating schema cache {0!r} with new schemas.', schema_cache_path\n        ))\n\n        with open(schema_cache_path, 'w') as local_cache_file:\n            json.dump(schemas, local_cache_file, indent=4)\n\n    def _load_schemas(self, schema_cache_path):\n        '''Load schemas.\n\n        First try to load schemas from cache at *schema_cache_path*. If the\n        cache is not available or the cache appears outdated then load schemas\n        from server and store fresh copy in cache.\n\n        If *schema_cache_path* is set to `False`, always load schemas from\n        server bypassing cache.\n\n        '''\n        local_schema_hash = None\n        schemas = []\n\n        if schema_cache_path:\n            try:\n                schemas, local_schema_hash = self._read_schemas_from_cache(\n                    schema_cache_path\n                )\n            except (IOError, TypeError, AttributeError, ValueError):\n                # Catch any known exceptions when trying to read the local\n                # schema cache to prevent API from being unusable.\n                self.logger.exception(L(\n                    'Schema cache could not be loaded from {0!r}',\n                    schema_cache_path\n                ))\n\n        # Use `dictionary.get` to retrieve hash to support older version of\n        # ftrack server not returning a schema hash.\n        server_hash = self._server_information.get(\n            'schema_hash', False\n        )\n        if local_schema_hash != server_hash:\n            self.logger.debug(L(\n                'Loading schemas from server due to hash not matching.'\n                'Local: {0!r} != Server: {1!r}', local_schema_hash, server_hash\n            ))\n            schemas = self.call([{'action': 'query_schemas'}])[0]\n\n            if schema_cache_path:\n                try:\n                    self._write_schemas_to_cache(schemas, schema_cache_path)\n                except (IOError, TypeError):\n                    self.logger.exception(L(\n                        'Failed to update schema cache {0!r}.',\n                        schema_cache_path\n                    ))\n\n        else:\n            self.logger.debug(L(\n                'Using cached schemas from {0!r}', schema_cache_path\n            ))\n\n        return schemas\n\n    def _build_entity_type_classes(self, schemas):\n        '''Build default entity type classes.'''\n        fallback_factory = ftrack_api.entity.factory.StandardFactory()\n        classes = {}\n\n        for schema in schemas:\n            results = self.event_hub.publish(\n                ftrack_api.event.base.Event(\n                    topic='ftrack.api.session.construct-entity-type',\n                    data=dict(\n                        schema=schema,\n                        schemas=schemas\n                    )\n                ),\n                synchronous=True\n            )\n\n            results = [result for result in results if result is not None]\n\n            if not results:\n                self.logger.debug(L(\n                    'Using default StandardFactory to construct entity type '\n                    'class for \"{0}\"', schema['id']\n                ))\n                entity_type_class = fallback_factory.create(schema)\n\n            elif len(results) > 1:\n                raise ValueError(\n                    'Expected single entity type to represent schema \"{0}\" but '\n                    'received {1} entity types instead.'\n                    .format(schema['id'], len(results))\n                )\n\n            else:\n                entity_type_class = results[0]\n\n            classes[entity_type_class.entity_type] = entity_type_class\n\n        return classes\n\n    def _configure_locations(self):\n        '''Configure locations.'''\n        # First configure builtin locations, by injecting them into local cache.\n\n        # Origin.\n        location = self.create(\n            'Location',\n            data=dict(\n                name='ftrack.origin',\n                id=ftrack_api.symbol.ORIGIN_LOCATION_ID\n            ),\n            reconstructing=True\n        )\n        ftrack_api.mixin(\n            location, ftrack_api.entity.location.OriginLocationMixin,\n            name='OriginLocation'\n        )\n        location.accessor = ftrack_api.accessor.disk.DiskAccessor(prefix='')\n        location.structure = ftrack_api.structure.origin.OriginStructure()\n        location.priority = 100\n\n        # Unmanaged.\n        location = self.create(\n            'Location',\n            data=dict(\n                name='ftrack.unmanaged',\n                id=ftrack_api.symbol.UNMANAGED_LOCATION_ID\n            ),\n            reconstructing=True\n        )\n        ftrack_api.mixin(\n            location, ftrack_api.entity.location.UnmanagedLocationMixin,\n            name='UnmanagedLocation'\n        )\n        location.accessor = ftrack_api.accessor.disk.DiskAccessor(prefix='')\n        location.structure = ftrack_api.structure.origin.OriginStructure()\n        # location.resource_identifier_transformer = (\n        #     ftrack_api.resource_identifier_transformer.internal.InternalResourceIdentifierTransformer(session)\n        # )\n        location.priority = 90\n\n        # Review.\n        location = self.create(\n            'Location',\n            data=dict(\n                name='ftrack.review',\n                id=ftrack_api.symbol.REVIEW_LOCATION_ID\n            ),\n            reconstructing=True\n        )\n        ftrack_api.mixin(\n            location, ftrack_api.entity.location.UnmanagedLocationMixin,\n            name='UnmanagedLocation'\n        )\n        location.accessor = ftrack_api.accessor.disk.DiskAccessor(prefix='')\n        location.structure = ftrack_api.structure.origin.OriginStructure()\n        location.priority = 110\n\n        # Server.\n        location = self.create(\n            'Location',\n            data=dict(\n                name='ftrack.server',\n                id=ftrack_api.symbol.SERVER_LOCATION_ID\n            ),\n            reconstructing=True\n        )\n        ftrack_api.mixin(\n            location, ftrack_api.entity.location.ServerLocationMixin,\n            name='ServerLocation'\n        )\n        location.accessor = ftrack_api.accessor.server._ServerAccessor(\n            session=self\n        )\n        location.structure = ftrack_api.structure.entity_id.EntityIdStructure()\n        location.priority = 150\n\n        # Master location based on server scenario.\n        storage_scenario = self.server_information.get('storage_scenario')\n\n        if (\n            storage_scenario and\n            storage_scenario.get('scenario')\n        ):\n            self.event_hub.publish(\n                ftrack_api.event.base.Event(\n                    topic='ftrack.storage-scenario.activate',\n                    data=dict(\n                        storage_scenario=storage_scenario\n                    )\n                ),\n                synchronous=True\n            )\n\n        # Next, allow further configuration of locations via events.\n        self.event_hub.publish(\n            ftrack_api.event.base.Event(\n                topic='ftrack.api.session.configure-location',\n                data=dict(\n                    session=self\n                )\n            ),\n            synchronous=True\n        )\n\n    @ftrack_api.logging.deprecation_warning(\n        'Session._call is now available as public method Session.call. The '\n        'private method will be removed in version 2.0.'\n    )\n    def _call(self, data):\n        '''Make request to server with *data* batch describing the actions.\n\n        .. note::\n\n            This private method is now available as public method\n            :meth:`entity_reference`. This alias remains for backwards\n            compatibility, but will be removed in version 2.0.\n\n        '''\n        return self.call(data)\n\n    def call(self, data):\n        '''Make request to server with *data* batch describing the actions.'''\n        url = self._server_url + '/api'\n        headers = {\n            'content-type': 'application/json',\n            'accept': 'application/json'\n        }\n        data = self.encode(data, entity_attribute_strategy='modified_only')\n\n        self.logger.debug(L('Calling server {0} with {1!r}', url, data))\n\n        response = self._request.post(\n            url,\n            headers=headers,\n            data=data\n        )\n\n        self.logger.debug(L('Call took: {0}', response.elapsed.total_seconds()))\n\n        self.logger.debug(L('Response: {0!r}', response.text))\n        try:\n            result = self.decode(response.text)\n\n        except Exception:\n            error_message = (\n                'Server reported error in unexpected format. Raw error was: {0}'\n                .format(response.text)\n            )\n            self.logger.exception(error_message)\n            raise ftrack_api.exception.ServerError(error_message)\n\n        else:\n            if 'exception' in result:\n                # Handle exceptions.\n                error_message = 'Server reported error: {0}({1})'.format(\n                    result['exception'], result['content']\n                )\n                self.logger.exception(error_message)\n                raise ftrack_api.exception.ServerError(error_message)\n\n        return result\n\n    def encode(self, data, entity_attribute_strategy='set_only'):\n        '''Return *data* encoded as JSON formatted string.\n\n        *entity_attribute_strategy* specifies how entity attributes should be\n        handled. The following strategies are available:\n\n        * *all* - Encode all attributes, loading any that are currently NOT_SET.\n        * *set_only* - Encode only attributes that are currently set without\n          loading any from the remote.\n        * *modified_only* - Encode only attributes that have been modified\n          locally.\n        * *persisted_only* - Encode only remote (persisted) attribute values.\n\n        '''\n        entity_attribute_strategies = (\n            'all', 'set_only', 'modified_only', 'persisted_only'\n        )\n        if entity_attribute_strategy not in entity_attribute_strategies:\n            raise ValueError(\n                'Unsupported entity_attribute_strategy \"{0}\". Must be one of '\n                '{1}'.format(\n                    entity_attribute_strategy,\n                    ', '.join(entity_attribute_strategies)\n                )\n            )\n\n        return json.dumps(\n            data,\n            sort_keys=True,\n            default=functools.partial(\n                self._encode,\n                entity_attribute_strategy=entity_attribute_strategy\n            )\n        )\n\n    def _encode(self, item, entity_attribute_strategy='set_only'):\n        '''Return JSON encodable version of *item*.\n\n        *entity_attribute_strategy* specifies how entity attributes should be\n        handled. See :meth:`Session.encode` for available strategies.\n\n        '''\n        if isinstance(item, (arrow.Arrow, datetime.datetime, datetime.date)):\n            return {\n                '__type__': 'datetime',\n                'value': item.isoformat()\n            }\n\n        if isinstance(item, OperationPayload):\n            data = dict(item.items())\n            if \"entity_data\" in data:\n                for key, value in data[\"entity_data\"].items():\n                    if isinstance(value, ftrack_api.entity.base.Entity):\n                        data[\"entity_data\"][key] = self.entity_reference(value)\n\n            return data\n\n        if isinstance(item, ftrack_api.entity.base.Entity):\n            data = self.entity_reference(item)\n\n            with self.auto_populating(True):\n\n                for attribute in item.attributes:\n                    value = ftrack_api.symbol.NOT_SET\n\n                    if entity_attribute_strategy == 'all':\n                        value = attribute.get_value(item)\n\n                    elif entity_attribute_strategy == 'set_only':\n                        if attribute.is_set(item):\n                            value = attribute.get_local_value(item)\n                            if value is ftrack_api.symbol.NOT_SET:\n                                value = attribute.get_remote_value(item)\n\n                    elif entity_attribute_strategy == 'modified_only':\n                        if attribute.is_modified(item):\n                            value = attribute.get_local_value(item)\n\n                    elif entity_attribute_strategy == 'persisted_only':\n                        if not attribute.computed:\n                            value = attribute.get_remote_value(item)\n\n                    if value is not ftrack_api.symbol.NOT_SET:\n                        if isinstance(\n                            attribute, ftrack_api.attribute.ReferenceAttribute\n                        ):\n                            if isinstance(value, ftrack_api.entity.base.Entity):\n                                value = self.entity_reference(value)\n\n                        data[attribute.name] = value\n\n            return data\n\n        if isinstance(\n            item, ftrack_api.collection.MappedCollectionProxy\n        ):\n            # Use proxied collection for serialisation.\n            item = item.collection\n\n        if isinstance(item, ftrack_api.collection.Collection):\n            data = []\n            for entity in item:\n                data.append(self.entity_reference(entity))\n\n            return data\n\n        raise TypeError('{0!r} is not JSON serializable'.format(item))\n\n    def entity_reference(self, entity):\n        '''Return entity reference that uniquely identifies *entity*.\n\n        Return a mapping containing the __entity_type__ of the entity along with\n        the key, value pairs that make up it's primary key.\n\n        '''\n        reference = {\n            '__entity_type__': entity.entity_type\n        }\n        with self.auto_populating(False):\n            reference.update(ftrack_api.inspection.primary_key(entity))\n\n        return reference\n\n    @ftrack_api.logging.deprecation_warning(\n        'Session._entity_reference is now available as public method '\n        'Session.entity_reference. The private method will be removed '\n        'in version 2.0.'\n    )\n    def _entity_reference(self, entity):\n        '''Return entity reference that uniquely identifies *entity*.\n\n        Return a mapping containing the __entity_type__ of the entity along\n        with the key, value pairs that make up it's primary key.\n\n        .. note::\n\n            This private method is now available as public method\n            :meth:`entity_reference`. This alias remains for backwards\n            compatibility, but will be removed in version 2.0.\n\n        '''\n        return self.entity_reference(entity)\n\n    def decode(self, string):\n        '''Return decoded JSON *string* as Python object.'''\n        with self.operation_recording(False):\n            return json.loads(string, object_hook=self._decode)\n\n    def _decode(self, item):\n        '''Return *item* transformed into appropriate representation.'''\n        if isinstance(item, collections.Mapping):\n            if '__type__' in item:\n                if item['__type__'] == 'datetime':\n                    item = arrow.get(item['value'])\n\n            elif '__entity_type__' in item:\n                item = self._create(\n                    item['__entity_type__'], item, reconstructing=True\n                )\n\n        return item\n\n    def _get_locations(self, filter_inaccessible=True):\n        '''Helper to returns locations ordered by priority.\n\n        If *filter_inaccessible* is True then only accessible locations will be\n        included in result.\n\n        '''\n        # Optimise this call.\n        locations = self.query('Location')\n\n        # Filter.\n        if filter_inaccessible:\n            locations = filter(\n                lambda location: location.accessor,\n                locations\n            )\n\n        # Sort by priority.\n        locations = sorted(\n            locations, key=lambda location: location.priority\n        )\n\n        return locations\n\n    def pick_location(self, component=None):\n        '''Return suitable location to use.\n\n        If no *component* specified then return highest priority accessible\n        location. Otherwise, return highest priority accessible location that\n        *component* is available in.\n\n        Return None if no suitable location could be picked.\n\n        '''\n        if component:\n            return self.pick_locations([component])[0]\n\n        else:\n            locations = self._get_locations()\n            if locations:\n                return locations[0]\n            else:\n                return None\n\n    def pick_locations(self, components):\n        '''Return suitable locations for *components*.\n\n        Return list of locations corresponding to *components* where each\n        picked location is the highest priority accessible location for that\n        component. If a component has no location available then its\n        corresponding entry will be None.\n\n        '''\n        candidate_locations = self._get_locations()\n        availabilities = self.get_component_availabilities(\n            components, locations=candidate_locations\n        )\n\n        locations = []\n        for component, availability in zip(components, availabilities):\n            location = None\n\n            for candidate_location in candidate_locations:\n                if availability.get(candidate_location['id']) > 0.0:\n                    location = candidate_location\n                    break\n\n            locations.append(location)\n\n        return locations\n\n    def create_component(\n        self, path, data=None, location='auto'\n    ):\n        '''Create a new component from *path* with additional *data*\n\n        .. note::\n\n            This is a helper method. To create components manually use the\n            standard :meth:`Session.create` method.\n\n        *path* can be a string representing a filesystem path to the data to\n        use for the component. The *path* can also be specified as a sequence\n        string, in which case a sequence component with child components for\n        each item in the sequence will be created automatically. The accepted\n        format for a sequence is '{head}{padding}{tail} [{ranges}]'. For\n        example::\n\n            '/path/to/file.%04d.ext [1-5, 7, 8, 10-20]'\n\n        .. seealso::\n\n            `Clique documentation <http://clique.readthedocs.org>`_\n\n        *data* should be a dictionary of any additional data to construct the\n        component with (as passed to :meth:`Session.create`).\n\n        If *location* is specified then automatically add component to that\n        location. The default of 'auto' will automatically pick a suitable\n        location to add the component to if one is available. To not add to any\n        location specifiy locations as None.\n\n        .. note::\n\n            A :meth:`Session.commit<ftrack_api.session.Session.commit>` may be\n            automatically issued as part of the components registration in the\n            location.\n        '''\n        if data is None:\n            data = {}\n\n        if location == 'auto':\n            # Check if the component name matches one of the ftrackreview\n            # specific names. Add the component to the ftrack.review location if\n            # so. This is used to not break backwards compatibility.\n            if data.get('name') in (\n                'ftrackreview-mp4', 'ftrackreview-webm', 'ftrackreview-image'\n            ):\n                location = self.get(\n                    'Location', ftrack_api.symbol.REVIEW_LOCATION_ID\n                )\n\n            else:\n                location = self.pick_location()\n\n        try:\n            collection = clique.parse(path)\n\n        except ValueError:\n            # Assume is a single file.\n            if 'size' not in data:\n                data['size'] = self._get_filesystem_size(path)\n\n            data.setdefault('file_type', os.path.splitext(path)[-1])\n\n            return self._create_component(\n                'FileComponent', path, data, location\n            )\n\n        else:\n            # Calculate size of container and members.\n            member_sizes = {}\n            container_size = data.get('size')\n\n            if container_size is not None:\n                if len(collection.indexes) > 0:\n                    member_size = int(\n                        round(container_size / len(collection.indexes))\n                    )\n                    for item in collection:\n                        member_sizes[item] = member_size\n\n            else:\n                container_size = 0\n                for item in collection:\n                    member_sizes[item] = self._get_filesystem_size(item)\n                    container_size += member_sizes[item]\n\n            # Create sequence component\n            container_path = collection.format('{head}{padding}{tail}')\n            data.setdefault('padding', collection.padding)\n            data.setdefault('file_type', os.path.splitext(container_path)[-1])\n            data.setdefault('size', container_size)\n\n            container = self._create_component(\n                'SequenceComponent', container_path, data, location=None\n            )\n\n            # Create member components for sequence.\n            for member_path in collection:\n                member_data = {\n                    'name': collection.match(member_path).group('index'),\n                    'container': container,\n                    'size': member_sizes[member_path],\n                    'file_type': os.path.splitext(member_path)[-1]\n                }\n\n                component = self._create_component(\n                    'FileComponent', member_path, member_data, location=None\n                )\n                container['members'].append(component)\n\n            if location:\n                origin_location = self.get(\n                    'Location', ftrack_api.symbol.ORIGIN_LOCATION_ID\n                )\n                location.add_component(\n                    container, origin_location, recursive=True\n                )\n\n            return container\n\n    def _create_component(self, entity_type, path, data, location):\n        '''Create and return component.\n\n        See public function :py:func:`createComponent` for argument details.\n\n        '''\n        component = self.create(entity_type, data)\n\n        # Add to special origin location so that it is possible to add to other\n        # locations.\n        origin_location = self.get(\n            'Location', ftrack_api.symbol.ORIGIN_LOCATION_ID\n        )\n        origin_location.add_component(component, path, recursive=False)\n\n        if location:\n            location.add_component(component, origin_location, recursive=False)\n\n        return component\n\n    def _get_filesystem_size(self, path):\n        '''Return size from *path*'''\n        try:\n            size = os.path.getsize(path)\n        except OSError:\n            size = 0\n\n        return size\n\n    def get_component_availability(self, component, locations=None):\n        '''Return availability of *component*.\n\n        If *locations* is set then limit result to availability of *component*\n        in those *locations*.\n\n        Return a dictionary of {location_id:percentage_availability}\n\n        '''\n        return self.get_component_availabilities(\n            [component], locations=locations\n        )[0]\n\n    def get_component_availabilities(self, components, locations=None):\n        '''Return availabilities of *components*.\n\n        If *locations* is set then limit result to availabilities of\n        *components* in those *locations*.\n\n        Return a list of dictionaries of {location_id:percentage_availability}.\n        The list indexes correspond to those of *components*.\n\n        '''\n        availabilities = []\n\n        if locations is None:\n            locations = self.query('Location')\n\n        # Separate components into two lists, those that are containers and\n        # those that are not, so that queries can be optimised.\n        standard_components = []\n        container_components = []\n\n        for component in components:\n            if 'members' in component.keys():\n                container_components.append(component)\n            else:\n                standard_components.append(component)\n\n        # Perform queries.\n        if standard_components:\n            self.populate(\n                standard_components, 'component_locations.location_id'\n            )\n\n        if container_components:\n            self.populate(\n                container_components,\n                'members, component_locations.location_id'\n            )\n\n        base_availability = {}\n        for location in locations:\n            base_availability[location['id']] = 0.0\n\n        for component in components:\n            availability = base_availability.copy()\n            availabilities.append(availability)\n\n            is_container = 'members' in component.keys()\n            if is_container and len(component['members']):\n                member_availabilities = self.get_component_availabilities(\n                    component['members'], locations=locations\n                )\n                multiplier = 1.0 / len(component['members'])\n                for member, member_availability in zip(\n                    component['members'], member_availabilities\n                ):\n                    for location_id, ratio in member_availability.items():\n                        availability[location_id] += (\n                            ratio * multiplier\n                        )\n            else:\n                for component_location in component['component_locations']:\n                    location_id = component_location['location_id']\n                    if location_id in availability:\n                        availability[location_id] = 100.0\n\n            for location_id, percentage in availability.items():\n                # Avoid quantization error by rounding percentage and clamping\n                # to range 0-100.\n                adjusted_percentage = round(percentage, 9)\n                adjusted_percentage = max(0.0, min(adjusted_percentage, 100.0))\n                availability[location_id] = adjusted_percentage\n\n        return availabilities\n\n    @ftrack_api.logging.deprecation_warning(\n        'Session.delayed_job has been deprecated in favour of session.call. '\n        'Please refer to the release notes for more information.'\n    )\n    def delayed_job(self, job_type):\n        '''Execute a delayed job on the server, a `ftrack.entity.job.Job` is returned.\n\n        *job_type* should be one of the allowed job types. There is currently\n        only one remote job type \"SYNC_USERS_LDAP\".\n        '''\n        if job_type not in (ftrack_api.symbol.JOB_SYNC_USERS_LDAP, ):\n            raise ValueError(\n                u'Invalid Job type: {0}.'.format(job_type)\n            )\n\n        operation = {\n            'action': 'delayed_job',\n            'job_type': job_type.name\n        }\n\n        try:\n            result = self.call(\n                [operation]\n            )[0]\n\n        except ftrack_api.exception.ServerError as error:\n            raise\n\n        return result['data']\n\n    def get_widget_url(self, name, entity=None, theme=None):\n        '''Return an authenticated URL for widget with *name* and given options.\n\n        The returned URL will be authenticated using a token which will expire\n        after 6 minutes.\n\n        *name* should be the name of the widget to return and should be one of\n        'info', 'tasks' or 'tasks_browser'.\n\n        Certain widgets require an entity to be specified. If so, specify it by\n        setting *entity* to a valid entity instance.\n\n        *theme* sets the theme of the widget and can be either 'light' or 'dark'\n        (defaulting to 'dark' if an invalid option given).\n\n        '''\n        operation = {\n            'action': 'get_widget_url',\n            'name': name,\n            'theme': theme\n        }\n        if entity:\n            operation['entity_type'] = entity.entity_type\n            operation['entity_key'] = (\n                ftrack_api.inspection.primary_key(entity).values()\n            )\n\n        try:\n            result = self.call([operation])\n\n        except ftrack_api.exception.ServerError as error:\n            # Raise informative error if the action is not supported.\n            if 'Invalid action u\\'get_widget_url\\'' in error.message:\n                raise ftrack_api.exception.ServerCompatibilityError(\n                    'Server version {0!r} does not support \"get_widget_url\", '\n                    'please update server and try again.'.format(\n                        self.server_information.get('version')\n                    )\n                )\n            else:\n                raise\n\n        else:\n            return result[0]['widget_url']\n\n    def encode_media(self, media, version_id=None, keep_original='auto'):\n        '''Return a new Job that encode *media* to make it playable in browsers.\n\n        *media* can be a path to a file or a FileComponent in the ftrack.server\n        location.\n\n        The job will encode *media* based on the file type and job data contains\n        information about encoding in the following format::\n\n            {\n                'output': [{\n                    'format': 'video/mp4',\n                    'component_id': 'e2dc0524-b576-11d3-9612-080027331d74'\n                }, {\n                    'format': 'image/jpeg',\n                    'component_id': '07b82a97-8cf9-11e3-9383-20c9d081909b'\n                }],\n                'source_component_id': 'e3791a09-7e11-4792-a398-3d9d4eefc294',\n                'keep_original': True\n            }\n\n        The output components are associated with the job via the job_components\n        relation.\n\n        An image component will always be generated if possible that can be used\n        as a thumbnail.\n\n        If *media* is a file path, a new source component will be created and\n        added to the ftrack server location and a call to :meth:`commit` will be\n        issued. If *media* is a FileComponent, it will be assumed to be in\n        available in the ftrack.server location.\n\n        If *version_id* is specified, the new components will automatically be\n        associated with the AssetVersion. Otherwise, the components will not\n        be associated to a version even if the supplied *media* belongs to one.\n        A server version of 3.3.32 or higher is required for the version_id\n        argument to function properly.\n\n        If *keep_original* is not set, the original media will be kept if it\n        is a FileComponent, and deleted if it is a file path. You can specify\n        True or False to change this behavior.\n        '''\n        if isinstance(media, basestring):\n            # Media is a path to a file.\n            server_location = self.get(\n                'Location', ftrack_api.symbol.SERVER_LOCATION_ID\n            )\n            if keep_original == 'auto':\n                keep_original = False\n\n            component_data = None\n            if keep_original:\n                component_data = dict(version_id=version_id)\n\n            component = self.create_component(\n                path=media,\n                data=component_data,\n                location=server_location\n            )\n\n            # Auto commit to ensure component exists when sent to server.\n            self.commit()\n\n        elif (\n            hasattr(media, 'entity_type') and\n            media.entity_type in ('FileComponent',)\n        ):\n            # Existing file component.\n            component = media\n            if keep_original == 'auto':\n                keep_original = True\n\n        else:\n            raise ValueError(\n                'Unable to encode media of type: {0}'.format(type(media))\n            )\n\n        operation = {\n            'action': 'encode_media',\n            'component_id': component['id'],\n            'version_id': version_id,\n            'keep_original': keep_original\n        }\n\n        try:\n            result = self.call([operation])\n\n        except ftrack_api.exception.ServerError as error:\n            # Raise informative error if the action is not supported.\n            if 'Invalid action u\\'encode_media\\'' in error.message:\n                raise ftrack_api.exception.ServerCompatibilityError(\n                    'Server version {0!r} does not support \"encode_media\", '\n                    'please update server and try again.'.format(\n                        self.server_information.get('version')\n                    )\n                )\n            else:\n                raise\n\n        return self.get('Job', result[0]['job_id'])\n\n    def get_upload_metadata(\n        self, component_id, file_name, file_size, checksum=None\n    ):\n        '''Return URL and headers used to upload data for *component_id*.\n\n        *file_name* and *file_size* should match the components details.\n\n        The returned URL should be requested using HTTP PUT with the specified\n        headers.\n\n        The *checksum* is used as the Content-MD5 header and should contain\n        the base64-encoded 128-bit MD5 digest of the message (without the\n        headers) according to RFC 1864. This can be used as a message integrity\n        check to verify that the data is the same data that was originally sent.\n        '''\n        operation = {\n            'action': 'get_upload_metadata',\n            'component_id': component_id,\n            'file_name': file_name,\n            'file_size': file_size,\n            'checksum': checksum\n        }\n\n        try:\n            result = self.call([operation])\n\n        except ftrack_api.exception.ServerError as error:\n            # Raise informative error if the action is not supported.\n            if 'Invalid action u\\'get_upload_metadata\\'' in error.message:\n                raise ftrack_api.exception.ServerCompatibilityError(\n                    'Server version {0!r} does not support '\n                    '\"get_upload_metadata\", please update server and try '\n                    'again.'.format(\n                        self.server_information.get('version')\n                    )\n                )\n            else:\n                raise\n\n        return result[0]\n\n    def send_user_invite(self, user):\n        '''Send a invitation to the provided *user*.\n\n        *user* is a User instance\n\n        '''\n\n        self.send_user_invites(\n            [user]\n        )\n\n    def send_user_invites(self, users):\n        '''Send a invitation to the provided *user*.\n\n        *users* is a list of User instances\n\n        '''\n\n        operations = []\n\n        for user in users:\n            operations.append(\n                {\n                    'action':'send_user_invite',\n                    'user_id': user['id']\n                }\n            )\n\n        try:\n            self.call(operations)\n\n        except ftrack_api.exception.ServerError as error:\n            # Raise informative error if the action is not supported.\n            if 'Invalid action u\\'send_user_invite\\'' in error.message:\n                raise ftrack_api.exception.ServerCompatibilityError(\n                    'Server version {0!r} does not support '\n                    '\"send_user_invite\", please update server and '\n                    'try again.'.format(\n                        self.server_information.get('version')\n                    )\n                )\n            else:\n                raise\n\n    def send_review_session_invite(self, invitee):\n        '''Send an invite to a review session to *invitee*.\n\n        *invitee* is a instance of ReviewSessionInvitee.\n\n        .. note::\n\n            The *invitee* must be committed.\n\n        '''\n        self.send_review_session_invites([invitee])\n\n    def send_review_session_invites(self, invitees):\n        '''Send an invite to a review session to a list of *invitees*.\n\n        *invitee* is a list of ReviewSessionInvitee objects.\n\n        .. note::\n\n            All *invitees* must be committed.\n\n        '''\n        operations = []\n\n        for invitee in invitees:\n            operations.append(\n                {\n                    'action': 'send_review_session_invite',\n                    'review_session_invitee_id': invitee['id']\n                }\n            )\n\n        try:\n            self.call(operations)\n        except ftrack_api.exception.ServerError as error:\n            # Raise informative error if the action is not supported.\n            if 'Invalid action u\\'send_review_session_invite\\'' in error.message:\n                raise ftrack_api.exception.ServerCompatibilityError(\n                    'Server version {0!r} does not support '\n                    '\"send_review_session_invite\", please update server and '\n                    'try again.'.format(\n                        self.server_information.get('version')\n                    )\n                )\n            else:\n                raise\n\n\nclass AutoPopulatingContext(object):\n    '''Context manager for temporary change of session auto_populate value.'''\n\n    def __init__(self, session, auto_populate):\n        '''Initialise context.'''\n        super(AutoPopulatingContext, self).__init__()\n        self._session = session\n        self._auto_populate = auto_populate\n        self._current_auto_populate = None\n\n    def __enter__(self):\n        '''Enter context switching to desired auto populate setting.'''\n        self._current_auto_populate = self._session.auto_populate\n        self._session.auto_populate = self._auto_populate\n\n    def __exit__(self, exception_type, exception_value, traceback):\n        '''Exit context resetting auto populate to original setting.'''\n        self._session.auto_populate = self._current_auto_populate\n\n\nclass OperationRecordingContext(object):\n    '''Context manager for temporary change of session record_operations.'''\n\n    def __init__(self, session, record_operations):\n        '''Initialise context.'''\n        super(OperationRecordingContext, self).__init__()\n        self._session = session\n        self._record_operations = record_operations\n        self._current_record_operations = None\n\n    def __enter__(self):\n        '''Enter context.'''\n        self._current_record_operations = self._session.record_operations\n        self._session.record_operations = self._record_operations\n\n    def __exit__(self, exception_type, exception_value, traceback):\n        '''Exit context.'''\n        self._session.record_operations = self._current_record_operations\n\n\nclass OperationPayload(collections.MutableMapping):\n    '''Represent operation payload.'''\n\n    def __init__(self, *args, **kwargs):\n        '''Initialise payload.'''\n        super(OperationPayload, self).__init__()\n        self._data = dict()\n        self.update(dict(*args, **kwargs))\n\n    def __str__(self):\n        '''Return string representation.'''\n        return '<{0} {1}>'.format(\n            self.__class__.__name__, str(self._data)\n        )\n\n    def __getitem__(self, key):\n        '''Return value for *key*.'''\n        return self._data[key]\n\n    def __setitem__(self, key, value):\n        '''Set *value* for *key*.'''\n        self._data[key] = value\n\n    def __delitem__(self, key):\n        '''Remove *key*.'''\n        del self._data[key]\n\n    def __iter__(self):\n        '''Iterate over all keys.'''\n        return iter(self._data)\n\n    def __len__(self):\n        '''Return count of keys.'''\n        return len(self._data)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/structure/__init__.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/structure/base.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nfrom abc import ABCMeta, abstractmethod\n\n\nclass Structure(object):\n    '''Structure plugin interface.\n\n    A structure plugin should compute appropriate paths for data.\n\n    '''\n\n    __metaclass__ = ABCMeta\n\n    def __init__(self, prefix=''):\n        '''Initialise structure.'''\n        self.prefix = prefix\n        self.path_separator = '/'\n        super(Structure, self).__init__()\n\n    @abstractmethod\n    def get_resource_identifier(self, entity, context=None):\n        '''Return a resource identifier for supplied *entity*.\n\n        *context* can be a mapping that supplies additional information.\n\n        '''\n\n    def _get_sequence_expression(self, sequence):\n        '''Return a sequence expression for *sequence* component.'''\n        padding = sequence['padding']\n        if padding:\n            expression = '%0{0}d'.format(padding)\n        else:\n            expression = '%d'\n\n        return expression\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/structure/entity_id.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport ftrack_api.structure.base\n\n\nclass EntityIdStructure(ftrack_api.structure.base.Structure):\n    '''Entity id pass-through structure.'''\n\n    def get_resource_identifier(self, entity, context=None):\n        '''Return a *resourceIdentifier* for supplied *entity*.'''\n        return entity['id']\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/structure/id.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport os\n\nimport ftrack_api.symbol\nimport ftrack_api.structure.base\n\n\nclass IdStructure(ftrack_api.structure.base.Structure):\n    '''Id based structure supporting Components only.\n\n    A components unique id will be used to form a path to store the data at.\n    To avoid millions of entries in one directory each id is chunked into four\n    prefix directories with the remainder used to name the file::\n\n        /prefix/1/2/3/4/56789\n\n    If the component has a defined filetype it will be added to the path::\n\n        /prefix/1/2/3/4/56789.exr\n\n    Components that are children of container components will be placed inside\n    the id structure of their parent::\n\n        /prefix/1/2/3/4/56789/355827648d.exr\n        /prefix/1/2/3/4/56789/ajf24215b5.exr\n\n    However, sequence children will be named using their label as an index and\n    a common prefix of 'file.'::\n\n        /prefix/1/2/3/4/56789/file.0001.exr\n        /prefix/1/2/3/4/56789/file.0002.exr\n\n    '''\n\n    def get_resource_identifier(self, entity, context=None):\n        '''Return a resource identifier for supplied *entity*.\n\n        *context* can be a mapping that supplies additional information.\n\n        '''\n        if entity.entity_type in ('FileComponent',):\n            # When in a container, place the file inside a directory named\n            # after the container.\n            container = entity['container']\n            if container and container is not ftrack_api.symbol.NOT_SET:\n                path = self.get_resource_identifier(container)\n\n                if container.entity_type in ('SequenceComponent',):\n                    # Label doubles as index for now.\n                    name = 'file.{0}{1}'.format(\n                        entity['name'], entity['file_type']\n                    )\n                    parts = [os.path.dirname(path), name]\n\n                else:\n                    # Just place uniquely identified file into directory\n                    name = entity['id'] + entity['file_type']\n                    parts = [path, name]\n\n            else:\n                name = entity['id'][4:] + entity['file_type']\n                parts = ([self.prefix] + list(entity['id'][:4]) + [name])\n\n        elif entity.entity_type in ('SequenceComponent',):\n            name = 'file'\n\n            # Add a sequence identifier.\n            sequence_expression = self._get_sequence_expression(entity)\n            name += '.{0}'.format(sequence_expression)\n\n            if (\n                entity['file_type'] and\n                entity['file_type'] is not ftrack_api.symbol.NOT_SET\n            ):\n                name += entity['file_type']\n\n            parts = ([self.prefix] + list(entity['id'][:4])\n                     + [entity['id'][4:]] + [name])\n\n        elif entity.entity_type in ('ContainerComponent',):\n            # Just an id directory\n            parts = ([self.prefix] +\n                     list(entity['id'][:4]) + [entity['id'][4:]])\n\n        else:\n            raise NotImplementedError('Cannot generate path for unsupported '\n                                      'entity {0}'.format(entity))\n\n        return self.path_separator.join(parts).strip('/')\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/structure/origin.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nfrom .base import Structure\n\n\nclass OriginStructure(Structure):\n    '''Origin structure that passes through existing resource identifier.'''\n\n    def get_resource_identifier(self, entity, context=None):\n        '''Return a resource identifier for supplied *entity*.\n\n        *context* should be a mapping that includes at least a\n        'source_resource_identifier' key that refers to the resource identifier\n        to pass through.\n\n        '''\n        if context is None:\n            context = {}\n\n        resource_identifier = context.get('source_resource_identifier')\n        if resource_identifier is None:\n            raise ValueError(\n                'Could not generate resource identifier as no source resource '\n                'identifier found in passed context.'\n            )\n\n        return resource_identifier\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/structure/standard.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport os\nimport re\nimport unicodedata\n\nimport ftrack_api.symbol\nimport ftrack_api.structure.base\n\n\nclass StandardStructure(ftrack_api.structure.base.Structure):\n    '''Project hierarchy based structure that only supports Components.\n\n    The resource identifier is generated from the project code, the name\n    of objects in the project structure, asset name and version number::\n\n        my_project/folder_a/folder_b/asset_name/v003\n\n    If the component is a `FileComponent` then the name of the component and the\n    file type are used as filename in the resource_identifier::\n\n        my_project/folder_a/folder_b/asset_name/v003/foo.jpg\n\n    If the component is a `SequenceComponent` then a sequence expression,\n    `%04d`, is used. E.g. a component with the name `foo` yields::\n\n        my_project/folder_a/folder_b/asset_name/v003/foo.%04d.jpg\n\n    For the member components their index in the sequence is used::\n\n        my_project/folder_a/folder_b/asset_name/v003/foo.0042.jpg\n\n    The name of the component is added to the resource identifier if the\n    component is a `ContainerComponent`. E.g. a container component with the\n    name `bar` yields::\n\n        my_project/folder_a/folder_b/asset_name/v003/bar\n\n    For a member of that container the file name is based on the component name\n    and file type::\n\n        my_project/folder_a/folder_b/asset_name/v003/bar/baz.pdf\n\n    '''\n\n    def __init__(\n        self, project_versions_prefix=None, illegal_character_substitute='_'\n    ):\n        '''Initialise structure.\n\n        If *project_versions_prefix* is defined, insert after the project code\n        for versions published directly under the project::\n\n            my_project/<project_versions_prefix>/v001/foo.jpg\n\n        Replace illegal characters with *illegal_character_substitute* if\n        defined.\n\n        .. note::\n\n            Nested component containers/sequences are not supported.\n\n        '''\n        super(StandardStructure, self).__init__()\n        self.project_versions_prefix = project_versions_prefix\n        self.illegal_character_substitute = illegal_character_substitute\n\n    def _get_parts(self, entity):\n        '''Return resource identifier parts from *entity*.'''\n        session = entity.session\n\n        version = entity['version']\n\n        if version is ftrack_api.symbol.NOT_SET and entity['version_id']:\n            version = session.get('AssetVersion', entity['version_id'])\n\n        error_message = (\n            'Component {0!r} must be attached to a committed '\n            'version and a committed asset with a parent context.'.format(\n                entity\n            )\n        )\n\n        if (\n            version is ftrack_api.symbol.NOT_SET or\n            version in session.created\n        ):\n            raise ftrack_api.exception.StructureError(error_message)\n\n        link = version['link']\n\n        if not link:\n            raise ftrack_api.exception.StructureError(error_message)\n\n        structure_names = [\n            item['name']\n            for item in link[1:-1]\n        ]\n\n        project_id = link[0]['id']\n        project = session.get('Project', project_id)\n        asset = version['asset']\n\n        version_number = self._format_version(version['version'])\n\n        parts = []\n        parts.append(project['name'])\n\n        if structure_names:\n            parts.extend(structure_names)\n        elif self.project_versions_prefix:\n            # Add *project_versions_prefix* if configured and the version is\n            # published directly under the project.\n            parts.append(self.project_versions_prefix)\n\n        parts.append(asset['name'])\n        parts.append(version_number)\n\n        return [self.sanitise_for_filesystem(part) for part in parts]\n\n    def _format_version(self, number):\n        '''Return a formatted string representing version *number*.'''\n        return 'v{0:03d}'.format(number)\n\n    def sanitise_for_filesystem(self, value):\n        '''Return *value* with illegal filesystem characters replaced.\n\n        An illegal character is one that is not typically valid for filesystem\n        usage, such as non ascii characters, or can be awkward to use in a\n        filesystem, such as spaces. Replace these characters with\n        the character specified by *illegal_character_substitute* on\n        initialisation. If no character was specified as substitute then return\n        *value* unmodified.\n\n        '''\n        if self.illegal_character_substitute is None:\n            return value\n\n        if isinstance(value, str):\n            value = value.decode('utf-8')\n\n        value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')\n        value = re.sub('[^\\w\\.-]', self.illegal_character_substitute, value)\n        return unicode(value.strip().lower())\n\n    def get_resource_identifier(self, entity, context=None):\n        '''Return a resource identifier for supplied *entity*.\n\n        *context* can be a mapping that supplies additional information, but\n        is unused in this implementation.\n\n\n        Raise a :py:exc:`ftrack_api.exeption.StructureError` if *entity* is not\n        attached to a committed version and a committed asset with a parent\n        context.\n\n        '''\n        if entity.entity_type in ('FileComponent',):\n            container = entity['container']\n\n            if container:\n                # Get resource identifier for container.\n                container_path = self.get_resource_identifier(container)\n\n                if container.entity_type in ('SequenceComponent',):\n                    # Strip the sequence component expression from the parent\n                    # container and back the correct filename, i.e.\n                    # /sequence/component/sequence_component_name.0012.exr.\n                    name = '{0}.{1}{2}'.format(\n                        container['name'], entity['name'], entity['file_type']\n                    )\n                    parts = [\n                        os.path.dirname(container_path),\n                        self.sanitise_for_filesystem(name)\n                    ]\n\n                else:\n                    # Container is not a sequence component so add it as a\n                    # normal component inside the container.\n                    name = entity['name'] + entity['file_type']\n                    parts = [\n                        container_path, self.sanitise_for_filesystem(name)\n                    ]\n\n            else:\n                # File component does not have a container, construct name from\n                # component name and file type.\n                parts = self._get_parts(entity)\n                name = entity['name'] + entity['file_type']\n                parts.append(self.sanitise_for_filesystem(name))\n\n        elif entity.entity_type in ('SequenceComponent',):\n            # Create sequence expression for the sequence component and add it\n            # to the parts.\n            parts = self._get_parts(entity)\n            sequence_expression = self._get_sequence_expression(entity)\n            parts.append(\n                '{0}.{1}{2}'.format(\n                    self.sanitise_for_filesystem(entity['name']),\n                    sequence_expression,\n                    self.sanitise_for_filesystem(entity['file_type'])\n                )\n            )\n\n        elif entity.entity_type in ('ContainerComponent',):\n            # Add the name of the container to the resource identifier parts.\n            parts = self._get_parts(entity)\n            parts.append(self.sanitise_for_filesystem(entity['name']))\n\n        else:\n            raise NotImplementedError(\n                'Cannot generate resource identifier for unsupported '\n                'entity {0!r}'.format(entity)\n            )\n\n        return self.path_separator.join(parts)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/source/ftrack_api/symbol.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport os\n\n\nclass Symbol(object):\n    '''A constant symbol.'''\n\n    def __init__(self, name, value=True):\n        '''Initialise symbol with unique *name* and *value*.\n\n        *value* is used for nonzero testing.\n\n        '''\n        self.name = name\n        self.value = value\n\n    def __str__(self):\n        '''Return string representation.'''\n        return self.name\n\n    def __repr__(self):\n        '''Return representation.'''\n        return '{0}({1})'.format(self.__class__.__name__, self.name)\n\n    def __nonzero__(self):\n        '''Return whether symbol represents non-zero value.'''\n        return bool(self.value)\n\n    def __copy__(self):\n        '''Return shallow copy.\n\n        Overridden to always return same instance.\n\n        '''\n        return self\n\n\n#: Symbol representing that no value has been set or loaded.\nNOT_SET = Symbol('NOT_SET', False)\n\n#: Symbol representing created state.\nCREATED = Symbol('CREATED')\n\n#: Symbol representing modified state.\nMODIFIED = Symbol('MODIFIED')\n\n#: Symbol representing deleted state.\nDELETED = Symbol('DELETED')\n\n#: Topic published when component added to a location.\nCOMPONENT_ADDED_TO_LOCATION_TOPIC = 'ftrack.location.component-added'\n\n#: Topic published when component removed from a location.\nCOMPONENT_REMOVED_FROM_LOCATION_TOPIC = 'ftrack.location.component-removed'\n\n#: Identifier of builtin origin location.\nORIGIN_LOCATION_ID = 'ce9b348f-8809-11e3-821c-20c9d081909b'\n\n#: Identifier of builtin unmanaged location.\nUNMANAGED_LOCATION_ID = 'cb268ecc-8809-11e3-a7e2-20c9d081909b'\n\n#: Identifier of builtin review location.\nREVIEW_LOCATION_ID = 'cd41be70-8809-11e3-b98a-20c9d081909b'\n\n#: Identifier of builtin connect location.\nCONNECT_LOCATION_ID = '07b82a97-8cf9-11e3-9383-20c9d081909b'\n\n#: Identifier of builtin server location.\nSERVER_LOCATION_ID = '3a372bde-05bc-11e4-8908-20c9d081909b'\n\n#: Chunk size used when working with data, default to 1Mb.\nCHUNK_SIZE = int(os.getenv('FTRACK_API_FILE_CHUNK_SIZE', 0)) or 1024*1024\n\n#: Symbol representing syncing users with ldap\nJOB_SYNC_USERS_LDAP = Symbol('SYNC_USERS_LDAP')\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/fixture/plugin/configure_locations.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport logging\n\nimport ftrack_api\nimport ftrack_api.entity.location\nimport ftrack_api.accessor.disk\n\n\ndef configure_locations(event):\n    '''Configure locations for session.'''\n    session = event['data']['session']\n\n    # Find location(s) and customise instances.\n    location = session.ensure('Location', {'name': 'test.location'})\n    ftrack_api.mixin(\n        location, ftrack_api.entity.location.UnmanagedLocationMixin\n    )\n    location.accessor = ftrack_api.accessor.disk.DiskAccessor(prefix='')\n\n\ndef register(session):\n    '''Register plugin with *session*.'''\n    logger = logging.getLogger('ftrack_plugin:configure_locations.register')\n\n    # Validate that session is an instance of ftrack_api.Session. If not, assume\n    # that register is being called from an old or incompatible API and return\n    # without doing anything.\n    if not isinstance(session, ftrack_api.Session):\n        logger.debug(\n            'Not subscribing plugin as passed argument {0} is not an '\n            'ftrack_api.Session instance.'.format(session)\n        )\n        return\n\n    session.event_hub.subscribe(\n        'topic=ftrack.api.session.configure-location',\n        configure_locations\n    )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/fixture/plugin/construct_entity_type.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport logging\n\nimport ftrack_api.entity.factory\n\n\ndef stub(self):\n    '''A stub method for testing only.'''\n\n\nclass Factory(ftrack_api.entity.factory.StandardFactory):\n    '''Entity class factory.'''\n\n    def create(self, schema, bases=None):\n        '''Create and return entity class from *schema*.'''\n        # Optionally change bases for class to be generated.\n        cls = super(Factory, self).create(schema, bases=bases)\n\n        # Further customise cls before returning.\n        if schema['id'] == 'User':\n            cls.stub = stub\n\n        return cls\n\n\ndef register(session):\n    '''Register plugin with *session*.'''\n    logger = logging.getLogger('ftrack_plugin:construct_entity_type.register')\n\n    # Validate that session is an instance of ftrack_api.Session. If not, assume\n    # that register is being called from an old or incompatible API and return\n    # without doing anything.\n    if not isinstance(session, ftrack_api.Session):\n        logger.debug(\n            'Not subscribing plugin as passed argument {0!r} is not an '\n            'ftrack_api.Session instance.'.format(session)\n        )\n        return\n\n    factory = Factory()\n\n    def construct_entity_type(event):\n        '''Return class to represent entity type specified by *event*.'''\n        schema = event['data']['schema']\n        return factory.create(schema)\n\n    session.event_hub.subscribe(\n        'topic=ftrack.api.session.construct-entity-type',\n        construct_entity_type\n    )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/fixture/plugin/count_session_event.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2018 ftrack\nimport logging\nimport collections\n\nimport ftrack_api.session\n\n\ndef count_session_event(event):\n    '''Called when session is ready to be used.'''\n    logger = logging.getLogger('com.ftrack.test-session-event-plugin')\n    event_topic = event['topic']\n    logger.debug(u'Event received: {}'.format(event_topic))\n    session = event['data']['session']\n    session._test_called_events[event_topic] += 1\n\n\ndef register(session, **kw):\n    '''Register plugin. Called when used as an plugin.'''\n    logger = logging.getLogger('com.ftrack.test-session-event-plugin')\n\n    # Validate that session is an instance of ftrack_api.Session. If not,\n    # assume that register is being called from an old or incompatible API and\n    # return without doing anything.\n    if not isinstance(session, ftrack_api.session.Session):\n        logger.debug(\n            'Not subscribing plugin as passed argument {0!r} is not an '\n            'ftrack_api.Session instance.'.format(session)\n        )\n        return\n\n    session._test_called_events = collections.defaultdict(int)\n    session.event_hub.subscribe(\n        'topic=ftrack.api.session.ready',\n        count_session_event\n    )\n    session.event_hub.subscribe(\n        'topic=ftrack.api.session.reset',\n        count_session_event\n    )\n    logger.debug('Plugin registered')\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/__init__.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/accessor/__init__.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/accessor/test_disk.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport os\nimport tempfile\n\nimport pytest\n\nimport ftrack_api\nimport ftrack_api.exception\nimport ftrack_api.accessor.disk\nimport ftrack_api.data\n\n\ndef test_get_filesystem_path(temporary_path):\n    '''Convert paths to filesystem paths.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n\n    # Absolute paths outside of configured prefix fail.\n    with pytest.raises(ftrack_api.exception.AccessorFilesystemPathError):\n        accessor.get_filesystem_path(os.path.join('/', 'test', 'foo.txt'))\n\n    # Absolute root path.\n    assert accessor.get_filesystem_path(temporary_path) == temporary_path\n\n    # Absolute path within prefix.\n    assert (\n        accessor.get_filesystem_path(\n            os.path.join(temporary_path, 'test.txt')\n        ) ==\n        os.path.join(temporary_path, 'test.txt')\n    )\n\n    # Relative root path\n    assert accessor.get_filesystem_path('') == temporary_path\n\n    # Relative path for file at root\n    assert (accessor.get_filesystem_path('test.txt') ==\n            os.path.join(temporary_path, 'test.txt'))\n\n    # Relative path for file in subdirectory\n    assert (accessor.get_filesystem_path('test/foo.txt') ==\n            os.path.join(temporary_path, 'test', 'foo.txt'))\n\n    # Relative path non-collapsed\n    assert (accessor.get_filesystem_path('test/../foo.txt') ==\n            os.path.join(temporary_path, 'foo.txt'))\n\n    # Relative directory path without trailing slash\n    assert (accessor.get_filesystem_path('test') ==\n            os.path.join(temporary_path, 'test'))\n\n    # Relative directory path with trailing slash\n    assert (accessor.get_filesystem_path('test/') ==\n            os.path.join(temporary_path, 'test'))\n\n\ndef test_list(temporary_path):\n    '''List entries.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n\n    # File in root directory\n    assert accessor.list('') == []\n    data = accessor.open('test.txt', 'w+')\n    data.close()\n    assert accessor.list('') == ['test.txt']\n\n    # File in subdirectory\n    accessor.make_container('test_dir')\n    assert accessor.list('test_dir') == []\n    data = accessor.open('test_dir/test.txt', 'w+')\n    data.close()\n\n    listing = accessor.list('test_dir')\n    assert listing == [os.path.join('test_dir', 'test.txt')]\n\n    # Is a valid resource\n    assert accessor.exists(listing[0]) is True\n\n\ndef test_exists(temporary_path):\n    '''Valid path exists.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n\n    _, temporary_file = tempfile.mkstemp(dir=temporary_path)\n    assert accessor.exists(temporary_file) is True\n\n\ndef test_missing_does_not_exist(temporary_path):\n    '''Missing path does not exist.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n    assert accessor.exists('non-existant.txt') is False\n\n\ndef test_is_file(temporary_path):\n    '''Valid file is considered a file.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n\n    _, temporary_file = tempfile.mkstemp(dir=temporary_path)\n    assert accessor.is_file(temporary_file) is True\n\n\ndef test_missing_is_not_file(temporary_path):\n    '''Missing path is not considered a file.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n    assert accessor.is_file('non_existant.txt') is False\n\n\ndef test_container_is_not_file(temporary_path):\n    '''Valid container is not considered a file.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n\n    temporary_directory = tempfile.mkdtemp(dir=temporary_path)\n    assert accessor.is_file(temporary_directory) is False\n\n\ndef test_is_container(temporary_path):\n    '''Valid container is considered a container.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n\n    temporary_directory = tempfile.mkdtemp(dir=temporary_path)\n    assert accessor.is_container(temporary_directory) is True\n\n\ndef test_missing_is_not_container(temporary_path):\n    '''Missing path is not considered a container.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n    assert accessor.is_container('non_existant') is False\n\n\ndef test_file_is_not_container(temporary_path):\n    '''Valid file is not considered a container.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n\n    _, temporary_file = tempfile.mkstemp(dir=temporary_path)\n    assert accessor.is_container(temporary_file) is False\n\n\ndef test_is_sequence(temporary_path):\n    '''Sequence detection unsupported.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n\n    with pytest.raises(\n        ftrack_api.exception.AccessorUnsupportedOperationError\n    ):\n        accessor.is_sequence('foo.%04d.exr')\n\n\ndef test_open(temporary_path):\n    '''Open file.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n\n    with pytest.raises(ftrack_api.exception.AccessorResourceNotFoundError):\n        accessor.open('test.txt', 'r')\n\n    data = accessor.open('test.txt', 'w+')\n    assert isinstance(data, ftrack_api.data.Data) is True\n    assert data.read() == ''\n    data.write('test data')\n    data.close()\n\n    data = accessor.open('test.txt', 'r')\n    assert (data.read() == 'test data')\n    data.close()\n\n\ndef test_remove_file(temporary_path):\n    '''Delete file at path.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n\n    file_handle, temporary_file = tempfile.mkstemp(dir=temporary_path)\n    os.close(file_handle)\n    accessor.remove(temporary_file)\n    assert os.path.exists(temporary_file) is False\n\n\ndef test_remove_container(temporary_path):\n    '''Delete container at path.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n\n    temporary_directory = tempfile.mkdtemp(dir=temporary_path)\n    accessor.remove(temporary_directory)\n    assert os.path.exists(temporary_directory) is False\n\n\ndef test_remove_missing(temporary_path):\n    '''Fail to remove path that does not exist.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n    with pytest.raises(ftrack_api.exception.AccessorResourceNotFoundError):\n        accessor.remove('non_existant')\n\n\ndef test_make_container(temporary_path):\n    '''Create container.'''\n    accessor = ftrack_api.accessor.disk.DiskAccessor(temporary_path)\n\n    accessor.make_container('test')\n    assert os.path.isdir(os.path.join(temporary_path, 'test')) is True\n\n    # Recursive\n    accessor.make_container('test/a/b/c')\n    assert (\n        os.path.isdir(\n            os.path.join(temporary_path, 'test', 'a', 'b', 'c')\n        ) is\n        True\n    )\n\n    # Non-recursive fail\n    with pytest.raises(\n        ftrack_api.exception.AccessorParentResourceNotFoundError\n    ):\n        accessor.make_container('test/d/e/f', recursive=False)\n\n    # Existing succeeds\n    accessor.make_container('test/a/b/c')\n\n\ndef test_get_container(temporary_path):\n    '''Get container from resource_identifier.'''\n    # With prefix.\n    accessor = ftrack_api.accessor.disk.DiskAccessor(prefix=temporary_path)\n\n    assert (\n        accessor.get_container(os.path.join('test', 'a')) ==\n        'test'\n    )\n\n    assert (\n        accessor.get_container(os.path.join('test', 'a/')) ==\n        'test'\n    )\n\n    assert (\n        accessor.get_container('test') ==\n        ''\n    )\n\n    with pytest.raises(\n        ftrack_api.exception.AccessorParentResourceNotFoundError\n    ):\n        accessor.get_container('')\n\n    with pytest.raises(\n        ftrack_api.exception.AccessorParentResourceNotFoundError\n    ):\n        accessor.get_container(temporary_path)\n\n    # Without prefix.\n    accessor = ftrack_api.accessor.disk.DiskAccessor(prefix='')\n\n    assert (\n        accessor.get_container(os.path.join(temporary_path, 'test', 'a')) ==\n        os.path.join(temporary_path, 'test')\n    )\n\n    assert (\n        accessor.get_container(\n            os.path.join(temporary_path, 'test', 'a/')\n        ) ==\n        os.path.join(temporary_path, 'test')\n    )\n\n    assert (\n        accessor.get_container(os.path.join(temporary_path, 'test')) ==\n        temporary_path\n    )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/accessor/test_server.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport uuid\n\nimport pytest\n\nimport ftrack_api\nimport ftrack_api.exception\nimport ftrack_api.accessor.server\nimport ftrack_api.data\n\n\ndef test_read_and_write(new_component, session):\n    '''Read and write data from server accessor.'''\n    random_data = uuid.uuid1().hex\n\n    accessor = ftrack_api.accessor.server._ServerAccessor(session)\n    http_file = accessor.open(new_component['id'], mode='wb')\n    http_file.write(random_data)\n    http_file.close()\n\n    data = accessor.open(new_component['id'], 'r')\n    assert data.read() == random_data, 'Read data is the same as written.'\n    data.close()\n\n\ndef test_remove_data(new_component, session):\n    '''Remove data using server accessor.'''\n    random_data = uuid.uuid1().hex\n\n    accessor = ftrack_api.accessor.server._ServerAccessor(session)\n    http_file = accessor.open(new_component['id'], mode='wb')\n    http_file.write(random_data)\n    http_file.close()\n\n    accessor.remove(new_component['id'])\n\n    data = accessor.open(new_component['id'], 'r')\n    with pytest.raises(ftrack_api.exception.AccessorOperationFailedError):\n        data.read()\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/conftest.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport uuid\nimport tempfile\nimport shutil\nimport os\n\nimport pytest\nimport clique\n\nimport ftrack_api\nimport ftrack_api.symbol\n\n\ndef pytest_generate_tests(metafunc):\n    '''Parametrize tests dynamically.\n\n    If a test function has a corresponding parametrize function then call it\n    passing along the *metafunc*. For example, for a \"test_foo\" function, look\n    for and call \"parametrize_test_foo\" if it exists.\n\n    This is useful when more complex dynamic parametrization is needed than the\n    standard pytest.mark.parametrize decorator can provide.\n\n    '''\n    generator_name = 'parametrize_{}'.format(metafunc.function.__name__)\n    generator = getattr(metafunc.module, generator_name, None)\n    if callable(generator):\n        generator(metafunc)\n\n\ndef _temporary_file(request, **kwargs):\n    '''Return temporary file.'''\n    file_handle, path = tempfile.mkstemp(**kwargs)\n    os.close(file_handle)\n\n    def cleanup():\n        '''Remove temporary file.'''\n        try:\n            os.remove(path)\n        except OSError:\n            pass\n\n    request.addfinalizer(cleanup)\n    return path\n\n\n@pytest.fixture()\ndef temporary_file(request):\n    '''Return temporary file.'''\n    return _temporary_file(request)\n\n\n@pytest.fixture()\ndef temporary_image(request):\n    '''Return temporary file.'''\n    return _temporary_file(request, suffix='.jpg')\n\n\n@pytest.fixture()\ndef temporary_directory(request):\n    '''Return temporary directory.'''\n    path = tempfile.mkdtemp()\n\n    def cleanup():\n        '''Remove temporary directory.'''\n        shutil.rmtree(path)\n\n    request.addfinalizer(cleanup)\n\n    return path\n\n\n@pytest.fixture()\ndef temporary_sequence(temporary_directory):\n    '''Return temporary sequence of three files.\n\n    Return the path using the `clique\n    <http://clique.readthedocs.org/en/latest/>`_ format, for example::\n\n        /tmp/asfjsfjoj3/%04d.jpg [1-3]\n\n    '''\n    items = []\n    for index in range(3):\n        item_path = os.path.join(\n            temporary_directory, '{0:04d}.jpg'.format(index)\n        )\n        with open(item_path, 'w') as file_descriptor:\n            file_descriptor.write(uuid.uuid4().hex)\n            file_descriptor.close()\n\n        items.append(item_path)\n\n    collections, _ = clique.assemble(items)\n    sequence_path = collections[0].format()\n\n    return sequence_path\n\n\n@pytest.fixture()\ndef video_path():\n    '''Return a path to a video file.'''\n    video = os.path.abspath(\n        os.path.join(\n            os.path.dirname(__file__),\n            '..',\n            'fixture',\n            'media',\n            'colour_wheel.mov'\n        )\n    )\n\n    return video\n\n\n@pytest.fixture()\ndef session():\n    '''Return session instance.'''\n    return ftrack_api.Session()\n\n\n@pytest.fixture()\ndef session_no_autoconnect_hub():\n    '''Return session instance not auto connected to hub.'''\n    return ftrack_api.Session(auto_connect_event_hub=False)\n\n\n@pytest.fixture()\ndef unique_name():\n    '''Return a unique name.'''\n    return 'test-{0}'.format(uuid.uuid4())\n\n\n@pytest.fixture()\ndef temporary_path(request):\n    '''Return temporary path.'''\n    path = tempfile.mkdtemp()\n\n    def cleanup():\n        '''Remove created path.'''\n        try:\n            shutil.rmtree(path)\n        except OSError:\n            pass\n\n    request.addfinalizer(cleanup)\n\n    return path\n\n\n@pytest.fixture()\ndef new_user(request, session, unique_name):\n    '''Return a newly created unique user.'''\n    entity = session.create('User', {'username': unique_name})\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        session.delete(entity)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return entity\n\n\n@pytest.fixture()\ndef user(session):\n    '''Return the same user entity for entire session.'''\n    # Jenkins user\n    entity = session.get('User', 'd07ae5d0-66e1-11e1-b5e9-f23c91df25eb')\n    assert entity is not None\n\n    return entity\n\n\n@pytest.fixture()\ndef project_schema(session):\n    '''Return project schema.'''\n    # VFX Scheme\n    entity = session.get(\n        'ProjectSchema', '69cb7f92-4dbf-11e1-9902-f23c91df25eb'\n    )\n    assert entity is not None\n    return entity\n\n\n@pytest.fixture()\ndef new_project_tree(request, session, user):\n    '''Return new project with basic tree.'''\n    project_schema = session.query('ProjectSchema').first()\n    default_shot_status = project_schema.get_statuses('Shot')[0]\n    default_task_type = project_schema.get_types('Task')[0]\n    default_task_status = project_schema.get_statuses(\n        'Task', default_task_type['id']\n    )[0]\n\n    project_name = 'python_api_test_{0}'.format(uuid.uuid1().hex)\n    project = session.create('Project', {\n        'name': project_name,\n        'full_name': project_name + '_full',\n        'project_schema': project_schema\n    })\n\n    for sequence_number in range(1):\n        sequence = session.create('Sequence', {\n            'name': 'sequence_{0:03d}'.format(sequence_number),\n            'parent': project\n        })\n\n        for shot_number in range(1):\n            shot = session.create('Shot', {\n                'name': 'shot_{0:03d}'.format(shot_number * 10),\n                'parent': sequence,\n                'status': default_shot_status\n            })\n\n            for task_number in range(1):\n                task = session.create('Task', {\n                    'name': 'task_{0:03d}'.format(task_number),\n                    'parent': shot,\n                    'status': default_task_status,\n                    'type': default_task_type\n                })\n\n                session.create('Appointment', {\n                    'type': 'assignment',\n                    'context': task,\n                    'resource': user\n                })\n\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        session.delete(project)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return project\n\n\n@pytest.fixture()\ndef new_project(request, session, user):\n    '''Return new empty project.'''\n    project_schema = session.query('ProjectSchema').first()\n    project_name = 'python_api_test_{0}'.format(uuid.uuid1().hex)\n    project = session.create('Project', {\n        'name': project_name,\n        'full_name': project_name + '_full',\n        'project_schema': project_schema\n    })\n\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        session.delete(project)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return project\n\n\n@pytest.fixture()\ndef project(session):\n    '''Return same project for entire session.'''\n    # Test project.\n    entity = session.get('Project', '5671dcb0-66de-11e1-8e6e-f23c91df25eb')\n    assert entity is not None\n\n    return entity\n\n\n@pytest.fixture()\ndef new_task(request, session, unique_name):\n    '''Return a new task.'''\n    project = session.query(\n        'Project where id is 5671dcb0-66de-11e1-8e6e-f23c91df25eb'\n    ).one()\n    project_schema = project['project_schema']\n    default_task_type = project_schema.get_types('Task')[0]\n    default_task_status = project_schema.get_statuses(\n        'Task', default_task_type['id']\n    )[0]\n\n    task = session.create('Task', {\n        'name': unique_name,\n        'parent': project,\n        'status': default_task_status,\n        'type': default_task_type\n    })\n\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        session.delete(task)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return task\n\n\n@pytest.fixture()\ndef task(session):\n    '''Return same task for entire session.'''\n    # Tests/python_api/tasks/t1\n    entity = session.get('Task', 'adb4ad6c-7679-11e2-8df2-f23c91df25eb')\n    assert entity is not None\n\n    return entity\n\n\n@pytest.fixture()\ndef new_scope(request, session, unique_name):\n    '''Return a new scope.'''\n    scope = session.create('Scope', {\n        'name': unique_name\n    })\n\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        session.delete(scope)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return scope\n\n\n@pytest.fixture()\ndef new_job(request, session, unique_name, user):\n    '''Return a new scope.'''\n    job = session.create('Job', {\n        'type': 'api_job',\n        'user': user\n    })\n\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        session.delete(job)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return job\n\n\n@pytest.fixture()\ndef new_note(request, session, unique_name, new_task, user):\n    '''Return a new note attached to a task.'''\n    note = new_task.create_note(unique_name, user)\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        session.delete(note)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return note\n\n\n@pytest.fixture()\ndef new_asset_version(request, session):\n    '''Return a new asset version.'''\n    asset_version = session.create('AssetVersion', {\n        'asset_id': 'dd9a7e2e-c5eb-11e1-9885-f23c91df25eb'\n    })\n    session.commit()\n\n    # Do not cleanup the version as that will sometimes result in a deadlock\n    # database error.\n\n    return asset_version\n\n\n@pytest.fixture()\ndef new_component(request, session, temporary_file):\n    '''Return a new component not in any location except origin.'''\n    component = session.create_component(temporary_file, location=None)\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        session.delete(component)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return component\n\n\n@pytest.fixture()\ndef new_container_component(request, session, temporary_directory):\n    '''Return a new container component not in any location except origin.'''\n    component = session.create('ContainerComponent')\n\n    # Add to special origin location so that it is possible to add to other\n    # locations.\n    origin_location = session.get(\n        'Location', ftrack_api.symbol.ORIGIN_LOCATION_ID\n    )\n    origin_location.add_component(\n        component, temporary_directory, recursive=False\n    )\n\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        session.delete(component)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return component\n\n\n@pytest.fixture()\ndef new_sequence_component(request, session, temporary_sequence):\n    '''Return a new sequence component not in any location except origin.'''\n    component = session.create_component(temporary_sequence, location=None)\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        session.delete(component)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return component\n\n\n@pytest.fixture\ndef mocked_schemas():\n    '''Return a list of mocked schemas.'''\n    return [{\n        'id': 'Foo',\n        'type': 'object',\n        'properties': {\n            'id': {\n                'type': 'string'\n            },\n            'string': {\n                'type': 'string'\n            },\n            'integer': {\n                'type': 'integer'\n            },\n            'number': {\n                'type': 'number'\n            },\n            'boolean': {\n                'type': 'boolean'\n            },\n            'bars': {\n                'type': 'array',\n                'items': {\n                    'ref': '$Bar'\n                }\n            },\n            'date': {\n                'type': 'string',\n                'format': 'date-time'\n            }\n        },\n        'immutable': [\n            'id'\n        ],\n        'primary_key': [\n            'id'\n        ],\n        'required': [\n            'id'\n        ],\n        'default_projections': [\n            'id'\n        ]\n    }, {\n        'id': 'Bar',\n        'type': 'object',\n        'properties': {\n            'id': {\n                'type': 'string'\n            },\n            'name': {\n                'type': 'string'\n            },\n            'computed_value': {\n                'type': 'string',\n            }\n        },\n        'computed': [\n            'computed_value'\n        ],\n        'immutable': [\n            'id'\n        ],\n        'primary_key': [\n            'id'\n        ],\n        'required': [\n            'id'\n        ],\n        'default_projections': [\n            'id'\n        ]\n    }]\n\n\n@pytest.yield_fixture\ndef mocked_schema_session(mocker, mocked_schemas):\n    '''Return a session instance with mocked schemas.'''\n    with mocker.patch.object(\n        ftrack_api.Session,\n        '_load_schemas',\n        return_value=mocked_schemas\n    ):\n        # Mock _configure_locations since it will fail if no location schemas\n        # exist.\n        with mocker.patch.object(\n            ftrack_api.Session,\n            '_configure_locations'\n        ):\n            patched_session = ftrack_api.Session()\n            yield patched_session\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/entity/__init__.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/entity/test_asset_version.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\nimport json\n\n\ndef test_create_component(new_asset_version, temporary_file):\n    '''Create component on asset version.'''\n    session = new_asset_version.session\n    component = new_asset_version.create_component(\n        temporary_file, location=None\n    )\n    assert component['version'] is new_asset_version\n\n    # Have to delete component before can delete asset version.\n    session.delete(component)\n\n\ndef test_create_component_specifying_different_version(\n    new_asset_version, temporary_file\n):\n    '''Create component on asset version ignoring specified version.'''\n    session = new_asset_version.session\n    component = new_asset_version.create_component(\n        temporary_file, location=None,\n        data=dict(\n            version_id='this-value-should-be-ignored',\n            version='this-value-should-be-overridden'\n        )\n    )\n    assert component['version'] is new_asset_version\n\n    # Have to delete component before can delete asset version.\n    session.delete(component)\n\n\ndef test_encode_media(new_asset_version, video_path):\n    '''Encode media based on a file path\n\n    Encoded components should be associated with the version.\n    '''\n    session = new_asset_version.session\n    job = new_asset_version.encode_media(video_path)\n    assert job.entity_type == 'Job'\n\n    job_data = json.loads(job['data'])\n    assert 'output' in job_data\n    assert len(job_data['output'])\n    assert 'component_id' in job_data['output'][0]\n\n    component_id = job_data['output'][0]['component_id']\n    component = session.get('FileComponent', component_id) \n\n    # Component should be associated with the version.\n    assert component['version_id'] == new_asset_version['id']\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/entity/test_base.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2016 ftrack\n\nimport pytest\n\n\ndef test_hash(project, task, user):\n    '''Entities can be hashed.'''\n    test_set = set()\n    test_set.add(project)\n    test_set.add(task)\n    test_set.add(user)\n\n    assert test_set == set((project, task, user))\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/entity/test_component.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\nimport os\n\nimport pytest\n\n\ndef test_get_availability(new_component):\n    '''Retrieve availability in locations.'''\n    session = new_component.session\n    availability = new_component.get_availability()\n\n    # Note: Currently the origin location is also 0.0 as the link is not\n    # persisted to the server. This may change in future and this test would\n    # need updating as a result.\n    assert set(availability.values()) == set([0.0])\n\n    # Add to a location.\n    source_location = session.query(\n        'Location where name is \"ftrack.origin\"'\n    ).one()\n\n    target_location = session.query(\n        'Location where name is \"ftrack.unmanaged\"'\n    ).one()\n\n    target_location.add_component(new_component, source_location)\n\n    # Recalculate availability.\n\n    # Currently have to manually expire the related attribute. This should be\n    # solved in future by bi-directional relationship updating.\n    del new_component['component_locations']\n\n    availability = new_component.get_availability()\n    target_availability = availability.pop(target_location['id'])\n    assert target_availability == 100.0\n\n    # All other locations should still be 0.\n    assert set(availability.values()) == set([0.0])\n\n@pytest.fixture()\ndef image_path():\n    '''Return a path to an image file.'''\n    image_path = os.path.abspath(\n        os.path.join(\n            os.path.dirname(__file__),\n            '..',\n            '..',\n            'fixture',\n            'media',\n            'image.png'\n        )\n    )\n\n    return image_path\n\ndef test_create_task_thumbnail(task, image_path):\n    '''Successfully create thumbnail component and set as task thumbnail.'''\n    component = task.create_thumbnail(image_path)\n    component.session.commit()\n    assert component['id'] == task['thumbnail_id']\n\n\ndef test_create_thumbnail_with_data(task, image_path, unique_name):\n    '''Successfully create thumbnail component with custom data.'''\n    data = {'name': unique_name}\n    component = task.create_thumbnail(image_path, data=data)\n    component.session.commit()\n    assert component['name'] == unique_name\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/entity/test_factory.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport ftrack_api.entity.factory\n\n\nclass CustomUser(ftrack_api.entity.base.Entity):\n    '''Represent custom user.'''\n\n\ndef test_extend_standard_factory_with_bases(session):\n    '''Successfully add extra bases to standard factory.'''\n    standard_factory = ftrack_api.entity.factory.StandardFactory()\n\n    schemas = session._load_schemas(False)\n    user_schema = [\n        schema for schema in schemas if schema['id'] == 'User'\n    ].pop()\n\n    user_class = standard_factory.create(user_schema, bases=[CustomUser])\n    session.types[user_class.entity_type] = user_class\n\n    user = session.query('User').first()\n\n    assert CustomUser in type(user).__mro__\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/entity/test_job.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport pytest\n\n\ndef test_create_job(session, user):\n    '''Create job.'''\n    job = session.create('Job', {\n        'user': user\n    })\n\n    assert job\n    session.commit()\n    assert job['type'] == 'api_job'\n\n    session.delete(job)\n    session.commit()\n\n\ndef test_create_job_with_valid_type(session, user):\n    '''Create job explicitly specifying valid type.'''\n    job = session.create('Job', {\n        'user': user,\n        'type': 'api_job'\n    })\n\n    assert job\n    session.commit()\n    assert job['type'] == 'api_job'\n\n    session.delete(job)\n    session.commit()\n\n\ndef test_create_job_using_faulty_type(session, user):\n    '''Fail to create job with faulty type.'''\n    with pytest.raises(ValueError):\n        session.create('Job', {\n            'user': user,\n            'type': 'not-allowed-type'\n        })\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/entity/test_location.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport os\nimport base64\nimport filecmp\n\nimport pytest\nimport requests\n\nimport ftrack_api.exception\nimport ftrack_api.accessor.disk\nimport ftrack_api.structure.origin\nimport ftrack_api.structure.id\nimport ftrack_api.entity.location\nimport ftrack_api.resource_identifier_transformer.base as _transformer\nimport ftrack_api.symbol\n\n\nclass Base64ResourceIdentifierTransformer(\n    _transformer.ResourceIdentifierTransformer\n):\n    '''Resource identifier transformer for test purposes.\n\n    Store resource identifier as base 64 encoded string.\n\n    '''\n\n    def encode(self, resource_identifier, context=None):\n        '''Return encoded *resource_identifier* for storing centrally.\n\n        A mapping of *context* values may be supplied to guide the\n        transformation.\n\n        '''\n        return base64.encodestring(resource_identifier)\n\n    def decode(self, resource_identifier, context=None):\n        '''Return decoded *resource_identifier* for use locally.\n\n        A mapping of *context* values may be supplied to guide the\n        transformation.\n\n        '''\n        return base64.decodestring(resource_identifier)\n\n\n@pytest.fixture()\ndef new_location(request, session, unique_name, temporary_directory):\n    '''Return new managed location.'''\n    location = session.create('Location', {\n        'name': 'test-location-{}'.format(unique_name)\n    })\n\n    location.accessor = ftrack_api.accessor.disk.DiskAccessor(\n        prefix=os.path.join(temporary_directory, 'location')\n    )\n    location.structure = ftrack_api.structure.id.IdStructure()\n    location.priority = 10\n\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        # First auto-remove all components in location.\n        for location_component in location['location_components']:\n            session.delete(location_component)\n\n        # At present, need this intermediate commit otherwise server errors\n        # complaining that location still has components in it.\n        session.commit()\n\n        session.delete(location)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return location\n\n\n@pytest.fixture()\ndef new_unmanaged_location(request, session, unique_name):\n    '''Return new unmanaged location.'''\n    location = session.create('Location', {\n        'name': 'test-location-{}'.format(unique_name)\n    })\n\n    # TODO: Change to managed and use a temporary directory cleaned up after.\n    ftrack_api.mixin(\n        location, ftrack_api.entity.location.UnmanagedLocationMixin,\n        name='UnmanagedTestLocation'\n    )\n    location.accessor = ftrack_api.accessor.disk.DiskAccessor(prefix='')\n    location.structure = ftrack_api.structure.origin.OriginStructure()\n    location.priority = 10\n\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        # First auto-remove all components in location.\n        for location_component in location['location_components']:\n            session.delete(location_component)\n\n        # At present, need this intermediate commit otherwise server errors\n        # complaining that location still has components in it.\n        session.commit()\n\n        session.delete(location)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return location\n\n\n@pytest.fixture()\ndef origin_location(session):\n    '''Return origin location.'''\n    return session.query('Location where name is \"ftrack.origin\"').one()\n\n@pytest.fixture()\ndef server_location(session):\n    '''Return server location.'''\n    return session.get('Location', ftrack_api.symbol.SERVER_LOCATION_ID)\n\n\n@pytest.fixture()\ndef server_image_component(request, session, server_location):\n    image_file = os.path.abspath(\n        os.path.join(\n            os.path.dirname(__file__),\n            '..',\n            '..',\n            'fixture',\n            'media',\n            'image.png'\n        )\n    )\n    component = session.create_component(\n        image_file, location=server_location\n    )\n\n    def cleanup():\n        server_location.remove_component(component)\n    request.addfinalizer(cleanup)\n\n    return component\n\n\n@pytest.mark.parametrize('name', [\n    'named',\n    None\n], ids=[\n    'named',\n    'unnamed'\n])\ndef test_string_representation(session, name):\n    '''Return string representation.'''\n    location = session.create('Location', {'id': '1'})\n    if name:\n        location['name'] = name\n        assert str(location) == '<Location(\"named\", 1)>'\n    else:\n        assert str(location) == '<Location(1)>'\n\n\ndef test_add_components(new_location, origin_location, session, temporary_file):\n    '''Add components.'''\n    component_a = session.create_component(\n        temporary_file, location=None\n    )\n    component_b = session.create_component(\n        temporary_file, location=None\n    )\n\n    assert (\n        new_location.get_component_availabilities([component_a, component_b])\n        == [0.0, 0.0]\n    )\n\n    new_location.add_components(\n        [component_a, component_b], [origin_location, origin_location]\n    )\n\n    # Recalculate availability.\n\n    # Currently have to manually expire the related attribute. This should be\n    # solved in future by bi-directional relationship updating.\n    del component_a['component_locations']\n    del component_b['component_locations']\n\n    assert (\n        new_location.get_component_availabilities([component_a, component_b])\n        == [100.0, 100.0]\n    )\n\n\ndef test_add_components_from_single_location(\n    new_location, origin_location, session, temporary_file\n):\n    '''Add components from single location.'''\n    component_a = session.create_component(\n        temporary_file, location=None\n    )\n    component_b = session.create_component(\n        temporary_file, location=None\n    )\n\n    assert (\n        new_location.get_component_availabilities([component_a, component_b])\n        == [0.0, 0.0]\n    )\n\n    new_location.add_components([component_a, component_b], origin_location)\n\n    # Recalculate availability.\n\n    # Currently have to manually expire the related attribute. This should be\n    # solved in future by bi-directional relationship updating.\n    del component_a['component_locations']\n    del component_b['component_locations']\n\n    assert (\n        new_location.get_component_availabilities([component_a, component_b])\n        == [100.0, 100.0]\n    )\n\n\ndef test_add_components_with_mismatching_sources(new_location, new_component):\n    '''Fail to add components when sources mismatched.'''\n    with pytest.raises(ValueError):\n        new_location.add_components([new_component], [])\n\n\ndef test_add_components_with_undefined_structure(new_location, mocker):\n    '''Fail to add components when location structure undefined.'''\n    mocker.patch.object(new_location, 'structure', None)\n\n    with pytest.raises(ftrack_api.exception.LocationError):\n        new_location.add_components([], [])\n\n\ndef test_add_components_already_in_location(\n    session, temporary_file, new_location, new_component, origin_location\n):\n    '''Fail to add components already in location.'''\n    new_location.add_component(new_component, origin_location)\n\n    another_new_component = session.create_component(\n        temporary_file, location=None\n    )\n\n    with pytest.raises(ftrack_api.exception.ComponentInLocationError):\n        new_location.add_components(\n            [another_new_component, new_component], origin_location\n        )\n\n\ndef test_add_component_when_data_already_exists(\n    new_location, new_component, origin_location\n):\n    '''Fail to add component when data already exists.'''\n    # Inject pre-existing data on disk.\n    resource_identifier = new_location.structure.get_resource_identifier(\n        new_component\n    )\n    container = new_location.accessor.get_container(resource_identifier)\n    new_location.accessor.make_container(container)\n    data = new_location.accessor.open(resource_identifier, 'w')\n    data.close()\n\n    with pytest.raises(ftrack_api.exception.LocationError):\n        new_location.add_component(new_component, origin_location)\n\n\ndef test_add_component_missing_source_accessor(\n    new_location, new_component, origin_location, mocker\n):\n    '''Fail to add component when source is missing accessor.'''\n    mocker.patch.object(origin_location, 'accessor', None)\n\n    with pytest.raises(ftrack_api.exception.LocationError):\n        new_location.add_component(new_component, origin_location)\n\n\ndef test_add_component_missing_target_accessor(\n    new_location, new_component, origin_location, mocker\n):\n    '''Fail to add component when target is missing accessor.'''\n    mocker.patch.object(new_location, 'accessor', None)\n\n    with pytest.raises(ftrack_api.exception.LocationError):\n        new_location.add_component(new_component, origin_location)\n\n\ndef test_add_container_component(\n    new_container_component, new_location, origin_location\n):\n    '''Add container component.'''\n    new_location.add_component(new_container_component, origin_location)\n\n    assert (\n        new_location.get_component_availability(new_container_component)\n        == 100.0\n    )\n\n\ndef test_add_sequence_component_recursively(\n    new_sequence_component, new_location, origin_location\n):\n    '''Add sequence component recursively.'''\n    new_location.add_component(\n        new_sequence_component, origin_location, recursive=True\n    )\n\n    assert (\n        new_location.get_component_availability(new_sequence_component)\n        == 100.0\n    )\n\n\ndef test_add_sequence_component_non_recursively(\n    new_sequence_component, new_location, origin_location\n):\n    '''Add sequence component non recursively.'''\n    new_location.add_component(\n        new_sequence_component, origin_location, recursive=False\n    )\n\n    assert (\n        new_location.get_component_availability(new_sequence_component)\n        == 0.0\n    )\n\n\ndef test_remove_components(\n    session, new_location, origin_location, temporary_file\n):\n    '''Remove components.'''\n    component_a = session.create_component(\n        temporary_file, location=None\n    )\n    component_b = session.create_component(\n        temporary_file, location=None\n    )\n\n    new_location.add_components([component_a, component_b], origin_location)\n    assert (\n        new_location.get_component_availabilities([component_a, component_b])\n        == [100.0, 100.0]\n    )\n\n    new_location.remove_components([\n        component_a, component_b\n    ])\n\n    # Recalculate availability.\n\n    # Currently have to manually expire the related attribute. This should be\n    # solved in future by bi-directional relationship updating.\n    del component_a['component_locations']\n    del component_b['component_locations']\n\n    assert (\n        new_location.get_component_availabilities([component_a, component_b])\n        == [0.0, 0.0]\n    )\n\n\ndef test_remove_sequence_component_recursively(\n    new_sequence_component, new_location, origin_location\n):\n    '''Remove sequence component recursively.'''\n    new_location.add_component(\n        new_sequence_component, origin_location, recursive=True\n    )\n\n    new_location.remove_component(\n        new_sequence_component, recursive=True\n    )\n\n    assert (\n        new_location.get_component_availability(new_sequence_component)\n        == 0.0\n    )\n\n\ndef test_remove_sequence_component_non_recursively(\n    new_sequence_component, new_location, origin_location\n):\n    '''Remove sequence component non recursively.'''\n    new_location.add_component(\n        new_sequence_component, origin_location, recursive=False\n    )\n\n    new_location.remove_component(\n        new_sequence_component, recursive=False\n    )\n\n    assert (\n        new_location.get_component_availability(new_sequence_component)\n        == 0.0\n    )\n\n\ndef test_remove_component_missing_accessor(\n    new_location, new_component, origin_location, mocker\n):\n    '''Fail to remove component when location is missing accessor.'''\n    new_location.add_component(new_component, origin_location)\n    mocker.patch.object(new_location, 'accessor', None)\n\n    with pytest.raises(ftrack_api.exception.LocationError):\n        new_location.remove_component(new_component)\n\n\ndef test_resource_identifier_transformer(\n    new_component, new_unmanaged_location, origin_location, mocker\n):\n    '''Transform resource identifier.'''\n    session = new_unmanaged_location.session\n\n    transformer = Base64ResourceIdentifierTransformer(session)\n    mocker.patch.object(\n        new_unmanaged_location, 'resource_identifier_transformer', transformer\n    )\n\n    new_unmanaged_location.add_component(new_component, origin_location)\n\n    original_resource_identifier = origin_location.get_resource_identifier(\n        new_component\n    )\n    assert (\n        new_component['component_locations'][0]['resource_identifier']\n        == base64.encodestring(original_resource_identifier)\n    )\n\n    assert (\n        new_unmanaged_location.get_resource_identifier(new_component)\n        == original_resource_identifier\n    )\n\n\ndef test_get_filesystem_path(new_component, new_location, origin_location):\n    '''Retrieve filesystem path.'''\n    new_location.add_component(new_component, origin_location)\n    resource_identifier = new_location.structure.get_resource_identifier(\n        new_component\n    )\n    expected = os.path.normpath(\n        os.path.join(new_location.accessor.prefix, resource_identifier)\n    )\n    assert new_location.get_filesystem_path(new_component) == expected\n\n\ndef test_get_context(new_component, new_location, origin_location):\n    '''Retrieve context for component.'''\n    resource_identifier = origin_location.get_resource_identifier(\n        new_component\n    )\n    context = new_location._get_context(new_component, origin_location)\n    assert context == {\n        'source_resource_identifier': resource_identifier\n    }\n\n\ndef test_get_context_for_component_not_in_source(new_component, new_location):\n    '''Retrieve context for component not in source location.'''\n    context = new_location._get_context(new_component, new_location)\n    assert context == {}\n\n\ndef test_data_transfer(session, new_location, origin_location):\n    '''Transfer a real file and make sure it is identical.'''\n    video_file = os.path.abspath(\n        os.path.join(\n            os.path.dirname(__file__),\n            '..',\n            '..',\n            'fixture',\n            'media',\n            'colour_wheel.mov'\n        )\n    )\n    component = session.create_component(\n        video_file, location=new_location\n    )\n    new_video_file = new_location.get_filesystem_path(component)\n\n    assert filecmp.cmp(video_file, new_video_file)\n\n\ndef test_get_thumbnail_url(server_location, server_image_component):\n    '''Test download a thumbnail image from server location'''\n    thumbnail_url = server_location.get_thumbnail_url(\n        server_image_component,\n        size=10\n    )\n    assert thumbnail_url\n\n    response = requests.get(thumbnail_url)\n    response.raise_for_status()\n\n    image_file = os.path.abspath(\n        os.path.join(\n            os.path.dirname(__file__),\n            '..',\n            '..',\n            'fixture',\n            'media',\n            'image-resized-10.png'\n        )\n    )\n    expected_image_contents = open(image_file).read()\n    assert response.content == expected_image_contents\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/entity/test_metadata.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport uuid\n\nimport ftrack_api\n\n\ndef test_query_metadata(new_project):\n    '''Query metadata.'''\n    session = new_project.session\n\n    metadata_key = uuid.uuid1().hex\n    metadata_value = uuid.uuid1().hex\n    new_project['metadata'][metadata_key] = metadata_value\n    session.commit()\n\n    results = session.query(\n        'Project where metadata.key is {0}'.format(metadata_key)\n    )\n\n    assert len(results) == 1\n    assert new_project['id'] == results[0]['id']\n\n    results = session.query(\n        'Project where metadata.value is {0}'.format(metadata_value)\n    )\n\n    assert len(results) == 1\n    assert new_project['id'] == results[0]['id']\n\n    results = session.query(\n        'Project where metadata.key is {0} and '\n        'metadata.value is {1}'.format(metadata_key, metadata_value)\n    )\n\n    assert len(results) == 1\n    assert new_project['id'] == results[0]['id']\n\n\ndef test_set_get_metadata_from_different_sessions(new_project):\n    '''Get and set metadata using different sessions.'''\n    session = new_project.session\n\n    metadata_key = uuid.uuid1().hex\n    metadata_value = uuid.uuid1().hex\n    new_project['metadata'][metadata_key] = metadata_value\n    session.commit()\n\n    new_session = ftrack_api.Session()\n    project = new_session.query(\n        'Project where id is {0}'.format(new_project['id'])\n    )[0]\n\n    assert project['metadata'][metadata_key] == metadata_value\n\n    project['metadata'][metadata_key] = uuid.uuid1().hex\n\n    new_session.commit()\n\n    new_session = ftrack_api.Session()\n    project = new_session.query(\n        'Project where id is {0}'.format(project['id'])\n    )[0]\n\n    assert project['metadata'][metadata_key] != metadata_value\n\n\ndef test_get_set_multiple_metadata(new_project):\n    '''Get and set multiple metadata.'''\n    session = new_project.session\n\n    new_project['metadata'] = {\n        'key1': 'value1',\n        'key2': 'value2'\n    }\n    session.commit()\n\n    assert set(new_project['metadata'].keys()) == set(['key1', 'key2'])\n\n    new_session = ftrack_api.Session()\n    retrieved = new_session.query(\n        'Project where id is {0}'.format(new_project['id'])\n    )[0]\n\n    assert set(retrieved['metadata'].keys()) == set(['key1', 'key2'])\n\n\ndef test_metadata_parent_type_remains_in_schema_id_format(session, new_project):\n    '''Metadata parent_type remains in schema id format post commit.'''\n    entity = session.create('Metadata', {\n        'key': 'key', 'value': 'value',\n        'parent_type': new_project.entity_type,\n        'parent_id':  new_project['id']\n    })\n\n    session.commit()\n\n    assert entity['parent_type'] == new_project.entity_type\n\n\ndef test_set_metadata_twice(new_project):\n    '''Set metadata twice in a row.'''\n    session = new_project.session\n\n    new_project['metadata'] = {\n        'key1': 'value1',\n        'key2': 'value2'\n    }\n    session.commit()\n\n    assert set(new_project['metadata'].keys()) == set(['key1', 'key2'])\n\n    new_project['metadata'] = {\n        'key3': 'value3',\n        'key4': 'value4'\n    }\n    session.commit()\n\n\ndef test_set_same_metadata_on_retrieved_entity(new_project):\n    '''Set same metadata on retrieved entity.'''\n    session = new_project.session\n\n    new_project['metadata'] = {\n        'key1': 'value1'\n    }\n    session.commit()\n\n    project = session.get('Project', new_project['id'])\n\n    project['metadata'] = {\n        'key1': 'value1'\n    }\n    session.commit()\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/entity/test_note.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport ftrack_api\nimport ftrack_api.inspection\n\n\ndef test_create_reply(session, new_note, user, unique_name):\n    '''Create reply to a note.'''\n    reply_text = 'My reply on note'\n    new_note.create_reply(reply_text, user)\n\n    session.commit()\n\n    assert len(new_note['replies']) == 1\n\n    assert reply_text == new_note['replies'][0]['content']\n\n\ndef test_create_note_on_entity(session, new_task, user, unique_name):\n    '''Create note attached to an entity.'''\n    note = new_task.create_note(unique_name, user)\n    session.commit()\n\n    session.reset()\n    retrieved_task = session.get(*ftrack_api.inspection.identity(new_task))\n    assert len(retrieved_task['notes']) == 1\n    assert (\n        ftrack_api.inspection.identity(retrieved_task['notes'][0])\n        == ftrack_api.inspection.identity(note)\n    )\n\n\ndef test_create_note_on_entity_specifying_recipients(\n    session, new_task, user, unique_name, new_user\n):\n    '''Create note with specified recipients attached to an entity.'''\n    recipient = new_user\n    note = new_task.create_note(unique_name, user, recipients=[recipient])\n    session.commit()\n\n    session.reset()\n    retrieved_note = session.get(*ftrack_api.inspection.identity(note))\n\n    # Note: The calling user is automatically added server side so there will be\n    # 2 recipients.\n    assert len(retrieved_note['recipients']) == 2\n    specified_recipient_present = False\n    for entry in retrieved_note['recipients']:\n        if entry['resource_id'] == recipient['id']:\n            specified_recipient_present = True\n            break\n\n    assert specified_recipient_present\n\n\ndef test_create_note_on_entity_specifying_category(\n    session, new_task, user, unique_name\n):\n    '''Create note with specified category attached to an entity.'''\n    category = session.query('NoteCategory').first()\n    note = new_task.create_note(unique_name, user, category=category)\n    session.commit()\n\n    session.reset()\n    retrieved_note = session.get(*ftrack_api.inspection.identity(note))\n    assert retrieved_note['category']['id'] == category['id']\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/entity/test_project_schema.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport inspect\n\nimport pytest\n\n\n@pytest.mark.parametrize('schema, expected', [\n    ('Task', [\n        'Not started', 'In progress', 'Awaiting approval', 'Approved'\n    ]),\n    ('Shot', [\n        'Normal', 'Omitted', 'On Hold'\n    ]),\n    ('AssetVersion', [\n        'Approved', 'Pending'\n    ]),\n    ('AssetBuild', [\n        'Normal', 'Omitted', 'On Hold'\n    ]),\n    ('Invalid', ValueError)\n], ids=[\n    'task',\n    'shot',\n    'asset version',\n    'asset build',\n    'invalid'\n])\ndef test_get_statuses(project_schema, schema, expected):\n    '''Retrieve statuses for schema and optional type.'''\n    if inspect.isclass(expected) and issubclass(expected, Exception):\n        with pytest.raises(expected):\n            project_schema.get_statuses(schema)\n\n    else:\n        statuses = project_schema.get_statuses(schema)\n        status_names = [status['name'] for status in statuses]\n        assert sorted(status_names) == sorted(expected)\n\n\n@pytest.mark.parametrize('schema, expected', [\n    ('Task', [\n        'Generic', 'Animation', 'Modeling', 'Previz', 'Lookdev', 'Hair',\n        'Cloth', 'FX', 'Lighting', 'Compositing', 'Tracking', 'Rigging',\n        'test 1', 'test type 2'\n    ]),\n    ('AssetBuild', ['Character', 'Prop', 'Environment', 'Matte Painting']),\n    ('Invalid', ValueError)\n], ids=[\n    'task',\n    'asset build',\n    'invalid'\n])\ndef test_get_types(project_schema, schema, expected):\n    '''Retrieve types for schema.'''\n    if inspect.isclass(expected) and issubclass(expected, Exception):\n        with pytest.raises(expected):\n            project_schema.get_types(schema)\n\n    else:\n        types = project_schema.get_types(schema)\n        type_names = [type_['name'] for type_ in types]\n        assert sorted(type_names) == sorted(expected)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/entity/test_scopes.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\n\ndef test_add_remove_and_query_scopes_for_tasks(session, new_task, new_scope):\n    '''Add, remove and query scopes for task.'''\n    query_string = 'Task where scopes.name is {0}'.format(new_scope['name'])\n    tasks = session.query(query_string)\n\n    assert len(tasks) == 0\n\n    new_task['scopes'].append(new_scope)\n    session.commit()\n\n    tasks = session.query(query_string)\n\n    assert len(tasks) == 1 and tasks[0] == new_task\n\n    new_task['scopes'].remove(new_scope)\n    session.commit()\n\n    tasks = session.query(query_string)\n\n    assert len(tasks) == 0\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/entity/test_user.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2016 ftrack\n\n\ndef test_force_start_timer(new_user, task):\n    '''Successfully force starting a timer when another timer is running.'''\n    first_timer = new_user.start_timer(context=task)\n    second_timer = new_user.start_timer(context=task, force=True)\n\n    assert first_timer['id']\n    assert second_timer['id']\n    assert first_timer['id'] != second_timer['id']\n\n\ndef test_timer_creates_timelog(new_user, task, unique_name):\n    '''Successfully create time log when stopping timer.\n\n    A timer which was immediately stopped should have a duration less than\n    a minute.\n\n    '''\n    comment = 'comment' + unique_name\n    timer = new_user.start_timer(\n        context=task,\n        name=unique_name,\n        comment=comment\n    )\n    timer_start = timer['start']\n    timelog = new_user.stop_timer()\n\n    assert timelog['user_id'] == new_user['id']\n    assert timelog['context_id']== task['id']\n    assert timelog['name'] == unique_name\n    assert timelog['comment'] == comment\n    assert timelog['start'] == timer_start\n    assert isinstance(timelog['duration'], (int, long, float))\n    assert timelog['duration'] < 60\n\n\ndef test_reset_user_api_key(new_user):\n    '''Test resetting of api keys.'''\n\n    api_keys = list()\n    for i in range(0, 10):\n        api_keys.append(new_user.reset_api_key())\n\n    # make sure all api keys are unique\n    assert len(set(api_keys)) == 10\n\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/event/__init__.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/event/event_hub_server_heartbeat.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport sys\nimport time\nimport logging\nimport argparse\n\nimport ftrack_api\nfrom ftrack_api.event.base import Event\n\n\nTOPIC = 'test_event_hub_server_heartbeat'\nRECEIVED = []\n\n\ndef callback(event):\n    '''Track received messages.'''\n    counter = event['data']['counter']\n    RECEIVED.append(counter)\n    print('Received message {0} ({1} in total)'.format(counter, len(RECEIVED)))\n\n\ndef main(arguments=None):\n    '''Publish and receive heartbeat test.'''\n    parser = argparse.ArgumentParser()\n    parser.add_argument('mode', choices=['publish', 'subscribe'])\n\n    namespace = parser.parse_args(arguments)\n    logging.basicConfig(level=logging.INFO)\n\n    session = ftrack_api.Session()\n\n    message_count = 100\n    sleep_time_per_message = 1\n\n    if namespace.mode == 'publish':\n        max_atempts = 100\n        retry_interval = 0.1\n        atempt = 0\n        while not session.event_hub.connected:\n            print (\n                'Session is not yet connected to event hub, sleeping for 0.1s'\n            )\n            time.sleep(retry_interval)\n\n            atempt = atempt + 1\n            if atempt > max_atempts:\n                raise Exception(\n                    'Unable to connect to server within {0} seconds'.format(\n                        max_atempts * retry_interval\n                    )\n                )\n\n        print('Sending {0} messages...'.format(message_count))\n\n        for counter in range(1, message_count + 1):\n            session.event_hub.publish(\n                Event(topic=TOPIC, data=dict(counter=counter))\n            )\n            print('Sent message {0}'.format(counter))\n\n            if counter < message_count:\n                time.sleep(sleep_time_per_message)\n\n    elif namespace.mode == 'subscribe':\n        session.event_hub.subscribe('topic={0}'.format(TOPIC), callback)\n        session.event_hub.wait(\n            duration=(\n                ((message_count - 1) * sleep_time_per_message) + 15\n            )\n        )\n\n        if len(RECEIVED) != message_count:\n            print(\n                '>> Failed to receive all messages. Dropped {0} <<'\n                .format(message_count - len(RECEIVED))\n            )\n            return False\n\n    # Give time to flush all buffers.\n    time.sleep(5)\n\n    return True\n\n\nif __name__ == '__main__':\n    result = main(sys.argv[1:])\n    if not result:\n        raise SystemExit(1)\n    else:\n        raise SystemExit(0)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/event/test_base.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport ftrack_api.event.base\n\n\ndef test_string_representation():\n    '''String representation.'''\n    event = ftrack_api.event.base.Event('test', id='some-id')\n    assert str(event) == (\n        \"<Event {'topic': 'test', 'source': {}, 'target': '', 'data': {}, \"\n        \"'in_reply_to_event': None, 'id': 'some-id', 'sent': None}>\"\n    )\n\n\ndef test_stop():\n    '''Set stopped flag on event.'''\n    event = ftrack_api.event.base.Event('test', id='some-id')\n\n    assert event.is_stopped() is False\n\n    event.stop()\n    assert event.is_stopped() is True\n\n\ndef test_is_stopped():\n    '''Report stopped status of event.'''\n    event = ftrack_api.event.base.Event('test', id='some-id')\n\n    assert event.is_stopped() is False\n\n    event.stop()\n    assert event.is_stopped() is True\n\n    event.stop()\n    assert event.is_stopped() is True\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/event/test_expression.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport operator\nimport inspect\n\nimport pytest\n\nfrom ftrack_api.event.expression import (\n    Expression, All, Any, Not, Condition, Parser\n)\nfrom ftrack_api.exception import ParseError\n\n\n@pytest.fixture()\ndef candidate():\n    '''Return common candidate to test expressions against.'''\n    return {\n        'id': 10,\n        'name': 'value',\n        'change': {\n            'name': 'value',\n            'new_value': 10\n        }\n    }\n\n\n@pytest.mark.parametrize('expression, expected', [\n    pytest.mark.xfail(('', Expression())),\n    ('invalid', ParseError),\n    ('key=value nor other=value', ParseError),\n    ('key=value', Condition('key', operator.eq, 'value')),\n    ('key=\"value\"', Condition('key', operator.eq, 'value')),\n    (\n        'a=b and ((c=d or e!=f) and not g.h > 10)',\n        All([\n            Condition('a', operator.eq, 'b'),\n            All([\n                Any([\n                    Condition('c', operator.eq, 'd'),\n                    Condition('e', operator.ne, 'f')\n                ]),\n                Not(\n                    Condition('g.h', operator.gt, 10)\n                )\n            ])\n        ])\n    )\n], ids=[\n    'empty expression',\n    'invalid expression',\n    'invalid conjunction',\n    'basic condition',\n    'basic quoted condition',\n    'complex condition'\n])\ndef test_parser_parse(expression, expected):\n    '''Parse expression into Expression instances.'''\n    parser = Parser()\n\n    if inspect.isclass(expected)and issubclass(expected, Exception):\n        with pytest.raises(expected):\n            parser.parse(expression)\n    else:\n        assert str(parser.parse(expression)) == str(expected)\n\n\n@pytest.mark.parametrize('expression, expected', [\n    (Expression(), '<Expression>'),\n    (All([Expression(), Expression()]), '<All [<Expression> <Expression>]>'),\n    (Any([Expression(), Expression()]), '<Any [<Expression> <Expression>]>'),\n    (Not(Expression()), '<Not <Expression>>'),\n    (Condition('key', '=', 'value'), '<Condition key=value>')\n], ids=[\n    'Expression',\n    'All',\n    'Any',\n    'Not',\n    'Condition'\n])\ndef test_string_representation(expression, expected):\n    '''String representation of expression.'''\n    assert str(expression) == expected\n\n\n@pytest.mark.parametrize('expression, expected', [\n    # Expression\n    (Expression(), True),\n\n    # All\n    (All(), True),\n    (All([Expression(), Expression()]), True),\n    (All([Expression(), Condition('test', operator.eq, 'value')]), False),\n\n    # Any\n    (Any(), False),\n    (Any([Expression(), Condition('test', operator.eq, 'value')]), True),\n    (Any([\n        Condition('test', operator.eq, 'value'),\n        Condition('other', operator.eq, 'value')\n    ]), False),\n\n    # Not\n    (Not(Expression()), False),\n    (Not(Not(Expression())), True)\n], ids=[\n    'Expression-always matches',\n\n    'All-no expressions always matches',\n    'All-all match',\n    'All-not all match',\n\n    'Any-no expressions never matches',\n    'Any-some match',\n    'Any-none match',\n\n    'Not-invert positive match',\n    'Not-double negative is positive match'\n])\ndef test_match(expression, candidate, expected):\n    '''Determine if candidate matches expression.'''\n    assert expression.match(candidate) is expected\n\n\ndef parametrize_test_condition_match(metafunc):\n    '''Parametrize condition_match tests.'''\n    identifiers = []\n    data = []\n\n    matrix = {\n        # Operator, match, no match\n        operator.eq: {\n            'match': 10, 'no-match': 20,\n            'wildcard-match': 'valu*', 'wildcard-no-match': 'values*'\n        },\n        operator.ne: {'match': 20, 'no-match': 10},\n        operator.ge: {'match': 10, 'no-match': 20},\n        operator.le: {'match': 10, 'no-match': 0},\n        operator.gt: {'match': 0, 'no-match': 10},\n        operator.lt: {'match': 20, 'no-match': 10}\n    }\n\n    for operator_function, values in matrix.items():\n        for value_label, value in values.items():\n            if value_label.startswith('wildcard'):\n                key_options = {\n                    'plain': 'name',\n                    'nested': 'change.name'\n                }\n            else:\n                key_options = {\n                    'plain': 'id',\n                    'nested': 'change.new_value'\n                }\n\n            for key_label, key in key_options.items():\n                identifiers.append('{} operator {} key {}'.format(\n                    operator_function.__name__, key_label, value_label\n                ))\n\n                data.append((\n                    key, operator_function, value,\n                    'no-match' not in value_label\n                ))\n\n    metafunc.parametrize(\n        'key, operator, value, expected', data, ids=identifiers\n    )\n\n\ndef test_condition_match(key, operator, value, candidate, expected):\n    '''Determine if candidate matches condition expression.'''\n    condition = Condition(key, operator, value)\n    assert condition.match(candidate) is expected\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/event/test_hub.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport inspect\nimport json\nimport os\nimport time\nimport subprocess\nimport sys\n\nimport pytest\n\nimport ftrack_api.event.hub\nimport ftrack_api.event.subscriber\nfrom ftrack_api.event.base import Event\nimport ftrack_api.exception\n\n\nclass MockClass(object):\n    '''Mock class for testing.'''\n\n    def method(self):\n        '''Mock method for testing.'''\n\n\ndef mockFunction():\n    '''Mock function for testing.'''\n\n\nclass MockConnection(object):\n    '''Mock connection for testing.'''\n\n    @property\n    def connected(self):\n        '''Return whether connected.'''\n        return True\n\n    def close(self):\n        '''Close mock connection.'''\n        pass\n\n\ndef assert_callbacks(hub, callbacks):\n    '''Assert hub has exactly *callbacks* subscribed.'''\n    # Subscribers always starts with internal handle_reply subscriber.\n    subscribers = hub._subscribers[:]\n    subscribers.pop(0)\n\n    if len(subscribers) != len(callbacks):\n        raise AssertionError(\n            'Number of subscribers ({0}) != number of callbacks ({1})'\n            .format(len(subscribers), len(callbacks))\n        )\n\n    for index, subscriber in enumerate(subscribers):\n        if subscriber.callback != callbacks[index]:\n            raise AssertionError(\n                'Callback at {0} != subscriber callback at same index.'\n                .format(index)\n            )\n\n\n@pytest.fixture()\ndef event_hub(request, session):\n    '''Return event hub to test against.\n\n    Hub is automatically connected at start of test and disconnected at end.\n\n    '''\n    hub = ftrack_api.event.hub.EventHub(\n        session.server_url, session.api_user, session.api_key\n    )\n    hub.connect()\n\n    def cleanup():\n        '''Cleanup.'''\n        if hub.connected:\n            hub.disconnect()\n\n    request.addfinalizer(cleanup)\n\n    return hub\n\n\n@pytest.mark.parametrize('server_url, expected', [\n    ('https://test.ftrackapp.com', 'https://test.ftrackapp.com'),\n    ('https://test.ftrackapp.com:9000', 'https://test.ftrackapp.com:9000')\n], ids=[\n    'with port',\n    'without port'\n])\ndef test_get_server_url(server_url, expected):\n    '''Return server url.'''\n    event_hub = ftrack_api.event.hub.EventHub(\n        server_url, 'user', 'key'\n    )\n    assert event_hub.get_server_url() == expected\n\n\n@pytest.mark.parametrize('server_url, expected', [\n    ('https://test.ftrackapp.com', 'test.ftrackapp.com'),\n    ('https://test.ftrackapp.com:9000', 'test.ftrackapp.com:9000')\n], ids=[\n    'with port',\n    'without port'\n])\ndef test_get_network_location(server_url, expected):\n    '''Return network location of server url.'''\n    event_hub = ftrack_api.event.hub.EventHub(\n        server_url, 'user', 'key'\n    )\n    assert event_hub.get_network_location() == expected\n\n\n@pytest.mark.parametrize('server_url, expected', [\n    ('https://test.ftrackapp.com', True),\n    ('http://test.ftrackapp.com', False)\n], ids=[\n    'secure',\n    'not secure'\n])\ndef test_secure_property(server_url, expected, mocker):\n    '''Return whether secure connection used.'''\n    event_hub = ftrack_api.event.hub.EventHub(\n        server_url, 'user', 'key'\n    )\n    assert event_hub.secure is expected\n\n\ndef test_connected_property(session):\n    '''Return connected state.'''\n    event_hub = ftrack_api.event.hub.EventHub(\n        session.server_url, session.api_user, session.api_key\n    )\n    assert event_hub.connected is False\n\n    event_hub.connect()\n    assert event_hub.connected is True\n\n    event_hub.disconnect()\n    assert event_hub.connected is False\n\n\n@pytest.mark.parametrize('server_url, expected', [\n    ('https://test.ftrackapp.com', 'https://test.ftrackapp.com'),\n    ('https://test.ftrackapp.com:9000', 'https://test.ftrackapp.com:9000'),\n    ('test.ftrackapp.com', ValueError),\n    ('https://:9000', ValueError),\n], ids=[\n    'with port',\n    'without port',\n    'missing scheme',\n    'missing hostname'\n])\ndef test_initialise_against_server_url(server_url, expected):\n    '''Initialise against server url.'''\n    if inspect.isclass(expected) and issubclass(expected, Exception):\n        with pytest.raises(expected):\n            ftrack_api.event.hub.EventHub(\n                server_url, 'user', 'key'\n            )\n    else:\n        event_hub = ftrack_api.event.hub.EventHub(\n            server_url, 'user', 'key'\n        )\n        assert event_hub.get_server_url() == expected\n\n\ndef test_connect(session):\n    '''Connect.'''\n    event_hub = ftrack_api.event.hub.EventHub(\n        session.server_url, session.api_user, session.api_key\n    )\n    event_hub.connect()\n\n    assert event_hub.connected is True\n    event_hub.disconnect()\n\n\ndef test_connect_when_already_connected(event_hub):\n    '''Fail to connect when already connected'''\n    assert event_hub.connected is True\n\n    with pytest.raises(ftrack_api.exception.EventHubConnectionError) as error:\n        event_hub.connect()\n\n    assert 'Already connected' in str(error)\n\n\ndef test_connect_failure(session, mocker):\n    '''Fail to connect to server.'''\n    event_hub = ftrack_api.event.hub.EventHub(\n        session.server_url, session.api_user, session.api_key\n    )\n\n    def force_fail(*args, **kwargs):\n        '''Force connection failure.'''\n        raise Exception('Forced fail.')\n\n    mocker.patch('websocket.create_connection', force_fail)\n    with pytest.raises(ftrack_api.exception.EventHubConnectionError):\n        event_hub.connect()\n\n\ndef test_connect_missing_required_transport(session, mocker, caplog):\n    '''Fail to connect to server that does not provide correct transport.'''\n    event_hub = ftrack_api.event.hub.EventHub(\n        session.server_url, session.api_user, session.api_key\n    )\n\n    original_get_socket_io_session = event_hub._get_socket_io_session\n\n    def _get_socket_io_session():\n        '''Patched to return no transports.'''\n        session = original_get_socket_io_session()\n        return ftrack_api.event.hub.SocketIoSession(\n            session[0], session[1], []\n        )\n\n    mocker.patch.object(\n        event_hub, '_get_socket_io_session', _get_socket_io_session\n    )\n\n    with pytest.raises(ftrack_api.exception.EventHubConnectionError):\n        event_hub.connect()\n\n    logs = caplog.records()\n    assert (\n        'Server does not support websocket sessions.' in str(logs[-1].exc_info)\n    )\n\n\ndef test_disconnect(event_hub):\n    '''Disconnect and unsubscribe all subscribers.'''\n    event_hub.disconnect()\n    assert len(event_hub._subscribers) == 0\n    assert event_hub.connected is False\n\n\ndef test_disconnect_without_unsubscribing(event_hub):\n    '''Disconnect without unsubscribing all subscribers.'''\n    event_hub.disconnect(unsubscribe=False)\n    assert len(event_hub._subscribers) > 0\n    assert event_hub.connected is False\n\n\ndef test_close_connection_from_manually_connected_hub(session_no_autoconnect_hub):\n    '''Close connection from manually connected hub.'''\n    session_no_autoconnect_hub.event_hub.connect()\n    session_no_autoconnect_hub.close()\n    assert session_no_autoconnect_hub.event_hub.connected is False\n\n\ndef test_disconnect_when_not_connected(session):\n    '''Fail to disconnect when not connected'''\n    event_hub = ftrack_api.event.hub.EventHub(\n        session.server_url, session.api_user, session.api_key\n    )\n    with pytest.raises(ftrack_api.exception.EventHubConnectionError) as error:\n        event_hub.disconnect()\n\n    assert 'Not currently connected' in str(error)\n\n\ndef test_reconnect(event_hub):\n    '''Reconnect successfully.'''\n    assert event_hub.connected is True\n    event_hub.reconnect()\n    assert event_hub.connected is True\n\n\ndef test_reconnect_when_not_connected(session):\n    '''Reconnect successfully even if not already connected.'''\n    event_hub = ftrack_api.event.hub.EventHub(\n        session.server_url, session.api_user, session.api_key\n    )\n    assert event_hub.connected is False\n\n    event_hub.reconnect()\n    assert event_hub.connected is True\n\n    event_hub.disconnect()\n\n\ndef test_fail_to_reconnect(session, mocker):\n    '''Fail to reconnect.'''\n    event_hub = ftrack_api.event.hub.EventHub(\n        session.server_url, session.api_user, session.api_key\n    )\n    event_hub.connect()\n    assert event_hub.connected is True\n\n    def force_fail(*args, **kwargs):\n        '''Force connection failure.'''\n        raise Exception('Forced fail.')\n\n    mocker.patch('websocket.create_connection', force_fail)\n\n    attempts = 2\n    with pytest.raises(ftrack_api.exception.EventHubConnectionError) as error:\n        event_hub.reconnect(attempts=attempts, delay=0.5)\n\n    assert 'Failed to reconnect to event server' in str(error)\n    assert 'after {} attempts'.format(attempts) in str(error)\n\n\ndef test_wait(event_hub):\n    '''Wait for event and handle as they arrive.'''\n    called = {'callback': False}\n\n    def callback(event):\n        called['callback'] = True\n\n    event_hub.subscribe('topic=test-subscribe', callback)\n\n    event_hub.publish(Event(topic='test-subscribe'))\n\n    # Until wait, the event should not have been processed even if received.\n    time.sleep(1)\n    assert called == {'callback': False}\n\n    event_hub.wait(2)\n    assert called == {'callback': True}\n\n\ndef test_wait_interrupted_by_disconnect(event_hub):\n    '''Interrupt wait loop with disconnect event.'''\n    wait_time = 5\n    start = time.time()\n\n    # Inject event directly for test purposes.\n    event = Event(topic='ftrack.meta.disconnected')\n    event_hub._event_queue.put(event)\n\n    event_hub.wait(wait_time)\n\n    assert time.time() - start < wait_time\n\n\n@pytest.mark.parametrize('identifier, registered', [\n    ('registered-test-subscriber', True),\n    ('unregistered-test-subscriber', False)\n], ids=[\n    'registered',\n    'missing'\n])\ndef test_get_subscriber_by_identifier(event_hub, identifier, registered):\n    '''Return subscriber by identifier.'''\n    def callback(event):\n        pass\n\n    subscriber = {\n        'id': 'registered-test-subscriber'\n    }\n\n    event_hub.subscribe('topic=test-subscribe', callback, subscriber)\n    retrieved = event_hub.get_subscriber_by_identifier(identifier)\n\n    if registered:\n        assert isinstance(retrieved, ftrack_api.event.subscriber.Subscriber)\n        assert retrieved.metadata.get('id') == subscriber['id']\n    else:\n        assert retrieved is None\n\n\ndef test_subscribe(event_hub):\n    '''Subscribe to topics.'''\n    called = {'a': False, 'b': False}\n\n    def callback_a(event):\n        called['a'] = True\n\n    def callback_b(event):\n        called['b'] = True\n\n    event_hub.subscribe('topic=test-subscribe', callback_a)\n    event_hub.subscribe('topic=test-subscribe-other', callback_b)\n\n    event_hub.publish(Event(topic='test-subscribe'))\n    event_hub.wait(2)\n\n    assert called == {'a': True, 'b': False}\n\n\ndef test_subscribe_before_connected(session):\n    '''Subscribe to topic before connected.'''\n    event_hub = ftrack_api.event.hub.EventHub(\n        session.server_url, session.api_user, session.api_key\n    )\n\n    called = {'callback': False}\n\n    def callback(event):\n        called['callback'] = True\n\n    identifier = 'test-subscriber'\n    event_hub.subscribe(\n        'topic=test-subscribe', callback, subscriber={'id': identifier}\n    )\n    assert event_hub.get_subscriber_by_identifier(identifier) is not None\n\n    event_hub.connect()\n\n    try:\n        event_hub.publish(Event(topic='test-subscribe'))\n        event_hub.wait(2)\n    finally:\n        event_hub.disconnect()\n\n    assert called == {'callback': True}\n\n\ndef test_duplicate_subscriber(event_hub):\n    '''Fail to subscribe same subscriber more than once.'''\n    subscriber = {'id': 'test-subscriber'}\n    event_hub.subscribe('topic=test', None, subscriber=subscriber)\n\n    with pytest.raises(ftrack_api.exception.NotUniqueError) as error:\n        event_hub.subscribe('topic=test', None, subscriber=subscriber)\n\n    assert '{0} already exists'.format(subscriber['id']) in str(error)\n\n\ndef test_unsubscribe(event_hub):\n    '''Unsubscribe a specific callback.'''\n    def callback_a(event):\n        pass\n\n    def callback_b(event):\n        pass\n\n    identifier_a = event_hub.subscribe('topic=test', callback_a)\n    identifier_b = event_hub.subscribe('topic=test', callback_b)\n\n    assert_callbacks(event_hub, [callback_a, callback_b])\n\n    event_hub.unsubscribe(identifier_a)\n\n    # Unsubscribe requires confirmation event so wait here to give event a\n    # chance to process.\n    time.sleep(5)\n\n    assert_callbacks(event_hub, [callback_b])\n\n\ndef test_unsubscribe_whilst_disconnected(event_hub):\n    '''Unsubscribe whilst disconnected.'''\n    identifier = event_hub.subscribe('topic=test', None)\n    event_hub.disconnect(unsubscribe=False)\n\n    event_hub.unsubscribe(identifier)\n    assert_callbacks(event_hub, [])\n\n\ndef test_unsubscribe_missing_subscriber(event_hub):\n    '''Fail to unsubscribe a non-subscribed subscriber.'''\n    identifier = 'non-subscribed-subscriber'\n    with pytest.raises(ftrack_api.exception.NotFoundError) as error:\n        event_hub.unsubscribe(identifier)\n\n    assert (\n        'missing subscriber with identifier {}'.format(identifier)\n        in str(error)\n    )\n\n\n@pytest.mark.parametrize('event_data', [\n    dict(source=dict(id='1', user=dict(username='auto'))),\n    dict(source=dict(user=dict(username='auto'))),\n    dict(source=dict(id='1')),\n    dict()\n], ids=[\n    'pre-prepared',\n    'missing id',\n    'missing user',\n    'no source'\n])\ndef test_prepare_event(session, event_data):\n    '''Prepare event.'''\n    # Replace username `auto` in event data with API user.\n    try:\n        if event_data['source']['user']['username'] == 'auto':\n            event_data['source']['user']['username'] = session.api_user\n    except KeyError:\n        pass\n\n    event_hub = ftrack_api.event.hub.EventHub(\n        session.server_url, session.api_user, session.api_key\n    )\n    event_hub.id = '1'\n\n    event = Event('test', id='event-id', **event_data)\n    expected = Event(\n        'test', id='event-id', source=dict(id='1', user=dict(username=session.api_user))\n    )\n    event_hub._prepare_event(event)\n    assert event == expected\n\n\ndef test_prepare_reply_event(session):\n    '''Prepare reply event.'''\n    event_hub = ftrack_api.event.hub.EventHub(\n        session.server_url, session.api_user, session.api_key\n    )\n\n    source_event = Event('source', source=dict(id='source-id'))\n    reply_event = Event('reply')\n\n    event_hub._prepare_reply_event(reply_event, source_event)\n    assert source_event['source']['id'] in reply_event['target']\n    assert reply_event['in_reply_to_event'] == source_event['id']\n\n    event_hub._prepare_reply_event(reply_event, source_event, {'id': 'source'})\n    assert reply_event['source'] == {'id': 'source'}\n\n\ndef test_publish(event_hub):\n    '''Publish asynchronous event.'''\n    called = {'callback': False}\n\n    def callback(event):\n        called['callback'] = True\n\n    event_hub.subscribe('topic=test-subscribe', callback)\n\n    event_hub.publish(Event(topic='test-subscribe'))\n    event_hub.wait(2)\n\n    assert called == {'callback': True}\n\n\ndef test_publish_raising_error(event_hub):\n    '''Raise error, when configured, on failed publish.'''\n    # Note that the event hub currently only fails publish when not connected.\n    # All other errors are inconsistently swallowed.\n    event_hub.disconnect()\n    event = Event(topic='a-topic', data=dict(status='fail'))\n\n    with pytest.raises(Exception):\n        event_hub.publish(event, on_error='raise')\n\n\ndef test_publish_ignoring_error(event_hub):\n    '''Ignore error, when configured, on failed publish.'''\n    # Note that the event hub currently only fails publish when not connected.\n    # All other errors are inconsistently swallowed.\n    event_hub.disconnect()\n    event = Event(topic='a-topic', data=dict(status='fail'))\n    event_hub.publish(event, on_error='ignore')\n\n\ndef test_publish_logs_other_errors(event_hub, caplog, mocker):\n    '''Log publish errors other than connection error.'''\n    # Mock connection to force error.\n    mocker.patch.object(event_hub, '_connection', MockConnection())\n\n    event = Event(topic='a-topic', data=dict(status='fail'))\n    event_hub.publish(event)\n\n    expected = 'Error sending event {0}.'.format(event)\n    messages = [record.getMessage().strip() for record in caplog.records()]\n    assert expected in messages, 'Expected log message missing in output.'\n\n\ndef test_synchronous_publish(event_hub):\n    '''Publish event synchronously and collect results.'''\n    def callback_a(event):\n        return 'A'\n\n    def callback_b(event):\n        return 'B'\n\n    def callback_c(event):\n        return 'C'\n\n    event_hub.subscribe('topic=test', callback_a, priority=50)\n    event_hub.subscribe('topic=test', callback_b, priority=60)\n    event_hub.subscribe('topic=test', callback_c, priority=70)\n\n    results = event_hub.publish(Event(topic='test'), synchronous=True)\n    assert results == ['A', 'B', 'C']\n\n\ndef test_publish_with_reply(event_hub):\n    '''Publish asynchronous event with on reply handler.'''\n\n    def replier(event):\n        '''Replier.'''\n        return 'Replied'\n\n    event_hub.subscribe('topic=test', replier)\n\n    called = {'callback': None}\n\n    def on_reply(event):\n        called['callback'] = event['data']\n\n    event_hub.publish(Event(topic='test'), on_reply=on_reply)\n    event_hub.wait(2)\n\n    assert called['callback'] == 'Replied'\n\n\ndef test_publish_with_multiple_replies(event_hub):\n    '''Publish asynchronous event and retrieve multiple replies.'''\n\n    def replier_one(event):\n        '''Replier.'''\n        return 'One'\n\n    def replier_two(event):\n        '''Replier.'''\n        return 'Two'\n\n    event_hub.subscribe('topic=test', replier_one)\n    event_hub.subscribe('topic=test', replier_two)\n\n    called = {'callback': []}\n\n    def on_reply(event):\n        called['callback'].append(event['data'])\n\n    event_hub.publish(Event(topic='test'), on_reply=on_reply)\n    event_hub.wait(2)\n\n    assert sorted(called['callback']) == ['One', 'Two']\n\n\n@pytest.mark.slow\ndef test_server_heartbeat_response():\n    '''Maintain connection by responding to server heartbeat request.'''\n    test_script = os.path.join(\n        os.path.dirname(__file__), 'event_hub_server_heartbeat.py'\n    )\n\n    # Start subscriber that will listen for all three messages.\n    subscriber = subprocess.Popen([sys.executable, test_script, 'subscribe'])\n\n    # Give subscriber time to connect to server.\n    time.sleep(10)\n\n    # Start publisher to publish three messages.\n    publisher = subprocess.Popen([sys.executable, test_script, 'publish'])\n\n    publisher.wait()\n    subscriber.wait()\n\n    assert subscriber.returncode == 0\n\n\ndef test_stop_event(event_hub):\n    '''Stop processing of subsequent local handlers when stop flag set.'''\n    called = {\n        'a': False,\n        'b': False,\n        'c': False\n    }\n\n    def callback_a(event):\n        called['a'] = True\n\n    def callback_b(event):\n        called['b'] = True\n        event.stop()\n\n    def callback_c(event):\n        called['c'] = True\n\n    event_hub.subscribe('topic=test', callback_a, priority=50)\n    event_hub.subscribe('topic=test', callback_b, priority=60)\n    event_hub.subscribe('topic=test', callback_c, priority=70)\n\n    event_hub.publish(Event(topic='test'))\n    event_hub.wait(2)\n\n    assert called == {\n        'a': True,\n        'b': True,\n        'c': False\n    }\n\n\ndef test_encode(session):\n    '''Encode event data.'''\n    encoded = session.event_hub._encode(\n        dict(name='ftrack.event', args=[Event('test')])\n    )\n    assert 'inReplyToEvent' in encoded\n    assert 'in_reply_to_event' not in encoded\n\n\ndef test_decode(session):\n    '''Decode event data.'''\n    decoded = session.event_hub._decode(\n        json.dumps({\n            'inReplyToEvent': 'id'\n        })\n    )\n\n    assert 'in_reply_to_event' in decoded\n    assert 'inReplyToEvent' not in decoded\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/event/test_subscriber.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport pytest\n\nimport ftrack_api.event.subscriber\nfrom ftrack_api.event.base import Event\n\n\ndef test_string_representation():\n    '''String representation.'''\n    subscriber = ftrack_api.event.subscriber.Subscriber(\n        'topic=test', lambda x: None, {'meta': 'info'}, 100\n    )\n\n    assert str(subscriber) == (\n        '<Subscriber metadata={\\'meta\\': \\'info\\'} subscription=\"topic=test\">'\n    )\n\n\n@pytest.mark.parametrize('expression, event, expected', [\n    ('topic=test', Event(topic='test'), True),\n    ('topic=test', Event(topic='other-test'), False)\n], ids=[\n    'interested',\n    'not interested'\n])\ndef test_interested_in(expression, event, expected):\n    '''Determine if subscriber interested in event.'''\n    subscriber = ftrack_api.event.subscriber.Subscriber(\n        expression, lambda x: None, {'meta': 'info'}, 100\n    )\n    assert subscriber.interested_in(event) is expected\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/event/test_subscription.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport pytest\n\nimport ftrack_api.event.subscription\nfrom ftrack_api.event.base import Event\n\n\ndef test_string_representation():\n    '''String representation is subscription expression.'''\n    expression = 'topic=some-topic'\n    subscription = ftrack_api.event.subscription.Subscription(expression)\n\n    assert str(subscription) == expression\n\n\n@pytest.mark.parametrize('expression, event, expected', [\n    ('topic=test', Event(topic='test'), True),\n    ('topic=test', Event(topic='other-test'), False)\n], ids=[\n    'match',\n    'no match'\n])\ndef test_includes(expression, event, expected):\n    '''Subscription includes event.'''\n    subscription = ftrack_api.event.subscription.Subscription(expression)\n    assert subscription.includes(event) is expected\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/resource_identifier_transformer/__init__.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/resource_identifier_transformer/test_base.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport pytest\n\nimport ftrack_api.resource_identifier_transformer.base as _transformer\n\n\n@pytest.fixture()\ndef transformer(session):\n    '''Return instance of ResourceIdentifierTransformer.'''\n    return _transformer.ResourceIdentifierTransformer(session)\n\n\n@pytest.mark.parametrize('resource_identifier, context, expected', [\n    ('identifier', None, 'identifier'),\n    ('identifier', {'user': {'username': 'user'}}, 'identifier')\n], ids=[\n    'no context',\n    'basic context'\n])\ndef test_encode(transformer, resource_identifier, context, expected):\n    '''Encode resource identifier.'''\n    assert transformer.encode(resource_identifier, context) == expected\n\n\n@pytest.mark.parametrize('resource_identifier, context, expected', [\n    ('identifier', None, 'identifier'),\n    ('identifier', {'user': {'username': 'user'}}, 'identifier')\n], ids=[\n    'no context',\n    'basic context'\n])\ndef test_decode(transformer, resource_identifier, context, expected):\n    '''Encode resource identifier.'''\n    assert transformer.decode(resource_identifier, context) == expected\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/structure/__init__.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/structure/test_base.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport pytest\n\nimport ftrack_api.structure.base\n\n\nclass Concrete(ftrack_api.structure.base.Structure):\n    '''Concrete implementation to allow testing non-abstract methods.'''\n\n    def get_resource_identifier(self, entity, context=None):\n        '''Return a resource identifier for supplied *entity*.\n\n        *context* can be a mapping that supplies additional information.\n\n        '''\n        return 'resource_identifier'\n\n\n@pytest.mark.parametrize('sequence, expected', [\n    ({'padding': None}, '%d'),\n    ({'padding': 4}, '%04d')\n], ids=[\n    'no padding',\n    'padded'\n])\ndef test_get_sequence_expression(sequence, expected):\n    '''Get sequence expression from sequence.'''\n    structure = Concrete()\n    assert structure._get_sequence_expression(sequence) == expected\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/structure/test_entity_id.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport inspect\n\nimport pytest\nimport mock\n\nimport ftrack_api\nimport ftrack_api.structure.entity_id\n\n\n@pytest.fixture(scope='session')\ndef structure():\n    '''Return structure.'''\n    return ftrack_api.structure.entity_id.EntityIdStructure()\n\n\n# Note: When it is possible to use indirect=True on just a few arguments, the\n# called functions here can change to standard fixtures.\n# https://github.com/pytest-dev/pytest/issues/579\n\ndef valid_entity():\n    '''Return valid entity.'''\n    session = ftrack_api.Session()\n\n    entity = session.create('FileComponent', {\n        'id': 'f6cd40cb-d1c0-469f-a2d5-10369be8a724',\n        'name': 'file_component',\n        'file_type': '.png'\n    })\n\n    return entity\n\n\n@pytest.mark.parametrize('entity, context, expected', [\n    (valid_entity(), {}, 'f6cd40cb-d1c0-469f-a2d5-10369be8a724'),\n    (mock.Mock(), {}, Exception)\n], ids=[\n    'valid-entity',\n    'non-entity'\n])\ndef test_get_resource_identifier(structure, entity, context, expected):\n    '''Get resource identifier.'''\n    if inspect.isclass(expected) and issubclass(expected, Exception):\n        with pytest.raises(expected):\n            structure.get_resource_identifier(entity, context)\n    else:\n        assert structure.get_resource_identifier(entity, context) == expected\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/structure/test_id.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport inspect\n\nimport pytest\n\nimport ftrack_api\nimport ftrack_api.structure.id\n\n\n@pytest.fixture(scope='session')\ndef structure():\n    '''Return structure.'''\n    return ftrack_api.structure.id.IdStructure(prefix='path')\n\n\n# Note: When it is possible to use indirect=True on just a few arguments, the\n# called functions here can change to standard fixtures.\n# https://github.com/pytest-dev/pytest/issues/579\n\ndef file_component(container=None):\n    '''Return file component.'''\n    session = ftrack_api.Session()\n\n    entity = session.create('FileComponent', {\n        'id': 'f6cd40cb-d1c0-469f-a2d5-10369be8a724',\n        'name': '0001',\n        'file_type': '.png',\n        'container': container\n    })\n\n    return entity\n\n\ndef sequence_component(padding=0):\n    '''Return sequence component with *padding*.'''\n    session = ftrack_api.Session()\n\n    entity = session.create('SequenceComponent', {\n        'id': 'ff17edad-2129-483b-8b59-d1a654c8497b',\n        'name': 'sequence_component',\n        'file_type': '.png',\n        'padding': padding\n    })\n\n    return entity\n\n\ndef container_component():\n    '''Return container component.'''\n    session = ftrack_api.Session()\n\n    entity = session.create('ContainerComponent', {\n        'id': '03ab9967-f86c-4b55-8252-cd187d0c244a',\n        'name': 'container_component'\n    })\n\n    return entity\n\n\ndef unsupported_entity():\n    '''Return an unsupported entity.'''\n    session = ftrack_api.Session()\n\n    entity = session.create('User', {\n        'username': 'martin'\n    })\n\n    return entity\n\n\n@pytest.mark.parametrize('entity, context, expected', [\n    (\n        file_component(), {},\n        'path/f/6/c/d/40cb-d1c0-469f-a2d5-10369be8a724.png'\n    ),\n    (\n        file_component(container_component()), {},\n        'path/0/3/a/b/9967-f86c-4b55-8252-cd187d0c244a/'\n        'f6cd40cb-d1c0-469f-a2d5-10369be8a724.png'\n    ),\n    (\n        file_component(sequence_component()), {},\n        'path/f/f/1/7/edad-2129-483b-8b59-d1a654c8497b/file.0001.png'\n    ),\n    (\n        sequence_component(padding=0), {},\n        'path/f/f/1/7/edad-2129-483b-8b59-d1a654c8497b/file.%d.png'\n    ),\n    (\n        sequence_component(padding=4), {},\n        'path/f/f/1/7/edad-2129-483b-8b59-d1a654c8497b/file.%04d.png'\n    ),\n    (\n        container_component(), {},\n        'path/0/3/a/b/9967-f86c-4b55-8252-cd187d0c244a'\n    ),\n    (unsupported_entity(), {}, NotImplementedError)\n], ids=[\n    'file-component',\n    'file-component-in-container',\n    'file-component-in-sequence',\n    'unpadded-sequence-component',\n    'padded-sequence-component',\n    'container-component',\n    'unsupported-entity'\n])\ndef test_get_resource_identifier(structure, entity, context, expected):\n    '''Get resource identifier.'''\n    if inspect.isclass(expected) and issubclass(expected, Exception):\n        with pytest.raises(expected):\n            structure.get_resource_identifier(entity, context)\n    else:\n        assert structure.get_resource_identifier(entity, context) == expected\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/structure/test_origin.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport inspect\n\nimport pytest\nimport mock\n\nimport ftrack_api.structure.origin\n\n\n@pytest.fixture(scope='session')\ndef structure():\n    '''Return structure.'''\n    return ftrack_api.structure.origin.OriginStructure()\n\n\n@pytest.mark.parametrize('entity, context, expected', [\n    (mock.Mock(), {'source_resource_identifier': 'identifier'}, 'identifier'),\n    (mock.Mock(), {}, ValueError),\n    (mock.Mock(), None, ValueError)\n], ids=[\n    'valid-context',\n    'invalid-context',\n    'unspecified-context'\n])\ndef test_get_resource_identifier(structure, entity, context, expected):\n    '''Get resource identifier.'''\n    if inspect.isclass(expected) and issubclass(expected, Exception):\n        with pytest.raises(expected):\n            structure.get_resource_identifier(entity, context)\n    else:\n        assert structure.get_resource_identifier(entity, context) == expected\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/structure/test_standard.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport uuid\n\nimport pytest\n\nimport ftrack_api\nimport ftrack_api.structure.standard\n\n\n@pytest.fixture(scope='session')\ndef new_project(request):\n    '''Return new empty project.'''\n    session = ftrack_api.Session()\n\n    project_schema = session.query('ProjectSchema').first()\n    project_name = 'python_api_test_{0}'.format(uuid.uuid1().hex)\n    project = session.create('Project', {\n        'name': project_name,\n        'full_name': project_name + '_full',\n        'project_schema': project_schema\n    })\n\n    session.commit()\n\n    def cleanup():\n        '''Remove created entity.'''\n        session.delete(project)\n        session.commit()\n\n    request.addfinalizer(cleanup)\n\n    return project\n\n\ndef new_container_component():\n    '''Return container component.'''\n    session = ftrack_api.Session()\n\n    entity = session.create('ContainerComponent', {\n        'name': 'container_component'\n    })\n\n    return entity\n\n\ndef new_sequence_component():\n    '''Return sequence component.'''\n    session = ftrack_api.Session()\n\n    entity = session.create_component(\n        '/tmp/foo/%04d.jpg [1-10]', location=None, data={'name': 'baz'}\n    )\n\n    return entity\n\n\ndef new_file_component(name='foo', container=None):\n    '''Return file component with *name* and *container*.'''\n    if container:\n        session = container.session\n    else:\n        session = ftrack_api.Session()\n\n    entity = session.create('FileComponent', {\n        'name': name,\n        'file_type': '.png',\n        'container': container\n    })\n\n    return entity\n\n\n# Reusable fixtures.\nfile_component = new_file_component()\ncontainer_component = new_container_component()\nsequence_component = new_sequence_component()\n\n\n# Note: to improve test performance the same project is reused throughout the\n# tests. This means that all hierarchical names must be unique, otherwise an\n# IntegrityError will be raised on the server.\n\n@pytest.mark.parametrize(\n    'component, hierarchy, expected, structure, asset_name',\n    [\n        (\n            file_component,\n            [],\n            '{project_name}/my_new_asset/v001/foo.png',\n            ftrack_api.structure.standard.StandardStructure(),\n            'my_new_asset'\n        ),\n        (\n            file_component,\n            [],\n            '{project_name}/foobar/my_new_asset/v001/foo.png',\n            ftrack_api.structure.standard.StandardStructure(\n                project_versions_prefix='foobar'\n            ),\n            'my_new_asset'\n        ),\n        (\n            file_component,\n            ['baz1', 'bar'],\n            '{project_name}/baz1/bar/my_new_asset/v001/foo.png',\n            ftrack_api.structure.standard.StandardStructure(),\n            'my_new_asset'\n        ),\n        (\n            sequence_component,\n            ['baz2', 'bar'],\n            '{project_name}/baz2/bar/my_new_asset/v001/baz.%04d.jpg',\n            ftrack_api.structure.standard.StandardStructure(),\n            'my_new_asset'\n        ),\n        (\n            sequence_component['members'][3],\n            ['baz3', 'bar'],\n            '{project_name}/baz3/bar/my_new_asset/v001/baz.0004.jpg',\n            ftrack_api.structure.standard.StandardStructure(),\n            'my_new_asset'\n        ),\n        (\n            container_component,\n            ['baz4', 'bar'],\n            '{project_name}/baz4/bar/my_new_asset/v001/container_component',\n            ftrack_api.structure.standard.StandardStructure(),\n            'my_new_asset'\n        ),\n        (\n            new_file_component(container=container_component),\n            ['baz5', 'bar'],\n            (\n                '{project_name}/baz5/bar/my_new_asset/v001/container_component/'\n                'foo.png'\n            ),\n            ftrack_api.structure.standard.StandardStructure(),\n            'my_new_asset'\n        ),\n        (\n            file_component,\n            [u'björn'],\n            '{project_name}/bjorn/my_new_asset/v001/foo.png',\n            ftrack_api.structure.standard.StandardStructure(),\n            'my_new_asset'\n        ),\n        (\n            file_component,\n            [u'björn!'],\n            '{project_name}/bjorn_/my_new_asset/v001/foo.png',\n            ftrack_api.structure.standard.StandardStructure(),\n            'my_new_asset'\n        ),\n        (\n            new_file_component(name=u'fää'),\n            [],\n            '{project_name}/my_new_asset/v001/faa.png',\n            ftrack_api.structure.standard.StandardStructure(),\n            'my_new_asset'\n        ),\n        (\n            new_file_component(name=u'fo/o'),\n            [],\n            '{project_name}/my_new_asset/v001/fo_o.png',\n            ftrack_api.structure.standard.StandardStructure(),\n            'my_new_asset'\n        ),\n        (\n            file_component,\n            [],\n            '{project_name}/aao/v001/foo.png',\n            ftrack_api.structure.standard.StandardStructure(),\n            u'åäö'\n        ),\n        (\n            file_component,\n            [],\n            '{project_name}/my_ne____w_asset/v001/foo.png',\n            ftrack_api.structure.standard.StandardStructure(),\n            u'my_ne!!!!w_asset'\n        ),\n        (\n            file_component,\n            [u'björn2'],\n            u'{project_name}/björn2/my_new_asset/v001/foo.png',\n            ftrack_api.structure.standard.StandardStructure(\n                illegal_character_substitute=None\n            ),\n            'my_new_asset'\n        ),\n        (\n            file_component,\n            [u'bj!rn'],\n            '{project_name}/bj^rn/my_new_asset/v001/foo.png',\n            ftrack_api.structure.standard.StandardStructure(\n                illegal_character_substitute='^'\n            ),\n            'my_new_asset'\n        )\n    ], ids=[\n        'file_component_on_project',\n        'file_component_on_project_with_prefix',\n        'file_component_with_hierarchy',\n        'sequence_component',\n        'sequence_component_member',\n        'container_component',\n        'container_component_member',\n        'slugify_non_ascii_hierarchy',\n        'slugify_illegal_hierarchy',\n        'slugify_non_ascii_component_name',\n        'slugify_illegal_component_name',\n        'slugify_non_ascii_asset_name',\n        'slugify_illegal_asset_name',\n        'slugify_none',\n        'slugify_other_character'\n    ]\n)\ndef test_get_resource_identifier(\n    component, hierarchy, expected, structure, asset_name, new_project\n):\n    '''Get resource identifier.'''\n    session = component.session\n\n    # Create structure, asset and version.\n    context_id = new_project['id']\n    for name in hierarchy:\n        context_id = session.create('Folder', {\n            'name': name,\n            'project_id': new_project['id'],\n            'parent_id': context_id\n        })['id']\n\n    asset = session.create(\n        'Asset', {'name': asset_name, 'context_id': context_id}\n    )\n    version = session.create('AssetVersion', {'asset': asset})\n\n    # Update component with version.\n    if component['container']:\n        component['container']['version'] = version\n    else:\n        component['version'] = version\n\n    session.commit()\n\n    assert structure.get_resource_identifier(component) == expected.format(\n        project_name=new_project['name']\n    )\n\n\ndef test_unsupported_entity(user):\n    '''Fail to get resource identifier for unsupported entity.'''\n    structure = ftrack_api.structure.standard.StandardStructure()\n    with pytest.raises(NotImplementedError):\n        structure.get_resource_identifier(user)\n\n\ndef test_component_without_version_relation(new_project):\n    '''Get an identifer for component without a version relation.'''\n    session = new_project.session\n\n    asset = session.create(\n        'Asset', {'name': 'foo', 'context_id': new_project['id']}\n    )\n    version = session.create('AssetVersion', {'asset': asset})\n\n    session.commit()\n\n    file_component = new_file_component()\n    file_component['version_id'] = version['id']\n\n    structure = ftrack_api.structure.standard.StandardStructure()\n    structure.get_resource_identifier(file_component)\n\n\ndef test_component_without_committed_version_relation():\n    '''Fail to get an identifer for component without a committed version.'''\n    file_component = new_file_component()\n    session = file_component.session\n    version = session.create('AssetVersion', {})\n\n    file_component['version'] = version\n\n    structure = ftrack_api.structure.standard.StandardStructure()\n\n    with pytest.raises(ftrack_api.exception.StructureError):\n        structure.get_resource_identifier(file_component)\n\n\n@pytest.mark.xfail(\n    raises=ftrack_api.exception.ServerError, \n    reason='Due to user permission errors.'\n)\ndef test_component_without_committed_asset_relation():\n    '''Fail to get an identifer for component without a committed asset.'''\n    file_component = new_file_component()\n    session = file_component.session\n    version = session.create('AssetVersion', {})\n\n    file_component['version'] = version\n\n    session.commit()\n\n    structure = ftrack_api.structure.standard.StandardStructure()\n\n    with pytest.raises(ftrack_api.exception.StructureError):\n        structure.get_resource_identifier(file_component)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_attribute.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport pytest\n\nimport ftrack_api.attribute\nimport ftrack_api.exception\n\n\n@pytest.mark.parametrize('attributes', [\n    [],\n    [ftrack_api.attribute.Attribute('test')]\n], ids=[\n    'no initial attributes',\n    'with initial attributes'\n])\ndef test_initialise_attributes_collection(attributes):\n    '''Initialise attributes collection.'''\n    attribute_collection = ftrack_api.attribute.Attributes(attributes)\n    assert sorted(list(attribute_collection)) == sorted(attributes)\n\n\ndef test_add_attribute_to_attributes_collection():\n    '''Add valid attribute to attributes collection.'''\n    attribute_collection = ftrack_api.attribute.Attributes()\n    attribute = ftrack_api.attribute.Attribute('test')\n\n    assert attribute_collection.keys() == []\n    attribute_collection.add(attribute)\n    assert attribute_collection.keys() == ['test']\n\n\ndef test_add_duplicate_attribute_to_attributes_collection():\n    '''Fail to add attribute with duplicate name to attributes collection.'''\n    attribute_collection = ftrack_api.attribute.Attributes()\n    attribute = ftrack_api.attribute.Attribute('test')\n\n    attribute_collection.add(attribute)\n    with pytest.raises(ftrack_api.exception.NotUniqueError):\n        attribute_collection.add(attribute)\n\n\ndef test_remove_attribute_from_attributes_collection():\n    '''Remove attribute from attributes collection.'''\n    attribute_collection = ftrack_api.attribute.Attributes()\n    attribute = ftrack_api.attribute.Attribute('test')\n\n    attribute_collection.add(attribute)\n    assert len(attribute_collection) == 1\n\n    attribute_collection.remove(attribute)\n    assert len(attribute_collection) == 0\n\n\ndef test_remove_missing_attribute_from_attributes_collection():\n    '''Fail to remove attribute not present in attributes collection.'''\n    attribute_collection = ftrack_api.attribute.Attributes()\n    attribute = ftrack_api.attribute.Attribute('test')\n\n    with pytest.raises(KeyError):\n        attribute_collection.remove(attribute)\n\n\ndef test_get_attribute_from_attributes_collection():\n    '''Get attribute from attributes collection.'''\n    attribute_collection = ftrack_api.attribute.Attributes()\n    attribute = ftrack_api.attribute.Attribute('test')\n    attribute_collection.add(attribute)\n\n    retrieved_attribute = attribute_collection.get('test')\n\n    assert retrieved_attribute is attribute\n\n\ndef test_get_missing_attribute_from_attributes_collection():\n    '''Get attribute not present in attributes collection.'''\n    attribute_collection = ftrack_api.attribute.Attributes()\n    assert attribute_collection.get('test') is None\n\n\n@pytest.mark.parametrize('attributes, expected', [\n    ([], []),\n    ([ftrack_api.attribute.Attribute('test')], ['test'])\n], ids=[\n    'no initial attributes',\n    'with initial attributes'\n])\ndef test_attribute_collection_keys(attributes, expected):\n    '''Retrieve keys for attribute collection.'''\n    attribute_collection = ftrack_api.attribute.Attributes(attributes)\n    assert sorted(attribute_collection.keys()) == sorted(expected)\n\n\n@pytest.mark.parametrize('attribute, expected', [\n    (None, False),\n    (ftrack_api.attribute.Attribute('b'), True),\n    (ftrack_api.attribute.Attribute('c'), False)\n], ids=[\n    'none attribute',\n    'present attribute',\n    'missing attribute'\n])\ndef test_attributes_collection_contains(attribute, expected):\n    '''Check presence in attributes collection.'''\n    attribute_collection = ftrack_api.attribute.Attributes([\n        ftrack_api.attribute.Attribute('a'),\n        ftrack_api.attribute.Attribute('b')\n    ])\n\n    assert (attribute in attribute_collection) is expected\n\n\n@pytest.mark.parametrize('attributes, expected', [\n    ([], 0),\n    ([ftrack_api.attribute.Attribute('test')], 1),\n    (\n        [\n            ftrack_api.attribute.Attribute('a'),\n            ftrack_api.attribute.Attribute('b')\n        ],\n        2\n    )\n], ids=[\n    'no attributes',\n    'single attribute',\n    'multiple attributes'\n])\ndef test_attributes_collection_count(attributes, expected):\n    '''Count attributes in attributes collection.'''\n    attribute_collection = ftrack_api.attribute.Attributes(attributes)\n    assert len(attribute_collection) == expected\n\n\ndef test_iterate_over_attributes_collection():\n    '''Iterate over attributes collection.'''\n    attributes = [\n        ftrack_api.attribute.Attribute('a'),\n        ftrack_api.attribute.Attribute('b')\n    ]\n\n    attribute_collection = ftrack_api.attribute.Attributes(attributes)\n    for attribute in attribute_collection:\n        attributes.remove(attribute)\n\n    assert len(attributes) == 0\n\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_cache.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport os\nimport uuid\nimport tempfile\n\nimport pytest\n\nimport ftrack_api.cache\n\n\n@pytest.fixture(params=['proxy', 'layered', 'memory', 'file', 'serialised'])\ndef cache(request):\n    '''Return cache.'''\n    if request.param == 'proxy':\n        cache = ftrack_api.cache.ProxyCache(\n            ftrack_api.cache.MemoryCache()\n        )\n\n    elif request.param == 'layered':\n        cache = ftrack_api.cache.LayeredCache(\n            [ftrack_api.cache.MemoryCache()]\n        )\n\n    elif request.param == 'memory':\n        cache = ftrack_api.cache.MemoryCache()\n\n    elif request.param == 'file':\n        cache_path = os.path.join(\n            tempfile.gettempdir(), '{0}.dbm'.format(uuid.uuid4().hex)\n        )\n\n        cache = ftrack_api.cache.FileCache(cache_path)\n\n        def cleanup():\n            '''Cleanup.'''\n            try:\n                os.remove(cache_path)\n            except OSError:\n                # BSD DB (Mac OSX) implementation of the interface will append\n                # a .db extension.\n                os.remove(cache_path + '.db')\n\n        request.addfinalizer(cleanup)\n\n    elif request.param == 'serialised':\n        cache = ftrack_api.cache.SerialisedCache(\n            ftrack_api.cache.MemoryCache(),\n            encode=lambda value: value,\n            decode=lambda value: value\n        )\n\n    else:\n        raise ValueError(\n            'Unrecognised cache fixture type {0!r}'.format(request.param)\n        )\n\n    return cache\n\n\n\nclass Class(object):\n    '''Class for testing.'''\n\n    def method(self, key):\n        '''Method for testing.'''\n\n\ndef function(mutable, x, y=2):\n    '''Function for testing.'''\n    mutable['called'] = True\n    return {'result': x + y}\n\n\ndef assert_memoised_call(\n    memoiser, function, expected, args=None, kw=None, memoised=True\n):\n    '''Assert *function* call via *memoiser* was *memoised*.'''\n    mapping = {'called': False}\n    if args is not None:\n        args = (mapping,) + args\n    else:\n        args = (mapping,)\n\n    result = memoiser.call(function, args, kw)\n\n    assert result == expected\n    assert mapping['called'] is not memoised\n\n\ndef test_get(cache):\n    '''Retrieve item from cache.'''\n    cache.set('key', 'value')\n    assert cache.get('key') == 'value'\n\n\ndef test_get_missing_key(cache):\n    '''Fail to retrieve missing item from cache.'''\n    with pytest.raises(KeyError):\n        cache.get('key')\n\n\ndef test_set(cache):\n    '''Set item in cache.'''\n    with pytest.raises(KeyError):\n        cache.get('key')\n\n    cache.set('key', 'value')\n    assert cache.get('key') == 'value'\n\n\ndef test_remove(cache):\n    '''Remove item from cache.'''\n    cache.set('key', 'value')\n    cache.remove('key')\n\n    with pytest.raises(KeyError):\n        cache.get('key')\n\n\ndef test_remove_missing_key(cache):\n    '''Fail to remove missing key.'''\n    with pytest.raises(KeyError):\n        cache.remove('key')\n\n\ndef test_keys(cache):\n    '''Retrieve keys of items in cache.'''\n    assert cache.keys() == []\n    cache.set('a', 'a_value')\n    cache.set('b', 'b_value')\n    cache.set('c', 'c_value')\n    assert sorted(cache.keys()) == sorted(['a', 'b', 'c'])\n\n\ndef test_clear(cache):\n    '''Remove items from cache.'''\n    cache.set('a', 'a_value')\n    cache.set('b', 'b_value')\n    cache.set('c', 'c_value')\n\n    assert cache.keys()\n    cache.clear()\n\n    assert not cache.keys()\n\n\ndef test_clear_using_pattern(cache):\n    '''Remove items that match pattern from cache.'''\n    cache.set('matching_key', 'value')\n    cache.set('another_matching_key', 'value')\n    cache.set('key_not_matching', 'value')\n\n    assert cache.keys()\n    cache.clear(pattern='.*matching_key$')\n\n    assert cache.keys() == ['key_not_matching']\n\n\ndef test_clear_encountering_missing_key(cache, mocker):\n    '''Clear missing key.'''\n    # Force reporting keys that are not actually valid for test purposes.\n    mocker.patch.object(cache, 'keys', lambda: ['missing'])\n    assert cache.keys() == ['missing']\n\n    # Should not error even though key not valid.\n    cache.clear()\n\n    # The key was not successfully removed so should still be present.\n    assert cache.keys() == ['missing']\n\n\ndef test_layered_cache_propagates_value_on_get():\n    '''Layered cache propagates value on get.'''\n    caches = [\n        ftrack_api.cache.MemoryCache(),\n        ftrack_api.cache.MemoryCache(),\n        ftrack_api.cache.MemoryCache()\n    ]\n\n    cache = ftrack_api.cache.LayeredCache(caches)\n\n    # Set item on second level cache only.\n    caches[1].set('key', 'value')\n\n    # Retrieving key via layered cache should propagate it automatically to\n    # higher level caches only.\n    assert cache.get('key') == 'value'\n    assert caches[0].get('key') == 'value'\n\n    with pytest.raises(KeyError):\n        caches[2].get('key')\n\n\ndef test_layered_cache_remove_at_depth():\n    '''Remove key that only exists at depth in LayeredCache.'''\n    caches = [\n        ftrack_api.cache.MemoryCache(),\n        ftrack_api.cache.MemoryCache()\n    ]\n\n    cache = ftrack_api.cache.LayeredCache(caches)\n\n    # Set item on second level cache only.\n    caches[1].set('key', 'value')\n\n    # Removing key that only exists at depth should not raise key error.\n    cache.remove('key')\n\n    # Ensure key was removed.\n    assert not cache.keys()\n\n\ndef test_expand_references():\n    '''Test that references are expanded from serialized cache.'''\n\n    cache_path = os.path.join(\n        tempfile.gettempdir(), '{0}.dbm'.format(uuid.uuid4().hex)\n    )\n\n    def make_cache(session, cache_path):\n        '''Create a serialised file cache.'''\n        serialized_file_cache = ftrack_api.cache.SerialisedCache(\n            ftrack_api.cache.FileCache(cache_path),\n            encode=session.encode,\n            decode=session.decode\n        )\n\n        return serialized_file_cache\n\n    # Populate the serialized file cache.\n    session = ftrack_api.Session(\n        cache=lambda session, cache_path=cache_path:make_cache(\n            session, cache_path\n        )\n    )\n\n    expanded_results = dict()\n\n    query_string = 'select asset.parent from AssetVersion where asset is_not None limit 10'\n\n    for sequence in session.query(query_string):\n        asset = sequence.get('asset')\n\n        expanded_results.setdefault(\n            asset.get('id'), asset.get('parent')\n        )\n\n    # Fetch the data from cache.\n    new_session = ftrack_api.Session(\n        cache=lambda session, cache_path=cache_path:make_cache(\n            session, cache_path\n        )\n    )\n\n\n    new_session_two = ftrack_api.Session(\n        cache=lambda session, cache_path=cache_path:make_cache(\n            session, cache_path\n        )\n    )\n\n\n    # Make sure references are merged.\n    for sequence in new_session.query(query_string):\n        asset = sequence.get('asset')\n\n        assert (\n            asset.get('parent') == expanded_results[asset.get('id')]\n        )\n\n        # Use for fetching directly using get.\n        assert (\n            new_session_two.get(asset.entity_type, asset.get('id')).get('parent') ==\n            expanded_results[asset.get('id')]\n        )\n\n\n\n@pytest.mark.parametrize('items, key', [\n    (({},), '{}'),\n    (({}, {}), '{}{}')\n], ids=[\n    'single object',\n    'multiple objects'\n])\ndef test_string_key_maker_key(items, key):\n    '''Generate key using string key maker.'''\n    key_maker = ftrack_api.cache.StringKeyMaker()\n    assert key_maker.key(*items) == key\n\n\n@pytest.mark.parametrize('items, key', [\n    (\n        ({},),\n        '\\x01\\x01'\n    ),\n    (\n        ({'a': 'b'}, [1, 2]),\n        '\\x01'\n            '\\x80\\x02U\\x01a.' '\\x02' '\\x80\\x02U\\x01b.'\n        '\\x01'\n        '\\x00'\n        '\\x03'\n            '\\x80\\x02K\\x01.' '\\x00' '\\x80\\x02K\\x02.'\n        '\\x03'\n    ),\n    (\n        (function,),\n        '\\x04function\\x00unit.test_cache'\n    ),\n    (\n        (Class,),\n        '\\x04Class\\x00unit.test_cache'\n    ),\n    (\n        (Class.method,),\n        '\\x04method\\x00Class\\x00unit.test_cache'\n    ),\n    (\n        (callable,),\n        '\\x04callable'\n    )\n], ids=[\n    'single mapping',\n    'multiple objects',\n    'function',\n    'class',\n    'method',\n    'builtin'\n])\ndef test_object_key_maker_key(items, key):\n    '''Generate key using string key maker.'''\n    key_maker = ftrack_api.cache.ObjectKeyMaker()\n    assert key_maker.key(*items) == key\n\n\ndef test_memoised_call():\n    '''Call memoised function.'''\n    memoiser = ftrack_api.cache.Memoiser()\n\n    # Initial call should not be memoised so function is executed.\n    assert_memoised_call(\n        memoiser, function, args=(1,), expected={'result': 3}, memoised=False\n    )\n\n    # Identical call should be memoised so function is not executed again.\n    assert_memoised_call(\n        memoiser, function, args=(1,), expected={'result': 3}, memoised=True\n    )\n\n    # Differing call is not memoised so function is executed.\n    assert_memoised_call(\n        memoiser, function, args=(3,), expected={'result': 5}, memoised=False\n    )\n\n\ndef test_memoised_call_variations():\n    '''Call memoised function with identical arguments using variable format.'''\n    memoiser = ftrack_api.cache.Memoiser()\n    expected = {'result': 3}\n\n    # Call function once to ensure is memoised.\n    assert_memoised_call(\n        memoiser, function, args=(1,), expected=expected, memoised=False\n    )\n\n    # Each of the following calls should equate to the same key and make\n    # use of the memoised value.\n    for args, kw in [\n        ((), {'x': 1}),\n        ((), {'x': 1, 'y': 2}),\n        ((1,), {'y': 2}),\n        ((1,), {})\n    ]:\n        assert_memoised_call(\n            memoiser, function, args=args, kw=kw, expected=expected\n        )\n\n    # The following calls should all be treated as new variations and so\n    # not use any memoised value.\n    assert_memoised_call(\n        memoiser, function, kw={'x': 2}, expected={'result': 4}, memoised=False\n    )\n    assert_memoised_call(\n        memoiser, function, kw={'x': 3, 'y': 2}, expected={'result': 5},\n        memoised=False\n    )\n    assert_memoised_call(\n        memoiser, function, args=(4, ), kw={'y': 2}, expected={'result': 6},\n        memoised=False\n    )\n    assert_memoised_call(\n        memoiser, function, args=(5, ), expected={'result': 7}, memoised=False\n    )\n\n\ndef test_memoised_mutable_return_value():\n    '''Avoid side effects for returned mutable arguments when memoising.'''\n    memoiser = ftrack_api.cache.Memoiser()\n    arguments = ({'called': False}, 1)\n\n    result_a = memoiser.call(function, arguments)\n    assert result_a == {'result': 3}\n    assert arguments[0]['called']\n\n    # Modify mutable externally and check that stored memoised value is\n    # unchanged.\n    del result_a['result']\n\n    arguments[0]['called'] = False\n    result_b = memoiser.call(function, arguments)\n\n    assert result_b == {'result': 3}\n    assert not arguments[0]['called']\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_collection.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport copy\nimport uuid\n\nimport mock\nimport pytest\n\nimport ftrack_api.collection\nimport ftrack_api.symbol\nimport ftrack_api.inspection\nimport ftrack_api.exception\nimport ftrack_api.operation\n\n\ndef create_mock_entity(session):\n    '''Return new mock entity for *session*.'''\n    entity = mock.MagicMock()\n    entity.session = session\n    entity.primary_key_attributes = ['id']\n    entity['id'] = str(uuid.uuid4())\n    return entity\n\n\n@pytest.fixture\ndef mock_entity(session):\n    '''Return mock entity.'''\n    return create_mock_entity(session)\n\n\n@pytest.fixture\ndef mock_entities(session):\n    '''Return list of two mock entities.'''\n    return [\n        create_mock_entity(session),\n        create_mock_entity(session)\n    ]\n\n\n@pytest.fixture\ndef mock_attribute():\n    '''Return mock attribute.'''\n    attribute = mock.MagicMock()\n    attribute.name = 'test'\n    return attribute\n\n\ndef test_collection_initialisation_does_not_modify_entity_state(\n    mock_entity, mock_attribute, mock_entities\n):\n    '''Initialising collection does not modify entity state.'''\n    ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities\n    )\n\n    assert ftrack_api.inspection.state(mock_entity) is ftrack_api.symbol.NOT_SET\n\n\ndef test_immutable_collection_initialisation(\n    mock_entity, mock_attribute, mock_entities\n):\n    '''Initialise immutable collection.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities, mutable=False\n    )\n\n    assert list(collection) == mock_entities\n    assert collection.mutable is False\n\n\ndef test_collection_shallow_copy(\n    mock_entity, mock_attribute, mock_entities, session\n):\n    '''Shallow copying collection should avoid indirect mutation.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities\n    )\n\n    with mock_entity.session.operation_recording(False):\n        collection_copy = copy.copy(collection)\n        new_entity = create_mock_entity(session)\n        collection_copy.append(new_entity)\n\n    assert list(collection) == mock_entities\n    assert list(collection_copy) == mock_entities + [new_entity]\n\n\ndef test_collection_insert(\n    mock_entity, mock_attribute, mock_entities, session\n):\n    '''Insert a value into collection.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities\n    )\n\n    new_entity = create_mock_entity(session)\n    collection.insert(0, new_entity)\n    assert list(collection) == [new_entity] + mock_entities\n\n\ndef test_collection_insert_duplicate(\n    mock_entity, mock_attribute, mock_entities\n):\n    '''Fail to insert a duplicate value into collection.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities\n    )\n\n    with pytest.raises(ftrack_api.exception.DuplicateItemInCollectionError):\n        collection.insert(0, mock_entities[1])\n\n\ndef test_immutable_collection_insert(\n    mock_entity, mock_attribute, mock_entities, session\n):\n    '''Fail to insert a value into immutable collection.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities, mutable=False\n    )\n\n    with pytest.raises(ftrack_api.exception.ImmutableCollectionError):\n        collection.insert(0, create_mock_entity(session))\n\n\ndef test_collection_set_item(\n    mock_entity, mock_attribute, mock_entities, session\n):\n    '''Set item at index in collection.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities\n    )\n\n    new_entity = create_mock_entity(session)\n    collection[0] = new_entity\n    assert list(collection) == [new_entity, mock_entities[1]]\n\n\ndef test_collection_re_set_item(\n    mock_entity, mock_attribute, mock_entities\n):\n    '''Re-set value at exact same index in collection.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities\n    )\n\n    collection[0] = mock_entities[0]\n    assert list(collection) == mock_entities\n\n\ndef test_collection_set_duplicate_item(\n    mock_entity, mock_attribute, mock_entities\n):\n    '''Fail to set a duplicate value into collection at different index.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities\n    )\n\n    with pytest.raises(ftrack_api.exception.DuplicateItemInCollectionError):\n        collection[0] = mock_entities[1]\n\n\ndef test_immutable_collection_set_item(\n    mock_entity, mock_attribute, mock_entities\n):\n    '''Fail to set item at index in immutable collection.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities, mutable=False\n    )\n\n    with pytest.raises(ftrack_api.exception.ImmutableCollectionError):\n        collection[0] = mock_entities[0]\n\n\ndef test_collection_delete_item(\n    mock_entity, mock_attribute, mock_entities\n):\n    '''Remove item at index from collection.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities\n    )\n    del collection[0]\n    assert list(collection) == [mock_entities[1]]\n\n\ndef test_collection_delete_item_at_invalid_index(\n    mock_entity, mock_attribute, mock_entities\n):\n    '''Fail to remove item at missing index from immutable collection.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities\n    )\n\n    with pytest.raises(IndexError):\n        del collection[4]\n\n\ndef test_immutable_collection_delete_item(\n    mock_entity, mock_attribute, mock_entities\n):\n    '''Fail to remove item at index from immutable collection.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities, mutable=False\n    )\n\n    with pytest.raises(ftrack_api.exception.ImmutableCollectionError):\n        del collection[0]\n\n\ndef test_collection_count(\n    mock_entity, mock_attribute, mock_entities, session\n):\n    '''Count items in collection.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities\n    )\n    assert len(collection) == 2\n\n    collection.append(create_mock_entity(session))\n    assert len(collection) == 3\n\n    del collection[0]\n    assert len(collection) == 2\n\n\n@pytest.mark.parametrize('other, expected', [\n    ([], False),\n    ([1, 2], True),\n    ([1, 2, 3], False),\n    ([1], False)\n], ids=[\n    'empty',\n    'same',\n    'additional',\n    'missing'\n])\ndef test_collection_equal(mocker, mock_entity, mock_attribute, other, expected):\n    '''Determine collection equality against another collection.'''\n    # Temporarily override determination of entity identity so that it works\n    # against simple scalar values for purpose of test.\n    mocker.patch.object(\n        ftrack_api.inspection, 'identity', lambda entity: str(entity)\n    )\n\n    collection_a = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=[1, 2]\n    )\n\n    collection_b = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=other\n    )\n    assert (collection_a == collection_b) is expected\n\n\ndef test_collection_not_equal_to_non_collection(\n    mocker, mock_entity, mock_attribute\n):\n    '''Collection not equal to a non-collection.'''\n    # Temporarily override determination of entity identity so that it works\n    # against simple scalar values for purpose of test.\n    mocker.patch.object(\n        ftrack_api.inspection, 'identity', lambda entity: str(entity)\n    )\n\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=[1, 2]\n    )\n\n    assert (collection != {}) is True\n\n\ndef test_collection_notify_on_modification(\n    mock_entity, mock_attribute, mock_entities, session\n):\n    '''Record UpdateEntityOperation on collection modification.'''\n    collection = ftrack_api.collection.Collection(\n        mock_entity, mock_attribute, data=mock_entities\n    )\n    assert len(session.recorded_operations) == 0\n\n    collection.append(create_mock_entity(session))\n    assert len(session.recorded_operations) == 1\n    operation = session.recorded_operations.pop()\n    assert isinstance(operation, ftrack_api.operation.UpdateEntityOperation)\n    assert operation.new_value == collection\n\n\ndef test_mapped_collection_proxy_shallow_copy(new_project, unique_name):\n    '''Shallow copying mapped collection proxy avoids indirect mutation.'''\n    metadata = new_project['metadata']\n\n    with new_project.session.operation_recording(False):\n        metadata_copy = copy.copy(metadata)\n        metadata_copy[unique_name] = True\n\n    assert unique_name not in metadata\n    assert unique_name in metadata_copy\n\n\ndef test_mapped_collection_proxy_mutable_property(new_project):\n    '''Mapped collection mutable property maps to underlying collection.'''\n    metadata = new_project['metadata']\n\n    assert metadata.mutable is True\n    assert metadata.collection.mutable is True\n\n    metadata.mutable = False\n    assert metadata.collection.mutable is False\n\n\ndef test_mapped_collection_proxy_attribute_property(\n    new_project, mock_attribute\n):\n    '''Mapped collection attribute property maps to underlying collection.'''\n    metadata = new_project['metadata']\n\n    assert metadata.attribute is metadata.collection.attribute\n\n    metadata.attribute = mock_attribute\n    assert metadata.collection.attribute is mock_attribute\n\n\ndef test_mapped_collection_proxy_get_item(new_project, unique_name):\n    '''Retrieve item in mapped collection proxy.'''\n    session = new_project.session\n\n    # Prepare data.\n    metadata = new_project['metadata']\n    value = 'value'\n    metadata[unique_name] = value\n    session.commit()\n\n    # Check in clean session retrieval of value.\n    session.reset()\n    retrieved = session.get(*ftrack_api.inspection.identity(new_project))\n\n    assert retrieved is not new_project\n    assert retrieved['metadata'].keys() == [unique_name]\n    assert retrieved['metadata'][unique_name] == value\n\n\ndef test_mapped_collection_proxy_set_item(new_project, unique_name):\n    '''Set new item in mapped collection proxy.'''\n    session = new_project.session\n\n    metadata = new_project['metadata']\n    assert unique_name not in metadata\n\n    value = 'value'\n    metadata[unique_name] = value\n    assert metadata[unique_name] == value\n\n    # Check change persisted correctly.\n    session.commit()\n    session.reset()\n    retrieved = session.get(*ftrack_api.inspection.identity(new_project))\n\n    assert retrieved is not new_project\n    assert retrieved['metadata'].keys() == [unique_name]\n    assert retrieved['metadata'][unique_name] == value\n\n\ndef test_mapped_collection_proxy_update_item(new_project, unique_name):\n    '''Update existing item in mapped collection proxy.'''\n    session = new_project.session\n\n    # Prepare a pre-existing value.\n    metadata = new_project['metadata']\n    value = 'value'\n    metadata[unique_name] = value\n    session.commit()\n\n    # Set new value.\n    new_value = 'new_value'\n    metadata[unique_name] = new_value\n\n    # Confirm change persisted correctly.\n    session.commit()\n    session.reset()\n    retrieved = session.get(*ftrack_api.inspection.identity(new_project))\n\n    assert retrieved is not new_project\n    assert retrieved['metadata'].keys() == [unique_name]\n    assert retrieved['metadata'][unique_name] == new_value\n\n\ndef test_mapped_collection_proxy_delete_item(new_project, unique_name):\n    '''Remove existing item from mapped collection proxy.'''\n    session = new_project.session\n\n    # Prepare a pre-existing value to remove.\n    metadata = new_project['metadata']\n    value = 'value'\n    metadata[unique_name] = value\n    session.commit()\n\n    # Now remove value.\n    del new_project['metadata'][unique_name]\n    assert unique_name not in new_project['metadata']\n\n    # Confirm change persisted correctly.\n    session.commit()\n    session.reset()\n    retrieved = session.get(*ftrack_api.inspection.identity(new_project))\n\n    assert retrieved is not new_project\n    assert retrieved['metadata'].keys() == []\n    assert unique_name not in retrieved['metadata']\n\n\ndef test_mapped_collection_proxy_delete_missing_item(new_project, unique_name):\n    '''Fail to remove item for missing key from mapped collection proxy.'''\n    metadata = new_project['metadata']\n    assert unique_name not in metadata\n    with pytest.raises(KeyError):\n        del metadata[unique_name]\n\n\ndef test_mapped_collection_proxy_iterate_keys(new_project, unique_name):\n    '''Iterate over keys in mapped collection proxy.'''\n    metadata = new_project['metadata']\n    metadata.update({\n        'a': 'value-a',\n        'b': 'value-b',\n        'c': 'value-c'\n    })\n\n    # Commit here as otherwise cleanup operation will fail because transaction\n    # will include updating metadata to refer to a deleted entity.\n    new_project.session.commit()\n\n    iterated = set()\n    for key in metadata:\n        iterated.add(key)\n\n    assert iterated == set(['a', 'b', 'c'])\n\n\ndef test_mapped_collection_proxy_count(new_project, unique_name):\n    '''Count items in mapped collection proxy.'''\n    metadata = new_project['metadata']\n    metadata.update({\n        'a': 'value-a',\n        'b': 'value-b',\n        'c': 'value-c'\n    })\n\n    # Commit here as otherwise cleanup operation will fail because transaction\n    # will include updating metadata to refer to a deleted entity.\n    new_project.session.commit()\n\n    assert len(metadata) == 3\n\n\ndef test_mapped_collection_on_create(session, unique_name, project):\n    '''Test that it is possible to set relational attributes on create'''\n    metadata = {\n        'a': 'value-a',\n        'b': 'value-b',\n        'c': 'value-c'\n    }\n\n    task_id = session.create(\n        'Task', {\n            'name': unique_name,\n            'parent': project,\n            'metadata': metadata,\n\n        }\n    ).get('id')\n\n    session.commit()\n\n    # Reset the session and check that we have the expected\n    # values.\n    session.reset()\n\n    task = session.get(\n        'Task', task_id\n    )\n\n    for key, value in metadata.items():\n        assert value == task['metadata'][key]\n\n\ndef test_collection_refresh(new_asset_version, new_component):\n    '''Test collection reload.'''\n    session_two = ftrack_api.Session(auto_connect_event_hub=False)\n\n    query_string = 'select components from AssetVersion where id is \"{0}\"'.format(\n        new_asset_version.get('id')\n    )\n\n    # Fetch the new asset version in a new session.\n    new_asset_version_two = session_two.query(\n        query_string\n    ).one()\n\n    # Modify our asset version\n    new_asset_version.get('components').append(\n        new_component\n    )\n\n    new_asset_version.session.commit()\n\n    # Query the same asset version again and make sure we get the newly\n    # populated data.\n    session_two.query(\n        query_string\n    ).all()\n\n    assert (\n        new_asset_version.get('components') == new_asset_version_two.get('components')\n    )\n\n    # Make a local change to our asset version\n    new_asset_version_two.get('components').pop()\n\n    # Query the same asset version again and make sure our local changes\n    # are not overwritten.\n\n    session_two.query(\n        query_string\n    ).all()\n\n    assert len(new_asset_version_two.get('components')) == 0\n\n\ndef test_mapped_collection_reload(new_asset_version):\n    '''Test mapped collection reload.'''\n    session_two = ftrack_api.Session(auto_connect_event_hub=False)\n\n    query_string = 'select metadata from AssetVersion where id is \"{0}\"'.format(\n        new_asset_version.get('id')\n    )\n\n    # Fetch the new asset version in a new session.\n    new_asset_version_two = session_two.query(\n        query_string\n    ).one()\n\n    # Modify our asset version\n    new_asset_version['metadata']['test'] = str(uuid.uuid4())\n\n    new_asset_version.session.commit()\n\n    # Query the same asset version again and make sure we get the newly\n    # populated data.\n    session_two.query(\n        query_string\n    ).all()\n\n    assert (\n        new_asset_version['metadata']['test'] == new_asset_version_two['metadata']['test']\n    )\n\n    local_data = str(uuid.uuid4())\n\n    new_asset_version_two['metadata']['test'] = local_data\n\n    # Modify our asset version again\n    new_asset_version['metadata']['test'] = str(uuid.uuid4())\n\n    new_asset_version.session.commit()\n\n    # Query the same asset version again and make sure our local changes\n    # are not overwritten.\n    session_two.query(\n        query_string\n    ).all()\n\n    assert (\n        new_asset_version_two['metadata']['test'] == local_data\n    )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_custom_attribute.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport uuid\n\nimport pytest\n\nimport ftrack_api\n\n@pytest.fixture(\n    params=[\n        'AssetVersion', 'Shot', 'AssetVersionList', 'TypedContextList', 'User',\n        'Asset'\n    ]\n)\ndef new_entity_and_custom_attribute(request, session):\n    '''Return tuple with new entity, custom attribute name and value.'''\n    if request.param == 'AssetVersion':\n        entity = session.create(\n            request.param, {\n                'asset': session.query('Asset').first()\n            }\n        )\n        return (entity, 'versiontest', 123)\n\n    elif request.param == 'Shot':\n        sequence = session.query('Sequence').first()\n        entity = session.create(\n            request.param, {\n                'parent_id': sequence['id'],\n                'project_id': sequence['project_id'],\n                'name': str(uuid.uuid1())\n            }\n        )\n        return (entity, 'fstart', 1005)\n\n    elif request.param == 'Asset':\n        shot = session.query('Shot').first()\n        entity = session.create(\n            request.param, {\n                'context_id': shot['project_id'],\n                'name': str(uuid.uuid1())\n            }\n        )\n        return (entity, 'htest', 1005)\n\n    elif request.param in ('AssetVersionList', 'TypedContextList'):\n        entity = session.create(\n            request.param, {\n                'project_id': session.query('Project').first()['id'],\n                'category_id': session.query('ListCategory').first()['id'],\n                'name': str(uuid.uuid1())\n            }\n        )\n        return (entity, 'listbool', True)\n\n    elif request.param == 'User':\n        entity = session.create(\n            request.param, {\n                'first_name': 'Custom attribute test',\n                'last_name': 'Custom attribute test',\n                'username': str(uuid.uuid1())\n            }\n        )\n        return (entity, 'teststring', 'foo')\n\n\n@pytest.mark.parametrize(\n    'entity_type, entity_model_name, custom_attribute_name',\n    [\n        ('Task', 'task', 'customNumber'),\n        ('AssetVersion', 'assetversion', 'NumberField')\n    ],\n    ids=[\n        'task',\n        'asset_version'\n    ]\n)\ndef test_read_set_custom_attribute(\n    session, entity_type, entity_model_name, custom_attribute_name\n):\n    '''Retrieve custom attribute value set on instance.'''\n    custom_attribute_value = session.query(\n        'CustomAttributeValue where configuration.key is '\n        '{custom_attribute_name}'\n        .format(\n            custom_attribute_name=custom_attribute_name\n        )\n    ).first()\n\n    entity = session.query(\n        'select custom_attributes from {entity_type} where id is '\n        '{entity_id}'.format(\n            entity_type=entity_type,\n            entity_id=custom_attribute_value['entity_id'],\n        )\n    ).first()\n\n    assert custom_attribute_value\n\n    assert entity['id'] == entity['custom_attributes'].collection.entity['id']\n    assert entity is entity['custom_attributes'].collection.entity\n    assert (\n        entity['custom_attributes'][custom_attribute_name] ==\n        custom_attribute_value['value']\n    )\n\n    assert custom_attribute_name in entity['custom_attributes'].keys()\n\n\n@pytest.mark.parametrize(\n    'entity_type, custom_attribute_name',\n    [\n        ('Task', 'customNumber'),\n        ('Shot', 'fstart'),\n        (\n            'AssetVersion', 'NumberField'\n        )\n    ],\n    ids=[\n        'task',\n        'shot',\n        'asset_version'\n    ]\n)\ndef test_write_set_custom_attribute_value(\n    session, entity_type, custom_attribute_name\n):\n    '''Overwrite existing instance level custom attribute value.'''\n    entity = session.query(\n        'select custom_attributes from {entity_type} where '\n        'custom_attributes.configuration.key is {custom_attribute_name}'.format(\n            entity_type=entity_type,\n            custom_attribute_name=custom_attribute_name\n        )\n    ).first()\n\n    entity['custom_attributes'][custom_attribute_name] = 42\n\n    assert entity['custom_attributes'][custom_attribute_name] == 42\n\n    session.commit()\n\n\n@pytest.mark.parametrize(\n    'entity_type, custom_attribute_name',\n    [\n        ('Task', 'fstart'),\n        ('Shot', 'Not existing'),\n        ('AssetVersion', 'fstart')\n    ],\n    ids=[\n        'task',\n        'shot',\n        'asset_version'\n    ]\n)\ndef test_read_custom_attribute_that_does_not_exist(\n    session, entity_type, custom_attribute_name\n):\n    '''Fail to read value from a custom attribute that does not exist.'''\n    entity = session.query(\n        'select custom_attributes from {entity_type}'.format(\n            entity_type=entity_type\n        )\n    ).first()\n\n    with pytest.raises(KeyError):\n        entity['custom_attributes'][custom_attribute_name]\n\n\n@pytest.mark.parametrize(\n    'entity_type, custom_attribute_name',\n    [\n        ('Task', 'fstart'),\n        ('Shot', 'Not existing'),\n        ('AssetVersion', 'fstart')\n    ],\n    ids=[\n        'task',\n        'shot',\n        'asset_version'\n    ]\n)\ndef test_write_custom_attribute_that_does_not_exist(\n    session, entity_type, custom_attribute_name\n):\n    '''Fail to write a value to a custom attribute that does not exist.'''\n    entity = session.query(\n        'select custom_attributes from {entity_type}'.format(\n            entity_type=entity_type\n        )\n    ).first()\n\n    with pytest.raises(KeyError):\n        entity['custom_attributes'][custom_attribute_name] = 42\n\n\ndef test_set_custom_attribute_on_new_but_persisted_version(\n    session, new_asset_version\n):\n    '''Set custom attribute on new persisted version.'''\n    new_asset_version['custom_attributes']['versiontest'] = 5\n    session.commit()\n\n\n@pytest.mark.xfail(\n    raises=ftrack_api.exception.ServerError, \n    reason='Due to user permission errors.'\n)\ndef test_batch_create_entity_and_custom_attributes(\n    new_entity_and_custom_attribute\n):\n    '''Write custom attribute value and entity in the same batch.'''\n    entity, name, value = new_entity_and_custom_attribute\n    session = entity.session\n    entity['custom_attributes'][name] = value\n\n    assert entity['custom_attributes'][name] == value\n    session.commit()\n\n    assert entity['custom_attributes'][name] == value\n\n\ndef test_refresh_custom_attribute(new_asset_version):\n    '''Test custom attribute refresh.'''\n    session_two = ftrack_api.Session()\n\n    query_string = 'select custom_attributes from AssetVersion where id is \"{0}\"'.format(\n        new_asset_version.get('id')\n    )\n\n    asset_version_two = session_two.query(\n        query_string\n    ).first()\n\n    new_asset_version['custom_attributes']['versiontest'] = 42\n\n    new_asset_version.session.commit()\n\n    asset_version_two = session_two.query(\n        query_string\n    ).first()\n\n    assert (\n        new_asset_version['custom_attributes']['versiontest'] ==\n        asset_version_two['custom_attributes']['versiontest']\n    )\n\n\n\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_data.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport os\nimport tempfile\n\nimport pytest\n\nimport ftrack_api.data\n\n\n@pytest.fixture()\ndef content():\n    '''Return initial content.'''\n    return 'test data'\n\n\n@pytest.fixture(params=['file', 'file_wrapper', 'string'])\ndef data(request, content):\n    '''Return cache.'''\n\n    if request.param == 'string':\n        data_object = ftrack_api.data.String(content)\n\n    elif request.param == 'file':\n        file_handle, path = tempfile.mkstemp()\n        file_object = os.fdopen(file_handle, 'r+')\n        file_object.write(content)\n        file_object.flush()\n        file_object.close()\n\n        data_object = ftrack_api.data.File(path, 'r+')\n\n        def cleanup():\n            '''Cleanup.'''\n            data_object.close()\n            os.remove(path)\n\n        request.addfinalizer(cleanup)\n\n    elif request.param == 'file_wrapper':\n        file_handle, path = tempfile.mkstemp()\n        file_object = os.fdopen(file_handle, 'r+')\n        file_object.write(content)\n        file_object.seek(0)\n\n        data_object = ftrack_api.data.FileWrapper(file_object)\n\n        def cleanup():\n            '''Cleanup.'''\n            data_object.close()\n            os.remove(path)\n\n        request.addfinalizer(cleanup)\n\n    else:\n        raise ValueError('Unrecognised parameter: {0}'.format(request.param))\n\n    return data_object\n\n\ndef test_read(data, content):\n    '''Return content from current position up to *limit*.'''\n    assert data.read(5) == content[:5]\n    assert data.read() == content[5:]\n\n\ndef test_write(data, content):\n    '''Write content at current position.'''\n    assert data.read() == content\n    data.write('more test data')\n    data.seek(0)\n    assert data.read() == content + 'more test data'\n\n\ndef test_flush(data):\n    '''Flush buffers ensuring data written.'''\n    # TODO: Implement better test than just calling function.\n    data.flush()\n\n\ndef test_seek(data, content):\n    '''Move internal pointer to *position*.'''\n    data.seek(5)\n    assert data.read() == content[5:]\n\n\ndef test_tell(data):\n    '''Return current position of internal pointer.'''\n    assert data.tell() == 0\n    data.seek(5)\n    assert data.tell() == 5\n\n\ndef test_close(data):\n    '''Flush buffers and prevent further access.'''\n    data.close()\n    with pytest.raises(ValueError) as error:\n        data.read()\n\n    assert 'I/O operation on closed file' in str(error.value)\n\n\nclass Dummy(ftrack_api.data.Data):\n    '''Dummy string.'''\n\n    def read(self, limit=None):\n        '''Return content from current position up to *limit*.'''\n\n    def write(self, content):\n        '''Write content at current position.'''\n\n\ndef test_unsupported_tell():\n    '''Fail when tell unsupported.'''\n    data = Dummy()\n    with pytest.raises(NotImplementedError) as error:\n        data.tell()\n\n    assert 'Tell not supported' in str(error.value)\n\n\ndef test_unsupported_seek():\n    '''Fail when seek unsupported.'''\n    data = Dummy()\n    with pytest.raises(NotImplementedError) as error:\n        data.seek(5)\n\n    assert 'Seek not supported' in str(error.value)\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_formatter.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport termcolor\n\nimport ftrack_api.formatter\n\n\ndef colored(text, *args, **kwargs):\n    '''Pass through so there are no escape sequences in output.'''\n    return text\n\n\ndef test_format(user, mocker):\n    '''Return formatted representation of entity.'''\n    mocker.patch.object(termcolor, 'colored', colored)\n\n    result = ftrack_api.formatter.format(user)\n\n    # Cannot test entire string as too variable so check for key text.\n    assert result.startswith('User\\n')\n    assert ' username: jenkins' in result\n    assert ' email: ' in result\n\n\ndef test_format_using_custom_formatters(user):\n    '''Return formatted representation of entity using custom formatters.'''\n    result = ftrack_api.formatter.format(\n        user, formatters={\n            'header': lambda text: '*{0}*'.format(text),\n            'label': lambda text: '-{0}'.format(text)\n        }\n    )\n\n    # Cannot test entire string as too variable so check for key text.\n    assert result.startswith('*User*\\n')\n    assert ' -username: jenkins' in result\n    assert ' -email: ' in result\n\n\ndef test_format_filtering(new_user, mocker):\n    '''Return formatted representation using custom filter.'''\n    mocker.patch.object(termcolor, 'colored', colored)\n\n    with new_user.session.auto_populating(False):\n        result = ftrack_api.formatter.format(\n            new_user,\n            attribute_filter=ftrack_api.formatter.FILTER['ignore_unset']\n        )\n\n    # Cannot test entire string as too variable so check for key text.\n    assert result.startswith('User\\n')\n    assert ' username: {0}'.format(new_user['username']) in result\n    assert ' email: ' not in result\n\n\ndef test_format_recursive(user, mocker):\n    '''Return formatted recursive representation.'''\n    mocker.patch.object(termcolor, 'colored', colored)\n\n    user.session.populate(user, 'timelogs.user')\n\n    with user.session.auto_populating(False):\n        result = ftrack_api.formatter.format(user, recursive=True)\n\n    # Cannot test entire string as too variable so check for key text.\n    assert result.startswith('User\\n')\n    assert ' username: jenkins'\n    assert ' timelogs: Timelog' in result\n    assert ' user: User{...}' in result\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_inspection.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2014 ftrack\n\nimport ftrack_api.inspection\nimport ftrack_api.symbol\n\n\ndef test_identity(user):\n    '''Retrieve identity of *user*.'''\n    identity = ftrack_api.inspection.identity(user)\n    assert identity[0] == 'User'\n    assert identity[1] == ['d07ae5d0-66e1-11e1-b5e9-f23c91df25eb']\n\n\ndef test_primary_key(user):\n    '''Retrieve primary key of *user*.'''\n    primary_key = ftrack_api.inspection.primary_key(user)\n    assert primary_key == {\n        'id': 'd07ae5d0-66e1-11e1-b5e9-f23c91df25eb'\n    }\n\n\ndef test_created_entity_state(session, unique_name):\n    '''Created entity has CREATED state.'''\n    new_user = session.create('User', {'username': unique_name})\n    assert ftrack_api.inspection.state(new_user) is ftrack_api.symbol.CREATED\n\n    # Even after a modification the state should remain as CREATED.\n    new_user['username'] = 'changed'\n    assert ftrack_api.inspection.state(new_user) is ftrack_api.symbol.CREATED\n\n\ndef test_retrieved_entity_state(user):\n    '''Retrieved entity has NOT_SET state.'''\n    assert ftrack_api.inspection.state(user) is ftrack_api.symbol.NOT_SET\n\n\ndef test_modified_entity_state(user):\n    '''Modified entity has MODIFIED state.'''\n    user['username'] = 'changed'\n    assert ftrack_api.inspection.state(user) is ftrack_api.symbol.MODIFIED\n\n\ndef test_deleted_entity_state(session, user):\n    '''Deleted entity has DELETED state.'''\n    session.delete(user)\n    assert ftrack_api.inspection.state(user) is ftrack_api.symbol.DELETED\n\n\ndef test_post_commit_entity_state(session, unique_name):\n    '''Entity has NOT_SET state post commit.'''\n    new_user = session.create('User', {'username': unique_name})\n    assert ftrack_api.inspection.state(new_user) is ftrack_api.symbol.CREATED\n\n    session.commit()\n\n    assert ftrack_api.inspection.state(new_user) is ftrack_api.symbol.NOT_SET\n\n\ndef test_states(session, unique_name, user):\n    '''Determine correct states for multiple entities.'''\n    # NOT_SET\n    user_a = session.create('User', {'username': unique_name})\n    session.commit()\n\n    # CREATED\n    user_b = session.create('User', {'username': unique_name})\n    user_b['username'] = 'changed'\n\n    # MODIFIED\n    user_c = user\n    user_c['username'] = 'changed'\n\n    # DELETED\n    user_d = session.create('User', {'username': unique_name})\n    session.delete(user_d)\n\n    # Assert states.\n    states = ftrack_api.inspection.states([user_a, user_b, user_c, user_d])\n\n    assert states == [\n        ftrack_api.symbol.NOT_SET,\n        ftrack_api.symbol.CREATED,\n        ftrack_api.symbol.MODIFIED,\n        ftrack_api.symbol.DELETED\n    ]\n\n\ndef test_states_for_no_entities():\n    '''Return empty list of states when no entities passed.'''\n    states = ftrack_api.inspection.states([])\n    assert states == []\n\n\ndef test_skip_operations_for_non_inspected_entities(session, unique_name):\n    '''Skip operations for non inspected entities.'''\n    user_a = session.create('User', {'username': unique_name + '-1'})\n    user_b = session.create('User', {'username': unique_name + '-2'})\n\n    states = ftrack_api.inspection.states([user_a])\n    assert states == [ftrack_api.symbol.CREATED]\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_operation.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport ftrack_api.operation\n\n\ndef test_operations_initialise():\n    '''Initialise empty operations stack.'''\n    operations = ftrack_api.operation.Operations()\n    assert len(operations) == 0\n\n\ndef test_operations_push():\n    '''Push new operation onto stack.'''\n    operations = ftrack_api.operation.Operations()\n    assert len(operations) == 0\n\n    operation = ftrack_api.operation.Operation()\n    operations.push(operation)\n    assert list(operations)[-1] is operation\n\n\ndef test_operations_pop():\n    '''Pop and return operation from stack.'''\n    operations = ftrack_api.operation.Operations()\n    assert len(operations) == 0\n\n    operations.push(ftrack_api.operation.Operation())\n    operations.push(ftrack_api.operation.Operation())\n    operation = ftrack_api.operation.Operation()\n    operations.push(operation)\n\n    assert len(operations) == 3\n    popped = operations.pop()\n    assert popped is operation\n    assert len(operations) == 2\n\n\ndef test_operations_count():\n    '''Count operations in stack.'''\n    operations = ftrack_api.operation.Operations()\n    assert len(operations) == 0\n\n    operations.push(ftrack_api.operation.Operation())\n    assert len(operations) == 1\n\n    operations.pop()\n    assert len(operations) == 0\n\n\ndef test_operations_clear():\n    '''Clear operations stack.'''\n    operations = ftrack_api.operation.Operations()\n    operations.push(ftrack_api.operation.Operation())\n    operations.push(ftrack_api.operation.Operation())\n    operations.push(ftrack_api.operation.Operation())\n    assert len(operations) == 3\n\n    operations.clear()\n    assert len(operations) == 0\n\n\ndef test_operations_iter():\n    '''Iterate over operations stack.'''\n    operations = ftrack_api.operation.Operations()\n    operation_a = ftrack_api.operation.Operation()\n    operation_b = ftrack_api.operation.Operation()\n    operation_c = ftrack_api.operation.Operation()\n\n    operations.push(operation_a)\n    operations.push(operation_b)\n    operations.push(operation_c)\n\n    assert len(operations) == 3\n    for operation, expected in zip(\n        operations, [operation_a, operation_b, operation_c]\n    ):\n        assert operation is expected\n\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_package.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport ftrack_api\n\n\nclass Class(object):\n    '''Class.'''\n\n\nclass Mixin(object):\n    '''Mixin.'''\n\n    def method(self):\n        '''Method.'''\n        return True\n\n\ndef test_mixin():\n    '''Mixin class to instance.'''\n    instance_a = Class()\n    instance_b = Class()\n\n    assert not hasattr(instance_a, 'method')\n    assert not hasattr(instance_b, 'method')\n\n    ftrack_api.mixin(instance_a, Mixin)\n\n    assert hasattr(instance_a, 'method')\n    assert instance_a.method() is True\n    assert not hasattr(instance_b, 'method')\n\n\ndef test_mixin_same_class_multiple_times():\n    '''Mixin class to instance multiple times.'''\n    instance = Class()\n    assert not hasattr(instance, 'method')\n    assert len(instance.__class__.mro()) == 2\n\n    ftrack_api.mixin(instance, Mixin)\n    assert hasattr(instance, 'method')\n    assert instance.method() is True\n    assert len(instance.__class__.mro()) == 4\n\n    ftrack_api.mixin(instance, Mixin)\n    assert hasattr(instance, 'method')\n    assert instance.method() is True\n    assert len(instance.__class__.mro()) == 4\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_plugin.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport os\nimport textwrap\nimport logging\nimport re\n\nimport pytest\n\nimport ftrack_api.plugin\n\n\n@pytest.fixture()\ndef valid_plugin(temporary_path):\n    '''Return path to directory containing a valid plugin.'''\n    with open(os.path.join(temporary_path, 'plugin.py'), 'w') as file_object:\n        file_object.write(textwrap.dedent('''\n            def register(*args, **kw):\n                print \"Registered\", args, kw\n        '''))\n\n    return temporary_path\n\n\n@pytest.fixture()\ndef python_non_plugin(temporary_path):\n    '''Return path to directory containing Python file that is non plugin.'''\n    with open(os.path.join(temporary_path, 'non.py'), 'w') as file_object:\n        file_object.write(textwrap.dedent('''\n            print \"Not a plugin\"\n\n            def not_called():\n                print \"Not called\"\n        '''))\n\n    return temporary_path\n\n\n@pytest.fixture()\ndef non_plugin(temporary_path):\n    '''Return path to directory containing file that is non plugin.'''\n    with open(os.path.join(temporary_path, 'non.txt'), 'w') as file_object:\n        file_object.write('Never seen')\n\n    return temporary_path\n\n\n@pytest.fixture()\ndef broken_plugin(temporary_path):\n    '''Return path to directory containing broken plugin.'''\n    with open(os.path.join(temporary_path, 'broken.py'), 'w') as file_object:\n        file_object.write('syntax error')\n\n    return temporary_path\n\n\n@pytest.fixture()\ndef plugin(request, temporary_path):\n    '''Return path containing a plugin with requested specification.'''\n    specification = request.param\n    output = re.sub('(\\w+)=\\w+', '\"\\g<1>={}\".format(\\g<1>)', specification)\n    output = re.sub('\\*args', 'args', output)\n    output = re.sub('\\*\\*kwargs', 'sorted(kwargs.items())', output)\n\n    with open(os.path.join(temporary_path, 'plugin.py'), 'w') as file_object:\n        content = textwrap.dedent('''\n            def register({}):\n                print {}\n        '''.format(specification, output))\n        file_object.write(content)\n\n    return temporary_path\n\n\ndef test_discover_empty_paths(capsys):\n    '''Discover no plugins when paths are empty.'''\n    ftrack_api.plugin.discover(['   '])\n    output, error = capsys.readouterr()\n    assert not output\n    assert not error\n\n\ndef test_discover_valid_plugin(valid_plugin, capsys):\n    '''Discover valid plugin.'''\n    ftrack_api.plugin.discover([valid_plugin], (1, 2), {'3': 4})\n    output, error = capsys.readouterr()\n    assert 'Registered (1, 2) {\\'3\\': 4}' in output\n\n\ndef test_discover_python_non_plugin(python_non_plugin, capsys):\n    '''Discover Python non plugin.'''\n    ftrack_api.plugin.discover([python_non_plugin])\n    output, error = capsys.readouterr()\n    assert 'Not a plugin' in output\n    assert 'Not called' not in output\n\n\ndef test_discover_non_plugin(non_plugin, capsys):\n    '''Discover non plugin.'''\n    ftrack_api.plugin.discover([non_plugin])\n    output, error = capsys.readouterr()\n    assert not output\n    assert not error\n\n\ndef test_discover_broken_plugin(broken_plugin, caplog):\n    '''Discover broken plugin.'''\n    ftrack_api.plugin.discover([broken_plugin])\n\n    records = caplog.records()\n    assert len(records) == 1\n    assert records[0].levelno is logging.WARNING\n    assert 'Failed to load plugin' in records[0].message\n\n\n@pytest.mark.parametrize(\n    'plugin, positional, keyword, expected',\n    [\n        (\n            'a, b=False, c=False, d=False',\n            (1, 2), {'c': True, 'd': True, 'e': True},\n            '1 b=2 c=True d=True'\n        ),\n        (\n            '*args',\n            (1, 2), {'b': True, 'c': False},\n            '(1, 2)'\n        ),\n        (\n            '**kwargs',\n            tuple(), {'b': True, 'c': False},\n            '[(\\'b\\', True), (\\'c\\', False)]'\n        ),\n        (\n            'a=False, b=False',\n            (True,), {'b': True},\n            'a=True b=True'\n        ),\n        (\n            'a, c=False, *args',\n            (1, 2, 3, 4), {},\n            '1 c=2 (3, 4)'\n        ),\n        (\n            'a, c=False, **kwargs',\n            tuple(), {'a': 1, 'b': 2, 'c': 3, 'd': 4},\n            '1 c=3 [(\\'b\\', 2), (\\'d\\', 4)]'\n        ),\n    ],\n    indirect=['plugin'],\n    ids=[\n        'mixed-explicit',\n        'variable-args-only',\n        'variable-kwargs-only',\n        'keyword-from-positional',\n        'trailing-variable-args',\n        'trailing-keyword-args'\n    ]\n)\ndef test_discover_plugin_with_specific_signature(\n    plugin, positional, keyword, expected, capsys\n):\n    '''Discover plugin passing only supported arguments.'''\n    ftrack_api.plugin.discover(\n        [plugin], positional, keyword\n    )\n    output, error = capsys.readouterr()\n    assert expected in output\n\n\ndef test_discover_plugin_varying_signatures(temporary_path, capsys):\n    '''Discover multiple plugins with varying signatures.'''\n    with open(os.path.join(temporary_path, 'plugin_a.py'), 'w') as file_object:\n        file_object.write(textwrap.dedent('''\n            def register(a):\n                print (a,)\n        '''))\n\n    with open(os.path.join(temporary_path, 'plugin_b.py'), 'w') as file_object:\n        file_object.write(textwrap.dedent('''\n            def register(a, b=False):\n                print (a,), {'b': b}\n        '''))\n\n    ftrack_api.plugin.discover(\n        [temporary_path], (True,), {'b': True}\n    )\n\n    output, error = capsys.readouterr()\n    assert '(True,)'in output\n    assert '(True,) {\\'b\\': True}' in output\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_query.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport math\n\nimport pytest\n\nimport ftrack_api\nimport ftrack_api.query\nimport ftrack_api.exception\n\n\ndef test_index(session):\n    '''Index into query result.'''\n    results = session.query('User')\n    assert isinstance(results[2], session.types['User'])\n\n\ndef test_len(session):\n    '''Return count of results using len.'''\n    results = session.query('User where username is jenkins')\n    assert len(results) == 1\n\n\ndef test_all(session):\n    '''Return all results using convenience method.'''\n    results = session.query('User').all()\n    assert isinstance(results, list)\n    assert len(results)\n\n\ndef test_implicit_iteration(session):\n    '''Implicitly iterate through query result.'''\n    results = session.query('User')\n    assert isinstance(results, ftrack_api.query.QueryResult)\n\n    records = []\n    for record in results:\n        records.append(record)\n\n    assert len(records) == len(results)\n\n\ndef test_one(session):\n    '''Return single result using convenience method.'''\n    user = session.query('User where username is jenkins').one()\n    assert user['username'] == 'jenkins'\n\n\ndef test_one_fails_for_no_results(session):\n    '''Fail to fetch single result when no results available.'''\n    with pytest.raises(ftrack_api.exception.NoResultFoundError):\n        session.query('User where username is does_not_exist').one()\n\n\ndef test_one_fails_for_multiple_results(session):\n    '''Fail to fetch single result when multiple results available.'''\n    with pytest.raises(ftrack_api.exception.MultipleResultsFoundError):\n        session.query('User').one()\n\n\ndef test_one_with_existing_limit(session):\n    '''Fail to return single result when existing limit in expression.'''\n    with pytest.raises(ValueError):\n        session.query('User where username is jenkins limit 0').one()\n\n\ndef test_one_with_existing_offset(session):\n    '''Fail to return single result when existing offset in expression.'''\n    with pytest.raises(ValueError):\n        session.query('User where username is jenkins offset 2').one()\n\n\ndef test_one_with_prefetched_data(session):\n    '''Return single result ignoring prefetched data.'''\n    query = session.query('User where username is jenkins')\n    query.all()\n\n    user = query.one()\n    assert user['username'] == 'jenkins'\n\n\ndef test_first(session):\n    '''Return first result using convenience method.'''\n    users = session.query('User').all()\n\n    user = session.query('User').first()\n    assert user == users[0]\n\n\ndef test_first_returns_none_when_no_results(session):\n    '''Return None when no results available.'''\n    user = session.query('User where username is does_not_exist').first()\n    assert user is None\n\n\ndef test_first_with_existing_limit(session):\n    '''Fail to return first result when existing limit in expression.'''\n    with pytest.raises(ValueError):\n        session.query('User where username is jenkins limit 0').first()\n\n\ndef test_first_with_existing_offset(session):\n    '''Return first result whilst respecting custom offset.'''\n    users = session.query('User').all()\n\n    user = session.query('User offset 2').first()\n    assert user == users[2]\n\n\ndef test_first_with_prefetched_data(session):\n    '''Return first result ignoring prefetched data.'''\n    query = session.query('User where username is jenkins')\n    query.all()\n\n    user = query.first()\n    assert user['username'] == 'jenkins'\n\n\ndef test_paging(session, mocker):\n    '''Page through results.'''\n    mocker.patch.object(session, 'call', wraps=session.call)\n\n    page_size = 5\n    query = session.query('User limit 50', page_size=page_size)\n    records = query.all()\n\n    assert session.call.call_count == (\n        math.ceil(len(records) / float(page_size))\n    )\n\n\ndef test_paging_respects_offset_and_limit(session, mocker):\n    '''Page through results respecting offset and limit.'''\n    users = session.query('User').all()\n\n    mocker.patch.object(session, 'call', wraps=session.call)\n\n    page_size = 6\n    query = session.query('User offset 2 limit 8', page_size=page_size)\n    records = query.all()\n\n    assert session.call.call_count == 2\n    assert len(records) == 8\n    assert records == users[2:10]\n\n\ndef test_paging_respects_limit_smaller_than_page_size(session, mocker):\n    '''Use initial limit when less than page size.'''\n    mocker.patch.object(session, 'call', wraps=session.call)\n\n    page_size = 100\n    query = session.query('User limit 10', page_size=page_size)\n    records = query.all()\n\n    assert session.call.call_count == 1\n    session.call.assert_called_once_with(\n        [{\n            'action': 'query',\n            'expression': 'select id from User offset 0 limit 10'\n        }]\n    )\n\n    assert len(records) == 10"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_session.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport os\nimport tempfile\nimport functools\nimport uuid\nimport textwrap\nimport datetime\nimport json\nimport random\n\nimport pytest\nimport mock\nimport arrow\nimport requests\n\nimport ftrack_api\nimport ftrack_api.cache\nimport ftrack_api.inspection\nimport ftrack_api.symbol\nimport ftrack_api.exception\nimport ftrack_api.session\nimport ftrack_api.collection\n\n\n@pytest.fixture(params=['memory', 'persisted'])\ndef cache(request):\n    '''Return cache.'''\n    if request.param == 'memory':\n        cache = None  # There is already a default Memory cache present.\n    elif request.param == 'persisted':\n        cache_path = os.path.join(\n            tempfile.gettempdir(), '{0}.dbm'.format(uuid.uuid4().hex)\n        )\n\n        cache = lambda session: ftrack_api.cache.SerialisedCache(\n            ftrack_api.cache.FileCache(cache_path),\n            encode=functools.partial(\n                session.encode, entity_attribute_strategy='persisted_only'\n            ),\n            decode=session.decode\n        )\n\n        def cleanup():\n            '''Cleanup.'''\n            try:\n                os.remove(cache_path)\n            except OSError:\n                # BSD DB (Mac OSX) implementation of the interface will append\n                # a .db extension.\n                os.remove(cache_path + '.db')\n\n        request.addfinalizer(cleanup)\n\n    return cache\n\n\n@pytest.fixture()\ndef temporary_invalid_schema_cache(request):\n    '''Return schema cache path to invalid schema cache file.'''\n    schema_cache_path = os.path.join(\n        tempfile.gettempdir(),\n        'ftrack_api_schema_cache_test_{0}.json'.format(uuid.uuid4().hex)\n    )\n\n    with open(schema_cache_path, 'w') as file_:\n        file_.write('${invalid json}')\n\n    def cleanup():\n        '''Cleanup.'''\n        os.remove(schema_cache_path)\n\n    request.addfinalizer(cleanup)\n\n    return schema_cache_path\n\n\n@pytest.fixture()\ndef temporary_valid_schema_cache(request, mocked_schemas):\n    '''Return schema cache path to valid schema cache file.'''\n    schema_cache_path = os.path.join(\n        tempfile.gettempdir(),\n        'ftrack_api_schema_cache_test_{0}.json'.format(uuid.uuid4().hex)\n    )\n\n    with open(schema_cache_path, 'w') as file_:\n        json.dump(mocked_schemas, file_, indent=4)\n\n    def cleanup():\n        '''Cleanup.'''\n        os.remove(schema_cache_path)\n\n    request.addfinalizer(cleanup)\n\n    return schema_cache_path\n\n\nclass SelectiveCache(ftrack_api.cache.ProxyCache):\n    '''Proxy cache that should not cache newly created entities.'''\n\n    def set(self, key, value):\n        '''Set *value* for *key*.'''\n        if isinstance(value, ftrack_api.entity.base.Entity):\n            if (\n                ftrack_api.inspection.state(value)\n                is ftrack_api.symbol.CREATED\n            ):\n                return\n\n        super(SelectiveCache, self).set(key, value)\n\n\ndef test_get_entity(session, user):\n    '''Retrieve an entity by type and id.'''\n    matching = session.get(*ftrack_api.inspection.identity(user))\n    assert matching == user\n\n\ndef test_get_non_existant_entity(session):\n    '''Retrieve a non-existant entity by type and id.'''\n    matching = session.get('User', 'non-existant-id')\n    assert matching is None\n\n\ndef test_get_entity_of_invalid_type(session):\n    '''Fail to retrieve an entity using an invalid type.'''\n    with pytest.raises(KeyError):\n        session.get('InvalidType', 'id')\n\n\ndef test_create(session):\n    '''Create entity.'''\n    user = session.create('User', {'username': 'martin'})\n    with session.auto_populating(False):\n        assert user['id'] is not ftrack_api.symbol.NOT_SET\n        assert user['username'] == 'martin'\n        assert user['email'] is ftrack_api.symbol.NOT_SET\n\n\ndef test_create_using_only_defaults(session):\n    '''Create entity using defaults only.'''\n    user = session.create('User')\n    with session.auto_populating(False):\n        assert user['id'] is not ftrack_api.symbol.NOT_SET\n        assert user['username'] is ftrack_api.symbol.NOT_SET\n\n\ndef test_create_using_server_side_defaults(session):\n    '''Create entity using server side defaults.'''\n    user = session.create('User')\n    with session.auto_populating(False):\n        assert user['id'] is not ftrack_api.symbol.NOT_SET\n        assert user['username'] is ftrack_api.symbol.NOT_SET\n\n    session.commit()\n    assert user['username'] is not ftrack_api.symbol.NOT_SET\n\n\ndef test_create_overriding_defaults(session):\n    '''Create entity overriding defaults.'''\n    uid = str(uuid.uuid4())\n    user = session.create('User', {'id': uid})\n    with session.auto_populating(False):\n        assert user['id'] == uid\n\n\ndef test_create_with_reference(session):\n    '''Create entity with a reference to another.'''\n    status = session.query('Status')[0]\n    task = session.create('Task', {'status': status})\n    assert task['status'] is status\n\n\ndef test_ensure_new_entity(session, unique_name):\n    '''Ensure entity, creating first.'''\n    entity = session.ensure('User', {'username': unique_name})\n    assert entity['username'] == unique_name\n\n\ndef test_ensure_entity_with_non_string_data_types(session):\n    '''Ensure entity against non-string data types, creating first.'''\n    datetime = arrow.get()\n\n    task = session.query('Task').first()\n    user = session.query(\n        'User where username is {}'.format(session.api_user)\n    ).first()\n\n    first = session.ensure(\n        'Timelog', \n        {\n            'start': datetime,\n            'duration': 10,\n            'user_id': user['id'],\n            'context_id': task['id']\n        }\n    )\n\n    with mock.patch.object(session, 'create') as mocked:\n        session.ensure(\n            'Timelog', \n            {\n                'start': datetime, \n                'duration': 10, \n                'user_id': user['id'],\n                'context_id': task['id']\n            }\n        )\n        assert not mocked.called\n\n    assert first['start'] == datetime\n    assert first['duration'] == 10\n\n\ndef test_ensure_entity_with_identifying_keys(session, unique_name):\n    '''Ensure entity, checking using keys subset and then creating.'''\n    entity = session.ensure(\n        'User', {'username': unique_name, 'email': 'test@example.com'},\n        identifying_keys=['username']\n    )\n    assert entity['username'] == unique_name\n\n\ndef test_ensure_entity_with_invalid_identifying_keys(session, unique_name):\n    '''Fail to ensure entity when identifying key missing from data.'''\n    with pytest.raises(KeyError):\n        session.ensure(\n            'User', {'username': unique_name, 'email': 'test@example.com'},\n            identifying_keys=['invalid']\n        )\n\n\ndef test_ensure_entity_with_missing_identifying_keys(session):\n    '''Fail to ensure entity when no identifying keys determined.'''\n    with pytest.raises(ValueError):\n        session.ensure('User', {})\n\n\ndef test_ensure_existing_entity(session, unique_name):\n    '''Ensure existing entity.'''\n    entity = session.ensure('User', {'first_name': unique_name})\n\n    # Second call should not commit any new entity, just retrieve the existing.\n    with mock.patch.object(session, 'create') as mocked:\n        retrieved = session.ensure('User', {'first_name': unique_name})\n        assert not mocked.called\n        assert retrieved == entity\n\n\ndef test_ensure_update_existing_entity(session, unique_name):\n    '''Ensure and update existing entity.'''\n    entity = session.ensure(\n        'User', {'first_name': unique_name, 'email': 'anon@example.com'}\n    )\n    assert entity['email'] == 'anon@example.com'\n\n    # Second call should commit updates.\n    retrieved = session.ensure(\n        'User', {'first_name': unique_name, 'email': 'test@example.com'},\n        identifying_keys=['first_name']\n    )\n    assert retrieved == entity\n    assert retrieved['email'] == 'test@example.com'\n\n\ndef test_reconstruct_entity(session):\n    '''Reconstruct entity.'''\n    uid = str(uuid.uuid4())\n    data = {\n        'id': uid,\n        'username': 'martin',\n        'email': 'martin@example.com'\n    }\n    user = session.create('User', data, reconstructing=True)\n\n    for attribute in user.attributes:\n        # No local attributes should be set.\n        assert attribute.get_local_value(user) is ftrack_api.symbol.NOT_SET\n\n        # Only remote attributes that had explicit values should be set.\n        value = attribute.get_remote_value(user)\n        if attribute.name in data:\n            assert value == data[attribute.name]\n        else:\n            assert value is ftrack_api.symbol.NOT_SET\n\n\ndef test_reconstruct_entity_does_not_apply_defaults(session):\n    '''Reconstruct entity does not apply defaults.'''\n    # Note: Use private method to avoid merge which requires id be set.\n    user = session._create('User', {}, reconstructing=True)\n    with session.auto_populating(False):\n        assert user['id'] is ftrack_api.symbol.NOT_SET\n\n\ndef test_reconstruct_empty_entity(session):\n    '''Reconstruct empty entity.'''\n    # Note: Use private method to avoid merge which requires id be set.\n    user = session._create('User', {}, reconstructing=True)\n\n    for attribute in user.attributes:\n        # No local attributes should be set.\n        assert attribute.get_local_value(user) is ftrack_api.symbol.NOT_SET\n\n        # No remote attributes should be set.\n        assert attribute.get_remote_value(user) is ftrack_api.symbol.NOT_SET\n\n\ndef test_delete_operation_ordering(session, unique_name):\n    '''Delete entities in valid order.'''\n    # Construct entities.\n    project_schema = session.query('ProjectSchema').first()\n    project = session.create('Project', {\n        'name': unique_name,\n        'full_name': unique_name,\n        'project_schema': project_schema\n    })\n\n    sequence = session.create('Sequence', {\n        'name': unique_name,\n        'parent': project\n    })\n\n    session.commit()\n\n    # Delete in order that should succeed.\n    session.delete(sequence)\n    session.delete(project)\n\n    session.commit()\n\n\ndef test_create_then_delete_operation_ordering(session, unique_name):\n    '''Create and delete entity in one transaction.'''\n    entity = session.create('User', {'username': unique_name})\n    session.delete(entity)\n    session.commit()\n\n\ndef test_create_and_modify_to_have_required_attribute(session, unique_name):\n    '''Create and modify entity to have required attribute in transaction.'''\n    entity = session.create('Scope', {})\n    other = session.create('Scope', {'name': unique_name})\n    entity['name'] = '{0}2'.format(unique_name)\n    session.commit()\n\n\ndef test_ignore_in_create_entity_payload_values_set_to_not_set(\n    mocker, unique_name, session\n):\n    '''Ignore in commit, created entity data set to NOT_SET'''\n    mocked = mocker.patch.object(session, 'call')\n\n    # Should ignore 'email' attribute in payload.\n    new_user = session.create(\n        'User', {'username': unique_name, 'email': 'test'}\n    )\n    new_user['email'] = ftrack_api.symbol.NOT_SET\n    session.commit()\n    payloads = mocked.call_args[0][0]\n    assert len(payloads) == 1\n\n\ndef test_ignore_operation_that_modifies_attribute_to_not_set(\n    mocker, session, user\n):\n    '''Ignore in commit, operation that sets attribute value to NOT_SET'''\n    mocked = mocker.patch.object(session, 'call')\n\n    # Should result in no call to server.\n    user['email'] = ftrack_api.symbol.NOT_SET\n    session.commit()\n\n    assert not mocked.called\n\n\ndef test_operation_optimisation_on_commit(session, mocker):\n    '''Optimise operations on commit.'''\n    mocked = mocker.patch.object(session, 'call')\n\n    user_a = session.create('User', {'username': 'bob'})\n    user_a['username'] = 'foo'\n    user_a['email'] = 'bob@example.com'\n\n    user_b = session.create('User', {'username': 'martin'})\n    user_b['email'] = 'martin@ftrack.com'\n\n    user_a['email'] = 'bob@example.com'\n    user_a['first_name'] = 'Bob'\n\n    user_c = session.create('User', {'username': 'neverexist'})\n    user_c['email'] = 'ignore@example.com'\n    session.delete(user_c)\n\n    user_a_entity_key = ftrack_api.inspection.primary_key(user_a).values()\n    user_b_entity_key = ftrack_api.inspection.primary_key(user_b).values()\n\n    session.commit()\n\n    # The above operations should have translated into three payloads to call\n    # (two creates and one update).\n    payloads = mocked.call_args[0][0]\n    assert len(payloads) == 3\n\n    assert payloads[0]['action'] == 'create'\n    assert payloads[0]['entity_key'] == user_a_entity_key\n    assert set(payloads[0]['entity_data'].keys()) == set([\n        '__entity_type__', 'id', 'resource_type', 'username'\n    ])\n\n    assert payloads[1]['action'] == 'create'\n    assert payloads[1]['entity_key'] == user_b_entity_key\n    assert set(payloads[1]['entity_data'].keys()) == set([\n        '__entity_type__', 'id', 'resource_type', 'username', 'email'\n    ])\n\n    assert payloads[2]['action'] == 'update'\n    assert payloads[2]['entity_key'] == user_a_entity_key\n    assert set(payloads[2]['entity_data'].keys()) == set([\n        '__entity_type__', 'email', 'first_name'\n    ])\n\n\ndef test_state_collection(session, unique_name, user):\n    '''Session state collection holds correct entities.'''\n    # NOT_SET\n    user_a = session.create('User', {'username': unique_name})\n    session.commit()\n\n    # CREATED\n    user_b = session.create('User', {'username': unique_name})\n    user_b['username'] = 'changed'\n\n    # MODIFIED\n    user_c = user\n    user_c['username'] = 'changed'\n\n    # DELETED\n    user_d = session.create('User', {'username': unique_name})\n    session.delete(user_d)\n\n    assert session.created == [user_b]\n    assert session.modified == [user_c]\n    assert session.deleted == [user_d]\n\n\ndef test_get_entity_with_composite_primary_key(session, new_project):\n    '''Retrieve entity that uses a composite primary key.'''\n    entity = session.create('Metadata', {\n        'key': 'key', 'value': 'value',\n        'parent_type': new_project.entity_type,\n        'parent_id': new_project['id']\n    })\n\n    session.commit()\n\n    # Avoid cache.\n    new_session = ftrack_api.Session()\n    retrieved_entity = new_session.get(\n        'Metadata', ftrack_api.inspection.primary_key(entity).values()\n    )\n\n    assert retrieved_entity == entity\n\n\ndef test_get_entity_with_incomplete_composite_primary_key(session, new_project):\n    '''Fail to retrieve entity using incomplete composite primary key.'''\n    entity = session.create('Metadata', {\n        'key': 'key', 'value': 'value',\n        'parent_type': new_project.entity_type,\n        'parent_id': new_project['id']\n    })\n\n    session.commit()\n\n    # Avoid cache.\n    new_session = ftrack_api.Session()\n    with pytest.raises(ValueError):\n        new_session.get(\n            'Metadata', ftrack_api.inspection.primary_key(entity).values()[0]\n        )\n\n\ndef test_populate_entity(session, new_user):\n    '''Populate entity that uses single primary key.'''\n    with session.auto_populating(False):\n        assert new_user['email'] is ftrack_api.symbol.NOT_SET\n\n    session.populate(new_user, 'email')\n    assert new_user['email'] is not ftrack_api.symbol.NOT_SET\n\n\ndef test_populate_entities(session, unique_name):\n    '''Populate multiple entities that use single primary key.'''\n    users = []\n    for index in range(3):\n        users.append(\n            session.create(\n                'User', {'username': '{0}-{1}'.format(unique_name, index)}\n            )\n        )\n\n    session.commit()\n\n    with session.auto_populating(False):\n        for user in users:\n            assert user['email'] is ftrack_api.symbol.NOT_SET\n\n    session.populate(users, 'email')\n\n    for user in users:\n        assert user['email'] is not ftrack_api.symbol.NOT_SET\n\n\ndef test_populate_entity_with_composite_primary_key(session, new_project):\n    '''Populate entity that uses a composite primary key.'''\n    entity = session.create('Metadata', {\n        'key': 'key', 'value': 'value',\n        'parent_type': new_project.entity_type,\n        'parent_id': new_project['id']\n    })\n\n    session.commit()\n\n    # Avoid cache.\n    new_session = ftrack_api.Session()\n    retrieved_entity = new_session.get(\n        'Metadata', ftrack_api.inspection.primary_key(entity).values()\n    )\n\n    # Manually change already populated remote value so can test it gets reset\n    # on populate call.\n    retrieved_entity.attributes.get('value').set_remote_value(\n        retrieved_entity, 'changed'\n    )\n\n    new_session.populate(retrieved_entity, 'value')\n    assert retrieved_entity['value'] == 'value'\n\n\n@pytest.mark.parametrize('server_information, compatible', [\n    ({}, False),\n    ({'version': '3.3.11'}, True),\n    ({'version': '3.3.12'}, True),\n    ({'version': '3.4'}, True),\n    ({'version': '3.4.1'}, True),\n    ({'version': '3.5.16'}, True),\n    ({'version': '3.3.10'}, False)\n], ids=[\n    'No information',\n    'Valid current version',\n    'Valid higher version',\n    'Valid higher version',\n    'Valid higher version',\n    'Valid higher version',\n    'Invalid lower version'\n])\ndef test_check_server_compatibility(\n    server_information, compatible, session\n):\n    '''Check server compatibility.'''\n    with mock.patch.dict(\n        session._server_information, server_information, clear=True\n    ):\n        if compatible:\n            session.check_server_compatibility()\n        else:\n            with pytest.raises(ftrack_api.exception.ServerCompatibilityError):\n                session.check_server_compatibility()\n\n\ndef test_encode_entity_using_all_attributes_strategy(mocked_schema_session):\n    '''Encode entity using \"all\" entity_attribute_strategy.'''\n    new_bar = mocked_schema_session.create(\n        'Bar',\n        {\n            'name': 'myBar',\n            'id': 'bar_unique_id'\n        }\n    )\n\n    new_foo = mocked_schema_session.create(\n        'Foo',\n        {\n            'id': 'a_unique_id',\n            'string': 'abc',\n            'integer': 42,\n            'number': 12345678.9,\n            'boolean': False,\n            'date': arrow.get('2015-11-18 15:24:09'),\n            'bars': [new_bar]\n        }\n    )\n\n    encoded = mocked_schema_session.encode(\n        new_foo, entity_attribute_strategy='all'\n    )\n\n    assert encoded == textwrap.dedent('''\n        {\"__entity_type__\": \"Foo\",\n         \"bars\": [{\"__entity_type__\": \"Bar\", \"id\": \"bar_unique_id\"}],\n         \"boolean\": false,\n         \"date\": {\"__type__\": \"datetime\", \"value\": \"2015-11-18T15:24:09+00:00\"},\n         \"id\": \"a_unique_id\",\n         \"integer\": 42,\n         \"number\": 12345678.9,\n         \"string\": \"abc\"}\n    ''').replace('\\n', '')\n\n\ndef test_encode_entity_using_only_set_attributes_strategy(\n    mocked_schema_session\n):\n    '''Encode entity using \"set_only\" entity_attribute_strategy.'''\n    new_foo = mocked_schema_session.create(\n        'Foo',\n        {\n            'id': 'a_unique_id',\n            'string': 'abc',\n            'integer': 42\n        }\n    )\n\n    encoded = mocked_schema_session.encode(\n        new_foo, entity_attribute_strategy='set_only'\n    )\n\n    assert encoded == textwrap.dedent('''\n        {\"__entity_type__\": \"Foo\",\n         \"id\": \"a_unique_id\",\n         \"integer\": 42,\n         \"string\": \"abc\"}\n    ''').replace('\\n', '')\n\n\ndef test_encode_computed_attribute_using_persisted_only_attributes_strategy(\n    mocked_schema_session\n):\n    '''Encode computed attribute, \"persisted_only\" entity_attribute_strategy.'''\n    new_bar = mocked_schema_session._create(\n        'Bar',\n        {\n            'name': 'myBar',\n            'id': 'bar_unique_id',\n            'computed_value': 'FOO'\n        },\n        reconstructing=True\n    )\n\n    encoded = mocked_schema_session.encode(\n        new_bar, entity_attribute_strategy='persisted_only'\n    )\n\n    assert encoded == textwrap.dedent('''\n        {\"__entity_type__\": \"Bar\",\n         \"id\": \"bar_unique_id\",\n         \"name\": \"myBar\"}\n    ''').replace('\\n', '')\n\n\ndef test_encode_entity_using_only_modified_attributes_strategy(\n    mocked_schema_session\n):\n    '''Encode entity using \"modified_only\" entity_attribute_strategy.'''\n    new_foo = mocked_schema_session._create(\n        'Foo',\n        {\n            'id': 'a_unique_id',\n            'string': 'abc',\n            'integer': 42\n        },\n        reconstructing=True\n    )\n\n    new_foo['string'] = 'Modified'\n\n    encoded = mocked_schema_session.encode(\n        new_foo, entity_attribute_strategy='modified_only'\n    )\n\n    assert encoded == textwrap.dedent('''\n        {\"__entity_type__\": \"Foo\",\n         \"id\": \"a_unique_id\",\n         \"string\": \"Modified\"}\n    ''').replace('\\n', '')\n\n\ndef test_encode_entity_using_invalid_strategy(session, new_task):\n    '''Fail to encode entity using invalid strategy.'''\n    with pytest.raises(ValueError):\n        session.encode(new_task, entity_attribute_strategy='invalid')\n\n\ndef test_encode_operation_payload(session):\n    '''Encode operation payload.'''\n    sequence_component = session.create_component(\n        \"/path/to/sequence.%d.jpg [1]\", location=None\n    )\n    file_component = sequence_component[\"members\"][0]\n\n    encoded = session.encode([\n        ftrack_api.session.OperationPayload({\n            'action': 'create',\n            'entity_data': {\n                '__entity_type__': u'FileComponent',\n                u'container': sequence_component,\n                'id': file_component['id']\n            },\n            'entity_key': [file_component['id']],\n            'entity_type': u'FileComponent'\n        }),\n        ftrack_api.session.OperationPayload({\n            'action': 'update',\n            'entity_data': {\n                '__entity_type__': u'SequenceComponent',\n                u'members': ftrack_api.collection.Collection(\n                    sequence_component,\n                    sequence_component.attributes.get('members'),\n                    data=[file_component]\n                )\n            },\n            'entity_key': [sequence_component['id']],\n            'entity_type': u'SequenceComponent'\n        })\n    ])\n\n    expected = textwrap.dedent('''\n        [{{\"action\": \"create\",\n         \"entity_data\": {{\"__entity_type__\": \"FileComponent\",\n         \"container\": {{\"__entity_type__\": \"SequenceComponent\",\n         \"id\": \"{0[id]}\"}},\n         \"id\": \"{1[id]}\"}},\n         \"entity_key\": [\"{1[id]}\"],\n         \"entity_type\": \"FileComponent\"}},\n         {{\"action\": \"update\",\n         \"entity_data\": {{\"__entity_type__\": \"SequenceComponent\",\n         \"members\": [{{\"__entity_type__\": \"FileComponent\", \"id\": \"{1[id]}\"}}]}},\n         \"entity_key\": [\"{0[id]}\"],\n         \"entity_type\": \"SequenceComponent\"}}]\n    '''.format(sequence_component, file_component)).replace('\\n', '')\n\n    assert encoded == expected\n\n\ndef test_decode_partial_entity(\n    session, new_task\n):\n    '''Decode partially encoded entity.'''\n    encoded = session.encode(\n        new_task, entity_attribute_strategy='set_only'\n    )\n\n    entity = session.decode(encoded)\n\n    assert entity == new_task\n    assert entity is not new_task\n\n\ndef test_reset(mocker):\n    '''Reset session.'''\n    plugin_path = os.path.abspath(\n        os.path.join(os.path.dirname(__file__), '..', 'fixture', 'plugin')\n    )\n    session = ftrack_api.Session(plugin_paths=[plugin_path])\n\n    assert hasattr(session.types.get('User'), 'stub')\n    location = session.query('Location where name is \"test.location\"').one()\n    assert location.accessor is not ftrack_api.symbol.NOT_SET\n\n    mocked_close = mocker.patch.object(session._request, 'close')\n    mocked_fetch = mocker.patch.object(session, '_load_schemas')\n\n    session.reset()\n\n    # Assert custom entity type maintained.\n    assert hasattr(session.types.get('User'), 'stub')\n\n    # Assert location plugin re-configured.\n    location = session.query('Location where name is \"test.location\"').one()\n    assert location.accessor is not ftrack_api.symbol.NOT_SET\n\n    # Assert connection not closed and no schema fetch issued.\n    assert not mocked_close.called\n    assert not mocked_fetch.called\n\n\ndef test_rollback_scalar_attribute_change(session, new_user):\n    '''Rollback scalar attribute change via session.'''\n    assert not session.recorded_operations\n    current_first_name = new_user['first_name']\n\n    new_user['first_name'] = 'NewName'\n    assert new_user['first_name'] == 'NewName'\n    assert session.recorded_operations\n\n    session.rollback()\n\n    assert not session.recorded_operations\n    assert new_user['first_name'] == current_first_name\n\n\ndef test_rollback_collection_attribute_change(session, new_user):\n    '''Rollback collection attribute change via session.'''\n    assert not session.recorded_operations\n    current_timelogs = new_user['timelogs']\n    assert list(current_timelogs) == []\n\n    timelog = session.create('Timelog', {})\n    new_user['timelogs'].append(timelog)\n    assert list(new_user['timelogs']) == [timelog]\n    assert session.recorded_operations\n\n    session.rollback()\n\n    assert not session.recorded_operations\n    assert list(new_user['timelogs']) == []\n\n\ndef test_rollback_entity_creation(session):\n    '''Rollback entity creation via session.'''\n    assert not session.recorded_operations\n\n    new_user = session.create('User')\n    assert session.recorded_operations\n    assert new_user in session.created\n\n    session.rollback()\n\n    assert not session.recorded_operations\n    assert new_user not in session.created\n    assert new_user not in session._local_cache.values()\n\n\ndef test_rollback_entity_deletion(session, new_user):\n    '''Rollback entity deletion via session.'''\n    assert not session.recorded_operations\n\n    session.delete(new_user)\n    assert session.recorded_operations\n    assert new_user in session.deleted\n\n    session.rollback()\n    assert not session.recorded_operations\n    assert new_user not in session.deleted\n    assert new_user in session._local_cache.values()\n\n\n# Caching\n# ------------------------------------------------------------------------------\n\n\ndef test_get_entity_bypassing_cache(session, user, mocker):\n    '''Retrieve an entity by type and id bypassing cache.'''\n    mocker.patch.object(session, 'call', wraps=session.call)\n\n    session.cache.remove(\n        session.cache_key_maker.key(ftrack_api.inspection.identity(user))\n    )\n\n    matching = session.get(*ftrack_api.inspection.identity(user))\n\n    # Check a different instance returned.\n    assert matching is not user\n\n    # Check instances have the same identity.\n    assert matching == user\n\n    # Check cache was bypassed and server was called.\n    assert session.call.called\n\n\ndef test_get_entity_from_cache(cache, task, mocker):\n    '''Retrieve an entity by type and id from cache.'''\n    session = ftrack_api.Session(cache=cache)\n\n    # Prepare cache.\n    session.merge(task)\n\n    # Disable server calls.\n    mocker.patch.object(session, 'call')\n\n    # Retrieve entity from cache.\n    entity = session.get(*ftrack_api.inspection.identity(task))\n\n    assert entity is not None, 'Failed to retrieve entity from cache.'\n    assert entity == task\n    assert entity is not task\n\n    # Check that no call was made to server.\n    assert not session.call.called\n\n\ndef test_get_entity_tree_from_cache(cache, new_project_tree, mocker):\n    '''Retrieve an entity tree from cache.'''\n    session = ftrack_api.Session(cache=cache)\n\n    # Prepare cache.\n    # TODO: Maybe cache should be prepopulated for a better check here.\n    session.query(\n        'select children, children.children, children.children.children, '\n        'children.children.children.assignments, '\n        'children.children.children.assignments.resource '\n        'from Project where id is \"{0}\"'\n        .format(new_project_tree['id'])\n    ).one()\n\n    # Disable server calls.\n    mocker.patch.object(session, 'call')\n\n    # Retrieve entity from cache.\n    entity = session.get(*ftrack_api.inspection.identity(new_project_tree))\n\n    assert entity is not None, 'Failed to retrieve entity from cache.'\n    assert entity == new_project_tree\n    assert entity is not new_project_tree\n\n    # Check tree.\n    with session.auto_populating(False):\n        for sequence in entity['children']:\n            for shot in sequence['children']:\n                for task in shot['children']:\n                    assignments = task['assignments']\n                    for assignment in assignments:\n                        resource = assignment['resource']\n\n                        assert resource is not ftrack_api.symbol.NOT_SET\n\n    # Check that no call was made to server.\n    assert not session.call.called\n\n\ndef test_get_metadata_from_cache(session, mocker, cache, new_task):\n    '''Retrieve an entity along with its metadata from cache.'''\n    new_task['metadata']['key'] = 'value'\n    session.commit()\n\n    fresh_session = ftrack_api.Session(cache=cache)\n\n    # Prepare cache.\n    fresh_session.query(\n        'select metadata.key, metadata.value from '\n        'Task where id is \"{0}\"'\n        .format(new_task['id'])\n    ).all()\n\n    # Disable server calls.\n    mocker.patch.object(fresh_session, 'call')\n\n    # Retrieve entity from cache.\n    entity = fresh_session.get(*ftrack_api.inspection.identity(new_task))\n\n    assert entity is not None, 'Failed to retrieve entity from cache.'\n    assert entity == new_task\n    assert entity is not new_task\n\n    # Check metadata cached correctly.\n    with fresh_session.auto_populating(False):\n        metadata = entity['metadata']\n        assert metadata['key'] == 'value'\n\n    assert not fresh_session.call.called\n\n\ndef test_merge_circular_reference(cache, temporary_file):\n    '''Merge circular reference into cache.'''\n    session = ftrack_api.Session(cache=cache)\n    # The following will test the condition as a FileComponent will be created\n    # with corresponding ComponentLocation. The server will return the file\n    # component data with the component location embedded. The component\n    # location will in turn have an embedded reference to the file component.\n    # If the merge does not prioritise the primary keys of the instance then\n    # any cache that relies on using the identity of the file component will\n    # fail.\n    component = session.create_component(path=temporary_file)\n    assert component\n\n\ndef test_create_with_selective_cache(session):\n    '''Create entity does not store entity in selective cache.'''\n    cache = ftrack_api.cache.MemoryCache()\n    session.cache.caches.append(SelectiveCache(cache))\n    try:\n        user = session.create('User', {'username': 'martin'})\n        cache_key = session.cache_key_maker.key(\n            ftrack_api.inspection.identity(user)\n        )\n\n        with pytest.raises(KeyError):\n            cache.get(cache_key)\n\n    finally:\n        session.cache.caches.pop()\n\n\ndef test_correct_file_type_on_sequence_component(session):\n    '''Create sequence component with correct file type.'''\n    path = '/path/to/image/sequence.%04d.dpx [1-10]'\n    sequence_component = session.create_component(path)\n\n    assert sequence_component['file_type'] == '.dpx'\n\n\ndef test_read_schemas_from_cache(\n    session, temporary_valid_schema_cache\n):\n    '''Read valid content from schema cache.'''\n    expected_hash = 'a98d0627b5e33966e43e1cb89b082db7'\n\n    schemas, hash_ = session._read_schemas_from_cache(\n        temporary_valid_schema_cache\n    )\n\n    assert expected_hash == hash_\n\n\ndef test_fail_to_read_schemas_from_invalid_cache(\n    session, temporary_invalid_schema_cache\n):\n    '''Fail to read invalid content from schema cache.'''\n    with pytest.raises(ValueError):\n        session._read_schemas_from_cache(\n            temporary_invalid_schema_cache\n        )\n\n\ndef test_write_schemas_to_cache(\n    session, temporary_valid_schema_cache\n):\n    '''Write valid content to schema cache.'''\n    expected_hash = 'a98d0627b5e33966e43e1cb89b082db7'\n    schemas, _ = session._read_schemas_from_cache(temporary_valid_schema_cache)\n\n    session._write_schemas_to_cache(schemas, temporary_valid_schema_cache)\n\n    schemas, hash_ = session._read_schemas_from_cache(\n        temporary_valid_schema_cache\n    )\n\n    assert expected_hash == hash_\n\n\ndef test_fail_to_write_invalid_schemas_to_cache(\n    session, temporary_valid_schema_cache\n):\n    '''Fail to write invalid content to schema cache.'''\n    # Datetime not serialisable by default.\n    invalid_content = datetime.datetime.now()\n\n    with pytest.raises(TypeError):\n        session._write_schemas_to_cache(\n            invalid_content, temporary_valid_schema_cache\n        )\n\n\ndef test_load_schemas_from_valid_cache(\n    mocker, session, temporary_valid_schema_cache, mocked_schemas\n):\n    '''Load schemas from cache.'''\n    expected_schemas = session._load_schemas(temporary_valid_schema_cache)\n\n    mocked = mocker.patch.object(session, 'call')\n    schemas = session._load_schemas(temporary_valid_schema_cache)\n\n    assert schemas == expected_schemas\n    assert not mocked.called\n\n\ndef test_load_schemas_from_server_when_cache_invalid(\n    mocker, session, temporary_invalid_schema_cache\n):\n    '''Load schemas from server when cache invalid.'''\n    mocked = mocker.patch.object(session, 'call', wraps=session.call)\n\n    session._load_schemas(temporary_invalid_schema_cache)\n    assert mocked.called\n\n\ndef test_load_schemas_from_server_when_cache_outdated(\n    mocker, session, temporary_valid_schema_cache\n):\n    '''Load schemas from server when cache outdated.'''\n    schemas, _ = session._read_schemas_from_cache(temporary_valid_schema_cache)\n    schemas.append({\n        'id': 'NewTest'\n    })\n    session._write_schemas_to_cache(schemas, temporary_valid_schema_cache)\n\n    mocked = mocker.patch.object(session, 'call', wraps=session.call)\n    session._load_schemas(temporary_valid_schema_cache)\n\n    assert mocked.called\n\n\ndef test_load_schemas_from_server_not_reporting_schema_hash(\n    mocker, session, temporary_valid_schema_cache\n):\n    '''Load schemas from server when server does not report schema hash.'''\n    mocked_write = mocker.patch.object(\n        session, '_write_schemas_to_cache',\n        wraps=session._write_schemas_to_cache\n    )\n\n    server_information = session._server_information.copy()\n    server_information.pop('schema_hash')\n    mocker.patch.object(\n        session, '_server_information', new=server_information\n    )\n\n    session._load_schemas(temporary_valid_schema_cache)\n\n    # Cache still written even if hash not reported.\n    assert mocked_write.called\n\n    mocked = mocker.patch.object(session, 'call', wraps=session.call)\n    session._load_schemas(temporary_valid_schema_cache)\n\n    # No hash reported by server so cache should have been bypassed.\n    assert mocked.called\n\n\ndef test_load_schemas_bypassing_cache(\n    mocker, session, temporary_valid_schema_cache\n):\n    '''Load schemas bypassing cache when set to False.'''\n    with mocker.patch.object(session, 'call', wraps=session.call):\n\n        session._load_schemas(temporary_valid_schema_cache)\n        assert session.call.call_count == 1\n\n        session._load_schemas(False)\n        assert session.call.call_count == 2\n\n\ndef test_get_tasks_widget_url(session):\n    '''Tasks widget URL returns valid HTTP status.'''\n    url = session.get_widget_url('tasks')\n    response = requests.get(url)\n    response.raise_for_status()\n\n\ndef test_get_info_widget_url(session, task):\n    '''Info widget URL for *task* returns valid HTTP status.'''\n    url = session.get_widget_url('info', entity=task, theme='light')\n    response = requests.get(url)\n    response.raise_for_status()\n\n\ndef test_encode_media_from_path(session, video_path):\n    '''Encode media based on a file path.'''\n    job = session.encode_media(video_path)\n\n    assert job.entity_type == 'Job'\n\n    job_data = json.loads(job['data'])\n    assert 'output' in job_data\n    assert 'source_component_id' in job_data\n    assert 'keep_original' in job_data and job_data['keep_original'] is False\n    assert len(job_data['output'])\n    assert 'component_id' in job_data['output'][0]\n    assert 'format' in job_data['output'][0]\n\n\ndef test_encode_media_from_component(session, video_path):\n    '''Encode media based on a component.'''\n    location = session.query('Location where name is \"ftrack.server\"').one()\n    component = session.create_component(\n        video_path,\n        location=location\n    )\n    session.commit()\n\n    job = session.encode_media(component)\n\n    assert job.entity_type == 'Job'\n\n    job_data = json.loads(job['data'])\n    assert 'keep_original' in job_data and job_data['keep_original'] is True\n\n\ndef test_create_sequence_component_with_size(session, temporary_sequence):\n    '''Create a sequence component and verify that is has a size.'''\n    location = session.query('Location where name is \"ftrack.server\"').one()\n    component = session.create_component(\n        temporary_sequence\n    )\n\n    assert component['size'] > 0\n\n\ndef test_plugin_arguments(mocker):\n    '''Pass plugin arguments to plugin discovery mechanism.'''\n    mock = mocker.patch(\n        'ftrack_api.plugin.discover'\n    )\n    session = ftrack_api.Session(\n        plugin_paths=[], plugin_arguments={\"test\": \"value\"}\n    )\n    assert mock.called\n    mock.assert_called_once_with([], [session], {\"test\": \"value\"})\n\ndef test_remote_reset(session, new_user):\n    '''Reset user api key.'''\n    key_1 = session.reset_remote(\n        'api_key', entity=new_user\n    )\n\n    key_2 = session.reset_remote(\n        'api_key', entity=new_user\n    )\n\n\n    assert key_1 != key_2\n\n\n@pytest.mark.parametrize('attribute', [\n    ('id',),\n    ('email',)\n\n], ids=[\n    'Fail resetting primary key',\n    'Fail resetting attribute without default value',\n])\ndef test_fail_remote_reset(session, user, attribute):\n    '''Fail trying to rest invalid attributes.'''\n\n    with pytest.raises(ftrack_api.exception.ServerError):\n        session.reset_remote(\n            attribute, user\n        )\n\n\ndef test_close(session):\n    '''Close session.'''\n    assert session.closed is False\n    session.close()\n    assert session.closed is True\n\n\ndef test_close_already_closed_session(session):\n    '''Close session that is already closed.'''\n    session.close()\n    assert session.closed is True\n    session.close()\n    assert session.closed is True\n\n\ndef test_server_call_after_close(session):\n    '''Fail to issue calls to server after session closed.'''\n    session.close()\n    assert session.closed is True\n\n    with pytest.raises(ftrack_api.exception.ConnectionClosedError):\n        session.query('User').first()\n\n\ndef test_context_manager(session):\n    '''Use session as context manager.'''\n    with session:\n        assert session.closed is False\n\n    assert session.closed is True\n\n\ndef test_delayed_job(session):\n    '''Test the delayed_job action'''\n\n    with pytest.raises(ValueError):\n        session.delayed_job(\n            'DUMMY_JOB'\n        )\n\n\n@pytest.mark.skip(reason='No configured ldap server.')\ndef test_delayed_job_ldap_sync(session):\n    '''Test the a delayed_job ldap sync action'''\n    result = session.delayed_job(\n        ftrack_api.symbol.JOB_SYNC_USERS_LDAP\n    )\n\n    assert isinstance(\n        result, ftrack_api.entity.job.Job\n    )\n\n\ndef test_query_nested_custom_attributes(session, new_asset_version):\n    '''Query custom attributes nested and update a value and query again.\n\n    This test will query custom attributes via 2 relations, then update the\n    value in one API session and read it back in another to verify that it gets\n    the new value.\n\n    '''\n    session_one = session\n    session_two = ftrack_api.Session(\n        auto_connect_event_hub=False\n    )\n\n    # Read the version via a relation in both sessions.\n    def get_versions(sessions):\n        versions = []\n        for _session in sessions:\n            asset = _session.query(\n                'select versions.custom_attributes from Asset where id is \"{0}\"'.format(\n                    new_asset_version.get('asset_id')\n                )\n            ).first()\n\n            for version in asset['versions']:\n                if version.get('id') == new_asset_version.get('id'):\n                    versions.append(version)\n\n        return versions\n\n    # Get version from both sessions.\n    versions = get_versions((session_one, session_two))\n\n    # Read attribute for both sessions.\n    for version in versions:\n        version['custom_attributes']['versiontest']\n\n    # Set attribute on session_one.\n    versions[0]['custom_attributes']['versiontest'] = random.randint(\n        0, 99999\n    )\n\n    session.commit()\n\n    # Read version from server for session_two.\n    session_two_version = get_versions((session_two, ))[0]\n\n    # Verify that value in session 2 is the same as set and committed in\n    # session 1.\n    assert (\n        session_two_version['custom_attributes']['versiontest'] ==\n        versions[0]['custom_attributes']['versiontest']\n    )\n\n\ndef test_query_nested(session):\n    '''Query components nested and update a value and query again.\n\n    This test will query components via 2 relations, then update the\n    value in one API session and read it back in another to verify that it gets\n    the new value.\n\n    '''\n    session_one = session\n    session_two = ftrack_api.Session(\n        auto_connect_event_hub=False\n    )\n\n    query = (\n        'select versions.components.name from Asset where id is '\n        '\"12939d0c-6766-11e1-8104-f23c91df25eb\"'\n    )\n\n    def get_version(session):\n        '''Return the test version from *session*.'''\n        asset = session.query(query).first()\n        asset_version = None\n        for version in asset['versions']:\n            if version['version'] == 8:\n                asset_version = version\n                break\n\n        return asset_version\n\n    asset_version = get_version(session_one)\n    asset_version2 = get_version(session_two)\n\n    # This assert is not needed, but reading the collections are to ensure they\n    # are inflated.\n    assert (\n        asset_version2['components'][0]['name'] ==\n        asset_version['components'][0]['name']\n    )\n\n    asset_version['components'][0]['name'] = str(uuid.uuid4())\n\n    session.commit()\n\n    asset_version2 = get_version(session_two)\n\n    assert (\n        asset_version['components'][0]['name'] ==\n        asset_version2['components'][0]['name']\n    )\n\n\ndef test_merge_iterations(session, mocker, project):\n    '''Ensure merge does not happen to many times when querying.'''\n    mocker.spy(session, '_merge')\n\n    session.query(\n        'select status from Task where project_id is {} limit 10'.format(\n            project['id']\n        )\n    ).all()\n\n    assert session._merge.call_count < 75\n\n\n@pytest.mark.parametrize(\n    'get_versions',\n    [\n        lambda component, asset_version, asset: component['version']['asset']['versions'],\n        lambda component, asset_version, asset: asset_version['asset']['versions'],\n        lambda component, asset_version, asset: asset['versions'],\n    ],\n    ids=[\n        'from_component',\n        'from_asset_version',\n        'from_asset',\n    ]\n)\ndef test_query_nested2(session, get_versions):\n    '''Query version.asset.versions from component and then add new version.\n\n    This test will query versions via multiple relations and ensure a new\n    version appears when added to a different session and then is queried\n    again.\n\n    '''\n    session_one = session\n    session_two = ftrack_api.Session(\n        auto_connect_event_hub=False\n    )\n\n    # Get a random component that is linked to a version and asset.\n    component_id = session_two.query(\n        'FileComponent where version.asset_id != None'\n    ).first()['id']\n\n    query = (\n        'select version.asset.versions from Component where id is \"{}\"'.format(\n            component_id\n        )\n    )\n\n    component = session_one.query(query).one()\n    asset_version = component['version']\n    asset = component['version']['asset']\n    versions = component['version']['asset']['versions']\n    length = len(versions)\n\n    session_two.create('AssetVersion', {\n        'asset_id': asset['id']\n    })\n\n    session_two.commit()\n\n    component = session_one.query(query).one()\n    versions = get_versions(component, asset_version, asset)\n    new_length = len(versions)\n\n    assert length + 1 == new_length\n\n\ndef test_session_ready_reset_events(mocker):\n    '''Session ready and reset events.'''\n    plugin_path = os.path.abspath(\n        os.path.join(os.path.dirname(__file__), '..', 'fixture', 'plugin')\n    )\n    session = ftrack_api.Session(plugin_paths=[plugin_path])\n\n    assert session._test_called_events['ftrack.api.session.ready'] is 1\n    assert session._test_called_events['ftrack.api.session.reset'] is 0\n\n    session.reset()\n    assert session._test_called_events['ftrack.api.session.ready'] is 1\n    assert session._test_called_events['ftrack.api.session.reset'] is 1\n\n\ndef test_entity_reference(mocker, session):\n    '''Return entity reference that uniquely identifies entity.'''\n    mock_entity = mocker.Mock(entity_type=\"MockEntityType\")\n    mock_auto_populating = mocker.patch.object(session, \"auto_populating\")\n    mock_primary_key = mocker.patch(\n        \"ftrack_api.inspection.primary_key\", return_value={\"id\": \"mock-id\"}\n    )\n\n    reference = session.entity_reference(mock_entity)\n\n    assert reference == {\n        \"__entity_type__\": \"MockEntityType\",\n        \"id\": \"mock-id\"\n    }\n\n    mock_auto_populating.assert_called_once_with(False)\n    mock_primary_key.assert_called_once_with(mock_entity)\n\n\ndef test__entity_reference(mocker, session):\n    '''Act as alias to entity_reference.'''\n    mock_entity = mocker.Mock(entity_type=\"MockEntityType\")\n    mock_entity_reference = mocker.patch.object(session, \"entity_reference\")\n    mocker.patch(\"warnings.warn\")\n\n    session._entity_reference(mock_entity)\n\n    mock_entity_reference.assert_called_once_with(mock_entity)\n\n\ndef test__entity_reference_issues_deprecation_warning(mocker, session):\n    '''Issue deprecation warning for usage of _entity_reference.'''\n    mocker.patch.object(session, \"entity_reference\")\n    mock_warn = mocker.patch(\"warnings.warn\")\n\n    session._entity_reference({})\n\n    mock_warn.assert_called_once_with(\n        (\n            \"Session._entity_reference is now available as public method \"\n            \"Session.entity_reference. The private method will be removed \"\n            \"in version 2.0.\"\n        ),\n        PendingDeprecationWarning\n    )\n"
  },
  {
    "path": "openpype/modules/ftrack/python2_vendor/ftrack-python-api/test/unit/test_timer.py",
    "content": "# :coding: utf-8\n# :copyright: Copyright (c) 2015 ftrack\n\nimport pytest\nimport ftrack_api.exception\n\n\ndef test_manually_create_multiple_timers_with_error(session, new_user):\n    '''Fail to create a second timer.'''\n    session.create('Timer', {\n        'user': new_user\n    })\n\n    session.commit()\n\n    with pytest.raises(ftrack_api.exception.ServerError):\n        session.create('Timer', {\n            'user': new_user\n        })\n\n        session.commit()\n\n    session.reset()\n\n\ndef test_create_multiple_timers_with_error(session, new_user):\n    '''Fail to create a second timer.'''\n    new_user.start_timer()\n\n    with pytest.raises(ftrack_api.exception.NotUniqueError):\n        new_user.start_timer()\n\n    session.reset()\n\n\ndef test_start_and_stop_a_timer(session, new_user, new_task):\n    '''Start a new timer and stop it to create a timelog.'''\n    new_user.start_timer(new_task)\n\n    new_user.stop_timer()\n\n    timelog = session.query(\n        'Timelog where context_id = \"{0}\"'.format(new_task['id'])\n    ).one()\n\n    assert timelog['user_id'] == new_user['id'], 'User id is correct.'\n    assert timelog['context_id'] == new_task['id'], 'Task id is correct.'\n\n\ndef test_start_a_timer_when_timer_is_running(session, new_user, new_task):\n    '''Start a timer when an existing timer is already running.'''\n    new_user.start_timer(new_task)\n\n    # Create the second timer without context.\n    new_user.start_timer(force=True)\n\n    # There should be only one existing timelog for this user.\n    timelogs = session.query(\n        'Timelog where user_id = \"{0}\"'.format(new_user['id'])\n    ).all()\n    assert len(timelogs) == 1, 'One timelog exists.'\n\n    timelog = session.query(\n        'Timer where user_id = \"{0}\"'.format(new_user['id'])\n    ).one()\n\n    # Make sure running timer has no context.\n    assert timelog['context_id'] is None, 'Timer does not have a context.'\n\n\ndef test_stop_timer_without_timer_running(session, new_user):\n    '''Stop a timer when no timer is running.'''\n    with pytest.raises(ftrack_api.exception.NoResultFoundError):\n        new_user.stop_timer()\n"
  },
  {
    "path": "openpype/modules/ftrack/scripts/sub_event_processor.py",
    "content": "import os\nimport sys\nimport signal\nimport socket\nimport datetime\n\nimport ftrack_api\n\nfrom openpype_modules.ftrack.ftrack_server.ftrack_server import FtrackServer\nfrom openpype_modules.ftrack.ftrack_server.lib import (\n    SocketSession,\n    ProcessEventHub,\n    TOPIC_STATUS_SERVER\n)\nfrom openpype.modules import ModulesManager\n\nfrom openpype.lib import (\n    Logger,\n    get_openpype_version,\n    get_build_version\n)\n\nsubprocess_started = datetime.datetime.now()\n\n\nclass SessionFactory:\n    session = None\n\n\ndef send_status(event):\n    subprocess_id = event[\"data\"].get(\"subprocess_id\")\n    if not subprocess_id:\n        return\n\n    if subprocess_id != os.environ[\"FTRACK_EVENT_SUB_ID\"]:\n        return\n\n    session = SessionFactory.session\n    if not session:\n        return\n\n    new_event_data = {\n        \"subprocess_id\": subprocess_id,\n        \"source\": \"processor\",\n        \"status_info\": [\n            [\"created_at\", subprocess_started.strftime(\"%Y.%m.%d %H:%M:%S\")],\n            [\"OpenPype version\", get_openpype_version() or \"N/A\"],\n            [\"OpenPype build version\", get_build_version() or \"N/A\"]\n        ]\n    }\n\n    new_event = ftrack_api.event.base.Event(\n        topic=\"openpype.event.server.status.result\",\n        data=new_event_data\n    )\n\n    session.event_hub.publish(new_event)\n\n\ndef register(session):\n    '''Registers the event, subscribing the discover and launch topics.'''\n    session.event_hub.subscribe(\n        \"topic={}\".format(TOPIC_STATUS_SERVER), send_status\n    )\n\n\ndef main(args):\n    log = Logger.get_logger(\"Event processor\")\n\n    port = int(args[-1])\n    # Create a TCP/IP socket\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n\n    # Connect the socket to the port where the server is listening\n    server_address = (\"localhost\", port)\n    log.debug(\"Processor connected to {} port {}\".format(*server_address))\n    sock.connect(server_address)\n\n    sock.sendall(b\"CreatedProcess\")\n\n    returncode = 0\n    try:\n        session = SocketSession(\n            auto_connect_event_hub=True, sock=sock, Eventhub=ProcessEventHub\n        )\n        register(session)\n        SessionFactory.session = session\n\n        manager = ModulesManager()\n        ftrack_module = manager.modules_by_name[\"ftrack\"]\n        server = FtrackServer(\n            ftrack_module.server_event_handlers_paths\n        )\n        log.debug(\"Launched Ftrack Event processor\")\n        server.run_server(session)\n\n    except Exception:\n        returncode = 1\n        log.error(\"Event server crashed. See traceback below\", exc_info=True)\n\n    finally:\n        log.debug(\"First closing socket\")\n        sock.close()\n        return returncode\n\n\nif __name__ == \"__main__\":\n    # Register interupt signal\n    def signal_handler(sig, frame):\n        print(\"You pressed Ctrl+C. Process ended.\")\n        sys.exit(0)\n\n    signal.signal(signal.SIGINT, signal_handler)\n    signal.signal(signal.SIGTERM, signal_handler)\n\n    sys.exit(main(sys.argv))\n"
  },
  {
    "path": "openpype/modules/ftrack/scripts/sub_event_status.py",
    "content": "import os\nimport sys\nimport json\nimport threading\nimport collections\nimport signal\nimport socket\nimport datetime\n\nimport appdirs\n\nimport ftrack_api\nfrom openpype_modules.ftrack.ftrack_server.ftrack_server import FtrackServer\nfrom openpype_modules.ftrack.ftrack_server.lib import (\n    SocketSession,\n    StatusEventHub,\n    TOPIC_STATUS_SERVER,\n    TOPIC_STATUS_SERVER_RESULT,\n    get_host_ip\n)\nfrom openpype.lib import (\n    Logger,\n    is_current_version_studio_latest,\n    is_running_from_build,\n    get_expected_version,\n    get_openpype_version\n)\n\nlog = Logger.get_logger(\"Event storer\")\naction_identifier = (\n    \"event.server.status\" + os.environ[\"FTRACK_EVENT_SUB_ID\"]\n)\nhost_ip = get_host_ip()\naction_data = {\n    \"label\": \"OpenPype Admin\",\n    \"variant\": \"- Event server Status ({})\".format(host_ip or \"IP N/A\"),\n    \"description\": \"Get Infromation about event server\",\n    \"actionIdentifier\": action_identifier\n}\n\n\nclass ObjectFactory:\n    session = None\n    status_factory = None\n    checker_thread = None\n    last_trigger = None\n\n\nclass Status:\n    default_item = {\n        \"type\": \"label\",\n        \"value\": \"Process info is not available at this moment.\"\n    }\n\n    def __init__(self, name, label, parent):\n        self.name = name\n        self.label = label or name\n        self.parent = parent\n\n        self.info = None\n        self.last_update = None\n\n    def update(self, info):\n        self.last_update = datetime.datetime.now()\n        self.info = info\n\n    def get_delta_string(self, delta):\n        days, hours, minutes = (\n            delta.days, delta.seconds // 3600, delta.seconds // 60 % 60\n        )\n        delta_items = [\n            \"{}d\".format(days),\n            \"{}h\".format(hours),\n            \"{}m\".format(minutes)\n        ]\n        if not days:\n            delta_items.pop(0)\n            if not hours:\n                delta_items.pop(0)\n                delta_items.append(\"{}s\".format(delta.seconds % 60))\n                if not minutes:\n                    delta_items.pop(0)\n\n        return \" \".join(delta_items)\n\n    def get_items(self):\n        items = []\n        last_update = \"N/A\"\n        if self.last_update:\n            delta = datetime.datetime.now() - self.last_update\n            last_update = \"{} ago\".format(\n                self.get_delta_string(delta)\n            )\n\n        last_update = \"Updated: {}\".format(last_update)\n        items.append({\n            \"type\": \"label\",\n            \"value\": \"#{}\".format(self.label)\n        })\n        items.append({\n            \"type\": \"label\",\n            \"value\": \"##{}\".format(last_update)\n        })\n\n        if not self.info:\n            if self.info is None:\n                trigger_info_get()\n            items.append(self.default_item)\n            return items\n\n        info = {}\n        for key, value in self.info.items():\n            if key not in [\"created_at:\", \"created_at\"]:\n                info[key] = value\n                continue\n\n            datetime_value = datetime.datetime.strptime(\n                value, \"%Y.%m.%d %H:%M:%S\"\n            )\n            delta = datetime.datetime.now() - datetime_value\n\n            running_for = self.get_delta_string(delta)\n            info[\"Started at\"] = \"{} [running: {}]\".format(value, running_for)\n\n        for key, value in info.items():\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"<b>{}:</b> {}\".format(key, value)\n            })\n\n        return items\n\n\nclass StatusFactory:\n\n    note_item = {\n        \"type\": \"label\",\n        \"value\": (\n            \"<i>HINT: To refresh data uncheck\"\n            \" all checkboxes and hit `Submit` button.</i>\"\n        )\n    }\n    splitter_item = {\n        \"type\": \"label\",\n        \"value\": \"---\"\n    }\n\n    def __init__(self, statuses={}):\n        self.statuses = []\n        for status in statuses.items():\n            self.create_status(*status)\n\n    def __getitem__(self, key):\n        return self.get(key)\n\n    def get(self, key, default=None):\n        for status in self.statuses:\n            if status.name == key:\n                return status\n        return default\n\n    def is_filled(self):\n        for status in self.statuses:\n            if status.info is None:\n                return False\n        return True\n\n    def create_status(self, name, label):\n        new_status = Status(name, label, self)\n        self.statuses.append(new_status)\n\n    def process_event_result(self, event):\n        subprocess_id = event[\"data\"].get(\"subprocess_id\")\n        if subprocess_id != os.environ[\"FTRACK_EVENT_SUB_ID\"]:\n            return\n\n        source = event[\"data\"][\"source\"]\n        data = collections.OrderedDict(event[\"data\"][\"status_info\"])\n\n        self.update_status_info(source, data)\n\n    def update_status_info(self, process_name, info):\n        for status in self.statuses:\n            if status.name == process_name:\n                status.update(info)\n                break\n\n    def bool_items(self):\n        items = []\n        items.append({\n            \"type\": \"label\",\n            \"value\": \"#Restart process\"\n        })\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"<i><b>WARNING:</b> Main process may shut down when checked\"\n                \" if does not run as a service!</i>\"\n            )\n        })\n\n        name_labels = {}\n        for status in self.statuses:\n            name_labels[status.name] = status.label\n\n        for name, label in name_labels.items():\n            items.append({\n                \"type\": \"boolean\",\n                \"value\": False,\n                \"label\": label,\n                \"name\": name\n            })\n        return items\n\n    def openpype_version_items(self):\n        items = []\n        is_latest = is_current_version_studio_latest()\n        items.append({\n            \"type\": \"label\",\n            \"value\": \"# OpenPype version\"\n        })\n        if not is_running_from_build():\n            items.append({\n                \"type\": \"label\",\n                \"value\": (\n                    \"OpenPype event server is running from code <b>{}</b>.\"\n                ).format(str(get_openpype_version()))\n            })\n\n        elif is_latest is None:\n            items.append({\n                \"type\": \"label\",\n                \"value\": (\n                    \"Can't determine if OpenPype version is outdated\"\n                    \" <b>{}</b>. OpenPype build version should be updated.\"\n                ).format(str(get_openpype_version()))\n            })\n        elif is_latest:\n            items.append({\n                \"type\": \"label\",\n                \"value\": \"OpenPype version is up to date <b>{}</b>.\".format(\n                    str(get_openpype_version())\n                )\n            })\n        else:\n            items.append({\n                \"type\": \"label\",\n                \"value\": (\n                    \"Using <b>outdated</b> OpenPype version <b>{}</b>.\"\n                    \" Expected version is <b>{}</b>.\"\n                    \"<br/>- Please restart event server for automatic\"\n                    \" updates or update manually.\"\n                ).format(\n                    str(get_openpype_version()),\n                    str(get_expected_version())\n                )\n            })\n\n        items.append({\n            \"type\": \"label\",\n            \"value\": (\n                \"Local versions dir: {}<br/>Version repository path: {}\"\n            ).format(\n                appdirs.user_data_dir(\"openpype\", \"pypeclub\"),\n                os.environ.get(\"OPENPYPE_PATH\")\n            )\n        })\n        items.append({\"type\": \"label\", \"value\": \"---\"})\n\n        return items\n\n    def items(self):\n        items = []\n        items.extend(self.openpype_version_items())\n        items.append(self.note_item)\n        items.extend(self.bool_items())\n\n        for status in self.statuses:\n            items.append(self.splitter_item)\n            items.extend(status.get_items())\n\n        return items\n\n\ndef server_activity_validate_user(event):\n    \"\"\"Validate user permissions to show server info.\"\"\"\n    session = ObjectFactory.session\n\n    username = event[\"source\"].get(\"user\", {}).get(\"username\")\n    if not username:\n        return False\n\n    user_ent = session.query(\n        \"User where username = \\\"{}\\\"\".format(username)\n    ).first()\n    if not user_ent:\n        return False\n\n    role_list = {\"pypeclub\", \"administrator\"}\n    for role in user_ent[\"user_security_roles\"]:\n        if role[\"security_role\"][\"name\"].lower() in role_list:\n            return True\n    return False\n\n\ndef server_activity_discover(event):\n    \"\"\"Discover action in actions menu conditions.\"\"\"\n    session = ObjectFactory.session\n    if session is None:\n        return\n\n    if not server_activity_validate_user(event):\n        return\n\n    return {\"items\": [action_data]}\n\n\ndef server_activity(event):\n    session = ObjectFactory.session\n    if session is None:\n        msg = \"Session is not set. Can't trigger Reset action.\"\n        log.warning(msg)\n        return {\n            \"success\": False,\n            \"message\": msg\n        }\n\n    if not server_activity_validate_user(event):\n        return {\n            \"success\": False,\n            \"message\": \"You don't have permissions to see Event server status!\"\n        }\n\n    values = event[\"data\"].get(\"values\") or {}\n    is_checked = False\n    for value in values.values():\n        if value:\n            is_checked = True\n            break\n\n    if not is_checked:\n        return {\n            \"items\": ObjectFactory.status_factory.items(),\n            \"title\": \"Server current status\"\n        }\n\n    session = ObjectFactory.session\n    if values[\"main\"]:\n        session.event_hub.sock.sendall(b\"RestartM\")\n        return\n\n    if values[\"storer\"]:\n        session.event_hub.sock.sendall(b\"RestartS\")\n\n    if values[\"processor\"]:\n        session.event_hub.sock.sendall(b\"RestartP\")\n\n\ndef trigger_info_get():\n    if ObjectFactory.last_trigger:\n        delta = datetime.datetime.now() - ObjectFactory.last_trigger\n        if delta.seconds() < 5:\n            return\n\n    session = ObjectFactory.session\n    session.event_hub.publish(\n        ftrack_api.event.base.Event(\n            topic=TOPIC_STATUS_SERVER,\n            data={\"subprocess_id\": os.environ[\"FTRACK_EVENT_SUB_ID\"]}\n        ),\n        on_error=\"ignore\"\n    )\n\n\ndef on_start(event):\n    session = ObjectFactory.session\n    source_id = event.get(\"source\", {}).get(\"id\")\n    if not source_id or source_id != session.event_hub.id:\n        return\n\n    if session is None:\n        log.warning(\"Session is not set. Can't trigger Sync to avalon action.\")\n        return True\n    trigger_info_get()\n\n\ndef register(session):\n    '''Registers the event, subscribing the discover and launch topics.'''\n    session.event_hub.subscribe(\n        \"topic=ftrack.action.discover\",\n        server_activity_discover\n    )\n    session.event_hub.subscribe(\"topic=openpype.status.started\", on_start)\n\n    status_launch_subscription = (\n        \"topic=ftrack.action.launch and data.actionIdentifier={}\"\n    ).format(action_identifier)\n\n    session.event_hub.subscribe(\n        status_launch_subscription,\n        server_activity\n    )\n\n    session.event_hub.subscribe(\n        \"topic={}\".format(TOPIC_STATUS_SERVER_RESULT),\n        ObjectFactory.status_factory.process_event_result\n    )\n\n\ndef heartbeat():\n    if ObjectFactory.status_factory.is_filled():\n        return\n\n    trigger_info_get()\n\n\ndef main(args):\n    port = int(args[-1])\n    server_info = collections.OrderedDict(json.loads(args[-2]))\n\n    # Create a TCP/IP socket\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n\n    # Connect the socket to the port where the server is listening\n    server_address = (\"localhost\", port)\n    log.debug(\"Statuser connected to {} port {}\".format(*server_address))\n    sock.connect(server_address)\n    sock.sendall(b\"CreatedStatus\")\n    # store socket connection object\n    ObjectFactory.sock = sock\n\n    ObjectFactory.status_factory[\"main\"].update(server_info)\n    _returncode = 0\n    try:\n        session = SocketSession(\n            auto_connect_event_hub=True, sock=sock, Eventhub=StatusEventHub\n        )\n        ObjectFactory.session = session\n        session.event_hub.heartbeat_callbacks.append(heartbeat)\n        register(session)\n        server = FtrackServer()\n        log.debug(\"Launched Ftrack Event statuser\")\n\n        server.run_server(session, load_files=False)\n\n    except Exception:\n        _returncode = 1\n        log.error(\"ServerInfo subprocess crashed\", exc_info=True)\n\n    finally:\n        log.debug(\"Ending. Closing socket.\")\n        sock.close()\n        return _returncode\n\n\nclass OutputChecker(threading.Thread):\n    read_input = True\n\n    def run(self):\n        while self.read_input:\n            for line in sys.stdin:\n                line = line.rstrip().lower()\n                if not line.startswith(\"reset:\"):\n                    continue\n                process_name = line.replace(\"reset:\", \"\")\n\n                ObjectFactory.status_factory.update_status_info(\n                    process_name, None\n                )\n\n    def stop(self):\n        self.read_input = False\n\n\nif __name__ == \"__main__\":\n    # Register interupt signal\n    def signal_handler(sig, frame):\n        print(\"You pressed Ctrl+C. Process ended.\")\n        ObjectFactory.checker_thread.stop()\n        sys.exit(0)\n\n    signal.signal(signal.SIGINT, signal_handler)\n    signal.signal(signal.SIGTERM, signal_handler)\n\n    statuse_names = {\n        \"main\": \"Main process\",\n        \"storer\": \"Event Storer\",\n        \"processor\": \"Event Processor\"\n    }\n    ObjectFactory.status_factory = StatusFactory(statuse_names)\n\n    checker_thread = OutputChecker()\n    ObjectFactory.checker_thread = checker_thread\n    checker_thread.start()\n\n    sys.exit(main(sys.argv))\n"
  },
  {
    "path": "openpype/modules/ftrack/scripts/sub_event_storer.py",
    "content": "import os\nimport sys\nimport datetime\nimport signal\nimport socket\nimport pymongo\n\nimport ftrack_api\n\nfrom openpype.client import OpenPypeMongoConnection\nfrom openpype_modules.ftrack.ftrack_server.ftrack_server import FtrackServer\nfrom openpype_modules.ftrack.ftrack_server.lib import (\n    SocketSession,\n    StorerEventHub,\n    TOPIC_STATUS_SERVER,\n    TOPIC_STATUS_SERVER_RESULT\n)\nfrom openpype_modules.ftrack.lib import get_ftrack_event_mongo_info\nfrom openpype.lib import (\n    Logger,\n    get_openpype_version,\n    get_build_version\n)\n\nlog = Logger.get_logger(\"Event storer\")\nsubprocess_started = datetime.datetime.now()\n\n\nclass SessionFactory:\n    session = None\n\n\ndatabase_name, collection_name = get_ftrack_event_mongo_info()\ndbcon = None\n\n# ignore_topics = [\"ftrack.meta.connected\"]\nignore_topics = []\n\n\ndef install_db():\n    global dbcon\n    try:\n        mongo_client = OpenPypeMongoConnection.get_mongo_client()\n        dbcon = mongo_client[database_name][collection_name]\n    except pymongo.errors.AutoReconnect:\n        log.error(\"Mongo server \\\"{}\\\" is not responding, exiting.\".format(\n            OpenPypeMongoConnection.get_default_mongo_url()\n        ))\n        sys.exit(0)\n\n\ndef launch(event):\n    if event.get(\"topic\") in ignore_topics:\n        return\n\n    event_data = event._data\n    event_id = event[\"id\"]\n\n    event_data[\"pype_data\"] = {\n        \"stored\": datetime.datetime.utcnow(),\n        \"is_processed\": False\n    }\n\n    try:\n        # dbcon.insert_one(event_data)\n        dbcon.replace_one({\"id\": event_id}, event_data, upsert=True)\n        log.debug(\"Event: {} stored\".format(event_id))\n\n    except pymongo.errors.AutoReconnect:\n        log.error(\"Mongo server \\\"{}\\\" is not responding, exiting.\".format(\n            os.environ[\"OPENPYPE_MONGO\"]\n        ))\n        sys.exit(0)\n\n    except Exception as exc:\n        log.error(\n            \"Event: {} failed to store\".format(event_id),\n            exc_info=True\n        )\n\n\ndef trigger_sync(event):\n    session = SessionFactory.session\n    source_id = event.get(\"source\", {}).get(\"id\")\n    if not source_id or source_id != session.event_hub.id:\n        return\n\n    if session is None:\n        log.warning(\"Session is not set. Can't trigger Sync to avalon action.\")\n        return True\n\n    projects = session.query(\"Project\").all()\n    if not projects:\n        return True\n\n    query = {\n        \"pype_data.is_processed\": False,\n        \"topic\": \"ftrack.action.launch\",\n        \"data.actionIdentifier\": \"sync.to.avalon.server\"\n    }\n    set_dict = {\n        \"$set\": {\"pype_data.is_processed\": True}\n    }\n    dbcon.update_many(query, set_dict)\n\n    selections = []\n    for project in projects:\n        if project[\"status\"] != \"active\":\n            continue\n\n        auto_sync = project[\"custom_attributes\"].get(\"avalon_auto_sync\")\n        if not auto_sync:\n            continue\n\n        selections.append({\n            \"entityId\": project[\"id\"],\n            \"entityType\": \"show\"\n        })\n\n    if not selections:\n        return\n\n    user = session.query(\n        \"User where username is \\\"{}\\\"\".format(session.api_user)\n    ).one()\n    user_data = {\n        \"username\": user[\"username\"],\n        \"id\": user[\"id\"]\n    }\n\n    for selection in selections:\n        event_data = {\n            \"actionIdentifier\": \"sync.to.avalon.server\",\n            \"selection\": [selection]\n        }\n        session.event_hub.publish(\n            ftrack_api.event.base.Event(\n                topic=\"ftrack.action.launch\",\n                data=event_data,\n                source=dict(user=user_data)\n            ),\n            on_error=\"ignore\"\n        )\n\n\ndef send_status(event):\n    session = SessionFactory.session\n    if not session:\n        return\n\n    subprocess_id = event[\"data\"].get(\"subprocess_id\")\n    if not subprocess_id:\n        return\n\n    if subprocess_id != os.environ[\"FTRACK_EVENT_SUB_ID\"]:\n        return\n\n    new_event_data = {\n        \"subprocess_id\": os.environ[\"FTRACK_EVENT_SUB_ID\"],\n        \"source\": \"storer\",\n        \"status_info\": [\n            [\"created_at\", subprocess_started.strftime(\"%Y.%m.%d %H:%M:%S\")],\n            [\"OpenPype version\", get_openpype_version() or \"N/A\"],\n            [\"OpenPype build version\", get_build_version() or \"N/A\"]\n        ]\n    }\n\n    new_event = ftrack_api.event.base.Event(\n        topic=TOPIC_STATUS_SERVER_RESULT,\n        data=new_event_data\n    )\n\n    session.event_hub.publish(new_event)\n\n\ndef register(session):\n    '''Registers the event, subscribing the discover and launch topics.'''\n    install_db()\n    session.event_hub.subscribe(\"topic=*\", launch)\n    session.event_hub.subscribe(\"topic=openpype.storer.started\", trigger_sync)\n    session.event_hub.subscribe(\n        \"topic={}\".format(TOPIC_STATUS_SERVER), send_status\n    )\n\n\ndef main(args):\n    port = int(args[-1])\n\n    # Create a TCP/IP socket\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n\n    # Connect the socket to the port where the server is listening\n    server_address = (\"localhost\", port)\n    log.debug(\"Storer connected to {} port {}\".format(*server_address))\n    sock.connect(server_address)\n    sock.sendall(b\"CreatedStore\")\n\n    try:\n        session = SocketSession(\n            auto_connect_event_hub=True, sock=sock, Eventhub=StorerEventHub\n        )\n        SessionFactory.session = session\n        register(session)\n        server = FtrackServer()\n        log.debug(\"Launched Ftrack Event storer\")\n        server.run_server(session, load_files=False)\n\n    except pymongo.errors.OperationFailure:\n        log.error((\n            \"Error with Mongo access, probably permissions.\"\n            \"Check if exist database with name \\\"{}\\\"\"\n            \" and collection \\\"{}\\\" inside.\"\n        ).format(database_name, collection_name))\n        sock.sendall(b\"MongoError\")\n\n    finally:\n        log.debug(\"First closing socket\")\n        sock.close()\n        return 1\n\n\nif __name__ == \"__main__\":\n    # Register interupt signal\n    def signal_handler(sig, frame):\n        print(\"You pressed Ctrl+C. Process ended.\")\n        sys.exit(0)\n\n    signal.signal(signal.SIGINT, signal_handler)\n    signal.signal(signal.SIGTERM, signal_handler)\n\n    sys.exit(main(sys.argv))\n"
  },
  {
    "path": "openpype/modules/ftrack/scripts/sub_legacy_server.py",
    "content": "import sys\nimport time\nimport datetime\nimport signal\nimport threading\n\nimport ftrack_api\nfrom openpype.lib import Logger\nfrom openpype.modules import ModulesManager\nfrom openpype_modules.ftrack.ftrack_server.ftrack_server import FtrackServer\n\nlog = Logger.get_logger(\"Event Server Legacy\")\n\n\nclass TimerChecker(threading.Thread):\n    max_time_out = 35\n\n    def __init__(self, server, session):\n        self.server = server\n        self.session = session\n        self.is_running = False\n        self.failed = False\n        super().__init__()\n\n    def stop(self):\n        self.is_running = False\n\n    def run(self):\n        start = datetime.datetime.now()\n        self.is_running = True\n        connected = False\n\n        while True:\n            if not self.is_running:\n                break\n\n            if not self.session.event_hub.connected:\n                if not connected:\n                    if (\n                        (datetime.datetime.now() - start).seconds >\n                        self.max_time_out\n                    ):\n                        log.error((\n                            \"Exiting event server. Session was not connected\"\n                            \" to ftrack server in {} seconds.\"\n                        ).format(self.max_time_out))\n                        self.failed = True\n                        break\n                else:\n                    log.error(\n                        \"Exiting event server. Event Hub is not connected.\"\n                    )\n                    self.server.stop_session()\n                    self.failed = True\n                    break\n            else:\n                if not connected:\n                    connected = True\n\n            time.sleep(1)\n\n\ndef main(args):\n    check_thread = None\n    try:\n        manager = ModulesManager()\n        ftrack_module = manager.modules_by_name[\"ftrack\"]\n        server = FtrackServer(\n            ftrack_module.server_event_handlers_paths\n        )\n        session = ftrack_api.Session(auto_connect_event_hub=True)\n\n        check_thread = TimerChecker(server, session)\n        check_thread.start()\n\n        log.debug(\"Launching Ftrack Event Legacy Server\")\n        server.run_server(session)\n\n    except Exception as exc:\n        import traceback\n        traceback.print_tb(exc.__traceback__)\n\n    finally:\n        log_info = True\n        if check_thread is not None:\n            check_thread.stop()\n            check_thread.join()\n            if check_thread.failed:\n                log_info = False\n        if log_info:\n            log.info(\"Exiting Event server subprocess\")\n        return 1\n\n\nif __name__ == \"__main__\":\n    # Register interupt signal\n    def signal_handler(sig, frame):\n        print(\"You pressed Ctrl+C. Process ended.\")\n        sys.exit(0)\n\n    signal.signal(signal.SIGINT, signal_handler)\n    signal.signal(signal.SIGTERM, signal_handler)\n\n    sys.exit(main(sys.argv))\n"
  },
  {
    "path": "openpype/modules/ftrack/scripts/sub_user_server.py",
    "content": "import sys\nimport signal\nimport socket\n\nfrom openpype.lib import Logger\nfrom openpype_modules.ftrack.ftrack_server.ftrack_server import FtrackServer\nfrom openpype_modules.ftrack.ftrack_server.lib import (\n    SocketSession,\n    SocketBaseEventHub\n)\nfrom openpype.modules import ModulesManager\n\nlog = Logger.get_logger(\"FtrackUserServer\")\n\n\ndef main(args):\n    port = int(args[-1])\n\n    # Create a TCP/IP socket\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n\n    # Connect the socket to the port where the server is listening\n    server_address = (\"localhost\", port)\n    log.debug(\n        \"User Ftrack Server connected to {} port {}\".format(*server_address)\n    )\n    sock.connect(server_address)\n    sock.sendall(b\"CreatedUser\")\n\n    try:\n        session = SocketSession(\n            auto_connect_event_hub=True, sock=sock, Eventhub=SocketBaseEventHub\n        )\n        manager = ModulesManager()\n        ftrack_module = manager.modules_by_name[\"ftrack\"]\n        ftrack_module.user_event_handlers_paths\n        server = FtrackServer(\n            ftrack_module.user_event_handlers_paths\n        )\n        log.debug(\"Launching User Ftrack Server\")\n        server.run_server(session=session)\n\n    except Exception:\n        log.warning(\"Ftrack session server failed.\", exc_info=True)\n\n    finally:\n        log.debug(\"Closing socket\")\n        sock.close()\n        return 1\n\n\nif __name__ == \"__main__\":\n    Logger.set_process_name(\"Ftrack User server\")\n\n    # Register interupt signal\n    def signal_handler(sig, frame):\n        log.info(\n            \"Process was forced to stop. Process ended.\"\n        )\n        sys.exit(0)\n\n    signal.signal(signal.SIGINT, signal_handler)\n    signal.signal(signal.SIGTERM, signal_handler)\n\n    sys.exit(main(sys.argv))\n"
  },
  {
    "path": "openpype/modules/ftrack/tray/__init__.py",
    "content": "from .ftrack_tray import FtrackTrayWrapper\n\n\n__all__ = (\n    \"FtrackTrayWrapper\",\n)\n"
  },
  {
    "path": "openpype/modules/ftrack/tray/ftrack_tray.py",
    "content": "import os\nimport time\nimport datetime\nimport threading\n\nimport ftrack_api\nfrom qtpy import QtCore, QtWidgets, QtGui\n\nfrom openpype import resources\nfrom openpype.lib import Logger\nfrom openpype_modules.ftrack import resolve_ftrack_url, FTRACK_MODULE_DIR\nfrom openpype_modules.ftrack.ftrack_server import socket_thread\nfrom openpype_modules.ftrack.lib import credentials\nfrom . import login_dialog\n\n\nclass FtrackTrayWrapper:\n    def __init__(self, module):\n        self.module = module\n        self.log = Logger.get_logger(self.__class__.__name__)\n\n        self.thread_action_server = None\n        self.thread_socket_server = None\n        self.thread_timer = None\n\n        self.bool_logged = False\n        self.bool_action_server_running = False\n        self.bool_action_thread_running = False\n        self.bool_timer_event = False\n\n        self.widget_login = login_dialog.CredentialsDialog(module)\n        self.widget_login.login_changed.connect(self.on_login_change)\n        self.widget_login.logout_signal.connect(self.on_logout)\n\n        self.action_credentials = None\n        self.tray_server_menu = None\n        self.icon_logged = QtGui.QIcon(\n            resources.get_resource(\"icons\", \"circle_green.png\")\n        )\n        self.icon_not_logged = QtGui.QIcon(\n            resources.get_resource(\"icons\", \"circle_orange.png\")\n        )\n\n    def show_login_widget(self):\n        self.widget_login.show()\n        self.widget_login.activateWindow()\n        self.widget_login.raise_()\n\n    def show_ftrack_browser(self):\n        QtGui.QDesktopServices.openUrl(self.module.ftrack_url)\n\n    def validate(self):\n        validation = False\n        cred = credentials.get_credentials()\n        ft_user = cred.get(\"username\")\n        ft_api_key = cred.get(\"api_key\")\n        validation = credentials.check_credentials(ft_user, ft_api_key)\n        if validation:\n            self.widget_login.set_credentials(ft_user, ft_api_key)\n            self.module.set_credentials_to_env(ft_user, ft_api_key)\n            self.log.info(\"Connected to Ftrack successfully\")\n            self.on_login_change()\n\n            return validation\n\n        if not validation and ft_user and ft_api_key:\n            self.log.warning(\n                \"Current Ftrack credentials are not valid. {}: {} - {}\".format(\n                    str(os.environ.get(\"FTRACK_SERVER\")), ft_user, ft_api_key\n                )\n            )\n\n        self.log.info(\"Please sign in to Ftrack\")\n        self.bool_logged = False\n        self.show_login_widget()\n        self.set_menu_visibility()\n\n        return validation\n\n    # Necessary - login_dialog works with this method after logging in\n    def on_login_change(self):\n        self.bool_logged = True\n\n        if self.action_credentials:\n            self.action_credentials.setIcon(self.icon_logged)\n            self.action_credentials.setToolTip(\n                \"Logged as user \\\"{}\\\"\".format(\n                    self.widget_login.user_input.text()\n                )\n            )\n\n        self.set_menu_visibility()\n        self.start_action_server()\n\n    def on_logout(self):\n        credentials.clear_credentials()\n        self.stop_action_server()\n\n        if self.action_credentials:\n            self.action_credentials.setIcon(self.icon_not_logged)\n            self.action_credentials.setToolTip(\"Logged out\")\n\n        self.log.info(\"Logged out of Ftrack\")\n        self.bool_logged = False\n        self.set_menu_visibility()\n\n    # Actions part\n    def start_action_server(self):\n        if self.thread_action_server is None:\n            self.thread_action_server = threading.Thread(\n                target=self.set_action_server\n            )\n            self.thread_action_server.start()\n\n    def set_action_server(self):\n        if self.bool_action_server_running:\n            return\n\n        self.bool_action_server_running = True\n        self.bool_action_thread_running = False\n\n        ftrack_url = self.module.ftrack_url\n        os.environ[\"FTRACK_SERVER\"] = ftrack_url\n\n        min_fail_seconds = 5\n        max_fail_count = 3\n        wait_time_after_max_fail = 10\n\n        # Threads data\n        thread_name = \"ActionServerThread\"\n        thread_port = 10021\n        subprocess_path = (\n            \"{}/scripts/sub_user_server.py\".format(FTRACK_MODULE_DIR)\n        )\n        if self.thread_socket_server is not None:\n            self.thread_socket_server.stop()\n            self.thread_socket_server.join()\n            self.thread_socket_server = None\n\n        last_failed = datetime.datetime.now()\n        failed_count = 0\n\n        ftrack_accessible = False\n        printed_ftrack_error = False\n\n        # Main loop\n        while True:\n            if not self.bool_action_server_running:\n                self.log.debug(\"Action server was pushed to stop.\")\n                break\n\n            # Check if accessible Ftrack and Mongo url\n            if not ftrack_accessible:\n                ftrack_accessible = resolve_ftrack_url(ftrack_url)\n\n            # Run threads only if Ftrack is accessible\n            if not ftrack_accessible:\n                if not printed_ftrack_error:\n                    self.log.warning(\n                        \"Can't access Ftrack {}\".format(ftrack_url)\n                    )\n\n                if self.thread_socket_server is not None:\n                    self.thread_socket_server.stop()\n                    self.thread_socket_server.join()\n                    self.thread_socket_server = None\n                    self.bool_action_thread_running = False\n                    self.set_menu_visibility()\n\n                printed_ftrack_error = True\n\n                time.sleep(1)\n                continue\n\n            printed_ftrack_error = False\n\n            # Run backup thread which does not require mongo to work\n            if self.thread_socket_server is None:\n                if failed_count < max_fail_count:\n                    self.thread_socket_server = socket_thread.SocketThread(\n                        thread_name, thread_port, subprocess_path\n                    )\n                    self.thread_socket_server.start()\n                    self.bool_action_thread_running = True\n                    self.set_menu_visibility()\n\n                elif failed_count == max_fail_count:\n                    self.log.warning((\n                        \"Action server failed {} times.\"\n                        \" I'll try to run again {}s later\"\n                    ).format(\n                        str(max_fail_count), str(wait_time_after_max_fail))\n                    )\n                    failed_count += 1\n\n                elif ((\n                    datetime.datetime.now() - last_failed\n                ).seconds > wait_time_after_max_fail):\n                    failed_count = 0\n\n            # If thread failed test Ftrack and Mongo connection\n            elif not self.thread_socket_server.is_alive():\n                self.thread_socket_server.join()\n                self.thread_socket_server = None\n                ftrack_accessible = False\n\n                self.bool_action_thread_running = False\n                self.set_menu_visibility()\n\n                _last_failed = datetime.datetime.now()\n                delta_time = (_last_failed - last_failed).seconds\n                if delta_time < min_fail_seconds:\n                    failed_count += 1\n                else:\n                    failed_count = 0\n                last_failed = _last_failed\n\n            time.sleep(1)\n\n        self.bool_action_thread_running = False\n        self.bool_action_server_running = False\n        self.set_menu_visibility()\n\n    def reset_action_server(self):\n        self.stop_action_server()\n        self.start_action_server()\n\n    def stop_action_server(self):\n        try:\n            self.bool_action_server_running = False\n            if self.thread_socket_server is not None:\n                self.thread_socket_server.stop()\n                self.thread_socket_server.join()\n                self.thread_socket_server = None\n\n            if self.thread_action_server is not None:\n                self.thread_action_server.join()\n                self.thread_action_server = None\n\n            self.log.info(\"Ftrack action server was forced to stop\")\n\n        except Exception:\n            self.log.warning(\n                \"Error has happened during Killing action server\",\n                exc_info=True\n            )\n\n    # Definition of Tray menu\n    def tray_menu(self, parent_menu):\n        # Menu for Tray App\n        tray_menu = QtWidgets.QMenu(\"Ftrack\", parent_menu)\n\n        # Actions - basic\n        action_credentials = QtWidgets.QAction(\"Credentials\", tray_menu)\n        action_credentials.triggered.connect(self.show_login_widget)\n        if self.bool_logged:\n            icon = self.icon_logged\n        else:\n            icon = self.icon_not_logged\n        action_credentials.setIcon(icon)\n        tray_menu.addAction(action_credentials)\n        self.action_credentials = action_credentials\n\n        # Actions - server\n        tray_server_menu = tray_menu.addMenu(\"Action server\")\n\n        self.action_server_run = QtWidgets.QAction(\n            \"Run action server\", tray_server_menu\n        )\n        self.action_server_reset = QtWidgets.QAction(\n            \"Reset action server\", tray_server_menu\n        )\n        self.action_server_stop = QtWidgets.QAction(\n            \"Stop action server\", tray_server_menu\n        )\n\n        self.action_server_run.triggered.connect(self.start_action_server)\n        self.action_server_reset.triggered.connect(self.reset_action_server)\n        self.action_server_stop.triggered.connect(self.stop_action_server)\n\n        tray_server_menu.addAction(self.action_server_run)\n        tray_server_menu.addAction(self.action_server_reset)\n        tray_server_menu.addAction(self.action_server_stop)\n\n        self.tray_server_menu = tray_server_menu\n\n        # Ftrack Browser\n        browser_open = QtWidgets.QAction(\"Open Ftrack...\", tray_menu)\n        browser_open.triggered.connect(self.show_ftrack_browser)\n        tray_menu.addAction(browser_open)\n        self.browser_open = browser_open\n\n        self.bool_logged = False\n        self.set_menu_visibility()\n\n        parent_menu.addMenu(tray_menu)\n\n    def tray_exit(self):\n        self.stop_action_server()\n        self.stop_timer_thread()\n\n    # Definition of visibility of each menu actions\n    def set_menu_visibility(self):\n        self.tray_server_menu.menuAction().setVisible(self.bool_logged)\n        if self.bool_logged is False:\n            if self.bool_timer_event is True:\n                self.stop_timer_thread()\n            return\n\n        self.action_server_run.setVisible(not self.bool_action_server_running)\n        self.action_server_reset.setVisible(self.bool_action_thread_running)\n        self.action_server_stop.setVisible(self.bool_action_server_running)\n\n        if self.bool_timer_event is False:\n            self.start_timer_thread()\n\n    def start_timer_thread(self):\n        try:\n            if self.thread_timer is None:\n                self.thread_timer = FtrackEventsThread(self)\n                self.bool_timer_event = True\n                self.thread_timer.signal_timer_started.connect(\n                    self.timer_started\n                )\n                self.thread_timer.signal_timer_stopped.connect(\n                    self.timer_stopped\n                )\n                self.thread_timer.start()\n        except Exception:\n            pass\n\n    def stop_timer_thread(self):\n        try:\n            if self.thread_timer is not None:\n                self.thread_timer.terminate()\n                self.thread_timer.wait()\n                self.thread_timer = None\n\n        except Exception as e:\n            self.log.error(\"During Killing Timer event server: {0}\".format(e))\n\n    def changed_user(self):\n        self.stop_action_server()\n        self.module.set_credentials_to_env(None, None)\n        self.validate()\n\n    def start_timer_manager(self, data):\n        if self.thread_timer is not None:\n            self.thread_timer.ftrack_start_timer(data)\n\n    def stop_timer_manager(self):\n        if self.thread_timer is not None:\n            self.thread_timer.ftrack_stop_timer()\n\n    def timer_started(self, data):\n        self.module.timer_started(data)\n\n    def timer_stopped(self):\n        self.module.timer_stopped()\n\n\nclass FtrackEventsThread(QtCore.QThread):\n    # Senders\n    signal_timer_started = QtCore.Signal(object)\n    signal_timer_stopped = QtCore.Signal()\n\n    def __init__(self, parent):\n        super(FtrackEventsThread, self).__init__()\n        cred = credentials.get_credentials()\n        self.username = cred['username']\n        self.user = None\n        self.last_task = None\n\n    def run(self):\n        self.timer_session = ftrack_api.Session(auto_connect_event_hub=True)\n        self.timer_session.event_hub.subscribe(\n            'topic=ftrack.update and source.user.username={}'.format(\n                self.username\n            ),\n            self.event_handler)\n\n        user_query = 'User where username is \"{}\"'.format(self.username)\n        self.user = self.timer_session.query(user_query).one()\n\n        timer_query = 'Timer where user.username is \"{}\"'.format(self.username)\n        timer = self.timer_session.query(timer_query).first()\n        if timer is not None:\n            self.last_task = timer['context']\n            self.signal_timer_started.emit(\n                self.get_data_from_task(self.last_task)\n            )\n\n        self.timer_session.event_hub.wait()\n\n    def get_data_from_task(self, task_entity):\n        data = {}\n        data['task_name'] = task_entity['name']\n        data['task_type'] = task_entity['type']['name']\n        data['project_name'] = task_entity['project']['full_name']\n        data['hierarchy'] = self.get_parents(task_entity['parent'])\n\n        return data\n\n    def get_parents(self, entity):\n        output = []\n        if entity.entity_type.lower() == 'project':\n            return output\n        output.extend(self.get_parents(entity['parent']))\n        output.append(entity['name'])\n\n        return output\n\n    def event_handler(self, event):\n        try:\n            if event['data']['entities'][0]['objectTypeId'] != 'timer':\n                return\n        except Exception:\n            return\n\n        new = event['data']['entities'][0]['changes']['start']['new']\n        old = event['data']['entities'][0]['changes']['start']['old']\n\n        if old is None and new is None:\n            return\n\n        timer_query = 'Timer where user.username is \"{}\"'.format(self.username)\n        timer = self.timer_session.query(timer_query).first()\n        if timer is not None:\n            self.last_task = timer['context']\n\n        if old is None:\n            self.signal_timer_started.emit(\n                self.get_data_from_task(self.last_task)\n            )\n        elif new is None:\n            self.signal_timer_stopped.emit()\n\n    def ftrack_stop_timer(self):\n        actual_timer = self.timer_session.query(\n            'Timer where user_id = \"{0}\"'.format(self.user['id'])\n        ).first()\n\n        if actual_timer is not None:\n            self.user.stop_timer()\n            self.timer_session.commit()\n            self.signal_timer_stopped.emit()\n\n    def ftrack_start_timer(self, input_data):\n        if self.user is None:\n            return\n\n        actual_timer = self.timer_session.query(\n            'Timer where user_id = \"{0}\"'.format(self.user['id'])\n        ).first()\n        if (\n            actual_timer is not None and\n            input_data['task_name'] == self.last_task['name'] and\n            input_data['hierarchy'][-1] == self.last_task['parent']['name']\n        ):\n            return\n\n        input_data['entity_name'] = input_data['hierarchy'][-1]\n\n        task_query = (\n            'Task where name is \"{task_name}\"'\n            ' and parent.name is \"{entity_name}\"'\n            ' and project.full_name is \"{project_name}\"'\n        ).format(**input_data)\n\n        task = self.timer_session.query(task_query).one()\n        self.last_task = task\n        self.user.start_timer(task)\n        self.timer_session.commit()\n        self.signal_timer_started.emit(\n            self.get_data_from_task(self.last_task)\n        )\n"
  },
  {
    "path": "openpype/modules/ftrack/tray/login_dialog.py",
    "content": "import requests\nfrom qtpy import QtCore, QtGui, QtWidgets\n\nfrom openpype import style\nfrom openpype_modules.ftrack.lib import credentials\nfrom openpype import resources\n\nfrom . import login_tools\n\n\nclass CredentialsDialog(QtWidgets.QDialog):\n    SIZE_W = 300\n    SIZE_H = 230\n\n    login_changed = QtCore.Signal()\n    logout_signal = QtCore.Signal()\n\n    def __init__(self, module, parent=None):\n        super(CredentialsDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"OpenPype - Ftrack Login\")\n\n        self._module = module\n\n        self._login_server_thread = None\n        self._is_logged = False\n        self._in_advance_mode = False\n\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n\n        self.setWindowFlags(\n            QtCore.Qt.WindowCloseButtonHint |\n            QtCore.Qt.WindowMinimizeButtonHint\n        )\n\n        self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))\n        self.setMaximumSize(QtCore.QSize(self.SIZE_W + 100, self.SIZE_H + 100))\n        self.setStyleSheet(style.load_stylesheet())\n\n        self.login_changed.connect(self._on_login)\n\n        self.ui_init()\n\n    def ui_init(self):\n        self.ftsite_label = QtWidgets.QLabel(\"Ftrack URL:\")\n        self.user_label = QtWidgets.QLabel(\"Username:\")\n        self.api_label = QtWidgets.QLabel(\"API Key:\")\n\n        self.ftsite_input = QtWidgets.QLabel()\n        self.ftsite_input.setTextInteractionFlags(\n            QtCore.Qt.TextBrowserInteraction\n        )\n        # self.ftsite_input.setReadOnly(True)\n        self.ftsite_input.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))\n\n        self.user_input = QtWidgets.QLineEdit()\n        self.user_input.setPlaceholderText(\"user.name\")\n        self.user_input.textChanged.connect(self._user_changed)\n\n        self.api_input = QtWidgets.QLineEdit()\n        self.api_input.setPlaceholderText(\n            \"e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n        )\n        self.api_input.textChanged.connect(self._api_changed)\n\n        input_layout = QtWidgets.QFormLayout()\n        input_layout.setContentsMargins(10, 15, 10, 5)\n\n        input_layout.addRow(self.ftsite_label, self.ftsite_input)\n        input_layout.addRow(self.user_label, self.user_input)\n        input_layout.addRow(self.api_label, self.api_input)\n\n        self.btn_advanced = QtWidgets.QPushButton(\"Advanced\")\n        self.btn_advanced.clicked.connect(self._on_advanced_clicked)\n\n        self.btn_simple = QtWidgets.QPushButton(\"Simple\")\n        self.btn_simple.clicked.connect(self._on_simple_clicked)\n\n        self.btn_login = QtWidgets.QPushButton(\"Login\")\n        self.btn_login.setToolTip(\n            \"Set Username and API Key with entered values\"\n        )\n        self.btn_login.clicked.connect(self._on_login_clicked)\n\n        self.btn_ftrack_login = QtWidgets.QPushButton(\"Ftrack login\")\n        self.btn_ftrack_login.setToolTip(\"Open browser for Login to Ftrack\")\n        self.btn_ftrack_login.clicked.connect(self._on_ftrack_login_clicked)\n\n        self.btn_logout = QtWidgets.QPushButton(\"Logout\")\n        self.btn_logout.clicked.connect(self._on_logout_clicked)\n\n        self.btn_close = QtWidgets.QPushButton(\"Close\")\n        self.btn_close.setToolTip(\"Close this window\")\n        self.btn_close.clicked.connect(self._close_widget)\n\n        btns_layout = QtWidgets.QHBoxLayout()\n        btns_layout.addWidget(self.btn_advanced)\n        btns_layout.addWidget(self.btn_simple)\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(self.btn_ftrack_login)\n        btns_layout.addWidget(self.btn_login)\n        btns_layout.addWidget(self.btn_logout)\n        btns_layout.addWidget(self.btn_close)\n\n        self.note_label = QtWidgets.QLabel((\n            \"NOTE: Click on \\\"{}\\\" button to log with your default browser\"\n            \" or click on \\\"{}\\\" button to enter API key manually.\"\n        ).format(self.btn_ftrack_login.text(), self.btn_advanced.text()))\n\n        self.note_label.setWordWrap(True)\n        self.note_label.hide()\n\n        self.error_label = QtWidgets.QLabel(\"\")\n        self.error_label.setWordWrap(True)\n        self.error_label.hide()\n\n        label_layout = QtWidgets.QVBoxLayout()\n        label_layout.setContentsMargins(10, 5, 10, 5)\n        label_layout.addWidget(self.note_label)\n        label_layout.addWidget(self.error_label)\n\n        main = QtWidgets.QVBoxLayout(self)\n        main.addLayout(input_layout)\n        main.addLayout(label_layout)\n        main.addStretch(1)\n        main.addLayout(btns_layout)\n\n        self.fill_ftrack_url()\n\n        self.set_is_logged(self._is_logged)\n\n        self.setLayout(main)\n\n    def show(self, *args, **kwargs):\n        super(CredentialsDialog, self).show(*args, **kwargs)\n        self.fill_ftrack_url()\n\n    def fill_ftrack_url(self):\n        checked_url = self.check_url()\n        if checked_url == self.ftsite_input.text():\n            return\n\n        self.ftsite_input.setText(checked_url or \"< Not set >\")\n\n        enabled = bool(checked_url)\n\n        self.btn_login.setEnabled(enabled)\n        self.btn_ftrack_login.setEnabled(enabled)\n\n        self.api_input.setEnabled(enabled)\n        self.user_input.setEnabled(enabled)\n\n        if not checked_url:\n            self.btn_advanced.hide()\n            self.btn_simple.hide()\n            self.btn_ftrack_login.hide()\n            self.btn_login.hide()\n            self.note_label.hide()\n            self.api_input.hide()\n            self.user_input.hide()\n\n    def set_advanced_mode(self, is_advanced):\n        self._in_advance_mode = is_advanced\n\n        self.error_label.setVisible(False)\n\n        is_logged = self._is_logged\n\n        self.note_label.setVisible(not is_logged and not is_advanced)\n        self.btn_ftrack_login.setVisible(not is_logged and not is_advanced)\n        self.btn_advanced.setVisible(not is_logged and not is_advanced)\n\n        self.btn_login.setVisible(not is_logged and is_advanced)\n        self.btn_simple.setVisible(not is_logged and is_advanced)\n\n        self.user_label.setVisible(is_logged or is_advanced)\n        self.user_input.setVisible(is_logged or is_advanced)\n        self.api_label.setVisible(is_logged or is_advanced)\n        self.api_input.setVisible(is_logged or is_advanced)\n        if is_advanced:\n            self.user_input.setFocus()\n        else:\n            self.btn_ftrack_login.setFocus()\n\n    def set_is_logged(self, is_logged):\n        self._is_logged = is_logged\n\n        self.user_input.setReadOnly(is_logged)\n        self.api_input.setReadOnly(is_logged)\n        self.user_input.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))\n        self.api_input.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))\n\n        self.btn_logout.setVisible(is_logged)\n\n        self.set_advanced_mode(self._in_advance_mode)\n\n    def set_error(self, msg):\n        self.error_label.setText(msg)\n        self.error_label.show()\n\n    def _on_logout_clicked(self):\n        self.user_input.setText(\"\")\n        self.api_input.setText(\"\")\n        self.set_is_logged(False)\n        self.logout_signal.emit()\n\n    def _on_simple_clicked(self):\n        self.set_advanced_mode(False)\n\n    def _on_advanced_clicked(self):\n        self.set_advanced_mode(True)\n\n    def _user_changed(self):\n        self._not_invalid_input(self.user_input)\n\n    def _api_changed(self):\n        self._not_invalid_input(self.api_input)\n\n    def _not_invalid_input(self, input_widget):\n        input_widget.setStyleSheet(\"\")\n\n    def _invalid_input(self, input_widget):\n        input_widget.setStyleSheet(\"border: 1px solid red;\")\n\n    def _on_login(self):\n        self.set_is_logged(True)\n        self._close_widget()\n\n    def _on_login_clicked(self):\n        username = self.user_input.text().strip()\n        api_key = self.api_input.text().strip()\n        missing = []\n        if username == \"\":\n            missing.append(\"Username\")\n            self._invalid_input(self.user_input)\n\n        if api_key == \"\":\n            missing.append(\"API Key\")\n            self._invalid_input(self.api_input)\n\n        if len(missing) > 0:\n            self.set_error(\"You didn't enter {}\".format(\" and \".join(missing)))\n            return\n\n        if not self.login_with_credentials(username, api_key):\n            self._invalid_input(self.user_input)\n            self._invalid_input(self.api_input)\n            self.set_error(\n                \"We're unable to sign in to Ftrack with these credentials\"\n            )\n\n    def _on_ftrack_login_clicked(self):\n        url = self.check_url()\n        if not url:\n            return\n\n        # If there is an existing server thread running we need to stop it.\n        if self._login_server_thread:\n            if self._login_server_thread.is_alive():\n                self._login_server_thread.stop()\n            self._login_server_thread.join()\n            self._login_server_thread = None\n\n        # If credentials are not properly set, try to get them using a http\n        # server.\n        self._login_server_thread = login_tools.LoginServerThread(\n            url, self._result_of_ftrack_thread\n        )\n        self._login_server_thread.start()\n\n    def _result_of_ftrack_thread(self, username, api_key):\n        if not self.login_with_credentials(username, api_key):\n            self._invalid_input(self.api_input)\n            self.set_error((\n                \"Somthing happened with Ftrack login.\"\n                \" Try enter Username and API key manually.\"\n            ))\n\n    def login_with_credentials(self, username, api_key):\n        verification = credentials.check_credentials(username, api_key)\n        if verification:\n            credentials.save_credentials(username, api_key, False)\n            self._module.set_credentials_to_env(username, api_key)\n            self.set_credentials(username, api_key)\n            self.login_changed.emit()\n        return verification\n\n    def set_credentials(self, username, api_key, is_logged=True):\n        self.user_input.setText(username)\n        self.api_input.setText(api_key)\n\n        self.error_label.hide()\n\n        self._not_invalid_input(self.ftsite_input)\n        self._not_invalid_input(self.user_input)\n        self._not_invalid_input(self.api_input)\n\n        if is_logged is not None:\n            self.set_is_logged(is_logged)\n\n    def check_url(self):\n        settings_url = self._module.settings_ftrack_url\n        url = self._module.ftrack_url\n        if not settings_url:\n            self.set_error(\n                \"Ftrack URL is not defined in settings!\"\n            )\n            return\n\n        if url is None:\n            self.set_error(\n                \"Specified URL does not lead to a valid Ftrack server.\"\n            )\n            return\n\n        try:\n            result = requests.get(\n                url,\n                # Old python API will not work with redirect.\n                allow_redirects=False\n            )\n        except requests.exceptions.RequestException:\n            self.set_error(\n                \"Specified URL could not be reached.\"\n            )\n            return\n\n        if (\n            result.status_code != 200\n            or \"FTRACK_VERSION\" not in result.headers\n        ):\n            self.set_error(\n                \"Specified URL does not lead to a valid Ftrack server.\"\n            )\n            return\n        return url\n\n    def closeEvent(self, event):\n        event.ignore()\n        self._close_widget()\n\n    def _close_widget(self):\n        self.hide()\n"
  },
  {
    "path": "openpype/modules/ftrack/tray/login_tools.py",
    "content": "from http.server import BaseHTTPRequestHandler, HTTPServer\nfrom urllib import parse\nimport webbrowser\nimport functools\nimport threading\nfrom openpype import resources\n\n\nclass LoginServerHandler(BaseHTTPRequestHandler):\n    '''Login server handler.'''\n\n    message_filepath = resources.get_resource(\"ftrack\", \"sign_in_message.html\")\n\n    def __init__(self, login_callback, *args, **kw):\n        '''Initialise handler.'''\n        self.login_callback = login_callback\n        BaseHTTPRequestHandler.__init__(self, *args, **kw)\n\n    def log_message(self, format_str, *args):\n        \"\"\"Override method of BaseHTTPRequestHandler.\n\n        Goal is to use `print` instead of `sys.stderr.write`\n        \"\"\"\n        # Change\n        print(\"%s - - [%s] %s\\n\" % (\n            self.client_address[0],\n            self.log_date_time_string(),\n            format_str % args\n        ))\n\n    def do_GET(self):\n        '''Override to handle requests ourselves.'''\n        parsed_path = parse.urlparse(self.path)\n        query = parsed_path.query\n\n        api_user = None\n        api_key = None\n        login_credentials = None\n        if 'api_user' and 'api_key' in query:\n            login_credentials = parse.parse_qs(query)\n            api_user = login_credentials['api_user'][0]\n            api_key = login_credentials['api_key'][0]\n\n            with open(self.message_filepath, \"r\") as message_file:\n                sign_in_message = message_file.read()\n\n            # formatting html code for python\n            replacements = (\n                (\"{\", \"{{\"),\n                (\"}\", \"}}\"),\n                (\"{{}}\", \"{}\")\n            )\n            for replacement in (replacements):\n                sign_in_message = sign_in_message.replace(*replacement)\n            message = sign_in_message.format(api_user)\n        else:\n            message = \"<h1>Failed to sign in</h1>\"\n\n        self.send_response(200)\n        self.end_headers()\n        self.wfile.write(message.encode())\n\n        if login_credentials:\n            self.login_callback(\n                api_user,\n                api_key\n            )\n\n\nclass LoginServerThread(threading.Thread):\n    '''Login server thread.'''\n\n    def __init__(self, url, callback):\n        self.url = url\n        self.callback = callback\n        self._server = None\n        super(LoginServerThread, self).__init__()\n\n    def _handle_login(self, api_user, api_key):\n        '''Login to server with *api_user* and *api_key*.'''\n        self.callback(api_user, api_key)\n\n    def stop(self):\n        if self._server:\n            self._server.server_close()\n\n    def run(self):\n        '''Listen for events.'''\n        self._server = HTTPServer(\n            ('localhost', 0),\n            functools.partial(\n                LoginServerHandler, self._handle_login\n            )\n        )\n        unformated_url = (\n            '{0}/user/api_credentials?''redirect_url=http://localhost:{1}'\n        )\n        webbrowser.open_new_tab(\n            unformated_url.format(\n                self.url, self._server.server_port\n            )\n        )\n        self._server.handle_request()\n"
  },
  {
    "path": "openpype/modules/interfaces.py",
    "content": "from abc import ABCMeta, abstractmethod, abstractproperty\n\nimport six\n\nfrom openpype import resources\n\n\nclass _OpenPypeInterfaceMeta(ABCMeta):\n    \"\"\"OpenPypeInterface meta class to print proper string.\"\"\"\n\n    def __str__(self):\n        return \"<'OpenPypeInterface.{}'>\".format(self.__name__)\n\n    def __repr__(self):\n        return str(self)\n\n\n@six.add_metaclass(_OpenPypeInterfaceMeta)\nclass OpenPypeInterface:\n    \"\"\"Base class of Interface that can be used as Mixin with abstract parts.\n\n    This is way how OpenPype module or addon can tell OpenPype that contain\n    implementation for specific functionality.\n\n    Child classes of OpenPypeInterface may be used as mixin in different\n    OpenPype modules which means they have to have implemented methods defined\n    in the interface. By default, interface does not have any abstract parts.\n    \"\"\"\n\n    pass\n\n\nclass IPluginPaths(OpenPypeInterface):\n    \"\"\"Module has plugin paths to return.\n\n    Expected result is dictionary with keys \"publish\", \"create\", \"load\",\n    \"actions\" or \"inventory\" and values as list or string.\n    {\n        \"publish\": [\"path/to/publish_plugins\"]\n    }\n    \"\"\"\n\n    @abstractmethod\n    def get_plugin_paths(self):\n        pass\n\n    def _get_plugin_paths_by_type(self, plugin_type):\n        paths = self.get_plugin_paths()\n        if not paths or plugin_type not in paths:\n            return []\n\n        paths = paths[plugin_type]\n        if not paths:\n            return []\n\n        if not isinstance(paths, (list, tuple, set)):\n            paths = [paths]\n        return paths\n\n    def get_create_plugin_paths(self, host_name):\n        \"\"\"Receive create plugin paths.\n\n        Give addons ability to add create plugin paths based on host name.\n\n        Notes:\n            Default implementation uses 'get_plugin_paths' and always return\n                all create plugin paths.\n\n        Args:\n            host_name (str): For which host are the plugins meant.\n        \"\"\"\n\n        if hasattr(self, \"get_creator_plugin_paths\"):\n            # TODO remove in 3.16\n            self.log.warning((\n                \"DEPRECATION WARNING: Using method 'get_creator_plugin_paths'\"\n                \" which was renamed to 'get_create_plugin_paths'.\"\n            ))\n            return self.get_creator_plugin_paths(host_name)\n        return self._get_plugin_paths_by_type(\"create\")\n\n    def get_load_plugin_paths(self, host_name):\n        \"\"\"Receive load plugin paths.\n\n        Give addons ability to add load plugin paths based on host name.\n\n        Notes:\n            Default implementation uses 'get_plugin_paths' and always return\n                all load plugin paths.\n\n        Args:\n            host_name (str): For which host are the plugins meant.\n        \"\"\"\n\n        return self._get_plugin_paths_by_type(\"load\")\n\n    def get_publish_plugin_paths(self, host_name):\n        \"\"\"Receive publish plugin paths.\n\n        Give addons ability to add publish plugin paths based on host name.\n\n        Notes:\n           Default implementation uses 'get_plugin_paths' and always return\n               all publish plugin paths.\n\n        Args:\n           host_name (str): For which host are the plugins meant.\n        \"\"\"\n\n        return self._get_plugin_paths_by_type(\"publish\")\n\n    def get_inventory_action_paths(self, host_name):\n        \"\"\"Receive inventory action paths.\n\n        Give addons ability to add inventory action plugin paths.\n\n        Notes:\n           Default implementation uses 'get_plugin_paths' and always return\n               all publish plugin paths.\n\n        Args:\n           host_name (str): For which host are the plugins meant.\n        \"\"\"\n\n        return self._get_plugin_paths_by_type(\"inventory\")\n\n\nclass ILaunchHookPaths(OpenPypeInterface):\n    \"\"\"Module has launch hook paths to return.\n\n    Modules don't have to inherit from this interface (changed 8.11.2022).\n    Module just have to have implemented 'get_launch_hook_paths' to be able to\n    use the advantage.\n\n    Expected result is list of paths.\n    [\"path/to/launch_hooks_dir\"]\n\n    Deprecated:\n        This interface is not needed since OpenPype 3.14.*. Addon just have to\n        implement 'get_launch_hook_paths' which can expect Application object\n        or nothing as argument.\n\n        Interface class will be removed after 3.16.*.\n    \"\"\"\n\n    @abstractmethod\n    def get_launch_hook_paths(self, app):\n        \"\"\"Paths to directory with application launch hooks.\n\n        Method can be also defined without arguments.\n        ```python\n        def get_launch_hook_paths(self):\n            return []\n        ```\n\n        Args:\n            app (Application): Application object which can be used for\n                filtering of which launch hook paths are returned.\n\n        Returns:\n            Iterable[str]: Path to directories where launch hooks can be found.\n        \"\"\"\n\n        pass\n\n\nclass ITrayModule(OpenPypeInterface):\n    \"\"\"Module has special procedures when used in Pype Tray.\n\n    IMPORTANT:\n    The module still must be usable if is not used in tray even if\n    would do nothing.\n    \"\"\"\n\n    tray_initialized = False\n    _tray_manager = None\n\n    @abstractmethod\n    def tray_init(self):\n        \"\"\"Initialization part of tray implementation.\n\n        Triggered between `initialization` and `connect_with_modules`.\n\n        This is where GUIs should be loaded or tray specific parts should be\n        prepared.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def tray_menu(self, tray_menu):\n        \"\"\"Add module's action to tray menu.\"\"\"\n\n        pass\n\n    @abstractmethod\n    def tray_start(self):\n        \"\"\"Start procedure in Pype tray.\"\"\"\n\n        pass\n\n    @abstractmethod\n    def tray_exit(self):\n        \"\"\"Cleanup method which is executed on tray shutdown.\n\n        This is place where all threads should be shut.\n        \"\"\"\n\n        pass\n\n    def execute_in_main_thread(self, callback):\n        \"\"\" Pushes callback to the queue or process 'callback' on a main thread\n\n            Some callbacks need to be processed on main thread (menu actions\n            must be added on main thread or they won't get triggered etc.)\n        \"\"\"\n\n        if not self.tray_initialized:\n            # TODO Called without initialized tray, still main thread needed\n            try:\n                callback()\n\n            except Exception:\n                self.log.warning(\n                    \"Failed to execute {} in main thread\".format(callback),\n                    exc_info=True)\n\n            return\n        self.manager.tray_manager.execute_in_main_thread(callback)\n\n    def show_tray_message(self, title, message, icon=None, msecs=None):\n        \"\"\"Show tray message.\n\n        Args:\n            title (str): Title of message.\n            message (str): Content of message.\n            icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is\n                Information icon, may differ by Qt version.\n            msecs (int): Duration of message visibility in milliseconds.\n                Default is 10000 msecs, may differ by Qt version.\n        \"\"\"\n\n        if self._tray_manager:\n            self._tray_manager.show_tray_message(title, message, icon, msecs)\n\n    def add_doubleclick_callback(self, callback):\n        if hasattr(self.manager, \"add_doubleclick_callback\"):\n            self.manager.add_doubleclick_callback(self, callback)\n\n\nclass ITrayAction(ITrayModule):\n    \"\"\"Implementation of Tray action.\n\n    Add action to tray menu which will trigger `on_action_trigger`.\n    It is expected to be used for showing tools.\n\n    Methods `tray_start`, `tray_exit` and `connect_with_modules` are overridden\n    as it's not expected that action will use them. But it is possible if\n    necessary.\n    \"\"\"\n\n    admin_action = False\n    _admin_submenu = None\n    _action_item = None\n\n    @property\n    @abstractmethod\n    def label(self):\n        \"\"\"Service label showed in menu.\"\"\"\n        pass\n\n    @abstractmethod\n    def on_action_trigger(self):\n        \"\"\"What happens on actions click.\"\"\"\n        pass\n\n    def tray_menu(self, tray_menu):\n        from qtpy import QtWidgets\n\n        if self.admin_action:\n            menu = self.admin_submenu(tray_menu)\n            action = QtWidgets.QAction(self.label, menu)\n            menu.addAction(action)\n            if not menu.menuAction().isVisible():\n                menu.menuAction().setVisible(True)\n\n        else:\n            action = QtWidgets.QAction(self.label, tray_menu)\n            tray_menu.addAction(action)\n\n        action.triggered.connect(self.on_action_trigger)\n        self._action_item = action\n\n    def tray_start(self):\n        return\n\n    def tray_exit(self):\n        return\n\n    @staticmethod\n    def admin_submenu(tray_menu):\n        if ITrayAction._admin_submenu is None:\n            from qtpy import QtWidgets\n\n            admin_submenu = QtWidgets.QMenu(\"Admin\", tray_menu)\n            admin_submenu.menuAction().setVisible(False)\n            ITrayAction._admin_submenu = admin_submenu\n        return ITrayAction._admin_submenu\n\n\nclass ITrayService(ITrayModule):\n    # Module's property\n    menu_action = None\n\n    # Class properties\n    _services_submenu = None\n    _icon_failed = None\n    _icon_running = None\n    _icon_idle = None\n\n    @property\n    @abstractmethod\n    def label(self):\n        \"\"\"Service label showed in menu.\"\"\"\n        pass\n\n    # TODO be able to get any sort of information to show/print\n    # @abstractmethod\n    # def get_service_info(self):\n    #     pass\n\n    @staticmethod\n    def services_submenu(tray_menu):\n        if ITrayService._services_submenu is None:\n            from qtpy import QtWidgets\n\n            services_submenu = QtWidgets.QMenu(\"Services\", tray_menu)\n            services_submenu.menuAction().setVisible(False)\n            ITrayService._services_submenu = services_submenu\n        return ITrayService._services_submenu\n\n    @staticmethod\n    def add_service_action(action):\n        ITrayService._services_submenu.addAction(action)\n        if not ITrayService._services_submenu.menuAction().isVisible():\n            ITrayService._services_submenu.menuAction().setVisible(True)\n\n    @staticmethod\n    def _load_service_icons():\n        from qtpy import QtGui\n\n        ITrayService._failed_icon = QtGui.QIcon(\n            resources.get_resource(\"icons\", \"circle_red.png\")\n        )\n        ITrayService._icon_running = QtGui.QIcon(\n            resources.get_resource(\"icons\", \"circle_green.png\")\n        )\n        ITrayService._icon_idle = QtGui.QIcon(\n            resources.get_resource(\"icons\", \"circle_orange.png\")\n        )\n\n    @staticmethod\n    def get_icon_running():\n        if ITrayService._icon_running is None:\n            ITrayService._load_service_icons()\n        return ITrayService._icon_running\n\n    @staticmethod\n    def get_icon_idle():\n        if ITrayService._icon_idle is None:\n            ITrayService._load_service_icons()\n        return ITrayService._icon_idle\n\n    @staticmethod\n    def get_icon_failed():\n        if ITrayService._failed_icon is None:\n            ITrayService._load_service_icons()\n        return ITrayService._failed_icon\n\n    def tray_menu(self, tray_menu):\n        from qtpy import QtWidgets\n\n        action = QtWidgets.QAction(\n            self.label,\n            self.services_submenu(tray_menu)\n        )\n        self.menu_action = action\n\n        self.add_service_action(action)\n\n        self.set_service_running_icon()\n\n    def set_service_running_icon(self):\n        \"\"\"Change icon of an QAction to green circle.\"\"\"\n\n        if self.menu_action:\n            self.menu_action.setIcon(self.get_icon_running())\n\n    def set_service_failed_icon(self):\n        \"\"\"Change icon of an QAction to red circle.\"\"\"\n\n        if self.menu_action:\n            self.menu_action.setIcon(self.get_icon_failed())\n\n    def set_service_idle_icon(self):\n        \"\"\"Change icon of an QAction to orange circle.\"\"\"\n\n        if self.menu_action:\n            self.menu_action.setIcon(self.get_icon_idle())\n\n\nclass ISettingsChangeListener(OpenPypeInterface):\n    \"\"\"Module tries to listen to settings changes.\n\n    Only settings changes in the current process are propagated.\n    Changes made in other processes or machines won't  trigger the callbacks.\n\n    \"\"\"\n\n    @abstractmethod\n    def on_system_settings_save(\n        self, old_value, new_value, changes, new_value_metadata\n    ):\n        pass\n\n    @abstractmethod\n    def on_project_settings_save(\n        self, old_value, new_value, changes, project_name, new_value_metadata\n    ):\n        pass\n\n    @abstractmethod\n    def on_project_anatomy_save(\n        self, old_value, new_value, changes, project_name, new_value_metadata\n    ):\n        pass\n\n\nclass IHostAddon(OpenPypeInterface):\n    \"\"\"Addon which also contain a host implementation.\"\"\"\n\n    @abstractproperty\n    def host_name(self):\n        \"\"\"Name of host which module represents.\"\"\"\n\n        pass\n\n    def get_workfile_extensions(self):\n        \"\"\"Define workfile extensions for host.\n\n        Not all hosts support workfiles thus this is optional implementation.\n\n        Returns:\n            List[str]: Extensions used for workfiles with dot.\n        \"\"\"\n\n        return []\n"
  },
  {
    "path": "openpype/modules/job_queue/__init__.py",
    "content": "from .module import JobQueueModule\n\n\n__all__ = (\n    \"JobQueueModule\",\n)\n"
  },
  {
    "path": "openpype/modules/job_queue/job_server/__init__.py",
    "content": "from .server import WebServerManager\nfrom .utils import main\n\n\n__all__ = (\n    \"WebServerManager\",\n    \"main\"\n)\n"
  },
  {
    "path": "openpype/modules/job_queue/job_server/job_queue_route.py",
    "content": "import json\n\nfrom aiohttp.web_response import Response\n\n\nclass JobQueueResource:\n    def __init__(self, job_queue, server_manager):\n        self.server_manager = server_manager\n\n        self._prefix = \"/api\"\n\n        self._job_queue = job_queue\n\n        self.endpoint_defs = (\n            (\"POST\", \"/jobs\", self.post_job),\n            (\"GET\", \"/jobs\", self.get_jobs),\n            (\"GET\", \"/jobs/{job_id}\", self.get_job)\n        )\n\n        self.register()\n\n    def register(self):\n        for methods, url, callback in self.endpoint_defs:\n            final_url = self._prefix + url\n            self.server_manager.add_route(\n                methods, final_url, callback\n            )\n\n    async def get_jobs(self, request):\n        jobs_data = []\n        for job in self._job_queue.get_jobs():\n            jobs_data.append(job.status())\n        return Response(status=200, body=self.encode(jobs_data))\n\n    async def post_job(self, request):\n        data = await request.json()\n        host_name = data.get(\"host_name\")\n        if not host_name:\n            return Response(\n                status=400, message=\"Key \\\"host_name\\\" not filled.\"\n            )\n\n        job = self._job_queue.create_job(host_name, data)\n        return Response(status=201, text=job.id)\n\n    async def get_job(self, request):\n        job_id = request.match_info[\"job_id\"]\n        content = self._job_queue.get_job_status(job_id)\n        if content is None:\n            content = {}\n        return Response(\n            status=200,\n            body=self.encode(content),\n            content_type=\"application/json\"\n        )\n\n    @classmethod\n    def encode(cls, data):\n        return json.dumps(\n            data,\n            indent=4\n        ).encode(\"utf-8\")\n"
  },
  {
    "path": "openpype/modules/job_queue/job_server/jobs.py",
    "content": "import datetime\nimport collections\nfrom uuid import uuid4\n\n\nclass Job:\n    \"\"\"Job related to specific host name.\n\n    Data must contain everything needed to finish the job.\n    \"\"\"\n    # Remove done jobs each n days to clear memory\n    keep_in_memory_days = 3\n\n    def __init__(self, host_name, data, job_id=None, created_time=None):\n        if job_id is None:\n            job_id = str(uuid4())\n        self._id = job_id\n        if created_time is None:\n            created_time = datetime.datetime.now()\n        self._created_time = created_time\n        self._started_time = None\n        self._done_time = None\n        self.host_name = host_name\n        self.data = data\n        self._result_data = None\n\n        self._started = False\n        self._done = False\n        self._errored = False\n        self._message = None\n        self._deleted = False\n\n        self._worker = None\n\n    def keep_in_memory(self):\n        if self._done_time is None:\n            return True\n\n        now = datetime.datetime.now()\n        delta = now - self._done_time\n        return delta.days < self.keep_in_memory_days\n\n    @property\n    def id(self):\n        return self._id\n\n    @property\n    def done(self):\n        return self._done\n\n    def reset(self):\n        self._started = False\n        self._started_time = None\n        self._done = False\n        self._done_time = None\n        self._errored = False\n        self._message = None\n\n        self._worker = None\n\n    @property\n    def started(self):\n        return self._started\n\n    @property\n    def deleted(self):\n        return self._deleted\n\n    def set_deleted(self):\n        self._deleted = True\n        self.set_worker(None)\n\n    def set_worker(self, worker):\n        if worker is self._worker:\n            return\n\n        if self._worker is not None:\n            self._worker.set_current_job(None)\n\n        self._worker = worker\n        if worker is not None:\n            worker.set_current_job(self)\n\n    def set_started(self):\n        self._started_time = datetime.datetime.now()\n        self._started = True\n\n    def set_done(self, success=True, message=None, data=None):\n        self._done = True\n        self._done_time = datetime.datetime.now()\n        self._errored = not success\n        self._message = message\n        self._result_data = data\n        if self._worker is not None:\n            self._worker.set_current_job(None)\n\n    def status(self):\n        worker_id = None\n        if self._worker is not None:\n            worker_id = self._worker.id\n        output = {\n            \"id\": self.id,\n            \"worker_id\": worker_id,\n            \"done\": self._done\n        }\n        output[\"message\"] = self._message or None\n\n        state = \"waiting\"\n        if self._deleted:\n            state = \"deleted\"\n        elif self._errored:\n            state = \"error\"\n        elif self._done:\n            state = \"done\"\n        elif self._started:\n            state = \"started\"\n\n        output[\"result\"] = self._result_data\n\n        output[\"state\"] = state\n\n        return output\n\n\nclass JobQueue:\n    \"\"\"Queue holds jobs that should be done and workers that can do them.\n\n    Also asign jobs to a worker.\n    \"\"\"\n    old_jobs_check_minutes_interval = 30\n\n    def __init__(self):\n        self._last_old_jobs_check = datetime.datetime.now()\n        self._jobs_by_id = {}\n        self._job_queue_by_host_name = collections.defaultdict(\n            collections.deque\n        )\n        self._workers_by_id = {}\n        self._workers_by_host_name = collections.defaultdict(list)\n\n    def workers(self):\n        \"\"\"All currently registered workers.\"\"\"\n        return self._workers_by_id.values()\n\n    def add_worker(self, worker):\n        host_name = worker.host_name\n        print(\"Added new worker for \\\"{}\\\"\".format(host_name))\n        self._workers_by_id[worker.id] = worker\n        self._workers_by_host_name[host_name].append(worker)\n\n    def get_worker(self, worker_id):\n        return self._workers_by_id.get(worker_id)\n\n    def remove_worker(self, worker):\n        # Look if worker had assigned job to do\n        job = worker.current_job\n        if job is not None and not job.done:\n            # Reset job\n            job.set_worker(None)\n            job.reset()\n            # Add job back to queue\n            self._job_queue_by_host_name[job.host_name].appendleft(job)\n\n        # Remove worker from registered workers\n        self._workers_by_id.pop(worker.id, None)\n        host_name = worker.host_name\n        if worker in self._workers_by_host_name[host_name]:\n            self._workers_by_host_name[host_name].remove(worker)\n\n        print(\"Removed worker for \\\"{}\\\"\".format(host_name))\n\n    def assign_jobs(self):\n        \"\"\"Try to assign job for each idle worker.\n\n        Error all jobs without needed worker.\n        \"\"\"\n        available_host_names = set()\n        for worker in self._workers_by_id.values():\n            host_name = worker.host_name\n            available_host_names.add(host_name)\n            if worker.is_idle():\n                jobs = self._job_queue_by_host_name[host_name]\n                while jobs:\n                    job = jobs.popleft()\n                    if not job.deleted:\n                        worker.set_current_job(job)\n                        break\n\n        for host_name in tuple(self._job_queue_by_host_name.keys()):\n            if host_name in available_host_names:\n                continue\n\n            jobs_deque = self._job_queue_by_host_name[host_name]\n            message = (\"Not available workers for \\\"{}\\\"\").format(host_name)\n            while jobs_deque:\n                job = jobs_deque.popleft()\n                if not job.deleted:\n                    job.set_done(False, message)\n        self._remove_old_jobs()\n\n    def get_jobs(self):\n        return self._jobs_by_id.values()\n\n    def get_job(self, job_id):\n        \"\"\"Job by it's id.\"\"\"\n        return self._jobs_by_id.get(job_id)\n\n    def create_job(self, host_name, job_data):\n        \"\"\"Create new job from passed data and add it to queue.\"\"\"\n        job = Job(host_name, job_data)\n        self._jobs_by_id[job.id] = job\n        self._job_queue_by_host_name[host_name].append(job)\n        return job\n\n    def _remove_old_jobs(self):\n        \"\"\"Once in specific time look if should remove old finished jobs.\"\"\"\n        delta = datetime.datetime.now() - self._last_old_jobs_check\n        if delta.seconds < self.old_jobs_check_minutes_interval:\n            return\n\n        for job_id in tuple(self._jobs_by_id.keys()):\n            job = self._jobs_by_id[job_id]\n            if not job.keep_in_memory():\n                self._jobs_by_id.pop(job_id)\n\n    def remove_job(self, job_id):\n        \"\"\"Delete job and eventually stop it.\"\"\"\n        job = self._jobs_by_id.get(job_id)\n        if job is None:\n            return\n\n        job.set_deleted()\n        self._jobs_by_id.pop(job.id)\n\n    def get_job_status(self, job_id):\n        \"\"\"Job's status based on id.\"\"\"\n        job = self._jobs_by_id.get(job_id)\n        if job is None:\n            return {}\n        return job.status()\n"
  },
  {
    "path": "openpype/modules/job_queue/job_server/server.py",
    "content": "import threading\nimport asyncio\nimport logging\n\nfrom aiohttp import web\n\nfrom .jobs import JobQueue\nfrom .job_queue_route import JobQueueResource\nfrom .workers_rpc_route import WorkerRpc\n\nlog = logging.getLogger(__name__)\n\n\nclass WebServerManager:\n    \"\"\"Manger that care about web server thread.\"\"\"\n    def __init__(self, port, host, loop=None):\n        self.port = port\n        self.host = host\n        self.app = web.Application()\n        if loop is None:\n            loop = asyncio.new_event_loop()\n\n        # add route with multiple methods for single \"external app\"\n        self.webserver_thread = WebServerThread(self, loop)\n\n    @property\n    def url(self):\n        return \"http://{}:{}\".format(self.host, self.port)\n\n    def add_route(self, *args, **kwargs):\n        self.app.router.add_route(*args, **kwargs)\n\n    def add_static(self, *args, **kwargs):\n        self.app.router.add_static(*args, **kwargs)\n\n    def start_server(self):\n        if self.webserver_thread and not self.webserver_thread.is_alive():\n            self.webserver_thread.start()\n\n    def stop_server(self):\n        if not self.is_running:\n            return\n\n        try:\n            log.debug(\"Stopping Web server\")\n            self.webserver_thread.stop()\n\n        except Exception as exc:\n            print(\"Errored\", str(exc))\n            log.warning(\n                \"Error has happened during Killing Web server\",\n                exc_info=True\n            )\n\n    @property\n    def is_running(self):\n        if self.webserver_thread is not None:\n            return self.webserver_thread.is_running\n        return False\n\n\nclass WebServerThread(threading.Thread):\n    \"\"\" Listener for requests in thread.\"\"\"\n    def __init__(self, manager, loop):\n        super(WebServerThread, self).__init__()\n\n        self._is_running = False\n        self._stopped = False\n        self.manager = manager\n        self.loop = loop\n        self.runner = None\n        self.site = None\n\n        job_queue = JobQueue()\n        self.job_queue_route = JobQueueResource(job_queue, manager)\n        self.workers_route = WorkerRpc(job_queue, manager, loop=loop)\n\n    @property\n    def port(self):\n        return self.manager.port\n\n    @property\n    def host(self):\n        return self.manager.host\n\n    @property\n    def stopped(self):\n        return self._stopped\n\n    @property\n    def is_running(self):\n        return self._is_running\n\n    def run(self):\n        self._is_running = True\n\n        try:\n            log.info(\"Starting WebServer server\")\n            asyncio.set_event_loop(self.loop)\n            self.loop.run_until_complete(self.start_server())\n\n            asyncio.ensure_future(self.check_shutdown(), loop=self.loop)\n            self.loop.run_forever()\n\n        except Exception:\n            log.warning(\n                \"Web Server service has failed\", exc_info=True\n            )\n        finally:\n            self.loop.close()\n\n        self._is_running = False\n        log.info(\"Web server stopped\")\n\n    async def start_server(self):\n        \"\"\" Starts runner and TCPsite \"\"\"\n        self.runner = web.AppRunner(self.manager.app)\n        await self.runner.setup()\n        self.site = web.TCPSite(self.runner, self.host, self.port)\n        await self.site.start()\n\n    def stop(self):\n        \"\"\"Sets _stopped flag to True, 'check_shutdown' shuts server down\"\"\"\n        self._stopped = True\n\n    async def check_shutdown(self):\n        \"\"\" Future that is running and checks if server should be running\n            periodically.\n        \"\"\"\n        while not self._stopped:\n            await asyncio.sleep(0.5)\n\n        print(\"Starting shutdown\")\n        if self.workers_route:\n            await self.workers_route.stop()\n\n        print(\"Stopping site\")\n        await self.site.stop()\n        print(\"Site stopped\")\n        await self.runner.cleanup()\n\n        print(\"Runner stopped\")\n        tasks = [\n            task\n            for task in asyncio.all_tasks()\n            if task is not asyncio.current_task()\n        ]\n        list(map(lambda task: task.cancel(), tasks))  # cancel all the tasks\n        results = await asyncio.gather(*tasks, return_exceptions=True)\n        log.debug(f'Finished awaiting cancelled tasks, results: {results}...')\n        await self.loop.shutdown_asyncgens()\n        # to really make sure everything else has time to stop\n        await asyncio.sleep(0.07)\n        self.loop.stop()\n"
  },
  {
    "path": "openpype/modules/job_queue/job_server/utils.py",
    "content": "import sys\nimport signal\nimport time\nimport socket\n\nfrom .server import WebServerManager\n\n\nclass SharedObjects:\n    stopped = False\n\n    @classmethod\n    def stop(cls):\n        cls.stopped = True\n\n\ndef main(port=None, host=None):\n    def signal_handler(sig, frame):\n        print(\"Signal to kill process received. Termination starts.\")\n        SharedObjects.stop()\n\n    signal.signal(signal.SIGINT, signal_handler)\n    signal.signal(signal.SIGTERM, signal_handler)\n\n    port = int(port or 8079)\n    host = str(host or \"localhost\")\n\n    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as con:\n        result_of_check = con.connect_ex((host, port))\n\n    if result_of_check == 0:\n        print((\n            \"Server {}:{} is already running or address is occupied.\"\n        ).format(host, port))\n        return 1\n\n    print(\"Running server {}:{}\".format(host, port))\n    manager = WebServerManager(port, host)\n    manager.start_server()\n\n    stopped = False\n    while manager.is_running:\n        if not stopped and SharedObjects.stopped:\n            stopped = True\n            manager.stop_server()\n        time.sleep(0.1)\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "openpype/modules/job_queue/job_server/workers.py",
    "content": "import asyncio\nfrom uuid import uuid4\nfrom aiohttp import WSCloseCode\nfrom aiohttp_json_rpc.protocol import encode_request\n\n\nclass WorkerState:\n    IDLE = object()\n    JOB_ASSIGNED = object()\n    JOB_SENT = object()\n\n\nclass Worker:\n    \"\"\"Worker that can handle jobs of specific host.\"\"\"\n    def __init__(self, host_name, http_request):\n        self._id = None\n        self.host_name = host_name\n        self._http_request = http_request\n        self._state = WorkerState.IDLE\n        self._job = None\n\n        # Give ability to send requests to worker\n        http_request.request_id = str(uuid4())\n        http_request.pending_requests = {}\n\n    async def send_job(self):\n        if self._job is not None:\n            data = {\n                \"job_id\": self._job.id,\n                \"worker_id\": self.id,\n                \"data\": self._job.data\n            }\n            return await self.call(\"start_job\", data)\n        return False\n\n    async def call(self, method, params=None, timeout=None):\n        \"\"\"Call method on worker's side.\"\"\"\n        request_id = self._http_request.request_id\n        self._http_request.request_id = str(uuid4())\n        pending_requests = self._http_request.pending_requests\n        pending_requests[request_id] = asyncio.Future()\n\n        request = encode_request(method, id=request_id, params=params)\n\n        await self._http_request.ws.send_str(request)\n\n        if timeout:\n            await asyncio.wait_for(\n                pending_requests[request_id],\n                timeout=timeout\n            )\n\n        else:\n            await pending_requests[request_id]\n\n        result = pending_requests[request_id].result()\n        del pending_requests[request_id]\n\n        return result\n\n    async def close(self):\n        return await self.ws.close(\n            code=WSCloseCode.GOING_AWAY,\n            message=\"Server shutdown\"\n        )\n\n    @property\n    def id(self):\n        if self._id is None:\n            self._id = str(uuid4())\n        return self._id\n\n    @property\n    def state(self):\n        return self._state\n\n    @property\n    def current_job(self):\n        return self._job\n\n    @property\n    def http_request(self):\n        return self._http_request\n\n    @property\n    def ws(self):\n        return self.http_request.ws\n\n    def connection_is_alive(self):\n        if self.ws.closed or self.ws._writer.transport.is_closing():\n            return False\n        return True\n\n    def is_idle(self):\n        return self._state is WorkerState.IDLE\n\n    def job_assigned(self):\n        return (\n            self._state is WorkerState.JOB_ASSIGNED\n            or self._state is WorkerState.JOB_SENT\n        )\n\n    def is_working(self):\n        return self._state is WorkerState.JOB_SENT\n\n    def set_current_job(self, job):\n        if job is self._job:\n            return\n\n        self._job = job\n        if job is None:\n            self._set_idle()\n        else:\n            self._state = WorkerState.JOB_ASSIGNED\n            job.set_worker(self)\n\n    def _set_idle(self):\n        self._job = None\n        self._state = WorkerState.IDLE\n\n    def set_working(self):\n        self._state = WorkerState.JOB_SENT\n"
  },
  {
    "path": "openpype/modules/job_queue/job_server/workers_rpc_route.py",
    "content": "import asyncio\n\nimport aiohttp\nfrom aiohttp_json_rpc import JsonRpc\nfrom aiohttp_json_rpc.protocol import (\n    encode_error, decode_msg, JsonRpcMsgTyp\n)\nfrom aiohttp_json_rpc.exceptions import RpcError\nfrom .workers import Worker\n\n\nclass WorkerRpc(JsonRpc):\n    def __init__(self, job_queue, manager, **kwargs):\n        super().__init__(**kwargs)\n\n        self._job_queue = job_queue\n        self._manager = manager\n\n        self._stopped = False\n\n        # Register methods\n        self.add_methods(\n            (\"\", self.register_worker),\n            (\"\", self.job_done)\n        )\n        asyncio.ensure_future(self._rpc_loop(), loop=self.loop)\n\n        self._manager.add_route(\n            \"*\", \"/ws\", self.handle_request\n        )\n\n    # Panel routes for tools\n    async def register_worker(self, request, host_name):\n        worker = Worker(host_name, request.http_request)\n        self._job_queue.add_worker(worker)\n        return worker.id\n\n    async def _rpc_loop(self):\n        while self.loop.is_running():\n            if self._stopped:\n                break\n\n            for worker in tuple(self._job_queue.workers()):\n                if not worker.connection_is_alive():\n                    self._job_queue.remove_worker(worker)\n            self._job_queue.assign_jobs()\n\n            await self.send_jobs()\n            await asyncio.sleep(5)\n\n    async def job_done(self, worker_id, job_id, success, message, data):\n        worker = self._job_queue.get_worker(worker_id)\n        if worker is not None:\n            worker.set_current_job(None)\n\n        job = self._job_queue.get_job(job_id)\n        if job is not None:\n            job.set_done(success, message, data)\n        return True\n\n    async def send_jobs(self):\n        invalid_workers = []\n        for worker in self._job_queue.workers():\n            if worker.job_assigned() and not worker.is_working():\n                try:\n                    await worker.send_job()\n\n                except ConnectionResetError:\n                    invalid_workers.append(worker)\n\n        for worker in invalid_workers:\n            self._job_queue.remove_worker(worker)\n\n    async def handle_websocket_request(self, http_request):\n        \"\"\"Override this method to catch CLOSING messages.\"\"\"\n        http_request.msg_id = 0\n        http_request.pending = {}\n\n        # prepare and register websocket\n        ws = aiohttp.web_ws.WebSocketResponse()\n        await ws.prepare(http_request)\n        http_request.ws = ws\n        self.clients.append(http_request)\n\n        while not ws.closed:\n            self.logger.debug('waiting for messages')\n            raw_msg = await ws.receive()\n\n            if raw_msg.type == aiohttp.WSMsgType.TEXT:\n                self.logger.debug('raw msg received: %s', raw_msg.data)\n                self.loop.create_task(\n                    self._handle_rpc_msg(http_request, raw_msg)\n                )\n\n            elif raw_msg.type == aiohttp.WSMsgType.CLOSING:\n                break\n\n        self.clients.remove(http_request)\n        return ws\n\n    async def _handle_rpc_msg(self, http_request, raw_msg):\n        # This is duplicated code from super but there is no way how to do it\n        # to be able handle server->client requests\n        try:\n            _raw_message = raw_msg.data\n            msg = decode_msg(_raw_message)\n\n        except RpcError as error:\n            await self._ws_send_str(http_request, encode_error(error))\n            return\n\n        if msg.type in (JsonRpcMsgTyp.RESULT, JsonRpcMsgTyp.ERROR):\n            request_id = msg.data[\"id\"]\n            if request_id in http_request.pending_requests:\n                future = http_request.pending_requests[request_id]\n                future.set_result(msg.data[\"result\"])\n                return\n\n        return await super()._handle_rpc_msg(http_request, raw_msg)\n\n    async def stop(self):\n        self._stopped = True\n        for worker in tuple(self._job_queue.workers()):\n            await worker.close()\n"
  },
  {
    "path": "openpype/modules/job_queue/job_workers/__init__.py",
    "content": "from .base_worker import WorkerJobsConnection\n\n__all__ = (\n    \"WorkerJobsConnection\",\n)\n"
  },
  {
    "path": "openpype/modules/job_queue/job_workers/base_worker.py",
    "content": "import sys\nimport datetime\nimport asyncio\nimport traceback\n\nfrom aiohttp_json_rpc import JsonRpcClient\n\n\nclass WorkerClient(JsonRpcClient):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        self.add_methods(\n            (\"\", self.start_job),\n        )\n        self.current_job = None\n        self._id = None\n\n    def set_id(self, worker_id):\n        self._id = worker_id\n\n    async def start_job(self, job_data):\n        if self.current_job is not None:\n            return False\n\n        print(\"Got new job {}\".format(str(job_data)))\n        self.current_job = job_data\n        return True\n\n    def finish_job(self, success, message, data):\n        asyncio.ensure_future(\n            self._finish_job(success, message, data),\n            loop=self._loop\n        )\n\n    async def _finish_job(self, success, message, data):\n        print(\"Current job\", self.current_job)\n        job_id = self.current_job[\"job_id\"]\n        self.current_job = None\n\n        return await self.call(\n            \"job_done\", [self._id, job_id, success, message, data]\n        )\n\n\nclass WorkerJobsConnection:\n    \"\"\"WS connection to Job server.\n\n    Helper class to create a connection to process jobs from job server.\n\n    To be able receive jobs is needed to create a connection and then register\n    as worker for specific host.\n    \"\"\"\n    retry_time_seconds = 5\n\n    def __init__(self, server_url, host_name, loop=None):\n        self.client = None\n        self._loop = loop\n\n        self._host_name = host_name\n        self._server_url = server_url\n\n        self._is_running = False\n        self._connecting = False\n        self._connected = False\n        self._stopped = False\n\n    def stop(self):\n        print(\"Stopping worker\")\n        self._stopped = True\n\n    @property\n    def is_running(self):\n        return self._is_running\n\n    @property\n    def current_job(self):\n        if self.client is not None:\n            return self.client.current_job\n        return None\n\n    def finish_job(self, success=True, message=None, data=None):\n        \"\"\"Worker finished job and sets the result which is send to server.\"\"\"\n        if self.client is None:\n            print((\n                \"Couldn't sent job status to server because\"\n                \" client is not connected.\"\n            ))\n        else:\n            self.client.finish_job(success, message, data)\n\n    async def main_loop(self, register_worker=True):\n        \"\"\"Main loop of connection which keep connection to server alive.\"\"\"\n        self._is_running = True\n\n        while not self._stopped:\n            start_time = datetime.datetime.now()\n            await self._connection_loop(register_worker)\n            delta = datetime.datetime.now() - start_time\n            print(\"Connection loop took {}s\".format(str(delta)))\n            # Check if was stopped and stop while loop in that case\n            if self._stopped:\n                break\n\n            if delta.seconds < 60:\n                print((\n                    \"Can't connect to server will try in {} seconds.\"\n                ).format(self.retry_time_seconds))\n\n                await asyncio.sleep(self.retry_time_seconds)\n        self._is_running = False\n\n    async def _connect(self):\n        self.client = WorkerClient()\n        print(\"Connecting to {}\".format(self._server_url))\n        try:\n            await self.client.connect_url(self._server_url)\n        except KeyboardInterrupt:\n            raise\n        except Exception:\n            traceback.print_exception(*sys.exc_info())\n\n    async def _connection_loop(self, register_worker):\n        self._connecting = True\n        future = asyncio.run_coroutine_threadsafe(\n            self._connect(), loop=self._loop\n        )\n\n        while self._connecting:\n            if not future.done():\n                await asyncio.sleep(0.07)\n                continue\n\n            session = getattr(self.client, \"_session\", None)\n            ws = getattr(self.client, \"_ws\", None)\n            if session is not None:\n                if session.closed:\n                    self._connecting = False\n                    self._connected = False\n                    break\n\n                elif ws is not None:\n                    self._connecting = False\n                    self._connected = True\n\n            if self._stopped:\n                break\n\n            await asyncio.sleep(0.07)\n\n        if not self._connected:\n            self.client = None\n            return\n\n        print(\"Connected to job queue server\")\n        if register_worker:\n            self.register_as_worker()\n\n        while self._connected and self._loop.is_running():\n            if self._stopped or ws.closed:\n                break\n\n            await asyncio.sleep(0.3)\n\n        await self._stop_cleanup()\n\n    def register_as_worker(self):\n        \"\"\"Register as worker ready to work on server side.\"\"\"\n        asyncio.ensure_future(self._register_as_worker(), loop=self._loop)\n\n    async def _register_as_worker(self):\n        worker_id = await self.client.call(\n            \"register_worker\", [self._host_name]\n        )\n        self.client.set_id(worker_id)\n        print(\n            \"Registered as worker with id {}\".format(worker_id)\n        )\n\n    async def disconnect(self):\n        await self._stop_cleanup()\n\n    async def _stop_cleanup(self):\n        print(\"Cleanup after stop\")\n        if self.client is not None and hasattr(self.client, \"_ws\"):\n            await self.client.disconnect()\n\n        self.client = None\n        self._connecting = False\n        self._connected = False\n"
  },
  {
    "path": "openpype/modules/job_queue/module.py",
    "content": "\"\"\"Job queue OpenPype module was created for remote execution of commands.\n\n## Why is needed\nPrimarily created for hosts which are not easilly controlled from command line\nor in headless mode and is easier to keep one process of host running listening\nfor jobs to do.\n\n### Example\nOne of examples is TVPaint which does not have headless mode, can run only one\nprocess at one time and it's impossible to know what should be executed inside\nTVPaint before we know all data about the file that should be processed.\n\n## Idea\nIdea is that there is a server, workers and workstation/s which need to process\nsomething on a worker.\n\nWorkers and workstation/s must have access to server through adress to it's\nrunning instance. Workers use WebSockets and workstations are using HTTP calls.\nAlso both of them must have access to job queue root which is set in\nsettings. Root is used as temp where files needed for job can be stored before\nsending the job or where result files are stored when job is done.\n\nServer's address must be set in settings when is running so workers and\nworkstations know where to send or receive jobs.\n\n## Command line commands\n### start_server\n- start server which is handles jobs\n- it is possible to specify port and host address (default is localhost:8079)\n\n### start_worker\n- start worker which will process jobs\n- has required possitional argument which is application name from OpenPype\n    settings e.g. 'tvpaint/11-5' ('tvpaint' is group '11-5' is variant)\n- it is possible to specify server url but url from settings is used when not\n    passed (this is added mainly for developing purposes)\n\"\"\"\n\nimport sys\nimport json\nimport copy\nimport platform\n\nfrom openpype.modules import OpenPypeModule, click_wrap\nfrom openpype.settings import get_system_settings\n\n\nclass JobQueueModule(OpenPypeModule):\n    name = \"job_queue\"\n\n    def initialize(self, modules_settings):\n        module_settings = modules_settings.get(self.name) or {}\n        server_url = module_settings.get(\"server_url\") or \"\"\n\n        self._server_url = self.url_conversion(server_url)\n        jobs_root_mapping = self._roots_mapping_conversion(\n            module_settings.get(\"jobs_root\")\n        )\n\n        self._jobs_root_mapping = jobs_root_mapping\n\n        # Is always enabled\n        #   - the module does nothing until is used\n        self.enabled = True\n\n    @classmethod\n    def _root_conversion(cls, root_path):\n        \"\"\"Make sure root path does not end with slash.\"\"\"\n        # Return empty string if path is invalid\n        if not root_path:\n            return \"\"\n\n        # Remove all slashes\n        while root_path.endswith(\"/\") or root_path.endswith(\"\\\\\"):\n            root_path = root_path[:-1]\n        return root_path\n\n    @classmethod\n    def _roots_mapping_conversion(cls, roots_mapping):\n        roots_mapping = roots_mapping or {}\n        for platform_name in (\"windows\", \"linux\", \"darwin\"):\n            roots_mapping[platform_name] = cls._root_conversion(\n                roots_mapping.get(platform_name)\n            )\n        return roots_mapping\n\n    @staticmethod\n    def url_conversion(url, ws=False):\n        if sys.version_info[0] == 2:\n            from urlparse import urlsplit, urlunsplit\n        else:\n            from urllib.parse import urlsplit, urlunsplit\n\n        if not url:\n            return url\n\n        url_parts = list(urlsplit(url))\n        scheme = url_parts[0]\n        if not scheme:\n            if ws:\n                url = \"ws://{}\".format(url)\n            else:\n                url = \"http://{}\".format(url)\n            url_parts = list(urlsplit(url))\n\n        elif ws:\n            if scheme not in (\"ws\", \"wss\"):\n                if scheme == \"https\":\n                    url_parts[0] = \"wss\"\n                else:\n                    url_parts[0] = \"ws\"\n\n        elif scheme not in (\"http\", \"https\"):\n            if scheme == \"wss\":\n                url_parts[0] = \"https\"\n            else:\n                url_parts[0] = \"http\"\n\n        return urlunsplit(url_parts)\n\n    def get_jobs_root_mapping(self):\n        return copy.deepcopy(self._jobs_root_mapping)\n\n    def get_jobs_root(self):\n        return self._jobs_root_mapping.get(platform.system().lower())\n\n    @classmethod\n    def get_jobs_root_from_settings(cls):\n        module_settings = get_system_settings()[\"modules\"]\n        jobs_root_mapping = module_settings.get(cls.name, {}).get(\"jobs_root\")\n        converted_mapping = cls._roots_mapping_conversion(jobs_root_mapping)\n\n        return converted_mapping[platform.system().lower()]\n\n    @property\n    def server_url(self):\n        return self._server_url\n\n    def send_job(self, host_name, job_data):\n        import requests\n\n        job_data = job_data or {}\n        job_data[\"host_name\"] = host_name\n        api_path = \"{}/api/jobs\".format(self._server_url)\n        post_request = requests.post(api_path, data=json.dumps(job_data))\n        return str(post_request.content.decode())\n\n    def get_job_status(self, job_id):\n        import requests\n\n        api_path = \"{}/api/jobs/{}\".format(self._server_url, job_id)\n        return requests.get(api_path).json()\n\n    def cli(self, click_group):\n        click_group.add_command(cli_main.to_click_obj())\n\n    @classmethod\n    def get_server_url_from_settings(cls):\n        module_settings = get_system_settings()[\"modules\"]\n        return cls.url_conversion(\n            module_settings\n            .get(cls.name, {})\n            .get(\"server_url\")\n        )\n\n    @classmethod\n    def start_server(cls, port=None, host=None):\n        from .job_server import main\n\n        return main(port, host)\n\n    @classmethod\n    def start_worker(cls, app_name, server_url=None):\n        import requests\n        from openpype.lib import ApplicationManager\n\n        if not server_url:\n            server_url = cls.get_server_url_from_settings()\n\n        if not server_url:\n            raise ValueError(\"Server url is not set.\")\n\n        http_server_url = cls.url_conversion(server_url)\n\n        # Validate url\n        requests.get(http_server_url)\n\n        ws_server_url = cls.url_conversion(server_url) + \"/ws\"\n\n        app_manager = ApplicationManager()\n        app = app_manager.applications.get(app_name)\n        if app is None:\n            raise ValueError(\n                \"Didn't find application \\\"{}\\\" in settings.\".format(app_name)\n            )\n\n        if app.host_name == \"tvpaint\":\n            return cls._start_tvpaint_worker(app, ws_server_url)\n        raise ValueError(\"Unknown host \\\"{}\\\"\".format(app.host_name))\n\n    @classmethod\n    def _start_tvpaint_worker(cls, app, server_url):\n        from openpype.hosts.tvpaint.worker import main\n\n        executable = app.find_executable()\n        if not executable:\n            raise ValueError((\n                \"Executable for app \\\"{}\\\" is not set\"\n                \" or accessible on this workstation.\"\n            ).format(app.full_name))\n\n        return main(str(executable), server_url)\n\n\n@click_wrap.group(\n    JobQueueModule.name,\n    help=\"Application job server. Can be used as render farm.\"\n)\ndef cli_main():\n    pass\n\n\n@cli_main.command(\n    \"start_server\",\n    help=\"Start server handling workers and their jobs.\"\n)\n@click_wrap.option(\"--port\", help=\"Server port\")\n@click_wrap.option(\"--host\", help=\"Server host (ip address)\")\ndef cli_start_server(port, host):\n    JobQueueModule.start_server(port, host)\n\n\n@cli_main.command(\n    \"start_worker\", help=(\n        \"Start a worker for a specific application. (e.g. \\\"tvpaint/11.5\\\")\"\n    )\n)\n@click_wrap.argument(\"app_name\")\n@click_wrap.option(\n    \"--server_url\",\n    help=\"Server url which handle workers and jobs.\")\ndef cli_start_worker(app_name, server_url):\n    JobQueueModule.start_worker(app_name, server_url)\n"
  },
  {
    "path": "openpype/modules/kitsu/__init__.py",
    "content": "\"\"\" Addon class definition and Settings definition must be imported here.\n\nIf addon class or settings definition won't be here their definition won't\nbe found by OpenPype discovery.\n\"\"\"\n\nfrom .kitsu_module import KitsuModule\n\n__all__ = (\"KitsuModule\",)\n"
  },
  {
    "path": "openpype/modules/kitsu/actions/launcher_show_in_kitsu.py",
    "content": "import webbrowser\n\nfrom openpype.pipeline import LauncherAction\nfrom openpype.modules import ModulesManager\nfrom openpype.client import get_project, get_asset_by_name\n\n\nclass ShowInKitsu(LauncherAction):\n    name = \"showinkitsu\"\n    label = \"Show in Kitsu\"\n    icon = \"external-link-square\"\n    color = \"#e0e1e1\"\n    order = 10\n\n    @staticmethod\n    def get_kitsu_module():\n        return ModulesManager().modules_by_name.get(\"kitsu\")\n\n    def is_compatible(self, session):\n        if not session.get(\"AVALON_PROJECT\"):\n            return False\n\n        return True\n\n    def process(self, session, **kwargs):\n        # Context inputs\n        project_name = session[\"AVALON_PROJECT\"]\n        asset_name = session.get(\"AVALON_ASSET\", None)\n        task_name = session.get(\"AVALON_TASK\", None)\n\n        project = get_project(\n            project_name=project_name, fields=[\"data.zou_id\"]\n        )\n        if not project:\n            raise RuntimeError(\"Project {} not found.\".format(project_name))\n\n        project_zou_id = project[\"data\"].get(\"zou_id\")\n        if not project_zou_id:\n            raise RuntimeError(\n                \"Project {} has no connected kitsu id.\".format(project_name)\n            )\n\n        asset_zou_name = None\n        asset_zou_id = None\n        asset_zou_type = \"Assets\"\n        task_zou_id = None\n        zou_sub_type = [\"AssetType\", \"Sequence\"]\n        if asset_name:\n            asset_zou_name = asset_name\n            asset_fields = [\"data.zou.id\", \"data.zou.type\"]\n            if task_name:\n                asset_fields.append(\"data.tasks.{}.zou.id\".format(task_name))\n\n            asset = get_asset_by_name(\n                project_name, asset_name=asset_name, fields=asset_fields\n            )\n\n            asset_zou_data = asset[\"data\"].get(\"zou\")\n\n            if asset_zou_data:\n                asset_zou_type = asset_zou_data[\"type\"]\n                if asset_zou_type not in zou_sub_type:\n                    asset_zou_id = asset_zou_data[\"id\"]\n            else:\n                asset_zou_type = asset_name\n\n            if task_name:\n                task_data = asset[\"data\"][\"tasks\"][task_name]\n                task_zou_data = task_data.get(\"zou\", {})\n                if not task_zou_data:\n                    self.log.debug(\n                        \"No zou task data for task: {}\".format(task_name)\n                    )\n                task_zou_id = task_zou_data[\"id\"]\n\n        # Define URL\n        url = self.get_url(\n            project_id=project_zou_id,\n            asset_name=asset_zou_name,\n            asset_id=asset_zou_id,\n            asset_type=asset_zou_type,\n            task_id=task_zou_id,\n        )\n\n        # Open URL in webbrowser\n        self.log.info(\"Opening URL: {}\".format(url))\n        webbrowser.open(\n            url,\n            # Try in new tab\n            new=2,\n        )\n\n    def get_url(\n        self,\n        project_id,\n        asset_name=None,\n        asset_id=None,\n        asset_type=None,\n        task_id=None,\n    ):\n        shots_url = {\"Shots\", \"Sequence\", \"Shot\"}\n        sub_type = {\"AssetType\", \"Sequence\"}\n        kitsu_module = self.get_kitsu_module()\n\n        # Get kitsu url with /api stripped\n        kitsu_url = kitsu_module.server_url\n        if kitsu_url.endswith(\"/api\"):\n            kitsu_url = kitsu_url[: -len(\"/api\")]\n\n        sub_url = f\"/productions/{project_id}\"\n        asset_type_url = \"shots\" if asset_type in shots_url else \"assets\"\n\n        if task_id:\n            # Go to task page\n            # /productions/{project-id}/{asset_type}/tasks/{task_id}\n            sub_url += f\"/{asset_type_url}/tasks/{task_id}\"\n\n        elif asset_id:\n            # Go to asset or shot page\n            # /productions/{project-id}/assets/{entity_id}\n            # /productions/{project-id}/shots/{entity_id}\n            sub_url += f\"/{asset_type_url}/{asset_id}\"\n\n        else:\n            # Go to project page\n            # Project page must end with a view\n            # /productions/{project-id}/assets/\n            # Add search method if is a sub_type\n            sub_url += f\"/{asset_type_url}\"\n            if asset_type in sub_type:\n                sub_url += f\"?search={asset_name}\"\n\n        return f\"{kitsu_url}{sub_url}\"\n"
  },
  {
    "path": "openpype/modules/kitsu/kitsu_module.py",
    "content": "\"\"\"Kitsu module.\"\"\"\n\nimport os\n\nfrom openpype.modules import (\n    click_wrap,\n    OpenPypeModule,\n    IPluginPaths,\n    ITrayAction,\n)\n\n\nclass KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction):\n    \"\"\"Kitsu module class.\"\"\"\n\n    label = \"Kitsu Connect\"\n    name = \"kitsu\"\n\n    def initialize(self, settings):\n        \"\"\"Initialization of module.\"\"\"\n        module_settings = settings[self.name]\n\n        # Enabled by settings\n        self.enabled = module_settings.get(\"enabled\", False)\n\n        # Add API URL schema\n        kitsu_url = module_settings[\"server\"].strip()\n        if kitsu_url:\n            # Ensure web url\n            if not kitsu_url.startswith(\"http\"):\n                kitsu_url = \"https://\" + kitsu_url\n\n            # Check for \"/api\" url validity\n            if not kitsu_url.endswith(\"api\"):\n                kitsu_url = \"{}{}api\".format(\n                    kitsu_url, \"\" if kitsu_url.endswith(\"/\") else \"/\"\n                )\n\n        self.server_url = kitsu_url\n\n        # UI which must not be created at this time\n        self._dialog = None\n\n    def tray_init(self):\n        \"\"\"Tray init.\"\"\"\n\n        self._create_dialog()\n\n    def tray_start(self):\n        \"\"\"Tray start.\"\"\"\n        from .utils.credentials import (\n            load_credentials,\n            validate_credentials,\n            set_credentials_envs,\n        )\n\n        login, password = load_credentials()\n\n        # Check credentials, ask them if needed\n        if validate_credentials(login, password):\n            set_credentials_envs(login, password)\n        else:\n            self.show_dialog()\n\n    def get_global_environments(self):\n        \"\"\"Kitsu's global environments.\"\"\"\n        return {\"KITSU_SERVER\": self.server_url}\n\n    def _create_dialog(self):\n        # Don't recreate dialog if already exists\n        if self._dialog is not None:\n            return\n\n        from .kitsu_widgets import KitsuPasswordDialog\n\n        self._dialog = KitsuPasswordDialog()\n\n    def show_dialog(self):\n        \"\"\"Show dialog to log-in.\"\"\"\n\n        # Make sure dialog is created\n        self._create_dialog()\n\n        # Show dialog\n        self._dialog.open()\n\n    def on_action_trigger(self):\n        \"\"\"Implementation of abstract method for `ITrayAction`.\"\"\"\n        self.show_dialog()\n\n    def get_plugin_paths(self):\n        \"\"\"Implementation of abstract method for `IPluginPaths`.\"\"\"\n        current_dir = os.path.dirname(os.path.abspath(__file__))\n\n        return {\n            \"publish\": [os.path.join(current_dir, \"plugins\", \"publish\")],\n            \"actions\": [os.path.join(current_dir, \"actions\")],\n        }\n\n    def cli(self, click_group):\n        click_group.add_command(cli_main.to_click_obj())\n\n\n@click_wrap.group(KitsuModule.name, help=\"Kitsu dynamic cli commands.\")\ndef cli_main():\n    pass\n\n\n@cli_main.command()\n@click_wrap.option(\"--login\", envvar=\"KITSU_LOGIN\", help=\"Kitsu login\")\n@click_wrap.option(\n    \"--password\", envvar=\"KITSU_PWD\", help=\"Password for kitsu username\"\n)\ndef push_to_zou(login, password):\n    \"\"\"Synchronize Zou database (Kitsu backend) with openpype database.\n\n    Args:\n        login (str): Kitsu user login\n        password (str): Kitsu user password\n    \"\"\"\n    from .utils.update_zou_with_op import sync_zou\n\n    sync_zou(login, password)\n\n\n@cli_main.command()\n@click_wrap.option(\"-l\", \"--login\", envvar=\"KITSU_LOGIN\", help=\"Kitsu login\")\n@click_wrap.option(\n    \"-p\", \"--password\", envvar=\"KITSU_PWD\", help=\"Password for kitsu username\"\n)\n@click_wrap.option(\n    \"-prj\",\n    \"--project\",\n    \"projects\",\n    multiple=True,\n    default=[],\n    help=\"Sync specific kitsu projects\",\n)\n@click_wrap.option(\n    \"-lo\",\n    \"--listen-only\",\n    \"listen_only\",\n    is_flag=True,\n    default=False,\n    help=\"Listen to events only without any syncing\",\n)\ndef sync_service(login, password, projects, listen_only):\n    \"\"\"Synchronize openpype database from Zou sever database.\n\n    Args:\n        login (str): Kitsu user login\n        password (str): Kitsu user password\n        projects (tuple): specific kitsu projects\n        listen_only (bool): run listen only without any syncing\n    \"\"\"\n    from .utils.update_op_with_zou import sync_all_projects\n    from .utils.sync_service import start_listeners\n\n    if not listen_only:\n        sync_all_projects(login, password, filter_projects=projects)\n\n    start_listeners(login, password)\n"
  },
  {
    "path": "openpype/modules/kitsu/kitsu_widgets.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import style\nfrom openpype.modules.kitsu.utils.credentials import (\n    clear_credentials,\n    load_credentials,\n    save_credentials,\n    set_credentials_envs,\n    validate_credentials,\n)\nfrom openpype.resources import get_resource\nfrom openpype.settings.lib import (\n    get_system_settings,\n)\n\nfrom openpype.widgets.password_dialog import PressHoverButton\n\n\nclass KitsuPasswordDialog(QtWidgets.QDialog):\n    \"\"\"Kitsu login dialog.\"\"\"\n\n    finished = QtCore.Signal(bool)\n\n    def __init__(self, parent=None):\n        super(KitsuPasswordDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"Kitsu Credentials\")\n        self.resize(300, 120)\n\n        system_settings = get_system_settings()\n        user_login, user_pwd = load_credentials()\n        remembered = bool(user_login or user_pwd)\n\n        self._final_result = None\n        self._connectable = bool(\n            system_settings[\"modules\"].get(\"kitsu\", {}).get(\"server\")\n        )\n\n        # Server label\n        server_message = (\n            system_settings[\"modules\"][\"kitsu\"][\"server\"]\n            if self._connectable\n            else \"no server url set in Studio Settings...\"\n        )\n        server_label = QtWidgets.QLabel(\n            f\"Server: {server_message}\",\n            self,\n        )\n\n        # Login input\n        login_widget = QtWidgets.QWidget(self)\n\n        login_label = QtWidgets.QLabel(\"Login:\", login_widget)\n\n        login_input = QtWidgets.QLineEdit(\n            login_widget,\n            text=user_login if remembered else None,\n        )\n        login_input.setPlaceholderText(\"Your Kitsu account login...\")\n\n        login_layout = QtWidgets.QHBoxLayout(login_widget)\n        login_layout.setContentsMargins(0, 0, 0, 0)\n        login_layout.addWidget(login_label)\n        login_layout.addWidget(login_input)\n\n        # Password input\n        password_widget = QtWidgets.QWidget(self)\n\n        password_label = QtWidgets.QLabel(\"Password:\", password_widget)\n\n        password_input = QtWidgets.QLineEdit(\n            password_widget,\n            text=user_pwd if remembered else None,\n        )\n        password_input.setPlaceholderText(\"Your password...\")\n        password_input.setEchoMode(QtWidgets.QLineEdit.Password)\n\n        show_password_icon_path = get_resource(\"icons\", \"eye.png\")\n        show_password_icon = QtGui.QIcon(show_password_icon_path)\n        show_password_btn = PressHoverButton(password_widget)\n        show_password_btn.setObjectName(\"PasswordBtn\")\n        show_password_btn.setIcon(show_password_icon)\n        show_password_btn.setFocusPolicy(QtCore.Qt.ClickFocus)\n\n        password_layout = QtWidgets.QHBoxLayout(password_widget)\n        password_layout.setContentsMargins(0, 0, 0, 0)\n        password_layout.addWidget(password_label)\n        password_layout.addWidget(password_input)\n        password_layout.addWidget(show_password_btn)\n\n        # Message label\n        message_label = QtWidgets.QLabel(\"\", self)\n\n        # Buttons\n        buttons_widget = QtWidgets.QWidget(self)\n\n        remember_checkbox = QtWidgets.QCheckBox(\"Remember\", buttons_widget)\n        remember_checkbox.setObjectName(\"RememberCheckbox\")\n        remember_checkbox.setChecked(remembered)\n\n        ok_btn = QtWidgets.QPushButton(\"Ok\", buttons_widget)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", buttons_widget)\n\n        buttons_layout = QtWidgets.QHBoxLayout(buttons_widget)\n        buttons_layout.setContentsMargins(0, 0, 0, 0)\n        buttons_layout.addWidget(remember_checkbox)\n        buttons_layout.addStretch(1)\n        buttons_layout.addWidget(ok_btn)\n        buttons_layout.addWidget(cancel_btn)\n\n        # Main layout\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addSpacing(5)\n        layout.addWidget(server_label, 0)\n        layout.addSpacing(5)\n        layout.addWidget(login_widget, 0)\n        layout.addWidget(password_widget, 0)\n        layout.addWidget(message_label, 0)\n        layout.addStretch(1)\n        layout.addWidget(buttons_widget, 0)\n\n        ok_btn.clicked.connect(self._on_ok_click)\n        cancel_btn.clicked.connect(self._on_cancel_click)\n        show_password_btn.change_state.connect(self._on_show_password)\n\n        self.login_input = login_input\n        self.password_input = password_input\n        self.remember_checkbox = remember_checkbox\n        self.message_label = message_label\n\n        self.setStyleSheet(style.load_stylesheet())\n\n    def result(self):\n        return self._final_result\n\n    def keyPressEvent(self, event):\n        if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):\n            self._on_ok_click()\n            return event.accept()\n        super(KitsuPasswordDialog, self).keyPressEvent(event)\n\n    def closeEvent(self, event):\n        super(KitsuPasswordDialog, self).closeEvent(event)\n        self.finished.emit(self.result())\n\n    def _on_ok_click(self):\n        # Check if is connectable\n        if not self._connectable:\n            self.message_label.setText(\n                \"Please set server url in Studio Settings!\"\n            )\n            return\n\n        # Collect values\n        login_value = self.login_input.text()\n        pwd_value = self.password_input.text()\n        remember = self.remember_checkbox.isChecked()\n\n        # Authenticate\n        if validate_credentials(login_value, pwd_value):\n            set_credentials_envs(login_value, pwd_value)\n        else:\n            self.message_label.setText(\"Authentication failed...\")\n            return\n\n        # Remember password cases\n        if remember:\n            save_credentials(login_value, pwd_value)\n        else:\n            # Clear local settings\n            clear_credentials()\n\n            # Clear input fields\n            self.login_input.clear()\n            self.password_input.clear()\n\n        self._final_result = True\n        self.close()\n\n    def _on_show_password(self, show_password):\n        if show_password:\n            echo_mode = QtWidgets.QLineEdit.Normal\n        else:\n            echo_mode = QtWidgets.QLineEdit.Password\n        self.password_input.setEchoMode(echo_mode)\n\n    def _on_cancel_click(self):\n        self.close()\n"
  },
  {
    "path": "openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\n\nimport gazu\nimport pyblish.api\n\n\nclass CollectKitsuSession(pyblish.api.ContextPlugin):  # rename log in\n    \"\"\"Collect Kitsu session using user credentials\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = \"Kitsu user session\"\n    # families = [\"kitsu\"]\n\n    def process(self, context):\n        gazu.client.set_host(os.environ[\"KITSU_SERVER\"])\n        gazu.log_in(os.environ[\"KITSU_LOGIN\"], os.environ[\"KITSU_PWD\"])\n"
  },
  {
    "path": "openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py",
    "content": "# -*- coding: utf-8 -*-\nimport gazu\nimport pyblish.api\n\n\nclass CollectKitsuEntities(pyblish.api.ContextPlugin):\n    \"\"\"Collect Kitsu entities according to the current context\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.499\n    label = \"Kitsu entities\"\n\n    def process(self, context):\n        kitsu_project = gazu.project.get_project_by_name(\n            context.data[\"projectName\"]\n        )\n        if not kitsu_project:\n            raise ValueError(\"Project not found in kitsu!\")\n\n        context.data[\"kitsu_project\"] = kitsu_project\n        self.log.debug(\"Collect kitsu project: {}\".format(kitsu_project))\n\n        kitsu_entities_by_id = {}\n        for instance in context:\n            asset_doc = instance.data.get(\"assetEntity\")\n            if not asset_doc:\n                continue\n\n            zou_asset_data = asset_doc[\"data\"].get(\"zou\")\n            if not zou_asset_data:\n                raise ValueError(\"Zou asset data not found in OpenPype!\")\n\n            task_name = instance.data.get(\"task\", context.data.get(\"task\"))\n            if not task_name:\n                continue\n\n            zou_task_data = asset_doc[\"data\"][\"tasks\"][task_name].get(\"zou\")\n            self.log.debug(\n                \"Collected zou task data: {}\".format(zou_task_data)\n            )\n\n            entity_id = zou_asset_data[\"id\"]\n            entity = kitsu_entities_by_id.get(entity_id)\n            if not entity:\n                entity = gazu.entity.get_entity(entity_id)\n                if not entity:\n                    raise ValueError(\n                        \"{} was not found in kitsu!\".format(\n                            zou_asset_data[\"name\"]\n                        )\n                    )\n\n            kitsu_entities_by_id[entity_id] = entity\n            instance.data[\"entity\"] = entity\n            self.log.debug(\n                \"Collect kitsu {}: {}\".format(zou_asset_data[\"type\"], entity)\n            )\n\n            if zou_task_data:\n                kitsu_task_id = zou_task_data[\"id\"]\n                kitsu_task = kitsu_entities_by_id.get(kitsu_task_id)\n                if not kitsu_task:\n                    kitsu_task = gazu.task.get_task(zou_task_data[\"id\"])\n                    kitsu_entities_by_id[kitsu_task_id] = kitsu_task\n            else:\n                kitsu_task_type = gazu.task.get_task_type_by_name(task_name)\n                if not kitsu_task_type:\n                    raise ValueError(\n                        \"Task type {} not found in Kitsu!\".format(task_name)\n                    )\n\n                kitsu_task = gazu.task.get_task_by_name(\n                    entity, kitsu_task_type\n                )\n\n            if not kitsu_task:\n                raise ValueError(\"Task not found in kitsu!\")\n            instance.data[\"kitsu_task\"] = kitsu_task\n            self.log.debug(\"Collect kitsu task: {}\".format(kitsu_task))\n"
  },
  {
    "path": "openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nimport re\n\nimport pyblish.api\n\n\nclass CollectKitsuUsername(pyblish.api.ContextPlugin):\n    \"\"\"Collect Kitsu username from the kitsu login\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.499\n    label = \"Kitsu username\"\n\n    def process(self, context):\n        kitsu_login = os.environ.get('KITSU_LOGIN')\n\n        if not kitsu_login:\n            return\n\n        kitsu_username = kitsu_login.split(\"@\")[0].replace('.', ' ')\n        new_username = re.sub('[^a-zA-Z]', ' ', kitsu_username).title()\n\n        for instance in context:\n            # Don't override customData if it already exists\n            if 'customData' not in instance.data:\n                instance.data['customData'] = {}\n\n            instance.data['customData'][\"kitsuUsername\"] = new_username\n"
  },
  {
    "path": "openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py",
    "content": "# -*- coding: utf-8 -*-\nimport gazu\nimport pyblish.api\nimport re\n\n\nclass IntegrateKitsuNote(pyblish.api.ContextPlugin):\n    \"\"\"Integrate Kitsu Note\"\"\"\n\n    order = pyblish.api.IntegratorOrder\n    label = \"Kitsu Note and Status\"\n    families = [\"render\", \"image\", \"online\", \"plate\", \"kitsu\"]\n\n    # status settings\n    set_status_note = False\n    note_status_shortname = \"wfa\"\n    status_change_conditions = {\n        \"status_conditions\": [],\n        \"family_requirements\": [],\n    }\n\n    # comment settings\n    custom_comment_template = {\n        \"enabled\": False,\n        \"comment_template\": \"{comment}\",\n    }\n\n    def format_publish_comment(self, instance):\n        \"\"\"Format the instance's publish comment\n\n        Formats `instance.data` against the custom template.\n        \"\"\"\n\n        def replace_missing_key(match):\n            \"\"\"If key is not found in kwargs, set None instead\"\"\"\n            key = match.group(1)\n            if key not in instance.data:\n                self.log.warning(\n                    \"Key '{}' was not found in instance.data \"\n                    \"and will be rendered as an empty string \"\n                    \"in the comment\".format(key)\n                )\n                return \"\"\n            else:\n                return str(instance.data[key])\n\n        template = self.custom_comment_template[\"comment_template\"]\n        pattern = r\"\\{([^}]*)\\}\"\n        return re.sub(pattern, replace_missing_key, template)\n\n    def process(self, context):\n        for instance in context:\n            # Check if instance is a review by checking its family\n            # Allow a match to primary family or any of families\n            families = set(\n                [instance.data[\"family\"]] + instance.data.get(\"families\", [])\n            )\n            if \"review\" not in families:\n                continue\n\n            kitsu_task = instance.data.get(\"kitsu_task\")\n            if not kitsu_task:\n                continue\n\n            # Get note status, by default uses the task status for the note\n            # if it is not specified in the configuration\n            shortname = kitsu_task[\"task_status\"][\"short_name\"].upper()\n            note_status = kitsu_task[\"task_status_id\"]\n\n            # Check if any status condition is not met\n            allow_status_change = True\n            for status_cond in self.status_change_conditions[\n                \"status_conditions\"\n            ]:\n                condition = status_cond[\"condition\"] == \"equal\"\n                match = status_cond[\"short_name\"].upper() == shortname\n                if match and not condition or condition and not match:\n                    allow_status_change = False\n                    break\n\n            if allow_status_change:\n                # Get families\n                families = {\n                    instance.data.get(\"family\")\n                    for instance in context\n                    if instance.data.get(\"publish\")\n                }\n\n                # Check if any family requirement is met\n                for family_requirement in self.status_change_conditions[\n                    \"family_requirements\"\n                ]:\n                    condition = family_requirement[\"condition\"] == \"equal\"\n\n                    for family in families:\n                        match = family_requirement[\"family\"].lower() == family\n                        if match and not condition or condition and not match:\n                            allow_status_change = False\n                            break\n\n                    if allow_status_change:\n                        break\n\n            # Set note status\n            if self.set_status_note and allow_status_change:\n                kitsu_status = gazu.task.get_task_status_by_short_name(\n                    self.note_status_shortname\n                )\n                if kitsu_status:\n                    note_status = kitsu_status\n                    self.log.info(\"Note Kitsu status: {}\".format(note_status))\n                else:\n                    self.log.info(\n                        \"Cannot find {} status. The status will not be \"\n                        \"changed!\".format(self.note_status_shortname)\n                    )\n\n            # Get comment text body\n            publish_comment = instance.data.get(\"comment\")\n            if self.custom_comment_template[\"enabled\"]:\n                publish_comment = self.format_publish_comment(instance)\n\n            if not publish_comment:\n                self.log.debug(\"Comment is not set.\")\n            else:\n                self.log.debug(\"Comment is `{}`\".format(publish_comment))\n\n            # Add comment to kitsu task\n            self.log.debug(\n                \"Add new note in tasks id {}\".format(kitsu_task[\"id\"])\n            )\n            kitsu_comment = gazu.task.add_comment(\n                kitsu_task, note_status, comment=publish_comment\n            )\n\n            instance.data[\"kitsu_comment\"] = kitsu_comment\n"
  },
  {
    "path": "openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py",
    "content": "# -*- coding: utf-8 -*-\nimport gazu\nimport pyblish.api\n\n\nclass IntegrateKitsuReview(pyblish.api.InstancePlugin):\n    \"\"\"Integrate Kitsu Review\"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.01\n    label = \"Kitsu Review\"\n    families = [\"render\", \"image\", \"online\", \"plate\", \"kitsu\"]\n    optional = True\n\n    def process(self, instance):\n        # Check comment has been created\n        comment_id = instance.data.get(\"kitsu_comment\", {}).get(\"id\")\n        if not comment_id:\n            self.log.debug(\n                \"Comment not created, review not pushed to preview.\"\n            )\n            return\n\n        # Add review representations as preview of comment\n        task_id = instance.data[\"kitsu_task\"][\"id\"]\n        for representation in instance.data.get(\"representations\", []):\n            # Skip if not tagged as review\n            if \"kitsureview\" not in representation.get(\"tags\", []):\n                continue\n            review_path = representation.get(\"published_path\")\n            self.log.debug(\"Found review at: {}\".format(review_path))\n\n            gazu.task.add_preview(\n                task_id, comment_id, review_path, normalize_movie=True\n            )\n            self.log.info(\"Review upload on comment\")\n"
  },
  {
    "path": "openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py",
    "content": "# -*- coding: utf-8 -*-\nimport gazu\nimport pyblish.api\n\n\nclass KitsuLogOut(pyblish.api.ContextPlugin):\n    \"\"\"\n    Log out from Kitsu API\n    \"\"\"\n\n    order = pyblish.api.IntegratorOrder + 10\n    label = \"Kitsu Log Out\"\n\n    def process(self, context):\n        gazu.log_out()\n"
  },
  {
    "path": "openpype/modules/kitsu/utils/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/modules/kitsu/utils/credentials.py",
    "content": "\"\"\"Kitsu credentials functions.\"\"\"\n\nimport os\nfrom typing import Tuple\nimport gazu\n\nfrom openpype.lib.local_settings import OpenPypeSecureRegistry\nfrom openpype.lib import emit_event\n\n\ndef validate_credentials(\n    login: str, password: str, kitsu_url: str = None\n) -> bool:\n    \"\"\"Validate credentials by trying to connect to Kitsu host URL.\n\n    Args:\n        login (str): Kitsu user login\n        password (str): Kitsu user password\n        kitsu_url (str, optional): Kitsu host URL. Defaults to None.\n\n    Returns:\n        bool: Are credentials valid?\n    \"\"\"\n    if kitsu_url is None:\n        kitsu_url = os.environ.get(\"KITSU_SERVER\")\n\n    # Connect to server\n    validate_host(kitsu_url)\n\n    # Authenticate\n    try:\n        gazu.log_in(login, password)\n    except gazu.exception.AuthFailedException:\n        return False\n\n    emit_event(\"kitsu.user.logged\", data={\"username\": login}, source=\"kitsu\")\n\n    return True\n\n\ndef validate_host(kitsu_url: str) -> bool:\n    \"\"\"Validate credentials by trying to connect to Kitsu host URL.\n\n    Args:\n        kitsu_url (str, optional): Kitsu host URL.\n\n    Returns:\n        bool: Is host valid?\n    \"\"\"\n    # Connect to server\n    gazu.set_host(kitsu_url)\n\n    # Test host\n    if gazu.client.host_is_valid():\n        return True\n    else:\n        raise gazu.exception.HostException(\n            \"Host '{}' is invalid.\".format(kitsu_url))\n\n\ndef clear_credentials():\n    \"\"\"Clear credentials in Secure Registry.\"\"\"\n    # Get user registry\n    user_registry = OpenPypeSecureRegistry(\"kitsu_user\")\n\n    # Set local settings\n    if user_registry.get_item(\"login\", None) is not None:\n        user_registry.delete_item(\"login\")\n    if user_registry.get_item(\"password\", None) is not None:\n        user_registry.delete_item(\"password\")\n\n\ndef save_credentials(login: str, password: str):\n    \"\"\"Save credentials in Secure Registry.\n\n    Args:\n        login (str): Kitsu user login\n        password (str): Kitsu user password\n    \"\"\"\n    # Get user registry\n    user_registry = OpenPypeSecureRegistry(\"kitsu_user\")\n\n    # Set local settings\n    user_registry.set_item(\"login\", login)\n    user_registry.set_item(\"password\", password)\n\n\ndef load_credentials() -> Tuple[str, str]:\n    \"\"\"Load registered credentials.\n\n    Returns:\n        Tuple[str, str]: (Login, Password)\n    \"\"\"\n    # Get user registry\n    user_registry = OpenPypeSecureRegistry(\"kitsu_user\")\n\n    return (\n        user_registry.get_item(\"login\", None),\n        user_registry.get_item(\"password\", None)\n    )\n\n\ndef set_credentials_envs(login: str, password: str):\n    \"\"\"Set environment variables with Kitsu login and password.\n\n    Args:\n        login (str): Kitsu user login\n        password (str): Kitsu user password\n    \"\"\"\n    os.environ[\"KITSU_LOGIN\"] = login\n    os.environ[\"KITSU_PWD\"] = password\n"
  },
  {
    "path": "openpype/modules/kitsu/utils/sync_service.py",
    "content": "\"\"\"\nBugs:\n    * Error when adding task type to anything that isn't Shot or Assets\n    * Assets don't get added under an episode if TV show\n    * Assets added under Main Pack throws error. No Main Pack name in dict\n\nFeatures ToDo:\n    * Select in settings what types you wish to sync\n    * Print what's updated on entity-update\n    * Add listener for Edits\n\"\"\"\n\nimport os\nimport threading\n\nimport gazu\n\nfrom openpype.client import get_project, get_assets, get_asset_by_name\nfrom openpype.pipeline import AvalonMongoDB\nfrom openpype.lib import Logger\nfrom .credentials import validate_credentials\nfrom .update_op_with_zou import (\n    create_op_asset,\n    set_op_project,\n    get_kitsu_project_name,\n    write_project_to_op,\n    update_op_assets,\n)\n\nlog = Logger.get_logger(__name__)\n\n\nclass Listener:\n    \"\"\"Host Kitsu listener.\"\"\"\n\n    def __init__(self, login, password):\n        \"\"\"Create client and add listeners to events without starting it.\n\n            Run `listener.start()` to actually start the service.\n\n        Args:\n            login (str): Kitsu user login\n            password (str): Kitsu user password\n\n        Raises:\n            AuthFailedException: Wrong user login and/or password\n        \"\"\"\n        self.dbcon = AvalonMongoDB()\n        self.dbcon.install()\n\n        gazu.client.set_host(os.environ[\"KITSU_SERVER\"])\n\n        # Authenticate\n        if not validate_credentials(login, password):\n            raise gazu.exception.AuthFailedException(\n                'Kitsu authentication failed for login: \"{}\"...'.format(login)\n            )\n\n        gazu.set_event_host(\n            os.environ[\"KITSU_SERVER\"].replace(\"api\", \"socket.io\")\n        )\n        self.event_client = gazu.events.init()\n\n        gazu.events.add_listener(\n            self.event_client, \"project:new\", self._new_project\n        )\n        gazu.events.add_listener(\n            self.event_client, \"project:update\", self._update_project\n        )\n        gazu.events.add_listener(\n            self.event_client, \"project:delete\", self._delete_project\n        )\n\n        gazu.events.add_listener(\n            self.event_client, \"asset:new\", self._new_asset\n        )\n        gazu.events.add_listener(\n            self.event_client, \"asset:update\", self._update_asset\n        )\n        gazu.events.add_listener(\n            self.event_client, \"asset:delete\", self._delete_asset\n        )\n\n        gazu.events.add_listener(\n            self.event_client, \"episode:new\", self._new_episode\n        )\n        gazu.events.add_listener(\n            self.event_client, \"episode:update\", self._update_episode\n        )\n        gazu.events.add_listener(\n            self.event_client, \"episode:delete\", self._delete_episode\n        )\n\n        gazu.events.add_listener(\n            self.event_client, \"sequence:new\", self._new_sequence\n        )\n        gazu.events.add_listener(\n            self.event_client, \"sequence:update\", self._update_sequence\n        )\n        gazu.events.add_listener(\n            self.event_client, \"sequence:delete\", self._delete_sequence\n        )\n\n        gazu.events.add_listener(\n            self.event_client, \"shot:new\", self._new_shot\n        )\n        gazu.events.add_listener(\n            self.event_client, \"shot:update\", self._update_shot\n        )\n        gazu.events.add_listener(\n            self.event_client, \"shot:delete\", self._delete_shot\n        )\n\n        gazu.events.add_listener(\n            self.event_client, \"task:new\", self._new_task\n        )\n        gazu.events.add_listener(\n            self.event_client, \"task:update\", self._update_task\n        )\n        gazu.events.add_listener(\n            self.event_client, \"task:delete\", self._delete_task\n        )\n\n    def start(self):\n        \"\"\"Start listening for events.\"\"\"\n        log.info(\"Listening to Kitsu events...\")\n        gazu.events.run_client(self.event_client)\n\n    def get_ep_dict(self, ep_id):\n        if ep_id and ep_id != \"\":\n            return gazu.entity.get_entity(ep_id)\n        return\n\n    # == Project ==\n    def _new_project(self, data):\n        \"\"\"Create new project into OP DB.\"\"\"\n\n        # Use update process to avoid duplicating code\n        self._update_project(data, new_project=True)\n\n    def _update_project(self, data, new_project=False):\n        \"\"\"Update project into OP DB.\"\"\"\n        # Get project entity\n        project = gazu.project.get_project(data[\"project_id\"])\n\n        update_project = write_project_to_op(project, self.dbcon)\n\n        # Write into DB\n        if update_project:\n            self.dbcon.Session[\"AVALON_PROJECT\"] = get_kitsu_project_name(\n                data[\"project_id\"]\n            )\n            self.dbcon.bulk_write([update_project])\n\n            if new_project:\n                log.info(\"Project created: {}\".format(project[\"name\"]))\n\n    def _delete_project(self, data):\n        \"\"\"Delete project.\"\"\"\n\n        collections = self.dbcon.database.list_collection_names()\n        for collection in collections:\n            project = self.dbcon.database[collection].find_one(\n                {\"data.zou_id\": data[\"project_id\"]}\n            )\n            if project:\n                # Delete project collection\n                self.dbcon.database[project[\"name\"]].drop()\n\n                # Print message\n                log.info(\"Project deleted: {}\".format(project[\"name\"]))\n                return\n\n    # == Asset ==\n    def _new_asset(self, data):\n        \"\"\"Create new asset into OP DB.\"\"\"\n        # Get project entity\n        set_op_project(self.dbcon, data[\"project_id\"])\n\n        # Get asset entity\n        asset = gazu.asset.get_asset(data[\"asset_id\"])\n\n        # Insert doc in DB\n        self.dbcon.insert_one(create_op_asset(asset))\n\n        # Update\n        self._update_asset(data)\n\n        # Print message\n        ep_id = asset.get(\"episode_id\")\n        ep = self.get_ep_dict(ep_id)\n\n        msg = (\n            \"Asset created: {proj_name} - {ep_name}\"\n            \"{asset_type_name} - {asset_name}\".format(\n                proj_name=asset[\"project_name\"],\n                ep_name=ep[\"name\"] + \" - \" if ep is not None else \"\",\n                asset_type_name=asset[\"asset_type_name\"],\n                asset_name=asset[\"name\"],\n            )\n        )\n        log.info(msg)\n\n    def _update_asset(self, data):\n        \"\"\"Update asset into OP DB.\"\"\"\n        set_op_project(self.dbcon, data[\"project_id\"])\n        project_name = self.dbcon.active_project()\n        project_doc = get_project(project_name)\n\n        # Get gazu entity\n        asset = gazu.asset.get_asset(data[\"asset_id\"])\n\n        # Find asset doc\n        # Query all assets of the local project\n        zou_ids_and_asset_docs = {\n            asset_doc[\"data\"][\"zou\"][\"id\"]: asset_doc\n            for asset_doc in get_assets(project_name)\n            if asset_doc[\"data\"].get(\"zou\", {}).get(\"id\")\n        }\n        zou_ids_and_asset_docs[asset[\"project_id\"]] = project_doc\n        gazu_project = gazu.project.get_project(asset[\"project_id\"])\n\n        # Update\n        update_op_result = update_op_assets(\n            self.dbcon,\n            gazu_project,\n            project_doc,\n            [asset],\n            zou_ids_and_asset_docs,\n        )\n        if update_op_result:\n            asset_doc_id, asset_update = update_op_result[0]\n            self.dbcon.update_one({\"_id\": asset_doc_id}, asset_update)\n\n    def _delete_asset(self, data):\n        \"\"\"Delete asset of OP DB.\"\"\"\n        set_op_project(self.dbcon, data[\"project_id\"])\n\n        asset = self.dbcon.find_one({\"data.zou.id\": data[\"asset_id\"]})\n        if asset:\n            # Delete\n            self.dbcon.delete_one(\n                {\"type\": \"asset\", \"data.zou.id\": data[\"asset_id\"]}\n            )\n\n            # Print message\n            ep_id = asset[\"data\"][\"zou\"].get(\"episode_id\")\n            ep = self.get_ep_dict(ep_id)\n\n            msg = (\n                \"Asset deleted: {proj_name} - {ep_name}\"\n                \"{type_name} - {asset_name}\".format(\n                    proj_name=asset[\"data\"][\"zou\"][\"project_name\"],\n                    ep_name=ep[\"name\"] + \" - \" if ep is not None else \"\",\n                    type_name=asset[\"data\"][\"zou\"][\"asset_type_name\"],\n                    asset_name=asset[\"name\"],\n                )\n            )\n            log.info(msg)\n\n    # == Episode ==\n    def _new_episode(self, data):\n        \"\"\"Create new episode into OP DB.\"\"\"\n        # Get project entity\n        set_op_project(self.dbcon, data[\"project_id\"])\n\n        # Get gazu entity\n        ep = gazu.shot.get_episode(data[\"episode_id\"])\n\n        # Insert doc in DB\n        self.dbcon.insert_one(create_op_asset(ep))\n\n        # Update\n        self._update_episode(data)\n\n        # Print message\n        msg = \"Episode created: {proj_name} - {ep_name}\".format(\n            proj_name=ep[\"project_name\"], ep_name=ep[\"name\"]\n        )\n        log.info(msg)\n\n    def _update_episode(self, data):\n        \"\"\"Update episode into OP DB.\"\"\"\n        set_op_project(self.dbcon, data[\"project_id\"])\n        project_name = self.dbcon.active_project()\n        project_doc = get_project(project_name)\n\n        # Get gazu entity\n        ep = gazu.shot.get_episode(data[\"episode_id\"])\n\n        # Find asset doc\n        # Query all assets of the local project\n        zou_ids_and_asset_docs = {\n            asset_doc[\"data\"][\"zou\"][\"id\"]: asset_doc\n            for asset_doc in get_assets(project_name)\n            if asset_doc[\"data\"].get(\"zou\", {}).get(\"id\")\n        }\n        zou_ids_and_asset_docs[ep[\"project_id\"]] = project_doc\n        gazu_project = gazu.project.get_project(ep[\"project_id\"])\n\n        # Update\n        update_op_result = update_op_assets(\n            self.dbcon,\n            gazu_project,\n            project_doc,\n            [ep],\n            zou_ids_and_asset_docs,\n        )\n        if update_op_result:\n            asset_doc_id, asset_update = update_op_result[0]\n            self.dbcon.update_one({\"_id\": asset_doc_id}, asset_update)\n\n    def _delete_episode(self, data):\n        \"\"\"Delete shot of OP DB.\"\"\"\n        set_op_project(self.dbcon, data[\"project_id\"])\n\n        ep = self.dbcon.find_one({\"data.zou.id\": data[\"episode_id\"]})\n        if ep:\n            # Delete\n            self.dbcon.delete_one(\n                {\"type\": \"asset\", \"data.zou.id\": data[\"episode_id\"]}\n            )\n\n            # Print message\n            project = gazu.project.get_project(\n                ep[\"data\"][\"zou\"][\"project_id\"]\n            )\n\n            msg = \"Episode deleted: {proj_name} - {ep_name}\".format(\n                proj_name=project[\"name\"], ep_name=ep[\"name\"]\n            )\n            log.info(msg)\n\n    # == Sequence ==\n    def _new_sequence(self, data):\n        \"\"\"Create new sequnce into OP DB.\"\"\"\n        # Get project entity\n        set_op_project(self.dbcon, data[\"project_id\"])\n\n        # Get gazu entity\n        sequence = gazu.shot.get_sequence(data[\"sequence_id\"])\n\n        # Insert doc in DB\n        self.dbcon.insert_one(create_op_asset(sequence))\n\n        # Update\n        self._update_sequence(data)\n\n        # Print message\n        ep_id = sequence.get(\"episode_id\")\n        ep = self.get_ep_dict(ep_id)\n\n        msg = (\n            \"Sequence created: {proj_name} - {ep_name}\"\n            \"{sequence_name}\".format(\n                proj_name=sequence[\"project_name\"],\n                ep_name=ep[\"name\"] + \" - \" if ep is not None else \"\",\n                sequence_name=sequence[\"name\"],\n            )\n        )\n        log.info(msg)\n\n    def _update_sequence(self, data):\n        \"\"\"Update sequence into OP DB.\"\"\"\n        set_op_project(self.dbcon, data[\"project_id\"])\n        project_name = self.dbcon.active_project()\n        project_doc = get_project(project_name)\n\n        # Get gazu entity\n        sequence = gazu.shot.get_sequence(data[\"sequence_id\"])\n\n        # Find asset doc\n        # Query all assets of the local project\n        zou_ids_and_asset_docs = {\n            asset_doc[\"data\"][\"zou\"][\"id\"]: asset_doc\n            for asset_doc in get_assets(project_name)\n            if asset_doc[\"data\"].get(\"zou\", {}).get(\"id\")\n        }\n        zou_ids_and_asset_docs[sequence[\"project_id\"]] = project_doc\n        gazu_project = gazu.project.get_project(sequence[\"project_id\"])\n\n        # Update\n        update_op_result = update_op_assets(\n            self.dbcon,\n            gazu_project,\n            project_doc,\n            [sequence],\n            zou_ids_and_asset_docs,\n        )\n        if update_op_result:\n            asset_doc_id, asset_update = update_op_result[0]\n            self.dbcon.update_one({\"_id\": asset_doc_id}, asset_update)\n\n    def _delete_sequence(self, data):\n        \"\"\"Delete sequence of OP DB.\"\"\"\n        set_op_project(self.dbcon, data[\"project_id\"])\n        sequence = self.dbcon.find_one({\"data.zou.id\": data[\"sequence_id\"]})\n        if sequence:\n            # Delete\n            self.dbcon.delete_one(\n                {\"type\": \"asset\", \"data.zou.id\": data[\"sequence_id\"]}\n            )\n\n            # Print message\n            ep_id = sequence[\"data\"][\"zou\"].get(\"episode_id\")\n            ep = self.get_ep_dict(ep_id)\n\n            gazu_project = gazu.project.get_project(\n                sequence[\"data\"][\"zou\"][\"project_id\"]\n            )\n\n            msg = (\n                \"Sequence deleted: {proj_name} - {ep_name}\"\n                \"{sequence_name}\".format(\n                    proj_name=gazu_project[\"name\"],\n                    ep_name=ep[\"name\"] + \" - \" if ep is not None else \"\",\n                    sequence_name=sequence[\"name\"],\n                )\n            )\n            log.info(msg)\n\n    # == Shot ==\n    def _new_shot(self, data):\n        \"\"\"Create new shot into OP DB.\"\"\"\n        # Get project entity\n        set_op_project(self.dbcon, data[\"project_id\"])\n\n        # Get gazu entity\n        shot = gazu.shot.get_shot(data[\"shot_id\"])\n\n        # Insert doc in DB\n        self.dbcon.insert_one(create_op_asset(shot))\n\n        # Update\n        self._update_shot(data)\n\n        # Print message\n        ep_id = shot[\"episode_id\"]\n        ep = self.get_ep_dict(ep_id)\n\n        msg = (\n            \"Shot created: {proj_name} - {ep_name}\"\n            \"{sequence_name} - {shot_name}\".format(\n                proj_name=shot[\"project_name\"],\n                ep_name=ep[\"name\"] + \" - \" if ep is not None else \"\",\n                sequence_name=shot[\"sequence_name\"],\n                shot_name=shot[\"name\"],\n            )\n        )\n        log.info(msg)\n\n    def _update_shot(self, data):\n        \"\"\"Update shot into OP DB.\"\"\"\n        set_op_project(self.dbcon, data[\"project_id\"])\n        project_name = self.dbcon.active_project()\n        project_doc = get_project(project_name)\n\n        # Get gazu entity\n        shot = gazu.shot.get_shot(data[\"shot_id\"])\n\n        # Find asset doc\n        # Query all assets of the local project\n        zou_ids_and_asset_docs = {\n            asset_doc[\"data\"][\"zou\"][\"id\"]: asset_doc\n            for asset_doc in get_assets(project_name)\n            if asset_doc[\"data\"].get(\"zou\", {}).get(\"id\")\n        }\n        zou_ids_and_asset_docs[shot[\"project_id\"]] = project_doc\n        gazu_project = gazu.project.get_project(shot[\"project_id\"])\n\n        # Update\n        update_op_result = update_op_assets(\n            self.dbcon,\n            gazu_project,\n            project_doc,\n            [shot],\n            zou_ids_and_asset_docs,\n        )\n\n        if update_op_result:\n            asset_doc_id, asset_update = update_op_result[0]\n            self.dbcon.update_one({\"_id\": asset_doc_id}, asset_update)\n\n    def _delete_shot(self, data):\n        \"\"\"Delete shot of OP DB.\"\"\"\n        set_op_project(self.dbcon, data[\"project_id\"])\n        shot = self.dbcon.find_one({\"data.zou.id\": data[\"shot_id\"]})\n\n        if shot:\n            # Delete\n            self.dbcon.delete_one(\n                {\"type\": \"asset\", \"data.zou.id\": data[\"shot_id\"]}\n            )\n\n            # Print message\n            ep_id = shot[\"data\"][\"zou\"].get(\"episode_id\")\n            ep = self.get_ep_dict(ep_id)\n\n            msg = (\n                \"Shot deleted: {proj_name} - {ep_name}\"\n                \"{sequence_name} - {shot_name}\".format(\n                    proj_name=shot[\"data\"][\"zou\"][\"project_name\"],\n                    ep_name=ep[\"name\"] + \" - \" if ep is not None else \"\",\n                    sequence_name=shot[\"data\"][\"zou\"][\"sequence_name\"],\n                    shot_name=shot[\"name\"],\n                )\n            )\n            log.info(msg)\n\n    # == Task ==\n    def _new_task(self, data):\n        \"\"\"Create new task into OP DB.\"\"\"\n        # Get project entity\n        set_op_project(self.dbcon, data[\"project_id\"])\n        project_name = self.dbcon.active_project()\n\n        # Get gazu entity\n        task = gazu.task.get_task(data[\"task_id\"])\n\n        # Print message\n        ep_id = task.get(\"episode_id\")\n        ep = self.get_ep_dict(ep_id)\n\n        parent_name = None\n        asset_name = None\n        ent_type = None\n\n        if task[\"task_type\"][\"for_entity\"] == \"Asset\":\n            parent_name = task[\"entity\"][\"name\"]\n            asset_name = task[\"entity\"][\"name\"]\n            ent_type = task[\"entity_type\"][\"name\"]\n        elif task[\"task_type\"][\"for_entity\"] == \"Shot\":\n            parent_name = \"{ep_name}{sequence_name} - {shot_name}\".format(\n                ep_name=ep[\"name\"] + \" - \" if ep is not None else \"\",\n                sequence_name=task[\"sequence\"][\"name\"],\n                shot_name=task[\"entity\"][\"name\"],\n            )\n            asset_name = \"{ep_name}{sequence_name}_{shot_name}\".format(\n                ep_name=ep[\"name\"] + \"_\" if ep is not None else \"\",\n                sequence_name=task[\"sequence\"][\"name\"],\n                shot_name=task[\"entity\"][\"name\"],\n            )\n\n        # Update asset tasks with new one\n        asset_doc = get_asset_by_name(project_name, asset_name)\n        if asset_doc:\n            asset_tasks = asset_doc[\"data\"].get(\"tasks\")\n            task_type_name = task[\"task_type\"][\"name\"]\n            asset_tasks[task_type_name] = {\n                \"type\": task_type_name,\n                \"zou\": task,\n            }\n            self.dbcon.update_one(\n                {\"_id\": asset_doc[\"_id\"]},\n                {\"$set\": {\"data.tasks\": asset_tasks}},\n            )\n\n            # Print message\n            msg = (\n                \"Task created: {proj} - {ent_type}{parent}\"\n                \" - {task}\".format(\n                    proj=task[\"project\"][\"name\"],\n                    ent_type=ent_type + \" - \" if ent_type is not None else \"\",\n                    parent=parent_name,\n                    task=task[\"task_type\"][\"name\"],\n                )\n            )\n            log.info(msg)\n\n    def _update_task(self, data):\n        \"\"\"Update task into OP DB.\"\"\"\n        # TODO is it necessary?\n\n    def _delete_task(self, data):\n        \"\"\"Delete task of OP DB.\"\"\"\n\n        set_op_project(self.dbcon, data[\"project_id\"])\n        project_name = self.dbcon.active_project()\n        # Find asset doc\n        asset_docs = list(get_assets(project_name))\n        for doc in asset_docs:\n            # Match task\n            for name, task in doc[\"data\"][\"tasks\"].items():\n                if task.get(\"zou\") and data[\"task_id\"] == task[\"zou\"][\"id\"]:\n                    # Pop task\n                    asset_tasks = doc[\"data\"].get(\"tasks\", {})\n                    asset_tasks.pop(name)\n\n                    # Delete task in DB\n                    self.dbcon.update_one(\n                        {\"_id\": doc[\"_id\"]},\n                        {\"$set\": {\"data.tasks\": asset_tasks}},\n                    )\n\n                    # Print message\n                    entity = gazu.entity.get_entity(task[\"zou\"][\"entity_id\"])\n                    ep = self.get_ep_dict(entity[\"source_id\"])\n\n                    if entity[\"type\"] == \"Asset\":\n                        parent_name = \"{ep}{entity_type} - {entity}\".format(\n                            ep=ep[\"name\"] + \" - \" if ep is not None else \"\",\n                            entity_type=task[\"zou\"][\"entity_type\"][\"name\"],\n                            entity=task[\"zou\"][\"entity\"][\"name\"],\n                        )\n                    elif entity[\"type\"] == \"Shot\":\n                        parent_name = \"{ep}{sequence} - {shot}\".format(\n                            ep=ep[\"name\"] + \" - \" if ep is not None else \"\",\n                            sequence=task[\"zou\"][\"sequence\"][\"name\"],\n                            shot=task[\"zou\"][\"entity\"][\"name\"],\n                        )\n\n                    msg = \"Task deleted: {proj} - {parent} - {task}\".format(\n                        proj=task[\"zou\"][\"project\"][\"name\"],\n                        parent=parent_name,\n                        task=name,\n                    )\n                    log.info(msg)\n\n                    return\n\n\ndef start_listeners(login: str, password: str):\n    \"\"\"Start listeners to keep OpenPype up-to-date with Kitsu.\n\n    Args:\n        login (str): Kitsu user login\n        password (str): Kitsu user password\n    \"\"\"\n\n    # Refresh token every week\n    def refresh_token_every_week():\n        log.info(\"Refreshing token...\")\n        gazu.refresh_token()\n        threading.Timer(7 * 3600 * 24, refresh_token_every_week).start()\n\n    refresh_token_every_week()\n\n    # Connect to server\n    listener = Listener(login, password)\n    listener.start()\n"
  },
  {
    "path": "openpype/modules/kitsu/utils/update_op_with_zou.py",
    "content": "\"\"\"Functions to update OpenPype data using Kitsu DB (a.k.a Zou).\"\"\"\nfrom copy import deepcopy\nimport re\nfrom typing import Dict, List\n\nfrom pymongo import DeleteOne, UpdateOne\nimport gazu\n\nfrom openpype.client import (\n    get_project,\n    get_assets,\n    get_asset_by_id,\n    get_asset_by_name,\n    create_project,\n)\nfrom openpype.pipeline import AvalonMongoDB\nfrom openpype.modules.kitsu.utils.credentials import validate_credentials\n\nfrom openpype.lib import Logger\n\nlog = Logger.get_logger(__name__)\n\n# Accepted namin pattern for OP\nnaming_pattern = re.compile(\"^[a-zA-Z0-9_.]*$\")\n\n\ndef create_op_asset(gazu_entity: dict) -> dict:\n    \"\"\"Create OP asset dict from gazu entity.\n\n    :param gazu_entity:\n    \"\"\"\n    return {\n        \"name\": gazu_entity[\"name\"],\n        \"type\": \"asset\",\n        \"schema\": \"openpype:asset-3.0\",\n        \"data\": {\"zou\": gazu_entity, \"tasks\": {}},\n    }\n\n\ndef get_kitsu_project_name(project_id: str) -> str:\n    \"\"\"Get project name based on project id in kitsu.\n\n    Args:\n        project_id (str): UUID of project in Kitsu.\n\n    Returns:\n        str: Name of Kitsu project.\n    \"\"\"\n\n    project = gazu.project.get_project(project_id)\n    return project[\"name\"]\n\n\ndef set_op_project(dbcon: AvalonMongoDB, project_id: str):\n    \"\"\"Set project context.\n\n    Args:\n        dbcon (AvalonMongoDB): Connection to DB\n        project_id (str): Project zou ID\n    \"\"\"\n\n    dbcon.Session[\"AVALON_PROJECT\"] = get_kitsu_project_name(project_id)\n\n\ndef update_op_assets(\n    dbcon: AvalonMongoDB,\n    gazu_project: dict,\n    project_doc: dict,\n    entities_list: List[dict],\n    asset_doc_ids: Dict[str, dict],\n) -> List[Dict[str, dict]]:\n    \"\"\"Update OpenPype assets.\n    Set 'data' and 'parent' fields.\n\n    Args:\n        dbcon (AvalonMongoDB): Connection to DB\n        gazu_project (dict): Dict of gazu,\n        project_doc (dict): Dict of project,\n        entities_list (List[dict]): List of zou entities to update\n        asset_doc_ids (Dict[str, dict]): Dicts of [{zou_id: asset_doc}, ...]\n\n    Returns:\n        List[Dict[str, dict]]: List of (doc_id, update_dict) tuples\n    \"\"\"\n    if not project_doc:\n        return\n\n    project_name = project_doc[\"name\"]\n\n    assets_with_update = []\n    for item in entities_list:\n        # Check asset exists\n        item_doc = asset_doc_ids.get(item[\"id\"])\n        if not item_doc:  # Create asset\n            op_asset = create_op_asset(item)\n            insert_result = dbcon.insert_one(op_asset)\n            item_doc = get_asset_by_id(project_name, insert_result.inserted_id)\n\n        # Update asset\n        item_data = deepcopy(item_doc[\"data\"])\n        item_data.update(item.get(\"data\") or {})\n        item_data[\"zou\"] = item\n\n        # == Asset settings ==\n        # Frame in, fallback to project's value or default value (1001)\n        # TODO: get default from settings/project_anatomy/attributes.json\n        try:\n            frame_in = int(\n                item_data.pop(\n                    \"frame_in\", project_doc[\"data\"].get(\"frameStart\")\n                )\n            )\n        except (TypeError, ValueError):\n            frame_in = 1001\n        item_data[\"frameStart\"] = frame_in\n        # Frames duration, fallback on 1\n        try:\n            # NOTE nb_frames is stored directly in item\n            # because of zou's legacy design\n            frames_duration = int(item.get(\"nb_frames\", 1))\n        except (TypeError, ValueError):\n            frames_duration = None\n        # Frame out, fallback on frame_in + duration or project's value or 1001\n        frame_out = item_data.pop(\"frame_out\", None)\n        if not frame_out:\n            if frames_duration:\n                frame_out = frame_in + frames_duration - 1\n            else:\n                frame_out = project_doc[\"data\"].get(\"frameEnd\", frame_in)\n        item_data[\"frameEnd\"] = int(frame_out)\n        # Fps, fallback to project's value or default value (25.0)\n        try:\n            fps = float(item_data.get(\"fps\"))\n        except (TypeError, ValueError):\n            fps = float(\n                gazu_project.get(\"fps\", project_doc[\"data\"].get(\"fps\", 25))\n            )\n        item_data[\"fps\"] = fps\n        # Resolution, fall back to project default\n        match_res = re.match(\n            r\"(\\d+)x(\\d+)\",\n            item_data.get(\"resolution\", gazu_project.get(\"resolution\")),\n        )\n        if match_res:\n            item_data[\"resolutionWidth\"] = int(match_res.group(1))\n            item_data[\"resolutionHeight\"] = int(match_res.group(2))\n        else:\n            item_data[\"resolutionWidth\"] = int(\n                project_doc[\"data\"].get(\"resolutionWidth\")\n            )\n            item_data[\"resolutionHeight\"] = int(\n                project_doc[\"data\"].get(\"resolutionHeight\")\n            )\n        # Properties that doesn't fully exist in Kitsu.\n        # Guessing those property names below:\n        # Pixel Aspect Ratio\n        item_data[\"pixelAspect\"] = float(\n            item_data.get(\n                \"pixel_aspect\", project_doc[\"data\"].get(\"pixelAspect\")\n            )\n        )\n        # Handle Start\n        item_data[\"handleStart\"] = int(\n            item_data.get(\n                \"handle_start\", project_doc[\"data\"].get(\"handleStart\")\n            )\n        )\n        # Handle End\n        item_data[\"handleEnd\"] = int(\n            item_data.get(\"handle_end\", project_doc[\"data\"].get(\"handleEnd\"))\n        )\n        # Clip In\n        item_data[\"clipIn\"] = int(\n            item_data.get(\"clip_in\", project_doc[\"data\"].get(\"clipIn\"))\n        )\n        # Clip Out\n        item_data[\"clipOut\"] = int(\n            item_data.get(\"clip_out\", project_doc[\"data\"].get(\"clipOut\"))\n        )\n\n        # Tasks\n        tasks_list = []\n        item_type = item[\"type\"]\n        if item_type == \"Asset\":\n            tasks_list = gazu.task.all_tasks_for_asset(item)\n        elif item_type == \"Shot\":\n            tasks_list = gazu.task.all_tasks_for_shot(item)\n        item_data[\"tasks\"] = {\n            t[\"task_type_name\"]: {\n                \"type\": t[\"task_type_name\"],\n                \"zou\": gazu.task.get_task(t[\"id\"]),\n            }\n            for t in tasks_list\n        }\n\n        # Get zou parent id for correct hierarchy\n        # Use parent substitutes if existing\n        substitute_parent_item = (\n            item_data[\"parent_substitutes\"][0]\n            if item_data.get(\"parent_substitutes\")\n            else None\n        )\n        if substitute_parent_item:\n            parent_zou_id = substitute_parent_item[\"parent_id\"]\n        else:\n            parent_zou_id = (\n                # For Asset, put under asset type directory\n                item.get(\"entity_type_id\")\n                if item_type == \"Asset\"\n                else None\n                # Else, fallback on usual hierarchy\n                or item.get(\"parent_id\")\n                or item.get(\"episode_id\")\n                or item.get(\"source_id\")\n            )\n\n        # Substitute item type for general classification (assets or shots)\n        if item_type in [\"Asset\", \"AssetType\"]:\n            entity_root_asset_name = \"Assets\"\n        elif item_type in [\"Episode\", \"Sequence\", \"Shot\"]:\n            entity_root_asset_name = \"Shots\"\n\n        # Root parent folder if exist\n        visual_parent_doc_id = None\n        if parent_zou_id is not None:\n            parent_zou_id_dict = asset_doc_ids.get(parent_zou_id)\n            if parent_zou_id_dict is not None:\n                visual_parent_doc_id = (\n                    parent_zou_id_dict.get(\"_id\")\n                    if parent_zou_id_dict\n                    else None\n                )\n\n        if visual_parent_doc_id is None:\n            # Find root folder doc (\"Assets\" or \"Shots\")\n            root_folder_doc = get_asset_by_name(\n                project_name,\n                asset_name=entity_root_asset_name,\n                fields=[\"_id\", \"data.root_of\"],\n            )\n\n            if root_folder_doc:\n                visual_parent_doc_id = root_folder_doc[\"_id\"]\n\n        # Visual parent for hierarchy\n        item_data[\"visualParent\"] = visual_parent_doc_id\n\n        # Add parents for hierarchy\n        item_data[\"parents\"] = []\n        ancestor_id = parent_zou_id\n        while ancestor_id is not None:\n            parent_doc = asset_doc_ids.get(ancestor_id)\n            if parent_doc is not None:\n                item_data[\"parents\"].insert(0, parent_doc[\"name\"])\n\n                # Get parent entity\n                parent_entity = parent_doc[\"data\"][\"zou\"]\n                ancestor_id = parent_entity.get(\"parent_id\")\n            else:\n                ancestor_id = None\n\n        # Build OpenPype compatible name\n        if item_type in [\"Shot\", \"Sequence\"] and parent_zou_id is not None:\n            # Name with parents hierarchy \"({episode}_){sequence}_{shot}\"\n            # to avoid duplicate name issue\n            item_name = f\"{item_data['parents'][-1]}_{item['name']}\"\n\n            # Update doc name\n            asset_doc_ids[item[\"id\"]][\"name\"] = item_name\n        else:\n            item_name = item[\"name\"]\n\n        # Set root folders parents\n        item_data[\"parents\"] = [entity_root_asset_name] + item_data[\"parents\"]\n\n        # Update 'data' different in zou DB\n        updated_data = {\n            k: v for k, v in item_data.items() if item_doc[\"data\"].get(k) != v\n        }\n        if updated_data or not item_doc.get(\"parent\"):\n            assets_with_update.append(\n                (\n                    item_doc[\"_id\"],\n                    {\n                        \"$set\": {\n                            \"name\": item_name,\n                            \"data\": item_data,\n                            \"parent\": project_doc[\"_id\"],\n                        }\n                    },\n                )\n            )\n    return assets_with_update\n\n\ndef write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne:\n    \"\"\"Write gazu project to OP database.\n    Create project if doesn't exist.\n\n    Args:\n        project (dict): Gazu project\n        dbcon (AvalonMongoDB): DB to create project in\n\n    Returns:\n        UpdateOne: Update instance for the project\n    \"\"\"\n    project_name = project[\"name\"]\n    project_dict = get_project(project_name)\n    if not project_dict:\n        project_dict = create_project(project_name, project_name)\n\n    # Project data and tasks\n    project_data = project_dict[\"data\"] or {}\n\n    # Build project code and update Kitsu\n    project_code = project.get(\"code\")\n    if not project_code:\n        project_code = project[\"name\"].replace(\" \", \"_\").lower()\n        project[\"code\"] = project_code\n\n        # Update Zou\n        gazu.project.update_project(project)\n\n    # Update data\n    project_data.update(\n        {\n            \"code\": project_code,\n            \"fps\": float(project[\"fps\"]),\n            \"zou_id\": project[\"id\"],\n            \"active\": project[\"project_status_name\"] != \"Closed\",\n        }\n    )\n\n    match_res = re.match(r\"(\\d+)x(\\d+)\", project[\"resolution\"])\n    if match_res:\n        project_data[\"resolutionWidth\"] = int(match_res.group(1))\n        project_data[\"resolutionHeight\"] = int(match_res.group(2))\n    else:\n        log.warning(\n            f\"'{project['resolution']}' does not match the expected\"\n            \" format for the resolution, for example: 1920x1080\"\n        )\n\n    return UpdateOne(\n        {\"_id\": project_dict[\"_id\"]},\n        {\n            \"$set\": {\n                \"config.tasks\": {\n                    t[\"name\"]: {\"short_name\": t.get(\"short_name\", t[\"name\"])}\n                    for t in gazu.task.all_task_types_for_project(project)\n                    or gazu.task.all_task_types()\n                },\n                \"data\": project_data,\n            }\n        },\n    )\n\n\ndef sync_all_projects(\n    login: str,\n    password: str,\n    ignore_projects: list = None,\n    filter_projects: tuple = None,\n):\n    \"\"\"Update all OP projects in DB with Zou data.\n\n    Args:\n        login (str): Kitsu user login\n        password (str): Kitsu user password\n        ignore_projects (list): List of unsynced project names\n        filter_projects (tuple): Tuple of filter project names to sync with\n    Raises:\n        gazu.exception.AuthFailedException: Wrong user login and/or password\n    \"\"\"\n\n    # Authenticate\n    if not validate_credentials(login, password):\n        raise gazu.exception.AuthFailedException(\n            f\"Kitsu authentication failed for login: '{login}'...\"\n        )\n\n    # Iterate projects\n    dbcon = AvalonMongoDB()\n    dbcon.install()\n    all_projects = gazu.project.all_projects()\n\n    project_to_sync = []\n\n    if filter_projects:\n        all_kitsu_projects = {p[\"name\"]: p for p in all_projects}\n        for proj_name in filter_projects:\n            if proj_name in all_kitsu_projects:\n                project_to_sync.append(all_kitsu_projects[proj_name])\n            else:\n                log.info(\n                    f\"`{proj_name}` project does not exist in Kitsu.\"\n                    f\" Please make sure the project is spelled correctly.\"\n                )\n    else:\n        # all project\n        project_to_sync = all_projects\n\n    for project in project_to_sync:\n        if ignore_projects and project[\"name\"] in ignore_projects:\n            continue\n        sync_project_from_kitsu(dbcon, project)\n\n\ndef sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict):\n    \"\"\"Update OP project in DB with Zou data.\n\n    `root_of` is meant to sort entities by type for a better readability in\n    the data tree. It puts all shot like (Shot and Episode and Sequence) and\n    asset entities under two different root folders or hierarchy, defined in\n    settings.\n\n    Args:\n        dbcon (AvalonMongoDB): MongoDB connection\n        project (dict): Project dict got using gazu.\n    \"\"\"\n    bulk_writes = []\n\n    # Get project from zou\n    if not project:\n        project = gazu.project.get_project_by_name(project[\"name\"])\n\n    # Get all statuses for projects from Kitsu\n    all_status = gazu.project.all_project_status()\n    for status in all_status:\n        if project[\"project_status_id\"] == status[\"id\"]:\n            project[\"project_status_name\"] = status[\"name\"]\n            break\n\n    #  Do not sync closed kitsu project that is not found in openpype\n    if project[\"project_status_name\"] == \"Closed\" and not get_project(\n        project[\"name\"]\n    ):\n        return\n\n    log.info(f\"Synchronizing {project['name']}...\")\n\n    # Get all assets from zou\n    all_assets = gazu.asset.all_assets_for_project(project)\n    all_asset_types = gazu.asset.all_asset_types_for_project(project)\n    all_episodes = gazu.shot.all_episodes_for_project(project)\n    all_seqs = gazu.shot.all_sequences_for_project(project)\n    all_shots = gazu.shot.all_shots_for_project(project)\n    all_entities = [\n        item\n        for item in all_assets\n        + all_asset_types\n        + all_episodes\n        + all_seqs\n        + all_shots\n        if naming_pattern.match(item[\"name\"])\n    ]\n\n    # Sync project. Create if doesn't exist\n    project_name = project[\"name\"]\n    project_dict = get_project(project_name)\n    if not project_dict:\n        log.info(\"Project created: {}\".format(project_name))\n    bulk_writes.append(write_project_to_op(project, dbcon))\n\n    if project[\"project_status_name\"] == \"Closed\":\n        return\n\n    # Try to find project document\n    if not project_dict:\n        project_dict = get_project(project_name)\n    dbcon.Session[\"AVALON_PROJECT\"] = project_name\n\n    # Query all assets of the local project\n    zou_ids_and_asset_docs = {\n        asset_doc[\"data\"][\"zou\"][\"id\"]: asset_doc\n        for asset_doc in get_assets(project_name)\n        if asset_doc[\"data\"].get(\"zou\", {}).get(\"id\")\n    }\n    zou_ids_and_asset_docs[project[\"id\"]] = project_dict\n\n    # Create entities root folders\n    to_insert = [\n        {\n            \"name\": r,\n            \"type\": \"asset\",\n            \"schema\": \"openpype:asset-3.0\",\n            \"data\": {\n                \"root_of\": r,\n                \"tasks\": {},\n                \"visualParent\": None,\n                \"parents\": [],\n            },\n        }\n        for r in [\"Assets\", \"Shots\"]\n        if not get_asset_by_name(\n            project_name, r, fields=[\"_id\", \"data.root_of\"]\n        )\n    ]\n\n    # Create\n    to_insert.extend(\n        [\n            create_op_asset(item)\n            for item in all_entities\n            if item[\"id\"] not in zou_ids_and_asset_docs.keys()\n        ]\n    )\n    if to_insert:\n        # Insert doc in DB\n        dbcon.insert_many(to_insert)\n\n        # Update existing docs\n        zou_ids_and_asset_docs.update(\n            {\n                asset_doc[\"data\"][\"zou\"][\"id\"]: asset_doc\n                for asset_doc in get_assets(project_name)\n                if asset_doc[\"data\"].get(\"zou\")\n            }\n        )\n\n    # Update\n    bulk_writes.extend(\n        [\n            UpdateOne({\"_id\": id}, update)\n            for id, update in update_op_assets(\n                dbcon,\n                project,\n                project_dict,\n                all_entities,\n                zou_ids_and_asset_docs,\n            )\n        ]\n    )\n\n    # Delete\n    diff_assets = set(zou_ids_and_asset_docs.keys()) - {\n        e[\"id\"] for e in all_entities + [project]\n    }\n    if diff_assets:\n        bulk_writes.extend(\n            [\n                DeleteOne(zou_ids_and_asset_docs[asset_id])\n                for asset_id in diff_assets\n            ]\n        )\n\n    # Write into DB\n    if bulk_writes:\n        dbcon.bulk_write(bulk_writes)\n"
  },
  {
    "path": "openpype/modules/kitsu/utils/update_zou_with_op.py",
    "content": "\"\"\"Functions to update Kitsu DB (a.k.a Zou) using OpenPype Data.\"\"\"\n\nimport re\nfrom typing import List\n\nimport gazu\nfrom pymongo import UpdateOne\n\nfrom openpype.client import (\n    get_projects,\n    get_project,\n    get_assets,\n)\nfrom openpype.pipeline import AvalonMongoDB\nfrom openpype.settings import get_project_settings\nfrom openpype.modules.kitsu.utils.credentials import validate_credentials\n\n\ndef sync_zou(login: str, password: str):\n    \"\"\"Synchronize Zou database (Kitsu backend) with openpype database.\n    This is an utility function to help updating zou data with OP's, it may not\n    handle correctly all cases, a human intervention might\n    be required after all.\n    Will work better if OP DB has been previously synchronized from zou/kitsu.\n\n    Args:\n        login (str): Kitsu user login\n        password (str): Kitsu user password\n\n    Raises:\n        gazu.exception.AuthFailedException: Wrong user login and/or password\n    \"\"\"\n\n    # Authenticate\n    if not validate_credentials(login, password):\n        raise gazu.exception.AuthFailedException(\n            f\"Kitsu authentication failed for login: '{login}'...\"\n        )\n\n    # Iterate projects\n    dbcon = AvalonMongoDB()\n    dbcon.install()\n\n    op_projects = list(get_projects())\n    for project_doc in op_projects:\n        sync_zou_from_op_project(project_doc[\"name\"], dbcon, project_doc)\n\n\ndef sync_zou_from_op_project(\n    project_name: str, dbcon: AvalonMongoDB, project_doc: dict = None\n) -> List[UpdateOne]:\n    \"\"\"Update OP project in DB with Zou data.\n\n    Args:\n        project_name (str): Name of project to sync\n        dbcon (AvalonMongoDB): MongoDB connection\n        project_doc (str, optional): Project doc to sync\n    \"\"\"\n    # Get project doc if not provided\n    if not project_doc:\n        project_doc = get_project(project_name)\n\n    # Get all entities from zou\n    print(\"Synchronizing {}...\".format(project_name))\n    zou_project = gazu.project.get_project_by_name(project_name)\n\n    # Create project\n    if zou_project is None:\n        raise RuntimeError(\n            f\"Project '{project_name}' doesn't exist in Zou database, \"\n            \"please create it in Kitsu and add OpenPype user to it before \"\n            \"running synchronization.\"\n        )\n\n    # Update project settings and data\n    if project_doc[\"data\"]:\n        zou_project.update(\n            {\n                \"code\": project_doc[\"data\"][\"code\"],\n                \"fps\": project_doc[\"data\"][\"fps\"],\n                \"resolution\": f\"{project_doc['data']['resolutionWidth']}\"\n                f\"x{project_doc['data']['resolutionHeight']}\",\n            }\n        )\n        gazu.project.update_project_data(\n            zou_project, data=project_doc[\"data\"]\n        )\n    gazu.project.update_project(zou_project)\n\n    asset_types = gazu.asset.all_asset_types()\n    all_assets = gazu.asset.all_assets_for_project(zou_project)\n    all_episodes = gazu.shot.all_episodes_for_project(zou_project)\n    all_seqs = gazu.shot.all_sequences_for_project(zou_project)\n    all_shots = gazu.shot.all_shots_for_project(zou_project)\n    all_entities_ids = {\n        e[\"id\"] for e in all_episodes + all_seqs + all_shots + all_assets\n    }\n\n    # Query all assets of the local project\n    project_module_settings = get_project_settings(project_name)[\"kitsu\"]\n    dbcon.Session[\"AVALON_PROJECT\"] = project_name\n    asset_docs = {\n        asset_doc[\"_id\"]: asset_doc for asset_doc in get_assets(project_name)\n    }\n\n    # Create new assets\n    new_assets_docs = [\n        doc\n        for doc in asset_docs.values()\n        if doc[\"data\"].get(\"zou\", {}).get(\"id\") not in all_entities_ids\n    ]\n    naming_pattern = project_module_settings[\"entities_naming_pattern\"]\n    regex_ep = re.compile(\n        r\"(.*{}.*)|(.*{}.*)|(.*{}.*)\".format(\n            naming_pattern[\"shot\"].replace(\"#\", \"\"),\n            naming_pattern[\"sequence\"].replace(\"#\", \"\"),\n            naming_pattern[\"episode\"].replace(\"#\", \"\"),\n        ),\n        re.IGNORECASE,\n    )\n    bulk_writes = []\n    for doc in new_assets_docs:\n        visual_parent_id = doc[\"data\"][\"visualParent\"]\n        parent_substitutes = []\n\n        # Match asset type by it's name\n        match = regex_ep.match(doc[\"name\"])\n        if not match:  # Asset\n            new_entity = gazu.asset.new_asset(\n                zou_project, asset_types[0], doc[\"name\"]\n            )\n        # Match case in shot<sequence<episode order to support\n        # composed names like 'ep01_sq01_sh01'\n        elif match.group(1):  # Shot\n            # Match and check parent doc\n            parent_doc = asset_docs[visual_parent_id]\n            zou_parent_id = parent_doc[\"data\"][\"zou\"][\"id\"]\n            if parent_doc[\"data\"].get(\"zou\", {}).get(\"type\") != \"Sequence\":\n                # Substitute name\n                digits_padding = naming_pattern[\"sequence\"].count(\"#\")\n                episode_name = naming_pattern[\"episode\"].replace(\n                    \"#\" * digits_padding, \"1\".zfill(digits_padding)\n                )\n                sequence_name = naming_pattern[\"sequence\"].replace(\n                    \"#\" * digits_padding, \"1\".zfill(digits_padding)\n                )\n                substitute_sequence_name = f\"{episode_name}_{sequence_name}\"\n\n                # Warn\n                print(\n                    f\"Shot {doc['name']} must be parented to a Sequence \"\n                    \"in Kitsu. \"\n                    f\"Creating automatically one substitute sequence \"\n                    f\"called {substitute_sequence_name} in Kitsu...\"\n                )\n\n                # Create new sequence and set it as substitute\n                created_sequence = gazu.shot.new_sequence(\n                    zou_project,\n                    substitute_sequence_name,\n                    episode=zou_parent_id,\n                )\n                gazu.shot.update_sequence_data(\n                    created_sequence, {\"is_substitute\": True}\n                )\n                parent_substitutes.append(created_sequence)\n\n                # Update parent ID\n                zou_parent_id = created_sequence[\"id\"]\n\n            # Create shot\n            new_entity = gazu.shot.new_shot(\n                zou_project,\n                zou_parent_id,\n                doc[\"name\"],\n                frame_in=doc[\"data\"][\"frameStart\"],\n                frame_out=doc[\"data\"][\"frameEnd\"],\n                nb_frames=(\n                    doc[\"data\"][\"frameEnd\"] - doc[\"data\"][\"frameStart\"] + 1\n                ),\n            )\n\n        elif match.group(2):  # Sequence\n            parent_doc = asset_docs[visual_parent_id]\n            new_entity = gazu.shot.new_sequence(\n                zou_project,\n                doc[\"name\"],\n                episode=parent_doc[\"data\"][\"zou\"][\"id\"],\n            )\n\n        elif match.group(3):  # Episode\n            new_entity = gazu.shot.new_episode(zou_project, doc[\"name\"])\n\n        # Update doc with zou id\n        doc[\"data\"].update(\n            {\n                \"visualParent\": visual_parent_id,\n                \"zou\": new_entity,\n            }\n        )\n        bulk_writes.append(\n            UpdateOne(\n                {\"_id\": doc[\"_id\"]},\n                {\n                    \"$set\": {\n                        \"data.visualParent\": visual_parent_id,\n                        \"data.zou\": new_entity,\n                        \"data.parent_substitutes\": parent_substitutes,\n                    }\n                },\n            )\n        )\n\n    # Update assets\n    all_tasks_types = {t[\"name\"]: t for t in gazu.task.all_task_types()}\n    assets_docs_to_update = [\n        doc\n        for doc in asset_docs.values()\n        if doc[\"data\"].get(\"zou\", {}).get(\"id\") in all_entities_ids\n    ]\n    for doc in assets_docs_to_update:\n        zou_id = doc[\"data\"][\"zou\"][\"id\"]\n        if zou_id:\n            # Data\n            entity_data = {}\n            frame_in = doc[\"data\"].get(\"frameStart\")\n            frame_out = doc[\"data\"].get(\"frameEnd\")\n            if frame_in or frame_out:\n                entity_data.update(\n                    {\n                        \"data\": {\n                            \"frame_in\": frame_in,\n                            \"frame_out\": frame_out,\n                        },\n                        \"nb_frames\": frame_out - frame_in + 1,\n                    }\n                )\n            entity = gazu.raw.update(\"entities\", zou_id, entity_data)\n\n            # Tasks\n            all_tasks_func = getattr(\n                gazu.task, f\"all_tasks_for_{entity['type'].lower()}\"\n            )\n            entity_tasks = {t[\"name\"] for t in all_tasks_func(entity)}\n            for task_name in doc[\"data\"][\"tasks\"].keys():\n                # Create only if new\n                if task_name not in entity_tasks:\n                    task_type = all_tasks_types.get(task_name)\n\n                    # Create non existing task\n                    if not task_type:\n                        task_type = gazu.task.new_task_type(task_name)\n                        all_tasks_types[task_name] = task_type\n\n                    # New task for entity\n                    gazu.task.new_task(entity, task_type)\n\n    # Delete\n    deleted_entities = all_entities_ids - {\n        asset_doc[\"data\"].get(\"zou\", {}).get(\"id\")\n        for asset_doc in asset_docs.values()\n    }\n    for entity_id in deleted_entities:\n        gazu.raw.delete(\"data/entities/{}\".format(entity_id))\n\n    # Write into DB\n    if bulk_writes:\n        dbcon.bulk_write(bulk_writes)\n"
  },
  {
    "path": "openpype/modules/launcher_action.py",
    "content": "import os\n\nfrom openpype import PLUGINS_DIR, AYON_SERVER_ENABLED\nfrom openpype.modules import (\n    OpenPypeModule,\n    ITrayAction,\n)\n\n\nclass LauncherAction(OpenPypeModule, ITrayAction):\n    label = \"Launcher\"\n    name = \"launcher_tool\"\n\n    def initialize(self, _modules_settings):\n        # This module is always enabled\n        self.enabled = True\n\n        # Tray attributes\n        self._window = None\n\n    def tray_init(self):\n        self._create_window()\n\n        self.add_doubleclick_callback(self._show_launcher)\n\n    def tray_start(self):\n        return\n\n    def connect_with_modules(self, enabled_modules):\n        # Register actions\n        if not self.tray_initialized:\n            return\n\n        from openpype.pipeline.actions import register_launcher_action_path\n\n        actions_dir = os.path.join(PLUGINS_DIR, \"actions\")\n        if os.path.exists(actions_dir):\n            register_launcher_action_path(actions_dir)\n\n        actions_paths = self.manager.collect_plugin_paths()[\"actions\"]\n        for path in actions_paths:\n            if path and os.path.exists(path):\n                register_launcher_action_path(path)\n\n        paths_str = os.environ.get(\"AVALON_ACTIONS\") or \"\"\n        if paths_str:\n            self.log.warning(\n                \"WARNING: 'AVALON_ACTIONS' is deprecated. Support of this\"\n                \" environment variable will be removed in future versions.\"\n                \" Please consider using 'OpenPypeModule' to define custom\"\n                \" action paths. Planned version to drop the support\"\n                \" is 3.17.2 or 3.18.0 .\"\n            )\n\n        for path in paths_str.split(os.pathsep):\n            if path and os.path.exists(path):\n                register_launcher_action_path(path)\n\n    def on_action_trigger(self):\n        \"\"\"Implementation for ITrayAction interface.\n\n        Show launcher tool on action trigger.\n        \"\"\"\n\n        self._show_launcher()\n\n    def _create_window(self):\n        if self._window:\n            return\n        if AYON_SERVER_ENABLED:\n            from openpype.tools.ayon_launcher.ui import LauncherWindow\n        else:\n            from openpype.tools.launcher import LauncherWindow\n        self._window = LauncherWindow()\n\n    def _show_launcher(self):\n        if self._window is None:\n            return\n        self._window.show()\n        self._window.raise_()\n        self._window.activateWindow()\n"
  },
  {
    "path": "openpype/modules/log_viewer/__init__.py",
    "content": "from .log_view_module import LogViewModule\n\n\n__all__ = (\n    \"LogViewModule\",\n)\n"
  },
  {
    "path": "openpype/modules/log_viewer/log_view_module.py",
    "content": "from openpype import AYON_SERVER_ENABLED\nfrom openpype.modules import OpenPypeModule, ITrayModule\n\n\nclass LogViewModule(OpenPypeModule, ITrayModule):\n    name = \"log_viewer\"\n\n    def initialize(self, modules_settings):\n        logging_settings = modules_settings[self.name]\n        self.enabled = logging_settings[\"enabled\"]\n        if AYON_SERVER_ENABLED:\n            self.enabled = False\n\n        # Tray attributes\n        self.window = None\n\n    def tray_init(self):\n        try:\n            from .tray.app import LogsWindow\n            self.window = LogsWindow()\n        except Exception:\n            self.log.warning(\n                \"Couldn't set Logging GUI due to error.\", exc_info=True\n            )\n\n    # Definition of Tray menu\n    def tray_menu(self, tray_menu):\n        from qtpy import QtWidgets\n        # Menu for Tray App\n        menu = QtWidgets.QMenu('Logging', tray_menu)\n\n        show_action = QtWidgets.QAction(\"Show Logs\", menu)\n        show_action.triggered.connect(self._show_logs_gui)\n        menu.addAction(show_action)\n\n        tray_menu.addMenu(menu)\n\n    def tray_start(self):\n        return\n\n    def tray_exit(self):\n        return\n\n    def _show_logs_gui(self):\n        if self.window:\n            self.window.show()\n"
  },
  {
    "path": "openpype/modules/log_viewer/tray/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/modules/log_viewer/tray/app.py",
    "content": "from qtpy import QtWidgets, QtCore\nfrom .widgets import LogsWidget, OutputWidget\nfrom openpype import style\n\n\nclass LogsWindow(QtWidgets.QWidget):\n    def __init__(self, parent=None):\n        super(LogsWindow, self).__init__(parent)\n\n        self.setWindowTitle(\"Logs viewer\")\n\n        self.resize(1400, 800)\n        log_detail = OutputWidget(parent=self)\n        logs_widget = LogsWidget(log_detail, parent=self)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n\n        log_splitter = QtWidgets.QSplitter(self)\n        log_splitter.setOrientation(QtCore.Qt.Horizontal)\n        log_splitter.addWidget(logs_widget)\n        log_splitter.addWidget(log_detail)\n\n        main_layout.addWidget(log_splitter)\n\n        self.logs_widget = logs_widget\n        self.log_detail = log_detail\n\n        self.setStyleSheet(style.load_stylesheet())\n\n        self._first_show = True\n\n    def showEvent(self, event):\n        super(LogsWindow, self).showEvent(event)\n\n        if self._first_show:\n            self._first_show = False\n            self.logs_widget.refresh()\n"
  },
  {
    "path": "openpype/modules/log_viewer/tray/models.py",
    "content": "import collections\nfrom qtpy import QtCore, QtGui\nfrom openpype.lib import Logger\n\n\nclass LogModel(QtGui.QStandardItemModel):\n    COLUMNS = (\n        \"process_name\",\n        \"hostname\",\n        \"hostip\",\n        \"username\",\n        \"system_name\",\n        \"started\"\n    )\n    colums_mapping = {\n        \"process_name\": \"Process Name\",\n        \"process_id\": \"Process Id\",\n        \"hostname\": \"Hostname\",\n        \"hostip\": \"Host IP\",\n        \"username\": \"Username\",\n        \"system_name\": \"System name\",\n        \"started\": \"Started at\"\n    }\n    process_keys = (\n        \"process_id\", \"hostname\", \"hostip\",\n        \"username\", \"system_name\", \"process_name\"\n    )\n    log_keys = (\n        \"timestamp\", \"level\", \"thread\", \"threadName\", \"message\", \"loggerName\",\n        \"fileName\", \"module\", \"method\", \"lineNumber\"\n    )\n    default_value = \"- Not set -\"\n\n    ROLE_LOGS = QtCore.Qt.UserRole + 2\n    ROLE_PROCESS_ID = QtCore.Qt.UserRole + 3\n\n    def __init__(self, parent=None):\n        super(LogModel, self).__init__(parent)\n\n        self.log_by_process = None\n        self.dbcon = None\n\n        # Crash if connection is not possible to skip this module\n        if not Logger.initialized:\n            Logger.initialize()\n\n        connection = Logger.get_log_mongo_connection()\n        if connection:\n            Logger.bootstrap_mongo_log()\n            database = connection[Logger.log_database_name]\n            self.dbcon = database[Logger.log_collection_name]\n\n    def headerData(self, section, orientation, role):\n        if (\n            role == QtCore.Qt.DisplayRole\n            and orientation == QtCore.Qt.Horizontal\n        ):\n            if section < len(self.COLUMNS):\n                key = self.COLUMNS[section]\n                return self.colums_mapping.get(key, key)\n\n        super(LogModel, self).headerData(section, orientation, role)\n\n    def add_process_logs(self, process_logs):\n        items = []\n        first_item = True\n        for key in self.COLUMNS:\n            display_value = str(process_logs[key])\n            item = QtGui.QStandardItem(display_value)\n            if first_item:\n                first_item = False\n                item.setData(process_logs[\"_logs\"], self.ROLE_LOGS)\n                item.setData(process_logs[\"process_id\"], self.ROLE_PROCESS_ID)\n            items.append(item)\n        self.appendRow(items)\n\n    def refresh(self):\n        self.log_by_process = collections.defaultdict(list)\n        self.process_info = {}\n\n        self.clear()\n        self.beginResetModel()\n        if self.dbcon:\n            result = self.dbcon.find({})\n            for item in result:\n                process_id = item.get(\"process_id\")\n                # backwards (in)compatibility\n                if not process_id:\n                    continue\n\n                if process_id not in self.process_info:\n                    proc_dict = {\"_logs\": []}\n                    for key in self.process_keys:\n                        proc_dict[key] = (\n                            item.get(key) or self.default_value\n                        )\n                    self.process_info[process_id] = proc_dict\n\n                log_item = {}\n                for key in self.log_keys:\n                    log_item[key] = item.get(key) or self.default_value\n\n                if \"exception\" in item:\n                    log_item[\"exception\"] = item[\"exception\"]\n\n                self.process_info[process_id][\"_logs\"].append(log_item)\n\n        for item in self.process_info.values():\n            item[\"_logs\"] = sorted(\n                item[\"_logs\"], key=lambda item: item[\"timestamp\"]\n            )\n            item[\"started\"] = item[\"_logs\"][0][\"timestamp\"]\n            self.add_process_logs(item)\n\n        self.endResetModel()\n\n\nclass LogsFilterProxy(QtCore.QSortFilterProxyModel):\n    def __init__(self, *args, **kwargs):\n        super(LogsFilterProxy, self).__init__(*args, **kwargs)\n        self.col_usernames = None\n        self.filter_usernames = set()\n\n    def update_users_filter(self, users):\n        self.filter_usernames = set()\n        for user in users or tuple():\n            self.filter_usernames.add(user)\n        self.invalidateFilter()\n\n    def filterAcceptsRow(self, source_row, source_parent):\n        if self.col_usernames is not None:\n            index = self.sourceModel().index(\n                source_row, self.col_usernames, source_parent\n            )\n            user = index.data(QtCore.Qt.DisplayRole)\n            if user not in self.filter_usernames:\n                return False\n        return True\n"
  },
  {
    "path": "openpype/modules/log_viewer/tray/widgets.py",
    "content": "import html\nfrom qtpy import QtCore, QtWidgets\nimport qtawesome\nfrom .models import LogModel, LogsFilterProxy\n\n\nclass SearchComboBox(QtWidgets.QComboBox):\n    \"\"\"Searchable ComboBox with empty placeholder value as first value\"\"\"\n\n    def __init__(self, parent=None, placeholder=\"\"):\n        super(SearchComboBox, self).__init__(parent)\n\n        self.setEditable(True)\n        self.setInsertPolicy(QtWidgets.QComboBox.NoInsert)\n        self.lineEdit().setPlaceholderText(placeholder)\n\n        # Apply completer settings\n        completer = self.completer()\n        completer.setCompletionMode(completer.PopupCompletion)\n        completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        # Force style sheet on popup menu\n        # It won't take the parent stylesheet for some reason\n        # todo: better fix for completer popup stylesheet\n        if parent:\n            popup = completer.popup()\n            popup.setStyleSheet(parent.styleSheet())\n\n        self.currentIndexChanged.connect(self.onIndexChange)\n\n    def onIndexChange(self, index):\n        print(index)\n\n    def populate(self, items):\n        self.clear()\n        self.addItems([\"\"])     # ensure first item is placeholder\n        self.addItems(items)\n\n    def get_valid_value(self):\n        \"\"\"Return the current text if it's a valid value else None\n\n        Note: The empty placeholder value is valid and returns as \"\"\n\n        \"\"\"\n\n        text = self.currentText()\n        lookup = set(self.itemText(i) for i in range(self.count()))\n        if text not in lookup:\n            return None\n\n        return text\n\n\nclass SelectableMenu(QtWidgets.QMenu):\n\n    selection_changed = QtCore.Signal()\n\n    def mouseReleaseEvent(self, event):\n        action = self.activeAction()\n        if action and action.isEnabled():\n            action.trigger()\n            self.selection_changed.emit()\n        else:\n            super(SelectableMenu, self).mouseReleaseEvent(event)\n\n\nclass CustomCombo(QtWidgets.QWidget):\n\n    selection_changed = QtCore.Signal()\n\n    def __init__(self, title, parent=None):\n        super(CustomCombo, self).__init__(parent)\n        toolbutton = QtWidgets.QToolButton(self)\n        toolbutton.setText(title)\n\n        toolmenu = SelectableMenu(self)\n\n        toolbutton.setMenu(toolmenu)\n        toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(toolbutton)\n\n        toolmenu.selection_changed.connect(self.selection_changed)\n\n        self.toolbutton = toolbutton\n        self.toolmenu = toolmenu\n        self.main_layout = layout\n\n    def populate(self, items):\n        self.toolmenu.clear()\n        self.addItems(items)\n\n    def addItems(self, items):\n        for item in items:\n            action = self.toolmenu.addAction(item)\n            action.setCheckable(True)\n            action.setChecked(True)\n            self.toolmenu.addAction(action)\n\n    def items(self):\n        for action in self.toolmenu.actions():\n            yield action\n\n\nclass LogsWidget(QtWidgets.QWidget):\n    \"\"\"A widget that lists the published subsets for an asset\"\"\"\n\n    def __init__(self, detail_widget, parent=None):\n        super(LogsWidget, self).__init__(parent=parent)\n\n        model = LogModel()\n        proxy_model = LogsFilterProxy()\n        proxy_model.setSourceModel(model)\n        proxy_model.col_usernames = model.COLUMNS.index(\"username\")\n\n        filter_layout = QtWidgets.QHBoxLayout()\n\n        user_filter = CustomCombo(\"Users\", self)\n        users = model.dbcon.distinct(\"username\")\n        user_filter.populate(users)\n        user_filter.selection_changed.connect(self._user_changed)\n\n        proxy_model.update_users_filter(users)\n\n        level_filter = CustomCombo(\"Levels\", self)\n        levels = model.dbcon.distinct(\"level\")\n        level_filter.addItems(levels)\n        level_filter.selection_changed.connect(self._level_changed)\n\n        detail_widget.update_level_filter(levels)\n\n        icon = qtawesome.icon(\"fa.refresh\", color=\"white\")\n        refresh_btn = QtWidgets.QPushButton(icon, \"\")\n\n        filter_layout.addWidget(user_filter)\n        filter_layout.addWidget(level_filter)\n        filter_layout.addStretch(1)\n        filter_layout.addWidget(refresh_btn)\n\n        view = QtWidgets.QTreeView(self)\n        view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addLayout(filter_layout)\n        layout.addWidget(view)\n\n        view.setModel(proxy_model)\n\n        view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        view.setSortingEnabled(True)\n        view.sortByColumn(\n            model.COLUMNS.index(\"started\"),\n            QtCore.Qt.DescendingOrder\n        )\n\n        refresh_triggered_timer = QtCore.QTimer()\n        refresh_triggered_timer.setSingleShot(True)\n        refresh_triggered_timer.setInterval(200)\n\n        refresh_triggered_timer.timeout.connect(self._on_refresh_timeout)\n        view.selectionModel().selectionChanged.connect(self._on_index_change)\n        refresh_btn.clicked.connect(self._on_refresh_clicked)\n\n        # Store to memory\n        self.model = model\n        self.proxy_model = proxy_model\n        self.view = view\n\n        self.user_filter = user_filter\n        self.level_filter = level_filter\n\n        self.detail_widget = detail_widget\n        self.refresh_btn = refresh_btn\n\n        self._refresh_triggered_timer = refresh_triggered_timer\n\n    def refresh(self):\n        self._refresh_triggered_timer.start()\n\n    def _on_refresh_timeout(self):\n        self.model.refresh()\n        self.detail_widget.refresh()\n\n    def _on_refresh_clicked(self):\n        self.refresh()\n\n    def _on_index_change(self, to_index, from_index):\n        index = self._selected_log()\n        if index:\n            logs = index.data(self.model.ROLE_LOGS)\n        else:\n            logs = []\n        self.detail_widget.set_detail(logs)\n\n    def _user_changed(self):\n        checked_values = set()\n        for action in self.user_filter.items():\n            if action.isChecked():\n                checked_values.add(action.text())\n        self.proxy_model.update_users_filter(checked_values)\n\n    def _level_changed(self):\n        checked_values = set()\n        for action in self.level_filter.items():\n            if action.isChecked():\n                checked_values.add(action.text())\n        self.detail_widget.update_level_filter(checked_values)\n\n    def on_context_menu(self, point):\n        # TODO will be any actions? it's ready\n        return\n\n        point_index = self.view.indexAt(point)\n        if not point_index.isValid():\n            return\n\n        # Get selected subsets without groups\n        selection = self.view.selectionModel()\n        rows = selection.selectedRows(column=0)\n\n    def _selected_log(self):\n        selection = self.view.selectionModel()\n        rows = selection.selectedRows(column=0)\n        if len(rows) == 1:\n            return rows[0]\n        return None\n\n\nclass OutputWidget(QtWidgets.QWidget):\n    def __init__(self, parent=None):\n        super(OutputWidget, self).__init__(parent=parent)\n        layout = QtWidgets.QVBoxLayout(self)\n\n        show_timecode_checkbox = QtWidgets.QCheckBox(\"Show timestamp\", self)\n\n        output_text = QtWidgets.QTextEdit(self)\n        output_text.setReadOnly(True)\n        # output_text.setLineWrapMode(QtWidgets.QTextEdit.FixedPixelWidth)\n\n        layout.addWidget(show_timecode_checkbox)\n        layout.addWidget(output_text)\n\n        show_timecode_checkbox.stateChanged.connect(\n            self.on_show_timecode_change\n        )\n        self.setLayout(layout)\n        self.output_text = output_text\n        self.show_timecode_checkbox = show_timecode_checkbox\n\n        self.refresh()\n\n    def refresh(self):\n        self.set_detail()\n\n    def show_timecode(self):\n        return self.show_timecode_checkbox.isChecked()\n\n    def on_show_timecode_change(self):\n        self.set_detail(self.las_logs)\n\n    def update_level_filter(self, levels):\n        self.filter_levels = set()\n        for level in levels or tuple():\n            self.filter_levels.add(level.lower())\n\n        self.set_detail(self.las_logs)\n\n    def add_line(self, line):\n        self.output_text.append(line)\n\n    def set_detail(self, logs=None):\n        self.las_logs = logs\n        self.output_text.clear()\n        if not logs:\n            return\n\n        show_timecode = self.show_timecode()\n        for log in logs:\n            level = log[\"level\"].lower()\n            if level not in self.filter_levels:\n                continue\n\n            line_f = \"<font color=\\\"White\\\">{message}\"\n            if level == \"debug\":\n                line_f = (\n                    \"<font color=\\\"Yellow\\\"> -\"\n                    \" <font color=\\\"Lime\\\">{{  {logger_name}  }}: [\"\n                    \" <font color=\\\"White\\\">{message}\"\n                    \" <font color=\\\"Lime\\\">]\"\n                )\n            elif level == \"info\":\n                line_f = (\n                    \"<font color=\\\"Lime\\\">>>> [\"\n                    \" <font color=\\\"White\\\">{message}\"\n                    \" <font color=\\\"Lime\\\">]\"\n                )\n            elif level == \"warning\":\n                line_f = (\n                    \"<font color=\\\"Yellow\\\">*** WRN:\"\n                    \" <font color=\\\"Lime\\\"> >>> {{ {logger_name} }}: [\"\n                    \" <font color=\\\"White\\\">{message}\"\n                    \" <font color=\\\"Lime\\\">]\"\n                )\n            elif level == \"error\":\n                line_f = (\n                    \"<font color=\\\"Red\\\">!!! ERR:\"\n                    \" <font color=\\\"White\\\">{timestamp}\"\n                    \" <font color=\\\"Lime\\\">>>> {{ {logger_name} }}: [\"\n                    \" <font color=\\\"White\\\">{message}\"\n                    \" <font color=\\\"Lime\\\">]\"\n                )\n\n            logger_name = log[\"loggerName\"]\n            timestamp = \"\"\n            if not show_timecode:\n                timestamp = log[\"timestamp\"]\n            message = log[\"message\"]\n            exc = log.get(\"exception\")\n            if exc:\n                message = exc[\"message\"]\n\n            line = line_f.format(\n                message=html.escape(message),\n                logger_name=logger_name,\n                timestamp=timestamp\n            )\n\n            if show_timecode:\n                timestamp = log[\"timestamp\"]\n                line = timestamp.strftime(\"%Y-%d-%m %H:%M:%S\") + \" \" + line\n\n            self.add_line(line)\n\n            if not exc:\n                continue\n            for _line in exc[\"stackTrace\"].split(\"\\n\"):\n                self.add_line(_line)\n"
  },
  {
    "path": "openpype/modules/project_manager_action.py",
    "content": "from openpype import AYON_SERVER_ENABLED\nfrom openpype.modules import OpenPypeModule, ITrayAction\n\n\nclass ProjectManagerAction(OpenPypeModule, ITrayAction):\n    label = \"Project Manager (beta)\"\n    name = \"project_manager\"\n    admin_action = True\n\n    def initialize(self, modules_settings):\n        enabled = False\n        module_settings = modules_settings.get(self.name)\n        if module_settings:\n            enabled = module_settings.get(\"enabled\", enabled)\n\n        if AYON_SERVER_ENABLED:\n            enabled = False\n        self.enabled = enabled\n\n        # Tray attributes\n        self.project_manager_window = None\n\n    def tray_init(self):\n        \"\"\"Initialization in tray implementation of ITrayAction.\"\"\"\n        self.create_project_manager_window()\n\n    def on_action_trigger(self):\n        \"\"\"Implementation for action trigger of ITrayAction.\"\"\"\n        self.show_project_manager_window()\n\n    def create_project_manager_window(self):\n        \"\"\"Initializa Settings Qt window.\"\"\"\n        if self.project_manager_window:\n            return\n        from openpype.tools.project_manager import ProjectManagerWindow\n\n        self.project_manager_window = ProjectManagerWindow()\n\n    def show_project_manager_window(self):\n        \"\"\"Show project manager tool window.\n\n        Raises:\n            AssertionError: Window must be already created. Call\n                `create_project_manager_window` before calling this method.\n        \"\"\"\n        if not self.project_manager_window:\n            raise AssertionError(\"Window is not initialized.\")\n\n        # Store if was visible\n        was_minimized = self.project_manager_window.isMinimized()\n\n        # Show settings gui\n        self.project_manager_window.show()\n\n        if was_minimized:\n            self.project_manager_window.showNormal()\n\n        # Pull window to the front.\n        self.project_manager_window.raise_()\n        self.project_manager_window.activateWindow()\n"
  },
  {
    "path": "openpype/modules/python_console_interpreter/__init__.py",
    "content": "from .module import (\n    PythonInterpreterAction\n)\n\n\n__all__ = (\n    \"PythonInterpreterAction\",\n)\n"
  },
  {
    "path": "openpype/modules/python_console_interpreter/module.py",
    "content": "from openpype.modules import OpenPypeModule, ITrayAction\n\n\nclass PythonInterpreterAction(OpenPypeModule, ITrayAction):\n    label = \"Console\"\n    name = \"python_interpreter\"\n    admin_action = True\n\n    def initialize(self, modules_settings):\n        self.enabled = True\n        self._interpreter_window = None\n\n    def tray_init(self):\n        self.create_interpreter_window()\n\n    def tray_exit(self):\n        if self._interpreter_window is not None:\n            self._interpreter_window.save_registry()\n\n    def create_interpreter_window(self):\n        \"\"\"Initializa Settings Qt window.\"\"\"\n        if self._interpreter_window:\n            return\n\n        from openpype_modules.python_console_interpreter.window import (\n            PythonInterpreterWidget\n        )\n\n        self._interpreter_window = PythonInterpreterWidget()\n\n    def on_action_trigger(self):\n        self.show_interpreter_window()\n\n    def show_interpreter_window(self):\n        self.create_interpreter_window()\n\n        if self._interpreter_window.isVisible():\n            self._interpreter_window.activateWindow()\n            self._interpreter_window.raise_()\n            return\n\n        self._interpreter_window.show()\n"
  },
  {
    "path": "openpype/modules/python_console_interpreter/window/__init__.py",
    "content": "from .widgets import (\n    PythonInterpreterWidget\n)\n\n\n__all__ = (\n    \"PythonInterpreterWidget\",\n)\n"
  },
  {
    "path": "openpype/modules/python_console_interpreter/window/widgets.py",
    "content": "import os\nimport re\nimport sys\nimport collections\nfrom code import InteractiveInterpreter\n\nimport appdirs\nfrom qtpy import QtCore, QtWidgets, QtGui\n\nfrom openpype import resources\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.style import load_stylesheet\nfrom openpype.lib import JSONSettingRegistry\n\n\n\nopenpype_art = \"\"\"\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~.   ..   ~2p.  ..  ....  .  .\n    .Ppo . .pPO3Op.. . O:. . . .\n   .3Pp . oP3'. 'P33. . 4 ..   .  .   . .. .  .  .\n  .~OP    3PO.  .Op3    : . ..  _____  _____  _____\n  .P3O  . oP3oP3O3P' . . .   . /    /./    /./    /\n   O3:.   O3p~ .       .:. . ./____/./____/ /____/\n   'P .   3p3.  oP3~. ..P:. .  . ..  .   . .. .  .  .\n  . ':  . Po'  .Opo'. .3O. .  o[ by Pype Club ]]]==- - - .  .\n    . '_ ..  .    . _OP3..  .  .https://openpype.io.. .\n         ~P3.OPPPO3OP~ . ..  .\n           .  ' '. .  .. . . . ..  .\n\n\"\"\"\n\nayon_art = r\"\"\"\n\n                    ▄██▄\n         ▄███▄ ▀██▄ ▀██▀ ▄██▀ ▄██▀▀▀██▄    ▀███▄      █▄\n        ▄▄ ▀██▄  ▀██▄  ▄██▀ ██▀      ▀██▄  ▄  ▀██▄    ███\n       ▄██▀  ██▄   ▀ ▄▄ ▀  ██         ▄██  ███  ▀██▄  ███\n      ▄██▀    ▀██▄   ██    ▀██▄      ▄██▀  ███    ▀██ ▀█▀\n     ▄██▀      ▀██▄  ▀█      ▀██▄▄▄▄██▀    █▀      ▀██▄\n\n     ·  · - =[ by YNPUT ]:[ http://ayon.ynput.io ]= - ·  ·\n\n\"\"\"\n\n\nclass PythonInterpreterRegistry(JSONSettingRegistry):\n    \"\"\"Class handling OpenPype general settings registry.\n\n    Attributes:\n        vendor (str): Name used for path construction.\n        product (str): Additional name used for path construction.\n\n    \"\"\"\n\n    def __init__(self):\n        if AYON_SERVER_ENABLED:\n            self.vendor = \"ynput\"\n            self.product = \"ayon\"\n        else:\n            self.vendor = \"pypeclub\"\n            self.product = \"openpype\"\n        name = \"python_interpreter_tool\"\n        path = appdirs.user_data_dir(self.product, self.vendor)\n        super(PythonInterpreterRegistry, self).__init__(name, path)\n\n\nclass StdOEWrap:\n    def __init__(self):\n        self._origin_stdout_write = None\n        self._origin_stderr_write = None\n        self._listening = False\n        self.lines = collections.deque()\n\n        if not sys.stdout:\n            sys.stdout = open(os.devnull, \"w\")\n\n        if not sys.stderr:\n            sys.stderr = open(os.devnull, \"w\")\n\n        if self._origin_stdout_write is None:\n            self._origin_stdout_write = sys.stdout.write\n\n        if self._origin_stderr_write is None:\n            self._origin_stderr_write = sys.stderr.write\n\n        self._listening = True\n        sys.stdout.write = self._stdout_listener\n        sys.stderr.write = self._stderr_listener\n\n    def stop_listen(self):\n        self._listening = False\n\n    def _stdout_listener(self, text):\n        if self._listening:\n            self.lines.append(text)\n        if self._origin_stdout_write is not None:\n            self._origin_stdout_write(text)\n\n    def _stderr_listener(self, text):\n        if self._listening:\n            self.lines.append(text)\n        if self._origin_stderr_write is not None:\n            self._origin_stderr_write(text)\n\n\nclass PythonCodeEditor(QtWidgets.QPlainTextEdit):\n    execute_requested = QtCore.Signal()\n\n    def __init__(self, parent):\n        super(PythonCodeEditor, self).__init__(parent)\n\n        self.setObjectName(\"PythonCodeEditor\")\n\n        self._indent = 4\n\n    def _tab_shift_right(self):\n        cursor = self.textCursor()\n        selected_text = cursor.selectedText()\n        if not selected_text:\n            cursor.insertText(\" \" * self._indent)\n            return\n\n        sel_start = cursor.selectionStart()\n        sel_end = cursor.selectionEnd()\n        cursor.setPosition(sel_end)\n        end_line = cursor.blockNumber()\n        cursor.setPosition(sel_start)\n        while True:\n            cursor.movePosition(QtGui.QTextCursor.StartOfLine)\n            text = cursor.block().text()\n            spaces = len(text) - len(text.lstrip(\" \"))\n            new_spaces = spaces % self._indent\n            if not new_spaces:\n                new_spaces = self._indent\n\n            cursor.insertText(\" \" * new_spaces)\n            if cursor.blockNumber() == end_line:\n                break\n\n            cursor.movePosition(QtGui.QTextCursor.NextBlock)\n\n    def _tab_shift_left(self):\n        tmp_cursor = self.textCursor()\n        sel_start = tmp_cursor.selectionStart()\n        sel_end = tmp_cursor.selectionEnd()\n\n        cursor = QtGui.QTextCursor(self.document())\n        cursor.setPosition(sel_end)\n        end_line = cursor.blockNumber()\n        cursor.setPosition(sel_start)\n        while True:\n            cursor.movePosition(QtGui.QTextCursor.StartOfLine)\n            text = cursor.block().text()\n            spaces = len(text) - len(text.lstrip(\" \"))\n            if spaces:\n                spaces_to_remove = (spaces % self._indent) or self._indent\n                if spaces_to_remove > spaces:\n                    spaces_to_remove = spaces\n\n                cursor.setPosition(\n                    cursor.position() + spaces_to_remove,\n                    QtGui.QTextCursor.KeepAnchor\n                )\n                cursor.removeSelectedText()\n\n            if cursor.blockNumber() == end_line:\n                break\n\n            cursor.movePosition(QtGui.QTextCursor.NextBlock)\n\n    def keyPressEvent(self, event):\n        if event.key() == QtCore.Qt.Key_Backtab:\n            self._tab_shift_left()\n            event.accept()\n            return\n\n        if event.key() == QtCore.Qt.Key_Tab:\n            if event.modifiers() == QtCore.Qt.NoModifier:\n                self._tab_shift_right()\n            event.accept()\n            return\n\n        if (\n            event.key() == QtCore.Qt.Key_Return\n            and event.modifiers() == QtCore.Qt.ControlModifier\n        ):\n            self.execute_requested.emit()\n            event.accept()\n            return\n\n        super(PythonCodeEditor, self).keyPressEvent(event)\n\n\nclass PythonTabWidget(QtWidgets.QWidget):\n    add_tab_requested = QtCore.Signal()\n    before_execute = QtCore.Signal(str)\n\n    def __init__(self, parent):\n        super(PythonTabWidget, self).__init__(parent)\n\n        code_input = PythonCodeEditor(self)\n\n        self.setFocusProxy(code_input)\n\n        add_tab_btn = QtWidgets.QPushButton(\"Add tab...\", self)\n        add_tab_btn.setToolTip(\"Add new tab\")\n\n        execute_btn = QtWidgets.QPushButton(\"Execute\", self)\n        execute_btn.setToolTip(\"Execute command (Ctrl + Enter)\")\n\n        btns_layout = QtWidgets.QHBoxLayout()\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n        btns_layout.addWidget(add_tab_btn)\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(execute_btn)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(code_input, 1)\n        layout.addLayout(btns_layout, 0)\n\n        add_tab_btn.clicked.connect(self._on_add_tab_clicked)\n        execute_btn.clicked.connect(self._on_execute_clicked)\n        code_input.execute_requested.connect(self.execute)\n\n        self._code_input = code_input\n        self._interpreter = InteractiveInterpreter()\n\n    def _on_add_tab_clicked(self):\n        self.add_tab_requested.emit()\n\n    def _on_execute_clicked(self):\n        self.execute()\n\n    def get_code(self):\n        return self._code_input.toPlainText()\n\n    def set_code(self, code_text):\n        self._code_input.setPlainText(code_text)\n\n    def execute(self):\n        code_text = self._code_input.toPlainText()\n        self.before_execute.emit(code_text)\n        self._interpreter.runcode(code_text)\n\n\nclass TabNameDialog(QtWidgets.QDialog):\n    default_width = 330\n    default_height = 85\n\n    def __init__(self, parent):\n        super(TabNameDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"Enter tab name\")\n\n        name_label = QtWidgets.QLabel(\"Tab name:\", self)\n        name_input = QtWidgets.QLineEdit(self)\n\n        inputs_layout = QtWidgets.QHBoxLayout()\n        inputs_layout.addWidget(name_label)\n        inputs_layout.addWidget(name_input)\n\n        ok_btn = QtWidgets.QPushButton(\"Ok\", self)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", self)\n        btns_layout = QtWidgets.QHBoxLayout()\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(ok_btn)\n        btns_layout.addWidget(cancel_btn)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addLayout(inputs_layout)\n        layout.addStretch(1)\n        layout.addLayout(btns_layout)\n\n        ok_btn.clicked.connect(self._on_ok_clicked)\n        cancel_btn.clicked.connect(self._on_cancel_clicked)\n\n        self._name_input = name_input\n        self._ok_btn = ok_btn\n        self._cancel_btn = cancel_btn\n\n        self._result = None\n\n        self.resize(self.default_width, self.default_height)\n\n    def set_tab_name(self, name):\n        self._name_input.setText(name)\n\n    def result(self):\n        return self._result\n\n    def showEvent(self, event):\n        super(TabNameDialog, self).showEvent(event)\n        btns_width = max(\n            self._ok_btn.width(),\n            self._cancel_btn.width()\n        )\n\n        self._ok_btn.setMinimumWidth(btns_width)\n        self._cancel_btn.setMinimumWidth(btns_width)\n\n    def _on_ok_clicked(self):\n        self._result = self._name_input.text()\n        self.accept()\n\n    def _on_cancel_clicked(self):\n        self._result = None\n        self.reject()\n\n\nclass OutputTextWidget(QtWidgets.QTextEdit):\n    v_max_offset = 4\n\n    def vertical_scroll_at_max(self):\n        v_scroll = self.verticalScrollBar()\n        return v_scroll.value() > v_scroll.maximum() - self.v_max_offset\n\n    def scroll_to_bottom(self):\n        v_scroll = self.verticalScrollBar()\n        return v_scroll.setValue(v_scroll.maximum())\n\n\nclass EnhancedTabBar(QtWidgets.QTabBar):\n    double_clicked = QtCore.Signal(QtCore.QPoint)\n    right_clicked = QtCore.Signal(QtCore.QPoint)\n    mid_clicked = QtCore.Signal(QtCore.QPoint)\n\n    def __init__(self, parent):\n        super(EnhancedTabBar, self).__init__(parent)\n\n        self.setDrawBase(False)\n\n    def mouseDoubleClickEvent(self, event):\n        self.double_clicked.emit(event.globalPos())\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() == QtCore.Qt.RightButton:\n            self.right_clicked.emit(event.globalPos())\n            event.accept()\n            return\n\n        elif event.button() == QtCore.Qt.MidButton:\n            self.mid_clicked.emit(event.globalPos())\n            event.accept()\n\n        else:\n            super(EnhancedTabBar, self).mouseReleaseEvent(event)\n\n\nclass PythonInterpreterWidget(QtWidgets.QWidget):\n    default_width = 1000\n    default_height = 600\n\n    def __init__(self, allow_save_registry=True, parent=None):\n        super(PythonInterpreterWidget, self).__init__(parent)\n\n        self.setWindowTitle(\"{} Console\".format(\n            \"AYON\" if AYON_SERVER_ENABLED else \"OpenPype\"\n        ))\n        self.setWindowIcon(QtGui.QIcon(resources.get_openpype_icon_filepath()))\n\n        self.ansi_escape = re.compile(\n            r\"(?:\\x1B[@-_]|[\\x80-\\x9F])[0-?]*[ -/]*[@-~]\"\n        )\n\n        self._tabs = []\n\n        self._stdout_err_wrapper = StdOEWrap()\n\n        output_widget = OutputTextWidget(self)\n        output_widget.setObjectName(\"PythonInterpreterOutput\")\n        output_widget.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)\n        output_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)\n\n        tab_widget = QtWidgets.QTabWidget(self)\n        tab_bar = EnhancedTabBar(tab_widget)\n        tab_widget.setTabBar(tab_bar)\n        tab_widget.setTabsClosable(False)\n        tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n\n        widgets_splitter = QtWidgets.QSplitter(self)\n        widgets_splitter.setOrientation(QtCore.Qt.Vertical)\n        widgets_splitter.addWidget(output_widget)\n        widgets_splitter.addWidget(tab_widget)\n        widgets_splitter.setStretchFactor(0, 1)\n        widgets_splitter.setStretchFactor(1, 1)\n        height = int(self.default_height / 2)\n        widgets_splitter.setSizes([height, self.default_height - height])\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(widgets_splitter)\n\n        line_check_timer = QtCore.QTimer()\n        line_check_timer.setInterval(200)\n\n        line_check_timer.timeout.connect(self._on_timer_timeout)\n        tab_bar.right_clicked.connect(self._on_tab_right_click)\n        tab_bar.double_clicked.connect(self._on_tab_double_click)\n        tab_bar.mid_clicked.connect(self._on_tab_mid_click)\n        tab_widget.tabCloseRequested.connect(self._on_tab_close_req)\n\n        self._widgets_splitter = widgets_splitter\n        self._output_widget = output_widget\n        self._tab_widget = tab_widget\n        self._line_check_timer = line_check_timer\n\n        if AYON_SERVER_ENABLED:\n            self._append_lines([ayon_art])\n        else:\n            self._append_lines([openpype_art])\n\n        self._first_show = True\n        self._splitter_size_ratio = None\n        self._allow_save_registry = allow_save_registry\n        self._registry_saved = True\n\n        self._init_from_registry()\n\n        if self._tab_widget.count() < 1:\n            self.add_tab(\"Python\")\n\n    def _init_from_registry(self):\n        setting_registry = PythonInterpreterRegistry()\n        width = None\n        height = None\n        try:\n            width = setting_registry.get_item(\"width\")\n            height = setting_registry.get_item(\"height\")\n\n        except ValueError:\n            pass\n\n        if width is None or width < 200:\n            width = self.default_width\n\n        if height is None or height < 200:\n            height = self.default_height\n\n        self.resize(width, height)\n\n        try:\n            self._splitter_size_ratio = (\n                setting_registry.get_item(\"splitter_sizes\")\n            )\n\n        except ValueError:\n            pass\n\n        try:\n            tab_defs = setting_registry.get_item(\"tabs\") or []\n            for tab_def in tab_defs:\n                widget = self.add_tab(tab_def[\"name\"])\n                widget.set_code(tab_def[\"code\"])\n\n        except ValueError:\n            pass\n\n    def save_registry(self):\n        # Window was not showed\n        if not self._allow_save_registry or self._registry_saved:\n            return\n\n        self._registry_saved = True\n        setting_registry = PythonInterpreterRegistry()\n\n        setting_registry.set_item(\"width\", self.width())\n        setting_registry.set_item(\"height\", self.height())\n\n        setting_registry.set_item(\n            \"splitter_sizes\", self._widgets_splitter.sizes()\n        )\n\n        tabs = []\n        for tab_idx in range(self._tab_widget.count()):\n            widget = self._tab_widget.widget(tab_idx)\n            tab_code = widget.get_code()\n            tab_name = self._tab_widget.tabText(tab_idx)\n            tabs.append({\n                \"name\": tab_name,\n                \"code\": tab_code\n            })\n\n        setting_registry.set_item(\"tabs\", tabs)\n\n    def _on_tab_right_click(self, global_point):\n        point = self._tab_widget.mapFromGlobal(global_point)\n        tab_bar = self._tab_widget.tabBar()\n        tab_idx = tab_bar.tabAt(point)\n        last_index = tab_bar.count() - 1\n        if tab_idx < 0 or tab_idx > last_index:\n            return\n\n        menu = QtWidgets.QMenu(self._tab_widget)\n\n        add_tab_action = QtWidgets.QAction(\"Add tab...\", menu)\n        add_tab_action.setToolTip(\"Add new tab\")\n\n        rename_tab_action = QtWidgets.QAction(\"Rename...\", menu)\n        rename_tab_action.setToolTip(\"Rename tab\")\n\n        duplicate_tab_action = QtWidgets.QAction(\"Duplicate...\", menu)\n        duplicate_tab_action.setToolTip(\"Duplicate code to new tab\")\n\n        close_tab_action = QtWidgets.QAction(\"Close\", menu)\n        close_tab_action.setToolTip(\"Close tab and lose content\")\n        close_tab_action.setEnabled(self._tab_widget.tabsClosable())\n\n        menu.addAction(add_tab_action)\n        menu.addAction(rename_tab_action)\n        menu.addAction(duplicate_tab_action)\n        menu.addAction(close_tab_action)\n\n        result = menu.exec_(global_point)\n        if result is None:\n            return\n\n        if result is rename_tab_action:\n            self._rename_tab_req(tab_idx)\n\n        elif result is add_tab_action:\n            self._on_add_requested()\n\n        elif result is duplicate_tab_action:\n            self._duplicate_requested(tab_idx)\n\n        elif result is close_tab_action:\n            self._on_tab_close_req(tab_idx)\n\n    def _rename_tab_req(self, tab_idx):\n        dialog = TabNameDialog(self)\n        dialog.set_tab_name(self._tab_widget.tabText(tab_idx))\n        dialog.exec_()\n        tab_name = dialog.result()\n        if tab_name:\n            self._tab_widget.setTabText(tab_idx, tab_name)\n\n    def _duplicate_requested(self, tab_idx=None):\n        if tab_idx is None:\n            tab_idx = self._tab_widget.currentIndex()\n\n        src_widget = self._tab_widget.widget(tab_idx)\n        dst_widget = self._add_tab()\n        if dst_widget is None:\n            return\n        dst_widget.set_code(src_widget.get_code())\n\n    def _on_tab_mid_click(self, global_point):\n        point = self._tab_widget.mapFromGlobal(global_point)\n        tab_bar = self._tab_widget.tabBar()\n        tab_idx = tab_bar.tabAt(point)\n        last_index = tab_bar.count() - 1\n        if tab_idx < 0 or tab_idx > last_index:\n            return\n\n        self._on_tab_close_req(tab_idx)\n\n    def _on_tab_double_click(self, global_point):\n        point = self._tab_widget.mapFromGlobal(global_point)\n        tab_bar = self._tab_widget.tabBar()\n        tab_idx = tab_bar.tabAt(point)\n        last_index = tab_bar.count() - 1\n        if tab_idx < 0 or tab_idx > last_index:\n            return\n\n        self._rename_tab_req(tab_idx)\n\n    def _on_tab_close_req(self, tab_index):\n        if self._tab_widget.count() == 1:\n            return\n\n        widget = self._tab_widget.widget(tab_index)\n        if widget in self._tabs:\n            self._tabs.remove(widget)\n        self._tab_widget.removeTab(tab_index)\n\n        if self._tab_widget.count() == 1:\n            self._tab_widget.setTabsClosable(False)\n\n    def _append_lines(self, lines):\n        at_max = self._output_widget.vertical_scroll_at_max()\n        tmp_cursor = QtGui.QTextCursor(self._output_widget.document())\n        tmp_cursor.movePosition(QtGui.QTextCursor.End)\n        for line in lines:\n            tmp_cursor.insertText(line)\n\n        if at_max:\n            self._output_widget.scroll_to_bottom()\n\n    def _on_timer_timeout(self):\n        if self._stdout_err_wrapper.lines:\n            lines = []\n            while self._stdout_err_wrapper.lines:\n                line = self._stdout_err_wrapper.lines.popleft()\n                lines.append(self.ansi_escape.sub(\"\", line))\n            self._append_lines(lines)\n\n    def _on_add_requested(self):\n        self._add_tab()\n\n    def _add_tab(self):\n        dialog = TabNameDialog(self)\n        dialog.exec_()\n        tab_name = dialog.result()\n        if tab_name:\n            return self.add_tab(tab_name)\n\n        return None\n\n    def _on_before_execute(self, code_text):\n        at_max = self._output_widget.vertical_scroll_at_max()\n        document = self._output_widget.document()\n        tmp_cursor = QtGui.QTextCursor(document)\n        tmp_cursor.movePosition(QtGui.QTextCursor.End)\n        tmp_cursor.insertText(\"{}\\nExecuting command:\\n\".format(20 * \"-\"))\n\n        code_block_format = QtGui.QTextFrameFormat()\n        code_block_format.setBackground(QtGui.QColor(27, 27, 27))\n        code_block_format.setPadding(4)\n\n        tmp_cursor.insertFrame(code_block_format)\n        char_format = tmp_cursor.charFormat()\n        char_format.setForeground(\n            QtGui.QBrush(QtGui.QColor(114, 224, 198))\n        )\n        tmp_cursor.setCharFormat(char_format)\n        tmp_cursor.insertText(code_text)\n\n        # Create new cursor\n        tmp_cursor = QtGui.QTextCursor(document)\n        tmp_cursor.movePosition(QtGui.QTextCursor.End)\n        tmp_cursor.insertText(\"{}\\n\".format(20 * \"-\"))\n\n        if at_max:\n            self._output_widget.scroll_to_bottom()\n\n    def add_tab(self, tab_name, index=None):\n        widget = PythonTabWidget(self)\n        widget.before_execute.connect(self._on_before_execute)\n        widget.add_tab_requested.connect(self._on_add_requested)\n        if index is None:\n            if self._tab_widget.count() > 0:\n                index = self._tab_widget.currentIndex() + 1\n            else:\n                index = 0\n\n        self._tabs.append(widget)\n        self._tab_widget.insertTab(index, widget, tab_name)\n        self._tab_widget.setCurrentIndex(index)\n\n        if self._tab_widget.count() > 1:\n            self._tab_widget.setTabsClosable(True)\n        widget.setFocus()\n        return widget\n\n    def showEvent(self, event):\n        self._line_check_timer.start()\n        self._registry_saved = False\n        super(PythonInterpreterWidget, self).showEvent(event)\n        # First show setup\n        if self._first_show:\n            self._first_show = False\n            self._on_first_show()\n\n        self._output_widget.scroll_to_bottom()\n\n    def _on_first_show(self):\n        # Change stylesheet\n        self.setStyleSheet(load_stylesheet())\n        # Check if splitter size ratio is set\n        # - first store value to local variable and then unset it\n        splitter_size_ratio = self._splitter_size_ratio\n        self._splitter_size_ratio = None\n        # Skip if is not set\n        if not splitter_size_ratio:\n            return\n\n        # Skip if number of size items does not match to splitter\n        splitters_count = len(self._widgets_splitter.sizes())\n        if len(splitter_size_ratio) == splitters_count:\n            self._widgets_splitter.setSizes(splitter_size_ratio)\n\n    def closeEvent(self, event):\n        self.save_registry()\n        super(PythonInterpreterWidget, self).closeEvent(event)\n        self._line_check_timer.stop()\n"
  },
  {
    "path": "openpype/modules/royalrender/__init__.py",
    "content": "from .royal_render_module import RoyalRenderModule\n\n\n__all__ = (\n    \"RoyalRenderModule\",\n)\n"
  },
  {
    "path": "openpype/modules/royalrender/api.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Wrapper around Royal Render API.\"\"\"\nimport sys\nimport os\n\nfrom openpype.lib.local_settings import OpenPypeSettingsRegistry\nfrom openpype.lib import Logger, run_subprocess\nfrom .rr_job import RRJob, SubmitFile, SubmitterParameter\nfrom openpype.lib.vendor_bin_utils import find_tool_in_custom_paths\n\n\nclass Api:\n\n    _settings = None\n    RR_SUBMIT_CONSOLE = 1\n    RR_SUBMIT_API = 2\n\n    def __init__(self, rr_path=None):\n        self.log = Logger.get_logger(\"RoyalRender\")\n        self._rr_path = rr_path\n        os.environ[\"RR_ROOT\"] = rr_path\n\n    @staticmethod\n    def get_rr_bin_path(rr_root, tool_name=None):\n        # type: (str, str) -> str\n        \"\"\"Get path to RR bin folder.\n\n        Args:\n            tool_name (str): Name of RR executable you want.\n            rr_root (str): Custom RR root if needed.\n\n        Returns:\n            str: Path to the tool based on current platform.\n\n        \"\"\"\n        is_64bit_python = sys.maxsize > 2 ** 32\n\n        rr_bin_parts = [rr_root, \"bin\"]\n        if sys.platform.lower() == \"win32\":\n            rr_bin_parts.append(\"win\")\n\n        if sys.platform.lower() == \"darwin\":\n            rr_bin_parts.append(\"mac\")\n\n        if sys.platform.lower().startswith(\"linux\"):\n            rr_bin_parts.append(\"lx\")\n\n        rr_bin_path = os.sep.join(rr_bin_parts)\n\n        paths_to_check = []\n        # if we use 64bit python, append 64bit specific path first\n        if is_64bit_python:\n            if not tool_name:\n                return rr_bin_path + \"64\"\n            paths_to_check.append(rr_bin_path + \"64\")\n\n        # otherwise use 32bit\n        if not tool_name:\n            return rr_bin_path\n        paths_to_check.append(rr_bin_path)\n\n        return find_tool_in_custom_paths(paths_to_check, tool_name)\n\n    def _initialize_module_path(self):\n        # type: () -> None\n        \"\"\"Set RR modules for Python.\"\"\"\n        # default for linux\n        rr_bin = self.get_rr_bin_path(self._rr_path)\n        rr_module_path = os.path.join(rr_bin, \"lx64/lib\")\n\n        if sys.platform.lower() == \"win32\":\n            rr_module_path = rr_bin\n            rr_module_path = rr_module_path.replace(\n                \"/\", os.path.sep\n            )\n\n        if sys.platform.lower() == \"darwin\":\n            rr_module_path = os.path.join(rr_bin, \"lib/python/27\")\n\n        sys.path.append(os.path.join(self._rr_path, rr_module_path))\n\n    @staticmethod\n    def create_submission(jobs, submitter_attributes):\n        # type: (list[RRJob], list[SubmitterParameter]) -> SubmitFile\n        \"\"\"Create jobs submission file.\n\n        Args:\n            jobs (list): List of :class:`RRJob`\n            submitter_attributes (list): List of submitter attributes\n                :class:`SubmitterParameter` for whole submission batch.\n\n        Returns:\n            str: XML data of job submission files.\n\n        \"\"\"\n        return SubmitFile(SubmitterParameters=submitter_attributes, Jobs=jobs)\n\n    def submit_file(self, file, mode=RR_SUBMIT_CONSOLE):\n        # type: (SubmitFile, int) -> None\n        if mode == self.RR_SUBMIT_CONSOLE:\n            self._submit_using_console(file)\n            return\n\n        # RR v7 supports only Python 2.7, so we bail out in fear\n        # until there is support for Python 3 😰\n        raise NotImplementedError(\n            \"Submission via RoyalRender API is not supported yet\")\n        # self._submit_using_api(file)\n\n    def _submit_using_console(self, job_file):\n        # type: (SubmitFile) -> None\n        rr_start_local = self.get_rr_bin_path(\n            self._rr_path, \"rrStartLocal\")\n\n        self.log.info(\"rr_console: {}\".format(rr_start_local))\n\n        args = [rr_start_local, \"rrSubmitterconsole\", job_file]\n        self.log.info(\"Executing: {}\".format(\" \".join(args)))\n        env = os.environ\n        env[\"RR_ROOT\"] = self._rr_path\n        run_subprocess(args, logger=self.log, env=env)\n\n    def _submit_using_api(self, file):\n        # type: (SubmitFile) -> None\n        \"\"\"Use RR API to submit jobs.\n\n        Args:\n            file (SubmitFile): Submit jobs definition.\n\n        Throws:\n            RoyalRenderException: When something fails.\n\n        \"\"\"\n        self._initialize_module_path()\n        import libpyRR2 as rrLib  # noqa\n        from rrJob import getClass_JobBasics  # noqa\n        import libpyRR2 as _RenderAppBasic  # noqa\n\n        tcp = rrLib._rrTCP(\"\")  # noqa\n        rr_server = tcp.getRRServer()\n\n        if len(rr_server) == 0:\n            self.log.info(\"Got RR IP address {}\".format(rr_server))\n\n        # TODO: Port is hardcoded in RR? If not, move it to Settings\n        if not tcp.setServer(rr_server, 7773):\n            self.log.error(\n                \"Can not set RR server: {}\".format(tcp.errorMessage()))\n            raise RoyalRenderException(tcp.errorMessage())\n\n        # TODO: This need UI and better handling of username/password.\n        # We can't store password in keychain as it is pulled multiple\n        # times and users on linux must enter keychain password every time.\n        # Probably best way until we setup our own user management would be\n        # to encrypt password and save it to json locally. Not bulletproof\n        # but at least it is not stored in plaintext.\n        reg = OpenPypeSettingsRegistry()\n        try:\n            rr_user = reg.get_item(\"rr_username\")\n            rr_password = reg.get_item(\"rr_password\")\n        except ValueError:\n            # user has no rr credentials set\n            pass\n        else:\n            # login to RR\n            tcp.setLogin(rr_user, rr_password)\n\n        job = getClass_JobBasics()\n        renderer = _RenderAppBasic()\n\n        # iterate over SubmitFile, set _JobBasic (job) and renderer\n        # and feed it to jobSubmitNew()\n        # not implemented yet\n        job.renderer = renderer\n        tcp.jobSubmitNew(job)\n\n\nclass RoyalRenderException(Exception):\n    \"\"\"Exception used in various error states coming from RR.\"\"\"\n    pass\n"
  },
  {
    "path": "openpype/modules/royalrender/lib.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Submitting render job to RoyalRender.\"\"\"\nimport os\nimport json\nimport platform\nimport re\nimport tempfile\nimport uuid\nfrom datetime import datetime\n\nimport pyblish.api\n\nfrom openpype.lib import BoolDef, NumberDef, is_running_from_build\nfrom openpype.lib.execute import run_openpype_process\nfrom openpype.modules.royalrender.api import Api as rrApi\nfrom openpype.modules.royalrender.rr_job import (\n    CustomAttribute,\n    RRJob,\n    RREnvList,\n    get_rr_platform,\n    SubmitterParameter\n)\nfrom openpype.pipeline import OpenPypePyblishPluginMixin\nfrom openpype.pipeline.publish import KnownPublishError\nfrom openpype.pipeline.publish.lib import get_published_workfile_instance\nfrom openpype.tests.lib import is_in_tests\n\n\nclass BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin,\n                               OpenPypePyblishPluginMixin):\n    \"\"\"Creates separate rendering job for Royal Render\"\"\"\n    label = \"Create Nuke Render job in RR\"\n    order = pyblish.api.IntegratorOrder + 0.1\n    hosts = [\"nuke\"]\n    families = [\"render\", \"prerender\"]\n    targets = [\"local\"]\n    optional = True\n\n    priority = 50\n    chunk_size = 1\n    concurrent_tasks = 1\n    use_gpu = True\n    use_published = True\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            NumberDef(\n                \"priority\",\n                label=\"Priority\",\n                default=cls.priority,\n                decimals=0\n            ),\n            NumberDef(\n                \"chunk\",\n                label=\"Frames Per Task\",\n                default=cls.chunk_size,\n                decimals=0,\n                minimum=1,\n                maximum=1000\n            ),\n            NumberDef(\n                \"concurrency\",\n                label=\"Concurrency\",\n                default=cls.concurrent_tasks,\n                decimals=0,\n                minimum=1,\n                maximum=10\n            ),\n            BoolDef(\n                \"use_gpu\",\n                default=cls.use_gpu,\n                label=\"Use GPU\"\n            ),\n            BoolDef(\n                \"suspend_publish\",\n                default=False,\n                label=\"Suspend publish\"\n            ),\n            BoolDef(\n                \"use_published\",\n                default=cls.use_published,\n                label=\"Use published workfile\"\n            )\n        ]\n\n    def __init__(self, *args, **kwargs):\n        self._rr_root = None\n        self.scene_path = None\n        self.job = None\n        self.submission_parameters = None\n        self.rr_api = None\n\n    def process(self, instance):\n        if not instance.data.get(\"farm\"):\n            self.log.info(\"Skipping local instance.\")\n            return\n\n        instance.data[\"attributeValues\"] = self.get_attr_values_from_data(\n            instance.data)\n\n        # add suspend_publish attributeValue to instance data\n        instance.data[\"suspend_publish\"] = instance.data[\"attributeValues\"][\n            \"suspend_publish\"]\n\n        context = instance.context\n\n        self._rr_root = self._resolve_rr_path(context, instance.data.get(\n            \"rrPathName\"))  # noqa\n        self.log.debug(self._rr_root)\n        if not self._rr_root:\n            raise KnownPublishError(\n                (\"Missing RoyalRender root. \"\n                 \"Admin needs to configure RoyalRender module in Settings .\"))\n\n        self.rr_api = rrApi(self._rr_root)\n\n        self.scene_path = context.data[\"currentFile\"]\n        if self.use_published:\n            published_workfile = get_published_workfile_instance(context)\n\n            # fallback if nothing was set\n            if published_workfile is None:\n                self.log.warning(\"Falling back to workfile\")\n                file_path = context.data[\"currentFile\"]\n            else:\n                workfile_repre = published_workfile.data[\"representations\"][0]\n                file_path = workfile_repre[\"published_path\"]\n\n            self.scene_path = file_path\n            self.log.info(\n                \"Using published scene for render {}\".format(self.scene_path)\n            )\n\n        if not instance.data.get(\"expectedFiles\"):\n            instance.data[\"expectedFiles\"] = []\n\n        if not instance.data.get(\"rrJobs\"):\n            instance.data[\"rrJobs\"] = []\n\n    def get_job(self, instance, script_path, render_path, node_name):\n        \"\"\"Get RR job based on current instance.\n\n        Args:\n            script_path (str): Path to Nuke script.\n            render_path (str): Output path.\n            node_name (str): Name of the render node.\n\n        Returns:\n            RRJob: RoyalRender Job instance.\n\n        \"\"\"\n        start_frame = int(instance.data[\"frameStartHandle\"])\n        end_frame = int(instance.data[\"frameEndHandle\"])\n\n        batch_name = os.path.basename(script_path)\n        jobname = \"%s - %s\" % (batch_name, instance.name)\n        if is_in_tests():\n            batch_name += datetime.now().strftime(\"%d%m%Y%H%M%S\")\n\n        render_dir = os.path.normpath(os.path.dirname(render_path))\n        output_filename_0 = self.pad_file_name(render_path, str(start_frame))\n        file_name, file_ext = os.path.splitext(\n            os.path.basename(output_filename_0))\n\n        custom_attributes = []\n        if is_running_from_build():\n            custom_attributes = [\n                CustomAttribute(\n                    name=\"OpenPypeVersion\",\n                    value=os.environ.get(\"OPENPYPE_VERSION\"))\n            ]\n\n        # this will append expected files to instance as needed.\n        expected_files = self.expected_files(\n            instance, render_path, start_frame, end_frame)\n        instance.data[\"expectedFiles\"].extend(expected_files)\n\n        submitter_parameters = []\n\n        anatomy_data = instance.context.data[\"anatomyData\"]\n        environment = RREnvList({\n            \"AVALON_PROJECT\": anatomy_data[\"project\"][\"name\"],\n            \"AVALON_ASSET\": instance.context.data[\"asset\"],\n            \"AVALON_TASK\": anatomy_data[\"task\"][\"name\"],\n            \"AVALON_APP_NAME\": instance.context.data.get(\"appName\"),\n            \"AYON_RENDER_JOB\": \"1\",\n            \"AYON_BUNDLE_NAME\": os.environ[\"AYON_BUNDLE_NAME\"]\n        })\n\n        render_dir = render_dir.replace(\"\\\\\", \"/\")\n        job = RRJob(\n            Software=\"\",\n            Renderer=\"\",\n            SeqStart=int(start_frame),\n            SeqEnd=int(end_frame),\n            SeqStep=int(instance.data.get(\"byFrameStep\", 1)),\n            SeqFileOffset=0,\n            Version=0,\n            SceneName=script_path,\n            IsActive=True,\n            ImageDir=render_dir,\n            ImageFilename=file_name,\n            ImageExtension=file_ext,\n            ImagePreNumberLetter=\"\",\n            ImageSingleOutputFile=False,\n            SceneOS=get_rr_platform(),\n            Layer=node_name,\n            SceneDatabaseDir=script_path,\n            CustomSHotName=jobname,\n            CompanyProjectName=instance.context.data[\"projectName\"],\n            ImageWidth=instance.data[\"resolutionWidth\"],\n            ImageHeight=instance.data[\"resolutionHeight\"],\n            CustomAttributes=custom_attributes,\n            SubmitterParameters=submitter_parameters,\n            rrEnvList=environment.serialize(),\n            rrEnvFile=os.path.join(render_dir, \"rrEnv.rrEenv\"),\n        )\n\n        return job\n\n    def update_job_with_host_specific(self, instance, job):\n        \"\"\"Host specific mapping for RRJob\"\"\"\n        raise NotImplementedError\n\n    @staticmethod\n    def _resolve_rr_path(context, rr_path_name):\n        # type: (pyblish.api.Context, str) -> str\n        rr_settings = (\n            context.data\n            [\"system_settings\"]\n            [\"modules\"]\n            [\"royalrender\"]\n        )\n        try:\n            default_servers = rr_settings[\"rr_paths\"]\n            project_servers = (\n                context.data\n                [\"project_settings\"]\n                [\"royalrender\"]\n                [\"rr_paths\"]\n            )\n            rr_servers = {\n                k: default_servers[k]\n                for k in project_servers\n                if k in default_servers\n            }\n\n        except (AttributeError, KeyError):\n            # Handle situation were we had only one url for royal render.\n            return context.data[\"defaultRRPath\"][platform.system().lower()]\n\n        return rr_servers[rr_path_name][platform.system().lower()]\n\n    def expected_files(self, instance, path, start_frame, end_frame):\n        \"\"\"Get expected files.\n\n        This function generate expected files from provided\n        path and start/end frames.\n\n        It was taken from Deadline module, but this should be\n        probably handled better in collector to support more\n        flexible scenarios.\n\n        Args:\n            instance (Instance)\n            path (str): Output path.\n            start_frame (int): Start frame.\n            end_frame (int): End frame.\n\n        Returns:\n            list: List of expected files.\n\n        \"\"\"\n        dir_name = os.path.dirname(path)\n        file = os.path.basename(path)\n\n        expected_files = []\n\n        if \"#\" in file:\n            pparts = file.split(\"#\")\n            padding = \"%0{}d\".format(len(pparts) - 1)\n            file = pparts[0] + padding + pparts[-1]\n\n        if \"%\" not in file:\n            expected_files.append(path)\n            return expected_files\n\n        if instance.data.get(\"slate\"):\n            start_frame -= 1\n\n        expected_files.extend(\n            os.path.join(dir_name, (file % i)).replace(\"\\\\\", \"/\")\n            for i in range(start_frame, (end_frame + 1))\n        )\n        return expected_files\n\n    def pad_file_name(self, path, first_frame):\n        \"\"\"Return output file path with #### for padding.\n\n        RR requires the path to be formatted with # in place of numbers.\n        For example `/path/to/render.####.png`\n\n        Args:\n            path (str): path to rendered image\n            first_frame (str): from representation to cleany replace with #\n                padding\n\n        Returns:\n            str\n\n        \"\"\"\n        self.log.debug(\"pad_file_name path: `{}`\".format(path))\n        if \"%\" in path:\n            search_results = re.search(r\"(%0)(\\d)(d.)\", path).groups()\n            self.log.debug(\"_ search_results: `{}`\".format(search_results))\n            return int(search_results[1])\n        if \"#\" in path:\n            self.log.debug(\"already padded: `{}`\".format(path))\n            return path\n\n        if first_frame:\n            padding = len(first_frame)\n            path = path.replace(first_frame, \"#\" * padding)\n\n        return path\n"
  },
  {
    "path": "openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\n\n\nclass CollectRRPathFromInstance(pyblish.api.InstancePlugin):\n    \"\"\"Collect RR Path from instance.\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = \"Collect Royal Render path name from the Instance\"\n    families = [\"render\", \"prerender\", \"renderlayer\"]\n\n    def process(self, instance):\n        instance.data[\"rrPathName\"] = self._collect_rr_path_name(instance)\n        self.log.info(\n            \"Using '{}' for submission.\".format(instance.data[\"rrPathName\"]))\n\n    @staticmethod\n    def _collect_rr_path_name(instance):\n        # type: (pyblish.api.Instance) -> str\n        \"\"\"Get Royal Render pat name from render instance.\"\"\"\n        rr_settings = (\n            instance.context.data\n            [\"system_settings\"]\n            [\"modules\"]\n            [\"royalrender\"]\n        )\n        if not instance.data.get(\"rrPaths\"):\n            return \"default\"\n        try:\n            default_servers = rr_settings[\"rr_paths\"]\n            project_servers = (\n                instance.context.data\n                [\"project_settings\"]\n                [\"royalrender\"]\n                [\"rr_paths\"]\n            )\n            rr_servers = {\n                k: default_servers[k]\n                for k in project_servers\n                if k in default_servers\n            }\n\n        except (AttributeError, KeyError):\n            # Handle situation were we had only one url for royal render.\n            return rr_settings[\"rr_paths\"][\"default\"]\n\n        return list(rr_servers.keys())[int(instance.data.get(\"rrPaths\"))]\n"
  },
  {
    "path": "openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect sequences from Royal Render Job.\"\"\"\nimport os\nimport re\nimport copy\nimport json\nfrom pprint import pformat\n\nimport pyblish.api\n\nfrom openpype.pipeline import legacy_io\n\n\ndef collect(root,\n            regex=None,\n            exclude_regex=None,\n            frame_start=None,\n            frame_end=None):\n    \"\"\"Collect sequence collections in root\"\"\"\n\n    import clique\n\n    files = []\n    for filename in os.listdir(root):\n\n        # Must have extension\n        ext = os.path.splitext(filename)[1]\n        if not ext:\n            continue\n\n        # Only files\n        if not os.path.isfile(os.path.join(root, filename)):\n            continue\n\n        # Include and exclude regex\n        if regex and not re.search(regex, filename):\n            continue\n        if exclude_regex and re.search(exclude_regex, filename):\n            continue\n\n        files.append(filename)\n\n    # Match collections\n    # Support filenames like: projectX_shot01_0010.tiff with this regex\n    pattern = r\"(?P<index>(?P<padding>0*)\\d+)\\.\\D+\\d?$\"\n    collections, remainder = clique.assemble(files,\n                                             patterns=[pattern],\n                                             minimum_items=1)\n\n    # Ignore any remainders\n    if remainder:\n        print(\"Skipping remainder {}\".format(remainder))\n\n    # Exclude any frames outside start and end frame.\n    for collection in collections:\n        for index in list(collection.indexes):\n            if frame_start is not None and index < frame_start:\n                collection.indexes.discard(index)\n                continue\n            if frame_end is not None and index > frame_end:\n                collection.indexes.discard(index)\n                continue\n\n    # Keep only collections that have at least a single frame\n    collections = [c for c in collections if c.indexes]\n\n    return collections\n\n\nclass CollectSequencesFromJob(pyblish.api.ContextPlugin):\n    \"\"\"Gather file sequences from job directory.\n\n    When \"OPENPYPE_PUBLISH_DATA\" environment variable is set these paths\n    (folders or .json files) are parsed for image sequences. Otherwise, the\n    current working directory is searched for file sequences.\n\n    \"\"\"\n    order = pyblish.api.CollectorOrder\n    targets = [\"rr_control\"]\n    label = \"Collect Rendered Frames\"\n    review = True\n\n    def process(self, context):\n\n        self.review = (\n            context.data\n            [\"project_settings\"]\n            [\"royalrender\"]\n            [\"publish\"]\n            [\"CollectSequencesFromJob\"]\n            [\"review\"]\n        )\n\n        if os.environ.get(\"OPENPYPE_PUBLISH_DATA\"):\n            self.log.debug(os.environ.get(\"OPENPYPE_PUBLISH_DATA\"))\n            paths = os.environ[\"OPENPYPE_PUBLISH_DATA\"].split(os.pathsep)\n            self.log.info(\"Collecting paths: {}\".format(paths))\n        else:\n            cwd = context.get(\"workspaceDir\", os.getcwd())\n            paths = [cwd]\n\n        for path in paths:\n\n            self.log.info(\"Loading: {}\".format(path))\n\n            if path.endswith(\".json\"):\n                # Search using .json configuration\n                with open(path, \"r\") as f:\n                    try:\n                        data = json.load(f)\n                    except Exception as exc:\n                        self.log.error(\"Error loading json: \"\n                                       \"{} - Exception: {}\".format(path, exc))\n                        raise\n\n                cwd = os.path.dirname(path)\n                root_override = data.get(\"root\")\n                if root_override:\n                    if os.path.isabs(root_override):\n                        root = root_override\n                    else:\n                        root = os.path.join(cwd, root_override)\n                else:\n                    root = cwd\n\n                metadata = data.get(\"metadata\")\n                if metadata:\n                    session = metadata.get(\"session\")\n                    if session:\n                        self.log.info(\"setting session using metadata\")\n                        legacy_io.Session.update(session)\n                        os.environ.update(session)\n\n            else:\n                # Search in directory\n                data = {}\n                root = path\n\n            self.log.info(\"Collecting: {}\".format(root))\n            regex = data.get(\"regex\")\n            if regex:\n                self.log.info(\"Using regex: {}\".format(regex))\n\n            collections = collect(root=root,\n                                  regex=regex,\n                                  exclude_regex=data.get(\"exclude_regex\"),\n                                  frame_start=data.get(\"frameStart\"),\n                                  frame_end=data.get(\"frameEnd\"))\n\n            self.log.info(\"Found collections: {}\".format(collections))\n\n            if data.get(\"subset\") and len(collections) > 1:\n                self.log.error(\"Forced subset can only work with a single \"\n                               \"found sequence\")\n                raise RuntimeError(\"Invalid sequence\")\n\n            fps = data.get(\"fps\", 25)\n\n            # Get family from the data\n            families = data.get(\"families\", [\"render\"])\n            if \"render\" not in families:\n                families.append(\"render\")\n            if \"ftrack\" not in families:\n                families.append(\"ftrack\")\n            if \"review\" not in families and self.review:\n                self.log.info(\"attaching review\")\n                families.append(\"review\")\n\n            for collection in collections:\n                instance = context.create_instance(str(collection))\n                self.log.info(\"Collection: %s\" % list(collection))\n\n                # Ensure each instance gets a unique reference to the data\n                data = copy.deepcopy(data)\n\n                # If no subset provided, get it from collection's head\n                subset = data.get(\"subset\", collection.head.rstrip(\"_. \"))\n\n                # If no start or end frame provided, get it from collection\n                indices = list(collection.indexes)\n                start = data.get(\"frameStart\", indices[0])\n                end = data.get(\"frameEnd\", indices[-1])\n\n                ext = list(collection)[0].split('.')[-1]\n\n                instance.data.update({\n                    \"name\": str(collection),\n                    \"family\": families[0],  # backwards compatibility / pyblish\n                    \"families\": list(families),\n                    \"subset\": subset,\n                    \"asset\": data.get(\n                        \"asset\", context.data[\"asset\"]\n                    ),\n                    \"stagingDir\": root,\n                    \"frameStart\": start,\n                    \"frameEnd\": end,\n                    \"fps\": fps,\n                    \"source\": data.get('source', '')\n                })\n                instance.append(collection)\n                instance.context.data['fps'] = fps\n\n                if \"representations\" not in instance.data:\n                    instance.data[\"representations\"] = []\n\n                representation = {\n                    'name': ext,\n                    'ext': '{}'.format(ext),\n                    'files': list(collection),\n                    \"frameStart\": start,\n                    \"frameEnd\": end,\n                    \"stagingDir\": root,\n                    \"anatomy_template\": \"render\",\n                    \"fps\": fps,\n                    \"tags\": ['review']\n                }\n                instance.data[\"representations\"].append(representation)\n\n                if data.get('user'):\n                    context.data[\"user\"] = data['user']\n\n                self.log.debug(\"Collected instance:\\n\"\n                               \"{}\".format(pformat(instance.data)))\n"
  },
  {
    "path": "openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Submitting render job to RoyalRender.\"\"\"\nimport os\n\nfrom maya.OpenMaya import MGlobal  # noqa: F401\n\nfrom openpype.modules.royalrender import lib\nfrom openpype.pipeline.farm.tools import iter_expected_files\n\n\nclass CreateMayaRoyalRenderJob(lib.BaseCreateRoyalRenderJob):\n    label = \"Create Maya Render job in RR\"\n    hosts = [\"maya\"]\n    families = [\"renderlayer\"]\n\n    def update_job_with_host_specific(self, instance, job):\n        job.Software = \"Maya\"\n        job.Version = \"{0:.2f}\".format(MGlobal.apiVersion() / 10000)\n        if instance.data.get(\"cameras\"):\n            job.Camera = instance.data[\"cameras\"][0].replace(\"'\", '\"')\n        workspace = instance.context.data[\"workspaceDir\"]\n        job.SceneDatabaseDir = workspace\n\n        return job\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        super(CreateMayaRoyalRenderJob, self).process(instance)\n\n        expected_files = instance.data[\"expectedFiles\"]\n        first_file_path = next(iter_expected_files(expected_files))\n        output_dir = os.path.dirname(first_file_path)\n        instance.data[\"outputDir\"] = output_dir\n\n        layer = instance.data[\"setMembers\"]  # type: str\n        layer_name = layer.removeprefix(\"rs_\")\n\n        job = self.get_job(instance, self.scene_path, first_file_path,\n                           layer_name)\n        job = self.update_job_with_host_specific(instance, job)\n\n        instance.data[\"rrJobs\"].append(job)\n"
  },
  {
    "path": "openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Submitting render job to RoyalRender.\"\"\"\nimport re\n\nfrom openpype.modules.royalrender import lib\n\n\nclass CreateNukeRoyalRenderJob(lib.BaseCreateRoyalRenderJob):\n    \"\"\"Creates separate rendering job for Royal Render\"\"\"\n    label = \"Create Nuke Render job in RR\"\n    hosts = [\"nuke\"]\n    families = [\"render\", \"prerender\"]\n\n    def process(self, instance):\n        super(CreateNukeRoyalRenderJob, self).process(instance)\n\n        # redefinition of families\n        if \"render\" in instance.data[\"family\"]:\n            instance.data[\"family\"] = \"write\"\n            instance.data[\"families\"].insert(0, \"render2d\")\n        elif \"prerender\" in instance.data[\"family\"]:\n            instance.data[\"family\"] = \"write\"\n            instance.data[\"families\"].insert(0, \"prerender\")\n\n        jobs = self.create_jobs(instance)\n        for job in jobs:\n            job = self.update_job_with_host_specific(instance, job)\n\n            instance.data[\"rrJobs\"].append(job)\n\n    def update_job_with_host_specific(self, instance, job):\n        nuke_version = re.search(\n            r\"\\d+\\.\\d+\", instance.context.data.get(\"hostVersion\"))\n\n        job.Software = \"Nuke\"\n        job.Version = nuke_version.group()\n\n        return job\n\n    def create_jobs(self, instance):\n        \"\"\"Nuke creates multiple RR jobs - for baking etc.\"\"\"\n        # get output path\n        render_path = instance.data['path']\n        script_path = self.scene_path\n        node = instance.data[\"transientData\"][\"node\"]\n\n        # main job\n        jobs = [\n            self.get_job(\n                instance,\n                script_path,\n                render_path,\n                node.name()\n            )\n        ]\n\n        for baking_script in instance.data.get(\"bakingNukeScripts\", []):\n            render_path = baking_script[\"bakeRenderPath\"]\n            script_path = baking_script[\"bakeScriptPath\"]\n            exe_node_name = baking_script[\"bakeWriteNodeName\"]\n\n            jobs.append(self.get_job(\n                instance,\n                script_path,\n                render_path,\n                exe_node_name\n            ))\n\n        return jobs\n"
  },
  {
    "path": "openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Create publishing job on RoyalRender.\"\"\"\nimport os\nimport attr\nimport json\nimport re\n\nimport pyblish.api\n\nfrom openpype.modules.royalrender.rr_job import (\n    RRJob,\n    RREnvList,\n    get_rr_platform\n)\nfrom openpype.pipeline.publish import KnownPublishError\nfrom openpype.pipeline import (\n    legacy_io,\n)\nfrom openpype.pipeline.farm.pyblish_functions import (\n    create_skeleton_instance,\n    create_instances_for_aov,\n    attach_instances_to_subset,\n    prepare_representations,\n    create_metadata_path\n)\nfrom openpype.pipeline import publish\n\n\nclass CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin,\n                                  publish.ColormanagedPyblishPluginMixin):\n    \"\"\"Creates job which publishes rendered files to publish area.\n\n    Job waits until all rendering jobs are finished, triggers `publish` command\n    where it reads from prepared .json file with metadata about what should\n    be published, renames prepared images and publishes them.\n\n    When triggered it produces .log file next to .json file in work area.\n    \"\"\"\n    label = \"Create publish job in RR\"\n    order = pyblish.api.IntegratorOrder + 0.2\n    icon = \"tractor\"\n    targets = [\"local\"]\n    hosts = [\"fusion\", \"maya\", \"nuke\", \"celaction\", \"aftereffects\", \"harmony\"]\n    families = [\"render.farm\", \"prerender.farm\",\n                \"renderlayer\", \"imagesequence\", \"vrayscene\"]\n    aov_filter = {\"maya\": [r\".*([Bb]eauty).*\"],\n                  \"aftereffects\": [r\".*\"],  # for everything from AE\n                  \"harmony\": [r\".*\"],  # for everything from AE\n                  \"celaction\": [r\".*\"]}\n\n    skip_integration_repre_list = []\n\n    # mapping of instance properties to be transferred to new instance\n    #     for every specified family\n    instance_transfer = {\n        \"slate\": [\"slateFrames\", \"slate\"],\n        \"review\": [\"lutPath\"],\n        \"render2d\": [\"bakingNukeScripts\", \"version\"],\n        \"renderlayer\": [\"convertToScanline\"]\n    }\n\n    # list of family names to transfer to new family if present\n    families_transfer = [\"render3d\", \"render2d\", \"ftrack\", \"slate\"]\n\n    environ_job_filter = [\n        \"OPENPYPE_METADATA_FILE\"\n    ]\n\n    environ_keys = [\n        \"FTRACK_API_USER\",\n        \"FTRACK_API_KEY\",\n        \"FTRACK_SERVER\",\n        \"AVALON_APP_NAME\",\n        \"OPENPYPE_USERNAME\",\n        \"OPENPYPE_SG_USER\",\n        \"AYON_BUNDLE_NAME\"\n    ]\n    priority = 50\n\n    def process(self, instance):\n        context = instance.context\n        self.context = context\n        self.anatomy = instance.context.data[\"anatomy\"]\n\n        if not instance.data.get(\"farm\"):\n            self.log.info(\"Skipping local instance.\")\n            return\n\n        instance_skeleton_data = create_skeleton_instance(\n            instance,\n            families_transfer=self.families_transfer,\n            instance_transfer=self.instance_transfer)\n\n        do_not_add_review = False\n        if instance.data.get(\"review\") is False:\n            self.log.debug(\"Instance has review explicitly disabled.\")\n            do_not_add_review = True\n\n        if isinstance(instance.data.get(\"expectedFiles\")[0], dict):\n            instances = create_instances_for_aov(\n                instance, instance_skeleton_data,\n                self.aov_filter, self.skip_integration_repre_list,\n                do_not_add_review)\n\n        else:\n            representations = prepare_representations(\n                instance_skeleton_data,\n                instance.data.get(\"expectedFiles\"),\n                self.anatomy,\n                self.aov_filter,\n                self.skip_integration_repre_list,\n                do_not_add_review,\n                instance.context,\n                self\n            )\n\n            if \"representations\" not in instance_skeleton_data.keys():\n                instance_skeleton_data[\"representations\"] = []\n\n            # add representation\n            instance_skeleton_data[\"representations\"] += representations\n            instances = [instance_skeleton_data]\n\n        # attach instances to subset\n        if instance.data.get(\"attachTo\"):\n            instances = attach_instances_to_subset(\n                instance.data.get(\"attachTo\"), instances\n            )\n\n        self.log.info(\"Creating RoyalRender Publish job ...\")\n\n        if not instance.data.get(\"rrJobs\"):\n            self.log.error((\"There is no prior RoyalRender \"\n                            \"job on the instance.\"))\n            raise KnownPublishError(\n                \"Can't create publish job without prior rendering jobs first\")\n\n        rr_job = self.get_job(instance, instances)\n        instance.data[\"rrJobs\"].append(rr_job)\n\n        # publish job file\n        publish_job = {\n            \"asset\": instance_skeleton_data[\"asset\"],\n            \"frameStart\": instance_skeleton_data[\"frameStart\"],\n            \"frameEnd\": instance_skeleton_data[\"frameEnd\"],\n            \"fps\": instance_skeleton_data[\"fps\"],\n            \"source\": instance_skeleton_data[\"source\"],\n            \"user\": instance.context.data[\"user\"],\n            \"version\": instance.context.data[\"version\"],   # workfile version\n            \"intent\": instance.context.data.get(\"intent\"),\n            \"comment\": instance.context.data.get(\"comment\"),\n            \"job\": attr.asdict(rr_job),\n            \"session\": legacy_io.Session.copy(),\n            \"instances\": instances\n        }\n\n        metadata_path, rootless_metadata_path = \\\n            create_metadata_path(instance, self.anatomy)\n\n        self.log.info(\"Writing json file: {}\".format(metadata_path))\n        with open(metadata_path, \"w\") as f:\n            json.dump(publish_job, f, indent=4, sort_keys=True)\n\n    def get_job(self, instance, instances):\n        \"\"\"Create RR publishing job.\n\n        Based on provided original instance and additional instances,\n        create publishing job and return it to be submitted to farm.\n\n        Args:\n            instance (Instance): Original instance.\n            instances (list of Instance): List of instances to\n                be published on farm.\n\n        Returns:\n            RRJob: RoyalRender publish job.\n\n        \"\"\"\n        data = instance.data.copy()\n        subset = data[\"subset\"]\n        jobname = \"Publish - {subset}\".format(subset=subset)\n\n        # Transfer the environment from the original job to this dependent\n        # job, so they use the same environment\n        metadata_path, rootless_metadata_path = \\\n            create_metadata_path(instance, self.anatomy)\n\n        anatomy_data = instance.context.data[\"anatomyData\"]\n\n        environment = RREnvList({\n            \"AVALON_PROJECT\": anatomy_data[\"project\"][\"name\"],\n            \"AVALON_ASSET\": instance.context.data[\"asset\"],\n            \"AVALON_TASK\": anatomy_data[\"task\"][\"name\"],\n            \"OPENPYPE_USERNAME\": anatomy_data[\"user\"]\n        })\n\n        # add environments from self.environ_keys\n        for env_key in self.environ_keys:\n            if os.getenv(env_key):\n                environment[env_key] = os.environ[env_key]\n\n        # pass environment keys from self.environ_job_filter\n        # and collect all pre_ids to wait for\n        job_environ = {}\n        jobs_pre_ids = []\n        for job in instance.data[\"rrJobs\"]:  # type: RRJob\n            if job.rrEnvList:\n                if len(job.rrEnvList) > 2000:\n                    self.log.warning((\"Job environment is too long \"\n                                      f\"{len(job.rrEnvList)} > 2000\"))\n                job_environ.update(\n                    dict(RREnvList.parse(job.rrEnvList))\n                )\n            jobs_pre_ids.append(job.PreID)\n\n        for env_j_key in self.environ_job_filter:\n            if job_environ.get(env_j_key):\n                environment[env_j_key] = job_environ[env_j_key]\n\n        priority = self.priority or instance.data.get(\"priority\", 50)\n\n        # rr requires absolut path or all jobs won't show up in rControl\n        abs_metadata_path = self.anatomy.fill_root(rootless_metadata_path)\n\n        # command line set in E01__OpenPype__PublishJob.cfg, here only\n        # additional logging\n        args = [\n            \">\", os.path.join(os.path.dirname(abs_metadata_path),\n                              \"rr_out.log\"),\n            \"2>&1\"\n        ]\n\n        job = RRJob(\n            Software=\"AYON\",\n            Renderer=\"Once\",\n            SeqStart=1,\n            SeqEnd=1,\n            SeqStep=1,\n            SeqFileOffset=0,\n            Version=os.environ[\"AYON_BUNDLE_NAME\"],\n            SceneName=abs_metadata_path,\n            # command line arguments\n            CustomAddCmdFlags=\" \".join(args),\n            IsActive=True,\n            ImageFilename=\"execOnce.file\",\n            ImageDir=\"<SceneFolder>\",\n            ImageExtension=\"\",\n            ImagePreNumberLetter=\"\",\n            SceneOS=get_rr_platform(),\n            rrEnvList=environment.serialize(),\n            Priority=priority,\n            CustomSHotName=jobname,\n            CompanyProjectName=instance.context.data[\"projectName\"]\n        )\n\n        # add assembly jobs as dependencies\n        if instance.data.get(\"tileRendering\"):\n            self.log.info(\"Adding tile assembly jobs as dependencies...\")\n            job.WaitForPreIDs += instance.data.get(\"assemblySubmissionJobs\")\n        elif instance.data.get(\"bakingSubmissionJobs\"):\n            self.log.info(\"Adding baking submission jobs as dependencies...\")\n            job.WaitForPreIDs += instance.data[\"bakingSubmissionJobs\"]\n        else:\n            job.WaitForPreIDs += jobs_pre_ids\n\n        return job\n"
  },
  {
    "path": "openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Submit jobs to RoyalRender.\"\"\"\nimport tempfile\nimport platform\n\nimport pyblish.api\nfrom openpype.modules.royalrender.api import (\n    RRJob,\n    Api as rrApi,\n    SubmitterParameter\n)\nfrom openpype.pipeline.publish import KnownPublishError\n\n\nclass SubmitJobsToRoyalRender(pyblish.api.ContextPlugin):\n    \"\"\"Find all jobs, create submission XML and submit it to RoyalRender.\"\"\"\n    label = \"Submit jobs to RoyalRender\"\n    order = pyblish.api.IntegratorOrder + 0.3\n    targets = [\"local\"]\n\n    def __init__(self):\n        super(SubmitJobsToRoyalRender, self).__init__()\n        self._rr_root = None\n        self._rr_api = None\n        self._submission_parameters = []\n\n    def process(self, context):\n        rr_settings = (\n            context.data\n            [\"system_settings\"]\n            [\"modules\"]\n            [\"royalrender\"]\n        )\n\n        if rr_settings[\"enabled\"] is not True:\n            self.log.warning(\"RoyalRender modules is disabled.\")\n            return\n\n        # iterate over all instances and try to find RRJobs\n        jobs = []\n        instance_rr_path = None\n        for instance in context:\n            if isinstance(instance.data.get(\"rrJob\"), RRJob):\n                jobs.append(instance.data.get(\"rrJob\"))\n            if instance.data.get(\"rrJobs\"):\n                if all(\n                        isinstance(job, RRJob)\n                        for job in instance.data.get(\"rrJobs\")):\n                    jobs += instance.data.get(\"rrJobs\")\n            if instance.data.get(\"rrPathName\"):\n                instance_rr_path = instance.data[\"rrPathName\"]\n\n        if jobs:\n            self._rr_root = self._resolve_rr_path(context, instance_rr_path)\n            if not self._rr_root:\n                raise KnownPublishError(\n                    (\"Missing RoyalRender root. \"\n                     \"You need to configure RoyalRender module.\"))\n            self._rr_api = rrApi(self._rr_root)\n            self._submission_parameters = self.get_submission_parameters()\n            self.process_submission(jobs)\n            return\n\n        self.log.info(\"No RoyalRender jobs found\")\n\n    def process_submission(self, jobs):\n        # type: ([RRJob]) -> None\n\n        idx_pre_id = 0\n        for job in jobs:\n            job.PreID = idx_pre_id\n            if idx_pre_id > 0:\n                job.WaitForPreIDs.append(idx_pre_id - 1)\n            idx_pre_id += 1\n\n        submission = rrApi.create_submission(\n            jobs,\n            self._submission_parameters)\n\n        xml = tempfile.NamedTemporaryFile(suffix=\".xml\", delete=False)\n        with open(xml.name, \"w\") as f:\n            f.write(submission.serialize())\n\n        self.log.info(\"submitting job(s) file: {}\".format(xml.name))\n        self._rr_api.submit_file(file=xml.name)\n\n    def create_file(self, name, ext, contents=None):\n        temp = tempfile.NamedTemporaryFile(\n            dir=self.tempdir,\n            suffix=ext,\n            prefix=name + '.',\n            delete=False,\n        )\n\n        if contents:\n            with open(temp.name, 'w') as f:\n                f.write(contents)\n\n        return temp.name\n\n    def get_submission_parameters(self):\n        return [SubmitterParameter(\"RequiredMemory\", \"0\"),\n                SubmitterParameter(\"PPAyoninjectenvvar\", \"1~1\")]\n\n    @staticmethod\n    def _resolve_rr_path(context, rr_path_name):\n        # type: (pyblish.api.Context, str) -> str\n        rr_settings = (\n            context.data\n            [\"system_settings\"]\n            [\"modules\"]\n            [\"royalrender\"]\n        )\n        try:\n            default_servers = rr_settings[\"rr_paths\"]\n            project_servers = (\n                context.data\n                [\"project_settings\"]\n                [\"royalrender\"]\n                [\"rr_paths\"]\n            )\n            rr_servers = {\n                k: default_servers[k]\n                for k in project_servers\n                if k in default_servers\n            }\n\n        except (AttributeError, KeyError):\n            # Handle situation were we had only one url for royal render.\n            return context.data[\"defaultRRPath\"][platform.system().lower()]\n\n        return rr_servers[rr_path_name][platform.system().lower()]\n"
  },
  {
    "path": "openpype/modules/royalrender/royal_render_module.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Module providing support for Royal Render.\"\"\"\nimport os\nimport openpype.modules\nfrom openpype.modules import OpenPypeModule, IPluginPaths\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.lib import Logger\n\n\n\nclass RoyalRenderModule(OpenPypeModule, IPluginPaths):\n    \"\"\"Class providing basic Royal Render implementation logic.\"\"\"\n    name = \"royalrender\"\n\n    @property\n    def api(self):\n        if not self._api:\n            # import royal render modules\n            from . import api as rr_api\n            self._api = rr_api.Api(self.settings)\n\n        return self._api\n\n    def __init__(self, manager, settings):\n        # type: (openpype.modules.base.ModulesManager, dict) -> None\n        self.rr_paths = {}\n        self._api = None\n        self.settings = settings\n        super(RoyalRenderModule, self).__init__(manager, settings)\n\n    def initialize(self, module_settings):\n        # type: (dict) -> None\n        rr_settings = module_settings[self.name]\n        self.enabled = rr_settings[\"enabled\"]\n        self.rr_paths = rr_settings.get(\"rr_paths\")\n\n        # Ayon only\n        if not AYON_SERVER_ENABLED:\n            self.log.info(\"RoyalRender is not implemented for Openpype\")\n            self.enabled = False\n\n    @staticmethod\n    def get_plugin_paths():\n        # type: () -> dict\n        \"\"\"Royal Render plugin paths.\n\n        Returns:\n            dict: Dictionary of plugin paths for RR.\n        \"\"\"\n        current_dir = os.path.dirname(os.path.abspath(__file__))\n        return {\n            \"publish\": [os.path.join(current_dir, \"plugins\", \"publish\")]\n        }\n"
  },
  {
    "path": "openpype/modules/royalrender/rr_job.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Python wrapper for RoyalRender XML job file.\"\"\"\nimport sys\nfrom xml.dom import minidom as md\nimport attr\nfrom collections import namedtuple, OrderedDict\n\n\nCustomAttribute = namedtuple(\"CustomAttribute\", [\"name\", \"value\"])\n\n\ndef get_rr_platform():\n    # type: () -> str\n    \"\"\"Returns name of platform used in rr jobs.\"\"\"\n    if sys.platform.lower() in [\"win32\", \"win64\"]:\n        return \"windows\"\n    elif sys.platform.lower() == \"darwin\":\n        return \"mac\"\n    else:\n        return \"linux\"\n\n\nclass RREnvList(dict):\n    def serialize(self):\n        # <rrEnvList>VariableA=ValueA~~~VariableB=ValueB</rrEnvList>\n        return \"~~~\".join(\n            [\"{}={}\".format(k, v) for k, v in sorted(self.items())])\n\n    @staticmethod\n    def parse(data):\n        # type: (str) -> RREnvList\n        \"\"\"Parse rrEnvList string and return it as RREnvList object.\"\"\"\n        out = RREnvList()\n        for var in data.split(\"~~~\"):\n            k, v = var.split(\"=\", maxsplit=1)\n            out[k] = v\n        return out\n\n\n@attr.s\nclass RRJob(object):\n    \"\"\"Mapping of Royal Render job file to a data class.\"\"\"\n\n    # Required\n    # --------\n\n    # Name of your render application. Same as in the render config file.\n    # (Maya, Softimage)\n    Software = attr.ib()  # type: str\n\n    # The OS the scene was created on, all texture paths are set on\n    # that OS. Possible values are windows, linux, osx\n    SceneOS = attr.ib()  # type: str\n\n    # Renderer you use. Same as in the render config file\n    # (VRay, Mental Ray, Arnold)\n    Renderer = attr.ib()  # type: str\n\n    # Version you want to render with. (5.11, 2010, 12)\n    Version = attr.ib()  # type: str\n\n    # Name of the scene file with full path.\n    SceneName = attr.ib()  # type: str\n\n    # Is the job enabled for submission?\n    # enabled by default\n    IsActive = attr.ib()  # type: bool\n\n    # Sequence settings of this job\n    SeqStart = attr.ib()  # type: int\n    SeqEnd = attr.ib()  # type: int\n    SeqStep = attr.ib()  # type: int\n    SeqFileOffset = attr.ib()  # type: int\n\n    # If you specify ImageDir, then ImageFilename has no path. If you do\n    # NOT specify ImageDir, then ImageFilename has to include the path.\n    # Same for ImageExtension.\n    # Important: Do not forget any _ or . in front or after the frame\n    # numbering. Usually ImageExtension always starts with a . (.tga, .exr)\n    ImageDir = attr.ib()  # type: str\n    ImageFilename = attr.ib()  # type: str\n    ImageExtension = attr.ib()  # type: str\n\n    # Some applications always add a . or _ in front of the frame number.\n    # Set this variable to that character. The user can then change\n    # the filename at the rrSubmitter and the submitter keeps\n    # track of this character.\n    ImagePreNumberLetter = attr.ib()  # type: str\n\n    # If you render a single file, e.g. Quicktime or Avi, then you have to\n    # set this value. Videos have to be rendered at once on one client.\n    ImageSingleOutputFile = attr.ib(default=False)  # type: bool\n\n    # Semi-Required (required for some render applications)\n    # -----------------------------------------------------\n\n    # The database of your scene file. In Maya and XSI called \"project\",\n    # in Lightwave \"content dir\"\n    SceneDatabaseDir = attr.ib(default=None)  # type: str\n\n    # Required if you want to split frames on multiple clients\n    ImageWidth = attr.ib(default=None)  # type: int\n    ImageHeight = attr.ib(default=None)  # type: int\n    Camera = attr.ib(default=None)  # type: str\n    Layer = attr.ib(default=None)  # type: str\n    Channel = attr.ib(default=None)  # type: str\n\n    # Optional\n    # --------\n\n    # Used for the RR render license function.\n    # E.g. If you render with mentalRay, then add mentalRay. If you render\n    # with Nuke and you use Furnace plugins in your comp, add Furnace.\n    # TODO: determine how this work for multiple plugins\n    RequiredPlugins = attr.ib(default=None)  # type: str\n\n    # Frame Padding of the frame number in the rendered filename.\n    # Some render config files are setting the padding at render time.\n    ImageFramePadding = attr.ib(default=None)  # type: int\n\n    # Some render applications support overriding the image format at\n    # the render commandline.\n    OverrideImageFormat = attr.ib(default=None)  # type: str\n\n    # rrControl can display the name of additonal channels that are\n    # rendered. Each channel requires these two values. ChannelFilename\n    # contains the full path.\n    ChannelFilename = attr.ib(default=None)  # type: str\n    ChannelExtension = attr.ib(default=None)  # type: str\n\n    # A value between 0 and 255. Each job gets the Pre ID attached as small\n    # letter to the main ID. A new main ID is generated for every machine\n    # for every 5/1000s.\n    PreID = attr.ib(default=None)  # type: int\n\n    # When the job is received by the server, the server checks for other\n    # jobs send from this machine. If a job with the PreID was found, then\n    # this jobs waits for the other job. Note: This flag can be used multiple\n    # times to wait for multiple jobs.\n    WaitForPreIDs = attr.ib(factory=list)  # type: list\n\n    # List of submitter options per job\n    # list item must be of `SubmitterParameter` type\n    SubmitterParameters = attr.ib(factory=list)  # type: list\n\n    # List of Custom job attributes\n    # Royal Render support custom attributes in format <CustomFoo> or\n    # <CustomSomeOtherAttr>\n    # list item must be of `CustomAttribute` named tuple\n    CustomAttributes = attr.ib(factory=list)  # type: list\n\n    # This is used to hold command line arguments for Execute job\n    CustomAddCmdFlags = attr.ib(default=None)  # type: str\n\n    # Additional information for subsequent publish script and\n    # for better display in rrControl\n    UserName = attr.ib(default=None)  # type: str\n    CustomSeQName = attr.ib(default=None)  # type: str\n    CustomSHotName = attr.ib(default=None)  # type: str\n    CustomVersionName = attr.ib(default=None)  # type: str\n    CustomUserInfo = attr.ib(default=None)  # type: str\n    SubmitMachine = attr.ib(default=None)  # type: str\n    Color_ID = attr.ib(default=2)  # type: int\n    CompanyProjectName = attr.ib(default=None)  # type: str\n\n    RequiredLicenses = attr.ib(default=None)  # type: str\n\n    # Additional frame info\n    Priority = attr.ib(default=50)  # type: int\n    TotalFrames = attr.ib(default=None)  # type: int\n    Tiled = attr.ib(default=None)  # type: str\n\n    # Environment\n    # only used in RR 8.3 and newer\n    rrEnvList = attr.ib(default=None, type=str)  # type: str\n    rrEnvFile = attr.ib(default=None, type=str)  # type: str\n\n\nclass SubmitterParameter:\n    \"\"\"Wrapper for Submitter Parameters.\"\"\"\n    def __init__(self, parameter, *args):\n        # type: (str, list) -> None\n        self._parameter = parameter\n        self._values = args\n\n    def serialize(self):\n        # type: () -> str\n        \"\"\"Serialize submitter parameter as a string value.\n\n        This can be later on used as text node in job xml file.\n\n        Returns:\n            str: concatenated string of parameter values.\n\n        \"\"\"\n        return '\"{param}={val}\"'.format(\n            param=self._parameter, val=\"~\".join(self._values))\n\n\n@attr.s\nclass SubmitFile(object):\n    \"\"\"Class wrapping Royal Render submission XML file.\"\"\"\n\n    # Syntax version of the submission file.\n    syntax_version = attr.ib(default=\"6.0\")  # type: str\n\n    # Delete submission file after processing\n    DeleteXML = attr.ib(default=1)  # type: int\n\n    # List of the submitter options per job.\n    # list item must be of `SubmitterParameter` type\n    SubmitterParameters = attr.ib(factory=list)  # type: list\n\n    # List of the jobs in submission batch.\n    # list item must be of type `RRJob`\n    Jobs = attr.ib(factory=list)  # type: list\n\n    @staticmethod\n    def _process_submitter_parameters(parameters, dom, append_to):\n        # type: (list[SubmitterParameter], md.Document, md.Element) -> None\n        \"\"\"Take list of :class:`SubmitterParameter` and process it as XML.\n\n        This will take :class:`SubmitterParameter`, create XML element\n        for them and convert value to Royal Render compatible string\n        (options and values separated by ~)\n\n        Args:\n            parameters (list of SubmitterParameter): List of parameters.\n            dom (xml.dom.minidom.Document): XML Document\n            append_to (xml.dom.minidom.Element): Element to append to.\n\n        \"\"\"\n        for param in parameters:\n            if not isinstance(param, SubmitterParameter):\n                raise AttributeError(\n                    \"{} is not of type `SubmitterParameter`\".format(param))\n            xml_parameter = dom.createElement(\"SubmitterParameter\")\n            xml_parameter.appendChild(dom.createTextNode(param.serialize()))\n            append_to.appendChild(xml_parameter)\n\n    def serialize(self):\n        # type: () -> str\n        \"\"\"Return all data serialized as XML.\n\n        Returns:\n            str: XML data as string.\n\n        \"\"\"\n        def filter_data(a, v):\n            \"\"\"Skip private attributes.\"\"\"\n            if a.name.startswith(\"_\"):\n                return False\n            if v is None:\n                return False\n            return True\n\n        root = md.Document()\n        # root element: <RR_Job_File syntax_version=\"6.0\">\n        job_file = root.createElement('RR_Job_File')\n        job_file.setAttribute(\"syntax_version\", self.syntax_version)\n\n        # handle Submitter Parameters for batch\n        # <SubmitterParameter>foo=bar~baz~goo</SubmitterParameter>\n        self._process_submitter_parameters(\n            self.SubmitterParameters, root, job_file)\n        root.appendChild(job_file)\n        for job in self.Jobs:  # type: RRJob\n            if not isinstance(job, RRJob):\n                raise AttributeError(\n                    \"{} is not of type `SubmitterParameter`\".format(job))\n            xml_job = root.createElement(\"Job\")\n            # handle Submitter Parameters for job\n            self._process_submitter_parameters(\n                job.SubmitterParameters, root, xml_job\n            )\n            job_custom_attributes = job.CustomAttributes\n\n            serialized_job = attr.asdict(\n                job, dict_factory=OrderedDict, filter=filter_data)\n            serialized_job.pop(\"CustomAttributes\")\n            serialized_job.pop(\"SubmitterParameters\")\n            # we are handling `WaitForPreIDs` separately.\n            wait_pre_ids = serialized_job.pop(\"WaitForPreIDs\", [])\n\n            for custom_attr in job_custom_attributes:  # type: CustomAttribute\n                serialized_job[\"Custom{}\".format(\n                    custom_attr.name)] = custom_attr.value\n\n            for item, value in serialized_job.items():\n                xml_attr = root.createElement(item)\n                xml_attr.appendChild(\n                    root.createTextNode(str(value))\n                )\n                xml_job.appendChild(xml_attr)\n\n            # WaitForPreID - can be used multiple times\n            for pre_id in wait_pre_ids:\n                xml_attr = root.createElement(\"WaitForPreID\")\n                xml_attr.appendChild(\n                    root.createTextNode(str(pre_id))\n                )\n                xml_job.appendChild(xml_attr)\n\n            job_file.appendChild(xml_job)\n\n        return root.toprettyxml(indent=\"\\t\")\n"
  },
  {
    "path": "openpype/modules/royalrender/rr_root/README.md",
    "content": "## OpenPype RoyalRender integration plugins\n\n### Installation\n\nCopy content of this folder to your `RR_ROOT` (place where RoyalRender studio wide installation is)."
  },
  {
    "path": "openpype/modules/royalrender/rr_root/render_apps/_config/E05__Ayon__PublishJob.cfg",
    "content": "IconApp= E05__Ayon.png\nName= Ayon\nrendererName= Once\nVersion= 1\nVersion_Minor= 0\nType=Execute\nTYPEv9=Execute\nExecuteJobType=Once\n\n\n################################# [Windows] [Linux] [Osx] ##################################\n\n\nCommandLine=<envFileExecute <rrEnvFile>>\n\nCommandLine=<rrEnvLine>\n\n\n::win CommandLine= set   \"CUDA_VISIBLE_DEVICES=<GpuListC>\"\n::lx  CommandLine= setenv CUDA_VISIBLE_DEVICES <GpuListC>\n::osx CommandLine= setenv CUDA_VISIBLE_DEVICES <GpuListC>\n\n\nCommandLine=\n\t<SetEnvGlobal>\n\nCommandLine=\n\t<SetEnvSoft>\n\nCommandLine=\n\t<ResetExitCode>\n\nCommandLine= \"<Exe>\" --headless publish <Scene>\n\t--targets royalrender\n\t--targets farm\n\t<AdditionalCommandlineParam>\n    <CustomFlags>\n\nCommandLine=\n\t<CheckExitCode>\n\n\n\n################################## Render Settings ##################################\n\n\n\n################################## Submitter Settings ##################################\nStartMultipleInstances= 0~0\nSceneFileExtension= *.json\nAllowImageNameChange= 0\nAllowImageDirChange= 0\nSequenceDivide= 0~1\nPPSequenceCheck=0~0\nPPCreateSmallVideo=0~0\nPPCreateFullVideo=0~0\nAllowLocalRenderOut= 0~0\n\n\n################################## Client Settings ##################################\n\nIconApp=E05__Ayon.png\n\nlicenseFailLine=\n\nerrorSearchLine=\n\npermanentErrorSearchLine =\n\nFrozen_MinCoreUsage=0.3\nFrozen_Minutes=30\n"
  },
  {
    "path": "openpype/modules/royalrender/rr_root/render_apps/_config/E05__Ayon___global.inc",
    "content": "IconApp= E05__Ayon.png\nName= Ayon\n"
  },
  {
    "path": "openpype/modules/royalrender/rr_root/render_apps/_install_paths/Ayon.cfg",
    "content": "[Windows]\nExecutable= ayon_console.exe\nPath= OS; <ProgramFiles(x86)>\\Ayon\\*\\ayon_console.exe\nPath= 32; <ProgramFiles(x86)>\\Ayon\\*\\ayon_console.exe\n\n[Linux]\nExecutable= ayon_console\nPath= OS; /opt/ayon/*/ayon_console\n\n[Mac]\nExecutable= ayon_console\nPath= OS; /Applications/Ayon*/Content/MacOS/ayon_console\n"
  },
  {
    "path": "openpype/modules/royalrender/rr_root/render_apps/_prepost_scripts/Ayon_ayon_inject_envvar.cfg",
    "content": "# config file format version 7.0\n#\n# Author: Royal Render, Holger Schoenberger, Binary Alchemy\n#\n# Last change: v8.2.24\n#\n#\n#  Deletes this job from the rrServer queue\n#\n#  AuthStr is required in case anonymous does not have the right to delete jobs.\n#  Or if you have enabled \"Authorization is required for all connections\"\n#  AuthStr will not work via a router/remote connection\n#\n#\n################################## Identify script ##################################\nName= Ayon inject env var\n\nPrePostType=Pre\n\n# Optional flags: The following lines are disabled and set to the default value\n# AllowedForExecuteOnceJobs= false\n# AllowedForSingleOutput = true\nPrePostChecked= true\n# ExecutePerChannel = false\n# PrePostShowParamA= false\n# PrePostShowParamB= false\n# PrePostParamA= 100\n# PrePostParamB= 100\n\n\n##################################  [Windows] [Linux] [OSX]  ##################################\n\nCommandLine=\n\t<ResetExitCode>\n\n\nCommandLine=  <OsxApp \"<rrBin64>rrPythonconsole\" > \"<RR_DIR>render_apps/_prepost_scripts/ayon_inject_envvar.py\"  -jid <JID>\n\n\n\nCommandLine=\n\t<CheckExitCode> <FN>\n"
  },
  {
    "path": "openpype/modules/royalrender/rr_root/render_apps/_prepost_scripts/ayon_inject_envvar.py",
    "content": "import os\nimport sys\nimport json\nimport glob\nfrom datetime import datetime\nimport tempfile\nimport subprocess\nimport uuid\nimport argparse\n\nmodPath = rrGlobal.rrModPyrrPath()\nsys.path.append(modPath)\nimport libpyRR39 as rr\n\nlogs = []\n\n\nclass InjectEnvironment:\n    \"\"\"Creates rrEnv file.\n\n    RR evnList has limitation on 2000 characters, which might not be enough.\n    This script should be triggered by render jobs that were published from\n    Ayon, it uses .json metadata to parse context and required Ayon launch\n    environments to generate environment variable file for particular context.\n\n    This file is converted into rrEnv file.\n\n    Render job already points to non-existent location which got filled only\n    by this process. (Couldn't figure way how to attach new file to existing\n    job.)\n\n    Expected set environments on RR worker:\n    - AYON_SERVER_URL\n    - AYON_API_KEY - API key to Ayon server, most likely from service account\n    - AYON_EXECUTABLE_PATH - locally accessible path for `ayon_console`\n    (could be removed if it would be possible to have it in renderApps config\n    and to be accessible from there as there it is required for publish jobs).\n    - AYON_FILTER_ENVIRONMENTS - potential black list of unwanted environment\n    variables (separated by ';') - will be filtered out from created .rrEnv.\n\n    Ayon submission job must be adding this line to .xml submission file:\n    <SubmitterParameter>PPAyoninjectenvvar=1~1</SubmitterParameter>\n\n    Scripts logs into folder with metadata json - could be removed if there\n    is a way how to log into RR output.\n\n    \"\"\"\n\n    def __init__(self):\n        self.meta_dir = None\n\n    def inject(self):\n        logs.append(\"InjectEnvironment starting\")\n        meta_dir = self._get_metadata_dir()\n        self.meta_dir = meta_dir\n\n        envs = self._get_job_environments()\n        if not envs.get(\"AYON_RENDER_JOB\"):\n            logs.append(\"Not a ayon render job, skipping.\")\n            return\n\n        if not self._is_required_environment():\n            return\n\n        context = self._get_context()\n        logs.append(\"context {}\".format(context))\n\n        executable = self._get_executable()\n        logs.append(\"executable {}\".format(executable))\n\n        extracted_env = self._extract_environments(executable, context)\n\n        rrEnv_path = self._create_rrEnv(meta_dir, extracted_env)\n\n        logs.append(f\"InjectEnvironment ending, rrEnv file {rrEnv_path}\")\n\n    def _get_metadata_dir(self):\n        \"\"\"Get folder where metadata.json and renders should be produced.\"\"\"\n        job = self._get_job()\n        image_dir = job.imageDir\n        logs.append(f\"_get_metadata_dir::{image_dir}\")\n\n        return image_dir\n\n    def _is_required_environment(self):\n        if (not os.environ.get(\"AYON_API_KEY\") or\n                not os.path.exists(os.environ.get(\"AYON_EXECUTABLE_PATH\", \"\"))):\n            msg = (\"AYON_API_KEY and AYON_EXECUTABLE_PATHenv var must be set \"\n                   \"for Ayon jobs!\")\n            logs.append(msg)\n            return False\n        return True\n\n    def _get_context(self):\n        envs = self._get_job_environments()\n\n        return {\"project\": envs[\"AVALON_PROJECT\"],\n                \"asset\": envs[\"AVALON_ASSET\"],\n                \"task\": envs[\"AVALON_TASK\"],\n                \"app\": envs[\"AVALON_APP_NAME\"],\n                \"envgroup\": \"farm\"}\n\n    def _get_job(self):\n        logs.append(\"get_jobs\")\n        parser = argparse.ArgumentParser()\n        parser.add_argument(\"-jid\")\n        parser.add_argument(\"-authStr\")\n        args = parser.parse_args()\n\n        # print(\"Set up server and login info\")\n        tcp = rr._rrTCP(\"\")\n        tcp.setServer(rrGlobal.rrServer(), 7773)\n        # tcp.setLogin(args.authStr, \"\")\n        tcp.setLogin(\"\", \"\")\n        logs.append(\"jid:{}\".format(int(args.jid)))\n        if not tcp.jobList_GetInfo(int(args.jid)):\n            print(\"Error jobList_GetInfo: \" + tcp.errorMessage())\n            sys.exit()\n\n        return tcp.jobs.getJobSend(int(args.jid))\n\n    def _get_job_environments(self):\n        \"\"\"Gets environments set on job.\n\n        It seems that it is not possible to query \"rrEnvList\" on job directly,\n        it must be parsed from .json document.\n        \"\"\"\n        job = self._get_job()\n        env_list = job.customData_Str('rrEnvList')\n        envs = {}\n        for env in env_list.split(\"~~~\"):\n            key, value = env.split(\"=\")\n            envs[key] = value\n        return envs\n\n    def _get_executable(self):\n        # rr_python_utils.cache.get_rr_bin_folder()  # TODO maybe useful\n        return os.environ[\"AYON_EXECUTABLE_PATH\"]\n\n    def _get_launch_environments(self):\n        \"\"\" Enhances environemnt with required for Ayon to be launched.\"\"\"\n        job_envs = self._get_job_environments()\n        ayon_environment = {\n            \"AYON_SERVER_URL\": os.environ[\"AYON_SERVER_URL\"],\n            \"AYON_API_KEY\": os.environ[\"AYON_API_KEY\"],\n            \"AYON_BUNDLE_NAME\": job_envs[\"AYON_BUNDLE_NAME\"],\n        }\n        logs.append(\"Ayon launch environments:: {}\".format(ayon_environment))\n        environment = os.environ.copy()\n        environment.update(ayon_environment)\n        return environment\n\n    def _get_export_url(self):\n        \"\"\"Returns unique path with extracted env variables from Ayon.\"\"\"\n        temp_file_name = \"{}_{}.json\".format(\n            datetime.utcnow().strftime('%Y%m%d%H%M%S%f'),\n            str(uuid.uuid1())\n        )\n        export_url = os.path.join(tempfile.gettempdir(), temp_file_name)\n        return export_url\n\n    def _extract_environments(self, executable, context):\n        # tempfile.TemporaryFile cannot be used because of locking\n        export_url = self._get_export_url()\n\n        args = [\n            executable,\n            \"--headless\",\n            \"extractenvironments\",\n            export_url\n        ]\n\n        if all(context.values()):\n            for key, value in context.items():\n                args.extend([\"--{}\".format(key), value])\n\n        environments = self._get_launch_environments()\n\n        logs.append(\"Running:: {}\".format(args))\n        proc = subprocess.Popen(args, env=environments,\n                                stdout=subprocess.PIPE,\n                                stderr=subprocess.PIPE)\n        output, error = proc.communicate()\n\n        if not os.path.exists(export_url):\n            logs.append(\"output::{}\".format(output))\n            logs.append(\"error::{}\".format(error))\n            raise RuntimeError(\"Extract failed with {}\".format(error))\n\n        with open(export_url) as json_file:\n            return json.load(json_file)\n\n    def _create_rrEnv(self, meta_dir, extracted_env):\n        \"\"\"Create rrEnv.rrEnv file in metadata folder that render job points\"\"\"\n        filter_out = os.environ.get(\"AYON_FILTER_ENVIRONMENTS\")\n        filter_envs = set()\n        if filter_out:\n            filter_envs = set(filter_out.split(\";\"))\n\n        lines = []\n        for key, value in extracted_env.items():\n            if key in filter_envs:\n                continue\n\n            line = f\"{key} = {value}\"\n            lines.append(line)\n\n        rrenv_path = os.path.join(meta_dir, \"rrEnv.rrEnv\")\n        with open(rrenv_path, \"w\") as fp:\n            fp.writelines(s + '\\n' for s in lines)\n\n        return rrenv_path\n\n\nif __name__ == \"__main__\":\n    try:\n        injector = InjectEnvironment()\n        injector.inject()\n    except Exception as exp:\n        logs.append(f\"Error happened::{str(exp)}\")\n\n    tmpdir = injector.meta_dir\n    log_path = os.path.join(tmpdir, \"log.txt\")\n    with open(log_path, \"a\") as fp:\n        fp.writelines(s.replace(\"\\\\r\\\\n\", \"\\n\") + '\\n' for s in logs)\n"
  },
  {
    "path": "openpype/modules/settings_action.py",
    "content": "from openpype import AYON_SERVER_ENABLED\nfrom openpype.modules import OpenPypeModule, ITrayAction\n\n\nclass SettingsAction(OpenPypeModule, ITrayAction):\n    \"\"\"Action to show Settings tool.\"\"\"\n    name = \"settings\"\n    label = \"Studio Settings\"\n    admin_action = True\n\n    def initialize(self, _modules_settings):\n        # This action is always enabled\n        self.enabled = True\n        if AYON_SERVER_ENABLED:\n            self.enabled = False\n\n        # User role\n        # TODO should be changeable\n        self.user_role = \"manager\"\n\n        # Tray attributes\n        self.settings_window = None\n\n    def tray_init(self):\n        \"\"\"Initialization in tray implementation of ITrayAction.\"\"\"\n        self.create_settings_window()\n\n    def tray_exit(self):\n        # Close settings UI to remove settings lock\n        if self.settings_window:\n            self.settings_window.close()\n\n    def on_action_trigger(self):\n        \"\"\"Implementation for action trigger of ITrayAction.\"\"\"\n        self.show_settings_window()\n\n    def create_settings_window(self):\n        \"\"\"Initializa Settings Qt window.\"\"\"\n        if self.settings_window:\n            return\n        from openpype.tools.settings import MainWidget\n\n        self.settings_window = MainWidget(self.user_role, reset_on_show=False)\n        self.settings_window.trigger_restart.connect(self._on_trigger_restart)\n\n    def _on_trigger_restart(self):\n        self.manager.restart_tray()\n\n    def show_settings_window(self):\n        \"\"\"Show settings tool window.\n\n        Raises:\n            AssertionError: Window must be already created. Call\n                `create_settings_window` before calling this method.\n        \"\"\"\n        if not self.settings_window:\n            raise AssertionError(\"Window is not initialized.\")\n\n        # Store if was visible\n        was_visible = self.settings_window.isVisible()\n        was_minimized = self.settings_window.isMinimized()\n\n        # Show settings gui\n        self.settings_window.show()\n\n        if was_minimized:\n            self.settings_window.showNormal()\n\n        # Pull window to the front.\n        self.settings_window.raise_()\n        self.settings_window.activateWindow()\n\n        # Reset content if was not visible\n        if not was_visible and not was_minimized:\n            self.settings_window.reset()\n\n\nclass LocalSettingsAction(OpenPypeModule, ITrayAction):\n    \"\"\"Action to show Settings tool.\"\"\"\n    name = \"local_settings\"\n    label = \"Settings\"\n\n    def initialize(self, _modules_settings):\n        # This action is always enabled\n        self.enabled = True\n        if AYON_SERVER_ENABLED:\n            self.enabled = False\n\n        # Tray attributes\n        self.settings_window = None\n        self._first_trigger = True\n\n    def tray_init(self):\n        \"\"\"Initialization in tray implementation of ITrayAction.\"\"\"\n        self.create_settings_window()\n\n    def on_action_trigger(self):\n        \"\"\"Implementation for action trigger of ITrayAction.\"\"\"\n        self.show_settings_window()\n\n    def create_settings_window(self):\n        \"\"\"Initializa Settings Qt window.\"\"\"\n        if self.settings_window:\n            return\n        from openpype.tools.settings import LocalSettingsWindow\n        self.settings_window = LocalSettingsWindow()\n\n    def show_settings_window(self):\n        \"\"\"Show settings tool window.\n\n        Raises:\n            AssertionError: Window must be already created. Call\n                `create_settings_window` before calling this method.\n        \"\"\"\n        if not self.settings_window:\n            raise AssertionError(\"Window is not initialized.\")\n\n        # Store if was visible\n        was_visible = self.settings_window.isVisible()\n\n        # Show settings gui\n        self.settings_window.show()\n\n        # Pull window to the front.\n        self.settings_window.raise_()\n        self.settings_window.activateWindow()\n\n        # Do not reset if it's first trigger of action\n        if self._first_trigger:\n            self._first_trigger = False\n        elif not was_visible:\n            # Reset content if was not visible\n            self.settings_window.reset()\n"
  },
  {
    "path": "openpype/modules/shotgrid/README.md",
    "content": "## Shotgrid Module\n\n### Pre-requisites\n\nInstall and launch a [shotgrid leecher](https://github.com/Ellipsanime/shotgrid-leecher) server\n\n### Quickstart\n\nThe goal of this tutorial is to synchronize an already existing shotgrid project with OpenPype.\n\n- Activate the shotgrid module in the **system settings** and inform the shotgrid leecher server API url\n\n- Create a new OpenPype project with the **project manager**\n\n- Inform the shotgrid authentication infos (url, script name, api key) and the shotgrid project ID related to this OpenPype project in the **project settings**\n\n- Use the batch interface (Tray > shotgrid > Launch batch), select your project and click \"batch\"\n\n- You can now access your shotgrid entities within the **avalon launcher** and publish informations to shotgrid with **pyblish**\n"
  },
  {
    "path": "openpype/modules/shotgrid/__init__.py",
    "content": "from .shotgrid_module import (\n    ShotgridModule,\n)\n\n__all__ = (\"ShotgridModule\",)\n"
  },
  {
    "path": "openpype/modules/shotgrid/lib/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/modules/shotgrid/lib/const.py",
    "content": "MODULE_NAME = \"shotgrid\"\n"
  },
  {
    "path": "openpype/modules/shotgrid/lib/credentials.py",
    "content": "\nfrom urllib.parse import urlparse\n\nimport shotgun_api3\nfrom shotgun_api3.shotgun import AuthenticationFault\n\nfrom openpype.lib import OpenPypeSecureRegistry, OpenPypeSettingsRegistry\nfrom openpype.modules.shotgrid.lib.record import Credentials\n\n\ndef _get_shotgrid_secure_key(hostname, key):\n    \"\"\"Secure item key for entered hostname.\"\"\"\n    return f\"shotgrid/{hostname}/{key}\"\n\n\ndef _get_secure_value_and_registry(\n    hostname,\n    name,\n):\n    key = _get_shotgrid_secure_key(hostname, name)\n    registry = OpenPypeSecureRegistry(key)\n    return registry.get_item(name, None), registry\n\n\ndef get_shotgrid_hostname(shotgrid_url):\n\n    if not shotgrid_url:\n        raise Exception(\"Shotgrid url cannot be a null\")\n    valid_shotgrid_url = (\n        f\"//{shotgrid_url}\" if \"//\" not in shotgrid_url else shotgrid_url\n    )\n    return urlparse(valid_shotgrid_url).hostname\n\n\n# Credentials storing function (using keyring)\n\n\ndef get_credentials(shotgrid_url):\n    hostname = get_shotgrid_hostname(shotgrid_url)\n    if not hostname:\n        return None\n    login_value, _ = _get_secure_value_and_registry(\n        hostname,\n        Credentials.login_key_prefix(),\n    )\n    password_value, _ = _get_secure_value_and_registry(\n        hostname,\n        Credentials.password_key_prefix(),\n    )\n    return Credentials(login_value, password_value)\n\n\ndef save_credentials(login, password, shotgrid_url):\n    hostname = get_shotgrid_hostname(shotgrid_url)\n    _, login_registry = _get_secure_value_and_registry(\n        hostname,\n        Credentials.login_key_prefix(),\n    )\n    _, password_registry = _get_secure_value_and_registry(\n        hostname,\n        Credentials.password_key_prefix(),\n    )\n    clear_credentials(shotgrid_url)\n    login_registry.set_item(Credentials.login_key_prefix(), login)\n    password_registry.set_item(Credentials.password_key_prefix(), password)\n\n\ndef clear_credentials(shotgrid_url):\n    hostname = get_shotgrid_hostname(shotgrid_url)\n    login_value, login_registry = _get_secure_value_and_registry(\n        hostname,\n        Credentials.login_key_prefix(),\n    )\n    password_value, password_registry = _get_secure_value_and_registry(\n        hostname,\n        Credentials.password_key_prefix(),\n    )\n\n    if login_value is not None:\n        login_registry.delete_item(Credentials.login_key_prefix())\n\n    if password_value is not None:\n        password_registry.delete_item(Credentials.password_key_prefix())\n\n\n# Login storing function (using json)\n\n\ndef get_local_login():\n    reg = OpenPypeSettingsRegistry()\n    try:\n        return str(reg.get_item(\"shotgrid_login\"))\n    except Exception:\n        return None\n\n\ndef save_local_login(login):\n    reg = OpenPypeSettingsRegistry()\n    reg.set_item(\"shotgrid_login\", login)\n\n\ndef clear_local_login():\n    reg = OpenPypeSettingsRegistry()\n    reg.delete_item(\"shotgrid_login\")\n\n\ndef check_credentials(\n    login,\n    password,\n    shotgrid_url,\n):\n\n    if not shotgrid_url or not login or not password:\n        return False\n    try:\n        session = shotgun_api3.Shotgun(\n            shotgrid_url,\n            login=login,\n            password=password,\n        )\n        session.preferences_read()\n        session.close()\n    except AuthenticationFault:\n        return False\n    return True\n"
  },
  {
    "path": "openpype/modules/shotgrid/lib/record.py",
    "content": "\nclass Credentials:\n    login = None\n    password = None\n\n    def __init__(self, login, password) -> None:\n        super().__init__()\n        self.login = login\n        self.password = password\n\n    def is_empty(self):\n        return not (self.login and self.password)\n\n    @staticmethod\n    def login_key_prefix():\n        return \"login\"\n\n    @staticmethod\n    def password_key_prefix():\n        return \"password\"\n"
  },
  {
    "path": "openpype/modules/shotgrid/lib/settings.py",
    "content": "from openpype.settings import get_system_settings, get_project_settings\nfrom openpype.modules.shotgrid.lib.const import MODULE_NAME\n\n\ndef get_shotgrid_project_settings(project):\n    return get_project_settings(project).get(MODULE_NAME, {})\n\n\ndef get_shotgrid_settings():\n    return get_system_settings().get(\"modules\", {}).get(MODULE_NAME, {})\n\n\ndef get_shotgrid_servers():\n    return get_shotgrid_settings().get(\"shotgrid_settings\", {})\n\n\ndef get_leecher_backend_url():\n    return get_shotgrid_settings().get(\"leecher_backend_url\")\n"
  },
  {
    "path": "openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py",
    "content": "import pyblish.api\nfrom openpype.client.mongo import OpenPypeMongoConnection\n\n\nclass CollectShotgridEntities(pyblish.api.ContextPlugin):\n    \"\"\"Collect shotgrid entities according to the current context\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.499\n    label = \"Shotgrid entities\"\n\n    def process(self, context):\n\n        avalon_project = context.data.get(\"projectEntity\")\n        avalon_asset = context.data.get(\"assetEntity\")\n        avalon_task_name = context.data.get(\"task\")\n\n        self.log.info(avalon_project)\n        self.log.info(avalon_asset)\n\n        sg_project = _get_shotgrid_project(context)\n        sg_task = _get_shotgrid_task(\n            avalon_project,\n            avalon_asset,\n            avalon_task_name\n        )\n        sg_entity = _get_shotgrid_entity(avalon_project, avalon_asset)\n\n        if sg_project:\n            context.data[\"shotgridProject\"] = sg_project\n            self.log.info(\n                \"Collected correspondig shotgrid project : {}\".format(\n                    sg_project\n                )\n            )\n\n        if sg_task:\n            context.data[\"shotgridTask\"] = sg_task\n            self.log.info(\n                \"Collected correspondig shotgrid task : {}\".format(sg_task)\n            )\n\n        if sg_entity:\n            context.data[\"shotgridEntity\"] = sg_entity\n            self.log.info(\n                \"Collected correspondig shotgrid entity : {}\".format(sg_entity)\n            )\n\n    def _find_existing_version(self, code, context):\n\n        filters = [\n            [\"project\", \"is\", context.data.get(\"shotgridProject\")],\n            [\"sg_task\", \"is\", context.data.get(\"shotgridTask\")],\n            [\"entity\", \"is\", context.data.get(\"shotgridEntity\")],\n            [\"code\", \"is\", code],\n        ]\n\n        sg = context.data.get(\"shotgridSession\")\n        return sg.find_one(\"Version\", filters, [])\n\n\ndef _get_shotgrid_collection(project):\n    client = OpenPypeMongoConnection.get_mongo_client()\n    return client.get_database(\"shotgrid_openpype\").get_collection(project)\n\n\ndef _get_shotgrid_project(context):\n    shotgrid_project_id = context.data[\"project_settings\"].get(\n        \"shotgrid_project_id\")\n    if shotgrid_project_id:\n        return {\"type\": \"Project\", \"id\": shotgrid_project_id}\n    return {}\n\n\ndef _get_shotgrid_task(avalon_project, avalon_asset, avalon_task):\n    sg_col = _get_shotgrid_collection(avalon_project[\"name\"])\n    shotgrid_task_hierarchy_row = sg_col.find_one(\n        {\n            \"type\": \"Task\",\n            \"_id\": {\"$regex\": \"^\" + avalon_task + \"_[0-9]*\"},\n            \"parent\": {\"$regex\": \".*,\" + avalon_asset[\"name\"] + \",\"},\n        }\n    )\n    if shotgrid_task_hierarchy_row:\n        return {\"type\": \"Task\", \"id\": shotgrid_task_hierarchy_row[\"src_id\"]}\n    return {}\n\n\ndef _get_shotgrid_entity(avalon_project, avalon_asset):\n    sg_col = _get_shotgrid_collection(avalon_project[\"name\"])\n    shotgrid_entity_hierarchy_row = sg_col.find_one(\n        {\"_id\": avalon_asset[\"name\"]}\n    )\n    if shotgrid_entity_hierarchy_row:\n        return {\n            \"type\": shotgrid_entity_hierarchy_row[\"type\"],\n            \"id\": shotgrid_entity_hierarchy_row[\"src_id\"],\n        }\n    return {}\n"
  },
  {
    "path": "openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py",
    "content": "import os\n\nimport pyblish.api\nimport shotgun_api3\nfrom shotgun_api3.shotgun import AuthenticationFault\n\nfrom openpype.lib import OpenPypeSettingsRegistry\nfrom openpype.modules.shotgrid.lib.settings import (\n    get_shotgrid_servers,\n    get_shotgrid_project_settings,\n)\n\n\nclass CollectShotgridSession(pyblish.api.ContextPlugin):\n    \"\"\"Collect shotgrid session using user credentials\"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = \"Shotgrid user session\"\n\n    def process(self, context):\n\n        certificate_path = os.getenv(\"SHOTGUN_API_CACERTS\")\n        if certificate_path is None or not os.path.exists(certificate_path):\n            self.log.info(\n                \"SHOTGUN_API_CACERTS does not contains a valid \\\n                path: {}\".format(\n                    certificate_path\n                )\n            )\n            certificate_path = get_shotgrid_certificate()\n            self.log.info(\"Get Certificate from shotgrid_api\")\n\n        if not os.path.exists(certificate_path):\n            self.log.error(\n                \"Could not find certificate in shotgun_api3: \\\n                {}\".format(\n                    certificate_path\n                )\n            )\n            return\n\n        set_shotgrid_certificate(certificate_path)\n        self.log.info(\"Set Certificate: {}\".format(certificate_path))\n\n        avalon_project = os.getenv(\"AVALON_PROJECT\")\n\n        shotgrid_settings = get_shotgrid_project_settings(avalon_project)\n        self.log.info(\"shotgrid settings: {}\".format(shotgrid_settings))\n        shotgrid_servers_settings = get_shotgrid_servers()\n        self.log.info(\n            \"shotgrid_servers_settings: {}\".format(shotgrid_servers_settings)\n        )\n\n        shotgrid_server = shotgrid_settings.get(\"shotgrid_server\", \"\")\n        if not shotgrid_server:\n            self.log.error(\n                \"No Shotgrid server found, please choose a credential\"\n                \"in script name and script key in OpenPype settings\"\n            )\n\n        shotgrid_server_setting = shotgrid_servers_settings.get(\n            shotgrid_server, {}\n        )\n        shotgrid_url = shotgrid_server_setting.get(\"shotgrid_url\", \"\")\n\n        shotgrid_script_name = shotgrid_server_setting.get(\n            \"shotgrid_script_name\", \"\"\n        )\n        shotgrid_script_key = shotgrid_server_setting.get(\n            \"shotgrid_script_key\", \"\"\n        )\n        if not shotgrid_script_name and not shotgrid_script_key:\n            self.log.error(\n                \"No Shotgrid api credential found, please enter \"\n                \"script name and script key in OpenPype settings\"\n            )\n\n        login = get_login() or os.getenv(\"OPENPYPE_SG_USER\")\n\n        if not login:\n            self.log.error(\n                \"No Shotgrid login found, please \"\n                \"login to shotgrid withing openpype Tray\"\n            )\n\n        # Set OPENPYPE_SG_USER with login so other deadline tasks can make\n        # use of it\n        self.log.info(\"Setting OPENPYPE_SG_USER to '%s'.\", login)\n        os.environ[\"OPENPYPE_SG_USER\"] = login\n\n        session = shotgun_api3.Shotgun(\n            base_url=shotgrid_url,\n            script_name=shotgrid_script_name,\n            api_key=shotgrid_script_key,\n            sudo_as_login=login,\n        )\n\n        try:\n            session.preferences_read()\n        except AuthenticationFault:\n            raise ValueError(\n                \"Could not connect to shotgrid {} with user {}\".format(\n                    shotgrid_url, login\n                )\n            )\n\n        self.log.info(\n            \"Logged to shotgrid {} with user {}\".format(shotgrid_url, login)\n        )\n        context.data[\"shotgridSession\"] = session\n        context.data[\"shotgridUser\"] = login\n\n\ndef get_shotgrid_certificate():\n    shotgun_api_path = os.path.dirname(shotgun_api3.__file__)\n    return os.path.join(shotgun_api_path, \"lib\", \"certifi\", \"cacert.pem\")\n\n\ndef set_shotgrid_certificate(certificate):\n    os.environ[\"SHOTGUN_API_CACERTS\"] = certificate\n\n\ndef get_login():\n    reg = OpenPypeSettingsRegistry()\n    try:\n        return str(reg.get_item(\"shotgrid_login\"))\n    except Exception:\n        return None\n"
  },
  {
    "path": "openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py",
    "content": "import os\nimport pyblish.api\n\nfrom openpype.pipeline.publish import get_publish_repre_path\n\n\nclass IntegrateShotgridPublish(pyblish.api.InstancePlugin):\n    \"\"\"\n    Create published Files from representations and add it to version. If\n    representation is tagged as shotgrid review, it will add it in\n    path to movie for a movie file or path to frame for an image sequence.\n    \"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.499\n    label = \"Shotgrid Published Files\"\n\n    def process(self, instance):\n\n        context = instance.context\n\n        self.sg = context.data.get(\"shotgridSession\")\n\n        shotgrid_version = instance.data.get(\"shotgridVersion\")\n\n        for representation in instance.data.get(\"representations\", []):\n\n            local_path = get_publish_repre_path(\n                instance, representation, False\n            )\n\n            if representation.get(\"tags\", []):\n                continue\n\n            code = os.path.basename(local_path)\n            published_file = self._find_existing_publish(\n                code, context, shotgrid_version\n            )\n\n            published_file_data = {\n                \"project\": context.data.get(\"shotgridProject\"),\n                \"code\": code,\n                \"entity\": context.data.get(\"shotgridEntity\"),\n                \"task\": context.data.get(\"shotgridTask\"),\n                \"version\": shotgrid_version,\n                \"path\": {\"local_path\": local_path},\n            }\n            if not published_file:\n                published_file = self._create_published(published_file_data)\n                self.log.info(\n                    \"Create Shotgrid PublishedFile: {}\".format(published_file)\n                )\n            else:\n                self.sg.update(\n                    published_file[\"type\"],\n                    published_file[\"id\"],\n                    published_file_data,\n                )\n                self.log.info(\n                    \"Update Shotgrid PublishedFile: {}\".format(published_file)\n                )\n\n            if instance.data[\"family\"] == \"image\":\n                self.sg.upload_thumbnail(\n                    published_file[\"type\"], published_file[\"id\"], local_path\n                )\n            instance.data[\"shotgridPublishedFile\"] = published_file\n\n    def _find_existing_publish(self, code, context, shotgrid_version):\n\n        filters = [\n            [\"project\", \"is\", context.data.get(\"shotgridProject\")],\n            [\"task\", \"is\", context.data.get(\"shotgridTask\")],\n            [\"entity\", \"is\", context.data.get(\"shotgridEntity\")],\n            [\"version\", \"is\", shotgrid_version],\n            [\"code\", \"is\", code],\n        ]\n        return self.sg.find_one(\"PublishedFile\", filters, [])\n\n    def _create_published(self, published_file_data):\n\n        return self.sg.create(\"PublishedFile\", published_file_data)\n"
  },
  {
    "path": "openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import get_publish_repre_path\n\n\nclass IntegrateShotgridVersion(pyblish.api.InstancePlugin):\n    \"\"\"Integrate Shotgrid Version\"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.497\n    label = \"Shotgrid Version\"\n\n    sg = None\n\n    def process(self, instance):\n\n        context = instance.context\n        self.sg = context.data.get(\"shotgridSession\")\n\n        # TODO: Use path template solver to build version code from settings\n        anatomy = instance.data.get(\"anatomyData\", {})\n        code = \"_\".join(\n            [\n                anatomy[\"project\"][\"code\"],\n                anatomy[\"parent\"],\n                anatomy[\"asset\"],\n                anatomy[\"task\"][\"name\"],\n                \"v{:03}\".format(int(anatomy[\"version\"])),\n            ]\n        )\n\n        version = self._find_existing_version(code, context)\n\n        if not version:\n            version = self._create_version(code, context)\n            self.log.info(\"Create Shotgrid version: {}\".format(version))\n        else:\n            self.log.info(\"Use existing Shotgrid version: {}\".format(version))\n\n        data_to_update = {}\n        intent = context.data.get(\"intent\")\n        if intent:\n            data_to_update[\"sg_status_list\"] = intent[\"value\"]\n\n        for representation in instance.data.get(\"representations\", []):\n            local_path = get_publish_repre_path(\n                instance, representation, False\n            )\n\n            if \"shotgridreview\" in representation.get(\"tags\", []):\n\n                if representation[\"ext\"] in [\"mov\", \"avi\"]:\n                    self.log.info(\n                        \"Upload review: {} for version shotgrid {}\".format(\n                            local_path, version.get(\"id\")\n                        )\n                    )\n                    self.sg.upload(\n                        \"Version\",\n                        version.get(\"id\"),\n                        local_path,\n                        field_name=\"sg_uploaded_movie\",\n                    )\n\n                    data_to_update[\"sg_path_to_movie\"] = local_path\n\n                elif representation[\"ext\"] in [\"jpg\", \"png\", \"exr\", \"tga\"]:\n                    path_to_frame = local_path.replace(\"0000\", \"#\")\n                    data_to_update[\"sg_path_to_frames\"] = path_to_frame\n\n        self.log.info(\"Update Shotgrid version with {}\".format(data_to_update))\n        self.sg.update(\"Version\", version[\"id\"], data_to_update)\n\n        instance.data[\"shotgridVersion\"] = version\n\n    def _find_existing_version(self, code, context):\n\n        filters = [\n            [\"project\", \"is\", context.data.get(\"shotgridProject\")],\n            [\"sg_task\", \"is\", context.data.get(\"shotgridTask\")],\n            [\"entity\", \"is\", context.data.get(\"shotgridEntity\")],\n            [\"code\", \"is\", code],\n        ]\n        return self.sg.find_one(\"Version\", filters, [])\n\n    def _create_version(self, code, context):\n\n        version_data = {\n            \"project\": context.data.get(\"shotgridProject\"),\n            \"sg_task\": context.data.get(\"shotgridTask\"),\n            \"entity\": context.data.get(\"shotgridEntity\"),\n            \"code\": code,\n        }\n\n        return self.sg.create(\"Version\", version_data)\n"
  },
  {
    "path": "openpype/modules/shotgrid/plugins/publish/validate_shotgrid_user.py",
    "content": "import pyblish.api\nfrom openpype.pipeline.publish import ValidateContentsOrder\n\n\nclass ValidateShotgridUser(pyblish.api.ContextPlugin):\n    \"\"\"\n    Check if user is valid and have access to the project.\n    \"\"\"\n\n    label = \"Validate Shotgrid User\"\n    order = ValidateContentsOrder\n\n    def process(self, context):\n        sg = context.data.get(\"shotgridSession\")\n\n        login = context.data.get(\"shotgridUser\")\n        self.log.info(\"Login shotgrid set in OpenPype is {}\".format(login))\n        project = context.data.get(\"shotgridProject\")\n        self.log.info(\"Current shotgun project is {}\".format(project))\n\n        if not (login and sg and project):\n            raise KeyError()\n\n        user = sg.find_one(\"HumanUser\", [[\"login\", \"is\", login]], [\"projects\"])\n\n        self.log.info(user)\n        self.log.info(login)\n        user_projects_id = [p[\"id\"] for p in user.get(\"projects\", [])]\n        if not project.get(\"id\") in user_projects_id:\n            raise PermissionError(\n                \"Login {} don't have access to the project {}\".format(\n                    login, project\n                )\n            )\n\n        self.log.info(\n            \"Login {} have access to the project {}\".format(login, project)\n        )\n"
  },
  {
    "path": "openpype/modules/shotgrid/server/README.md",
    "content": "\n### Shotgrid server\n\nPlease refer to the external project that covers Openpype/Shotgrid communication:\n  - https://github.com/Ellipsanime/shotgrid-leecher\n"
  },
  {
    "path": "openpype/modules/shotgrid/shotgrid_module.py",
    "content": "import os\n\nfrom openpype.modules import (\n    OpenPypeModule,\n    ITrayModule,\n    IPluginPaths,\n)\n\nSHOTGRID_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass ShotgridModule(OpenPypeModule, ITrayModule, IPluginPaths):\n    leecher_manager_url = None\n    name = \"shotgrid\"\n    enabled = False\n    project_id = None\n    tray_wrapper = None\n\n    def initialize(self, modules_settings):\n        shotgrid_settings = modules_settings.get(self.name, dict())\n        self.enabled = shotgrid_settings.get(\"enabled\", False)\n        self.leecher_manager_url = shotgrid_settings.get(\n            \"leecher_manager_url\", \"\"\n        )\n\n    def connect_with_modules(self, enabled_modules):\n        pass\n\n    def get_global_environments(self):\n        return {\"PROJECT_ID\": self.project_id}\n\n    def get_plugin_paths(self):\n        return {\n            \"publish\": [\n                os.path.join(SHOTGRID_MODULE_DIR, \"plugins\", \"publish\")\n            ]\n        }\n\n    def get_launch_hook_paths(self):\n        return os.path.join(SHOTGRID_MODULE_DIR, \"hooks\")\n\n    def tray_init(self):\n        from .tray.shotgrid_tray import ShotgridTrayWrapper\n\n        self.tray_wrapper = ShotgridTrayWrapper(self)\n\n    def tray_start(self):\n        return self.tray_wrapper.validate()\n\n    def tray_exit(self, *args, **kwargs):\n        return self.tray_wrapper\n\n    def tray_menu(self, tray_menu):\n        return self.tray_wrapper.tray_menu(tray_menu)\n"
  },
  {
    "path": "openpype/modules/shotgrid/tests/shotgrid/lib/test_credentials.py",
    "content": "import pytest\nfrom assertpy import assert_that\n\nimport openpype.modules.shotgrid.lib.credentials as sut\n\n\ndef test_missing_shotgrid_url():\n    with pytest.raises(Exception) as ex:\n        # arrange\n        url = \"\"\n        # act\n        sut.get_shotgrid_hostname(url)\n        # assert\n        assert_that(ex).is_equal_to(\"Shotgrid url cannot be a null\")\n\n\ndef test_full_shotgrid_url():\n    # arrange\n    url = \"https://shotgrid.com/myinstance\"\n    # act\n    actual = sut.get_shotgrid_hostname(url)\n    # assert\n    assert_that(actual).is_not_empty()\n    assert_that(actual).is_equal_to(\"shotgrid.com\")\n\n\ndef test_incomplete_shotgrid_url():\n    # arrange\n    url = \"shotgrid.com/myinstance\"\n    # act\n    actual = sut.get_shotgrid_hostname(url)\n    # assert\n    assert_that(actual).is_not_empty()\n    assert_that(actual).is_equal_to(\"shotgrid.com\")\n"
  },
  {
    "path": "openpype/modules/shotgrid/tray/credential_dialog.py",
    "content": "import os\nfrom qtpy import QtCore, QtWidgets, QtGui\n\nfrom openpype import style\nfrom openpype import resources\nfrom openpype.modules.shotgrid.lib import settings, credentials\n\n\nclass CredentialsDialog(QtWidgets.QDialog):\n    SIZE_W = 450\n    SIZE_H = 200\n\n    _module = None\n    _is_logged = False\n    url_label = None\n    login_label = None\n    password_label = None\n    url_input = None\n    login_input = None\n    password_input = None\n    input_layout = None\n    login_button = None\n    buttons_layout = None\n    main_widget = None\n\n    login_changed = QtCore.Signal()\n\n    def __init__(self, module, parent=None):\n        super(CredentialsDialog, self).__init__(parent)\n\n        self._module = module\n        self._is_logged = False\n\n        self.setWindowTitle(\"OpenPype - Shotgrid Login\")\n\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n\n        self.setWindowFlags(\n            QtCore.Qt.WindowCloseButtonHint\n            | QtCore.Qt.WindowMinimizeButtonHint\n        )\n        self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))\n        self.setMaximumSize(QtCore.QSize(self.SIZE_W + 100, self.SIZE_H + 100))\n        self.setStyleSheet(style.load_stylesheet())\n\n        self.ui_init()\n\n    def ui_init(self):\n        self.url_label = QtWidgets.QLabel(\"Shotgrid server:\")\n        self.login_label = QtWidgets.QLabel(\"Login:\")\n        self.password_label = QtWidgets.QLabel(\"Password:\")\n\n        self.url_input = QtWidgets.QComboBox()\n        # self.url_input.setReadOnly(True)\n\n        self.login_input = QtWidgets.QLineEdit()\n        self.login_input.setPlaceholderText(\"login\")\n\n        self.password_input = QtWidgets.QLineEdit()\n        self.password_input.setPlaceholderText(\"password\")\n        self.password_input.setEchoMode(QtWidgets.QLineEdit.Password)\n\n        self.error_label = QtWidgets.QLabel(\"\")\n        self.error_label.setStyleSheet(\"color: red;\")\n        self.error_label.setWordWrap(True)\n        self.error_label.hide()\n\n        self.input_layout = QtWidgets.QFormLayout()\n        self.input_layout.setContentsMargins(10, 15, 10, 5)\n\n        self.input_layout.addRow(self.url_label, self.url_input)\n        self.input_layout.addRow(self.login_label, self.login_input)\n        self.input_layout.addRow(self.password_label, self.password_input)\n        self.input_layout.addRow(self.error_label)\n\n        self.login_button = QtWidgets.QPushButton(\"Login\")\n        self.login_button.setToolTip(\"Log in shotgrid instance\")\n        self.login_button.clicked.connect(self._on_shotgrid_login_clicked)\n\n        self.logout_button = QtWidgets.QPushButton(\"Logout\")\n        self.logout_button.setToolTip(\"Log out shotgrid instance\")\n        self.logout_button.clicked.connect(self._on_shotgrid_logout_clicked)\n\n        self.buttons_layout = QtWidgets.QHBoxLayout()\n        self.buttons_layout.addWidget(self.logout_button)\n        self.buttons_layout.addWidget(self.login_button)\n\n        self.main_widget = QtWidgets.QVBoxLayout(self)\n        self.main_widget.addLayout(self.input_layout)\n        self.main_widget.addLayout(self.buttons_layout)\n        self.setLayout(self.main_widget)\n\n    def show(self, *args, **kwargs):\n        super(CredentialsDialog, self).show(*args, **kwargs)\n        self._fill_shotgrid_url()\n        self._fill_shotgrid_login()\n\n    def _fill_shotgrid_url(self):\n        servers = settings.get_shotgrid_servers()\n\n        if servers:\n            for _, v in servers.items():\n                self.url_input.addItem(\"{}\".format(v.get('shotgrid_url')))\n            self._valid_input(self.url_input)\n            self.login_button.show()\n            self.logout_button.show()\n            enabled = True\n        else:\n            self.set_error(\"Ask your admin to add shotgrid server in settings\")\n            self._invalid_input(self.url_input)\n            self.login_button.hide()\n            self.logout_button.hide()\n            enabled = False\n\n        self.login_input.setEnabled(enabled)\n        self.password_input.setEnabled(enabled)\n\n    def _fill_shotgrid_login(self):\n        login = credentials.get_local_login()\n\n        if login:\n            self.login_input.setText(login)\n\n    def _clear_shotgrid_login(self):\n        self.login_input.setText(\"\")\n        self.password_input.setText(\"\")\n\n    def _on_shotgrid_login_clicked(self):\n        login = self.login_input.text().strip()\n        password = self.password_input.text().strip()\n        missing = []\n\n        if login == \"\":\n            missing.append(\"login\")\n            self._invalid_input(self.login_input)\n\n        if password == \"\":\n            missing.append(\"password\")\n            self._invalid_input(self.password_input)\n\n        url = self.url_input.currentText()\n        if url == \"\":\n            missing.append(\"url\")\n            self._invalid_input(self.url_input)\n\n        if len(missing) > 0:\n            self.set_error(\"You didn't enter {}\".format(\" and \".join(missing)))\n            return\n\n        # if credentials.check_credentials(\n        #     login=login,\n        #     password=password,\n        #     shotgrid_url=url,\n        # ):\n        credentials.save_local_login(\n            login=login\n        )\n        os.environ['OPENPYPE_SG_USER'] = login\n        self._on_login()\n\n        self.set_error(\"CANT LOGIN\")\n\n    def _on_shotgrid_logout_clicked(self):\n        credentials.clear_local_login()\n        del os.environ['OPENPYPE_SG_USER']\n        self._clear_shotgrid_login()\n        self._on_logout()\n\n    def set_error(self, msg):\n        self.error_label.setText(msg)\n        self.error_label.show()\n\n    def _on_login(self):\n        self._is_logged = True\n        self.login_changed.emit()\n        self._close_widget()\n\n    def _on_logout(self):\n        self._is_logged = False\n        self.login_changed.emit()\n\n    def _close_widget(self):\n        self.hide()\n\n    def _valid_input(self, input_widget):\n        input_widget.setStyleSheet(\"\")\n\n    def _invalid_input(self, input_widget):\n        input_widget.setStyleSheet(\"border: 1px solid red;\")\n\n    def login_with_credentials(\n        self, url, login, password\n    ):\n        verification = credentials.check_credentials(url, login, password)\n        if verification:\n            credentials.save_credentials(login, password, False)\n            self._module.set_credentials_to_env(login, password)\n            self.set_credentials(login, password)\n            self.login_changed.emit()\n        return verification\n"
  },
  {
    "path": "openpype/modules/shotgrid/tray/shotgrid_tray.py",
    "content": "import os\nimport webbrowser\n\nfrom qtpy import QtWidgets\n\nfrom openpype.modules.shotgrid.lib import credentials\nfrom openpype.modules.shotgrid.tray.credential_dialog import (\n    CredentialsDialog,\n)\n\n\nclass ShotgridTrayWrapper:\n    module = None\n    credentials_dialog = None\n    logged_user_label = None\n\n    def __init__(self, module):\n        self.module = module\n        self.credentials_dialog = CredentialsDialog(module)\n        self.credentials_dialog.login_changed.connect(self.set_login_label)\n        self.logged_user_label = QtWidgets.QAction(\"\")\n        self.logged_user_label.setDisabled(True)\n        self.set_login_label()\n\n    def show_batch_dialog(self):\n        if self.module.leecher_manager_url:\n            webbrowser.open(self.module.leecher_manager_url)\n\n    def show_connect_dialog(self):\n        self.show_credential_dialog()\n\n    def show_credential_dialog(self):\n        self.credentials_dialog.show()\n        self.credentials_dialog.activateWindow()\n        self.credentials_dialog.raise_()\n\n    def set_login_label(self):\n        login = credentials.get_local_login()\n        if login:\n            self.logged_user_label.setText(\"{}\".format(login))\n        else:\n            self.logged_user_label.setText(\n                \"No User logged in {0}\".format(login)\n            )\n\n    def tray_menu(self, tray_menu):\n        # Add login to user menu\n        menu = QtWidgets.QMenu(\"Shotgrid\", tray_menu)\n        show_connect_action = QtWidgets.QAction(\"Connect to Shotgrid\", menu)\n        show_connect_action.triggered.connect(self.show_connect_dialog)\n        menu.addAction(self.logged_user_label)\n        menu.addSeparator()\n        menu.addAction(show_connect_action)\n        tray_menu.addMenu(menu)\n\n        # Add manager to Admin menu\n        for m in tray_menu.findChildren(QtWidgets.QMenu):\n            if m.title() == \"Admin\":\n                shotgrid_manager_action = QtWidgets.QAction(\n                    \"Shotgrid manager\", menu\n                )\n                shotgrid_manager_action.triggered.connect(\n                    self.show_batch_dialog\n                )\n                m.addAction(shotgrid_manager_action)\n\n    def validate(self):\n        login = credentials.get_local_login()\n\n        if not login:\n            self.show_credential_dialog()\n        else:\n            os.environ[\"OPENPYPE_SG_USER\"] = login\n\n        return True\n"
  },
  {
    "path": "openpype/modules/slack/README.md",
    "content": "Slack notification for publishing\n---------------------------------\n\nThis module allows configuring profiles(when to trigger, for which combination of task, host and family)\nand templates(could contain {} placeholder, as \"{asset} published\").\n\nThese need to be configured in \n```Project settings > Slack > Publish plugins > Notification to Slack```\n\nSlack module must be enabled in System Setting, could be configured per Project.\n\n## App installation\n\nSlack app needs to be installed to company's workspace. Attached .yaml file could be\nused, follow instruction https://api.slack.com/reference/manifests#using\n\n## Settings\n\n### Token\nMost important for module to work is to fill authentication token \n```Project settings > Slack > Publish plugins > Token```\n\nThis token should be available after installation of app in Slack dashboard.\nIt is possible to create multiple tokens and configure different scopes for them.\n\n### Profiles\nProfiles are used to select when to trigger notification. One or multiple profiles\ncould be configured, 'family', 'task name' (regex available) and host combination is needed.\n\nEg. If I want to be notified when render is published from Maya, setting is:\n\n- family: 'render'\n- host: 'Maya'\n\n### Channel\nMessage could be delivered to one or multiple channels, by default app allows Slack bot\nto send messages to 'public' channels (eg. bot doesn't need to join channel first).\n\nThis could be configured in Slack dashboard and scopes might be modified.\n\n### Message\nPlaceholders {} could be used in message content which will be filled during runtime.\nOnly keys available in 'anatomyData' are currently implemented.\n\nExample of message content:\n```{SUBSET} for {Asset} was published.```\n\nIntegration can upload 'thumbnail' file (if present in instance), for that bot must be \nmanually added to target channel by Slack admin!\n(In target channel write: ```/invite @OpenPypeNotifier``)"
  },
  {
    "path": "openpype/modules/slack/__init__.py",
    "content": "from .slack_module import (\n    SlackIntegrationModule,\n    SLACK_MODULE_DIR\n)\n\n__all__ = (\n    \"SlackIntegrationModule\",\n    \"SLACK_MODULE_DIR\"\n)\n"
  },
  {
    "path": "openpype/modules/slack/launch_hooks/pre_python2_vendor.py",
    "content": "import os\nfrom openpype.lib.applications import PreLaunchHook\nfrom openpype_modules.slack import SLACK_MODULE_DIR\n\n\nclass PrePython2Support(PreLaunchHook):\n    \"\"\"Add python slack api module for Python 2 to PYTHONPATH.\n\n    Path to vendor modules is added to the beginning of PYTHONPATH.\n    \"\"\"\n    launch_types = set()\n\n    def execute(self):\n        if not self.application.use_python_2:\n            return\n\n        self.log.info(\"Adding Slack Python 2 packages to PYTHONPATH.\")\n\n        # Prepare vendor dir path\n        python_2_vendor = os.path.join(SLACK_MODULE_DIR, \"python2_vendor\")\n\n        # Add Python 2 modules\n        python_paths = [\n            # `python-ftrack-api`\n            os.path.join(python_2_vendor, \"python-slack-sdk-1\", \"slackclient\"),\n            os.path.join(python_2_vendor, \"python-slack-sdk-1\")\n        ]\n        self.log.info(\"python_paths {}\".format(python_paths))\n        # Load PYTHONPATH from current launch context\n        python_path = self.launch_context.env.get(\"PYTHONPATH\")\n        if python_path:\n            python_paths.append(python_path)\n\n        # Set new PYTHONPATH to launch context environments\n        self.launch_context.env[\"PYTHONPATH\"] = os.pathsep.join(python_paths)\n"
  },
  {
    "path": "openpype/modules/slack/manifest.yml",
    "content": "_metadata:\n  major_version: 1\n  minor_version: 1\ndisplay_information:\n  name: OpenPypeNotifier\nfeatures:\n  app_home:\n    home_tab_enabled: false\n    messages_tab_enabled: true\n    messages_tab_read_only_enabled: true\n  bot_user:\n    display_name: OpenPypeNotifier\n    always_online: false\noauth_config:\n  scopes:\n    bot:\n      - chat:write\n      - chat:write.customize\n      - chat:write.public\n      - files:write\n      - channels:read\nsettings:\n  org_deploy_enabled: false\n  socket_mode_enabled: false\n  is_hosted: false\n"
  },
  {
    "path": "openpype/modules/slack/plugins/publish/collect_slack_family.py",
    "content": "import pyblish.api\n\nfrom openpype.lib.profiles_filtering import filter_profiles\nfrom openpype.lib import attribute_definitions\nfrom openpype.pipeline import OpenPypePyblishPluginMixin\n\n\nclass CollectSlackFamilies(pyblish.api.InstancePlugin,\n                           OpenPypePyblishPluginMixin):\n    \"\"\"Collect family for Slack notification\n\n        Expects configured profile in\n        Project settings > Slack > Publish plugins > Notification to Slack\n\n        Add Slack family to those instance that should be messaged to Slack\n    \"\"\"\n    order = pyblish.api.CollectorOrder + 0.4999\n    label = 'Collect Slack family'\n\n    profiles = None\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            attribute_definitions.TextDef(\n                # Key under which it will be stored\n                \"additional_message\",\n                # Use plugin label as label for attribute\n                label=\"Additional Slack message\",\n                placeholder=\"<Only if Slack is configured>\"\n            )\n        ]\n\n    def process(self, instance):\n        task_data = instance.data[\"anatomyData\"].get(\"task\", {})\n        family = self.main_family_from_instance(instance)\n        key_values = {\n            \"families\": family,\n            \"tasks\": task_data.get(\"name\"),\n            \"task_types\": task_data.get(\"type\"),\n            \"hosts\": instance.context.data[\"hostName\"],\n            \"subsets\": instance.data[\"subset\"]\n        }\n        profile = filter_profiles(self.profiles, key_values,\n                                  logger=self.log)\n\n        if not profile:\n            self.log.info(\"No profile found, notification won't be send\")\n            return\n\n        # make slack publishable\n        if not profile:\n            return\n\n        self.log.info(\"Found profile: {}\".format(profile))\n        if instance.data.get('families'):\n            instance.data['families'].append('slack')\n        else:\n            instance.data['families'] = ['slack']\n\n        selected_profiles = profile[\"channel_messages\"]\n        for prof in selected_profiles:\n            prof[\"review_upload_limit\"] = profile.get(\"review_upload_limit\",\n                                                      50)\n        instance.data[\"slack_channel_message_profiles\"] = selected_profiles\n\n        slack_token = (instance.context.data[\"project_settings\"]\n                                            [\"slack\"]\n                                            [\"token\"])\n        instance.data[\"slack_token\"] = slack_token\n\n        attribute_values = self.get_attr_values_from_data(instance.data)\n        additional_message = attribute_values.get(\"additional_message\")\n        if additional_message:\n            instance.data[\"slack_additional_message\"] = additional_message\n\n    def main_family_from_instance(self, instance):  # TODO yank from integrate\n        \"\"\"Returns main family of entered instance.\"\"\"\n        family = instance.data.get(\"family\")\n        if not family:\n            family = instance.data[\"families\"][0]\n        return family\n"
  },
  {
    "path": "openpype/modules/slack/plugins/publish/integrate_slack_api.py",
    "content": "import os\nimport re\nimport six\nimport pyblish.api\nimport copy\nfrom datetime import datetime\nfrom abc import ABCMeta, abstractmethod\nimport time\n\nfrom openpype.client import OpenPypeMongoConnection\nfrom openpype.pipeline.publish import get_publish_repre_path\nfrom openpype.lib.plugin_tools import prepare_template_data\n\n\nclass IntegrateSlackAPI(pyblish.api.InstancePlugin):\n    \"\"\" Send message notification to a channel.\n        Triggers on instances with \"slack\" family, filled by\n        'collect_slack_family'.\n        Expects configured profile in\n        Project settings > Slack > Publish plugins > Notification to Slack.\n        If instance contains 'thumbnail' it uploads it. Bot must be present\n        in the target channel.\n        If instance contains 'review' it could upload (if configured) or place\n        link with {review_filepath} placeholder.\n        Message template can contain {} placeholders from anatomyData.\n    \"\"\"\n    order = pyblish.api.IntegratorOrder + 0.499\n    label = \"Integrate Slack Api\"\n    families = [\"slack\"]\n\n    optional = True\n\n    def process(self, instance):\n        thumbnail_path = self._get_thumbnail_path(instance)\n        review_path = self._get_review_path(instance)\n\n        publish_files = set()\n        message = ''\n        additional_message = instance.data.get(\"slack_additional_message\")\n        token = instance.data[\"slack_token\"]\n        if additional_message:\n            message = \"{} \\n\".format(additional_message)\n        users = groups = None\n        for message_profile in instance.data[\"slack_channel_message_profiles\"]:\n            message += self._get_filled_message(message_profile[\"message\"],\n                                                instance,\n                                                review_path)\n            if not message:\n                return\n\n            if message_profile[\"upload_thumbnail\"] and thumbnail_path:\n                publish_files.add(thumbnail_path)\n\n            if message_profile[\"upload_review\"] and review_path:\n                message, publish_files = self._handle_review_upload(\n                    message, message_profile, publish_files, review_path)\n\n            project = instance.context.data[\"anatomyData\"][\"project\"][\"code\"]\n            for channel in message_profile[\"channels\"]:\n                if six.PY2:\n                    client = SlackPython2Operations(token, self.log)\n                else:\n                    client = SlackPython3Operations(token, self.log)\n\n                if \"@\" in message:\n                    cache_key = \"__cache_slack_ids\"\n                    slack_ids = instance.context.data.get(cache_key, None)\n                    if slack_ids is None:\n                        users, groups = client.get_users_and_groups()\n                        instance.context.data[cache_key] = {}\n                        instance.context.data[cache_key][\"users\"] = users\n                        instance.context.data[cache_key][\"groups\"] = groups\n                    else:\n                        users = slack_ids[\"users\"]\n                        groups = slack_ids[\"groups\"]\n                    message = self._translate_users(message, users, groups)\n\n                msg_id, file_ids = client.send_message(channel,\n                                                       message,\n                                                       publish_files)\n                if not msg_id:\n                    return\n\n                msg = {\n                    \"type\": \"slack\",\n                    \"msg_id\": msg_id,\n                    \"file_ids\": file_ids,\n                    \"project\": project,\n                    \"created_dt\": datetime.now()\n                }\n                mongo_client = OpenPypeMongoConnection.get_mongo_client()\n                database_name = os.environ[\"OPENPYPE_DATABASE_NAME\"]\n                dbcon = mongo_client[database_name][\"notification_messages\"]\n                dbcon.insert_one(msg)\n\n    def _handle_review_upload(self, message, message_profile, publish_files,\n                              review_path):\n        \"\"\"Check if uploaded file is not too large\"\"\"\n        review_file_size_MB = os.path.getsize(review_path) / 1024 / 1024\n        file_limit = message_profile.get(\"review_upload_limit\", 50)\n        if review_file_size_MB > file_limit:\n            message += \"\\nReview upload omitted because of file size.\"\n            if review_path not in message:\n                message += \"\\nFile located at: {}\".format(review_path)\n        else:\n            publish_files.add(review_path)\n        return message, publish_files\n\n    def _get_filled_message(self, message_templ, instance, review_path=None):\n        \"\"\"Use message_templ and data from instance to get message content.\n\n        Reviews might be large, so allow only adding link to message instead of\n        uploading only.\n        \"\"\"\n\n        fill_data = copy.deepcopy(instance.context.data[\"anatomyData\"])\n\n        username = fill_data.get(\"user\")\n        fill_pairs = [\n            (\"asset\", instance.data.get(\"asset\", fill_data.get(\"asset\"))),\n            (\"subset\", instance.data.get(\"subset\", fill_data.get(\"subset\"))),\n            (\"user\", username),\n            (\"username\", username),\n            (\"app\", instance.data.get(\"app\", fill_data.get(\"app\"))),\n            (\"family\", instance.data.get(\"family\", fill_data.get(\"family\"))),\n            (\"version\", str(instance.data.get(\"version\",\n                                              fill_data.get(\"version\"))))\n        ]\n        if review_path:\n            fill_pairs.append((\"review_filepath\", review_path))\n\n        task_data = (\n                copy.deepcopy(instance.data.get(\"anatomyData\", {})).get(\"task\")\n                or fill_data.get(\"task\")\n        )\n        if not isinstance(task_data, dict):\n            # fallback for legacy - if task_data is only task name\n            task_data[\"name\"] = task_data\n        if task_data:\n            if (\n                \"{task}\" in message_templ\n                or \"{Task}\" in message_templ\n                or \"{TASK}\" in message_templ\n            ):\n                fill_pairs.append((\"task\", task_data[\"name\"]))\n\n            else:\n                for key, value in task_data.items():\n                    fill_key = \"task[{}]\".format(key)\n                    fill_pairs.append((fill_key, value))\n\n        multiple_case_variants = prepare_template_data(fill_pairs)\n        fill_data.update(multiple_case_variants)\n        message = ''\n        try:\n            message = self._escape_missing_keys(message_templ, fill_data).\\\n                format(**fill_data)\n        except Exception:\n            # shouldn't happen\n            self.log.warning(\n                \"Some keys are missing in {}\".format(message_templ),\n                exc_info=True)\n\n        return message\n\n    def _get_thumbnail_path(self, instance):\n        \"\"\"Returns abs url for thumbnail if present in instance repres\"\"\"\n        thumbnail_path = None\n        for repre in instance.data.get(\"representations\", []):\n            if repre.get('thumbnail') or \"thumbnail\" in repre.get('tags', []):\n                repre_thumbnail_path = get_publish_repre_path(\n                    instance, repre, False\n                )\n                if os.path.exists(repre_thumbnail_path):\n                    thumbnail_path = repre_thumbnail_path\n                break\n        return thumbnail_path\n\n    def _get_review_path(self, instance):\n        \"\"\"Returns abs url for review if present in instance repres\"\"\"\n        review_path = None\n        for repre in instance.data.get(\"representations\", []):\n            tags = repre.get('tags', [])\n            if (repre.get(\"review\")\n                    or \"review\" in tags\n                    or \"burnin\" in tags):\n                repre_review_path = get_publish_repre_path(\n                    instance, repre, False\n                )\n                if repre_review_path and os.path.exists(repre_review_path):\n                    review_path = repre_review_path\n                if \"burnin\" in tags:  # burnin has precedence if exists\n                    break\n        return review_path\n\n    def _get_user_id(self, users, user_name):\n        \"\"\"Returns internal slack id for user name\"\"\"\n        user_id = None\n        user_name_lower = user_name.lower()\n        for user in users:\n            if (not user.get(\"deleted\") and\n                    (user_name_lower == user[\"name\"].lower() or\n                     # bots dont have display_name\n                     user_name_lower == user[\"profile\"].get(\"display_name\",\n                                                            '').lower() or\n                     user_name_lower == user[\"profile\"].get(\"real_name\",\n                                                            '').lower())):\n                user_id = user[\"id\"]\n                break\n        return user_id\n\n    def _get_group_id(self, groups, group_name):\n        \"\"\"Returns internal group id for string name\"\"\"\n        group_id = None\n        for group in groups:\n            if (not group.get(\"date_delete\") and\n                    (group_name.lower() == group[\"name\"].lower() or\n                     group_name.lower() == group[\"handle\"])):\n                group_id = group[\"id\"]\n                break\n        return group_id\n\n    def _translate_users(self, message, users, groups):\n        \"\"\"Replace all occurences of @mentions with proper <@name> format.\"\"\"\n        matches = re.findall(r\"(?<!<)@\\S+\", message)\n        in_quotes = re.findall(r\"(?<!<)(['\\\"])(@[^'\\\"]+)\", message)\n        for item in in_quotes:\n            matches.append(item[1])\n        if not matches:\n            return message\n\n        for orig_user in matches:\n            user_name = orig_user.replace(\"@\", '')\n            slack_id = self._get_user_id(users, user_name)\n            mention = None\n            if slack_id:\n                mention = \"<@{}>\".format(slack_id)\n            else:\n                slack_id = self._get_group_id(groups, user_name)\n                if slack_id:\n                    mention = \"<!subteam^{}>\".format(slack_id)\n            if mention:\n                message = message.replace(orig_user, mention)\n\n        return message\n\n    def _escape_missing_keys(self, message, fill_data):\n        \"\"\"Double escapes placeholder which are missing in 'fill_data'\"\"\"\n        placeholder_keys = re.findall(r\"\\{([^}]+)\\}\", message)\n\n        fill_keys = []\n        for key, value in fill_data.items():\n            fill_keys.append(key)\n            if isinstance(value, dict):\n                for child_key in value.keys():\n                    fill_keys.append(\"{}[{}]\".format(key, child_key))\n\n        not_matched = set(placeholder_keys) - set(fill_keys)\n\n        for not_matched_item in not_matched:\n            message = message.replace(\"{}\".format(not_matched_item),\n                                      \"{{{}}}\".format(not_matched_item))\n\n        return message\n\n\n@six.add_metaclass(ABCMeta)\nclass AbstractSlackOperations:\n\n    @abstractmethod\n    def _get_users_list(self):\n        \"\"\"Return response with user list, different methods Python 2 vs 3\"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def _get_usergroups_list(self):\n        \"\"\"Return response with user list, different methods Python 2 vs 3\"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def get_users_and_groups(self):\n        \"\"\"Return users and groups, different retry in Python 2 vs 3\"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def send_message(self, channel, message, publish_files):\n        \"\"\"Sends message to channel, different methods in Python 2 vs 3\"\"\"\n        pass\n\n    def _get_users(self):\n        \"\"\"Parse users.list response into list of users (dicts)\"\"\"\n        first = True\n        next_page = None\n        users = []\n        while first or next_page:\n            response = self._get_users_list()\n            first = False\n            next_page = response.get(\"response_metadata\").get(\"next_cursor\")\n            for user in response.get(\"members\"):\n                users.append(user)\n\n        return users\n\n    def _get_groups(self):\n        \"\"\"Parses usergroups.list response into list of groups (dicts)\"\"\"\n        response = self._get_usergroups_list()\n        groups = []\n        for group in response.get(\"usergroups\"):\n            groups.append(group)\n        return groups\n\n    def _enrich_error(self, error_str, channel):\n        \"\"\"Enhance known errors with more helpful notations.\"\"\"\n        if 'not_in_channel' in error_str:\n            # there is no file.write.public scope, app must be explicitly in\n            # the channel\n            msg = \" - application must added to channel '{}'.\".format(channel)\n            error_str += msg + \" Ask Slack admin.\"\n        return error_str\n\n\nclass SlackPython3Operations(AbstractSlackOperations):\n\n    def __init__(self, token, log):\n        from slack_sdk import WebClient\n\n        self.client = WebClient(token=token)\n        self.log = log\n\n    def _get_users_list(self):\n        return self.client.users_list()\n\n    def _get_usergroups_list(self):\n        return self.client.usergroups_list()\n\n    def get_users_and_groups(self):\n        from slack_sdk.errors import SlackApiError\n        while True:\n            try:\n                users = self._get_users()\n                groups = self._get_groups()\n                break\n            except SlackApiError as e:\n                retry_after = e.response.headers.get(\"Retry-After\")\n                if retry_after:\n                    print(\n                        \"Rate limit hit, sleeping for {}\".format(retry_after))\n                    time.sleep(int(retry_after))\n                else:\n                    self.log.warning(\"Cannot pull user info, \"\n                                     \"mentions won't work\", exc_info=True)\n                    return [], []\n            except Exception:\n                self.log.warning(\"Cannot pull user info, \"\n                                 \"mentions won't work\", exc_info=True)\n                return [], []\n\n        return users, groups\n\n    def send_message(self, channel, message, publish_files):\n        from slack_sdk.errors import SlackApiError\n        try:\n            attachment_str = \"\\n\\n Attachment links: \\n\"\n            file_ids = []\n            for published_file in publish_files:\n                response = self.client.files_upload(\n                    file=published_file,\n                    filename=os.path.basename(published_file))\n                attachment_str += \"\\n<{}|{}>\".format(\n                    response[\"file\"][\"permalink\"],\n                    os.path.basename(published_file))\n                file_ids.append(response[\"file\"][\"id\"])\n\n            if publish_files:\n                message += attachment_str\n\n            response = self.client.chat_postMessage(\n                channel=channel,\n                text=message\n            )\n            return response.data[\"ts\"], file_ids\n        except SlackApiError as e:\n            # # You will get a SlackApiError if \"ok\" is False\n            if e.response.get(\"error\"):\n                error_str = self._enrich_error(str(e.response[\"error\"]), channel)\n            else:\n                error_str = self._enrich_error(str(e), channel)\n            self.log.warning(\"Error happened: {}\".format(error_str),\n                             exc_info=True)\n        except Exception as e:\n            error_str = self._enrich_error(str(e), channel)\n            self.log.warning(\"Not SlackAPI error\", exc_info=True)\n\n        return None, []\n\n\nclass SlackPython2Operations(AbstractSlackOperations):\n\n    def __init__(self, token, log):\n        from slackclient import SlackClient\n\n        self.client = SlackClient(token=token)\n        self.log = log\n\n    def _get_users_list(self):\n        return self.client.api_call(\"users.list\")\n\n    def _get_usergroups_list(self):\n        return self.client.api_call(\"usergroups.list\")\n\n    def get_users_and_groups(self):\n        while True:\n            try:\n                users = self._get_users()\n                groups = self._get_groups()\n                break\n            except Exception:\n                self.log.warning(\"Cannot pull user info, \"\n                                 \"mentions won't work\", exc_info=True)\n                return [], []\n\n        return users, groups\n\n    def send_message(self, channel, message, publish_files):\n        try:\n            attachment_str = \"\\n\\n Attachment links: \\n\"\n            file_ids = []\n            for p_file in publish_files:\n                with open(p_file, 'rb') as pf:\n                    response = self.client.api_call(\n                        \"files.upload\",\n                        file=pf,\n                        channel=channel,\n                        title=os.path.basename(p_file)\n                    )\n                    if response.get(\"error\"):\n                        error_str = self._enrich_error(\n                            str(response.get(\"error\")),\n                            channel)\n                        self.log.warning(\n                            \"Error happened: {}\".format(error_str))\n                    else:\n                        attachment_str += \"\\n<{}|{}>\".format(\n                            response[\"file\"][\"permalink\"],\n                            os.path.basename(p_file))\n                        file_ids.append(response[\"file\"][\"id\"])\n\n            if publish_files:\n                message += attachment_str\n\n            response = self.client.api_call(\n                \"chat.postMessage\",\n                channel=channel,\n                text=message\n            )\n            if response.get(\"error\"):\n                error_str = self._enrich_error(str(response.get(\"error\")),\n                                               channel)\n                self.log.warning(\"Error happened: {}\".format(error_str),\n                                 exc_info=True)\n            else:\n                return response[\"ts\"], file_ids\n        except Exception as e:\n            # You will get a SlackApiError if \"ok\" is False\n            error_str = self._enrich_error(str(e), channel)\n            self.log.warning(\"Error happened: {}\".format(error_str),\n                             exc_info=True)\n\n        return None, []\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml",
    "content": "# credit: https://packaging.python.org/guides/supporting-windows-using-appveyor/\n\nenvironment:\n  matrix:\n    - PYTHON: \"C:\\\\Python27\"\n      PYTHON_VERSION: \"py27-x86\"\n    - PYTHON: \"C:\\\\Python34\"\n      PYTHON_VERSION: \"py34-x86\"\n    - PYTHON: \"C:\\\\Python35\"\n      PYTHON_VERSION: \"py35-x86\"\n    - PYTHON: \"C:\\\\Python27-x64\"\n      PYTHON_VERSION: \"py27-x64\"\n    - PYTHON: \"C:\\\\Python34-x64\"\n      PYTHON_VERSION: \"py34-x64\"\n    - PYTHON: \"C:\\\\Python35-x64\"\n      PYTHON_VERSION: \"py35-x64\"\n\ninstall:\n  - \"%PYTHON%\\\\python.exe -m pip install wheel\"\n  - \"%PYTHON%\\\\python.exe -m pip install -r requirements.txt\"\n  - \"%PYTHON%\\\\python.exe -m pip install flake8\"\n  - \"%PYTHON%\\\\python.exe -m pip install -r test_requirements.txt\"\n\nbuild: off\n\ntest_script:\n  - \"%PYTHON%\\\\python.exe -m flake8 slackclient\"\n  - \"%PYTHON%\\\\python.exe -m pytest --cov-report= --cov=slackclient tests\"\n\n# maybe `after_test:`?\non_success:\n  - \"%PYTHON%\\\\python.exe -m codecov -e win-%PYTHON_VERSION%\""
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/.coveragerc",
    "content": "[run]\nbranch = True\nsource = slackclient\n\n[report]\nexclude_lines =\n    if self.debug:\n    pragma: no cover\n    raise NotImplementedError\n    if __name__ == .__main__.:\nignore_errors = True\nomit =\n    tests/*"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/.flake8",
    "content": "[flake8]\nmax-line-length = 100"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md",
    "content": "# Contributors Guide\n\nInterested in contributing? Awesome! Before you do though, please read our\n[Code of Conduct](https://slackhq.github.io/code-of-conduct). We take it very seriously, and expect that you will as\nwell.\n\nThere are many ways you can contribute! :heart:\n\n### Bug Reports and Fixes :bug:\n-  If you find a bug, please search for it in the [Issues](https://github.com/slackapi/python-slackclient/issues), and if it isn't already tracked,\n   [create a new issue](https://github.com/slackapi/python-slackclient/issues/new). Fill out the \"Bug Report\" section of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still\n   be reviewed.\n-  Issues that have already been identified as a bug (note: able to reproduce) will be labelled `bug`.\n-  If you'd like to submit a fix for a bug, [send a Pull Request](#creating_a_pull_request) and mention the Issue number.\n  -  Include tests that isolate the bug and verifies that it was fixed.\n\n### New Features :bulb:\n-  If you'd like to add new functionality to this project, describe the problem you want to solve in a [new Issue](https://github.com/slackapi/python-slackclient/issues/new).\n-  Issues that have been identified as a feature request will be labelled `enhancement`.\n-  If you'd like to implement the new feature, please wait for feedback from the project\n   maintainers before spending too much time writing the code. In some cases, `enhancement`s may\n   not align well with the project objectives at the time.\n\n### Tests :mag:, Documentation :books:, Miscellaneous :sparkles:\n-  If you'd like to improve the tests, you want to make the documentation clearer, you have an\n   alternative implementation of something that may have advantages over the way its currently\n   done, or you have any other change, we would be happy to hear about it!\n  -  If its a trivial change, go ahead and [send a Pull Request](#creating_a_pull_request) with the changes you have in mind.\n  -  If not, [open an Issue](https://github.com/slackapi/python-slackclient/issues/new) to discuss the idea first.\n\nIf you're new to our project and looking for some way to make your first contribution, look for\nIssues labelled `good first contribution`.\n\n## Requirements\n\nFor your contribution to be accepted:\n\n- [x] You must have signed the [Contributor License Agreement (CLA)](https://cla-assistant.io/slackapi/python-slackclient).\n- [x] The test suite must be complete and pass.\n- [x] The changes must be approved by code review.\n- [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number.\n\nIf the contribution doesn't meet the above criteria, you may fail our automated checks or a maintainer will discuss it with you. You can continue to improve a Pull Request by adding commits to the branch from which the PR was created.\n\n[Interested in knowing more about about pull requests at Slack?](https://slack.engineering/on-empathy-pull-requests-979e4257d158#.awxtvmb2z)\n\n## Creating a Pull Request\n\n1.  :fork_and_knife: Fork the repository on GitHub.\n2.  :runner: Clone/fetch your fork to your local development machine. It's a good idea to run the tests just\n    to make sure everything is in order.\n3.  :herb: Create a new branch and check it out.\n4.  :crystal_ball: Make your changes and commit them locally. Magic happens here!\n5.  :arrow_heading_up: Push your new branch to your fork. (e.g. `git push username fix-issue-16`).\n6.  :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `master` in this\n    repository.\n\n## Maintainers\n\nThere are more details about processes and workflow in the [Maintainer's Guide](./maintainers_guide.md)."
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md",
    "content": "### Description\n\nDescribe your issue here.\n\n### What type of issue is this? (place an `x` in one of the `[ ]`)\n- [ ] bug\n- [ ] enhancement (feature request)\n- [ ] question\n- [ ] documentation related\n- [ ] testing related\n- [ ] discussion\n\n### Requirements (place an `x` in each of the `[ ]`)\n* [ ] I've read and understood the [Contributing guidelines](https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md) and have done my best effort to follow them.\n* [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).\n* [ ] I've searched for any related issues and avoided creating a duplicate issue.\n\n---\n\n### Bug Report\n\nFilling out the following details about bugs will help us solve your issue sooner.\n\n#### Reproducible in:\n\nslackclient version:\n\npython version:\n\nOS version(s):\n\n#### Steps to reproduce:\n\n1.\n2.\n3.\n\n#### Expected result:\n\nWhat you expected to happen\n\n#### Actual result:\n\nWhat actually happened\n\n#### Attachments:\n\nLogs, screenshots, screencast, sample project, funny gif, etc."
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md",
    "content": "# Maintainers Guide\n\nThis document describes tools, tasks and workflow that one needs to be familiar with in order to effectively maintain\nthis project. If you use this package within your own software as is but don't plan on modifying it, this guide is\n**not** for you.\n\n## Tools\n\n### Python (and friends)\n\nNot surprisingly, you will need to have Python installed on your system to work on this package. We support non-EOL,\nstable versions of CPython. The current supported versions are listed in the CI configurations (e.g. `.travis.yml`).\nAt a minimum, you should have the latest version of Python 2 and the latest version of Python 3 to develop against.\nIt's tricky to set up a system that has more than that, so you can lean on the CI servers to test changes on the\nin-between versions for you.\n\nYou should also make sure you have the latest versions of `pip`, `setuptools`, `virtualenv`, `wheel`, `twine` and\n[`tox`](https://tox.readthedocs.io/en/latest/) installed with your version of Python.\n\nOn macOS, the easiest way to install these tools is by using [Homebrew](https://brew.sh/) and installing the `python`\nand `python3` packages. Some of the above packages are preinstalled and you can install the remaining on your own:\n`pip install virtualenv wheel twine tox && pip3 install virtualenv twine tox`.\n\n## Tasks\n\n### Testing\n\nTox is used to run the test suite across multiple isolated versions of Python. It is configured in `tox.ini` to\nrun all the supported versions of Python, but when you invoke it, you should only select the versions you have on your\nsystem. For example, on a system with Python 2.7.13 and Python 3.6.1, you would run the tests using the following\ncommand: `tox -e flake8,py27,py36` (flake8 is a quality analysis tool).\n\n### Generating Documentation\n\nThe documentation is generated from the source and templates in the `docs-src` directory. The generated documentation\ngets committed to the repo in `docs` and also published to a GitHub Pages website.\n\nYou can generate the documentation by running `tox -e docs`.\n\n### Releasing\n\n1.  Create the commit for the release:\n    *  Bump the version number in adherence to [Semantic Versioning](http://semver.org/) in `slackclient/version.py`.\n    *  Commit with a message including the new version number. For example `1.0.6`.\n\n2.  Distribute the release\n    *  Build the distribtuions: `python setup.py sdist bdist_wheel`. This will create artifacts in the `dist` directory.\n    *  Publish to PyPI: `twine upload dist/*`. You must have access to the credentials to publish.\n    *  Create a GitHub Release. You will select the commit with updated version number (e.g. `1.0.6`) to assiociate with\n       the tag, and name the tag after this version (e.g. `1.0.6`). This will also serve as a Changelog for the project.\n       Add a description of changes to the Release. Mention Issue and PR #'s and @-mention contributors.\n\n3.  (Slack Internal) Communicate the release internally. Include a link to the GitHub Release.\n\n4.  Announce on Slack Team dev4slack in #slack-api\n\n5.  (Slack Internal) Tweet? Not necessary for patch updates, might be needed for minor updates, definitely needed for\n    major updates. Include a link to the GitHub Release.\n\n## Workflow\n\n### Versioning and Tags\n\nThis project uses semantic versioning, expressed through the numbering scheme of\n[PEP-0440](https://www.python.org/dev/peps/pep-0440/).\n\n### Branches\n\n`master` is where active development occurs. Long running named feature branches are occasionally created for\ncollaboration on a feature that has a large scope (because everyone cannot push commits to another person's open Pull\nRequest). At some point in the future after a major version increment, there may be maintenance branches for older major\nversions.\n\n### Issue Management\n\nLabels are used to run issues through an organized workflow. Here are the basic definitions:\n\n*  `bug`: A confirmed bug report. A bug is considered confirmed when reproduction steps have been\n   documented and the issue has been reproduced.\n*  `enhancement`: A feature request for something this package might not already do.\n*  `docs`: An issue that is purely about documentation work.\n*  `tests`: An issue that is purely about testing work.\n*  `needs feedback`: An issue that may have claimed to be a bug but was not reproducible, or was otherwise missing some information.\n*  `discussion`: An issue that is purely meant to hold a discussion. Typically the maintainers are looking for feedback in this issues.\n*  `question`: An issue that is like a support request because the user's usage was not correct.\n*  `semver:major|minor|patch`: Metadata about how resolving this issue would affect the version number.\n*  `security`: An issue that has special consideration for security reasons.\n*  `good first contribution`: An issue that has a well-defined relatively-small scope, with clear expectations. It helps when the testing approach is also known.\n*  `duplicate`: An issue that is functionally the same as another issue. Apply this only if you've linked the other issue by number.\n\n**Triage** is the process of taking new issues that aren't yet \"seen\" and marking them with a basic level of information\nwith labels. An issue should have **one** of the following labels applied: `bug`, `enhancement`, `question`,\n`needs feedback`, `docs`, `tests`, or `discussion`.\n\nIssues are closed when a resolution has been reached. If for any reason a closed issue seems relevant once again,\nreopening is great and better than creating a duplicate issue.\n\n## Everything else\n\nWhen in doubt, find the other maintainers and ask."
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md",
    "content": "###  Summary\n\nDescribe the goal of this PR. Mention any related Issue numbers.\n\n### Requirements (place an `x` in each `[ ]`)\n\n* [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md) and have done my best effort to follow them.\n* [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct)."
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/.gitignore",
    "content": "# general things to ignore\nbuild/\ndist/\ndocs/_sources/\ndocs/.doctrees\n*.egg-info/\n*.egg\n*.py[cod]\n__pycache__/\n*.so\n*~\n\n# virtualenv\nenv/\nvenv/\n\n# codecov / coverage\n.coverage\ncov_*\n\n# due to using tox and pytest\n.tox\n.cache\n.pytest_cache/\n.python-version\npip"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/.travis.yml",
    "content": "sudo: false\ndist: trusty\nlanguage: python\npython:\n  - \"2.7\"\n  - \"3.4\"\n  - \"3.5\"\n  - \"3.6\"\ninstall:\n  - travis_retry pip install -r requirements.txt\n  - travis_retry pip install flake8\n  - travis_retry pip install -r test_requirements.txt\nscript:\n  - flake8 slackclient\n  - py.test --cov-report= --cov=slackclient tests\nafter_success:\n  - codecov -e $TRAVIS_PYTHON_VERSION\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015-2016 Slack Technologies, Inc\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in",
    "content": "include LICENSE\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/README.rst",
    "content": "python-slackclient\n===================\n\nA client for Slack, which supports the Slack Web API and Real Time Messaging (RTM) API.\n\n|build-status| |windows-build-status| |codecov| |doc-status| |pypi-version| |python-version|\n\n.. |build-status| image:: https://travis-ci.org/slackapi/python-slackclient.svg?branch=master\n    :target: https://travis-ci.org/slackapi/python-slackclient\n.. |windows-build-status| image:: https://ci.appveyor.com/api/projects/status/rif04t60ptslj32x/branch/master?svg=true\n    :target: https://ci.appveyor.com/project/slackapi/python-slackclient\n.. |codecov| image:: https://codecov.io/gh/slackapi/python-slackclient/branch/master/graph/badge.svg\n    :target: https://codecov.io/gh/slackapi/python-slackclient\n.. |doc-status| image:: https://readthedocs.org/projects/python-slackclient/badge/?version=latest\n    :target: http://python-slackclient.readthedocs.io/en/latest/?badge=latest\n.. |pypi-version| image:: https://badge.fury.io/py/slackclient.svg\n    :target: https://pypi.python.org/pypi/slackclient\n.. |python-version| image:: https://img.shields.io/pypi/pyversions/slackclient.svg\n    :target: https://pypi.python.org/pypi/slackclient\n\nOverview\n--------\n\nWhether you're building a custom app for your team, or integrating a third party\nservice into your Slack workflows, Slack Developer Kit for Python allows you to leverage the flexibility\nof Python to get your project up and running as quickly as possible.\n\nDocumentation\n***************\n\nFor comprehensive method information and usage examples, see the `full documentation <http://slackapi.github.io/python-slackclient>`_.\n\nIf you're building a project to receive content and events from Slack, check out the `Python Slack Events API Adapter <https://github.com/slackapi/python-slack-events-api/>`_ library.\n\nYou may also review our `Development Roadmap <https://github.com/slackapi/python-slackclient/wiki/Slack-Python-SDK-Roadmap>`_ in the project wiki.\n\n\nRequirements and Installation\n******************************\n\nWe recommend using `PyPI <https://pypi.python.org/pypi>`_ to install Slack Developer Kit for Python\n\n.. code-block:: bash\n\n\tpip install slackclient\n\nOf course, if you prefer doing things the hard way, you can always implement Slack Developer Kit for Python\nby pulling down the source code directly into your project:\n\n.. code-block:: bash\n\n\tgit clone https://github.com/slackapi/python-slackclient.git\n\tpip install -r requirements.txt\n\nGetting Help\n*************\n\nIf you get stuck, we’re here to help. The following are the best ways to get assistance working through your issue:\n\n- Use our `Github Issue Tracker <https://github.com/slackapi/python-slackclient/issues>`_ for reporting bugs or requesting features.\n- Visit the `Bot Developer Hangout <http://community.botkit.ai>`_ for getting help using Slack Developer Kit for Python or just generally bond with your fellow Slack developers.\n\nBasic Usage\n------------\nThe Slack Web API allows you to build applications that interact with Slack in more complex ways than the integrations\nwe provide out of the box.\n\nThis package is a modular wrapper designed to make Slack `Web API <https://api.slack.com/web>`_ calls simpler and easier for your\napp. Provided below are examples of how to interact with commonly used API endpoints, but this is by no means\na complete list. Review the full list of available methods `here <https://api.slack.com/methods>`_.\n\n\nSending a message\n********************\nThe primary use of Slack is sending messages. Whether you're sending a message\nto a user or to a channel, this method handles both.\n\nTo send a message to a channel, use the channel's ID. For IMs, use the user's ID.\n\n.. code-block:: python\n\n  import os\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"chat.postMessage\",\n    channel=\"C0XXXXXX\",\n    text=\"Hello from Python! :tada:\"\n  )\n\nThere are some unique options specific to sending IMs, so be sure to read the **channels**\nsection of the `chat.postMessage <https://api.slack.com/methods/chat.postMessage#channels>`_\npage for a full list of formatting and authorship options.\n\nSending an ephemeral message, which is only visible to an assigned user in a specified channel, is nearly the same\nas sending a regular message, but with an additional ``user`` parameter.\n\n.. code-block:: python\n\n  import os\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"chat.postEphemeral\",\n    channel=\"C0XXXXXX\",\n    text=\"Hello from Python! :tada:\",\n    user=\"U0XXXXXXX\"\n  )\n\nSee `chat.postEphemeral <https://api.slack.com/methods/chat.postEphemeral>`_ for more info.\n\n\nReplying to messages and creating threads\n*****************************************\nThreaded messages are just like regular messages, except thread replies are grouped together to provide greater context\nto the user. You can reply to a thread or start a new threaded conversation by simply passing the original message's ``ts``\nID in the ``thread_ts`` attribute when posting a message. If you're replying to a threaded message, you'll pass the `thread_ts`\nID of the message you're replying to.\n\nA channel or DM conversation is a nearly linear timeline of messages exchanged between people, bots, and apps.\nWhen one of these messages is replied to, it becomes the parent of a thread. By default, threaded replies do not\nappear directly in the channel, instead relegated to a kind of forked timeline descending from the parent message.\n\n.. code-block:: python\n\n  import os\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"chat.postMessage\",\n    channel=\"C0XXXXXX\",\n    text=\"Hello from Python! :tada:\",\n    thread_ts=\"1476746830.000003\"\n  )\n\n\nBy default, ``reply_broadcast`` is set to ``False``. To indicate your reply is germane to all members of a channel,\nset the ``reply_broadcast`` boolean parameter to ``True``.\n\n.. code-block:: python\n\n  import os\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"chat.postMessage\",\n    channel=\"C0XXXXXX\",\n    text=\"Hello from Python! :tada:\",\n    thread_ts=\"1476746830.000003\",\n    reply_broadcast=True\n  )\n\n\n**Note:** While threaded messages may contain attachments and message buttons, when your reply is broadcast to the\nchannel, it'll actually be a reference to your reply, not the reply itself.\nSo, when appearing in the channel, it won't contain any attachments or message buttons. Also note that updates and\ndeletion of threaded replies works the same as regular messages.\n\nSee the `Threading messages together <https://api.slack.com/docs/message-threading#forking_conversations>`_\narticle for more information.\n\n\nDeleting a message\n********************\nSometimes you need to delete things.\n\n.. code-block:: python\n\n  import os\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"chat.delete\",\n    channel=\"C0XXXXXX\",\n    ts=\"1476745373.000002\"\n  )\n\nSee `chat.delete <https://api.slack.com/methods/chat.delete>`_ for more info.\n\nAdding or removing an emoji reaction\n****************************************\nYou can quickly respond to any message on Slack with an emoji reaction. Reactions\ncan be used for any purpose: voting, checking off to-do items, showing excitement — and just for fun.\n\nThis method adds a reaction (emoji) to an item (``file``, ``file comment``, ``channel message``, ``group message``, or ``direct message``). One of file, file_comment, or the combination of channel and timestamp must be specified.\n\n.. code-block:: python\n\n  import os\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"reactions.add\",\n    channel=\"C0XXXXXXX\",\n    name=\"thumbsup\",\n    timestamp=\"1234567890.123456\"\n  )\n\nRemoving an emoji reaction is basically the same format, but you'll use ``reactions.remove`` instead of ``reactions.add``\n\n.. code-block:: python\n\n  sc.api_call(\n    \"reactions.remove\",\n    channel=\"C0XXXXXXX\",\n    name=\"thumbsup\",\n    timestamp=\"1234567890.123456\"\n  )\n\n\nSee `reactions.add <https://api.slack.com/methods/reactions.add>`_ and `reactions.remove <https://api.slack.com/methods/reactions.remove>`_ for more info.\n\nGetting a list of channels\n******************************\nAt some point, you'll want to find out what channels are available to your app. This is how you get that list.\n\n**Note:** This call requires the ``channels:read`` scope.\n\n.. code-block:: python\n\n  sc.api_call(\"channels.list\")\n\nArchived channels are included by default. You can exclude them by passing ``exclude_archived=1`` to your request.\n\n.. code-block:: python\n\n  sc.api_call(\n    \"channels.list\",\n    exclude_archived=1\n  )\n\nSee `channels.list <https://api.slack.com/methods/channels.list>`_ for more info.\n\nGetting a channel's info\n*************************\nOnce you have the ID for a specific channel, you can fetch information about that channel.\n\n.. code-block:: python\n\n  sc.api_call(\n    \"channels.info\",\n    channel=\"C0XXXXXXX\"\n  )\n\nSee `channels.info <https://api.slack.com/methods/channels.info>`_ for more info.\n\nJoining a channel\n********************\nChannels are the social hub of most Slack teams. Here's how you hop into one:\n\n.. code-block:: python\n\n  sc.api_call(\n    \"channels.join\",\n    channel=\"C0XXXXXXY\"\n  )\n\nIf you are already in the channel, the response is slightly different.\n``already_in_channel`` will be true, and a limited ``channel`` object will be returned. Bot users cannot join a channel on their own, they need to be invited by another user.\n\nSee `channels.join <https://api.slack.com/methods/channels.join>`_ for more info.\n\nLeaving a channel\n********************\nMaybe you've finished up all the business you had in a channel, or maybe you\njoined one by accident. This is how you leave a channel.\n\n.. code-block:: python\n\n  sc.api_call(\n    \"channels.leave\",\n    channel=\"C0XXXXXXX\"\n  )\n\nSee `channels.leave <https://api.slack.com/methods/channels.leave>`_ for more info.\n\n\nTokens and Authentication\n**************************\n\nThe simplest way to create an instance of the client, as shown in the samples above, is to use a bot (xoxb) access token:\n\n.. code-block:: python\n\n  # Get the access token from environmental variable\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n\nThe SlackClient library allows you to use a variety of Slack authentication tokens.\n\nTo take advantage of automatic token refresh, you'll need to instantiate the client a little differently than when using\na bot access token. With a bot token, you have the access (xoxb) token when you create the client, when using refresh tokens,\nyou won't know the access token when the client is created.\n\nUpon the first request, the SlackClient will request a new access (xoxa) token on behalf of your application, using your app's\nrefresh token, client ID, and client secret.\n\n.. code-block:: python\n\n    # Get the access token from environmental variable\n    slack_refresh_token = os.environ[\"SLACK_REFRESH_TOKEN\"]\n    slack_client_id = os.environ[\"SLACK_CLIENT_ID\"]\n    slack_client_secret = os.environ[\"SLACK_CLIENT_SECRET\"]\n\n\nSince your app's access tokens will be expiring and refreshed, the client requires a callback method to be passed in on creation of the client.\nOnce Slack returns an access token for your app, the SlackClient will call your provided callback to update the access token in your datastore.\n\n.. code-block:: python\n\n    # This is where you'll add your data store update logic\n    def token_update_callback(update_data):\n        print(\"Enterprise ID: {}\".format(update_data[\"enterprise_id\"]))\n        print(\"Workspace ID: {}\".format(update_data[\"team_id\"]))\n        print(\"Access Token: {}\".format(update_data[\"access_token\"]))\n        print(\"Access Token expires in (ms): {}\".format(update_data[\"expires_in\"]))\n\n    # When creating an instance of the client, pass the client details and token update callback\n    sc = SlackClient(\n      refresh_token=slack_refresh_token,\n      client_id=slack_client_id,\n      client_secret=slack_client_secret,\n      token_update_callback=token_update_callback\n    )\n\n\nSlack will send your callback function the **app's access token**, **token expiration TTL**, **team ID**, and **enterprise ID** (for enterprise workspaces)\n\n\nSee `Tokens & Authentication <http://slackapi.github.io/python-slackclient/auth.html#handling-tokens>`_ for API token handling best practices.\n\n\n\nAdditional Information\n********************************************************************************************\nFor comprehensive method information and usage examples, see the `full documentation`_.\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo",
    "content": "# Sphinx build info version 1\n# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.\nconfig: 6abbc33d255b00e789666fcb765fbf2d\ntags: 645f666f9bcd5a90fca523b33c5a78b7\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.nojekyll",
    "content": ""
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css",
    "content": "/*\n * basic.css\n * ~~~~~~~~~\n *\n * Sphinx stylesheet -- basic theme.\n *\n * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.\n * :license: BSD, see LICENSE for details.\n *\n */\n\n/* -- main layout ----------------------------------------------------------- */\n\ndiv.clearer {\n    clear: both;\n}\n\n/* -- relbar ---------------------------------------------------------------- */\n\ndiv.related {\n    width: 100%;\n    font-size: 90%;\n}\n\ndiv.related h3 {\n    display: none;\n}\n\ndiv.related ul {\n    margin: 0;\n    padding: 0 0 0 10px;\n    list-style: none;\n}\n\ndiv.related li {\n    display: inline;\n}\n\ndiv.related li.right {\n    float: right;\n    margin-right: 5px;\n}\n\n/* -- sidebar --------------------------------------------------------------- */\n\ndiv.sphinxsidebarwrapper {\n    padding: 10px 5px 0 10px;\n}\n\ndiv.sphinxsidebar {\n    float: left;\n    width: 230px;\n    margin-left: -100%;\n    font-size: 90%;\n    word-wrap: break-word;\n    overflow-wrap : break-word;\n}\n\ndiv.sphinxsidebar ul {\n    list-style: none;\n}\n\ndiv.sphinxsidebar ul ul,\ndiv.sphinxsidebar ul.want-points {\n    margin-left: 20px;\n    list-style: square;\n}\n\ndiv.sphinxsidebar ul ul {\n    margin-top: 0;\n    margin-bottom: 0;\n}\n\ndiv.sphinxsidebar form {\n    margin-top: 10px;\n}\n\ndiv.sphinxsidebar input {\n    border: 1px solid #98dbcc;\n    font-family: sans-serif;\n    font-size: 1em;\n}\n\ndiv.sphinxsidebar #searchbox form.search {\n    overflow: hidden;\n}\n\ndiv.sphinxsidebar #searchbox input[type=\"text\"] {\n    float: left;\n    width: 80%;\n    padding: 0.25em;\n    box-sizing: border-box;\n}\n\ndiv.sphinxsidebar #searchbox input[type=\"submit\"] {\n    float: left;\n    width: 20%;\n    border-left: none;\n    padding: 0.25em;\n    box-sizing: border-box;\n}\n\n\nimg {\n    border: 0;\n    max-width: 100%;\n}\n\n/* -- search page ----------------------------------------------------------- */\n\nul.search {\n    margin: 10px 0 0 20px;\n    padding: 0;\n}\n\nul.search li {\n    padding: 5px 0 5px 20px;\n    background-image: url(file.png);\n    background-repeat: no-repeat;\n    background-position: 0 7px;\n}\n\nul.search li a {\n    font-weight: bold;\n}\n\nul.search li div.context {\n    color: #888;\n    margin: 2px 0 0 30px;\n    text-align: left;\n}\n\nul.keywordmatches li.goodmatch a {\n    font-weight: bold;\n}\n\n/* -- index page ------------------------------------------------------------ */\n\ntable.contentstable {\n    width: 90%;\n    margin-left: auto;\n    margin-right: auto;\n}\n\ntable.contentstable p.biglink {\n    line-height: 150%;\n}\n\na.biglink {\n    font-size: 1.3em;\n}\n\nspan.linkdescr {\n    font-style: italic;\n    padding-top: 5px;\n    font-size: 90%;\n}\n\n/* -- general index --------------------------------------------------------- */\n\ntable.indextable {\n    width: 100%;\n}\n\ntable.indextable td {\n    text-align: left;\n    vertical-align: top;\n}\n\ntable.indextable ul {\n    margin-top: 0;\n    margin-bottom: 0;\n    list-style-type: none;\n}\n\ntable.indextable > tbody > tr > td > ul {\n    padding-left: 0em;\n}\n\ntable.indextable tr.pcap {\n    height: 10px;\n}\n\ntable.indextable tr.cap {\n    margin-top: 10px;\n    background-color: #f2f2f2;\n}\n\nimg.toggler {\n    margin-right: 3px;\n    margin-top: 3px;\n    cursor: pointer;\n}\n\ndiv.modindex-jumpbox {\n    border-top: 1px solid #ddd;\n    border-bottom: 1px solid #ddd;\n    margin: 1em 0 1em 0;\n    padding: 0.4em;\n}\n\ndiv.genindex-jumpbox {\n    border-top: 1px solid #ddd;\n    border-bottom: 1px solid #ddd;\n    margin: 1em 0 1em 0;\n    padding: 0.4em;\n}\n\n/* -- domain module index --------------------------------------------------- */\n\ntable.modindextable td {\n    padding: 2px;\n    border-collapse: collapse;\n}\n\n/* -- general body styles --------------------------------------------------- */\n\ndiv.body {\n    min-width: 450px;\n    max-width: 800px;\n}\n\ndiv.body p, div.body dd, div.body li, div.body blockquote {\n    -moz-hyphens: auto;\n    -ms-hyphens: auto;\n    -webkit-hyphens: auto;\n    hyphens: auto;\n}\n\na.headerlink {\n    visibility: hidden;\n}\n\nh1:hover > a.headerlink,\nh2:hover > a.headerlink,\nh3:hover > a.headerlink,\nh4:hover > a.headerlink,\nh5:hover > a.headerlink,\nh6:hover > a.headerlink,\ndt:hover > a.headerlink,\ncaption:hover > a.headerlink,\np.caption:hover > a.headerlink,\ndiv.code-block-caption:hover > a.headerlink {\n    visibility: visible;\n}\n\ndiv.body p.caption {\n    text-align: inherit;\n}\n\ndiv.body td {\n    text-align: left;\n}\n\n.first {\n    margin-top: 0 !important;\n}\n\np.rubric {\n    margin-top: 30px;\n    font-weight: bold;\n}\n\nimg.align-left, .figure.align-left, object.align-left {\n    clear: left;\n    float: left;\n    margin-right: 1em;\n}\n\nimg.align-right, .figure.align-right, object.align-right {\n    clear: right;\n    float: right;\n    margin-left: 1em;\n}\n\nimg.align-center, .figure.align-center, object.align-center {\n  display: block;\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.align-left {\n    text-align: left;\n}\n\n.align-center {\n    text-align: center;\n}\n\n.align-right {\n    text-align: right;\n}\n\n/* -- sidebars -------------------------------------------------------------- */\n\ndiv.sidebar {\n    margin: 0 0 0.5em 1em;\n    border: 1px solid #ddb;\n    padding: 7px 7px 0 7px;\n    background-color: #ffe;\n    width: 40%;\n    float: right;\n}\n\np.sidebar-title {\n    font-weight: bold;\n}\n\n/* -- topics ---------------------------------------------------------------- */\n\ndiv.topic {\n    border: 1px solid #ccc;\n    padding: 7px 7px 0 7px;\n    margin: 10px 0 10px 0;\n}\n\np.topic-title {\n    font-size: 1.1em;\n    font-weight: bold;\n    margin-top: 10px;\n}\n\n/* -- admonitions ----------------------------------------------------------- */\n\ndiv.admonition {\n    margin-top: 10px;\n    margin-bottom: 10px;\n    padding: 7px;\n}\n\ndiv.admonition dt {\n    font-weight: bold;\n}\n\ndiv.admonition dl {\n    margin-bottom: 0;\n}\n\np.admonition-title {\n    margin: 0px 10px 5px 0px;\n    font-weight: bold;\n}\n\ndiv.body p.centered {\n    text-align: center;\n    margin-top: 25px;\n}\n\n/* -- tables ---------------------------------------------------------------- */\n\ntable.docutils {\n    border: 0;\n    border-collapse: collapse;\n}\n\ntable.align-center {\n    margin-left: auto;\n    margin-right: auto;\n}\n\ntable caption span.caption-number {\n    font-style: italic;\n}\n\ntable caption span.caption-text {\n}\n\ntable.docutils td, table.docutils th {\n    padding: 1px 8px 1px 5px;\n    border-top: 0;\n    border-left: 0;\n    border-right: 0;\n    border-bottom: 1px solid #aaa;\n}\n\ntable.footnote td, table.footnote th {\n    border: 0 !important;\n}\n\nth {\n    text-align: left;\n    padding-right: 5px;\n}\n\ntable.citation {\n    border-left: solid 1px gray;\n    margin-left: 1px;\n}\n\ntable.citation td {\n    border-bottom: none;\n}\n\n/* -- figures --------------------------------------------------------------- */\n\ndiv.figure {\n    margin: 0.5em;\n    padding: 0.5em;\n}\n\ndiv.figure p.caption {\n    padding: 0.3em;\n}\n\ndiv.figure p.caption span.caption-number {\n    font-style: italic;\n}\n\ndiv.figure p.caption span.caption-text {\n}\n\n/* -- field list styles ----------------------------------------------------- */\n\ntable.field-list td, table.field-list th {\n    border: 0 !important;\n}\n\n.field-list ul {\n    margin: 0;\n    padding-left: 1em;\n}\n\n.field-list p {\n    margin: 0;\n}\n\n.field-name {\n    -moz-hyphens: manual;\n    -ms-hyphens: manual;\n    -webkit-hyphens: manual;\n    hyphens: manual;\n}\n\n/* -- hlist styles ---------------------------------------------------------- */\n\ntable.hlist td {\n    vertical-align: top;\n}\n\n\n/* -- other body styles ----------------------------------------------------- */\n\nol.arabic {\n    list-style: decimal;\n}\n\nol.loweralpha {\n    list-style: lower-alpha;\n}\n\nol.upperalpha {\n    list-style: upper-alpha;\n}\n\nol.lowerroman {\n    list-style: lower-roman;\n}\n\nol.upperroman {\n    list-style: upper-roman;\n}\n\ndl {\n    margin-bottom: 15px;\n}\n\ndd p {\n    margin-top: 0px;\n}\n\ndd ul, dd table {\n    margin-bottom: 10px;\n}\n\ndd {\n    margin-top: 3px;\n    margin-bottom: 10px;\n    margin-left: 30px;\n}\n\ndt:target, span.highlighted {\n    background-color: #fbe54e;\n}\n\nrect.highlighted {\n    fill: #fbe54e;\n}\n\ndl.glossary dt {\n    font-weight: bold;\n    font-size: 1.1em;\n}\n\n.optional {\n    font-size: 1.3em;\n}\n\n.sig-paren {\n    font-size: larger;\n}\n\n.versionmodified {\n    font-style: italic;\n}\n\n.system-message {\n    background-color: #fda;\n    padding: 5px;\n    border: 3px solid red;\n}\n\n.footnote:target  {\n    background-color: #ffa;\n}\n\n.line-block {\n    display: block;\n    margin-top: 1em;\n    margin-bottom: 1em;\n}\n\n.line-block .line-block {\n    margin-top: 0;\n    margin-bottom: 0;\n    margin-left: 1.5em;\n}\n\n.guilabel, .menuselection {\n    font-family: sans-serif;\n}\n\n.accelerator {\n    text-decoration: underline;\n}\n\n.classifier {\n    font-style: oblique;\n}\n\nabbr, acronym {\n    border-bottom: dotted 1px;\n    cursor: help;\n}\n\n/* -- code displays --------------------------------------------------------- */\n\npre {\n    overflow: auto;\n    overflow-y: hidden;  /* fixes display issues on Chrome browsers */\n}\n\nspan.pre {\n    -moz-hyphens: none;\n    -ms-hyphens: none;\n    -webkit-hyphens: none;\n    hyphens: none;\n}\n\ntd.linenos pre {\n    padding: 5px 0px;\n    border: 0;\n    background-color: transparent;\n    color: #aaa;\n}\n\ntable.highlighttable {\n    margin-left: 0.5em;\n}\n\ntable.highlighttable td {\n    padding: 0 0.5em 0 0.5em;\n}\n\ndiv.code-block-caption {\n    padding: 2px 5px;\n    font-size: small;\n}\n\ndiv.code-block-caption code {\n    background-color: transparent;\n}\n\ndiv.code-block-caption + div > div.highlight > pre {\n    margin-top: 0;\n}\n\ndiv.code-block-caption span.caption-number {\n    padding: 0.1em 0.3em;\n    font-style: italic;\n}\n\ndiv.code-block-caption span.caption-text {\n}\n\ndiv.literal-block-wrapper {\n    padding: 1em 1em 0;\n}\n\ndiv.literal-block-wrapper div.highlight {\n    margin: 0;\n}\n\ncode.descname {\n    background-color: transparent;\n    font-weight: bold;\n    font-size: 1.2em;\n}\n\ncode.descclassname {\n    background-color: transparent;\n}\n\ncode.xref, a code {\n    background-color: transparent;\n    font-weight: bold;\n}\n\nh1 code, h2 code, h3 code, h4 code, h5 code, h6 code {\n    background-color: transparent;\n}\n\n.viewcode-link {\n    float: right;\n}\n\n.viewcode-back {\n    float: right;\n    font-family: sans-serif;\n}\n\ndiv.viewcode-block:target {\n    margin: -1px -10px;\n    padding: 0 10px;\n}\n\n/* -- math display ---------------------------------------------------------- */\n\nimg.math {\n    vertical-align: middle;\n}\n\ndiv.body div.math p {\n    text-align: center;\n}\n\nspan.eqno {\n    float: right;\n}\n\nspan.eqno a.headerlink {\n    position: relative;\n    left: 0px;\n    z-index: 1;\n}\n\ndiv.math:hover a.headerlink {\n    visibility: visible;\n}\n\n/* -- printout stylesheet --------------------------------------------------- */\n\n@media print {\n    div.document,\n    div.documentwrapper,\n    div.bodywrapper {\n        margin: 0 !important;\n        width: 100%;\n    }\n\n    div.sphinxsidebar,\n    div.related,\n    div.footer,\n    #top-link {\n        display: none;\n    }\n}"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css",
    "content": "/*\n * classic.css_t\n * ~~~~~~~~~~~~~\n *\n * Sphinx stylesheet -- classic theme.\n *\n * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.\n * :license: BSD, see LICENSE for details.\n *\n */\n\n@import url(\"basic.css\");\n\n/* -- page layout ----------------------------------------------------------- */\n\nbody {\n    font-family: sans-serif;\n    font-size: 100%;\n    background-color: #11303d;\n    color: #000;\n    margin: 0;\n    padding: 0;\n}\n\ndiv.document {\n    background-color: #1c4e63;\n}\n\ndiv.documentwrapper {\n    float: left;\n    width: 100%;\n}\n\ndiv.bodywrapper {\n    margin: 0 0 0 230px;\n}\n\ndiv.body {\n    background-color: #ffffff;\n    color: #000000;\n    padding: 0 20px 30px 20px;\n}\n\ndiv.footer {\n    color: #ffffff;\n    width: 100%;\n    padding: 9px 0 9px 0;\n    text-align: center;\n    font-size: 75%;\n}\n\ndiv.footer a {\n    color: #ffffff;\n    text-decoration: underline;\n}\n\ndiv.related {\n    background-color: #133f52;\n    line-height: 30px;\n    color: #ffffff;\n}\n\ndiv.related a {\n    color: #ffffff;\n}\n\ndiv.sphinxsidebar {\n}\n\ndiv.sphinxsidebar h3 {\n    font-family: 'Trebuchet MS', sans-serif;\n    color: #ffffff;\n    font-size: 1.4em;\n    font-weight: normal;\n    margin: 0;\n    padding: 0;\n}\n\ndiv.sphinxsidebar h3 a {\n    color: #ffffff;\n}\n\ndiv.sphinxsidebar h4 {\n    font-family: 'Trebuchet MS', sans-serif;\n    color: #ffffff;\n    font-size: 1.3em;\n    font-weight: normal;\n    margin: 5px 0 0 0;\n    padding: 0;\n}\n\ndiv.sphinxsidebar p {\n    color: #ffffff;\n}\n\ndiv.sphinxsidebar p.topless {\n    margin: 5px 10px 10px 10px;\n}\n\ndiv.sphinxsidebar ul {\n    margin: 10px;\n    padding: 0;\n    color: #ffffff;\n}\n\ndiv.sphinxsidebar a {\n    color: #98dbcc;\n}\n\ndiv.sphinxsidebar input {\n    border: 1px solid #98dbcc;\n    font-family: sans-serif;\n    font-size: 1em;\n}\n\n\n\n/* -- hyperlink styles ------------------------------------------------------ */\n\na {\n    color: #355f7c;\n    text-decoration: none;\n}\n\na:visited {\n    color: #355f7c;\n    text-decoration: none;\n}\n\na:hover {\n    text-decoration: underline;\n}\n\n\n\n/* -- body styles ----------------------------------------------------------- */\n\ndiv.body h1,\ndiv.body h2,\ndiv.body h3,\ndiv.body h4,\ndiv.body h5,\ndiv.body h6 {\n    font-family: 'Trebuchet MS', sans-serif;\n    background-color: #f2f2f2;\n    font-weight: normal;\n    color: #20435c;\n    border-bottom: 1px solid #ccc;\n    margin: 20px -20px 10px -20px;\n    padding: 3px 0 3px 10px;\n}\n\ndiv.body h1 { margin-top: 0; font-size: 200%; }\ndiv.body h2 { font-size: 160%; }\ndiv.body h3 { font-size: 140%; }\ndiv.body h4 { font-size: 120%; }\ndiv.body h5 { font-size: 110%; }\ndiv.body h6 { font-size: 100%; }\n\na.headerlink {\n    color: #c60f0f;\n    font-size: 0.8em;\n    padding: 0 4px 0 4px;\n    text-decoration: none;\n}\n\na.headerlink:hover {\n    background-color: #c60f0f;\n    color: white;\n}\n\ndiv.body p, div.body dd, div.body li, div.body blockquote {\n    text-align: justify;\n    line-height: 130%;\n}\n\ndiv.admonition p.admonition-title + p {\n    display: inline;\n}\n\ndiv.admonition p {\n    margin-bottom: 5px;\n}\n\ndiv.admonition pre {\n    margin-bottom: 5px;\n}\n\ndiv.admonition ul, div.admonition ol {\n    margin-bottom: 5px;\n}\n\ndiv.note {\n    background-color: #eee;\n    border: 1px solid #ccc;\n}\n\ndiv.seealso {\n    background-color: #ffc;\n    border: 1px solid #ff6;\n}\n\ndiv.topic {\n    background-color: #eee;\n}\n\ndiv.warning {\n    background-color: #ffe4e4;\n    border: 1px solid #f66;\n}\n\np.admonition-title {\n    display: inline;\n}\n\np.admonition-title:after {\n    content: \":\";\n}\n\npre {\n    padding: 5px;\n    background-color: #eeffcc;\n    color: #333333;\n    line-height: 120%;\n    border: 1px solid #ac9;\n    border-left: none;\n    border-right: none;\n}\n\ncode {\n    background-color: #ecf0f3;\n    padding: 0 1px 0 1px;\n    font-size: 0.95em;\n}\n\nth {\n    background-color: #ede;\n}\n\n.warning code {\n    background: #efc2c2;\n}\n\n.note code {\n    background: #d6d6d6;\n}\n\n.viewcode-back {\n    font-family: sans-serif;\n}\n\ndiv.viewcode-block:target {\n    background-color: #f4debf;\n    border-top: 1px solid #ac9;\n    border-bottom: 1px solid #ac9;\n}\n\ndiv.code-block-caption {\n    color: #efefef;\n    background-color: #1c4e63;\n}"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css",
    "content": "a.headerlink {\n    display: none !important;\n}\n\n.section-title {\n  font-size: 2rem;\n  line-height: 2.5rem;\n  letter-spacing: -1px;\n  font-weight: 700;\n  margin: 0 0 1rem;\n}\n\nnav#api_nav .toctree-l1 {\n  margin-bottom: 1.5rem;\n}\n\nnav#api_nav #api_sections ul {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\nnav#api_nav #api_sections ul li.toctree-l1>a {\n  color: #1264a3;\n  letter-spacing: 0;\n  font-size: .8rem;\n  font-weight: 800;\n  text-transform: uppercase;\n  border: none;\n  padding: 0;\n}\n\nnav#api_nav #api_sections ul li.toctree-l2 {\n  margin: 0;\n  padding: 0;\n}\n\nnav#api_nav #api_sections ul li.toctree-l2 a {\n  color: #1d1c1d;\n  text-transform: none;\n  font-weight: inherit;\n  padding: 0;\n  display: block;\n  text-overflow: ellipsis;\n  overflow: hidden;\n  white-space: nowrap;\n  font-size: 15px!important;\n  line-height:15px;\n  padding: 4px 8px;\n  border: 1px solid transparent;\n  border-radius: 4px;\n}\n\nnav#api_nav #api_sections ul li.toctree-l2 a:hover {\n  cursor: pointer;\n  text-decoration: none;\n  background-color:#e8f5fa;\n  border-color:#dcf0fb;\n}\n\nnav#api_nav #footer #footer_nav {\n  font-size: .9375rem;\n}\n\nnav#api_nav #footer #footer_nav a {\n  border: none;\n  padding: 0;\n  color: #616061;\n}\n\nnav#api_nav #footer #footer_nav a:hover {\n  text-decoration: none;\n  color: #1c1c1c;\n}"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css",
    "content": "/* Updates body font */\nbody {\n  font-family: Slack-Lato,appleLogo,sans-serif;\n}\n\n/* Replaces old sidebar styled links */\n.sidebar_menu h5 {\n  font-size: 0.8rem;\n  font-weight: 800;\n  margin-bottom: 3px;\n}\n\n/* Aligns footer navigation to the left of the sidebar */\n.footer_nav {\n  margin: 0 !important;\n}\n\n/* Styles the signature all nice and pretty <3 */\n#footer_signature {\n  color:#e01e5a;\n  font-size:.9rem;\n  margin-top: 10px;\n}\n\n/* Fixes link hover state */\na:hover {\n  text-decoration: underline;\n}\n\n/* Makes footer consistent */\nfooter {\n  background-color: transparent;\n  border: 0;\n}"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js",
    "content": "/*\n * doctools.js\n * ~~~~~~~~~~~\n *\n * Sphinx JavaScript utilities for all documentation.\n *\n * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.\n * :license: BSD, see LICENSE for details.\n *\n */\n\n/**\n * select a different prefix for underscore\n */\n$u = _.noConflict();\n\n/**\n * make the code below compatible with browsers without\n * an installed firebug like debugger\nif (!window.console || !console.firebug) {\n  var names = [\"log\", \"debug\", \"info\", \"warn\", \"error\", \"assert\", \"dir\",\n    \"dirxml\", \"group\", \"groupEnd\", \"time\", \"timeEnd\", \"count\", \"trace\",\n    \"profile\", \"profileEnd\"];\n  window.console = {};\n  for (var i = 0; i < names.length; ++i)\n    window.console[names[i]] = function() {};\n}\n */\n\n/**\n * small helper function to urldecode strings\n */\njQuery.urldecode = function(x) {\n  return decodeURIComponent(x).replace(/\\+/g, ' ');\n};\n\n/**\n * small helper function to urlencode strings\n */\njQuery.urlencode = encodeURIComponent;\n\n/**\n * This function returns the parsed url parameters of the\n * current request. Multiple values per key are supported,\n * it will always return arrays of strings for the value parts.\n */\njQuery.getQueryParameters = function(s) {\n  if (typeof s === 'undefined')\n    s = document.location.search;\n  var parts = s.substr(s.indexOf('?') + 1).split('&');\n  var result = {};\n  for (var i = 0; i < parts.length; i++) {\n    var tmp = parts[i].split('=', 2);\n    var key = jQuery.urldecode(tmp[0]);\n    var value = jQuery.urldecode(tmp[1]);\n    if (key in result)\n      result[key].push(value);\n    else\n      result[key] = [value];\n  }\n  return result;\n};\n\n/**\n * highlight a given string on a jquery object by wrapping it in\n * span elements with the given class name.\n */\njQuery.fn.highlightText = function(text, className) {\n  function highlight(node, addItems) {\n    if (node.nodeType === 3) {\n      var val = node.nodeValue;\n      var pos = val.toLowerCase().indexOf(text);\n      if (pos >= 0 &&\n          !jQuery(node.parentNode).hasClass(className) &&\n          !jQuery(node.parentNode).hasClass(\"nohighlight\")) {\n        var span;\n        var isInSVG = jQuery(node).closest(\"body, svg, foreignObject\").is(\"svg\");\n        if (isInSVG) {\n          span = document.createElementNS(\"http://www.w3.org/2000/svg\", \"tspan\");\n        } else {\n          span = document.createElement(\"span\");\n          span.className = className;\n        }\n        span.appendChild(document.createTextNode(val.substr(pos, text.length)));\n        node.parentNode.insertBefore(span, node.parentNode.insertBefore(\n          document.createTextNode(val.substr(pos + text.length)),\n          node.nextSibling));\n        node.nodeValue = val.substr(0, pos);\n        if (isInSVG) {\n          var bbox = span.getBBox();\n          var rect = document.createElementNS(\"http://www.w3.org/2000/svg\", \"rect\");\n       \t  rect.x.baseVal.value = bbox.x;\n          rect.y.baseVal.value = bbox.y;\n          rect.width.baseVal.value = bbox.width;\n          rect.height.baseVal.value = bbox.height;\n          rect.setAttribute('class', className);\n          var parentOfText = node.parentNode.parentNode;\n          addItems.push({\n              \"parent\": node.parentNode,\n              \"target\": rect});\n        }\n      }\n    }\n    else if (!jQuery(node).is(\"button, select, textarea\")) {\n      jQuery.each(node.childNodes, function() {\n        highlight(this, addItems);\n      });\n    }\n  }\n  var addItems = [];\n  var result = this.each(function() {\n    highlight(this, addItems);\n  });\n  for (var i = 0; i < addItems.length; ++i) {\n    jQuery(addItems[i].parent).before(addItems[i].target);\n  }\n  return result;\n};\n\n/*\n * backward compatibility for jQuery.browser\n * This will be supported until firefox bug is fixed.\n */\nif (!jQuery.browser) {\n  jQuery.uaMatch = function(ua) {\n    ua = ua.toLowerCase();\n\n    var match = /(chrome)[ \\/]([\\w.]+)/.exec(ua) ||\n      /(webkit)[ \\/]([\\w.]+)/.exec(ua) ||\n      /(opera)(?:.*version|)[ \\/]([\\w.]+)/.exec(ua) ||\n      /(msie) ([\\w.]+)/.exec(ua) ||\n      ua.indexOf(\"compatible\") < 0 && /(mozilla)(?:.*? rv:([\\w.]+)|)/.exec(ua) ||\n      [];\n\n    return {\n      browser: match[ 1 ] || \"\",\n      version: match[ 2 ] || \"0\"\n    };\n  };\n  jQuery.browser = {};\n  jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;\n}\n\n/**\n * Small JavaScript module for the documentation.\n */\nvar Documentation = {\n\n  init : function() {\n    this.fixFirefoxAnchorBug();\n    this.highlightSearchWords();\n    this.initIndexTable();\n    if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) {\n      this.initOnKeyListeners();\n    }\n  },\n\n  /**\n   * i18n support\n   */\n  TRANSLATIONS : {},\n  PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },\n  LOCALE : 'unknown',\n\n  // gettext and ngettext don't access this so that the functions\n  // can safely bound to a different name (_ = Documentation.gettext)\n  gettext : function(string) {\n    var translated = Documentation.TRANSLATIONS[string];\n    if (typeof translated === 'undefined')\n      return string;\n    return (typeof translated === 'string') ? translated : translated[0];\n  },\n\n  ngettext : function(singular, plural, n) {\n    var translated = Documentation.TRANSLATIONS[singular];\n    if (typeof translated === 'undefined')\n      return (n == 1) ? singular : plural;\n    return translated[Documentation.PLURALEXPR(n)];\n  },\n\n  addTranslations : function(catalog) {\n    for (var key in catalog.messages)\n      this.TRANSLATIONS[key] = catalog.messages[key];\n    this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');\n    this.LOCALE = catalog.locale;\n  },\n\n  /**\n   * add context elements like header anchor links\n   */\n  addContextElements : function() {\n    $('div[id] > :header:first').each(function() {\n      $('<a class=\"headerlink\">\\u00B6</a>').\n      attr('href', '#' + this.id).\n      attr('title', _('Permalink to this headline')).\n      appendTo(this);\n    });\n    $('dt[id]').each(function() {\n      $('<a class=\"headerlink\">\\u00B6</a>').\n      attr('href', '#' + this.id).\n      attr('title', _('Permalink to this definition')).\n      appendTo(this);\n    });\n  },\n\n  /**\n   * workaround a firefox stupidity\n   * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075\n   */\n  fixFirefoxAnchorBug : function() {\n    if (document.location.hash && $.browser.mozilla)\n      window.setTimeout(function() {\n        document.location.href += '';\n      }, 10);\n  },\n\n  /**\n   * highlight the search words provided in the url in the text\n   */\n  highlightSearchWords : function() {\n    var params = $.getQueryParameters();\n    var terms = (params.highlight) ? params.highlight[0].split(/\\s+/) : [];\n    if (terms.length) {\n      var body = $('div.body');\n      if (!body.length) {\n        body = $('body');\n      }\n      window.setTimeout(function() {\n        $.each(terms, function() {\n          body.highlightText(this.toLowerCase(), 'highlighted');\n        });\n      }, 10);\n      $('<p class=\"highlight-link\"><a href=\"javascript:Documentation.' +\n        'hideSearchWords()\">' + _('Hide Search Matches') + '</a></p>')\n          .appendTo($('#searchbox'));\n    }\n  },\n\n  /**\n   * init the domain index toggle buttons\n   */\n  initIndexTable : function() {\n    var togglers = $('img.toggler').click(function() {\n      var src = $(this).attr('src');\n      var idnum = $(this).attr('id').substr(7);\n      $('tr.cg-' + idnum).toggle();\n      if (src.substr(-9) === 'minus.png')\n        $(this).attr('src', src.substr(0, src.length-9) + 'plus.png');\n      else\n        $(this).attr('src', src.substr(0, src.length-8) + 'minus.png');\n    }).css('display', '');\n    if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) {\n        togglers.click();\n    }\n  },\n\n  /**\n   * helper function to hide the search marks again\n   */\n  hideSearchWords : function() {\n    $('#searchbox .highlight-link').fadeOut(300);\n    $('span.highlighted').removeClass('highlighted');\n  },\n\n  /**\n   * make the url absolute\n   */\n  makeURL : function(relativeURL) {\n    return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;\n  },\n\n  /**\n   * get the current relative url\n   */\n  getCurrentURL : function() {\n    var path = document.location.pathname;\n    var parts = path.split(/\\//);\n    $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\\//), function() {\n      if (this === '..')\n        parts.pop();\n    });\n    var url = parts.join('/');\n    return path.substring(url.lastIndexOf('/') + 1, path.length - 1);\n  },\n\n  initOnKeyListeners: function() {\n    $(document).keyup(function(event) {\n      var activeElementType = document.activeElement.tagName;\n      // don't navigate when in search box or textarea\n      if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT') {\n        switch (event.keyCode) {\n          case 37: // left\n            var prevHref = $('link[rel=\"prev\"]').prop('href');\n            if (prevHref) {\n              window.location.href = prevHref;\n              return false;\n            }\n          case 39: // right\n            var nextHref = $('link[rel=\"next\"]').prop('href');\n            if (nextHref) {\n              window.location.href = nextHref;\n              return false;\n            }\n        }\n      }\n    });\n  }\n};\n\n// quick alias for translations\n_ = Documentation.gettext;\n\n$(document).ready(function() {\n  Documentation.init();\n});\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/documentation_options.js",
    "content": "var DOCUMENTATION_OPTIONS = {\n    URL_ROOT: document.getElementById(\"documentation_options\").getAttribute('data-url_root'),\n    VERSION: '1.0.1',\n    LANGUAGE: 'None',\n    COLLAPSE_INDEX: false,\n    FILE_SUFFIX: '.html',\n    HAS_SOURCE: true,\n    SOURCELINK_SUFFIX: '.txt',\n    NAVIGATION_WITH_KEYS: false,\n};"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery-3.2.1.js",
    "content": "/*!\n * jQuery JavaScript Library v3.2.1\n * https://jquery.com/\n *\n * Includes Sizzle.js\n * https://sizzlejs.com/\n *\n * Copyright JS Foundation and other contributors\n * Released under the MIT license\n * https://jquery.org/license\n *\n * Date: 2017-03-20T18:59Z\n */\n( function( global, factory ) {\n\n\t\"use strict\";\n\n\tif ( typeof module === \"object\" && typeof module.exports === \"object\" ) {\n\n\t\t// For CommonJS and CommonJS-like environments where a proper `window`\n\t\t// is present, execute the factory and get jQuery.\n\t\t// For environments that do not have a `window` with a `document`\n\t\t// (such as Node.js), expose a factory as module.exports.\n\t\t// This accentuates the need for the creation of a real `window`.\n\t\t// e.g. var jQuery = require(\"jquery\")(window);\n\t\t// See ticket #14549 for more info.\n\t\tmodule.exports = global.document ?\n\t\t\tfactory( global, true ) :\n\t\t\tfunction( w ) {\n\t\t\t\tif ( !w.document ) {\n\t\t\t\t\tthrow new Error( \"jQuery requires a window with a document\" );\n\t\t\t\t}\n\t\t\t\treturn factory( w );\n\t\t\t};\n\t} else {\n\t\tfactory( global );\n\t}\n\n// Pass this if window is not defined yet\n} )( typeof window !== \"undefined\" ? window : this, function( window, noGlobal ) {\n\n// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1\n// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode\n// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common\n// enough that all such attempts are guarded in a try block.\n\"use strict\";\n\nvar arr = [];\n\nvar document = window.document;\n\nvar getProto = Object.getPrototypeOf;\n\nvar slice = arr.slice;\n\nvar concat = arr.concat;\n\nvar push = arr.push;\n\nvar indexOf = arr.indexOf;\n\nvar class2type = {};\n\nvar toString = class2type.toString;\n\nvar hasOwn = class2type.hasOwnProperty;\n\nvar fnToString = hasOwn.toString;\n\nvar ObjectFunctionString = fnToString.call( Object );\n\nvar support = {};\n\n\n\n\tfunction DOMEval( code, doc ) {\n\t\tdoc = doc || document;\n\n\t\tvar script = doc.createElement( \"script\" );\n\n\t\tscript.text = code;\n\t\tdoc.head.appendChild( script ).parentNode.removeChild( script );\n\t}\n/* global Symbol */\n// Defining this global in .eslintrc.json would create a danger of using the global\n// unguarded in another place, it seems safer to define global only for this module\n\n\n\nvar\n\tversion = \"3.2.1\",\n\n\t// Define a local copy of jQuery\n\tjQuery = function( selector, context ) {\n\n\t\t// The jQuery object is actually just the init constructor 'enhanced'\n\t\t// Need init if jQuery is called (just allow error to be thrown if not included)\n\t\treturn new jQuery.fn.init( selector, context );\n\t},\n\n\t// Support: Android <=4.0 only\n\t// Make sure we trim BOM and NBSP\n\trtrim = /^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g,\n\n\t// Matches dashed string for camelizing\n\trmsPrefix = /^-ms-/,\n\trdashAlpha = /-([a-z])/g,\n\n\t// Used by jQuery.camelCase as callback to replace()\n\tfcamelCase = function( all, letter ) {\n\t\treturn letter.toUpperCase();\n\t};\n\njQuery.fn = jQuery.prototype = {\n\n\t// The current version of jQuery being used\n\tjquery: version,\n\n\tconstructor: jQuery,\n\n\t// The default length of a jQuery object is 0\n\tlength: 0,\n\n\ttoArray: function() {\n\t\treturn slice.call( this );\n\t},\n\n\t// Get the Nth element in the matched element set OR\n\t// Get the whole matched element set as a clean array\n\tget: function( num ) {\n\n\t\t// Return all the elements in a clean array\n\t\tif ( num == null ) {\n\t\t\treturn slice.call( this );\n\t\t}\n\n\t\t// Return just the one element from the set\n\t\treturn num < 0 ? this[ num + this.length ] : this[ num ];\n\t},\n\n\t// Take an array of elements and push it onto the stack\n\t// (returning the new matched element set)\n\tpushStack: function( elems ) {\n\n\t\t// Build a new jQuery matched element set\n\t\tvar ret = jQuery.merge( this.constructor(), elems );\n\n\t\t// Add the old object onto the stack (as a reference)\n\t\tret.prevObject = this;\n\n\t\t// Return the newly-formed element set\n\t\treturn ret;\n\t},\n\n\t// Execute a callback for every element in the matched set.\n\teach: function( callback ) {\n\t\treturn jQuery.each( this, callback );\n\t},\n\n\tmap: function( callback ) {\n\t\treturn this.pushStack( jQuery.map( this, function( elem, i ) {\n\t\t\treturn callback.call( elem, i, elem );\n\t\t} ) );\n\t},\n\n\tslice: function() {\n\t\treturn this.pushStack( slice.apply( this, arguments ) );\n\t},\n\n\tfirst: function() {\n\t\treturn this.eq( 0 );\n\t},\n\n\tlast: function() {\n\t\treturn this.eq( -1 );\n\t},\n\n\teq: function( i ) {\n\t\tvar len = this.length,\n\t\t\tj = +i + ( i < 0 ? len : 0 );\n\t\treturn this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );\n\t},\n\n\tend: function() {\n\t\treturn this.prevObject || this.constructor();\n\t},\n\n\t// For internal use only.\n\t// Behaves like an Array's method, not like a jQuery method.\n\tpush: push,\n\tsort: arr.sort,\n\tsplice: arr.splice\n};\n\njQuery.extend = jQuery.fn.extend = function() {\n\tvar options, name, src, copy, copyIsArray, clone,\n\t\ttarget = arguments[ 0 ] || {},\n\t\ti = 1,\n\t\tlength = arguments.length,\n\t\tdeep = false;\n\n\t// Handle a deep copy situation\n\tif ( typeof target === \"boolean\" ) {\n\t\tdeep = target;\n\n\t\t// Skip the boolean and the target\n\t\ttarget = arguments[ i ] || {};\n\t\ti++;\n\t}\n\n\t// Handle case when target is a string or something (possible in deep copy)\n\tif ( typeof target !== \"object\" && !jQuery.isFunction( target ) ) {\n\t\ttarget = {};\n\t}\n\n\t// Extend jQuery itself if only one argument is passed\n\tif ( i === length ) {\n\t\ttarget = this;\n\t\ti--;\n\t}\n\n\tfor ( ; i < length; i++ ) {\n\n\t\t// Only deal with non-null/undefined values\n\t\tif ( ( options = arguments[ i ] ) != null ) {\n\n\t\t\t// Extend the base object\n\t\t\tfor ( name in options ) {\n\t\t\t\tsrc = target[ name ];\n\t\t\t\tcopy = options[ name ];\n\n\t\t\t\t// Prevent never-ending loop\n\t\t\t\tif ( target === copy ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Recurse if we're merging plain objects or arrays\n\t\t\t\tif ( deep && copy && ( jQuery.isPlainObject( copy ) ||\n\t\t\t\t\t( copyIsArray = Array.isArray( copy ) ) ) ) {\n\n\t\t\t\t\tif ( copyIsArray ) {\n\t\t\t\t\t\tcopyIsArray = false;\n\t\t\t\t\t\tclone = src && Array.isArray( src ) ? src : [];\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\tclone = src && jQuery.isPlainObject( src ) ? src : {};\n\t\t\t\t\t}\n\n\t\t\t\t\t// Never move original objects, clone them\n\t\t\t\t\ttarget[ name ] = jQuery.extend( deep, clone, copy );\n\n\t\t\t\t// Don't bring in undefined values\n\t\t\t\t} else if ( copy !== undefined ) {\n\t\t\t\t\ttarget[ name ] = copy;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the modified object\n\treturn target;\n};\n\njQuery.extend( {\n\n\t// Unique for each copy of jQuery on the page\n\texpando: \"jQuery\" + ( version + Math.random() ).replace( /\\D/g, \"\" ),\n\n\t// Assume jQuery is ready without the ready module\n\tisReady: true,\n\n\terror: function( msg ) {\n\t\tthrow new Error( msg );\n\t},\n\n\tnoop: function() {},\n\n\tisFunction: function( obj ) {\n\t\treturn jQuery.type( obj ) === \"function\";\n\t},\n\n\tisWindow: function( obj ) {\n\t\treturn obj != null && obj === obj.window;\n\t},\n\n\tisNumeric: function( obj ) {\n\n\t\t// As of jQuery 3.0, isNumeric is limited to\n\t\t// strings and numbers (primitives or objects)\n\t\t// that can be coerced to finite numbers (gh-2662)\n\t\tvar type = jQuery.type( obj );\n\t\treturn ( type === \"number\" || type === \"string\" ) &&\n\n\t\t\t// parseFloat NaNs numeric-cast false positives (\"\")\n\t\t\t// ...but misinterprets leading-number strings, particularly hex literals (\"0x...\")\n\t\t\t// subtraction forces infinities to NaN\n\t\t\t!isNaN( obj - parseFloat( obj ) );\n\t},\n\n\tisPlainObject: function( obj ) {\n\t\tvar proto, Ctor;\n\n\t\t// Detect obvious negatives\n\t\t// Use toString instead of jQuery.type to catch host objects\n\t\tif ( !obj || toString.call( obj ) !== \"[object Object]\" ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tproto = getProto( obj );\n\n\t\t// Objects with no prototype (e.g., `Object.create( null )`) are plain\n\t\tif ( !proto ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Objects with prototype are plain iff they were constructed by a global Object function\n\t\tCtor = hasOwn.call( proto, \"constructor\" ) && proto.constructor;\n\t\treturn typeof Ctor === \"function\" && fnToString.call( Ctor ) === ObjectFunctionString;\n\t},\n\n\tisEmptyObject: function( obj ) {\n\n\t\t/* eslint-disable no-unused-vars */\n\t\t// See https://github.com/eslint/eslint/issues/6125\n\t\tvar name;\n\n\t\tfor ( name in obj ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t},\n\n\ttype: function( obj ) {\n\t\tif ( obj == null ) {\n\t\t\treturn obj + \"\";\n\t\t}\n\n\t\t// Support: Android <=2.3 only (functionish RegExp)\n\t\treturn typeof obj === \"object\" || typeof obj === \"function\" ?\n\t\t\tclass2type[ toString.call( obj ) ] || \"object\" :\n\t\t\ttypeof obj;\n\t},\n\n\t// Evaluates a script in a global context\n\tglobalEval: function( code ) {\n\t\tDOMEval( code );\n\t},\n\n\t// Convert dashed to camelCase; used by the css and data modules\n\t// Support: IE <=9 - 11, Edge 12 - 13\n\t// Microsoft forgot to hump their vendor prefix (#9572)\n\tcamelCase: function( string ) {\n\t\treturn string.replace( rmsPrefix, \"ms-\" ).replace( rdashAlpha, fcamelCase );\n\t},\n\n\teach: function( obj, callback ) {\n\t\tvar length, i = 0;\n\n\t\tif ( isArrayLike( obj ) ) {\n\t\t\tlength = obj.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor ( i in obj ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn obj;\n\t},\n\n\t// Support: Android <=4.0 only\n\ttrim: function( text ) {\n\t\treturn text == null ?\n\t\t\t\"\" :\n\t\t\t( text + \"\" ).replace( rtrim, \"\" );\n\t},\n\n\t// results is for internal usage only\n\tmakeArray: function( arr, results ) {\n\t\tvar ret = results || [];\n\n\t\tif ( arr != null ) {\n\t\t\tif ( isArrayLike( Object( arr ) ) ) {\n\t\t\t\tjQuery.merge( ret,\n\t\t\t\t\ttypeof arr === \"string\" ?\n\t\t\t\t\t[ arr ] : arr\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tpush.call( ret, arr );\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\tinArray: function( elem, arr, i ) {\n\t\treturn arr == null ? -1 : indexOf.call( arr, elem, i );\n\t},\n\n\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t// push.apply(_, arraylike) throws on ancient WebKit\n\tmerge: function( first, second ) {\n\t\tvar len = +second.length,\n\t\t\tj = 0,\n\t\t\ti = first.length;\n\n\t\tfor ( ; j < len; j++ ) {\n\t\t\tfirst[ i++ ] = second[ j ];\n\t\t}\n\n\t\tfirst.length = i;\n\n\t\treturn first;\n\t},\n\n\tgrep: function( elems, callback, invert ) {\n\t\tvar callbackInverse,\n\t\t\tmatches = [],\n\t\t\ti = 0,\n\t\t\tlength = elems.length,\n\t\t\tcallbackExpect = !invert;\n\n\t\t// Go through the array, only saving the items\n\t\t// that pass the validator function\n\t\tfor ( ; i < length; i++ ) {\n\t\t\tcallbackInverse = !callback( elems[ i ], i );\n\t\t\tif ( callbackInverse !== callbackExpect ) {\n\t\t\t\tmatches.push( elems[ i ] );\n\t\t\t}\n\t\t}\n\n\t\treturn matches;\n\t},\n\n\t// arg is for internal usage only\n\tmap: function( elems, callback, arg ) {\n\t\tvar length, value,\n\t\t\ti = 0,\n\t\t\tret = [];\n\n\t\t// Go through the array, translating each of the items to their new values\n\t\tif ( isArrayLike( elems ) ) {\n\t\t\tlength = elems.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Go through every key on the object,\n\t\t} else {\n\t\t\tfor ( i in elems ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Flatten any nested arrays\n\t\treturn concat.apply( [], ret );\n\t},\n\n\t// A global GUID counter for objects\n\tguid: 1,\n\n\t// Bind a function to a context, optionally partially applying any\n\t// arguments.\n\tproxy: function( fn, context ) {\n\t\tvar tmp, args, proxy;\n\n\t\tif ( typeof context === \"string\" ) {\n\t\t\ttmp = fn[ context ];\n\t\t\tcontext = fn;\n\t\t\tfn = tmp;\n\t\t}\n\n\t\t// Quick check to determine if target is callable, in the spec\n\t\t// this throws a TypeError, but we will just return undefined.\n\t\tif ( !jQuery.isFunction( fn ) ) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// Simulated bind\n\t\targs = slice.call( arguments, 2 );\n\t\tproxy = function() {\n\t\t\treturn fn.apply( context || this, args.concat( slice.call( arguments ) ) );\n\t\t};\n\n\t\t// Set the guid of unique handler to the same of original handler, so it can be removed\n\t\tproxy.guid = fn.guid = fn.guid || jQuery.guid++;\n\n\t\treturn proxy;\n\t},\n\n\tnow: Date.now,\n\n\t// jQuery.support is not used in Core but other projects attach their\n\t// properties to it so it needs to exist.\n\tsupport: support\n} );\n\nif ( typeof Symbol === \"function\" ) {\n\tjQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];\n}\n\n// Populate the class2type map\njQuery.each( \"Boolean Number String Function Array Date RegExp Object Error Symbol\".split( \" \" ),\nfunction( i, name ) {\n\tclass2type[ \"[object \" + name + \"]\" ] = name.toLowerCase();\n} );\n\nfunction isArrayLike( obj ) {\n\n\t// Support: real iOS 8.2 only (not reproducible in simulator)\n\t// `in` check used to prevent JIT error (gh-2145)\n\t// hasOwn isn't used here due to false negatives\n\t// regarding Nodelist length in IE\n\tvar length = !!obj && \"length\" in obj && obj.length,\n\t\ttype = jQuery.type( obj );\n\n\tif ( type === \"function\" || jQuery.isWindow( obj ) ) {\n\t\treturn false;\n\t}\n\n\treturn type === \"array\" || length === 0 ||\n\t\ttypeof length === \"number\" && length > 0 && ( length - 1 ) in obj;\n}\nvar Sizzle =\n/*!\n * Sizzle CSS Selector Engine v2.3.3\n * https://sizzlejs.com/\n *\n * Copyright jQuery Foundation and other contributors\n * Released under the MIT license\n * http://jquery.org/license\n *\n * Date: 2016-08-08\n */\n(function( window ) {\n\nvar i,\n\tsupport,\n\tExpr,\n\tgetText,\n\tisXML,\n\ttokenize,\n\tcompile,\n\tselect,\n\toutermostContext,\n\tsortInput,\n\thasDuplicate,\n\n\t// Local document vars\n\tsetDocument,\n\tdocument,\n\tdocElem,\n\tdocumentIsHTML,\n\trbuggyQSA,\n\trbuggyMatches,\n\tmatches,\n\tcontains,\n\n\t// Instance-specific data\n\texpando = \"sizzle\" + 1 * new Date(),\n\tpreferredDoc = window.document,\n\tdirruns = 0,\n\tdone = 0,\n\tclassCache = createCache(),\n\ttokenCache = createCache(),\n\tcompilerCache = createCache(),\n\tsortOrder = function( a, b ) {\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t}\n\t\treturn 0;\n\t},\n\n\t// Instance methods\n\thasOwn = ({}).hasOwnProperty,\n\tarr = [],\n\tpop = arr.pop,\n\tpush_native = arr.push,\n\tpush = arr.push,\n\tslice = arr.slice,\n\t// Use a stripped-down indexOf as it's faster than native\n\t// https://jsperf.com/thor-indexof-vs-for/5\n\tindexOf = function( list, elem ) {\n\t\tvar i = 0,\n\t\t\tlen = list.length;\n\t\tfor ( ; i < len; i++ ) {\n\t\t\tif ( list[i] === elem ) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t},\n\n\tbooleans = \"checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped\",\n\n\t// Regular expressions\n\n\t// http://www.w3.org/TR/css3-selectors/#whitespace\n\twhitespace = \"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\",\n\n\t// http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier\n\tidentifier = \"(?:\\\\\\\\.|[\\\\w-]|[^\\0-\\\\xa0])+\",\n\n\t// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors\n\tattributes = \"\\\\[\" + whitespace + \"*(\" + identifier + \")(?:\" + whitespace +\n\t\t// Operator (capture 2)\n\t\t\"*([*^$|!~]?=)\" + whitespace +\n\t\t// \"Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]\"\n\t\t\"*(?:'((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\"|(\" + identifier + \"))|)\" + whitespace +\n\t\t\"*\\\\]\",\n\n\tpseudos = \":(\" + identifier + \")(?:\\\\((\" +\n\t\t// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:\n\t\t// 1. quoted (capture 3; capture 4 or capture 5)\n\t\t\"('((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\")|\" +\n\t\t// 2. simple (capture 6)\n\t\t\"((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\" + attributes + \")*)|\" +\n\t\t// 3. anything else (capture 2)\n\t\t\".*\" +\n\t\t\")\\\\)|)\",\n\n\t// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter\n\trwhitespace = new RegExp( whitespace + \"+\", \"g\" ),\n\trtrim = new RegExp( \"^\" + whitespace + \"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\" + whitespace + \"+$\", \"g\" ),\n\n\trcomma = new RegExp( \"^\" + whitespace + \"*,\" + whitespace + \"*\" ),\n\trcombinators = new RegExp( \"^\" + whitespace + \"*([>+~]|\" + whitespace + \")\" + whitespace + \"*\" ),\n\n\trattributeQuotes = new RegExp( \"=\" + whitespace + \"*([^\\\\]'\\\"]*?)\" + whitespace + \"*\\\\]\", \"g\" ),\n\n\trpseudo = new RegExp( pseudos ),\n\tridentifier = new RegExp( \"^\" + identifier + \"$\" ),\n\n\tmatchExpr = {\n\t\t\"ID\": new RegExp( \"^#(\" + identifier + \")\" ),\n\t\t\"CLASS\": new RegExp( \"^\\\\.(\" + identifier + \")\" ),\n\t\t\"TAG\": new RegExp( \"^(\" + identifier + \"|[*])\" ),\n\t\t\"ATTR\": new RegExp( \"^\" + attributes ),\n\t\t\"PSEUDO\": new RegExp( \"^\" + pseudos ),\n\t\t\"CHILD\": new RegExp( \"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\" + whitespace +\n\t\t\t\"*(even|odd|(([+-]|)(\\\\d*)n|)\" + whitespace + \"*(?:([+-]|)\" + whitespace +\n\t\t\t\"*(\\\\d+)|))\" + whitespace + \"*\\\\)|)\", \"i\" ),\n\t\t\"bool\": new RegExp( \"^(?:\" + booleans + \")$\", \"i\" ),\n\t\t// For use in libraries implementing .is()\n\t\t// We use this for POS matching in `select`\n\t\t\"needsContext\": new RegExp( \"^\" + whitespace + \"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\" +\n\t\t\twhitespace + \"*((?:-\\\\d)?\\\\d*)\" + whitespace + \"*\\\\)|)(?=[^-]|$)\", \"i\" )\n\t},\n\n\trinputs = /^(?:input|select|textarea|button)$/i,\n\trheader = /^h\\d$/i,\n\n\trnative = /^[^{]+\\{\\s*\\[native \\w/,\n\n\t// Easily-parseable/retrievable ID or TAG or CLASS selectors\n\trquickExpr = /^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,\n\n\trsibling = /[+~]/,\n\n\t// CSS escapes\n\t// http://www.w3.org/TR/CSS21/syndata.html#escaped-characters\n\trunescape = new RegExp( \"\\\\\\\\([\\\\da-f]{1,6}\" + whitespace + \"?|(\" + whitespace + \")|.)\", \"ig\" ),\n\tfunescape = function( _, escaped, escapedWhitespace ) {\n\t\tvar high = \"0x\" + escaped - 0x10000;\n\t\t// NaN means non-codepoint\n\t\t// Support: Firefox<24\n\t\t// Workaround erroneous numeric interpretation of +\"0x\"\n\t\treturn high !== high || escapedWhitespace ?\n\t\t\tescaped :\n\t\t\thigh < 0 ?\n\t\t\t\t// BMP codepoint\n\t\t\t\tString.fromCharCode( high + 0x10000 ) :\n\t\t\t\t// Supplemental Plane codepoint (surrogate pair)\n\t\t\t\tString.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );\n\t},\n\n\t// CSS string/identifier serialization\n\t// https://drafts.csswg.org/cssom/#common-serializing-idioms\n\trcssescape = /([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\0-\\x1f\\x7f-\\uFFFF\\w-]/g,\n\tfcssescape = function( ch, asCodePoint ) {\n\t\tif ( asCodePoint ) {\n\n\t\t\t// U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER\n\t\t\tif ( ch === \"\\0\" ) {\n\t\t\t\treturn \"\\uFFFD\";\n\t\t\t}\n\n\t\t\t// Control characters and (dependent upon position) numbers get escaped as code points\n\t\t\treturn ch.slice( 0, -1 ) + \"\\\\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + \" \";\n\t\t}\n\n\t\t// Other potentially-special ASCII characters get backslash-escaped\n\t\treturn \"\\\\\" + ch;\n\t},\n\n\t// Used for iframes\n\t// See setDocument()\n\t// Removing the function wrapper causes a \"Permission Denied\"\n\t// error in IE\n\tunloadHandler = function() {\n\t\tsetDocument();\n\t},\n\n\tdisabledAncestor = addCombinator(\n\t\tfunction( elem ) {\n\t\t\treturn elem.disabled === true && (\"form\" in elem || \"label\" in elem);\n\t\t},\n\t\t{ dir: \"parentNode\", next: \"legend\" }\n\t);\n\n// Optimize for push.apply( _, NodeList )\ntry {\n\tpush.apply(\n\t\t(arr = slice.call( preferredDoc.childNodes )),\n\t\tpreferredDoc.childNodes\n\t);\n\t// Support: Android<4.0\n\t// Detect silently failing push.apply\n\tarr[ preferredDoc.childNodes.length ].nodeType;\n} catch ( e ) {\n\tpush = { apply: arr.length ?\n\n\t\t// Leverage slice if possible\n\t\tfunction( target, els ) {\n\t\t\tpush_native.apply( target, slice.call(els) );\n\t\t} :\n\n\t\t// Support: IE<9\n\t\t// Otherwise append directly\n\t\tfunction( target, els ) {\n\t\t\tvar j = target.length,\n\t\t\t\ti = 0;\n\t\t\t// Can't trust NodeList.length\n\t\t\twhile ( (target[j++] = els[i++]) ) {}\n\t\t\ttarget.length = j - 1;\n\t\t}\n\t};\n}\n\nfunction Sizzle( selector, context, results, seed ) {\n\tvar m, i, elem, nid, match, groups, newSelector,\n\t\tnewContext = context && context.ownerDocument,\n\n\t\t// nodeType defaults to 9, since context defaults to document\n\t\tnodeType = context ? context.nodeType : 9;\n\n\tresults = results || [];\n\n\t// Return early from calls with invalid selector or context\n\tif ( typeof selector !== \"string\" || !selector ||\n\t\tnodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {\n\n\t\treturn results;\n\t}\n\n\t// Try to shortcut find operations (as opposed to filters) in HTML documents\n\tif ( !seed ) {\n\n\t\tif ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {\n\t\t\tsetDocument( context );\n\t\t}\n\t\tcontext = context || document;\n\n\t\tif ( documentIsHTML ) {\n\n\t\t\t// If the selector is sufficiently simple, try using a \"get*By*\" DOM method\n\t\t\t// (excepting DocumentFragment context, where the methods don't exist)\n\t\t\tif ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {\n\n\t\t\t\t// ID selector\n\t\t\t\tif ( (m = match[1]) ) {\n\n\t\t\t\t\t// Document context\n\t\t\t\t\tif ( nodeType === 9 ) {\n\t\t\t\t\t\tif ( (elem = context.getElementById( m )) ) {\n\n\t\t\t\t\t\t\t// Support: IE, Opera, Webkit\n\t\t\t\t\t\t\t// TODO: identify versions\n\t\t\t\t\t\t\t// getElementById can match elements by name instead of ID\n\t\t\t\t\t\t\tif ( elem.id === m ) {\n\t\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t// Element context\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// Support: IE, Opera, Webkit\n\t\t\t\t\t\t// TODO: identify versions\n\t\t\t\t\t\t// getElementById can match elements by name instead of ID\n\t\t\t\t\t\tif ( newContext && (elem = newContext.getElementById( m )) &&\n\t\t\t\t\t\t\tcontains( context, elem ) &&\n\t\t\t\t\t\t\telem.id === m ) {\n\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t// Type selector\n\t\t\t\t} else if ( match[2] ) {\n\t\t\t\t\tpush.apply( results, context.getElementsByTagName( selector ) );\n\t\t\t\t\treturn results;\n\n\t\t\t\t// Class selector\n\t\t\t\t} else if ( (m = match[3]) && support.getElementsByClassName &&\n\t\t\t\t\tcontext.getElementsByClassName ) {\n\n\t\t\t\t\tpush.apply( results, context.getElementsByClassName( m ) );\n\t\t\t\t\treturn results;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Take advantage of querySelectorAll\n\t\t\tif ( support.qsa &&\n\t\t\t\t!compilerCache[ selector + \" \" ] &&\n\t\t\t\t(!rbuggyQSA || !rbuggyQSA.test( selector )) ) {\n\n\t\t\t\tif ( nodeType !== 1 ) {\n\t\t\t\t\tnewContext = context;\n\t\t\t\t\tnewSelector = selector;\n\n\t\t\t\t// qSA looks outside Element context, which is not what we want\n\t\t\t\t// Thanks to Andrew Dupont for this workaround technique\n\t\t\t\t// Support: IE <=8\n\t\t\t\t// Exclude object elements\n\t\t\t\t} else if ( context.nodeName.toLowerCase() !== \"object\" ) {\n\n\t\t\t\t\t// Capture the context ID, setting it first if necessary\n\t\t\t\t\tif ( (nid = context.getAttribute( \"id\" )) ) {\n\t\t\t\t\t\tnid = nid.replace( rcssescape, fcssescape );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontext.setAttribute( \"id\", (nid = expando) );\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prefix every selector in the list\n\t\t\t\t\tgroups = tokenize( selector );\n\t\t\t\t\ti = groups.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tgroups[i] = \"#\" + nid + \" \" + toSelector( groups[i] );\n\t\t\t\t\t}\n\t\t\t\t\tnewSelector = groups.join( \",\" );\n\n\t\t\t\t\t// Expand context for sibling selectors\n\t\t\t\t\tnewContext = rsibling.test( selector ) && testContext( context.parentNode ) ||\n\t\t\t\t\t\tcontext;\n\t\t\t\t}\n\n\t\t\t\tif ( newSelector ) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tpush.apply( results,\n\t\t\t\t\t\t\tnewContext.querySelectorAll( newSelector )\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t} catch ( qsaError ) {\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif ( nid === expando ) {\n\t\t\t\t\t\t\tcontext.removeAttribute( \"id\" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// All others\n\treturn select( selector.replace( rtrim, \"$1\" ), context, results, seed );\n}\n\n/**\n * Create key-value caches of limited size\n * @returns {function(string, object)} Returns the Object data after storing it on itself with\n *\tproperty name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)\n *\tdeleting the oldest entry\n */\nfunction createCache() {\n\tvar keys = [];\n\n\tfunction cache( key, value ) {\n\t\t// Use (key + \" \") to avoid collision with native prototype properties (see Issue #157)\n\t\tif ( keys.push( key + \" \" ) > Expr.cacheLength ) {\n\t\t\t// Only keep the most recent entries\n\t\t\tdelete cache[ keys.shift() ];\n\t\t}\n\t\treturn (cache[ key + \" \" ] = value);\n\t}\n\treturn cache;\n}\n\n/**\n * Mark a function for special use by Sizzle\n * @param {Function} fn The function to mark\n */\nfunction markFunction( fn ) {\n\tfn[ expando ] = true;\n\treturn fn;\n}\n\n/**\n * Support testing using an element\n * @param {Function} fn Passed the created element and returns a boolean result\n */\nfunction assert( fn ) {\n\tvar el = document.createElement(\"fieldset\");\n\n\ttry {\n\t\treturn !!fn( el );\n\t} catch (e) {\n\t\treturn false;\n\t} finally {\n\t\t// Remove from its parent by default\n\t\tif ( el.parentNode ) {\n\t\t\tel.parentNode.removeChild( el );\n\t\t}\n\t\t// release memory in IE\n\t\tel = null;\n\t}\n}\n\n/**\n * Adds the same handler for all of the specified attrs\n * @param {String} attrs Pipe-separated list of attributes\n * @param {Function} handler The method that will be applied\n */\nfunction addHandle( attrs, handler ) {\n\tvar arr = attrs.split(\"|\"),\n\t\ti = arr.length;\n\n\twhile ( i-- ) {\n\t\tExpr.attrHandle[ arr[i] ] = handler;\n\t}\n}\n\n/**\n * Checks document order of two siblings\n * @param {Element} a\n * @param {Element} b\n * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b\n */\nfunction siblingCheck( a, b ) {\n\tvar cur = b && a,\n\t\tdiff = cur && a.nodeType === 1 && b.nodeType === 1 &&\n\t\t\ta.sourceIndex - b.sourceIndex;\n\n\t// Use IE sourceIndex if available on both nodes\n\tif ( diff ) {\n\t\treturn diff;\n\t}\n\n\t// Check if b follows a\n\tif ( cur ) {\n\t\twhile ( (cur = cur.nextSibling) ) {\n\t\t\tif ( cur === b ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn a ? 1 : -1;\n}\n\n/**\n * Returns a function to use in pseudos for input types\n * @param {String} type\n */\nfunction createInputPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn name === \"input\" && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for buttons\n * @param {String} type\n */\nfunction createButtonPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn (name === \"input\" || name === \"button\") && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for :enabled/:disabled\n * @param {Boolean} disabled true for :disabled; false for :enabled\n */\nfunction createDisabledPseudo( disabled ) {\n\n\t// Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable\n\treturn function( elem ) {\n\n\t\t// Only certain elements can match :enabled or :disabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled\n\t\tif ( \"form\" in elem ) {\n\n\t\t\t// Check for inherited disabledness on relevant non-disabled elements:\n\t\t\t// * listed form-associated elements in a disabled fieldset\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#category-listed\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled\n\t\t\t// * option elements in a disabled optgroup\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled\n\t\t\t// All such elements have a \"form\" property.\n\t\t\tif ( elem.parentNode && elem.disabled === false ) {\n\n\t\t\t\t// Option elements defer to a parent optgroup if present\n\t\t\t\tif ( \"label\" in elem ) {\n\t\t\t\t\tif ( \"label\" in elem.parentNode ) {\n\t\t\t\t\t\treturn elem.parentNode.disabled === disabled;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn elem.disabled === disabled;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Support: IE 6 - 11\n\t\t\t\t// Use the isDisabled shortcut property to check for disabled fieldset ancestors\n\t\t\t\treturn elem.isDisabled === disabled ||\n\n\t\t\t\t\t// Where there is no isDisabled, check manually\n\t\t\t\t\t/* jshint -W018 */\n\t\t\t\t\telem.isDisabled !== !disabled &&\n\t\t\t\t\t\tdisabledAncestor( elem ) === disabled;\n\t\t\t}\n\n\t\t\treturn elem.disabled === disabled;\n\n\t\t// Try to winnow out elements that can't be disabled before trusting the disabled property.\n\t\t// Some victims get caught in our net (label, legend, menu, track), but it shouldn't\n\t\t// even exist on them, let alone have a boolean value.\n\t\t} else if ( \"label\" in elem ) {\n\t\t\treturn elem.disabled === disabled;\n\t\t}\n\n\t\t// Remaining elements are neither :enabled nor :disabled\n\t\treturn false;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for positionals\n * @param {Function} fn\n */\nfunction createPositionalPseudo( fn ) {\n\treturn markFunction(function( argument ) {\n\t\targument = +argument;\n\t\treturn markFunction(function( seed, matches ) {\n\t\t\tvar j,\n\t\t\t\tmatchIndexes = fn( [], seed.length, argument ),\n\t\t\t\ti = matchIndexes.length;\n\n\t\t\t// Match elements found at the specified indexes\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( seed[ (j = matchIndexes[i]) ] ) {\n\t\t\t\t\tseed[j] = !(matches[j] = seed[j]);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n}\n\n/**\n * Checks a node for validity as a Sizzle context\n * @param {Element|Object=} context\n * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value\n */\nfunction testContext( context ) {\n\treturn context && typeof context.getElementsByTagName !== \"undefined\" && context;\n}\n\n// Expose support vars for convenience\nsupport = Sizzle.support = {};\n\n/**\n * Detects XML nodes\n * @param {Element|Object} elem An element or a document\n * @returns {Boolean} True iff elem is a non-HTML XML node\n */\nisXML = Sizzle.isXML = function( elem ) {\n\t// documentElement is verified for cases where it doesn't yet exist\n\t// (such as loading iframes in IE - #4833)\n\tvar documentElement = elem && (elem.ownerDocument || elem).documentElement;\n\treturn documentElement ? documentElement.nodeName !== \"HTML\" : false;\n};\n\n/**\n * Sets document-related variables once based on the current document\n * @param {Element|Object} [doc] An element or document object to use to set the document\n * @returns {Object} Returns the current document\n */\nsetDocument = Sizzle.setDocument = function( node ) {\n\tvar hasCompare, subWindow,\n\t\tdoc = node ? node.ownerDocument || node : preferredDoc;\n\n\t// Return early if doc is invalid or already selected\n\tif ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {\n\t\treturn document;\n\t}\n\n\t// Update global variables\n\tdocument = doc;\n\tdocElem = document.documentElement;\n\tdocumentIsHTML = !isXML( document );\n\n\t// Support: IE 9-11, Edge\n\t// Accessing iframe documents after unload throws \"permission denied\" errors (jQuery #13936)\n\tif ( preferredDoc !== document &&\n\t\t(subWindow = document.defaultView) && subWindow.top !== subWindow ) {\n\n\t\t// Support: IE 11, Edge\n\t\tif ( subWindow.addEventListener ) {\n\t\t\tsubWindow.addEventListener( \"unload\", unloadHandler, false );\n\n\t\t// Support: IE 9 - 10 only\n\t\t} else if ( subWindow.attachEvent ) {\n\t\t\tsubWindow.attachEvent( \"onunload\", unloadHandler );\n\t\t}\n\t}\n\n\t/* Attributes\n\t---------------------------------------------------------------------- */\n\n\t// Support: IE<8\n\t// Verify that getAttribute really returns attributes and not properties\n\t// (excepting IE8 booleans)\n\tsupport.attributes = assert(function( el ) {\n\t\tel.className = \"i\";\n\t\treturn !el.getAttribute(\"className\");\n\t});\n\n\t/* getElement(s)By*\n\t---------------------------------------------------------------------- */\n\n\t// Check if getElementsByTagName(\"*\") returns only elements\n\tsupport.getElementsByTagName = assert(function( el ) {\n\t\tel.appendChild( document.createComment(\"\") );\n\t\treturn !el.getElementsByTagName(\"*\").length;\n\t});\n\n\t// Support: IE<9\n\tsupport.getElementsByClassName = rnative.test( document.getElementsByClassName );\n\n\t// Support: IE<10\n\t// Check if getElementById returns elements by name\n\t// The broken getElementById methods don't pick up programmatically-set names,\n\t// so use a roundabout getElementsByName test\n\tsupport.getById = assert(function( el ) {\n\t\tdocElem.appendChild( el ).id = expando;\n\t\treturn !document.getElementsByName || !document.getElementsByName( expando ).length;\n\t});\n\n\t// ID filter and find\n\tif ( support.getById ) {\n\t\tExpr.filter[\"ID\"] = function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn elem.getAttribute(\"id\") === attrId;\n\t\t\t};\n\t\t};\n\t\tExpr.find[\"ID\"] = function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && documentIsHTML ) {\n\t\t\t\tvar elem = context.getElementById( id );\n\t\t\t\treturn elem ? [ elem ] : [];\n\t\t\t}\n\t\t};\n\t} else {\n\t\tExpr.filter[\"ID\"] =  function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\tvar node = typeof elem.getAttributeNode !== \"undefined\" &&\n\t\t\t\t\telem.getAttributeNode(\"id\");\n\t\t\t\treturn node && node.value === attrId;\n\t\t\t};\n\t\t};\n\n\t\t// Support: IE 6 - 7 only\n\t\t// getElementById is not reliable as a find shortcut\n\t\tExpr.find[\"ID\"] = function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && documentIsHTML ) {\n\t\t\t\tvar node, i, elems,\n\t\t\t\t\telem = context.getElementById( id );\n\n\t\t\t\tif ( elem ) {\n\n\t\t\t\t\t// Verify the id attribute\n\t\t\t\t\tnode = elem.getAttributeNode(\"id\");\n\t\t\t\t\tif ( node && node.value === id ) {\n\t\t\t\t\t\treturn [ elem ];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Fall back on getElementsByName\n\t\t\t\t\telems = context.getElementsByName( id );\n\t\t\t\t\ti = 0;\n\t\t\t\t\twhile ( (elem = elems[i++]) ) {\n\t\t\t\t\t\tnode = elem.getAttributeNode(\"id\");\n\t\t\t\t\t\tif ( node && node.value === id ) {\n\t\t\t\t\t\t\treturn [ elem ];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn [];\n\t\t\t}\n\t\t};\n\t}\n\n\t// Tag\n\tExpr.find[\"TAG\"] = support.getElementsByTagName ?\n\t\tfunction( tag, context ) {\n\t\t\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\t\t\treturn context.getElementsByTagName( tag );\n\n\t\t\t// DocumentFragment nodes don't have gEBTN\n\t\t\t} else if ( support.qsa ) {\n\t\t\t\treturn context.querySelectorAll( tag );\n\t\t\t}\n\t\t} :\n\n\t\tfunction( tag, context ) {\n\t\t\tvar elem,\n\t\t\t\ttmp = [],\n\t\t\t\ti = 0,\n\t\t\t\t// By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too\n\t\t\t\tresults = context.getElementsByTagName( tag );\n\n\t\t\t// Filter out possible comments\n\t\t\tif ( tag === \"*\" ) {\n\t\t\t\twhile ( (elem = results[i++]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\ttmp.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn tmp;\n\t\t\t}\n\t\t\treturn results;\n\t\t};\n\n\t// Class\n\tExpr.find[\"CLASS\"] = support.getElementsByClassName && function( className, context ) {\n\t\tif ( typeof context.getElementsByClassName !== \"undefined\" && documentIsHTML ) {\n\t\t\treturn context.getElementsByClassName( className );\n\t\t}\n\t};\n\n\t/* QSA/matchesSelector\n\t---------------------------------------------------------------------- */\n\n\t// QSA and matchesSelector support\n\n\t// matchesSelector(:active) reports false when true (IE9/Opera 11.5)\n\trbuggyMatches = [];\n\n\t// qSa(:focus) reports false when true (Chrome 21)\n\t// We allow this because of a bug in IE8/9 that throws an error\n\t// whenever `document.activeElement` is accessed on an iframe\n\t// So, we allow :focus to pass through QSA all the time to avoid the IE error\n\t// See https://bugs.jquery.com/ticket/13378\n\trbuggyQSA = [];\n\n\tif ( (support.qsa = rnative.test( document.querySelectorAll )) ) {\n\t\t// Build QSA regex\n\t\t// Regex strategy adopted from Diego Perini\n\t\tassert(function( el ) {\n\t\t\t// Select is set to empty string on purpose\n\t\t\t// This is to test IE's treatment of not explicitly\n\t\t\t// setting a boolean content attribute,\n\t\t\t// since its presence should be enough\n\t\t\t// https://bugs.jquery.com/ticket/12359\n\t\t\tdocElem.appendChild( el ).innerHTML = \"<a id='\" + expando + \"'></a>\" +\n\t\t\t\t\"<select id='\" + expando + \"-\\r\\\\' msallowcapture=''>\" +\n\t\t\t\t\"<option selected=''></option></select>\";\n\n\t\t\t// Support: IE8, Opera 11-12.16\n\t\t\t// Nothing should be selected when empty strings follow ^= or $= or *=\n\t\t\t// The test attribute must be unknown in Opera but \"safe\" for WinRT\n\t\t\t// https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section\n\t\t\tif ( el.querySelectorAll(\"[msallowcapture^='']\").length ) {\n\t\t\t\trbuggyQSA.push( \"[*^$]=\" + whitespace + \"*(?:''|\\\"\\\")\" );\n\t\t\t}\n\n\t\t\t// Support: IE8\n\t\t\t// Boolean attributes and \"value\" are not treated correctly\n\t\t\tif ( !el.querySelectorAll(\"[selected]\").length ) {\n\t\t\t\trbuggyQSA.push( \"\\\\[\" + whitespace + \"*(?:value|\" + booleans + \")\" );\n\t\t\t}\n\n\t\t\t// Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+\n\t\t\tif ( !el.querySelectorAll( \"[id~=\" + expando + \"-]\" ).length ) {\n\t\t\t\trbuggyQSA.push(\"~=\");\n\t\t\t}\n\n\t\t\t// Webkit/Opera - :checked should return selected option elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( !el.querySelectorAll(\":checked\").length ) {\n\t\t\t\trbuggyQSA.push(\":checked\");\n\t\t\t}\n\n\t\t\t// Support: Safari 8+, iOS 8+\n\t\t\t// https://bugs.webkit.org/show_bug.cgi?id=136851\n\t\t\t// In-page `selector#id sibling-combinator selector` fails\n\t\t\tif ( !el.querySelectorAll( \"a#\" + expando + \"+*\" ).length ) {\n\t\t\t\trbuggyQSA.push(\".#.+[+~]\");\n\t\t\t}\n\t\t});\n\n\t\tassert(function( el ) {\n\t\t\tel.innerHTML = \"<a href='' disabled='disabled'></a>\" +\n\t\t\t\t\"<select disabled='disabled'><option/></select>\";\n\n\t\t\t// Support: Windows 8 Native Apps\n\t\t\t// The type and name attributes are restricted during .innerHTML assignment\n\t\t\tvar input = document.createElement(\"input\");\n\t\t\tinput.setAttribute( \"type\", \"hidden\" );\n\t\t\tel.appendChild( input ).setAttribute( \"name\", \"D\" );\n\n\t\t\t// Support: IE8\n\t\t\t// Enforce case-sensitivity of name attribute\n\t\t\tif ( el.querySelectorAll(\"[name=d]\").length ) {\n\t\t\t\trbuggyQSA.push( \"name\" + whitespace + \"*[*^$|!~]?=\" );\n\t\t\t}\n\n\t\t\t// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( el.querySelectorAll(\":enabled\").length !== 2 ) {\n\t\t\t\trbuggyQSA.push( \":enabled\", \":disabled\" );\n\t\t\t}\n\n\t\t\t// Support: IE9-11+\n\t\t\t// IE's :disabled selector does not pick up the children of disabled fieldsets\n\t\t\tdocElem.appendChild( el ).disabled = true;\n\t\t\tif ( el.querySelectorAll(\":disabled\").length !== 2 ) {\n\t\t\t\trbuggyQSA.push( \":enabled\", \":disabled\" );\n\t\t\t}\n\n\t\t\t// Opera 10-11 does not throw on post-comma invalid pseudos\n\t\t\tel.querySelectorAll(\"*,:x\");\n\t\t\trbuggyQSA.push(\",.*:\");\n\t\t});\n\t}\n\n\tif ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||\n\t\tdocElem.webkitMatchesSelector ||\n\t\tdocElem.mozMatchesSelector ||\n\t\tdocElem.oMatchesSelector ||\n\t\tdocElem.msMatchesSelector) )) ) {\n\n\t\tassert(function( el ) {\n\t\t\t// Check to see if it's possible to do matchesSelector\n\t\t\t// on a disconnected node (IE 9)\n\t\t\tsupport.disconnectedMatch = matches.call( el, \"*\" );\n\n\t\t\t// This should fail with an exception\n\t\t\t// Gecko does not error, returns false instead\n\t\t\tmatches.call( el, \"[s!='']:x\" );\n\t\t\trbuggyMatches.push( \"!=\", pseudos );\n\t\t});\n\t}\n\n\trbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join(\"|\") );\n\trbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join(\"|\") );\n\n\t/* Contains\n\t---------------------------------------------------------------------- */\n\thasCompare = rnative.test( docElem.compareDocumentPosition );\n\n\t// Element contains another\n\t// Purposefully self-exclusive\n\t// As in, an element does not contain itself\n\tcontains = hasCompare || rnative.test( docElem.contains ) ?\n\t\tfunction( a, b ) {\n\t\t\tvar adown = a.nodeType === 9 ? a.documentElement : a,\n\t\t\t\tbup = b && b.parentNode;\n\t\t\treturn a === bup || !!( bup && bup.nodeType === 1 && (\n\t\t\t\tadown.contains ?\n\t\t\t\t\tadown.contains( bup ) :\n\t\t\t\t\ta.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16\n\t\t\t));\n\t\t} :\n\t\tfunction( a, b ) {\n\t\t\tif ( b ) {\n\t\t\t\twhile ( (b = b.parentNode) ) {\n\t\t\t\t\tif ( b === a ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\n\t/* Sorting\n\t---------------------------------------------------------------------- */\n\n\t// Document order sorting\n\tsortOrder = hasCompare ?\n\tfunction( a, b ) {\n\n\t\t// Flag for duplicate removal\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\t// Sort on method existence if only one input has compareDocumentPosition\n\t\tvar compare = !a.compareDocumentPosition - !b.compareDocumentPosition;\n\t\tif ( compare ) {\n\t\t\treturn compare;\n\t\t}\n\n\t\t// Calculate position if both inputs belong to the same document\n\t\tcompare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?\n\t\t\ta.compareDocumentPosition( b ) :\n\n\t\t\t// Otherwise we know they are disconnected\n\t\t\t1;\n\n\t\t// Disconnected nodes\n\t\tif ( compare & 1 ||\n\t\t\t(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {\n\n\t\t\t// Choose the first element that is related to our preferred document\n\t\t\tif ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tif ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\t// Maintain original order\n\t\t\treturn sortInput ?\n\t\t\t\t( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :\n\t\t\t\t0;\n\t\t}\n\n\t\treturn compare & 4 ? -1 : 1;\n\t} :\n\tfunction( a, b ) {\n\t\t// Exit early if the nodes are identical\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\taup = a.parentNode,\n\t\t\tbup = b.parentNode,\n\t\t\tap = [ a ],\n\t\t\tbp = [ b ];\n\n\t\t// Parentless nodes are either documents or disconnected\n\t\tif ( !aup || !bup ) {\n\t\t\treturn a === document ? -1 :\n\t\t\t\tb === document ? 1 :\n\t\t\t\taup ? -1 :\n\t\t\t\tbup ? 1 :\n\t\t\t\tsortInput ?\n\t\t\t\t( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :\n\t\t\t\t0;\n\n\t\t// If the nodes are siblings, we can do a quick check\n\t\t} else if ( aup === bup ) {\n\t\t\treturn siblingCheck( a, b );\n\t\t}\n\n\t\t// Otherwise we need full lists of their ancestors for comparison\n\t\tcur = a;\n\t\twhile ( (cur = cur.parentNode) ) {\n\t\t\tap.unshift( cur );\n\t\t}\n\t\tcur = b;\n\t\twhile ( (cur = cur.parentNode) ) {\n\t\t\tbp.unshift( cur );\n\t\t}\n\n\t\t// Walk down the tree looking for a discrepancy\n\t\twhile ( ap[i] === bp[i] ) {\n\t\t\ti++;\n\t\t}\n\n\t\treturn i ?\n\t\t\t// Do a sibling check if the nodes have a common ancestor\n\t\t\tsiblingCheck( ap[i], bp[i] ) :\n\n\t\t\t// Otherwise nodes in our document sort first\n\t\t\tap[i] === preferredDoc ? -1 :\n\t\t\tbp[i] === preferredDoc ? 1 :\n\t\t\t0;\n\t};\n\n\treturn document;\n};\n\nSizzle.matches = function( expr, elements ) {\n\treturn Sizzle( expr, null, null, elements );\n};\n\nSizzle.matchesSelector = function( elem, expr ) {\n\t// Set document vars if needed\n\tif ( ( elem.ownerDocument || elem ) !== document ) {\n\t\tsetDocument( elem );\n\t}\n\n\t// Make sure that attribute selectors are quoted\n\texpr = expr.replace( rattributeQuotes, \"='$1']\" );\n\n\tif ( support.matchesSelector && documentIsHTML &&\n\t\t!compilerCache[ expr + \" \" ] &&\n\t\t( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&\n\t\t( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {\n\n\t\ttry {\n\t\t\tvar ret = matches.call( elem, expr );\n\n\t\t\t// IE 9's matchesSelector returns false on disconnected nodes\n\t\t\tif ( ret || support.disconnectedMatch ||\n\t\t\t\t\t// As well, disconnected nodes are said to be in a document\n\t\t\t\t\t// fragment in IE 9\n\t\t\t\t\telem.document && elem.document.nodeType !== 11 ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\t\t} catch (e) {}\n\t}\n\n\treturn Sizzle( expr, document, null, [ elem ] ).length > 0;\n};\n\nSizzle.contains = function( context, elem ) {\n\t// Set document vars if needed\n\tif ( ( context.ownerDocument || context ) !== document ) {\n\t\tsetDocument( context );\n\t}\n\treturn contains( context, elem );\n};\n\nSizzle.attr = function( elem, name ) {\n\t// Set document vars if needed\n\tif ( ( elem.ownerDocument || elem ) !== document ) {\n\t\tsetDocument( elem );\n\t}\n\n\tvar fn = Expr.attrHandle[ name.toLowerCase() ],\n\t\t// Don't get fooled by Object.prototype properties (jQuery #13807)\n\t\tval = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?\n\t\t\tfn( elem, name, !documentIsHTML ) :\n\t\t\tundefined;\n\n\treturn val !== undefined ?\n\t\tval :\n\t\tsupport.attributes || !documentIsHTML ?\n\t\t\telem.getAttribute( name ) :\n\t\t\t(val = elem.getAttributeNode(name)) && val.specified ?\n\t\t\t\tval.value :\n\t\t\t\tnull;\n};\n\nSizzle.escape = function( sel ) {\n\treturn (sel + \"\").replace( rcssescape, fcssescape );\n};\n\nSizzle.error = function( msg ) {\n\tthrow new Error( \"Syntax error, unrecognized expression: \" + msg );\n};\n\n/**\n * Document sorting and removing duplicates\n * @param {ArrayLike} results\n */\nSizzle.uniqueSort = function( results ) {\n\tvar elem,\n\t\tduplicates = [],\n\t\tj = 0,\n\t\ti = 0;\n\n\t// Unless we *know* we can detect duplicates, assume their presence\n\thasDuplicate = !support.detectDuplicates;\n\tsortInput = !support.sortStable && results.slice( 0 );\n\tresults.sort( sortOrder );\n\n\tif ( hasDuplicate ) {\n\t\twhile ( (elem = results[i++]) ) {\n\t\t\tif ( elem === results[ i ] ) {\n\t\t\t\tj = duplicates.push( i );\n\t\t\t}\n\t\t}\n\t\twhile ( j-- ) {\n\t\t\tresults.splice( duplicates[ j ], 1 );\n\t\t}\n\t}\n\n\t// Clear input after sorting to release objects\n\t// See https://github.com/jquery/sizzle/pull/225\n\tsortInput = null;\n\n\treturn results;\n};\n\n/**\n * Utility function for retrieving the text value of an array of DOM nodes\n * @param {Array|Element} elem\n */\ngetText = Sizzle.getText = function( elem ) {\n\tvar node,\n\t\tret = \"\",\n\t\ti = 0,\n\t\tnodeType = elem.nodeType;\n\n\tif ( !nodeType ) {\n\t\t// If no nodeType, this is expected to be an array\n\t\twhile ( (node = elem[i++]) ) {\n\t\t\t// Do not traverse comment nodes\n\t\t\tret += getText( node );\n\t\t}\n\t} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {\n\t\t// Use textContent for elements\n\t\t// innerText usage removed for consistency of new lines (jQuery #11153)\n\t\tif ( typeof elem.textContent === \"string\" ) {\n\t\t\treturn elem.textContent;\n\t\t} else {\n\t\t\t// Traverse its children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tret += getText( elem );\n\t\t\t}\n\t\t}\n\t} else if ( nodeType === 3 || nodeType === 4 ) {\n\t\treturn elem.nodeValue;\n\t}\n\t// Do not include comment or processing instruction nodes\n\n\treturn ret;\n};\n\nExpr = Sizzle.selectors = {\n\n\t// Can be adjusted by the user\n\tcacheLength: 50,\n\n\tcreatePseudo: markFunction,\n\n\tmatch: matchExpr,\n\n\tattrHandle: {},\n\n\tfind: {},\n\n\trelative: {\n\t\t\">\": { dir: \"parentNode\", first: true },\n\t\t\" \": { dir: \"parentNode\" },\n\t\t\"+\": { dir: \"previousSibling\", first: true },\n\t\t\"~\": { dir: \"previousSibling\" }\n\t},\n\n\tpreFilter: {\n\t\t\"ATTR\": function( match ) {\n\t\t\tmatch[1] = match[1].replace( runescape, funescape );\n\n\t\t\t// Move the given value to match[3] whether quoted or unquoted\n\t\t\tmatch[3] = ( match[3] || match[4] || match[5] || \"\" ).replace( runescape, funescape );\n\n\t\t\tif ( match[2] === \"~=\" ) {\n\t\t\t\tmatch[3] = \" \" + match[3] + \" \";\n\t\t\t}\n\n\t\t\treturn match.slice( 0, 4 );\n\t\t},\n\n\t\t\"CHILD\": function( match ) {\n\t\t\t/* matches from matchExpr[\"CHILD\"]\n\t\t\t\t1 type (only|nth|...)\n\t\t\t\t2 what (child|of-type)\n\t\t\t\t3 argument (even|odd|\\d*|\\d*n([+-]\\d+)?|...)\n\t\t\t\t4 xn-component of xn+y argument ([+-]?\\d*n|)\n\t\t\t\t5 sign of xn-component\n\t\t\t\t6 x of xn-component\n\t\t\t\t7 sign of y-component\n\t\t\t\t8 y of y-component\n\t\t\t*/\n\t\t\tmatch[1] = match[1].toLowerCase();\n\n\t\t\tif ( match[1].slice( 0, 3 ) === \"nth\" ) {\n\t\t\t\t// nth-* requires argument\n\t\t\t\tif ( !match[3] ) {\n\t\t\t\t\tSizzle.error( match[0] );\n\t\t\t\t}\n\n\t\t\t\t// numeric x and y parameters for Expr.filter.CHILD\n\t\t\t\t// remember that false/true cast respectively to 0/1\n\t\t\t\tmatch[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === \"even\" || match[3] === \"odd\" ) );\n\t\t\t\tmatch[5] = +( ( match[7] + match[8] ) || match[3] === \"odd\" );\n\n\t\t\t// other types prohibit arguments\n\t\t\t} else if ( match[3] ) {\n\t\t\t\tSizzle.error( match[0] );\n\t\t\t}\n\n\t\t\treturn match;\n\t\t},\n\n\t\t\"PSEUDO\": function( match ) {\n\t\t\tvar excess,\n\t\t\t\tunquoted = !match[6] && match[2];\n\n\t\t\tif ( matchExpr[\"CHILD\"].test( match[0] ) ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Accept quoted arguments as-is\n\t\t\tif ( match[3] ) {\n\t\t\t\tmatch[2] = match[4] || match[5] || \"\";\n\n\t\t\t// Strip excess characters from unquoted arguments\n\t\t\t} else if ( unquoted && rpseudo.test( unquoted ) &&\n\t\t\t\t// Get excess from tokenize (recursively)\n\t\t\t\t(excess = tokenize( unquoted, true )) &&\n\t\t\t\t// advance to the next closing parenthesis\n\t\t\t\t(excess = unquoted.indexOf( \")\", unquoted.length - excess ) - unquoted.length) ) {\n\n\t\t\t\t// excess is a negative index\n\t\t\t\tmatch[0] = match[0].slice( 0, excess );\n\t\t\t\tmatch[2] = unquoted.slice( 0, excess );\n\t\t\t}\n\n\t\t\t// Return only captures needed by the pseudo filter method (type and argument)\n\t\t\treturn match.slice( 0, 3 );\n\t\t}\n\t},\n\n\tfilter: {\n\n\t\t\"TAG\": function( nodeNameSelector ) {\n\t\t\tvar nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn nodeNameSelector === \"*\" ?\n\t\t\t\tfunction() { return true; } :\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn elem.nodeName && elem.nodeName.toLowerCase() === nodeName;\n\t\t\t\t};\n\t\t},\n\n\t\t\"CLASS\": function( className ) {\n\t\t\tvar pattern = classCache[ className + \" \" ];\n\n\t\t\treturn pattern ||\n\t\t\t\t(pattern = new RegExp( \"(^|\" + whitespace + \")\" + className + \"(\" + whitespace + \"|$)\" )) &&\n\t\t\t\tclassCache( className, function( elem ) {\n\t\t\t\t\treturn pattern.test( typeof elem.className === \"string\" && elem.className || typeof elem.getAttribute !== \"undefined\" && elem.getAttribute(\"class\") || \"\" );\n\t\t\t\t});\n\t\t},\n\n\t\t\"ATTR\": function( name, operator, check ) {\n\t\t\treturn function( elem ) {\n\t\t\t\tvar result = Sizzle.attr( elem, name );\n\n\t\t\t\tif ( result == null ) {\n\t\t\t\t\treturn operator === \"!=\";\n\t\t\t\t}\n\t\t\t\tif ( !operator ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tresult += \"\";\n\n\t\t\t\treturn operator === \"=\" ? result === check :\n\t\t\t\t\toperator === \"!=\" ? result !== check :\n\t\t\t\t\toperator === \"^=\" ? check && result.indexOf( check ) === 0 :\n\t\t\t\t\toperator === \"*=\" ? check && result.indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"$=\" ? check && result.slice( -check.length ) === check :\n\t\t\t\t\toperator === \"~=\" ? ( \" \" + result.replace( rwhitespace, \" \" ) + \" \" ).indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"|=\" ? result === check || result.slice( 0, check.length + 1 ) === check + \"-\" :\n\t\t\t\t\tfalse;\n\t\t\t};\n\t\t},\n\n\t\t\"CHILD\": function( type, what, argument, first, last ) {\n\t\t\tvar simple = type.slice( 0, 3 ) !== \"nth\",\n\t\t\t\tforward = type.slice( -4 ) !== \"last\",\n\t\t\t\tofType = what === \"of-type\";\n\n\t\t\treturn first === 1 && last === 0 ?\n\n\t\t\t\t// Shortcut for :nth-*(n)\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn !!elem.parentNode;\n\t\t\t\t} :\n\n\t\t\t\tfunction( elem, context, xml ) {\n\t\t\t\t\tvar cache, uniqueCache, outerCache, node, nodeIndex, start,\n\t\t\t\t\t\tdir = simple !== forward ? \"nextSibling\" : \"previousSibling\",\n\t\t\t\t\t\tparent = elem.parentNode,\n\t\t\t\t\t\tname = ofType && elem.nodeName.toLowerCase(),\n\t\t\t\t\t\tuseCache = !xml && !ofType,\n\t\t\t\t\t\tdiff = false;\n\n\t\t\t\t\tif ( parent ) {\n\n\t\t\t\t\t\t// :(first|last|only)-(child|of-type)\n\t\t\t\t\t\tif ( simple ) {\n\t\t\t\t\t\t\twhile ( dir ) {\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\twhile ( (node = node[ dir ]) ) {\n\t\t\t\t\t\t\t\t\tif ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnode.nodeName.toLowerCase() === name :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) {\n\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Reverse direction for :only-* (if we haven't yet done so)\n\t\t\t\t\t\t\t\tstart = dir = type === \"only\" && !start && \"nextSibling\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstart = [ forward ? parent.firstChild : parent.lastChild ];\n\n\t\t\t\t\t\t// non-xml :nth-child(...) stores cache data on `parent`\n\t\t\t\t\t\tif ( forward && useCache ) {\n\n\t\t\t\t\t\t\t// Seek `elem` from a previously-cached index\n\n\t\t\t\t\t\t\t// ...in a gzip-friendly way\n\t\t\t\t\t\t\tnode = parent;\n\t\t\t\t\t\t\touterCache = node[ expando ] || (node[ expando ] = {});\n\n\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t(outerCache[ node.uniqueID ] = {});\n\n\t\t\t\t\t\t\tcache = uniqueCache[ type ] || [];\n\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\tdiff = nodeIndex && cache[ 2 ];\n\t\t\t\t\t\t\tnode = nodeIndex && parent.childNodes[ nodeIndex ];\n\n\t\t\t\t\t\t\twhile ( (node = ++nodeIndex && node && node[ dir ] ||\n\n\t\t\t\t\t\t\t\t// Fallback to seeking `elem` from the start\n\t\t\t\t\t\t\t\t(diff = nodeIndex = 0) || start.pop()) ) {\n\n\t\t\t\t\t\t\t\t// When found, cache indexes on `parent` and break\n\t\t\t\t\t\t\t\tif ( node.nodeType === 1 && ++diff && node === elem ) {\n\t\t\t\t\t\t\t\t\tuniqueCache[ type ] = [ dirruns, nodeIndex, diff ];\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Use previously-cached element index if available\n\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t// ...in a gzip-friendly way\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\touterCache = node[ expando ] || (node[ expando ] = {});\n\n\t\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t\t(outerCache[ node.uniqueID ] = {});\n\n\t\t\t\t\t\t\t\tcache = uniqueCache[ type ] || [];\n\t\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\t\tdiff = nodeIndex;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// xml :nth-child(...)\n\t\t\t\t\t\t\t// or :nth-last-child(...) or :nth(-last)?-of-type(...)\n\t\t\t\t\t\t\tif ( diff === false ) {\n\t\t\t\t\t\t\t\t// Use the same loop as above to seek `elem` from the start\n\t\t\t\t\t\t\t\twhile ( (node = ++nodeIndex && node && node[ dir ] ||\n\t\t\t\t\t\t\t\t\t(diff = nodeIndex = 0) || start.pop()) ) {\n\n\t\t\t\t\t\t\t\t\tif ( ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnode.nodeName.toLowerCase() === name :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) &&\n\t\t\t\t\t\t\t\t\t\t++diff ) {\n\n\t\t\t\t\t\t\t\t\t\t// Cache the index of each encountered element\n\t\t\t\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t\t\t\touterCache = node[ expando ] || (node[ expando ] = {});\n\n\t\t\t\t\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t\t\t\t\t(outerCache[ node.uniqueID ] = {});\n\n\t\t\t\t\t\t\t\t\t\t\tuniqueCache[ type ] = [ dirruns, diff ];\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\tif ( node === elem ) {\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Incorporate the offset, then check against cycle size\n\t\t\t\t\t\tdiff -= last;\n\t\t\t\t\t\treturn diff === first || ( diff % first === 0 && diff / first >= 0 );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t},\n\n\t\t\"PSEUDO\": function( pseudo, argument ) {\n\t\t\t// pseudo-class names are case-insensitive\n\t\t\t// http://www.w3.org/TR/selectors/#pseudo-classes\n\t\t\t// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters\n\t\t\t// Remember that setFilters inherits from pseudos\n\t\t\tvar args,\n\t\t\t\tfn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||\n\t\t\t\t\tSizzle.error( \"unsupported pseudo: \" + pseudo );\n\n\t\t\t// The user may use createPseudo to indicate that\n\t\t\t// arguments are needed to create the filter function\n\t\t\t// just as Sizzle does\n\t\t\tif ( fn[ expando ] ) {\n\t\t\t\treturn fn( argument );\n\t\t\t}\n\n\t\t\t// But maintain support for old signatures\n\t\t\tif ( fn.length > 1 ) {\n\t\t\t\targs = [ pseudo, pseudo, \"\", argument ];\n\t\t\t\treturn Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?\n\t\t\t\t\tmarkFunction(function( seed, matches ) {\n\t\t\t\t\t\tvar idx,\n\t\t\t\t\t\t\tmatched = fn( seed, argument ),\n\t\t\t\t\t\t\ti = matched.length;\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tidx = indexOf( seed, matched[i] );\n\t\t\t\t\t\t\tseed[ idx ] = !( matches[ idx ] = matched[i] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}) :\n\t\t\t\t\tfunction( elem ) {\n\t\t\t\t\t\treturn fn( elem, 0, args );\n\t\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn fn;\n\t\t}\n\t},\n\n\tpseudos: {\n\t\t// Potentially complex pseudos\n\t\t\"not\": markFunction(function( selector ) {\n\t\t\t// Trim the selector passed to compile\n\t\t\t// to avoid treating leading and trailing\n\t\t\t// spaces as combinators\n\t\t\tvar input = [],\n\t\t\t\tresults = [],\n\t\t\t\tmatcher = compile( selector.replace( rtrim, \"$1\" ) );\n\n\t\t\treturn matcher[ expando ] ?\n\t\t\t\tmarkFunction(function( seed, matches, context, xml ) {\n\t\t\t\t\tvar elem,\n\t\t\t\t\t\tunmatched = matcher( seed, null, xml, [] ),\n\t\t\t\t\t\ti = seed.length;\n\n\t\t\t\t\t// Match elements unmatched by `matcher`\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( (elem = unmatched[i]) ) {\n\t\t\t\t\t\t\tseed[i] = !(matches[i] = elem);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}) :\n\t\t\t\tfunction( elem, context, xml ) {\n\t\t\t\t\tinput[0] = elem;\n\t\t\t\t\tmatcher( input, null, xml, results );\n\t\t\t\t\t// Don't keep the element (issue #299)\n\t\t\t\t\tinput[0] = null;\n\t\t\t\t\treturn !results.pop();\n\t\t\t\t};\n\t\t}),\n\n\t\t\"has\": markFunction(function( selector ) {\n\t\t\treturn function( elem ) {\n\t\t\t\treturn Sizzle( selector, elem ).length > 0;\n\t\t\t};\n\t\t}),\n\n\t\t\"contains\": markFunction(function( text ) {\n\t\t\ttext = text.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;\n\t\t\t};\n\t\t}),\n\n\t\t// \"Whether an element is represented by a :lang() selector\n\t\t// is based solely on the element's language value\n\t\t// being equal to the identifier C,\n\t\t// or beginning with the identifier C immediately followed by \"-\".\n\t\t// The matching of C against the element's language value is performed case-insensitively.\n\t\t// The identifier C does not have to be a valid language name.\"\n\t\t// http://www.w3.org/TR/selectors/#lang-pseudo\n\t\t\"lang\": markFunction( function( lang ) {\n\t\t\t// lang value must be a valid identifier\n\t\t\tif ( !ridentifier.test(lang || \"\") ) {\n\t\t\t\tSizzle.error( \"unsupported lang: \" + lang );\n\t\t\t}\n\t\t\tlang = lang.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn function( elem ) {\n\t\t\t\tvar elemLang;\n\t\t\t\tdo {\n\t\t\t\t\tif ( (elemLang = documentIsHTML ?\n\t\t\t\t\t\telem.lang :\n\t\t\t\t\t\telem.getAttribute(\"xml:lang\") || elem.getAttribute(\"lang\")) ) {\n\n\t\t\t\t\t\telemLang = elemLang.toLowerCase();\n\t\t\t\t\t\treturn elemLang === lang || elemLang.indexOf( lang + \"-\" ) === 0;\n\t\t\t\t\t}\n\t\t\t\t} while ( (elem = elem.parentNode) && elem.nodeType === 1 );\n\t\t\t\treturn false;\n\t\t\t};\n\t\t}),\n\n\t\t// Miscellaneous\n\t\t\"target\": function( elem ) {\n\t\t\tvar hash = window.location && window.location.hash;\n\t\t\treturn hash && hash.slice( 1 ) === elem.id;\n\t\t},\n\n\t\t\"root\": function( elem ) {\n\t\t\treturn elem === docElem;\n\t\t},\n\n\t\t\"focus\": function( elem ) {\n\t\t\treturn elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);\n\t\t},\n\n\t\t// Boolean properties\n\t\t\"enabled\": createDisabledPseudo( false ),\n\t\t\"disabled\": createDisabledPseudo( true ),\n\n\t\t\"checked\": function( elem ) {\n\t\t\t// In CSS3, :checked should return both checked and selected elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\tvar nodeName = elem.nodeName.toLowerCase();\n\t\t\treturn (nodeName === \"input\" && !!elem.checked) || (nodeName === \"option\" && !!elem.selected);\n\t\t},\n\n\t\t\"selected\": function( elem ) {\n\t\t\t// Accessing this property makes selected-by-default\n\t\t\t// options in Safari work properly\n\t\t\tif ( elem.parentNode ) {\n\t\t\t\telem.parentNode.selectedIndex;\n\t\t\t}\n\n\t\t\treturn elem.selected === true;\n\t\t},\n\n\t\t// Contents\n\t\t\"empty\": function( elem ) {\n\t\t\t// http://www.w3.org/TR/selectors/#empty-pseudo\n\t\t\t// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),\n\t\t\t//   but not by others (comment: 8; processing instruction: 7; etc.)\n\t\t\t// nodeType < 6 works because attributes (2) do not appear as children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tif ( elem.nodeType < 6 ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\t\"parent\": function( elem ) {\n\t\t\treturn !Expr.pseudos[\"empty\"]( elem );\n\t\t},\n\n\t\t// Element/input types\n\t\t\"header\": function( elem ) {\n\t\t\treturn rheader.test( elem.nodeName );\n\t\t},\n\n\t\t\"input\": function( elem ) {\n\t\t\treturn rinputs.test( elem.nodeName );\n\t\t},\n\n\t\t\"button\": function( elem ) {\n\t\t\tvar name = elem.nodeName.toLowerCase();\n\t\t\treturn name === \"input\" && elem.type === \"button\" || name === \"button\";\n\t\t},\n\n\t\t\"text\": function( elem ) {\n\t\t\tvar attr;\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" &&\n\t\t\t\telem.type === \"text\" &&\n\n\t\t\t\t// Support: IE<8\n\t\t\t\t// New HTML5 attribute values (e.g., \"search\") appear with elem.type === \"text\"\n\t\t\t\t( (attr = elem.getAttribute(\"type\")) == null || attr.toLowerCase() === \"text\" );\n\t\t},\n\n\t\t// Position-in-collection\n\t\t\"first\": createPositionalPseudo(function() {\n\t\t\treturn [ 0 ];\n\t\t}),\n\n\t\t\"last\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\treturn [ length - 1 ];\n\t\t}),\n\n\t\t\"eq\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\treturn [ argument < 0 ? argument + length : argument ];\n\t\t}),\n\n\t\t\"even\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"odd\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\tvar i = 1;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"lt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; --i >= 0; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"gt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; ++i < length; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t})\n\t}\n};\n\nExpr.pseudos[\"nth\"] = Expr.pseudos[\"eq\"];\n\n// Add button/input type pseudos\nfor ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {\n\tExpr.pseudos[ i ] = createInputPseudo( i );\n}\nfor ( i in { submit: true, reset: true } ) {\n\tExpr.pseudos[ i ] = createButtonPseudo( i );\n}\n\n// Easy API for creating new setFilters\nfunction setFilters() {}\nsetFilters.prototype = Expr.filters = Expr.pseudos;\nExpr.setFilters = new setFilters();\n\ntokenize = Sizzle.tokenize = function( selector, parseOnly ) {\n\tvar matched, match, tokens, type,\n\t\tsoFar, groups, preFilters,\n\t\tcached = tokenCache[ selector + \" \" ];\n\n\tif ( cached ) {\n\t\treturn parseOnly ? 0 : cached.slice( 0 );\n\t}\n\n\tsoFar = selector;\n\tgroups = [];\n\tpreFilters = Expr.preFilter;\n\n\twhile ( soFar ) {\n\n\t\t// Comma and first run\n\t\tif ( !matched || (match = rcomma.exec( soFar )) ) {\n\t\t\tif ( match ) {\n\t\t\t\t// Don't consume trailing commas as valid\n\t\t\t\tsoFar = soFar.slice( match[0].length ) || soFar;\n\t\t\t}\n\t\t\tgroups.push( (tokens = []) );\n\t\t}\n\n\t\tmatched = false;\n\n\t\t// Combinators\n\t\tif ( (match = rcombinators.exec( soFar )) ) {\n\t\t\tmatched = match.shift();\n\t\t\ttokens.push({\n\t\t\t\tvalue: matched,\n\t\t\t\t// Cast descendant combinators to space\n\t\t\t\ttype: match[0].replace( rtrim, \" \" )\n\t\t\t});\n\t\t\tsoFar = soFar.slice( matched.length );\n\t\t}\n\n\t\t// Filters\n\t\tfor ( type in Expr.filter ) {\n\t\t\tif ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||\n\t\t\t\t(match = preFilters[ type ]( match ))) ) {\n\t\t\t\tmatched = match.shift();\n\t\t\t\ttokens.push({\n\t\t\t\t\tvalue: matched,\n\t\t\t\t\ttype: type,\n\t\t\t\t\tmatches: match\n\t\t\t\t});\n\t\t\t\tsoFar = soFar.slice( matched.length );\n\t\t\t}\n\t\t}\n\n\t\tif ( !matched ) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Return the length of the invalid excess\n\t// if we're just parsing\n\t// Otherwise, throw an error or return tokens\n\treturn parseOnly ?\n\t\tsoFar.length :\n\t\tsoFar ?\n\t\t\tSizzle.error( selector ) :\n\t\t\t// Cache the tokens\n\t\t\ttokenCache( selector, groups ).slice( 0 );\n};\n\nfunction toSelector( tokens ) {\n\tvar i = 0,\n\t\tlen = tokens.length,\n\t\tselector = \"\";\n\tfor ( ; i < len; i++ ) {\n\t\tselector += tokens[i].value;\n\t}\n\treturn selector;\n}\n\nfunction addCombinator( matcher, combinator, base ) {\n\tvar dir = combinator.dir,\n\t\tskip = combinator.next,\n\t\tkey = skip || dir,\n\t\tcheckNonElements = base && key === \"parentNode\",\n\t\tdoneName = done++;\n\n\treturn combinator.first ?\n\t\t// Check against closest ancestor/preceding element\n\t\tfunction( elem, context, xml ) {\n\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\treturn matcher( elem, context, xml );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t} :\n\n\t\t// Check against all ancestor/preceding elements\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar oldCache, uniqueCache, outerCache,\n\t\t\t\tnewCache = [ dirruns, doneName ];\n\n\t\t\t// We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching\n\t\t\tif ( xml ) {\n\t\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\tif ( matcher( elem, context, xml ) ) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\touterCache = elem[ expando ] || (elem[ expando ] = {});\n\n\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\tuniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});\n\n\t\t\t\t\t\tif ( skip && skip === elem.nodeName.toLowerCase() ) {\n\t\t\t\t\t\t\telem = elem[ dir ] || elem;\n\t\t\t\t\t\t} else if ( (oldCache = uniqueCache[ key ]) &&\n\t\t\t\t\t\t\toldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {\n\n\t\t\t\t\t\t\t// Assign to newCache so results back-propagate to previous elements\n\t\t\t\t\t\t\treturn (newCache[ 2 ] = oldCache[ 2 ]);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Reuse newcache so results back-propagate to previous elements\n\t\t\t\t\t\t\tuniqueCache[ key ] = newCache;\n\n\t\t\t\t\t\t\t// A match means we're done; a fail means we have to keep checking\n\t\t\t\t\t\t\tif ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n}\n\nfunction elementMatcher( matchers ) {\n\treturn matchers.length > 1 ?\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar i = matchers.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( !matchers[i]( elem, context, xml ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t} :\n\t\tmatchers[0];\n}\n\nfunction multipleContexts( selector, contexts, results ) {\n\tvar i = 0,\n\t\tlen = contexts.length;\n\tfor ( ; i < len; i++ ) {\n\t\tSizzle( selector, contexts[i], results );\n\t}\n\treturn results;\n}\n\nfunction condense( unmatched, map, filter, context, xml ) {\n\tvar elem,\n\t\tnewUnmatched = [],\n\t\ti = 0,\n\t\tlen = unmatched.length,\n\t\tmapped = map != null;\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( (elem = unmatched[i]) ) {\n\t\t\tif ( !filter || filter( elem, context, xml ) ) {\n\t\t\t\tnewUnmatched.push( elem );\n\t\t\t\tif ( mapped ) {\n\t\t\t\t\tmap.push( i );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn newUnmatched;\n}\n\nfunction setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {\n\tif ( postFilter && !postFilter[ expando ] ) {\n\t\tpostFilter = setMatcher( postFilter );\n\t}\n\tif ( postFinder && !postFinder[ expando ] ) {\n\t\tpostFinder = setMatcher( postFinder, postSelector );\n\t}\n\treturn markFunction(function( seed, results, context, xml ) {\n\t\tvar temp, i, elem,\n\t\t\tpreMap = [],\n\t\t\tpostMap = [],\n\t\t\tpreexisting = results.length,\n\n\t\t\t// Get initial elements from seed or context\n\t\t\telems = seed || multipleContexts( selector || \"*\", context.nodeType ? [ context ] : context, [] ),\n\n\t\t\t// Prefilter to get matcher input, preserving a map for seed-results synchronization\n\t\t\tmatcherIn = preFilter && ( seed || !selector ) ?\n\t\t\t\tcondense( elems, preMap, preFilter, context, xml ) :\n\t\t\t\telems,\n\n\t\t\tmatcherOut = matcher ?\n\t\t\t\t// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,\n\t\t\t\tpostFinder || ( seed ? preFilter : preexisting || postFilter ) ?\n\n\t\t\t\t\t// ...intermediate processing is necessary\n\t\t\t\t\t[] :\n\n\t\t\t\t\t// ...otherwise use results directly\n\t\t\t\t\tresults :\n\t\t\t\tmatcherIn;\n\n\t\t// Find primary matches\n\t\tif ( matcher ) {\n\t\t\tmatcher( matcherIn, matcherOut, context, xml );\n\t\t}\n\n\t\t// Apply postFilter\n\t\tif ( postFilter ) {\n\t\t\ttemp = condense( matcherOut, postMap );\n\t\t\tpostFilter( temp, [], context, xml );\n\n\t\t\t// Un-match failing elements by moving them back to matcherIn\n\t\t\ti = temp.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( (elem = temp[i]) ) {\n\t\t\t\t\tmatcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( seed ) {\n\t\t\tif ( postFinder || preFilter ) {\n\t\t\t\tif ( postFinder ) {\n\t\t\t\t\t// Get the final matcherOut by condensing this intermediate into postFinder contexts\n\t\t\t\t\ttemp = [];\n\t\t\t\t\ti = matcherOut.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( (elem = matcherOut[i]) ) {\n\t\t\t\t\t\t\t// Restore matcherIn since elem is not yet a final match\n\t\t\t\t\t\t\ttemp.push( (matcherIn[i] = elem) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpostFinder( null, (matcherOut = []), temp, xml );\n\t\t\t\t}\n\n\t\t\t\t// Move matched elements from seed to results to keep them synchronized\n\t\t\t\ti = matcherOut.length;\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\tif ( (elem = matcherOut[i]) &&\n\t\t\t\t\t\t(temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {\n\n\t\t\t\t\t\tseed[temp] = !(results[temp] = elem);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Add elements to results, through postFinder if defined\n\t\t} else {\n\t\t\tmatcherOut = condense(\n\t\t\t\tmatcherOut === results ?\n\t\t\t\t\tmatcherOut.splice( preexisting, matcherOut.length ) :\n\t\t\t\t\tmatcherOut\n\t\t\t);\n\t\t\tif ( postFinder ) {\n\t\t\t\tpostFinder( null, results, matcherOut, xml );\n\t\t\t} else {\n\t\t\t\tpush.apply( results, matcherOut );\n\t\t\t}\n\t\t}\n\t});\n}\n\nfunction matcherFromTokens( tokens ) {\n\tvar checkContext, matcher, j,\n\t\tlen = tokens.length,\n\t\tleadingRelative = Expr.relative[ tokens[0].type ],\n\t\timplicitRelative = leadingRelative || Expr.relative[\" \"],\n\t\ti = leadingRelative ? 1 : 0,\n\n\t\t// The foundational matcher ensures that elements are reachable from top-level context(s)\n\t\tmatchContext = addCombinator( function( elem ) {\n\t\t\treturn elem === checkContext;\n\t\t}, implicitRelative, true ),\n\t\tmatchAnyContext = addCombinator( function( elem ) {\n\t\t\treturn indexOf( checkContext, elem ) > -1;\n\t\t}, implicitRelative, true ),\n\t\tmatchers = [ function( elem, context, xml ) {\n\t\t\tvar ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (\n\t\t\t\t(checkContext = context).nodeType ?\n\t\t\t\t\tmatchContext( elem, context, xml ) :\n\t\t\t\t\tmatchAnyContext( elem, context, xml ) );\n\t\t\t// Avoid hanging onto element (issue #299)\n\t\t\tcheckContext = null;\n\t\t\treturn ret;\n\t\t} ];\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( (matcher = Expr.relative[ tokens[i].type ]) ) {\n\t\t\tmatchers = [ addCombinator(elementMatcher( matchers ), matcher) ];\n\t\t} else {\n\t\t\tmatcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );\n\n\t\t\t// Return special upon seeing a positional matcher\n\t\t\tif ( matcher[ expando ] ) {\n\t\t\t\t// Find the next relative operator (if any) for proper handling\n\t\t\t\tj = ++i;\n\t\t\t\tfor ( ; j < len; j++ ) {\n\t\t\t\t\tif ( Expr.relative[ tokens[j].type ] ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn setMatcher(\n\t\t\t\t\ti > 1 && elementMatcher( matchers ),\n\t\t\t\t\ti > 1 && toSelector(\n\t\t\t\t\t\t// If the preceding token was a descendant combinator, insert an implicit any-element `*`\n\t\t\t\t\t\ttokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === \" \" ? \"*\" : \"\" })\n\t\t\t\t\t).replace( rtrim, \"$1\" ),\n\t\t\t\t\tmatcher,\n\t\t\t\t\ti < j && matcherFromTokens( tokens.slice( i, j ) ),\n\t\t\t\t\tj < len && matcherFromTokens( (tokens = tokens.slice( j )) ),\n\t\t\t\t\tj < len && toSelector( tokens )\n\t\t\t\t);\n\t\t\t}\n\t\t\tmatchers.push( matcher );\n\t\t}\n\t}\n\n\treturn elementMatcher( matchers );\n}\n\nfunction matcherFromGroupMatchers( elementMatchers, setMatchers ) {\n\tvar bySet = setMatchers.length > 0,\n\t\tbyElement = elementMatchers.length > 0,\n\t\tsuperMatcher = function( seed, context, xml, results, outermost ) {\n\t\t\tvar elem, j, matcher,\n\t\t\t\tmatchedCount = 0,\n\t\t\t\ti = \"0\",\n\t\t\t\tunmatched = seed && [],\n\t\t\t\tsetMatched = [],\n\t\t\t\tcontextBackup = outermostContext,\n\t\t\t\t// We must always have either seed elements or outermost context\n\t\t\t\telems = seed || byElement && Expr.find[\"TAG\"]( \"*\", outermost ),\n\t\t\t\t// Use integer dirruns iff this is the outermost matcher\n\t\t\t\tdirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),\n\t\t\t\tlen = elems.length;\n\n\t\t\tif ( outermost ) {\n\t\t\t\toutermostContext = context === document || context || outermost;\n\t\t\t}\n\n\t\t\t// Add elements passing elementMatchers directly to results\n\t\t\t// Support: IE<9, Safari\n\t\t\t// Tolerate NodeList properties (IE: \"length\"; Safari: <number>) matching elements by id\n\t\t\tfor ( ; i !== len && (elem = elems[i]) != null; i++ ) {\n\t\t\t\tif ( byElement && elem ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\tif ( !context && elem.ownerDocument !== document ) {\n\t\t\t\t\t\tsetDocument( elem );\n\t\t\t\t\t\txml = !documentIsHTML;\n\t\t\t\t\t}\n\t\t\t\t\twhile ( (matcher = elementMatchers[j++]) ) {\n\t\t\t\t\t\tif ( matcher( elem, context || document, xml) ) {\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( outermost ) {\n\t\t\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Track unmatched elements for set filters\n\t\t\t\tif ( bySet ) {\n\t\t\t\t\t// They will have gone through all possible matchers\n\t\t\t\t\tif ( (elem = !matcher && elem) ) {\n\t\t\t\t\t\tmatchedCount--;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Lengthen the array for every element, matched or not\n\t\t\t\t\tif ( seed ) {\n\t\t\t\t\t\tunmatched.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// `i` is now the count of elements visited above, and adding it to `matchedCount`\n\t\t\t// makes the latter nonnegative.\n\t\t\tmatchedCount += i;\n\n\t\t\t// Apply set filters to unmatched elements\n\t\t\t// NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`\n\t\t\t// equals `i`), unless we didn't visit _any_ elements in the above loop because we have\n\t\t\t// no element matchers and no seed.\n\t\t\t// Incrementing an initially-string \"0\" `i` allows `i` to remain a string only in that\n\t\t\t// case, which will result in a \"00\" `matchedCount` that differs from `i` but is also\n\t\t\t// numerically zero.\n\t\t\tif ( bySet && i !== matchedCount ) {\n\t\t\t\tj = 0;\n\t\t\t\twhile ( (matcher = setMatchers[j++]) ) {\n\t\t\t\t\tmatcher( unmatched, setMatched, context, xml );\n\t\t\t\t}\n\n\t\t\t\tif ( seed ) {\n\t\t\t\t\t// Reintegrate element matches to eliminate the need for sorting\n\t\t\t\t\tif ( matchedCount > 0 ) {\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tif ( !(unmatched[i] || setMatched[i]) ) {\n\t\t\t\t\t\t\t\tsetMatched[i] = pop.call( results );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Discard index placeholder values to get only actual matches\n\t\t\t\t\tsetMatched = condense( setMatched );\n\t\t\t\t}\n\n\t\t\t\t// Add matches to results\n\t\t\t\tpush.apply( results, setMatched );\n\n\t\t\t\t// Seedless set matches succeeding multiple successful matchers stipulate sorting\n\t\t\t\tif ( outermost && !seed && setMatched.length > 0 &&\n\t\t\t\t\t( matchedCount + setMatchers.length ) > 1 ) {\n\n\t\t\t\t\tSizzle.uniqueSort( results );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Override manipulation of globals by nested matchers\n\t\t\tif ( outermost ) {\n\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\toutermostContext = contextBackup;\n\t\t\t}\n\n\t\t\treturn unmatched;\n\t\t};\n\n\treturn bySet ?\n\t\tmarkFunction( superMatcher ) :\n\t\tsuperMatcher;\n}\n\ncompile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {\n\tvar i,\n\t\tsetMatchers = [],\n\t\telementMatchers = [],\n\t\tcached = compilerCache[ selector + \" \" ];\n\n\tif ( !cached ) {\n\t\t// Generate a function of recursive functions that can be used to check each element\n\t\tif ( !match ) {\n\t\t\tmatch = tokenize( selector );\n\t\t}\n\t\ti = match.length;\n\t\twhile ( i-- ) {\n\t\t\tcached = matcherFromTokens( match[i] );\n\t\t\tif ( cached[ expando ] ) {\n\t\t\t\tsetMatchers.push( cached );\n\t\t\t} else {\n\t\t\t\telementMatchers.push( cached );\n\t\t\t}\n\t\t}\n\n\t\t// Cache the compiled function\n\t\tcached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );\n\n\t\t// Save selector and tokenization\n\t\tcached.selector = selector;\n\t}\n\treturn cached;\n};\n\n/**\n * A low-level selection function that works with Sizzle's compiled\n *  selector functions\n * @param {String|Function} selector A selector or a pre-compiled\n *  selector function built with Sizzle.compile\n * @param {Element} context\n * @param {Array} [results]\n * @param {Array} [seed] A set of elements to match against\n */\nselect = Sizzle.select = function( selector, context, results, seed ) {\n\tvar i, tokens, token, type, find,\n\t\tcompiled = typeof selector === \"function\" && selector,\n\t\tmatch = !seed && tokenize( (selector = compiled.selector || selector) );\n\n\tresults = results || [];\n\n\t// Try to minimize operations if there is only one selector in the list and no seed\n\t// (the latter of which guarantees us context)\n\tif ( match.length === 1 ) {\n\n\t\t// Reduce context if the leading compound selector is an ID\n\t\ttokens = match[0] = match[0].slice( 0 );\n\t\tif ( tokens.length > 2 && (token = tokens[0]).type === \"ID\" &&\n\t\t\t\tcontext.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) {\n\n\t\t\tcontext = ( Expr.find[\"ID\"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];\n\t\t\tif ( !context ) {\n\t\t\t\treturn results;\n\n\t\t\t// Precompiled matchers will still verify ancestry, so step up a level\n\t\t\t} else if ( compiled ) {\n\t\t\t\tcontext = context.parentNode;\n\t\t\t}\n\n\t\t\tselector = selector.slice( tokens.shift().value.length );\n\t\t}\n\n\t\t// Fetch a seed set for right-to-left matching\n\t\ti = matchExpr[\"needsContext\"].test( selector ) ? 0 : tokens.length;\n\t\twhile ( i-- ) {\n\t\t\ttoken = tokens[i];\n\n\t\t\t// Abort if we hit a combinator\n\t\t\tif ( Expr.relative[ (type = token.type) ] ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( (find = Expr.find[ type ]) ) {\n\t\t\t\t// Search, expanding context for leading sibling combinators\n\t\t\t\tif ( (seed = find(\n\t\t\t\t\ttoken.matches[0].replace( runescape, funescape ),\n\t\t\t\t\trsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context\n\t\t\t\t)) ) {\n\n\t\t\t\t\t// If seed is empty or no tokens remain, we can return early\n\t\t\t\t\ttokens.splice( i, 1 );\n\t\t\t\t\tselector = seed.length && toSelector( tokens );\n\t\t\t\t\tif ( !selector ) {\n\t\t\t\t\t\tpush.apply( results, seed );\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Compile and execute a filtering function if one is not provided\n\t// Provide `match` to avoid retokenization if we modified the selector above\n\t( compiled || compile( selector, match ) )(\n\t\tseed,\n\t\tcontext,\n\t\t!documentIsHTML,\n\t\tresults,\n\t\t!context || rsibling.test( selector ) && testContext( context.parentNode ) || context\n\t);\n\treturn results;\n};\n\n// One-time assignments\n\n// Sort stability\nsupport.sortStable = expando.split(\"\").sort( sortOrder ).join(\"\") === expando;\n\n// Support: Chrome 14-35+\n// Always assume duplicates if they aren't passed to the comparison function\nsupport.detectDuplicates = !!hasDuplicate;\n\n// Initialize against the default document\nsetDocument();\n\n// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)\n// Detached nodes confoundingly follow *each other*\nsupport.sortDetached = assert(function( el ) {\n\t// Should return 1, but returns 4 (following)\n\treturn el.compareDocumentPosition( document.createElement(\"fieldset\") ) & 1;\n});\n\n// Support: IE<8\n// Prevent attribute/property \"interpolation\"\n// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx\nif ( !assert(function( el ) {\n\tel.innerHTML = \"<a href='#'></a>\";\n\treturn el.firstChild.getAttribute(\"href\") === \"#\" ;\n}) ) {\n\taddHandle( \"type|href|height|width\", function( elem, name, isXML ) {\n\t\tif ( !isXML ) {\n\t\t\treturn elem.getAttribute( name, name.toLowerCase() === \"type\" ? 1 : 2 );\n\t\t}\n\t});\n}\n\n// Support: IE<9\n// Use defaultValue in place of getAttribute(\"value\")\nif ( !support.attributes || !assert(function( el ) {\n\tel.innerHTML = \"<input/>\";\n\tel.firstChild.setAttribute( \"value\", \"\" );\n\treturn el.firstChild.getAttribute( \"value\" ) === \"\";\n}) ) {\n\taddHandle( \"value\", function( elem, name, isXML ) {\n\t\tif ( !isXML && elem.nodeName.toLowerCase() === \"input\" ) {\n\t\t\treturn elem.defaultValue;\n\t\t}\n\t});\n}\n\n// Support: IE<9\n// Use getAttributeNode to fetch booleans when getAttribute lies\nif ( !assert(function( el ) {\n\treturn el.getAttribute(\"disabled\") == null;\n}) ) {\n\taddHandle( booleans, function( elem, name, isXML ) {\n\t\tvar val;\n\t\tif ( !isXML ) {\n\t\t\treturn elem[ name ] === true ? name.toLowerCase() :\n\t\t\t\t\t(val = elem.getAttributeNode( name )) && val.specified ?\n\t\t\t\t\tval.value :\n\t\t\t\tnull;\n\t\t}\n\t});\n}\n\nreturn Sizzle;\n\n})( window );\n\n\n\njQuery.find = Sizzle;\njQuery.expr = Sizzle.selectors;\n\n// Deprecated\njQuery.expr[ \":\" ] = jQuery.expr.pseudos;\njQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;\njQuery.text = Sizzle.getText;\njQuery.isXMLDoc = Sizzle.isXML;\njQuery.contains = Sizzle.contains;\njQuery.escapeSelector = Sizzle.escape;\n\n\n\n\nvar dir = function( elem, dir, until ) {\n\tvar matched = [],\n\t\ttruncate = until !== undefined;\n\n\twhile ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {\n\t\tif ( elem.nodeType === 1 ) {\n\t\t\tif ( truncate && jQuery( elem ).is( until ) ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmatched.push( elem );\n\t\t}\n\t}\n\treturn matched;\n};\n\n\nvar siblings = function( n, elem ) {\n\tvar matched = [];\n\n\tfor ( ; n; n = n.nextSibling ) {\n\t\tif ( n.nodeType === 1 && n !== elem ) {\n\t\t\tmatched.push( n );\n\t\t}\n\t}\n\n\treturn matched;\n};\n\n\nvar rneedsContext = jQuery.expr.match.needsContext;\n\n\n\nfunction nodeName( elem, name ) {\n\n  return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();\n\n};\nvar rsingleTag = ( /^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i );\n\n\n\nvar risSimple = /^.[^:#\\[\\.,]*$/;\n\n// Implement the identical functionality for filter and not\nfunction winnow( elements, qualifier, not ) {\n\tif ( jQuery.isFunction( qualifier ) ) {\n\t\treturn jQuery.grep( elements, function( elem, i ) {\n\t\t\treturn !!qualifier.call( elem, i, elem ) !== not;\n\t\t} );\n\t}\n\n\t// Single element\n\tif ( qualifier.nodeType ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( elem === qualifier ) !== not;\n\t\t} );\n\t}\n\n\t// Arraylike of elements (jQuery, arguments, Array)\n\tif ( typeof qualifier !== \"string\" ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( indexOf.call( qualifier, elem ) > -1 ) !== not;\n\t\t} );\n\t}\n\n\t// Simple selector that can be filtered directly, removing non-Elements\n\tif ( risSimple.test( qualifier ) ) {\n\t\treturn jQuery.filter( qualifier, elements, not );\n\t}\n\n\t// Complex selector, compare the two sets, removing non-Elements\n\tqualifier = jQuery.filter( qualifier, elements );\n\treturn jQuery.grep( elements, function( elem ) {\n\t\treturn ( indexOf.call( qualifier, elem ) > -1 ) !== not && elem.nodeType === 1;\n\t} );\n}\n\njQuery.filter = function( expr, elems, not ) {\n\tvar elem = elems[ 0 ];\n\n\tif ( not ) {\n\t\texpr = \":not(\" + expr + \")\";\n\t}\n\n\tif ( elems.length === 1 && elem.nodeType === 1 ) {\n\t\treturn jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];\n\t}\n\n\treturn jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {\n\t\treturn elem.nodeType === 1;\n\t} ) );\n};\n\njQuery.fn.extend( {\n\tfind: function( selector ) {\n\t\tvar i, ret,\n\t\t\tlen = this.length,\n\t\t\tself = this;\n\n\t\tif ( typeof selector !== \"string\" ) {\n\t\t\treturn this.pushStack( jQuery( selector ).filter( function() {\n\t\t\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\t\t\tif ( jQuery.contains( self[ i ], this ) ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} ) );\n\t\t}\n\n\t\tret = this.pushStack( [] );\n\n\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\tjQuery.find( selector, self[ i ], ret );\n\t\t}\n\n\t\treturn len > 1 ? jQuery.uniqueSort( ret ) : ret;\n\t},\n\tfilter: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], false ) );\n\t},\n\tnot: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], true ) );\n\t},\n\tis: function( selector ) {\n\t\treturn !!winnow(\n\t\t\tthis,\n\n\t\t\t// If this is a positional/relative selector, check membership in the returned set\n\t\t\t// so $(\"p:first\").is(\"p:last\") won't return true for a doc with two \"p\".\n\t\t\ttypeof selector === \"string\" && rneedsContext.test( selector ) ?\n\t\t\t\tjQuery( selector ) :\n\t\t\t\tselector || [],\n\t\t\tfalse\n\t\t).length;\n\t}\n} );\n\n\n// Initialize a jQuery object\n\n\n// A central reference to the root jQuery(document)\nvar rootjQuery,\n\n\t// A simple way to check for HTML strings\n\t// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)\n\t// Strict HTML recognition (#11290: must start with <)\n\t// Shortcut simple #id case for speed\n\trquickExpr = /^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]+))$/,\n\n\tinit = jQuery.fn.init = function( selector, context, root ) {\n\t\tvar match, elem;\n\n\t\t// HANDLE: $(\"\"), $(null), $(undefined), $(false)\n\t\tif ( !selector ) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// Method init() accepts an alternate rootjQuery\n\t\t// so migrate can support jQuery.sub (gh-2101)\n\t\troot = root || rootjQuery;\n\n\t\t// Handle HTML strings\n\t\tif ( typeof selector === \"string\" ) {\n\t\t\tif ( selector[ 0 ] === \"<\" &&\n\t\t\t\tselector[ selector.length - 1 ] === \">\" &&\n\t\t\t\tselector.length >= 3 ) {\n\n\t\t\t\t// Assume that strings that start and end with <> are HTML and skip the regex check\n\t\t\t\tmatch = [ null, selector, null ];\n\n\t\t\t} else {\n\t\t\t\tmatch = rquickExpr.exec( selector );\n\t\t\t}\n\n\t\t\t// Match html or make sure no context is specified for #id\n\t\t\tif ( match && ( match[ 1 ] || !context ) ) {\n\n\t\t\t\t// HANDLE: $(html) -> $(array)\n\t\t\t\tif ( match[ 1 ] ) {\n\t\t\t\t\tcontext = context instanceof jQuery ? context[ 0 ] : context;\n\n\t\t\t\t\t// Option to run scripts is true for back-compat\n\t\t\t\t\t// Intentionally let the error be thrown if parseHTML is not present\n\t\t\t\t\tjQuery.merge( this, jQuery.parseHTML(\n\t\t\t\t\t\tmatch[ 1 ],\n\t\t\t\t\t\tcontext && context.nodeType ? context.ownerDocument || context : document,\n\t\t\t\t\t\ttrue\n\t\t\t\t\t) );\n\n\t\t\t\t\t// HANDLE: $(html, props)\n\t\t\t\t\tif ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {\n\t\t\t\t\t\tfor ( match in context ) {\n\n\t\t\t\t\t\t\t// Properties of context are called as methods if possible\n\t\t\t\t\t\t\tif ( jQuery.isFunction( this[ match ] ) ) {\n\t\t\t\t\t\t\t\tthis[ match ]( context[ match ] );\n\n\t\t\t\t\t\t\t// ...and otherwise set as attributes\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tthis.attr( match, context[ match ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn this;\n\n\t\t\t\t// HANDLE: $(#id)\n\t\t\t\t} else {\n\t\t\t\t\telem = document.getElementById( match[ 2 ] );\n\n\t\t\t\t\tif ( elem ) {\n\n\t\t\t\t\t\t// Inject the element directly into the jQuery object\n\t\t\t\t\t\tthis[ 0 ] = elem;\n\t\t\t\t\t\tthis.length = 1;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\n\t\t\t// HANDLE: $(expr, $(...))\n\t\t\t} else if ( !context || context.jquery ) {\n\t\t\t\treturn ( context || root ).find( selector );\n\n\t\t\t// HANDLE: $(expr, context)\n\t\t\t// (which is just equivalent to: $(context).find(expr)\n\t\t\t} else {\n\t\t\t\treturn this.constructor( context ).find( selector );\n\t\t\t}\n\n\t\t// HANDLE: $(DOMElement)\n\t\t} else if ( selector.nodeType ) {\n\t\t\tthis[ 0 ] = selector;\n\t\t\tthis.length = 1;\n\t\t\treturn this;\n\n\t\t// HANDLE: $(function)\n\t\t// Shortcut for document ready\n\t\t} else if ( jQuery.isFunction( selector ) ) {\n\t\t\treturn root.ready !== undefined ?\n\t\t\t\troot.ready( selector ) :\n\n\t\t\t\t// Execute immediately if ready is not present\n\t\t\t\tselector( jQuery );\n\t\t}\n\n\t\treturn jQuery.makeArray( selector, this );\n\t};\n\n// Give the init function the jQuery prototype for later instantiation\ninit.prototype = jQuery.fn;\n\n// Initialize central reference\nrootjQuery = jQuery( document );\n\n\nvar rparentsprev = /^(?:parents|prev(?:Until|All))/,\n\n\t// Methods guaranteed to produce a unique set when starting from a unique set\n\tguaranteedUnique = {\n\t\tchildren: true,\n\t\tcontents: true,\n\t\tnext: true,\n\t\tprev: true\n\t};\n\njQuery.fn.extend( {\n\thas: function( target ) {\n\t\tvar targets = jQuery( target, this ),\n\t\t\tl = targets.length;\n\n\t\treturn this.filter( function() {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tif ( jQuery.contains( this, targets[ i ] ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t},\n\n\tclosest: function( selectors, context ) {\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\tl = this.length,\n\t\t\tmatched = [],\n\t\t\ttargets = typeof selectors !== \"string\" && jQuery( selectors );\n\n\t\t// Positional selectors never match, since there's no _selection_ context\n\t\tif ( !rneedsContext.test( selectors ) ) {\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tfor ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {\n\n\t\t\t\t\t// Always skip document fragments\n\t\t\t\t\tif ( cur.nodeType < 11 && ( targets ?\n\t\t\t\t\t\ttargets.index( cur ) > -1 :\n\n\t\t\t\t\t\t// Don't pass non-elements to Sizzle\n\t\t\t\t\t\tcur.nodeType === 1 &&\n\t\t\t\t\t\t\tjQuery.find.matchesSelector( cur, selectors ) ) ) {\n\n\t\t\t\t\t\tmatched.push( cur );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );\n\t},\n\n\t// Determine the position of an element within the set\n\tindex: function( elem ) {\n\n\t\t// No argument, return index in parent\n\t\tif ( !elem ) {\n\t\t\treturn ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;\n\t\t}\n\n\t\t// Index in selector\n\t\tif ( typeof elem === \"string\" ) {\n\t\t\treturn indexOf.call( jQuery( elem ), this[ 0 ] );\n\t\t}\n\n\t\t// Locate the position of the desired element\n\t\treturn indexOf.call( this,\n\n\t\t\t// If it receives a jQuery object, the first element is used\n\t\t\telem.jquery ? elem[ 0 ] : elem\n\t\t);\n\t},\n\n\tadd: function( selector, context ) {\n\t\treturn this.pushStack(\n\t\t\tjQuery.uniqueSort(\n\t\t\t\tjQuery.merge( this.get(), jQuery( selector, context ) )\n\t\t\t)\n\t\t);\n\t},\n\n\taddBack: function( selector ) {\n\t\treturn this.add( selector == null ?\n\t\t\tthis.prevObject : this.prevObject.filter( selector )\n\t\t);\n\t}\n} );\n\nfunction sibling( cur, dir ) {\n\twhile ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}\n\treturn cur;\n}\n\njQuery.each( {\n\tparent: function( elem ) {\n\t\tvar parent = elem.parentNode;\n\t\treturn parent && parent.nodeType !== 11 ? parent : null;\n\t},\n\tparents: function( elem ) {\n\t\treturn dir( elem, \"parentNode\" );\n\t},\n\tparentsUntil: function( elem, i, until ) {\n\t\treturn dir( elem, \"parentNode\", until );\n\t},\n\tnext: function( elem ) {\n\t\treturn sibling( elem, \"nextSibling\" );\n\t},\n\tprev: function( elem ) {\n\t\treturn sibling( elem, \"previousSibling\" );\n\t},\n\tnextAll: function( elem ) {\n\t\treturn dir( elem, \"nextSibling\" );\n\t},\n\tprevAll: function( elem ) {\n\t\treturn dir( elem, \"previousSibling\" );\n\t},\n\tnextUntil: function( elem, i, until ) {\n\t\treturn dir( elem, \"nextSibling\", until );\n\t},\n\tprevUntil: function( elem, i, until ) {\n\t\treturn dir( elem, \"previousSibling\", until );\n\t},\n\tsiblings: function( elem ) {\n\t\treturn siblings( ( elem.parentNode || {} ).firstChild, elem );\n\t},\n\tchildren: function( elem ) {\n\t\treturn siblings( elem.firstChild );\n\t},\n\tcontents: function( elem ) {\n        if ( nodeName( elem, \"iframe\" ) ) {\n            return elem.contentDocument;\n        }\n\n        // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only\n        // Treat the template element as a regular one in browsers that\n        // don't support it.\n        if ( nodeName( elem, \"template\" ) ) {\n            elem = elem.content || elem;\n        }\n\n        return jQuery.merge( [], elem.childNodes );\n\t}\n}, function( name, fn ) {\n\tjQuery.fn[ name ] = function( until, selector ) {\n\t\tvar matched = jQuery.map( this, fn, until );\n\n\t\tif ( name.slice( -5 ) !== \"Until\" ) {\n\t\t\tselector = until;\n\t\t}\n\n\t\tif ( selector && typeof selector === \"string\" ) {\n\t\t\tmatched = jQuery.filter( selector, matched );\n\t\t}\n\n\t\tif ( this.length > 1 ) {\n\n\t\t\t// Remove duplicates\n\t\t\tif ( !guaranteedUnique[ name ] ) {\n\t\t\t\tjQuery.uniqueSort( matched );\n\t\t\t}\n\n\t\t\t// Reverse order for parents* and prev-derivatives\n\t\t\tif ( rparentsprev.test( name ) ) {\n\t\t\t\tmatched.reverse();\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched );\n\t};\n} );\nvar rnothtmlwhite = ( /[^\\x20\\t\\r\\n\\f]+/g );\n\n\n\n// Convert String-formatted options into Object-formatted ones\nfunction createOptions( options ) {\n\tvar object = {};\n\tjQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {\n\t\tobject[ flag ] = true;\n\t} );\n\treturn object;\n}\n\n/*\n * Create a callback list using the following parameters:\n *\n *\toptions: an optional list of space-separated options that will change how\n *\t\t\tthe callback list behaves or a more traditional option object\n *\n * By default a callback list will act like an event callback list and can be\n * \"fired\" multiple times.\n *\n * Possible options:\n *\n *\tonce:\t\t\twill ensure the callback list can only be fired once (like a Deferred)\n *\n *\tmemory:\t\t\twill keep track of previous values and will call any callback added\n *\t\t\t\t\tafter the list has been fired right away with the latest \"memorized\"\n *\t\t\t\t\tvalues (like a Deferred)\n *\n *\tunique:\t\t\twill ensure a callback can only be added once (no duplicate in the list)\n *\n *\tstopOnFalse:\tinterrupt callings when a callback returns false\n *\n */\njQuery.Callbacks = function( options ) {\n\n\t// Convert options from String-formatted to Object-formatted if needed\n\t// (we check in cache first)\n\toptions = typeof options === \"string\" ?\n\t\tcreateOptions( options ) :\n\t\tjQuery.extend( {}, options );\n\n\tvar // Flag to know if list is currently firing\n\t\tfiring,\n\n\t\t// Last fire value for non-forgettable lists\n\t\tmemory,\n\n\t\t// Flag to know if list was already fired\n\t\tfired,\n\n\t\t// Flag to prevent firing\n\t\tlocked,\n\n\t\t// Actual callback list\n\t\tlist = [],\n\n\t\t// Queue of execution data for repeatable lists\n\t\tqueue = [],\n\n\t\t// Index of currently firing callback (modified by add/remove as needed)\n\t\tfiringIndex = -1,\n\n\t\t// Fire callbacks\n\t\tfire = function() {\n\n\t\t\t// Enforce single-firing\n\t\t\tlocked = locked || options.once;\n\n\t\t\t// Execute callbacks for all pending executions,\n\t\t\t// respecting firingIndex overrides and runtime changes\n\t\t\tfired = firing = true;\n\t\t\tfor ( ; queue.length; firingIndex = -1 ) {\n\t\t\t\tmemory = queue.shift();\n\t\t\t\twhile ( ++firingIndex < list.length ) {\n\n\t\t\t\t\t// Run callback and check for early termination\n\t\t\t\t\tif ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&\n\t\t\t\t\t\toptions.stopOnFalse ) {\n\n\t\t\t\t\t\t// Jump to end and forget the data so .add doesn't re-fire\n\t\t\t\t\t\tfiringIndex = list.length;\n\t\t\t\t\t\tmemory = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Forget the data if we're done with it\n\t\t\tif ( !options.memory ) {\n\t\t\t\tmemory = false;\n\t\t\t}\n\n\t\t\tfiring = false;\n\n\t\t\t// Clean up if we're done firing for good\n\t\t\tif ( locked ) {\n\n\t\t\t\t// Keep an empty list if we have data for future add calls\n\t\t\t\tif ( memory ) {\n\t\t\t\t\tlist = [];\n\n\t\t\t\t// Otherwise, this object is spent\n\t\t\t\t} else {\n\t\t\t\t\tlist = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t// Actual Callbacks object\n\t\tself = {\n\n\t\t\t// Add a callback or a collection of callbacks to the list\n\t\t\tadd: function() {\n\t\t\t\tif ( list ) {\n\n\t\t\t\t\t// If we have memory from a past run, we should fire after adding\n\t\t\t\t\tif ( memory && !firing ) {\n\t\t\t\t\t\tfiringIndex = list.length - 1;\n\t\t\t\t\t\tqueue.push( memory );\n\t\t\t\t\t}\n\n\t\t\t\t\t( function add( args ) {\n\t\t\t\t\t\tjQuery.each( args, function( _, arg ) {\n\t\t\t\t\t\t\tif ( jQuery.isFunction( arg ) ) {\n\t\t\t\t\t\t\t\tif ( !options.unique || !self.has( arg ) ) {\n\t\t\t\t\t\t\t\t\tlist.push( arg );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if ( arg && arg.length && jQuery.type( arg ) !== \"string\" ) {\n\n\t\t\t\t\t\t\t\t// Inspect recursively\n\t\t\t\t\t\t\t\tadd( arg );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} );\n\t\t\t\t\t} )( arguments );\n\n\t\t\t\t\tif ( memory && !firing ) {\n\t\t\t\t\t\tfire();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Remove a callback from the list\n\t\t\tremove: function() {\n\t\t\t\tjQuery.each( arguments, function( _, arg ) {\n\t\t\t\t\tvar index;\n\t\t\t\t\twhile ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {\n\t\t\t\t\t\tlist.splice( index, 1 );\n\n\t\t\t\t\t\t// Handle firing indexes\n\t\t\t\t\t\tif ( index <= firingIndex ) {\n\t\t\t\t\t\t\tfiringIndex--;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Check if a given callback is in the list.\n\t\t\t// If no argument is given, return whether or not list has callbacks attached.\n\t\t\thas: function( fn ) {\n\t\t\t\treturn fn ?\n\t\t\t\t\tjQuery.inArray( fn, list ) > -1 :\n\t\t\t\t\tlist.length > 0;\n\t\t\t},\n\n\t\t\t// Remove all callbacks from the list\n\t\t\tempty: function() {\n\t\t\t\tif ( list ) {\n\t\t\t\t\tlist = [];\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Disable .fire and .add\n\t\t\t// Abort any current/pending executions\n\t\t\t// Clear all callbacks and values\n\t\t\tdisable: function() {\n\t\t\t\tlocked = queue = [];\n\t\t\t\tlist = memory = \"\";\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\tdisabled: function() {\n\t\t\t\treturn !list;\n\t\t\t},\n\n\t\t\t// Disable .fire\n\t\t\t// Also disable .add unless we have memory (since it would have no effect)\n\t\t\t// Abort any pending executions\n\t\t\tlock: function() {\n\t\t\t\tlocked = queue = [];\n\t\t\t\tif ( !memory && !firing ) {\n\t\t\t\t\tlist = memory = \"\";\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\tlocked: function() {\n\t\t\t\treturn !!locked;\n\t\t\t},\n\n\t\t\t// Call all callbacks with the given context and arguments\n\t\t\tfireWith: function( context, args ) {\n\t\t\t\tif ( !locked ) {\n\t\t\t\t\targs = args || [];\n\t\t\t\t\targs = [ context, args.slice ? args.slice() : args ];\n\t\t\t\t\tqueue.push( args );\n\t\t\t\t\tif ( !firing ) {\n\t\t\t\t\t\tfire();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Call all the callbacks with the given arguments\n\t\t\tfire: function() {\n\t\t\t\tself.fireWith( this, arguments );\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// To know if the callbacks have already been called at least once\n\t\t\tfired: function() {\n\t\t\t\treturn !!fired;\n\t\t\t}\n\t\t};\n\n\treturn self;\n};\n\n\nfunction Identity( v ) {\n\treturn v;\n}\nfunction Thrower( ex ) {\n\tthrow ex;\n}\n\nfunction adoptValue( value, resolve, reject, noValue ) {\n\tvar method;\n\n\ttry {\n\n\t\t// Check for promise aspect first to privilege synchronous behavior\n\t\tif ( value && jQuery.isFunction( ( method = value.promise ) ) ) {\n\t\t\tmethod.call( value ).done( resolve ).fail( reject );\n\n\t\t// Other thenables\n\t\t} else if ( value && jQuery.isFunction( ( method = value.then ) ) ) {\n\t\t\tmethod.call( value, resolve, reject );\n\n\t\t// Other non-thenables\n\t\t} else {\n\n\t\t\t// Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:\n\t\t\t// * false: [ value ].slice( 0 ) => resolve( value )\n\t\t\t// * true: [ value ].slice( 1 ) => resolve()\n\t\t\tresolve.apply( undefined, [ value ].slice( noValue ) );\n\t\t}\n\n\t// For Promises/A+, convert exceptions into rejections\n\t// Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in\n\t// Deferred#then to conditionally suppress rejection.\n\t} catch ( value ) {\n\n\t\t// Support: Android 4.0 only\n\t\t// Strict mode functions invoked without .call/.apply get global-object context\n\t\treject.apply( undefined, [ value ] );\n\t}\n}\n\njQuery.extend( {\n\n\tDeferred: function( func ) {\n\t\tvar tuples = [\n\n\t\t\t\t// action, add listener, callbacks,\n\t\t\t\t// ... .then handlers, argument index, [final state]\n\t\t\t\t[ \"notify\", \"progress\", jQuery.Callbacks( \"memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"memory\" ), 2 ],\n\t\t\t\t[ \"resolve\", \"done\", jQuery.Callbacks( \"once memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"once memory\" ), 0, \"resolved\" ],\n\t\t\t\t[ \"reject\", \"fail\", jQuery.Callbacks( \"once memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"once memory\" ), 1, \"rejected\" ]\n\t\t\t],\n\t\t\tstate = \"pending\",\n\t\t\tpromise = {\n\t\t\t\tstate: function() {\n\t\t\t\t\treturn state;\n\t\t\t\t},\n\t\t\t\talways: function() {\n\t\t\t\t\tdeferred.done( arguments ).fail( arguments );\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\t\t\t\t\"catch\": function( fn ) {\n\t\t\t\t\treturn promise.then( null, fn );\n\t\t\t\t},\n\n\t\t\t\t// Keep pipe for back-compat\n\t\t\t\tpipe: function( /* fnDone, fnFail, fnProgress */ ) {\n\t\t\t\t\tvar fns = arguments;\n\n\t\t\t\t\treturn jQuery.Deferred( function( newDefer ) {\n\t\t\t\t\t\tjQuery.each( tuples, function( i, tuple ) {\n\n\t\t\t\t\t\t\t// Map tuples (progress, done, fail) to arguments (done, fail, progress)\n\t\t\t\t\t\t\tvar fn = jQuery.isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];\n\n\t\t\t\t\t\t\t// deferred.progress(function() { bind to newDefer or newDefer.notify })\n\t\t\t\t\t\t\t// deferred.done(function() { bind to newDefer or newDefer.resolve })\n\t\t\t\t\t\t\t// deferred.fail(function() { bind to newDefer or newDefer.reject })\n\t\t\t\t\t\t\tdeferred[ tuple[ 1 ] ]( function() {\n\t\t\t\t\t\t\t\tvar returned = fn && fn.apply( this, arguments );\n\t\t\t\t\t\t\t\tif ( returned && jQuery.isFunction( returned.promise ) ) {\n\t\t\t\t\t\t\t\t\treturned.promise()\n\t\t\t\t\t\t\t\t\t\t.progress( newDefer.notify )\n\t\t\t\t\t\t\t\t\t\t.done( newDefer.resolve )\n\t\t\t\t\t\t\t\t\t\t.fail( newDefer.reject );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tnewDefer[ tuple[ 0 ] + \"With\" ](\n\t\t\t\t\t\t\t\t\t\tthis,\n\t\t\t\t\t\t\t\t\t\tfn ? [ returned ] : arguments\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t} );\n\t\t\t\t\t\tfns = null;\n\t\t\t\t\t} ).promise();\n\t\t\t\t},\n\t\t\t\tthen: function( onFulfilled, onRejected, onProgress ) {\n\t\t\t\t\tvar maxDepth = 0;\n\t\t\t\t\tfunction resolve( depth, deferred, handler, special ) {\n\t\t\t\t\t\treturn function() {\n\t\t\t\t\t\t\tvar that = this,\n\t\t\t\t\t\t\t\targs = arguments,\n\t\t\t\t\t\t\t\tmightThrow = function() {\n\t\t\t\t\t\t\t\t\tvar returned, then;\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.3\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-59\n\t\t\t\t\t\t\t\t\t// Ignore double-resolution attempts\n\t\t\t\t\t\t\t\t\tif ( depth < maxDepth ) {\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\treturned = handler.apply( that, args );\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.1\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-48\n\t\t\t\t\t\t\t\t\tif ( returned === deferred.promise() ) {\n\t\t\t\t\t\t\t\t\t\tthrow new TypeError( \"Thenable self-resolution\" );\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ sections 2.3.3.1, 3.5\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-54\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-75\n\t\t\t\t\t\t\t\t\t// Retrieve `then` only once\n\t\t\t\t\t\t\t\t\tthen = returned &&\n\n\t\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.4\n\t\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-64\n\t\t\t\t\t\t\t\t\t\t// Only check objects and functions for thenability\n\t\t\t\t\t\t\t\t\t\t( typeof returned === \"object\" ||\n\t\t\t\t\t\t\t\t\t\t\ttypeof returned === \"function\" ) &&\n\t\t\t\t\t\t\t\t\t\treturned.then;\n\n\t\t\t\t\t\t\t\t\t// Handle a returned thenable\n\t\t\t\t\t\t\t\t\tif ( jQuery.isFunction( then ) ) {\n\n\t\t\t\t\t\t\t\t\t\t// Special processors (notify) just wait for resolution\n\t\t\t\t\t\t\t\t\t\tif ( special ) {\n\t\t\t\t\t\t\t\t\t\t\tthen.call(\n\t\t\t\t\t\t\t\t\t\t\t\treturned,\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Thrower, special )\n\t\t\t\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\t\t\t// Normal processors (resolve) also hook into progress\n\t\t\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\t\t\t// ...and disregard older resolution values\n\t\t\t\t\t\t\t\t\t\t\tmaxDepth++;\n\n\t\t\t\t\t\t\t\t\t\t\tthen.call(\n\t\t\t\t\t\t\t\t\t\t\t\treturned,\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Thrower, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity,\n\t\t\t\t\t\t\t\t\t\t\t\t\tdeferred.notifyWith )\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// Handle all other returned values\n\t\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\t\t// Only substitute handlers pass on context\n\t\t\t\t\t\t\t\t\t\t// and multiple values (non-spec behavior)\n\t\t\t\t\t\t\t\t\t\tif ( handler !== Identity ) {\n\t\t\t\t\t\t\t\t\t\t\tthat = undefined;\n\t\t\t\t\t\t\t\t\t\t\targs = [ returned ];\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t// Process the value(s)\n\t\t\t\t\t\t\t\t\t\t// Default process is resolve\n\t\t\t\t\t\t\t\t\t\t( special || deferred.resolveWith )( that, args );\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\n\t\t\t\t\t\t\t\t// Only normal processors (resolve) catch and reject exceptions\n\t\t\t\t\t\t\t\tprocess = special ?\n\t\t\t\t\t\t\t\t\tmightThrow :\n\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\tmightThrow();\n\t\t\t\t\t\t\t\t\t\t} catch ( e ) {\n\n\t\t\t\t\t\t\t\t\t\t\tif ( jQuery.Deferred.exceptionHook ) {\n\t\t\t\t\t\t\t\t\t\t\t\tjQuery.Deferred.exceptionHook( e,\n\t\t\t\t\t\t\t\t\t\t\t\t\tprocess.stackTrace );\n\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.4.1\n\t\t\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-61\n\t\t\t\t\t\t\t\t\t\t\t// Ignore post-resolution exceptions\n\t\t\t\t\t\t\t\t\t\t\tif ( depth + 1 >= maxDepth ) {\n\n\t\t\t\t\t\t\t\t\t\t\t\t// Only substitute handlers pass on context\n\t\t\t\t\t\t\t\t\t\t\t\t// and multiple values (non-spec behavior)\n\t\t\t\t\t\t\t\t\t\t\t\tif ( handler !== Thrower ) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tthat = undefined;\n\t\t\t\t\t\t\t\t\t\t\t\t\targs = [ e ];\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\t\tdeferred.rejectWith( that, args );\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.1\n\t\t\t\t\t\t\t// https://promisesaplus.com/#point-57\n\t\t\t\t\t\t\t// Re-resolve promises immediately to dodge false rejection from\n\t\t\t\t\t\t\t// subsequent errors\n\t\t\t\t\t\t\tif ( depth ) {\n\t\t\t\t\t\t\t\tprocess();\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t// Call an optional hook to record the stack, in case of exception\n\t\t\t\t\t\t\t\t// since it's otherwise lost when execution goes async\n\t\t\t\t\t\t\t\tif ( jQuery.Deferred.getStackHook ) {\n\t\t\t\t\t\t\t\t\tprocess.stackTrace = jQuery.Deferred.getStackHook();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\twindow.setTimeout( process );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\treturn jQuery.Deferred( function( newDefer ) {\n\n\t\t\t\t\t\t// progress_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 0 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tjQuery.isFunction( onProgress ) ?\n\t\t\t\t\t\t\t\t\tonProgress :\n\t\t\t\t\t\t\t\t\tIdentity,\n\t\t\t\t\t\t\t\tnewDefer.notifyWith\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// fulfilled_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 1 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tjQuery.isFunction( onFulfilled ) ?\n\t\t\t\t\t\t\t\t\tonFulfilled :\n\t\t\t\t\t\t\t\t\tIdentity\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// rejected_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 2 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tjQuery.isFunction( onRejected ) ?\n\t\t\t\t\t\t\t\t\tonRejected :\n\t\t\t\t\t\t\t\t\tThrower\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t} ).promise();\n\t\t\t\t},\n\n\t\t\t\t// Get a promise for this deferred\n\t\t\t\t// If obj is provided, the promise aspect is added to the object\n\t\t\t\tpromise: function( obj ) {\n\t\t\t\t\treturn obj != null ? jQuery.extend( obj, promise ) : promise;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdeferred = {};\n\n\t\t// Add list-specific methods\n\t\tjQuery.each( tuples, function( i, tuple ) {\n\t\t\tvar list = tuple[ 2 ],\n\t\t\t\tstateString = tuple[ 5 ];\n\n\t\t\t// promise.progress = list.add\n\t\t\t// promise.done = list.add\n\t\t\t// promise.fail = list.add\n\t\t\tpromise[ tuple[ 1 ] ] = list.add;\n\n\t\t\t// Handle state\n\t\t\tif ( stateString ) {\n\t\t\t\tlist.add(\n\t\t\t\t\tfunction() {\n\n\t\t\t\t\t\t// state = \"resolved\" (i.e., fulfilled)\n\t\t\t\t\t\t// state = \"rejected\"\n\t\t\t\t\t\tstate = stateString;\n\t\t\t\t\t},\n\n\t\t\t\t\t// rejected_callbacks.disable\n\t\t\t\t\t// fulfilled_callbacks.disable\n\t\t\t\t\ttuples[ 3 - i ][ 2 ].disable,\n\n\t\t\t\t\t// progress_callbacks.lock\n\t\t\t\t\ttuples[ 0 ][ 2 ].lock\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// progress_handlers.fire\n\t\t\t// fulfilled_handlers.fire\n\t\t\t// rejected_handlers.fire\n\t\t\tlist.add( tuple[ 3 ].fire );\n\n\t\t\t// deferred.notify = function() { deferred.notifyWith(...) }\n\t\t\t// deferred.resolve = function() { deferred.resolveWith(...) }\n\t\t\t// deferred.reject = function() { deferred.rejectWith(...) }\n\t\t\tdeferred[ tuple[ 0 ] ] = function() {\n\t\t\t\tdeferred[ tuple[ 0 ] + \"With\" ]( this === deferred ? undefined : this, arguments );\n\t\t\t\treturn this;\n\t\t\t};\n\n\t\t\t// deferred.notifyWith = list.fireWith\n\t\t\t// deferred.resolveWith = list.fireWith\n\t\t\t// deferred.rejectWith = list.fireWith\n\t\t\tdeferred[ tuple[ 0 ] + \"With\" ] = list.fireWith;\n\t\t} );\n\n\t\t// Make the deferred a promise\n\t\tpromise.promise( deferred );\n\n\t\t// Call given func if any\n\t\tif ( func ) {\n\t\t\tfunc.call( deferred, deferred );\n\t\t}\n\n\t\t// All done!\n\t\treturn deferred;\n\t},\n\n\t// Deferred helper\n\twhen: function( singleValue ) {\n\t\tvar\n\n\t\t\t// count of uncompleted subordinates\n\t\t\tremaining = arguments.length,\n\n\t\t\t// count of unprocessed arguments\n\t\t\ti = remaining,\n\n\t\t\t// subordinate fulfillment data\n\t\t\tresolveContexts = Array( i ),\n\t\t\tresolveValues = slice.call( arguments ),\n\n\t\t\t// the master Deferred\n\t\t\tmaster = jQuery.Deferred(),\n\n\t\t\t// subordinate callback factory\n\t\t\tupdateFunc = function( i ) {\n\t\t\t\treturn function( value ) {\n\t\t\t\t\tresolveContexts[ i ] = this;\n\t\t\t\t\tresolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;\n\t\t\t\t\tif ( !( --remaining ) ) {\n\t\t\t\t\t\tmaster.resolveWith( resolveContexts, resolveValues );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t};\n\n\t\t// Single- and empty arguments are adopted like Promise.resolve\n\t\tif ( remaining <= 1 ) {\n\t\t\tadoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,\n\t\t\t\t!remaining );\n\n\t\t\t// Use .then() to unwrap secondary thenables (cf. gh-3000)\n\t\t\tif ( master.state() === \"pending\" ||\n\t\t\t\tjQuery.isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {\n\n\t\t\t\treturn master.then();\n\t\t\t}\n\t\t}\n\n\t\t// Multiple arguments are aggregated like Promise.all array elements\n\t\twhile ( i-- ) {\n\t\t\tadoptValue( resolveValues[ i ], updateFunc( i ), master.reject );\n\t\t}\n\n\t\treturn master.promise();\n\t}\n} );\n\n\n// These usually indicate a programmer mistake during development,\n// warn about them ASAP rather than swallowing them by default.\nvar rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;\n\njQuery.Deferred.exceptionHook = function( error, stack ) {\n\n\t// Support: IE 8 - 9 only\n\t// Console exists when dev tools are open, which can happen at any time\n\tif ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) {\n\t\twindow.console.warn( \"jQuery.Deferred exception: \" + error.message, error.stack, stack );\n\t}\n};\n\n\n\n\njQuery.readyException = function( error ) {\n\twindow.setTimeout( function() {\n\t\tthrow error;\n\t} );\n};\n\n\n\n\n// The deferred used on DOM ready\nvar readyList = jQuery.Deferred();\n\njQuery.fn.ready = function( fn ) {\n\n\treadyList\n\t\t.then( fn )\n\n\t\t// Wrap jQuery.readyException in a function so that the lookup\n\t\t// happens at the time of error handling instead of callback\n\t\t// registration.\n\t\t.catch( function( error ) {\n\t\t\tjQuery.readyException( error );\n\t\t} );\n\n\treturn this;\n};\n\njQuery.extend( {\n\n\t// Is the DOM ready to be used? Set to true once it occurs.\n\tisReady: false,\n\n\t// A counter to track how many items to wait for before\n\t// the ready event fires. See #6781\n\treadyWait: 1,\n\n\t// Handle when the DOM is ready\n\tready: function( wait ) {\n\n\t\t// Abort if there are pending holds or we're already ready\n\t\tif ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Remember that the DOM is ready\n\t\tjQuery.isReady = true;\n\n\t\t// If a normal DOM Ready event fired, decrement, and wait if need be\n\t\tif ( wait !== true && --jQuery.readyWait > 0 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// If there are functions bound, to execute\n\t\treadyList.resolveWith( document, [ jQuery ] );\n\t}\n} );\n\njQuery.ready.then = readyList.then;\n\n// The ready event handler and self cleanup method\nfunction completed() {\n\tdocument.removeEventListener( \"DOMContentLoaded\", completed );\n\twindow.removeEventListener( \"load\", completed );\n\tjQuery.ready();\n}\n\n// Catch cases where $(document).ready() is called\n// after the browser event has already occurred.\n// Support: IE <=9 - 10 only\n// Older IE sometimes signals \"interactive\" too soon\nif ( document.readyState === \"complete\" ||\n\t( document.readyState !== \"loading\" && !document.documentElement.doScroll ) ) {\n\n\t// Handle it asynchronously to allow scripts the opportunity to delay ready\n\twindow.setTimeout( jQuery.ready );\n\n} else {\n\n\t// Use the handy event callback\n\tdocument.addEventListener( \"DOMContentLoaded\", completed );\n\n\t// A fallback to window.onload, that will always work\n\twindow.addEventListener( \"load\", completed );\n}\n\n\n\n\n// Multifunctional method to get and set values of a collection\n// The value/s can optionally be executed if it's a function\nvar access = function( elems, fn, key, value, chainable, emptyGet, raw ) {\n\tvar i = 0,\n\t\tlen = elems.length,\n\t\tbulk = key == null;\n\n\t// Sets many values\n\tif ( jQuery.type( key ) === \"object\" ) {\n\t\tchainable = true;\n\t\tfor ( i in key ) {\n\t\t\taccess( elems, fn, i, key[ i ], true, emptyGet, raw );\n\t\t}\n\n\t// Sets one value\n\t} else if ( value !== undefined ) {\n\t\tchainable = true;\n\n\t\tif ( !jQuery.isFunction( value ) ) {\n\t\t\traw = true;\n\t\t}\n\n\t\tif ( bulk ) {\n\n\t\t\t// Bulk operations run against the entire set\n\t\t\tif ( raw ) {\n\t\t\t\tfn.call( elems, value );\n\t\t\t\tfn = null;\n\n\t\t\t// ...except when executing function values\n\t\t\t} else {\n\t\t\t\tbulk = fn;\n\t\t\t\tfn = function( elem, key, value ) {\n\t\t\t\t\treturn bulk.call( jQuery( elem ), value );\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif ( fn ) {\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\tfn(\n\t\t\t\t\telems[ i ], key, raw ?\n\t\t\t\t\tvalue :\n\t\t\t\t\tvalue.call( elems[ i ], i, fn( elems[ i ], key ) )\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( chainable ) {\n\t\treturn elems;\n\t}\n\n\t// Gets\n\tif ( bulk ) {\n\t\treturn fn.call( elems );\n\t}\n\n\treturn len ? fn( elems[ 0 ], key ) : emptyGet;\n};\nvar acceptData = function( owner ) {\n\n\t// Accepts only:\n\t//  - Node\n\t//    - Node.ELEMENT_NODE\n\t//    - Node.DOCUMENT_NODE\n\t//  - Object\n\t//    - Any\n\treturn owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );\n};\n\n\n\n\nfunction Data() {\n\tthis.expando = jQuery.expando + Data.uid++;\n}\n\nData.uid = 1;\n\nData.prototype = {\n\n\tcache: function( owner ) {\n\n\t\t// Check if the owner object already has a cache\n\t\tvar value = owner[ this.expando ];\n\n\t\t// If not, create one\n\t\tif ( !value ) {\n\t\t\tvalue = {};\n\n\t\t\t// We can accept data for non-element nodes in modern browsers,\n\t\t\t// but we should not, see #8335.\n\t\t\t// Always return an empty object.\n\t\t\tif ( acceptData( owner ) ) {\n\n\t\t\t\t// If it is a node unlikely to be stringify-ed or looped over\n\t\t\t\t// use plain assignment\n\t\t\t\tif ( owner.nodeType ) {\n\t\t\t\t\towner[ this.expando ] = value;\n\n\t\t\t\t// Otherwise secure it in a non-enumerable property\n\t\t\t\t// configurable must be true to allow the property to be\n\t\t\t\t// deleted when data is removed\n\t\t\t\t} else {\n\t\t\t\t\tObject.defineProperty( owner, this.expando, {\n\t\t\t\t\t\tvalue: value,\n\t\t\t\t\t\tconfigurable: true\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn value;\n\t},\n\tset: function( owner, data, value ) {\n\t\tvar prop,\n\t\t\tcache = this.cache( owner );\n\n\t\t// Handle: [ owner, key, value ] args\n\t\t// Always use camelCase key (gh-2257)\n\t\tif ( typeof data === \"string\" ) {\n\t\t\tcache[ jQuery.camelCase( data ) ] = value;\n\n\t\t// Handle: [ owner, { properties } ] args\n\t\t} else {\n\n\t\t\t// Copy the properties one-by-one to the cache object\n\t\t\tfor ( prop in data ) {\n\t\t\t\tcache[ jQuery.camelCase( prop ) ] = data[ prop ];\n\t\t\t}\n\t\t}\n\t\treturn cache;\n\t},\n\tget: function( owner, key ) {\n\t\treturn key === undefined ?\n\t\t\tthis.cache( owner ) :\n\n\t\t\t// Always use camelCase key (gh-2257)\n\t\t\towner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ];\n\t},\n\taccess: function( owner, key, value ) {\n\n\t\t// In cases where either:\n\t\t//\n\t\t//   1. No key was specified\n\t\t//   2. A string key was specified, but no value provided\n\t\t//\n\t\t// Take the \"read\" path and allow the get method to determine\n\t\t// which value to return, respectively either:\n\t\t//\n\t\t//   1. The entire cache object\n\t\t//   2. The data stored at the key\n\t\t//\n\t\tif ( key === undefined ||\n\t\t\t\t( ( key && typeof key === \"string\" ) && value === undefined ) ) {\n\n\t\t\treturn this.get( owner, key );\n\t\t}\n\n\t\t// When the key is not a string, or both a key and value\n\t\t// are specified, set or extend (existing objects) with either:\n\t\t//\n\t\t//   1. An object of properties\n\t\t//   2. A key and value\n\t\t//\n\t\tthis.set( owner, key, value );\n\n\t\t// Since the \"set\" path can have two possible entry points\n\t\t// return the expected data based on which path was taken[*]\n\t\treturn value !== undefined ? value : key;\n\t},\n\tremove: function( owner, key ) {\n\t\tvar i,\n\t\t\tcache = owner[ this.expando ];\n\n\t\tif ( cache === undefined ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( key !== undefined ) {\n\n\t\t\t// Support array or space separated string of keys\n\t\t\tif ( Array.isArray( key ) ) {\n\n\t\t\t\t// If key is an array of keys...\n\t\t\t\t// We always set camelCase keys, so remove that.\n\t\t\t\tkey = key.map( jQuery.camelCase );\n\t\t\t} else {\n\t\t\t\tkey = jQuery.camelCase( key );\n\n\t\t\t\t// If a key with the spaces exists, use it.\n\t\t\t\t// Otherwise, create an array by matching non-whitespace\n\t\t\t\tkey = key in cache ?\n\t\t\t\t\t[ key ] :\n\t\t\t\t\t( key.match( rnothtmlwhite ) || [] );\n\t\t\t}\n\n\t\t\ti = key.length;\n\n\t\t\twhile ( i-- ) {\n\t\t\t\tdelete cache[ key[ i ] ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove the expando if there's no more data\n\t\tif ( key === undefined || jQuery.isEmptyObject( cache ) ) {\n\n\t\t\t// Support: Chrome <=35 - 45\n\t\t\t// Webkit & Blink performance suffers when deleting properties\n\t\t\t// from DOM nodes, so set to undefined instead\n\t\t\t// https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)\n\t\t\tif ( owner.nodeType ) {\n\t\t\t\towner[ this.expando ] = undefined;\n\t\t\t} else {\n\t\t\t\tdelete owner[ this.expando ];\n\t\t\t}\n\t\t}\n\t},\n\thasData: function( owner ) {\n\t\tvar cache = owner[ this.expando ];\n\t\treturn cache !== undefined && !jQuery.isEmptyObject( cache );\n\t}\n};\nvar dataPriv = new Data();\n\nvar dataUser = new Data();\n\n\n\n//\tImplementation Summary\n//\n//\t1. Enforce API surface and semantic compatibility with 1.9.x branch\n//\t2. Improve the module's maintainability by reducing the storage\n//\t\tpaths to a single mechanism.\n//\t3. Use the same single mechanism to support \"private\" and \"user\" data.\n//\t4. _Never_ expose \"private\" data to user code (TODO: Drop _data, _removeData)\n//\t5. Avoid exposing implementation details on user objects (eg. expando properties)\n//\t6. Provide a clear path for implementation upgrade to WeakMap in 2014\n\nvar rbrace = /^(?:\\{[\\w\\W]*\\}|\\[[\\w\\W]*\\])$/,\n\trmultiDash = /[A-Z]/g;\n\nfunction getData( data ) {\n\tif ( data === \"true\" ) {\n\t\treturn true;\n\t}\n\n\tif ( data === \"false\" ) {\n\t\treturn false;\n\t}\n\n\tif ( data === \"null\" ) {\n\t\treturn null;\n\t}\n\n\t// Only convert to a number if it doesn't change the string\n\tif ( data === +data + \"\" ) {\n\t\treturn +data;\n\t}\n\n\tif ( rbrace.test( data ) ) {\n\t\treturn JSON.parse( data );\n\t}\n\n\treturn data;\n}\n\nfunction dataAttr( elem, key, data ) {\n\tvar name;\n\n\t// If nothing was found internally, try to fetch any\n\t// data from the HTML5 data-* attribute\n\tif ( data === undefined && elem.nodeType === 1 ) {\n\t\tname = \"data-\" + key.replace( rmultiDash, \"-$&\" ).toLowerCase();\n\t\tdata = elem.getAttribute( name );\n\n\t\tif ( typeof data === \"string\" ) {\n\t\t\ttry {\n\t\t\t\tdata = getData( data );\n\t\t\t} catch ( e ) {}\n\n\t\t\t// Make sure we set the data so it isn't changed later\n\t\t\tdataUser.set( elem, key, data );\n\t\t} else {\n\t\t\tdata = undefined;\n\t\t}\n\t}\n\treturn data;\n}\n\njQuery.extend( {\n\thasData: function( elem ) {\n\t\treturn dataUser.hasData( elem ) || dataPriv.hasData( elem );\n\t},\n\n\tdata: function( elem, name, data ) {\n\t\treturn dataUser.access( elem, name, data );\n\t},\n\n\tremoveData: function( elem, name ) {\n\t\tdataUser.remove( elem, name );\n\t},\n\n\t// TODO: Now that all calls to _data and _removeData have been replaced\n\t// with direct calls to dataPriv methods, these can be deprecated.\n\t_data: function( elem, name, data ) {\n\t\treturn dataPriv.access( elem, name, data );\n\t},\n\n\t_removeData: function( elem, name ) {\n\t\tdataPriv.remove( elem, name );\n\t}\n} );\n\njQuery.fn.extend( {\n\tdata: function( key, value ) {\n\t\tvar i, name, data,\n\t\t\telem = this[ 0 ],\n\t\t\tattrs = elem && elem.attributes;\n\n\t\t// Gets all values\n\t\tif ( key === undefined ) {\n\t\t\tif ( this.length ) {\n\t\t\t\tdata = dataUser.get( elem );\n\n\t\t\t\tif ( elem.nodeType === 1 && !dataPriv.get( elem, \"hasDataAttrs\" ) ) {\n\t\t\t\t\ti = attrs.length;\n\t\t\t\t\twhile ( i-- ) {\n\n\t\t\t\t\t\t// Support: IE 11 only\n\t\t\t\t\t\t// The attrs elements can be null (#14894)\n\t\t\t\t\t\tif ( attrs[ i ] ) {\n\t\t\t\t\t\t\tname = attrs[ i ].name;\n\t\t\t\t\t\t\tif ( name.indexOf( \"data-\" ) === 0 ) {\n\t\t\t\t\t\t\t\tname = jQuery.camelCase( name.slice( 5 ) );\n\t\t\t\t\t\t\t\tdataAttr( elem, name, data[ name ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdataPriv.set( elem, \"hasDataAttrs\", true );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn data;\n\t\t}\n\n\t\t// Sets multiple values\n\t\tif ( typeof key === \"object\" ) {\n\t\t\treturn this.each( function() {\n\t\t\t\tdataUser.set( this, key );\n\t\t\t} );\n\t\t}\n\n\t\treturn access( this, function( value ) {\n\t\t\tvar data;\n\n\t\t\t// The calling jQuery object (element matches) is not empty\n\t\t\t// (and therefore has an element appears at this[ 0 ]) and the\n\t\t\t// `value` parameter was not undefined. An empty jQuery object\n\t\t\t// will result in `undefined` for elem = this[ 0 ] which will\n\t\t\t// throw an exception if an attempt to read a data cache is made.\n\t\t\tif ( elem && value === undefined ) {\n\n\t\t\t\t// Attempt to get data from the cache\n\t\t\t\t// The key will always be camelCased in Data\n\t\t\t\tdata = dataUser.get( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// Attempt to \"discover\" the data in\n\t\t\t\t// HTML5 custom data-* attrs\n\t\t\t\tdata = dataAttr( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// We tried really hard, but the data doesn't exist.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Set the data...\n\t\t\tthis.each( function() {\n\n\t\t\t\t// We always store the camelCased key\n\t\t\t\tdataUser.set( this, key, value );\n\t\t\t} );\n\t\t}, null, value, arguments.length > 1, null, true );\n\t},\n\n\tremoveData: function( key ) {\n\t\treturn this.each( function() {\n\t\t\tdataUser.remove( this, key );\n\t\t} );\n\t}\n} );\n\n\njQuery.extend( {\n\tqueue: function( elem, type, data ) {\n\t\tvar queue;\n\n\t\tif ( elem ) {\n\t\t\ttype = ( type || \"fx\" ) + \"queue\";\n\t\t\tqueue = dataPriv.get( elem, type );\n\n\t\t\t// Speed up dequeue by getting out quickly if this is just a lookup\n\t\t\tif ( data ) {\n\t\t\t\tif ( !queue || Array.isArray( data ) ) {\n\t\t\t\t\tqueue = dataPriv.access( elem, type, jQuery.makeArray( data ) );\n\t\t\t\t} else {\n\t\t\t\t\tqueue.push( data );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn queue || [];\n\t\t}\n\t},\n\n\tdequeue: function( elem, type ) {\n\t\ttype = type || \"fx\";\n\n\t\tvar queue = jQuery.queue( elem, type ),\n\t\t\tstartLength = queue.length,\n\t\t\tfn = queue.shift(),\n\t\t\thooks = jQuery._queueHooks( elem, type ),\n\t\t\tnext = function() {\n\t\t\t\tjQuery.dequeue( elem, type );\n\t\t\t};\n\n\t\t// If the fx queue is dequeued, always remove the progress sentinel\n\t\tif ( fn === \"inprogress\" ) {\n\t\t\tfn = queue.shift();\n\t\t\tstartLength--;\n\t\t}\n\n\t\tif ( fn ) {\n\n\t\t\t// Add a progress sentinel to prevent the fx queue from being\n\t\t\t// automatically dequeued\n\t\t\tif ( type === \"fx\" ) {\n\t\t\t\tqueue.unshift( \"inprogress\" );\n\t\t\t}\n\n\t\t\t// Clear up the last queue stop function\n\t\t\tdelete hooks.stop;\n\t\t\tfn.call( elem, next, hooks );\n\t\t}\n\n\t\tif ( !startLength && hooks ) {\n\t\t\thooks.empty.fire();\n\t\t}\n\t},\n\n\t// Not public - generate a queueHooks object, or return the current one\n\t_queueHooks: function( elem, type ) {\n\t\tvar key = type + \"queueHooks\";\n\t\treturn dataPriv.get( elem, key ) || dataPriv.access( elem, key, {\n\t\t\tempty: jQuery.Callbacks( \"once memory\" ).add( function() {\n\t\t\t\tdataPriv.remove( elem, [ type + \"queue\", key ] );\n\t\t\t} )\n\t\t} );\n\t}\n} );\n\njQuery.fn.extend( {\n\tqueue: function( type, data ) {\n\t\tvar setter = 2;\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tdata = type;\n\t\t\ttype = \"fx\";\n\t\t\tsetter--;\n\t\t}\n\n\t\tif ( arguments.length < setter ) {\n\t\t\treturn jQuery.queue( this[ 0 ], type );\n\t\t}\n\n\t\treturn data === undefined ?\n\t\t\tthis :\n\t\t\tthis.each( function() {\n\t\t\t\tvar queue = jQuery.queue( this, type, data );\n\n\t\t\t\t// Ensure a hooks for this queue\n\t\t\t\tjQuery._queueHooks( this, type );\n\n\t\t\t\tif ( type === \"fx\" && queue[ 0 ] !== \"inprogress\" ) {\n\t\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t\t}\n\t\t\t} );\n\t},\n\tdequeue: function( type ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.dequeue( this, type );\n\t\t} );\n\t},\n\tclearQueue: function( type ) {\n\t\treturn this.queue( type || \"fx\", [] );\n\t},\n\n\t// Get a promise resolved when queues of a certain type\n\t// are emptied (fx is the type by default)\n\tpromise: function( type, obj ) {\n\t\tvar tmp,\n\t\t\tcount = 1,\n\t\t\tdefer = jQuery.Deferred(),\n\t\t\telements = this,\n\t\t\ti = this.length,\n\t\t\tresolve = function() {\n\t\t\t\tif ( !( --count ) ) {\n\t\t\t\t\tdefer.resolveWith( elements, [ elements ] );\n\t\t\t\t}\n\t\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tobj = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\ttype = type || \"fx\";\n\n\t\twhile ( i-- ) {\n\t\t\ttmp = dataPriv.get( elements[ i ], type + \"queueHooks\" );\n\t\t\tif ( tmp && tmp.empty ) {\n\t\t\t\tcount++;\n\t\t\t\ttmp.empty.add( resolve );\n\t\t\t}\n\t\t}\n\t\tresolve();\n\t\treturn defer.promise( obj );\n\t}\n} );\nvar pnum = ( /[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/ ).source;\n\nvar rcssNum = new RegExp( \"^(?:([+-])=|)(\" + pnum + \")([a-z%]*)$\", \"i\" );\n\n\nvar cssExpand = [ \"Top\", \"Right\", \"Bottom\", \"Left\" ];\n\nvar isHiddenWithinTree = function( elem, el ) {\n\n\t\t// isHiddenWithinTree might be called from jQuery#filter function;\n\t\t// in that case, element will be second argument\n\t\telem = el || elem;\n\n\t\t// Inline style trumps all\n\t\treturn elem.style.display === \"none\" ||\n\t\t\telem.style.display === \"\" &&\n\n\t\t\t// Otherwise, check computed style\n\t\t\t// Support: Firefox <=43 - 45\n\t\t\t// Disconnected elements can have computed display: none, so first confirm that elem is\n\t\t\t// in the document.\n\t\t\tjQuery.contains( elem.ownerDocument, elem ) &&\n\n\t\t\tjQuery.css( elem, \"display\" ) === \"none\";\n\t};\n\nvar swap = function( elem, options, callback, args ) {\n\tvar ret, name,\n\t\told = {};\n\n\t// Remember the old values, and insert the new ones\n\tfor ( name in options ) {\n\t\told[ name ] = elem.style[ name ];\n\t\telem.style[ name ] = options[ name ];\n\t}\n\n\tret = callback.apply( elem, args || [] );\n\n\t// Revert the old values\n\tfor ( name in options ) {\n\t\telem.style[ name ] = old[ name ];\n\t}\n\n\treturn ret;\n};\n\n\n\n\nfunction adjustCSS( elem, prop, valueParts, tween ) {\n\tvar adjusted,\n\t\tscale = 1,\n\t\tmaxIterations = 20,\n\t\tcurrentValue = tween ?\n\t\t\tfunction() {\n\t\t\t\treturn tween.cur();\n\t\t\t} :\n\t\t\tfunction() {\n\t\t\t\treturn jQuery.css( elem, prop, \"\" );\n\t\t\t},\n\t\tinitial = currentValue(),\n\t\tunit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" ),\n\n\t\t// Starting value computation is required for potential unit mismatches\n\t\tinitialInUnit = ( jQuery.cssNumber[ prop ] || unit !== \"px\" && +initial ) &&\n\t\t\trcssNum.exec( jQuery.css( elem, prop ) );\n\n\tif ( initialInUnit && initialInUnit[ 3 ] !== unit ) {\n\n\t\t// Trust units reported by jQuery.css\n\t\tunit = unit || initialInUnit[ 3 ];\n\n\t\t// Make sure we update the tween properties later on\n\t\tvalueParts = valueParts || [];\n\n\t\t// Iteratively approximate from a nonzero starting point\n\t\tinitialInUnit = +initial || 1;\n\n\t\tdo {\n\n\t\t\t// If previous iteration zeroed out, double until we get *something*.\n\t\t\t// Use string for doubling so we don't accidentally see scale as unchanged below\n\t\t\tscale = scale || \".5\";\n\n\t\t\t// Adjust and apply\n\t\t\tinitialInUnit = initialInUnit / scale;\n\t\t\tjQuery.style( elem, prop, initialInUnit + unit );\n\n\t\t// Update scale, tolerating zero or NaN from tween.cur()\n\t\t// Break the loop if scale is unchanged or perfect, or if we've just had enough.\n\t\t} while (\n\t\t\tscale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations\n\t\t);\n\t}\n\n\tif ( valueParts ) {\n\t\tinitialInUnit = +initialInUnit || +initial || 0;\n\n\t\t// Apply relative offset (+=/-=) if specified\n\t\tadjusted = valueParts[ 1 ] ?\n\t\t\tinitialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :\n\t\t\t+valueParts[ 2 ];\n\t\tif ( tween ) {\n\t\t\ttween.unit = unit;\n\t\t\ttween.start = initialInUnit;\n\t\t\ttween.end = adjusted;\n\t\t}\n\t}\n\treturn adjusted;\n}\n\n\nvar defaultDisplayMap = {};\n\nfunction getDefaultDisplay( elem ) {\n\tvar temp,\n\t\tdoc = elem.ownerDocument,\n\t\tnodeName = elem.nodeName,\n\t\tdisplay = defaultDisplayMap[ nodeName ];\n\n\tif ( display ) {\n\t\treturn display;\n\t}\n\n\ttemp = doc.body.appendChild( doc.createElement( nodeName ) );\n\tdisplay = jQuery.css( temp, \"display\" );\n\n\ttemp.parentNode.removeChild( temp );\n\n\tif ( display === \"none\" ) {\n\t\tdisplay = \"block\";\n\t}\n\tdefaultDisplayMap[ nodeName ] = display;\n\n\treturn display;\n}\n\nfunction showHide( elements, show ) {\n\tvar display, elem,\n\t\tvalues = [],\n\t\tindex = 0,\n\t\tlength = elements.length;\n\n\t// Determine new display value for elements that need to change\n\tfor ( ; index < length; index++ ) {\n\t\telem = elements[ index ];\n\t\tif ( !elem.style ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tdisplay = elem.style.display;\n\t\tif ( show ) {\n\n\t\t\t// Since we force visibility upon cascade-hidden elements, an immediate (and slow)\n\t\t\t// check is required in this first loop unless we have a nonempty display value (either\n\t\t\t// inline or about-to-be-restored)\n\t\t\tif ( display === \"none\" ) {\n\t\t\t\tvalues[ index ] = dataPriv.get( elem, \"display\" ) || null;\n\t\t\t\tif ( !values[ index ] ) {\n\t\t\t\t\telem.style.display = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( elem.style.display === \"\" && isHiddenWithinTree( elem ) ) {\n\t\t\t\tvalues[ index ] = getDefaultDisplay( elem );\n\t\t\t}\n\t\t} else {\n\t\t\tif ( display !== \"none\" ) {\n\t\t\t\tvalues[ index ] = \"none\";\n\n\t\t\t\t// Remember what we're overwriting\n\t\t\t\tdataPriv.set( elem, \"display\", display );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Set the display of the elements in a second loop to avoid constant reflow\n\tfor ( index = 0; index < length; index++ ) {\n\t\tif ( values[ index ] != null ) {\n\t\t\telements[ index ].style.display = values[ index ];\n\t\t}\n\t}\n\n\treturn elements;\n}\n\njQuery.fn.extend( {\n\tshow: function() {\n\t\treturn showHide( this, true );\n\t},\n\thide: function() {\n\t\treturn showHide( this );\n\t},\n\ttoggle: function( state ) {\n\t\tif ( typeof state === \"boolean\" ) {\n\t\t\treturn state ? this.show() : this.hide();\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tif ( isHiddenWithinTree( this ) ) {\n\t\t\t\tjQuery( this ).show();\n\t\t\t} else {\n\t\t\t\tjQuery( this ).hide();\n\t\t\t}\n\t\t} );\n\t}\n} );\nvar rcheckableType = ( /^(?:checkbox|radio)$/i );\n\nvar rtagName = ( /<([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]+)/i );\n\nvar rscriptType = ( /^$|\\/(?:java|ecma)script/i );\n\n\n\n// We have to close these tags to support XHTML (#13200)\nvar wrapMap = {\n\n\t// Support: IE <=9 only\n\toption: [ 1, \"<select multiple='multiple'>\", \"</select>\" ],\n\n\t// XHTML parsers do not magically insert elements in the\n\t// same way that tag soup parsers do. So we cannot shorten\n\t// this by omitting <tbody> or other required elements.\n\tthead: [ 1, \"<table>\", \"</table>\" ],\n\tcol: [ 2, \"<table><colgroup>\", \"</colgroup></table>\" ],\n\ttr: [ 2, \"<table><tbody>\", \"</tbody></table>\" ],\n\ttd: [ 3, \"<table><tbody><tr>\", \"</tr></tbody></table>\" ],\n\n\t_default: [ 0, \"\", \"\" ]\n};\n\n// Support: IE <=9 only\nwrapMap.optgroup = wrapMap.option;\n\nwrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;\nwrapMap.th = wrapMap.td;\n\n\nfunction getAll( context, tag ) {\n\n\t// Support: IE <=9 - 11 only\n\t// Use typeof to avoid zero-argument method invocation on host objects (#15151)\n\tvar ret;\n\n\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\tret = context.getElementsByTagName( tag || \"*\" );\n\n\t} else if ( typeof context.querySelectorAll !== \"undefined\" ) {\n\t\tret = context.querySelectorAll( tag || \"*\" );\n\n\t} else {\n\t\tret = [];\n\t}\n\n\tif ( tag === undefined || tag && nodeName( context, tag ) ) {\n\t\treturn jQuery.merge( [ context ], ret );\n\t}\n\n\treturn ret;\n}\n\n\n// Mark scripts as having already been evaluated\nfunction setGlobalEval( elems, refElements ) {\n\tvar i = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\tdataPriv.set(\n\t\t\telems[ i ],\n\t\t\t\"globalEval\",\n\t\t\t!refElements || dataPriv.get( refElements[ i ], \"globalEval\" )\n\t\t);\n\t}\n}\n\n\nvar rhtml = /<|&#?\\w+;/;\n\nfunction buildFragment( elems, context, scripts, selection, ignored ) {\n\tvar elem, tmp, tag, wrap, contains, j,\n\t\tfragment = context.createDocumentFragment(),\n\t\tnodes = [],\n\t\ti = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\telem = elems[ i ];\n\n\t\tif ( elem || elem === 0 ) {\n\n\t\t\t// Add nodes directly\n\t\t\tif ( jQuery.type( elem ) === \"object\" ) {\n\n\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\tjQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );\n\n\t\t\t// Convert non-html into a text node\n\t\t\t} else if ( !rhtml.test( elem ) ) {\n\t\t\t\tnodes.push( context.createTextNode( elem ) );\n\n\t\t\t// Convert html into DOM nodes\n\t\t\t} else {\n\t\t\t\ttmp = tmp || fragment.appendChild( context.createElement( \"div\" ) );\n\n\t\t\t\t// Deserialize a standard representation\n\t\t\t\ttag = ( rtagName.exec( elem ) || [ \"\", \"\" ] )[ 1 ].toLowerCase();\n\t\t\t\twrap = wrapMap[ tag ] || wrapMap._default;\n\t\t\t\ttmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];\n\n\t\t\t\t// Descend through wrappers to the right content\n\t\t\t\tj = wrap[ 0 ];\n\t\t\t\twhile ( j-- ) {\n\t\t\t\t\ttmp = tmp.lastChild;\n\t\t\t\t}\n\n\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\tjQuery.merge( nodes, tmp.childNodes );\n\n\t\t\t\t// Remember the top-level container\n\t\t\t\ttmp = fragment.firstChild;\n\n\t\t\t\t// Ensure the created nodes are orphaned (#12392)\n\t\t\t\ttmp.textContent = \"\";\n\t\t\t}\n\t\t}\n\t}\n\n\t// Remove wrapper from fragment\n\tfragment.textContent = \"\";\n\n\ti = 0;\n\twhile ( ( elem = nodes[ i++ ] ) ) {\n\n\t\t// Skip elements already in the context collection (trac-4087)\n\t\tif ( selection && jQuery.inArray( elem, selection ) > -1 ) {\n\t\t\tif ( ignored ) {\n\t\t\t\tignored.push( elem );\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tcontains = jQuery.contains( elem.ownerDocument, elem );\n\n\t\t// Append to fragment\n\t\ttmp = getAll( fragment.appendChild( elem ), \"script\" );\n\n\t\t// Preserve script evaluation history\n\t\tif ( contains ) {\n\t\t\tsetGlobalEval( tmp );\n\t\t}\n\n\t\t// Capture executables\n\t\tif ( scripts ) {\n\t\t\tj = 0;\n\t\t\twhile ( ( elem = tmp[ j++ ] ) ) {\n\t\t\t\tif ( rscriptType.test( elem.type || \"\" ) ) {\n\t\t\t\t\tscripts.push( elem );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fragment;\n}\n\n\n( function() {\n\tvar fragment = document.createDocumentFragment(),\n\t\tdiv = fragment.appendChild( document.createElement( \"div\" ) ),\n\t\tinput = document.createElement( \"input\" );\n\n\t// Support: Android 4.0 - 4.3 only\n\t// Check state lost if the name is set (#11217)\n\t// Support: Windows Web Apps (WWA)\n\t// `name` and `type` must use .setAttribute for WWA (#14901)\n\tinput.setAttribute( \"type\", \"radio\" );\n\tinput.setAttribute( \"checked\", \"checked\" );\n\tinput.setAttribute( \"name\", \"t\" );\n\n\tdiv.appendChild( input );\n\n\t// Support: Android <=4.1 only\n\t// Older WebKit doesn't clone checked state correctly in fragments\n\tsupport.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;\n\n\t// Support: IE <=11 only\n\t// Make sure textarea (and checkbox) defaultValue is properly cloned\n\tdiv.innerHTML = \"<textarea>x</textarea>\";\n\tsupport.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;\n} )();\nvar documentElement = document.documentElement;\n\n\n\nvar\n\trkeyEvent = /^key/,\n\trmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,\n\trtypenamespace = /^([^.]*)(?:\\.(.+)|)/;\n\nfunction returnTrue() {\n\treturn true;\n}\n\nfunction returnFalse() {\n\treturn false;\n}\n\n// Support: IE <=9 only\n// See #13393 for more info\nfunction safeActiveElement() {\n\ttry {\n\t\treturn document.activeElement;\n\t} catch ( err ) { }\n}\n\nfunction on( elem, types, selector, data, fn, one ) {\n\tvar origFn, type;\n\n\t// Types can be a map of types/handlers\n\tif ( typeof types === \"object\" ) {\n\n\t\t// ( types-Object, selector, data )\n\t\tif ( typeof selector !== \"string\" ) {\n\n\t\t\t// ( types-Object, data )\n\t\t\tdata = data || selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tfor ( type in types ) {\n\t\t\ton( elem, type, selector, data, types[ type ], one );\n\t\t}\n\t\treturn elem;\n\t}\n\n\tif ( data == null && fn == null ) {\n\n\t\t// ( types, fn )\n\t\tfn = selector;\n\t\tdata = selector = undefined;\n\t} else if ( fn == null ) {\n\t\tif ( typeof selector === \"string\" ) {\n\n\t\t\t// ( types, selector, fn )\n\t\t\tfn = data;\n\t\t\tdata = undefined;\n\t\t} else {\n\n\t\t\t// ( types, data, fn )\n\t\t\tfn = data;\n\t\t\tdata = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t}\n\tif ( fn === false ) {\n\t\tfn = returnFalse;\n\t} else if ( !fn ) {\n\t\treturn elem;\n\t}\n\n\tif ( one === 1 ) {\n\t\torigFn = fn;\n\t\tfn = function( event ) {\n\n\t\t\t// Can use an empty set, since event contains the info\n\t\t\tjQuery().off( event );\n\t\t\treturn origFn.apply( this, arguments );\n\t\t};\n\n\t\t// Use same guid so caller can remove using origFn\n\t\tfn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );\n\t}\n\treturn elem.each( function() {\n\t\tjQuery.event.add( this, types, fn, data, selector );\n\t} );\n}\n\n/*\n * Helper functions for managing events -- not part of the public interface.\n * Props to Dean Edwards' addEvent library for many of the ideas.\n */\njQuery.event = {\n\n\tglobal: {},\n\n\tadd: function( elem, types, handler, data, selector ) {\n\n\t\tvar handleObjIn, eventHandle, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = dataPriv.get( elem );\n\n\t\t// Don't attach events to noData or text/comment nodes (but allow plain objects)\n\t\tif ( !elemData ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Caller can pass in an object of custom data in lieu of the handler\n\t\tif ( handler.handler ) {\n\t\t\thandleObjIn = handler;\n\t\t\thandler = handleObjIn.handler;\n\t\t\tselector = handleObjIn.selector;\n\t\t}\n\n\t\t// Ensure that invalid selectors throw exceptions at attach time\n\t\t// Evaluate against documentElement in case elem is a non-element node (e.g., document)\n\t\tif ( selector ) {\n\t\t\tjQuery.find.matchesSelector( documentElement, selector );\n\t\t}\n\n\t\t// Make sure that the handler has a unique ID, used to find/remove it later\n\t\tif ( !handler.guid ) {\n\t\t\thandler.guid = jQuery.guid++;\n\t\t}\n\n\t\t// Init the element's event structure and main handler, if this is the first\n\t\tif ( !( events = elemData.events ) ) {\n\t\t\tevents = elemData.events = {};\n\t\t}\n\t\tif ( !( eventHandle = elemData.handle ) ) {\n\t\t\teventHandle = elemData.handle = function( e ) {\n\n\t\t\t\t// Discard the second event of a jQuery.event.trigger() and\n\t\t\t\t// when an event is called after a page has unloaded\n\t\t\t\treturn typeof jQuery !== \"undefined\" && jQuery.event.triggered !== e.type ?\n\t\t\t\t\tjQuery.event.dispatch.apply( elem, arguments ) : undefined;\n\t\t\t};\n\t\t}\n\n\t\t// Handle multiple events separated by a space\n\t\ttypes = ( types || \"\" ).match( rnothtmlwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[ t ] ) || [];\n\t\t\ttype = origType = tmp[ 1 ];\n\t\t\tnamespaces = ( tmp[ 2 ] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// There *must* be a type, no attaching namespace-only handlers\n\t\t\tif ( !type ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// If event changes its type, use the special event handlers for the changed type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// If selector defined, determine special event api type, otherwise given type\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\n\t\t\t// Update special based on newly reset type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// handleObj is passed to all event handlers\n\t\t\thandleObj = jQuery.extend( {\n\t\t\t\ttype: type,\n\t\t\t\torigType: origType,\n\t\t\t\tdata: data,\n\t\t\t\thandler: handler,\n\t\t\t\tguid: handler.guid,\n\t\t\t\tselector: selector,\n\t\t\t\tneedsContext: selector && jQuery.expr.match.needsContext.test( selector ),\n\t\t\t\tnamespace: namespaces.join( \".\" )\n\t\t\t}, handleObjIn );\n\n\t\t\t// Init the event handler queue if we're the first\n\t\t\tif ( !( handlers = events[ type ] ) ) {\n\t\t\t\thandlers = events[ type ] = [];\n\t\t\t\thandlers.delegateCount = 0;\n\n\t\t\t\t// Only use addEventListener if the special events handler returns false\n\t\t\t\tif ( !special.setup ||\n\t\t\t\t\tspecial.setup.call( elem, data, namespaces, eventHandle ) === false ) {\n\n\t\t\t\t\tif ( elem.addEventListener ) {\n\t\t\t\t\t\telem.addEventListener( type, eventHandle );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( special.add ) {\n\t\t\t\tspecial.add.call( elem, handleObj );\n\n\t\t\t\tif ( !handleObj.handler.guid ) {\n\t\t\t\t\thandleObj.handler.guid = handler.guid;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add to the element's handler list, delegates in front\n\t\t\tif ( selector ) {\n\t\t\t\thandlers.splice( handlers.delegateCount++, 0, handleObj );\n\t\t\t} else {\n\t\t\t\thandlers.push( handleObj );\n\t\t\t}\n\n\t\t\t// Keep track of which events have ever been used, for event optimization\n\t\t\tjQuery.event.global[ type ] = true;\n\t\t}\n\n\t},\n\n\t// Detach an event or set of events from an element\n\tremove: function( elem, types, handler, selector, mappedTypes ) {\n\n\t\tvar j, origCount, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = dataPriv.hasData( elem ) && dataPriv.get( elem );\n\n\t\tif ( !elemData || !( events = elemData.events ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Once for each type.namespace in types; type may be omitted\n\t\ttypes = ( types || \"\" ).match( rnothtmlwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[ t ] ) || [];\n\t\t\ttype = origType = tmp[ 1 ];\n\t\t\tnamespaces = ( tmp[ 2 ] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// Unbind all events (on this namespace, if provided) for the element\n\t\t\tif ( !type ) {\n\t\t\t\tfor ( type in events ) {\n\t\t\t\t\tjQuery.event.remove( elem, type + types[ t ], handler, selector, true );\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\t\t\thandlers = events[ type ] || [];\n\t\t\ttmp = tmp[ 2 ] &&\n\t\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join( \"\\\\.(?:.*\\\\.|)\" ) + \"(\\\\.|$)\" );\n\n\t\t\t// Remove matching events\n\t\t\torigCount = j = handlers.length;\n\t\t\twhile ( j-- ) {\n\t\t\t\thandleObj = handlers[ j ];\n\n\t\t\t\tif ( ( mappedTypes || origType === handleObj.origType ) &&\n\t\t\t\t\t( !handler || handler.guid === handleObj.guid ) &&\n\t\t\t\t\t( !tmp || tmp.test( handleObj.namespace ) ) &&\n\t\t\t\t\t( !selector || selector === handleObj.selector ||\n\t\t\t\t\t\tselector === \"**\" && handleObj.selector ) ) {\n\t\t\t\t\thandlers.splice( j, 1 );\n\n\t\t\t\t\tif ( handleObj.selector ) {\n\t\t\t\t\t\thandlers.delegateCount--;\n\t\t\t\t\t}\n\t\t\t\t\tif ( special.remove ) {\n\t\t\t\t\t\tspecial.remove.call( elem, handleObj );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Remove generic event handler if we removed something and no more handlers exist\n\t\t\t// (avoids potential for endless recursion during removal of special event handlers)\n\t\t\tif ( origCount && !handlers.length ) {\n\t\t\t\tif ( !special.teardown ||\n\t\t\t\t\tspecial.teardown.call( elem, namespaces, elemData.handle ) === false ) {\n\n\t\t\t\t\tjQuery.removeEvent( elem, type, elemData.handle );\n\t\t\t\t}\n\n\t\t\t\tdelete events[ type ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove data and the expando if it's no longer used\n\t\tif ( jQuery.isEmptyObject( events ) ) {\n\t\t\tdataPriv.remove( elem, \"handle events\" );\n\t\t}\n\t},\n\n\tdispatch: function( nativeEvent ) {\n\n\t\t// Make a writable jQuery.Event from the native event object\n\t\tvar event = jQuery.event.fix( nativeEvent );\n\n\t\tvar i, j, ret, matched, handleObj, handlerQueue,\n\t\t\targs = new Array( arguments.length ),\n\t\t\thandlers = ( dataPriv.get( this, \"events\" ) || {} )[ event.type ] || [],\n\t\t\tspecial = jQuery.event.special[ event.type ] || {};\n\n\t\t// Use the fix-ed jQuery.Event rather than the (read-only) native event\n\t\targs[ 0 ] = event;\n\n\t\tfor ( i = 1; i < arguments.length; i++ ) {\n\t\t\targs[ i ] = arguments[ i ];\n\t\t}\n\n\t\tevent.delegateTarget = this;\n\n\t\t// Call the preDispatch hook for the mapped type, and let it bail if desired\n\t\tif ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine handlers\n\t\thandlerQueue = jQuery.event.handlers.call( this, event, handlers );\n\n\t\t// Run delegates first; they may want to stop propagation beneath us\n\t\ti = 0;\n\t\twhile ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {\n\t\t\tevent.currentTarget = matched.elem;\n\n\t\t\tj = 0;\n\t\t\twhile ( ( handleObj = matched.handlers[ j++ ] ) &&\n\t\t\t\t!event.isImmediatePropagationStopped() ) {\n\n\t\t\t\t// Triggered event must either 1) have no namespace, or 2) have namespace(s)\n\t\t\t\t// a subset or equal to those in the bound event (both can have no namespace).\n\t\t\t\tif ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) {\n\n\t\t\t\t\tevent.handleObj = handleObj;\n\t\t\t\t\tevent.data = handleObj.data;\n\n\t\t\t\t\tret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||\n\t\t\t\t\t\thandleObj.handler ).apply( matched.elem, args );\n\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\tif ( ( event.result = ret ) === false ) {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Call the postDispatch hook for the mapped type\n\t\tif ( special.postDispatch ) {\n\t\t\tspecial.postDispatch.call( this, event );\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\thandlers: function( event, handlers ) {\n\t\tvar i, handleObj, sel, matchedHandlers, matchedSelectors,\n\t\t\thandlerQueue = [],\n\t\t\tdelegateCount = handlers.delegateCount,\n\t\t\tcur = event.target;\n\n\t\t// Find delegate handlers\n\t\tif ( delegateCount &&\n\n\t\t\t// Support: IE <=9\n\t\t\t// Black-hole SVG <use> instance trees (trac-13180)\n\t\t\tcur.nodeType &&\n\n\t\t\t// Support: Firefox <=42\n\t\t\t// Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)\n\t\t\t// https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click\n\t\t\t// Support: IE 11 only\n\t\t\t// ...but not arrow key \"clicks\" of radio inputs, which can have `button` -1 (gh-2343)\n\t\t\t!( event.type === \"click\" && event.button >= 1 ) ) {\n\n\t\t\tfor ( ; cur !== this; cur = cur.parentNode || this ) {\n\n\t\t\t\t// Don't check non-elements (#13208)\n\t\t\t\t// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)\n\t\t\t\tif ( cur.nodeType === 1 && !( event.type === \"click\" && cur.disabled === true ) ) {\n\t\t\t\t\tmatchedHandlers = [];\n\t\t\t\t\tmatchedSelectors = {};\n\t\t\t\t\tfor ( i = 0; i < delegateCount; i++ ) {\n\t\t\t\t\t\thandleObj = handlers[ i ];\n\n\t\t\t\t\t\t// Don't conflict with Object.prototype properties (#13203)\n\t\t\t\t\t\tsel = handleObj.selector + \" \";\n\n\t\t\t\t\t\tif ( matchedSelectors[ sel ] === undefined ) {\n\t\t\t\t\t\t\tmatchedSelectors[ sel ] = handleObj.needsContext ?\n\t\t\t\t\t\t\t\tjQuery( sel, this ).index( cur ) > -1 :\n\t\t\t\t\t\t\t\tjQuery.find( sel, this, null, [ cur ] ).length;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( matchedSelectors[ sel ] ) {\n\t\t\t\t\t\t\tmatchedHandlers.push( handleObj );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( matchedHandlers.length ) {\n\t\t\t\t\t\thandlerQueue.push( { elem: cur, handlers: matchedHandlers } );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add the remaining (directly-bound) handlers\n\t\tcur = this;\n\t\tif ( delegateCount < handlers.length ) {\n\t\t\thandlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );\n\t\t}\n\n\t\treturn handlerQueue;\n\t},\n\n\taddProp: function( name, hook ) {\n\t\tObject.defineProperty( jQuery.Event.prototype, name, {\n\t\t\tenumerable: true,\n\t\t\tconfigurable: true,\n\n\t\t\tget: jQuery.isFunction( hook ) ?\n\t\t\t\tfunction() {\n\t\t\t\t\tif ( this.originalEvent ) {\n\t\t\t\t\t\t\treturn hook( this.originalEvent );\n\t\t\t\t\t}\n\t\t\t\t} :\n\t\t\t\tfunction() {\n\t\t\t\t\tif ( this.originalEvent ) {\n\t\t\t\t\t\t\treturn this.originalEvent[ name ];\n\t\t\t\t\t}\n\t\t\t\t},\n\n\t\t\tset: function( value ) {\n\t\t\t\tObject.defineProperty( this, name, {\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t\twritable: true,\n\t\t\t\t\tvalue: value\n\t\t\t\t} );\n\t\t\t}\n\t\t} );\n\t},\n\n\tfix: function( originalEvent ) {\n\t\treturn originalEvent[ jQuery.expando ] ?\n\t\t\toriginalEvent :\n\t\t\tnew jQuery.Event( originalEvent );\n\t},\n\n\tspecial: {\n\t\tload: {\n\n\t\t\t// Prevent triggered image.load events from bubbling to window.load\n\t\t\tnoBubble: true\n\t\t},\n\t\tfocus: {\n\n\t\t\t// Fire native event if possible so blur/focus sequence is correct\n\t\t\ttrigger: function() {\n\t\t\t\tif ( this !== safeActiveElement() && this.focus ) {\n\t\t\t\t\tthis.focus();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdelegateType: \"focusin\"\n\t\t},\n\t\tblur: {\n\t\t\ttrigger: function() {\n\t\t\t\tif ( this === safeActiveElement() && this.blur ) {\n\t\t\t\t\tthis.blur();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdelegateType: \"focusout\"\n\t\t},\n\t\tclick: {\n\n\t\t\t// For checkbox, fire native event so checked state will be right\n\t\t\ttrigger: function() {\n\t\t\t\tif ( this.type === \"checkbox\" && this.click && nodeName( this, \"input\" ) ) {\n\t\t\t\t\tthis.click();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t},\n\n\t\t\t// For cross-browser consistency, don't fire native .click() on links\n\t\t\t_default: function( event ) {\n\t\t\t\treturn nodeName( event.target, \"a\" );\n\t\t\t}\n\t\t},\n\n\t\tbeforeunload: {\n\t\t\tpostDispatch: function( event ) {\n\n\t\t\t\t// Support: Firefox 20+\n\t\t\t\t// Firefox doesn't alert if the returnValue field is not set.\n\t\t\t\tif ( event.result !== undefined && event.originalEvent ) {\n\t\t\t\t\tevent.originalEvent.returnValue = event.result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n};\n\njQuery.removeEvent = function( elem, type, handle ) {\n\n\t// This \"if\" is needed for plain objects\n\tif ( elem.removeEventListener ) {\n\t\telem.removeEventListener( type, handle );\n\t}\n};\n\njQuery.Event = function( src, props ) {\n\n\t// Allow instantiation without the 'new' keyword\n\tif ( !( this instanceof jQuery.Event ) ) {\n\t\treturn new jQuery.Event( src, props );\n\t}\n\n\t// Event object\n\tif ( src && src.type ) {\n\t\tthis.originalEvent = src;\n\t\tthis.type = src.type;\n\n\t\t// Events bubbling up the document may have been marked as prevented\n\t\t// by a handler lower down the tree; reflect the correct value.\n\t\tthis.isDefaultPrevented = src.defaultPrevented ||\n\t\t\t\tsrc.defaultPrevented === undefined &&\n\n\t\t\t\t// Support: Android <=2.3 only\n\t\t\t\tsrc.returnValue === false ?\n\t\t\treturnTrue :\n\t\t\treturnFalse;\n\n\t\t// Create target properties\n\t\t// Support: Safari <=6 - 7 only\n\t\t// Target should not be a text node (#504, #13143)\n\t\tthis.target = ( src.target && src.target.nodeType === 3 ) ?\n\t\t\tsrc.target.parentNode :\n\t\t\tsrc.target;\n\n\t\tthis.currentTarget = src.currentTarget;\n\t\tthis.relatedTarget = src.relatedTarget;\n\n\t// Event type\n\t} else {\n\t\tthis.type = src;\n\t}\n\n\t// Put explicitly provided properties onto the event object\n\tif ( props ) {\n\t\tjQuery.extend( this, props );\n\t}\n\n\t// Create a timestamp if incoming event doesn't have one\n\tthis.timeStamp = src && src.timeStamp || jQuery.now();\n\n\t// Mark it as fixed\n\tthis[ jQuery.expando ] = true;\n};\n\n// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding\n// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html\njQuery.Event.prototype = {\n\tconstructor: jQuery.Event,\n\tisDefaultPrevented: returnFalse,\n\tisPropagationStopped: returnFalse,\n\tisImmediatePropagationStopped: returnFalse,\n\tisSimulated: false,\n\n\tpreventDefault: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isDefaultPrevented = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.preventDefault();\n\t\t}\n\t},\n\tstopPropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isPropagationStopped = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.stopPropagation();\n\t\t}\n\t},\n\tstopImmediatePropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isImmediatePropagationStopped = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.stopImmediatePropagation();\n\t\t}\n\n\t\tthis.stopPropagation();\n\t}\n};\n\n// Includes all common event props including KeyEvent and MouseEvent specific props\njQuery.each( {\n\taltKey: true,\n\tbubbles: true,\n\tcancelable: true,\n\tchangedTouches: true,\n\tctrlKey: true,\n\tdetail: true,\n\teventPhase: true,\n\tmetaKey: true,\n\tpageX: true,\n\tpageY: true,\n\tshiftKey: true,\n\tview: true,\n\t\"char\": true,\n\tcharCode: true,\n\tkey: true,\n\tkeyCode: true,\n\tbutton: true,\n\tbuttons: true,\n\tclientX: true,\n\tclientY: true,\n\toffsetX: true,\n\toffsetY: true,\n\tpointerId: true,\n\tpointerType: true,\n\tscreenX: true,\n\tscreenY: true,\n\ttargetTouches: true,\n\ttoElement: true,\n\ttouches: true,\n\n\twhich: function( event ) {\n\t\tvar button = event.button;\n\n\t\t// Add which for key events\n\t\tif ( event.which == null && rkeyEvent.test( event.type ) ) {\n\t\t\treturn event.charCode != null ? event.charCode : event.keyCode;\n\t\t}\n\n\t\t// Add which for click: 1 === left; 2 === middle; 3 === right\n\t\tif ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {\n\t\t\tif ( button & 1 ) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\tif ( button & 2 ) {\n\t\t\t\treturn 3;\n\t\t\t}\n\n\t\t\tif ( button & 4 ) {\n\t\t\t\treturn 2;\n\t\t\t}\n\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn event.which;\n\t}\n}, jQuery.event.addProp );\n\n// Create mouseenter/leave events using mouseover/out and event-time checks\n// so that event delegation works in jQuery.\n// Do the same for pointerenter/pointerleave and pointerover/pointerout\n//\n// Support: Safari 7 only\n// Safari sends mouseenter too often; see:\n// https://bugs.chromium.org/p/chromium/issues/detail?id=470258\n// for the description of the bug (it existed in older Chrome versions as well).\njQuery.each( {\n\tmouseenter: \"mouseover\",\n\tmouseleave: \"mouseout\",\n\tpointerenter: \"pointerover\",\n\tpointerleave: \"pointerout\"\n}, function( orig, fix ) {\n\tjQuery.event.special[ orig ] = {\n\t\tdelegateType: fix,\n\t\tbindType: fix,\n\n\t\thandle: function( event ) {\n\t\t\tvar ret,\n\t\t\t\ttarget = this,\n\t\t\t\trelated = event.relatedTarget,\n\t\t\t\thandleObj = event.handleObj;\n\n\t\t\t// For mouseenter/leave call the handler if related is outside the target.\n\t\t\t// NB: No relatedTarget if the mouse left/entered the browser window\n\t\t\tif ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {\n\t\t\t\tevent.type = handleObj.origType;\n\t\t\t\tret = handleObj.handler.apply( this, arguments );\n\t\t\t\tevent.type = fix;\n\t\t\t}\n\t\t\treturn ret;\n\t\t}\n\t};\n} );\n\njQuery.fn.extend( {\n\n\ton: function( types, selector, data, fn ) {\n\t\treturn on( this, types, selector, data, fn );\n\t},\n\tone: function( types, selector, data, fn ) {\n\t\treturn on( this, types, selector, data, fn, 1 );\n\t},\n\toff: function( types, selector, fn ) {\n\t\tvar handleObj, type;\n\t\tif ( types && types.preventDefault && types.handleObj ) {\n\n\t\t\t// ( event )  dispatched jQuery.Event\n\t\t\thandleObj = types.handleObj;\n\t\t\tjQuery( types.delegateTarget ).off(\n\t\t\t\thandleObj.namespace ?\n\t\t\t\t\thandleObj.origType + \".\" + handleObj.namespace :\n\t\t\t\t\thandleObj.origType,\n\t\t\t\thandleObj.selector,\n\t\t\t\thandleObj.handler\n\t\t\t);\n\t\t\treturn this;\n\t\t}\n\t\tif ( typeof types === \"object\" ) {\n\n\t\t\t// ( types-object [, selector] )\n\t\t\tfor ( type in types ) {\n\t\t\t\tthis.off( type, selector, types[ type ] );\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\t\tif ( selector === false || typeof selector === \"function\" ) {\n\n\t\t\t// ( types [, fn] )\n\t\t\tfn = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tif ( fn === false ) {\n\t\t\tfn = returnFalse;\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.remove( this, types, fn, selector );\n\t\t} );\n\t}\n} );\n\n\nvar\n\n\t/* eslint-disable max-len */\n\n\t// See https://github.com/eslint/eslint/issues/3229\n\trxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]*)[^>]*)\\/>/gi,\n\n\t/* eslint-enable */\n\n\t// Support: IE <=10 - 11, Edge 12 - 13\n\t// In IE/Edge using regex groups here causes severe slowdowns.\n\t// See https://connect.microsoft.com/IE/feedback/details/1736512/\n\trnoInnerhtml = /<script|<style|<link/i,\n\n\t// checked=\"checked\" or checked\n\trchecked = /checked\\s*(?:[^=]|=\\s*.checked.)/i,\n\trscriptTypeMasked = /^true\\/(.*)/,\n\trcleanScript = /^\\s*<!(?:\\[CDATA\\[|--)|(?:\\]\\]|--)>\\s*$/g;\n\n// Prefer a tbody over its parent table for containing new rows\nfunction manipulationTarget( elem, content ) {\n\tif ( nodeName( elem, \"table\" ) &&\n\t\tnodeName( content.nodeType !== 11 ? content : content.firstChild, \"tr\" ) ) {\n\n\t\treturn jQuery( \">tbody\", elem )[ 0 ] || elem;\n\t}\n\n\treturn elem;\n}\n\n// Replace/restore the type attribute of script elements for safe DOM manipulation\nfunction disableScript( elem ) {\n\telem.type = ( elem.getAttribute( \"type\" ) !== null ) + \"/\" + elem.type;\n\treturn elem;\n}\nfunction restoreScript( elem ) {\n\tvar match = rscriptTypeMasked.exec( elem.type );\n\n\tif ( match ) {\n\t\telem.type = match[ 1 ];\n\t} else {\n\t\telem.removeAttribute( \"type\" );\n\t}\n\n\treturn elem;\n}\n\nfunction cloneCopyEvent( src, dest ) {\n\tvar i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;\n\n\tif ( dest.nodeType !== 1 ) {\n\t\treturn;\n\t}\n\n\t// 1. Copy private data: events, handlers, etc.\n\tif ( dataPriv.hasData( src ) ) {\n\t\tpdataOld = dataPriv.access( src );\n\t\tpdataCur = dataPriv.set( dest, pdataOld );\n\t\tevents = pdataOld.events;\n\n\t\tif ( events ) {\n\t\t\tdelete pdataCur.handle;\n\t\t\tpdataCur.events = {};\n\n\t\t\tfor ( type in events ) {\n\t\t\t\tfor ( i = 0, l = events[ type ].length; i < l; i++ ) {\n\t\t\t\t\tjQuery.event.add( dest, type, events[ type ][ i ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2. Copy user data\n\tif ( dataUser.hasData( src ) ) {\n\t\tudataOld = dataUser.access( src );\n\t\tudataCur = jQuery.extend( {}, udataOld );\n\n\t\tdataUser.set( dest, udataCur );\n\t}\n}\n\n// Fix IE bugs, see support tests\nfunction fixInput( src, dest ) {\n\tvar nodeName = dest.nodeName.toLowerCase();\n\n\t// Fails to persist the checked state of a cloned checkbox or radio button.\n\tif ( nodeName === \"input\" && rcheckableType.test( src.type ) ) {\n\t\tdest.checked = src.checked;\n\n\t// Fails to return the selected option to the default selected state when cloning options\n\t} else if ( nodeName === \"input\" || nodeName === \"textarea\" ) {\n\t\tdest.defaultValue = src.defaultValue;\n\t}\n}\n\nfunction domManip( collection, args, callback, ignored ) {\n\n\t// Flatten any nested arrays\n\targs = concat.apply( [], args );\n\n\tvar fragment, first, scripts, hasScripts, node, doc,\n\t\ti = 0,\n\t\tl = collection.length,\n\t\tiNoClone = l - 1,\n\t\tvalue = args[ 0 ],\n\t\tisFunction = jQuery.isFunction( value );\n\n\t// We can't cloneNode fragments that contain checked, in WebKit\n\tif ( isFunction ||\n\t\t\t( l > 1 && typeof value === \"string\" &&\n\t\t\t\t!support.checkClone && rchecked.test( value ) ) ) {\n\t\treturn collection.each( function( index ) {\n\t\t\tvar self = collection.eq( index );\n\t\t\tif ( isFunction ) {\n\t\t\t\targs[ 0 ] = value.call( this, index, self.html() );\n\t\t\t}\n\t\t\tdomManip( self, args, callback, ignored );\n\t\t} );\n\t}\n\n\tif ( l ) {\n\t\tfragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );\n\t\tfirst = fragment.firstChild;\n\n\t\tif ( fragment.childNodes.length === 1 ) {\n\t\t\tfragment = first;\n\t\t}\n\n\t\t// Require either new content or an interest in ignored elements to invoke the callback\n\t\tif ( first || ignored ) {\n\t\t\tscripts = jQuery.map( getAll( fragment, \"script\" ), disableScript );\n\t\t\thasScripts = scripts.length;\n\n\t\t\t// Use the original fragment for the last item\n\t\t\t// instead of the first because it can end up\n\t\t\t// being emptied incorrectly in certain situations (#8070).\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tnode = fragment;\n\n\t\t\t\tif ( i !== iNoClone ) {\n\t\t\t\t\tnode = jQuery.clone( node, true, true );\n\n\t\t\t\t\t// Keep references to cloned scripts for later restoration\n\t\t\t\t\tif ( hasScripts ) {\n\n\t\t\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\t\t\tjQuery.merge( scripts, getAll( node, \"script\" ) );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcallback.call( collection[ i ], node, i );\n\t\t\t}\n\n\t\t\tif ( hasScripts ) {\n\t\t\t\tdoc = scripts[ scripts.length - 1 ].ownerDocument;\n\n\t\t\t\t// Reenable scripts\n\t\t\t\tjQuery.map( scripts, restoreScript );\n\n\t\t\t\t// Evaluate executable scripts on first document insertion\n\t\t\t\tfor ( i = 0; i < hasScripts; i++ ) {\n\t\t\t\t\tnode = scripts[ i ];\n\t\t\t\t\tif ( rscriptType.test( node.type || \"\" ) &&\n\t\t\t\t\t\t!dataPriv.access( node, \"globalEval\" ) &&\n\t\t\t\t\t\tjQuery.contains( doc, node ) ) {\n\n\t\t\t\t\t\tif ( node.src ) {\n\n\t\t\t\t\t\t\t// Optional AJAX dependency, but won't run scripts if not present\n\t\t\t\t\t\t\tif ( jQuery._evalUrl ) {\n\t\t\t\t\t\t\t\tjQuery._evalUrl( node.src );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tDOMEval( node.textContent.replace( rcleanScript, \"\" ), doc );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn collection;\n}\n\nfunction remove( elem, selector, keepData ) {\n\tvar node,\n\t\tnodes = selector ? jQuery.filter( selector, elem ) : elem,\n\t\ti = 0;\n\n\tfor ( ; ( node = nodes[ i ] ) != null; i++ ) {\n\t\tif ( !keepData && node.nodeType === 1 ) {\n\t\t\tjQuery.cleanData( getAll( node ) );\n\t\t}\n\n\t\tif ( node.parentNode ) {\n\t\t\tif ( keepData && jQuery.contains( node.ownerDocument, node ) ) {\n\t\t\t\tsetGlobalEval( getAll( node, \"script\" ) );\n\t\t\t}\n\t\t\tnode.parentNode.removeChild( node );\n\t\t}\n\t}\n\n\treturn elem;\n}\n\njQuery.extend( {\n\thtmlPrefilter: function( html ) {\n\t\treturn html.replace( rxhtmlTag, \"<$1></$2>\" );\n\t},\n\n\tclone: function( elem, dataAndEvents, deepDataAndEvents ) {\n\t\tvar i, l, srcElements, destElements,\n\t\t\tclone = elem.cloneNode( true ),\n\t\t\tinPage = jQuery.contains( elem.ownerDocument, elem );\n\n\t\t// Fix IE cloning issues\n\t\tif ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&\n\t\t\t\t!jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2\n\t\t\tdestElements = getAll( clone );\n\t\t\tsrcElements = getAll( elem );\n\n\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\tfixInput( srcElements[ i ], destElements[ i ] );\n\t\t\t}\n\t\t}\n\n\t\t// Copy the events from the original to the clone\n\t\tif ( dataAndEvents ) {\n\t\t\tif ( deepDataAndEvents ) {\n\t\t\t\tsrcElements = srcElements || getAll( elem );\n\t\t\t\tdestElements = destElements || getAll( clone );\n\n\t\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\t\tcloneCopyEvent( srcElements[ i ], destElements[ i ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcloneCopyEvent( elem, clone );\n\t\t\t}\n\t\t}\n\n\t\t// Preserve script evaluation history\n\t\tdestElements = getAll( clone, \"script\" );\n\t\tif ( destElements.length > 0 ) {\n\t\t\tsetGlobalEval( destElements, !inPage && getAll( elem, \"script\" ) );\n\t\t}\n\n\t\t// Return the cloned set\n\t\treturn clone;\n\t},\n\n\tcleanData: function( elems ) {\n\t\tvar data, elem, type,\n\t\t\tspecial = jQuery.event.special,\n\t\t\ti = 0;\n\n\t\tfor ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {\n\t\t\tif ( acceptData( elem ) ) {\n\t\t\t\tif ( ( data = elem[ dataPriv.expando ] ) ) {\n\t\t\t\t\tif ( data.events ) {\n\t\t\t\t\t\tfor ( type in data.events ) {\n\t\t\t\t\t\t\tif ( special[ type ] ) {\n\t\t\t\t\t\t\t\tjQuery.event.remove( elem, type );\n\n\t\t\t\t\t\t\t// This is a shortcut to avoid jQuery.event.remove's overhead\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjQuery.removeEvent( elem, type, data.handle );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t\t\t// Assign undefined instead of using delete, see Data#remove\n\t\t\t\t\telem[ dataPriv.expando ] = undefined;\n\t\t\t\t}\n\t\t\t\tif ( elem[ dataUser.expando ] ) {\n\n\t\t\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t\t\t// Assign undefined instead of using delete, see Data#remove\n\t\t\t\t\telem[ dataUser.expando ] = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n} );\n\njQuery.fn.extend( {\n\tdetach: function( selector ) {\n\t\treturn remove( this, selector, true );\n\t},\n\n\tremove: function( selector ) {\n\t\treturn remove( this, selector );\n\t},\n\n\ttext: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\treturn value === undefined ?\n\t\t\t\tjQuery.text( this ) :\n\t\t\t\tthis.empty().each( function() {\n\t\t\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\t\t\tthis.textContent = value;\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t}, null, value, arguments.length );\n\t},\n\n\tappend: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.appendChild( elem );\n\t\t\t}\n\t\t} );\n\t},\n\n\tprepend: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.insertBefore( elem, target.firstChild );\n\t\t\t}\n\t\t} );\n\t},\n\n\tbefore: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this );\n\t\t\t}\n\t\t} );\n\t},\n\n\tafter: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this.nextSibling );\n\t\t\t}\n\t\t} );\n\t},\n\n\tempty: function() {\n\t\tvar elem,\n\t\t\ti = 0;\n\n\t\tfor ( ; ( elem = this[ i ] ) != null; i++ ) {\n\t\t\tif ( elem.nodeType === 1 ) {\n\n\t\t\t\t// Prevent memory leaks\n\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\n\t\t\t\t// Remove any remaining nodes\n\t\t\t\telem.textContent = \"\";\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tclone: function( dataAndEvents, deepDataAndEvents ) {\n\t\tdataAndEvents = dataAndEvents == null ? false : dataAndEvents;\n\t\tdeepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;\n\n\t\treturn this.map( function() {\n\t\t\treturn jQuery.clone( this, dataAndEvents, deepDataAndEvents );\n\t\t} );\n\t},\n\n\thtml: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\tvar elem = this[ 0 ] || {},\n\t\t\t\ti = 0,\n\t\t\t\tl = this.length;\n\n\t\t\tif ( value === undefined && elem.nodeType === 1 ) {\n\t\t\t\treturn elem.innerHTML;\n\t\t\t}\n\n\t\t\t// See if we can take a shortcut and just use innerHTML\n\t\t\tif ( typeof value === \"string\" && !rnoInnerhtml.test( value ) &&\n\t\t\t\t!wrapMap[ ( rtagName.exec( value ) || [ \"\", \"\" ] )[ 1 ].toLowerCase() ] ) {\n\n\t\t\t\tvalue = jQuery.htmlPrefilter( value );\n\n\t\t\t\ttry {\n\t\t\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\t\t\telem = this[ i ] || {};\n\n\t\t\t\t\t\t// Remove element nodes and prevent memory leaks\n\t\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\t\t\t\t\t\t\telem.innerHTML = value;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\telem = 0;\n\n\t\t\t\t// If using innerHTML throws an exception, use the fallback method\n\t\t\t\t} catch ( e ) {}\n\t\t\t}\n\n\t\t\tif ( elem ) {\n\t\t\t\tthis.empty().append( value );\n\t\t\t}\n\t\t}, null, value, arguments.length );\n\t},\n\n\treplaceWith: function() {\n\t\tvar ignored = [];\n\n\t\t// Make the changes, replacing each non-ignored context element with the new content\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tvar parent = this.parentNode;\n\n\t\t\tif ( jQuery.inArray( this, ignored ) < 0 ) {\n\t\t\t\tjQuery.cleanData( getAll( this ) );\n\t\t\t\tif ( parent ) {\n\t\t\t\t\tparent.replaceChild( elem, this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Force callback invocation\n\t\t}, ignored );\n\t}\n} );\n\njQuery.each( {\n\tappendTo: \"append\",\n\tprependTo: \"prepend\",\n\tinsertBefore: \"before\",\n\tinsertAfter: \"after\",\n\treplaceAll: \"replaceWith\"\n}, function( name, original ) {\n\tjQuery.fn[ name ] = function( selector ) {\n\t\tvar elems,\n\t\t\tret = [],\n\t\t\tinsert = jQuery( selector ),\n\t\t\tlast = insert.length - 1,\n\t\t\ti = 0;\n\n\t\tfor ( ; i <= last; i++ ) {\n\t\t\telems = i === last ? this : this.clone( true );\n\t\t\tjQuery( insert[ i ] )[ original ]( elems );\n\n\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t// .get() because push.apply(_, arraylike) throws on ancient WebKit\n\t\t\tpush.apply( ret, elems.get() );\n\t\t}\n\n\t\treturn this.pushStack( ret );\n\t};\n} );\nvar rmargin = ( /^margin/ );\n\nvar rnumnonpx = new RegExp( \"^(\" + pnum + \")(?!px)[a-z%]+$\", \"i\" );\n\nvar getStyles = function( elem ) {\n\n\t\t// Support: IE <=11 only, Firefox <=30 (#15098, #14150)\n\t\t// IE throws on elements created in popups\n\t\t// FF meanwhile throws on frame elements through \"defaultView.getComputedStyle\"\n\t\tvar view = elem.ownerDocument.defaultView;\n\n\t\tif ( !view || !view.opener ) {\n\t\t\tview = window;\n\t\t}\n\n\t\treturn view.getComputedStyle( elem );\n\t};\n\n\n\n( function() {\n\n\t// Executing both pixelPosition & boxSizingReliable tests require only one layout\n\t// so they're executed at the same time to save the second computation.\n\tfunction computeStyleTests() {\n\n\t\t// This is a singleton, we need to execute it only once\n\t\tif ( !div ) {\n\t\t\treturn;\n\t\t}\n\n\t\tdiv.style.cssText =\n\t\t\t\"box-sizing:border-box;\" +\n\t\t\t\"position:relative;display:block;\" +\n\t\t\t\"margin:auto;border:1px;padding:1px;\" +\n\t\t\t\"top:1%;width:50%\";\n\t\tdiv.innerHTML = \"\";\n\t\tdocumentElement.appendChild( container );\n\n\t\tvar divStyle = window.getComputedStyle( div );\n\t\tpixelPositionVal = divStyle.top !== \"1%\";\n\n\t\t// Support: Android 4.0 - 4.3 only, Firefox <=3 - 44\n\t\treliableMarginLeftVal = divStyle.marginLeft === \"2px\";\n\t\tboxSizingReliableVal = divStyle.width === \"4px\";\n\n\t\t// Support: Android 4.0 - 4.3 only\n\t\t// Some styles come back with percentage values, even though they shouldn't\n\t\tdiv.style.marginRight = \"50%\";\n\t\tpixelMarginRightVal = divStyle.marginRight === \"4px\";\n\n\t\tdocumentElement.removeChild( container );\n\n\t\t// Nullify the div so it wouldn't be stored in the memory and\n\t\t// it will also be a sign that checks already performed\n\t\tdiv = null;\n\t}\n\n\tvar pixelPositionVal, boxSizingReliableVal, pixelMarginRightVal, reliableMarginLeftVal,\n\t\tcontainer = document.createElement( \"div\" ),\n\t\tdiv = document.createElement( \"div\" );\n\n\t// Finish early in limited (non-browser) environments\n\tif ( !div.style ) {\n\t\treturn;\n\t}\n\n\t// Support: IE <=9 - 11 only\n\t// Style of cloned element affects source element cloned (#8908)\n\tdiv.style.backgroundClip = \"content-box\";\n\tdiv.cloneNode( true ).style.backgroundClip = \"\";\n\tsupport.clearCloneStyle = div.style.backgroundClip === \"content-box\";\n\n\tcontainer.style.cssText = \"border:0;width:8px;height:0;top:0;left:-9999px;\" +\n\t\t\"padding:0;margin-top:1px;position:absolute\";\n\tcontainer.appendChild( div );\n\n\tjQuery.extend( support, {\n\t\tpixelPosition: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn pixelPositionVal;\n\t\t},\n\t\tboxSizingReliable: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn boxSizingReliableVal;\n\t\t},\n\t\tpixelMarginRight: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn pixelMarginRightVal;\n\t\t},\n\t\treliableMarginLeft: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn reliableMarginLeftVal;\n\t\t}\n\t} );\n} )();\n\n\nfunction curCSS( elem, name, computed ) {\n\tvar width, minWidth, maxWidth, ret,\n\n\t\t// Support: Firefox 51+\n\t\t// Retrieving style before computed somehow\n\t\t// fixes an issue with getting wrong values\n\t\t// on detached elements\n\t\tstyle = elem.style;\n\n\tcomputed = computed || getStyles( elem );\n\n\t// getPropertyValue is needed for:\n\t//   .css('filter') (IE 9 only, #12537)\n\t//   .css('--customProperty) (#3144)\n\tif ( computed ) {\n\t\tret = computed.getPropertyValue( name ) || computed[ name ];\n\n\t\tif ( ret === \"\" && !jQuery.contains( elem.ownerDocument, elem ) ) {\n\t\t\tret = jQuery.style( elem, name );\n\t\t}\n\n\t\t// A tribute to the \"awesome hack by Dean Edwards\"\n\t\t// Android Browser returns percentage for some values,\n\t\t// but width seems to be reliably pixels.\n\t\t// This is against the CSSOM draft spec:\n\t\t// https://drafts.csswg.org/cssom/#resolved-values\n\t\tif ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) {\n\n\t\t\t// Remember the original values\n\t\t\twidth = style.width;\n\t\t\tminWidth = style.minWidth;\n\t\t\tmaxWidth = style.maxWidth;\n\n\t\t\t// Put in the new values to get a computed value out\n\t\t\tstyle.minWidth = style.maxWidth = style.width = ret;\n\t\t\tret = computed.width;\n\n\t\t\t// Revert the changed values\n\t\t\tstyle.width = width;\n\t\t\tstyle.minWidth = minWidth;\n\t\t\tstyle.maxWidth = maxWidth;\n\t\t}\n\t}\n\n\treturn ret !== undefined ?\n\n\t\t// Support: IE <=9 - 11 only\n\t\t// IE returns zIndex value as an integer.\n\t\tret + \"\" :\n\t\tret;\n}\n\n\nfunction addGetHookIf( conditionFn, hookFn ) {\n\n\t// Define the hook, we'll check on the first run if it's really needed.\n\treturn {\n\t\tget: function() {\n\t\t\tif ( conditionFn() ) {\n\n\t\t\t\t// Hook not needed (or it's not possible to use it due\n\t\t\t\t// to missing dependency), remove it.\n\t\t\t\tdelete this.get;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Hook needed; redefine it so that the support test is not executed again.\n\t\t\treturn ( this.get = hookFn ).apply( this, arguments );\n\t\t}\n\t};\n}\n\n\nvar\n\n\t// Swappable if display is none or starts with table\n\t// except \"table\", \"table-cell\", or \"table-caption\"\n\t// See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display\n\trdisplayswap = /^(none|table(?!-c[ea]).+)/,\n\trcustomProp = /^--/,\n\tcssShow = { position: \"absolute\", visibility: \"hidden\", display: \"block\" },\n\tcssNormalTransform = {\n\t\tletterSpacing: \"0\",\n\t\tfontWeight: \"400\"\n\t},\n\n\tcssPrefixes = [ \"Webkit\", \"Moz\", \"ms\" ],\n\temptyStyle = document.createElement( \"div\" ).style;\n\n// Return a css property mapped to a potentially vendor prefixed property\nfunction vendorPropName( name ) {\n\n\t// Shortcut for names that are not vendor prefixed\n\tif ( name in emptyStyle ) {\n\t\treturn name;\n\t}\n\n\t// Check for vendor prefixed names\n\tvar capName = name[ 0 ].toUpperCase() + name.slice( 1 ),\n\t\ti = cssPrefixes.length;\n\n\twhile ( i-- ) {\n\t\tname = cssPrefixes[ i ] + capName;\n\t\tif ( name in emptyStyle ) {\n\t\t\treturn name;\n\t\t}\n\t}\n}\n\n// Return a property mapped along what jQuery.cssProps suggests or to\n// a vendor prefixed property.\nfunction finalPropName( name ) {\n\tvar ret = jQuery.cssProps[ name ];\n\tif ( !ret ) {\n\t\tret = jQuery.cssProps[ name ] = vendorPropName( name ) || name;\n\t}\n\treturn ret;\n}\n\nfunction setPositiveNumber( elem, value, subtract ) {\n\n\t// Any relative (+/-) values have already been\n\t// normalized at this point\n\tvar matches = rcssNum.exec( value );\n\treturn matches ?\n\n\t\t// Guard against undefined \"subtract\", e.g., when used as in cssHooks\n\t\tMath.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || \"px\" ) :\n\t\tvalue;\n}\n\nfunction augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {\n\tvar i,\n\t\tval = 0;\n\n\t// If we already have the right measurement, avoid augmentation\n\tif ( extra === ( isBorderBox ? \"border\" : \"content\" ) ) {\n\t\ti = 4;\n\n\t// Otherwise initialize for horizontal or vertical properties\n\t} else {\n\t\ti = name === \"width\" ? 1 : 0;\n\t}\n\n\tfor ( ; i < 4; i += 2 ) {\n\n\t\t// Both box models exclude margin, so add it if we want it\n\t\tif ( extra === \"margin\" ) {\n\t\t\tval += jQuery.css( elem, extra + cssExpand[ i ], true, styles );\n\t\t}\n\n\t\tif ( isBorderBox ) {\n\n\t\t\t// border-box includes padding, so remove it if we want content\n\t\t\tif ( extra === \"content\" ) {\n\t\t\t\tval -= jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\t\t\t}\n\n\t\t\t// At this point, extra isn't border nor margin, so remove border\n\t\t\tif ( extra !== \"margin\" ) {\n\t\t\t\tval -= jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\t\t} else {\n\n\t\t\t// At this point, extra isn't content, so add padding\n\t\t\tval += jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\n\t\t\t// At this point, extra isn't content nor padding, so add border\n\t\t\tif ( extra !== \"padding\" ) {\n\t\t\t\tval += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\t\t}\n\t}\n\n\treturn val;\n}\n\nfunction getWidthOrHeight( elem, name, extra ) {\n\n\t// Start with computed style\n\tvar valueIsBorderBox,\n\t\tstyles = getStyles( elem ),\n\t\tval = curCSS( elem, name, styles ),\n\t\tisBorderBox = jQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\";\n\n\t// Computed unit is not pixels. Stop here and return.\n\tif ( rnumnonpx.test( val ) ) {\n\t\treturn val;\n\t}\n\n\t// Check for style in case a browser which returns unreliable values\n\t// for getComputedStyle silently falls back to the reliable elem.style\n\tvalueIsBorderBox = isBorderBox &&\n\t\t( support.boxSizingReliable() || val === elem.style[ name ] );\n\n\t// Fall back to offsetWidth/Height when value is \"auto\"\n\t// This happens for inline elements with no explicit setting (gh-3571)\n\tif ( val === \"auto\" ) {\n\t\tval = elem[ \"offset\" + name[ 0 ].toUpperCase() + name.slice( 1 ) ];\n\t}\n\n\t// Normalize \"\", auto, and prepare for extra\n\tval = parseFloat( val ) || 0;\n\n\t// Use the active box-sizing model to add/subtract irrelevant styles\n\treturn ( val +\n\t\taugmentWidthOrHeight(\n\t\t\telem,\n\t\t\tname,\n\t\t\textra || ( isBorderBox ? \"border\" : \"content\" ),\n\t\t\tvalueIsBorderBox,\n\t\t\tstyles\n\t\t)\n\t) + \"px\";\n}\n\njQuery.extend( {\n\n\t// Add in style property hooks for overriding the default\n\t// behavior of getting and setting a style property\n\tcssHooks: {\n\t\topacity: {\n\t\t\tget: function( elem, computed ) {\n\t\t\t\tif ( computed ) {\n\n\t\t\t\t\t// We should always get a number back from opacity\n\t\t\t\t\tvar ret = curCSS( elem, \"opacity\" );\n\t\t\t\t\treturn ret === \"\" ? \"1\" : ret;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\t// Don't automatically add \"px\" to these possibly-unitless properties\n\tcssNumber: {\n\t\t\"animationIterationCount\": true,\n\t\t\"columnCount\": true,\n\t\t\"fillOpacity\": true,\n\t\t\"flexGrow\": true,\n\t\t\"flexShrink\": true,\n\t\t\"fontWeight\": true,\n\t\t\"lineHeight\": true,\n\t\t\"opacity\": true,\n\t\t\"order\": true,\n\t\t\"orphans\": true,\n\t\t\"widows\": true,\n\t\t\"zIndex\": true,\n\t\t\"zoom\": true\n\t},\n\n\t// Add in properties whose names you wish to fix before\n\t// setting or getting the value\n\tcssProps: {\n\t\t\"float\": \"cssFloat\"\n\t},\n\n\t// Get and set the style property on a DOM Node\n\tstyle: function( elem, name, value, extra ) {\n\n\t\t// Don't set styles on text and comment nodes\n\t\tif ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Make sure that we're working with the right name\n\t\tvar ret, type, hooks,\n\t\t\torigName = jQuery.camelCase( name ),\n\t\t\tisCustomProp = rcustomProp.test( name ),\n\t\t\tstyle = elem.style;\n\n\t\t// Make sure that we're working with the right name. We don't\n\t\t// want to query the value if it is a CSS custom property\n\t\t// since they are user-defined.\n\t\tif ( !isCustomProp ) {\n\t\t\tname = finalPropName( origName );\n\t\t}\n\n\t\t// Gets hook for the prefixed version, then unprefixed version\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// Check if we're setting a value\n\t\tif ( value !== undefined ) {\n\t\t\ttype = typeof value;\n\n\t\t\t// Convert \"+=\" or \"-=\" to relative numbers (#7345)\n\t\t\tif ( type === \"string\" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {\n\t\t\t\tvalue = adjustCSS( elem, name, ret );\n\n\t\t\t\t// Fixes bug #9237\n\t\t\t\ttype = \"number\";\n\t\t\t}\n\n\t\t\t// Make sure that null and NaN values aren't set (#7116)\n\t\t\tif ( value == null || value !== value ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If a number was passed in, add the unit (except for certain CSS properties)\n\t\t\tif ( type === \"number\" ) {\n\t\t\t\tvalue += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? \"\" : \"px\" );\n\t\t\t}\n\n\t\t\t// background-* props affect original clone's values\n\t\t\tif ( !support.clearCloneStyle && value === \"\" && name.indexOf( \"background\" ) === 0 ) {\n\t\t\t\tstyle[ name ] = \"inherit\";\n\t\t\t}\n\n\t\t\t// If a hook was provided, use that value, otherwise just set the specified value\n\t\t\tif ( !hooks || !( \"set\" in hooks ) ||\n\t\t\t\t( value = hooks.set( elem, value, extra ) ) !== undefined ) {\n\n\t\t\t\tif ( isCustomProp ) {\n\t\t\t\t\tstyle.setProperty( name, value );\n\t\t\t\t} else {\n\t\t\t\t\tstyle[ name ] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t// If a hook was provided get the non-computed value from there\n\t\t\tif ( hooks && \"get\" in hooks &&\n\t\t\t\t( ret = hooks.get( elem, false, extra ) ) !== undefined ) {\n\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\t// Otherwise just get the value from the style object\n\t\t\treturn style[ name ];\n\t\t}\n\t},\n\n\tcss: function( elem, name, extra, styles ) {\n\t\tvar val, num, hooks,\n\t\t\torigName = jQuery.camelCase( name ),\n\t\t\tisCustomProp = rcustomProp.test( name );\n\n\t\t// Make sure that we're working with the right name. We don't\n\t\t// want to modify the value if it is a CSS custom property\n\t\t// since they are user-defined.\n\t\tif ( !isCustomProp ) {\n\t\t\tname = finalPropName( origName );\n\t\t}\n\n\t\t// Try prefixed name followed by the unprefixed name\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// If a hook was provided get the computed value from there\n\t\tif ( hooks && \"get\" in hooks ) {\n\t\t\tval = hooks.get( elem, true, extra );\n\t\t}\n\n\t\t// Otherwise, if a way to get the computed value exists, use that\n\t\tif ( val === undefined ) {\n\t\t\tval = curCSS( elem, name, styles );\n\t\t}\n\n\t\t// Convert \"normal\" to computed value\n\t\tif ( val === \"normal\" && name in cssNormalTransform ) {\n\t\t\tval = cssNormalTransform[ name ];\n\t\t}\n\n\t\t// Make numeric if forced or a qualifier was provided and val looks numeric\n\t\tif ( extra === \"\" || extra ) {\n\t\t\tnum = parseFloat( val );\n\t\t\treturn extra === true || isFinite( num ) ? num || 0 : val;\n\t\t}\n\n\t\treturn val;\n\t}\n} );\n\njQuery.each( [ \"height\", \"width\" ], function( i, name ) {\n\tjQuery.cssHooks[ name ] = {\n\t\tget: function( elem, computed, extra ) {\n\t\t\tif ( computed ) {\n\n\t\t\t\t// Certain elements can have dimension info if we invisibly show them\n\t\t\t\t// but it must have a current display style that would benefit\n\t\t\t\treturn rdisplayswap.test( jQuery.css( elem, \"display\" ) ) &&\n\n\t\t\t\t\t// Support: Safari 8+\n\t\t\t\t\t// Table columns in Safari have non-zero offsetWidth & zero\n\t\t\t\t\t// getBoundingClientRect().width unless display is changed.\n\t\t\t\t\t// Support: IE <=11 only\n\t\t\t\t\t// Running getBoundingClientRect on a disconnected node\n\t\t\t\t\t// in IE throws an error.\n\t\t\t\t\t( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?\n\t\t\t\t\t\tswap( elem, cssShow, function() {\n\t\t\t\t\t\t\treturn getWidthOrHeight( elem, name, extra );\n\t\t\t\t\t\t} ) :\n\t\t\t\t\t\tgetWidthOrHeight( elem, name, extra );\n\t\t\t}\n\t\t},\n\n\t\tset: function( elem, value, extra ) {\n\t\t\tvar matches,\n\t\t\t\tstyles = extra && getStyles( elem ),\n\t\t\t\tsubtract = extra && augmentWidthOrHeight(\n\t\t\t\t\telem,\n\t\t\t\t\tname,\n\t\t\t\t\textra,\n\t\t\t\t\tjQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\t\t\t\tstyles\n\t\t\t\t);\n\n\t\t\t// Convert to pixels if value adjustment is needed\n\t\t\tif ( subtract && ( matches = rcssNum.exec( value ) ) &&\n\t\t\t\t( matches[ 3 ] || \"px\" ) !== \"px\" ) {\n\n\t\t\t\telem.style[ name ] = value;\n\t\t\t\tvalue = jQuery.css( elem, name );\n\t\t\t}\n\n\t\t\treturn setPositiveNumber( elem, value, subtract );\n\t\t}\n\t};\n} );\n\njQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,\n\tfunction( elem, computed ) {\n\t\tif ( computed ) {\n\t\t\treturn ( parseFloat( curCSS( elem, \"marginLeft\" ) ) ||\n\t\t\t\telem.getBoundingClientRect().left -\n\t\t\t\t\tswap( elem, { marginLeft: 0 }, function() {\n\t\t\t\t\t\treturn elem.getBoundingClientRect().left;\n\t\t\t\t\t} )\n\t\t\t\t) + \"px\";\n\t\t}\n\t}\n);\n\n// These hooks are used by animate to expand properties\njQuery.each( {\n\tmargin: \"\",\n\tpadding: \"\",\n\tborder: \"Width\"\n}, function( prefix, suffix ) {\n\tjQuery.cssHooks[ prefix + suffix ] = {\n\t\texpand: function( value ) {\n\t\t\tvar i = 0,\n\t\t\t\texpanded = {},\n\n\t\t\t\t// Assumes a single number if not a string\n\t\t\t\tparts = typeof value === \"string\" ? value.split( \" \" ) : [ value ];\n\n\t\t\tfor ( ; i < 4; i++ ) {\n\t\t\t\texpanded[ prefix + cssExpand[ i ] + suffix ] =\n\t\t\t\t\tparts[ i ] || parts[ i - 2 ] || parts[ 0 ];\n\t\t\t}\n\n\t\t\treturn expanded;\n\t\t}\n\t};\n\n\tif ( !rmargin.test( prefix ) ) {\n\t\tjQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;\n\t}\n} );\n\njQuery.fn.extend( {\n\tcss: function( name, value ) {\n\t\treturn access( this, function( elem, name, value ) {\n\t\t\tvar styles, len,\n\t\t\t\tmap = {},\n\t\t\t\ti = 0;\n\n\t\t\tif ( Array.isArray( name ) ) {\n\t\t\t\tstyles = getStyles( elem );\n\t\t\t\tlen = name.length;\n\n\t\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\t\tmap[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );\n\t\t\t\t}\n\n\t\t\t\treturn map;\n\t\t\t}\n\n\t\t\treturn value !== undefined ?\n\t\t\t\tjQuery.style( elem, name, value ) :\n\t\t\t\tjQuery.css( elem, name );\n\t\t}, name, value, arguments.length > 1 );\n\t}\n} );\n\n\nfunction Tween( elem, options, prop, end, easing ) {\n\treturn new Tween.prototype.init( elem, options, prop, end, easing );\n}\njQuery.Tween = Tween;\n\nTween.prototype = {\n\tconstructor: Tween,\n\tinit: function( elem, options, prop, end, easing, unit ) {\n\t\tthis.elem = elem;\n\t\tthis.prop = prop;\n\t\tthis.easing = easing || jQuery.easing._default;\n\t\tthis.options = options;\n\t\tthis.start = this.now = this.cur();\n\t\tthis.end = end;\n\t\tthis.unit = unit || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" );\n\t},\n\tcur: function() {\n\t\tvar hooks = Tween.propHooks[ this.prop ];\n\n\t\treturn hooks && hooks.get ?\n\t\t\thooks.get( this ) :\n\t\t\tTween.propHooks._default.get( this );\n\t},\n\trun: function( percent ) {\n\t\tvar eased,\n\t\t\thooks = Tween.propHooks[ this.prop ];\n\n\t\tif ( this.options.duration ) {\n\t\t\tthis.pos = eased = jQuery.easing[ this.easing ](\n\t\t\t\tpercent, this.options.duration * percent, 0, 1, this.options.duration\n\t\t\t);\n\t\t} else {\n\t\t\tthis.pos = eased = percent;\n\t\t}\n\t\tthis.now = ( this.end - this.start ) * eased + this.start;\n\n\t\tif ( this.options.step ) {\n\t\t\tthis.options.step.call( this.elem, this.now, this );\n\t\t}\n\n\t\tif ( hooks && hooks.set ) {\n\t\t\thooks.set( this );\n\t\t} else {\n\t\t\tTween.propHooks._default.set( this );\n\t\t}\n\t\treturn this;\n\t}\n};\n\nTween.prototype.init.prototype = Tween.prototype;\n\nTween.propHooks = {\n\t_default: {\n\t\tget: function( tween ) {\n\t\t\tvar result;\n\n\t\t\t// Use a property on the element directly when it is not a DOM element,\n\t\t\t// or when there is no matching style property that exists.\n\t\t\tif ( tween.elem.nodeType !== 1 ||\n\t\t\t\ttween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {\n\t\t\t\treturn tween.elem[ tween.prop ];\n\t\t\t}\n\n\t\t\t// Passing an empty string as a 3rd parameter to .css will automatically\n\t\t\t// attempt a parseFloat and fallback to a string if the parse fails.\n\t\t\t// Simple values such as \"10px\" are parsed to Float;\n\t\t\t// complex values such as \"rotate(1rad)\" are returned as-is.\n\t\t\tresult = jQuery.css( tween.elem, tween.prop, \"\" );\n\n\t\t\t// Empty strings, null, undefined and \"auto\" are converted to 0.\n\t\t\treturn !result || result === \"auto\" ? 0 : result;\n\t\t},\n\t\tset: function( tween ) {\n\n\t\t\t// Use step hook for back compat.\n\t\t\t// Use cssHook if its there.\n\t\t\t// Use .style if available and use plain properties where available.\n\t\t\tif ( jQuery.fx.step[ tween.prop ] ) {\n\t\t\t\tjQuery.fx.step[ tween.prop ]( tween );\n\t\t\t} else if ( tween.elem.nodeType === 1 &&\n\t\t\t\t( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null ||\n\t\t\t\t\tjQuery.cssHooks[ tween.prop ] ) ) {\n\t\t\t\tjQuery.style( tween.elem, tween.prop, tween.now + tween.unit );\n\t\t\t} else {\n\t\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t\t}\n\t\t}\n\t}\n};\n\n// Support: IE <=9 only\n// Panic based approach to setting things on disconnected nodes\nTween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {\n\tset: function( tween ) {\n\t\tif ( tween.elem.nodeType && tween.elem.parentNode ) {\n\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t}\n\t}\n};\n\njQuery.easing = {\n\tlinear: function( p ) {\n\t\treturn p;\n\t},\n\tswing: function( p ) {\n\t\treturn 0.5 - Math.cos( p * Math.PI ) / 2;\n\t},\n\t_default: \"swing\"\n};\n\njQuery.fx = Tween.prototype.init;\n\n// Back compat <1.8 extension point\njQuery.fx.step = {};\n\n\n\n\nvar\n\tfxNow, inProgress,\n\trfxtypes = /^(?:toggle|show|hide)$/,\n\trrun = /queueHooks$/;\n\nfunction schedule() {\n\tif ( inProgress ) {\n\t\tif ( document.hidden === false && window.requestAnimationFrame ) {\n\t\t\twindow.requestAnimationFrame( schedule );\n\t\t} else {\n\t\t\twindow.setTimeout( schedule, jQuery.fx.interval );\n\t\t}\n\n\t\tjQuery.fx.tick();\n\t}\n}\n\n// Animations created synchronously will run synchronously\nfunction createFxNow() {\n\twindow.setTimeout( function() {\n\t\tfxNow = undefined;\n\t} );\n\treturn ( fxNow = jQuery.now() );\n}\n\n// Generate parameters to create a standard animation\nfunction genFx( type, includeWidth ) {\n\tvar which,\n\t\ti = 0,\n\t\tattrs = { height: type };\n\n\t// If we include width, step value is 1 to do all cssExpand values,\n\t// otherwise step value is 2 to skip over Left and Right\n\tincludeWidth = includeWidth ? 1 : 0;\n\tfor ( ; i < 4; i += 2 - includeWidth ) {\n\t\twhich = cssExpand[ i ];\n\t\tattrs[ \"margin\" + which ] = attrs[ \"padding\" + which ] = type;\n\t}\n\n\tif ( includeWidth ) {\n\t\tattrs.opacity = attrs.width = type;\n\t}\n\n\treturn attrs;\n}\n\nfunction createTween( value, prop, animation ) {\n\tvar tween,\n\t\tcollection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ \"*\" ] ),\n\t\tindex = 0,\n\t\tlength = collection.length;\n\tfor ( ; index < length; index++ ) {\n\t\tif ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {\n\n\t\t\t// We're done with this property\n\t\t\treturn tween;\n\t\t}\n\t}\n}\n\nfunction defaultPrefilter( elem, props, opts ) {\n\tvar prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display,\n\t\tisBox = \"width\" in props || \"height\" in props,\n\t\tanim = this,\n\t\torig = {},\n\t\tstyle = elem.style,\n\t\thidden = elem.nodeType && isHiddenWithinTree( elem ),\n\t\tdataShow = dataPriv.get( elem, \"fxshow\" );\n\n\t// Queue-skipping animations hijack the fx hooks\n\tif ( !opts.queue ) {\n\t\thooks = jQuery._queueHooks( elem, \"fx\" );\n\t\tif ( hooks.unqueued == null ) {\n\t\t\thooks.unqueued = 0;\n\t\t\toldfire = hooks.empty.fire;\n\t\t\thooks.empty.fire = function() {\n\t\t\t\tif ( !hooks.unqueued ) {\n\t\t\t\t\toldfire();\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\thooks.unqueued++;\n\n\t\tanim.always( function() {\n\n\t\t\t// Ensure the complete handler is called before this completes\n\t\t\tanim.always( function() {\n\t\t\t\thooks.unqueued--;\n\t\t\t\tif ( !jQuery.queue( elem, \"fx\" ).length ) {\n\t\t\t\t\thooks.empty.fire();\n\t\t\t\t}\n\t\t\t} );\n\t\t} );\n\t}\n\n\t// Detect show/hide animations\n\tfor ( prop in props ) {\n\t\tvalue = props[ prop ];\n\t\tif ( rfxtypes.test( value ) ) {\n\t\t\tdelete props[ prop ];\n\t\t\ttoggle = toggle || value === \"toggle\";\n\t\t\tif ( value === ( hidden ? \"hide\" : \"show\" ) ) {\n\n\t\t\t\t// Pretend to be hidden if this is a \"show\" and\n\t\t\t\t// there is still data from a stopped show/hide\n\t\t\t\tif ( value === \"show\" && dataShow && dataShow[ prop ] !== undefined ) {\n\t\t\t\t\thidden = true;\n\n\t\t\t\t// Ignore all other no-op show/hide data\n\t\t\t\t} else {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\torig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );\n\t\t}\n\t}\n\n\t// Bail out if this is a no-op like .hide().hide()\n\tpropTween = !jQuery.isEmptyObject( props );\n\tif ( !propTween && jQuery.isEmptyObject( orig ) ) {\n\t\treturn;\n\t}\n\n\t// Restrict \"overflow\" and \"display\" styles during box animations\n\tif ( isBox && elem.nodeType === 1 ) {\n\n\t\t// Support: IE <=9 - 11, Edge 12 - 13\n\t\t// Record all 3 overflow attributes because IE does not infer the shorthand\n\t\t// from identically-valued overflowX and overflowY\n\t\topts.overflow = [ style.overflow, style.overflowX, style.overflowY ];\n\n\t\t// Identify a display type, preferring old show/hide data over the CSS cascade\n\t\trestoreDisplay = dataShow && dataShow.display;\n\t\tif ( restoreDisplay == null ) {\n\t\t\trestoreDisplay = dataPriv.get( elem, \"display\" );\n\t\t}\n\t\tdisplay = jQuery.css( elem, \"display\" );\n\t\tif ( display === \"none\" ) {\n\t\t\tif ( restoreDisplay ) {\n\t\t\t\tdisplay = restoreDisplay;\n\t\t\t} else {\n\n\t\t\t\t// Get nonempty value(s) by temporarily forcing visibility\n\t\t\t\tshowHide( [ elem ], true );\n\t\t\t\trestoreDisplay = elem.style.display || restoreDisplay;\n\t\t\t\tdisplay = jQuery.css( elem, \"display\" );\n\t\t\t\tshowHide( [ elem ] );\n\t\t\t}\n\t\t}\n\n\t\t// Animate inline elements as inline-block\n\t\tif ( display === \"inline\" || display === \"inline-block\" && restoreDisplay != null ) {\n\t\t\tif ( jQuery.css( elem, \"float\" ) === \"none\" ) {\n\n\t\t\t\t// Restore the original display value at the end of pure show/hide animations\n\t\t\t\tif ( !propTween ) {\n\t\t\t\t\tanim.done( function() {\n\t\t\t\t\t\tstyle.display = restoreDisplay;\n\t\t\t\t\t} );\n\t\t\t\t\tif ( restoreDisplay == null ) {\n\t\t\t\t\t\tdisplay = style.display;\n\t\t\t\t\t\trestoreDisplay = display === \"none\" ? \"\" : display;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstyle.display = \"inline-block\";\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( opts.overflow ) {\n\t\tstyle.overflow = \"hidden\";\n\t\tanim.always( function() {\n\t\t\tstyle.overflow = opts.overflow[ 0 ];\n\t\t\tstyle.overflowX = opts.overflow[ 1 ];\n\t\t\tstyle.overflowY = opts.overflow[ 2 ];\n\t\t} );\n\t}\n\n\t// Implement show/hide animations\n\tpropTween = false;\n\tfor ( prop in orig ) {\n\n\t\t// General show/hide setup for this element animation\n\t\tif ( !propTween ) {\n\t\t\tif ( dataShow ) {\n\t\t\t\tif ( \"hidden\" in dataShow ) {\n\t\t\t\t\thidden = dataShow.hidden;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdataShow = dataPriv.access( elem, \"fxshow\", { display: restoreDisplay } );\n\t\t\t}\n\n\t\t\t// Store hidden/visible for toggle so `.stop().toggle()` \"reverses\"\n\t\t\tif ( toggle ) {\n\t\t\t\tdataShow.hidden = !hidden;\n\t\t\t}\n\n\t\t\t// Show elements before animating them\n\t\t\tif ( hidden ) {\n\t\t\t\tshowHide( [ elem ], true );\n\t\t\t}\n\n\t\t\t/* eslint-disable no-loop-func */\n\n\t\t\tanim.done( function() {\n\n\t\t\t/* eslint-enable no-loop-func */\n\n\t\t\t\t// The final step of a \"hide\" animation is actually hiding the element\n\t\t\t\tif ( !hidden ) {\n\t\t\t\t\tshowHide( [ elem ] );\n\t\t\t\t}\n\t\t\t\tdataPriv.remove( elem, \"fxshow\" );\n\t\t\t\tfor ( prop in orig ) {\n\t\t\t\t\tjQuery.style( elem, prop, orig[ prop ] );\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\t// Per-property setup\n\t\tpropTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );\n\t\tif ( !( prop in dataShow ) ) {\n\t\t\tdataShow[ prop ] = propTween.start;\n\t\t\tif ( hidden ) {\n\t\t\t\tpropTween.end = propTween.start;\n\t\t\t\tpropTween.start = 0;\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction propFilter( props, specialEasing ) {\n\tvar index, name, easing, value, hooks;\n\n\t// camelCase, specialEasing and expand cssHook pass\n\tfor ( index in props ) {\n\t\tname = jQuery.camelCase( index );\n\t\teasing = specialEasing[ name ];\n\t\tvalue = props[ index ];\n\t\tif ( Array.isArray( value ) ) {\n\t\t\teasing = value[ 1 ];\n\t\t\tvalue = props[ index ] = value[ 0 ];\n\t\t}\n\n\t\tif ( index !== name ) {\n\t\t\tprops[ name ] = value;\n\t\t\tdelete props[ index ];\n\t\t}\n\n\t\thooks = jQuery.cssHooks[ name ];\n\t\tif ( hooks && \"expand\" in hooks ) {\n\t\t\tvalue = hooks.expand( value );\n\t\t\tdelete props[ name ];\n\n\t\t\t// Not quite $.extend, this won't overwrite existing keys.\n\t\t\t// Reusing 'index' because we have the correct \"name\"\n\t\t\tfor ( index in value ) {\n\t\t\t\tif ( !( index in props ) ) {\n\t\t\t\t\tprops[ index ] = value[ index ];\n\t\t\t\t\tspecialEasing[ index ] = easing;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tspecialEasing[ name ] = easing;\n\t\t}\n\t}\n}\n\nfunction Animation( elem, properties, options ) {\n\tvar result,\n\t\tstopped,\n\t\tindex = 0,\n\t\tlength = Animation.prefilters.length,\n\t\tdeferred = jQuery.Deferred().always( function() {\n\n\t\t\t// Don't match elem in the :animated selector\n\t\t\tdelete tick.elem;\n\t\t} ),\n\t\ttick = function() {\n\t\t\tif ( stopped ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tvar currentTime = fxNow || createFxNow(),\n\t\t\t\tremaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),\n\n\t\t\t\t// Support: Android 2.3 only\n\t\t\t\t// Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)\n\t\t\t\ttemp = remaining / animation.duration || 0,\n\t\t\t\tpercent = 1 - temp,\n\t\t\t\tindex = 0,\n\t\t\t\tlength = animation.tweens.length;\n\n\t\t\tfor ( ; index < length; index++ ) {\n\t\t\t\tanimation.tweens[ index ].run( percent );\n\t\t\t}\n\n\t\t\tdeferred.notifyWith( elem, [ animation, percent, remaining ] );\n\n\t\t\t// If there's more to do, yield\n\t\t\tif ( percent < 1 && length ) {\n\t\t\t\treturn remaining;\n\t\t\t}\n\n\t\t\t// If this was an empty animation, synthesize a final progress notification\n\t\t\tif ( !length ) {\n\t\t\t\tdeferred.notifyWith( elem, [ animation, 1, 0 ] );\n\t\t\t}\n\n\t\t\t// Resolve the animation and report its conclusion\n\t\t\tdeferred.resolveWith( elem, [ animation ] );\n\t\t\treturn false;\n\t\t},\n\t\tanimation = deferred.promise( {\n\t\t\telem: elem,\n\t\t\tprops: jQuery.extend( {}, properties ),\n\t\t\topts: jQuery.extend( true, {\n\t\t\t\tspecialEasing: {},\n\t\t\t\teasing: jQuery.easing._default\n\t\t\t}, options ),\n\t\t\toriginalProperties: properties,\n\t\t\toriginalOptions: options,\n\t\t\tstartTime: fxNow || createFxNow(),\n\t\t\tduration: options.duration,\n\t\t\ttweens: [],\n\t\t\tcreateTween: function( prop, end ) {\n\t\t\t\tvar tween = jQuery.Tween( elem, animation.opts, prop, end,\n\t\t\t\t\t\tanimation.opts.specialEasing[ prop ] || animation.opts.easing );\n\t\t\t\tanimation.tweens.push( tween );\n\t\t\t\treturn tween;\n\t\t\t},\n\t\t\tstop: function( gotoEnd ) {\n\t\t\t\tvar index = 0,\n\n\t\t\t\t\t// If we are going to the end, we want to run all the tweens\n\t\t\t\t\t// otherwise we skip this part\n\t\t\t\t\tlength = gotoEnd ? animation.tweens.length : 0;\n\t\t\t\tif ( stopped ) {\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t\tstopped = true;\n\t\t\t\tfor ( ; index < length; index++ ) {\n\t\t\t\t\tanimation.tweens[ index ].run( 1 );\n\t\t\t\t}\n\n\t\t\t\t// Resolve when we played the last frame; otherwise, reject\n\t\t\t\tif ( gotoEnd ) {\n\t\t\t\t\tdeferred.notifyWith( elem, [ animation, 1, 0 ] );\n\t\t\t\t\tdeferred.resolveWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t} else {\n\t\t\t\t\tdeferred.rejectWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t}\n\t\t} ),\n\t\tprops = animation.props;\n\n\tpropFilter( props, animation.opts.specialEasing );\n\n\tfor ( ; index < length; index++ ) {\n\t\tresult = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );\n\t\tif ( result ) {\n\t\t\tif ( jQuery.isFunction( result.stop ) ) {\n\t\t\t\tjQuery._queueHooks( animation.elem, animation.opts.queue ).stop =\n\t\t\t\t\tjQuery.proxy( result.stop, result );\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tjQuery.map( props, createTween, animation );\n\n\tif ( jQuery.isFunction( animation.opts.start ) ) {\n\t\tanimation.opts.start.call( elem, animation );\n\t}\n\n\t// Attach callbacks from options\n\tanimation\n\t\t.progress( animation.opts.progress )\n\t\t.done( animation.opts.done, animation.opts.complete )\n\t\t.fail( animation.opts.fail )\n\t\t.always( animation.opts.always );\n\n\tjQuery.fx.timer(\n\t\tjQuery.extend( tick, {\n\t\t\telem: elem,\n\t\t\tanim: animation,\n\t\t\tqueue: animation.opts.queue\n\t\t} )\n\t);\n\n\treturn animation;\n}\n\njQuery.Animation = jQuery.extend( Animation, {\n\n\ttweeners: {\n\t\t\"*\": [ function( prop, value ) {\n\t\t\tvar tween = this.createTween( prop, value );\n\t\t\tadjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );\n\t\t\treturn tween;\n\t\t} ]\n\t},\n\n\ttweener: function( props, callback ) {\n\t\tif ( jQuery.isFunction( props ) ) {\n\t\t\tcallback = props;\n\t\t\tprops = [ \"*\" ];\n\t\t} else {\n\t\t\tprops = props.match( rnothtmlwhite );\n\t\t}\n\n\t\tvar prop,\n\t\t\tindex = 0,\n\t\t\tlength = props.length;\n\n\t\tfor ( ; index < length; index++ ) {\n\t\t\tprop = props[ index ];\n\t\t\tAnimation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];\n\t\t\tAnimation.tweeners[ prop ].unshift( callback );\n\t\t}\n\t},\n\n\tprefilters: [ defaultPrefilter ],\n\n\tprefilter: function( callback, prepend ) {\n\t\tif ( prepend ) {\n\t\t\tAnimation.prefilters.unshift( callback );\n\t\t} else {\n\t\t\tAnimation.prefilters.push( callback );\n\t\t}\n\t}\n} );\n\njQuery.speed = function( speed, easing, fn ) {\n\tvar opt = speed && typeof speed === \"object\" ? jQuery.extend( {}, speed ) : {\n\t\tcomplete: fn || !fn && easing ||\n\t\t\tjQuery.isFunction( speed ) && speed,\n\t\tduration: speed,\n\t\teasing: fn && easing || easing && !jQuery.isFunction( easing ) && easing\n\t};\n\n\t// Go to the end state if fx are off\n\tif ( jQuery.fx.off ) {\n\t\topt.duration = 0;\n\n\t} else {\n\t\tif ( typeof opt.duration !== \"number\" ) {\n\t\t\tif ( opt.duration in jQuery.fx.speeds ) {\n\t\t\t\topt.duration = jQuery.fx.speeds[ opt.duration ];\n\n\t\t\t} else {\n\t\t\t\topt.duration = jQuery.fx.speeds._default;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Normalize opt.queue - true/undefined/null -> \"fx\"\n\tif ( opt.queue == null || opt.queue === true ) {\n\t\topt.queue = \"fx\";\n\t}\n\n\t// Queueing\n\topt.old = opt.complete;\n\n\topt.complete = function() {\n\t\tif ( jQuery.isFunction( opt.old ) ) {\n\t\t\topt.old.call( this );\n\t\t}\n\n\t\tif ( opt.queue ) {\n\t\t\tjQuery.dequeue( this, opt.queue );\n\t\t}\n\t};\n\n\treturn opt;\n};\n\njQuery.fn.extend( {\n\tfadeTo: function( speed, to, easing, callback ) {\n\n\t\t// Show any hidden elements after setting opacity to 0\n\t\treturn this.filter( isHiddenWithinTree ).css( \"opacity\", 0 ).show()\n\n\t\t\t// Animate to the value specified\n\t\t\t.end().animate( { opacity: to }, speed, easing, callback );\n\t},\n\tanimate: function( prop, speed, easing, callback ) {\n\t\tvar empty = jQuery.isEmptyObject( prop ),\n\t\t\toptall = jQuery.speed( speed, easing, callback ),\n\t\t\tdoAnimation = function() {\n\n\t\t\t\t// Operate on a copy of prop so per-property easing won't be lost\n\t\t\t\tvar anim = Animation( this, jQuery.extend( {}, prop ), optall );\n\n\t\t\t\t// Empty animations, or finishing resolves immediately\n\t\t\t\tif ( empty || dataPriv.get( this, \"finish\" ) ) {\n\t\t\t\t\tanim.stop( true );\n\t\t\t\t}\n\t\t\t};\n\t\t\tdoAnimation.finish = doAnimation;\n\n\t\treturn empty || optall.queue === false ?\n\t\t\tthis.each( doAnimation ) :\n\t\t\tthis.queue( optall.queue, doAnimation );\n\t},\n\tstop: function( type, clearQueue, gotoEnd ) {\n\t\tvar stopQueue = function( hooks ) {\n\t\t\tvar stop = hooks.stop;\n\t\t\tdelete hooks.stop;\n\t\t\tstop( gotoEnd );\n\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tgotoEnd = clearQueue;\n\t\t\tclearQueue = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\tif ( clearQueue && type !== false ) {\n\t\t\tthis.queue( type || \"fx\", [] );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar dequeue = true,\n\t\t\t\tindex = type != null && type + \"queueHooks\",\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tdata = dataPriv.get( this );\n\n\t\t\tif ( index ) {\n\t\t\t\tif ( data[ index ] && data[ index ].stop ) {\n\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( index in data ) {\n\t\t\t\t\tif ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {\n\t\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this &&\n\t\t\t\t\t( type == null || timers[ index ].queue === type ) ) {\n\n\t\t\t\t\ttimers[ index ].anim.stop( gotoEnd );\n\t\t\t\t\tdequeue = false;\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Start the next in the queue if the last step wasn't forced.\n\t\t\t// Timers currently will call their complete callbacks, which\n\t\t\t// will dequeue but only if they were gotoEnd.\n\t\t\tif ( dequeue || !gotoEnd ) {\n\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t}\n\t\t} );\n\t},\n\tfinish: function( type ) {\n\t\tif ( type !== false ) {\n\t\t\ttype = type || \"fx\";\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tvar index,\n\t\t\t\tdata = dataPriv.get( this ),\n\t\t\t\tqueue = data[ type + \"queue\" ],\n\t\t\t\thooks = data[ type + \"queueHooks\" ],\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tlength = queue ? queue.length : 0;\n\n\t\t\t// Enable finishing flag on private data\n\t\t\tdata.finish = true;\n\n\t\t\t// Empty the queue first\n\t\t\tjQuery.queue( this, type, [] );\n\n\t\t\tif ( hooks && hooks.stop ) {\n\t\t\t\thooks.stop.call( this, true );\n\t\t\t}\n\n\t\t\t// Look for any active animations, and finish them\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this && timers[ index ].queue === type ) {\n\t\t\t\t\ttimers[ index ].anim.stop( true );\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Look for any animations in the old queue and finish them\n\t\t\tfor ( index = 0; index < length; index++ ) {\n\t\t\t\tif ( queue[ index ] && queue[ index ].finish ) {\n\t\t\t\t\tqueue[ index ].finish.call( this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Turn off finishing flag\n\t\t\tdelete data.finish;\n\t\t} );\n\t}\n} );\n\njQuery.each( [ \"toggle\", \"show\", \"hide\" ], function( i, name ) {\n\tvar cssFn = jQuery.fn[ name ];\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn speed == null || typeof speed === \"boolean\" ?\n\t\t\tcssFn.apply( this, arguments ) :\n\t\t\tthis.animate( genFx( name, true ), speed, easing, callback );\n\t};\n} );\n\n// Generate shortcuts for custom animations\njQuery.each( {\n\tslideDown: genFx( \"show\" ),\n\tslideUp: genFx( \"hide\" ),\n\tslideToggle: genFx( \"toggle\" ),\n\tfadeIn: { opacity: \"show\" },\n\tfadeOut: { opacity: \"hide\" },\n\tfadeToggle: { opacity: \"toggle\" }\n}, function( name, props ) {\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn this.animate( props, speed, easing, callback );\n\t};\n} );\n\njQuery.timers = [];\njQuery.fx.tick = function() {\n\tvar timer,\n\t\ti = 0,\n\t\ttimers = jQuery.timers;\n\n\tfxNow = jQuery.now();\n\n\tfor ( ; i < timers.length; i++ ) {\n\t\ttimer = timers[ i ];\n\n\t\t// Run the timer and safely remove it when done (allowing for external removal)\n\t\tif ( !timer() && timers[ i ] === timer ) {\n\t\t\ttimers.splice( i--, 1 );\n\t\t}\n\t}\n\n\tif ( !timers.length ) {\n\t\tjQuery.fx.stop();\n\t}\n\tfxNow = undefined;\n};\n\njQuery.fx.timer = function( timer ) {\n\tjQuery.timers.push( timer );\n\tjQuery.fx.start();\n};\n\njQuery.fx.interval = 13;\njQuery.fx.start = function() {\n\tif ( inProgress ) {\n\t\treturn;\n\t}\n\n\tinProgress = true;\n\tschedule();\n};\n\njQuery.fx.stop = function() {\n\tinProgress = null;\n};\n\njQuery.fx.speeds = {\n\tslow: 600,\n\tfast: 200,\n\n\t// Default speed\n\t_default: 400\n};\n\n\n// Based off of the plugin by Clint Helfers, with permission.\n// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/\njQuery.fn.delay = function( time, type ) {\n\ttime = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;\n\ttype = type || \"fx\";\n\n\treturn this.queue( type, function( next, hooks ) {\n\t\tvar timeout = window.setTimeout( next, time );\n\t\thooks.stop = function() {\n\t\t\twindow.clearTimeout( timeout );\n\t\t};\n\t} );\n};\n\n\n( function() {\n\tvar input = document.createElement( \"input\" ),\n\t\tselect = document.createElement( \"select\" ),\n\t\topt = select.appendChild( document.createElement( \"option\" ) );\n\n\tinput.type = \"checkbox\";\n\n\t// Support: Android <=4.3 only\n\t// Default value for a checkbox should be \"on\"\n\tsupport.checkOn = input.value !== \"\";\n\n\t// Support: IE <=11 only\n\t// Must access selectedIndex to make default options select\n\tsupport.optSelected = opt.selected;\n\n\t// Support: IE <=11 only\n\t// An input loses its value after becoming a radio\n\tinput = document.createElement( \"input\" );\n\tinput.value = \"t\";\n\tinput.type = \"radio\";\n\tsupport.radioValue = input.value === \"t\";\n} )();\n\n\nvar boolHook,\n\tattrHandle = jQuery.expr.attrHandle;\n\njQuery.fn.extend( {\n\tattr: function( name, value ) {\n\t\treturn access( this, jQuery.attr, name, value, arguments.length > 1 );\n\t},\n\n\tremoveAttr: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.removeAttr( this, name );\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tattr: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set attributes on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Fallback to prop when attributes are not supported\n\t\tif ( typeof elem.getAttribute === \"undefined\" ) {\n\t\t\treturn jQuery.prop( elem, name, value );\n\t\t}\n\n\t\t// Attribute hooks are determined by the lowercase version\n\t\t// Grab necessary hook if one is defined\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\t\t\thooks = jQuery.attrHooks[ name.toLowerCase() ] ||\n\t\t\t\t( jQuery.expr.match.bool.test( name ) ? boolHook : undefined );\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( value === null ) {\n\t\t\t\tjQuery.removeAttr( elem, name );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\telem.setAttribute( name, value + \"\" );\n\t\t\treturn value;\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\tret = jQuery.find.attr( elem, name );\n\n\t\t// Non-existent attributes return null, we normalize to undefined\n\t\treturn ret == null ? undefined : ret;\n\t},\n\n\tattrHooks: {\n\t\ttype: {\n\t\t\tset: function( elem, value ) {\n\t\t\t\tif ( !support.radioValue && value === \"radio\" &&\n\t\t\t\t\tnodeName( elem, \"input\" ) ) {\n\t\t\t\t\tvar val = elem.value;\n\t\t\t\t\telem.setAttribute( \"type\", value );\n\t\t\t\t\tif ( val ) {\n\t\t\t\t\t\telem.value = val;\n\t\t\t\t\t}\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\tremoveAttr: function( elem, value ) {\n\t\tvar name,\n\t\t\ti = 0,\n\n\t\t\t// Attribute names can contain non-HTML whitespace characters\n\t\t\t// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2\n\t\t\tattrNames = value && value.match( rnothtmlwhite );\n\n\t\tif ( attrNames && elem.nodeType === 1 ) {\n\t\t\twhile ( ( name = attrNames[ i++ ] ) ) {\n\t\t\t\telem.removeAttribute( name );\n\t\t\t}\n\t\t}\n\t}\n} );\n\n// Hooks for boolean attributes\nboolHook = {\n\tset: function( elem, value, name ) {\n\t\tif ( value === false ) {\n\n\t\t\t// Remove boolean attributes when set to false\n\t\t\tjQuery.removeAttr( elem, name );\n\t\t} else {\n\t\t\telem.setAttribute( name, name );\n\t\t}\n\t\treturn name;\n\t}\n};\n\njQuery.each( jQuery.expr.match.bool.source.match( /\\w+/g ), function( i, name ) {\n\tvar getter = attrHandle[ name ] || jQuery.find.attr;\n\n\tattrHandle[ name ] = function( elem, name, isXML ) {\n\t\tvar ret, handle,\n\t\t\tlowercaseName = name.toLowerCase();\n\n\t\tif ( !isXML ) {\n\n\t\t\t// Avoid an infinite loop by temporarily removing this function from the getter\n\t\t\thandle = attrHandle[ lowercaseName ];\n\t\t\tattrHandle[ lowercaseName ] = ret;\n\t\t\tret = getter( elem, name, isXML ) != null ?\n\t\t\t\tlowercaseName :\n\t\t\t\tnull;\n\t\t\tattrHandle[ lowercaseName ] = handle;\n\t\t}\n\t\treturn ret;\n\t};\n} );\n\n\n\n\nvar rfocusable = /^(?:input|select|textarea|button)$/i,\n\trclickable = /^(?:a|area)$/i;\n\njQuery.fn.extend( {\n\tprop: function( name, value ) {\n\t\treturn access( this, jQuery.prop, name, value, arguments.length > 1 );\n\t},\n\n\tremoveProp: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tdelete this[ jQuery.propFix[ name ] || name ];\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tprop: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set properties on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// Fix name and attach hooks\n\t\t\tname = jQuery.propFix[ name ] || name;\n\t\t\thooks = jQuery.propHooks[ name ];\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\treturn ( elem[ name ] = value );\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\treturn elem[ name ];\n\t},\n\n\tpropHooks: {\n\t\ttabIndex: {\n\t\t\tget: function( elem ) {\n\n\t\t\t\t// Support: IE <=9 - 11 only\n\t\t\t\t// elem.tabIndex doesn't always return the\n\t\t\t\t// correct value when it hasn't been explicitly set\n\t\t\t\t// https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/\n\t\t\t\t// Use proper attribute retrieval(#12072)\n\t\t\t\tvar tabindex = jQuery.find.attr( elem, \"tabindex\" );\n\n\t\t\t\tif ( tabindex ) {\n\t\t\t\t\treturn parseInt( tabindex, 10 );\n\t\t\t\t}\n\n\t\t\t\tif (\n\t\t\t\t\trfocusable.test( elem.nodeName ) ||\n\t\t\t\t\trclickable.test( elem.nodeName ) &&\n\t\t\t\t\telem.href\n\t\t\t\t) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t},\n\n\tpropFix: {\n\t\t\"for\": \"htmlFor\",\n\t\t\"class\": \"className\"\n\t}\n} );\n\n// Support: IE <=11 only\n// Accessing the selectedIndex property\n// forces the browser to respect setting selected\n// on the option\n// The getter ensures a default option is selected\n// when in an optgroup\n// eslint rule \"no-unused-expressions\" is disabled for this code\n// since it considers such accessions noop\nif ( !support.optSelected ) {\n\tjQuery.propHooks.selected = {\n\t\tget: function( elem ) {\n\n\t\t\t/* eslint no-unused-expressions: \"off\" */\n\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent && parent.parentNode ) {\n\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t\tset: function( elem ) {\n\n\t\t\t/* eslint no-unused-expressions: \"off\" */\n\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent ) {\n\t\t\t\tparent.selectedIndex;\n\n\t\t\t\tif ( parent.parentNode ) {\n\t\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\njQuery.each( [\n\t\"tabIndex\",\n\t\"readOnly\",\n\t\"maxLength\",\n\t\"cellSpacing\",\n\t\"cellPadding\",\n\t\"rowSpan\",\n\t\"colSpan\",\n\t\"useMap\",\n\t\"frameBorder\",\n\t\"contentEditable\"\n], function() {\n\tjQuery.propFix[ this.toLowerCase() ] = this;\n} );\n\n\n\n\n\t// Strip and collapse whitespace according to HTML spec\n\t// https://html.spec.whatwg.org/multipage/infrastructure.html#strip-and-collapse-whitespace\n\tfunction stripAndCollapse( value ) {\n\t\tvar tokens = value.match( rnothtmlwhite ) || [];\n\t\treturn tokens.join( \" \" );\n\t}\n\n\nfunction getClass( elem ) {\n\treturn elem.getAttribute && elem.getAttribute( \"class\" ) || \"\";\n}\n\njQuery.fn.extend( {\n\taddClass: function( value ) {\n\t\tvar classes, elem, cur, curValue, clazz, j, finalValue,\n\t\t\ti = 0;\n\n\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\treturn this.each( function( j ) {\n\t\t\t\tjQuery( this ).addClass( value.call( this, j, getClass( this ) ) );\n\t\t\t} );\n\t\t}\n\n\t\tif ( typeof value === \"string\" && value ) {\n\t\t\tclasses = value.match( rnothtmlwhite ) || [];\n\n\t\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\t\tcurValue = getClass( elem );\n\t\t\t\tcur = elem.nodeType === 1 && ( \" \" + stripAndCollapse( curValue ) + \" \" );\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( ( clazz = classes[ j++ ] ) ) {\n\t\t\t\t\t\tif ( cur.indexOf( \" \" + clazz + \" \" ) < 0 ) {\n\t\t\t\t\t\t\tcur += clazz + \" \";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = stripAndCollapse( cur );\n\t\t\t\t\tif ( curValue !== finalValue ) {\n\t\t\t\t\t\telem.setAttribute( \"class\", finalValue );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tremoveClass: function( value ) {\n\t\tvar classes, elem, cur, curValue, clazz, j, finalValue,\n\t\t\ti = 0;\n\n\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\treturn this.each( function( j ) {\n\t\t\t\tjQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );\n\t\t\t} );\n\t\t}\n\n\t\tif ( !arguments.length ) {\n\t\t\treturn this.attr( \"class\", \"\" );\n\t\t}\n\n\t\tif ( typeof value === \"string\" && value ) {\n\t\t\tclasses = value.match( rnothtmlwhite ) || [];\n\n\t\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\t\tcurValue = getClass( elem );\n\n\t\t\t\t// This expression is here for better compressibility (see addClass)\n\t\t\t\tcur = elem.nodeType === 1 && ( \" \" + stripAndCollapse( curValue ) + \" \" );\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( ( clazz = classes[ j++ ] ) ) {\n\n\t\t\t\t\t\t// Remove *all* instances\n\t\t\t\t\t\twhile ( cur.indexOf( \" \" + clazz + \" \" ) > -1 ) {\n\t\t\t\t\t\t\tcur = cur.replace( \" \" + clazz + \" \", \" \" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = stripAndCollapse( cur );\n\t\t\t\t\tif ( curValue !== finalValue ) {\n\t\t\t\t\t\telem.setAttribute( \"class\", finalValue );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\ttoggleClass: function( value, stateVal ) {\n\t\tvar type = typeof value;\n\n\t\tif ( typeof stateVal === \"boolean\" && type === \"string\" ) {\n\t\t\treturn stateVal ? this.addClass( value ) : this.removeClass( value );\n\t\t}\n\n\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\treturn this.each( function( i ) {\n\t\t\t\tjQuery( this ).toggleClass(\n\t\t\t\t\tvalue.call( this, i, getClass( this ), stateVal ),\n\t\t\t\t\tstateVal\n\t\t\t\t);\n\t\t\t} );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar className, i, self, classNames;\n\n\t\t\tif ( type === \"string\" ) {\n\n\t\t\t\t// Toggle individual class names\n\t\t\t\ti = 0;\n\t\t\t\tself = jQuery( this );\n\t\t\t\tclassNames = value.match( rnothtmlwhite ) || [];\n\n\t\t\t\twhile ( ( className = classNames[ i++ ] ) ) {\n\n\t\t\t\t\t// Check each className given, space separated list\n\t\t\t\t\tif ( self.hasClass( className ) ) {\n\t\t\t\t\t\tself.removeClass( className );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.addClass( className );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t// Toggle whole class name\n\t\t\t} else if ( value === undefined || type === \"boolean\" ) {\n\t\t\t\tclassName = getClass( this );\n\t\t\t\tif ( className ) {\n\n\t\t\t\t\t// Store className if set\n\t\t\t\t\tdataPriv.set( this, \"__className__\", className );\n\t\t\t\t}\n\n\t\t\t\t// If the element has a class name or if we're passed `false`,\n\t\t\t\t// then remove the whole classname (if there was one, the above saved it).\n\t\t\t\t// Otherwise bring back whatever was previously saved (if anything),\n\t\t\t\t// falling back to the empty string if nothing was stored.\n\t\t\t\tif ( this.setAttribute ) {\n\t\t\t\t\tthis.setAttribute( \"class\",\n\t\t\t\t\t\tclassName || value === false ?\n\t\t\t\t\t\t\"\" :\n\t\t\t\t\t\tdataPriv.get( this, \"__className__\" ) || \"\"\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t},\n\n\thasClass: function( selector ) {\n\t\tvar className, elem,\n\t\t\ti = 0;\n\n\t\tclassName = \" \" + selector + \" \";\n\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\tif ( elem.nodeType === 1 &&\n\t\t\t\t( \" \" + stripAndCollapse( getClass( elem ) ) + \" \" ).indexOf( className ) > -1 ) {\n\t\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n} );\n\n\n\n\nvar rreturn = /\\r/g;\n\njQuery.fn.extend( {\n\tval: function( value ) {\n\t\tvar hooks, ret, isFunction,\n\t\t\telem = this[ 0 ];\n\n\t\tif ( !arguments.length ) {\n\t\t\tif ( elem ) {\n\t\t\t\thooks = jQuery.valHooks[ elem.type ] ||\n\t\t\t\t\tjQuery.valHooks[ elem.nodeName.toLowerCase() ];\n\n\t\t\t\tif ( hooks &&\n\t\t\t\t\t\"get\" in hooks &&\n\t\t\t\t\t( ret = hooks.get( elem, \"value\" ) ) !== undefined\n\t\t\t\t) {\n\t\t\t\t\treturn ret;\n\t\t\t\t}\n\n\t\t\t\tret = elem.value;\n\n\t\t\t\t// Handle most common string cases\n\t\t\t\tif ( typeof ret === \"string\" ) {\n\t\t\t\t\treturn ret.replace( rreturn, \"\" );\n\t\t\t\t}\n\n\t\t\t\t// Handle cases where value is null/undef or number\n\t\t\t\treturn ret == null ? \"\" : ret;\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tisFunction = jQuery.isFunction( value );\n\n\t\treturn this.each( function( i ) {\n\t\t\tvar val;\n\n\t\t\tif ( this.nodeType !== 1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( isFunction ) {\n\t\t\t\tval = value.call( this, i, jQuery( this ).val() );\n\t\t\t} else {\n\t\t\t\tval = value;\n\t\t\t}\n\n\t\t\t// Treat null/undefined as \"\"; convert numbers to string\n\t\t\tif ( val == null ) {\n\t\t\t\tval = \"\";\n\n\t\t\t} else if ( typeof val === \"number\" ) {\n\t\t\t\tval += \"\";\n\n\t\t\t} else if ( Array.isArray( val ) ) {\n\t\t\t\tval = jQuery.map( val, function( value ) {\n\t\t\t\t\treturn value == null ? \"\" : value + \"\";\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\thooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];\n\n\t\t\t// If set returns undefined, fall back to normal setting\n\t\t\tif ( !hooks || !( \"set\" in hooks ) || hooks.set( this, val, \"value\" ) === undefined ) {\n\t\t\t\tthis.value = val;\n\t\t\t}\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tvalHooks: {\n\t\toption: {\n\t\t\tget: function( elem ) {\n\n\t\t\t\tvar val = jQuery.find.attr( elem, \"value\" );\n\t\t\t\treturn val != null ?\n\t\t\t\t\tval :\n\n\t\t\t\t\t// Support: IE <=10 - 11 only\n\t\t\t\t\t// option.text throws exceptions (#14686, #14858)\n\t\t\t\t\t// Strip and collapse whitespace\n\t\t\t\t\t// https://html.spec.whatwg.org/#strip-and-collapse-whitespace\n\t\t\t\t\tstripAndCollapse( jQuery.text( elem ) );\n\t\t\t}\n\t\t},\n\t\tselect: {\n\t\t\tget: function( elem ) {\n\t\t\t\tvar value, option, i,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tindex = elem.selectedIndex,\n\t\t\t\t\tone = elem.type === \"select-one\",\n\t\t\t\t\tvalues = one ? null : [],\n\t\t\t\t\tmax = one ? index + 1 : options.length;\n\n\t\t\t\tif ( index < 0 ) {\n\t\t\t\t\ti = max;\n\n\t\t\t\t} else {\n\t\t\t\t\ti = one ? index : 0;\n\t\t\t\t}\n\n\t\t\t\t// Loop through all the selected options\n\t\t\t\tfor ( ; i < max; i++ ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t// IE8-9 doesn't update selected after form reset (#2551)\n\t\t\t\t\tif ( ( option.selected || i === index ) &&\n\n\t\t\t\t\t\t\t// Don't return options that are disabled or in a disabled optgroup\n\t\t\t\t\t\t\t!option.disabled &&\n\t\t\t\t\t\t\t( !option.parentNode.disabled ||\n\t\t\t\t\t\t\t\t!nodeName( option.parentNode, \"optgroup\" ) ) ) {\n\n\t\t\t\t\t\t// Get the specific value for the option\n\t\t\t\t\t\tvalue = jQuery( option ).val();\n\n\t\t\t\t\t\t// We don't need an array for one selects\n\t\t\t\t\t\tif ( one ) {\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Multi-Selects return an array\n\t\t\t\t\t\tvalues.push( value );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn values;\n\t\t\t},\n\n\t\t\tset: function( elem, value ) {\n\t\t\t\tvar optionSet, option,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tvalues = jQuery.makeArray( value ),\n\t\t\t\t\ti = options.length;\n\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t/* eslint-disable no-cond-assign */\n\n\t\t\t\t\tif ( option.selected =\n\t\t\t\t\t\tjQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1\n\t\t\t\t\t) {\n\t\t\t\t\t\toptionSet = true;\n\t\t\t\t\t}\n\n\t\t\t\t\t/* eslint-enable no-cond-assign */\n\t\t\t\t}\n\n\t\t\t\t// Force browsers to behave consistently when non-matching value is set\n\t\t\t\tif ( !optionSet ) {\n\t\t\t\t\telem.selectedIndex = -1;\n\t\t\t\t}\n\t\t\t\treturn values;\n\t\t\t}\n\t\t}\n\t}\n} );\n\n// Radios and checkboxes getter/setter\njQuery.each( [ \"radio\", \"checkbox\" ], function() {\n\tjQuery.valHooks[ this ] = {\n\t\tset: function( elem, value ) {\n\t\t\tif ( Array.isArray( value ) ) {\n\t\t\t\treturn ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );\n\t\t\t}\n\t\t}\n\t};\n\tif ( !support.checkOn ) {\n\t\tjQuery.valHooks[ this ].get = function( elem ) {\n\t\t\treturn elem.getAttribute( \"value\" ) === null ? \"on\" : elem.value;\n\t\t};\n\t}\n} );\n\n\n\n\n// Return jQuery for attributes-only inclusion\n\n\nvar rfocusMorph = /^(?:focusinfocus|focusoutblur)$/;\n\njQuery.extend( jQuery.event, {\n\n\ttrigger: function( event, data, elem, onlyHandlers ) {\n\n\t\tvar i, cur, tmp, bubbleType, ontype, handle, special,\n\t\t\teventPath = [ elem || document ],\n\t\t\ttype = hasOwn.call( event, \"type\" ) ? event.type : event,\n\t\t\tnamespaces = hasOwn.call( event, \"namespace\" ) ? event.namespace.split( \".\" ) : [];\n\n\t\tcur = tmp = elem = elem || document;\n\n\t\t// Don't do events on text and comment nodes\n\t\tif ( elem.nodeType === 3 || elem.nodeType === 8 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// focus/blur morphs to focusin/out; ensure we're not firing them right now\n\t\tif ( rfocusMorph.test( type + jQuery.event.triggered ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( type.indexOf( \".\" ) > -1 ) {\n\n\t\t\t// Namespaced trigger; create a regexp to match event type in handle()\n\t\t\tnamespaces = type.split( \".\" );\n\t\t\ttype = namespaces.shift();\n\t\t\tnamespaces.sort();\n\t\t}\n\t\tontype = type.indexOf( \":\" ) < 0 && \"on\" + type;\n\n\t\t// Caller can pass in a jQuery.Event object, Object, or just an event type string\n\t\tevent = event[ jQuery.expando ] ?\n\t\t\tevent :\n\t\t\tnew jQuery.Event( type, typeof event === \"object\" && event );\n\n\t\t// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)\n\t\tevent.isTrigger = onlyHandlers ? 2 : 3;\n\t\tevent.namespace = namespaces.join( \".\" );\n\t\tevent.rnamespace = event.namespace ?\n\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join( \"\\\\.(?:.*\\\\.|)\" ) + \"(\\\\.|$)\" ) :\n\t\t\tnull;\n\n\t\t// Clean up the event in case it is being reused\n\t\tevent.result = undefined;\n\t\tif ( !event.target ) {\n\t\t\tevent.target = elem;\n\t\t}\n\n\t\t// Clone any incoming data and prepend the event, creating the handler arg list\n\t\tdata = data == null ?\n\t\t\t[ event ] :\n\t\t\tjQuery.makeArray( data, [ event ] );\n\n\t\t// Allow special events to draw outside the lines\n\t\tspecial = jQuery.event.special[ type ] || {};\n\t\tif ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine event propagation path in advance, per W3C events spec (#9951)\n\t\t// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)\n\t\tif ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {\n\n\t\t\tbubbleType = special.delegateType || type;\n\t\t\tif ( !rfocusMorph.test( bubbleType + type ) ) {\n\t\t\t\tcur = cur.parentNode;\n\t\t\t}\n\t\t\tfor ( ; cur; cur = cur.parentNode ) {\n\t\t\t\teventPath.push( cur );\n\t\t\t\ttmp = cur;\n\t\t\t}\n\n\t\t\t// Only add window if we got to document (e.g., not plain obj or detached DOM)\n\t\t\tif ( tmp === ( elem.ownerDocument || document ) ) {\n\t\t\t\teventPath.push( tmp.defaultView || tmp.parentWindow || window );\n\t\t\t}\n\t\t}\n\n\t\t// Fire handlers on the event path\n\t\ti = 0;\n\t\twhile ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {\n\n\t\t\tevent.type = i > 1 ?\n\t\t\t\tbubbleType :\n\t\t\t\tspecial.bindType || type;\n\n\t\t\t// jQuery handler\n\t\t\thandle = ( dataPriv.get( cur, \"events\" ) || {} )[ event.type ] &&\n\t\t\t\tdataPriv.get( cur, \"handle\" );\n\t\t\tif ( handle ) {\n\t\t\t\thandle.apply( cur, data );\n\t\t\t}\n\n\t\t\t// Native handler\n\t\t\thandle = ontype && cur[ ontype ];\n\t\t\tif ( handle && handle.apply && acceptData( cur ) ) {\n\t\t\t\tevent.result = handle.apply( cur, data );\n\t\t\t\tif ( event.result === false ) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tevent.type = type;\n\n\t\t// If nobody prevented the default action, do it now\n\t\tif ( !onlyHandlers && !event.isDefaultPrevented() ) {\n\n\t\t\tif ( ( !special._default ||\n\t\t\t\tspecial._default.apply( eventPath.pop(), data ) === false ) &&\n\t\t\t\tacceptData( elem ) ) {\n\n\t\t\t\t// Call a native DOM method on the target with the same name as the event.\n\t\t\t\t// Don't do default actions on window, that's where global variables be (#6170)\n\t\t\t\tif ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) {\n\n\t\t\t\t\t// Don't re-trigger an onFOO event when we call its FOO() method\n\t\t\t\t\ttmp = elem[ ontype ];\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = null;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prevent re-triggering of the same event, since we already bubbled it above\n\t\t\t\t\tjQuery.event.triggered = type;\n\t\t\t\t\telem[ type ]();\n\t\t\t\t\tjQuery.event.triggered = undefined;\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = tmp;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\t// Piggyback on a donor event to simulate a different one\n\t// Used only for `focus(in | out)` events\n\tsimulate: function( type, elem, event ) {\n\t\tvar e = jQuery.extend(\n\t\t\tnew jQuery.Event(),\n\t\t\tevent,\n\t\t\t{\n\t\t\t\ttype: type,\n\t\t\t\tisSimulated: true\n\t\t\t}\n\t\t);\n\n\t\tjQuery.event.trigger( e, null, elem );\n\t}\n\n} );\n\njQuery.fn.extend( {\n\n\ttrigger: function( type, data ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.trigger( type, data, this );\n\t\t} );\n\t},\n\ttriggerHandler: function( type, data ) {\n\t\tvar elem = this[ 0 ];\n\t\tif ( elem ) {\n\t\t\treturn jQuery.event.trigger( type, data, elem, true );\n\t\t}\n\t}\n} );\n\n\njQuery.each( ( \"blur focus focusin focusout resize scroll click dblclick \" +\n\t\"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave \" +\n\t\"change select submit keydown keypress keyup contextmenu\" ).split( \" \" ),\n\tfunction( i, name ) {\n\n\t// Handle event binding\n\tjQuery.fn[ name ] = function( data, fn ) {\n\t\treturn arguments.length > 0 ?\n\t\t\tthis.on( name, null, data, fn ) :\n\t\t\tthis.trigger( name );\n\t};\n} );\n\njQuery.fn.extend( {\n\thover: function( fnOver, fnOut ) {\n\t\treturn this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );\n\t}\n} );\n\n\n\n\nsupport.focusin = \"onfocusin\" in window;\n\n\n// Support: Firefox <=44\n// Firefox doesn't have focus(in | out) events\n// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787\n//\n// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1\n// focus(in | out) events fire after focus & blur events,\n// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order\n// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857\nif ( !support.focusin ) {\n\tjQuery.each( { focus: \"focusin\", blur: \"focusout\" }, function( orig, fix ) {\n\n\t\t// Attach a single capturing handler on the document while someone wants focusin/focusout\n\t\tvar handler = function( event ) {\n\t\t\tjQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) );\n\t\t};\n\n\t\tjQuery.event.special[ fix ] = {\n\t\t\tsetup: function() {\n\t\t\t\tvar doc = this.ownerDocument || this,\n\t\t\t\t\tattaches = dataPriv.access( doc, fix );\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.addEventListener( orig, handler, true );\n\t\t\t\t}\n\t\t\t\tdataPriv.access( doc, fix, ( attaches || 0 ) + 1 );\n\t\t\t},\n\t\t\tteardown: function() {\n\t\t\t\tvar doc = this.ownerDocument || this,\n\t\t\t\t\tattaches = dataPriv.access( doc, fix ) - 1;\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.removeEventListener( orig, handler, true );\n\t\t\t\t\tdataPriv.remove( doc, fix );\n\n\t\t\t\t} else {\n\t\t\t\t\tdataPriv.access( doc, fix, attaches );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t} );\n}\nvar location = window.location;\n\nvar nonce = jQuery.now();\n\nvar rquery = ( /\\?/ );\n\n\n\n// Cross-browser xml parsing\njQuery.parseXML = function( data ) {\n\tvar xml;\n\tif ( !data || typeof data !== \"string\" ) {\n\t\treturn null;\n\t}\n\n\t// Support: IE 9 - 11 only\n\t// IE throws on parseFromString with invalid input.\n\ttry {\n\t\txml = ( new window.DOMParser() ).parseFromString( data, \"text/xml\" );\n\t} catch ( e ) {\n\t\txml = undefined;\n\t}\n\n\tif ( !xml || xml.getElementsByTagName( \"parsererror\" ).length ) {\n\t\tjQuery.error( \"Invalid XML: \" + data );\n\t}\n\treturn xml;\n};\n\n\nvar\n\trbracket = /\\[\\]$/,\n\trCRLF = /\\r?\\n/g,\n\trsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,\n\trsubmittable = /^(?:input|select|textarea|keygen)/i;\n\nfunction buildParams( prefix, obj, traditional, add ) {\n\tvar name;\n\n\tif ( Array.isArray( obj ) ) {\n\n\t\t// Serialize array item.\n\t\tjQuery.each( obj, function( i, v ) {\n\t\t\tif ( traditional || rbracket.test( prefix ) ) {\n\n\t\t\t\t// Treat each array item as a scalar.\n\t\t\t\tadd( prefix, v );\n\n\t\t\t} else {\n\n\t\t\t\t// Item is non-scalar (array or object), encode its numeric index.\n\t\t\t\tbuildParams(\n\t\t\t\t\tprefix + \"[\" + ( typeof v === \"object\" && v != null ? i : \"\" ) + \"]\",\n\t\t\t\t\tv,\n\t\t\t\t\ttraditional,\n\t\t\t\t\tadd\n\t\t\t\t);\n\t\t\t}\n\t\t} );\n\n\t} else if ( !traditional && jQuery.type( obj ) === \"object\" ) {\n\n\t\t// Serialize object item.\n\t\tfor ( name in obj ) {\n\t\t\tbuildParams( prefix + \"[\" + name + \"]\", obj[ name ], traditional, add );\n\t\t}\n\n\t} else {\n\n\t\t// Serialize scalar item.\n\t\tadd( prefix, obj );\n\t}\n}\n\n// Serialize an array of form elements or a set of\n// key/values into a query string\njQuery.param = function( a, traditional ) {\n\tvar prefix,\n\t\ts = [],\n\t\tadd = function( key, valueOrFunction ) {\n\n\t\t\t// If value is a function, invoke it and use its return value\n\t\t\tvar value = jQuery.isFunction( valueOrFunction ) ?\n\t\t\t\tvalueOrFunction() :\n\t\t\t\tvalueOrFunction;\n\n\t\t\ts[ s.length ] = encodeURIComponent( key ) + \"=\" +\n\t\t\t\tencodeURIComponent( value == null ? \"\" : value );\n\t\t};\n\n\t// If an array was passed in, assume that it is an array of form elements.\n\tif ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {\n\n\t\t// Serialize the form elements\n\t\tjQuery.each( a, function() {\n\t\t\tadd( this.name, this.value );\n\t\t} );\n\n\t} else {\n\n\t\t// If traditional, encode the \"old\" way (the way 1.3.2 or older\n\t\t// did it), otherwise encode params recursively.\n\t\tfor ( prefix in a ) {\n\t\t\tbuildParams( prefix, a[ prefix ], traditional, add );\n\t\t}\n\t}\n\n\t// Return the resulting serialization\n\treturn s.join( \"&\" );\n};\n\njQuery.fn.extend( {\n\tserialize: function() {\n\t\treturn jQuery.param( this.serializeArray() );\n\t},\n\tserializeArray: function() {\n\t\treturn this.map( function() {\n\n\t\t\t// Can add propHook for \"elements\" to filter or add form elements\n\t\t\tvar elements = jQuery.prop( this, \"elements\" );\n\t\t\treturn elements ? jQuery.makeArray( elements ) : this;\n\t\t} )\n\t\t.filter( function() {\n\t\t\tvar type = this.type;\n\n\t\t\t// Use .is( \":disabled\" ) so that fieldset[disabled] works\n\t\t\treturn this.name && !jQuery( this ).is( \":disabled\" ) &&\n\t\t\t\trsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&\n\t\t\t\t( this.checked || !rcheckableType.test( type ) );\n\t\t} )\n\t\t.map( function( i, elem ) {\n\t\t\tvar val = jQuery( this ).val();\n\n\t\t\tif ( val == null ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tif ( Array.isArray( val ) ) {\n\t\t\t\treturn jQuery.map( val, function( val ) {\n\t\t\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t} ).get();\n\t}\n} );\n\n\nvar\n\tr20 = /%20/g,\n\trhash = /#.*$/,\n\trantiCache = /([?&])_=[^&]*/,\n\trheaders = /^(.*?):[ \\t]*([^\\r\\n]*)$/mg,\n\n\t// #7653, #8125, #8152: local protocol detection\n\trlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,\n\trnoContent = /^(?:GET|HEAD)$/,\n\trprotocol = /^\\/\\//,\n\n\t/* Prefilters\n\t * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)\n\t * 2) These are called:\n\t *    - BEFORE asking for a transport\n\t *    - AFTER param serialization (s.data is a string if s.processData is true)\n\t * 3) key is the dataType\n\t * 4) the catchall symbol \"*\" can be used\n\t * 5) execution will start with transport dataType and THEN continue down to \"*\" if needed\n\t */\n\tprefilters = {},\n\n\t/* Transports bindings\n\t * 1) key is the dataType\n\t * 2) the catchall symbol \"*\" can be used\n\t * 3) selection will start with transport dataType and THEN go to \"*\" if needed\n\t */\n\ttransports = {},\n\n\t// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression\n\tallTypes = \"*/\".concat( \"*\" ),\n\n\t// Anchor tag for parsing the document origin\n\toriginAnchor = document.createElement( \"a\" );\n\toriginAnchor.href = location.href;\n\n// Base \"constructor\" for jQuery.ajaxPrefilter and jQuery.ajaxTransport\nfunction addToPrefiltersOrTransports( structure ) {\n\n\t// dataTypeExpression is optional and defaults to \"*\"\n\treturn function( dataTypeExpression, func ) {\n\n\t\tif ( typeof dataTypeExpression !== \"string\" ) {\n\t\t\tfunc = dataTypeExpression;\n\t\t\tdataTypeExpression = \"*\";\n\t\t}\n\n\t\tvar dataType,\n\t\t\ti = 0,\n\t\t\tdataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || [];\n\n\t\tif ( jQuery.isFunction( func ) ) {\n\n\t\t\t// For each dataType in the dataTypeExpression\n\t\t\twhile ( ( dataType = dataTypes[ i++ ] ) ) {\n\n\t\t\t\t// Prepend if requested\n\t\t\t\tif ( dataType[ 0 ] === \"+\" ) {\n\t\t\t\t\tdataType = dataType.slice( 1 ) || \"*\";\n\t\t\t\t\t( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func );\n\n\t\t\t\t// Otherwise append\n\t\t\t\t} else {\n\t\t\t\t\t( structure[ dataType ] = structure[ dataType ] || [] ).push( func );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\n// Base inspection function for prefilters and transports\nfunction inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {\n\n\tvar inspected = {},\n\t\tseekingTransport = ( structure === transports );\n\n\tfunction inspect( dataType ) {\n\t\tvar selected;\n\t\tinspected[ dataType ] = true;\n\t\tjQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {\n\t\t\tvar dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );\n\t\t\tif ( typeof dataTypeOrTransport === \"string\" &&\n\t\t\t\t!seekingTransport && !inspected[ dataTypeOrTransport ] ) {\n\n\t\t\t\toptions.dataTypes.unshift( dataTypeOrTransport );\n\t\t\t\tinspect( dataTypeOrTransport );\n\t\t\t\treturn false;\n\t\t\t} else if ( seekingTransport ) {\n\t\t\t\treturn !( selected = dataTypeOrTransport );\n\t\t\t}\n\t\t} );\n\t\treturn selected;\n\t}\n\n\treturn inspect( options.dataTypes[ 0 ] ) || !inspected[ \"*\" ] && inspect( \"*\" );\n}\n\n// A special extend for ajax options\n// that takes \"flat\" options (not to be deep extended)\n// Fixes #9887\nfunction ajaxExtend( target, src ) {\n\tvar key, deep,\n\t\tflatOptions = jQuery.ajaxSettings.flatOptions || {};\n\n\tfor ( key in src ) {\n\t\tif ( src[ key ] !== undefined ) {\n\t\t\t( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];\n\t\t}\n\t}\n\tif ( deep ) {\n\t\tjQuery.extend( true, target, deep );\n\t}\n\n\treturn target;\n}\n\n/* Handles responses to an ajax request:\n * - finds the right dataType (mediates between content-type and expected dataType)\n * - returns the corresponding response\n */\nfunction ajaxHandleResponses( s, jqXHR, responses ) {\n\n\tvar ct, type, finalDataType, firstDataType,\n\t\tcontents = s.contents,\n\t\tdataTypes = s.dataTypes;\n\n\t// Remove auto dataType and get content-type in the process\n\twhile ( dataTypes[ 0 ] === \"*\" ) {\n\t\tdataTypes.shift();\n\t\tif ( ct === undefined ) {\n\t\t\tct = s.mimeType || jqXHR.getResponseHeader( \"Content-Type\" );\n\t\t}\n\t}\n\n\t// Check if we're dealing with a known content-type\n\tif ( ct ) {\n\t\tfor ( type in contents ) {\n\t\t\tif ( contents[ type ] && contents[ type ].test( ct ) ) {\n\t\t\t\tdataTypes.unshift( type );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check to see if we have a response for the expected dataType\n\tif ( dataTypes[ 0 ] in responses ) {\n\t\tfinalDataType = dataTypes[ 0 ];\n\t} else {\n\n\t\t// Try convertible dataTypes\n\t\tfor ( type in responses ) {\n\t\t\tif ( !dataTypes[ 0 ] || s.converters[ type + \" \" + dataTypes[ 0 ] ] ) {\n\t\t\t\tfinalDataType = type;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( !firstDataType ) {\n\t\t\t\tfirstDataType = type;\n\t\t\t}\n\t\t}\n\n\t\t// Or just use first one\n\t\tfinalDataType = finalDataType || firstDataType;\n\t}\n\n\t// If we found a dataType\n\t// We add the dataType to the list if needed\n\t// and return the corresponding response\n\tif ( finalDataType ) {\n\t\tif ( finalDataType !== dataTypes[ 0 ] ) {\n\t\t\tdataTypes.unshift( finalDataType );\n\t\t}\n\t\treturn responses[ finalDataType ];\n\t}\n}\n\n/* Chain conversions given the request and the original response\n * Also sets the responseXXX fields on the jqXHR instance\n */\nfunction ajaxConvert( s, response, jqXHR, isSuccess ) {\n\tvar conv2, current, conv, tmp, prev,\n\t\tconverters = {},\n\n\t\t// Work with a copy of dataTypes in case we need to modify it for conversion\n\t\tdataTypes = s.dataTypes.slice();\n\n\t// Create converters map with lowercased keys\n\tif ( dataTypes[ 1 ] ) {\n\t\tfor ( conv in s.converters ) {\n\t\t\tconverters[ conv.toLowerCase() ] = s.converters[ conv ];\n\t\t}\n\t}\n\n\tcurrent = dataTypes.shift();\n\n\t// Convert to each sequential dataType\n\twhile ( current ) {\n\n\t\tif ( s.responseFields[ current ] ) {\n\t\t\tjqXHR[ s.responseFields[ current ] ] = response;\n\t\t}\n\n\t\t// Apply the dataFilter if provided\n\t\tif ( !prev && isSuccess && s.dataFilter ) {\n\t\t\tresponse = s.dataFilter( response, s.dataType );\n\t\t}\n\n\t\tprev = current;\n\t\tcurrent = dataTypes.shift();\n\n\t\tif ( current ) {\n\n\t\t\t// There's only work to do if current dataType is non-auto\n\t\t\tif ( current === \"*\" ) {\n\n\t\t\t\tcurrent = prev;\n\n\t\t\t// Convert response if prev dataType is non-auto and differs from current\n\t\t\t} else if ( prev !== \"*\" && prev !== current ) {\n\n\t\t\t\t// Seek a direct converter\n\t\t\t\tconv = converters[ prev + \" \" + current ] || converters[ \"* \" + current ];\n\n\t\t\t\t// If none found, seek a pair\n\t\t\t\tif ( !conv ) {\n\t\t\t\t\tfor ( conv2 in converters ) {\n\n\t\t\t\t\t\t// If conv2 outputs current\n\t\t\t\t\t\ttmp = conv2.split( \" \" );\n\t\t\t\t\t\tif ( tmp[ 1 ] === current ) {\n\n\t\t\t\t\t\t\t// If prev can be converted to accepted input\n\t\t\t\t\t\t\tconv = converters[ prev + \" \" + tmp[ 0 ] ] ||\n\t\t\t\t\t\t\t\tconverters[ \"* \" + tmp[ 0 ] ];\n\t\t\t\t\t\t\tif ( conv ) {\n\n\t\t\t\t\t\t\t\t// Condense equivalence converters\n\t\t\t\t\t\t\t\tif ( conv === true ) {\n\t\t\t\t\t\t\t\t\tconv = converters[ conv2 ];\n\n\t\t\t\t\t\t\t\t// Otherwise, insert the intermediate dataType\n\t\t\t\t\t\t\t\t} else if ( converters[ conv2 ] !== true ) {\n\t\t\t\t\t\t\t\t\tcurrent = tmp[ 0 ];\n\t\t\t\t\t\t\t\t\tdataTypes.unshift( tmp[ 1 ] );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Apply converter (if not an equivalence)\n\t\t\t\tif ( conv !== true ) {\n\n\t\t\t\t\t// Unless errors are allowed to bubble, catch and return them\n\t\t\t\t\tif ( conv && s.throws ) {\n\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t\t} catch ( e ) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tstate: \"parsererror\",\n\t\t\t\t\t\t\t\terror: conv ? e : \"No conversion from \" + prev + \" to \" + current\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { state: \"success\", data: response };\n}\n\njQuery.extend( {\n\n\t// Counter for holding the number of active queries\n\tactive: 0,\n\n\t// Last-Modified header cache for next request\n\tlastModified: {},\n\tetag: {},\n\n\tajaxSettings: {\n\t\turl: location.href,\n\t\ttype: \"GET\",\n\t\tisLocal: rlocalProtocol.test( location.protocol ),\n\t\tglobal: true,\n\t\tprocessData: true,\n\t\tasync: true,\n\t\tcontentType: \"application/x-www-form-urlencoded; charset=UTF-8\",\n\n\t\t/*\n\t\ttimeout: 0,\n\t\tdata: null,\n\t\tdataType: null,\n\t\tusername: null,\n\t\tpassword: null,\n\t\tcache: null,\n\t\tthrows: false,\n\t\ttraditional: false,\n\t\theaders: {},\n\t\t*/\n\n\t\taccepts: {\n\t\t\t\"*\": allTypes,\n\t\t\ttext: \"text/plain\",\n\t\t\thtml: \"text/html\",\n\t\t\txml: \"application/xml, text/xml\",\n\t\t\tjson: \"application/json, text/javascript\"\n\t\t},\n\n\t\tcontents: {\n\t\t\txml: /\\bxml\\b/,\n\t\t\thtml: /\\bhtml/,\n\t\t\tjson: /\\bjson\\b/\n\t\t},\n\n\t\tresponseFields: {\n\t\t\txml: \"responseXML\",\n\t\t\ttext: \"responseText\",\n\t\t\tjson: \"responseJSON\"\n\t\t},\n\n\t\t// Data converters\n\t\t// Keys separate source (or catchall \"*\") and destination types with a single space\n\t\tconverters: {\n\n\t\t\t// Convert anything to text\n\t\t\t\"* text\": String,\n\n\t\t\t// Text to html (true = no transformation)\n\t\t\t\"text html\": true,\n\n\t\t\t// Evaluate text as a json expression\n\t\t\t\"text json\": JSON.parse,\n\n\t\t\t// Parse text as xml\n\t\t\t\"text xml\": jQuery.parseXML\n\t\t},\n\n\t\t// For options that shouldn't be deep extended:\n\t\t// you can add your own custom options here if\n\t\t// and when you create one that shouldn't be\n\t\t// deep extended (see ajaxExtend)\n\t\tflatOptions: {\n\t\t\turl: true,\n\t\t\tcontext: true\n\t\t}\n\t},\n\n\t// Creates a full fledged settings object into target\n\t// with both ajaxSettings and settings fields.\n\t// If target is omitted, writes into ajaxSettings.\n\tajaxSetup: function( target, settings ) {\n\t\treturn settings ?\n\n\t\t\t// Building a settings object\n\t\t\tajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :\n\n\t\t\t// Extending ajaxSettings\n\t\t\tajaxExtend( jQuery.ajaxSettings, target );\n\t},\n\n\tajaxPrefilter: addToPrefiltersOrTransports( prefilters ),\n\tajaxTransport: addToPrefiltersOrTransports( transports ),\n\n\t// Main method\n\tajax: function( url, options ) {\n\n\t\t// If url is an object, simulate pre-1.5 signature\n\t\tif ( typeof url === \"object\" ) {\n\t\t\toptions = url;\n\t\t\turl = undefined;\n\t\t}\n\n\t\t// Force options to be an object\n\t\toptions = options || {};\n\n\t\tvar transport,\n\n\t\t\t// URL without anti-cache param\n\t\t\tcacheURL,\n\n\t\t\t// Response headers\n\t\t\tresponseHeadersString,\n\t\t\tresponseHeaders,\n\n\t\t\t// timeout handle\n\t\t\ttimeoutTimer,\n\n\t\t\t// Url cleanup var\n\t\t\turlAnchor,\n\n\t\t\t// Request state (becomes false upon send and true upon completion)\n\t\t\tcompleted,\n\n\t\t\t// To know if global events are to be dispatched\n\t\t\tfireGlobals,\n\n\t\t\t// Loop variable\n\t\t\ti,\n\n\t\t\t// uncached part of the url\n\t\t\tuncached,\n\n\t\t\t// Create the final options object\n\t\t\ts = jQuery.ajaxSetup( {}, options ),\n\n\t\t\t// Callbacks context\n\t\t\tcallbackContext = s.context || s,\n\n\t\t\t// Context for global events is callbackContext if it is a DOM node or jQuery collection\n\t\t\tglobalEventContext = s.context &&\n\t\t\t\t( callbackContext.nodeType || callbackContext.jquery ) ?\n\t\t\t\t\tjQuery( callbackContext ) :\n\t\t\t\t\tjQuery.event,\n\n\t\t\t// Deferreds\n\t\t\tdeferred = jQuery.Deferred(),\n\t\t\tcompleteDeferred = jQuery.Callbacks( \"once memory\" ),\n\n\t\t\t// Status-dependent callbacks\n\t\t\tstatusCode = s.statusCode || {},\n\n\t\t\t// Headers (they are sent all at once)\n\t\t\trequestHeaders = {},\n\t\t\trequestHeadersNames = {},\n\n\t\t\t// Default abort message\n\t\t\tstrAbort = \"canceled\",\n\n\t\t\t// Fake xhr\n\t\t\tjqXHR = {\n\t\t\t\treadyState: 0,\n\n\t\t\t\t// Builds headers hashtable if needed\n\t\t\t\tgetResponseHeader: function( key ) {\n\t\t\t\t\tvar match;\n\t\t\t\t\tif ( completed ) {\n\t\t\t\t\t\tif ( !responseHeaders ) {\n\t\t\t\t\t\t\tresponseHeaders = {};\n\t\t\t\t\t\t\twhile ( ( match = rheaders.exec( responseHeadersString ) ) ) {\n\t\t\t\t\t\t\t\tresponseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmatch = responseHeaders[ key.toLowerCase() ];\n\t\t\t\t\t}\n\t\t\t\t\treturn match == null ? null : match;\n\t\t\t\t},\n\n\t\t\t\t// Raw string\n\t\t\t\tgetAllResponseHeaders: function() {\n\t\t\t\t\treturn completed ? responseHeadersString : null;\n\t\t\t\t},\n\n\t\t\t\t// Caches the header\n\t\t\t\tsetRequestHeader: function( name, value ) {\n\t\t\t\t\tif ( completed == null ) {\n\t\t\t\t\t\tname = requestHeadersNames[ name.toLowerCase() ] =\n\t\t\t\t\t\t\trequestHeadersNames[ name.toLowerCase() ] || name;\n\t\t\t\t\t\trequestHeaders[ name ] = value;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Overrides response content-type header\n\t\t\t\toverrideMimeType: function( type ) {\n\t\t\t\t\tif ( completed == null ) {\n\t\t\t\t\t\ts.mimeType = type;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Status-dependent callbacks\n\t\t\t\tstatusCode: function( map ) {\n\t\t\t\t\tvar code;\n\t\t\t\t\tif ( map ) {\n\t\t\t\t\t\tif ( completed ) {\n\n\t\t\t\t\t\t\t// Execute the appropriate callbacks\n\t\t\t\t\t\t\tjqXHR.always( map[ jqXHR.status ] );\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// Lazy-add the new callbacks in a way that preserves old ones\n\t\t\t\t\t\t\tfor ( code in map ) {\n\t\t\t\t\t\t\t\tstatusCode[ code ] = [ statusCode[ code ], map[ code ] ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Cancel the request\n\t\t\t\tabort: function( statusText ) {\n\t\t\t\t\tvar finalText = statusText || strAbort;\n\t\t\t\t\tif ( transport ) {\n\t\t\t\t\t\ttransport.abort( finalText );\n\t\t\t\t\t}\n\t\t\t\t\tdone( 0, finalText );\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t};\n\n\t\t// Attach deferreds\n\t\tdeferred.promise( jqXHR );\n\n\t\t// Add protocol if not provided (prefilters might expect it)\n\t\t// Handle falsy url in the settings object (#10093: consistency with old signature)\n\t\t// We also use the url parameter if available\n\t\ts.url = ( ( url || s.url || location.href ) + \"\" )\n\t\t\t.replace( rprotocol, location.protocol + \"//\" );\n\n\t\t// Alias method option to type as per ticket #12004\n\t\ts.type = options.method || options.type || s.method || s.type;\n\n\t\t// Extract dataTypes list\n\t\ts.dataTypes = ( s.dataType || \"*\" ).toLowerCase().match( rnothtmlwhite ) || [ \"\" ];\n\n\t\t// A cross-domain request is in order when the origin doesn't match the current origin.\n\t\tif ( s.crossDomain == null ) {\n\t\t\turlAnchor = document.createElement( \"a\" );\n\n\t\t\t// Support: IE <=8 - 11, Edge 12 - 13\n\t\t\t// IE throws exception on accessing the href property if url is malformed,\n\t\t\t// e.g. http://example.com:80x/\n\t\t\ttry {\n\t\t\t\turlAnchor.href = s.url;\n\n\t\t\t\t// Support: IE <=8 - 11 only\n\t\t\t\t// Anchor's host property isn't correctly set when s.url is relative\n\t\t\t\turlAnchor.href = urlAnchor.href;\n\t\t\t\ts.crossDomain = originAnchor.protocol + \"//\" + originAnchor.host !==\n\t\t\t\t\turlAnchor.protocol + \"//\" + urlAnchor.host;\n\t\t\t} catch ( e ) {\n\n\t\t\t\t// If there is an error parsing the URL, assume it is crossDomain,\n\t\t\t\t// it can be rejected by the transport if it is invalid\n\t\t\t\ts.crossDomain = true;\n\t\t\t}\n\t\t}\n\n\t\t// Convert data if not already a string\n\t\tif ( s.data && s.processData && typeof s.data !== \"string\" ) {\n\t\t\ts.data = jQuery.param( s.data, s.traditional );\n\t\t}\n\n\t\t// Apply prefilters\n\t\tinspectPrefiltersOrTransports( prefilters, s, options, jqXHR );\n\n\t\t// If request was aborted inside a prefilter, stop there\n\t\tif ( completed ) {\n\t\t\treturn jqXHR;\n\t\t}\n\n\t\t// We can fire global events as of now if asked to\n\t\t// Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)\n\t\tfireGlobals = jQuery.event && s.global;\n\n\t\t// Watch for a new set of requests\n\t\tif ( fireGlobals && jQuery.active++ === 0 ) {\n\t\t\tjQuery.event.trigger( \"ajaxStart\" );\n\t\t}\n\n\t\t// Uppercase the type\n\t\ts.type = s.type.toUpperCase();\n\n\t\t// Determine if request has content\n\t\ts.hasContent = !rnoContent.test( s.type );\n\n\t\t// Save the URL in case we're toying with the If-Modified-Since\n\t\t// and/or If-None-Match header later on\n\t\t// Remove hash to simplify url manipulation\n\t\tcacheURL = s.url.replace( rhash, \"\" );\n\n\t\t// More options handling for requests with no content\n\t\tif ( !s.hasContent ) {\n\n\t\t\t// Remember the hash so we can put it back\n\t\t\tuncached = s.url.slice( cacheURL.length );\n\n\t\t\t// If data is available, append data to url\n\t\t\tif ( s.data ) {\n\t\t\t\tcacheURL += ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + s.data;\n\n\t\t\t\t// #9682: remove data so that it's not used in an eventual retry\n\t\t\t\tdelete s.data;\n\t\t\t}\n\n\t\t\t// Add or update anti-cache param if needed\n\t\t\tif ( s.cache === false ) {\n\t\t\t\tcacheURL = cacheURL.replace( rantiCache, \"$1\" );\n\t\t\t\tuncached = ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + \"_=\" + ( nonce++ ) + uncached;\n\t\t\t}\n\n\t\t\t// Put hash and anti-cache on the URL that will be requested (gh-1732)\n\t\t\ts.url = cacheURL + uncached;\n\n\t\t// Change '%20' to '+' if this is encoded form body content (gh-2658)\n\t\t} else if ( s.data && s.processData &&\n\t\t\t( s.contentType || \"\" ).indexOf( \"application/x-www-form-urlencoded\" ) === 0 ) {\n\t\t\ts.data = s.data.replace( r20, \"+\" );\n\t\t}\n\n\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\tif ( s.ifModified ) {\n\t\t\tif ( jQuery.lastModified[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-Modified-Since\", jQuery.lastModified[ cacheURL ] );\n\t\t\t}\n\t\t\tif ( jQuery.etag[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-None-Match\", jQuery.etag[ cacheURL ] );\n\t\t\t}\n\t\t}\n\n\t\t// Set the correct header, if data is being sent\n\t\tif ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {\n\t\t\tjqXHR.setRequestHeader( \"Content-Type\", s.contentType );\n\t\t}\n\n\t\t// Set the Accepts header for the server, depending on the dataType\n\t\tjqXHR.setRequestHeader(\n\t\t\t\"Accept\",\n\t\t\ts.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ?\n\t\t\t\ts.accepts[ s.dataTypes[ 0 ] ] +\n\t\t\t\t\t( s.dataTypes[ 0 ] !== \"*\" ? \", \" + allTypes + \"; q=0.01\" : \"\" ) :\n\t\t\t\ts.accepts[ \"*\" ]\n\t\t);\n\n\t\t// Check for headers option\n\t\tfor ( i in s.headers ) {\n\t\t\tjqXHR.setRequestHeader( i, s.headers[ i ] );\n\t\t}\n\n\t\t// Allow custom headers/mimetypes and early abort\n\t\tif ( s.beforeSend &&\n\t\t\t( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) {\n\n\t\t\t// Abort if not done already and return\n\t\t\treturn jqXHR.abort();\n\t\t}\n\n\t\t// Aborting is no longer a cancellation\n\t\tstrAbort = \"abort\";\n\n\t\t// Install callbacks on deferreds\n\t\tcompleteDeferred.add( s.complete );\n\t\tjqXHR.done( s.success );\n\t\tjqXHR.fail( s.error );\n\n\t\t// Get transport\n\t\ttransport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );\n\n\t\t// If no transport, we auto-abort\n\t\tif ( !transport ) {\n\t\t\tdone( -1, \"No Transport\" );\n\t\t} else {\n\t\t\tjqXHR.readyState = 1;\n\n\t\t\t// Send global event\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxSend\", [ jqXHR, s ] );\n\t\t\t}\n\n\t\t\t// If request was aborted inside ajaxSend, stop there\n\t\t\tif ( completed ) {\n\t\t\t\treturn jqXHR;\n\t\t\t}\n\n\t\t\t// Timeout\n\t\t\tif ( s.async && s.timeout > 0 ) {\n\t\t\t\ttimeoutTimer = window.setTimeout( function() {\n\t\t\t\t\tjqXHR.abort( \"timeout\" );\n\t\t\t\t}, s.timeout );\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tcompleted = false;\n\t\t\t\ttransport.send( requestHeaders, done );\n\t\t\t} catch ( e ) {\n\n\t\t\t\t// Rethrow post-completion exceptions\n\t\t\t\tif ( completed ) {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\n\t\t\t\t// Propagate others as results\n\t\t\t\tdone( -1, e );\n\t\t\t}\n\t\t}\n\n\t\t// Callback for when everything is done\n\t\tfunction done( status, nativeStatusText, responses, headers ) {\n\t\t\tvar isSuccess, success, error, response, modified,\n\t\t\t\tstatusText = nativeStatusText;\n\n\t\t\t// Ignore repeat invocations\n\t\t\tif ( completed ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcompleted = true;\n\n\t\t\t// Clear timeout if it exists\n\t\t\tif ( timeoutTimer ) {\n\t\t\t\twindow.clearTimeout( timeoutTimer );\n\t\t\t}\n\n\t\t\t// Dereference transport for early garbage collection\n\t\t\t// (no matter how long the jqXHR object will be used)\n\t\t\ttransport = undefined;\n\n\t\t\t// Cache response headers\n\t\t\tresponseHeadersString = headers || \"\";\n\n\t\t\t// Set readyState\n\t\t\tjqXHR.readyState = status > 0 ? 4 : 0;\n\n\t\t\t// Determine if successful\n\t\t\tisSuccess = status >= 200 && status < 300 || status === 304;\n\n\t\t\t// Get response data\n\t\t\tif ( responses ) {\n\t\t\t\tresponse = ajaxHandleResponses( s, jqXHR, responses );\n\t\t\t}\n\n\t\t\t// Convert no matter what (that way responseXXX fields are always set)\n\t\t\tresponse = ajaxConvert( s, response, jqXHR, isSuccess );\n\n\t\t\t// If successful, handle type chaining\n\t\t\tif ( isSuccess ) {\n\n\t\t\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\t\t\tif ( s.ifModified ) {\n\t\t\t\t\tmodified = jqXHR.getResponseHeader( \"Last-Modified\" );\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.lastModified[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t\tmodified = jqXHR.getResponseHeader( \"etag\" );\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.etag[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// if no content\n\t\t\t\tif ( status === 204 || s.type === \"HEAD\" ) {\n\t\t\t\t\tstatusText = \"nocontent\";\n\n\t\t\t\t// if not modified\n\t\t\t\t} else if ( status === 304 ) {\n\t\t\t\t\tstatusText = \"notmodified\";\n\n\t\t\t\t// If we have data, let's convert it\n\t\t\t\t} else {\n\t\t\t\t\tstatusText = response.state;\n\t\t\t\t\tsuccess = response.data;\n\t\t\t\t\terror = response.error;\n\t\t\t\t\tisSuccess = !error;\n\t\t\t\t}\n\t\t\t} else {\n\n\t\t\t\t// Extract error from statusText and normalize for non-aborts\n\t\t\t\terror = statusText;\n\t\t\t\tif ( status || !statusText ) {\n\t\t\t\t\tstatusText = \"error\";\n\t\t\t\t\tif ( status < 0 ) {\n\t\t\t\t\t\tstatus = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set data for the fake xhr object\n\t\t\tjqXHR.status = status;\n\t\t\tjqXHR.statusText = ( nativeStatusText || statusText ) + \"\";\n\n\t\t\t// Success/Error\n\t\t\tif ( isSuccess ) {\n\t\t\t\tdeferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );\n\t\t\t} else {\n\t\t\t\tdeferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );\n\t\t\t}\n\n\t\t\t// Status-dependent callbacks\n\t\t\tjqXHR.statusCode( statusCode );\n\t\t\tstatusCode = undefined;\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( isSuccess ? \"ajaxSuccess\" : \"ajaxError\",\n\t\t\t\t\t[ jqXHR, s, isSuccess ? success : error ] );\n\t\t\t}\n\n\t\t\t// Complete\n\t\t\tcompleteDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxComplete\", [ jqXHR, s ] );\n\n\t\t\t\t// Handle the global AJAX counter\n\t\t\t\tif ( !( --jQuery.active ) ) {\n\t\t\t\t\tjQuery.event.trigger( \"ajaxStop\" );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn jqXHR;\n\t},\n\n\tgetJSON: function( url, data, callback ) {\n\t\treturn jQuery.get( url, data, callback, \"json\" );\n\t},\n\n\tgetScript: function( url, callback ) {\n\t\treturn jQuery.get( url, undefined, callback, \"script\" );\n\t}\n} );\n\njQuery.each( [ \"get\", \"post\" ], function( i, method ) {\n\tjQuery[ method ] = function( url, data, callback, type ) {\n\n\t\t// Shift arguments if data argument was omitted\n\t\tif ( jQuery.isFunction( data ) ) {\n\t\t\ttype = type || callback;\n\t\t\tcallback = data;\n\t\t\tdata = undefined;\n\t\t}\n\n\t\t// The url can be an options object (which then must have .url)\n\t\treturn jQuery.ajax( jQuery.extend( {\n\t\t\turl: url,\n\t\t\ttype: method,\n\t\t\tdataType: type,\n\t\t\tdata: data,\n\t\t\tsuccess: callback\n\t\t}, jQuery.isPlainObject( url ) && url ) );\n\t};\n} );\n\n\njQuery._evalUrl = function( url ) {\n\treturn jQuery.ajax( {\n\t\turl: url,\n\n\t\t// Make this explicit, since user can override this through ajaxSetup (#11264)\n\t\ttype: \"GET\",\n\t\tdataType: \"script\",\n\t\tcache: true,\n\t\tasync: false,\n\t\tglobal: false,\n\t\t\"throws\": true\n\t} );\n};\n\n\njQuery.fn.extend( {\n\twrapAll: function( html ) {\n\t\tvar wrap;\n\n\t\tif ( this[ 0 ] ) {\n\t\t\tif ( jQuery.isFunction( html ) ) {\n\t\t\t\thtml = html.call( this[ 0 ] );\n\t\t\t}\n\n\t\t\t// The elements to wrap the target around\n\t\t\twrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );\n\n\t\t\tif ( this[ 0 ].parentNode ) {\n\t\t\t\twrap.insertBefore( this[ 0 ] );\n\t\t\t}\n\n\t\t\twrap.map( function() {\n\t\t\t\tvar elem = this;\n\n\t\t\t\twhile ( elem.firstElementChild ) {\n\t\t\t\t\telem = elem.firstElementChild;\n\t\t\t\t}\n\n\t\t\t\treturn elem;\n\t\t\t} ).append( this );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\twrapInner: function( html ) {\n\t\tif ( jQuery.isFunction( html ) ) {\n\t\t\treturn this.each( function( i ) {\n\t\t\t\tjQuery( this ).wrapInner( html.call( this, i ) );\n\t\t\t} );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar self = jQuery( this ),\n\t\t\t\tcontents = self.contents();\n\n\t\t\tif ( contents.length ) {\n\t\t\t\tcontents.wrapAll( html );\n\n\t\t\t} else {\n\t\t\t\tself.append( html );\n\t\t\t}\n\t\t} );\n\t},\n\n\twrap: function( html ) {\n\t\tvar isFunction = jQuery.isFunction( html );\n\n\t\treturn this.each( function( i ) {\n\t\t\tjQuery( this ).wrapAll( isFunction ? html.call( this, i ) : html );\n\t\t} );\n\t},\n\n\tunwrap: function( selector ) {\n\t\tthis.parent( selector ).not( \"body\" ).each( function() {\n\t\t\tjQuery( this ).replaceWith( this.childNodes );\n\t\t} );\n\t\treturn this;\n\t}\n} );\n\n\njQuery.expr.pseudos.hidden = function( elem ) {\n\treturn !jQuery.expr.pseudos.visible( elem );\n};\njQuery.expr.pseudos.visible = function( elem ) {\n\treturn !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );\n};\n\n\n\n\njQuery.ajaxSettings.xhr = function() {\n\ttry {\n\t\treturn new window.XMLHttpRequest();\n\t} catch ( e ) {}\n};\n\nvar xhrSuccessStatus = {\n\n\t\t// File protocol always yields status code 0, assume 200\n\t\t0: 200,\n\n\t\t// Support: IE <=9 only\n\t\t// #1450: sometimes IE returns 1223 when it should be 204\n\t\t1223: 204\n\t},\n\txhrSupported = jQuery.ajaxSettings.xhr();\n\nsupport.cors = !!xhrSupported && ( \"withCredentials\" in xhrSupported );\nsupport.ajax = xhrSupported = !!xhrSupported;\n\njQuery.ajaxTransport( function( options ) {\n\tvar callback, errorCallback;\n\n\t// Cross domain only allowed if supported through XMLHttpRequest\n\tif ( support.cors || xhrSupported && !options.crossDomain ) {\n\t\treturn {\n\t\t\tsend: function( headers, complete ) {\n\t\t\t\tvar i,\n\t\t\t\t\txhr = options.xhr();\n\n\t\t\t\txhr.open(\n\t\t\t\t\toptions.type,\n\t\t\t\t\toptions.url,\n\t\t\t\t\toptions.async,\n\t\t\t\t\toptions.username,\n\t\t\t\t\toptions.password\n\t\t\t\t);\n\n\t\t\t\t// Apply custom fields if provided\n\t\t\t\tif ( options.xhrFields ) {\n\t\t\t\t\tfor ( i in options.xhrFields ) {\n\t\t\t\t\t\txhr[ i ] = options.xhrFields[ i ];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Override mime type if needed\n\t\t\t\tif ( options.mimeType && xhr.overrideMimeType ) {\n\t\t\t\t\txhr.overrideMimeType( options.mimeType );\n\t\t\t\t}\n\n\t\t\t\t// X-Requested-With header\n\t\t\t\t// For cross-domain requests, seeing as conditions for a preflight are\n\t\t\t\t// akin to a jigsaw puzzle, we simply never set it to be sure.\n\t\t\t\t// (it can always be set on a per-request basis or even using ajaxSetup)\n\t\t\t\t// For same-domain requests, won't change header if already provided.\n\t\t\t\tif ( !options.crossDomain && !headers[ \"X-Requested-With\" ] ) {\n\t\t\t\t\theaders[ \"X-Requested-With\" ] = \"XMLHttpRequest\";\n\t\t\t\t}\n\n\t\t\t\t// Set headers\n\t\t\t\tfor ( i in headers ) {\n\t\t\t\t\txhr.setRequestHeader( i, headers[ i ] );\n\t\t\t\t}\n\n\t\t\t\t// Callback\n\t\t\t\tcallback = function( type ) {\n\t\t\t\t\treturn function() {\n\t\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\t\tcallback = errorCallback = xhr.onload =\n\t\t\t\t\t\t\t\txhr.onerror = xhr.onabort = xhr.onreadystatechange = null;\n\n\t\t\t\t\t\t\tif ( type === \"abort\" ) {\n\t\t\t\t\t\t\t\txhr.abort();\n\t\t\t\t\t\t\t} else if ( type === \"error\" ) {\n\n\t\t\t\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t\t\t\t// On a manual native abort, IE9 throws\n\t\t\t\t\t\t\t\t// errors on any property access that is not readyState\n\t\t\t\t\t\t\t\tif ( typeof xhr.status !== \"number\" ) {\n\t\t\t\t\t\t\t\t\tcomplete( 0, \"error\" );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tcomplete(\n\n\t\t\t\t\t\t\t\t\t\t// File: protocol always yields status 0; see #8605, #14207\n\t\t\t\t\t\t\t\t\t\txhr.status,\n\t\t\t\t\t\t\t\t\t\txhr.statusText\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcomplete(\n\t\t\t\t\t\t\t\t\txhrSuccessStatus[ xhr.status ] || xhr.status,\n\t\t\t\t\t\t\t\t\txhr.statusText,\n\n\t\t\t\t\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t\t\t\t\t// IE9 has no XHR2 but throws on binary (trac-11426)\n\t\t\t\t\t\t\t\t\t// For XHR2 non-text, let the caller handle it (gh-2498)\n\t\t\t\t\t\t\t\t\t( xhr.responseType || \"text\" ) !== \"text\"  ||\n\t\t\t\t\t\t\t\t\ttypeof xhr.responseText !== \"string\" ?\n\t\t\t\t\t\t\t\t\t\t{ binary: xhr.response } :\n\t\t\t\t\t\t\t\t\t\t{ text: xhr.responseText },\n\t\t\t\t\t\t\t\t\txhr.getAllResponseHeaders()\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t};\n\n\t\t\t\t// Listen to events\n\t\t\t\txhr.onload = callback();\n\t\t\t\terrorCallback = xhr.onerror = callback( \"error\" );\n\n\t\t\t\t// Support: IE 9 only\n\t\t\t\t// Use onreadystatechange to replace onabort\n\t\t\t\t// to handle uncaught aborts\n\t\t\t\tif ( xhr.onabort !== undefined ) {\n\t\t\t\t\txhr.onabort = errorCallback;\n\t\t\t\t} else {\n\t\t\t\t\txhr.onreadystatechange = function() {\n\n\t\t\t\t\t\t// Check readyState before timeout as it changes\n\t\t\t\t\t\tif ( xhr.readyState === 4 ) {\n\n\t\t\t\t\t\t\t// Allow onerror to be called first,\n\t\t\t\t\t\t\t// but that will not handle a native abort\n\t\t\t\t\t\t\t// Also, save errorCallback to a variable\n\t\t\t\t\t\t\t// as xhr.onerror cannot be accessed\n\t\t\t\t\t\t\twindow.setTimeout( function() {\n\t\t\t\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\t\t\t\terrorCallback();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// Create the abort callback\n\t\t\t\tcallback = callback( \"abort\" );\n\n\t\t\t\ttry {\n\n\t\t\t\t\t// Do send the request (this may raise an exception)\n\t\t\t\t\txhr.send( options.hasContent && options.data || null );\n\t\t\t\t} catch ( e ) {\n\n\t\t\t\t\t// #14683: Only rethrow if this hasn't been notified as an error yet\n\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\tthrow e;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tabort: function() {\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n} );\n\n\n\n\n// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432)\njQuery.ajaxPrefilter( function( s ) {\n\tif ( s.crossDomain ) {\n\t\ts.contents.script = false;\n\t}\n} );\n\n// Install script dataType\njQuery.ajaxSetup( {\n\taccepts: {\n\t\tscript: \"text/javascript, application/javascript, \" +\n\t\t\t\"application/ecmascript, application/x-ecmascript\"\n\t},\n\tcontents: {\n\t\tscript: /\\b(?:java|ecma)script\\b/\n\t},\n\tconverters: {\n\t\t\"text script\": function( text ) {\n\t\t\tjQuery.globalEval( text );\n\t\t\treturn text;\n\t\t}\n\t}\n} );\n\n// Handle cache's special case and crossDomain\njQuery.ajaxPrefilter( \"script\", function( s ) {\n\tif ( s.cache === undefined ) {\n\t\ts.cache = false;\n\t}\n\tif ( s.crossDomain ) {\n\t\ts.type = \"GET\";\n\t}\n} );\n\n// Bind script tag hack transport\njQuery.ajaxTransport( \"script\", function( s ) {\n\n\t// This transport only deals with cross domain requests\n\tif ( s.crossDomain ) {\n\t\tvar script, callback;\n\t\treturn {\n\t\t\tsend: function( _, complete ) {\n\t\t\t\tscript = jQuery( \"<script>\" ).prop( {\n\t\t\t\t\tcharset: s.scriptCharset,\n\t\t\t\t\tsrc: s.url\n\t\t\t\t} ).on(\n\t\t\t\t\t\"load error\",\n\t\t\t\t\tcallback = function( evt ) {\n\t\t\t\t\t\tscript.remove();\n\t\t\t\t\t\tcallback = null;\n\t\t\t\t\t\tif ( evt ) {\n\t\t\t\t\t\t\tcomplete( evt.type === \"error\" ? 404 : 200, evt.type );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t);\n\n\t\t\t\t// Use native DOM manipulation to avoid our domManip AJAX trickery\n\t\t\t\tdocument.head.appendChild( script[ 0 ] );\n\t\t\t},\n\t\t\tabort: function() {\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n} );\n\n\n\n\nvar oldCallbacks = [],\n\trjsonp = /(=)\\?(?=&|$)|\\?\\?/;\n\n// Default jsonp settings\njQuery.ajaxSetup( {\n\tjsonp: \"callback\",\n\tjsonpCallback: function() {\n\t\tvar callback = oldCallbacks.pop() || ( jQuery.expando + \"_\" + ( nonce++ ) );\n\t\tthis[ callback ] = true;\n\t\treturn callback;\n\t}\n} );\n\n// Detect, normalize options and install callbacks for jsonp requests\njQuery.ajaxPrefilter( \"json jsonp\", function( s, originalSettings, jqXHR ) {\n\n\tvar callbackName, overwritten, responseContainer,\n\t\tjsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?\n\t\t\t\"url\" :\n\t\t\ttypeof s.data === \"string\" &&\n\t\t\t\t( s.contentType || \"\" )\n\t\t\t\t\t.indexOf( \"application/x-www-form-urlencoded\" ) === 0 &&\n\t\t\t\trjsonp.test( s.data ) && \"data\"\n\t\t);\n\n\t// Handle iff the expected data type is \"jsonp\" or we have a parameter to set\n\tif ( jsonProp || s.dataTypes[ 0 ] === \"jsonp\" ) {\n\n\t\t// Get callback name, remembering preexisting value associated with it\n\t\tcallbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?\n\t\t\ts.jsonpCallback() :\n\t\t\ts.jsonpCallback;\n\n\t\t// Insert callback into url or form data\n\t\tif ( jsonProp ) {\n\t\t\ts[ jsonProp ] = s[ jsonProp ].replace( rjsonp, \"$1\" + callbackName );\n\t\t} else if ( s.jsonp !== false ) {\n\t\t\ts.url += ( rquery.test( s.url ) ? \"&\" : \"?\" ) + s.jsonp + \"=\" + callbackName;\n\t\t}\n\n\t\t// Use data converter to retrieve json after script execution\n\t\ts.converters[ \"script json\" ] = function() {\n\t\t\tif ( !responseContainer ) {\n\t\t\t\tjQuery.error( callbackName + \" was not called\" );\n\t\t\t}\n\t\t\treturn responseContainer[ 0 ];\n\t\t};\n\n\t\t// Force json dataType\n\t\ts.dataTypes[ 0 ] = \"json\";\n\n\t\t// Install callback\n\t\toverwritten = window[ callbackName ];\n\t\twindow[ callbackName ] = function() {\n\t\t\tresponseContainer = arguments;\n\t\t};\n\n\t\t// Clean-up function (fires after converters)\n\t\tjqXHR.always( function() {\n\n\t\t\t// If previous value didn't exist - remove it\n\t\t\tif ( overwritten === undefined ) {\n\t\t\t\tjQuery( window ).removeProp( callbackName );\n\n\t\t\t// Otherwise restore preexisting value\n\t\t\t} else {\n\t\t\t\twindow[ callbackName ] = overwritten;\n\t\t\t}\n\n\t\t\t// Save back as free\n\t\t\tif ( s[ callbackName ] ) {\n\n\t\t\t\t// Make sure that re-using the options doesn't screw things around\n\t\t\t\ts.jsonpCallback = originalSettings.jsonpCallback;\n\n\t\t\t\t// Save the callback name for future use\n\t\t\t\toldCallbacks.push( callbackName );\n\t\t\t}\n\n\t\t\t// Call if it was a function and we have a response\n\t\t\tif ( responseContainer && jQuery.isFunction( overwritten ) ) {\n\t\t\t\toverwritten( responseContainer[ 0 ] );\n\t\t\t}\n\n\t\t\tresponseContainer = overwritten = undefined;\n\t\t} );\n\n\t\t// Delegate to script\n\t\treturn \"script\";\n\t}\n} );\n\n\n\n\n// Support: Safari 8 only\n// In Safari 8 documents created via document.implementation.createHTMLDocument\n// collapse sibling forms: the second one becomes a child of the first one.\n// Because of that, this security measure has to be disabled in Safari 8.\n// https://bugs.webkit.org/show_bug.cgi?id=137337\nsupport.createHTMLDocument = ( function() {\n\tvar body = document.implementation.createHTMLDocument( \"\" ).body;\n\tbody.innerHTML = \"<form></form><form></form>\";\n\treturn body.childNodes.length === 2;\n} )();\n\n\n// Argument \"data\" should be string of html\n// context (optional): If specified, the fragment will be created in this context,\n// defaults to document\n// keepScripts (optional): If true, will include scripts passed in the html string\njQuery.parseHTML = function( data, context, keepScripts ) {\n\tif ( typeof data !== \"string\" ) {\n\t\treturn [];\n\t}\n\tif ( typeof context === \"boolean\" ) {\n\t\tkeepScripts = context;\n\t\tcontext = false;\n\t}\n\n\tvar base, parsed, scripts;\n\n\tif ( !context ) {\n\n\t\t// Stop scripts or inline event handlers from being executed immediately\n\t\t// by using document.implementation\n\t\tif ( support.createHTMLDocument ) {\n\t\t\tcontext = document.implementation.createHTMLDocument( \"\" );\n\n\t\t\t// Set the base href for the created document\n\t\t\t// so any parsed elements with URLs\n\t\t\t// are based on the document's URL (gh-2965)\n\t\t\tbase = context.createElement( \"base\" );\n\t\t\tbase.href = document.location.href;\n\t\t\tcontext.head.appendChild( base );\n\t\t} else {\n\t\t\tcontext = document;\n\t\t}\n\t}\n\n\tparsed = rsingleTag.exec( data );\n\tscripts = !keepScripts && [];\n\n\t// Single tag\n\tif ( parsed ) {\n\t\treturn [ context.createElement( parsed[ 1 ] ) ];\n\t}\n\n\tparsed = buildFragment( [ data ], context, scripts );\n\n\tif ( scripts && scripts.length ) {\n\t\tjQuery( scripts ).remove();\n\t}\n\n\treturn jQuery.merge( [], parsed.childNodes );\n};\n\n\n/**\n * Load a url into a page\n */\njQuery.fn.load = function( url, params, callback ) {\n\tvar selector, type, response,\n\t\tself = this,\n\t\toff = url.indexOf( \" \" );\n\n\tif ( off > -1 ) {\n\t\tselector = stripAndCollapse( url.slice( off ) );\n\t\turl = url.slice( 0, off );\n\t}\n\n\t// If it's a function\n\tif ( jQuery.isFunction( params ) ) {\n\n\t\t// We assume that it's the callback\n\t\tcallback = params;\n\t\tparams = undefined;\n\n\t// Otherwise, build a param string\n\t} else if ( params && typeof params === \"object\" ) {\n\t\ttype = \"POST\";\n\t}\n\n\t// If we have elements to modify, make the request\n\tif ( self.length > 0 ) {\n\t\tjQuery.ajax( {\n\t\t\turl: url,\n\n\t\t\t// If \"type\" variable is undefined, then \"GET\" method will be used.\n\t\t\t// Make value of this field explicit since\n\t\t\t// user can override it through ajaxSetup method\n\t\t\ttype: type || \"GET\",\n\t\t\tdataType: \"html\",\n\t\t\tdata: params\n\t\t} ).done( function( responseText ) {\n\n\t\t\t// Save response for use in complete callback\n\t\t\tresponse = arguments;\n\n\t\t\tself.html( selector ?\n\n\t\t\t\t// If a selector was specified, locate the right elements in a dummy div\n\t\t\t\t// Exclude scripts to avoid IE 'Permission Denied' errors\n\t\t\t\tjQuery( \"<div>\" ).append( jQuery.parseHTML( responseText ) ).find( selector ) :\n\n\t\t\t\t// Otherwise use the full result\n\t\t\t\tresponseText );\n\n\t\t// If the request succeeds, this function gets \"data\", \"status\", \"jqXHR\"\n\t\t// but they are ignored because response was set above.\n\t\t// If it fails, this function gets \"jqXHR\", \"status\", \"error\"\n\t\t} ).always( callback && function( jqXHR, status ) {\n\t\t\tself.each( function() {\n\t\t\t\tcallback.apply( this, response || [ jqXHR.responseText, status, jqXHR ] );\n\t\t\t} );\n\t\t} );\n\t}\n\n\treturn this;\n};\n\n\n\n\n// Attach a bunch of functions for handling common AJAX events\njQuery.each( [\n\t\"ajaxStart\",\n\t\"ajaxStop\",\n\t\"ajaxComplete\",\n\t\"ajaxError\",\n\t\"ajaxSuccess\",\n\t\"ajaxSend\"\n], function( i, type ) {\n\tjQuery.fn[ type ] = function( fn ) {\n\t\treturn this.on( type, fn );\n\t};\n} );\n\n\n\n\njQuery.expr.pseudos.animated = function( elem ) {\n\treturn jQuery.grep( jQuery.timers, function( fn ) {\n\t\treturn elem === fn.elem;\n\t} ).length;\n};\n\n\n\n\njQuery.offset = {\n\tsetOffset: function( elem, options, i ) {\n\t\tvar curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,\n\t\t\tposition = jQuery.css( elem, \"position\" ),\n\t\t\tcurElem = jQuery( elem ),\n\t\t\tprops = {};\n\n\t\t// Set position first, in-case top/left are set even on static elem\n\t\tif ( position === \"static\" ) {\n\t\t\telem.style.position = \"relative\";\n\t\t}\n\n\t\tcurOffset = curElem.offset();\n\t\tcurCSSTop = jQuery.css( elem, \"top\" );\n\t\tcurCSSLeft = jQuery.css( elem, \"left\" );\n\t\tcalculatePosition = ( position === \"absolute\" || position === \"fixed\" ) &&\n\t\t\t( curCSSTop + curCSSLeft ).indexOf( \"auto\" ) > -1;\n\n\t\t// Need to be able to calculate position if either\n\t\t// top or left is auto and position is either absolute or fixed\n\t\tif ( calculatePosition ) {\n\t\t\tcurPosition = curElem.position();\n\t\t\tcurTop = curPosition.top;\n\t\t\tcurLeft = curPosition.left;\n\n\t\t} else {\n\t\t\tcurTop = parseFloat( curCSSTop ) || 0;\n\t\t\tcurLeft = parseFloat( curCSSLeft ) || 0;\n\t\t}\n\n\t\tif ( jQuery.isFunction( options ) ) {\n\n\t\t\t// Use jQuery.extend here to allow modification of coordinates argument (gh-1848)\n\t\t\toptions = options.call( elem, i, jQuery.extend( {}, curOffset ) );\n\t\t}\n\n\t\tif ( options.top != null ) {\n\t\t\tprops.top = ( options.top - curOffset.top ) + curTop;\n\t\t}\n\t\tif ( options.left != null ) {\n\t\t\tprops.left = ( options.left - curOffset.left ) + curLeft;\n\t\t}\n\n\t\tif ( \"using\" in options ) {\n\t\t\toptions.using.call( elem, props );\n\n\t\t} else {\n\t\t\tcurElem.css( props );\n\t\t}\n\t}\n};\n\njQuery.fn.extend( {\n\toffset: function( options ) {\n\n\t\t// Preserve chaining for setter\n\t\tif ( arguments.length ) {\n\t\t\treturn options === undefined ?\n\t\t\t\tthis :\n\t\t\t\tthis.each( function( i ) {\n\t\t\t\t\tjQuery.offset.setOffset( this, options, i );\n\t\t\t\t} );\n\t\t}\n\n\t\tvar doc, docElem, rect, win,\n\t\t\telem = this[ 0 ];\n\n\t\tif ( !elem ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Return zeros for disconnected and hidden (display: none) elements (gh-2310)\n\t\t// Support: IE <=11 only\n\t\t// Running getBoundingClientRect on a\n\t\t// disconnected node in IE throws an error\n\t\tif ( !elem.getClientRects().length ) {\n\t\t\treturn { top: 0, left: 0 };\n\t\t}\n\n\t\trect = elem.getBoundingClientRect();\n\n\t\tdoc = elem.ownerDocument;\n\t\tdocElem = doc.documentElement;\n\t\twin = doc.defaultView;\n\n\t\treturn {\n\t\t\ttop: rect.top + win.pageYOffset - docElem.clientTop,\n\t\t\tleft: rect.left + win.pageXOffset - docElem.clientLeft\n\t\t};\n\t},\n\n\tposition: function() {\n\t\tif ( !this[ 0 ] ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar offsetParent, offset,\n\t\t\telem = this[ 0 ],\n\t\t\tparentOffset = { top: 0, left: 0 };\n\n\t\t// Fixed elements are offset from window (parentOffset = {top:0, left: 0},\n\t\t// because it is its only offset parent\n\t\tif ( jQuery.css( elem, \"position\" ) === \"fixed\" ) {\n\n\t\t\t// Assume getBoundingClientRect is there when computed position is fixed\n\t\t\toffset = elem.getBoundingClientRect();\n\n\t\t} else {\n\n\t\t\t// Get *real* offsetParent\n\t\t\toffsetParent = this.offsetParent();\n\n\t\t\t// Get correct offsets\n\t\t\toffset = this.offset();\n\t\t\tif ( !nodeName( offsetParent[ 0 ], \"html\" ) ) {\n\t\t\t\tparentOffset = offsetParent.offset();\n\t\t\t}\n\n\t\t\t// Add offsetParent borders\n\t\t\tparentOffset = {\n\t\t\t\ttop: parentOffset.top + jQuery.css( offsetParent[ 0 ], \"borderTopWidth\", true ),\n\t\t\t\tleft: parentOffset.left + jQuery.css( offsetParent[ 0 ], \"borderLeftWidth\", true )\n\t\t\t};\n\t\t}\n\n\t\t// Subtract parent offsets and element margins\n\t\treturn {\n\t\t\ttop: offset.top - parentOffset.top - jQuery.css( elem, \"marginTop\", true ),\n\t\t\tleft: offset.left - parentOffset.left - jQuery.css( elem, \"marginLeft\", true )\n\t\t};\n\t},\n\n\t// This method will return documentElement in the following cases:\n\t// 1) For the element inside the iframe without offsetParent, this method will return\n\t//    documentElement of the parent window\n\t// 2) For the hidden or detached element\n\t// 3) For body or html element, i.e. in case of the html node - it will return itself\n\t//\n\t// but those exceptions were never presented as a real life use-cases\n\t// and might be considered as more preferable results.\n\t//\n\t// This logic, however, is not guaranteed and can change at any point in the future\n\toffsetParent: function() {\n\t\treturn this.map( function() {\n\t\t\tvar offsetParent = this.offsetParent;\n\n\t\t\twhile ( offsetParent && jQuery.css( offsetParent, \"position\" ) === \"static\" ) {\n\t\t\t\toffsetParent = offsetParent.offsetParent;\n\t\t\t}\n\n\t\t\treturn offsetParent || documentElement;\n\t\t} );\n\t}\n} );\n\n// Create scrollLeft and scrollTop methods\njQuery.each( { scrollLeft: \"pageXOffset\", scrollTop: \"pageYOffset\" }, function( method, prop ) {\n\tvar top = \"pageYOffset\" === prop;\n\n\tjQuery.fn[ method ] = function( val ) {\n\t\treturn access( this, function( elem, method, val ) {\n\n\t\t\t// Coalesce documents and windows\n\t\t\tvar win;\n\t\t\tif ( jQuery.isWindow( elem ) ) {\n\t\t\t\twin = elem;\n\t\t\t} else if ( elem.nodeType === 9 ) {\n\t\t\t\twin = elem.defaultView;\n\t\t\t}\n\n\t\t\tif ( val === undefined ) {\n\t\t\t\treturn win ? win[ prop ] : elem[ method ];\n\t\t\t}\n\n\t\t\tif ( win ) {\n\t\t\t\twin.scrollTo(\n\t\t\t\t\t!top ? val : win.pageXOffset,\n\t\t\t\t\ttop ? val : win.pageYOffset\n\t\t\t\t);\n\n\t\t\t} else {\n\t\t\t\telem[ method ] = val;\n\t\t\t}\n\t\t}, method, val, arguments.length );\n\t};\n} );\n\n// Support: Safari <=7 - 9.1, Chrome <=37 - 49\n// Add the top/left cssHooks using jQuery.fn.position\n// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084\n// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347\n// getComputedStyle returns percent when specified for top/left/bottom/right;\n// rather than make the css module depend on the offset module, just check for it here\njQuery.each( [ \"top\", \"left\" ], function( i, prop ) {\n\tjQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,\n\t\tfunction( elem, computed ) {\n\t\t\tif ( computed ) {\n\t\t\t\tcomputed = curCSS( elem, prop );\n\n\t\t\t\t// If curCSS returns percentage, fallback to offset\n\t\t\t\treturn rnumnonpx.test( computed ) ?\n\t\t\t\t\tjQuery( elem ).position()[ prop ] + \"px\" :\n\t\t\t\t\tcomputed;\n\t\t\t}\n\t\t}\n\t);\n} );\n\n\n// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods\njQuery.each( { Height: \"height\", Width: \"width\" }, function( name, type ) {\n\tjQuery.each( { padding: \"inner\" + name, content: type, \"\": \"outer\" + name },\n\t\tfunction( defaultExtra, funcName ) {\n\n\t\t// Margin is only for outerHeight, outerWidth\n\t\tjQuery.fn[ funcName ] = function( margin, value ) {\n\t\t\tvar chainable = arguments.length && ( defaultExtra || typeof margin !== \"boolean\" ),\n\t\t\t\textra = defaultExtra || ( margin === true || value === true ? \"margin\" : \"border\" );\n\n\t\t\treturn access( this, function( elem, type, value ) {\n\t\t\t\tvar doc;\n\n\t\t\t\tif ( jQuery.isWindow( elem ) ) {\n\n\t\t\t\t\t// $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)\n\t\t\t\t\treturn funcName.indexOf( \"outer\" ) === 0 ?\n\t\t\t\t\t\telem[ \"inner\" + name ] :\n\t\t\t\t\t\telem.document.documentElement[ \"client\" + name ];\n\t\t\t\t}\n\n\t\t\t\t// Get document width or height\n\t\t\t\tif ( elem.nodeType === 9 ) {\n\t\t\t\t\tdoc = elem.documentElement;\n\n\t\t\t\t\t// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],\n\t\t\t\t\t// whichever is greatest\n\t\t\t\t\treturn Math.max(\n\t\t\t\t\t\telem.body[ \"scroll\" + name ], doc[ \"scroll\" + name ],\n\t\t\t\t\t\telem.body[ \"offset\" + name ], doc[ \"offset\" + name ],\n\t\t\t\t\t\tdoc[ \"client\" + name ]\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn value === undefined ?\n\n\t\t\t\t\t// Get width or height on the element, requesting but not forcing parseFloat\n\t\t\t\t\tjQuery.css( elem, type, extra ) :\n\n\t\t\t\t\t// Set width or height on the element\n\t\t\t\t\tjQuery.style( elem, type, value, extra );\n\t\t\t}, type, chainable ? margin : undefined, chainable );\n\t\t};\n\t} );\n} );\n\n\njQuery.fn.extend( {\n\n\tbind: function( types, data, fn ) {\n\t\treturn this.on( types, null, data, fn );\n\t},\n\tunbind: function( types, fn ) {\n\t\treturn this.off( types, null, fn );\n\t},\n\n\tdelegate: function( selector, types, data, fn ) {\n\t\treturn this.on( types, selector, data, fn );\n\t},\n\tundelegate: function( selector, types, fn ) {\n\n\t\t// ( namespace ) or ( selector, types [, fn] )\n\t\treturn arguments.length === 1 ?\n\t\t\tthis.off( selector, \"**\" ) :\n\t\t\tthis.off( types, selector || \"**\", fn );\n\t}\n} );\n\njQuery.holdReady = function( hold ) {\n\tif ( hold ) {\n\t\tjQuery.readyWait++;\n\t} else {\n\t\tjQuery.ready( true );\n\t}\n};\njQuery.isArray = Array.isArray;\njQuery.parseJSON = JSON.parse;\njQuery.nodeName = nodeName;\n\n\n\n\n// Register as a named AMD module, since jQuery can be concatenated with other\n// files that may use define, but not via a proper concatenation script that\n// understands anonymous AMD modules. A named AMD is safest and most robust\n// way to register. Lowercase jquery is used because AMD module names are\n// derived from file names, and jQuery is normally delivered in a lowercase\n// file name. Do this after creating the global so that if an AMD module wants\n// to call noConflict to hide this version of jQuery, it will work.\n\n// Note that for maximum portability, libraries that are not jQuery should\n// declare themselves as anonymous modules, and avoid setting a global if an\n// AMD loader is present. jQuery is a special case. For more information, see\n// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon\n\nif ( typeof define === \"function\" && define.amd ) {\n\tdefine( \"jquery\", [], function() {\n\t\treturn jQuery;\n\t} );\n}\n\n\n\n\nvar\n\n\t// Map over jQuery in case of overwrite\n\t_jQuery = window.jQuery,\n\n\t// Map over the $ in case of overwrite\n\t_$ = window.$;\n\njQuery.noConflict = function( deep ) {\n\tif ( window.$ === jQuery ) {\n\t\twindow.$ = _$;\n\t}\n\n\tif ( deep && window.jQuery === jQuery ) {\n\t\twindow.jQuery = _jQuery;\n\t}\n\n\treturn jQuery;\n};\n\n// Expose jQuery and $ identifiers, even in AMD\n// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)\n// and CommonJS for browser emulators (#13566)\nif ( !noGlobal ) {\n\twindow.jQuery = window.$ = jQuery;\n}\n\n\n\n\nreturn jQuery;\n} );\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery.js",
    "content": "/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */\n!function(a,b){\"use strict\";\"object\"==typeof module&&\"object\"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error(\"jQuery requires a window with a document\");return b(a)}:b(a)}(\"undefined\"!=typeof window?window:this,function(a,b){\"use strict\";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement(\"script\");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q=\"3.2.1\",r=function(a,b){return new r.fn.init(a,b)},s=/^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for(\"boolean\"==typeof g&&(j=g,g=arguments[h]||{},h++),\"object\"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=Array.isArray(d)))?(e?(e=!1,f=c&&Array.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:\"jQuery\"+(q+Math.random()).replace(/\\D/g,\"\"),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return\"function\"===r.type(a)},isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return(\"number\"===b||\"string\"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||\"[object Object]\"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,\"constructor\")&&b.constructor,\"function\"==typeof c&&m.call(c)===n))},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+\"\":\"object\"==typeof a||\"function\"==typeof a?j[k.call(a)]||\"object\":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,\"ms-\").replace(u,v)},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?\"\":(a+\"\").replace(s,\"\")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,\"string\"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if(\"string\"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),\"function\"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each(\"Boolean Number String Function Array Date RegExp Object Error Symbol\".split(\" \"),function(a,b){j[\"[object \"+b+\"]\"]=b.toLowerCase()});function w(a){var b=!!a&&\"length\"in a&&a.length,c=r.type(a);return\"function\"!==c&&!r.isWindow(a)&&(\"array\"===c||0===b||\"number\"==typeof b&&b>0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u=\"sizzle\"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J=\"checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped\",K=\"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\",L=\"(?:\\\\\\\\.|[\\\\w-]|[^\\0-\\\\xa0])+\",M=\"\\\\[\"+K+\"*(\"+L+\")(?:\"+K+\"*([*^$|!~]?=)\"+K+\"*(?:'((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\"|(\"+L+\"))|)\"+K+\"*\\\\]\",N=\":(\"+L+\")(?:\\\\((('((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\")|((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\"+M+\")*)|.*)\\\\)|)\",O=new RegExp(K+\"+\",\"g\"),P=new RegExp(\"^\"+K+\"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\"+K+\"+$\",\"g\"),Q=new RegExp(\"^\"+K+\"*,\"+K+\"*\"),R=new RegExp(\"^\"+K+\"*([>+~]|\"+K+\")\"+K+\"*\"),S=new RegExp(\"=\"+K+\"*([^\\\\]'\\\"]*?)\"+K+\"*\\\\]\",\"g\"),T=new RegExp(N),U=new RegExp(\"^\"+L+\"$\"),V={ID:new RegExp(\"^#(\"+L+\")\"),CLASS:new RegExp(\"^\\\\.(\"+L+\")\"),TAG:new RegExp(\"^(\"+L+\"|[*])\"),ATTR:new RegExp(\"^\"+M),PSEUDO:new RegExp(\"^\"+N),CHILD:new RegExp(\"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\"+K+\"*(even|odd|(([+-]|)(\\\\d*)n|)\"+K+\"*(?:([+-]|)\"+K+\"*(\\\\d+)|))\"+K+\"*\\\\)|)\",\"i\"),bool:new RegExp(\"^(?:\"+J+\")$\",\"i\"),needsContext:new RegExp(\"^\"+K+\"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\"+K+\"*((?:-\\\\d)?\\\\d*)\"+K+\"*\\\\)|)(?=[^-]|$)\",\"i\")},W=/^(?:input|select|textarea|button)$/i,X=/^h\\d$/i,Y=/^[^{]+\\{\\s*\\[native \\w/,Z=/^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,$=/[+~]/,_=new RegExp(\"\\\\\\\\([\\\\da-f]{1,6}\"+K+\"?|(\"+K+\")|.)\",\"ig\"),aa=function(a,b,c){var d=\"0x\"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\0-\\x1f\\x7f-\\uFFFF\\w-]/g,ca=function(a,b){return b?\"\\0\"===a?\"\\ufffd\":a.slice(0,-1)+\"\\\\\"+a.charCodeAt(a.length-1).toString(16)+\" \":\"\\\\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&(\"form\"in a||\"label\"in a)},{dir:\"parentNode\",next:\"legend\"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],\"string\"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+\" \"]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if(\"object\"!==b.nodeName.toLowerCase()){(k=b.getAttribute(\"id\"))?k=k.replace(ba,ca):b.setAttribute(\"id\",k=u),o=g(a),h=o.length;while(h--)o[h]=\"#\"+k+\" \"+sa(o[h]);r=o.join(\",\"),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute(\"id\")}}}return i(a.replace(P,\"$1\"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+\" \")>d.cacheLength&&delete b[a.shift()],b[c+\" \"]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement(\"fieldset\");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split(\"|\"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return\"input\"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return(\"input\"===c||\"button\"===c)&&b.type===a}}function oa(a){return function(b){return\"form\"in b?b.parentNode&&b.disabled===!1?\"label\"in b?\"label\"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:\"label\"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&\"undefined\"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&\"HTML\"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener(\"unload\",da,!1):e.attachEvent&&e.attachEvent(\"onunload\",da)),c.attributes=ja(function(a){return a.className=\"i\",!a.getAttribute(\"className\")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment(\"\")),!a.getElementsByTagName(\"*\").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute(\"id\")===b}},d.find.ID=function(a,b){if(\"undefined\"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c=\"undefined\"!=typeof a.getAttributeNode&&a.getAttributeNode(\"id\");return c&&c.value===b}},d.find.ID=function(a,b){if(\"undefined\"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode(\"id\"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode(\"id\"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return\"undefined\"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if(\"*\"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if(\"undefined\"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML=\"<a id='\"+u+\"'></a><select id='\"+u+\"-\\r\\\\' msallowcapture=''><option selected=''></option></select>\",a.querySelectorAll(\"[msallowcapture^='']\").length&&q.push(\"[*^$]=\"+K+\"*(?:''|\\\"\\\")\"),a.querySelectorAll(\"[selected]\").length||q.push(\"\\\\[\"+K+\"*(?:value|\"+J+\")\"),a.querySelectorAll(\"[id~=\"+u+\"-]\").length||q.push(\"~=\"),a.querySelectorAll(\":checked\").length||q.push(\":checked\"),a.querySelectorAll(\"a#\"+u+\"+*\").length||q.push(\".#.+[+~]\")}),ja(function(a){a.innerHTML=\"<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>\";var b=n.createElement(\"input\");b.setAttribute(\"type\",\"hidden\"),a.appendChild(b).setAttribute(\"name\",\"D\"),a.querySelectorAll(\"[name=d]\").length&&q.push(\"name\"+K+\"*[*^$|!~]?=\"),2!==a.querySelectorAll(\":enabled\").length&&q.push(\":enabled\",\":disabled\"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(\":disabled\").length&&q.push(\":enabled\",\":disabled\"),a.querySelectorAll(\"*,:x\"),q.push(\",.*:\")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,\"*\"),s.call(a,\"[s!='']:x\"),r.push(\"!=\",N)}),q=q.length&&new RegExp(q.join(\"|\")),r=r.length&&new RegExp(r.join(\"|\")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,\"='$1']\"),c.matchesSelector&&p&&!A[b+\" \"]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+\"\").replace(ba,ca)},ga.error=function(a){throw new Error(\"Syntax error, unrecognized expression: \"+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c=\"\",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if(\"string\"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{\">\":{dir:\"parentNode\",first:!0},\" \":{dir:\"parentNode\"},\"+\":{dir:\"previousSibling\",first:!0},\"~\":{dir:\"previousSibling\"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||\"\").replace(_,aa),\"~=\"===a[2]&&(a[3]=\" \"+a[3]+\" \"),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),\"nth\"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*(\"even\"===a[3]||\"odd\"===a[3])),a[5]=+(a[7]+a[8]||\"odd\"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||\"\":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(\")\",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return\"*\"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+\" \"];return b||(b=new RegExp(\"(^|\"+K+\")\"+a+\"(\"+K+\"|$)\"))&&y(a,function(a){return b.test(\"string\"==typeof a.className&&a.className||\"undefined\"!=typeof a.getAttribute&&a.getAttribute(\"class\")||\"\")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?\"!=\"===b:!b||(e+=\"\",\"=\"===b?e===c:\"!=\"===b?e!==c:\"^=\"===b?c&&0===e.indexOf(c):\"*=\"===b?c&&e.indexOf(c)>-1:\"$=\"===b?c&&e.slice(-c.length)===c:\"~=\"===b?(\" \"+e.replace(O,\" \")+\" \").indexOf(c)>-1:\"|=\"===b&&(e===c||e.slice(0,c.length+1)===c+\"-\"))}},CHILD:function(a,b,c,d,e){var f=\"nth\"!==a.slice(0,3),g=\"last\"!==a.slice(-4),h=\"of-type\"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?\"nextSibling\":\"previousSibling\",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p=\"only\"===a&&!o&&\"nextSibling\"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error(\"unsupported pseudo: \"+a);return e[u]?e(b):e.length>1?(c=[a,a,\"\",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,\"$1\"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||\"\")||ga.error(\"unsupported lang: \"+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute(\"xml:lang\")||b.getAttribute(\"lang\"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+\"-\");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return\"input\"===b&&!!a.checked||\"option\"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return\"input\"===b&&\"button\"===a.type||\"button\"===b},text:function(a){var b;return\"input\"===a.nodeName.toLowerCase()&&\"text\"===a.type&&(null==(b=a.getAttribute(\"type\"))||\"text\"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+\" \"];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P,\" \")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d=\"\";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&\"parentNode\"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e);return!1}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}return!1}}function ua(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||\"*\",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[\" \"],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:\" \"===a[i-2].type?\"*\":\"\"})).replace(P,\"$1\"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s=\"0\",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG(\"*\",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+\" \"];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m=\"function\"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&\"ID\"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split(\"\").sort(B).join(\"\")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement(\"fieldset\"))}),ja(function(a){return a.innerHTML=\"<a href='#'></a>\",\"#\"===a.firstChild.getAttribute(\"href\")})||ka(\"type|href|height|width\",function(a,b,c){if(!c)return a.getAttribute(b,\"type\"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML=\"<input/>\",a.firstChild.setAttribute(\"value\",\"\"),\"\"===a.firstChild.getAttribute(\"value\")})||ka(\"value\",function(a,b,c){if(!c&&\"input\"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute(\"disabled\")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[\":\"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i,D=/^.[^:#\\[\\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):\"string\"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=\":not(\"+a+\")\"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if(\"string\"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,\"string\"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,\"string\"==typeof a){if(e=\"<\"===a[0]&&\">\"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g=\"string\"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?\"string\"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,\"parentNode\")},parentsUntil:function(a,b,c){return y(a,\"parentNode\",c)},next:function(a){return K(a,\"nextSibling\")},prev:function(a){return K(a,\"previousSibling\")},nextAll:function(a){return y(a,\"nextSibling\")},prevAll:function(a){return y(a,\"previousSibling\")},nextUntil:function(a,b,c){return y(a,\"nextSibling\",c)},prevUntil:function(a,b,c){return y(a,\"previousSibling\",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,\"iframe\")?a.contentDocument:(B(a,\"template\")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return\"Until\"!==a.slice(-5)&&(d=c),d&&\"string\"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\\x20\\t\\r\\n\\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a=\"string\"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:\"\")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&\"string\"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c=\"\",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=\"\"),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[[\"notify\",\"progress\",r.Callbacks(\"memory\"),r.Callbacks(\"memory\"),2],[\"resolve\",\"done\",r.Callbacks(\"once memory\"),r.Callbacks(\"once memory\"),0,\"resolved\"],[\"reject\",\"fail\",r.Callbacks(\"once memory\"),r.Callbacks(\"once memory\"),1,\"rejected\"]],d=\"pending\",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},\"catch\":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+\"With\"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError(\"Thenable self-resolution\");j=a&&(\"object\"==typeof a||\"function\"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,N,e),g(f,c,O,e)):(f++,j.call(a,g(f,c,N,e),g(f,c,O,e),g(f,c,N,c.notifyWith))):(d!==N&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+\"With\"](this===f?void 0:this,arguments),this},f[b[0]+\"With\"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),\"pending\"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn(\"jQuery.Deferred exception: \"+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)[\"catch\"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener(\"DOMContentLoaded\",S),\na.removeEventListener(\"load\",S),r.ready()}\"complete\"===d.readyState||\"loading\"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener(\"DOMContentLoaded\",S),a.addEventListener(\"load\",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if(\"object\"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},U=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function V(){this.expando=r.expando+V.uid++}V.uid=1,V.prototype={cache:function(a){var b=a[this.expando];return b||(b={},U(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if(\"string\"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&\"string\"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){Array.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(L)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var W=new V,X=new V,Y=/^(?:\\{[\\w\\W]*\\}|\\[[\\w\\W]*\\])$/,Z=/[A-Z]/g;function $(a){return\"true\"===a||\"false\"!==a&&(\"null\"===a?null:a===+a+\"\"?+a:Y.test(a)?JSON.parse(a):a)}function _(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d=\"data-\"+b.replace(Z,\"-$&\").toLowerCase(),c=a.getAttribute(d),\"string\"==typeof c){try{c=$(c)}catch(e){}X.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return X.hasData(a)||W.hasData(a)},data:function(a,b,c){return X.access(a,b,c)},removeData:function(a,b){X.remove(a,b)},_data:function(a,b,c){return W.access(a,b,c)},_removeData:function(a,b){W.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=X.get(f),1===f.nodeType&&!W.get(f,\"hasDataAttrs\"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf(\"data-\")&&(d=r.camelCase(d.slice(5)),_(f,d,e[d])));W.set(f,\"hasDataAttrs\",!0)}return e}return\"object\"==typeof a?this.each(function(){X.set(this,a)}):T(this,function(b){var c;if(f&&void 0===b){if(c=X.get(f,a),void 0!==c)return c;if(c=_(f,a),void 0!==c)return c}else this.each(function(){X.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||\"fx\")+\"queue\",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||\"fx\";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};\"inprogress\"===e&&(e=c.shift(),d--),e&&(\"fx\"===b&&c.unshift(\"inprogress\"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+\"queueHooks\";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks(\"once memory\").add(function(){W.remove(a,[b+\"queue\",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return\"string\"!=typeof a&&(b=a,a=\"fx\",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),\"fx\"===a&&\"inprogress\"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||\"fx\",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};\"string\"!=typeof a&&(b=a,a=void 0),a=a||\"fx\";while(g--)c=W.get(f[g],a+\"queueHooks\"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var aa=/[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/.source,ba=new RegExp(\"^(?:([+-])=|)(\"+aa+\")([a-z%]*)$\",\"i\"),ca=[\"Top\",\"Right\",\"Bottom\",\"Left\"],da=function(a,b){return a=b||a,\"none\"===a.style.display||\"\"===a.style.display&&r.contains(a.ownerDocument,a)&&\"none\"===r.css(a,\"display\")},ea=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function fa(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,\"\")},i=h(),j=c&&c[3]||(r.cssNumber[b]?\"\":\"px\"),k=(r.cssNumber[b]||\"px\"!==j&&+i)&&ba.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||\".5\",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var ga={};function ha(a){var b,c=a.ownerDocument,d=a.nodeName,e=ga[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,\"display\"),b.parentNode.removeChild(b),\"none\"===e&&(e=\"block\"),ga[d]=e,e)}function ia(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?(\"none\"===c&&(e[f]=W.get(d,\"display\")||null,e[f]||(d.style.display=\"\")),\"\"===d.style.display&&da(d)&&(e[f]=ha(d))):\"none\"!==c&&(e[f]=\"none\",W.set(d,\"display\",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ia(this,!0)},hide:function(){return ia(this)},toggle:function(a){return\"boolean\"==typeof a?a?this.show():this.hide():this.each(function(){da(this)?r(this).show():r(this).hide()})}});var ja=/^(?:checkbox|radio)$/i,ka=/<([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]+)/i,la=/^$|\\/(?:java|ecma)script/i,ma={option:[1,\"<select multiple='multiple'>\",\"</select>\"],thead:[1,\"<table>\",\"</table>\"],col:[2,\"<table><colgroup>\",\"</colgroup></table>\"],tr:[2,\"<table><tbody>\",\"</tbody></table>\"],td:[3,\"<table><tbody><tr>\",\"</tr></tbody></table>\"],_default:[0,\"\",\"\"]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c=\"undefined\"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||\"*\"):\"undefined\"!=typeof a.querySelectorAll?a.querySelectorAll(b||\"*\"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c<d;c++)W.set(a[c],\"globalEval\",!b||W.get(b[c],\"globalEval\"))}var pa=/<|&#?\\w+;/;function qa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if(\"object\"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(pa.test(f)){g=g||l.appendChild(b.createElement(\"div\")),h=(ka.exec(f)||[\"\",\"\"])[1].toLowerCase(),i=ma[h]||ma._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=\"\"}else m.push(b.createTextNode(f));l.textContent=\"\",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),\"script\"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||\"\")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement(\"div\")),c=d.createElement(\"input\");c.setAttribute(\"type\",\"radio\"),c.setAttribute(\"checked\",\"checked\"),c.setAttribute(\"name\",\"t\"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML=\"<textarea>x</textarea>\",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if(\"object\"==typeof b){\"string\"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&(\"string\"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return\"undefined\"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||\"\").match(L)||[\"\"],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||\"\").split(\".\").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(\".\")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||\"\").match(L)||[\"\"],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||\"\").split(\".\").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp(\"(^|\\\\.)\"+o.join(\"\\\\.(?:.*\\\\.|)\")+\"(\\\\.|$)\"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&(\"**\"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,\"handle events\")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,\"events\")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!(\"click\"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&(\"click\"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+\" \",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==xa()&&this.focus)return this.focus(),!1},delegateType:\"focusin\"},blur:{trigger:function(){if(this===xa()&&this.blur)return this.blur(),!1},delegateType:\"focusout\"},click:{trigger:function(){if(\"checkbox\"===this.type&&this.click&&B(this,\"input\"))return this.click(),!1},_default:function(a){return B(a.target,\"a\")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?va:wa,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:wa,isPropagationStopped:wa,isImmediatePropagationStopped:wa,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=va,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=va,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=va,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,\"char\":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&sa.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&ta.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:\"mouseover\",mouseleave:\"mouseout\",pointerenter:\"pointerover\",pointerleave:\"pointerout\"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return ya(this,a,b,c,d)},one:function(a,b,c,d){return ya(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+\".\"+d.namespace:d.origType,d.selector,d.handler),this;if(\"object\"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&\"function\"!=typeof b||(c=b,b=void 0),c===!1&&(c=wa),this.each(function(){r.event.remove(this,a,c,b)})}});var za=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]*)[^>]*)\\/>/gi,Aa=/<script|<style|<link/i,Ba=/checked\\s*(?:[^=]|=\\s*.checked.)/i,Ca=/^true\\/(.*)/,Da=/^\\s*<!(?:\\[CDATA\\[|--)|(?:\\]\\]|--)>\\s*$/g;function Ea(a,b){return B(a,\"table\")&&B(11!==b.nodeType?b:b.firstChild,\"tr\")?r(\">tbody\",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute(\"type\"))+\"/\"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute(\"type\"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}X.hasData(a)&&(h=X.access(a),i=r.extend({},h),X.set(b,i))}}function Ia(a,b){var c=b.nodeName.toLowerCase();\"input\"===c&&ja.test(a.type)?b.checked=a.checked:\"input\"!==c&&\"textarea\"!==c||(b.defaultValue=a.defaultValue)}function Ja(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&\"string\"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,\"script\"),Fa),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,na(j,\"script\"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Ga),l=0;l<i;l++)j=h[l],la.test(j.type||\"\")&&!W.access(j,\"globalEval\")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Da,\"\"),k))}return a}function Ka(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(na(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&oa(na(d,\"script\")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(za,\"<$1></$2>\")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d<e;d++)Ia(f[d],g[d]);if(b)if(c)for(f=f||na(a),g=g||na(h),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);else Ha(a,h);return g=na(h,\"script\"),g.length>0&&oa(g,!i&&na(a,\"script\")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent=\"\");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if(\"string\"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||[\"\",\"\"])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(na(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ja(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(na(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:\"append\",prependTo:\"prepend\",insertBefore:\"before\",insertAfter:\"after\",replaceAll:\"replaceWith\"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var La=/^margin/,Ma=new RegExp(\"^(\"+aa+\")(?!px)[a-z%]+$\",\"i\"),Na=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText=\"box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%\",i.innerHTML=\"\",ra.appendChild(h);var b=a.getComputedStyle(i);c=\"1%\"!==b.top,g=\"2px\"===b.marginLeft,e=\"4px\"===b.width,i.style.marginRight=\"50%\",f=\"4px\"===b.marginRight,ra.removeChild(h),i=null}}var c,e,f,g,h=d.createElement(\"div\"),i=d.createElement(\"div\");i.style&&(i.style.backgroundClip=\"content-box\",i.cloneNode(!0).style.backgroundClip=\"\",o.clearCloneStyle=\"content-box\"===i.style.backgroundClip,h.style.cssText=\"border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute\",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Oa(a,b,c){var d,e,f,g,h=a.style;return c=c||Na(a),c&&(g=c.getPropertyValue(b)||c[b],\"\"!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&Ma.test(g)&&La.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+\"\":g}function Pa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Qa=/^(none|table(?!-c[ea]).+)/,Ra=/^--/,Sa={position:\"absolute\",visibility:\"hidden\",display:\"block\"},Ta={letterSpacing:\"0\",fontWeight:\"400\"},Ua=[\"Webkit\",\"Moz\",\"ms\"],Va=d.createElement(\"div\").style;function Wa(a){if(a in Va)return a;var b=a[0].toUpperCase()+a.slice(1),c=Ua.length;while(c--)if(a=Ua[c]+b,a in Va)return a}function Xa(a){var b=r.cssProps[a];return b||(b=r.cssProps[a]=Wa(a)||a),b}function Ya(a,b,c){var d=ba.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||\"px\"):b}function Za(a,b,c,d,e){var f,g=0;for(f=c===(d?\"border\":\"content\")?4:\"width\"===b?1:0;f<4;f+=2)\"margin\"===c&&(g+=r.css(a,c+ca[f],!0,e)),d?(\"content\"===c&&(g-=r.css(a,\"padding\"+ca[f],!0,e)),\"margin\"!==c&&(g-=r.css(a,\"border\"+ca[f]+\"Width\",!0,e))):(g+=r.css(a,\"padding\"+ca[f],!0,e),\"padding\"!==c&&(g+=r.css(a,\"border\"+ca[f]+\"Width\",!0,e)));return g}function $a(a,b,c){var d,e=Na(a),f=Oa(a,b,e),g=\"border-box\"===r.css(a,\"boxSizing\",!1,e);return Ma.test(f)?f:(d=g&&(o.boxSizingReliable()||f===a.style[b]),\"auto\"===f&&(f=a[\"offset\"+b[0].toUpperCase()+b.slice(1)]),f=parseFloat(f)||0,f+Za(a,b,c||(g?\"border\":\"content\"),d,e)+\"px\")}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Oa(a,\"opacity\");return\"\"===c?\"1\":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{\"float\":\"cssFloat\"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=Ra.test(b),j=a.style;return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&\"get\"in g&&void 0!==(e=g.get(a,!1,d))?e:j[b]:(f=typeof c,\"string\"===f&&(e=ba.exec(c))&&e[1]&&(c=fa(a,b,e),f=\"number\"),null!=c&&c===c&&(\"number\"===f&&(c+=e&&e[3]||(r.cssNumber[h]?\"\":\"px\")),o.clearCloneStyle||\"\"!==c||0!==b.indexOf(\"background\")||(j[b]=\"inherit\"),g&&\"set\"in g&&void 0===(c=g.set(a,c,d))||(i?j.setProperty(b,c):j[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b),i=Ra.test(b);return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],g&&\"get\"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Oa(a,b,d)),\"normal\"===e&&b in Ta&&(e=Ta[b]),\"\"===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each([\"height\",\"width\"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Qa.test(r.css(a,\"display\"))||a.getClientRects().length&&a.getBoundingClientRect().width?$a(a,b,d):ea(a,Sa,function(){return $a(a,b,d)})},set:function(a,c,d){var e,f=d&&Na(a),g=d&&Za(a,b,d,\"border-box\"===r.css(a,\"boxSizing\",!1,f),f);return g&&(e=ba.exec(c))&&\"px\"!==(e[3]||\"px\")&&(a.style[b]=c,c=r.css(a,b)),Ya(a,c,g)}}}),r.cssHooks.marginLeft=Pa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Oa(a,\"marginLeft\"))||a.getBoundingClientRect().left-ea(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+\"px\"}),r.each({margin:\"\",padding:\"\",border:\"Width\"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f=\"string\"==typeof c?c.split(\" \"):[c];d<4;d++)e[a+ca[d]+b]=f[d]||f[d-2]||f[0];return e}},La.test(a)||(r.cssHooks[a+b].set=Ya)}),r.fn.extend({css:function(a,b){return T(this,function(a,b,c){var d,e,f={},g=0;if(Array.isArray(b)){for(d=Na(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?\"\":\"px\")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,\"\"),b&&\"auto\"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:\"swing\"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e[\"margin\"+c]=e[\"padding\"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners[\"*\"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function ib(a,b,c){var d,e,f,g,h,i,j,k,l=\"width\"in b||\"height\"in b,m=this,n={},o=a.style,p=a.nodeType&&da(a),q=W.get(a,\"fxshow\");c.queue||(g=r._queueHooks(a,\"fx\"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,\"fx\").length||g.empty.fire()})}));for(d in b)if(e=b[d],cb.test(e)){if(delete b[d],f=f||\"toggle\"===e,e===(p?\"hide\":\"show\")){if(\"show\"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=W.get(a,\"display\")),k=r.css(a,\"display\"),\"none\"===k&&(j?k=j:(ia([a],!0),j=a.style.display||j,k=r.css(a,\"display\"),ia([a]))),(\"inline\"===k||\"inline-block\"===k&&null!=j)&&\"none\"===r.css(a,\"float\")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j=\"none\"===k?\"\":k)),o.display=\"inline-block\")),c.overflow&&(o.overflow=\"hidden\",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?\"hidden\"in q&&(p=q.hidden):q=W.access(a,\"fxshow\",{display:j}),f&&(q.hidden=!p),p&&ia([a],!0),m.done(function(){p||ia([a]),W.remove(a,\"fxshow\");for(d in n)r.style(a,d,n[d])})),i=hb(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function jb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],Array.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&\"expand\"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kb(a,b,c){var d,e,f=0,g=kb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=ab||fb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(i||h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:ab||fb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jb(k,j.opts.specialEasing);f<g;f++)if(d=kb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,hb,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j}r.Animation=r.extend(kb,{tweeners:{\"*\":[function(a,b){var c=this.createTween(a,b);return fa(c.elem,a,ba.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=[\"*\"]):a=a.match(L);for(var c,d=0,e=a.length;d<e;d++)c=a[d],kb.tweeners[c]=kb.tweeners[c]||[],kb.tweeners[c].unshift(b)},prefilters:[ib],prefilter:function(a,b){b?kb.prefilters.unshift(a):kb.prefilters.push(a)}}),r.speed=function(a,b,c){var d=a&&\"object\"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off?d.duration=0:\"number\"!=typeof d.duration&&(d.duration in r.fx.speeds?d.duration=r.fx.speeds[d.duration]:d.duration=r.fx.speeds._default),null!=d.queue&&d.queue!==!0||(d.queue=\"fx\"),d.old=d.complete,d.complete=function(){r.isFunction(d.old)&&d.old.call(this),d.queue&&r.dequeue(this,d.queue)},d},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(da).css(\"opacity\",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=kb(this,r.extend({},a),f);(e||W.get(this,\"finish\"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return\"string\"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||\"fx\",[]),this.each(function(){var b=!0,e=null!=a&&a+\"queueHooks\",f=r.timers,g=W.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&db.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||\"fx\"),this.each(function(){var b,c=W.get(this),d=c[a+\"queue\"],e=c[a+\"queueHooks\"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each([\"toggle\",\"show\",\"hide\"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||\"boolean\"==typeof a?c.apply(this,arguments):this.animate(gb(b,!0),a,d,e)}}),r.each({slideDown:gb(\"show\"),slideUp:gb(\"hide\"),slideToggle:gb(\"toggle\"),fadeIn:{opacity:\"show\"},fadeOut:{opacity:\"hide\"},fadeToggle:{opacity:\"toggle\"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(ab=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),ab=void 0},r.fx.timer=function(a){r.timers.push(a),r.fx.start()},r.fx.interval=13,r.fx.start=function(){bb||(bb=!0,eb())},r.fx.stop=function(){bb=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||\"fx\",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement(\"input\"),b=d.createElement(\"select\"),c=b.appendChild(d.createElement(\"option\"));a.type=\"checkbox\",o.checkOn=\"\"!==a.value,o.optSelected=c.selected,a=d.createElement(\"input\"),a.value=\"t\",a.type=\"radio\",o.radioValue=\"t\"===a.value}();var lb,mb=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return T(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return\"undefined\"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&\"set\"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+\"\"),c):e&&\"get\"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),\nnull==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&\"radio\"===b&&B(a,\"input\")){var c=a.value;return a.setAttribute(\"type\",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&\"set\"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&\"get\"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,\"tabindex\");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{\"for\":\"htmlFor\",\"class\":\"className\"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each([\"tabIndex\",\"readOnly\",\"maxLength\",\"cellSpacing\",\"cellPadding\",\"rowSpan\",\"colSpan\",\"useMap\",\"frameBorder\",\"contentEditable\"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(\" \")}function qb(a){return a.getAttribute&&a.getAttribute(\"class\")||\"\"}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if(\"string\"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&\" \"+pb(e)+\" \"){g=0;while(f=b[g++])d.indexOf(\" \"+f+\" \")<0&&(d+=f+\" \");h=pb(d),e!==h&&c.setAttribute(\"class\",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr(\"class\",\"\");if(\"string\"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&\" \"+pb(e)+\" \"){g=0;while(f=b[g++])while(d.indexOf(\" \"+f+\" \")>-1)d=d.replace(\" \"+f+\" \",\" \");h=pb(d),e!==h&&c.setAttribute(\"class\",h)}}return this},toggleClass:function(a,b){var c=typeof a;return\"boolean\"==typeof b&&\"string\"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if(\"string\"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&\"boolean\"!==c||(b=qb(this),b&&W.set(this,\"__className__\",b),this.setAttribute&&this.setAttribute(\"class\",b||a===!1?\"\":W.get(this,\"__className__\")||\"\"))})},hasClass:function(a){var b,c,d=0;b=\" \"+a+\" \";while(c=this[d++])if(1===c.nodeType&&(\" \"+pb(qb(c))+\" \").indexOf(b)>-1)return!0;return!1}});var rb=/\\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e=\"\":\"number\"==typeof e?e+=\"\":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?\"\":a+\"\"})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&\"set\"in b&&void 0!==b.set(this,e,\"value\")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&\"get\"in b&&void 0!==(c=b.get(e,\"value\"))?c:(c=e.value,\"string\"==typeof c?c.replace(rb,\"\"):null==c?\"\":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,\"value\");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g=\"select-one\"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!B(c.parentNode,\"optgroup\"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each([\"radio\",\"checkbox\"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute(\"value\")?\"on\":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,\"type\")?b.type:b,q=l.call(b,\"namespace\")?b.namespace.split(\".\"):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(\".\")>-1&&(q=p.split(\".\"),p=q.shift(),q.sort()),k=p.indexOf(\":\")<0&&\"on\"+p,b=b[r.expando]?b:new r.Event(p,\"object\"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join(\".\"),b.rnamespace=b.namespace?new RegExp(\"(^|\\\\.)\"+q.join(\"\\\\.(?:.*\\\\.|)\")+\"(\\\\.|$)\"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,\"events\")||{})[b.type]&&W.get(h,\"handle\"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each(\"blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu\".split(\" \"),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin=\"onfocusin\"in a,o.focusin||r.each({focus:\"focusin\",blur:\"focusout\"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\\?/;r.parseXML=function(b){var c;if(!b||\"string\"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,\"text/xml\")}catch(d){c=void 0}return c&&!c.getElementsByTagName(\"parsererror\").length||r.error(\"Invalid XML: \"+b),c};var wb=/\\[\\]$/,xb=/\\r?\\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+\"[\"+(\"object\"==typeof e&&null!=e?b:\"\")+\"]\",e,c,d)});else if(c||\"object\"!==r.type(b))d(a,b);else for(e in b)Ab(a+\"[\"+e+\"]\",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+\"=\"+encodeURIComponent(null==c?\"\":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join(\"&\")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,\"elements\");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(\":disabled\")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,\"\\r\\n\")}}):{name:b.name,value:c.replace(xb,\"\\r\\n\")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \\t]*([^\\r\\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\\/\\//,Ib={},Jb={},Kb=\"*/\".concat(\"*\"),Lb=d.createElement(\"a\");Lb.href=tb.href;function Mb(a){return function(b,c){\"string\"!=typeof b&&(c=b,b=\"*\");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])\"+\"===d[0]?(d=d.slice(1)||\"*\",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return\"string\"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e[\"*\"]&&g(\"*\")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while(\"*\"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader(\"Content-Type\"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+\" \"+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if(\"*\"===f)f=i;else if(\"*\"!==i&&i!==f){if(g=j[i+\" \"+f]||j[\"* \"+f],!g)for(e in j)if(h=e.split(\" \"),h[1]===f&&(g=j[i+\" \"+h[0]]||j[\"* \"+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a[\"throws\"])b=g(b);else try{b=g(b)}catch(l){return{state:\"parsererror\",error:g?l:\"No conversion from \"+i+\" to \"+f}}}return{state:\"success\",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:\"GET\",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:\"application/x-www-form-urlencoded; charset=UTF-8\",accepts:{\"*\":Kb,text:\"text/plain\",html:\"text/html\",xml:\"application/xml, text/xml\",json:\"application/json, text/javascript\"},contents:{xml:/\\bxml\\b/,html:/\\bhtml/,json:/\\bjson\\b/},responseFields:{xml:\"responseXML\",text:\"responseText\",json:\"responseJSON\"},converters:{\"* text\":String,\"text html\":!0,\"text json\":JSON.parse,\"text xml\":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){\"object\"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks(\"once memory\"),u=o.statusCode||{},v={},w={},x=\"canceled\",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+\"\").replace(Hb,tb.protocol+\"//\"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||\"*\").toLowerCase().match(L)||[\"\"],null==o.crossDomain){j=d.createElement(\"a\");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+\"//\"+Lb.host!=j.protocol+\"//\"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&\"string\"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger(\"ajaxStart\"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,\"\"),o.hasContent?o.data&&o.processData&&0===(o.contentType||\"\").indexOf(\"application/x-www-form-urlencoded\")&&(o.data=o.data.replace(Bb,\"+\")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?\"&\":\"?\")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,\"$1\"),n=(vb.test(f)?\"&\":\"?\")+\"_=\"+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader(\"If-Modified-Since\",r.lastModified[f]),r.etag[f]&&y.setRequestHeader(\"If-None-Match\",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader(\"Content-Type\",o.contentType),y.setRequestHeader(\"Accept\",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+(\"*\"!==o.dataTypes[0]?\", \"+Kb+\"; q=0.01\":\"\"):o.accepts[\"*\"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x=\"abort\",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger(\"ajaxSend\",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort(\"timeout\")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,\"No Transport\");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||\"\",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader(\"Last-Modified\"),w&&(r.lastModified[f]=w),w=y.getResponseHeader(\"etag\"),w&&(r.etag[f]=w)),204===b||\"HEAD\"===o.type?x=\"nocontent\":304===b?x=\"notmodified\":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x=\"error\",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+\"\",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?\"ajaxSuccess\":\"ajaxError\",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger(\"ajaxComplete\",[y,o]),--r.active||r.event.trigger(\"ajaxStop\")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,\"json\")},getScript:function(a,b){return r.get(a,void 0,b,\"script\")}}),r.each([\"get\",\"post\"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:\"GET\",dataType:\"script\",cache:!0,async:!1,global:!1,\"throws\":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not(\"body\").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&\"withCredentials\"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e[\"X-Requested-With\"]||(e[\"X-Requested-With\"]=\"XMLHttpRequest\");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,\"abort\"===a?h.abort():\"error\"===a?\"number\"!=typeof h.status?f(0,\"error\"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,\"text\"!==(h.responseType||\"text\")||\"string\"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c(\"error\"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c(\"abort\");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:\"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript\"},contents:{script:/\\b(?:java|ecma)script\\b/},converters:{\"text script\":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter(\"script\",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type=\"GET\")}),r.ajaxTransport(\"script\",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(\"<script>\").prop({charset:a.scriptCharset,src:a.url}).on(\"load error\",c=function(a){b.remove(),c=null,a&&f(\"error\"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Tb=[],Ub=/(=)\\?(?=&|$)|\\?\\?/;r.ajaxSetup({jsonp:\"callback\",jsonpCallback:function(){var a=Tb.pop()||r.expando+\"_\"+ub++;return this[a]=!0,a}}),r.ajaxPrefilter(\"json jsonp\",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Ub.test(b.url)?\"url\":\"string\"==typeof b.data&&0===(b.contentType||\"\").indexOf(\"application/x-www-form-urlencoded\")&&Ub.test(b.data)&&\"data\");if(h||\"jsonp\"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Ub,\"$1\"+e):b.jsonp!==!1&&(b.url+=(vb.test(b.url)?\"&\":\"?\")+b.jsonp+\"=\"+e),b.converters[\"script json\"]=function(){return g||r.error(e+\" was not called\"),g[0]},b.dataTypes[0]=\"json\",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Tb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),\"script\"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument(\"\").body;return a.innerHTML=\"<form></form><form></form>\",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if(\"string\"!=typeof a)return[];\"boolean\"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(\"\"),e=b.createElement(\"base\"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=C.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=qa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(\" \");return h>-1&&(d=pb(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&\"object\"==typeof b&&(e=\"POST\"),g.length>0&&r.ajax({url:a,type:e||\"GET\",dataType:\"html\",data:b}).done(function(a){f=arguments,g.html(d?r(\"<div>\").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each([\"ajaxStart\",\"ajaxStop\",\"ajaxComplete\",\"ajaxError\",\"ajaxSuccess\",\"ajaxSend\"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length},r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,\"position\"),l=r(a),m={};\"static\"===k&&(a.style.position=\"relative\"),h=l.offset(),f=r.css(a,\"top\"),i=r.css(a,\"left\"),j=(\"absolute\"===k||\"fixed\"===k)&&(f+i).indexOf(\"auto\")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),\"using\"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),b=f.ownerDocument,c=b.documentElement,e=b.defaultView,{top:d.top+e.pageYOffset-c.clientTop,left:d.left+e.pageXOffset-c.clientLeft}):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return\"fixed\"===r.css(c,\"position\")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),B(a[0],\"html\")||(d=a.offset()),d={top:d.top+r.css(a[0],\"borderTopWidth\",!0),left:d.left+r.css(a[0],\"borderLeftWidth\",!0)}),{top:b.top-d.top-r.css(c,\"marginTop\",!0),left:b.left-d.left-r.css(c,\"marginLeft\",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&\"static\"===r.css(a,\"position\"))a=a.offsetParent;return a||ra})}}),r.each({scrollLeft:\"pageXOffset\",scrollTop:\"pageYOffset\"},function(a,b){var c=\"pageYOffset\"===b;r.fn[a]=function(d){return T(this,function(a,d,e){var f;return r.isWindow(a)?f=a:9===a.nodeType&&(f=a.defaultView),void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each([\"top\",\"left\"],function(a,b){r.cssHooks[b]=Pa(o.pixelPosition,function(a,c){if(c)return c=Oa(a,b),Ma.test(c)?r(a).position()[b]+\"px\":c})}),r.each({Height:\"height\",Width:\"width\"},function(a,b){r.each({padding:\"inner\"+a,content:b,\"\":\"outer\"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||\"boolean\"!=typeof e),h=c||(e===!0||f===!0?\"margin\":\"border\");return T(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf(\"outer\")?b[\"inner\"+a]:b.document.documentElement[\"client\"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body[\"scroll\"+a],f[\"scroll\"+a],b.body[\"offset\"+a],f[\"offset\"+a],f[\"client\"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,\"**\"):this.off(b,a||\"**\",c)}}),r.holdReady=function(a){a?r.readyWait++:r.ready(!0)},r.isArray=Array.isArray,r.parseJSON=JSON.parse,r.nodeName=B,\"function\"==typeof define&&define.amd&&define(\"jquery\",[],function(){return r});var Vb=a.jQuery,Wb=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Wb),b&&a.jQuery===r&&(a.jQuery=Vb),r},b||(a.jQuery=a.$=r),r});\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/language_data.js",
    "content": "/*\n * language_data.js\n * ~~~~~~~~~~~~~~~~\n *\n * This script contains the language-specific data used by searchtools.js,\n * namely the list of stopwords, stemmer, scorer and splitter.\n *\n * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.\n * :license: BSD, see LICENSE for details.\n *\n */\n\nvar 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\"];\n\n\n/* Non-minified version JS is _stemmer.js if file is provided */ \n/**\n * Porter Stemmer\n */\nvar Stemmer = function() {\n\n  var step2list = {\n    ational: 'ate',\n    tional: 'tion',\n    enci: 'ence',\n    anci: 'ance',\n    izer: 'ize',\n    bli: 'ble',\n    alli: 'al',\n    entli: 'ent',\n    eli: 'e',\n    ousli: 'ous',\n    ization: 'ize',\n    ation: 'ate',\n    ator: 'ate',\n    alism: 'al',\n    iveness: 'ive',\n    fulness: 'ful',\n    ousness: 'ous',\n    aliti: 'al',\n    iviti: 'ive',\n    biliti: 'ble',\n    logi: 'log'\n  };\n\n  var step3list = {\n    icate: 'ic',\n    ative: '',\n    alize: 'al',\n    iciti: 'ic',\n    ical: 'ic',\n    ful: '',\n    ness: ''\n  };\n\n  var c = \"[^aeiou]\";          // consonant\n  var v = \"[aeiouy]\";          // vowel\n  var C = c + \"[^aeiouy]*\";    // consonant sequence\n  var V = v + \"[aeiou]*\";      // vowel sequence\n\n  var mgr0 = \"^(\" + C + \")?\" + V + C;                      // [C]VC... is m>0\n  var meq1 = \"^(\" + C + \")?\" + V + C + \"(\" + V + \")?$\";    // [C]VC[V] is m=1\n  var mgr1 = \"^(\" + C + \")?\" + V + C + V + C;              // [C]VCVC... is m>1\n  var s_v   = \"^(\" + C + \")?\" + v;                         // vowel in stem\n\n  this.stemWord = function (w) {\n    var stem;\n    var suffix;\n    var firstch;\n    var origword = w;\n\n    if (w.length < 3)\n      return w;\n\n    var re;\n    var re2;\n    var re3;\n    var re4;\n\n    firstch = w.substr(0,1);\n    if (firstch == \"y\")\n      w = firstch.toUpperCase() + w.substr(1);\n\n    // Step 1a\n    re = /^(.+?)(ss|i)es$/;\n    re2 = /^(.+?)([^s])s$/;\n\n    if (re.test(w))\n      w = w.replace(re,\"$1$2\");\n    else if (re2.test(w))\n      w = w.replace(re2,\"$1$2\");\n\n    // Step 1b\n    re = /^(.+?)eed$/;\n    re2 = /^(.+?)(ed|ing)$/;\n    if (re.test(w)) {\n      var fp = re.exec(w);\n      re = new RegExp(mgr0);\n      if (re.test(fp[1])) {\n        re = /.$/;\n        w = w.replace(re,\"\");\n      }\n    }\n    else if (re2.test(w)) {\n      var fp = re2.exec(w);\n      stem = fp[1];\n      re2 = new RegExp(s_v);\n      if (re2.test(stem)) {\n        w = stem;\n        re2 = /(at|bl|iz)$/;\n        re3 = new RegExp(\"([^aeiouylsz])\\\\1$\");\n        re4 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n        if (re2.test(w))\n          w = w + \"e\";\n        else if (re3.test(w)) {\n          re = /.$/;\n          w = w.replace(re,\"\");\n        }\n        else if (re4.test(w))\n          w = w + \"e\";\n      }\n    }\n\n    // Step 1c\n    re = /^(.+?)y$/;\n    if (re.test(w)) {\n      var fp = re.exec(w);\n      stem = fp[1];\n      re = new RegExp(s_v);\n      if (re.test(stem))\n        w = stem + \"i\";\n    }\n\n    // Step 2\n    re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;\n    if (re.test(w)) {\n      var fp = re.exec(w);\n      stem = fp[1];\n      suffix = fp[2];\n      re = new RegExp(mgr0);\n      if (re.test(stem))\n        w = stem + step2list[suffix];\n    }\n\n    // Step 3\n    re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;\n    if (re.test(w)) {\n      var fp = re.exec(w);\n      stem = fp[1];\n      suffix = fp[2];\n      re = new RegExp(mgr0);\n      if (re.test(stem))\n        w = stem + step3list[suffix];\n    }\n\n    // Step 4\n    re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;\n    re2 = /^(.+?)(s|t)(ion)$/;\n    if (re.test(w)) {\n      var fp = re.exec(w);\n      stem = fp[1];\n      re = new RegExp(mgr1);\n      if (re.test(stem))\n        w = stem;\n    }\n    else if (re2.test(w)) {\n      var fp = re2.exec(w);\n      stem = fp[1] + fp[2];\n      re2 = new RegExp(mgr1);\n      if (re2.test(stem))\n        w = stem;\n    }\n\n    // Step 5\n    re = /^(.+?)e$/;\n    if (re.test(w)) {\n      var fp = re.exec(w);\n      stem = fp[1];\n      re = new RegExp(mgr1);\n      re2 = new RegExp(meq1);\n      re3 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n      if (re.test(stem) || (re2.test(stem) && !(re3.test(stem))))\n        w = stem;\n    }\n    re = /ll$/;\n    re2 = new RegExp(mgr1);\n    if (re.test(w) && re2.test(w)) {\n      re = /.$/;\n      w = w.replace(re,\"\");\n    }\n\n    // and turn initial Y back to y\n    if (firstch == \"y\")\n      w = firstch.toLowerCase() + w.substr(1);\n    return w;\n  }\n}\n\n\n\n\n\nvar splitChars = (function() {\n    var result = {};\n    var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648,\n         1748, 1809, 2416, 2473, 2481, 2526, 2601, 2609, 2612, 2615, 2653, 2702,\n         2706, 2729, 2737, 2740, 2857, 2865, 2868, 2910, 2928, 2948, 2961, 2971,\n         2973, 3085, 3089, 3113, 3124, 3213, 3217, 3241, 3252, 3295, 3341, 3345,\n         3369, 3506, 3516, 3633, 3715, 3721, 3736, 3744, 3748, 3750, 3756, 3761,\n         3781, 3912, 4239, 4347, 4681, 4695, 4697, 4745, 4785, 4799, 4801, 4823,\n         4881, 5760, 5901, 5997, 6313, 7405, 8024, 8026, 8028, 8030, 8117, 8125,\n         8133, 8181, 8468, 8485, 8487, 8489, 8494, 8527, 11311, 11359, 11687, 11695,\n         11703, 11711, 11719, 11727, 11735, 12448, 12539, 43010, 43014, 43019, 43587,\n         43696, 43713, 64286, 64297, 64311, 64317, 64319, 64322, 64325, 65141];\n    var i, j, start, end;\n    for (i = 0; i < singles.length; i++) {\n        result[singles[i]] = true;\n    }\n    var ranges = [[0, 47], [58, 64], [91, 94], [123, 169], [171, 177], [182, 184], [706, 709],\n         [722, 735], [741, 747], [751, 879], [888, 889], [894, 901], [1154, 1161],\n         [1318, 1328], [1367, 1368], [1370, 1376], [1416, 1487], [1515, 1519], [1523, 1568],\n         [1611, 1631], [1642, 1645], [1750, 1764], [1767, 1773], [1789, 1790], [1792, 1807],\n         [1840, 1868], [1958, 1968], [1970, 1983], [2027, 2035], [2038, 2041], [2043, 2047],\n         [2070, 2073], [2075, 2083], [2085, 2087], [2089, 2307], [2362, 2364], [2366, 2383],\n         [2385, 2391], [2402, 2405], [2419, 2424], [2432, 2436], [2445, 2446], [2449, 2450],\n         [2483, 2485], [2490, 2492], [2494, 2509], [2511, 2523], [2530, 2533], [2546, 2547],\n         [2554, 2564], [2571, 2574], [2577, 2578], [2618, 2648], [2655, 2661], [2672, 2673],\n         [2677, 2692], [2746, 2748], [2750, 2767], [2769, 2783], [2786, 2789], [2800, 2820],\n         [2829, 2830], [2833, 2834], [2874, 2876], [2878, 2907], [2914, 2917], [2930, 2946],\n         [2955, 2957], [2966, 2968], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3023],\n         [3025, 3045], [3059, 3076], [3130, 3132], [3134, 3159], [3162, 3167], [3170, 3173],\n         [3184, 3191], [3199, 3204], [3258, 3260], [3262, 3293], [3298, 3301], [3312, 3332],\n         [3386, 3388], [3390, 3423], [3426, 3429], [3446, 3449], [3456, 3460], [3479, 3481],\n         [3518, 3519], [3527, 3584], [3636, 3647], [3655, 3663], [3674, 3712], [3717, 3718],\n         [3723, 3724], [3726, 3731], [3752, 3753], [3764, 3772], [3774, 3775], [3783, 3791],\n         [3802, 3803], [3806, 3839], [3841, 3871], [3892, 3903], [3949, 3975], [3980, 4095],\n         [4139, 4158], [4170, 4175], [4182, 4185], [4190, 4192], [4194, 4196], [4199, 4205],\n         [4209, 4212], [4226, 4237], [4250, 4255], [4294, 4303], [4349, 4351], [4686, 4687],\n         [4702, 4703], [4750, 4751], [4790, 4791], [4806, 4807], [4886, 4887], [4955, 4968],\n         [4989, 4991], [5008, 5023], [5109, 5120], [5741, 5742], [5787, 5791], [5867, 5869],\n         [5873, 5887], [5906, 5919], [5938, 5951], [5970, 5983], [6001, 6015], [6068, 6102],\n         [6104, 6107], [6109, 6111], [6122, 6127], [6138, 6159], [6170, 6175], [6264, 6271],\n         [6315, 6319], [6390, 6399], [6429, 6469], [6510, 6511], [6517, 6527], [6572, 6592],\n         [6600, 6607], [6619, 6655], [6679, 6687], [6741, 6783], [6794, 6799], [6810, 6822],\n         [6824, 6916], [6964, 6980], [6988, 6991], [7002, 7042], [7073, 7085], [7098, 7167],\n         [7204, 7231], [7242, 7244], [7294, 7400], [7410, 7423], [7616, 7679], [7958, 7959],\n         [7966, 7967], [8006, 8007], [8014, 8015], [8062, 8063], [8127, 8129], [8141, 8143],\n         [8148, 8149], [8156, 8159], [8173, 8177], [8189, 8303], [8306, 8307], [8314, 8318],\n         [8330, 8335], [8341, 8449], [8451, 8454], [8456, 8457], [8470, 8472], [8478, 8483],\n         [8506, 8507], [8512, 8516], [8522, 8525], [8586, 9311], [9372, 9449], [9472, 10101],\n         [10132, 11263], [11493, 11498], [11503, 11516], [11518, 11519], [11558, 11567],\n         [11622, 11630], [11632, 11647], [11671, 11679], [11743, 11822], [11824, 12292],\n         [12296, 12320], [12330, 12336], [12342, 12343], [12349, 12352], [12439, 12444],\n         [12544, 12548], [12590, 12592], [12687, 12689], [12694, 12703], [12728, 12783],\n         [12800, 12831], [12842, 12880], [12896, 12927], [12938, 12976], [12992, 13311],\n         [19894, 19967], [40908, 40959], [42125, 42191], [42238, 42239], [42509, 42511],\n         [42540, 42559], [42592, 42593], [42607, 42622], [42648, 42655], [42736, 42774],\n         [42784, 42785], [42889, 42890], [42893, 43002], [43043, 43055], [43062, 43071],\n         [43124, 43137], [43188, 43215], [43226, 43249], [43256, 43258], [43260, 43263],\n         [43302, 43311], [43335, 43359], [43389, 43395], [43443, 43470], [43482, 43519],\n         [43561, 43583], [43596, 43599], [43610, 43615], [43639, 43641], [43643, 43647],\n         [43698, 43700], [43703, 43704], [43710, 43711], [43715, 43738], [43742, 43967],\n         [44003, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 55295],\n         [57344, 63743], [64046, 64047], [64110, 64111], [64218, 64255], [64263, 64274],\n         [64280, 64284], [64434, 64466], [64830, 64847], [64912, 64913], [64968, 65007],\n         [65020, 65135], [65277, 65295], [65306, 65312], [65339, 65344], [65371, 65381],\n         [65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497]];\n    for (i = 0; i < ranges.length; i++) {\n        start = ranges[i][0];\n        end = ranges[i][1];\n        for (j = start; j <= end; j++) {\n            result[j] = true;\n        }\n    }\n    return result;\n})();\n\nfunction splitQuery(query) {\n    var result = [];\n    var start = -1;\n    for (var i = 0; i < query.length; i++) {\n        if (splitChars[query.charCodeAt(i)]) {\n            if (start !== -1) {\n                result.push(query.slice(start, i));\n                start = -1;\n            }\n        } else if (start === -1) {\n            start = i;\n        }\n    }\n    if (start !== -1) {\n        result.push(query.slice(start));\n    }\n    return result;\n}\n\n\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/pygments.css",
    "content": ".highlight .hll { background-color: #ffffcc }\n.highlight  { background: #ffffff; }\n.highlight .c { color: #999988; font-style: italic } /* Comment */\n.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */\n.highlight .k { font-weight: bold } /* Keyword */\n.highlight .o { font-weight: bold } /* Operator */\n.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */\n.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */\n.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */\n.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */\n.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */\n.highlight .ge { font-style: italic } /* Generic.Emph */\n.highlight .gr { color: #aa0000 } /* Generic.Error */\n.highlight .gh { color: #999999 } /* Generic.Heading */\n.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */\n.highlight .go { color: #888888 } /* Generic.Output */\n.highlight .gp { color: #555555 } /* Generic.Prompt */\n.highlight .gs { font-weight: bold } /* Generic.Strong */\n.highlight .gu { color: #aaaaaa } /* Generic.Subheading */\n.highlight .gt { color: #aa0000 } /* Generic.Traceback */\n.highlight .kc { font-weight: bold } /* Keyword.Constant */\n.highlight .kd { font-weight: bold } /* Keyword.Declaration */\n.highlight .kn { font-weight: bold } /* Keyword.Namespace */\n.highlight .kp { font-weight: bold } /* Keyword.Pseudo */\n.highlight .kr { font-weight: bold } /* Keyword.Reserved */\n.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */\n.highlight .m { color: #009999 } /* Literal.Number */\n.highlight .s { color: #bb8844 } /* Literal.String */\n.highlight .na { color: #008080 } /* Name.Attribute */\n.highlight .nb { color: #999999 } /* Name.Builtin */\n.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */\n.highlight .no { color: #008080 } /* Name.Constant */\n.highlight .ni { color: #800080 } /* Name.Entity */\n.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */\n.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */\n.highlight .nn { color: #555555 } /* Name.Namespace */\n.highlight .nt { color: #000080 } /* Name.Tag */\n.highlight .nv { color: #008080 } /* Name.Variable */\n.highlight .ow { font-weight: bold } /* Operator.Word */\n.highlight .w { color: #bbbbbb } /* Text.Whitespace */\n.highlight .mf { color: #009999 } /* Literal.Number.Float */\n.highlight .mh { color: #009999 } /* Literal.Number.Hex */\n.highlight .mi { color: #009999 } /* Literal.Number.Integer */\n.highlight .mo { color: #009999 } /* Literal.Number.Oct */\n.highlight .sb { color: #bb8844 } /* Literal.String.Backtick */\n.highlight .sc { color: #bb8844 } /* Literal.String.Char */\n.highlight .sd { color: #bb8844 } /* Literal.String.Doc */\n.highlight .s2 { color: #bb8844 } /* Literal.String.Double */\n.highlight .se { color: #bb8844 } /* Literal.String.Escape */\n.highlight .sh { color: #bb8844 } /* Literal.String.Heredoc */\n.highlight .si { color: #bb8844 } /* Literal.String.Interpol */\n.highlight .sx { color: #bb8844 } /* Literal.String.Other */\n.highlight .sr { color: #808000 } /* Literal.String.Regex */\n.highlight .s1 { color: #bb8844 } /* Literal.String.Single */\n.highlight .ss { color: #bb8844 } /* Literal.String.Symbol */\n.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */\n.highlight .vc { color: #008080 } /* Name.Variable.Class */\n.highlight .vg { color: #008080 } /* Name.Variable.Global */\n.highlight .vi { color: #008080 } /* Name.Variable.Instance */\n.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/searchtools.js",
    "content": "/*\n * searchtools.js\n * ~~~~~~~~~~~~~~~~\n *\n * Sphinx JavaScript utilities for the full-text search.\n *\n * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.\n * :license: BSD, see LICENSE for details.\n *\n */\n\nif (!Scorer) {\n  /**\n   * Simple result scoring code.\n   */\n  var Scorer = {\n    // Implement the following function to further tweak the score for each result\n    // The function takes a result array [filename, title, anchor, descr, score]\n    // and returns the new score.\n    /*\n    score: function(result) {\n      return result[4];\n    },\n    */\n\n    // query matches the full name of an object\n    objNameMatch: 11,\n    // or matches in the last dotted part of the object name\n    objPartialMatch: 6,\n    // Additive scores depending on the priority of the object\n    objPrio: {0:  15,   // used to be importantResults\n              1:  5,   // used to be objectResults\n              2: -5},  // used to be unimportantResults\n    //  Used when the priority is not in the mapping.\n    objPrioDefault: 0,\n\n    // query found in title\n    title: 15,\n    // query found in terms\n    term: 5\n  };\n}\n\nif (!splitQuery) {\n  function splitQuery(query) {\n    return query.split(/\\s+/);\n  }\n}\n\n/**\n * Search Module\n */\nvar Search = {\n\n  _index : null,\n  _queued_query : null,\n  _pulse_status : -1,\n\n  init : function() {\n      var params = $.getQueryParameters();\n      if (params.q) {\n          var query = params.q[0];\n          $('input[name=\"q\"]')[0].value = query;\n          this.performSearch(query);\n      }\n  },\n\n  loadIndex : function(url) {\n    $.ajax({type: \"GET\", url: url, data: null,\n            dataType: \"script\", cache: true,\n            complete: function(jqxhr, textstatus) {\n              if (textstatus != \"success\") {\n                document.getElementById(\"searchindexloader\").src = url;\n              }\n            }});\n  },\n\n  setIndex : function(index) {\n    var q;\n    this._index = index;\n    if ((q = this._queued_query) !== null) {\n      this._queued_query = null;\n      Search.query(q);\n    }\n  },\n\n  hasIndex : function() {\n      return this._index !== null;\n  },\n\n  deferQuery : function(query) {\n      this._queued_query = query;\n  },\n\n  stopPulse : function() {\n      this._pulse_status = 0;\n  },\n\n  startPulse : function() {\n    if (this._pulse_status >= 0)\n        return;\n    function pulse() {\n      var i;\n      Search._pulse_status = (Search._pulse_status + 1) % 4;\n      var dotString = '';\n      for (i = 0; i < Search._pulse_status; i++)\n        dotString += '.';\n      Search.dots.text(dotString);\n      if (Search._pulse_status > -1)\n        window.setTimeout(pulse, 500);\n    }\n    pulse();\n  },\n\n  /**\n   * perform a search for something (or wait until index is loaded)\n   */\n  performSearch : function(query) {\n    // create the required interface elements\n    this.out = $('#search-results');\n    this.title = $('<h2>' + _('Searching') + '</h2>').appendTo(this.out);\n    this.dots = $('<span></span>').appendTo(this.title);\n    this.status = $('<p style=\"display: none\"></p>').appendTo(this.out);\n    this.output = $('<ul class=\"search\"/>').appendTo(this.out);\n\n    $('#search-progress').text(_('Preparing search...'));\n    this.startPulse();\n\n    // index already loaded, the browser was quick!\n    if (this.hasIndex())\n      this.query(query);\n    else\n      this.deferQuery(query);\n  },\n\n  /**\n   * execute search (requires search index to be loaded)\n   */\n  query : function(query) {\n    var i;\n\n    // stem the searchterms and add them to the correct list\n    var stemmer = new Stemmer();\n    var searchterms = [];\n    var excluded = [];\n    var hlterms = [];\n    var tmp = splitQuery(query);\n    var objectterms = [];\n    for (i = 0; i < tmp.length; i++) {\n      if (tmp[i] !== \"\") {\n          objectterms.push(tmp[i].toLowerCase());\n      }\n\n      if ($u.indexOf(stopwords, tmp[i].toLowerCase()) != -1 || tmp[i].match(/^\\d+$/) ||\n          tmp[i] === \"\") {\n        // skip this \"word\"\n        continue;\n      }\n      // stem the word\n      var word = stemmer.stemWord(tmp[i].toLowerCase());\n      // prevent stemmer from cutting word smaller than two chars\n      if(word.length < 3 && tmp[i].length >= 3) {\n        word = tmp[i];\n      }\n      var toAppend;\n      // select the correct list\n      if (word[0] == '-') {\n        toAppend = excluded;\n        word = word.substr(1);\n      }\n      else {\n        toAppend = searchterms;\n        hlterms.push(tmp[i].toLowerCase());\n      }\n      // only add if not already in the list\n      if (!$u.contains(toAppend, word))\n        toAppend.push(word);\n    }\n    var highlightstring = '?highlight=' + $.urlencode(hlterms.join(\" \"));\n\n    // console.debug('SEARCH: searching for:');\n    // console.info('required: ', searchterms);\n    // console.info('excluded: ', excluded);\n\n    // prepare search\n    var terms = this._index.terms;\n    var titleterms = this._index.titleterms;\n\n    // array of [filename, title, anchor, descr, score]\n    var results = [];\n    $('#search-progress').empty();\n\n    // lookup as object\n    for (i = 0; i < objectterms.length; i++) {\n      var others = [].concat(objectterms.slice(0, i),\n                             objectterms.slice(i+1, objectterms.length));\n      results = results.concat(this.performObjectSearch(objectterms[i], others));\n    }\n\n    // lookup as search terms in fulltext\n    results = results.concat(this.performTermsSearch(searchterms, excluded, terms, titleterms));\n\n    // let the scorer override scores with a custom scoring function\n    if (Scorer.score) {\n      for (i = 0; i < results.length; i++)\n        results[i][4] = Scorer.score(results[i]);\n    }\n\n    // now sort the results by score (in opposite order of appearance, since the\n    // display function below uses pop() to retrieve items) and then\n    // alphabetically\n    results.sort(function(a, b) {\n      var left = a[4];\n      var right = b[4];\n      if (left > right) {\n        return 1;\n      } else if (left < right) {\n        return -1;\n      } else {\n        // same score: sort alphabetically\n        left = a[1].toLowerCase();\n        right = b[1].toLowerCase();\n        return (left > right) ? -1 : ((left < right) ? 1 : 0);\n      }\n    });\n\n    // for debugging\n    //Search.lastresults = results.slice();  // a copy\n    //console.info('search results:', Search.lastresults);\n\n    // print the results\n    var resultCount = results.length;\n    function displayNextItem() {\n      // results left, load the summary and display it\n      if (results.length) {\n        var item = results.pop();\n        var listItem = $('<li style=\"display:none\"></li>');\n        if (DOCUMENTATION_OPTIONS.FILE_SUFFIX === '') {\n          // dirhtml builder\n          var dirname = item[0] + '/';\n          if (dirname.match(/\\/index\\/$/)) {\n            dirname = dirname.substring(0, dirname.length-6);\n          } else if (dirname == 'index/') {\n            dirname = '';\n          }\n          listItem.append($('<a/>').attr('href',\n            DOCUMENTATION_OPTIONS.URL_ROOT + dirname +\n            highlightstring + item[2]).html(item[1]));\n        } else {\n          // normal html builders\n          listItem.append($('<a/>').attr('href',\n            item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX +\n            highlightstring + item[2]).html(item[1]));\n        }\n        if (item[3]) {\n          listItem.append($('<span> (' + item[3] + ')</span>'));\n          Search.output.append(listItem);\n          listItem.slideDown(5, function() {\n            displayNextItem();\n          });\n        } else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {\n          var suffix = DOCUMENTATION_OPTIONS.SOURCELINK_SUFFIX;\n          if (suffix === undefined) {\n            suffix = '.txt';\n          }\n          $.ajax({url: DOCUMENTATION_OPTIONS.URL_ROOT + '_sources/' + item[5] + (item[5].slice(-suffix.length) === suffix ? '' : suffix),\n                  dataType: \"text\",\n                  complete: function(jqxhr, textstatus) {\n                    var data = jqxhr.responseText;\n                    if (data !== '' && data !== undefined) {\n                      listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));\n                    }\n                    Search.output.append(listItem);\n                    listItem.slideDown(5, function() {\n                      displayNextItem();\n                    });\n                  }});\n        } else {\n          // no source available, just display title\n          Search.output.append(listItem);\n          listItem.slideDown(5, function() {\n            displayNextItem();\n          });\n        }\n      }\n      // search finished, update title and status message\n      else {\n        Search.stopPulse();\n        Search.title.text(_('Search Results'));\n        if (!resultCount)\n          Search.status.text(_('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\\'ve selected enough categories.'));\n        else\n            Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));\n        Search.status.fadeIn(500);\n      }\n    }\n    displayNextItem();\n  },\n\n  /**\n   * search for object names\n   */\n  performObjectSearch : function(object, otherterms) {\n    var filenames = this._index.filenames;\n    var docnames = this._index.docnames;\n    var objects = this._index.objects;\n    var objnames = this._index.objnames;\n    var titles = this._index.titles;\n\n    var i;\n    var results = [];\n\n    for (var prefix in objects) {\n      for (var name in objects[prefix]) {\n        var fullname = (prefix ? prefix + '.' : '') + name;\n        if (fullname.toLowerCase().indexOf(object) > -1) {\n          var score = 0;\n          var parts = fullname.split('.');\n          // check for different match types: exact matches of full name or\n          // \"last name\" (i.e. last dotted part)\n          if (fullname == object || parts[parts.length - 1] == object) {\n            score += Scorer.objNameMatch;\n          // matches in last name\n          } else if (parts[parts.length - 1].indexOf(object) > -1) {\n            score += Scorer.objPartialMatch;\n          }\n          var match = objects[prefix][name];\n          var objname = objnames[match[1]][2];\n          var title = titles[match[0]];\n          // If more than one term searched for, we require other words to be\n          // found in the name/title/description\n          if (otherterms.length > 0) {\n            var haystack = (prefix + ' ' + name + ' ' +\n                            objname + ' ' + title).toLowerCase();\n            var allfound = true;\n            for (i = 0; i < otherterms.length; i++) {\n              if (haystack.indexOf(otherterms[i]) == -1) {\n                allfound = false;\n                break;\n              }\n            }\n            if (!allfound) {\n              continue;\n            }\n          }\n          var descr = objname + _(', in ') + title;\n\n          var anchor = match[3];\n          if (anchor === '')\n            anchor = fullname;\n          else if (anchor == '-')\n            anchor = objnames[match[1]][1] + '-' + fullname;\n          // add custom score for some objects according to scorer\n          if (Scorer.objPrio.hasOwnProperty(match[2])) {\n            score += Scorer.objPrio[match[2]];\n          } else {\n            score += Scorer.objPrioDefault;\n          }\n          results.push([docnames[match[0]], fullname, '#'+anchor, descr, score, filenames[match[0]]]);\n        }\n      }\n    }\n\n    return results;\n  },\n\n  /**\n   * search for full-text terms in the index\n   */\n  performTermsSearch : function(searchterms, excluded, terms, titleterms) {\n    var docnames = this._index.docnames;\n    var filenames = this._index.filenames;\n    var titles = this._index.titles;\n\n    var i, j, file;\n    var fileMap = {};\n    var scoreMap = {};\n    var results = [];\n\n    // perform the search on the required terms\n    for (i = 0; i < searchterms.length; i++) {\n      var word = searchterms[i];\n      var files = [];\n      var _o = [\n        {files: terms[word], score: Scorer.term},\n        {files: titleterms[word], score: Scorer.title}\n      ];\n\n      // no match but word was a required one\n      if ($u.every(_o, function(o){return o.files === undefined;})) {\n        break;\n      }\n      // found search word in contents\n      $u.each(_o, function(o) {\n        var _files = o.files;\n        if (_files === undefined)\n          return\n\n        if (_files.length === undefined)\n          _files = [_files];\n        files = files.concat(_files);\n\n        // set score for the word in each file to Scorer.term\n        for (j = 0; j < _files.length; j++) {\n          file = _files[j];\n          if (!(file in scoreMap))\n            scoreMap[file] = {}\n          scoreMap[file][word] = o.score;\n        }\n      });\n\n      // create the mapping\n      for (j = 0; j < files.length; j++) {\n        file = files[j];\n        if (file in fileMap)\n          fileMap[file].push(word);\n        else\n          fileMap[file] = [word];\n      }\n    }\n\n    // now check if the files don't contain excluded terms\n    for (file in fileMap) {\n      var valid = true;\n\n      // check if all requirements are matched\n      if (fileMap[file].length != searchterms.length)\n          continue;\n\n      // ensure that none of the excluded terms is in the search result\n      for (i = 0; i < excluded.length; i++) {\n        if (terms[excluded[i]] == file ||\n            titleterms[excluded[i]] == file ||\n            $u.contains(terms[excluded[i]] || [], file) ||\n            $u.contains(titleterms[excluded[i]] || [], file)) {\n          valid = false;\n          break;\n        }\n      }\n\n      // if we have still a valid result we can add it to the result list\n      if (valid) {\n        // select one (max) score for the file.\n        // for better ranking, we should calculate ranking by using words statistics like basic tf-idf...\n        var score = $u.max($u.map(fileMap[file], function(w){return scoreMap[file][w]}));\n        results.push([docnames[file], titles[file], '', null, score, filenames[file]]);\n      }\n    }\n    return results;\n  },\n\n  /**\n   * helper function to return a node containing the\n   * search summary for a given text. keywords is a list\n   * of stemmed words, hlwords is the list of normal, unstemmed\n   * words. the first one is used to find the occurrence, the\n   * latter for highlighting it.\n   */\n  makeSearchSummary : function(text, keywords, hlwords) {\n    var textLower = text.toLowerCase();\n    var start = 0;\n    $.each(keywords, function() {\n      var i = textLower.indexOf(this.toLowerCase());\n      if (i > -1)\n        start = i;\n    });\n    start = Math.max(start - 120, 0);\n    var excerpt = ((start > 0) ? '...' : '') +\n      $.trim(text.substr(start, 240)) +\n      ((start + 240 - text.length) ? '...' : '');\n    var rv = $('<div class=\"context\"></div>').text(excerpt);\n    $.each(hlwords, function() {\n      rv = rv.highlightText(this, 'highlighted');\n    });\n    return rv;\n  }\n};\n\n$(document).ready(function() {\n  Search.init();\n});\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/sidebar.js",
    "content": "/*\n * sidebar.js\n * ~~~~~~~~~~\n *\n * This script makes the Sphinx sidebar collapsible.\n *\n * .sphinxsidebar contains .sphinxsidebarwrapper.  This script adds\n * in .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton\n * used to collapse and expand the sidebar.\n *\n * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden\n * and the width of the sidebar and the margin-left of the document\n * are decreased. When the sidebar is expanded the opposite happens.\n * This script saves a per-browser/per-session cookie used to\n * remember the position of the sidebar among the pages.\n * Once the browser is closed the cookie is deleted and the position\n * reset to the default (expanded).\n *\n * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.\n * :license: BSD, see LICENSE for details.\n *\n */\n\n$(function() {\n  \n  \n  \n  \n  \n  \n  \n\n  // global elements used by the functions.\n  // the 'sidebarbutton' element is defined as global after its\n  // creation, in the add_sidebar_button function\n  var bodywrapper = $('.bodywrapper');\n  var sidebar = $('.sphinxsidebar');\n  var sidebarwrapper = $('.sphinxsidebarwrapper');\n\n  // for some reason, the document has no sidebar; do not run into errors\n  if (!sidebar.length) return;\n\n  // original margin-left of the bodywrapper and width of the sidebar\n  // with the sidebar expanded\n  var bw_margin_expanded = bodywrapper.css('margin-left');\n  var ssb_width_expanded = sidebar.width();\n\n  // margin-left of the bodywrapper and width of the sidebar\n  // with the sidebar collapsed\n  var bw_margin_collapsed = '.8em';\n  var ssb_width_collapsed = '.8em';\n\n  // colors used by the current theme\n  var dark_color = $('.related').css('background-color');\n  var light_color = $('.document').css('background-color');\n\n  function sidebar_is_collapsed() {\n    return sidebarwrapper.is(':not(:visible)');\n  }\n\n  function toggle_sidebar() {\n    if (sidebar_is_collapsed())\n      expand_sidebar();\n    else\n      collapse_sidebar();\n  }\n\n  function collapse_sidebar() {\n    sidebarwrapper.hide();\n    sidebar.css('width', ssb_width_collapsed);\n    bodywrapper.css('margin-left', bw_margin_collapsed);\n    sidebarbutton.css({\n        'margin-left': '0',\n        'height': bodywrapper.height()\n    });\n    sidebarbutton.find('span').text('»');\n    sidebarbutton.attr('title', _('Expand sidebar'));\n    document.cookie = 'sidebar=collapsed';\n  }\n\n  function expand_sidebar() {\n    bodywrapper.css('margin-left', bw_margin_expanded);\n    sidebar.css('width', ssb_width_expanded);\n    sidebarwrapper.show();\n    sidebarbutton.css({\n        'margin-left': ssb_width_expanded-12,\n        'height': bodywrapper.height()\n    });\n    sidebarbutton.find('span').text('«');\n    sidebarbutton.attr('title', _('Collapse sidebar'));\n    document.cookie = 'sidebar=expanded';\n  }\n\n  function add_sidebar_button() {\n    sidebarwrapper.css({\n        'float': 'left',\n        'margin-right': '0',\n        'width': ssb_width_expanded - 28\n    });\n    // create the button\n    sidebar.append(\n        '<div id=\"sidebarbutton\"><span>&laquo;</span></div>'\n    );\n    var sidebarbutton = $('#sidebarbutton');\n    light_color = sidebarbutton.css('background-color');\n    // find the height of the viewport to center the '<<' in the page\n    var viewport_height;\n    if (window.innerHeight)\n \t  viewport_height = window.innerHeight;\n    else\n\t  viewport_height = $(window).height();\n    sidebarbutton.find('span').css({\n        'display': 'block',\n        'margin-top': (viewport_height - sidebar.position().top - 20) / 2\n    });\n\n    sidebarbutton.click(toggle_sidebar);\n    sidebarbutton.attr('title', _('Collapse sidebar'));\n    sidebarbutton.css({\n        'color': '#FFFFFF',\n        'border-left': '1px solid ' + dark_color,\n        'font-size': '1.2em',\n        'cursor': 'pointer',\n        'height': bodywrapper.height(),\n        'padding-top': '1px',\n        'margin-left': ssb_width_expanded - 12\n    });\n\n    sidebarbutton.hover(\n      function () {\n          $(this).css('background-color', dark_color);\n      },\n      function () {\n          $(this).css('background-color', light_color);\n      }\n    );\n  }\n\n  function set_position_from_cookie() {\n    if (!document.cookie)\n      return;\n    var items = document.cookie.split(';');\n    for(var k=0; k<items.length; k++) {\n      var key_val = items[k].split('=');\n      var key = key_val[0].replace(/ /, \"\");  // strip leading spaces\n      if (key == 'sidebar') {\n        var value = key_val[1];\n        if ((value == 'collapsed') && (!sidebar_is_collapsed()))\n          collapse_sidebar();\n        else if ((value == 'expanded') && (sidebar_is_collapsed()))\n          expand_sidebar();\n      }\n    }\n  }\n\n  add_sidebar_button();\n  var sidebarbutton = $('#sidebarbutton');\n  set_position_from_cookie();\n});"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore-1.3.1.js",
    "content": "//     Underscore.js 1.3.1\n//     (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.\n//     Underscore is freely distributable under the MIT license.\n//     Portions of Underscore are inspired or borrowed from Prototype,\n//     Oliver Steele's Functional, and John Resig's Micro-Templating.\n//     For all details and documentation:\n//     http://documentcloud.github.com/underscore\n\n(function() {\n\n  // Baseline setup\n  // --------------\n\n  // Establish the root object, `window` in the browser, or `global` on the server.\n  var root = this;\n\n  // Save the previous value of the `_` variable.\n  var previousUnderscore = root._;\n\n  // Establish the object that gets returned to break out of a loop iteration.\n  var breaker = {};\n\n  // Save bytes in the minified (but not gzipped) version:\n  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;\n\n  // Create quick reference variables for speed access to core prototypes.\n  var slice            = ArrayProto.slice,\n      unshift          = ArrayProto.unshift,\n      toString         = ObjProto.toString,\n      hasOwnProperty   = ObjProto.hasOwnProperty;\n\n  // All **ECMAScript 5** native function implementations that we hope to use\n  // are declared here.\n  var\n    nativeForEach      = ArrayProto.forEach,\n    nativeMap          = ArrayProto.map,\n    nativeReduce       = ArrayProto.reduce,\n    nativeReduceRight  = ArrayProto.reduceRight,\n    nativeFilter       = ArrayProto.filter,\n    nativeEvery        = ArrayProto.every,\n    nativeSome         = ArrayProto.some,\n    nativeIndexOf      = ArrayProto.indexOf,\n    nativeLastIndexOf  = ArrayProto.lastIndexOf,\n    nativeIsArray      = Array.isArray,\n    nativeKeys         = Object.keys,\n    nativeBind         = FuncProto.bind;\n\n  // Create a safe reference to the Underscore object for use below.\n  var _ = function(obj) { return new wrapper(obj); };\n\n  // Export the Underscore object for **Node.js**, with\n  // backwards-compatibility for the old `require()` API. If we're in\n  // the browser, add `_` as a global object via a string identifier,\n  // for Closure Compiler \"advanced\" mode.\n  if (typeof exports !== 'undefined') {\n    if (typeof module !== 'undefined' && module.exports) {\n      exports = module.exports = _;\n    }\n    exports._ = _;\n  } else {\n    root['_'] = _;\n  }\n\n  // Current version.\n  _.VERSION = '1.3.1';\n\n  // Collection Functions\n  // --------------------\n\n  // The cornerstone, an `each` implementation, aka `forEach`.\n  // Handles objects with the built-in `forEach`, arrays, and raw objects.\n  // Delegates to **ECMAScript 5**'s native `forEach` if available.\n  var each = _.each = _.forEach = function(obj, iterator, context) {\n    if (obj == null) return;\n    if (nativeForEach && obj.forEach === nativeForEach) {\n      obj.forEach(iterator, context);\n    } else if (obj.length === +obj.length) {\n      for (var i = 0, l = obj.length; i < l; i++) {\n        if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;\n      }\n    } else {\n      for (var key in obj) {\n        if (_.has(obj, key)) {\n          if (iterator.call(context, obj[key], key, obj) === breaker) return;\n        }\n      }\n    }\n  };\n\n  // Return the results of applying the iterator to each element.\n  // Delegates to **ECMAScript 5**'s native `map` if available.\n  _.map = _.collect = function(obj, iterator, context) {\n    var results = [];\n    if (obj == null) return results;\n    if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);\n    each(obj, function(value, index, list) {\n      results[results.length] = iterator.call(context, value, index, list);\n    });\n    if (obj.length === +obj.length) results.length = obj.length;\n    return results;\n  };\n\n  // **Reduce** builds up a single result from a list of values, aka `inject`,\n  // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.\n  _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {\n    var initial = arguments.length > 2;\n    if (obj == null) obj = [];\n    if (nativeReduce && obj.reduce === nativeReduce) {\n      if (context) iterator = _.bind(iterator, context);\n      return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);\n    }\n    each(obj, function(value, index, list) {\n      if (!initial) {\n        memo = value;\n        initial = true;\n      } else {\n        memo = iterator.call(context, memo, value, index, list);\n      }\n    });\n    if (!initial) throw new TypeError('Reduce of empty array with no initial value');\n    return memo;\n  };\n\n  // The right-associative version of reduce, also known as `foldr`.\n  // Delegates to **ECMAScript 5**'s native `reduceRight` if available.\n  _.reduceRight = _.foldr = function(obj, iterator, memo, context) {\n    var initial = arguments.length > 2;\n    if (obj == null) obj = [];\n    if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {\n      if (context) iterator = _.bind(iterator, context);\n      return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);\n    }\n    var reversed = _.toArray(obj).reverse();\n    if (context && !initial) iterator = _.bind(iterator, context);\n    return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator);\n  };\n\n  // Return the first value which passes a truth test. Aliased as `detect`.\n  _.find = _.detect = function(obj, iterator, context) {\n    var result;\n    any(obj, function(value, index, list) {\n      if (iterator.call(context, value, index, list)) {\n        result = value;\n        return true;\n      }\n    });\n    return result;\n  };\n\n  // Return all the elements that pass a truth test.\n  // Delegates to **ECMAScript 5**'s native `filter` if available.\n  // Aliased as `select`.\n  _.filter = _.select = function(obj, iterator, context) {\n    var results = [];\n    if (obj == null) return results;\n    if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);\n    each(obj, function(value, index, list) {\n      if (iterator.call(context, value, index, list)) results[results.length] = value;\n    });\n    return results;\n  };\n\n  // Return all the elements for which a truth test fails.\n  _.reject = function(obj, iterator, context) {\n    var results = [];\n    if (obj == null) return results;\n    each(obj, function(value, index, list) {\n      if (!iterator.call(context, value, index, list)) results[results.length] = value;\n    });\n    return results;\n  };\n\n  // Determine whether all of the elements match a truth test.\n  // Delegates to **ECMAScript 5**'s native `every` if available.\n  // Aliased as `all`.\n  _.every = _.all = function(obj, iterator, context) {\n    var result = true;\n    if (obj == null) return result;\n    if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);\n    each(obj, function(value, index, list) {\n      if (!(result = result && iterator.call(context, value, index, list))) return breaker;\n    });\n    return result;\n  };\n\n  // Determine if at least one element in the object matches a truth test.\n  // Delegates to **ECMAScript 5**'s native `some` if available.\n  // Aliased as `any`.\n  var any = _.some = _.any = function(obj, iterator, context) {\n    iterator || (iterator = _.identity);\n    var result = false;\n    if (obj == null) return result;\n    if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);\n    each(obj, function(value, index, list) {\n      if (result || (result = iterator.call(context, value, index, list))) return breaker;\n    });\n    return !!result;\n  };\n\n  // Determine if a given value is included in the array or object using `===`.\n  // Aliased as `contains`.\n  _.include = _.contains = function(obj, target) {\n    var found = false;\n    if (obj == null) return found;\n    if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;\n    found = any(obj, function(value) {\n      return value === target;\n    });\n    return found;\n  };\n\n  // Invoke a method (with arguments) on every item in a collection.\n  _.invoke = function(obj, method) {\n    var args = slice.call(arguments, 2);\n    return _.map(obj, function(value) {\n      return (_.isFunction(method) ? method || value : value[method]).apply(value, args);\n    });\n  };\n\n  // Convenience version of a common use case of `map`: fetching a property.\n  _.pluck = function(obj, key) {\n    return _.map(obj, function(value){ return value[key]; });\n  };\n\n  // Return the maximum element or (element-based computation).\n  _.max = function(obj, iterator, context) {\n    if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);\n    if (!iterator && _.isEmpty(obj)) return -Infinity;\n    var result = {computed : -Infinity};\n    each(obj, function(value, index, list) {\n      var computed = iterator ? iterator.call(context, value, index, list) : value;\n      computed >= result.computed && (result = {value : value, computed : computed});\n    });\n    return result.value;\n  };\n\n  // Return the minimum element (or element-based computation).\n  _.min = function(obj, iterator, context) {\n    if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);\n    if (!iterator && _.isEmpty(obj)) return Infinity;\n    var result = {computed : Infinity};\n    each(obj, function(value, index, list) {\n      var computed = iterator ? iterator.call(context, value, index, list) : value;\n      computed < result.computed && (result = {value : value, computed : computed});\n    });\n    return result.value;\n  };\n\n  // Shuffle an array.\n  _.shuffle = function(obj) {\n    var shuffled = [], rand;\n    each(obj, function(value, index, list) {\n      if (index == 0) {\n        shuffled[0] = value;\n      } else {\n        rand = Math.floor(Math.random() * (index + 1));\n        shuffled[index] = shuffled[rand];\n        shuffled[rand] = value;\n      }\n    });\n    return shuffled;\n  };\n\n  // Sort the object's values by a criterion produced by an iterator.\n  _.sortBy = function(obj, iterator, context) {\n    return _.pluck(_.map(obj, function(value, index, list) {\n      return {\n        value : value,\n        criteria : iterator.call(context, value, index, list)\n      };\n    }).sort(function(left, right) {\n      var a = left.criteria, b = right.criteria;\n      return a < b ? -1 : a > b ? 1 : 0;\n    }), 'value');\n  };\n\n  // Groups the object's values by a criterion. Pass either a string attribute\n  // to group by, or a function that returns the criterion.\n  _.groupBy = function(obj, val) {\n    var result = {};\n    var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; };\n    each(obj, function(value, index) {\n      var key = iterator(value, index);\n      (result[key] || (result[key] = [])).push(value);\n    });\n    return result;\n  };\n\n  // Use a comparator function to figure out at what index an object should\n  // be inserted so as to maintain order. Uses binary search.\n  _.sortedIndex = function(array, obj, iterator) {\n    iterator || (iterator = _.identity);\n    var low = 0, high = array.length;\n    while (low < high) {\n      var mid = (low + high) >> 1;\n      iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;\n    }\n    return low;\n  };\n\n  // Safely convert anything iterable into a real, live array.\n  _.toArray = function(iterable) {\n    if (!iterable)                return [];\n    if (iterable.toArray)         return iterable.toArray();\n    if (_.isArray(iterable))      return slice.call(iterable);\n    if (_.isArguments(iterable))  return slice.call(iterable);\n    return _.values(iterable);\n  };\n\n  // Return the number of elements in an object.\n  _.size = function(obj) {\n    return _.toArray(obj).length;\n  };\n\n  // Array Functions\n  // ---------------\n\n  // Get the first element of an array. Passing **n** will return the first N\n  // values in the array. Aliased as `head`. The **guard** check allows it to work\n  // with `_.map`.\n  _.first = _.head = function(array, n, guard) {\n    return (n != null) && !guard ? slice.call(array, 0, n) : array[0];\n  };\n\n  // Returns everything but the last entry of the array. Especcialy useful on\n  // the arguments object. Passing **n** will return all the values in\n  // the array, excluding the last N. The **guard** check allows it to work with\n  // `_.map`.\n  _.initial = function(array, n, guard) {\n    return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));\n  };\n\n  // Get the last element of an array. Passing **n** will return the last N\n  // values in the array. The **guard** check allows it to work with `_.map`.\n  _.last = function(array, n, guard) {\n    if ((n != null) && !guard) {\n      return slice.call(array, Math.max(array.length - n, 0));\n    } else {\n      return array[array.length - 1];\n    }\n  };\n\n  // Returns everything but the first entry of the array. Aliased as `tail`.\n  // Especially useful on the arguments object. Passing an **index** will return\n  // the rest of the values in the array from that index onward. The **guard**\n  // check allows it to work with `_.map`.\n  _.rest = _.tail = function(array, index, guard) {\n    return slice.call(array, (index == null) || guard ? 1 : index);\n  };\n\n  // Trim out all falsy values from an array.\n  _.compact = function(array) {\n    return _.filter(array, function(value){ return !!value; });\n  };\n\n  // Return a completely flattened version of an array.\n  _.flatten = function(array, shallow) {\n    return _.reduce(array, function(memo, value) {\n      if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value));\n      memo[memo.length] = value;\n      return memo;\n    }, []);\n  };\n\n  // Return a version of the array that does not contain the specified value(s).\n  _.without = function(array) {\n    return _.difference(array, slice.call(arguments, 1));\n  };\n\n  // Produce a duplicate-free version of the array. If the array has already\n  // been sorted, you have the option of using a faster algorithm.\n  // Aliased as `unique`.\n  _.uniq = _.unique = function(array, isSorted, iterator) {\n    var initial = iterator ? _.map(array, iterator) : array;\n    var result = [];\n    _.reduce(initial, function(memo, el, i) {\n      if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) {\n        memo[memo.length] = el;\n        result[result.length] = array[i];\n      }\n      return memo;\n    }, []);\n    return result;\n  };\n\n  // Produce an array that contains the union: each distinct element from all of\n  // the passed-in arrays.\n  _.union = function() {\n    return _.uniq(_.flatten(arguments, true));\n  };\n\n  // Produce an array that contains every item shared between all the\n  // passed-in arrays. (Aliased as \"intersect\" for back-compat.)\n  _.intersection = _.intersect = function(array) {\n    var rest = slice.call(arguments, 1);\n    return _.filter(_.uniq(array), function(item) {\n      return _.every(rest, function(other) {\n        return _.indexOf(other, item) >= 0;\n      });\n    });\n  };\n\n  // Take the difference between one array and a number of other arrays.\n  // Only the elements present in just the first array will remain.\n  _.difference = function(array) {\n    var rest = _.flatten(slice.call(arguments, 1));\n    return _.filter(array, function(value){ return !_.include(rest, value); });\n  };\n\n  // Zip together multiple lists into a single array -- elements that share\n  // an index go together.\n  _.zip = function() {\n    var args = slice.call(arguments);\n    var length = _.max(_.pluck(args, 'length'));\n    var results = new Array(length);\n    for (var i = 0; i < length; i++) results[i] = _.pluck(args, \"\" + i);\n    return results;\n  };\n\n  // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),\n  // we need this function. Return the position of the first occurrence of an\n  // item in an array, or -1 if the item is not included in the array.\n  // Delegates to **ECMAScript 5**'s native `indexOf` if available.\n  // If the array is large and already in sort order, pass `true`\n  // for **isSorted** to use binary search.\n  _.indexOf = function(array, item, isSorted) {\n    if (array == null) return -1;\n    var i, l;\n    if (isSorted) {\n      i = _.sortedIndex(array, item);\n      return array[i] === item ? i : -1;\n    }\n    if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);\n    for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i;\n    return -1;\n  };\n\n  // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.\n  _.lastIndexOf = function(array, item) {\n    if (array == null) return -1;\n    if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);\n    var i = array.length;\n    while (i--) if (i in array && array[i] === item) return i;\n    return -1;\n  };\n\n  // Generate an integer Array containing an arithmetic progression. A port of\n  // the native Python `range()` function. See\n  // [the Python documentation](http://docs.python.org/library/functions.html#range).\n  _.range = function(start, stop, step) {\n    if (arguments.length <= 1) {\n      stop = start || 0;\n      start = 0;\n    }\n    step = arguments[2] || 1;\n\n    var len = Math.max(Math.ceil((stop - start) / step), 0);\n    var idx = 0;\n    var range = new Array(len);\n\n    while(idx < len) {\n      range[idx++] = start;\n      start += step;\n    }\n\n    return range;\n  };\n\n  // Function (ahem) Functions\n  // ------------------\n\n  // Reusable constructor function for prototype setting.\n  var ctor = function(){};\n\n  // Create a function bound to a given object (assigning `this`, and arguments,\n  // optionally). Binding with arguments is also known as `curry`.\n  // Delegates to **ECMAScript 5**'s native `Function.bind` if available.\n  // We check for `func.bind` first, to fail fast when `func` is undefined.\n  _.bind = function bind(func, context) {\n    var bound, args;\n    if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));\n    if (!_.isFunction(func)) throw new TypeError;\n    args = slice.call(arguments, 2);\n    return bound = function() {\n      if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));\n      ctor.prototype = func.prototype;\n      var self = new ctor;\n      var result = func.apply(self, args.concat(slice.call(arguments)));\n      if (Object(result) === result) return result;\n      return self;\n    };\n  };\n\n  // Bind all of an object's methods to that object. Useful for ensuring that\n  // all callbacks defined on an object belong to it.\n  _.bindAll = function(obj) {\n    var funcs = slice.call(arguments, 1);\n    if (funcs.length == 0) funcs = _.functions(obj);\n    each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });\n    return obj;\n  };\n\n  // Memoize an expensive function by storing its results.\n  _.memoize = function(func, hasher) {\n    var memo = {};\n    hasher || (hasher = _.identity);\n    return function() {\n      var key = hasher.apply(this, arguments);\n      return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));\n    };\n  };\n\n  // Delays a function for the given number of milliseconds, and then calls\n  // it with the arguments supplied.\n  _.delay = function(func, wait) {\n    var args = slice.call(arguments, 2);\n    return setTimeout(function(){ return func.apply(func, args); }, wait);\n  };\n\n  // Defers a function, scheduling it to run after the current call stack has\n  // cleared.\n  _.defer = function(func) {\n    return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));\n  };\n\n  // Returns a function, that, when invoked, will only be triggered at most once\n  // during a given window of time.\n  _.throttle = function(func, wait) {\n    var context, args, timeout, throttling, more;\n    var whenDone = _.debounce(function(){ more = throttling = false; }, wait);\n    return function() {\n      context = this; args = arguments;\n      var later = function() {\n        timeout = null;\n        if (more) func.apply(context, args);\n        whenDone();\n      };\n      if (!timeout) timeout = setTimeout(later, wait);\n      if (throttling) {\n        more = true;\n      } else {\n        func.apply(context, args);\n      }\n      whenDone();\n      throttling = true;\n    };\n  };\n\n  // Returns a function, that, as long as it continues to be invoked, will not\n  // be triggered. The function will be called after it stops being called for\n  // N milliseconds.\n  _.debounce = function(func, wait) {\n    var timeout;\n    return function() {\n      var context = this, args = arguments;\n      var later = function() {\n        timeout = null;\n        func.apply(context, args);\n      };\n      clearTimeout(timeout);\n      timeout = setTimeout(later, wait);\n    };\n  };\n\n  // Returns a function that will be executed at most one time, no matter how\n  // often you call it. Useful for lazy initialization.\n  _.once = function(func) {\n    var ran = false, memo;\n    return function() {\n      if (ran) return memo;\n      ran = true;\n      return memo = func.apply(this, arguments);\n    };\n  };\n\n  // Returns the first function passed as an argument to the second,\n  // allowing you to adjust arguments, run code before and after, and\n  // conditionally execute the original function.\n  _.wrap = function(func, wrapper) {\n    return function() {\n      var args = [func].concat(slice.call(arguments, 0));\n      return wrapper.apply(this, args);\n    };\n  };\n\n  // Returns a function that is the composition of a list of functions, each\n  // consuming the return value of the function that follows.\n  _.compose = function() {\n    var funcs = arguments;\n    return function() {\n      var args = arguments;\n      for (var i = funcs.length - 1; i >= 0; i--) {\n        args = [funcs[i].apply(this, args)];\n      }\n      return args[0];\n    };\n  };\n\n  // Returns a function that will only be executed after being called N times.\n  _.after = function(times, func) {\n    if (times <= 0) return func();\n    return function() {\n      if (--times < 1) { return func.apply(this, arguments); }\n    };\n  };\n\n  // Object Functions\n  // ----------------\n\n  // Retrieve the names of an object's properties.\n  // Delegates to **ECMAScript 5**'s native `Object.keys`\n  _.keys = nativeKeys || function(obj) {\n    if (obj !== Object(obj)) throw new TypeError('Invalid object');\n    var keys = [];\n    for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;\n    return keys;\n  };\n\n  // Retrieve the values of an object's properties.\n  _.values = function(obj) {\n    return _.map(obj, _.identity);\n  };\n\n  // Return a sorted list of the function names available on the object.\n  // Aliased as `methods`\n  _.functions = _.methods = function(obj) {\n    var names = [];\n    for (var key in obj) {\n      if (_.isFunction(obj[key])) names.push(key);\n    }\n    return names.sort();\n  };\n\n  // Extend a given object with all the properties in passed-in object(s).\n  _.extend = function(obj) {\n    each(slice.call(arguments, 1), function(source) {\n      for (var prop in source) {\n        obj[prop] = source[prop];\n      }\n    });\n    return obj;\n  };\n\n  // Fill in a given object with default properties.\n  _.defaults = function(obj) {\n    each(slice.call(arguments, 1), function(source) {\n      for (var prop in source) {\n        if (obj[prop] == null) obj[prop] = source[prop];\n      }\n    });\n    return obj;\n  };\n\n  // Create a (shallow-cloned) duplicate of an object.\n  _.clone = function(obj) {\n    if (!_.isObject(obj)) return obj;\n    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);\n  };\n\n  // Invokes interceptor with the obj, and then returns obj.\n  // The primary purpose of this method is to \"tap into\" a method chain, in\n  // order to perform operations on intermediate results within the chain.\n  _.tap = function(obj, interceptor) {\n    interceptor(obj);\n    return obj;\n  };\n\n  // Internal recursive comparison function.\n  function eq(a, b, stack) {\n    // Identical objects are equal. `0 === -0`, but they aren't identical.\n    // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.\n    if (a === b) return a !== 0 || 1 / a == 1 / b;\n    // A strict comparison is necessary because `null == undefined`.\n    if (a == null || b == null) return a === b;\n    // Unwrap any wrapped objects.\n    if (a._chain) a = a._wrapped;\n    if (b._chain) b = b._wrapped;\n    // Invoke a custom `isEqual` method if one is provided.\n    if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b);\n    if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a);\n    // Compare `[[Class]]` names.\n    var className = toString.call(a);\n    if (className != toString.call(b)) return false;\n    switch (className) {\n      // Strings, numbers, dates, and booleans are compared by value.\n      case '[object String]':\n        // Primitives and their corresponding object wrappers are equivalent; thus, `\"5\"` is\n        // equivalent to `new String(\"5\")`.\n        return a == String(b);\n      case '[object Number]':\n        // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for\n        // other numeric values.\n        return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);\n      case '[object Date]':\n      case '[object Boolean]':\n        // Coerce dates and booleans to numeric primitive values. Dates are compared by their\n        // millisecond representations. Note that invalid dates with millisecond representations\n        // of `NaN` are not equivalent.\n        return +a == +b;\n      // RegExps are compared by their source patterns and flags.\n      case '[object RegExp]':\n        return a.source == b.source &&\n               a.global == b.global &&\n               a.multiline == b.multiline &&\n               a.ignoreCase == b.ignoreCase;\n    }\n    if (typeof a != 'object' || typeof b != 'object') return false;\n    // Assume equality for cyclic structures. The algorithm for detecting cyclic\n    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n    var length = stack.length;\n    while (length--) {\n      // Linear search. Performance is inversely proportional to the number of\n      // unique nested structures.\n      if (stack[length] == a) return true;\n    }\n    // Add the first object to the stack of traversed objects.\n    stack.push(a);\n    var size = 0, result = true;\n    // Recursively compare objects and arrays.\n    if (className == '[object Array]') {\n      // Compare array lengths to determine if a deep comparison is necessary.\n      size = a.length;\n      result = size == b.length;\n      if (result) {\n        // Deep compare the contents, ignoring non-numeric properties.\n        while (size--) {\n          // Ensure commutative equality for sparse arrays.\n          if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break;\n        }\n      }\n    } else {\n      // Objects with different constructors are not equivalent.\n      if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false;\n      // Deep compare objects.\n      for (var key in a) {\n        if (_.has(a, key)) {\n          // Count the expected number of properties.\n          size++;\n          // Deep compare each member.\n          if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break;\n        }\n      }\n      // Ensure that both objects contain the same number of properties.\n      if (result) {\n        for (key in b) {\n          if (_.has(b, key) && !(size--)) break;\n        }\n        result = !size;\n      }\n    }\n    // Remove the first object from the stack of traversed objects.\n    stack.pop();\n    return result;\n  }\n\n  // Perform a deep comparison to check if two objects are equal.\n  _.isEqual = function(a, b) {\n    return eq(a, b, []);\n  };\n\n  // Is a given array, string, or object empty?\n  // An \"empty\" object has no enumerable own-properties.\n  _.isEmpty = function(obj) {\n    if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;\n    for (var key in obj) if (_.has(obj, key)) return false;\n    return true;\n  };\n\n  // Is a given value a DOM element?\n  _.isElement = function(obj) {\n    return !!(obj && obj.nodeType == 1);\n  };\n\n  // Is a given value an array?\n  // Delegates to ECMA5's native Array.isArray\n  _.isArray = nativeIsArray || function(obj) {\n    return toString.call(obj) == '[object Array]';\n  };\n\n  // Is a given variable an object?\n  _.isObject = function(obj) {\n    return obj === Object(obj);\n  };\n\n  // Is a given variable an arguments object?\n  _.isArguments = function(obj) {\n    return toString.call(obj) == '[object Arguments]';\n  };\n  if (!_.isArguments(arguments)) {\n    _.isArguments = function(obj) {\n      return !!(obj && _.has(obj, 'callee'));\n    };\n  }\n\n  // Is a given value a function?\n  _.isFunction = function(obj) {\n    return toString.call(obj) == '[object Function]';\n  };\n\n  // Is a given value a string?\n  _.isString = function(obj) {\n    return toString.call(obj) == '[object String]';\n  };\n\n  // Is a given value a number?\n  _.isNumber = function(obj) {\n    return toString.call(obj) == '[object Number]';\n  };\n\n  // Is the given value `NaN`?\n  _.isNaN = function(obj) {\n    // `NaN` is the only value for which `===` is not reflexive.\n    return obj !== obj;\n  };\n\n  // Is a given value a boolean?\n  _.isBoolean = function(obj) {\n    return obj === true || obj === false || toString.call(obj) == '[object Boolean]';\n  };\n\n  // Is a given value a date?\n  _.isDate = function(obj) {\n    return toString.call(obj) == '[object Date]';\n  };\n\n  // Is the given value a regular expression?\n  _.isRegExp = function(obj) {\n    return toString.call(obj) == '[object RegExp]';\n  };\n\n  // Is a given value equal to null?\n  _.isNull = function(obj) {\n    return obj === null;\n  };\n\n  // Is a given variable undefined?\n  _.isUndefined = function(obj) {\n    return obj === void 0;\n  };\n\n  // Has own property?\n  _.has = function(obj, key) {\n    return hasOwnProperty.call(obj, key);\n  };\n\n  // Utility Functions\n  // -----------------\n\n  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its\n  // previous owner. Returns a reference to the Underscore object.\n  _.noConflict = function() {\n    root._ = previousUnderscore;\n    return this;\n  };\n\n  // Keep the identity function around for default iterators.\n  _.identity = function(value) {\n    return value;\n  };\n\n  // Run a function **n** times.\n  _.times = function (n, iterator, context) {\n    for (var i = 0; i < n; i++) iterator.call(context, i);\n  };\n\n  // Escape a string for HTML interpolation.\n  _.escape = function(string) {\n    return (''+string).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\\//g,'&#x2F;');\n  };\n\n  // Add your own custom functions to the Underscore object, ensuring that\n  // they're correctly added to the OOP wrapper as well.\n  _.mixin = function(obj) {\n    each(_.functions(obj), function(name){\n      addToWrapper(name, _[name] = obj[name]);\n    });\n  };\n\n  // Generate a unique integer id (unique within the entire client session).\n  // Useful for temporary DOM ids.\n  var idCounter = 0;\n  _.uniqueId = function(prefix) {\n    var id = idCounter++;\n    return prefix ? prefix + id : id;\n  };\n\n  // By default, Underscore uses ERB-style template delimiters, change the\n  // following template settings to use alternative delimiters.\n  _.templateSettings = {\n    evaluate    : /<%([\\s\\S]+?)%>/g,\n    interpolate : /<%=([\\s\\S]+?)%>/g,\n    escape      : /<%-([\\s\\S]+?)%>/g\n  };\n\n  // When customizing `templateSettings`, if you don't want to define an\n  // interpolation, evaluation or escaping regex, we need one that is\n  // guaranteed not to match.\n  var noMatch = /.^/;\n\n  // Within an interpolation, evaluation, or escaping, remove HTML escaping\n  // that had been previously added.\n  var unescape = function(code) {\n    return code.replace(/\\\\\\\\/g, '\\\\').replace(/\\\\'/g, \"'\");\n  };\n\n  // JavaScript micro-templating, similar to John Resig's implementation.\n  // Underscore templating handles arbitrary delimiters, preserves whitespace,\n  // and correctly escapes quotes within interpolated code.\n  _.template = function(str, data) {\n    var c  = _.templateSettings;\n    var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +\n      'with(obj||{}){__p.push(\\'' +\n      str.replace(/\\\\/g, '\\\\\\\\')\n         .replace(/'/g, \"\\\\'\")\n         .replace(c.escape || noMatch, function(match, code) {\n           return \"',_.escape(\" + unescape(code) + \"),'\";\n         })\n         .replace(c.interpolate || noMatch, function(match, code) {\n           return \"',\" + unescape(code) + \",'\";\n         })\n         .replace(c.evaluate || noMatch, function(match, code) {\n           return \"');\" + unescape(code).replace(/[\\r\\n\\t]/g, ' ') + \";__p.push('\";\n         })\n         .replace(/\\r/g, '\\\\r')\n         .replace(/\\n/g, '\\\\n')\n         .replace(/\\t/g, '\\\\t')\n         + \"');}return __p.join('');\";\n    var func = new Function('obj', '_', tmpl);\n    if (data) return func(data, _);\n    return function(data) {\n      return func.call(this, data, _);\n    };\n  };\n\n  // Add a \"chain\" function, which will delegate to the wrapper.\n  _.chain = function(obj) {\n    return _(obj).chain();\n  };\n\n  // The OOP Wrapper\n  // ---------------\n\n  // If Underscore is called as a function, it returns a wrapped object that\n  // can be used OO-style. This wrapper holds altered versions of all the\n  // underscore functions. Wrapped objects may be chained.\n  var wrapper = function(obj) { this._wrapped = obj; };\n\n  // Expose `wrapper.prototype` as `_.prototype`\n  _.prototype = wrapper.prototype;\n\n  // Helper function to continue chaining intermediate results.\n  var result = function(obj, chain) {\n    return chain ? _(obj).chain() : obj;\n  };\n\n  // A method to easily add functions to the OOP wrapper.\n  var addToWrapper = function(name, func) {\n    wrapper.prototype[name] = function() {\n      var args = slice.call(arguments);\n      unshift.call(args, this._wrapped);\n      return result(func.apply(_, args), this._chain);\n    };\n  };\n\n  // Add all of the Underscore functions to the wrapper object.\n  _.mixin(_);\n\n  // Add all mutator Array functions to the wrapper.\n  each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {\n    var method = ArrayProto[name];\n    wrapper.prototype[name] = function() {\n      var wrapped = this._wrapped;\n      method.apply(wrapped, arguments);\n      var length = wrapped.length;\n      if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0];\n      return result(wrapped, this._chain);\n    };\n  });\n\n  // Add all accessor Array functions to the wrapper.\n  each(['concat', 'join', 'slice'], function(name) {\n    var method = ArrayProto[name];\n    wrapper.prototype[name] = function() {\n      return result(method.apply(this._wrapped, arguments), this._chain);\n    };\n  });\n\n  // Start chaining a wrapped Underscore object.\n  wrapper.prototype.chain = function() {\n    this._chain = true;\n    return this;\n  };\n\n  // Extracts the result from a wrapped and chained object.\n  wrapper.prototype.value = function() {\n    return this._wrapped;\n  };\n\n}).call(this);\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore.js",
    "content": "// Underscore.js 1.3.1\n// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.\n// Underscore is freely distributable under the MIT license.\n// Portions of Underscore are inspired or borrowed from Prototype,\n// Oliver Steele's Functional, and John Resig's Micro-Templating.\n// For all details and documentation:\n// http://documentcloud.github.com/underscore\n(function(){function q(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null||c==null)return a===c;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return false;switch(e){case \"[object String]\":return a==String(c);case \"[object Number]\":return a!=+a?c!=+c:a==0?1/a==1/c:a==+c;case \"[object Date]\":case \"[object Boolean]\":return+a==+c;case \"[object RegExp]\":return a.source==\nc.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if(typeof a!=\"object\"||typeof c!=\"object\")return false;for(var f=d.length;f--;)if(d[f]==a)return true;d.push(a);var f=0,g=true;if(e==\"[object Array]\"){if(f=a.length,g=f==c.length)for(;f--;)if(!(g=f in a==f in c&&q(a[f],c[f],d)))break}else{if(\"constructor\"in a!=\"constructor\"in c||a.constructor!=c.constructor)return false;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&q(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,\nh)&&!f--)break;g=!f}}d.pop();return g}var r=this,G=r._,n={},k=Array.prototype,o=Object.prototype,i=k.slice,H=k.unshift,l=o.toString,I=o.hasOwnProperty,w=k.forEach,x=k.map,y=k.reduce,z=k.reduceRight,A=k.filter,B=k.every,C=k.some,p=k.indexOf,D=k.lastIndexOf,o=Array.isArray,J=Object.keys,s=Function.prototype.bind,b=function(a){return new m(a)};if(typeof exports!==\"undefined\"){if(typeof module!==\"undefined\"&&module.exports)exports=module.exports=b;exports._=b}else r._=b;b.VERSION=\"1.3.1\";var j=b.each=\nb.forEach=function(a,c,d){if(a!=null)if(w&&a.forEach===w)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===n)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===n)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(x&&a.map===x)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==\nnull&&(a=[]);if(y&&a.reduce===y)return e&&(c=b.bind(c,e)),f?a.reduce(c,d):a.reduce(c);j(a,function(a,b,i){f?d=c.call(e,d,a,b,i):(d=a,f=true)});if(!f)throw new TypeError(\"Reduce of empty array with no initial value\");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(z&&a.reduceRight===z)return e&&(c=b.bind(c,e)),f?a.reduceRight(c,d):a.reduceRight(c);var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=\nfunction(a,c,b){var e;E(a,function(a,g,h){if(c.call(b,a,g,h))return e=a,true});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(A&&a.filter===A)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(B&&a.every===B)return a.every(c,b);j(a,function(a,g,h){if(!(e=\ne&&c.call(b,a,g,h)))return n});return e};var E=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(C&&a.some===C)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return n});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;return p&&a.indexOf===p?a.indexOf(c)!=-1:b=E(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=\nfunction(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&(e={value:a,computed:b})});\nreturn e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){f==0?b[0]=a:(d=Math.floor(Math.random()*(f+1)),b[f]=b[d],b[d]=a)});return b};b.sortBy=function(a,c,d){return b.pluck(b.map(a,function(a,b,g){return{value:a,criteria:c.call(d,a,b,g)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c<d?-1:c>d?1:0}),\"value\")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,\nc,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:a.toArray?a.toArray():b.isArray(a)?i.call(a):b.isArguments(a)?i.call(a):b.values(a)};b.size=function(a){return b.toArray(a).length};b.first=b.head=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=\nb.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,e=[];b.reduce(d,function(d,g,h){if(0==h||(c===true?b.last(d)!=g:!b.include(d,g)))d[d.length]=g,e[e.length]=a[h];return d},[]);\nreturn e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1));return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,\"length\")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,\"\"+e);return d};b.indexOf=function(a,c,\nd){if(a==null)return-1;var e;if(d)return d=b.sortedIndex(a,c),a[d]===c?d:-1;if(p&&a.indexOf===p)return a.indexOf(c);for(d=0,e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(D&&a.lastIndexOf===D)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){arguments.length<=1&&(b=a||0,a=0);for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;)g[f++]=a,a+=d;return g};\nvar F=function(){};b.bind=function(a,c){var d,e;if(a.bind===s&&s)return s.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));F.prototype=a.prototype;var b=new F,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,\nc){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(a,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i=b.debounce(function(){h=g=false},c);return function(){d=this;e=arguments;var b;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);i()},c));g?h=true:\na.apply(d,e);i();g=true}};b.debounce=function(a,b){var d;return function(){var e=this,f=arguments;clearTimeout(d);d=setTimeout(function(){d=null;a.apply(e,f)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};\nb.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=J||function(a){if(a!==Object(a))throw new TypeError(\"Invalid object\");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.defaults=function(a){j(i.call(arguments,\n1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return q(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=o||function(a){return l.call(a)==\"[object Array]\"};b.isObject=function(a){return a===Object(a)};\nb.isArguments=function(a){return l.call(a)==\"[object Arguments]\"};if(!b.isArguments(arguments))b.isArguments=function(a){return!(!a||!b.has(a,\"callee\"))};b.isFunction=function(a){return l.call(a)==\"[object Function]\"};b.isString=function(a){return l.call(a)==\"[object String]\"};b.isNumber=function(a){return l.call(a)==\"[object Number]\"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)==\"[object Boolean]\"};b.isDate=function(a){return l.call(a)==\"[object Date]\"};\nb.isRegExp=function(a){return l.call(a)==\"[object RegExp]\"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,b){return I.call(a,b)};b.noConflict=function(){r._=G;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(\"\"+a).replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\").replace(/\"/g,\"&quot;\").replace(/'/g,\"&#x27;\").replace(/\\//g,\"&#x2F;\")};b.mixin=function(a){j(b.functions(a),\nfunction(c){K(c,b[c]=a[c])})};var L=0;b.uniqueId=function(a){var b=L++;return a?a+b:b};b.templateSettings={evaluate:/<%([\\s\\S]+?)%>/g,interpolate:/<%=([\\s\\S]+?)%>/g,escape:/<%-([\\s\\S]+?)%>/g};var t=/.^/,u=function(a){return a.replace(/\\\\\\\\/g,\"\\\\\").replace(/\\\\'/g,\"'\")};b.template=function(a,c){var d=b.templateSettings,d=\"var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('\"+a.replace(/\\\\/g,\"\\\\\\\\\").replace(/'/g,\"\\\\'\").replace(d.escape||t,function(a,b){return\"',_.escape(\"+\nu(b)+\"),'\"}).replace(d.interpolate||t,function(a,b){return\"',\"+u(b)+\",'\"}).replace(d.evaluate||t,function(a,b){return\"');\"+u(b).replace(/[\\r\\n\\t]/g,\" \")+\";__p.push('\"}).replace(/\\r/g,\"\\\\r\").replace(/\\n/g,\"\\\\n\").replace(/\\t/g,\"\\\\t\")+\"');}return __p.join('');\",e=new Function(\"obj\",\"_\",d);return c?e(c,b):function(a){return e.call(this,a,b)}};b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var v=function(a,c){return c?b(a).chain():a},K=function(a,c){m.prototype[a]=\nfunction(){var a=i.call(arguments);H.call(a,this._wrapped);return v(c.apply(b,a),this._chain)}};b.mixin(b);j(\"pop,push,reverse,shift,sort,splice,unshift\".split(\",\"),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a==\"shift\"||a==\"splice\")&&e===0&&delete d[0];return v(d,this._chain)}});j([\"concat\",\"join\",\"slice\"],function(a){var b=k[a];m.prototype[a]=function(){return v(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=\ntrue;return this};m.prototype.value=function(){return this._wrapped}}).call(this);\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/websupport.js",
    "content": "/*\n * websupport.js\n * ~~~~~~~~~~~~~\n *\n * sphinx.websupport utilities for all documentation.\n *\n * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.\n * :license: BSD, see LICENSE for details.\n *\n */\n\n(function($) {\n  $.fn.autogrow = function() {\n    return this.each(function() {\n    var textarea = this;\n\n    $.fn.autogrow.resize(textarea);\n\n    $(textarea)\n      .focus(function() {\n        textarea.interval = setInterval(function() {\n          $.fn.autogrow.resize(textarea);\n        }, 500);\n      })\n      .blur(function() {\n        clearInterval(textarea.interval);\n      });\n    });\n  };\n\n  $.fn.autogrow.resize = function(textarea) {\n    var lineHeight = parseInt($(textarea).css('line-height'), 10);\n    var lines = textarea.value.split('\\n');\n    var columns = textarea.cols;\n    var lineCount = 0;\n    $.each(lines, function() {\n      lineCount += Math.ceil(this.length / columns) || 1;\n    });\n    var height = lineHeight * (lineCount + 1);\n    $(textarea).css('height', height);\n  };\n})(jQuery);\n\n(function($) {\n  var comp, by;\n\n  function init() {\n    initEvents();\n    initComparator();\n  }\n\n  function initEvents() {\n    $(document).on(\"click\", 'a.comment-close', function(event) {\n      event.preventDefault();\n      hide($(this).attr('id').substring(2));\n    });\n    $(document).on(\"click\", 'a.vote', function(event) {\n      event.preventDefault();\n      handleVote($(this));\n    });\n    $(document).on(\"click\", 'a.reply', function(event) {\n      event.preventDefault();\n      openReply($(this).attr('id').substring(2));\n    });\n    $(document).on(\"click\", 'a.close-reply', function(event) {\n      event.preventDefault();\n      closeReply($(this).attr('id').substring(2));\n    });\n    $(document).on(\"click\", 'a.sort-option', function(event) {\n      event.preventDefault();\n      handleReSort($(this));\n    });\n    $(document).on(\"click\", 'a.show-proposal', function(event) {\n      event.preventDefault();\n      showProposal($(this).attr('id').substring(2));\n    });\n    $(document).on(\"click\", 'a.hide-proposal', function(event) {\n      event.preventDefault();\n      hideProposal($(this).attr('id').substring(2));\n    });\n    $(document).on(\"click\", 'a.show-propose-change', function(event) {\n      event.preventDefault();\n      showProposeChange($(this).attr('id').substring(2));\n    });\n    $(document).on(\"click\", 'a.hide-propose-change', function(event) {\n      event.preventDefault();\n      hideProposeChange($(this).attr('id').substring(2));\n    });\n    $(document).on(\"click\", 'a.accept-comment', function(event) {\n      event.preventDefault();\n      acceptComment($(this).attr('id').substring(2));\n    });\n    $(document).on(\"click\", 'a.delete-comment', function(event) {\n      event.preventDefault();\n      deleteComment($(this).attr('id').substring(2));\n    });\n    $(document).on(\"click\", 'a.comment-markup', function(event) {\n      event.preventDefault();\n      toggleCommentMarkupBox($(this).attr('id').substring(2));\n    });\n  }\n\n  /**\n   * Set comp, which is a comparator function used for sorting and\n   * inserting comments into the list.\n   */\n  function setComparator() {\n    // If the first three letters are \"asc\", sort in ascending order\n    // and remove the prefix.\n    if (by.substring(0,3) == 'asc') {\n      var i = by.substring(3);\n      comp = function(a, b) { return a[i] - b[i]; };\n    } else {\n      // Otherwise sort in descending order.\n      comp = function(a, b) { return b[by] - a[by]; };\n    }\n\n    // Reset link styles and format the selected sort option.\n    $('a.sel').attr('href', '#').removeClass('sel');\n    $('a.by' + by).removeAttr('href').addClass('sel');\n  }\n\n  /**\n   * Create a comp function. If the user has preferences stored in\n   * the sortBy cookie, use those, otherwise use the default.\n   */\n  function initComparator() {\n    by = 'rating'; // Default to sort by rating.\n    // If the sortBy cookie is set, use that instead.\n    if (document.cookie.length > 0) {\n      var start = document.cookie.indexOf('sortBy=');\n      if (start != -1) {\n        start = start + 7;\n        var end = document.cookie.indexOf(\";\", start);\n        if (end == -1) {\n          end = document.cookie.length;\n          by = unescape(document.cookie.substring(start, end));\n        }\n      }\n    }\n    setComparator();\n  }\n\n  /**\n   * Show a comment div.\n   */\n  function show(id) {\n    $('#ao' + id).hide();\n    $('#ah' + id).show();\n    var context = $.extend({id: id}, opts);\n    var popup = $(renderTemplate(popupTemplate, context)).hide();\n    popup.find('textarea[name=\"proposal\"]').hide();\n    popup.find('a.by' + by).addClass('sel');\n    var form = popup.find('#cf' + id);\n    form.submit(function(event) {\n      event.preventDefault();\n      addComment(form);\n    });\n    $('#s' + id).after(popup);\n    popup.slideDown('fast', function() {\n      getComments(id);\n    });\n  }\n\n  /**\n   * Hide a comment div.\n   */\n  function hide(id) {\n    $('#ah' + id).hide();\n    $('#ao' + id).show();\n    var div = $('#sc' + id);\n    div.slideUp('fast', function() {\n      div.remove();\n    });\n  }\n\n  /**\n   * Perform an ajax request to get comments for a node\n   * and insert the comments into the comments tree.\n   */\n  function getComments(id) {\n    $.ajax({\n     type: 'GET',\n     url: opts.getCommentsURL,\n     data: {node: id},\n     success: function(data, textStatus, request) {\n       var ul = $('#cl' + id);\n       var speed = 100;\n       $('#cf' + id)\n         .find('textarea[name=\"proposal\"]')\n         .data('source', data.source);\n\n       if (data.comments.length === 0) {\n         ul.html('<li>No comments yet.</li>');\n         ul.data('empty', true);\n       } else {\n         // If there are comments, sort them and put them in the list.\n         var comments = sortComments(data.comments);\n         speed = data.comments.length * 100;\n         appendComments(comments, ul);\n         ul.data('empty', false);\n       }\n       $('#cn' + id).slideUp(speed + 200);\n       ul.slideDown(speed);\n     },\n     error: function(request, textStatus, error) {\n       showError('Oops, there was a problem retrieving the comments.');\n     },\n     dataType: 'json'\n    });\n  }\n\n  /**\n   * Add a comment via ajax and insert the comment into the comment tree.\n   */\n  function addComment(form) {\n    var node_id = form.find('input[name=\"node\"]').val();\n    var parent_id = form.find('input[name=\"parent\"]').val();\n    var text = form.find('textarea[name=\"comment\"]').val();\n    var proposal = form.find('textarea[name=\"proposal\"]').val();\n\n    if (text == '') {\n      showError('Please enter a comment.');\n      return;\n    }\n\n    // Disable the form that is being submitted.\n    form.find('textarea,input').attr('disabled', 'disabled');\n\n    // Send the comment to the server.\n    $.ajax({\n      type: \"POST\",\n      url: opts.addCommentURL,\n      dataType: 'json',\n      data: {\n        node: node_id,\n        parent: parent_id,\n        text: text,\n        proposal: proposal\n      },\n      success: function(data, textStatus, error) {\n        // Reset the form.\n        if (node_id) {\n          hideProposeChange(node_id);\n        }\n        form.find('textarea')\n          .val('')\n          .add(form.find('input'))\n          .removeAttr('disabled');\n\tvar ul = $('#cl' + (node_id || parent_id));\n        if (ul.data('empty')) {\n          $(ul).empty();\n          ul.data('empty', false);\n        }\n        insertComment(data.comment);\n        var ao = $('#ao' + node_id);\n        ao.find('img').attr({'src': opts.commentBrightImage});\n        if (node_id) {\n          // if this was a \"root\" comment, remove the commenting box\n          // (the user can get it back by reopening the comment popup)\n          $('#ca' + node_id).slideUp();\n        }\n      },\n      error: function(request, textStatus, error) {\n        form.find('textarea,input').removeAttr('disabled');\n        showError('Oops, there was a problem adding the comment.');\n      }\n    });\n  }\n\n  /**\n   * Recursively append comments to the main comment list and children\n   * lists, creating the comment tree.\n   */\n  function appendComments(comments, ul) {\n    $.each(comments, function() {\n      var div = createCommentDiv(this);\n      ul.append($(document.createElement('li')).html(div));\n      appendComments(this.children, div.find('ul.comment-children'));\n      // To avoid stagnating data, don't store the comments children in data.\n      this.children = null;\n      div.data('comment', this);\n    });\n  }\n\n  /**\n   * After adding a new comment, it must be inserted in the correct\n   * location in the comment tree.\n   */\n  function insertComment(comment) {\n    var div = createCommentDiv(comment);\n\n    // To avoid stagnating data, don't store the comments children in data.\n    comment.children = null;\n    div.data('comment', comment);\n\n    var ul = $('#cl' + (comment.node || comment.parent));\n    var siblings = getChildren(ul);\n\n    var li = $(document.createElement('li'));\n    li.hide();\n\n    // Determine where in the parents children list to insert this comment.\n    for(var i=0; i < siblings.length; i++) {\n      if (comp(comment, siblings[i]) <= 0) {\n        $('#cd' + siblings[i].id)\n          .parent()\n          .before(li.html(div));\n        li.slideDown('fast');\n        return;\n      }\n    }\n\n    // If we get here, this comment rates lower than all the others,\n    // or it is the only comment in the list.\n    ul.append(li.html(div));\n    li.slideDown('fast');\n  }\n\n  function acceptComment(id) {\n    $.ajax({\n      type: 'POST',\n      url: opts.acceptCommentURL,\n      data: {id: id},\n      success: function(data, textStatus, request) {\n        $('#cm' + id).fadeOut('fast');\n        $('#cd' + id).removeClass('moderate');\n      },\n      error: function(request, textStatus, error) {\n        showError('Oops, there was a problem accepting the comment.');\n      }\n    });\n  }\n\n  function deleteComment(id) {\n    $.ajax({\n      type: 'POST',\n      url: opts.deleteCommentURL,\n      data: {id: id},\n      success: function(data, textStatus, request) {\n        var div = $('#cd' + id);\n        if (data == 'delete') {\n          // Moderator mode: remove the comment and all children immediately\n          div.slideUp('fast', function() {\n            div.remove();\n          });\n          return;\n        }\n        // User mode: only mark the comment as deleted\n        div\n          .find('span.user-id:first')\n          .text('[deleted]').end()\n          .find('div.comment-text:first')\n          .text('[deleted]').end()\n          .find('#cm' + id + ', #dc' + id + ', #ac' + id + ', #rc' + id +\n                ', #sp' + id + ', #hp' + id + ', #cr' + id + ', #rl' + id)\n          .remove();\n        var comment = div.data('comment');\n        comment.username = '[deleted]';\n        comment.text = '[deleted]';\n        div.data('comment', comment);\n      },\n      error: function(request, textStatus, error) {\n        showError('Oops, there was a problem deleting the comment.');\n      }\n    });\n  }\n\n  function showProposal(id) {\n    $('#sp' + id).hide();\n    $('#hp' + id).show();\n    $('#pr' + id).slideDown('fast');\n  }\n\n  function hideProposal(id) {\n    $('#hp' + id).hide();\n    $('#sp' + id).show();\n    $('#pr' + id).slideUp('fast');\n  }\n\n  function showProposeChange(id) {\n    $('#pc' + id).hide();\n    $('#hc' + id).show();\n    var textarea = $('#pt' + id);\n    textarea.val(textarea.data('source'));\n    $.fn.autogrow.resize(textarea[0]);\n    textarea.slideDown('fast');\n  }\n\n  function hideProposeChange(id) {\n    $('#hc' + id).hide();\n    $('#pc' + id).show();\n    var textarea = $('#pt' + id);\n    textarea.val('').removeAttr('disabled');\n    textarea.slideUp('fast');\n  }\n\n  function toggleCommentMarkupBox(id) {\n    $('#mb' + id).toggle();\n  }\n\n  /** Handle when the user clicks on a sort by link. */\n  function handleReSort(link) {\n    var classes = link.attr('class').split(/\\s+/);\n    for (var i=0; i<classes.length; i++) {\n      if (classes[i] != 'sort-option') {\n\tby = classes[i].substring(2);\n      }\n    }\n    setComparator();\n    // Save/update the sortBy cookie.\n    var expiration = new Date();\n    expiration.setDate(expiration.getDate() + 365);\n    document.cookie= 'sortBy=' + escape(by) +\n                     ';expires=' + expiration.toUTCString();\n    $('ul.comment-ul').each(function(index, ul) {\n      var comments = getChildren($(ul), true);\n      comments = sortComments(comments);\n      appendComments(comments, $(ul).empty());\n    });\n  }\n\n  /**\n   * Function to process a vote when a user clicks an arrow.\n   */\n  function handleVote(link) {\n    if (!opts.voting) {\n      showError(\"You'll need to login to vote.\");\n      return;\n    }\n\n    var id = link.attr('id');\n    if (!id) {\n      // Didn't click on one of the voting arrows.\n      return;\n    }\n    // If it is an unvote, the new vote value is 0,\n    // Otherwise it's 1 for an upvote, or -1 for a downvote.\n    var value = 0;\n    if (id.charAt(1) != 'u') {\n      value = id.charAt(0) == 'u' ? 1 : -1;\n    }\n    // The data to be sent to the server.\n    var d = {\n      comment_id: id.substring(2),\n      value: value\n    };\n\n    // Swap the vote and unvote links.\n    link.hide();\n    $('#' + id.charAt(0) + (id.charAt(1) == 'u' ? 'v' : 'u') + d.comment_id)\n      .show();\n\n    // The div the comment is displayed in.\n    var div = $('div#cd' + d.comment_id);\n    var data = div.data('comment');\n\n    // If this is not an unvote, and the other vote arrow has\n    // already been pressed, unpress it.\n    if ((d.value !== 0) && (data.vote === d.value * -1)) {\n      $('#' + (d.value == 1 ? 'd' : 'u') + 'u' + d.comment_id).hide();\n      $('#' + (d.value == 1 ? 'd' : 'u') + 'v' + d.comment_id).show();\n    }\n\n    // Update the comments rating in the local data.\n    data.rating += (data.vote === 0) ? d.value : (d.value - data.vote);\n    data.vote = d.value;\n    div.data('comment', data);\n\n    // Change the rating text.\n    div.find('.rating:first')\n      .text(data.rating + ' point' + (data.rating == 1 ? '' : 's'));\n\n    // Send the vote information to the server.\n    $.ajax({\n      type: \"POST\",\n      url: opts.processVoteURL,\n      data: d,\n      error: function(request, textStatus, error) {\n        showError('Oops, there was a problem casting that vote.');\n      }\n    });\n  }\n\n  /**\n   * Open a reply form used to reply to an existing comment.\n   */\n  function openReply(id) {\n    // Swap out the reply link for the hide link\n    $('#rl' + id).hide();\n    $('#cr' + id).show();\n\n    // Add the reply li to the children ul.\n    var div = $(renderTemplate(replyTemplate, {id: id})).hide();\n    $('#cl' + id)\n      .prepend(div)\n      // Setup the submit handler for the reply form.\n      .find('#rf' + id)\n      .submit(function(event) {\n        event.preventDefault();\n        addComment($('#rf' + id));\n        closeReply(id);\n      })\n      .find('input[type=button]')\n      .click(function() {\n        closeReply(id);\n      });\n    div.slideDown('fast', function() {\n      $('#rf' + id).find('textarea').focus();\n    });\n  }\n\n  /**\n   * Close the reply form opened with openReply.\n   */\n  function closeReply(id) {\n    // Remove the reply div from the DOM.\n    $('#rd' + id).slideUp('fast', function() {\n      $(this).remove();\n    });\n\n    // Swap out the hide link for the reply link\n    $('#cr' + id).hide();\n    $('#rl' + id).show();\n  }\n\n  /**\n   * Recursively sort a tree of comments using the comp comparator.\n   */\n  function sortComments(comments) {\n    comments.sort(comp);\n    $.each(comments, function() {\n      this.children = sortComments(this.children);\n    });\n    return comments;\n  }\n\n  /**\n   * Get the children comments from a ul. If recursive is true,\n   * recursively include childrens' children.\n   */\n  function getChildren(ul, recursive) {\n    var children = [];\n    ul.children().children(\"[id^='cd']\")\n      .each(function() {\n        var comment = $(this).data('comment');\n        if (recursive)\n          comment.children = getChildren($(this).find('#cl' + comment.id), true);\n        children.push(comment);\n      });\n    return children;\n  }\n\n  /** Create a div to display a comment in. */\n  function createCommentDiv(comment) {\n    if (!comment.displayed && !opts.moderator) {\n      return $('<div class=\"moderate\">Thank you!  Your comment will show up '\n               + 'once it is has been approved by a moderator.</div>');\n    }\n    // Prettify the comment rating.\n    comment.pretty_rating = comment.rating + ' point' +\n      (comment.rating == 1 ? '' : 's');\n    // Make a class (for displaying not yet moderated comments differently)\n    comment.css_class = comment.displayed ? '' : ' moderate';\n    // Create a div for this comment.\n    var context = $.extend({}, opts, comment);\n    var div = $(renderTemplate(commentTemplate, context));\n\n    // If the user has voted on this comment, highlight the correct arrow.\n    if (comment.vote) {\n      var direction = (comment.vote == 1) ? 'u' : 'd';\n      div.find('#' + direction + 'v' + comment.id).hide();\n      div.find('#' + direction + 'u' + comment.id).show();\n    }\n\n    if (opts.moderator || comment.text != '[deleted]') {\n      div.find('a.reply').show();\n      if (comment.proposal_diff)\n        div.find('#sp' + comment.id).show();\n      if (opts.moderator && !comment.displayed)\n        div.find('#cm' + comment.id).show();\n      if (opts.moderator || (opts.username == comment.username))\n        div.find('#dc' + comment.id).show();\n    }\n    return div;\n  }\n\n  /**\n   * A simple template renderer. Placeholders such as <%id%> are replaced\n   * by context['id'] with items being escaped. Placeholders such as <#id#>\n   * are not escaped.\n   */\n  function renderTemplate(template, context) {\n    var esc = $(document.createElement('div'));\n\n    function handle(ph, escape) {\n      var cur = context;\n      $.each(ph.split('.'), function() {\n        cur = cur[this];\n      });\n      return escape ? esc.text(cur || \"\").html() : cur;\n    }\n\n    return template.replace(/<([%#])([\\w\\.]*)\\1>/g, function() {\n      return handle(arguments[2], arguments[1] == '%' ? true : false);\n    });\n  }\n\n  /** Flash an error message briefly. */\n  function showError(message) {\n    $(document.createElement('div')).attr({'class': 'popup-error'})\n      .append($(document.createElement('div'))\n               .attr({'class': 'error-message'}).text(message))\n      .appendTo('body')\n      .fadeIn(\"slow\")\n      .delay(2000)\n      .fadeOut(\"slow\");\n  }\n\n  /** Add a link the user uses to open the comments popup. */\n  $.fn.comment = function() {\n    return this.each(function() {\n      var id = $(this).attr('id').substring(1);\n      var count = COMMENT_METADATA[id];\n      var title = count + ' comment' + (count == 1 ? '' : 's');\n      var image = count > 0 ? opts.commentBrightImage : opts.commentImage;\n      var addcls = count == 0 ? ' nocomment' : '';\n      $(this)\n        .append(\n          $(document.createElement('a')).attr({\n            href: '#',\n            'class': 'sphinx-comment-open' + addcls,\n            id: 'ao' + id\n          })\n            .append($(document.createElement('img')).attr({\n              src: image,\n              alt: 'comment',\n              title: title\n            }))\n            .click(function(event) {\n              event.preventDefault();\n              show($(this).attr('id').substring(2));\n            })\n        )\n        .append(\n          $(document.createElement('a')).attr({\n            href: '#',\n            'class': 'sphinx-comment-close hidden',\n            id: 'ah' + id\n          })\n            .append($(document.createElement('img')).attr({\n              src: opts.closeCommentImage,\n              alt: 'close',\n              title: 'close'\n            }))\n            .click(function(event) {\n              event.preventDefault();\n              hide($(this).attr('id').substring(2));\n            })\n        );\n    });\n  };\n\n  var opts = {\n    processVoteURL: '/_process_vote',\n    addCommentURL: '/_add_comment',\n    getCommentsURL: '/_get_comments',\n    acceptCommentURL: '/_accept_comment',\n    deleteCommentURL: '/_delete_comment',\n    commentImage: '/static/_static/comment.png',\n    closeCommentImage: '/static/_static/comment-close.png',\n    loadingImage: '/static/_static/ajax-loader.gif',\n    commentBrightImage: '/static/_static/comment-bright.png',\n    upArrow: '/static/_static/up.png',\n    downArrow: '/static/_static/down.png',\n    upArrowPressed: '/static/_static/up-pressed.png',\n    downArrowPressed: '/static/_static/down-pressed.png',\n    voting: false,\n    moderator: false\n  };\n\n  if (typeof COMMENT_OPTIONS != \"undefined\") {\n    opts = jQuery.extend(opts, COMMENT_OPTIONS);\n  }\n\n  var popupTemplate = '\\\n    <div class=\"sphinx-comments\" id=\"sc<%id%>\">\\\n      <p class=\"sort-options\">\\\n        Sort by:\\\n        <a href=\"#\" class=\"sort-option byrating\">best rated</a>\\\n        <a href=\"#\" class=\"sort-option byascage\">newest</a>\\\n        <a href=\"#\" class=\"sort-option byage\">oldest</a>\\\n      </p>\\\n      <div class=\"comment-header\">Comments</div>\\\n      <div class=\"comment-loading\" id=\"cn<%id%>\">\\\n        loading comments... <img src=\"<%loadingImage%>\" alt=\"\" /></div>\\\n      <ul id=\"cl<%id%>\" class=\"comment-ul\"></ul>\\\n      <div id=\"ca<%id%>\">\\\n      <p class=\"add-a-comment\">Add a comment\\\n        (<a href=\"#\" class=\"comment-markup\" id=\"ab<%id%>\">markup</a>):</p>\\\n      <div class=\"comment-markup-box\" id=\"mb<%id%>\">\\\n        reStructured text markup: <i>*emph*</i>, <b>**strong**</b>, \\\n        <code>``code``</code>, \\\n        code blocks: <code>::</code> and an indented block after blank line</div>\\\n      <form method=\"post\" id=\"cf<%id%>\" class=\"comment-form\" action=\"\">\\\n        <textarea name=\"comment\" cols=\"80\"></textarea>\\\n        <p class=\"propose-button\">\\\n          <a href=\"#\" id=\"pc<%id%>\" class=\"show-propose-change\">\\\n            Propose a change &#9657;\\\n          </a>\\\n          <a href=\"#\" id=\"hc<%id%>\" class=\"hide-propose-change\">\\\n            Propose a change &#9663;\\\n          </a>\\\n        </p>\\\n        <textarea name=\"proposal\" id=\"pt<%id%>\" cols=\"80\"\\\n                  spellcheck=\"false\"></textarea>\\\n        <input type=\"submit\" value=\"Add comment\" />\\\n        <input type=\"hidden\" name=\"node\" value=\"<%id%>\" />\\\n        <input type=\"hidden\" name=\"parent\" value=\"\" />\\\n      </form>\\\n      </div>\\\n    </div>';\n\n  var commentTemplate = '\\\n    <div id=\"cd<%id%>\" class=\"sphinx-comment<%css_class%>\">\\\n      <div class=\"vote\">\\\n        <div class=\"arrow\">\\\n          <a href=\"#\" id=\"uv<%id%>\" class=\"vote\" title=\"vote up\">\\\n            <img src=\"<%upArrow%>\" />\\\n          </a>\\\n          <a href=\"#\" id=\"uu<%id%>\" class=\"un vote\" title=\"vote up\">\\\n            <img src=\"<%upArrowPressed%>\" />\\\n          </a>\\\n        </div>\\\n        <div class=\"arrow\">\\\n          <a href=\"#\" id=\"dv<%id%>\" class=\"vote\" title=\"vote down\">\\\n            <img src=\"<%downArrow%>\" id=\"da<%id%>\" />\\\n          </a>\\\n          <a href=\"#\" id=\"du<%id%>\" class=\"un vote\" title=\"vote down\">\\\n            <img src=\"<%downArrowPressed%>\" />\\\n          </a>\\\n        </div>\\\n      </div>\\\n      <div class=\"comment-content\">\\\n        <p class=\"tagline comment\">\\\n          <span class=\"user-id\"><%username%></span>\\\n          <span class=\"rating\"><%pretty_rating%></span>\\\n          <span class=\"delta\"><%time.delta%></span>\\\n        </p>\\\n        <div class=\"comment-text comment\"><#text#></div>\\\n        <p class=\"comment-opts comment\">\\\n          <a href=\"#\" class=\"reply hidden\" id=\"rl<%id%>\">reply &#9657;</a>\\\n          <a href=\"#\" class=\"close-reply\" id=\"cr<%id%>\">reply &#9663;</a>\\\n          <a href=\"#\" id=\"sp<%id%>\" class=\"show-proposal\">proposal &#9657;</a>\\\n          <a href=\"#\" id=\"hp<%id%>\" class=\"hide-proposal\">proposal &#9663;</a>\\\n          <a href=\"#\" id=\"dc<%id%>\" class=\"delete-comment hidden\">delete</a>\\\n          <span id=\"cm<%id%>\" class=\"moderation hidden\">\\\n            <a href=\"#\" id=\"ac<%id%>\" class=\"accept-comment\">accept</a>\\\n          </span>\\\n        </p>\\\n        <pre class=\"proposal\" id=\"pr<%id%>\">\\\n<#proposal_diff#>\\\n        </pre>\\\n          <ul class=\"comment-children\" id=\"cl<%id%>\"></ul>\\\n        </div>\\\n        <div class=\"clearleft\"></div>\\\n      </div>\\\n    </div>';\n\n  var replyTemplate = '\\\n    <li>\\\n      <div class=\"reply-div\" id=\"rd<%id%>\">\\\n        <form id=\"rf<%id%>\">\\\n          <textarea name=\"comment\" cols=\"80\"></textarea>\\\n          <input type=\"submit\" value=\"Add reply\" />\\\n          <input type=\"button\" value=\"Cancel\" />\\\n          <input type=\"hidden\" name=\"parent\" value=\"<%id%>\" />\\\n          <input type=\"hidden\" name=\"node\" value=\"\" />\\\n        </form>\\\n      </div>\\\n    </li>';\n\n  $(document).ready(function() {\n    init();\n  });\n})(jQuery);\n\n$(document).ready(function() {\n  // add comment anchors for all paragraphs that are commentable\n  $('.sphinx-has-comment').comment();\n\n  // highlight search words in search results\n  $(\"div.context\").each(function() {\n    var params = $.getQueryParameters();\n    var terms = (params.q) ? params.q[0].split(/\\s+/) : [];\n    var result = $(this);\n    $.each(terms, function() {\n      result.highlightText(this.toLowerCase(), 'highlighted');\n    });\n  });\n\n  // directly open comment window if requested\n  var anchor = document.location.hash;\n  if (anchor.substring(0, 9) == '#comment-') {\n    $('#ao' + anchor.substring(9)).click();\n    document.location.hash = '#s' + anchor.substring(9);\n  }\n});\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/about.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n  \n  <title>About &mdash; Slack Developer Kit for Python</title>\n\n  <!-- Google Tag Manager -->\n  <script>(function (w, d, s, l, i) {\n      w[l] = w[l] || []; w[l].push({\n        'gtm.start':\n          new Date().getTime(), event: 'gtm.js'\n      }); var f = d.getElementsByTagName(s)[0],\n        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =\n          'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);\n    })(window, document, 'script', 'dataLayer', 'GTM-KFZ5MK7');</script>\n  <!-- End Google Tag Manager -->\n  \n  <link href=\"https://a.slack-edge.com/4f227/style/rollup-slack_kit_legacy_adapters.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link href=\"https://a.slack-edge.com/3e02c0/style/rollup-api_site.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link rel=\"stylesheet\" href=\"./_static/default.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/pygments.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/docs.css\" type=\"text/css\" />\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"https://a.slack-edge.com/4f28/img/icons/favicon-32.png\" type=\"image/png\" />\n  <link rel=\"top\" title=\"Slack Developer Kit for Python 1.0.1 documentation\" href=\"index.html\" />\n\n\n\n</head>\n\n<body class=\"api light_theme\">\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-KFZ5MK7\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n  <header>\n    <a id=\"menu_toggle\" class=\"no_transition show_on_mobile\">\n      <span class=\"menu_icon\"></span>\n      <span class=\"vert_divider\"></span>\n    </a>\n    <a href=\"https://api.slack.com/\" id=\"header_logo\" class=\"api hide_on_mobile\" style=\"float:left; display: inline-block;\">\n      <img alt=\"Slack API\" src=\"https://a.slack-edge.com/3026cb/img/slack_api_logo_vogue.png\" style=\"width: 225px; padding-right: 25px; border-right: 1px solid #DDD;\" />\n    </a>\n    <span style=\"display: inline-block; padding-left: 20px; margin-top: 25px; font-weight: bold;\">\n      <a style=\"color: #555459;\" href=\"./index.html\">Slack Developer Kit for Python</a>\n    </span>\n    <div class=\"header_nav\">\n      <a href=\"https://github.com/SlackAPI/python-slackclient\" class=\"btn header_btn float_right\" data-qa=\"go_to_slack\">Go\n        to GitHub</a>\n    </div>\n  </header>\n  \n\n  <div id=\"page\">\n    <div id=\"page_contents\" class=\"clearfix\">\n      <!-- Sidebar Content -->\n      <nav id=\"api_nav\" class=\"col span_1_of_4\">\n        <div id=\"api_sections\">\n          \n          <ul class=\"current\">\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"index.html\">Slack Developer Kit for Python</a></li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"auth.html\">Tokens &amp; Authentication</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#handling-tokens-and-other-sensitive-data\">Handling tokens and other sensitive data</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#single-workspace-apps\">Single-Workspace Apps</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#the-oauth-flow\">The OAuth flow</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"basic_usage.html\">Basic Usage</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#sending-a-message\">Sending a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#customizing-a-message-s-layout\">Customizing a message’s layout</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#replying-to-messages-and-creating-threads\">Replying to messages and creating threads</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#updating-the-content-of-a-message\">Updating the content of a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#deleting-a-message\">Deleting a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#adding-or-removing-an-emoji-reaction\">Adding or removing an emoji reaction</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-list-of-channels\">Getting a list of channels</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-channel-s-info\">Getting a channel’s info</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#joining-a-channel\">Joining a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#leaving-a-channel\">Leaving a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#get-a-list-of-team-members\">Get a list of team members</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#uploading-files\">Uploading files</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#web-api-rate-limits\">Web API Rate Limits</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"conversations.html\">Conversations API</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-direct-message-or-multi-person-direct-message\">Creating a direct message or multi-person direct message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-public-or-private-channel\">Creating a public or private channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-information-about-a-conversation\">Getting information about a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-a-list-of-conversations\">Getting a list of conversations</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#leaving-a-conversation\">Leaving a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#get-conversation-members\">Get conversation members</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"real_time_messaging.html\">Real Time Messaging (RTM)</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#connecting-to-the-rtm-api\">Connecting to the RTM API</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-start-vs-rtm-connect\">rtm.start vs rtm.connect</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-events\">RTM Events</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#sending-messages-via-the-rtm-api\">Sending messages via the RTM API</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"faq.html\">Frequently Asked Questions</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#what-even-is-product-name-and-why-should-i-care\">What even is Slack Developer Kit for Python and why should I care?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#omg-i-found-a-bug\">OMG I found a bug!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#hey-there-s-a-feature-missing\">Hey, there’s a feature missing!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#i-d-like-to-contribute-but-how\">I’d like to contribute…but how?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#how-do-i-compile-the-documentation\">How do I compile the documentation?</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"changelog.html\">Changelog</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-3-0-2018-09-11\">v1.3.0 (2018-09-11)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-1-2018-03-26\">v1.2.1 (2018-03-26)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-0-2018-03-20\">v1.2.0 (2018-03-20)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-3-2018-03-01\">v1.1.3 (2018-03-01)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-2-2018-01-31\">v1.1.2 (2018-01-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-1-2018-01-30\">v1.1.1 (2018-01-30)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-0-2017-11-21\">v1.1.0 (2017-11-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-9-2017-08-31\">v1.0.9 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-8-2017-08-31\">v1.0.8 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-7-2017-08-02\">v1.0.7 (2017-08-02)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-6-2017-06-12\">v1.0.6 (2017-06-12)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-5-2017-01-23\">v1.0.5 (2017-01-23)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-4-2016-12-15\">v1.0.4 (2016-12-15)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-3-2016-12-13\">v1.0.3 (2016-12-13)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-2-2016-09-22\">v1.0.2 (2016-09-22)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-1-2016-03-25\">v1.0.1 (2016-03-25)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-0-2016-02-28\">v1.0.0 (2016-02-28)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-18-0-2016-02-21\">v0.18.0 (2016-02-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-17-0-2016-02-15\">v0.17.0 (2016-02-15)</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1 current\"><a class=\"current reference internal\" href=\"#\">About</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#product-name\">Slack Developer Kit for Python</a></li>\n</ul>\n</li>\n</ul>\n\n\n<div id=\"footer\">\n\n    <ul id=\"footer_nav\">\n        <li><a href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n        <li><a href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n        <li><a href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n        <li><a href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n    </ul>\n\n    <p id=\"footer_signature\">Made with <i class=\"ts_icon ts_icon_heart\"></i> by Slack<br/>and our Lovely\n        Community\n    </p>\n</div>\n          \n        </div>\n      </nav>\n      <!-- /Sidebar Content -->\n\n      <!-- Body Content -->\n      <div class=\"col span_3_of_4\">\n        <div class=\"section-title\">About</div>\n        <div class=\"card\">\n  <div class=\"section\" id=\"about\">\n<h1>About<a class=\"headerlink\" href=\"#about\" title=\"Permalink to this headline\">¶</a></h1>\n<div class=\"section\" id=\"product-name\">\n<h2>Slack Developer Kit for Python<a class=\"headerlink\" href=\"#product-name\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Access the Slack Platform from your Python app. Slack Developer Kit for Python lets you build on the Slack Web APIs pythonically.</p>\n<p>Slack Developer Kit for Python is proudly maintained with 💖 by the Slack Developer Tools team</p>\n<ul class=\"simple\">\n<li><a class=\"reference external\" href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n<li><a class=\"reference external\" href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n<li><a class=\"reference external\" href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n<li><a class=\"reference external\" href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n</ul>\n</div>\n</div>\n\n\n          <div class=\"clear_both large_bottom_margin\"></div>\n        </div>\n      </div>\n      <!-- /Body Content -->\n\n    </div>\n  </div>\n\n  <footer>\n    <p class=\"light tiny align_center\">© 2019 Slack Technologies, Inc. and contributors</p>\n  </footer>\n\n  <script>\n    window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;\n    ga('create', 'UA-56978219-13', 'auto');\n    ga('send', 'pageview');\n  </script>\n  <script async src='https://www.google-analytics.com/analytics.js'></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/auth.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n  \n  <title>Tokens &amp; Authentication &mdash; Slack Developer Kit for Python</title>\n\n  <!-- Google Tag Manager -->\n  <script>(function (w, d, s, l, i) {\n      w[l] = w[l] || []; w[l].push({\n        'gtm.start':\n          new Date().getTime(), event: 'gtm.js'\n      }); var f = d.getElementsByTagName(s)[0],\n        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =\n          'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);\n    })(window, document, 'script', 'dataLayer', 'GTM-KFZ5MK7');</script>\n  <!-- End Google Tag Manager -->\n  \n  <link href=\"https://a.slack-edge.com/4f227/style/rollup-slack_kit_legacy_adapters.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link href=\"https://a.slack-edge.com/3e02c0/style/rollup-api_site.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link rel=\"stylesheet\" href=\"./_static/default.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/pygments.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/docs.css\" type=\"text/css\" />\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"https://a.slack-edge.com/4f28/img/icons/favicon-32.png\" type=\"image/png\" />\n  <link rel=\"top\" title=\"Slack Developer Kit for Python 1.0.1 documentation\" href=\"index.html\" />\n\n\n\n</head>\n\n<body class=\"api light_theme\">\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-KFZ5MK7\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n  <header>\n    <a id=\"menu_toggle\" class=\"no_transition show_on_mobile\">\n      <span class=\"menu_icon\"></span>\n      <span class=\"vert_divider\"></span>\n    </a>\n    <a href=\"https://api.slack.com/\" id=\"header_logo\" class=\"api hide_on_mobile\" style=\"float:left; display: inline-block;\">\n      <img alt=\"Slack API\" src=\"https://a.slack-edge.com/3026cb/img/slack_api_logo_vogue.png\" style=\"width: 225px; padding-right: 25px; border-right: 1px solid #DDD;\" />\n    </a>\n    <span style=\"display: inline-block; padding-left: 20px; margin-top: 25px; font-weight: bold;\">\n      <a style=\"color: #555459;\" href=\"./index.html\">Slack Developer Kit for Python</a>\n    </span>\n    <div class=\"header_nav\">\n      <a href=\"https://github.com/SlackAPI/python-slackclient\" class=\"btn header_btn float_right\" data-qa=\"go_to_slack\">Go\n        to GitHub</a>\n    </div>\n  </header>\n  \n\n  <div id=\"page\">\n    <div id=\"page_contents\" class=\"clearfix\">\n      <!-- Sidebar Content -->\n      <nav id=\"api_nav\" class=\"col span_1_of_4\">\n        <div id=\"api_sections\">\n          \n          <ul class=\"current\">\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"index.html\">Slack Developer Kit for Python</a></li>\n<li class=\"toctree-l1 current\"><a class=\"current reference internal\" href=\"#\">Tokens &amp; Authentication</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#handling-tokens-and-other-sensitive-data\">Handling tokens and other sensitive data</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#single-workspace-apps\">Single-Workspace Apps</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#the-oauth-flow\">The OAuth flow</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"basic_usage.html\">Basic Usage</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#sending-a-message\">Sending a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#customizing-a-message-s-layout\">Customizing a message’s layout</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#replying-to-messages-and-creating-threads\">Replying to messages and creating threads</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#updating-the-content-of-a-message\">Updating the content of a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#deleting-a-message\">Deleting a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#adding-or-removing-an-emoji-reaction\">Adding or removing an emoji reaction</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-list-of-channels\">Getting a list of channels</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-channel-s-info\">Getting a channel’s info</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#joining-a-channel\">Joining a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#leaving-a-channel\">Leaving a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#get-a-list-of-team-members\">Get a list of team members</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#uploading-files\">Uploading files</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#web-api-rate-limits\">Web API Rate Limits</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"conversations.html\">Conversations API</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-direct-message-or-multi-person-direct-message\">Creating a direct message or multi-person direct message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-public-or-private-channel\">Creating a public or private channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-information-about-a-conversation\">Getting information about a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-a-list-of-conversations\">Getting a list of conversations</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#leaving-a-conversation\">Leaving a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#get-conversation-members\">Get conversation members</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"real_time_messaging.html\">Real Time Messaging (RTM)</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#connecting-to-the-rtm-api\">Connecting to the RTM API</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-start-vs-rtm-connect\">rtm.start vs rtm.connect</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-events\">RTM Events</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#sending-messages-via-the-rtm-api\">Sending messages via the RTM API</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"faq.html\">Frequently Asked Questions</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#what-even-is-product-name-and-why-should-i-care\">What even is Slack Developer Kit for Python and why should I care?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#omg-i-found-a-bug\">OMG I found a bug!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#hey-there-s-a-feature-missing\">Hey, there’s a feature missing!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#i-d-like-to-contribute-but-how\">I’d like to contribute…but how?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#how-do-i-compile-the-documentation\">How do I compile the documentation?</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"changelog.html\">Changelog</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-3-0-2018-09-11\">v1.3.0 (2018-09-11)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-1-2018-03-26\">v1.2.1 (2018-03-26)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-0-2018-03-20\">v1.2.0 (2018-03-20)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-3-2018-03-01\">v1.1.3 (2018-03-01)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-2-2018-01-31\">v1.1.2 (2018-01-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-1-2018-01-30\">v1.1.1 (2018-01-30)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-0-2017-11-21\">v1.1.0 (2017-11-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-9-2017-08-31\">v1.0.9 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-8-2017-08-31\">v1.0.8 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-7-2017-08-02\">v1.0.7 (2017-08-02)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-6-2017-06-12\">v1.0.6 (2017-06-12)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-5-2017-01-23\">v1.0.5 (2017-01-23)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-4-2016-12-15\">v1.0.4 (2016-12-15)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-3-2016-12-13\">v1.0.3 (2016-12-13)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-2-2016-09-22\">v1.0.2 (2016-09-22)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-1-2016-03-25\">v1.0.1 (2016-03-25)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-0-2016-02-28\">v1.0.0 (2016-02-28)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-18-0-2016-02-21\">v0.18.0 (2016-02-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-17-0-2016-02-15\">v0.17.0 (2016-02-15)</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"about.html\">About</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"about.html#product-name\">Slack Developer Kit for Python</a></li>\n</ul>\n</li>\n</ul>\n\n\n<div id=\"footer\">\n\n    <ul id=\"footer_nav\">\n        <li><a href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n        <li><a href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n        <li><a href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n        <li><a href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n    </ul>\n\n    <p id=\"footer_signature\">Made with <i class=\"ts_icon ts_icon_heart\"></i> by Slack<br/>and our Lovely\n        Community\n    </p>\n</div>\n          \n        </div>\n      </nav>\n      <!-- /Sidebar Content -->\n\n      <!-- Body Content -->\n      <div class=\"col span_3_of_4\">\n        <div class=\"section-title\">Tokens &amp; Authentication</div>\n        <div class=\"card\">\n  <div class=\"section\" id=\"tokens-authentication\">\n<h1>Tokens &amp; Authentication<a class=\"headerlink\" href=\"#tokens-authentication\" title=\"Permalink to this headline\">¶</a></h1>\n<div class=\"section\" id=\"handling-tokens-and-other-sensitive-data\">\n<span id=\"handling-tokens\"></span><h2>Handling tokens and other sensitive data<a class=\"headerlink\" href=\"#handling-tokens-and-other-sensitive-data\" title=\"Permalink to this headline\">¶</a></h2>\n<p>⚠️ <strong>Slack tokens are the keys to your—or your customers’—data.Keep them secret. Keep them safe.</strong></p>\n<p>One way to do that is to never explicitly hardcode them.</p>\n<p>Try to avoid this when possible:</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"n\">token</span> <span class=\"o\">=</span> <span class=\"s1\">&#39;xoxb-abc-1232&#39;</span>\n</pre></div>\n</div>\n<p>⚠️ <strong>Never share test tokens with other users or applications. Do not publish test tokens in public code repositories.</strong></p>\n<p>We recommend you pass tokens in as environment variables, or persist them in a database that is accessed at runtime. You can add a token to the environment by starting your app as:</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"n\">SLACK_BOT_TOKEN</span><span class=\"o\">=</span><span class=\"s2\">&quot;xoxb-abc-1232&quot;</span> <span class=\"n\">python</span> <span class=\"n\">myapp</span><span class=\"o\">.</span><span class=\"n\">py</span>\n</pre></div>\n</div>\n<p>Then retrieve the key with:</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">import</span> <span class=\"nn\">os</span>\n<span class=\"n\">SLACK_BOT_TOKEN</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_BOT_TOKEN&quot;</span><span class=\"p\">]</span>\n</pre></div>\n</div>\n<p>You can use the same technique for other kinds of sensitive data that ne’er-do-wells could use in nefarious ways, including</p>\n<ul class=\"simple\">\n<li>Incoming webhook URLs</li>\n<li>Slash command verification tokens</li>\n<li>App client ids and client secrets</li>\n</ul>\n<p>For additional information, please see our <a class=\"reference external\" href=\"https://api.slack.com/docs/oauth-safety\">Safely Storing Credentials</a> page.</p>\n</div>\n<div class=\"section\" id=\"single-workspace-apps\">\n<h2>Single-Workspace Apps<a class=\"headerlink\" href=\"#single-workspace-apps\" title=\"Permalink to this headline\">¶</a></h2>\n<p>If you’re building an application for a single Slack workspace, there’s no need to build out the entire OAuth flow.</p>\n<p>Once you’ve setup your features, click on the <strong>Install App to Team</strong> button found on the <strong>Install App</strong> page.\nIf you add new permission scopes or Slack app features after an app has been installed, you must reinstall the app to\nyour workspace for changes to take effect.</p>\n<p>For additional information, see the <a class=\"reference external\" href=\"https://api.slack.com/slack-apps#installing_apps\">Installing Apps</a> of our <a class=\"reference external\" href=\"https://api.slack.com/slack-apps#installing_apps\">Building Slack apps</a> page.</p>\n</div>\n<div class=\"section\" id=\"the-oauth-flow\">\n<h2>The OAuth flow<a class=\"headerlink\" href=\"#the-oauth-flow\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Authentication for Slack’s APIs is done using OAuth, so you’ll want to read up on <a class=\"reference external\" href=\"https://api.slack.com/docs/oauth\">OAuth</a>.</p>\n<p>In order to implement OAuth in your app, you will need to include a web server. In this example, we’ll use <a class=\"reference external\" href=\"http://flask.pocoo.org/\">Flask</a>.</p>\n<p>As mentioned above, we’re setting the app tokens and other configs in environment variables and pulling them into global variables.</p>\n<p>Depending on what actions your app will need to perform, you’ll need different OAuth permission scopes. Review the available scopes <a class=\"reference external\" href=\"https://api.slack.com/docs/oauth-scopes\">here</a>.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">import</span> <span class=\"nn\">os</span>\n<span class=\"kn\">from</span> <span class=\"nn\">flask</span> <span class=\"kn\">import</span> <span class=\"n\">Flask</span><span class=\"p\">,</span> <span class=\"n\">request</span>\n<span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">client_id</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_CLIENT_ID&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">client_secret</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_CLIENT_SECRET&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">oauth_scope</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_BOT_SCOPE&quot;</span><span class=\"p\">]</span>\n\n<span class=\"n\">app</span> <span class=\"o\">=</span> <span class=\"n\">Flask</span><span class=\"p\">(</span><span class=\"vm\">__name__</span><span class=\"p\">)</span>\n</pre></div>\n</div>\n<p><strong>The OAuth initiation link:</strong></p>\n<p>To begin the OAuth flow, you’ll need to provide the user with a link to Slack’s OAuth page.\nThis directs the user to Slack’s OAuth acceptance page, where the user will review and accept\nor refuse the permissions your app is requesting as defined by the requested scope(s).</p>\n<p>For the best user experience, use the <a class=\"reference external\" href=\"https://api.slack.com/docs/slack-button\">Add to Slack button</a> to direct users to approve your application for access or <a class=\"reference external\" href=\"https://api.slack.com/docs/sign-in-with-slack\">Sign in with Slack</a> to log users in.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"nd\">@app.route</span><span class=\"p\">(</span><span class=\"s2\">&quot;/begin_auth&quot;</span><span class=\"p\">,</span> <span class=\"n\">methods</span><span class=\"o\">=</span><span class=\"p\">[</span><span class=\"s2\">&quot;GET&quot;</span><span class=\"p\">])</span>\n<span class=\"k\">def</span> <span class=\"nf\">pre_install</span><span class=\"p\">():</span>\n  <span class=\"k\">return</span> <span class=\"s1\">&#39;&#39;&#39;</span>\n<span class=\"s1\">      &lt;a href=&quot;https://slack.com/oauth/authorize?scope={0}&amp;client_id={1}&quot;&gt;</span>\n<span class=\"s1\">          Add to Slack</span>\n<span class=\"s1\">      &lt;/a&gt;</span>\n<span class=\"s1\">  &#39;&#39;&#39;</span><span class=\"o\">.</span><span class=\"n\">format</span><span class=\"p\">(</span><span class=\"n\">oauth_scope</span><span class=\"p\">,</span> <span class=\"n\">client_id</span><span class=\"p\">)</span>\n</pre></div>\n</div>\n<p><strong>The OAuth completion page</strong></p>\n<p>Once the user has agreed to the permissions you’ve requested on Slack’s OAuth\npage, Slack will redirect the user to your auth completion page. Included in this\nredirect is a <code class=\"docutils literal notranslate\"><span class=\"pre\">code</span></code> query string param which you’ll use to request access\ntokens from the <code class=\"docutils literal notranslate\"><span class=\"pre\">oauth.access</span></code> endpoint.</p>\n<p>Generally, Web API requests require a valid OAuth token, but there are a few endpoints\nwhich do not require a token. <code class=\"docutils literal notranslate\"><span class=\"pre\">oauth.access</span></code> is one example of this. Since this\nis the endpoint you’ll use to retrieve the tokens for later API requests,\nan empty string <code class=\"docutils literal notranslate\"><span class=\"pre\">&quot;&quot;</span></code> is acceptable for this request.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"nd\">@app.route</span><span class=\"p\">(</span><span class=\"s2\">&quot;/finish_auth&quot;</span><span class=\"p\">,</span> <span class=\"n\">methods</span><span class=\"o\">=</span><span class=\"p\">[</span><span class=\"s2\">&quot;GET&quot;</span><span class=\"p\">,</span> <span class=\"s2\">&quot;POST&quot;</span><span class=\"p\">])</span>\n<span class=\"k\">def</span> <span class=\"nf\">post_install</span><span class=\"p\">():</span>\n\n  <span class=\"c1\"># Retrieve the auth code from the request params</span>\n  <span class=\"n\">auth_code</span> <span class=\"o\">=</span> <span class=\"n\">request</span><span class=\"o\">.</span><span class=\"n\">args</span><span class=\"p\">[</span><span class=\"s1\">&#39;code&#39;</span><span class=\"p\">]</span>\n\n  <span class=\"c1\"># An empty string is a valid token for this request</span>\n  <span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"s2\">&quot;&quot;</span><span class=\"p\">)</span>\n\n  <span class=\"c1\"># Request the auth tokens from Slack</span>\n  <span class=\"n\">auth_response</span> <span class=\"o\">=</span> <span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n    <span class=\"s2\">&quot;oauth.access&quot;</span><span class=\"p\">,</span>\n    <span class=\"n\">client_id</span><span class=\"o\">=</span><span class=\"n\">client_id</span><span class=\"p\">,</span>\n    <span class=\"n\">client_secret</span><span class=\"o\">=</span><span class=\"n\">client_secret</span><span class=\"p\">,</span>\n    <span class=\"n\">code</span><span class=\"o\">=</span><span class=\"n\">auth_code</span>\n  <span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>A successful request to <code class=\"docutils literal notranslate\"><span class=\"pre\">oauth.access</span></code> will yield two tokens: A <code class=\"docutils literal notranslate\"><span class=\"pre\">user</span></code>\ntoken and a <code class=\"docutils literal notranslate\"><span class=\"pre\">bot</span></code> token. The <code class=\"docutils literal notranslate\"><span class=\"pre\">user</span></code> token <code class=\"docutils literal notranslate\"><span class=\"pre\">auth_response['access_token']</span></code>\nis used to make requests on behalf of the authorizing user and the <code class=\"docutils literal notranslate\"><span class=\"pre\">bot</span></code>\ntoken <code class=\"docutils literal notranslate\"><span class=\"pre\">auth_response['bot']['bot_access_token']</span></code> is for making requests\non behalf of your app’s bot user.</p>\n<p>If your Slack app includes a bot user, upon approval the JSON response will contain\nan additional node containing an access token to be specifically used for your bot\nuser, within the context of the approving team.</p>\n<p>When you use Web API methods on behalf of your bot user, you should use this bot\nuser access token instead of the top-level access token granted to your application.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"c1\"># Save the bot token to an environmental variable or to your data store</span>\n<span class=\"c1\"># for later use</span>\n<span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_USER_TOKEN&quot;</span><span class=\"p\">]</span> <span class=\"o\">=</span> <span class=\"n\">auth_response</span><span class=\"p\">[</span><span class=\"s1\">&#39;access_token&#39;</span><span class=\"p\">]</span>\n<span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_BOT_TOKEN&quot;</span><span class=\"p\">]</span> <span class=\"o\">=</span> <span class=\"n\">auth_response</span><span class=\"p\">[</span><span class=\"s1\">&#39;bot&#39;</span><span class=\"p\">][</span><span class=\"s1\">&#39;bot_access_token&#39;</span><span class=\"p\">]</span>\n\n<span class=\"c1\"># Don&#39;t forget to let the user know that auth has succeeded!</span>\n<span class=\"k\">return</span> <span class=\"s2\">&quot;Auth complete!&quot;</span>\n</pre></div>\n</div>\n<p>Once your user has completed the OAuth flow, you’ll be able to use the provided\ntokens to make a variety of Web API calls on behalf of the user and your app’s bot user.</p>\n<p>See the <a class=\"reference internal\" href=\"basic_usage.html#web-api-examples\"><span class=\"std std-ref\">Web API usage</span></a> section of this documentation for usage examples.</p>\n</div>\n</div>\n\n\n          <div class=\"clear_both large_bottom_margin\"></div>\n        </div>\n      </div>\n      <!-- /Body Content -->\n\n    </div>\n  </div>\n\n  <footer>\n    <p class=\"light tiny align_center\">© 2019 Slack Technologies, Inc. and contributors</p>\n  </footer>\n\n  <script>\n    window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;\n    ga('create', 'UA-56978219-13', 'auto');\n    ga('send', 'pageview');\n  </script>\n  <script async src='https://www.google-analytics.com/analytics.js'></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/basic_usage.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n  \n  <title>Basic Usage &mdash; Slack Developer Kit for Python</title>\n\n  <!-- Google Tag Manager -->\n  <script>(function (w, d, s, l, i) {\n      w[l] = w[l] || []; w[l].push({\n        'gtm.start':\n          new Date().getTime(), event: 'gtm.js'\n      }); var f = d.getElementsByTagName(s)[0],\n        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =\n          'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);\n    })(window, document, 'script', 'dataLayer', 'GTM-KFZ5MK7');</script>\n  <!-- End Google Tag Manager -->\n  \n  <link href=\"https://a.slack-edge.com/4f227/style/rollup-slack_kit_legacy_adapters.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link href=\"https://a.slack-edge.com/3e02c0/style/rollup-api_site.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link rel=\"stylesheet\" href=\"./_static/default.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/pygments.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/docs.css\" type=\"text/css\" />\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"https://a.slack-edge.com/4f28/img/icons/favicon-32.png\" type=\"image/png\" />\n  <link rel=\"top\" title=\"Slack Developer Kit for Python 1.0.1 documentation\" href=\"index.html\" />\n\n\n\n</head>\n\n<body class=\"api light_theme\">\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-KFZ5MK7\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n  <header>\n    <a id=\"menu_toggle\" class=\"no_transition show_on_mobile\">\n      <span class=\"menu_icon\"></span>\n      <span class=\"vert_divider\"></span>\n    </a>\n    <a href=\"https://api.slack.com/\" id=\"header_logo\" class=\"api hide_on_mobile\" style=\"float:left; display: inline-block;\">\n      <img alt=\"Slack API\" src=\"https://a.slack-edge.com/3026cb/img/slack_api_logo_vogue.png\" style=\"width: 225px; padding-right: 25px; border-right: 1px solid #DDD;\" />\n    </a>\n    <span style=\"display: inline-block; padding-left: 20px; margin-top: 25px; font-weight: bold;\">\n      <a style=\"color: #555459;\" href=\"./index.html\">Slack Developer Kit for Python</a>\n    </span>\n    <div class=\"header_nav\">\n      <a href=\"https://github.com/SlackAPI/python-slackclient\" class=\"btn header_btn float_right\" data-qa=\"go_to_slack\">Go\n        to GitHub</a>\n    </div>\n  </header>\n  \n\n  <div id=\"page\">\n    <div id=\"page_contents\" class=\"clearfix\">\n      <!-- Sidebar Content -->\n      <nav id=\"api_nav\" class=\"col span_1_of_4\">\n        <div id=\"api_sections\">\n          \n          <ul class=\"current\">\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"index.html\">Slack Developer Kit for Python</a></li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"auth.html\">Tokens &amp; Authentication</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#handling-tokens-and-other-sensitive-data\">Handling tokens and other sensitive data</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#single-workspace-apps\">Single-Workspace Apps</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#the-oauth-flow\">The OAuth flow</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1 current\"><a class=\"current reference internal\" href=\"#\">Basic Usage</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#sending-a-message\">Sending a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#customizing-a-message-s-layout\">Customizing a message’s layout</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#replying-to-messages-and-creating-threads\">Replying to messages and creating threads</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#updating-the-content-of-a-message\">Updating the content of a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#deleting-a-message\">Deleting a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#adding-or-removing-an-emoji-reaction\">Adding or removing an emoji reaction</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#getting-a-list-of-channels\">Getting a list of channels</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#getting-a-channel-s-info\">Getting a channel’s info</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#joining-a-channel\">Joining a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#leaving-a-channel\">Leaving a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#get-a-list-of-team-members\">Get a list of team members</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#uploading-files\">Uploading files</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#web-api-rate-limits\">Web API Rate Limits</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"conversations.html\">Conversations API</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-direct-message-or-multi-person-direct-message\">Creating a direct message or multi-person direct message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-public-or-private-channel\">Creating a public or private channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-information-about-a-conversation\">Getting information about a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-a-list-of-conversations\">Getting a list of conversations</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#leaving-a-conversation\">Leaving a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#get-conversation-members\">Get conversation members</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"real_time_messaging.html\">Real Time Messaging (RTM)</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#connecting-to-the-rtm-api\">Connecting to the RTM API</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-start-vs-rtm-connect\">rtm.start vs rtm.connect</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-events\">RTM Events</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#sending-messages-via-the-rtm-api\">Sending messages via the RTM API</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"faq.html\">Frequently Asked Questions</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#what-even-is-product-name-and-why-should-i-care\">What even is Slack Developer Kit for Python and why should I care?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#omg-i-found-a-bug\">OMG I found a bug!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#hey-there-s-a-feature-missing\">Hey, there’s a feature missing!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#i-d-like-to-contribute-but-how\">I’d like to contribute…but how?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#how-do-i-compile-the-documentation\">How do I compile the documentation?</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"changelog.html\">Changelog</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-3-0-2018-09-11\">v1.3.0 (2018-09-11)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-1-2018-03-26\">v1.2.1 (2018-03-26)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-0-2018-03-20\">v1.2.0 (2018-03-20)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-3-2018-03-01\">v1.1.3 (2018-03-01)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-2-2018-01-31\">v1.1.2 (2018-01-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-1-2018-01-30\">v1.1.1 (2018-01-30)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-0-2017-11-21\">v1.1.0 (2017-11-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-9-2017-08-31\">v1.0.9 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-8-2017-08-31\">v1.0.8 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-7-2017-08-02\">v1.0.7 (2017-08-02)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-6-2017-06-12\">v1.0.6 (2017-06-12)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-5-2017-01-23\">v1.0.5 (2017-01-23)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-4-2016-12-15\">v1.0.4 (2016-12-15)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-3-2016-12-13\">v1.0.3 (2016-12-13)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-2-2016-09-22\">v1.0.2 (2016-09-22)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-1-2016-03-25\">v1.0.1 (2016-03-25)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-0-2016-02-28\">v1.0.0 (2016-02-28)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-18-0-2016-02-21\">v0.18.0 (2016-02-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-17-0-2016-02-15\">v0.17.0 (2016-02-15)</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"about.html\">About</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"about.html#product-name\">Slack Developer Kit for Python</a></li>\n</ul>\n</li>\n</ul>\n\n\n<div id=\"footer\">\n\n    <ul id=\"footer_nav\">\n        <li><a href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n        <li><a href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n        <li><a href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n        <li><a href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n    </ul>\n\n    <p id=\"footer_signature\">Made with <i class=\"ts_icon ts_icon_heart\"></i> by Slack<br/>and our Lovely\n        Community\n    </p>\n</div>\n          \n        </div>\n      </nav>\n      <!-- /Sidebar Content -->\n\n      <!-- Body Content -->\n      <div class=\"col span_3_of_4\">\n        <div class=\"section-title\">Basic Usage</div>\n        <div class=\"card\">\n  <div class=\"section\" id=\"basic-usage\">\n<span id=\"web-api-examples\"></span><h1>Basic Usage<a class=\"headerlink\" href=\"#basic-usage\" title=\"Permalink to this headline\">¶</a></h1>\n<p>The Slack Web API allows you to build applications that interact with Slack in more complex ways than the integrations\nwe provide out of the box.</p>\n<p>This package is a modular wrapper designed to make Slack <a class=\"reference external\" href=\"https://api.slack.com/web\">Web API</a> calls simpler and easier for your\napp. Provided below are examples of how to interact with commonly used API endpoints, but this is by no means\na complete list. Review the full list of available methods <a class=\"reference external\" href=\"https://api.slack.com/methods\">here</a>.</p>\n<p>See <a class=\"reference internal\" href=\"auth.html#handling-tokens\"><span class=\"std std-ref\">Tokens &amp; Authentication</span></a> for API token handling best practices.</p>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"sending-a-message\">\n<h2>Sending a message<a class=\"headerlink\" href=\"#sending-a-message\" title=\"Permalink to this headline\">¶</a></h2>\n<p>The primary use of Slack is sending messages. Whether you’re sending a message\nto a user or to a channel, this method handles both.</p>\n<p>To send a message to a channel, use the channel’s ID. For IMs, use the user’s ID.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;chat.postMessage&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXX&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">text</span><span class=\"o\">=</span><span class=\"s2\">&quot;Hello from Python! :tada:&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>There are some unique options specific to sending IMs, so be sure to read the <strong>channels</strong>\nsection of the <a class=\"reference external\" href=\"https://api.slack.com/methods/chat.postMessage#channels\">chat.postMessage</a>\npage for a full list of formatting and authorship options.</p>\n<p>Sending an ephemeral message, which is only visible to an assigned user in a specified channel, is nearly the same\nas sending a regular message, but with an additional <code class=\"docutils literal notranslate\"><span class=\"pre\">user</span></code> parameter.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;chat.postEphemeral&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXX&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">text</span><span class=\"o\">=</span><span class=\"s2\">&quot;Hello from Python! :tada:&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">user</span><span class=\"o\">=</span><span class=\"s2\">&quot;U0XXXXXXX&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/chat.postEphemeral\">chat.postEphemeral</a> for more info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"customizing-a-message-s-layout\">\n<h2>Customizing a message’s layout<a class=\"headerlink\" href=\"#customizing-a-message-s-layout\" title=\"Permalink to this headline\">¶</a></h2>\n<p>The chat.postMessage method takes an optional blocks argument that allows you to customize the layout of a message.\nBlocks for Web API methods are all specified in a single object literal, so just add additional keys for any optional argument.</p>\n<p>To send a message to a channel, use the channel’s ID. For IMs, use the user’s ID.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;chat.postMessage&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXX&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">blocks</span><span class=\"o\">=</span><span class=\"p\">[</span>\n    <span class=\"p\">{</span>\n        <span class=\"s2\">&quot;type&quot;</span><span class=\"p\">:</span> <span class=\"s2\">&quot;section&quot;</span><span class=\"p\">,</span>\n        <span class=\"s2\">&quot;text&quot;</span><span class=\"p\">:</span> <span class=\"p\">{</span>\n            <span class=\"s2\">&quot;type&quot;</span><span class=\"p\">:</span> <span class=\"s2\">&quot;mrkdwn&quot;</span><span class=\"p\">,</span>\n            <span class=\"s2\">&quot;text&quot;</span><span class=\"p\">:</span> <span class=\"s2\">&quot;Danny Torrence left the following review for your property:&quot;</span>\n        <span class=\"p\">}</span>\n    <span class=\"p\">},</span>\n    <span class=\"p\">{</span>\n        <span class=\"s2\">&quot;type&quot;</span><span class=\"p\">:</span> <span class=\"s2\">&quot;section&quot;</span><span class=\"p\">,</span>\n        <span class=\"s2\">&quot;text&quot;</span><span class=\"p\">:</span> <span class=\"p\">{</span>\n            <span class=\"s2\">&quot;type&quot;</span><span class=\"p\">:</span> <span class=\"s2\">&quot;mrkdwn&quot;</span><span class=\"p\">,</span>\n            <span class=\"s2\">&quot;text&quot;</span><span class=\"p\">:</span> <span class=\"s2\">&quot;&lt;https://example.com|Overlook Hotel&gt; </span><span class=\"se\">\\n</span><span class=\"s2\"> :star: </span><span class=\"se\">\\n</span><span class=\"s2\"> Doors had too many axe holes, guest in room &quot;</span> <span class=\"o\">+</span>\n            <span class=\"s2\">&quot;237 was far too rowdy, whole place felt stuck in the 1920s.&quot;</span>\n        <span class=\"p\">},</span>\n        <span class=\"s2\">&quot;accessory&quot;</span><span class=\"p\">:</span> <span class=\"p\">{</span>\n            <span class=\"s2\">&quot;type&quot;</span><span class=\"p\">:</span> <span class=\"s2\">&quot;image&quot;</span><span class=\"p\">,</span>\n            <span class=\"s2\">&quot;image_url&quot;</span><span class=\"p\">:</span> <span class=\"s2\">&quot;https://images.pexels.com/photos/750319/pexels-photo-750319.jpeg&quot;</span><span class=\"p\">,</span>\n            <span class=\"s2\">&quot;alt_text&quot;</span><span class=\"p\">:</span> <span class=\"s2\">&quot;Haunted hotel image&quot;</span>\n        <span class=\"p\">}</span>\n    <span class=\"p\">},</span>\n    <span class=\"p\">{</span>\n        <span class=\"s2\">&quot;type&quot;</span><span class=\"p\">:</span> <span class=\"s2\">&quot;section&quot;</span><span class=\"p\">,</span>\n        <span class=\"s2\">&quot;fields&quot;</span><span class=\"p\">:</span> <span class=\"p\">[</span>\n            <span class=\"p\">{</span>\n                <span class=\"s2\">&quot;type&quot;</span><span class=\"p\">:</span> <span class=\"s2\">&quot;mrkdwn&quot;</span><span class=\"p\">,</span>\n                <span class=\"s2\">&quot;text&quot;</span><span class=\"p\">:</span> <span class=\"s2\">&quot;*Average Rating*</span><span class=\"se\">\\n</span><span class=\"s2\">1.0&quot;</span>\n            <span class=\"p\">}</span>\n        <span class=\"p\">]</span>\n    <span class=\"p\">}</span>\n  <span class=\"p\">]</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p><strong>Note:</strong> You can use the <a href=\"#id1\"><span class=\"problematic\" id=\"id2\">`</span></a>Block Kit Builder &lt;<a class=\"reference external\" href=\"https://api.slack.com/tools/block-kit-builder\">https://api.slack.com/tools/block-kit-builder</a>&gt;`for a playground where you can prototype your message’s look and feel.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"replying-to-messages-and-creating-threads\">\n<h2>Replying to messages and creating threads<a class=\"headerlink\" href=\"#replying-to-messages-and-creating-threads\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Threaded messages are just like regular messages, except thread replies are grouped together to provide greater context\nto the user. You can reply to a thread or start a new threaded conversation by simply passing the original message’s <code class=\"docutils literal notranslate\"><span class=\"pre\">ts</span></code>\nID in the <code class=\"docutils literal notranslate\"><span class=\"pre\">thread_ts</span></code> attribute when posting a message. If you’re replying to a threaded message, you’ll pass the <cite>thread_ts</cite>\nID of the message you’re replying to.</p>\n<p>A channel or DM conversation is a nearly linear timeline of messages exchanged between people, bots, and apps.\nWhen one of these messages is replied to, it becomes the parent of a thread. By default, threaded replies do not\nappear directly in the channel, instead relegated to a kind of forked timeline descending from the parent message.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;chat.postMessage&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXX&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">text</span><span class=\"o\">=</span><span class=\"s2\">&quot;Hello from Python! :tada:&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">thread_ts</span><span class=\"o\">=</span><span class=\"s2\">&quot;1476746830.000003&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>By default, <code class=\"docutils literal notranslate\"><span class=\"pre\">reply_broadcast</span></code> is set to <code class=\"docutils literal notranslate\"><span class=\"pre\">False</span></code>. To indicate your reply is germane to all members of a channel,\nset the <code class=\"docutils literal notranslate\"><span class=\"pre\">reply_broadcast</span></code> boolean parameter to <code class=\"docutils literal notranslate\"><span class=\"pre\">True</span></code>.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;chat.postMessage&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXX&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">text</span><span class=\"o\">=</span><span class=\"s2\">&quot;Hello from Python! :tada:&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">thread_ts</span><span class=\"o\">=</span><span class=\"s2\">&quot;1476746830.000003&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">reply_broadcast</span><span class=\"o\">=</span><span class=\"bp\">True</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p><strong>Note:</strong> While threaded messages may contain attachments and message buttons, when your reply is broadcast to the\nchannel, it’ll actually be a reference to your reply, not the reply itself.\nSo, when appearing in the channel, it won’t contain any attachments or message buttons. Also note that updates and\ndeletion of threaded replies works the same as regular messages.</p>\n<p>See the <a class=\"reference external\" href=\"https://api.slack.com/docs/message-threading#forking_conversations\">Threading messages together</a>\narticle for more information.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"updating-the-content-of-a-message\">\n<h2>Updating the content of a message<a class=\"headerlink\" href=\"#updating-the-content-of-a-message\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Let’s say you have a bot which posts the status of a request. When that request\nis updated, you’ll want to update the message to reflect it’s state. Or your user\nmight want to fix a typo or change some wording. This is how you’ll make those changes.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;chat.update&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">ts</span><span class=\"o\">=</span><span class=\"s2\">&quot;1476746830.000003&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXX&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">text</span><span class=\"o\">=</span><span class=\"s2\">&quot;Hello from Python! :tada:&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/chat.update\">chat.update</a> for formatting options\nand some special considerations when calling this with a bot user.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"deleting-a-message\">\n<h2>Deleting a message<a class=\"headerlink\" href=\"#deleting-a-message\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Sometimes you need to delete things.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;chat.delete&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXX&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">ts</span><span class=\"o\">=</span><span class=\"s2\">&quot;1476745373.000002&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/chat.delete\">chat.delete</a> for more info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"adding-or-removing-an-emoji-reaction\">\n<h2>Adding or removing an emoji reaction<a class=\"headerlink\" href=\"#adding-or-removing-an-emoji-reaction\" title=\"Permalink to this headline\">¶</a></h2>\n<p>You can quickly respond to any message on Slack with an emoji reaction. Reactions\ncan be used for any purpose: voting, checking off to-do items, showing excitement — and just for fun.</p>\n<p>This method adds a reaction (emoji) to an item (<code class=\"docutils literal notranslate\"><span class=\"pre\">file</span></code>, <code class=\"docutils literal notranslate\"><span class=\"pre\">file</span> <span class=\"pre\">comment</span></code>, <code class=\"docutils literal notranslate\"><span class=\"pre\">channel</span> <span class=\"pre\">message</span></code>, <code class=\"docutils literal notranslate\"><span class=\"pre\">group</span> <span class=\"pre\">message</span></code>, or <code class=\"docutils literal notranslate\"><span class=\"pre\">direct</span> <span class=\"pre\">message</span></code>). One of file, file_comment, or the combination of channel and timestamp must be specified.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;reactions.add&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXXX&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">name</span><span class=\"o\">=</span><span class=\"s2\">&quot;thumbsup&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">timestamp</span><span class=\"o\">=</span><span class=\"s2\">&quot;1234567890.123456&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>Removing an emoji reaction is basically the same format, but you’ll use <code class=\"docutils literal notranslate\"><span class=\"pre\">reactions.remove</span></code> instead of <code class=\"docutils literal notranslate\"><span class=\"pre\">reactions.add</span></code></p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;reactions.remove&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXXX&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">name</span><span class=\"o\">=</span><span class=\"s2\">&quot;thumbsup&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">timestamp</span><span class=\"o\">=</span><span class=\"s2\">&quot;1234567890.123456&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/reactions.add\">reactions.add</a> and <a class=\"reference external\" href=\"https://api.slack.com/methods/reactions.remove\">reactions.remove</a> for more info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"getting-a-list-of-channels\">\n<h2>Getting a list of channels<a class=\"headerlink\" href=\"#getting-a-list-of-channels\" title=\"Permalink to this headline\">¶</a></h2>\n<p>At some point, you’ll want to find out what channels are available to your app. This is how you get that list.</p>\n<p><strong>Note:</strong> This call requires the <code class=\"docutils literal notranslate\"><span class=\"pre\">channels:read</span></code> scope.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span><span class=\"s2\">&quot;channels.list&quot;</span><span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>Archived channels are included by default. You can exclude them by passing <code class=\"docutils literal notranslate\"><span class=\"pre\">exclude_archived=1</span></code> to your request.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;channels.list&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">exclude_archived</span><span class=\"o\">=</span><span class=\"mi\">1</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/channels.list\">channels.list</a> for more info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"getting-a-channel-s-info\">\n<h2>Getting a channel’s info<a class=\"headerlink\" href=\"#getting-a-channel-s-info\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Once you have the ID for a specific channel, you can fetch information about that channel.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;channels.info&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXXX&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/channels.info\">channels.info</a> for more info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"joining-a-channel\">\n<h2>Joining a channel<a class=\"headerlink\" href=\"#joining-a-channel\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Channels are the social hub of most Slack teams. Here’s how you hop into one:</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;channels.join&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXXY&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>If you are already in the channel, the response is slightly different.\n<code class=\"docutils literal notranslate\"><span class=\"pre\">already_in_channel</span></code> will be true, and a limited <code class=\"docutils literal notranslate\"><span class=\"pre\">channel</span></code> object will be returned. Bot users cannot join a channel on their own, they need to be invited by another user.</p>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/channels.join\">channels.join</a> for more info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"leaving-a-channel\">\n<h2>Leaving a channel<a class=\"headerlink\" href=\"#leaving-a-channel\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Maybe you’ve finished up all the business you had in a channel, or maybe you\njoined one by accident. This is how you leave a channel.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;channels.leave&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXXX&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/channels.leave\">channels.leave</a> for more info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"get-a-list-of-team-members\">\n<h2>Get a list of team members<a class=\"headerlink\" href=\"#get-a-list-of-team-members\" title=\"Permalink to this headline\">¶</a></h2>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span><span class=\"s2\">&quot;users.list&quot;</span><span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/users.list\">users.list</a> for more info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"uploading-files\">\n<h2>Uploading files<a class=\"headerlink\" href=\"#uploading-files\" title=\"Permalink to this headline\">¶</a></h2>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"k\">with</span> <span class=\"nb\">open</span><span class=\"p\">(</span><span class=\"s1\">&#39;thinking_very_much.png&#39;</span><span class=\"p\">)</span> <span class=\"k\">as</span> <span class=\"n\">file_content</span><span class=\"p\">:</span>\n    <span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n        <span class=\"s2\">&quot;files.upload&quot;</span><span class=\"p\">,</span>\n        <span class=\"n\">channels</span><span class=\"o\">=</span><span class=\"s2\">&quot;C3UKJTQAC&quot;</span><span class=\"p\">,</span>\n        <span class=\"nb\">file</span><span class=\"o\">=</span><span class=\"n\">file_content</span><span class=\"p\">,</span>\n        <span class=\"n\">title</span><span class=\"o\">=</span><span class=\"s2\">&quot;Test upload&quot;</span>\n    <span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/files.upload\">files.upload</a> for more info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"web-api-rate-limits\">\n<h2>Web API Rate Limits<a class=\"headerlink\" href=\"#web-api-rate-limits\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Slack allows applications to send no more than one message per second. We allow bursts over that\nlimit for short periods. However, if your app continues to exceed the limit over a longer period\nof time it will be rate limited.</p>\n<p>Here’s a very basic example of how one might deal with rate limited requests.</p>\n<p>If you go over these limits, Slack will start returning a HTTP 429 Too Many Requests error,\na JSON object containing the number of calls you have been making, and a Retry-After header\ncontaining the number of seconds until you can retry.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n<span class=\"kn\">import</span> <span class=\"nn\">time</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"c1\"># Simple wrapper for sending a Slack message</span>\n<span class=\"k\">def</span> <span class=\"nf\">send_slack_message</span><span class=\"p\">(</span><span class=\"n\">channel</span><span class=\"p\">,</span> <span class=\"n\">message</span><span class=\"p\">):</span>\n  <span class=\"k\">return</span> <span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n    <span class=\"s2\">&quot;chat.postMessage&quot;</span><span class=\"p\">,</span>\n    <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"n\">channel</span><span class=\"p\">,</span>\n    <span class=\"n\">text</span><span class=\"o\">=</span><span class=\"n\">message</span>\n  <span class=\"p\">)</span>\n\n<span class=\"c1\"># Make the API call and save results to `response`</span>\n<span class=\"n\">response</span> <span class=\"o\">=</span> <span class=\"n\">send_slack_message</span><span class=\"p\">(</span><span class=\"s2\">&quot;C0XXXXXX&quot;</span><span class=\"p\">,</span> <span class=\"s2\">&quot;Hello, from Python!&quot;</span><span class=\"p\">)</span>\n\n<span class=\"c1\"># Check to see if the message sent successfully.</span>\n<span class=\"c1\"># If the message succeeded, `response[&quot;ok&quot;]`` will be `True`</span>\n<span class=\"k\">if</span> <span class=\"n\">response</span><span class=\"p\">[</span><span class=\"s2\">&quot;ok&quot;</span><span class=\"p\">]:</span>\n  <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">&quot;Message posted successfully: &quot;</span> <span class=\"o\">+</span> <span class=\"n\">response</span><span class=\"p\">[</span><span class=\"s2\">&quot;message&quot;</span><span class=\"p\">][</span><span class=\"s2\">&quot;ts&quot;</span><span class=\"p\">])</span>\n  <span class=\"c1\"># If the message failed, check for rate limit headers in the response</span>\n<span class=\"k\">elif</span> <span class=\"n\">response</span><span class=\"p\">[</span><span class=\"s2\">&quot;ok&quot;</span><span class=\"p\">]</span> <span class=\"ow\">is</span> <span class=\"bp\">False</span> <span class=\"ow\">and</span> <span class=\"n\">response</span><span class=\"p\">[</span><span class=\"s2\">&quot;headers&quot;</span><span class=\"p\">][</span><span class=\"s2\">&quot;Retry-After&quot;</span><span class=\"p\">]:</span>\n  <span class=\"c1\"># The `Retry-After` header will tell you how long to wait before retrying</span>\n  <span class=\"n\">delay</span> <span class=\"o\">=</span> <span class=\"nb\">int</span><span class=\"p\">(</span><span class=\"n\">response</span><span class=\"p\">[</span><span class=\"s2\">&quot;headers&quot;</span><span class=\"p\">][</span><span class=\"s2\">&quot;Retry-After&quot;</span><span class=\"p\">])</span>\n  <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">&quot;Rate limited. Retrying in &quot;</span> <span class=\"o\">+</span> <span class=\"nb\">str</span><span class=\"p\">(</span><span class=\"n\">delay</span><span class=\"p\">)</span> <span class=\"o\">+</span> <span class=\"s2\">&quot; seconds&quot;</span><span class=\"p\">)</span>\n  <span class=\"n\">time</span><span class=\"o\">.</span><span class=\"n\">sleep</span><span class=\"p\">(</span><span class=\"n\">delay</span><span class=\"p\">)</span>\n  <span class=\"n\">send_slack_message</span><span class=\"p\">(</span><span class=\"n\">message</span><span class=\"p\">,</span> <span class=\"n\">channel</span><span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See the documentation on <a class=\"reference external\" href=\"https://api.slack.com/docs/rate-limits\">Rate Limiting</a> for more info.</p>\n</div>\n</div>\n\n\n          <div class=\"clear_both large_bottom_margin\"></div>\n        </div>\n      </div>\n      <!-- /Body Content -->\n\n    </div>\n  </div>\n\n  <footer>\n    <p class=\"light tiny align_center\">© 2019 Slack Technologies, Inc. and contributors</p>\n  </footer>\n\n  <script>\n    window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;\n    ga('create', 'UA-56978219-13', 'auto');\n    ga('send', 'pageview');\n  </script>\n  <script async src='https://www.google-analytics.com/analytics.js'></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/changelog.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n  \n  <title>Changelog &mdash; Slack Developer Kit for Python</title>\n\n  <!-- Google Tag Manager -->\n  <script>(function (w, d, s, l, i) {\n      w[l] = w[l] || []; w[l].push({\n        'gtm.start':\n          new Date().getTime(), event: 'gtm.js'\n      }); var f = d.getElementsByTagName(s)[0],\n        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =\n          'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);\n    })(window, document, 'script', 'dataLayer', 'GTM-KFZ5MK7');</script>\n  <!-- End Google Tag Manager -->\n  \n  <link href=\"https://a.slack-edge.com/4f227/style/rollup-slack_kit_legacy_adapters.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link href=\"https://a.slack-edge.com/3e02c0/style/rollup-api_site.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link rel=\"stylesheet\" href=\"./_static/default.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/pygments.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/docs.css\" type=\"text/css\" />\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"https://a.slack-edge.com/4f28/img/icons/favicon-32.png\" type=\"image/png\" />\n  <link rel=\"top\" title=\"Slack Developer Kit for Python 1.0.1 documentation\" href=\"index.html\" />\n\n\n\n</head>\n\n<body class=\"api light_theme\">\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-KFZ5MK7\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n  <header>\n    <a id=\"menu_toggle\" class=\"no_transition show_on_mobile\">\n      <span class=\"menu_icon\"></span>\n      <span class=\"vert_divider\"></span>\n    </a>\n    <a href=\"https://api.slack.com/\" id=\"header_logo\" class=\"api hide_on_mobile\" style=\"float:left; display: inline-block;\">\n      <img alt=\"Slack API\" src=\"https://a.slack-edge.com/3026cb/img/slack_api_logo_vogue.png\" style=\"width: 225px; padding-right: 25px; border-right: 1px solid #DDD;\" />\n    </a>\n    <span style=\"display: inline-block; padding-left: 20px; margin-top: 25px; font-weight: bold;\">\n      <a style=\"color: #555459;\" href=\"./index.html\">Slack Developer Kit for Python</a>\n    </span>\n    <div class=\"header_nav\">\n      <a href=\"https://github.com/SlackAPI/python-slackclient\" class=\"btn header_btn float_right\" data-qa=\"go_to_slack\">Go\n        to GitHub</a>\n    </div>\n  </header>\n  \n\n  <div id=\"page\">\n    <div id=\"page_contents\" class=\"clearfix\">\n      <!-- Sidebar Content -->\n      <nav id=\"api_nav\" class=\"col span_1_of_4\">\n        <div id=\"api_sections\">\n          \n          <ul class=\"current\">\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"index.html\">Slack Developer Kit for Python</a></li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"auth.html\">Tokens &amp; Authentication</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#handling-tokens-and-other-sensitive-data\">Handling tokens and other sensitive data</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#single-workspace-apps\">Single-Workspace Apps</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#the-oauth-flow\">The OAuth flow</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"basic_usage.html\">Basic Usage</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#sending-a-message\">Sending a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#customizing-a-message-s-layout\">Customizing a message’s layout</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#replying-to-messages-and-creating-threads\">Replying to messages and creating threads</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#updating-the-content-of-a-message\">Updating the content of a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#deleting-a-message\">Deleting a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#adding-or-removing-an-emoji-reaction\">Adding or removing an emoji reaction</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-list-of-channels\">Getting a list of channels</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-channel-s-info\">Getting a channel’s info</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#joining-a-channel\">Joining a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#leaving-a-channel\">Leaving a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#get-a-list-of-team-members\">Get a list of team members</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#uploading-files\">Uploading files</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#web-api-rate-limits\">Web API Rate Limits</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"conversations.html\">Conversations API</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-direct-message-or-multi-person-direct-message\">Creating a direct message or multi-person direct message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-public-or-private-channel\">Creating a public or private channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-information-about-a-conversation\">Getting information about a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-a-list-of-conversations\">Getting a list of conversations</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#leaving-a-conversation\">Leaving a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#get-conversation-members\">Get conversation members</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"real_time_messaging.html\">Real Time Messaging (RTM)</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#connecting-to-the-rtm-api\">Connecting to the RTM API</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-start-vs-rtm-connect\">rtm.start vs rtm.connect</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-events\">RTM Events</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#sending-messages-via-the-rtm-api\">Sending messages via the RTM API</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"faq.html\">Frequently Asked Questions</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#what-even-is-product-name-and-why-should-i-care\">What even is Slack Developer Kit for Python and why should I care?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#omg-i-found-a-bug\">OMG I found a bug!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#hey-there-s-a-feature-missing\">Hey, there’s a feature missing!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#i-d-like-to-contribute-but-how\">I’d like to contribute…but how?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#how-do-i-compile-the-documentation\">How do I compile the documentation?</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1 current\"><a class=\"current reference internal\" href=\"#\">Changelog</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-3-0-2018-09-11\">v1.3.0 (2018-09-11)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-2-1-2018-03-26\">v1.2.1 (2018-03-26)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-2-0-2018-03-20\">v1.2.0 (2018-03-20)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-1-3-2018-03-01\">v1.1.3 (2018-03-01)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-1-2-2018-01-31\">v1.1.2 (2018-01-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-1-1-2018-01-30\">v1.1.1 (2018-01-30)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-1-0-2017-11-21\">v1.1.0 (2017-11-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-0-9-2017-08-31\">v1.0.9 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-0-8-2017-08-31\">v1.0.8 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-0-7-2017-08-02\">v1.0.7 (2017-08-02)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-0-6-2017-06-12\">v1.0.6 (2017-06-12)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-0-5-2017-01-23\">v1.0.5 (2017-01-23)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-0-4-2016-12-15\">v1.0.4 (2016-12-15)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-0-3-2016-12-13\">v1.0.3 (2016-12-13)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-0-2-2016-09-22\">v1.0.2 (2016-09-22)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-0-1-2016-03-25\">v1.0.1 (2016-03-25)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v1-0-0-2016-02-28\">v1.0.0 (2016-02-28)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v0-18-0-2016-02-21\">v0.18.0 (2016-02-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#v0-17-0-2016-02-15\">v0.17.0 (2016-02-15)</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"about.html\">About</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"about.html#product-name\">Slack Developer Kit for Python</a></li>\n</ul>\n</li>\n</ul>\n\n\n<div id=\"footer\">\n\n    <ul id=\"footer_nav\">\n        <li><a href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n        <li><a href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n        <li><a href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n        <li><a href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n    </ul>\n\n    <p id=\"footer_signature\">Made with <i class=\"ts_icon ts_icon_heart\"></i> by Slack<br/>and our Lovely\n        Community\n    </p>\n</div>\n          \n        </div>\n      </nav>\n      <!-- /Sidebar Content -->\n\n      <!-- Body Content -->\n      <div class=\"col span_3_of_4\">\n        <div class=\"section-title\">Changelog</div>\n        <div class=\"card\">\n  <div class=\"section\" id=\"changelog\">\n<h1>Changelog<a class=\"headerlink\" href=\"#changelog\" title=\"Permalink to this headline\">¶</a></h1>\n<div class=\"section\" id=\"v1-3-0-2018-09-11\">\n<h2>v1.3.0 (2018-09-11)<a class=\"headerlink\" href=\"#v1-3-0-2018-09-11\" title=\"Permalink to this headline\">¶</a></h2>\n<p>## New Features\n- Adds support for short lived tokens and automatic token refresh #347 (Thanks &#64;roach!)</p>\n<p>## Other\n- update RTM rate limiting comment and error message #308 (Thanks &#64;benoitlavigne!)\n- Use logging instead of traceback #309 (Thanks &#64;harlowja!)\n- Remove Python 3.3 from test environments #346 (Thanks &#64;roach!)\n- Enforced linting when using VSCode. #347 (Thanks &#64;roach!)</p>\n</div>\n<div class=\"section\" id=\"v1-2-1-2018-03-26\">\n<h2>v1.2.1 (2018-03-26)<a class=\"headerlink\" href=\"#v1-2-1-2018-03-26\" title=\"Permalink to this headline\">¶</a></h2>\n<ul class=\"simple\">\n<li>Added rate limit handling for rtm connections (thanks &#64;jayalane!)</li>\n</ul>\n</div>\n<div class=\"section\" id=\"v1-2-0-2018-03-20\">\n<h2>v1.2.0 (2018-03-20)<a class=\"headerlink\" href=\"#v1-2-0-2018-03-20\" title=\"Permalink to this headline\">¶</a></h2>\n<ul class=\"simple\">\n<li>You can now tell the RTM client to automatically reconnect by passing <cite>auto_reconnect=True</cite></li>\n</ul>\n</div>\n<div class=\"section\" id=\"v1-1-3-2018-03-01\">\n<h2>v1.1.3 (2018-03-01)<a class=\"headerlink\" href=\"#v1-1-3-2018-03-01\" title=\"Permalink to this headline\">¶</a></h2>\n<ul class=\"simple\">\n<li>Fixed another API param encoding bug. It encodes things properly now.</li>\n</ul>\n</div>\n<div class=\"section\" id=\"v1-1-2-2018-01-31\">\n<h2>v1.1.2 (2018-01-31)<a class=\"headerlink\" href=\"#v1-1-2-2018-01-31\" title=\"Permalink to this headline\">¶</a></h2>\n<ul class=\"simple\">\n<li>Fixed an encoding issue which was encoding some Web API params incorrectly (sorry)</li>\n</ul>\n</div>\n<div class=\"section\" id=\"v1-1-1-2018-01-30\">\n<h2>v1.1.1 (2018-01-30)<a class=\"headerlink\" href=\"#v1-1-1-2018-01-30\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>Adds HTTP response headers to <cite>api_call</cite> responses to expose things like rate limit info</li>\n<li>Moves <cite>token</cite> into auth header rather than request params</li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v1-1-0-2017-11-21\">\n<h2>v1.1.0 (2017-11-21)<a class=\"headerlink\" href=\"#v1-1-0-2017-11-21\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>Aadds new SlackClientError and ResponseParseError types to describe errors - thanks &#64;aoberoi!</li>\n<li>Fix Build Error (#245) - thanks &#64;stasfilin!</li>\n<li>include email as user property (#173) - thanks &#64;acaire!</li>\n<li>Add http reply into slack login and slack connection error (#216) - thanks &#64;harlowja!</li>\n<li>Removed unused exception class (#233)</li>\n<li>Fix rtm_send_message bug (#225) - thanks &#64;kt5356!</li>\n<li>Allow use of custom parameters on rtm_connect() (#210) - thanks &#64;kamushadenes!</li>\n<li>Fix link to rtm.connect docs (#223) - &#64;sampart!</li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v1-0-9-2017-08-31\">\n<h2>v1.0.9 (2017-08-31)<a class=\"headerlink\" href=\"#v1-0-9-2017-08-31\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>Fixed rtm_send_message ID bug introduced in 1.0.8</li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v1-0-8-2017-08-31\">\n<h2>v1.0.8 (2017-08-31)<a class=\"headerlink\" href=\"#v1-0-8-2017-08-31\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>Added rtm.connect support</li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v1-0-7-2017-08-02\">\n<h2>v1.0.7 (2017-08-02)<a class=\"headerlink\" href=\"#v1-0-7-2017-08-02\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>Fixes an issue where connecting over RTM to large teams may result in “Websocket URL expired” errors</li>\n<li>A load of packaging improvements</li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v1-0-6-2017-06-12\">\n<h2>v1.0.6 (2017-06-12)<a class=\"headerlink\" href=\"#v1-0-6-2017-06-12\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>Added proxy support (thanks &#64;timfeirg!)</li>\n<li>Tidied up docs (thanks &#64;schlueter!)</li>\n<li>Added tox settings for Python 3 testing (thanks &#64;cclauss!)</li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v1-0-5-2017-01-23\">\n<h2>v1.0.5 (2017-01-23)<a class=\"headerlink\" href=\"#v1-0-5-2017-01-23\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>Allow RTM Channel.send_message to reply to a thread</li>\n<li>Index users by ID instead of Name (non-breaking change)</li>\n<li>Added timeout to api calls.</li>\n<li>Fixed a typo about token access in auth.rst, thanks &#64;kelvintaywl!</li>\n<li>Added Message Threads to the docs</li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v1-0-4-2016-12-15\">\n<h2>v1.0.4 (2016-12-15)<a class=\"headerlink\" href=\"#v1-0-4-2016-12-15\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>fixed the ability to search for a user by ID</li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v1-0-3-2016-12-13\">\n<h2>v1.0.3 (2016-12-13)<a class=\"headerlink\" href=\"#v1-0-3-2016-12-13\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>fixed an issue causing RTM connections to fail for large teams</li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v1-0-2-2016-09-22\">\n<h2>v1.0.2 (2016-09-22)<a class=\"headerlink\" href=\"#v1-0-2-2016-09-22\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>removed unused ping counter</li>\n<li>fixed contributor guidelines links</li>\n<li>updated documentation</li>\n<li>Fix bug preventing API calls requiring a file ID</li>\n<li>Removes files from api_calls before JSON encoding, so the request is properly formatted</li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v1-0-1-2016-03-25\">\n<h2>v1.0.1 (2016-03-25)<a class=\"headerlink\" href=\"#v1-0-1-2016-03-25\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>fix for __eq__ comparison in channels using ‘#’ in channel name</li>\n<li>added copyright info to the LICENSE file</li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v1-0-0-2016-02-28\">\n<h2>v1.0.0 (2016-02-28)<a class=\"headerlink\" href=\"#v1-0-0-2016-02-28\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>the <code class=\"docutils literal notranslate\"><span class=\"pre\">api_call</span></code> function now returns a decoded JSON object, rather than a JSON encoded string</li>\n<li>some <code class=\"docutils literal notranslate\"><span class=\"pre\">api_call</span></code> calls now call actions on the parent server object:\n- <code class=\"docutils literal notranslate\"><span class=\"pre\">im.open</span></code>\n- <code class=\"docutils literal notranslate\"><span class=\"pre\">mpim.open</span></code>, <code class=\"docutils literal notranslate\"><span class=\"pre\">groups.create</span></code>, <code class=\"docutils literal notranslate\"><span class=\"pre\">groups.createChild</span></code>\n- <code class=\"docutils literal notranslate\"><span class=\"pre\">channels.create</span></code>, <cite>channels.join`</cite></li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v0-18-0-2016-02-21\">\n<h2>v0.18.0 (2016-02-21)<a class=\"headerlink\" href=\"#v0-18-0-2016-02-21\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>Moves to use semver for versioning</li>\n<li>Adds support for private groups and MPDMs</li>\n<li>Switches to use requests instead of urllib</li>\n<li>Gets Travis CI integration working</li>\n<li>Fixes some formatting issues so the code will work for python 2.6</li>\n<li>Cleans up some unused imports, some PEP-8 fixes and a couple bad default args fixes</li>\n</ul>\n</div></blockquote>\n</div>\n<div class=\"section\" id=\"v0-17-0-2016-02-15\">\n<h2>v0.17.0 (2016-02-15)<a class=\"headerlink\" href=\"#v0-17-0-2016-02-15\" title=\"Permalink to this headline\">¶</a></h2>\n<blockquote>\n<div><ul class=\"simple\">\n<li>Fixes the server so that it doesn’t add duplicate users or channels to its internal lists, <a class=\"reference external\" href=\"https://github.com/slackapi/python-slackclient/commit/0cb4bcd6e887b428e27e8059b6278b86ee661aaa\">https://github.com/slackapi/python-slackclient/commit/0cb4bcd6e887b428e27e8059b6278b86ee661aaa</a></li>\n<li>README updates:\n- Updates the URLs pointing to Slack docs for configuring authentication, <a class=\"reference external\" href=\"https://github.com/slackapi/python-slackclient/commit/7d01515cebc80918a29100b0e4793790eb83e7b9\">https://github.com/slackapi/python-slackclient/commit/7d01515cebc80918a29100b0e4793790eb83e7b9</a>\n- s/channnels/channels, <a class=\"reference external\" href=\"https://github.com/slackapi/python-slackclient/commit/d45285d2f1025899dcd65e259624ee73771f94bb\">https://github.com/slackapi/python-slackclient/commit/d45285d2f1025899dcd65e259624ee73771f94bb</a></li>\n<li>Adds users to the local cache when they join the team, <a class=\"reference external\" href=\"https://github.com/slackapi/python-slackclient/commit/f7bb8889580cc34471ba1ddc05afc34d1a5efa23\">https://github.com/slackapi/python-slackclient/commit/f7bb8889580cc34471ba1ddc05afc34d1a5efa23</a></li>\n<li>Fixes urllib py 2/3 compatibility, <a class=\"reference external\" href=\"https://github.com/slackapi/python-slackclient/commit/1046cc2375a85a22e94573e2aad954ba7287c886\">https://github.com/slackapi/python-slackclient/commit/1046cc2375a85a22e94573e2aad954ba7287c886</a></li>\n</ul>\n</div></blockquote>\n</div>\n</div>\n\n\n          <div class=\"clear_both large_bottom_margin\"></div>\n        </div>\n      </div>\n      <!-- /Body Content -->\n\n    </div>\n  </div>\n\n  <footer>\n    <p class=\"light tiny align_center\">© 2019 Slack Technologies, Inc. and contributors</p>\n  </footer>\n\n  <script>\n    window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;\n    ga('create', 'UA-56978219-13', 'auto');\n    ga('send', 'pageview');\n  </script>\n  <script async src='https://www.google-analytics.com/analytics.js'></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/conversations.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n  \n  <title>Conversations API &mdash; Slack Developer Kit for Python</title>\n\n  <!-- Google Tag Manager -->\n  <script>(function (w, d, s, l, i) {\n      w[l] = w[l] || []; w[l].push({\n        'gtm.start':\n          new Date().getTime(), event: 'gtm.js'\n      }); var f = d.getElementsByTagName(s)[0],\n        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =\n          'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);\n    })(window, document, 'script', 'dataLayer', 'GTM-KFZ5MK7');</script>\n  <!-- End Google Tag Manager -->\n  \n  <link href=\"https://a.slack-edge.com/4f227/style/rollup-slack_kit_legacy_adapters.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link href=\"https://a.slack-edge.com/3e02c0/style/rollup-api_site.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link rel=\"stylesheet\" href=\"./_static/default.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/pygments.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/docs.css\" type=\"text/css\" />\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"https://a.slack-edge.com/4f28/img/icons/favicon-32.png\" type=\"image/png\" />\n  <link rel=\"top\" title=\"Slack Developer Kit for Python 1.0.1 documentation\" href=\"index.html\" />\n\n\n\n</head>\n\n<body class=\"api light_theme\">\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-KFZ5MK7\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n  <header>\n    <a id=\"menu_toggle\" class=\"no_transition show_on_mobile\">\n      <span class=\"menu_icon\"></span>\n      <span class=\"vert_divider\"></span>\n    </a>\n    <a href=\"https://api.slack.com/\" id=\"header_logo\" class=\"api hide_on_mobile\" style=\"float:left; display: inline-block;\">\n      <img alt=\"Slack API\" src=\"https://a.slack-edge.com/3026cb/img/slack_api_logo_vogue.png\" style=\"width: 225px; padding-right: 25px; border-right: 1px solid #DDD;\" />\n    </a>\n    <span style=\"display: inline-block; padding-left: 20px; margin-top: 25px; font-weight: bold;\">\n      <a style=\"color: #555459;\" href=\"./index.html\">Slack Developer Kit for Python</a>\n    </span>\n    <div class=\"header_nav\">\n      <a href=\"https://github.com/SlackAPI/python-slackclient\" class=\"btn header_btn float_right\" data-qa=\"go_to_slack\">Go\n        to GitHub</a>\n    </div>\n  </header>\n  \n\n  <div id=\"page\">\n    <div id=\"page_contents\" class=\"clearfix\">\n      <!-- Sidebar Content -->\n      <nav id=\"api_nav\" class=\"col span_1_of_4\">\n        <div id=\"api_sections\">\n          \n          <ul class=\"current\">\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"index.html\">Slack Developer Kit for Python</a></li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"auth.html\">Tokens &amp; Authentication</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#handling-tokens-and-other-sensitive-data\">Handling tokens and other sensitive data</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#single-workspace-apps\">Single-Workspace Apps</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#the-oauth-flow\">The OAuth flow</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"basic_usage.html\">Basic Usage</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#sending-a-message\">Sending a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#customizing-a-message-s-layout\">Customizing a message’s layout</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#replying-to-messages-and-creating-threads\">Replying to messages and creating threads</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#updating-the-content-of-a-message\">Updating the content of a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#deleting-a-message\">Deleting a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#adding-or-removing-an-emoji-reaction\">Adding or removing an emoji reaction</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-list-of-channels\">Getting a list of channels</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-channel-s-info\">Getting a channel’s info</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#joining-a-channel\">Joining a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#leaving-a-channel\">Leaving a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#get-a-list-of-team-members\">Get a list of team members</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#uploading-files\">Uploading files</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#web-api-rate-limits\">Web API Rate Limits</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1 current\"><a class=\"current reference internal\" href=\"#\">Conversations API</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#creating-a-direct-message-or-multi-person-direct-message\">Creating a direct message or multi-person direct message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#creating-a-public-or-private-channel\">Creating a public or private channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#getting-information-about-a-conversation\">Getting information about a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#getting-a-list-of-conversations\">Getting a list of conversations</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#leaving-a-conversation\">Leaving a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#get-conversation-members\">Get conversation members</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"real_time_messaging.html\">Real Time Messaging (RTM)</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#connecting-to-the-rtm-api\">Connecting to the RTM API</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-start-vs-rtm-connect\">rtm.start vs rtm.connect</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-events\">RTM Events</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#sending-messages-via-the-rtm-api\">Sending messages via the RTM API</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"faq.html\">Frequently Asked Questions</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#what-even-is-product-name-and-why-should-i-care\">What even is Slack Developer Kit for Python and why should I care?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#omg-i-found-a-bug\">OMG I found a bug!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#hey-there-s-a-feature-missing\">Hey, there’s a feature missing!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#i-d-like-to-contribute-but-how\">I’d like to contribute…but how?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#how-do-i-compile-the-documentation\">How do I compile the documentation?</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"changelog.html\">Changelog</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-3-0-2018-09-11\">v1.3.0 (2018-09-11)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-1-2018-03-26\">v1.2.1 (2018-03-26)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-0-2018-03-20\">v1.2.0 (2018-03-20)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-3-2018-03-01\">v1.1.3 (2018-03-01)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-2-2018-01-31\">v1.1.2 (2018-01-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-1-2018-01-30\">v1.1.1 (2018-01-30)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-0-2017-11-21\">v1.1.0 (2017-11-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-9-2017-08-31\">v1.0.9 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-8-2017-08-31\">v1.0.8 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-7-2017-08-02\">v1.0.7 (2017-08-02)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-6-2017-06-12\">v1.0.6 (2017-06-12)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-5-2017-01-23\">v1.0.5 (2017-01-23)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-4-2016-12-15\">v1.0.4 (2016-12-15)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-3-2016-12-13\">v1.0.3 (2016-12-13)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-2-2016-09-22\">v1.0.2 (2016-09-22)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-1-2016-03-25\">v1.0.1 (2016-03-25)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-0-2016-02-28\">v1.0.0 (2016-02-28)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-18-0-2016-02-21\">v0.18.0 (2016-02-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-17-0-2016-02-15\">v0.17.0 (2016-02-15)</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"about.html\">About</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"about.html#product-name\">Slack Developer Kit for Python</a></li>\n</ul>\n</li>\n</ul>\n\n\n<div id=\"footer\">\n\n    <ul id=\"footer_nav\">\n        <li><a href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n        <li><a href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n        <li><a href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n        <li><a href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n    </ul>\n\n    <p id=\"footer_signature\">Made with <i class=\"ts_icon ts_icon_heart\"></i> by Slack<br/>and our Lovely\n        Community\n    </p>\n</div>\n          \n        </div>\n      </nav>\n      <!-- /Sidebar Content -->\n\n      <!-- Body Content -->\n      <div class=\"col span_3_of_4\">\n        <div class=\"section-title\">Conversations API</div>\n        <div class=\"card\">\n  <div class=\"section\" id=\"conversations-api\">\n<span id=\"id1\"></span><h1>Conversations API<a class=\"headerlink\" href=\"#conversations-api\" title=\"Permalink to this headline\">¶</a></h1>\n<p>The Slack Conversations API provides your app with a unified interface to work with all the\nchannel-like things encountered in Slack; public channels, private channels, direct messages, group\ndirect messages, and our newest channel type, Shared Channels.</p>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/docs/conversations-api\">Conversations API</a> docs for more info.</p>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"creating-a-direct-message-or-multi-person-direct-message\">\n<h2>Creating a direct message or multi-person direct message<a class=\"headerlink\" href=\"#creating-a-direct-message-or-multi-person-direct-message\" title=\"Permalink to this headline\">¶</a></h2>\n<p>This Conversations API method opens a multi-person direct message or just a 1:1 direct message.</p>\n<p><em>Use conversations.create for public or private channels.</em></p>\n<p>Provide 1 to 8 user IDs in the <code class=\"docutils literal notranslate\"><span class=\"pre\">user</span></code> parameter to open or resume a conversation. Providing only\n1 ID will create a direct message. Providing more will create an <code class=\"docutils literal notranslate\"><span class=\"pre\">mpim</span></code>.</p>\n<p>If there are no conversations already in progress including that exact set of members, a new\nmulti-person direct message conversation begins.</p>\n<p>Subsequent calls to <code class=\"docutils literal notranslate\"><span class=\"pre\">conversations.open</span></code> with the same set of users will return the already\nexisting conversation.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;conversations.open&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">users</span><span class=\"o\">=</span><span class=\"p\">[</span><span class=\"s2\">&quot;W1234567890&quot;</span><span class=\"p\">,</span><span class=\"s2\">&quot;U2345678901&quot;</span><span class=\"p\">,</span><span class=\"s2\">&quot;U3456789012&quot;</span><span class=\"p\">]</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/conversations.open\">conversations.open</a> additional info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"creating-a-public-or-private-channel\">\n<h2>Creating a public or private channel<a class=\"headerlink\" href=\"#creating-a-public-or-private-channel\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Initiates a public or private channel-based conversation</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;conversations.create&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">name</span><span class=\"o\">=</span><span class=\"s2\">&quot;myprivatechannel&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">is_private</span><span class=\"o\">=</span><span class=\"bp\">True</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/conversations.create\">conversations.create</a> additional info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"getting-information-about-a-conversation\">\n<h2>Getting information about a conversation<a class=\"headerlink\" href=\"#getting-information-about-a-conversation\" title=\"Permalink to this headline\">¶</a></h2>\n<p>This Conversations API method returns information about a workspace conversation.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;conversations.info&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXX&quot;</span><span class=\"p\">,</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/conversations.info\">conversations.info</a> for more info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"getting-a-list-of-conversations\">\n<h2>Getting a list of conversations<a class=\"headerlink\" href=\"#getting-a-list-of-conversations\" title=\"Permalink to this headline\">¶</a></h2>\n<p>This Conversations API method returns a list of all channel-like conversations in a workspace.\nThe “channels” returned depend on what the calling token has access to and the directives placed\nin the <code class=\"docutils literal notranslate\"><span class=\"pre\">types</span></code> parameter.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span><span class=\"s2\">&quot;conversations.list&quot;</span><span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>Only public conversations are included by default. You may include additional conversations types\nby passing <code class=\"docutils literal notranslate\"><span class=\"pre\">types</span></code> (as a string) into your list request. Additional conversation types include\n<code class=\"docutils literal notranslate\"><span class=\"pre\">public_channel</span></code> and <code class=\"docutils literal notranslate\"><span class=\"pre\">private_channel</span></code>.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"c1\"># Note that `types` is a string</span>\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;conversations.list&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">types</span><span class=\"o\">=</span><span class=\"s2\">&quot;public_channel, private_channel&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/conversations.list\">conversations.list</a> for more info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"leaving-a-conversation\">\n<h2>Leaving a conversation<a class=\"headerlink\" href=\"#leaving-a-conversation\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Maybe you’ve finished up all the business you had in a conversation, or maybe you\njoined one by accident. This is how you leave a conversation.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span>\n  <span class=\"s2\">&quot;conversations.leave&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXXX&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/conversations.leave\">conversations.leave</a> for more info.</p>\n</div>\n<hr class=\"docutils\" />\n<div class=\"section\" id=\"get-conversation-members\">\n<h2>Get conversation members<a class=\"headerlink\" href=\"#get-conversation-members\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Get a list fo the members of a conversation</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">api_call</span><span class=\"p\">(</span><span class=\"s2\">&quot;conversations.members&quot;</span><span class=\"p\">,</span>\n  <span class=\"n\">channel</span><span class=\"o\">=</span><span class=\"s2\">&quot;C0XXXXXXX&quot;</span>\n<span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/methods/conversations.members\">users.list</a> for more info.</p>\n</div>\n</div>\n\n\n          <div class=\"clear_both large_bottom_margin\"></div>\n        </div>\n      </div>\n      <!-- /Body Content -->\n\n    </div>\n  </div>\n\n  <footer>\n    <p class=\"light tiny align_center\">© 2019 Slack Technologies, Inc. and contributors</p>\n  </footer>\n\n  <script>\n    window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;\n    ga('create', 'UA-56978219-13', 'auto');\n    ga('send', 'pageview');\n  </script>\n  <script async src='https://www.google-analytics.com/analytics.js'></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/faq.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n  \n  <title>Frequently Asked Questions &mdash; Slack Developer Kit for Python</title>\n\n  <!-- Google Tag Manager -->\n  <script>(function (w, d, s, l, i) {\n      w[l] = w[l] || []; w[l].push({\n        'gtm.start':\n          new Date().getTime(), event: 'gtm.js'\n      }); var f = d.getElementsByTagName(s)[0],\n        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =\n          'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);\n    })(window, document, 'script', 'dataLayer', 'GTM-KFZ5MK7');</script>\n  <!-- End Google Tag Manager -->\n  \n  <link href=\"https://a.slack-edge.com/4f227/style/rollup-slack_kit_legacy_adapters.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link href=\"https://a.slack-edge.com/3e02c0/style/rollup-api_site.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link rel=\"stylesheet\" href=\"./_static/default.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/pygments.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/docs.css\" type=\"text/css\" />\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"https://a.slack-edge.com/4f28/img/icons/favicon-32.png\" type=\"image/png\" />\n  <link rel=\"top\" title=\"Slack Developer Kit for Python 1.0.1 documentation\" href=\"index.html\" />\n\n\n\n</head>\n\n<body class=\"api light_theme\">\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-KFZ5MK7\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n  <header>\n    <a id=\"menu_toggle\" class=\"no_transition show_on_mobile\">\n      <span class=\"menu_icon\"></span>\n      <span class=\"vert_divider\"></span>\n    </a>\n    <a href=\"https://api.slack.com/\" id=\"header_logo\" class=\"api hide_on_mobile\" style=\"float:left; display: inline-block;\">\n      <img alt=\"Slack API\" src=\"https://a.slack-edge.com/3026cb/img/slack_api_logo_vogue.png\" style=\"width: 225px; padding-right: 25px; border-right: 1px solid #DDD;\" />\n    </a>\n    <span style=\"display: inline-block; padding-left: 20px; margin-top: 25px; font-weight: bold;\">\n      <a style=\"color: #555459;\" href=\"./index.html\">Slack Developer Kit for Python</a>\n    </span>\n    <div class=\"header_nav\">\n      <a href=\"https://github.com/SlackAPI/python-slackclient\" class=\"btn header_btn float_right\" data-qa=\"go_to_slack\">Go\n        to GitHub</a>\n    </div>\n  </header>\n  \n\n  <div id=\"page\">\n    <div id=\"page_contents\" class=\"clearfix\">\n      <!-- Sidebar Content -->\n      <nav id=\"api_nav\" class=\"col span_1_of_4\">\n        <div id=\"api_sections\">\n          \n          <ul class=\"current\">\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"index.html\">Slack Developer Kit for Python</a></li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"auth.html\">Tokens &amp; Authentication</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#handling-tokens-and-other-sensitive-data\">Handling tokens and other sensitive data</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#single-workspace-apps\">Single-Workspace Apps</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#the-oauth-flow\">The OAuth flow</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"basic_usage.html\">Basic Usage</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#sending-a-message\">Sending a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#customizing-a-message-s-layout\">Customizing a message’s layout</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#replying-to-messages-and-creating-threads\">Replying to messages and creating threads</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#updating-the-content-of-a-message\">Updating the content of a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#deleting-a-message\">Deleting a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#adding-or-removing-an-emoji-reaction\">Adding or removing an emoji reaction</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-list-of-channels\">Getting a list of channels</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-channel-s-info\">Getting a channel’s info</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#joining-a-channel\">Joining a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#leaving-a-channel\">Leaving a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#get-a-list-of-team-members\">Get a list of team members</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#uploading-files\">Uploading files</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#web-api-rate-limits\">Web API Rate Limits</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"conversations.html\">Conversations API</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-direct-message-or-multi-person-direct-message\">Creating a direct message or multi-person direct message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-public-or-private-channel\">Creating a public or private channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-information-about-a-conversation\">Getting information about a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-a-list-of-conversations\">Getting a list of conversations</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#leaving-a-conversation\">Leaving a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#get-conversation-members\">Get conversation members</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"real_time_messaging.html\">Real Time Messaging (RTM)</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#connecting-to-the-rtm-api\">Connecting to the RTM API</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-start-vs-rtm-connect\">rtm.start vs rtm.connect</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-events\">RTM Events</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#sending-messages-via-the-rtm-api\">Sending messages via the RTM API</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1 current\"><a class=\"current reference internal\" href=\"#\">Frequently Asked Questions</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#what-even-is-product-name-and-why-should-i-care\">What even is Slack Developer Kit for Python and why should I care?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#omg-i-found-a-bug\">OMG I found a bug!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#hey-there-s-a-feature-missing\">Hey, there’s a feature missing!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#i-d-like-to-contribute-but-how\">I’d like to contribute…but how?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#how-do-i-compile-the-documentation\">How do I compile the documentation?</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"changelog.html\">Changelog</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-3-0-2018-09-11\">v1.3.0 (2018-09-11)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-1-2018-03-26\">v1.2.1 (2018-03-26)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-0-2018-03-20\">v1.2.0 (2018-03-20)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-3-2018-03-01\">v1.1.3 (2018-03-01)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-2-2018-01-31\">v1.1.2 (2018-01-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-1-2018-01-30\">v1.1.1 (2018-01-30)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-0-2017-11-21\">v1.1.0 (2017-11-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-9-2017-08-31\">v1.0.9 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-8-2017-08-31\">v1.0.8 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-7-2017-08-02\">v1.0.7 (2017-08-02)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-6-2017-06-12\">v1.0.6 (2017-06-12)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-5-2017-01-23\">v1.0.5 (2017-01-23)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-4-2016-12-15\">v1.0.4 (2016-12-15)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-3-2016-12-13\">v1.0.3 (2016-12-13)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-2-2016-09-22\">v1.0.2 (2016-09-22)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-1-2016-03-25\">v1.0.1 (2016-03-25)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-0-2016-02-28\">v1.0.0 (2016-02-28)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-18-0-2016-02-21\">v0.18.0 (2016-02-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-17-0-2016-02-15\">v0.17.0 (2016-02-15)</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"about.html\">About</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"about.html#product-name\">Slack Developer Kit for Python</a></li>\n</ul>\n</li>\n</ul>\n\n\n<div id=\"footer\">\n\n    <ul id=\"footer_nav\">\n        <li><a href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n        <li><a href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n        <li><a href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n        <li><a href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n    </ul>\n\n    <p id=\"footer_signature\">Made with <i class=\"ts_icon ts_icon_heart\"></i> by Slack<br/>and our Lovely\n        Community\n    </p>\n</div>\n          \n        </div>\n      </nav>\n      <!-- /Sidebar Content -->\n\n      <!-- Body Content -->\n      <div class=\"col span_3_of_4\">\n        <div class=\"section-title\">Frequently Asked Questions</div>\n        <div class=\"card\">\n  <div class=\"section\" id=\"frequently-asked-questions\">\n<h1>Frequently Asked Questions<a class=\"headerlink\" href=\"#frequently-asked-questions\" title=\"Permalink to this headline\">¶</a></h1>\n<div class=\"section\" id=\"what-even-is-product-name-and-why-should-i-care\">\n<h2>What even is Slack Developer Kit for Python and why should I care?<a class=\"headerlink\" href=\"#what-even-is-product-name-and-why-should-i-care\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Slack Developer Kit for Python is a wrapper around commonly accessed parts of the Slack Platform. It provides basic mechanisms for\nusing the Slack Web API from within your Python app.</p>\n<p>On the other hand, Slack Developer Kit for Python does not provide access to the Events bot-building API, but\n[this adapter](<a class=\"reference external\" href=\"https://github.com/slackapi/python-slack-events-api\">https://github.com/slackapi/python-slack-events-api</a>) does.</p>\n</div>\n<div class=\"section\" id=\"omg-i-found-a-bug\">\n<h2>OMG I found a bug!<a class=\"headerlink\" href=\"#omg-i-found-a-bug\" title=\"Permalink to this headline\">¶</a></h2>\n<p>Well, poop. Take a deep breath, and then let us know on the <a class=\"reference external\" href=\"http://github.com/SlackAPI/python-slackclient/issues\">Issue Tracker</a>. If you’re feeling particularly ambitious,\nwhy not submit a <a class=\"reference external\" href=\"http://github.com/SlackAPI/python-slackclient/pulls\">pull request</a> with a bug fix?</p>\n</div>\n<div class=\"section\" id=\"hey-there-s-a-feature-missing\">\n<h2>Hey, there’s a feature missing!<a class=\"headerlink\" href=\"#hey-there-s-a-feature-missing\" title=\"Permalink to this headline\">¶</a></h2>\n<p>There’s always something more that could be added! You can let us know in the <a class=\"reference external\" href=\"http://github.com/SlackAPI/python-slackclient/issues\">Issue Tracker</a> to start a discussion\naround the proposed feature, that’s a good start. If you’re feeling particularly ambitious, why not write the feature\nyourself, and submit a <a class=\"reference external\" href=\"http://github.com/SlackAPI/python-slackclient/pulls\">pull request</a>! We love feedback and we love help and we don’t bite. Much.</p>\n</div>\n<div class=\"section\" id=\"i-d-like-to-contribute-but-how\">\n<h2>I’d like to contribute…but how?<a class=\"headerlink\" href=\"#i-d-like-to-contribute-but-how\" title=\"Permalink to this headline\">¶</a></h2>\n<p>What an excellent question. First of all, please have a look at our general <a class=\"reference external\" href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">contributing guidelines</a>. We’ll wait for\nyou here.</p>\n<p>All done? Great! While we’re super excited to incorporate your new feature into Slack Developer Kit for Python, there are a\ncouple of things we want to make sure you’ve given thought to.</p>\n<ul class=\"simple\">\n<li>Please write unit tests for your new code. But don’t <strong>just</strong> aim to increase the test coverage, rather, we expect you\nto have written <strong>thoughtful</strong> tests that ensure your new feature will continue to work as expected, and to help future\ncontributors to ensure they don’t break it!</li>\n<li>Please document your new feature. Think about <strong>concrete use cases</strong> for your feature, and add a section to the\nappropriate document, including a <strong>complete</strong> sample program that demonstrates your feature. Don’t forget to update\nthe changelog in <code class=\"docutils literal notranslate\"><span class=\"pre\">changelog.rst</span></code>!</li>\n</ul>\n<p>Including these two items with your pull request will totally make our day—and, more importantly, your future users’ days!</p>\n<p>On that note…</p>\n</div>\n<div class=\"section\" id=\"how-do-i-compile-the-documentation\">\n<h2>How do I compile the documentation?<a class=\"headerlink\" href=\"#how-do-i-compile-the-documentation\" title=\"Permalink to this headline\">¶</a></h2>\n<p>This project’s documentation is generated with <a class=\"reference external\" href=\"http://www.sphinx-doc.org\">Sphinx</a>. If you are editing one of the many\nreStructuredText files in the <code class=\"docutils literal notranslate\"><span class=\"pre\">docs-src</span></code> folder, you’ll need to rebuild the documentation. It is recommended to run\nthe following steps inside a <code class=\"docutils literal notranslate\"><span class=\"pre\">virtualenv</span></code> environment.</p>\n<div class=\"highlight-bash notranslate\"><div class=\"highlight\"><pre><span></span>tox -e docs\n</pre></div>\n</div>\n<p>Do be sure to add the <code class=\"docutils literal notranslate\"><span class=\"pre\">docs</span></code> folder and its contents to your pull request!</p>\n</div>\n</div>\n\n\n          <div class=\"clear_both large_bottom_margin\"></div>\n        </div>\n      </div>\n      <!-- /Body Content -->\n\n    </div>\n  </div>\n\n  <footer>\n    <p class=\"light tiny align_center\">© 2019 Slack Technologies, Inc. and contributors</p>\n  </footer>\n\n  <script>\n    window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;\n    ga('create', 'UA-56978219-13', 'auto');\n    ga('send', 'pageview');\n  </script>\n  <script async src='https://www.google-analytics.com/analytics.js'></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/genindex.html",
    "content": "\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n  \n  <title>Index &mdash; Slack Developer Kit for Python</title>\n\n  <!-- Google Tag Manager -->\n  <script>(function (w, d, s, l, i) {\n      w[l] = w[l] || []; w[l].push({\n        'gtm.start':\n          new Date().getTime(), event: 'gtm.js'\n      }); var f = d.getElementsByTagName(s)[0],\n        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =\n          'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);\n    })(window, document, 'script', 'dataLayer', 'GTM-KFZ5MK7');</script>\n  <!-- End Google Tag Manager -->\n  \n  <link href=\"https://a.slack-edge.com/4f227/style/rollup-slack_kit_legacy_adapters.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link href=\"https://a.slack-edge.com/3e02c0/style/rollup-api_site.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link rel=\"stylesheet\" href=\"./_static/default.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/pygments.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/docs.css\" type=\"text/css\" />\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"https://a.slack-edge.com/4f28/img/icons/favicon-32.png\" type=\"image/png\" />\n  <link rel=\"top\" title=\"Slack Developer Kit for Python 1.0.1 documentation\" href=\"index.html\" />\n\n\n\n</head>\n\n<body class=\"api light_theme\">\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-KFZ5MK7\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n  <header>\n    <a id=\"menu_toggle\" class=\"no_transition show_on_mobile\">\n      <span class=\"menu_icon\"></span>\n      <span class=\"vert_divider\"></span>\n    </a>\n    <a href=\"https://api.slack.com/\" id=\"header_logo\" class=\"api hide_on_mobile\" style=\"float:left; display: inline-block;\">\n      <img alt=\"Slack API\" src=\"https://a.slack-edge.com/3026cb/img/slack_api_logo_vogue.png\" style=\"width: 225px; padding-right: 25px; border-right: 1px solid #DDD;\" />\n    </a>\n    <span style=\"display: inline-block; padding-left: 20px; margin-top: 25px; font-weight: bold;\">\n      <a style=\"color: #555459;\" href=\"./index.html\">Slack Developer Kit for Python</a>\n    </span>\n    <div class=\"header_nav\">\n      <a href=\"https://github.com/SlackAPI/python-slackclient\" class=\"btn header_btn float_right\" data-qa=\"go_to_slack\">Go\n        to GitHub</a>\n    </div>\n  </header>\n  \n\n  <div id=\"page\">\n    <div id=\"page_contents\" class=\"clearfix\">\n      <!-- Sidebar Content -->\n      <nav id=\"api_nav\" class=\"col span_1_of_4\">\n        <div id=\"api_sections\">\n          \n          <ul>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"index.html\">Slack Developer Kit for Python</a></li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"auth.html\">Tokens &amp; Authentication</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#handling-tokens-and-other-sensitive-data\">Handling tokens and other sensitive data</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#single-workspace-apps\">Single-Workspace Apps</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#the-oauth-flow\">The OAuth flow</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"basic_usage.html\">Basic Usage</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#sending-a-message\">Sending a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#customizing-a-message-s-layout\">Customizing a message’s layout</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#replying-to-messages-and-creating-threads\">Replying to messages and creating threads</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#updating-the-content-of-a-message\">Updating the content of a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#deleting-a-message\">Deleting a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#adding-or-removing-an-emoji-reaction\">Adding or removing an emoji reaction</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-list-of-channels\">Getting a list of channels</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-channel-s-info\">Getting a channel’s info</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#joining-a-channel\">Joining a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#leaving-a-channel\">Leaving a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#get-a-list-of-team-members\">Get a list of team members</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#uploading-files\">Uploading files</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#web-api-rate-limits\">Web API Rate Limits</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"conversations.html\">Conversations API</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-direct-message-or-multi-person-direct-message\">Creating a direct message or multi-person direct message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-public-or-private-channel\">Creating a public or private channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-information-about-a-conversation\">Getting information about a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-a-list-of-conversations\">Getting a list of conversations</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#leaving-a-conversation\">Leaving a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#get-conversation-members\">Get conversation members</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"real_time_messaging.html\">Real Time Messaging (RTM)</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#connecting-to-the-rtm-api\">Connecting to the RTM API</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-start-vs-rtm-connect\">rtm.start vs rtm.connect</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-events\">RTM Events</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#sending-messages-via-the-rtm-api\">Sending messages via the RTM API</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"faq.html\">Frequently Asked Questions</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#what-even-is-product-name-and-why-should-i-care\">What even is Slack Developer Kit for Python and why should I care?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#omg-i-found-a-bug\">OMG I found a bug!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#hey-there-s-a-feature-missing\">Hey, there’s a feature missing!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#i-d-like-to-contribute-but-how\">I’d like to contribute…but how?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#how-do-i-compile-the-documentation\">How do I compile the documentation?</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"changelog.html\">Changelog</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-3-0-2018-09-11\">v1.3.0 (2018-09-11)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-1-2018-03-26\">v1.2.1 (2018-03-26)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-0-2018-03-20\">v1.2.0 (2018-03-20)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-3-2018-03-01\">v1.1.3 (2018-03-01)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-2-2018-01-31\">v1.1.2 (2018-01-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-1-2018-01-30\">v1.1.1 (2018-01-30)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-0-2017-11-21\">v1.1.0 (2017-11-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-9-2017-08-31\">v1.0.9 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-8-2017-08-31\">v1.0.8 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-7-2017-08-02\">v1.0.7 (2017-08-02)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-6-2017-06-12\">v1.0.6 (2017-06-12)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-5-2017-01-23\">v1.0.5 (2017-01-23)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-4-2016-12-15\">v1.0.4 (2016-12-15)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-3-2016-12-13\">v1.0.3 (2016-12-13)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-2-2016-09-22\">v1.0.2 (2016-09-22)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-1-2016-03-25\">v1.0.1 (2016-03-25)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-0-2016-02-28\">v1.0.0 (2016-02-28)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-18-0-2016-02-21\">v0.18.0 (2016-02-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-17-0-2016-02-15\">v0.17.0 (2016-02-15)</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"about.html\">About</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"about.html#product-name\">Slack Developer Kit for Python</a></li>\n</ul>\n</li>\n</ul>\n\n\n<div id=\"footer\">\n\n    <ul id=\"footer_nav\">\n        <li><a href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n        <li><a href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n        <li><a href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n        <li><a href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n    </ul>\n\n    <p id=\"footer_signature\">Made with <i class=\"ts_icon ts_icon_heart\"></i> by Slack<br/>and our Lovely\n        Community\n    </p>\n</div>\n          \n        </div>\n      </nav>\n      <!-- /Sidebar Content -->\n\n      <!-- Body Content -->\n      <div class=\"col span_3_of_4\">\n        <div class=\"section-title\">Index</div>\n        <div class=\"card\">\n\n<h1 id=\"index\">Index</h1>\n\n<div class=\"genindex-jumpbox\">\n \n</div>\n\n\n          <div class=\"clear_both large_bottom_margin\"></div>\n        </div>\n      </div>\n      <!-- /Body Content -->\n\n    </div>\n  </div>\n\n  <footer>\n    <p class=\"light tiny align_center\">© 2019 Slack Technologies, Inc. and contributors</p>\n  </footer>\n\n  <script>\n    window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;\n    ga('create', 'UA-56978219-13', 'auto');\n    ga('send', 'pageview');\n  </script>\n  <script async src='https://www.google-analytics.com/analytics.js'></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/index.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n  \n  <title>Slack Developer Kit for Python &mdash; Slack Developer Kit for Python</title>\n\n  <!-- Google Tag Manager -->\n  <script>(function (w, d, s, l, i) {\n      w[l] = w[l] || []; w[l].push({\n        'gtm.start':\n          new Date().getTime(), event: 'gtm.js'\n      }); var f = d.getElementsByTagName(s)[0],\n        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =\n          'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);\n    })(window, document, 'script', 'dataLayer', 'GTM-KFZ5MK7');</script>\n  <!-- End Google Tag Manager -->\n  \n  <link href=\"https://a.slack-edge.com/4f227/style/rollup-slack_kit_legacy_adapters.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link href=\"https://a.slack-edge.com/3e02c0/style/rollup-api_site.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link rel=\"stylesheet\" href=\"./_static/default.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/pygments.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/docs.css\" type=\"text/css\" />\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"https://a.slack-edge.com/4f28/img/icons/favicon-32.png\" type=\"image/png\" />\n  <link rel=\"top\" title=\"Slack Developer Kit for Python 1.0.1 documentation\" href=\"#\" />\n\n\n\n</head>\n\n<body class=\"api light_theme\">\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-KFZ5MK7\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n  <header>\n    <a id=\"menu_toggle\" class=\"no_transition show_on_mobile\">\n      <span class=\"menu_icon\"></span>\n      <span class=\"vert_divider\"></span>\n    </a>\n    <a href=\"https://api.slack.com/\" id=\"header_logo\" class=\"api hide_on_mobile\" style=\"float:left; display: inline-block;\">\n      <img alt=\"Slack API\" src=\"https://a.slack-edge.com/3026cb/img/slack_api_logo_vogue.png\" style=\"width: 225px; padding-right: 25px; border-right: 1px solid #DDD;\" />\n    </a>\n    <span style=\"display: inline-block; padding-left: 20px; margin-top: 25px; font-weight: bold;\">\n      <a style=\"color: #555459;\" href=\"./index.html\">Slack Developer Kit for Python</a>\n    </span>\n    <div class=\"header_nav\">\n      <a href=\"https://github.com/SlackAPI/python-slackclient\" class=\"btn header_btn float_right\" data-qa=\"go_to_slack\">Go\n        to GitHub</a>\n    </div>\n  </header>\n  \n\n  <div id=\"page\">\n    <div id=\"page_contents\" class=\"clearfix\">\n      <!-- Sidebar Content -->\n      <nav id=\"api_nav\" class=\"col span_1_of_4\">\n        <div id=\"api_sections\">\n          \n          <ul class=\"current\">\n<li class=\"toctree-l1 current\"><a class=\"current reference internal\" href=\"#\">Slack Developer Kit for Python</a></li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"auth.html\">Tokens &amp; Authentication</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#handling-tokens-and-other-sensitive-data\">Handling tokens and other sensitive data</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#single-workspace-apps\">Single-Workspace Apps</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#the-oauth-flow\">The OAuth flow</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"basic_usage.html\">Basic Usage</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#sending-a-message\">Sending a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#customizing-a-message-s-layout\">Customizing a message’s layout</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#replying-to-messages-and-creating-threads\">Replying to messages and creating threads</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#updating-the-content-of-a-message\">Updating the content of a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#deleting-a-message\">Deleting a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#adding-or-removing-an-emoji-reaction\">Adding or removing an emoji reaction</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-list-of-channels\">Getting a list of channels</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-channel-s-info\">Getting a channel’s info</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#joining-a-channel\">Joining a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#leaving-a-channel\">Leaving a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#get-a-list-of-team-members\">Get a list of team members</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#uploading-files\">Uploading files</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#web-api-rate-limits\">Web API Rate Limits</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"conversations.html\">Conversations API</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-direct-message-or-multi-person-direct-message\">Creating a direct message or multi-person direct message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-public-or-private-channel\">Creating a public or private channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-information-about-a-conversation\">Getting information about a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-a-list-of-conversations\">Getting a list of conversations</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#leaving-a-conversation\">Leaving a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#get-conversation-members\">Get conversation members</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"real_time_messaging.html\">Real Time Messaging (RTM)</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#connecting-to-the-rtm-api\">Connecting to the RTM API</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-start-vs-rtm-connect\">rtm.start vs rtm.connect</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-events\">RTM Events</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#sending-messages-via-the-rtm-api\">Sending messages via the RTM API</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"faq.html\">Frequently Asked Questions</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#what-even-is-product-name-and-why-should-i-care\">What even is Slack Developer Kit for Python and why should I care?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#omg-i-found-a-bug\">OMG I found a bug!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#hey-there-s-a-feature-missing\">Hey, there’s a feature missing!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#i-d-like-to-contribute-but-how\">I’d like to contribute…but how?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#how-do-i-compile-the-documentation\">How do I compile the documentation?</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"changelog.html\">Changelog</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-3-0-2018-09-11\">v1.3.0 (2018-09-11)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-1-2018-03-26\">v1.2.1 (2018-03-26)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-0-2018-03-20\">v1.2.0 (2018-03-20)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-3-2018-03-01\">v1.1.3 (2018-03-01)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-2-2018-01-31\">v1.1.2 (2018-01-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-1-2018-01-30\">v1.1.1 (2018-01-30)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-0-2017-11-21\">v1.1.0 (2017-11-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-9-2017-08-31\">v1.0.9 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-8-2017-08-31\">v1.0.8 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-7-2017-08-02\">v1.0.7 (2017-08-02)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-6-2017-06-12\">v1.0.6 (2017-06-12)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-5-2017-01-23\">v1.0.5 (2017-01-23)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-4-2016-12-15\">v1.0.4 (2016-12-15)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-3-2016-12-13\">v1.0.3 (2016-12-13)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-2-2016-09-22\">v1.0.2 (2016-09-22)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-1-2016-03-25\">v1.0.1 (2016-03-25)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-0-2016-02-28\">v1.0.0 (2016-02-28)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-18-0-2016-02-21\">v0.18.0 (2016-02-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-17-0-2016-02-15\">v0.17.0 (2016-02-15)</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"about.html\">About</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"about.html#product-name\">Slack Developer Kit for Python</a></li>\n</ul>\n</li>\n</ul>\n\n\n<div id=\"footer\">\n\n    <ul id=\"footer_nav\">\n        <li><a href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n        <li><a href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n        <li><a href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n        <li><a href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n    </ul>\n\n    <p id=\"footer_signature\">Made with <i class=\"ts_icon ts_icon_heart\"></i> by Slack<br/>and our Lovely\n        Community\n    </p>\n</div>\n          \n        </div>\n      </nav>\n      <!-- /Sidebar Content -->\n\n      <!-- Body Content -->\n      <div class=\"col span_3_of_4\">\n        <div class=\"section-title\">Slack Developer Kit for Python</div>\n        <div class=\"card\">\n  <div class=\"toctree-wrapper compound\">\n</div>\n<div class=\"section\" id=\"product-name\">\n<h1>Slack Developer Kit for Python<a class=\"headerlink\" href=\"#product-name\" title=\"Permalink to this headline\">¶</a></h1>\n<p>Whether you’re building a custom app for your team, or integrating a third party\nservice into your Slack workflows, Slack Developer Kit for Python allows you to leverage the flexibility\nof Python to get your project up and running as quickly as possible.</p>\n<div class=\"section\" id=\"requirements-and-installation\">\n<h2>Requirements and Installation<a class=\"headerlink\" href=\"#requirements-and-installation\" title=\"Permalink to this headline\">¶</a></h2>\n<p>We recommend using <a class=\"reference external\" href=\"https://pypi.python.org/pypi\">PyPI</a> to install Slack Developer Kit for Python</p>\n<div class=\"highlight-bash notranslate\"><div class=\"highlight\"><pre><span></span>pip install slackclient\n</pre></div>\n</div>\n<p>Of course, if you prefer doing things the hard way, you can always implement Slack Developer Kit for Python\nby pulling down the source code directly into your project:</p>\n<div class=\"highlight-bash notranslate\"><div class=\"highlight\"><pre><span></span>git clone https://github.com/slackapi/python-slackclient.git\npip install -r requirements.txt\n</pre></div>\n</div>\n</div>\n<div class=\"section\" id=\"getting-help\">\n<h2>Getting Help<a class=\"headerlink\" href=\"#getting-help\" title=\"Permalink to this headline\">¶</a></h2>\n<p>If you get stuck, we’re here to help. The following are the best ways to get assistance working through your issue:</p>\n<ul class=\"simple\">\n<li>Use our <a class=\"reference external\" href=\"https://github.com/slackapi/python-slackclient/issues\">Github Issue Tracker</a> for reporting bugs or requesting features.</li>\n<li>Visit the <a class=\"reference external\" href=\"http://community.botkit.ai\">Bot Developer Hangout</a> for getting help using Slack Developer Kit for Python or just generally bond with your fellow Slack developers.</li>\n</ul>\n</div>\n</div>\n\n\n          <div class=\"clear_both large_bottom_margin\"></div>\n        </div>\n      </div>\n      <!-- /Body Content -->\n\n    </div>\n  </div>\n\n  <footer>\n    <p class=\"light tiny align_center\">© 2019 Slack Technologies, Inc. and contributors</p>\n  </footer>\n\n  <script>\n    window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;\n    ga('create', 'UA-56978219-13', 'auto');\n    ga('send', 'pageview');\n  </script>\n  <script async src='https://www.google-analytics.com/analytics.js'></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/metadata.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n  \n  <title>&lt;no title&gt; &mdash; Slack Developer Kit for Python</title>\n\n  <!-- Google Tag Manager -->\n  <script>(function (w, d, s, l, i) {\n      w[l] = w[l] || []; w[l].push({\n        'gtm.start':\n          new Date().getTime(), event: 'gtm.js'\n      }); var f = d.getElementsByTagName(s)[0],\n        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =\n          'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);\n    })(window, document, 'script', 'dataLayer', 'GTM-KFZ5MK7');</script>\n  <!-- End Google Tag Manager -->\n  \n  <link href=\"https://a.slack-edge.com/4f227/style/rollup-slack_kit_legacy_adapters.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link href=\"https://a.slack-edge.com/3e02c0/style/rollup-api_site.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link rel=\"stylesheet\" href=\"./_static/default.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/pygments.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/docs.css\" type=\"text/css\" />\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"https://a.slack-edge.com/4f28/img/icons/favicon-32.png\" type=\"image/png\" />\n  <link rel=\"top\" title=\"Slack Developer Kit for Python 1.0.1 documentation\" href=\"index.html\" />\n\n\n\n</head>\n\n<body class=\"api light_theme\">\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-KFZ5MK7\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n  <header>\n    <a id=\"menu_toggle\" class=\"no_transition show_on_mobile\">\n      <span class=\"menu_icon\"></span>\n      <span class=\"vert_divider\"></span>\n    </a>\n    <a href=\"https://api.slack.com/\" id=\"header_logo\" class=\"api hide_on_mobile\" style=\"float:left; display: inline-block;\">\n      <img alt=\"Slack API\" src=\"https://a.slack-edge.com/3026cb/img/slack_api_logo_vogue.png\" style=\"width: 225px; padding-right: 25px; border-right: 1px solid #DDD;\" />\n    </a>\n    <span style=\"display: inline-block; padding-left: 20px; margin-top: 25px; font-weight: bold;\">\n      <a style=\"color: #555459;\" href=\"./index.html\">Slack Developer Kit for Python</a>\n    </span>\n    <div class=\"header_nav\">\n      <a href=\"https://github.com/SlackAPI/python-slackclient\" class=\"btn header_btn float_right\" data-qa=\"go_to_slack\">Go\n        to GitHub</a>\n    </div>\n  </header>\n  \n\n  <div id=\"page\">\n    <div id=\"page_contents\" class=\"clearfix\">\n      <!-- Sidebar Content -->\n      <nav id=\"api_nav\" class=\"col span_1_of_4\">\n        <div id=\"api_sections\">\n          \n          <ul>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"index.html\">Slack Developer Kit for Python</a></li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"auth.html\">Tokens &amp; Authentication</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#handling-tokens-and-other-sensitive-data\">Handling tokens and other sensitive data</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#single-workspace-apps\">Single-Workspace Apps</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#the-oauth-flow\">The OAuth flow</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"basic_usage.html\">Basic Usage</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#sending-a-message\">Sending a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#customizing-a-message-s-layout\">Customizing a message’s layout</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#replying-to-messages-and-creating-threads\">Replying to messages and creating threads</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#updating-the-content-of-a-message\">Updating the content of a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#deleting-a-message\">Deleting a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#adding-or-removing-an-emoji-reaction\">Adding or removing an emoji reaction</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-list-of-channels\">Getting a list of channels</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-channel-s-info\">Getting a channel’s info</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#joining-a-channel\">Joining a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#leaving-a-channel\">Leaving a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#get-a-list-of-team-members\">Get a list of team members</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#uploading-files\">Uploading files</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#web-api-rate-limits\">Web API Rate Limits</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"conversations.html\">Conversations API</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-direct-message-or-multi-person-direct-message\">Creating a direct message or multi-person direct message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-public-or-private-channel\">Creating a public or private channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-information-about-a-conversation\">Getting information about a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-a-list-of-conversations\">Getting a list of conversations</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#leaving-a-conversation\">Leaving a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#get-conversation-members\">Get conversation members</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"real_time_messaging.html\">Real Time Messaging (RTM)</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#connecting-to-the-rtm-api\">Connecting to the RTM API</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-start-vs-rtm-connect\">rtm.start vs rtm.connect</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-events\">RTM Events</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#sending-messages-via-the-rtm-api\">Sending messages via the RTM API</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"faq.html\">Frequently Asked Questions</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#what-even-is-product-name-and-why-should-i-care\">What even is Slack Developer Kit for Python and why should I care?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#omg-i-found-a-bug\">OMG I found a bug!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#hey-there-s-a-feature-missing\">Hey, there’s a feature missing!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#i-d-like-to-contribute-but-how\">I’d like to contribute…but how?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#how-do-i-compile-the-documentation\">How do I compile the documentation?</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"changelog.html\">Changelog</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-3-0-2018-09-11\">v1.3.0 (2018-09-11)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-1-2018-03-26\">v1.2.1 (2018-03-26)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-0-2018-03-20\">v1.2.0 (2018-03-20)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-3-2018-03-01\">v1.1.3 (2018-03-01)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-2-2018-01-31\">v1.1.2 (2018-01-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-1-2018-01-30\">v1.1.1 (2018-01-30)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-0-2017-11-21\">v1.1.0 (2017-11-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-9-2017-08-31\">v1.0.9 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-8-2017-08-31\">v1.0.8 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-7-2017-08-02\">v1.0.7 (2017-08-02)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-6-2017-06-12\">v1.0.6 (2017-06-12)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-5-2017-01-23\">v1.0.5 (2017-01-23)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-4-2016-12-15\">v1.0.4 (2016-12-15)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-3-2016-12-13\">v1.0.3 (2016-12-13)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-2-2016-09-22\">v1.0.2 (2016-09-22)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-1-2016-03-25\">v1.0.1 (2016-03-25)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-0-2016-02-28\">v1.0.0 (2016-02-28)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-18-0-2016-02-21\">v0.18.0 (2016-02-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-17-0-2016-02-15\">v0.17.0 (2016-02-15)</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"about.html\">About</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"about.html#product-name\">Slack Developer Kit for Python</a></li>\n</ul>\n</li>\n</ul>\n\n\n<div id=\"footer\">\n\n    <ul id=\"footer_nav\">\n        <li><a href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n        <li><a href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n        <li><a href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n        <li><a href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n    </ul>\n\n    <p id=\"footer_signature\">Made with <i class=\"ts_icon ts_icon_heart\"></i> by Slack<br/>and our Lovely\n        Community\n    </p>\n</div>\n          \n        </div>\n      </nav>\n      <!-- /Sidebar Content -->\n\n      <!-- Body Content -->\n      <div class=\"col span_3_of_4\">\n        <div class=\"section-title\">&lt;no title&gt;</div>\n        <div class=\"card\">\n  \n\n          <div class=\"clear_both large_bottom_margin\"></div>\n        </div>\n      </div>\n      <!-- /Body Content -->\n\n    </div>\n  </div>\n\n  <footer>\n    <p class=\"light tiny align_center\">© 2019 Slack Technologies, Inc. and contributors</p>\n  </footer>\n\n  <script>\n    window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;\n    ga('create', 'UA-56978219-13', 'auto');\n    ga('send', 'pageview');\n  </script>\n  <script async src='https://www.google-analytics.com/analytics.js'></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/objects.inv",
    "content": "# Sphinx inventory version 2\n# Project: Slack Developer Kit for Python\n# Version: 1.0\n# The remainder of this file is compressed using zlib.\nx}N0\fy\nKC\b\u000eE\u0010R\u0001!&4iqkIפc{{-\u0018Np:\u0003\u0014w!\u0005tMej\u0001;2L\u0015\u0005XjCR%V\"ix+Vyu\u001aK\n@\u0013\t,P$T\u0019\u0012\u0006Jr:\"BGOE|\u001a\u00196A\u001cHJ\u001c;\u001aM뿴t\u0001R\u0002;G$,h\u001f\u001fTOݑ\u000be\u0018?\b\u001872>\u0010@kiA[\u001b#( )_8!03\u001bq\u0003k`*ߚ\f\u001c!=h>T`\u0011jU9$\u00133Utn[B\u0018^SR\u0013. s\u0001\u00180\f`a].b9fgrQ\u0013y\u0015_k\u001c\u001e\u0017Ihu#d6\u0012o\u0005M\t"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/real_time_messaging.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n  \n  <title>Real Time Messaging (RTM) &mdash; Slack Developer Kit for Python</title>\n\n  <!-- Google Tag Manager -->\n  <script>(function (w, d, s, l, i) {\n      w[l] = w[l] || []; w[l].push({\n        'gtm.start':\n          new Date().getTime(), event: 'gtm.js'\n      }); var f = d.getElementsByTagName(s)[0],\n        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =\n          'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);\n    })(window, document, 'script', 'dataLayer', 'GTM-KFZ5MK7');</script>\n  <!-- End Google Tag Manager -->\n  \n  <link href=\"https://a.slack-edge.com/4f227/style/rollup-slack_kit_legacy_adapters.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link href=\"https://a.slack-edge.com/3e02c0/style/rollup-api_site.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link rel=\"stylesheet\" href=\"./_static/default.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/pygments.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/docs.css\" type=\"text/css\" />\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"https://a.slack-edge.com/4f28/img/icons/favicon-32.png\" type=\"image/png\" />\n  <link rel=\"top\" title=\"Slack Developer Kit for Python 1.0.1 documentation\" href=\"index.html\" />\n\n\n\n</head>\n\n<body class=\"api light_theme\">\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-KFZ5MK7\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n  <header>\n    <a id=\"menu_toggle\" class=\"no_transition show_on_mobile\">\n      <span class=\"menu_icon\"></span>\n      <span class=\"vert_divider\"></span>\n    </a>\n    <a href=\"https://api.slack.com/\" id=\"header_logo\" class=\"api hide_on_mobile\" style=\"float:left; display: inline-block;\">\n      <img alt=\"Slack API\" src=\"https://a.slack-edge.com/3026cb/img/slack_api_logo_vogue.png\" style=\"width: 225px; padding-right: 25px; border-right: 1px solid #DDD;\" />\n    </a>\n    <span style=\"display: inline-block; padding-left: 20px; margin-top: 25px; font-weight: bold;\">\n      <a style=\"color: #555459;\" href=\"./index.html\">Slack Developer Kit for Python</a>\n    </span>\n    <div class=\"header_nav\">\n      <a href=\"https://github.com/SlackAPI/python-slackclient\" class=\"btn header_btn float_right\" data-qa=\"go_to_slack\">Go\n        to GitHub</a>\n    </div>\n  </header>\n  \n\n  <div id=\"page\">\n    <div id=\"page_contents\" class=\"clearfix\">\n      <!-- Sidebar Content -->\n      <nav id=\"api_nav\" class=\"col span_1_of_4\">\n        <div id=\"api_sections\">\n          \n          <ul class=\"current\">\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"index.html\">Slack Developer Kit for Python</a></li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"auth.html\">Tokens &amp; Authentication</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#handling-tokens-and-other-sensitive-data\">Handling tokens and other sensitive data</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#single-workspace-apps\">Single-Workspace Apps</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#the-oauth-flow\">The OAuth flow</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"basic_usage.html\">Basic Usage</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#sending-a-message\">Sending a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#customizing-a-message-s-layout\">Customizing a message’s layout</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#replying-to-messages-and-creating-threads\">Replying to messages and creating threads</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#updating-the-content-of-a-message\">Updating the content of a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#deleting-a-message\">Deleting a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#adding-or-removing-an-emoji-reaction\">Adding or removing an emoji reaction</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-list-of-channels\">Getting a list of channels</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-channel-s-info\">Getting a channel’s info</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#joining-a-channel\">Joining a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#leaving-a-channel\">Leaving a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#get-a-list-of-team-members\">Get a list of team members</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#uploading-files\">Uploading files</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#web-api-rate-limits\">Web API Rate Limits</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"conversations.html\">Conversations API</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-direct-message-or-multi-person-direct-message\">Creating a direct message or multi-person direct message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-public-or-private-channel\">Creating a public or private channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-information-about-a-conversation\">Getting information about a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-a-list-of-conversations\">Getting a list of conversations</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#leaving-a-conversation\">Leaving a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#get-conversation-members\">Get conversation members</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1 current\"><a class=\"current reference internal\" href=\"#\">Real Time Messaging (RTM)</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#connecting-to-the-rtm-api\">Connecting to the RTM API</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#rtm-start-vs-rtm-connect\">rtm.start vs rtm.connect</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#rtm-events\">RTM Events</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"#sending-messages-via-the-rtm-api\">Sending messages via the RTM API</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"faq.html\">Frequently Asked Questions</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#what-even-is-product-name-and-why-should-i-care\">What even is Slack Developer Kit for Python and why should I care?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#omg-i-found-a-bug\">OMG I found a bug!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#hey-there-s-a-feature-missing\">Hey, there’s a feature missing!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#i-d-like-to-contribute-but-how\">I’d like to contribute…but how?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#how-do-i-compile-the-documentation\">How do I compile the documentation?</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"changelog.html\">Changelog</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-3-0-2018-09-11\">v1.3.0 (2018-09-11)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-1-2018-03-26\">v1.2.1 (2018-03-26)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-0-2018-03-20\">v1.2.0 (2018-03-20)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-3-2018-03-01\">v1.1.3 (2018-03-01)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-2-2018-01-31\">v1.1.2 (2018-01-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-1-2018-01-30\">v1.1.1 (2018-01-30)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-0-2017-11-21\">v1.1.0 (2017-11-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-9-2017-08-31\">v1.0.9 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-8-2017-08-31\">v1.0.8 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-7-2017-08-02\">v1.0.7 (2017-08-02)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-6-2017-06-12\">v1.0.6 (2017-06-12)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-5-2017-01-23\">v1.0.5 (2017-01-23)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-4-2016-12-15\">v1.0.4 (2016-12-15)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-3-2016-12-13\">v1.0.3 (2016-12-13)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-2-2016-09-22\">v1.0.2 (2016-09-22)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-1-2016-03-25\">v1.0.1 (2016-03-25)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-0-2016-02-28\">v1.0.0 (2016-02-28)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-18-0-2016-02-21\">v0.18.0 (2016-02-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-17-0-2016-02-15\">v0.17.0 (2016-02-15)</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"about.html\">About</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"about.html#product-name\">Slack Developer Kit for Python</a></li>\n</ul>\n</li>\n</ul>\n\n\n<div id=\"footer\">\n\n    <ul id=\"footer_nav\">\n        <li><a href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n        <li><a href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n        <li><a href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n        <li><a href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n    </ul>\n\n    <p id=\"footer_signature\">Made with <i class=\"ts_icon ts_icon_heart\"></i> by Slack<br/>and our Lovely\n        Community\n    </p>\n</div>\n          \n        </div>\n      </nav>\n      <!-- /Sidebar Content -->\n\n      <!-- Body Content -->\n      <div class=\"col span_3_of_4\">\n        <div class=\"section-title\">Real Time Messaging (RTM)</div>\n        <div class=\"card\">\n  <div class=\"section\" id=\"real-time-messaging-rtm\">\n<span id=\"real-time-messaging\"></span><h1>Real Time Messaging (RTM)<a class=\"headerlink\" href=\"#real-time-messaging-rtm\" title=\"Permalink to this headline\">¶</a></h1>\n<p>The <a class=\"reference external\" href=\"https://api.slack.com/rtm\">Real Time Messaging (RTM) API</a> is a WebSocket-based API that allows you to\nreceive events from Slack in real time and send messages as users.</p>\n<p>If you prefer events to be pushed to you instead, we recommend using the\nHTTP-based <a class=\"reference external\" href=\"https://api.slack.com/events-api\">Events API</a> instead.\nMost event types supported by the RTM API are also available\nin <a class=\"reference external\" href=\"https://api.slack.com/events/api\">the Events API</a>.</p>\n<p>See <a class=\"reference internal\" href=\"auth.html#handling-tokens\"><span class=\"std std-ref\">Tokens &amp; Authentication</span></a> for API token handling best practices.</p>\n<div class=\"section\" id=\"connecting-to-the-rtm-api\">\n<h2>Connecting to the RTM API<a class=\"headerlink\" href=\"#connecting-to-the-rtm-api\" title=\"Permalink to this headline\">¶</a></h2>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"k\">if</span> <span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">rtm_connect</span><span class=\"p\">():</span>\n  <span class=\"k\">while</span> <span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">server</span><span class=\"o\">.</span><span class=\"n\">connected</span> <span class=\"ow\">is</span> <span class=\"bp\">True</span><span class=\"p\">:</span>\n        <span class=\"k\">print</span> <span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">rtm_read</span><span class=\"p\">()</span>\n        <span class=\"n\">time</span><span class=\"o\">.</span><span class=\"n\">sleep</span><span class=\"p\">(</span><span class=\"mi\">1</span><span class=\"p\">)</span>\n<span class=\"k\">else</span><span class=\"p\">:</span>\n    <span class=\"k\">print</span> <span class=\"s2\">&quot;Connection Failed&quot;</span>\n</pre></div>\n</div>\n<p>If you connect successfully the first event received will be a hello:</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"p\">{</span>\n  <span class=\"sa\">u</span><span class=\"s1\">&#39;type&#39;</span><span class=\"p\">:</span> <span class=\"sa\">u</span><span class=\"s1\">&#39;hello&#39;</span>\n<span class=\"p\">}</span>\n</pre></div>\n</div>\n<p>If there was a problem connecting an error will be returned, including a descriptive error message:</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"p\">{</span>\n  <span class=\"sa\">u</span><span class=\"s1\">&#39;type&#39;</span><span class=\"p\">:</span> <span class=\"sa\">u</span><span class=\"s1\">&#39;error&#39;</span><span class=\"p\">,</span>\n    <span class=\"sa\">u</span><span class=\"s1\">&#39;error&#39;</span><span class=\"p\">:</span> <span class=\"p\">{</span>\n    <span class=\"sa\">u</span><span class=\"s1\">&#39;code&#39;</span><span class=\"p\">:</span> <span class=\"mi\">1</span><span class=\"p\">,</span>\n    <span class=\"sa\">u</span><span class=\"s1\">&#39;msg&#39;</span><span class=\"p\">:</span> <span class=\"sa\">u</span><span class=\"s1\">&#39;Socket URL has expired&#39;</span>\n  <span class=\"p\">}</span>\n<span class=\"p\">}</span>\n</pre></div>\n</div>\n</div>\n<div class=\"section\" id=\"rtm-start-vs-rtm-connect\">\n<h2>rtm.start vs rtm.connect<a class=\"headerlink\" href=\"#rtm-start-vs-rtm-connect\" title=\"Permalink to this headline\">¶</a></h2>\n<p>If you expect your app to be used on large teams, we recommend starting the RTM client with <cite>rtm.connect</cite> rather than the default connection method for this client, <cite>rtm.start</cite>.\n<cite>rtm.connect</cite> provides a lighter initial connection payload, without the team’s channel and user information included. You’ll need to request channel and user info via\nthe Web API separately.</p>\n<p>To do this, simply pass <cite>with_team_state=False</cite> into the <cite>rtm_connect</cite> call, like so:</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"k\">if</span> <span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">rtm_connect</span><span class=\"p\">(</span><span class=\"n\">with_team_state</span><span class=\"o\">=</span><span class=\"bp\">False</span><span class=\"p\">):</span>\n    <span class=\"k\">while</span> <span class=\"bp\">True</span><span class=\"p\">:</span>\n        <span class=\"k\">print</span> <span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">rtm_read</span><span class=\"p\">()</span>\n        <span class=\"n\">time</span><span class=\"o\">.</span><span class=\"n\">sleep</span><span class=\"p\">(</span><span class=\"mi\">1</span><span class=\"p\">)</span>\n<span class=\"k\">else</span><span class=\"p\">:</span>\n    <span class=\"k\">print</span> <span class=\"s2\">&quot;Connection Failed&quot;</span>\n</pre></div>\n</div>\n<p>Passing <cite>auto_reconnect=True</cite> will tell the websocket client to automatically reconnect if the connection gets dropped.</p>\n<p>See the <a class=\"reference external\" href=\"https://api.slack.com/methods/rtm.start\">rtm.start docs</a> and the <a class=\"reference external\" href=\"https://api.slack.com/methods/rtm.connect\">rtm.connect docs</a>\nfor more details.</p>\n</div>\n<div class=\"section\" id=\"rtm-events\">\n<h2>RTM Events<a class=\"headerlink\" href=\"#rtm-events\" title=\"Permalink to this headline\">¶</a></h2>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"p\">{</span>\n  <span class=\"sa\">u</span><span class=\"s1\">&#39;type&#39;</span><span class=\"p\">:</span> <span class=\"sa\">u</span><span class=\"s1\">&#39;message&#39;</span><span class=\"p\">,</span>\n  <span class=\"sa\">u</span><span class=\"s1\">&#39;ts&#39;</span><span class=\"p\">:</span> <span class=\"sa\">u</span><span class=\"s1\">&#39;1358878749.000002&#39;</span><span class=\"p\">,</span>\n  <span class=\"sa\">u</span><span class=\"s1\">&#39;user&#39;</span><span class=\"p\">:</span> <span class=\"sa\">u</span><span class=\"s1\">&#39;U023BECGF&#39;</span><span class=\"p\">,</span>\n  <span class=\"sa\">u</span><span class=\"s1\">&#39;text&#39;</span><span class=\"p\">:</span> <span class=\"sa\">u</span><span class=\"s1\">&#39;Hello&#39;</span>\n<span class=\"p\">}</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/rtm#events\">RTM Events</a> for a complete list of events.</p>\n</div>\n<div class=\"section\" id=\"sending-messages-via-the-rtm-api\">\n<h2>Sending messages via the RTM API<a class=\"headerlink\" href=\"#sending-messages-via-the-rtm-api\" title=\"Permalink to this headline\">¶</a></h2>\n<p>You can send a message to Slack by sending JSON over the websocket connection.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">rtm_send_message</span><span class=\"p\">(</span><span class=\"s2\">&quot;welcome-test&quot;</span><span class=\"p\">,</span> <span class=\"s2\">&quot;test&quot;</span><span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>You can send a message to a private group or direct message channel in the same\nway, but using a Group ID (<code class=\"docutils literal notranslate\"><span class=\"pre\">C024BE91L</span></code>) or DM channel ID (<code class=\"docutils literal notranslate\"><span class=\"pre\">D024BE91L</span></code>).</p>\n<p>You can send a message in reply to a thread using the <code class=\"docutils literal notranslate\"><span class=\"pre\">thread</span></code> argument, and\noptionally broadcast that message back to the channel by setting\n<code class=\"docutils literal notranslate\"><span class=\"pre\">reply_broadcast</span></code> to <code class=\"docutils literal notranslate\"><span class=\"pre\">True</span></code>.</p>\n<div class=\"highlight-python notranslate\"><div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">slackclient</span> <span class=\"kn\">import</span> <span class=\"n\">SlackClient</span>\n\n<span class=\"n\">slack_token</span> <span class=\"o\">=</span> <span class=\"n\">os</span><span class=\"o\">.</span><span class=\"n\">environ</span><span class=\"p\">[</span><span class=\"s2\">&quot;SLACK_API_TOKEN&quot;</span><span class=\"p\">]</span>\n<span class=\"n\">sc</span> <span class=\"o\">=</span> <span class=\"n\">SlackClient</span><span class=\"p\">(</span><span class=\"n\">slack_token</span><span class=\"p\">)</span>\n\n<span class=\"n\">sc</span><span class=\"o\">.</span><span class=\"n\">rtm_send_message</span><span class=\"p\">(</span><span class=\"s2\">&quot;welcome-test&quot;</span><span class=\"p\">,</span> <span class=\"s2\">&quot;test&quot;</span><span class=\"p\">,</span> <span class=\"s2\">&quot;1482960137.003543&quot;</span><span class=\"p\">,</span> <span class=\"bp\">True</span><span class=\"p\">)</span>\n</pre></div>\n</div>\n<p>See <a class=\"reference external\" href=\"https://api.slack.com/docs/message-threading#threads_party\">Threading messages</a>\nfor more details on using threads.</p>\n<p>The RTM API only supports posting messages with <a class=\"reference external\" href=\"https://api.slack.com/docs/message-formatting\">basic formatting</a>.\nIt does not support attachments or other message formatting modes.</p>\n<blockquote>\n<div>To post a more complex message as a user, see <a class=\"reference internal\" href=\"basic_usage.html#web-api-examples\"><span class=\"std std-ref\">Web API usage</span></a>.</div></blockquote>\n</div>\n</div>\n\n\n          <div class=\"clear_both large_bottom_margin\"></div>\n        </div>\n      </div>\n      <!-- /Body Content -->\n\n    </div>\n  </div>\n\n  <footer>\n    <p class=\"light tiny align_center\">© 2019 Slack Technologies, Inc. and contributors</p>\n  </footer>\n\n  <script>\n    window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;\n    ga('create', 'UA-56978219-13', 'auto');\n    ga('send', 'pageview');\n  </script>\n  <script async src='https://www.google-analytics.com/analytics.js'></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/search.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n  \n  <title>Search &mdash; Slack Developer Kit for Python</title>\n\n  <!-- Google Tag Manager -->\n  <script>(function (w, d, s, l, i) {\n      w[l] = w[l] || []; w[l].push({\n        'gtm.start':\n          new Date().getTime(), event: 'gtm.js'\n      }); var f = d.getElementsByTagName(s)[0],\n        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =\n          'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);\n    })(window, document, 'script', 'dataLayer', 'GTM-KFZ5MK7');</script>\n  <!-- End Google Tag Manager -->\n  \n  <link href=\"https://a.slack-edge.com/4f227/style/rollup-slack_kit_legacy_adapters.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link href=\"https://a.slack-edge.com/3e02c0/style/rollup-api_site.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link rel=\"stylesheet\" href=\"./_static/default.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/pygments.css\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"./_static/docs.css\" type=\"text/css\" />\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"https://a.slack-edge.com/4f28/img/icons/favicon-32.png\" type=\"image/png\" />\n  <link rel=\"top\" title=\"Slack Developer Kit for Python 1.0.1 documentation\" href=\"index.html\" />\n\n\n\n</head>\n\n<body class=\"api light_theme\">\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-KFZ5MK7\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n  <header>\n    <a id=\"menu_toggle\" class=\"no_transition show_on_mobile\">\n      <span class=\"menu_icon\"></span>\n      <span class=\"vert_divider\"></span>\n    </a>\n    <a href=\"https://api.slack.com/\" id=\"header_logo\" class=\"api hide_on_mobile\" style=\"float:left; display: inline-block;\">\n      <img alt=\"Slack API\" src=\"https://a.slack-edge.com/3026cb/img/slack_api_logo_vogue.png\" style=\"width: 225px; padding-right: 25px; border-right: 1px solid #DDD;\" />\n    </a>\n    <span style=\"display: inline-block; padding-left: 20px; margin-top: 25px; font-weight: bold;\">\n      <a style=\"color: #555459;\" href=\"./index.html\">Slack Developer Kit for Python</a>\n    </span>\n    <div class=\"header_nav\">\n      <a href=\"https://github.com/SlackAPI/python-slackclient\" class=\"btn header_btn float_right\" data-qa=\"go_to_slack\">Go\n        to GitHub</a>\n    </div>\n  </header>\n  \n\n  <div id=\"page\">\n    <div id=\"page_contents\" class=\"clearfix\">\n      <!-- Sidebar Content -->\n      <nav id=\"api_nav\" class=\"col span_1_of_4\">\n        <div id=\"api_sections\">\n          \n          <ul>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"index.html\">Slack Developer Kit for Python</a></li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"auth.html\">Tokens &amp; Authentication</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#handling-tokens-and-other-sensitive-data\">Handling tokens and other sensitive data</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#single-workspace-apps\">Single-Workspace Apps</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"auth.html#the-oauth-flow\">The OAuth flow</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"basic_usage.html\">Basic Usage</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#sending-a-message\">Sending a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#customizing-a-message-s-layout\">Customizing a message’s layout</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#replying-to-messages-and-creating-threads\">Replying to messages and creating threads</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#updating-the-content-of-a-message\">Updating the content of a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#deleting-a-message\">Deleting a message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#adding-or-removing-an-emoji-reaction\">Adding or removing an emoji reaction</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-list-of-channels\">Getting a list of channels</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#getting-a-channel-s-info\">Getting a channel’s info</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#joining-a-channel\">Joining a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#leaving-a-channel\">Leaving a channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#get-a-list-of-team-members\">Get a list of team members</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#uploading-files\">Uploading files</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"basic_usage.html#web-api-rate-limits\">Web API Rate Limits</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"conversations.html\">Conversations API</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-direct-message-or-multi-person-direct-message\">Creating a direct message or multi-person direct message</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#creating-a-public-or-private-channel\">Creating a public or private channel</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-information-about-a-conversation\">Getting information about a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#getting-a-list-of-conversations\">Getting a list of conversations</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#leaving-a-conversation\">Leaving a conversation</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"conversations.html#get-conversation-members\">Get conversation members</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"real_time_messaging.html\">Real Time Messaging (RTM)</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#connecting-to-the-rtm-api\">Connecting to the RTM API</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-start-vs-rtm-connect\">rtm.start vs rtm.connect</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#rtm-events\">RTM Events</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"real_time_messaging.html#sending-messages-via-the-rtm-api\">Sending messages via the RTM API</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"faq.html\">Frequently Asked Questions</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#what-even-is-product-name-and-why-should-i-care\">What even is Slack Developer Kit for Python and why should I care?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#omg-i-found-a-bug\">OMG I found a bug!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#hey-there-s-a-feature-missing\">Hey, there’s a feature missing!</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#i-d-like-to-contribute-but-how\">I’d like to contribute…but how?</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"faq.html#how-do-i-compile-the-documentation\">How do I compile the documentation?</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"changelog.html\">Changelog</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-3-0-2018-09-11\">v1.3.0 (2018-09-11)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-1-2018-03-26\">v1.2.1 (2018-03-26)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-2-0-2018-03-20\">v1.2.0 (2018-03-20)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-3-2018-03-01\">v1.1.3 (2018-03-01)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-2-2018-01-31\">v1.1.2 (2018-01-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-1-2018-01-30\">v1.1.1 (2018-01-30)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-1-0-2017-11-21\">v1.1.0 (2017-11-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-9-2017-08-31\">v1.0.9 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-8-2017-08-31\">v1.0.8 (2017-08-31)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-7-2017-08-02\">v1.0.7 (2017-08-02)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-6-2017-06-12\">v1.0.6 (2017-06-12)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-5-2017-01-23\">v1.0.5 (2017-01-23)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-4-2016-12-15\">v1.0.4 (2016-12-15)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-3-2016-12-13\">v1.0.3 (2016-12-13)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-2-2016-09-22\">v1.0.2 (2016-09-22)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-1-2016-03-25\">v1.0.1 (2016-03-25)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v1-0-0-2016-02-28\">v1.0.0 (2016-02-28)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-18-0-2016-02-21\">v0.18.0 (2016-02-21)</a></li>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"changelog.html#v0-17-0-2016-02-15\">v0.17.0 (2016-02-15)</a></li>\n</ul>\n</li>\n<li class=\"toctree-l1\"><a class=\"reference internal\" href=\"about.html\">About</a><ul>\n<li class=\"toctree-l2\"><a class=\"reference internal\" href=\"about.html#product-name\">Slack Developer Kit for Python</a></li>\n</ul>\n</li>\n</ul>\n\n\n<div id=\"footer\">\n\n    <ul id=\"footer_nav\">\n        <li><a href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n        <li><a href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n        <li><a href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n        <li><a href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n    </ul>\n\n    <p id=\"footer_signature\">Made with <i class=\"ts_icon ts_icon_heart\"></i> by Slack<br/>and our Lovely\n        Community\n    </p>\n</div>\n          \n        </div>\n      </nav>\n      <!-- /Sidebar Content -->\n\n      <!-- Body Content -->\n      <div class=\"col span_3_of_4\">\n        <div class=\"section-title\">Search</div>\n        <div class=\"card\">\n  <h1 id=\"search-documentation\">Search</h1>\n  <div id=\"fallback\" class=\"admonition warning\">\n  <script type=\"text/javascript\">$('#fallback').hide();</script>\n  <p>\n    Please activate JavaScript to enable the search\n    functionality.\n  </p>\n  </div>\n  <p>\n    From here you can search these documents. Enter your search\n    words into the box below and click \"search\". Note that the search\n    function will automatically search for all of the words. Pages\n    containing fewer words won't appear in the result list.\n  </p>\n  <form action=\"\" method=\"get\">\n    <input type=\"text\" name=\"q\" value=\"\" />\n    <input type=\"submit\" value=\"search\" />\n    <span id=\"search-progress\" style=\"padding-left: 10px\"></span>\n  </form>\n  \n  <div id=\"search-results\">\n  \n  </div>\n\n          <div class=\"clear_both large_bottom_margin\"></div>\n        </div>\n      </div>\n      <!-- /Body Content -->\n\n    </div>\n  </div>\n\n  <footer>\n    <p class=\"light tiny align_center\">© 2019 Slack Technologies, Inc. and contributors</p>\n  </footer>\n\n  <script>\n    window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;\n    ga('create', 'UA-56978219-13', 'auto');\n    ga('send', 'pageview');\n  </script>\n  <script async src='https://www.google-analytics.com/analytics.js'></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/searchindex.js",
    "content": "Search.setIndex({docnames:[\"about\",\"auth\",\"basic_usage\",\"changelog\",\"conversations\",\"faq\",\"index\",\"metadata\",\"real_time_messaging\"],envversion:{\"sphinx.domains.c\":1,\"sphinx.domains.changeset\":1,\"sphinx.domains.cpp\":1,\"sphinx.domains.javascript\":1,\"sphinx.domains.math\":2,\"sphinx.domains.python\":1,\"sphinx.domains.rst\":1,\"sphinx.domains.std\":1,sphinx:54},filenames:[\"about.rst\",\"auth.rst\",\"basic_usage.rst\",\"changelog.rst\",\"conversations.rst\",\"faq.rst\",\"index.rst\",\"metadata.rst\",\"real_time_messaging.rst\"],objects:{},objnames:{},objtypes:{},terms:{\"0cb4bcd6e887b428e27e8059b6278b86ee661aaa\":3,\"1046cc2375a85a22e94573e2aad954ba7287c886\":3,\"1920s\":2,\"7d01515cebc80918a29100b0e4793790eb83e7b9\":3,\"boolean\":2,\"break\":[3,5],\"case\":5,\"class\":3,\"default\":[2,3,4,8],\"function\":3,\"import\":[1,2,3,4,8],\"int\":2,\"long\":2,\"new\":[1,2,3,4,5],\"public\":1,\"return\":[1,2,3,4,8],\"short\":[2,3],\"super\":5,\"switch\":3,\"true\":[2,3,4,8],\"try\":1,\"while\":[2,5,8],Added:3,But:5,For:[1,2],IDs:4,IMs:2,One:[1,2],The:[2,4,6,8],Then:1,There:[2,5],Use:[3,4,6],__eq__:3,__name__:1,aadd:3,abc:1,abil:3,abl:1,about:[2,3,5],abov:1,acair:3,accept:1,access:[0,1,3,4,5],access_token:1,accessori:2,accid:[2,4],action:[1,3],actual:2,adapt:5,add:[1,2,3,5],added:[3,5],addit:[1,2,4],after:[1,2],agre:1,agreement:0,aim:5,all:[2,4,5],allow:[2,3,6,8],alreadi:[2,4],already_in_channel:2,also:[2,8],alt_text:2,alwai:[5,6],ambiti:5,ani:2,anoth:[2,3],aoberoi:3,api:[0,1,3,5],api_cal:[1,2,3,4],app:[0,2,4,5,6,8],appear:2,applic:[1,2],appropri:5,approv:1,archiv:2,arg:[1,3],argument:[2,8],around:5,articl:2,assign:2,assist:6,attach:[2,8],attribut:2,auth:[1,3],auth_cod:1,auth_respons:1,authent:[2,3,8],author:1,authorship:2,auto_reconnect:[3,8],automat:[3,8],avail:[1,2,8],averag:2,avoid:1,axe:2,back:8,bad:3,base:[4,8],basic:[5,8],becom:2,been:[1,2],befor:[2,3],begin:[1,4],begin_auth:1,behalf:1,below:2,benoitlavign:3,best:[1,2,6,8],between:2,bite:5,block:2,bond:6,bot:[1,2,5,6],bot_access_token:1,both:2,box:2,breath:5,broadcast:[2,8],bug:[3,6],build:[0,1,2,3,5,6],builder:2,burst:2,busi:[2,4],button:[1,2],c024be91l:8,c0xxxxxx:[2,4],c0xxxxxxx:[2,4],c0xxxxxxy:2,c3ukjtqac:2,cach:3,call:[1,2,3,4,8],can:[1,2,3,5,6,8],cannot:2,caus:3,cclauss:3,chang:[1,2,3],changelog:5,channel:[3,8],channnel:3,chat:2,check:2,clean:3,click:1,client:[1,3,8],client_id:1,client_secret:1,clone:6,code:[0,1,3,5,6,8],com:[1,2,3,5,6],combin:2,command:1,comment:[2,3],commit:3,commonli:[2,5],comparison:3,compat:3,complet:[1,2,5,8],complex:[2,8],concret:5,conduct:0,config:1,configur:3,connect:3,consider:2,contain:[1,2],content:5,context:[1,2],continu:[2,5],contribut:0,contributor:[0,3,5],convers:2,copyright:3,could:[1,5],counter:3,coupl:[3,5],cours:6,coverag:5,creat:3,createchild:3,credenti:1,custom:[1,3,6],d024be91l:8,d45285d2f1025899dcd65e259624ee73771f94bb:3,dai:5,danni:2,databas:1,deal:2,decod:3,deep:5,def:[1,2],defin:1,delai:2,demonstr:5,depend:[1,4],descend:2,describ:3,descript:8,design:2,detail:8,differ:[1,2],direct:[1,2,8],directli:[2,6],discuss:5,doc:[3,4,5,8],document:[1,2,3],doe:[5,8],doesn:3,doing:6,don:[1,5],done:[1,5],door:2,down:6,drop:8,duplic:3,easier:2,edit:5,effect:1,elif:2,els:8,email:3,empti:1,encod:3,encount:4,endpoint:[1,2],enforc:3,ensur:5,entir:1,environ:[1,2,3,4,5,8],environment:1,ephemer:2,error:[2,3,8],event:5,exact:4,exampl:[1,2],exce:2,excel:5,except:[2,3],exchang:2,excit:[2,5],exclud:2,exclude_archiv:2,exist:4,expect:[5,8],experi:1,expir:[3,8],explicitli:1,expos:3,f7bb8889580cc34471ba1ddc05afc34d1a5efa23:3,fail:[2,3,8],fals:[2,8],far:2,featur:[1,3,6],feedback:5,feel:[2,5],fellow:6,felt:2,fetch:2,few:1,field:2,file:[3,5],file_com:2,file_cont:2,find:2,finish:[2,4],finish_auth:1,first:[5,8],fix:[2,3,5],flask:1,flexibl:6,folder:5,follow:[2,5,6],forget:[1,5],fork:2,format:[1,2,3,8],found:1,from:[0,1,2,3,4,5,8],full:2,fun:2,futur:5,gener:[1,5,6],german:2,get:[1,3,8],git:6,github:[3,5,6],given:5,global:1,good:5,grant:1,great:5,greater:2,group:[2,3,4,8],guest:2,guidelin:[3,5],had:[2,4],hand:5,handl:[2,3,8],hangout:6,hard:6,hardcod:1,harlowja:3,has:[1,4,8],haunt:2,have:[2,5],header:[2,3],hello:[2,8],help:5,here:[1,2,5,6],hole:2,hop:2,hotel:2,how:[2,4],howev:2,href:1,http:[1,2,3,5,6,8],hub:2,ids:1,imag:2,image_url:2,implement:[1,6],importantli:5,improv:3,includ:[1,2,3,4,5,8],incom:1,incorpor:5,incorrectli:3,increas:5,index:3,indic:2,info:[3,4,8],inform:[1,2,8],initi:[1,4,8],insid:5,instal:1,instead:[1,2,3,8],integr:[2,3,6],interact:2,interfac:4,intern:3,introduc:3,invit:2,is_priv:4,issu:[3,5,6],item:[2,5],its:[3,5],itself:2,jayalan:3,join:[3,4],jpeg:2,json:[1,2,3,8],just:[2,4,5,6],kamushaden:3,keep:1,kei:[1,2],kelvintaywl:3,kind:[1,2],kit:2,know:[1,5],kt5356:3,larg:[3,8],later:1,left:2,let:[0,1,2,5],level:1,leverag:6,licens:[0,3],lighter:8,like:[2,3,4,8],limit:3,linear:2,link:[1,3],lint:3,list:[3,8],liter:2,live:3,load:3,local:3,log:[1,3],login:3,longer:2,look:[2,5],love:5,mai:[2,3,4],maintain:0,make:[1,2,5],mani:[2,5],mayb:[2,4],mean:2,mechan:5,mention:1,messag:3,method:[1,2,4,8],might:2,mode:8,modular:2,more:[2,4,5,8],most:[2,8],move:3,mpdm:3,mpim:[3,4],mrkdwn:2,msg:8,much:5,must:[1,2],myapp:1,myprivatechannel:4,name:[2,3,4],nearli:2,need:[1,2,5,8],nefari:1,never:1,newest:4,node:1,non:3,note:[2,4,5],now:3,number:2,oauth_scop:1,object:[2,3],off:2,onc:[1,2],one:[1,2,4,5],onli:[2,4,8],open:[2,3,4],option:[2,8],order:1,origin:2,other:[3,5,8],our:[1,4,5,6],out:[1,2],over:[2,3,8],overlook:2,own:2,packag:[2,3],page:[1,2],param:[1,3],paramet:[2,3,4],parent:[2,3],part:5,parti:6,particularli:5,pass:[1,2,3,4,8],payload:8,peopl:2,pep:3,per:2,perform:1,period:2,permiss:1,persist:1,pexel:2,photo:2,ping:3,pip:6,place:[2,4],platform:[0,5],playground:2,pleas:[1,5],png:2,point:[2,3],poop:5,possibl:[1,6],post:[1,2,8],post_instal:1,postephemer:2,postmessag:2,practic:[2,8],pre_instal:1,prefer:[6,8],prevent:3,primari:2,print:[2,8],privat:[3,8],private_channel:4,problem:8,program:5,progress:4,project:[5,6],properli:3,properti:[2,3],propos:5,prototyp:2,proudli:0,provid:[1,2,4,5,8],proxi:3,public_channel:4,publish:1,pull:[1,5,6],purpos:2,push:8,pypi:6,python:[1,2,3],queri:1,quickli:[2,6],rate:3,rather:[3,5,8],read:[1,2],readm:3,rebuild:5,receiv:8,recommend:[1,5,6,8],reconnect:[3,8],redirect:1,refer:2,reflect:2,refresh:3,refus:1,regular:2,reinstal:1,releg:2,remov:3,repli:[3,8],reply_broadcast:[2,8],report:6,repositori:1,request:[1,2,3,4,5,6,8],requir:[1,2,3],respond:2,respons:[1,2,3],responseparseerror:3,restructuredtext:5,result:[2,3],resum:4,retri:2,retriev:1,review:[1,2],roach:3,room:2,rout:1,rowdi:2,rst:[3,5],rtm:3,rtm_connect:[3,8],rtm_read:8,rtm_send_messag:[3,8],run:[5,6],runtim:1,safe:1,sai:2,same:[1,2,4,8],sampart:3,sampl:5,save:[1,2],schlueter:3,scope:[1,2],search:3,second:2,secret:1,section:[1,2,5],see:[1,2,4,8],semver:3,send_messag:3,send_slack_messag:2,sent:2,separ:8,server:[1,3,8],servic:6,set:[1,2,3,4,8],setup:1,share:[1,4],should:1,show:2,sign:1,simpl:2,simpler:2,simpli:[2,8],sinc:1,singl:2,slack:[1,2,3,4,8],slack_api_token:[2,4,8],slack_bot_scop:1,slack_bot_token:1,slack_client_id:1,slack_client_secret:1,slack_token:[2,4,8],slack_user_token:1,slackapi:[3,5,6],slackclient:[1,2,3,4,6,8],slackclienterror:3,slash:1,sleep:[2,8],slightli:2,social:2,socket:8,some:[2,3],someth:5,sometim:2,sorri:3,sourc:6,special:2,specif:[1,2],specifi:2,sphinx:5,src:5,star:2,start:[1,2,5],stasfilin:3,state:2,statu:2,step:5,store:1,str:2,string:[1,3,4],stuck:[2,6],submit:5,subsequ:4,succeed:[1,2],success:1,successfulli:[2,8],support:[3,8],sure:[2,5],tada:2,take:[1,2,5],team:[0,1,3,6,8],techniqu:1,tell:[2,3,8],test:[1,2,3,5,8],text:[2,8],than:[2,3,8],thank:3,thei:[2,3,5],them:[1,2],thi:[1,2,4,5,8],thing:[2,3,4,5,6],think:5,thinking_very_much:2,third:6,those:2,thought:5,thread:[3,8],thread_t:2,through:6,thumbsup:2,tidi:3,time:2,timelin:2,timeout:3,timestamp:2,timfeirg:3,titl:2,togeth:2,token:[2,3,4,8],too:2,tool:[0,2],top:1,torrenc:2,total:5,tox:[3,5],traceback:3,tracker:[5,6],travi:3,two:[1,5],txt:6,type:[2,3,4,8],typo:[2,3],u023becgf:8,u0xxxxxxx:2,u2345678901:4,u3456789012:4,unifi:4,uniqu:2,unit:5,until:2,unus:3,updat:[3,5],upon:1,url:[1,3,8],urllib:3,usag:[1,8],use:[1,2,3,5],used:[1,2,8],user:[1,2,3,4,5,8],using:[1,3,5,6,8],valid:1,variabl:1,varieti:1,veri:2,verif:1,version:3,virtualenv:5,visibl:2,visit:6,vote:2,vscode:3,w1234567890:4,wai:[1,2,6,8],wait:[2,5],want:[1,2,5],web:[0,1,3,5,8],webhook:1,websocket:[3,8],welcom:8,well:[1,5],what:[1,2,4],when:[1,2,3],where:[1,2,3],whether:[2,6],which:[1,2,3],whole:2,with_team_st:8,within:[1,5],without:8,won:2,word:2,work:[2,3,4,5,6],workflow:6,workspac:4,wrapper:[2,5],write:5,written:5,xoxb:1,yield:1,you:[0,1,2,3,4,5,6,8],your:[0,1,2,4,5,6,8],yourself:5},titles:[\"About\",\"Tokens &amp; Authentication\",\"Basic Usage\",\"Changelog\",\"Conversations API\",\"Frequently Asked Questions\",\"Slack Developer Kit for Python\",\"&lt;no title&gt;\",\"Real Time Messaging (RTM)\"],titleterms:{\"public\":4,Adding:2,The:1,about:[0,4],api:[2,4,8],app:1,ask:5,authent:1,basic:2,bug:5,care:5,changelog:3,channel:[2,4],compil:5,connect:8,content:2,contribut:5,convers:4,creat:[2,4],custom:2,data:1,delet:2,develop:[0,5,6],direct:4,document:5,emoji:2,even:5,event:8,featur:5,file:2,flow:1,found:5,frequent:5,get:[2,4,6],handl:1,hei:5,help:6,how:5,info:2,inform:4,instal:6,join:2,kit:[0,5,6],layout:2,leav:[2,4],like:5,limit:2,list:[2,4],member:[2,4],messag:[2,4,8],miss:5,multi:4,oauth:1,omg:5,other:1,person:4,privat:4,python:[0,5,6],question:5,rate:2,reaction:2,real:8,remov:2,repli:2,requir:6,rtm:8,send:[2,8],sensit:1,should:5,singl:1,slack:[0,5,6],start:8,team:2,thread:2,time:8,token:1,updat:2,upload:2,usag:2,via:8,web:2,what:5,why:5,workspac:1}})"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore",
    "content": "_build\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = ../docs/\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_paper_size=a4\nPAPEROPT_letter = -D latex_paper_size=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n# the i18n builder cannot share the environment and doctrees with the others\nI18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n\n.PHONY: help\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html       to make standalone HTML files\"\n\t@echo \"  dirhtml    to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml to make a single large HTML file\"\n\t@echo \"  pickle     to make pickle files\"\n\t@echo \"  json       to make JSON files\"\n\t@echo \"  htmlhelp   to make HTML files and a HTML help project\"\n\t@echo \"  qthelp     to make HTML files and a qthelp project\"\n\t@echo \"  applehelp  to make an Apple Help Book\"\n\t@echo \"  devhelp    to make HTML files and a Devhelp project\"\n\t@echo \"  epub       to make an epub\"\n\t@echo \"  epub3      to make an epub3\"\n\t@echo \"  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf   to make LaTeX files and run them through pdflatex\"\n\t@echo \"  latexpdfja to make LaTeX files and run them through platex/dvipdfmx\"\n\t@echo \"  text       to make text files\"\n\t@echo \"  man        to make manual pages\"\n\t@echo \"  texinfo    to make Texinfo files\"\n\t@echo \"  info       to make Texinfo files and run them through makeinfo\"\n\t@echo \"  gettext    to make PO message catalogs\"\n\t@echo \"  changes    to make an overview of all changed/added/deprecated items\"\n\t@echo \"  xml        to make Docutils-native XML files\"\n\t@echo \"  pseudoxml  to make pseudoxml-XML files for display purposes\"\n\t@echo \"  linkcheck  to check all external links for integrity\"\n\t@echo \"  doctest    to run all doctests embedded in the documentation (if enabled)\"\n\t@echo \"  coverage   to run coverage check of the documentation (if enabled)\"\n\t@echo \"  dummy      to check syntax errors of document sources\"\n\n.PHONY: clean\nclean:\n\trm -rf $(BUILDDIR)/*\n\n.PHONY: html\nhtml:\n\t$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\n.PHONY: dirhtml\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\n.PHONY: singlehtml\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\n.PHONY: pickle\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\n.PHONY: json\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\n.PHONY: htmlhelp\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\n.PHONY: qthelp\nqthelp:\n\t$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp\n\t@echo\n\t@echo \"Build finished; now you can run \"qcollectiongenerator\" with the\" \\\n\t      \".qhcp project file in $(BUILDDIR)/qthelp, like this:\"\n\t@echo \"# qcollectiongenerator $(BUILDDIR)/qthelp/python-slackclient.qhcp\"\n\t@echo \"To view the help file:\"\n\t@echo \"# assistant -collectionFile $(BUILDDIR)/qthelp/python-slackclient.qhc\"\n\n.PHONY: applehelp\napplehelp:\n\t$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp\n\t@echo\n\t@echo \"Build finished. The help book is in $(BUILDDIR)/applehelp.\"\n\t@echo \"N.B. You won't be able to view it unless you put it in\" \\\n\t      \"~/Library/Documentation/Help or install it in your application\" \\\n\t      \"bundle.\"\n\n.PHONY: devhelp\ndevhelp:\n\t$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp\n\t@echo\n\t@echo \"Build finished.\"\n\t@echo \"To view the help file:\"\n\t@echo \"# mkdir -p $$HOME/.local/share/devhelp/python-slackclient\"\n\t@echo \"# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-slackclient\"\n\t@echo \"# devhelp\"\n\n.PHONY: epub\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\n.PHONY: epub3\nepub3:\n\t$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3\n\t@echo\n\t@echo \"Build finished. The epub3 file is in $(BUILDDIR)/epub3.\"\n\n.PHONY: latex\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\n.PHONY: latexpdf\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\n.PHONY: latexpdfja\nlatexpdfja:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through platex and dvipdfmx...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\n.PHONY: text\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\n.PHONY: man\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\n.PHONY: texinfo\ntexinfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo\n\t@echo \"Build finished. The Texinfo files are in $(BUILDDIR)/texinfo.\"\n\t@echo \"Run \\`make' in that directory to run these through makeinfo\" \\\n\t      \"(use \\`make info' here to do that automatically).\"\n\n.PHONY: info\ninfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo \"Running Texinfo files through makeinfo...\"\n\tmake -C $(BUILDDIR)/texinfo info\n\t@echo \"makeinfo finished; the Info files are in $(BUILDDIR)/texinfo.\"\n\n.PHONY: gettext\ngettext:\n\t$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale\n\t@echo\n\t@echo \"Build finished. The message catalogs are in $(BUILDDIR)/locale.\"\n\n.PHONY: changes\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\n.PHONY: linkcheck\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\n.PHONY: doctest\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n\n.PHONY: coverage\ncoverage:\n\t$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage\n\t@echo \"Testing of coverage in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/coverage/python.txt.\"\n\n.PHONY: xml\nxml:\n\t$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml\n\t@echo\n\t@echo \"Build finished. The XML files are in $(BUILDDIR)/xml.\"\n\n.PHONY: pseudoxml\npseudoxml:\n\t$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml\n\t@echo\n\t@echo \"Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml.\"\n\n.PHONY: dummy\ndummy:\n\t$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy\n\t@echo\n\t@echo \"Build finished. Dummy builder generates no files.\"\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# python-slackclient documentation build configuration file, created by\n# sphinx-quickstart on Mon Jun 27 17:36:09 2016.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os\nimport sys\nsys.path.insert(0, os.path.abspath('../'))\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.coverage',\n]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['../../_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The encoding of source files.\n#\n# source_encoding = 'utf-8-sig'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'Slack Developer Kit for Python'\ncopyright = u'2015–2016 Slack Technologies, Inc. and contributors'\nauthor = u'Slack Technologies, Inc. and contributors'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'1.0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'1.0.1'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = None\n\n# There are two options for replacing |today|: either, you set today to some\n# non-false value, then it is used:\n#\n# today = ''\n#\n# Else, today_fmt is used as the format for a strftime call.\n#\n# today_fmt = '%B %d, %Y'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n# The reST default role (used for this markup: `text`) to use for all\n# documents.\n#\n# default_role = None\n\n# If true, '()' will be appended to :func: etc. cross-reference text.\n#\n# add_function_parentheses = True\n\n# If true, the current module name will be prepended to all description\n# unit titles (such as .. function::).\n#\n# add_module_names = True\n\n# If true, sectionauthor and moduleauthor directives will be shown in the\n# output. They are ignored by default.\n#\n# show_authors = False\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'emacs'\n\n# A list of ignored prefixes for module index sorting.\n# modindex_common_prefix = []\n\n# If true, keep warnings as \"system message\" paragraphs in the built documents.\n# keep_warnings = False\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = \"slack\"\nhtml_theme_path = [\"../../_themes\", ]\n\nhighlight_language = \"python\"\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom themes here, relative to this directory.\n# html_theme_path = []\n\n# The name for this set of Sphinx documents.\n# \"<project> v<release> documentation\" by default.\n#\n# html_title = u'python-slackclient v1.0.1'\n\n# A shorter title for the navigation bar.  Default is the same as html_title.\n#\n# html_short_title = None\n\n# The name of an image file (relative to this directory) to place at the top\n# of the sidebar.\n#\n# html_logo = None\n\n# The name of an image file (relative to this directory) to use as a favicon of\n# the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32\n# pixels large.\n#\n# html_favicon = None\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['static']\n\nhtml_context = {\n    'css_files': ['static/pygments.css'],\n}\n\n# Add any extra paths that contain custom files (such as robots.txt or\n# .htaccess) here, relative to this directory. These files are copied\n# directly to the root of the documentation.\n#\n# html_extra_path = []\n\n# If not None, a 'Last updated on:' timestamp is inserted at every page\n# bottom, using the given strftime format.\n# The empty string is equivalent to '%b %d, %Y'.\n#\n# html_last_updated_fmt = None\n\n# If true, SmartyPants will be used to convert quotes and dashes to\n# typographically correct entities.\n#\n# html_use_smartypants = True\n\n# Custom sidebar templates, maps document names to template names.\n#\n# html_sidebars = {}\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n#\n# html_additional_pages = {}\n\n# If false, no module index is generated.\n#\n# html_domain_indices = True\n\n# If false, no index is generated.\n#\n# html_use_index = True\n\n# If true, the index is split into individual pages for each letter.\n#\n# html_split_index = False\n\n# If true, links to the reST sources are added to the pages.\n#\n# html_show_sourcelink = True\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\n#\n# html_show_sphinx = True\n\n# If true, \"(C) Copyright ...\" is shown in the HTML footer. Default is True.\n#\n# html_show_copyright = True\n\n# If true, an OpenSearch description file will be output, and all pages will\n# contain a <link> tag referring to it.  The value of this option must be the\n# base URL from which the finished HTML is served.\n#\n# html_use_opensearch = ''\n\n# This is the file name suffix for HTML files (e.g. \".xhtml\").\n# html_file_suffix = None\n\n# Language to be used for generating the HTML full-text search index.\n# Sphinx supports the following languages:\n#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'\n#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'\n#\n# html_search_language = 'en'\n\n# A dictionary with options for the search language support, empty by default.\n# 'ja' uses this config value.\n# 'zh' user can custom change `jieba` dictionary path.\n#\n# html_search_options = {'type': 'default'}\n\n# The name of a javascript file (relative to the configuration directory) that\n# implements a search results scorer. If empty, the default will be used.\n#\n# html_search_scorer = 'scorer.js'\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'python-slackclientdoc'\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n     # The paper size ('letterpaper' or 'a4paper').\n     #\n     # 'papersize': 'letterpaper',\n\n     # The font size ('10pt', '11pt' or '12pt').\n     #\n     # 'pointsize': '10pt',\n\n     # Additional stuff for the LaTeX preamble.\n     #\n     # 'preamble': '',\n\n     # Latex figure (float) alignment\n     #\n     # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'python-slackclient.tex', u'python-slackclient Documentation',\n     u'Ryan Huber, Jeff Ammons', 'manual'),\n]\n\n# The name of an image file (relative to this directory) to place at the top of\n# the title page.\n#\n# latex_logo = None\n\n# For \"manual\" documents, if this is true, then toplevel headings are parts,\n# not chapters.\n#\n# latex_use_parts = False\n\n# If true, show page references after internal links.\n#\n# latex_show_pagerefs = False\n\n# If true, show URL addresses after external links.\n#\n# latex_show_urls = False\n\n# Documents to append as an appendix to all manuals.\n#\n# latex_appendices = []\n\n# If false, no module index is generated.\n#\n# latex_domain_indices = True\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'python-slackclient', u'python-slackclient Documentation',\n     [author], 1)\n]\n\n# If true, show URL addresses after external links.\n#\n# man_show_urls = False\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'python-slackclient', u'python-slackclient Documentation',\n     author, 'python-slackclient', 'A basic client for Slack.com, which can optionally connect to the Slack Real Time Messaging (RTM) API.',\n     'Miscellaneous'),\n]\n\n# Documents to append as an appendix to all manuals.\n#\n# texinfo_appendices = []\n\n# If false, no module index is generated.\n#\n# texinfo_domain_indices = True\n\n# How to display URL addresses: 'footnote', 'no', or 'inline'.\n#\n# texinfo_show_urls = 'footnote'\n\n# If true, do not generate a @detailmenu in the \"Top\" node's menu.\n#\n# texinfo_no_detailmenu = False\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset={{ encoding }}\" />\n  {{ metatags }}\n\n  {%- block htmltitle %}\n  <title>{{ title|striptags|e + \" &mdash; \"|safe + project|e }}</title>\n  {%- endblock %}\n\n  {%- macro css() %}\n  <link href=\"https://a.slack-edge.com/4f227/style/rollup-slack_kit_legacy_adapters.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link href=\"https://a.slack-edge.com/3e02c0/style/rollup-api_site.css\" rel=\"stylesheet\" type=\"text/css\">\n  <link rel=\"stylesheet\" href=\"{{ pathto('./_static/' + 'default.css', 1) }}\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"{{ pathto('./_static/' + 'pygments.css', 1) }}\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"{{ pathto('./_static/' + 'docs.css', 1) }}\" type=\"text/css\" />\n  {%- endmacro %}\n\n  <!-- Google Tag Manager -->\n  <script>(function (w, d, s, l, i) {\n      w[l] = w[l] || []; w[l].push({\n        'gtm.start':\n          new Date().getTime(), event: 'gtm.js'\n      }); var f = d.getElementsByTagName(s)[0],\n        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =\n          'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);\n    })(window, document, 'script', 'dataLayer', 'GTM-KFZ5MK7');</script>\n  <!-- End Google Tag Manager -->\n  {{ css() }}\n  {%- block linktags %}\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"https://a.slack-edge.com/4f28/img/icons/favicon-32.png\" type=\"image/png\" />\n  <link rel=\"top\" title=\"{{ docstitle|e }}\" href=\"{{ pathto(master_doc) }}\" />\n  {%- endblock %}\n\n\n\n</head>\n\n<body class=\"api light_theme\">\n  <!-- Google Tag Manager (noscript) -->\n  <noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-KFZ5MK7\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n  <!-- End Google Tag Manager (noscript) -->\n\n  {%- block header %}\n  <header>\n    <a id=\"menu_toggle\" class=\"no_transition show_on_mobile\">\n      <span class=\"menu_icon\"></span>\n      <span class=\"vert_divider\"></span>\n    </a>\n    <a href=\"https://api.slack.com/\" id=\"header_logo\" class=\"api hide_on_mobile\" style=\"float:left; display: inline-block;\">\n      <img alt=\"Slack API\" src=\"https://a.slack-edge.com/3026cb/img/slack_api_logo_vogue.png\" style=\"width: 225px; padding-right: 25px; border-right: 1px solid #DDD;\" />\n    </a>\n    <span style=\"display: inline-block; padding-left: 20px; margin-top: 25px; font-weight: bold;\">\n      <a style=\"color: #555459;\" href=\"./index.html\">{{ project }}</a>\n    </span>\n    <div class=\"header_nav\">\n      <a href=\"https://github.com/SlackAPI/python-slackclient\" class=\"btn header_btn float_right\" data-qa=\"go_to_slack\">Go\n        to GitHub</a>\n    </div>\n  </header>\n  {% endblock %}\n\n  <div id=\"page\">\n    <div id=\"page_contents\" class=\"clearfix\">\n      <!-- Sidebar Content -->\n      <nav id=\"api_nav\" class=\"col span_1_of_4\">\n        <div id=\"api_sections\">\n          {% block sidebar %}\n          {% include 'sidebar.html' %}\n          {% endblock %}\n        </div>\n      </nav>\n      <!-- /Sidebar Content -->\n\n      <!-- Body Content -->\n      <div class=\"col span_3_of_4\">\n        <div class=\"section-title\">{{ title }}</div>\n        <div class=\"card\">\n          {%- block body %}\n          {{ body }}\n          {% endblock %}\n          <div class=\"clear_both large_bottom_margin\"></div>\n        </div>\n      </div>\n      <!-- /Body Content -->\n\n    </div>\n  </div>\n\n  <footer>\n    <p class=\"light tiny align_center\">© 2019 Slack Technologies, Inc. and contributors</p>\n  </footer>\n\n  <script>\n    window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;\n    ga('create', 'UA-56978219-13', 'auto');\n    ga('send', 'pageview');\n  </script>\n  <script async src='https://www.google-analytics.com/analytics.js'></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/localtoc.html",
    "content": "<h5>Table of Contents?</h5>\n<ul class=\"localtoc\">\n  {{ toc }}\n</ul>"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/relations.html",
    "content": "{#\n    basic/relations.html\n    ~~~~~~~~~~~~~~~~~~~~\n\n    Sphinx sidebar template: relation links.\n\n    :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.\n    :license: BSD, see LICENSE for details.\n#}\n{%- if prev %}\n  <h4>{{ _('Previous topic') }}</h4>\n  <p class=\"topless\"><a href=\"{{ prev.link|e }}\"\n                        title=\"{{ _('previous chapter') }}\">{{ prev.title }}</a></p>\n{%- endif %}\n{%- if next %}\n  <h4>{{ _('Next topic') }}</h4>\n  <p class=\"topless\"><a href=\"{{ next.link|e }}\"\n                        title=\"{{ _('next chapter') }}\">{{ next.title }}</a></p>\n{%- endif %}\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html",
    "content": "{{ toctree(maxdepth=-1, collapse=False,includehidden=True) }}\n\n<div id=\"footer\">\n\n    <ul id=\"footer_nav\">\n        <li><a href=\"https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\">License</a></li>\n        <li><a href=\"https://slackhq.github.io/code-of-conduct\">Code of Conduct</a></li>\n        <li><a href=\"https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\">Contributing</a></li>\n        <li><a href=\"https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\">Contributor License Agreement</a></li>\n    </ul>\n\n    <p id=\"footer_signature\">Made with <i class=\"ts_icon ts_icon_heart\"></i> by Slack<br/>and our Lovely\n        Community\n    </p>\n</div>\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t",
    "content": "a.headerlink {\n    display: none !important;\n}\n\n.section-title {\n  font-size: 2rem;\n  line-height: 2.5rem;\n  letter-spacing: -1px;\n  font-weight: 700;\n  margin: 0 0 1rem;\n}\n\nnav#api_nav .toctree-l1 {\n  margin-bottom: 1.5rem;\n}\n\nnav#api_nav #api_sections ul {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\nnav#api_nav #api_sections ul li.toctree-l1>a {\n  color: #1264a3;\n  letter-spacing: 0;\n  font-size: .8rem;\n  font-weight: 800;\n  text-transform: uppercase;\n  border: none;\n  padding: 0;\n}\n\nnav#api_nav #api_sections ul li.toctree-l2 {\n  margin: 0;\n  padding: 0;\n}\n\nnav#api_nav #api_sections ul li.toctree-l2 a {\n  color: #1d1c1d;\n  text-transform: none;\n  font-weight: inherit;\n  padding: 0;\n  display: block;\n  text-overflow: ellipsis;\n  overflow: hidden;\n  white-space: nowrap;\n  font-size: 15px!important;\n  line-height:15px;\n  padding: 4px 8px;\n  border: 1px solid transparent;\n  border-radius: 4px;\n}\n\nnav#api_nav #api_sections ul li.toctree-l2 a:hover {\n  cursor: pointer;\n  text-decoration: none;\n  background-color:#e8f5fa;\n  border-color:#dcf0fb;\n}\n\nnav#api_nav #footer #footer_nav {\n  font-size: .9375rem;\n}\n\nnav#api_nav #footer #footer_nav a {\n  border: none;\n  padding: 0;\n  color: #616061;\n}\n\nnav#api_nav #footer #footer_nav a:hover {\n  text-decoration: none;\n  color: #1c1c1c;\n}\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t",
    "content": "/* Updates body font */\nbody {\n  font-family: Slack-Lato,appleLogo,sans-serif;\n}\n\n/* Replaces old sidebar styled links */\n.sidebar_menu h5 {\n  font-size: 0.8rem;\n  font-weight: 800;\n  margin-bottom: 3px;\n}\n\n/* Aligns footer navigation to the left of the sidebar */\n.footer_nav {\n  margin: 0 !important;\n}\n\n/* Styles the signature all nice and pretty <3 */\n#footer_signature {\n  color:#e01e5a;\n  font-size:.9rem;\n  margin-top: 10px;\n}\n\n/* Fixes link hover state */\na:hover {\n  text-decoration: underline;\n}\n\n/* Makes footer consistent */\nfooter {\n  background-color: transparent;\n  border: 0;\n}"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t",
    "content": ".highlight .hll { background-color: #ffffcc }\n.highlight  { background: #ffffff; }\n.highlight .c { color: #999988; font-style: italic } /* Comment */\n.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */\n.highlight .k { font-weight: bold } /* Keyword */\n.highlight .o { font-weight: bold } /* Operator */\n.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */\n.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */\n.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */\n.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */\n.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */\n.highlight .ge { font-style: italic } /* Generic.Emph */\n.highlight .gr { color: #aa0000 } /* Generic.Error */\n.highlight .gh { color: #999999 } /* Generic.Heading */\n.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */\n.highlight .go { color: #888888 } /* Generic.Output */\n.highlight .gp { color: #555555 } /* Generic.Prompt */\n.highlight .gs { font-weight: bold } /* Generic.Strong */\n.highlight .gu { color: #aaaaaa } /* Generic.Subheading */\n.highlight .gt { color: #aa0000 } /* Generic.Traceback */\n.highlight .kc { font-weight: bold } /* Keyword.Constant */\n.highlight .kd { font-weight: bold } /* Keyword.Declaration */\n.highlight .kn { font-weight: bold } /* Keyword.Namespace */\n.highlight .kp { font-weight: bold } /* Keyword.Pseudo */\n.highlight .kr { font-weight: bold } /* Keyword.Reserved */\n.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */\n.highlight .m { color: #009999 } /* Literal.Number */\n.highlight .s { color: #bb8844 } /* Literal.String */\n.highlight .na { color: #008080 } /* Name.Attribute */\n.highlight .nb { color: #999999 } /* Name.Builtin */\n.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */\n.highlight .no { color: #008080 } /* Name.Constant */\n.highlight .ni { color: #800080 } /* Name.Entity */\n.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */\n.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */\n.highlight .nn { color: #555555 } /* Name.Namespace */\n.highlight .nt { color: #000080 } /* Name.Tag */\n.highlight .nv { color: #008080 } /* Name.Variable */\n.highlight .ow { font-weight: bold } /* Operator.Word */\n.highlight .w { color: #bbbbbb } /* Text.Whitespace */\n.highlight .mf { color: #009999 } /* Literal.Number.Float */\n.highlight .mh { color: #009999 } /* Literal.Number.Hex */\n.highlight .mi { color: #009999 } /* Literal.Number.Integer */\n.highlight .mo { color: #009999 } /* Literal.Number.Oct */\n.highlight .sb { color: #bb8844 } /* Literal.String.Backtick */\n.highlight .sc { color: #bb8844 } /* Literal.String.Char */\n.highlight .sd { color: #bb8844 } /* Literal.String.Doc */\n.highlight .s2 { color: #bb8844 } /* Literal.String.Double */\n.highlight .se { color: #bb8844 } /* Literal.String.Escape */\n.highlight .sh { color: #bb8844 } /* Literal.String.Heredoc */\n.highlight .si { color: #bb8844 } /* Literal.String.Interpol */\n.highlight .sx { color: #bb8844 } /* Literal.String.Other */\n.highlight .sr { color: #808000 } /* Literal.String.Regex */\n.highlight .s1 { color: #bb8844 } /* Literal.String.Single */\n.highlight .ss { color: #bb8844 } /* Literal.String.Symbol */\n.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */\n.highlight .vc { color: #008080 } /* Name.Variable.Class */\n.highlight .vg { color: #008080 } /* Name.Variable.Global */\n.highlight .vi { color: #008080 } /* Name.Variable.Instance */\n.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf",
    "content": "[theme]\ninherit = default\nstylesheet = default.css\nshow_sphinx = False\n\n[options]\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst",
    "content": "==============================================\nAbout\n==============================================\n\n|product_name|\n**************\n\n\nAccess the Slack Platform from your Python app. |product_name| lets you build on the Slack Web APIs pythonically.\n\n|product_name| is proudly maintained with 💖 by the Slack Developer Tools team\n\n- `License`_\n- `Code of Conduct`_\n- `Contributing`_\n- `Contributor License Agreement`_\n\n.. include:: metadata.rst"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst",
    "content": "==============================================\nTokens & Authentication\n==============================================\n.. _handling-tokens:\n\nHandling tokens and other sensitive data\n----------------------------------------\n⚠️ **Slack tokens are the keys to your—or your customers’—data.Keep them secret. Keep them safe.**\n\nOne way to do that is to never explicitly hardcode them.\n\nTry to avoid this when possible:\n\n.. code-block:: python\n\n  token = 'xoxb-abc-1232'\n\n⚠️ **Never share test tokens with other users or applications. Do not publish test tokens in public code repositories.**\n\nWe recommend you pass tokens in as environment variables, or persist them in a database that is accessed at runtime. You can add a token to the environment by starting your app as:\n\n.. code-block:: python\n\n  SLACK_BOT_TOKEN=\"xoxb-abc-1232\" python myapp.py\n\nThen retrieve the key with:\n\n.. code-block:: python\n\n  import os\n  SLACK_BOT_TOKEN = os.environ[\"SLACK_BOT_TOKEN\"]\n\nYou can use the same technique for other kinds of sensitive data that ne’er-do-wells could use in nefarious ways, including\n\n- Incoming webhook URLs\n- Slash command verification tokens\n- App client ids and client secrets\n\nFor additional information, please see our `Safely Storing Credentials <https://api.slack.com/docs/oauth-safety>`_ page.\n\nSingle-Workspace Apps\n-----------------------\nIf you're building an application for a single Slack workspace, there's no need to build out the entire OAuth flow.\n\nOnce you've setup your features, click on the **Install App to Team** button found on the **Install App** page.\nIf you add new permission scopes or Slack app features after an app has been installed, you must reinstall the app to\nyour workspace for changes to take effect.\n\nFor additional information, see the `Installing Apps <https://api.slack.com/slack-apps#installing_apps>`_ of our `Building Slack apps <https://api.slack.com/slack-apps#installing_apps>`_ page.\n\nThe OAuth flow\n-------------------------\nAuthentication for Slack's APIs is done using OAuth, so you'll want to read up on `OAuth <https://api.slack.com/docs/oauth>`_.\n\nIn order to implement OAuth in your app, you will need to include a web server. In this example, we'll use `Flask <http://flask.pocoo.org/>`_.\n\nAs mentioned above, we're setting the app tokens and other configs in environment variables and pulling them into global variables.\n\nDepending on what actions your app will need to perform, you'll need different OAuth permission scopes. Review the available scopes `here <https://api.slack.com/docs/oauth-scopes>`_.\n\n.. code-block:: python\n\n  import os\n  from flask import Flask, request\n  from slackclient import SlackClient\n\n  client_id = os.environ[\"SLACK_CLIENT_ID\"]\n  client_secret = os.environ[\"SLACK_CLIENT_SECRET\"]\n  oauth_scope = os.environ[\"SLACK_BOT_SCOPE\"]\n\n  app = Flask(__name__)\n\n**The OAuth initiation link:**\n\nTo begin the OAuth flow, you'll need to provide the user with a link to Slack's OAuth page.\nThis directs the user to Slack's OAuth acceptance page, where the user will review and accept\nor refuse the permissions your app is requesting as defined by the requested scope(s).\n\nFor the best user experience, use the `Add to Slack button <https://api.slack.com/docs/slack-button>`_ to direct users to approve your application for access or `Sign in with Slack <https://api.slack.com/docs/sign-in-with-slack>`_ to log users in.\n\n.. code-block:: python\n\n  @app.route(\"/begin_auth\", methods=[\"GET\"])\n  def pre_install():\n    return '''\n        <a href=\"https://slack.com/oauth/authorize?scope={0}&client_id={1}\">\n            Add to Slack\n        </a>\n    '''.format(oauth_scope, client_id)\n\n**The OAuth completion page**\n\nOnce the user has agreed to the permissions you've requested on Slack's OAuth\npage, Slack will redirect the user to your auth completion page. Included in this\nredirect is a ``code`` query string param which you’ll use to request access\ntokens from the ``oauth.access`` endpoint.\n\nGenerally, Web API requests require a valid OAuth token, but there are a few endpoints\nwhich do not require a token. ``oauth.access`` is one example of this. Since this\nis the endpoint you'll use to retrieve the tokens for later API requests,\nan empty string ``\"\"`` is acceptable for this request.\n\n.. code-block:: python\n\n  @app.route(\"/finish_auth\", methods=[\"GET\", \"POST\"])\n  def post_install():\n\n    # Retrieve the auth code from the request params\n    auth_code = request.args['code']\n\n    # An empty string is a valid token for this request\n    sc = SlackClient(\"\")\n\n    # Request the auth tokens from Slack\n    auth_response = sc.api_call(\n      \"oauth.access\",\n      client_id=client_id,\n      client_secret=client_secret,\n      code=auth_code\n    )\n\nA successful request to ``oauth.access`` will yield two tokens: A ``user``\ntoken and a ``bot`` token. The ``user`` token ``auth_response['access_token']``\nis used to make requests on behalf of the authorizing user and the ``bot``\ntoken ``auth_response['bot']['bot_access_token']`` is for making requests\non behalf of your app's bot user.\n\nIf your Slack app includes a bot user, upon approval the JSON response will contain\nan additional node containing an access token to be specifically used for your bot\nuser, within the context of the approving team.\n\nWhen you use Web API methods on behalf of your bot user, you should use this bot\nuser access token instead of the top-level access token granted to your application.\n\n.. code-block:: python\n\n    # Save the bot token to an environmental variable or to your data store\n    # for later use\n    os.environ[\"SLACK_USER_TOKEN\"] = auth_response['access_token']\n    os.environ[\"SLACK_BOT_TOKEN\"] = auth_response['bot']['bot_access_token']\n\n    # Don't forget to let the user know that auth has succeeded!\n    return \"Auth complete!\"\n\nOnce your user has completed the OAuth flow, you'll be able to use the provided\ntokens to make a variety of Web API calls on behalf of the user and your app's bot user.\n\nSee the :ref:`Web API usage <web-api-examples>` section of this documentation for usage examples.\n\n.. include:: metadata.rst\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst",
    "content": ".. _web-api-examples:\n\n==============================================\nBasic Usage\n==============================================\nThe Slack Web API allows you to build applications that interact with Slack in more complex ways than the integrations\nwe provide out of the box.\n\nThis package is a modular wrapper designed to make Slack `Web API`_ calls simpler and easier for your\napp. Provided below are examples of how to interact with commonly used API endpoints, but this is by no means\na complete list. Review the full list of available methods `here <https://api.slack.com/methods>`_.\n\nSee :ref:`Tokens & Authentication <handling-tokens>` for API token handling best practices.\n\n--------\n\nSending a message\n-----------------------\nThe primary use of Slack is sending messages. Whether you're sending a message\nto a user or to a channel, this method handles both.\n\nTo send a message to a channel, use the channel's ID. For IMs, use the user's ID.\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"chat.postMessage\",\n    channel=\"C0XXXXXX\",\n    text=\"Hello from Python! :tada:\"\n  )\n\nThere are some unique options specific to sending IMs, so be sure to read the **channels**\nsection of the `chat.postMessage <https://api.slack.com/methods/chat.postMessage#channels>`_\npage for a full list of formatting and authorship options.\n\nSending an ephemeral message, which is only visible to an assigned user in a specified channel, is nearly the same \nas sending a regular message, but with an additional ``user`` parameter.\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"chat.postEphemeral\",\n    channel=\"C0XXXXXX\",\n    text=\"Hello from Python! :tada:\",\n    user=\"U0XXXXXXX\"\n  )\n\nSee `chat.postEphemeral <https://api.slack.com/methods/chat.postEphemeral>`_ for more info.\n\n--------\n\nCustomizing a message's layout\n-----------------------\nThe chat.postMessage method takes an optional blocks argument that allows you to customize the layout of a message. \nBlocks for Web API methods are all specified in a single object literal, so just add additional keys for any optional argument.\n\nTo send a message to a channel, use the channel's ID. For IMs, use the user's ID.\n\n.. code-block:: python\n\n  sc.api_call(\n    \"chat.postMessage\",\n    channel=\"C0XXXXXX\",\n    blocks=[\n      {\n          \"type\": \"section\",\n          \"text\": {\n              \"type\": \"mrkdwn\",\n              \"text\": \"Danny Torrence left the following review for your property:\"\n          }\n      },\n      {\n          \"type\": \"section\",\n          \"text\": {\n              \"type\": \"mrkdwn\",\n              \"text\": \"<https://example.com|Overlook Hotel> \\n :star: \\n Doors had too many axe holes, guest in room \" +\n              \"237 was far too rowdy, whole place felt stuck in the 1920s.\"\n          },\n          \"accessory\": {\n              \"type\": \"image\",\n              \"image_url\": \"https://images.pexels.com/photos/750319/pexels-photo-750319.jpeg\",\n              \"alt_text\": \"Haunted hotel image\"\n          }\n      },\n      {\n          \"type\": \"section\",\n          \"fields\": [\n              {\n                  \"type\": \"mrkdwn\",\n                  \"text\": \"*Average Rating*\\n1.0\"\n              }\n          ]\n      }\n    ]\n  )\n\n**Note:** You can use the `Block Kit Builder <https://api.slack.com/tools/block-kit-builder>`for a playground where you can prototype your message's look and feel.\n\n--------\n\nReplying to messages and creating threads\n------------------------------------------\nThreaded messages are just like regular messages, except thread replies are grouped together to provide greater context\nto the user. You can reply to a thread or start a new threaded conversation by simply passing the original message's ``ts``\nID in the ``thread_ts`` attribute when posting a message. If you're replying to a threaded message, you'll pass the `thread_ts`\nID of the message you're replying to.\n\nA channel or DM conversation is a nearly linear timeline of messages exchanged between people, bots, and apps.\nWhen one of these messages is replied to, it becomes the parent of a thread. By default, threaded replies do not\nappear directly in the channel, instead relegated to a kind of forked timeline descending from the parent message.\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"chat.postMessage\",\n    channel=\"C0XXXXXX\",\n    text=\"Hello from Python! :tada:\",\n    thread_ts=\"1476746830.000003\"\n  )\n\n\nBy default, ``reply_broadcast`` is set to ``False``. To indicate your reply is germane to all members of a channel,\nset the ``reply_broadcast`` boolean parameter to ``True``.\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"chat.postMessage\",\n    channel=\"C0XXXXXX\",\n    text=\"Hello from Python! :tada:\",\n    thread_ts=\"1476746830.000003\",\n    reply_broadcast=True\n  )\n\n\n**Note:** While threaded messages may contain attachments and message buttons, when your reply is broadcast to the\nchannel, it'll actually be a reference to your reply, not the reply itself.\nSo, when appearing in the channel, it won't contain any attachments or message buttons. Also note that updates and\ndeletion of threaded replies works the same as regular messages.\n\nSee the `Threading messages together <https://api.slack.com/docs/message-threading#forking_conversations>`_\narticle for more information.\n\n\n--------\n\nUpdating the content of a message\n----------------------------------\nLet's say you have a bot which posts the status of a request. When that request\nis updated, you'll want to update the message to reflect it's state. Or your user\nmight want to fix a typo or change some wording. This is how you'll make those changes.\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"chat.update\",\n    ts=\"1476746830.000003\",\n    channel=\"C0XXXXXX\",\n    text=\"Hello from Python! :tada:\"\n  )\n\nSee `chat.update <https://api.slack.com/methods/chat.update>`_ for formatting options\nand some special considerations when calling this with a bot user.\n\n--------\n\nDeleting a message\n-------------------\nSometimes you need to delete things.\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"chat.delete\",\n    channel=\"C0XXXXXX\",\n    ts=\"1476745373.000002\"\n  )\n\nSee `chat.delete <https://api.slack.com/methods/chat.delete>`_ for more info.\n\n--------\n\nAdding or removing an emoji reaction\n---------------------------------------\nYou can quickly respond to any message on Slack with an emoji reaction. Reactions\ncan be used for any purpose: voting, checking off to-do items, showing excitement — and just for fun.\n\nThis method adds a reaction (emoji) to an item (``file``, ``file comment``, ``channel message``, ``group message``, or ``direct message``). One of file, file_comment, or the combination of channel and timestamp must be specified.\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"reactions.add\",\n    channel=\"C0XXXXXXX\",\n    name=\"thumbsup\",\n    timestamp=\"1234567890.123456\"\n  )\n\nRemoving an emoji reaction is basically the same format, but you'll use ``reactions.remove`` instead of ``reactions.add``\n\n.. code-block:: python\n\n  sc.api_call(\n    \"reactions.remove\",\n    channel=\"C0XXXXXXX\",\n    name=\"thumbsup\",\n    timestamp=\"1234567890.123456\"\n  )\n\n\nSee `reactions.add <https://api.slack.com/methods/reactions.add>`_ and `reactions.remove <https://api.slack.com/methods/reactions.remove>`_ for more info.\n\n--------\n\nGetting a list of channels\n---------------------------\nAt some point, you'll want to find out what channels are available to your app. This is how you get that list.\n\n**Note:** This call requires the ``channels:read`` scope.\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\"channels.list\")\n\nArchived channels are included by default. You can exclude them by passing ``exclude_archived=1`` to your request.\n\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"channels.list\",\n    exclude_archived=1\n  )\n\nSee `channels.list <https://api.slack.com/methods/channels.list>`_ for more info.\n\n--------\n\nGetting a channel's info\n-------------------------\nOnce you have the ID for a specific channel, you can fetch information about that channel.\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"channels.info\",\n    channel=\"C0XXXXXXX\"\n  )\n\nSee `channels.info <https://api.slack.com/methods/channels.info>`_ for more info.\n\n--------\n\nJoining a channel\n------------------\nChannels are the social hub of most Slack teams. Here's how you hop into one:\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"channels.join\",\n    channel=\"C0XXXXXXY\"\n  )\n\nIf you are already in the channel, the response is slightly different.\n``already_in_channel`` will be true, and a limited ``channel`` object will be returned. Bot users cannot join a channel on their own, they need to be invited by another user.\n\nSee `channels.join <https://api.slack.com/methods/channels.join>`_ for more info.\n\n--------\n\nLeaving a channel\n------------------\nMaybe you've finished up all the business you had in a channel, or maybe you\njoined one by accident. This is how you leave a channel.\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"channels.leave\",\n    channel=\"C0XXXXXXX\"\n  )\n\nSee `channels.leave <https://api.slack.com/methods/channels.leave>`_ for more info.\n\n--------\n\nGet a list of team members\n------------------------------\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\"users.list\")\n\nSee `users.list <https://api.slack.com/methods/users.list>`_ for more info.\n\n\n--------\n\nUploading files\n------------------------------\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  with open('thinking_very_much.png') as file_content:\n      sc.api_call(\n          \"files.upload\",\n          channels=\"C3UKJTQAC\",\n          file=file_content,\n          title=\"Test upload\"\n      )\n\nSee `files.upload <https://api.slack.com/methods/files.upload>`_ for more info.\n\n\n--------\n\nWeb API Rate Limits\n--------------------\nSlack allows applications to send no more than one message per second. We allow bursts over that\nlimit for short periods. However, if your app continues to exceed the limit over a longer period\nof time it will be rate limited.\n\nHere's a very basic example of how one might deal with rate limited requests.\n\nIf you go over these limits, Slack will start returning a HTTP 429 Too Many Requests error,\na JSON object containing the number of calls you have been making, and a Retry-After header\ncontaining the number of seconds until you can retry.\n\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n  import time\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  # Simple wrapper for sending a Slack message\n  def send_slack_message(channel, message):\n    return sc.api_call(\n      \"chat.postMessage\",\n      channel=channel,\n      text=message\n    )\n\n  # Make the API call and save results to `response`\n  response = send_slack_message(\"C0XXXXXX\", \"Hello, from Python!\")\n\n  # Check to see if the message sent successfully.\n  # If the message succeeded, `response[\"ok\"]`` will be `True`\n  if response[\"ok\"]:\n    print(\"Message posted successfully: \" + response[\"message\"][\"ts\"])\n    # If the message failed, check for rate limit headers in the response\n  elif response[\"ok\"] is False and response[\"headers\"][\"Retry-After\"]:\n    # The `Retry-After` header will tell you how long to wait before retrying\n    delay = int(response[\"headers\"][\"Retry-After\"])\n    print(\"Rate limited. Retrying in \" + str(delay) + \" seconds\")\n    time.sleep(delay)\n    send_slack_message(message, channel)\n\nSee the documentation on `Rate Limiting <https://api.slack.com/docs/rate-limits>`_ for more info.\n\n.. include:: metadata.rst\n\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst",
    "content": "==============================================\nChangelog\n==============================================\n\nv1.3.2 (2019-05-30)\n-------------------\n- Fixing an issue where reconnects used rtm.start istead of rtm.connect. #422\n\n\nv1.3.1 (2019-02-28)\n-------------------\n\n- Lock websocket-client version to < 0.55.0: temp fix for #385\n\n\nv1.3.0 (2018-09-11)\n-------------------\n\n## New Features\n- Adds support for short lived tokens and automatic token refresh #347 (Thanks @roach!)\n\n## Other\n- update RTM rate limiting comment and error message #308 (Thanks @benoitlavigne!)\n- Use logging instead of traceback #309 (Thanks @harlowja!)\n- Remove Python 3.3 from test environments #346 (Thanks @roach!)\n- Enforced linting when using VSCode. #347 (Thanks @roach!)\n\n\nv1.2.1 (2018-03-26)\n-------------------\n\n- Added rate limit handling for rtm connections (thanks @jayalane!)\n\n\nv1.2.0 (2018-03-20)\n-------------------\n\n- You can now tell the RTM client to automatically reconnect by passing `auto_reconnect=True`\n\nv1.1.3 (2018-03-01)\n-------------------\n\n- Fixed another API param encoding bug. It encodes things properly now.\n\nv1.1.2 (2018-01-31)\n-------------------\n\n- Fixed an encoding issue which was encoding some Web API params incorrectly (sorry)\n\nv1.1.1 (2018-01-30)\n-------------------\n\n - Adds HTTP response headers to `api_call` responses to expose things like rate limit info\n - Moves `token` into auth header rather than request params\n\nv1.1.0 (2017-11-21)\n-------------------\n\n - Aadds new SlackClientError and ResponseParseError types to describe errors - thanks @aoberoi!\n - Fix Build Error (#245) - thanks @stasfilin!\n - include email as user property (#173) - thanks @acaire!\n - Add http reply into slack login and slack connection error (#216) - thanks @harlowja!\n - Removed unused exception class (#233)\n - Fix rtm_send_message bug (#225) - thanks @kt5356!\n - Allow use of custom parameters on rtm_connect() (#210) - thanks @kamushadenes!\n - Fix link to rtm.connect docs (#223) - @sampart!\n\nv1.0.9 (2017-08-31)\n-------------------\n\n  - Fixed rtm_send_message ID bug introduced in 1.0.8\n\nv1.0.8 (2017-08-31)\n-------------------\n\n  - Added rtm.connect support\n\nv1.0.7 (2017-08-02)\n-------------------\n\n  - Fixes an issue where connecting over RTM to large teams may result in “Websocket URL expired” errors\n  - A load of packaging improvements\n\nv1.0.6 (2017-06-12)\n-------------------\n\n  - Added proxy support (thanks @timfeirg!)\n  - Tidied up docs (thanks @schlueter!)\n  - Added tox settings for Python 3 testing (thanks @cclauss!)\n\nv1.0.5 (2017-01-23)\n-------------------\n\n  - Allow RTM Channel.send_message to reply to a thread\n  - Index users by ID instead of Name (non-breaking change)\n  - Added timeout to api calls.\n  - Fixed a typo about token access in auth.rst, thanks @kelvintaywl!\n  - Added Message Threads to the docs\n\nv1.0.4 (2016-12-15)\n-------------------\n\n  - fixed the ability to search for a user by ID\n\nv1.0.3 (2016-12-13)\n-------------------\n\n  - fixed an issue causing RTM connections to fail for large teams\n\nv1.0.2 (2016-09-22)\n-------------------\n\n  - removed unused ping counter\n  - fixed contributor guidelines links\n  - updated documentation\n  - Fix bug preventing API calls requiring a file ID\n  - Removes files from api_calls before JSON encoding, so the request is properly formatted\n\nv1.0.1 (2016-03-25)\n-------------------\n\n  - fix for __eq__ comparison in channels using '#' in channel name\n  - added copyright info to the LICENSE file\n\nv1.0.0 (2016-02-28)\n-------------------\n\n  - the ``api_call`` function now returns a decoded JSON object, rather than a JSON encoded string\n  - some ``api_call`` calls now call actions on the parent server object:\n    - ``im.open``\n    - ``mpim.open``, ``groups.create``, ``groups.createChild``\n    - ``channels.create``, `channels.join``\n\nv0.18.0 (2016-02-21)\n--------------------\n\n  - Moves to use semver for versioning\n  - Adds support for private groups and MPDMs\n  - Switches to use requests instead of urllib\n  - Gets Travis CI integration working\n  - Fixes some formatting issues so the code will work for python 2.6\n  - Cleans up some unused imports, some PEP-8 fixes and a couple bad default args fixes\n\nv0.17.0 (2016-02-15)\n--------------------\n\n  - Fixes the server so that it doesn't add duplicate users or channels to its internal lists, https://github.com/slackapi/python-slackclient/commit/0cb4bcd6e887b428e27e8059b6278b86ee661aaa\n  - README updates:\n    - Updates the URLs pointing to Slack docs for configuring authentication, https://github.com/slackapi/python-slackclient/commit/7d01515cebc80918a29100b0e4793790eb83e7b9\n    - s/channnels/channels, https://github.com/slackapi/python-slackclient/commit/d45285d2f1025899dcd65e259624ee73771f94bb\n  - Adds users to the local cache when they join the team, https://github.com/slackapi/python-slackclient/commit/f7bb8889580cc34471ba1ddc05afc34d1a5efa23\n  - Fixes urllib py 2/3 compatibility, https://github.com/slackapi/python-slackclient/commit/1046cc2375a85a22e94573e2aad954ba7287c886\n\n\n\n.. include:: metadata.rst\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# python-slackclient documentation build configuration file, created by\n# sphinx-quickstart on Mon Jun 27 17:36:09 2016.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os\nimport sys\nsys.path.insert(0, os.path.abspath('../'))\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.coverage',\n]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The encoding of source files.\n#\n# source_encoding = 'utf-8-sig'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'python-slackclient'\ncopyright = u'2016, Ryan Huber, Jeff Ammons'\nauthor = u'Ryan Huber, Jeff Ammons'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'1.0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'1.0.1'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = None\n\n# There are two options for replacing |today|: either, you set today to some\n# non-false value, then it is used:\n#\n# today = ''\n#\n# Else, today_fmt is used as the format for a strftime call.\n#\n# today_fmt = '%B %d, %Y'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n# The reST default role (used for this markup: `text`) to use for all\n# documents.\n#\n# default_role = None\n\n# If true, '()' will be appended to :func: etc. cross-reference text.\n#\n# add_function_parentheses = True\n\n# If true, the current module name will be prepended to all description\n# unit titles (such as .. function::).\n#\n# add_module_names = True\n\n# If true, sectionauthor and moduleauthor directives will be shown in the\n# output. They are ignored by default.\n#\n# show_authors = False\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# A list of ignored prefixes for module index sorting.\n# modindex_common_prefix = []\n\n# If true, keep warnings as \"system message\" paragraphs in the built documents.\n# keep_warnings = False\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nimport sphinx_rtd_theme  # noqa\nhtml_theme = \"sphinx_rtd_theme\"\n\nhtml_theme_path = [sphinx_rtd_theme.get_html_theme_path()]\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom themes here, relative to this directory.\n# html_theme_path = []\n\n# The name for this set of Sphinx documents.\n# \"<project> v<release> documentation\" by default.\n#\n# html_title = u'python-slackclient v1.0.1'\n\n# A shorter title for the navigation bar.  Default is the same as html_title.\n#\n# html_short_title = None\n\n# The name of an image file (relative to this directory) to place at the top\n# of the sidebar.\n#\n# html_logo = None\n\n# The name of an image file (relative to this directory) to use as a favicon of\n# the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32\n# pixels large.\n#\n# html_favicon = None\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n# html_static_path = ['_static']\n\n# Add any extra paths that contain custom files (such as robots.txt or\n# .htaccess) here, relative to this directory. These files are copied\n# directly to the root of the documentation.\n#\n# html_extra_path = []\n\n# If not None, a 'Last updated on:' timestamp is inserted at every page\n# bottom, using the given strftime format.\n# The empty string is equivalent to '%b %d, %Y'.\n#\n# html_last_updated_fmt = None\n\n# If true, SmartyPants will be used to convert quotes and dashes to\n# typographically correct entities.\n#\n# html_use_smartypants = True\n\n# Custom sidebar templates, maps document names to template names.\n#\n# html_sidebars = {}\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n#\n# html_additional_pages = {}\n\n# If false, no module index is generated.\n#\n# html_domain_indices = True\n\n# If false, no index is generated.\n#\n# html_use_index = True\n\n# If true, the index is split into individual pages for each letter.\n#\n# html_split_index = False\n\n# If true, links to the reST sources are added to the pages.\n#\n# html_show_sourcelink = True\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\n#\n# html_show_sphinx = True\n\n# If true, \"(C) Copyright ...\" is shown in the HTML footer. Default is True.\n#\n# html_show_copyright = True\n\n# If true, an OpenSearch description file will be output, and all pages will\n# contain a <link> tag referring to it.  The value of this option must be the\n# base URL from which the finished HTML is served.\n#\n# html_use_opensearch = ''\n\n# This is the file name suffix for HTML files (e.g. \".xhtml\").\n# html_file_suffix = None\n\n# Language to be used for generating the HTML full-text search index.\n# Sphinx supports the following languages:\n#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'\n#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'\n#\n# html_search_language = 'en'\n\n# A dictionary with options for the search language support, empty by default.\n# 'ja' uses this config value.\n# 'zh' user can custom change `jieba` dictionary path.\n#\n# html_search_options = {'type': 'default'}\n\n# The name of a javascript file (relative to the configuration directory) that\n# implements a search results scorer. If empty, the default will be used.\n#\n# html_search_scorer = 'scorer.js'\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'python-slackclientdoc'\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n     # The paper size ('letterpaper' or 'a4paper').\n     #\n     # 'papersize': 'letterpaper',\n\n     # The font size ('10pt', '11pt' or '12pt').\n     #\n     # 'pointsize': '10pt',\n\n     # Additional stuff for the LaTeX preamble.\n     #\n     # 'preamble': '',\n\n     # Latex figure (float) alignment\n     #\n     # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'python-slackclient.tex', u'python-slackclient Documentation',\n     u'Ryan Huber, Jeff Ammons', 'manual'),\n]\n\n# The name of an image file (relative to this directory) to place at the top of\n# the title page.\n#\n# latex_logo = None\n\n# For \"manual\" documents, if this is true, then toplevel headings are parts,\n# not chapters.\n#\n# latex_use_parts = False\n\n# If true, show page references after internal links.\n#\n# latex_show_pagerefs = False\n\n# If true, show URL addresses after external links.\n#\n# latex_show_urls = False\n\n# Documents to append as an appendix to all manuals.\n#\n# latex_appendices = []\n\n# If false, no module index is generated.\n#\n# latex_domain_indices = True\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'python-slackclient', u'python-slackclient Documentation',\n     [author], 1)\n]\n\n# If true, show URL addresses after external links.\n#\n# man_show_urls = False\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'python-slackclient', u'python-slackclient Documentation',\n     author, 'python-slackclient', 'A basic client for Slack.com, which can optionally connect to the Slack Real Time Messaging (RTM) API.',\n     'Miscellaneous'),\n]\n\n# Documents to append as an appendix to all manuals.\n#\n# texinfo_appendices = []\n\n# If false, no module index is generated.\n#\n# texinfo_domain_indices = True\n\n# How to display URL addresses: 'footnote', 'no', or 'inline'.\n#\n# texinfo_show_urls = 'footnote'\n\n# If true, do not generate a @detailmenu in the \"Top\" node's menu.\n#\n# texinfo_no_detailmenu = False\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst",
    "content": ".. _conversations_api:\n\n==============================================\nConversations API\n==============================================\nThe Slack Conversations API provides your app with a unified interface to work with all the\nchannel-like things encountered in Slack; public channels, private channels, direct messages, group\ndirect messages, and our newest channel type, Shared Channels.\n\n\nSee `Conversations API <https://api.slack.com/docs/conversations-api>`_ docs for more info.\n\n--------\n\nCreating a direct message or multi-person direct message\n---------------------------------------------------------\nThis Conversations API method opens a multi-person direct message or just a 1:1 direct message.\n\n*Use conversations.create for public or private channels.*\n\nProvide 1 to 8 user IDs in the ``user`` parameter to open or resume a conversation. Providing only\n1 ID will create a direct message. Providing more will create an ``mpim``.\n\nIf there are no conversations already in progress including that exact set of members, a new\nmulti-person direct message conversation begins.\n\nSubsequent calls to ``conversations.open`` with the same set of users will return the already\nexisting conversation.\n\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"conversations.open\",\n    users=[\"W1234567890\",\"U2345678901\",\"U3456789012\"]\n  )\n\nSee `conversations.open <https://api.slack.com/methods/conversations.open>`_ additional info.\n\n--------\n\nCreating a public or private channel\n-------------------------------------\nInitiates a public or private channel-based conversation\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"conversations.create\",\n    name=\"myprivatechannel\",\n    is_private=True\n  )\n\nSee `conversations.create <https://api.slack.com/methods/conversations.create>`_ additional info.\n\n--------\n\nGetting information about a conversation\n-----------------------------------------\nThis Conversations API method returns information about a workspace conversation.\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"conversations.info\",\n    channel=\"C0XXXXXX\",\n  )\n\nSee `conversations.info <https://api.slack.com/methods/conversations.info>`_ for more info.\n\n\n--------\n\nGetting a list of conversations\n--------------------------------\nThis Conversations API method returns a list of all channel-like conversations in a workspace.\nThe \"channels\" returned depend on what the calling token has access to and the directives placed\nin the ``types`` parameter.\n\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\"conversations.list\")\n\nOnly public conversations are included by default. You may include additional conversations types\nby passing ``types`` (as a string) into your list request. Additional conversation types include\n``public_channel`` and ``private_channel``.\n\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  # Note that `types` is a string\n  sc.api_call(\n    \"conversations.list\",\n    types=\"public_channel, private_channel\"\n  )\n\nSee `conversations.list <https://api.slack.com/methods/conversations.list>`_ for more info.\n\n\n--------\n\nLeaving a conversation\n-----------------------\nMaybe you've finished up all the business you had in a conversation, or maybe you\njoined one by accident. This is how you leave a conversation.\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\n    \"conversations.leave\",\n    channel=\"C0XXXXXXX\"\n  )\n\nSee `conversations.leave <https://api.slack.com/methods/conversations.leave>`_ for more info.\n\n--------\n\nGet conversation members\n------------------------------\nGet a list fo the members of a conversation\n\n.. code-block:: python\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.api_call(\"conversations.members\",\n    channel=\"C0XXXXXXX\"\n  )\n\nSee `users.list <https://api.slack.com/methods/conversations.members>`_ for more info.\n\n.. include:: metadata.rst\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst",
    "content": "==============================================\nFrequently Asked Questions\n==============================================\n\nWhat even is |product_name| and why should I care?\n**************************************************\n\n|product_name| is a wrapper around commonly accessed parts of the Slack Platform. It provides basic mechanisms for\nusing the Slack Web API from within your Python app.\n\nOn the other hand, |product_name| does not provide access to the Events bot-building API, but\n[this adapter](https://github.com/slackapi/python-slack-events-api) does.\n\nOMG I found a bug!\n******************\n\nWell, poop. Take a deep breath, and then let us know on the `Issue Tracker`_. If you're feeling particularly ambitious,\nwhy not submit a `pull request`_ with a bug fix?\n\nHey, there's a feature missing!\n*******************************\n\nThere's always something more that could be added! You can let us know in the `Issue Tracker`_ to start a discussion\naround the proposed feature, that's a good start. If you're feeling particularly ambitious, why not write the feature\nyourself, and submit a `pull request`_! We love feedback and we love help and we don't bite. Much.\n\nI'd like to contribute...but how?\n*********************************\n\nWhat an excellent question. First of all, please have a look at our general `contributing guidelines`_. We'll wait for\nyou here.\n\nAll done? Great! While we're super excited to incorporate your new feature into |product_name|, there are a\ncouple of things we want to make sure you've given thought to.\n\n- Please write unit tests for your new code. But don't **just** aim to increase the test coverage, rather, we expect you\n  to have written **thoughtful** tests that ensure your new feature will continue to work as expected, and to help future\n  contributors to ensure they don't break it!\n\n- Please document your new feature. Think about **concrete use cases** for your feature, and add a section to the\n  appropriate document, including a **complete** sample program that demonstrates your feature. Don't forget to update\n  the changelog in ``changelog.rst``!\n\nIncluding these two items with your pull request will totally make our day—and, more importantly, your future users' days!\n\nOn that note...\n\nHow do I compile the documentation?\n***********************************\n\nThis project's documentation is generated with `Sphinx <http://www.sphinx-doc.org>`_. If you are editing one of the many\nreStructuredText files in the ``docs-src`` folder, you'll need to rebuild the documentation. It is recommended to run\nthe following steps inside a ``virtualenv`` environment.\n\n.. code-block:: bash\n\n  tox -e docs\n\n\nDo be sure to add the ``docs`` folder and its contents to your pull request!\n\n\n.. include:: metadata.rst\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst",
    "content": ".. toctree::\n   :hidden:\n\n   self\n   auth\n   basic_usage\n   conversations\n   real_time_messaging\n   faq\n   changelog\n   about\n\n==============================================\n|product_name|\n==============================================\n\nWhether you're building a custom app for your team, or integrating a third party\nservice into your Slack workflows, |product_name| allows you to leverage the flexibility\nof Python to get your project up and running as quickly as possible.\n\nRequirements and Installation\n******************************\n\nWe recommend using `PyPI <https://pypi.python.org/pypi>`_ to install |product_name|\n\n.. code-block:: bash\n\n\tpip install slackclient\n\nOf course, if you prefer doing things the hard way, you can always implement |product_name|\nby pulling down the source code directly into your project:\n\n.. code-block:: bash\n\n\tgit clone https://github.com/slackapi/python-slackclient.git\n\tpip install -r requirements.txt\n\nGetting Help\n************\n\nIf you get stuck, we’re here to help. The following are the best ways to get assistance working through your issue:\n\n- Use our `Github Issue Tracker <https://github.com/slackapi/python-slackclient/issues>`_ for reporting bugs or requesting features.\n- Visit the `Bot Developer Hangout <http://community.botkit.ai>`_ for getting help using |product_name| or just generally bond with your fellow Slack developers.\n\n.. include:: metadata.rst\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat",
    "content": "@ECHO OFF\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset BUILDDIR=_build\r\nset ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .\r\nset I18NSPHINXOPTS=%SPHINXOPTS% .\r\nif NOT \"%PAPER%\" == \"\" (\r\n\tset ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%\r\n\tset I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%\r\n)\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\nif \"%1\" == \"help\" (\r\n\t:help\r\n\techo.Please use `make ^<target^>` where ^<target^> is one of\r\n\techo.  html       to make standalone HTML files\r\n\techo.  dirhtml    to make HTML files named index.html in directories\r\n\techo.  singlehtml to make a single large HTML file\r\n\techo.  pickle     to make pickle files\r\n\techo.  json       to make JSON files\r\n\techo.  htmlhelp   to make HTML files and a HTML help project\r\n\techo.  qthelp     to make HTML files and a qthelp project\r\n\techo.  devhelp    to make HTML files and a Devhelp project\r\n\techo.  epub       to make an epub\r\n\techo.  epub3      to make an epub3\r\n\techo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\r\n\techo.  text       to make text files\r\n\techo.  man        to make manual pages\r\n\techo.  texinfo    to make Texinfo files\r\n\techo.  gettext    to make PO message catalogs\r\n\techo.  changes    to make an overview over all changed/added/deprecated items\r\n\techo.  xml        to make Docutils-native XML files\r\n\techo.  pseudoxml  to make pseudoxml-XML files for display purposes\r\n\techo.  linkcheck  to check all external links for integrity\r\n\techo.  doctest    to run all doctests embedded in the documentation if enabled\r\n\techo.  coverage   to run coverage check of the documentation if enabled\r\n\techo.  dummy      to check syntax errors of document sources\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"clean\" (\r\n\tfor /d %%i in (%BUILDDIR%\\*) do rmdir /q /s %%i\r\n\tdel /q /s %BUILDDIR%\\*\r\n\tgoto end\r\n)\r\n\r\n\r\nREM Check if sphinx-build is available and fallback to Python version if any\r\n%SPHINXBUILD% 1>NUL 2>NUL\r\nif errorlevel 9009 goto sphinx_python\r\ngoto sphinx_ok\r\n\r\n:sphinx_python\r\n\r\nset SPHINXBUILD=python -m sphinx.__init__\r\n%SPHINXBUILD% 2> nul\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.http://sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n:sphinx_ok\r\n\r\n\r\nif \"%1\" == \"html\" (\r\n\t%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The HTML pages are in %BUILDDIR%/html.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"dirhtml\" (\r\n\t%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"singlehtml\" (\r\n\t%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"pickle\" (\r\n\t%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; now you can process the pickle files.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"json\" (\r\n\t%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; now you can process the JSON files.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"htmlhelp\" (\r\n\t%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; now you can run HTML Help Workshop with the ^\r\n.hhp project file in %BUILDDIR%/htmlhelp.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"qthelp\" (\r\n\t%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; now you can run \"qcollectiongenerator\" with the ^\r\n.qhcp project file in %BUILDDIR%/qthelp, like this:\r\n\techo.^> qcollectiongenerator %BUILDDIR%\\qthelp\\python-slackclient.qhcp\r\n\techo.To view the help file:\r\n\techo.^> assistant -collectionFile %BUILDDIR%\\qthelp\\python-slackclient.ghc\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"devhelp\" (\r\n\t%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"epub\" (\r\n\t%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The epub file is in %BUILDDIR%/epub.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"epub3\" (\r\n\t%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The epub3 file is in %BUILDDIR%/epub3.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"latex\" (\r\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; the LaTeX files are in %BUILDDIR%/latex.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"latexpdf\" (\r\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\r\n\tcd %BUILDDIR%/latex\r\n\tmake all-pdf\r\n\tcd %~dp0\r\n\techo.\r\n\techo.Build finished; the PDF files are in %BUILDDIR%/latex.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"latexpdfja\" (\r\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\r\n\tcd %BUILDDIR%/latex\r\n\tmake all-pdf-ja\r\n\tcd %~dp0\r\n\techo.\r\n\techo.Build finished; the PDF files are in %BUILDDIR%/latex.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"text\" (\r\n\t%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The text files are in %BUILDDIR%/text.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"man\" (\r\n\t%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The manual pages are in %BUILDDIR%/man.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"texinfo\" (\r\n\t%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"gettext\" (\r\n\t%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The message catalogs are in %BUILDDIR%/locale.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"changes\" (\r\n\t%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.The overview file is in %BUILDDIR%/changes.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"linkcheck\" (\r\n\t%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Link check complete; look for any errors in the above output ^\r\nor in %BUILDDIR%/linkcheck/output.txt.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"doctest\" (\r\n\t%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Testing of doctests in the sources finished, look at the ^\r\nresults in %BUILDDIR%/doctest/output.txt.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"coverage\" (\r\n\t%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Testing of coverage in the sources finished, look at the ^\r\nresults in %BUILDDIR%/coverage/python.txt.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"xml\" (\r\n\t%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The XML files are in %BUILDDIR%/xml.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"pseudoxml\" (\r\n\t%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"dummy\" (\r\n\t%SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. Dummy builder generates no files.\r\n\tgoto end\r\n)\r\n\r\n:end\r\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst",
    "content": ".. Site settings\n.. |product_name| replace:: Slack Developer Kit for Python\n.. |email| replace:: opensource@slack.com\n.. |repo_name| replace:: python-slackclient\n.. |github_username| replace:: SlackAPI\n.. |twitter_username| replace:: SlackAPI\n\n.. _Bot Developer Hangout: https://dev4slack.slack.com/archives/sdk-python-slackclient\n.. _Issue Tracker: http://github.com/SlackAPI/python-slackclient/issues\n.. _pull request: http://github.com/SlackAPI/python-slackclient/pulls\n.. _Python RTMBot: https://slackapi.github.io/python-rtmbot\n.. _License: https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE\n.. _Code of Conduct: https://slackhq.github.io/code-of-conduct\n.. _Contributing: https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\n.. _contributing guidelines: https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md\n.. _Contributor License Agreement: https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform\n.. _Real Time Messaging (RTM) API: https://api.slack.com/rtm\n.. _Web API: https://api.slack.com/web\n\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst",
    "content": ".. _real-time-messaging:\n\n==============================================\nReal Time Messaging (RTM)\n==============================================\nThe `Real Time Messaging (RTM) API`_ is a WebSocket-based API that allows you to\nreceive events from Slack in real time and send messages as users.\n\nIf you prefer events to be pushed to you instead, we recommend using the\nHTTP-based `Events API <https://api.slack.com/events-api>`_ instead.\nMost event types supported by the RTM API are also available\nin `the Events API <https://api.slack.com/events/api>`_.\n\nSee :ref:`Tokens & Authentication <handling-tokens>` for API token handling best practices.\n\nConnecting to the RTM API\n------------------------------------------\n::\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  if sc.rtm_connect():\n    while sc.server.connected is True:\n          print sc.rtm_read()\n          time.sleep(1)\n  else:\n      print \"Connection Failed\"\n\nIf you connect successfully the first event received will be a hello:\n::\n\n  {\n    u'type': u'hello'\n  }\n\nIf there was a problem connecting an error will be returned, including a descriptive error message:\n::\n\n  {\n    u'type': u'error',\n      u'error': {\n      u'code': 1,\n      u'msg': u'Socket URL has expired'\n    }\n  }\n\nrtm.start vs rtm.connect\n---------------------------\n\nIf you expect your app to be used on large teams, we recommend starting the RTM client with `rtm.connect` rather than the default connection method for this client, `rtm.start`.\n`rtm.connect` provides a lighter initial connection payload, without the team's channel and user information included. You'll need to request channel and user info via\nthe Web API separately.\n\nTo do this, simply pass `with_team_state=False` into the `rtm_connect` call, like so:\n::\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  if sc.rtm_connect(with_team_state=False):\n      while True:\n          print sc.rtm_read()\n          time.sleep(1)\n  else:\n      print \"Connection Failed\"\n\n\nPassing `auto_reconnect=True` will tell the websocket client to automatically reconnect if the connection gets dropped.\n\n\nSee the `rtm.start docs <https://api.slack.com/methods/rtm.start>`_ and the `rtm.connect docs <https://api.slack.com/methods/rtm.connect>`_\nfor more details.\n\n\nRTM Events\n-------------\n::\n\n  {\n    u'type': u'message',\n    u'ts': u'1358878749.000002',\n    u'user': u'U023BECGF',\n    u'text': u'Hello'\n  }\n\nSee `RTM Events <https://api.slack.com/rtm#events>`_ for a complete list of events.\n\nSending messages via the RTM API\n---------------------------------\nYou can send a message to Slack by sending JSON over the websocket connection.\n\n::\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.rtm_send_message(\"welcome-test\", \"test\")\n\nYou can send a message to a private group or direct message channel in the same\nway, but using a Group ID (``C024BE91L``) or DM channel ID (``D024BE91L``).\n\nYou can send a message in reply to a thread using the ``thread`` argument, and\noptionally broadcast that message back to the channel by setting\n``reply_broadcast`` to ``True``.\n\n::\n\n  from slackclient import SlackClient\n\n  slack_token = os.environ[\"SLACK_API_TOKEN\"]\n  sc = SlackClient(slack_token)\n\n  sc.rtm_send_message(\"welcome-test\", \"test\", \"1482960137.003543\", True)\n\nSee `Threading messages <https://api.slack.com/docs/message-threading#threads_party>`_\nfor more details on using threads.\n\nThe RTM API only supports posting messages with `basic formatting <https://api.slack.com/docs/message-formatting>`_.\nIt does not support attachments or other message formatting modes.\n\n To post a more complex message as a user, see :ref:`Web API usage <web-api-examples>`.\n\n.. include:: metadata.rst\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs.sh",
    "content": "#!/usr/bin/env bash\n\nsphinx-build -c ./docs-src/_themes/slack/ -b html docs-src docs && touch ./docs/.nojekyll"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/requirements.txt",
    "content": "--index-url https://pypi.python.org/simple/\n\n-e .\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/setup.cfg",
    "content": "[metadata]\ndescription-file = README.rst\n\n[bdist_wheel]\nuniversal=1"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/setup.py",
    "content": "# -*- coding: utf-8 -*-\nfrom setuptools import setup, find_packages\nimport io\nimport os\nimport re\n\n\ndef read(*names, **kwargs):\n    with io.open(\n        os.path.join(os.path.dirname(__file__), *names),\n        encoding=kwargs.get(\"encoding\", \"utf8\")\n    ) as fp:\n        return fp.read()\n\n\nlong_description = read('README.rst')\n\n\ndef find_version(*file_paths):\n    version_file = read(*file_paths)\n    version_match = re.search(r\"^__version__ = ['\\\"]([^'\\\"]*)['\\\"]\", version_file, re.M)\n    if version_match:\n        return version_match.group(1)\n    raise RuntimeError(\"Unable to find version string.\")\n\n\nsetup(name='slackclient',\n      version=find_version('slackclient', 'version.py'),\n      description='Slack API clients for Web API and RTM API',\n      long_description=long_description,\n      url='https://github.com/slackapi/python-slackclient',\n      author='Slack Technologies, Inc.',\n      author_email='opensource@slack.com',\n      license='MIT',\n      classifiers=[\n            'Development Status :: 5 - Production/Stable',\n            'Intended Audience :: Developers',\n            'Topic :: Communications :: Chat',\n            'Topic :: System :: Networking',\n            'Topic :: Office/Business',\n            'License :: OSI Approved :: MIT License',\n            'Programming Language :: Python :: 2',\n            'Programming Language :: Python :: 2.7',\n            'Programming Language :: Python :: 3',\n            'Programming Language :: Python :: 3.4',\n            'Programming Language :: Python :: 3.5',\n            'Programming Language :: Python :: 3.6',\n      ],\n      keywords='slack slack-web slack-rtm chat chatbots bots chatops',\n      packages=find_packages(exclude=['docs', 'docs-src', 'tests']),\n      install_requires=[\n          'websocket-client >=0.35, <0.55.0',\n          'requests >=2.11, <3.0a0',\n          'six >=1.10, <2.0a0',\n      ])\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/__init__.py",
    "content": "from .client import SlackClient # noqa\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/channel.py",
    "content": "class Channel(object):\n    '''\n    A Channel represents a public or private Slack Channel instance\n    '''\n    def __init__(self, server, name, channel_id, members=None):\n        self.server = server\n        self.name = name\n        self.id = channel_id\n        self.members = [] if members is None else members\n\n    def __eq__(self, compare_str):\n        if self.name == compare_str or \"#\" + self.name == compare_str or self.id == compare_str:\n            return True\n        else:\n            return False\n\n    def __hash__(self):\n        return hash(self.id)\n\n    def __str__(self):\n        data = \"\"\n        for key in list(self.__dict__.keys()):\n            data += \"{0} : {1}\\n\".format(key, str(self.__dict__[key])[:40])\n        return data\n\n    def __repr__(self):\n        return self.__str__()\n\n    def send_message(self, message, thread=None, reply_broadcast=False):\n        '''\n        Sends a message to a this Channel.\n\n        Include the parent message's thread_ts value in `thread`\n        to send to a thread.\n\n        :Args:\n            message (message) - the string you'd like to send to the channel\n            thread (str or None) - the parent message ID, if sending to a\n                thread\n            reply_broadcast (bool) - if messaging a thread, whether to\n                also send the message back to the channel\n\n        :Returns:\n            None\n        '''\n        self.server.rtm_send_message(self.id, message, thread, reply_broadcast)\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/client.py",
    "content": "#!/usr/bin/python\n# mostly a proxy object to abstract how some of this works\n\nimport json\nimport logging\nimport time\n\nfrom .server import Server\nfrom .exceptions import ParseResponseError, TokenRefreshError\n\n\nLOG = logging.getLogger(__name__)\n\n\nclass SlackClient(object):\n    \"\"\"\n    The SlackClient makes API Calls to the `Slack Web API <https://api.slack.com/web>`_ as well as\n    managing connections to the `Real-time Messaging API via websocket <https://api.slack.com/rtm>`_\n\n    It also manages some of the Client state for Channels that the associated token (User or Bot)\n    is associated with.\n\n    For more information, check out the `Slack API Docs <https://api.slack.com/>`_\n    \"\"\"\n\n    def __init__(\n        self,\n        token=None,\n        refresh_token=None,\n        token_update_callback=None,\n        client_id=None,\n        client_secret=None,\n        proxies=None,\n        **kwargs\n    ):\n        \"\"\"\n        Init:\n            :Args:\n                token (str): Your Slack Authentication token. You can find or generate a test token\n                `here <https://api.slack.com/docs/oauth-test-tokens>`_\n                Note: Be `careful with your token <https://api.slack.com/docs/oauth-safety>`_\n                proxies (dict): Proxies to use when create websocket or api calls,\n                declare http and websocket proxies using {'http': 'http://127.0.0.1'},\n                and https proxy using {'https': 'https://127.0.0.1:443'}\n                refresh_token (str): Your Slack app's refresh token. This token is used to\n                update your app's OAuth access token\n                client_id (str): Your app's Client ID\n                client_secret (srt): Your app's Client Secret (Used for OAuth requests)\n                refresh_callback (function): Your application's function for updating Slack\n                OAuth tokens inside your data store\n        \"\"\"\n\n        self.client_id = client_id\n        self.client_secret = client_secret\n        self.refresh_token = refresh_token\n        self.token_update_callback = token_update_callback\n        self.token = token\n        self.access_token_expires_at = 0\n\n        if refresh_token:\n            if callable(token_update_callback):\n                self.server = Server(\n                    connect=False,\n                    proxies=proxies,\n                    refresh_token=refresh_token,\n                    client_id=client_id,\n                    client_secret=client_secret,\n                    token_update_callback=token_update_callback,\n                )\n            else:\n                raise TokenRefreshError(\n                    \"Token refresh callback function is required when using refresh token.\"\n                )\n        else:\n            # Slack app configs\n            self.server = Server(token=token, connect=False, proxies=proxies)\n\n    def refresh_access_token(self):\n        \"\"\"\n        Refresh the client's OAUth access tokens\n        https://api.slack.com/docs/rotating-and-refreshing-credentials\n        \"\"\"\n        post_data = {\n            \"refresh_token\": self.refresh_token,\n            \"grant_type\": \"refresh_token\",\n            \"client_id\": self.client_id,\n            \"client_secret\": self.client_secret,\n        }\n        response = self.server.api_requester.post_http_request(\n            self.refresh_token, api_method=\"oauth.access\", post_data=post_data\n        )\n        response_json = json.loads(response.text)\n\n        # If Slack returned an updated access token, update the client, otherwise\n        # raise TokenRefreshError exception with the error returned from the API\n        if response_json[\"ok\"]:\n            # Update the client's access token and expiration timestamp\n            self.team_id = response_json[\"team_id\"]\n            # TODO: Minimize the numer of places token is stored.\n            self.token = response_json[\"access_token\"]\n            self.server.token = response_json[\"access_token\"]\n\n            # Update the token expiration timestamp\n            current_ts = int(time.time())\n            expires_at = int(current_ts + response_json[\"expires_in\"])\n            self.access_token_expires_at = expires_at\n            # Call the developer's token update callback\n            update_args = {\n                \"enterprise_id\": response_json[\"enterprise_id\"],\n                \"team_id\": response_json[\"team_id\"],\n                \"access_token\": response_json[\"access_token\"],\n                \"expires_in\": response_json[\"expires_in\"],\n            }\n            self.token_update_callback(update_args)\n        else:\n            raise TokenRefreshError(\"Token refresh failed\")\n\n    def append_user_agent(self, name, version):\n        self.server.append_user_agent(name, version)\n\n    def rtm_connect(self, with_team_state=True, **kwargs):\n        \"\"\"\n        Connects to the RTM Websocket\n\n        :Args:\n            with_team_state (bool): Connect via `rtm.start` to pull workspace state information.\n            `False` connects via `rtm.connect`, which is lighter weight and better for very large\n            teams.\n\n        :Returns:\n            False on exceptions\n        \"\"\"\n\n        if self.refresh_token:\n            raise TokenRefreshError(\n                \"Workspace tokens may not be used to connect to the RTM API.\"\n            )\n\n        try:\n            self.server.rtm_connect(use_rtm_start=with_team_state, **kwargs)\n            return self.server.connected\n        except Exception:\n            LOG.warn(\"Failed RTM connect\", exc_info=True)\n            return False\n\n    def api_call(self, method, timeout=None, **kwargs):\n        \"\"\"\n        Call the Slack Web API as documented here: https://api.slack.com/web\n\n        :Args:\n            method (str): The API Method to call. See\n            `the full list here <https://api.slack.com/methods>`_\n        :Kwargs:\n            (optional) kwargs: any arguments passed here will be bundled and sent to the api\n            requester as post_data and will be passed along to the API.\n\n            Example::\n\n                sc.api_call(\n                    \"channels.setPurpose\",\n                    channel=\"CABC12345\",\n                    purpose=\"Writing some code!\"\n                )\n\n        :Returns:\n            str -- returns the text of the HTTP response.\n\n            Examples::\n\n                u'{\"ok\":true,\"purpose\":\"Testing bots\"}'\n                or\n                u'{\"ok\":false,\"error\":\"channel_not_found\"}'\n\n            See here for more information on responses: https://api.slack.com/web\n        \"\"\"\n        # Check for missing or expired access token before submitting the request\n        if method != \"oauth.access\" and self.refresh_token:\n            current_ts = int(time.time())\n            token_is_expired = current_ts > self.access_token_expires_at\n            if token_is_expired or self.token is None:\n                self.refresh_access_token()\n\n        response_body = self.server.api_call(\n            self.token, request=method, timeout=timeout, **kwargs\n        )\n\n        # Attempt to parse the response as JSON\n        try:\n            result = json.loads(response_body)\n        except ValueError as json_decode_error:\n            raise ParseResponseError(response_body, json_decode_error)\n        response_json = json.loads(response_body)\n\n        if result.get(\"ok\", False):\n            if method == \"im.open\":\n                self.server.attach_channel(kwargs[\"user\"], result[\"channel\"][\"id\"])\n            elif method in (\"mpim.open\", \"groups.create\", \"groups.createchild\"):\n                self.server.parse_channel_data([result[\"group\"]])\n            elif method in (\"channels.create\", \"channels.join\"):\n                self.server.parse_channel_data([result[\"channel\"]])\n        else:\n            # if the API request returns an invalid_auth error, refresh the token and try again\n            if (\n                self.refresh_token\n                and \"error\" in response_json\n                and response_json[\"error\"] == \"invalid_auth\"\n            ):\n                self.refresh_access_token()\n                # If token refresh was successful, retry the original API request\n                return self.api_call(method, timeout, **kwargs)\n        return result\n\n    def rtm_read(self):\n        \"\"\"\n        Reads from the RTM Websocket stream then calls `self.process_changes(item)` for each line\n        in the returned data.\n\n        Multiple events may be returned, always returns a list [], which is empty if there are no\n        incoming messages.\n\n        :Args:\n            None\n\n        :Returns:\n            data (json) - The server response. For example::\n\n                [{u'presence': u'active', u'type': u'presence_change', u'user': u'UABC1234'}]\n\n        :Raises:\n            SlackNotConnected if self.server is not defined.\n        \"\"\"\n        # in the future, this should handle some events internally i.e. channel\n        # creation\n        if self.server:\n            json_data = self.server.websocket_safe_read()\n            data = []\n            if json_data != \"\":\n                for d in json_data.split(\"\\n\"):\n                    data.append(json.loads(d))\n            for item in data:\n                self.process_changes(item)\n            return data\n        else:\n            raise SlackNotConnected\n\n    def rtm_send_message(self, channel, message, thread=None, reply_broadcast=None):\n        \"\"\"\n        Sends a message to a given channel.\n\n        :Args:\n            channel (str) - the string identifier for a channel or channel name (e.g. 'C1234ABC',\n            'bot-test' or '#bot-test')\n            message (message) - the string you'd like to send to the channel\n            thread (str or None) - the parent message ID, if sending to a\n                thread\n            reply_broadcast (bool) - if messaging a thread, whether to\n                also send the message back to the channel\n\n        :Returns:\n            None\n\n        \"\"\"\n        # The `channel` argument can be a channel name or an ID. At first its assumed to be a\n        # name and an attempt is made to find the ID in the workspace state cache.\n        # If that lookup fails, the argument is used as the channel ID.\n        found_channel = self.server.channels.find(channel)\n        channel_id = found_channel.id if found_channel else channel\n        return self.server.rtm_send_message(\n            channel_id, message, thread, reply_broadcast\n        )\n\n    def process_changes(self, data):\n        \"\"\"\n        Internal method which processes RTM events and modifies the local data store\n        accordingly.\n\n        Stores new channels when joining a group (Multi-party DM), IM (DM) or channel.\n\n        Stores user data on a team join event.\n        \"\"\"\n        if \"type\" in data.keys():\n            if data[\"type\"] in (\"channel_created\", \"group_joined\"):\n                channel = data[\"channel\"]\n                self.server.attach_channel(channel[\"name\"], channel[\"id\"], [])\n            if data[\"type\"] == \"im_created\":\n                channel = data[\"channel\"]\n                self.server.attach_channel(channel[\"user\"], channel[\"id\"], [])\n            if data[\"type\"] == \"team_join\":\n                user = data[\"user\"]\n                self.server.parse_user_data([user])\n            pass\n\n\nclass SlackNotConnected(Exception):\n    pass\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/exceptions.py",
    "content": "class SlackClientError(Exception):\n    \"\"\"\n    Base exception for all errors raised by the SlackClient library\n    \"\"\"\n    def __init__(self, msg=None):\n        if msg is None:\n            # default error message\n            msg = \"An error occurred in the SlackClient library\"\n        super(SlackClientError, self).__init__(msg)\n\n\nclass ParseResponseError(SlackClientError, ValueError):\n    \"\"\"\n    Error raised when responses to Web API methods cannot be parsed as valid JSON\n    \"\"\"\n    def __init__(self, response_body, original_exception):\n        super(ParseResponseError, self).__init__(\n            \"Slack API response body could not be parsed: {0}. Original exception: {1}\".format(\n                response_body, original_exception\n            )\n        )\n        self.response_body = response_body\n        self.original_exception = original_exception\n\n\nclass TokenRefreshError(SlackClientError):\n    \"\"\"\n    This exception is rasied when a token related error occurs within the client\n    \"\"\"\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/im.py",
    "content": "class Im(object):\n    '''\n    IMs represent direct message channels between two users on Slack.\n    '''\n    def __init__(self, server, user, im_id):\n        self.server = server\n        self.user = user\n        self.id = im_id\n\n    def __eq__(self, compare_str):\n        if self.id == compare_str or self.user == compare_str:\n            return True\n        else:\n            return False\n\n    def __hash__(self):\n        return hash(self.id)\n\n    def __str__(self):\n        data = \"\"\n        for key in list(self.__dict__.keys()):\n            if key != \"server\":\n                data += \"{0} : {1}\\n\".format(key, str(self.__dict__[key])[:40])\n        return data\n\n    def __repr__(self):\n        return self.__str__()\n\n    def send_message(self, message):\n        '''\n        Sends a message to a this IM (or DM depending on your preferred terminology).\n\n        :Args:\n            message (message) - the string you'd like to send to the IM\n\n        :Returns:\n            None\n        '''\n        message_json = {\"type\": \"message\", \"channel\": self.id, \"text\": message}\n        self.server.send_to_websocket(message_json)\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/server.py",
    "content": "from .channel import Channel\nfrom .exceptions import SlackClientError\nfrom .slackrequest import SlackRequest\nfrom .user import User\nfrom .util import SearchList, SearchDict\n\nimport json\nimport logging\nimport time\nimport random\n\nfrom requests.packages.urllib3.util.url import parse_url\nfrom ssl import SSLError\nfrom websocket import create_connection\nfrom websocket._exceptions import WebSocketConnectionClosedException\n\n\nclass Server(object):\n    \"\"\"\n    The Server object owns the websocket connection and all attached channel information.\n    \"\"\"\n\n    def __init__(self, token=None, connect=True, proxies=None, **kwargs):\n        # Slack app configs\n        self.token = token\n\n        # api configs\n        self.proxies = proxies\n\n        # HTTP Request handler\n        self.api_requester = SlackRequest(proxies=proxies)\n\n        # Workspace metadata\n        self.username = None\n        self.domain = None\n        self.login_data = None\n        self.users = SearchDict()\n        self.channels = SearchList()\n\n        # RTM configs\n        self.websocket = None\n        self.ws_url = None\n        self.connected = False\n        self.auto_reconnect = False\n        self.last_connected_at = 0\n        self.reconnect_count = 0\n        self.rtm_connect_retries = 0\n\n        # Connect to RTM on load\n        if connect:\n            self.rtm_connect()\n\n    def __eq__(self, compare_str):\n        if compare_str == self.domain or compare_str == self.token:\n            return True\n        else:\n            return False\n\n    def __hash__(self):\n        return hash(self.token)\n\n    def __str__(self):\n        \"\"\"\n        Example Output::\n\n        username : None\n        domain : None\n        websocket : None\n        users : []\n        login_data : None\n        api_requester : <slackclient.slackrequest.SlackRequest\n        channels : []\n        token : xoxb-asdlfkyadsofii7asdf734lkasdjfllakjba7zbu\n        connected : False\n        ws_url : None\n        \"\"\"\n        data = \"\"\n        for key in list(self.__dict__.keys()):\n            data += \"{} : {}\\n\".format(key, str(self.__dict__[key])[:40])\n        return data\n\n    def __repr__(self):\n        return self.__str__()\n\n    def append_user_agent(self, name, version):\n        self.api_requester.append_user_agent(name, version)\n\n    def rtm_connect(self, reconnect=False, timeout=None, use_rtm_start=True, **kwargs):\n        \"\"\"\n        Connects to the RTM API - https://api.slack.com/rtm\n\n        If `auto_reconnect` is set to `True` then the SlackClient is initialized, this method\n        will be used to reconnect on websocket read failures, which indicate disconnection\n\n        :Args:\n            reconnect (boolean) Whether this method is being called to reconnect to RTM\n            timeout (int): Stop waiting for Web API response after this many seconds\n            use_rtm_start (boolean): `True` to connect using `rtm.start` or\n            `False` to connect using`rtm.connect`\n            https://api.slack.com/rtm#connecting_with_rtm.connect_vs._rtm.start\n\n        :Returns:\n            None\n\n        \"\"\"\n        # rtm.start returns user and channel info, rtm.connect does not.\n        connect_method = \"rtm.start\" if use_rtm_start else \"rtm.connect\"\n\n        # If the `auto_reconnect` param was passed, set the server's `auto_reconnect` attr\n        if \"auto_reconnect\" in kwargs:\n            self.auto_reconnect = kwargs[\"auto_reconnect\"]\n\n        # If this is an auto reconnect, rate limit reconnect attempts\n        if self.auto_reconnect and reconnect:\n            # Raise a SlackConnectionError after 5 retries within 3 minutes\n            recon_count = self.reconnect_count\n            if recon_count == 5:\n                logging.error(\"RTM connection failed, reached max reconnects.\")\n                raise SlackConnectionError(\n                    \"RTM connection failed, reached max reconnects.\"\n                )\n            # Wait to reconnect if the last reconnect was less than 3 minutes ago\n            if (time.time() - self.last_connected_at) < 180:\n                if recon_count > 0:\n                    # Back off after the the first attempt\n                    backoff_offset_multiplier = random.randint(1, 4)\n                    retry_timeout = (\n                        backoff_offset_multiplier * recon_count * recon_count\n                    )\n                    logging.debug(\"Reconnecting in %d seconds\", retry_timeout)\n\n                    time.sleep(retry_timeout)\n                self.reconnect_count += 1\n            else:\n                self.reconnect_count = 0\n\n        reply = self.api_requester.do(\n            self.token, connect_method, post_data=kwargs, timeout=timeout\n        )\n\n        if reply.status_code != 200:\n            if self.rtm_connect_retries < 5 and reply.status_code == 429:\n                self.rtm_connect_retries += 1\n                retry_after = int(reply.headers.get(\"retry-after\", 120))\n                logging.debug(\n                    \"HTTP 429: Rate limited. Retrying in %d seconds\", retry_after\n                )\n                time.sleep(retry_after)\n                self.rtm_connect(\n                    reconnect=reconnect,\n                    timeout=timeout,\n                    use_rtm_start=use_rtm_start,\n                    **kwargs\n                )\n            else:\n                raise SlackConnectionError(\n                    \"RTM connection attempt was rate limited 5 times.\"\n                )\n        else:\n            self.rtm_connect_retries = 0\n            login_data = reply.json()\n            if login_data[\"ok\"]:\n                self.ws_url = login_data[\"url\"]\n                self.connect_slack_websocket(self.ws_url)\n                if not reconnect:\n                    self.parse_slack_login_data(login_data, use_rtm_start)\n            else:\n                raise SlackLoginError(reply=reply)\n\n    def parse_slack_login_data(self, login_data, use_rtm_start):\n        self.login_data = login_data\n        self.domain = self.login_data[\"team\"][\"domain\"]\n        self.username = self.login_data[\"self\"][\"name\"]\n\n        # if the connection was made via rtm.start, update the server's state\n        if use_rtm_start:\n            self.parse_channel_data(login_data[\"channels\"])\n            self.parse_channel_data(login_data[\"groups\"])\n            self.parse_user_data(login_data[\"users\"])\n            self.parse_channel_data(login_data[\"ims\"])\n\n    def connect_slack_websocket(self, ws_url):\n        \"\"\"Uses http proxy if available\"\"\"\n        if self.proxies and \"http\" in self.proxies:\n            parts = parse_url(self.proxies[\"http\"])\n            proxy_host, proxy_port = parts.host, parts.port\n            auth = parts.auth\n            proxy_auth = auth and auth.split(\":\")\n        else:\n            proxy_auth, proxy_port, proxy_host = None, None, None\n\n        try:\n            self.websocket = create_connection(\n                ws_url,\n                http_proxy_host=proxy_host,\n                http_proxy_port=proxy_port,\n                http_proxy_auth=proxy_auth,\n            )\n            self.connected = True\n            self.last_connected_at = time.time()\n            logging.debug(\"RTM connected\")\n            self.websocket.sock.setblocking(0)\n        except Exception as e:\n            self.connected = False\n            raise SlackConnectionError(message=str(e))\n\n    def parse_channel_data(self, channel_data):\n        for channel in channel_data:\n            if \"name\" not in channel:\n                channel[\"name\"] = channel[\"id\"]\n            if \"members\" not in channel:\n                channel[\"members\"] = []\n            self.attach_channel(channel[\"name\"], channel[\"id\"], channel[\"members\"])\n\n    def parse_user_data(self, user_data):\n        for user in user_data:\n            if \"tz\" not in user:\n                user[\"tz\"] = \"unknown\"\n            if \"real_name\" not in user:\n                user[\"real_name\"] = user[\"name\"]\n            if \"email\" not in user[\"profile\"]:\n                user[\"profile\"][\"email\"] = \"\"\n            self.attach_user(\n                user[\"name\"],\n                user[\"id\"],\n                user[\"real_name\"],\n                user[\"tz\"],\n                user[\"profile\"][\"email\"],\n            )\n\n    def send_to_websocket(self, data):\n        \"\"\"\n        Send a JSON message directly to the websocket. See\n        `RTM documentation <https://api.slack.com/rtm` for allowed types.\n\n        :Args:\n            data (dict) the key/values to send the websocket.\n\n        \"\"\"\n        try:\n            data = json.dumps(data)\n            self.websocket.send(data)\n        except Exception:\n            self.rtm_connect(reconnect=True, use_rtm_start=False)\n\n    def rtm_send_message(self, channel, message, thread=None, reply_broadcast=None):\n        \"\"\"\n        Sends a message to a given channel.\n\n        :Args:\n            channel (str) - the string identifier for a channel or channel name (e.g. 'C1234ABC',\n            'bot-test' or '#bot-test')\n            message (message) - the string you'd like to send to the channel\n            thread (str or None) - the parent message ID, if sending to a\n                thread\n            reply_broadcast (bool) - if messaging a thread, whether to\n                also send the message back to the channel\n\n        :Returns:\n            None\n\n        \"\"\"\n        message_json = {\"type\": \"message\", \"channel\": channel, \"text\": message}\n        if thread is not None:\n            message_json[\"thread_ts\"] = thread\n            if reply_broadcast:\n                message_json[\"reply_broadcast\"] = True\n\n        self.send_to_websocket(message_json)\n\n    def ping(self):\n        return self.send_to_websocket({\"type\": \"ping\"})\n\n    def websocket_safe_read(self):\n        \"\"\"\n        Returns data if available, otherwise ''. Newlines indicate multiple\n        messages\n        \"\"\"\n\n        data = \"\"\n        while True:\n            try:\n                data += \"{0}\\n\".format(self.websocket.recv())\n            except SSLError as e:\n                if e.errno == 2:\n                    # errno 2 occurs when trying to read or write data, but more\n                    # data needs to be received on the underlying TCP transport\n                    # before the request can be fulfilled.\n                    #\n                    # Python 2.7.9+ and Python 3.3+ give this its own exception,\n                    # SSLWantReadError\n                    return \"\"\n                raise\n            except WebSocketConnectionClosedException:\n                logging.debug(\"RTM disconnected\")\n                self.connected = False\n                if self.auto_reconnect:\n                    self.rtm_connect(reconnect=True, use_rtm_start=False)\n                else:\n                    raise SlackConnectionError(\n                        \"Unable to send due to closed RTM websocket\"\n                    )\n            return data.rstrip()\n\n    def attach_user(self, name, user_id, real_name, tz, email):\n        self.users.update({user_id: User(self, name, user_id, real_name, tz, email)})\n\n    def attach_channel(self, name, channel_id, members=None):\n        if members is None:\n            members = []\n        if self.channels.find(channel_id) is None:\n            self.channels.append(Channel(self, name, channel_id, members))\n\n    def join_channel(self, name, timeout=None):\n        \"\"\"\n        Join a channel by name.\n\n        Note: this action is not allowed by bots, they must be invited to channels.\n        \"\"\"\n        response = self.api_call(\"channels.join\", channel=name, timeout=timeout)\n        return response\n\n    def api_call(self, token, request=\"?\", timeout=None, **kwargs):\n        \"\"\"\n        Call the Slack Web API as documented here: https://api.slack.com/web\n\n        :Args:\n            method (str): The API Method to call. See here for a list: https://api.slack.com/methods\n        :Kwargs:\n            (optional) timeout: stop waiting for a response after a given number of seconds\n            (optional) kwargs: any arguments passed here will be bundled and sent to the api\n            requester as post_data\n                and will be passed along to the API.\n\n        Example::\n\n            sc.server.api_call(\n                \"channels.setPurpose\",\n                channel=\"CABC12345\",\n                purpose=\"Writing some code!\"\n            )\n\n        Returns:\n            str -- returns HTTP response text and headers as JSON.\n\n            Examples::\n\n                u'{\"ok\":true,\"purpose\":\"Testing bots\"}'\n                or\n                u'{\"ok\":false,\"error\":\"channel_not_found\"}'\n\n            See here for more information on responses: https://api.slack.com/web\n        \"\"\"\n        response = self.api_requester.do(token, request, kwargs, timeout=timeout)\n        response_json = {}\n        resp_text = response.text\n        if resp_text:\n            response_json = json.loads(resp_text)\n        response_json[\"headers\"] = dict(response.headers)\n        return json.dumps(response_json)\n\n\n# TODO: Move the error types defined below into the .exceptions namespace. This would be a semver\n# major change because any clients already referencing these types in order to catch them\n# specifically would need to deal with the symbol names changing.\n\n\nclass SlackConnectionError(SlackClientError):\n    def __init__(self, message=\"\", reply=None):\n        super(SlackConnectionError, self).__init__(message)\n        self.reply = reply\n\n\nclass SlackLoginError(SlackClientError):\n    def __init__(self, message=\"\", reply=None):\n        super(SlackLoginError, self).__init__(message)\n        self.reply = reply\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/slackrequest.py",
    "content": "import json\nimport platform\nimport requests\nimport six\nimport sys\n\nfrom .version import __version__\n\n\nclass SlackRequest(object):\n    def __init__(\n            self,\n            proxies=None\n    ):\n        # HTTP configs\n        self.custom_user_agent = None\n        self.proxies = proxies\n\n        # Construct the user-agent header with the package info, Python version and OS version.\n        self.default_user_agent = {\n            # __name__ returns all classes, we only want the client\n            \"client\": \"{0}/{1}\".format(__name__.split('.')[0], __version__),\n            \"python\": \"Python/{v.major}.{v.minor}.{v.micro}\".format(v=sys.version_info),\n            \"system\": \"{0}/{1}\".format(platform.system(), platform.release())\n        }\n\n    def get_user_agent(self):\n        # Check for custom user-agent and append if found\n        if self.custom_user_agent:\n            custom_ua_list = [\"/\".join(client_info) for client_info in self.custom_user_agent]\n            custom_ua_string = \" \".join(custom_ua_list)\n            self.default_user_agent['custom'] = custom_ua_string\n\n        # Concatenate and format the user-agent string to be passed into request headers\n        ua_string = []\n        for key, val in self.default_user_agent.items():\n            ua_string.append(val)\n\n        user_agent_string = \" \".join(ua_string)\n        return user_agent_string\n\n    def append_user_agent(self, name, version):\n        if self.custom_user_agent:\n            self.custom_user_agent.append([name.replace(\"/\", \":\"), version.replace(\"/\", \":\")])\n        else:\n            self.custom_user_agent = [[name, version]]\n\n    def do(self, token=None, request=\"?\", post_data=None, domain=\"slack.com\", timeout=None):\n        \"\"\"\n        Perform a POST request to the Slack Web API\n        Args:\n            token (str): your authentication token\n            request (str): the method to call from the Slack API. For example: 'channels.list'\n            post_data (dict): key/value arguments to pass for the request. For example:\n                {'channel': 'CABC12345'}\n            domain (str): if for some reason you want to send your request to something other\n                than slack.com\n            timeout (float): stop waiting for a response after a given number of seconds\n        \"\"\"\n        # Pull `file` out so it isn't JSON encoded like normal fields.\n        # Only do this for requests that are UPLOADING files; downloading files\n        # use the 'file' argument to point to a File ID.\n        post_data = post_data or {}\n\n        # Move singular file objects into `files`\n        upload_requests = ['files.upload']\n\n        # Move file content into requests' `files` param\n        files = None\n        if request in upload_requests:\n            files = {'file': post_data.pop('file')} if 'file' in post_data else None\n\n        # Check for plural fields and convert them to comma-separated strings if needed\n        for field in {'channels', 'users', 'types'} & set(post_data.keys()):\n            if isinstance(post_data[field], list):\n                post_data[field] = \",\".join(post_data[field])\n\n        # Convert any params which are list-like to JSON strings\n        # Example: `attachments` is a dict, and needs to be passed as JSON\n        for k, v in six.iteritems(post_data):\n            if isinstance(v, (list, dict)):\n                post_data[k] = json.dumps(v)\n\n        return self.post_http_request(token, request, post_data, files, timeout, domain)\n\n    def post_http_request(self, token, api_method, post_data,\n                          files=None, timeout=None, domain=\"slack.com\"):\n        \"\"\"\n        This method build and submits the Web API HTTP request\n\n        :param token: You app's Slack access token\n        :param api_method: The API method endpoint to submit the request to\n        :param post_data: The request payload\n        :param domain: The URL to submit the API request to\n        :param files: Any files to be submitted during upload calls\n        :param timeout: Stop waiting for a response after a given number of seconds\n        :return:\n        \"\"\"\n        # Override token header if `token` is passed in post_data\n        if post_data is not None and \"token\" in post_data:\n            token = post_data['token']\n\n        # Set user-agent and auth headers\n        headers = {\n            'user-agent': self.get_user_agent(),\n            'Authorization': 'Bearer {}'.format(token)\n        }\n\n        # Submit the request\n        res = requests.post(\n            'https://{0}/api/{1}'.format(domain, api_method),\n            headers=headers,\n            data=post_data,\n            files=files,\n            timeout=timeout,\n            proxies=self.proxies\n        )\n        return res\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/user.py",
    "content": "class User(object):\n    def __init__(self, server, name, user_id, real_name, tz, email):\n        self.tz = tz\n        self.name = name\n        self.real_name = real_name\n        self.server = server\n        self.id = user_id\n        self.email = email\n\n    def __eq__(self, compare_str):\n        if compare_str in (self.id, self.name):\n            return True\n        else:\n            return False\n\n    def __hash__(self):\n        return hash(self.id)\n\n    def __str__(self):\n        data = \"\"\n        for key in list(self.__dict__.keys()):\n            if key != \"server\":\n                data += \"{0} : {1}\\n\".format(key, str(self.__dict__[key])[:40])\n        return data\n\n    def __repr__(self):\n        return self.__str__()\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/util.py",
    "content": "class SearchList(list):\n\n    def find(self, name):\n        items = []\n        for child in self:\n            if child.__class__ == self.__class__:\n                items += child.find(name)\n            else:\n                if child == name:\n                    items.append(child)\n\n        if len(items) == 1:\n            return items[0]\n        elif items:\n            return items\n        else:\n            return None\n\n\nclass SearchDict(dict):\n    def find(self, search_string):\n        # Find the user by ID\n        user = self.get(search_string)\n        if user:\n            return user\n        else:\n            # If the user can't be found by ID, try searching by name\n            for id, user in self.items():\n                if str(user.name) == search_string:\n                    return user\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/version.py",
    "content": "# see: http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers\n__version__ = \"1.3.2\"\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/test_requirements.txt",
    "content": "pytest<4\ncodecov\ncoverage\nmock\npytest-cov>=2.4.0,<2.6\npytest-mock\nresponses\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/conftest.py",
    "content": "import pytest\nimport requests\nfrom slackclient.channel import Channel\nfrom slackclient.server import Server\nfrom slackclient.client import SlackClient\n\n\n# This is so that tests work on Travis for python 2.6, it's really hacky, but expedient\ndef get_unverified_post():\n    requests_post = requests.post\n\n    def unverified_post(*args, **kwargs):\n        # don't throw SSL errors plz\n        kwargs['verify'] = False\n        return requests_post(*args, **kwargs)\n\n    return unverified_post\n\n\nrequests.post = get_unverified_post()\n\n\n@pytest.fixture\ndef server(monkeypatch):\n    my_server = Server(token='xoxp-1234123412341234-12341234-1234', connect=False)\n    return my_server\n\n\n@pytest.fixture\ndef slackclient():\n    my_slackclient = SlackClient('xoxp-1234123412341234-12341234-1234')\n    return my_slackclient\n\n\n@pytest.fixture\ndef channel(server):\n    my_channel = Channel(server, \"somechannel\", \"C12341234\", [\"user\"])\n    return my_channel\n\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/channel.created.json",
    "content": "{\n    \"type\": \"channel_created\",\n    \"channel\": {\n        \"id\": \"C024BE91L\",\n        \"name\": \"fun\",\n        \"created\": 1360782804,\n        \"creator\": \"U024BE7LH\"\n    }\n}\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/im.created.json",
    "content": "{\n    \"type\": \"im_created\",\n    \"user\": \"U024BE7LH\",\n    \"channel\": {\n        \"id\": \"D024BE91L\",\n        \"user\": \"U123BL234\",\n        \"created\": 1360782804\n    }\n}\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/rtm.start.json",
    "content": "{\n    \"ok\": true,\n    \"self\": {\n        \"id\": \"U10CX1234\",\n        \"name\": \"fakeuser\",\n        \"prefs\": {\n            \"highlight_words\": \"\",\n            \"user_colors\": \"\",\n            \"color_names_in_list\": true,\n            \"growls_enabled\": true,\n            \"tz\": \"America\\/Los_Angeles\",\n            \"push_dm_alert\": true,\n            \"push_mention_alert\": true,\n            \"push_everything\": true,\n            \"push_idle_wait\": 2,\n            \"push_sound\": \"b2.mp3\",\n            \"push_loud_channels\": \"\",\n            \"push_mention_channels\": \"\",\n            \"push_loud_channels_set\": \"\",\n            \"email_alerts\": \"instant\",\n            \"email_alerts_sleep_until\": 0,\n            \"email_misc\": true,\n            \"email_weekly\": true,\n            \"welcome_message_hidden\": false,\n            \"all_channels_loud\": true,\n            \"loud_channels\": \"\",\n            \"never_channels\": \"\",\n            \"loud_channels_set\": \"\",\n            \"show_member_presence\": true,\n            \"search_sort\": \"timestamp\",\n            \"expand_inline_imgs\": true,\n            \"expand_internal_inline_imgs\": true,\n            \"expand_snippets\": false,\n            \"posts_formatting_guide\": true,\n            \"seen_welcome_2\": true,\n            \"seen_ssb_prompt\": false,\n            \"search_only_my_channels\": false,\n            \"emoji_mode\": \"default\",\n            \"has_invited\": false,\n            \"has_uploaded\": false,\n            \"has_created_channel\": false,\n            \"search_exclude_channels\": \"\",\n            \"messages_theme\": \"default\",\n            \"webapp_spellcheck\": true,\n            \"no_joined_overlays\": false,\n            \"no_created_overlays\": false,\n            \"dropbox_enabled\": false,\n            \"seen_user_menu_tip_card\": true,\n            \"seen_team_menu_tip_card\": true,\n            \"seen_channel_menu_tip_card\": true,\n            \"seen_message_input_tip_card\": true,\n            \"seen_channels_tip_card\": true,\n            \"seen_domain_invite_reminder\": false,\n            \"seen_member_invite_reminder\": false,\n            \"seen_flexpane_tip_card\": true,\n            \"seen_search_input_tip_card\": true,\n            \"mute_sounds\": false,\n            \"arrow_history\": false,\n            \"tab_ui_return_selects\": true,\n            \"obey_inline_img_limit\": true,\n            \"new_msg_snd\": \"knock_brush.mp3\",\n            \"collapsible\": false,\n            \"collapsible_by_click\": true,\n            \"require_at\": false,\n            \"mac_ssb_bounce\": \"\",\n            \"mac_ssb_bullet\": true,\n            \"expand_non_media_attachments\": true,\n            \"show_typing\": true,\n            \"pagekeys_handled\": true,\n            \"last_snippet_type\": \"\",\n            \"display_real_names_override\": 0,\n            \"time24\": false,\n            \"enter_is_special_in_tbt\": false,\n            \"graphic_emoticons\": false,\n            \"convert_emoticons\": true,\n            \"autoplay_chat_sounds\": true,\n            \"ss_emojis\": true,\n            \"sidebar_behavior\": \"\",\n            \"mark_msgs_read_immediately\": true,\n            \"start_scroll_at_oldest\": true,\n            \"snippet_editor_wrap_long_lines\": false,\n            \"ls_disabled\": false,\n            \"sidebar_theme\": \"default\",\n            \"sidebar_theme_custom_values\": \"\",\n            \"f_key_search\": false,\n            \"k_key_omnibox\": true,\n            \"speak_growls\": false,\n            \"mac_speak_voice\": \"com.apple.speech.synthesis.voice.Alex\",\n            \"mac_speak_speed\": 250,\n            \"comma_key_prefs\": false,\n            \"at_channel_suppressed_channels\": \"\",\n            \"push_at_channel_suppressed_channels\": \"\",\n            \"prompted_for_email_disabling\": false,\n            \"full_text_extracts\": false,\n            \"no_text_in_notifications\": false,\n            \"muted_channels\": \"\",\n            \"no_macssb1_banner\": true,\n            \"no_winssb1_banner\": false,\n            \"privacy_policy_seen\": true,\n            \"search_exclude_bots\": false,\n            \"fuzzy_matching\": false,\n            \"load_lato_2\": false,\n            \"fuller_timestamps\": false,\n            \"last_seen_at_channel_warning\": 0,\n            \"enable_flexpane_rework\": false,\n            \"flex_resize_window\": false,\n            \"msg_preview\": false,\n            \"msg_preview_displaces\": true,\n            \"msg_preview_persistent\": true,\n            \"emoji_autocomplete_big\": false,\n            \"winssb_run_from_tray\": true\n        },\n        \"created\": 1421456475,\n        \"manual_presence\": \"active\"\n    },\n    \"team\": {\n        \"id\": \"T03CX4S34\",\n        \"name\": \"TESTteam, INC\",\n        \"email_domain\": \"\",\n        \"domain\": \"testteaminc\",\n        \"msg_edit_window_mins\": -1,\n        \"prefs\": {\n            \"default_channels\": [\n                \"C01CX1234\",\n                \"C05BX1234\"\n            ],\n            \"msg_edit_window_mins\": -1,\n            \"allow_message_deletion\": true,\n            \"hide_referers\": true,\n            \"display_real_names\": false,\n            \"who_can_at_everyone\": \"regular\",\n            \"who_can_at_channel\": \"ra\",\n            \"warn_before_at_channel\": \"always\",\n            \"who_can_create_channels\": \"regular\",\n            \"who_can_archive_channels\": \"regular\",\n            \"who_can_create_groups\": \"ra\",\n            \"who_can_post_general\": \"ra\",\n            \"who_can_kick_channels\": \"admin\",\n            \"who_can_kick_groups\": \"regular\",\n            \"retention_type\": 0,\n            \"retention_duration\": 0,\n            \"group_retention_type\": 0,\n            \"group_retention_duration\": 0,\n            \"dm_retention_type\": 0,\n            \"dm_retention_duration\": 0,\n            \"require_at_for_mention\": 0,\n            \"compliance_export_start\": 0\n        },\n        \"icon\": {\n            \"image_34\": \"https:\\/\\/slack.global.ssl.fastly.net\\/b3b7\\/img\\/avatars-teams\\/ava_0025-34.png\",\n            \"image_44\": \"https:\\/\\/slack.global.ssl.fastly.net\\/b3b7\\/img\\/avatars-teams\\/ava_0025-44.png\",\n            \"image_68\": \"https:\\/\\/slack.global.ssl.fastly.net\\/b3b7\\/img\\/avatars-teams\\/ava_0025-68.png\",\n            \"image_88\": \"https:\\/\\/slack.global.ssl.fastly.net\\/b3b7\\/img\\/avatars-teams\\/ava_0025-88.png\",\n            \"image_102\": \"https:\\/\\/slack.global.ssl.fastly.net\\/b3b7\\/img\\/avatars-teams\\/ava_0025-102.png\",\n            \"image_132\": \"https:\\/\\/slack.global.ssl.fastly.net\\/b3b7\\/img\\/avatars-teams\\/ava_0025-132.png\",\n            \"image_default\": true\n        },\n        \"over_storage_limit\": false\n    },\n    \"latest_event_ts\": \"1426103085.000000\",\n    \"channels\": [\n        {\n            \"id\": \"C01CX1234\",\n            \"name\": \"general\",\n            \"is_channel\": true,\n            \"created\": 1421456475,\n            \"creator\": \"U03CX4S38\",\n            \"is_archived\": false,\n            \"is_general\": true,\n            \"is_member\": true,\n            \"last_read\": \"0000000000.000000\",\n            \"latest\": {\n                \"type\": \"message\",\n                \"user\": \"U03CX4S38\",\n                \"text\": \"a\",\n                \"ts\": \"1425499421.000004\"\n            },\n            \"unread_count\": 0,\n            \"unread_count_display\": 0,\n            \"members\": [\n                \"U03CX4S38\"\n            ],\n            \"topic\": {\n                \"value\": \"\",\n                \"creator\": \"\",\n                \"last_set\": 0\n            },\n            \"purpose\": {\n                \"value\": \"This channel is for team-wide communication and announcements. All team members are in this channel.\",\n                \"creator\": \"\",\n                \"last_set\": 0\n            }\n        },\n        {\n            \"id\": \"C05BX1234\",\n            \"name\": \"random\",\n            \"is_channel\": true,\n            \"created\": 1421456475,\n            \"creator\": \"U03CX4S38\",\n            \"is_archived\": false,\n            \"is_general\": false,\n            \"is_member\": true,\n            \"last_read\": \"0000000000.000000\",\n            \"latest\": null,\n            \"unread_count\": 0,\n            \"unread_count_display\": 0,\n            \"members\": [\n                \"U03CX4S38\"\n            ],\n            \"topic\": {\n                \"value\": \"\",\n                \"creator\": \"\",\n                \"last_set\": 0\n            },\n            \"purpose\": {\n                \"value\": \"A place for non-work banter, links, articles of interest, humor or anything else which you'd like concentrated in some place other than work-related channels.\",\n                \"creator\": \"\",\n                \"last_set\": 0\n            }\n        }\n    ],\n    \"groups\": [],\n    \"ims\": [\n        {\n            \"id\": \"D03CX4S3E\",\n            \"is_im\": true,\n            \"user\": \"USLACKBOT\",\n            \"created\": 1421456475,\n            \"last_read\": \"1425318850.000003\",\n            \"latest\": {\n                \"type\": \"message\",\n                \"user\": \"USLACKBOT\",\n                \"text\": \"To start, what is your first name?\",\n                \"ts\": \"1425318850.000003\"\n            },\n            \"unread_count\": 0,\n            \"unread_count_display\": 0,\n            \"is_open\": true\n        }\n    ],\n    \"users\": [\n        {\n            \"id\": \"U10CX1234\",\n            \"name\": \"fakeuser\",\n            \"deleted\": false,\n            \"status\": null,\n            \"color\": \"9f69e7\",\n            \"profile\": {\n                \"email\": \"fakeuser@example.com\",\n                \"image_24\": \"https:\\/\\/secure.gravatar.com\\/avatar\\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-24.png\",\n                \"image_32\": \"https:\\/\\/secure.gravatar.com\\/avatar\\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-32.png\",\n                \"image_48\": \"https:\\/\\/secure.gravatar.com\\/avatar\\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F272a%2Fimg%2Favatars%2Fava_0002-48.png\",\n                \"image_72\": \"https:\\/\\/secure.gravatar.com\\/avatar\\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=72&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-72.png\",\n                \"image_192\": \"https:\\/\\/secure.gravatar.com\\/avatar\\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002.png\"\n            },\n            \"is_admin\": true,\n            \"is_owner\": true,\n            \"is_primary_owner\": true,\n            \"is_restricted\": false,\n            \"is_ultra_restricted\": false,\n            \"is_bot\": false,\n            \"has_files\": false,\n            \"presence\": \"away\"\n        },\n        {\n            \"id\": \"U10CX1235\",\n            \"name\": \"userwithoutemail\",\n            \"deleted\": false,\n            \"status\": null,\n            \"color\": \"9f69e7\",\n            \"profile\": {\n                \"image_24\": \"https:\\/\\/secure.gravatar.com\\/avatar\\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-24.png\",\n                \"image_32\": \"https:\\/\\/secure.gravatar.com\\/avatar\\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-32.png\",\n                \"image_48\": \"https:\\/\\/secure.gravatar.com\\/avatar\\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F272a%2Fimg%2Favatars%2Fava_0002-48.png\",\n                \"image_72\": \"https:\\/\\/secure.gravatar.com\\/avatar\\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=72&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-72.png\",\n                \"image_192\": \"https:\\/\\/secure.gravatar.com\\/avatar\\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002.png\"\n            },\n            \"is_admin\": true,\n            \"is_owner\": true,\n            \"is_primary_owner\": true,\n            \"is_restricted\": false,\n            \"is_ultra_restricted\": false,\n            \"is_bot\": false,\n            \"has_files\": false,\n            \"presence\": \"away\"\n        },\n        {\n            \"id\": \"USLACKBOT\",\n            \"name\": \"slackbot\",\n            \"deleted\": false,\n            \"status\": null,\n            \"color\": \"757575\",\n            \"real_name\": \"Slack Bot\",\n            \"tz\": null,\n            \"tz_label\": \"Pacific Daylight Time\",\n            \"tz_offset\": -25200,\n            \"profile\": {\n                \"first_name\": \"Slack\",\n                \"last_name\": \"Bot\",\n                \"image_24\": \"https:\\/\\/slack-assets2.s3-us-west-2.amazonaws.com\\/10068\\/img\\/slackbot_24.png\",\n                \"image_32\": \"https:\\/\\/slack-assets2.s3-us-west-2.amazonaws.com\\/10068\\/img\\/slackbot_32.png\",\n                \"image_48\": \"https:\\/\\/slack-assets2.s3-us-west-2.amazonaws.com\\/10068\\/img\\/slackbot_48.png\",\n                \"image_72\": \"https:\\/\\/slack-assets2.s3-us-west-2.amazonaws.com\\/10068\\/img\\/slackbot_72.png\",\n                \"image_192\": \"https:\\/\\/slack-assets2.s3-us-west-2.amazonaws.com\\/10068\\/img\\/slackbot_192.png\",\n                \"real_name\": \"Slack Bot\",\n                \"real_name_normalized\": \"Slack Bot\",\n                \"email\": null\n            },\n            \"is_admin\": false,\n            \"is_owner\": false,\n            \"is_primary_owner\": false,\n            \"is_restricted\": false,\n            \"is_ultra_restricted\": false,\n            \"is_bot\": false,\n            \"presence\": \"active\"\n        }\n    ],\n    \"bots\": [],\n    \"cache_version\": \"v5-dog\",\n    \"url\": \"wss:\\/\\/cerberus-xxxx.lb.slack-msgs.com\\/websocket\\/ifkp3MKfNXd6ftbrEGllwcHn\"\n}\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_channel.py",
    "content": "from slackclient.channel import Channel\n\n\ndef test_channel(channel):\n    assert type(channel) == Channel\n\n\ndef test_channel_eq(channel):\n    channel = Channel(\n        'test-server',\n        'test-channel',\n        'C12345678',\n    )\n    assert channel == 'test-channel'\n    assert channel == '#test-channel'\n    assert channel == 'C12345678'\n    assert 'C12345678' in str(channel)\n    assert 'C12345678' in \"%r\" % channel\n    assert (channel == 'foo') is False\n\n\ndef test_channel_is_hashable(channel):\n    channel = Channel(\n        'test-server',\n        'test-channel',\n        'C12345678',\n    )\n    channel_map = {channel: channel.id}\n    assert channel_map[channel] == 'C12345678'\n    assert (channel_map[channel] == 'foo') is False\n\n\ndef test_channel_send_message(channel, mocker, monkeypatch):\n    mock_server = mocker.Mock()\n    monkeypatch.setattr(channel, 'server', mock_server)\n    channel.send_message('hi')\n    mock_server.rtm_send_message.assert_called_with(channel.id, 'hi', None, False)\n\n\ndef test_channel_send_message_to_thread(channel, mocker, monkeypatch):\n    mock_server = mocker.Mock()\n    monkeypatch.setattr(channel, 'server', mock_server)\n    channel.send_message('hi', thread='123456.789')\n    mock_server.rtm_send_message.assert_called_with(channel.id, 'hi', '123456.789', False)\n\n    channel.send_message('hi', thread='123456.789', reply_broadcast=True)\n    mock_server.rtm_send_message.assert_called_with(channel.id, 'hi', '123456.789', True)\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_server.py",
    "content": "import json\nimport pytest\nimport responses\nimport time\nimport urllib3\n\nfrom mock import patch\n\nfrom slackclient.user import User\nfrom slackclient.server import Server, SlackConnectionError\nfrom slackclient.channel import Channel\n\nurllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n\n@pytest.fixture\ndef rtm_start_fixture():\n    file_login_data = open('tests/data/rtm.start.json', 'r').read()\n    json_login_data = json.loads(file_login_data)\n    return json_login_data\n\n\ndef test_server():\n    server = Server(token=\"valid_token\", connect=False)\n    assert type(server) == Server\n\n    # The server eqs to a string, either the token or workspace domain\n    assert server.token == \"valid_token\"\n\n\ndef test_server_connect(rtm_start_fixture):\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/rtm.start\",\n            status=200,\n            json=rtm_start_fixture\n        )\n\n        Server(token=\"token\", connect=True)\n\n        for call in rsps.calls:\n            assert call.request.url in [\n                \"https://slack.com/api/rtm.start\"\n            ]\n\n\ndef test_api_call_for_empty_slack_responses(server):\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/chat.postMessage\",\n            status=429,\n            headers={\"Retry-After\": \"1\"},\n        )\n\n        response_received = server.api_call(\"token\", \"chat.postMessage\")\n        chat_postMessage_response = rsps.calls[0].response\n\n        assert chat_postMessage_response.text == \"\"\n        expected_response = {\n            \"headers\": {\"Content-Type\": \"text/plain\", \"Retry-After\": \"1\"}\n        }\n        assert json.loads(response_received) == expected_response\n\n\ndef test_server_is_hashable(server):\n    server_map = {server: server.token}\n    assert server_map[server] == 'xoxp-1234123412341234-12341234-1234'\n    assert (server_map[server] == 'foo') is False\n\n\n@patch('time.sleep', return_value=None)\ndef test_rate_limiting(patched_time_sleep, server):\n    # Testing for rate limit retry headers\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/rtm.start\",\n            status=429,\n            json={\"ok\": False},\n            headers={'Retry-After': \"1\"}\n        )\n\n        with pytest.raises(SlackConnectionError) as e:\n            server.rtm_connect()\n            for call in rsps.calls:\n                assert call.response.status_code == 429\n            assert e.message == \"RTM connection attempt was rate limited 10 times.\"\n\n\ndef test_custom_agent(server):\n    server.append_user_agent(\"test agent\", 1.0)\n    assert server.api_requester.custom_user_agent[0] == ['test agent', 1.0]\n\n\ndef test_server_parse_channel_data(server, rtm_start_fixture):\n    server.parse_channel_data(rtm_start_fixture[\"channels\"])\n    assert type(server.channels.find('general')) == Channel\n\n\ndef test_server_parse_user_data(server, rtm_start_fixture):\n    server.parse_user_data(rtm_start_fixture[\"users\"])\n    # Find user by Name\n    user_by_name = server.users.find('fakeuser')\n    assert type(user_by_name) == User\n    assert user_by_name == \"fakeuser\"\n    assert user_by_name != \"someotheruser\"\n    # Find user by ID\n    user_by_id = server.users.find('U10CX1234')\n    assert type(user_by_id) == User\n    assert user_by_id == \"fakeuser\"\n    assert user_by_id.email == 'fakeuser@example.com'\n    # Don't find invalid user\n    user_by_id = server.users.find('invaliduser')\n    assert user_by_id is None\n\n\ndef test_server_cant_connect(server):\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/rtm.start\",\n            status=403,\n            json={\"ok\": False}\n        )\n\n        with pytest.raises(SlackConnectionError) as e:\n            server.rtm_connect()\n\n\ndef test_reconnect_flag(server, rtm_start_fixture):\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/rtm.start\",\n            status=200,\n            json=rtm_start_fixture\n        )\n\n        server.rtm_connect(auto_reconnect=True)\n        assert server.auto_reconnect is True\n\n        for call in rsps.calls:\n            assert call.request.url in [\n                \"https://slack.com/api/rtm.start\"\n            ]\n\n\ndef test_rtm_reconnect(server, rtm_start_fixture):\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/rtm.connect\",\n            status=200,\n            json=rtm_start_fixture\n        )\n\n        server.rtm_connect(auto_reconnect=True, reconnect=True, use_rtm_start=False)\n\n        for call in rsps.calls:\n            assert call.request.url in [\n                \"https://slack.com/api/rtm.connect\"\n            ]\n\n\n@patch('time.sleep', return_value=None)\ndef test_rtm_max_reconnect_timeout(patched_time_sleep, server, rtm_start_fixture):\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/rtm.connect\",\n            status=200,\n            json=rtm_start_fixture\n        )\n\n        server.reconnect_count = 4\n        server.last_connected_at = time.time()\n        server.rtm_connect(auto_reconnect=True, reconnect=True, use_rtm_start=False)\n\n        assert server.reconnect_count == 5\n\n\ndef test_rtm_reconnect_timeout_recently_connected(server, rtm_start_fixture):\n    # If reconnected recently, server must wait to reconnect and increment the counter\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/rtm.connect\",\n            status=200,\n            json=rtm_start_fixture\n        )\n\n        server.reconnect_count = 0\n        server.last_connected_at = time.time()\n        server.rtm_connect(auto_reconnect=True, reconnect=True, use_rtm_start=False)\n\n        assert server.reconnect_count == 1\n        for call in rsps.calls:\n            assert call.request.url in [\n                \"https://slack.com/api/rtm.connect\"\n            ]\n\n\ndef test_rtm_reconnect_timeout_not_recently_connected(server, rtm_start_fixture):\n    # If reconnecting after 3 minutes since last reconnect, reset counter and connect without wait\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/rtm.connect\",\n            status=200,\n            json=rtm_start_fixture\n        )\n\n        server.reconnect_count = 1\n        server.last_connected_at = time.time() - 180\n        server.rtm_connect(auto_reconnect=True, reconnect=True, use_rtm_start=False)\n\n        assert server.reconnect_count == 0\n        for call in rsps.calls:\n            assert call.request.url in [\n                \"https://slack.com/api/rtm.connect\"\n            ]\n\n\ndef test_max_rtm_reconnects(server, monkeypatch):\n    monkeypatch.setattr(\"time.sleep\", None)\n    with pytest.raises(SlackConnectionError) as e:\n        server.reconnect_count = 5\n        server.rtm_connect(auto_reconnect=True, reconnect=True, use_rtm_start=False)\n        assert e.message == \"RTM connection failed, reached max reconnects.\"\n\n\n@pytest.mark.xfail\ndef test_server_ping(server, monkeypatch):\n    monkeypatch.setattr(\"websocket.create_connection\", lambda: True)\n    reply = server.ping()\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackclient.py",
    "content": "import json\nimport pytest\nfrom requests.exceptions import ProxyError\nimport responses\nfrom slackclient.exceptions import TokenRefreshError\nfrom slackclient.channel import Channel\nfrom slackclient.client import SlackClient\nfrom slackclient.server import SlackConnectionError\n\n\n@pytest.fixture\ndef channel_created_fixture():\n    file_channel_created_data = open(\"tests/data/channel.created.json\", \"r\").read()\n    json_channel_created_data = json.loads(file_channel_created_data)\n    return json_channel_created_data\n\n\n@pytest.fixture\ndef im_created_fixture():\n    file_channel_created_data = open(\"tests/data/im.created.json\", \"r\").read()\n    json_channel_created_data = json.loads(file_channel_created_data)\n    return json_channel_created_data\n\n\ndef test_proxy():\n    proxies = {\"http\": \"some-bad-proxy\", \"https\": \"some-bad-proxy\"}\n    client = SlackClient(\"xoxp-1234123412341234-12341234-1234\", proxies=proxies)\n    server = client.server\n\n    assert server.proxies == proxies\n\n    with pytest.raises(ProxyError):\n        server.rtm_connect()\n\n    with pytest.raises(SlackConnectionError):\n        server.connect_slack_websocket(\n            \"wss://mpmulti-xw58.slack-msgs.com/websocket/bad-token\"\n        )\n\n    api_requester = server.api_requester\n    assert api_requester.proxies == proxies\n    with pytest.raises(ProxyError):\n        api_requester.do(\"xoxp-1234123412341234-12341234-1234\", request=\"channels.list\")\n\n\ndef test_SlackClient(slackclient):\n    assert type(slackclient) == SlackClient\n\n\ndef test_custom_user_agent(slackclient):\n    slackclient.append_user_agent(\"customua\", \"1.0.0\")\n    assert \"customua\" in slackclient.server.api_requester.get_user_agent()\n\n\ndef test_SlackClient_process_changes(\n    slackclient, channel_created_fixture, im_created_fixture\n):\n    slackclient.process_changes(channel_created_fixture)\n    assert type(slackclient.server.channels.find(\"fun\")) == Channel\n    slackclient.process_changes(im_created_fixture)\n    assert type(slackclient.server.channels.find(\"U123BL234\")) == Channel\n\n\ndef test_api_not_ok(slackclient):\n    # Testing for rate limit retry headers\n    client = SlackClient(\"xoxp-1234123412341234-12341234-1234\")\n\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/im.open\",\n            status=200,\n            json={\"ok\": False, \"error\": \"invalid_auth\"},\n            headers={},\n        )\n\n        client.api_call(\"im.open\", user=\"UXXXX\")\n\n        for call in rsps.calls:\n            assert call.response.status_code == 200\n            assert call.request.url in [\"https://slack.com/api/im.open\"]\n\n\ndef test_im_open(slackclient):\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/im.open\",\n            status=200,\n            json={\"ok\": True, \"channel\": {\"id\": \"CXXXXXX\"}},\n            headers={},\n        )\n\n        slackclient.api_call(\"im.open\", user=\"UXXXX\")\n\n        for call in rsps.calls:\n            assert call.response.status_code == 200\n            assert call.request.url in [\"https://slack.com/api/im.open\"]\n\n\ndef test_channel_join(slackclient):\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/channels.join\",\n            status=200,\n            json={\n                \"ok\": True,\n                \"channel\": {\n                    \"id\": \"CXXXX\",\n                    \"name\": \"test\",\n                    \"members\": (\"U0G9QF9C6\", \"U1QNSQB9U\"),\n                },\n            },\n        )\n\n        slackclient.api_call(\"channels.join\", channel=\"CXXXX\")\n\n        for call in rsps.calls:\n            assert call.response.status_code == 200\n            assert call.request.url in [\"https://slack.com/api/channels.join\"]\n            response_json = call.response.json()\n            assert response_json[\"ok\"] is True\n\n\ndef test_noncallable_refresh_callback():\n    with pytest.raises(TokenRefreshError):\n        SlackClient(\n            client_id=\"12345\",\n            client_secret=\"12345\",\n            refresh_token=\"refresh_token\",\n            token_update_callback=\"THIS IS A STRING, NOT A CALLABLE METHOD\",\n        )\n\n\ndef test_no_RTM_with_workspace_tokens():\n    def token_update_callback(update_data):\n        return update_data\n\n    with pytest.raises(TokenRefreshError):\n        sc = SlackClient(\n            client_id=\"12345\",\n            client_secret=\"12345\",\n            refresh_token=\"refresh_token\",\n            token_update_callback=token_update_callback,\n        )\n\n        sc.rtm_connect()\n\n\ndef test_token_refresh_on_initial_api_request():\n    # Client should fetch and append an access token on the first API request\n\n    # When the token is refreshed, the client will call this callback\n    access_token = \"xoxa-2-abcdef\"\n    client_args = {}\n\n    def token_update_callback(update_data):\n        client_args[update_data[\"team_id\"]] = update_data\n\n    sc = SlackClient(\n        client_id=\"12345\",\n        client_secret=\"12345\",\n        refresh_token=\"refresh_token\",\n        token_update_callback=token_update_callback,\n    )\n\n    # The client starts out with an empty token\n    assert sc.token is None\n\n    # Mock both the main API request and the token refresh request\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/auth.test\",\n            status=200,\n            json={\"ok\": True},\n        )\n\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/oauth.access\",\n            status=200,\n            json={\n                \"ok\": True,\n                \"access_token\": access_token,\n                \"token_type\": \"app\",\n                \"expires_in\": 3600,\n                \"team_id\": \"T2U81E2FP\",\n                \"enterprise_id\": \"T2U81ELK\",\n            },\n        )\n\n        # Calling the API for the first time will trigger a token refresh\n        sc.api_call(\"auth.test\")\n\n        # Store the calls in order\n        calls = {}\n        for index, call in enumerate(rsps.calls):\n            calls[index] = {\"url\": call.request.url}\n\n        # After the initial call, the refresh method will update the client's token,\n        # then the callback will update client_args\n        assert sc.token == access_token\n        assert client_args[\"T2U81E2FP\"][\"access_token\"] == access_token\n\n        # Verify that the client first tried to call the API, refreshed the token, then retried\n        assert calls[0][\"url\"] == \"https://slack.com/api/oauth.access\"\n        assert calls[1][\"url\"] == \"https://slack.com/api/auth.test\"\n\n\ndef test_token_refresh_failed():\n    # Client should raise TokenRefreshError is token refresh returns error\n    def token_update_callback(update_data):\n        return update_data\n\n    sc = SlackClient(\n        client_id=\"12345\",\n        client_secret=\"12345\",\n        refresh_token=\"refresh_token\",\n        token_update_callback=token_update_callback,\n    )\n\n    with pytest.raises(TokenRefreshError):\n        with responses.RequestsMock() as rsps:\n            rsps.add(\n                responses.POST,\n                \"https://slack.com/api/channels.list\",\n                status=200,\n                json={\"ok\": False, \"error\": \"invalid_auth\"},\n            )\n\n            rsps.add(\n                responses.POST,\n                \"https://slack.com/api/oauth.access\",\n                status=200,\n                json={\"ok\": False, \"error\": \"invalid_auth\"},\n            )\n\n            sc.api_call(\"channels.list\")\n\n\ndef test_token_refresh_on_expired_token():\n    # Client should fetch and append an access token on the first API request\n\n    # When the token is refreshed, the client will call this callback\n    client_args = {}\n\n    def token_update_callback(update_data):\n        client_args[update_data[\"team_id\"]] = update_data\n\n    sc = SlackClient(\n        client_id=\"12345\",\n        client_secret=\"12345\",\n        refresh_token=\"refresh_token\",\n        token_update_callback=token_update_callback,\n    )\n\n    # Set the token TTL to some time in the past\n    sc.access_token_expires_at = 0\n\n    # Mock both the main API request and the token refresh request\n    with responses.RequestsMock() as rsps:\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/auth.test\",\n            status=200,\n            json={\"ok\": True},\n        )\n\n        rsps.add(\n            responses.POST,\n            \"https://slack.com/api/oauth.access\",\n            status=200,\n            json={\n                \"ok\": True,\n                \"access_token\": \"xoxa-2-abcdef\",\n                \"token_type\": \"app\",\n                \"expires_in\": 3600,\n                \"team_id\": \"T2U81E2FP\",\n                \"enterprise_id\": \"T2U81ELK\",\n            },\n        )\n\n        # Calling the API for the first time will trigger a token refresh\n        sc.api_call(\"auth.test\")\n\n        # Store the calls in order\n        calls = {}\n        for index, call in enumerate(rsps.calls):\n            calls[index] = {\"url\": call.request.url}\n\n        # Verify that the client first fetches the token, then submits the request\n        assert calls[0][\"url\"] == \"https://slack.com/api/oauth.access\"\n        assert calls[1][\"url\"] == \"https://slack.com/api/auth.test\"\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackrequest.py",
    "content": "from slackclient.slackrequest import SlackRequest\nfrom slackclient.version import __version__\nimport os\n\n\ndef test_http_headers(mocker):\n    requests = mocker.patch('slackclient.slackrequest.requests')\n    request = SlackRequest()\n\n    request.do('xoxb-123', 'chat.postMessage', {'text': 'test', 'channel': '#general'})\n    args, kwargs = requests.post.call_args\n\n    assert kwargs['headers']['user-agent'] is not None\n\n\ndef test_custom_user_agent(mocker):\n    requests = mocker.patch('slackclient.slackrequest.requests')\n    request = SlackRequest()\n\n    request.append_user_agent(\"fooagent1\", \"0.1\")\n    request.append_user_agent(\"baragent/2\", \"0.2\")\n\n    request.do('xoxb-123', 'chat.postMessage', {'text': 'test', 'channel': '#general'})\n    args, kwargs = requests.post.call_args\n\n    # Verify user-agent includes both default and custom agent info\n    assert \"slackclient/{}\".format(__version__) in kwargs['headers']['user-agent']\n    assert \"fooagent1/0.1\" in kwargs['headers']['user-agent']\n\n    # verify escaping of slashes in custom agent name\n    assert \"baragent:2/0.2\" in kwargs['headers']['user-agent']\n\n\ndef test_auth_header(mocker):\n    requests = mocker.patch('slackclient.slackrequest.requests')\n    request = SlackRequest()\n\n    request.do('xoxb-123', 'chat.postMessage', {'text': 'test', 'channel': '#general'})\n    args, kwargs = requests.post.call_args\n\n    assert \"Bearer xoxb-123\" in kwargs['headers']['Authorization']\n\n\ndef test_token_override(mocker):\n    requests = mocker.patch('slackclient.slackrequest.requests')\n    request = SlackRequest()\n\n    request.do('xoxb-123', 'chat.postMessage',\n               {\n                   'token': \"newtoken\",\n                   'text': 'test',\n                   'channel': '#general'\n                })\n    args, kwargs = requests.post.call_args\n\n    assert \"Bearer newtoken\" in kwargs['headers']['Authorization']\n\n\ndef test_plural_field(mocker):\n    requests = mocker.patch('slackclient.slackrequest.requests')\n    request = SlackRequest()\n\n    request.do('xoxb-123', 'conversations.open', {'users': ['U123', 'U234', 'U345']})\n    args, kwargs = requests.post.call_args\n\n    assert kwargs['data'] == {'users': 'U123,U234,U345'}\n\n    request.do('xoxb-123', 'conversations.open', {'users': \"U123,U234,U345\"})\n    args2, kwargs2 = requests.post.call_args\n\n    assert kwargs2['data'] == {'users': 'U123,U234,U345'}\n\n\ndef test_post_file(mocker):\n    requests = mocker.patch('slackclient.slackrequest.requests')\n    request = SlackRequest()\n\n    request.do('xoxb-123',\n               'files.upload',\n               {'file': open(os.path.join('.', 'tests', 'data', 'slack_logo.png'), 'rb'),\n                'filename': 'slack_logo.png'})\n    args, kwargs = requests.post.call_args\n\n    assert requests.post.call_count == 1\n    assert 'https://slack.com/api/files.upload' == args[0]\n    assert {'filename': 'slack_logo.png'} == kwargs['data']\n    assert kwargs['files'] is not None\n\n\ndef test_get_file(mocker):\n    requests = mocker.patch('slackclient.slackrequest.requests')\n    request = SlackRequest()\n\n    request.do('xoxb-123', 'files.info', {'file': 'myFavoriteFileID'})\n    args, kwargs = requests.post.call_args\n\n    assert requests.post.call_count == 1\n    assert 'https://slack.com/api/files.info' == args[0]\n    assert {'file': \"myFavoriteFileID\"} == kwargs['data']\n    assert kwargs['files'] is None\n\n\ndef test_post_attachements(mocker):\n    requests = mocker.patch('slackclient.slackrequest.requests')\n    request = SlackRequest()\n\n    request.do('xoxb-123',\n               'chat.postMessage',\n               {'attachments': [{'title': 'hello'}]})\n    args, kwargs = requests.post.call_args\n\n    assert requests.post.call_count == 1\n    assert 'https://slack.com/api/chat.postMessage' == args[0]\n    assert isinstance(kwargs[\"data\"][\"attachments\"], str)\n"
  },
  {
    "path": "openpype/modules/slack/python2_vendor/python-slack-sdk-1/tox.ini",
    "content": "[tox]\n; you probably don't have all of these python versions on your machine. when you invoke tox, you should pick an\n; environment that you have (e.g. `tox -e py27,py36,flake8`).\n; for quality analysis, use `tox -e flake8` or just `flake8 slackclient`\n; to build the docs, use `tox -e docs`\nenvlist=\n    py{27,34,35,36},\n    flake8,\n    docs\n\n[testenv]\ndeps = -rtest_requirements.txt\ncommands =\n    ; `--cov-report=html:cov_html`: suppress terminal output, html report in `cov_html/`, populate `.coverage/`\n    ; `--cov=slackclient`: name project\n    ; `{posargs:tests}`: tests located in `tests` by default unless otherwise overriden by tox positional args\n    py.test --cov-report=html:cov_html --cov=slackclient {posargs:tests}\n    ; `codecov` will run the `coverage` utility and then upload results in xml format\n    ; `coverage` utility has configuration in `.coveragerc`\n    ;  CI systems use their own build matricies and virtualenvs and don't need tox. therefore tox shouldn't be used\n    ;  to upload coverage to codecov\n    ;  codecov -e TOXENV\n\n[testenv:flake8]\nbasepython = python\ndeps = flake8\ncommands = flake8 slackclient\n\n[testenv:docs]\nbasepython = python\nwhitelist_externals = /bin/bash\ndeps =\n    Sphinx\n    sphinx_rtd_theme\ncommands = bash ./docs.sh\n"
  },
  {
    "path": "openpype/modules/slack/slack_module.py",
    "content": "import os\nfrom openpype.modules import OpenPypeModule, IPluginPaths\n\nSLACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass SlackIntegrationModule(OpenPypeModule, IPluginPaths):\n    \"\"\"Allows sending notification to Slack channels during publishing.\"\"\"\n\n    name = \"slack\"\n\n    def initialize(self, modules_settings):\n        slack_settings = modules_settings[self.name]\n        self.enabled = slack_settings[\"enabled\"]\n\n    def get_launch_hook_paths(self):\n        \"\"\"Implementation for applications launch hooks.\"\"\"\n\n        return os.path.join(SLACK_MODULE_DIR, \"launch_hooks\")\n\n    def get_plugin_paths(self):\n        \"\"\"Deadline plugin paths.\"\"\"\n        current_dir = os.path.dirname(os.path.abspath(__file__))\n        return {\n            \"publish\": [os.path.join(current_dir, \"plugins\", \"publish\")]\n        }\n"
  },
  {
    "path": "openpype/modules/sync_server/README.md",
    "content": "Synchronization server\n---------------------\nThis server is scheduled at start of Pype, it periodically checks avalon DB\nfor 'representation' records which have in theirs files.sites record with \nname: 'gdrive' (or any other site name from 'gdrive.json') without \nfield 'created_dt'.\n\nThis denotes that this representation should be synced to GDrive.\nRecords like these are created by IntegrateNew process based on configuration.\nLeave 'config.json.remote_site' empty for not synchronizing at all.\n\nOne provider could have multiple sites. (GDrive implementation is 'a provider',\ntarget folder on it is 'a site')\n\nQuick HOWTOs:\n-------------\nI want to start syncing my newly published files:\n------------------------------------------------\n\n- Check that Sync server is enabled globally in \n    `pype/settings/defaults/system_settings/modules.json`\n    \n- Get credentials for service account, share target folder on Gdrive with it\n\n- Set path to stored credentials file in \n    `pype/settings/defaults/project_settings/global.json`.`credentials_url`\n    \n- Set name of site, root folder and provider('gdrive' in case of Google Drive) in \n    `pype/settings/defaults/project_settings/global.json`.`sites`\n    \n- Update `pype/settings/defaults/project_settings/global.json`.`remote_site`\nto name of site you set in previous step.\n\n- Check that project setting is enabled (in this `global.json` file)\n\n- Start Pype and publish\n\nMy published file is not syncing:\n--------------------------------\n\n- Check that representation record contains for all 'files.sites' skeleton in \nformat: `{name: \"MY_CONFIGURED_REMOTE_SITE\"}`\n- Check if that record doesn't have already 'created_dt' filled. That would \ndenote that file was synced but someone might have had removed it on remote\nsite.\n- If that records contains field \"error\", check that \"tries\" field doesn't \ncontain same value as threshold in config.json.retry_cnt. If it does fix \nthe problem mentioned in 'error' field, delete 'tries' field.\n\nI want to sync my already published files:\n-----------------------------------------\n\n- Configure your Pype for syncing (see first section of Howtos).\n- Manually add skeleton {name: \"MY_CONFIGURED_REMOTE_SITE\"} to all \nrepresentation.files.sites:\n`db.getCollection('MY_PROJECT').update({type:\"representation\"}, \n{$set:{\"files.$[].sites.MY_CONFIGURED_REMOTE_SITE\" : {}}}, true, true)`\n\nI want to create new custom provider:\n-----------------------------------\n- take `providers\\abstract_provider.py` as a base class\n- create provider class in `providers` with a name according to a provider (eg. 'gdrive.py' for gdrive provider etc.)\n- upload provider icon in png format, 24x24, into `providers\\resources`, its name must follow name of provider (eg. 'gdrive.png' for gdrive provider)\n- register new provider into `providers.lib.py`, test how many files could be manipulated at same time, check provider's API for limits\n\nNeeded configuration:\n--------------------\n`pype/settings/defaults/project_settings/global.json`.`sync_server`:\n - `\"local_id\": \"local_0\",` -- identifier of user pype\n - `\"retry_cnt\": 3,`        -- how many times try to synch file in case of error\n - `\"loop_delay\": 60,`      -- how many seconds between sync loops\n - `\"publish_site\": \"studio\",` -- which site user current, 'studio' by default, \n                              could by same as 'local_id' if user is working\n                              from home without connection to studio \n                              infrastructure\n - `\"remote_site\": \"gdrive\"` -- key for site to synchronize to. Must match to site\n                             configured lower in this file.\n                             Used in IntegrateNew to prepare skeleton for \n                             syncing in the representation record.\n                             Leave empty if no syncing is wanted.\n  This is a general configuration, 'local_id', 'publish_site' and 'remote_site'\n  will be set and changed by some GUI in the future.                           \n  \n`pype/settings/defaults/project_settings/global.json`.`sync_server`.`sites`:\n ```- \"gdrive\": {  - site name, must be unique\n -     \"provider\": \"gdrive\" -- type of provider, must be registered in 'sync_server\\providers\\lib.py'\n -     \"credentials_url\": \"/my_secret_folder/credentials.json\", \n            -- path to credentials for service account\n -     \"root\": {     -- \"root\": \"/My Drive\" in simple scenario, config here for\n                    --  multiroot projects\n -           \"root_one\": \"/My Drive/work_folder\",\n -           \"root_tow\": \"/My Drive/publish_folder\"\n     }\n  }``\n  \n  \n\n"
  },
  {
    "path": "openpype/modules/sync_server/__init__.py",
    "content": "from .sync_server_module import SyncServerModule\n\n\n__all__ = (\n    \"SyncServerModule\",\n)\n"
  },
  {
    "path": "openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py",
    "content": "import os\nimport shutil\nimport filecmp\n\nfrom openpype.client.entities import get_representations\nfrom openpype.lib.applications import PreLaunchHook, LaunchTypes\nfrom openpype.lib.profiles_filtering import filter_profiles\nfrom openpype.modules.sync_server.sync_server import (\n    download_last_published_workfile,\n)\nfrom openpype.pipeline.template_data import get_template_data\nfrom openpype.pipeline.workfile.path_resolving import (\n    get_workfile_template_key,\n)\nfrom openpype.settings.lib import get_project_settings\n\n\nclass CopyLastPublishedWorkfile(PreLaunchHook):\n    \"\"\"Copy last published workfile as first workfile.\n\n    Prelaunch hook works only if last workfile leads to not existing file.\n        - That is possible only if it's first version.\n    \"\"\"\n\n    # Before `AddLastWorkfileToLaunchArgs`\n    order = -1\n    # any DCC could be used but TrayPublisher and other specials\n    app_groups = [\"blender\", \"photoshop\", \"tvpaint\", \"aftereffects\",\n                  \"nuke\", \"nukeassist\", \"nukex\", \"hiero\", \"nukestudio\",\n                  \"maya\", \"harmony\", \"celaction\", \"flame\", \"fusion\",\n                  \"houdini\", \"tvpaint\"]\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        \"\"\"Check if local workfile doesn't exist, else copy it.\n\n        1- Check if setting for this feature is enabled\n        2- Check if workfile in work area doesn't exist\n        3- Check if published workfile exists and is copied locally in publish\n        4- Substitute copied published workfile as first workfile\n           with incremented version by +1\n\n        Returns:\n            None: This is a void method.\n        \"\"\"\n        sync_server = self.modules_manager.get(\"sync_server\")\n        if not sync_server or not sync_server.enabled:\n            self.log.debug(\"Sync server module is not enabled or available\")\n            return\n\n        # Check there is no workfile available\n        last_workfile = self.data.get(\"last_workfile_path\")\n        if os.path.exists(last_workfile):\n            self.log.debug(\n                \"Last workfile exists. Skipping {} process.\".format(\n                    self.__class__.__name__\n                )\n            )\n            return\n\n        # Get data\n        project_name = self.data[\"project_name\"]\n        asset_name = self.data[\"asset_name\"]\n        task_name = self.data[\"task_name\"]\n        task_type = self.data[\"task_type\"]\n        host_name = self.application.host_name\n\n        # Check settings has enabled it\n        project_settings = get_project_settings(project_name)\n        profiles = project_settings[\"global\"][\"tools\"][\"Workfiles\"][\n            \"last_workfile_on_startup\"\n        ]\n        filter_data = {\n            \"tasks\": task_name,\n            \"task_types\": task_type,\n            \"hosts\": host_name,\n        }\n        last_workfile_settings = filter_profiles(profiles, filter_data)\n        if not last_workfile_settings:\n            return\n        use_last_published_workfile = last_workfile_settings.get(\n            \"use_last_published_workfile\"\n        )\n        if use_last_published_workfile is None:\n            self.log.info(\n                (\n                    \"Seems like old version of settings is used.\"\n                    ' Can\\'t access custom templates in host \"{}\".'.format(\n                        host_name\n                    )\n                )\n            )\n            return\n        elif use_last_published_workfile is False:\n            self.log.info(\n                (\n                    'Project \"{}\" has turned off to use last published'\n                    ' workfile as first workfile for host \"{}\"'.format(\n                        project_name, host_name\n                    )\n                )\n            )\n            return\n\n        max_retries = int((sync_server.sync_project_settings[project_name]\n                                                            [\"config\"]\n                                                            [\"retry_cnt\"]))\n\n        self.log.info(\"Trying to fetch last published workfile...\")\n\n        asset_doc = self.data.get(\"asset_doc\")\n        anatomy = self.data.get(\"anatomy\")\n\n        context_filters = {\n            \"asset\": asset_name,\n            \"family\": \"workfile\",\n            \"task\": {\"name\": task_name, \"type\": task_type}\n        }\n\n        # Add version filter\n        workfile_version = self.launch_context.data.get(\"workfile_version\", -1)\n        if workfile_version > 0 and workfile_version not in {None, \"last\"}:\n            context_filters[\"version\"] = self.launch_context.data[\n                \"workfile_version\"\n            ]\n\n            # Only one version will be matched\n            version_index = 0\n        else:\n            version_index = workfile_version\n\n        workfile_representations = list(get_representations(\n            project_name,\n            context_filters=context_filters\n        ))\n\n        if not workfile_representations:\n            self.log.debug(\n                'No published workfile for task \"{}\" and host \"{}\".'.format(\n                    task_name, host_name\n                )\n            )\n            return\n\n        filtered_repres = filter(\n            lambda r: r[\"context\"].get(\"version\") is not None,\n            workfile_representations\n        )\n        # Get workfile version\n        workfile_representation = sorted(\n            filtered_repres, key=lambda r: r[\"context\"][\"version\"]\n        )[version_index]\n\n        # Copy file and substitute path\n        last_published_workfile_path = download_last_published_workfile(\n            host_name,\n            project_name,\n            task_name,\n            workfile_representation,\n            max_retries,\n            anatomy=anatomy\n        )\n        if not last_published_workfile_path:\n            self.log.debug(\n                \"Couldn't download {}\".format(last_published_workfile_path)\n            )\n            return\n\n        project_doc = self.data[\"project_doc\"]\n\n        project_settings = self.data[\"project_settings\"]\n        template_key = get_workfile_template_key(\n            task_name, host_name, project_name, project_settings\n        )\n\n        # Get workfile data\n        workfile_data = get_template_data(\n            project_doc, asset_doc, task_name, host_name\n        )\n\n        extension = last_published_workfile_path.split(\".\")[-1]\n        workfile_data[\"version\"] = (\n                workfile_representation[\"context\"][\"version\"] + 1)\n        workfile_data[\"ext\"] = extension\n\n        anatomy_result = anatomy.format(workfile_data)\n        local_workfile_path = anatomy_result[template_key][\"path\"]\n\n        # Copy last published workfile to local workfile directory\n        shutil.copy(\n            last_published_workfile_path,\n            local_workfile_path,\n        )\n\n        self.data[\"last_workfile_path\"] = local_workfile_path\n        # Keep source filepath for further path conformation\n        self.data[\"source_filepath\"] = last_published_workfile_path\n\n        # Get resources directory\n        resources_dir = os.path.join(\n            os.path.dirname(local_workfile_path), 'resources'\n        )\n        # Make resource directory if it doesn't exist\n        if not os.path.exists(resources_dir):\n            os.mkdir(resources_dir)\n\n        # Copy resources to the local resources directory\n        for file in workfile_representation['files']:\n            # Get resource main path\n            resource_main_path = anatomy.fill_root(file[\"path\"])\n\n            # Get resource file basename\n            resource_basename = os.path.basename(resource_main_path)\n\n            # Only copy if the resource file exists, and it's not the workfile\n            if (\n                not os.path.exists(resource_main_path)\n                or resource_basename == os.path.basename(\n                    last_published_workfile_path\n                )\n            ):\n                continue\n\n            # Get resource path in workfile folder\n            resource_work_path = os.path.join(\n                resources_dir, resource_basename\n            )\n\n            # Check if the resource file already exists in the resources folder\n            if os.path.exists(resource_work_path):\n                # Check if both files are the same\n                if filecmp.cmp(resource_main_path, resource_work_path):\n                    self.log.warning(\n                        'Resource \"{}\" already exists.'\n                        .format(resource_basename)\n                    )\n                    continue\n                else:\n                    # Add `.old` to existing resource path\n                    resource_path_old = resource_work_path + '.old'\n                    if os.path.exists(resource_work_path + '.old'):\n                        for i in range(1, 100):\n                            p = resource_path_old + '%02d' % i\n                            if not os.path.exists(p):\n                                # Rename existing resource file to\n                                # `resource_name.old` + 2 digits\n                                shutil.move(resource_work_path, p)\n                                break\n                        else:\n                            self.log.warning(\n                                'There are a hundred old files for '\n                                'resource \"{}\". '\n                                'Perhaps is it time to clean up your '\n                                'resources folder'\n                                .format(resource_basename)\n                            )\n                            continue\n                    else:\n                        # Rename existing resource file to `resource_name.old`\n                        shutil.move(resource_work_path, resource_path_old)\n\n            # Copy resource file to workfile resources folder\n            shutil.copy(resource_main_path, resources_dir)\n"
  },
  {
    "path": "openpype/modules/sync_server/plugins/load/add_site.py",
    "content": "from openpype.client import get_linked_representation_id\nfrom openpype.modules import ModulesManager\nfrom openpype.pipeline import load\nfrom openpype.modules.sync_server.utils import SiteAlreadyPresentError\n\n\nclass AddSyncSite(load.LoaderPlugin):\n    \"\"\"Add sync site to representation\n\n    If family of synced representation is 'workfile', it looks for all\n    representations which are referenced (loaded) in workfile with content of\n    'inputLinks'.\n    It doesn't do any checks for site, most common use case is when artist is\n    downloading workfile to his local site, but it might be helpful when\n    artist is re-uploading broken representation on remote site also.\n    \"\"\"\n    representations = [\"*\"]\n    families = [\"*\"]\n\n    label = \"Add Sync Site\"\n    order = 2  # lower means better\n    icon = \"download\"\n    color = \"#999999\"\n\n    _sync_server = None\n    is_add_site_loader = True\n\n    @property\n    def sync_server(self):\n        if not self._sync_server:\n            manager = ModulesManager()\n            self._sync_server = manager.modules_by_name[\"sync_server\"]\n\n        return self._sync_server\n\n    def load(self, context, name=None, namespace=None, data=None):\n        \"\"\"\"Adds site skeleton information on representation_id\n\n        Looks for loaded containers for workfile, adds them site skeleton too\n        (eg. they should be downloaded too).\n        Args:\n            context (dict):\n            name (str):\n            namespace (str):\n            data (dict): expects {\"site_name\": SITE_NAME_TO_ADD}\n        \"\"\"\n        # self.log wont propagate\n        project_name = context[\"project\"][\"name\"]\n        repre_doc = context[\"representation\"]\n        family = repre_doc[\"context\"][\"family\"]\n        repre_id = repre_doc[\"_id\"]\n        site_name = data[\"site_name\"]\n        print(\"Adding {} to representation: {}\".format(\n              data[\"site_name\"], repre_id))\n\n        self.sync_server.add_site(project_name, repre_id, site_name,\n                                  force=True)\n\n        if family == \"workfile\":\n            links = get_linked_representation_id(\n                project_name,\n                repre_id=repre_id,\n                link_type=\"reference\"\n            )\n            for link_repre_id in links:\n                try:\n                    print(\"Adding {} to linked representation: {}\".format(\n                        data[\"site_name\"], link_repre_id))\n                    self.sync_server.add_site(project_name, link_repre_id,\n                                              site_name,\n                                              force=False)\n                except SiteAlreadyPresentError:\n                    # do not add/reset working site for references\n                    self.log.debug(\"Site present\", exc_info=True)\n\n        self.log.debug(\"Site added.\")\n\n    def filepath_from_context(self, context):\n        \"\"\"No real file loading\"\"\"\n        return \"\"\n"
  },
  {
    "path": "openpype/modules/sync_server/plugins/load/remove_site.py",
    "content": "from openpype.modules import ModulesManager\nfrom openpype.pipeline import load\n\n\nclass RemoveSyncSite(load.LoaderPlugin):\n    \"\"\"Remove sync site and its files on representation.\n\n    Removes files only on local site!\n    \"\"\"\n    representations = [\"*\"]\n    families = [\"*\"]\n\n    label = \"Remove Sync Site\"\n    order = 4\n    icon = \"download\"\n    color = \"#999999\"\n\n    _sync_server = None\n    is_remove_site_loader = True\n\n    @property\n    def sync_server(self):\n        if not self._sync_server:\n            manager = ModulesManager()\n            self._sync_server = manager.modules_by_name[\"sync_server\"]\n\n        return self._sync_server\n\n    def load(self, context, name=None, namespace=None, data=None):\n        project_name = context[\"project\"][\"name\"]\n        repre_doc = context[\"representation\"]\n        repre_id = repre_doc[\"_id\"]\n        site_name = data[\"site_name\"]\n\n        print(\"Removing {} on representation: {}\".format(site_name, repre_id))\n\n        self.sync_server.remove_site(project_name,\n                                     repre_id,\n                                     site_name,\n                                     True)\n        self.log.debug(\"Site removed.\")\n\n    def filepath_from_context(self, context):\n        \"\"\"No real file loading\"\"\"\n        return \"\"\n"
  },
  {
    "path": "openpype/modules/sync_server/providers/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/modules/sync_server/providers/abstract_provider.py",
    "content": "import abc\nimport six\nfrom openpype.lib import Logger\n\nlog = Logger.get_logger(\"SyncServer\")\n\n\n@six.add_metaclass(abc.ABCMeta)\nclass AbstractProvider:\n    CODE = ''\n    LABEL = ''\n\n    _log = None\n\n    def __init__(self, project_name, site_name, tree=None, presets=None):\n        self.presets = None\n        self.active = False\n        self.site_name = site_name\n\n        self.presets = presets\n\n        super(AbstractProvider, self).__init__()\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    @abc.abstractmethod\n    def is_active(self):\n        \"\"\"\n            Returns True if provider is activated, eg. has working credentials.\n        Returns:\n            (boolean)\n        \"\"\"\n\n    @classmethod\n    @abc.abstractmethod\n    def get_system_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on system settings level\n\n\n            Returns:\n                (list) of dict\n        \"\"\"\n\n    @classmethod\n    @abc.abstractmethod\n    def get_project_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on project settings level\n\n\n            Returns:\n                (list) of dict\n        \"\"\"\n\n    @classmethod\n    @abc.abstractmethod\n    def get_local_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on local settings level\n\n\n            Returns:\n               (list) of dict\n        \"\"\"\n\n    @abc.abstractmethod\n    def upload_file(self, source_path, path,\n                    server, project_name, file, representation, site,\n                    overwrite=False):\n        \"\"\"\n            Copy file from 'source_path' to 'target_path' on provider.\n            Use 'overwrite' boolean to rewrite existing file on provider\n\n        Args:\n            source_path (string):\n            path (string): absolute path with or without name of the file\n            overwrite (boolean): replace existing file\n\n            arguments for saving progress:\n            server (SyncServer): server instance to call update_db on\n            project_name (str): name of project_name\n            file (dict): info about uploaded file (matches structure from db)\n            representation (dict): complete repre containing 'file'\n            site (str): site name\n        Returns:\n            (string) file_id of created/modified file ,\n                throws FileExistsError, FileNotFoundError exceptions\n        \"\"\"\n        pass\n\n    @abc.abstractmethod\n    def download_file(self, source_path, local_path,\n                      server, project_name, file, representation, site,\n                      overwrite=False):\n        \"\"\"\n            Download file from provider into local system\n\n        Args:\n            source_path (string): absolute path on provider\n            local_path (string): absolute path with or without name of the file\n            overwrite (boolean): replace existing file\n\n            arguments for saving progress:\n            server (SyncServer): server instance to call update_db on\n            project_name (str):\n            file (dict): info about uploaded file (matches structure from db)\n            representation (dict): complete repre containing 'file'\n            site (str): site name\n        Returns:\n            (string) file_id of created/modified file ,\n                throws FileExistsError, FileNotFoundError exceptions\n        \"\"\"\n        pass\n\n    @abc.abstractmethod\n    def delete_file(self, path):\n        \"\"\"\n            Deletes file from 'path'. Expects path to specific file.\n\n        Args:\n            path (string): absolute path to particular file\n\n        Returns:\n            None\n        \"\"\"\n        pass\n\n    @abc.abstractmethod\n    def list_folder(self, folder_path):\n        \"\"\"\n            List all files and subfolders of particular path non-recursively.\n        Args:\n            folder_path (string): absolut path on provider\n\n        Returns:\n            (list)\n        \"\"\"\n        pass\n\n    @abc.abstractmethod\n    def create_folder(self, folder_path):\n        \"\"\"\n            Create all nonexistent folders and subfolders in 'path'.\n\n        Args:\n            path (string): absolute path\n\n        Returns:\n            (string) folder id of lowest subfolder from 'path'\n        \"\"\"\n        pass\n\n    @abc.abstractmethod\n    def get_tree(self):\n        \"\"\"\n            Creates folder structure for providers which do not provide\n            tree folder structure (GDrive has no accessible tree structure,\n            only parents and their parents)\n        \"\"\"\n        pass\n\n    @abc.abstractmethod\n    def get_roots_config(self, anatomy=None):\n        \"\"\"\n            Returns root values for path resolving\n\n            Takes value from Anatomy which takes values from Settings\n            overridden by Local Settings\n\n        Returns:\n            (dict) - {\"root\": {\"root\": \"/My Drive\"}}\n                     OR\n                     {\"root\": {\"root_ONE\": \"value\", \"root_TWO\":\"value}}\n            Format is importing for usage of python's format ** approach\n        \"\"\"\n        pass\n\n    def resolve_path(self, path, root_config=None, anatomy=None):\n        \"\"\"\n            Replaces all root placeholders with proper values\n\n            Args:\n                path(string): root[work]/folder...\n                root_config (dict): {'work': \"c:/...\"...}\n                anatomy (Anatomy): object of Anatomy\n            Returns:\n                (string): proper url\n        \"\"\"\n        if not root_config:\n            root_config = self.get_roots_config(anatomy)\n\n        if root_config and not root_config.get(\"root\"):\n            root_config = {\"root\": root_config}\n\n        try:\n            if not root_config:\n                raise KeyError\n\n            path = path.format(**root_config)\n        except KeyError:\n            try:\n                path = anatomy.fill_root(path)\n            except KeyError:\n                msg = \"Error in resolving local root from anatomy\"\n                self.log.error(msg)\n                raise ValueError(msg)\n        except IndexError:\n            msg = \"Path {} contains unfillable placeholder\"\n            self.log.error(msg)\n            raise ValueError(msg)\n\n        return path\n"
  },
  {
    "path": "openpype/modules/sync_server/providers/dropbox.py",
    "content": "import os\n\nimport dropbox\n\nfrom .abstract_provider import AbstractProvider\nfrom ..utils import EditableScopes\n\n\nclass DropboxHandler(AbstractProvider):\n    CODE = 'dropbox'\n    LABEL = 'Dropbox'\n\n    def __init__(self, project_name, site_name, tree=None, presets=None):\n        self.active = False\n        self.site_name = site_name\n        self.presets = presets\n        self.dbx = None\n\n        if not self.presets:\n            self.log.info(\n                \"Sync Server: There are no presets for {}.\".format(site_name)\n            )\n            return\n\n        if not self.presets.get(\"enabled\"):\n            self.log.debug(\"Sync Server: Site {} not enabled for {}.\".\n                      format(site_name, project_name))\n            return\n\n        token = self.presets.get(\"token\", \"\")\n        if not token:\n            msg = \"Sync Server: No access token for dropbox provider\"\n            self.log.info(msg)\n            return\n\n        team_folder_name = self.presets.get(\"team_folder_name\", \"\")\n        if not team_folder_name:\n            msg = \"Sync Server: No team folder name for dropbox provider\"\n            self.log.info(msg)\n            return\n\n        acting_as_member = self.presets.get(\"acting_as_member\", \"\")\n        if not acting_as_member:\n            msg = (\n                \"Sync Server: No acting member for dropbox provider\"\n            )\n            self.log.info(msg)\n            return\n\n        try:\n            self.dbx = self._get_service(\n                token, acting_as_member, team_folder_name\n            )\n        except Exception as e:\n            self.log.info(\"Could not establish dropbox object: {}\".format(e))\n            return\n\n        super(AbstractProvider, self).__init__()\n\n    @classmethod\n    def get_system_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on system settings level\n\n\n            Returns:\n                (list) of dict\n        \"\"\"\n        return []\n\n    @classmethod\n    def get_project_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on project settings level\n\n\n            Returns:\n                (list) of dict\n        \"\"\"\n        # {platform} tells that value is multiplatform and only specific OS\n        # should be returned\n        return [\n            {\n                \"type\": \"text\",\n                \"key\": \"token\",\n                \"label\": \"Access Token\"\n            },\n            {\n                \"type\": \"text\",\n                \"key\": \"team_folder_name\",\n                \"label\": \"Team Folder Name\"\n            },\n            {\n                \"type\": \"text\",\n                \"key\": \"acting_as_member\",\n                \"label\": \"Acting As Member\"\n            },\n            # roots could be overridden only on Project level, User cannot\n            {\n                \"key\": \"root\",\n                \"label\": \"Roots\",\n                \"type\": \"dict-roots\",\n                \"object_type\": {\n                    \"type\": \"path\",\n                    \"multiplatform\": False,\n                    \"multipath\": False\n                }\n            }\n        ]\n\n    @classmethod\n    def get_local_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on local settings level\n\n\n            Returns:\n                (dict)\n        \"\"\"\n        return []\n\n    def _get_service(self, token, acting_as_member, team_folder_name):\n        dbx = dropbox.DropboxTeam(token)\n\n        # Getting member id.\n        member_id = None\n        member_names = []\n        for member in dbx.team_members_list().members:\n            member_names.append(member.profile.name.display_name)\n            if member.profile.name.display_name == acting_as_member:\n                member_id = member.profile.team_member_id\n\n        if member_id is None:\n            raise ValueError(\n                \"Could not find member \\\"{}\\\". Available members: {}\".format(\n                    acting_as_member, member_names\n                )\n            )\n\n        # Getting team folder id.\n        team_folder_id = None\n        team_folder_names = []\n        for entry in dbx.team_team_folder_list().team_folders:\n            team_folder_names.append(entry.name)\n            if entry.name == team_folder_name:\n                team_folder_id = entry.team_folder_id\n\n        if team_folder_id is None:\n            raise ValueError(\n                \"Could not find team folder \\\"{}\\\". Available folders: \"\n                \"{}\".format(\n                    team_folder_name, team_folder_names\n                )\n            )\n\n        # Establish dropbox object.\n        path_root = dropbox.common.PathRoot.namespace_id(team_folder_id)\n        return dropbox.DropboxTeam(\n            token\n        ).with_path_root(path_root).as_user(member_id)\n\n    def is_active(self):\n        \"\"\"\n            Returns True if provider is activated, eg. has working credentials.\n        Returns:\n            (boolean)\n        \"\"\"\n        return self.presets.get(\"enabled\") and self.dbx is not None\n\n    @classmethod\n    def get_configurable_items(cls):\n        \"\"\"\n            Returns filtered dict of editable properties\n\n\n            Returns:\n                (dict)\n        \"\"\"\n        editable = {\n            'token': {\n                'scope': [EditableScopes.PROJECT],\n                'label': \"Access Token\",\n                'type': 'text',\n                'namespace': (\n                    '{project_settings}/global/sync_server/sites/{site}/token'\n                )\n            },\n            'team_folder_name': {\n                'scope': [EditableScopes.PROJECT],\n                'label': \"Team Folder Name\",\n                'type': 'text',\n                'namespace': (\n                    '{project_settings}/global/sync_server/sites/{site}'\n                    '/team_folder_name'\n                )\n            },\n            'acting_as_member': {\n                'scope': [EditableScopes.PROJECT, EditableScopes.LOCAL],\n                'label': \"Acting As Member\",\n                'type': 'text',\n                'namespace': (\n                    '{project_settings}/global/sync_server/sites/{site}'\n                    '/acting_as_member'\n                )\n            }\n        }\n        return editable\n\n    def _path_exists(self, path):\n        try:\n            entries = self.dbx.files_list_folder(\n                path=os.path.dirname(path)\n            ).entries\n        except dropbox.exceptions.ApiError:\n            return False\n\n        for entry in entries:\n            if entry.name == os.path.basename(path):\n                return True\n\n        return False\n\n    def upload_file(self, source_path, path,\n                    server, project_name, file, representation, site,\n                    overwrite=False):\n        \"\"\"\n            Copy file from 'source_path' to 'target_path' on provider.\n            Use 'overwrite' boolean to rewrite existing file on provider\n\n        Args:\n            source_path (string):\n            path (string): absolute path with or without name of the file\n            overwrite (boolean): replace existing file\n\n            arguments for saving progress:\n            server (SyncServer): server instance to call update_db on\n            project_name (str):\n            file (dict): info about uploaded file (matches structure from db)\n            representation (dict): complete repre containing 'file'\n            site (str): site name\n        Returns:\n            (string) file_id of created file, raises exception\n        \"\"\"\n        # Check source path.\n        if not os.path.exists(source_path):\n            raise FileNotFoundError(\n                \"Source file {} doesn't exist.\".format(source_path)\n            )\n\n        if self._path_exists(path) and not overwrite:\n            raise FileExistsError(\n                \"File already exists, use 'overwrite' argument\"\n            )\n\n        mode = dropbox.files.WriteMode(\"add\", None)\n        if overwrite:\n            mode = dropbox.files.WriteMode.overwrite\n\n        with open(source_path, \"rb\") as f:\n            file_size = os.path.getsize(source_path)\n\n            CHUNK_SIZE = 50 * 1024 * 1024\n\n            if file_size <= CHUNK_SIZE:\n                self.dbx.files_upload(f.read(), path, mode=mode)\n            else:\n                upload_session_start_result = \\\n                    self.dbx.files_upload_session_start(f.read(CHUNK_SIZE))\n\n                cursor = dropbox.files.UploadSessionCursor(\n                    session_id=upload_session_start_result.session_id,\n                    offset=f.tell())\n\n                commit = dropbox.files.CommitInfo(path=path, mode=mode)\n\n                while f.tell() < file_size:\n                    if (file_size - f.tell()) <= CHUNK_SIZE:\n                        self.dbx.files_upload_session_finish(\n                            f.read(CHUNK_SIZE),\n                            cursor,\n                            commit)\n                    else:\n                        self.dbx.files_upload_session_append(\n                            f.read(CHUNK_SIZE),\n                            cursor.session_id,\n                            cursor.offset)\n                        cursor.offset = f.tell()\n\n        server.update_db(\n            project_name=project_name,\n            new_file_id=None,\n            file=file,\n            representation=representation,\n            site=site,\n            progress=100\n        )\n\n        return path\n\n    def download_file(self, source_path, local_path,\n                      server, project_name, file, representation, site,\n                      overwrite=False):\n        \"\"\"\n            Download file from provider into local system\n\n        Args:\n            source_path (string): absolute path on provider\n            local_path (string): absolute path with or without name of the file\n            overwrite (boolean): replace existing file\n\n            arguments for saving progress:\n            server (SyncServer): server instance to call update_db on\n            project_name (str):\n            file (dict): info about uploaded file (matches structure from db)\n            representation (dict): complete repre containing 'file'\n            site (str): site name\n        Returns:\n            None\n        \"\"\"\n        # Check source path.\n        if not self._path_exists(source_path):\n            raise FileNotFoundError(\n                \"Source file {} doesn't exist.\".format(source_path)\n            )\n\n        if os.path.exists(local_path) and not overwrite:\n            raise FileExistsError(\n                \"File already exists, use 'overwrite' argument\"\n            )\n\n        if os.path.exists(local_path) and overwrite:\n            os.unlink(local_path)\n\n        self.dbx.files_download_to_file(local_path, source_path)\n\n        server.update_db(\n            project_name=project_name,\n            new_file_id=None,\n            file=file,\n            representation=representation,\n            site=site,\n            progress=100\n        )\n\n        return os.path.basename(source_path)\n\n    def delete_file(self, path):\n        \"\"\"\n            Deletes file from 'path'. Expects path to specific file.\n\n        Args:\n            path (string): absolute path to particular file\n\n        Returns:\n            None\n        \"\"\"\n        if not self._path_exists(path):\n            raise FileExistsError(\"File {} doesn't exist\".format(path))\n\n        self.dbx.files_delete(path)\n\n    def list_folder(self, folder_path):\n        \"\"\"\n            List all files and subfolders of particular path non-recursively.\n        Args:\n            folder_path (string): absolut path on provider\n\n        Returns:\n            (list)\n        \"\"\"\n        if not self._path_exists(folder_path):\n            raise FileExistsError(\n                \"Folder \\\"{}\\\" does not exist\".format(folder_path)\n            )\n\n        entry_names = []\n        for entry in self.dbx.files_list_folder(path=folder_path).entries:\n            entry_names.append(entry.name)\n        return entry_names\n\n    def create_folder(self, folder_path):\n        \"\"\"\n            Create all nonexistent folders and subfolders in 'path'.\n\n        Args:\n            path (string): absolute path\n\n        Returns:\n            (string) folder id of lowest subfolder from 'path'\n        \"\"\"\n        if self._path_exists(folder_path):\n            return folder_path\n\n        self.dbx.files_create_folder_v2(folder_path)\n\n        return folder_path\n\n    def get_tree(self):\n        \"\"\"\n            Creates folder structure for providers which do not provide\n            tree folder structure (GDrive has no accessible tree structure,\n            only parents and their parents)\n        \"\"\"\n        pass\n\n    def get_roots_config(self, anatomy=None):\n        \"\"\"\n            Returns root values for path resolving\n\n            Takes value from Anatomy which takes values from Settings\n            overridden by Local Settings\n\n        Returns:\n            (dict) - {\"root\": {\"root\": \"/My Drive\"}}\n                     OR\n                     {\"root\": {\"root_ONE\": \"value\", \"root_TWO\":\"value}}\n            Format is importing for usage of python's format ** approach\n        \"\"\"\n        return self.presets['root']\n\n    def resolve_path(self, path, root_config=None, anatomy=None):\n        \"\"\"\n            Replaces all root placeholders with proper values\n\n            Args:\n                path(string): root[work]/folder...\n                root_config (dict): {'work': \"c:/...\"...}\n                anatomy (Anatomy): object of Anatomy\n            Returns:\n                (string): proper url\n        \"\"\"\n        if not root_config:\n            root_config = self.get_roots_config(anatomy)\n\n        if root_config and not root_config.get(\"root\"):\n            root_config = {\"root\": root_config}\n\n        try:\n            if not root_config:\n                raise KeyError\n\n            path = path.format(**root_config)\n        except KeyError:\n            try:\n                path = anatomy.fill_root(path)\n            except KeyError:\n                msg = \"Error in resolving local root from anatomy\"\n                self.log.error(msg)\n                raise ValueError(msg)\n\n        return path\n"
  },
  {
    "path": "openpype/modules/sync_server/providers/gdrive.py",
    "content": "from __future__ import print_function\nimport os.path\nimport time\nimport sys\nimport six\nimport platform\n\nfrom openpype.lib import Logger\nfrom openpype.settings import get_system_settings\nfrom .abstract_provider import AbstractProvider\nfrom ..utils import time_function, ResumableError\n\nlog = Logger.get_logger(\"GDriveHandler\")\n\ntry:\n    from googleapiclient.discovery import build\n    import google.oauth2.service_account as service_account\n    from googleapiclient import errors\n    from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload\nexcept (ImportError, SyntaxError):\n    if six.PY3:\n        six.reraise(*sys.exc_info())\n\n    # handle imports from Python 2 hosts - in those only basic methods are used\n    log.warning(\"Import failed, imported from Python 2, operations will fail.\")\n\nSCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly',\n          'https://www.googleapis.com/auth/drive.file',\n          'https://www.googleapis.com/auth/drive.readonly']  # for write|delete\n\n\nclass GDriveHandler(AbstractProvider):\n    \"\"\"\n        Implementation of Google Drive API.\n        As GD API doesn't have real folder structure, 'tree' in memory\n        structure is build in constructor to map folder paths to folder ids,\n        which are used in API. Building of this tree might be expensive and\n        slow and should be run only when necessary. Currently is set to\n        lazy creation, created only after first call when necessary.\n\n        Configuration for provider is in\n            'settings/defaults/project_settings/global.json'\n\n        Settings could be overwritten per project.\n\n        Example of config:\n          \"gdrive\": {   - site name\n            \"provider\": \"gdrive\", - type of provider, label must be registered\n            \"credentials_url\": \"/my_secret_folder/credentials.json\",\n            \"root\": {  - could be \"root\": \"/My Drive\" for single root\n                \"root_one\": \"/My Drive\",\n                \"root_two\": \"/My Drive/different_folder\"\n            }\n          }\n    \"\"\"\n    CODE = 'gdrive'\n    LABEL = 'Google Drive'\n\n    FOLDER_STR = 'application/vnd.google-apps.folder'\n    MY_DRIVE_STR = 'My Drive'  # name of root folder of regular Google drive\n    CHUNK_SIZE = 2097152  # must be divisible by 256! used for upload chunks\n\n    def __init__(self, project_name, site_name, tree=None, presets=None):\n        self.active = False\n        self.project_name = project_name\n        self.site_name = site_name\n        self.service = None\n        self.root = None\n\n        self.presets = presets\n        if not self.presets:\n            self.log.info(\n                \"Sync Server: There are no presets for {}.\".format(site_name)\n            )\n            return\n\n        if not self.presets.get(\"enabled\"):\n            self.log.debug(\n                \"Sync Server: Site {} not enabled for {}.\".format(\n                    site_name, project_name\n                )\n            )\n            return\n\n        current_platform = platform.system().lower()\n        cred_path = self.presets.get(\"credentials_url\", {}). \\\n            get(current_platform) or ''\n\n        if not cred_path:\n            msg = \"Sync Server: Please, fill the credentials for gdrive \"\\\n                  \"provider for platform '{}' !\".format(current_platform)\n            self.log.info(msg)\n            return\n\n        try:\n            cred_path = cred_path.format(**os.environ)\n        except KeyError as e:\n            self.log.info((\n                \"Sync Server: The key(s) {} does not exist in the \"\n                \"environment variables\"\n            ).format(\" \".join(e.args)))\n            return\n\n        if not os.path.exists(cred_path):\n            msg = \"Sync Server: No credentials for gdrive provider \" + \\\n                  \"for '{}' on path '{}'!\".format(site_name, cred_path)\n            self.log.info(msg)\n            return\n\n        self.service = None\n        self.service = self._get_gd_service(cred_path)\n\n        self._tree = tree\n        self.active = True\n\n    def is_active(self):\n        \"\"\"\n            Returns True if provider is activated, eg. has working credentials.\n        Returns:\n            (boolean)\n        \"\"\"\n        return self.presets.get(\"enabled\") and self.service is not None\n\n    @classmethod\n    def get_system_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on system settings level\n\n\n            Returns:\n                (list) of dict\n        \"\"\"\n        return []\n\n    @classmethod\n    def get_project_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on project settings level\n\n\n            Returns:\n                (list) of dict\n        \"\"\"\n        # {platform} tells that value is multiplatform and only specific OS\n        # should be returned\n        editable = [\n            # credentials could be overridden on Project or User level\n            {\n                \"type\": \"path\",\n                \"key\": \"credentials_url\",\n                \"label\": \"Credentials url\",\n                \"multiplatform\": True,\n                \"placeholder\": \"Credentials url\"\n            },\n            # roots could be overridden only on Project level, User cannot\n            {\n                \"key\": \"root\",\n                \"label\": \"Roots\",\n                \"type\": \"dict-roots\",\n                \"object_type\": {\n                    \"type\": \"path\",\n                    \"multiplatform\": False,\n                    \"multipath\": False\n                }\n            }\n        ]\n        return editable\n\n    @classmethod\n    def get_local_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on local settings level\n\n\n            Returns:\n                (dict)\n        \"\"\"\n        editable = [\n            # credentials could be override on Project or User level\n            {\n                'key': \"credentials_url\",\n                'label': \"Credentials url\",\n                'type': 'text',\n                'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}'  # noqa: E501\n            }\n        ]\n        return editable\n\n    def get_roots_config(self, anatomy=None):\n        \"\"\"\n            Returns root values for path resolving\n\n            Use only Settings as GDrive cannot be modified by Local Settings\n\n        Returns:\n            (dict) - {\"root\": {\"root\": \"/My Drive\"}}\n                     OR\n                     {\"root\": {\"root_ONE\": \"value\", \"root_TWO\":\"value}}\n            Format is importing for usage of python's format ** approach\n        \"\"\"\n        # GDrive roots cannot be locally overridden\n        return self.presets['root']\n\n    def get_tree(self):\n        \"\"\"\n            Building of the folder tree could be potentially expensive,\n            constructor provides argument that could inject previously created\n            tree.\n            Tree structure must be handled in thread safe fashion!\n        Returns:\n             (dictionary) - url to id mapping\n        \"\"\"\n        if not self._tree:\n            self._tree = self._build_tree(self.list_folders())\n        return self._tree\n\n    def create_folder(self, path):\n        \"\"\"\n            Create all nonexistent folders and subfolders in 'path'.\n            Updates self._tree structure with new paths\n\n        Args:\n            path (string): absolute path, starts with GDrive root,\n                           without filename\n        Returns:\n            (string) folder id of lowest subfolder from 'path'\n        \"\"\"\n        folder_id = self.folder_path_exists(path)\n        if folder_id:\n            return folder_id\n        parts = path.split('/')\n        folders_to_create = []\n\n        while parts:\n            folders_to_create.append(parts.pop())\n            path = '/'.join(parts)\n            path = path.strip()\n            folder_id = self.folder_path_exists(path)  # lowest common path\n            if folder_id:\n                while folders_to_create:\n                    new_folder_name = folders_to_create.pop()\n                    folder_metadata = {\n                        'name': new_folder_name,\n                        'mimeType': 'application/vnd.google-apps.folder',\n                        'parents': [folder_id]\n                    }\n                    folder = self.service.files().create(\n                        body=folder_metadata,\n                        supportsAllDrives=True,\n                        fields='id').execute()\n                    folder_id = folder[\"id\"]\n\n                    new_path_key = path + '/' + new_folder_name\n                    self.get_tree()[new_path_key] = {\"id\": folder_id}\n\n                    path = new_path_key\n                return folder_id\n\n    def upload_file(self, source_path, path,\n                    server, project_name, file, representation, site,\n                    overwrite=False):\n        \"\"\"\n            Uploads single file from 'source_path' to destination 'path'.\n            It creates all folders on the path if are not existing.\n\n        Args:\n            source_path (string):\n            path (string): absolute path with or without name of the file\n            overwrite (boolean): replace existing file\n\n            arguments for saving progress:\n            server (SyncServer): server instance to call update_db on\n            project_name (str):\n            file (dict): info about uploaded file (matches structure from db)\n            representation (dict): complete repre containing 'file'\n            site (str): site name\n\n        Returns:\n            (string) file_id of created/modified file ,\n                throws FileExistsError, FileNotFoundError exceptions\n        \"\"\"\n        if not os.path.isfile(source_path):\n            raise FileNotFoundError(\"Source file {} doesn't exist.\"\n                                    .format(source_path))\n\n        root, ext = os.path.splitext(path)\n        if ext:\n            # full path\n            target_name = os.path.basename(path)\n            path = os.path.dirname(path)\n        else:\n            target_name = os.path.basename(source_path)\n        target_file = self.file_path_exists(path + \"/\" + target_name)\n        if target_file and not overwrite:\n            raise FileExistsError(\"File already exists, \"\n                                  \"use 'overwrite' argument\")\n\n        folder_id = self.folder_path_exists(path)\n        if not folder_id:\n            raise NotADirectoryError(\"Folder {} doesn't exists\".format(path))\n\n        file_metadata = {\n            'name': target_name\n        }\n        media = MediaFileUpload(source_path,\n                                mimetype='application/octet-stream',\n                                chunksize=self.CHUNK_SIZE,\n                                resumable=True)\n\n        try:\n            if not target_file:\n                # update doesnt like parent\n                file_metadata['parents'] = [folder_id]\n\n                request = self.service.files().create(body=file_metadata,\n                                                      supportsAllDrives=True,\n                                                      media_body=media,\n                                                      fields='id')\n            else:\n                request = self.service.files().update(fileId=target_file[\"id\"],\n                                                      body=file_metadata,\n                                                      supportsAllDrives=True,\n                                                      media_body=media,\n                                                      fields='id')\n\n            media.stream()\n            self.log.debug(\"Start Upload! {}\".format(source_path))\n            last_tick = status = response = None\n            status_val = 0\n            while response is None:\n                if status:\n                    status_val = float(status.progress())\n                if not last_tick or \\\n                        time.time() - last_tick >= server.LOG_PROGRESS_SEC:\n                    last_tick = time.time()\n                    self.log.debug(\"Uploaded %d%%.\" %\n                              int(status_val * 100))\n                    server.update_db(project_name=project_name,\n                                     new_file_id=None,\n                                     file=file,\n                                     representation=representation,\n                                     site=site,\n                                     progress=status_val\n                                     )\n                if server.is_representation_paused(\n                    project_name,\n                    representation['_id'],\n                    site,\n                    check_parents=True\n                ):\n                    raise ValueError(\"Paused during process, please redo.\")\n                status, response = request.next_chunk()\n\n        except errors.HttpError as ex:\n            if ex.resp['status'] == '404':\n                return False\n            if ex.resp['status'] == '403':\n                # real permission issue\n                if 'has not granted' in ex._get_reason().strip():\n                    raise PermissionError(ex._get_reason().strip())\n\n                self.log.warning(\n                    \"Forbidden received, hit quota. Injecting 60s delay.\"\n                )\n                time.sleep(60)\n                return False\n            raise\n        return response['id']\n\n    def download_file(self, source_path, local_path,\n                      server, project_name, file, representation, site,\n                      overwrite=False):\n        \"\"\"\n            Downloads single file from 'source_path' (remote) to 'local_path'.\n            It creates all folders on the local_path if are not existing.\n            By default existing file on 'local_path' will trigger an exception\n\n        Args:\n            source_path (string): absolute path on provider\n            local_path (string): absolute path with or without name of the file\n            overwrite (boolean): replace existing file\n\n            arguments for saving progress:\n            server (SyncServer): server instance to call update_db on\n            project_name (str):\n            file (dict): info about uploaded file (matches structure from db)\n            representation (dict): complete repre containing 'file'\n            site (str): site name\n\n        Returns:\n            (string) file_id of created/modified file ,\n                throws FileExistsError, FileNotFoundError exceptions\n        \"\"\"\n        remote_file = self.file_path_exists(source_path)\n        if not remote_file:\n            raise FileNotFoundError(\"Source file {} doesn't exist.\"\n                                    .format(source_path))\n\n        root, ext = os.path.splitext(local_path)\n        if ext:\n            # full path with file name\n            target_name = os.path.basename(local_path)\n            local_path = os.path.dirname(local_path)\n        else:  # just folder, get file name from source\n            target_name = os.path.basename(source_path)\n\n        local_file = os.path.isfile(local_path + \"/\" + target_name)\n\n        if local_file and not overwrite:\n            raise FileExistsError(\"File already exists, \"\n                                  \"use 'overwrite' argument\")\n\n        request = self.service.files().get_media(fileId=remote_file[\"id\"],\n                                                 supportsAllDrives=True)\n\n        with open(local_path + \"/\" + target_name, \"wb\") as fh:\n            downloader = MediaIoBaseDownload(fh, request)\n            last_tick = status = response = None\n            status_val = 0\n            while response is None:\n                if status:\n                    status_val = float(status.progress())\n                if not last_tick or \\\n                        time.time() - last_tick >= server.LOG_PROGRESS_SEC:\n                    last_tick = time.time()\n                    self.log.debug(\"Downloaded %d%%.\" %\n                              int(status_val * 100))\n                    server.update_db(project_name=project_name,\n                                     new_file_id=None,\n                                     file=file,\n                                     representation=representation,\n                                     site=site,\n                                     progress=status_val\n                                     )\n                if server.is_representation_paused(\n                    project_name,\n                    representation['_id'],\n                    site,\n                    check_parents=True\n                ):\n                    raise ValueError(\"Paused during process, please redo.\")\n                status, response = downloader.next_chunk()\n\n        return target_name\n\n    def delete_folder(self, path, force=False):\n        \"\"\"\n            Deletes folder on GDrive. Checks if folder contains any files or\n            subfolders. In that case raises error, could be overridden by\n            'force' argument.\n            In that case deletes folder on 'path' and all its children.\n\n        Args:\n            path (string): absolute path on GDrive\n            force (boolean): delete even if children in folder\n\n        Returns:\n            None\n        \"\"\"\n        folder_id = self.folder_path_exists(path)\n        if not folder_id:\n            raise ValueError(\"Not valid folder path {}\".format(path))\n\n        fields = 'nextPageToken, files(id, name, parents)'\n        q = self._handle_q(\"'{}' in parents \".format(folder_id))\n        response = self.service.files().list(\n            q=q,\n            corpora=\"allDrives\",\n            includeItemsFromAllDrives=True,\n            supportsAllDrives=True,\n            pageSize='1',\n            fields=fields).execute()\n        children = response.get('files', [])\n        if children and not force:\n            raise ValueError(\"Folder {} is not empty, use 'force'\".\n                             format(path))\n\n        self.service.files().delete(fileId=folder_id,\n                                    supportsAllDrives=True).execute()\n\n    def delete_file(self, path):\n        \"\"\"\n            Deletes file from 'path'. Expects path to specific file.\n\n        Args:\n            path: absolute path to particular file\n\n        Returns:\n            None\n        \"\"\"\n        file = self.file_path_exists(path)\n        if not file:\n            raise ValueError(\"File {} doesn't exist\")\n        self.service.files().delete(fileId=file[\"id\"],\n                                    supportsAllDrives=True).execute()\n\n    def list_folder(self, folder_path):\n        \"\"\"\n            List all files and subfolders of particular path non-recursively.\n\n        Args:\n            folder_path (string): absolut path on provider\n        Returns:\n             (list)\n        \"\"\"\n        pass\n\n    @time_function\n    def list_folders(self):\n        \"\"\" Lists all folders in GDrive.\n            Used to build in-memory structure of path to folder ids model.\n\n        Returns:\n            (list) of dictionaries('id', 'name', [parents])\n        \"\"\"\n        folders = []\n        page_token = None\n        fields = 'nextPageToken, files(id, name, parents)'\n        while True:\n            q = self._handle_q(\"mimeType='application/vnd.google-apps.folder'\")\n            response = self.service.files().list(\n                q=q,\n                pageSize=1000,\n                corpora=\"allDrives\",\n                includeItemsFromAllDrives=True,\n                supportsAllDrives=True,\n                fields=fields,\n                pageToken=page_token).execute()\n            folders.extend(response.get('files', []))\n            page_token = response.get('nextPageToken', None)\n            if page_token is None:\n                break\n\n        return folders\n\n    def list_files(self):\n        \"\"\" Lists all files in GDrive\n            Runs loop through possibly multiple pages. Result could be large,\n            if it would be a problem, change it to generator\n        Returns:\n            (list) of dictionaries('id', 'name', [parents])\n        \"\"\"\n        files = []\n        page_token = None\n        fields = 'nextPageToken, files(id, name, parents)'\n        while True:\n            q = self._handle_q(\"\")\n            response = self.service.files().list(\n                q=q,\n                corpora=\"allDrives\",\n                includeItemsFromAllDrives=True,\n                supportsAllDrives=True,\n                fields=fields,\n                pageToken=page_token).execute()\n            files.extend(response.get('files', []))\n            page_token = response.get('nextPageToken', None)\n            if page_token is None:\n                break\n\n        return files\n\n    def folder_path_exists(self, file_path):\n        \"\"\"\n            Checks if path from 'file_path' exists. If so, return its\n            folder id.\n        Args:\n            file_path (string): gdrive path with / as a separator\n        Returns:\n            (string) folder id or False\n        \"\"\"\n        if not file_path:\n            return False\n\n        root, ext = os.path.splitext(file_path)\n        if not ext:\n            file_path += '/'\n\n        dir_path = os.path.dirname(file_path)\n\n        path = self.get_tree().get(dir_path, None)\n        if path:\n            return path[\"id\"]\n\n        return False\n\n    def file_path_exists(self, file_path):\n        \"\"\"\n            Checks if 'file_path' exists on GDrive\n\n        Args:\n            file_path (string): separated by '/', from root, with file name\n        Returns:\n            (dictionary|boolean) file metadata | False if not found\n        \"\"\"\n        folder_id = self.folder_path_exists(file_path)\n        if folder_id:\n            return self.file_exists(os.path.basename(file_path), folder_id)\n        return False\n\n    def file_exists(self, file_name, folder_id):\n        \"\"\"\n            Checks if 'file_name' exists in 'folder_id'\n\n        Args:\n            file_name (string):\n            folder_id (int): google drive folder id\n\n        Returns:\n            (dictionary|boolean) file metadata, False if not found\n        \"\"\"\n        q = self._handle_q(\"name = '{}' and '{}' in parents\"\n                           .format(file_name, folder_id))\n        response = self.service.files().list(\n            q=q,\n            corpora=\"allDrives\",\n            includeItemsFromAllDrives=True,\n            supportsAllDrives=True,\n            fields='nextPageToken, files(id, name, parents, '\n                   'mimeType, modifiedTime,size,md5Checksum)').execute()\n        if len(response.get('files')) > 1:\n            raise ValueError(\"Too many files returned for {} in {}\"\n                             .format(file_name, folder_id))\n\n        file = response.get('files', [])\n        if not file:\n            return False\n        return file[0]\n\n    @classmethod\n    def get_presets(cls):\n        \"\"\"\n            Get presets for this provider\n        Returns:\n            (dictionary) of configured sites\n        \"\"\"\n        provider_presets = None\n        try:\n            provider_presets = (\n                get_system_settings()[\"modules\"]\n                [\"sync_server\"]\n                [\"providers\"]\n                [\"gdrive\"]\n            )\n        except KeyError:\n            log.info((\n                \"Sync Server: There are no presets for Gdrive provider.\"\n            ).format(str(provider_presets)))\n            return\n        return provider_presets\n\n    def _get_gd_service(self, credentials_path):\n        \"\"\"\n            Authorize client with 'credentials.json', uses service account.\n            Service account needs to have target folder shared with.\n            Produces service that communicates with GDrive API.\n\n        Returns:\n            None\n        \"\"\"\n        service = None\n        try:\n            creds = service_account.Credentials.from_service_account_file(\n                credentials_path,\n                scopes=SCOPES)\n            service = build('drive', 'v3',\n                            credentials=creds, cache_discovery=False)\n        except Exception:\n            log.error(\"Connection failed, \" +\n                      \"check '{}' credentials file\".format(credentials_path),\n                      exc_info=True)\n\n        return service\n\n    def _prepare_root_info(self):\n        \"\"\"\n            Prepare info about roots and theirs folder ids from 'presets'.\n            Configuration might be for single or multiroot projects.\n            Regular My Drive and Shared drives are implemented, their root\n            folder ids need to be queried in slightly different way.\n\n        Returns:\n            (dicts) of dicts where root folders are keys\n            throws ResumableError in case of errors.HttpError\n        \"\"\"\n        roots = {}\n        config_roots = self.get_roots_config()\n        try:\n            for path in config_roots.values():\n                if self.MY_DRIVE_STR in path:\n                    roots[self.MY_DRIVE_STR] = self.service.files()\\\n                                                   .get(fileId='root')\\\n                                                   .execute()\n                else:\n                    shared_drives = []\n                    page_token = None\n\n                    while True:\n                        response = self.service.drives().list(\n                            pageSize=100,\n                            pageToken=page_token).execute()\n                        shared_drives.extend(response.get('drives', []))\n                        page_token = response.get('nextPageToken', None)\n                        if page_token is None:\n                            break\n\n                    folders = path.split('/')\n                    if len(folders) < 2:\n                        raise ValueError(\"Wrong root folder definition {}\".\n                                         format(path))\n\n                    for shared_drive in shared_drives:\n                        if folders[1] in shared_drive[\"name\"]:\n                            roots[shared_drive[\"name\"]] = {\n                                \"name\": shared_drive[\"name\"],\n                                \"id\": shared_drive[\"id\"]}\n            if self.MY_DRIVE_STR not in roots:  # add My Drive always\n                roots[self.MY_DRIVE_STR] = self.service.files() \\\n                    .get(fileId='root').execute()\n        except errors.HttpError:\n            self.log.warning(\"HttpError in sync loop, \"\n                        \"trying next loop\",\n                        exc_info=True)\n            raise ResumableError\n\n        return roots\n\n    @time_function\n    def _build_tree(self, folders):\n        \"\"\"\n            Create in-memory structure resolving paths to folder id as\n            recursive querying might be slower.\n            Initialized in the time of class initialization.\n            Maybe should be persisted\n            Tree is structure of path to id:\n                '/ROOT': {'id': '1234567'}\n                '/ROOT/PROJECT_FOLDER': {'id':'222222'}\n                '/ROOT/PROJECT_FOLDER/Assets': {'id': '3434545'}\n        Args:\n            folders (list): list of dictionaries with folder metadata\n        Returns:\n            (dictionary) path as a key, folder id as a value\n        \"\"\"\n        self.log.debug(\"build_tree len {}\".format(len(folders)))\n        if not self.root:  # build only when necessary, could be expensive\n            self.root = self._prepare_root_info()\n\n        root_ids = []\n        default_root_id = None\n        tree = {}\n        ending_by = {}\n        for root_name, root in self.root.items():  # might be multiple roots\n            if root[\"id\"] not in root_ids:\n                tree[\"/\" + root_name] = {\"id\": root[\"id\"]}\n                ending_by[root[\"id\"]] = \"/\" + root_name\n                root_ids.append(root[\"id\"])\n\n                if self.MY_DRIVE_STR == root_name:\n                    default_root_id = root[\"id\"]\n\n        no_parents_yet = {}\n        while folders:\n            folder = folders.pop(0)\n            parents = folder.get(\"parents\", [])\n            # weird cases, shared folders, etc, parent under root\n            if not parents:\n                parent = default_root_id\n            else:\n                parent = parents[0]\n\n            if folder[\"id\"] in root_ids:  # do not process root\n                continue\n\n            if parent in ending_by:\n                path_key = ending_by[parent] + \"/\" + folder[\"name\"]\n                ending_by[folder[\"id\"]] = path_key\n                tree[path_key] = {\"id\": folder[\"id\"]}\n            else:\n                no_parents_yet.setdefault(parent, []).append((folder[\"id\"],\n                                                              folder[\"name\"]))\n        loop_cnt = 0\n        # break if looped more then X times - safety against infinite loop\n        while no_parents_yet and loop_cnt < 20:\n\n            keys = list(no_parents_yet.keys())\n            for parent in keys:\n                if parent in ending_by.keys():\n                    subfolders = no_parents_yet.pop(parent)\n                    for folder_id, folder_name in subfolders:\n                        path_key = ending_by[parent] + \"/\" + folder_name\n                        ending_by[folder_id] = path_key\n                        tree[path_key] = {\"id\": folder_id}\n            loop_cnt += 1\n\n        if len(no_parents_yet) > 0:\n            self.log.debug(\"Some folders path are not resolved {}\".\n                      format(no_parents_yet))\n            self.log.debug(\"Remove deleted folders from trash.\")\n\n        return tree\n\n    def _get_folder_metadata(self, path):\n        \"\"\"\n            Get info about folder with 'path'\n        Args:\n            path (string):\n\n        Returns:\n         (dictionary) with metadata or raises ValueError\n        \"\"\"\n        try:\n            return self.get_tree()[path]\n        except Exception:\n            raise ValueError(\"Uknown folder id {}\".format(id))\n\n    def _handle_q(self, q, trashed=False):\n        \"\"\" API list call contain trashed and hidden files/folder by default.\n            Usually we dont want those, must be included in query explicitly.\n\n        Args:\n            q (string): query portion\n            trashed (boolean): False|True\n\n        Returns:\n            (string) - modified query\n        \"\"\"\n        parts = [q]\n        if not trashed:\n            parts.append(\" trashed = false \")\n\n        return \" and \".join(parts)\n\n\nif __name__ == '__main__':\n    gd = GDriveHandler('gdrive')\n    print(gd.root)\n    print(gd.get_tree())\n"
  },
  {
    "path": "openpype/modules/sync_server/providers/lib.py",
    "content": "from .gdrive import GDriveHandler\nfrom .dropbox import DropboxHandler\nfrom .local_drive import LocalDriveHandler\nfrom .sftp import SFTPHandler\n\n\nclass ProviderFactory:\n    \"\"\"\n        Factory class as a creator of multiple cloud destination.\n        Each new implementation needs to be registered and added to Providers\n        enum.\n    \"\"\"\n    def __init__(self):\n        self.providers = {}  # {'PROVIDER_LABEL: {cls, int},..}\n\n    def register_provider(self, provider, creator, batch_limit):\n        \"\"\"\n            Provide all necessary information for one specific remote provider\n        Args:\n            provider (string): name of provider\n            creator (class): class implementing AbstractProvider\n            batch_limit (int): number of files that could be processed in\n                                    one loop (based on provider API quota)\n        Returns:\n            modifies self.providers and self.sites\n        \"\"\"\n        self.providers[provider] = (creator, batch_limit)\n\n    def get_provider(self, provider, project_name, site_name,\n                     tree=None, presets=None):\n        \"\"\"\n            Returns new instance of provider client for specific site.\n            One provider could have multiple sites.\n\n            'tree' is used for injecting already created memory structure,\n            without it constructor of provider would need to calculate it\n            from scratch, which could be expensive.\n        Args:\n            provider (string):  'gdrive','S3'\n            site_name (string): descriptor of site, different service accounts\n                must have different site name\n            project_name (string): different projects could have diff. sites\n            tree (dictionary):  - folder paths to folder id structure\n            presets (dictionary): config for provider and site (eg.\n                \"credentials_url\"..)\n        Returns:\n            (implementation of AbstractProvider)\n        \"\"\"\n        creator_info = self._get_creator_info(provider)\n        # call init\n        site = creator_info[0](project_name, site_name, tree, presets)\n\n        return site\n\n    def get_provider_batch_limit(self, provider):\n        \"\"\"\n            Each provider has some limit of files that could be  processed in\n            one batch (loop step). It is not 'file' limit per se, but\n            calculation based on API queries for provider.\n            (For example 'gdrive' has 1000 queries for 100 sec, one file could\n            be multiple queries (one for each level of path + check if file\n            exists)\n        Args:\n            provider (string): 'gdrive','S3'\n        Returns:\n        \"\"\"\n        info = self._get_creator_info(provider)\n        return info[1]\n\n    def get_provider_configurable_items(self, provider):\n        \"\"\"\n            Returns dict of modifiable properties for 'provider'.\n\n            Provider contains information which its properties and on what\n            level could be override\n        \"\"\"\n        provider_info = self._get_creator_info(provider)\n\n        return provider_info[0].get_configurable_items()\n\n    def get_provider_cls(self, provider_code):\n        \"\"\"\n            Returns class object for 'provider_code' to run class methods on.\n        \"\"\"\n        provider_info = self._get_creator_info(provider_code)\n\n        return provider_info[0]\n\n    def _get_creator_info(self, provider):\n        \"\"\"\n            Collect all necessary info for provider. Currently only creator\n            class and batch limit.\n        Args:\n            provider (string): 'gdrive' etc\n        Returns:\n            (tuple): (creator, batch_limit)\n                creator is class of a provider (ex: GDriveHandler)\n                batch_limit denotes how many files synced at single loop\n                   its provided via 'register_provider' as its needed even\n                   before provider class is initialized itself\n                   (setting it as a class variable didn't work)\n        \"\"\"\n        creator_info = self.providers.get(provider)\n        if not creator_info:\n            raise ValueError(\n                \"Provider {} not registered yet\".format(provider))\n        return creator_info\n\n\nfactory = ProviderFactory()\n# this says that there is implemented provider with a label 'gdrive'\n# there is implementing 'GDriveHandler' class\n# 7 denotes number of files that could be synced in single loop - learned by\n# trial and error\nfactory.register_provider(GDriveHandler.CODE, GDriveHandler, 7)\nfactory.register_provider(DropboxHandler.CODE, DropboxHandler, 10)\nfactory.register_provider(LocalDriveHandler.CODE, LocalDriveHandler, 50)\nfactory.register_provider(SFTPHandler.CODE, SFTPHandler, 20)\n"
  },
  {
    "path": "openpype/modules/sync_server/providers/local_drive.py",
    "content": "from __future__ import print_function\nimport os.path\nimport shutil\nimport threading\nimport time\n\nfrom openpype.lib import Logger\nfrom openpype.lib.local_settings import get_local_site_id\nfrom openpype.pipeline import Anatomy\nfrom .abstract_provider import AbstractProvider\n\nlog = Logger.get_logger(\"SyncServer\")\n\n\nclass LocalDriveHandler(AbstractProvider):\n    CODE = 'local_drive'\n    LABEL = 'Local drive'\n\n    \"\"\" Handles required operations on mounted disks with OS \"\"\"\n    def __init__(self, project_name, site_name, tree=None, presets=None):\n        self.presets = None\n        self.active = False\n        self.project_name = project_name\n        self.site_name = site_name\n        self._editable_properties = {}\n\n        self.active = self.is_active()\n\n    def is_active(self):\n        return True\n\n    @classmethod\n    def get_system_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on system settings level\n\n\n            Returns:\n                (list) of dict\n        \"\"\"\n        return []\n\n    @classmethod\n    def get_project_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on project settings level\n\n\n            Returns:\n                (list) of dict\n        \"\"\"\n        # for non 'studio' sites, 'studio' is configured in Anatomy\n        editable = [\n            {\n                \"key\": \"root\",\n                \"label\": \"Roots\",\n                \"type\": \"dict-roots\",\n                \"object_type\": {\n                    \"type\": \"path\",\n                    \"multiplatform\": True,\n                    \"multipath\": False\n                }\n            }\n        ]\n        return editable\n\n    @classmethod\n    def get_local_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on local settings level\n\n\n            Returns:\n                (dict)\n        \"\"\"\n        editable = [\n            {\n                'key': \"root\",\n                'label': \"Roots\",\n                'type': 'dict'\n            }\n        ]\n        return editable\n\n    def upload_file(self, source_path, target_path,\n                    server, project_name, file, representation, site,\n                    overwrite=False, direction=\"Upload\"):\n        \"\"\"\n            Copies file from 'source_path' to 'target_path'\n        \"\"\"\n        if not os.path.isfile(source_path):\n            raise FileNotFoundError(\"Source file {} doesn't exist.\"\n                                    .format(source_path))\n\n        if overwrite:\n            thread = threading.Thread(target=self._copy,\n                                      args=(source_path, target_path))\n            thread.start()\n            self._mark_progress(project_name, file, representation, server,\n                                site, source_path, target_path, direction)\n        else:\n            if os.path.exists(target_path):\n                raise ValueError(\"File {} exists, set overwrite\".\n                                 format(target_path))\n\n        return os.path.basename(target_path)\n\n    def download_file(self, source_path, local_path,\n                      server, project_name, file, representation, site,\n                      overwrite=False):\n        \"\"\"\n            Download a file form 'source_path' to 'local_path'\n        \"\"\"\n        return self.upload_file(source_path, local_path,\n                                server, project_name, file,\n                                representation, site,\n                                overwrite, direction=\"Download\")\n\n    def delete_file(self, path):\n        \"\"\"\n            Deletes a file at 'path'\n        \"\"\"\n        if os.path.exists(path):\n            os.remove(path)\n\n    def list_folder(self, folder_path):\n        \"\"\"\n            Returns list of files and subfolder in a 'folder_path'. Non recurs\n        \"\"\"\n        lst = []\n        if os.path.isdir(folder_path):\n            for (dir_path, dir_names, file_names) in os.walk(folder_path):\n                for name in file_names:\n                    lst.append(os.path.join(dir_path, name))\n                for name in dir_names:\n                    lst.append(os.path.join(dir_path, name))\n\n        return lst\n\n    def create_folder(self, folder_path):\n        \"\"\"\n            Creates 'folder_path' on local system\n\n            Args:\n                folder_path (string): absolute path on local (and mounted) disk\n\n            Returns:\n                (string) - sends back folder_path to denote folder(s) was\n                    created\n        \"\"\"\n        os.makedirs(folder_path, exist_ok=True)\n        return folder_path\n\n    def get_roots_config(self, anatomy=None):\n        \"\"\"\n            Returns root values for path resolving\n\n            Takes value from Anatomy which takes values from Settings\n            overridden by Local Settings\n\n        Returns:\n            (dict) - {\"root\": {\"root\": \"/My Drive\"}}\n                     OR\n                     {\"root\": {\"root_ONE\": \"value\", \"root_TWO\":\"value}}\n            Format is importing for usage of python's format ** approach\n        \"\"\"\n        if not anatomy:\n            anatomy = Anatomy(self.project_name,\n                              self._normalize_site_name(self.site_name))\n\n        return {'root': anatomy.roots}\n\n    def get_tree(self):\n        return\n\n    def get_configurable_items_for_site(self):\n        \"\"\"\n            Returns list of items that should be configurable by User\n\n            Returns:\n                (list of dict)\n                [{key:\"root\", label:\"root\", value:\"valueFromSettings\"}]\n        \"\"\"\n        pass\n\n    def _copy(self, source_path, target_path):\n        print(\"copying {}->{}\".format(source_path, target_path))\n        try:\n            shutil.copy(source_path, target_path)\n        except shutil.SameFileError:\n            print(\"same files, skipping\")\n\n    def _mark_progress(self, project_name, file, representation, server, site,\n                       source_path, target_path, direction):\n        \"\"\"\n            Updates progress field in DB by values 0-1.\n\n            Compares file sizes of source and target.\n        \"\"\"\n        source_file_size = os.path.getsize(source_path)\n        target_file_size = 0\n        last_tick = status_val = None\n        while source_file_size != target_file_size:\n            if not last_tick or \\\n                    time.time() - last_tick >= server.LOG_PROGRESS_SEC:\n                status_val = target_file_size / source_file_size\n                last_tick = time.time()\n                log.debug(direction + \"ed %d%%.\" % int(status_val * 100))\n                server.update_db(project_name=project_name,\n                                 new_file_id=None,\n                                 file=file,\n                                 representation=representation,\n                                 site=site,\n                                 progress=status_val\n                                 )\n            try:\n                target_file_size = os.path.getsize(target_path)\n            except FileNotFoundError:\n                pass\n            time.sleep(0.5)\n\n    def _normalize_site_name(self, site_name):\n        \"\"\"Transform user id to 'local' for Local settings\"\"\"\n        if site_name == get_local_site_id():\n            return 'local'\n        return site_name\n"
  },
  {
    "path": "openpype/modules/sync_server/providers/sftp.py",
    "content": "import os\nimport os.path\nimport time\nimport threading\nimport platform\n\nfrom openpype.lib import Logger\nfrom openpype.settings import get_system_settings\nfrom .abstract_provider import AbstractProvider\nlog = Logger.get_logger(\"SyncServer-SFTPHandler\")\n\npysftp = None\ntry:\n    import pysftp\n    import paramiko\nexcept (ImportError, SyntaxError):\n    pass\n\n    # handle imports from Python 2 hosts - in those only basic methods are used\n    log.warning(\"Import failed, imported from Python 2, operations will fail.\")\n\n\nclass SFTPHandler(AbstractProvider):\n    \"\"\"\n        Implementation of SFTP API.\n\n        Authentication could be done in 2 ways:\n            - user and password\n            - ssh key file for user (optionally password for ssh key)\n\n        Settings could be overwritten per project.\n\n    \"\"\"\n    CODE = 'sftp'\n    LABEL = 'SFTP'\n\n    def __init__(self, project_name, site_name, tree=None, presets=None):\n        self.presets = None\n        self.project_name = project_name\n        self.site_name = site_name\n        self.root = None\n        self._conn = None\n\n        self.presets = presets\n        if not self.presets:\n            self.log.warning(\n                \"Sync Server: There are no presets for {}.\".format(site_name)\n            )\n            return\n\n        # store to instance for reconnect\n        self.sftp_host = presets[\"sftp_host\"]\n        self.sftp_port = presets[\"sftp_port\"]\n        self.sftp_user = presets[\"sftp_user\"]\n        self.sftp_pass = presets[\"sftp_pass\"]\n        self.sftp_key = presets[\"sftp_key\"]\n        self.sftp_key_pass = presets[\"sftp_key_pass\"]\n\n        self._tree = None\n\n    @property\n    def conn(self):\n        \"\"\"SFTP connection, cannot be used in all places though.\"\"\"\n        if not self._conn:\n            self._conn = self._get_conn()\n\n        return self._conn\n\n    def is_active(self):\n        \"\"\"\n            Returns True if provider is activated, eg. has working credentials.\n        Returns:\n            (boolean)\n        \"\"\"\n        return self.presets.get(\"enabled\") and self.conn is not None\n\n    @classmethod\n    def get_system_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on system settings level\n\n\n            Returns:\n                (list) of dict\n        \"\"\"\n        return []\n\n    @classmethod\n    def get_project_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on project settings level\n\n            Currently not implemented in Settings yet!\n\n            Returns:\n                (list) of dict\n        \"\"\"\n        # {platform} tells that value is multiplatform and only specific OS\n        # should be returned\n        editable = [\n            # credentials could be overridden on Project or User level\n            {\n                'key': \"sftp_host\",\n                'label': \"SFTP host name\",\n                'type': 'text'\n            },\n            {\n                \"type\": \"number\",\n                \"key\": \"sftp_port\",\n                \"label\": \"SFTP port\"\n            },\n            {\n                'key': \"sftp_user\",\n                'label': \"SFTP user name\",\n                'type': 'text'\n            },\n            {\n                'key': \"sftp_pass\",\n                'label': \"SFTP password\",\n                'type': 'text'\n            },\n            {\n                'key': \"sftp_key\",\n                'label': \"SFTP user ssh key\",\n                'type': 'path',\n                \"multiplatform\": True\n            },\n            {\n                'key': \"sftp_key_pass\",\n                'label': \"SFTP user ssh key password\",\n                'type': 'text'\n            },\n            # roots could be overridden only on Project level, User cannot\n            {\n                \"key\": \"root\",\n                \"label\": \"Roots\",\n                \"type\": \"dict-roots\",\n                \"object_type\": {\n                    \"type\": \"path\",\n                    \"multiplatform\": False,\n                    \"multipath\": False\n                }\n            }\n        ]\n        return editable\n\n    @classmethod\n    def get_local_settings_schema(cls):\n        \"\"\"\n            Returns dict for editable properties on local settings level\n\n            Currently not implemented in Settings yet!\n\n            Returns:\n                (dict)\n        \"\"\"\n        editable = [\n            # credentials could be override on Project or User level\n            {\n                'key': \"sftp_user\",\n                'label': \"SFTP user name\",\n                'type': 'text'\n            },\n            {\n                'key': \"sftp_pass\",\n                'label': \"SFTP password\",\n                'type': 'text'\n            },\n            {\n                'key': \"sftp_key\",\n                'label': \"SFTP user ssh key\",\n                'type': 'path',\n                \"multiplatform\": True\n            },\n            {\n                'key': \"sftp_key_pass\",\n                'label': \"SFTP user ssh key password\",\n                'type': 'text'\n            }\n        ]\n        return editable\n\n    def get_roots_config(self, anatomy=None):\n        \"\"\"\n            Returns root values for path resolving\n\n            Use only Settings as GDrive cannot be modified by Local Settings\n\n        Returns:\n            (dict) - {\"root\": {\"root\": \"/My Drive\"}}\n                     OR\n                     {\"root\": {\"root_ONE\": \"value\", \"root_TWO\":\"value}}\n            Format is importing for usage of python's format ** approach\n        \"\"\"\n        # roots cannot be locally overridden\n        return self.presets['root']\n\n    def get_tree(self):\n        \"\"\"\n            Building of the folder tree could be potentially expensive,\n            constructor provides argument that could inject previously created\n            tree.\n            Tree structure must be handled in thread safe fashion!\n        Returns:\n             (dictionary) - url to id mapping\n        \"\"\"\n        # not needed in this provider\n        pass\n\n    def create_folder(self, path):\n        \"\"\"\n            Create all nonexistent folders and subfolders in 'path'.\n            Updates self._tree structure with new paths\n\n        Args:\n            path (string): absolute path, starts with GDrive root,\n                           without filename\n        Returns:\n            (string) folder id of lowest subfolder from 'path'\n        \"\"\"\n        self.conn.makedirs(path)\n\n        return os.path.basename(path)\n\n    def upload_file(self, source_path, target_path,\n                    server, project_name, file, representation, site,\n                    overwrite=False):\n        \"\"\"\n            Uploads single file from 'source_path' to destination 'path'.\n            It creates all folders on the path if are not existing.\n\n        Args:\n            source_path (string):\n            target_path (string): absolute path with or without name of a file\n            overwrite (boolean): replace existing file\n\n            arguments for saving progress:\n            server (SyncServer): server instance to call update_db on\n            project_name (str):\n            file (dict): info about uploaded file (matches structure from db)\n            representation (dict): complete repre containing 'file'\n            site (str): site name\n\n        Returns:\n            (string) file_id of created/modified file ,\n                throws FileExistsError, FileNotFoundError exceptions\n        \"\"\"\n        if not os.path.isfile(source_path):\n            raise FileNotFoundError(\"Source file {} doesn't exist.\"\n                                    .format(source_path))\n\n        if self.file_path_exists(target_path):\n            if not overwrite:\n                raise ValueError(\"File {} exists, set overwrite\".\n                                 format(target_path))\n\n        thread = threading.Thread(target=self._upload,\n                                  args=(source_path, target_path))\n        thread.start()\n        self._mark_progress(project_name, file, representation, server,\n                            site, source_path, target_path, \"upload\")\n\n        return os.path.basename(target_path)\n\n    def _upload(self, source_path, target_path):\n        print(\"copying {}->{}\".format(source_path, target_path))\n        conn = self._get_conn()\n        conn.put(source_path, target_path)\n\n    def download_file(self, source_path, target_path,\n                      server, project_name, file, representation, site,\n                      overwrite=False):\n        \"\"\"\n            Downloads single file from 'source_path' (remote) to 'target_path'.\n            It creates all folders on the local_path if are not existing.\n            By default existing file on 'target_path' will trigger an exception\n\n        Args:\n            source_path (string): absolute path on provider\n            target_path (string): absolute path with or without name of a file\n            overwrite (boolean): replace existing file\n\n            arguments for saving progress:\n            server (SyncServer): server instance to call update_db on\n            project_name (str):\n            file (dict): info about uploaded file (matches structure from db)\n            representation (dict): complete repre containing 'file'\n            site (str): site name\n\n        Returns:\n            (string) file_id of created/modified file ,\n                throws FileExistsError, FileNotFoundError exceptions\n        \"\"\"\n        if not self.file_path_exists(source_path):\n            raise FileNotFoundError(\"Source file {} doesn't exist.\"\n                                    .format(source_path))\n\n        if os.path.isfile(target_path):\n            if not overwrite:\n                raise ValueError(\"File {} exists, set overwrite\".\n                                 format(target_path))\n\n        thread = threading.Thread(target=self._download,\n                                  args=(source_path, target_path))\n        thread.start()\n        self._mark_progress(project_name, file, representation, server,\n                            site, source_path, target_path, \"download\")\n\n        return os.path.basename(target_path)\n\n    def _download(self, source_path, target_path):\n        print(\"downloading {}->{}\".format(source_path, target_path))\n        conn = self._get_conn()\n        conn.get(source_path, target_path)\n\n    def delete_file(self, path):\n        \"\"\"\n            Deletes file from 'path'. Expects path to specific file.\n\n        Args:\n            path: absolute path to particular file\n\n        Returns:\n            None\n        \"\"\"\n        if not self.file_path_exists(path):\n            raise FileNotFoundError(\"File {} to be deleted doesn't exist.\"\n                                    .format(path))\n\n        self.conn.remove(path)\n\n    def list_folder(self, folder_path):\n        \"\"\"\n            List all files and subfolders of particular path non-recursively.\n\n        Args:\n            folder_path (string): absolut path on provider\n        Returns:\n             (list)\n        \"\"\"\n        return list(pysftp.path_advance(folder_path))\n\n    def folder_path_exists(self, file_path):\n        \"\"\"\n            Checks if path from 'file_path' exists. If so, return its\n            folder id.\n        Args:\n            file_path (string): path with / as a separator\n        Returns:\n            (string) folder id or False\n        \"\"\"\n        if not file_path:\n            return False\n\n        return self.conn.isdir(file_path)\n\n    def file_path_exists(self, file_path):\n        \"\"\"\n            Checks if 'file_path' exists on GDrive\n\n        Args:\n            file_path (string): separated by '/', from root, with file name\n        Returns:\n            (dictionary|boolean) file metadata | False if not found\n        \"\"\"\n        if not file_path:\n            return False\n\n        return self.conn.isfile(file_path)\n\n    @classmethod\n    def get_presets(cls):\n        \"\"\"\n            Get presets for this provider\n        Returns:\n            (dictionary) of configured sites\n        \"\"\"\n        provider_presets = None\n        try:\n            provider_presets = (\n                get_system_settings()[\"modules\"]\n                [\"sync_server\"]\n                [\"providers\"]\n                [\"sftp\"]\n            )\n        except KeyError:\n            log.info((\"Sync Server: There are no presets for SFTP \" +\n                      \"provider.\").\n                     format(str(provider_presets)))\n            return\n        return provider_presets\n\n    def _get_conn(self):\n        \"\"\"\n            Returns fresh sftp connection.\n\n            It seems that connection cannot be cached into self.conn, at least\n            for get and put which run in separate threads.\n\n        Returns:\n            pysftp.Connection\n        \"\"\"\n        if not pysftp:\n            raise ImportError\n\n        cnopts = pysftp.CnOpts()\n        cnopts.hostkeys = None\n\n        conn_params = {\n            'host': self.sftp_host,\n            'port': self.sftp_port,\n            'username': self.sftp_user,\n            'cnopts': cnopts\n        }\n        if self.sftp_pass and self.sftp_pass.strip():\n            conn_params['password'] = self.sftp_pass\n        if self.sftp_key:  # expects .pem format, not .ppk!\n            conn_params['private_key'] = \\\n                self.sftp_key[platform.system().lower()]\n        if self.sftp_key_pass:\n            conn_params['private_key_pass'] = self.sftp_key_pass\n\n        try:\n            return pysftp.Connection(**conn_params)\n        except (paramiko.ssh_exception.SSHException,\n                pysftp.exceptions.ConnectionException):\n            self.log.warning(\"Couldn't connect\", exc_info=True)\n\n    def _mark_progress(self, project_name, file, representation, server, site,\n                       source_path, target_path, direction):\n        \"\"\"\n            Updates progress field in DB by values 0-1.\n\n            Compares file sizes of source and target.\n        \"\"\"\n        pass\n        if direction == \"upload\":\n            source_file_size = os.path.getsize(source_path)\n        else:\n            source_file_size = self.conn.stat(source_path).st_size\n\n        target_file_size = 0\n        last_tick = status_val = None\n        while source_file_size != target_file_size:\n            if not last_tick or \\\n                    time.time() - last_tick >= server.LOG_PROGRESS_SEC:\n                status_val = target_file_size / source_file_size\n                last_tick = time.time()\n                self.log.debug(direction + \"ed %d%%.\" % int(status_val * 100))\n                server.update_db(project_name=project_name,\n                                 new_file_id=None,\n                                 file=file,\n                                 representation=representation,\n                                 site=site,\n                                 progress=status_val\n                                 )\n            try:\n                if direction == \"upload\":\n                    target_file_size = self.conn.stat(target_path).st_size\n                else:\n                    target_file_size = os.path.getsize(target_path)\n            except FileNotFoundError:\n                pass\n            time.sleep(0.5)\n"
  },
  {
    "path": "openpype/modules/sync_server/rest_api.py",
    "content": "from aiohttp.web_response import Response\nfrom openpype.lib import Logger\n\n\nclass SyncServerModuleRestApi:\n    \"\"\"\n    REST API endpoint used for calling from hosts when context change\n    happens in Workfile app.\n    \"\"\"\n\n    def __init__(self, user_module, server_manager):\n        self._log = None\n        self.module = user_module\n        self.server_manager = server_manager\n\n        self.prefix = \"/sync_server\"\n\n        self.register()\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    def register(self):\n        self.server_manager.add_route(\n            \"POST\",\n            self.prefix + \"/reset_timer\",\n            self.reset_timer,\n        )\n\n    async def reset_timer(self, _request):\n        \"\"\"Force timer to run immediately.\"\"\"\n        self.module.reset_timer()\n\n        return Response(status=200)\n"
  },
  {
    "path": "openpype/modules/sync_server/sync_server.py",
    "content": "\"\"\"Python 3 only implementation.\"\"\"\nimport os\nimport asyncio\nimport threading\nimport concurrent.futures\nfrom time import sleep\n\nfrom .providers import lib\nfrom openpype.client.entity_links import get_linked_representation_id\nfrom openpype.lib import Logger\nfrom openpype.lib.local_settings import get_local_site_id\nfrom openpype.modules.base import ModulesManager\nfrom openpype.pipeline import Anatomy\nfrom openpype.pipeline.load.utils import get_representation_path_with_anatomy\n\nfrom .utils import SyncStatus, ResumableError\n\n\nasync def upload(module, project_name, file, representation, provider_name,\n                 remote_site_name, tree=None, preset=None):\n    \"\"\"\n        Upload single 'file' of a 'representation' to 'provider'.\n        Source url is taken from 'file' portion, where {root} placeholder\n        is replaced by 'representation.Context.root'\n        Provider could be one of implemented in provider.py.\n\n        Updates MongoDB, fills in id of file from provider (ie. file_id\n        from GDrive), 'created_dt' - time of upload\n\n        'provider_name' doesn't have to match to 'site_name', single\n        provider (GDrive) might have multiple sites ('projectA',\n        'projectB')\n\n    Args:\n        module(SyncServerModule): object to run SyncServerModule API\n        project_name (str): source db\n        file (dictionary): of file from representation in Mongo\n        representation (dictionary): of representation\n        provider_name (string): gdrive, gdc etc.\n        site_name (string): site on provider, single provider(gdrive) could\n            have multiple sites (different accounts, credentials)\n        tree (dictionary): injected memory structure for performance\n        preset (dictionary): site config ('credentials_url', 'root'...)\n\n    \"\"\"\n    # create ids sequentially, upload file in parallel later\n    with module.lock:\n        # this part modifies structure on 'remote_site', only single\n        # thread can do that at a time, upload/download to prepared\n        # structure should be run in parallel\n        remote_handler = lib.factory.get_provider(provider_name,\n                                                  project_name,\n                                                  remote_site_name,\n                                                  tree=tree,\n                                                  presets=preset)\n\n        file_path = file.get(\"path\", \"\")\n        local_file_path, remote_file_path = resolve_paths(\n            module, file_path, project_name,\n            remote_site_name, remote_handler\n        )\n\n        target_folder = os.path.dirname(remote_file_path)\n        folder_id = remote_handler.create_folder(target_folder)\n\n        if not folder_id:\n            err = \"Folder {} wasn't created. Check permissions.\". \\\n                format(target_folder)\n            raise NotADirectoryError(err)\n\n    loop = asyncio.get_running_loop()\n    file_id = await loop.run_in_executor(None,\n                                         remote_handler.upload_file,\n                                         local_file_path,\n                                         remote_file_path,\n                                         module,\n                                         project_name,\n                                         file,\n                                         representation,\n                                         remote_site_name,\n                                         True\n                                         )\n\n    module.handle_alternate_site(project_name, representation,\n                                 remote_site_name,\n                                 file[\"_id\"], file_id)\n\n    return file_id\n\n\nasync def download(module, project_name, file, representation, provider_name,\n                   remote_site_name, tree=None, preset=None):\n    \"\"\"\n        Downloads file to local folder denoted in representation.Context.\n\n    Args:\n        module(SyncServerModule): object to run SyncServerModule API\n        project_name (str): source\n        file (dictionary) : info about processed file\n        representation (dictionary):  repr that 'file' belongs to\n        provider_name (string):  'gdrive' etc\n        site_name (string): site on provider, single provider(gdrive) could\n            have multiple sites (different accounts, credentials)\n        tree (dictionary): injected memory structure for performance\n        preset (dictionary): site config ('credentials_url', 'root'...)\n\n        Returns:\n        (string) - 'name' of local file\n    \"\"\"\n    with module.lock:\n        remote_handler = lib.factory.get_provider(provider_name,\n                                                  project_name,\n                                                  remote_site_name,\n                                                  tree=tree,\n                                                  presets=preset)\n\n        file_path = file.get(\"path\", \"\")\n        local_file_path, remote_file_path = resolve_paths(\n            module, file_path, project_name, remote_site_name, remote_handler\n        )\n\n        local_folder = os.path.dirname(local_file_path)\n        os.makedirs(local_folder, exist_ok=True)\n\n    local_site = module.get_active_site(project_name)\n\n    loop = asyncio.get_running_loop()\n    file_id = await loop.run_in_executor(None,\n                                         remote_handler.download_file,\n                                         remote_file_path,\n                                         local_file_path,\n                                         module,\n                                         project_name,\n                                         file,\n                                         representation,\n                                         local_site,\n                                         True\n                                         )\n\n    module.handle_alternate_site(project_name, representation, local_site,\n                                 file[\"_id\"], file_id)\n\n    return file_id\n\n\ndef resolve_paths(module, file_path, project_name,\n                  remote_site_name=None, remote_handler=None):\n    \"\"\"\n        Returns tuple of local and remote file paths with {root}\n        placeholders replaced with proper values from Settings or Anatomy\n\n        Ejected here because of Python 2 hosts (GDriveHandler is an issue)\n\n        Args:\n            module(SyncServerModule): object to run SyncServerModule API\n            file_path(string): path with {root}\n            project_name(string): project name\n            remote_site_name(string): remote site\n            remote_handler(AbstractProvider): implementation\n        Returns:\n            (string, string) - proper absolute paths, remote path is optional\n    \"\"\"\n    remote_file_path = ''\n    if remote_handler:\n        remote_file_path = remote_handler.resolve_path(file_path)\n\n    local_handler = lib.factory.get_provider(\n        'local_drive', project_name, module.get_active_site(project_name))\n    local_file_path = local_handler.resolve_path(file_path)\n\n    return local_file_path, remote_file_path\n\n\ndef _site_is_working(module, project_name, site_name, site_config):\n    \"\"\"\n        Confirm that 'site_name' is configured correctly for 'project_name'.\n\n        Must be here as lib.factory access doesn't work in Python 2 hosts.\n\n        Args:\n            module (SyncServerModule)\n            project_name(string):\n            site_name(string):\n            site_config (dict): configuration for site from Settings\n        Returns\n            (bool)\n    \"\"\"\n    provider = module.get_provider_for_site(site=site_name)\n    handler = lib.factory.get_provider(provider,\n                                       project_name,\n                                       site_name,\n                                       presets=site_config)\n\n    return handler.is_active()\n\n\ndef download_last_published_workfile(\n    host_name: str,\n    project_name: str,\n    task_name: str,\n    workfile_representation: dict,\n    max_retries: int,\n    anatomy: Anatomy = None,\n) -> str:\n    \"\"\"Download the last published workfile\n\n    Args:\n        host_name (str): Host name.\n        project_name (str): Project name.\n        task_name (str): Task name.\n        workfile_representation (dict): Workfile representation.\n        max_retries (int): complete file failure only after so many attempts\n        anatomy (Anatomy, optional): Anatomy (Used for optimization).\n            Defaults to None.\n\n    Returns:\n        str: last published workfile path localized\n    \"\"\"\n\n    if not anatomy:\n        anatomy = Anatomy(project_name)\n\n    # Get sync server module\n    sync_server = ModulesManager().modules_by_name.get(\"sync_server\")\n    if not sync_server or not sync_server.enabled:\n        print(\"Sync server module is disabled or unavailable.\")\n        return\n\n    if not workfile_representation:\n        print(\n            \"Not published workfile for task '{}' and host '{}'.\".format(\n                task_name, host_name\n            )\n        )\n        return\n\n    last_published_workfile_path = get_representation_path_with_anatomy(\n        workfile_representation, anatomy\n    )\n    if not last_published_workfile_path:\n        return\n\n    # If representation isn't available on remote site, then return.\n    if not sync_server.is_representation_on_site(\n        project_name,\n        workfile_representation[\"_id\"],\n        sync_server.get_remote_site(project_name),\n    ):\n        print(\n            \"Representation for task '{}' and host '{}'\".format(\n                task_name, host_name\n            )\n        )\n        return\n\n    # Get local site\n    local_site_id = get_local_site_id()\n\n    # Add workfile representation to local site\n    representation_ids = {workfile_representation[\"_id\"]}\n    representation_ids.update(\n        get_linked_representation_id(\n            project_name, repre_id=workfile_representation[\"_id\"]\n        )\n    )\n    for repre_id in representation_ids:\n        if not sync_server.is_representation_on_site(project_name, repre_id,\n                                                     local_site_id):\n            sync_server.add_site(\n                project_name,\n                repre_id,\n                local_site_id,\n                force=True,\n                priority=99\n            )\n    sync_server.reset_timer()\n    print(\"Starting to download:{}\".format(last_published_workfile_path))\n    # While representation unavailable locally, wait.\n    while not sync_server.is_representation_on_site(\n        project_name, workfile_representation[\"_id\"], local_site_id,\n        max_retries=max_retries\n    ):\n        sleep(5)\n\n    return last_published_workfile_path\n\n\nclass SyncServerThread(threading.Thread):\n    \"\"\"\n        Separate thread running synchronization server with asyncio loop.\n        Stopped when tray is closed.\n    \"\"\"\n    def __init__(self, module):\n        self.log = Logger.get_logger(self.__class__.__name__)\n\n        super(SyncServerThread, self).__init__()\n        self.module = module\n        self.loop = None\n        self.is_running = False\n        self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=3)\n        self.timer = None\n\n    def run(self):\n        self.is_running = True\n\n        try:\n            self.log.info(\"Starting Sync Server\")\n            self.loop = asyncio.new_event_loop()  # create new loop for thread\n            asyncio.set_event_loop(self.loop)\n            self.loop.set_default_executor(self.executor)\n\n            asyncio.ensure_future(self.check_shutdown(), loop=self.loop)\n            asyncio.ensure_future(self.sync_loop(), loop=self.loop)\n            self.log.info(\"Sync Server Started\")\n            self.loop.run_forever()\n        except Exception:\n            self.log.warning(\n                \"Sync Server service has failed\", exc_info=True\n            )\n        finally:\n            self.loop.close()  # optional\n\n    async def sync_loop(self):\n        \"\"\"\n            Runs permanently, each time:\n                - gets list of collections in DB\n                - gets list of active remote providers (has configuration,\n                    credentials)\n                - for each project_name it looks for representations that\n                  should be synced\n                - synchronize found collections\n                - update representations - fills error messages for exceptions\n                - waits X seconds and repeat\n        Returns:\n\n        \"\"\"\n        while self.is_running and not self.module.is_paused():\n            try:\n                import time\n                start_time = time.time()\n                self.module.set_sync_project_settings()  # clean cache\n                project_name = None\n                enabled_projects = self.module.get_enabled_projects()\n                for project_name in enabled_projects:\n                    preset = self.module.sync_project_settings[project_name]\n\n                    local_site, remote_site = self._working_sites(project_name,\n                                                                  preset)\n                    if not all([local_site, remote_site]):\n                        continue\n\n                    sync_repres = self.module.get_sync_representations(\n                        project_name,\n                        local_site,\n                        remote_site\n                    )\n\n                    task_files_to_process = []\n                    files_processed_info = []\n                    # process only unique file paths in one batch\n                    # multiple representation could have same file path\n                    # (textures),\n                    # upload process can find already uploaded file and\n                    # reuse same id\n                    processed_file_path = set()\n\n                    site_preset = preset.get('sites')[remote_site]\n                    remote_provider = \\\n                        self.module.get_provider_for_site(site=remote_site)\n                    handler = lib.factory.get_provider(remote_provider,\n                                                       project_name,\n                                                       remote_site,\n                                                       presets=site_preset)\n                    limit = lib.factory.get_provider_batch_limit(\n                        remote_provider)\n                    # first call to get_provider could be expensive, its\n                    # building folder tree structure in memory\n                    # call only if needed, eg. DO_UPLOAD or DO_DOWNLOAD\n                    for sync in sync_repres:\n                        if limit <= 0:\n                            continue\n                        files = sync.get(\"files\") or []\n                        if files:\n                            for file in files:\n                                # skip already processed files\n                                file_path = file.get('path', '')\n                                if file_path in processed_file_path:\n                                    continue\n                                status = self.module.check_status(\n                                    file,\n                                    local_site,\n                                    remote_site,\n                                    preset.get('config'))\n                                if status == SyncStatus.DO_UPLOAD:\n                                    tree = handler.get_tree()\n                                    limit -= 1\n                                    task = asyncio.create_task(\n                                        upload(self.module,\n                                               project_name,\n                                               file,\n                                               sync,\n                                               remote_provider,\n                                               remote_site,\n                                               tree,\n                                               site_preset))\n                                    task_files_to_process.append(task)\n                                    # store info for exception handlingy\n                                    files_processed_info.append((file,\n                                                                 sync,\n                                                                 remote_site,\n                                                                 project_name\n                                                                 ))\n                                    processed_file_path.add(file_path)\n                                if status == SyncStatus.DO_DOWNLOAD:\n                                    tree = handler.get_tree()\n                                    limit -= 1\n                                    task = asyncio.create_task(\n                                        download(self.module,\n                                                 project_name,\n                                                 file,\n                                                 sync,\n                                                 remote_provider,\n                                                 remote_site,\n                                                 tree,\n                                                 site_preset))\n                                    task_files_to_process.append(task)\n\n                                    files_processed_info.append((file,\n                                                                 sync,\n                                                                 local_site,\n                                                                 project_name\n                                                                 ))\n                                    processed_file_path.add(file_path)\n\n                    self.log.debug(\"Sync tasks count {}\".format(\n                        len(task_files_to_process)\n                    ))\n                    files_created = await asyncio.gather(\n                        *task_files_to_process,\n                        return_exceptions=True)\n                    for file_id, info in zip(files_created,\n                                             files_processed_info):\n                        file, representation, site, project_name = info\n                        error = None\n                        if isinstance(file_id, BaseException):\n                            error = str(file_id)\n                            file_id = None\n                        self.module.update_db(project_name,\n                                              file_id,\n                                              file,\n                                              representation,\n                                              site,\n                                              error)\n\n                duration = time.time() - start_time\n                self.log.debug(\"One loop took {:.2f}s\".format(duration))\n                delay = self.module.get_loop_delay(project_name)\n                self.log.debug(\n                    \"Waiting for {} seconds to new loop\".format(delay)\n                )\n                self.timer = asyncio.create_task(self.run_timer(delay))\n                await asyncio.gather(self.timer)\n\n            except ConnectionResetError:\n                self.log.warning(\n                    \"ConnectionResetError in sync loop, trying next loop\",\n                    exc_info=True)\n            except asyncio.exceptions.CancelledError:\n                # cancelling timer\n                pass\n            except ResumableError:\n                self.log.warning(\n                    \"ResumableError in sync loop, trying next loop\",\n                    exc_info=True)\n            except Exception:\n                self.stop()\n                self.log.warning(\n                    \"Unhandled except. in sync loop, stopping server\",\n                    exc_info=True)\n\n    def stop(self):\n        \"\"\"Sets is_running flag to false, 'check_shutdown' shuts server down\"\"\"\n        self.is_running = False\n\n    async def check_shutdown(self):\n        \"\"\" Future that is running and checks if server should be running\n            periodically.\n        \"\"\"\n        while self.is_running:\n            if self.module.long_running_tasks:\n                task = self.module.long_running_tasks.pop()\n                self.log.info(\"starting long running\")\n                await self.loop.run_in_executor(None, task[\"func\"])\n                self.log.info(\"finished long running\")\n                self.module.projects_processed.remove(task[\"project_name\"])\n            await asyncio.sleep(0.5)\n        tasks = [task for task in asyncio.all_tasks() if\n                 task is not asyncio.current_task()]\n        list(map(lambda task: task.cancel(), tasks))  # cancel all the tasks\n        results = await asyncio.gather(*tasks, return_exceptions=True)\n        self.log.debug(\n            f'Finished awaiting cancelled tasks, results: {results}...')\n        await self.loop.shutdown_asyncgens()\n        # to really make sure everything else has time to stop\n        self.executor.shutdown(wait=True)\n        await asyncio.sleep(0.07)\n        self.loop.stop()\n\n    async def run_timer(self, delay):\n        \"\"\"Wait for 'delay' seconds to start next loop\"\"\"\n        await asyncio.sleep(delay)\n\n    def reset_timer(self):\n        \"\"\"Called when waiting for next loop should be skipped\"\"\"\n        self.log.debug(\"Resetting timer\")\n        if self.timer:\n            self.timer.cancel()\n            self.timer = None\n\n    def _working_sites(self, project_name, sync_config):\n        if self.module.is_project_paused(project_name):\n            self.log.debug(\"Both sites same, skipping\")\n            return None, None\n\n        local_site = self.module.get_active_site(project_name)\n        remote_site = self.module.get_remote_site(project_name)\n        if local_site == remote_site:\n            self.log.debug(\"{}-{} sites same, skipping\".format(\n                local_site, remote_site))\n            return None, None\n\n        local_site_config = sync_config.get('sites')[local_site]\n        remote_site_config = sync_config.get('sites')[remote_site]\n        if not all([_site_is_working(self.module, project_name, local_site,\n                                     local_site_config),\n                    _site_is_working(self.module, project_name, remote_site,\n                                     remote_site_config)]):\n            self.log.debug(\n                \"Some of the sites {} - {} in {} is not working properly\".format(  # noqa\n                    local_site, remote_site, project_name\n                )\n            )\n\n            return None, None\n\n        return local_site, remote_site\n"
  },
  {
    "path": "openpype/modules/sync_server/sync_server_module.py",
    "content": "import os\nimport sys\nimport time\nfrom datetime import datetime\nimport threading\nimport copy\nimport signal\nfrom collections import deque, defaultdict\n\nfrom bson.objectid import ObjectId\n\nfrom openpype.client import (\n    get_projects,\n    get_representations,\n    get_representation_by_id,\n)\nfrom openpype.modules import (\n    OpenPypeModule,\n    ITrayModule,\n    IPluginPaths,\n    click_wrap,\n)\nfrom openpype.settings import (\n    get_project_settings,\n    get_system_settings,\n)\nfrom openpype.lib import Logger, get_local_site_id\nfrom openpype.pipeline import AvalonMongoDB, Anatomy\nfrom openpype.settings.lib import (\n    get_default_anatomy_settings,\n    get_anatomy_settings,\n    get_local_settings,\n)\nfrom openpype.settings.constants import (\n    DEFAULT_PROJECT_KEY\n)\n\nfrom .providers.local_drive import LocalDriveHandler\nfrom .providers import lib\n\nfrom .utils import (\n    time_function,\n    SyncStatus,\n    SiteAlreadyPresentError,\n    SYNC_SERVER_ROOT,\n)\n\nlog = Logger.get_logger(\"SyncServer\")\n\n\nclass SyncServerModule(OpenPypeModule, ITrayModule, IPluginPaths):\n    \"\"\"\n       Synchronization server that is syncing published files from local to\n       any of implemented providers (like GDrive, S3 etc.)\n       Runs in the background and checks all representations, looks for files\n       that are marked to be in different location than 'studio' (temporary),\n       checks if 'created_dt' field is present denoting successful sync\n       with provider destination.\n       Sites structure is created during publish OR by calling 'add_site'\n       method.\n\n       By default it will always contain 1 record with\n       \"name\" ==  self.presets[\"active_site\"] and\n       filled \"created_dt\" AND 1 or multiple records for all defined\n       remote sites, where \"created_dt\" is not present.\n       This highlights that file should be uploaded to\n       remote destination\n\n       ''' - example of synced file test_Cylinder_lookMain_v010.ma to GDrive\n        \"files\" : [\n        {\n            \"path\" : \"{root}/Test/Assets/Cylinder/publish/look/lookMain/v010/\n                     test_Cylinder_lookMain_v010.ma\",\n            \"_id\" : ObjectId(\"5eeb25e411e06a16209ab78f\"),\n            \"hash\" : \"test_Cylinder_lookMain_v010,ma|1592468963,24|4822\",\n            \"size\" : NumberLong(4822),\n            \"sites\" : [\n                {\n                    \"name\": \"john_local_XD4345\",\n                    \"created_dt\" : ISODate(\"2020-05-22T08:05:44.000Z\")\n                },\n                {\n                    \"id\" : ObjectId(\"5eeb25e411e06a16209ab78f\"),\n                    \"name\": \"gdrive\",\n                    \"created_dt\" : ISODate(\"2020-05-55T08:54:35.833Z\")\n                ]\n            }\n        },\n        '''\n        Each Tray app has assigned its own  self.presets[\"local_id\"]\n        used in sites as a name.\n        Tray is searching only for records where name matches its\n        self.presets[\"active_site\"] + self.presets[\"remote_site\"].\n        \"active_site\" could be storage in studio ('studio'), or specific\n        \"local_id\" when user is working disconnected from home.\n        If the local record has its \"created_dt\" filled, it is a source and\n        process will try to upload the file to all defined remote sites.\n\n        Remote files \"id\" is real id that could be used in appropriate API.\n        Local files have \"id\" too, for conformity, contains just file name.\n        It is expected that multiple providers will be implemented in separate\n        classes and registered in 'providers.py'.\n\n    \"\"\"\n    # limit querying DB to look for X number of representations that should\n    # be sync, we try to run more loops with less records\n    # actual number of files synced could be lower as providers can have\n    # different limits imposed by its API\n    # set 0 to no limit\n    REPRESENTATION_LIMIT = 100\n    DEFAULT_SITE = 'studio'\n    LOCAL_SITE = 'local'\n    LOG_PROGRESS_SEC = 5  # how often log progress to DB\n    DEFAULT_PRIORITY = 50  # higher is better, allowed range 1 - 1000\n\n    name = \"sync_server\"\n    label = \"Sync Queue\"\n\n    def initialize(self, module_settings):\n        \"\"\"\n            Called during Module Manager creation.\n\n            Collects needed data, checks asyncio presence.\n            Sets 'enabled' according to global settings for the module.\n            Shouldnt be doing any initialization, thats a job for 'tray_init'\n        \"\"\"\n        self.enabled = module_settings[self.name][\"enabled\"]\n\n        # some parts of code need to run sequentially, not in async\n        self.lock = None\n        self._sync_system_settings = None\n        # settings for all enabled projects for sync\n        self._sync_project_settings = None\n        self.sync_server_thread = None  # asyncio requires new thread\n\n        self.action_show_widget = None\n        self._paused = False\n        self._paused_projects = set()\n        self._anatomies = {}\n\n        self._connection = None\n\n        # list of long blocking tasks\n        self.long_running_tasks = deque()\n        # projects that long tasks are running on\n        self.projects_processed = set()\n\n    def get_plugin_paths(self):\n        \"\"\"Deadline plugin paths.\"\"\"\n        return {\n            \"load\": [os.path.join(SYNC_SERVER_ROOT, \"plugins\", \"load\")]\n        }\n\n    def get_site_icons(self):\n        \"\"\"Icons for sites.\n\n        Returns:\n            dict[str, str]: Path to icon by site.\n        \"\"\"\n\n        resource_path = os.path.join(\n            SYNC_SERVER_ROOT, \"providers\", \"resources\"\n        )\n        return {\n            provider: \"{}/{}.png\".format(resource_path, provider)\n            for provider in [\"studio\", \"local_drive\", \"gdrive\"]\n        }\n\n    \"\"\" Start of Public API \"\"\"\n    def add_site(self, project_name, representation_id, site_name=None,\n                 force=False, priority=None, reset_timer=False):\n        \"\"\"\n        Adds new site to representation to be synced.\n\n        'project_name' must have synchronization enabled (globally or\n        project only)\n\n        Used as an API endpoint from outside applications (Loader etc).\n\n        Use 'force' to reset existing site.\n\n        Args:\n            project_name (string): project name (must match DB)\n            representation_id (string): MongoDB _id value\n            site_name (string): name of configured and active site\n            force (bool): reset site if exists\n            priority (int): set priority\n            reset_timer (bool): if delay timer should be reset, eg. user mark\n                some representation to be synced manually\n\n        Throws:\n            SiteAlreadyPresentError - if adding already existing site and\n                not 'force'\n            ValueError - other errors (repre not found, misconfiguration)\n        \"\"\"\n        if not self.get_sync_project_setting(project_name):\n            raise ValueError(\"Project not configured\")\n\n        if not site_name:\n            site_name = self.DEFAULT_SITE\n\n        self.reset_site_on_representation(project_name,\n                                          representation_id,\n                                          site_name=site_name,\n                                          force=force,\n                                          priority=priority)\n\n        if reset_timer:\n            self.reset_timer()\n\n    def remove_site(self, project_name, representation_id, site_name,\n                    remove_local_files=False):\n        \"\"\"\n            Removes 'site_name' for particular 'representation_id' on\n            'project_name'\n\n            Args:\n                project_name (string): project name (must match DB)\n                representation_id (string): MongoDB _id value\n                site_name (string): name of configured and active site\n                remove_local_files (bool): remove only files for 'local_id'\n                    site\n\n            Returns:\n                throws ValueError if any issue\n        \"\"\"\n        if not self.get_sync_project_setting(project_name):\n            raise ValueError(\"Project not configured\")\n\n        self.reset_site_on_representation(project_name,\n                                          representation_id,\n                                          site_name=site_name,\n                                          remove=True)\n        if remove_local_files:\n            self._remove_local_file(project_name, representation_id, site_name)\n\n    def get_progress_for_repre(self, doc, active_site, remote_site):\n        \"\"\"\n            Calculates average progress for representation.\n            If site has created_dt >> fully available >> progress == 1\n            Could be calculated in aggregate if it would be too slow\n            Args:\n                doc(dict): representation dict\n            Returns:\n                (dict) with active and remote sites progress\n                {'studio': 1.0, 'gdrive': -1} - gdrive site is not present\n                    -1 is used to highlight the site should be added\n                {'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not\n                    uploaded yet\n        \"\"\"\n        progress = {active_site: -1,\n                    remote_site: -1}\n        if not doc:\n            return progress\n\n        files = {active_site: 0, remote_site: 0}\n        doc_files = doc.get(\"files\") or []\n        for doc_file in doc_files:\n            if not isinstance(doc_file, dict):\n                continue\n\n            sites = doc_file.get(\"sites\") or []\n            for site in sites:\n                if (\n                        # Pype 2 compatibility\n                        not isinstance(site, dict)\n                        # Check if site name is one of progress sites\n                        or site[\"name\"] not in progress\n                ):\n                    continue\n\n                files[site[\"name\"]] += 1\n                norm_progress = max(progress[site[\"name\"]], 0)\n                if site.get(\"created_dt\"):\n                    progress[site[\"name\"]] = norm_progress + 1\n                elif site.get(\"progress\"):\n                    progress[site[\"name\"]] = norm_progress + site[\"progress\"]\n                else:  # site exists, might be failed, do not add again\n                    progress[site[\"name\"]] = 0\n\n        # for example 13 fully avail. files out of 26 >> 13/26 = 0.5\n        avg_progress = {}\n        avg_progress[active_site] = \\\n            progress[active_site] / max(files[active_site], 1)\n        avg_progress[remote_site] = \\\n            progress[remote_site] / max(files[remote_site], 1)\n        return avg_progress\n\n    def compute_resource_sync_sites(self, project_name):\n        \"\"\"Get available resource sync sites state for publish process.\n\n        Returns dict with prepared state of sync sites for 'project_name'.\n        It checks if Site Sync is enabled, handles alternative sites.\n        Publish process stores this dictionary as a part of representation\n        document in DB.\n\n        Example:\n        [\n            {\n                'name': '42abbc09-d62a-44a4-815c-a12cd679d2d7',\n                'created_dt': datetime.datetime(2022, 3, 30, 12, 16, 9, 778637)\n            },\n            {'name': 'studio'},\n            {'name': 'SFTP'}\n        ] -- representation is published locally, artist or Settings have set\n        remote site as 'studio'. 'SFTP' is alternate site to 'studio'. Eg.\n        whenever file is on 'studio', it is also on 'SFTP'.\n        \"\"\"\n\n        def create_metadata(name, created=True):\n            \"\"\"Create sync site metadata for site with `name`\"\"\"\n            metadata = {\"name\": name}\n            if created:\n                metadata[\"created_dt\"] = datetime.now()\n            return metadata\n\n        if (\n                not self.sync_system_settings[\"enabled\"] or\n                not self.sync_project_settings[project_name][\"enabled\"]):\n            return [create_metadata(self.DEFAULT_SITE)]\n\n        local_site = self.get_active_site(project_name)\n        remote_site = self.get_remote_site(project_name)\n\n        # Attached sites metadata by site name\n        # That is the local site, remote site, the always accesible sites\n        # and their alternate sites (alias of sites with different protocol)\n        attached_sites = dict()\n        attached_sites[local_site] = create_metadata(local_site)\n\n        if remote_site and remote_site not in attached_sites:\n            attached_sites[remote_site] = create_metadata(remote_site,\n                                                          created=False)\n\n        attached_sites = self._add_alternative_sites(attached_sites)\n        # add skeleton for sites where it should be always synced to\n        # usually it would be a backup site which is handled by separate\n        # background process\n        for site in self._get_always_accessible_sites(project_name):\n            if site not in attached_sites:\n                attached_sites[site] = create_metadata(site, created=False)\n\n        return list(attached_sites.values())\n\n    def _get_always_accessible_sites(self, project_name):\n        \"\"\"Sites that synced to as a part of background process.\n\n        Artist machine doesn't handle those, explicit Tray with that site name\n        as a local id must be running.\n        Example is dropbox site serving as a backup solution\n        \"\"\"\n        always_accessible_sites = (\n            self.get_sync_project_setting(project_name)[\"config\"].\n            get(\"always_accessible_on\", [])\n        )\n        return [site.strip() for site in always_accessible_sites]\n\n    def _add_alternative_sites(self, attached_sites):\n        \"\"\"Add skeleton document for alternative sites\n\n        Each new configured site in System Setting could serve as a alternative\n        site, it's a kind of alias. It means that files on 'a site' are\n        physically accessible also on 'a alternative' site.\n        Example is sftp site serving studio files via sftp protocol, physically\n        file is only in studio, sftp server has this location mounted.\n        \"\"\"\n        additional_sites = self.sync_system_settings.get(\"sites\", {})\n\n        alt_site_pairs = self._get_alt_site_pairs(additional_sites)\n\n        for site_name in additional_sites.keys():\n            # Get alternate sites (stripped names) for this site name\n            alt_sites = alt_site_pairs.get(site_name)\n            alt_sites = [site.strip() for site in alt_sites]\n            alt_sites = set(alt_sites)\n\n            # If no alternative sites we don't need to add\n            if not alt_sites:\n                continue\n\n            # Take a copy of data of the first alternate site that is already\n            # defined as an attached site to match the same state.\n            match_meta = next((attached_sites[site] for site in alt_sites\n                               if site in attached_sites), None)\n            if not match_meta:\n                continue\n\n            alt_site_meta = copy.deepcopy(match_meta)\n            alt_site_meta[\"name\"] = site_name\n\n            # Note: We change mutable `attached_site` dict in-place\n            attached_sites[site_name] = alt_site_meta\n\n        return attached_sites\n\n    def _get_alt_site_pairs(self, conf_sites):\n        \"\"\"Returns dict of site and its alternative sites.\n\n        If `site` has alternative site, it means that alt_site has 'site' as\n        alternative site\n        Args:\n            conf_sites (dict)\n        Returns:\n            (dict): {'site': [alternative sites]...}\n        \"\"\"\n        alt_site_pairs = defaultdict(set)\n        for site_name, site_info in conf_sites.items():\n            alt_sites = set(site_info.get(\"alternative_sites\", []))\n            alt_site_pairs[site_name].update(alt_sites)\n\n            for alt_site in alt_sites:\n                alt_site_pairs[alt_site].add(site_name)\n\n        for site_name, alt_sites in alt_site_pairs.items():\n            sites_queue = deque(alt_sites)\n            while sites_queue:\n                alt_site = sites_queue.popleft()\n\n                # safety against wrong config\n                # {\"SFTP\": {\"alternative_site\": \"SFTP\"}\n                if alt_site == site_name or alt_site not in alt_site_pairs:\n                    continue\n\n                for alt_alt_site in alt_site_pairs[alt_site]:\n                    if (\n                            alt_alt_site != site_name\n                            and alt_alt_site not in alt_sites\n                    ):\n                        alt_sites.add(alt_alt_site)\n                        sites_queue.append(alt_alt_site)\n\n        return alt_site_pairs\n\n    def clear_project(self, project_name, site_name):\n        \"\"\"\n            Clear 'project_name' of 'site_name' and its local files\n\n            Works only on real local sites, not on 'studio'\n        \"\"\"\n        query = {\n            \"type\": \"representation\",\n            \"files.sites.name\": site_name\n        }\n\n        # TODO currently not possible to replace with get_representations\n        representations = list(\n            self.connection.database[project_name].find(query))\n        if not representations:\n            self.log.debug(\"No repre found\")\n            return\n\n        for repre in representations:\n            self.remove_site(project_name, repre.get(\"_id\"), site_name, True)\n\n    def create_validate_project_task(self, project_name, site_name):\n        \"\"\"Adds metadata about project files validation on a queue.\n\n        This process will loop through all representation and check if\n        their files actually exist on an active site.\n\n        It also checks if site is set in DB, but file is physically not\n        present\n\n        This might be useful for edge cases when artists is switching\n        between sites, remote site is actually physically mounted and\n        active site has same file urls etc.\n\n        Task will run on a asyncio loop, shouldn't be blocking.\n        \"\"\"\n        task = {\n            \"type\": \"validate\",\n            \"project_name\": project_name,\n            \"func\": lambda: self.validate_project(project_name, site_name,\n                                                  reset_missing=True)\n        }\n        self.projects_processed.add(project_name)\n        self.long_running_tasks.append(task)\n\n    def validate_project(self, project_name, site_name, reset_missing=False):\n        \"\"\"Validate 'project_name' of 'site_name' and its local files\n\n        If file present and not marked with a 'site_name' in DB, DB is\n        updated with site name and file modified date.\n\n        Args:\n            project_name (string): project name\n            site_name (string): active site name\n            reset_missing (bool): if True reset site in DB if missing\n                physically\n        \"\"\"\n        self.log.debug(\"Validation of {} for {} started\".format(project_name,\n                                                                site_name))\n        representations = list(get_representations(project_name))\n        if not representations:\n            self.log.debug(\"No repre found\")\n            return\n\n        sites_added = 0\n        sites_reset = 0\n        for repre in representations:\n            repre_id = repre[\"_id\"]\n            for repre_file in repre.get(\"files\", []):\n                try:\n                    is_on_site = site_name in [site[\"name\"]\n                                               for site in repre_file[\"sites\"]\n                                               if (site.get(\"created_dt\") and\n                                               not site.get(\"error\"))]\n                except (TypeError, AttributeError):\n                    self.log.debug(\"Structure error in {}\".format(repre_id))\n                    continue\n\n                file_path = repre_file.get(\"path\", \"\")\n                local_file_path = self.get_local_file_path(project_name,\n                                                           site_name,\n                                                           file_path)\n\n                file_exists = (local_file_path and\n                               os.path.exists(local_file_path))\n                if not is_on_site:\n                    if file_exists:\n                        self.log.debug(\n                            \"Adding site {} for {}\".format(site_name,\n                                                           repre_id))\n\n                        created_dt = datetime.fromtimestamp(\n                            os.path.getmtime(local_file_path))\n                        elem = {\"name\": site_name,\n                                \"created_dt\": created_dt}\n                        self._add_site(project_name, repre, elem,\n                                       site_name=site_name,\n                                       file_id=repre_file[\"_id\"],\n                                       force=True)\n                        sites_added += 1\n                else:\n                    if not file_exists and reset_missing:\n                        self.log.debug(\"Resetting site {} for {}\".\n                                       format(site_name, repre_id))\n                        self.reset_site_on_representation(\n                            project_name, repre_id, site_name=site_name,\n                            file_id=repre_file[\"_id\"])\n                        sites_reset += 1\n\n        if sites_added % 100 == 0:\n            self.log.debug(\"Sites added {}\".format(sites_added))\n\n        self.log.debug(\"Validation of {} for {} ended\".format(project_name,\n                                                              site_name))\n        self.log.info(\"Sites added {}, sites reset {}\".format(sites_added,\n                                                              reset_missing))\n\n    def pause_representation(self, project_name, representation_id, site_name):\n        \"\"\"\n            Sets 'representation_id' as paused, eg. no syncing should be\n            happening on it.\n\n            Args:\n                project_name (string): project name\n                representation_id (string): MongoDB objectId value\n                site_name (string): 'gdrive', 'studio' etc.\n        \"\"\"\n        self.log.info(\"Pausing SyncServer for {}\".format(representation_id))\n        self.reset_site_on_representation(project_name, representation_id,\n                                          site_name=site_name, pause=True)\n\n    def unpause_representation(self, project_name,\n                               representation_id, site_name):\n        \"\"\"\n            Sets 'representation_id' as unpaused.\n\n            Does not fail or warn if repre wasn't paused.\n\n            Args:\n                project_name (string): project name\n                representation_id (string): MongoDB objectId value\n                site_name (string): 'gdrive', 'studio' etc.\n        \"\"\"\n        self.log.info(\"Unpausing SyncServer for {}\".format(representation_id))\n        self.reset_site_on_representation(project_name, representation_id,\n                                          site_name=site_name, pause=False)\n\n    def is_representation_paused(self, project_name, representation_id,\n                                 site_name, check_parents=False):\n        \"\"\"\n            Returns if 'representation_id' is paused or not for site.\n\n            Args:\n                project_name (str): project to check if paused\n                representation_id (str): MongoDB objectId value\n                site (str): site to check representation is paused for\n                check_parents (bool): check if parent project or server itself\n                    are not paused\n\n            Returns:\n                (bool)\n        \"\"\"\n        # Check parents are paused\n        if check_parents and (\n            self.is_project_paused(project_name)\n            or self.is_paused()\n        ):\n            return True\n\n        # Get representation\n        representation = get_representation_by_id(project_name,\n                                                  representation_id,\n                                                  fields=[\"files.sites\"])\n        if not representation:\n            return False\n\n        # Check if representation is paused\n        for file_info in representation.get(\"files\", []):\n            for site in file_info.get(\"sites\", []):\n                if site[\"name\"] != site_name:\n                    continue\n\n                return site.get(\"paused\", False)\n\n        return False\n\n    def pause_project(self, project_name):\n        \"\"\"\n            Sets 'project_name' as paused, eg. no syncing should be\n            happening on all representation inside.\n\n            Args:\n                project_name (string): project_name name\n        \"\"\"\n        self.log.info(\"Pausing SyncServer for {}\".format(project_name))\n        self._paused_projects.add(project_name)\n\n    def unpause_project(self, project_name):\n        \"\"\"\n            Sets 'project_name' as unpaused\n\n            Does not fail or warn if project wasn't paused.\n\n            Args:\n                project_name (string):\n        \"\"\"\n        self.log.info(\"Unpausing SyncServer for {}\".format(project_name))\n        try:\n            self._paused_projects.remove(project_name)\n        except KeyError:\n            pass\n\n    def is_project_paused(self, project_name, check_parents=False):\n        \"\"\"\n            Returns if 'project_name' is paused or not.\n\n            Args:\n                project_name (string):\n                check_parents (bool): check if server itself\n                    is not paused\n            Returns:\n                (bool)\n        \"\"\"\n        condition = project_name in self._paused_projects\n        if check_parents:\n            condition = condition or self.is_paused()\n        return condition\n\n    def pause_server(self):\n        \"\"\"\n            Pause sync server\n\n            It won't check anything, not uploading/downloading...\n        \"\"\"\n        self.log.info(\"Pausing SyncServer\")\n        self._paused = True\n\n    def unpause_server(self):\n        \"\"\"\n            Unpause server\n        \"\"\"\n        self.log.info(\"Unpausing SyncServer\")\n        self._paused = False\n\n    def is_paused(self):\n        \"\"\" Is server paused \"\"\"\n        return self._paused\n\n    def get_active_sites(self, project_name):\n        \"\"\"\n            Returns list of active sites for 'project_name'.\n\n            By default it returns ['studio'], this site is default\n            and always present even if SyncServer is not enabled. (for publish)\n\n            Used mainly for Local settings for user override.\n\n            Args:\n                project_name (string):\n\n            Returns:\n                (list) of strings\n        \"\"\"\n        return self.get_active_sites_from_settings(\n            get_project_settings(project_name))\n\n    def get_active_sites_from_settings(self, settings):\n        \"\"\"\n            List available active sites from incoming 'settings'. Used for\n            returning 'default' values for Local Settings\n\n            Args:\n                settings (dict): full settings (global + project)\n            Returns:\n                (list) of strings\n        \"\"\"\n        sync_settings = self._parse_sync_settings_from_settings(settings)\n\n        return self._get_enabled_sites_from_settings(sync_settings)\n\n    def get_active_site(self, project_name):\n        \"\"\"\n            Returns active (mine) site for 'project_name' from settings\n\n            Returns:\n                (string)\n        \"\"\"\n        active_site = self.get_sync_project_setting(\n            project_name)['config']['active_site']\n        if active_site == self.LOCAL_SITE:\n            return get_local_site_id()\n        return active_site\n\n    def get_active_site_type(self, project_name, local_settings=None):\n        \"\"\"Active site which is defined by artist.\n\n        Unlike 'get_active_site' is this method also checking local settings\n        where might be different active site set by user. The output is limited\n        to \"studio\" and \"local\".\n\n        This method is used by Anatomy when is decided which\n\n        Todos:\n            Check if sync server is enabled for the project.\n            - To be able to do that the sync settings MUST NOT be cached for\n                all projects at once. The sync settings preparation for all\n                projects is reasonable only in sync server loop.\n\n        Args:\n            project_name (str): Name of project where to look for active site.\n            local_settings (Optional[dict[str, Any]]): Prepared local settings.\n\n        Returns:\n            Literal[\"studio\", \"local\"]: Active site.\n        \"\"\"\n\n        if not self.enabled:\n            return \"studio\"\n\n        if local_settings is None:\n            local_settings = get_local_settings()\n\n        local_project_settings = local_settings.get(\"projects\")\n        project_settings = get_project_settings(project_name)\n        sync_server_settings = project_settings[\"global\"][\"sync_server\"]\n        if not sync_server_settings[\"enabled\"]:\n            return \"studio\"\n\n        project_active_site = sync_server_settings[\"config\"][\"active_site\"]\n        if not local_project_settings:\n            return project_active_site\n\n        project_locals = local_project_settings.get(project_name) or {}\n        default_locals = local_project_settings.get(DEFAULT_PROJECT_KEY) or {}\n        active_site = (\n            project_locals.get(\"active_site\")\n            or default_locals.get(\"active_site\")\n        )\n        if active_site:\n            return active_site\n        return project_active_site\n\n    def get_site_root_overrides(\n        self, project_name, site_name, local_settings=None\n    ):\n        \"\"\"Get root overrides for project on a site.\n\n        Implemented to be used in 'Anatomy' for other than 'studio' site.\n\n        Args:\n            project_name (str): Project for which root overrides should be\n                received.\n            site_name (str): Name of site for which should be received roots.\n            local_settings (Optional[dict[str, Any]]): Prepare local settigns\n                values.\n\n        Returns:\n            Union[dict[str, Any], None]: Root overrides for this machine.\n        \"\"\"\n\n        # Validate that site name is valid\n        if site_name not in (\"studio\", \"local\"):\n            # Considure local site id as 'local'\n            if site_name != get_local_site_id():\n                raise ValueError((\n                    \"Root overrides are available only for\"\n                    \" default sites not for \\\"{}\\\"\"\n                ).format(site_name))\n            site_name = \"local\"\n\n        if local_settings is None:\n            local_settings = get_local_settings()\n\n        if not local_settings:\n            return\n\n        local_project_settings = local_settings.get(\"projects\") or {}\n\n        # Check for roots existence in local settings first\n        roots_project_locals = (\n            local_project_settings\n            .get(project_name, {})\n        )\n        roots_default_locals = (\n            local_project_settings\n            .get(DEFAULT_PROJECT_KEY, {})\n        )\n\n        # Skip rest of processing if roots are not set\n        if not roots_project_locals and not roots_default_locals:\n            return\n\n        # Combine roots from local settings\n        roots_locals = roots_default_locals.get(site_name) or {}\n        roots_locals.update(roots_project_locals.get(site_name) or {})\n        return roots_locals\n\n    # remote sites\n    def get_remote_sites(self, project_name):\n        \"\"\"\n            Returns all remote sites configured on 'project_name'.\n\n            If 'project_name' is not enabled for syncing returns [].\n\n            Used by Local setting to allow user choose remote site.\n\n            Args:\n                project_name (string):\n\n            Returns:\n                (list) of strings\n        \"\"\"\n        return self.get_remote_sites_from_settings(\n            get_project_settings(project_name))\n\n    def get_remote_sites_from_settings(self, settings):\n        \"\"\"\n            Get remote sites for returning 'default' values for Local Settings\n        \"\"\"\n        sync_settings = self._parse_sync_settings_from_settings(settings)\n\n        return self._get_remote_sites_from_settings(sync_settings)\n\n    def get_remote_site(self, project_name):\n        \"\"\"\n            Returns remote (theirs) site for 'project_name' from settings\n        \"\"\"\n        remote_site = self.get_sync_project_setting(\n            project_name)['config']['remote_site']\n        if remote_site == self.LOCAL_SITE:\n            return get_local_site_id()\n\n        return remote_site\n\n    def get_local_normalized_site(self, site_name):\n        \"\"\"\n            Return 'site_name' or 'local' if 'site_name' is local id.\n\n            In some places Settings or Local Settings require 'local' instead\n            of real site name.\n        \"\"\"\n        if site_name == get_local_site_id():\n            site_name = self.LOCAL_SITE\n\n        return site_name\n\n    # Methods for Settings UI to draw appropriate forms\n    @classmethod\n    def get_system_settings_schema(cls):\n        \"\"\" Gets system level schema of  configurable items\n\n            Used for Setting UI to provide forms.\n        \"\"\"\n        ret_dict = {}\n        for provider_code in lib.factory.providers:\n            ret_dict[provider_code] = \\\n                lib.factory.get_provider_cls(provider_code). \\\n                get_system_settings_schema()\n\n        return ret_dict\n\n    @classmethod\n    def get_project_settings_schema(cls):\n        \"\"\" Gets project level schema of configurable items.\n\n            It is not using Setting! Used for Setting UI to provide forms.\n        \"\"\"\n        ret_dict = {}\n        for provider_code in lib.factory.providers:\n            ret_dict[provider_code] = \\\n                lib.factory.get_provider_cls(provider_code). \\\n                get_project_settings_schema()\n\n        return ret_dict\n\n    @classmethod\n    def get_local_settings_schema(cls):\n        \"\"\" Gets local level schema of configurable items.\n\n            It is not using Setting! Used for Setting UI to provide forms.\n        \"\"\"\n        ret_dict = {}\n        for provider_code in lib.factory.providers:\n            ret_dict[provider_code] = \\\n                lib.factory.get_provider_cls(provider_code). \\\n                get_local_settings_schema()\n\n        return ret_dict\n\n    def get_launch_hook_paths(self):\n        \"\"\"Implementation for applications launch hooks.\n\n        Returns:\n            (str): full absolut path to directory with hooks for the module\n        \"\"\"\n\n        return os.path.join(SYNC_SERVER_ROOT, \"launch_hooks\")\n\n    # Needs to be refactored after Settings are updated\n    # # Methods for Settings to get appriate values to fill forms\n    # def get_configurable_items(self, scope=None):\n    #     \"\"\"\n    #         Returns list of sites that could be configurable for all projects\n    #\n    #         Could be filtered by 'scope' argument (list)\n    #\n    #         Args:\n    #             scope (list of utils.EditableScope)\n    #\n    #         Returns:\n    #             (dict of list of dict)\n    #             {\n    #                 siteA : [\n    #                     {\n    #                         key:\"root\", label:\"root\",\n    #                         \"value\":\"{'work': 'c:/projects'}\",\n    #                         \"type\": \"dict\",\n    #                         \"children\":[\n    #                             { \"key\": \"work\",\n    #                               \"type\": \"text\",\n    #                               \"value\": \"c:/projects\"}\n    #                         ]\n    #                     },\n    #                     {\n    #                         key:\"credentials_url\", label:\"Credentials url\",\n    #                         \"value\":\"'c:/projects/cred.json'\", \"type\": \"text\",  # noqa: E501\n    #                         \"namespace\": \"{project_setting}/global/sync_server/  # noqa: E501\n    #                                  sites\"\n    #                     }\n    #                 ]\n    #             }\n    #     \"\"\"\n    #     editable = {}\n    #     applicable_projects = list(self.connection.projects())\n    #     applicable_projects.append(None)\n    #     for project in applicable_projects:\n    #         project_name = None\n    #         if project:\n    #             project_name = project[\"name\"]\n    #\n    #         items = self.get_configurable_items_for_project(project_name,\n    #                                                         scope)\n    #         editable.update(items)\n    #\n    #     return editable\n    #\n    # def get_local_settings_schema_for_project(self, project_name):\n    #     \"\"\"Wrapper for Local settings - for specific 'project_name'\"\"\"\n    #     return self.get_configurable_items_for_project(project_name,\n    #                                                    EditableScopes.LOCAL)\n    #\n    # def get_configurable_items_for_project(self, project_name=None,\n    #                                        scope=None):\n    #     \"\"\"\n    #         Returns list of items that could be configurable for specific\n    #         'project_name'\n    #\n    #         Args:\n    #             project_name (str) - None > default project,\n    #             scope (list of utils.EditableScope)\n    #                 (optional, None is all scopes, default is LOCAL)\n    #\n    #         Returns:\n    #             (dict of list of dict)\n    #         {\n    #             siteA : [\n    #                 {\n    #                     key:\"root\", label:\"root\",\n    #                     \"type\": \"dict\",\n    #                     \"children\":[\n    #                         { \"key\": \"work\",\n    #                           \"type\": \"text\",\n    #                           \"value\": \"c:/projects\"}\n    #                     ]\n    #                 },\n    #                 {\n    #                     key:\"credentials_url\", label:\"Credentials url\",\n    #                     \"value\":\"'c:/projects/cred.json'\", \"type\": \"text\",\n    #                     \"namespace\": \"{project_setting}/global/sync_server/\n    #                                  sites\"\n    #                 }\n    #             ]\n    #         }\n    #     \"\"\"\n    #     allowed_sites = set()\n    #     sites = self.get_all_site_configs(project_name)\n    #     if project_name:\n    #         # Local Settings can select only from allowed sites for project\n    #         allowed_sites.update(set(self.get_active_sites(project_name)))\n    #         allowed_sites.update(set(self.get_remote_sites(project_name)))\n    #\n    #     editable = {}\n    #     for site_name in sites.keys():\n    #         if allowed_sites and site_name not in allowed_sites:\n    #             continue\n    #\n    #         items = self.get_configurable_items_for_site(project_name,\n    #                                                      site_name,\n    #                                                      scope)\n    #         # Local Settings need 'local' instead of real value\n    #         site_name = site_name.replace(get_local_site_id(), 'local')\n    #         editable[site_name] = items\n    #\n    #     return editable\n    #\n    # def get_configurable_items_for_site(self, project_name=None,\n    #                                     site_name=None,\n    #                                     scope=None):\n    #     \"\"\"\n    #         Returns list of items that could be configurable.\n    #\n    #         Args:\n    #             project_name (str) - None > default project\n    #             site_name (str)\n    #             scope (list of utils.EditableScope)\n    #                 (optional, None is all scopes)\n    #\n    #         Returns:\n    #             (list)\n    #             [\n    #                 {\n    #                     key:\"root\", label:\"root\", type:\"dict\",\n    #                     \"children\":[\n    #                         { \"key\": \"work\",\n    #                           \"type\": \"text\",\n    #                           \"value\": \"c:/projects\"}\n    #                     ]\n    #                 }, ...\n    #             ]\n    #     \"\"\"\n    #     provider_name = self.get_provider_for_site(site=site_name)\n    #     items = lib.factory.get_provider_configurable_items(provider_name)\n    #\n    #     if project_name:\n    #         sync_s = self.get_sync_project_setting(project_name,\n    #                                                exclude_locals=True,\n    #                                                cached=False)\n    #     else:\n    #         sync_s = get_default_project_settings(exclude_locals=True)\n    #         sync_s = sync_s[\"global\"][\"sync_server\"]\n    #         sync_s[\"sites\"].update(\n    #             self._get_default_site_configs(self.enabled))\n    #\n    #     editable = []\n    #     if type(scope) is not list:\n    #         scope = [scope]\n    #     scope = set(scope)\n    #     for key, properties in items.items():\n    #         if scope is None or scope.intersection(set(properties[\"scope\"])):\n    #             val = sync_s.get(\"sites\", {}).get(site_name, {}).get(key)\n    #\n    #             item = {\n    #                 \"key\": key,\n    #                 \"label\": properties[\"label\"],\n    #                 \"type\": properties[\"type\"]\n    #             }\n    #\n    #             if properties.get(\"namespace\"):\n    #                 item[\"namespace\"] = properties.get(\"namespace\")\n    #                 if \"platform\" in item[\"namespace\"]:\n    #                     try:\n    #                         if val:\n    #                             val = val[platform.system().lower()]\n    #                     except KeyError:\n    #                         st = \"{}'s field value {} should be\".format(key, val)  # noqa: E501\n    #                         self.log.error(st + \" multiplatform dict\")\n    #\n    #                 item[\"namespace\"] = item[\"namespace\"].replace('{site}',\n    #                                                               site_name)\n    #             children = []\n    #             if properties[\"type\"] == \"dict\":\n    #                 if val:\n    #                     for val_key, val_val in val.items():\n    #                         child = {\n    #                             \"type\": \"text\",\n    #                             \"key\": val_key,\n    #                             \"value\": val_val\n    #                         }\n    #                         children.append(child)\n    #\n    #             if properties[\"type\"] == \"dict\":\n    #                 item[\"children\"] = children\n    #             else:\n    #                 item[\"value\"] = val\n    #\n    #             editable.append(item)\n    #\n    #     return editable\n\n    def reset_timer(self):\n        \"\"\"\n            Called when waiting for next loop should be skipped.\n\n            In case of user's involvement (reset site), start that right away.\n        \"\"\"\n\n        if not self.enabled:\n            return\n\n        if self.sync_server_thread is None:\n            self._reset_timer_with_rest_api()\n        else:\n            self.sync_server_thread.reset_timer()\n\n    def is_representation_on_site(\n        self, project_name, representation_id, site_name, max_retries=None\n    ):\n        \"\"\"Checks if 'representation_id' has all files avail. on 'site_name'\n\n        Args:\n            project_name (str)\n            representation_id (str)\n            site_name (str)\n            max_retries (int) (optional) - provide only if method used in while\n                loop to bail out\n        Returns:\n            (bool): True if 'representation_id' has all files correctly on the\n            'site_name'\n        Raises:\n              (ValueError)  Only If 'max_retries' provided if upload/download\n        failed too many times to limit infinite loop check.\n        \"\"\"\n        representation = get_representation_by_id(project_name,\n                                                  representation_id,\n                                                  fields=[\"_id\", \"files\"])\n        if not representation:\n            return False\n\n        on_site = False\n        for file_info in representation.get(\"files\", []):\n            for site in file_info.get(\"sites\", []):\n                if site[\"name\"] != site_name:\n                    continue\n\n                if max_retries:\n                    tries = self._get_tries_count_from_rec(site)\n                    if tries >= max_retries:\n                        raise ValueError(\"Failed too many times\")\n\n                if (site.get(\"progress\") or site.get(\"error\") or\n                        not site.get(\"created_dt\")):\n                    return False\n                on_site = True\n\n        return on_site\n\n    def _reset_timer_with_rest_api(self):\n        # POST to webserver sites to add to representations\n        webserver_url = os.environ.get(\"OPENPYPE_WEBSERVER_URL\")\n        if not webserver_url:\n            self.log.warning(\"Couldn't find webserver url\")\n            return\n\n        rest_api_url = \"{}/sync_server/reset_timer\".format(\n            webserver_url\n        )\n\n        try:\n            import requests\n        except Exception:\n            self.log.warning(\n                \"Couldn't add sites to representations \"\n                \"('requests' is not available)\"\n            )\n            return\n\n        requests.post(rest_api_url)\n\n    def get_enabled_projects(self):\n        \"\"\"Returns list of projects which have SyncServer enabled.\"\"\"\n        enabled_projects = []\n\n        if self.enabled:\n            for project in get_projects(fields=[\"name\"]):\n                project_name = project[\"name\"]\n                if self.is_project_enabled(project_name):\n                    enabled_projects.append(project_name)\n\n        return enabled_projects\n\n    def is_project_enabled(self, project_name, single=False):\n        \"\"\"Checks if 'project_name' is enabled for syncing.\n        'get_sync_project_setting' is potentially expensive operation (pulls\n        settings for all projects if cached version is not available), using\n        project_settings for specific project should be faster.\n        Args:\n            project_name (str)\n            single (bool): use 'get_project_settings' method\n        \"\"\"\n        if self.enabled:\n            if single:\n                project_settings = get_project_settings(project_name)\n                project_settings = \\\n                    self._parse_sync_settings_from_settings(project_settings)\n            else:\n                project_settings = self.get_sync_project_setting(project_name)\n            if project_settings and project_settings.get(\"enabled\"):\n                return True\n        return False\n\n    def handle_alternate_site(self, project_name, representation,\n                              processed_site, file_id, synced_file_id):\n        \"\"\"\n            For special use cases where one site vendors another.\n\n            Current use case is sftp site vendoring (exposing) same data as\n            regular site (studio). Each site is accessible for different\n            audience. 'studio' for artists in a studio, 'sftp' for externals.\n\n            Change of file status on one site actually means same change on\n            'alternate' site. (eg. artists publish to 'studio', 'sftp' is using\n            same location >> file is accesible on 'sftp' site right away.\n\n            Args:\n                project_name (str): name of project\n                representation (dict)\n                processed_site (str): real site_name of published/uploaded file\n                file_id (ObjectId): DB id of file handled\n                synced_file_id (str): id of the created file returned\n                    by provider\n        \"\"\"\n        sites = self.sync_system_settings.get(\"sites\", {})\n        sites[self.DEFAULT_SITE] = {\"provider\": \"local_drive\",\n                                    \"alternative_sites\": []}\n\n        alternate_sites = []\n        for site_name, site_info in sites.items():\n            conf_alternative_sites = site_info.get(\"alternative_sites\", [])\n            if processed_site in conf_alternative_sites:\n                alternate_sites.append(site_name)\n                continue\n            if processed_site == site_name and conf_alternative_sites:\n                alternate_sites.extend(conf_alternative_sites)\n                continue\n\n        alternate_sites = set(alternate_sites)\n\n        for alt_site in alternate_sites:\n            elem = {\"name\": alt_site,\n                    \"created_dt\": datetime.now(),\n                    \"id\": synced_file_id}\n\n            self.log.debug(\"Adding alternate {} to {}\".format(\n                alt_site, representation[\"_id\"]))\n            self._add_site(project_name,\n                           representation, elem,\n                           alt_site, file_id=file_id, force=True)\n\n    def get_repre_info_for_versions(self, project_name, version_ids,\n                                    active_site, remote_site):\n        \"\"\"Returns representation documents for versions and sites combi\n\n        Args:\n            project_name (str)\n            version_ids (list): of version[_id]\n            active_site (string): 'local', 'studio' etc\n            remote_site (string): dtto\n        Returns:\n\n        \"\"\"\n        self.connection.Session[\"AVALON_PROJECT\"] = project_name\n        query = [\n            {\"$match\": {\"parent\": {\"$in\": version_ids},\n                        \"type\": \"representation\",\n                        \"files.sites.name\": {\"$exists\": 1}}},\n            {\"$unwind\": \"$files\"},\n            {'$addFields': {\n                'order_local': {\n                    '$filter': {\n                        'input': '$files.sites', 'as': 'p',\n                        'cond': {'$eq': ['$$p.name', active_site]}\n                    }\n                }\n            }},\n            {'$addFields': {\n                'order_remote': {\n                    '$filter': {\n                        'input': '$files.sites', 'as': 'p',\n                        'cond': {'$eq': ['$$p.name', remote_site]}\n                    }\n                }\n            }},\n            {'$addFields': {\n                'progress_local': {\"$arrayElemAt\": [{\n                    '$cond': [\n                        {'$size': \"$order_local.progress\"},\n                        \"$order_local.progress\",\n                        # if exists created_dt count is as available\n                        {'$cond': [\n                            {'$size': \"$order_local.created_dt\"},\n                            [1],\n                            [0]\n                        ]}\n                    ]},\n                    0\n                ]}\n            }},\n            {'$addFields': {\n                'progress_remote': {\"$arrayElemAt\": [{\n                    '$cond': [\n                        {'$size': \"$order_remote.progress\"},\n                        \"$order_remote.progress\",\n                        # if exists created_dt count is as available\n                        {'$cond': [\n                            {'$size': \"$order_remote.created_dt\"},\n                            [1],\n                            [0]\n                        ]}\n                    ]},\n                    0\n                ]}\n            }},\n            {'$group': {  # first group by repre\n                '_id': '$_id',\n                'parent': {'$first': '$parent'},\n                'avail_ratio_local': {\n                    '$first': {\n                        '$divide': [{'$sum': \"$progress_local\"}, {'$sum': 1}]\n                    }\n                },\n                'avail_ratio_remote': {\n                    '$first': {\n                        '$divide': [{'$sum': \"$progress_remote\"}, {'$sum': 1}]\n                    }\n                }\n            }},\n            {'$group': {  # second group by parent, eg version_id\n                '_id': '$parent',\n                'repre_count': {'$sum': 1},  # total representations\n                # fully available representation for site\n                'avail_repre_local': {'$sum': \"$avail_ratio_local\"},\n                'avail_repre_remote': {'$sum': \"$avail_ratio_remote\"},\n            }},\n        ]\n        # docs = list(self.connection.aggregate(query))\n        return self.connection.aggregate(query)\n\n    \"\"\" End of Public API \"\"\"\n\n    def get_local_file_path(self, project_name, site_name, file_path):\n        \"\"\"\n            Externalized for app\n        \"\"\"\n        handler = LocalDriveHandler(project_name, site_name)\n        local_file_path = handler.resolve_path(file_path)\n\n        return local_file_path\n\n    def _get_remote_sites_from_settings(self, sync_settings):\n        if not self.enabled or not sync_settings.get('enabled'):\n            return []\n\n        remote_sites = [self.DEFAULT_SITE, self.LOCAL_SITE]\n        if sync_settings:\n            remote_sites.extend(sync_settings.get(\"sites\").keys())\n\n        return list(set(remote_sites))\n\n    def _get_enabled_sites_from_settings(self, sync_settings):\n        sites = [self.DEFAULT_SITE]\n        if self.enabled and sync_settings.get('enabled'):\n            sites.append(self.LOCAL_SITE)\n\n        active_site = sync_settings[\"config\"][\"active_site\"]\n        # for Tray running background process\n        if active_site not in sites and active_site == get_local_site_id():\n            sites.append(active_site)\n\n        return sites\n\n    def tray_init(self):\n        \"\"\"\n            Actual initialization of Sync Server for Tray.\n\n            Called when tray is initialized, it checks if module should be\n            enabled. If not, no initialization necessary.\n        \"\"\"\n        self.server_init()\n\n    def server_init(self):\n        \"\"\"Actual initialization of Sync Server.\"\"\"\n        # import only in tray or Python3, because of Python2 hosts\n        if not self.enabled:\n            return\n\n        from .sync_server import SyncServerThread\n\n        self.lock = threading.Lock()\n\n        self.sync_server_thread = SyncServerThread(self)\n\n    def tray_start(self):\n        \"\"\"\n            Triggered when Tray is started.\n\n            Checks if configuration presets are available and if there is\n            any provider ('gdrive', 'S3') that is activated\n            (eg. has valid credentials).\n\n        Returns:\n            None\n        \"\"\"\n        self.server_start()\n\n    def server_start(self):\n        if self.enabled:\n            self.sync_server_thread.start()\n        else:\n            self.log.info(\"No presets or active providers. \" +\n                     \"Synchronization not possible.\")\n\n    def tray_exit(self):\n        \"\"\"\n            Stops sync thread if running.\n\n            Called from Module Manager\n        \"\"\"\n        self.server_exit()\n\n    def server_exit(self):\n        if not self.sync_server_thread:\n            return\n\n        if not self.is_running:\n            return\n        try:\n            self.log.info(\"Stopping sync server server\")\n            self.sync_server_thread.is_running = False\n            self.sync_server_thread.stop()\n            self.log.info(\"Sync server stopped\")\n        except Exception:\n            self.log.warning(\n                \"Error has happened during Killing sync server\",\n                exc_info=True\n            )\n\n    def tray_menu(self, parent_menu):\n        if not self.enabled:\n            return\n\n        from qtpy import QtWidgets\n        \"\"\"Add menu or action to Tray(or parent)'s menu\"\"\"\n        action = QtWidgets.QAction(self.label, parent_menu)\n        action.triggered.connect(self.show_widget)\n        parent_menu.addAction(action)\n        parent_menu.addSeparator()\n\n        self.action_show_widget = action\n\n    @property\n    def is_running(self):\n        return self.sync_server_thread.is_running\n\n    def get_anatomy(self, project_name):\n        \"\"\"\n            Get already created or newly created anatomy for project\n\n            Args:\n                project_name (string):\n\n            Return:\n                (Anatomy)\n        \"\"\"\n        return self._anatomies.get('project_name') or Anatomy(project_name)\n\n    @property\n    def connection(self):\n        if self._connection is None:\n            self._connection = AvalonMongoDB()\n\n        return self._connection\n\n    @property\n    def sync_system_settings(self):\n        if self._sync_system_settings is None:\n            self._sync_system_settings = get_system_settings()[\"modules\"].\\\n                get(\"sync_server\")\n\n        return self._sync_system_settings\n\n    @property\n    def sync_project_settings(self):\n        if self._sync_project_settings is None:\n            self.set_sync_project_settings()\n\n        return self._sync_project_settings\n\n    def set_sync_project_settings(self, exclude_locals=False):\n        \"\"\"\n            Set sync_project_settings for all projects (caching)\n            Args:\n                exclude_locals (bool): ignore overrides from Local Settings\n            For performance\n        \"\"\"\n        sync_project_settings = self._prepare_sync_project_settings(\n            exclude_locals)\n\n        self._sync_project_settings = sync_project_settings\n\n    def _prepare_sync_project_settings(self, exclude_locals):\n        sync_project_settings = {}\n        system_sites = self.get_all_site_configs()\n        project_docs = get_projects(fields=[\"name\"])\n        for project_doc in project_docs:\n            project_name = project_doc[\"name\"]\n            sites = copy.deepcopy(system_sites)  # get all configured sites\n            proj_settings = self._parse_sync_settings_from_settings(\n                get_project_settings(project_name,\n                                     exclude_locals=exclude_locals))\n            sites.update(self._get_default_site_configs(\n                proj_settings[\"enabled\"], project_name))\n            sites.update(proj_settings['sites'])\n            proj_settings[\"sites\"] = sites\n\n            sync_project_settings[project_name] = proj_settings\n        if not sync_project_settings:\n            self.log.info(\"No enabled and configured projects for sync.\")\n        return sync_project_settings\n\n    def get_sync_project_setting(self, project_name, exclude_locals=False,\n                                 cached=True):\n        \"\"\" Handles pulling sync_server's settings for enabled 'project_name'\n\n            Args:\n                project_name (str): used in project settings\n                exclude_locals (bool): ignore overrides from Local Settings\n                cached (bool): use pre-cached values, or return fresh ones\n                    cached values needed for single loop (with all overrides)\n                    fresh values needed for Local settings (without overrides)\n            Returns:\n                (dict): settings dictionary for the enabled project,\n                    empty if no settings or sync is disabled\n        \"\"\"\n        # presets set already, do not call again and again\n        # self.log.debug(\"project preset {}\".format(self.presets))\n        if not cached:\n            return self._prepare_sync_project_settings(exclude_locals)\\\n                [project_name]\n\n        if not self.sync_project_settings or \\\n               not self.sync_project_settings.get(project_name):\n            self.set_sync_project_settings(exclude_locals)\n\n        return self.sync_project_settings.get(project_name)\n\n    def _parse_sync_settings_from_settings(self, settings):\n        \"\"\" settings from api.get_project_settings, TOOD rename \"\"\"\n        sync_settings = settings.get(\"global\").get(\"sync_server\")\n\n        return sync_settings\n\n    def get_all_site_configs(self, project_name=None,\n                             local_editable_only=False):\n        \"\"\"\n            Returns (dict) with all sites configured system wide.\n\n            Args:\n                project_name (str)(optional): if present, check if not disabled\n                local_editable_only (bool)(opt): if True return only Local\n                    Setting configurable (for LS UI)\n            Returns:\n                (dict): {'studio': {'provider':'local_drive'...},\n                         'MY_LOCAL': {'provider':....}}\n        \"\"\"\n        sync_sett = self.sync_system_settings\n        project_enabled = True\n        project_settings = None\n        if project_name:\n            project_enabled = project_name in self.get_enabled_projects()\n            project_settings = self.get_sync_project_setting(project_name)\n        sync_enabled = sync_sett[\"enabled\"] and project_enabled\n\n        system_sites = {}\n        if sync_enabled:\n            for site, detail in sync_sett.get(\"sites\", {}).items():\n                if project_settings:\n                    site_settings = project_settings[\"sites\"].get(site)\n                    if site_settings:\n                        detail.update(site_settings)\n                system_sites[site] = detail\n        system_sites.update(self._get_default_site_configs(sync_enabled,\n                                                           project_name))\n        if local_editable_only:\n            local_schema = SyncServerModule.get_local_settings_schema()\n            editable_keys = {}\n            for provider_code, editables in local_schema.items():\n                editable_keys[provider_code] = [\"enabled\", \"provider\"]\n                for editable_item in editables:\n                    editable_keys[provider_code].append(editable_item[\"key\"])\n\n            for _, site in system_sites.items():\n                provider = site[\"provider\"]\n                for site_config_key in list(site.keys()):\n                    if site_config_key not in editable_keys[provider]:\n                        site.pop(site_config_key, None)\n\n        return system_sites\n\n    def _get_default_site_configs(self, sync_enabled=True, project_name=None):\n        \"\"\"\n            Returns settings for 'studio' and user's local site\n\n            Returns base values from setting, not overridden by Local Settings,\n            eg. value used to push TO LS not to get actual value for syncing.\n        \"\"\"\n        if not project_name:\n            anatomy_sett = get_default_anatomy_settings(exclude_locals=True)\n        else:\n            anatomy_sett = get_anatomy_settings(project_name,\n                                                exclude_locals=True)\n        roots = {}\n        for root, config in anatomy_sett[\"roots\"].items():\n            roots[root] = config\n        studio_config = {\n            'enabled': True,\n            'provider': 'local_drive',\n            \"root\": roots\n        }\n        all_sites = {self.DEFAULT_SITE: studio_config}\n        if sync_enabled:\n            all_sites[get_local_site_id()] = {'enabled': True,\n                                              'provider': 'local_drive',\n                                              \"root\": roots}\n            # duplicate values for normalized local name\n            all_sites[\"local\"] = {\n                'enabled': True,\n                'provider': 'local_drive',\n                \"root\": roots}\n        return all_sites\n\n    def get_provider_for_site(self, project_name=None, site=None):\n        \"\"\"\n            Return provider name for site (unique name across all projects.\n        \"\"\"\n        sites = {self.DEFAULT_SITE: \"local_drive\",\n                 self.LOCAL_SITE: \"local_drive\",\n                 get_local_site_id(): \"local_drive\"}\n\n        if site in sites.keys():\n            return sites[site]\n\n        if project_name:  # backward compatibility\n            proj_settings = self.get_sync_project_setting(project_name)\n            provider = proj_settings.get(\"sites\", {}).get(site, {}).\\\n                get(\"provider\")\n            if provider:\n                return provider\n\n        sync_sett = self.sync_system_settings\n        for conf_site, detail in sync_sett.get(\"sites\", {}).items():\n            sites[conf_site] = detail.get(\"provider\")\n\n        return sites.get(site, 'N/A')\n\n    @time_function\n    def get_sync_representations(self, project_name, active_site, remote_site):\n        \"\"\"\n            Get representations that should be synced, these could be\n            recognised by presence of document in 'files.sites', where key is\n            a provider (GDrive, S3) and value is empty document or document\n            without 'created_dt' field. (Don't put null to 'created_dt'!).\n\n            Querying of 'to-be-synched' files is offloaded to Mongod for\n            better performance. Goal is to get as few representations as\n            possible.\n        Args:\n            project_name (string):\n            active_site (string): identifier of current active site (could be\n                'local_0' when working from home, 'studio' when working in the\n                studio (default)\n            remote_site (string): identifier of remote site I want to sync to\n\n        Returns:\n            (list) of dictionaries\n        \"\"\"\n        self.log.debug(\"Check representations for : {}\".format(project_name))\n        self.connection.Session[\"AVALON_PROJECT\"] = project_name\n        # retry_cnt - number of attempts to sync specific file before giving up\n        retries_arr = self._get_retries_arr(project_name)\n        match = {\n            \"type\": \"representation\",\n            \"$or\": [\n                {\"$and\": [\n                    {\n                        \"files.sites\": {\n                            \"$elemMatch\": {\n                                \"name\": active_site,\n                                \"created_dt\": {\"$exists\": True}\n                            }\n                        }}, {\n                        \"files.sites\": {\n                            \"$elemMatch\": {\n                                \"name\": {\"$in\": [remote_site]},\n                                \"created_dt\": {\"$exists\": False},\n                                \"tries\": {\"$in\": retries_arr},\n                                \"paused\": {\"$exists\": False}\n                            }\n                        }\n                    }]},\n                {\"$and\": [\n                    {\n                        \"files.sites\": {\n                            \"$elemMatch\": {\n                                \"name\": active_site,\n                                \"created_dt\": {\"$exists\": False},\n                                \"tries\": {\"$in\": retries_arr},\n                                \"paused\": {\"$exists\": False}\n                            }\n                        }}, {\n                        \"files.sites\": {\n                            \"$elemMatch\": {\n                                \"name\": {\"$in\": [remote_site]},\n                                \"created_dt\": {\"$exists\": True}\n                            }\n                        }\n                    }\n                ]}\n            ]\n        }\n\n        aggr = [\n            {\"$match\": match},\n            {'$unwind': '$files'},\n            {'$addFields': {\n                'order_remote': {\n                    '$filter': {'input': '$files.sites', 'as': 'p',\n                                'cond': {'$eq': ['$$p.name', remote_site]}\n                                }},\n                'order_local': {\n                    '$filter': {'input': '$files.sites', 'as': 'p',\n                                'cond': {'$eq': ['$$p.name', active_site]}\n                                }},\n            }},\n            {'$addFields': {\n                'priority': {\n                    '$cond': [\n                        {'$size': '$order_local.priority'},\n                        {'$first': '$order_local.priority'},\n                        {'$cond': [\n                            {'$size': '$order_remote.priority'},\n                            {'$first': '$order_remote.priority'},\n                            self.DEFAULT_PRIORITY]}\n                    ]\n                },\n            }},\n            {'$group': {\n                '_id': '$_id',\n                # pass through context - same for representation\n                'context': {'$addToSet': '$context'},\n                'data': {'$addToSet': '$data'},\n                # pass through files as a list\n                'files': {'$addToSet': '$files'},\n                'priority': {'$max': \"$priority\"},\n            }},\n            {\"$sort\": {'priority': -1, '_id': 1}},\n        ]\n        self.log.debug(\"active_site:{} - remote_site:{}\".format(\n            active_site, remote_site\n        ))\n        self.log.debug(\"query: {}\".format(aggr))\n        representations = self.connection.aggregate(aggr)\n\n        return representations\n\n    def check_status(self, file, local_site, remote_site, config_preset):\n        \"\"\"\n            Check synchronization status for single 'file' of single\n            'representation' by single 'provider'.\n            (Eg. check if 'scene.ma' of lookdev.v10 should be synced to GDrive\n\n            Always is comparing local record, eg. site with\n            'name' == self.presets[PROJECT_NAME]['config'][\"active_site\"]\n\n            This leads to trigger actual upload or download, there is\n            a use case 'studio' <> 'remote' where user should publish\n            to 'studio', see progress in Tray GUI, but do not do\n            physical upload/download\n            (as multiple user would be doing that).\n\n            Do physical U/D only when any of the sites is user's local, in that\n            case only user has the data and must U/D.\n\n        Args:\n            file (dictionary):  of file from representation in Mongo\n            local_site (string):  - local side of compare (usually 'studio')\n            remote_site (string):  - gdrive etc.\n            config_preset (dict): config about active site, retries\n        Returns:\n            (string) - one of SyncStatus\n        \"\"\"\n        sites = file.get(\"sites\") or []\n\n        if get_local_site_id() not in (local_site, remote_site):\n            # don't do upload/download for studio sites\n            self.log.debug(\n                \"No local site {} - {}\".format(local_site, remote_site)\n            )\n            return SyncStatus.DO_NOTHING\n\n        _, remote_rec = self._get_site_rec(sites, remote_site) or {}\n        if remote_rec:  # sync remote target\n            created_dt = remote_rec.get(\"created_dt\")\n            if not created_dt:\n                tries = self._get_tries_count_from_rec(remote_rec)\n                # file will be skipped if unsuccessfully tried over threshold\n                # error metadata needs to be purged manually in DB to reset\n                if tries < int(config_preset[\"retry_cnt\"]):\n                    return SyncStatus.DO_UPLOAD\n            else:\n                _, local_rec = self._get_site_rec(sites, local_site) or {}\n                if not local_rec or not local_rec.get(\"created_dt\"):\n                    tries = self._get_tries_count_from_rec(local_rec)\n                    # file will be skipped if unsuccessfully tried over\n                    # threshold times, error metadata needs to be purged\n                    # manually in DB to reset\n                    if tries < int(config_preset[\"retry_cnt\"]):\n                        return SyncStatus.DO_DOWNLOAD\n\n        return SyncStatus.DO_NOTHING\n\n    def update_db(self, project_name, new_file_id, file, representation,\n                  site, error=None, progress=None, priority=None):\n        \"\"\"\n            Update 'provider' portion of records in DB with success (file_id)\n            or error (exception)\n\n        Args:\n            project_name (string): name of project - force to db connection as\n              each file might come from different collection\n            new_file_id (string): only present if file synced successfully\n            file (dictionary): info about processed file (pulled from DB)\n            representation (dictionary): parent repr of file (from DB)\n            site (string): label ('gdrive', 'S3')\n            error (string): exception message\n            progress (float): 0-0.99 of progress of upload/download\n            priority (int): 0-100 set priority\n\n        Returns:\n            None\n        \"\"\"\n        representation_id = representation.get(\"_id\")\n        file_id = None\n        if file:\n            file_id = file.get(\"_id\")\n\n        query = {\n            \"_id\": representation_id\n        }\n\n        update = {}\n        if new_file_id:\n            update[\"$set\"] = self._get_success_dict(new_file_id)\n            # reset previous errors if any\n            update[\"$unset\"] = self._get_error_dict(\"\", \"\", \"\")\n        elif progress is not None:\n            update[\"$set\"] = self._get_progress_dict(progress)\n        elif priority is not None:\n            update[\"$set\"] = self._get_priority_dict(priority, file_id)\n        else:\n            tries = self._get_tries_count(file, site)\n            tries += 1\n\n            update[\"$set\"] = self._get_error_dict(error, tries)\n\n        arr_filter = [\n            {'s.name': site}\n        ]\n        if file_id:\n            arr_filter.append({'f._id': ObjectId(file_id)})\n\n        self.connection.database[project_name].update_one(\n            query,\n            update,\n            upsert=True,\n            array_filters=arr_filter\n        )\n\n        if progress is not None or priority is not None:\n            return\n\n        status = 'failed'\n        error_str = 'with error {}'.format(error)\n        if new_file_id:\n            status = 'succeeded with id {}'.format(new_file_id)\n            error_str = ''\n\n        source_file = file.get(\"path\", \"\")\n        self.log.debug(\n            (\n                \"File for {} - {source_file} process {status} {error_str}\"\n            ).format(\n                representation_id,\n                status=status,\n                source_file=source_file,\n                error_str=error_str\n            )\n        )\n\n    def _get_file_info(self, files, _id):\n        \"\"\"\n            Return record from list of records which name matches to 'provider'\n            Could be possibly refactored with '_get_provider_rec' together.\n\n        Args:\n            files (list): of dictionaries with info about published files\n            _id (string): _id of specific file\n\n        Returns:\n            (int, dictionary): index from list and record with metadata\n                               about site (if/when created, errors..)\n            OR (-1, None) if not present\n        \"\"\"\n        for index, rec in enumerate(files):\n            if rec.get(\"_id\") == _id:\n                return index, rec\n\n        return -1, None\n\n    def _get_site_rec(self, sites, site_name):\n        \"\"\"\n            Return record from list of records which name matches to\n            'remote_site_name'\n\n        Args:\n            sites (list): of dictionaries\n            site_name (string): 'local_XXX', 'gdrive'\n\n        Returns:\n            (int, dictionary): index from list and record with metadata\n                               about site (if/when created, errors..)\n            OR (-1, None) if not present\n        \"\"\"\n        for index, rec in enumerate(sites):\n            if rec.get(\"name\") == site_name:\n                return index, rec\n\n        return -1, None\n\n    def reset_site_on_representation(self, project_name, representation_id,\n                                     side=None, file_id=None, site_name=None,\n                                     remove=False, pause=None, force=False,\n                                     priority=None):\n        \"\"\"\n            Reset information about synchronization for particular 'file_id'\n            and provider.\n            Useful for testing or forcing file to be reuploaded.\n\n            'side' and 'site_name' are disjunctive.\n\n            'side' is used for resetting local or remote side for\n            current user for repre.\n\n            'site_name' is used to set synchronization for particular site.\n            Should be used when repre should be synced to new site.\n\n        Args:\n            project_name (string): name of project (eg. collection) in DB\n            representation_id(string): _id of representation\n            file_id (string):  file _id in representation\n            side (string): local or remote side\n            site_name (string): for adding new site\n            remove (bool): if True remove site altogether\n            pause (bool or None): if True - pause, False - unpause\n            force (bool): hard reset - currently only for add_site\n            priority (int): set priority\n\n        Raises:\n            SiteAlreadyPresentError - if adding already existing site and\n                not 'force'\n            ValueError - other errors (repre not found, misconfiguration)\n        \"\"\"\n        representation = get_representation_by_id(project_name,\n                                                  representation_id)\n        if not representation:\n            raise ValueError(\"Representation {} not found in {}\".\n                             format(representation_id, project_name))\n\n        if side and site_name:\n            raise ValueError(\"Misconfiguration, only one of side and \" +\n                             \"site_name arguments should be passed.\")\n\n        local_site = self.get_active_site(project_name)\n        remote_site = self.get_remote_site(project_name)\n\n        if side:\n            if side == 'local':\n                site_name = local_site\n            else:\n                site_name = remote_site\n\n        elem = {\"name\": site_name}\n\n        # Add priority\n        if priority:\n            elem[\"priority\"] = priority\n\n        if file_id:  # reset site for particular file\n            self._reset_site_for_file(project_name, representation_id,\n                                      elem, file_id, site_name)\n        elif side:  # reset site for whole representation\n            self._reset_site(project_name, representation_id, elem, site_name)\n        elif remove:  # remove site for whole representation\n            self._remove_site(project_name,\n                              representation, site_name)\n        elif pause is not None:\n            self._pause_unpause_site(project_name,\n                                     representation, site_name, pause)\n        else:  # add new site to all files for representation\n            self._add_site(project_name, representation, elem, site_name,\n                           force=force)\n\n    def _update_site(self, project_name, representation_id,\n                     update, arr_filter):\n        \"\"\"\n            Auxiliary method to call update_one function on DB\n\n            Used for refactoring ugly reset_provider_for_file\n        \"\"\"\n        query = {\n            \"_id\": ObjectId(representation_id)\n        }\n\n        self.connection.database[project_name].update_one(\n            query,\n            update,\n            upsert=True,\n            array_filters=arr_filter\n        )\n\n    def _reset_site_for_file(self, project_name, representation_id,\n                             elem, file_id, site_name):\n        \"\"\"\n            Resets 'site_name' for 'file_id' on representation in 'query' on\n            'project_name'\n        \"\"\"\n        update = {\n            \"$set\": {\"files.$[f].sites.$[s]\": elem}\n        }\n        if not isinstance(file_id, ObjectId):\n            file_id = ObjectId(file_id)\n\n        arr_filter = [\n            {'s.name': site_name},\n            {'f._id': file_id}\n        ]\n\n        self._update_site(project_name, representation_id, update, arr_filter)\n\n    def _reset_site(self, project_name, representation_id, elem, site_name):\n        \"\"\"\n            Resets 'site_name' for all files of representation in 'query'\n        \"\"\"\n        update = {\n            \"$set\": {\"files.$[].sites.$[s]\": elem}\n        }\n\n        arr_filter = [\n            {'s.name': site_name}\n        ]\n\n        self._update_site(project_name, representation_id, update, arr_filter)\n\n    def _remove_site(self, project_name, representation, site_name):\n        \"\"\"\n            Removes 'site_name' for 'representation' in 'query'\n\n            Throws ValueError if 'site_name' not found on 'representation'\n        \"\"\"\n        found = False\n        for repre_file in representation.get(\"files\"):\n            for site in repre_file.get(\"sites\"):\n                if site.get(\"name\") == site_name:\n                    found = True\n                    break\n        if not found:\n            msg = \"Site {} not found\".format(site_name)\n            self.log.info(msg)\n            raise ValueError(msg)\n\n        update = {\n            \"$pull\": {\"files.$[].sites\": {\"name\": site_name}}\n        }\n        arr_filter = []\n\n        self._update_site(project_name, representation[\"_id\"],\n                          update, arr_filter)\n\n    def _pause_unpause_site(self, project_name, representation,\n                            site_name, pause):\n        \"\"\"\n            Pauses/unpauses all files for 'representation' based on 'pause'\n\n            Throws ValueError if 'site_name' not found on 'representation'\n        \"\"\"\n        found = False\n        site = None\n        for repre_file in representation.get(\"files\"):\n            for site in repre_file.get(\"sites\"):\n                if site[\"name\"] == site_name:\n                    found = True\n                    break\n        if not found:\n            msg = \"Site {} not found\".format(site_name)\n            self.log.info(msg)\n            raise ValueError(msg)\n\n        if pause:\n            site['paused'] = pause\n        else:\n            if site.get('paused'):\n                site.pop('paused')\n\n        update = {\n            \"$set\": {\"files.$[].sites.$[s]\": site}\n        }\n\n        arr_filter = [\n            {'s.name': site_name}\n        ]\n\n        self._update_site(project_name, representation[\"_id\"],\n                          update, arr_filter)\n\n    def _add_site(self, project_name, representation, elem, site_name,\n                  force=False, file_id=None):\n        \"\"\"\n            Adds 'site_name' to 'representation' on 'project_name'\n\n            Args:\n                representation (dict)\n                file_id (ObjectId)\n\n            Use 'force' to remove existing or raises ValueError\n        \"\"\"\n        representation_id = representation[\"_id\"]\n        reset_existing = False\n        files = representation.get(\"files\", [])\n        if not files:\n            self.log.debug(\"No files for {}\".format(representation_id))\n            return\n\n        for repre_file in files:\n            if file_id and file_id != repre_file[\"_id\"]:\n                continue\n\n            for site in repre_file.get(\"sites\"):\n                if site[\"name\"] == site_name:\n                    if force or site.get(\"error\"):\n                        self._reset_site_for_file(project_name,\n                                                  representation_id,\n                                                  elem, repre_file[\"_id\"],\n                                                  site_name)\n                        reset_existing = True\n                    else:\n                        msg = \"Site {} already present\".format(site_name)\n                        self.log.info(msg)\n                        raise SiteAlreadyPresentError(msg)\n\n        if reset_existing:\n            return\n\n        if not file_id:\n            update = {\n                \"$push\": {\"files.$[].sites\": elem}\n            }\n\n            arr_filter = []\n        else:\n            update = {\n                \"$push\": {\"files.$[f].sites\": elem}\n            }\n            arr_filter = [\n                {'f._id': file_id}\n            ]\n\n        self._update_site(project_name, representation_id,\n                          update, arr_filter)\n\n    def _remove_local_file(self, project_name, representation_id, site_name):\n        \"\"\"\n            Removes all local files for 'site_name' of 'representation_id'\n\n            Args:\n                project_name (string): project name (must match DB)\n                representation_id (string): MongoDB _id value\n                site_name (string): name of configured and active site\n\n            Returns:\n                only logs, catches IndexError and OSError\n        \"\"\"\n        my_local_site = get_local_site_id()\n        if my_local_site != site_name:\n            self.log.warning(\"Cannot remove non local file for {}\".\n                             format(site_name))\n            return\n\n        provider_name = self.get_provider_for_site(site=site_name)\n\n        if provider_name == 'local_drive':\n            representation = get_representation_by_id(project_name,\n                                                      representation_id,\n                                                      fields=[\"files\"])\n            if not representation:\n                self.log.debug(\"No repre {} found\".format(\n                    representation_id))\n                return\n\n            local_file_path = ''\n            for file in representation.get(\"files\"):\n                local_file_path = self.get_local_file_path(project_name,\n                                                           site_name,\n                                                           file.get(\"path\", \"\")\n                                                           )\n                try:\n                    self.log.debug(\"Removing {}\".format(local_file_path))\n                    os.remove(local_file_path)\n                except IndexError:\n                    msg = \"No file set for {}\".format(representation_id)\n                    self.log.debug(msg)\n                    raise ValueError(msg)\n                except OSError:\n                    msg = \"File {} cannot be removed\".format(file[\"path\"])\n                    self.log.warning(msg)\n                    raise ValueError(msg)\n\n            folder = None\n            try:\n                folder = os.path.dirname(local_file_path)\n                os.rmdir(folder)\n            except OSError:\n                msg = \"folder {} cannot be removed\".format(folder)\n                self.log.warning(msg)\n                raise ValueError(msg)\n\n    def get_loop_delay(self, project_name):\n        \"\"\"\n            Return count of seconds before next synchronization loop starts\n            after finish of previous loop.\n        Returns:\n            (int): in seconds\n        \"\"\"\n        if not project_name:\n            return 60\n\n        ld = self.sync_project_settings[project_name][\"config\"][\"loop_delay\"]\n        return int(ld)\n\n    def show_widget(self):\n        \"\"\"Show dialog for Sync Queue\"\"\"\n        no_errors = False\n        try:\n            from .tray.app import SyncServerWindow\n            self.widget = SyncServerWindow(self)\n            no_errors = True\n        except ValueError:\n            self.log.info(\n                \"No system setting for sync. Not syncing.\", exc_info=True\n            )\n        except KeyError:\n            self.log.info((\n                \"There are not set presets for SyncServer OR \"\n                \"Credentials provided are invalid, \"\n                \"no syncing possible\").\n                format(str(self.sync_project_settings)), exc_info=True)\n        except:\n            self.log.error(\n                \"Uncaught exception durin start of SyncServer\",\n                exc_info=True)\n        self.enabled = no_errors\n        self.widget.show()\n\n    def _get_success_dict(self, new_file_id):\n        \"\"\"\n            Provide success metadata (\"id\", \"created_dt\") to be stored in Db.\n            Used in $set: \"DICT\" part of query.\n            Sites are array inside of array(file), so real indexes for both\n            file and site are needed for upgrade in DB.\n        Args:\n            new_file_id: id of created file\n        Returns:\n            (dictionary)\n        \"\"\"\n        val = {\"files.$[f].sites.$[s].id\": new_file_id,\n               \"files.$[f].sites.$[s].created_dt\": datetime.now()}\n        return val\n\n    def _get_error_dict(self, error=\"\", tries=\"\", progress=\"\"):\n        \"\"\"\n            Provide error metadata to be stored in Db.\n            Used for set (error and tries provided) or unset mode.\n        Args:\n            error: (string) - message\n            tries: how many times failed\n        Returns:\n            (dictionary)\n        \"\"\"\n        val = {\"files.$[f].sites.$[s].last_failed_dt\": datetime.now(),\n               \"files.$[f].sites.$[s].error\": error,\n               \"files.$[f].sites.$[s].tries\": tries,\n               \"files.$[f].sites.$[s].progress\": progress\n               }\n        return val\n\n    def _get_tries_count_from_rec(self, rec):\n        \"\"\"\n            Get number of failed attempts to sync from site record\n        Args:\n            rec (dictionary): info about specific site record\n        Returns:\n            (int) - number of failed attempts\n        \"\"\"\n        if not rec:\n            return 0\n        return rec.get(\"tries\", 0)\n\n    def _get_tries_count(self, file, provider):\n        \"\"\"\n            Get number of failed attempts to sync\n        Args:\n            file (dictionary): info about specific file\n            provider (string): name of site ('gdrive' or specific user site)\n        Returns:\n            (int) - number of failed attempts\n        \"\"\"\n        _, rec = self._get_site_rec(file.get(\"sites\", []), provider)\n        return self._get_tries_count_from_rec(rec)\n\n    def _get_progress_dict(self, progress):\n        \"\"\"\n            Provide progress metadata to be stored in Db.\n            Used during upload/download for GUI to show.\n        Args:\n            progress: (float) - 0-1 progress of upload/download\n        Returns:\n            (dictionary)\n        \"\"\"\n        val = {\"files.$[f].sites.$[s].progress\": progress}\n        return val\n\n    def _get_priority_dict(self, priority, file_id):\n        \"\"\"\n            Provide priority metadata to be stored in Db.\n            Used during upload/download for GUI to show.\n        Args:\n            priority: (int) - priority for file(s)\n        Returns:\n            (dictionary)\n        \"\"\"\n        if file_id:\n            str_key = \"files.$[f].sites.$[s].priority\"\n        else:\n            str_key = \"files.$[].sites.$[s].priority\"\n        return {str_key: int(priority)}\n\n    def _get_retries_arr(self, project_name):\n        \"\"\"\n            Returns array with allowed values in 'tries' field. If repre\n            contains these values, it means it was tried to be synchronized\n            but failed. We try up to 'self.presets[\"retry_cnt\"]' times before\n            giving up and skipping representation.\n        Returns:\n            (list)\n        \"\"\"\n        retry_cnt = self.sync_project_settings[project_name].\\\n            get(\"config\")[\"retry_cnt\"]\n        arr = [i for i in range(int(retry_cnt))]\n        arr.append(None)\n\n        return arr\n\n    def _get_roots_config(self, presets, project_name, site_name):\n        \"\"\"\n            Returns configured root(s) for 'project_name' and 'site_name' from\n            settings ('presets')\n        \"\"\"\n        return presets[project_name]['sites'][site_name]['root']\n\n    def cli(self, click_group):\n        click_group.add_command(cli_main.to_click_obj())\n\n    # Webserver module implementation\n    def webserver_initialization(self, server_manager):\n        \"\"\"Add routes for syncs.\"\"\"\n        if self.tray_initialized:\n            from .rest_api import SyncServerModuleRestApi\n            self.rest_api_obj = SyncServerModuleRestApi(\n                self, server_manager\n            )\n\n\n@click_wrap.group(\n    SyncServerModule.name,\n    help=\"SyncServer module related commands.\")\ndef cli_main():\n    pass\n\n\n@cli_main.command()\n@click_wrap.option(\n    \"-a\",\n    \"--active_site\",\n    required=True,\n    help=\"Name of active stie\")\ndef syncservice(active_site):\n    \"\"\"Launch sync server under entered site.\n\n    This should be ideally used by system service (such us systemd or upstart\n    on linux and window service).\n    \"\"\"\n\n    from openpype.modules import ModulesManager\n\n    os.environ[\"OPENPYPE_LOCAL_ID\"] = active_site\n\n    def signal_handler(sig, frame):\n        print(\"You pressed Ctrl+C. Process ended.\")\n        sync_server_module.server_exit()\n        sys.exit(0)\n\n    signal.signal(signal.SIGINT, signal_handler)\n    signal.signal(signal.SIGTERM, signal_handler)\n\n    manager = ModulesManager()\n    sync_server_module = manager.modules_by_name[\"sync_server\"]\n\n    sync_server_module.server_init()\n    sync_server_module.server_start()\n\n    while True:\n        time.sleep(1.0)\n"
  },
  {
    "path": "openpype/modules/sync_server/tray/app.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.tools.settings import style\n\nfrom openpype import resources\n\nfrom .widgets import (\n    SyncProjectListWidget,\n    SyncRepresentationSummaryWidget\n)\n\n\nclass SyncServerWindow(QtWidgets.QDialog):\n    \"\"\"\n        Main window that contains list of synchronizable projects and summary\n        view with all synchronizable representations for first project\n    \"\"\"\n\n    def __init__(self, sync_server, parent=None):\n        super(SyncServerWindow, self).__init__(parent)\n        self.sync_server = sync_server\n        self.setWindowFlags(QtCore.Qt.Window)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        self.setStyleSheet(style.load_stylesheet())\n        self.setWindowIcon(QtGui.QIcon(resources.get_openpype_icon_filepath()))\n        self.resize(1450, 700)\n\n        self.timer = QtCore.QTimer()\n        self.timer.timeout.connect(self._hide_message)\n\n        body = QtWidgets.QWidget(self)\n        footer = QtWidgets.QWidget(self)\n        footer.setFixedHeight(20)\n\n        left_column = QtWidgets.QWidget(body)\n        left_column_layout = QtWidgets.QVBoxLayout(left_column)\n\n        self.projects = SyncProjectListWidget(sync_server, self)\n        self.projects.refresh()  # force selection of default\n        left_column_layout.addWidget(self.projects)\n        self.pause_btn = QtWidgets.QPushButton(\"Pause server\")\n\n        left_column_layout.addWidget(self.pause_btn)\n\n        checkbox = QtWidgets.QCheckBox(\"Show only enabled\", self)\n        checkbox.setStyleSheet(\"QCheckBox{spacing: 5px;\"\n                               \"padding:5px 5px 5px 5px;}\")\n        checkbox.setChecked(True)\n        self.show_only_enabled_chk = checkbox\n\n        left_column_layout.addWidget(self.show_only_enabled_chk)\n\n        repres = SyncRepresentationSummaryWidget(\n            sync_server,\n            project=self.projects.current_project,\n            parent=self)\n        container = QtWidgets.QWidget()\n        container_layout = QtWidgets.QHBoxLayout(container)\n        container_layout.setContentsMargins(0, 0, 0, 0)\n        split = QtWidgets.QSplitter()\n        split.addWidget(left_column)\n        split.addWidget(repres)\n        split.setSizes([180, 950, 200])\n        container_layout.addWidget(split)\n\n        body_layout = QtWidgets.QHBoxLayout(body)\n        body_layout.addWidget(container)\n        body_layout.setContentsMargins(0, 0, 0, 0)\n\n        self.message = QtWidgets.QLabel(footer)\n        self.message.hide()\n\n        footer_layout = QtWidgets.QVBoxLayout(footer)\n        footer_layout.addWidget(self.message)\n        footer_layout.setContentsMargins(20, 0, 0, 0)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(body)\n        layout.addWidget(footer)\n\n        self.setWindowTitle(\"Sync Queue\")\n\n        self.projects.project_changed.connect(\n            self._on_project_change\n        )\n\n        self.pause_btn.clicked.connect(self._pause)\n        self.pause_btn.setAutoDefault(False)\n        self.pause_btn.setDefault(False)\n        repres.message_generated.connect(self._update_message)\n        self.projects.message_generated.connect(self._update_message)\n\n        self.show_only_enabled_chk.stateChanged.connect(\n            self._on_enabled_change\n        )\n\n        self.representationWidget = repres\n\n    def showEvent(self, event):\n        self.representationWidget.set_project(self.projects.current_project)\n        self.projects.refresh()\n        self._set_running(True)\n        super().showEvent(event)\n\n    def closeEvent(self, event):\n        self._set_running(False)\n        super().closeEvent(event)\n\n    def _on_project_change(self):\n        if self.projects.current_project is None:\n            return\n\n        self.representationWidget.set_project(self.projects.current_project)\n\n        project_name = self.projects.current_project\n        if not self.sync_server.get_sync_project_setting(project_name):\n            self.projects.message_generated.emit(\n                \"Project {} not active anymore\".format(project_name))\n            self.projects.refresh()\n            return\n\n    def _on_enabled_change(self):\n        \"\"\"Called when enabled projects only checkbox is toggled.\"\"\"\n        self.projects.show_only_enabled = \\\n            self.show_only_enabled_chk.isChecked()\n        self.projects.refresh()\n        self.representationWidget.set_project(None)\n\n    def _set_running(self, running):\n        self.representationWidget.model.is_running = running\n        self.representationWidget.model.timer.setInterval(0)\n\n    def _pause(self):\n        if self.sync_server.is_paused():\n            self.sync_server.unpause_server()\n            self.pause_btn.setText(\"Pause server\")\n        else:\n            self.sync_server.pause_server()\n            self.pause_btn.setText(\"Unpause server\")\n        self.projects.refresh()\n\n    def _update_message(self, value):\n        \"\"\"\n            Update and show message in the footer\n        \"\"\"\n        self.message.setText(value)\n        if self.message.isVisible():\n            self.message.repaint()\n        else:\n            self.message.show()\n        msec_delay = 3000\n        self.timer.start(msec_delay)\n\n    def _hide_message(self):\n        \"\"\"\n            Hide message in footer\n\n            Called automatically by self.timer after a while\n        \"\"\"\n        self.message.setText(\"\")\n        self.message.hide()\n"
  },
  {
    "path": "openpype/modules/sync_server/tray/delegates.py",
    "content": "import os\nfrom qtpy import QtCore, QtWidgets, QtGui\n\nfrom openpype.lib import Logger\n\nfrom openpype.tools.utils.constants import (\n    LOCAL_PROVIDER_ROLE,\n    REMOTE_PROVIDER_ROLE,\n    LOCAL_PROGRESS_ROLE,\n    REMOTE_PROGRESS_ROLE,\n    LOCAL_DATE_ROLE,\n    REMOTE_DATE_ROLE,\n    LOCAL_FAILED_ROLE,\n    REMOTE_FAILED_ROLE,\n    EDIT_ICON_ROLE\n)\n\nlog = Logger.get_logger(\"SyncServer\")\n\n\nclass PriorityDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Creates editable line edit to set priority on representation\"\"\"\n    def paint(self, painter, option, index):\n        super(PriorityDelegate, self).paint(painter, option, index)\n\n        if option.widget.selectionModel().isSelected(index) or \\\n                option.state & QtWidgets.QStyle.State_MouseOver:\n            edit_icon = index.data(EDIT_ICON_ROLE)\n            if not edit_icon:\n                return\n\n            state = QtGui.QIcon.On\n            mode = QtGui.QIcon.Selected\n\n            icon_side = 16\n            icon_rect = QtCore.QRect(\n                option.rect.left() + option.rect.width() - icon_side - 4,\n                option.rect.top() + ((option.rect.height() - icon_side) / 2),\n                icon_side,\n                icon_side\n            )\n\n            edit_icon.paint(\n                painter, icon_rect,\n                QtCore.Qt.AlignRight, mode, state\n            )\n\n    def createEditor(self, parent, option, index):\n        editor = PriorityLineEdit(\n            parent,\n            option.widget.selectionModel().selectedRows())\n        editor.setFocus()\n        return editor\n\n    def setModelData(self, editor, model, index):\n        for index in editor.selected_idxs:\n            try:\n                val = int(editor.text())\n            except ValueError:\n                val = model.sync_server.DEFAULT_PRIORITY\n            model.set_priority_data(index, val)\n\n\nclass PriorityLineEdit(QtWidgets.QLineEdit):\n    \"\"\"Special LineEdit to consume Enter and store selected indexes\"\"\"\n    def __init__(self, parent=None, selected_idxs=None):\n        self.selected_idxs = selected_idxs\n        super(PriorityLineEdit, self).__init__(parent)\n\n    def keyPressEvent(self, event):\n        result = super(PriorityLineEdit, self).keyPressEvent(event)\n        if (\n            event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter)\n        ):\n            return event.accept()\n\n        return result\n\n\nclass ImageDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"\n        Prints icon of site and progress of synchronization\n    \"\"\"\n\n    def __init__(self, parent=None, side=None):\n        super(ImageDelegate, self).__init__(parent)\n        self.icons = {}\n        self.side = side\n\n    def paint(self, painter, option, index):\n        super(ImageDelegate, self).paint(painter, option, index)\n        option = QtWidgets.QStyleOptionViewItem(option)\n        option.showDecorationSelected = True\n\n        if not self.side:\n            log.warning(\"No side provided, delegate won't work\")\n            return\n\n        if self.side == 'local':\n            provider = index.data(LOCAL_PROVIDER_ROLE)\n            value = index.data(LOCAL_PROGRESS_ROLE)\n            date_value = index.data(LOCAL_DATE_ROLE)\n            is_failed = index.data(LOCAL_FAILED_ROLE)\n        else:\n            provider = index.data(REMOTE_PROVIDER_ROLE)\n            value = index.data(REMOTE_PROGRESS_ROLE)\n            date_value = index.data(REMOTE_DATE_ROLE)\n            is_failed = index.data(REMOTE_FAILED_ROLE)\n\n        if not self.icons.get(provider):\n            resource_path = os.path.dirname(__file__)\n            resource_path = os.path.join(resource_path, \"..\",\n                                         \"providers\", \"resources\")\n            pix_url = \"{}/{}.png\".format(resource_path, provider)\n            pixmap = QtGui.QPixmap(pix_url)\n            self.icons[provider] = pixmap\n        else:\n            pixmap = self.icons[provider]\n\n        padding = 10\n        point = QtCore.QPoint(option.rect.x() + padding,\n                              option.rect.y() +\n                              (option.rect.height() - pixmap.height()) / 2)\n        painter.drawPixmap(point, pixmap)\n\n        overlay_rect = option.rect.translated(0, 0)\n        overlay_rect.setHeight(overlay_rect.height() * (1.0 - float(value)))\n        painter.fillRect(overlay_rect,\n                         QtGui.QBrush(QtGui.QColor(0, 0, 0, 100)))\n        text_rect = option.rect.translated(10, 0)\n        painter.drawText(text_rect,\n                         QtCore.Qt.AlignCenter,\n                         date_value)\n\n        if is_failed:\n            overlay_rect = option.rect.translated(0, 0)\n            painter.fillRect(overlay_rect,\n                             QtGui.QBrush(QtGui.QColor(255, 0, 0, 35)))\n"
  },
  {
    "path": "openpype/modules/sync_server/tray/lib.py",
    "content": "import attr\nimport abc\nimport six\n\nSTATUS = {\n    0: 'In Progress',\n    1: 'Queued',\n    2: 'Failed',\n    3: 'Paused',\n    4: 'Synced OK',\n    -1: 'Not available'\n}\n\nDUMMY_PROJECT = \"No project configured\"\n\n\n@six.add_metaclass(abc.ABCMeta)\nclass AbstractColumnFilter:\n\n    def __init__(self, column_name, dbcon=None):\n        self.column_name = column_name\n        self.dbcon = dbcon\n        self._search_variants = []\n\n    def search_variants(self):\n        \"\"\"\n            Returns all flavors of search available for this column,\n        \"\"\"\n        return self._search_variants\n\n    @abc.abstractmethod\n    def values(self):\n        \"\"\"\n            Returns dict of available values for filter {'label':'value'}\n        \"\"\"\n        pass\n\n    @abc.abstractmethod\n    def prepare_match_part(self, values):\n        \"\"\"\n            Prepares format valid for $match part from 'values\n\n            Args:\n                values (dict): {'label': 'value'}\n            Returns:\n                (dict): {'COLUMN_NAME': {'$in': ['val1', 'val2']}}\n        \"\"\"\n        pass\n\n\nclass PredefinedSetFilter(AbstractColumnFilter):\n\n    def __init__(self, column_name, values):\n        super().__init__(column_name)\n        self._search_variants = ['checkbox']\n        self._values = values\n        if self._values and \\\n                list(self._values.keys())[0] == list(self._values.values())[0]:\n            self._search_variants.append('text')\n\n    def values(self):\n        return {k: v for k, v in self._values.items()}\n\n    def prepare_match_part(self, values):\n        return {'$in': list(values.keys())}\n\n\nclass RegexTextFilter(AbstractColumnFilter):\n\n    def __init__(self, column_name):\n        super().__init__(column_name)\n        self._search_variants = ['text']\n\n    def values(self):\n        return {}\n\n    def prepare_match_part(self, values):\n        \"\"\" values = {'text1 text2': 'text1 text2'} \"\"\"\n        if not values:\n            return {}\n\n        regex_strs = set()\n        text = list(values.keys())[0]  # only single key always expected\n        for word in text.split():\n            regex_strs.add('.*{}.*'.format(word))\n\n        return {\"$regex\": \"|\".join(regex_strs),\n                \"$options\": 'i'}\n\n\nclass MultiSelectFilter(AbstractColumnFilter):\n\n    def __init__(self, column_name, values=None, dbcon=None):\n        super().__init__(column_name)\n        self._values = values\n        self.dbcon = dbcon\n        self._search_variants = ['checkbox']\n\n    def values(self):\n        if self._values:\n            return {k: v for k, v in self._values.items()}\n\n        recs = self.dbcon.find({'type': self.column_name}, {\"name\": 1,\n                                                            \"_id\": -1})\n        values = {}\n        for item in recs:\n            values[item[\"name\"]] = item[\"name\"]\n        return dict(sorted(values.items(), key=lambda it: it[1]))\n\n    def prepare_match_part(self, values):\n        return {'$in': list(values.keys())}\n\n\n@attr.s\nclass FilterDefinition:\n    type = attr.ib()\n    values = attr.ib(factory=list)\n\n\ndef pretty_size(value, suffix='B'):\n    for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:\n        if abs(value) < 1024.0:\n            return \"%3.1f%s%s\" % (value, unit, suffix)\n        value /= 1024.0\n    return \"%.1f%s%s\" % (value, 'Yi', suffix)\n\n\ndef convert_progress(value):\n    try:\n        progress = float(value)\n    except (ValueError, TypeError):\n        progress = 0.0\n\n    return progress\n\n\ndef translate_provider_for_icon(sync_server, project, site):\n    \"\"\"\n        Get provider for 'site'\n\n        This is used for getting icon, 'studio' should have different icon\n        then local sites, even the provider 'local_drive' is same\n\n    \"\"\"\n    if site == sync_server.DEFAULT_SITE:\n        return sync_server.DEFAULT_SITE\n    return sync_server.get_provider_for_site(site=site)\n\n\ndef get_value_from_id_by_role(model, object_id, role):\n    \"\"\"Return value from item with 'object_id' with 'role'.\"\"\"\n    index = model.get_index(object_id)\n    return model.data(index, role)\n"
  },
  {
    "path": "openpype/modules/sync_server/tray/models.py",
    "content": "import os\nimport attr\nfrom bson.objectid import ObjectId\nimport datetime\n\nfrom qtpy import QtCore\nimport qtawesome\n\nfrom openpype.tools.utils.delegates import pretty_timestamp\n\nfrom openpype.lib import Logger, get_local_site_id\nfrom openpype.client import get_representation_by_id\n\nfrom . import lib\n\nfrom openpype.tools.utils.constants import (\n    LOCAL_PROVIDER_ROLE,\n    REMOTE_PROVIDER_ROLE,\n    LOCAL_PROGRESS_ROLE,\n    REMOTE_PROGRESS_ROLE,\n    HEADER_NAME_ROLE,\n    EDIT_ICON_ROLE,\n    LOCAL_DATE_ROLE,\n    REMOTE_DATE_ROLE,\n    LOCAL_FAILED_ROLE,\n    REMOTE_FAILED_ROLE,\n    STATUS_ROLE,\n    PATH_ROLE,\n    ERROR_ROLE,\n    TRIES_ROLE\n)\n\n\nlog = Logger.get_logger(\"SyncServer\")\n\n\nclass _SyncRepresentationModel(QtCore.QAbstractTableModel):\n\n    COLUMN_LABELS = []\n\n    PAGE_SIZE = 20  # default page size to query for\n    REFRESH_SEC = 5000  # in seconds, requery DB for new status\n\n    refresh_started = QtCore.Signal()\n    refresh_finished = QtCore.Signal()\n\n    @property\n    def dbcon(self):\n        \"\"\"\n            Database object with preselected project (collection) to run DB\n            operations (find, aggregate).\n\n            All queries should go through this (because of collection).\n        \"\"\"\n        if self.project:\n            return self.sync_server.connection.database[self.project]\n\n    @property\n    def project(self):\n        \"\"\"Returns project\"\"\"\n        return self._project\n\n    @property\n    def column_filtering(self):\n        return self._column_filtering\n\n    @property\n    def is_running(self):\n        return self._is_running\n\n    @is_running.setter\n    def is_running(self, state):\n        self._is_running = state\n\n    def rowCount(self, _index):\n        return len(self._data)\n\n    def columnCount(self, _index=None):\n        return len(self._header)\n\n    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):\n        if section >= len(self.COLUMN_LABELS):\n            return\n\n        if role == QtCore.Qt.DisplayRole:\n            if orientation == QtCore.Qt.Horizontal:\n                return self.COLUMN_LABELS[section][1]\n\n        if role == HEADER_NAME_ROLE:\n            if orientation == QtCore.Qt.Horizontal:\n                return self.COLUMN_LABELS[section][0]  # return name\n\n    def data(self, index, role):\n        item = self._data[index.row()]\n\n        header_value = self._header[index.column()]\n        if role == LOCAL_PROVIDER_ROLE:\n            return item.local_provider\n\n        if role == REMOTE_PROVIDER_ROLE:\n            return item.remote_provider\n\n        if role == LOCAL_PROGRESS_ROLE:\n            return item.local_progress\n\n        if role == REMOTE_PROGRESS_ROLE:\n            return item.remote_progress\n\n        if role == LOCAL_DATE_ROLE:\n            if item.created_dt:\n                return pretty_timestamp(item.created_dt)\n\n        if role == REMOTE_DATE_ROLE:\n            if item.sync_dt:\n                return pretty_timestamp(item.sync_dt)\n\n        if role == LOCAL_FAILED_ROLE:\n            return item.status == lib.STATUS[2] and \\\n                item.local_progress < 1\n\n        if role == REMOTE_FAILED_ROLE:\n            return item.status == lib.STATUS[2] and \\\n                item.remote_progress < 1\n\n        if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):\n            # because of ImageDelegate\n            if header_value in ['remote_site', 'local_site']:\n                return \"\"\n\n            return attr.asdict(item)[self._header[index.column()]]\n\n        if role == EDIT_ICON_ROLE:\n            if self.can_edit and header_value in self.EDITABLE_COLUMNS:\n                return self.edit_icon\n\n        if role == PATH_ROLE:\n            return item.path\n\n        if role == ERROR_ROLE:\n            return item.error\n\n        if role == TRIES_ROLE:\n            return item.tries\n\n        if role == STATUS_ROLE:\n            return item.status\n\n        if role == QtCore.Qt.UserRole:\n            return item._id\n\n    @property\n    def can_edit(self):\n        \"\"\"Returns true if some site is user local site, eg. could edit\"\"\"\n        if not self.project:\n            return False\n\n        return get_local_site_id() in (self.active_site, self.remote_site)\n\n    def get_column(self, index):\n        \"\"\"\n            Returns info about column\n\n            Args:\n                index (QModelIndex)\n            Returns:\n                (tuple): (COLUMN_NAME: COLUMN_LABEL)\n        \"\"\"\n        return self.COLUMN_LABELS[index]\n\n    def get_header_index(self, value):\n        \"\"\"\n            Returns index of 'value' in headers\n\n            Args:\n                value (str): header name value\n            Returns:\n                (int)\n        \"\"\"\n        return self._header.index(value)\n\n    def refresh(self, representations=None, load_records=0):\n        \"\"\"\n            Reloads representations from DB if necessary, adds them to model.\n\n            Runs periodically (every X seconds) or by demand (change of\n            sorting, filtering etc.)\n\n            Emits 'modelReset' signal.\n\n            Args:\n                representations (PaginationResult object): pass result of\n                    aggregate query from outside - mostly for testing only\n                load_records (int) - enforces how many records should be\n                    actually queried (scrolled a couple of times to list more\n                    than single page of records)\n        \"\"\"\n        if self.is_editing or not self.is_running or not self.project:\n            return\n        self.refresh_started.emit()\n        self.beginResetModel()\n        self._data = []\n        self._rec_loaded = 0\n\n        if not representations:\n            self.query = self.get_query(load_records)\n            representations = self.dbcon.aggregate(pipeline=self.query,\n                                                   allowDiskUse=True)\n\n        self.add_page_records(self.active_site, self.remote_site,\n                              representations)\n        self.endResetModel()\n        self.refresh_finished.emit()\n\n    def tick(self):\n        \"\"\"\n            Triggers refresh of model.\n\n            Because of pagination, prepared (sorting, filtering) query needs\n            to be run on DB every X seconds.\n        \"\"\"\n        self.refresh(representations=None, load_records=self._rec_loaded)\n        self.timer.start(self.REFRESH_SEC)\n\n    def canFetchMore(self, _index):\n        \"\"\"\n            Check if there are more records than currently loaded\n        \"\"\"\n        # 'skip' might be suboptimal when representation hits 500k+\n        return self._total_records > self._rec_loaded\n\n    def fetchMore(self, index):\n        \"\"\"\n            Add more record to model.\n\n            Called when 'canFetchMore' returns true, which means there are\n            more records in DB than loaded.\n        \"\"\"\n        log.debug(\"fetchMore\")\n        if not self.dbcon:\n            return\n\n        items_to_fetch = min(self._total_records - self._rec_loaded,\n                             self.PAGE_SIZE)\n        self.query = self.get_query(self._rec_loaded)\n        representations = self.dbcon.aggregate(pipeline=self.query,\n                                               allowDiskUse=True)\n        self.beginInsertRows(index,\n                             self._rec_loaded,\n                             self._rec_loaded + items_to_fetch - 1)\n\n        self.add_page_records(self.active_site, self.remote_site,\n                              representations)\n\n        self.endInsertRows()\n\n    def sort(self, index, order):\n        \"\"\"\n            Summary sort per representation.\n\n            Sort is happening on a DB side, model is reset, db queried\n            again.\n\n            It remembers one last sort, adds it as secondary after new sort.\n\n            Args:\n                index (int): column index\n                order (int): 0|\n        \"\"\"\n        # limit unwanted first re-sorting by view\n        if index < 0:\n            return\n\n        self._rec_loaded = 0\n        if order == 0:\n            order = 1\n        else:\n            order = -1\n\n        backup_sort = dict(self.sort_criteria)\n\n        self.sort_criteria = {self.SORT_BY_COLUMN[index]: order}  # reset\n        # add last one\n        for key, val in backup_sort.items():\n            if key != '_id' and key != self.SORT_BY_COLUMN[index]:\n                self.sort_criteria[key] = val\n                break\n        # add default one\n        self.sort_criteria['_id'] = 1\n\n        self.query = self.get_query()\n        # import json\n        # log.debug(json.dumps(self.query, indent=4).\\\n        #           replace('False', 'false').\\\n        #           replace('True', 'true').replace('None', 'null'))\n\n        if self.dbcon:\n            representations = self.dbcon.aggregate(pipeline=self.query,\n                                                   allowDiskUse=True)\n            self.refresh(representations)\n\n    def set_word_filter(self, word_filter):\n        \"\"\"\n            Adds text value filtering\n\n            Args:\n                word_filter (str): string inputted by user\n        \"\"\"\n        self._word_filter = word_filter\n        self.refresh()\n\n    def get_filters(self):\n        \"\"\"\n            Returns all available filter editors per column_name keys.\n        \"\"\"\n        filters = {}\n        for column_name, _ in self.COLUMN_LABELS:\n            filter_rec = self.COLUMN_FILTERS.get(column_name)\n            if filter_rec:\n                filter_rec.dbcon = self.dbcon\n            filters[column_name] = filter_rec\n\n        return filters\n\n    def get_column_filter(self, index):\n        \"\"\"\n            Returns filter object for column 'index\n\n            Args:\n                index(int): index of column in header\n\n            Returns:\n                (AbstractColumnFilter)\n        \"\"\"\n        column_name = self._header[index]\n\n        filter_rec = self.COLUMN_FILTERS.get(column_name)\n        if filter_rec:\n            filter_rec.dbcon = self.dbcon  # up-to-date db connection\n\n        return filter_rec\n\n    def set_column_filtering(self, checked_values):\n        \"\"\"\n            Sets dictionary used in '$match' part of MongoDB aggregate\n\n            Args:\n                checked_values(dict): key:values ({'status':{1:\"Foo\",3:\"Bar\"}}\n\n            Modifies:\n                self._column_filtering : {'status': {'$in': [1, 2, 3]}}\n        \"\"\"\n        filtering = {}\n        for column_name, dict_value in checked_values.items():\n            column_f = self.COLUMN_FILTERS.get(column_name)\n            if not column_f:\n                continue\n            column_f.dbcon = self.dbcon\n            filtering[column_name] = column_f.prepare_match_part(dict_value)\n\n        self._column_filtering = filtering\n\n    def get_column_filter_values(self, index):\n        \"\"\"\n            Returns list of available values for filtering in the column\n\n            Args:\n                index(int): index of column in header\n\n            Returns:\n                (dict) of value: label shown in filtering menu\n                'value' is used in MongoDB query, 'label' is human readable for\n                menu\n                for some columns ('subset') might be 'value' and 'label' same\n        \"\"\"\n        filter_rec = self.get_column_filter(index)\n        if not filter_rec:\n            return {}\n\n        return filter_rec.values()\n\n    def set_project(self, project):\n        \"\"\"\n            Changes project, called after project selection is changed\n\n            Args:\n                project (str): name of project\n        \"\"\"\n        self._project = project\n        # project might have been deactivated in the meantime\n        if not self.sync_server.get_sync_project_setting(project):\n            self._data = {}\n            return\n\n        self.active_site = self.sync_server.get_active_site(self.project)\n        self.remote_site = self.sync_server.get_remote_site(self.project)\n        self.refresh()\n\n    def get_index(self, id):\n        \"\"\"\n            Get index of 'id' value.\n\n            Used for keeping selection after refresh.\n\n            Args:\n                id (str): MongoDB _id\n            Returns:\n                (QModelIndex)\n        \"\"\"\n        for i in range(self.rowCount(None)):\n            index = self.index(i, 0)\n            value = self.data(index, QtCore.Qt.UserRole)\n            if value == id:\n                return index\n        return None\n\n    def _convert_date(self, date_value, current_date):\n        \"\"\"Converts 'date_value' to string.\n\n        Value of date_value might contain date in the future, used for nicely\n        sort queued items next to last downloaded.\n        \"\"\"\n        try:\n            converted_date = None\n            # ignore date in the future - for sorting only\n            if date_value and date_value < current_date:\n                converted_date = date_value.strftime(\"%Y%m%dT%H%M%SZ\")\n        except (AttributeError, TypeError):\n            # ignore unparseable values\n            pass\n\n        return converted_date\n\n\nclass SyncRepresentationSummaryModel(_SyncRepresentationModel):\n    \"\"\"\n        Model for summary of representations.\n\n        Groups files information per representation. Allows sorting and\n        full text filtering.\n\n        Allows pagination, most of heavy lifting is being done on DB side.\n        Single model matches to single project. When project is changed,\n        model is reset and refreshed.\n\n        Args:\n            sync_server (SyncServer) - object to call server operations (update\n                db status, set site status...)\n            header (list) - names of visible columns\n            project (string) - collection name, all queries must be called on\n                a specific collection\n\n    \"\"\"\n    COLUMN_LABELS = [\n        (\"asset\", \"Asset\"),\n        (\"subset\", \"Subset\"),\n        (\"version\", \"Version\"),\n        (\"representation\", \"Representation\"),\n        (\"local_site\", \"Active site\"),\n        (\"remote_site\", \"Remote site\"),\n        (\"files_count\", \"Files\"),\n        (\"files_size\", \"Size\"),\n        (\"priority\", \"Priority\"),\n        (\"status\", \"Status\")\n    ]\n\n    DEFAULT_SORT = {\n        \"updated_dt_remote\": -1,\n        \"_id\": 1\n    }\n    SORT_BY_COLUMN = [\n        \"asset\",  # asset\n        \"subset\",  # subset\n        \"version\",  # version\n        \"representation\",  # representation\n        \"updated_dt_local\",  # local created_dt\n        \"updated_dt_remote\",  # remote created_dt\n        \"files_count\",  # count of files\n        \"files_size\",  # file size of all files\n        \"priority\",  # priority\n        \"status\"  # status\n    ]\n\n    COLUMN_FILTERS = {\n        'status': lib.PredefinedSetFilter('status', lib.STATUS),\n        'subset': lib.RegexTextFilter('subset'),\n        'asset': lib.RegexTextFilter('asset'),\n        'representation': lib.MultiSelectFilter('representation')\n    }\n\n    EDITABLE_COLUMNS = [\"priority\"]\n\n    refresh_started = QtCore.Signal()\n    refresh_finished = QtCore.Signal()\n\n    @attr.s\n    class SyncRepresentation:\n        \"\"\"\n            Auxiliary object for easier handling.\n\n            Fields must contain all header values (+ any arbitrary values).\n        \"\"\"\n        _id = attr.ib()\n        asset = attr.ib()\n        subset = attr.ib()\n        version = attr.ib()\n        representation = attr.ib()\n        created_dt = attr.ib(default=None)\n        sync_dt = attr.ib(default=None)\n        local_site = attr.ib(default=None)\n        remote_site = attr.ib(default=None)\n        local_provider = attr.ib(default=None)\n        remote_provider = attr.ib(default=None)\n        local_progress = attr.ib(default=None)\n        remote_progress = attr.ib(default=None)\n        files_count = attr.ib(default=None)\n        files_size = attr.ib(default=None)\n        priority = attr.ib(default=None)\n        status = attr.ib(default=None)\n        path = attr.ib(default=None)\n\n    def __init__(self, sync_server, header, project=None, parent=None):\n        super(SyncRepresentationSummaryModel, self).__init__(parent=parent)\n        self._header = header\n        self._data = []\n        self._project = project\n        self._rec_loaded = 0\n        self._total_records = 0  # how many documents query actually found\n        self._word_filter = None\n        self._column_filtering = {}\n        self._is_running = False\n\n        self.edit_icon = qtawesome.icon(\"fa.edit\", color=\"white\")\n        self.is_editing = False\n\n        self._word_filter = None\n\n        self.sync_server = sync_server\n        # TODO think about admin mode\n        self.sort_criteria = self.DEFAULT_SORT\n\n        self.timer = QtCore.QTimer()\n        if not self._project or self._project == lib.DUMMY_PROJECT:\n            self.active_site = sync_server.DEFAULT_SITE\n            self.remote_site = sync_server.DEFAULT_SITE\n            return\n\n        # this is for regular user, always only single local and single remote\n        self.active_site = self.sync_server.get_active_site(self.project)\n        self.remote_site = self.sync_server.get_remote_site(self.project)\n\n        self.query = self.get_query()\n        self.default_query = list(self.get_query())\n\n        self.timer.timeout.connect(self.tick)\n        self.timer.start(self.REFRESH_SEC)\n\n    def add_page_records(self, local_site, remote_site, representations):\n        \"\"\"\n            Process all records from 'representation' and add them to storage.\n\n            Args:\n                local_site (str): name of local site (mine)\n                remote_site (str): name of cloud provider (theirs)\n                representations (Mongo Cursor) - mimics result set, 1 object\n                    with paginatedResults array and totalCount array\n        \"\"\"\n        result = representations.next()\n        count = 0\n        total_count = result.get(\"totalCount\")\n        if total_count:\n            count = total_count.pop().get('count')\n        self._total_records = count\n\n        local_provider = lib.translate_provider_for_icon(self.sync_server,\n                                                         self.project,\n                                                         local_site)\n        remote_provider = lib.translate_provider_for_icon(self.sync_server,\n                                                          self.project,\n                                                          remote_site)\n        current_date = datetime.datetime.now()\n        for repre in result.get(\"paginatedResults\"):\n            files = repre.get(\"files\", [])\n            if isinstance(files, dict):  # aggregate returns dictionary\n                files = [files]\n\n            # representation without files doesnt concern us\n            if not files:\n                continue\n\n            local_updated = self._convert_date(repre.get('updated_dt_local'),\n                                               current_date)\n            remote_updated = self._convert_date(repre.get('updated_dt_remote'),\n                                                current_date)\n\n            avg_progress_remote = lib.convert_progress(\n                repre.get('avg_progress_remote', '0'))\n            avg_progress_local = lib.convert_progress(\n                repre.get('avg_progress_local', '0'))\n\n            if repre.get(\"version\"):\n                version = \"v{:0>3d}\".format(repre.get(\"version\"))\n            else:\n                version = \"master\"\n\n            item = self.SyncRepresentation(\n                repre.get(\"_id\"),\n                repre.get(\"asset\"),\n                repre.get(\"subset\"),\n                version,\n                repre.get(\"representation\"),\n                local_updated,\n                remote_updated,\n                local_site,\n                remote_site,\n                local_provider,\n                remote_provider,\n                avg_progress_local,\n                avg_progress_remote,\n                repre.get(\"files_count\", 1),\n                lib.pretty_size(repre.get(\"files_size\", 0)),\n                repre.get(\"priority\"),\n                lib.STATUS[repre.get(\"status\", -1)],\n                files[0].get('path')\n            )\n\n            self._data.append(item)\n            self._rec_loaded += 1\n\n    def get_query(self, limit=0):\n        \"\"\"\n            Returns basic aggregate query for main table.\n\n            Main table provides summary information about representation,\n            which could have multiple files. Details are accessible after\n            double click on representation row.\n            Columns:\n                'created_dt' - max of created or updated (when failed) per repr\n                'sync_dt' - same for remote side\n                'local_site' - progress of repr on local side, 1 = finished\n                'remote_site' - progress on remote side, calculates from files\n                'status' -\n                    0 - in progress\n                    1 - failed\n                    2 - queued\n                    3 - paused\n                    4 - finished on both sides\n\n                are calculated and must be calculated in DB because of\n                pagination\n\n            Args:\n                limit (int): how many records should be returned, by default\n                    it 'PAGE_SIZE' for performance.\n                    Should be overridden by value of loaded records for refresh\n                    functionality (got more records by scrolling, refresh\n                    shouldn't reset that)\n        \"\"\"\n        if limit == 0:\n            limit = SyncRepresentationSummaryModel.PAGE_SIZE\n\n        # replace null with value in the future for better sorting\n        dummy_max_date = datetime.datetime(2099, 1, 1)\n        aggr = [\n            {\"$match\": self.get_match_part()},\n            {'$unwind': '$files'},\n            # merge potentially unwinded records back to single per repre\n            {'$addFields': {\n                'order_remote': {\n                    '$filter': {'input': '$files.sites', 'as': 'p',\n                                'cond': {'$eq': ['$$p.name', self.remote_site]}\n                                }},\n                'order_local': {\n                    '$filter': {'input': '$files.sites', 'as': 'p',\n                                'cond': {'$eq': ['$$p.name', self.active_site]}\n                                }}\n            }},\n            {'$addFields': {\n                # prepare progress per file, presence of 'created_dt' denotes\n                # successfully finished load/download\n                'progress_remote': {'$first': {\n                    '$cond': [{'$size': \"$order_remote.progress\"},\n                              \"$order_remote.progress\",\n                              {'$cond': [\n                                  {'$size': \"$order_remote.created_dt\"},\n                                  [1],\n                                  [0]\n                              ]}\n                              ]}},\n                'progress_local': {'$first': {\n                    '$cond': [{'$size': \"$order_local.progress\"},\n                              \"$order_local.progress\",\n                              {'$cond': [\n                                  {'$size': \"$order_local.created_dt\"},\n                                  [1],\n                                  [0]\n                              ]}\n                              ]}},\n                # file might be successfully created or failed, not both\n                'updated_dt_remote': {'$first': {\n                    '$cond': [{'$size': \"$order_remote.created_dt\"},\n                              \"$order_remote.created_dt\",\n                              {'$cond': [\n                                  {'$size': \"$order_remote.last_failed_dt\"},\n                                  \"$order_remote.last_failed_dt\",\n                                  [dummy_max_date]\n                              ]}\n                              ]}},\n                'updated_dt_local': {'$first': {\n                    '$cond': [{'$size': \"$order_local.created_dt\"},\n                              \"$order_local.created_dt\",\n                              {'$cond': [\n                                  {'$size': \"$order_local.last_failed_dt\"},\n                                  \"$order_local.last_failed_dt\",\n                                  [dummy_max_date]\n                              ]}\n                              ]}},\n                'files_size': {'$ifNull': [\"$files.size\", 0]},\n                'failed_remote': {\n                    '$cond': [{'$size': \"$order_remote.last_failed_dt\"},\n                              1,\n                              0]},\n                'failed_local': {\n                    '$cond': [{'$size': \"$order_local.last_failed_dt\"},\n                              1,\n                              0]},\n                'failed_local_tries': {\n                    '$cond': [{'$size': '$order_local.tries'},\n                              {'$first': '$order_local.tries'},\n                              0]},\n                'failed_remote_tries': {\n                    '$cond': [{'$size': '$order_remote.tries'},\n                              {'$first': '$order_remote.tries'},\n                              0]},\n                'paused_remote': {\n                    '$cond': [{'$size': \"$order_remote.paused\"},\n                              1,\n                              0]},\n                'paused_local': {\n                    '$cond': [{'$size': \"$order_local.paused\"},\n                              1,\n                              0]},\n                'priority': {\n                    '$cond': [\n                        {'$size': '$order_local.priority'},\n                        {'$first': '$order_local.priority'},\n                        {'$cond': [\n                            {'$size': '$order_remote.priority'},\n                            {'$first': '$order_remote.priority'},\n                            self.sync_server.DEFAULT_PRIORITY]}\n                    ]\n                },\n            }},\n            {'$group': {\n                '_id': '$_id',\n                # pass through context - same for representation\n                'context': {'$addToSet': '$context'},\n                'data': {'$addToSet': '$data'},\n                # pass through files as a list\n                'files': {'$addToSet': '$files'},\n                # count how many files\n                'files_count': {'$sum': 1},\n                'files_size': {'$sum': '$files_size'},\n                # sum avg progress, finished = 1\n                'avg_progress_remote': {'$avg': \"$progress_remote\"},\n                'avg_progress_local': {'$avg': \"$progress_local\"},\n                # select last touch of file\n                'updated_dt_remote': {'$max': \"$updated_dt_remote\"},\n                'failed_remote': {'$sum': '$failed_remote'},\n                'failed_local': {'$sum': '$failed_local'},\n                'failed_remote_tries': {'$sum': '$failed_remote_tries'},\n                'failed_local_tries': {'$sum': '$failed_local_tries'},\n                'paused_remote': {'$sum': '$paused_remote'},\n                'paused_local': {'$sum': '$paused_local'},\n                'updated_dt_local': {'$max': \"$updated_dt_local\"},\n                'priority': {'$max': \"$priority\"},\n            }},\n            {\"$project\": self.projection}\n        ]\n\n        if self.column_filtering:\n            aggr.append(\n                {\"$match\": self.column_filtering}\n            )\n\n        aggr.extend(\n            [{\"$sort\": self.sort_criteria},\n             {\n                '$facet': {\n                    'paginatedResults': [{'$skip': self._rec_loaded},\n                                         {'$limit': limit}],\n                    'totalCount': [{'$count': 'count'}]\n                }\n             }]\n        )\n\n        return aggr\n\n    def get_match_part(self):\n        \"\"\"\n            Extend match part with word_filter if present.\n\n            Filter is set by user input. Each model has different fields to be\n            checked.\n            If performance issues are found, '$text' and text indexes should\n            be investigated.\n\n            Fulltext searches in:\n                context.subset\n                context.asset\n                context.representation  names AND _id (ObjectId)\n        \"\"\"\n        base_match = {\n            \"type\": \"representation\",\n            'files.sites.name': {'$all': [self.active_site,\n                                          self.remote_site]}\n        }\n        if not self._word_filter:\n            return base_match\n        else:\n            regex_str = '.*{}.*'.format(self._word_filter)\n            base_match['$or'] = [\n                {'context.subset': {'$regex': regex_str, '$options': 'i'}},\n                {'context.asset': {'$regex': regex_str, '$options': 'i'}},\n                {'context.representation': {'$regex': regex_str,\n                                            '$options': 'i'}}]\n\n            if ObjectId.is_valid(self._word_filter):\n                base_match['$or'] = [{'_id': ObjectId(self._word_filter)}]\n\n            return base_match\n\n    @property\n    def projection(self):\n        \"\"\"\n            Projection part for aggregate query.\n\n            All fields with '1' will be returned, no others.\n\n            Returns:\n                (dict)\n        \"\"\"\n        return {\n            \"subset\": {\"$first\": \"$context.subset\"},\n            \"asset\": {\"$first\": \"$context.asset\"},\n            \"version\": {\"$first\": \"$context.version\"},\n            \"representation\": {\"$first\": \"$context.representation\"},\n            \"data.path\": 1,\n            \"files\": 1,\n            'files_count': 1,\n            \"files_size\": 1,\n            'avg_progress_remote': 1,\n            'avg_progress_local': 1,\n            'updated_dt_remote': 1,\n            'updated_dt_local': 1,\n            'paused_remote': 1,\n            'paused_local': 1,\n            'priority': 1,\n            'status': {\n                '$switch': {\n                    'branches': [\n                        {\n                            'case': {\n                                '$or': ['$paused_remote', '$paused_local']},\n                            'then': 3  # Paused\n                        },\n                        {\n                            'case': {\n                                '$or': [\n                                    {'$gte': ['$failed_local_tries', 3]},\n                                    {'$gte': ['$failed_remote_tries', 3]}\n                                ]},\n                            'then': 2},  # Failed\n                        {\n                            'case': {\n                                '$or': [{'$eq': ['$avg_progress_remote', 0]},\n                                        {'$eq': ['$avg_progress_local', 0]}]},\n                            'then': 1  # Queued\n                        },\n                        {\n                            'case': {'$or': [{'$and': [\n                                {'$gt': ['$avg_progress_remote', 0]},\n                                {'$lt': ['$avg_progress_remote', 1]}\n                            ]},\n                                {'$and': [\n                                    {'$gt': ['$avg_progress_local', 0]},\n                                    {'$lt': ['$avg_progress_local', 1]}\n                                ]}\n                            ]},\n                            'then': 0  # In progress\n                        },\n                        {\n                            'case': {'$and': [\n                                {'$eq': ['$avg_progress_remote', 1]},\n                                {'$eq': ['$avg_progress_local', 1]}\n                            ]},\n                            'then': 4  # Synced OK\n                        },\n                    ],\n                    'default': -1\n                }\n            }\n        }\n\n    def set_priority_data(self, index, value):\n        \"\"\"\n            Sets 'priority' flag and value on local site for selected reprs.\n\n            Args:\n                index (QItemIndex): selected index from View\n                value (int): priority value\n\n            Updates DB.\n            Potentially should allow set priority to any site when user\n            management is implemented.\n        \"\"\"\n        if not self.can_edit:\n            return\n\n        repre_id = self.data(index, QtCore.Qt.UserRole)\n\n        representation = get_representation_by_id(self.project, repre_id)\n        if representation:\n            self.sync_server.update_db(self.project, None, None,\n                                       representation,\n                                       get_local_site_id(),\n                                       priority=value)\n        self.is_editing = False\n\n        # all other approaches messed up selection to 0th index\n        self.timer.setInterval(0)\n\n\nclass SyncRepresentationDetailModel(_SyncRepresentationModel):\n    \"\"\"\n        List of all syncronizable files per single representation.\n\n        Used in detail window accessible after clicking on single repre in the\n        summary.\n\n        Args:\n            sync_server (SyncServer) - object to call server operations (update\n                db status, set site status...)\n            header (list) - names of visible columns\n            _id (string) - MongoDB _id of representation\n            project (string) - collection name, all queries must be called on\n                a specific collection\n    \"\"\"\n    COLUMN_LABELS = [\n        (\"file\", \"File name\"),\n        (\"local_site\", \"Active site\"),\n        (\"remote_site\", \"Remote site\"),\n        (\"files_size\", \"Size\"),\n        (\"priority\", \"Priority\"),\n        (\"status\", \"Status\")\n    ]\n\n    PAGE_SIZE = 30\n    DEFAULT_SORT = {\n        \"files.path\": 1\n    }\n    SORT_BY_COLUMN = [\n        \"files.path\",\n        \"updated_dt_local\",  # local created_dt\n        \"updated_dt_remote\",  # remote created_dt\n        \"size\",  # remote progress\n        \"priority\",  # priority\n        \"status\"  # status\n    ]\n\n    COLUMN_FILTERS = {\n        'status': lib.PredefinedSetFilter('status', lib.STATUS),\n        'file': lib.RegexTextFilter('file'),\n    }\n\n    EDITABLE_COLUMNS = [\"priority\"]\n\n    @attr.s\n    class SyncRepresentationDetail:\n        \"\"\"\n            Auxiliary object for easier handling.\n\n            Fields must contain all header values (+ any arbitrary values).\n        \"\"\"\n        _id = attr.ib()\n        file = attr.ib()\n        created_dt = attr.ib(default=None)\n        sync_dt = attr.ib(default=None)\n        local_site = attr.ib(default=None)\n        remote_site = attr.ib(default=None)\n        local_provider = attr.ib(default=None)\n        remote_provider = attr.ib(default=None)\n        local_progress = attr.ib(default=None)\n        remote_progress = attr.ib(default=None)\n        size = attr.ib(default=None)\n        priority = attr.ib(default=None)\n        status = attr.ib(default=None)\n        tries = attr.ib(default=None)\n        error = attr.ib(default=None)\n        path = attr.ib(default=None)\n\n    def __init__(self, sync_server, header, _id,\n                 project=None):\n        super(SyncRepresentationDetailModel, self).__init__()\n        self._header = header\n        self._data = []\n        self._project = project\n        self._rec_loaded = 0\n        self._total_records = 0  # how many documents query actually found\n        self._word_filter = None\n        self._id = _id\n        self._column_filtering = {}\n        self._is_running = False\n\n        self.is_editing = False\n        self.edit_icon = qtawesome.icon(\"fa.edit\", color=\"white\")\n\n        self.sync_server = sync_server\n        # TODO think about admin mode\n        # this is for regular user, always only single local and single remote\n        self.active_site = self.sync_server.get_active_site(self.project)\n        self.remote_site = self.sync_server.get_remote_site(self.project)\n\n        self.sort_criteria = self.DEFAULT_SORT\n\n        self.query = self.get_query()\n\n        self.timer = QtCore.QTimer()\n        self.timer.timeout.connect(self.tick)\n        self.timer.start(SyncRepresentationSummaryModel.REFRESH_SEC)\n\n    def add_page_records(self, local_site, remote_site, representations):\n        \"\"\"\n            Process all records from 'representation' and add them to storage.\n\n            Args:\n                local_site (str): name of local site (mine)\n                remote_site (str): name of cloud provider (theirs)\n                representations (Mongo Cursor) - mimics result set, 1 object\n                    with paginatedResults array and totalCount array\n        \"\"\"\n        # representations is a Cursor, get first\n        result = representations.next()\n        count = 0\n        total_count = result.get(\"totalCount\")\n        if total_count:\n            count = total_count.pop().get('count')\n        self._total_records = count\n\n        local_provider = lib.translate_provider_for_icon(self.sync_server,\n                                                         self.project,\n                                                         local_site)\n        remote_provider = lib.translate_provider_for_icon(self.sync_server,\n                                                          self.project,\n                                                          remote_site)\n\n        current_date = datetime.datetime.now()\n        for repre in result.get(\"paginatedResults\"):\n            # log.info(\"!!! repre:: {}\".format(repre))\n            files = repre.get(\"files\", [])\n            if isinstance(files, dict):  # aggregate returns dictionary\n                files = [files]\n\n            for file in files:\n                local_updated = self._convert_date(\n                    repre.get('updated_dt_local'),\n                    current_date)\n                remote_updated = self._convert_date(\n                    repre.get('updated_dt_remote'),\n                    current_date)\n\n                remote_progress = lib.convert_progress(\n                    repre.get('progress_remote', '0'))\n                local_progress = lib.convert_progress(\n                    repre.get('progress_local', '0'))\n\n                errors = []\n                if repre.get('failed_remote_error'):\n                    errors.append(repre.get('failed_remote_error'))\n                if repre.get('failed_local_error'):\n                    errors.append(repre.get('failed_local_error'))\n\n                item = self.SyncRepresentationDetail(\n                    file.get(\"_id\"),\n                    os.path.basename(file[\"path\"]),\n                    local_updated,\n                    remote_updated,\n                    local_site,\n                    remote_site,\n                    local_provider,\n                    remote_provider,\n                    local_progress,\n                    remote_progress,\n                    lib.pretty_size(file.get('size', 0)),\n                    repre.get(\"priority\"),\n                    lib.STATUS[repre.get(\"status\", -1)],\n                    repre.get(\"tries\"),\n                    '\\n'.join(errors),\n                    file.get('path')\n\n                )\n                self._data.append(item)\n                self._rec_loaded += 1\n\n    def get_query(self, limit=0):\n        \"\"\"\n            Gets query that gets used when no extra sorting, filtering or\n            projecting is needed.\n\n            Called for basic table view.\n\n            Returns:\n                [(dict)] - list with single dict - appropriate for aggregate\n                    function for MongoDB\n        \"\"\"\n        if limit == 0:\n            limit = SyncRepresentationSummaryModel.PAGE_SIZE\n\n        dummy_max_date = datetime.datetime(2099, 1, 1)\n        aggr = [\n            {\"$match\": self.get_match_part()},\n            {\"$unwind\": \"$files\"},\n            {'$addFields': {\n                'order_remote': {\n                    '$filter': {'input': '$files.sites', 'as': 'p',\n                                'cond': {'$eq': ['$$p.name', self.remote_site]}\n                                }},\n                'order_local': {\n                    '$filter': {'input': '$files.sites', 'as': 'p',\n                                'cond': {'$eq': ['$$p.name', self.active_site]}\n                                }}\n            }},\n            {'$addFields': {\n                # prepare progress per file, presence of 'created_dt' denotes\n                # successfully finished load/download\n                'progress_remote': {'$first': {\n                    '$cond': [{'$size': \"$order_remote.progress\"},\n                              \"$order_remote.progress\",\n                              {'$cond': [\n                                  {'$size': \"$order_remote.created_dt\"},\n                                  [1],\n                                  [0]\n                              ]}\n                              ]}},\n                'progress_local': {'$first': {\n                    '$cond': [{'$size': \"$order_local.progress\"},\n                              \"$order_local.progress\",\n                              {'$cond': [\n                                  {'$size': \"$order_local.created_dt\"},\n                                  [1],\n                                  [0]\n                              ]}\n                              ]}},\n                # file might be successfully created or failed, not both\n                'updated_dt_remote': {'$first': {\n                    '$cond': [\n                        {'$size': \"$order_remote.created_dt\"},\n                        \"$order_remote.created_dt\",\n                        {\n                            '$cond': [\n                                {'$size': \"$order_remote.last_failed_dt\"},\n                                \"$order_remote.last_failed_dt\",\n                                [dummy_max_date]\n                            ]\n                        }\n                    ]\n                }},\n                'updated_dt_local': {'$first': {\n                    '$cond': [\n                        {'$size': \"$order_local.created_dt\"},\n                        \"$order_local.created_dt\",\n                        {\n                            '$cond': [\n                                {'$size': \"$order_local.last_failed_dt\"},\n                                \"$order_local.last_failed_dt\",\n                                [dummy_max_date]\n                            ]\n                        }\n                    ]\n                }},\n                'paused_remote': {\n                    '$cond': [{'$size': \"$order_remote.paused\"},\n                              1,\n                              0]},\n                'paused_local': {\n                    '$cond': [{'$size': \"$order_local.paused\"},\n                              1,\n                              0]},\n                'failed_remote': {\n                    '$cond': [{'$size': \"$order_remote.last_failed_dt\"},\n                              1,\n                              0]},\n                'failed_local': {\n                    '$cond': [{'$size': \"$order_local.last_failed_dt\"},\n                              1,\n                              0]},\n                'failed_remote_error': {'$first': {\n                    '$cond': [{'$size': \"$order_remote.error\"},\n                              \"$order_remote.error\",\n                              [\"\"]]}},\n                'failed_local_error': {'$first': {\n                    '$cond': [{'$size': \"$order_local.error\"},\n                              \"$order_local.error\",\n                              [\"\"]]}},\n                'tries': {'$first': {\n                    '$cond': [\n                        {'$size': \"$order_local.tries\"},\n                        \"$order_local.tries\",\n                        {'$cond': [\n                            {'$size': \"$order_remote.tries\"},\n                            \"$order_remote.tries\",\n                            []\n                        ]}\n                    ]}},\n                'priority': {\n                    '$cond': [\n                        {'$size': '$order_local.priority'},\n                        {'$first': '$order_local.priority'},\n                        {'$cond': [\n                            {'$size': '$order_remote.priority'},\n                            {'$first': '$order_remote.priority'},\n                            self.sync_server.DEFAULT_PRIORITY]}\n                    ]\n                },\n            }},\n            {\"$project\": self.projection}\n        ]\n\n        if self.column_filtering:\n            aggr.append(\n                {\"$match\": self.column_filtering}\n            )\n            print(self.column_filtering)\n\n        aggr.extend([\n            {\"$sort\": self.sort_criteria},\n            {\n                '$facet': {\n                    'paginatedResults': [{'$skip': self._rec_loaded},\n                                         {'$limit': limit}],\n                    'totalCount': [{'$count': 'count'}]\n                }\n            }\n        ])\n\n        return aggr\n\n    def get_match_part(self):\n        \"\"\"\n            Returns different content for 'match' portion if filtering by\n            name is present\n\n            Returns:\n                (dict)\n        \"\"\"\n        if not self._word_filter:\n            return {\n                \"type\": \"representation\",\n                \"_id\": self._id\n            }\n        else:\n            regex_str = '.*{}.*'.format(self._word_filter)\n            return {\n                \"type\": \"representation\",\n                \"_id\": self._id,\n                '$or': [{'files.path': {'$regex': regex_str, '$options': 'i'}}]\n            }\n\n    @property\n    def projection(self):\n        \"\"\"\n            Projection part for aggregate query.\n\n            All fields with '1' will be returned, no others.\n\n            Returns:\n                (dict)\n        \"\"\"\n        return {\n            \"files\": 1,\n            'progress_remote': 1,\n            'progress_local': 1,\n            'updated_dt_remote': 1,\n            'updated_dt_local': 1,\n            'paused_remote': 1,\n            'paused_local': 1,\n            'failed_remote_error': 1,\n            'failed_local_error': 1,\n            'tries': 1,\n            'priority': 1,\n            'status': {\n                '$switch': {\n                    'branches': [\n                        {\n                            'case': {\n                                '$or': ['$paused_remote', '$paused_local']},\n                            'then': 3  # Paused\n                        },\n                        {\n                            'case': {\n                                '$and': [{'$or': ['$failed_remote',\n                                                  '$failed_local']},\n                                         {'$eq': ['$tries', 3]}]},\n                            'then': 2  # Failed (3 tries)\n                        },\n                        {\n                            'case': {\n                                '$or': [{'$eq': ['$progress_remote', 0]},\n                                        {'$eq': ['$progress_local', 0]}]},\n                            'then': 1  # Queued\n                        },\n                        {\n                            'case': {\n                                '$or': ['$failed_remote', '$failed_local']},\n                            'then': 2  # Failed\n                        },\n                        {\n                            'case': {'$or': [{'$and': [\n                                {'$gt': ['$progress_remote', 0]},\n                                {'$lt': ['$progress_remote', 1]}\n                            ]},\n                                {'$and': [\n                                    {'$gt': ['$progress_local', 0]},\n                                    {'$lt': ['$progress_local', 1]}\n                                ]}\n                            ]},\n                            'then': 0  # In Progress\n                        },\n                        {\n                            'case': {'$and': [\n                                {'$eq': ['$progress_remote', 1]},\n                                {'$eq': ['$progress_local', 1]}\n                            ]},\n                            'then': 4  # Synced OK\n                        },\n                    ],\n                    'default': -1\n                }\n            },\n            'data.path': 1\n        }\n\n    def set_priority_data(self, index, value):\n        \"\"\"\n            Sets 'priority' flag and value on local site for selected reprs.\n\n            Args:\n                index (QItemIndex): selected index from View\n                value (int): priority value\n\n            Updates DB\n        \"\"\"\n        if not self.can_edit:\n            return\n\n        file_id = self.data(index, QtCore.Qt.UserRole)\n\n        updated_file = None\n        representation = get_representation_by_id(self.project, self._id)\n        if not representation:\n            return\n\n        for repre_file in representation[\"files\"]:\n            if repre_file[\"_id\"] == file_id:\n                updated_file = repre_file\n                break\n\n        if representation and updated_file:\n            self.sync_server.update_db(self.project, None, updated_file,\n                                       representation, get_local_site_id(),\n                                       priority=value)\n        self.is_editing = False\n        # all other approaches messed up selection to 0th index\n        self.timer.setInterval(0)\n"
  },
  {
    "path": "openpype/modules/sync_server/tray/widgets.py",
    "content": "import os\nimport subprocess\nimport sys\nfrom functools import partial\n\nfrom qtpy import QtWidgets, QtCore, QtGui\nimport qtawesome\n\nfrom openpype.tools.settings import style\n\nfrom openpype.lib import Logger, get_local_site_id\n\nfrom openpype.tools.utils.delegates import pretty_timestamp\n\nfrom .models import (\n    SyncRepresentationSummaryModel,\n    SyncRepresentationDetailModel\n)\n\nfrom . import lib\nfrom . import delegates\n\nfrom openpype.tools.utils.constants import (\n    LOCAL_PROGRESS_ROLE,\n    REMOTE_PROGRESS_ROLE,\n    HEADER_NAME_ROLE,\n    STATUS_ROLE,\n    PATH_ROLE,\n    LOCAL_SITE_NAME_ROLE,\n    REMOTE_SITE_NAME_ROLE,\n    LOCAL_DATE_ROLE,\n    REMOTE_DATE_ROLE,\n    ERROR_ROLE,\n    TRIES_ROLE\n)\n\nlog = Logger.get_logger(\"SyncServer\")\n\n\nclass SyncProjectListWidget(QtWidgets.QWidget):\n    \"\"\"\n        Lists all projects that are synchronized to choose from\n    \"\"\"\n    project_changed = QtCore.Signal()\n    message_generated = QtCore.Signal(str)\n\n    refresh_msec = 10000\n    show_only_enabled = True\n\n    def __init__(self, sync_server, parent):\n        super(SyncProjectListWidget, self).__init__(parent)\n        self.setObjectName(\"ProjectListWidget\")\n\n        self._parent = parent\n\n        label_widget = QtWidgets.QLabel(\"Projects\", self)\n        project_list = QtWidgets.QListView(self)\n        project_model = QtGui.QStandardItemModel()\n        project_list.setModel(project_model)\n        project_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n\n        # Do not allow editing\n        project_list.setEditTriggers(\n            QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers\n        )\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(3)\n        layout.addWidget(label_widget, 0)\n        layout.addWidget(project_list, 1)\n\n        project_list.customContextMenuRequested.connect(self._on_context_menu)\n        project_list.selectionModel().selectionChanged.connect(\n            self._on_selection_changed\n        )\n\n        self.project_model = project_model\n        self.project_list = project_list\n        self.sync_server = sync_server\n        self.current_project = None\n        self.project_name = None\n        self.local_site = None\n        self.remote_site = None\n        self.icons = {}\n\n        self._selection_changed = False\n        self._model_reset = False\n\n        timer = QtCore.QTimer()\n        timer.setInterval(self.refresh_msec)\n        timer.timeout.connect(self.refresh)\n        timer.start()\n\n        self.timer = timer\n\n    def _on_selection_changed(self, new_selection, _old_selection):\n        # block involuntary selection changes\n        if self._selection_changed or self._model_reset:\n            return\n\n        indexes = new_selection.indexes()\n        if not indexes:\n            return\n\n        project_name = indexes[0].data(QtCore.Qt.DisplayRole)\n\n        if self.current_project == project_name:\n            return\n        self._selection_changed = True\n        self.current_project = project_name\n        self.project_changed.emit()\n        self.refresh()\n        self._selection_changed = False\n\n    def refresh(self):\n        selected_index = None\n        model = self.project_model\n        self._model_reset = True\n        model.clear()\n        self._model_reset = False\n\n        selected_item = None\n        sync_settings = self.sync_server.sync_project_settings\n        for project_name in sync_settings.keys():\n            if self.sync_server.is_paused() or \\\n               self.sync_server.is_project_paused(project_name):\n                icon = self._get_icon(\"paused\")\n            elif not sync_settings[project_name][\"enabled\"]:\n                if self.show_only_enabled:\n                    continue\n                icon = self._get_icon(\"disabled\")\n            else:\n                icon = self._get_icon(\"synced\")\n\n            if project_name in self.sync_server.projects_processed:\n                icon = self._get_icon(\"refresh\")\n\n            item = QtGui.QStandardItem(icon, project_name)\n            model.appendRow(item)\n\n            if self.current_project == project_name:\n                selected_item = item\n\n        if model.item(0) is None:\n            return\n\n        if selected_item:\n            selected_index = model.indexFromItem(selected_item)\n\n        if not self.current_project:\n            self.current_project = model.item(0).data(QtCore.Qt.DisplayRole)\n\n        self.project_model = model\n\n        if selected_index and \\\n           selected_index.isValid() and \\\n           not self._selection_changed:\n            mode = (\n                QtCore.QItemSelectionModel.Select\n                | QtCore.QItemSelectionModel.Rows\n            )\n            self.project_list.selectionModel().select(selected_index, mode)\n\n        if self.current_project:\n            self.local_site = self.sync_server.get_active_site(\n                self.current_project)\n            self.remote_site = self.sync_server.get_remote_site(\n                self.current_project)\n\n    def _can_edit(self):\n        \"\"\"Returns true if some site is user local site, eg. could edit\"\"\"\n        return get_local_site_id() in (self.local_site, self.remote_site)\n\n    def _get_icon(self, status):\n        if not self.icons.get(status):\n            resource_path = os.path.dirname(__file__)\n            resource_path = os.path.join(resource_path, \"..\",\n                                         \"resources\")\n            pix_url = \"{}/{}.png\".format(resource_path, status)\n            icon = QtGui.QIcon(pix_url)\n            self.icons[status] = icon\n        else:\n            icon = self.icons[status]\n        return icon\n\n    def _on_context_menu(self, point):\n        point_index = self.project_list.indexAt(point)\n        if not point_index.isValid():\n            return\n\n        self.project_name = point_index.data(QtCore.Qt.DisplayRole)\n\n        menu = QtWidgets.QMenu(self)\n        actions_mapping = {}\n\n        if self._can_edit():\n            if self.sync_server.is_project_paused(self.project_name):\n                action = QtWidgets.QAction(\"Unpause\")\n                actions_mapping[action] = self._unpause\n            else:\n                action = QtWidgets.QAction(\"Pause\")\n                actions_mapping[action] = self._pause\n            menu.addAction(action)\n\n        if self.local_site == get_local_site_id():\n            action = QtWidgets.QAction(\"Clear local project\")\n            actions_mapping[action] = self._clear_project\n            menu.addAction(action)\n\n        if self.project_name not in self.sync_server.projects_processed:\n            action = QtWidgets.QAction(\"Validate files on active site\")\n            actions_mapping[action] = self._validate_site\n            menu.addAction(action)\n\n        result = menu.exec_(QtGui.QCursor.pos())\n        if result:\n            to_run = actions_mapping[result]\n            if to_run:\n                to_run()\n\n    def _pause(self):\n        if self.project_name:\n            self.sync_server.pause_project(self.project_name)\n            self.project_name = None\n        self.refresh()\n\n    def _unpause(self):\n        if self.project_name:\n            self.sync_server.unpause_project(self.project_name)\n            self.project_name = None\n        self.refresh()\n\n    def _clear_project(self):\n        if self.project_name:\n            self.sync_server.clear_project(self.project_name, self.local_site)\n            self.project_name = None\n        self.refresh()\n\n    def _validate_site(self):\n        if self.project_name:\n            self.sync_server.create_validate_project_task(self.project_name,\n                                                          self.local_site)\n            self.project_name = None\n        self.refresh()\n\n\nclass _SyncRepresentationWidget(QtWidgets.QWidget):\n    \"\"\"\n        Summary dialog with list of representations that matches current\n        settings 'local_site' and 'remote_site'.\n    \"\"\"\n    active_changed = QtCore.Signal()  # active index changed\n    message_generated = QtCore.Signal(str)\n\n    def set_project(self, project):\n        self.model.set_project(project)\n\n    def _selection_changed(self, _new_selected, _all_selected):\n        idxs = self.selection_model.selectedRows()\n        self._selected_ids = set()\n\n        for index in idxs:\n            self._selected_ids.add(self.model.data(index, QtCore.Qt.UserRole))\n\n    def _set_selection(self):\n        \"\"\"\n            Sets selection to 'self._selected_id' if exists.\n\n            Keep selection during model refresh.\n        \"\"\"\n        existing_ids = set()\n        for selected_id in self._selected_ids:\n            index = self.model.get_index(selected_id)\n            if index and index.isValid():\n                mode = (\n                    QtCore.QItemSelectionModel.Select\n                    | QtCore.QItemSelectionModel.Rows\n                )\n                self.selection_model.select(index, mode)\n                existing_ids.add(selected_id)\n\n        self._selected_ids = existing_ids\n\n    def _double_clicked(self, index):\n        \"\"\"\n            Opens representation dialog with all files after doubleclick\n        \"\"\"\n        # priority editing\n        if self.model.can_edit:\n            column_name = self.model.get_column(index.column())\n            if column_name[0] in self.model.EDITABLE_COLUMNS:\n                self.model.is_editing = True\n                self.table_view.openPersistentEditor(index)\n                return\n\n        _id = self.model.data(index, QtCore.Qt.UserRole)\n        detail_window = SyncServerDetailWindow(\n            self.sync_server, _id, self.model.project, parent=self)\n        detail_window.exec()\n\n    def _on_context_menu(self, point):\n        \"\"\"\n            Shows menu with loader actions on Right-click.\n\n            Supports multiple selects - adds all available actions, each\n            action handles if it appropriate for item itself, if not it skips.\n        \"\"\"\n        is_multi = len(self._selected_ids) > 1\n        point_index = self.table_view.indexAt(point)\n        if not point_index.isValid() and not is_multi:\n            return\n\n        if is_multi:\n            index = self.model.get_index(list(self._selected_ids)[0])\n            local_progress = self.model.data(index, LOCAL_PROGRESS_ROLE)\n            remote_progress = self.model.data(index, REMOTE_PROGRESS_ROLE)\n            status = self.model.data(index, STATUS_ROLE)\n        else:\n            local_progress = self.model.data(point_index, LOCAL_PROGRESS_ROLE)\n            remote_progress = self.model.data(point_index,\n                                              REMOTE_PROGRESS_ROLE)\n            status = self.model.data(point_index, STATUS_ROLE)\n\n\n        can_edit = self.model.can_edit\n        action_kwarg_map, actions_mapping, menu = self._prepare_menu(\n            local_progress, remote_progress, is_multi, can_edit, status)\n\n        result = menu.exec_(QtGui.QCursor.pos())\n        if result:\n            to_run = actions_mapping[result]\n            to_run_kwargs = action_kwarg_map.get(result, {})\n            if to_run:\n                to_run(**to_run_kwargs)\n\n        self.model.refresh()\n\n    def _prepare_menu(self, local_progress, remote_progress,\n                      is_multi, can_edit, status=None):\n        menu = QtWidgets.QMenu(self)\n\n        actions_mapping = {}\n        action_kwarg_map = {}\n\n        active_site = self.model.active_site\n        remote_site = self.model.remote_site\n\n        for site, progress in {active_site: local_progress,\n                               remote_site: remote_progress}.items():\n            provider = self.sync_server.get_provider_for_site(site=site)\n            if provider == 'local_drive':\n                if 'studio' in site:\n                    txt = \" studio version\"\n                else:\n                    txt = \" local version\"\n                action = QtWidgets.QAction(\"Open in explorer\" + txt)\n                if progress == 1.0 or is_multi:\n                    actions_mapping[action] = self._open_in_explorer\n                    action_kwarg_map[action] = \\\n                        self._get_action_kwargs(site)\n                    menu.addAction(action)\n\n        if can_edit and (remote_progress == 1.0 or is_multi):\n            action = QtWidgets.QAction(\"Re-sync Active site\")\n            action_kwarg_map[action] = self._get_action_kwargs(active_site)\n            actions_mapping[action] = self._reset_site\n            menu.addAction(action)\n\n        if can_edit and (local_progress == 1.0 or is_multi):\n            action = QtWidgets.QAction(\"Re-sync Remote site\")\n            action_kwarg_map[action] = self._get_action_kwargs(remote_site)\n            actions_mapping[action] = self._reset_site\n            menu.addAction(action)\n\n        if can_edit and active_site == get_local_site_id():\n            action = QtWidgets.QAction(\"Completely remove from local\")\n            action_kwarg_map[action] = self._get_action_kwargs(active_site)\n            actions_mapping[action] = self._remove_site\n            menu.addAction(action)\n\n        if can_edit:\n            action = QtWidgets.QAction(\"Change priority\")\n            action_kwarg_map[action] = self._get_action_kwargs(active_site)\n            actions_mapping[action] = self._change_priority\n            menu.addAction(action)\n\n        if not actions_mapping:\n            action = QtWidgets.QAction(\"< No action >\")\n            actions_mapping[action] = None\n            menu.addAction(action)\n\n        return action_kwarg_map, actions_mapping, menu\n\n    def _pause(self, selected_ids=None):\n        log.debug(\"Pause {}\".format(selected_ids))\n        for representation_id in selected_ids:\n            status = lib.get_value_from_id_by_role(self.model,\n                                                   representation_id,\n                                                   STATUS_ROLE)\n            if status not in [lib.STATUS[0], lib.STATUS[1]]:\n                continue\n            for site_name in [self.model.active_site, self.model.remote_site]:\n                check_progress = self._get_progress(self.model,\n                                                    representation_id,\n                                                    site_name)\n                if check_progress < 1:\n                    self.sync_server.pause_representation(self.model.project,\n                                                          representation_id,\n                                                          site_name)\n\n            self.message_generated.emit(\"Paused {}\".format(representation_id))\n\n    def _unpause(self, selected_ids=None):\n        log.debug(\"UnPause {}\".format(selected_ids))\n        for representation_id in selected_ids:\n            status = lib.get_value_from_id_by_role(self.model,\n                                                   representation_id,\n                                                   STATUS_ROLE)\n            if status not in lib.STATUS[3]:\n                continue\n            for site_name in [self.model.active_site, self.model.remote_site]:\n                check_progress = self._get_progress(self.model,\n                                                    representation_id,\n                                                    site_name)\n                if check_progress < 1:\n                    self.sync_server.unpause_representation(\n                        self.model.project,\n                        representation_id,\n                        site_name)\n\n            self.message_generated.emit(\"Unpause {}\".format(representation_id))\n\n    # temporary here for testing, will be removed TODO\n    def _add_site(self, selected_ids=None, site_name=None):\n        log.debug(\"Add site {}:{}\".format(selected_ids, site_name))\n        for representation_id in selected_ids:\n            item_local_site = lib.get_value_from_id_by_role(\n                self.model, representation_id, LOCAL_SITE_NAME_ROLE)\n            item_remote_site = lib.get_value_from_id_by_role(\n                self.model, representation_id, REMOTE_SITE_NAME_ROLE)\n            if site_name in [item_local_site, item_remote_site]:\n                # site already exists skip\n                continue\n\n            try:\n                self.sync_server.add_site(\n                    self.model.project,\n                    representation_id,\n                    site_name)\n                self.message_generated.emit(\n                    \"Site {} added for {}\".format(site_name,\n                                                  representation_id))\n            except ValueError as exp:\n                self.message_generated.emit(\"Error {}\".format(str(exp)))\n        self.sync_server.reset_timer()\n\n    def _remove_site(self, selected_ids=None, site_name=None):\n        \"\"\"\n            Removes site record AND files.\n\n            This is ONLY for representations stored on local site, which\n            cannot be same as SyncServer.DEFAULT_SITE.\n\n            This could only happen when artist work on local machine, not\n            connected to studio mounted drives.\n        \"\"\"\n        log.debug(\"Remove site {}:{}\".format(selected_ids, site_name))\n        for representation_id in selected_ids:\n            log.info(\"Removing {}\".format(representation_id))\n            try:\n                self.sync_server.remove_site(\n                    self.model.project,\n                    representation_id,\n                    site_name,\n                    True)\n                self.message_generated.emit(\n                    \"Site {} removed\".format(site_name))\n            except ValueError as exp:\n                self.message_generated.emit(\"Error {}\".format(str(exp)))\n\n        self.model.refresh(\n            load_records=self.model._rec_loaded)\n        self.sync_server.reset_timer()\n\n    def _reset_site(self, selected_ids=None, site_name=None):\n        \"\"\"\n            Removes errors or success metadata for particular file >> forces\n            redo of upload/download\n        \"\"\"\n        log.debug(\"Reset site {}:{}\".format(selected_ids, site_name))\n        for representation_id in selected_ids:\n            check_progress = self._get_progress(self.model, representation_id,\n                                                site_name, True)\n\n            # do not reset if opposite side is not fully there\n            if check_progress != 1:\n                log.debug(\"Not fully available {} on other side, skipping\".\n                          format(check_progress))\n                continue\n\n            self.sync_server.reset_site_on_representation(\n                self.model.project,\n                representation_id,\n                site_name=site_name,\n                force=True)\n\n        self.model.refresh(\n            load_records=self.model._rec_loaded)\n        self.sync_server.reset_timer()\n\n    def _open_in_explorer(self, selected_ids=None, site_name=None):\n        log.debug(\"Open in Explorer {}:{}\".format(selected_ids, site_name))\n        for selected_id in selected_ids:\n            fpath = lib.get_value_from_id_by_role(self.model, selected_id,\n                                                  PATH_ROLE)\n            project = self.model.project\n            fpath = self.sync_server.get_local_file_path(project,\n                                                         site_name,\n                                                         fpath)\n\n            fpath = os.path.normpath(os.path.dirname(fpath))\n            if os.path.isdir(fpath):\n                if 'win' in sys.platform:  # windows\n                    subprocess.Popen('explorer \"%s\"' % fpath)\n                elif sys.platform == 'darwin':  # macOS\n                    subprocess.Popen(['open', fpath])\n                else:  # linux\n                    try:\n                        subprocess.Popen(['xdg-open', fpath])\n                    except OSError:\n                        raise OSError('unsupported xdg-open call??')\n\n    def _change_priority(self, **kwargs):\n        \"\"\"Open editor to change priority on first selected row\"\"\"\n        if self._selected_ids:\n            # get_index returns dummy index with column equals to 0\n            index = self.model.get_index(list(self._selected_ids)[0])\n            column_no = self.model.get_header_index(\"priority\")  # real column\n            real_index = self.model.index(index.row(), column_no)\n            self.model.is_editing = True\n            self.table_view.openPersistentEditor(real_index)\n\n    def _get_progress(self, model, representation_id,\n                      site_name, opposite=False):\n        \"\"\"Returns progress value according to site (side)\"\"\"\n        local_progress = lib.get_value_from_id_by_role(model,\n                                                       representation_id,\n                                                       LOCAL_PROGRESS_ROLE)\n        remote_progress = lib.get_value_from_id_by_role(model,\n                                                        representation_id,\n                                                        REMOTE_PROGRESS_ROLE)\n        progress = {'local': local_progress,\n                    'remote': remote_progress}\n        side = 'remote'\n        if site_name == self.model.active_site:\n            side = 'local'\n        if opposite:\n            side = 'remote' if side == 'local' else 'local'\n\n        return progress[side]\n\n    def _get_action_kwargs(self, site_name):\n        \"\"\"Default format of kwargs for action\"\"\"\n        return {\"selected_ids\": self._selected_ids, \"site_name\": site_name}\n\n    def _save_scrollbar(self):\n        self._scrollbar_pos = self.table_view.verticalScrollBar().value()\n\n    def _set_scrollbar(self):\n        if self._scrollbar_pos:\n            self.table_view.verticalScrollBar().setValue(self._scrollbar_pos)\n\n\nclass SyncRepresentationSummaryWidget(_SyncRepresentationWidget):\n\n    default_widths = (\n        (\"asset\", 190),\n        (\"subset\", 170),\n        (\"version\", 60),\n        (\"representation\", 145),\n        (\"local_site\", 160),\n        (\"remote_site\", 160),\n        (\"files_count\", 50),\n        (\"files_size\", 60),\n        (\"priority\", 70),\n        (\"status\", 110)\n    )\n\n    def __init__(self, sync_server, project=None, parent=None):\n        super(SyncRepresentationSummaryWidget, self).__init__(parent)\n\n        self.sync_server = sync_server\n        self._selected_ids = set()  # keep last selected _id\n\n        txt_filter = QtWidgets.QLineEdit()\n        txt_filter.setPlaceholderText(\"Quick filter representations..\")\n        txt_filter.setClearButtonEnabled(True)\n        txt_filter.addAction(\n            qtawesome.icon(\"fa.filter\", color=\"gray\"),\n            QtWidgets.QLineEdit.LeadingPosition)\n        self.txt_filter = txt_filter\n\n        self._scrollbar_pos = None\n\n        top_bar_layout = QtWidgets.QHBoxLayout()\n        top_bar_layout.addWidget(self.txt_filter)\n\n        table_view = QtWidgets.QTableView()\n        headers = [item[0] for item in self.default_widths]\n\n        model = SyncRepresentationSummaryModel(sync_server, headers, project,\n                                               parent=self)\n        table_view.setModel(model)\n        table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        table_view.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection)\n        table_view.setSelectionBehavior(\n            QtWidgets.QAbstractItemView.SelectRows)\n        table_view.horizontalHeader().setSortIndicator(\n            -1, QtCore.Qt.AscendingOrder)\n        table_view.setAlternatingRowColors(True)\n        table_view.verticalHeader().hide()\n        table_view.viewport().setAttribute(QtCore.Qt.WA_Hover, True)\n\n        column = table_view.model().get_header_index(\"local_site\")\n        delegate = delegates.ImageDelegate(self, side=\"local\")\n        table_view.setItemDelegateForColumn(column, delegate)\n\n        column = table_view.model().get_header_index(\"remote_site\")\n        delegate = delegates.ImageDelegate(self, side=\"remote\")\n        table_view.setItemDelegateForColumn(column, delegate)\n\n        column = table_view.model().get_header_index(\"priority\")\n        priority_delegate = delegates.PriorityDelegate(self)\n        table_view.setItemDelegateForColumn(column, priority_delegate)\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addLayout(top_bar_layout)\n        layout.addWidget(table_view)\n\n        self.table_view = table_view\n        self.model = model\n        horizontal_header = HorizontalHeader(self)\n        table_view.setHorizontalHeader(horizontal_header)\n        table_view.setSortingEnabled(True)\n        for column_name, width in self.default_widths:\n            idx = model.get_header_index(column_name)\n            table_view.setColumnWidth(idx, width)\n        table_view.doubleClicked.connect(self._double_clicked)\n        self.txt_filter.textChanged.connect(lambda: model.set_word_filter(\n            self.txt_filter.text()))\n        table_view.customContextMenuRequested.connect(self._on_context_menu)\n        model.refresh_started.connect(self._save_scrollbar)\n        model.refresh_finished.connect(self._set_scrollbar)\n        model.modelReset.connect(self._set_selection)\n\n        self.selection_model = self.table_view.selectionModel()\n        self.selection_model.selectionChanged.connect(self._selection_changed)\n\n    def _prepare_menu(self, local_progress, remote_progress,\n                      is_multi, can_edit, status=None):\n        action_kwarg_map, actions_mapping, menu = \\\n            super()._prepare_menu(local_progress, remote_progress,\n                                  is_multi, can_edit)\n\n        if can_edit and (\n                status in [lib.STATUS[0], lib.STATUS[1]] or is_multi):\n            action = QtWidgets.QAction(\"Pause in queue\")\n            actions_mapping[action] = self._pause\n            # pause handles which site_name it will pause itself\n            action_kwarg_map[action] = {\"selected_ids\": self._selected_ids}\n            menu.addAction(action)\n\n        if can_edit and (status == lib.STATUS[3] or is_multi):\n            action = QtWidgets.QAction(\"Unpause  in queue\")\n            actions_mapping[action] = self._unpause\n            action_kwarg_map[action] = {\"selected_ids\": self._selected_ids}\n            menu.addAction(action)\n\n        return action_kwarg_map, actions_mapping, menu\n\n\nclass SyncServerDetailWindow(QtWidgets.QDialog):\n    \"\"\"Wrapper window for SyncRepresentationDetailWidget\n\n        Creates standalone window with list of files for selected repre_id.\n    \"\"\"\n    def __init__(self, sync_server, _id, project, parent=None):\n        log.debug(\n            \"!!! SyncServerDetailWindow _id:: {}\".format(_id))\n        super(SyncServerDetailWindow, self).__init__(parent)\n        self.setWindowFlags(QtCore.Qt.Window)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        self.setStyleSheet(style.load_stylesheet())\n        self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))\n        self.resize(1000, 400)\n\n        body = QtWidgets.QWidget()\n        footer = QtWidgets.QWidget()\n        footer.setFixedHeight(20)\n\n        container = SyncRepresentationDetailWidget(sync_server, _id, project,\n                                                   parent=self)\n        body_layout = QtWidgets.QHBoxLayout(body)\n        body_layout.addWidget(container)\n        body_layout.setContentsMargins(0, 0, 0, 0)\n\n        self.message = QtWidgets.QLabel()\n        self.message.hide()\n\n        footer_layout = QtWidgets.QVBoxLayout(footer)\n        footer_layout.addWidget(self.message)\n        footer_layout.setContentsMargins(0, 0, 0, 0)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(body)\n        layout.addWidget(footer)\n\n        self.setWindowTitle(\"Sync Representation Detail\")\n\n\nclass SyncRepresentationDetailWidget(_SyncRepresentationWidget):\n    \"\"\"\n        Widget to display list of synchronizable files for single repre.\n\n        Args:\n            _id (str): representation _id\n            project (str): name of project with repre\n            parent (QDialog): SyncServerDetailWindow\n    \"\"\"\n    active_changed = QtCore.Signal()  # active index changed\n\n    default_widths = (\n        (\"file\", 290),\n        (\"local_site\", 185),\n        (\"remote_site\", 185),\n        (\"size\", 60),\n        (\"priority\", 60),\n        (\"status\", 110)\n    )\n\n    def __init__(self, sync_server, _id=None, project=None, parent=None):\n        super(SyncRepresentationDetailWidget, self).__init__(parent)\n\n        log.debug(\"Representation_id:{}\".format(_id))\n        self.project = project\n\n        self.sync_server = sync_server\n\n        self.representation_id = _id\n        self._selected_ids = set()\n\n        self.txt_filter = QtWidgets.QLineEdit()\n        self.txt_filter.setPlaceholderText(\"Quick filter representation..\")\n        self.txt_filter.setClearButtonEnabled(True)\n        self.txt_filter.addAction(qtawesome.icon(\"fa.filter\", color=\"gray\"),\n                                  QtWidgets.QLineEdit.LeadingPosition)\n\n        self._scrollbar_pos = None\n\n        top_bar_layout = QtWidgets.QHBoxLayout()\n        top_bar_layout.addWidget(self.txt_filter)\n\n        table_view = QtWidgets.QTableView()\n        headers = [item[0] for item in self.default_widths]\n\n        model = SyncRepresentationDetailModel(sync_server, headers, _id,\n                                              project)\n        model.is_running = True\n\n        table_view.setModel(model)\n        table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        table_view.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection)\n        table_view.setSelectionBehavior(\n            QtWidgets.QTableView.SelectRows)\n        table_view.horizontalHeader().setSortIndicator(\n            -1, QtCore.Qt.AscendingOrder)\n        table_view.horizontalHeader().setSortIndicatorShown(True)\n        table_view.setAlternatingRowColors(True)\n        table_view.verticalHeader().hide()\n\n        column = model.get_header_index(\"local_site\")\n        delegate = delegates.ImageDelegate(self, side=\"local\")\n        table_view.setItemDelegateForColumn(column, delegate)\n\n        column = model.get_header_index(\"remote_site\")\n        delegate = delegates.ImageDelegate(self, side=\"remote\")\n        table_view.setItemDelegateForColumn(column, delegate)\n\n        if model.can_edit:\n            column = table_view.model().get_header_index(\"priority\")\n            priority_delegate = delegates.PriorityDelegate(self)\n            table_view.setItemDelegateForColumn(column, priority_delegate)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addLayout(top_bar_layout)\n        layout.addWidget(table_view)\n\n        self.model = model\n\n        self.selection_model = table_view.selectionModel()\n        self.selection_model.selectionChanged.connect(self._selection_changed)\n\n        horizontal_header = HorizontalHeader(self)\n\n        table_view.setHorizontalHeader(horizontal_header)\n        table_view.setSortingEnabled(True)\n\n        for column_name, width in self.default_widths:\n            idx = model.get_header_index(column_name)\n            table_view.setColumnWidth(idx, width)\n\n        self.table_view = table_view\n\n        self.txt_filter.textChanged.connect(lambda: model.set_word_filter(\n            self.txt_filter.text()))\n        table_view.doubleClicked.connect(self._double_clicked)\n        table_view.customContextMenuRequested.connect(self._on_context_menu)\n\n        model.refresh_started.connect(self._save_scrollbar)\n        model.refresh_finished.connect(self._set_scrollbar)\n        model.modelReset.connect(self._set_selection)\n\n    def _double_clicked(self, index):\n        \"\"\"\n            Opens representation dialog with all files after doubleclick\n        \"\"\"\n        # priority editing\n        if self.model.can_edit:\n            column_name = self.model.get_column(index.column())\n            if column_name[0] in self.model.EDITABLE_COLUMNS:\n                self.model.is_editing = True\n                self.table_view.openPersistentEditor(index)\n                return\n\n    def _show_detail(self, selected_ids=None):\n        \"\"\"\n            Shows windows with error message for failed sync of a file.\n        \"\"\"\n        detail_window = SyncRepresentationErrorWindow(self.model, selected_ids)\n\n        detail_window.exec()\n\n    def _prepare_menu(self, local_progress, remote_progress,\n                      is_multi, can_edit, status=None):\n        \"\"\"Adds view (and model) dependent actions to default ones\"\"\"\n        action_kwarg_map, actions_mapping, menu = \\\n            super()._prepare_menu(local_progress, remote_progress,\n                                  is_multi, can_edit, status)\n\n        if status == lib.STATUS[2] or is_multi:\n            action = QtWidgets.QAction(\"Open error detail\")\n            actions_mapping[action] = self._show_detail\n            action_kwarg_map[action] = {\"selected_ids\": self._selected_ids}\n\n            menu.addAction(action)\n\n        return action_kwarg_map, actions_mapping, menu\n\n    def _reset_site(self, selected_ids=None, site_name=None):\n        \"\"\"\n            Removes errors or success metadata for particular file >> forces\n            redo of upload/download\n        \"\"\"\n        for file_id in selected_ids:\n            check_progress = self._get_progress(self.model, file_id,\n                                                site_name, True)\n\n            # do not reset if opposite side is not fully there\n            if check_progress != 1:\n                log.debug(\"Not fully available {} on other side, skipping\".\n                          format(check_progress))\n                continue\n\n            self.sync_server.reset_site_on_representation(\n                self.model.project,\n                self.representation_id,\n                site_name=site_name,\n                file_id=file_id,\n                force=True)\n        self.model.refresh(\n            load_records=self.model._rec_loaded)\n\n\nclass SyncRepresentationErrorWindow(QtWidgets.QDialog):\n    \"\"\"Wrapper window to show errors during sync on file(s)\"\"\"\n    def __init__(self, model, selected_ids, parent=None):\n        super(SyncRepresentationErrorWindow, self).__init__(parent)\n        self.setWindowFlags(QtCore.Qt.Window)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        self.setStyleSheet(style.load_stylesheet())\n        self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))\n        self.resize(900, 150)\n\n        body = QtWidgets.QWidget()\n\n        container = SyncRepresentationErrorWidget(model,\n                                                  selected_ids,\n                                                  parent=self)\n        body_layout = QtWidgets.QHBoxLayout(body)\n        body_layout.addWidget(container)\n        body_layout.setContentsMargins(0, 0, 0, 0)\n\n        message = QtWidgets.QLabel()\n        message.hide()\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(body)\n\n        self.setLayout(body_layout)\n        self.setWindowTitle(\"Sync Representation Error Detail\")\n\n\nclass SyncRepresentationErrorWidget(QtWidgets.QWidget):\n    \"\"\"\n        Dialog to show when sync error happened, prints formatted error message\n    \"\"\"\n    def __init__(self, model, selected_ids, parent=None):\n        super(SyncRepresentationErrorWidget, self).__init__(parent)\n\n        layout = QtWidgets.QVBoxLayout(self)\n\n        no_errors = True\n        for file_id in selected_ids:\n            created_dt = lib.get_value_from_id_by_role(model, file_id,\n                                                       LOCAL_DATE_ROLE)\n            sync_dt = lib.get_value_from_id_by_role(model, file_id,\n                                                    REMOTE_DATE_ROLE)\n            errors = lib.get_value_from_id_by_role(model, file_id,\n                                                   ERROR_ROLE)\n            if not created_dt or not sync_dt or not errors:\n                continue\n\n            tries = lib.get_value_from_id_by_role(model, file_id,\n                                                  TRIES_ROLE)\n\n            no_errors = False\n            dt = max(created_dt, sync_dt)\n\n            txts = []\n            txts.append(\"{}: {}<br>\".format(\"<b>Last update date</b>\",\n                                            pretty_timestamp(dt)))\n            txts.append(\"{}: {}<br>\".format(\"<b>Retries</b>\",\n                                            str(tries)))\n            txts.append(\"{}: {}<br>\".format(\"<b>Error message</b>\",\n                                            errors))\n\n            text_area = QtWidgets.QTextEdit(\"\\n\\n\".join(txts))\n            text_area.setReadOnly(True)\n            layout.addWidget(text_area)\n\n        if no_errors:\n            text_area = QtWidgets.QTextEdit()\n            text_area.setText(\"<h4>No errors located</h4>\")\n            text_area.setReadOnly(True)\n            layout.addWidget(text_area)\n\n\nclass HorizontalHeader(QtWidgets.QHeaderView):\n    \"\"\"Reiplemented QHeaderView to contain clickable changeable button\"\"\"\n    def __init__(self, parent=None):\n        super(HorizontalHeader, self).__init__(QtCore.Qt.Horizontal, parent)\n        self._parent = parent\n        self.checked_values = {}\n        self.setModel(self._parent.model)\n\n        self.setSectionsClickable(True)\n\n        self.menu_items_dict = {}\n        self.menu = None\n        self.header_cells = []\n        self.filter_buttons = {}\n\n        self.filter_icon = qtawesome.icon(\"fa.filter\", color=\"gray\")\n        self.filter_set_icon = qtawesome.icon(\"fa.filter\", color=\"white\")\n\n        self.init_layout()\n\n        self._resetting = False\n\n    @property\n    def model(self):\n        \"\"\"Keep model synchronized with parent widget\"\"\"\n        return self._parent.model\n\n    def init_layout(self):\n        \"\"\"Initial preparation of header's content\"\"\"\n        for column_idx in range(self.model.columnCount()):\n            column_name, column_label = self.model.get_column(column_idx)\n            filter_rec = self.model.get_filters().get(column_name)\n            if not filter_rec:\n                continue\n\n            icon = self.filter_icon\n            button = QtWidgets.QPushButton(icon, \"\", self)\n\n            button.setFixedSize(24, 24)\n            button.setStyleSheet(\n                \"QPushButton::menu-indicator{width:0px;}\"\n                \"QPushButton{border: none;background: transparent;}\")\n            button.clicked.connect(partial(self._get_menu,\n                                           column_name, column_idx))\n            button.setFlat(True)\n            self.filter_buttons[column_name] = button\n\n    def showEvent(self, event):\n        \"\"\"Paint header\"\"\"\n        super(HorizontalHeader, self).showEvent(event)\n\n        for i in range(len(self.header_cells)):\n            cell_content = self.header_cells[i]\n            cell_content.setGeometry(self.sectionViewportPosition(i), 0,\n                                     self.sectionSize(i) - 1, self.height())\n\n            cell_content.show()\n\n    def _set_filter_icon(self, column_name):\n        \"\"\"Set different states of button depending on its engagement\"\"\"\n        button = self.filter_buttons.get(column_name)\n        if button:\n            if self.checked_values.get(column_name):\n                button.setIcon(self.filter_set_icon)\n            else:\n                button.setIcon(self.filter_icon)\n\n    def _reset_filter(self, column_name):\n        \"\"\"\n            Remove whole column from filter >> not in $match at all (faster)\n        \"\"\"\n        self._resetting = True  # mark changes to consume them\n        if self.checked_values.get(column_name) is not None:\n            self.checked_values.pop(column_name)\n            self._set_filter_icon(column_name)\n        self._filter_and_refresh_model_and_menu(column_name, True, True)\n        self._resetting = False\n\n    def _apply_filter(self, column_name, values, state):\n        \"\"\"\n            Sets 'values' to specific 'state' (checked/unchecked),\n            sends to model.\n        \"\"\"\n        if self._resetting:  # event triggered by _resetting, skip it\n            return\n\n        self._update_checked_values(column_name, values, state)\n        self._set_filter_icon(column_name)\n        self._filter_and_refresh_model_and_menu(column_name, True, False)\n\n    def _apply_text_filter(self, column_name, items, line_edit):\n        \"\"\"\n            Resets all checkboxes, prefers inserted text.\n        \"\"\"\n        le_text = line_edit.text()\n        self._update_checked_values(column_name, items, 0)  # reset other\n        if self.checked_values.get(column_name) is not None or \\\n                le_text == '':\n            self.checked_values.pop(column_name)  # reset during typing\n\n        if le_text:\n            self._update_checked_values(column_name, {le_text: le_text}, 2)\n        self._set_filter_icon(column_name)\n        self._filter_and_refresh_model_and_menu(column_name, True, True)\n\n    def _filter_and_refresh_model_and_menu(self, column_name,\n                                           model=True, menu=True):\n        \"\"\"\n            Refresh model and its content and possibly menu for big changes.\n        \"\"\"\n        if model:\n            self.model.set_column_filtering(self.checked_values)\n            self.model.refresh()\n        if menu:\n            self._menu_refresh(column_name)\n\n    def _get_menu(self, column_name, index):\n        \"\"\"Prepares content of menu for 'column_name'\"\"\"\n        menu = QtWidgets.QMenu(self)\n        filter_rec = self.model.get_filters()[column_name]\n        self.menu_items_dict[column_name] = filter_rec.values()\n\n        # text filtering only if labels same as values, not if codes are used\n        if 'text' in filter_rec.search_variants():\n            line_edit = QtWidgets.QLineEdit(menu)\n            line_edit.setClearButtonEnabled(True)\n            line_edit.addAction(self.filter_icon,\n                                QtWidgets.QLineEdit.LeadingPosition)\n\n            line_edit.setFixedHeight(line_edit.height())\n            txt = \"\"\n            if self.checked_values.get(column_name):\n                txt = list(self.checked_values.get(column_name).keys())[0]\n            line_edit.setText(txt)\n\n            action_le = QtWidgets.QWidgetAction(menu)\n            action_le.setDefaultWidget(line_edit)\n            line_edit.textChanged.connect(\n                partial(self._apply_text_filter, column_name,\n                        filter_rec.values(), line_edit))\n            menu.addAction(action_le)\n            menu.addSeparator()\n\n        if 'checkbox' in filter_rec.search_variants():\n            action_all = QtWidgets.QAction(\"All\", self)\n            action_all.triggered.connect(partial(self._reset_filter,\n                                                 column_name))\n            menu.addAction(action_all)\n\n            action_none = QtWidgets.QAction(\"Unselect all\", self)\n            state_unchecked = 0\n            action_none.triggered.connect(partial(self._apply_filter,\n                                                  column_name,\n                                                  filter_rec.values(),\n                                                  state_unchecked))\n            menu.addAction(action_none)\n            menu.addSeparator()\n\n        # nothing explicitly >> ALL implicitly >> first time\n        if self.checked_values.get(column_name) is None:\n            checked_keys = self.menu_items_dict[column_name].keys()\n        else:\n            checked_keys = self.checked_values[column_name]\n\n        for value, label in self.menu_items_dict[column_name].items():\n            checkbox = QtWidgets.QCheckBox(str(label), menu)\n\n            # temp\n            checkbox.setStyleSheet(\"QCheckBox{spacing: 5px;\"\n                                   \"padding:5px 5px 5px 5px;}\")\n            if value in checked_keys:\n                checkbox.setChecked(True)\n\n            action = QtWidgets.QWidgetAction(menu)\n            action.setDefaultWidget(checkbox)\n\n            checkbox.stateChanged.connect(partial(self._apply_filter,\n                                                  column_name, {value: label}))\n            menu.addAction(action)\n\n        self.menu = menu\n\n        self._show_menu(index, menu)\n\n    def _show_menu(self, index, menu):\n        \"\"\"Shows 'menu' under header column of 'index'\"\"\"\n        global_pos_point = self.mapToGlobal(\n            QtCore.QPoint(self.sectionViewportPosition(index), 0))\n        menu.setMinimumWidth(self.sectionSize(index))\n        menu.setMinimumHeight(self.height())\n        menu.exec_(QtCore.QPoint(global_pos_point.x(),\n                                 global_pos_point.y() + self.height()))\n\n    def _menu_refresh(self, column_name):\n        \"\"\"\n            Reset boxes after big change - word filtering or reset\n        \"\"\"\n        for action in self.menu.actions():\n            if not isinstance(action, QtWidgets.QWidgetAction):\n                continue\n\n            widget = action.defaultWidget()\n            if not isinstance(widget, QtWidgets.QCheckBox):\n                continue\n\n            if not self.checked_values.get(column_name) or \\\n                    widget.text() in self.checked_values[column_name].values():\n                widget.setChecked(True)\n            else:\n                widget.setChecked(False)\n\n    def _update_checked_values(self, column_name, values, state):\n        \"\"\"\n            Modify dictionary of set values in columns for filtering.\n\n            Modifies 'self.checked_values'\n        \"\"\"\n        copy_menu_items = dict(self.menu_items_dict[column_name])\n        checked = self.checked_values.get(column_name, copy_menu_items)\n        set_items = dict(values.items())  # prevent dict change during loop\n        for value, label in set_items.items():\n            if state == 2 and label:  # checked\n                checked[value] = label\n            elif state == 0 and checked.get(value):\n                checked.pop(value)\n\n        self.checked_values[column_name] = checked\n\n    def paintEvent(self, event):\n        self._fix_size()\n        super(HorizontalHeader, self).paintEvent(event)\n\n    def _fix_size(self):\n        for column_idx in range(self.model.columnCount()):\n            vis_index = self.visualIndex(column_idx)\n            index = self.logicalIndex(vis_index)\n            section_width = self.sectionSize(index)\n\n            column_name = self.model.headerData(column_idx,\n                                                QtCore.Qt.Horizontal,\n                                                HEADER_NAME_ROLE)\n            button = self.filter_buttons.get(column_name)\n            if not button:\n                continue\n\n            pos_x = self.sectionViewportPosition(\n                index) + section_width - self.height()\n\n            pos_y = 0\n            if button.height() < self.height():\n                pos_y = int((self.height() - button.height()) / 2)\n            button.setGeometry(\n                pos_x,\n                pos_y,\n                self.height(),\n                self.height())\n"
  },
  {
    "path": "openpype/modules/sync_server/utils.py",
    "content": "import os\nimport time\n\nfrom openpype.lib import Logger\n\nlog = Logger.get_logger(\"SyncServer\")\n\nSYNC_SERVER_ROOT = os.path.dirname(os.path.abspath(__file__))\n\n\nclass ResumableError(Exception):\n    \"\"\"Error which could be temporary, skip current loop, try next time\"\"\"\n    pass\n\n\nclass SiteAlreadyPresentError(Exception):\n    \"\"\"Representation has already site skeleton present.\"\"\"\n    pass\n\n\nclass SyncStatus:\n    DO_NOTHING = 0\n    DO_UPLOAD = 1\n    DO_DOWNLOAD = 2\n\n\ndef time_function(method):\n    \"\"\" Decorator to print how much time function took.\n        For debugging.\n        Depends on presence of 'log' object\n    \"\"\"\n\n    def timed(*args, **kw):\n        ts = time.time()\n        result = method(*args, **kw)\n        te = time.time()\n        if 'log_time' in kw:\n            name = kw.get('log_name', method.__name__.upper())\n            kw['log_time'][name] = int((te - ts) * 1000)\n        else:\n            log.debug('%r  %2.2f ms' % (method.__name__, (te - ts) * 1000))\n        return result\n\n    return timed\n\n\nclass EditableScopes:\n    SYSTEM = 0\n    PROJECT = 1\n    LOCAL = 2\n"
  },
  {
    "path": "openpype/modules/timers_manager/__init__.py",
    "content": "from .timers_manager import (\n    TimersManager\n)\n\n__all__ = (\n    \"TimersManager\",\n)\n"
  },
  {
    "path": "openpype/modules/timers_manager/exceptions.py",
    "content": "class InvalidContextError(ValueError):\n    \"\"\"Context for which the timer should be started is invalid.\"\"\"\n    pass\n"
  },
  {
    "path": "openpype/modules/timers_manager/idle_threads.py",
    "content": "import time\nfrom qtpy import QtCore\nfrom pynput import mouse, keyboard\n\nfrom openpype.lib import Logger\n\n\nclass IdleItem:\n    \"\"\"Python object holds information if state of idle changed.\n\n    This item is used to be independent from Qt objects.\n    \"\"\"\n    def __init__(self):\n        self.changed = False\n\n    def reset(self):\n        self.changed = False\n\n    def set_changed(self, changed=True):\n        self.changed = changed\n\n\nclass IdleManager(QtCore.QThread):\n    \"\"\" Measure user's idle time in seconds.\n    Idle time resets on keyboard/mouse input.\n    Is able to emit signals at specific time idle.\n    \"\"\"\n    time_signals = {}\n    idle_time = 0\n    signal_reset_timer = QtCore.Signal()\n\n    def __init__(self):\n        super(IdleManager, self).__init__()\n        self.log = Logger.get_logger(self.__class__.__name__)\n        self.signal_reset_timer.connect(self._reset_time)\n\n        self.idle_item = IdleItem()\n\n        self._is_running = False\n        self._mouse_thread = None\n        self._keyboard_thread = None\n\n    def add_time_signal(self, emit_time, signal):\n        \"\"\" If any module want to use IdleManager, need to use add_time_signal\n\n        Args:\n            emit_time(int): Time when signal will be emitted.\n            signal(QtCore.Signal): Signal that will be emitted\n                (without objects).\n        \"\"\"\n        if emit_time not in self.time_signals:\n            self.time_signals[emit_time] = []\n        self.time_signals[emit_time].append(signal)\n\n    @property\n    def is_running(self):\n        return self._is_running\n\n    def _reset_time(self):\n        self.idle_time = 0\n\n    def stop(self):\n        self._is_running = False\n\n    def _on_mouse_destroy(self):\n        self._mouse_thread = None\n\n    def _on_keyboard_destroy(self):\n        self._keyboard_thread = None\n\n    def run(self):\n        self.log.info('IdleManager has started')\n        self._is_running = True\n\n        thread_mouse = MouseThread(self.idle_item)\n        thread_keyboard = KeyboardThread(self.idle_item)\n\n        thread_mouse.destroyed.connect(self._on_mouse_destroy)\n        thread_keyboard.destroyed.connect(self._on_keyboard_destroy)\n\n        self._mouse_thread = thread_mouse\n        self._keyboard_thread = thread_keyboard\n\n        thread_mouse.start()\n        thread_keyboard.start()\n\n        # Main loop here is each second checked if idle item changed state\n        while self._is_running:\n            if self.idle_item.changed:\n                self.idle_item.reset()\n                self.signal_reset_timer.emit()\n            else:\n                self.idle_time += 1\n\n            if self.idle_time in self.time_signals:\n                for signal in self.time_signals[self.idle_time]:\n                    signal.emit()\n            time.sleep(1)\n\n        self._post_run()\n        self.log.info('IdleManager has stopped')\n\n    def _post_run(self):\n        # Stop threads if still exist\n        if self._mouse_thread is not None:\n            self._mouse_thread.signal_stop.emit()\n            self._mouse_thread.terminate()\n            self._mouse_thread.wait()\n\n        if self._keyboard_thread is not None:\n            self._keyboard_thread.signal_stop.emit()\n            self._keyboard_thread.terminate()\n            self._keyboard_thread.wait()\n\n\nclass MouseThread(QtCore.QThread):\n    \"\"\"Listens user's mouse movement.\"\"\"\n    signal_stop = QtCore.Signal()\n\n    def __init__(self, idle_item):\n        super(MouseThread, self).__init__()\n        self.signal_stop.connect(self.stop)\n        self.m_listener = None\n        self.idle_item = idle_item\n\n    def stop(self):\n        if self.m_listener is not None:\n            self.m_listener.stop()\n\n    def on_move(self, *args, **kwargs):\n        self.idle_item.set_changed()\n\n    def run(self):\n        self.m_listener = mouse.Listener(on_move=self.on_move)\n        self.m_listener.start()\n\n\nclass KeyboardThread(QtCore.QThread):\n    \"\"\"Listens user's keyboard input\n    \"\"\"\n    signal_stop = QtCore.Signal()\n\n    def __init__(self, idle_item):\n        super(KeyboardThread, self).__init__()\n        self.signal_stop.connect(self.stop)\n        self.k_listener = None\n        self.idle_item = idle_item\n\n    def stop(self):\n        if self.k_listener is not None:\n            listener = self.k_listener\n            self.k_listener = None\n            listener.stop()\n\n    def on_press(self, *args, **kwargs):\n        self.idle_item.set_changed()\n\n    def run(self):\n        self.k_listener = keyboard.Listener(on_press=self.on_press)\n        self.k_listener.start()\n"
  },
  {
    "path": "openpype/modules/timers_manager/launch_hooks/post_start_timer.py",
    "content": "from openpype.lib.applications import PostLaunchHook, LaunchTypes\n\n\nclass PostStartTimerHook(PostLaunchHook):\n    \"\"\"Start timer with TimersManager module.\n\n    This module requires enabled TimerManager module.\n    \"\"\"\n    order = None\n    launch_types = {LaunchTypes.local}\n\n    def execute(self):\n        project_name = self.data.get(\"project_name\")\n        asset_name = self.data.get(\"asset_name\")\n        task_name = self.data.get(\"task_name\")\n\n        missing_context_keys = set()\n        if not project_name:\n            missing_context_keys.add(\"project_name\")\n        if not asset_name:\n            missing_context_keys.add(\"asset_name\")\n        if not task_name:\n            missing_context_keys.add(\"task_name\")\n\n        if missing_context_keys:\n            missing_keys_str = \", \".join([\n                \"\\\"{}\\\"\".format(key) for key in missing_context_keys\n            ])\n            self.log.debug(\"Hook {} skipped. Missing data keys: {}\".format(\n                self.__class__.__name__, missing_keys_str\n            ))\n            return\n\n        timers_manager = self.modules_manager.modules_by_name.get(\n            \"timers_manager\"\n        )\n        if not timers_manager or not timers_manager.enabled:\n            self.log.info((\n                \"Skipping starting timer because\"\n                \" TimersManager is not available.\"\n            ))\n            return\n\n        timers_manager.start_timer_with_webserver(\n            project_name, asset_name, task_name, logger=self.log\n        )\n"
  },
  {
    "path": "openpype/modules/timers_manager/plugins/publish/start_timer.py",
    "content": "\"\"\"\nRequires:\n    context -> system_settings\n    context -> openPypeModules\n\"\"\"\n\nimport pyblish.api\n\n\nclass StartTimer(pyblish.api.ContextPlugin):\n    label = \"Start Timer\"\n    order = pyblish.api.IntegratorOrder + 1\n    hosts = [\"*\"]\n\n    def process(self, context):\n        timers_manager = context.data[\"openPypeModules\"][\"timers_manager\"]\n        if not timers_manager.enabled:\n            self.log.debug(\"TimersManager is disabled\")\n            return\n\n        modules_settings = context.data[\"system_settings\"][\"modules\"]\n        if not modules_settings[\"timers_manager\"][\"disregard_publishing\"]:\n            self.log.debug(\"Publish is not affecting running timers.\")\n            return\n\n        project_name = context.data[\"projectName\"]\n        asset_name = context.data.get(\"asset\")\n        task_name = context.data.get(\"task\")\n        if not project_name or not asset_name or not task_name:\n            self.log.info((\n                \"Current context does not contain all\"\n                \" required information to start a timer.\"\n            ))\n            return\n        timers_manager.start_timer_with_webserver(\n            project_name, asset_name, task_name, self.log\n        )\n"
  },
  {
    "path": "openpype/modules/timers_manager/plugins/publish/stop_timer.py",
    "content": "\"\"\"\nRequires:\n    context -> system_settings\n    context -> openPypeModules\n\"\"\"\n\n\nimport pyblish.api\n\n\nclass StopTimer(pyblish.api.ContextPlugin):\n    label = \"Stop Timer\"\n    order = pyblish.api.ExtractorOrder - 0.49\n    hosts = [\"*\"]\n\n    def process(self, context):\n        timers_manager = context.data[\"openPypeModules\"][\"timers_manager\"]\n        if not timers_manager.enabled:\n            self.log.debug(\"TimersManager is disabled\")\n            return\n\n        modules_settings = context.data[\"system_settings\"][\"modules\"]\n        if not modules_settings[\"timers_manager\"][\"disregard_publishing\"]:\n            self.log.debug(\"Publish is not affecting running timers.\")\n            return\n\n        timers_manager.stop_timer_with_webserver(self.log)\n"
  },
  {
    "path": "openpype/modules/timers_manager/rest_api.py",
    "content": "import json\n\nfrom aiohttp.web_response import Response\nfrom openpype.lib import Logger\n\n\nclass TimersManagerModuleRestApi:\n    \"\"\"\n        REST API endpoint used for calling from hosts when context change\n        happens in Workfile app.\n    \"\"\"\n    def __init__(self, user_module, server_manager):\n        self._log = None\n        self.module = user_module\n        self.server_manager = server_manager\n\n        self.prefix = \"/timers_manager\"\n\n        self.register()\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    def register(self):\n        self.server_manager.add_route(\n            \"POST\",\n            self.prefix + \"/start_timer\",\n            self.start_timer\n        )\n        self.server_manager.add_route(\n            \"POST\",\n            self.prefix + \"/stop_timer\",\n            self.stop_timer\n        )\n        self.server_manager.add_route(\n            \"GET\",\n            self.prefix + \"/get_task_time\",\n            self.get_task_time\n        )\n\n    async def start_timer(self, request):\n        data = await request.json()\n        try:\n            project_name = data[\"project_name\"]\n            asset_name = data[\"asset_name\"]\n            task_name = data[\"task_name\"]\n        except KeyError:\n            msg = (\n                \"Payload must contain fields 'project_name,\"\n                \" 'asset_name' and 'task_name'\"\n            )\n            self.log.error(msg)\n            return Response(status=400, message=msg)\n\n        self.module.stop_timers()\n        try:\n            self.module.start_timer(project_name, asset_name, task_name)\n        except Exception as exc:\n            return Response(status=404, message=str(exc))\n\n        return Response(status=200)\n\n    async def stop_timer(self, request):\n        self.module.stop_timers()\n        return Response(status=200)\n\n    async def get_task_time(self, request):\n        data = await request.json()\n        try:\n            project_name = data['project_name']\n            asset_name = data['asset_name']\n            task_name = data['task_name']\n        except KeyError:\n            message = (\n                \"Payload must contain fields 'project_name, 'asset_name',\"\n                \" 'task_name'\"\n            )\n            self.log.warning(message)\n            return Response(text=message, status=404)\n\n        time = self.module.get_task_time(project_name, asset_name, task_name)\n        return Response(text=json.dumps(time))\n"
  },
  {
    "path": "openpype/modules/timers_manager/timers_manager.py",
    "content": "import os\nimport platform\n\n\nfrom openpype.client import get_asset_by_name\nfrom openpype.modules import (\n    OpenPypeModule,\n    ITrayService,\n    IPluginPaths\n)\nfrom openpype.lib.events import register_event_callback\n\nfrom .exceptions import InvalidContextError\n\nTIMER_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\nclass ExampleTimersManagerConnector:\n    \"\"\"Timers manager can handle timers of multiple modules/addons.\n\n    Module must have object under `timers_manager_connector` attribute with\n    few methods. This is example class of the object that could be stored under\n    module.\n\n    Required methods are 'stop_timer' and 'start_timer'.\n\n    # TODO pass asset document instead of `hierarchy`\n    Example of `data` that are passed during changing timer:\n    ```\n    data = {\n        \"project_name\": project_name,\n        \"task_name\": task_name,\n        \"task_type\": task_type,\n        \"hierarchy\": hierarchy\n    }\n    ```\n    \"\"\"\n\n    # Not needed at all\n    def __init__(self, module):\n        # Store timer manager module to be able call it's methods when needed\n        self._timers_manager_module = None\n\n        # Store module which want to use timers manager to have access\n        self._module = module\n\n    # Required\n    def stop_timer(self):\n        \"\"\"Called by timers manager when module should stop timer.\"\"\"\n        self._module.stop_timer()\n\n    # Required\n    def start_timer(self, data):\n        \"\"\"Method called by timers manager when should start timer.\"\"\"\n        self._module.start_timer(data)\n\n    # Optional\n    def register_timers_manager(self, timer_manager_module):\n        \"\"\"Method called by timers manager where it's object is passed.\n\n        This is moment when timers manager module can be store to be able\n        call it's callbacks (e.g. timer started).\n        \"\"\"\n        self._timers_manager_module = timer_manager_module\n\n    # Custom implementation\n    def timer_started(self, data):\n        \"\"\"This is example of possibility to trigger callbacks on manager.\"\"\"\n        if self._timers_manager_module is not None:\n            self._timers_manager_module.timer_started(self._module.id, data)\n\n    # Custom implementation\n    def timer_stopped(self):\n        if self._timers_manager_module is not None:\n            self._timers_manager_module.timer_stopped(self._module.id)\n\n\nclass TimersManager(\n    OpenPypeModule,\n    ITrayService,\n    IPluginPaths\n):\n    \"\"\" Handles about Timers.\n\n    Should be able to start/stop all timers at once.\n\n    To be able use this advantage module has to have attribute with name\n    `timers_manager_connector` which has two methods 'stop_timer'\n    and 'start_timer'. Optionally may have `register_timers_manager` where\n    object of TimersManager module is passed to be able call it's callbacks.\n\n    See `ExampleTimersManagerConnector`.\n    \"\"\"\n    name = \"timers_manager\"\n    label = \"Timers Service\"\n\n    _required_methods = (\n        \"stop_timer\",\n        \"start_timer\"\n    )\n\n    def initialize(self, modules_settings):\n        timers_settings = modules_settings[self.name]\n\n        self.enabled = timers_settings[\"enabled\"]\n\n        # When timer will stop if idle manager is running (minutes)\n        full_time = int(timers_settings[\"full_time\"] * 60)\n        # How many minutes before the timer is stopped will popup the message\n        message_time = int(timers_settings[\"message_time\"] * 60)\n\n        auto_stop = timers_settings[\"auto_stop\"]\n        platform_name = platform.system().lower()\n        # Turn of auto stop on MacOs because pynput requires root permissions\n        #    and on linux can cause thread locks on application close\n        if full_time <= 0 or platform_name in (\"darwin\", \"linux\"):\n            auto_stop = False\n\n        self.auto_stop = auto_stop\n        self.time_show_message = full_time - message_time\n        self.time_stop_timer = full_time\n\n        self.is_running = False\n        self.last_task = None\n\n        # Tray attributes\n        self._signal_handler = None\n        self._widget_user_idle = None\n        self._idle_manager = None\n\n        self._connectors_by_module_id = {}\n        self._modules_by_id = {}\n\n    def tray_init(self):\n        if not self.auto_stop:\n            return\n\n        from .idle_threads import IdleManager\n        from .widget_user_idle import WidgetUserIdle, SignalHandler\n\n        signal_handler = SignalHandler(self)\n        idle_manager = IdleManager()\n        widget_user_idle = WidgetUserIdle(self)\n        widget_user_idle.set_countdown_start(\n            self.time_stop_timer - self.time_show_message\n        )\n\n        idle_manager.signal_reset_timer.connect(\n            widget_user_idle.reset_countdown\n        )\n        idle_manager.add_time_signal(\n            self.time_show_message, signal_handler.signal_show_message\n        )\n        idle_manager.add_time_signal(\n            self.time_stop_timer, signal_handler.signal_stop_timers\n        )\n\n        self._signal_handler = signal_handler\n        self._widget_user_idle = widget_user_idle\n        self._idle_manager = idle_manager\n\n    def tray_start(self, *_a, **_kw):\n        if self._idle_manager:\n            self._idle_manager.start()\n\n    def tray_exit(self):\n        if self._idle_manager:\n            self._idle_manager.stop()\n            self._idle_manager.wait()\n\n    def get_timer_data_for_path(self, task_path):\n        \"\"\"Convert string path to a timer data.\n\n        It is expected that first item is project name, last item is task name\n        and parent asset name is before task name.\n        \"\"\"\n        path_items = task_path.split(\"/\")\n        if len(path_items) < 3:\n            raise InvalidContextError(\"Invalid path \\\"{}\\\"\".format(task_path))\n        task_name = path_items.pop(-1)\n        asset_name = path_items.pop(-1)\n        project_name = path_items.pop(0)\n        return self.get_timer_data_for_context(\n            project_name, asset_name, task_name, self.log\n        )\n\n    def get_launch_hook_paths(self):\n        \"\"\"Implementation for applications launch hooks.\"\"\"\n\n        return [\n            os.path.join(TIMER_MODULE_DIR, \"launch_hooks\")\n        ]\n\n    def get_plugin_paths(self):\n        \"\"\"Implementation of `IPluginPaths`.\"\"\"\n\n        return {\n            \"publish\": [os.path.join(TIMER_MODULE_DIR, \"plugins\", \"publish\")]\n        }\n\n    @staticmethod\n    def get_timer_data_for_context(\n        project_name, asset_name, task_name, logger=None\n    ):\n        \"\"\"Prepare data for timer related callbacks.\n\n        TODO:\n        - return predefined object that has access to asset document etc.\n        \"\"\"\n        if not project_name or not asset_name or not task_name:\n            raise InvalidContextError((\n                \"Missing context information got\"\n                \" Project: \\\"{}\\\" Asset: \\\"{}\\\" Task: \\\"{}\\\"\"\n            ).format(str(project_name), str(asset_name), str(task_name)))\n\n        asset_doc = get_asset_by_name(\n            project_name,\n            asset_name,\n            fields=[\"_id\", \"name\", \"data.tasks\", \"data.parents\"]\n        )\n\n        if not asset_doc:\n            raise InvalidContextError((\n                \"Asset \\\"{}\\\" not found in project \\\"{}\\\"\"\n            ).format(asset_name, project_name))\n\n        asset_data = asset_doc.get(\"data\") or {}\n        asset_tasks = asset_data.get(\"tasks\") or {}\n        if task_name not in asset_tasks:\n            raise InvalidContextError((\n                \"Task \\\"{}\\\" not found on asset \\\"{}\\\" in project \\\"{}\\\"\"\n            ).format(task_name, asset_name, project_name))\n\n        task_type = \"\"\n        try:\n            task_type = asset_tasks[task_name][\"type\"]\n        except KeyError:\n            msg = \"Couldn't find task_type for {}\".format(task_name)\n            if logger is not None:\n                logger.warning(msg)\n            else:\n                print(msg)\n\n        hierarchy_items = asset_data.get(\"parents\") or []\n        hierarchy_items.append(asset_name)\n\n        return {\n            \"project_name\": project_name,\n            \"asset_id\": str(asset_doc[\"_id\"]),\n            \"asset_name\": asset_name,\n            \"task_name\": task_name,\n            \"task_type\": task_type,\n            \"hierarchy\": hierarchy_items\n        }\n\n    def start_timer(self, project_name, asset_name, task_name):\n        \"\"\"Start timer for passed context.\n\n        Args:\n            project_name (str): Project name\n            asset_name (str): Asset name\n            task_name (str): Task name\n        \"\"\"\n        data = self.get_timer_data_for_context(\n            project_name, asset_name, task_name, self.log\n        )\n        self.timer_started(None, data)\n\n    def get_task_time(self, project_name, asset_name, task_name):\n        \"\"\"Get total time for passed context.\n\n        TODO:\n        - convert context to timer data\n        \"\"\"\n        times = {}\n        for module_id, connector in self._connectors_by_module_id.items():\n            if hasattr(connector, \"get_task_time\"):\n                module = self._modules_by_id[module_id]\n                times[module.name] = connector.get_task_time(\n                    project_name, asset_name, task_name\n                )\n        return times\n\n    def timer_started(self, source_id, data):\n        \"\"\"Connector triggered that timer has started.\n\n        New timer has started for context in data.\n        \"\"\"\n        for module_id, connector in self._connectors_by_module_id.items():\n            if module_id == source_id:\n                continue\n\n            try:\n                connector.start_timer(data)\n            except Exception:\n                self.log.info(\n                    \"Failed to start timer on connector {}\".format(\n                        str(connector)\n                    )\n                )\n\n        self.last_task = data\n        self.is_running = True\n\n    def timer_stopped(self, source_id):\n        \"\"\"Connector triggered that hist timer has stopped.\n\n        Should stop all other timers.\n\n        TODO:\n        - pass context for which timer has stopped to validate if timers are\n            same and valid\n        \"\"\"\n        for module_id, connector in self._connectors_by_module_id.items():\n            if module_id == source_id:\n                continue\n\n            try:\n                connector.stop_timer()\n            except Exception:\n                self.log.info(\n                    \"Failed to stop timer on connector {}\".format(\n                        str(connector)\n                    )\n                )\n\n    def restart_timers(self):\n        if self.last_task is not None:\n            self.timer_started(None, self.last_task)\n\n    def stop_timers(self):\n        \"\"\"Stop all timers.\"\"\"\n        if self.is_running is False:\n            return\n\n        if self._widget_user_idle is not None:\n            self._widget_user_idle.set_timer_stopped()\n        self.is_running = False\n\n        self.timer_stopped(None)\n\n    def connect_with_modules(self, enabled_modules):\n        for module in enabled_modules:\n            connector = getattr(module, \"timers_manager_connector\", None)\n            if connector is None:\n                continue\n\n            missing_methods = set()\n            for method_name in self._required_methods:\n                if not hasattr(connector, method_name):\n                    missing_methods.add(method_name)\n\n            if missing_methods:\n                joined = \", \".join(\n                    ['\"{}\"'.format(name for name in missing_methods)]\n                )\n                self.log.info((\n                    \"Module \\\"{}\\\" has missing required methods {}.\"\n                ).format(module.name, joined))\n                continue\n\n            self._connectors_by_module_id[module.id] = connector\n            self._modules_by_id[module.id] = module\n\n            # Optional method\n            if hasattr(connector, \"register_timers_manager\"):\n                try:\n                    connector.register_timers_manager(self)\n                except Exception:\n                    self.log.info((\n                        \"Failed to register timers manager\"\n                        \" for connector of module \\\"{}\\\".\"\n                    ).format(module.name))\n\n    def show_message(self):\n        if self.is_running is False:\n            return\n        if not self._widget_user_idle.is_showed():\n            self._widget_user_idle.reset_countdown()\n            self._widget_user_idle.show()\n\n    # Webserver module implementation\n    def webserver_initialization(self, server_manager):\n        \"\"\"Add routes for timers to be able start/stop with rest api.\"\"\"\n        if self.tray_initialized:\n            from .rest_api import TimersManagerModuleRestApi\n            self.rest_api_obj = TimersManagerModuleRestApi(\n                self, server_manager\n            )\n\n    @staticmethod\n    def start_timer_with_webserver(\n        project_name, asset_name, task_name, logger=None\n    ):\n        \"\"\"Prepared method for calling change timers on REST api.\n\n        Webserver must be active. At the moment is Webserver running only when\n        OpenPype Tray is used.\n\n        Args:\n            project_name (str): Project name.\n            asset_name (str): Asset name.\n            task_name (str): Task name.\n            logger (logging.Logger): Logger object. Using 'print' if not\n                passed.\n        \"\"\"\n\n        webserver_url = os.environ.get(\"OPENPYPE_WEBSERVER_URL\")\n        if not webserver_url:\n            msg = \"Couldn't find webserver url\"\n            if logger is not None:\n                logger.warning(msg)\n            else:\n                print(msg)\n            return\n\n        rest_api_url = \"{}/timers_manager/start_timer\".format(webserver_url)\n        try:\n            import requests\n        except Exception:\n            msg = \"Couldn't start timer ('requests' is not available)\"\n            if logger is not None:\n                logger.warning(msg)\n            else:\n                print(msg)\n            return\n        data = {\n            \"project_name\": project_name,\n            \"asset_name\": asset_name,\n            \"task_name\": task_name\n        }\n\n        return requests.post(rest_api_url, json=data)\n\n    @staticmethod\n    def stop_timer_with_webserver(logger=None):\n        \"\"\"Prepared method for calling stop timers on REST api.\n\n        Args:\n            logger (logging.Logger): Logger used for logging messages.\n        \"\"\"\n\n        webserver_url = os.environ.get(\"OPENPYPE_WEBSERVER_URL\")\n        if not webserver_url:\n            msg = \"Couldn't find webserver url\"\n            if logger is not None:\n                logger.warning(msg)\n            else:\n                print(msg)\n            return\n\n        rest_api_url = \"{}/timers_manager/stop_timer\".format(webserver_url)\n        try:\n            import requests\n        except Exception:\n            msg = \"Couldn't start timer ('requests' is not available)\"\n            if logger is not None:\n                logger.warning(msg)\n            else:\n                print(msg)\n            return\n\n        return requests.post(rest_api_url)\n\n    def on_host_install(self, host, host_name, project_name):\n        self.log.debug(\"Installing task changed callback\")\n        register_event_callback(\"taskChanged\", self._on_host_task_change)\n\n    def _on_host_task_change(self, event):\n        project_name = event[\"project_name\"]\n        asset_name = event[\"asset_name\"]\n        task_name = event[\"task_name\"]\n        self.log.debug((\n            \"Sending message that timer should change to\"\n            \" Project: {} Asset: {} Task: {}\"\n        ).format(project_name, asset_name, task_name))\n\n        self.start_timer_with_webserver(\n            project_name, asset_name, task_name, self.log\n        )\n"
  },
  {
    "path": "openpype/modules/timers_manager/widget_user_idle.py",
    "content": "from qtpy import QtCore, QtGui, QtWidgets\nfrom openpype import resources, style\n\n\nclass WidgetUserIdle(QtWidgets.QWidget):\n    SIZE_W = 300\n    SIZE_H = 160\n\n    def __init__(self, module):\n        super(WidgetUserIdle, self).__init__()\n\n        self.setWindowTitle(\"OpenPype - Stop timers\")\n\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n\n        self.setWindowFlags(\n            QtCore.Qt.WindowCloseButtonHint\n            | QtCore.Qt.WindowMinimizeButtonHint\n            | QtCore.Qt.WindowStaysOnTopHint\n        )\n\n        self._is_showed = False\n        self._timer_stopped = False\n        self._countdown = 0\n        self._countdown_start = 0\n\n        self.module = module\n\n        msg_info = \"You didn't work for a long time.\"\n        msg_question = \"Would you like to stop Timers?\"\n        msg_stopped = (\n            \"Your Timers were stopped. Do you want to start them again?\"\n        )\n\n        lbl_info = QtWidgets.QLabel(msg_info, self)\n        lbl_info.setTextFormat(QtCore.Qt.RichText)\n        lbl_info.setWordWrap(True)\n\n        lbl_question = QtWidgets.QLabel(msg_question, self)\n        lbl_question.setTextFormat(QtCore.Qt.RichText)\n        lbl_question.setWordWrap(True)\n\n        lbl_stopped = QtWidgets.QLabel(msg_stopped, self)\n        lbl_stopped.setTextFormat(QtCore.Qt.RichText)\n        lbl_stopped.setWordWrap(True)\n\n        lbl_rest_time = QtWidgets.QLabel(self)\n        lbl_rest_time.setTextFormat(QtCore.Qt.RichText)\n        lbl_rest_time.setWordWrap(True)\n        lbl_rest_time.setAlignment(QtCore.Qt.AlignCenter)\n\n        form = QtWidgets.QFormLayout()\n        form.setContentsMargins(10, 15, 10, 5)\n\n        form.addRow(lbl_info)\n        form.addRow(lbl_question)\n        form.addRow(lbl_stopped)\n        form.addRow(lbl_rest_time)\n\n        btn_stop = QtWidgets.QPushButton(\"Stop timer\", self)\n        btn_stop.setToolTip(\"Stop's All timers\")\n\n        btn_continue = QtWidgets.QPushButton(\"Continue\", self)\n        btn_continue.setToolTip(\"Timer won't stop\")\n\n        btn_close = QtWidgets.QPushButton(\"Close\", self)\n        btn_close.setToolTip(\"Close window\")\n\n        btn_restart = QtWidgets.QPushButton(\"Start timers\", self)\n        btn_restart.setToolTip(\"Timer will be started again\")\n\n        group_layout = QtWidgets.QHBoxLayout()\n        group_layout.addStretch(1)\n        group_layout.addWidget(btn_continue)\n        group_layout.addWidget(btn_stop)\n        group_layout.addWidget(btn_restart)\n        group_layout.addWidget(btn_close)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addLayout(form)\n        layout.addLayout(group_layout)\n\n        count_timer = QtCore.QTimer()\n        count_timer.setInterval(1000)\n\n        btn_stop.clicked.connect(self._on_stop_clicked)\n        btn_continue.clicked.connect(self._on_continue_clicked)\n        btn_close.clicked.connect(self._close_widget)\n        btn_restart.clicked.connect(self._on_restart_clicked)\n        count_timer.timeout.connect(self._on_count_timeout)\n\n        self.lbl_info = lbl_info\n        self.lbl_question = lbl_question\n        self.lbl_stopped = lbl_stopped\n        self.lbl_rest_time = lbl_rest_time\n\n        self.btn_stop = btn_stop\n        self.btn_continue = btn_continue\n        self.btn_close = btn_close\n        self.btn_restart = btn_restart\n\n        self._count_timer = count_timer\n\n        self.resize(self.SIZE_W, self.SIZE_H)\n        self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))\n        self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100))\n        self.setStyleSheet(style.load_stylesheet())\n\n    def set_countdown_start(self, countdown):\n        self._countdown_start = countdown\n        if not self.is_showed():\n            self.reset_countdown()\n\n    def reset_countdown(self):\n        self._countdown = self._countdown_start\n        self._update_countdown_label()\n\n    def is_showed(self):\n        return self._is_showed\n\n    def set_timer_stopped(self):\n        self._timer_stopped = True\n        self._refresh_context()\n\n    def _update_countdown_label(self):\n        self.lbl_rest_time.setText(str(self._countdown))\n\n    def _on_count_timeout(self):\n        if self._timer_stopped or not self._is_showed:\n            self._count_timer.stop()\n            return\n\n        if self._countdown <= 0:\n            self._stop_timers()\n            self.set_timer_stopped()\n        else:\n            self._countdown -= 1\n            self._update_countdown_label()\n\n    def _refresh_context(self):\n        self.lbl_question.setVisible(not self._timer_stopped)\n        self.lbl_rest_time.setVisible(not self._timer_stopped)\n        self.lbl_stopped.setVisible(self._timer_stopped)\n\n        self.btn_continue.setVisible(not self._timer_stopped)\n        self.btn_stop.setVisible(not self._timer_stopped)\n        self.btn_restart.setVisible(self._timer_stopped)\n        self.btn_close.setVisible(self._timer_stopped)\n\n    def _stop_timers(self):\n        self.module.stop_timers()\n\n    def _on_stop_clicked(self):\n        self._stop_timers()\n        self._close_widget()\n\n    def _on_restart_clicked(self):\n        self.module.restart_timers()\n        self._close_widget()\n\n    def _on_continue_clicked(self):\n        self._close_widget()\n\n    def _close_widget(self):\n        self._is_showed = False\n        self._timer_stopped = False\n        self._refresh_context()\n        self.hide()\n\n    def showEvent(self, event):\n        if not self._is_showed:\n            self._is_showed = True\n            self._refresh_context()\n\n        if not self._count_timer.isActive():\n            self._count_timer.start()\n        super(WidgetUserIdle, self).showEvent(event)\n\n    def closeEvent(self, event):\n        event.ignore()\n        if self._timer_stopped:\n            self._close_widget()\n        else:\n            self._on_continue_clicked()\n\n\nclass SignalHandler(QtCore.QObject):\n    signal_show_message = QtCore.Signal()\n    signal_stop_timers = QtCore.Signal()\n\n    def __init__(self, module):\n        super(SignalHandler, self).__init__()\n        self.module = module\n        self.signal_show_message.connect(module.show_message)\n        self.signal_stop_timers.connect(module.stop_timers)\n"
  },
  {
    "path": "openpype/modules/webserver/__init__.py",
    "content": "from .webserver_module import (\n    WebServerModule\n)\n\n\n__all__ = (\n    \"WebServerModule\",\n)\n"
  },
  {
    "path": "openpype/modules/webserver/base_routes.py",
    "content": "\"\"\"Helper functions or classes for Webserver module.\n\nThese must not be imported in module itself to not break Python 2\napplications.\n\"\"\"\n\nimport inspect\nfrom aiohttp.http_exceptions import HttpBadRequest\nfrom aiohttp.web_exceptions import HTTPMethodNotAllowed\nfrom aiohttp.web_request import Request\n\n\nDEFAULT_METHODS = (\"GET\", \"POST\", \"PUT\", \"DELETE\")\n\n\nclass RestApiEndpoint:\n    \"\"\"Helper endpoint class for single endpoint.\n\n    Class can define `get`, `post`, `put` or `delete` async methods for the\n    endpoint.\n    \"\"\"\n    def __init__(self):\n        methods = {}\n\n        for method_name in DEFAULT_METHODS:\n            method = getattr(self, method_name.lower(), None)\n            if method:\n                methods[method_name.upper()] = method\n\n        self.methods = methods\n\n    async def dispatch(self, request: Request):\n        method = self.methods.get(request.method.upper())\n        if not method:\n            raise HTTPMethodNotAllowed(\"\", DEFAULT_METHODS)\n\n        wanted_args = list(inspect.signature(method).parameters.keys())\n\n        available_args = request.match_info.copy()\n        available_args[\"request\"] = request\n\n        unsatisfied_args = set(wanted_args) - set(available_args.keys())\n        if unsatisfied_args:\n            # Expected match info that doesn't exist\n            raise HttpBadRequest(\"\")\n\n        return await method(**{\n            arg_name: available_args[arg_name]\n            for arg_name in wanted_args\n        })\n"
  },
  {
    "path": "openpype/modules/webserver/cors_middleware.py",
    "content": "r\"\"\"\n===============\nCORS Middleware\n===============\n.. versionadded:: 0.2.0\nDealing with CORS headers for aiohttp applications.\n**IMPORTANT:** There is a `aiohttp-cors\n<https://pypi.org/project/aiohttp_cors/>`_ library, which handles CORS\nheaders by attaching additional handlers to aiohttp application for\nOPTIONS (preflight) requests. In same time this CORS middleware mimics the\nlogic of `django-cors-headers <https://pypi.org/project/django-cors-headers>`_,\nwhere all handling done in the middleware without any additional handlers. This\napproach allows aiohttp application to respond with CORS headers for OPTIONS or\nwildcard handlers, which is not possible with ``aiohttp-cors`` due to\nhttps://github.com/aio-libs/aiohttp-cors/issues/241 issue.\nFor detailed information about CORS (Cross Origin Resource Sharing) please\nvisit:\n- `Wikipedia <https://en.m.wikipedia.org/wiki/Cross-origin_resource_sharing>`_\n- Or `MDN <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_\nConfiguration\n=============\n**IMPORTANT:** By default, CORS middleware do not allow any origins to access\ncontent from your aiohttp appliction. Which means, you need carefully check\npossible options and provide custom values for your needs.\nUsage\n=====\n.. code-block:: python\n    import re\n    from aiohttp import web\n    from aiohttp_middlewares import cors_middleware\n    from aiohttp_middlewares.cors import DEFAULT_ALLOW_HEADERS\n    # Unsecure configuration to allow all CORS requests\n    app = web.Application(\n        middlewares=[cors_middleware(allow_all=True)]\n    )\n    # Allow CORS requests from URL http://localhost:3000\n    app = web.Application(\n        middlewares=[\n            cors_middleware(origins=[\"http://localhost:3000\"])\n        ]\n    )\n    # Allow CORS requests from all localhost urls\n    app = web.Application(\n        middlewares=[\n            cors_middleware(\n                origins=[re.compile(r\"^https?\\:\\/\\/localhost\")]\n            )\n        ]\n    )\n    # Allow CORS requests from https://frontend.myapp.com as well\n    # as allow credentials\n    CORS_ALLOW_ORIGINS = [\"https://frontend.myapp.com\"]\n    app = web.Application(\n        middlewares=[\n            cors_middleware(\n                origins=CORS_ALLOW_ORIGINS,\n                allow_credentials=True,\n            )\n        ]\n    )\n    # Allow CORS requests only for API urls\n    app = web.Application(\n        middelwares=[\n            cors_middleware(\n                origins=CORS_ALLOW_ORIGINS,\n                urls=[re.compile(r\"^\\/api\")],\n            )\n        ]\n    )\n    # Allow CORS requests for POST & PATCH methods, and for all\n    # default headers and `X-Client-UID`\n    app = web.Application(\n        middlewares=[\n            cors_middleware(\n                origings=CORS_ALLOW_ORIGINS,\n                allow_methods=(\"POST\", \"PATCH\"),\n                allow_headers=DEFAULT_ALLOW_HEADERS\n                + (\"X-Client-UID\",),\n            )\n        ]\n    )\n\"\"\"\n\nimport logging\nimport re\nfrom typing import Pattern, Tuple\n\nfrom aiohttp import web\n\nfrom aiohttp_middlewares.annotations import (\n    Handler,\n    Middleware,\n    StrCollection,\n    UrlCollection,\n)\nfrom aiohttp_middlewares.utils import match_path\n\n\nACCESS_CONTROL = \"Access-Control\"\nACCESS_CONTROL_ALLOW = f\"{ACCESS_CONTROL}-Allow\"\nACCESS_CONTROL_ALLOW_CREDENTIALS = f\"{ACCESS_CONTROL_ALLOW}-Credentials\"\nACCESS_CONTROL_ALLOW_HEADERS = f\"{ACCESS_CONTROL_ALLOW}-Headers\"\nACCESS_CONTROL_ALLOW_METHODS = f\"{ACCESS_CONTROL_ALLOW}-Methods\"\nACCESS_CONTROL_ALLOW_ORIGIN = f\"{ACCESS_CONTROL_ALLOW}-Origin\"\nACCESS_CONTROL_EXPOSE_HEADERS = f\"{ACCESS_CONTROL}-Expose-Headers\"\nACCESS_CONTROL_MAX_AGE = f\"{ACCESS_CONTROL}-Max-Age\"\nACCESS_CONTROL_REQUEST_METHOD = f\"{ACCESS_CONTROL}-Request-Method\"\n\nDEFAULT_ALLOW_HEADERS = (\n    \"accept\",\n    \"accept-encoding\",\n    \"authorization\",\n    \"content-type\",\n    \"dnt\",\n    \"origin\",\n    \"user-agent\",\n    \"x-csrftoken\",\n    \"x-requested-with\",\n)\nDEFAULT_ALLOW_METHODS = (\"DELETE\", \"GET\", \"OPTIONS\", \"PATCH\", \"POST\", \"PUT\")\nDEFAULT_URLS: Tuple[Pattern[str]] = (re.compile(r\".*\"),)\n\nlogger = logging.getLogger(__name__)\n\n\ndef cors_middleware(\n    *,\n    allow_all: bool = False,\n    origins: UrlCollection = None,\n    urls: UrlCollection = None,\n    expose_headers: StrCollection = None,\n    allow_headers: StrCollection = DEFAULT_ALLOW_HEADERS,\n    allow_methods: StrCollection = DEFAULT_ALLOW_METHODS,\n    allow_credentials: bool = False,\n    max_age: int = None,\n) -> Middleware:\n    \"\"\"Middleware to provide CORS headers for aiohttp applications.\n    :param allow_all:\n        When enabled, allow any Origin to access content from your aiohttp web\n        application. **Please be careful with enabling this option as it may\n        result in security issues for your application.** By default: ``False``\n    :param origins:\n        Allow content access for given list of origins. Support supplying\n        strings for exact origin match or regex instances. By default: ``None``\n    :param urls:\n        Allow contect access for given list of URLs in aiohttp application.\n        By default: *apply CORS headers for all URLs*\n    :param expose_headers:\n        List of headers to be exposed with every CORS request. By default:\n        ``None``\n    :param allow_headers:\n        List of allowed headers. By default:\n        .. code-block:: python\n            (\n                \"accept\",\n                \"accept-encoding\",\n                \"authorization\",\n                \"content-type\",\n                \"dnt\",\n                \"origin\",\n                \"user-agent\",\n                \"x-csrftoken\",\n                \"x-requested-with\",\n            )\n    :param allow_methods:\n        List of allowed methods. By default:\n        .. code-block:: python\n            (\"DELETE\", \"GET\", \"OPTIONS\", \"PATCH\", \"POST\", \"PUT\")\n    :param allow_credentials:\n        When enabled apply allow credentials header in response, which results\n        in sharing cookies on shared resources. **Please be careful with\n        allowing credentials for CORS requests.** By default: ``False``\n    :param max_age: Access control max age in seconds. By default: ``None``\n    \"\"\"\n    check_urls: UrlCollection = DEFAULT_URLS if urls is None else urls\n\n    @web.middleware\n    async def middleware(\n        request: web.Request, handler: Handler\n    ) -> web.StreamResponse:\n        # Initial vars\n        request_method = request.method\n        request_path = request.rel_url.path\n\n        # Is this an OPTIONS request\n        is_options_request = request_method == \"OPTIONS\"\n\n        # Is this a preflight request\n        is_preflight_request = (\n            is_options_request\n            and ACCESS_CONTROL_REQUEST_METHOD in request.headers\n        )\n\n        # Log extra data\n        log_extra = {\n            \"is_preflight_request\": is_preflight_request,\n            \"method\": request_method.lower(),\n            \"path\": request_path,\n        }\n\n        # Check whether CORS should be enabled for given URL or not. By default\n        # CORS enabled for all URLs\n        if not match_items(check_urls, request_path):\n            logger.debug(\n                \"Request should not be processed via CORS middleware\",\n                extra=log_extra,\n            )\n            return await handler(request)\n\n        # If this is a preflight request - generate empty response\n        if is_preflight_request:\n            response = web.StreamResponse()\n        # Otherwise - call actual handler\n        else:\n            response = await handler(request)\n\n        # Now check origin heaer\n        origin = request.headers.get(\"Origin\")\n        # Empty origin - do nothing\n        if not origin:\n            logger.debug(\n                \"Request does not have Origin header. CORS headers not \"\n                \"available for given requests\",\n                extra=log_extra,\n            )\n            return response\n\n        # Set allow credentials header if necessary\n        if allow_credentials:\n            response.headers[ACCESS_CONTROL_ALLOW_CREDENTIALS] = \"true\"\n\n        # Check whether current origin satisfies CORS policy\n        if not allow_all and not (origins and match_items(origins, origin)):\n            logger.debug(\n                \"CORS headers not allowed for given Origin\", extra=log_extra\n            )\n            return response\n\n        # Now start supplying CORS headers\n        # First one is Access-Control-Allow-Origin\n        if allow_all and not allow_credentials:\n            cors_origin = \"*\"\n        else:\n            cors_origin = origin\n        response.headers[ACCESS_CONTROL_ALLOW_ORIGIN] = cors_origin\n\n        # Then Access-Control-Expose-Headers\n        if expose_headers:\n            response.headers[ACCESS_CONTROL_EXPOSE_HEADERS] = \", \".join(\n                expose_headers\n            )\n\n        # Now, if this is an options request, respond with extra Allow headers\n        if is_options_request:\n            response.headers[ACCESS_CONTROL_ALLOW_HEADERS] = \", \".join(\n                allow_headers\n            )\n            response.headers[ACCESS_CONTROL_ALLOW_METHODS] = \", \".join(\n                allow_methods\n            )\n            if max_age is not None:\n                response.headers[ACCESS_CONTROL_MAX_AGE] = str(max_age)\n\n        # If this is preflight request - do not allow other middlewares to\n        # process this request\n        if is_preflight_request:\n            logger.debug(\n                \"Provide CORS headers with empty response for preflight \"\n                \"request\",\n                extra=log_extra,\n            )\n            raise web.HTTPOk(text=\"\", headers=response.headers)\n\n        # Otherwise return normal response\n        logger.debug(\"Provide CORS headers for request\", extra=log_extra)\n        return response\n\n    return middleware\n\n\ndef match_items(items: UrlCollection, value: str) -> bool:\n    \"\"\"Go through all items and try to match item with given value.\"\"\"\n    return any(match_path(item, value) for item in items)\n"
  },
  {
    "path": "openpype/modules/webserver/host_console_listener.py",
    "content": "import aiohttp\nfrom aiohttp import web\nimport json\nimport logging\nfrom concurrent.futures import CancelledError\nfrom qtpy import QtWidgets\n\nfrom openpype.modules import ITrayService\n\nlog = logging.getLogger(__name__)\n\n\nclass IconType:\n    IDLE = \"idle\"\n    RUNNING = \"running\"\n    FAILED = \"failed\"\n\n\nclass MsgAction:\n    CONNECTING = \"connecting\"\n    INITIALIZED = \"initialized\"\n    ADD = \"add\"\n    CLOSE = \"close\"\n\n\nclass HostListener:\n    def __init__(self, webserver, module):\n        self._window_per_id = {}\n        self.module = module\n        self.webserver = webserver\n        self._window_per_id = {}  # dialogs per host name\n        self._action_per_id = {}  # QAction per host name\n\n        webserver.add_route('*', \"/ws/host_listener\", self.websocket_handler)\n\n    def _host_is_connecting(self, host_name, label):\n        from openpype.tools.stdout_broker.window import ConsoleDialog\n        \"\"\" Initialize dialog, adds to submenu. \"\"\"\n        services_submenu = self.module._services_submenu\n        action = QtWidgets.QAction(label, services_submenu)\n        action.triggered.connect(lambda: self.show_widget(host_name))\n\n        services_submenu.addAction(action)\n        self._action_per_id[host_name] = action\n        self._set_host_icon(host_name, IconType.IDLE)\n        widget = ConsoleDialog(\"\")\n        self._window_per_id[host_name] = widget\n\n    def _set_host_icon(self, host_name, icon_type):\n        \"\"\"Assigns icon to action for 'host_name' with 'icon_type'.\n\n            Action must exist in self._action_per_id\n\n            Args:\n                host_name (str)\n                icon_type (IconType)\n        \"\"\"\n        action = self._action_per_id.get(host_name)\n        if not action:\n            raise ValueError(\"Unknown host {}\".format(host_name))\n\n        icon = None\n        if icon_type == IconType.IDLE:\n            icon = ITrayService.get_icon_idle()\n        elif icon_type == IconType.RUNNING:\n            icon = ITrayService.get_icon_running()\n        elif icon_type == IconType.FAILED:\n            icon = ITrayService.get_icon_failed()\n        else:\n            log.info(\"Unknown icon type {} for {}\".format(icon_type,\n                                                          host_name))\n        action.setIcon(icon)\n\n    def show_widget(self, host_name):\n        \"\"\"Shows prepared widget for 'host_name'.\n\n            Dialog get initialized when 'host_name' is connecting.\n        \"\"\"\n        self.module.execute_in_main_thread(\n            lambda: self._show_widget(host_name))\n\n    def _show_widget(self, host_name):\n        widget = self._window_per_id[host_name]\n        widget.show()\n        widget.raise_()\n        widget.activateWindow()\n\n    async def websocket_handler(self, request):\n        ws = web.WebSocketResponse()\n        await ws.prepare(request)\n\n        widget = None\n        try:\n            async for msg in ws:\n                if msg.type == aiohttp.WSMsgType.TEXT:\n                    host_name, action, text = self._parse_message(msg)\n\n                    if action == MsgAction.CONNECTING:\n                        self._action_per_id[host_name] = None\n                        # must be sent to main thread, or action wont trigger\n                        self.module.execute_in_main_thread(\n                            lambda: self._host_is_connecting(host_name, text))\n                    elif action == MsgAction.CLOSE:\n                        # clean close\n                        self._close(host_name)\n                        await ws.close()\n                    elif action == MsgAction.INITIALIZED:\n                        self.module.execute_in_main_thread(\n                            # must be queued as _host_is_connecting might not\n                            # be triggered/finished yet\n                            lambda: self._set_host_icon(host_name,\n                                                        IconType.RUNNING))\n                    elif action == MsgAction.ADD:\n                        self.module.execute_in_main_thread(\n                            lambda: self._add_text(host_name, text))\n                elif msg.type == aiohttp.WSMsgType.ERROR:\n                    print('ws connection closed with exception %s' %\n                          ws.exception())\n                    host_name, _, _ = self._parse_message(msg)\n                    self._set_host_icon(host_name, IconType.FAILED)\n        except CancelledError:  # recoverable\n            pass\n        except Exception as exc:\n            log.warning(\"Exception during communication\", exc_info=True)\n            if widget:\n                error_msg = str(exc)\n                widget.append_text(error_msg)\n\n        return ws\n\n    def _add_text(self, host_name, text):\n        widget = self._window_per_id[host_name]\n        widget.append_text(text)\n\n    def _close(self, host_name):\n        \"\"\" Clean close - remove from menu, delete widget.\"\"\"\n        services_submenu = self.module._services_submenu\n        action = self._action_per_id.pop(host_name)\n        services_submenu.removeAction(action)\n        widget = self._window_per_id.pop(host_name)\n        if widget.isVisible():\n            widget.hide()\n        widget.deleteLater()\n\n    def _parse_message(self, msg):\n        data = json.loads(msg.data)\n        action = data.get(\"action\")\n        host_name = data[\"host\"]\n        value = data.get(\"text\")\n\n        return host_name, action, value\n"
  },
  {
    "path": "openpype/modules/webserver/server.py",
    "content": "import re\nimport threading\nimport asyncio\n\nfrom aiohttp import web\n\nfrom openpype.lib import Logger\nfrom .cors_middleware import cors_middleware\n\n\nclass WebServerManager:\n    \"\"\"Manger that care about web server thread.\"\"\"\n\n    def __init__(self, port=None, host=None):\n        self._log = None\n\n        self.port = port or 8079\n        self.host = host or \"localhost\"\n\n        self.client = None\n        self.handlers = {}\n        self.on_stop_callbacks = []\n\n        self.app = web.Application(\n            middlewares=[\n                cors_middleware(\n                    origins=[re.compile(r\"^https?\\:\\/\\/localhost\")]\n                )\n            ]\n        )\n\n        # add route with multiple methods for single \"external app\"\n\n        self.webserver_thread = WebServerThread(self)\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    @property\n    def url(self):\n        return \"http://{}:{}\".format(self.host, self.port)\n\n    def add_route(self, *args, **kwargs):\n        self.app.router.add_route(*args, **kwargs)\n\n    def add_static(self, *args, **kwargs):\n        self.app.router.add_static(*args, **kwargs)\n\n    def start_server(self):\n        if self.webserver_thread and not self.webserver_thread.is_alive():\n            self.webserver_thread.start()\n\n    def stop_server(self):\n        if not self.is_running:\n            return\n        try:\n            self.log.debug(\"Stopping Web server\")\n            self.webserver_thread.is_running = False\n            self.webserver_thread.stop()\n\n        except Exception:\n            self.log.warning(\n                \"Error has happened during Killing Web server\",\n                exc_info=True\n            )\n\n    @property\n    def is_running(self):\n        if not self.webserver_thread:\n            return False\n        return self.webserver_thread.is_running\n\n    def thread_stopped(self):\n        for callback in self.on_stop_callbacks:\n            callback()\n\n\nclass WebServerThread(threading.Thread):\n    \"\"\" Listener for requests in thread.\"\"\"\n\n    def __init__(self, manager):\n        self._log = None\n\n        super(WebServerThread, self).__init__()\n\n        self.is_running = False\n        self.manager = manager\n        self.loop = None\n        self.runner = None\n        self.site = None\n        self.tasks = []\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    @property\n    def port(self):\n        return self.manager.port\n\n    @property\n    def host(self):\n        return self.manager.host\n\n    def run(self):\n        self.is_running = True\n\n        try:\n            self.log.info(\"Starting WebServer server\")\n            self.loop = asyncio.new_event_loop()  # create new loop for thread\n            asyncio.set_event_loop(self.loop)\n\n            self.loop.run_until_complete(self.start_server())\n\n            self.log.debug(\n                \"Running Web server on URL: \\\"localhost:{}\\\"\".format(self.port)\n            )\n\n            asyncio.ensure_future(self.check_shutdown(), loop=self.loop)\n            self.loop.run_forever()\n\n        except Exception:\n            self.log.warning(\n                \"Web Server service has failed\", exc_info=True\n            )\n        finally:\n            self.loop.close()  # optional\n\n        self.is_running = False\n        self.manager.thread_stopped()\n        self.log.info(\"Web server stopped\")\n\n    async def start_server(self):\n        \"\"\" Starts runner and TCPsite \"\"\"\n        self.runner = web.AppRunner(self.manager.app)\n        await self.runner.setup()\n        self.site = web.TCPSite(self.runner, self.host, self.port)\n        await self.site.start()\n\n    def stop(self):\n        \"\"\"Sets is_running flag to false, 'check_shutdown' shuts server down\"\"\"\n        self.is_running = False\n\n    async def check_shutdown(self):\n        \"\"\" Future that is running and checks if server should be running\n            periodically.\n        \"\"\"\n        while self.is_running:\n            while self.tasks:\n                task = self.tasks.pop(0)\n                self.log.debug(\"waiting for task {}\".format(task))\n                await task\n                self.log.debug(\"returned value {}\".format(task.result))\n\n            await asyncio.sleep(0.5)\n\n        self.log.debug(\"Starting shutdown\")\n        await self.site.stop()\n        self.log.debug(\"Site stopped\")\n        await self.runner.cleanup()\n        self.log.debug(\"Runner stopped\")\n        tasks = [\n            task\n            for task in asyncio.all_tasks()\n            if task is not asyncio.current_task()\n        ]\n        list(map(lambda task: task.cancel(), tasks))  # cancel all the tasks\n        results = await asyncio.gather(*tasks, return_exceptions=True)\n        self.log.debug(\n            f'Finished awaiting cancelled tasks, results: {results}...'\n        )\n        await self.loop.shutdown_asyncgens()\n        # to really make sure everything else has time to stop\n        await asyncio.sleep(0.07)\n        self.loop.stop()\n"
  },
  {
    "path": "openpype/modules/webserver/webserver_module.py",
    "content": "\"\"\"WebServerModule spawns aiohttp server in asyncio loop.\n\nMain usage of the module is in OpenPype tray where make sense to add ability\nof other modules to add theirs routes. Module which would want use that\noption must have implemented method `webserver_initialization` which must\nexpect `WebServerManager` object where is possible to add routes or paths\nwith handlers.\n\nWebServerManager is by default created only in tray.\n\nIt is possible to create server manager without using module logic at all\nusing `create_new_server_manager`. That can be handy for standalone scripts\nwith predefined host and port and separated routes and logic.\n\nRunning multiple servers in one process is not recommended and probably won't\nwork as expected. It is because of few limitations connected to asyncio module.\n\nWhen module's `create_server_manager` is called it is also set environment\nvariable \"OPENPYPE_WEBSERVER_URL\". Which should lead to root access point\nof server.\n\"\"\"\n\nimport os\nimport socket\n\nfrom openpype import resources\nfrom openpype.modules import OpenPypeModule, ITrayService\n\n\nclass WebServerModule(OpenPypeModule, ITrayService):\n    name = \"webserver\"\n    label = \"WebServer\"\n\n    webserver_url_env = \"OPENPYPE_WEBSERVER_URL\"\n\n    def initialize(self, _module_settings):\n        self.enabled = True\n        self.server_manager = None\n        self._host_listener = None\n\n        self.port = self.find_free_port()\n        self.webserver_url = None\n\n    def connect_with_modules(self, enabled_modules):\n        if not self.server_manager:\n            return\n\n        for module in enabled_modules:\n            if not hasattr(module, \"webserver_initialization\"):\n                continue\n\n            try:\n                module.webserver_initialization(self.server_manager)\n            except Exception:\n                self.log.warning(\n                    (\n                        \"Failed to connect module \\\"{}\\\" to webserver.\"\n                    ).format(module.name),\n                    exc_info=True\n                )\n\n    def tray_init(self):\n        self.create_server_manager()\n        self._add_resources_statics()\n        self._add_listeners()\n\n    def tray_start(self):\n        self.start_server()\n\n    def tray_exit(self):\n        self.stop_server()\n\n    def _add_resources_statics(self):\n        static_prefix = \"/res\"\n        self.server_manager.add_static(static_prefix, resources.RESOURCES_DIR)\n\n        os.environ[\"OPENPYPE_STATICS_SERVER\"] = \"{}{}\".format(\n            self.webserver_url, static_prefix\n        )\n\n    def _add_listeners(self):\n        from openpype_modules.webserver import host_console_listener\n\n        self._host_listener = host_console_listener.HostListener(\n            self.server_manager, self\n        )\n\n    def start_server(self):\n        if self.server_manager:\n            self.server_manager.start_server()\n\n    def stop_server(self):\n        if self.server_manager:\n            self.server_manager.stop_server()\n\n    @staticmethod\n    def create_new_server_manager(port=None, host=None):\n        \"\"\"Create webserver manager for passed port and host.\n\n        Args:\n            port(int): Port on which wil webserver listen.\n            host(str): Host name or IP address. Default is 'localhost'.\n\n        Returns:\n            WebServerManager: Prepared manager.\n        \"\"\"\n        from .server import WebServerManager\n\n        return WebServerManager(port, host)\n\n    def create_server_manager(self):\n        if self.server_manager:\n            return\n\n        self.server_manager = self.create_new_server_manager(self.port)\n        self.server_manager.on_stop_callbacks.append(\n            self.set_service_failed_icon\n        )\n\n        webserver_url = self.server_manager.url\n        os.environ[self.webserver_url_env] = str(webserver_url)\n        self.webserver_url = webserver_url\n\n    @staticmethod\n    def find_free_port(\n        port_from=None, port_to=None, exclude_ports=None, host=None\n    ):\n        \"\"\"Find available socket port from entered range.\n\n        It is also possible to only check if entered port is available.\n\n        Args:\n            port_from (int): Port number which is checked as first.\n            port_to (int): Last port that is checked in sequence from entered\n                `port_from`. Only `port_from` is checked if is not entered.\n                Nothing is processed if is equeal to `port_from`!\n            exclude_ports (list, tuple, set): List of ports that won't be\n                checked form entered range.\n            host (str): Host where will check for free ports. Set to\n                \"localhost\" by default.\n        \"\"\"\n        if port_from is None:\n            port_from = 8079\n\n        if port_to is None:\n            port_to = 65535\n\n        # Excluded ports (e.g. reserved for other servers/clients)\n        if exclude_ports is None:\n            exclude_ports = []\n\n        # Default host is localhost but it is possible to look for other hosts\n        if host is None:\n            host = \"localhost\"\n\n        found_port = None\n        for port in range(port_from, port_to + 1):\n            if port in exclude_ports:\n                continue\n\n            sock = None\n            try:\n                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n                sock.bind((host, port))\n                found_port = port\n\n            except socket.error:\n                continue\n\n            finally:\n                if sock:\n                    sock.close()\n\n            if found_port is not None:\n                break\n\n        return found_port\n"
  },
  {
    "path": "openpype/pipeline/__init__.py",
    "content": "from .constants import (\n    AVALON_CONTAINER_ID,\n    AYON_CONTAINER_ID,\n    HOST_WORKFILE_EXTENSIONS,\n)\n\nfrom .mongodb import (\n    AvalonMongoDB,\n)\nfrom .anatomy import Anatomy\n\nfrom .create import (\n    BaseCreator,\n    Creator,\n    AutoCreator,\n    HiddenCreator,\n    CreatedInstance,\n    CreatorError,\n\n    LegacyCreator,\n    legacy_create,\n\n    discover_creator_plugins,\n    discover_legacy_creator_plugins,\n    register_creator_plugin,\n    deregister_creator_plugin,\n    register_creator_plugin_path,\n    deregister_creator_plugin_path,\n)\n\nfrom .load import (\n    HeroVersionType,\n    IncompatibleLoaderError,\n    LoaderPlugin,\n    SubsetLoaderPlugin,\n\n    discover_loader_plugins,\n    register_loader_plugin,\n    deregister_loader_plugin_path,\n    register_loader_plugin_path,\n    deregister_loader_plugin,\n\n    load_container,\n    remove_container,\n    update_container,\n    switch_container,\n\n    loaders_from_representation,\n    get_representation_path,\n    get_representation_context,\n    get_repres_contexts,\n)\n\nfrom .publish import (\n    PublishValidationError,\n    PublishXmlValidationError,\n    KnownPublishError,\n    OpenPypePyblishPluginMixin,\n    OptionalPyblishPluginMixin,\n)\n\nfrom .actions import (\n    LauncherAction,\n\n    InventoryAction,\n\n    discover_launcher_actions,\n    register_launcher_action,\n    register_launcher_action_path,\n\n    discover_inventory_actions,\n    register_inventory_action,\n    register_inventory_action_path,\n    deregister_inventory_action,\n    deregister_inventory_action_path,\n)\n\nfrom .context_tools import (\n    install_openpype_plugins,\n    install_host,\n    uninstall_host,\n    is_installed,\n\n    register_root,\n    registered_root,\n\n    register_host,\n    registered_host,\n    deregister_host,\n    get_process_id,\n\n    get_global_context,\n    get_current_context,\n    get_current_host_name,\n    get_current_project_name,\n    get_current_asset_name,\n    get_current_task_name\n)\ninstall = install_host\nuninstall = uninstall_host\n\n\n__all__ = (\n    \"AVALON_CONTAINER_ID\",\n    \"AYON_CONTAINER_ID\",\n    \"HOST_WORKFILE_EXTENSIONS\",\n\n    # --- MongoDB ---\n    \"AvalonMongoDB\",\n\n    # --- Anatomy ---\n    \"Anatomy\",\n\n    # --- Create ---\n    \"BaseCreator\",\n    \"Creator\",\n    \"AutoCreator\",\n    \"HiddenCreator\",\n    \"CreatedInstance\",\n    \"CreatorError\",\n\n    \"CreatorError\",\n\n    # - legacy creation\n    \"LegacyCreator\",\n    \"legacy_create\",\n\n    \"discover_creator_plugins\",\n    \"discover_legacy_creator_plugins\",\n    \"register_creator_plugin\",\n    \"deregister_creator_plugin\",\n    \"register_creator_plugin_path\",\n    \"deregister_creator_plugin_path\",\n\n    # --- Load ---\n    \"HeroVersionType\",\n    \"IncompatibleLoaderError\",\n    \"LoaderPlugin\",\n    \"SubsetLoaderPlugin\",\n\n    \"discover_loader_plugins\",\n    \"register_loader_plugin\",\n    \"deregister_loader_plugin_path\",\n    \"register_loader_plugin_path\",\n    \"deregister_loader_plugin\",\n\n    \"load_container\",\n    \"remove_container\",\n    \"update_container\",\n    \"switch_container\",\n\n    \"loaders_from_representation\",\n    \"get_representation_path\",\n    \"get_representation_context\",\n    \"get_repres_contexts\",\n\n    # --- Publish ---\n    \"PublishValidationError\",\n    \"PublishXmlValidationError\",\n    \"KnownPublishError\",\n    \"OpenPypePyblishPluginMixin\",\n    \"OptionalPyblishPluginMixin\",\n\n    # --- Actions ---\n    \"LauncherAction\",\n    \"InventoryAction\",\n\n    \"discover_launcher_actions\",\n    \"register_launcher_action\",\n    \"register_launcher_action_path\",\n\n    \"discover_inventory_actions\",\n    \"register_inventory_action\",\n    \"register_inventory_action_path\",\n    \"deregister_inventory_action\",\n    \"deregister_inventory_action_path\",\n\n    # --- Process context ---\n    \"install_openpype_plugins\",\n    \"install_host\",\n    \"uninstall_host\",\n    \"is_installed\",\n\n    \"register_root\",\n    \"registered_root\",\n\n    \"register_host\",\n    \"registered_host\",\n    \"deregister_host\",\n    \"get_process_id\",\n\n    \"get_global_context\",\n    \"get_current_context\",\n    \"get_current_host_name\",\n    \"get_current_project_name\",\n    \"get_current_asset_name\",\n    \"get_current_task_name\",\n\n    # Backwards compatible function names\n    \"install\",\n    \"uninstall\",\n)\n"
  },
  {
    "path": "openpype/pipeline/actions.py",
    "content": "import logging\nfrom openpype.pipeline.plugin_discover import (\n    discover,\n    register_plugin,\n    register_plugin_path,\n    deregister_plugin,\n    deregister_plugin_path\n)\n\nfrom .load.utils import get_representation_path_from_context\n\n\nclass LauncherAction(object):\n    \"\"\"A custom action available\"\"\"\n    name = None\n    label = None\n    icon = None\n    color = None\n    order = 0\n\n    log = logging.getLogger(\"LauncherAction\")\n    log.propagate = True\n\n    def is_compatible(self, session):\n        \"\"\"Return whether the class is compatible with the Session.\n\n        Args:\n            session (dict[str, Union[str, None]]): Session data with\n                AVALON_PROJECT, AVALON_ASSET and AVALON_TASK.\n        \"\"\"\n\n        return True\n\n    def process(self, session, **kwargs):\n        pass\n\n\nclass InventoryAction(object):\n    \"\"\"A custom action for the scene inventory tool\n\n    If registered the action will be visible in the Right Mouse Button menu\n    under the submenu \"Actions\".\n\n    \"\"\"\n\n    label = None\n    icon = None\n    color = None\n    order = 0\n\n    log = logging.getLogger(\"InventoryAction\")\n    log.propagate = True\n\n    @staticmethod\n    def is_compatible(container):\n        \"\"\"Override function in a custom class\n\n        This method is specifically used to ensure the action can operate on\n        the container.\n\n        Args:\n            container(dict): the data of a loaded asset, see host.ls()\n\n        Returns:\n            bool\n        \"\"\"\n        return bool(container.get(\"objectName\"))\n\n    def process(self, containers):\n        \"\"\"Override function in a custom class\n\n        This method will receive all containers even those which are\n        incompatible. It is advised to create a small filter along the lines\n        of this example:\n\n        valid_containers = filter(self.is_compatible(c) for c in containers)\n\n        The return value will need to be a True-ish value to trigger\n        the data_changed signal in order to refresh the view.\n\n        You can return a list of container names to trigger GUI to select\n        treeview items.\n\n        You can return a dict to carry extra GUI options. For example:\n            {\n                \"objectNames\": [container names...],\n                \"options\": {\"mode\": \"toggle\",\n                            \"clear\": False}\n            }\n        Currently workable GUI options are:\n            - clear (bool): Clear current selection before selecting by action.\n                            Default `True`.\n            - mode (str): selection mode, use one of these:\n                          \"select\", \"deselect\", \"toggle\". Default is \"select\".\n\n        Args:\n            containers (list): list of dictionaries\n\n        Return:\n            bool, list or dict\n\n        \"\"\"\n        return True\n\n    @classmethod\n    def filepath_from_context(cls, context):\n        return get_representation_path_from_context(context)\n\n\n# Launcher action\ndef discover_launcher_actions():\n    return discover(LauncherAction)\n\n\ndef register_launcher_action(plugin):\n    return register_plugin(LauncherAction, plugin)\n\n\ndef register_launcher_action_path(path):\n    return register_plugin_path(LauncherAction, path)\n\n\n# Inventory action\ndef discover_inventory_actions():\n    actions = discover(InventoryAction)\n    filtered_actions = []\n    for action in actions:\n        if action is not InventoryAction:\n            filtered_actions.append(action)\n\n    return filtered_actions\n\n\ndef register_inventory_action(plugin):\n    return register_plugin(InventoryAction, plugin)\n\n\ndef deregister_inventory_action(plugin):\n    deregister_plugin(InventoryAction, plugin)\n\n\ndef register_inventory_action_path(path):\n    return register_plugin_path(InventoryAction, path)\n\n\ndef deregister_inventory_action_path(path):\n    return deregister_plugin_path(InventoryAction, path)\n"
  },
  {
    "path": "openpype/pipeline/anatomy.py",
    "content": "import os\nimport re\nimport copy\nimport platform\nimport collections\nimport numbers\n\nimport six\nimport time\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.settings.lib import (\n    get_local_settings,\n)\nfrom openpype.settings.constants import (\n    DEFAULT_PROJECT_KEY\n)\nfrom openpype.client import get_project, get_ayon_server_api_connection\nfrom openpype.lib import Logger, get_local_site_id\nfrom openpype.lib.path_templates import (\n    TemplateUnsolved,\n    TemplateResult,\n    StringTemplate,\n    TemplatesDict,\n    FormatObject,\n)\nfrom openpype.modules import ModulesManager\n\nlog = Logger.get_logger(__name__)\n\n\nclass ProjectNotSet(Exception):\n    \"\"\"Exception raised when is created Anatomy without project name.\"\"\"\n\n\nclass RootCombinationError(Exception):\n    \"\"\"This exception is raised when templates has combined root types.\"\"\"\n\n    def __init__(self, roots):\n        joined_roots = \", \".join(\n            [\"\\\"{}\\\"\".format(_root) for _root in roots]\n        )\n        # TODO better error message\n        msg = (\n            \"Combination of root with and\"\n            \" without root name in AnatomyTemplates. {}\"\n        ).format(joined_roots)\n\n        super(RootCombinationError, self).__init__(msg)\n\n\nclass BaseAnatomy(object):\n    \"\"\"Anatomy module helps to keep project settings.\n\n    Wraps key project specifications, AnatomyTemplates and Roots.\n    \"\"\"\n    root_key_regex = re.compile(r\"{(root?[^}]+)}\")\n    root_name_regex = re.compile(r\"root\\[([^]]+)\\]\")\n\n    def __init__(self, project_doc, root_overrides=None):\n        project_name = project_doc[\"name\"]\n        self.project_name = project_name\n        self.project_code = project_doc[\"data\"][\"code\"]\n\n        self._data = self._prepare_anatomy_data(\n            project_doc, root_overrides\n        )\n        self._templates_obj = AnatomyTemplates(self)\n        self._roots_obj = Roots(self)\n\n    # Anatomy used as dictionary\n    # - implemented only getters returning copy\n    def __getitem__(self, key):\n        return copy.deepcopy(self._data[key])\n\n    def get(self, key, default=None):\n        return copy.deepcopy(self._data).get(key, default)\n\n    def keys(self):\n        return copy.deepcopy(self._data).keys()\n\n    def values(self):\n        return copy.deepcopy(self._data).values()\n\n    def items(self):\n        return copy.deepcopy(self._data).items()\n\n    def _prepare_anatomy_data(self, project_doc, root_overrides):\n        \"\"\"Prepare anatomy data for further processing.\n\n        Method added to replace `{task}` with `{task[name]}` in templates.\n        \"\"\"\n\n        anatomy_data = self._project_doc_to_anatomy_data(project_doc)\n\n        self._apply_local_settings_on_anatomy_data(\n            anatomy_data,\n            root_overrides\n        )\n\n        return anatomy_data\n\n    @property\n    def templates(self):\n        \"\"\"Wrap property `templates` of Anatomy's AnatomyTemplates instance.\"\"\"\n        return self._templates_obj.templates\n\n    @property\n    def templates_obj(self):\n        \"\"\"Return `AnatomyTemplates` object of current Anatomy instance.\"\"\"\n        return self._templates_obj\n\n    def format(self, *args, **kwargs):\n        \"\"\"Wrap `format` method of Anatomy's `templates_obj`.\"\"\"\n        return self._templates_obj.format(*args, **kwargs)\n\n    def format_all(self, *args, **kwargs):\n        \"\"\"Wrap `format_all` method of Anatomy's `templates_obj`.\"\"\"\n        return self._templates_obj.format_all(*args, **kwargs)\n\n    @property\n    def roots(self):\n        \"\"\"Wrap `roots` property of Anatomy's `roots_obj`.\"\"\"\n        return self._roots_obj.roots\n\n    @property\n    def roots_obj(self):\n        \"\"\"Return `Roots` object of current Anatomy instance.\"\"\"\n        return self._roots_obj\n\n    def root_environments(self):\n        \"\"\"Return OPENPYPE_ROOT_* environments for current project in dict.\"\"\"\n        return self._roots_obj.root_environments()\n\n    def root_environmets_fill_data(self, template=None):\n        \"\"\"Environment variable values in dictionary for rootless path.\n\n        Args:\n            template (str): Template for environment variable key fill.\n                By default is set to `\"${}\"`.\n        \"\"\"\n        return self.roots_obj.root_environmets_fill_data(template)\n\n    def find_root_template_from_path(self, *args, **kwargs):\n        \"\"\"Wrapper for Roots `find_root_template_from_path`.\"\"\"\n        return self.roots_obj.find_root_template_from_path(*args, **kwargs)\n\n    def path_remapper(self, *args, **kwargs):\n        \"\"\"Wrapper for Roots `path_remapper`.\"\"\"\n        return self.roots_obj.path_remapper(*args, **kwargs)\n\n    def all_root_paths(self):\n        \"\"\"Wrapper for Roots `all_root_paths`.\"\"\"\n        return self.roots_obj.all_root_paths()\n\n    def set_root_environments(self):\n        \"\"\"Set OPENPYPE_ROOT_* environments for current project.\"\"\"\n        self._roots_obj.set_root_environments()\n\n    def root_names(self):\n        \"\"\"Return root names for current project.\"\"\"\n        return self.root_names_from_templates(self.templates)\n\n    def _root_keys_from_templates(self, data):\n        \"\"\"Extract root key from templates in data.\n\n        Args:\n            data (dict): Data that may contain templates as string.\n\n        Return:\n            set: Set of all root names from templates as strings.\n\n        Output example: `{\"root[work]\", \"root[publish]\"}`\n        \"\"\"\n\n        output = set()\n        if isinstance(data, dict):\n            for value in data.values():\n                for root in self._root_keys_from_templates(value):\n                    output.add(root)\n\n        elif isinstance(data, str):\n            for group in re.findall(self.root_key_regex, data):\n                output.add(group)\n\n        return output\n\n    def root_value_for_template(self, template):\n        \"\"\"Returns value of root key from template.\"\"\"\n        root_templates = []\n        for group in re.findall(self.root_key_regex, template):\n            root_templates.append(\"{\" + group + \"}\")\n\n        if not root_templates:\n            return None\n\n        return root_templates[0].format(**{\"root\": self.roots})\n\n    def root_names_from_templates(self, templates):\n        \"\"\"Extract root names form anatomy templates.\n\n        Returns None if values in templates contain only \"{root}\".\n        Empty list is returned if there is no \"root\" in templates.\n        Else returns all root names from templates in list.\n\n        RootCombinationError is raised when templates contain both root types,\n        basic \"{root}\" and with root name specification \"{root[work]}\".\n\n        Args:\n            templates (dict): Anatomy templates where roots are not filled.\n\n        Return:\n            list/None: List of all root names from templates as strings when\n            multiroot setup is used, otherwise None is returned.\n        \"\"\"\n        roots = list(self._root_keys_from_templates(templates))\n        # Return empty list if no roots found in templates\n        if not roots:\n            return roots\n\n        # Raise exception when root keys have roots with and without root name.\n        # Invalid output example: [\"root\", \"root[project]\", \"root[render]\"]\n        if len(roots) > 1 and \"root\" in roots:\n            raise RootCombinationError(roots)\n\n        # Return None if \"root\" without root name in templates\n        if len(roots) == 1 and roots[0] == \"root\":\n            return None\n\n        names = set()\n        for root in roots:\n            for group in re.findall(self.root_name_regex, root):\n                names.add(group)\n        return list(names)\n\n    def fill_root(self, template_path):\n        \"\"\"Fill template path where is only \"root\" key unfilled.\n\n        Args:\n            template_path (str): Path with \"root\" key in.\n                Example path: \"{root}/projects/MyProject/Shot01/Lighting/...\"\n\n        Return:\n            str: formatted path\n        \"\"\"\n        # NOTE does not care if there are different keys than \"root\"\n        return template_path.format(**{\"root\": self.roots})\n\n    @classmethod\n    def fill_root_with_path(cls, rootless_path, root_path):\n        \"\"\"Fill path without filled \"root\" key with passed path.\n\n        This is helper to fill root with different directory path than anatomy\n        has defined no matter if is single or multiroot.\n\n        Output path is same as input path if `rootless_path` does not contain\n        unfilled root key.\n\n        Args:\n            rootless_path (str): Path without filled \"root\" key. Example:\n                \"{root[work]}/MyProject/...\"\n            root_path (str): What should replace root key in `rootless_path`.\n\n        Returns:\n            str: Path with filled root.\n        \"\"\"\n        output = str(rootless_path)\n        for group in re.findall(cls.root_key_regex, rootless_path):\n            replacement = \"{\" + group + \"}\"\n            output = output.replace(replacement, root_path)\n\n        return output\n\n    def replace_root_with_env_key(self, filepath, template=None):\n        \"\"\"Replace root of path with environment key.\n\n        # Example:\n        ## Project with roots:\n        ```\n        {\n            \"nas\": {\n                \"windows\": P:/projects\",\n                ...\n            }\n            ...\n        }\n        ```\n\n        ## Entered filepath\n        \"P:/projects/project/asset/task/animation_v001.ma\"\n\n        ## Entered template\n        \"<{}>\"\n\n        ## Output\n        \"<OPENPYPE_PROJECT_ROOT_NAS>/project/asset/task/animation_v001.ma\"\n\n        Args:\n            filepath (str): Full file path where root should be replaced.\n            template (str): Optional template for environment key. Must\n                have one index format key.\n                Default value if not entered: \"${}\"\n\n        Returns:\n            str: Path where root is replaced with environment root key.\n\n        Raise:\n            ValueError: When project's roots were not found in entered path.\n        \"\"\"\n        success, rootless_path = self.find_root_template_from_path(filepath)\n        if not success:\n            raise ValueError(\n                \"{}: Project's roots were not found in path: {}\".format(\n                    self.project_name, filepath\n                )\n            )\n\n        data = self.root_environmets_fill_data(template)\n        return rootless_path.format(**data)\n\n    def _project_doc_to_anatomy_data(self, project_doc):\n        \"\"\"Convert project document to anatomy data.\n\n        Probably should fill missing keys and values.\n        \"\"\"\n\n        output = copy.deepcopy(project_doc[\"config\"])\n        output[\"attributes\"] = copy.deepcopy(project_doc[\"data\"])\n\n        return output\n\n    def _apply_local_settings_on_anatomy_data(\n        self, anatomy_data, root_overrides\n    ):\n        \"\"\"Apply local settings on anatomy data.\n\n        ATM local settings can modify project roots. Project name is required\n        as local settings have data stored data by project's name.\n\n        Local settings override root values in this order:\n        1.) Check if local settings contain overrides for default project and\n            apply it's values on roots if there are any.\n        2.) If passed `project_name` is not None then check project specific\n            overrides in local settings for the project and apply it's value on\n            roots if there are any.\n\n        NOTE: Root values of default project from local settings are always\n        applied if are set.\n\n        Args:\n            anatomy_data (dict): Data for anatomy.\n            root_overrides (dict): Data of local settings.\n        \"\"\"\n\n        # Skip processing if roots for current active site are not available in\n        #   local settings\n        if not root_overrides:\n            return\n\n        current_platform = platform.system().lower()\n\n        root_data = anatomy_data[\"roots\"]\n        for root_name, path in root_overrides.items():\n            if root_name not in root_data:\n                continue\n            anatomy_data[\"roots\"][root_name][current_platform] = (\n                path\n            )\n\n\nclass CacheItem:\n    \"\"\"Helper to cache data.\n\n    Helper does not handle refresh of data and does not mark data as outdated.\n    Who uses the object should check of outdated state on his own will.\n    \"\"\"\n\n    default_lifetime = 10\n\n    def __init__(self, lifetime=None):\n        self._data = None\n        self._cached = None\n        self._lifetime = lifetime or self.default_lifetime\n\n    @property\n    def data(self):\n        \"\"\"Cached data/object.\n\n        Returns:\n            Any: Whatever was cached.\n        \"\"\"\n\n        return self._data\n\n    @property\n    def is_outdated(self):\n        \"\"\"Item has outdated cache.\n\n        Lifetime of cache item expired or was not yet set.\n\n        Returns:\n            bool: Item is outdated.\n        \"\"\"\n\n        if self._cached is None:\n            return True\n        return (time.time() - self._cached) > self._lifetime\n\n    def update_data(self, data):\n        \"\"\"Update cache of data.\n\n        Args:\n            data (Any): Data to cache.\n        \"\"\"\n\n        self._data = data\n        self._cached = time.time()\n\n\nclass Anatomy(BaseAnatomy):\n    _sync_server_addon_cache = CacheItem()\n    _project_cache = collections.defaultdict(CacheItem)\n    _default_site_id_cache = collections.defaultdict(CacheItem)\n    _root_overrides_cache = collections.defaultdict(\n        lambda: collections.defaultdict(CacheItem)\n    )\n\n    def __init__(self, project_name=None, site_name=None):\n        if not project_name:\n            project_name = os.environ.get(\"AVALON_PROJECT\")\n\n        if not project_name:\n            raise ProjectNotSet((\n                \"Implementation bug: Project name is not set. Anatomy requires\"\n                \" to load data for specific project.\"\n            ))\n\n        project_doc = self.get_project_doc_from_cache(project_name)\n        root_overrides = self._get_site_root_overrides(project_name, site_name)\n\n        super(Anatomy, self).__init__(project_doc, root_overrides)\n\n    @classmethod\n    def get_project_doc_from_cache(cls, project_name):\n        project_cache = cls._project_cache[project_name]\n        if project_cache.is_outdated:\n            project_cache.update_data(get_project(project_name))\n        return copy.deepcopy(project_cache.data)\n\n    @classmethod\n    def get_sync_server_addon(cls):\n        if cls._sync_server_addon_cache.is_outdated:\n            manager = ModulesManager()\n            cls._sync_server_addon_cache.update_data(\n                manager.get_enabled_module(\"sync_server\")\n            )\n        return cls._sync_server_addon_cache.data\n\n    @classmethod\n    def _get_studio_roots_overrides(cls, project_name, local_settings=None):\n        \"\"\"This would return 'studio' site override by local settings.\n\n        Notes:\n            This logic handles local overrides of studio site which may be\n                available even when sync server is not enabled.\n            Handling of 'studio' and 'local' site was separated as preparation\n                for AYON development where that will be received from\n                separated sources.\n\n        Args:\n            project_name (str): Name of project.\n            local_settings (Optional[dict[str, Any]]): Prepared local settings.\n\n        Returns:\n            Union[Dict[str, str], None]): Local root overrides.\n        \"\"\"\n\n        if AYON_SERVER_ENABLED:\n            if not project_name:\n                return\n            con = get_ayon_server_api_connection()\n            return con.get_project_roots_for_site(\n                project_name, get_local_site_id()\n            )\n\n        if local_settings is None:\n            local_settings = get_local_settings()\n\n        local_project_settings = local_settings.get(\"projects\") or {}\n        if not local_project_settings:\n            return None\n\n        # Check for roots existence in local settings first\n        roots_project_locals = (\n            local_project_settings\n            .get(project_name, {})\n        )\n        roots_default_locals = (\n            local_project_settings\n            .get(DEFAULT_PROJECT_KEY, {})\n        )\n\n        # Skip rest of processing if roots are not set\n        if not roots_project_locals and not roots_default_locals:\n            return\n\n        # Combine roots from local settings\n        roots_locals = roots_default_locals.get(\"studio\") or {}\n        roots_locals.update(roots_project_locals.get(\"studio\") or {})\n        return roots_locals\n\n    @classmethod\n    def _get_site_root_overrides(cls, project_name, site_name):\n        \"\"\"Get root overrides for site.\n\n        Args:\n            project_name (str): Project name for which root overrides should be\n                received.\n            site_name (Union[str, None]): Name of site for which root overrides\n                should be returned.\n        \"\"\"\n\n        # Local settings may be used more than once or may not be used at all\n        # - to avoid slowdowns 'get_local_settings' is not called until it's\n        #   really needed\n        local_settings = None\n\n        # First check if sync server is available and enabled\n        sync_server = cls.get_sync_server_addon()\n        if sync_server is None or not sync_server.enabled:\n            # QUESTION is ok to force 'studio' when site sync is not enabled?\n            site_name = \"studio\"\n\n        elif not site_name:\n            # Use sync server to receive active site name\n            project_cache = cls._default_site_id_cache[project_name]\n            if project_cache.is_outdated:\n                local_settings = get_local_settings()\n                project_cache.update_data(\n                    sync_server.get_active_site_type(\n                        project_name, local_settings\n                    )\n                )\n            site_name = project_cache.data\n\n        site_cache = cls._root_overrides_cache[project_name][site_name]\n        if site_cache.is_outdated:\n            if site_name == \"studio\":\n                # Handle studio root overrides without sync server\n                # - studio root overrides can be done even without sync server\n                roots_overrides = cls._get_studio_roots_overrides(\n                    project_name, local_settings\n                )\n            else:\n                # Ask sync server to get roots overrides\n                roots_overrides = sync_server.get_site_root_overrides(\n                    project_name, site_name, local_settings\n                )\n            site_cache.update_data(roots_overrides)\n        return site_cache.data\n\n\nclass AnatomyTemplateUnsolved(TemplateUnsolved):\n    \"\"\"Exception for unsolved template when strict is set to True.\"\"\"\n\n    msg = \"Anatomy template \\\"{0}\\\" is unsolved.{1}{2}\"\n\n\nclass AnatomyTemplateResult(TemplateResult):\n    rootless = None\n\n    def __new__(cls, result, rootless_path):\n        new_obj = super(AnatomyTemplateResult, cls).__new__(\n            cls,\n            str(result),\n            result.template,\n            result.solved,\n            result.used_values,\n            result.missing_keys,\n            result.invalid_types\n        )\n        new_obj.rootless = rootless_path\n        return new_obj\n\n    def validate(self):\n        if not self.solved:\n            raise AnatomyTemplateUnsolved(\n                self.template,\n                self.missing_keys,\n                self.invalid_types\n            )\n\n    def copy(self):\n        tmp = TemplateResult(\n            str(self),\n            self.template,\n            self.solved,\n            self.used_values,\n            self.missing_keys,\n            self.invalid_types\n        )\n        return self.__class__(tmp, self.rootless)\n\n    def normalized(self):\n        \"\"\"Convert to normalized path.\"\"\"\n\n        tmp = TemplateResult(\n            os.path.normpath(self),\n            self.template,\n            self.solved,\n            self.used_values,\n            self.missing_keys,\n            self.invalid_types\n        )\n        return self.__class__(tmp, self.rootless)\n\n\nclass AnatomyStringTemplate(StringTemplate):\n    \"\"\"String template which has access to anatomy.\"\"\"\n\n    def __init__(self, anatomy_templates, template):\n        self.anatomy_templates = anatomy_templates\n        super(AnatomyStringTemplate, self).__init__(template)\n\n    def format(self, data):\n        \"\"\"Format template and add 'root' key to data if not available.\n\n        Args:\n            data (dict[str, Any]): Formatting data for template.\n\n        Returns:\n            AnatomyTemplateResult: Formatting result.\n        \"\"\"\n\n        anatomy_templates = self.anatomy_templates\n        if not data.get(\"root\"):\n            data = copy.deepcopy(data)\n            data[\"root\"] = anatomy_templates.anatomy.roots\n        result = StringTemplate.format(self, data)\n        rootless_path = anatomy_templates.rootless_path_from_result(result)\n        return AnatomyTemplateResult(result, rootless_path)\n\n\nclass AnatomyTemplates(TemplatesDict):\n    inner_key_pattern = re.compile(r\"(\\{@.*?[^{}0]*\\})\")\n    inner_key_name_pattern = re.compile(r\"\\{@(.*?[^{}0]*)\\}\")\n\n    def __init__(self, anatomy):\n        super(AnatomyTemplates, self).__init__()\n        self.anatomy = anatomy\n        self.loaded_project = None\n\n    def reset(self):\n        self._raw_templates = None\n        self._templates = None\n        self._objected_templates = None\n\n    @property\n    def project_name(self):\n        return self.anatomy.project_name\n\n    @property\n    def roots(self):\n        return self.anatomy.roots\n\n    @property\n    def templates(self):\n        self._validate_discovery()\n        return self._templates\n\n    @property\n    def objected_templates(self):\n        self._validate_discovery()\n        return self._objected_templates\n\n    def _validate_discovery(self):\n        if self.project_name != self.loaded_project:\n            self.reset()\n\n        if self._templates is None:\n            self._discover()\n            self.loaded_project = self.project_name\n\n    def _format_value(self, value, data):\n        if isinstance(value, RootItem):\n            return self._solve_dict(value, data)\n        return super(AnatomyTemplates, self)._format_value(value, data)\n\n    def set_templates(self, templates):\n        if not templates:\n            self.reset()\n            return\n\n        self._raw_templates = copy.deepcopy(templates)\n        templates = copy.deepcopy(templates)\n        v_queue = collections.deque()\n        v_queue.append(templates)\n        while v_queue:\n            item = v_queue.popleft()\n            if not isinstance(item, dict):\n                continue\n\n            for key in tuple(item.keys()):\n                value = item[key]\n                if isinstance(value, dict):\n                    v_queue.append(value)\n\n                elif (\n                    isinstance(value, six.string_types)\n                    and \"{task}\" in value\n                ):\n                    item[key] = value.replace(\"{task}\", \"{task[name]}\")\n\n        solved_templates = self.solve_template_inner_links(templates)\n        self._templates = solved_templates\n        self._objected_templates = self.create_objected_templates(\n            solved_templates\n        )\n\n    def _create_template_object(self, template):\n        return AnatomyStringTemplate(self, template)\n\n    def default_templates(self):\n        \"\"\"Return default templates data with solved inner keys.\"\"\"\n        return self.solve_template_inner_links(\n            self.anatomy[\"templates\"]\n        )\n\n    def _discover(self):\n        \"\"\" Loads anatomy templates from yaml.\n        Default templates are loaded if project is not set or project does\n        not have set it's own.\n        TODO: create templates if not exist.\n\n        Returns:\n            TemplatesResultDict: Contain templates data for current project of\n                default templates.\n        \"\"\"\n\n        if self.project_name is None:\n            # QUESTION create project specific if not found?\n            raise AssertionError((\n                \"Project \\\"{0}\\\" does not have his own templates.\"\n                \" Trying to use default.\"\n            ).format(self.project_name))\n\n        self.set_templates(self.anatomy[\"templates\"])\n\n    @classmethod\n    def replace_inner_keys(cls, matches, value, key_values, key):\n        \"\"\"Replacement of inner keys in template values.\"\"\"\n        for match in matches:\n            anatomy_sub_keys = (\n                cls.inner_key_name_pattern.findall(match)\n            )\n            if key in anatomy_sub_keys:\n                raise ValueError((\n                    \"Unsolvable recursion in inner keys, \"\n                    \"key: \\\"{}\\\" is in his own value.\"\n                    \" Can't determine source, please check Anatomy templates.\"\n                ).format(key))\n\n            for anatomy_sub_key in anatomy_sub_keys:\n                replace_value = key_values.get(anatomy_sub_key)\n                if replace_value is None:\n                    raise KeyError((\n                        \"Anatomy templates can't be filled.\"\n                        \" Anatomy key `{0}` has\"\n                        \" invalid inner key `{1}`.\"\n                    ).format(key, anatomy_sub_key))\n\n                if not (\n                    isinstance(replace_value, numbers.Number)\n                    or isinstance(replace_value, six.string_types)\n                ):\n                    raise ValueError((\n                        \"Anatomy templates can't be filled.\"\n                        \" Anatomy key `{0}` has\"\n                        \" invalid inner key `{1}`\"\n                        \" with value `{2}`.\"\n                    ).format(key, anatomy_sub_key, str(replace_value)))\n\n                value = value.replace(match, str(replace_value))\n\n        return value\n\n    @classmethod\n    def prepare_inner_keys(cls, key_values):\n        \"\"\"Check values of inner keys.\n\n        Check if inner key exist in template group and has valid value.\n        It is also required to avoid infinite loop with unsolvable recursion\n        when first inner key's value refers to second inner key's value where\n        first is used.\n        \"\"\"\n        keys_to_solve = set(key_values.keys())\n        while True:\n            found = False\n            for key in tuple(keys_to_solve):\n                value = key_values[key]\n\n                if isinstance(value, six.string_types):\n                    matches = cls.inner_key_pattern.findall(value)\n                    if not matches:\n                        keys_to_solve.remove(key)\n                        continue\n\n                    found = True\n                    key_values[key] = cls.replace_inner_keys(\n                        matches, value, key_values, key\n                    )\n                    continue\n\n                elif not isinstance(value, dict):\n                    keys_to_solve.remove(key)\n                    continue\n\n                subdict_found = False\n                for _key, _value in tuple(value.items()):\n                    matches = cls.inner_key_pattern.findall(_value)\n                    if not matches:\n                        continue\n\n                    subdict_found = True\n                    found = True\n                    key_values[key][_key] = cls.replace_inner_keys(\n                        matches, _value, key_values,\n                        \"{}.{}\".format(key, _key)\n                    )\n\n                if not subdict_found:\n                    keys_to_solve.remove(key)\n\n            if not found:\n                break\n\n        return key_values\n\n    @classmethod\n    def solve_template_inner_links(cls, templates):\n        \"\"\"Solve templates inner keys identified by \"{@*}\".\n\n        Process is split into 2 parts.\n        First is collecting all global keys (keys in top hierarchy where value\n        is not dictionary). All global keys are set for all group keys (keys\n        in top hierarchy where value is dictionary). Value of a key is not\n        overridden in group if already contain value for the key.\n\n        In second part all keys with \"at\" symbol in value are replaced with\n        value of the key afterward \"at\" symbol from the group.\n\n        Args:\n            templates (dict): Raw templates data.\n\n        Example:\n            templates::\n                key_1: \"value_1\",\n                key_2: \"{@key_1}/{filling_key}\"\n\n                group_1:\n                    key_3: \"value_3/{@key_2}\"\n\n                group_2:\n                    key_2\": \"value_2\"\n                    key_4\": \"value_4/{@key_2}\"\n\n            output::\n                key_1: \"value_1\"\n                key_2: \"value_1/{filling_key}\"\n\n                group_1: {\n                    key_1: \"value_1\"\n                    key_2: \"value_1/{filling_key}\"\n                    key_3: \"value_3/value_1/{filling_key}\"\n\n                group_2: {\n                    key_1: \"value_1\"\n                    key_2: \"value_2\"\n                    key_4: \"value_3/value_2\"\n        \"\"\"\n        default_key_values = templates.pop(\"defaults\", {})\n        for key, value in tuple(templates.items()):\n            if isinstance(value, dict):\n                continue\n            default_key_values[key] = templates.pop(key)\n\n        # Pop \"others\" key before before expected keys are processed\n        other_templates = templates.pop(\"others\") or {}\n\n        keys_by_subkey = {}\n        for sub_key, sub_value in templates.items():\n            key_values = {}\n            key_values.update(default_key_values)\n            key_values.update(sub_value)\n            keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values)\n\n        for sub_key, sub_value in other_templates.items():\n            if sub_key in keys_by_subkey:\n                log.warning((\n                    \"Key \\\"{}\\\" is duplicated in others. Skipping.\"\n                ).format(sub_key))\n                continue\n\n            key_values = {}\n            key_values.update(default_key_values)\n            key_values.update(sub_value)\n            keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values)\n\n        default_keys_by_subkeys = cls.prepare_inner_keys(default_key_values)\n\n        for key, value in default_keys_by_subkeys.items():\n            keys_by_subkey[key] = value\n\n        return keys_by_subkey\n\n    @classmethod\n    def _dict_to_subkeys_list(cls, subdict, pre_keys=None):\n        if pre_keys is None:\n            pre_keys = []\n        output = []\n        for key in subdict:\n            value = subdict[key]\n            result = list(pre_keys)\n            result.append(key)\n            if isinstance(value, dict):\n                for item in cls._dict_to_subkeys_list(value, result):\n                    output.append(item)\n            else:\n                output.append(result)\n        return output\n\n    def _keys_to_dicts(self, key_list, value):\n        if not key_list:\n            return None\n        if len(key_list) == 1:\n            return {key_list[0]: value}\n        return {key_list[0]: self._keys_to_dicts(key_list[1:], value)}\n\n    @classmethod\n    def rootless_path_from_result(cls, result):\n        \"\"\"Calculate rootless path from formatting result.\n\n        Args:\n            result (TemplateResult): Result of StringTemplate formatting.\n\n        Returns:\n            str: Rootless path if result contains one of anatomy roots.\n        \"\"\"\n\n        used_values = result.used_values\n        missing_keys = result.missing_keys\n        template = result.template\n        invalid_types = result.invalid_types\n        if (\n            \"root\" not in used_values\n            or \"root\" in missing_keys\n            or \"{root\" not in template\n        ):\n            return\n\n        for invalid_type in invalid_types:\n            if \"root\" in invalid_type:\n                return\n\n        root_keys = cls._dict_to_subkeys_list({\"root\": used_values[\"root\"]})\n        if not root_keys:\n            return\n\n        output = str(result)\n        for used_root_keys in root_keys:\n            if not used_root_keys:\n                continue\n\n            used_value = used_values\n            root_key = None\n            for key in used_root_keys:\n                used_value = used_value[key]\n                if root_key is None:\n                    root_key = key\n                else:\n                    root_key += \"[{}]\".format(key)\n\n            root_key = \"{\" + root_key + \"}\"\n            output = output.replace(str(used_value), root_key)\n\n        return output\n\n    def format(self, data, strict=True):\n        copy_data = copy.deepcopy(data)\n        roots = self.roots\n        if roots:\n            copy_data[\"root\"] = roots\n        result = super(AnatomyTemplates, self).format(copy_data)\n        result.strict = strict\n        return result\n\n    def format_all(self, in_data, only_keys=True):\n        \"\"\" Solves templates based on entered data.\n\n        Args:\n            data (dict): Containing keys to be filled into template.\n\n        Returns:\n            TemplatesResultDict: Output `TemplateResult` have `strict`\n                attribute set to False so accessing unfilled keys in templates\n                won't raise any exceptions.\n        \"\"\"\n        return self.format(in_data, strict=False)\n\n\nclass RootItem(FormatObject):\n    \"\"\"Represents one item or roots.\n\n    Holds raw data of root item specification. Raw data contain value\n    for each platform, but current platform value is used when object\n    is used for formatting of template.\n\n    Args:\n        root_raw_data (dict): Dictionary containing root values by platform\n            names. [\"windows\", \"linux\" and \"darwin\"]\n        name (str, optional): Root name which is representing. Used with\n            multi root setup otherwise None value is expected.\n        parent_keys (list, optional): All dictionary parent keys. Values of\n            `parent_keys` are used for get full key which RootItem is\n            representing. Used for replacing root value in path with\n            formattable key. e.g. parent_keys == [\"work\"] -> {root[work]}\n        parent (object, optional): It is expected to be `Roots` object.\n            Value of `parent` won't affect code logic much.\n    \"\"\"\n\n    def __init__(\n        self, root_raw_data, name=None, parent_keys=None, parent=None\n    ):\n        lowered_platform_keys = {}\n        for key, value in root_raw_data.items():\n            lowered_platform_keys[key.lower()] = value\n        self.raw_data = lowered_platform_keys\n        self.cleaned_data = self._clean_roots(lowered_platform_keys)\n        self.name = name\n        self.parent_keys = parent_keys or []\n        self.parent = parent\n\n        self.available_platforms = list(lowered_platform_keys.keys())\n        self.value = lowered_platform_keys.get(platform.system().lower())\n        self.clean_value = self.clean_root(self.value)\n\n    def __format__(self, *args, **kwargs):\n        return self.value.__format__(*args, **kwargs)\n\n    def __str__(self):\n        return str(self.value)\n\n    def __repr__(self):\n        return self.__str__()\n\n    def __getitem__(self, key):\n        if isinstance(key, numbers.Number):\n            return self.value[key]\n\n        additional_info = \"\"\n        if self.parent and self.parent.project_name:\n            additional_info += \" for project \\\"{}\\\"\".format(\n                self.parent.project_name\n            )\n\n        raise AssertionError(\n            \"Root key \\\"{}\\\" is missing{}.\".format(\n                key, additional_info\n            )\n        )\n\n    def full_key(self):\n        \"\"\"Full key value for dictionary formatting in template.\n\n        Returns:\n            str: Return full replacement key for formatting. This helps when\n                multiple roots are set. In that case e.g. `\"root[work]\"` is\n                returned.\n        \"\"\"\n        if not self.name:\n            return \"root\"\n\n        joined_parent_keys = \"\".join(\n            [\"[{}]\".format(key) for key in self.parent_keys]\n        )\n        return \"root{}\".format(joined_parent_keys)\n\n    def clean_path(self, path):\n        \"\"\"Just replace backslashes with forward slashes.\"\"\"\n        return str(path).replace(\"\\\\\", \"/\")\n\n    def clean_root(self, root):\n        \"\"\"Makes sure root value does not end with slash.\"\"\"\n        if root:\n            root = self.clean_path(root)\n            while root.endswith(\"/\"):\n                root = root[:-1]\n        return root\n\n    def _clean_roots(self, raw_data):\n        \"\"\"Clean all values of raw root item values.\"\"\"\n        cleaned = {}\n        for key, value in raw_data.items():\n            cleaned[key] = self.clean_root(value)\n        return cleaned\n\n    def path_remapper(self, path, dst_platform=None, src_platform=None):\n        \"\"\"Remap path for specific platform.\n\n        Args:\n            path (str): Source path which need to be remapped.\n            dst_platform (str, optional): Specify destination platform\n                for which remapping should happen.\n            src_platform (str, optional): Specify source platform. This is\n                recommended to not use and keep unset until you really want\n                to use specific platform.\n            roots (dict/RootItem/None, optional): It is possible to remap\n                path with different roots then instance where method was\n                called has.\n\n        Returns:\n            str/None: When path does not contain known root then\n                None is returned else returns remapped path with \"{root}\"\n                or \"{root[<name>]}\".\n        \"\"\"\n        cleaned_path = self.clean_path(path)\n        if dst_platform:\n            dst_root_clean = self.cleaned_data.get(dst_platform)\n            if not dst_root_clean:\n                key_part = \"\"\n                full_key = self.full_key()\n                if full_key != \"root\":\n                    key_part += \"\\\"{}\\\" \".format(full_key)\n\n                log.warning(\n                    \"Root {}miss platform \\\"{}\\\" definition.\".format(\n                        key_part, dst_platform\n                    )\n                )\n                return None\n\n            if cleaned_path.startswith(dst_root_clean):\n                return cleaned_path\n\n        if src_platform:\n            src_root_clean = self.cleaned_data.get(src_platform)\n            if src_root_clean is None:\n                log.warning(\n                    \"Root \\\"{}\\\" miss platform \\\"{}\\\" definition.\".format(\n                        self.full_key(), src_platform\n                    )\n                )\n                return None\n\n            if not cleaned_path.startswith(src_root_clean):\n                return None\n\n            subpath = cleaned_path[len(src_root_clean):]\n            if dst_platform:\n                # `dst_root_clean` is used from upper condition\n                return dst_root_clean + subpath\n            return self.clean_value + subpath\n\n        result, template = self.find_root_template_from_path(path)\n        if not result:\n            return None\n\n        def parent_dict(keys, value):\n            if not keys:\n                return value\n\n            key = keys.pop(0)\n            return {key: parent_dict(keys, value)}\n\n        if dst_platform:\n            format_value = parent_dict(list(self.parent_keys), dst_root_clean)\n        else:\n            format_value = parent_dict(list(self.parent_keys), self.value)\n\n        return template.format(**{\"root\": format_value})\n\n    def find_root_template_from_path(self, path):\n        \"\"\"Replaces known root value with formattable key in path.\n\n        All platform values are checked for this replacement.\n\n        Args:\n            path (str): Path where root value should be found.\n\n        Returns:\n            tuple: Tuple contain 2 values: `success` (bool) and `path` (str).\n                When success it True then path should contain replaced root\n                value with formattable key.\n\n        Example:\n            When input path is::\n                \"C:/windows/path/root/projects/my_project/file.ext\"\n\n            And raw data of item looks like::\n                {\n                    \"windows\": \"C:/windows/path/root\",\n                    \"linux\": \"/mount/root\"\n                }\n\n            Output will be::\n                (True, \"{root}/projects/my_project/file.ext\")\n\n            If any of raw data value wouldn't match path's root output is::\n                (False, \"C:/windows/path/root/projects/my_project/file.ext\")\n        \"\"\"\n        result = False\n        output = str(path)\n\n        mod_path = self.clean_path(path)\n        for root_os, root_path in self.cleaned_data.items():\n            # Skip empty paths\n            if not root_path:\n                continue\n\n            _mod_path = mod_path  # reset to original cleaned value\n            if root_os == \"windows\":\n                root_path = root_path.lower()\n                _mod_path = _mod_path.lower()\n\n            if _mod_path.startswith(root_path):\n                result = True\n                replacement = \"{\" + self.full_key() + \"}\"\n                output = replacement + mod_path[len(root_path):]\n                break\n\n        return (result, output)\n\n\nclass Roots:\n    \"\"\"Object which should be used for formatting \"root\" key in templates.\n\n    Args:\n        anatomy Anatomy: Anatomy object created for a specific project.\n    \"\"\"\n\n    env_prefix = \"OPENPYPE_PROJECT_ROOT\"\n    roots_filename = \"roots.json\"\n\n    def __init__(self, anatomy):\n        self.anatomy = anatomy\n        self.loaded_project = None\n        self._roots = None\n\n    def __format__(self, *args, **kwargs):\n        return self.roots.__format__(*args, **kwargs)\n\n    def __getitem__(self, key):\n        return self.roots[key]\n\n    def reset(self):\n        \"\"\"Reset current roots value.\"\"\"\n        self._roots = None\n\n    def path_remapper(\n        self, path, dst_platform=None, src_platform=None, roots=None\n    ):\n        \"\"\"Remap path for specific platform.\n\n        Args:\n            path (str): Source path which need to be remapped.\n            dst_platform (str, optional): Specify destination platform\n                for which remapping should happen.\n            src_platform (str, optional): Specify source platform. This is\n                recommended to not use and keep unset until you really want\n                to use specific platform.\n            roots (dict/RootItem/None, optional): It is possible to remap\n                path with different roots then instance where method was\n                called has.\n\n        Returns:\n            str/None: When path does not contain known root then\n                None is returned else returns remapped path with \"{root}\"\n                or \"{root[<name>]}\".\n        \"\"\"\n        if roots is None:\n            roots = self.roots\n\n        if roots is None:\n            raise ValueError(\"Roots are not set. Can't find path.\")\n\n        if \"{root\" in path:\n            path = path.format(**{\"root\": roots})\n            # If `dst_platform` is not specified then return else continue.\n            if not dst_platform:\n                return path\n\n        if isinstance(roots, RootItem):\n            return roots.path_remapper(path, dst_platform, src_platform)\n\n        for _root in roots.values():\n            result = self.path_remapper(\n                path, dst_platform, src_platform, _root\n            )\n            if result is not None:\n                return result\n\n    def find_root_template_from_path(self, path, roots=None):\n        \"\"\"Find root value in entered path and replace it with formatting key.\n\n        Args:\n            path (str): Source path where root will be searched.\n            roots (Roots/dict, optional): It is possible to use different\n                roots than instance where method was triggered has.\n\n        Returns:\n            tuple: Output contains tuple with bool representing success as\n                first value and path with or without replaced root with\n                formatting key as second value.\n\n        Raises:\n            ValueError: When roots are not entered and can't be loaded.\n        \"\"\"\n        if roots is None:\n            log.debug(\n                \"Looking for matching root in path \\\"{}\\\".\".format(path)\n            )\n            roots = self.roots\n\n        if roots is None:\n            raise ValueError(\"Roots are not set. Can't find path.\")\n\n        if isinstance(roots, RootItem):\n            return roots.find_root_template_from_path(path)\n\n        for root_name, _root in roots.items():\n            success, result = self.find_root_template_from_path(path, _root)\n            if success:\n                log.info(\"Found match in root \\\"{}\\\".\".format(root_name))\n                return success, result\n\n        log.warning(\"No matching root was found in current setting.\")\n        return (False, path)\n\n    def set_root_environments(self):\n        \"\"\"Set root environments for current project.\"\"\"\n        for key, value in self.root_environments().items():\n            os.environ[key] = value\n\n    def root_environments(self):\n        \"\"\"Use root keys to create unique keys for environment variables.\n\n        Concatenates prefix \"OPENPYPE_ROOT\" with root keys to create unique\n        keys.\n\n        Returns:\n            dict: Result is `{(str): (str)}` dicitonary where key represents\n                unique key concatenated by keys and value is root value of\n                current platform root.\n\n        Example:\n            With raw root values::\n                \"work\": {\n                    \"windows\": \"P:/projects/work\",\n                    \"linux\": \"/mnt/share/projects/work\",\n                    \"darwin\": \"/darwin/path/work\"\n                },\n                \"publish\": {\n                    \"windows\": \"P:/projects/publish\",\n                    \"linux\": \"/mnt/share/projects/publish\",\n                    \"darwin\": \"/darwin/path/publish\"\n                }\n\n            Result on windows platform::\n                {\n                    \"OPENPYPE_ROOT_WORK\": \"P:/projects/work\",\n                    \"OPENPYPE_ROOT_PUBLISH\": \"P:/projects/publish\"\n                }\n\n            Short example when multiroot is not used::\n                {\n                    \"OPENPYPE_ROOT\": \"P:/projects\"\n                }\n        \"\"\"\n        return self._root_environments()\n\n    def all_root_paths(self, roots=None):\n        \"\"\"Return all paths for all roots of all platforms.\"\"\"\n        if roots is None:\n            roots = self.roots\n\n        output = []\n        if isinstance(roots, RootItem):\n            for value in roots.raw_data.values():\n                output.append(value)\n            return output\n\n        for _roots in roots.values():\n            output.extend(self.all_root_paths(_roots))\n        return output\n\n    def _root_environments(self, keys=None, roots=None):\n        if not keys:\n            keys = []\n        if roots is None:\n            roots = self.roots\n\n        if isinstance(roots, RootItem):\n            key_items = [self.env_prefix]\n            for _key in keys:\n                key_items.append(_key.upper())\n\n            key = \"_\".join(key_items)\n            # Make sure key and value does not contain unicode\n            #   - can happen in Python 2 hosts\n            return {str(key): str(roots.value)}\n\n        output = {}\n        for _key, _value in roots.items():\n            _keys = list(keys)\n            _keys.append(_key)\n            output.update(self._root_environments(_keys, _value))\n        return output\n\n    def root_environmets_fill_data(self, template=None):\n        \"\"\"Environment variable values in dictionary for rootless path.\n\n        Args:\n            template (str): Template for environment variable key fill.\n                By default is set to `\"${}\"`.\n        \"\"\"\n        if template is None:\n            template = \"${}\"\n        return self._root_environmets_fill_data(template)\n\n    def _root_environmets_fill_data(self, template, keys=None, roots=None):\n        if keys is None and roots is None:\n            return {\n                \"root\": self._root_environmets_fill_data(\n                    template, [], self.roots\n                )\n            }\n\n        if isinstance(roots, RootItem):\n            key_items = [Roots.env_prefix]\n            for _key in keys:\n                key_items.append(_key.upper())\n            key = \"_\".join(key_items)\n            return template.format(key)\n\n        output = {}\n        for key, value in roots.items():\n            _keys = list(keys)\n            _keys.append(key)\n            output[key] = self._root_environmets_fill_data(\n                template, _keys, value\n            )\n        return output\n\n    @property\n    def project_name(self):\n        \"\"\"Return project name which will be used for loading root values.\"\"\"\n        return self.anatomy.project_name\n\n    @property\n    def roots(self):\n        \"\"\"Property for filling \"root\" key in templates.\n\n        This property returns roots for current project or default root values.\n        Warning:\n            Default roots value may cause issues when project use different\n            roots settings. That may happen when project use multiroot\n            templates but default roots miss their keys.\n        \"\"\"\n        if self.project_name != self.loaded_project:\n            self._roots = None\n\n        if self._roots is None:\n            self._roots = self._discover()\n            self.loaded_project = self.project_name\n        return self._roots\n\n    def _discover(self):\n        \"\"\" Loads current project's roots or default.\n\n        Default roots are loaded if project override's does not contain roots.\n\n        Returns:\n            `RootItem` or `dict` with multiple `RootItem`s when multiroot\n            setting is used.\n        \"\"\"\n\n        return self._parse_dict(self.anatomy[\"roots\"], parent=self)\n\n    @staticmethod\n    def _parse_dict(data, key=None, parent_keys=None, parent=None):\n        \"\"\"Parse roots raw data into RootItem or dictionary with RootItems.\n\n        Converting raw roots data to `RootItem` helps to handle platform keys.\n        This method is recursive to be able handle multiroot setup and\n        is static to be able to load default roots without creating new object.\n\n        Args:\n            data (dict): Should contain raw roots data to be parsed.\n            key (str, optional): Current root key. Set by recursion.\n            parent_keys (list): Parent dictionary keys. Set by recursion.\n            parent (Roots, optional): Parent object set in `RootItem`\n                helps to keep RootItem instance updated with `Roots` object.\n\n        Returns:\n            `RootItem` or `dict` with multiple `RootItem`s when multiroot\n            setting is used.\n        \"\"\"\n        if not parent_keys:\n            parent_keys = []\n        is_last = False\n        for value in data.values():\n            if isinstance(value, six.string_types):\n                is_last = True\n                break\n\n        if is_last:\n            return RootItem(data, key, parent_keys, parent=parent)\n\n        output = {}\n        for _key, value in data.items():\n            _parent_keys = list(parent_keys)\n            _parent_keys.append(_key)\n            output[_key] = Roots._parse_dict(value, _key, _parent_keys, parent)\n        return output\n"
  },
  {
    "path": "openpype/pipeline/colorspace.py",
    "content": "import re\nimport os\nimport json\nimport contextlib\nimport functools\nimport platform\nimport tempfile\nimport warnings\nfrom copy import deepcopy\n\nfrom openpype import PACKAGE_DIR\nfrom openpype.settings import get_project_settings\nfrom openpype.lib import (\n    StringTemplate,\n    run_openpype_process,\n    Logger\n)\nfrom openpype.pipeline import Anatomy\nfrom openpype.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS\n\n\nlog = Logger.get_logger(__name__)\n\n\nclass CachedData:\n    remapping = None\n    has_compatible_ocio_package = None\n    config_version_data = {}\n    ocio_config_colorspaces = {}\n    allowed_exts = {\n        ext.lstrip(\".\") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)\n    }\n\n\nclass DeprecatedWarning(DeprecationWarning):\n    pass\n\n\ndef deprecated(new_destination):\n    \"\"\"Mark functions as deprecated.\n\n    It will result in a warning being emitted when the function is used.\n    \"\"\"\n\n    func = None\n    if callable(new_destination):\n        func = new_destination\n        new_destination = None\n\n    def _decorator(decorated_func):\n        if new_destination is None:\n            warning_message = (\n                \" Please check content of deprecated function to figure out\"\n                \" possible replacement.\"\n            )\n        else:\n            warning_message = \" Please replace your usage with '{}'.\".format(\n                new_destination\n            )\n\n        @functools.wraps(decorated_func)\n        def wrapper(*args, **kwargs):\n            warnings.simplefilter(\"always\", DeprecatedWarning)\n            warnings.warn(\n                (\n                    \"Call to deprecated function '{}'\"\n                    \"\\nFunction was moved or removed.{}\"\n                ).format(decorated_func.__name__, warning_message),\n                category=DeprecatedWarning,\n                stacklevel=4\n            )\n            return decorated_func(*args, **kwargs)\n        return wrapper\n\n    if func is None:\n        return _decorator\n    return _decorator(func)\n\n\n@contextlib.contextmanager\ndef _make_temp_json_file():\n    \"\"\"Wrapping function for json temp file\n    \"\"\"\n    try:\n        # Store dumped json to temporary file\n        temporary_json_file = tempfile.NamedTemporaryFile(\n            mode=\"w\", suffix=\".json\", delete=False\n        )\n        temporary_json_file.close()\n        temporary_json_filepath = temporary_json_file.name.replace(\n            \"\\\\\", \"/\"\n        )\n\n        yield temporary_json_filepath\n\n    except IOError as _error:\n        raise IOError(\n            \"Unable to create temp json file: {}\".format(\n                _error\n            )\n        )\n\n    finally:\n        # Remove the temporary json\n        os.remove(temporary_json_filepath)\n\n\ndef get_ocio_config_script_path():\n    \"\"\"Get path to ocio wrapper script\n\n    Returns:\n        str: path string\n    \"\"\"\n    return os.path.normpath(\n        os.path.join(\n            PACKAGE_DIR,\n            \"scripts\",\n            \"ocio_wrapper.py\"\n        )\n    )\n\n\ndef get_colorspace_name_from_filepath(\n    filepath, host_name, project_name,\n    config_data=None, file_rules=None,\n    project_settings=None,\n    validate=True\n):\n    \"\"\"Get colorspace name from filepath\n\n    Args:\n        filepath (str): path string, file rule pattern is tested on it\n        host_name (str): host name\n        project_name (str): project name\n        config_data (Optional[dict]): config path and template in dict.\n                                      Defaults to None.\n        file_rules (Optional[dict]): file rule data from settings.\n                                     Defaults to None.\n        project_settings (Optional[dict]): project settings. Defaults to None.\n        validate (Optional[bool]): should resulting colorspace be validated\n                                with config file? Defaults to True.\n\n    Returns:\n        str: name of colorspace\n    \"\"\"\n    project_settings, config_data, file_rules = _get_context_settings(\n        host_name, project_name,\n        config_data=config_data, file_rules=file_rules,\n        project_settings=project_settings\n    )\n\n    if not config_data:\n        # in case global or host color management is not enabled\n        return None\n\n    # use ImageIO file rules\n    colorspace_name = get_imageio_file_rules_colorspace_from_filepath(\n        filepath, host_name, project_name,\n        config_data=config_data, file_rules=file_rules,\n        project_settings=project_settings\n    )\n\n    # try to get colorspace from OCIO v2 file rules\n    if (\n        not colorspace_name\n        and compatibility_check_config_version(config_data[\"path\"], major=2)\n    ):\n        colorspace_name = get_config_file_rules_colorspace_from_filepath(\n            config_data[\"path\"], filepath)\n\n    # use parse colorspace from filepath as fallback\n    colorspace_name = colorspace_name or parse_colorspace_from_filepath(\n        filepath, config_path=config_data[\"path\"]\n    )\n\n    if not colorspace_name:\n        log.info(\"No imageio file rule matched input path: '{}'\".format(\n            filepath\n        ))\n        return None\n\n    # validate matching colorspace with config\n    if validate:\n        validate_imageio_colorspace_in_config(\n            config_data[\"path\"], colorspace_name)\n\n    return colorspace_name\n\n\n# TODO: remove this in future - backward compatibility\n@deprecated(\"get_imageio_file_rules_colorspace_from_filepath\")\ndef get_imageio_colorspace_from_filepath(*args, **kwargs):\n    return get_imageio_file_rules_colorspace_from_filepath(*args, **kwargs)\n\n# TODO: remove this in future - backward compatibility\n@deprecated(\"get_imageio_file_rules_colorspace_from_filepath\")\ndef get_colorspace_from_filepath(*args, **kwargs):\n    return get_imageio_file_rules_colorspace_from_filepath(*args, **kwargs)\n\n\ndef _get_context_settings(\n    host_name, project_name,\n    config_data=None, file_rules=None,\n    project_settings=None\n):\n    project_settings = project_settings or get_project_settings(\n        project_name\n    )\n\n    config_data = config_data or get_imageio_config(\n        project_name, host_name, project_settings)\n\n    # in case host color management is not enabled\n    if not config_data:\n        return (None, None, None)\n\n    file_rules = file_rules or get_imageio_file_rules(\n        project_name, host_name, project_settings)\n\n    return project_settings, config_data, file_rules\n\n\ndef get_imageio_file_rules_colorspace_from_filepath(\n    filepath, host_name, project_name,\n    config_data=None, file_rules=None,\n    project_settings=None\n):\n    \"\"\"Get colorspace name from filepath\n\n    ImageIO Settings file rules are tested for matching rule.\n\n    Args:\n        filepath (str): path string, file rule pattern is tested on it\n        host_name (str): host name\n        project_name (str): project name\n        config_data (Optional[dict]): config path and template in dict.\n                                      Defaults to None.\n        file_rules (Optional[dict]): file rule data from settings.\n                                     Defaults to None.\n        project_settings (Optional[dict]): project settings. Defaults to None.\n\n    Returns:\n        str: name of colorspace\n    \"\"\"\n    project_settings, config_data, file_rules = _get_context_settings(\n        host_name, project_name,\n        config_data=config_data, file_rules=file_rules,\n        project_settings=project_settings\n    )\n\n    if not config_data:\n        # in case global or host color management is not enabled\n        return None\n\n    # match file rule from path\n    colorspace_name = None\n    for file_rule in file_rules.values():\n        pattern = file_rule[\"pattern\"]\n        extension = file_rule[\"ext\"]\n        ext_match = re.match(\n            r\".*(?=.{})\".format(extension), filepath\n        )\n        file_match = re.search(\n            pattern, filepath\n        )\n\n        if ext_match and file_match:\n            colorspace_name = file_rule[\"colorspace\"]\n\n    return colorspace_name\n\n\ndef get_config_file_rules_colorspace_from_filepath(config_path, filepath):\n    \"\"\"Get colorspace from file path wrapper.\n\n    Wrapper function for getting colorspace from file path\n    with use of OCIO v2 file-rules.\n\n    Args:\n        config_path (str): path leading to config.ocio file\n        filepath (str): path leading to a file\n\n    Returns:\n        Any[str, None]: matching colorspace name\n    \"\"\"\n    if not compatibility_check():\n        # python environment is not compatible with PyOpenColorIO\n        # needs to be run in subprocess\n        result_data = _get_wrapped_with_subprocess(\n            \"colorspace\", \"get_config_file_rules_colorspace_from_filepath\",\n            config_path=config_path,\n            filepath=filepath\n        )\n        if result_data:\n            return result_data[0]\n\n    # TODO: refactor this so it is not imported but part of this file\n    from openpype.scripts.ocio_wrapper import _get_config_file_rules_colorspace_from_filepath  # noqa: E501\n\n    result_data = _get_config_file_rules_colorspace_from_filepath(\n        config_path, filepath)\n\n    if result_data:\n        return result_data[0]\n\n\ndef parse_colorspace_from_filepath(\n    filepath, colorspaces=None, config_path=None\n):\n    \"\"\"Parse colorspace name from filepath\n\n    An input path can have colorspace name used as part of name\n    or as folder name.\n\n    Example:\n        >>> config_path = \"path/to/config.ocio\"\n        >>> colorspaces = get_ocio_config_colorspaces(config_path)\n        >>> colorspace = parse_colorspace_from_filepath(\n                \"path/to/file/acescg/file.exr\",\n                colorspaces=colorspaces\n            )\n        >>> print(colorspace)\n        acescg\n\n    Args:\n        filepath (str): path string\n        colorspaces (Optional[dict[str]]): list of colorspaces\n        config_path (Optional[str]): path to config.ocio file\n\n    Returns:\n        str: name of colorspace\n    \"\"\"\n    def _get_colorspace_match_regex(colorspaces):\n        \"\"\"Return a regex pattern\n\n        Allows to search a colorspace match in a filename\n\n        Args:\n            colorspaces (list): List of colorspace names\n\n        Returns:\n            re.Pattern: regex pattern\n        \"\"\"\n        pattern = \"|\".join(\n            # Allow to match spaces also as underscores because the\n            # integrator replaces spaces with underscores in filenames\n            re.escape(colorspace) for colorspace in\n            # Sort by longest first so the regex matches longer matches\n            # over smaller matches, e.g. matching 'Output - sRGB' over 'sRGB'\n            sorted(colorspaces, key=len, reverse=True)\n        )\n        return re.compile(pattern)\n\n    if not colorspaces and not config_path:\n        raise ValueError(\n            \"Must provide `config_path` if `colorspaces` is not provided.\"\n        )\n\n    colorspaces = (\n        colorspaces\n        or get_ocio_config_colorspaces(config_path)[\"colorspaces\"]\n    )\n    underscored_colorspaces = {\n        key.replace(\" \", \"_\"): key for key in colorspaces\n        if \" \" in key\n    }\n\n    # match colorspace from  filepath\n    regex_pattern = _get_colorspace_match_regex(\n        list(colorspaces) + list(underscored_colorspaces))\n    match = regex_pattern.search(filepath)\n    colorspace = match.group(0) if match else None\n\n    if colorspace in underscored_colorspaces:\n        return underscored_colorspaces[colorspace]\n\n    if colorspace:\n        return colorspace\n\n    log.info(\"No matching colorspace in config '{}' for path: '{}'\".format(\n        config_path, filepath\n    ))\n    return None\n\n\ndef validate_imageio_colorspace_in_config(config_path, colorspace_name):\n    \"\"\"Validator making sure colorspace name is used in config.ocio\n\n    Args:\n        config_path (str): path leading to config.ocio file\n        colorspace_name (str): tested colorspace name\n\n    Raises:\n        KeyError: missing colorspace name\n\n    Returns:\n        bool: True if exists\n    \"\"\"\n    colorspaces = get_ocio_config_colorspaces(config_path)[\"colorspaces\"]\n    if colorspace_name not in colorspaces:\n        raise KeyError(\n            \"Missing colorspace '{}' in config file '{}'\".format(\n                colorspace_name, config_path)\n        )\n    return True\n\n\n# TODO: remove this in future - backward compatibility\n@deprecated(\"_get_wrapped_with_subprocess\")\ndef get_data_subprocess(config_path, data_type):\n    \"\"\"[Deprecated] Get data via subprocess\n\n    Wrapper for Python 2 hosts.\n\n    Args:\n        config_path (str): path leading to config.ocio file\n    \"\"\"\n    return _get_wrapped_with_subprocess(\n        \"config\", data_type, in_path=config_path,\n    )\n\n\ndef _get_wrapped_with_subprocess(command_group, command, **kwargs):\n    \"\"\"Get data via subprocess\n\n    Wrapper for Python 2 hosts.\n\n    Args:\n        command_group (str): command group name\n        command (str): command name\n        **kwargs: command arguments\n\n    Returns:\n        Any[dict, None]: data\n    \"\"\"\n    with _make_temp_json_file() as tmp_json_path:\n        # Prepare subprocess arguments\n        args = [\n            \"run\", get_ocio_config_script_path(),\n            command_group, command\n        ]\n\n        for key_, value_ in kwargs.items():\n            args.extend((\"--{}\".format(key_), value_))\n\n        args.append(\"--out_path\")\n        args.append(tmp_json_path)\n\n        log.info(\"Executing: {}\".format(\" \".join(args)))\n\n        run_openpype_process(*args, logger=log)\n\n        # return all colorspaces\n        with open(tmp_json_path, \"r\") as f_:\n            return json.load(f_)\n\n\n# TODO: this should be part of ocio_wrapper.py\ndef compatibility_check():\n    \"\"\"Making sure PyOpenColorIO is importable\"\"\"\n    if CachedData.has_compatible_ocio_package is not None:\n        return CachedData.has_compatible_ocio_package\n\n    try:\n        import PyOpenColorIO  # noqa: F401\n        # Requirement, introduced in newer ocio version\n        config = PyOpenColorIO.GetCurrentConfig()\n        CachedData.has_compatible_ocio_package = (\n            hasattr(config, \"getDisplayViewColorSpaceName\")\n        )\n    except ImportError:\n        CachedData.has_compatible_ocio_package = False\n\n    # compatible\n    return CachedData.has_compatible_ocio_package\n\n\n# TODO: this should be part of ocio_wrapper.py\ndef compatibility_check_config_version(config_path, major=1, minor=None):\n    \"\"\"Making sure PyOpenColorIO config version is compatible\"\"\"\n\n    if not CachedData.config_version_data.get(config_path):\n        if compatibility_check():\n            # TODO: refactor this so it is not imported but part of this file\n            from openpype.scripts.ocio_wrapper import _get_version_data\n\n            CachedData.config_version_data[config_path] = \\\n                _get_version_data(config_path)\n\n        else:\n            # python environment is not compatible with PyOpenColorIO\n            # needs to be run in subprocess\n            CachedData.config_version_data[config_path] = \\\n                _get_wrapped_with_subprocess(\n                    \"config\", \"get_version\", config_path=config_path\n            )\n\n    # check major version\n    if CachedData.config_version_data[config_path][\"major\"] != major:\n        return False\n\n    # check minor version\n    if minor and CachedData.config_version_data[config_path][\"minor\"] != minor:\n        return False\n\n    # compatible\n    return True\n\n\ndef get_ocio_config_colorspaces(config_path):\n    \"\"\"Get all colorspace data\n\n    Wrapper function for aggregating all names and its families.\n    Families can be used for building menu and submenus in gui.\n\n    Args:\n        config_path (str): path leading to config.ocio file\n\n    Returns:\n        dict: colorspace and family in couple\n    \"\"\"\n    if not CachedData.ocio_config_colorspaces.get(config_path):\n        if not compatibility_check():\n            # python environment is not compatible with PyOpenColorIO\n            # needs to be run in subprocess\n            CachedData.ocio_config_colorspaces[config_path] = \\\n                _get_wrapped_with_subprocess(\n                    \"config\", \"get_colorspace\", in_path=config_path\n            )\n        else:\n            # TODO: refactor this so it is not imported but part of this file\n            from openpype.scripts.ocio_wrapper import _get_colorspace_data\n\n            CachedData.ocio_config_colorspaces[config_path] = \\\n                _get_colorspace_data(config_path)\n\n    return CachedData.ocio_config_colorspaces[config_path]\n\n\ndef convert_colorspace_enumerator_item(\n    colorspace_enum_item,\n    config_items\n):\n    \"\"\"Convert colorspace enumerator item to dictionary\n\n    Args:\n        colorspace_item (str): colorspace and family in couple\n        config_items (dict[str,dict]): colorspace data\n\n    Returns:\n        dict: colorspace data\n    \"\"\"\n    if \"::\" not in colorspace_enum_item:\n        return None\n\n    # split string with `::` separator and set first as key and second as value\n    item_type, item_name = colorspace_enum_item.split(\"::\")\n\n    item_data = None\n    if item_type == \"aliases\":\n        # loop through all colorspaces and find matching alias\n        for name, _data in config_items.get(\"colorspaces\", {}).items():\n            if item_name in _data.get(\"aliases\", []):\n                item_data = deepcopy(_data)\n                item_data.update({\n                    \"name\": name,\n                    \"type\": \"colorspace\"\n                })\n                break\n    else:\n        # find matching colorspace item found in labeled_colorspaces\n        item_data = config_items.get(item_type, {}).get(item_name)\n        if item_data:\n            item_data = deepcopy(item_data)\n            item_data.update({\n                \"name\": item_name,\n                \"type\": item_type\n            })\n\n    # raise exception if item is not found\n    if not item_data:\n        message_config_keys = \", \".join(\n            \"'{}':{}\".format(\n                key,\n                set(config_items.get(key, {}).keys())\n            ) for key in config_items.keys()\n        )\n        raise KeyError(\n            \"Missing colorspace item '{}' in config data: [{}]\".format(\n                colorspace_enum_item, message_config_keys\n            )\n        )\n\n    return item_data\n\n\ndef get_colorspaces_enumerator_items(\n    config_items,\n    include_aliases=False,\n    include_looks=False,\n    include_roles=False,\n    include_display_views=False\n):\n    \"\"\"Get all colorspace data with labels\n\n    Wrapper function for aggregating all names and its families.\n    Families can be used for building menu and submenus in gui.\n\n    Args:\n        config_items (dict[str,dict]): colorspace data coming from\n            `get_ocio_config_colorspaces` function\n        include_aliases (bool): include aliases in result\n        include_looks (bool): include looks in result\n        include_roles (bool): include roles in result\n\n    Returns:\n        list[tuple[str,str]]: colorspace and family in couple\n    \"\"\"\n    labeled_colorspaces = []\n    aliases = set()\n    colorspaces = set()\n    looks = set()\n    roles = set()\n    display_views = set()\n    for items_type, colorspace_items in config_items.items():\n        if items_type == \"colorspaces\":\n            for color_name, color_data in colorspace_items.items():\n                if color_data.get(\"aliases\"):\n                    aliases.update([\n                        (\n                            \"aliases::{}\".format(alias_name),\n                            \"[alias] {} ({})\".format(alias_name, color_name)\n                        )\n                        for alias_name in color_data[\"aliases\"]\n                    ])\n                colorspaces.add((\n                    \"{}::{}\".format(items_type, color_name),\n                    \"[colorspace] {}\".format(color_name)\n                ))\n\n        elif items_type == \"looks\":\n            looks.update([\n                (\n                    \"{}::{}\".format(items_type, name),\n                    \"[look] {} ({})\".format(name, role_data[\"process_space\"])\n                )\n                for name, role_data in colorspace_items.items()\n            ])\n\n        elif items_type == \"displays_views\":\n            display_views.update([\n                (\n                    \"{}::{}\".format(items_type, name),\n                    \"[view (display)] {}\".format(name)\n                )\n                for name, _ in colorspace_items.items()\n            ])\n\n        elif items_type == \"roles\":\n            roles.update([\n                (\n                    \"{}::{}\".format(items_type, name),\n                    \"[role] {} ({})\".format(name, role_data[\"colorspace\"])\n                )\n                for name, role_data in colorspace_items.items()\n            ])\n\n    if roles and include_roles:\n        roles = sorted(roles, key=lambda x: x[0])\n        labeled_colorspaces.extend(roles)\n\n    # add colorspaces as second so it is not first in menu\n    colorspaces = sorted(colorspaces, key=lambda x: x[0])\n    labeled_colorspaces.extend(colorspaces)\n\n    if aliases and include_aliases:\n        aliases = sorted(aliases, key=lambda x: x[0])\n        labeled_colorspaces.extend(aliases)\n\n    if looks and include_looks:\n        looks = sorted(looks, key=lambda x: x[0])\n        labeled_colorspaces.extend(looks)\n\n    if display_views and include_display_views:\n        display_views = sorted(display_views, key=lambda x: x[0])\n        labeled_colorspaces.extend(display_views)\n\n    return labeled_colorspaces\n\n\n# TODO: remove this in future - backward compatibility\n@deprecated(\"_get_wrapped_with_subprocess\")\ndef get_colorspace_data_subprocess(config_path):\n    \"\"\"[Deprecated] Get colorspace data via subprocess\n\n    Wrapper for Python 2 hosts.\n\n    Args:\n        config_path (str): path leading to config.ocio file\n\n    Returns:\n        dict: colorspace and family in couple\n    \"\"\"\n    return _get_wrapped_with_subprocess(\n        \"config\", \"get_colorspace\", in_path=config_path\n    )\n\n\ndef get_ocio_config_views(config_path):\n    \"\"\"Get all viewer data\n\n    Wrapper function for aggregating all display and related viewers.\n    Key can be used for building gui menu with submenus.\n\n    Args:\n        config_path (str): path leading to config.ocio file\n\n    Returns:\n        dict: `display/viewer` and viewer data\n    \"\"\"\n    if not compatibility_check():\n        # python environment is not compatible with PyOpenColorIO\n        # needs to be run in subprocess\n        return _get_wrapped_with_subprocess(\n            \"config\", \"get_views\", in_path=config_path\n        )\n\n    # TODO: refactor this so it is not imported but part of this file\n    from openpype.scripts.ocio_wrapper import _get_views_data\n\n    return _get_views_data(config_path)\n\n\n# TODO: remove this in future - backward compatibility\n@deprecated(\"_get_wrapped_with_subprocess\")\ndef get_views_data_subprocess(config_path):\n    \"\"\"[Deprecated] Get viewers data via subprocess\n\n    Wrapper for Python 2 hosts.\n\n    Args:\n        config_path (str): path leading to config.ocio file\n\n    Returns:\n        dict: `display/viewer` and viewer data\n    \"\"\"\n    return _get_wrapped_with_subprocess(\n        \"config\", \"get_views\", in_path=config_path\n    )\n\n\ndef get_imageio_config(\n    project_name,\n    host_name,\n    project_settings=None,\n    anatomy_data=None,\n    anatomy=None,\n    env=None\n):\n    \"\"\"Returns config data from settings\n\n    Config path is formatted in `path` key\n    and original settings input is saved into `template` key.\n\n    Args:\n        project_name (str): project name\n        host_name (str): host name\n        project_settings (Optional[dict]): Project settings.\n        anatomy_data (Optional[dict]): anatomy formatting data.\n        anatomy (Optional[Anatomy]): Anatomy object.\n        env (Optional[dict]): Environment variables.\n\n    Returns:\n        dict: config path data or empty dict\n    \"\"\"\n    project_settings = project_settings or get_project_settings(project_name)\n    anatomy = anatomy or Anatomy(project_name)\n\n    if not anatomy_data:\n        from openpype.pipeline.context_tools import (\n            get_template_data_from_session)\n        anatomy_data = get_template_data_from_session()\n\n    formatting_data = deepcopy(anatomy_data)\n\n    # Add project roots to anatomy data\n    formatting_data[\"root\"] = anatomy.roots\n    formatting_data[\"platform\"] = platform.system().lower()\n\n    # Get colorspace settings\n    imageio_global, imageio_host = _get_imageio_settings(\n        project_settings, host_name)\n\n    # Host 'ocio_config' is optional\n    host_ocio_config = imageio_host.get(\"ocio_config\") or {}\n\n    # Global color management must be enabled to be able to use host settings\n    activate_color_management = imageio_global.get(\n        \"activate_global_color_management\")\n    # TODO: remove this in future - backward compatibility\n    # For already saved overrides from previous version look for 'enabled'\n    #   on host settings.\n    if activate_color_management is None:\n        activate_color_management = host_ocio_config.get(\"enabled\", False)\n\n    if not activate_color_management:\n        # if global settings are disabled return empty dict because\n        # it is expected that no colorspace management is needed\n        log.info(\"Colorspace management is disabled globally.\")\n        return {}\n\n    # Check if host settings group is having 'activate_host_color_management'\n    # - if it does not have activation key then default it to True so it uses\n    #       global settings\n    # This is for backward compatibility.\n    # TODO: in future rewrite this to be more explicit\n    activate_host_color_management = imageio_host.get(\n        \"activate_host_color_management\")\n\n    # TODO: remove this in future - backward compatibility\n    if activate_host_color_management is None:\n        activate_host_color_management = host_ocio_config.get(\"enabled\", False)\n\n    if not activate_host_color_management:\n        # if host settings are disabled return False because\n        # it is expected that no colorspace management is needed\n        log.info(\n            \"Colorspace management for host '{}' is disabled.\".format(\n                host_name)\n        )\n        return {}\n\n    # get config path from either global or host settings\n    # depending on override flag\n    # TODO: in future rewrite this to be more explicit\n    override_global_config = host_ocio_config.get(\"override_global_config\")\n    if override_global_config is None:\n        # for already saved overrides from previous version\n        # TODO: remove this in future - backward compatibility\n        override_global_config = host_ocio_config.get(\"enabled\")\n\n    if override_global_config:\n        config_data = _get_config_data(\n            host_ocio_config[\"filepath\"], formatting_data, env\n        )\n    else:\n        # get config path from global\n        config_global = imageio_global[\"ocio_config\"]\n        config_data = _get_config_data(\n            config_global[\"filepath\"], formatting_data, env\n        )\n\n    if not config_data:\n        raise FileExistsError(\n            \"No OCIO config found in settings. It is \"\n            \"either missing or there is typo in path inputs\"\n        )\n\n    return config_data\n\n\ndef _get_config_data(path_list, anatomy_data, env=None):\n    \"\"\"Return first existing path in path list.\n\n    If template is used in path inputs,\n    then it is formatted by anatomy data\n    and environment variables\n\n    Args:\n        path_list (list[str]): list of abs paths\n        anatomy_data (dict): formatting data\n        env (Optional[dict]): Environment variables.\n\n    Returns:\n        dict: config data\n    \"\"\"\n    formatting_data = deepcopy(anatomy_data)\n\n    environment_vars = env or dict(**os.environ)\n\n    # format the path for potential env vars\n    formatting_data.update(environment_vars)\n\n    # first try host config paths\n    for path_ in path_list:\n        formatted_path = _format_path(path_, formatting_data)\n\n        if not os.path.exists(formatted_path):\n            continue\n\n        return {\n            \"path\": os.path.normpath(formatted_path),\n            \"template\": path_\n        }\n\n\ndef _format_path(template_path, formatting_data):\n    \"\"\"Single template path formatting.\n\n    Args:\n        template_path (str): template string\n        formatting_data (dict): data to be used for\n                                template formatting\n\n    Returns:\n        str: absolute formatted path\n    \"\"\"\n    # format path for anatomy keys\n    formatted_path = StringTemplate(template_path).format(\n        formatting_data)\n\n    return os.path.abspath(formatted_path)\n\n\ndef get_imageio_file_rules(project_name, host_name, project_settings=None):\n    \"\"\"Get ImageIO File rules from project settings\n\n    Args:\n        project_name (str): project name\n        host_name (str): host name\n        project_settings (dict, optional): project settings.\n                                           Defaults to None.\n\n    Returns:\n        dict: file rules data\n    \"\"\"\n    project_settings = project_settings or get_project_settings(project_name)\n\n    imageio_global, imageio_host = _get_imageio_settings(\n        project_settings, host_name)\n\n    # get file rules from global and host_name\n    frules_global = imageio_global[\"file_rules\"]\n    activate_global_rules = (\n        frules_global.get(\"activate_global_file_rules\", False)\n        # TODO: remove this in future - backward compatibility\n        or frules_global.get(\"enabled\")\n    )\n    global_rules = frules_global[\"rules\"]\n\n    if not activate_global_rules:\n        log.info(\n            \"Colorspace global file rules are disabled.\"\n        )\n        global_rules = {}\n\n    # host is optional, some might not have any settings\n    frules_host = imageio_host.get(\"file_rules\", {})\n\n    # compile file rules dictionary\n    activate_host_rules = frules_host.get(\"activate_host_rules\")\n    if activate_host_rules is None:\n        # TODO: remove this in future - backward compatibility\n        activate_host_rules = frules_host.get(\"enabled\", False)\n\n    # return host rules if activated or global rules\n    return frules_host[\"rules\"] if activate_host_rules else global_rules\n\n\ndef get_remapped_colorspace_to_native(\n    ocio_colorspace_name, host_name, imageio_host_settings\n):\n    \"\"\"Return native colorspace name.\n\n    Args:\n        ocio_colorspace_name (str | None): ocio colorspace name\n        host_name (str): Host name.\n        imageio_host_settings (dict[str, Any]): ImageIO host settings.\n\n    Returns:\n        Union[str, None]: native colorspace name defined in remapping or None\n    \"\"\"\n\n    CachedData.remapping.setdefault(host_name, {})\n    if CachedData.remapping[host_name].get(\"to_native\") is None:\n        remapping_rules = imageio_host_settings[\"remapping\"][\"rules\"]\n        CachedData.remapping[host_name][\"to_native\"] = {\n            rule[\"ocio_name\"]: rule[\"host_native_name\"]\n            for rule in remapping_rules\n        }\n\n    return CachedData.remapping[host_name][\"to_native\"].get(\n        ocio_colorspace_name)\n\n\ndef get_remapped_colorspace_from_native(\n    host_native_colorspace_name, host_name, imageio_host_settings\n):\n    \"\"\"Return ocio colorspace name remapped from host native used name.\n\n    Args:\n        host_native_colorspace_name (str): host native colorspace name\n        host_name (str): Host name.\n        imageio_host_settings (dict[str, Any]): ImageIO host settings.\n\n    Returns:\n        Union[str, None]: Ocio colorspace name defined in remapping or None.\n    \"\"\"\n\n    CachedData.remapping.setdefault(host_name, {})\n    if CachedData.remapping[host_name].get(\"from_native\") is None:\n        remapping_rules = imageio_host_settings[\"remapping\"][\"rules\"]\n        CachedData.remapping[host_name][\"from_native\"] = {\n            rule[\"host_native_name\"]: rule[\"ocio_name\"]\n            for rule in remapping_rules\n        }\n\n    return CachedData.remapping[host_name][\"from_native\"].get(\n        host_native_colorspace_name)\n\n\ndef _get_imageio_settings(project_settings, host_name):\n    \"\"\"Get ImageIO settings for global and host\n\n    Args:\n        project_settings (dict): project settings.\n                                 Defaults to None.\n        host_name (str): host name\n\n    Returns:\n        tuple[dict, dict]: image io settings for global and host\n    \"\"\"\n    # get image io from global and host_name\n    imageio_global = project_settings[\"global\"][\"imageio\"]\n    # host is optional, some might not have any settings\n    imageio_host = project_settings.get(host_name, {}).get(\"imageio\", {})\n\n    return imageio_global, imageio_host\n\n\ndef get_colorspace_settings_from_publish_context(context_data):\n    \"\"\"Returns solved settings for the host context.\n\n    Args:\n        context_data (publish.Context.data): publishing context data\n\n    Returns:\n        tuple | bool: config, file rules or None\n    \"\"\"\n    if \"imageioSettings\" in context_data and context_data[\"imageioSettings\"]:\n        return context_data[\"imageioSettings\"]\n\n    project_name = context_data[\"projectName\"]\n    host_name = context_data[\"hostName\"]\n    anatomy_data = context_data[\"anatomyData\"]\n    project_settings_ = context_data[\"project_settings\"]\n\n    config_data = get_imageio_config(\n        project_name, host_name,\n        project_settings=project_settings_,\n        anatomy_data=anatomy_data\n    )\n\n    # caching invalid state, so it's not recalculated all the time\n    file_rules = None\n    if config_data:\n        file_rules = get_imageio_file_rules(\n            project_name, host_name,\n            project_settings=project_settings_\n        )\n\n    # caching settings for future instance processing\n    context_data[\"imageioSettings\"] = (config_data, file_rules)\n\n    return config_data, file_rules\n\n\ndef set_colorspace_data_to_representation(\n    representation, context_data,\n    colorspace=None,\n    log=None\n):\n    \"\"\"Sets colorspace data to representation.\n\n    Args:\n        representation (dict): publishing representation\n        context_data (publish.Context.data): publishing context data\n        colorspace (str, optional): colorspace name. Defaults to None.\n        log (logging.Logger, optional): logger instance. Defaults to None.\n\n    Example:\n        ```\n        {\n            # for other publish plugins and loaders\n            \"colorspace\": \"linear\",\n            \"config\": {\n                # for future references in case need\n                \"path\": \"/abs/path/to/config.ocio\",\n                # for other plugins within remote publish cases\n                \"template\": \"{project[root]}/path/to/config.ocio\"\n            }\n        }\n        ```\n\n    \"\"\"\n    log = log or Logger.get_logger(__name__)\n\n    file_ext = representation[\"ext\"]\n\n    # check if `file_ext` in lower case is in CachedData.allowed_exts\n    if file_ext.lstrip(\".\").lower() not in CachedData.allowed_exts:\n        log.debug(\n            \"Extension '{}' is not in allowed extensions.\".format(file_ext)\n        )\n        return\n\n    # get colorspace settings\n    config_data, file_rules = get_colorspace_settings_from_publish_context(\n        context_data)\n\n    # in case host color management is not enabled\n    if not config_data:\n        log.warning(\"Host's colorspace management is disabled.\")\n        return\n\n    log.debug(\"Config data is: `{}`\".format(config_data))\n\n    project_name = context_data[\"projectName\"]\n    host_name = context_data[\"hostName\"]\n    project_settings = context_data[\"project_settings\"]\n\n    # get one filename\n    filename = representation[\"files\"]\n    if isinstance(filename, list):\n        filename = filename[0]\n\n    # get matching colorspace from rules\n    colorspace = colorspace or get_imageio_colorspace_from_filepath(\n        filename, host_name, project_name,\n        config_data=config_data,\n        file_rules=file_rules,\n        project_settings=project_settings\n    )\n\n    # infuse data to representation\n    if colorspace:\n        colorspace_data = {\n            \"colorspace\": colorspace,\n            \"config\": config_data\n        }\n\n        # update data key\n        representation[\"colorspaceData\"] = colorspace_data\n\n\ndef get_display_view_colorspace_name(config_path, display, view):\n    \"\"\"Returns the colorspace attribute of the (display, view) pair.\n\n    Args:\n        config_path (str): path string leading to config.ocio\n        display (str): display name e.g. \"ACES\"\n        view (str): view name e.g. \"sRGB\"\n\n    Returns:\n        view color space name (str) e.g. \"Output - sRGB\"\n    \"\"\"\n\n    if not compatibility_check():\n        # python environment is not compatible with PyOpenColorIO\n        # needs to be run in subprocess\n        return get_display_view_colorspace_subprocess(config_path,\n                                                      display, view)\n\n    from openpype.scripts.ocio_wrapper import _get_display_view_colorspace_name  # noqa\n\n    return _get_display_view_colorspace_name(config_path, display, view)\n\n\ndef get_display_view_colorspace_subprocess(config_path, display, view):\n    \"\"\"Returns the colorspace attribute of the (display, view) pair\n        via subprocess.\n\n    Args:\n        config_path (str): path string leading to config.ocio\n        display (str): display name e.g. \"ACES\"\n        view (str): view name e.g. \"sRGB\"\n\n    Returns:\n        view color space name (str) e.g. \"Output - sRGB\"\n    \"\"\"\n\n    with _make_temp_json_file() as tmp_json_path:\n        # Prepare subprocess arguments\n        args = [\n            \"run\", get_ocio_config_script_path(),\n            \"config\", \"get_display_view_colorspace_name\",\n            \"--in_path\", config_path,\n            \"--out_path\", tmp_json_path,\n            \"--display\", display,\n            \"--view\", view\n        ]\n        log.debug(\"Executing: {}\".format(\" \".join(args)))\n\n        run_openpype_process(*args, logger=log)\n\n        # return default view colorspace name\n        with open(tmp_json_path, \"r\") as f:\n            return json.load(f)\n"
  },
  {
    "path": "openpype/pipeline/constants.py",
    "content": "# Metadata ID of loaded container into scene\nAVALON_CONTAINER_ID = AYON_CONTAINER_ID = \"pyblish.avalon.container\"\n\n# TODO get extensions from host implementations\nHOST_WORKFILE_EXTENSIONS = {\n    \"blender\": [\".blend\"],\n    \"celaction\": [\".scn\"],\n    \"tvpaint\": [\".tvpp\"],\n    \"fusion\": [\".comp\"],\n    \"harmony\": [\".zip\"],\n    \"houdini\": [\".hip\", \".hiplc\", \".hipnc\"],\n    \"maya\": [\".ma\", \".mb\"],\n    \"nuke\": [\".nk\"],\n    \"hiero\": [\".hrox\"],\n    \"photoshop\": [\".psd\", \".psb\"],\n    \"premiere\": [\".prproj\"],\n    \"resolve\": [\".drp\"],\n    \"aftereffects\": [\".aep\"]\n}\n"
  },
  {
    "path": "openpype/pipeline/context_tools.py",
    "content": "\"\"\"Core pipeline functionality\"\"\"\n\nimport os\nimport json\nimport types\nimport logging\nimport platform\nimport uuid\n\nimport pyblish.api\nfrom pyblish.lib import MessageHandler\n\nimport openpype\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.host import HostBase\nfrom openpype.client import (\n    get_project,\n    get_asset_by_id,\n    get_asset_by_name,\n    version_is_latest,\n    get_asset_name_identifier,\n    get_ayon_server_api_connection,\n)\nfrom openpype.lib.events import emit_event\nfrom openpype.modules import load_modules, ModulesManager\nfrom openpype.settings import get_project_settings\nfrom openpype.tests.lib import is_in_tests\n\nfrom .publish.lib import filter_pyblish_plugins\nfrom .anatomy import Anatomy\nfrom .template_data import get_template_data_with_names\nfrom .workfile import (\n    get_workfile_template_key,\n    get_custom_workfile_template_by_string_context,\n)\nfrom . import (\n    legacy_io,\n    register_loader_plugin_path,\n    register_inventory_action_path,\n    register_creator_plugin_path,\n    deregister_loader_plugin_path,\n    deregister_inventory_action_path\n)\n\n\n_is_installed = False\n_process_id = None\n_registered_root = {\"_\": {}}\n_registered_host = {\"_\": None}\n# Keep modules manager (and it's modules) in memory\n# - that gives option to register modules' callbacks\n_modules_manager = None\n\nlog = logging.getLogger(__name__)\n\nPACKAGE_DIR = os.path.dirname(os.path.abspath(openpype.__file__))\nPLUGINS_DIR = os.path.join(PACKAGE_DIR, \"plugins\")\n\n# Global plugin paths\nPUBLISH_PATH = os.path.join(PLUGINS_DIR, \"publish\")\nLOAD_PATH = os.path.join(PLUGINS_DIR, \"load\")\nINVENTORY_PATH = os.path.join(PLUGINS_DIR, \"inventory\")\n\n\ndef _get_modules_manager():\n    \"\"\"Get or create modules manager for host installation.\n\n    This is not meant for public usage. Reason is to keep modules\n    in memory of process to be able trigger their event callbacks if they\n    need any.\n\n    Returns:\n        ModulesManager: Manager wrapping discovered modules.\n    \"\"\"\n\n    global _modules_manager\n    if _modules_manager is None:\n        _modules_manager = ModulesManager()\n    return _modules_manager\n\n\ndef register_root(path):\n    \"\"\"Register currently active root\"\"\"\n    log.info(\"Registering root: %s\" % path)\n    _registered_root[\"_\"] = path\n\n\ndef registered_root():\n    \"\"\"Return registered roots from current project anatomy.\n\n    Consider this does return roots only for current project and current\n        platforms, only if host was installer using 'install_host'.\n\n    Deprecated:\n        Please use project 'Anatomy' to get roots. This function is still used\n            at current core functions of load logic, but that will change\n            in future and this function will be removed eventually. Using this\n            function at new places can cause problems in the future.\n\n    Returns:\n        dict[str, str]: Root paths.\n    \"\"\"\n\n    return _registered_root[\"_\"]\n\n\ndef install_host(host):\n    \"\"\"Install `host` into the running Python session.\n\n    Args:\n        host (module): A Python module containing the Avalon\n            avalon host-interface.\n    \"\"\"\n    global _is_installed\n\n    _is_installed = True\n\n    # Make sure global AYON connection has set site id and version\n    if AYON_SERVER_ENABLED:\n        get_ayon_server_api_connection()\n\n    legacy_io.install()\n    modules_manager = _get_modules_manager()\n\n    missing = list()\n    for key in (\"AVALON_PROJECT\", \"AVALON_ASSET\"):\n        if key not in legacy_io.Session:\n            missing.append(key)\n\n    assert not missing, (\n        \"%s missing from environment, %s\" % (\n            \", \".join(missing),\n            json.dumps(legacy_io.Session, indent=4, sort_keys=True)\n        ))\n\n    project_name = legacy_io.Session[\"AVALON_PROJECT\"]\n    log.info(\"Activating %s..\" % project_name)\n\n    # Optional host install function\n    if hasattr(host, \"install\"):\n        host.install()\n\n    register_host(host)\n\n    def modified_emit(obj, record):\n        \"\"\"Method replacing `emit` in Pyblish's MessageHandler.\"\"\"\n        record.msg = record.getMessage()\n        obj.records.append(record)\n\n    MessageHandler.emit = modified_emit\n\n    if os.environ.get(\"OPENPYPE_REMOTE_PUBLISH\"):\n        # target \"farm\" == rendering on farm, expects OPENPYPE_PUBLISH_DATA\n        # target \"remote\" == remote execution, installs host\n        print(\"Registering pyblish target: remote\")\n        pyblish.api.register_target(\"remote\")\n    else:\n        pyblish.api.register_target(\"local\")\n\n    if is_in_tests():\n        print(\"Registering pyblish target: automated\")\n        pyblish.api.register_target(\"automated\")\n\n    project_name = os.environ.get(\"AVALON_PROJECT\")\n    host_name = os.environ.get(\"AVALON_APP\")\n\n    # Give option to handle host installation\n    for module in modules_manager.get_enabled_modules():\n        module.on_host_install(host, host_name, project_name)\n\n    install_openpype_plugins(project_name, host_name)\n\n\ndef install_openpype_plugins(project_name=None, host_name=None):\n    # Make sure modules are loaded\n    load_modules()\n\n    log.info(\"Registering global plug-ins..\")\n    pyblish.api.register_plugin_path(PUBLISH_PATH)\n    pyblish.api.register_discovery_filter(filter_pyblish_plugins)\n    register_loader_plugin_path(LOAD_PATH)\n    register_inventory_action_path(INVENTORY_PATH)\n\n    if host_name is None:\n        host_name = os.environ.get(\"AVALON_APP\")\n\n    modules_manager = _get_modules_manager()\n    publish_plugin_dirs = modules_manager.collect_publish_plugin_paths(\n        host_name)\n    for path in publish_plugin_dirs:\n        pyblish.api.register_plugin_path(path)\n\n    create_plugin_paths = modules_manager.collect_create_plugin_paths(\n        host_name)\n    for path in create_plugin_paths:\n        register_creator_plugin_path(path)\n\n    load_plugin_paths = modules_manager.collect_load_plugin_paths(\n        host_name)\n    for path in load_plugin_paths:\n        register_loader_plugin_path(path)\n\n    inventory_action_paths = modules_manager.collect_inventory_action_paths(\n        host_name)\n    for path in inventory_action_paths:\n        register_inventory_action_path(path)\n\n    if project_name is None:\n        project_name = os.environ.get(\"AVALON_PROJECT\")\n\n    # Register studio specific plugins\n    if project_name:\n        anatomy = Anatomy(project_name)\n        anatomy.set_root_environments()\n        register_root(anatomy.roots)\n\n        project_settings = get_project_settings(project_name)\n        platform_name = platform.system().lower()\n        project_plugins = (\n            project_settings\n            .get(\"global\", {})\n            .get(\"project_plugins\", {})\n            .get(platform_name)\n        ) or []\n        for path in project_plugins:\n            try:\n                path = str(path.format(**os.environ))\n            except KeyError:\n                pass\n\n            if not path or not os.path.exists(path):\n                continue\n\n            pyblish.api.register_plugin_path(path)\n            register_loader_plugin_path(path)\n            register_creator_plugin_path(path)\n            register_inventory_action_path(path)\n\n\ndef uninstall_host():\n    \"\"\"Undo all of what `install()` did\"\"\"\n    host = registered_host()\n\n    try:\n        host.uninstall()\n    except AttributeError:\n        pass\n\n    log.info(\"Deregistering global plug-ins..\")\n    pyblish.api.deregister_plugin_path(PUBLISH_PATH)\n    pyblish.api.deregister_discovery_filter(filter_pyblish_plugins)\n    deregister_loader_plugin_path(LOAD_PATH)\n    deregister_inventory_action_path(INVENTORY_PATH)\n    log.info(\"Global plug-ins unregistred\")\n\n    deregister_host()\n\n    legacy_io.uninstall()\n\n    log.info(\"Successfully uninstalled Avalon!\")\n\n\ndef is_installed():\n    \"\"\"Return state of installation\n\n    Returns:\n        True if installed, False otherwise\n\n    \"\"\"\n\n    return _is_installed\n\n\ndef register_host(host):\n    \"\"\"Register a new host for the current process\n\n    Arguments:\n        host (ModuleType): A module implementing the\n            Host API interface. See the Host API\n            documentation for details on what is\n            required, or browse the source code.\n\n    \"\"\"\n\n    _registered_host[\"_\"] = host\n\n\ndef registered_host():\n    \"\"\"Return currently registered host\"\"\"\n    return _registered_host[\"_\"]\n\n\ndef deregister_host():\n    _registered_host[\"_\"] = None\n\n\ndef debug_host():\n    \"\"\"A debug host, useful to debugging features that depend on a host\"\"\"\n\n    host = types.ModuleType(\"debugHost\")\n\n    def ls():\n        containers = [\n            {\n                \"representation\": \"ee-ft-a-uuid1\",\n                \"schema\": \"openpype:container-1.0\",\n                \"name\": \"Bruce01\",\n                \"objectName\": \"Bruce01_node\",\n                \"namespace\": \"_bruce01_\",\n                \"version\": 3,\n            },\n            {\n                \"representation\": \"aa-bc-s-uuid2\",\n                \"schema\": \"openpype:container-1.0\",\n                \"name\": \"Bruce02\",\n                \"objectName\": \"Bruce01_node\",\n                \"namespace\": \"_bruce02_\",\n                \"version\": 2,\n            }\n        ]\n\n        for container in containers:\n            yield container\n\n    host.__dict__.update({\n        \"ls\": ls,\n        \"open_file\": lambda fname: None,\n        \"save_file\": lambda fname: None,\n        \"current_file\": lambda: os.path.expanduser(\"~/temp.txt\"),\n        \"has_unsaved_changes\": lambda: False,\n        \"work_root\": lambda: os.path.expanduser(\"~/temp\"),\n        \"file_extensions\": lambda: [\"txt\"],\n    })\n\n    return host\n\n\ndef get_current_host_name():\n    \"\"\"Current host name.\n\n    Function is based on currently registered host integration or environment\n    variable 'AVALON_APP'.\n\n    Returns:\n        Union[str, None]: Name of host integration in current process or None.\n    \"\"\"\n\n    host = registered_host()\n    if isinstance(host, HostBase):\n        return host.name\n    return os.environ.get(\"AVALON_APP\")\n\n\ndef get_global_context():\n    \"\"\"Global context defined in environment variables.\n\n    Values here may not reflect current context of host integration. The\n    function can be used on startup before a host is registered.\n\n    Use 'get_current_context' to make sure you'll get current host integration\n    context info.\n\n    Example:\n        {\n            \"project_name\": \"Commercial\",\n            \"asset_name\": \"Bunny\",\n            \"task_name\": \"Animation\",\n        }\n\n    Returns:\n        dict[str, Union[str, None]]: Context defined with environment\n            variables.\n    \"\"\"\n\n    return {\n        \"project_name\": os.environ.get(\"AVALON_PROJECT\"),\n        \"asset_name\": os.environ.get(\"AVALON_ASSET\"),\n        \"task_name\": os.environ.get(\"AVALON_TASK\"),\n    }\n\n\ndef get_current_context():\n    host = registered_host()\n    if isinstance(host, HostBase):\n        return host.get_current_context()\n    return get_global_context()\n\n\ndef get_current_project_name():\n    host = registered_host()\n    if isinstance(host, HostBase):\n        return host.get_current_project_name()\n    return get_global_context()[\"project_name\"]\n\n\ndef get_current_asset_name():\n    host = registered_host()\n    if isinstance(host, HostBase):\n        return host.get_current_asset_name()\n    return get_global_context()[\"asset_name\"]\n\n\ndef get_current_task_name():\n    host = registered_host()\n    if isinstance(host, HostBase):\n        return host.get_current_task_name()\n    return get_global_context()[\"task_name\"]\n\n\ndef get_current_project(fields=None):\n    \"\"\"Helper function to get project document based on global Session.\n\n    This function should be called only in process where host is installed.\n\n    Returns:\n        dict: Project document.\n        None: Project is not set.\n    \"\"\"\n\n    project_name = get_current_project_name()\n    return get_project(project_name, fields=fields)\n\n\ndef get_current_project_asset(asset_name=None, asset_id=None, fields=None):\n    \"\"\"Helper function to get asset document based on global Session.\n\n    This function should be called only in process where host is installed.\n\n    Asset is found out based on passed asset name or id (not both). Asset name\n    is not used for filtering if asset id is passed. When both asset name and\n    id are missing then asset name from current process is used.\n\n    Args:\n        asset_name (str): Name of asset used for filter.\n        asset_id (Union[str, ObjectId]): Asset document id. If entered then\n            is used as only filter.\n        fields (Union[List[str], None]): Limit returned data of asset documents\n            to specific keys.\n\n    Returns:\n        dict: Asset document.\n        None: Asset is not set or not exist.\n    \"\"\"\n\n    project_name = get_current_project_name()\n    if asset_id:\n        return get_asset_by_id(project_name, asset_id, fields=fields)\n\n    if not asset_name:\n        asset_name = get_current_asset_name()\n        # Skip if is not set even on context\n        if not asset_name:\n            return None\n    return get_asset_by_name(project_name, asset_name, fields=fields)\n\n\ndef is_representation_from_latest(representation):\n    \"\"\"Return whether the representation is from latest version\n\n    Args:\n        representation (dict): The representation document from the database.\n\n    Returns:\n        bool: Whether the representation is of latest version.\n    \"\"\"\n\n    project_name = get_current_project_name()\n    return version_is_latest(project_name, representation[\"parent\"])\n\n\ndef get_template_data_from_session(session=None, system_settings=None):\n    \"\"\"Template data for template fill from session keys.\n\n    Args:\n        session (Union[Dict[str, str], None]): The Session to use. If not\n            provided use the currently active global Session.\n        system_settings (Union[Dict[str, Any], Any]): Prepared system settings.\n            Optional are auto received if not passed.\n\n    Returns:\n        Dict[str, Any]: All available data from session.\n    \"\"\"\n\n    if session is None:\n        session = legacy_io.Session\n\n    project_name = session[\"AVALON_PROJECT\"]\n    asset_name = session[\"AVALON_ASSET\"]\n    task_name = session[\"AVALON_TASK\"]\n    host_name = session[\"AVALON_APP\"]\n\n    return get_template_data_with_names(\n        project_name, asset_name, task_name, host_name, system_settings\n    )\n\n\ndef get_current_context_template_data(system_settings=None):\n    \"\"\"Prepare template data for current context.\n\n    Args:\n        system_settings (Optional[Dict[str, Any]]): Prepared system settings.\n\n    Returns:\n        Dict[str, Any] Template data for current context.\n    \"\"\"\n\n    context = get_current_context()\n    project_name = context[\"project_name\"]\n    asset_name = context[\"asset_name\"]\n    task_name = context[\"task_name\"]\n    host_name = get_current_host_name()\n\n    return get_template_data_with_names(\n        project_name, asset_name, task_name, host_name, system_settings\n    )\n\n\ndef get_workdir_from_session(session=None, template_key=None):\n    \"\"\"Template data for template fill from session keys.\n\n    Args:\n        session (Union[Dict[str, str], None]): The Session to use. If not\n            provided use the currently active global Session.\n        template_key (str): Prepared template key from which workdir is\n            calculated.\n\n    Returns:\n        str: Workdir path.\n    \"\"\"\n\n    if session is None:\n        session = legacy_io.Session\n    project_name = session[\"AVALON_PROJECT\"]\n    host_name = session[\"AVALON_APP\"]\n    template_data = get_template_data_from_session(session)\n\n    if not template_key:\n        task_type = template_data[\"task\"][\"type\"]\n        template_key = get_workfile_template_key(\n            task_type,\n            host_name,\n            project_name=project_name\n        )\n\n    anatomy = Anatomy(project_name)\n    template_obj = anatomy.templates_obj[template_key][\"folder\"]\n    path = template_obj.format_strict(template_data)\n    if path:\n        path = os.path.normpath(path)\n    return path\n\n\ndef get_custom_workfile_template_from_session(\n    session=None, project_settings=None\n):\n    \"\"\"Filter and fill workfile template profiles by current context.\n\n    Current context is defined by `legacy_io.Session`. That's why this\n    function should be used only inside host where context is set and stable.\n\n    Args:\n        session (Union[None, Dict[str, str]]): Session from which are taken\n            data.\n        project_settings(Dict[str, Any]): Template profiles from settings.\n\n    Returns:\n        str: Path to template or None if none of profiles match current\n            context. (Existence of formatted path is not validated.)\n    \"\"\"\n\n    if session is None:\n        session = legacy_io.Session\n\n    return get_custom_workfile_template_by_string_context(\n        session[\"AVALON_PROJECT\"],\n        session[\"AVALON_ASSET\"],\n        session[\"AVALON_TASK\"],\n        session[\"AVALON_APP\"],\n        project_settings=project_settings\n    )\n\n\ndef compute_session_changes(\n    session, asset_doc, task_name, template_key=None\n):\n    \"\"\"Compute the changes for a session object on task under asset.\n\n    Function does not change the session object, only returns changes.\n\n    Args:\n        session (Dict[str, str]): The initial session to compute changes to.\n            This is required for computing the full Work Directory, as that\n            also depends on the values that haven't changed.\n        asset_doc (Dict[str, Any]): Asset document to switch to.\n        task_name (str): Name of task to switch to.\n        template_key (Union[str, None]): Prepare workfile template key in\n            anatomy templates.\n\n    Returns:\n        Dict[str, str]: Changes in the Session dictionary.\n    \"\"\"\n\n    # Get asset document and asset\n    if not asset_doc:\n        task_name = None\n        asset_name = None\n    else:\n        asset_name = get_asset_name_identifier(asset_doc)\n\n    # Detect any changes compared session\n    mapping = {\n        \"AVALON_ASSET\": asset_name,\n        \"AVALON_TASK\": task_name,\n    }\n    changes = {\n        key: value\n        for key, value in mapping.items()\n        if value != session.get(key)\n    }\n    if not changes:\n        return changes\n\n    # Compute work directory (with the temporary changed session so far)\n    changed_session = session.copy()\n    changed_session.update(changes)\n\n    workdir = None\n    if asset_doc:\n        workdir = get_workdir_from_session(\n            changed_session, template_key\n        )\n\n    changes[\"AVALON_WORKDIR\"] = workdir\n\n    return changes\n\n\ndef change_current_context(asset_doc, task_name, template_key=None):\n    \"\"\"Update active Session to a new task work area.\n\n    This updates the live Session to a different task under asset.\n\n    Args:\n        asset_doc (Dict[str, Any]): The asset document to set.\n        task_name (str): The task to set under asset.\n        template_key (Union[str, None]): Prepared template key to be used for\n            workfile template in Anatomy.\n\n    Returns:\n        Dict[str, str]: The changed key, values in the current Session.\n    \"\"\"\n\n    changes = compute_session_changes(\n        legacy_io.Session,\n        asset_doc,\n        task_name,\n        template_key=template_key\n    )\n\n    # Update the Session and environments. Pop from environments all keys with\n    # value set to None.\n    for key, value in changes.items():\n        legacy_io.Session[key] = value\n        if value is None:\n            os.environ.pop(key, None)\n        else:\n            os.environ[key] = value\n\n    data = changes.copy()\n    # Convert env keys to human readable keys\n    data[\"project_name\"] = legacy_io.Session[\"AVALON_PROJECT\"]\n    data[\"asset_name\"] = legacy_io.Session[\"AVALON_ASSET\"]\n    data[\"task_name\"] = legacy_io.Session[\"AVALON_TASK\"]\n\n    # Emit session change\n    emit_event(\"taskChanged\", data)\n\n    return changes\n\n\ndef get_process_id():\n    \"\"\"Fake process id created on demand using uuid.\n\n    Can be used to create process specific folders in temp directory.\n\n    Returns:\n        str: Process id.\n    \"\"\"\n\n    global _process_id\n    if _process_id is None:\n        _process_id = str(uuid.uuid4())\n    return _process_id\n"
  },
  {
    "path": "openpype/pipeline/create/README.md",
    "content": "# Create\nCreation is process defying what and how will be published. May work in a different way based on host implementation.\n\n## CreateContext\nEntry point of creation. All data and metadata are handled through create context. Context hold all global data and instances. Is responsible for loading of plugins (create, publish), triggering creator methods, validation of host implementation and emitting changes to creators and host.\n\nDiscovers Creator plugins to be able create new instances and convert existing instances. Creators may have defined attributes that are specific for their instances. Attributes definition can enhance behavior of instance during publishing.\n\nPublish plugins are loaded because they can also define attributes definitions. These are less family specific To be able define attributes Publish plugin must inherit from `OpenPypePyblishPluginMixin` and must override `get_attribute_defs` class method which must return list of attribute definitions. Values of publish plugin definitions are stored per plugin name under `publish_attributes`. Also can override `convert_attribute_values` class method which gives ability to modify values on instance before are used in CreatedInstance. Method `convert_attribute_values` can be also used without `get_attribute_defs` to modify values when changing compatibility (remove metadata from instance because are irrelevant).\n\nPossible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`.\n\nExcept creating and removing instances are all changes not automatically propagated to host context (scene/workfile/...) to propagate changes call `save_changes` which trigger update of all instances in context using Creators implementation.\n\n\n## CreatedInstance\nProduct of creation is \"instance\" which holds basic data defying it. Core data are `creator_identifier`, `family` and `subset`. Other data can be keys used to fill subset name or metadata modifying publishing process of the instance (more described later). All instances have `id` which holds constant `pyblish.avalon.instance` and `instance_id` which is identifier of the instance.\nFamily tells how should be instance processed and subset what name will published item have.\n- There are cases when subset is not fully filled during creation and may change during publishing. That is in most of cases caused because instance is related to other instance or instance data do not represent final product.\n\n`CreatedInstance` is entity holding the data which are stored and used.\n\n```python\n{\n    # Immutable data after creation\n    ## Identifier that this data represents instance for publishing (automatically assigned)\n    \"id\": \"pyblish.avalon.instance\",\n    ## Identifier of this specific instance (automatically assigned)\n    \"instance_id\": <uuid4>,\n    ## Instance family (used from Creator)\n    \"family\": <family>,\n\n    # Mutable data\n    ## Subset name based on subset name template - may change overtime (on context change)\n    \"subset\": <subset>,\n    ## Instance is active and will be published\n    \"active\": True,\n    ## Version of instance\n    \"version\": 1,\n    # Identifier of creator (is unique)\n    \"creator_identifier\": \"\",\n    ## Creator specific attributes (defined by Creator)\n    \"creator_attributes\": {...},\n    ## Publish plugin specific plugins (defined by Publish plugin)\n    \"publish_attributes\": {\n        # Attribute values are stored by publish plugin name\n        #   - Duplicated plugin names can cause clashes!\n        <Plugin name>: {...},\n        ...\n    },\n    ## Additional data related to instance (`asset`, `task`, etc.)\n    ...\n}\n```\n\n## Creator\nTo be able create, update, remove or collect existing instances there must be defined a creator. Creator must have unique identifier and can represents a family. There can be multiple Creators for single family. Identifier of creator should contain family (advise).\n\nCreator has abstract methods to handle instances. For new instance creation is used `create` which should create metadata in host context and add new instance object to `CreateContext`. To collect existing instances is used `collect_instances` which should find all existing instances related to creator and add them to `CreateContext`. To update data of instance is used `update_instances` which is called from `CreateContext` on `save_changes`. To remove instance use `remove_instances` which should remove metadata from host context and remove instance from `CreateContext`.\n\nCreator has access to `CreateContext` which created object of the creator. All new instances or removed instances must be told to context. To do so use methods `_add_instance_to_context` and `_remove_instance_from_context` where `CreatedInstance` is passed. They should be called from `create` if new instance was created and from `remove_instances` if instance was removed.\n\nCreators don't have strictly defined how are instances handled but it is good practice to define a way which is host specific. It is not strict because there are cases when host implementation just can't handle all requirements of all creators.\n\n### AutoCreator\nAuto-creators are automatically executed when `CreateContext` is reset. They can be used to create instances that should be always available and may not require artist's manual creation (e.g. `workfile`). Should not create duplicated instance and validate existence before creates a new. Method `remove_instances` is implemented to do nothing.\n\n## Host\nHost implementation must have available global context metadata handler functions. One to get current context data and second to update them. Currently are to context data stored only context publish plugin attribute values.\n\n### Get global context data (`get_context_data`)\nThere are data that are not specific for any instance but are specific for whole context (e.g. Context plugins values).\n\n### Update global context data (`update_context_data`)\nUpdate global context data.\n\n### Optional title of context\nIt is recommended to implement `get_context_title` function. String returned from this function will be shown in UI as context in which artist is.\n"
  },
  {
    "path": "openpype/pipeline/create/__init__.py",
    "content": "from .constants import (\n    SUBSET_NAME_ALLOWED_SYMBOLS,\n    DEFAULT_SUBSET_TEMPLATE,\n    PRE_CREATE_THUMBNAIL_KEY,\n    DEFAULT_VARIANT_VALUE,\n)\n\nfrom .utils import (\n    get_last_versions_for_instances,\n    get_next_versions_for_instances,\n)\n\nfrom .subset_name import (\n    TaskNotSetError,\n    get_subset_name_template,\n    get_subset_name,\n)\n\nfrom .creator_plugins import (\n    CreatorError,\n\n    BaseCreator,\n    Creator,\n    AutoCreator,\n    HiddenCreator,\n\n    discover_legacy_creator_plugins,\n    get_legacy_creator_by_name,\n\n    discover_creator_plugins,\n    register_creator_plugin,\n    deregister_creator_plugin,\n    register_creator_plugin_path,\n    deregister_creator_plugin_path,\n\n    cache_and_get_instances,\n)\n\nfrom .context import (\n    CreatedInstance,\n    CreateContext\n)\n\nfrom .legacy_create import (\n    LegacyCreator,\n    legacy_create,\n)\n\n\n__all__ = (\n    \"SUBSET_NAME_ALLOWED_SYMBOLS\",\n    \"DEFAULT_SUBSET_TEMPLATE\",\n    \"PRE_CREATE_THUMBNAIL_KEY\",\n    \"DEFAULT_VARIANT_VALUE\",\n\n    \"get_last_versions_for_instances\",\n    \"get_next_versions_for_instances\",\n\n    \"TaskNotSetError\",\n    \"get_subset_name_template\",\n    \"get_subset_name\",\n\n    \"CreatorError\",\n\n    \"BaseCreator\",\n    \"Creator\",\n    \"AutoCreator\",\n    \"HiddenCreator\",\n\n    \"discover_legacy_creator_plugins\",\n    \"get_legacy_creator_by_name\",\n\n    \"discover_creator_plugins\",\n    \"register_creator_plugin\",\n    \"deregister_creator_plugin\",\n    \"register_creator_plugin_path\",\n    \"deregister_creator_plugin_path\",\n\n    \"cache_and_get_instances\",\n\n    \"CreatedInstance\",\n    \"CreateContext\",\n\n    \"LegacyCreator\",\n    \"legacy_create\",\n)\n"
  },
  {
    "path": "openpype/pipeline/create/constants.py",
    "content": "SUBSET_NAME_ALLOWED_SYMBOLS = \"a-zA-Z0-9_.\"\nDEFAULT_SUBSET_TEMPLATE = \"{family}{Variant}\"\nPRE_CREATE_THUMBNAIL_KEY = \"thumbnail_source\"\nDEFAULT_VARIANT_VALUE = \"Main\"\n\n\n__all__ = (\n    \"SUBSET_NAME_ALLOWED_SYMBOLS\",\n    \"DEFAULT_SUBSET_TEMPLATE\",\n    \"PRE_CREATE_THUMBNAIL_KEY\",\n    \"DEFAULT_VARIANT_VALUE\",\n)\n"
  },
  {
    "path": "openpype/pipeline/create/context.py",
    "content": "import os\nimport sys\nimport copy\nimport logging\nimport traceback\nimport collections\nimport inspect\nfrom uuid import uuid4\nfrom contextlib import contextmanager\n\nimport pyblish.logic\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_assets,\n    get_asset_by_name,\n    get_asset_name_identifier,\n)\nfrom openpype.settings import (\n    get_system_settings,\n    get_project_settings\n)\nfrom openpype.lib.attribute_definitions import (\n    UnknownDef,\n    serialize_attr_defs,\n    deserialize_attr_defs,\n    get_default_values,\n)\nfrom openpype.host import IPublishHost, IWorkfileHost\nfrom openpype.pipeline import legacy_io, Anatomy\nfrom openpype.pipeline.plugin_discover import DiscoverResult\n\nfrom .creator_plugins import (\n    Creator,\n    AutoCreator,\n    discover_creator_plugins,\n    discover_convertor_plugins,\n    CreatorError,\n)\n\n# Changes of instances and context are send as tuple of 2 information\nUpdateData = collections.namedtuple(\"UpdateData\", [\"instance\", \"changes\"])\n\n\nclass UnavailableSharedData(Exception):\n    \"\"\"Shared data are not available at the moment when are accessed.\"\"\"\n    pass\n\n\nclass ImmutableKeyError(TypeError):\n    \"\"\"Accessed key is immutable so does not allow changes or removements.\"\"\"\n\n    def __init__(self, key, msg=None):\n        self.immutable_key = key\n        if not msg:\n            msg = \"Key \\\"{}\\\" is immutable and does not allow changes.\".format(\n                key\n            )\n        super(ImmutableKeyError, self).__init__(msg)\n\n\nclass HostMissRequiredMethod(Exception):\n    \"\"\"Host does not have implemented required functions for creation.\"\"\"\n\n    def __init__(self, host, missing_methods):\n        self.missing_methods = missing_methods\n        self.host = host\n        joined_methods = \", \".join(\n            ['\"{}\"'.format(name) for name in missing_methods]\n        )\n        dirpath = os.path.dirname(\n            os.path.normpath(inspect.getsourcefile(host))\n        )\n        dirpath_parts = dirpath.split(os.path.sep)\n        host_name = dirpath_parts.pop(-1)\n        if host_name == \"api\":\n            host_name = dirpath_parts.pop(-1)\n\n        msg = \"Host \\\"{}\\\" does not have implemented method/s {}\".format(\n            host_name, joined_methods\n        )\n        super(HostMissRequiredMethod, self).__init__(msg)\n\n\nclass ConvertorsOperationFailed(Exception):\n    def __init__(self, msg, failed_info):\n        super(ConvertorsOperationFailed, self).__init__(msg)\n        self.failed_info = failed_info\n\n\nclass ConvertorsFindFailed(ConvertorsOperationFailed):\n    def __init__(self, failed_info):\n        msg = \"Failed to find incompatible subsets\"\n        super(ConvertorsFindFailed, self).__init__(\n            msg, failed_info\n        )\n\n\nclass ConvertorsConversionFailed(ConvertorsOperationFailed):\n    def __init__(self, failed_info):\n        msg = \"Failed to convert incompatible subsets\"\n        super(ConvertorsConversionFailed, self).__init__(\n            msg, failed_info\n        )\n\n\ndef prepare_failed_convertor_operation_info(identifier, exc_info):\n    exc_type, exc_value, exc_traceback = exc_info\n    formatted_traceback = \"\".join(traceback.format_exception(\n        exc_type, exc_value, exc_traceback\n    ))\n\n    return {\n        \"convertor_identifier\": identifier,\n        \"message\": str(exc_value),\n        \"traceback\": formatted_traceback\n    }\n\n\nclass CreatorsOperationFailed(Exception):\n    \"\"\"Raised when a creator process crashes in 'CreateContext'.\n\n    The exception contains information about the creator and error. The data\n    are prepared using 'prepare_failed_creator_operation_info' and can be\n    serialized using json.\n\n    Usage is for UI purposes which may not have access to exceptions directly\n    and would not have ability to catch exceptions 'per creator'.\n\n    Args:\n        msg (str): General error message.\n        failed_info (list[dict[str, Any]]): List of failed creators with\n            exception message and optionally formatted traceback.\n    \"\"\"\n\n    def __init__(self, msg, failed_info):\n        super(CreatorsOperationFailed, self).__init__(msg)\n        self.failed_info = failed_info\n\n\nclass CreatorsCollectionFailed(CreatorsOperationFailed):\n    def __init__(self, failed_info):\n        msg = \"Failed to collect instances\"\n        super(CreatorsCollectionFailed, self).__init__(\n            msg, failed_info\n        )\n\n\nclass CreatorsSaveFailed(CreatorsOperationFailed):\n    def __init__(self, failed_info):\n        msg = \"Failed update instance changes\"\n        super(CreatorsSaveFailed, self).__init__(\n            msg, failed_info\n        )\n\n\nclass CreatorsRemoveFailed(CreatorsOperationFailed):\n    def __init__(self, failed_info):\n        msg = \"Failed to remove instances\"\n        super(CreatorsRemoveFailed, self).__init__(\n            msg, failed_info\n        )\n\n\nclass CreatorsCreateFailed(CreatorsOperationFailed):\n    def __init__(self, failed_info):\n        msg = \"Failed to create instances\"\n        super(CreatorsCreateFailed, self).__init__(\n            msg, failed_info\n        )\n\n\ndef prepare_failed_creator_operation_info(\n    identifier, label, exc_info, add_traceback=True\n):\n    formatted_traceback = None\n    exc_type, exc_value, exc_traceback = exc_info\n    if add_traceback:\n        formatted_traceback = \"\".join(traceback.format_exception(\n            exc_type, exc_value, exc_traceback\n        ))\n\n    return {\n        \"creator_identifier\": identifier,\n        \"creator_label\": label,\n        \"message\": str(exc_value),\n        \"traceback\": formatted_traceback\n    }\n\n\n_EMPTY_VALUE = object()\n\n\nclass TrackChangesItem(object):\n    \"\"\"Helper object to track changes in data.\n\n    Has access to full old and new data and will create deep copy of them,\n    so it is not needed to create copy before passed in.\n\n    Can work as a dictionary if old or new value is a dictionary. In\n    that case received object is another object of 'TrackChangesItem'.\n\n    Goal is to be able to get old or new value as was or only changed values\n    or get information about removed/changed keys, and all of that on\n    any \"dictionary level\".\n\n    ```\n    # Example of possible usages\n    >>> old_value = {\n    ...     \"key_1\": \"value_1\",\n    ...     \"key_2\": {\n    ...         \"key_sub_1\": 1,\n    ...         \"key_sub_2\": {\n    ...             \"enabled\": True\n    ...         }\n    ...     },\n    ...     \"key_3\": \"value_2\"\n    ... }\n    >>> new_value = {\n    ...     \"key_1\": \"value_1\",\n    ...     \"key_2\": {\n    ...         \"key_sub_2\": {\n    ...             \"enabled\": False\n    ...         },\n    ...         \"key_sub_3\": 3\n    ...     },\n    ...     \"key_3\": \"value_3\"\n    ... }\n\n    >>> changes = TrackChangesItem(old_value, new_value)\n    >>> changes.changed\n    True\n\n    >>> changes[\"key_2\"][\"key_sub_1\"].new_value is None\n    True\n\n    >>> list(sorted(changes.changed_keys))\n    ['key_2', 'key_3']\n\n    >>> changes[\"key_2\"][\"key_sub_2\"][\"enabled\"].changed\n    True\n\n    >>> changes[\"key_2\"].removed_keys\n    {'key_sub_1'}\n\n    >>> list(sorted(changes[\"key_2\"].available_keys))\n    ['key_sub_1', 'key_sub_2', 'key_sub_3']\n\n    >>> changes.new_value == new_value\n    True\n\n    # Get only changed values\n    only_changed_new_values = {\n        key: changes[key].new_value\n        for key in changes.changed_keys\n    }\n    ```\n\n    Args:\n        old_value (Any): Old value.\n        new_value (Any): New value.\n    \"\"\"\n\n    def __init__(self, old_value, new_value):\n        self._changed = old_value != new_value\n        # Resolve if value is '_EMPTY_VALUE' after comparison of the values\n        if old_value is _EMPTY_VALUE:\n            old_value = None\n        if new_value is _EMPTY_VALUE:\n            new_value = None\n        self._old_value = copy.deepcopy(old_value)\n        self._new_value = copy.deepcopy(new_value)\n\n        self._old_is_dict = isinstance(old_value, dict)\n        self._new_is_dict = isinstance(new_value, dict)\n\n        self._old_keys = None\n        self._new_keys = None\n        self._available_keys = None\n        self._removed_keys = None\n\n        self._changed_keys = None\n\n        self._sub_items = None\n\n    def __getitem__(self, key):\n        \"\"\"Getter looks into subitems if object is dictionary.\"\"\"\n\n        if self._sub_items is None:\n            self._prepare_sub_items()\n        return self._sub_items[key]\n\n    def __bool__(self):\n        \"\"\"Boolean of object is if old and new value are the same.\"\"\"\n\n        return self._changed\n\n    def get(self, key, default=None):\n        \"\"\"Try to get sub item.\"\"\"\n\n        if self._sub_items is None:\n            self._prepare_sub_items()\n        return self._sub_items.get(key, default)\n\n    @property\n    def old_value(self):\n        \"\"\"Get copy of old value.\n\n        Returns:\n            Any: Whatever old value was.\n        \"\"\"\n\n        return copy.deepcopy(self._old_value)\n\n    @property\n    def new_value(self):\n        \"\"\"Get copy of new value.\n\n        Returns:\n            Any: Whatever new value was.\n        \"\"\"\n\n        return copy.deepcopy(self._new_value)\n\n    @property\n    def changed(self):\n        \"\"\"Value changed.\n\n        Returns:\n            bool: If data changed.\n        \"\"\"\n\n        return self._changed\n\n    @property\n    def is_dict(self):\n        \"\"\"Object can be used as dictionary.\n\n        Returns:\n            bool: When can be used that way.\n        \"\"\"\n\n        return self._old_is_dict or self._new_is_dict\n\n    @property\n    def changes(self):\n        \"\"\"Get changes in raw data.\n\n        This method should be used only if 'is_dict' value is 'True'.\n\n        Returns:\n            Dict[str, Tuple[Any, Any]]: Changes are by key in tuple\n                (<old value>, <new value>). If 'is_dict' is 'False' then\n                output is always empty dictionary.\n        \"\"\"\n\n        output = {}\n        if not self.is_dict:\n            return output\n\n        old_value = self.old_value\n        new_value = self.new_value\n        for key in self.changed_keys:\n            _old = None\n            _new = None\n            if self._old_is_dict:\n                _old = old_value.get(key)\n            if self._new_is_dict:\n                _new = new_value.get(key)\n            output[key] = (_old, _new)\n        return output\n\n    # Methods/properties that can be used when 'is_dict' is 'True'\n    @property\n    def old_keys(self):\n        \"\"\"Keys from old value.\n\n        Empty set is returned if old value is not a dict.\n\n        Returns:\n            Set[str]: Keys from old value.\n        \"\"\"\n\n        if self._old_keys is None:\n            self._prepare_keys()\n        return set(self._old_keys)\n\n    @property\n    def new_keys(self):\n        \"\"\"Keys from new value.\n\n        Empty set is returned if old value is not a dict.\n\n        Returns:\n            Set[str]: Keys from new value.\n        \"\"\"\n\n        if self._new_keys is None:\n            self._prepare_keys()\n        return set(self._new_keys)\n\n    @property\n    def changed_keys(self):\n        \"\"\"Keys that has changed from old to new value.\n\n        Empty set is returned if both old and new value are not a dict.\n\n        Returns:\n            Set[str]: Keys of changed keys.\n        \"\"\"\n\n        if self._changed_keys is None:\n            self._prepare_sub_items()\n        return set(self._changed_keys)\n\n    @property\n    def available_keys(self):\n        \"\"\"All keys that are available in old and new value.\n\n        Empty set is returned if both old and new value are not a dict.\n        Output is Union of 'old_keys' and 'new_keys'.\n\n        Returns:\n            Set[str]: All keys from old and new value.\n        \"\"\"\n\n        if self._available_keys is None:\n            self._prepare_keys()\n        return set(self._available_keys)\n\n    @property\n    def removed_keys(self):\n        \"\"\"Key that are not available in new value but were in old value.\n\n        Returns:\n            Set[str]: All removed keys.\n        \"\"\"\n\n        if self._removed_keys is None:\n            self._prepare_sub_items()\n        return set(self._removed_keys)\n\n    def _prepare_keys(self):\n        old_keys = set()\n        new_keys = set()\n        if self._old_is_dict and self._new_is_dict:\n            old_keys = set(self._old_value.keys())\n            new_keys = set(self._new_value.keys())\n\n        elif self._old_is_dict:\n            old_keys = set(self._old_value.keys())\n\n        elif self._new_is_dict:\n            new_keys = set(self._new_value.keys())\n\n        self._old_keys = old_keys\n        self._new_keys = new_keys\n        self._available_keys = old_keys | new_keys\n        self._removed_keys = old_keys - new_keys\n\n    def _prepare_sub_items(self):\n        sub_items = {}\n        changed_keys = set()\n\n        old_keys = self.old_keys\n        new_keys = self.new_keys\n        new_value = self.new_value\n        old_value = self.old_value\n        if self._old_is_dict and self._new_is_dict:\n            for key in self.available_keys:\n                item = TrackChangesItem(\n                    old_value.get(key), new_value.get(key)\n                )\n                sub_items[key] = item\n                if item.changed or key not in old_keys or key not in new_keys:\n                    changed_keys.add(key)\n\n        elif self._old_is_dict:\n            old_keys = set(old_value.keys())\n            available_keys = set(old_keys)\n            changed_keys = set(available_keys)\n            for key in available_keys:\n                # NOTE Use '_EMPTY_VALUE' because old value could be 'None'\n                #   which would result in \"unchanged\" item\n                sub_items[key] = TrackChangesItem(\n                    old_value.get(key), _EMPTY_VALUE\n                )\n\n        elif self._new_is_dict:\n            new_keys = set(new_value.keys())\n            available_keys = set(new_keys)\n            changed_keys = set(available_keys)\n            for key in available_keys:\n                # NOTE Use '_EMPTY_VALUE' because new value could be 'None'\n                #   which would result in \"unchanged\" item\n                sub_items[key] = TrackChangesItem(\n                    _EMPTY_VALUE, new_value.get(key)\n                )\n\n        self._sub_items = sub_items\n        self._changed_keys = changed_keys\n\n\nclass InstanceMember:\n    \"\"\"Representation of instance member.\n\n    TODO:\n    Implement and use!\n    \"\"\"\n\n    def __init__(self, instance, name):\n        self.instance = instance\n\n        instance.add_members(self)\n\n        self.name = name\n        self._actions = []\n\n    def add_action(self, label, callback):\n        self._actions.append({\n            \"label\": label,\n            \"callback\": callback\n        })\n\n\nclass AttributeValues(object):\n    \"\"\"Container which keep values of Attribute definitions.\n\n    Goal is to have one object which hold values of attribute definitions for\n    single instance.\n\n    Has dictionary like methods. Not all of them are allowed all the time.\n\n    Args:\n        attr_defs(AbstractAttrDef): Defintions of value type and properties.\n        values(dict): Values after possible conversion.\n        origin_data(dict): Values loaded from host before conversion.\n    \"\"\"\n\n    def __init__(self, attr_defs, values, origin_data=None):\n        if origin_data is None:\n            origin_data = copy.deepcopy(values)\n        self._origin_data = origin_data\n\n        attr_defs_by_key = {\n            attr_def.key: attr_def\n            for attr_def in attr_defs\n            if attr_def.is_value_def\n        }\n        for key, value in values.items():\n            if key not in attr_defs_by_key:\n                new_def = UnknownDef(key, label=key, default=value)\n                attr_defs.append(new_def)\n                attr_defs_by_key[key] = new_def\n\n        self._attr_defs = attr_defs\n        self._attr_defs_by_key = attr_defs_by_key\n\n        self._data = {}\n        for attr_def in attr_defs:\n            value = values.get(attr_def.key)\n            if value is not None:\n                self._data[attr_def.key] = value\n\n    def __setitem__(self, key, value):\n        if key not in self._attr_defs_by_key:\n            raise KeyError(\"Key \\\"{}\\\" was not found.\".format(key))\n\n        old_value = self._data.get(key)\n        if old_value == value:\n            return\n        self._data[key] = value\n\n    def __getitem__(self, key):\n        if key not in self._attr_defs_by_key:\n            return self._data[key]\n        return self._data.get(key, self._attr_defs_by_key[key].default)\n\n    def __contains__(self, key):\n        return key in self._attr_defs_by_key\n\n    def get(self, key, default=None):\n        if key in self._attr_defs_by_key:\n            return self[key]\n        return default\n\n    def keys(self):\n        return self._attr_defs_by_key.keys()\n\n    def values(self):\n        for key in self._attr_defs_by_key.keys():\n            yield self._data.get(key)\n\n    def items(self):\n        for key in self._attr_defs_by_key.keys():\n            yield key, self._data.get(key)\n\n    def update(self, value):\n        for _key, _value in dict(value):\n            self[_key] = _value\n\n    def pop(self, key, default=None):\n        value = self._data.pop(key, default)\n        # Remove attribute definition if is 'UnknownDef'\n        # - gives option to get rid of unknown values\n        attr_def = self._attr_defs_by_key.get(key)\n        if isinstance(attr_def, UnknownDef):\n            self._attr_defs_by_key.pop(key)\n            self._attr_defs.remove(attr_def)\n        return value\n\n    def reset_values(self):\n        self._data = {}\n\n    def mark_as_stored(self):\n        self._origin_data = copy.deepcopy(self._data)\n\n    @property\n    def attr_defs(self):\n        \"\"\"Pointer to attribute definitions.\n\n        Returns:\n            List[AbstractAttrDef]: Attribute definitions.\n        \"\"\"\n\n        return list(self._attr_defs)\n\n    @property\n    def origin_data(self):\n        return copy.deepcopy(self._origin_data)\n\n    def data_to_store(self):\n        \"\"\"Create new dictionary with data to store.\n\n        Returns:\n            Dict[str, Any]: Attribute values that should be stored.\n        \"\"\"\n\n        output = {}\n        for key in self._data:\n            output[key] = self[key]\n\n        for key, attr_def in self._attr_defs_by_key.items():\n            if key not in output:\n                output[key] = attr_def.default\n        return output\n\n    def get_serialized_attr_defs(self):\n        \"\"\"Serialize attribute definitions to json serializable types.\n\n        Returns:\n            List[Dict[str, Any]]: Serialized attribute definitions.\n        \"\"\"\n\n        return serialize_attr_defs(self._attr_defs)\n\n\nclass CreatorAttributeValues(AttributeValues):\n    \"\"\"Creator specific attribute values of an instance.\n\n    Args:\n        instance (CreatedInstance): Instance for which are values hold.\n    \"\"\"\n\n    def __init__(self, instance, *args, **kwargs):\n        self.instance = instance\n        super(CreatorAttributeValues, self).__init__(*args, **kwargs)\n\n\nclass PublishAttributeValues(AttributeValues):\n    \"\"\"Publish plugin specific attribute values.\n\n    Values are for single plugin which can be on `CreatedInstance`\n    or context values stored on `CreateContext`.\n\n    Args:\n        publish_attributes(PublishAttributes): Wrapper for multiple publish\n            attributes is used as parent object.\n    \"\"\"\n\n    def __init__(self, publish_attributes, *args, **kwargs):\n        self.publish_attributes = publish_attributes\n        super(PublishAttributeValues, self).__init__(*args, **kwargs)\n\n    @property\n    def parent(self):\n        self.publish_attributes.parent\n\n\nclass PublishAttributes:\n    \"\"\"Wrapper for publish plugin attribute definitions.\n\n    Cares about handling attribute definitions of multiple publish plugins.\n    Keep information about attribute definitions and their values.\n\n    Args:\n        parent(CreatedInstance, CreateContext): Parent for which will be\n            data stored and from which are data loaded.\n        origin_data(dict): Loaded data by plugin class name.\n        attr_plugins(Union[List[pyblish.api.Plugin], None]): List of publish\n            plugins that may have defined attribute definitions.\n    \"\"\"\n\n    def __init__(self, parent, origin_data, attr_plugins=None):\n        self.parent = parent\n        self._origin_data = copy.deepcopy(origin_data)\n\n        attr_plugins = attr_plugins or []\n        self.attr_plugins = attr_plugins\n\n        self._data = copy.deepcopy(origin_data)\n        self._plugin_names_order = []\n        self._missing_plugins = []\n\n        self.set_publish_plugins(attr_plugins)\n\n    def __getitem__(self, key):\n        return self._data[key]\n\n    def __contains__(self, key):\n        return key in self._data\n\n    def keys(self):\n        return self._data.keys()\n\n    def values(self):\n        return self._data.values()\n\n    def items(self):\n        return self._data.items()\n\n    def pop(self, key, default=None):\n        \"\"\"Remove or reset value for plugin.\n\n        Plugin values are reset to defaults if plugin is available but\n        data of plugin which was not found are removed.\n\n        Args:\n            key(str): Plugin name.\n            default: Default value if plugin was not found.\n        \"\"\"\n\n        if key not in self._data:\n            return default\n\n        if key in self._missing_plugins:\n            self._missing_plugins.remove(key)\n            removed_item = self._data.pop(key)\n            return removed_item.data_to_store()\n\n        value_item = self._data[key]\n        # Prepare value to return\n        output = value_item.data_to_store()\n        # Reset values\n        value_item.reset_values()\n        return output\n\n    def plugin_names_order(self):\n        \"\"\"Plugin names order by their 'order' attribute.\"\"\"\n\n        for name in self._plugin_names_order:\n            yield name\n\n    def mark_as_stored(self):\n        self._origin_data = copy.deepcopy(self.data_to_store())\n\n    def data_to_store(self):\n        \"\"\"Convert attribute values to \"data to store\".\"\"\"\n\n        output = {}\n        for key, attr_value in self._data.items():\n            output[key] = attr_value.data_to_store()\n        return output\n\n    @property\n    def origin_data(self):\n        return copy.deepcopy(self._origin_data)\n\n    def set_publish_plugins(self, attr_plugins):\n        \"\"\"Set publish plugins attribute definitions.\"\"\"\n\n        self._plugin_names_order = []\n        self._missing_plugins = []\n        self.attr_plugins = attr_plugins or []\n\n        origin_data = self._origin_data\n        data = self._data\n        self._data = {}\n        added_keys = set()\n        for plugin in attr_plugins:\n            output = plugin.convert_attribute_values(data)\n            if output is not None:\n                data = output\n            attr_defs = plugin.get_attribute_defs()\n            if not attr_defs:\n                continue\n\n            key = plugin.__name__\n            added_keys.add(key)\n            self._plugin_names_order.append(key)\n\n            value = data.get(key) or {}\n            orig_value = copy.deepcopy(origin_data.get(key) or {})\n            self._data[key] = PublishAttributeValues(\n                self, attr_defs, value, orig_value\n            )\n\n        for key, value in data.items():\n            if key not in added_keys:\n                self._missing_plugins.append(key)\n                self._data[key] = PublishAttributeValues(\n                    self, [], value, value\n                )\n\n    def serialize_attributes(self):\n        return {\n            \"attr_defs\": {\n                plugin_name: attrs_value.get_serialized_attr_defs()\n                for plugin_name, attrs_value in self._data.items()\n            },\n            \"plugin_names_order\": self._plugin_names_order,\n            \"missing_plugins\": self._missing_plugins\n        }\n\n    def deserialize_attributes(self, data):\n        self._plugin_names_order = data[\"plugin_names_order\"]\n        self._missing_plugins = data[\"missing_plugins\"]\n\n        attr_defs = deserialize_attr_defs(data[\"attr_defs\"])\n\n        origin_data = self._origin_data\n        data = self._data\n        self._data = {}\n\n        added_keys = set()\n        for plugin_name, attr_defs_data in attr_defs.items():\n            attr_defs = deserialize_attr_defs(attr_defs_data)\n            value = data.get(plugin_name) or {}\n            orig_value = copy.deepcopy(origin_data.get(plugin_name) or {})\n            self._data[plugin_name] = PublishAttributeValues(\n                self, attr_defs, value, orig_value\n            )\n\n        for key, value in data.items():\n            if key not in added_keys:\n                self._missing_plugins.append(key)\n                self._data[key] = PublishAttributeValues(\n                    self, [], value, value\n                )\n\n\nclass CreatedInstance:\n    \"\"\"Instance entity with data that will be stored to workfile.\n\n    I think `data` must be required argument containing all minimum information\n    about instance like \"asset\" and \"task\" and all data used for filling subset\n    name as creators may have custom data for subset name filling.\n\n    Notes:\n        Object have 2 possible initialization. One using 'creator' object which\n            is recommended for api usage. Second by passing information about\n            creator.\n\n    Args:\n        family (str): Name of family that will be created.\n        subset_name (str): Name of subset that will be created.\n        data (Dict[str, Any]): Data used for filling subset name or override\n            data from already existing instance.\n        creator (Union[BaseCreator, None]): Creator responsible for instance.\n        creator_identifier (str): Identifier of creator plugin.\n        creator_label (str): Creator plugin label.\n        group_label (str): Default group label from creator plugin.\n        creator_attr_defs (List[AbstractAttrDef]): Attribute definitions from\n            creator.\n    \"\"\"\n\n    # Keys that can't be changed or removed from data after loading using\n    #   creator.\n    # - 'creator_attributes' and 'publish_attributes' can change values of\n    #   their individual children but not on their own\n    __immutable_keys = (\n        \"id\",\n        \"instance_id\",\n        \"family\",\n        \"creator_identifier\",\n        \"creator_attributes\",\n        \"publish_attributes\"\n    )\n\n    def __init__(\n        self,\n        family,\n        subset_name,\n        data,\n        creator=None,\n        creator_identifier=None,\n        creator_label=None,\n        group_label=None,\n        creator_attr_defs=None,\n    ):\n        if creator is not None:\n            creator_identifier = creator.identifier\n            group_label = creator.get_group_label()\n            creator_label = creator.label\n            creator_attr_defs = creator.get_instance_attr_defs()\n\n        self._creator_label = creator_label\n        self._group_label = group_label or creator_identifier\n\n        # Instance members may have actions on them\n        # TODO implement members logic\n        self._members = []\n\n        # Data that can be used for lifetime of object\n        self._transient_data = {}\n\n        # Create a copy of passed data to avoid changing them on the fly\n        data = copy.deepcopy(data or {})\n\n        # Pop dictionary values that will be converted to objects to be able\n        #   catch changes\n        orig_creator_attributes = data.pop(\"creator_attributes\", None) or {}\n        orig_publish_attributes = data.pop(\"publish_attributes\", None) or {}\n\n        # Store original value of passed data\n        self._orig_data = copy.deepcopy(data)\n\n        # Pop family and subset to prevent unexpected changes\n        # TODO change to 'productType' and 'productName' in AYON\n        data.pop(\"family\", None)\n        data.pop(\"subset\", None)\n\n        if AYON_SERVER_ENABLED:\n            asset_name = data.pop(\"asset\", None)\n            if \"folderPath\" not in data:\n                data[\"folderPath\"] = asset_name\n\n        elif \"folderPath\" in data:\n            asset_name = data.pop(\"folderPath\").split(\"/\")[-1]\n            if \"asset\" not in data:\n                data[\"asset\"] = asset_name\n\n        # QUESTION Does it make sense to have data stored as ordered dict?\n        self._data = collections.OrderedDict()\n        # QUESTION Do we need this \"id\" information on instance?\n        self._data[\"id\"] = \"pyblish.avalon.instance\"\n        self._data[\"family\"] = family\n        self._data[\"subset\"] = subset_name\n        self._data[\"active\"] = data.get(\"active\", True)\n        self._data[\"creator_identifier\"] = creator_identifier\n\n        # Pop from source data all keys that are defined in `_data` before\n        #   this moment and through their values away\n        # - they should be the same and if are not then should not change\n        #   already set values\n        for key in self._data.keys():\n            if key in data:\n                data.pop(key)\n\n        self._data[\"variant\"] = self._data.get(\"variant\") or \"\"\n        # Stored creator specific attribute values\n        # {key: value}\n        creator_values = copy.deepcopy(orig_creator_attributes)\n\n        self._data[\"creator_attributes\"] = CreatorAttributeValues(\n            self,\n            list(creator_attr_defs),\n            creator_values,\n            orig_creator_attributes\n        )\n\n        # Stored publish specific attribute values\n        # {<plugin name>: {key: value}}\n        # - must be set using 'set_publish_plugins'\n        self._data[\"publish_attributes\"] = PublishAttributes(\n            self, orig_publish_attributes, None\n        )\n        if data:\n            self._data.update(data)\n\n        if not self._data.get(\"instance_id\"):\n            self._data[\"instance_id\"] = str(uuid4())\n\n        self._asset_is_valid = self.has_set_asset\n        self._task_is_valid = self.has_set_task\n\n    def __str__(self):\n        return (\n            \"<CreatedInstance {subset} ({family}[{creator_identifier}])>\"\n            \" {data}\"\n        ).format(\n            subset=str(self._data),\n            creator_identifier=self.creator_identifier,\n            family=self.family,\n            data=str(self._data)\n        )\n\n    # --- Dictionary like methods ---\n    def __getitem__(self, key):\n        return self._data[key]\n\n    def __contains__(self, key):\n        return key in self._data\n\n    def __setitem__(self, key, value):\n        # Validate immutable keys\n        if key not in self.__immutable_keys:\n            self._data[key] = value\n\n        elif value != self._data.get(key):\n            # Raise exception if key is immutable and value has changed\n            raise ImmutableKeyError(key)\n\n    def get(self, key, default=None):\n        return self._data.get(key, default)\n\n    def pop(self, key, *args, **kwargs):\n        # Raise exception if is trying to pop key which is immutable\n        if key in self.__immutable_keys:\n            raise ImmutableKeyError(key)\n\n        self._data.pop(key, *args, **kwargs)\n\n    def keys(self):\n        return self._data.keys()\n\n    def values(self):\n        return self._data.values()\n\n    def items(self):\n        return self._data.items()\n    # ------\n\n    @property\n    def family(self):\n        return self._data[\"family\"]\n\n    @property\n    def subset_name(self):\n        return self._data[\"subset\"]\n\n    @property\n    def label(self):\n        label = self._data.get(\"label\")\n        if not label:\n            label = self.subset_name\n        return label\n\n    @property\n    def group_label(self):\n        label = self._data.get(\"group\")\n        if label:\n            return label\n        return self._group_label\n\n    @property\n    def origin_data(self):\n        output = copy.deepcopy(self._orig_data)\n        output[\"creator_attributes\"] = self.creator_attributes.origin_data\n        output[\"publish_attributes\"] = self.publish_attributes.origin_data\n        return output\n\n    @property\n    def creator_identifier(self):\n        return self._data[\"creator_identifier\"]\n\n    @property\n    def creator_label(self):\n        return self._creator_label or self.creator_identifier\n\n    @property\n    def id(self):\n        \"\"\"Instance identifier.\n\n        Returns:\n            str: UUID of instance.\n        \"\"\"\n\n        return self._data[\"instance_id\"]\n\n    @property\n    def data(self):\n        \"\"\"Legacy access to data.\n\n        Access to data is needed to modify values.\n\n        Returns:\n            CreatedInstance: Object can be used as dictionary but with\n                validations of immutable keys.\n        \"\"\"\n\n        return self\n\n    @property\n    def transient_data(self):\n        \"\"\"Data stored for lifetime of instance object.\n\n        These data are not stored to scene and will be lost on object\n        deletion.\n\n        Can be used to store objects. In some host implementations is not\n        possible to reference to object in scene with some unique identifier\n        (e.g. node in Fusion.). In that case it is handy to store the object\n        here. Should be used that way only if instance data are stored on the\n        node itself.\n\n        Returns:\n            Dict[str, Any]: Dictionary object where you can store data related\n                to instance for lifetime of instance object.\n        \"\"\"\n\n        return self._transient_data\n\n    def changes(self):\n        \"\"\"Calculate and return changes.\"\"\"\n\n        return TrackChangesItem(self.origin_data, self.data_to_store())\n\n    def mark_as_stored(self):\n        \"\"\"Should be called when instance data are stored.\n\n        Origin data are replaced by current data so changes are cleared.\n        \"\"\"\n\n        orig_keys = set(self._orig_data.keys())\n        for key, value in self._data.items():\n            orig_keys.discard(key)\n            if key in (\"creator_attributes\", \"publish_attributes\"):\n                continue\n            self._orig_data[key] = copy.deepcopy(value)\n\n        for key in orig_keys:\n            self._orig_data.pop(key)\n\n        self.creator_attributes.mark_as_stored()\n        self.publish_attributes.mark_as_stored()\n\n    @property\n    def creator_attributes(self):\n        return self._data[\"creator_attributes\"]\n\n    @property\n    def creator_attribute_defs(self):\n        \"\"\"Attribute definitions defined by creator plugin.\n\n        Returns:\n              List[AbstractAttrDef]: Attribute definitions.\n        \"\"\"\n\n        return self.creator_attributes.attr_defs\n\n    @property\n    def publish_attributes(self):\n        return self._data[\"publish_attributes\"]\n\n    def data_to_store(self):\n        \"\"\"Collect data that contain json parsable types.\n\n        It is possible to recreate the instance using these data.\n\n        Todos:\n            We probably don't need OrderedDict. When data are loaded they\n                are not ordered anymore.\n\n        Returns:\n            OrderedDict: Ordered dictionary with instance data.\n        \"\"\"\n\n        output = collections.OrderedDict()\n        for key, value in self._data.items():\n            if key in (\"creator_attributes\", \"publish_attributes\"):\n                continue\n            output[key] = value\n\n        output[\"creator_attributes\"] = self.creator_attributes.data_to_store()\n        output[\"publish_attributes\"] = self.publish_attributes.data_to_store()\n\n        return output\n\n    @classmethod\n    def from_existing(cls, instance_data, creator):\n        \"\"\"Convert instance data from workfile to CreatedInstance.\n\n        Args:\n            instance_data (Dict[str, Any]): Data in a structure ready for\n                'CreatedInstance' object.\n            creator (BaseCreator): Creator plugin which is creating the\n                instance of for which the instance belong.\n        \"\"\"\n\n        instance_data = copy.deepcopy(instance_data)\n\n        family = instance_data.get(\"family\", None)\n        if family is None:\n            family = creator.family\n        subset_name = instance_data.get(\"subset\", None)\n\n        return cls(\n            family, subset_name, instance_data, creator\n        )\n\n    def set_publish_plugins(self, attr_plugins):\n        \"\"\"Set publish plugins with attribute definitions.\n\n        This method should be called only from 'CreateContext'.\n\n        Args:\n            attr_plugins (List[pyblish.api.Plugin]): Pyblish plugins which\n                inherit from 'OpenPypePyblishPluginMixin' and may contain\n                attribute definitions.\n        \"\"\"\n\n        self.publish_attributes.set_publish_plugins(attr_plugins)\n\n    def add_members(self, members):\n        \"\"\"Currently unused method.\"\"\"\n\n        for member in members:\n            if member not in self._members:\n                self._members.append(member)\n\n    def serialize_for_remote(self):\n        \"\"\"Serialize object into data to be possible recreated object.\n\n        Returns:\n            Dict[str, Any]: Serialized data.\n        \"\"\"\n\n        creator_attr_defs = self.creator_attributes.get_serialized_attr_defs()\n        publish_attributes = self.publish_attributes.serialize_attributes()\n        return {\n            \"data\": self.data_to_store(),\n            \"orig_data\": self.origin_data,\n            \"creator_attr_defs\": creator_attr_defs,\n            \"publish_attributes\": publish_attributes,\n            \"creator_label\": self._creator_label,\n            \"group_label\": self._group_label,\n        }\n\n    @classmethod\n    def deserialize_on_remote(cls, serialized_data):\n        \"\"\"Convert instance data to CreatedInstance.\n\n        This is fake instance in remote process e.g. in UI process. The creator\n        is not a full creator and should not be used for calling methods when\n        instance is created from this method (matters on implementation).\n\n        Args:\n            serialized_data (Dict[str, Any]): Serialized data for remote\n                recreating. Should contain 'data' and 'orig_data'.\n        \"\"\"\n\n        instance_data = copy.deepcopy(serialized_data[\"data\"])\n        creator_identifier = instance_data[\"creator_identifier\"]\n\n        family = instance_data[\"family\"]\n        subset_name = instance_data.get(\"subset\", None)\n\n        creator_label = serialized_data[\"creator_label\"]\n        group_label = serialized_data[\"group_label\"]\n        creator_attr_defs = deserialize_attr_defs(\n            serialized_data[\"creator_attr_defs\"]\n        )\n        publish_attributes = serialized_data[\"publish_attributes\"]\n\n        obj = cls(\n            family,\n            subset_name,\n            instance_data,\n            creator_identifier=creator_identifier,\n            creator_label=creator_label,\n            group_label=group_label,\n            creator_attr_defs=creator_attr_defs\n        )\n        obj._orig_data = serialized_data[\"orig_data\"]\n        obj.publish_attributes.deserialize_attributes(publish_attributes)\n\n        return obj\n\n    # Context validation related methods/properties\n    @property\n    def has_set_asset(self):\n        \"\"\"Asset name is set in data.\"\"\"\n\n        if AYON_SERVER_ENABLED:\n            return \"folderPath\" in self._data\n        return \"asset\" in self._data\n\n    @property\n    def has_set_task(self):\n        \"\"\"Task name is set in data.\"\"\"\n\n        return \"task\" in self._data\n\n    @property\n    def has_valid_context(self):\n        \"\"\"Context data are valid for publishing.\"\"\"\n\n        return self.has_valid_asset and self.has_valid_task\n\n    @property\n    def has_valid_asset(self):\n        \"\"\"Asset set in context exists in project.\"\"\"\n\n        if not self.has_set_asset:\n            return False\n        return self._asset_is_valid\n\n    @property\n    def has_valid_task(self):\n        \"\"\"Task set in context exists in project.\"\"\"\n\n        if not self.has_set_task:\n            return False\n        return self._task_is_valid\n\n    def set_asset_invalid(self, invalid):\n        # TODO replace with `set_asset_name`\n        self._asset_is_valid = not invalid\n\n    def set_task_invalid(self, invalid):\n        # TODO replace with `set_task_name`\n        self._task_is_valid = not invalid\n\n\nclass ConvertorItem(object):\n    \"\"\"Item representing convertor plugin.\n\n    Args:\n        identifier (str): Identifier of convertor.\n        label (str): Label which will be shown in UI.\n    \"\"\"\n\n    def __init__(self, identifier, label):\n        self._id = str(uuid4())\n        self.identifier = identifier\n        self.label = label\n\n    @property\n    def id(self):\n        return self._id\n\n    def to_data(self):\n        return {\n            \"id\": self.id,\n            \"identifier\": self.identifier,\n            \"label\": self.label\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        obj = cls(data[\"identifier\"], data[\"label\"])\n        obj._id = data[\"id\"]\n        return obj\n\n\nclass CreateContext:\n    \"\"\"Context of instance creation.\n\n    Context itself also can store data related to whole creation (workfile).\n    - those are mainly for Context publish plugins\n\n    Todos:\n        Don't use 'AvalonMongoDB'. It's used only to keep track about current\n            context which should be handled by host.\n\n    Args:\n        host(ModuleType): Host implementation which handles implementation and\n            global metadata.\n        headless(bool): Context is created out of UI (Current not used).\n        reset(bool): Reset context on initialization.\n        discover_publish_plugins(bool): Discover publish plugins during reset\n            phase.\n    \"\"\"\n\n    def __init__(\n        self, host, headless=False, reset=True, discover_publish_plugins=True\n    ):\n        self.host = host\n\n        # Prepare attribute for logger (Created on demand in `log` property)\n        self._log = None\n\n        # Publish context plugins attributes and it's values\n        self._publish_attributes = PublishAttributes(self, {})\n        self._original_context_data = {}\n\n        # Validate host implementation\n        # - defines if context is capable of handling context data\n        host_is_valid = True\n        missing_methods = self.get_host_misssing_methods(host)\n        if missing_methods:\n            host_is_valid = False\n            joined_methods = \", \".join(\n                ['\"{}\"'.format(name) for name in missing_methods]\n            )\n            self.log.warning((\n                \"Host miss required methods to be able use creation.\"\n                \" Missing methods: {}\"\n            ).format(joined_methods))\n\n        self._current_project_name = None\n        self._current_asset_name = None\n        self._current_task_name = None\n        self._current_workfile_path = None\n\n        self._current_project_anatomy = None\n\n        self._host_is_valid = host_is_valid\n        # Currently unused variable\n        self.headless = headless\n\n        # Instances by their ID\n        self._instances_by_id = {}\n\n        self.creator_discover_result = None\n        self.convertor_discover_result = None\n        # Discovered creators\n        self.creators = {}\n        # Prepare categories of creators\n        self.autocreators = {}\n        # Manual creators\n        self.manual_creators = {}\n        # Creators that are disabled\n        self.disabled_creators = {}\n\n        self.convertors_plugins = {}\n        self.convertor_items_by_id = {}\n\n        self.publish_discover_result = None\n        self.publish_plugins_mismatch_targets = []\n        self.publish_plugins = []\n        self.plugins_with_defs = []\n        self._attr_plugins_by_family = {}\n\n        # Helpers for validating context of collected instances\n        #   - they can be validation for multiple instances at one time\n        #       using context manager which will trigger validation\n        #       after leaving of last context manager scope\n        self._bulk_counter = 0\n        self._bulk_instances_to_process = []\n\n        # Shared data across creators during collection phase\n        self._collection_shared_data = None\n\n        self.thumbnail_paths_by_instance_id = {}\n\n        # Trigger reset if was enabled\n        if reset:\n            self.reset(discover_publish_plugins)\n\n    @property\n    def instances(self):\n        return self._instances_by_id.values()\n\n    @property\n    def instances_by_id(self):\n        return self._instances_by_id\n\n    @property\n    def publish_attributes(self):\n        \"\"\"Access to global publish attributes.\"\"\"\n        return self._publish_attributes\n\n    def get_instance_by_id(self, instance_id):\n        \"\"\"Receive instance by id.\n\n        Args:\n            instance_id (str): Instance id.\n\n        Returns:\n            Union[CreatedInstance, None]: Instance or None if instance with\n                given id is not available.\n        \"\"\"\n\n        return self._instances_by_id.get(instance_id)\n\n    def get_sorted_creators(self, identifiers=None):\n        \"\"\"Sorted creators by 'order' attribute.\n\n        Args:\n            identifiers (Iterable[str]): Filter creators by identifiers. All\n                creators are returned if 'None' is passed.\n\n        Returns:\n            List[BaseCreator]: Sorted creator plugins by 'order' value.\n        \"\"\"\n\n        if identifiers is not None:\n            identifiers = set(identifiers)\n            creators = [\n                creator\n                for identifier, creator in self.creators.items()\n                if identifier in identifiers\n            ]\n        else:\n            creators = self.creators.values()\n\n        return sorted(\n            creators, key=lambda creator: creator.order\n        )\n\n    @property\n    def sorted_creators(self):\n        \"\"\"Sorted creators by 'order' attribute.\n\n        Returns:\n            List[BaseCreator]: Sorted creator plugins by 'order' value.\n        \"\"\"\n\n        return self.get_sorted_creators()\n\n    @property\n    def sorted_autocreators(self):\n        \"\"\"Sorted auto-creators by 'order' attribute.\n\n        Returns:\n            List[AutoCreator]: Sorted plugins by 'order' value.\n        \"\"\"\n\n        return sorted(\n            self.autocreators.values(), key=lambda creator: creator.order\n        )\n\n    @classmethod\n    def get_host_misssing_methods(cls, host):\n        \"\"\"Collect missing methods from host.\n\n        Args:\n            host(ModuleType): Host implementaion.\n        \"\"\"\n\n        missing = set(\n            IPublishHost.get_missing_publish_methods(host)\n        )\n        return missing\n\n    @property\n    def host_is_valid(self):\n        \"\"\"Is host valid for creation.\"\"\"\n        return self._host_is_valid\n\n    @property\n    def host_name(self):\n        if hasattr(self.host, \"name\"):\n            return self.host.name\n        return os.environ[\"AVALON_APP\"]\n\n    def get_current_project_name(self):\n        \"\"\"Project name which was used as current context on context reset.\n\n        Returns:\n            Union[str, None]: Project name.\n        \"\"\"\n\n        return self._current_project_name\n\n    def get_current_asset_name(self):\n        \"\"\"Asset name which was used as current context on context reset.\n\n        Returns:\n            Union[str, None]: Asset name.\n        \"\"\"\n\n        return self._current_asset_name\n\n    def get_current_task_name(self):\n        \"\"\"Task name which was used as current context on context reset.\n\n        Returns:\n            Union[str, None]: Task name.\n        \"\"\"\n\n        return self._current_task_name\n\n    def get_current_workfile_path(self):\n        \"\"\"Workfile path which was opened on context reset.\n\n        Returns:\n            Union[str, None]: Workfile path.\n        \"\"\"\n\n        return self._current_workfile_path\n\n    def get_current_project_anatomy(self):\n        \"\"\"Project anatomy for current project.\n\n        Returns:\n            Anatomy: Anatomy object ready to be used.\n        \"\"\"\n\n        if self._current_project_anatomy is None:\n            self._current_project_anatomy = Anatomy(\n                self._current_project_name)\n        return self._current_project_anatomy\n\n    @property\n    def context_has_changed(self):\n        \"\"\"Host context has changed.\n\n        As context is used project, asset, task name and workfile path if\n        host does support workfiles.\n\n        Returns:\n            bool: Context changed.\n        \"\"\"\n\n        project_name, asset_name, task_name, workfile_path = (\n            self._get_current_host_context()\n        )\n        return (\n            self._current_project_name != project_name\n            or self._current_asset_name != asset_name\n            or self._current_task_name != task_name\n            or self._current_workfile_path != workfile_path\n        )\n\n    project_name = property(get_current_project_name)\n    project_anatomy = property(get_current_project_anatomy)\n\n    @property\n    def log(self):\n        \"\"\"Dynamic access to logger.\"\"\"\n        if self._log is None:\n            self._log = logging.getLogger(self.__class__.__name__)\n        return self._log\n\n    def reset(self, discover_publish_plugins=True):\n        \"\"\"Reset context with all plugins and instances.\n\n        All changes will be lost if were not saved explicitely.\n        \"\"\"\n\n        self.reset_preparation()\n\n        self.reset_current_context()\n        self.reset_plugins(discover_publish_plugins)\n        self.reset_context_data()\n\n        with self.bulk_instances_collection():\n            self.reset_instances()\n            self.find_convertor_items()\n            self.execute_autocreators()\n\n        self.reset_finalization()\n\n    def refresh_thumbnails(self):\n        \"\"\"Cleanup thumbnail paths.\n\n        Remove all thumbnail filepaths that are empty or lead to files which\n        does not exists or of instances that are not available anymore.\n        \"\"\"\n\n        invalid = set()\n        for instance_id, path in self.thumbnail_paths_by_instance_id.items():\n            instance_available = True\n            if instance_id is not None:\n                instance_available = instance_id in self._instances_by_id\n\n            if (\n                not instance_available\n                or not path\n                or not os.path.exists(path)\n            ):\n                invalid.add(instance_id)\n\n        for instance_id in invalid:\n            self.thumbnail_paths_by_instance_id.pop(instance_id)\n\n    def reset_preparation(self):\n        \"\"\"Prepare attributes that must be prepared/cleaned before reset.\"\"\"\n\n        # Give ability to store shared data for collection phase\n        self._collection_shared_data = {}\n\n    def reset_finalization(self):\n        \"\"\"Cleanup of attributes after reset.\"\"\"\n\n        # Stop access to collection shared data\n        self._collection_shared_data = None\n        self.refresh_thumbnails()\n\n    def _get_current_host_context(self):\n        project_name = asset_name = task_name = workfile_path = None\n        if hasattr(self.host, \"get_current_context\"):\n            host_context = self.host.get_current_context()\n            if host_context:\n                project_name = host_context.get(\"project_name\")\n                asset_name = host_context.get(\"asset_name\")\n                task_name = host_context.get(\"task_name\")\n\n        if isinstance(self.host, IWorkfileHost):\n            workfile_path = self.host.get_current_workfile()\n\n        # --- TODO remove these conditions ---\n        if not project_name:\n            project_name = legacy_io.Session.get(\"AVALON_PROJECT\")\n        if not asset_name:\n            asset_name = legacy_io.Session.get(\"AVALON_ASSET\")\n        if not task_name:\n            task_name = legacy_io.Session.get(\"AVALON_TASK\")\n        # ---\n        return project_name, asset_name, task_name, workfile_path\n\n    def reset_current_context(self):\n        \"\"\"Refresh current context.\n\n        Reset is based on optional host implementation of `get_current_context`\n        function or using `legacy_io.Session`.\n\n        Some hosts have ability to change context file without using workfiles\n        tool but that change is not propagated to 'legacy_io.Session'\n        nor 'os.environ'.\n\n        Todos:\n            UI: Current context should be also checked on save - compare\n                initial values vs. current values.\n            Related to UI checks: Current workfile can be also considered\n                as current context information as that's where the metadata\n                are stored. We should store the workfile (if is available) too.\n        \"\"\"\n\n        project_name, asset_name, task_name, workfile_path = (\n            self._get_current_host_context()\n        )\n\n        self._current_project_name = project_name\n        self._current_asset_name = asset_name\n        self._current_task_name = task_name\n        self._current_workfile_path = workfile_path\n\n        self._current_project_anatomy = None\n\n    def reset_plugins(self, discover_publish_plugins=True):\n        \"\"\"Reload plugins.\n\n        Reloads creators from preregistered paths and can load publish plugins\n        if it's enabled on context.\n        \"\"\"\n\n        self._reset_publish_plugins(discover_publish_plugins)\n        self._reset_creator_plugins()\n        self._reset_convertor_plugins()\n\n    def _reset_publish_plugins(self, discover_publish_plugins):\n        from openpype.pipeline import OpenPypePyblishPluginMixin\n        from openpype.pipeline.publish import (\n            publish_plugins_discover\n        )\n\n        # Reset publish plugins\n        self._attr_plugins_by_family = {}\n\n        discover_result = DiscoverResult(pyblish.api.Plugin)\n        plugins_with_defs = []\n        plugins_by_targets = []\n        plugins_mismatch_targets = []\n        if discover_publish_plugins:\n            discover_result = publish_plugins_discover()\n            publish_plugins = discover_result.plugins\n\n            targets = set(pyblish.logic.registered_targets())\n            targets.add(\"default\")\n            plugins_by_targets = pyblish.logic.plugins_by_targets(\n                publish_plugins, list(targets)\n            )\n\n            # Collect plugins that can have attribute definitions\n            for plugin in publish_plugins:\n                if OpenPypePyblishPluginMixin in inspect.getmro(plugin):\n                    plugins_with_defs.append(plugin)\n\n            plugins_mismatch_targets = [\n                plugin\n                for plugin in publish_plugins\n                if plugin not in plugins_by_targets\n            ]\n\n        self.publish_plugins_mismatch_targets = plugins_mismatch_targets\n        self.publish_discover_result = discover_result\n        self.publish_plugins = plugins_by_targets\n        self.plugins_with_defs = plugins_with_defs\n\n    def _reset_creator_plugins(self):\n        # Prepare settings\n        system_settings = get_system_settings()\n        project_settings = get_project_settings(self.project_name)\n\n        # Discover and prepare creators\n        creators = {}\n        disabled_creators = {}\n        autocreators = {}\n        manual_creators = {}\n        report = discover_creator_plugins(return_report=True)\n        self.creator_discover_result = report\n        for creator_class in report.plugins:\n            if inspect.isabstract(creator_class):\n                self.log.debug(\n                    \"Skipping abstract Creator {}\".format(str(creator_class))\n                )\n                continue\n\n            creator_identifier = creator_class.identifier\n            if creator_identifier in creators:\n                self.log.warning((\n                    \"Duplicated Creator identifier. \"\n                    \"Using first and skipping following\"\n                ))\n                continue\n\n            # Filter by host name\n            if (\n                creator_class.host_name\n                and creator_class.host_name != self.host_name\n            ):\n                self.log.info((\n                    \"Creator's host name \\\"{}\\\"\"\n                    \" is not supported for current host \\\"{}\\\"\"\n                ).format(creator_class.host_name, self.host_name))\n                continue\n\n            creator = creator_class(\n                project_settings,\n                system_settings,\n                self,\n                self.headless\n            )\n\n            if not creator.enabled:\n                disabled_creators[creator_identifier] = creator\n                continue\n            creators[creator_identifier] = creator\n            if isinstance(creator, AutoCreator):\n                autocreators[creator_identifier] = creator\n            elif isinstance(creator, Creator):\n                manual_creators[creator_identifier] = creator\n\n        self.autocreators = autocreators\n        self.manual_creators = manual_creators\n\n        self.creators = creators\n        self.disabled_creators = disabled_creators\n\n    def _reset_convertor_plugins(self):\n        convertors_plugins = {}\n        report = discover_convertor_plugins(return_report=True)\n        self.convertor_discover_result = report\n        for convertor_class in report.plugins:\n            if inspect.isabstract(convertor_class):\n                self.log.info(\n                    \"Skipping abstract Creator {}\".format(str(convertor_class))\n                )\n                continue\n\n            convertor_identifier = convertor_class.identifier\n            if convertor_identifier in convertors_plugins:\n                self.log.warning((\n                    \"Duplicated Converter identifier. \"\n                    \"Using first and skipping following\"\n                ))\n                continue\n\n            convertors_plugins[convertor_identifier] = convertor_class(self)\n\n        self.convertors_plugins = convertors_plugins\n\n    def reset_context_data(self):\n        \"\"\"Reload context data using host implementation.\n\n        These data are not related to any instance but may be needed for whole\n        publishing.\n        \"\"\"\n        if not self.host_is_valid:\n            self._original_context_data = {}\n            self._publish_attributes = PublishAttributes(self, {})\n            return\n\n        original_data = self.host.get_context_data() or {}\n        self._original_context_data = copy.deepcopy(original_data)\n\n        publish_attributes = original_data.get(\"publish_attributes\") or {}\n\n        attr_plugins = self._get_publish_plugins_with_attr_for_context()\n        self._publish_attributes = PublishAttributes(\n            self, publish_attributes, attr_plugins\n        )\n\n    def context_data_to_store(self):\n        \"\"\"Data that should be stored by host function.\n\n        The same data should be returned on loading.\n        \"\"\"\n        return {\n            \"publish_attributes\": self._publish_attributes.data_to_store()\n        }\n\n    def context_data_changes(self):\n        \"\"\"Changes of attributes.\"\"\"\n\n        return TrackChangesItem(\n            self._original_context_data, self.context_data_to_store()\n        )\n\n    def creator_adds_instance(self, instance):\n        \"\"\"Creator adds new instance to context.\n\n        Instances should be added only from creators.\n\n        Args:\n            instance(CreatedInstance): Instance with prepared data from\n                creator.\n\n        TODO: Rename method to more suit.\n        \"\"\"\n        # Add instance to instances list\n        if instance.id in self._instances_by_id:\n            self.log.warning((\n                \"Instance with id {} is already added to context.\"\n            ).format(instance.id))\n            return\n\n        self._instances_by_id[instance.id] = instance\n        # Prepare publish plugin attributes and set it on instance\n        attr_plugins = self._get_publish_plugins_with_attr_for_family(\n            instance.family\n        )\n        instance.set_publish_plugins(attr_plugins)\n\n        # Add instance to be validated inside 'bulk_instances_collection'\n        #   context manager if is inside bulk\n        with self.bulk_instances_collection():\n            self._bulk_instances_to_process.append(instance)\n\n    def _get_creator_in_create(self, identifier):\n        \"\"\"Creator by identifier with unified error.\n\n        Helper method to get creator by identifier with same error when creator\n        is not available.\n\n        Args:\n            identifier (str): Identifier of creator plugin.\n\n        Returns:\n            BaseCreator: Creator found by identifier.\n\n        Raises:\n            CreatorError: When identifier is not known.\n        \"\"\"\n\n        creator = self.creators.get(identifier)\n        # Fake CreatorError (Could be maybe specific exception?)\n        if creator is None:\n            raise CreatorError(\n                \"Creator {} was not found\".format(identifier)\n            )\n        return creator\n\n    def create(\n        self,\n        creator_identifier,\n        variant,\n        asset_doc=None,\n        task_name=None,\n        pre_create_data=None\n    ):\n        \"\"\"Trigger create of plugins with standartized arguments.\n\n        Arguments 'asset_doc' and 'task_name' use current context as default\n        values. If only 'task_name' is provided it will be overriden by\n        task name from current context. If 'task_name' is not provided\n        when 'asset_doc' is, it is considered that task name is not specified,\n        which can lead to error if subset name template requires task name.\n\n        Args:\n            creator_identifier (str): Identifier of creator plugin.\n            variant (str): Variant used for subset name.\n            asset_doc (Dict[str, Any]): Asset document which define context of\n                creation (possible context of created instance/s).\n            task_name (str): Name of task to which is context related.\n            pre_create_data (Dict[str, Any]): Pre-create attribute values.\n\n        Returns:\n            Any: Output of triggered creator's 'create' method.\n\n        Raises:\n            CreatorError: If creator was not found or asset is empty.\n        \"\"\"\n\n        creator = self._get_creator_in_create(creator_identifier)\n\n        project_name = self.project_name\n        if asset_doc is None:\n            asset_name = self.get_current_asset_name()\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            task_name = self.get_current_task_name()\n            if asset_doc is None:\n                raise CreatorError(\n                    \"Asset with name {} was not found\".format(asset_name)\n                )\n\n        if pre_create_data is None:\n            pre_create_data = {}\n\n        precreate_attr_defs = []\n        # Hidden creators do not have or need the pre-create attributes.\n        if isinstance(creator, Creator):\n            precreate_attr_defs = creator.get_pre_create_attr_defs()\n\n        # Create default values of precreate data\n        _pre_create_data = get_default_values(precreate_attr_defs)\n        # Update passed precreate data to default values\n        # TODO validate types\n        _pre_create_data.update(pre_create_data)\n\n        subset_name = creator.get_subset_name(\n            variant,\n            task_name,\n            asset_doc,\n            project_name,\n            self.host_name\n        )\n        asset_name = get_asset_name_identifier(asset_doc)\n        if AYON_SERVER_ENABLED:\n            asset_name_key = \"folderPath\"\n        else:\n            asset_name_key = \"asset\"\n\n        instance_data = {\n            asset_name_key: asset_name,\n            \"task\": task_name,\n            \"family\": creator.family,\n            \"variant\": variant\n        }\n        return creator.create(\n            subset_name,\n            instance_data,\n            _pre_create_data\n        )\n\n    def _create_with_unified_error(\n        self, identifier, creator, *args, **kwargs\n    ):\n        error_message = \"Failed to run Creator with identifier \\\"{}\\\". {}\"\n\n        label = None\n        add_traceback = False\n        result = None\n        fail_info = None\n        success = False\n\n        try:\n            # Try to get creator and his label\n            if creator is None:\n                creator = self._get_creator_in_create(identifier)\n            label = getattr(creator, \"label\", label)\n\n            # Run create\n            result = creator.create(*args, **kwargs)\n            success = True\n\n        except CreatorError:\n            exc_info = sys.exc_info()\n            self.log.warning(error_message.format(identifier, exc_info[1]))\n\n        except:\n            add_traceback = True\n            exc_info = sys.exc_info()\n            self.log.warning(\n                error_message.format(identifier, \"\"),\n                exc_info=True\n            )\n\n        if not success:\n            fail_info = prepare_failed_creator_operation_info(\n                identifier, label, exc_info, add_traceback\n            )\n        return result, fail_info\n\n    def create_with_unified_error(self, identifier, *args, **kwargs):\n        \"\"\"Trigger create but raise only one error if anything fails.\n\n        Added to raise unified exception. Capture any possible issues and\n        reraise it with unified information.\n\n        Args:\n            identifier (str): Identifier of creator.\n            *args (Tuple[Any]): Arguments for create method.\n            **kwargs (Dict[Any, Any]): Keyword argument for create method.\n\n        Raises:\n            CreatorsCreateFailed: When creation fails due to any possible\n                reason. If anything goes wrong this is only possible exception\n                the method should raise.\n        \"\"\"\n\n        result, fail_info = self._create_with_unified_error(\n            identifier, None, *args, **kwargs\n        )\n        if fail_info is not None:\n            raise CreatorsCreateFailed([fail_info])\n        return result\n\n    def _remove_instance(self, instance):\n        self._instances_by_id.pop(instance.id, None)\n\n    def creator_removed_instance(self, instance):\n        \"\"\"When creator removes instance context should be acknowledged.\n\n        If creator removes instance conext should know about it to avoid\n        possible issues in the session.\n\n        Args:\n            instance (CreatedInstance): Object of instance which was removed\n                from scene metadata.\n        \"\"\"\n\n        self._remove_instance(instance)\n\n    def add_convertor_item(self, convertor_identifier, label):\n        self.convertor_items_by_id[convertor_identifier] = ConvertorItem(\n            convertor_identifier, label\n        )\n\n    def remove_convertor_item(self, convertor_identifier):\n        self.convertor_items_by_id.pop(convertor_identifier, None)\n\n    @contextmanager\n    def bulk_instances_collection(self):\n        \"\"\"Validate context of instances in bulk.\n\n        This can be used for single instance or for adding multiple instances\n            which is helpfull on reset.\n\n        Should not be executed from multiple threads.\n        \"\"\"\n        self._bulk_counter += 1\n        try:\n            yield\n        finally:\n            self._bulk_counter -= 1\n\n        # Trigger validation if there is no more context manager for bulk\n        #   instance validation\n        if self._bulk_counter == 0:\n            (\n                self._bulk_instances_to_process,\n                instances_to_validate\n            ) = (\n                [],\n                self._bulk_instances_to_process\n            )\n            self.validate_instances_context(instances_to_validate)\n\n    def reset_instances(self):\n        \"\"\"Reload instances\"\"\"\n        self._instances_by_id = collections.OrderedDict()\n\n        # Collect instances\n        error_message = \"Collection of instances for creator {} failed. {}\"\n        failed_info = []\n        for creator in self.sorted_creators:\n            label = creator.label\n            identifier = creator.identifier\n            failed = False\n            add_traceback = False\n            exc_info = None\n            try:\n                creator.collect_instances()\n\n            except CreatorError:\n                failed = True\n                exc_info = sys.exc_info()\n                self.log.warning(error_message.format(identifier, exc_info[1]))\n\n            except:\n                failed = True\n                add_traceback = True\n                exc_info = sys.exc_info()\n                self.log.warning(\n                    error_message.format(identifier, \"\"),\n                    exc_info=True\n                )\n\n            if failed:\n                failed_info.append(\n                    prepare_failed_creator_operation_info(\n                        identifier, label, exc_info, add_traceback\n                    )\n                )\n\n        if failed_info:\n            raise CreatorsCollectionFailed(failed_info)\n\n    def find_convertor_items(self):\n        \"\"\"Go through convertor plugins to look for items to convert.\n\n        Raises:\n            ConvertorsFindFailed: When one or more convertors fails during\n                finding.\n        \"\"\"\n\n        self.convertor_items_by_id = {}\n\n        failed_info = []\n        for convertor in self.convertors_plugins.values():\n            try:\n                convertor.find_instances()\n\n            except:\n                failed_info.append(\n                    prepare_failed_convertor_operation_info(\n                        convertor.identifier, sys.exc_info()\n                    )\n                )\n                self.log.warning(\n                    \"Failed to find instances of convertor \\\"{}\\\"\".format(\n                        convertor.identifier\n                    ),\n                    exc_info=True\n                )\n\n        if failed_info:\n            raise ConvertorsFindFailed(failed_info)\n\n    def execute_autocreators(self):\n        \"\"\"Execute discovered AutoCreator plugins.\n\n        Reset instances if any autocreator executed properly.\n        \"\"\"\n\n        failed_info = []\n        for creator in self.sorted_autocreators:\n            identifier = creator.identifier\n            _, fail_info = self._create_with_unified_error(identifier, creator)\n            if fail_info is not None:\n                failed_info.append(fail_info)\n\n        if failed_info:\n            raise CreatorsCreateFailed(failed_info)\n\n    def validate_instances_context(self, instances=None):\n        \"\"\"Validate 'asset' and 'task' instance context.\"\"\"\n        # Use all instances from context if 'instances' are not passed\n        if instances is None:\n            instances = tuple(self._instances_by_id.values())\n\n        # Skip if instances are empty\n        if not instances:\n            return\n\n        task_names_by_asset_name = {}\n        for instance in instances:\n            task_name = instance.get(\"task\")\n            if AYON_SERVER_ENABLED:\n                asset_name = instance.get(\"folderPath\")\n            else:\n                asset_name = instance.get(\"asset\")\n            if asset_name:\n                task_names_by_asset_name[asset_name] = set()\n                if task_name:\n                    task_names_by_asset_name[asset_name].add(task_name)\n\n        asset_names = {\n            asset_name\n            for asset_name in task_names_by_asset_name.keys()\n            if asset_name is not None\n        }\n        fields = {\"name\", \"data.tasks\"}\n        if AYON_SERVER_ENABLED:\n            fields |= {\"data.parents\"}\n        asset_docs = list(get_assets(\n            self.project_name,\n            asset_names=asset_names,\n            fields=fields\n        ))\n\n        task_names_by_asset_name = {}\n        asset_docs_by_name = collections.defaultdict(list)\n        for asset_doc in asset_docs:\n            asset_name = get_asset_name_identifier(asset_doc)\n            tasks = asset_doc.get(\"data\", {}).get(\"tasks\") or {}\n            task_names_by_asset_name[asset_name] = set(tasks.keys())\n            asset_docs_by_name[asset_doc[\"name\"]].append(asset_doc)\n\n        for instance in instances:\n            if not instance.has_valid_asset or not instance.has_valid_task:\n                continue\n\n            if AYON_SERVER_ENABLED:\n                asset_name = instance[\"folderPath\"]\n                if asset_name and \"/\" not in asset_name:\n                    asset_docs = asset_docs_by_name.get(asset_name)\n                    if len(asset_docs) == 1:\n                        asset_name = get_asset_name_identifier(asset_docs[0])\n                        instance[\"folderPath\"] = asset_name\n            else:\n                asset_name = instance[\"asset\"]\n\n            if asset_name not in task_names_by_asset_name:\n                instance.set_asset_invalid(True)\n                continue\n\n            task_name = instance[\"task\"]\n            if not task_name:\n                continue\n\n            if task_name not in task_names_by_asset_name[asset_name]:\n                instance.set_task_invalid(True)\n\n    def save_changes(self):\n        \"\"\"Save changes. Update all changed values.\"\"\"\n        if not self.host_is_valid:\n            missing_methods = self.get_host_misssing_methods(self.host)\n            raise HostMissRequiredMethod(self.host, missing_methods)\n\n        self._save_context_changes()\n        self._save_instance_changes()\n\n    def _save_context_changes(self):\n        \"\"\"Save global context values.\"\"\"\n        changes = self.context_data_changes()\n        if changes:\n            data = self.context_data_to_store()\n            self.host.update_context_data(data, changes)\n\n    def _save_instance_changes(self):\n        \"\"\"Save instance specific values.\"\"\"\n        instances_by_identifier = collections.defaultdict(list)\n        for instance in self._instances_by_id.values():\n            instance_changes = instance.changes()\n            if not instance_changes:\n                continue\n\n            identifier = instance.creator_identifier\n            instances_by_identifier[identifier].append(\n                UpdateData(instance, instance_changes)\n            )\n\n        if not instances_by_identifier:\n            return\n\n        error_message = \"Instances update of creator \\\"{}\\\" failed. {}\"\n        failed_info = []\n\n        for creator in self.get_sorted_creators(\n            instances_by_identifier.keys()\n        ):\n            identifier = creator.identifier\n            update_list = instances_by_identifier[identifier]\n            if not update_list:\n                continue\n\n            label = creator.label\n            failed = False\n            add_traceback = False\n            exc_info = None\n            try:\n                creator.update_instances(update_list)\n\n            except CreatorError:\n                failed = True\n                exc_info = sys.exc_info()\n                self.log.warning(error_message.format(identifier, exc_info[1]))\n\n            except:\n                failed = True\n                add_traceback = True\n                exc_info = sys.exc_info()\n                self.log.warning(\n                    error_message.format(identifier, \"\"), exc_info=True)\n\n            if failed:\n                failed_info.append(\n                    prepare_failed_creator_operation_info(\n                        identifier, label, exc_info, add_traceback\n                    )\n                )\n            else:\n                for update_data in update_list:\n                    instance = update_data.instance\n                    instance.mark_as_stored()\n\n        if failed_info:\n            raise CreatorsSaveFailed(failed_info)\n\n    def remove_instances(self, instances):\n        \"\"\"Remove instances from context.\n\n        All instances that don't have creator identifier leading to existing\n            creator are just removed from context.\n\n        Args:\n            instances(List[CreatedInstance]): Instances that should be removed.\n                Remove logic is done using creator, which may require to\n                do other cleanup than just remove instance from context.\n        \"\"\"\n\n        instances_by_identifier = collections.defaultdict(list)\n        for instance in instances:\n            identifier = instance.creator_identifier\n            instances_by_identifier[identifier].append(instance)\n\n        # Just remove instances from context if creator is not available\n        missing_creators = set(instances_by_identifier) - set(self.creators)\n        for identifier in missing_creators:\n            for instance in instances_by_identifier[identifier]:\n                self._remove_instance(instance)\n\n        error_message = \"Instances removement of creator \\\"{}\\\" failed. {}\"\n        failed_info = []\n        # Remove instances by creator plugin order\n        for creator in self.get_sorted_creators(\n            instances_by_identifier.keys()\n        ):\n            identifier = creator.identifier\n            creator_instances = instances_by_identifier[identifier]\n\n            label = creator.label\n            failed = False\n            add_traceback = False\n            exc_info = None\n            try:\n                creator.remove_instances(creator_instances)\n\n            except CreatorError:\n                failed = True\n                exc_info = sys.exc_info()\n                self.log.warning(\n                    error_message.format(identifier, exc_info[1])\n                )\n\n            except:\n                failed = True\n                add_traceback = True\n                exc_info = sys.exc_info()\n                self.log.warning(\n                    error_message.format(identifier, \"\"),\n                    exc_info=True\n                )\n\n            if failed:\n                failed_info.append(\n                    prepare_failed_creator_operation_info(\n                        identifier, label, exc_info, add_traceback\n                    )\n                )\n\n        if failed_info:\n            raise CreatorsRemoveFailed(failed_info)\n\n    def _get_publish_plugins_with_attr_for_family(self, family):\n        \"\"\"Publish plugin attributes for passed family.\n\n        Attribute definitions for specific family are cached.\n\n        Args:\n            family(str): Instance family for which should be attribute\n                definitions returned.\n        \"\"\"\n\n        if family not in self._attr_plugins_by_family:\n            import pyblish.logic\n\n            filtered_plugins = pyblish.logic.plugins_by_families(\n                self.plugins_with_defs, [family]\n            )\n            plugins = []\n            for plugin in filtered_plugins:\n                if plugin.__instanceEnabled__:\n                    plugins.append(plugin)\n            self._attr_plugins_by_family[family] = plugins\n\n        return self._attr_plugins_by_family[family]\n\n    def _get_publish_plugins_with_attr_for_context(self):\n        \"\"\"Publish plugins attributes for Context plugins.\n\n        Returns:\n            List[pyblish.api.Plugin]: Publish plugins that have attribute\n                definitions for context.\n        \"\"\"\n\n        plugins = []\n        for plugin in self.plugins_with_defs:\n            if not plugin.__instanceEnabled__:\n                plugins.append(plugin)\n        return plugins\n\n    @property\n    def collection_shared_data(self):\n        \"\"\"Access to shared data that can be used during creator's collection.\n\n        Retruns:\n            Dict[str, Any]: Shared data.\n\n        Raises:\n            UnavailableSharedData: When called out of collection phase.\n        \"\"\"\n\n        if self._collection_shared_data is None:\n            raise UnavailableSharedData(\n                \"Accessed Collection shared data out of collection phase\"\n            )\n        return self._collection_shared_data\n\n    def run_convertor(self, convertor_identifier):\n        \"\"\"Run convertor plugin by identifier.\n\n        Conversion is skipped if convertor is not available.\n\n        Args:\n            convertor_identifier (str): Identifier of convertor.\n        \"\"\"\n\n        convertor = self.convertors_plugins.get(convertor_identifier)\n        if convertor is not None:\n            convertor.convert()\n\n    def run_convertors(self, convertor_identifiers):\n        \"\"\"Run convertor plugins by identifiers.\n\n        Conversion is skipped if convertor is not available. It is recommended\n        to trigger reset after conversion to reload instances.\n\n        Args:\n            convertor_identifiers (Iterator[str]): Identifiers of convertors\n                to run.\n\n        Raises:\n            ConvertorsConversionFailed: When one or more convertors fails.\n        \"\"\"\n\n        failed_info = []\n        for convertor_identifier in convertor_identifiers:\n            try:\n                self.run_convertor(convertor_identifier)\n\n            except:\n                failed_info.append(\n                    prepare_failed_convertor_operation_info(\n                        convertor_identifier, sys.exc_info()\n                    )\n                )\n                self.log.warning(\n                    \"Failed to convert instances of convertor \\\"{}\\\"\".format(\n                        convertor_identifier\n                    ),\n                    exc_info=True\n                )\n\n        if failed_info:\n            raise ConvertorsConversionFailed(failed_info)\n"
  },
  {
    "path": "openpype/pipeline/create/creator_plugins.py",
    "content": "# -*- coding: utf-8 -*-\nimport copy\nimport collections\n\nfrom abc import ABCMeta, abstractmethod\n\nimport six\n\nfrom openpype.settings import get_system_settings, get_project_settings\nfrom openpype.lib import Logger, is_func_signature_supported\nfrom openpype.pipeline.plugin_discover import (\n    discover,\n    register_plugin,\n    register_plugin_path,\n    deregister_plugin,\n    deregister_plugin_path\n)\n\nfrom .constants import DEFAULT_VARIANT_VALUE\nfrom .subset_name import get_subset_name\nfrom .utils import get_next_versions_for_instances\nfrom .legacy_create import LegacyCreator\n\n\nclass CreatorError(Exception):\n    \"\"\"Should be raised when creator failed because of known issue.\n\n    Message of error should be user readable.\n    \"\"\"\n\n    def __init__(self, message):\n        super(CreatorError, self).__init__(message)\n\n\n@six.add_metaclass(ABCMeta)\nclass SubsetConvertorPlugin(object):\n    \"\"\"Helper for conversion of instances created using legacy creators.\n\n    Conversion from legacy creators would mean to loose legacy instances,\n    convert them automatically or write a script which must user run. All of\n    these solutions are workign but will happen without asking or user must\n    know about them. This plugin can be used to show legacy instances in\n    Publisher and give user ability to run conversion script.\n\n    Convertor logic should be very simple. Method 'find_instances' is to\n    look for legacy instances in scene a possibly call\n    pre-implemented 'add_convertor_item'.\n\n    User will have ability to trigger conversion which is executed by calling\n    'convert' which should call 'remove_convertor_item' when is done.\n\n    It does make sense to add only one or none legacy item to create context\n    for convertor as it's not possible to choose which instace are converted\n    and which are not.\n\n    Convertor can use 'collection_shared_data' property like creators. Also\n    can store any information to it's object for conversion purposes.\n\n    Args:\n        create_context\n    \"\"\"\n\n    _log = None\n\n    def __init__(self, create_context):\n        self._create_context = create_context\n\n    @property\n    def log(self):\n        \"\"\"Logger of the plugin.\n\n        Returns:\n            logging.Logger: Logger with name of the plugin.\n        \"\"\"\n\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    @property\n    def host(self):\n        return self._create_context.host\n\n    @property\n    @abstractmethod\n    def identifier(self):\n        \"\"\"Converted identifier.\n\n        Returns:\n            str: Converted identifier unique for all converters in host.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def find_instances(self):\n        \"\"\"Look for legacy instances in the scene.\n\n        Should call 'add_convertor_item' if there is at least one instance to\n        convert.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def convert(self):\n        \"\"\"Conversion code.\"\"\"\n\n        pass\n\n    @property\n    def create_context(self):\n        \"\"\"Quick access to create context.\n\n        Returns:\n            CreateContext: Context which initialized the plugin.\n        \"\"\"\n\n        return self._create_context\n\n    @property\n    def collection_shared_data(self):\n        \"\"\"Access to shared data that can be used during 'find_instances'.\n\n        Retruns:\n            Dict[str, Any]: Shared data.\n\n        Raises:\n            UnavailableSharedData: When called out of collection phase.\n        \"\"\"\n\n        return self._create_context.collection_shared_data\n\n    def add_convertor_item(self, label):\n        \"\"\"Add item to CreateContext.\n\n        Args:\n            label (str): Label of item which will show in UI.\n        \"\"\"\n\n        self._create_context.add_convertor_item(self.identifier, label)\n\n    def remove_convertor_item(self):\n        \"\"\"Remove legacy item from create context when conversion finished.\"\"\"\n\n        self._create_context.remove_convertor_item(self.identifier)\n\n\n@six.add_metaclass(ABCMeta)\nclass BaseCreator:\n    \"\"\"Plugin that create and modify instance data before publishing process.\n\n    We should maybe find better name as creation is only one part of it's logic\n    and to avoid expectations that it is the same as `avalon.api.Creator`.\n\n    Single object should be used for multiple instances instead of single\n    instance per one creator object. Do not store temp data or mid-process data\n    to `self` if it's not Plugin specific.\n\n    Args:\n        project_settings (Dict[str, Any]): Project settings.\n        create_context (CreateContext): Context which initialized creator.\n        headless (bool): Running in headless mode.\n    \"\"\"\n\n    # Label shown in UI\n    label = None\n    group_label = None\n    # Cached group label after first call 'get_group_label'\n    _cached_group_label = None\n\n    # Order in which will be plugin executed (collect & update instances)\n    #   less == earlier -> Order '90' will be processed before '100'\n    order = 100\n\n    # Variable to store logger\n    _log = None\n\n    # Creator is enabled (Probably does not have reason of existence?)\n    enabled = True\n\n    # Creator (and family) icon\n    # - may not be used if `get_icon` is reimplemented\n    icon = None\n\n    # Instance attribute definitions that can be changed per instance\n    # - returns list of attribute definitions from\n    #       `openpype.pipeline.attribute_definitions`\n    instance_attr_defs = []\n\n    # Filtering by host name - can be used to be filtered by host name\n    # - used on all hosts when set to 'None' for Backwards compatibility\n    #   - was added afterwards\n    # QUESTION make this required?\n    host_name = None\n\n    # Settings auto-apply helpers\n    # Root key in project settings (mandatory for auto-apply to work)\n    settings_category = None\n    # Name of plugin in create settings > class name is used if not set\n    settings_name = None\n\n    def __init__(\n        self, project_settings, system_settings, create_context, headless=False\n    ):\n        # Reference to CreateContext\n        self.create_context = create_context\n        self.project_settings = project_settings\n\n        # Creator is running in headless mode (without UI elemets)\n        # - we may use UI inside processing this attribute should be checked\n        self.headless = headless\n\n        expect_system_settings = False\n        if is_func_signature_supported(\n            self.apply_settings, project_settings\n        ):\n            self.apply_settings(project_settings)\n        else:\n            expect_system_settings = True\n            # Backwards compatibility for system settings\n            self.apply_settings(project_settings, system_settings)\n\n        init_use_base = any(\n            self.__class__.__init__ is cls.__init__\n            for cls in {\n                BaseCreator,\n                Creator,\n                HiddenCreator,\n                AutoCreator,\n            }\n        )\n        if not init_use_base or expect_system_settings:\n            self.log.warning((\n                \"WARNING: Source - Create plugin {}.\"\n                \" System settings argument will not be passed to\"\n                \" '__init__' and 'apply_settings' methods in future versions\"\n                \" of OpenPype. Planned version to drop the support\"\n                \" is 3.16.6 or 3.17.0. Please contact Ynput core team if you\"\n                \" need to keep system settings.\"\n            ).format(self.__class__.__name__))\n\n    @staticmethod\n    def _get_settings_values(project_settings, category_name, plugin_name):\n        \"\"\"Helper method to get settings values.\n\n        Args:\n            project_settings (dict[str, Any]): Project settings.\n            category_name (str): Category of settings.\n            plugin_name (str): Name of settings.\n\n        Returns:\n            Union[dict[str, Any], None]: Settings values or None.\n        \"\"\"\n\n        settings = project_settings.get(category_name)\n        if not settings:\n            return None\n\n        create_settings = settings.get(\"create\")\n        if not create_settings:\n            return None\n\n        return create_settings.get(plugin_name)\n\n    def apply_settings(self, project_settings):\n        \"\"\"Method called on initialization of plugin to apply settings.\n\n        Default implementation tries to auto-apply settings values if are\n            in expected hierarchy.\n\n        Data hierarchy to auto-apply settings:\n            ├─ {self.settings_category}                 - Root key in settings\n            │ └─ \"create\"                               - Hardcoded key\n            │   └─ {self.settings_name} | {class name}  - Name of plugin\n            │     ├─ ... attribute values...            - Attribute/value pair\n\n        It is mandatory to define 'settings_category' attribute. Attribute\n        'settings_name' is optional and class name is used if is not defined.\n\n        Example data:\n            ProjectSettings {\n                \"maya\": {                    # self.settings_category\n                    \"create\": {              # Hardcoded key\n                        \"CreateAnimation\": { # self.settings_name / class name\n                            \"enabled\": True, # --- Attributes to set ---\n                            \"optional\": True,#\n                            \"active\": True,  #\n                            \"fps\": 25,       # -------------------------\n                        },\n                        ...\n                    },\n                    ...\n                },\n                ...\n            }\n\n        Args:\n            project_settings (dict[str, Any]): Project settings.\n        \"\"\"\n\n        settings_category = self.settings_category\n        if not settings_category:\n            return\n\n        cls_name = self.__class__.__name__\n        settings_name = self.settings_name or cls_name\n\n        settings = self._get_settings_values(\n            project_settings, settings_category, settings_name\n        )\n        if settings is None:\n            self.log.debug(\"No settings found for {}\".format(cls_name))\n            return\n\n        for key, value in settings.items():\n            # Log out attributes that are not defined on plugin object\n            # - those may be potential dangerous typos in settings\n            if not hasattr(self, key):\n                self.log.debug((\n                    \"Applying settings to unknown attribute '{}' on '{}'.\"\n                ).format(\n                    key, cls_name\n                ))\n            setattr(self, key, value)\n\n\n    @property\n    def identifier(self):\n        \"\"\"Identifier of creator (must be unique).\n\n        Default implementation returns plugin's family.\n        \"\"\"\n\n        return self.family\n\n    @property\n    @abstractmethod\n    def family(self):\n        \"\"\"Family that plugin represents.\"\"\"\n\n        pass\n\n    @property\n    def project_name(self):\n        \"\"\"Current project name.\n\n        Returns:\n            str: Name of a project.\n        \"\"\"\n\n        return self.create_context.project_name\n\n    @property\n    def project_anatomy(self):\n        \"\"\"Current project anatomy.\n\n        Returns:\n            Anatomy: Project anatomy object.\n        \"\"\"\n\n        return self.create_context.project_anatomy\n\n    @property\n    def host(self):\n        return self.create_context.host\n\n    def get_group_label(self):\n        \"\"\"Group label under which are instances grouped in UI.\n\n        Default implementation use attributes in this order:\n            - 'group_label' -> 'label' -> 'identifier'\n                Keep in mind that 'identifier' use 'family' by default.\n\n        Returns:\n            str: Group label that can be used for grouping of instances in UI.\n                Group label can be overriden by instance itself.\n        \"\"\"\n\n        if self._cached_group_label is None:\n            label = self.identifier\n            if self.group_label:\n                label = self.group_label\n            elif self.label:\n                label = self.label\n            self._cached_group_label = label\n        return self._cached_group_label\n\n    @property\n    def log(self):\n        \"\"\"Logger of the plugin.\n\n        Returns:\n            logging.Logger: Logger with name of the plugin.\n        \"\"\"\n\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    def _add_instance_to_context(self, instance):\n        \"\"\"Helper method to add instance to create context.\n\n        Instances should be stored to DCC workfile metadata to be able reload\n        them and also stored to CreateContext in which is creator plugin\n        existing at the moment to be able use it without refresh of\n        CreateContext.\n\n        Args:\n            instance (CreatedInstance): New created instance.\n        \"\"\"\n\n        self.create_context.creator_adds_instance(instance)\n\n    def _remove_instance_from_context(self, instance):\n        \"\"\"Helper method to remove instance from create context.\n\n        Instances must be removed from DCC workfile metadat aand from create\n        context in which plugin is existing at the moment of removement to\n        propagate the change without restarting create context.\n\n        Args:\n            instance (CreatedInstance): Instance which should be removed.\n        \"\"\"\n\n        self.create_context.creator_removed_instance(instance)\n\n    @abstractmethod\n    def create(self):\n        \"\"\"Create new instance.\n\n        Replacement of `process` method from avalon implementation.\n        - must expect all data that were passed to init in previous\n            implementation\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def collect_instances(self):\n        \"\"\"Collect existing instances related to this creator plugin.\n\n        The implementation differs on host abilities. The creator has to\n        collect metadata about instance and create 'CreatedInstance' object\n        which should be added to 'CreateContext'.\n\n        Example:\n        ```python\n        def collect_instances(self):\n            # Getting existing instances is different per host implementation\n            for instance_data in pipeline.list_instances():\n                # Process only instances that were created by this creator\n                creator_id = instance_data.get(\"creator_identifier\")\n                if creator_id == self.identifier:\n                    # Create instance object from existing data\n                    instance = CreatedInstance.from_existing(\n                        instance_data, self\n                    )\n                    # Add instance to create context\n                    self._add_instance_to_context(instance)\n        ```\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def update_instances(self, update_list):\n        \"\"\"Store changes of existing instances so they can be recollected.\n\n        Args:\n            update_list(List[UpdateData]): Gets list of tuples. Each item\n                contain changed instance and it's changes.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def remove_instances(self, instances):\n        \"\"\"Method called on instance removement.\n\n        Can also remove instance metadata from context but should return\n        'True' if did so.\n\n        Args:\n            instance(List[CreatedInstance]): Instance objects which should be\n                removed.\n        \"\"\"\n\n        pass\n\n    def get_icon(self):\n        \"\"\"Icon of creator (family).\n\n        Can return path to image file or awesome icon name.\n        \"\"\"\n\n        return self.icon\n\n    def get_dynamic_data(\n        self, variant, task_name, asset_doc, project_name, host_name, instance\n    ):\n        \"\"\"Dynamic data for subset name filling.\n\n        These may be get dynamically created based on current context of\n        workfile.\n        \"\"\"\n\n        return {}\n\n    def get_subset_name(\n        self,\n        variant,\n        task_name,\n        asset_doc,\n        project_name,\n        host_name=None,\n        instance=None\n    ):\n        \"\"\"Return subset name for passed context.\n\n        CHANGES:\n        Argument `asset_id` was replaced with `asset_doc`. It is easier to\n        query asset before. In some cases would this method be called multiple\n        times and it would be too slow to query asset document on each\n        callback.\n\n        NOTE:\n        Asset document is not used yet but is required if would like to use\n        task type in subset templates.\n\n        Method is also called on subset name update. In that case origin\n        instance is passed in.\n\n        Args:\n            variant(str): Subset name variant. In most of cases user input.\n            task_name(str): For which task subset is created.\n            asset_doc(dict): Asset document for which subset is created.\n            project_name(str): Project name.\n            host_name(str): Which host creates subset.\n            instance(CreatedInstance|None): Object of 'CreatedInstance' for\n                which is subset name updated. Passed only on subset name\n                update.\n        \"\"\"\n\n        dynamic_data = self.get_dynamic_data(\n            variant, task_name, asset_doc, project_name, host_name, instance\n        )\n\n        return get_subset_name(\n            self.family,\n            variant,\n            task_name,\n            asset_doc,\n            project_name,\n            host_name,\n            dynamic_data=dynamic_data,\n            project_settings=self.project_settings\n        )\n\n    def get_instance_attr_defs(self):\n        \"\"\"Plugin attribute definitions.\n\n        Attribute definitions of plugin that hold data about created instance\n        and values are stored to metadata for future usage and for publishing\n        purposes.\n\n        NOTE:\n        Convert method should be implemented which should care about updating\n        keys/values when plugin attributes change.\n\n        Returns:\n            List[AbstractAttrDef]: Attribute definitions that can be tweaked\n                for created instance.\n        \"\"\"\n\n        return self.instance_attr_defs\n\n    @property\n    def collection_shared_data(self):\n        \"\"\"Access to shared data that can be used during creator's collection.\n\n        Retruns:\n            Dict[str, Any]: Shared data.\n\n        Raises:\n            UnavailableSharedData: When called out of collection phase.\n        \"\"\"\n\n        return self.create_context.collection_shared_data\n\n    def set_instance_thumbnail_path(self, instance_id, thumbnail_path=None):\n        \"\"\"Set path to thumbnail for instance.\"\"\"\n\n        self.create_context.thumbnail_paths_by_instance_id[instance_id] = (\n            thumbnail_path\n        )\n\n    def get_next_versions_for_instances(self, instances):\n        \"\"\"Prepare next versions for instances.\n\n        This is helper method to receive next possible versions for instances.\n        It is using context information on instance to receive them, 'asset'\n        and 'subset'.\n\n        Output will contain version by each instance id.\n\n        Args:\n            instances (list[CreatedInstance]): Instances for which to get next\n                versions.\n\n        Returns:\n            Dict[str, int]: Next versions by instance id.\n        \"\"\"\n\n        return get_next_versions_for_instances(\n            self.create_context.project_name, instances\n        )\n\n\nclass Creator(BaseCreator):\n    \"\"\"Creator that has more information for artist to show in UI.\n\n    Creation requires prepared subset name and instance data.\n    \"\"\"\n\n    # GUI Purposes\n    # - default_variants may not be used if `get_default_variants` is overriden\n    default_variants = []\n\n    # Default variant used in 'get_default_variant'\n    _default_variant = None\n\n    # Short description of family\n    # - may not be used if `get_description` is overriden\n    description = None\n\n    # Detailed description of family for artists\n    # - may not be used if `get_detail_description` is overriden\n    detailed_description = None\n\n    # It does make sense to change context on creation\n    # - in some cases it may confuse artists because it would not be used\n    #      e.g. for buld creators\n    create_allow_context_change = True\n    # A thumbnail can be passed in precreate attributes\n    # - if is set to True is should expect that a thumbnail path under key\n    #   PRE_CREATE_THUMBNAIL_KEY can be sent in data with precreate data\n    # - is disabled by default because the feature was added in later stages\n    #   and creators who would not expect PRE_CREATE_THUMBNAIL_KEY could\n    #   cause issues with instance data\n    create_allow_thumbnail = False\n\n    # Precreate attribute definitions showed before creation\n    # - similar to instance attribute definitions\n    pre_create_attr_defs = []\n\n    def __init__(self, *args, **kwargs):\n        cls = self.__class__\n\n        # Fix backwards compatibility for plugins which override\n        #   'default_variant' attribute directly\n        if not isinstance(cls.default_variant, property):\n            # Move value from 'default_variant' to '_default_variant'\n            self._default_variant = self.default_variant\n            # Create property 'default_variant' on the class\n            cls.default_variant = property(\n                cls._get_default_variant_wrap,\n                cls._set_default_variant_wrap\n            )\n        super(Creator, self).__init__(*args, **kwargs)\n\n    @property\n    def show_order(self):\n        \"\"\"Order in which is creator shown in UI.\n\n        Returns:\n            int: Order in which is creator shown (less == earlier). By default\n                is using Creator's 'order' or processing.\n        \"\"\"\n\n        return self.order\n\n    @abstractmethod\n    def create(self, subset_name, instance_data, pre_create_data):\n        \"\"\"Create new instance and store it.\n\n        Ideally should be stored to workfile using host implementation.\n\n        Args:\n            subset_name(str): Subset name of created instance.\n            instance_data(dict): Base data for instance.\n            pre_create_data(dict): Data based on pre creation attributes.\n                Those may affect how creator works.\n        \"\"\"\n\n        # instance = CreatedInstance(\n        #     self.family, subset_name, instance_data\n        # )\n        pass\n\n    def get_description(self):\n        \"\"\"Short description of family and plugin.\n\n        Returns:\n            str: Short description of family.\n        \"\"\"\n\n        return self.description\n\n    def get_detail_description(self):\n        \"\"\"Description of family and plugin.\n\n        Can be detailed with markdown or html tags.\n\n        Returns:\n            str: Detailed description of family for artist.\n        \"\"\"\n\n        return self.detailed_description\n\n    def get_default_variants(self):\n        \"\"\"Default variant values for UI tooltips.\n\n        Replacement of `default_variants` attribute. Using method gives\n        ability to have some \"logic\" other than attribute values.\n\n        By default, returns `default_variants` value.\n\n        Returns:\n            List[str]: Whisper variants for user input.\n        \"\"\"\n\n        return copy.deepcopy(self.default_variants)\n\n    def get_default_variant(self, only_explicit=False):\n        \"\"\"Default variant value that will be used to prefill variant input.\n\n        This is for user input and value may not be content of result from\n        `get_default_variants`.\n\n        Note:\n            This method does not allow to have empty string as\n                default variant.\n\n        Args:\n            only_explicit (Optional[bool]): If True, only explicit default\n                variant from '_default_variant' will be returned.\n\n        Returns:\n            str: Variant value.\n        \"\"\"\n\n        if only_explicit or self._default_variant:\n            return self._default_variant\n\n        for variant in self.get_default_variants():\n            return variant\n        return DEFAULT_VARIANT_VALUE\n\n    def _get_default_variant_wrap(self):\n        \"\"\"Default variant value that will be used to prefill variant input.\n\n        Wrapper for 'get_default_variant'.\n\n        Notes:\n            This method is wrapper for 'get_default_variant'\n                for 'default_variant' property, so creator can override\n                the method.\n\n        Returns:\n            str: Variant value.\n        \"\"\"\n\n        return self.get_default_variant()\n\n    def _set_default_variant_wrap(self, variant):\n        \"\"\"Set default variant value.\n\n        This method is needed for automated settings overrides which are\n        changing attributes based on keys in settings.\n\n        Args:\n            variant (str): New default variant value.\n        \"\"\"\n\n        self._default_variant = variant\n\n    default_variant = property(\n        _get_default_variant_wrap,\n        _set_default_variant_wrap\n    )\n\n    def get_pre_create_attr_defs(self):\n        \"\"\"Plugin attribute definitions needed for creation.\n        Attribute definitions of plugin that define how creation will work.\n        Values of these definitions are passed to `create` method.\n\n        Note:\n            Convert method should be implemented which should care about\n            updating keys/values when plugin attributes change.\n\n        Returns:\n            List[AbstractAttrDef]: Attribute definitions that can be tweaked\n                for created instance.\n        \"\"\"\n        return self.pre_create_attr_defs\n\n\nclass HiddenCreator(BaseCreator):\n    @abstractmethod\n    def create(self, instance_data, source_data):\n        pass\n\n\nclass AutoCreator(BaseCreator):\n    \"\"\"Creator which is automatically triggered without user interaction.\n\n    Can be used e.g. for `workfile`.\n    \"\"\"\n\n    def remove_instances(self, instances):\n        \"\"\"Skip removement.\"\"\"\n        pass\n\n\ndef discover_creator_plugins(*args, **kwargs):\n    return discover(BaseCreator, *args, **kwargs)\n\n\ndef discover_convertor_plugins(*args, **kwargs):\n    return discover(SubsetConvertorPlugin, *args, **kwargs)\n\n\ndef discover_legacy_creator_plugins():\n    from openpype.pipeline import get_current_project_name\n\n    log = Logger.get_logger(\"CreatorDiscover\")\n\n    plugins = discover(LegacyCreator)\n    project_name = get_current_project_name()\n    system_settings = get_system_settings()\n    project_settings = get_project_settings(project_name)\n    for plugin in plugins:\n        try:\n            plugin.apply_settings(project_settings, system_settings)\n        except Exception:\n            log.warning(\n                \"Failed to apply settings to creator {}\".format(\n                    plugin.__name__\n                ),\n                exc_info=True\n            )\n    return plugins\n\n\ndef get_legacy_creator_by_name(creator_name, case_sensitive=False):\n    \"\"\"Find creator plugin by name.\n\n    Args:\n        creator_name (str): Name of creator class that should be returned.\n        case_sensitive (bool): Match of creator plugin name is case sensitive.\n            Set to `False` by default.\n\n    Returns:\n        Creator: Return first matching plugin or `None`.\n    \"\"\"\n\n    # Lower input creator name if is not case sensitive\n    if not case_sensitive:\n        creator_name = creator_name.lower()\n\n    for creator_plugin in discover_legacy_creator_plugins():\n        _creator_name = creator_plugin.__name__\n\n        # Lower creator plugin name if is not case sensitive\n        if not case_sensitive:\n            _creator_name = _creator_name.lower()\n\n        if _creator_name == creator_name:\n            return creator_plugin\n    return None\n\n\ndef register_creator_plugin(plugin):\n    if issubclass(plugin, BaseCreator):\n        register_plugin(BaseCreator, plugin)\n\n    elif issubclass(plugin, LegacyCreator):\n        register_plugin(LegacyCreator, plugin)\n\n    elif issubclass(plugin, SubsetConvertorPlugin):\n        register_plugin(SubsetConvertorPlugin, plugin)\n\n\ndef deregister_creator_plugin(plugin):\n    if issubclass(plugin, BaseCreator):\n        deregister_plugin(BaseCreator, plugin)\n\n    elif issubclass(plugin, LegacyCreator):\n        deregister_plugin(LegacyCreator, plugin)\n\n    elif issubclass(plugin, SubsetConvertorPlugin):\n        deregister_plugin(SubsetConvertorPlugin, plugin)\n\n\ndef register_creator_plugin_path(path):\n    register_plugin_path(BaseCreator, path)\n    register_plugin_path(LegacyCreator, path)\n    register_plugin_path(SubsetConvertorPlugin, path)\n\n\ndef deregister_creator_plugin_path(path):\n    deregister_plugin_path(BaseCreator, path)\n    deregister_plugin_path(LegacyCreator, path)\n    deregister_plugin_path(SubsetConvertorPlugin, path)\n\n\ndef cache_and_get_instances(creator, shared_key, list_instances_func):\n    \"\"\"Common approach to cache instances in shared data.\n\n    This is helper function which does not handle cases when a 'shared_key' is\n    used for different list instances functions. The same approach of caching\n    instances into 'collection_shared_data' is not required but is so common\n    we've decided to unify it to some degree.\n\n    Function 'list_instances_func' is called only if 'shared_key' is not\n    available in 'collection_shared_data' on creator.\n\n    Args:\n        creator (Creator): Plugin which would like to get instance data.\n        shared_key (str): Key under which output of function will be stored.\n        list_instances_func (Function): Function that will return instance data\n            if data were not yet stored under 'shared_key'.\n\n    Returns:\n        Dict[str, Dict[str, Any]]: Cached instances by creator identifier from\n            result of passed function.\n    \"\"\"\n\n    if shared_key not in creator.collection_shared_data:\n        value = collections.defaultdict(list)\n        for instance in list_instances_func():\n            identifier = instance.get(\"creator_identifier\")\n            value[identifier].append(instance)\n        creator.collection_shared_data[shared_key] = value\n    return creator.collection_shared_data[shared_key]\n"
  },
  {
    "path": "openpype/pipeline/create/legacy_create.py",
    "content": "\"\"\"Create workflow moved from avalon-core repository.\n\nRenamed classes and functions\n- 'Creator' -> 'LegacyCreator'\n- 'create'  -> 'legacy_create'\n\"\"\"\n\nimport os\nimport logging\nimport collections\n\nfrom openpype.client import get_asset_by_id\n\nfrom .subset_name import get_subset_name\n\n\nclass LegacyCreator(object):\n    \"\"\"Determine how assets are created\"\"\"\n    label = None\n    family = None\n    defaults = None\n    maintain_selection = True\n    enabled = True\n\n    dynamic_subset_keys = []\n\n    log = logging.getLogger(\"LegacyCreator\")\n    log.propagate = True\n\n    def __init__(self, name, asset, options=None, data=None):\n        self.name = name  # For backwards compatibility\n        self.options = options\n\n        # Default data\n        self.data = collections.OrderedDict()\n        self.data[\"id\"] = \"pyblish.avalon.instance\"\n        self.data[\"family\"] = self.family\n        self.data[\"asset\"] = asset\n        self.data[\"subset\"] = name\n        self.data[\"active\"] = True\n\n        self.data.update(data or {})\n\n    @classmethod\n    def apply_settings(cls, project_settings, system_settings):\n        \"\"\"Apply OpenPype settings to a plugin class.\"\"\"\n\n        host_name = os.environ.get(\"AVALON_APP\")\n        plugin_type = \"create\"\n        plugin_type_settings = (\n            project_settings\n            .get(host_name, {})\n            .get(plugin_type, {})\n        )\n        global_type_settings = (\n            project_settings\n            .get(\"global\", {})\n            .get(plugin_type, {})\n        )\n        if not global_type_settings and not plugin_type_settings:\n            return\n\n        plugin_name = cls.__name__\n\n        plugin_settings = None\n        # Look for plugin settings in host specific settings\n        if plugin_name in plugin_type_settings:\n            plugin_settings = plugin_type_settings[plugin_name]\n\n        # Look for plugin settings in global settings\n        elif plugin_name in global_type_settings:\n            plugin_settings = global_type_settings[plugin_name]\n\n        if not plugin_settings:\n            return\n\n        cls.log.debug(\">>> We have preset for {}\".format(plugin_name))\n        for option, value in plugin_settings.items():\n            if option == \"enabled\" and value is False:\n                cls.log.debug(\"  - is disabled by preset\")\n            else:\n                cls.log.debug(\"  - setting `{}`: `{}`\".format(option, value))\n            setattr(cls, option, value)\n\n    def process(self):\n        pass\n\n    @classmethod\n    def get_dynamic_data(\n        cls, variant, task_name, asset_id, project_name, host_name\n    ):\n        \"\"\"Return dynamic data for current Creator plugin.\n\n        By default return keys from `dynamic_subset_keys` attribute as mapping\n        to keep formatted template unchanged.\n\n        ```\n        dynamic_subset_keys = [\"my_key\"]\n        ---\n        output = {\n            \"my_key\": \"{my_key}\"\n        }\n        ```\n\n        Dynamic keys may override default Creator keys (family, task, asset,\n        ...) but do it wisely if you need.\n\n        All of keys will be converted into 3 variants unchanged, capitalized\n        and all upper letters. Because of that are all keys lowered.\n\n        This method can be modified to prefill some values just keep in mind it\n        is class method.\n\n        Returns:\n            dict: Fill data for subset name template.\n        \"\"\"\n        dynamic_data = {}\n        for key in cls.dynamic_subset_keys:\n            key = key.lower()\n            dynamic_data[key] = \"{\" + key + \"}\"\n        return dynamic_data\n\n    @classmethod\n    def get_subset_name(\n        cls, variant, task_name, asset_id, project_name, host_name=None\n    ):\n        \"\"\"Return subset name created with entered arguments.\n\n        Logic extracted from Creator tool. This method should give ability\n        to get subset name without the tool.\n\n        TODO: Maybe change `variant` variable.\n\n        By default is output concatenated family with user text.\n\n        Args:\n            variant (str): What is entered by user in creator tool.\n            task_name (str): Context's task name.\n            asset_id (ObjectId): Mongo ID of context's asset.\n            project_name (str): Context's project name.\n            host_name (str): Name of host.\n\n        Returns:\n            str: Formatted subset name with entered arguments. Should match\n                config's logic.\n        \"\"\"\n\n        dynamic_data = cls.get_dynamic_data(\n            variant, task_name, asset_id, project_name, host_name\n        )\n\n        asset_doc = get_asset_by_id(\n            project_name, asset_id, fields=[\"data.tasks\"]\n        )\n\n        return get_subset_name(\n            cls.family,\n            variant,\n            task_name,\n            asset_doc,\n            project_name,\n            host_name,\n            dynamic_data=dynamic_data\n        )\n\n\ndef legacy_create(Creator, name, asset, options=None, data=None):\n    \"\"\"Create a new instance\n\n    Associate nodes with a subset and family. These nodes are later\n    validated, according to their `family`, and integrated into the\n    shared environment, relative their `subset`.\n\n    Data relative each family, along with default data, are imprinted\n    into the resulting objectSet. This data is later used by extractors\n    and finally asset browsers to help identify the origin of the asset.\n\n    Arguments:\n        Creator (Creator): Class of creator\n        name (str): Name of subset\n        asset (str): Name of asset\n        options (dict, optional): Additional options from GUI\n        data (dict, optional): Additional data from GUI\n\n    Raises:\n        NameError on `subset` already exists\n        KeyError on invalid dynamic property\n        RuntimeError on host error\n\n    Returns:\n        Name of instance\n\n    \"\"\"\n    from openpype.pipeline import registered_host\n\n    host = registered_host()\n    plugin = Creator(name, asset, options, data)\n\n    if plugin.maintain_selection is True:\n        with host.maintained_selection():\n            print(\"Running %s with maintained selection\" % plugin)\n            instance = plugin.process()\n        return instance\n\n    print(\"Running %s\" % plugin)\n    instance = plugin.process()\n    return instance\n"
  },
  {
    "path": "openpype/pipeline/create/subset_name.py",
    "content": "import os\n\nfrom openpype.settings import get_project_settings\nfrom openpype.lib import filter_profiles, prepare_template_data\nfrom openpype.pipeline import legacy_io\n\nfrom .constants import DEFAULT_SUBSET_TEMPLATE\n\n\nclass TaskNotSetError(KeyError):\n    def __init__(self, msg=None):\n        if not msg:\n            msg = \"Creator's subset name template requires task name.\"\n        super(TaskNotSetError, self).__init__(msg)\n\n\nclass TemplateFillError(Exception):\n    def __init__(self, msg=None):\n        if not msg:\n            msg = \"Creator's subset name template is missing key value.\"\n        super(TemplateFillError, self).__init__(msg)\n\n\ndef get_subset_name_template(\n    project_name,\n    family,\n    task_name,\n    task_type,\n    host_name,\n    default_template=None,\n    project_settings=None\n):\n    \"\"\"Get subset name template based on passed context.\n\n    Args:\n        project_name (str): Project on which the context lives.\n        family (str): Family (subset type) for which the subset name is\n            calculated.\n        host_name (str): Name of host in which the subset name is calculated.\n        task_name (str): Name of task in which context the subset is created.\n        task_type (str): Type of task in which context the subset is created.\n        default_template (Union[str, None]): Default template which is used if\n            settings won't find any matching possitibility. Constant\n            'DEFAULT_SUBSET_TEMPLATE' is used if not defined.\n        project_settings (Union[Dict[str, Any], None]): Prepared settings for\n            project. Settings are queried if not passed.\n    \"\"\"\n\n    if project_settings is None:\n        project_settings = get_project_settings(project_name)\n    tools_settings = project_settings[\"global\"][\"tools\"]\n    profiles = tools_settings[\"creator\"][\"subset_name_profiles\"]\n    filtering_criteria = {\n        \"families\": family,\n        \"hosts\": host_name,\n        \"tasks\": task_name,\n        \"task_types\": task_type\n    }\n\n    matching_profile = filter_profiles(profiles, filtering_criteria)\n    template = None\n    if matching_profile:\n        template = matching_profile[\"template\"]\n\n    # Make sure template is set (matching may have empty string)\n    if not template:\n        template = default_template or DEFAULT_SUBSET_TEMPLATE\n    return template\n\n\ndef get_subset_name(\n    family,\n    variant,\n    task_name,\n    asset_doc,\n    project_name=None,\n    host_name=None,\n    default_template=None,\n    dynamic_data=None,\n    project_settings=None,\n    family_filter=None,\n):\n    \"\"\"Calculate subset name based on passed context and OpenPype settings.\n\n    Subst name templates are defined in `project_settings/global/tools/creator\n    /subset_name_profiles` where are profiles with host name, family, task name\n    and task type filters. If context does not match any profile then\n    `DEFAULT_SUBSET_TEMPLATE` is used as default template.\n\n    That's main reason why so many arguments are required to calculate subset\n    name.\n\n    Option to pass family filter was added for special cases when creator or\n    automated publishing require special subset name template which would be\n    hard to maintain using its family value.\n        Why not just pass the right family? -> Family is also used as fill\n            value and for filtering of publish plugins.\n\n    Todos:\n        Find better filtering options to avoid requirement of\n            argument 'family_filter'.\n\n    Args:\n        family (str): Instance family.\n        variant (str): In most of the cases it is user input during creation.\n        task_name (str): Task name on which context is instance created.\n        asset_doc (dict): Queried asset document with its tasks in data.\n            Used to get task type.\n        project_name (Optional[str]): Name of project on which is instance\n            created. Important for project settings that are loaded.\n        host_name (Optional[str]): One of filtering criteria for template\n            profile filters.\n        default_template (Optional[str]): Default template if any profile does\n            not match passed context. Constant 'DEFAULT_SUBSET_TEMPLATE'\n            is used if is not passed.\n        dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for\n            a creator which creates instance.\n        project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings\n            for project. Settings are queried if not passed.\n        family_filter (Optional[str]): Use different family for subset template\n            filtering. Value of 'family' is used when not passed.\n\n    Raises:\n        TemplateFillError: If filled template contains placeholder key which is not\n            collected.\n    \"\"\"\n\n    if not family:\n        return \"\"\n\n    if not host_name:\n        host_name = os.environ.get(\"AVALON_APP\")\n\n    # Use only last part of class family value split by dot (`.`)\n    family = family.rsplit(\".\", 1)[-1]\n\n    if project_name is None:\n        project_name = legacy_io.Session[\"AVALON_PROJECT\"]\n\n    asset_tasks = asset_doc.get(\"data\", {}).get(\"tasks\") or {}\n    task_info = asset_tasks.get(task_name) or {}\n    task_type = task_info.get(\"type\")\n\n    template = get_subset_name_template(\n        project_name,\n        family_filter or family,\n        task_name,\n        task_type,\n        host_name,\n        default_template=default_template,\n        project_settings=project_settings\n    )\n    # Simple check of task name existence for template with {task} in\n    #   - missing task should be possible only in Standalone publisher\n    if not task_name and \"{task\" in template.lower():\n        raise TaskNotSetError()\n\n    fill_pairs = {\n        \"variant\": variant,\n        \"family\": family,\n        \"task\": task_name\n    }\n    if dynamic_data:\n        # Dynamic data may override default values\n        for key, value in dynamic_data.items():\n            fill_pairs[key] = value\n\n    try:\n        return template.format(**prepare_template_data(fill_pairs))\n    except KeyError as exp:\n        raise TemplateFillError(\n            \"Value for {} key is missing in template '{}'.\"\n            \" Available values are {}\".format(str(exp), template, fill_pairs)\n        )\n"
  },
  {
    "path": "openpype/pipeline/create/utils.py",
    "content": "import collections\n\nfrom openpype.client import (\n    get_assets,\n    get_subsets,\n    get_last_versions,\n    get_asset_name_identifier,\n)\n\n\ndef get_last_versions_for_instances(\n    project_name, instances, use_value_for_missing=False\n):\n    \"\"\"Get last versions for instances by their asset and subset name.\n\n    Args:\n        project_name (str): Project name.\n        instances (list[CreatedInstance]): Instances to get next versions for.\n        use_value_for_missing (Optional[bool]): Missing values are replaced\n            with negative value if True. Otherwise None is used. -2 is used\n            for instances without filled asset or subset name. -1 is used\n            for missing entities.\n\n    Returns:\n        dict[str, Union[int, None]]: Last versions by instance id.\n    \"\"\"\n\n    output = {\n        instance.id: -1 if use_value_for_missing else None\n        for instance in instances\n    }\n    subset_names_by_asset_name = collections.defaultdict(set)\n    instances_by_hierarchy = {}\n    for instance in instances:\n        asset_name = instance.data.get(\"asset\")\n        subset_name = instance.subset_name\n        if not asset_name or not subset_name:\n            if use_value_for_missing:\n                output[instance.id] = -2\n            continue\n\n        (\n            instances_by_hierarchy\n            .setdefault(asset_name, {})\n            .setdefault(subset_name, [])\n            .append(instance)\n        )\n        subset_names_by_asset_name[asset_name].add(subset_name)\n\n    subset_names = set()\n    for names in subset_names_by_asset_name.values():\n        subset_names |= names\n\n    if not subset_names:\n        return output\n\n    asset_docs = get_assets(\n        project_name,\n        asset_names=subset_names_by_asset_name.keys(),\n        fields=[\"name\", \"_id\", \"data.parents\"]\n    )\n    asset_names_by_id = {\n        asset_doc[\"_id\"]: get_asset_name_identifier(asset_doc)\n        for asset_doc in asset_docs\n    }\n    if not asset_names_by_id:\n        return output\n\n    subset_docs = get_subsets(\n        project_name,\n        asset_ids=asset_names_by_id.keys(),\n        subset_names=subset_names,\n        fields=[\"_id\", \"name\", \"parent\"]\n    )\n    subset_docs_by_id = {}\n    for subset_doc in subset_docs:\n        # Filter subset docs by subset names under parent\n        asset_id = subset_doc[\"parent\"]\n        asset_name = asset_names_by_id[asset_id]\n        subset_name = subset_doc[\"name\"]\n        if subset_name not in subset_names_by_asset_name[asset_name]:\n            continue\n        subset_docs_by_id[subset_doc[\"_id\"]] = subset_doc\n\n    if not subset_docs_by_id:\n        return output\n\n    last_versions_by_subset_id = get_last_versions(\n        project_name,\n        subset_docs_by_id.keys(),\n        fields=[\"name\", \"parent\"]\n    )\n    for subset_id, version_doc in last_versions_by_subset_id.items():\n        subset_doc = subset_docs_by_id[subset_id]\n        asset_id = subset_doc[\"parent\"]\n        asset_name = asset_names_by_id[asset_id]\n        _instances = instances_by_hierarchy[asset_name][subset_doc[\"name\"]]\n        for instance in _instances:\n            output[instance.id] = version_doc[\"name\"]\n\n    return output\n\n\ndef get_next_versions_for_instances(project_name, instances):\n    \"\"\"Get next versions for instances by their asset and subset name.\n\n    Args:\n        project_name (str): Project name.\n        instances (list[CreatedInstance]): Instances to get next versions for.\n\n    Returns:\n        dict[str, Union[int, None]]: Next versions by instance id. Version is\n            'None' if instance has no asset or subset name.\n    \"\"\"\n\n    last_versions = get_last_versions_for_instances(\n        project_name, instances, True)\n\n    output = {}\n    for instance_id, version in last_versions.items():\n        if version == -2:\n            output[instance_id] = None\n        elif version == -1:\n            output[instance_id] = 1\n        else:\n            output[instance_id] = version + 1\n    return output\n"
  },
  {
    "path": "openpype/pipeline/delivery.py",
    "content": "\"\"\"Functions useful for delivery of published representations.\"\"\"\nimport os\nimport copy\nimport shutil\nimport glob\nimport clique\nimport collections\n\nfrom openpype.lib import create_hard_link\n\n\ndef _copy_file(src_path, dst_path):\n    \"\"\"Hardlink file if possible(to save space), copy if not.\n\n    Because of using hardlinks should not be function used in other parts\n    of pipeline.\n    \"\"\"\n\n    if os.path.exists(dst_path):\n        return\n    try:\n        create_hard_link(\n            src_path,\n            dst_path\n        )\n    except OSError:\n        shutil.copyfile(src_path, dst_path)\n\n\ndef get_format_dict(anatomy, location_path):\n    \"\"\"Returns replaced root values from user provider value.\n\n    Args:\n        anatomy (Anatomy): Project anatomy.\n        location_path (str): User provided value.\n\n    Returns:\n        (dict): Prepared data for formatting of a template.\n    \"\"\"\n\n    format_dict = {}\n    if not location_path:\n        return format_dict\n\n    location_path = location_path.replace(\"\\\\\", \"/\")\n    root_names = anatomy.root_names_from_templates(\n        anatomy.templates[\"delivery\"]\n    )\n    format_dict[\"root\"] = {}\n    for name in root_names:\n        format_dict[\"root\"][name] = location_path\n    return format_dict\n\n\ndef check_destination_path(\n    repre_id,\n    anatomy,\n    anatomy_data,\n    datetime_data,\n    template_name\n):\n    \"\"\" Try to create destination path based on 'template_name'.\n\n    In the case that path cannot be filled, template contains unmatched\n    keys, provide error message to filter out repre later.\n\n    Args:\n        repre_id (str): Representation id.\n        anatomy (Anatomy): Project anatomy.\n        anatomy_data (dict): Template data to fill anatomy templates.\n        datetime_data (dict): Values with actual date.\n        template_name (str): Name of template which should be used from anatomy\n            templates.\n    Returns:\n        Dict[str, List[str]]: Report of happened errors. Key is message title\n            value is detailed information.\n    \"\"\"\n\n    anatomy_data.update(datetime_data)\n    anatomy_filled = anatomy.format_all(anatomy_data)\n    dest_path = anatomy_filled[\"delivery\"][template_name]\n    report_items = collections.defaultdict(list)\n\n    if not dest_path.solved:\n        msg = (\n            \"Missing keys in Representation's context\"\n            \" for anatomy template \\\"{}\\\".\"\n        ).format(template_name)\n\n        sub_msg = (\n            \"Representation: {}<br>\"\n        ).format(repre_id)\n\n        if dest_path.missing_keys:\n            keys = \", \".join(dest_path.missing_keys)\n            sub_msg += (\n                \"- Missing keys: \\\"{}\\\"<br>\"\n            ).format(keys)\n\n        if dest_path.invalid_types:\n            items = []\n            for key, value in dest_path.invalid_types.items():\n                items.append(\"\\\"{}\\\" {}\".format(key, str(value)))\n\n            keys = \", \".join(items)\n            sub_msg += (\n                \"- Invalid value DataType: \\\"{}\\\"<br>\"\n            ).format(keys)\n\n        report_items[msg].append(sub_msg)\n\n    return report_items\n\n\ndef deliver_single_file(\n    src_path,\n    repre,\n    anatomy,\n    template_name,\n    anatomy_data,\n    format_dict,\n    report_items,\n    log\n):\n    \"\"\"Copy single file to calculated path based on template\n\n    Args:\n        src_path(str): path of source representation file\n        repre (dict): full repre, used only in deliver_sequence, here only\n            as to share same signature\n        anatomy (Anatomy)\n        template_name (string): user selected delivery template name\n        anatomy_data (dict): data from repre to fill anatomy with\n        format_dict (dict): root dictionary with names and values\n        report_items (collections.defaultdict): to return error messages\n        log (logging.Logger): for log printing\n\n    Returns:\n        (collections.defaultdict, int)\n    \"\"\"\n\n    # Make sure path is valid for all platforms\n    src_path = os.path.normpath(src_path.replace(\"\\\\\", \"/\"))\n\n    if not os.path.exists(src_path):\n        msg = \"{} doesn't exist for {}\".format(src_path, repre[\"_id\"])\n        report_items[\"Source file was not found\"].append(msg)\n        return report_items, 0\n\n    if format_dict:\n        anatomy_data = copy.deepcopy(anatomy_data)\n        anatomy_data[\"root\"] = format_dict[\"root\"]\n    template_obj = anatomy.templates_obj[\"delivery\"][template_name]\n    delivery_path = template_obj.format_strict(anatomy_data)\n\n    # Backwards compatibility when extension contained `.`\n    delivery_path = delivery_path.replace(\"..\", \".\")\n    # Make sure path is valid for all platforms\n    delivery_path = os.path.normpath(delivery_path.replace(\"\\\\\", \"/\"))\n    # Remove newlines from the end of the string to avoid OSError during copy\n    delivery_path = delivery_path.rstrip()\n\n    delivery_folder = os.path.dirname(delivery_path)\n    if not os.path.exists(delivery_folder):\n        os.makedirs(delivery_folder)\n\n    log.debug(\"Copying single: {} -> {}\".format(src_path, delivery_path))\n    _copy_file(src_path, delivery_path)\n\n    return report_items, 1\n\n\ndef deliver_sequence(\n    src_path,\n    repre,\n    anatomy,\n    template_name,\n    anatomy_data,\n    format_dict,\n    report_items,\n    log,\n    has_renumbered_frame=False,\n    new_frame_start=0\n):\n    \"\"\" For Pype2(mainly - works in 3 too) where representation might not\n        contain files.\n\n        Uses listing physical files (not 'files' on repre as a)might not be\n         present, b)might not be reliable for representation and copying them.\n\n         TODO Should be refactored when files are sufficient to drive all\n         representations.\n\n    Args:\n        src_path(str): path of source representation file\n        repre (dict): full representation\n        anatomy (Anatomy)\n        template_name (string): user selected delivery template name\n        anatomy_data (dict): data from repre to fill anatomy with\n        format_dict (dict): root dictionary with names and values\n        report_items (collections.defaultdict): to return error messages\n        log (logging.Logger): for log printing\n\n    Returns:\n        (collections.defaultdict, int)\n    \"\"\"\n\n    src_path = os.path.normpath(src_path.replace(\"\\\\\", \"/\"))\n\n    def hash_path_exist(myPath):\n        res = myPath.replace('#', '*')\n        glob_search_results = glob.glob(res)\n        if len(glob_search_results) > 0:\n            return True\n        return False\n\n    if not hash_path_exist(src_path):\n        msg = \"{} doesn't exist for {}\".format(\n            src_path, repre[\"_id\"])\n        report_items[\"Source file was not found\"].append(msg)\n        return report_items, 0\n\n    delivery_templates = anatomy.templates.get(\"delivery\") or {}\n    delivery_template = delivery_templates.get(template_name)\n    if delivery_template is None:\n        msg = (\n            \"Delivery template \\\"{}\\\" in anatomy of project \\\"{}\\\"\"\n            \" was not found\"\n        ).format(template_name, anatomy.project_name)\n        report_items[\"\"].append(msg)\n        return report_items, 0\n\n    # Check if 'frame' key is available in template which is required\n    #   for sequence delivery\n    if \"{frame\" not in delivery_template:\n        msg = (\n            \"Delivery template \\\"{}\\\" in anatomy of project \\\"{}\\\"\"\n            \"does not contain '{{frame}}' key to fill. Delivery of sequence\"\n            \" can't be processed.\"\n        ).format(template_name, anatomy.project_name)\n        report_items[\"\"].append(msg)\n        return report_items, 0\n\n    dir_path, file_name = os.path.split(str(src_path))\n\n    context = repre[\"context\"]\n    ext = context.get(\"ext\", context.get(\"representation\"))\n\n    if not ext:\n        msg = \"Source extension not found, cannot find collection\"\n        report_items[msg].append(src_path)\n        log.warning(\"{} <{}>\".format(msg, context))\n        return report_items, 0\n\n    ext = \".\" + ext\n    # context.representation could be .psd\n    ext = ext.replace(\"..\", \".\")\n\n    src_collections, remainder = clique.assemble(os.listdir(dir_path))\n    src_collection = None\n    for col in src_collections:\n        if col.tail != ext:\n            continue\n\n        src_collection = col\n        break\n\n    if src_collection is None:\n        msg = \"Source collection of files was not found\"\n        report_items[msg].append(src_path)\n        log.warning(\"{} <{}>\".format(msg, src_path))\n        return report_items, 0\n\n    frame_indicator = \"@####@\"\n\n    anatomy_data = copy.deepcopy(anatomy_data)\n    anatomy_data[\"frame\"] = frame_indicator\n    if format_dict:\n        anatomy_data[\"root\"] = format_dict[\"root\"]\n    template_obj = anatomy.templates_obj[\"delivery\"][template_name]\n    delivery_path = template_obj.format_strict(anatomy_data)\n\n    delivery_path = os.path.normpath(delivery_path.replace(\"\\\\\", \"/\"))\n    delivery_folder = os.path.dirname(delivery_path)\n    dst_head, dst_tail = delivery_path.split(frame_indicator)\n    dst_padding = src_collection.padding\n    dst_collection = clique.Collection(\n        head=dst_head,\n        tail=dst_tail,\n        padding=dst_padding\n    )\n\n    if not os.path.exists(delivery_folder):\n        os.makedirs(delivery_folder)\n\n    src_head = src_collection.head\n    src_tail = src_collection.tail\n    uploaded = 0\n    first_frame = min(src_collection.indexes)\n    for index in src_collection.indexes:\n        src_padding = src_collection.format(\"{padding}\") % index\n        src_file_name = \"{}{}{}\".format(src_head, src_padding, src_tail)\n        src = os.path.normpath(\n            os.path.join(dir_path, src_file_name)\n        )\n        dst_index = index\n        if has_renumbered_frame:\n            # Calculate offset between first frame and current frame\n            # - '0' for first frame\n            offset = new_frame_start - first_frame\n            # Add offset to new frame start\n            dst_index = index + offset\n            if dst_index < 0:\n                msg = \"Renumber frame has a smaller number than original frame\"     # noqa\n                report_items[msg].append(src_file_name)\n                log.warning(\"{} <{}>\".format(msg, context))\n                return report_items, 0\n        dst_padding = dst_collection.format(\"{padding}\") % dst_index\n        dst = \"{}{}{}\".format(dst_head, dst_padding, dst_tail)\n        log.debug(\"Copying single: {} -> {}\".format(src, dst))\n        _copy_file(src, dst)\n\n        uploaded += 1\n\n    return report_items, uploaded\n"
  },
  {
    "path": "openpype/pipeline/editorial.py",
    "content": "import os\nimport re\nimport clique\n\nimport opentimelineio as otio\nfrom opentimelineio import opentime as _ot\n\n\ndef otio_range_to_frame_range(otio_range):\n    start = _ot.to_frames(\n        otio_range.start_time, otio_range.start_time.rate)\n    end = start + _ot.to_frames(\n        otio_range.duration, otio_range.duration.rate)\n    return start, end\n\n\ndef otio_range_with_handles(otio_range, instance):\n    handle_start = instance.data[\"handleStart\"]\n    handle_end = instance.data[\"handleEnd\"]\n    handles_duration = handle_start + handle_end\n    fps = float(otio_range.start_time.rate)\n    start = _ot.to_frames(otio_range.start_time, fps)\n    duration = _ot.to_frames(otio_range.duration, fps)\n\n    return _ot.TimeRange(\n        start_time=_ot.RationalTime((start - handle_start), fps),\n        duration=_ot.RationalTime((duration + handles_duration), fps)\n    )\n\n\ndef is_overlapping_otio_ranges(test_otio_range, main_otio_range, strict=False):\n    test_start, test_end = otio_range_to_frame_range(test_otio_range)\n    main_start, main_end = otio_range_to_frame_range(main_otio_range)\n    covering_exp = bool(\n        (test_start <= main_start) and (test_end >= main_end)\n    )\n    inside_exp = bool(\n        (test_start >= main_start) and (test_end <= main_end)\n    )\n    overlaying_right_exp = bool(\n        (test_start <= main_end) and (test_end >= main_end)\n    )\n    overlaying_left_exp = bool(\n        (test_end >= main_start) and (test_start <= main_start)\n    )\n\n    if not strict:\n        return any((\n            covering_exp,\n            inside_exp,\n            overlaying_right_exp,\n            overlaying_left_exp\n        ))\n    else:\n        return covering_exp\n\n\ndef convert_to_padded_path(path, padding):\n    \"\"\"\n    Return correct padding in sequence string\n\n    Args:\n        path (str): path url or simple file name\n        padding (int): number of padding\n\n    Returns:\n        type: string with reformated path\n\n    Example:\n        convert_to_padded_path(\"plate.%d.exr\") > plate.%04d.exr\n\n    \"\"\"\n    if \"%d\" in path:\n        path = re.sub(\"%d\", \"%0{padding}d\".format(padding=padding), path)\n    return path\n\n\ndef trim_media_range(media_range, source_range):\n    \"\"\"\n    Trim input media range with clip source range.\n\n    Args:\n        media_range (otio._ot._ot.TimeRange): available range of media\n        source_range (otio._ot._ot.TimeRange): clip required range\n\n    Returns:\n        otio._ot._ot.TimeRange: trimmed media range\n\n    \"\"\"\n    rw_media_start = _ot.RationalTime(\n        media_range.start_time.value + source_range.start_time.value,\n        media_range.start_time.rate\n    )\n    rw_media_duration = _ot.RationalTime(\n        source_range.duration.value,\n        media_range.duration.rate\n    )\n    return _ot.TimeRange(\n        rw_media_start, rw_media_duration)\n\n\ndef range_from_frames(start, duration, fps):\n    \"\"\"\n    Returns otio time range.\n\n    Args:\n        start (int): frame start\n        duration (int): frame duration\n        fps (float): frame range\n\n    Returns:\n        otio._ot._ot.TimeRange: created range\n\n    \"\"\"\n    return _ot.TimeRange(\n        _ot.RationalTime(start, fps),\n        _ot.RationalTime(duration, fps)\n    )\n\n\ndef frames_to_seconds(frames, framerate):\n    \"\"\"\n    Returning seconds.\n\n    Args:\n        frames (int): frame\n        framerate (float): frame rate\n\n    Returns:\n        float: second value\n    \"\"\"\n\n    rt = _ot.from_frames(frames, framerate)\n    return _ot.to_seconds(rt)\n\n\ndef frames_to_timecode(frames, framerate):\n    rt = _ot.from_frames(frames, framerate)\n    return _ot.to_timecode(rt)\n\n\ndef make_sequence_collection(path, otio_range, metadata):\n    \"\"\"\n    Make collection from path otio range and otio metadata.\n\n    Args:\n        path (str): path to image sequence with `%d`\n        otio_range (otio._ot._ot.TimeRange): range to be used\n        metadata (dict): data where padding value can be found\n\n    Returns:\n        list: dir_path (str): path to sequence, collection object\n\n    \"\"\"\n    if \"%\" not in path:\n        return None\n    file_name = os.path.basename(path)\n    dir_path = os.path.dirname(path)\n    head = file_name.split(\"%\")[0]\n    tail = os.path.splitext(file_name)[-1]\n    first, last = otio_range_to_frame_range(otio_range)\n    collection = clique.Collection(\n        head=head, tail=tail, padding=metadata[\"padding\"])\n    collection.indexes.update([i for i in range(first, last)])\n    return dir_path, collection\n\n\ndef _sequence_resize(source, length):\n    step = float(len(source) - 1) / (length - 1)\n    for i in range(length):\n        low, ratio = divmod(i * step, 1)\n        high = low + 1 if ratio > 0 else low\n        yield (1 - ratio) * source[int(low)] + ratio * source[int(high)]\n\n\ndef get_media_range_with_retimes(otio_clip, handle_start, handle_end):\n    source_range = otio_clip.source_range\n    available_range = otio_clip.available_range()\n    media_in = available_range.start_time.value\n    media_out = available_range.end_time_inclusive().value\n\n    # modifiers\n    time_scalar = 1.\n    offset_in = 0\n    offset_out = 0\n    time_warp_nodes = []\n\n    # Check for speed effects and adjust playback speed accordingly\n    for effect in otio_clip.effects:\n        if isinstance(effect, otio.schema.LinearTimeWarp):\n            time_scalar = effect.time_scalar\n\n        elif isinstance(effect, otio.schema.FreezeFrame):\n            # For freeze frame, playback speed must be set after range\n            time_scalar = 0.\n\n        elif isinstance(effect, otio.schema.TimeEffect):\n            # For freeze frame, playback speed must be set after range\n            name = effect.name\n            effect_name = effect.effect_name\n            if \"TimeWarp\" not in effect_name:\n                continue\n            metadata = effect.metadata\n            lookup = metadata.get(\"lookup\")\n            if not lookup:\n                continue\n\n            # time warp node\n            tw_node = {\n                \"Class\": \"TimeWarp\",\n                \"name\": name\n            }\n            tw_node.update(metadata)\n            tw_node[\"lookup\"] = list(lookup)\n\n            # get first and last frame offsets\n            offset_in += lookup[0]\n            offset_out += lookup[-1]\n\n            # add to timewarp nodes\n            time_warp_nodes.append(tw_node)\n\n    # multiply by time scalar\n    offset_in *= time_scalar\n    offset_out *= time_scalar\n\n    # filip offset if reversed speed\n    if time_scalar < 0:\n        _offset_in = offset_out\n        _offset_out = offset_in\n        offset_in = _offset_in\n        offset_out = _offset_out\n\n    # scale handles\n    handle_start *= abs(time_scalar)\n    handle_end *= abs(time_scalar)\n\n    # filip handles if reversed speed\n    if time_scalar < 0:\n        _handle_start = handle_end\n        _handle_end = handle_start\n        handle_start = _handle_start\n        handle_end = _handle_end\n\n    source_in = source_range.start_time.value\n\n    media_in_trimmed = (\n        media_in + source_in + offset_in)\n    media_out_trimmed = (\n        media_in + source_in + (\n            ((source_range.duration.value - 1) * abs(\n                time_scalar)) + offset_out))\n\n    # calculate available handles\n    if (media_in_trimmed - media_in) < handle_start:\n        handle_start = (media_in_trimmed - media_in)\n    if (media_out - media_out_trimmed) < handle_end:\n        handle_end = (media_out - media_out_trimmed)\n\n    # create version data\n    version_data = {\n        \"versionData\": {\n            \"retime\": True,\n            \"speed\": time_scalar,\n            \"timewarps\": time_warp_nodes,\n            \"handleStart\": int(round(handle_start)),\n            \"handleEnd\": int(round(handle_end))\n        }\n    }\n\n    returning_dict = {\n        \"mediaIn\": media_in_trimmed,\n        \"mediaOut\": media_out_trimmed,\n        \"handleStart\": int(round(handle_start)),\n        \"handleEnd\": int(round(handle_end)),\n        \"speed\": time_scalar\n    }\n\n    # add version data only if retime\n    if time_warp_nodes or time_scalar != 1.:\n        returning_dict.update(version_data)\n\n    return returning_dict\n"
  },
  {
    "path": "openpype/pipeline/farm/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/pipeline/farm/patterning.py",
    "content": "# -*- coding: utf-8 -*-\nimport re\n\n\ndef match_aov_pattern(host_name, aov_patterns, render_file_name):\n    \"\"\"Matching against a `AOV` pattern in the render files.\n\n    In order to match the AOV name we must compare\n    against the render filename string that we are\n    grabbing the render filename string  from the collection\n    that we have grabbed from `exp_files`.\n\n    Args:\n        app (str): Host name.\n        aov_patterns (dict):  AOV patterns from AOV filters.\n        render_file_name (str): Incoming file name to match against.\n\n    Returns:\n        bool: Review state for rendered file (render_file_name).\n    \"\"\"\n    aov_pattern = aov_patterns.get(host_name, [])\n    if not aov_pattern:\n        return False\n    return any(re.match(p, render_file_name) for p in aov_pattern)\n"
  },
  {
    "path": "openpype/pipeline/farm/pyblish_functions.py",
    "content": "import copy\nimport attr\nimport pyblish.api\nimport os\nimport clique\nfrom copy import deepcopy\nimport re\nimport warnings\n\nfrom openpype.pipeline import (\n    get_current_project_name,\n    get_representation_path,\n    Anatomy,\n)\nfrom openpype.client import (\n    get_last_version_by_subset_name,\n    get_representations\n)\nfrom openpype.lib import Logger\nfrom openpype.pipeline.publish import KnownPublishError\nfrom openpype.pipeline.farm.patterning import match_aov_pattern\n\n\n@attr.s\nclass TimeData(object):\n    \"\"\"Structure used to handle time related data.\"\"\"\n    start = attr.ib(type=int)\n    end = attr.ib(type=int)\n    fps = attr.ib()\n    step = attr.ib(default=1, type=int)\n    handle_start = attr.ib(default=0, type=int)\n    handle_end = attr.ib(default=0, type=int)\n\n\ndef remap_source(path, anatomy):\n    \"\"\"Try to remap path to rootless path.\n\n    Args:\n        path (str): Path to be remapped to rootless.\n        anatomy (Anatomy): Anatomy object to handle remapping\n            itself.\n\n    Returns:\n        str: Remapped path.\n\n    Throws:\n        ValueError: if the root cannot be found.\n\n    \"\"\"\n    success, rootless_path = (\n        anatomy.find_root_template_from_path(path)\n    )\n    if success:\n        source = rootless_path\n    else:\n        raise ValueError(\n            \"Root from template path cannot be found: {}\".format(path))\n    return source\n\n\ndef extend_frames(asset, subset, start, end):\n    \"\"\"Get latest version of asset nad update frame range.\n\n    Based on minimum and maximum values.\n\n    Arguments:\n        asset (str): asset name\n        subset (str): subset name\n        start (int): start frame\n        end (int): end frame\n\n    Returns:\n        (int, int): update frame start/end\n\n    \"\"\"\n    # Frame comparison\n    prev_start = None\n    prev_end = None\n\n    project_name = get_current_project_name()\n    version = get_last_version_by_subset_name(\n        project_name,\n        subset,\n        asset_name=asset\n    )\n\n    # Set prev start / end frames for comparison\n    if not prev_start and not prev_end:\n        prev_start = version[\"data\"][\"frameStart\"]\n        prev_end = version[\"data\"][\"frameEnd\"]\n\n    updated_start = min(start, prev_start)\n    updated_end = max(end, prev_end)\n\n    return updated_start, updated_end\n\n\ndef get_time_data_from_instance_or_context(instance):\n    \"\"\"Get time data from instance (or context).\n\n    If time data is not found on instance, data from context will be used.\n\n    Args:\n        instance (pyblish.api.Instance): Source instance.\n\n    Returns:\n        TimeData: dataclass holding time information.\n\n    \"\"\"\n    context = instance.context\n    return TimeData(\n        start=instance.data.get(\"frameStart\", context.data.get(\"frameStart\")),\n        end=instance.data.get(\"frameEnd\", context.data.get(\"frameEnd\")),\n        fps=instance.data.get(\"fps\", context.data.get(\"fps\")),\n        step=instance.data.get(\"byFrameStep\", instance.data.get(\"step\", 1)),\n        handle_start=instance.data.get(\n            \"handleStart\", context.data.get(\"handleStart\")\n        ),\n        handle_end=instance.data.get(\n            \"handleEnd\", context.data.get(\"handleEnd\")\n        )\n    )\n\n\ndef get_transferable_representations(instance):\n    \"\"\"Transfer representations from original instance.\n\n    This will get all representations on the original instance that\n    are flagged with `publish_on_farm` and return them to be included\n    on skeleton instance if needed.\n\n    Args:\n        instance (pyblish.api.Instance): Original instance to be processed.\n\n    Return:\n        list of dicts: List of transferable representations.\n\n    \"\"\"\n    anatomy = instance.context.data[\"anatomy\"]  # type: Anatomy\n    to_transfer = []\n\n    for representation in instance.data.get(\"representations\", []):\n        if \"publish_on_farm\" not in representation.get(\"tags\", []):\n            continue\n\n        trans_rep = representation.copy()\n\n        # remove publish_on_farm from representations tags\n        trans_rep[\"tags\"].remove(\"publish_on_farm\")\n\n        staging_dir = trans_rep.get(\"stagingDir\")\n\n        if staging_dir:\n            try:\n                trans_rep[\"stagingDir\"] = remap_source(staging_dir, anatomy)\n            except ValueError:\n                log = Logger.get_logger(\"farm_publishing\")\n                log.warning(\n                    (\"Could not find root path for remapping \\\"{}\\\". \"\n                     \"This may cause issues on farm.\").format(staging_dir))\n\n        to_transfer.append(trans_rep)\n    return to_transfer\n\n\ndef create_skeleton_instance(\n        instance, families_transfer=None, instance_transfer=None):\n    # type: (pyblish.api.Instance, list, dict) -> dict\n    \"\"\"Create skeleton instance from original instance data.\n\n    This will create dictionary containing skeleton\n    - common - data used for publishing rendered instances.\n    This skeleton instance is then extended with additional data\n    and serialized to be processed by farm job.\n\n    Args:\n        instance (pyblish.api.Instance): Original instance to\n            be used as a source of data.\n        families_transfer (list): List of family names to transfer\n            from the original instance to the skeleton.\n        instance_transfer (dict): Dict with keys as families and\n            values as a list of property names to transfer to the\n            new skeleton.\n\n    Returns:\n        dict: Dictionary with skeleton instance data.\n\n    \"\"\"\n    # list of family names to transfer to new family if present\n\n    context = instance.context\n    data = instance.data.copy()\n    anatomy = instance.context.data[\"anatomy\"]  # type: Anatomy\n\n    # get time related data from instance (or context)\n    time_data = get_time_data_from_instance_or_context(instance)\n\n    if data.get(\"extendFrames\", False):\n        time_data.start, time_data.end = extend_frames(\n            data[\"asset\"],\n            data[\"subset\"],\n            time_data.start,\n            time_data.end,\n        )\n\n    source = data.get(\"source\") or context.data.get(\"currentFile\")\n    success, rootless_path = (\n        anatomy.find_root_template_from_path(source)\n    )\n    if success:\n        source = rootless_path\n    else:\n        # `rootless_path` is not set to `source` if none of roots match\n        log = Logger.get_logger(\"farm_publishing\")\n        log.warning((\"Could not find root path for remapping \\\"{}\\\". \"\n                     \"This may cause issues.\").format(source))\n\n    family = (\"render\"\n              if \"prerender.farm\" not in instance.data[\"families\"]\n              else \"prerender\")\n    families = [family]\n\n    # pass review to families if marked as review\n    if data.get(\"review\"):\n        families.append(\"review\")\n\n    instance_skeleton_data = {\n        \"family\": family,\n        \"subset\": data[\"subset\"],\n        \"families\": families,\n        \"asset\": data[\"asset\"],\n        \"frameStart\": time_data.start,\n        \"frameEnd\": time_data.end,\n        \"handleStart\": time_data.handle_start,\n        \"handleEnd\": time_data.handle_end,\n        \"frameStartHandle\": time_data.start - time_data.handle_start,\n        \"frameEndHandle\": time_data.end + time_data.handle_end,\n        \"comment\": data.get(\"comment\"),\n        \"fps\": time_data.fps,\n        \"source\": source,\n        \"extendFrames\": data.get(\"extendFrames\"),\n        \"overrideExistingFrame\": data.get(\"overrideExistingFrame\"),\n        \"pixelAspect\": data.get(\"pixelAspect\", 1),\n        \"resolutionWidth\": data.get(\"resolutionWidth\", 1920),\n        \"resolutionHeight\": data.get(\"resolutionHeight\", 1080),\n        \"multipartExr\": data.get(\"multipartExr\", False),\n        \"jobBatchName\": data.get(\"jobBatchName\", \"\"),\n        \"useSequenceForReview\": data.get(\"useSequenceForReview\", True),\n        # map inputVersions `ObjectId` -> `str` so json supports it\n        \"inputVersions\": list(map(str, data.get(\"inputVersions\", []))),\n        \"colorspace\": data.get(\"colorspace\")\n    }\n\n    # skip locking version if we are creating v01\n    instance_version = data.get(\"version\")  # take this if exists\n    if instance_version != 1:\n        instance_skeleton_data[\"version\"] = instance_version\n\n    # transfer specific families from original instance to new render\n    for item in families_transfer:\n        if item in instance.data.get(\"families\", []):\n            instance_skeleton_data[\"families\"] += [item]\n\n    # transfer specific properties from original instance based on\n    # mapping dictionary `instance_transfer`\n    for key, values in instance_transfer.items():\n        if key in instance.data.get(\"families\", []):\n            for v in values:\n                instance_skeleton_data[v] = instance.data.get(v)\n\n    representations = get_transferable_representations(instance)\n    instance_skeleton_data[\"representations\"] = representations\n\n    persistent = instance.data.get(\"stagingDir_persistent\") is True\n    instance_skeleton_data[\"stagingDir_persistent\"] = persistent\n\n    return instance_skeleton_data\n\n\ndef _add_review_families(families):\n    \"\"\"Adds review flag to families.\n\n    Handles situation when new instances are created which should have review\n    in families. In that case they should have 'ftrack' too.\n\n    TODO: This is ugly and needs to be refactored. Ftrack family should be\n          added in different way (based on if the module is enabled?)\n\n    \"\"\"\n    # if we have one representation with preview tag\n    # flag whole instance for review and for ftrack\n    if \"ftrack\" not in families and os.environ.get(\"FTRACK_SERVER\"):\n        families.append(\"ftrack\")\n    if \"review\" not in families:\n        families.append(\"review\")\n    return families\n\n\ndef prepare_representations(skeleton_data, exp_files, anatomy, aov_filter,\n                            skip_integration_repre_list,\n                            do_not_add_review,\n                            context,\n                            color_managed_plugin):\n    \"\"\"Create representations for file sequences.\n\n    This will return representations of expected files if they are not\n    in hierarchy of aovs. There should be only one sequence of files for\n    most cases, but if not - we create representation from each of them.\n\n    Arguments:\n        skeleton_data (dict): instance data for which we are\n                         setting representations\n        exp_files (list): list of expected files\n        anatomy (Anatomy):\n        aov_filter (dict): add review for specific aov names\n        skip_integration_repre_list (list): exclude specific extensions,\n        do_not_add_review (bool): explicitly skip review\n        color_managed_plugin (publish.ColormanagedPyblishPluginMixin)\n    Returns:\n        list of representations\n\n    \"\"\"\n    representations = []\n    host_name = os.environ.get(\"AVALON_APP\", \"\")\n    collections, remainders = clique.assemble(exp_files)\n\n    log = Logger.get_logger(\"farm_publishing\")\n\n    # create representation for every collected sequence\n    for collection in collections:\n        ext = collection.tail.lstrip(\".\")\n        preview = False\n        # TODO 'useSequenceForReview' is temporary solution which does\n        #   not work for 100% of cases. We must be able to tell what\n        #   expected files contains more explicitly and from what\n        #   should be review made.\n        # - \"review\" tag is never added when is set to 'False'\n        if skeleton_data[\"useSequenceForReview\"]:\n            # toggle preview on if multipart is on\n            if skeleton_data.get(\"multipartExr\", False):\n                log.debug(\n                    \"Adding preview tag because its multipartExr\"\n                )\n                preview = True\n            else:\n                render_file_name = list(collection)[0]\n                # if filtered aov name is found in filename, toggle it for\n                # preview video rendering\n                preview = match_aov_pattern(\n                    host_name, aov_filter, render_file_name\n                )\n\n        staging = os.path.dirname(list(collection)[0])\n        success, rootless_staging_dir = (\n            anatomy.find_root_template_from_path(staging)\n        )\n        if success:\n            staging = rootless_staging_dir\n        else:\n            log.warning((\n                \"Could not find root path for remapping \\\"{}\\\".\"\n                \" This may cause issues on farm.\"\n            ).format(staging))\n\n        frame_start = int(skeleton_data.get(\"frameStartHandle\"))\n        if skeleton_data.get(\"slate\"):\n            frame_start -= 1\n\n        # explicitly disable review by user\n        preview = preview and not do_not_add_review\n        rep = {\n            \"name\": ext,\n            \"ext\": ext,\n            \"files\": [os.path.basename(f) for f in list(collection)],\n            \"frameStart\": frame_start,\n            \"frameEnd\": int(skeleton_data.get(\"frameEndHandle\")),\n            # If expectedFile are absolute, we need only filenames\n            \"stagingDir\": staging,\n            \"fps\": skeleton_data.get(\"fps\"),\n            \"tags\": [\"review\"] if preview else [],\n        }\n\n        # poor man exclusion\n        if ext in skip_integration_repre_list:\n            rep[\"tags\"].append(\"delete\")\n\n        if skeleton_data.get(\"multipartExr\", False):\n            rep[\"tags\"].append(\"multipartExr\")\n\n        # support conversion from tiled to scanline\n        if skeleton_data.get(\"convertToScanline\"):\n            log.info(\"Adding scanline conversion.\")\n            rep[\"tags\"].append(\"toScanline\")\n\n        representations.append(rep)\n\n        if preview:\n            skeleton_data[\"families\"] = _add_review_families(\n                skeleton_data[\"families\"])\n\n    # add remainders as representations\n    for remainder in remainders:\n        ext = remainder.split(\".\")[-1]\n\n        staging = os.path.dirname(remainder)\n        success, rootless_staging_dir = (\n            anatomy.find_root_template_from_path(staging)\n        )\n        if success:\n            staging = rootless_staging_dir\n        else:\n            log.warning((\n                \"Could not find root path for remapping \\\"{}\\\".\"\n                \" This may cause issues on farm.\"\n            ).format(staging))\n\n        rep = {\n            \"name\": ext,\n            \"ext\": ext,\n            \"files\": os.path.basename(remainder),\n            \"stagingDir\": staging,\n        }\n\n        preview = match_aov_pattern(\n            host_name, aov_filter, remainder\n        )\n        preview = preview and not do_not_add_review\n        if preview:\n            rep.update({\n                \"fps\": skeleton_data.get(\"fps\"),\n                \"tags\": [\"review\"]\n            })\n            skeleton_data[\"families\"] = \\\n                _add_review_families(skeleton_data[\"families\"])\n\n        already_there = False\n        for repre in skeleton_data.get(\"representations\", []):\n            # might be added explicitly before by publish_on_farm\n            already_there = repre.get(\"files\") == rep[\"files\"]\n            if already_there:\n                log.debug(\"repre {} already_there\".format(repre))\n                break\n\n        if not already_there:\n            representations.append(rep)\n\n    for rep in representations:\n        # inject colorspace data\n        color_managed_plugin.set_representation_colorspace(\n            rep, context,\n            colorspace=skeleton_data[\"colorspace\"]\n        )\n\n    return representations\n\n\ndef create_instances_for_aov(instance, skeleton, aov_filter,\n                             skip_integration_repre_list,\n                             do_not_add_review):\n    \"\"\"Create instances from AOVs.\n\n    This will create new pyblish.api.Instances by going over expected\n    files defined on original instance.\n\n    Args:\n        instance (pyblish.api.Instance): Original instance.\n        skeleton (dict): Skeleton instance data.\n        skip_integration_repre_list (list): skip\n\n    Returns:\n        list of pyblish.api.Instance: Instances created from\n            expected files.\n\n    \"\"\"\n    # we cannot attach AOVs to other subsets as we consider every\n    # AOV subset of its own.\n\n    log = Logger.get_logger(\"farm_publishing\")\n    additional_color_data = {\n        \"renderProducts\": instance.data[\"renderProducts\"],\n        \"colorspaceConfig\": instance.data[\"colorspaceConfig\"],\n        \"display\": instance.data[\"colorspaceDisplay\"],\n        \"view\": instance.data[\"colorspaceView\"]\n    }\n\n    # Get templated path from absolute config path.\n    anatomy = instance.context.data[\"anatomy\"]\n    colorspace_template = instance.data[\"colorspaceConfig\"]\n    try:\n        additional_color_data[\"colorspaceTemplate\"] = remap_source(\n            colorspace_template, anatomy)\n    except ValueError as e:\n        log.warning(e)\n        additional_color_data[\"colorspaceTemplate\"] = colorspace_template\n\n    # if there are subset to attach to and more than one AOV,\n    # we cannot proceed.\n    if (\n        len(instance.data.get(\"attachTo\", [])) > 0\n        and len(instance.data.get(\"expectedFiles\")[0].keys()) != 1\n    ):\n        raise KnownPublishError(\n            \"attaching multiple AOVs or renderable cameras to \"\n            \"subset is not supported yet.\")\n\n    # create instances for every AOV we found in expected files.\n    # NOTE: this is done for every AOV and every render camera (if\n    #       there are multiple renderable cameras in scene)\n    return _create_instances_for_aov(\n        instance,\n        skeleton,\n        aov_filter,\n        additional_color_data,\n        skip_integration_repre_list,\n        do_not_add_review\n    )\n\n\ndef _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,\n                              skip_integration_repre_list, do_not_add_review):\n    \"\"\"Create instance for each AOV found.\n\n    This will create new instance for every AOV it can detect in expected\n    files list.\n\n    Args:\n        instance (pyblish.api.Instance): Original instance.\n        skeleton (dict): Skeleton data for instance (those needed) later\n            by collector.\n        additional_data (dict): ..\n        skip_integration_repre_list (list): list of extensions that shouldn't\n            be published\n        do_not_addbe _review (bool): explicitly disable review\n\n\n    Returns:\n        list of instances\n\n    Throws:\n        ValueError:\n\n    \"\"\"\n    # TODO: this needs to be taking the task from context or instance\n    task = os.environ[\"AVALON_TASK\"]\n\n    anatomy = instance.context.data[\"anatomy\"]\n    subset = skeleton[\"subset\"]\n    cameras = instance.data.get(\"cameras\", [])\n    exp_files = instance.data[\"expectedFiles\"]\n    log = Logger.get_logger(\"farm_publishing\")\n\n    instances = []\n    # go through AOVs in expected files\n    for aov, files in exp_files[0].items():\n        cols, rem = clique.assemble(files)\n        # we shouldn't have any reminders. And if we do, it should\n        # be just one item for single frame renders.\n        if not cols and rem:\n            if len(rem) != 1:\n                raise ValueError(\"Found multiple non related files \"\n                                 \"to render, don't know what to do \"\n                                 \"with them.\")\n            col = rem[0]\n            ext = os.path.splitext(col)[1].lstrip(\".\")\n        else:\n            # but we really expect only one collection.\n            # Nothing else make sense.\n            if len(cols) != 1:\n                raise ValueError(\"Only one image sequence type is expected.\")  # noqa: E501\n            ext = cols[0].tail.lstrip(\".\")\n            col = list(cols[0])\n\n        # create subset name `familyTaskSubset_AOV`\n        # TODO refactor/remove me\n        family = skeleton[\"family\"]\n        if not subset.startswith(family):\n            group_name = '{}{}{}{}{}'.format(\n                family,\n                task[0].upper(), task[1:],\n                subset[0].upper(), subset[1:])\n        else:\n            group_name = subset\n\n        # if there are multiple cameras, we need to add camera name\n        expected_filepath = col[0] if isinstance(col, (list, tuple)) else col\n        cams = [cam for cam in cameras if cam in expected_filepath]\n        if cams:\n            for cam in cams:\n                if aov:\n                    if not aov.startswith(cam):\n                        subset_name = '{}_{}_{}'.format(group_name, cam, aov)\n                    else:\n                        subset_name = \"{}_{}\".format(group_name, aov)\n                else:\n                    subset_name = '{}_{}'.format(group_name, cam)\n        else:\n            if aov:\n                subset_name = '{}_{}'.format(group_name, aov)\n            else:\n                subset_name = '{}'.format(group_name)\n\n        if isinstance(col, (list, tuple)):\n            staging = os.path.dirname(col[0])\n        else:\n            staging = os.path.dirname(col)\n\n        try:\n            staging = remap_source(staging, anatomy)\n        except ValueError as e:\n            log.warning(e)\n\n        log.info(\"Creating data for: {}\".format(subset_name))\n\n        app = os.environ.get(\"AVALON_APP\", \"\")\n\n        if isinstance(col, list):\n            render_file_name = os.path.basename(col[0])\n        else:\n            render_file_name = os.path.basename(col)\n        aov_patterns = aov_filter\n\n        preview = match_aov_pattern(app, aov_patterns, render_file_name)\n\n        new_instance = deepcopy(skeleton)\n        new_instance[\"subset\"] = subset_name\n        new_instance[\"subsetGroup\"] = group_name\n\n        # toggle preview on if multipart is on\n        # Because we cant query the multipartExr data member of each AOV we'll\n        # need to have hardcoded rule of excluding any renders with\n        # \"cryptomatte\" in the file name from being a multipart EXR. This issue\n        # happens with Redshift that forces Cryptomatte renders to be separate\n        # files even when the rest of the AOVs are merged into a single EXR.\n        # There might be an edge case where the main instance has cryptomatte\n        # in the name even though it's a multipart EXR.\n        if instance.data.get(\"renderer\") == \"redshift\":\n            if (\n                instance.data.get(\"multipartExr\") and\n                \"cryptomatte\" not in render_file_name.lower()\n            ):\n                log.debug(\"Adding preview tag because it's multipartExr\")\n                preview = True\n            else:\n                new_instance[\"multipartExr\"] = False\n        elif instance.data.get(\"multipartExr\"):\n            log.debug(\"Adding preview tag because its multipartExr\")\n            preview = True\n\n        # explicitly disable review by user\n        preview = preview and not do_not_add_review\n        if preview:\n            new_instance[\"review\"] = True\n\n        # create representation\n        if isinstance(col, (list, tuple)):\n            files = [os.path.basename(f) for f in col]\n        else:\n            files = os.path.basename(col)\n\n        # Copy render product \"colorspace\" data to representation.\n        colorspace = \"\"\n        products = additional_data[\"renderProducts\"].layer_data.products\n        for product in products:\n            if product.productName == aov:\n                colorspace = product.colorspace\n                break\n\n        rep = {\n            \"name\": ext,\n            \"ext\": ext,\n            \"files\": files,\n            \"frameStart\": int(skeleton[\"frameStartHandle\"]),\n            \"frameEnd\": int(skeleton[\"frameEndHandle\"]),\n            # If expectedFile are absolute, we need only filenames\n            \"stagingDir\": staging,\n            \"fps\": new_instance.get(\"fps\"),\n            \"tags\": [\"review\"] if preview else [],\n            \"colorspaceData\": {\n                \"colorspace\": colorspace,\n                \"config\": {\n                    \"path\": additional_data[\"colorspaceConfig\"],\n                    \"template\": additional_data[\"colorspaceTemplate\"]\n                },\n                \"display\": additional_data[\"display\"],\n                \"view\": additional_data[\"view\"]\n            }\n        }\n\n        # support conversion from tiled to scanline\n        if instance.data.get(\"convertToScanline\"):\n            log.info(\"Adding scanline conversion.\")\n            rep[\"tags\"].append(\"toScanline\")\n\n        # poor man exclusion\n        if ext in skip_integration_repre_list:\n            rep[\"tags\"].append(\"delete\")\n\n        if preview:\n            new_instance[\"families\"] = _add_review_families(\n                new_instance[\"families\"])\n\n        new_instance[\"representations\"] = [rep]\n\n        # if extending frames from existing version, copy files from there\n        # into our destination directory\n        if new_instance.get(\"extendFrames\", False):\n            copy_extend_frames(new_instance, rep)\n        instances.append(new_instance)\n        log.debug(\"instances:{}\".format(instances))\n    return instances\n\n\ndef get_resources(project_name, version, extension=None):\n    \"\"\"Get the files from the specific version.\n\n    This will return all get all files from representation.\n\n    Todo:\n        This is really weird function, and it's use is\n        highly controversial. First, it will not probably work\n        ar all in final release of AYON, second, the logic isn't sound.\n        It should try to find representation matching the current one -\n        because it is used to pull out files from previous version to\n        be included in this one.\n\n    .. deprecated:: 3.15.5\n       This won't work in AYON and even the logic must be refactored.\n\n    Args:\n        project_name (str): Name of the project.\n        version (dict): Version document.\n        extension (str): extension used to filter\n            representations.\n\n    Returns:\n        list: of files\n\n    \"\"\"\n    warnings.warn((\n        \"This won't work in AYON and even \"\n        \"the logic must be refactored.\"), DeprecationWarning)\n    extensions = []\n    if extension:\n        extensions = [extension]\n\n    # there is a `context_filter` argument that won't probably work in\n    # final release of AYON. SO we'll rather not use it\n    repre_docs = list(get_representations(\n        project_name, version_ids=[version[\"_id\"]]))\n\n    filtered = []\n    for doc in repre_docs:\n        if doc[\"context\"][\"ext\"] in extensions:\n            filtered.append(doc)\n\n    representation = filtered[0]\n    directory = get_representation_path(representation)\n    print(\"Source: \", directory)\n    resources = sorted(\n        [\n            os.path.normpath(os.path.join(directory, file_name))\n            for file_name in os.listdir(directory)\n        ]\n    )\n\n    return resources\n\n\ndef create_skeleton_instance_cache(instance):\n    # type: (pyblish.api.Instance, list, dict) -> dict\n    \"\"\"Create skeleton instance from original instance data.\n\n    This will create dictionary containing skeleton\n    - common - data used for publishing rendered instances.\n    This skeleton instance is then extended with additional data\n    and serialized to be processed by farm job.\n\n    Args:\n        instance (pyblish.api.Instance): Original instance to\n            be used as a source of data.\n\n    Returns:\n        dict: Dictionary with skeleton instance data.\n\n    \"\"\"\n    # list of family names to transfer to new family if present\n\n    context = instance.context\n    data = instance.data.copy()\n    anatomy = instance.context.data[\"anatomy\"]  # type: Anatomy\n\n    # get time related data from instance (or context)\n    time_data = get_time_data_from_instance_or_context(instance)\n\n    if data.get(\"extendFrames\", False):\n        time_data.start, time_data.end = extend_frames(\n            data[\"asset\"],\n            data[\"subset\"],\n            time_data.start,\n            time_data.end,\n        )\n\n    source = data.get(\"source\") or context.data.get(\"currentFile\")\n    success, rootless_path = (\n        anatomy.find_root_template_from_path(source)\n    )\n    if success:\n        source = rootless_path\n    else:\n        # `rootless_path` is not set to `source` if none of roots match\n        log = Logger.get_logger(\"farm_publishing\")\n        log.warning((\"Could not find root path for remapping \\\"{}\\\". \"\n                     \"This may cause issues.\").format(source))\n\n    family = instance.data[\"family\"]\n    # Make sure \"render\" is in the families to go through\n    # validating expected and rendered files\n    # during publishing job.\n    families = [\"render\", family]\n\n    instance_skeleton_data = {\n        \"family\": family,\n        \"subset\": data[\"subset\"],\n        \"families\": families,\n        \"asset\": data[\"asset\"],\n        \"frameStart\": time_data.start,\n        \"frameEnd\": time_data.end,\n        \"handleStart\": time_data.handle_start,\n        \"handleEnd\": time_data.handle_end,\n        \"frameStartHandle\": time_data.start - time_data.handle_start,\n        \"frameEndHandle\": time_data.end + time_data.handle_end,\n        \"comment\": data.get(\"comment\"),\n        \"fps\": time_data.fps,\n        \"source\": source,\n        \"extendFrames\": data.get(\"extendFrames\"),\n        \"overrideExistingFrame\": data.get(\"overrideExistingFrame\"),\n        \"jobBatchName\": data.get(\"jobBatchName\", \"\"),\n        # map inputVersions `ObjectId` -> `str` so json supports it\n        \"inputVersions\": list(map(str, data.get(\"inputVersions\", []))),\n    }\n\n    # skip locking version if we are creating v01\n    instance_version = data.get(\"version\")  # take this if exists\n    if instance_version != 1:\n        instance_skeleton_data[\"version\"] = instance_version\n\n    representations = get_transferable_representations(instance)\n    instance_skeleton_data[\"representations\"] = representations\n\n    persistent = instance.data.get(\"stagingDir_persistent\") is True\n    instance_skeleton_data[\"stagingDir_persistent\"] = persistent\n\n    return instance_skeleton_data\n\n\ndef prepare_cache_representations(skeleton_data, exp_files, anatomy):\n    \"\"\"Create representations for file sequences.\n\n    This will return representations of expected files if they are not\n    in hierarchy of aovs. There should be only one sequence of files for\n    most cases, but if not - we create representation from each of them.\n\n    Arguments:\n        skeleton_data (dict): instance data for which we are\n                         setting representations\n        exp_files (list): list of expected files\n        anatomy (Anatomy)\n    Returns:\n        list of representations\n\n    \"\"\"\n    representations = []\n    collections, remainders = clique.assemble(exp_files)\n\n    log = Logger.get_logger(\"farm_publishing\")\n\n    # create representation for every collected sequence\n    for collection in collections:\n        ext = collection.tail.lstrip(\".\")\n\n        staging = os.path.dirname(list(collection)[0])\n        success, rootless_staging_dir = (\n            anatomy.find_root_template_from_path(staging)\n        )\n        if success:\n            staging = rootless_staging_dir\n        else:\n            log.warning((\n                \"Could not find root path for remapping \\\"{}\\\".\"\n                \" This may cause issues on farm.\"\n            ).format(staging))\n\n        frame_start = int(skeleton_data.get(\"frameStartHandle\"))\n        rep = {\n            \"name\": ext,\n            \"ext\": ext,\n            \"files\": [os.path.basename(f) for f in list(collection)],\n            \"frameStart\": frame_start,\n            \"frameEnd\": int(skeleton_data.get(\"frameEndHandle\")),\n            # If expectedFile are absolute, we need only filenames\n            \"stagingDir\": staging,\n            \"fps\": skeleton_data.get(\"fps\")\n        }\n\n        representations.append(rep)\n\n    return representations\n\n\ndef create_instances_for_cache(instance, skeleton):\n    \"\"\"Create instance for cache.\n\n    This will create new instance for every AOV it can detect in expected\n    files list.\n\n    Args:\n        instance (pyblish.api.Instance): Original instance.\n        skeleton (dict): Skeleton data for instance (those needed) later\n            by collector.\n\n\n    Returns:\n        list of instances\n\n    Throws:\n        ValueError:\n\n    \"\"\"\n    anatomy = instance.context.data[\"anatomy\"]\n    subset = skeleton[\"subset\"]\n    family = skeleton[\"family\"]\n    exp_files = instance.data[\"expectedFiles\"]\n    log = Logger.get_logger(\"farm_publishing\")\n\n    instances = []\n    # go through AOVs in expected files\n    for _, files in exp_files[0].items():\n        cols, rem = clique.assemble(files)\n        # we shouldn't have any reminders. And if we do, it should\n        # be just one item for single frame renders.\n        if not cols and rem:\n            if len(rem) != 1:\n                raise ValueError(\"Found multiple non related files \"\n                                 \"to render, don't know what to do \"\n                                 \"with them.\")\n            col = rem[0]\n            ext = os.path.splitext(col)[1].lstrip(\".\")\n        else:\n            # but we really expect only one collection.\n            # Nothing else make sense.\n            if len(cols) != 1:\n                raise ValueError(\"Only one image sequence type is expected.\")  # noqa: E501\n            ext = cols[0].tail.lstrip(\".\")\n            col = list(cols[0])\n\n        if isinstance(col, (list, tuple)):\n            staging = os.path.dirname(col[0])\n        else:\n            staging = os.path.dirname(col)\n\n        try:\n            staging = remap_source(staging, anatomy)\n        except ValueError as e:\n            log.warning(e)\n\n        new_instance = deepcopy(skeleton)\n\n        new_instance[\"subset\"] = subset\n        log.info(\"Creating data for: {}\".format(subset))\n        new_instance[\"family\"] = family\n        new_instance[\"families\"] = skeleton[\"families\"]\n        # create representation\n        if isinstance(col, (list, tuple)):\n            files = [os.path.basename(f) for f in col]\n        else:\n            files = os.path.basename(col)\n\n        rep = {\n            \"name\": ext,\n            \"ext\": ext,\n            \"files\": files,\n            \"frameStart\": int(skeleton[\"frameStartHandle\"]),\n            \"frameEnd\": int(skeleton[\"frameEndHandle\"]),\n            # If expectedFile are absolute, we need only filenames\n            \"stagingDir\": staging,\n            \"fps\": new_instance.get(\"fps\"),\n            \"tags\": [],\n        }\n\n        new_instance[\"representations\"] = [rep]\n\n        # if extending frames from existing version, copy files from there\n        # into our destination directory\n        if new_instance.get(\"extendFrames\", False):\n            copy_extend_frames(new_instance, rep)\n        instances.append(new_instance)\n        log.debug(\"instances:{}\".format(instances))\n    return instances\n\n\ndef copy_extend_frames(instance, representation):\n    \"\"\"Copy existing frames from latest version.\n\n    This will copy all existing frames from subset's latest version back\n    to render directory and rename them to what renderer is expecting.\n\n    Arguments:\n        instance (pyblish.plugin.Instance): instance to get required\n            data from\n        representation (dict): presentation to operate on\n\n    \"\"\"\n    import speedcopy\n\n    R_FRAME_NUMBER = re.compile(\n        r\".+\\.(?P<frame>[0-9]+)\\..+\")\n\n    log = Logger.get_logger(\"farm_publishing\")\n    log.info(\"Preparing to copy ...\")\n    start = instance.data.get(\"frameStart\")\n    end = instance.data.get(\"frameEnd\")\n    project_name = instance.context.data[\"project\"]\n    anatomy = instance.context.data[\"anatomy\"]  # type: Anatomy\n\n    # get latest version of subset\n    # this will stop if subset wasn't published yet\n\n    version = get_last_version_by_subset_name(\n        project_name,\n        instance.data.get(\"subset\"),\n        asset_name=instance.data.get(\"asset\")\n    )\n\n    # get its files based on extension\n    subset_resources = get_resources(\n        project_name, version, representation.get(\"ext\")\n    )\n    r_col, _ = clique.assemble(subset_resources)\n\n    # if override remove all frames we are expecting to be rendered,\n    # so we'll copy only those missing from current render\n    if instance.data.get(\"overrideExistingFrame\"):\n        for frame in range(start, end + 1):\n            if frame not in r_col.indexes:\n                continue\n            r_col.indexes.remove(frame)\n\n    # now we need to translate published names from representation\n    # back. This is tricky, right now we'll just use same naming\n    # and only switch frame numbers\n    resource_files = []\n    r_filename = os.path.basename(\n        representation.get(\"files\")[0])  # first file\n    op = re.search(R_FRAME_NUMBER, r_filename)\n    pre = r_filename[:op.start(\"frame\")]\n    post = r_filename[op.end(\"frame\"):]\n    assert op is not None, \"padding string wasn't found\"\n    for frame in list(r_col):\n        fn = re.search(R_FRAME_NUMBER, frame)\n        # silencing linter as we need to compare to True, not to\n        # type\n        assert fn is not None, \"padding string wasn't found\"\n        # list of tuples (source, destination)\n        staging = representation.get(\"stagingDir\")\n        staging = anatomy.fill_root(staging)\n        resource_files.append(\n            (frame, os.path.join(\n                staging, \"{}{}{}\".format(pre, fn[\"frame\"], post)))\n        )\n\n    # test if destination dir exists and create it if not\n    output_dir = os.path.dirname(representation.get(\"files\")[0])\n    if not os.path.isdir(output_dir):\n        os.makedirs(output_dir)\n\n    # copy files\n    for source in resource_files:\n        speedcopy.copy(source[0], source[1])\n        log.info(\"  > {}\".format(source[1]))\n\n    log.info(\"Finished copying %i files\" % len(resource_files))\n\n\ndef attach_instances_to_subset(attach_to, instances):\n    \"\"\"Attach instance to subset.\n\n    If we are attaching to other subsets, create copy of existing\n    instances, change data to match its subset and replace\n    existing instances with modified data.\n\n    Args:\n        attach_to (list): List of instances to attach to.\n        instances (list): List of instances to attach.\n\n    Returns:\n          list: List of attached instances.\n\n    \"\"\"\n    new_instances = []\n    for attach_instance in attach_to:\n        for i in instances:\n            new_inst = copy.deepcopy(i)\n            new_inst[\"version\"] = attach_instance.get(\"version\")\n            new_inst[\"subset\"] = attach_instance.get(\"subset\")\n            new_inst[\"family\"] = attach_instance.get(\"family\")\n            new_inst[\"append\"] = True\n            # don't set subsetGroup if we are attaching\n            new_inst.pop(\"subsetGroup\")\n            new_instances.append(new_inst)\n    return new_instances\n\n\ndef create_metadata_path(instance, anatomy):\n    ins_data = instance.data\n    # Ensure output dir exists\n    output_dir = ins_data.get(\n        \"publishRenderMetadataFolder\", ins_data[\"outputDir\"])\n\n    log = Logger.get_logger(\"farm_publishing\")\n\n    try:\n        if not os.path.isdir(output_dir):\n            os.makedirs(output_dir)\n    except OSError:\n        # directory is not available\n        log.warning(\"Path is unreachable: `{}`\".format(output_dir))\n\n    metadata_filename = \"{}_metadata.json\".format(ins_data[\"subset\"])\n\n    metadata_path = os.path.join(output_dir, metadata_filename)\n\n    # Convert output dir to `{root}/rest/of/path/...` with Anatomy\n    success, rootless_mtdt_p = anatomy.find_root_template_from_path(\n        metadata_path)\n    if not success:\n        # `rootless_path` is not set to `output_dir` if none of roots match\n        log.warning((\n            \"Could not find root path for remapping \\\"{}\\\".\"\n            \" This may cause issues on farm.\"\n        ).format(output_dir))\n        rootless_mtdt_p = metadata_path\n\n    return metadata_path, rootless_mtdt_p\n"
  },
  {
    "path": "openpype/pipeline/farm/pyblish_functions.pyi",
    "content": "import pyblish.api\nfrom openpype.pipeline import Anatomy\nfrom typing import Tuple, Union, List\n\n\nclass TimeData:\n    start: int\n    end: int\n    fps: float | int\n    step: int\n    handle_start: int\n    handle_end: int\n\n    def __init__(self, start: int, end: int, fps: float | int, step: int, handle_start: int, handle_end: int):\n        ...\n    ...\n\ndef remap_source(source: str, anatomy: Anatomy): ...\ndef extend_frames(asset: str, subset: str, start: int, end: int) -> Tuple[int, int]: ...\ndef get_time_data_from_instance_or_context(instance: pyblish.api.Instance) -> TimeData: ...\ndef get_transferable_representations(instance: pyblish.api.Instance) -> list: ...\ndef create_skeleton_instance(instance: pyblish.api.Instance, families_transfer: list = ..., instance_transfer: dict = ...) -> dict: ...\ndef create_instances_for_aov(instance: pyblish.api.Instance, skeleton: dict, aov_filter: dict) -> List[pyblish.api.Instance]: ...\ndef attach_instances_to_subset(attach_to: list, instances: list) -> list: ...\n"
  },
  {
    "path": "openpype/pipeline/farm/tools.py",
    "content": "import os\n\n\ndef get_published_workfile_instance(context):\n    \"\"\"Find workfile instance in context\"\"\"\n    for i in context:\n        is_workfile = (\n            \"workfile\" in i.data.get(\"families\", []) or\n            i.data[\"family\"] == \"workfile\"\n        )\n        if not is_workfile:\n            continue\n\n        # test if there is instance of workfile waiting\n        # to be published.\n        if i.data[\"publish\"] is not True:\n            continue\n\n        return i\n\n\ndef from_published_scene(instance, replace_in_path=True):\n    \"\"\"Switch work scene for published scene.\n\n    If rendering/exporting from published scenes is enabled, this will\n    replace paths from working scene to published scene.\n\n    Args:\n        instance (pyblish.api.Instance): Instance data to process.\n        replace_in_path (bool): if True, it will try to find\n            old scene name in path of expected files and replace it\n            with name of published scene.\n\n    Returns:\n        str: Published scene path.\n        None: if no published scene is found.\n\n    Note:\n        Published scene path is actually determined from project Anatomy\n        as at the time this plugin is running the scene can be still\n        un-published.\n\n    \"\"\"\n    workfile_instance = get_published_workfile_instance(instance.context)\n    if workfile_instance is None:\n        return\n\n    # determine published path from Anatomy.\n    template_data = workfile_instance.data.get(\"anatomyData\")\n    rep = workfile_instance.data[\"representations\"][0]\n    template_data[\"representation\"] = rep.get(\"name\")\n    template_data[\"ext\"] = rep.get(\"ext\")\n    template_data[\"comment\"] = None\n\n    anatomy = instance.context.data['anatomy']\n    template_obj = anatomy.templates_obj[\"publish\"][\"path\"]\n    template_filled = template_obj.format_strict(template_data)\n    file_path = os.path.normpath(template_filled)\n\n    if not os.path.exists(file_path):\n        raise\n\n    if not replace_in_path:\n        return file_path\n\n    # now we need to switch scene in expected files\n    # because <scene> token will now point to published\n    # scene file and that might differ from current one\n    def _clean_name(path):\n        return os.path.splitext(os.path.basename(path))[0]\n\n    new_scene = _clean_name(file_path)\n    orig_scene = _clean_name(instance.context.data[\"currentFile\"])\n    expected_files = instance.data.get(\"expectedFiles\")\n\n    if isinstance(expected_files[0], dict):\n        # we have aovs and we need to iterate over them\n        new_exp = {}\n        for aov, files in expected_files[0].items():\n            replaced_files = []\n            for f in files:\n                replaced_files.append(\n                    str(f).replace(orig_scene, new_scene)\n                )\n            new_exp[aov] = replaced_files\n        # [] might be too much here, TODO\n        instance.data[\"expectedFiles\"] = [new_exp]\n    else:\n        new_exp = []\n        for f in expected_files:\n            new_exp.append(\n                str(f).replace(orig_scene, new_scene)\n            )\n        instance.data[\"expectedFiles\"] = new_exp\n\n    metadata_folder = instance.data.get(\"publishRenderMetadataFolder\")\n    if metadata_folder:\n        metadata_folder = metadata_folder.replace(orig_scene,\n                                                  new_scene)\n        instance.data[\"publishRenderMetadataFolder\"] = metadata_folder\n\n    return file_path\n\n\ndef iter_expected_files(exp):\n    if isinstance(exp[0], dict):\n        for _aov, files in exp[0].items():\n            for file in files:\n                yield file\n    else:\n        for file in exp:\n            yield file\n"
  },
  {
    "path": "openpype/pipeline/legacy_io.py",
    "content": "\"\"\"Wrapper around interactions with the database\"\"\"\n\nimport sys\nimport logging\nimport functools\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom . import schema\nfrom .mongodb import AvalonMongoDB, session_data_from_environment\n\nmodule = sys.modules[__name__]\n\nSession = {}\n_is_installed = False\n_connection_object = AvalonMongoDB(Session)\n_mongo_client = None\n_database = database = None\n\nlog = logging.getLogger(__name__)\n\n\ndef is_installed():\n    return module._is_installed\n\n\ndef install():\n    \"\"\"Establish a persistent connection to the database\"\"\"\n    if is_installed():\n        return\n\n    session = session_data_from_environment(context_keys=True)\n\n    session[\"schema\"] = \"openpype:session-4.0\"\n    try:\n        schema.validate(session)\n    except schema.ValidationError as e:\n        # TODO(marcus): Make this mandatory\n        log.warning(e)\n\n    _connection_object.Session.update(session)\n    _connection_object.install()\n\n    if not AYON_SERVER_ENABLED:\n        module._mongo_client = _connection_object.mongo_client\n        module._database = module.database = _connection_object.database\n\n    module._is_installed = True\n\n\ndef uninstall():\n    \"\"\"Close any connection to the database\"\"\"\n    module._mongo_client = None\n    module._database = module.database = None\n    module._is_installed = False\n    try:\n        module._connection_object.uninstall()\n    except AttributeError:\n        pass\n\n\ndef requires_install(func):\n    @functools.wraps(func)\n    def decorated(*args, **kwargs):\n        if not is_installed():\n            install()\n        return func(*args, **kwargs)\n    return decorated\n\n\n@requires_install\ndef projects(*args, **kwargs):\n    return _connection_object.projects(*args, **kwargs)\n\n\n@requires_install\ndef insert_one(doc, *args, **kwargs):\n    return _connection_object.insert_one(doc, *args, **kwargs)\n\n\n@requires_install\ndef insert_many(docs, *args, **kwargs):\n    return _connection_object.insert_many(docs, *args, **kwargs)\n\n\n@requires_install\ndef update_one(*args, **kwargs):\n    return _connection_object.update_one(*args, **kwargs)\n\n\n@requires_install\ndef update_many(*args, **kwargs):\n    return _connection_object.update_many(*args, **kwargs)\n\n\n@requires_install\ndef replace_one(*args, **kwargs):\n    return _connection_object.replace_one(*args, **kwargs)\n\n\n@requires_install\ndef replace_many(*args, **kwargs):\n    return _connection_object.replace_many(*args, **kwargs)\n\n\n@requires_install\ndef delete_one(*args, **kwargs):\n    return _connection_object.delete_one(*args, **kwargs)\n\n\n@requires_install\ndef delete_many(*args, **kwargs):\n    return _connection_object.delete_many(*args, **kwargs)\n\n\n@requires_install\ndef find(*args, **kwargs):\n    return _connection_object.find(*args, **kwargs)\n\n\n@requires_install\ndef find_one(*args, **kwargs):\n    return _connection_object.find_one(*args, **kwargs)\n\n\n@requires_install\ndef distinct(*args, **kwargs):\n    return _connection_object.distinct(*args, **kwargs)\n\n\n@requires_install\ndef aggregate(*args, **kwargs):\n    return _connection_object.aggregate(*args, **kwargs)\n\n\n@requires_install\ndef save(*args, **kwargs):\n    return _connection_object.save(*args, **kwargs)\n\n\n@requires_install\ndef drop(*args, **kwargs):\n    return _connection_object.drop(*args, **kwargs)\n\n\n@requires_install\ndef parenthood(*args, **kwargs):\n    return _connection_object.parenthood(*args, **kwargs)\n\n\n@requires_install\ndef bulk_write(*args, **kwargs):\n    return _connection_object.bulk_write(*args, **kwargs)\n\n\n@requires_install\ndef active_project(*args, **kwargs):\n    return _connection_object.active_project(*args, **kwargs)\n\n\ndef current_project(*args, **kwargs):\n    return Session.get(\"AVALON_PROJECT\")\n"
  },
  {
    "path": "openpype/pipeline/load/__init__.py",
    "content": "from .utils import (\n    HeroVersionType,\n\n    LoadError,\n    IncompatibleLoaderError,\n    InvalidRepresentationContext,\n    LoaderSwitchNotImplementedError,\n    LoaderNotFoundError,\n\n    get_repres_contexts,\n    get_contexts_for_repre_docs,\n    get_subset_contexts,\n    get_representation_context,\n\n    load_with_repre_context,\n    load_with_subset_context,\n    load_with_subset_contexts,\n\n    load_container,\n    remove_container,\n    update_container,\n    switch_container,\n\n    get_loader_identifier,\n    get_loaders_by_name,\n\n    get_representation_path_from_context,\n    get_representation_path,\n    get_representation_path_with_anatomy,\n\n    is_compatible_loader,\n\n    loaders_from_repre_context,\n    loaders_from_representation,\n    filter_repre_contexts_by_loader,\n\n    any_outdated_containers,\n    get_outdated_containers,\n    filter_containers,\n)\n\nfrom .plugins import (\n    LoaderPlugin,\n    SubsetLoaderPlugin,\n\n    discover_loader_plugins,\n    register_loader_plugin,\n    deregister_loader_plugin_path,\n    register_loader_plugin_path,\n    deregister_loader_plugin,\n)\n\n\n__all__ = (\n    # utils.py\n    \"HeroVersionType\",\n\n    \"LoadError\",\n    \"IncompatibleLoaderError\",\n    \"InvalidRepresentationContext\",\n    \"LoaderSwitchNotImplementedError\",\n    \"LoaderNotFoundError\",\n\n    \"get_repres_contexts\",\n    \"get_contexts_for_repre_docs\",\n    \"get_subset_contexts\",\n    \"get_representation_context\",\n\n    \"load_with_repre_context\",\n    \"load_with_subset_context\",\n    \"load_with_subset_contexts\",\n\n    \"load_container\",\n    \"remove_container\",\n    \"update_container\",\n    \"switch_container\",\n\n    \"get_loader_identifier\",\n    \"get_loaders_by_name\",\n\n    \"get_representation_path_from_context\",\n    \"get_representation_path\",\n    \"get_representation_path_with_anatomy\",\n\n    \"is_compatible_loader\",\n\n    \"loaders_from_repre_context\",\n    \"loaders_from_representation\",\n    \"filter_repre_contexts_by_loader\",\n\n    \"any_outdated_containers\",\n    \"get_outdated_containers\",\n    \"filter_containers\",\n\n    # plugins.py\n    \"LoaderPlugin\",\n    \"SubsetLoaderPlugin\",\n\n    \"discover_loader_plugins\",\n    \"register_loader_plugin\",\n    \"deregister_loader_plugin_path\",\n    \"register_loader_plugin_path\",\n    \"deregister_loader_plugin\",\n)\n"
  },
  {
    "path": "openpype/pipeline/load/plugins.py",
    "content": "import os\nimport logging\n\nfrom openpype.settings import get_system_settings, get_project_settings\nfrom openpype.pipeline import (\n    schema,\n    legacy_io,\n)\nfrom openpype.pipeline.plugin_discover import (\n    discover,\n    register_plugin,\n    register_plugin_path,\n    deregister_plugin,\n    deregister_plugin_path\n)\nfrom .utils import get_representation_path_from_context\n\n\nclass LoaderPlugin(list):\n    \"\"\"Load representation into host application\n\n    Arguments:\n        context (dict): avalon-core:context-1.0\n\n    .. versionadded:: 4.0\n       This class was introduced\n\n    \"\"\"\n\n    families = []\n    representations = []\n    extensions = {\"*\"}\n    order = 0\n    is_multiple_contexts_compatible = False\n    enabled = True\n\n    options = []\n\n    log = logging.getLogger(\"SubsetLoader\")\n    log.propagate = True\n\n    @classmethod\n    def apply_settings(cls, project_settings, system_settings):\n        host_name = os.environ.get(\"AVALON_APP\")\n        plugin_type = \"load\"\n        plugin_type_settings = (\n            project_settings\n            .get(host_name, {})\n            .get(plugin_type, {})\n        )\n        global_type_settings = (\n            project_settings\n            .get(\"global\", {})\n            .get(plugin_type, {})\n        )\n        if not global_type_settings and not plugin_type_settings:\n            return\n\n        plugin_name = cls.__name__\n\n        plugin_settings = None\n        # Look for plugin settings in host specific settings\n        if plugin_name in plugin_type_settings:\n            plugin_settings = plugin_type_settings[plugin_name]\n\n        # Look for plugin settings in global settings\n        elif plugin_name in global_type_settings:\n            plugin_settings = global_type_settings[plugin_name]\n\n        if not plugin_settings:\n            return\n\n        print(\">>> We have preset for {}\".format(plugin_name))\n        for option, value in plugin_settings.items():\n            if option == \"enabled\" and value is False:\n                print(\"  - is disabled by preset\")\n            else:\n                print(\"  - setting `{}`: `{}`\".format(option, value))\n            setattr(cls, option, value)\n\n    @classmethod\n    def has_valid_extension(cls, repre_doc):\n        \"\"\"Has representation document valid extension for loader.\n\n        Args:\n            repre_doc (dict[str, Any]): Representation document.\n\n        Returns:\n             bool: Representation has valid extension\n        \"\"\"\n\n        if \"*\" in cls.extensions:\n            return True\n\n        # Get representation main file extension from 'context'\n        repre_context = repre_doc.get(\"context\") or {}\n        ext = repre_context.get(\"ext\")\n        if not ext:\n            # Legacy way how to get extensions\n            path = repre_doc.get(\"data\", {}).get(\"path\")\n            if not path:\n                cls.log.info(\n                    \"Representation doesn't have known source of extension\"\n                    \" information.\"\n                )\n                return False\n\n            cls.log.debug(\"Using legacy source of extension from path.\")\n            ext = os.path.splitext(path)[-1].lstrip(\".\")\n\n        # If representation does not have extension then can't be valid\n        if not ext:\n            return False\n\n        valid_extensions_low = {ext.lower() for ext in cls.extensions}\n        return ext.lower() in valid_extensions_low\n\n    @classmethod\n    def is_compatible_loader(cls, context):\n        \"\"\"Return whether a loader is compatible with a context.\n\n        On override make sure it is overriden as class or static method.\n\n        This checks the version's families and the representation for the given\n        loader plugin.\n\n        Args:\n            context (dict[str, Any]): Documents of context for which should\n                be loader used.\n\n        Returns:\n            bool: Is loader compatible for context.\n        \"\"\"\n\n        plugin_repre_names = cls.get_representations()\n        plugin_families = cls.families\n        if (\n            not plugin_repre_names\n            or not plugin_families\n            or not cls.extensions\n        ):\n            return False\n\n        repre_doc = context.get(\"representation\")\n        if not repre_doc:\n            return False\n\n        plugin_repre_names = set(plugin_repre_names)\n        if (\n            \"*\" not in plugin_repre_names\n            and repre_doc[\"name\"] not in plugin_repre_names\n        ):\n            return False\n\n        if not cls.has_valid_extension(repre_doc):\n            return False\n\n        plugin_families = set(plugin_families)\n        if \"*\" in plugin_families:\n            return True\n\n        subset_doc = context[\"subset\"]\n        maj_version, _ = schema.get_schema_version(subset_doc[\"schema\"])\n        if maj_version < 3:\n            families = context[\"version\"][\"data\"].get(\"families\")\n        else:\n            families = subset_doc[\"data\"].get(\"families\")\n            if families is None:\n                family = subset_doc[\"data\"].get(\"family\")\n                if family:\n                    families = [family]\n\n        if not families:\n            return False\n        return any(family in plugin_families for family in families)\n\n    @classmethod\n    def get_representations(cls):\n        return cls.representations\n\n    @classmethod\n    def filepath_from_context(cls, context):\n        return get_representation_path_from_context(context)\n\n    def load(self, context, name=None, namespace=None, options=None):\n        \"\"\"Load asset via database\n\n        Arguments:\n            context (dict): Full parenthood of representation to load\n            name (str, optional): Use pre-defined name\n            namespace (str, optional): Use pre-defined namespace\n            options (dict, optional): Additional settings dictionary\n\n        \"\"\"\n        raise NotImplementedError(\"Loader.load() must be \"\n                                  \"implemented by subclass\")\n\n    def update(self, container, representation):\n        \"\"\"Update `container` to `representation`\n\n        Arguments:\n            container (avalon-core:container-1.0): Container to update,\n                from `host.ls()`.\n            representation (dict): Update the container to this representation.\n\n        \"\"\"\n        raise NotImplementedError(\"Loader.update() must be \"\n                                  \"implemented by subclass\")\n\n    def remove(self, container):\n        \"\"\"Remove a container\n\n        Arguments:\n            container (avalon-core:container-1.0): Container to remove,\n                from `host.ls()`.\n\n        Returns:\n            bool: Whether the container was deleted\n\n        \"\"\"\n\n        raise NotImplementedError(\"Loader.remove() must be \"\n                                  \"implemented by subclass\")\n\n    @classmethod\n    def get_options(cls, contexts):\n        \"\"\"\n            Returns static (cls) options or could collect from 'contexts'.\n\n            Args:\n                contexts (list): of repre or subset contexts\n            Returns:\n                (list)\n        \"\"\"\n        return cls.options or []\n\n    @property\n    def fname(self):\n        \"\"\"Backwards compatibility with deprecation warning\"\"\"\n\n        self.log.warning((\n            \"DEPRECATION WARNING: Source - Loader plugin {}.\"\n            \" The 'fname' property on the Loader plugin will be removed in\"\n            \" future versions of OpenPype. Planned version to drop the support\"\n            \" is 3.16.6 or 3.17.0.\"\n        ).format(self.__class__.__name__))\n        if hasattr(self, \"_fname\"):\n            return self._fname\n\n\nclass SubsetLoaderPlugin(LoaderPlugin):\n    \"\"\"Load subset into host application\n    Arguments:\n        context (dict): avalon-core:context-1.0\n        name (str, optional): Use pre-defined name\n        namespace (str, optional): Use pre-defined namespace\n    \"\"\"\n\n\ndef discover_loader_plugins(project_name=None):\n    from openpype.lib import Logger\n\n    log = Logger.get_logger(\"LoaderDiscover\")\n    plugins = discover(LoaderPlugin)\n    if not project_name:\n        project_name = legacy_io.active_project()\n    system_settings = get_system_settings()\n    project_settings = get_project_settings(project_name)\n    for plugin in plugins:\n        try:\n            plugin.apply_settings(project_settings, system_settings)\n        except Exception:\n            log.warning(\n                \"Failed to apply settings to loader {}\".format(\n                    plugin.__name__\n                ),\n                exc_info=True\n            )\n    return plugins\n\n\ndef register_loader_plugin(plugin):\n    return register_plugin(LoaderPlugin, plugin)\n\n\ndef deregister_loader_plugin(plugin):\n    deregister_plugin(LoaderPlugin, plugin)\n\n\ndef deregister_loader_plugin_path(path):\n    deregister_plugin_path(LoaderPlugin, path)\n\n\ndef register_loader_plugin_path(path):\n    return register_plugin_path(LoaderPlugin, path)\n"
  },
  {
    "path": "openpype/pipeline/load/utils.py",
    "content": "import os\nimport platform\nimport copy\nimport getpass\nimport logging\nimport inspect\nimport collections\nimport numbers\n\nfrom openpype.host import ILoadHost\nfrom openpype.client import (\n    get_project,\n    get_assets,\n    get_subsets,\n    get_versions,\n    get_version_by_id,\n    get_last_version_by_subset_id,\n    get_hero_version_by_subset_id,\n    get_version_by_name,\n    get_last_versions,\n    get_representations,\n    get_representation_by_id,\n    get_representation_by_name,\n    get_representation_parents\n)\nfrom openpype.lib import (\n    StringTemplate,\n    TemplateUnsolved,\n)\nfrom openpype.pipeline import (\n    legacy_io,\n    Anatomy,\n)\n\nlog = logging.getLogger(__name__)\n\nContainersFilterResult = collections.namedtuple(\n    \"ContainersFilterResult\",\n    [\"latest\", \"outdated\", \"not_found\", \"invalid\"]\n)\n\n\nclass HeroVersionType(object):\n    def __init__(self, version):\n        assert isinstance(version, numbers.Integral), (\n            \"Version is not an integer. \\\"{}\\\" {}\".format(\n                version, str(type(version))\n            )\n        )\n        self.version = version\n\n    def __str__(self):\n        return str(self.version)\n\n    def __int__(self):\n        return int(self.version)\n\n    def __format__(self, format_spec):\n        return self.version.__format__(format_spec)\n\n\nclass LoadError(Exception):\n    \"\"\"Known error that happened during loading.\n\n    A message is shown to user (without traceback). Make sure an artist can\n    understand the problem.\n    \"\"\"\n\n    pass\n\n\nclass IncompatibleLoaderError(ValueError):\n    \"\"\"Error when Loader is incompatible with a representation.\"\"\"\n    pass\n\n\nclass InvalidRepresentationContext(ValueError):\n    \"\"\"Representation path can't be received using representation document.\"\"\"\n    pass\n\n\nclass LoaderSwitchNotImplementedError(NotImplementedError):\n    \"\"\"Error when `switch` is used with Loader that has no implementation.\"\"\"\n    pass\n\n\nclass LoaderNotFoundError(RuntimeError):\n    \"\"\"Error when Loader plugin for a loader name is not found.\"\"\"\n    pass\n\n\ndef get_repres_contexts(representation_ids, dbcon=None):\n    \"\"\"Return parenthood context for representation.\n\n    Args:\n        representation_ids (list): The representation ids.\n        dbcon (AvalonMongoDB): Mongo connection object. `avalon.io` used when\n            not entered.\n\n    Returns:\n        dict: The full representation context by representation id.\n            keys are repre_id, value is dictionary with full documents of\n            asset, subset, version and representation.\n    \"\"\"\n\n    if not dbcon:\n        dbcon = legacy_io\n\n    if not representation_ids:\n        return {}\n\n    project_name = dbcon.active_project()\n    repre_docs = get_representations(project_name, representation_ids)\n\n    return get_contexts_for_repre_docs(project_name, repre_docs)\n\n\ndef get_contexts_for_repre_docs(project_name, repre_docs):\n    contexts = {}\n    if not repre_docs:\n        return contexts\n\n    repre_docs_by_id = {}\n    version_ids = set()\n    for repre_doc in repre_docs:\n        version_ids.add(repre_doc[\"parent\"])\n        repre_docs_by_id[repre_doc[\"_id\"]] = repre_doc\n\n    version_docs = get_versions(\n        project_name, version_ids, hero=True\n    )\n\n    version_docs_by_id = {}\n    hero_version_docs = []\n    versions_for_hero = set()\n    subset_ids = set()\n    for version_doc in version_docs:\n        if version_doc[\"type\"] == \"hero_version\":\n            hero_version_docs.append(version_doc)\n            versions_for_hero.add(version_doc[\"version_id\"])\n        version_docs_by_id[version_doc[\"_id\"]] = version_doc\n        subset_ids.add(version_doc[\"parent\"])\n\n    if versions_for_hero:\n        _version_docs = get_versions(project_name, versions_for_hero)\n        _version_data_by_id = {\n            version_doc[\"_id\"]: version_doc[\"data\"]\n            for version_doc in _version_docs\n        }\n\n        for hero_version_doc in hero_version_docs:\n            hero_version_id = hero_version_doc[\"_id\"]\n            version_id = hero_version_doc[\"version_id\"]\n            version_data = copy.deepcopy(_version_data_by_id[version_id])\n            version_docs_by_id[hero_version_id][\"data\"] = version_data\n\n    subset_docs = get_subsets(project_name, subset_ids)\n    subset_docs_by_id = {}\n    asset_ids = set()\n    for subset_doc in subset_docs:\n        subset_docs_by_id[subset_doc[\"_id\"]] = subset_doc\n        asset_ids.add(subset_doc[\"parent\"])\n\n    asset_docs = get_assets(project_name, asset_ids)\n    asset_docs_by_id = {\n        asset_doc[\"_id\"]: asset_doc\n        for asset_doc in asset_docs\n    }\n\n    project_doc = get_project(project_name)\n\n    for repre_id, repre_doc in repre_docs_by_id.items():\n        version_doc = version_docs_by_id[repre_doc[\"parent\"]]\n        subset_doc = subset_docs_by_id[version_doc[\"parent\"]]\n        asset_doc = asset_docs_by_id[subset_doc[\"parent\"]]\n        context = {\n            \"project\": {\n                \"name\": project_doc[\"name\"],\n                \"code\": project_doc[\"data\"].get(\"code\")\n            },\n            \"asset\": asset_doc,\n            \"subset\": subset_doc,\n            \"version\": version_doc,\n            \"representation\": repre_doc,\n        }\n        contexts[repre_id] = context\n\n    return contexts\n\n\ndef get_subset_contexts(subset_ids, dbcon=None):\n    \"\"\"Return parenthood context for subset.\n\n        Provides context on subset granularity - less detail than\n        'get_repre_contexts'.\n    Args:\n        subset_ids (list): The subset ids.\n        dbcon (AvalonMongoDB): Mongo connection object. `avalon.io` used when\n            not entered.\n    Returns:\n        dict: The full representation context by representation id.\n    \"\"\"\n    if not dbcon:\n        dbcon = legacy_io\n\n    contexts = {}\n    if not subset_ids:\n        return contexts\n\n    project_name = dbcon.active_project()\n    subset_docs = get_subsets(project_name, subset_ids)\n    subset_docs_by_id = {}\n    asset_ids = set()\n    for subset_doc in subset_docs:\n        subset_docs_by_id[subset_doc[\"_id\"]] = subset_doc\n        asset_ids.add(subset_doc[\"parent\"])\n\n    asset_docs = get_assets(project_name, asset_ids)\n    asset_docs_by_id = {\n        asset_doc[\"_id\"]: asset_doc\n        for asset_doc in asset_docs\n    }\n\n    project_doc = get_project(project_name)\n\n    for subset_id, subset_doc in subset_docs_by_id.items():\n        asset_doc = asset_docs_by_id[subset_doc[\"parent\"]]\n        context = {\n            \"project\": {\n                \"name\": project_doc[\"name\"],\n                \"code\": project_doc[\"data\"].get(\"code\")\n            },\n            \"asset\": asset_doc,\n            \"subset\": subset_doc\n        }\n        contexts[subset_id] = context\n\n    return contexts\n\n\ndef get_representation_context(representation):\n    \"\"\"Return parenthood context for representation.\n\n    Args:\n        representation (str or ObjectId or dict): The representation id\n            or full representation as returned by the database.\n\n    Returns:\n        dict: The full representation context.\n    \"\"\"\n\n    assert representation is not None, \"This is a bug\"\n\n    project_name = legacy_io.active_project()\n    if not isinstance(representation, dict):\n        representation = get_representation_by_id(\n            project_name, representation\n        )\n\n    if not representation:\n        raise AssertionError(\"Representation was not found in database\")\n\n    version, subset, asset, project = get_representation_parents(\n        project_name, representation\n    )\n    if not version:\n        raise AssertionError(\"Version was not found in database\")\n    if not subset:\n        raise AssertionError(\"Subset was not found in database\")\n    if not asset:\n        raise AssertionError(\"Asset was not found in database\")\n    if not project:\n        raise AssertionError(\"Project was not found in database\")\n\n    context = {\n        \"project\": {\n            \"name\": project[\"name\"],\n            \"code\": project[\"data\"].get(\"code\", '')\n        },\n        \"asset\": asset,\n        \"subset\": subset,\n        \"version\": version,\n        \"representation\": representation,\n    }\n\n    return context\n\n\ndef load_with_repre_context(\n    Loader, repre_context, namespace=None, name=None, options=None, **kwargs\n):\n\n    # Ensure the Loader is compatible for the representation\n    if not is_compatible_loader(Loader, repre_context):\n        raise IncompatibleLoaderError(\n            \"Loader {} is incompatible with {}\".format(\n                Loader.__name__, repre_context[\"subset\"][\"name\"]\n            )\n        )\n\n    # Ensure options is a dictionary when no explicit options provided\n    if options is None:\n        options = kwargs.get(\"data\", dict())  # \"data\" for backward compat\n\n    assert isinstance(options, dict), \"Options must be a dictionary\"\n\n    # Fallback to subset when name is None\n    if name is None:\n        name = repre_context[\"subset\"][\"name\"]\n\n    log.info(\n        \"Running '%s' on '%s'\" % (\n            Loader.__name__, repre_context[\"asset\"][\"name\"]\n        )\n    )\n\n    loader = Loader()\n\n    # Backwards compatibility: Originally the loader's __init__ required the\n    # representation context to set `fname` attribute to the filename to load\n    # Deprecated - to be removed in OpenPype 3.16.6 or 3.17.0.\n    loader._fname = get_representation_path_from_context(repre_context)\n\n    return loader.load(repre_context, name, namespace, options)\n\n\ndef load_with_subset_context(\n    Loader, subset_context, namespace=None, name=None, options=None, **kwargs\n):\n\n    # Ensure options is a dictionary when no explicit options provided\n    if options is None:\n        options = kwargs.get(\"data\", dict())  # \"data\" for backward compat\n\n    assert isinstance(options, dict), \"Options must be a dictionary\"\n\n    # Fallback to subset when name is None\n    if name is None:\n        name = subset_context[\"subset\"][\"name\"]\n\n    log.info(\n        \"Running '%s' on '%s'\" % (\n            Loader.__name__, subset_context[\"asset\"][\"name\"]\n        )\n    )\n\n    return Loader().load(subset_context, name, namespace, options)\n\n\ndef load_with_subset_contexts(\n    Loader, subset_contexts, namespace=None, name=None, options=None, **kwargs\n):\n\n    # Ensure options is a dictionary when no explicit options provided\n    if options is None:\n        options = kwargs.get(\"data\", dict())  # \"data\" for backward compat\n\n    assert isinstance(options, dict), \"Options must be a dictionary\"\n\n    # Fallback to subset when name is None\n    joined_subset_names = \" | \".join(\n        context[\"subset\"][\"name\"]\n        for context in subset_contexts\n    )\n    if name is None:\n        name = joined_subset_names\n\n    log.info(\n        \"Running '{}' on '{}'\".format(Loader.__name__, joined_subset_names)\n    )\n\n    return Loader().load(subset_contexts, name, namespace, options)\n\n\ndef load_container(\n    Loader, representation, namespace=None, name=None, options=None, **kwargs\n):\n    \"\"\"Use Loader to load a representation.\n\n    Args:\n        Loader (Loader): The loader class to trigger.\n        representation (str or ObjectId or dict): The representation id\n            or full representation as returned by the database.\n        namespace (str, Optional): The namespace to assign. Defaults to None.\n        name (str, Optional): The name to assign. Defaults to subset name.\n        options (dict, Optional): Additional options to pass on to the loader.\n\n    Returns:\n        The return of the `loader.load()` method.\n\n    Raises:\n        IncompatibleLoaderError: When the loader is not compatible with\n            the representation.\n\n    \"\"\"\n\n    context = get_representation_context(representation)\n    return load_with_repre_context(\n        Loader,\n        context,\n        namespace=namespace,\n        name=name,\n        options=options,\n        **kwargs\n    )\n\n\ndef get_loader_identifier(loader):\n    \"\"\"Loader identifier from loader plugin or object.\n\n    Identifier should be stored to container for future management.\n    \"\"\"\n    if not inspect.isclass(loader):\n        loader = loader.__class__\n    return loader.__name__\n\n\ndef get_loaders_by_name():\n    from .plugins import discover_loader_plugins\n\n    loaders_by_name = {}\n    for loader in discover_loader_plugins():\n        loader_name = loader.__name__\n        if loader_name in loaders_by_name:\n            raise KeyError(\n                \"Duplicated loader name {} !\".format(loader_name)\n            )\n        loaders_by_name[loader_name] = loader\n    return loaders_by_name\n\n\ndef _get_container_loader(container):\n    \"\"\"Return the Loader corresponding to the container\"\"\"\n    from .plugins import discover_loader_plugins\n\n    loader = container[\"loader\"]\n    for Plugin in discover_loader_plugins():\n        # TODO: Ensure the loader is valid\n        if get_loader_identifier(Plugin) == loader:\n            return Plugin\n    return None\n\n\ndef remove_container(container):\n    \"\"\"Remove a container\"\"\"\n\n    Loader = _get_container_loader(container)\n    if not Loader:\n        raise LoaderNotFoundError(\n            \"Can't remove container because loader '{}' was not found.\"\n            .format(container.get(\"loader\"))\n        )\n\n    return Loader().remove(container)\n\n\ndef update_container(container, version=-1):\n    \"\"\"Update a container\"\"\"\n\n    # Compute the different version from 'representation'\n    project_name = legacy_io.active_project()\n    current_representation = get_representation_by_id(\n        project_name, container[\"representation\"]\n    )\n\n    assert current_representation is not None, \"This is a bug\"\n\n    current_version = get_version_by_id(\n        project_name, current_representation[\"parent\"], fields=[\"parent\"]\n    )\n    if version == -1:\n        new_version = get_last_version_by_subset_id(\n            project_name, current_version[\"parent\"], fields=[\"_id\"]\n        )\n\n    elif isinstance(version, HeroVersionType):\n        new_version = get_hero_version_by_subset_id(\n            project_name, current_version[\"parent\"], fields=[\"_id\"]\n        )\n\n    else:\n        new_version = get_version_by_name(\n            project_name, version, current_version[\"parent\"], fields=[\"_id\"]\n        )\n\n    assert new_version is not None, \"This is a bug\"\n\n    new_representation = get_representation_by_name(\n        project_name, current_representation[\"name\"], new_version[\"_id\"]\n    )\n    assert new_representation is not None, \"Representation wasn't found\"\n\n    path = get_representation_path(new_representation)\n    assert os.path.exists(path), \"Path {} doesn't exist\".format(path)\n\n    # Run update on the Loader for this container\n    Loader = _get_container_loader(container)\n    if not Loader:\n        raise LoaderNotFoundError(\n            \"Can't update container because loader '{}' was not found.\"\n            .format(container.get(\"loader\"))\n        )\n\n    return Loader().update(container, new_representation)\n\n\ndef switch_container(container, representation, loader_plugin=None):\n    \"\"\"Switch a container to representation\n\n    Args:\n        container (dict): container information\n        representation (dict): representation data from document\n\n    Returns:\n        function call\n    \"\"\"\n\n    # Get the Loader for this container\n    if loader_plugin is None:\n        loader_plugin = _get_container_loader(container)\n\n    if not loader_plugin:\n        raise LoaderNotFoundError(\n            \"Can't switch container because loader '{}' was not found.\"\n            .format(container.get(\"loader\"))\n        )\n\n    if not hasattr(loader_plugin, \"switch\"):\n        # Backwards compatibility (classes without switch support\n        # might be better to just have \"switch\" raise NotImplementedError\n        # on the base class of Loader\\\n        raise LoaderSwitchNotImplementedError(\n            \"Loader {} does not support 'switch'\".format(loader_plugin.label)\n        )\n\n    # Get the new representation to switch to\n    project_name = legacy_io.active_project()\n    new_representation = get_representation_by_id(\n        project_name, representation[\"_id\"]\n    )\n\n    new_context = get_representation_context(new_representation)\n    if not is_compatible_loader(loader_plugin, new_context):\n        raise IncompatibleLoaderError(\n            \"Loader {} is incompatible with {}\".format(\n                loader_plugin.__name__, new_context[\"subset\"][\"name\"]\n            )\n        )\n\n    loader = loader_plugin(new_context)\n\n    return loader.switch(container, new_representation)\n\n\ndef get_representation_path_from_context(context):\n    \"\"\"Preparation wrapper using only context as a argument\"\"\"\n    representation = context['representation']\n    project_doc = context.get(\"project\")\n    root = None\n    session_project = legacy_io.Session.get(\"AVALON_PROJECT\")\n    if project_doc and project_doc[\"name\"] != session_project:\n        anatomy = Anatomy(project_doc[\"name\"])\n        root = anatomy.roots\n\n    return get_representation_path(representation, root)\n\n\ndef get_representation_path_with_anatomy(repre_doc, anatomy):\n    \"\"\"Receive representation path using representation document and anatomy.\n\n    Anatomy is used to replace 'root' key in representation file. Ideally\n    should be used instead of 'get_representation_path' which is based on\n    \"current context\".\n\n    Future notes:\n        We want also be able store resources into representation and I can\n        imagine the result should also contain paths to possible resources.\n\n    Args:\n        repre_doc (Dict[str, Any]): Representation document.\n        anatomy (Anatomy): Project anatomy object.\n\n    Returns:\n        Union[None, TemplateResult]: None if path can't be received\n\n    Raises:\n        InvalidRepresentationContext: When representation data are probably\n            invalid or not available.\n    \"\"\"\n\n    try:\n        template = repre_doc[\"data\"][\"template\"]\n\n    except KeyError:\n        raise InvalidRepresentationContext((\n            \"Representation document does not\"\n            \" contain template in data ('data.template')\"\n        ))\n\n    try:\n        context = repre_doc[\"context\"]\n        context[\"root\"] = anatomy.roots\n        path = StringTemplate.format_strict_template(template, context)\n\n    except TemplateUnsolved as exc:\n        raise InvalidRepresentationContext((\n            \"Couldn't resolve representation template with available data.\"\n            \" Reason: {}\".format(str(exc))\n        ))\n\n    return path.normalized()\n\n\ndef get_representation_path(representation, root=None, dbcon=None):\n    \"\"\"Get filename from representation document\n\n    There are three ways of getting the path from representation which are\n    tried in following sequence until successful.\n    1. Get template from representation['data']['template'] and data from\n       representation['context']. Then format template with the data.\n    2. Get template from project['config'] and format it with default data set\n    3. Get representation['data']['path'] and use it directly\n\n    Args:\n        representation(dict): representation document from the database\n\n    Returns:\n        str: fullpath of the representation\n\n    \"\"\"\n\n    if dbcon is None:\n        dbcon = legacy_io\n\n    if root is None:\n        from openpype.pipeline import registered_root\n\n        root = registered_root()\n\n    def path_from_representation():\n        try:\n            template = representation[\"data\"][\"template\"]\n        except KeyError:\n            return None\n\n        try:\n            context = representation[\"context\"]\n            context[\"root\"] = root\n            path = StringTemplate.format_strict_template(\n                template, context\n            )\n            # Force replacing backslashes with forward slashed if not on\n            #   windows\n            if platform.system().lower() != \"windows\":\n                path = path.replace(\"\\\\\", \"/\")\n        except (TemplateUnsolved, KeyError):\n            # Template references unavailable data\n            return None\n\n        if not path:\n            return path\n\n        normalized_path = os.path.normpath(path)\n        if os.path.exists(normalized_path):\n            return normalized_path\n        return path\n\n    def path_from_config():\n        try:\n            project_name = dbcon.active_project()\n            version_, subset, asset, project = get_representation_parents(\n                project_name, representation\n            )\n        except ValueError:\n            log.debug(\n                \"Representation %s wasn't found in database, \"\n                \"like a bug\" % representation[\"name\"]\n            )\n            return None\n\n        try:\n            template = project[\"config\"][\"template\"][\"publish\"]\n        except KeyError:\n            log.debug(\n                \"No template in project %s, \"\n                \"likely a bug\" % project[\"name\"]\n            )\n            return None\n\n        # default list() in get would not discover missing parents on asset\n        parents = asset.get(\"data\", {}).get(\"parents\")\n        if parents is not None:\n            hierarchy = \"/\".join(parents)\n\n        # Cannot fail, required members only\n        data = {\n            \"root\": root,\n            \"project\": {\n                \"name\": project[\"name\"],\n                \"code\": project.get(\"data\", {}).get(\"code\")\n            },\n            \"asset\": asset[\"name\"],\n            \"hierarchy\": hierarchy,\n            \"subset\": subset[\"name\"],\n            \"version\": version_[\"name\"],\n            \"representation\": representation[\"name\"],\n            \"family\": representation.get(\"context\", {}).get(\"family\"),\n            \"user\": dbcon.Session.get(\"AVALON_USER\", getpass.getuser()),\n            \"app\": dbcon.Session.get(\"AVALON_APP\", \"\"),\n            \"task\": dbcon.Session.get(\"AVALON_TASK\", \"\")\n        }\n\n        try:\n            template_obj = StringTemplate(template)\n            path = str(template_obj.format(data))\n            # Force replacing backslashes with forward slashed if not on\n            #   windows\n            if platform.system().lower() != \"windows\":\n                path = path.replace(\"\\\\\", \"/\")\n\n        except KeyError as e:\n            log.debug(\"Template references unavailable data: %s\" % e)\n            return None\n\n        normalized_path = os.path.normpath(path)\n        if os.path.exists(normalized_path):\n            return normalized_path\n        return path\n\n    def path_from_data():\n        if \"path\" not in representation[\"data\"]:\n            return None\n\n        path = representation[\"data\"][\"path\"]\n        # Force replacing backslashes with forward slashed if not on\n        #   windows\n        if platform.system().lower() != \"windows\":\n            path = path.replace(\"\\\\\", \"/\")\n\n        if os.path.exists(path):\n            return os.path.normpath(path)\n\n        dir_path, file_name = os.path.split(path)\n        if not os.path.exists(dir_path):\n            return\n\n        base_name, ext = os.path.splitext(file_name)\n        file_name_items = None\n        if \"#\" in base_name:\n            file_name_items = [part for part in base_name.split(\"#\") if part]\n        elif \"%\" in base_name:\n            file_name_items = base_name.split(\"%\")\n\n        if not file_name_items:\n            return\n\n        filename_start = file_name_items[0]\n\n        for _file in os.listdir(dir_path):\n            if _file.startswith(filename_start) and _file.endswith(ext):\n                return os.path.normpath(path)\n\n    return (\n        path_from_representation() or\n        path_from_config() or\n        path_from_data()\n    )\n\n\ndef is_compatible_loader(Loader, context):\n    \"\"\"Return whether a loader is compatible with a context.\n\n    This checks the version's families and the representation for the given\n    Loader.\n\n    Returns:\n        bool\n    \"\"\"\n\n    return Loader.is_compatible_loader(context)\n\n\ndef loaders_from_repre_context(loaders, repre_context):\n    \"\"\"Return compatible loaders for by representaiton's context.\"\"\"\n\n    return [\n        loader\n        for loader in loaders\n        if is_compatible_loader(loader, repre_context)\n    ]\n\n\ndef filter_repre_contexts_by_loader(repre_contexts, loader):\n    \"\"\"Filter representation contexts for loader.\n\n    Args:\n        repre_contexts (list[dict[str, Ant]]): Representation context.\n        loader (LoaderPlugin): Loader plugin to filter contexts for.\n\n    Returns:\n        list[dict[str, Any]]: Filtered representation contexts.\n    \"\"\"\n\n    return [\n        repre_context\n        for repre_context in repre_contexts\n        if is_compatible_loader(loader, repre_context)\n    ]\n\n\ndef loaders_from_representation(loaders, representation):\n    \"\"\"Return all compatible loaders for a representation.\"\"\"\n\n    context = get_representation_context(representation)\n    return loaders_from_repre_context(loaders, context)\n\n\ndef any_outdated_containers(host=None, project_name=None):\n    \"\"\"Check if there are any outdated containers in scene.\"\"\"\n\n    if get_outdated_containers(host, project_name):\n        return True\n    return False\n\n\ndef get_outdated_containers(host=None, project_name=None):\n    \"\"\"Collect outdated containers from host scene.\n\n    Currently registered host and project in global session are used if\n    arguments are not passed.\n\n    Args:\n        host (ModuleType): Host implementation with 'ls' function available.\n        project_name (str): Name of project in which context we are.\n    \"\"\"\n\n    if host is None:\n        from openpype.pipeline import registered_host\n\n        host = registered_host()\n\n    if project_name is None:\n        project_name = legacy_io.active_project()\n\n    if isinstance(host, ILoadHost):\n        containers = host.get_containers()\n    else:\n        containers = host.ls()\n    return filter_containers(containers, project_name).outdated\n\n\ndef filter_containers(containers, project_name):\n    \"\"\"Filter containers and split them into 4 categories.\n\n    Categories are 'latest', 'outdated', 'invalid' and 'not_found'.\n    The 'lastest' containers are from last version, 'outdated' are not,\n    'invalid' are invalid containers (invalid content) and 'not_found' has\n    some missing entity in database.\n\n    Args:\n        containers (Iterable[dict]): List of containers referenced into scene.\n        project_name (str): Name of project in which context shoud look for\n            versions.\n\n    Returns:\n        ContainersFilterResult: Named tuple with 'latest', 'outdated',\n            'invalid' and 'not_found' containers.\n    \"\"\"\n\n    # Make sure containers is list that won't change\n    containers = list(containers)\n\n    outdated_containers = []\n    uptodate_containers = []\n    not_found_containers = []\n    invalid_containers = []\n    output = ContainersFilterResult(\n        uptodate_containers,\n        outdated_containers,\n        not_found_containers,\n        invalid_containers\n    )\n    # Query representation docs to get it's version ids\n    repre_ids = {\n        container[\"representation\"]\n        for container in containers\n        if container[\"representation\"]\n    }\n    if not repre_ids:\n        if containers:\n            invalid_containers.extend(containers)\n        return output\n\n    repre_docs = get_representations(\n        project_name,\n        representation_ids=repre_ids,\n        fields=[\"_id\", \"parent\"]\n    )\n    # Store representations by stringified representation id\n    repre_docs_by_str_id = {}\n    repre_docs_by_version_id = collections.defaultdict(list)\n    for repre_doc in repre_docs:\n        repre_id = str(repre_doc[\"_id\"])\n        version_id = repre_doc[\"parent\"]\n        repre_docs_by_str_id[repre_id] = repre_doc\n        repre_docs_by_version_id[version_id].append(repre_doc)\n\n    # Query version docs to get it's subset ids\n    # - also query hero version to be able identify if representation\n    #   belongs to existing version\n    version_docs = get_versions(\n        project_name,\n        version_ids=repre_docs_by_version_id.keys(),\n        hero=True,\n        fields=[\"_id\", \"parent\", \"type\"]\n    )\n    verisons_by_id = {}\n    versions_by_subset_id = collections.defaultdict(list)\n    hero_version_ids = set()\n    for version_doc in version_docs:\n        version_id = version_doc[\"_id\"]\n        # Store versions by their ids\n        verisons_by_id[version_id] = version_doc\n        # There's no need to query subsets for hero versions\n        #   - they are considered as latest?\n        if version_doc[\"type\"] == \"hero_version\":\n            hero_version_ids.add(version_id)\n            continue\n        subset_id = version_doc[\"parent\"]\n        versions_by_subset_id[subset_id].append(version_doc)\n\n    last_versions = get_last_versions(\n        project_name,\n        subset_ids=versions_by_subset_id.keys(),\n        fields=[\"_id\"]\n    )\n    # Figure out which versions are outdated\n    outdated_version_ids = set()\n    for subset_id, last_version_doc in last_versions.items():\n        for version_doc in versions_by_subset_id[subset_id]:\n            version_id = version_doc[\"_id\"]\n            if version_id != last_version_doc[\"_id\"]:\n                outdated_version_ids.add(version_id)\n\n    # Based on all collected data figure out which containers are outdated\n    #   - log out if there are missing representation or version documents\n    for container in containers:\n        container_name = container[\"objectName\"]\n        repre_id = container[\"representation\"]\n        if not repre_id:\n            invalid_containers.append(container)\n            continue\n\n        repre_doc = repre_docs_by_str_id.get(repre_id)\n        if not repre_doc:\n            log.debug((\n                \"Container '{}' has an invalid representation.\"\n                \" It is missing in the database.\"\n            ).format(container_name))\n            not_found_containers.append(container)\n            continue\n\n        version_id = repre_doc[\"parent\"]\n        if version_id in outdated_version_ids:\n            outdated_containers.append(container)\n\n        elif version_id not in verisons_by_id:\n            log.debug((\n                \"Representation on container '{}' has an invalid version.\"\n                \" It is missing in the database.\"\n            ).format(container_name))\n            not_found_containers.append(container)\n\n        else:\n            uptodate_containers.append(container)\n\n    return output\n"
  },
  {
    "path": "openpype/pipeline/mongodb.py",
    "content": "import os\nimport time\nimport functools\nimport logging\nimport pymongo\nfrom uuid import uuid4\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import OpenPypeMongoConnection\n\nfrom . import schema\n\n\ndef requires_install(func):\n    func_obj = getattr(func, \"__self__\", None)\n\n    @functools.wraps(func)\n    def decorated(*args, **kwargs):\n        if func_obj is not None:\n            _obj = func_obj\n        else:\n            _obj = args[0]\n        if not _obj.is_installed():\n            if _obj.auto_install:\n                _obj.install()\n            else:\n                raise IOError(\n                    \"'{}.{}()' requires to run install() first\".format(\n                        _obj.__class__.__name__, func.__name__\n                    )\n                )\n        return func(*args, **kwargs)\n    return decorated\n\n\ndef auto_reconnect(func):\n    \"\"\"Handling auto reconnect in 3 retry times\"\"\"\n    retry_times = 3\n    reconnect_msg = \"Reconnecting...\"\n    func_obj = getattr(func, \"__self__\", None)\n\n    @functools.wraps(func)\n    def decorated(*args, **kwargs):\n        if func_obj is not None:\n            _obj = func_obj\n        else:\n            _obj = args[0]\n\n        for retry in range(1, retry_times + 1):\n            try:\n                return func(*args, **kwargs)\n            except pymongo.errors.AutoReconnect:\n                if hasattr(_obj, \"log\"):\n                    _obj.log.warning(reconnect_msg)\n                else:\n                    print(reconnect_msg)\n\n                if retry >= retry_times:\n                    raise\n                time.sleep(0.1)\n    return decorated\n\n\nSESSION_CONTEXT_KEYS = (\n    # Name of current Project\n    \"AVALON_PROJECT\",\n    # Name of current Asset\n    \"AVALON_ASSET\",\n    # Name of current task\n    \"AVALON_TASK\",\n    # Name of current app\n    \"AVALON_APP\",\n    # Path to working directory\n    \"AVALON_WORKDIR\",\n    # Optional path to scenes directory (see Work Files API)\n    \"AVALON_SCENEDIR\"\n)\n\n\ndef session_data_from_environment(context_keys=False):\n    session_data = {}\n    if context_keys:\n        for key in SESSION_CONTEXT_KEYS:\n            value = os.environ.get(key)\n            session_data[key] = value or \"\"\n    else:\n        for key in SESSION_CONTEXT_KEYS:\n            session_data[key] = None\n\n    for key, default_value in (\n        # Name of Avalon in graphical user interfaces\n        # Use this to customise the visual appearance of Avalon\n        # to better integrate with your surrounding pipeline\n        (\"AVALON_LABEL\", \"Avalon\"),\n\n        # Used during any connections to the outside world\n        (\"AVALON_TIMEOUT\", \"1000\"),\n\n        # Name of database used in MongoDB\n        (\"AVALON_DB\", \"avalon\"),\n    ):\n        value = os.environ.get(key) or default_value\n        if value is not None:\n            session_data[key] = value\n\n    return session_data\n\n\nclass AvalonMongoDB:\n    def __init__(self, session=None, auto_install=True):\n        self._id = uuid4()\n        self._database = None\n        self.auto_install = auto_install\n        self._installed = False\n\n        if session is None:\n            session = session_data_from_environment(context_keys=False)\n\n        self.Session = session\n\n        self.log = logging.getLogger(self.__class__.__name__)\n\n    def __getattr__(self, attr_name):\n        attr = None\n        if not self.is_installed() and self.auto_install:\n            self.install()\n\n        if not self.is_installed():\n            raise IOError(\n                \"'{}.{}()' requires to run install() first\".format(\n                    self.__class__.__name__, attr_name\n                )\n            )\n\n        project_name = self.active_project()\n        if project_name is None:\n            raise ValueError(\n                \"Value of 'Session[\\\"AVALON_PROJECT\\\"]' is not set.\"\n            )\n\n        collection = self._database[project_name]\n        not_set = object()\n        attr = getattr(collection, attr_name, not_set)\n\n        if attr is not_set:\n            # Raise attribute error\n            raise AttributeError(\n                \"{} has no attribute '{}'.\".format(\n                    collection.__class__.__name__, attr_name\n                )\n            )\n\n        # Decorate function\n        if callable(attr):\n            attr = auto_reconnect(attr)\n        return attr\n\n    @property\n    def mongo_client(self):\n        return OpenPypeMongoConnection.get_mongo_client()\n\n    @property\n    def id(self):\n        return self._id\n\n    @property\n    def database(self):\n        if not self.is_installed() and self.auto_install:\n            self.install()\n\n        if self.is_installed():\n            return self._database\n\n        raise IOError(\n            \"'{}.database' requires to run install() first\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def is_installed(self):\n        return self._installed\n\n    def install(self):\n        \"\"\"Establish a persistent connection to the database\"\"\"\n        if self.is_installed():\n            return\n\n        self._installed = True\n        if not AYON_SERVER_ENABLED:\n            self._database = self.mongo_client[str(os.environ[\"AVALON_DB\"])]\n\n    def uninstall(self):\n        \"\"\"Close any connection to the database\"\"\"\n        self._installed = False\n        self._database = None\n\n    @requires_install\n    def active_project(self):\n        \"\"\"Return the name of the active project\"\"\"\n        return self.Session[\"AVALON_PROJECT\"]\n\n    def current_project(self):\n        \"\"\"Currently set project in Session without triggering installation.\"\"\"\n        return self.Session.get(\"AVALON_PROJECT\")\n\n    @requires_install\n    @auto_reconnect\n    def projects(self, projection=None, only_active=True):\n        \"\"\"Iter project documents\n\n        Args:\n            projection (optional): MongoDB query projection operation\n            only_active (optional): Skip inactive projects, default True.\n\n        Returns:\n            Project documents iterator\n\n        \"\"\"\n        query_filter = {\"type\": \"project\"}\n        if only_active:\n            query_filter.update({\n                \"$or\": [\n                    {\"data.active\": {\"$exists\": 0}},\n                    {\"data.active\": True},\n                ]\n            })\n\n        for project_name in self._database.collection_names():\n            if project_name in (\"system.indexes\",):\n                continue\n\n            # Each collection will have exactly one project document\n\n            doc = self._database[project_name].find_one(\n                query_filter, projection=projection\n            )\n            if doc is not None:\n                yield doc\n\n    @auto_reconnect\n    def insert_one(self, item, *args, **kwargs):\n        assert isinstance(item, dict), \"item must be of type <dict>\"\n        schema.validate(item)\n        return self._database[self.active_project()].insert_one(\n            item, *args, **kwargs\n        )\n\n    @auto_reconnect\n    def insert_many(self, items, *args, **kwargs):\n        # check if all items are valid\n        assert isinstance(items, list), \"`items` must be of type <list>\"\n        for item in items:\n            assert isinstance(item, dict), \"`item` must be of type <dict>\"\n            schema.validate(item)\n\n        return self._database[self.active_project()].insert_many(\n            items, *args, **kwargs\n        )\n\n    def parenthood(self, document):\n        assert document is not None, \"This is a bug\"\n\n        parents = list()\n\n        while document.get(\"parent\") is not None:\n            document = self.find_one({\"_id\": document[\"parent\"]})\n            if document is None:\n                break\n\n            if document.get(\"type\") == \"hero_version\":\n                _document = self.find_one({\"_id\": document[\"version_id\"]})\n                document[\"data\"] = _document[\"data\"]\n\n            parents.append(document)\n\n        return parents\n"
  },
  {
    "path": "openpype/pipeline/plugin_discover.py",
    "content": "import os\nimport inspect\nimport traceback\n\nfrom openpype.lib import Logger\nfrom openpype.lib.python_module_tools import (\n    modules_from_path,\n    classes_from_module,\n)\n\nlog = Logger.get_logger(__name__)\n\n\nclass DiscoverResult:\n    \"\"\"Result of Plug-ins discovery of a single superclass type.\n\n    Stores discovered, duplicated, ignored and abstract plugins and file paths\n    which crashed on execution of file.\n    \"\"\"\n\n    def __init__(self, superclass):\n        self.superclass = superclass\n        self.plugins = []\n        self.crashed_file_paths = {}\n        self.duplicated_plugins = []\n        self.abstract_plugins = []\n        self.ignored_plugins = set()\n        # Store loaded modules to keep them in memory\n        self._modules = set()\n\n    def __iter__(self):\n        for plugin in self.plugins:\n            yield plugin\n\n    def __getitem__(self, item):\n        return self.plugins[item]\n\n    def __setitem__(self, item, value):\n        self.plugins[item] = value\n\n    def add_module(self, module):\n        \"\"\"Add dynamically loaded python module to keep it in memory.\"\"\"\n        self._modules.add(module)\n\n    def get_report(self, only_errors=True, exc_info=True, full_report=False):\n        lines = []\n        if not only_errors:\n            # Successfully discovered plugins\n            if self.plugins or full_report:\n                lines.append(\n                    \"*** Discovered {} plugins\".format(len(self.plugins))\n                )\n                for cls in self.plugins:\n                    lines.append(\"- {}\".format(cls.__class__.__name__))\n\n            # Plugin that were defined to be ignored\n            if self.ignored_plugins or full_report:\n                lines.append(\"*** Ignored plugins {}\".format(len(\n                    self.ignored_plugins\n                )))\n                for cls in self.ignored_plugins:\n                    lines.append(\"- {}\".format(cls.__name__))\n\n        # Abstract classes\n        if self.abstract_plugins or full_report:\n            lines.append(\"*** Discovered {} abstract plugins\".format(len(\n                self.abstract_plugins\n            )))\n            for cls in self.abstract_plugins:\n                lines.append(\"- {}\".format(cls.__name__))\n\n        # Abstract classes\n        if self.duplicated_plugins or full_report:\n            lines.append(\"*** There were {} duplicated plugins\".format(len(\n                self.duplicated_plugins\n            )))\n            for cls in self.duplicated_plugins:\n                lines.append(\"- {}\".format(cls.__name__))\n\n        if self.crashed_file_paths or full_report:\n            lines.append(\"*** Failed to load {} files\".format(len(\n                self.crashed_file_paths\n            )))\n            for path, exc_info_args in self.crashed_file_paths.items():\n                lines.append(\"- {}\".format(path))\n                if exc_info:\n                    lines.append(10 * \"*\")\n                    lines.extend(traceback.format_exception(*exc_info_args))\n                    lines.append(10 * \"*\")\n\n        return \"\\n\".join(lines)\n\n    def log_report(self, only_errors=True, exc_info=True):\n        report = self.get_report(only_errors, exc_info)\n        if report:\n            log.info(report)\n\n\nclass PluginDiscoverContext(object):\n    \"\"\"Store and discover registered types nad registered paths to types.\n\n    Keeps in memory all registered types and their paths. Paths are dynamically\n    loaded on discover so different discover calls won't return the same\n    class objects even if were loaded from same file.\n    \"\"\"\n\n    def __init__(self):\n        self._registered_plugins = {}\n        self._registered_plugin_paths = {}\n        self._last_discovered_plugins = {}\n        # Store the last result to memory\n        self._last_discovered_results = {}\n\n    def get_last_discovered_plugins(self, superclass):\n        \"\"\"Access last discovered plugin by a subperclass.\n\n        Returns:\n            None: When superclass was not discovered yet.\n            list: Lastly discovered plugins of the superclass.\n        \"\"\"\n\n        return self._last_discovered_plugins.get(superclass)\n\n    def discover(\n        self,\n        superclass,\n        allow_duplicates=True,\n        ignore_classes=None,\n        return_report=False\n    ):\n        \"\"\"Find and return subclasses of `superclass`\n\n        Args:\n            superclass (type): Class which determines discovered subclasses.\n            allow_duplicates (bool): Validate class name duplications.\n            ignore_classes (list): List of classes that will be ignored\n                and not added to result.\n            return_report (bool): Output will be full report if set to 'True'.\n\n        Returns:\n            Union[DiscoverResult, list[Any]]: Object holding successfully\n                discovered plugins, ignored plugins, plugins with missing\n                abstract implementation and duplicated plugin.\n        \"\"\"\n\n        if not ignore_classes:\n            ignore_classes = []\n\n        result = DiscoverResult(superclass)\n        plugin_names = set()\n        registered_classes = self._registered_plugins.get(superclass) or []\n        registered_paths = self._registered_plugin_paths.get(superclass) or []\n        for cls in registered_classes:\n            if cls is superclass or cls in ignore_classes:\n                result.ignored_plugins.add(cls)\n                continue\n\n            if inspect.isabstract(cls):\n                result.abstract_plugins.append(cls)\n                continue\n\n            class_name = cls.__name__\n            if class_name in plugin_names:\n                result.duplicated_plugins.append(cls)\n                continue\n            plugin_names.add(class_name)\n            result.plugins.append(cls)\n\n        # Include plug-ins from registered paths\n        for path in registered_paths:\n            modules, crashed = modules_from_path(path)\n            for item in crashed:\n                filepath, exc_info = item\n                result.crashed_file_paths[filepath] = exc_info\n\n            for item in modules:\n                filepath, module = item\n                result.add_module(module)\n                for cls in classes_from_module(superclass, module):\n                    if cls is superclass or cls in ignore_classes:\n                        result.ignored_plugins.add(cls)\n                        continue\n\n                    if inspect.isabstract(cls):\n                        result.abstract_plugins.append(cls)\n                        continue\n\n                    if not allow_duplicates:\n                        class_name = cls.__name__\n                        if class_name in plugin_names:\n                            result.duplicated_plugins.append(cls)\n                            continue\n                        plugin_names.add(class_name)\n\n                    result.plugins.append(cls)\n\n        # Store in memory last result to keep in memory loaded modules\n        self._last_discovered_results[superclass] = result\n        self._last_discovered_plugins[superclass] = list(\n            result.plugins\n        )\n        result.log_report()\n        if return_report:\n            return result\n        return result.plugins\n\n    def register_plugin(self, superclass, cls):\n        \"\"\"Register a directory containing plug-ins of type `superclass`\n\n        Arguments:\n            superclass (type): Superclass of plug-in\n            cls (object): Subclass of `superclass`\n        \"\"\"\n\n        if superclass not in self._registered_plugins:\n            self._registered_plugins[superclass] = list()\n\n        if cls not in self._registered_plugins[superclass]:\n            self._registered_plugins[superclass].append(cls)\n\n    def register_plugin_path(self, superclass, path):\n        \"\"\"Register a directory of one or more plug-ins\n\n        Arguments:\n            superclass (type): Superclass of plug-ins to look for during\n                discovery\n            path (str): Absolute path to directory in which to discover\n                plug-ins\n        \"\"\"\n\n        if superclass not in self._registered_plugin_paths:\n            self._registered_plugin_paths[superclass] = list()\n\n        path = os.path.normpath(path)\n        if path not in self._registered_plugin_paths[superclass]:\n            self._registered_plugin_paths[superclass].append(path)\n\n    def registered_plugin_paths(self):\n        \"\"\"Return all currently registered plug-in paths\"\"\"\n        # Return shallow copy so we the original data can't be changed\n        return {\n            superclass: paths[:]\n            for superclass, paths in self._registered_plugin_paths.items()\n        }\n\n    def deregister_plugin(self, superclass, plugin):\n        \"\"\"Opposite of `register_plugin()`\"\"\"\n        if superclass in self._registered_plugins:\n            self._registered_plugins[superclass].remove(plugin)\n\n    def deregister_plugin_path(self, superclass, path):\n        \"\"\"Opposite of `register_plugin_path()`\"\"\"\n        self._registered_plugin_paths[superclass].remove(path)\n\n\nclass _GlobalDiscover:\n    \"\"\"Access to global object of PluginDiscoverContext.\n\n    Using singleton object to register/deregister plugins and plugin paths\n    and then discover them by superclass.\n    \"\"\"\n\n    _context = None\n\n    @classmethod\n    def get_context(cls):\n        if cls._context is None:\n            cls._context = PluginDiscoverContext()\n        return cls._context\n\n\ndef discover(\n    superclass,\n    allow_duplicates=True,\n    ignore_classes=None,\n    return_report=False\n):\n    \"\"\"Find and return subclasses of `superclass`\n\n    Args:\n        superclass (type): Class which determines discovered subclasses.\n        allow_duplicates (bool): Validate class name duplications.\n        ignore_classes (list): List of classes that will be ignored\n            and not added to result.\n        return_report (bool): Output will be full report if set to 'True'.\n\n    Returns:\n        Union[DiscoverResult, list[Any]]: Object holding successfully\n            discovered plugins, ignored plugins, plugins with missing\n            abstract implementation and duplicated plugin.\n    \"\"\"\n\n    context = _GlobalDiscover.get_context()\n    return context.discover(\n        superclass,\n        allow_duplicates,\n        ignore_classes,\n        return_report\n    )\n\n\ndef get_last_discovered_plugins(superclass):\n    context = _GlobalDiscover.get_context()\n    return context.get_last_discovered_plugins(superclass)\n\n\ndef register_plugin(superclass, cls):\n    context = _GlobalDiscover.get_context()\n    context.register_plugin(superclass, cls)\n\n\ndef register_plugin_path(superclass, path):\n    context = _GlobalDiscover.get_context()\n    context.register_plugin_path(superclass, path)\n\n\ndef deregister_plugin(superclass, cls):\n    context = _GlobalDiscover.get_context()\n    context.deregister_plugin(superclass, cls)\n\n\ndef deregister_plugin_path(superclass, path):\n    context = _GlobalDiscover.get_context()\n    context.deregister_plugin_path(superclass, path)\n"
  },
  {
    "path": "openpype/pipeline/project_folders.py",
    "content": "import os\nimport re\nimport json\n\nimport six\n\nfrom openpype.settings import get_project_settings\nfrom openpype.lib import Logger\n\nfrom .anatomy import Anatomy\nfrom .template_data import get_project_template_data\n\n\ndef concatenate_splitted_paths(split_paths, anatomy):\n    log = Logger.get_logger(\"concatenate_splitted_paths\")\n    pattern_array = re.compile(r\"\\[.*\\]\")\n    output = []\n    for path_items in split_paths:\n        clean_items = []\n        if isinstance(path_items, str):\n            path_items = [path_items]\n\n        for path_item in path_items:\n            if not re.match(r\"{.+}\", path_item):\n                path_item = re.sub(pattern_array, \"\", path_item)\n            clean_items.append(path_item)\n\n        # backward compatibility\n        if \"__project_root__\" in path_items:\n            for root, root_path in anatomy.roots.items():\n                if not root_path or not os.path.exists(str(root_path)):\n                    log.debug(\n                        \"Root {} path path {} not exist on computer!\".format(\n                            root, root_path\n                        )\n                    )\n                    continue\n\n                root_items = [\n                    \"{{root[{}]}}\".format(root),\n                    \"{project[name]}\"\n                ]\n                root_items.extend(clean_items[1:])\n                output.append(os.path.normpath(os.path.sep.join(root_items)))\n            continue\n\n        output.append(os.path.normpath(os.path.sep.join(clean_items)))\n\n    return output\n\n\ndef fill_paths(path_list, anatomy):\n    format_data = get_project_template_data(project_name=anatomy.project_name)\n    format_data[\"root\"] = anatomy.roots\n    filled_paths = []\n\n    for path in path_list:\n        new_path = path.format(**format_data)\n        filled_paths.append(new_path)\n\n    return filled_paths\n\n\ndef create_project_folders(project_name, basic_paths=None):\n    log = Logger.get_logger(\"create_project_folders\")\n    anatomy = Anatomy(project_name)\n    if basic_paths is None:\n        basic_paths = get_project_basic_paths(project_name)\n\n    if not basic_paths:\n        return\n\n    concat_paths = concatenate_splitted_paths(basic_paths, anatomy)\n    filled_paths = fill_paths(concat_paths, anatomy)\n\n    # Create folders\n    for path in filled_paths:\n        if os.path.exists(path):\n            log.debug(\"Folder already exists: {}\".format(path))\n        else:\n            log.debug(\"Creating folder: {}\".format(path))\n            os.makedirs(path)\n\n\ndef _list_path_items(folder_structure):\n    output = []\n    for key, value in folder_structure.items():\n        if not value:\n            output.append(key)\n            continue\n\n        paths = _list_path_items(value)\n        for path in paths:\n            if not isinstance(path, (list, tuple)):\n                path = [path]\n\n            item = [key]\n            item.extend(path)\n            output.append(item)\n\n    return output\n\n\ndef get_project_basic_paths(project_name):\n    project_settings = get_project_settings(project_name)\n    folder_structure = (\n        project_settings[\"global\"][\"project_folder_structure\"]\n    )\n    if not folder_structure:\n        return []\n\n    if isinstance(folder_structure, six.string_types):\n        folder_structure = json.loads(folder_structure)\n    return _list_path_items(folder_structure)\n"
  },
  {
    "path": "openpype/pipeline/publish/README.md",
    "content": "# Publish\nOpenPype is using `pyblish` for publishing process which is a little bit extented and modified mainly for UI purposes. OpenPype's (new) publish UI does not allow to enable/disable instances or plugins that can be done during creation part. Also does support actions only for validators after validation exception.\n\n## Exceptions\nOpenPype define few specific exceptions that should be used in publish plugins.\n\n### Validation exception\nValidation plugins should raise `PublishValidationError` to show to an artist what's wrong and give him actions to fix it. The exception says that error happened in plugin can be fixed by artist himself (with or without action on plugin). Any other errors will stop publishing immediately. Exception `PublishValidationError` raised after validation order has same effect as any other exception.\n\nException `PublishValidationError` 3 arguments:\n- **message** Which is not used in UI but for headless publishing.\n- **title** Short description of error (2-5 words). Title is used for grouping of exceptions per plugin.\n- **description** Detailed description of happened issue where markdown and html can be used.\n\n\n### Known errors\nWhen there is a known error that can't be fixed by user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raise. The only difference is that it's message is shown in UI to artist otherwise a neutral message without context is shown.\n\n## Plugin extension\nPublish plugins can be extended by additional logic when inherits from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class).\n\n```python\nimport pyblish.api\nfrom openpype.pipeline import OpenPypePyblishPluginMixin\n\n\n# Example context plugin\nclass MyExtendedPlugin(\n    pyblish.api.ContextPlugin, OpenPypePyblishPluginMixin\n):\n    pass\n\n```\n\n### Extensions\nCurrently only extension is ability to define attributes for instances during creation. Method `get_attribute_defs` returns attribute definitions for families defined in plugin's `families` attribute if it's instance plugin or for whole context if it's context plugin. To convert existing values (or to remove legacy values) can be implemented `convert_attribute_values`. Values of publish attributes from created instance are never removed automatically so implementing of this method is best way to remove legacy data or convert them to new data structure.\n\nPossible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`.\n"
  },
  {
    "path": "openpype/pipeline/publish/__init__.py",
    "content": "from .constants import (\n    ValidatePipelineOrder,\n    ValidateContentsOrder,\n    ValidateSceneOrder,\n    ValidateMeshOrder,\n)\n\nfrom .publish_plugins import (\n    AbstractMetaInstancePlugin,\n    AbstractMetaContextPlugin,\n\n    PublishValidationError,\n    PublishXmlValidationError,\n    KnownPublishError,\n    OpenPypePyblishPluginMixin,\n    OptionalPyblishPluginMixin,\n\n    RepairAction,\n    RepairContextAction,\n\n    Extractor,\n    ColormanagedPyblishPluginMixin\n)\n\nfrom .lib import (\n    get_publish_template_name,\n\n    publish_plugins_discover,\n    load_help_content_from_plugin,\n    load_help_content_from_filepath,\n\n    get_errored_instances_from_context,\n    get_errored_plugins_from_context,\n\n    filter_instances_for_context_plugin,\n    context_plugin_should_run,\n    get_instance_staging_dir,\n    get_publish_repre_path,\n\n    apply_plugin_settings_automatically,\n    get_plugin_settings,\n    get_publish_instance_label,\n    get_publish_instance_families,\n)\n\nfrom .abstract_expected_files import ExpectedFiles\nfrom .abstract_collect_render import (\n    RenderInstance,\n    AbstractCollectRender,\n)\n\n\n__all__ = (\n    \"ValidatePipelineOrder\",\n    \"ValidateContentsOrder\",\n    \"ValidateSceneOrder\",\n    \"ValidateMeshOrder\",\n\n    \"AbstractMetaInstancePlugin\",\n    \"AbstractMetaContextPlugin\",\n\n    \"PublishValidationError\",\n    \"PublishXmlValidationError\",\n    \"KnownPublishError\",\n    \"OpenPypePyblishPluginMixin\",\n    \"OptionalPyblishPluginMixin\",\n\n    \"RepairAction\",\n    \"RepairContextAction\",\n\n    \"Extractor\",\n    \"ColormanagedPyblishPluginMixin\",\n\n    \"get_publish_template_name\",\n\n    \"publish_plugins_discover\",\n    \"load_help_content_from_plugin\",\n    \"load_help_content_from_filepath\",\n\n    \"get_errored_instances_from_context\",\n    \"get_errored_plugins_from_context\",\n\n    \"filter_instances_for_context_plugin\",\n    \"context_plugin_should_run\",\n    \"get_instance_staging_dir\",\n    \"get_publish_repre_path\",\n\n    \"apply_plugin_settings_automatically\",\n    \"get_plugin_settings\",\n    \"get_publish_instance_label\",\n    \"get_publish_instance_families\",\n\n    \"ExpectedFiles\",\n\n    \"RenderInstance\",\n    \"AbstractCollectRender\",\n)\n"
  },
  {
    "path": "openpype/pipeline/publish/abstract_collect_render.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect render template.\n\nTODO: use @dataclass when times come.\n\n\"\"\"\nfrom abc import abstractmethod\n\nimport attr\nimport six\n\nimport pyblish.api\n\nfrom .publish_plugins import AbstractMetaContextPlugin\n\n\n@attr.s\nclass RenderInstance(object):\n    \"\"\"Data collected by collectors.\n\n    This data class later on passed to collected instances.\n    Those attributes are required later on.\n\n    \"\"\"\n\n    # metadata\n    version = attr.ib()  # instance version\n    time = attr.ib()  # time of instance creation (get_formatted_current_time)\n    source = attr.ib()  # path to source scene file\n    label = attr.ib()  # label to show in GUI\n    subset = attr.ib()  # subset name\n    task = attr.ib()  # task name\n    asset = attr.ib()  # asset name\n    attachTo = attr.ib()  # subset name to attach render to\n    setMembers = attr.ib()  # list of nodes/members producing render output\n    publish = attr.ib()  # bool, True to publish instance\n    name = attr.ib()  # instance name\n\n    # format settings\n    resolutionWidth = attr.ib()  # resolution width (1920)\n    resolutionHeight = attr.ib()  # resolution height (1080)\n    pixelAspect = attr.ib()  # pixel aspect (1.0)\n\n    # time settings\n    frameStart = attr.ib()  # start frame\n    frameEnd = attr.ib()  # start end\n    frameStep = attr.ib()  # frame step\n\n    handleStart = attr.ib(default=None)  # start frame\n    handleEnd = attr.ib(default=None)  # start frame\n\n    # for software (like Harmony) where frame range cannot be set by DB\n    # handles need to be propagated if exist\n    ignoreFrameHandleCheck = attr.ib(default=False)\n\n    # --------------------\n    # With default values\n    # metadata\n    renderer = attr.ib(default=\"\")  # renderer - can be used in Deadline\n    review = attr.ib(default=None)  # False - explicitly skip review\n    priority = attr.ib(default=50)  # job priority on farm\n\n    family = attr.ib(default=\"renderlayer\")\n    families = attr.ib(default=[\"renderlayer\"])  # list of families\n    # True if should be rendered on farm, eg not integrate\n    farm = attr.ib(default=False)\n\n    # format settings\n    multipartExr = attr.ib(default=False)  # flag for multipart exrs\n    convertToScanline = attr.ib(default=False)  # flag for exr conversion\n\n    tileRendering = attr.ib(default=False)  # bool: treat render as tiles\n    tilesX = attr.ib(default=0)  # number of tiles in X\n    tilesY = attr.ib(default=0)  # number of tiles in Y\n\n    # submit_publish_job\n    deadlineSubmissionJob = attr.ib(default=None)\n    anatomyData = attr.ib(default=None)\n    outputDir = attr.ib(default=None)\n    context = attr.ib(default=None)\n\n    @frameStart.validator\n    def check_frame_start(self, _, value):\n        \"\"\"Validate if frame start is not larger then end.\"\"\"\n        if value > self.frameEnd:\n            raise ValueError(\"frameStart must be smaller \"\n                             \"or equal then frameEnd\")\n\n    @frameEnd.validator\n    def check_frame_end(self, _, value):\n        \"\"\"Validate if frame end is not less then start.\"\"\"\n        if value < self.frameStart:\n            raise ValueError(\"frameEnd must be smaller \"\n                             \"or equal then frameStart\")\n\n    @tilesX.validator\n    def check_tiles_x(self, _, value):\n        \"\"\"Validate if tile x isn't less then 1.\"\"\"\n        if not self.tileRendering:\n            return\n        if value < 1:\n            raise ValueError(\"tile X size cannot be less then 1\")\n\n        if value == 1 and self.tilesY == 1:\n            raise ValueError(\"both tiles X a Y sizes are set to 1\")\n\n    @tilesY.validator\n    def check_tiles_y(self, _, value):\n        \"\"\"Validate if tile y isn't less then 1.\"\"\"\n        if not self.tileRendering:\n            return\n        if value < 1:\n            raise ValueError(\"tile Y size cannot be less then 1\")\n\n        if value == 1 and self.tilesX == 1:\n            raise ValueError(\"both tiles X a Y sizes are set to 1\")\n\n\n@six.add_metaclass(AbstractMetaContextPlugin)\nclass AbstractCollectRender(pyblish.api.ContextPlugin):\n    \"\"\"Gather all publishable render layers from renderSetup.\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.01\n    label = \"Collect Render\"\n    sync_workfile_version = False\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"Constructor.\"\"\"\n        super(AbstractCollectRender, self).__init__(*args, **kwargs)\n        self._file_path = None\n        self._context = None\n\n    def process(self, context):\n        \"\"\"Entry point to collector.\"\"\"\n        self._context = context\n        for instance in context:\n            # make sure workfile instance publishing is enabled\n            try:\n                if \"workfile\" in instance.data[\"families\"]:\n                    instance.data[\"publish\"] = True\n                # TODO merge renderFarm and render.farm\n                if (\"renderFarm\" in instance.data[\"families\"] or\n                        \"render.farm\" in instance.data[\"families\"]):\n                    instance.data[\"remove\"] = True\n            except KeyError:\n                # be tolerant if 'families' is missing.\n                pass\n\n        self._file_path = context.data[\"currentFile\"].replace(\"\\\\\", \"/\")\n\n        render_instances = self.get_instances(context)\n        for render_instance in render_instances:\n            exp_files = self.get_expected_files(render_instance)\n            assert exp_files, \"no file names were generated, this is bug\"\n\n            # if we want to attach render to subset, check if we have AOV's\n            # in expectedFiles. If so, raise error as we cannot attach AOV\n            # (considered to be subset on its own) to another subset\n            if render_instance.attachTo:\n                assert isinstance(exp_files, list), (\n                    \"attaching multiple AOVs or renderable cameras to \"\n                    \"subset is not supported\"\n                )\n\n            frame_start_render = int(render_instance.frameStart)\n            frame_end_render = int(render_instance.frameEnd)\n            # TODO: Refactor hacky frame range workaround below\n            if (render_instance.ignoreFrameHandleCheck or\n                    int(context.data['frameStartHandle']) == frame_start_render\n                    and int(context.data['frameEndHandle']) == frame_end_render):  # noqa: W503, E501\n                # only for Harmony where frame range cannot be set by DB\n                handle_start = context.data['handleStart']\n                handle_end = context.data['handleEnd']\n                frame_start = context.data['frameStart']\n                frame_end = context.data['frameEnd']\n                frame_start_handle = context.data['frameStartHandle']\n                frame_end_handle = context.data['frameEndHandle']\n            elif (hasattr(render_instance, \"frameStartHandle\")\n                  and hasattr(render_instance, \"frameEndHandle\")):\n                handle_start = int(render_instance.handleStart)\n                handle_end = int(render_instance.handleEnd)\n                frame_start = int(render_instance.frameStart)\n                frame_end = int(render_instance.frameEnd)\n                frame_start_handle = int(render_instance.frameStartHandle)\n                frame_end_handle = int(render_instance.frameEndHandle)\n            else:\n                handle_start = 0\n                handle_end = 0\n                frame_start = frame_start_render\n                frame_end = frame_end_render\n                frame_start_handle = frame_start_render\n                frame_end_handle = frame_end_render\n\n            data = {\n                \"handleStart\": handle_start,\n                \"handleEnd\": handle_end,\n                \"frameStart\": frame_start,\n                \"frameEnd\": frame_end,\n                \"frameStartHandle\": frame_start_handle,\n                \"frameEndHandle\": frame_end_handle,\n                \"byFrameStep\": int(render_instance.frameStep),\n\n                \"author\": context.data[\"user\"],\n                # Add source to allow tracing back to the scene from\n                # which was submitted originally\n                \"expectedFiles\": exp_files,\n            }\n            if self.sync_workfile_version:\n                data[\"version\"] = context.data[\"version\"]\n\n            # add additional data\n            data = self.add_additional_data(data)\n            render_instance_dict = attr.asdict(render_instance)\n\n            instance = context.create_instance(render_instance.name)\n            instance.data[\"label\"] = render_instance.label\n            instance.data.update(render_instance_dict)\n            instance.data.update(data)\n\n        self.post_collecting_action()\n\n    @abstractmethod\n    def get_instances(self, context):\n        \"\"\"Get all renderable instances and their data.\n\n        Args:\n            context (pyblish.api.Context): Context object.\n\n        Returns:\n            list of :class:`RenderInstance`: All collected renderable instances\n                (like render layers, write nodes, etc.)\n\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_expected_files(self, render_instance):\n        \"\"\"Get list of expected files.\n\n        Returns:\n            list: expected files. This can be either simple list of files with\n                their paths, or list of dictionaries, where key is name of AOV\n                for example and value is list of files for that AOV.\n\n        Example::\n\n            ['/path/to/file.001.exr', '/path/to/file.002.exr']\n\n            or as dictionary:\n\n            [\n                {\n                    \"beauty\": ['/path/to/beauty.001.exr', ...],\n                    \"mask\": ['/path/to/mask.001.exr']\n                }\n            ]\n\n        \"\"\"\n        pass\n\n    def add_additional_data(self, data):\n        \"\"\"Add additional data to collected instance.\n\n        This can be overridden by host implementation to add custom\n        additional data.\n\n        \"\"\"\n        return data\n\n    def post_collecting_action(self):\n        \"\"\"Execute some code after collection is done.\n\n        This is useful for example for restoring current render layer.\n\n        \"\"\"\n        pass\n"
  },
  {
    "path": "openpype/pipeline/publish/abstract_expected_files.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Abstract ExpectedFile class definition.\"\"\"\nfrom abc import ABCMeta, abstractmethod\nimport six\n\n\n@six.add_metaclass(ABCMeta)\nclass ExpectedFiles:\n    \"\"\"Class grouping functionality for all supported renderers.\n\n    Attributes:\n        multipart (bool): Flag if multipart exrs are used.\n\n    \"\"\"\n\n    multipart = False\n\n    @abstractmethod\n    def get(self, render_instance):\n        \"\"\"Get expected files for given renderer and render layer.\n\n        This method should return dictionary of all files we are expecting\n        to be rendered from the host. Usually `render_instance` corresponds\n        to *render layer*. Result can be either flat list with the file\n        paths or it can be list of dictionaries. Each key corresponds to\n        for example AOV name or channel, etc.\n\n        Example::\n\n            ['/path/to/file.001.exr', '/path/to/file.002.exr']\n\n            or as dictionary:\n\n            [\n                {\n                    \"beauty\": ['/path/to/beauty.001.exr', ...],\n                    \"mask\": ['/path/to/mask.001.exr']\n                }\n            ]\n\n\n        Args:\n            render_instance (:class:`RenderInstance`): Data passed from\n                collector to determine files. This should be instance of\n                :class:`abstract_collect_render.RenderInstance`\n\n        Returns:\n            list: Full paths to expected rendered files.\n            list of dict: Path to expected rendered files categorized by\n                AOVs, etc.\n\n        \"\"\"\n        raise NotImplementedError()\n"
  },
  {
    "path": "openpype/pipeline/publish/constants.py",
    "content": "import pyblish.api\n\n\nValidatePipelineOrder = pyblish.api.ValidatorOrder + 0.05\nValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1\nValidateSceneOrder = pyblish.api.ValidatorOrder + 0.2\nValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3\n\nDEFAULT_PUBLISH_TEMPLATE = \"publish\"\nDEFAULT_HERO_PUBLISH_TEMPLATE = \"hero\"\nTRANSIENT_DIR_TEMPLATE = \"transient\"\n"
  },
  {
    "path": "openpype/pipeline/publish/lib.py",
    "content": "import os\nimport sys\nimport inspect\nimport copy\nimport tempfile\nimport xml.etree.ElementTree\n\nimport pyblish.util\nimport pyblish.plugin\nimport pyblish.api\n\nfrom openpype.lib import (\n    Logger,\n    import_filepath,\n    filter_profiles,\n    is_func_signature_supported,\n)\nfrom openpype.settings import (\n    get_project_settings,\n    get_system_settings,\n)\nfrom openpype.pipeline import (\n    tempdir,\n    Anatomy\n)\nfrom openpype.pipeline.plugin_discover import DiscoverResult\n\nfrom .constants import (\n    DEFAULT_PUBLISH_TEMPLATE,\n    DEFAULT_HERO_PUBLISH_TEMPLATE,\n    TRANSIENT_DIR_TEMPLATE\n)\n\n\ndef get_template_name_profiles(\n    project_name, project_settings=None, logger=None\n):\n    \"\"\"Receive profiles for publish template keys.\n\n    At least one of arguments must be passed.\n\n    Args:\n        project_name (str): Name of project where to look for templates.\n        project_settings (Dict[str, Any]): Prepared project settings.\n        logger (Optional[logging.Logger]): Logger object to be used instead\n            of default logger.\n\n    Returns:\n        List[Dict[str, Any]]: Publish template profiles.\n    \"\"\"\n\n    if not project_name and not project_settings:\n        raise ValueError((\n            \"Both project name and project settings are missing.\"\n            \" At least one must be entered.\"\n        ))\n\n    if not project_settings:\n        project_settings = get_project_settings(project_name)\n\n    return copy.deepcopy(\n        project_settings\n        [\"global\"]\n        [\"tools\"]\n        [\"publish\"]\n        [\"template_name_profiles\"]\n    )\n\n\ndef get_hero_template_name_profiles(\n    project_name, project_settings=None, logger=None\n):\n    \"\"\"Receive profiles for hero publish template keys.\n\n    At least one of arguments must be passed.\n\n    Args:\n        project_name (str): Name of project where to look for templates.\n        project_settings (Dict[str, Any]): Prepared project settings.\n        logger (Optional[logging.Logger]): Logger object to be used instead\n            of default logger.\n\n    Returns:\n        List[Dict[str, Any]]: Publish template profiles.\n    \"\"\"\n\n    if not project_name and not project_settings:\n        raise ValueError((\n            \"Both project name and project settings are missing.\"\n            \" At least one must be entered.\"\n        ))\n\n    if not project_settings:\n        project_settings = get_project_settings(project_name)\n\n    return copy.deepcopy(\n        project_settings\n        [\"global\"]\n        [\"tools\"]\n        [\"publish\"]\n        [\"hero_template_name_profiles\"]\n    )\n\n\ndef get_publish_template_name(\n    project_name,\n    host_name,\n    family,\n    task_name,\n    task_type,\n    project_settings=None,\n    hero=False,\n    logger=None\n):\n    \"\"\"Get template name which should be used for passed context.\n\n    Publish templates are filtered by host name, family, task name and\n    task type.\n\n    Default template which is used at if profiles are not available or profile\n    has empty value is defined by 'DEFAULT_PUBLISH_TEMPLATE' constant.\n\n    Args:\n        project_name (str): Name of project where to look for settings.\n        host_name (str): Name of host integration.\n        family (str): Family for which should be found template.\n        task_name (str): Task name on which is instance working.\n        task_type (str): Task type on which is instance working.\n        project_settings (Dict[str, Any]): Prepared project settings.\n        hero (bool): Template is for hero version publishing.\n        logger (logging.Logger): Custom logger used for 'filter_profiles'\n            function.\n\n    Returns:\n        str: Template name which should be used for integration.\n    \"\"\"\n\n    template = None\n    filter_criteria = {\n        \"hosts\": host_name,\n        \"families\": family,\n        \"task_names\": task_name,\n        \"task_types\": task_type,\n    }\n    if hero:\n        default_template = DEFAULT_HERO_PUBLISH_TEMPLATE\n        profiles = get_hero_template_name_profiles(\n            project_name, project_settings, logger\n        )\n\n    else:\n        profiles = get_template_name_profiles(\n            project_name, project_settings, logger\n        )\n        default_template = DEFAULT_PUBLISH_TEMPLATE\n\n    profile = filter_profiles(profiles, filter_criteria, logger=logger)\n    if profile:\n        template = profile[\"template_name\"]\n    return template or default_template\n\n\nclass HelpContent:\n    def __init__(self, title, description, detail=None):\n        self.title = title\n        self.description = description\n        self.detail = detail\n\n\ndef load_help_content_from_filepath(filepath):\n    \"\"\"Load help content from xml file.\n    Xml file may containt errors and warnings.\n    \"\"\"\n    errors = {}\n    warnings = {}\n    output = {\n        \"errors\": errors,\n        \"warnings\": warnings\n    }\n    if not os.path.exists(filepath):\n        return output\n    tree = xml.etree.ElementTree.parse(filepath)\n    root = tree.getroot()\n    for child in root:\n        child_id = child.attrib.get(\"id\")\n        if child_id is None:\n            continue\n\n        # Make sure ID is string\n        child_id = str(child_id)\n\n        title = child.find(\"title\").text\n        description = child.find(\"description\").text\n        detail_node = child.find(\"detail\")\n        detail = None\n        if detail_node is not None:\n            detail = detail_node.text\n        if child.tag == \"error\":\n            errors[child_id] = HelpContent(title, description, detail)\n        elif child.tag == \"warning\":\n            warnings[child_id] = HelpContent(title, description, detail)\n    return output\n\n\ndef load_help_content_from_plugin(plugin):\n    cls = plugin\n    if not inspect.isclass(plugin):\n        cls = plugin.__class__\n    plugin_filepath = inspect.getfile(cls)\n    plugin_dir = os.path.dirname(plugin_filepath)\n    basename = os.path.splitext(os.path.basename(plugin_filepath))[0]\n    filename = basename + \".xml\"\n    filepath = os.path.join(plugin_dir, \"help\", filename)\n    return load_help_content_from_filepath(filepath)\n\n\ndef publish_plugins_discover(paths=None):\n    \"\"\"Find and return available pyblish plug-ins\n\n    Overridden function from `pyblish` module to be able to collect\n        crashed files and reason of their crash.\n\n    Arguments:\n        paths (list, optional): Paths to discover plug-ins from.\n            If no paths are provided, all paths are searched.\n    \"\"\"\n\n    # The only difference with `pyblish.api.discover`\n    result = DiscoverResult(pyblish.api.Plugin)\n\n    plugins = {}\n    plugin_names = []\n\n    allow_duplicates = pyblish.plugin.ALLOW_DUPLICATES\n    log = pyblish.plugin.log\n\n    # Include plug-ins from registered paths\n    if not paths:\n        paths = pyblish.plugin.plugin_paths()\n\n    for path in paths:\n        path = os.path.normpath(path)\n        if not os.path.isdir(path):\n            continue\n\n        for fname in os.listdir(path):\n            if fname.startswith(\"_\"):\n                continue\n\n            abspath = os.path.join(path, fname)\n\n            if not os.path.isfile(abspath):\n                continue\n\n            mod_name, mod_ext = os.path.splitext(fname)\n\n            if mod_ext != \".py\":\n                continue\n\n            try:\n                module = import_filepath(abspath, mod_name)\n\n                # Store reference to original module, to avoid\n                # garbage collection from collecting it's global\n                # imports, such as `import os`.\n                sys.modules[abspath] = module\n\n            except Exception as err:\n                result.crashed_file_paths[abspath] = sys.exc_info()\n\n                log.debug(\"Skipped: \\\"%s\\\" (%s)\", mod_name, err)\n                continue\n\n            for plugin in pyblish.plugin.plugins_from_module(module):\n                # Ignore base plugin classes\n                # NOTE 'pyblish.api.discover' does not ignore them!\n                if (\n                    plugin is pyblish.api.Plugin\n                    or plugin is pyblish.api.ContextPlugin\n                    or plugin is pyblish.api.InstancePlugin\n                ):\n                    continue\n                if not allow_duplicates and plugin.__name__ in plugin_names:\n                    result.duplicated_plugins.append(plugin)\n                    log.debug(\"Duplicate plug-in found: %s\", plugin)\n                    continue\n\n                plugin_names.append(plugin.__name__)\n\n                plugin.__module__ = module.__file__\n                key = \"{0}.{1}\".format(plugin.__module__, plugin.__name__)\n                plugins[key] = plugin\n\n    # Include plug-ins from registration.\n    # Directly registered plug-ins take precedence.\n    for plugin in pyblish.plugin.registered_plugins():\n        if not allow_duplicates and plugin.__name__ in plugin_names:\n            result.duplicated_plugins.append(plugin)\n            log.debug(\"Duplicate plug-in found: %s\", plugin)\n            continue\n\n        plugin_names.append(plugin.__name__)\n\n        plugins[plugin.__name__] = plugin\n\n    plugins = list(plugins.values())\n    pyblish.plugin.sort(plugins)  # In-place\n\n    # In-place user-defined filter\n    for filter_ in pyblish.plugin._registered_plugin_filters:\n        filter_(plugins)\n\n    result.plugins = plugins\n\n    return result\n\n\ndef get_plugin_settings(plugin, project_settings, log, category=None):\n    \"\"\"Get plugin settings based on host name and plugin name.\n\n    Note:\n        Default implementation of automated settings is passing host name\n            into 'category'.\n\n    Args:\n        plugin (pyblish.Plugin): Plugin where settings are applied.\n        project_settings (dict[str, Any]): Project settings.\n        log (logging.Logger): Logger to log messages.\n        category (Optional[str]): Settings category key where to look\n            for plugin settings.\n\n    Returns:\n        dict[str, Any]: Plugin settings {'attribute': 'value'}.\n    \"\"\"\n\n    # Plugin can define settings category by class attribute\n    # - it's impossible to set `settings_category` via settings because\n    #     obviously settings are not applied before it.\n    # - if `settings_category` is set the fallback category method is ignored\n    settings_category = getattr(plugin, \"settings_category\", None)\n    if settings_category:\n        try:\n            return (\n                project_settings\n                [settings_category]\n                [\"publish\"]\n                [plugin.__name__]\n            )\n        except KeyError:\n            log.warning((\n                \"Couldn't find plugin '{}' settings\"\n                \" under settings category '{}'\"\n            ).format(plugin.__name__, settings_category))\n            return {}\n\n    # Use project settings based on a category name\n    if category:\n        try:\n            return (\n                project_settings\n                [category]\n                [\"publish\"]\n                [plugin.__name__]\n            )\n        except KeyError:\n            pass\n\n    # Settings category determined from path\n    # - usually path is './<category>/plugins/publish/<plugin file>'\n    # - category can be host name of addon name ('maya', 'deadline', ...)\n    filepath = os.path.normpath(inspect.getsourcefile(plugin))\n\n    split_path = filepath.rsplit(os.path.sep, 5)\n    if len(split_path) < 4:\n        log.debug((\n            \"Plugin path is too short to automatically\"\n            \" extract settings category. {}\"\n        ).format(filepath))\n        return {}\n\n    category_from_file = split_path[-4]\n    plugin_kind = split_path[-2]\n\n    # TODO: change after all plugins are moved one level up\n    if category_from_file == \"openpype\":\n        category_from_file = \"global\"\n\n    try:\n        return (\n            project_settings\n            [category_from_file]\n            [plugin_kind]\n            [plugin.__name__]\n        )\n    except KeyError:\n        pass\n    return {}\n\n\ndef apply_plugin_settings_automatically(plugin, settings, logger=None):\n    \"\"\"Automatically apply plugin settings to a plugin object.\n\n    Note:\n        This function was created to be able to use it in custom overrides of\n            'apply_settings' class method.\n\n    Args:\n        plugin (type[pyblish.api.Plugin]): Class of a plugin.\n        settings (dict[str, Any]): Plugin specific settings.\n        logger (Optional[logging.Logger]): Logger to log debug messages about\n            applied settings values.\n    \"\"\"\n\n    for option, value in settings.items():\n        if logger:\n            logger.debug(\"Plugin %s - Attr: %s -> %s\",\n                         plugin.__name__, option, value)\n        setattr(plugin, option, value)\n\n\ndef filter_pyblish_plugins(plugins):\n    \"\"\"Pyblish plugin filter which applies OpenPype settings.\n\n    Apply OpenPype settings on discovered plugins. On plugin with implemented\n    class method 'def apply_settings(cls, project_settings, system_settings)'\n    is called the method. Default behavior looks for plugin name and current\n    host name to look for\n\n    Args:\n        plugins (List[pyblish.plugin.Plugin]): Discovered plugins on which\n            are applied settings.\n    \"\"\"\n\n    log = Logger.get_logger(\"filter_pyblish_plugins\")\n\n    # TODO: Don't use host from 'pyblish.api' but from defined host by us.\n    #   - kept becau on farm is probably used host 'shell' which propably\n    #       affect how settings are applied there\n    host_name = pyblish.api.current_host()\n    project_name = os.environ.get(\"AVALON_PROJECT\")\n\n    project_settings = get_project_settings(project_name)\n    system_settings = get_system_settings()\n\n    # iterate over plugins\n    for plugin in plugins[:]:\n        # Apply settings to plugins\n\n        apply_settings_func = getattr(plugin, \"apply_settings\", None)\n        if apply_settings_func is not None:\n            # Use classmethod 'apply_settings'\n            # - can be used to target settings from custom settings place\n            # - skip default behavior when successful\n            try:\n                # Support to pass only project settings\n                # - make sure that both settings are passed, when can be\n                #   - that covers cases when *args are in method parameters\n                both_supported = is_func_signature_supported(\n                    apply_settings_func, project_settings, system_settings\n                )\n                project_supported = is_func_signature_supported(\n                    apply_settings_func, project_settings\n                )\n                if not both_supported and project_supported:\n                    plugin.apply_settings(project_settings)\n                else:\n                    plugin.apply_settings(project_settings, system_settings)\n\n            except Exception:\n                log.warning(\n                    (\n                        \"Failed to apply settings on plugin {}\"\n                    ).format(plugin.__name__),\n                    exc_info=True\n                )\n        else:\n            # Automated\n            plugin_settins = get_plugin_settings(\n                plugin, project_settings, log, host_name\n            )\n            apply_plugin_settings_automatically(plugin, plugin_settins, log)\n\n        # Remove disabled plugins\n        if getattr(plugin, \"enabled\", True) is False:\n            plugins.remove(plugin)\n\n\ndef remote_publish(log):\n    \"\"\"Loops through all plugins, logs to console. Used for tests.\n\n    Args:\n        log (Logger)\n    \"\"\"\n\n    # Error exit as soon as any error occurs.\n    error_format = \"Failed {plugin.__name__}: {error}\\n{error.traceback}\"\n\n    for result in pyblish.util.publish_iter():\n        if not result[\"error\"]:\n            continue\n\n        error_message = error_format.format(**result)\n        log.error(error_message)\n        # 'Fatal Error: ' is because of Deadline\n        raise RuntimeError(\"Fatal Error: {}\".format(error_message))\n\n\ndef get_errored_instances_from_context(context, plugin=None):\n    \"\"\"Collect failed instances from pyblish context.\n\n    Args:\n        context (pyblish.api.Context): Publish context where we're looking\n            for failed instances.\n        plugin (pyblish.api.Plugin): If provided then only consider errors\n            related to that plug-in.\n\n    Returns:\n        List[pyblish.lib.Instance]: Instances which failed during processing.\n    \"\"\"\n\n    instances = list()\n    for result in context.data[\"results\"]:\n        if result[\"instance\"] is None:\n            # When instance is None we are on the \"context\" result\n            continue\n\n        if plugin is not None and result.get(\"plugin\") != plugin:\n            continue\n\n        if result[\"error\"]:\n            instances.append(result[\"instance\"])\n\n    return instances\n\n\ndef get_errored_plugins_from_context(context):\n    \"\"\"Collect failed plugins from pyblish context.\n\n    Args:\n        context (pyblish.api.Context): Publish context where we're looking\n            for failed plugins.\n\n    Returns:\n        List[pyblish.api.Plugin]: Plugins which failed during processing.\n    \"\"\"\n\n    plugins = list()\n    results = context.data.get(\"results\", [])\n    for result in results:\n        if result[\"success\"] is True:\n            continue\n        plugins.append(result[\"plugin\"])\n\n    return plugins\n\n\ndef filter_instances_for_context_plugin(plugin, context):\n    \"\"\"Filter instances on context by context plugin filters.\n\n    This is for cases when context plugin need similar filtering like instance\n    plugin have, but for some reason must run on context or should find out\n    if there is at least one instance with a family.\n\n    Args:\n        plugin (pyblish.api.Plugin): Plugin with filters.\n        context (pyblish.api.Context): Pyblish context with insances.\n\n    Returns:\n        Iterator[pyblish.lib.Instance]: Iteration of valid instances.\n    \"\"\"\n\n    instances = []\n    plugin_families = set()\n    all_families = False\n    if plugin.families:\n        instances = context\n        plugin_families = set(plugin.families)\n        all_families = \"*\" in plugin_families\n\n    for instance in instances:\n        # Ignore inactive instances\n        if (\n            not instance.data.get(\"publish\", True)\n            or not instance.data.get(\"active\", True)\n        ):\n            continue\n\n        family = instance.data.get(\"family\")\n        families = instance.data.get(\"families\") or []\n        if (\n            all_families\n            or (family and family in plugin_families)\n            or any(f in plugin_families for f in families)\n        ):\n            yield instance\n\n\ndef context_plugin_should_run(plugin, context):\n    \"\"\"Return whether the ContextPlugin should run on the given context.\n\n    This is a helper function to work around a bug pyblish-base#250\n    Whenever a ContextPlugin sets specific families it will still trigger even\n    when no instances are present that have those families.\n\n    This actually checks it correctly and returns whether it should run.\n\n    Args:\n        plugin (pyblish.api.Plugin): Plugin with filters.\n        context (pyblish.api.Context): Pyblish context with instances.\n\n    Returns:\n        bool: Context plugin should run based on valid instances.\n    \"\"\"\n\n    for _ in filter_instances_for_context_plugin(plugin, context):\n        return True\n    return False\n\n\ndef get_instance_staging_dir(instance):\n    \"\"\"Unified way how staging dir is stored and created on instances.\n\n    First check if 'stagingDir' is already set in instance data.\n    In case there already is new tempdir will not be created.\n\n    It also supports `OPENPYPE_TMPDIR`, so studio can define own temp\n    shared repository per project or even per more granular context.\n    Template formatting is supported also with optional keys. Folder is\n    created in case it doesn't exists.\n\n    Available anatomy formatting keys:\n        - root[work | <root name key>]\n        - project[name | code]\n\n    Note:\n        Staging dir does not have to be necessarily in tempdir so be careful\n        about its usage.\n\n    Args:\n        instance (pyblish.lib.Instance): Instance for which we want to get\n            staging dir.\n\n    Returns:\n        str: Path to staging dir of instance.\n    \"\"\"\n    staging_dir = instance.data.get('stagingDir')\n    if staging_dir:\n        return staging_dir\n\n    anatomy = instance.context.data.get(\"anatomy\")\n\n    # get customized tempdir path from `OPENPYPE_TMPDIR` env var\n    custom_temp_dir = tempdir.create_custom_tempdir(\n        anatomy.project_name, anatomy)\n\n    if custom_temp_dir:\n        staging_dir = os.path.normpath(\n            tempfile.mkdtemp(\n                prefix=\"pyblish_tmp_\",\n                dir=custom_temp_dir\n            )\n        )\n    else:\n        staging_dir = os.path.normpath(\n            tempfile.mkdtemp(prefix=\"pyblish_tmp_\")\n        )\n    instance.data['stagingDir'] = staging_dir\n\n    return staging_dir\n\n\ndef get_publish_repre_path(instance, repre, only_published=False):\n    \"\"\"Get representation path that can be used for integration.\n\n    When 'only_published' is set to true the validation of path is not\n    relevant. In that case we just need what is set in 'published_path'\n    as \"reference\". The reference is not used to get or upload the file but\n    for reference where the file was published.\n\n    Args:\n        instance (pyblish.Instance): Processed instance object. Used\n            for source of staging dir if representation does not have\n            filled it.\n        repre (dict): Representation on instance which could be and\n            could not be integrated with main integrator.\n        only_published (bool): Care only about published paths and\n            ignore if filepath is not existing anymore.\n\n    Returns:\n        str: Path to representation file.\n        None: Path is not filled or does not exists.\n    \"\"\"\n\n    published_path = repre.get(\"published_path\")\n    if published_path:\n        published_path = os.path.normpath(published_path)\n        if os.path.exists(published_path):\n            return published_path\n\n    if only_published:\n        return published_path\n\n    comp_files = repre[\"files\"]\n    if isinstance(comp_files, (tuple, list, set)):\n        filename = comp_files[0]\n    else:\n        filename = comp_files\n\n    staging_dir = repre.get(\"stagingDir\")\n    if not staging_dir:\n        staging_dir = get_instance_staging_dir(instance)\n\n    # Expand the staging dir path in case it's been stored with the root\n    # template syntax\n    anatomy = instance.context.data[\"anatomy\"]\n    staging_dir = anatomy.fill_root(staging_dir)\n\n    src_path = os.path.normpath(os.path.join(staging_dir, filename))\n    if os.path.exists(src_path):\n        return src_path\n    return None\n\n\ndef get_custom_staging_dir_info(project_name, host_name, family, task_name,\n                                task_type, subset_name,\n                                project_settings=None,\n                                anatomy=None, log=None):\n    \"\"\"Checks profiles if context should use special custom dir as staging.\n\n    Args:\n        project_name (str)\n        host_name (str)\n        family (str)\n        task_name (str)\n        task_type (str)\n        subset_name (str)\n        project_settings(Dict[str, Any]): Prepared project settings.\n        anatomy (Dict[str, Any])\n        log (Logger) (optional)\n\n    Returns:\n        (tuple)\n    Raises:\n        ValueError - if misconfigured template should be used\n    \"\"\"\n    settings = project_settings or get_project_settings(project_name)\n    custom_staging_dir_profiles = (settings[\"global\"]\n                                           [\"tools\"]\n                                           [\"publish\"]\n                                           [\"custom_staging_dir_profiles\"])\n    if not custom_staging_dir_profiles:\n        return None, None\n\n    if not log:\n        log = Logger.get_logger(\"get_custom_staging_dir_info\")\n\n    filtering_criteria = {\n        \"hosts\": host_name,\n        \"families\": family,\n        \"task_names\": task_name,\n        \"task_types\": task_type,\n        \"subsets\": subset_name\n    }\n    profile = filter_profiles(custom_staging_dir_profiles,\n                              filtering_criteria,\n                              logger=log)\n\n    if not profile or not profile[\"active\"]:\n        return None, None\n\n    if not anatomy:\n        anatomy = Anatomy(project_name)\n\n    template_name = profile[\"template_name\"] or TRANSIENT_DIR_TEMPLATE\n    _validate_transient_template(project_name, template_name, anatomy)\n\n    custom_staging_dir = anatomy.templates[template_name][\"folder\"]\n    is_persistent = profile[\"custom_staging_dir_persistent\"]\n\n    return custom_staging_dir, is_persistent\n\n\ndef _validate_transient_template(project_name, template_name, anatomy):\n    \"\"\"Check that transient template is correctly configured.\n\n    Raises:\n        ValueError - if misconfigured template\n    \"\"\"\n    if template_name not in anatomy.templates:\n        raise ValueError((\"Anatomy of project \\\"{}\\\" does not have set\"\n                          \" \\\"{}\\\" template key!\"\n                          ).format(project_name, template_name))\n\n    if \"folder\" not in anatomy.templates[template_name]:\n        raise ValueError((\"There is not set \\\"folder\\\" template in \\\"{}\\\" anatomy\"  # noqa\n                             \" for project \\\"{}\\\".\"\n                         ).format(template_name, project_name))\n\n\ndef get_published_workfile_instance(context):\n    \"\"\"Find workfile instance in context\"\"\"\n    for i in context:\n        is_workfile = (\n            \"workfile\" in i.data.get(\"families\", []) or\n            i.data[\"family\"] == \"workfile\"\n        )\n        if not is_workfile:\n            continue\n\n        # test if there is instance of workfile waiting\n        # to be published.\n        if not i.data.get(\"publish\", True):\n            continue\n\n        return i\n\n\ndef replace_with_published_scene_path(instance, replace_in_path=True):\n    \"\"\"Switch work scene path for published scene.\n    If rendering/exporting from published scenes is enabled, this will\n    replace paths from working scene to published scene.\n    This only works if publish contains workfile instance!\n    Args:\n        instance (pyblish.api.Instance): Pyblish instance.\n        replace_in_path (bool): if True, it will try to find\n            old scene name in path of expected files and replace it\n            with name of published scene.\n    Returns:\n        str: Published scene path.\n        None: if no published scene is found.\n    Note:\n        Published scene path is actually determined from project Anatomy\n        as at the time this plugin is running scene can still not be\n        published.\n    \"\"\"\n    log = Logger.get_logger(\"published_workfile\")\n    workfile_instance = get_published_workfile_instance(instance.context)\n    if workfile_instance is None:\n        return\n\n    # determine published path from Anatomy.\n    template_data = workfile_instance.data.get(\"anatomyData\")\n    rep = workfile_instance.data[\"representations\"][0]\n    template_data[\"representation\"] = rep.get(\"name\")\n    template_data[\"ext\"] = rep.get(\"ext\")\n    template_data[\"comment\"] = None\n\n    anatomy = instance.context.data['anatomy']\n    anatomy_filled = anatomy.format(template_data)\n    template_filled = anatomy_filled[\"publish\"][\"path\"]\n    file_path = os.path.normpath(template_filled)\n\n    log.info(\"Using published scene for render {}\".format(file_path))\n\n    if not os.path.exists(file_path):\n        log.error(\"published scene does not exist!\")\n        raise\n\n    if not replace_in_path:\n        return file_path\n\n    # now we need to switch scene in expected files\n    # because <scene> token will now point to published\n    # scene file and that might differ from current one\n    def _clean_name(path):\n        return os.path.splitext(os.path.basename(path))[0]\n\n    new_scene = _clean_name(file_path)\n    orig_scene = _clean_name(instance.context.data[\"currentFile\"])\n    expected_files = instance.data.get(\"expectedFiles\")\n\n    if isinstance(expected_files[0], dict):\n        # we have aovs and we need to iterate over them\n        new_exp = {}\n        for aov, files in expected_files[0].items():\n            replaced_files = []\n            for f in files:\n                replaced_files.append(\n                    str(f).replace(orig_scene, new_scene)\n                )\n            new_exp[aov] = replaced_files\n        # [] might be too much here, TODO\n        instance.data[\"expectedFiles\"] = [new_exp]\n    else:\n        new_exp = []\n        for f in expected_files:\n            new_exp.append(\n                str(f).replace(orig_scene, new_scene)\n            )\n        instance.data[\"expectedFiles\"] = new_exp\n\n    metadata_folder = instance.data.get(\"publishRenderMetadataFolder\")\n    if metadata_folder:\n        metadata_folder = metadata_folder.replace(orig_scene,\n                                                  new_scene)\n        instance.data[\"publishRenderMetadataFolder\"] = metadata_folder\n\n    log.info(\"Scene name was switched {} -> {}\".format(\n        orig_scene, new_scene\n    ))\n\n    return file_path\n\n\ndef add_repre_files_for_cleanup(instance, repre):\n    \"\"\" Explicitly mark repre files to be deleted.\n\n    Should be used on intermediate files (eg. review, thumbnails) to be\n    explicitly deleted.\n    \"\"\"\n    files = repre[\"files\"]\n    staging_dir = repre.get(\"stagingDir\")\n\n    # first make sure representation level is not persistent\n    if (\n        not staging_dir\n        or repre.get(\"stagingDir_persistent\")\n    ):\n        return\n\n    # then look into instance level if it's not persistent\n    if instance.data.get(\"stagingDir_persistent\"):\n        return\n\n    if isinstance(files, str):\n        files = [files]\n\n    for file_name in files:\n        expected_file = os.path.join(staging_dir, file_name)\n        instance.context.data[\"cleanupFullPaths\"].append(expected_file)\n\n\ndef get_publish_instance_label(instance):\n    \"\"\"Try to get label from pyblish instance.\n\n    First are used values in instance data under 'label' and 'name' keys. Then\n    is used string conversion of instance object -> 'instance._name'.\n\n    Todos:\n        Maybe 'subset' key could be used too.\n\n    Args:\n        instance (pyblish.api.Instance): Pyblish instance.\n\n    Returns:\n        str: Instance label.\n    \"\"\"\n\n    return (\n        instance.data.get(\"label\")\n        or instance.data.get(\"name\")\n        or str(instance)\n    )\n\n\ndef get_publish_instance_families(instance):\n    \"\"\"Get all families of the instance.\n\n    Look for families under 'family' and 'families' keys in instance data.\n    Value of 'family' is used as first family and then all other families\n    in random order.\n\n    Args:\n        pyblish.api.Instance: Instance to get families from.\n\n    Returns:\n        list[str]: List of families.\n    \"\"\"\n\n    family = instance.data.get(\"family\")\n    families = set(instance.data.get(\"families\") or [])\n    output = []\n    if family:\n        output.append(family)\n        families.discard(family)\n    output.extend(families)\n    return output\n"
  },
  {
    "path": "openpype/pipeline/publish/publish_plugins.py",
    "content": "import inspect\nfrom abc import ABCMeta\nimport pyblish.api\nfrom pyblish.plugin import MetaPlugin, ExplicitMetaPlugin\nfrom openpype.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS\nfrom openpype.lib import BoolDef\n\nfrom .lib import (\n    load_help_content_from_plugin,\n    get_errored_instances_from_context,\n    get_errored_plugins_from_context,\n    get_instance_staging_dir,\n)\n\nfrom openpype.pipeline.colorspace import (\n    get_colorspace_settings_from_publish_context,\n    set_colorspace_data_to_representation\n)\n\n\nclass AbstractMetaInstancePlugin(ABCMeta, MetaPlugin):\n    pass\n\n\nclass AbstractMetaContextPlugin(ABCMeta, ExplicitMetaPlugin):\n    pass\n\n\nclass PublishValidationError(Exception):\n    \"\"\"Validation error happened during publishing.\n\n    This exception should be used when validation publishing failed.\n\n    Has additional UI specific attributes that may be handy for artist.\n\n    Args:\n        message(str): Message of error. Short explanation an issue.\n        title(str): Title showed in UI. All instances are grouped under\n            single title.\n        description(str): Detailed description of an error. It is possible\n            to use Markdown syntax.\n    \"\"\"\n\n    def __init__(self, message, title=None, description=None, detail=None):\n        self.message = message\n        self.title = title\n        self.description = description or message\n        self.detail = detail\n        super(PublishValidationError, self).__init__(message)\n\n\nclass PublishXmlValidationError(PublishValidationError):\n    def __init__(\n        self, plugin, message, key=None, formatting_data=None\n    ):\n        if key is None:\n            key = \"main\"\n\n        if not formatting_data:\n            formatting_data = {}\n        result = load_help_content_from_plugin(plugin)\n        content_obj = result[\"errors\"][key]\n        description = content_obj.description.format(**formatting_data)\n        detail = content_obj.detail\n        if detail:\n            detail = detail.format(**formatting_data)\n        super(PublishXmlValidationError, self).__init__(\n            message, content_obj.title, description, detail\n        )\n\n\nclass KnownPublishError(Exception):\n    \"\"\"Publishing crashed because of known error.\n\n    Message will be shown in UI for artist.\n    \"\"\"\n\n    pass\n\n\nclass OpenPypePyblishPluginMixin:\n    # TODO\n    # executable_in_thread = False\n    #\n    # state_message = None\n    # state_percent = None\n    # _state_change_callbacks = []\n    #\n    # def set_state(self, percent=None, message=None):\n    #     \"\"\"Inner callback of plugin that would help to show in UI state.\n    #\n    #     Plugin have registered callbacks on state change which could trigger\n    #     update message and percent in UI and repaint the change.\n    #\n    #     This part must be optional and should not be used to display errors\n    #     or for logging.\n    #\n    #     Message should be short without details.\n    #\n    #     Args:\n    #         percent(int): Percent of processing in range <1-100>.\n    #         message(str): Message which will be shown to user (if in UI).\n    #     \"\"\"\n    #     if percent is not None:\n    #         self.state_percent = percent\n    #\n    #     if message:\n    #         self.state_message = message\n    #\n    #     for callback in self._state_change_callbacks:\n    #         callback(self)\n\n    @classmethod\n    def get_attribute_defs(cls):\n        \"\"\"Publish attribute definitions.\n\n        Attributes available for all families in plugin's `families` attribute.\n        Returns:\n            list<AbstractAttrDef>: Attribute definitions for plugin.\n        \"\"\"\n\n        return []\n\n    @classmethod\n    def convert_attribute_values(cls, attribute_values):\n        if cls.__name__ not in attribute_values:\n            return attribute_values\n\n        plugin_values = attribute_values[cls.__name__]\n\n        attr_defs = cls.get_attribute_defs()\n        for attr_def in attr_defs:\n            key = attr_def.key\n            if key in plugin_values:\n                plugin_values[key] = attr_def.convert_value(\n                    plugin_values[key]\n                )\n        return attribute_values\n\n    @staticmethod\n    def get_attr_values_from_data_for_plugin(plugin, data):\n        \"\"\"Get attribute values for attribute definitions from data.\n\n        Args:\n            plugin (Union[publish.api.Plugin, Type[publish.api.Plugin]]): The\n                plugin for which attributes are extracted.\n            data(dict): Data from instance or context.\n        \"\"\"\n\n        if not inspect.isclass(plugin):\n            plugin = plugin.__class__\n\n        return (\n            data\n            .get(\"publish_attributes\", {})\n            .get(plugin.__name__, {})\n        )\n\n    def get_attr_values_from_data(self, data):\n        \"\"\"Get attribute values for attribute definitions from data.\n\n        Args:\n            data(dict): Data from instance or context.\n        \"\"\"\n\n        return self.get_attr_values_from_data_for_plugin(self.__class__, data)\n\n\nclass OptionalPyblishPluginMixin(OpenPypePyblishPluginMixin):\n    \"\"\"Prepare mixin for optional plugins.\n\n    Defined active attribute definition prepared for published and\n    prepares method which will check if is active or not.\n\n    ```\n    class ValidateScene(\n        pyblish.api.InstancePlugin, OptionalPyblishPluginMixin\n    ):\n        def process(self, instance):\n            # Skip the instance if is not active by data on the instance\n            if not self.is_active(instance.data):\n                return\n    ```\n    \"\"\"\n\n    @classmethod\n    def get_attribute_defs(cls):\n        \"\"\"Attribute definitions based on plugin's optional attribute.\"\"\"\n\n        # Empty list if plugin is not optional\n        if not getattr(cls, \"optional\", None):\n            return []\n\n        # Get active value from class as default value\n        active = getattr(cls, \"active\", True)\n        # Return boolean stored under 'active' key with label of the class name\n        label = cls.label or cls.__name__\n        return [\n            BoolDef(\"active\", default=active, label=label)\n        ]\n\n    def is_active(self, data):\n        \"\"\"Check if plugins is active for instance/context based on their data.\n\n        Args:\n            data(dict): Data from instance or context.\n        \"\"\"\n        # Skip if is not optional and return True\n        if not getattr(self, \"optional\", None):\n            return True\n        attr_values = self.get_attr_values_from_data(data)\n        active = attr_values.get(\"active\")\n        if active is None:\n            active = getattr(self, \"active\", True)\n        return active\n\n\nclass RepairAction(pyblish.api.Action):\n    \"\"\"Repairs the action\n\n    To process the repairing this requires a static `repair(instance)` method\n    is available on the plugin.\n    \"\"\"\n\n    label = \"Repair\"\n    on = \"failed\"  # This action is only available on a failed plug-in\n    icon = \"wrench\"  # Icon from Awesome Icon\n\n    def process(self, context, plugin):\n        if not hasattr(plugin, \"repair\"):\n            raise RuntimeError(\"Plug-in does not have repair method.\")\n\n        # Get the errored instances\n        self.log.debug(\"Finding failed instances..\")\n        errored_instances = get_errored_instances_from_context(context,\n                                                               plugin=plugin)\n        for instance in errored_instances:\n            self.log.debug(\n                \"Attempting repair for instance: {} ...\".format(instance)\n            )\n            plugin.repair(instance)\n\n\nclass RepairContextAction(pyblish.api.Action):\n    \"\"\"Repairs the action\n\n    To process the repairing this requires a static `repair(context)` method\n    is available on the plugin.\n    \"\"\"\n\n    label = \"Repair\"\n    on = \"failed\"  # This action is only available on a failed plug-in\n    icon = \"wrench\"  # Icon from Awesome Icon\n\n    def process(self, context, plugin):\n        if not hasattr(plugin, \"repair\"):\n            raise RuntimeError(\"Plug-in does not have repair method.\")\n\n        # Get the failed instances\n        self.log.debug(\"Finding failed plug-ins..\")\n        failed_plugins = get_errored_plugins_from_context(context)\n\n        # Apply pyblish.logic to get the instances for the plug-in\n        if plugin in failed_plugins:\n            self.log.debug(\"Attempting repair ...\")\n            plugin.repair(context)\n\n\nclass Extractor(pyblish.api.InstancePlugin):\n    \"\"\"Extractor base class.\n\n    The extractor base class implements a \"staging_dir\" function used to\n    generate a temporary directory for an instance to extract to.\n\n    This temporary directory is generated through `tempfile.mkdtemp()`\n\n    \"\"\"\n\n    order = 2.0\n\n    def staging_dir(self, instance):\n        \"\"\"Provide a temporary directory in which to store extracted files\n\n        Upon calling this method the staging directory is stored inside\n        the instance.data['stagingDir']\n        \"\"\"\n\n        return get_instance_staging_dir(instance)\n\n\nclass ColormanagedPyblishPluginMixin(object):\n    \"\"\"Mixin for colormanaged plugins.\n\n    This class is used to set colorspace data to a publishing\n    representation. It contains a static method,\n    get_colorspace_settings, which returns config and\n    file rules data for the host context.\n    It also contains a method, set_representation_colorspace,\n    which sets colorspace data to the representation.\n    The allowed file extensions are listed in the allowed_ext variable.\n    The method first checks if the file extension is in\n    the list of allowed extensions. If it is, it then gets the\n    colorspace settings from the host context and gets a\n    matching colorspace from rules. Finally, it infuses this\n    data into the representation.\n    \"\"\"\n\n    def get_colorspace_settings(self, context):\n        \"\"\"Returns solved settings for the host context.\n\n        Args:\n            context (publish.Context): publishing context\n\n        Returns:\n            tuple | bool: config, file rules or None\n        \"\"\"\n        return get_colorspace_settings_from_publish_context(context.data)\n\n    def set_representation_colorspace(\n        self, representation, context,\n        colorspace=None,\n    ):\n        \"\"\"Sets colorspace data to representation.\n\n        Args:\n            representation (dict): publishing representation\n            context (publish.Context): publishing context\n            colorspace (str, optional): colorspace name. Defaults to None.\n\n        Example:\n            ```\n            {\n                # for other publish plugins and loaders\n                \"colorspace\": \"linear\",\n                \"config\": {\n                    # for future references in case need\n                    \"path\": \"/abs/path/to/config.ocio\",\n                    # for other plugins within remote publish cases\n                    \"template\": \"{project[root]}/path/to/config.ocio\"\n                }\n            }\n            ```\n\n        \"\"\"\n\n        # using cached settings if available\n        set_colorspace_data_to_representation(\n            representation, context.data,\n            colorspace,\n            log=self.log\n        )\n"
  },
  {
    "path": "openpype/pipeline/schema/__init__.py",
    "content": "\"\"\"Wrapper around :mod:`jsonschema`\n\nSchemas are implicitly loaded from the /schema directory of this project.\n\nAttributes:\n    _cache: Cache of previously loaded schemas\n\nResources:\n    http://json-schema.org/\n    http://json-schema.org/latest/json-schema-core.html\n    http://spacetelescope.github.io/understanding-json-schema/index.html\n\n\"\"\"\n\nimport os\nimport re\nimport json\nimport logging\n\nimport jsonschema\nimport six\n\nlog_ = logging.getLogger(__name__)\n\nValidationError = jsonschema.ValidationError\nSchemaError = jsonschema.SchemaError\nCURRENT_DIR = os.path.dirname(os.path.abspath(__file__))\n\n_CACHED = False\n\n\ndef get_schema_version(schema_name):\n    \"\"\"Extract version form schema name.\n\n    It is expected that schema name contain only major and minor version.\n\n    Expected name should match to:\n    \"{name}:{type}-{major version}.{minor version}\"\n    - `name` - must not contain colon\n    - `type` - must not contain dash\n    - major and minor versions must be numbers separated by dot\n\n    Args:\n    schema_name(str): Name of schema that should be parsed.\n\n    Returns:\n    tuple: Contain two values major version as first and minor version as\n    second. When schema does not match parsing regex then `(0, 0)` is\n    returned.\n    \"\"\"\n    schema_regex = re.compile(r\"[^:]+:[^-]+-(\\d.\\d)\")\n    groups = schema_regex.findall(schema_name)\n    if not groups:\n        return 0, 0\n\n    maj_version, min_version = groups[0].split(\".\")\n    return int(maj_version), int(min_version)\n\n\ndef validate(data, schema=None):\n    \"\"\"Validate `data` with `schema`\n\n    Arguments:\n        data (dict): JSON-compatible data\n        schema (str): DEPRECATED Name of schema. Now included in the data.\n\n    Raises:\n        ValidationError on invalid schema\n\n    \"\"\"\n    if not _CACHED:\n        _precache()\n\n    root, schema = data[\"schema\"].rsplit(\":\", 1)\n    # assert root in (\n    #     \"mindbender-core\",  # Backwards compatiblity\n    #     \"avalon-core\",\n    #     \"pype\"\n    # )\n\n    if isinstance(schema, six.string_types):\n        schema = _cache[schema + \".json\"]\n\n    resolver = jsonschema.RefResolver(\n        \"\",\n        None,\n        store=_cache,\n        cache_remote=True\n    )\n\n    jsonschema.validate(data,\n                        schema,\n                        types={\"array\": (list, tuple)},\n                        resolver=resolver)\n\n\n_cache = {\n    # A mock schema for docstring tests\n    \"_doctest.json\": {\n        \"$schema\": \"http://json-schema.org/schema#\",\n\n        \"title\": \"_doctest\",\n        \"description\": \"A test schema\",\n\n        \"type\": \"object\",\n\n        \"additionalProperties\": False,\n\n        \"required\": [\"key\"],\n\n        \"properties\": {\n            \"key\": {\n                \"description\": \"A test key\",\n                \"type\": \"string\"\n            }\n        }\n    }\n}\n\n\ndef _precache():\n    \"\"\"Store available schemas in-memory for reduced disk access\"\"\"\n    global _CACHED\n\n    for schema in os.listdir(CURRENT_DIR):\n        if schema.startswith((\"_\", \".\")):\n            continue\n        if not schema.endswith(\".json\"):\n            continue\n        if not os.path.isfile(os.path.join(CURRENT_DIR, schema)):\n            continue\n        with open(os.path.join(CURRENT_DIR, schema)) as f:\n            log_.debug(\"Installing schema '%s'..\" % schema)\n            _cache[schema] = json.load(f)\n    _CACHED = True\n"
  },
  {
    "path": "openpype/pipeline/schema/application-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:application-1.0\",\n    \"description\": \"An application definition.\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"label\",\n        \"application_dir\",\n        \"executable\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\"\n        },\n        \"label\": {\n            \"description\": \"Nice name of application.\",\n            \"type\": \"string\"\n        },\n        \"application_dir\": {\n            \"description\": \"Name of directory used for application resources.\",\n            \"type\": \"string\"\n        },\n        \"executable\": {\n            \"description\": \"Name of callable executable, this is called to launch the application\",\n            \"type\": \"string\"\n        },\n        \"description\": {\n            \"description\": \"Description of application.\",\n            \"type\": \"string\"\n        },\n        \"environment\": {\n            \"description\": \"Key/value pairs for environment variables related to this application. Supports lists for paths, such as PYTHONPATH.\",\n            \"type\": \"object\",\n            \"items\": {\n                \"oneOf\": [\n                    {\"type\": \"string\"},\n                    {\"type\": \"array\", \"items\": {\"type\": \"string\"}}\n                ]\n            }\n        },\n        \"default_dirs\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"string\"\n            }\n        },\n        \"copy\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n                \"^.*$\": {\n                    \"anyOf\": [\n                        {\"type\": \"string\"},\n                        {\"type\": \"null\"}\n                    ]\n                }\n            },\n            \"additionalProperties\": false\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/asset-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:asset-1.0\",\n    \"description\": \"A unit of data\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"name\",\n        \"subsets\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\"\n        },\n        \"name\": {\n            \"description\": \"Name of directory\",\n            \"type\": \"string\"\n        },\n        \"subsets\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"$ref\": \"subset.json\"\n            }\n        }\n    },\n\n    \"definitions\": {}\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/asset-2.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:asset-2.0\",\n    \"description\": \"A unit of data\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"type\",\n        \"name\",\n        \"silo\",\n        \"data\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:asset-2.0\"],\n            \"example\": \"openpype:asset-2.0\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"asset\"],\n            \"example\": \"asset\"\n        },\n        \"parent\": {\n            \"description\": \"Unique identifier to parent document\",\n            \"example\": \"592c33475f8c1b064c4d1696\"\n        },\n        \"name\": {\n            \"description\": \"Name of asset\",\n            \"type\": \"string\",\n            \"pattern\": \"^[a-zA-Z0-9_.]*$\",\n            \"example\": \"Bruce\"\n        },\n        \"silo\": {\n            \"description\": \"Group or container of asset\",\n            \"type\": \"string\",\n            \"example\": \"assets\"\n        },\n        \"data\": {\n            \"description\": \"Document metadata\",\n            \"type\": \"object\",\n            \"example\": {\"key\": \"value\"}\n        }\n    },\n\n    \"definitions\": {}\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/asset-3.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:asset-3.0\",\n    \"description\": \"A unit of data\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"type\",\n        \"name\",\n        \"data\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:asset-3.0\"],\n            \"example\": \"openpype:asset-3.0\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"asset\"],\n            \"example\": \"asset\"\n        },\n        \"parent\": {\n            \"description\": \"Unique identifier to parent document\",\n            \"example\": \"592c33475f8c1b064c4d1696\"\n        },\n        \"name\": {\n            \"description\": \"Name of asset\",\n            \"type\": \"string\",\n            \"pattern\": \"^[a-zA-Z0-9_.]*$\",\n            \"example\": \"Bruce\"\n        },\n        \"silo\": {\n            \"description\": \"Group or container of asset\",\n            \"type\": \"string\",\n            \"pattern\": \"^[a-zA-Z0-9_.]*$\",\n            \"example\": \"assets\"\n        },\n        \"data\": {\n            \"description\": \"Document metadata\",\n            \"type\": \"object\",\n            \"example\": {\"key\": \"value\"}\n        }\n    },\n\n    \"definitions\": {}\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/config-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:config-1.0\",\n    \"description\": \"A project configuration.\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": false,\n    \"required\": [\n        \"tasks\",\n        \"apps\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\"\n        },\n        \"template\": {\n            \"type\": \"object\",\n            \"additionalProperties\": false,\n            \"patternProperties\": {\n                \"^.*$\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"tasks\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"icon\": {\"type\": \"string\"},\n                    \"group\": {\"type\": \"string\"},\n                    \"label\": {\"type\": \"string\"}\n                },\n                \"required\": [\"name\"]\n            }\n        },\n        \"apps\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"icon\": {\"type\": \"string\"},\n                    \"group\": {\"type\": \"string\"},\n                    \"label\": {\"type\": \"string\"}\n                },\n                \"required\": [\"name\"]\n            }\n        },\n        \"families\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"icon\": {\"type\": \"string\"},\n                    \"label\": {\"type\": \"string\"},\n                    \"hideFilter\": {\"type\": \"boolean\"}\n                },\n                \"required\": [\"name\"]\n            }\n        },\n        \"groups\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"icon\": {\"type\": \"string\"},\n                    \"color\": {\"type\": \"string\"},\n                    \"order\": {\"type\": [\"integer\", \"number\"]}\n                },\n                \"required\": [\"name\"]\n            }\n        },\n        \"copy\": {\n            \"type\": \"object\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/config-1.1.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:config-1.1\",\n    \"description\": \"A project configuration.\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": false,\n    \"required\": [\n        \"tasks\",\n        \"apps\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\"\n        },\n        \"template\": {\n            \"type\": \"object\",\n            \"additionalProperties\": false,\n            \"patternProperties\": {\n                \"^.*$\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"tasks\": {\n            \"type\": \"object\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"icon\": {\"type\": \"string\"},\n                    \"group\": {\"type\": \"string\"},\n                    \"label\": {\"type\": \"string\"}\n                },\n                \"required\": [\n                    \"short_name\"\n                ]\n            }\n        },\n        \"apps\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"icon\": {\"type\": \"string\"},\n                    \"group\": {\"type\": \"string\"},\n                    \"label\": {\"type\": \"string\"}\n                },\n                \"required\": [\"name\"]\n            }\n        },\n        \"families\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"icon\": {\"type\": \"string\"},\n                    \"label\": {\"type\": \"string\"},\n                    \"hideFilter\": {\"type\": \"boolean\"}\n                },\n                \"required\": [\"name\"]\n            }\n        },\n        \"groups\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"icon\": {\"type\": \"string\"},\n                    \"color\": {\"type\": \"string\"},\n                    \"order\": {\"type\": [\"integer\", \"number\"]}\n                },\n                \"required\": [\"name\"]\n            }\n        },\n        \"copy\": {\n            \"type\": \"object\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/config-2.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:config-2.0\",\n    \"description\": \"A project configuration.\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": false,\n    \"required\": [\n        \"tasks\",\n        \"apps\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\"\n        },\n        \"templates\": {\n            \"type\": \"object\"\n        },\n        \"roots\": {\n            \"type\": \"object\"\n        },\n        \"imageio\": {\n            \"type\": \"object\"\n        },\n        \"tasks\": {\n            \"type\": \"object\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"icon\": {\"type\": \"string\"},\n                    \"group\": {\"type\": \"string\"},\n                    \"label\": {\"type\": \"string\"}\n                },\n                \"required\": [\n                    \"short_name\"\n                ]\n            }\n        },\n        \"apps\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"icon\": {\"type\": \"string\"},\n                    \"group\": {\"type\": \"string\"},\n                    \"label\": {\"type\": \"string\"}\n                },\n                \"required\": [\"name\"]\n            }\n        },\n        \"families\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"icon\": {\"type\": \"string\"},\n                    \"label\": {\"type\": \"string\"},\n                    \"hideFilter\": {\"type\": \"boolean\"}\n                },\n                \"required\": [\"name\"]\n            }\n        },\n        \"groups\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"icon\": {\"type\": \"string\"},\n                    \"color\": {\"type\": \"string\"},\n                    \"order\": {\"type\": [\"integer\", \"number\"]}\n                },\n                \"required\": [\"name\"]\n            }\n        },\n        \"copy\": {\n            \"type\": \"object\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/container-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:container-1.0\",\n    \"description\": \"A loaded asset\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"id\",\n        \"objectName\",\n        \"name\",\n        \"author\",\n        \"loader\",\n        \"families\",\n        \"time\",\n        \"subset\",\n        \"asset\",\n        \"representation\",\n        \"version\",\n        \"silo\",\n        \"path\",\n        \"source\"\n    ],\n    \"properties\": {\n        \"id\": {\n            \"description\": \"Identifier for finding object in host\",\n            \"type\": \"string\",\n            \"enum\": [\"pyblish.mindbender.container\"],\n            \"example\": \"pyblish.mindbender.container\"\n        },\n        \"objectName\": {\n            \"description\": \"Name of internal object, such as the objectSet in Maya.\",\n            \"type\": \"string\",\n            \"example\": \"Bruce_:rigDefault_CON\"\n        },\n        \"name\": {\n            \"description\": \"Full name of application object\",\n            \"type\": \"string\",\n            \"example\": \"modelDefault\"\n        },\n        \"author\": {\n            \"description\": \"Name of the author of the published version\",\n            \"type\": \"string\",\n            \"example\": \"Marcus Ottosson\"\n        },\n        \"loader\": {\n            \"description\": \"Name of loader plug-in used to produce this container\",\n            \"type\": \"string\",\n            \"example\": \"ModelLoader\"\n        },\n        \"families\": {\n            \"description\": \"Families associated with the this subset\",\n            \"type\": \"string\",\n            \"example\": \"mindbender.model\"\n        },\n        \"time\": {\n            \"description\": \"File-system safe, formatted time\",\n            \"type\": \"string\",\n            \"example\": \"20170329T131545Z\"\n        },\n        \"subset\": {\n            \"description\": \"Name of source subset\",\n            \"type\": \"string\",\n            \"example\": \"modelDefault\"\n        },\n        \"asset\": {\n            \"description\": \"Name of source asset\",\n            \"type\": \"string\"  ,\n            \"example\": \"Bruce\"\n        },\n        \"representation\": {\n            \"description\": \"Name of source representation\",\n            \"type\": \"string\"  ,\n            \"example\": \".ma\"\n        },\n        \"version\": {\n            \"description\": \"Version number\",\n            \"type\": \"number\",\n            \"example\": 12\n        },\n        \"silo\": {\n            \"description\": \"Silo of parent asset\",\n            \"type\": \"string\",\n            \"example\": \"assets\"\n        },\n        \"path\": {\n            \"description\": \"Absolute path on disk\",\n            \"type\": \"string\",\n            \"example\": \"{root}/assets/Bruce/publish/rigDefault/v002\"\n        },\n        \"source\": {\n            \"description\": \"Absolute path to file from which this version was published\",\n            \"type\": \"string\",\n            \"example\": \"{root}/assets/Bruce/work/rigging/maya/scenes/rig_v001.ma\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/container-2.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:container-2.0\",\n    \"description\": \"A loaded asset\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"id\",\n        \"objectName\",\n        \"name\",\n        \"namespace\",\n        \"loader\",\n        \"representation\"\n    ],\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:container-2.0\"],\n            \"example\": \"openpype:container-2.0\"\n        },\n          \"id\": {\n            \"description\": \"Identifier for finding object in host\",\n            \"type\": \"string\",\n            \"enum\": [\"pyblish.avalon.container\"],\n            \"example\": \"pyblish.avalon.container\"\n        },\n        \"objectName\": {\n            \"description\": \"Name of internal object, such as the objectSet in Maya.\",\n            \"type\": \"string\",\n            \"example\": \"Bruce_:rigDefault_CON\"\n        },\n        \"loader\": {\n            \"description\": \"Name of loader plug-in used to produce this container\",\n            \"type\": \"string\",\n            \"example\": \"ModelLoader\"\n        },\n        \"name\": {\n            \"description\": \"Internal object name of container in application\",\n            \"type\": \"string\",\n            \"example\": \"modelDefault_01\"\n        },\n        \"namespace\": {\n            \"description\": \"Internal namespace of container in application\",\n            \"type\": \"string\",\n            \"example\": \"Bruce_\"\n        },\n        \"representation\": {\n            \"description\": \"Unique id of representation in database\",\n            \"type\": \"string\",\n            \"example\": \"59523f355f8c1b5f6c5e8348\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/hero_version-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:hero_version-1.0\",\n    \"description\": \"Hero version of asset\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"version_id\",\n        \"schema\",\n        \"type\",\n        \"parent\"\n    ],\n\n    \"properties\": {\n        \"_id\": {\n            \"description\": \"Document's id (database will create it's if not entered)\",\n            \"example\": \"ObjectId(592c33475f8c1b064c4d1696)\"\n        },\n        \"version_id\": {\n            \"description\": \"The version ID from which it was created\",\n            \"example\": \"ObjectId(592c33475f8c1b064c4d1695)\"\n        },\n        \"schema\": {\n            \"description\": \"The schema associated with this document\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:hero_version-1.0\"],\n            \"example\": \"openpype:hero_version-1.0\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"hero_version\"],\n            \"example\": \"hero_version\"\n        },\n        \"parent\": {\n            \"description\": \"Unique identifier to parent document\",\n            \"example\": \"ObjectId(592c33475f8c1b064c4d1697)\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/inventory-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:config-1.0\",\n    \"description\": \"A project configuration.\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/inventory-1.1.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:config-1.1\",\n    \"description\": \"A project configuration.\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/project-2.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:project-2.0\",\n    \"description\": \"A unit of data\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"type\",\n        \"name\",\n        \"data\",\n        \"config\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:project-2.0\"],\n            \"example\": \"openpype:project-2.0\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"project\"],\n            \"example\": \"project\"\n        },\n        \"parent\": {\n            \"description\": \"Unique identifier to parent document\",\n            \"example\": \"592c33475f8c1b064c4d1696\"\n        },\n        \"name\": {\n            \"description\": \"Name of directory\",\n            \"type\": \"string\",\n            \"pattern\": \"^[a-zA-Z0-9_.]*$\",\n            \"example\": \"hulk\"\n        },\n        \"data\": {\n            \"description\": \"Document metadata\",\n            \"type\": \"object\",\n            \"example\": {\n                \"fps\": 24,\n                \"width\": 1920,\n                \"height\": 1080\n            }\n        },\n        \"config\": {\n            \"type\": \"object\",\n            \"description\": \"Document metadata\",\n            \"example\": {\n                \"schema\": \"openpype:config-1.0\",\n                \"apps\": [\n                    {\n                        \"name\": \"maya2016\",\n                        \"label\": \"Autodesk Maya 2016\"\n                    },\n                    {\n                        \"name\": \"nuke10\",\n                        \"label\": \"The Foundry Nuke 10.0\"\n                    }\n                ],\n                \"tasks\": [\n                    {\"name\": \"model\"},\n                    {\"name\": \"render\"},\n                    {\"name\": \"animate\"},\n                    {\"name\": \"rig\"},\n                    {\"name\": \"lookdev\"},\n                    {\"name\": \"layout\"}\n                ],\n                \"template\": {\n                    \"work\":\n                        \"{root}/{project}/{silo}/{asset}/work/{task}/{app}\",\n                    \"publish\":\n                        \"{root}/{project}/{silo}/{asset}/publish/{subset}/v{version:0>3}/{subset}.{representation}\"\n                }\n            },\n            \"$ref\": \"config-1.0.json\"\n        }\n    },\n\n    \"definitions\": {}\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/project-2.1.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:project-2.1\",\n    \"description\": \"A unit of data\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"type\",\n        \"name\",\n        \"data\",\n        \"config\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:project-2.1\"],\n            \"example\": \"openpype:project-2.1\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"project\"],\n            \"example\": \"project\"\n        },\n        \"parent\": {\n            \"description\": \"Unique identifier to parent document\",\n            \"example\": \"592c33475f8c1b064c4d1696\"\n        },\n        \"name\": {\n            \"description\": \"Name of directory\",\n            \"type\": \"string\",\n            \"pattern\": \"^[a-zA-Z0-9_.]*$\",\n            \"example\": \"hulk\"\n        },\n        \"data\": {\n            \"description\": \"Document metadata\",\n            \"type\": \"object\",\n            \"example\": {\n                \"fps\": 24,\n                \"width\": 1920,\n                \"height\": 1080\n            }\n        },\n        \"config\": {\n            \"type\": \"object\",\n            \"description\": \"Document metadata\",\n            \"example\": {\n                \"schema\": \"openpype:config-1.1\",\n                \"apps\": [\n                    {\n                        \"name\": \"maya2016\",\n                        \"label\": \"Autodesk Maya 2016\"\n                    },\n                    {\n                        \"name\": \"nuke10\",\n                        \"label\": \"The Foundry Nuke 10.0\"\n                    }\n                ],\n                \"tasks\": {\n                    \"Model\":   {\"short_name\": \"mdl\"},\n                    \"Render\":  {\"short_name\": \"rnd\"},\n                    \"Animate\": {\"short_name\": \"anim\"},\n                    \"Rig\":     {\"short_name\": \"rig\"},\n                    \"Lookdev\": {\"short_name\": \"look\"},\n                    \"Layout\":  {\"short_name\": \"lay\"}\n                },\n                \"template\": {\n                    \"work\":\n                        \"{root}/{project}/{silo}/{asset}/work/{task}/{app}\",\n                    \"publish\":\n                        \"{root}/{project}/{silo}/{asset}/publish/{subset}/v{version:0>3}/{subset}.{representation}\"\n                }\n            },\n            \"$ref\": \"config-1.1.json\"\n        }\n    },\n\n    \"definitions\": {}\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/project-3.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:project-3.0\",\n    \"description\": \"A unit of data\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"type\",\n        \"name\",\n        \"data\",\n        \"config\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:project-3.0\"],\n            \"example\": \"openpype:project-3.0\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"project\"],\n            \"example\": \"project\"\n        },\n        \"parent\": {\n            \"description\": \"Unique identifier to parent document\",\n            \"example\": \"592c33475f8c1b064c4d1696\"\n        },\n        \"name\": {\n            \"description\": \"Name of directory\",\n            \"type\": \"string\",\n            \"pattern\": \"^[a-zA-Z0-9_.]*$\",\n            \"example\": \"hulk\"\n        },\n        \"data\": {\n            \"description\": \"Document metadata\",\n            \"type\": \"object\",\n            \"example\": {\n                \"fps\": 24,\n                \"width\": 1920,\n                \"height\": 1080\n            }\n        },\n        \"config\": {\n            \"type\": \"object\",\n            \"description\": \"Document metadata\",\n            \"$ref\": \"config-2.0.json\"\n        }\n    },\n\n    \"definitions\": {}\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/representation-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:representation-1.0\",\n    \"description\": \"The inverse of an instance\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"format\",\n        \"path\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\"type\": \"string\"},\n        \"format\": {\n            \"description\": \"File extension, including '.'\",\n            \"type\": \"string\"\n        },\n        \"path\": {\n            \"description\": \"Unformatted path to version.\",\n            \"type\": \"string\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/representation-2.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:representation-2.0\",\n    \"description\": \"The inverse of an instance\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"type\",\n        \"parent\",\n        \"name\",\n        \"data\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:representation-2.0\"],\n            \"example\": \"openpype:representation-2.0\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"representation\"],\n            \"example\": \"representation\"\n        },\n        \"parent\": {\n            \"description\": \"Unique identifier to parent document\",\n            \"example\": \"592c33475f8c1b064c4d1696\"\n        },\n        \"name\": {\n            \"description\": \"Name of representation\",\n            \"type\": \"string\",\n            \"pattern\": \"^[a-zA-Z0-9_.]*$\",\n            \"example\": \"abc\"\n        },\n        \"data\": {\n            \"description\": \"Document metadata\",\n            \"type\": \"object\",\n            \"example\": {\n                \"label\": \"Alembic\"\n            }\n        },\n        \"dependencies\": {\n            \"description\": \"Other representation that this representation depends on\",\n            \"type\": \"array\",\n            \"items\": {\"type\": \"string\"},\n            \"example\": [\n                \"592d547a5f8c1b388093c145\"\n            ]\n        },\n        \"context\": {\n            \"description\": \"Summary of the context to which this representation belong.\",\n            \"type\": \"object\",\n            \"properties\": {\n                \"project\": {\"type\": \"object\"},\n                \"asset\": {\"type\": \"string\"},\n                \"silo\": {\"type\": [\"string\", \"null\"]},\n                \"subset\": {\"type\": \"string\"},\n                \"version\": {\"type\": \"number\"},\n                \"representation\": {\"type\": \"string\"}\n            },\n            \"example\": {\n                \"project\": \"hulk\",\n                \"asset\": \"Bruce\",\n                \"silo\": \"assets\",\n                \"subset\": \"rigDefault\",\n                \"version\": 12,\n                \"representation\": \"ma\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/session-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:session-1.0\",\n    \"description\": \"The Avalon environment\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"AVALON_PROJECTS\",\n        \"AVALON_PROJECT\",\n        \"AVALON_ASSET\",\n        \"AVALON_SILO\",\n        \"AVALON_CONFIG\"\n    ],\n\n    \"properties\": {\n        \"AVALON_PROJECTS\": {\n            \"description\": \"Absolute path to root of project directories\",\n            \"type\": \"string\",\n            \"example\": \"/nas/projects\"\n        },\n        \"AVALON_PROJECT\": {\n            \"description\": \"Name of project\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"Hulk\"\n        },\n        \"AVALON_ASSET\": {\n            \"description\": \"Name of asset\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"Bruce\"\n        },\n        \"AVALON_SILO\": {\n            \"description\": \"Name of asset group or container\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"assets\"\n        },\n        \"AVALON_TASK\": {\n            \"description\": \"Name of task\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"modeling\"\n        },\n        \"AVALON_CONFIG\": {\n            \"description\": \"Name of Avalon configuration\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"polly\"\n        },\n        \"AVALON_APP\": {\n            \"description\": \"Name of application\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"maya2016\"\n        },\n        \"AVALON_MONGO\": {\n            \"description\": \"Address to the asset database\",\n            \"type\": \"string\",\n            \"pattern\": \"^mongodb://[\\\\w/@:.]*$\",\n            \"example\": \"mongodb://localhost:27017\",\n            \"default\": \"mongodb://localhost:27017\"\n        },\n        \"AVALON_DB\": {\n            \"description\": \"Name of database\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"avalon\",\n            \"default\": \"avalon\"\n        },\n        \"AVALON_LABEL\": {\n            \"description\": \"Nice name of Avalon, used in e.g. graphical user interfaces\",\n            \"type\": \"string\",\n            \"example\": \"Mindbender\",\n            \"default\": \"Avalon\"\n        },\n        \"AVALON_SENTRY\": {\n            \"description\": \"Address to Sentry\",\n            \"type\": \"string\",\n            \"pattern\": \"^http[\\\\w/@:.]*$\",\n            \"example\": \"https://5b872b280de742919b115bdc8da076a5:8d278266fe764361b8fa6024af004a9c@logs.mindbender.com/2\",\n            \"default\": null\n        },\n        \"AVALON_DEADLINE\": {\n            \"description\": \"Address to Deadline\",\n            \"type\": \"string\",\n            \"pattern\": \"^http[\\\\w/@:.]*$\",\n            \"example\": \"http://192.168.99.101\",\n            \"default\": null\n        },\n        \"AVALON_TIMEOUT\": {\n            \"description\": \"Wherever there is a need for a timeout, this is the default value.\",\n            \"type\": \"string\",\n            \"pattern\": \"^[0-9]*$\",\n            \"default\": \"1000\",\n            \"example\": \"1000\"\n        },\n        \"AVALON_UPLOAD\": {\n            \"description\": \"Boolean of whether to upload published material to central asset repository\",\n            \"type\": \"string\",\n            \"default\": null,\n            \"example\": \"True\"\n        },\n        \"AVALON_USERNAME\": {\n            \"description\": \"Generic username\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"default\": \"avalon\",\n            \"example\": \"myself\"\n        },\n        \"AVALON_PASSWORD\": {\n            \"description\": \"Generic password\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"default\": \"secret\",\n            \"example\": \"abc123\"\n        },\n        \"AVALON_INSTANCE_ID\": {\n            \"description\": \"Unique identifier for instances in a working file\",\n            \"type\": \"string\",\n            \"pattern\": \"^[\\\\w.]*$\",\n            \"default\": \"avalon.instance\",\n            \"example\": \"avalon.instance\"\n        },\n        \"AVALON_CONTAINER_ID\": {\n            \"description\": \"Unique identifier for a loaded representation in a working file\",\n            \"type\": \"string\",\n            \"pattern\": \"^[\\\\w.]*$\",\n            \"default\": \"avalon.container\",\n            \"example\": \"avalon.container\"\n        },\n        \"AVALON_DEBUG\": {\n            \"description\": \"Enable debugging mode. Some applications may use this for e.g. extended verbosity or mock plug-ins.\",\n            \"type\": \"string\",\n            \"default\": null,\n            \"example\": \"True\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/session-2.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:session-2.0\",\n    \"description\": \"The Avalon environment\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"AVALON_PROJECT\",\n        \"AVALON_ASSET\",\n        \"AVALON_CONFIG\"\n    ],\n\n    \"properties\": {\n        \"AVALON_PROJECTS\": {\n            \"description\": \"Absolute path to root of project directories\",\n            \"type\": \"string\",\n            \"example\": \"/nas/projects\"\n        },\n        \"AVALON_PROJECT\": {\n            \"description\": \"Name of project\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"Hulk\"\n        },\n        \"AVALON_ASSET\": {\n            \"description\": \"Name of asset\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"Bruce\"\n        },\n        \"AVALON_SILO\": {\n            \"description\": \"Name of asset group or container\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"assets\"\n        },\n        \"AVALON_TASK\": {\n            \"description\": \"Name of task\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"modeling\"\n        },\n        \"AVALON_CONFIG\": {\n            \"description\": \"Name of Avalon configuration\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"polly\"\n        },\n        \"AVALON_APP\": {\n            \"description\": \"Name of application\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"maya2016\"\n        },\n        \"AVALON_DB\": {\n            \"description\": \"Name of database\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"avalon\",\n            \"default\": \"avalon\"\n        },\n        \"AVALON_LABEL\": {\n            \"description\": \"Nice name of Avalon, used in e.g. graphical user interfaces\",\n            \"type\": \"string\",\n            \"example\": \"Mindbender\",\n            \"default\": \"Avalon\"\n        },\n        \"AVALON_SENTRY\": {\n            \"description\": \"Address to Sentry\",\n            \"type\": \"string\",\n            \"pattern\": \"^http[\\\\w/@:.]*$\",\n            \"example\": \"https://5b872b280de742919b115bdc8da076a5:8d278266fe764361b8fa6024af004a9c@logs.mindbender.com/2\",\n            \"default\": null\n        },\n        \"AVALON_DEADLINE\": {\n            \"description\": \"Address to Deadline\",\n            \"type\": \"string\",\n            \"pattern\": \"^http[\\\\w/@:.]*$\",\n            \"example\": \"http://192.168.99.101\",\n            \"default\": null\n        },\n        \"AVALON_TIMEOUT\": {\n            \"description\": \"Wherever there is a need for a timeout, this is the default value.\",\n            \"type\": \"string\",\n            \"pattern\": \"^[0-9]*$\",\n            \"default\": \"1000\",\n            \"example\": \"1000\"\n        },\n        \"AVALON_UPLOAD\": {\n            \"description\": \"Boolean of whether to upload published material to central asset repository\",\n            \"type\": \"string\",\n            \"default\": null,\n            \"example\": \"True\"\n        },\n        \"AVALON_USERNAME\": {\n            \"description\": \"Generic username\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"default\": \"avalon\",\n            \"example\": \"myself\"\n        },\n        \"AVALON_PASSWORD\": {\n            \"description\": \"Generic password\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"default\": \"secret\",\n            \"example\": \"abc123\"\n        },\n        \"AVALON_INSTANCE_ID\": {\n            \"description\": \"Unique identifier for instances in a working file\",\n            \"type\": \"string\",\n            \"pattern\": \"^[\\\\w.]*$\",\n            \"default\": \"avalon.instance\",\n            \"example\": \"avalon.instance\"\n        },\n        \"AVALON_CONTAINER_ID\": {\n            \"description\": \"Unique identifier for a loaded representation in a working file\",\n            \"type\": \"string\",\n            \"pattern\": \"^[\\\\w.]*$\",\n            \"default\": \"avalon.container\",\n            \"example\": \"avalon.container\"\n        },\n        \"AVALON_DEBUG\": {\n            \"description\": \"Enable debugging mode. Some applications may use this for e.g. extended verbosity or mock plug-ins.\",\n            \"type\": \"string\",\n            \"default\": null,\n            \"example\": \"True\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/session-3.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:session-3.0\",\n    \"description\": \"The Avalon environment\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"AVALON_PROJECT\",\n        \"AVALON_ASSET\"\n    ],\n\n    \"properties\": {\n        \"AVALON_PROJECTS\": {\n            \"description\": \"Absolute path to root of project directories\",\n            \"type\": \"string\",\n            \"example\": \"/nas/projects\"\n        },\n        \"AVALON_PROJECT\": {\n            \"description\": \"Name of project\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"Hulk\"\n        },\n        \"AVALON_ASSET\": {\n            \"description\": \"Name of asset\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"Bruce\"\n        },\n        \"AVALON_TASK\": {\n            \"description\": \"Name of task\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"modeling\"\n        },\n        \"AVALON_APP\": {\n            \"description\": \"Name of host\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"maya2016\"\n        },\n        \"AVALON_DB\": {\n            \"description\": \"Name of database\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"avalon\",\n            \"default\": \"avalon\"\n        },\n        \"AVALON_LABEL\": {\n            \"description\": \"Nice name of Avalon, used in e.g. graphical user interfaces\",\n            \"type\": \"string\",\n            \"example\": \"Mindbender\",\n            \"default\": \"Avalon\"\n        },\n        \"AVALON_TIMEOUT\": {\n            \"description\": \"Wherever there is a need for a timeout, this is the default value.\",\n            \"type\": \"string\",\n            \"pattern\": \"^[0-9]*$\",\n            \"default\": \"1000\",\n            \"example\": \"1000\"\n        },\n        \"AVALON_INSTANCE_ID\": {\n            \"description\": \"Unique identifier for instances in a working file\",\n            \"type\": \"string\",\n            \"pattern\": \"^[\\\\w.]*$\",\n            \"default\": \"avalon.instance\",\n            \"example\": \"avalon.instance\"\n        },\n        \"AVALON_CONTAINER_ID\": {\n            \"description\": \"Unique identifier for a loaded representation in a working file\",\n            \"type\": \"string\",\n            \"pattern\": \"^[\\\\w.]*$\",\n            \"default\": \"avalon.container\",\n            \"example\": \"avalon.container\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/session-4.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:session-4.0\",\n    \"description\": \"The Avalon environment\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"AVALON_PROJECT\"\n    ],\n\n    \"properties\": {\n        \"AVALON_PROJECT\": {\n            \"description\": \"Name of project\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"Hulk\"\n        },\n        \"AVALON_ASSET\": {\n            \"description\": \"Name of asset\",\n            \"type\": \"string\",\n            \"pattern\": \"^[\\\\/\\\\w]*$\",\n            \"example\": \"Bruce\"\n        },\n        \"AVALON_TASK\": {\n            \"description\": \"Name of task\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"modeling\"\n        },\n        \"AVALON_APP\": {\n            \"description\": \"Name of host\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"maya\"\n        },\n        \"AVALON_DB\": {\n            \"description\": \"Name of database\",\n            \"type\": \"string\",\n            \"pattern\": \"^\\\\w*$\",\n            \"example\": \"avalon\",\n            \"default\": \"avalon\"\n        },\n        \"AVALON_LABEL\": {\n            \"description\": \"Nice name of Avalon, used in e.g. graphical user interfaces\",\n            \"type\": \"string\",\n            \"example\": \"MyLabel\",\n            \"default\": \"Avalon\"\n        },\n        \"AVALON_TIMEOUT\": {\n            \"description\": \"Wherever there is a need for a timeout, this is the default value.\",\n            \"type\": \"string\",\n            \"pattern\": \"^[0-9]*$\",\n            \"default\": \"1000\",\n            \"example\": \"1000\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/shaders-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:shaders-1.0\",\n    \"description\": \"Relationships between shaders and Avalon IDs\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"shader\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\"\n        },\n        \"shader\": {\n            \"description\": \"Name of directory\",\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"str\",\n                \"description\": \"Avalon ID and optional face indexes, e.g. 'f9520572-ac1d-11e6-b39e-3085a99791c9.f[5002:5185]'\"\n            }\n        }\n    },\n\n    \"definitions\": {}\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/subset-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:subset-1.0\",\n    \"description\": \"A container of instances\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"name\",\n        \"versions\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\"\n        },\n        \"name\": {\n            \"description\": \"Name of directory\",\n            \"type\": \"string\"\n        },\n        \"versions\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"$ref\": \"version.json\"\n            }\n        }\n    },\n\n    \"definitions\": {}\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/subset-2.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:subset-2.0\",\n    \"description\": \"A container of instances\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"type\",\n        \"parent\",\n        \"name\",\n        \"data\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"The schema associated with this document\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:subset-2.0\"],\n            \"example\": \"openpype:subset-2.0\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"subset\"],\n            \"example\": \"subset\"\n        },\n        \"parent\": {\n            \"description\": \"Unique identifier to parent document\",\n            \"example\": \"592c33475f8c1b064c4d1696\"\n        },\n        \"name\": {\n            \"description\": \"Name of directory\",\n            \"type\": \"string\",\n            \"pattern\": \"^[a-zA-Z0-9_.]*$\",\n            \"example\": \"shot01\"\n        },\n        \"data\": {\n            \"type\": \"object\",\n            \"description\": \"Document metadata\",\n            \"example\": {\n                \"frameStart\": 1000,\n                \"frameEnd\": 1201\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/subset-3.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:subset-3.0\",\n    \"description\": \"A container of instances\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"type\",\n        \"parent\",\n        \"name\",\n        \"data\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"The schema associated with this document\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:subset-3.0\"],\n            \"example\": \"openpype:subset-3.0\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"subset\"],\n            \"example\": \"subset\"\n        },\n        \"parent\": {\n            \"description\": \"Unique identifier to parent document\",\n            \"example\": \"592c33475f8c1b064c4d1696\"\n        },\n        \"name\": {\n            \"description\": \"Name of directory\",\n            \"type\": \"string\",\n            \"pattern\": \"^[a-zA-Z0-9_.]*$\",\n            \"example\": \"shot01\"\n        },\n        \"data\": {\n            \"description\": \"Document metadata\",\n            \"type\": \"object\",\n            \"required\": [\"families\"],\n            \"properties\": {\n                \"families\": {\n                    \"type\": \"array\",\n                    \"items\": {\"type\": \"string\"},\n                    \"description\": \"One or more families associated with this subset\"\n                }\n            },\n            \"example\": {\n                \"families\" : [\n                    \"avalon.camera\"\n                ],\n                \"frameStart\": 1000,\n                \"frameEnd\": 1201\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/thumbnail-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:thumbnail-1.0\",\n    \"description\": \"Entity with thumbnail data\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"type\",\n        \"data\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"The schema associated with this document\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:thumbnail-1.0\"],\n            \"example\": \"openpype:thumbnail-1.0\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"thumbnail\"],\n            \"example\": \"thumbnail\"\n        },\n        \"data\": {\n            \"description\": \"Thumbnail data\",\n            \"type\": \"object\",\n            \"example\": {\n                \"binary_data\": \"Binary({byte data of image})\",\n                \"template\": \"{thumbnail_root}/{project[name]}/{_id}{ext}}\",\n                \"template_data\": {\n                    \"ext\": \".jpg\"\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/version-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:version-1.0\",\n    \"description\": \"An individual version\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"version\",\n        \"path\",\n        \"time\",\n        \"author\",\n        \"source\",\n        \"representations\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\"type\": \"string\"},\n        \"representations\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"$ref\": \"representation.json\"\n            }\n        },\n        \"time\": {\n            \"description\": \"ISO formatted, file-system compatible time\",\n            \"type\": \"string\"\n        },\n        \"author\": {\n            \"description\": \"User logged on to the machine at time of publish\",\n            \"type\": \"string\"\n        },\n        \"version\": {\n            \"description\": \"Number of this version\",\n            \"type\": \"number\"\n        },\n        \"path\": {\n            \"description\": \"Unformatted path, e.g. '{root}/assets/Bruce/publish/lookdevDefault/v001\",\n            \"type\": \"string\"\n        },\n        \"source\": {\n            \"description\": \"Original file from which this version was made.\",\n            \"type\": \"string\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/version-2.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:version-2.0\",\n    \"description\": \"An individual version\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"type\",\n        \"parent\",\n        \"name\",\n        \"data\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"The schema associated with this document\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:version-2.0\"],\n            \"example\": \"openpype:version-2.0\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"version\"],\n            \"example\": \"version\"\n        },\n        \"parent\": {\n            \"description\": \"Unique identifier to parent document\",\n            \"example\": \"592c33475f8c1b064c4d1696\"\n        },\n        \"name\": {\n            \"description\": \"Number of version\",\n            \"type\": \"number\",\n            \"example\": 12\n        },\n        \"locations\": {\n            \"description\": \"Where on the planet this version can be found.\",\n            \"type\": \"array\",\n            \"items\": {\"type\": \"string\"},\n            \"example\": [\"data.avalon.com\"]\n        },\n        \"data\": {\n            \"description\": \"Document metadata\",\n            \"type\": \"object\",\n            \"required\": [\"families\", \"author\", \"source\", \"time\"],\n            \"properties\": {\n                \"time\": {\n                    \"description\": \"ISO formatted, file-system compatible time\",\n                    \"type\": \"string\"\n                },\n                \"timeFormat\": {\n                    \"description\": \"ISO format of time\",\n                    \"type\": \"string\"\n                },\n                \"author\": {\n                    \"description\": \"User logged on to the machine at time of publish\",\n                    \"type\": \"string\"\n                },\n                \"version\": {\n                    \"description\": \"Number of this version\",\n                    \"type\": \"number\"\n                },\n                \"path\": {\n                    \"description\": \"Unformatted path, e.g. '{root}/assets/Bruce/publish/lookdevDefault/v001\",\n                    \"type\": \"string\"\n                },\n                \"source\": {\n                    \"description\": \"Original file from which this version was made.\",\n                    \"type\": \"string\"\n                },\n                \"families\": {\n                    \"type\": \"array\",\n                    \"items\": {\"type\": \"string\"},\n                    \"description\": \"One or more families associated with this version\"\n                }\n            },\n            \"example\": {\n                \"source\" : \"{root}/f02_prod/assets/BubbleWitch/work/modeling/marcus/maya/scenes/model_v001.ma\",\n                \"author\" : \"marcus\",\n                \"families\" : [\n                    \"avalon.model\"\n                ],\n                \"time\" : \"20170510T090203Z\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/version-3.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:version-3.0\",\n    \"description\": \"An individual version\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"type\",\n        \"parent\",\n        \"name\",\n        \"data\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"The schema associated with this document\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:version-3.0\"],\n            \"example\": \"openpype:version-3.0\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"version\"],\n            \"example\": \"version\"\n        },\n        \"parent\": {\n            \"description\": \"Unique identifier to parent document\",\n            \"example\": \"592c33475f8c1b064c4d1696\"\n        },\n        \"name\": {\n            \"description\": \"Number of version\",\n            \"type\": \"number\",\n            \"example\": 12\n        },\n        \"locations\": {\n            \"description\": \"Where on the planet this version can be found.\",\n            \"type\": \"array\",\n            \"items\": {\"type\": \"string\"},\n            \"example\": [\"data.avalon.com\"]\n        },\n        \"data\": {\n            \"description\": \"Document metadata\",\n            \"type\": \"object\",\n            \"required\": [\"author\", \"source\", \"time\"],\n            \"properties\": {\n                \"time\": {\n                    \"description\": \"ISO formatted, file-system compatible time\",\n                    \"type\": \"string\"\n                },\n                \"timeFormat\": {\n                    \"description\": \"ISO format of time\",\n                    \"type\": \"string\"\n                },\n                \"author\": {\n                    \"description\": \"User logged on to the machine at time of publish\",\n                    \"type\": \"string\"\n                },\n                \"version\": {\n                    \"description\": \"Number of this version\",\n                    \"type\": \"number\"\n                },\n                \"path\": {\n                    \"description\": \"Unformatted path, e.g. '{root}/assets/Bruce/publish/lookdevDefault/v001\",\n                    \"type\": \"string\"\n                },\n                \"source\": {\n                    \"description\": \"Original file from which this version was made.\",\n                    \"type\": \"string\"\n                }\n            },\n            \"example\": {\n                \"source\" : \"{root}/f02_prod/assets/BubbleWitch/work/modeling/marcus/maya/scenes/model_v001.ma\",\n                \"author\" : \"marcus\",\n                \"time\" : \"20170510T090203Z\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/schema/workfile-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n\n    \"title\": \"openpype:workfile-1.0\",\n    \"description\": \"Workfile additional information.\",\n\n    \"type\": \"object\",\n\n    \"additionalProperties\": true,\n\n    \"required\": [\n        \"schema\",\n        \"type\",\n        \"filename\",\n        \"task_name\",\n        \"parent\"\n    ],\n\n    \"properties\": {\n        \"schema\": {\n            \"description\": \"Schema identifier for payload\",\n            \"type\": \"string\",\n            \"enum\": [\"openpype:workfile-1.0\"],\n            \"example\": \"openpype:workfile-1.0\"\n        },\n        \"type\": {\n            \"description\": \"The type of document\",\n            \"type\": \"string\",\n            \"enum\": [\"workfile\"],\n            \"example\": \"workfile\"\n        },\n        \"parent\": {\n            \"description\": \"Unique identifier to parent document\",\n            \"example\": \"592c33475f8c1b064c4d1696\"\n        },\n        \"filename\": {\n            \"description\": \"Workfile's filename\",\n            \"type\": \"string\",\n            \"example\": \"kuba_each_case_Alpaca_01_animation_v001.ma\"\n        },\n        \"task_name\": {\n            \"description\": \"Task name\",\n            \"type\": \"string\",\n            \"example\": \"animation\"\n        },\n        \"data\": {\n            \"description\": \"Document metadata\",\n            \"type\": \"object\",\n            \"example\": {\"key\": \"value\"}\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/pipeline/tempdir.py",
    "content": "\"\"\"\nTemporary folder operations\n\"\"\"\n\nimport os\nfrom openpype.lib import StringTemplate\nfrom openpype.pipeline import Anatomy\n\n\ndef create_custom_tempdir(project_name, anatomy=None):\n    \"\"\" Create custom tempdir\n\n    Template path formatting is supporting:\n    - optional key formatting\n    - available keys:\n        - root[work | <root name key>]\n        - project[name | code]\n\n    Args:\n        project_name (str): project name\n        anatomy (openpype.pipeline.Anatomy)[optional]: Anatomy object\n\n    Returns:\n        str | None: formatted path or None\n    \"\"\"\n    openpype_tempdir = os.getenv(\"OPENPYPE_TMPDIR\")\n    if not openpype_tempdir:\n        return\n\n    custom_tempdir = None\n    if \"{\" in openpype_tempdir:\n        if anatomy is None:\n            anatomy = Anatomy(project_name)\n        # create base formate data\n        data = {\n            \"root\": anatomy.roots,\n            \"project\": {\n                \"name\": anatomy.project_name,\n                \"code\": anatomy.project_code,\n            }\n        }\n        # path is anatomy template\n        custom_tempdir = StringTemplate.format_template(\n            openpype_tempdir, data).normalized()\n\n    else:\n        # path is absolute\n        custom_tempdir = openpype_tempdir\n\n    # create the dir path if it doesn't exists\n    if not os.path.exists(custom_tempdir):\n        try:\n            # create it if it doesn't exists\n            os.makedirs(custom_tempdir)\n        except IOError as error:\n            raise IOError(\n                \"Path couldn't be created: {}\".format(error))\n\n    return custom_tempdir\n"
  },
  {
    "path": "openpype/pipeline/template_data.py",
    "content": "from openpype.client import get_project, get_asset_by_name\nfrom openpype.settings import get_system_settings\nfrom openpype.lib.local_settings import get_openpype_username\n\n\ndef get_general_template_data(system_settings=None):\n    \"\"\"General template data based on system settings or machine.\n\n    Output contains formatting keys:\n    - 'studio[name]'    - Studio name filled from system settings\n    - 'studio[code]'    - Studio code filled from system settings\n    - 'user'            - User's name using 'get_openpype_username'\n\n    Args:\n        system_settings (Dict[str, Any]): System settings.\n    \"\"\"\n\n    if not system_settings:\n        system_settings = get_system_settings()\n    studio_name = system_settings[\"general\"][\"studio_name\"]\n    studio_code = system_settings[\"general\"][\"studio_code\"]\n    return {\n        \"studio\": {\n            \"name\": studio_name,\n            \"code\": studio_code\n        },\n        \"user\": get_openpype_username()\n    }\n\n\ndef get_project_template_data(project_doc=None, project_name=None):\n    \"\"\"Extract data from project document that are used in templates.\n\n    Project document must have 'name' and (at this moment) optional\n        key 'data.code'.\n\n    One of 'project_name' or 'project_doc' must be passed. With prepared\n    project document is function much faster because don't have to query.\n\n    Output contains formatting keys:\n    - 'project[name]'   - Project name\n    - 'project[code]'   - Project code\n\n    Args:\n        project_doc (Dict[str, Any]): Queried project document.\n        project_name (str): Name of project.\n\n    Returns:\n        Dict[str, Dict[str, str]]: Template data based on project document.\n    \"\"\"\n\n    if not project_name:\n        project_name = project_doc[\"name\"]\n\n    if not project_doc:\n        project_doc = get_project(project_name, fields=[\"data.code\"])\n\n    project_code = project_doc.get(\"data\", {}).get(\"code\")\n    return {\n        \"project\": {\n            \"name\": project_name,\n            \"code\": project_code\n        }\n    }\n\n\ndef get_asset_template_data(asset_doc, project_name):\n    \"\"\"Extract data from asset document that are used in templates.\n\n    Output dictionary contains keys:\n    - 'asset'       - asset name\n    - 'hierarchy'   - parent asset names joined with '/'\n    - 'parent'      - direct parent name, project name used if is under project\n\n    Required document fields:\n        Asset: 'name', 'data.parents'\n\n    Args:\n        asset_doc (Dict[str, Any]): Queried asset document.\n        project_name (str): Is used for 'parent' key if asset doc does not have\n            any.\n\n    Returns:\n        Dict[str, str]: Data that are based on asset document and can be used\n            in templates.\n    \"\"\"\n\n    asset_parents = asset_doc[\"data\"][\"parents\"]\n    hierarchy = \"/\".join(asset_parents)\n    if asset_parents:\n        parent_name = asset_parents[-1]\n    else:\n        parent_name = project_name\n\n    return {\n        \"asset\": asset_doc[\"name\"],\n        \"folder\": {\n            \"name\": asset_doc[\"name\"]\n        },\n        \"hierarchy\": hierarchy,\n        \"parent\": parent_name\n    }\n\n\ndef get_task_type(asset_doc, task_name):\n    \"\"\"Get task type based on asset document and task name.\n\n    Required document fields:\n        Asset: 'data.tasks'\n\n    Args:\n        asset_doc (Dict[str, Any]): Queried asset document.\n        task_name (str): Task name which is under asset.\n\n    Returns:\n        str: Task type name.\n        None: Task was not found on asset document.\n    \"\"\"\n\n    asset_tasks_info = asset_doc[\"data\"][\"tasks\"]\n    return asset_tasks_info.get(task_name, {}).get(\"type\")\n\n\ndef get_task_template_data(project_doc, asset_doc, task_name):\n    \"\"\"\"Extract task specific data from project and asset documents.\n\n    Required document fields:\n        Project: 'config.tasks'\n        Asset: 'data.tasks'.\n\n    Args:\n        project_doc (Dict[str, Any]): Queried project document.\n        asset_doc (Dict[str, Any]): Queried asset document.\n        task_name (str): Name of task for which data should be returned.\n\n    Returns:\n        Dict[str, Dict[str, str]]: Template data\n    \"\"\"\n\n    project_task_types = project_doc[\"config\"][\"tasks\"]\n    task_type = get_task_type(asset_doc, task_name)\n    task_code = project_task_types.get(task_type, {}).get(\"short_name\")\n\n    return {\n        \"task\": {\n            \"name\": task_name,\n            \"type\": task_type,\n            \"short\": task_code,\n        }\n    }\n\n\ndef get_template_data(\n    project_doc,\n    asset_doc=None,\n    task_name=None,\n    host_name=None,\n    system_settings=None\n):\n    \"\"\"Prepare data for templates filling from entered documents and info.\n\n    This function does not \"auto fill\" any values except system settings and\n    it's on purpose.\n\n    Universal function to receive template data from passed arguments. Only\n    required argument is project document all other arguments are optional\n    and their values won't be added to template data if are not passed.\n\n    Required document fields:\n        Project: 'name', 'data.code', 'config.tasks'\n        Asset: 'name', 'data.parents', 'data.tasks'\n\n    Args:\n        project_doc (Dict[str, Any]): Mongo document of project from MongoDB.\n        asset_doc (Dict[str, Any]): Mongo document of asset from MongoDB.\n        task_name (Union[str, None]): Task name under passed asset.\n        host_name (Union[str, None]): Used to fill '{app}' key.\n        system_settings (Union[Dict, None]): Prepared system settings.\n            They're queried if not passed (may be slower).\n\n    Returns:\n        Dict[str, Any]: Data prepared for filling workdir template.\n    \"\"\"\n\n    template_data = get_general_template_data(system_settings)\n    template_data.update(get_project_template_data(project_doc))\n    if asset_doc:\n        template_data.update(get_asset_template_data(\n            asset_doc, project_doc[\"name\"]\n        ))\n        if task_name:\n            template_data.update(get_task_template_data(\n                project_doc, asset_doc, task_name\n            ))\n\n    if host_name:\n        template_data[\"app\"] = host_name\n\n    return template_data\n\n\ndef get_template_data_with_names(\n    project_name,\n    asset_name=None,\n    task_name=None,\n    host_name=None,\n    system_settings=None\n):\n    \"\"\"Prepare data for templates filling from entered entity names and info.\n\n    Copy of 'get_template_data' but based on entity names instead of documents.\n    Only difference is that documents are queried.\n\n    Args:\n        project_name (str): Project name for which template data are\n            calculated.\n        asset_name (Union[str, None]): Asset name for which template data are\n            calculated.\n        task_name (Union[str, None]): Task name under passed asset.\n        host_name (Union[str, None]):Used to fill '{app}' key.\n            because workdir template may contain `{app}` key.\n        system_settings (Union[Dict, None]): Prepared system settings.\n            They're queried if not passed.\n\n    Returns:\n        Dict[str, Any]: Data prepared for filling workdir template.\n    \"\"\"\n\n    project_doc = get_project(\n        project_name, fields=[\"name\", \"data.code\", \"config.tasks\"]\n    )\n    asset_doc = None\n    if asset_name:\n        asset_doc = get_asset_by_name(\n            project_name,\n            asset_name,\n            fields=[\"name\", \"data.parents\", \"data.tasks\"]\n        )\n    return get_template_data(\n        project_doc, asset_doc, task_name, host_name, system_settings\n    )\n"
  },
  {
    "path": "openpype/pipeline/thumbnail.py",
    "content": "import os\nimport copy\nimport logging\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.lib import Logger\nfrom openpype.client import get_project, get_ayon_server_api_connection\nfrom . import legacy_io\nfrom .anatomy import Anatomy\nfrom .plugin_discover import (\n    discover,\n    register_plugin,\n    register_plugin_path,\n)\n\n\ndef get_thumbnail_binary(thumbnail_entity, thumbnail_type, dbcon=None):\n    if not thumbnail_entity:\n        return\n\n    log = Logger.get_logger(__name__)\n    resolvers = discover_thumbnail_resolvers()\n    resolvers = sorted(resolvers, key=lambda cls: cls.priority)\n    if dbcon is None:\n        dbcon = legacy_io\n\n    for Resolver in resolvers:\n        available_types = Resolver.thumbnail_types\n        if (\n            thumbnail_type not in available_types\n            and \"*\" not in available_types\n            and (\n                isinstance(available_types, (list, tuple))\n                and len(available_types) == 0\n            )\n        ):\n            continue\n        try:\n            instance = Resolver(dbcon)\n            result = instance.process(thumbnail_entity, thumbnail_type)\n            if result:\n                return result\n\n        except Exception:\n            log.warning(\"Resolver {0} failed durring process.\".format(\n                Resolver.__class__.__name__, exc_info=True\n            ))\n\n\nclass ThumbnailResolver(object):\n    \"\"\"Determine how to get data from thumbnail entity.\n\n    \"priority\" - determines the order of processing in `get_thumbnail_binary`,\n        lower number is processed earlier.\n    \"thumbnail_types\" - it is expected that thumbnails will be used in more\n        more than one level, there is only [\"thumbnail\"] type at the moment\n        of creating this docstring but it is expected to add \"ico\" and \"full\"\n        in future.\n    \"\"\"\n\n    priority = 100\n    thumbnail_types = [\"*\"]\n\n    def __init__(self, dbcon):\n        self._log = None\n        self.dbcon = dbcon\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = logging.getLogger(self.__class__.__name__)\n        return self._log\n\n    def process(self, thumbnail_entity, thumbnail_type):\n        pass\n\n\nclass TemplateResolver(ThumbnailResolver):\n    priority = 90\n\n    def process(self, thumbnail_entity, thumbnail_type):\n        template = thumbnail_entity[\"data\"].get(\"template\")\n        if not template:\n            self.log.debug(\"Thumbnail entity does not have set template\")\n            return\n\n        thumbnail_root_format_key = \"{thumbnail_root}\"\n        thumbnail_root = os.environ.get(\"AVALON_THUMBNAIL_ROOT\") or \"\"\n        # Check if template require thumbnail root and if is avaiable\n        if thumbnail_root_format_key in template and not thumbnail_root:\n            return\n\n        project_name = self.dbcon.active_project()\n        project = get_project(project_name, fields=[\"name\", \"data.code\"])\n\n        template_data = copy.deepcopy(\n            thumbnail_entity[\"data\"].get(\"template_data\") or {}\n        )\n        template_data.update({\n            \"_id\": str(thumbnail_entity[\"_id\"]),\n            \"thumbnail_type\": thumbnail_type,\n            \"thumbnail_root\": thumbnail_root,\n            \"project\": {\n                \"name\": project[\"name\"],\n                \"code\": project[\"data\"].get(\"code\")\n            },\n        })\n        # Add anatomy roots if is in template\n        if \"{root\" in template:\n            anatomy = Anatomy(project_name)\n            template_data[\"root\"] = anatomy.roots\n\n        try:\n            filepath = os.path.normpath(template.format(**template_data))\n        except KeyError:\n            self.log.warning((\n                \"Missing template data keys for template <{0}> || Data: {1}\"\n            ).format(template, str(template_data)))\n            return\n\n        if not os.path.exists(filepath):\n            self.log.warning(\"File does not exist \\\"{0}\\\"\".format(filepath))\n            return\n\n        with open(filepath, \"rb\") as _file:\n            content = _file.read()\n\n        return content\n\n\nclass BinaryThumbnail(ThumbnailResolver):\n    def process(self, thumbnail_entity, thumbnail_type):\n        return thumbnail_entity[\"data\"].get(\"binary_data\")\n\n\nclass ServerThumbnailResolver(ThumbnailResolver):\n    _cache = None\n\n    @classmethod\n    def _get_cache(cls):\n        if cls._cache is None:\n            from openpype.client.server.thumbnails import AYONThumbnailCache\n\n            cls._cache = AYONThumbnailCache()\n        return cls._cache\n\n    def process(self, thumbnail_entity, thumbnail_type):\n        if not AYON_SERVER_ENABLED:\n            return None\n        data = thumbnail_entity[\"data\"]\n        entity_type = data.get(\"entity_type\")\n        entity_id = data.get(\"entity_id\")\n        if not entity_type or not entity_id:\n            return None\n\n        project_name = self.dbcon.active_project()\n        thumbnail_id = thumbnail_entity[\"_id\"]\n\n        cache = self._get_cache()\n        filepath = cache.get_thumbnail_filepath(project_name, thumbnail_id)\n        if filepath:\n            with open(filepath, \"rb\") as stream:\n                return stream.read()\n\n        # This is new way how thumbnails can be received from server\n        #   - output is 'ThumbnailContent' object\n        # NOTE Use 'get_server_api_connection' because public function\n        #   'get_thumbnail_by_id' does not return output of 'ServerAPI'\n        #   method.\n        con = get_ayon_server_api_connection()\n        if hasattr(con, \"get_thumbnail_by_id\"):\n            result = con.get_thumbnail_by_id(thumbnail_id)\n            if result.is_valid:\n                filepath = cache.store_thumbnail(\n                    project_name,\n                    thumbnail_id,\n                    result.content,\n                    result.content_type\n                )\n        else:\n            # Backwards compatibility for ayon api where 'get_thumbnail_by_id'\n            #   is not implemented and output is filepath\n            filepath = con.get_thumbnail(\n                project_name, entity_type, entity_id, thumbnail_id\n            )\n\n        if not filepath:\n            return None\n\n        with open(filepath, \"rb\") as stream:\n            return stream.read()\n\n\n# Thumbnail resolvers\ndef discover_thumbnail_resolvers():\n    return discover(ThumbnailResolver)\n\n\ndef register_thumbnail_resolver(plugin):\n    register_plugin(ThumbnailResolver, plugin)\n\n\ndef register_thumbnail_resolver_path(path):\n    register_plugin_path(ThumbnailResolver, path)\n\n\nregister_thumbnail_resolver(TemplateResolver)\nregister_thumbnail_resolver(BinaryThumbnail)\nregister_thumbnail_resolver(ServerThumbnailResolver)\n"
  },
  {
    "path": "openpype/pipeline/version_start.py",
    "content": "from openpype.lib.profiles_filtering import filter_profiles\nfrom openpype.settings import get_project_settings\n\n\ndef get_versioning_start(\n    project_name,\n    host_name,\n    task_name=None,\n    task_type=None,\n    family=None,\n    subset=None,\n    project_settings=None,\n):\n    \"\"\"Get anatomy versioning start\"\"\"\n    if not project_settings:\n        project_settings = get_project_settings(project_name)\n\n    version_start = 1\n    settings = project_settings[\"global\"]\n    profiles = settings.get(\"version_start_category\", {}).get(\"profiles\", [])\n\n    if not profiles:\n        return version_start\n\n    filtering_criteria = {\n        \"host_names\": host_name,\n        \"families\": family,\n        \"task_names\": task_name,\n        \"task_types\": task_type,\n        \"subsets\": subset\n    }\n    profile = filter_profiles(profiles, filtering_criteria)\n\n    if profile is None:\n        return version_start\n\n    return profile[\"version_start\"]\n"
  },
  {
    "path": "openpype/pipeline/workfile/__init__.py",
    "content": "from .path_resolving import (\n    get_workfile_template_key_from_context,\n    get_workfile_template_key,\n    get_workdir_with_workdir_data,\n    get_workdir,\n\n    get_last_workfile_with_version,\n    get_last_workfile,\n\n    get_custom_workfile_template,\n    get_custom_workfile_template_by_string_context,\n\n    create_workdir_extra_folders,\n)\n\nfrom .build_workfile import BuildWorkfile\n\n\n__all__ = (\n    \"get_workfile_template_key_from_context\",\n    \"get_workfile_template_key\",\n    \"get_workdir_with_workdir_data\",\n    \"get_workdir\",\n\n    \"get_last_workfile_with_version\",\n    \"get_last_workfile\",\n\n    \"get_custom_workfile_template\",\n    \"get_custom_workfile_template_by_string_context\",\n\n    \"create_workdir_extra_folders\",\n\n    \"BuildWorkfile\",\n)\n"
  },
  {
    "path": "openpype/pipeline/workfile/build_workfile.py",
    "content": "\"\"\"Workfile build based on settings.\n\nWorkfile builder will do stuff based on project settings. Advantage is that\nit need only access to settings. Disadvantage is that it is hard to focus\nbuild per context and being explicit about loaded content.\n\nFor more explicit workfile build is recommended 'AbstractTemplateBuilder'\nfrom '~/openpype/pipeline/workfile/workfile_template_builder'. Which gives\nmore abilities to define how build happens but require more code to achive it.\n\"\"\"\n\nimport re\nimport collections\nimport json\n\nfrom openpype.client import (\n    get_asset_by_name,\n    get_subsets,\n    get_last_versions,\n    get_representations,\n    get_linked_assets,\n)\nfrom openpype.settings import get_project_settings\nfrom openpype.lib import (\n    filter_profiles,\n    Logger,\n)\nfrom openpype.pipeline.load import (\n    discover_loader_plugins,\n    IncompatibleLoaderError,\n    load_container,\n)\n\n\nclass BuildWorkfile:\n    \"\"\"Wrapper for build workfile process.\n\n    Load representations for current context by build presets. Build presets\n    are host related, since each host has it's loaders.\n    \"\"\"\n\n    _log = None\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    @staticmethod\n    def map_subsets_by_family(subsets):\n        subsets_by_family = collections.defaultdict(list)\n        for subset in subsets:\n            family = subset[\"data\"].get(\"family\")\n            if not family:\n                families = subset[\"data\"].get(\"families\")\n                if not families:\n                    continue\n                family = families[0]\n\n            subsets_by_family[family].append(subset)\n        return subsets_by_family\n\n    def process(self):\n        \"\"\"Main method of this wrapper.\n\n        Building of workfile is triggered and is possible to implement\n        post processing of loaded containers if necessary.\n\n        Returns:\n            List[Dict[str, Any]]: Loaded containers during build.\n        \"\"\"\n\n        return self.build_workfile()\n\n    def build_workfile(self):\n        \"\"\"Prepares and load containers into workfile.\n\n        Loads latest versions of current and linked assets to workfile by logic\n        stored in Workfile profiles from presets. Profiles are set by host,\n        filtered by current task name and used by families.\n\n        Each family can specify representation names and loaders for\n        representations and first available and successful loaded\n        representation is returned as container.\n\n        At the end you'll get list of loaded containers per each asset.\n\n        loaded_containers [{\n            \"asset_entity\": <AssetEntity1>,\n            \"containers\": [<Container1>, <Container2>, ...]\n        }, {\n            \"asset_entity\": <AssetEntity2>,\n            \"containers\": [<Container3>, ...]\n        }, {\n            ...\n        }]\n\n        Returns:\n            List[Dict[str, Any]]: Loaded containers during build.\n        \"\"\"\n\n        from openpype.pipeline.context_tools import (\n            get_current_project_name,\n            get_current_asset_name,\n            get_current_task_name,\n        )\n\n        loaded_containers = []\n\n        # Get current asset name and entity\n        project_name = get_current_project_name()\n        current_asset_name = get_current_asset_name()\n        current_asset_entity = get_asset_by_name(\n            project_name, current_asset_name\n        )\n        # Skip if asset was not found\n        if not current_asset_entity:\n            print(\"Asset entity with name `{}` was not found\".format(\n                current_asset_name\n            ))\n            return loaded_containers\n\n        # Prepare available loaders\n        loaders_by_name = {}\n        for loader in discover_loader_plugins():\n            if not loader.enabled:\n                continue\n            loader_name = loader.__name__\n            if loader_name in loaders_by_name:\n                raise KeyError(\n                    \"Duplicated loader name {0}!\".format(loader_name)\n                )\n            loaders_by_name[loader_name] = loader\n\n        # Skip if there are any loaders\n        if not loaders_by_name:\n            self.log.warning(\"There are no registered loaders.\")\n            return loaded_containers\n\n        # Get current task name\n        current_task_name = get_current_task_name()\n\n        # Load workfile presets for task\n        self.build_presets = self.get_build_presets(\n            current_task_name, current_asset_entity\n        )\n\n        # Skip if there are any presets for task\n        if not self.build_presets:\n            self.log.warning(\n                \"Current task `{}` does not have any loading preset.\".format(\n                    current_task_name\n                )\n            )\n            return loaded_containers\n\n        # Get presets for loading current asset\n        current_context_profiles = self.build_presets.get(\"current_context\")\n        # Get presets for loading linked assets\n        link_context_profiles = self.build_presets.get(\"linked_assets\")\n        # Skip if both are missing\n        if not current_context_profiles and not link_context_profiles:\n            self.log.warning(\n                \"Current task `{}` has empty loading preset.\".format(\n                    current_task_name\n                )\n            )\n            return loaded_containers\n\n        elif not current_context_profiles:\n            self.log.warning((\n                \"Current task `{}` doesn't have any loading\"\n                \" preset for it's context.\"\n            ).format(current_task_name))\n\n        elif not link_context_profiles:\n            self.log.warning((\n                \"Current task `{}` doesn't have any\"\n                \"loading preset for it's linked assets.\"\n            ).format(current_task_name))\n\n        # Prepare assets to process by workfile presets\n        assets = []\n        current_asset_id = None\n        if current_context_profiles:\n            # Add current asset entity if preset has current context set\n            assets.append(current_asset_entity)\n            current_asset_id = current_asset_entity[\"_id\"]\n\n        if link_context_profiles:\n            # Find and append linked assets if preset has set linked mapping\n            link_assets = get_linked_assets(project_name, current_asset_entity)\n            if link_assets:\n                assets.extend(link_assets)\n\n        # Skip if there are no assets. This can happen if only linked mapping\n        # is set and there are no links for his asset.\n        if not assets:\n            self.log.warning(\n                \"Asset does not have linked assets. Nothing to process.\"\n            )\n            return loaded_containers\n\n        # Prepare entities from database for assets\n        prepared_entities = self._collect_last_version_repres(assets)\n\n        # Load containers by prepared entities and presets\n        # - Current asset containers\n        if current_asset_id and current_asset_id in prepared_entities:\n            current_context_data = prepared_entities.pop(current_asset_id)\n            loaded_data = self.load_containers_by_asset_data(\n                current_context_data, current_context_profiles, loaders_by_name\n            )\n            if loaded_data:\n                loaded_containers.append(loaded_data)\n\n        # - Linked assets container\n        for linked_asset_data in prepared_entities.values():\n            loaded_data = self.load_containers_by_asset_data(\n                linked_asset_data, link_context_profiles, loaders_by_name\n            )\n            if loaded_data:\n                loaded_containers.append(loaded_data)\n\n        # Return list of loaded containers\n        return loaded_containers\n\n    def get_build_presets(self, task_name, asset_doc):\n        \"\"\" Returns presets to build workfile for task name.\n\n        Presets are loaded for current project set in\n        io.Session[\"AVALON_PROJECT\"], filtered by registered host\n        and entered task name.\n\n        Args:\n            task_name (str): Task name used for filtering build presets.\n\n        Returns:\n            Dict[str, Any]: preset per entered task name\n        \"\"\"\n\n        from openpype.pipeline.context_tools import (\n            get_current_host_name,\n            get_current_project_name,\n        )\n\n        host_name = get_current_host_name()\n        project_settings = get_project_settings(\n            get_current_project_name()\n        )\n\n        host_settings = project_settings.get(host_name) or {}\n        # Get presets for host\n        wb_settings = host_settings.get(\"workfile_builder\")\n        if not wb_settings:\n            # backward compatibility\n            wb_settings = host_settings.get(\"workfile_build\") or {}\n\n        builder_profiles = wb_settings.get(\"profiles\")\n        if not builder_profiles:\n            return None\n\n        task_type = (\n            asset_doc\n            .get(\"data\", {})\n            .get(\"tasks\", {})\n            .get(task_name, {})\n            .get(\"type\")\n        )\n        filter_data = {\n            \"task_types\": task_type,\n            \"tasks\": task_name\n        }\n        return filter_profiles(builder_profiles, filter_data)\n\n    def _filter_build_profiles(self, build_profiles, loaders_by_name):\n        \"\"\" Filter build profiles by loaders and prepare process data.\n\n        Valid profile must have \"loaders\", \"families\" and \"repre_names\" keys\n        with valid values.\n        - \"loaders\" expects list of strings representing possible loaders.\n        - \"families\" expects list of strings for filtering\n                     by main subset family.\n        - \"repre_names\" expects list of strings for filtering by\n                        representation name.\n\n        Lowered \"families\" and \"repre_names\" are prepared for each profile with\n        all required keys.\n\n        Args:\n            build_profiles (Dict[str, Any]): Profiles for building workfile.\n            loaders_by_name (Dict[str, LoaderPlugin]): Available loaders\n                per name.\n\n        Returns:\n            List[Dict[str, Any]]: Filtered and prepared profiles.\n        \"\"\"\n\n        valid_profiles = []\n        for profile in build_profiles:\n            # Check loaders\n            profile_loaders = profile.get(\"loaders\")\n            if not profile_loaders:\n                self.log.warning((\n                    \"Build profile has missing loaders configuration: {0}\"\n                ).format(json.dumps(profile, indent=4)))\n                continue\n\n            # Check if any loader is available\n            loaders_match = False\n            for loader_name in profile_loaders:\n                if loader_name in loaders_by_name:\n                    loaders_match = True\n                    break\n\n            if not loaders_match:\n                self.log.warning((\n                    \"All loaders from Build profile are not available: {0}\"\n                ).format(json.dumps(profile, indent=4)))\n                continue\n\n            # Check families\n            profile_families = profile.get(\"families\")\n            if not profile_families:\n                self.log.warning((\n                    \"Build profile is missing families configuration: {0}\"\n                ).format(json.dumps(profile, indent=4)))\n                continue\n\n            # Check representation names\n            profile_repre_names = profile.get(\"repre_names\")\n            if not profile_repre_names:\n                self.log.warning((\n                    \"Build profile is missing\"\n                    \" representation names filtering: {0}\"\n                ).format(json.dumps(profile, indent=4)))\n                continue\n\n            # Prepare lowered families and representation names\n            profile[\"families_lowered\"] = [\n                fam.lower() for fam in profile_families\n            ]\n            profile[\"repre_names_lowered\"] = [\n                name.lower() for name in profile_repre_names\n            ]\n\n            valid_profiles.append(profile)\n\n        return valid_profiles\n\n    def _prepare_profile_for_subsets(self, subsets, profiles):\n        \"\"\"Select profile for each subset by it's data.\n\n        Profiles are filtered for each subset individually.\n        Profile is filtered by subset's family, optionally by name regex and\n        representation names set in profile.\n        It is possible to not find matching profile for subset, in that case\n        subset is skipped and it is possible that none of subsets have\n        matching profile.\n\n        Args:\n            subsets (List[Dict[str, Any]]): Subset documents.\n            profiles (List[Dict[str, Any]]): Build profiles.\n\n        Returns:\n            Dict[str, Any]: Profile by subset's id.\n        \"\"\"\n\n        # Prepare subsets\n        subsets_by_family = self.map_subsets_by_family(subsets)\n\n        profiles_per_subset_id = {}\n        for family, subsets in subsets_by_family.items():\n            family_low = family.lower()\n            for profile in profiles:\n                # Skip profile if does not contain family\n                if family_low not in profile[\"families_lowered\"]:\n                    continue\n\n                # Precompile name filters as regexes\n                profile_regexes = profile.get(\"subset_name_filters\")\n                if profile_regexes:\n                    _profile_regexes = []\n                    for regex in profile_regexes:\n                        _profile_regexes.append(re.compile(regex))\n                    profile_regexes = _profile_regexes\n\n                # TODO prepare regex compilation\n                for subset in subsets:\n                    # Verify regex filtering (optional)\n                    if profile_regexes:\n                        valid = False\n                        for pattern in profile_regexes:\n                            if re.match(pattern, subset[\"name\"]):\n                                valid = True\n                                break\n\n                        if not valid:\n                            continue\n\n                    profiles_per_subset_id[subset[\"_id\"]] = profile\n\n                # break profiles loop on finding the first matching profile\n                break\n        return profiles_per_subset_id\n\n    def load_containers_by_asset_data(\n        self, asset_entity_data, build_profiles, loaders_by_name\n    ):\n        \"\"\"Load containers for entered asset entity by Build profiles.\n\n        Args:\n            asset_entity_data (Dict[str, Any]): Prepared data with subsets,\n                last versions and representations for specific asset.\n            build_profiles (Dict[str, Any]): Build profiles.\n            loaders_by_name (Dict[str, LoaderPlugin]): Available loaders\n                per name.\n\n        Returns:\n            Dict[str, Any]: Output contains asset document\n                and loaded containers.\n        \"\"\"\n\n        # Make sure all data are not empty\n        if not asset_entity_data or not build_profiles or not loaders_by_name:\n            return\n\n        asset_entity = asset_entity_data[\"asset_entity\"]\n\n        valid_profiles = self._filter_build_profiles(\n            build_profiles, loaders_by_name\n        )\n        if not valid_profiles:\n            self.log.warning(\n                \"There are not valid Workfile profiles. Skipping process.\"\n            )\n            return\n\n        self.log.debug(\"Valid Workfile profiles: {}\".format(valid_profiles))\n\n        subsets_by_id = {}\n        version_by_subset_id = {}\n        repres_by_version_id = {}\n        for subset_id, in_data in asset_entity_data[\"subsets\"].items():\n            subset_entity = in_data[\"subset_entity\"]\n            subsets_by_id[subset_entity[\"_id\"]] = subset_entity\n\n            version_data = in_data[\"version\"]\n            version_entity = version_data[\"version_entity\"]\n            version_by_subset_id[subset_id] = version_entity\n            repres_by_version_id[version_entity[\"_id\"]] = (\n                version_data[\"repres\"]\n            )\n\n        if not subsets_by_id:\n            self.log.warning(\"There are not subsets for asset {0}\".format(\n                asset_entity[\"name\"]\n            ))\n            return\n\n        profiles_per_subset_id = self._prepare_profile_for_subsets(\n            subsets_by_id.values(), valid_profiles\n        )\n        if not profiles_per_subset_id:\n            self.log.warning(\"There are not valid subsets.\")\n            return\n\n        valid_repres_by_subset_id = collections.defaultdict(list)\n        for subset_id, profile in profiles_per_subset_id.items():\n            profile_repre_names = profile[\"repre_names_lowered\"]\n\n            version_entity = version_by_subset_id[subset_id]\n            version_id = version_entity[\"_id\"]\n            repres = repres_by_version_id[version_id]\n            for repre in repres:\n                repre_name_low = repre[\"name\"].lower()\n                if repre_name_low in profile_repre_names:\n                    valid_repres_by_subset_id[subset_id].append(repre)\n\n        # DEBUG message\n        msg = \"Valid representations for Asset: `{}`\".format(\n            asset_entity[\"name\"]\n        )\n        for subset_id, repres in valid_repres_by_subset_id.items():\n            subset = subsets_by_id[subset_id]\n            msg += \"\\n# Subset Name/ID: `{}`/{}\".format(\n                subset[\"name\"], subset_id\n            )\n            for repre in repres:\n                msg += \"\\n## Repre name: `{}`\".format(repre[\"name\"])\n\n        self.log.debug(msg)\n\n        containers = self._load_containers(\n            valid_repres_by_subset_id, subsets_by_id,\n            profiles_per_subset_id, loaders_by_name\n        )\n\n        return {\n            \"asset_entity\": asset_entity,\n            \"containers\": containers\n        }\n\n    def _load_containers(\n        self, repres_by_subset_id, subsets_by_id,\n        profiles_per_subset_id, loaders_by_name\n    ):\n        \"\"\"Real load by collected data happens here.\n\n        Loading of representations per subset happens here. Each subset can\n        loads one representation. Loading is tried in specific order.\n        Representations are tried to load by names defined in configuration.\n        If subset has representation matching representation name each loader\n        is tried to load it until any is successful. If none of them was\n        successful then next representation name is tried.\n        Subset process loop ends when any representation is loaded or\n        all matching representations were already tried.\n\n        Args:\n            repres_by_subset_id (Dict[str, Dict[str, Any]]): Available\n                representations mapped by their parent (subset) id.\n            subsets_by_id (Dict[str, Dict[str, Any]]): Subset documents\n                mapped by their id.\n            profiles_per_subset_id (Dict[str, Dict[str, Any]]): Build profiles\n                mapped by subset id.\n            loaders_by_name (Dict[str, LoaderPlugin]): Available loaders\n                per name.\n\n        Returns:\n            List[Dict[str, Any]]: Objects of loaded containers.\n        \"\"\"\n\n        loaded_containers = []\n\n        # Get subset id order from build presets.\n        build_presets = self.build_presets.get(\"current_context\", [])\n        build_presets += self.build_presets.get(\"linked_assets\", [])\n        subset_ids_ordered = []\n        for preset in build_presets:\n            for preset_family in preset[\"families\"]:\n                for id, subset in subsets_by_id.items():\n                    if preset_family not in subset[\"data\"].get(\"families\", []):\n                        continue\n\n                    subset_ids_ordered.append(id)\n\n        # Order representations from subsets.\n        print(\"repres_by_subset_id\", repres_by_subset_id)\n        representations_ordered = []\n        representations = []\n        for id in subset_ids_ordered:\n            for subset_id, repres in repres_by_subset_id.items():\n                if repres in representations:\n                    continue\n\n                if id == subset_id:\n                    representations_ordered.append((subset_id, repres))\n                    representations.append(repres)\n\n        print(\"representations\", representations)\n\n        # Load ordered representations.\n        for subset_id, repres in representations_ordered:\n            subset_name = subsets_by_id[subset_id][\"name\"]\n\n            profile = profiles_per_subset_id[subset_id]\n            loaders_last_idx = len(profile[\"loaders\"]) - 1\n            repre_names_last_idx = len(profile[\"repre_names_lowered\"]) - 1\n\n            repre_by_low_name = {\n                repre[\"name\"].lower(): repre for repre in repres\n            }\n\n            is_loaded = False\n            for repre_name_idx, profile_repre_name in enumerate(\n                profile[\"repre_names_lowered\"]\n            ):\n                # Break iteration if representation was already loaded\n                if is_loaded:\n                    break\n\n                repre = repre_by_low_name.get(profile_repre_name)\n                if not repre:\n                    continue\n\n                for loader_idx, loader_name in enumerate(profile[\"loaders\"]):\n                    if is_loaded:\n                        break\n\n                    loader = loaders_by_name.get(loader_name)\n                    if not loader:\n                        continue\n                    try:\n                        container = load_container(\n                            loader,\n                            repre[\"_id\"],\n                            name=subset_name\n                        )\n                        loaded_containers.append(container)\n                        is_loaded = True\n\n                    except Exception as exc:\n                        if exc == IncompatibleLoaderError:\n                            self.log.info((\n                                \"Loader `{}` is not compatible with\"\n                                \" representation `{}`\"\n                            ).format(loader_name, repre[\"name\"]))\n\n                        else:\n                            self.log.error(\n                                \"Unexpected error happened during loading\",\n                                exc_info=True\n                            )\n\n                        msg = \"Loading failed.\"\n                        if loader_idx < loaders_last_idx:\n                            msg += \" Trying next loader.\"\n                        elif repre_name_idx < repre_names_last_idx:\n                            msg += (\n                                \" Loading of subset `{}` was not successful.\"\n                            ).format(subset_name)\n                        else:\n                            msg += \" Trying next representation.\"\n                        self.log.info(msg)\n\n        return loaded_containers\n\n    def _collect_last_version_repres(self, asset_docs):\n        \"\"\"Collect subsets, versions and representations for asset_entities.\n\n        Args:\n            asset_docs (List[Dict[str, Any]]): Asset entities for which\n                want to find data.\n\n        Returns:\n            Dict[str, Any]: collected entities\n\n        Example output:\n        ```\n        {\n            {Asset ID}: {\n                \"asset_entity\": <AssetEntity>,\n                \"subsets\": {\n                    {Subset ID}: {\n                        \"subset_entity\": <SubsetEntity>,\n                        \"version\": {\n                            \"version_entity\": <VersionEntity>,\n                            \"repres\": [\n                                <RepreEntity1>, <RepreEntity2>, ...\n                            ]\n                        }\n                    },\n                    ...\n                }\n            },\n            ...\n        }\n        output[asset_id][\"subsets\"][subset_id][\"version\"][\"repres\"]\n        ```\n        \"\"\"\n\n        from openpype.pipeline.context_tools import get_current_project_name\n\n        output = {}\n        if not asset_docs:\n            return output\n\n        asset_docs_by_ids = {asset[\"_id\"]: asset for asset in asset_docs}\n\n        project_name = get_current_project_name()\n        subsets = list(get_subsets(\n            project_name, asset_ids=asset_docs_by_ids.keys()\n        ))\n        subset_entity_by_ids = {subset[\"_id\"]: subset for subset in subsets}\n\n        last_version_by_subset_id = get_last_versions(\n            project_name, subset_entity_by_ids.keys()\n        )\n        last_version_docs_by_id = {\n            version[\"_id\"]: version\n            for version in last_version_by_subset_id.values()\n        }\n        repre_docs = get_representations(\n            project_name, version_ids=last_version_docs_by_id.keys()\n        )\n\n        for repre_doc in repre_docs:\n            version_id = repre_doc[\"parent\"]\n            version_doc = last_version_docs_by_id[version_id]\n\n            subset_id = version_doc[\"parent\"]\n            subset_doc = subset_entity_by_ids[subset_id]\n\n            asset_id = subset_doc[\"parent\"]\n            asset_doc = asset_docs_by_ids[asset_id]\n\n            if asset_id not in output:\n                output[asset_id] = {\n                    \"asset_entity\": asset_doc,\n                    \"subsets\": {}\n                }\n\n            if subset_id not in output[asset_id][\"subsets\"]:\n                output[asset_id][\"subsets\"][subset_id] = {\n                    \"subset_entity\": subset_doc,\n                    \"version\": {\n                        \"version_entity\": version_doc,\n                        \"repres\": []\n                    }\n                }\n\n            output[asset_id][\"subsets\"][subset_id][\"version\"][\"repres\"].append(\n                repre_doc\n            )\n\n        return output\n"
  },
  {
    "path": "openpype/pipeline/workfile/lock_workfile.py",
    "content": "import os\nimport json\nfrom openpype.lib import Logger, filter_profiles\nfrom openpype.lib.pype_info import get_workstation_info\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import get_process_id\n\n\ndef _read_lock_file(lock_filepath):\n    if not os.path.exists(lock_filepath):\n        log = Logger.get_logger(\"_read_lock_file\")\n        log.debug(\"lock file is not created or readable as expected!\")\n    with open(lock_filepath, \"r\") as stream:\n        data = json.load(stream)\n    return data\n\n\ndef _get_lock_file(filepath):\n    return filepath + \".oplock\"\n\n\ndef is_workfile_locked(filepath):\n    lock_filepath = _get_lock_file(filepath)\n    if not os.path.exists(lock_filepath):\n        return False\n    return True\n\n\ndef get_workfile_lock_data(filepath):\n    lock_filepath = _get_lock_file(filepath)\n    return _read_lock_file(lock_filepath)\n\n\ndef is_workfile_locked_for_current_process(filepath):\n    if not is_workfile_locked(filepath):\n        return False\n\n    lock_filepath = _get_lock_file(filepath)\n    data = _read_lock_file(lock_filepath)\n    return data[\"process_id\"] == get_process_id()\n\n\ndef delete_workfile_lock(filepath):\n    lock_filepath = _get_lock_file(filepath)\n    if os.path.exists(lock_filepath):\n        os.remove(lock_filepath)\n\n\ndef create_workfile_lock(filepath):\n    lock_filepath = _get_lock_file(filepath)\n    info = get_workstation_info()\n    info[\"process_id\"] = get_process_id()\n    with open(lock_filepath, \"w\") as stream:\n        json.dump(info, stream)\n\n\ndef remove_workfile_lock(filepath):\n    if is_workfile_locked_for_current_process(filepath):\n        delete_workfile_lock(filepath)\n\n\ndef is_workfile_lock_enabled(host_name, project_name, project_setting=None):\n    if project_setting is None:\n        project_setting = get_project_settings(project_name)\n    workfile_lock_profiles = (\n        project_setting\n        [\"global\"]\n        [\"tools\"]\n        [\"Workfiles\"]\n        [\"workfile_lock_profiles\"])\n    profile = filter_profiles(workfile_lock_profiles, {\"host_name\": host_name})\n    if not profile:\n        return False\n    return profile[\"enabled\"]\n"
  },
  {
    "path": "openpype/pipeline/workfile/path_resolving.py",
    "content": "import os\nimport re\nimport copy\nimport platform\n\nfrom openpype.client import get_project, get_asset_by_name\nfrom openpype.settings import get_project_settings\nfrom openpype.lib import (\n    filter_profiles,\n    Logger,\n    StringTemplate,\n)\nfrom openpype.pipeline import version_start, Anatomy\nfrom openpype.pipeline.template_data import get_template_data\n\n\ndef get_workfile_template_key_from_context(\n    asset_name, task_name, host_name, project_name, project_settings=None\n):\n    \"\"\"Helper function to get template key for workfile template.\n\n    Do the same as `get_workfile_template_key` but returns value for \"session\n    context\".\n\n    Args:\n        asset_name(str): Name of asset document.\n        task_name(str): Task name for which is template key retrieved.\n            Must be available on asset document under `data.tasks`.\n        host_name(str): Name of host implementation for which is workfile\n            used.\n        project_name(str): Project name where asset and task is.\n        project_settings(Dict[str, Any]): Project settings for passed\n            'project_name'. Not required at all but makes function faster.\n    \"\"\"\n\n    asset_doc = get_asset_by_name(\n        project_name, asset_name, fields=[\"data.tasks\"]\n    )\n    asset_tasks = asset_doc.get(\"data\", {}).get(\"tasks\") or {}\n    task_info = asset_tasks.get(task_name) or {}\n    task_type = task_info.get(\"type\")\n\n    return get_workfile_template_key(\n        task_type, host_name, project_name, project_settings\n    )\n\n\ndef get_workfile_template_key(\n    task_type, host_name, project_name, project_settings=None\n):\n    \"\"\"Workfile template key which should be used to get workfile template.\n\n    Function is using profiles from project settings to return right template\n    for passet task type and host name.\n\n    Args:\n        task_type(str): Name of task type.\n        host_name(str): Name of host implementation (e.g. \"maya\", \"nuke\", ...)\n        project_name(str): Name of project in which context should look for\n            settings.\n        project_settings(Dict[str, Any]): Prepared project settings for\n            project name. Optional to make processing faster.\n    \"\"\"\n\n    default = \"work\"\n    if not task_type or not host_name:\n        return default\n\n    if not project_settings:\n        project_settings = get_project_settings(project_name)\n\n    try:\n        profiles = (\n            project_settings\n            [\"global\"]\n            [\"tools\"]\n            [\"Workfiles\"]\n            [\"workfile_template_profiles\"]\n        )\n    except Exception:\n        profiles = []\n\n    if not profiles:\n        return default\n\n    profile_filter = {\n        \"task_types\": task_type,\n        \"hosts\": host_name\n    }\n    profile = filter_profiles(profiles, profile_filter)\n    if profile:\n        return profile[\"workfile_template\"] or default\n    return default\n\n\ndef get_workdir_with_workdir_data(\n    workdir_data,\n    project_name,\n    anatomy=None,\n    template_key=None,\n    project_settings=None\n):\n    \"\"\"Fill workdir path from entered data and project's anatomy.\n\n    It is possible to pass only project's name instead of project's anatomy but\n    one of them **must** be entered. It is preferred to enter anatomy if is\n    available as initialization of a new Anatomy object may be time consuming.\n\n    Args:\n        workdir_data (Dict[str, Any]): Data to fill workdir template.\n        project_name (str): Project's name.\n        anatomy (Anatomy): Anatomy object for specific project. Faster\n            processing if is passed.\n        template_key (str): Key of work templates in anatomy templates. If not\n            passed `get_workfile_template_key_from_context` is used to get it.\n        project_settings(Dict[str, Any]): Prepared project settings for\n            project name. Optional to make processing faster. Ans id used only\n            if 'template_key' is not passed.\n\n    Returns:\n        TemplateResult: Workdir path.\n    \"\"\"\n\n    if not anatomy:\n        anatomy = Anatomy(project_name)\n\n    if not template_key:\n        template_key = get_workfile_template_key(\n            workdir_data[\"task\"][\"type\"],\n            workdir_data[\"app\"],\n            workdir_data[\"project\"][\"name\"],\n            project_settings\n        )\n\n    template_obj = anatomy.templates_obj[template_key][\"folder\"]\n    # Output is TemplateResult object which contain useful data\n    output = template_obj.format_strict(workdir_data)\n    if output:\n        return output.normalized()\n    return output\n\n\ndef get_workdir(\n    project_doc,\n    asset_doc,\n    task_name,\n    host_name,\n    anatomy=None,\n    template_key=None,\n    project_settings=None\n):\n    \"\"\"Fill workdir path from entered data and project's anatomy.\n\n    Args:\n        project_doc (Dict[str, Any]): Mongo document of project from MongoDB.\n        asset_doc (Dict[str, Any]): Mongo document of asset from MongoDB.\n        task_name (str): Task name for which are workdir data preapred.\n        host_name (str): Host which is used to workdir. This is required\n            because workdir template may contain `{app}` key. In `Session`\n            is stored under `AVALON_APP` key.\n        anatomy (Anatomy): Optional argument. Anatomy object is created using\n            project name from `project_doc`. It is preferred to pass this\n            argument as initialization of a new Anatomy object may be time\n            consuming.\n        template_key (str): Key of work templates in anatomy templates. Default\n            value is defined in `get_workdir_with_workdir_data`.\n        project_settings(Dict[str, Any]): Prepared project settings for\n            project name. Optional to make processing faster. Ans id used only\n            if 'template_key' is not passed.\n\n    Returns:\n        TemplateResult: Workdir path.\n    \"\"\"\n\n    if not anatomy:\n        anatomy = Anatomy(project_doc[\"name\"])\n\n    workdir_data = get_template_data(\n        project_doc, asset_doc, task_name, host_name\n    )\n    # Output is TemplateResult object which contain useful data\n    return get_workdir_with_workdir_data(\n        workdir_data,\n        anatomy.project_name,\n        anatomy,\n        template_key,\n        project_settings\n    )\n\n\ndef get_last_workfile_with_version(\n    workdir, file_template, fill_data, extensions\n):\n    \"\"\"Return last workfile version.\n\n    Usign workfile template and it's filling data find most possible last\n    version of workfile which was created for the context.\n\n    Functionality is fully based on knowing which keys are optional or what\n    values are expected as value.\n\n    The last modified file is used if more files can be considered as\n    last workfile.\n\n    Args:\n        workdir (str): Path to dir where workfiles are stored.\n        file_template (str): Template of file name.\n        fill_data (Dict[str, Any]): Data for filling template.\n        extensions (Iterable[str]): All allowed file extensions of workfile.\n\n    Returns:\n        Tuple[Union[str, None], Union[int, None]]: Last workfile with version\n            if there is any workfile otherwise None for both.\n    \"\"\"\n\n    if not os.path.exists(workdir):\n        return None, None\n\n    dotted_extensions = set()\n    for ext in extensions:\n        if not ext.startswith(\".\"):\n            ext = \".{}\".format(ext)\n        dotted_extensions.add(ext)\n\n    # Fast match on extension\n    filenames = [\n        filename\n        for filename in os.listdir(workdir)\n        if os.path.splitext(filename)[-1] in dotted_extensions\n    ]\n\n    # Build template without optionals, version to digits only regex\n    # and comment to any definable value.\n    # Escape extensions dot for regex\n    regex_exts = [\n        \"\\\\\" + ext\n        for ext in dotted_extensions\n    ]\n    ext_expression = \"(?:\" + \"|\".join(regex_exts) + \")\"\n\n    # Replace `.{ext}` with `{ext}` so we are sure there is not dot at the end\n    file_template = re.sub(r\"\\.?{ext}\", ext_expression, file_template)\n    # Replace optional keys with optional content regex\n    file_template = re.sub(r\"<.*?>\", r\".*?\", file_template)\n    # Replace `{version}` with group regex\n    file_template = re.sub(r\"{version.*?}\", r\"([0-9]+)\", file_template)\n    file_template = re.sub(r\"{comment.*?}\", r\".+?\", file_template)\n    file_template = StringTemplate.format_strict_template(\n        file_template, fill_data\n    )\n\n    # Match with ignore case on Windows due to the Windows\n    # OS not being case-sensitive. This avoids later running\n    # into the error that the file did exist if it existed\n    # with a different upper/lower-case.\n    kwargs = {}\n    if platform.system().lower() == \"windows\":\n        kwargs[\"flags\"] = re.IGNORECASE\n\n    # Get highest version among existing matching files\n    version = None\n    output_filenames = []\n    for filename in sorted(filenames):\n        match = re.match(file_template, filename, **kwargs)\n        if not match:\n            continue\n\n        if not match.groups():\n            output_filenames.append(filename)\n            continue\n\n        file_version = int(match.group(1))\n        if version is None or file_version > version:\n            output_filenames[:] = []\n            version = file_version\n\n        if file_version == version:\n            output_filenames.append(filename)\n\n    output_filename = None\n    if output_filenames:\n        if len(output_filenames) == 1:\n            output_filename = output_filenames[0]\n        else:\n            last_time = None\n            for _output_filename in output_filenames:\n                full_path = os.path.join(workdir, _output_filename)\n                mod_time = os.path.getmtime(full_path)\n                if last_time is None or last_time < mod_time:\n                    output_filename = _output_filename\n                    last_time = mod_time\n\n    return output_filename, version\n\n\ndef get_last_workfile(\n    workdir, file_template, fill_data, extensions, full_path=False\n):\n    \"\"\"Return last workfile filename.\n\n    Returns file with version 1 if there is not workfile yet.\n\n    Args:\n        workdir(str): Path to dir where workfiles are stored.\n        file_template(str): Template of file name.\n        fill_data(Dict[str, Any]): Data for filling template.\n        extensions(Iterable[str]): All allowed file extensions of workfile.\n        full_path(bool): Full path to file is returned if set to True.\n\n    Returns:\n        str: Last or first workfile as filename of full path to filename.\n    \"\"\"\n\n    filename, version = get_last_workfile_with_version(\n        workdir, file_template, fill_data, extensions\n    )\n    if filename is None:\n        data = copy.deepcopy(fill_data)\n        data[\"version\"] = version_start.get_versioning_start(\n            data[\"project\"][\"name\"],\n            data[\"app\"],\n            task_name=data[\"task\"][\"name\"],\n            task_type=data[\"task\"][\"type\"],\n            family=\"workfile\"\n        )\n        data.pop(\"comment\", None)\n        if not data.get(\"ext\"):\n            data[\"ext\"] = extensions[0]\n        data[\"ext\"] = data[\"ext\"].replace('.', '')\n        filename = StringTemplate.format_strict_template(file_template, data)\n\n    if full_path:\n        return os.path.normpath(os.path.join(workdir, filename))\n\n    return filename\n\n\ndef get_custom_workfile_template(\n    project_doc,\n    asset_doc,\n    task_name,\n    host_name,\n    anatomy=None,\n    project_settings=None\n):\n    \"\"\"Filter and fill workfile template profiles by passed context.\n\n    Custom workfile template can be used as first version of workfiles.\n    Template is a file on a disk which is set in settings. Expected settings\n    structure to have this feature enabled is:\n    project settings\n    |- <host name>\n      |- workfile_builder\n        |- create_first_version   - a bool which must be set to 'True'\n        |- custom_templates       - profiles based on task name/type which\n                                    points to a file which is copied as\n                                    first workfile\n\n    It is expected that passed argument are already queried documents of\n    project and asset as parents of processing task name.\n\n    Args:\n        project_doc (Dict[str, Any]): Project document from MongoDB.\n        asset_doc (Dict[str, Any]): Asset document from MongoDB.\n        task_name (str): Name of task for which templates are filtered.\n        host_name (str): Name of host.\n        anatomy (Anatomy): Optionally passed anatomy object for passed project\n            name.\n        project_settings(Dict[str, Any]): Preloaded project settings.\n\n    Returns:\n        str: Path to template or None if none of profiles match current\n            context. Existence of formatted path is not validated.\n        None: If no profile is matching context.\n    \"\"\"\n\n    log = Logger.get_logger(\"CustomWorkfileResolve\")\n\n    project_name = project_doc[\"name\"]\n    if project_settings is None:\n        project_settings = get_project_settings(project_name)\n\n    host_settings = project_settings.get(host_name)\n    if not host_settings:\n        log.info(\"Host \\\"{}\\\" doesn't have settings\".format(host_name))\n        return None\n\n    workfile_builder_settings = host_settings.get(\"workfile_builder\")\n    if not workfile_builder_settings:\n        log.info((\n            \"Seems like old version of settings is used.\"\n            \" Can't access custom templates in host \\\"{}\\\".\"\n        ).format(host_name))\n        return\n\n    if not workfile_builder_settings[\"create_first_version\"]:\n        log.info((\n            \"Project \\\"{}\\\" has turned off to create first workfile for\"\n            \" host \\\"{}\\\"\"\n        ).format(project_name, host_name))\n        return\n\n    # Backwards compatibility\n    template_profiles = workfile_builder_settings.get(\"custom_templates\")\n    if not template_profiles:\n        log.info(\n            \"Custom templates are not filled. Skipping template copy.\"\n        )\n        return\n\n    if anatomy is None:\n        anatomy = Anatomy(project_name)\n\n    # get project, asset, task anatomy context data\n    anatomy_context_data = get_template_data(\n        project_doc, asset_doc, task_name, host_name\n    )\n    # add root dict\n    anatomy_context_data[\"root\"] = anatomy.roots\n\n    # get task type for the task in context\n    current_task_type = anatomy_context_data[\"task\"][\"type\"]\n\n    # get path from matching profile\n    matching_item = filter_profiles(\n        template_profiles,\n        {\"task_types\": current_task_type}\n    )\n    # when path is available try to format it in case\n    # there are some anatomy template strings\n    if matching_item:\n        # extend anatomy context with os.environ to\n        # also allow formatting against env\n        full_context_data = os.environ.copy()\n        full_context_data.update(anatomy_context_data)\n\n        template = matching_item[\"path\"][platform.system().lower()]\n        return StringTemplate.format_strict_template(\n            template, full_context_data\n        ).normalized()\n\n    return None\n\n\ndef get_custom_workfile_template_by_string_context(\n    project_name,\n    asset_name,\n    task_name,\n    host_name,\n    anatomy=None,\n    project_settings=None\n):\n    \"\"\"Filter and fill workfile template profiles by passed context.\n\n    Passed context are string representations of project, asset and task.\n    Function will query documents of project and asset to be able use\n    `get_custom_workfile_template` for rest of logic.\n\n    Args:\n        project_name(str): Project name.\n        asset_name(str): Asset name.\n        task_name(str): Task name.\n        host_name (str): Name of host.\n        anatomy(Anatomy): Optionally prepared anatomy object for passed\n            project.\n        project_settings(Dict[str, Any]): Preloaded project settings.\n\n    Returns:\n        str: Path to template or None if none of profiles match current\n            context. (Existence of formatted path is not validated.)\n        None: If no profile is matching context.\n    \"\"\"\n\n    project_doc = get_project(project_name)\n    asset_doc = get_asset_by_name(project_name, asset_name)\n\n    return get_custom_workfile_template(\n        project_doc, asset_doc, task_name, host_name, anatomy, project_settings\n    )\n\n\ndef create_workdir_extra_folders(\n    workdir,\n    host_name,\n    task_type,\n    task_name,\n    project_name,\n    project_settings=None\n):\n    \"\"\"Create extra folders in work directory based on context.\n\n    Args:\n        workdir (str): Path to workdir where workfiles is stored.\n        host_name (str): Name of host implementation.\n        task_type (str): Type of task for which extra folders should be\n            created.\n        task_name (str): Name of task for which extra folders should be\n            created.\n        project_name (str): Name of project on which task is.\n        project_settings (dict): Prepared project settings. Are loaded if not\n            passed.\n    \"\"\"\n\n    # Load project settings if not set\n    if not project_settings:\n        project_settings = get_project_settings(project_name)\n\n    # Load extra folders profiles\n    extra_folders_profiles = (\n        project_settings[\"global\"][\"tools\"][\"Workfiles\"][\"extra_folders\"]\n    )\n    # Skip if are empty\n    if not extra_folders_profiles:\n        return\n\n    # Prepare profiles filters\n    filter_data = {\n        \"task_types\": task_type,\n        \"task_names\": task_name,\n        \"hosts\": host_name\n    }\n    profile = filter_profiles(extra_folders_profiles, filter_data)\n    if profile is None:\n        return\n\n    for subfolder in profile[\"folders\"]:\n        # Make sure backslashes are converted to forwards slashes\n        #   and does not start with slash\n        subfolder = subfolder.replace(\"\\\\\", \"/\").lstrip(\"/\")\n        # Skip empty strings\n        if not subfolder:\n            continue\n\n        fullpath = os.path.join(workdir, subfolder)\n        if not os.path.exists(fullpath):\n            os.makedirs(fullpath)\n"
  },
  {
    "path": "openpype/pipeline/workfile/workfile_template_builder.py",
    "content": "\"\"\"Workfile build mechanism using workfile templates.\n\nBuild templates are manually prepared using plugin definitions which create\nplaceholders inside the template which are populated on import.\n\nThis approach is very explicit to achive very specific build logic that can be\ntargeted by task types and names.\n\nPlaceholders are created using placeholder plugins which should care about\nlogic and data of placeholder items. 'PlaceholderItem' is used to keep track\nabout it's progress.\n\"\"\"\n\nimport os\nimport re\nimport collections\nimport copy\nfrom abc import ABCMeta, abstractmethod\n\nimport six\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_asset_by_name,\n    get_linked_assets,\n    get_representations,\n)\nfrom openpype.settings import (\n    get_project_settings,\n    get_system_settings,\n)\nfrom openpype.host import IWorkfileHost, HostBase\nfrom openpype.lib import (\n    Logger,\n    StringTemplate,\n    filter_profiles,\n    attribute_definitions,\n)\nfrom openpype.lib.attribute_definitions import get_attributes_keys\nfrom openpype.pipeline import Anatomy\nfrom openpype.pipeline.load import (\n    get_loaders_by_name,\n    get_contexts_for_repre_docs,\n    load_with_repre_context,\n)\n\nfrom openpype.pipeline.create import (\n    discover_legacy_creator_plugins,\n    CreateContext,\n)\n\n\nclass TemplateNotFound(Exception):\n    \"\"\"Exception raised when template does not exist.\"\"\"\n    pass\n\n\nclass TemplateProfileNotFound(Exception):\n    \"\"\"Exception raised when current profile\n    doesn't match any template profile\"\"\"\n    pass\n\n\nclass TemplateAlreadyImported(Exception):\n    \"\"\"Error raised when Template was already imported by host for\n    this session\"\"\"\n    pass\n\n\nclass TemplateLoadFailed(Exception):\n    \"\"\"Error raised whend Template loader was unable to load the template\"\"\"\n    pass\n\n\n@six.add_metaclass(ABCMeta)\nclass AbstractTemplateBuilder(object):\n    \"\"\"Abstraction of Template Builder.\n\n    Builder cares about context, shared data, cache, discovery of plugins\n    and trigger logic. Provides public api for host workfile build systen.\n\n    Rest of logic is based on plugins that care about collection and creation\n    of placeholder items.\n\n    Population of placeholders happens in loops. Each loop will collect all\n    available placeholders, skip already populated, and populate the rest.\n\n    Builder item has 2 types of shared data. Refresh lifetime which are cleared\n    on refresh and populate lifetime which are cleared after loop of\n    placeholder population.\n\n    Args:\n        host (Union[HostBase, ModuleType]): Implementation of host.\n    \"\"\"\n\n    _log = None\n    use_legacy_creators = False\n\n    def __init__(self, host):\n        # Get host name\n        if isinstance(host, HostBase):\n            host_name = host.name\n        else:\n            host_name = os.environ.get(\"AVALON_APP\")\n\n        self._host = host\n        self._host_name = host_name\n\n        # Shared data across placeholder plugins\n        self._shared_data = {}\n        self._shared_populate_data = {}\n\n        # Where created objects of placeholder plugins will be stored\n        self._placeholder_plugins = None\n        self._loaders_by_name = None\n        self._creators_by_name = None\n        self._create_context = None\n\n        self._system_settings = None\n        self._project_settings = None\n\n        self._current_asset_doc = None\n        self._linked_asset_docs = None\n        self._task_type = None\n\n    @property\n    def project_name(self):\n        if isinstance(self._host, HostBase):\n            return self._host.get_current_project_name()\n        return os.getenv(\"AVALON_PROJECT\")\n\n    @property\n    def current_asset_name(self):\n        if isinstance(self._host, HostBase):\n            return self._host.get_current_asset_name()\n        return os.getenv(\"AVALON_ASSET\")\n\n    @property\n    def current_task_name(self):\n        if isinstance(self._host, HostBase):\n            return self._host.get_current_task_name()\n        return os.getenv(\"AVALON_TASK\")\n\n    def get_current_context(self):\n        if isinstance(self._host, HostBase):\n            return self._host.get_current_context()\n        return {\n            \"project_name\": self.project_name,\n            \"asset_name\": self.current_asset_name,\n            \"task_name\": self.current_task_name\n        }\n\n    @property\n    def system_settings(self):\n        if self._system_settings is None:\n            self._system_settings = get_system_settings()\n        return self._system_settings\n\n    @property\n    def project_settings(self):\n        if self._project_settings is None:\n            self._project_settings = get_project_settings(self.project_name)\n        return self._project_settings\n\n    @property\n    def current_asset_doc(self):\n        if self._current_asset_doc is None:\n            self._current_asset_doc = get_asset_by_name(\n                self.project_name, self.current_asset_name\n            )\n        return self._current_asset_doc\n\n    @property\n    def linked_asset_docs(self):\n        if self._linked_asset_docs is None:\n            self._linked_asset_docs = get_linked_assets(\n                self.project_name, self.current_asset_doc\n            )\n        return self._linked_asset_docs\n\n    @property\n    def current_task_type(self):\n        asset_doc = self.current_asset_doc\n        if not asset_doc:\n            return None\n        return (\n            asset_doc\n            .get(\"data\", {})\n            .get(\"tasks\", {})\n            .get(self.current_task_name, {})\n            .get(\"type\")\n        )\n\n    @property\n    def create_context(self):\n        if self._create_context is None:\n            self._create_context = CreateContext(\n                self.host,\n                discover_publish_plugins=False,\n                headless=True\n            )\n        return self._create_context\n\n    def get_placeholder_plugin_classes(self):\n        \"\"\"Get placeholder plugin classes that can be used to build template.\n\n        Default implementation looks for method\n            'get_workfile_build_placeholder_plugins' on host.\n\n        Returns:\n            List[PlaceholderPlugin]: Plugin classes available for host.\n        \"\"\"\n\n        if hasattr(self._host, \"get_workfile_build_placeholder_plugins\"):\n            return self._host.get_workfile_build_placeholder_plugins()\n        return []\n\n    @property\n    def host(self):\n        \"\"\"Access to host implementation.\n\n        Returns:\n            Union[HostBase, ModuleType]: Implementation of host.\n        \"\"\"\n\n        return self._host\n\n    @property\n    def host_name(self):\n        \"\"\"Name of 'host' implementation.\n\n        Returns:\n            str: Host's name.\n        \"\"\"\n\n        return self._host_name\n\n    @property\n    def log(self):\n        \"\"\"Dynamically created logger for the plugin.\"\"\"\n\n        if self._log is None:\n            self._log = Logger.get_logger(repr(self))\n        return self._log\n\n    def refresh(self):\n        \"\"\"Reset cached data.\"\"\"\n\n        self._placeholder_plugins = None\n        self._loaders_by_name = None\n        self._creators_by_name = None\n\n        self._current_asset_doc = None\n        self._linked_asset_docs = None\n        self._task_type = None\n\n        self._system_settings = None\n        self._project_settings = None\n\n        self.clear_shared_data()\n        self.clear_shared_populate_data()\n\n    def get_loaders_by_name(self):\n        if self._loaders_by_name is None:\n            self._loaders_by_name = get_loaders_by_name()\n        return self._loaders_by_name\n\n    def _collect_legacy_creators(self):\n        creators_by_name = {}\n        for creator in discover_legacy_creator_plugins():\n            if not creator.enabled:\n                continue\n            creator_name = creator.__name__\n            if creator_name in creators_by_name:\n                raise KeyError(\n                    \"Duplicated creator name {} !\".format(creator_name)\n                )\n            creators_by_name[creator_name] = creator\n        self._creators_by_name = creators_by_name\n\n    def _collect_creators(self):\n        self._creators_by_name = dict(self.create_context.creators)\n\n    def get_creators_by_name(self):\n        if self._creators_by_name is None:\n            if self.use_legacy_creators:\n                self._collect_legacy_creators()\n            else:\n                self._collect_creators()\n\n        return self._creators_by_name\n\n    def get_shared_data(self, key):\n        \"\"\"Receive shared data across plugins and placeholders.\n\n        This can be used to scroll scene only once to look for placeholder\n        items if the storing is unified but each placeholder plugin would have\n        to call it again.\n\n        Args:\n            key (str): Key under which are shared data stored.\n\n        Returns:\n            Union[None, Any]: None if key was not set.\n        \"\"\"\n\n        return self._shared_data.get(key)\n\n    def set_shared_data(self, key, value):\n        \"\"\"Store share data across plugins and placeholders.\n\n        Store data that can be afterwards accessed from any future call. It\n        is good practice to check if the same value is not already stored under\n        different key or if the key is not already used for something else.\n\n        Key should be self explanatory to content.\n        - wrong: 'asset'\n        - good: 'asset_name'\n\n        Args:\n            key (str): Key under which is key stored.\n            value (Any): Value that should be stored under the key.\n        \"\"\"\n\n        self._shared_data[key] = value\n\n    def clear_shared_data(self):\n        \"\"\"Clear shared data.\n\n        Method only clear shared data to default state.\n        \"\"\"\n\n        self._shared_data = {}\n\n    def clear_shared_populate_data(self):\n        \"\"\"Receive shared data across plugins and placeholders.\n\n        These data are cleared after each loop of populating of template.\n\n        This can be used to scroll scene only once to look for placeholder\n        items if the storing is unified but each placeholder plugin would have\n        to call it again.\n\n        Args:\n            key (str): Key under which are shared data stored.\n\n        Returns:\n            Union[None, Any]: None if key was not set.\n        \"\"\"\n\n        self._shared_populate_data = {}\n\n    def get_shared_populate_data(self, key):\n        \"\"\"Store share populate data across plugins and placeholders.\n\n        These data are cleared after each loop of populating of template.\n\n        Store data that can be afterwards accessed from any future call. It\n        is good practice to check if the same value is not already stored under\n        different key or if the key is not already used for something else.\n\n        Key should be self explanatory to content.\n        - wrong: 'asset'\n        - good: 'asset_name'\n\n        Args:\n            key (str): Key under which is key stored.\n            value (Any): Value that should be stored under the key.\n        \"\"\"\n\n        return self._shared_populate_data.get(key)\n\n    def set_shared_populate_data(self, key, value):\n        \"\"\"Store share populate data across plugins and placeholders.\n\n        These data are cleared after each loop of populating of template.\n\n        Store data that can be afterwards accessed from any future call. It\n        is good practice to check if the same value is not already stored under\n        different key or if the key is not already used for something else.\n\n        Key should be self explanatory to content.\n        - wrong: 'asset'\n        - good: 'asset_name'\n\n        Args:\n            key (str): Key under which is key stored.\n            value (Any): Value that should be stored under the key.\n        \"\"\"\n\n        self._shared_populate_data[key] = value\n\n    @property\n    def placeholder_plugins(self):\n        \"\"\"Access to initialized placeholder plugins.\n\n        Returns:\n            List[PlaceholderPlugin]: Initialized plugins available for host.\n        \"\"\"\n\n        if self._placeholder_plugins is None:\n            placeholder_plugins = {}\n            for cls in self.get_placeholder_plugin_classes():\n                try:\n                    plugin = cls(self)\n                    placeholder_plugins[plugin.identifier] = plugin\n\n                except Exception:\n                    self.log.warning(\n                        \"Failed to initialize placeholder plugin {}\".format(\n                            cls.__name__\n                        ),\n                        exc_info=True\n                    )\n\n            self._placeholder_plugins = placeholder_plugins\n        return self._placeholder_plugins\n\n    def create_placeholder(self, plugin_identifier, placeholder_data):\n        \"\"\"Create new placeholder using plugin identifier and data.\n\n        Args:\n            plugin_identifier (str): Identifier of plugin. That's how builder\n                know which plugin should be used.\n            placeholder_data (Dict[str, Any]): Placeholder item data. They\n                should match options required by the plugin.\n\n        Returns:\n            PlaceholderItem: Created placeholder item.\n        \"\"\"\n\n        plugin = self.placeholder_plugins[plugin_identifier]\n        return plugin.create_placeholder(placeholder_data)\n\n    def get_placeholders(self):\n        \"\"\"Collect placeholder items from scene.\n\n        Each placeholder plugin can collect it's placeholders and return them.\n        This method does not use cached values but always go through the scene.\n\n        Returns:\n            List[PlaceholderItem]: Sorted placeholder items.\n        \"\"\"\n\n        placeholders = []\n        for placeholder_plugin in self.placeholder_plugins.values():\n            result = placeholder_plugin.collect_placeholders()\n            if result:\n                placeholders.extend(result)\n\n        return list(sorted(\n            placeholders,\n            key=lambda i: i.order\n        ))\n\n    def build_template(\n        self,\n        template_path=None,\n        level_limit=None,\n        keep_placeholders=None,\n        create_first_version=None,\n        workfile_creation_enabled=False\n    ):\n        \"\"\"Main callback for building workfile from template path.\n\n        Todo:\n            Handle report of populated placeholders from\n                'populate_scene_placeholders' to be shown to a user.\n\n        Args:\n            template_path (str): Path to a template file with placeholders.\n                Template from settings 'get_template_preset' used when not\n                passed.\n            level_limit (int): Limit of populate loops. Related to\n                'populate_scene_placeholders' method.\n            keep_placeholders (bool): Add flag to placeholder data for\n                hosts to decide if they want to remove\n                placeholder after it is used.\n            create_first_version (bool): create first version of a workfile\n            workfile_creation_enabled (bool): If True, it might create\n                                              first version but ignore\n                                              process if version is created\n\n        \"\"\"\n        template_preset = self.get_template_preset()\n\n        if template_path is None:\n            template_path = template_preset[\"path\"]\n\n        if keep_placeholders is None:\n            keep_placeholders = template_preset[\"keep_placeholder\"]\n        if create_first_version is None:\n            create_first_version = template_preset[\"create_first_version\"]\n\n        # check if first version is created\n        created_version_workfile = False\n        if create_first_version:\n            created_version_workfile = self.create_first_workfile_version()\n\n        # if first version is created, import template\n        # and populate placeholders\n        if (\n            create_first_version\n            and workfile_creation_enabled\n            and created_version_workfile\n        ):\n            self.import_template(template_path)\n            self.populate_scene_placeholders(\n                level_limit, keep_placeholders)\n\n            # save workfile after template is populated\n            self.save_workfile(created_version_workfile)\n\n        # ignore process if first workfile is enabled\n        # but a version is already created\n        if workfile_creation_enabled:\n            return\n\n        self.import_template(template_path)\n        self.populate_scene_placeholders(\n            level_limit, keep_placeholders)\n\n    def rebuild_template(self):\n        \"\"\"Go through existing placeholders in scene and update them.\n\n        This could not make sense for all plugin types so this is optional\n        logic for plugins.\n\n        Note:\n            Logic is not importing the template again but using placeholders\n                that were already available. We should maybe change the method\n                name.\n\n        Question:\n            Should this also handle subloops as it is possible that another\n                template is loaded during processing?\n        \"\"\"\n\n        if not self.placeholder_plugins:\n            self.log.info(\"There are no placeholder plugins available.\")\n            return\n\n        placeholders = self.get_placeholders()\n        if not placeholders:\n            self.log.info(\"No placeholders were found.\")\n            return\n\n        for placeholder in placeholders:\n            plugin = placeholder.plugin\n            plugin.repopulate_placeholder(placeholder)\n\n        self.clear_shared_populate_data()\n\n    @abstractmethod\n    def import_template(self, template_path):\n        \"\"\"\n        Import template in current host.\n\n        Should load the content of template into scene so\n        'populate_scene_placeholders' can be started.\n\n        Args:\n            template_path (str): Fullpath for current task and\n                host's template file.\n        \"\"\"\n\n        pass\n\n    def create_first_workfile_version(self):\n        \"\"\"\n        Create first version of workfile.\n\n        Should load the content of template into scene so\n        'populate_scene_placeholders' can be started.\n\n        Args:\n            template_path (str): Fullpath for current task and\n                host's template file.\n        \"\"\"\n        last_workfile_path = os.environ.get(\"AVALON_LAST_WORKFILE\")\n        self.log.info(\"__ last_workfile_path: {}\".format(last_workfile_path))\n        if os.path.exists(last_workfile_path):\n            # ignore in case workfile existence\n            self.log.info(\"Workfile already exists, skipping creation.\")\n            return False\n\n        # Create first version\n        self.log.info(\"Creating first version of workfile.\")\n        self.save_workfile(last_workfile_path)\n\n        # Confirm creation of first version\n        return last_workfile_path\n\n    def save_workfile(self, workfile_path):\n        \"\"\"Save workfile in current host.\"\"\"\n        # Save current scene, continue to open file\n        if isinstance(self.host, IWorkfileHost):\n            self.host.save_workfile(workfile_path)\n        else:\n            self.host.save_file(workfile_path)\n\n    def _prepare_placeholders(self, placeholders):\n        \"\"\"Run preparation part for placeholders on plugins.\n\n        Args:\n            placeholders (List[PlaceholderItem]): Placeholder items that will\n                be processed.\n        \"\"\"\n\n        # Prepare placeholder items by plugin\n        plugins_by_identifier = {}\n        placeholders_by_plugin_id = collections.defaultdict(list)\n        for placeholder in placeholders:\n            plugin = placeholder.plugin\n            identifier = plugin.identifier\n            plugins_by_identifier[identifier] = plugin\n            placeholders_by_plugin_id[identifier].append(placeholder)\n\n        # Plugin should prepare data for passed placeholders\n        for identifier, placeholders in placeholders_by_plugin_id.items():\n            plugin = plugins_by_identifier[identifier]\n            plugin.prepare_placeholders(placeholders)\n\n    def populate_scene_placeholders(\n        self, level_limit=None, keep_placeholders=None\n    ):\n        \"\"\"Find placeholders in scene using plugins and process them.\n\n        This should happen after 'import_template'.\n\n        Collect available placeholders from scene. All of them are processed\n        after that shared data are cleared. Placeholder items are collected\n        again and if there are any new the loop happens again. This is possible\n        to change with defying 'level_limit'.\n\n        Placeholders are marked as processed so they're not re-processed. To\n        identify which placeholders were already processed is used\n        placeholder's 'scene_identifier'.\n\n        Args:\n            level_limit (int): Level of loops that can happen. Default is 1000.\n            keep_placeholders (bool): Add flag to placeholder data for\n                hosts to decide if they want to remove\n                placeholder after it is used.\n        \"\"\"\n\n        if not self.placeholder_plugins:\n            self.log.warning(\"There are no placeholder plugins available.\")\n            return\n\n        placeholders = self.get_placeholders()\n        if not placeholders:\n            self.log.warning(\"No placeholders were found.\")\n            return\n\n        # Avoid infinite loop\n        # - 1000 iterations of placeholders processing must be enough\n        if not level_limit:\n            level_limit = 1000\n\n        placeholder_by_scene_id = {\n            placeholder.scene_identifier: placeholder\n            for placeholder in placeholders\n        }\n        all_processed = len(placeholders) == 0\n        # Counter is checked at the ned of a loop so the loop happens at least\n        #   once.\n        iter_counter = 0\n        while not all_processed:\n            filtered_placeholders = []\n            for placeholder in placeholders:\n                if placeholder.finished:\n                    continue\n\n                if placeholder.in_progress:\n                    self.log.warning((\n                        \"Placeholder that should be processed\"\n                        \" is already in progress.\"\n                    ))\n                    continue\n\n                # add flag for keeping placeholders in scene\n                # after they are processed\n                placeholder.data[\"keep_placeholder\"] = keep_placeholders\n\n                filtered_placeholders.append(placeholder)\n\n            self._prepare_placeholders(filtered_placeholders)\n\n            for placeholder in filtered_placeholders:\n                placeholder.set_in_progress()\n                placeholder_plugin = placeholder.plugin\n                try:\n                    placeholder_plugin.populate_placeholder(placeholder)\n\n                except Exception as exc:\n                    self.log.warning(\n                        (\n                            \"Failed to process placeholder {} with plugin {}\"\n                        ).format(\n                            placeholder.scene_identifier,\n                            placeholder_plugin.__class__.__name__\n                        ),\n                        exc_info=True\n                    )\n                    placeholder.set_failed(exc)\n\n                placeholder.set_finished()\n\n            # Clear shared data before getting new placeholders\n            self.clear_shared_populate_data()\n\n            iter_counter += 1\n            if iter_counter >= level_limit:\n                break\n\n            all_processed = True\n            collected_placeholders = self.get_placeholders()\n            for placeholder in collected_placeholders:\n                identifier = placeholder.scene_identifier\n                if identifier in placeholder_by_scene_id:\n                    continue\n\n                all_processed = False\n                placeholder_by_scene_id[identifier] = placeholder\n                placeholders.append(placeholder)\n\n        self.refresh()\n\n    def _get_build_profiles(self):\n        \"\"\"Get build profiles for workfile build template path.\n\n        Returns:\n            List[Dict[str, Any]]: Profiles for template path resolving.\n        \"\"\"\n\n        return (\n            self.project_settings\n            [self.host_name]\n            [\"templated_workfile_build\"]\n            [\"profiles\"]\n        )\n\n    def get_template_preset(self):\n        \"\"\"Unified way how template preset is received usign settings.\n\n        Method is dependent on '_get_build_profiles' which should return filter\n        profiles to resolve path to a template. Default implementation looks\n        into host settings:\n        - 'project_settings/{host name}/templated_workfile_build/profiles'\n\n        Returns:\n            str: Path to a template file with placeholders.\n\n        Raises:\n            TemplateProfileNotFound: When profiles are not filled.\n            TemplateLoadFailed: Profile was found but path is not set.\n            TemplateNotFound: Path was set but file does not exists.\n        \"\"\"\n\n        host_name = self.host_name\n        project_name = self.project_name\n        task_name = self.current_task_name\n        task_type = self.current_task_type\n\n        build_profiles = self._get_build_profiles()\n        profile = filter_profiles(\n            build_profiles,\n            {\n                \"task_types\": task_type,\n                \"task_names\": task_name\n            }\n        )\n\n        if not profile:\n            raise TemplateProfileNotFound((\n                \"No matching profile found for task '{}' of type '{}' \"\n                \"with host '{}'\"\n            ).format(task_name, task_type, host_name))\n\n        path = profile[\"path\"]\n\n        # switch to remove placeholders after they are used\n        keep_placeholder = profile.get(\"keep_placeholder\")\n        create_first_version = profile.get(\"create_first_version\")\n\n        # backward compatibility, since default is True\n        if keep_placeholder is None:\n            keep_placeholder = True\n\n        if not path:\n            raise TemplateLoadFailed((\n                \"Template path is not set.\\n\"\n                \"Path need to be set in {}\\\\Template Workfile Build \"\n                \"Settings\\\\Profiles\"\n            ).format(host_name.title()))\n\n        # Try fill path with environments and anatomy roots\n        anatomy = Anatomy(project_name)\n        fill_data = {\n            key: value\n            for key, value in os.environ.items()\n        }\n\n        fill_data[\"root\"] = anatomy.roots\n        fill_data[\"project\"] = {\n            \"name\": project_name,\n            \"code\": anatomy.project_code,\n        }\n\n        result = StringTemplate.format_template(path, fill_data)\n        if result.solved:\n            path = result.normalized()\n\n        if path and os.path.exists(path):\n            self.log.info(\"Found template at: '{}'\".format(path))\n            return {\n                \"path\": path,\n                \"keep_placeholder\": keep_placeholder,\n                \"create_first_version\": create_first_version\n            }\n\n        solved_path = None\n        while True:\n            try:\n                solved_path = anatomy.path_remapper(path)\n            except KeyError as missing_key:\n                raise KeyError(\n                    \"Could not solve key '{}' in template path '{}'\".format(\n                        missing_key, path))\n\n            if solved_path is None:\n                solved_path = path\n            if solved_path == path:\n                break\n            path = solved_path\n\n        solved_path = os.path.normpath(solved_path)\n        if not os.path.exists(solved_path):\n            raise TemplateNotFound(\n                \"Template found in openPype settings for task '{}' with host \"\n                \"'{}' does not exists. (Not found : {})\".format(\n                    task_name, host_name, solved_path))\n\n        self.log.info(\"Found template at: '{}'\".format(solved_path))\n\n        return {\n            \"path\": solved_path,\n            \"keep_placeholder\": keep_placeholder,\n            \"create_first_version\": create_first_version\n        }\n\n\n@six.add_metaclass(ABCMeta)\nclass PlaceholderPlugin(object):\n    \"\"\"Plugin which care about handling of placeholder items logic.\n\n    Plugin create and update placeholders in scene and populate them on\n    template import. Populating means that based on placeholder data happens\n    a logic in the scene. Most common logic is to load representation using\n    loaders or to create instances in scene.\n    \"\"\"\n\n    label = None\n    _log = None\n\n    def __init__(self, builder):\n        self._builder = builder\n\n    @property\n    def builder(self):\n        \"\"\"Access to builder which initialized the plugin.\n\n        Returns:\n            AbstractTemplateBuilder: Loader of template build.\n        \"\"\"\n\n        return self._builder\n\n    @property\n    def project_name(self):\n        return self._builder.project_name\n\n    @property\n    def log(self):\n        \"\"\"Dynamically created logger for the plugin.\"\"\"\n\n        if self._log is None:\n            self._log = Logger.get_logger(repr(self))\n        return self._log\n\n    @property\n    def identifier(self):\n        \"\"\"Identifier which will be stored to placeholder.\n\n        Default implementation uses class name.\n\n        Returns:\n            str: Unique identifier of placeholder plugin.\n        \"\"\"\n\n        return self.__class__.__name__\n\n    @abstractmethod\n    def create_placeholder(self, placeholder_data):\n        \"\"\"Create new placeholder in scene and get it's item.\n\n        It matters on the plugin implementation if placeholder will use\n        selection in scene or create new node.\n\n        Args:\n            placeholder_data (Dict[str, Any]): Data that were created\n                based on attribute definitions from 'get_placeholder_options'.\n\n        Returns:\n            PlaceholderItem: Created placeholder item.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def update_placeholder(self, placeholder_item, placeholder_data):\n        \"\"\"Update placeholder item with new data.\n\n        New data should be propagated to object of placeholder item itself\n        and also into the scene.\n\n        Reason:\n            Some placeholder plugins may require some special way how the\n            updates should be propagated to object.\n\n        Args:\n            placeholder_item (PlaceholderItem): Object of placeholder that\n                should be updated.\n            placeholder_data (Dict[str, Any]): Data related to placeholder.\n                Should match plugin options.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def collect_placeholders(self):\n        \"\"\"Collect placeholders from scene.\n\n        Returns:\n            List[PlaceholderItem]: Placeholder objects.\n        \"\"\"\n\n        pass\n\n    def get_placeholder_options(self, options=None):\n        \"\"\"Placeholder options for data showed.\n\n        Returns:\n            List[AbstractAttrDef]: Attribute definitions of\n                placeholder options.\n        \"\"\"\n\n        return []\n\n    def get_placeholder_keys(self):\n        \"\"\"Get placeholder keys that are stored in scene.\n\n        Returns:\n            Set[str]: Key of placeholder keys that are stored in scene.\n        \"\"\"\n\n        option_keys = get_attributes_keys(self.get_placeholder_options())\n        option_keys.add(\"plugin_identifier\")\n        return option_keys\n\n    def prepare_placeholders(self, placeholders):\n        \"\"\"Preparation part of placeholders.\n\n        Args:\n            placeholders (List[PlaceholderItem]): List of placeholders that\n                will be processed.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def populate_placeholder(self, placeholder):\n        \"\"\"Process single placeholder item.\n\n        Processing of placeholders is defined by their order thus can't be\n        processed in batch.\n\n        Args:\n            placeholder (PlaceholderItem): Placeholder that should be\n                processed.\n        \"\"\"\n\n        pass\n\n    def repopulate_placeholder(self, placeholder):\n        \"\"\"Update scene with current context for passed placeholder.\n\n        Can be used to re-run placeholder logic (if it make sense).\n        \"\"\"\n\n        pass\n\n    def get_plugin_shared_data(self, key):\n        \"\"\"Receive shared data across plugin and placeholders.\n\n        Using shared data from builder but stored under plugin identifier.\n\n        Args:\n            key (str): Key under which are shared data stored.\n\n        Returns:\n            Union[None, Any]: None if key was not set.\n        \"\"\"\n\n        plugin_data = self.builder.get_shared_data(self.identifier)\n        if plugin_data is None:\n            return None\n        return plugin_data.get(key)\n\n    def set_plugin_shared_data(self, key, value):\n        \"\"\"Store share data across plugin and placeholders.\n\n        Using shared data from builder but stored under plugin identifier.\n\n        Key should be self explanatory to content.\n        - wrong: 'asset'\n        - good: 'asset_name'\n\n        Args:\n            key (str): Key under which is key stored.\n            value (Any): Value that should be stored under the key.\n        \"\"\"\n\n        plugin_data = self.builder.get_shared_data(self.identifier)\n        if plugin_data is None:\n            plugin_data = {}\n        plugin_data[key] = value\n        self.builder.set_shared_data(self.identifier, plugin_data)\n\n    def get_plugin_shared_populate_data(self, key):\n        \"\"\"Receive shared data across plugin and placeholders.\n\n        Using shared populate data from builder but stored under plugin\n        identifier.\n\n        Shared populate data are cleaned up during populate while loop.\n\n        Args:\n            key (str): Key under which are shared data stored.\n\n        Returns:\n            Union[None, Any]: None if key was not set.\n        \"\"\"\n\n        plugin_data = self.builder.get_shared_populate_data(self.identifier)\n        if plugin_data is None:\n            return None\n        return plugin_data.get(key)\n\n    def set_plugin_shared_populate_data(self, key, value):\n        \"\"\"Store share data across plugin and placeholders.\n\n        Using shared data from builder but stored under plugin identifier.\n\n        Key should be self explanatory to content.\n        - wrong: 'asset'\n        - good: 'asset_name'\n\n        Shared populate data are cleaned up during populate while loop.\n\n        Args:\n            key (str): Key under which is key stored.\n            value (Any): Value that should be stored under the key.\n        \"\"\"\n\n        plugin_data = self.builder.get_shared_populate_data(self.identifier)\n        if plugin_data is None:\n            plugin_data = {}\n        plugin_data[key] = value\n        self.builder.set_shared_populate_data(self.identifier, plugin_data)\n\n\nclass PlaceholderItem(object):\n    \"\"\"Item representing single item in scene that is a placeholder to process.\n\n    Items are always created and updated by their plugins. Each plugin can use\n    modified class of 'PlacehoderItem' but only to add more options instead of\n    new other.\n\n    Scene identifier is used to avoid processing of the palceholder item\n    multiple times so must be unique across whole workfile builder.\n\n    Args:\n        scene_identifier (str): Unique scene identifier. If placeholder is\n            created from the same \"node\" it must have same identifier.\n        data (Dict[str, Any]): Data related to placeholder. They're defined\n            by plugin.\n        plugin (PlaceholderPlugin): Plugin which created the placeholder item.\n    \"\"\"\n\n    default_order = 100\n\n    def __init__(self, scene_identifier, data, plugin):\n        self._log = None\n        self._scene_identifier = scene_identifier\n        self._data = data\n        self._plugin = plugin\n\n        # Keep track about state of Placeholder process\n        self._state = 0\n\n        # Error messages to be shown in UI\n        # - all other messages should be logged\n        self._errors = []  # -> List[str]\n\n    @property\n    def plugin(self):\n        \"\"\"Access to plugin which created placeholder.\n\n        Returns:\n            PlaceholderPlugin: Plugin object.\n        \"\"\"\n\n        return self._plugin\n\n    @property\n    def builder(self):\n        \"\"\"Access to builder.\n\n        Returns:\n            AbstractTemplateBuilder: Builder which is the top part of\n                placeholder.\n        \"\"\"\n\n        return self.plugin.builder\n\n    @property\n    def data(self):\n        \"\"\"Placeholder data which can modify how placeholder is processed.\n\n        Possible general keys\n        - order: Can define the order in which is palceholder processed.\n                    Lower == earlier.\n\n        Other keys are defined by placeholder and should validate them on item\n        creation.\n\n        Returns:\n            Dict[str, Any]: Placeholder item data.\n        \"\"\"\n\n        return self._data\n\n    def to_dict(self):\n        \"\"\"Create copy of item's data.\n\n        Returns:\n            Dict[str, Any]: Placeholder data.\n        \"\"\"\n\n        return copy.deepcopy(self.data)\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(repr(self))\n        return self._log\n\n    def __repr__(self):\n        return \"< {} {} >\".format(\n            self.__class__.__name__,\n            self._scene_identifier\n        )\n\n    @property\n    def order(self):\n        \"\"\"Order of item processing.\"\"\"\n\n        order = self._data.get(\"order\")\n        if order is None:\n            return self.default_order\n        return order\n\n    @property\n    def scene_identifier(self):\n        return self._scene_identifier\n\n    @property\n    def finished(self):\n        \"\"\"Item was already processed.\"\"\"\n\n        return self._state == 2\n\n    @property\n    def in_progress(self):\n        \"\"\"Processing is in progress.\"\"\"\n\n        return self._state == 1\n\n    def set_in_progress(self):\n        \"\"\"Change to in progress state.\"\"\"\n\n        self._state = 1\n\n    def set_finished(self):\n        \"\"\"Change to finished state.\"\"\"\n\n        self._state = 2\n\n    def set_failed(self, exception):\n        self.add_error(str(exception))\n\n    def add_error(self, error):\n        \"\"\"Set placeholder item as failed and mark it as finished.\"\"\"\n\n        self._errors.append(error)\n\n    def get_errors(self):\n        \"\"\"Exception with which the placeholder process failed.\n\n        Gives ability to access the exception.\n        \"\"\"\n\n        return self._errors\n\n\nclass PlaceholderLoadMixin(object):\n    \"\"\"Mixin prepared for loading placeholder plugins.\n\n    Implementation prepares options for placeholders with\n    'get_load_plugin_options'.\n\n    For placeholder population is implemented 'populate_load_placeholder'.\n\n    PlaceholderItem can have implemented methods:\n    - 'load_failed' - called when loading of one representation failed\n    - 'load_succeed' - called when loading of one representation succeeded\n    \"\"\"\n\n    def get_load_plugin_options(self, options=None):\n        \"\"\"Unified attribute definitions for load placeholder.\n\n        Common function for placeholder plugins used for loading of\n        repsentations. Use it in 'get_placeholder_options'.\n\n        Args:\n            plugin (PlaceholderPlugin): Plugin used for loading of\n                representations.\n            options (Dict[str, Any]): Already available options which are used\n                as defaults for attributes.\n\n        Returns:\n            List[AbstractAttrDef]: Attribute definitions common for load\n                plugins.\n        \"\"\"\n\n        loaders_by_name = self.builder.get_loaders_by_name()\n        loader_items = [\n            {\"value\": loader_name, \"label\": loader.label or loader_name}\n            for loader_name, loader in loaders_by_name.items()\n        ]\n\n        loader_items = list(sorted(loader_items, key=lambda i: i[\"label\"]))\n        options = options or {}\n\n        # Get families from all loaders excluding \"*\"\n        families = set()\n        for loader in loaders_by_name.values():\n            families.update(loader.families)\n        families.discard(\"*\")\n\n        # Sort for readability\n        families = list(sorted(families))\n\n        if AYON_SERVER_ENABLED:\n            builder_type_enum_items = [\n                {\"label\": \"Current folder\", \"value\": \"context_folder\"},\n                # TODO implement linked folders\n                # {\"label\": \"Linked folders\", \"value\": \"linked_folders\"},\n                {\"label\": \"All folders\", \"value\": \"all_folders\"},\n            ]\n            build_type_label = \"Folder Builder Type\"\n            build_type_help = (\n                \"Folder Builder Type\\n\"\n                \"\\nBuilder type describe what template loader will look\"\n                \" for.\"\n                \"\\nCurrent Folder: Template loader will look for products\"\n                \" of current context folder (Folder /assets/bob will\"\n                \" find asset)\"\n                \"\\nAll folders: All folders matching the regex will be\"\n                \" used.\"\n            )\n        else:\n            builder_type_enum_items = [\n                {\"label\": \"Current asset\", \"value\": \"context_asset\"},\n                {\"label\": \"Linked assets\", \"value\": \"linked_asset\"},\n                {\"label\": \"All assets\", \"value\": \"all_assets\"},\n            ]\n            build_type_label = \"Asset Builder Type\"\n            build_type_help = (\n                \"Asset Builder Type\\n\"\n                \"\\nBuilder type describe what template loader will look\"\n                \" for.\"\n                \"\\ncontext_asset : Template loader will look for subsets\"\n                \" of current context asset (Asset bob will find asset)\"\n                \"\\nlinked_asset : Template loader will look for assets\"\n                \" linked to current context asset.\"\n                \"\\nLinked asset are looked in database under\"\n                \" field \\\"inputLinks\\\"\"\n            )\n\n        attr_defs = [\n            attribute_definitions.UISeparatorDef(),\n            attribute_definitions.UILabelDef(\"Main attributes\"),\n            attribute_definitions.UISeparatorDef(),\n\n            attribute_definitions.EnumDef(\n                \"builder_type\",\n                label=build_type_label,\n                default=options.get(\"builder_type\"),\n                items=builder_type_enum_items,\n                tooltip=build_type_help\n            ),\n            attribute_definitions.EnumDef(\n                \"family\",\n                label=\"Family\",\n                default=options.get(\"family\"),\n                items=families\n            ),\n            attribute_definitions.TextDef(\n                \"representation\",\n                label=\"Representation name\",\n                default=options.get(\"representation\"),\n                placeholder=\"ma, abc, ...\"\n            ),\n            attribute_definitions.EnumDef(\n                \"loader\",\n                label=\"Loader\",\n                default=options.get(\"loader\"),\n                items=loader_items,\n                tooltip=(\n                    \"Loader\"\n                    \"\\nDefines what OpenPype loader will be used to\"\n                    \" load assets.\"\n                    \"\\nUseable loader depends on current host's loader list.\"\n                    \"\\nField is case sensitive.\"\n                )\n            ),\n            attribute_definitions.TextDef(\n                \"loader_args\",\n                label=\"Loader Arguments\",\n                default=options.get(\"loader_args\"),\n                placeholder='{\"camera\":\"persp\", \"lights\":True}',\n                tooltip=(\n                    \"Loader\"\n                    \"\\nDefines a dictionnary of arguments used to load assets.\"\n                    \"\\nUseable arguments depend on current placeholder Loader.\"\n                    \"\\nField should be a valid python dict.\"\n                    \" Anything else will be ignored.\"\n                )\n            ),\n            attribute_definitions.NumberDef(\n                \"order\",\n                label=\"Order\",\n                default=options.get(\"order\") or 0,\n                decimals=0,\n                minimum=0,\n                maximum=999,\n                tooltip=(\n                    \"Order\"\n                    \"\\nOrder defines asset loading priority (0 to 999)\"\n                    \"\\nPriority rule is : \\\"lowest is first to load\\\".\"\n                )\n            ),\n            attribute_definitions.UISeparatorDef(),\n            attribute_definitions.UILabelDef(\"Optional attributes\"),\n            attribute_definitions.UISeparatorDef(),\n        ]\n        if AYON_SERVER_ENABLED:\n            attr_defs.extend([\n                attribute_definitions.TextDef(\n                    \"folder_path\",\n                    label=\"Folder filter\",\n                    default=options.get(\"folder_path\"),\n                    placeholder=\"regex filtering by folder path\",\n                    tooltip=(\n                        \"Filtering assets by matching\"\n                        \" field regex to folder path\"\n                    )\n                ),\n                attribute_definitions.TextDef(\n                    \"product_name\",\n                    label=\"Product filter\",\n                    default=options.get(\"product_name\"),\n                    placeholder=\"regex filtering by product name\",\n                    tooltip=(\n                        \"Filtering assets by matching\"\n                        \" field regex to product name\"\n                    )\n                ),\n            ])\n        else:\n            attr_defs.extend([\n                attribute_definitions.TextDef(\n                    \"asset\",\n                    label=\"Asset filter\",\n                    default=options.get(\"asset\"),\n                    placeholder=\"regex filtering by asset name\",\n                    tooltip=(\n                        \"Filtering assets by matching\"\n                        \" field regex to asset's name\"\n                    )\n                ),\n                attribute_definitions.TextDef(\n                    \"subset\",\n                    label=\"Subset filter\",\n                    default=options.get(\"subset\"),\n                    placeholder=\"regex filtering by subset name\",\n                    tooltip=(\n                        \"Filtering assets by matching\"\n                        \" field regex to subset's name\"\n                    )\n                ),\n                attribute_definitions.TextDef(\n                    \"hierarchy\",\n                    label=\"Hierarchy filter\",\n                    default=options.get(\"hierarchy\"),\n                    placeholder=\"regex filtering by asset's hierarchy\",\n                    tooltip=(\n                        \"Filtering assets by matching field asset's hierarchy\"\n                    )\n                )\n            ])\n        return attr_defs\n\n    def parse_loader_args(self, loader_args):\n        \"\"\"Helper function to parse string of loader arugments.\n\n        Empty dictionary is returned if conversion fails.\n\n        Args:\n            loader_args (str): Loader args filled by user.\n\n        Returns:\n            Dict[str, Any]: Parsed arguments used as dictionary.\n        \"\"\"\n\n        if not loader_args:\n            return {}\n\n        try:\n            parsed_args = eval(loader_args)\n            if isinstance(parsed_args, dict):\n                return parsed_args\n\n        except Exception as err:\n            print(\n                \"Error while parsing loader arguments '{}'.\\n{}: {}\\n\\n\"\n                \"Continuing with default arguments. . .\".format(\n                    loader_args, err.__class__.__name__, err))\n\n        return {}\n\n    def _query_by_folder_regex(self, project_name, folder_regex):\n        \"\"\"Query folders by folder path regex.\n\n        WARNING:\n            This method will be removed once the same functionality is\n                available in ayon-python-api.\n\n        Args:\n            project_name (str): Project name.\n            folder_regex (str): Regex for folder path.\n\n        Returns:\n            list[str]: List of folder paths.\n        \"\"\"\n\n        from ayon_api.graphql_queries import folders_graphql_query\n        from openpype.client import get_ayon_server_api_connection\n\n        query = folders_graphql_query({\"id\"})\n\n        folders_field = None\n        for child in query._children:\n            if child.path != \"project\":\n                continue\n\n            for project_child in child._children:\n                if project_child.path == \"project/folders\":\n                    folders_field = project_child\n                    break\n            if folders_field:\n                break\n\n        if \"folderPathRegex\" not in query._variables:\n            folder_path_regex_var = query.add_variable(\n                \"folderPathRegex\", \"String!\"\n            )\n            folders_field.set_filter(\"pathEx\", folder_path_regex_var)\n\n        query.set_variable_value(\"projectName\", project_name)\n        if folder_regex:\n            query.set_variable_value(\"folderPathRegex\", folder_regex)\n\n        api = get_ayon_server_api_connection()\n        for parsed_data in query.continuous_query(api):\n            for folder in parsed_data[\"project\"][\"folders\"]:\n                yield folder[\"id\"]\n\n    def _get_representations_ayon(self, placeholder):\n        # An OpenPype placeholder loaded in AYON\n        if \"asset\" in placeholder.data:\n            return []\n\n        representation_name = placeholder.data[\"representation\"]\n        if not representation_name:\n            return []\n\n        project_name = self.builder.project_name\n        current_asset_doc = self.builder.current_asset_doc\n\n        folder_path_regex = placeholder.data[\"folder_path\"]\n        product_name_regex_value = placeholder.data[\"product_name\"]\n        product_name_regex = None\n        if product_name_regex_value:\n            product_name_regex = re.compile(product_name_regex_value)\n        product_type = placeholder.data[\"family\"]\n\n        builder_type = placeholder.data[\"builder_type\"]\n        folder_ids = []\n        if builder_type == \"context_folder\":\n            folder_ids = [current_asset_doc[\"_id\"]]\n\n        elif builder_type == \"all_folders\":\n            folder_ids = list(self._query_by_folder_regex(\n                project_name, folder_path_regex\n            ))\n\n        if not folder_ids:\n            return []\n\n        from ayon_api import get_products, get_last_versions\n\n        products = list(get_products(\n            project_name,\n            folder_ids=folder_ids,\n            product_types=[product_type],\n            fields={\"id\", \"name\"}\n        ))\n        filtered_product_ids = set()\n        for product in products:\n            if (\n                product_name_regex is None\n                or product_name_regex.match(product[\"name\"])\n            ):\n                filtered_product_ids.add(product[\"id\"])\n\n        if not filtered_product_ids:\n            return []\n\n        version_ids = set(\n            version[\"id\"]\n            for version in get_last_versions(\n                project_name, filtered_product_ids, fields={\"id\"}\n            ).values()\n        )\n        return list(get_representations(\n            project_name,\n            representation_names=[representation_name],\n            version_ids=version_ids\n        ))\n\n\n    def _get_representations(self, placeholder):\n        \"\"\"Prepared query of representations based on load options.\n\n        This function is directly connected to options defined in\n        'get_load_plugin_options'.\n\n        Note:\n            This returns all representation documents from all versions of\n                matching subset. To filter for last version use\n                '_reduce_last_version_repre_docs'.\n\n        Args:\n            placeholder (PlaceholderItem): Item which should be populated.\n\n        Returns:\n            List[Dict[str, Any]]: Representation documents matching filters\n                from placeholder data.\n        \"\"\"\n\n        if AYON_SERVER_ENABLED:\n            return self._get_representations_ayon(placeholder)\n\n        # An AYON placeholder loaded in OpenPype\n        if \"folder_path\" in placeholder.data:\n            return []\n\n        project_name = self.builder.project_name\n        current_asset_doc = self.builder.current_asset_doc\n        linked_asset_docs = self.builder.linked_asset_docs\n\n        builder_type = placeholder.data[\"builder_type\"]\n        if builder_type == \"context_asset\":\n            context_filters = {\n                \"asset\": [current_asset_doc[\"name\"]],\n                \"subset\": [re.compile(placeholder.data[\"subset\"])],\n                \"hierarchy\": [re.compile(placeholder.data[\"hierarchy\"])],\n                \"representation\": [placeholder.data[\"representation\"]],\n                \"family\": [placeholder.data[\"family\"]]\n            }\n\n        elif builder_type == \"linked_asset\":\n            asset_regex = re.compile(placeholder.data[\"asset\"])\n            linked_asset_names = []\n            for asset_doc in linked_asset_docs:\n                asset_name = asset_doc[\"name\"]\n                if asset_regex.match(asset_name):\n                    linked_asset_names.append(asset_name)\n\n            context_filters = {\n                \"asset\": linked_asset_names,\n                \"subset\": [re.compile(placeholder.data[\"subset\"])],\n                \"hierarchy\": [re.compile(placeholder.data[\"hierarchy\"])],\n                \"representation\": [placeholder.data[\"representation\"]],\n                \"family\": [placeholder.data[\"family\"]],\n            }\n\n        else:\n            context_filters = {\n                \"asset\": [re.compile(placeholder.data[\"asset\"])],\n                \"subset\": [re.compile(placeholder.data[\"subset\"])],\n                \"hierarchy\": [re.compile(placeholder.data[\"hierarchy\"])],\n                \"representation\": [placeholder.data[\"representation\"]],\n                \"family\": [placeholder.data[\"family\"]]\n            }\n\n        return list(get_representations(\n            project_name,\n            context_filters=context_filters\n        ))\n\n    def _before_placeholder_load(self, placeholder):\n        \"\"\"Can be overridden. It's called before placeholder representations\n        are loaded.\n        \"\"\"\n\n        pass\n\n    def _before_repre_load(self, placeholder, representation):\n        \"\"\"Can be overridden. It's called before representation is loaded.\"\"\"\n\n        pass\n\n    def _reduce_last_version_repre_docs(self, representations):\n        \"\"\"Reduce representations to last verison.\"\"\"\n\n        mapping = {}\n        for repre_doc in representations:\n            repre_context = repre_doc[\"context\"]\n\n            asset_name = repre_context[\"asset\"]\n            subset_name = repre_context[\"subset\"]\n            version = repre_context.get(\"version\", -1)\n\n            if asset_name not in mapping:\n                mapping[asset_name] = {}\n\n            subset_mapping = mapping[asset_name]\n            if subset_name not in subset_mapping:\n                subset_mapping[subset_name] = collections.defaultdict(list)\n\n            version_mapping = subset_mapping[subset_name]\n            version_mapping[version].append(repre_doc)\n\n        output = []\n        for subset_mapping in mapping.values():\n            for version_mapping in subset_mapping.values():\n                last_version = tuple(sorted(version_mapping.keys()))[-1]\n                output.extend(version_mapping[last_version])\n        return output\n\n    def populate_load_placeholder(self, placeholder, ignore_repre_ids=None):\n        \"\"\"Load placeholder is going to load matching representations.\n\n        Note:\n            Ignore repre ids is to avoid loading the same representation again\n            on load. But the representation can be loaded with different loader\n            and there could be published new version of matching subset for the\n            representation. We should maybe expect containers.\n\n            Also import loaders don't have containers at all...\n\n        Args:\n            placeholder (PlaceholderItem): Placeholder item with information\n                about requested representations.\n            ignore_repre_ids (Iterable[Union[str, ObjectId]]): Representation\n                ids that should be skipped.\n        \"\"\"\n\n        if ignore_repre_ids is None:\n            ignore_repre_ids = set()\n\n        # TODO check loader existence\n        loader_name = placeholder.data[\"loader\"]\n        loader_args = self.parse_loader_args(placeholder.data[\"loader_args\"])\n\n        placeholder_representations = self._get_representations(placeholder)\n\n        filtered_representations = []\n        for representation in self._reduce_last_version_repre_docs(\n            placeholder_representations\n        ):\n            repre_id = str(representation[\"_id\"])\n            if repre_id not in ignore_repre_ids:\n                filtered_representations.append(representation)\n\n        if not filtered_representations:\n            self.log.info((\n                \"There's no representation for this placeholder: {}\"\n            ).format(placeholder.scene_identifier))\n            return\n\n        repre_load_contexts = get_contexts_for_repre_docs(\n            self.project_name, filtered_representations\n        )\n        loaders_by_name = self.builder.get_loaders_by_name()\n        self._before_placeholder_load(\n            placeholder\n        )\n\n        failed = False\n        for repre_load_context in repre_load_contexts.values():\n            representation = repre_load_context[\"representation\"]\n            repre_context = representation[\"context\"]\n            self._before_repre_load(\n                placeholder, representation\n            )\n            self.log.info(\n                \"Loading {} from {} with loader {}\\n\"\n                \"Loader arguments used : {}\".format(\n                    repre_context[\"subset\"],\n                    repre_context[\"asset\"],\n                    loader_name,\n                    placeholder.data[\"loader_args\"],\n                )\n            )\n            try:\n                container = load_with_repre_context(\n                    loaders_by_name[loader_name],\n                    repre_load_context,\n                    options=loader_args\n                )\n\n            except Exception:\n                self.load_failed(placeholder, representation)\n                failed = True\n            else:\n                self.load_succeed(placeholder, container)\n\n        # Run post placeholder process after load of all representations\n        self.post_placeholder_process(placeholder, failed)\n\n        if failed:\n            self.log.debug(\n                \"Placeholder cleanup skipped due to failed placeholder \"\n                \"population.\"\n            )\n            return\n        if not placeholder.data.get(\"keep_placeholder\", True):\n            self.delete_placeholder(placeholder)\n\n    def load_failed(self, placeholder, representation):\n        if hasattr(placeholder, \"load_failed\"):\n            placeholder.load_failed(representation)\n\n    def load_succeed(self, placeholder, container):\n        if hasattr(placeholder, \"load_succeed\"):\n            placeholder.load_succeed(container)\n\n    def post_placeholder_process(self, placeholder, failed):\n        \"\"\"Cleanup placeholder after load of its corresponding representations.\n\n        Args:\n            placeholder (PlaceholderItem): Item which was just used to load\n                representation.\n            failed (bool): Loading of representation failed.\n        \"\"\"\n\n        pass\n\n    def delete_placeholder(self, placeholder):\n        \"\"\"Called when all item population is done.\"\"\"\n        self.log.debug(\"Clean up of placeholder is not implemented.\")\n\n\nclass PlaceholderCreateMixin(object):\n    \"\"\"Mixin prepared for creating placeholder plugins.\n\n    Implementation prepares options for placeholders with\n    'get_create_plugin_options'.\n\n    For placeholder population is implemented 'populate_create_placeholder'.\n\n    PlaceholderItem can have implemented methods:\n    - 'create_failed' - called when creating of an instance failed\n    - 'create_succeed' - called when creating of an instance succeeded\n    \"\"\"\n\n    def get_create_plugin_options(self, options=None):\n        \"\"\"Unified attribute definitions for create placeholder.\n\n        Common function for placeholder plugins used for creating of\n        publishable instances. Use it with 'get_placeholder_options'.\n\n        Args:\n            plugin (PlaceholderPlugin): Plugin used for creating of\n                publish instances.\n            options (Dict[str, Any]): Already available options which are used\n                as defaults for attributes.\n\n        Returns:\n            List[AbstractAttrDef]: Attribute definitions common for create\n                plugins.\n        \"\"\"\n\n        creators_by_name = self.builder.get_creators_by_name()\n\n        creator_items = [\n            (creator_name, creator.label or creator_name)\n            for creator_name, creator in creators_by_name.items()\n        ]\n\n        creator_items.sort(key=lambda i: i[1])\n        options = options or {}\n        return [\n            attribute_definitions.UISeparatorDef(),\n            attribute_definitions.UILabelDef(\"Main attributes\"),\n            attribute_definitions.UISeparatorDef(),\n\n            attribute_definitions.EnumDef(\n                \"creator\",\n                label=\"Creator\",\n                default=options.get(\"creator\"),\n                items=creator_items,\n                tooltip=(\n                    \"Creator\"\n                    \"\\nDefines what OpenPype creator will be used to\"\n                    \" create publishable instance.\"\n                    \"\\nUseable creator depends on current host's creator list.\"\n                    \"\\nField is case sensitive.\"\n                )\n            ),\n            attribute_definitions.TextDef(\n                \"create_variant\",\n                label=\"Variant\",\n                default=options.get(\"create_variant\"),\n                placeholder='Main',\n                tooltip=(\n                    \"Creator\"\n                    \"\\nDefines variant name which will be use for \"\n                    \"\\ncompiling of subset name.\"\n                )\n            ),\n            attribute_definitions.UISeparatorDef(),\n            attribute_definitions.NumberDef(\n                \"order\",\n                label=\"Order\",\n                default=options.get(\"order\") or 0,\n                decimals=0,\n                minimum=0,\n                maximum=999,\n                tooltip=(\n                    \"Order\"\n                    \"\\nOrder defines creating instance priority (0 to 999)\"\n                    \"\\nPriority rule is : \\\"lowest is first to load\\\".\"\n                )\n            )\n        ]\n\n    def populate_create_placeholder(self, placeholder, pre_create_data=None):\n        \"\"\"Create placeholder is going to create matching publishabe instance.\n\n        Args:\n            placeholder (PlaceholderItem): Placeholder item with information\n                about requested publishable instance.\n            pre_create_data (dict): dictionary of configuration from Creator\n                configuration in UI\n        \"\"\"\n\n        legacy_create = self.builder.use_legacy_creators\n        creator_name = placeholder.data[\"creator\"]\n        create_variant = placeholder.data[\"create_variant\"]\n\n        creator_plugin = self.builder.get_creators_by_name()[creator_name]\n\n        # create subset name\n        context = self._builder.get_current_context()\n        project_name = context[\"project_name\"]\n        asset_name = context[\"asset_name\"]\n        task_name = context[\"task_name\"]\n\n        if legacy_create:\n            asset_doc = get_asset_by_name(\n                project_name, asset_name, fields=[\"_id\"]\n            )\n            assert asset_doc, \"No current asset found in Session\"\n            subset_name = creator_plugin.get_subset_name(\n                create_variant,\n                task_name,\n                asset_doc[\"_id\"],\n                project_name\n            )\n\n        else:\n            asset_doc = get_asset_by_name(project_name, asset_name)\n            assert asset_doc, \"No current asset found in Session\"\n            subset_name = creator_plugin.get_subset_name(\n                create_variant,\n                task_name,\n                asset_doc,\n                project_name,\n                self.builder.host_name\n            )\n\n        creator_data = {\n            \"creator_name\": creator_name,\n            \"create_variant\": create_variant,\n            \"subset_name\": subset_name,\n            \"creator_plugin\": creator_plugin\n        }\n\n        self._before_instance_create(placeholder)\n\n        # compile subset name from variant\n        try:\n            if legacy_create:\n                creator_instance = creator_plugin(\n                    subset_name,\n                    asset_name\n                ).process()\n            else:\n                creator_instance = self.builder.create_context.create(\n                    creator_plugin.identifier,\n                    create_variant,\n                    asset_doc,\n                    task_name=task_name,\n                    pre_create_data=pre_create_data\n                )\n\n        except:  # noqa: E722\n            failed = True\n            self.create_failed(placeholder, creator_data)\n\n        else:\n            failed = False\n            self.create_succeed(placeholder, creator_instance)\n\n        self.post_placeholder_process(placeholder, failed)\n\n        if failed:\n            self.log.debug(\n                \"Placeholder cleanup skipped due to failed placeholder \"\n                \"population.\"\n            )\n            return\n\n        if not placeholder.data.get(\"keep_placeholder\", True):\n            self.delete_placeholder(placeholder)\n\n    def create_failed(self, placeholder, creator_data):\n        if hasattr(placeholder, \"create_failed\"):\n            placeholder.create_failed(creator_data)\n\n    def create_succeed(self, placeholder, creator_instance):\n        if hasattr(placeholder, \"create_succeed\"):\n            placeholder.create_succeed(creator_instance)\n\n    def post_placeholder_process(self, placeholder, failed):\n        \"\"\"Cleanup placeholder after load of its corresponding representations.\n\n        Args:\n            placeholder (PlaceholderItem): Item which was just used to load\n                representation.\n            failed (bool): Loading of representation failed.\n        \"\"\"\n        pass\n\n    def delete_placeholder(self, placeholder):\n        \"\"\"Called when all item population is done.\"\"\"\n        self.log.debug(\"Clean up of placeholder is not implemented.\")\n\n    def _before_instance_create(self, placeholder):\n        \"\"\"Can be overriden. Is called before instance is created.\"\"\"\n\n        pass\n\n\nclass LoadPlaceholderItem(PlaceholderItem):\n    \"\"\"PlaceholderItem for plugin which is loading representations.\n\n    Connected to 'PlaceholderLoadMixin'.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(LoadPlaceholderItem, self).__init__(*args, **kwargs)\n        self._failed_representations = []\n\n    def get_errors(self):\n        if not self._failed_representations:\n            return []\n        message = (\n            \"Failed to load {} representations using Loader {}\"\n        ).format(\n            len(self._failed_representations),\n            self.data[\"loader\"]\n        )\n        return [message]\n\n    def load_failed(self, representation):\n        self._failed_representations.append(representation)\n\n\nclass CreatePlaceholderItem(PlaceholderItem):\n    \"\"\"PlaceholderItem for plugin which is creating publish instance.\n\n    Connected to 'PlaceholderCreateMixin'.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(CreatePlaceholderItem, self).__init__(*args, **kwargs)\n        self._failed_created_publish_instances = []\n\n    def get_errors(self):\n        if not self._failed_created_publish_instances:\n            return []\n        message = (\n            \"Failed to create {} instance using Creator {}\"\n        ).format(\n            len(self._failed_created_publish_instances),\n            self.data[\"creator\"]\n        )\n        return [message]\n\n    def create_failed(self, creator_data):\n        self._failed_created_publish_instances.append(creator_data)\n"
  },
  {
    "path": "openpype/plugins/actions/open_file_explorer.py",
    "content": "import os\nimport platform\nimport subprocess\n\nfrom string import Formatter\nfrom openpype.client import (\n    get_project,\n    get_asset_by_name,\n)\nfrom openpype.pipeline import (\n    Anatomy,\n    LauncherAction,\n)\nfrom openpype.pipeline.template_data import get_template_data\n\n\nclass OpenTaskPath(LauncherAction):\n    name = \"open_task_path\"\n    label = \"Explore here\"\n    icon = \"folder-open\"\n    order = 500\n\n    def is_compatible(self, session):\n        \"\"\"Return whether the action is compatible with the session\"\"\"\n        return bool(session.get(\"AVALON_ASSET\"))\n\n    def process(self, session, **kwargs):\n        from qtpy import QtCore, QtWidgets\n\n        project_name = session[\"AVALON_PROJECT\"]\n        asset_name = session[\"AVALON_ASSET\"]\n        task_name = session.get(\"AVALON_TASK\", None)\n\n        path = self._get_workdir(project_name, asset_name, task_name)\n        if not path:\n            return\n\n        app = QtWidgets.QApplication.instance()\n        ctrl_pressed = QtCore.Qt.ControlModifier & app.keyboardModifiers()\n        if ctrl_pressed:\n            # Copy path to clipboard\n            self.copy_path_to_clipboard(path)\n        else:\n            self.open_in_explorer(path)\n\n    def _find_first_filled_path(self, path):\n        if not path:\n            return \"\"\n\n        fields = set()\n        for item in Formatter().parse(path):\n            _, field_name, format_spec, conversion = item\n            if not field_name:\n                continue\n            conversion = \"!{}\".format(conversion) if conversion else \"\"\n            format_spec = \":{}\".format(format_spec) if format_spec else \"\"\n            orig_key = \"{{{}{}{}}}\".format(\n                field_name, conversion, format_spec)\n            fields.add(orig_key)\n\n        for field in fields:\n            path = path.split(field, 1)[0]\n        return path\n\n    def _get_workdir(self, project_name, asset_name, task_name):\n        project = get_project(project_name)\n        asset = get_asset_by_name(project_name, asset_name)\n\n        data = get_template_data(project, asset, task_name)\n\n        anatomy = Anatomy(project_name)\n        workdir = anatomy.templates_obj[\"work\"][\"folder\"].format(data)\n\n        # Remove any potential un-formatted parts of the path\n        valid_workdir = self._find_first_filled_path(workdir)\n\n        # Path is not filled at all\n        if not valid_workdir:\n            raise AssertionError(\"Failed to calculate workdir.\")\n\n        # Normalize\n        valid_workdir = os.path.normpath(valid_workdir)\n        if os.path.exists(valid_workdir):\n            return valid_workdir\n\n        data.pop(\"task\", None)\n        workdir = anatomy.templates_obj[\"work\"][\"folder\"].format(data)\n        valid_workdir = self._find_first_filled_path(workdir)\n        if valid_workdir:\n            # Normalize\n            valid_workdir = os.path.normpath(valid_workdir)\n            if os.path.exists(valid_workdir):\n                return valid_workdir\n        raise AssertionError(\"Folder does not exist yet.\")\n\n    @staticmethod\n    def open_in_explorer(path):\n        platform_name = platform.system().lower()\n        if platform_name == \"windows\":\n            args = [\"start\", path]\n        elif platform_name == \"darwin\":\n            args = [\"open\", \"-na\", path]\n        elif platform_name == \"linux\":\n            args = [\"xdg-open\", path]\n        else:\n            raise RuntimeError(f\"Unknown platform {platform.system()}\")\n        # Make sure path is converted correctly for 'os.system'\n        os.system(subprocess.list2cmdline(args))\n\n    @staticmethod\n    def copy_path_to_clipboard(path):\n        from qtpy import QtWidgets\n\n        path = path.replace(\"\\\\\", \"/\")\n        print(f\"Copied to clipboard: {path}\")\n        app = QtWidgets.QApplication.instance()\n        assert app, \"Must have running QApplication instance\"\n\n        # Set to Clipboard\n        clipboard = QtWidgets.QApplication.clipboard()\n        clipboard.setText(os.path.normpath(path))\n"
  },
  {
    "path": "openpype/plugins/inventory/remove_and_load.py",
    "content": "from openpype.pipeline import InventoryAction\nfrom openpype.pipeline import get_current_project_name\nfrom openpype.pipeline.load.plugins import discover_loader_plugins\nfrom openpype.pipeline.load.utils import (\n    get_loader_identifier,\n    remove_container,\n    load_container,\n)\nfrom openpype.client import get_representation_by_id\n\n\nclass RemoveAndLoad(InventoryAction):\n    \"\"\"Delete inventory item and reload it.\"\"\"\n\n    label = \"Remove and load\"\n    icon = \"refresh\"\n\n    def process(self, containers):\n        project_name = get_current_project_name()\n        loaders_by_name = {\n            get_loader_identifier(plugin): plugin\n            for plugin in discover_loader_plugins(project_name=project_name)\n        }\n        for container in containers:\n            # Get loader\n            loader_name = container[\"loader\"]\n            loader = loaders_by_name.get(loader_name, None)\n            if not loader:\n                raise RuntimeError(\n                    \"Failed to get loader '{}', can't remove \"\n                    \"and load container\".format(loader_name)\n                )\n\n            # Get representation\n            representation = get_representation_by_id(\n                project_name, container[\"representation\"]\n            )\n            if not representation:\n                self.log.warning(\n                    \"Skipping remove and load because representation id is not\"\n                    \" found in database: '{}'\".format(\n                        container[\"representation\"]\n                    )\n                )\n                continue\n\n            # Remove container\n            remove_container(container)\n\n            # Load container\n            load_container(loader, representation)\n"
  },
  {
    "path": "openpype/plugins/load/copy_file.py",
    "content": "from openpype.style import get_default_entity_icon_color\nfrom openpype.pipeline import load\n\n\nclass CopyFile(load.LoaderPlugin):\n    \"\"\"Copy the published file to be pasted at the desired location\"\"\"\n\n    representations = [\"*\"]\n    families = [\"*\"]\n\n    label = \"Copy File\"\n    order = 10\n    icon = \"copy\"\n    color = get_default_entity_icon_color()\n\n    def load(self, context, name=None, namespace=None, data=None):\n        path = self.filepath_from_context(context)\n        self.log.info(\"Added copy to clipboard: {0}\".format(path))\n        self.copy_file_to_clipboard(path)\n\n    @staticmethod\n    def copy_file_to_clipboard(path):\n        from qtpy import QtCore, QtWidgets\n\n        clipboard = QtWidgets.QApplication.clipboard()\n        assert clipboard, \"Must have running QApplication instance\"\n\n        # Build mime data for clipboard\n        data = QtCore.QMimeData()\n        url = QtCore.QUrl.fromLocalFile(path)\n        data.setUrls([url])\n\n        # Set to Clipboard\n        clipboard.setMimeData(data)\n"
  },
  {
    "path": "openpype/plugins/load/copy_file_path.py",
    "content": "import os\n\nfrom openpype.pipeline import load\n\n\nclass CopyFilePath(load.LoaderPlugin):\n    \"\"\"Copy published file path to clipboard\"\"\"\n    representations = [\"*\"]\n    families = [\"*\"]\n\n    label = \"Copy File Path\"\n    order = 20\n    icon = \"clipboard\"\n    color = \"#999999\"\n\n    def load(self, context, name=None, namespace=None, data=None):\n        path = self.filepath_from_context(context)\n        self.log.info(\"Added file path to clipboard: {0}\".format(path))\n        self.copy_path_to_clipboard(path)\n\n    @staticmethod\n    def copy_path_to_clipboard(path):\n        from qtpy import QtWidgets\n\n        clipboard = QtWidgets.QApplication.clipboard()\n        assert clipboard, \"Must have running QApplication instance\"\n\n        # Set to Clipboard\n        clipboard.setText(os.path.normpath(path))\n"
  },
  {
    "path": "openpype/plugins/load/delete_old_versions.py",
    "content": "import collections\nimport os\nimport uuid\n\nimport clique\nfrom pymongo import UpdateOne\nimport qargparse\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype import style\nfrom openpype.client import get_versions, get_representations\nfrom openpype.modules import ModulesManager\nfrom openpype.lib import format_file_size\nfrom openpype.pipeline import load, AvalonMongoDB, Anatomy\nfrom openpype.pipeline.load import (\n    get_representation_path_with_anatomy,\n    InvalidRepresentationContext,\n)\n\n\nclass DeleteOldVersions(load.SubsetLoaderPlugin):\n    \"\"\"Deletes specific number of old version\"\"\"\n\n    is_multiple_contexts_compatible = True\n    sequence_splitter = \"__sequence_splitter__\"\n\n    representations = [\"*\"]\n    families = [\"*\"]\n    tool_names = [\"library_loader\"]\n\n    label = \"Delete Old Versions\"\n    order = 35\n    icon = \"trash\"\n    color = \"#d8d8d8\"\n\n    options = [\n        qargparse.Integer(\n            \"versions_to_keep\", default=2, min=0, help=\"Versions to keep:\"\n        ),\n        qargparse.Boolean(\n            \"remove_publish_folder\", help=\"Remove publish folder:\"\n        )\n    ]\n\n    def delete_whole_dir_paths(self, dir_paths, delete=True):\n        size = 0\n\n        for dir_path in dir_paths:\n            # Delete all files and fodlers in dir path\n            for root, dirs, files in os.walk(dir_path, topdown=False):\n                for name in files:\n                    file_path = os.path.join(root, name)\n                    size += os.path.getsize(file_path)\n                    if delete:\n                        os.remove(file_path)\n                        self.log.debug(\"Removed file: {}\".format(file_path))\n\n                for name in dirs:\n                    if delete:\n                        os.rmdir(os.path.join(root, name))\n\n            if not delete:\n                continue\n\n            # Delete even the folder and it's parents folders if they are empty\n            while True:\n                if not os.path.exists(dir_path):\n                    dir_path = os.path.dirname(dir_path)\n                    continue\n\n                if len(os.listdir(dir_path)) != 0:\n                    break\n\n                os.rmdir(os.path.join(dir_path))\n\n        return size\n\n    def path_from_representation(self, representation, anatomy):\n        try:\n            context = representation[\"context\"]\n        except KeyError:\n            return (None, None)\n\n        try:\n            path = get_representation_path_with_anatomy(\n                representation, anatomy\n            )\n        except InvalidRepresentationContext:\n            return (None, None)\n\n        sequence_path = None\n        if \"frame\" in context:\n            context[\"frame\"] = self.sequence_splitter\n            sequence_path = get_representation_path_with_anatomy(\n                representation, anatomy\n            )\n\n        if sequence_path:\n            sequence_path = sequence_path.normalized()\n\n        return (path.normalized(), sequence_path)\n\n    def delete_only_repre_files(self, dir_paths, file_paths, delete=True):\n        size = 0\n\n        for dir_id, dir_path in dir_paths.items():\n            dir_files = os.listdir(dir_path)\n            collections, remainders = clique.assemble(dir_files)\n            for file_path, seq_path in file_paths[dir_id]:\n                file_path_base = os.path.split(file_path)[1]\n                # Just remove file if `frame` key was not in context or\n                # filled path is in remainders (single file sequence)\n                if not seq_path or file_path_base in remainders:\n                    if not os.path.exists(file_path):\n                        self.log.debug(\n                            \"File was not found: {}\".format(file_path)\n                        )\n                        continue\n\n                    size += os.path.getsize(file_path)\n\n                    if delete:\n                        os.remove(file_path)\n                        self.log.debug(\"Removed file: {}\".format(file_path))\n\n                    if file_path_base in remainders:\n                        remainders.remove(file_path_base)\n                    continue\n\n                seq_path_base = os.path.split(seq_path)[1]\n                head, tail = seq_path_base.split(self.sequence_splitter)\n\n                final_col = None\n                for collection in collections:\n                    if head != collection.head or tail != collection.tail:\n                        continue\n                    final_col = collection\n                    break\n\n                if final_col is not None:\n                    # Fill full path to head\n                    final_col.head = os.path.join(dir_path, final_col.head)\n                    for _file_path in final_col:\n                        if os.path.exists(_file_path):\n\n                            size += os.path.getsize(_file_path)\n\n                            if delete:\n                                os.remove(_file_path)\n                                self.log.debug(\n                                    \"Removed file: {}\".format(_file_path)\n                                )\n\n                    _seq_path = final_col.format(\"{head}{padding}{tail}\")\n                    self.log.debug(\"Removed files: {}\".format(_seq_path))\n                    collections.remove(final_col)\n\n                elif os.path.exists(file_path):\n                    size += os.path.getsize(file_path)\n\n                    if delete:\n                        os.remove(file_path)\n                        self.log.debug(\"Removed file: {}\".format(file_path))\n                else:\n                    self.log.debug(\n                        \"File was not found: {}\".format(file_path)\n                    )\n\n        # Delete as much as possible parent folders\n        if not delete:\n            return size\n\n        for dir_path in dir_paths.values():\n            while True:\n                if not os.path.exists(dir_path):\n                    dir_path = os.path.dirname(dir_path)\n                    continue\n\n                if len(os.listdir(dir_path)) != 0:\n                    break\n\n                self.log.debug(\"Removed folder: {}\".format(dir_path))\n                os.rmdir(dir_path)\n\n        return size\n\n    def message(self, text):\n        msgBox = QtWidgets.QMessageBox()\n        msgBox.setText(text)\n        msgBox.setStyleSheet(style.load_stylesheet())\n        msgBox.setWindowFlags(\n            msgBox.windowFlags() | QtCore.Qt.FramelessWindowHint\n        )\n        msgBox.exec_()\n\n    def get_data(self, context, versions_count):\n        subset = context[\"subset\"]\n        asset = context[\"asset\"]\n        project_name = context[\"project\"][\"name\"]\n        anatomy = Anatomy(project_name)\n\n        versions = list(get_versions(project_name, subset_ids=[subset[\"_id\"]]))\n\n        versions_by_parent = collections.defaultdict(list)\n        for ent in versions:\n            versions_by_parent[ent[\"parent\"]].append(ent)\n\n        def sort_func(ent):\n            return int(ent[\"name\"])\n\n        all_last_versions = []\n        for _parent_id, _versions in versions_by_parent.items():\n            for idx, version in enumerate(\n                sorted(_versions, key=sort_func, reverse=True)\n            ):\n                if idx >= versions_count:\n                    break\n                all_last_versions.append(version)\n\n        self.log.debug(\"Collected versions ({})\".format(len(versions)))\n\n        # Filter latest versions\n        for version in all_last_versions:\n            versions.remove(version)\n\n        # Update versions_by_parent without filtered versions\n        versions_by_parent = collections.defaultdict(list)\n        for ent in versions:\n            versions_by_parent[ent[\"parent\"]].append(ent)\n\n        # Filter already deleted versions\n        versions_to_pop = []\n        for version in versions:\n            version_tags = version[\"data\"].get(\"tags\")\n            if version_tags and \"deleted\" in version_tags:\n                versions_to_pop.append(version)\n\n        for version in versions_to_pop:\n            msg = \"Asset: \\\"{}\\\" | Subset: \\\"{}\\\" | Version: \\\"{}\\\"\".format(\n                asset[\"name\"], subset[\"name\"], version[\"name\"]\n            )\n            self.log.debug((\n                \"Skipping version. Already tagged as `deleted`. < {} >\"\n            ).format(msg))\n            versions.remove(version)\n\n        version_ids = [ent[\"_id\"] for ent in versions]\n\n        self.log.debug(\n            \"Filtered versions to delete ({})\".format(len(version_ids))\n        )\n\n        if not version_ids:\n            msg = \"Skipping processing. Nothing to delete on {}/{}\".format(\n                asset[\"name\"], subset[\"name\"]\n            )\n            self.log.info(msg)\n            print(msg)\n            return\n\n        repres = list(get_representations(\n            project_name, version_ids=version_ids\n        ))\n\n        self.log.debug(\n            \"Collected representations to remove ({})\".format(len(repres))\n        )\n\n        dir_paths = {}\n        file_paths_by_dir = collections.defaultdict(list)\n        for repre in repres:\n            file_path, seq_path = self.path_from_representation(repre, anatomy)\n            if file_path is None:\n                self.log.debug((\n                    \"Could not format path for represenation \\\"{}\\\"\"\n                ).format(str(repre)))\n                continue\n\n            dir_path = os.path.dirname(file_path)\n            dir_id = None\n            for _dir_id, _dir_path in dir_paths.items():\n                if _dir_path == dir_path:\n                    dir_id = _dir_id\n                    break\n\n            if dir_id is None:\n                dir_id = uuid.uuid4()\n                dir_paths[dir_id] = dir_path\n\n            file_paths_by_dir[dir_id].append([file_path, seq_path])\n\n        dir_ids_to_pop = []\n        for dir_id, dir_path in dir_paths.items():\n            if os.path.exists(dir_path):\n                continue\n\n            dir_ids_to_pop.append(dir_id)\n\n        # Pop dirs from both dictionaries\n        for dir_id in dir_ids_to_pop:\n            dir_paths.pop(dir_id)\n            paths = file_paths_by_dir.pop(dir_id)\n            # TODO report of missing directories?\n            paths_msg = \", \".join([\n                \"'{}'\".format(path[0].replace(\"\\\\\", \"/\")) for path in paths\n            ])\n            self.log.debug((\n                \"Folder does not exist. Deleting it's files skipped: {}\"\n            ).format(paths_msg))\n\n        data = {\n            \"dir_paths\": dir_paths,\n            \"file_paths_by_dir\": file_paths_by_dir,\n            \"versions\": versions,\n            \"asset\": asset,\n            \"subset\": subset,\n            \"archive_subset\": versions_count == 0\n        }\n\n        return data\n\n    def main(self, project_name, data, remove_publish_folder):\n        # Size of files.\n        size = 0\n        if not data:\n            return size\n\n        if remove_publish_folder:\n            size = self.delete_whole_dir_paths(data[\"dir_paths\"].values())\n        else:\n            size = self.delete_only_repre_files(\n                data[\"dir_paths\"], data[\"file_paths_by_dir\"]\n            )\n\n        mongo_changes_bulk = []\n        for version in data[\"versions\"]:\n            orig_version_tags = version[\"data\"].get(\"tags\") or []\n            version_tags = [tag for tag in orig_version_tags]\n            if \"deleted\" not in version_tags:\n                version_tags.append(\"deleted\")\n\n            if version_tags == orig_version_tags:\n                continue\n\n            update_query = {\"_id\": version[\"_id\"]}\n            update_data = {\"$set\": {\"data.tags\": version_tags}}\n            mongo_changes_bulk.append(UpdateOne(update_query, update_data))\n\n        if data[\"archive_subset\"]:\n            mongo_changes_bulk.append(UpdateOne(\n                {\n                    \"_id\": data[\"subset\"][\"_id\"],\n                    \"type\": \"subset\"\n                },\n                {\"$set\": {\"type\": \"archived_subset\"}}\n            ))\n\n        if mongo_changes_bulk:\n            dbcon = AvalonMongoDB()\n            dbcon.Session[\"AVALON_PROJECT\"] = project_name\n            dbcon.install()\n            dbcon.bulk_write(mongo_changes_bulk)\n            dbcon.uninstall()\n\n        self._ftrack_delete_versions(data)\n\n        return size\n\n    def _ftrack_delete_versions(self, data):\n        \"\"\"Delete version on ftrack.\n\n        Handling of ftrack logic in this plugin is not ideal. But in OP3 it is\n        almost impossible to solve the issue other way.\n\n        Note:\n            Asset versions on ftrack are not deleted but marked as\n                \"not published\" which cause that they're invisible.\n\n        Args:\n            data (dict): Data sent to subset loader with full context.\n        \"\"\"\n\n        # First check for ftrack id on asset document\n        #   - skip if ther is none\n        asset_ftrack_id = data[\"asset\"][\"data\"].get(\"ftrackId\")\n        if not asset_ftrack_id:\n            self.log.info((\n                \"Asset does not have filled ftrack id. Skipped delete\"\n                \" of ftrack version.\"\n            ))\n            return\n\n        # Check if ftrack module is enabled\n        modules_manager = ModulesManager()\n        ftrack_module = modules_manager.modules_by_name.get(\"ftrack\")\n        if not ftrack_module or not ftrack_module.enabled:\n            return\n\n        import ftrack_api\n\n        session = ftrack_api.Session()\n        subset_name = data[\"subset\"][\"name\"]\n        versions = {\n            '\"{}\"'.format(version_doc[\"name\"])\n            for version_doc in data[\"versions\"]\n        }\n        asset_versions = session.query(\n            (\n                \"select id, is_published from AssetVersion where\"\n                \" asset.parent.id is \\\"{}\\\"\"\n                \" and asset.name is \\\"{}\\\"\"\n                \" and version in ({})\"\n            ).format(\n                asset_ftrack_id,\n                subset_name,\n                \",\".join(versions)\n            )\n        ).all()\n\n        # Set attribute `is_published` to `False` on ftrack AssetVersions\n        for asset_version in asset_versions:\n            asset_version[\"is_published\"] = False\n\n        try:\n            session.commit()\n\n        except Exception:\n            msg = (\n                \"Could not set `is_published` attribute to `False`\"\n                \" for selected AssetVersions.\"\n            )\n            self.log.error(msg)\n            self.message(msg)\n\n    def load(self, contexts, name=None, namespace=None, options=None):\n        try:\n            size = 0\n            for count, context in enumerate(contexts):\n                versions_to_keep = 2\n                remove_publish_folder = False\n                if options:\n                    versions_to_keep = options.get(\n                        \"versions_to_keep\", versions_to_keep\n                    )\n                    remove_publish_folder = options.get(\n                        \"remove_publish_folder\", remove_publish_folder\n                    )\n\n                data = self.get_data(context, versions_to_keep)\n                if not data:\n                    continue\n\n                project_name = context[\"project\"][\"name\"]\n                size += self.main(project_name, data, remove_publish_folder)\n                print(\"Progressing {}/{}\".format(count + 1, len(contexts)))\n\n            msg = \"Total size of files: {}\".format(format_file_size(size))\n            self.log.info(msg)\n            self.message(msg)\n\n        except Exception:\n            self.log.error(\"Failed to delete versions.\", exc_info=True)\n\n\nclass CalculateOldVersions(DeleteOldVersions):\n    \"\"\"Calculate file size of old versions\"\"\"\n    label = \"Calculate Old Versions\"\n    order = 30\n    tool_names = [\"library_loader\"]\n\n    options = [\n        qargparse.Integer(\n            \"versions_to_keep\", default=2, min=0, help=\"Versions to keep:\"\n        ),\n        qargparse.Boolean(\n            \"remove_publish_folder\", help=\"Remove publish folder:\"\n        )\n    ]\n\n    def main(self, project_name, data, remove_publish_folder):\n        size = 0\n\n        if not data:\n            return size\n\n        if remove_publish_folder:\n            size = self.delete_whole_dir_paths(\n                data[\"dir_paths\"].values(), delete=False\n            )\n        else:\n            size = self.delete_only_repre_files(\n                data[\"dir_paths\"], data[\"file_paths_by_dir\"], delete=False\n            )\n\n        return size\n"
  },
  {
    "path": "openpype/plugins/load/delivery.py",
    "content": "import copy\nimport platform\nfrom collections import defaultdict\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.client import get_representations\nfrom openpype.pipeline import load, Anatomy\nfrom openpype import resources, style\n\nfrom openpype.lib import (\n    format_file_size,\n    collect_frames,\n    get_datetime_data,\n)\nfrom openpype.pipeline.load import get_representation_path_with_anatomy\nfrom openpype.pipeline.delivery import (\n    get_format_dict,\n    check_destination_path,\n    deliver_single_file,\n    deliver_sequence,\n)\n\n\nclass Delivery(load.SubsetLoaderPlugin):\n    \"\"\"Export selected versions to folder structure from Template\"\"\"\n\n    is_multiple_contexts_compatible = True\n    sequence_splitter = \"__sequence_splitter__\"\n\n    representations = [\"*\"]\n    families = [\"*\"]\n    tool_names = [\"library_loader\"]\n\n    label = \"Deliver Versions\"\n    order = 35\n    icon = \"upload\"\n    color = \"#d8d8d8\"\n\n    def message(self, text):\n        msgBox = QtWidgets.QMessageBox()\n        msgBox.setText(text)\n        msgBox.setStyleSheet(style.load_stylesheet())\n        msgBox.setWindowFlags(\n            msgBox.windowFlags() | QtCore.Qt.FramelessWindowHint\n        )\n        msgBox.exec_()\n\n    def load(self, contexts, name=None, namespace=None, options=None):\n        try:\n            dialog = DeliveryOptionsDialog(contexts, self.log)\n            dialog.exec_()\n        except Exception:\n            self.log.error(\"Failed to deliver versions.\", exc_info=True)\n\n\nclass DeliveryOptionsDialog(QtWidgets.QDialog):\n    \"\"\"Dialog to select template where to deliver selected representations.\"\"\"\n\n    def __init__(self, contexts, log=None, parent=None):\n        super(DeliveryOptionsDialog, self).__init__(parent=parent)\n\n        self.setWindowTitle(\"OpenPype - Deliver versions\")\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n\n        self.setWindowFlags(\n            QtCore.Qt.WindowStaysOnTopHint\n            | QtCore.Qt.WindowCloseButtonHint\n            | QtCore.Qt.WindowMinimizeButtonHint\n        )\n\n        self.setStyleSheet(style.load_stylesheet())\n\n        project_name = contexts[0][\"project\"][\"name\"]\n        self.anatomy = Anatomy(project_name)\n        self._representations = None\n        self.log = log\n        self.currently_uploaded = 0\n\n        self._set_representations(project_name, contexts)\n\n        dropdown = QtWidgets.QComboBox()\n        self.templates = self._get_templates(self.anatomy)\n        for name, _ in self.templates.items():\n            dropdown.addItem(name)\n        if self.templates and platform.system() == \"Darwin\":\n            # fix macos QCombobox Style\n            dropdown.setItemDelegate(QtWidgets.QStyledItemDelegate())\n            # update combo box length to longest entry\n            longest_key = max(self.templates.keys(), key=len)\n            dropdown.setMinimumContentsLength(len(longest_key))\n\n        template_label = QtWidgets.QLabel()\n        template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))\n        template_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)\n\n        renumber_frame = QtWidgets.QCheckBox()\n\n        first_frame_start = QtWidgets.QSpinBox()\n        max_int = (1 << 32) // 2\n        first_frame_start.setRange(0, max_int - 1)\n\n        root_line_edit = QtWidgets.QLineEdit()\n\n        repre_checkboxes_layout = QtWidgets.QFormLayout()\n        repre_checkboxes_layout.setContentsMargins(10, 5, 5, 10)\n\n        self._representation_checkboxes = {}\n        for repre in self._get_representation_names():\n            checkbox = QtWidgets.QCheckBox()\n            checkbox.setChecked(False)\n            self._representation_checkboxes[repre] = checkbox\n\n            checkbox.stateChanged.connect(self._update_selected_label)\n            repre_checkboxes_layout.addRow(repre, checkbox)\n\n        selected_label = QtWidgets.QLabel()\n\n        input_widget = QtWidgets.QWidget(self)\n        input_layout = QtWidgets.QFormLayout(input_widget)\n        input_layout.setContentsMargins(10, 15, 5, 5)\n\n        input_layout.addRow(\"Selected representations\", selected_label)\n        input_layout.addRow(\"Delivery template\", dropdown)\n        input_layout.addRow(\"Template value\", template_label)\n        input_layout.addRow(\"Renumber Frame\", renumber_frame)\n        input_layout.addRow(\"Renumber start frame\", first_frame_start)\n        input_layout.addRow(\"Root\", root_line_edit)\n        input_layout.addRow(\"Representations\", repre_checkboxes_layout)\n\n        btn_delivery = QtWidgets.QPushButton(\"Deliver\")\n        btn_delivery.setEnabled(False)\n\n        progress_bar = QtWidgets.QProgressBar(self)\n        progress_bar.setMinimum = 0\n        progress_bar.setMaximum = 100\n        progress_bar.setVisible(False)\n\n        text_area = QtWidgets.QTextEdit()\n        text_area.setReadOnly(True)\n        text_area.setVisible(False)\n        text_area.setMinimumHeight(100)\n\n        layout = QtWidgets.QVBoxLayout(self)\n\n        layout.addWidget(input_widget)\n        layout.addStretch(1)\n        layout.addWidget(btn_delivery)\n        layout.addWidget(progress_bar)\n        layout.addWidget(text_area)\n\n        self.selected_label = selected_label\n        self.template_label = template_label\n        self.dropdown = dropdown\n        self.first_frame_start = first_frame_start\n        self.renumber_frame = renumber_frame\n        self.root_line_edit = root_line_edit\n        self.progress_bar = progress_bar\n        self.text_area = text_area\n        self.btn_delivery = btn_delivery\n\n        self.files_selected, self.size_selected = \\\n            self._get_counts(self._get_selected_repres())\n\n        self._update_selected_label()\n        self._update_template_value()\n\n        btn_delivery.clicked.connect(self.deliver)\n        dropdown.currentIndexChanged.connect(self._update_template_value)\n\n        if not self.dropdown.count():\n            self.text_area.setVisible(True)\n            error_message = (\n                \"No Delivery Templates found!\\n\"\n                \"Add Template in [project_anatomy/templates/delivery]\"\n            )\n            self.text_area.setText(error_message)\n            self.log.error(error_message.replace(\"\\n\", \" \"))\n\n    def deliver(self):\n        \"\"\"Main method to loop through all selected representations\"\"\"\n        self.progress_bar.setVisible(True)\n        self.btn_delivery.setEnabled(False)\n        QtWidgets.QApplication.processEvents()\n\n        report_items = defaultdict(list)\n\n        selected_repres = self._get_selected_repres()\n\n        datetime_data = get_datetime_data()\n        template_name = self.dropdown.currentText()\n        format_dict = get_format_dict(self.anatomy, self.root_line_edit.text())\n        renumber_frame = self.renumber_frame.isChecked()\n        frame_offset = self.first_frame_start.value()\n        for repre in self._representations:\n            if repre[\"name\"] not in selected_repres:\n                continue\n\n            repre_path = get_representation_path_with_anatomy(\n                repre, self.anatomy\n            )\n\n            anatomy_data = copy.deepcopy(repre[\"context\"])\n            new_report_items = check_destination_path(str(repre[\"_id\"]),\n                                                      self.anatomy,\n                                                      anatomy_data,\n                                                      datetime_data,\n                                                      template_name)\n\n            report_items.update(new_report_items)\n            if new_report_items:\n                continue\n\n            args = [\n                repre_path,\n                repre,\n                self.anatomy,\n                template_name,\n                anatomy_data,\n                format_dict,\n                report_items,\n                self.log\n            ]\n\n            if repre.get(\"files\"):\n                src_paths = []\n                for repre_file in repre[\"files\"]:\n                    src_path = self.anatomy.fill_root(repre_file[\"path\"])\n                    src_paths.append(src_path)\n                sources_and_frames = collect_frames(src_paths)\n\n                frames = set(sources_and_frames.values())\n                frames.discard(None)\n                first_frame = None\n                if frames:\n                    first_frame = min(frames)\n\n                for src_path, frame in sources_and_frames.items():\n                    args[0] = src_path\n                    # Renumber frames\n                    if renumber_frame and frame is not None:\n                        # Calculate offset between\n                        # first frame and current frame\n                        # - '0' for first frame\n                        offset = frame_offset - int(first_frame)\n                        # Add offset to new frame start\n                        dst_frame = int(frame) + offset\n                        if dst_frame < 0:\n                            msg = \"Renumber frame has a smaller number than original frame\"     # noqa\n                            report_items[msg].append(src_path)\n                            self.log.warning(\"{} <{}>\".format(\n                                msg, dst_frame))\n                            continue\n                        frame = dst_frame\n\n                    if frame is not None:\n                        anatomy_data[\"frame\"] = frame\n                    new_report_items, uploaded = deliver_single_file(*args)\n                    report_items.update(new_report_items)\n                    self._update_progress(uploaded)\n            else:  # fallback for Pype2 and representations without files\n                frame = repre['context'].get('frame')\n                if frame:\n                    repre[\"context\"][\"frame\"] = len(str(frame)) * \"#\"\n\n                if not frame:\n                    new_report_items, uploaded = deliver_single_file(*args)\n                else:\n                    new_report_items, uploaded = deliver_sequence(*args)\n                report_items.update(new_report_items)\n                self._update_progress(uploaded)\n\n        self.text_area.setText(self._format_report(report_items))\n        self.text_area.setVisible(True)\n\n    def _get_representation_names(self):\n        \"\"\"Get set of representation names for checkbox filtering.\"\"\"\n        return set([repre[\"name\"] for repre in self._representations])\n\n    def _get_templates(self, anatomy):\n        \"\"\"Adds list of delivery templates from Anatomy to dropdown.\"\"\"\n        templates = {}\n        for template_name, value in anatomy.templates[\"delivery\"].items():\n            if not isinstance(value, str) or not value.startswith('{root'):\n                continue\n\n            templates[template_name] = value\n\n        return templates\n\n    def _set_representations(self, project_name, contexts):\n        version_ids = [context[\"version\"][\"_id\"] for context in contexts]\n\n        repres = list(get_representations(\n            project_name, version_ids=version_ids\n        ))\n\n        self._representations = repres\n\n    def _get_counts(self, selected_repres=None):\n        \"\"\"Returns tuple of number of selected files and their size.\"\"\"\n        files_selected = 0\n        size_selected = 0\n        for repre in self._representations:\n            if repre[\"name\"] in selected_repres:\n                files = repre.get(\"files\", [])\n                if not files:  # for repre without files, cannot divide by 0\n                    files_selected += 1\n                    size_selected += 0\n                else:\n                    for repre_file in files:\n                        files_selected += 1\n                        size_selected += repre_file[\"size\"]\n\n        return files_selected, size_selected\n\n    def _prepare_label(self):\n        \"\"\"Provides text with no of selected files and their size.\"\"\"\n        label = \"{} files, size {}\".format(\n            self.files_selected,\n            format_file_size(self.size_selected))\n        return label\n\n    def _get_selected_repres(self):\n        \"\"\"Returns list of representation names filtered from checkboxes.\"\"\"\n        selected_repres = []\n        for repre_name, chckbox in self._representation_checkboxes.items():\n            if chckbox.isChecked():\n                selected_repres.append(repre_name)\n\n        return selected_repres\n\n    def _update_selected_label(self):\n        \"\"\"Updates label with list of number of selected files.\"\"\"\n        selected_repres = self._get_selected_repres()\n        self.files_selected, self.size_selected = \\\n            self._get_counts(selected_repres)\n        self.selected_label.setText(self._prepare_label())\n        # update delivery button state if any templates found\n        if self.dropdown.count():\n            self.btn_delivery.setEnabled(bool(selected_repres))\n\n    def _update_template_value(self, _index=None):\n        \"\"\"Sets template value to label after selection in dropdown.\"\"\"\n        name = self.dropdown.currentText()\n        template_value = self.templates.get(name)\n        if template_value:\n            self.template_label.setText(template_value)\n            self.btn_delivery.setEnabled(bool(self._get_selected_repres()))\n\n    def _update_progress(self, uploaded):\n        \"\"\"Update progress bar after each repre copied.\"\"\"\n        self.currently_uploaded += uploaded\n\n        ratio = self.currently_uploaded / self.files_selected\n        self.progress_bar.setValue(ratio * self.progress_bar.maximum())\n\n    def _format_report(self, report_items):\n        \"\"\"Format final result and error details as html.\"\"\"\n        msg = \"Delivery finished\"\n        if not report_items:\n            msg += \" successfully\"\n        else:\n            msg += \" with errors\"\n        txt = \"<h2>{}</h2>\".format(msg)\n        for header, data in report_items.items():\n            txt += \"<h3>{}</h3>\".format(header)\n            for item in data:\n                txt += \"{}<br>\".format(item)\n\n        return txt\n"
  },
  {
    "path": "openpype/plugins/load/open_djv.py",
    "content": "import os\nfrom openpype.lib import ApplicationManager\nfrom openpype.pipeline import load\n\n\ndef existing_djv_path():\n    app_manager = ApplicationManager()\n    djv_list = []\n\n    for app_name, app in app_manager.applications.items():\n        if 'djv' in app_name and app.find_executable():\n            djv_list.append(app_name)\n\n    return djv_list\n\n\nclass OpenInDJV(load.LoaderPlugin):\n    \"\"\"Open Image Sequence with system default\"\"\"\n\n    djv_list = existing_djv_path()\n    families = [\"*\"] if djv_list else []\n    representations = [\"*\"]\n    extensions = {\n        \"cin\", \"dpx\", \"avi\", \"dv\", \"gif\", \"flv\", \"mkv\", \"mov\", \"mpg\", \"mpeg\",\n        \"mp4\", \"m4v\", \"mxf\", \"iff\", \"z\", \"ifl\", \"jpeg\", \"jpg\", \"jfif\", \"lut\",\n        \"1dl\", \"exr\", \"pic\", \"png\", \"ppm\", \"pnm\", \"pgm\", \"pbm\", \"rla\", \"rpf\",\n        \"sgi\", \"rgba\", \"rgb\", \"bw\", \"tga\", \"tiff\", \"tif\", \"img\", \"h264\",\n    }\n\n    label = \"Open in DJV\"\n    order = -10\n    icon = \"play-circle\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data):\n        import clique\n\n        path = self.filepath_from_context(context)\n        directory = os.path.dirname(path)\n\n        pattern = clique.PATTERNS[\"frames\"]\n        files = os.listdir(directory)\n        collections, remainder = clique.assemble(\n            files,\n            patterns=[pattern],\n            minimum_items=1\n        )\n\n        if not remainder:\n            sequence = collections[0]\n            first_image = list(sequence)[0]\n        else:\n            first_image = path\n        filepath = os.path.normpath(os.path.join(directory, first_image))\n\n        self.log.info(\"Opening : {}\".format(filepath))\n\n        last_djv_version = sorted(self.djv_list)[-1]\n\n        app_manager = ApplicationManager()\n        djv = app_manager.applications.get(last_djv_version)\n        djv.arguments.append(filepath)\n\n        app_manager.launch(last_djv_version)\n"
  },
  {
    "path": "openpype/plugins/load/open_file.py",
    "content": "import sys\nimport os\nimport subprocess\n\nfrom openpype.pipeline import load\n\n\ndef open(filepath):\n    \"\"\"Open file with system default executable\"\"\"\n    if sys.platform.startswith('darwin'):\n        subprocess.call(('open', filepath))\n    elif os.name == 'nt':\n        os.startfile(filepath)\n    elif os.name == 'posix':\n        subprocess.call(('xdg-open', filepath))\n\n\nclass OpenFile(load.LoaderPlugin):\n    \"\"\"Open Image Sequence or Video with system default\"\"\"\n\n    families = [\"render2d\"]\n    representations = [\"*\"]\n\n    label = \"Open\"\n    order = -10\n    icon = \"play-circle\"\n    color = \"orange\"\n\n    def load(self, context, name, namespace, data):\n\n        path = self.filepath_from_context(context)\n        if not os.path.exists(path):\n            raise RuntimeError(\"File not found: {}\".format(path))\n\n        self.log.info(\"Opening : {}\".format(path))\n        open(path)\n"
  },
  {
    "path": "openpype/plugins/load/push_to_library.py",
    "content": "import os\n\nfrom openpype import PACKAGE_DIR, AYON_SERVER_ENABLED\nfrom openpype.lib import get_openpype_execute_args, run_detached_process\nfrom openpype.pipeline import load\nfrom openpype.pipeline.load import LoadError\n\n\nclass PushToLibraryProject(load.SubsetLoaderPlugin):\n    \"\"\"Export selected versions to folder structure from Template\"\"\"\n\n    is_multiple_contexts_compatible = True\n\n    representations = [\"*\"]\n    families = [\"*\"]\n\n    label = \"Push to Library project\"\n    order = 35\n    icon = \"send\"\n    color = \"#d8d8d8\"\n\n    def load(self, contexts, name=None, namespace=None, options=None):\n        filtered_contexts = [\n            context\n            for context in contexts\n            if context.get(\"project\") and context.get(\"version\")\n        ]\n        if not filtered_contexts:\n            raise LoadError(\"Nothing to push for your selection\")\n\n        if len(filtered_contexts) > 1:\n            raise LoadError(\"Please select only one item\")\n\n        context = tuple(filtered_contexts)[0]\n\n        if AYON_SERVER_ENABLED:\n            push_tool_script_path = os.path.join(\n                PACKAGE_DIR,\n                \"tools\",\n                \"ayon_push_to_project\",\n                \"main.py\"\n            )\n        else:\n            push_tool_script_path = os.path.join(\n                PACKAGE_DIR,\n                \"tools\",\n                \"push_to_project\",\n                \"app.py\"\n            )\n\n        project_doc = context[\"project\"]\n        version_doc = context[\"version\"]\n        project_name = project_doc[\"name\"]\n        version_id = str(version_doc[\"_id\"])\n\n        args = get_openpype_execute_args(\n            \"run\",\n            push_tool_script_path,\n            \"--project\", project_name,\n            \"--version\", version_id\n        )\n        run_detached_process(args)\n"
  },
  {
    "path": "openpype/plugins/publish/cleanup.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Cleanup leftover files from publish.\"\"\"\nimport os\nimport shutil\nimport pyblish.api\nimport re\n\nfrom openpype.tests.lib import is_in_tests\n\n\nclass CleanUp(pyblish.api.InstancePlugin):\n    \"\"\"Cleans up the staging directory after a successful publish.\n\n    This will also clean published renders and delete their parent directories.\n\n    \"\"\"\n\n    order = pyblish.api.IntegratorOrder + 10\n    label = \"Clean Up\"\n    hosts = [\n        \"aftereffects\",\n        \"blender\",\n        \"celaction\",\n        \"flame\",\n        \"fusion\",\n        \"harmony\",\n        \"hiero\",\n        \"houdini\",\n        \"maya\",\n        \"nuke\",\n        \"photoshop\",\n        \"resolve\",\n        \"tvpaint\",\n        \"unreal\",\n        \"standalonepublisher\",\n        \"webpublisher\",\n        \"shell\"\n    ]\n    exclude_families = [\"clip\"]\n    optional = True\n    active = True\n\n    # Presets\n    paterns = None  # list of regex paterns\n    remove_temp_renders = True\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        if is_in_tests():\n            # let automatic test process clean up temporary data\n            return\n        # Get the errored instances\n        failed = []\n        for result in instance.context.data[\"results\"]:\n            if (result[\"error\"] is not None and result[\"instance\"] is not None\n               and result[\"instance\"] not in failed):\n                failed.append(result[\"instance\"])\n        assert instance not in failed, (\n            \"Result of '{}' instance were not success\".format(\n                instance.data[\"name\"]\n            )\n        )\n\n        _skip_cleanup_filepaths = instance.context.data.get(\n            \"skipCleanupFilepaths\"\n        ) or []\n        skip_cleanup_filepaths = set()\n        for path in _skip_cleanup_filepaths:\n            skip_cleanup_filepaths.add(os.path.normpath(path))\n\n        if self.remove_temp_renders:\n            self.log.debug(\"Cleaning renders new...\")\n            self.clean_renders(instance, skip_cleanup_filepaths)\n\n        if [ef for ef in self.exclude_families\n                if instance.data[\"family\"] in ef]:\n            return\n        import tempfile\n\n        temp_root = tempfile.gettempdir()\n        staging_dir = instance.data.get(\"stagingDir\", None)\n\n        if not staging_dir:\n            self.log.debug(\"Skipping cleanup. Staging dir not set \"\n                           \"on instance: {}.\".format(instance))\n            return\n\n        if not os.path.normpath(staging_dir).startswith(temp_root):\n            self.log.debug(\"Skipping cleanup. Staging directory is not in the \"\n                           \"temp folder: %s\" % staging_dir)\n            return\n\n        if not os.path.exists(staging_dir):\n            self.log.debug(\"No staging directory found at: %s\" % staging_dir)\n            return\n\n        if instance.data.get(\"stagingDir_persistent\"):\n            self.log.debug(\n                \"Staging dir {} should be persistent\".format(staging_dir)\n            )\n            return\n\n        self.log.debug(\"Removing staging directory {}\".format(staging_dir))\n        shutil.rmtree(staging_dir)\n\n    def clean_renders(self, instance, skip_cleanup_filepaths):\n        transfers = instance.data.get(\"transfers\", list())\n\n        current_families = instance.data.get(\"families\", list())\n        instance_family = instance.data.get(\"family\", None)\n        dirnames = []\n        transfers_dirs = []\n\n        for src, dest in transfers:\n            # fix path inconsistency\n            src = os.path.normpath(src)\n            dest = os.path.normpath(dest)\n\n            # add src dir into clearing dir paths (regex paterns)\n            transfers_dirs.append(os.path.dirname(src))\n\n            # add dest dir into clearing dir paths (regex paterns)\n            transfers_dirs.append(os.path.dirname(dest))\n\n            if src in skip_cleanup_filepaths:\n                self.log.debug((\n                    \"Source file is marked to be skipped in cleanup. {}\"\n                ).format(src))\n                continue\n\n            if os.path.normpath(src) != os.path.normpath(dest):\n                if instance_family == 'render' or 'render' in current_families:\n                    self.log.info(\"Removing src: `{}`...\".format(src))\n                    try:\n                        os.remove(src)\n                    except PermissionError:\n                        self.log.warning(\n                            \"Insufficient permission to delete {}\".format(src)\n                        )\n                        continue\n\n                    # add dir for cleanup\n                    dirnames.append(os.path.dirname(src))\n\n        # clean by regex paterns\n        # make unique set\n        transfers_dirs = set(transfers_dirs)\n\n        self.log.debug(\"__ transfers_dirs: `{}`\".format(transfers_dirs))\n        self.log.debug(\"__ self.paterns: `{}`\".format(self.paterns))\n        if self.paterns:\n            files = list()\n            # get list of all available content of dirs\n            for _dir in transfers_dirs:\n                if not os.path.exists(_dir):\n                    continue\n                files.extend([\n                    os.path.join(_dir, f)\n                    for f in os.listdir(_dir)])\n\n            self.log.debug(\"__ files: `{}`\".format(files))\n\n            # remove all files which match regex patern\n            for f in files:\n                if os.path.normpath(f) in skip_cleanup_filepaths:\n                    continue\n\n                for p in self.paterns:\n                    patern = re.compile(p)\n                    if not patern.findall(f):\n                        continue\n                    if not os.path.exists(f):\n                        continue\n\n                    self.log.info(\"Removing file by regex: `{}`\".format(f))\n                    os.remove(f)\n\n                    # add dir for cleanup\n                    dirnames.append(os.path.dirname(f))\n\n        # make unique set\n        cleanup_dirs = set(dirnames)\n\n        # clean dirs which are empty\n        for dir in cleanup_dirs:\n            try:\n                os.rmdir(dir)\n            except OSError:\n                # directory is not empty, skipping\n                continue\n"
  },
  {
    "path": "openpype/plugins/publish/cleanup_explicit.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Cleanup files when publishing is done.\"\"\"\nimport os\nimport shutil\nimport pyblish.api\n\n\nclass ExplicitCleanUp(pyblish.api.ContextPlugin):\n    \"\"\"Cleans up the files and folder defined to be deleted.\n\n    plugin is looking for 2 keys into context data:\n    - `cleanupFullPaths` - full paths that should be removed not matter if\n        is path to file or to directory\n    - `cleanupEmptyDirs` - full paths to directories that should be removed\n        only if do not contain any file in it but will be removed if contain\n        sub-folders\n    \"\"\"\n\n    order = pyblish.api.IntegratorOrder + 10\n    label = \"Explicit Clean Up\"\n    optional = True\n    active = True\n\n    def process(self, context):\n        cleanup_full_paths = context.data.get(\"cleanupFullPaths\")\n        cleanup_empty_dirs = context.data.get(\"cleanupEmptyDirs\")\n\n        self._remove_full_paths(cleanup_full_paths)\n        self._remove_empty_dirs(cleanup_empty_dirs)\n\n    def _remove_full_paths(self, full_paths):\n        \"\"\"Remove files and folders from disc.\n\n        Folders are removed with whole content.\n        \"\"\"\n        if not full_paths:\n            self.log.debug(\"No full paths to cleanup were collected.\")\n            return\n\n        # Separate paths into files and directories\n        filepaths = set()\n        dirpaths = set()\n        for path in full_paths:\n            # Skip empty items\n            if not path:\n                continue\n            # Normalize path\n            normalized = os.path.normpath(path)\n            # Check if path exists\n            if not os.path.exists(normalized):\n                continue\n\n            if os.path.isfile(normalized):\n                filepaths.add(normalized)\n            else:\n                dirpaths.add(normalized)\n\n        # Store failed paths with exception\n        failed = []\n        # Store removed filepaths for logging\n        succeeded_files = set()\n        # Remove file by file\n        for filepath in filepaths:\n            try:\n                os.remove(filepath)\n                succeeded_files.add(filepath)\n            except Exception as exc:\n                failed.append((filepath, exc))\n\n        if succeeded_files:\n            self.log.info(\n                \"Removed files:\\n{}\".format(\"\\n\".join(sorted(succeeded_files)))\n            )\n\n        # Delete folders with its content\n        succeeded = set()\n        for dirpath in dirpaths:\n            # Check if directory still exists\n            #   - it is possible that directory was already deleted with\n            #       different dirpath to delete\n            if os.path.exists(dirpath):\n                try:\n                    shutil.rmtree(dirpath)\n                    succeeded.add(dirpath)\n                except Exception:\n                    failed.append(dirpath)\n\n        if succeeded:\n            self.log.info(\n                \"Removed directories:\\n{}\".format(\n                    \"\\n\".join(sorted(succeeded))\n                )\n            )\n\n        # Prepare lines for report of failed removals\n        lines = []\n        for filepath, exc in failed:\n            lines.append(\"{}: {}\".format(filepath, str(exc)))\n\n        if lines:\n            self.log.warning(\n                \"Failed to remove filepaths:\\n{}\".format(\n                    \"\\n\".join(sorted(lines))\n                )\n            )\n\n    def _remove_empty_dirs(self, empty_dirpaths):\n        \"\"\"Remove directories if do not contain any files.\"\"\"\n        if not empty_dirpaths:\n            self.log.debug(\"No empty dirs to cleanup were collected.\")\n            return\n\n        # First filtering of directories and making sure those are\n        #   existing directories\n        filtered_dirpaths = set()\n        for path in empty_dirpaths:\n            if (\n                path\n                and os.path.exists(path)\n                and os.path.isdir(path)\n            ):\n                filtered_dirpaths.add(os.path.normpath(path))\n\n        to_delete_dirpaths = set()\n        to_skip_dirpaths = set()\n        # Check if contain any files (or it's subfolders contain files)\n        for dirpath in filtered_dirpaths:\n            valid = True\n            for _, _, filenames in os.walk(dirpath):\n                if filenames:\n                    valid = False\n                    break\n\n            if valid:\n                to_delete_dirpaths.add(dirpath)\n            else:\n                to_skip_dirpaths.add(dirpath)\n\n        if to_skip_dirpaths:\n            self.log.debug(\n                \"Skipped directories because they contain files:\\n{}\".format(\n                    \"\\n\".join(sorted(to_skip_dirpaths))\n                )\n            )\n\n        # Remove empty directies\n        for dirpath in to_delete_dirpaths:\n            if os.path.exists(dirpath):\n                shutil.rmtree(dirpath)\n\n        if to_delete_dirpaths:\n            self.log.debug(\n                \"Deleted empty directories:\\n{}\".format(\n                    \"\\n\".join(sorted(to_delete_dirpaths))\n                )\n            )\n"
  },
  {
    "path": "openpype/plugins/publish/cleanup_farm.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Cleanup leftover files from publish.\"\"\"\nimport os\nimport shutil\nimport pyblish.api\n\n\nclass CleanUpFarm(pyblish.api.ContextPlugin):\n    \"\"\"Cleans up the staging directory after a successful publish.\n\n    This will also clean published renders and delete their parent directories.\n    \"\"\"\n\n    order = pyblish.api.IntegratorOrder + 11\n    label = \"Clean Up Farm\"\n    enabled = True\n\n    # Keep \"filesequence\" for backwards compatibility of older jobs\n    targets = [\"filesequence\", \"farm\"]\n    allowed_hosts = (\"maya\", )\n\n    def process(self, context):\n        # Get source host from which farm publishing was started\n        src_host_name = context.data[\"hostName\"]\n        self.log.debug(\"Host name from context is {}\".format(src_host_name))\n        # Skip process if is not in list of source hosts in which this\n        #    plugin should run\n        if src_host_name not in self.allowed_hosts:\n            self.log.debug(\n                \"Source host \\\"{}\\\" is not in list of enabled hosts {}.\"\n                \" Skipping\".format(src_host_name, self.allowed_hosts)\n            )\n            return\n\n        self.log.debug(\"Preparing filepaths to remove\")\n        # Collect directories to remove\n        dirpaths_to_remove = set()\n        for instance in context:\n            staging_dir = instance.data.get(\"stagingDir\")\n            if staging_dir and not instance.data.get(\"stagingDir_persistent\"):\n                dirpaths_to_remove.add(os.path.normpath(staging_dir))\n\n            if \"representations\" in instance.data:\n                for repre in instance.data[\"representations\"]:\n                    staging_dir = repre.get(\"stagingDir\")\n                    if staging_dir:\n                        dirpaths_to_remove.add(os.path.normpath(staging_dir))\n\n        if not dirpaths_to_remove:\n            self.log.debug(\"Nothing to remove. Skipping\")\n            return\n\n        self.log.debug(\"Filepaths to remove are:\\n{}\".format(\n            \"\\n\".join([\"- {}\".format(path) for path in dirpaths_to_remove])\n        ))\n\n        # clean dirs which are empty\n        for dirpath in dirpaths_to_remove:\n            if not os.path.exists(dirpath):\n                self.log.debug(\"Skipping not existing directory \\\"{}\\\"\".format(\n                    dirpath\n                ))\n                continue\n\n            self.log.debug(\"Removing directory \\\"{}\\\"\".format(dirpath))\n            try:\n                shutil.rmtree(dirpath)\n            except OSError:\n                self.log.warning(\n                    \"Failed to remove directory \\\"{}\\\"\".format(dirpath),\n                    exc_info=True\n                )\n"
  },
  {
    "path": "openpype/plugins/publish/collect_anatomy_context_data.py",
    "content": "\"\"\"Collect global context Anatomy data.\n\nRequires:\n    context -> anatomy\n    context -> projectEntity\n    context -> assetEntity\n    context -> username\n    context -> datetimeData\n    session -> AVALON_TASK\n\nProvides:\n    context -> anatomyData\n\"\"\"\n\nimport json\nimport pyblish.api\n\nfrom openpype.pipeline.template_data import get_template_data\n\n\nclass CollectAnatomyContextData(pyblish.api.ContextPlugin):\n    \"\"\"Collect Anatomy Context data.\n\n    Example:\n    context.data[\"anatomyData\"] = {\n        \"project\": {\n            \"name\": \"MyProject\",\n            \"code\": \"myproj\"\n        },\n        \"asset\": \"AssetName\",\n        \"hierarchy\": \"path/to/asset\",\n        \"task\": \"Working\",\n        \"user\": \"MeDespicable\",\n        # Duplicated entry\n        \"username\": \"MeDespicable\",\n\n        # Current host name\n        \"app\": \"maya\"\n\n        *** OPTIONAL ***\n        + mutliple keys from `datetimeData` (See it's collector)\n    }\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.002\n    label = \"Collect Anatomy Context Data\"\n\n    def process(self, context):\n        host_name = context.data[\"hostName\"]\n        system_settings = context.data[\"system_settings\"]\n        project_entity = context.data[\"projectEntity\"]\n        asset_entity = context.data.get(\"assetEntity\")\n        task_name = None\n        if asset_entity:\n            task_name = context.data[\"task\"]\n\n        anatomy_data = get_template_data(\n            project_entity, asset_entity, task_name, host_name, system_settings\n        )\n        anatomy_data.update(context.data.get(\"datetimeData\") or {})\n\n        username = context.data[\"user\"]\n        anatomy_data[\"user\"] = username\n        # Backwards compatibility for 'username' key\n        anatomy_data[\"username\"] = username\n\n        # Store\n        context.data[\"anatomyData\"] = anatomy_data\n\n        self.log.debug(\"Global Anatomy Context Data collected:\\n{}\".format(\n            json.dumps(anatomy_data, indent=4)\n        ))\n"
  },
  {
    "path": "openpype/plugins/publish/collect_anatomy_instance_data.py",
    "content": "\"\"\"\nRequires:\n    context     -> anatomyData\n    context     -> projectEntity\n    context     -> assetEntity\n    instance    -> asset\n    instance    -> subset\n    instance    -> family\n\nOptional:\n    instance    -> version\n    instance    -> resolutionWidth\n    instance    -> resolutionHeight\n    instance    -> fps\n\nProvides:\n    instance    -> projectEntity\n    instance    -> assetEntity\n    instance    -> anatomyData\n    instance    -> version\n    instance    -> latestVersion\n\"\"\"\n\nimport copy\nimport json\nimport collections\n\nimport pyblish.api\n\nfrom openpype.client import (\n    get_assets,\n    get_subsets,\n    get_last_versions,\n    get_asset_name_identifier,\n)\nfrom openpype.pipeline.version_start import get_versioning_start\n\n\nclass CollectAnatomyInstanceData(pyblish.api.ContextPlugin):\n    \"\"\"Collect Instance specific Anatomy data.\n\n    Plugin is running for all instances on context even not active instances.\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.49\n    label = \"Collect Anatomy Instance data\"\n\n    follow_workfile_version = False\n\n    def process(self, context):\n        self.log.debug(\"Collecting anatomy data for all instances.\")\n\n        project_name = context.data[\"projectName\"]\n        self.fill_missing_asset_docs(context, project_name)\n        self.fill_latest_versions(context, project_name)\n        self.fill_anatomy_data(context)\n\n        self.log.debug(\"Anatomy Data collection finished.\")\n\n    def fill_missing_asset_docs(self, context, project_name):\n        self.log.debug(\"Querying asset documents for instances.\")\n\n        context_asset_doc = context.data.get(\"assetEntity\")\n        context_asset_name = None\n        if context_asset_doc:\n            context_asset_name = get_asset_name_identifier(context_asset_doc)\n\n        instances_with_missing_asset_doc = collections.defaultdict(list)\n        for instance in context:\n            instance_asset_doc = instance.data.get(\"assetEntity\")\n            _asset_name = instance.data[\"asset\"]\n\n            # There is possibility that assetEntity on instance is already set\n            # which can happen in standalone publisher\n            if instance_asset_doc:\n                instance_asset_name = get_asset_name_identifier(\n                    instance_asset_doc)\n                if instance_asset_name == _asset_name:\n                    continue\n\n            # Check if asset name is the same as what is in context\n            # - they may be different, e.g. in NukeStudio\n            if context_asset_name and context_asset_name == _asset_name:\n                instance.data[\"assetEntity\"] = context_asset_doc\n\n            else:\n                instances_with_missing_asset_doc[_asset_name].append(instance)\n\n        if not instances_with_missing_asset_doc:\n            self.log.debug(\"All instances already had right asset document.\")\n            return\n\n        asset_names = list(instances_with_missing_asset_doc.keys())\n        self.log.debug(\"Querying asset documents with names: {}\".format(\n            \", \".join([\"\\\"{}\\\"\".format(name) for name in asset_names])\n        ))\n\n        asset_docs = get_assets(project_name, asset_names=asset_names)\n        asset_docs_by_name = {\n            get_asset_name_identifier(asset_doc): asset_doc\n            for asset_doc in asset_docs\n        }\n\n        not_found_asset_names = []\n        for asset_name, instances in instances_with_missing_asset_doc.items():\n            asset_doc = asset_docs_by_name.get(asset_name)\n            if not asset_doc:\n                not_found_asset_names.append(asset_name)\n                continue\n\n            for _instance in instances:\n                _instance.data[\"assetEntity\"] = asset_doc\n\n        if not_found_asset_names:\n            joined_asset_names = \", \".join(\n                [\"\\\"{}\\\"\".format(name) for name in not_found_asset_names]\n            )\n            self.log.warning((\n                \"Not found asset documents with names \\\"{}\\\".\"\n            ).format(joined_asset_names))\n\n    def fill_latest_versions(self, context, project_name):\n        \"\"\"Try to find latest version for each instance's subset.\n\n        Key \"latestVersion\" is always set to latest version or `None`.\n\n        Args:\n            context (pyblish.Context)\n\n        Returns:\n            None\n\n        \"\"\"\n        self.log.debug(\"Querying latest versions for instances.\")\n\n        hierarchy = {}\n        names_by_asset_ids = collections.defaultdict(set)\n        for instance in context:\n            # Make sure `\"latestVersion\"` key is set\n            latest_version = instance.data.get(\"latestVersion\")\n            instance.data[\"latestVersion\"] = latest_version\n\n            # Skip instances without \"assetEntity\"\n            asset_doc = instance.data.get(\"assetEntity\")\n            if not asset_doc:\n                continue\n\n            # Store asset ids and subset names for queries\n            asset_id = asset_doc[\"_id\"]\n            subset_name = instance.data[\"subset\"]\n\n            # Prepare instance hierarchy for faster filling latest versions\n            if asset_id not in hierarchy:\n                hierarchy[asset_id] = {}\n            if subset_name not in hierarchy[asset_id]:\n                hierarchy[asset_id][subset_name] = []\n            hierarchy[asset_id][subset_name].append(instance)\n            names_by_asset_ids[asset_id].add(subset_name)\n\n        subset_docs = []\n        if names_by_asset_ids:\n            subset_docs = list(get_subsets(\n                project_name, names_by_asset_ids=names_by_asset_ids\n            ))\n\n        subset_ids = [\n            subset_doc[\"_id\"]\n            for subset_doc in subset_docs\n        ]\n\n        last_version_docs_by_subset_id = get_last_versions(\n            project_name, subset_ids, fields=[\"name\"]\n        )\n        for subset_doc in subset_docs:\n            subset_id = subset_doc[\"_id\"]\n            last_version_doc = last_version_docs_by_subset_id.get(subset_id)\n            if last_version_doc is None:\n                continue\n\n            asset_id = subset_doc[\"parent\"]\n            subset_name = subset_doc[\"name\"]\n            _instances = hierarchy[asset_id][subset_name]\n            for _instance in _instances:\n                _instance.data[\"latestVersion\"] = last_version_doc[\"name\"]\n\n    def fill_anatomy_data(self, context):\n        self.log.debug(\"Storing anatomy data to instance data.\")\n\n        project_doc = context.data[\"projectEntity\"]\n        project_task_types = project_doc[\"config\"][\"tasks\"]\n\n        for instance in context:\n            anatomy_data = copy.deepcopy(context.data[\"anatomyData\"])\n            anatomy_data.update({\n                \"family\": instance.data[\"family\"],\n                \"subset\": instance.data[\"subset\"],\n            })\n\n            self._fill_asset_data(instance, project_doc, anatomy_data)\n            self._fill_task_data(instance, project_task_types, anatomy_data)\n\n            # Define version\n            version_number = None\n            if self.follow_workfile_version:\n                version_number = context.data(\"version\")\n\n            # Even if 'follow_workfile_version' is enabled, it may not be set\n            #   because workfile version was not collected to 'context.data'\n            # - that can happen e.g. in 'traypublisher' or other hosts without\n            #   a workfile\n            if version_number is None:\n                version_number = instance.data.get(\"version\")\n\n            # use latest version (+1) if already any exist\n            if version_number is None:\n                latest_version = instance.data[\"latestVersion\"]\n                if latest_version is not None:\n                    version_number = int(latest_version) + 1\n\n            # If version is not specified for instance or context\n            if version_number is None:\n                task_data = anatomy_data.get(\"task\") or {}\n                task_name = task_data.get(\"name\")\n                task_type = task_data.get(\"type\")\n                version_number = get_versioning_start(\n                    context.data[\"projectName\"],\n                    instance.context.data[\"hostName\"],\n                    task_name=task_name,\n                    task_type=task_type,\n                    family=instance.data[\"family\"],\n                    subset=instance.data[\"subset\"]\n                )\n            anatomy_data[\"version\"] = version_number\n\n            # Additional data\n            resolution_width = instance.data.get(\"resolutionWidth\")\n            if resolution_width:\n                anatomy_data[\"resolution_width\"] = resolution_width\n\n            resolution_height = instance.data.get(\"resolutionHeight\")\n            if resolution_height:\n                anatomy_data[\"resolution_height\"] = resolution_height\n\n            pixel_aspect = instance.data.get(\"pixelAspect\")\n            if pixel_aspect:\n                anatomy_data[\"pixel_aspect\"] = float(\n                    \"{:0.2f}\".format(float(pixel_aspect))\n                )\n\n            fps = instance.data.get(\"fps\")\n            if fps:\n                anatomy_data[\"fps\"] = float(\"{:0.2f}\".format(float(fps)))\n\n            # Store anatomy data\n            instance.data[\"projectEntity\"] = project_doc\n            instance.data[\"anatomyData\"] = anatomy_data\n            instance.data[\"version\"] = version_number\n\n            # Log collected data\n            instance_name = instance.data[\"name\"]\n            instance_label = instance.data.get(\"label\")\n            if instance_label:\n                instance_name += \" ({})\".format(instance_label)\n            self.log.debug(\"Anatomy data for instance {}: {}\".format(\n                instance_name,\n                json.dumps(anatomy_data, indent=4)\n            ))\n\n    def _fill_asset_data(self, instance, project_doc, anatomy_data):\n        # QUESTION should we make sure that all asset data are poped if asset\n        #   data cannot be found?\n        # - 'asset', 'hierarchy', 'parent', 'folder'\n        asset_doc = instance.data.get(\"assetEntity\")\n        if asset_doc:\n            parents = asset_doc[\"data\"].get(\"parents\") or list()\n            parent_name = project_doc[\"name\"]\n            if parents:\n                parent_name = parents[-1]\n\n            hierarchy = \"/\".join(parents)\n            anatomy_data.update({\n                \"asset\": asset_doc[\"name\"],\n                \"hierarchy\": hierarchy,\n                \"parent\": parent_name,\n                \"folder\": {\n                    \"name\": asset_doc[\"name\"],\n                },\n            })\n            return\n\n        if instance.data.get(\"newAssetPublishing\"):\n            hierarchy = instance.data[\"hierarchy\"]\n            anatomy_data[\"hierarchy\"] = hierarchy\n\n            parent_name = project_doc[\"name\"]\n            if hierarchy:\n                parent_name = hierarchy.split(\"/\")[-1]\n\n            asset_name = instance.data[\"asset\"].split(\"/\")[-1]\n            anatomy_data.update({\n                \"asset\": asset_name,\n                \"hierarchy\": hierarchy,\n                \"parent\": parent_name,\n                \"folder\": {\n                    \"name\": asset_name,\n                },\n            })\n\n    def _fill_task_data(self, instance, project_task_types, anatomy_data):\n        # QUESTION should we make sure that all task data are poped if task\n        #   data cannot be resolved?\n        # - 'task'\n\n        # Skip if there is no task\n        task_name = instance.data.get(\"task\")\n        if not task_name:\n            return\n\n        # Find task data based on asset entity\n        asset_doc = instance.data.get(\"assetEntity\")\n        task_data = self._get_task_data_from_asset(\n            asset_doc, task_name, project_task_types\n        )\n        if task_data:\n            # Fill task data\n            # - if we're in editorial, make sure the task type is filled\n            if (\n                not instance.data.get(\"newAssetPublishing\")\n                or task_data[\"type\"]\n            ):\n                anatomy_data[\"task\"] = task_data\n                return\n\n        # New hierarchy is not created, so we can only skip rest of the logic\n        if not instance.data.get(\"newAssetPublishing\"):\n            return\n\n        # Try to find task data based on hierarchy context and asset name\n        hierarchy_context = instance.context.data.get(\"hierarchyContext\")\n        asset_name = instance.data.get(\"asset\")\n        if not hierarchy_context or not asset_name:\n            return\n\n        project_name = instance.context.data[\"projectName\"]\n        # OpenPype approach vs AYON approach\n        if \"/\" not in asset_name:\n            tasks_info = self._find_tasks_info_in_hierarchy(\n                hierarchy_context, asset_name\n            )\n        else:\n            current_data = hierarchy_context.get(project_name, {})\n            for key in asset_name.split(\"/\"):\n                if key:\n                    current_data = current_data.get(\"childs\", {}).get(key, {})\n            tasks_info = current_data.get(\"tasks\", {})\n\n        task_info = tasks_info.get(task_name, {})\n        task_type = task_info.get(\"type\")\n        task_code = (\n            project_task_types\n            .get(task_type, {})\n            .get(\"short_name\")\n        )\n        anatomy_data[\"task\"] = {\n            \"name\": task_name,\n            \"type\": task_type,\n            \"short\": task_code\n        }\n\n    def _get_task_data_from_asset(\n        self, asset_doc, task_name, project_task_types\n    ):\n        \"\"\"\n\n        Args:\n            asset_doc (Union[dict[str, Any], None]): Asset document.\n            task_name (Union[str, None]): Task name.\n            project_task_types (dict[str, dict[str, Any]]): Project task\n                types.\n\n        Returns:\n            Union[dict[str, str], None]: Task data or None if not found.\n        \"\"\"\n\n        if not asset_doc or not task_name:\n            return None\n\n        asset_tasks = asset_doc[\"data\"][\"tasks\"]\n        task_type = asset_tasks.get(task_name, {}).get(\"type\")\n        task_code = (\n            project_task_types\n            .get(task_type, {})\n            .get(\"short_name\")\n        )\n        return {\n            \"name\": task_name,\n            \"type\": task_type,\n            \"short\": task_code\n        }\n\n    def _find_tasks_info_in_hierarchy(self, hierarchy_context, asset_name):\n        \"\"\"Find tasks info for an asset in editorial hierarchy.\n\n        Args:\n            hierarchy_context (dict[str, Any]): Editorial hierarchy context.\n            asset_name (str): Asset name.\n\n        Returns:\n            dict[str, dict[str, Any]]: Tasks info by name.\n        \"\"\"\n\n        hierarchy_queue = collections.deque()\n        hierarchy_queue.append(copy.deepcopy(hierarchy_context))\n        while hierarchy_queue:\n            item = hierarchy_queue.popleft()\n            if asset_name in item:\n                return item[asset_name].get(\"tasks\") or {}\n\n            for subitem in item.values():\n                hierarchy_queue.extend(subitem.get(\"childs\") or [])\n        return {}\n"
  },
  {
    "path": "openpype/plugins/publish/collect_anatomy_object.py",
    "content": "\"\"\"Collect Anatomy object.\n\nRequires:\n    context -> projectName\n\nProvides:\n    context -> anatomy (openpype.pipeline.anatomy.Anatomy)\n\"\"\"\n\nimport pyblish.api\nfrom openpype.pipeline import Anatomy, KnownPublishError\n\n\nclass CollectAnatomyObject(pyblish.api.ContextPlugin):\n    \"\"\"Collect Anatomy object into Context.\n\n    Order offset could be changed to '-0.45'.\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.4\n    label = \"Collect Anatomy Object\"\n\n    def process(self, context):\n        project_name = context.data.get(\"projectName\")\n        if project_name is None:\n            raise KnownPublishError((\n                \"Project name is not set in 'projectName'.\"\n                \"Could not initialize project's Anatomy.\"\n            ))\n\n        context.data[\"anatomy\"] = Anatomy(project_name)\n\n        self.log.debug(\n            \"Anatomy object collected for project \\\"{}\\\".\".format(project_name)\n        )\n"
  },
  {
    "path": "openpype/plugins/publish/collect_audio.py",
    "content": "import collections\nimport pyblish.api\n\nfrom openpype.client import (\n    get_assets,\n    get_subsets,\n    get_last_versions,\n    get_representations,\n    get_asset_name_identifier,\n)\nfrom openpype.pipeline.load import get_representation_path_with_anatomy\n\n\nclass CollectAudio(pyblish.api.ContextPlugin):\n    \"\"\"Collect asset's last published audio.\n\n    The audio subset name searched for is defined in:\n        project settings > Collect Audio\n\n    Note:\n        The plugin was instance plugin but because of so much queries the\n            plugin was slowing down whole collection phase a lot thus was\n            converted to context plugin which requires only 4 queries top.\n    \"\"\"\n\n    label = \"Collect Asset Audio\"\n    order = pyblish.api.CollectorOrder + 0.1\n    families = [\"review\"]\n    hosts = [\n        \"nuke\",\n        \"maya\",\n        \"shell\",\n        \"hiero\",\n        \"premiere\",\n        \"harmony\",\n        \"traypublisher\",\n        \"standalonepublisher\",\n        \"fusion\",\n        \"tvpaint\",\n        \"resolve\",\n        \"webpublisher\",\n        \"aftereffects\",\n        \"flame\",\n        \"unreal\"\n    ]\n\n    audio_subset_name = \"audioMain\"\n\n    def process(self, context):\n        # Fake filtering by family inside context plugin\n        filtered_instances = []\n        for instance in pyblish.api.instances_by_plugin(\n            context, self.__class__\n        ):\n            # Skip instances that already have audio filled\n            if instance.data.get(\"audio\"):\n                self.log.debug(\n                    \"Skipping Audio collection. It is already collected\"\n                )\n                continue\n            filtered_instances.append(instance)\n\n        # Skip if none of instances remained\n        if not filtered_instances:\n            return\n\n        # Add audio to instance if exists.\n        instances_by_asset_name = collections.defaultdict(list)\n        for instance in filtered_instances:\n            asset_name = instance.data[\"asset\"]\n            instances_by_asset_name[asset_name].append(instance)\n\n        asset_names = set(instances_by_asset_name.keys())\n        self.log.debug((\n            \"Searching for audio subset '{subset}' in assets {assets}\"\n        ).format(\n            subset=self.audio_subset_name,\n            assets=\", \".join([\n                '\"{}\"'.format(asset_name)\n                for asset_name in asset_names\n            ])\n        ))\n\n        # Query all required documents\n        project_name = context.data[\"projectName\"]\n        anatomy = context.data[\"anatomy\"]\n        repre_docs_by_asset_names = self.query_representations(\n            project_name, asset_names)\n\n        for asset_name, instances in instances_by_asset_name.items():\n            repre_docs = repre_docs_by_asset_names[asset_name]\n            if not repre_docs:\n                continue\n\n            repre_doc = repre_docs[0]\n            repre_path = get_representation_path_with_anatomy(\n                repre_doc, anatomy\n            )\n            for instance in instances:\n                instance.data[\"audio\"] = [{\n                    \"offset\": 0,\n                    \"filename\": repre_path\n                }]\n                self.log.debug(\"Audio Data added to instance ...\")\n\n    def query_representations(self, project_name, asset_names):\n        \"\"\"Query representations related to audio subsets for passed assets.\n\n        Args:\n            project_name (str): Project in which we're looking for all\n                entities.\n            asset_names (Iterable[str]): Asset names where to look for audio\n                subsets and their representations.\n\n        Returns:\n            collections.defaultdict[str, List[Dict[Str, Any]]]: Representations\n                related to audio subsets by asset name.\n        \"\"\"\n\n        output = collections.defaultdict(list)\n        # Query asset documents\n        asset_docs = get_assets(\n            project_name,\n            asset_names=asset_names,\n            fields=[\"_id\", \"name\", \"data.parents\"]\n        )\n\n        asset_id_by_name = {\n            get_asset_name_identifier(asset_doc): asset_doc[\"_id\"]\n            for asset_doc in asset_docs\n        }\n        asset_ids = set(asset_id_by_name.values())\n\n        # Query subsets with name define by 'audio_subset_name' attr\n        # - one or none subsets with the name should be available on an asset\n        subset_docs = get_subsets(\n            project_name,\n            subset_names=[self.audio_subset_name],\n            asset_ids=asset_ids,\n            fields=[\"_id\", \"parent\"]\n        )\n        subset_id_by_asset_id = {}\n        for subset_doc in subset_docs:\n            asset_id = subset_doc[\"parent\"]\n            subset_id_by_asset_id[asset_id] = subset_doc[\"_id\"]\n\n        subset_ids = set(subset_id_by_asset_id.values())\n        if not subset_ids:\n            return output\n\n        # Find all latest versions for the subsets\n        version_docs_by_subset_id = get_last_versions(\n            project_name, subset_ids=subset_ids, fields=[\"_id\", \"parent\"]\n        )\n        version_id_by_subset_id = {\n            subset_id: version_doc[\"_id\"]\n            for subset_id, version_doc in version_docs_by_subset_id.items()\n        }\n        version_ids = set(version_id_by_subset_id.values())\n        if not version_ids:\n            return output\n\n        # Find representations under latest versions of audio subsets\n        repre_docs = get_representations(\n            project_name, version_ids=version_ids\n        )\n        repre_docs_by_version_id = collections.defaultdict(list)\n        for repre_doc in repre_docs:\n            version_id = repre_doc[\"parent\"]\n            repre_docs_by_version_id[version_id].append(repre_doc)\n\n        if not repre_docs_by_version_id:\n            return output\n\n        for asset_name in asset_names:\n            asset_id = asset_id_by_name.get(asset_name)\n            subset_id = subset_id_by_asset_id.get(asset_id)\n            version_id = version_id_by_subset_id.get(subset_id)\n            output[asset_name] = repre_docs_by_version_id[version_id]\n        return output\n"
  },
  {
    "path": "openpype/plugins/publish/collect_cleanup_keys.py",
    "content": "\"\"\"\nRequires:\n    None\nProvides:\n    context\n        - cleanupFullPaths (list)\n        - cleanupEmptyDirs (list)\n\"\"\"\n\nimport pyblish.api\n\n\nclass CollectCleanupKeys(pyblish.api.ContextPlugin):\n    \"\"\"Prepare keys for 'ExplicitCleanUp' plugin.\"\"\"\n\n    label = \"Collect Cleanup Keys\"\n    order = pyblish.api.CollectorOrder - 0.5\n\n    def process(self, context):\n        context.data[\"cleanupFullPaths\"] = []\n        context.data[\"cleanupEmptyDirs\"] = []\n"
  },
  {
    "path": "openpype/plugins/publish/collect_comment.py",
    "content": "\"\"\"Collect comment and add option to enter comment per instance.\n\nCombination of plugins. One define optional input for instances in Publisher\nUI (CollectInstanceCommentDef) and second cares that each instance during\ncollection has available \"comment\" key in data (CollectComment).\n\nPlugin 'CollectInstanceCommentDef' define \"comment\" attribute which won't be\nfilled with any value if instance does not match families filter or when\nplugin is disabled.\n\nPlugin 'CollectComment' makes sure that each instance in context has\navailable \"comment\" key in data which can be set to 'str' or 'None' if is not\nset.\n- In case instance already has filled comment the plugin's logic is skipped\n- The comment is always set and value should be always 'str' even if is empty\n\nWhy are separated:\n- 'CollectInstanceCommentDef' can have specific settings to show comment\n    attribute only to defined families in publisher UI\n- 'CollectComment' will run all the time\n\nTodos:\n    The comment per instance is not sent via farm.\n\"\"\"\n\nimport pyblish.api\nfrom openpype.lib.attribute_definitions import TextDef\nfrom openpype.pipeline.publish import OpenPypePyblishPluginMixin\n\n\nclass CollectInstanceCommentDef(\n    pyblish.api.InstancePlugin,\n    OpenPypePyblishPluginMixin\n):\n    label = \"Comment per instance\"\n    targets = [\"local\"]\n    # Disable plugin by default\n    families = []\n    enabled = False\n\n    def process(self, instance):\n        pass\n\n    @classmethod\n    def apply_settings(cls, project_setting, _):\n        plugin_settings = project_setting[\"global\"][\"publish\"].get(\n            \"collect_comment_per_instance\"\n        )\n        if not plugin_settings:\n            return\n\n        if plugin_settings.get(\"enabled\") is not None:\n            cls.enabled = plugin_settings[\"enabled\"]\n\n        if plugin_settings.get(\"families\") is not None:\n            cls.families = plugin_settings[\"families\"]\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            TextDef(\"comment\", label=\"Comment\")\n        ]\n\n\nclass CollectComment(\n    pyblish.api.ContextPlugin,\n    OpenPypePyblishPluginMixin\n):\n    \"\"\"Collect comment per each instance.\n\n    Plugin makes sure each instance to publish has set \"comment\" in data so any\n    further plugin can use it directly.\n    \"\"\"\n\n    label = \"Collect Instance Comment\"\n    # TODO change to CollectorOrder after Pyblish is purged\n    # Pyblish allows modifying comment after collect phase\n    order = pyblish.api.ExtractorOrder - 0.49\n\n    def process(self, context):\n        context_comment = self.cleanup_comment(context.data.get(\"comment\"))\n        # Set it back\n        context.data[\"comment\"] = context_comment\n        for instance in context:\n            instance_label = str(instance)\n            # Check if comment is already set\n            instance_comment = self.cleanup_comment(\n                instance.data.get(\"comment\"))\n\n            # If comment on instance is not set then look for attributes\n            if not instance_comment:\n                attr_values = self.get_attr_values_from_data_for_plugin(\n                    CollectInstanceCommentDef, instance.data\n                )\n                instance_comment = self.cleanup_comment(\n                    attr_values.get(\"comment\")\n                )\n\n            # Use context comment if instance has all options of comment\n            #   empty\n            if not instance_comment:\n                instance_comment = context_comment\n\n            instance.data[\"comment\"] = instance_comment\n            if instance_comment:\n                msg_end = \"has comment set to: \\\"{}\\\"\".format(\n                    instance_comment)\n            else:\n                msg_end = \"does not have set comment\"\n            self.log.debug(\"Instance {} {}\".format(instance_label, msg_end))\n\n    def cleanup_comment(self, comment):\n        \"\"\"Cleanup comment value.\n\n        Args:\n            comment (Union[str, None]): Comment value from data.\n\n        Returns:\n            str: Cleaned comment which is stripped or empty string if input\n                was 'None'.\n        \"\"\"\n\n        if comment:\n            return comment.strip()\n        return \"\"\n"
  },
  {
    "path": "openpype/plugins/publish/collect_context_entities.py",
    "content": "\"\"\"Collect Anatomy and global anatomy data.\n\nRequires:\n    session -> AVALON_ASSET\n    context -> projectName\n    context -> asset\n    context -> task\n\nProvides:\n    context -> projectEntity - Project document from database.\n    context -> assetEntity - Asset document from database only if 'asset' is\n        set in context.\n\"\"\"\n\nimport pyblish.api\n\nfrom openpype.client import get_project, get_asset_by_name\nfrom openpype.pipeline import KnownPublishError\n\n\nclass CollectContextEntities(pyblish.api.ContextPlugin):\n    \"\"\"Collect entities into Context.\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.1\n    label = \"Collect Context Entities\"\n\n    def process(self, context):\n        project_name = context.data[\"projectName\"]\n        asset_name = context.data[\"asset\"]\n        task_name = context.data[\"task\"]\n\n        project_entity = get_project(project_name)\n        if not project_entity:\n            raise KnownPublishError(\n                \"Project '{0}' was not found.\".format(project_name)\n            )\n        self.log.debug(\"Collected Project \\\"{}\\\"\".format(project_entity))\n\n        context.data[\"projectEntity\"] = project_entity\n\n        if not asset_name:\n            self.log.info(\"Context is not set. Can't collect global data.\")\n            return\n\n        asset_entity = get_asset_by_name(project_name, asset_name)\n        assert asset_entity, (\n            \"No asset found by the name '{0}' in project '{1}'\"\n        ).format(asset_name, project_name)\n\n        self.log.debug(\"Collected Asset \\\"{}\\\"\".format(asset_entity))\n\n        context.data[\"assetEntity\"] = asset_entity\n\n        data = asset_entity['data']\n\n        # Task type\n        asset_tasks = data.get(\"tasks\") or {}\n        task_info = asset_tasks.get(task_name) or {}\n        task_type = task_info.get(\"type\")\n        context.data[\"taskType\"] = task_type\n\n        frame_start = data.get(\"frameStart\")\n        if frame_start is None:\n            frame_start = 1\n            self.log.warning(\"Missing frame start. Defaulting to 1.\")\n\n        frame_end = data.get(\"frameEnd\")\n        if frame_end is None:\n            frame_end = 2\n            self.log.warning(\"Missing frame end. Defaulting to 2.\")\n\n        context.data[\"frameStart\"] = frame_start\n        context.data[\"frameEnd\"] = frame_end\n\n        handle_start = data.get(\"handleStart\") or 0\n        handle_end = data.get(\"handleEnd\") or 0\n\n        context.data[\"handleStart\"] = int(handle_start)\n        context.data[\"handleEnd\"] = int(handle_end)\n\n        frame_start_h = frame_start - context.data[\"handleStart\"]\n        frame_end_h = frame_end + context.data[\"handleEnd\"]\n        context.data[\"frameStartHandle\"] = frame_start_h\n        context.data[\"frameEndHandle\"] = frame_end_h\n\n        context.data[\"fps\"] = data[\"fps\"]\n"
  },
  {
    "path": "openpype/plugins/publish/collect_context_label.py",
    "content": "\"\"\"\nOptional:\n    context     -> hostName (str)\n    context     -> currentFile (str)\nProvides:\n    context     -> label (str)\n\"\"\"\n\nimport os\nimport pyblish.api\n\n\nclass CollectContextLabel(pyblish.api.ContextPlugin):\n    \"\"\"Labelize context using the registered host and current file\"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.25\n    label = \"Context Label\"\n\n    def process(self, context):\n        # Add ability to use custom context label\n        label = context.data.get(\"label\")\n        if label:\n            self.log.debug(\"Context label is already set to \\\"{}\\\"\".format(\n                label\n            ))\n            return\n\n        host_name = context.data.get(\"hostName\")\n        if not host_name:\n            host_name = pyblish.api.registered_hosts()[-1]\n        # Use host name as base for label\n        label = host_name.title()\n\n        # Get scene name from \"currentFile\" and use basename as ending of label\n        path = context.data.get(\"currentFile\")\n        if path:\n            label += \" - {}\".format(os.path.basename(path))\n\n        # Set label\n        context.data[\"label\"] = label\n        self.log.debug(\"Context label is changed to \\\"{}\\\"\".format(\n            label\n        ))\n"
  },
  {
    "path": "openpype/plugins/publish/collect_current_context.py",
    "content": "\"\"\"\nProvides:\n    context -> projectName (str)\n    context -> asset (str)\n    context -> task (str)\n\"\"\"\n\nimport pyblish.api\nfrom openpype.pipeline import get_current_context\n\n\nclass CollectCurrentContext(pyblish.api.ContextPlugin):\n    \"\"\"Collect project context into publish context data.\n\n    Plugin does not override any value if is already set.\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.5\n    label = \"Collect Current context\"\n\n    def process(self, context):\n        # Check if values are already set\n        project_name = context.data.get(\"projectName\")\n        asset_name = context.data.get(\"asset\")\n        task_name = context.data.get(\"task\")\n\n        current_context = get_current_context()\n        if not project_name:\n            context.data[\"projectName\"] = current_context[\"project_name\"]\n\n        if not asset_name:\n            context.data[\"asset\"] = current_context[\"asset_name\"]\n\n        if not task_name:\n            context.data[\"task\"] = current_context[\"task_name\"]\n\n        # QUESTION should we be explicit with keys? (the same on instances)\n        #   - 'asset' -> 'assetName'\n        #   - 'task' -> 'taskName'\n\n        self.log.info((\n            \"Collected project context\\n\"\n            \"Project: {project_name}\\n\"\n            \"Asset: {asset_name}\\n\"\n            \"Task: {task_name}\"\n        ).format(\n            project_name=context.data[\"projectName\"],\n            asset_name=context.data[\"asset\"],\n            task_name=context.data[\"task\"]\n        ))\n"
  },
  {
    "path": "openpype/plugins/publish/collect_current_pype_user.py",
    "content": "import pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.lib import get_openpype_username\n\n\nclass CollectCurrentUserPype(pyblish.api.ContextPlugin):\n    \"\"\"Inject the currently logged on user into the Context\"\"\"\n\n    # Order must be after default pyblish-base CollectCurrentUser\n    order = pyblish.api.CollectorOrder + 0.001\n    label = (\n        \"Collect AYON User\"\n        if AYON_SERVER_ENABLED\n        else \"Collect OpenPype User\"\n    )\n\n    def process(self, context):\n        user = get_openpype_username()\n        context.data[\"user\"] = user\n        self.log.debug(\"Collected user \\\"{}\\\"\".format(user))\n"
  },
  {
    "path": "openpype/plugins/publish/collect_current_shell_file.py",
    "content": "\"\"\"\nRequires:\n    None\n\nProvides:\n    context         -> currentFile (str)\n\"\"\"\n\nimport os\nimport pyblish.api\n\n\nclass CollectCurrentShellFile(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current working file into context\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.5\n    label = \"Current File\"\n    hosts = [\"shell\"]\n\n    def process(self, context):\n        \"\"\"Inject the current working file\"\"\"\n        context.data[\"currentFile\"] = os.path.join(os.getcwd(), \"<shell>\")\n"
  },
  {
    "path": "openpype/plugins/publish/collect_custom_staging_dir.py",
    "content": "\"\"\"\nRequires:\n    anatomy\n\n\nProvides:\n    instance.data     -> stagingDir (folder path)\n                      -> stagingDir_persistent (bool)\n\"\"\"\nimport copy\nimport os.path\n\nimport pyblish.api\n\nfrom openpype.pipeline.publish.lib import get_custom_staging_dir_info\n\n\nclass CollectCustomStagingDir(pyblish.api.InstancePlugin):\n    \"\"\"Looks through profiles if stagingDir should be persistent and in special\n    location.\n\n    Transient staging dir could be useful in specific use cases where is\n    desirable to have temporary renders in specific, persistent folders, could\n    be on disks optimized for speed for example.\n\n    It is studio responsibility to clean up obsolete folders with data.\n\n    Location of the folder is configured in `project_anatomy/templates/others`.\n    ('transient' key is expected, with 'folder' key)\n\n    Which family/task type/subset is applicable is configured in:\n    `project_settings/global/tools/publish/custom_staging_dir_profiles`\n\n    \"\"\"\n    label = \"Collect Custom Staging Directory\"\n    order = pyblish.api.CollectorOrder + 0.4990\n\n    template_key = \"transient\"\n\n    def process(self, instance):\n        family = instance.data[\"family\"]\n        subset_name = instance.data[\"subset\"]\n        host_name = instance.context.data[\"hostName\"]\n        project_name = instance.context.data[\"projectName\"]\n        project_settings = instance.context.data[\"project_settings\"]\n        anatomy = instance.context.data[\"anatomy\"]\n        task = instance.data[\"anatomyData\"].get(\"task\", {})\n\n        transient_tml, is_persistent = get_custom_staging_dir_info(\n            project_name, host_name, family, task.get(\"name\"),\n            task.get(\"type\"), subset_name, project_settings=project_settings,\n            anatomy=anatomy, log=self.log)\n\n        if transient_tml:\n            anatomy_data = copy.deepcopy(instance.data[\"anatomyData\"])\n            anatomy_data[\"root\"] = anatomy.roots\n            scene_name = instance.context.data.get(\"currentFile\")\n            if scene_name:\n                anatomy_data[\"scene_name\"] = os.path.basename(scene_name)\n            transient_dir = transient_tml.format(**anatomy_data)\n            instance.data[\"stagingDir\"] = transient_dir\n\n            instance.data[\"stagingDir_persistent\"] = is_persistent\n            result_str = \"Adding '{}' as\".format(transient_dir)\n        else:\n            result_str = \"Not adding\"\n\n        self.log.debug(\"{} custom staging dir for instance with '{}'\".format(\n            result_str, family\n        ))\n"
  },
  {
    "path": "openpype/plugins/publish/collect_datetime_data.py",
    "content": "\"\"\"These data *must* be collected only once during publishing process.\n\nProvides:\n    context -> datetimeData\n\"\"\"\n\nimport pyblish.api\nfrom openpype.lib.dateutils import get_datetime_data\n\n\nclass CollectDateTimeData(pyblish.api.ContextPlugin):\n    order = pyblish.api.CollectorOrder - 0.5\n    label = \"Collect DateTime data\"\n\n    def process(self, context):\n        key = \"datetimeData\"\n        if key not in context.data:\n            context.data[key] = get_datetime_data()\n"
  },
  {
    "path": "openpype/plugins/publish/collect_farm_target.py",
    "content": "# -*- coding: utf-8 -*-\nimport pyblish.api\n\n\nclass CollectFarmTarget(pyblish.api.InstancePlugin):\n    \"\"\"Collects the render target for the instance\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder + 0.499\n    label = \"Collect Farm Target\"\n    targets = [\"local\"]\n\n    def process(self, instance):\n        if not instance.data.get(\"farm\"):\n            return\n\n        context = instance.context\n\n        farm_name = \"\"\n        op_modules = context.data.get(\"openPypeModules\")\n\n        for farm_renderer in [\"deadline\", \"royalrender\"]:\n            op_module = op_modules.get(farm_renderer, False)\n\n            if op_module and op_module.enabled:\n                farm_name = farm_renderer\n            elif not op_module:\n                self.log.error(\"Cannot get OpenPype {0} module.\".format(\n                    farm_renderer))\n\n        if farm_name:\n            self.log.debug(\"Collected render target: {0}\".format(farm_name))\n            instance.data[\"toBeRenderedOn\"] = farm_name\n        else:\n            AssertionError(\"No OpenPype renderer module found\")\n"
  },
  {
    "path": "openpype/plugins/publish/collect_frames_fix.py",
    "content": "import pyblish.api\nfrom openpype.lib.attribute_definitions import (\n    TextDef,\n    BoolDef\n)\n\nfrom openpype.pipeline.publish import OpenPypePyblishPluginMixin\nfrom openpype.client.entities import (\n    get_last_version_by_subset_name,\n    get_representations\n)\n\n\nclass CollectFramesFixDef(\n    pyblish.api.InstancePlugin,\n    OpenPypePyblishPluginMixin\n):\n    \"\"\"Provides text field to insert frame(s) to be rerendered.\n\n    Published files of last version of an instance subset are collected into\n    instance.data[\"last_version_published_files\"]. All these but frames\n    mentioned in text field will be reused for new version.\n    \"\"\"\n    order = pyblish.api.CollectorOrder + 0.495\n    label = \"Collect Frames to Fix\"\n    targets = [\"local\"]\n    hosts = [\"nuke\"]\n    families = [\"render\", \"prerender\"]\n\n    rewrite_version_enable = False\n\n    def process(self, instance):\n        attribute_values = self.get_attr_values_from_data(instance.data)\n        frames_to_fix = attribute_values.get(\"frames_to_fix\")\n\n        rewrite_version = attribute_values.get(\"rewrite_version\")\n\n        if not frames_to_fix:\n            return\n\n        instance.data[\"frames_to_fix\"] = frames_to_fix\n\n        subset_name = instance.data[\"subset\"]\n        asset_name = instance.data[\"asset\"]\n\n        project_entity = instance.data[\"projectEntity\"]\n        project_name = project_entity[\"name\"]\n\n        version = get_last_version_by_subset_name(\n            project_name,\n            subset_name,\n            asset_name=asset_name\n        )\n        if not version:\n            self.log.warning(\n                \"No last version found, re-render not possible\"\n            )\n            return\n\n        representations = get_representations(\n            project_name, version_ids=[version[\"_id\"]]\n        )\n        published_files = []\n        for repre in representations:\n            if repre[\"context\"][\"family\"] not in self.families:\n                continue\n\n            for file_info in repre.get(\"files\"):\n                published_files.append(file_info[\"path\"])\n\n        instance.data[\"last_version_published_files\"] = published_files\n        self.log.debug(\"last_version_published_files::{}\".format(\n            instance.data[\"last_version_published_files\"]))\n\n        if self.rewrite_version_enable and rewrite_version:\n            instance.data[\"version\"] = version[\"name\"]\n            # limits triggering version validator\n            instance.data.pop(\"latestVersion\")\n\n    @classmethod\n    def get_attribute_defs(cls):\n        attributes = [\n            TextDef(\"frames_to_fix\", label=\"Frames to fix\",\n                    placeholder=\"5,10-15\",\n                    regex=\"[0-9,-]+\")\n        ]\n\n        if cls.rewrite_version_enable:\n            attributes.append(\n                BoolDef(\n                    \"rewrite_version\",\n                    label=\"Rewrite latest version\",\n                    default=False\n                )\n            )\n\n        return attributes\n"
  },
  {
    "path": "openpype/plugins/publish/collect_from_create_context.py",
    "content": "\"\"\"Create instances based on CreateContext.\n\n\"\"\"\nimport os\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.host import IPublishHost\nfrom openpype.pipeline import legacy_io, registered_host\nfrom openpype.pipeline.create import CreateContext\n\n\nclass CollectFromCreateContext(pyblish.api.ContextPlugin):\n    \"\"\"Collect instances and data from CreateContext from new publishing.\"\"\"\n\n    label = \"Collect From Create Context\"\n    order = pyblish.api.CollectorOrder - 0.5\n\n    def process(self, context):\n        create_context = context.data.get(\"create_context\")\n        if not create_context:\n            host = registered_host()\n            if isinstance(host, IPublishHost):\n                create_context = CreateContext(host)\n\n        if not create_context:\n            return\n\n        thumbnail_paths_by_instance_id = (\n            create_context.thumbnail_paths_by_instance_id\n        )\n        context.data[\"thumbnailSource\"] = (\n            thumbnail_paths_by_instance_id.get(None)\n        )\n\n        project_name = create_context.get_current_project_name()\n        if project_name:\n            context.data[\"projectName\"] = project_name\n\n        for created_instance in create_context.instances:\n            instance_data = created_instance.data_to_store()\n            if AYON_SERVER_ENABLED:\n                instance_data[\"asset\"] = instance_data.pop(\"folderPath\")\n            if instance_data[\"active\"]:\n                thumbnail_path = thumbnail_paths_by_instance_id.get(\n                    created_instance.id\n                )\n                self.create_instance(\n                    context,\n                    instance_data,\n                    created_instance.transient_data,\n                    thumbnail_path\n                )\n\n        # Update global data to context\n        context.data.update(create_context.context_data_to_store())\n        context.data[\"newPublishing\"] = True\n        # Update context data\n        asset_name = create_context.get_current_asset_name()\n        task_name = create_context.get_current_task_name()\n        for key, value in (\n            (\"AVALON_PROJECT\", project_name),\n            (\"AVALON_ASSET\", asset_name),\n            (\"AVALON_TASK\", task_name)\n        ):\n            legacy_io.Session[key] = value\n            os.environ[key] = value\n\n    def create_instance(\n        self,\n        context,\n        in_data,\n        transient_data,\n        thumbnail_path\n    ):\n        subset = in_data[\"subset\"]\n        # If instance data already contain families then use it\n        instance_families = in_data.get(\"families\") or []\n\n        instance = context.create_instance(subset)\n        instance.data.update({\n            \"subset\": subset,\n            \"asset\": in_data[\"asset\"],\n            \"task\": in_data[\"task\"],\n            \"label\": in_data.get(\"label\") or subset,\n            \"name\": subset,\n            \"family\": in_data[\"family\"],\n            \"families\": instance_families,\n            \"representations\": [],\n            \"thumbnailSource\": thumbnail_path\n        })\n        for key, value in in_data.items():\n            if key not in instance.data:\n                instance.data[key] = value\n\n        instance.data[\"transientData\"] = transient_data\n\n        self.log.debug(\"collected instance: {}\".format(instance.data))\n        self.log.debug(\"parsing data: {}\".format(in_data))\n"
  },
  {
    "path": "openpype/plugins/publish/collect_hierarchy.py",
    "content": "import pyblish.api\n\n\nclass CollectHierarchy(pyblish.api.ContextPlugin):\n    \"\"\"Collecting hierarchy from `parents`.\n\n    present in `clip` family instances coming from the request json data file\n\n    It will add `hierarchical_context` into each instance for integrate\n    plugins to be able to create needed parents for the context if they\n    don't exist yet\n    \"\"\"\n\n    label = \"Collect Hierarchy\"\n    order = pyblish.api.CollectorOrder - 0.076\n    families = [\"shot\"]\n    hosts = [\"resolve\", \"hiero\", \"flame\"]\n\n    def process(self, context):\n        temp_context = {}\n        project_name = context.data[\"projectName\"]\n        final_context = {}\n        final_context[project_name] = {}\n        final_context[project_name]['entity_type'] = 'Project'\n\n        for instance in context:\n            self.log.debug(\"Processing instance: `{}` ...\".format(instance))\n\n            # shot data dict\n            shot_data = {}\n            family = instance.data[\"family\"]\n            families = instance.data[\"families\"]\n\n            # exclude other families then self.families with intersection\n            if not set(self.families).intersection(set(families + [family])):\n                continue\n\n            # exclude if not masterLayer True\n            if not instance.data.get(\"heroTrack\"):\n                continue\n\n            # get asset build data if any available\n            shot_data[\"inputs\"] = [\n                x[\"_id\"] for x in instance.data.get(\"assetbuilds\", [])\n            ]\n\n            # suppose that all instances are Shots\n            shot_data['entity_type'] = 'Shot'\n            shot_data['tasks'] = instance.data.get(\"tasks\") or {}\n            shot_data[\"comments\"] = instance.data.get(\"comments\", [])\n\n            shot_data['custom_attributes'] = {\n                \"handleStart\": instance.data[\"handleStart\"],\n                \"handleEnd\": instance.data[\"handleEnd\"],\n                \"frameStart\": instance.data[\"frameStart\"],\n                \"frameEnd\": instance.data[\"frameEnd\"],\n                \"clipIn\": instance.data[\"clipIn\"],\n                \"clipOut\": instance.data[\"clipOut\"],\n                \"fps\": instance.data[\"fps\"],\n                \"resolutionWidth\": instance.data[\"resolutionWidth\"],\n                \"resolutionHeight\": instance.data[\"resolutionHeight\"],\n                \"pixelAspect\": instance.data[\"pixelAspect\"]\n            }\n            # Split by '/' for AYON where asset is a path\n            name = instance.data[\"asset\"].split(\"/\")[-1]\n            actual = {name: shot_data}\n\n            for parent in reversed(instance.data[\"parents\"]):\n                next_dict = {}\n                parent_name = parent[\"entity_name\"]\n                next_dict[parent_name] = {}\n                next_dict[parent_name][\"entity_type\"] = parent[\n                    \"entity_type\"].capitalize()\n                next_dict[parent_name][\"childs\"] = actual\n                actual = next_dict\n\n            temp_context = self._update_dict(temp_context, actual)\n\n        # skip if nothing for hierarchy available\n        if not temp_context:\n            return\n\n        final_context[project_name]['childs'] = temp_context\n\n        # adding hierarchy context to context\n        context.data[\"hierarchyContext\"] = final_context\n        self.log.debug(\"context.data[hierarchyContext] is: {}\".format(\n            context.data[\"hierarchyContext\"]))\n\n    def _update_dict(self, parent_dict, child_dict):\n        \"\"\"\n        Nesting each children into its parent.\n\n        Args:\n            parent_dict (dict): parent dict wich should be nested with children\n            child_dict (dict): children dict which should be injested\n        \"\"\"\n\n        for key in parent_dict:\n            if key in child_dict and isinstance(parent_dict[key], dict):\n                child_dict[key] = self._update_dict(\n                    parent_dict[key], child_dict[key]\n                )\n            else:\n                if parent_dict.get(key) and child_dict.get(key):\n                    continue\n                else:\n                    child_dict[key] = parent_dict[key]\n\n        return child_dict\n"
  },
  {
    "path": "openpype/plugins/publish/collect_host_name.py",
    "content": "\"\"\"\nRequires:\n    None\nProvides:\n    context -> host (str)\n\"\"\"\nimport os\nimport pyblish.api\n\nfrom openpype.lib import ApplicationManager\n\n\nclass CollectHostName(pyblish.api.ContextPlugin):\n    \"\"\"Collect avalon host name to context.\"\"\"\n\n    label = \"Collect Host Name\"\n    order = pyblish.api.CollectorOrder - 0.5\n\n    def process(self, context):\n        host_name = context.data.get(\"hostName\")\n        app_name = context.data.get(\"appName\")\n        app_label = context.data.get(\"appLabel\")\n        # Don't override value if is already set\n        if host_name and app_name and app_label:\n            return\n\n        # Use AVALON_APP to get host name if available\n        if not host_name:\n            host_name = os.environ.get(\"AVALON_APP\")\n\n        # Use AVALON_APP_NAME to get full app name\n        if not app_name:\n            app_name = os.environ.get(\"AVALON_APP_NAME\")\n\n        # Fill missing values based on app full name\n        if (not host_name or not app_label) and app_name:\n            app_manager = ApplicationManager()\n            app = app_manager.applications.get(app_name)\n            if app:\n                if not host_name:\n                    host_name = app.host_name\n                if not app_label:\n                    app_label = app.full_label\n\n        context.data[\"hostName\"] = host_name\n        context.data[\"appName\"] = app_name\n        context.data[\"appLabel\"] = app_label\n"
  },
  {
    "path": "openpype/plugins/publish/collect_input_representations_to_versions.py",
    "content": "import pyblish.api\n\nfrom bson.objectid import ObjectId\n\nfrom openpype.client import get_representations\n\n\nclass CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin):\n    \"\"\"Converts collected input representations to input versions.\n\n    Any data in `instance.data[\"inputRepresentations\"]` gets converted into\n    `instance.data[\"inputVersions\"]` as supported in OpenPype v3.\n\n    \"\"\"\n    # This is a ContextPlugin because then we can query the database only once\n    # for the conversion of representation ids to version ids (optimization)\n    label = \"Input Representations to Versions\"\n    order = pyblish.api.CollectorOrder + 0.499\n    hosts = [\"*\"]\n\n    def process(self, context):\n        # Query all version ids for representation ids from the database once\n        representations = set()\n        for instance in context:\n            inst_repre = instance.data.get(\"inputRepresentations\", [])\n            if inst_repre:\n                representations.update(inst_repre)\n\n        representations_docs = get_representations(\n            project_name=context.data[\"projectEntity\"][\"name\"],\n            representation_ids=representations,\n            fields=[\"_id\", \"parent\"])\n\n        representation_id_to_version_id = {\n            str(repre[\"_id\"]): repre[\"parent\"]\n            for repre in representations_docs\n        }\n\n        for instance in context:\n            inst_repre = instance.data.get(\"inputRepresentations\", [])\n            if not inst_repre:\n                continue\n\n            input_versions = instance.data.setdefault(\"inputVersions\", [])\n            for repre_id in inst_repre:\n                version_id = representation_id_to_version_id.get(repre_id)\n                if version_id:\n                    input_versions.append(version_id)\n                else:\n                    self.log.debug(\n                        \"Representation id {} skipped because its version is \"\n                        \"not found in current project. Likely it is loaded \"\n                        \"from a library project or uses a deleted \"\n                        \"representation or version.\".format(repre_id)\n                    )\n"
  },
  {
    "path": "openpype/plugins/publish/collect_machine_name.py",
    "content": "\"\"\"\nRequires:\n    none\n\nProvides:\n    context     -> machine (str)\n\"\"\"\n\nimport pyblish.api\n\n\nclass CollectMachineName(pyblish.api.ContextPlugin):\n    label = \"Local Machine Name\"\n    order = pyblish.api.CollectorOrder - 0.5\n    hosts = [\"*\"]\n\n    def process(self, context):\n        import socket\n\n        machine_name = socket.gethostname()\n        self.log.info(\"Machine name: %s\" % machine_name)\n        context.data[\"machine\"] = machine_name\n"
  },
  {
    "path": "openpype/plugins/publish/collect_modules.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Collect OpenPype modules.\"\"\"\nfrom openpype.modules import ModulesManager\nimport pyblish.api\n\n\nclass CollectModules(pyblish.api.ContextPlugin):\n    \"\"\"Collect OpenPype modules.\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.5\n    label = \"OpenPype Modules\"\n\n    def process(self, context):\n        manager = ModulesManager()\n        context.data[\"openPypeModules\"] = manager.modules_by_name\n"
  },
  {
    "path": "openpype/plugins/publish/collect_otio_frame_ranges.py",
    "content": "\"\"\"\nRequires:\n    otioTimeline -> context data attribute\n    review -> instance data attribute\n    masterLayer -> instance data attribute\n    otioClipRange -> instance data attribute\n\"\"\"\nfrom pprint import pformat\n\nimport pyblish.api\n\n\nclass CollectOtioFrameRanges(pyblish.api.InstancePlugin):\n    \"\"\"Getting otio ranges from otio_clip\n\n    Adding timeline and source ranges to instance data\"\"\"\n\n    label = \"Collect OTIO Frame Ranges\"\n    order = pyblish.api.CollectorOrder - 0.08\n    families = [\"shot\", \"clip\"]\n    hosts = [\"resolve\", \"hiero\", \"flame\", \"traypublisher\"]\n\n    def process(self, instance):\n        # Not all hosts can import these modules.\n        import opentimelineio as otio\n        from openpype.pipeline.editorial import (\n            get_media_range_with_retimes,\n            otio_range_to_frame_range,\n            otio_range_with_handles\n        )\n\n        # get basic variables\n        otio_clip = instance.data[\"otioClip\"]\n        workfile_start = instance.data[\"workfileFrameStart\"]\n        workfile_source_duration = instance.data.get(\"shotDurationFromSource\")\n\n        # get ranges\n        otio_tl_range = otio_clip.range_in_parent()\n        otio_src_range = otio_clip.source_range\n        otio_avalable_range = otio_clip.available_range()\n        otio_tl_range_handles = otio_range_with_handles(\n            otio_tl_range, instance)\n        otio_src_range_handles = otio_range_with_handles(\n            otio_src_range, instance)\n\n        # get source avalable start frame\n        src_starting_from = otio.opentime.to_frames(\n            otio_avalable_range.start_time,\n            otio_avalable_range.start_time.rate)\n\n        # convert to frames\n        range_convert = otio_range_to_frame_range\n        tl_start, tl_end = range_convert(otio_tl_range)\n        tl_start_h, tl_end_h = range_convert(otio_tl_range_handles)\n        src_start, src_end = range_convert(otio_src_range)\n        src_start_h, src_end_h = range_convert(otio_src_range_handles)\n        frame_start = workfile_start\n        frame_end = frame_start + otio.opentime.to_frames(\n            otio_tl_range.duration, otio_tl_range.duration.rate) - 1\n\n        # in case of retimed clip and frame range should not be retimed\n        if workfile_source_duration:\n            # get available range trimmed with processed retimes\n            retimed_attributes = get_media_range_with_retimes(\n                otio_clip, 0, 0)\n            self.log.debug(\n                \">> retimed_attributes: {}\".format(retimed_attributes))\n            media_in = int(retimed_attributes[\"mediaIn\"])\n            media_out = int(retimed_attributes[\"mediaOut\"])\n            frame_end = frame_start + (media_out - media_in) + 1\n            self.log.debug(frame_end)\n\n        data = {\n            \"frameStart\": frame_start,\n            \"frameEnd\": frame_end,\n            \"clipIn\": tl_start,\n            \"clipOut\": tl_end - 1,\n            \"clipInH\": tl_start_h,\n            \"clipOutH\": tl_end_h - 1,\n            \"sourceStart\": src_starting_from + src_start,\n            \"sourceEnd\": src_starting_from + src_end - 1,\n            \"sourceStartH\": src_starting_from + src_start_h,\n            \"sourceEndH\": src_starting_from + src_end_h - 1,\n        }\n        instance.data.update(data)\n        self.log.debug(\n            \"_ data: {}\".format(pformat(data)))\n        self.log.debug(\n            \"_ instance.data: {}\".format(pformat(instance.data)))\n"
  },
  {
    "path": "openpype/plugins/publish/collect_otio_review.py",
    "content": "\"\"\"\nRequires:\n    instance -> otioClip\n    context -> otioTimeline\n\nOptional:\n    instance -> reviewTrack\n\nProvides:\n    instance -> otioReviewClips\n    instance -> families (adding [\"review\", \"ftrack\"])\n\"\"\"\n\nfrom pprint import pformat\n\nimport pyblish.api\n\n\nclass CollectOtioReview(pyblish.api.InstancePlugin):\n    \"\"\"Get matching otio track from defined review layer\"\"\"\n\n    label = \"Collect OTIO Review\"\n    order = pyblish.api.CollectorOrder - 0.078\n    families = [\"clip\"]\n    hosts = [\"resolve\", \"hiero\", \"flame\"]\n\n    def process(self, instance):\n        # Not all hosts can import this module.\n        import opentimelineio as otio\n\n        # get basic variables\n        otio_review_clips = []\n        otio_timeline = instance.context.data[\"otioTimeline\"]\n        otio_clip = instance.data[\"otioClip\"]\n        self.log.debug(\"__ otioClip: {}\".format(otio_clip))\n        # optionally get `reviewTrack`\n        review_track_name = instance.data.get(\"reviewTrack\")\n\n        # generate range in parent\n        otio_tl_range = otio_clip.range_in_parent()\n\n        # calculate real timeline end needed for the clip\n        clip_frame_end = int(\n            otio_tl_range.start_time.value + otio_tl_range.duration.value)\n\n        # skip if no review track available\n        if not review_track_name:\n            return\n\n        # loop all tracks and match with name in `reviewTrack`\n        for track in otio_timeline.tracks:\n            if review_track_name != track.name:\n                continue\n\n            # process correct track\n            # establish gap\n            otio_gap = None\n\n            # get track parent range\n            track_rip = track.range_in_parent()\n\n            # calculate real track end frame\n            track_frame_end = int(track_rip.end_time_exclusive().value)\n\n            # check if the end of track is not lower then clip requirement\n            if clip_frame_end > track_frame_end:\n                # calculate diference duration\n                gap_duration = clip_frame_end - track_frame_end\n                # create rational time range for gap\n                otio_gap_range = otio.opentime.TimeRange(\n                    start_time=otio.opentime.RationalTime(\n                        float(0),\n                        track_rip.start_time.rate\n                    ),\n                    duration=otio.opentime.RationalTime(\n                        float(gap_duration),\n                        track_rip.start_time.rate\n                    )\n                )\n                # crate gap\n                otio_gap = otio.schema.Gap(source_range=otio_gap_range)\n\n            # trim available clips from devined track as reviewable source\n            otio_review_clips = otio.algorithms.track_trimmed_to_range(\n                track,\n                otio_tl_range\n            )\n            # add gap at the end if track end is shorter then needed\n            if otio_gap:\n                otio_review_clips.append(otio_gap)\n\n        if otio_review_clips:\n            # add review track to instance and change label to reflect it\n            label = instance.data.get(\"label\", instance.data[\"subset\"])\n            instance.data[\"label\"] = label + \" (review)\"\n            instance.data[\"families\"] += [\"review\", \"ftrack\"]\n            instance.data[\"otioReviewClips\"] = otio_review_clips\n            self.log.info(\n                \"Creating review track: {}\".format(otio_review_clips))\n\n        self.log.debug(\n            \"_ instance.data: {}\".format(pformat(instance.data)))\n        self.log.debug(\n            \"_ families: {}\".format(instance.data[\"families\"]))\n"
  },
  {
    "path": "openpype/plugins/publish/collect_otio_subset_resources.py",
    "content": "\"\"\"\nRequires:\n    instance -> otio_clip\n\nProvides:\n    instance -> otioReviewClips\n\"\"\"\nimport os\n\nimport clique\nimport pyblish.api\n\nfrom openpype.pipeline.publish import (\n    get_publish_template_name\n)\n\n\nclass CollectOtioSubsetResources(pyblish.api.InstancePlugin):\n    \"\"\"Get Resources for a subset version\"\"\"\n\n    label = \"Collect OTIO Subset Resources\"\n    order = pyblish.api.CollectorOrder + 0.491\n    families = [\"clip\"]\n    hosts = [\"resolve\", \"hiero\", \"flame\"]\n\n    def process(self, instance):\n        # Not all hosts can import these modules.\n        import opentimelineio as otio\n        from openpype.pipeline.editorial import (\n            get_media_range_with_retimes,\n            range_from_frames,\n            make_sequence_collection\n        )\n\n        if \"audio\" in instance.data[\"family\"]:\n            return\n\n        if not instance.data.get(\"representations\"):\n            instance.data[\"representations\"] = []\n\n        if not instance.data.get(\"versionData\"):\n            instance.data[\"versionData\"] = {}\n\n        template_name = self.get_template_name(instance)\n        anatomy = instance.context.data[\"anatomy\"]\n        publish_template_category = anatomy.templates[template_name]\n        template = os.path.normpath(publish_template_category[\"path\"])\n        self.log.debug(\n            \">> template: {}\".format(template))\n\n        handle_start = instance.data[\"handleStart\"]\n        handle_end = instance.data[\"handleEnd\"]\n\n        # get basic variables\n        otio_clip = instance.data[\"otioClip\"]\n        otio_available_range = otio_clip.available_range()\n        media_fps = otio_available_range.start_time.rate\n        available_duration = otio_available_range.duration.value\n\n        # get available range trimmed with processed retimes\n        retimed_attributes = get_media_range_with_retimes(\n            otio_clip, handle_start, handle_end)\n        self.log.debug(\n            \">> retimed_attributes: {}\".format(retimed_attributes))\n\n        # break down into variables\n        media_in = int(retimed_attributes[\"mediaIn\"])\n        media_out = int(retimed_attributes[\"mediaOut\"])\n        handle_start = int(retimed_attributes[\"handleStart\"])\n        handle_end = int(retimed_attributes[\"handleEnd\"])\n\n        # set versiondata if any retime\n        version_data = retimed_attributes.get(\"versionData\")\n\n        if version_data:\n            instance.data[\"versionData\"].update(version_data)\n\n        # convert to available frame range with handles\n        a_frame_start_h = media_in - handle_start\n        a_frame_end_h = media_out + handle_end\n\n        # create trimmed otio time range\n        trimmed_media_range_h = range_from_frames(\n            a_frame_start_h, (a_frame_end_h - a_frame_start_h) + 1,\n            media_fps\n        )\n        trimmed_duration = trimmed_media_range_h.duration.value\n\n        self.log.debug(\"trimmed_media_range_h: {}\".format(\n            trimmed_media_range_h))\n        self.log.debug(\"a_frame_start_h: {}\".format(\n            a_frame_start_h))\n        self.log.debug(\"a_frame_end_h: {}\".format(\n            a_frame_end_h))\n\n        # create frame start and end\n        frame_start = instance.data[\"frameStart\"]\n        frame_end = frame_start + (media_out - media_in)\n\n        # Fit start /end frame to media in /out\n        if \"{originalBasename}\" in template:\n            frame_start = media_in\n            frame_end = media_out\n\n        # add to version data start and end range data\n        # for loader plugins to be correctly displayed and loaded\n        instance.data[\"versionData\"].update({\n            \"fps\": media_fps\n        })\n\n        if not instance.data[\"versionData\"].get(\"retime\"):\n            instance.data[\"versionData\"].update({\n                \"frameStart\": frame_start,\n                \"frameEnd\": frame_end,\n                \"handleStart\": handle_start,\n                \"handleEnd\": handle_end,\n            })\n        else:\n            instance.data[\"versionData\"].update({\n                \"frameStart\": frame_start,\n                \"frameEnd\": frame_end\n            })\n\n        # change frame_start and frame_end values\n        # for representation to be correctly renumbered in integrate_new\n        frame_start -= handle_start\n        frame_end += handle_end\n\n        media_ref = otio_clip.media_reference\n        metadata = media_ref.metadata\n\n        is_sequence = None\n        # check in two way if it is sequence\n        if hasattr(otio.schema, \"ImageSequenceReference\"):\n            # for OpenTimelineIO 0.13 and newer\n            if isinstance(\n                media_ref,\n                otio.schema.ImageSequenceReference\n            ):\n                is_sequence = True\n        elif metadata.get(\"padding\"):\n            is_sequence = True\n\n        self.log.info(\n            \"frame_start-frame_end: {}-{}\".format(frame_start, frame_end))\n\n        if is_sequence:\n            # file sequence way\n            if hasattr(media_ref, \"target_url_base\"):\n                self.staging_dir = media_ref.target_url_base\n                head = media_ref.name_prefix\n                tail = media_ref.name_suffix\n                collection = clique.Collection(\n                    head=head,\n                    tail=tail,\n                    padding=media_ref.frame_zero_padding\n                )\n                collection.indexes.update(\n                    list(range(a_frame_start_h, (a_frame_end_h + 1)))\n                )\n\n            else:\n                # in case it is file sequence but not new OTIO schema\n                # `ImageSequenceReference`\n                path = media_ref.target_url\n                collection_data = make_sequence_collection(\n                    path, trimmed_media_range_h, metadata)\n                self.staging_dir, collection = collection_data\n\n            self.log.debug(collection)\n            repre = self._create_representation(\n                frame_start, frame_end, collection=collection)\n\n        else:\n            _trim = False\n            dirname, filename = os.path.split(media_ref.target_url)\n            self.staging_dir = dirname\n            if trimmed_duration < available_duration:\n                self.log.debug(\"Ready for Trimming\")\n                instance.data[\"families\"].append(\"trim\")\n                instance.data[\"otioTrimmingRange\"] = trimmed_media_range_h\n                _trim = True\n\n            self.log.debug(filename)\n            repre = self._create_representation(\n                frame_start, frame_end, file=filename, trim=_trim)\n\n        instance.data[\"originalDirname\"] = self.staging_dir\n\n        if repre:\n            # add representation to instance data\n            instance.data[\"representations\"].append(repre)\n            self.log.debug(\">>>>>>>> {}\".format(repre))\n\n        self.log.debug(instance.data)\n\n    def _create_representation(self, start, end, **kwargs):\n        \"\"\"\n        Creating representation data.\n\n        Args:\n            start (int): start frame\n            end (int): end frame\n            kwargs (dict): optional data\n\n        Returns:\n            dict: representation data\n        \"\"\"\n\n        # create default representation data\n        representation_data = {\n            \"frameStart\": start,\n            \"frameEnd\": end,\n            \"stagingDir\": self.staging_dir\n        }\n\n        if kwargs.get(\"collection\"):\n            collection = kwargs.get(\"collection\")\n            files = list(collection)\n            ext = collection.format(\"{tail}\")\n            representation_data.update({\n                \"name\": ext[1:],\n                \"ext\": ext[1:],\n                \"files\": files,\n                \"frameStart\": start,\n                \"frameEnd\": end,\n            })\n\n        if kwargs.get(\"file\"):\n            file = kwargs.get(\"file\")\n            ext = os.path.splitext(file)[-1]\n            representation_data.update({\n                \"name\": ext[1:],\n                \"ext\": ext[1:],\n                \"files\": file,\n                \"frameStart\": start,\n                \"frameEnd\": end,\n            })\n\n        if kwargs.get(\"trim\") is True:\n            representation_data[\"tags\"] = [\"trim\"]\n        return representation_data\n\n    def get_template_name(self, instance):\n        \"\"\"Return anatomy template name to use for integration\"\"\"\n\n        # Anatomy data is pre-filled by Collectors\n        context = instance.context\n        project_name = context.data[\"projectName\"]\n\n        # Task can be optional in anatomy data\n        host_name = context.data[\"hostName\"]\n        family = instance.data[\"family\"]\n        anatomy_data = instance.data[\"anatomyData\"]\n        task_info = anatomy_data.get(\"task\") or {}\n\n        return get_publish_template_name(\n            project_name,\n            host_name,\n            family,\n            task_name=task_info.get(\"name\"),\n            task_type=task_info.get(\"type\"),\n            project_settings=context.data[\"project_settings\"],\n            logger=self.log\n        )\n"
  },
  {
    "path": "openpype/plugins/publish/collect_rendered_files.py",
    "content": "\"\"\"Loads publishing context from json and continues in publish process.\n\nRequires:\n    anatomy -> context[\"anatomy\"] *(pyblish.api.CollectorOrder - 0.4)\n\nProvides:\n    context, instances -> All data from previous publishing process.\n\"\"\"\n\nimport os\nimport json\n\nimport pyblish.api\n\nfrom openpype.pipeline import legacy_io, KnownPublishError\nfrom openpype.pipeline.publish.lib import add_repre_files_for_cleanup\n\n\nclass CollectRenderedFiles(pyblish.api.ContextPlugin):\n    \"\"\"\n    This collector will try to find json files in provided\n    `OPENPYPE_PUBLISH_DATA`. Those files _MUST_ share same context.\n\n    Note:\n        We should split this collector and move the part which handle reading\n            of file and it's context from session data before collect anatomy\n            and instance creation dependent on anatomy can be done here.\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.2\n    # Keep \"filesequence\" for backwards compatibility of older jobs\n    targets = [\"filesequence\", \"farm\"]\n    label = \"Collect rendered frames\"\n\n    _context = None\n\n    def _load_json(self, path):\n        path = path.strip('\\\"')\n        assert os.path.isfile(path), (\n            \"Path to json file doesn't exist. \\\"{}\\\"\".format(path)\n        )\n        data = None\n        with open(path, \"r\") as json_file:\n            try:\n                data = json.load(json_file)\n            except Exception as exc:\n                self.log.error(\n                    \"Error loading json: \"\n                    \"{} - Exception: {}\".format(path, exc)\n                )\n        return data\n\n    def _fill_staging_dir(self, data_object, anatomy):\n        staging_dir = data_object.get(\"stagingDir\")\n        if staging_dir:\n            data_object[\"stagingDir\"] = anatomy.fill_root(staging_dir)\n            self.log.debug(\"Filling stagingDir with root to: %s\",\n                           data_object[\"stagingDir\"])\n\n    def _process_path(self, data, anatomy):\n        \"\"\"Process data of a single JSON publish metadata file.\n\n        Args:\n            data: The loaded metadata from the JSON file\n            anatomy: Anatomy for the current context\n\n        Returns:\n            bool: Whether any instance of this particular metadata file\n                has a persistent staging dir.\n\n        \"\"\"\n        # validate basic necessary data\n        data_err = \"invalid json file - missing data\"\n        required = [\"asset\", \"user\", \"comment\",\n                    \"job\", \"instances\", \"session\", \"version\"]\n        assert all(elem in data.keys() for elem in required), data_err\n\n        # set context by first json file\n        ctx = self._context.data\n\n        ctx[\"asset\"] = ctx.get(\"asset\") or data.get(\"asset\")\n        ctx[\"intent\"] = ctx.get(\"intent\") or data.get(\"intent\")\n        ctx[\"comment\"] = ctx.get(\"comment\") or data.get(\"comment\")\n        ctx[\"user\"] = ctx.get(\"user\") or data.get(\"user\")\n        ctx[\"version\"] = ctx.get(\"version\") or data.get(\"version\")\n\n        # basic sanity check to see if we are working in same context\n        # if some other json file has different context, bail out.\n        ctx_err = \"inconsistent contexts in json files - %s\"\n        assert ctx.get(\"asset\") == data.get(\"asset\"), ctx_err % \"asset\"\n        assert ctx.get(\"intent\") == data.get(\"intent\"), ctx_err % \"intent\"\n        assert ctx.get(\"comment\") == data.get(\"comment\"), ctx_err % \"comment\"\n        assert ctx.get(\"user\") == data.get(\"user\"), ctx_err % \"user\"\n        assert ctx.get(\"version\") == data.get(\"version\"), ctx_err % \"version\"\n\n        # now we can just add instances from json file and we are done\n        any_staging_dir_persistent = False\n        for instance_data in data.get(\"instances\"):\n\n            self.log.debug(\"  - processing instance for {}\".format(\n                instance_data.get(\"subset\")))\n            instance = self._context.create_instance(\n                instance_data.get(\"subset\")\n            )\n\n            self._fill_staging_dir(instance_data, anatomy)\n            instance.data.update(instance_data)\n\n            # stash render job id for later validation\n            instance.data[\"render_job_id\"] = data.get(\"job\").get(\"_id\")\n            staging_dir_persistent = instance.data.get(\n                \"stagingDir_persistent\", False\n            )\n            if staging_dir_persistent:\n                any_staging_dir_persistent = True\n\n            representations = []\n            for repre_data in instance_data.get(\"representations\") or []:\n                self._fill_staging_dir(repre_data, anatomy)\n                representations.append(repre_data)\n\n                if not staging_dir_persistent:\n                    add_repre_files_for_cleanup(instance, repre_data)\n\n            instance.data[\"representations\"] = representations\n\n            # add audio if in metadata data\n            if data.get(\"audio\"):\n                instance.data.update({\n                    \"audio\": [{\n                        \"filename\": data.get(\"audio\"),\n                        \"offset\": 0\n                    }]\n                })\n                self.log.debug(\n                    f\"Adding audio to instance: {instance.data['audio']}\")\n\n        return any_staging_dir_persistent\n\n    def process(self, context):\n        self._context = context\n\n        if not os.environ.get(\"OPENPYPE_PUBLISH_DATA\"):\n            raise KnownPublishError(\"Missing `OPENPYPE_PUBLISH_DATA`\")\n\n        # QUESTION\n        #   Do we support (or want support) multiple files in the variable?\n        #   - what if they have different context?\n        paths = os.environ[\"OPENPYPE_PUBLISH_DATA\"].split(os.pathsep)\n\n        # Using already collected Anatomy\n        anatomy = context.data[\"anatomy\"]\n        self.log.debug(\"Getting root setting for project \\\"{}\\\"\".format(\n            anatomy.project_name\n        ))\n\n        self.log.debug(\"Anatomy roots: {}\".format(anatomy.roots))\n        try:\n            session_is_set = False\n            for path in paths:\n                path = anatomy.fill_root(path)\n                data = self._load_json(path)\n                assert data, \"failed to load json file\"\n                if not session_is_set:\n                    session_data = data[\"session\"]\n                    remapped = anatomy.roots_obj.path_remapper(\n                        session_data[\"AVALON_WORKDIR\"]\n                    )\n                    if remapped:\n                        session_data[\"AVALON_WORKDIR\"] = remapped\n\n                    self.log.debug(\"Setting session using data from file\")\n                    legacy_io.Session.update(session_data)\n                    os.environ.update(session_data)\n                    session_is_set = True\n                staging_dir_persistent = self._process_path(data, anatomy)\n                if not staging_dir_persistent:\n                    context.data[\"cleanupFullPaths\"].append(path)\n                    context.data[\"cleanupEmptyDirs\"].append(\n                        os.path.dirname(path)\n                    )\n        except Exception as e:\n            self.log.error(e, exc_info=True)\n            raise Exception(\"Error\") from e\n"
  },
  {
    "path": "openpype/plugins/publish/collect_resources_path.py",
    "content": "\"\"\"\nRequires:\n    context     -> anatomy\n    context     -> anatomyData\n\nProvides:\n    instance    -> publishDir\n    instance    -> resourcesDir\n\"\"\"\n\nimport os\nimport copy\n\nimport pyblish.api\n\n\nclass CollectResourcesPath(pyblish.api.InstancePlugin):\n    \"\"\"Generate directory path where the files and resources will be stored.\n\n    Collects folder name and file name from files, if exists, for in-situ\n    publishing.\n    \"\"\"\n\n    label = \"Collect Resources Path\"\n    order = pyblish.api.CollectorOrder + 0.495\n    families = [\"workfile\",\n                \"pointcache\",\n                \"proxyAbc\",\n                \"camera\",\n                \"animation\",\n                \"model\",\n                \"mayaAscii\",\n                \"mayaScene\",\n                \"setdress\",\n                \"layout\",\n                \"ass\",\n                \"vdbcache\",\n                \"scene\",\n                \"vrayproxy\",\n                \"render\",\n                \"prerender\",\n                \"imagesequence\",\n                \"rendersetup\",\n                \"rig\",\n                \"plate\",\n                \"look\",\n                \"mvLook\",\n                \"yetiRig\",\n                \"yeticache\",\n                \"nukenodes\",\n                \"gizmo\",\n                \"source\",\n                \"matchmove\",\n                \"image\",\n                \"source\",\n                \"assembly\",\n                \"fbx\",\n                \"gltf\",\n                \"textures\",\n                \"action\",\n                \"background\",\n                \"effect\",\n                \"staticMesh\",\n                \"skeletalMesh\",\n                \"xgen\",\n                \"yeticacheUE\",\n                \"tycache\"\n                ]\n\n    def process(self, instance):\n        anatomy = instance.context.data[\"anatomy\"]\n\n        template_data = copy.deepcopy(instance.data[\"anatomyData\"])\n\n        # This is for cases of Deprecated anatomy without `folder`\n        # TODO remove when all clients have solved this issue\n        template_data.update({\n            \"frame\": \"FRAME_TEMP\",\n            \"representation\": \"TEMP\"\n        })\n\n        publish_templates = anatomy.templates_obj[\"publish\"]\n        if \"folder\" in publish_templates:\n            publish_folder = publish_templates[\"folder\"].format_strict(\n                template_data\n            )\n        else:\n            # solve deprecated situation when `folder` key is not underneath\n            # `publish` anatomy\n            self.log.warning((\n                \"Deprecation warning: Anatomy does not have set `folder`\"\n                \" key underneath `publish` (in global of for project `{}`).\"\n            ).format(anatomy.project_name))\n\n            file_path = publish_templates[\"path\"].format_strict(template_data)\n            publish_folder = os.path.dirname(file_path)\n\n        publish_folder = os.path.normpath(publish_folder)\n        resources_folder = os.path.join(publish_folder, \"resources\")\n\n        instance.data[\"publishDir\"] = publish_folder\n        instance.data[\"resourcesDir\"] = resources_folder\n\n        self.log.debug(\"publishDir: \\\"{}\\\"\".format(publish_folder))\n        self.log.debug(\"resourcesDir: \\\"{}\\\"\".format(resources_folder))\n"
  },
  {
    "path": "openpype/plugins/publish/collect_scene_loaded_versions.py",
    "content": "import pyblish.api\n\nfrom openpype.client import get_representations\nfrom openpype.pipeline import registered_host\n\n\nclass CollectSceneLoadedVersions(pyblish.api.ContextPlugin):\n\n    order = pyblish.api.CollectorOrder + 0.0001\n    label = \"Collect Versions Loaded in Scene\"\n    hosts = [\n        \"aftereffects\",\n        \"blender\",\n        \"celaction\",\n        \"fusion\",\n        \"harmony\",\n        \"hiero\",\n        \"houdini\",\n        \"maya\",\n        \"nuke\",\n        \"photoshop\",\n        \"resolve\",\n        \"tvpaint\",\n        \"equalizer\",\n    ]\n\n    def process(self, context):\n        host = registered_host()\n        if host is None:\n            self.log.warn(\"No registered host.\")\n            return\n\n        if not hasattr(host, \"ls\"):\n            host_name = host.__name__\n            self.log.warn(\"Host %r doesn't have ls() implemented.\" % host_name)\n            return\n\n        loaded_versions = []\n        containers = list(host.ls())\n        repre_ids = {\n            container[\"representation\"]\n            for container in containers\n        }\n\n        project_name = context.data[\"projectName\"]\n        repre_docs = get_representations(\n            project_name,\n            representation_ids=repre_ids,\n            fields=[\"_id\", \"parent\"]\n        )\n        repre_doc_by_str_id = {\n            str(doc[\"_id\"]): doc\n            for doc in repre_docs\n        }\n\n        # QUESTION should we add same representation id when loaded multiple\n        #   times?\n        for con in containers:\n            repre_id = con[\"representation\"]\n            repre_doc = repre_doc_by_str_id.get(repre_id)\n            if repre_doc is None:\n                self.log.warning((\n                    \"Skipping container,\"\n                    \" did not find representation document. {}\"\n                ).format(str(con)))\n                continue\n\n            # NOTE:\n            # may have more then one representation that are same version\n            version = {\n                \"subsetName\": con[\"name\"],\n                \"representation\": repre_doc[\"_id\"],\n                \"version\": repre_doc[\"parent\"],\n            }\n            loaded_versions.append(version)\n\n        context.data[\"loadedVersions\"] = loaded_versions\n"
  },
  {
    "path": "openpype/plugins/publish/collect_scene_version.py",
    "content": "import os\nimport pyblish.api\n\nfrom openpype.lib import get_version_from_path\nfrom openpype.tests.lib import is_in_tests\nfrom openpype.pipeline import KnownPublishError\n\n\nclass CollectSceneVersion(pyblish.api.ContextPlugin):\n    \"\"\"Finds version in the filename or passes the one found in the context\n        Arguments:\n        version (int, optional): version number of the publish\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = 'Collect Scene Version'\n    # configurable in Settings\n    hosts = [\n        \"aftereffects\",\n        \"blender\",\n        \"celaction\",\n        \"fusion\",\n        \"harmony\",\n        \"hiero\",\n        \"houdini\",\n        \"maya\",\n        \"max\",\n        \"nuke\",\n        \"photoshop\",\n        \"resolve\",\n        \"tvpaint\"\n    ]\n\n    # in some cases of headless publishing (for example webpublisher using PS)\n    # you want to ignore version from name and let integrate use next version\n    skip_hosts_headless_publish = []\n\n    def process(self, context):\n        # tests should be close to regular publish as possible\n        if (\n            os.environ.get(\"HEADLESS_PUBLISH\")\n            and not is_in_tests()\n            and context.data[\"hostName\"] in self.skip_hosts_headless_publish\n        ):\n            self.log.debug(\"Skipping for headless publishing\")\n            return\n\n        if not context.data.get('currentFile'):\n            raise KnownPublishError(\"Cannot get current workfile path. \"\n                                    \"Make sure your scene is saved.\")\n\n        filename = os.path.basename(context.data.get('currentFile'))\n\n        if '<shell>' in filename:\n            return\n\n        self.log.debug(\n            \"Collecting scene version from filename: {}\".format(filename)\n        )\n\n        version = get_version_from_path(filename)\n        if version is None:\n            raise KnownPublishError(\"Unable to retrieve version number from \"\n                                    \"filename: {}\".format(filename))\n\n        context.data['version'] = int(version)\n        self.log.debug(\n            \"Collected scene version: {}\".format(context.data.get('version'))\n        )\n"
  },
  {
    "path": "openpype/plugins/publish/collect_settings.py",
    "content": "from pyblish import api\nfrom openpype.settings import (\n    get_current_project_settings,\n    get_system_settings,\n)\n\n\nclass CollectSettings(api.ContextPlugin):\n    \"\"\"Collect Settings and store in the context.\"\"\"\n\n    order = api.CollectorOrder - 0.491\n    label = \"Collect Settings\"\n\n    def process(self, context):\n        context.data[\"project_settings\"] = get_current_project_settings()\n        context.data[\"system_settings\"] = get_system_settings()\n"
  },
  {
    "path": "openpype/plugins/publish/collect_shell_workspace.py",
    "content": "import os\nimport pyblish.api\n\n\nclass CollectShellWorkspace(pyblish.api.ContextPlugin):\n    \"\"\"Inject the current workspace into context\"\"\"\n\n    order = pyblish.api.CollectorOrder - 0.5\n    label = \"Shell Workspace\"\n\n    hosts = [\"shell\"]\n\n    def process(self, context):\n        context.data[\"workspaceDir\"] = os.getcwd()\n"
  },
  {
    "path": "openpype/plugins/publish/collect_source_for_source.py",
    "content": "\"\"\"\nRequires:\n    instance     -> currentFile\n    instance     -> source\n\nProvides:\n    instance    -> originalBasename\n    instance    -> originalDirname\n\"\"\"\n\nimport os\n\nimport pyblish.api\n\n\nclass CollectSourceForSource(pyblish.api.InstancePlugin):\n    \"\"\"Collects source location of file for instance.\n\n    Used for 'source' template name which handles in place publishing.\n    For this kind of publishing files are present with correct file name\n    pattern and correct location.\n    \"\"\"\n\n    label = \"Collect Source\"\n    order = pyblish.api.CollectorOrder + 0.495\n\n    def process(self, instance):\n        # parse folder name and file name for online and source templates\n        # currentFile comes from hosts workfiles\n        # source comes from Publisher\n        current_file = instance.data.get(\"currentFile\")\n        source = instance.data.get(\"source\")\n        source_file = current_file or source\n        if source_file:\n            self.log.debug(\"Parsing paths for {}\".format(source_file))\n            if not instance.data.get(\"originalBasename\"):\n                instance.data[\"originalBasename\"] = \\\n                    os.path.basename(source_file)\n\n            if not instance.data.get(\"originalDirname\"):\n                instance.data[\"originalDirname\"] = \\\n                    os.path.dirname(source_file)\n"
  },
  {
    "path": "openpype/plugins/publish/collect_time.py",
    "content": "import pyblish.api\nfrom openpype.lib import get_formatted_current_time\n\n\nclass CollectTime(pyblish.api.ContextPlugin):\n    \"\"\"Store global time at the time of publish\"\"\"\n\n    label = \"Collect Current Time\"\n    order = pyblish.api.CollectorOrder - 0.499\n\n    def process(self, context):\n        context.data[\"time\"] = get_formatted_current_time()\n"
  },
  {
    "path": "openpype/plugins/publish/extract_burnin.py",
    "content": "import os\nimport json\nimport copy\nimport tempfile\nimport platform\nimport shutil\n\nimport clique\nimport six\nimport pyblish.api\n\nfrom openpype import resources, PACKAGE_DIR\nfrom openpype.pipeline import publish\nfrom openpype.lib import (\n    run_openpype_process,\n\n    get_transcode_temp_directory,\n    convert_input_paths_for_ffmpeg,\n    should_convert_for_ffmpeg\n)\nfrom openpype.lib.profiles_filtering import filter_profiles\nfrom openpype.pipeline.publish.lib import add_repre_files_for_cleanup\n\n\nclass ExtractBurnin(publish.Extractor):\n    \"\"\"\n    Extractor to create video with pre-defined burnins from\n    existing extracted video representation.\n\n    It will work only on represenations having `burnin = True` or\n    `tags` including `burnin`\n    \"\"\"\n\n    label = \"Extract burnins\"\n    order = pyblish.api.ExtractorOrder + 0.03\n\n    families = [\"review\", \"burnin\"]\n    hosts = [\n        \"nuke\",\n        \"maya\",\n        \"shell\",\n        \"hiero\",\n        \"premiere\",\n        \"traypublisher\",\n        \"standalonepublisher\",\n        \"harmony\",\n        \"fusion\",\n        \"aftereffects\",\n        \"tvpaint\",\n        \"webpublisher\",\n        \"aftereffects\",\n        \"photoshop\",\n        \"flame\",\n        \"houdini\",\n        \"max\",\n        \"blender\",\n        \"unreal\"\n    ]\n\n    optional = True\n\n    positions = [\n        \"top_left\", \"top_centered\", \"top_right\",\n        \"bottom_right\", \"bottom_centered\", \"bottom_left\"\n    ]\n    # Default options for burnins for cases that are not set in presets.\n    default_options = {\n        \"font_size\": 42,\n        \"font_color\": [255, 255, 255, 255],\n        \"bg_color\": [0, 0, 0, 127],\n        \"bg_padding\": 5,\n        \"x_offset\": 5,\n        \"y_offset\": 5\n    }\n\n    # Configurable by Settings\n    profiles = None\n    options = None\n\n    def process(self, instance):\n        if not self.profiles:\n            self.log.warning(\"No profiles present for create burnin\")\n            return\n\n        if not instance.data.get(\"representations\"):\n            self.log.debug(\n                \"Instance does not have filled representations. Skipping\")\n            return\n\n        self.main_process(instance)\n\n        # Remove only representation tagged with both\n        # tags `delete` and `burnin`\n        for repre in tuple(instance.data[\"representations\"]):\n            if all(x in repre.get(\"tags\", []) for x in ['delete', 'burnin']):\n                self.log.debug(\"Removing representation: {}\".format(repre))\n                instance.data[\"representations\"].remove(repre)\n\n    def _get_burnins_per_representations(self, instance, src_burnin_defs):\n        self.log.debug(\"Filtering of representations and their burnins starts\")\n\n        filtered_repres = []\n        repres = instance.data.get(\"representations\") or []\n        for idx, repre in enumerate(repres):\n            self.log.debug(\"repre ({}): `{}`\".format(idx + 1, repre[\"name\"]))\n            if not self.repres_is_valid(repre):\n                continue\n\n            repre_burnin_links = repre.get(\"burnins\", [])\n            self.log.debug(\n                \"repre_burnin_links: {}\".format(repre_burnin_links)\n            )\n\n            burnin_defs = copy.deepcopy(src_burnin_defs)\n            self.log.debug(\n                \"burnin_defs.keys(): {}\".format(burnin_defs.keys())\n            )\n\n            # Filter output definition by `burnin` represetation key\n            repre_linked_burnins = {\n                name: output\n                for name, output in burnin_defs.items()\n                if name in repre_burnin_links\n            }\n            self.log.debug(\n                \"repre_linked_burnins: {}\".format(repre_linked_burnins)\n            )\n\n            # if any match then replace burnin defs and follow tag filtering\n            if repre_linked_burnins:\n                burnin_defs = repre_linked_burnins\n\n            # Filter output definition by representation tags (optional)\n            repre_burnin_defs = self.filter_burnins_by_tags(\n                burnin_defs, repre[\"tags\"]\n            )\n            if not repre_burnin_defs:\n                self.log.debug(\n                    \"Skipped representation. All burnin definitions from\"\n                    \" selected profile do not match to representation's\"\n                    \" tags. \\\"{}\\\"\".format(repre[\"tags\"])\n                )\n                continue\n            filtered_repres.append((repre, repre_burnin_defs))\n\n        return filtered_repres\n\n    def main_process(self, instance):\n        host_name = instance.context.data[\"hostName\"]\n        family = instance.data[\"family\"]\n        task_data = instance.data[\"anatomyData\"].get(\"task\", {})\n        task_name = task_data.get(\"name\")\n        task_type = task_data.get(\"type\")\n        subset = instance.data[\"subset\"]\n\n        filtering_criteria = {\n            \"hosts\": host_name,\n            \"families\": family,\n            \"task_names\": task_name,\n            \"task_types\": task_type,\n            \"subset\": subset\n        }\n        profile = filter_profiles(self.profiles, filtering_criteria,\n                                  logger=self.log)\n\n        if not profile:\n            self.log.debug((\n                \"Skipped instance. None of profiles in presets are for\"\n                \" Host: \\\"{}\\\" | Families: \\\"{}\\\" | Task \\\"{}\\\"\"\n                \" | Task type \\\"{}\\\" | Subset \\\"{}\\\" \"\n            ).format(host_name, family, task_name, task_type, subset))\n            return\n\n        # Pre-filter burnin definitions by instance families\n        burnin_defs = self.filter_burnins_defs(profile, instance)\n        if not burnin_defs:\n            self.log.debug((\n                \"Skipped instance. Burnin definitions are not set for profile\"\n                \" Host: \\\"{}\\\" | Families: \\\"{}\\\" | Task \\\"{}\\\"\"\n                \" | Profile \\\"{}\\\"\"\n            ).format(host_name, family, task_name, profile))\n            return\n\n        burnin_options = self._get_burnin_options()\n\n        # Prepare basic data for processing\n        _burnin_data, _temp_data = self.prepare_basic_data(instance)\n\n        anatomy = instance.context.data[\"anatomy\"]\n        scriptpath = self.burnin_script_path()\n\n        # Args that will execute the script\n        executable_args = [\"run\", scriptpath]\n        burnins_per_repres = self._get_burnins_per_representations(\n            instance, burnin_defs\n        )\n        for repre, repre_burnin_defs in burnins_per_repres:\n            # Create copy of `_burnin_data` and `_temp_data` for repre.\n            burnin_data = copy.deepcopy(_burnin_data)\n            temp_data = copy.deepcopy(_temp_data)\n\n            # Prepare representation based data.\n            self.prepare_repre_data(instance, repre, burnin_data, temp_data)\n\n            src_repre_staging_dir = repre[\"stagingDir\"]\n            # Should convert representation source files before processing?\n            repre_files = repre[\"files\"]\n            if isinstance(repre_files, (tuple, list)):\n                filename = repre_files[0]\n                src_filepaths = [\n                    os.path.join(src_repre_staging_dir, filename)\n                    for filename in repre_files\n                ]\n            else:\n                filename = repre_files\n                src_filepaths = [os.path.join(src_repre_staging_dir, filename)]\n\n            first_input_path = os.path.join(src_repre_staging_dir, filename)\n            # Determine if representation requires pre conversion for ffmpeg\n            do_convert = should_convert_for_ffmpeg(first_input_path)\n            # If result is None the requirement of conversion can't be\n            #   determined\n            if do_convert is None:\n                self.log.debug(\n                    \"Can't determine if representation requires conversion.\"\n                    \" Skipped.\"\n                )\n                continue\n\n            # Do conversion if needed\n            #   - change staging dir of source representation\n            #   - must be set back after output definitions processing\n            if do_convert:\n                new_staging_dir = get_transcode_temp_directory()\n                repre[\"stagingDir\"] = new_staging_dir\n\n                convert_input_paths_for_ffmpeg(\n                    src_filepaths,\n                    new_staging_dir,\n                    self.log\n                )\n\n            # Add anatomy keys to burnin_data.\n            filled_anatomy = anatomy.format_all(burnin_data)\n            burnin_data[\"anatomy\"] = filled_anatomy.get_solved()\n\n            custom_data = copy.deepcopy(\n                instance.data.get(\"customData\") or {}\n            )\n            # Backwards compatibility (since 2022/04/07)\n            custom_data.update(\n                instance.data.get(\"custom_burnin_data\") or {}\n            )\n\n            # Add context data burnin_data.\n            burnin_data[\"custom\"] = custom_data\n\n            # Add data members.\n            burnin_data.update(instance.data.get(\"burninDataMembers\", {}))\n\n            # Add source camera name to burnin data\n            camera_name = repre.get(\"camera_name\")\n            if camera_name:\n                burnin_data[\"camera_name\"] = camera_name\n\n            first_output = True\n\n            files_to_delete = []\n\n            repre_burnin_options = copy.deepcopy(burnin_options)\n            # Use fps from representation for output in options\n            fps = repre.get(\"fps\")\n            if fps is not None:\n                repre_burnin_options[\"fps\"] = fps\n                # TODO Should we use fps from source representation to fill\n                #  it in review?\n                # burnin_data[\"fps\"] = fps\n\n            for filename_suffix, burnin_def in repre_burnin_defs.items():\n                new_repre = copy.deepcopy(repre)\n                new_repre[\"stagingDir\"] = src_repre_staging_dir\n\n                # Keep \"ftrackreview\" tag only on first output\n                if first_output:\n                    first_output = False\n                elif \"ftrackreview\" in new_repre[\"tags\"]:\n                    new_repre[\"tags\"].remove(\"ftrackreview\")\n\n                burnin_values = {}\n                for key in self.positions:\n                    value = burnin_def.get(key)\n                    if value:\n                        burnin_values[key] = value.replace(\n                            \"{task}\", \"{task[name]}\"\n                        )\n\n                # Remove \"delete\" tag from new representation\n                if \"delete\" in new_repre[\"tags\"]:\n                    new_repre[\"tags\"].remove(\"delete\")\n\n                if len(repre_burnin_defs.keys()) > 1:\n                    # Update name and outputName to be\n                    # able have multiple outputs in case of more burnin presets\n                    # Join previous \"outputName\" with filename suffix\n                    new_name = \"_\".join(\n                        [new_repre[\"outputName\"], filename_suffix]\n                    )\n                    new_repre[\"name\"] = new_name\n                    new_repre[\"outputName\"] = new_name\n\n                # Prepare paths and files for process.\n                self.input_output_paths(\n                    repre, new_repre, temp_data, filename_suffix\n                )\n\n                # Data for burnin script\n                script_data = {\n                    \"input\": temp_data[\"full_input_path\"],\n                    \"output\": temp_data[\"full_output_path\"],\n                    \"burnin_data\": burnin_data,\n                    \"options\": repre_burnin_options,\n                    \"values\": burnin_values,\n                    \"full_input_path\": temp_data[\"full_input_paths\"][0],\n                    \"first_frame\": temp_data[\"first_frame\"],\n                    \"ffmpeg_cmd\": new_repre.get(\"ffmpeg_cmd\", \"\")\n                }\n\n                self.log.debug(\n                    \"script_data: {}\".format(json.dumps(script_data, indent=4))\n                )\n\n                # Dump data to string\n                dumped_script_data = json.dumps(script_data)\n\n                # Store dumped json to temporary file\n                temporary_json_file = tempfile.NamedTemporaryFile(\n                    mode=\"w\", suffix=\".json\", delete=False\n                )\n                temporary_json_file.write(dumped_script_data)\n                temporary_json_file.close()\n                temporary_json_filepath = temporary_json_file.name.replace(\n                    \"\\\\\", \"/\"\n                )\n\n                # Prepare subprocess arguments\n                args = list(executable_args)\n                args.append(temporary_json_filepath)\n                self.log.debug(\"Executing: {}\".format(\" \".join(args)))\n\n                # Run burnin script\n                process_kwargs = {\n                    \"logger\": self.log\n                }\n\n                run_openpype_process(*args, **process_kwargs)\n                # Remove the temporary json\n                os.remove(temporary_json_filepath)\n\n                for filepath in temp_data[\"full_input_paths\"]:\n                    filepath = filepath.replace(\"\\\\\", \"/\")\n                    if filepath not in files_to_delete:\n                        files_to_delete.append(filepath)\n\n                # Add new representation to instance\n                instance.data[\"representations\"].append(new_repre)\n\n                add_repre_files_for_cleanup(instance, new_repre)\n\n            # Cleanup temp staging dir after procesisng of output definitions\n            if do_convert:\n                temp_dir = repre[\"stagingDir\"]\n                shutil.rmtree(temp_dir)\n                # Set staging dir of source representation back to previous\n                #   value\n                repre[\"stagingDir\"] = src_repre_staging_dir\n\n            # Remove source representation\n            # NOTE we maybe can keep source representation if necessary\n            instance.data[\"representations\"].remove(repre)\n\n            self.log.debug(\"Files to delete: {}\".format(files_to_delete))\n\n            # Delete input files\n            for filepath in files_to_delete:\n                if os.path.exists(filepath):\n                    os.remove(filepath)\n                    self.log.debug(\"Removed: \\\"{}\\\"\".format(filepath))\n\n    def _get_burnin_options(self):\n        # Prepare burnin options\n        burnin_options = copy.deepcopy(self.default_options)\n        if self.options:\n            for key, value in self.options.items():\n                if value is not None:\n                    burnin_options[key] = copy.deepcopy(value)\n\n        # Convert colors defined as list of numbers RGBA (0-255)\n        # BG Color\n        bg_color = burnin_options.get(\"bg_color\")\n        if bg_color and isinstance(bg_color, list):\n            bg_red, bg_green, bg_blue, bg_alpha = bg_color\n            bg_color_hex = \"#{0:0>2X}{1:0>2X}{2:0>2X}\".format(\n                bg_red, bg_green, bg_blue\n            )\n            bg_color_alpha = float(bg_alpha) / 255\n            burnin_options[\"bg_opacity\"] = bg_color_alpha\n            burnin_options[\"bg_color\"] = bg_color_hex\n\n        # FG Color\n        font_color = burnin_options.get(\"font_color\")\n        if font_color and isinstance(font_color, list):\n            fg_red, fg_green, fg_blue, fg_alpha = font_color\n            fg_color_hex = \"#{0:0>2X}{1:0>2X}{2:0>2X}\".format(\n                fg_red, fg_green, fg_blue\n            )\n            fg_color_alpha = float(fg_alpha) / 255\n            burnin_options[\"opacity\"] = fg_color_alpha\n            burnin_options[\"font_color\"] = fg_color_hex\n\n        # Define font filepath\n        # - font filepath may be defined in settings\n        font_filepath = burnin_options.get(\"font_filepath\")\n        if font_filepath and isinstance(font_filepath, dict):\n            sys_name = platform.system().lower()\n            font_filepath = font_filepath.get(sys_name)\n\n        if font_filepath and isinstance(font_filepath, six.string_types):\n            font_filepath = font_filepath.format(**os.environ)\n            if not os.path.exists(font_filepath):\n                font_filepath = None\n\n        # Use OpenPype default font\n        if not font_filepath:\n            font_filepath = resources.get_liberation_font_path()\n\n        burnin_options[\"font\"] = font_filepath\n\n        return burnin_options\n\n    def prepare_basic_data(self, instance):\n        \"\"\"Pick data from instance for processing and for burnin strings.\n\n        Args:\n            instance (Instance): Currently processed instance.\n\n        Returns:\n            tuple: `(burnin_data, temp_data)` - `burnin_data` contain data for\n                filling burnin strings. `temp_data` are for repre pre-process\n                preparation.\n        \"\"\"\n        self.log.debug(\"Preparing basic data for burnins\")\n        context = instance.context\n\n        version = instance.data.get(\"version\")\n        if version is None:\n            version = context.data.get(\"version\")\n\n        frame_start = instance.data.get(\"frameStart\")\n        if frame_start is None:\n            self.log.warning(\n                \"Key \\\"frameStart\\\" is not set. Setting to \\\"0\\\".\"\n            )\n            frame_start = 0\n        frame_start = int(frame_start)\n\n        frame_end = instance.data.get(\"frameEnd\")\n        if frame_end is None:\n            self.log.warning(\n                \"Key \\\"frameEnd\\\" is not set. Setting to \\\"1\\\".\"\n            )\n            frame_end = 1\n        frame_end = int(frame_end)\n\n        handle_start = instance.data.get(\"handleStart\")\n        if handle_start is None:\n            handle_start = context.data.get(\"handleStart\") or 0\n\n        handle_end = instance.data.get(\"handleEnd\")\n        if handle_end is None:\n            handle_end = context.data.get(\"handleEnd\") or 0\n\n        frame_start_handle = frame_start - handle_start\n        frame_end_handle = frame_end + handle_end\n\n        burnin_data = copy.deepcopy(instance.data[\"anatomyData\"])\n\n        if \"slate.farm\" in instance.data[\"families\"]:\n            frame_start_handle += 1\n\n        burnin_data.update({\n            \"version\": int(version),\n            \"comment\": instance.data[\"comment\"]\n        })\n\n        intent_label = context.data.get(\"intent\") or \"\"\n        if intent_label and isinstance(intent_label, dict):\n            value = intent_label.get(\"value\")\n            if value:\n                intent_label = intent_label[\"label\"]\n            else:\n                intent_label = \"\"\n\n        burnin_data[\"intent\"] = intent_label\n\n        temp_data = {\n            \"frame_start\": frame_start,\n            \"frame_end\": frame_end,\n            \"frame_start_handle\": frame_start_handle,\n            \"frame_end_handle\": frame_end_handle\n        }\n\n        self.log.debug(\n            \"Basic burnin_data: {}\".format(json.dumps(burnin_data, indent=4))\n        )\n\n        return burnin_data, temp_data\n\n    def repres_is_valid(self, repre):\n        \"\"\"Validation if representaion should be processed.\n\n        Args:\n            repre (dict): Representation which should be checked.\n\n        Returns:\n            bool: False if can't be processed else True.\n        \"\"\"\n\n        if \"burnin\" not in (repre.get(\"tags\") or []):\n            self.log.debug((\n                \"Representation \\\"{}\\\" does not have \\\"burnin\\\" tag. Skipped.\"\n            ).format(repre[\"name\"]))\n            return False\n\n        if not repre.get(\"files\"):\n            self.log.warning((\n                \"Representation \\\"{}\\\" have empty files. Skipped.\"\n            ).format(repre[\"name\"]))\n            return False\n        return True\n\n    def filter_burnins_by_tags(self, burnin_defs, tags):\n        \"\"\"Filter burnin definitions by entered representation tags.\n\n        Burnin definitions without tags filter are marked as valid.\n\n        Args:\n            outputs (list): Contain list of burnin definitions from presets.\n            tags (list): Tags of processed representation.\n\n        Returns:\n            list: Containg all burnin definitions matching entered tags.\n        \"\"\"\n        filtered_burnins = {}\n        repre_tags_low = set(tag.lower() for tag in tags)\n        for filename_suffix, burnin_def in burnin_defs.items():\n            valid = True\n            tag_filters = burnin_def[\"filter\"][\"tags\"]\n            if tag_filters:\n                # Check tag filters\n                tag_filters_low = set(tag.lower() for tag in tag_filters)\n\n                valid = bool(repre_tags_low & tag_filters_low)\n\n            if valid:\n                filtered_burnins[filename_suffix] = burnin_def\n\n        return filtered_burnins\n\n    def input_output_paths(\n        self, src_repre, new_repre, temp_data, filename_suffix\n    ):\n        \"\"\"Prepare input and output paths for representation.\n\n        Store data to `temp_data` for keys \"full_input_path\" which is full path\n        to source files optionally with sequence formatting,\n        \"full_output_path\" full path to otput with optionally with sequence\n        formatting, \"full_input_paths\" list of all source files which will be\n        deleted when burnin script ends, \"repre_files\" list of output\n        filenames.\n\n        Args:\n            new_repre (dict): Currently processed new representation.\n            temp_data (dict): Temp data of representation process.\n            filename_suffix (str): Filename suffix added to inputl filename.\n\n        Returns:\n            None: This is processing method.\n        \"\"\"\n        # TODO we should find better way to know if input is sequence\n        input_filenames = new_repre[\"files\"]\n        is_sequence = False\n        if isinstance(input_filenames, (tuple, list)):\n            if len(input_filenames) > 1:\n                is_sequence = True\n\n        # Sequence must have defined first frame\n        # - not used if input is not a sequence\n        first_frame = None\n        if is_sequence:\n            collections, _ = clique.assemble(input_filenames)\n            if not collections:\n                is_sequence = False\n            else:\n                input_filename = new_repre[\"sequence_file\"]\n                collection = collections[0]\n                indexes = list(collection.indexes)\n                padding = len(str(max(indexes)))\n                head = collection.format(\"{head}\")\n                tail = collection.format(\"{tail}\")\n                output_filename = \"{}%{:0>2}d{}{}\".format(\n                    head, padding, filename_suffix, tail\n                )\n                repre_files = []\n                for idx in indexes:\n                    repre_files.append(output_filename % idx)\n\n                first_frame = min(indexes)\n\n        if not is_sequence:\n            input_filename = input_filenames\n            if isinstance(input_filename, (tuple, list)):\n                input_filename = input_filename[0]\n\n            filepart_start, ext = os.path.splitext(input_filename)\n            dir_path, basename = os.path.split(filepart_start)\n            output_filename = basename + filename_suffix + ext\n            if dir_path:\n                output_filename = os.path.join(dir_path, output_filename)\n\n            repre_files = output_filename\n\n        src_stagingdir = src_repre[\"stagingDir\"]\n        dst_stagingdir = new_repre[\"stagingDir\"]\n        full_input_path = os.path.join(\n            os.path.normpath(src_stagingdir), input_filename\n        ).replace(\"\\\\\", \"/\")\n        full_output_path = os.path.join(\n            os.path.normpath(dst_stagingdir), output_filename\n        ).replace(\"\\\\\", \"/\")\n\n        temp_data[\"full_input_path\"] = full_input_path\n        temp_data[\"full_output_path\"] = full_output_path\n        temp_data[\"first_frame\"] = first_frame\n\n        new_repre[\"files\"] = repre_files\n\n        self.log.debug(\"full_input_path: {}\".format(full_input_path))\n        self.log.debug(\"full_output_path: {}\".format(full_output_path))\n\n        # Prepare full paths to input files and filenames for reprensetation\n        full_input_paths = []\n        if is_sequence:\n            for filename in input_filenames:\n                filepath = os.path.join(\n                    os.path.normpath(src_stagingdir), filename\n                ).replace(\"\\\\\", \"/\")\n                full_input_paths.append(filepath)\n\n        else:\n            full_input_paths.append(full_input_path)\n\n        temp_data[\"full_input_paths\"] = full_input_paths\n\n    def prepare_repre_data(self, instance, repre, burnin_data, temp_data):\n        \"\"\"Prepare data for representation.\n\n        Args:\n            instance (Instance): Currently processed Instance.\n            repre (dict): Currently processed representation.\n            burnin_data (dict): Copy of basic burnin data based on instance\n                data.\n            temp_data (dict): Copy of basic temp data\n        \"\"\"\n        # Add representation name to burnin data\n        burnin_data[\"representation\"] = repre[\"name\"]\n\n        # no handles switch from profile tags\n        if \"no-handles\" in repre[\"tags\"]:\n            burnin_frame_start = temp_data[\"frame_start\"]\n            burnin_frame_end = temp_data[\"frame_end\"]\n\n        else:\n            burnin_frame_start = temp_data[\"frame_start_handle\"]\n            burnin_frame_end = temp_data[\"frame_end_handle\"]\n\n        burnin_duration = burnin_frame_end - burnin_frame_start + 1\n\n        burnin_data.update({\n            \"frame_start\": burnin_frame_start,\n            \"frame_end\": burnin_frame_end,\n            \"duration\": burnin_duration,\n        })\n        temp_data[\"duration\"] = burnin_duration\n\n        # Add values for slate frames\n        burnin_slate_frame_start = burnin_frame_start\n\n        # Move frame start by 1 frame when slate is used.\n        if (\n            \"slate\" in instance.data[\"families\"]\n            and \"slate-frame\" in repre[\"tags\"]\n        ):\n            burnin_slate_frame_start -= 1\n\n        self.log.debug(\"burnin_slate_frame_start: {}\".format(\n            burnin_slate_frame_start\n        ))\n\n        burnin_data.update({\n            \"slate_frame_start\": burnin_slate_frame_start,\n            \"slate_frame_end\": burnin_frame_end,\n            \"slate_duration\": (\n                burnin_frame_end - burnin_slate_frame_start + 1\n            )\n        })\n\n    def filter_burnins_defs(self, profile, instance):\n        \"\"\"Filter outputs by their values from settings.\n\n        Output definitions with at least one value are marked as valid.\n\n        Args:\n            profile (dict): Profile from presets matching current context.\n\n        Returns:\n            list: Containg all valid output definitions.\n        \"\"\"\n        filtered_burnin_defs = {}\n\n        burnin_defs = profile.get(\"burnins\")\n        if not burnin_defs:\n            return filtered_burnin_defs\n\n        families = self.families_from_instance(instance)\n\n        for filename_suffix, orig_burnin_def in burnin_defs.items():\n            burnin_def = copy.deepcopy(orig_burnin_def)\n            def_filter = burnin_def.get(\"filter\", None) or {}\n            for key in (\"families\", \"tags\"):\n                if key not in def_filter:\n                    def_filter[key] = []\n\n            families_filters = def_filter[\"families\"]\n            if not self.families_filter_validation(\n                families, families_filters\n            ):\n                self.log.debug((\n                    \"Skipped burnin definition \\\"{}\\\". Family\"\n                    \" fiters ({}) does not match current instance families: {}\"\n                ).format(\n                    filename_suffix, str(families_filters), str(families)\n                ))\n                continue\n\n            # Burnin values\n            burnin_values = {}\n            for key, value in tuple(burnin_def.items()):\n                key_low = key.lower()\n                if key_low in self.positions and value:\n                    burnin_values[key_low] = value\n\n            # Skip processing if burnin values are not set\n            if not burnin_values:\n                self.log.warning((\n                    \"Burnin values for Burnin definition \\\"{}\\\"\"\n                    \" are not filled. Definition will be skipped.\"\n                    \" Origin value: {}\"\n                ).format(filename_suffix, str(orig_burnin_def)))\n                continue\n\n            burnin_values[\"filter\"] = def_filter\n\n            filtered_burnin_defs[filename_suffix] = burnin_values\n\n            self.log.debug((\n                \"Burnin definition \\\"{}\\\" passed first filtering.\"\n            ).format(filename_suffix))\n\n        return filtered_burnin_defs\n\n    def families_filter_validation(self, families, output_families_filter):\n        \"\"\"Determines if entered families intersect with families filters.\n\n        All family values are lowered to avoid unexpected results.\n        \"\"\"\n\n        families_filter_lower = set(family.lower() for family in\n                                    output_families_filter\n                                    # Exclude empty filter values\n                                    if family)\n        if not families_filter_lower:\n            return True\n        return any(family.lower() in families_filter_lower\n                   for family in families)\n\n    def families_from_instance(self, instance):\n        \"\"\"Return all families of entered instance.\"\"\"\n        families = []\n        family = instance.data.get(\"family\")\n        if family:\n            families.append(family)\n\n        for family in (instance.data.get(\"families\") or tuple()):\n            if family not in families:\n                families.append(family)\n        return families\n\n    def burnin_script_path(self):\n        \"\"\"Return path to python script for burnin processing.\"\"\"\n        scriptpath = os.path.normpath(\n            os.path.join(\n                PACKAGE_DIR,\n                \"scripts\",\n                \"otio_burnin.py\"\n            )\n        )\n\n        self.log.debug(\"scriptpath: {}\".format(scriptpath))\n\n        return scriptpath\n"
  },
  {
    "path": "openpype/plugins/publish/extract_color_transcode.py",
    "content": "import os\nimport copy\nimport clique\nimport pyblish.api\n\nfrom openpype.pipeline import publish\nfrom openpype.lib import (\n\n    is_oiio_supported,\n)\n\nfrom openpype.lib.transcoding import (\n    convert_colorspace,\n    get_transcode_temp_directory,\n)\n\nfrom openpype.lib.profiles_filtering import filter_profiles\n\n\nclass ExtractOIIOTranscode(publish.Extractor):\n    \"\"\"\n    Extractor to convert colors from one colorspace to different.\n\n    Expects \"colorspaceData\" on representation. This dictionary is collected\n    previously and denotes that representation files should be converted.\n    This dict contains source colorspace information, collected by hosts.\n\n    Target colorspace is selected by profiles in the Settings, based on:\n    - families\n    - host\n    - task types\n    - task names\n    - subset names\n\n    Can produce one or more representations (with different extensions) based\n    on output definition in format:\n        \"output_name: {\n            \"extension\": \"png\",\n            \"colorspace\": \"ACES - ACEScg\",\n            \"display\": \"\",\n            \"view\": \"\",\n            \"tags\": [],\n            \"custom_tags\": []\n        }\n\n    If 'extension' is empty original representation extension is used.\n    'output_name' will be used as name of new representation. In case of value\n        'passthrough' name of original representation will be used.\n\n    'colorspace' denotes target colorspace to be transcoded into. Could be\n    empty if transcoding should be only into display and viewer colorspace.\n    (In that case both 'display' and 'view' must be filled.)\n    \"\"\"\n\n    label = \"Transcode color spaces\"\n    order = pyblish.api.ExtractorOrder + 0.019\n\n    optional = True\n\n    # Supported extensions\n    supported_exts = [\"exr\", \"jpg\", \"jpeg\", \"png\", \"dpx\"]\n\n    # Configurable by Settings\n    profiles = None\n    options = None\n\n    def process(self, instance):\n        if not self.profiles:\n            self.log.debug(\"No profiles present for color transcode\")\n            return\n\n        if \"representations\" not in instance.data:\n            self.log.debug(\"No representations, skipping.\")\n            return\n\n        if not is_oiio_supported():\n            self.log.warning(\"OIIO not supported, no transcoding possible.\")\n            return\n\n        profile = self._get_profile(instance)\n        if not profile:\n            return\n\n        new_representations = []\n        repres = instance.data[\"representations\"]\n        for idx, repre in enumerate(list(repres)):\n            self.log.debug(\"repre ({}): `{}`\".format(idx + 1, repre[\"name\"]))\n            if not self._repre_is_valid(repre):\n                continue\n\n            added_representations = False\n            added_review = False\n\n            colorspace_data = repre[\"colorspaceData\"]\n            source_colorspace = colorspace_data[\"colorspace\"]\n            config_path = colorspace_data.get(\"config\", {}).get(\"path\")\n            if not config_path or not os.path.exists(config_path):\n                self.log.warning(\"Config file doesn't exist, skipping\")\n                continue\n\n            for output_name, output_def in profile.get(\"outputs\", {}).items():\n                new_repre = copy.deepcopy(repre)\n\n                original_staging_dir = new_repre[\"stagingDir\"]\n                new_staging_dir = get_transcode_temp_directory()\n                new_repre[\"stagingDir\"] = new_staging_dir\n\n                if isinstance(new_repre[\"files\"], list):\n                    files_to_convert = copy.deepcopy(new_repre[\"files\"])\n                else:\n                    files_to_convert = [new_repre[\"files\"]]\n\n                output_extension = output_def[\"extension\"]\n                output_extension = output_extension.replace('.', '')\n                self._rename_in_representation(new_repre,\n                                               files_to_convert,\n                                               output_name,\n                                               output_extension)\n\n                transcoding_type = output_def[\"transcoding_type\"]\n\n                target_colorspace = view = display = None\n                if transcoding_type == \"colorspace\":\n                    target_colorspace = (output_def[\"colorspace\"] or\n                                         colorspace_data.get(\"colorspace\"))\n                else:\n                    view = output_def[\"view\"] or colorspace_data.get(\"view\")\n                    display = (output_def[\"display\"] or\n                               colorspace_data.get(\"display\"))\n\n                # both could be already collected by DCC,\n                # but could be overwritten when transcoding\n                if view:\n                    new_repre[\"colorspaceData\"][\"view\"] = view\n                if display:\n                    new_repre[\"colorspaceData\"][\"display\"] = display\n                if target_colorspace:\n                    new_repre[\"colorspaceData\"][\"colorspace\"] = \\\n                        target_colorspace\n\n                additional_command_args = (output_def[\"oiiotool_args\"]\n                                           [\"additional_command_args\"])\n\n                files_to_convert = self._translate_to_sequence(\n                    files_to_convert)\n                for file_name in files_to_convert:\n                    input_path = os.path.join(original_staging_dir,\n                                              file_name)\n                    output_path = self._get_output_file_path(input_path,\n                                                             new_staging_dir,\n                                                             output_extension)\n                    convert_colorspace(\n                        input_path,\n                        output_path,\n                        config_path,\n                        source_colorspace,\n                        target_colorspace,\n                        view,\n                        display,\n                        additional_command_args,\n                        self.log\n                    )\n\n                # cleanup temporary transcoded files\n                for file_name in new_repre[\"files\"]:\n                    transcoded_file_path = os.path.join(new_staging_dir,\n                                                        file_name)\n                    instance.context.data[\"cleanupFullPaths\"].append(\n                        transcoded_file_path)\n\n                custom_tags = output_def.get(\"custom_tags\")\n                if custom_tags:\n                    if new_repre.get(\"custom_tags\") is None:\n                        new_repre[\"custom_tags\"] = []\n                    new_repre[\"custom_tags\"].extend(custom_tags)\n\n                # Add additional tags from output definition to representation\n                if new_repre.get(\"tags\") is None:\n                    new_repre[\"tags\"] = []\n                for tag in output_def[\"tags\"]:\n                    if tag not in new_repre[\"tags\"]:\n                        new_repre[\"tags\"].append(tag)\n\n                    if tag == \"review\":\n                        added_review = True\n\n                # If there is only 1 file outputted then convert list to\n                # string, cause that'll indicate that its not a sequence.\n                if len(new_repre[\"files\"]) == 1:\n                    new_repre[\"files\"] = new_repre[\"files\"][0]\n\n                # If the source representation has \"review\" tag, but its not\n                # part of the output defintion tags, then both the\n                # representations will be transcoded in ExtractReview and\n                # their outputs will clash in integration.\n                if \"review\" in repre.get(\"tags\", []):\n                    added_review = True\n\n                new_representations.append(new_repre)\n                added_representations = True\n\n            if added_representations:\n                self._mark_original_repre_for_deletion(repre, profile,\n                                                       added_review)\n\n        for repre in tuple(instance.data[\"representations\"]):\n            tags = repre.get(\"tags\") or []\n            if \"delete\" in tags and \"thumbnail\" not in tags:\n                instance.data[\"representations\"].remove(repre)\n\n        instance.data[\"representations\"].extend(new_representations)\n\n    def _rename_in_representation(self, new_repre, files_to_convert,\n                                  output_name, output_extension):\n        \"\"\"Replace old extension with new one everywhere in representation.\n\n        Args:\n            new_repre (dict)\n            files_to_convert (list): of filenames from repre[\"files\"],\n                standardized to always list\n            output_name (str): key of output definition from Settings,\n                if \"<passthrough>\" token used, keep original repre name\n            output_extension (str): extension from output definition\n        \"\"\"\n        if output_name != \"passthrough\":\n            new_repre[\"name\"] = output_name\n        if not output_extension:\n            return\n\n        new_repre[\"ext\"] = output_extension\n\n        renamed_files = []\n        for file_name in files_to_convert:\n            file_name, _ = os.path.splitext(file_name)\n            file_name = '{}.{}'.format(file_name,\n                                       output_extension)\n            renamed_files.append(file_name)\n        new_repre[\"files\"] = renamed_files\n\n    def _rename_in_representation(self, new_repre, files_to_convert,\n                                  output_name, output_extension):\n        \"\"\"Replace old extension with new one everywhere in representation.\n\n        Args:\n            new_repre (dict)\n            files_to_convert (list): of filenames from repre[\"files\"],\n                standardized to always list\n            output_name (str): key of output definition from Settings,\n                if \"<passthrough>\" token used, keep original repre name\n            output_extension (str): extension from output definition\n        \"\"\"\n        if output_name != \"passthrough\":\n            new_repre[\"name\"] = output_name\n        if not output_extension:\n            return\n\n        new_repre[\"ext\"] = output_extension\n\n        renamed_files = []\n        for file_name in files_to_convert:\n            file_name, _ = os.path.splitext(file_name)\n            file_name = '{}.{}'.format(file_name,\n                                       output_extension)\n            renamed_files.append(file_name)\n        new_repre[\"files\"] = renamed_files\n\n    def _translate_to_sequence(self, files_to_convert):\n        \"\"\"Returns original list or list with filename formatted in single\n        sequence format.\n\n        Uses clique to find frame sequence, in this case it merges all frames\n        into sequence format (FRAMESTART-FRAMEEND#) and returns it.\n        If sequence not found, it returns original list\n\n        Args:\n            files_to_convert (list): list of file names\n        Returns:\n            (list) of [file.1001-1010#.exr] or [fileA.exr, fileB.exr]\n        \"\"\"\n        pattern = [clique.PATTERNS[\"frames\"]]\n        collections, remainder = clique.assemble(\n            files_to_convert, patterns=pattern,\n            assume_padded_when_ambiguous=True)\n\n        if collections:\n            if len(collections) > 1:\n                raise ValueError(\n                    \"Too many collections {}\".format(collections))\n\n            collection = collections[0]\n            frames = list(collection.indexes)\n            frame_str = \"{}-{}#\".format(frames[0], frames[-1])\n            file_name = \"{}{}{}\".format(collection.head, frame_str,\n                                        collection.tail)\n\n            files_to_convert = [file_name]\n\n        return files_to_convert\n\n    def _get_output_file_path(self, input_path, output_dir,\n                              output_extension):\n        \"\"\"Create output file name path.\"\"\"\n        file_name = os.path.basename(input_path)\n        file_name, input_extension = os.path.splitext(file_name)\n        if not output_extension:\n            output_extension = input_extension.replace(\".\", \"\")\n        new_file_name = '{}.{}'.format(file_name,\n                                       output_extension)\n        return os.path.join(output_dir, new_file_name)\n\n    def _get_profile(self, instance):\n        \"\"\"Returns profile if and how repre should be color transcoded.\"\"\"\n        host_name = instance.context.data[\"hostName\"]\n        family = instance.data[\"family\"]\n        task_data = instance.data[\"anatomyData\"].get(\"task\", {})\n        task_name = task_data.get(\"name\")\n        task_type = task_data.get(\"type\")\n        subset = instance.data[\"subset\"]\n        filtering_criteria = {\n            \"hosts\": host_name,\n            \"families\": family,\n            \"task_names\": task_name,\n            \"task_types\": task_type,\n            \"subsets\": subset\n        }\n        profile = filter_profiles(self.profiles, filtering_criteria,\n                                  logger=self.log)\n\n        if not profile:\n            self.log.debug((\n              \"Skipped instance. None of profiles in presets are for\"\n              \" Host: \\\"{}\\\" | Families: \\\"{}\\\" | Task \\\"{}\\\"\"\n              \" | Task type \\\"{}\\\" | Subset \\\"{}\\\" \"\n            ).format(host_name, family, task_name, task_type, subset))\n\n        return profile\n\n    def _repre_is_valid(self, repre):\n        \"\"\"Validation if representation should be processed.\n\n        Args:\n            repre (dict): Representation which should be checked.\n\n        Returns:\n            bool: False if can't be processed else True.\n        \"\"\"\n\n        if repre.get(\"ext\") not in self.supported_exts:\n            self.log.debug((\n                \"Representation '{}' has unsupported extension: '{}'. Skipped.\"\n            ).format(repre[\"name\"], repre.get(\"ext\")))\n            return False\n\n        if not repre.get(\"files\"):\n            self.log.debug((\n                \"Representation '{}' has empty files. Skipped.\"\n            ).format(repre[\"name\"]))\n            return False\n\n        if not repre.get(\"colorspaceData\"):\n            self.log.debug(\"Representation '{}' has no colorspace data. \"\n                           \"Skipped.\")\n            return False\n\n        return True\n\n    def _mark_original_repre_for_deletion(self, repre, profile, added_review):\n        \"\"\"If new transcoded representation created, delete old.\"\"\"\n        if not repre.get(\"tags\"):\n            repre[\"tags\"] = []\n\n        delete_original = profile[\"delete_original\"]\n\n        if delete_original:\n            if \"delete\" not in repre[\"tags\"]:\n                repre[\"tags\"].append(\"delete\")\n\n        if added_review and \"review\" in repre[\"tags\"]:\n            repre[\"tags\"].remove(\"review\")\n"
  },
  {
    "path": "openpype/plugins/publish/extract_colorspace_data.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import publish\n\n\nclass ExtractColorspaceData(publish.Extractor,\n                            publish.ColormanagedPyblishPluginMixin):\n    \"\"\" Inject Colorspace data to available representations.\n\n    Input data:\n    - context.data[colorspace_config_path]:\n        for anatomy formatting of possible template tokens in config path\n    - context.data[colorspace_config_path]:\n        for resolving project and host related config.ocio\n    - context.data[colorspace_file_rules]:\n        for resolving matched file rule from representation file name\n        and adding it to representation\n\n    Output data:\n        representation[colorspaceData] = {\n            \"colorspace\": \"linear\",\n            \"config\": {\n                \"path\": \"/abs/path/to/config.ocio\",\n                \"template\": \"{project[root]}/path/to/config.ocio\"\n            }\n        }\n    \"\"\"\n    label = \"Extract Colorspace data\"\n    order = pyblish.api.ExtractorOrder + 0.49\n\n    def process(self, instance):\n        representations = instance.data.get(\"representations\")\n        if not representations:\n            self.log.debug(\"No representations at instance : `{}`\".format(\n                instance))\n            return\n\n        # get colorspace settings\n        context = instance.context\n\n        # loop representations\n        for representation in representations:\n            # skip if colorspaceData is already at representation\n            if representation.get(\"colorspaceData\"):\n                continue\n\n            self.set_representation_colorspace(\n                representation, context\n            )\n"
  },
  {
    "path": "openpype/plugins/publish/extract_hierarchy_avalon.py",
    "content": "import collections\nfrom copy import deepcopy\nimport pyblish.api\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_assets,\n    get_archived_assets\n)\nfrom openpype.pipeline import legacy_io\n\n\nclass ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):\n    \"\"\"Create entities in Avalon based on collected data.\"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.01\n    label = \"Extract Hierarchy To Avalon\"\n    families = [\"clip\", \"shot\"]\n\n    def process(self, context):\n        if AYON_SERVER_ENABLED:\n            return\n\n        if \"hierarchyContext\" not in context.data:\n            self.log.debug(\"skipping ExtractHierarchyToAvalon\")\n            return\n\n        if not legacy_io.Session:\n            legacy_io.install()\n\n        hierarchy_context = self._get_active_assets(context)\n        self.log.debug(\"__ hierarchy_context: {}\".format(hierarchy_context))\n\n        project_name = context.data[\"projectName\"]\n        asset_names = self.extract_asset_names(hierarchy_context)\n\n        asset_docs_by_name = {}\n        for asset_doc in get_assets(project_name, asset_names=asset_names):\n            name = asset_doc[\"name\"]\n            asset_docs_by_name[name] = asset_doc\n\n        archived_asset_docs_by_name = collections.defaultdict(list)\n        for asset_doc in get_archived_assets(\n            project_name, asset_names=asset_names\n        ):\n            name = asset_doc[\"name\"]\n            archived_asset_docs_by_name[name].append(asset_doc)\n\n        project_doc = None\n        hierarchy_queue = collections.deque()\n        for name, data in hierarchy_context.items():\n            hierarchy_queue.append((name, data, None))\n\n        while hierarchy_queue:\n            item = hierarchy_queue.popleft()\n            name, entity_data, parent = item\n\n            entity_type = entity_data[\"entity_type\"]\n            if entity_type.lower() == \"project\":\n                new_parent = project_doc = self.sync_project(\n                    context,\n                    entity_data\n                )\n\n            else:\n                new_parent = self.sync_asset(\n                    name,\n                    entity_data,\n                    parent,\n                    project_doc,\n                    asset_docs_by_name,\n                    archived_asset_docs_by_name\n                )\n                # make sure all relative instances have correct avalon data\n                self._set_avalon_data_to_relative_instances(\n                    context,\n                    project_name,\n                    new_parent\n                )\n\n            children = entity_data.get(\"childs\")\n            if not children:\n                continue\n\n            for child_name, child_data in children.items():\n                hierarchy_queue.append((child_name, child_data, new_parent))\n\n    def extract_asset_names(self, hierarchy_context):\n        \"\"\"Extract all possible asset names from hierarchy context.\n\n        Args:\n            hierarchy_context (Dict[str, Any]): Nested hierarchy structure.\n\n        Returns:\n            Set[str]: All asset names from the hierarchy structure.\n        \"\"\"\n\n        hierarchy_queue = collections.deque()\n        for name, data in hierarchy_context.items():\n            hierarchy_queue.append((name, data))\n\n        asset_names = set()\n        while hierarchy_queue:\n            item = hierarchy_queue.popleft()\n            name, data = item\n            if data[\"entity_type\"].lower() != \"project\":\n                asset_names.add(name)\n\n            children = data.get(\"childs\")\n            if children:\n                for child_name, child_data in children.items():\n                    hierarchy_queue.append((child_name, child_data))\n        return asset_names\n\n    def sync_project(self, context, entity_data):\n        project_doc = context.data[\"projectEntity\"]\n\n        if \"data\" not in project_doc:\n            project_doc[\"data\"] = {}\n        current_data = project_doc[\"data\"]\n\n        changes = {}\n        entity_type = entity_data[\"entity_type\"]\n        if current_data.get(\"entityType\") != entity_type:\n            changes[\"entityType\"] = entity_type\n\n        # Custom attributes.\n        attributes = entity_data.get(\"custom_attributes\") or {}\n        for key, value in attributes.items():\n            if key not in current_data or current_data[key] != value:\n                update_key = \"data.{}\".format(key)\n                changes[update_key] = value\n                current_data[key] = value\n\n        if changes:\n            # Update entity data with input data\n            legacy_io.update_one(\n                {\"_id\": project_doc[\"_id\"]},\n                {\"$set\": changes}\n            )\n        return project_doc\n\n    def _prepare_new_tasks(self, asset_doc, entity_data):\n        new_tasks = entity_data.get(\"tasks\") or {}\n        if not asset_doc:\n            return new_tasks\n\n        old_tasks = asset_doc.get(\"data\", {}).get(\"tasks\")\n        # Just use new tasks if old are not available\n        if not old_tasks:\n            return new_tasks\n\n        output = deepcopy(old_tasks)\n        # Create mapping of lowered task names from old tasks\n        cur_task_low_mapping = {\n            task_name.lower(): task_name\n            for task_name in old_tasks\n        }\n        # Add/update tasks from new entity data\n        for task_name, task_info in new_tasks.items():\n            task_info = deepcopy(task_info)\n            task_name_low = task_name.lower()\n            # Add new task\n            if task_name_low not in cur_task_low_mapping:\n                output[task_name] = task_info\n                continue\n\n            # Update existing task with new info\n            mapped_task_name = cur_task_low_mapping.pop(task_name_low)\n            src_task_info = output.pop(mapped_task_name)\n            src_task_info.update(task_info)\n            output[task_name] = src_task_info\n        return output\n\n    def sync_asset(\n        self,\n        asset_name,\n        entity_data,\n        parent,\n        project,\n        asset_docs_by_name,\n        archived_asset_docs_by_name\n    ):\n        # Prepare data for new asset or for update comparison\n        data = {\n            \"entityType\": entity_data[\"entity_type\"]\n        }\n\n        # Custom attributes.\n        attributes = entity_data.get(\"custom_attributes\") or {}\n        for key, value in attributes.items():\n            data[key] = value\n\n        data[\"inputs\"] = entity_data.get(\"inputs\") or []\n\n        # Parents and visual parent are empty if parent is project\n        parents = []\n        parent_id = None\n        if project[\"_id\"] != parent[\"_id\"]:\n            parent_id = parent[\"_id\"]\n            # Use parent's parents as source value\n            parents.extend(parent[\"data\"][\"parents\"])\n            # Add parent's name to parents\n            parents.append(parent[\"name\"])\n\n        data[\"visualParent\"] = parent_id\n        data[\"parents\"] = parents\n\n        asset_doc = asset_docs_by_name.get(asset_name)\n\n        # Tasks\n        data[\"tasks\"] = self._prepare_new_tasks(asset_doc, entity_data)\n\n        # --- Create/Unarchive asset and end ---\n        if not asset_doc:\n            archived_asset_doc = None\n            for archived_entity in archived_asset_docs_by_name[asset_name]:\n                archived_parents = (\n                    archived_entity\n                    .get(\"data\", {})\n                    .get(\"parents\")\n                )\n                if data[\"parents\"] == archived_parents:\n                    archived_asset_doc = archived_entity\n                    break\n\n            # Create entity if doesn't exist\n            if archived_asset_doc is None:\n                return self.create_avalon_asset(\n                    asset_name, data, project\n                )\n\n            return self.unarchive_entity(\n                archived_asset_doc, data, project\n            )\n\n        # --- Update existing asset ---\n        # Make sure current entity has \"data\" key\n        if \"data\" not in asset_doc:\n            asset_doc[\"data\"] = {}\n        cur_entity_data = asset_doc[\"data\"]\n\n        changes = {}\n        for key, value in data.items():\n            if key not in cur_entity_data or value != cur_entity_data[key]:\n                update_key = \"data.{}\".format(key)\n                changes[update_key] = value\n                cur_entity_data[key] = value\n\n        # Update asset in database if necessary\n        if changes:\n            # Update entity data with input data\n            legacy_io.update_one(\n                {\"_id\": asset_doc[\"_id\"]},\n                {\"$set\": changes}\n            )\n        return asset_doc\n\n    def unarchive_entity(self, archived_doc, data, project):\n        # Unarchived asset should not use same data\n        asset_doc = {\n            \"_id\": archived_doc[\"_id\"],\n            \"schema\": \"openpype:asset-3.0\",\n            \"name\": archived_doc[\"name\"],\n            \"parent\": project[\"_id\"],\n            \"type\": \"asset\",\n            \"data\": data\n        }\n        legacy_io.replace_one(\n            {\"_id\": archived_doc[\"_id\"]},\n            asset_doc\n        )\n\n        return asset_doc\n\n    def create_avalon_asset(self, name, data, project):\n        asset_doc = {\n            \"schema\": \"openpype:asset-3.0\",\n            \"name\": name,\n            \"parent\": project[\"_id\"],\n            \"type\": \"asset\",\n            \"data\": data\n        }\n        self.log.debug(\"Creating asset: {}\".format(asset_doc))\n        asset_doc[\"_id\"] = legacy_io.insert_one(asset_doc).inserted_id\n\n        return asset_doc\n\n    def _set_avalon_data_to_relative_instances(\n        self,\n        context,\n        project_name,\n        asset_doc\n    ):\n        asset_name = asset_doc[\"name\"]\n        new_parents = asset_doc[\"data\"][\"parents\"]\n        hierarchy = \"/\".join(new_parents)\n        parent_name = project_name\n        if new_parents:\n            parent_name = new_parents[-1]\n\n        for instance in context:\n            # Skip if instance asset does not match\n            instance_asset_name = instance.data.get(\"asset\")\n            if asset_name != instance_asset_name:\n                continue\n\n            instance_asset_doc = instance.data.get(\"assetEntity\")\n            # Update asset entity with new possible changes of asset document\n            instance.data[\"assetEntity\"] = asset_doc\n\n            # Update anatomy data if asset was not set on instance\n            if not instance_asset_doc:\n                instance.data[\"anatomyData\"].update({\n                    \"hierarchy\": hierarchy,\n                    \"task\": {},\n                    \"parent\": parent_name\n                })\n\n    def _get_active_assets(self, context):\n        \"\"\" Returns only asset dictionary.\n            Usually the last part of deep dictionary which\n            is not having any children\n        \"\"\"\n        def get_pure_hierarchy_data(input_dict):\n            input_dict_copy = deepcopy(input_dict)\n            for key in input_dict.keys():\n                self.log.debug(\"__ key: {}\".format(key))\n                # check if child key is available\n                if input_dict[key].get(\"childs\"):\n                    # loop deeper\n                    input_dict_copy[\n                        key][\"childs\"] = get_pure_hierarchy_data(\n                            input_dict[key][\"childs\"])\n                elif key not in active_assets:\n                    input_dict_copy.pop(key, None)\n            return input_dict_copy\n\n        hierarchy_context = context.data[\"hierarchyContext\"]\n\n        active_assets = []\n        # filter only the active publishing instances\n        for instance in context:\n            if instance.data.get(\"publish\") is False:\n                continue\n\n            if not instance.data.get(\"asset\"):\n                continue\n\n            active_assets.append(instance.data[\"asset\"])\n\n        # remove duplicity in list\n        active_assets = list(set(active_assets))\n        self.log.debug(\"__ active_assets: {}\".format(active_assets))\n\n        return get_pure_hierarchy_data(hierarchy_context)\n"
  },
  {
    "path": "openpype/plugins/publish/extract_hierarchy_to_ayon.py",
    "content": "import collections\nimport copy\nimport json\nimport uuid\nimport pyblish.api\n\nfrom ayon_api import slugify_string\nfrom ayon_api.entity_hub import EntityHub\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import get_assets, get_asset_name_identifier\nfrom openpype.pipeline.template_data import (\n    get_asset_template_data,\n    get_task_template_data,\n)\n\n\ndef _default_json_parse(value):\n    return str(value)\n\n\nclass ExtractHierarchyToAYON(pyblish.api.ContextPlugin):\n    \"\"\"Create entities in AYON based on collected data.\"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.01\n    label = \"Extract Hierarchy To AYON\"\n    families = [\"clip\", \"shot\"]\n\n    def process(self, context):\n        if not AYON_SERVER_ENABLED:\n            return\n\n        if not context.data.get(\"hierarchyContext\"):\n            self.log.debug(\"Skipping ExtractHierarchyToAYON\")\n            return\n\n        project_name = context.data[\"projectName\"]\n        self._create_hierarchy(context, project_name)\n        self._fill_instance_entities(context, project_name)\n\n    def _fill_instance_entities(self, context, project_name):\n        instances_by_asset_name = collections.defaultdict(list)\n        for instance in context:\n            if instance.data.get(\"publish\") is False:\n                continue\n\n            instance_entity = instance.data.get(\"assetEntity\")\n            if instance_entity:\n                continue\n\n            # Skip if instance asset does not match\n            instance_asset_name = instance.data.get(\"asset\")\n            instances_by_asset_name[instance_asset_name].append(instance)\n\n        project_doc = context.data[\"projectEntity\"]\n        asset_docs = get_assets(\n            project_name, asset_names=instances_by_asset_name.keys()\n        )\n        asset_docs_by_name = {\n            get_asset_name_identifier(asset_doc): asset_doc\n            for asset_doc in asset_docs\n        }\n        for asset_name, instances in instances_by_asset_name.items():\n            asset_doc = asset_docs_by_name[asset_name]\n            asset_data = get_asset_template_data(asset_doc, project_name)\n            for instance in instances:\n                task_name = instance.data.get(\"task\")\n                template_data = get_task_template_data(\n                    project_doc, asset_doc, task_name)\n                template_data.update(copy.deepcopy(asset_data))\n\n                instance.data[\"anatomyData\"].update(template_data)\n                instance.data[\"assetEntity\"] = asset_doc\n\n    def _create_hierarchy(self, context, project_name):\n        hierarchy_context = self._filter_hierarchy(context)\n        if not hierarchy_context:\n            self.log.debug(\"All folders were filtered out\")\n            return\n\n        self.log.debug(\"Hierarchy_context: {}\".format(\n            json.dumps(hierarchy_context, default=_default_json_parse)\n        ))\n\n        entity_hub = EntityHub(project_name)\n        project = entity_hub.project_entity\n\n        hierarchy_match_queue = collections.deque()\n        hierarchy_match_queue.append((project, hierarchy_context))\n        while hierarchy_match_queue:\n            item = hierarchy_match_queue.popleft()\n            entity, entity_info = item\n\n            # Update attributes of entities\n            for attr_name, attr_value in entity_info[\"attributes\"].items():\n                if attr_name in entity.attribs:\n                    entity.attribs[attr_name] = attr_value\n\n            # Check if info has any children to sync\n            children_info = entity_info[\"children\"]\n            tasks_info = entity_info[\"tasks\"]\n            if not tasks_info and not children_info:\n                continue\n\n            # Prepare children by lowered name to easily find matching entities\n            children_by_low_name = {\n                child.name.lower(): child\n                for child in entity.children\n            }\n\n            # Create tasks if are not available\n            for task_info in tasks_info:\n                task_label = task_info[\"name\"]\n                task_name = slugify_string(task_label)\n                if task_name == task_label:\n                    task_label = None\n                task_entity = children_by_low_name.get(task_name.lower())\n                # TODO propagate updates of tasks if there are any\n                # TODO check if existing entity have 'task' type\n                if task_entity is None:\n                    task_entity = entity_hub.add_new_task(\n                        task_info[\"type\"],\n                        parent_id=entity.id,\n                        name=task_name\n                    )\n\n                if task_label:\n                    task_entity.label = task_label\n\n            # Create/Update sub-folders\n            for child_info in children_info:\n                child_label = child_info[\"name\"]\n                child_name = slugify_string(child_label)\n                if child_name == child_label:\n                    child_label = None\n                # TODO check if existing entity have 'folder' type\n                child_entity = children_by_low_name.get(child_name.lower())\n                if child_entity is None:\n                    child_entity = entity_hub.add_new_folder(\n                        child_info[\"entity_type\"],\n                        parent_id=entity.id,\n                        name=child_name\n                    )\n\n                if child_label:\n                    child_entity.label = child_label\n\n                # Add folder to queue\n                hierarchy_match_queue.append((child_entity, child_info))\n\n        entity_hub.commit_changes()\n\n    def _filter_hierarchy(self, context):\n        \"\"\"Filter hierarchy context by active folder names.\n\n        Hierarchy context is filtered to folder names on active instances.\n\n        Change hierarchy context to unified structure which suits logic in\n        entity creation.\n\n        Output example:\n            {\n                \"name\": \"MyProject\",\n                \"entity_type\": \"Project\",\n                \"attributes\": {},\n                \"tasks\": [],\n                \"children\": [\n                    {\n                        \"name\": \"seq_01\",\n                        \"entity_type\": \"Sequence\",\n                        \"attributes\": {},\n                        \"tasks\": [],\n                        \"children\": [\n                            ...\n                        ]\n                    },\n                    ...\n                ]\n            }\n\n        Todos:\n            Change how active folder are defined (names won't be enough in\n                AYON).\n\n        Args:\n            context (pyblish.api.Context): Pyblish context.\n\n        Returns:\n            dict[str, Any]: Hierarchy structure filtered by folder names.\n        \"\"\"\n\n        # filter only the active publishing instances\n        active_folder_paths = set()\n        for instance in context:\n            if instance.data.get(\"publish\") is not False:\n                active_folder_paths.add(instance.data.get(\"asset\"))\n\n        active_folder_paths.discard(None)\n\n        self.log.debug(\"Active folder paths: {}\".format(active_folder_paths))\n        if not active_folder_paths:\n            return None\n\n        project_item = None\n        project_children_context = None\n        hierarchy_context = copy.deepcopy(context.data[\"hierarchyContext\"])\n        for key, value in hierarchy_context.items():\n            project_item = copy.deepcopy(value)\n            project_children_context = project_item.pop(\"childs\", None)\n            project_item[\"name\"] = key\n            project_item[\"tasks\"] = []\n            project_item[\"attributes\"] = project_item.pop(\n                \"custom_attributes\", {}\n            )\n            project_item[\"children\"] = []\n\n        if not project_children_context:\n            return None\n\n        project_id = uuid.uuid4().hex\n        items_by_id = {project_id: project_item}\n        parent_id_by_item_id = {project_id: None}\n        valid_ids = set()\n\n        hierarchy_queue = collections.deque()\n        hierarchy_queue.append((project_id, \"\", project_children_context))\n        while hierarchy_queue:\n            queue_item = hierarchy_queue.popleft()\n            parent_id, parent_path, children_context = queue_item\n            if not children_context:\n                continue\n\n            for folder_name, folder_info in children_context.items():\n                folder_path = \"{}/{}\".format(parent_path, folder_name)\n                if (\n                    folder_path not in active_folder_paths\n                    and not folder_info.get(\"childs\")\n                ):\n                    continue\n\n                item_id = uuid.uuid4().hex\n                new_item = copy.deepcopy(folder_info)\n                new_item[\"name\"] = folder_name\n                new_item[\"children\"] = []\n                new_children_context = new_item.pop(\"childs\", None)\n                tasks = new_item.pop(\"tasks\", {})\n                task_items = []\n                for task_name, task_info in tasks.items():\n                    task_info[\"name\"] = task_name\n                    task_items.append(task_info)\n                new_item[\"tasks\"] = task_items\n                new_item[\"attributes\"] = new_item.pop(\"custom_attributes\", {})\n\n                items_by_id[item_id] = new_item\n                parent_id_by_item_id[item_id] = parent_id\n\n                if folder_path in active_folder_paths:\n                    valid_ids.add(item_id)\n                hierarchy_queue.append(\n                    (item_id, folder_path, new_children_context)\n                )\n\n        if not valid_ids:\n            return None\n\n        for item_id in set(valid_ids):\n            parent_id = parent_id_by_item_id[item_id]\n            while parent_id is not None and parent_id not in valid_ids:\n                valid_ids.add(parent_id)\n                parent_id = parent_id_by_item_id[parent_id]\n\n        valid_ids.discard(project_id)\n        for item_id in valid_ids:\n            parent_id = parent_id_by_item_id[item_id]\n            item = items_by_id[item_id]\n            parent_item = items_by_id[parent_id]\n            parent_item[\"children\"].append(item)\n\n        if not project_item[\"children\"]:\n            return None\n        return project_item\n"
  },
  {
    "path": "openpype/plugins/publish/extract_otio_audio_tracks.py",
    "content": "import os\nimport tempfile\n\nimport pyblish\n\nfrom openpype.lib import (\n    get_ffmpeg_tool_args,\n    run_subprocess\n)\n\n\nclass ExtractOtioAudioTracks(pyblish.api.ContextPlugin):\n    \"\"\"Extract Audio tracks from OTIO timeline.\n\n    Process will merge all found audio tracks into one long .wav file at frist\n    stage. Then it will trim it into individual short audio files relative to\n    asset length and add it to each marked instance data representation. This\n    is influenced by instance data audio attribute \"\"\"\n\n    order = pyblish.api.ExtractorOrder - 0.44\n    label = \"Extract OTIO Audio Tracks\"\n    hosts = [\"hiero\", \"resolve\", \"flame\"]\n\n    def process(self, context):\n        \"\"\"Convert otio audio track's content to audio representations\n\n        Args:\n            context (pyblish.Context): context of publisher\n        \"\"\"\n        # split the long audio file to peces devided by isntances\n        audio_instances = self.get_audio_instances(context)\n        self.log.debug(\"Audio instances: {}\".format(len(audio_instances)))\n\n        if len(audio_instances) < 1:\n            self.log.info(\"No audio instances available\")\n            return\n\n        # get sequence\n        otio_timeline = context.data[\"otioTimeline\"]\n\n        # get all audio inputs from otio timeline\n        audio_inputs = self.get_audio_track_items(otio_timeline)\n\n        if not audio_inputs:\n            return\n\n        # temp file\n        audio_temp_fpath = self.create_temp_file(\"audio\")\n\n        # create empty audio with longest duration\n        empty = self.create_empty(audio_inputs)\n\n        # add empty to list of audio inputs\n        audio_inputs.insert(0, empty)\n\n        # create cmd\n        self.mix_audio(audio_inputs, audio_temp_fpath)\n\n        # remove empty\n        os.remove(empty[\"mediaPath\"])\n\n        # cut instance framerange and add to representations\n        self.add_audio_to_instances(audio_temp_fpath, audio_instances)\n\n        # remove full mixed audio file\n        os.remove(audio_temp_fpath)\n\n    def add_audio_to_instances(self, audio_file, instances):\n        created_files = []\n        for inst in instances:\n            name = inst.data[\"asset\"]\n\n            recycling_file = [f for f in created_files if name in f]\n\n            # frameranges\n            timeline_in_h = inst.data[\"clipInH\"]\n            timeline_out_h = inst.data[\"clipOutH\"]\n            fps = inst.data[\"fps\"]\n\n            # create duration\n            duration = (timeline_out_h - timeline_in_h) + 1\n\n            # ffmpeg generate new file only if doesnt exists already\n            if not recycling_file:\n                # convert to seconds\n                start_sec = float(timeline_in_h / fps)\n                duration_sec = float(duration / fps)\n\n                # temp audio file\n                audio_fpath = self.create_temp_file(name)\n\n                cmd = get_ffmpeg_tool_args(\n                    \"ffmpeg\",\n                    \"-ss\", str(start_sec),\n                    \"-t\", str(duration_sec),\n                    \"-i\", audio_file,\n                    audio_fpath\n                )\n\n                # run subprocess\n                self.log.debug(\"Executing: {}\".format(\" \".join(cmd)))\n                run_subprocess(cmd, logger=self.log)\n            else:\n                audio_fpath = recycling_file.pop()\n\n            if \"audio\" in (inst.data[\"families\"] + [inst.data[\"family\"]]):\n                # create empty representation attr\n                if \"representations\" not in inst.data:\n                    inst.data[\"representations\"] = []\n                # add to representations\n                inst.data[\"representations\"].append({\n                    \"files\": os.path.basename(audio_fpath),\n                    \"name\": \"wav\",\n                    \"ext\": \"wav\",\n                    \"stagingDir\": os.path.dirname(audio_fpath),\n                    \"frameStart\": 0,\n                    \"frameEnd\": duration\n                })\n\n            elif \"reviewAudio\" in inst.data.keys():\n                audio_attr = inst.data.get(\"audio\") or []\n                audio_attr.append({\n                    \"filename\": audio_fpath,\n                    \"offset\": 0\n                })\n                inst.data[\"audio\"] = audio_attr\n\n            # add generated audio file to created files for recycling\n            if audio_fpath not in created_files:\n                created_files.append(audio_fpath)\n\n    def get_audio_instances(self, context):\n        \"\"\"Return only instances which are having audio in families\n\n        Args:\n            context (pyblish.context): context of publisher\n\n        Returns:\n            list: list of selected instances\n        \"\"\"\n        return [\n            _i for _i in context\n            # filter only those with audio family\n            # and also with reviewAudio data key\n            if bool(\"audio\" in (\n                _i.data.get(\"families\", []) + [_i.data[\"family\"]])\n            ) or _i.data.get(\"reviewAudio\")\n        ]\n\n    def get_audio_track_items(self, otio_timeline):\n        \"\"\"Get all audio clips form OTIO audio tracks\n\n        Args:\n            otio_timeline (otio.schema.timeline): timeline object\n\n        Returns:\n            list: list of audio clip dictionaries\n        \"\"\"\n        # Not all hosts can import this module.\n        import opentimelineio as otio\n\n        output = []\n        # go trough all audio tracks\n        for otio_track in otio_timeline.tracks:\n            if \"Audio\" not in otio_track.kind:\n                continue\n            self.log.debug(\"_\" * 50)\n            playhead = 0\n            for otio_clip in otio_track:\n                self.log.debug(otio_clip)\n                if isinstance(otio_clip, otio.schema.Gap):\n                    playhead += otio_clip.source_range.duration.value\n                elif isinstance(otio_clip, otio.schema.Clip):\n                    start = otio_clip.source_range.start_time.value\n                    duration = otio_clip.source_range.duration.value\n                    fps = otio_clip.source_range.start_time.rate\n                    media_path = otio_clip.media_reference.target_url\n                    input = {\n                        \"mediaPath\": media_path,\n                        \"delayFrame\": playhead,\n                        \"startFrame\": start,\n                        \"durationFrame\": duration,\n                        \"delayMilSec\": int(float(playhead / fps) * 1000),\n                        \"startSec\": float(start / fps),\n                        \"durationSec\": float(duration / fps),\n                        \"fps\": fps\n                    }\n                    if input not in output:\n                        output.append(input)\n                        self.log.debug(\"__ input: {}\".format(input))\n                    playhead += otio_clip.source_range.duration.value\n\n        return output\n\n    def create_empty(self, inputs):\n        \"\"\"Create an empty audio file used as duration placeholder\n\n        Args:\n            inputs (list): list of audio clip dictionaries\n\n        Returns:\n            dict: audio clip dictionary\n        \"\"\"\n        # temp file\n        empty_fpath = self.create_temp_file(\"empty\")\n\n        # get all end frames\n        end_secs = [(_i[\"delayFrame\"] + _i[\"durationFrame\"]) / _i[\"fps\"]\n                    for _i in inputs]\n        # get the max of end frames\n        max_duration_sec = max(end_secs)\n\n        # create empty cmd\n        cmd = get_ffmpeg_tool_args(\n            \"ffmpeg\",\n            \"-f\", \"lavfi\",\n            \"-i\", \"anullsrc=channel_layout=stereo:sample_rate=48000\",\n            \"-t\", str(max_duration_sec),\n            empty_fpath\n        )\n\n        # generate empty with ffmpeg\n        # run subprocess\n        self.log.debug(\"Executing: {}\".format(\" \".join(cmd)))\n\n        run_subprocess(\n            cmd, logger=self.log\n        )\n\n        # return dict with output\n        return {\n            \"mediaPath\": empty_fpath,\n            \"delayMilSec\": 0,\n            \"startSec\": 0.00,\n            \"durationSec\": max_duration_sec\n        }\n\n    def mix_audio(self, audio_inputs, audio_temp_fpath):\n        \"\"\"Creating multiple input cmd string\n\n        Args:\n            audio_inputs (list): list of input dicts. Order mater.\n\n        Returns:\n            str: the command body\n        \"\"\"\n\n        longest_input = 0\n        for audio_input in audio_inputs:\n            audio_len = audio_input[\"durationSec\"]\n            if audio_len > longest_input:\n                longest_input = audio_len\n\n        # create cmd segments\n        input_args = []\n        filters = []\n        tag_names = []\n        for index, audio_input in enumerate(audio_inputs):\n            input_args.extend([\n                \"-ss\", str(audio_input[\"startSec\"]),\n                \"-t\", str(audio_input[\"durationSec\"]),\n                \"-i\", audio_input[\"mediaPath\"]\n            ])\n\n            # Output tag of a filtered audio input\n            tag_name = \"[r{}]\".format(index)\n            tag_names.append(tag_name)\n            # Delay in audio by delay in item\n            filters.append(\"[{}]adelay={}:all=1{}\".format(\n                index, audio_input[\"delayMilSec\"], tag_name\n            ))\n\n        # Mixing filter\n        #   - dropout transition (when audio will get loader) is set to be\n        #       higher then any input audio item\n        #   - volume is set to number of inputs - each mix adds 1/n volume\n        #       where n is input inder (to get more info read ffmpeg docs and\n        #       send a giftcard to contributor)\n        filters.append(\n            (\n                \"{}amix=inputs={}:duration=first:\"\n                \"dropout_transition={},volume={}[a]\"\n            ).format(\n                \"\".join(tag_names),\n                len(audio_inputs),\n                (longest_input * 1000) + 1000,\n                len(audio_inputs),\n            )\n        )\n\n        # Store filters to a file (separated by ',')\n        #   - this is to avoid \"too long\" command issue in ffmpeg\n        with tempfile.NamedTemporaryFile(\n            delete=False, mode=\"w\", suffix=\".txt\"\n        ) as tmp_file:\n            filters_tmp_filepath = tmp_file.name\n            tmp_file.write(\",\".join(filters))\n\n        args = get_ffmpeg_tool_args(\"ffmpeg\")\n        args.extend(input_args)\n        args.extend([\n            \"-filter_complex_script\", filters_tmp_filepath,\n            \"-map\", \"[a]\"\n        ])\n        args.append(audio_temp_fpath)\n\n        # run subprocess\n        self.log.debug(\"Executing: {}\".format(args))\n        run_subprocess(args, logger=self.log)\n\n        os.remove(filters_tmp_filepath)\n\n    def create_temp_file(self, name):\n        \"\"\"Create temp wav file\n\n        Args:\n            name (str): name to be used in file name\n\n        Returns:\n            str: temp fpath\n        \"\"\"\n        name = name.replace(\"/\", \"_\")\n        return os.path.normpath(\n            tempfile.mktemp(\n                prefix=\"pyblish_tmp_{}_\".format(name),\n                suffix=\".wav\"\n            )\n        )\n"
  },
  {
    "path": "openpype/plugins/publish/extract_otio_file.py",
    "content": "import os\n\nimport pyblish.api\n\nfrom openpype.pipeline import publish\n\n\nclass ExtractOTIOFile(publish.Extractor):\n    \"\"\"\n    Extractor export OTIO file\n    \"\"\"\n\n    label = \"Extract OTIO file\"\n    order = pyblish.api.ExtractorOrder - 0.45\n    families = [\"workfile\"]\n    hosts = [\"resolve\", \"hiero\", \"traypublisher\"]\n\n    def process(self, instance):\n        # Not all hosts can import this module.\n        import opentimelineio as otio\n\n        if not instance.context.data.get(\"otioTimeline\"):\n            return\n        # create representation data\n        if \"representations\" not in instance.data:\n            instance.data[\"representations\"] = []\n\n        name = instance.data[\"name\"]\n        staging_dir = self.staging_dir(instance)\n\n        otio_timeline = instance.context.data[\"otioTimeline\"]\n        # create otio timeline representation\n        otio_file_name = name + \".otio\"\n        otio_file_path = os.path.join(staging_dir, otio_file_name)\n        otio.adapters.write_to_file(otio_timeline, otio_file_path)\n\n        representation_otio = {\n            'name': \"otio\",\n            'ext': \"otio\",\n            'files': otio_file_name,\n            \"stagingDir\": staging_dir,\n        }\n\n        instance.data[\"representations\"].append(representation_otio)\n\n        self.log.info(\"Added OTIO file representation: {}\".format(\n            representation_otio))\n"
  },
  {
    "path": "openpype/plugins/publish/extract_otio_review.py",
    "content": "\"\"\"\nRequires:\n    instance -> handleStart\n    instance -> handleEnd\n    instance -> otioClip\n    instance -> otioReviewClips\n\nOptional:\n    instance -> workfileFrameStart\n    instance -> resolutionWidth\n    instance -> resolutionHeight\n\nProvides:\n    instance -> otioReviewClips\n\"\"\"\n\nimport os\n\nimport clique\nfrom pyblish import api\n\nfrom openpype.lib import (\n    get_ffmpeg_tool_args,\n    run_subprocess,\n)\nfrom openpype.pipeline import publish\n\n\nclass ExtractOTIOReview(publish.Extractor):\n    \"\"\"\n    Extract OTIO timeline into one concuted image sequence file.\n\n    The `otioReviewClip` is holding trimmed range of clips relative to\n    the `otioClip`. Handles are added during looping by available list\n    of Gap and clips in the track. Handle start (head) is added before\n    first Gap or Clip and Handle end (tail) is added at the end of last\n    Clip or Gap. In case there is missing source material after the\n    handles addition Gap will be added. At the end all Gaps are converted\n    to black frames and available material is converted to image sequence\n    frames. At the end representation is created and added to the instance.\n\n    At the moment only image sequence output is supported\n\n    \"\"\"\n\n    order = api.ExtractorOrder - 0.45\n    label = \"Extract OTIO review\"\n    families = [\"review\"]\n    hosts = [\"resolve\", \"hiero\", \"flame\"]\n\n    # plugin default attributes\n    temp_file_head = \"tempFile.\"\n    to_width = 1280\n    to_height = 720\n    output_ext = \".jpg\"\n\n    def process(self, instance):\n        # Not all hosts can import these modules.\n        import opentimelineio as otio\n        from openpype.pipeline.editorial import (\n            otio_range_to_frame_range,\n            make_sequence_collection\n        )\n\n        # TODO: convert resulting image sequence to mp4\n\n        # get otio clip and other time info from instance clip\n        # TODO: what if handles are different in `versionData`?\n        handle_start = instance.data[\"handleStart\"]\n        handle_end = instance.data[\"handleEnd\"]\n        otio_review_clips = instance.data[\"otioReviewClips\"]\n\n        # add plugin wide attributes\n        self.representation_files = list()\n        self.used_frames = list()\n        self.workfile_start = int(instance.data.get(\n            \"workfileFrameStart\", 1001)) - handle_start\n        self.padding = len(str(self.workfile_start))\n        self.used_frames.append(self.workfile_start)\n        self.to_width = instance.data.get(\n            \"resolutionWidth\") or self.to_width\n        self.to_height = instance.data.get(\n            \"resolutionHeight\") or self.to_height\n\n        # skip instance if no reviewable data available\n        if (not isinstance(otio_review_clips[0], otio.schema.Clip)) \\\n                and (len(otio_review_clips) == 1):\n            self.log.warning(\n                \"Instance `{}` has nothing to process\".format(instance))\n            return\n        else:\n            self.staging_dir = self.staging_dir(instance)\n            if not instance.data.get(\"representations\"):\n                instance.data[\"representations\"] = list()\n\n        # loop available clips in otio track\n        for index, r_otio_cl in enumerate(otio_review_clips):\n            # QUESTION: what if transition on clip?\n\n            # check if resolution is the same\n            width = self.to_width\n            height = self.to_height\n            otio_media = r_otio_cl.media_reference\n            media_metadata = otio_media.metadata\n\n            # get from media reference metadata source\n            if media_metadata.get(\"openpype.source.width\"):\n                width = int(media_metadata.get(\"openpype.source.width\"))\n            if media_metadata.get(\"openpype.source.height\"):\n                height = int(media_metadata.get(\"openpype.source.height\"))\n\n            # compare and reset\n            if width != self.to_width:\n                self.to_width = width\n            if height != self.to_height:\n                self.to_height = height\n\n            self.log.debug(\"> self.to_width x self.to_height: {} x {}\".format(\n                self.to_width, self.to_height\n            ))\n\n            # get frame range values\n            src_range = r_otio_cl.source_range\n            start = src_range.start_time.value\n            duration = src_range.duration.value\n            available_range = None\n            self.actual_fps = src_range.duration.rate\n\n            # add available range only if not gap\n            if isinstance(r_otio_cl, otio.schema.Clip):\n                available_range = r_otio_cl.available_range()\n                self.actual_fps = available_range.duration.rate\n\n            # reframing handles conditions\n            if (len(otio_review_clips) > 1) and (index == 0):\n                # more clips | first clip reframing with handle\n                start -= handle_start\n                duration += handle_start\n            elif len(otio_review_clips) > 1 \\\n                    and (index == len(otio_review_clips) - 1):\n                # more clips | last clip reframing with handle\n                duration += handle_end\n            elif len(otio_review_clips) == 1:\n                # one clip | add both handles\n                start -= handle_start\n                duration += (handle_start + handle_end)\n\n            if available_range:\n                available_range = self._trim_available_range(\n                    available_range, start, duration, self.actual_fps)\n\n            # process all track items of the track\n            if isinstance(r_otio_cl, otio.schema.Clip):\n                # process Clip\n                media_ref = r_otio_cl.media_reference\n                metadata = media_ref.metadata\n                is_sequence = None\n\n                # check in two way if it is sequence\n                if hasattr(otio.schema, \"ImageSequenceReference\"):\n                    # for OpenTimelineIO 0.13 and newer\n                    if isinstance(media_ref,\n                                  otio.schema.ImageSequenceReference):\n                        is_sequence = True\n                else:\n                    # for OpenTimelineIO 0.12 and older\n                    if metadata.get(\"padding\"):\n                        is_sequence = True\n\n                if is_sequence:\n                    # file sequence way\n                    if hasattr(media_ref, \"target_url_base\"):\n                        dirname = media_ref.target_url_base\n                        head = media_ref.name_prefix\n                        tail = media_ref.name_suffix\n                        first, last = otio_range_to_frame_range(\n                            available_range)\n                        collection = clique.Collection(\n                            head=head,\n                            tail=tail,\n                            padding=media_ref.frame_zero_padding\n                        )\n                        collection.indexes.update(\n                            [i for i in range(first, (last + 1))])\n                        # render segment\n                        self._render_seqment(\n                            sequence=[dirname, collection])\n                        # generate used frames\n                        self._generate_used_frames(\n                            len(collection.indexes))\n                    else:\n                        # in case it is file sequence but not new OTIO schema\n                        # `ImageSequenceReference`\n                        path = media_ref.target_url\n                        collection_data = make_sequence_collection(\n                            path, available_range, metadata)\n                        dir_path, collection = collection_data\n\n                        # render segment\n                        self._render_seqment(\n                            sequence=[dir_path, collection])\n                        # generate used frames\n                        self._generate_used_frames(\n                            len(collection.indexes))\n                else:\n                    # single video file way\n                    path = media_ref.target_url\n                    # render video file to sequence\n                    self._render_seqment(\n                        video=[path, available_range])\n                    # generate used frames\n                    self._generate_used_frames(\n                        available_range.duration.value)\n            # QUESTION: what if nested track composition is in place?\n            else:\n                # at last process a Gap\n                self._render_seqment(gap=duration)\n                # generate used frames\n                self._generate_used_frames(duration)\n\n        # creating and registering representation\n        representation = self._create_representation(start, duration)\n        instance.data[\"representations\"].append(representation)\n        self.log.info(\"Adding representation: {}\".format(representation))\n\n    def _create_representation(self, start, duration):\n        \"\"\"\n        Creating representation data.\n\n        Args:\n            start (int): start frame\n            duration (int): duration frames\n\n        Returns:\n            dict: representation data\n        \"\"\"\n\n        end = start + duration\n\n        # create default representation data\n        representation_data = {\n            \"frameStart\": start,\n            \"frameEnd\": end,\n            \"stagingDir\": self.staging_dir,\n            \"tags\": [\"review\", \"delete\"]\n        }\n\n        collection = clique.Collection(\n            self.temp_file_head,\n            tail=self.output_ext,\n            padding=self.padding,\n            indexes=set(self.used_frames)\n        )\n        start = min(collection.indexes)\n        end = max(collection.indexes)\n\n        files = [f for f in collection]\n        ext = collection.format(\"{tail}\")\n        representation_data.update({\n            \"name\": ext[1:],\n            \"ext\": ext[1:],\n            \"files\": files,\n            \"frameStart\": start,\n            \"frameEnd\": end,\n        })\n        return representation_data\n\n    def _trim_available_range(self, avl_range, start, duration, fps):\n        \"\"\"\n        Trim available media range to source range.\n\n        If missing media range is detected it will convert it into\n        black frames gaps.\n\n        Args:\n            avl_range (otio.time.TimeRange): media available time range\n            start (int): start frame\n            duration (int): duration frames\n            fps (float): frame rate\n\n        Returns:\n            otio.time.TimeRange: trimmed available range\n        \"\"\"\n        # Not all hosts can import these modules.\n        from openpype.pipeline.editorial import (\n            trim_media_range,\n            range_from_frames\n        )\n\n        avl_start = int(avl_range.start_time.value)\n        src_start = int(avl_start + start)\n        avl_durtation = int(avl_range.duration.value)\n\n        self.need_offset = bool(avl_start != 0 and src_start != 0)\n\n        # if media start is les then clip requires\n        if src_start < avl_start:\n            # calculate gap\n            gap_duration = avl_start - src_start\n\n            # create gap data to disk\n            self._render_seqment(gap=gap_duration)\n            # generate used frames\n            self._generate_used_frames(gap_duration)\n\n            # fix start and end to correct values\n            start = 0\n            duration -= gap_duration\n\n        # if media duration is shorter then clip requirement\n        if duration > avl_durtation:\n            # calculate gap\n            gap_start = int(src_start + avl_durtation)\n            gap_end = int(src_start + duration)\n            gap_duration = gap_end - gap_start\n\n            # create gap data to disk\n            self._render_seqment(gap=gap_duration, end_offset=avl_durtation)\n            # generate used frames\n            self._generate_used_frames(gap_duration, end_offset=avl_durtation)\n\n            # fix duration lenght\n            duration = avl_durtation\n\n        # return correct trimmed range\n        return trim_media_range(\n            avl_range, range_from_frames(start, duration, fps)\n        )\n\n    def _render_seqment(self, sequence=None,\n                        video=None, gap=None, end_offset=None):\n        \"\"\"\n        Render seqment into image sequence frames.\n\n        Using ffmpeg to convert compatible video and image source\n        to defined image sequence format.\n\n        Args:\n            sequence (list): input dir path string, collection object in list\n            video (list)[optional]: video_path string, otio_range in list\n            gap (int)[optional]: gap duration\n            end_offset (int)[optional]: offset gap frame start in frames\n\n        Returns:\n            otio.time.TimeRange: trimmed available range\n        \"\"\"\n        # Not all hosts can import this module.\n        from openpype.pipeline.editorial import frames_to_seconds\n\n        # create path  and frame start to destination\n        output_path, out_frame_start = self._get_ffmpeg_output()\n\n        if end_offset:\n            out_frame_start += end_offset\n\n        # start command list\n        command = get_ffmpeg_tool_args(\"ffmpeg\")\n\n        input_extension = None\n        if sequence:\n            input_dir, collection = sequence\n            in_frame_start = min(collection.indexes)\n\n            # converting image sequence to image sequence\n            input_file = collection.format(\"{head}{padding}{tail}\")\n            input_path = os.path.join(input_dir, input_file)\n            input_extension = os.path.splitext(input_path)[-1]\n\n            # form command for rendering gap files\n            command.extend([\n                \"-start_number\", str(in_frame_start),\n                \"-i\", input_path\n            ])\n\n        elif video:\n            video_path, otio_range = video\n            frame_start = otio_range.start_time.value\n            input_fps = otio_range.start_time.rate\n            frame_duration = otio_range.duration.value\n            sec_start = frames_to_seconds(frame_start, input_fps)\n            sec_duration = frames_to_seconds(\n                frame_duration, input_fps\n            )\n            input_extension = os.path.splitext(video_path)[-1]\n\n            # form command for rendering gap files\n            command.extend([\n                \"-ss\", str(sec_start),\n                \"-t\", str(sec_duration),\n                \"-i\", video_path\n            ])\n\n        elif gap:\n            sec_duration = frames_to_seconds(gap, self.actual_fps)\n\n            # form command for rendering gap files\n            command.extend([\n                \"-t\", str(sec_duration),\n                \"-r\", str(self.actual_fps),\n                \"-f\", \"lavfi\",\n                \"-i\", \"color=c=black:s={}x{}\".format(\n                    self.to_width, self.to_height\n                ),\n                \"-tune\", \"stillimage\"\n            ])\n\n        # add output attributes\n        command.extend([\n            \"-start_number\", str(out_frame_start)\n        ])\n\n        # add copying if extensions are matching\n        if (\n            input_extension\n            and self.output_ext == input_extension\n        ):\n            command.extend([\n                \"-c\", \"copy\"\n            ])\n\n        # add output path at the end\n        command.append(output_path)\n\n        # execute\n        self.log.debug(\"Executing: {}\".format(\" \".join(command)))\n        output = run_subprocess(\n            command, logger=self.log\n        )\n        self.log.debug(\"Output: {}\".format(output))\n\n    def _generate_used_frames(self, duration, end_offset=None):\n        \"\"\"\n        Generating used frames into plugin argument `used_frames`.\n\n        The argument `used_frames` is used for checking next available\n        frame to start with during rendering sequence segments.\n\n        Args:\n            duration (int): duration of frames needed to be generated\n            end_offset (int)[optional]: in case frames need to be offseted\n\n        \"\"\"\n\n        padding = \"{{:0{}d}}\".format(self.padding)\n\n        # create frame offset\n        offset = 0\n        if self.need_offset:\n            offset = 1\n\n        if end_offset:\n            new_frames = list()\n            start_frame = self.used_frames[-1]\n            for index in range((end_offset + offset),\n                               (int(end_offset + duration) + offset)):\n                seq_number = padding.format(start_frame + index)\n                self.log.debug(\n                    \"index: `{}` | seq_number: `{}`\".format(index, seq_number))\n                new_frames.append(int(seq_number))\n            new_frames += self.used_frames\n            self.used_frames = new_frames\n        else:\n            for _i in range(1, (int(duration) + 1)):\n                if self.used_frames[-1] == self.workfile_start:\n                    seq_number = padding.format(self.used_frames[-1])\n                    self.workfile_start -= 1\n                else:\n                    seq_number = padding.format(self.used_frames[-1] + 1)\n                    self.used_frames.append(int(seq_number))\n\n    def _get_ffmpeg_output(self):\n        \"\"\"\n        Returning ffmpeg output command arguments.\n\n        Returns:\n            str: output_path is path for image sequence output\n            int: out_frame_start is starting sequence frame\n\n        \"\"\"\n        output_file = \"{}{}{}\".format(\n            self.temp_file_head,\n            \"%0{}d\".format(self.padding),\n            self.output_ext\n        )\n        # create path to destination\n        output_path = os.path.join(self.staging_dir, output_file)\n\n        # generate frame start\n        out_frame_start = self.used_frames[-1] + 1\n        if self.used_frames[-1] == self.workfile_start:\n            out_frame_start = self.used_frames[-1]\n\n        return output_path, out_frame_start\n"
  },
  {
    "path": "openpype/plugins/publish/extract_otio_trimming_video.py",
    "content": "\"\"\"\nRequires:\n    instance -> otioTrimmingRange\n    instance -> representations\n\n\"\"\"\n\nimport os\nfrom copy import deepcopy\n\nimport pyblish.api\n\nfrom openpype.lib import (\n    get_ffmpeg_tool_args,\n    run_subprocess,\n)\nfrom openpype.pipeline import publish\n\n\nclass ExtractOTIOTrimmingVideo(publish.Extractor):\n    \"\"\"\n    Trimming video file longer then required lenght\n\n    \"\"\"\n    order = pyblish.api.ExtractorOrder\n    label = \"Extract OTIO trim longer video\"\n    families = [\"trim\"]\n    hosts = [\"resolve\", \"hiero\", \"flame\"]\n\n    def process(self, instance):\n        self.staging_dir = self.staging_dir(instance)\n        otio_trim_range = instance.data[\"otioTrimmingRange\"]\n        representations = instance.data[\"representations\"]\n        self.log.debug(\"otio_trim_range: {}\".format(otio_trim_range))\n        self.log.debug(\"self.staging_dir: {}\".format(self.staging_dir))\n\n        # get corresponding representation\n        for _repre in representations:\n            if \"trim\" not in _repre.get(\"tags\", []):\n                continue\n\n            input_file = _repre[\"files\"]\n            input_file_path = os.path.normpath(os.path.join(\n                _repre[\"stagingDir\"], input_file\n            ))\n            self.log.debug(\"input_file_path: {}\".format(input_file_path))\n\n            # trim via ffmpeg\n            new_file = self._ffmpeg_trim_seqment(\n                input_file_path, otio_trim_range)\n\n            # prepare new representation data\n            repre_data = deepcopy(_repre)\n            # remove tags as we dont need them\n            repre_data.pop(\"tags\")\n            repre_data[\"stagingDir\"] = self.staging_dir\n            repre_data[\"files\"] = new_file\n\n            # romove `trim` tagged representation\n            representations.remove(_repre)\n            representations.append(repre_data)\n            self.log.debug(repre_data)\n\n        self.log.debug(\"representations: {}\".format(representations))\n\n    def _ffmpeg_trim_seqment(self, input_file_path, otio_range):\n        \"\"\"\n        Trim seqment of video file.\n\n        Using ffmpeg to trim video to desired length.\n\n        Args:\n            input_file_path (str): path string\n            otio_range (opentime.TimeRange): range to trim to\n\n        \"\"\"\n        # Not all hosts can import this module.\n        from openpype.pipeline.editorial import frames_to_seconds\n\n        # create path to destination\n        output_path = self._get_ffmpeg_output(input_file_path)\n\n        # start command list\n        command = get_ffmpeg_tool_args(\"ffmpeg\")\n\n        video_path = input_file_path\n        frame_start = otio_range.start_time.value\n        input_fps = otio_range.start_time.rate\n        frame_duration = otio_range.duration.value - 1\n        sec_start = frames_to_seconds(frame_start, input_fps)\n        sec_duration = frames_to_seconds(frame_duration, input_fps)\n\n        # form command for rendering gap files\n        command.extend([\n            \"-ss\", str(sec_start),\n            \"-t\", str(sec_duration),\n            \"-i\", video_path,\n            \"-c\", \"copy\",\n            output_path\n        ])\n\n        # execute\n        self.log.debug(\"Executing: {}\".format(\" \".join(command)))\n        output = run_subprocess(\n            command, logger=self.log\n        )\n        self.log.debug(\"Output: {}\".format(output))\n\n        return os.path.basename(output_path)\n\n    def _get_ffmpeg_output(self, file_path):\n        \"\"\"\n        Returning ffmpeg output command arguments.\n\n        Arguments\"\n            file_path (str): path string\n\n        Returns:\n            str: output_path is path\n\n        \"\"\"\n        basename = os.path.basename(file_path)\n        name, ext = os.path.splitext(basename)\n\n        output_file = \"{}_{}{}\".format(\n            name,\n            \"trimmed\",\n            ext\n        )\n        # create path to destination\n        return os.path.join(self.staging_dir, output_file)\n"
  },
  {
    "path": "openpype/plugins/publish/extract_review.py",
    "content": "import os\nimport re\nimport copy\nimport json\nimport shutil\nimport subprocess\nfrom abc import ABCMeta, abstractmethod\n\nimport six\nimport clique\nimport speedcopy\nimport pyblish.api\n\nfrom openpype.lib import (\n    get_ffmpeg_tool_args,\n    filter_profiles,\n    path_to_subprocess_arg,\n    run_subprocess,\n)\nfrom openpype.lib.transcoding import (\n    IMAGE_EXTENSIONS,\n    get_ffprobe_streams,\n    should_convert_for_ffmpeg,\n    get_review_layer_name,\n    convert_input_paths_for_ffmpeg,\n    get_transcode_temp_directory,\n)\nfrom openpype.pipeline.publish import (\n    KnownPublishError,\n    get_publish_instance_label,\n)\nfrom openpype.pipeline.publish.lib import add_repre_files_for_cleanup\n\n\nclass ExtractReview(pyblish.api.InstancePlugin):\n    \"\"\"Extracting Review mov file for Ftrack\n\n    Compulsory attribute of representation is tags list with \"review\",\n    otherwise the representation is ignored.\n\n    All new representations are created and encoded by ffmpeg following\n    presets found in OpenPype Settings interface at\n    `project_settings/global/publish/ExtractReview/profiles:outputs`.\n    \"\"\"\n\n    label = \"Extract Review\"\n    order = pyblish.api.ExtractorOrder + 0.02\n    families = [\"review\"]\n    hosts = [\n        \"nuke\",\n        \"maya\",\n        \"blender\",\n        \"houdini\",\n        \"max\",\n        \"shell\",\n        \"hiero\",\n        \"premiere\",\n        \"harmony\",\n        \"traypublisher\",\n        \"standalonepublisher\",\n        \"fusion\",\n        \"tvpaint\",\n        \"resolve\",\n        \"webpublisher\",\n        \"aftereffects\",\n        \"flame\",\n        \"unreal\"\n    ]\n\n    # Supported extensions\n    image_exts = [\"exr\", \"jpg\", \"jpeg\", \"png\", \"dpx\", \"tga\"]\n    video_exts = [\"mov\", \"mp4\"]\n    supported_exts = image_exts + video_exts\n\n    alpha_exts = [\"exr\", \"png\", \"dpx\"]\n\n    # Preset attributes\n    profiles = None\n\n    def process(self, instance):\n        self.log.debug(str(instance.data[\"representations\"]))\n        # Skip review when requested.\n        if not instance.data.get(\"review\", True):\n            return\n\n        # Run processing\n        self.main_process(instance)\n\n        # Make sure cleanup happens and pop representations with \"delete\" tag.\n        for repre in tuple(instance.data[\"representations\"]):\n            tags = repre.get(\"tags\") or []\n            # Representation is not marked to be deleted\n            if \"delete\" not in tags:\n                continue\n\n            # The representation can be used as thumbnail source\n            if \"thumbnail\" in tags or \"need_thumbnail\" in tags:\n                continue\n\n            self.log.debug(\n                \"Removing representation: {}\".format(repre)\n            )\n            instance.data[\"representations\"].remove(repre)\n\n    def _get_outputs_for_instance(self, instance):\n        host_name = instance.context.data[\"hostName\"]\n        family = self.main_family_from_instance(instance)\n\n        self.log.debug(\"Host: \\\"{}\\\"\".format(host_name))\n        self.log.debug(\"Family: \\\"{}\\\"\".format(family))\n\n        profile = filter_profiles(\n            self.profiles,\n            {\n                \"hosts\": host_name,\n                \"families\": family,\n            },\n            logger=self.log)\n        if not profile:\n            self.log.info((\n                \"Skipped instance. None of profiles in presets are for\"\n                \" Host: \\\"{}\\\" | Family: \\\"{}\\\"\"\n            ).format(host_name, family))\n            return\n\n        self.log.debug(\"Matching profile: \\\"{}\\\"\".format(json.dumps(profile)))\n\n        subset_name = instance.data.get(\"subset\")\n        instance_families = self.families_from_instance(instance)\n        filtered_outputs = self.filter_output_defs(\n            profile, subset_name, instance_families\n        )\n        if not filtered_outputs:\n            self.log.info((\n                \"Skipped instance. All output definitions from selected\"\n                \" profile do not match instance families \\\"{}\\\" or\"\n                \" subset name \\\"{}\\\".\"\n            ).format(str(instance_families), subset_name))\n\n        # Store `filename_suffix` to save arguments\n        profile_outputs = []\n        for filename_suffix, definition in filtered_outputs.items():\n            definition[\"filename_suffix\"] = filename_suffix\n            profile_outputs.append(definition)\n\n        return profile_outputs\n\n    def _get_outputs_per_representations(self, instance, profile_outputs):\n        outputs_per_representations = []\n        for repre in instance.data[\"representations\"]:\n            repre_name = str(repre.get(\"name\"))\n            tags = repre.get(\"tags\") or []\n            custom_tags = repre.get(\"custom_tags\")\n            if \"review\" not in tags:\n                self.log.debug((\n                    \"Repre: {} - Didn't find \\\"review\\\" in tags. Skipping\"\n                ).format(repre_name))\n                continue\n\n            if \"thumbnail\" in tags:\n                self.log.debug((\n                    \"Repre: {} - Found \\\"thumbnail\\\" in tags. Skipping\"\n                ).format(repre_name))\n                continue\n\n            if \"passing\" in tags:\n                self.log.debug((\n                    \"Repre: {} - Found \\\"passing\\\" in tags. Skipping\"\n                ).format(repre_name))\n                continue\n\n            input_ext = repre[\"ext\"]\n            if input_ext.startswith(\".\"):\n                input_ext = input_ext[1:]\n\n            if input_ext not in self.supported_exts:\n                self.log.info(\n                    \"Representation has unsupported extension \\\"{}\\\"\".format(\n                        input_ext\n                    )\n                )\n                continue\n\n            # Filter output definition by representation's\n            # custom tags (optional)\n            outputs = self.filter_outputs_by_custom_tags(\n                profile_outputs, custom_tags)\n            if not outputs:\n                self.log.info((\n                    \"Skipped representation. All output definitions from\"\n                    \" selected profile does not match to representation's\"\n                    \" custom tags. \\\"{}\\\"\"\n                ).format(str(custom_tags)))\n                continue\n\n            outputs_per_representations.append((repre, outputs))\n        return outputs_per_representations\n\n    def _single_frame_filter(self, input_filepaths, output_defs):\n        single_frame_image = False\n        if len(input_filepaths) == 1:\n            ext = os.path.splitext(input_filepaths[0])[-1]\n            single_frame_image = ext.lower() in IMAGE_EXTENSIONS\n\n        filtered_defs = []\n        for output_def in output_defs:\n            output_filters = output_def.get(\"filter\") or {}\n            frame_filter = output_filters.get(\"single_frame_filter\")\n            if (\n                (not single_frame_image and frame_filter == \"single_frame\")\n                or (single_frame_image and frame_filter == \"multi_frame\")\n            ):\n                continue\n\n            filtered_defs.append(output_def)\n\n        return filtered_defs\n\n    def main_process(self, instance):\n        instance_label = get_publish_instance_label(instance)\n        self.log.debug(\"Processing instance \\\"{}\\\"\".format(instance_label))\n        profile_outputs = self._get_outputs_for_instance(instance)\n        if not profile_outputs:\n            return\n\n        # Loop through representations\n        outputs_per_repres = self._get_outputs_per_representations(\n            instance, profile_outputs\n        )\n\n        for repre, output_defs in outputs_per_repres:\n            # Check if input should be preconverted before processing\n            # Store original staging dir (it's value may change)\n            src_repre_staging_dir = repre[\"stagingDir\"]\n            # Receive filepath to first file in representation\n            first_input_path = None\n            input_filepaths = []\n            if not self.input_is_sequence(repre):\n                first_input_path = os.path.join(\n                    src_repre_staging_dir, repre[\"files\"]\n                )\n                input_filepaths.append(first_input_path)\n            else:\n                for filename in repre[\"files\"]:\n                    filepath = os.path.join(\n                        src_repre_staging_dir, filename\n                    )\n                    input_filepaths.append(filepath)\n                    if first_input_path is None:\n                        first_input_path = filepath\n\n            filtered_output_defs = self._single_frame_filter(\n                input_filepaths, output_defs\n            )\n            if not filtered_output_defs:\n                self.log.debug((\n                    \"Repre: {} - All output definitions were filtered\"\n                    \" out by single frame filter. Skipping\"\n                ).format(repre[\"name\"]))\n                continue\n\n            # Skip if file is not set\n            if first_input_path is None:\n                self.log.warning((\n                    \"Representation \\\"{}\\\" have empty files. Skipped.\"\n                ).format(repre[\"name\"]))\n                continue\n\n            # Determine if representation requires pre conversion for ffmpeg\n            do_convert = should_convert_for_ffmpeg(first_input_path)\n            # If result is None the requirement of conversion can't be\n            #   determined\n            if do_convert is None:\n                self.log.info((\n                    \"Can't determine if representation requires conversion.\"\n                    \" Skipped.\"\n                ))\n                continue\n\n            layer_name = get_review_layer_name(first_input_path)\n\n            # Do conversion if needed\n            #   - change staging dir of source representation\n            #   - must be set back after output definitions processing\n            if do_convert:\n                new_staging_dir = get_transcode_temp_directory()\n                repre[\"stagingDir\"] = new_staging_dir\n\n                convert_input_paths_for_ffmpeg(\n                    input_filepaths,\n                    new_staging_dir,\n                    self.log\n                )\n\n            try:\n                self._render_output_definitions(\n                    instance,\n                    repre,\n                    src_repre_staging_dir,\n                    filtered_output_defs,\n                    layer_name\n                )\n\n            finally:\n                # Make sure temporary staging is cleaned up and representation\n                #   has set origin stagingDir\n                if do_convert:\n                    # Set staging dir of source representation back to previous\n                    #   value\n                    repre[\"stagingDir\"] = src_repre_staging_dir\n                    if os.path.exists(new_staging_dir):\n                        shutil.rmtree(new_staging_dir)\n\n    def _render_output_definitions(\n        self,\n        instance,\n        repre,\n        src_repre_staging_dir,\n        output_definitions,\n        layer_name\n    ):\n        fill_data = copy.deepcopy(instance.data[\"anatomyData\"])\n        for _output_def in output_definitions:\n            output_def = copy.deepcopy(_output_def)\n            # Make sure output definition has \"tags\" key\n            if \"tags\" not in output_def:\n                output_def[\"tags\"] = []\n\n            if \"burnins\" not in output_def:\n                output_def[\"burnins\"] = []\n\n            # Create copy of representation\n            new_repre = copy.deepcopy(repre)\n            new_tags = new_repre.get(\"tags\") or []\n            # Make sure new representation has origin staging dir\n            #   - this is because source representation may change\n            #       it's staging dir because of ffmpeg conversion\n            new_repre[\"stagingDir\"] = src_repre_staging_dir\n\n            # Remove \"delete\" tag from new repre if there is\n            if \"delete\" in new_tags:\n                new_tags.remove(\"delete\")\n\n            if \"need_thumbnail\" in new_tags:\n                new_tags.remove(\"need_thumbnail\")\n\n            # Add additional tags from output definition to representation\n            for tag in output_def[\"tags\"]:\n                if tag not in new_tags:\n                    new_tags.append(tag)\n\n            # Return tags to new representation\n            new_repre[\"tags\"] = new_tags\n\n            # Add burnin link from output definition to representation\n            for burnin in output_def[\"burnins\"]:\n                if burnin not in new_repre.get(\"burnins\", []):\n                    if not new_repre.get(\"burnins\"):\n                        new_repre[\"burnins\"] = []\n                    new_repre[\"burnins\"].append(str(burnin))\n\n            self.log.debug(\n                \"Linked burnins: `{}`\".format(new_repre.get(\"burnins\"))\n            )\n\n            self.log.debug(\n                \"New representation tags: `{}`\".format(\n                    new_repre.get(\"tags\"))\n            )\n\n            temp_data = self.prepare_temp_data(instance, repre, output_def)\n            files_to_clean = []\n            if temp_data[\"input_is_sequence\"]:\n                self.log.debug(\"Checking sequence to fill gaps in sequence..\")\n                files_to_clean = self.fill_sequence_gaps(\n                    files=temp_data[\"origin_repre\"][\"files\"],\n                    staging_dir=new_repre[\"stagingDir\"],\n                    start_frame=temp_data[\"frame_start\"],\n                    end_frame=temp_data[\"frame_end\"]\n                )\n\n            # create or update outputName\n            output_name = new_repre.get(\"outputName\", \"\")\n            output_ext = new_repre[\"ext\"]\n            if output_name:\n                output_name += \"_\"\n            output_name += output_def[\"filename_suffix\"]\n            if temp_data[\"without_handles\"]:\n                output_name += \"_noHandles\"\n\n            # add outputName to anatomy format fill_data\n            fill_data.update({\n                \"output\": output_name,\n                \"ext\": output_ext\n            })\n\n            try:  # temporary until oiiotool is supported cross platform\n                ffmpeg_args = self._ffmpeg_arguments(\n                    output_def,\n                    instance,\n                    new_repre,\n                    temp_data,\n                    fill_data,\n                    layer_name,\n                )\n            except ZeroDivisionError:\n                # TODO recalculate width and height using OIIO before\n                #   conversion\n                if 'exr' in temp_data[\"origin_repre\"][\"ext\"]:\n                    self.log.warning(\n                        (\n                            \"Unsupported compression on input files.\"\n                            \" Skipping!!!\"\n                        ),\n                        exc_info=True\n                    )\n                    return\n                raise NotImplementedError\n\n            subprcs_cmd = \" \".join(ffmpeg_args)\n            if os.getenv(\"SHELL\") in (\"/bin/bash\", \"/bin/sh\"):\n                # Escape parentheses for bash\n                subprcs_cmd = (\n                    subprcs_cmd\n                    .replace(\"(\", \"\\\\(\")\n                    .replace(\")\", \"\\\\)\")\n                )\n\n            # run subprocess\n            self.log.debug(\"Executing: {}\".format(subprcs_cmd))\n\n            run_subprocess(subprcs_cmd, shell=True, logger=self.log)\n\n            # delete files added to fill gaps\n            if files_to_clean:\n                for f in files_to_clean:\n                    os.unlink(f)\n\n            new_repre.update({\n                \"fps\": temp_data[\"fps\"],\n                \"name\": \"{}_{}\".format(output_name, output_ext),\n                \"outputName\": output_name,\n                \"outputDef\": output_def,\n                \"frameStartFtrack\": temp_data[\"output_frame_start\"],\n                \"frameEndFtrack\": temp_data[\"output_frame_end\"],\n                \"ffmpeg_cmd\": subprcs_cmd\n            })\n\n            # Force to pop these key if are in new repre\n            new_repre.pop(\"thumbnail\", None)\n            if \"clean_name\" in new_repre.get(\"tags\", []):\n                new_repre.pop(\"outputName\")\n\n            # adding representation\n            self.log.debug(\n                \"Adding new representation: {}\".format(new_repre)\n            )\n            instance.data[\"representations\"].append(new_repre)\n\n            add_repre_files_for_cleanup(instance, new_repre)\n\n    def input_is_sequence(self, repre):\n        \"\"\"Deduce from representation data if input is sequence.\"\"\"\n        # TODO GLOBAL ISSUE - Find better way how to find out if input\n        #  is sequence. Issues (in theory):\n        #   - there may be multiple files ant not be sequence\n        #   - remainders are not checked at all\n        #   - there can be more than one collection\n        return isinstance(repre[\"files\"], (list, tuple))\n\n    def prepare_temp_data(self, instance, repre, output_def):\n        \"\"\"Prepare dictionary with values used across extractor's process.\n\n        All data are collected from instance, context, origin representation\n        and output definition.\n\n        There are few required keys in Instance data: \"frameStart\", \"frameEnd\"\n        and \"fps\".\n\n        Args:\n            instance (Instance): Currently processed instance.\n            repre (dict): Representation from which new representation was\n                copied.\n            output_def (dict): Definition of output of this plugin.\n\n        Returns:\n            dict: All data which are used across methods during process.\n                Their values should not change during process but new keys\n                with values may be added.\n        \"\"\"\n\n        frame_start = instance.data[\"frameStart\"]\n        frame_end = instance.data[\"frameEnd\"]\n\n        # Try to get handles from instance\n        handle_start = instance.data.get(\"handleStart\")\n        handle_end = instance.data.get(\"handleEnd\")\n        # If even one of handle values is not set on instance use\n        # handles from context\n        if handle_start is None or handle_end is None:\n            handle_start = instance.context.data[\"handleStart\"]\n            handle_end = instance.context.data[\"handleEnd\"]\n\n        frame_start_handle = frame_start - handle_start\n        frame_end_handle = frame_end + handle_end\n\n        # Change output frames when output should be without handles\n        without_handles = bool(\"no-handles\" in output_def[\"tags\"])\n        if without_handles:\n            output_frame_start = frame_start\n            output_frame_end = frame_end\n        else:\n            output_frame_start = frame_start_handle\n            output_frame_end = frame_end_handle\n\n        handles_are_set = handle_start > 0 or handle_end > 0\n\n        with_audio = True\n        if (\n            # Check if has `no-audio` tag\n            \"no-audio\" in output_def[\"tags\"]\n            # Check if instance has ny audio in data\n            or not instance.data.get(\"audio\")\n        ):\n            with_audio = False\n\n        input_is_sequence = self.input_is_sequence(repre)\n        input_allow_bg = False\n        first_sequence_frame = None\n        if input_is_sequence and repre[\"files\"]:\n            # Calculate first frame that should be used\n            cols, _ = clique.assemble(repre[\"files\"])\n            input_frames = list(sorted(cols[0].indexes))\n            first_sequence_frame = input_frames[0]\n            # WARNING: This is an issue as we don't know if first frame\n            #   is with or without handles!\n            # - handle start is added but how do not know if we should\n            output_duration = (output_frame_end - output_frame_start) + 1\n            if (\n                without_handles\n                and len(input_frames) - handle_start >= output_duration\n            ):\n                first_sequence_frame += handle_start\n\n            ext = os.path.splitext(repre[\"files\"][0])[1].replace(\".\", \"\")\n            if ext.lower() in self.alpha_exts:\n                input_allow_bg = True\n\n        return {\n            \"fps\": float(instance.data[\"fps\"]),\n            \"frame_start\": frame_start,\n            \"frame_end\": frame_end,\n            \"handle_start\": handle_start,\n            \"handle_end\": handle_end,\n            \"frame_start_handle\": frame_start_handle,\n            \"frame_end_handle\": frame_end_handle,\n            \"output_frame_start\": int(output_frame_start),\n            \"output_frame_end\": int(output_frame_end),\n            \"pixel_aspect\": instance.data.get(\"pixelAspect\", 1),\n            \"resolution_width\": instance.data.get(\"resolutionWidth\"),\n            \"resolution_height\": instance.data.get(\"resolutionHeight\"),\n            \"origin_repre\": repre,\n            \"input_is_sequence\": input_is_sequence,\n            \"first_sequence_frame\": first_sequence_frame,\n            \"input_allow_bg\": input_allow_bg,\n            \"with_audio\": with_audio,\n            \"without_handles\": without_handles,\n            \"handles_are_set\": handles_are_set\n        }\n\n    def _ffmpeg_arguments(\n        self,\n        output_def,\n        instance,\n        new_repre,\n        temp_data,\n        fill_data,\n        layer_name\n    ):\n        \"\"\"Prepares ffmpeg arguments for expected extraction.\n\n        Prepares input and output arguments based on output definition and\n        input files.\n\n        Args:\n            output_def (dict): Currently processed output definition.\n            instance (Instance): Currently processed instance.\n            new_repre (dict): Representation representing output of this\n                process.\n            temp_data (dict): Base data for successful process.\n        \"\"\"\n\n        # Get FFmpeg arguments from profile presets\n        out_def_ffmpeg_args = output_def.get(\"ffmpeg_args\") or {}\n\n        _ffmpeg_input_args = out_def_ffmpeg_args.get(\"input\") or []\n        _ffmpeg_output_args = out_def_ffmpeg_args.get(\"output\") or []\n        _ffmpeg_video_filters = out_def_ffmpeg_args.get(\"video_filters\") or []\n        _ffmpeg_audio_filters = out_def_ffmpeg_args.get(\"audio_filters\") or []\n\n        # Cleanup empty strings\n        ffmpeg_input_args = [\n            value for value in _ffmpeg_input_args if value.strip()\n        ]\n        ffmpeg_video_filters = [\n            value for value in _ffmpeg_video_filters if value.strip()\n        ]\n        ffmpeg_audio_filters = [\n            value for value in _ffmpeg_audio_filters if value.strip()\n        ]\n\n        ffmpeg_output_args = []\n        for value in _ffmpeg_output_args:\n            value = value.strip()\n            if not value:\n                continue\n            try:\n                value = value.format(**fill_data)\n            except Exception:\n                self.log.warning(\n                    \"Failed to format ffmpeg argument: {}\".format(value),\n                    exc_info=True\n                )\n                pass\n            ffmpeg_output_args.append(value)\n\n        # Prepare input and output filepaths\n        self.input_output_paths(new_repre, output_def, temp_data)\n\n        # Set output frames len to 1 when ouput is single image\n        if (\n            temp_data[\"output_ext_is_image\"]\n            and not temp_data[\"output_is_sequence\"]\n        ):\n            output_frames_len = 1\n\n        else:\n            output_frames_len = (\n                temp_data[\"output_frame_end\"]\n                - temp_data[\"output_frame_start\"]\n                + 1\n            )\n\n        duration_seconds = float(output_frames_len / temp_data[\"fps\"])\n\n        # Define which layer should be used\n        if layer_name:\n            ffmpeg_input_args.extend([\"-layer\", layer_name])\n\n        if temp_data[\"input_is_sequence\"]:\n            # Set start frame of input sequence (just frame in filename)\n            # - definition of input filepath\n            # - add handle start if output should be without handles\n            start_number = temp_data[\"first_sequence_frame\"]\n            if temp_data[\"without_handles\"] and temp_data[\"handles_are_set\"]:\n                start_number += temp_data[\"handle_start\"]\n            ffmpeg_input_args.extend([\n                \"-start_number\", str(start_number)\n            ])\n\n            # TODO add fps mapping `{fps: fraction}` ?\n            # - e.g.: {\n            #     \"25\": \"25/1\",\n            #     \"24\": \"24/1\",\n            #     \"23.976\": \"24000/1001\"\n            # }\n            # Add framerate to input when input is sequence\n            ffmpeg_input_args.extend([\n                \"-framerate\", str(temp_data[\"fps\"])\n            ])\n            # Add duration of an input sequence if output is video\n            if not temp_data[\"output_is_sequence\"]:\n                ffmpeg_input_args.extend([\n                    \"-to\", \"{:0.10f}\".format(duration_seconds)\n                ])\n\n        if temp_data[\"output_is_sequence\"]:\n            # Set start frame of output sequence (just frame in filename)\n            # - this is definition of an output\n            ffmpeg_output_args.extend([\n                \"-start_number\", str(temp_data[\"output_frame_start\"])\n            ])\n\n        # Change output's duration and start point if should not contain\n        # handles\n        if temp_data[\"without_handles\"] and temp_data[\"handles_are_set\"]:\n            # Set output duration in seconds\n            ffmpeg_output_args.extend([\n                \"-t\", \"{:0.10}\".format(duration_seconds)\n            ])\n\n            # Add -ss (start offset in seconds) if input is not sequence\n            if not temp_data[\"input_is_sequence\"]:\n                start_sec = float(temp_data[\"handle_start\"]) / temp_data[\"fps\"]\n                # Set start time without handles\n                # - Skip if start sec is 0.0\n                if start_sec > 0.0:\n                    ffmpeg_input_args.extend([\n                        \"-ss\", \"{:0.10f}\".format(start_sec)\n                    ])\n\n        # Set frame range of output when input or output is sequence\n        elif temp_data[\"output_is_sequence\"]:\n            ffmpeg_output_args.extend([\n                \"-frames:v\", str(output_frames_len)\n            ])\n\n        # Add video/image input path\n        ffmpeg_input_args.extend([\n            \"-i\", path_to_subprocess_arg(temp_data[\"full_input_path\"])\n        ])\n\n        # Add audio arguments if there are any. Skipped when output are images.\n        if not temp_data[\"output_ext_is_image\"] and temp_data[\"with_audio\"]:\n            audio_in_args, audio_filters, audio_out_args = self.audio_args(\n                instance, temp_data, duration_seconds\n            )\n            ffmpeg_input_args.extend(audio_in_args)\n            ffmpeg_audio_filters.extend(audio_filters)\n            ffmpeg_output_args.extend(audio_out_args)\n\n        res_filters = self.rescaling_filters(temp_data, output_def, new_repre)\n        ffmpeg_video_filters.extend(res_filters)\n\n        ffmpeg_input_args = self.split_ffmpeg_args(ffmpeg_input_args)\n\n        lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args)\n        ffmpeg_video_filters.extend(lut_filters)\n\n        bg_alpha = 0\n        bg_color = output_def.get(\"bg_color\")\n        if bg_color:\n            bg_red, bg_green, bg_blue, bg_alpha = bg_color\n\n        if bg_alpha > 0:\n            if not temp_data[\"input_allow_bg\"]:\n                self.log.info((\n                    \"Output definition has defined BG color input was\"\n                    \" resolved as does not support adding BG.\"\n                ))\n            else:\n                bg_color_hex = \"#{0:0>2X}{1:0>2X}{2:0>2X}\".format(\n                    bg_red, bg_green, bg_blue\n                )\n                bg_color_alpha = float(bg_alpha) / 255\n                bg_color_str = \"{}@{}\".format(bg_color_hex, bg_color_alpha)\n\n                self.log.info(\"Applying BG color {}\".format(bg_color_str))\n                color_args = [\n                    \"split=2[bg][fg]\",\n                    \"[bg]drawbox=c={}:replace=1:t=fill[bg]\".format(\n                        bg_color_str\n                    ),\n                    \"[bg][fg]overlay=format=auto\"\n                ]\n                # Prepend bg color change before all video filters\n                # NOTE at the time of creation it is required as video filters\n                #   from settings may affect color of BG\n                #   e.g. `eq` can remove alpha from input\n                for arg in reversed(color_args):\n                    ffmpeg_video_filters.insert(0, arg)\n\n        # Add argument to override output file\n        ffmpeg_output_args.append(\"-y\")\n\n        # NOTE This must be latest added item to output arguments.\n        ffmpeg_output_args.append(\n            path_to_subprocess_arg(temp_data[\"full_output_path\"])\n        )\n\n        return self.ffmpeg_full_args(\n            ffmpeg_input_args,\n            ffmpeg_video_filters,\n            ffmpeg_audio_filters,\n            ffmpeg_output_args\n        )\n\n    def split_ffmpeg_args(self, in_args):\n        \"\"\"Makes sure all entered arguments are separated in individual items.\n\n        Split each argument string with \" -\" to identify if string contains\n        one or more arguments.\n        \"\"\"\n        splitted_args = []\n        for arg in in_args:\n            sub_args = arg.split(\" -\")\n            if len(sub_args) == 1:\n                if arg and arg not in splitted_args:\n                    splitted_args.append(arg)\n                continue\n\n            for idx, arg in enumerate(sub_args):\n                if idx != 0:\n                    arg = \"-\" + arg\n\n                if arg and arg not in splitted_args:\n                    splitted_args.append(arg)\n        return splitted_args\n\n    def ffmpeg_full_args(\n        self, input_args, video_filters, audio_filters, output_args\n    ):\n        \"\"\"Post processing of collected FFmpeg arguments.\n\n        Just verify that output arguments does not contain video or audio\n        filters which may cause issues because of duplicated argument entry.\n        Filters found in output arguments are moved to list they belong to.\n\n        Args:\n            input_args (list): All collected ffmpeg arguments with inputs.\n            video_filters (list): All collected video filters.\n            audio_filters (list): All collected audio filters.\n            output_args (list): All collected ffmpeg output arguments with\n                output filepath.\n\n        Returns:\n            list: Containing all arguments ready to run in subprocess.\n        \"\"\"\n        output_args = self.split_ffmpeg_args(output_args)\n\n        video_args_dentifiers = [\"-vf\", \"-filter:v\"]\n        audio_args_dentifiers = [\"-af\", \"-filter:a\"]\n        for arg in tuple(output_args):\n            for identifier in video_args_dentifiers:\n                if arg.startswith(\"{} \".format(identifier)):\n                    output_args.remove(arg)\n                    arg = arg.replace(identifier, \"\").strip()\n                    video_filters.append(arg)\n\n            for identifier in audio_args_dentifiers:\n                if arg.startswith(\"{} \".format(identifier)):\n                    output_args.remove(arg)\n                    arg = arg.replace(identifier, \"\").strip()\n                    audio_filters.append(arg)\n\n        all_args = [\n            subprocess.list2cmdline(get_ffmpeg_tool_args(\"ffmpeg\"))\n        ]\n        all_args.extend(input_args)\n        if video_filters:\n            all_args.append(\"-filter:v\")\n            all_args.append(\"\\\"{}\\\"\".format(\",\".join(video_filters)))\n\n        if audio_filters:\n            all_args.append(\"-filter:a\")\n            all_args.append(\"\\\"{}\\\"\".format(\",\".join(audio_filters)))\n\n        all_args.extend(output_args)\n\n        return all_args\n\n    def fill_sequence_gaps(self, files, staging_dir, start_frame, end_frame):\n        # type: (list, str, int, int) -> list\n        \"\"\"Fill missing files in sequence by duplicating existing ones.\n\n        This will take nearest frame file and copy it with so as to fill\n        gaps in sequence. Last existing file there is is used to for the\n        hole ahead.\n\n        Args:\n            files (list): List of representation files.\n            staging_dir (str): Path to staging directory.\n            start_frame (int): Sequence start (no matter what files are there)\n            end_frame (int): Sequence end (no matter what files are there)\n\n        Returns:\n            list of added files. Those should be cleaned after work\n                is done.\n\n        Raises:\n            KnownPublishError: if more than one collection is obtained.\n        \"\"\"\n\n        collections = clique.assemble(files)[0]\n        if len(collections) != 1:\n            raise KnownPublishError(\n                \"Multiple collections {} found.\".format(collections))\n\n        col = collections[0]\n\n        # Prepare which hole is filled with what frame\n        #   - the frame is filled only with already existing frames\n        prev_frame = next(iter(col.indexes))\n        hole_frame_to_nearest = {}\n        for frame in range(int(start_frame), int(end_frame) + 1):\n            if frame in col.indexes:\n                prev_frame = frame\n            else:\n                # Use previous frame as source for hole\n                hole_frame_to_nearest[frame] = prev_frame\n\n        # Calculate paths\n        added_files = []\n        col_format = col.format(\"{head}{padding}{tail}\")\n        for hole_frame, src_frame in hole_frame_to_nearest.items():\n            hole_fpath = os.path.join(staging_dir, col_format % hole_frame)\n            src_fpath = os.path.join(staging_dir, col_format % src_frame)\n            if not os.path.isfile(src_fpath):\n                raise KnownPublishError(\n                    \"Missing previously detected file: {}\".format(src_fpath))\n\n            speedcopy.copyfile(src_fpath, hole_fpath)\n            added_files.append(hole_fpath)\n\n        return added_files\n\n    def input_output_paths(self, new_repre, output_def, temp_data):\n        \"\"\"Deduce input nad output file paths based on entered data.\n\n        Input may be sequence of images, video file or single image file and\n        same can be said about output, this method helps to find out what\n        their paths are.\n\n        It is validated that output directory exist and creates if not.\n\n        During process are set \"files\", \"stagingDir\", \"ext\" and\n        \"sequence_file\" (if output is sequence) keys to new representation.\n        \"\"\"\n\n        repre = temp_data[\"origin_repre\"]\n        src_staging_dir = repre[\"stagingDir\"]\n        dst_staging_dir = new_repre[\"stagingDir\"]\n\n        if temp_data[\"input_is_sequence\"]:\n            collections = clique.assemble(repre[\"files\"])[0]\n            full_input_path = os.path.join(\n                src_staging_dir,\n                collections[0].format(\"{head}{padding}{tail}\")\n            )\n\n            filename = collections[0].format(\"{head}\")\n            if filename.endswith(\".\"):\n                filename = filename[:-1]\n\n            # Make sure to have full path to one input file\n            full_input_path_single_file = os.path.join(\n                src_staging_dir, repre[\"files\"][0]\n            )\n\n        else:\n            full_input_path = os.path.join(\n                src_staging_dir, repre[\"files\"]\n            )\n            filename = os.path.splitext(repre[\"files\"])[0]\n\n            # Make sure to have full path to one input file\n            full_input_path_single_file = full_input_path\n\n        filename_suffix = output_def[\"filename_suffix\"]\n\n        output_ext = output_def.get(\"ext\")\n        # Use input extension if output definition do not specify it\n        if output_ext is None:\n            output_ext = os.path.splitext(full_input_path)[1]\n\n        # TODO Define if extension should have dot or not\n        if output_ext.startswith(\".\"):\n            output_ext = output_ext[1:]\n\n        output_ext = output_ext.lower()\n\n        # Store extension to representation\n        new_repre[\"ext\"] = output_ext\n\n        self.log.debug(\"New representation ext: `{}`\".format(output_ext))\n\n        # Output is image file sequence witht frames\n        output_ext_is_image = bool(output_ext in self.image_exts)\n        output_is_sequence = bool(\n            output_ext_is_image\n            and \"sequence\" in output_def[\"tags\"]\n        )\n        if output_is_sequence:\n            new_repre_files = []\n            frame_start = temp_data[\"output_frame_start\"]\n            frame_end = temp_data[\"output_frame_end\"]\n\n            filename_base = \"{}_{}\".format(filename, filename_suffix)\n            # Temporary tempalte for frame filling. Example output:\n            # \"basename.%04d.exr\" when `frame_end` == 1001\n            repr_file = \"{}.%{:0>2}d.{}\".format(\n                filename_base, len(str(frame_end)), output_ext\n            )\n\n            for frame in range(frame_start, frame_end + 1):\n                new_repre_files.append(repr_file % frame)\n\n            new_repre[\"sequence_file\"] = repr_file\n            full_output_path = os.path.join(\n                dst_staging_dir, filename_base, repr_file\n            )\n\n        else:\n            repr_file = \"{}_{}.{}\".format(\n                filename, filename_suffix, output_ext\n            )\n            full_output_path = os.path.join(dst_staging_dir, repr_file)\n            new_repre_files = repr_file\n\n        # Store files to representation\n        new_repre[\"files\"] = new_repre_files\n\n        # Make sure stagingDire exists\n        dst_staging_dir = os.path.normpath(os.path.dirname(full_output_path))\n        if not os.path.exists(dst_staging_dir):\n            self.log.debug(\"Creating dir: {}\".format(dst_staging_dir))\n            os.makedirs(dst_staging_dir)\n\n        # Store stagingDir to representaion\n        new_repre[\"stagingDir\"] = dst_staging_dir\n\n        # Store paths to temp data\n        temp_data[\"full_input_path\"] = full_input_path\n        temp_data[\"full_input_path_single_file\"] = full_input_path_single_file\n        temp_data[\"full_output_path\"] = full_output_path\n\n        # Store information about output\n        temp_data[\"output_ext_is_image\"] = output_ext_is_image\n        temp_data[\"output_is_sequence\"] = output_is_sequence\n\n        self.log.debug(\"Input path {}\".format(full_input_path))\n        self.log.debug(\"Output path {}\".format(full_output_path))\n\n    def audio_args(self, instance, temp_data, duration_seconds):\n        \"\"\"Prepares FFMpeg arguments for audio inputs.\"\"\"\n        audio_in_args = []\n        audio_filters = []\n        audio_out_args = []\n        audio_inputs = instance.data.get(\"audio\")\n        if not audio_inputs:\n            return audio_in_args, audio_filters, audio_out_args\n\n        for audio in audio_inputs:\n            # NOTE modified, always was expected \"frameStartFtrack\" which is\n            # STRANGE?!!! There should be different key, right?\n            # TODO use different frame start!\n            offset_seconds = 0\n            frame_start_ftrack = instance.data.get(\"frameStartFtrack\")\n            if frame_start_ftrack is not None:\n                offset_frames = frame_start_ftrack - audio[\"offset\"]\n                offset_seconds = offset_frames / temp_data[\"fps\"]\n\n            if offset_seconds > 0:\n                audio_in_args.append(\n                    \"-ss {}\".format(offset_seconds)\n                )\n\n            elif offset_seconds < 0:\n                audio_in_args.append(\n                    \"-itsoffset {}\".format(abs(offset_seconds))\n                )\n\n            # Audio duration is offset from `-ss`\n            audio_duration = duration_seconds + offset_seconds\n\n            # Set audio duration\n            audio_in_args.append(\"-to {:0.10f}\".format(audio_duration))\n\n            # Ignore video data from audio input\n            audio_in_args.append(\"-vn\")\n\n            # Add audio input path\n            audio_in_args.append(\"-i {}\".format(\n                path_to_subprocess_arg(audio[\"filename\"])\n            ))\n\n        # NOTE: These were changed from input to output arguments.\n        # NOTE: value in \"-ac\" was hardcoded to 2, changed to audio inputs len.\n        # Need to merge audio if there are more than 1 input.\n        if len(audio_inputs) > 1:\n            audio_out_args.append(\"-filter_complex amerge\")\n            audio_out_args.append(\"-ac {}\".format(len(audio_inputs)))\n\n        return audio_in_args, audio_filters, audio_out_args\n\n    def get_letterbox_filters(\n        self,\n        letter_box_def,\n        output_width,\n        output_height\n    ):\n        output = []\n\n        ratio = letter_box_def[\"ratio\"]\n        fill_color = letter_box_def[\"fill_color\"]\n        f_red, f_green, f_blue, f_alpha = fill_color\n        fill_color_hex = \"{0:0>2X}{1:0>2X}{2:0>2X}\".format(\n            f_red, f_green, f_blue\n        )\n        fill_color_alpha = float(f_alpha) / 255\n\n        line_thickness = letter_box_def[\"line_thickness\"]\n        line_color = letter_box_def[\"line_color\"]\n        l_red, l_green, l_blue, l_alpha = line_color\n        line_color_hex = \"{0:0>2X}{1:0>2X}{2:0>2X}\".format(\n            l_red, l_green, l_blue\n        )\n        line_color_alpha = float(l_alpha) / 255\n\n        # test ratios and define if pillar or letter boxes\n        output_ratio = float(output_width) / float(output_height)\n        self.log.debug(\"Output ratio: {} LetterBox ratio: {}\".format(\n            output_ratio, ratio\n        ))\n        pillar = output_ratio > ratio\n        need_mask = format(output_ratio, \".3f\") != format(ratio, \".3f\")\n        if not need_mask:\n            return []\n\n        if not pillar:\n            if fill_color_alpha > 0:\n                top_box = (\n                    \"drawbox=0:0:{width}\"\n                    \":round(({height}-({width}/{ratio}))/2)\"\n                    \":t=fill:c={color}@{alpha}\"\n                ).format(\n                    width=output_width,\n                    height=output_height,\n                    ratio=ratio,\n                    color=fill_color_hex,\n                    alpha=fill_color_alpha\n                )\n\n                bottom_box = (\n                    \"drawbox=0\"\n                    \":{height}-round(({height}-({width}/{ratio}))/2)\"\n                    \":{width}\"\n                    \":round(({height}-({width}/{ratio}))/2)\"\n                    \":t=fill:c={color}@{alpha}\"\n                ).format(\n                    width=output_width,\n                    height=output_height,\n                    ratio=ratio,\n                    color=fill_color_hex,\n                    alpha=fill_color_alpha\n                )\n                output.extend([top_box, bottom_box])\n\n            if line_color_alpha > 0 and line_thickness > 0:\n                top_line = (\n                    \"drawbox=0\"\n                    \":round(({height}-({width}/{ratio}))/2)-{l_thick}\"\n                    \":{width}:{l_thick}:t=fill:c={l_color}@{l_alpha}\"\n                ).format(\n                    width=output_width,\n                    height=output_height,\n                    ratio=ratio,\n                    l_thick=line_thickness,\n                    l_color=line_color_hex,\n                    l_alpha=line_color_alpha\n                )\n                bottom_line = (\n                    \"drawbox=0\"\n                    \":{height}-round(({height}-({width}/{ratio}))/2)\"\n                    \":{width}:{l_thick}:t=fill:c={l_color}@{l_alpha}\"\n                ).format(\n                    width=output_width,\n                    height=output_height,\n                    ratio=ratio,\n                    l_thick=line_thickness,\n                    l_color=line_color_hex,\n                    l_alpha=line_color_alpha\n                )\n                output.extend([top_line, bottom_line])\n\n        else:\n            if fill_color_alpha > 0:\n                left_box = (\n                    \"drawbox=0:0\"\n                    \":round(({width}-({height}*{ratio}))/2)\"\n                    \":{height}\"\n                    \":t=fill:c={color}@{alpha}\"\n                ).format(\n                    width=output_width,\n                    height=output_height,\n                    ratio=ratio,\n                    color=fill_color_hex,\n                    alpha=fill_color_alpha\n                )\n\n                right_box = (\n                    \"drawbox=\"\n                    \"{width}-round(({width}-({height}*{ratio}))/2)\"\n                    \":0\"\n                    \":round(({width}-({height}*{ratio}))/2)\"\n                    \":{height}\"\n                    \":t=fill:c={color}@{alpha}\"\n                ).format(\n                    width=output_width,\n                    height=output_height,\n                    ratio=ratio,\n                    color=fill_color_hex,\n                    alpha=fill_color_alpha\n                )\n                output.extend([left_box, right_box])\n\n            if line_color_alpha > 0 and line_thickness > 0:\n                left_line = (\n                    \"drawbox=round(({width}-({height}*{ratio}))/2)\"\n                    \":0:{l_thick}:{height}:t=fill:c={l_color}@{l_alpha}\"\n                ).format(\n                    width=output_width,\n                    height=output_height,\n                    ratio=ratio,\n                    l_thick=line_thickness,\n                    l_color=line_color_hex,\n                    l_alpha=line_color_alpha\n                )\n\n                right_line = (\n                    \"drawbox={width}-round(({width}-({height}*{ratio}))/2)\"\n                    \":0:{l_thick}:{height}:t=fill:c={l_color}@{l_alpha}\"\n                ).format(\n                    width=output_width,\n                    height=output_height,\n                    ratio=ratio,\n                    l_thick=line_thickness,\n                    l_color=line_color_hex,\n                    l_alpha=line_color_alpha\n                )\n                output.extend([left_line, right_line])\n\n        return output\n\n    def rescaling_filters(self, temp_data, output_def, new_repre):\n        \"\"\"Prepare vieo filters based on tags in new representation.\n\n        It is possible to add letterboxes to output video or rescale to\n        different resolution.\n\n        During this preparation \"resolutionWidth\" and \"resolutionHeight\" are\n        set to new representation.\n        \"\"\"\n        filters = []\n\n        # if reformat input video file is already reforamted from upstream\n        reformat_in_baking = bool(\"reformated\" in new_repre[\"tags\"])\n        self.log.debug(\"reformat_in_baking: `{}`\".format(reformat_in_baking))\n\n        # Get instance data\n        pixel_aspect = temp_data[\"pixel_aspect\"]\n\n        if reformat_in_baking:\n            self.log.debug((\n                \"Using resolution from input. It is already \"\n                \"reformated from upstream process\"\n            ))\n            pixel_aspect = 1\n\n        # NOTE Skipped using instance's resolution\n        full_input_path_single_file = temp_data[\"full_input_path_single_file\"]\n        try:\n            streams = get_ffprobe_streams(\n                full_input_path_single_file, self.log\n            )\n        except Exception as exc:\n            raise AssertionError((\n                \"FFprobe couldn't read information about input file: \\\"{}\\\".\"\n                \" Error message: {}\"\n            ).format(full_input_path_single_file, str(exc)))\n\n        # Try to find first stream with defined 'width' and 'height'\n        # - this is to avoid order of streams where audio can be as first\n        # - there may be a better way (checking `codec_type`?)\n        input_width = None\n        input_height = None\n        output_width = None\n        output_height = None\n        for stream in streams:\n            if \"width\" in stream and \"height\" in stream:\n                input_width = int(stream[\"width\"])\n                input_height = int(stream[\"height\"])\n                break\n\n        # Get instance data\n        pixel_aspect = temp_data[\"pixel_aspect\"]\n        if reformat_in_baking:\n            self.log.debug((\n                \"Using resolution from input. It is already \"\n                \"reformated from upstream process\"\n            ))\n            pixel_aspect = 1\n            output_width = input_width\n            output_height = input_height\n\n        # Raise exception of any stream didn't define input resolution\n        if input_width is None:\n            raise AssertionError((\n                \"FFprobe couldn't read resolution from input file: \\\"{}\\\"\"\n            ).format(full_input_path_single_file))\n\n        # NOTE Setting only one of `width` or `heigth` is not allowed\n        # - settings value can't have None but has value of 0\n        output_width = output_def.get(\"width\") or output_width or None\n        output_height = output_def.get(\"height\") or output_height or None\n        # Force to use input resolution if output resolution was not defined\n        #   in settings. Resolution from instance is not used when\n        #   'use_input_res' is set to 'True'.\n        use_input_res = False\n\n        # Overscan color\n        overscan_color_value = \"black\"\n        overscan_color = output_def.get(\"overscan_color\")\n        if overscan_color:\n            bg_red, bg_green, bg_blue, _ = overscan_color\n            overscan_color_value = \"#{0:0>2X}{1:0>2X}{2:0>2X}\".format(\n                bg_red, bg_green, bg_blue\n            )\n        self.log.debug(\"Overscan color: `{}`\".format(overscan_color_value))\n\n        # Scale input to have proper pixel aspect ratio\n        # - scale width by the pixel aspect ratio\n        scale_pixel_aspect = output_def.get(\"scale_pixel_aspect\", True)\n        if scale_pixel_aspect and pixel_aspect != 1:\n            # Change input width after pixel aspect\n            input_width = int(input_width * pixel_aspect)\n            use_input_res = True\n            filters.append((\n                \"scale={}x{}:flags=lanczos\".format(input_width, input_height)\n            ))\n\n        # Convert overscan value video filters\n        overscan_crop = output_def.get(\"overscan_crop\")\n        overscan = OverscanCrop(\n            input_width, input_height, overscan_crop, overscan_color_value\n        )\n        overscan_crop_filters = overscan.video_filters()\n        # Add overscan filters to filters if are any and modify input\n        #   resolution by it's values\n        if overscan_crop_filters:\n            filters.extend(overscan_crop_filters)\n            # Change input resolution after overscan crop\n            input_width = overscan.width()\n            input_height = overscan.height()\n            use_input_res = True\n\n        # Make sure input width and height is not an odd number\n        input_width_is_odd = bool(input_width % 2 != 0)\n        input_height_is_odd = bool(input_height % 2 != 0)\n        if input_width_is_odd or input_height_is_odd:\n            # Add padding to input and make sure this filter is at first place\n            filters.append(\"pad=width=ceil(iw/2)*2:height=ceil(ih/2)*2\")\n\n            # Change input width or height as first filter will change them\n            if input_width_is_odd:\n                self.log.info((\n                    \"Converting input width from odd to even number. {} -> {}\"\n                ).format(input_width, input_width + 1))\n                input_width += 1\n\n            if input_height_is_odd:\n                self.log.info((\n                    \"Converting input height from odd to even number. {} -> {}\"\n                ).format(input_height, input_height + 1))\n                input_height += 1\n\n        self.log.debug(\"pixel_aspect: `{}`\".format(pixel_aspect))\n        self.log.debug(\"input_width: `{}`\".format(input_width))\n        self.log.debug(\"input_height: `{}`\".format(input_height))\n\n        # Use instance resolution if output definition has not set it\n        #   - use instance resolution only if there were not scale changes\n        #       that may massivelly affect output 'use_input_res'\n        if not use_input_res and output_width is None or output_height is None:\n            output_width = temp_data[\"resolution_width\"]\n            output_height = temp_data[\"resolution_height\"]\n\n        # Use source's input resolution instance does not have set it.\n        if output_width is None or output_height is None:\n            self.log.debug(\"Using resolution from input.\")\n            output_width = input_width\n            output_height = input_height\n\n        output_width = int(output_width)\n        output_height = int(output_height)\n\n        # Make sure output width and height is not an odd number\n        # When this can happen:\n        # - if output definition has set width and height with odd number\n        # - `instance.data` contain width and height with odd numbeer\n        if output_width % 2 != 0:\n            self.log.warning((\n                \"Converting output width from odd to even number. {} -> {}\"\n            ).format(output_width, output_width + 1))\n            output_width += 1\n\n        if output_height % 2 != 0:\n            self.log.warning((\n                \"Converting output height from odd to even number. {} -> {}\"\n            ).format(output_height, output_height + 1))\n            output_height += 1\n\n        self.log.debug(\n            \"Output resolution is {}x{}\".format(output_width, output_height)\n        )\n\n        letter_box_def = output_def[\"letter_box\"]\n        letter_box_enabled = letter_box_def[\"enabled\"]\n\n        # Skip processing if resolution is same as input's and letterbox is\n        # not set\n        if (\n            output_width == input_width\n            and output_height == input_height\n            and not letter_box_enabled\n        ):\n            self.log.debug(\n                \"Output resolution is same as input's\"\n                \" and \\\"letter_box\\\" key is not set. Skipping reformat part.\"\n            )\n            new_repre[\"resolutionWidth\"] = input_width\n            new_repre[\"resolutionHeight\"] = input_height\n            return filters\n\n        # scaling none square pixels and 1920 width\n        if input_height != output_height or input_width != output_width:\n            filters.extend([\n                (\n                    \"scale={}x{}\"\n                    \":flags=lanczos\"\n                    \":force_original_aspect_ratio=decrease\"\n                ).format(output_width, output_height),\n                \"pad={}:{}:(ow-iw)/2:(oh-ih)/2:{}\".format(\n                    output_width, output_height,\n                    overscan_color_value\n                ),\n                \"setsar=1\"\n            ])\n\n        # letter_box\n        if letter_box_enabled:\n            filters.extend(\n                self.get_letterbox_filters(\n                    letter_box_def,\n                    output_width,\n                    output_height\n                )\n            )\n\n        new_repre[\"resolutionWidth\"] = output_width\n        new_repre[\"resolutionHeight\"] = output_height\n\n        return filters\n\n    def lut_filters(self, new_repre, instance, input_args):\n        \"\"\"Add lut file to output ffmpeg filters.\"\"\"\n        filters = []\n        # baking lut file application\n        lut_path = instance.data.get(\"lutPath\")\n        if not lut_path or \"bake-lut\" not in new_repre[\"tags\"]:\n            return filters\n\n        # Prepare path for ffmpeg argument\n        lut_path = lut_path.replace(\"\\\\\", \"/\").replace(\":\", \"\\\\:\")\n\n        # Remove gamma from input arguments\n        if \"-gamma\" in input_args:\n            input_args.remove(\"-gamme\")\n\n        # Prepare filters\n        filters.append(\"lut3d=file='{}'\".format(lut_path))\n        # QUESTION hardcoded colormatrix?\n        filters.append(\"colormatrix=bt601:bt709\")\n\n        self.log.info(\"Added Lut to ffmpeg command.\")\n\n        return filters\n\n    def main_family_from_instance(self, instance):\n        \"\"\"Returns main family of entered instance.\"\"\"\n        family = instance.data.get(\"family\")\n        if not family:\n            family = instance.data[\"families\"][0]\n        return family\n\n    def families_from_instance(self, instance):\n        \"\"\"Returns all families of entered instance.\"\"\"\n        families = []\n        family = instance.data.get(\"family\")\n        if family:\n            families.append(family)\n\n        for family in (instance.data.get(\"families\") or tuple()):\n            if family not in families:\n                families.append(family)\n        return families\n\n    def families_filter_validation(self, families, output_families_filter):\n        \"\"\"Determines if entered families intersect with families filters.\n\n        All family values are lowered to avoid unexpected results.\n        \"\"\"\n\n        families_filter_lower = set(family.lower() for family in\n                                    output_families_filter\n                                    # Exclude empty filter values\n                                    if family)\n        if not families_filter_lower:\n            return True\n        return any(family.lower() in families_filter_lower\n                   for family in families)\n\n    def filter_output_defs(self, profile, subset_name, families):\n        \"\"\"Return outputs matching input instance families.\n\n        Output definitions without families filter are marked as valid.\n\n        Args:\n            profile (dict): Profile from presets matching current context.\n            families (list): All families of current instance.\n            subset_name (str): name of subset\n\n        Returns:\n            list: Containg all output definitions matching entered families.\n        \"\"\"\n        outputs = profile.get(\"outputs\") or {}\n        if not outputs:\n            return outputs\n\n        filtered_outputs = {}\n        for filename_suffix, output_def in outputs.items():\n            output_filters = output_def.get(\"filter\")\n            # If no filter on output preset, skip filtering and add output\n            # profile for farther processing\n            if not output_filters:\n                filtered_outputs[filename_suffix] = output_def\n                continue\n\n            families_filters = output_filters.get(\"families\")\n            if not self.families_filter_validation(families, families_filters):\n                continue\n\n            # Subsets name filters\n            subset_filters = [\n                subset_filter\n                for subset_filter in output_filters.get(\"subsets\", [])\n                # Skip empty strings\n                if subset_filter\n            ]\n            if subset_name and subset_filters:\n                match = False\n                for subset_filter in subset_filters:\n                    compiled = re.compile(subset_filter)\n                    if compiled.search(subset_name):\n                        match = True\n                        break\n\n                if not match:\n                    continue\n\n            filtered_outputs[filename_suffix] = output_def\n\n        return filtered_outputs\n\n    def filter_outputs_by_custom_tags(self, outputs, custom_tags):\n        \"\"\"Filter output definitions by entered representation custom_tags.\n\n        Output definitions without custom_tags filter are marked as invalid,\n        only in case representation is having any custom_tags defined.\n\n        Args:\n            outputs (list): Contain list of output definitions from presets.\n            custom_tags (list): Custom Tags of processed representation.\n\n        Returns:\n            list: Containg all output definitions matching entered tags.\n        \"\"\"\n\n        filtered_outputs = []\n        repre_c_tags_low = [tag.lower() for tag in (custom_tags or [])]\n        for output_def in outputs:\n            tag_filters = output_def.get(\"filter\", {}).get(\"custom_tags\")\n\n            if not custom_tags and not tag_filters:\n                # Definition is valid if both tags are empty\n                valid = True\n\n            elif not custom_tags or not tag_filters:\n                # Invalid if one is empty\n                valid = False\n\n            else:\n                # Check if output definition tags are in representation tags\n                valid = False\n                # lower all filter tags\n                tag_filters_low = [tag.lower() for tag in tag_filters]\n                # check if any repre tag is not in filter tags\n                for tag in repre_c_tags_low:\n                    if tag in tag_filters_low:\n                        valid = True\n                        break\n\n            if valid:\n                filtered_outputs.append(output_def)\n\n        self.log.debug(\"__ filtered_outputs: {}\".format(\n            [_o[\"filename_suffix\"] for _o in filtered_outputs]\n        ))\n\n        return filtered_outputs\n\n    def add_video_filter_args(self, args, inserting_arg):\n        \"\"\"\n        Fixing video filter arguments to be one long string\n\n        Args:\n            args (list): list of string arguments\n            inserting_arg (str): string argument we want to add\n                                 (without flag `-vf`)\n\n        Returns:\n            str: long joined argument to be added back to list of arguments\n\n        \"\"\"\n        # find all video format settings\n        vf_settings = [p for p in args\n                       for v in [\"-filter:v\", \"-vf\"]\n                       if v in p]\n        self.log.debug(\"_ vf_settings: `{}`\".format(vf_settings))\n\n        # remove them from output args list\n        for p in vf_settings:\n            self.log.debug(\"_ remove p: `{}`\".format(p))\n            args.remove(p)\n            self.log.debug(\"_ args: `{}`\".format(args))\n\n        # strip them from all flags\n        vf_fixed = [p.replace(\"-vf \", \"\").replace(\"-filter:v \", \"\")\n                    for p in vf_settings]\n\n        self.log.debug(\"_ vf_fixed: `{}`\".format(vf_fixed))\n        vf_fixed.insert(0, inserting_arg)\n        self.log.debug(\"_ vf_fixed: `{}`\".format(vf_fixed))\n        # create new video filter setting\n        vf_back = \"-vf \" + \",\".join(vf_fixed)\n\n        return vf_back\n\n\n@six.add_metaclass(ABCMeta)\nclass _OverscanValue:\n    def __repr__(self):\n        return \"<{}> {}\".format(self.__class__.__name__, str(self))\n\n    @abstractmethod\n    def copy(self):\n        \"\"\"Create a copy of object.\"\"\"\n        pass\n\n    @abstractmethod\n    def size_for(self, value):\n        \"\"\"Calculate new value for passed value.\"\"\"\n        pass\n\n\nclass PixValueExplicit(_OverscanValue):\n    def __init__(self, value):\n        self._value = int(value)\n\n    def __str__(self):\n        return \"{}px\".format(self._value)\n\n    def copy(self):\n        return PixValueExplicit(self._value)\n\n    def size_for(self, value):\n        if self._value == 0:\n            return value\n        return self._value\n\n\nclass PercentValueExplicit(_OverscanValue):\n    def __init__(self, value):\n        self._value = float(value)\n\n    def __str__(self):\n        return \"{}%\".format(abs(self._value))\n\n    def copy(self):\n        return PercentValueExplicit(self._value)\n\n    def size_for(self, value):\n        if self._value == 0:\n            return value\n        return int((value / 100) * self._value)\n\n\nclass PixValueRelative(_OverscanValue):\n    def __init__(self, value):\n        self._value = int(value)\n\n    def __str__(self):\n        sign = \"-\" if self._value < 0 else \"+\"\n        return \"{}{}px\".format(sign, abs(self._value))\n\n    def copy(self):\n        return PixValueRelative(self._value)\n\n    def size_for(self, value):\n        return value + self._value\n\n\nclass PercentValueRelative(_OverscanValue):\n    def __init__(self, value):\n        self._value = float(value)\n\n    def __str__(self):\n        return \"{}%\".format(self._value)\n\n    def copy(self):\n        return PercentValueRelative(self._value)\n\n    def size_for(self, value):\n        if self._value == 0:\n            return value\n\n        offset = int((value / 100) * self._value)\n\n        return value + offset\n\n\nclass PercentValueRelativeSource(_OverscanValue):\n    def __init__(self, value, source_sign):\n        self._value = float(value)\n        if source_sign not in (\"-\", \"+\"):\n            raise ValueError(\n                \"Invalid sign value \\\"{}\\\" expected \\\"-\\\" or \\\"+\\\"\".format(\n                    source_sign\n                )\n            )\n        self._source_sign = source_sign\n\n    def __str__(self):\n        return \"{}%{}\".format(self._value, self._source_sign)\n\n    def copy(self):\n        return PercentValueRelativeSource(self._value, self._source_sign)\n\n    def size_for(self, value):\n        if self._value == 0:\n            return value\n        return int((value * 100) / (100 - self._value))\n\n\nclass OverscanCrop:\n    \"\"\"Helper class to read overscan string and calculate output resolution.\n\n    It is possible to enter single value for both width and height, or\n    two values for width and height. Overscan string may have a few variants.\n    Each variant define output size for input size.\n\n    ### Example\n    For input size: 2200px\n\n    | String   | Output | Description                                     |\n    |----------|--------|-------------------------------------------------|\n    | \"\"       | 2200px | Empty string does nothing.                      |\n    | \"10%\"    | 220px  | Explicit percent size.                          |\n    | \"-10%\"   | 1980px | Relative percent size (decrease).               |\n    | \"+10%\"   | 2420px | Relative percent size (increase).               |\n    | \"-10%+\"  | 2000px | Relative percent size to output size.           |\n    | \"300px\"  | 300px  | Explicit output size cropped or expanded.       |\n    | \"-300px\" | 1900px | Relative pixel size (decrease).                 |\n    | \"+300px\" | 2500px | Relative pixel size (increase).                 |\n    | \"300\"    | 300px  | Value without \"%\" and \"px\" is used as has \"px\". |\n\n    Value without sign (+/-) in is always explicit and value with sign is\n    relative. Output size for \"200px\" and \"+200px\" are not the same.\n    Values \"0\", \"0px\" or \"0%\" are ignored.\n\n    All values that cause output resolution smaller than 1 pixel are invalid.\n\n    Value \"-10%+\" is a special case which says that input's resolution is\n    bigger by 10% than expected output.\n\n    It is possible to combine these variants to define different output for\n    width and height.\n\n    Resolution: 2000px 1000px\n\n    | String        | Output        |\n    |---------------|---------------|\n    | \"100px 120px\" | 2100px 1120px |\n    | \"-10% -200px\" | 1800px 800px  |\n    \"\"\"\n\n    item_regex = re.compile(r\"([\\+\\-])?([0-9]+)(.+)?\")\n    relative_source_regex = re.compile(r\"%([\\+\\-])\")\n\n    def __init__(\n        self, input_width, input_height, string_value, overscan_color=None\n    ):\n        # Make sure that is not None\n        string_value = string_value or \"\"\n\n        self.input_width = input_width\n        self.input_height = input_height\n        self.overscan_color = overscan_color\n\n        width, height = self._convert_string_to_values(string_value)\n        self._width_value = width\n        self._height_value = height\n\n        self._string_value = string_value\n\n    def __str__(self):\n        return \"{}\".format(self._string_value)\n\n    def __repr__(self):\n        return \"<{}>\".format(self.__class__.__name__)\n\n    def width(self):\n        \"\"\"Calculated width.\"\"\"\n        return self._width_value.size_for(self.input_width)\n\n    def height(self):\n        \"\"\"Calculated height.\"\"\"\n        return self._height_value.size_for(self.input_height)\n\n    def video_filters(self):\n        \"\"\"FFmpeg video filters to achieve expected result.\n\n        Filter may be empty, use \"crop\" filter, \"pad\" filter or combination of\n        \"crop\" and \"pad\".\n\n        Returns:\n            list: FFmpeg video filters.\n        \"\"\"\n        # crop=width:height:x:y - explicit start x, y position\n        # crop=width:height     - x, y are related to center by width/height\n        # pad=width:heigth:x:y  - explicit start x, y position\n        # pad=width:heigth      - x, y are set to 0 by default\n\n        width = self.width()\n        height = self.height()\n\n        output = []\n        if self.input_width == width and self.input_height == height:\n            return output\n\n        # Make sure resolution has odd numbers\n        if width % 2 == 1:\n            width -= 1\n\n        if height % 2 == 1:\n            height -= 1\n\n        if width <= self.input_width and height <= self.input_height:\n            output.append(\"crop={}:{}\".format(width, height))\n\n        elif width >= self.input_width and height >= self.input_height:\n            output.append(\n                \"pad={}:{}:(iw-ow)/2:(ih-oh)/2:{}\".format(\n                    width, height, self.overscan_color\n                )\n            )\n\n        elif width > self.input_width and height < self.input_height:\n            output.append(\"crop=iw:{}\".format(height))\n            output.append(\"pad={}:ih:(iw-ow)/2:(ih-oh)/2:{}\".format(\n                width, self.overscan_color\n            ))\n\n        elif width < self.input_width and height > self.input_height:\n            output.append(\"crop={}:ih\".format(width))\n            output.append(\"pad=iw:{}:(iw-ow)/2:(ih-oh)/2:{}\".format(\n                height, self.overscan_color\n            ))\n\n        return output\n\n    def _convert_string_to_values(self, orig_string_value):\n        string_value = orig_string_value.strip().lower()\n        if not string_value:\n            return [PixValueRelative(0), PixValueRelative(0)]\n\n        # Replace \"px\" (and spaces before) with single space\n        string_value = re.sub(r\"([ ]+)?px\", \" \", string_value)\n        string_value = re.sub(r\"([ ]+)%\", \"%\", string_value)\n        # Make sure +/- sign at the beggining of string is next to number\n        string_value = re.sub(r\"^([\\+\\-])[ ]+\", \"\\g<1>\", string_value)\n        # Make sure +/- sign in the middle has zero spaces before number under\n        #   which belongs\n        string_value = re.sub(\n            r\"[ ]([\\+\\-])[ ]+([0-9])\",\n            r\" \\g<1>\\g<2>\",\n            string_value\n        )\n        string_parts = [\n            part\n            for part in string_value.split(\" \")\n            if part\n        ]\n\n        error_msg = \"Invalid string for rescaling \\\"{}\\\"\".format(\n            orig_string_value\n        )\n        if 1 > len(string_parts) > 2:\n            raise ValueError(error_msg)\n\n        output = []\n        for item in string_parts:\n            groups = self.item_regex.findall(item)\n            if not groups:\n                raise ValueError(error_msg)\n\n            relative_sign, value, ending = groups[0]\n            if not relative_sign:\n                if not ending:\n                    output.append(PixValueExplicit(value))\n                else:\n                    output.append(PercentValueExplicit(value))\n            else:\n                source_sign_group = self.relative_source_regex.findall(ending)\n                if not ending:\n                    output.append(PixValueRelative(int(relative_sign + value)))\n\n                elif source_sign_group:\n                    source_sign = source_sign_group[0]\n                    output.append(PercentValueRelativeSource(\n                        float(relative_sign + value), source_sign\n                    ))\n                else:\n                    output.append(\n                        PercentValueRelative(float(relative_sign + value))\n                    )\n\n        if len(output) == 1:\n            width = output.pop(0)\n            height = width.copy()\n        else:\n            width, height = output\n\n        return width, height\n"
  },
  {
    "path": "openpype/plugins/publish/extract_review_slate.py",
    "content": "import os\nimport re\nimport subprocess\nfrom pprint import pformat\n\nimport pyblish.api\n\nfrom openpype.lib import (\n    path_to_subprocess_arg,\n    run_subprocess,\n    get_ffmpeg_tool_args,\n    get_ffprobe_data,\n    get_ffprobe_streams,\n    get_ffmpeg_codec_args,\n    get_ffmpeg_format_args,\n)\nfrom openpype.pipeline import publish\nfrom openpype.pipeline.publish import KnownPublishError\n\n\nclass ExtractReviewSlate(publish.Extractor):\n    \"\"\"\n    Will add slate frame at the start of the video files\n    \"\"\"\n\n    label = \"Review with Slate frame\"\n    order = pyblish.api.ExtractorOrder + 0.031\n    families = [\"slate\", \"review\"]\n    match = pyblish.api.Subset\n\n    SUFFIX = \"_slate\"\n\n    hosts = [\"nuke\", \"shell\"]\n    optional = True\n\n    def process(self, instance):\n        inst_data = instance.data\n        if \"representations\" not in inst_data:\n            raise RuntimeError(\"Burnin needs already created mov to work on.\")\n\n        # get slates frame from upstream\n        slates_data = inst_data.get(\"slateFrames\")\n        if not slates_data:\n            # make it backward compatible and open for slates generator\n            # premium plugin\n            slates_data = {\n                \"*\": inst_data[\"slateFrame\"]\n            }\n\n        self.log.debug(\"_ slates_data: {}\".format(pformat(slates_data)))\n\n        if \"reviewToWidth\" in inst_data:\n            use_legacy_code = True\n        else:\n            use_legacy_code = False\n\n        pixel_aspect = inst_data.get(\"pixelAspect\", 1)\n        fps = inst_data.get(\"fps\")\n        self.log.debug(\"fps {} \".format(fps))\n\n        for idx, repre in enumerate(inst_data[\"representations\"]):\n            self.log.debug(\"repre ({}): `{}`\".format(idx + 1, repre))\n\n            p_tags = repre.get(\"tags\", [])\n            if \"slate-frame\" not in p_tags:\n                continue\n\n            # get repre file\n            stagingdir = repre[\"stagingDir\"]\n            input_file = \"{0}\".format(repre[\"files\"])\n            input_path = os.path.join(\n                os.path.normpath(stagingdir), repre[\"files\"])\n            self.log.debug(\"__ input_path: {}\".format(input_path))\n\n            streams = get_ffprobe_streams(\n                input_path, self.log\n            )\n            # get slate data\n            slate_path = self._get_slate_path(input_file, slates_data)\n            self.log.debug(\"_ slate_path: {}\".format(slate_path))\n\n            slate_width, slate_height = self._get_slates_resolution(slate_path)\n\n            # Get video metadata\n            (\n                input_width,\n                input_height,\n                input_timecode,\n                input_frame_rate,\n                input_pixel_aspect\n            ) = self._get_video_metadata(streams)\n            if input_pixel_aspect:\n                pixel_aspect = input_pixel_aspect\n\n            # Raise exception of any stream didn't define input resolution\n            if input_width is None:\n                raise KnownPublishError(\n                    \"FFprobe couldn't read resolution from input file: \\\"{}\\\"\"\n                    .format(input_path)\n                )\n\n            (\n                audio_codec,\n                audio_channels,\n                audio_sample_rate,\n                audio_channel_layout,\n                input_audio\n            ) = self._get_audio_metadata(streams)\n\n            # values are set in ExtractReview\n            if use_legacy_code:\n                to_width = inst_data[\"reviewToWidth\"]\n                to_height = inst_data[\"reviewToHeight\"]\n            else:\n                to_width = input_width\n                to_height = input_height\n\n            self.log.debug(\"to_width: `{}`\".format(to_width))\n            self.log.debug(\"to_height: `{}`\".format(to_height))\n\n            # defining image ratios\n            resolution_ratio = (\n                (float(slate_width) * pixel_aspect) / slate_height\n            )\n            delivery_ratio = float(to_width) / float(to_height)\n            self.log.debug(\"resolution_ratio: `{}`\".format(resolution_ratio))\n            self.log.debug(\"delivery_ratio: `{}`\".format(delivery_ratio))\n\n            # get scale factor\n            scale_factor_by_height = float(to_height) / slate_height\n            scale_factor_by_width = float(to_width) / (\n                slate_width * pixel_aspect\n            )\n\n            # shorten two decimals long float number for testing conditions\n            resolution_ratio_test = float(\"{:0.2f}\".format(resolution_ratio))\n            delivery_ratio_test = float(\"{:0.2f}\".format(delivery_ratio))\n\n            self.log.debug(\"__ scale_factor_by_width: `{}`\".format(\n                scale_factor_by_width\n            ))\n            self.log.debug(\"__ scale_factor_by_height: `{}`\".format(\n                scale_factor_by_height\n            ))\n\n            _remove_at_end = []\n\n            ext = os.path.splitext(input_file)[1]\n            output_file = input_file.replace(ext, \"\") + self.SUFFIX + ext\n\n            _remove_at_end.append(input_path)\n\n            output_path = os.path.join(\n                os.path.normpath(stagingdir), output_file)\n            self.log.debug(\"__ output_path: {}\".format(output_path))\n\n            input_args = []\n            output_args = []\n\n            # preset's input data\n            if use_legacy_code:\n                input_args.extend(repre[\"_profile\"].get('input', []))\n            else:\n                input_args.extend(repre[\"outputDef\"].get('input', []))\n\n            input_args.extend([\n                \"-loop\", \"1\",\n                \"-i\", path_to_subprocess_arg(slate_path),\n                \"-r\", str(input_frame_rate),\n                \"-frames:v\", \"1\",\n            ])\n\n            # add timecode from source to the slate, substract one frame\n            offset_timecode = \"\"\n            if input_timecode:\n                offset_timecode = self._tc_offset(\n                    str(input_timecode),\n                    framerate=fps,\n                    frame_offset=-1\n                )\n                self.log.debug(\"Slate Timecode: `{}`\".format(\n                    offset_timecode\n                ))\n\n            if use_legacy_code:\n                format_args = []\n                codec_args = repre[\"_profile\"].get('codec', [])\n                output_args.extend(codec_args)\n                # preset's output data\n                output_args.extend(repre[\"_profile\"].get('output', []))\n            else:\n                # Codecs are copied from source for whole input\n                format_args, codec_args = self._get_format_codec_args(repre)\n                output_args.extend(format_args)\n                output_args.extend(codec_args)\n\n            # make sure colors are correct\n            output_args.extend([\n                \"-color_primaries\", \"bt709\",\n                \"-color_trc\", \"bt709\",\n                \"-colorspace\", \"bt709\",\n            ])\n\n            # scaling none square pixels and 1920 width\n            if (\n                # Always scale slate if not legacy\n                not use_legacy_code or\n                # Legacy code required reformat tag\n                (use_legacy_code and \"reformat\" in p_tags)\n            ):\n                if resolution_ratio_test < delivery_ratio_test:\n                    self.log.debug(\"lower then delivery\")\n                    width_scale = int(slate_width * scale_factor_by_height)\n                    width_half_pad = int((to_width - width_scale) / 2)\n                    height_scale = to_height\n                    height_half_pad = 0\n                else:\n                    self.log.debug(\"heigher then delivery\")\n                    width_scale = to_width\n                    width_half_pad = 0\n                    height_scale = int(slate_height * scale_factor_by_width)\n                    height_half_pad = int((to_height - height_scale) / 2)\n\n                self.log.debug(\n                    \"__ width_scale: `{}`\".format(width_scale)\n                )\n                self.log.debug(\n                    \"__ width_half_pad: `{}`\".format(width_half_pad)\n                )\n                self.log.debug(\n                    \"__ height_scale: `{}`\".format(height_scale)\n                )\n                self.log.debug(\n                    \"__ height_half_pad: `{}`\".format(height_half_pad)\n                )\n\n                scaling_arg = (\n                    \"scale={0}x{1}:flags=lanczos\"\n                    \":out_color_matrix=bt709\"\n                    \",pad={2}:{3}:{4}:{5}:black\"\n                    \",setsar=1\"\n                    \",fps={6}\"\n                ).format(\n                    width_scale,\n                    height_scale,\n                    to_width,\n                    to_height,\n                    width_half_pad,\n                    height_half_pad,\n                    input_frame_rate\n                )\n\n                vf_back = self.add_video_filter_args(output_args, scaling_arg)\n                # add it to output_args\n                output_args.insert(0, vf_back)\n\n            # overrides output file\n            output_args.append(\"-y\")\n\n            slate_v_path = slate_path.replace(\".png\", ext)\n            output_args.append(\n                path_to_subprocess_arg(slate_v_path)\n            )\n            _remove_at_end.append(slate_v_path)\n\n            slate_args = [\n                subprocess.list2cmdline(get_ffmpeg_tool_args(\"ffmpeg\")),\n                \" \".join(input_args),\n                \" \".join(output_args)\n            ]\n            slate_subprocess_cmd = \" \".join(slate_args)\n\n            if os.getenv(\"SHELL\") in (\"/bin/bash\", \"/bin/sh\"):\n                # Escape parentheses for bash\n                slate_subprocess_cmd = (\n                    slate_subprocess_cmd\n                    .replace(\"(\", \"\\\\(\")\n                    .replace(\")\", \"\\\\)\")\n                )\n\n            # run slate generation subprocess\n            self.log.debug(\n                \"Slate Executing: {}\".format(slate_subprocess_cmd)\n            )\n            run_subprocess(\n                slate_subprocess_cmd, shell=True, logger=self.log\n            )\n\n            # Create slate with silent audio track\n            if input_audio:\n                # silent slate output path\n                slate_silent_path = \"_silent\".join(\n                    os.path.splitext(slate_v_path))\n                _remove_at_end.append(slate_silent_path)\n                self._create_silent_slate(\n                    slate_v_path,\n                    slate_silent_path,\n                    audio_codec,\n                    audio_channels,\n                    audio_sample_rate,\n                    audio_channel_layout,\n                    input_frame_rate\n                )\n\n                # replace slate with silent slate for concat\n                slate_v_path = slate_silent_path\n\n            # concat slate and videos together with concat filter\n            # this will reencode the output\n            if input_audio:\n                fmap = [\n                    \"-filter_complex\",\n                    \"[0:v] [0:a] [1:v] [1:a] concat=n=2:v=1:a=1 [v] [a]\",\n                    \"-map\", '[v]',\n                    \"-map\", '[a]'\n                ]\n            else:\n                fmap = [\n                    \"-filter_complex\",\n                    \"[0:v] [1:v] concat=n=2:v=1:a=0 [v]\",\n                    \"-map\", '[v]'\n                ]\n            concat_args = get_ffmpeg_tool_args(\n                \"ffmpeg\",\n                \"-y\",\n                \"-i\", slate_v_path,\n                \"-i\", input_path,\n            )\n            concat_args.extend(fmap)\n            if offset_timecode:\n                concat_args.extend([\"-timecode\", offset_timecode])\n            # NOTE: Added because of OP Atom demuxers\n            # Add format arguments if there are any\n            # - keep format of output\n            if format_args:\n                concat_args.extend(format_args)\n\n            if codec_args:\n                concat_args.extend(codec_args)\n\n            # Use arguments from ffmpeg preset\n            source_ffmpeg_cmd = repre.get(\"ffmpeg_cmd\")\n            if source_ffmpeg_cmd:\n                copy_args = (\n                    \"-metadata\",\n                    \"-metadata:s:v:0\",\n                    \"-b:v\",\n                    \"-b:a\",\n                )\n                args = source_ffmpeg_cmd.split(\" \")\n                for indx, arg in enumerate(args):\n                    if arg in copy_args:\n                        concat_args.append(arg)\n                        # assumes arg has one parameter\n                        concat_args.append(args[indx + 1])\n\n            # add final output path\n            concat_args.append(output_path)\n\n            # ffmpeg concat subprocess\n            self.log.debug(\n                \"Executing concat filter: {}\".format\n                (\" \".join(concat_args))\n            )\n            run_subprocess(\n                concat_args, logger=self.log\n            )\n\n            self.log.debug(\"__ repre[tags]: {}\".format(repre[\"tags\"]))\n            repre_update = {\n                \"files\": output_file,\n                \"name\": repre[\"name\"],\n                \"tags\": [x for x in repre[\"tags\"] if x != \"delete\"]\n            }\n            inst_data[\"representations\"][idx].update(repre_update)\n            self.log.debug(\n                \"_ representation {}: `{}`\".format(\n                    idx, inst_data[\"representations\"][idx]))\n\n            # removing temp files\n            for f in _remove_at_end:\n                os.remove(f)\n                self.log.debug(\"Removed: `{}`\".format(f))\n\n        # Remove any representations tagged for deletion.\n        for repre in inst_data.get(\"representations\", []):\n            tags = repre.get(\"tags\", [])\n            if \"delete\" not in tags:\n                continue\n            if \"need_thumbnail\" in tags:\n                continue\n            self.log.debug(\"Removing representation: {}\".format(repre))\n            inst_data[\"representations\"].remove(repre)\n\n        self.log.debug(inst_data[\"representations\"])\n\n    def _get_slate_path(self, input_file, slates_data):\n        slate_path = None\n        for sl_n, _slate_path in slates_data.items():\n            if \"*\" in sl_n:\n                slate_path = _slate_path\n                break\n            elif re.search(sl_n, input_file):\n                slate_path = _slate_path\n                break\n\n        if not slate_path:\n            raise AttributeError(\n                \"Missing slates paths: {}\".format(slates_data))\n\n        return slate_path\n\n    def _get_slates_resolution(self, slate_path):\n        slate_streams = get_ffprobe_streams(slate_path, self.log)\n        # Try to find first stream with defined 'width' and 'height'\n        # - this is to avoid order of streams where audio can be as first\n        # - there may be a better way (checking `codec_type`?)+\n        slate_width = None\n        slate_height = None\n        for slate_stream in slate_streams:\n            if \"width\" in slate_stream and \"height\" in slate_stream:\n                slate_width = int(slate_stream[\"width\"])\n                slate_height = int(slate_stream[\"height\"])\n                break\n\n        # Raise exception of any stream didn't define input resolution\n        if slate_width is None:\n            raise AssertionError((\n                \"FFprobe couldn't read resolution from input file: \\\"{}\\\"\"\n            ).format(slate_path))\n\n        return (slate_width, slate_height)\n\n    def _get_video_metadata(self, streams):\n        input_timecode = \"\"\n        input_width = None\n        input_height = None\n        input_frame_rate = None\n        input_pixel_aspect = None\n        for stream in streams:\n            if stream.get(\"codec_type\") != \"video\":\n                continue\n            self.log.debug(\"FFprobe Video: {}\".format(stream))\n\n            if \"width\" not in stream or \"height\" not in stream:\n                continue\n            width = int(stream[\"width\"])\n            height = int(stream[\"height\"])\n            if not width or not height:\n                continue\n\n            # Make sure that width and height are captured even if frame rate\n            #    is not available\n            input_width = width\n            input_height = height\n\n            input_pixel_aspect = stream.get(\"sample_aspect_ratio\")\n            if input_pixel_aspect is not None:\n                try:\n                    input_pixel_aspect = float(\n                        eval(str(input_pixel_aspect).replace(':', '/')))\n                except Exception:\n                    self.log.debug(\n                        \"__Converting pixel aspect to float failed: {}\".format(\n                            input_pixel_aspect))\n\n            tags = stream.get(\"tags\") or {}\n            input_timecode = tags.get(\"timecode\") or \"\"\n\n            input_frame_rate = stream.get(\"r_frame_rate\")\n            if input_frame_rate is not None:\n                break\n        return (\n            input_width,\n            input_height,\n            input_timecode,\n            input_frame_rate,\n            input_pixel_aspect\n        )\n\n    def _get_audio_metadata(self, streams):\n        # Get audio metadata\n        audio_codec = None\n        audio_channels = None\n        audio_sample_rate = None\n        audio_channel_layout = None\n        input_audio = False\n\n        for stream in streams:\n            if stream.get(\"codec_type\") != \"audio\":\n                continue\n            self.log.debug(\"__Ffprobe Audio: {}\".format(stream))\n\n            if all(\n                stream.get(key)\n                for key in (\n                    \"codec_name\",\n                    \"channels\",\n                    \"sample_rate\",\n                    \"channel_layout\",\n                )\n            ):\n                audio_codec = stream[\"codec_name\"]\n                audio_channels = stream[\"channels\"]\n                audio_sample_rate = stream[\"sample_rate\"]\n                audio_channel_layout = stream[\"channel_layout\"]\n                input_audio = True\n                break\n\n        return (\n            audio_codec,\n            audio_channels,\n            audio_sample_rate,\n            audio_channel_layout,\n            input_audio,\n        )\n\n    def _create_silent_slate(\n        self,\n        src_path,\n        dst_path,\n        audio_codec,\n        audio_channels,\n        audio_sample_rate,\n        audio_channel_layout,\n        input_frame_rate\n    ):\n        # Get duration of one frame in micro seconds\n        items = input_frame_rate.split(\"/\")\n        if len(items) == 1:\n            one_frame_duration = 1.0 / float(items[0])\n        elif len(items) == 2:\n            one_frame_duration = float(items[1]) / float(items[0])\n        else:\n            one_frame_duration = None\n\n        if one_frame_duration is None:\n            one_frame_duration = \"40000us\"\n        else:\n            one_frame_duration *= 1000000\n            one_frame_duration = str(int(one_frame_duration)) + \"us\"\n        self.log.debug(\"One frame duration is {}\".format(one_frame_duration))\n\n        slate_silent_args = get_ffmpeg_tool_args(\n            \"ffmpeg\",\n            \"-i\", src_path,\n            \"-f\", \"lavfi\", \"-i\",\n            \"anullsrc=r={}:cl={}:d={}\".format(\n                audio_sample_rate,\n                audio_channel_layout,\n                one_frame_duration\n            ),\n            \"-c:v\", \"copy\",\n            \"-c:a\", audio_codec,\n            \"-map\", \"0:v\",\n            \"-map\", \"1:a\",\n            \"-shortest\",\n            \"-y\",\n            dst_path\n        )\n        # run slate generation subprocess\n        self.log.debug(\"Silent Slate Executing: {}\".format(\n            \" \".join(slate_silent_args)\n        ))\n        run_subprocess(\n            slate_silent_args, logger=self.log\n        )\n\n    def add_video_filter_args(self, args, inserting_arg):\n        \"\"\"\n        Fixing video filter argumets to be one long string\n\n        Args:\n            args (list): list of string arguments\n            inserting_arg (str): string argument we want to add\n                                 (without flag `-vf`)\n\n        Returns:\n            str: long joined argument to be added back to list of arguments\n\n        \"\"\"\n        # find all video format settings\n        vf_settings = [p for p in args\n                       for v in [\"-filter:v\", \"-vf\"]\n                       if v in p]\n        self.log.debug(\"_ vf_settings: `{}`\".format(vf_settings))\n\n        # remove them from output args list\n        for p in vf_settings:\n            self.log.debug(\"_ remove p: `{}`\".format(p))\n            args.remove(p)\n            self.log.debug(\"_ args: `{}`\".format(args))\n\n        # strip them from all flags\n        vf_fixed = [p.replace(\"-vf \", \"\").replace(\"-filter:v \", \"\")\n                    for p in vf_settings]\n\n        self.log.debug(\"_ vf_fixed: `{}`\".format(vf_fixed))\n        vf_fixed.insert(0, inserting_arg)\n        self.log.debug(\"_ vf_fixed: `{}`\".format(vf_fixed))\n        # create new video filter setting\n        vf_back = \"-vf \" + \",\".join(vf_fixed)\n\n        return vf_back\n\n    def _get_format_codec_args(self, repre):\n        \"\"\"Detect possible codec arguments from representation.\"\"\"\n        codec_args = []\n\n        # Get one filename of representation files\n        filename = repre[\"files\"]\n        # If files is list then pick first filename in list\n        if isinstance(filename, (tuple, list)):\n            filename = filename[0]\n        # Get full path to the file\n        full_input_path = os.path.join(repre[\"stagingDir\"], filename)\n\n        try:\n            # Get information about input file via ffprobe tool\n            ffprobe_data = get_ffprobe_data(full_input_path, self.log)\n        except Exception:\n            self.log.warning(\n                \"Could not get codec data from input.\",\n                exc_info=True\n            )\n            return codec_args\n\n        source_ffmpeg_cmd = repre.get(\"ffmpeg_cmd\")\n        format_args = get_ffmpeg_format_args(ffprobe_data, source_ffmpeg_cmd)\n        codec_args = get_ffmpeg_codec_args(\n            ffprobe_data, source_ffmpeg_cmd, logger=self.log\n        )\n\n        return format_args, codec_args\n\n    def _tc_offset(self, timecode, framerate=24.0, frame_offset=-1):\n        \"\"\"Offsets timecode by frame\"\"\"\n        def _seconds(value, framerate):\n            if isinstance(value, str):\n                _zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':'))\n                _s = sum(f * float(t) for f, t in _zip_ft)\n            elif isinstance(value, (int, float)):\n                _s = value / framerate\n            else:\n                _s = 0\n            return _s\n\n        def _frames(seconds, framerate, frame_offset):\n            _f = seconds * framerate + frame_offset\n            if _f < 0:\n                _f = framerate * 60 * 60 * 24 + _f\n            return _f\n\n        def _timecode(seconds, framerate):\n            return '{h:02d}:{m:02d}:{s:02d}:{f:02d}'.format(\n                h=int(seconds / 3600),\n                m=int(seconds / 60 % 60),\n                s=int(seconds % 60),\n                f=int(round((seconds - int(seconds)) * framerate)))\n        drop = False\n        if ';' in timecode:\n            timecode = timecode.replace(';', ':')\n            drop = True\n        frames = _frames(\n            _seconds(timecode, framerate),\n            framerate,\n            frame_offset\n        )\n        tc = _timecode(_seconds(frames, framerate), framerate)\n        if drop:\n            tc = ';'.join(tc.rsplit(':', 1))\n        return tc\n"
  },
  {
    "path": "openpype/plugins/publish/extract_scanline_exr.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Convert exrs in representation to tiled exrs usin oiio tools.\"\"\"\nimport os\nimport shutil\n\nimport pyblish.api\n\nfrom openpype.lib import (\n    run_subprocess,\n    get_oiio_tool_args,\n    ToolNotFoundError,\n)\nfrom openpype.pipeline import KnownPublishError\n\n\nclass ExtractScanlineExr(pyblish.api.InstancePlugin):\n    \"\"\"Convert tiled EXRs to scanline using OIIO tool.\"\"\"\n\n    label = \"Extract Scanline EXR\"\n    hosts = [\"shell\"]\n    order = pyblish.api.ExtractorOrder\n    families = [\"imagesequence\", \"render\", \"render2d\", \"source\"]\n\n    def process(self, instance):\n        \"\"\"Plugin entry point.\"\"\"\n        # get representation and loop them\n        representations = instance.data[\"representations\"]\n\n        representations_new = []\n\n        for repre in representations:\n            self.log.debug(\n                \"Processing representation {}\".format(repre.get(\"name\")))\n            tags = repre.get(\"tags\", [])\n            if \"toScanline\" not in tags:\n                self.log.debug(\" - missing toScanline tag\")\n                continue\n\n            # run only on exrs\n            if repre.get(\"ext\") != \"exr\":\n                self.log.debug(\"- not EXR files\")\n                continue\n\n            if not isinstance(repre['files'], (list, tuple)):\n                input_files = [repre['files']]\n                self.log.debug(\"We have a single frame\")\n            else:\n                input_files = repre['files']\n                self.log.debug(\"We have a sequence\")\n\n            stagingdir = os.path.normpath(repre.get(\"stagingDir\"))\n\n            try:\n                oiio_tool_args = get_oiio_tool_args(\"oiiotool\")\n            except ToolNotFoundError:\n                self.log.error(\"OIIO tool not found.\")\n                raise KnownPublishError(\"OIIO tool not found\")\n\n            for file in input_files:\n\n                original_name = os.path.join(stagingdir, file)\n                temp_name = os.path.join(stagingdir, \"__{}\".format(file))\n                # move original render to temp location\n                shutil.move(original_name, temp_name)\n                oiio_cmd = oiio_tool_args + [\n                    os.path.join(stagingdir, temp_name), \"--scanline\", \"-o\",\n                    os.path.join(stagingdir, original_name)\n                ]\n\n                subprocess_exr = \" \".join(oiio_cmd)\n                self.log.debug(f\"running: {subprocess_exr}\")\n                run_subprocess(subprocess_exr, logger=self.log)\n\n                # raise error if there is no ouptput\n                if not os.path.exists(os.path.join(stagingdir, original_name)):\n                    self.log.error(\n                        (\"File {} was not converted \"\n                         \"by oiio tool!\").format(original_name))\n                    raise AssertionError(\"OIIO tool conversion failed\")\n                else:\n                    try:\n                        os.remove(temp_name)\n                    except OSError as e:\n                        self.log.warning(\"Unable to delete temp file\")\n                        self.log.warning(e)\n\n            repre['name'] = 'exr'\n            try:\n                repre['tags'].remove('toScanline')\n            except ValueError:\n                # no `toScanline` tag present\n                pass\n\n        instance.data[\"representations\"] += representations_new\n"
  },
  {
    "path": "openpype/plugins/publish/extract_thumbnail.py",
    "content": "import copy\nimport os\nimport subprocess\nimport tempfile\nimport re\n\nimport pyblish.api\nfrom openpype.lib import (\n    get_ffmpeg_tool_args,\n    get_ffprobe_data,\n\n    is_oiio_supported,\n    get_rescaled_command_arguments,\n\n    path_to_subprocess_arg,\n    run_subprocess,\n)\nfrom openpype.lib.transcoding import (\n    convert_colorspace,\n    VIDEO_EXTENSIONS,\n)\n\n\nclass ExtractThumbnail(pyblish.api.InstancePlugin):\n    \"\"\"Create jpg thumbnail from sequence using ffmpeg\"\"\"\n\n    label = \"Extract Thumbnail\"\n    order = pyblish.api.ExtractorOrder + 0.49\n    families = [\n        \"imagesequence\", \"render\", \"render2d\", \"prerender\",\n        \"source\", \"clip\", \"take\", \"online\", \"image\"\n    ]\n    hosts = [\n        \"shell\",\n        \"fusion\",\n        \"resolve\",\n        \"traypublisher\",\n        \"substancepainter\",\n        \"nuke\",\n        \"aftereffects\"\n    ]\n    enabled = False\n\n    integrate_thumbnail = False\n    target_size = {\n        \"type\": \"resize\",\n        \"width\": 1920,\n        \"height\": 1080\n    }\n    background_color = None\n    duration_split = 0.5\n    # attribute presets from settings\n    oiiotool_defaults = None\n    ffmpeg_args = None\n    subsets = []\n    product_names = []\n\n    def process(self, instance):\n        # run main process\n        self._main_process(instance)\n\n        # Make sure cleanup happens to representations which are having both\n        # tags `delete` and `need_thumbnail`\n        for repre in tuple(instance.data.get(\"representations\", [])):\n            tags = repre.get(\"tags\") or []\n            # skip representations which are going to be published on farm\n            if \"publish_on_farm\" in tags:\n                continue\n            if (\n                \"delete\" in tags\n                and \"need_thumbnail\" in tags\n            ):\n                self.log.debug(\n                    \"Removing representation: {}\".format(repre)\n                )\n                instance.data[\"representations\"].remove(repre)\n\n    def _main_process(self, instance):\n        subset_name = instance.data[\"subset\"]\n        instance_repres = instance.data.get(\"representations\")\n        if not instance_repres:\n            self.log.debug((\n                \"Instance {} does not have representations. Skipping\"\n            ).format(subset_name))\n            return\n\n        self.log.debug(\n            \"Processing instance with subset name {}\".format(subset_name)\n        )\n\n        # Skip if instance have 'review' key in data set to 'False'\n        if not self._is_review_instance(instance):\n            self.log.debug(\"Skipping - no review set on instance.\")\n            return\n\n        # Check if already has thumbnail created\n        if self._already_has_thumbnail(instance_repres):\n            self.log.debug(\"Thumbnail representation already present.\")\n            return\n\n        # skip crypto passes.\n        # TODO: This is just a quick fix and has its own side-effects - it is\n        #       affecting every subset name with `crypto` in its name.\n        #       This must be solved properly, maybe using tags on\n        #       representation that can be determined much earlier and\n        #       with better precision.\n        if \"crypto\" in subset_name.lower():\n            self.log.debug(\"Skipping crypto passes.\")\n            return\n\n        # We only want to process the subsets needed from settings.\n        def validate_string_against_patterns(input_str, patterns):\n            for pattern in patterns:\n                if re.match(pattern, input_str):\n                    return True\n            return False\n\n        product_names = self.subsets + self.product_names\n        if product_names:\n            result = validate_string_against_patterns(\n                instance.data[\"subset\"], product_names\n            )\n            if not result:\n                self.log.debug(\n                    \"Subset \\\"{}\\\" did not match any valid subsets: {}\".format(\n                        instance.data[\"subset\"], product_names\n                    )\n                )\n                return\n\n        # first check for any explicitly marked representations for thumbnail\n        explicit_repres = self._get_explicit_repres_for_thumbnail(instance)\n        if explicit_repres:\n            filtered_repres = explicit_repres\n        else:\n            filtered_repres = self._get_filtered_repres(instance)\n\n        if not filtered_repres:\n            self.log.info(\n                \"Instance doesn't have representations that can be used \"\n                \"as source for thumbnail. Skipping thumbnail extraction.\"\n            )\n            return\n\n        # Create temp directory for thumbnail\n        # - this is to avoid \"override\" of source file\n        dst_staging = tempfile.mkdtemp(prefix=\"pyblish_tmp_\")\n        self.log.debug(\n            \"Create temp directory {} for thumbnail\".format(dst_staging)\n        )\n        # Store new staging to cleanup paths\n        instance.context.data[\"cleanupFullPaths\"].append(dst_staging)\n\n        thumbnail_created = False\n        oiio_supported = is_oiio_supported()\n        for repre in filtered_repres:\n            repre_files = repre[\"files\"]\n            src_staging = os.path.normpath(repre[\"stagingDir\"])\n            if not isinstance(repre_files, (list, tuple)):\n                # convert any video file to frame so oiio doesn't need to\n                # read video file (it is slow) and also we are having control\n                # over which frame is used for thumbnail\n                # this will also work with ffmpeg fallback conversion in case\n                # oiio is not supported\n                repre_extension = os.path.splitext(repre_files)[1]\n                if repre_extension in VIDEO_EXTENSIONS:\n                    video_file_path = os.path.join(\n                        src_staging, repre_files\n                    )\n                    file_path = self._create_frame_from_video(\n                        video_file_path,\n                        dst_staging\n                    )\n                    if file_path:\n                        src_staging, input_file = os.path.split(file_path)\n                else:\n                    # if it is not video file then just use first file\n                    input_file = repre_files\n            else:\n                repre_files_thumb = copy.deepcopy(repre_files)\n                # exclude first frame if slate in representation tags\n                if \"slate-frame\" in repre.get(\"tags\", []):\n                    repre_files_thumb = repre_files_thumb[1:]\n                file_index = int(\n                    float(len(repre_files_thumb)) * self.duration_split)\n                input_file = repre_files[file_index]\n\n            full_input_path = os.path.join(src_staging, input_file)\n            self.log.debug(\"input {}\".format(full_input_path))\n\n            filename = os.path.splitext(input_file)[0]\n            jpeg_file = filename + \"_thumb.jpg\"\n            full_output_path = os.path.join(dst_staging, jpeg_file)\n            colorspace_data = repre.get(\"colorspaceData\")\n\n            # only use OIIO if it is supported and representation has\n            # colorspace data\n            if oiio_supported and colorspace_data:\n                self.log.debug(\n                    \"Trying to convert with OIIO \"\n                    \"with colorspace data: {}\".format(colorspace_data)\n                )\n                # If the input can read by OIIO then use OIIO method for\n                # conversion otherwise use ffmpeg\n                thumbnail_created = self._create_thumbnail_oiio(\n                    full_input_path,\n                    full_output_path,\n                    colorspace_data\n                )\n\n            # Try to use FFMPEG if OIIO is not supported or for cases when\n            #   oiiotool isn't available or representation is not having\n            #   colorspace data\n            if not thumbnail_created:\n                if oiio_supported:\n                    self.log.debug(\n                        \"Converting with FFMPEG because input\"\n                        \" can't be read by OIIO.\"\n                    )\n\n                thumbnail_created = self._create_thumbnail_ffmpeg(\n                    full_input_path, full_output_path\n                )\n\n            # Skip representation and try next one if  wasn't created\n            if not thumbnail_created:\n                continue\n\n            if len(explicit_repres) > 1:\n                repre_name = \"thumbnail_{}\".format(repre[\"outputName\"])\n            else:\n                repre_name = \"thumbnail\"\n\n            # add thumbnail path to instance data for integrator\n            instance_thumb_path = instance.data.get(\"thumbnailPath\")\n            if (\n                not instance_thumb_path\n                or not os.path.isfile(instance_thumb_path)\n            ):\n                self.log.debug(\n                    \"Adding thumbnail path to instance data: {}\".format(\n                        full_output_path\n                    )\n                )\n                instance.data[\"thumbnailPath\"] = full_output_path\n\n            new_repre_tags = [\"thumbnail\"]\n            # for workflows which needs to have thumbnails published as\n            # separate representations `delete` tag should not be added\n            if not self.integrate_thumbnail:\n                new_repre_tags.append(\"delete\")\n\n            new_repre = {\n                \"name\": repre_name,\n                \"ext\": \"jpg\",\n                \"files\": jpeg_file,\n                \"stagingDir\": dst_staging,\n                \"thumbnail\": True,\n                \"tags\": new_repre_tags,\n                # If source image is jpg then there can be clash when\n                # integrating to making the output name explicit.\n                \"outputName\": \"thumbnail\"\n            }\n\n            # adding representation\n            instance.data[\"representations\"].append(new_repre)\n\n            if explicit_repres:\n                # this key will then align assetVersion ftrack thumbnail sync\n                new_repre[\"outputName\"] = (\n                    repre.get(\"outputName\") or repre[\"name\"])\n                self.log.debug(\n                    \"Adding explicit thumbnail representation: {}\".format(\n                        new_repre))\n            else:\n                self.log.debug(\n                    \"Adding thumbnail representation: {}\".format(new_repre)\n                )\n                # There is no need to create more then one thumbnail\n                break\n\n        if not thumbnail_created:\n            self.log.warning(\"Thumbnail has not been created.\")\n\n    def _is_review_instance(self, instance):\n        # TODO: We should probably handle \"not creating\" of thumbnail\n        #   other way then checking for \"review\" key on instance data?\n        if instance.data.get(\"review\", True):\n            return True\n        return False\n\n    def _already_has_thumbnail(self, repres):\n        for repre in repres:\n            self.log.debug(\"repre {}\".format(repre))\n            if repre[\"name\"] == \"thumbnail\":\n                return True\n        return False\n\n    def _get_explicit_repres_for_thumbnail(self, instance):\n        src_repres = instance.data.get(\"representations\") or []\n        # This is mainly for Nuke where we have multiple representations for\n        #   one instance and representations are tagged for thumbnail.\n        # First check if any of the representations have\n        # `need_thumbnail` in tags and add them to filtered_repres\n        need_thumb_repres = [\n            repre for repre in src_repres\n            if \"need_thumbnail\" in repre.get(\"tags\", [])\n            if \"publish_on_farm\" not in repre.get(\"tags\", [])\n        ]\n        if not need_thumb_repres:\n            return []\n\n        self.log.info(\n            \"Instance has representation with tag `need_thumbnail`. \"\n            \"Using only this representations for thumbnail creation. \"\n        )\n        self.log.debug(\n            \"Representations: {}\".format(need_thumb_repres)\n        )\n        return need_thumb_repres\n\n    def _get_filtered_repres(self, instance):\n        filtered_repres = []\n        src_repres = instance.data.get(\"representations\") or []\n\n        for repre in src_repres:\n            self.log.debug(repre)\n            tags = repre.get(\"tags\") or []\n\n            if \"publish_on_farm\" in tags:\n                # only process representations with are going\n                # to be published locally\n                continue\n\n            valid = \"review\" in tags or \"thumb-nuke\" in tags\n            if not valid:\n                continue\n\n            if not repre.get(\"files\"):\n                self.log.debug((\n                    \"Representation \\\"{}\\\" doesn't have files. Skipping\"\n                ).format(repre[\"name\"]))\n                continue\n\n            filtered_repres.append(repre)\n        return filtered_repres\n\n    def _create_thumbnail_oiio(\n        self,\n        src_path,\n        dst_path,\n        colorspace_data,\n    ):\n        \"\"\"Create thumbnail using OIIO tool oiiotool\n\n        Args:\n            src_path (str): path to source file\n            dst_path (str): path to destination file\n            colorspace_data (dict): colorspace data from representation\n                keys:\n                    colorspace (str)\n                    config (dict)\n                    display (Optional[str])\n                    view (Optional[str])\n\n        Returns:\n            str: path to created thumbnail\n        \"\"\"\n        self.log.info(\"Extracting thumbnail {}\".format(dst_path))\n        resolution_arg = self._get_resolution_arg(\"oiiotool\", src_path)\n\n        repre_display = colorspace_data.get(\"display\")\n        repre_view = colorspace_data.get(\"view\")\n        oiio_default_type = None\n        oiio_default_display = None\n        oiio_default_view = None\n        oiio_default_colorspace = None\n        # first look into representation colorspaceData, perhaps it has\n        #   display and view\n        if all([repre_display, repre_view]):\n            self.log.info(\n                \"Using Display & View from \"\n                \"representation: '{} ({})'\".format(\n                    repre_view,\n                    repre_display\n                )\n            )\n        # if representation doesn't have display and view then use\n        #   oiiotool_defaults\n        elif self.oiiotool_defaults:\n            oiio_default_type = self.oiiotool_defaults[\"type\"]\n            if \"colorspace\" in oiio_default_type:\n                oiio_default_colorspace = self.oiiotool_defaults[\"colorspace\"]\n            else:\n                oiio_default_display = self.oiiotool_defaults[\"display\"]\n                oiio_default_view = self.oiiotool_defaults[\"view\"]\n\n        try:\n            convert_colorspace(\n                src_path,\n                dst_path,\n                colorspace_data[\"config\"][\"path\"],\n                colorspace_data[\"colorspace\"],\n                display=repre_display or oiio_default_display,\n                view=repre_view or oiio_default_view,\n                target_colorspace=oiio_default_colorspace,\n                additional_command_args=resolution_arg,\n                logger=self.log,\n            )\n        except Exception:\n            self.log.warning(\n                \"Failed to create thumbnail using oiiotool\",\n                exc_info=True\n            )\n            return False\n\n        return True\n\n    def _create_thumbnail_ffmpeg(self, src_path, dst_path):\n        self.log.debug(\"Extracting thumbnail with FFMPEG: {}\".format(dst_path))\n        resolution_arg = self._get_resolution_arg(\"ffmpeg\", src_path)\n        ffmpeg_path_args = get_ffmpeg_tool_args(\"ffmpeg\")\n        ffmpeg_args = self.ffmpeg_args or {}\n\n        jpeg_items = [\n            subprocess.list2cmdline(ffmpeg_path_args)\n        ]\n        # flag for large file sizes\n        max_int = 2147483647\n        jpeg_items.extend([\n            \"-y\",\n            \"-analyzeduration\", str(max_int),\n            \"-probesize\", str(max_int),\n        ])\n        # use same input args like with mov\n        jpeg_items.extend(ffmpeg_args.get(\"input\") or [])\n        # input file\n        jpeg_items.extend([\"-i\", path_to_subprocess_arg(src_path)])\n        # output arguments from presets\n        jpeg_items.extend(ffmpeg_args.get(\"output\") or [])\n        # we just want one frame from movie files\n        jpeg_items.extend([\"-vframes\", \"1\"])\n\n        if resolution_arg:\n            jpeg_items.extend(resolution_arg)\n\n        # output file\n        jpeg_items.append(path_to_subprocess_arg(dst_path))\n        subprocess_command = \" \".join(jpeg_items)\n        if os.getenv(\"SHELL\") in (\"/bin/bash\", \"/bin/sh\"):\n            # Escape parentheses for bash\n            subprocess_command = (\n                subprocess_command\n                .replace(\"(\", \"\\\\(\")\n                .replace(\")\", \"\\\\)\")\n            )\n\n        try:\n            run_subprocess(\n                subprocess_command, shell=True, logger=self.log\n            )\n            return True\n        except Exception:\n            self.log.warning(\n                \"Failed to create thumbnail using ffmpeg\",\n                exc_info=True\n            )\n            return False\n\n    def _create_frame_from_video(self, video_file_path, output_dir):\n        \"\"\"Convert video file to one frame image via ffmpeg\"\"\"\n        # create output file path\n        base_name = os.path.basename(video_file_path)\n        filename = os.path.splitext(base_name)[0]\n        output_thumb_file_path = os.path.join(\n            output_dir, \"{}.png\".format(filename))\n\n        # Set video input attributes\n        max_int = str(2147483647)\n        video_data = get_ffprobe_data(video_file_path, logger=self.log)\n        # Use duration of the individual streams since it is returned with\n        # higher decimal precision than 'format.duration'. We need this\n        # more precise value for calculating the correct amount of frames\n        # for higher FPS ranges or decimal ranges, e.g. 29.97 FPS\n        duration = max(\n            float(stream.get(\"duration\", 0))\n            for stream in video_data[\"streams\"]\n            if stream.get(\"codec_type\") == \"video\"\n        )\n\n        cmd_args = [\n            \"-y\",\n            \"-ss\", str(duration * self.duration_split),\n            \"-i\", video_file_path,\n            \"-analyzeduration\", max_int,\n            \"-probesize\", max_int,\n            \"-vframes\", \"1\"\n        ]\n\n        # add output file path\n        cmd_args.append(output_thumb_file_path)\n\n        # create ffmpeg command\n        cmd = get_ffmpeg_tool_args(\n            \"ffmpeg\",\n            *cmd_args\n        )\n        try:\n            # run subprocess\n            self.log.debug(\"Executing: {}\".format(\" \".join(cmd)))\n            run_subprocess(cmd, logger=self.log)\n            self.log.debug(\n                \"Thumbnail created: {}\".format(output_thumb_file_path))\n            return output_thumb_file_path\n        except RuntimeError as error:\n            self.log.warning(\n                \"Failed intermediate thumb source using ffmpeg: {}\".format(\n                    error)\n            )\n            return None\n\n    def _get_resolution_arg(\n        self,\n        application,\n        input_path,\n    ):\n        # get settings\n        if self.target_size.get(\"type\") == \"source\":\n            return []\n\n        target_width = self.target_size[\"width\"]\n        target_height = self.target_size[\"height\"]\n\n        # form arg string per application\n        return get_rescaled_command_arguments(\n            application,\n            input_path,\n            target_width,\n            target_height,\n            bg_color=self.background_color,\n            log=self.log\n        )\n"
  },
  {
    "path": "openpype/plugins/publish/extract_thumbnail_from_source.py",
    "content": "\"\"\"Create instance thumbnail from \"thumbnailSource\" on 'instance.data'.\n\nOutput is new representation with \"thumbnail\" name on instance. If instance\nalready have such representation the process is skipped.\n\nThis way a collector can point to a file from which should be thumbnail\ngenerated. This is different approach then what global plugin for thumbnails\ndoes. The global plugin has specific logic which does not support\n\nTodos:\n    No size handling. Size of input is used for output thumbnail which can\n        cause issues.\n\"\"\"\n\nimport os\nimport tempfile\n\nimport pyblish.api\nfrom openpype.lib import (\n    get_ffmpeg_tool_args,\n    get_oiio_tool_args,\n    is_oiio_supported,\n\n    run_subprocess,\n)\n\n\nclass ExtractThumbnailFromSource(pyblish.api.InstancePlugin):\n    \"\"\"Create jpg thumbnail for instance based on 'thumbnailSource'.\n\n    Thumbnail source must be a single image or video filepath.\n    \"\"\"\n\n    label = \"Extract Thumbnail (from source)\"\n    # Before 'ExtractThumbnail' in global plugins\n    order = pyblish.api.ExtractorOrder - 0.00001\n\n    def process(self, instance):\n        self._create_context_thumbnail(instance.context)\n\n        subset_name = instance.data[\"subset\"]\n        self.log.debug(\n            \"Processing instance with subset name {}\".format(subset_name)\n        )\n        thumbnail_source = instance.data.get(\"thumbnailSource\")\n        if not thumbnail_source:\n            self.log.debug(\"Thumbnail source not filled. Skipping.\")\n            return\n\n        # Check if already has thumbnail created\n        if self._instance_has_thumbnail(instance):\n            self.log.debug(\"Thumbnail representation already present.\")\n            return\n\n        dst_filepath = self._create_thumbnail(\n            instance.context, thumbnail_source\n        )\n        if not dst_filepath:\n            return\n\n        dst_staging, dst_filename = os.path.split(dst_filepath)\n        new_repre = {\n            \"name\": \"thumbnail\",\n            \"ext\": \"jpg\",\n            \"files\": dst_filename,\n            \"stagingDir\": dst_staging,\n            \"thumbnail\": True,\n            \"tags\": [\"thumbnail\"],\n            \"outputName\": \"thumbnail\",\n        }\n\n        # adding representation\n        self.log.debug(\n            \"Adding thumbnail representation: {}\".format(new_repre)\n        )\n        instance.data[\"representations\"].append(new_repre)\n        instance.data[\"thumbnailPath\"] = dst_filepath\n\n    def _create_thumbnail(self, context, thumbnail_source):\n        if not thumbnail_source:\n            self.log.debug(\"Thumbnail source not filled. Skipping.\")\n            return\n\n        if not os.path.exists(thumbnail_source):\n            self.log.debug((\n                \"Thumbnail source is set but file was not found {}. Skipping.\"\n            ).format(thumbnail_source))\n            return\n\n        # Create temp directory for thumbnail\n        # - this is to avoid \"override\" of source file\n        dst_staging = tempfile.mkdtemp(prefix=\"pyblish_tmp_\")\n        self.log.debug(\n            \"Create temp directory {} for thumbnail\".format(dst_staging)\n        )\n        # Store new staging to cleanup paths\n        context.data[\"cleanupFullPaths\"].append(dst_staging)\n\n        thumbnail_created = False\n        oiio_supported = is_oiio_supported()\n\n        self.log.debug(\"Thumbnail source: {}\".format(thumbnail_source))\n        src_basename = os.path.basename(thumbnail_source)\n        dst_filename = os.path.splitext(src_basename)[0] + \"_thumb.jpg\"\n        full_output_path = os.path.join(dst_staging, dst_filename)\n\n        if oiio_supported:\n            self.log.debug(\"Trying to convert with OIIO\")\n            # If the input can read by OIIO then use OIIO method for\n            # conversion otherwise use ffmpeg\n            thumbnail_created = self.create_thumbnail_oiio(\n                thumbnail_source, full_output_path\n            )\n\n        # Try to use FFMPEG if OIIO is not supported or for cases when\n        #    oiiotool isn't available\n        if not thumbnail_created:\n            if oiio_supported:\n                self.log.info(\n                    \"Converting with FFMPEG because input\"\n                    \" can't be read by OIIO.\"\n                )\n\n            thumbnail_created = self.create_thumbnail_ffmpeg(\n                thumbnail_source, full_output_path\n            )\n\n        # Skip representation and try next one if  wasn't created\n        if thumbnail_created:\n            return full_output_path\n\n        self.log.warning(\"Thumbnail has not been created.\")\n\n    def _instance_has_thumbnail(self, instance):\n        if \"representations\" not in instance.data:\n            self.log.warning(\n                \"Instance does not have 'representations' key filled\"\n            )\n            instance.data[\"representations\"] = []\n\n        for repre in instance.data[\"representations\"]:\n            if repre[\"name\"] == \"thumbnail\":\n                return True\n        return False\n\n    def create_thumbnail_oiio(self, src_path, dst_path):\n        self.log.debug(\"Outputting thumbnail with OIIO: {}\".format(dst_path))\n        oiio_cmd = get_oiio_tool_args(\n            \"oiiotool\",\n            \"-a\", src_path,\n            \"--ch\", \"R,G,B\",\n            \"-o\", dst_path\n        )\n        self.log.debug(\"Running: {}\".format(\" \".join(oiio_cmd)))\n        try:\n            run_subprocess(oiio_cmd, logger=self.log)\n            return True\n        except Exception:\n            self.log.warning(\n                \"Failed to create thumbnail using oiiotool\",\n                exc_info=True\n            )\n            return False\n\n    def create_thumbnail_ffmpeg(self, src_path, dst_path):\n        max_int = str(2147483647)\n        ffmpeg_cmd = get_ffmpeg_tool_args(\n            \"ffmpeg\",\n            \"-y\",\n            \"-analyzeduration\", max_int,\n            \"-probesize\", max_int,\n            \"-i\", src_path,\n            \"-vframes\", \"1\",\n            dst_path\n        )\n\n        self.log.debug(\"Running: {}\".format(\" \".join(ffmpeg_cmd)))\n        try:\n            run_subprocess(ffmpeg_cmd, logger=self.log)\n            return True\n        except Exception:\n            self.log.warning(\n                \"Failed to create thumbnail using ffmpeg\",\n                exc_info=True\n            )\n            return False\n\n    def _create_context_thumbnail(self, context):\n        if \"thumbnailPath\" in context.data:\n            return\n\n        thumbnail_source = context.data.get(\"thumbnailSource\")\n        thumbnail_path = self._create_thumbnail(context, thumbnail_source)\n        context.data[\"thumbnailPath\"] = thumbnail_path\n"
  },
  {
    "path": "openpype/plugins/publish/extract_trim_video_audio.py",
    "content": "import os\nfrom pprint import pformat\n\nimport pyblish.api\n\nfrom openpype.lib import (\n    get_ffmpeg_tool_args,\n    run_subprocess,\n)\nfrom openpype.pipeline import publish\n\n\nclass ExtractTrimVideoAudio(publish.Extractor):\n    \"\"\"Trim with ffmpeg \"mov\" and \"wav\" files.\"\"\"\n\n    # must be before `ExtractThumbnailSP`\n    order = pyblish.api.ExtractorOrder - 0.01\n    label = \"Extract Trim Video/Audio\"\n    hosts = [\"standalonepublisher\", \"traypublisher\"]\n    families = [\"clip\", \"trimming\"]\n\n    # make sure it is enabled only if at least both families are available\n    match = pyblish.api.Subset\n\n    # presets\n\n    def process(self, instance):\n        representation = instance.data.get(\"representations\")\n        self.log.debug(f\"_ representation: {representation}\")\n\n        if not representation:\n            instance.data[\"representations\"] = list()\n\n        # get ffmpet path\n        ffmpeg_tool_args = get_ffmpeg_tool_args(\"ffmpeg\")\n\n        # get staging dir\n        staging_dir = self.staging_dir(instance)\n        self.log.debug(\"Staging dir set to: `{}`\".format(staging_dir))\n\n        # Generate mov file.\n        fps = instance.data[\"fps\"]\n        video_file_path = instance.data[\"editorialSourcePath\"]\n        extensions = instance.data.get(\"extensions\", [\"mov\"])\n        output_file_type = instance.data.get(\"outputFileType\")\n        reviewable = \"review\" in instance.data[\"families\"]\n\n        frame_start = int(instance.data[\"frameStart\"])\n        frame_end = int(instance.data[\"frameEnd\"])\n        handle_start = instance.data[\"handleStart\"]\n        handle_end = instance.data[\"handleEnd\"]\n\n        clip_start_h = float(instance.data[\"clipInH\"])\n        _dur = instance.data[\"clipDuration\"]\n        handle_dur = (handle_start + handle_end)\n        clip_dur_h = float(_dur + handle_dur)\n\n        if output_file_type:\n            extensions = [output_file_type]\n\n        for ext in extensions:\n            self.log.debug(\"Processing ext: `{}`\".format(ext))\n\n            if not ext.startswith(\".\"):\n                ext = \".\" + ext\n\n            clip_trimed_path = os.path.join(\n                staging_dir, instance.data[\"name\"] + ext)\n\n            if ext == \".wav\":\n                # offset time as ffmpeg is having bug\n                clip_start_h += 0.5\n                # remove \"review\" from families\n                instance.data[\"families\"] = [\n                    fml for fml in instance.data[\"families\"]\n                    if \"trimming\" not in fml\n                ]\n\n            ffmpeg_args = ffmpeg_tool_args + [\n                \"-ss\", str(clip_start_h / fps),\n                \"-i\", video_file_path,\n                \"-t\", str(clip_dur_h / fps)\n            ]\n            if ext in [\".mov\", \".mp4\"]:\n                ffmpeg_args.extend([\n                    \"-crf\", \"18\",\n                    \"-pix_fmt\", \"yuv420p\"\n                ])\n            elif ext in \".wav\":\n                ffmpeg_args.extend([\n                    \"-vn\",\n                    \"-acodec\", \"pcm_s16le\",\n                    \"-ar\", \"48000\",\n                    \"-ac\", \"2\"\n                ])\n\n            # add output path\n            ffmpeg_args.append(clip_trimed_path)\n\n            joined_args = \" \".join(ffmpeg_args)\n            self.log.debug(f\"Processing: {joined_args}\")\n            run_subprocess(\n                ffmpeg_args, logger=self.log\n            )\n\n            repre = {\n                \"name\": ext[1:],\n                \"ext\": ext[1:],\n                \"files\": os.path.basename(clip_trimed_path),\n                \"stagingDir\": staging_dir,\n                \"frameStart\": frame_start,\n                \"frameEnd\": frame_end,\n                \"frameStartFtrack\": frame_start - handle_start,\n                \"frameEndFtrack\": frame_end + handle_end,\n                \"fps\": fps,\n                \"tags\": []\n            }\n\n            if ext in [\".mov\", \".mp4\"] and reviewable:\n                repre.update({\n                    \"thumbnail\": True,\n                    \"tags\": [\"review\", \"ftrackreview\", \"delete\"]})\n\n            instance.data[\"representations\"].append(repre)\n\n            self.log.debug(f\"Instance data: {pformat(instance.data)}\")\n"
  },
  {
    "path": "openpype/plugins/publish/help/validate_containers.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Not up-to-date assets</title>\n<description>\n## Outdated containers found\n\nScene contains one or more outdated loaded containers, eg. versions loaded into scene by Loader are not latest.\n\n### How to repair?\n\nUse 'Scene Inventory' and update all highlighted old container to latest OR\n    refresh Publish and switch 'Validate Containers' toggle on 'Options' tab.\n\n    WARNING: Skipping this validator will result in publishing (and probably rendering) old version of loaded assets.\n</description>\n<detail>\n### __Detailed Info__ (optional)\n\nThis validates whether you're working with the latest versions of published content loaded into your scene. This protects you from using outdated versions of an asset.\n</detail>\n</error>\n</root>"
  },
  {
    "path": "openpype/plugins/publish/help/validate_publish_dir.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Source directory not collected</title>\n<description>\n## Source directory not collected\n\nInstance is marked for in place publishing. Its 'originalDirname' must be collected. Contact OP developer to modify collector.\n\n</description>\n<detail>\n### __Detailed Info__ (optional)\n\nIn place publishing uses source directory and file name in resulting path and file name of published item. For this instance\n    all required metadata weren't filled. This is not recoverable error, unless instance itself is removed.\n    Collector for this instance must be updated for instance to be published.\n</detail>\n</error>\n<error id=\"not_in_dir\">\n<title>Source file not in project dir</title>\n<description>\n## Source file not in project dir\n\nPath '{original_dirname}' not in project folder. Please publish from inside of project folder.\n\n### How to repair?\n\nRestart publish after you moved source file into project directory.\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/plugins/publish/help/validate_unique_subsets.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n<error id=\"main\">\n<title>Subset not unique</title>\n<description>\n## Clashing subset names found\n\nMultiples instances from your scene are set to publish into the same asset > subset.\n\n    Non unique subset names: '{non_unique}'\n\n### How to repair?\n\nRemove the offending instances or rename to have a unique name.\n</description>\n</error>\n</root>"
  },
  {
    "path": "openpype/plugins/publish/integrate.py",
    "content": "import os\nimport logging\nimport sys\nimport copy\nimport datetime\n\nimport clique\nimport six\nfrom bson.objectid import ObjectId\nimport pyblish.api\n\nfrom openpype.client.operations import (\n    OperationsSession,\n    new_subset_document,\n    new_version_doc,\n    new_representation_doc,\n    prepare_subset_update_data,\n    prepare_version_update_data,\n    prepare_representation_update_data,\n)\n\nfrom openpype.client import (\n    get_representations,\n    get_subset_by_name,\n    get_version_by_name,\n)\nfrom openpype.lib import source_hash\nfrom openpype.lib.file_transaction import (\n    FileTransaction,\n    DuplicateDestinationError\n)\nfrom openpype.pipeline.publish import (\n    KnownPublishError,\n    get_publish_template_name,\n)\n\nlog = logging.getLogger(__name__)\n\n\ndef get_instance_families(instance):\n    \"\"\"Get all families of the instance\"\"\"\n    # todo: move this to lib?\n    family = instance.data.get(\"family\")\n    families = []\n    if family:\n        families.append(family)\n\n    for _family in (instance.data.get(\"families\") or []):\n        if _family not in families:\n            families.append(_family)\n\n    return families\n\n\ndef get_frame_padded(frame, padding):\n    \"\"\"Return frame number as string with `padding` amount of padded zeros\"\"\"\n    return \"{frame:0{padding}d}\".format(padding=padding, frame=frame)\n\n\nclass IntegrateAsset(pyblish.api.InstancePlugin):\n    \"\"\"Register publish in the database and transfer files to destinations.\n\n    Steps:\n        1) Register the subset and version\n        2) Transfer the representation files to the destination\n        3) Register the representation\n\n    Requires:\n        instance.data['representations'] - must be a list and each member\n        must be a dictionary with following data:\n            'files': list of filenames for sequence, string for single file.\n                     Only the filename is allowed, without the folder path.\n            'stagingDir': \"path/to/folder/with/files\"\n            'name': representation name (usually the same as extension)\n            'ext': file extension\n        optional data\n            \"frameStart\"\n            \"frameEnd\"\n            'fps'\n            \"data\": additional metadata for each representation.\n    \"\"\"\n\n    label = \"Integrate Asset\"\n    order = pyblish.api.IntegratorOrder\n    families = [\"workfile\",\n                \"pointcache\",\n                \"pointcloud\",\n                \"proxyAbc\",\n                \"camera\",\n                \"animation\",\n                \"model\",\n                \"maxScene\",\n                \"mayaAscii\",\n                \"mayaScene\",\n                \"setdress\",\n                \"layout\",\n                \"ass\",\n                \"assProxy\",\n                \"vdbcache\",\n                \"scene\",\n                \"vrayproxy\",\n                \"vrayscene_layer\",\n                \"render\",\n                \"prerender\",\n                \"imagesequence\",\n                \"review\",\n                \"rendersetup\",\n                \"rig\",\n                \"plate\",\n                \"look\",\n                \"ociolook\",\n                \"audio\",\n                \"yetiRig\",\n                \"yeticache\",\n                \"nukenodes\",\n                \"gizmo\",\n                \"source\",\n                \"matchmove\",\n                \"image\",\n                \"assembly\",\n                \"fbx\",\n                \"gltf\",\n                \"textures\",\n                \"action\",\n                \"harmony.template\",\n                \"harmony.palette\",\n                \"editorial\",\n                \"background\",\n                \"camerarig\",\n                \"redshiftproxy\",\n                \"effect\",\n                \"xgen\",\n                \"hda\",\n                \"usd\",\n                \"staticMesh\",\n                \"skeletalMesh\",\n                \"mvLook\",\n                \"mvUsd\",\n                \"mvUsdComposition\",\n                \"mvUsdOverride\",\n                \"online\",\n                \"uasset\",\n                \"blendScene\",\n                \"yeticacheUE\",\n                \"tycache\",\n                \"lensDistortion\",\n                ]\n\n    default_template_name = \"publish\"\n\n    # Representation context keys that should always be written to\n    # the database even if not used by the destination template\n    db_representation_context_keys = [\n        \"project\", \"asset\", \"task\", \"subset\", \"version\", \"representation\",\n        \"family\", \"hierarchy\", \"username\", \"user\", \"output\"\n    ]\n\n    def process(self, instance):\n\n        # Instance should be integrated on a farm\n        if instance.data.get(\"farm\"):\n            self.log.debug(\n                \"Instance is marked to be processed on farm. Skipping\")\n            return\n\n        # Instance is marked to not get integrated\n        if not instance.data.get(\"integrate\", True):\n            self.log.debug(\"Instance is marked to skip integrating. Skipping\")\n            return\n\n        filtered_repres = self.filter_representations(instance)\n        # Skip instance if there are not representations to integrate\n        #   all representations should not be integrated\n        if not filtered_repres:\n            self.log.warning((\n                \"Skipping, there are no representations\"\n                \" to integrate for instance {}\"\n            ).format(instance.data[\"family\"]))\n            return\n\n        file_transactions = FileTransaction(log=self.log,\n                                            # Enforce unique transfers\n                                            allow_queue_replacements=False)\n        try:\n            self.register(instance, file_transactions, filtered_repres)\n        except DuplicateDestinationError as exc:\n            # Raise DuplicateDestinationError as KnownPublishError\n            # and rollback the transactions\n            file_transactions.rollback()\n            six.reraise(KnownPublishError,\n                        KnownPublishError(exc),\n                        sys.exc_info()[2])\n        except Exception:\n            # clean destination\n            # todo: preferably we'd also rollback *any* changes to the database\n            file_transactions.rollback()\n            self.log.critical(\"Error when registering\", exc_info=True)\n            six.reraise(*sys.exc_info())\n\n        # Finalizing can't rollback safely so no use for moving it to\n        # the try, except.\n        file_transactions.finalize()\n\n    def filter_representations(self, instance):\n        # Prepare repsentations that should be integrated\n        repres = instance.data.get(\"representations\")\n        # Raise error if instance don't have any representations\n        if not repres:\n            raise KnownPublishError(\n                \"Instance {} has no representations to integrate\".format(\n                    instance.data[\"family\"]\n                )\n            )\n\n        # Validate type of stored representations\n        if not isinstance(repres, (list, tuple)):\n            raise TypeError(\n                \"Instance 'files' must be a list, got: {0} {1}\".format(\n                    str(type(repres)), str(repres)\n                )\n            )\n\n        # Filter representations\n        filtered_repres = []\n        for repre in repres:\n            if \"delete\" in repre.get(\"tags\", []):\n                continue\n            filtered_repres.append(repre)\n\n        return filtered_repres\n\n    def register(self, instance, file_transactions, filtered_repres):\n        project_name = instance.context.data[\"projectName\"]\n\n        instance_stagingdir = instance.data.get(\"stagingDir\")\n        if not instance_stagingdir:\n            self.log.debug((\n                \"{0} is missing reference to staging directory.\"\n                \" Will try to get it from representation.\"\n            ).format(instance))\n\n        else:\n            self.log.debug(\n                \"Establishing staging directory \"\n                \"@ {0}\".format(instance_stagingdir)\n            )\n\n        template_name = self.get_template_name(instance)\n\n        op_session = OperationsSession()\n        subset = self.prepare_subset(\n            instance, op_session, project_name\n        )\n        version = self.prepare_version(\n            instance, op_session, subset, project_name\n        )\n        instance.data[\"versionEntity\"] = version\n\n        anatomy = instance.context.data[\"anatomy\"]\n\n        # Get existing representations (if any)\n        existing_repres_by_name = {\n            repre_doc[\"name\"].lower(): repre_doc\n            for repre_doc in get_representations(\n                project_name,\n                version_ids=[version[\"_id\"]],\n                fields=[\"_id\", \"name\"]\n            )\n        }\n\n        # Prepare all representations\n        prepared_representations = []\n        for repre in filtered_repres:\n            # todo: reduce/simplify what is returned from this function\n            prepared = self.prepare_representation(\n                repre,\n                template_name,\n                existing_repres_by_name,\n                version,\n                instance_stagingdir,\n                instance)\n\n            for src, dst in prepared[\"transfers\"]:\n                # todo: add support for hardlink transfers\n                file_transactions.add(src, dst)\n\n            prepared_representations.append(prepared)\n\n        # Each instance can also have pre-defined transfers not explicitly\n        # part of a representation - like texture resources used by a\n        # .ma representation. Those destination paths are pre-defined, etc.\n        # todo: should we move or simplify this logic?\n        resource_destinations = set()\n\n        file_copy_modes = [\n            (\"transfers\", FileTransaction.MODE_COPY),\n            (\"hardlinks\", FileTransaction.MODE_HARDLINK)\n        ]\n        for files_type, copy_mode in file_copy_modes:\n            for src, dst in instance.data.get(files_type, []):\n                self._validate_path_in_project_roots(anatomy, dst)\n\n                file_transactions.add(src, dst, mode=copy_mode)\n                resource_destinations.add(os.path.abspath(dst))\n\n        # Bulk write to the database\n        # We write the subset and version to the database before the File\n        # Transaction to reduce the chances of another publish trying to\n        # publish to the same version number since that chance can greatly\n        # increase if the file transaction takes a long time.\n        op_session.commit()\n\n        self.log.info(\"Subset '{subset[name]}' version {version[name]} \"\n                      \"written to database..\".format(subset=subset,\n                                                     version=version))\n\n        # Process all file transfers of all integrations now\n        self.log.debug(\"Integrating source files to destination ...\")\n        file_transactions.process()\n        self.log.debug(\n            \"Backed up existing files: {}\".format(file_transactions.backups))\n        self.log.debug(\n            \"Transferred files: {}\".format(file_transactions.transferred))\n        self.log.debug(\"Retrieving Representation Site Sync information ...\")\n\n        # Get the accessible sites for Site Sync\n        modules_by_name = instance.context.data[\"openPypeModules\"]\n        sync_server_module = modules_by_name.get(\"sync_server\")\n        if sync_server_module is None:\n            sites = [{\n                \"name\": \"studio\",\n                \"created_dt\": datetime.datetime.now()\n            }]\n        else:\n            sites = sync_server_module.compute_resource_sync_sites(\n                project_name=instance.data[\"projectEntity\"][\"name\"]\n            )\n        self.log.debug(\"Sync Server Sites: {}\".format(sites))\n\n        # Compute the resource file infos once (files belonging to the\n        # version instance instead of an individual representation) so\n        # we can re-use those file infos per representation\n        resource_file_infos = self.get_files_info(resource_destinations,\n                                                  sites=sites,\n                                                  anatomy=anatomy)\n\n        # Finalize the representations now the published files are integrated\n        # Get 'files' info for representations and its attached resources\n        new_repre_names_low = set()\n        for prepared in prepared_representations:\n            repre_doc = prepared[\"representation\"]\n            repre_update_data = prepared[\"repre_doc_update_data\"]\n            transfers = prepared[\"transfers\"]\n            destinations = [dst for src, dst in transfers]\n            repre_doc[\"files\"] = self.get_files_info(\n                destinations, sites=sites, anatomy=anatomy\n            )\n\n            # Add the version resource file infos to each representation\n            repre_doc[\"files\"] += resource_file_infos\n\n            # Set up representation for writing to the database. Since\n            # we *might* be overwriting an existing entry if the version\n            # already existed we'll use ReplaceOnce with `upsert=True`\n            if repre_update_data is None:\n                op_session.create_entity(\n                    project_name, repre_doc[\"type\"], repre_doc\n                )\n            else:\n                op_session.update_entity(\n                    project_name,\n                    repre_doc[\"type\"],\n                    repre_doc[\"_id\"],\n                    repre_update_data\n                )\n\n            new_repre_names_low.add(repre_doc[\"name\"].lower())\n\n        # Delete any existing representations that didn't get any new data\n        # if the instance is not set to append mode\n        if not instance.data.get(\"append\", False):\n            for name, existing_repres in existing_repres_by_name.items():\n                if name not in new_repre_names_low:\n                    # We add the exact representation name because `name` is\n                    # lowercase for name matching only and not in the database\n                    op_session.delete_entity(\n                        project_name, \"representation\", existing_repres[\"_id\"]\n                    )\n\n        self.log.debug(\"{}\".format(op_session.to_data()))\n        op_session.commit()\n\n        # Backwards compatibility used in hero integration.\n        # todo: can we avoid the need to store this?\n        instance.data[\"published_representations\"] = {\n            p[\"representation\"][\"_id\"]: p for p in prepared_representations\n        }\n\n        self.log.info(\n            \"Registered {} representations: {}\".format(\n                len(prepared_representations),\n                \", \".join(p[\"representation\"][\"name\"]\n                          for p in prepared_representations)\n            )\n        )\n\n    def prepare_subset(self, instance, op_session, project_name):\n        asset_doc = instance.data[\"assetEntity\"]\n        subset_name = instance.data[\"subset\"]\n        family = instance.data[\"family\"]\n        self.log.debug(\"Subset: {}\".format(subset_name))\n\n        # Get existing subset if it exists\n        existing_subset_doc = get_subset_by_name(\n            project_name, subset_name, asset_doc[\"_id\"]\n        )\n\n        # Define subset data\n        data = {\n            \"families\": get_instance_families(instance)\n        }\n\n        subset_group = instance.data.get(\"subsetGroup\")\n        if subset_group:\n            data[\"subsetGroup\"] = subset_group\n        elif existing_subset_doc:\n            # Preserve previous subset group if new version does not set it\n            if \"subsetGroup\" in existing_subset_doc.get(\"data\", {}):\n                subset_group = existing_subset_doc[\"data\"][\"subsetGroup\"]\n                data[\"subsetGroup\"] = subset_group\n\n        subset_id = None\n        if existing_subset_doc:\n            subset_id = existing_subset_doc[\"_id\"]\n        subset_doc = new_subset_document(\n            subset_name, family, asset_doc[\"_id\"], data, subset_id\n        )\n\n        if existing_subset_doc is None:\n            # Create a new subset\n            self.log.info(\"Subset '%s' not found, creating ...\" % subset_name)\n            op_session.create_entity(\n                project_name, subset_doc[\"type\"], subset_doc\n            )\n\n        else:\n            # Update existing subset data with new data and set in database.\n            # We also change the found subset in-place so we don't need to\n            # re-query the subset afterwards\n            subset_doc[\"data\"].update(data)\n            update_data = prepare_subset_update_data(\n                existing_subset_doc, subset_doc\n            )\n            op_session.update_entity(\n                project_name,\n                subset_doc[\"type\"],\n                subset_doc[\"_id\"],\n                update_data\n            )\n\n        self.log.debug(\"Prepared subset: {}\".format(subset_name))\n        return subset_doc\n\n    def prepare_version(self, instance, op_session, subset_doc, project_name):\n        version_number = instance.data[\"version\"]\n\n        existing_version = get_version_by_name(\n            project_name,\n            version_number,\n            subset_doc[\"_id\"],\n            fields=[\"_id\"]\n        )\n        version_id = None\n        if existing_version:\n            version_id = existing_version[\"_id\"]\n\n        version_data = self.create_version_data(instance)\n        version_doc = new_version_doc(\n            version_number,\n            subset_doc[\"_id\"],\n            version_data,\n            version_id\n        )\n\n        if existing_version:\n            self.log.debug(\"Updating existing version ...\")\n            update_data = prepare_version_update_data(\n                existing_version, version_doc\n            )\n            op_session.update_entity(\n                project_name,\n                version_doc[\"type\"],\n                version_doc[\"_id\"],\n                update_data\n            )\n        else:\n            self.log.debug(\"Creating new version ...\")\n            op_session.create_entity(\n                project_name, version_doc[\"type\"], version_doc\n            )\n\n        self.log.debug(\n            \"Prepared version: v{0:03d}\".format(version_doc[\"name\"])\n        )\n\n        return version_doc\n\n    def _validate_repre_files(self, files, is_sequence_representation):\n        \"\"\"Validate representation files before transfer preparation.\n\n        Check if files contain only filenames instead of full paths and check\n        if sequence don't contain more than one sequence or has remainders.\n\n        Args:\n            files (Union[str, List[str]]): Files from representation.\n            is_sequence_representation (bool): Files are for sequence.\n\n        Raises:\n            KnownPublishError: If validations don't pass.\n        \"\"\"\n\n        if not files:\n            return\n\n        if not is_sequence_representation:\n            files = [files]\n\n        if any(os.path.isabs(fname) for fname in files):\n            raise KnownPublishError(\"Given file names contain full paths\")\n\n        if not is_sequence_representation:\n            return\n\n        src_collections, remainders = clique.assemble(files)\n        if len(files) < 2 or len(src_collections) != 1 or remainders:\n            raise KnownPublishError((\n                \"Files of representation does not contain proper\"\n                \" sequence files.\\nCollected collections: {}\"\n                \"\\nCollected remainders: {}\"\n            ).format(\n                \", \".join([str(col) for col in src_collections]),\n                \", \".join([str(rem) for rem in remainders])\n            ))\n\n    def prepare_representation(self, repre,\n                               template_name,\n                               existing_repres_by_name,\n                               version,\n                               instance_stagingdir,\n                               instance):\n\n        # pre-flight validations\n        if repre[\"ext\"].startswith(\".\"):\n            raise KnownPublishError((\n                \"Extension must not start with a dot '.': {}\"\n            ).format(repre[\"ext\"]))\n\n        if repre.get(\"transfers\"):\n            raise KnownPublishError((\n                \"Representation is not allowed to have transfers\"\n                \"data before integration. They are computed in \"\n                \"the integrator. Got: {}\"\n            ).format(repre[\"transfers\"]))\n\n        # create template data for Anatomy\n        template_data = copy.deepcopy(instance.data[\"anatomyData\"])\n\n        # required representation keys\n        files = repre[\"files\"]\n        template_data[\"representation\"] = repre[\"name\"]\n        template_data[\"ext\"] = repre[\"ext\"]\n\n        # allow overwriting existing version\n        template_data[\"version\"] = version[\"name\"]\n\n        # add template data for colorspaceData\n        if repre.get(\"colorspaceData\"):\n            colorspace = repre[\"colorspaceData\"][\"colorspace\"]\n            # replace spaces with underscores\n            # pipeline.colorspace.parse_colorspace_from_filepath\n            # is checking it with underscores too\n            colorspace = colorspace.replace(\" \", \"_\")\n            template_data[\"colorspace\"] = colorspace\n\n        stagingdir = repre.get(\"stagingDir\")\n        if not stagingdir:\n            # Fall back to instance staging dir if not explicitly\n            # set for representation in the instance\n            self.log.debug((\n                \"Representation uses instance staging dir: {}\"\n            ).format(instance_stagingdir))\n            stagingdir = instance_stagingdir\n\n        if not stagingdir:\n            raise KnownPublishError(\n                \"No staging directory set for representation: {}\".format(repre)\n            )\n\n        # optionals\n        # retrieve additional anatomy data from representation if exists\n        for key, anatomy_key in {\n            # Representation Key: Anatomy data key\n            \"resolutionWidth\": \"resolution_width\",\n            \"resolutionHeight\": \"resolution_height\",\n            \"fps\": \"fps\",\n            \"outputName\": \"output\",\n            \"originalBasename\": \"originalBasename\"\n        }.items():\n            # Allow to take value from representation\n            # if not found also consider instance.data\n            value = repre.get(key)\n            if value is None:\n                value = instance.data.get(key)\n\n            if value is not None:\n                template_data[anatomy_key] = value\n\n        self.log.debug(\"Anatomy template name: {}\".format(template_name))\n        anatomy = instance.context.data[\"anatomy\"]\n        publish_template_category = anatomy.templates[template_name]\n        template = os.path.normpath(publish_template_category[\"path\"])\n\n        is_udim = bool(repre.get(\"udim\"))\n\n        # handle publish in place\n        if \"{originalDirname}\" in template:\n            # store as originalDirname only original value without project root\n            # if instance collected originalDirname is present, it should be\n            # used for all represe\n            # from temp to final\n            original_directory = (\n                instance.data.get(\"originalDirname\") or instance_stagingdir)\n\n            _rootless = self.get_rootless_path(anatomy, original_directory)\n            if _rootless == original_directory:\n                raise KnownPublishError((\n                        \"Destination path '{}' \".format(original_directory) +\n                        \"must be in project dir\"\n                ))\n            relative_path_start = _rootless.rfind('}') + 2\n            without_root = _rootless[relative_path_start:]\n            template_data[\"originalDirname\"] = without_root\n\n        is_sequence_representation = isinstance(files, (list, tuple))\n        self._validate_repre_files(files, is_sequence_representation)\n\n        # Output variables of conditions below:\n        # - transfers (List[Tuple[str, str]]): src -> dst filepaths to copy\n        # - repre_context (Dict[str, Any]): context data used to fill template\n        # - template_data (Dict[str, Any]): source data used to fill template\n        #   - to add required data to 'repre_context' not used for\n        #       formatting\n        path_template_obj = anatomy.templates_obj[template_name][\"path\"]\n\n        # Treat template with 'orignalBasename' in special way\n        if \"{originalBasename}\" in template:\n            # Remove 'frame' from template data\n            template_data.pop(\"frame\", None)\n\n            # Find out first frame string value\n            first_index_padded = None\n            if not is_udim and is_sequence_representation:\n                col = clique.assemble(files)[0][0]\n                sorted_frames = tuple(sorted(col.indexes))\n                # First frame used for end value\n                first_frame = sorted_frames[0]\n                # Get last frame for padding\n                last_frame = sorted_frames[-1]\n                # Use padding from collection of length of last frame as string\n                padding = max(col.padding, len(str(last_frame)))\n                first_index_padded = get_frame_padded(\n                    frame=first_frame,\n                    padding=padding\n                )\n\n            # Convert files to list for single file as remaining part is only\n            #   transfers creation (iteration over files)\n            if not is_sequence_representation:\n                files = [files]\n\n            repre_context = None\n            transfers = []\n            for src_file_name in files:\n                template_data[\"originalBasename\"], _ = os.path.splitext(\n                    src_file_name)\n\n                dst = path_template_obj.format_strict(template_data)\n                src = os.path.join(stagingdir, src_file_name)\n                transfers.append((src, dst))\n                if repre_context is None:\n                    repre_context = dst.used_values\n\n            if not is_udim and first_index_padded is not None:\n                repre_context[\"frame\"] = first_index_padded\n\n        elif is_sequence_representation:\n            # Collection of files (sequence)\n            src_collections, remainders = clique.assemble(files)\n\n            src_collection = src_collections[0]\n            destination_indexes = list(src_collection.indexes)\n            # Use last frame for minimum padding\n            #   - that should cover both 'udim' and 'frame' minimum padding\n            destination_padding = len(str(destination_indexes[-1]))\n            if not is_udim:\n                # Change padding for frames if template has defined higher\n                #   padding.\n                template_padding = int(\n                    publish_template_category[\"frame_padding\"]\n                )\n                if template_padding > destination_padding:\n                    destination_padding = template_padding\n\n                # If the representation has `frameStart` set it renumbers the\n                # frame indices of the published collection. It will start from\n                # that `frameStart` index instead. Thus if that frame start\n                # differs from the collection we want to shift the destination\n                # frame indices from the source collection.\n                # In case source are published in place we need to\n                # skip renumbering\n                repre_frame_start = repre.get(\"frameStart\")\n                if repre_frame_start is not None:\n                    index_frame_start = int(repre_frame_start)\n                    # Shift destination sequence to the start frame\n                    destination_indexes = [\n                        index_frame_start + idx\n                        for idx in range(len(destination_indexes))\n                    ]\n\n            # To construct the destination template with anatomy we require\n            # a Frame or UDIM tile set for the template data. We use the first\n            # index of the destination for that because that could've shifted\n            # from the source indexes, etc.\n            first_index_padded = get_frame_padded(\n                frame=destination_indexes[0],\n                padding=destination_padding\n            )\n\n            # Construct destination collection from template\n            repre_context = None\n            dst_filepaths = []\n            for index in destination_indexes:\n                if is_udim:\n                    template_data[\"udim\"] = index\n                else:\n                    template_data[\"frame\"] = index\n                template_filled = path_template_obj.format_strict(\n                    template_data\n                )\n                dst_filepaths.append(template_filled)\n                if repre_context is None:\n                    self.log.debug(\n                        \"Template filled: {}\".format(str(template_filled))\n                    )\n                    repre_context = template_filled.used_values\n\n            # Make sure context contains frame\n            # NOTE: Frame would not be available only if template does not\n            #   contain '{frame}' in template -> Do we want support it?\n            if not is_udim:\n                repre_context[\"frame\"] = first_index_padded\n\n            # Update the destination indexes and padding\n            dst_collection = clique.assemble(dst_filepaths)[0][0]\n            dst_collection.padding = destination_padding\n            if len(src_collection.indexes) != len(dst_collection.indexes):\n                raise KnownPublishError((\n                    \"This is a bug. Source sequence frames length\"\n                    \" does not match integration frames length\"\n                ))\n\n            # Multiple file transfers\n            transfers = []\n            for src_file_name, dst in zip(src_collection, dst_collection):\n                src = os.path.join(stagingdir, src_file_name)\n                transfers.append((src, dst))\n\n        else:\n            # Single file\n            # Manage anatomy template data\n            template_data.pop(\"frame\", None)\n            if is_udim:\n                template_data[\"udim\"] = repre[\"udim\"][0]\n            # Construct destination filepath from template\n            template_filled = path_template_obj.format_strict(template_data)\n            repre_context = template_filled.used_values\n            dst = os.path.normpath(template_filled)\n\n            # Single file transfer\n            src = os.path.join(stagingdir, files)\n            transfers = [(src, dst)]\n\n        # todo: Are we sure the assumption each representation\n        #       ends up in the same folder is valid?\n        if not instance.data.get(\"publishDir\"):\n            template_obj = anatomy.templates_obj[template_name][\"folder\"]\n            template_filled = template_obj.format_strict(template_data)\n            instance.data[\"publishDir\"] = template_filled\n\n        for key in self.db_representation_context_keys:\n            # Also add these values to the context even if not used by the\n            # destination template\n            value = template_data.get(key)\n            if value is not None:\n                repre_context[key] = value\n\n        # Explicitly store the full list even though template data might\n        # have a different value because it uses just a single udim tile\n        if repre.get(\"udim\"):\n            repre_context[\"udim\"] = repre.get(\"udim\")  # store list\n\n        # Use previous representation's id if there is a name match\n        existing = existing_repres_by_name.get(repre[\"name\"].lower())\n        repre_id = None\n        if existing:\n            repre_id = existing[\"_id\"]\n\n        # Store first transferred destination as published path data\n        # - used primarily for reviews that are integrated to custom modules\n        # TODO we should probably store all integrated files\n        #   related to the representation?\n        published_path = transfers[0][1]\n        repre[\"published_path\"] = published_path\n\n        # todo: `repre` is not the actual `representation` entity\n        #       we should simplify/clarify difference between data above\n        #       and the actual representation entity for the database\n        data = repre.get(\"data\", {})\n        data.update({\"path\": published_path, \"template\": template})\n\n        # add colorspace data if any exists on representation\n        if repre.get(\"colorspaceData\"):\n            data[\"colorspaceData\"] = repre[\"colorspaceData\"]\n\n        repre_doc = new_representation_doc(\n            repre[\"name\"], version[\"_id\"], repre_context, data, repre_id\n        )\n        update_data = None\n        if repre_id is not None:\n            update_data = prepare_representation_update_data(\n                existing, repre_doc\n            )\n\n        return {\n            \"representation\": repre_doc,\n            \"repre_doc_update_data\": update_data,\n            \"anatomy_data\": template_data,\n            \"transfers\": transfers,\n            # todo: avoid the need for 'published_files' used by Integrate Hero\n            # backwards compatibility\n            \"published_files\": [transfer[1] for transfer in transfers]\n        }\n\n    def create_version_data(self, instance):\n        \"\"\"Create the data dictionary for the version\n\n        Args:\n            instance: the current instance being published\n\n        Returns:\n            dict: the required information for version[\"data\"]\n        \"\"\"\n\n        context = instance.context\n\n        # create relative source path for DB\n        if \"source\" in instance.data:\n            source = instance.data[\"source\"]\n        else:\n            source = context.data[\"currentFile\"]\n            anatomy = instance.context.data[\"anatomy\"]\n            source = self.get_rootless_path(anatomy, source)\n        self.log.debug(\"Source: {}\".format(source))\n\n        version_data = {\n            \"families\": get_instance_families(instance),\n            \"time\": context.data[\"time\"],\n            \"author\": context.data[\"user\"],\n            \"source\": source,\n            \"comment\": instance.data[\"comment\"],\n            \"machine\": context.data.get(\"machine\"),\n            \"fps\": instance.data.get(\"fps\", context.data.get(\"fps\"))\n        }\n\n        # todo: preferably we wouldn't need this \"if dict\" etc. logic and\n        #       instead be able to rely what the input value is if it's set.\n        intent_value = context.data.get(\"intent\")\n        if intent_value and isinstance(intent_value, dict):\n            intent_value = intent_value.get(\"value\")\n\n        if intent_value:\n            version_data[\"intent\"] = intent_value\n\n        # Include optional data if present in\n        optionals = [\n            \"frameStart\", \"frameEnd\", \"step\",\n            \"handleEnd\", \"handleStart\", \"sourceHashes\"\n        ]\n        for key in optionals:\n            if key in instance.data:\n                version_data[key] = instance.data[key]\n\n        # Include instance.data[versionData] directly\n        version_data_instance = instance.data.get(\"versionData\")\n        if version_data_instance:\n            version_data.update(version_data_instance)\n\n        return version_data\n\n    def get_template_name(self, instance):\n        \"\"\"Return anatomy template name to use for integration\"\"\"\n\n        # Anatomy data is pre-filled by Collectors\n        context = instance.context\n        project_name = context.data[\"projectName\"]\n\n        # Task can be optional in anatomy data\n        host_name = context.data[\"hostName\"]\n        anatomy_data = instance.data[\"anatomyData\"]\n        family = anatomy_data[\"family\"]\n        task_info = anatomy_data.get(\"task\") or {}\n\n        return get_publish_template_name(\n            project_name,\n            host_name,\n            family,\n            task_name=task_info.get(\"name\"),\n            task_type=task_info.get(\"type\"),\n            project_settings=context.data[\"project_settings\"],\n            logger=self.log\n        )\n\n    def get_rootless_path(self, anatomy, path):\n        \"\"\"Returns, if possible, path without absolute portion from root\n            (eg. 'c:\\' or '/opt/..')\n\n         This information is platform dependent and shouldn't be captured.\n         Example:\n             'c:/projects/MyProject1/Assets/publish...' >\n             '{root}/MyProject1/Assets...'\n\n        Args:\n            anatomy: anatomy part from instance\n            path: path (absolute)\n        Returns:\n            path: modified path if possible, or unmodified path\n            + warning logged\n        \"\"\"\n\n        success, rootless_path = anatomy.find_root_template_from_path(path)\n        if success:\n            path = rootless_path\n        else:\n            self.log.warning((\n                \"Could not find root path for remapping \\\"{}\\\".\"\n                \" This may cause issues on farm.\"\n            ).format(path))\n        return path\n\n    def get_files_info(self, destinations, sites, anatomy):\n        \"\"\"Prepare 'files' info portion for representations.\n\n        Arguments:\n            destinations (list): List of transferred file destinations\n            sites (list): array of published locations\n            anatomy: anatomy part from instance\n        Returns:\n            output_resources: array of dictionaries to be added to 'files' key\n            in representation\n        \"\"\"\n\n        file_infos = []\n        for file_path in destinations:\n            file_info = self.prepare_file_info(file_path, anatomy, sites=sites)\n            file_infos.append(file_info)\n        return file_infos\n\n    def prepare_file_info(self, path, anatomy, sites):\n        \"\"\" Prepare information for one file (asset or resource)\n\n        Arguments:\n            path: destination url of published file\n            anatomy: anatomy part from instance\n            sites: array of published locations,\n                [ {'name':'studio', 'created_dt':date} by default\n                keys expected ['studio', 'site1', 'gdrive1']\n\n        Returns:\n            dict: file info dictionary\n        \"\"\"\n\n        return {\n            \"_id\": ObjectId(),\n            \"path\": self.get_rootless_path(anatomy, path),\n            \"size\": os.path.getsize(path),\n            \"hash\": source_hash(path),\n            \"sites\": sites\n        }\n\n    def _validate_path_in_project_roots(self, anatomy, file_path):\n        \"\"\"Checks if 'file_path' starts with any of the roots.\n\n        Used to check that published path belongs to project, eg. we are not\n        trying to publish to local only folder.\n        Args:\n            anatomy (Anatomy)\n            file_path (str)\n        Raises\n            (KnownPublishError)\n        \"\"\"\n        path = self.get_rootless_path(anatomy, file_path)\n        if not path:\n            raise KnownPublishError((\n                \"Destination path '{}' \".format(file_path) +\n                \"must be in project dir\"\n            ))\n"
  },
  {
    "path": "openpype/plugins/publish/integrate_hero_version.py",
    "content": "import os\nimport copy\nimport clique\nimport errno\nimport shutil\n\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_version_by_id,\n    get_hero_version_by_subset_id,\n    get_archived_representations,\n    get_representations,\n)\nfrom openpype.client.operations import (\n    OperationsSession,\n    new_hero_version_doc,\n    prepare_hero_version_update_data,\n    prepare_representation_update_data,\n)\nfrom openpype.lib import create_hard_link\nfrom openpype.pipeline import (\n    schema\n)\nfrom openpype.pipeline.publish import get_publish_template_name\n\n\nclass IntegrateHeroVersion(pyblish.api.InstancePlugin):\n    label = \"Integrate Hero Version\"\n    # Must happen after IntegrateNew\n    order = pyblish.api.IntegratorOrder + 0.1\n\n    optional = True\n    active = True\n\n    # Families are modified using settings\n    families = [\n        \"model\",\n        \"rig\",\n        \"setdress\",\n        \"look\",\n        \"pointcache\",\n        \"animation\"\n    ]\n\n    # Can specify representation names that will be ignored (lower case)\n    ignored_representation_names = []\n    db_representation_context_keys = [\n        \"project\", \"asset\", \"task\", \"subset\", \"representation\",\n        \"family\", \"hierarchy\", \"task\", \"username\", \"user\"\n    ]\n    # QUESTION/TODO this process should happen on server if crashed due to\n    # permissions error on files (files were used or user didn't have perms)\n    # *but all other plugins must be sucessfully completed\n\n    _default_template_name = \"hero\"\n\n    def process(self, instance):\n        self.log.debug(\n            \"--- Integration of Hero version for subset `{}` begins.\".format(\n                instance.data.get(\"subset\", str(instance))\n            )\n        )\n        published_repres = instance.data.get(\"published_representations\")\n        if not published_repres:\n            self.log.debug(\n                \"*** There are no published representations on the instance.\"\n            )\n            return\n\n        anatomy = instance.context.data[\"anatomy\"]\n        project_name = anatomy.project_name\n\n        template_key = self._get_template_key(project_name, instance)\n\n        if template_key not in anatomy.templates:\n            self.log.warning((\n                \"!!! Anatomy of project \\\"{}\\\" does not have set\"\n                \" \\\"{}\\\" template key!\"\n            ).format(project_name, template_key))\n            return\n\n        if \"path\" not in anatomy.templates[template_key]:\n            self.log.warning((\n                \"!!! There is not set \\\"path\\\" template in \\\"{}\\\" anatomy\"\n                \" for project \\\"{}\\\".\"\n            ).format(template_key, project_name))\n            return\n\n        hero_template = anatomy.templates[template_key][\"path\"]\n        self.log.debug(\"`hero` template check was successful. `{}`\".format(\n            hero_template\n        ))\n\n        self.integrate_instance(\n            instance, project_name, template_key, hero_template\n        )\n\n    def integrate_instance(\n        self, instance, project_name, template_key, hero_template\n    ):\n        anatomy = instance.context.data[\"anatomy\"]\n        published_repres = instance.data[\"published_representations\"]\n        hero_publish_dir = self.get_publish_dir(instance, template_key)\n\n        src_version_entity = instance.data.get(\"versionEntity\")\n        filtered_repre_ids = []\n        for repre_id, repre_info in published_repres.items():\n            repre = repre_info[\"representation\"]\n            if repre[\"name\"].lower() in self.ignored_representation_names:\n                self.log.debug(\n                    \"Filtering representation with name: `{}`\".format(\n                        repre[\"name\"].lower()\n                    )\n                )\n                filtered_repre_ids.append(repre_id)\n\n        for repre_id in filtered_repre_ids:\n            published_repres.pop(repre_id, None)\n\n        if not published_repres:\n            self.log.debug(\n                \"*** All published representations were filtered by name.\"\n            )\n            return\n\n        if src_version_entity is None:\n            self.log.debug((\n                \"Published version entity was not sent in representation data.\"\n                \" Querying entity from database.\"\n            ))\n            src_version_entity = self.version_from_representations(\n                project_name, published_repres\n            )\n\n        if not src_version_entity:\n            self.log.warning((\n                \"!!! Can't find origin version in database.\"\n                \" Skipping hero version publish.\"\n            ))\n            return\n\n        if AYON_SERVER_ENABLED and src_version_entity[\"name\"] == 0:\n            self.log.debug(\n                \"Version 0 cannot have hero version. Skipping.\"\n            )\n            return\n\n        all_copied_files = []\n        transfers = instance.data.get(\"transfers\", list())\n        for _src, dst in transfers:\n            dst = os.path.normpath(dst)\n            if dst not in all_copied_files:\n                all_copied_files.append(dst)\n\n        hardlinks = instance.data.get(\"hardlinks\", list())\n        for _src, dst in hardlinks:\n            dst = os.path.normpath(dst)\n            if dst not in all_copied_files:\n                all_copied_files.append(dst)\n\n        all_repre_file_paths = []\n        for repre_info in published_repres.values():\n            published_files = repre_info.get(\"published_files\") or []\n            for file_path in published_files:\n                file_path = os.path.normpath(file_path)\n                if file_path not in all_repre_file_paths:\n                    all_repre_file_paths.append(file_path)\n\n        # TODO this is not best practice of getting resources for publish\n        # WARNING due to this we must remove all files from hero publish dir\n        instance_publish_dir = os.path.normpath(\n            instance.data[\"publishDir\"]\n        )\n        other_file_paths_mapping = []\n        for file_path in all_copied_files:\n            # Check if it is from publishDir\n            if not file_path.startswith(instance_publish_dir):\n                continue\n\n            if file_path in all_repre_file_paths:\n                continue\n\n            dst_filepath = file_path.replace(\n                instance_publish_dir, hero_publish_dir\n            )\n            other_file_paths_mapping.append((file_path, dst_filepath))\n\n        # Current version\n        old_version, old_repres = self.current_hero_ents(\n            project_name, src_version_entity\n        )\n\n        old_repres_by_name = {\n            repre[\"name\"].lower(): repre for repre in old_repres\n        }\n\n        op_session = OperationsSession()\n\n        entity_id = None\n        if old_version:\n            entity_id = old_version[\"_id\"]\n\n        if AYON_SERVER_ENABLED:\n            new_hero_version = new_hero_version_doc(\n                src_version_entity[\"parent\"],\n                copy.deepcopy(src_version_entity[\"data\"]),\n                src_version_entity[\"name\"],\n                entity_id=entity_id\n            )\n        else:\n            new_hero_version = new_hero_version_doc(\n                src_version_entity[\"_id\"],\n                src_version_entity[\"parent\"],\n                entity_id=entity_id\n            )\n\n        if old_version:\n            self.log.debug(\"Replacing old hero version.\")\n            update_data = prepare_hero_version_update_data(\n                old_version, new_hero_version\n            )\n            op_session.update_entity(\n                project_name,\n                new_hero_version[\"type\"],\n                old_version[\"_id\"],\n                update_data\n            )\n        else:\n            self.log.debug(\"Creating first hero version.\")\n            op_session.create_entity(\n                project_name, new_hero_version[\"type\"], new_hero_version\n            )\n\n        # Separate old representations into `to replace` and `to delete`\n        old_repres_to_replace = {}\n        old_repres_to_delete = {}\n        for repre_info in published_repres.values():\n            repre = repre_info[\"representation\"]\n            repre_name_low = repre[\"name\"].lower()\n            if repre_name_low in old_repres_by_name:\n                old_repres_to_replace[repre_name_low] = (\n                    old_repres_by_name.pop(repre_name_low)\n                )\n\n        if old_repres_by_name:\n            old_repres_to_delete = old_repres_by_name\n\n        archived_repres = list(get_archived_representations(\n            project_name,\n            # Check what is type of archived representation\n            version_ids=[new_hero_version[\"_id\"]]\n        ))\n        archived_repres_by_name = {}\n        for repre in archived_repres:\n            repre_name_low = repre[\"name\"].lower()\n            archived_repres_by_name[repre_name_low] = repre\n\n        backup_hero_publish_dir = None\n        if os.path.exists(hero_publish_dir):\n            backup_hero_publish_dir = hero_publish_dir + \".BACKUP\"\n            max_idx = 10\n            idx = 0\n            _backup_hero_publish_dir = backup_hero_publish_dir\n            while os.path.exists(_backup_hero_publish_dir):\n                self.log.debug((\n                    \"Backup folder already exists.\"\n                    \" Trying to remove \\\"{}\\\"\"\n                ).format(_backup_hero_publish_dir))\n\n                try:\n                    shutil.rmtree(_backup_hero_publish_dir)\n                    backup_hero_publish_dir = _backup_hero_publish_dir\n                    break\n                except Exception:\n                    self.log.info(\n                        \"Could not remove previous backup folder.\"\n                        \" Trying to add index to folder name.\"\n                    )\n\n                _backup_hero_publish_dir = (\n                    backup_hero_publish_dir + str(idx)\n                )\n                if not os.path.exists(_backup_hero_publish_dir):\n                    backup_hero_publish_dir = _backup_hero_publish_dir\n                    break\n\n                if idx > max_idx:\n                    raise AssertionError((\n                        \"Backup folders are fully occupied to max index \\\"{}\\\"\"\n                    ).format(max_idx))\n                    break\n\n                idx += 1\n\n            self.log.debug(\"Backup folder path is \\\"{}\\\"\".format(\n                backup_hero_publish_dir\n            ))\n            try:\n                os.rename(hero_publish_dir, backup_hero_publish_dir)\n            except PermissionError:\n                raise AssertionError((\n                    \"Could not create hero version because it is not\"\n                    \" possible to replace current hero files.\"\n                ))\n        try:\n            src_to_dst_file_paths = []\n            path_template_obj = anatomy.templates_obj[template_key][\"path\"]\n            for repre_info in published_repres.values():\n\n                # Skip if new repre does not have published repre files\n                published_files = repre_info[\"published_files\"]\n                if len(published_files) == 0:\n                    continue\n\n                # Prepare anatomy data\n                anatomy_data = copy.deepcopy(repre_info[\"anatomy_data\"])\n                anatomy_data.pop(\"version\", None)\n\n                # Get filled path to repre context\n                template_filled = path_template_obj.format_strict(anatomy_data)\n                repre_data = {\n                    \"path\": str(template_filled),\n                    \"template\": hero_template\n                }\n                repre_context = template_filled.used_values\n                for key in self.db_representation_context_keys:\n                    value = anatomy_data.get(key)\n                    if value is not None:\n                        repre_context[key] = value\n\n                # Prepare new repre\n                repre = copy.deepcopy(repre_info[\"representation\"])\n                repre[\"parent\"] = new_hero_version[\"_id\"]\n                repre[\"context\"] = repre_context\n                repre[\"data\"] = repre_data\n                repre.pop(\"_id\", None)\n\n                # Prepare paths of source and destination files\n                if len(published_files) == 1:\n                    src_to_dst_file_paths.append(\n                        (published_files[0], template_filled)\n                    )\n                else:\n                    collections, remainders = clique.assemble(published_files)\n                    if remainders or not collections or len(collections) > 1:\n                        raise Exception((\n                            \"Integrity error. Files of published\"\n                            \" representation is combination of frame\"\n                            \" collections and single files. Collections:\"\n                            \" `{}` Single files: `{}`\"\n                        ).format(str(collections), str(remainders)))\n\n                    src_col = collections[0]\n\n                    # Get head and tail for collection\n                    frame_splitter = \"_-_FRAME_SPLIT_-_\"\n                    anatomy_data[\"frame\"] = frame_splitter\n                    _template_filled = path_template_obj.format_strict(\n                        anatomy_data\n                    )\n                    head, tail = _template_filled.split(frame_splitter)\n                    padding = int(\n                        anatomy.templates[template_key][\"frame_padding\"]\n                    )\n\n                    dst_col = clique.Collection(\n                        head=head, padding=padding, tail=tail\n                    )\n                    dst_col.indexes.clear()\n                    dst_col.indexes.update(src_col.indexes)\n                    for src_file, dst_file in zip(src_col, dst_col):\n                        src_to_dst_file_paths.append(\n                            (src_file, dst_file)\n                        )\n\n                # replace original file name with hero name in repre doc\n                for index in range(len(repre.get(\"files\"))):\n                    file = repre.get(\"files\")[index]\n                    file_name = os.path.basename(file.get('path'))\n                    for src_file, dst_file in src_to_dst_file_paths:\n                        src_file_name = os.path.basename(src_file)\n                        if src_file_name == file_name:\n                            repre[\"files\"][index][\"path\"] = self._update_path(\n                                anatomy, repre[\"files\"][index][\"path\"],\n                                src_file, dst_file)\n\n                            repre[\"files\"][index][\"hash\"] = self._update_hash(\n                                repre[\"files\"][index][\"hash\"],\n                                src_file_name, dst_file\n                            )\n\n                schema.validate(repre)\n\n                repre_name_low = repre[\"name\"].lower()\n                # Replace current representation\n                if repre_name_low in old_repres_to_replace:\n                    old_repre = old_repres_to_replace.pop(repre_name_low)\n\n                    repre[\"_id\"] = old_repre[\"_id\"]\n                    update_data = prepare_representation_update_data(\n                        old_repre, repre)\n\n                    # Keep previously synchronized sites up-to-date\n                    #   by comparing old and new sites and adding old sites\n                    #   if missing in new ones\n                    # Prepare all sites from all files in old representation\n                    old_site_names = set()\n                    for file_info in old_repre.get(\"files\", []):\n                        old_site_names |= {\n                            site[\"name\"]\n                            for site in file_info[\"sites\"]\n                        }\n\n                    for file_info in update_data.get(\"files\", []):\n                        file_info.setdefault(\"sites\", [])\n                        file_info_site_names = {\n                            site[\"name\"]\n                            for site in file_info[\"sites\"]\n                        }\n                        for site_name in old_site_names:\n                            if site_name not in file_info_site_names:\n                                file_info[\"sites\"].append({\n                                    \"name\": site_name\n                                })\n\n                    op_session.update_entity(\n                        project_name,\n                        old_repre[\"type\"],\n                        old_repre[\"_id\"],\n                        update_data\n                    )\n\n                # Unarchive representation\n                elif repre_name_low in archived_repres_by_name:\n                    archived_repre = archived_repres_by_name.pop(\n                        repre_name_low\n                    )\n                    repre[\"_id\"] = archived_repre[\"old_id\"]\n                    update_data = prepare_representation_update_data(\n                        archived_repre, repre)\n                    op_session.update_entity(\n                        project_name,\n                        old_repre[\"type\"],\n                        archived_repre[\"_id\"],\n                        update_data\n                    )\n\n                # Create representation\n                else:\n                    repre.pop(\"_id\", None)\n                    op_session.create_entity(project_name, \"representation\",\n                                             repre)\n\n            self.path_checks = []\n\n            # Copy(hardlink) paths of source and destination files\n            # TODO should we *only* create hardlinks?\n            # TODO should we keep files for deletion until this is successful?\n            for src_path, dst_path in src_to_dst_file_paths:\n                self.copy_file(src_path, dst_path)\n\n            for src_path, dst_path in other_file_paths_mapping:\n                self.copy_file(src_path, dst_path)\n\n            # Archive not replaced old representations\n            for repre_name_low, repre in old_repres_to_delete.items():\n                # Replace archived representation (This is backup)\n                # - should not happen to have both repre and archived repre\n                if repre_name_low in archived_repres_by_name:\n                    archived_repre = archived_repres_by_name.pop(\n                        repre_name_low\n                    )\n\n                    changes = {\"old_id\": repre[\"_id\"],\n                               \"_id\": archived_repre[\"_id\"],\n                               \"type\": archived_repre[\"type\"]}\n                    op_session.update_entity(project_name,\n                                             archived_repre[\"type\"],\n                                             archived_repre[\"_id\"],\n                                             changes)\n                else:\n                    repre[\"old_id\"] = repre.pop(\"_id\")\n                    repre[\"type\"] = \"archived_representation\"\n                    op_session.create_entity(project_name,\n                                             \"archived_representation\",\n                                             repre)\n\n            op_session.commit()\n\n            # Remove backuped previous hero\n            if (\n                backup_hero_publish_dir is not None and\n                os.path.exists(backup_hero_publish_dir)\n            ):\n                shutil.rmtree(backup_hero_publish_dir)\n\n        except Exception:\n            if (\n                backup_hero_publish_dir is not None and\n                os.path.exists(backup_hero_publish_dir)\n            ):\n                if os.path.exists(hero_publish_dir):\n                    shutil.rmtree(hero_publish_dir)\n                os.rename(backup_hero_publish_dir, hero_publish_dir)\n            self.log.error((\n                \"!!! Creating of hero version failed.\"\n                \" Previous hero version maybe lost some data!\"\n            ))\n            raise\n\n        self.log.debug((\n            \"--- hero version integration for subset `{}`\"\n            \" seems to be successful.\"\n        ).format(\n            instance.data.get(\"subset\", str(instance))\n        ))\n\n    def get_all_files_from_path(self, path):\n        files = []\n        for (dir_path, dir_names, file_names) in os.walk(path):\n            for file_name in file_names:\n                _path = os.path.join(dir_path, file_name)\n                files.append(_path)\n        return files\n\n    def get_publish_dir(self, instance, template_key):\n        anatomy = instance.context.data[\"anatomy\"]\n        template_data = copy.deepcopy(instance.data[\"anatomyData\"])\n\n        if \"originalBasename\" in instance.data:\n            template_data.update({\n                \"originalBasename\": instance.data.get(\"originalBasename\")\n            })\n\n        if \"folder\" in anatomy.templates[template_key]:\n            template_obj = anatomy.templates_obj[template_key][\"folder\"]\n            publish_folder = template_obj.format_strict(template_data)\n        else:\n            # This is for cases of Deprecated anatomy without `folder`\n            # TODO remove when all clients have solved this issue\n            self.log.warning((\n                \"Deprecation warning: Anatomy does not have set `folder`\"\n                \" key underneath `publish` (in global of for project `{}`).\"\n            ).format(anatomy.project_name))\n            # solve deprecated situation when `folder` key is not underneath\n            # `publish` anatomy\n            template_data.update({\n                \"frame\": \"FRAME_TEMP\",\n                \"representation\": \"TEMP\"\n            })\n            template_obj = anatomy.templates_obj[template_key][\"path\"]\n            file_path = template_obj.format_strict(template_data)\n\n            # Directory\n            publish_folder = os.path.dirname(file_path)\n\n        publish_folder = os.path.normpath(publish_folder)\n\n        self.log.debug(\"hero publish dir: \\\"{}\\\"\".format(publish_folder))\n\n        return publish_folder\n\n    def _get_template_key(self, project_name, instance):\n        anatomy_data = instance.data[\"anatomyData\"]\n        task_info = anatomy_data.get(\"task\") or {}\n        host_name = instance.context.data[\"hostName\"]\n\n        # TODO raise error if Hero not set?\n        family = self.main_family_from_instance(instance)\n\n        return get_publish_template_name(\n            project_name,\n            host_name,\n            family,\n            task_info.get(\"name\"),\n            task_info.get(\"type\"),\n            project_settings=instance.context.data[\"project_settings\"],\n            hero=True,\n            logger=self.log\n        )\n\n    def main_family_from_instance(self, instance):\n        \"\"\"Returns main family of entered instance.\"\"\"\n        family = instance.data.get(\"family\")\n        if not family:\n            family = instance.data[\"families\"][0]\n        return family\n\n    def copy_file(self, src_path, dst_path):\n        # TODO check drives if are the same to check if cas hardlink\n        dirname = os.path.dirname(dst_path)\n\n        try:\n            os.makedirs(dirname)\n            self.log.debug(\"Folder(s) created: \\\"{}\\\"\".format(dirname))\n        except OSError as exc:\n            if exc.errno != errno.EEXIST:\n                self.log.error(\"An unexpected error occurred.\", exc_info=True)\n                raise\n\n            self.log.debug(\"Folder already exists: \\\"{}\\\"\".format(dirname))\n\n        self.log.debug(\"Copying file \\\"{}\\\" to \\\"{}\\\"\".format(\n            src_path, dst_path\n        ))\n\n        # First try hardlink and copy if paths are cross drive\n        try:\n            create_hard_link(src_path, dst_path)\n            # Return when successful\n            return\n\n        except OSError as exc:\n            # re-raise exception if different than\n            # EXDEV - cross drive path\n            # EINVAL - wrong format, must be NTFS\n            self.log.debug(\"Hardlink failed with errno:'{}'\".format(exc.errno))\n            if exc.errno not in [errno.EXDEV, errno.EINVAL]:\n                raise\n\n        shutil.copy(src_path, dst_path)\n\n    def version_from_representations(self, project_name, repres):\n        for repre in repres:\n            version = get_version_by_id(project_name, repre[\"parent\"])\n            if version:\n                return version\n\n    def current_hero_ents(self, project_name, version):\n        hero_version = get_hero_version_by_subset_id(\n            project_name, version[\"parent\"]\n        )\n\n        if not hero_version:\n            return (None, [])\n\n        hero_repres = list(get_representations(\n            project_name, version_ids=[hero_version[\"_id\"]]\n        ))\n        return (hero_version, hero_repres)\n\n    def _update_path(self, anatomy, path, src_file, dst_file):\n        \"\"\"\n            Replaces source path with new hero path\n\n            'path' contains original path with version, must be replaced with\n            'hero' path (with 'hero' label and without version)\n\n            Args:\n                anatomy (Anatomy) - to get rootless style of path\n                path (string) - path from DB\n                src_file (string) - original file path\n                dst_file (string) - hero file path\n        \"\"\"\n        _, rootless = anatomy.find_root_template_from_path(dst_file)\n        _, rtls_src = anatomy.find_root_template_from_path(src_file)\n        return path.replace(rtls_src, rootless)\n\n    def _update_hash(self, hash, src_file_name, dst_file):\n        \"\"\"\n            Updates hash value with proper hero name\n        \"\"\"\n        src_file_name = self._get_name_without_ext(src_file_name)\n        hero_file_name = self._get_name_without_ext(dst_file)\n        return hash.replace(src_file_name, hero_file_name)\n\n    def _get_name_without_ext(self, value):\n        file_name = os.path.basename(value)\n        file_name, _ = os.path.splitext(file_name)\n        return file_name\n"
  },
  {
    "path": "openpype/plugins/publish/integrate_inputlinks.py",
    "content": "from collections import OrderedDict\n\nfrom bson.objectid import ObjectId\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline import legacy_io\n\n\nclass IntegrateInputLinks(pyblish.api.ContextPlugin):\n    \"\"\"Connecting version level dependency links\"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.2\n    label = \"Connect Dependency InputLinks\"\n\n    def process(self, context):\n        \"\"\"Connect dependency links for all instances, globally\n\n        Code steps:\n        * filter out instances that has \"versionEntity\" entry in data\n        * find workfile instance within context\n        * if workfile found:\n            - link all `loadedVersions` as input of the workfile\n            - link workfile as input of all publishing instances\n        * else:\n            - show \"no workfile\" warning\n        * link instances' inputs if it's data has \"inputVersions\" entry\n        * Write into database\n\n        inputVersions:\n            The \"inputVersions\" in instance.data should be a list of\n            version document's Id (str or ObjectId), which are the\n            dependencies of the publishing instance that should be\n            extracted from working scene by the DCC specific publish\n            plugin.\n\n        \"\"\"\n\n        workfile = None\n        publishing = []\n\n        for instance in context:\n            if not instance.data.get(\"publish\", True):\n                # Skip inactive instances\n                continue\n\n            version_doc = instance.data.get(\"versionEntity\")\n            if not version_doc:\n                self.log.debug(\"Instance %s doesn't have version.\" % instance)\n                continue\n\n            version_data = version_doc.get(\"data\", {})\n            families = version_data.get(\"families\", [])\n\n            if \"workfile\" in families:\n                workfile = instance\n            else:\n                publishing.append(instance)\n\n        if workfile is None:\n            self.log.warn(\"No workfile in this publish session.\")\n        else:\n            workfile_version_doc = workfile.data[\"versionEntity\"]\n            # link all loaded versions in scene into workfile\n            for version in context.data.get(\"loadedVersions\", []):\n                self.add_link(\n                    link_type=\"reference\",\n                    input_id=version[\"version\"],\n                    version_doc=workfile_version_doc,\n                )\n            # link workfile to all publishing versions\n            for instance in publishing:\n                self.add_link(\n                    link_type=\"generative\",\n                    input_id=workfile_version_doc[\"_id\"],\n                    version_doc=instance.data[\"versionEntity\"],\n                )\n\n        # link versions as dependencies to the instance\n        for instance in publishing:\n            for input_version in instance.data.get(\"inputVersions\") or []:\n                self.add_link(\n                    link_type=\"generative\",\n                    input_id=input_version,\n                    version_doc=instance.data[\"versionEntity\"],\n                )\n\n        if workfile is not None:\n            publishing.append(workfile)\n        self.write_links_to_database(publishing)\n\n    def add_link(self, link_type, input_id, version_doc):\n        \"\"\"Add dependency link data into version document\n\n        Args:\n            link_type (str): Type of link, one of 'reference' or 'generative'\n            input_id (str or ObjectId): Document Id of input version\n            version_doc (dict): The version document that takes the input\n\n        Returns:\n            None\n\n        \"\"\"\n        # NOTE:\n        # using OrderedDict() here is just for ensuring field order between\n        # python versions, if we ever need to use mongodb operation '$addToSet'\n        # to update and avoid duplicating elements in 'inputLinks' array in the\n        # future.\n        link = OrderedDict()\n        link[\"type\"] = link_type\n        link[\"id\"] = ObjectId(input_id)\n        link[\"linkedBy\"] = \"publish\"\n\n        if \"inputLinks\" not in version_doc[\"data\"]:\n            version_doc[\"data\"][\"inputLinks\"] = []\n        version_doc[\"data\"][\"inputLinks\"].append(link)\n\n    def write_links_to_database(self, instances):\n        \"\"\"Iter instances in context to update database\n\n        If `versionEntity.data.inputLinks` not None in `instance.data`, doc\n        in database will be updated.\n\n        \"\"\"\n        for instance in instances:\n            version_doc = instance.data.get(\"versionEntity\")\n            if version_doc is None:\n                continue\n\n            input_links = version_doc[\"data\"].get(\"inputLinks\")\n            if input_links is None:\n                continue\n\n            legacy_io.update_one(\n                {\"_id\": version_doc[\"_id\"]},\n                {\"$set\": {\"data.inputLinks\": input_links}}\n            )\n\n\nif AYON_SERVER_ENABLED:\n    del IntegrateInputLinks\n"
  },
  {
    "path": "openpype/plugins/publish/integrate_inputlinks_ayon.py",
    "content": "import collections\n\nimport pyblish.api\nfrom ayon_api import (\n    create_link,\n    make_sure_link_type_exists,\n    get_versions_links,\n)\n\nfrom openpype import AYON_SERVER_ENABLED\n\n\nclass IntegrateInputLinksAYON(pyblish.api.ContextPlugin):\n    \"\"\"Connecting version level dependency links\"\"\"\n\n    order = pyblish.api.IntegratorOrder + 0.2\n    label = \"Connect Dependency InputLinks AYON\"\n\n    def process(self, context):\n        \"\"\"Connect dependency links for all instances, globally\n\n        Code steps:\n        - filter instances that integrated version\n            - have \"versionEntity\" entry in data\n        - separate workfile instance within filtered instances\n        - when workfile instance is available:\n            - link all `loadedVersions` as input of the workfile\n            - link workfile as input of all other integrated versions\n        - link version's inputs if it's instance have \"inputVersions\" entry\n        -\n\n        inputVersions:\n            The \"inputVersions\" in instance.data should be a list of\n            version ids (str), which are the dependencies of the publishing\n            instance that should be extracted from working scene by the DCC\n            specific publish plugin.\n        \"\"\"\n\n        workfile_instance, other_instances = self.split_instances(context)\n\n        # Variable where links are stored in submethods\n        new_links_by_type = collections.defaultdict(list)\n\n        self.create_workfile_links(\n            workfile_instance, other_instances, new_links_by_type)\n\n        self.create_generative_links(other_instances, new_links_by_type)\n\n        self.create_links_on_server(context, new_links_by_type)\n\n    def split_instances(self, context):\n        workfile_instance = None\n        other_instances = []\n\n        for instance in context:\n            # Skip inactive instances\n            if not instance.data.get(\"publish\", True):\n                continue\n\n            version_doc = instance.data.get(\"versionEntity\")\n            if not version_doc:\n                self.log.debug(\n                    \"Instance {} doesn't have version.\".format(instance))\n                continue\n\n            family = instance.data.get(\"family\")\n            if family == \"workfile\":\n                workfile_instance = instance\n            else:\n                other_instances.append(instance)\n        return workfile_instance, other_instances\n\n    def add_link(self, new_links_by_type, link_type, input_id, output_id):\n        \"\"\"Add dependency link data into temporary variable.\n\n        Args:\n            new_links_by_type (dict[str, list[dict[str, Any]]]): Object where\n                output is stored.\n            link_type (str): Type of link, one of 'reference' or 'generative'\n            input_id (str): Input version id.\n            output_id (str): Output version id.\n        \"\"\"\n\n        new_links_by_type[link_type].append((input_id, output_id))\n\n    def create_workfile_links(\n        self, workfile_instance, other_instances, new_links_by_type\n    ):\n        if workfile_instance is None:\n            self.log.warn(\"No workfile in this publish session.\")\n            return\n\n        workfile_version_id = workfile_instance.data[\"versionEntity\"][\"_id\"]\n        # link workfile to all publishing versions\n        for instance in other_instances:\n            self.add_link(\n                new_links_by_type,\n                \"generative\",\n                workfile_version_id,\n                instance.data[\"versionEntity\"][\"_id\"],\n            )\n\n        loaded_versions = workfile_instance.context.get(\"loadedVersions\")\n        if not loaded_versions:\n            return\n\n        # link all loaded versions in scene into workfile\n        for version in loaded_versions:\n            self.add_link(\n                new_links_by_type,\n                \"reference\",\n                version[\"version\"],\n                workfile_version_id,\n            )\n\n    def create_generative_links(self, other_instances, new_links_by_type):\n        for instance in other_instances:\n            input_versions = instance.data.get(\"inputVersions\")\n            if not input_versions:\n                continue\n\n            version_entity = instance.data[\"versionEntity\"]\n            for input_version in input_versions:\n                self.add_link(\n                    new_links_by_type,\n                    \"generative\",\n                    input_version,\n                    version_entity[\"_id\"],\n                )\n\n    def _get_existing_links(self, project_name, link_type, entity_ids):\n        \"\"\"Find all existing links for given version ids.\n\n        Args:\n            project_name (str): Name of project.\n            link_type (str): Type of link.\n            entity_ids (set[str]): Set of version ids.\n\n        Returns:\n            dict[str, set[str]]: Existing links by version id.\n        \"\"\"\n\n        output = collections.defaultdict(set)\n        if not entity_ids:\n            return output\n\n        existing_in_links = get_versions_links(\n            project_name, entity_ids, [link_type], \"output\"\n        )\n\n        for entity_id, links in existing_in_links.items():\n            if not links:\n                continue\n            for link in links:\n                output[entity_id].add(link[\"entityId\"])\n        return output\n\n    def create_links_on_server(self, context, new_links):\n        \"\"\"Create new links on server.\n\n        Args:\n            dict[str, list[tuple[str, str]]]: Version links by link type.\n        \"\"\"\n\n        if not new_links:\n            return\n\n        project_name = context.data[\"projectName\"]\n\n        # Make sure link types are available on server\n        for link_type in new_links.keys():\n            make_sure_link_type_exists(\n                project_name, link_type, \"version\", \"version\"\n            )\n\n        # Create link themselves\n        for link_type, items in new_links.items():\n            mapping = collections.defaultdict(set)\n            # Make sure there are no duplicates of src > dst ids\n            for item in items:\n                _input_id, _output_id = item\n                mapping[_input_id].add(_output_id)\n\n            existing_links_by_in_id = self._get_existing_links(\n                project_name, link_type, set(mapping.keys())\n            )\n\n            for input_id, output_ids in mapping.items():\n                existing_links = existing_links_by_in_id[input_id]\n                for output_id in output_ids:\n                    # Skip creation of link if already exists\n                    # NOTE: AYON server does not support\n                    #     to have same links\n                    if output_id in existing_links:\n                        continue\n                    create_link(\n                        project_name,\n                        link_type,\n                        input_id,\n                        \"version\",\n                        output_id,\n                        \"version\"\n                    )\n\n\nif not AYON_SERVER_ENABLED:\n    del IntegrateInputLinksAYON\n"
  },
  {
    "path": "openpype/plugins/publish/integrate_resources_path.py",
    "content": "import os\nimport pyblish.api\n\n\nclass IntegrateResourcesPath(pyblish.api.InstancePlugin):\n    \"\"\"Generate directory path where the files and resources will be stored\"\"\"\n\n    label = \"Integrate Resources Path\"\n    order = pyblish.api.IntegratorOrder - 0.05\n    families = [\"clip\",  \"projectfile\", \"plate\"]\n\n    def process(self, instance):\n        resources = instance.data.get(\"resources\") or []\n        transfers = instance.data.get(\"transfers\") or []\n\n        if not resources and not transfers:\n            self.log.debug(\n                \"Instance does not have `resources` and `transfers`\"\n            )\n            return\n\n        resources_folder = instance.data[\"resourcesDir\"]\n\n        # Define resource destination and transfers\n        for resource in resources:\n            # Add destination to the resource\n            source_filename = os.path.basename(\n                resource[\"source\"]).replace(\"\\\\\", \"/\")\n            destination = os.path.join(resources_folder, source_filename)\n\n            # Force forward slashes to fix issue with software unable\n            # to work correctly with backslashes in specific scenarios\n            # (e.g. escape characters in PLN-151 V-Ray UDIM)\n            destination = destination.replace(\"\\\\\", \"/\")\n\n            resource['destination'] = destination\n\n            # Collect transfers for the individual files of the resource\n            # e.g. all individual files of a cache or UDIM textures.\n            files = resource['files']\n            for fsrc in files:\n                fname = os.path.basename(fsrc)\n                fdest = os.path.join(\n                    resources_folder, fname\n                ).replace(\"\\\\\", \"/\")\n                transfers.append([fsrc, fdest])\n\n        instance.data[\"resources\"] = resources\n        instance.data[\"transfers\"] = transfers\n"
  },
  {
    "path": "openpype/plugins/publish/integrate_subset_group.py",
    "content": "\"\"\"Produces instance.data[\"subsetGroup\"] data used during integration.\n\nRequires:\n    dict -> context[\"anatomyData\"] *(pyblish.api.CollectorOrder + 0.49)\n\nProvides:\n    instance -> subsetGroup (str)\n\n\"\"\"\nimport pyblish.api\n\nfrom openpype.lib.profiles_filtering import filter_profiles\nfrom openpype.lib import (\n    prepare_template_data,\n    StringTemplate,\n    TemplateUnsolved\n)\n\n\nclass IntegrateSubsetGroup(pyblish.api.InstancePlugin):\n    \"\"\"Integrate Subset Group for publish.\"\"\"\n\n    # Run after CollectAnatomyInstanceData\n    order = pyblish.api.IntegratorOrder - 0.1\n    label = \"Subset Group\"\n\n    # Attributes set by settings\n    subset_grouping_profiles = None\n\n    def process(self, instance):\n        \"\"\"Look into subset group profiles set by settings.\n\n        Attribute 'subset_grouping_profiles' is defined by OpenPype settings.\n        \"\"\"\n\n        # Skip if 'subset_grouping_profiles' is empty\n        if not self.subset_grouping_profiles:\n            return\n\n        if instance.data.get(\"subsetGroup\"):\n            # If subsetGroup is already set then allow that value to remain\n            self.log.debug((\n                \"Skipping collect subset group due to existing value: {}\"\n            ).format(instance.data[\"subsetGroup\"]))\n            return\n\n        # Skip if there is no matching profile\n        filter_criteria = self.get_profile_filter_criteria(instance)\n        profile = filter_profiles(\n            self.subset_grouping_profiles,\n            filter_criteria,\n            logger=self.log\n        )\n\n        if not profile:\n            return\n\n        template = profile[\"template\"]\n\n        fill_pairs = prepare_template_data({\n            \"family\": filter_criteria[\"families\"],\n            \"task\": filter_criteria[\"tasks\"],\n            \"host\": filter_criteria[\"hosts\"],\n            \"subset\": instance.data[\"subset\"],\n            \"renderlayer\": instance.data.get(\"renderlayer\")\n        })\n\n        filled_template = None\n        try:\n            filled_template = StringTemplate.format_strict_template(\n                template, fill_pairs\n            )\n        except (KeyError, TemplateUnsolved):\n            keys = fill_pairs.keys()\n            self.log.warning((\n                \"Subset grouping failed. Only {} are expected in Settings\"\n            ).format(','.join(keys)))\n\n        if filled_template:\n            instance.data[\"subsetGroup\"] = filled_template\n\n    def get_profile_filter_criteria(self, instance):\n        \"\"\"Return filter criteria for `filter_profiles`\"\"\"\n        # TODO: This logic is used in much more plug-ins in one way or another\n        #       Maybe better suited for lib?\n        # Anatomy data is pre-filled by Collectors\n        anatomy_data = instance.data[\"anatomyData\"]\n\n        # Task can be optional in anatomy data\n        task = anatomy_data.get(\"task\", {})\n\n        # Return filter criteria\n        return {\n            \"families\": anatomy_data[\"family\"],\n            \"tasks\": task.get(\"name\"),\n            \"hosts\": instance.context.data[\"hostName\"],\n            \"task_types\": task.get(\"type\")\n        }\n"
  },
  {
    "path": "openpype/plugins/publish/integrate_thumbnail.py",
    "content": "\"\"\" Integrate Thumbnails for Openpype use in Loaders.\n\n    This thumbnail is different from 'thumbnail' representation which could\n    be uploaded to Ftrack, or used as any other representation in Loaders to\n    pull into a scene.\n\n    This one is used only as image describing content of published item and\n    shows up only in Loader in right column section.\n\"\"\"\n\nimport os\nimport sys\nimport errno\nimport shutil\nimport copy\nimport collections\n\nimport six\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import get_versions\nfrom openpype.client.operations import OperationsSession, new_thumbnail_doc\nfrom openpype.pipeline.publish import get_publish_instance_label\n\nInstanceFilterResult = collections.namedtuple(\n    \"InstanceFilterResult\",\n    [\"instance\", \"thumbnail_path\", \"version_id\"]\n)\n\n\nclass IntegrateThumbnails(pyblish.api.ContextPlugin):\n    \"\"\"Integrate Thumbnails for Openpype use in Loaders.\"\"\"\n\n    label = \"Integrate Thumbnails\"\n    order = pyblish.api.IntegratorOrder + 0.01\n\n    required_context_keys = [\n        \"project\", \"asset\", \"task\", \"subset\", \"version\"\n    ]\n\n    def process(self, context):\n        if AYON_SERVER_ENABLED:\n            self.log.debug(\n                \"AYON is enabled. Skipping v3 thumbnail integration\"\n            )\n            return\n\n        # Filter instances which can be used for integration\n        filtered_instance_items = self._prepare_instances(context)\n        if not filtered_instance_items:\n            self.log.debug(\n                \"All instances were filtered. Thumbnail integration skipped.\"\n            )\n            return\n\n        # Initial validation of available templated and required keys\n        env_key = \"AVALON_THUMBNAIL_ROOT\"\n        thumbnail_root_format_key = \"{thumbnail_root}\"\n        thumbnail_root = os.environ.get(env_key) or \"\"\n\n        anatomy = context.data[\"anatomy\"]\n        project_name = anatomy.project_name\n        if \"publish\" not in anatomy.templates:\n            self.log.warning(\n                \"Anatomy is missing the \\\"publish\\\" key. Skipping.\"\n            )\n            return\n\n        if \"thumbnail\" not in anatomy.templates[\"publish\"]:\n            self.log.warning((\n                \"There is no \\\"thumbnail\\\" template set for the project\"\n                \" \\\"{}\\\". Skipping.\"\n            ).format(project_name))\n            return\n\n        thumbnail_template = anatomy.templates[\"publish\"][\"thumbnail\"]\n        if not thumbnail_template:\n            self.log.debug(\"Thumbnail template is not filled. Skipping.\")\n            return\n\n        if (\n            not thumbnail_root\n            and thumbnail_root_format_key in thumbnail_template\n        ):\n            self.log.warning(\"{} is not set. Skipping.\".format(env_key))\n            return\n\n        # Collect verion ids from all filtered instance\n        version_ids = {\n            instance_items.version_id\n            for instance_items in filtered_instance_items\n        }\n        # Query versions\n        version_docs = get_versions(\n            project_name,\n            version_ids=version_ids,\n            hero=True,\n            fields=[\"_id\", \"type\", \"name\"]\n        )\n        # Store version by their id (converted to string)\n        version_docs_by_str_id = {\n            str(version_doc[\"_id\"]): version_doc\n            for version_doc in version_docs\n        }\n        self._integrate_thumbnails(\n            filtered_instance_items,\n            version_docs_by_str_id,\n            anatomy,\n            thumbnail_root\n        )\n\n    def _get_thumbnail_from_instance(self, instance):\n        # 1. Look for thumbnail in published representations\n        published_repres = instance.data.get(\"published_representations\")\n        path = self._get_thumbnail_path_from_published(published_repres)\n        if path and os.path.exists(path):\n            return path\n\n        if path:\n            self.log.warning(\n                \"Could not find published thumbnail path {}\".format(path)\n            )\n\n        # 2. Look for thumbnail in \"not published\" representations\n        thumbnail_path = self._get_thumbnail_path_from_unpublished(instance)\n        if thumbnail_path and os.path.exists(thumbnail_path):\n            return thumbnail_path\n\n        # 3. Look for thumbnail path on instance in 'thumbnailPath'\n        thumbnail_path = instance.data.get(\"thumbnailPath\")\n        if thumbnail_path and os.path.exists(thumbnail_path):\n            return thumbnail_path\n        return None\n\n    def _prepare_instances(self, context):\n        context_thumbnail_path = context.data.get(\"thumbnailPath\")\n        valid_context_thumbnail = False\n        if context_thumbnail_path and os.path.exists(context_thumbnail_path):\n            valid_context_thumbnail = True\n\n        filtered_instances = []\n        for instance in context:\n            instance_label = get_publish_instance_label(instance)\n            # Skip instances without published representations\n            # - there is no place where to put the thumbnail\n            published_repres = instance.data.get(\"published_representations\")\n            if not published_repres:\n                self.log.debug((\n                    \"There are no published representations\"\n                    \" on the instance {}.\"\n                ).format(instance_label))\n                continue\n\n            # Find thumbnail path on instance\n            thumbnail_path = self._get_thumbnail_from_instance(instance)\n            if thumbnail_path:\n                self.log.debug((\n                    \"Found thumbnail path for instance \\\"{}\\\".\"\n                    \" Thumbnail path: {}\"\n                ).format(instance_label, thumbnail_path))\n\n            elif valid_context_thumbnail:\n                # Use context thumbnail path if is available\n                thumbnail_path = context_thumbnail_path\n                self.log.debug((\n                    \"Using context thumbnail path for instance \\\"{}\\\".\"\n                    \" Thumbnail path: {}\"\n                ).format(instance_label, thumbnail_path))\n\n            # Skip instance if thumbnail path is not available for it\n            if not thumbnail_path:\n                self.log.debug((\n                    \"Skipping thumbnail integration for instance \\\"{}\\\".\"\n                    \" Instance and context\"\n                    \" thumbnail paths are not available.\"\n                ).format(instance_label))\n                continue\n\n            version_id = str(self._get_version_id(published_repres))\n            filtered_instances.append(\n                InstanceFilterResult(instance, thumbnail_path, version_id)\n            )\n        return filtered_instances\n\n    def _get_version_id(self, published_representations):\n        for repre_info in published_representations.values():\n            return repre_info[\"representation\"][\"parent\"]\n\n    def _get_thumbnail_path_from_published(self, published_representations):\n        if not published_representations:\n            return None\n\n        thumb_repre_doc = None\n        for repre_info in published_representations.values():\n            repre_doc = repre_info[\"representation\"]\n            if repre_doc[\"name\"].lower() == \"thumbnail\":\n                thumb_repre_doc = repre_doc\n                break\n\n        if thumb_repre_doc is None:\n            self.log.debug(\n                \"There is no representation with name \\\"thumbnail\\\"\"\n            )\n            return None\n\n        path = thumb_repre_doc[\"data\"][\"path\"]\n        if not os.path.exists(path):\n            self.log.warning(\n                \"Thumbnail file cannot be found. Path: {}\".format(path)\n            )\n            return None\n        return os.path.normpath(path)\n\n    def _get_thumbnail_path_from_unpublished(self, instance):\n        repres = instance.data.get(\"representations\")\n        if not repres:\n            return None\n\n        thumbnail_repre = next(\n            (\n                repre\n                for repre in repres\n                if repre[\"name\"] == \"thumbnail\"\n            ),\n            None\n        )\n        if not thumbnail_repre:\n            return None\n\n        staging_dir = thumbnail_repre.get(\"stagingDir\")\n        if not staging_dir:\n            staging_dir = instance.data.get(\"stagingDir\")\n\n        filename = thumbnail_repre.get(\"files\")\n        if not staging_dir or not filename:\n            return None\n\n        if isinstance(filename, (list, tuple, set)):\n            filename = filename[0]\n\n        thumbnail_path = os.path.join(staging_dir, filename)\n        if os.path.exists(thumbnail_path):\n            return thumbnail_path\n        return None\n\n    def _integrate_thumbnails(\n        self,\n        filtered_instance_items,\n        version_docs_by_str_id,\n        anatomy,\n        thumbnail_root\n    ):\n        op_session = OperationsSession()\n        project_name = anatomy.project_name\n\n        for instance_item in filtered_instance_items:\n            instance, thumbnail_path, version_id = instance_item\n            instance_label = get_publish_instance_label(instance)\n            version_doc = version_docs_by_str_id.get(version_id)\n            if not version_doc:\n                self.log.warning((\n                    \"Version entity for instance \\\"{}\\\" was not found.\"\n                ).format(instance_label))\n                continue\n\n            filename, file_extension = os.path.splitext(thumbnail_path)\n            # Create id for mongo entity now to fill anatomy template\n            thumbnail_doc = new_thumbnail_doc()\n            thumbnail_id = thumbnail_doc[\"_id\"]\n\n            # Prepare anatomy template fill data\n            template_data = copy.deepcopy(instance.data[\"anatomyData\"])\n            template_data.update({\n                \"_id\": str(thumbnail_id),\n                \"ext\": file_extension[1:],\n                \"name\": \"thumbnail\",\n                \"thumbnail_root\": thumbnail_root,\n                \"thumbnail_type\": \"thumbnail\"\n            })\n\n            template_obj = anatomy.templates_obj[\"publish\"][\"thumbnail\"]\n            template_filled = template_obj.format_strict(template_data)\n            thumbnail_template = template_filled.template\n\n            dst_full_path = os.path.normpath(str(template_filled))\n            self.log.debug(\"Copying file .. {} -> {}\".format(\n                thumbnail_path, dst_full_path\n            ))\n            dirname = os.path.dirname(dst_full_path)\n            try:\n                os.makedirs(dirname)\n            except OSError as e:\n                if e.errno != errno.EEXIST:\n                    tp, value, tb = sys.exc_info()\n                    six.reraise(tp, value, tb)\n\n            shutil.copy(thumbnail_path, dst_full_path)\n\n            # Clean template data from keys that are dynamic\n            for key in (\"_id\", \"thumbnail_root\"):\n                template_data.pop(key, None)\n\n            repre_context = template_filled.used_values\n            for key in self.required_context_keys:\n                value = template_data.get(key)\n                if not value:\n                    continue\n                repre_context[key] = template_data[key]\n\n            thumbnail_doc[\"data\"] = {\n                \"template\": thumbnail_template,\n                \"template_data\": repre_context\n            }\n            op_session.create_entity(\n                project_name, thumbnail_doc[\"type\"], thumbnail_doc\n            )\n            # Create thumbnail entity\n            self.log.debug(\n                \"Creating entity in database {}\".format(str(thumbnail_doc))\n            )\n\n            # Set thumbnail id for version\n            op_session.update_entity(\n                project_name,\n                version_doc[\"type\"],\n                version_doc[\"_id\"],\n                {\"data.thumbnail_id\": thumbnail_id}\n            )\n            if version_doc[\"type\"] == \"hero_version\":\n                version_name = \"Hero\"\n            else:\n                version_name = version_doc[\"name\"]\n            self.log.debug(\"Setting thumbnail for version \\\"{}\\\" <{}>\".format(\n                version_name, version_id\n            ))\n\n            asset_entity = instance.data[\"assetEntity\"]\n            op_session.update_entity(\n                project_name,\n                asset_entity[\"type\"],\n                asset_entity[\"_id\"],\n                {\"data.thumbnail_id\": thumbnail_id}\n            )\n            self.log.debug(\"Setting thumbnail for asset \\\"{}\\\" <{}>\".format(\n                asset_entity[\"name\"], version_id\n            ))\n\n        op_session.commit()\n"
  },
  {
    "path": "openpype/plugins/publish/integrate_thumbnail_ayon.py",
    "content": "\"\"\" Integrate Thumbnails for Openpype use in Loaders.\n\n    This thumbnail is different from 'thumbnail' representation which could\n    be uploaded to Ftrack, or used as any other representation in Loaders to\n    pull into a scene.\n\n    This one is used only as image describing content of published item and\n        shows up only in Loader or WebUI.\n\n    Instance must have 'published_representations' to\n        be able to integrate thumbnail.\n    Possible sources of thumbnail paths:\n    - instance.data[\"thumbnailPath\"]\n    - representation with 'thumbnail' name in 'published_representations'\n    - context.data[\"thumbnailPath\"]\n\n    Notes:\n        Issue with 'thumbnail' representation is that we most likely don't\n            want to integrate it as representation. Integrated representation\n            is polluting Loader and database without real usage. That's why\n            they usually have 'delete' tag to skip the integration.\n\n\"\"\"\n\nimport os\nimport collections\n\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import get_versions\nfrom openpype.client.operations import OperationsSession\n\nInstanceFilterResult = collections.namedtuple(\n    \"InstanceFilterResult\",\n    [\"instance\", \"thumbnail_path\", \"version_id\"]\n)\n\n\nclass IntegrateThumbnailsAYON(pyblish.api.ContextPlugin):\n    \"\"\"Integrate Thumbnails for Openpype use in Loaders.\"\"\"\n\n    label = \"Integrate Thumbnails to AYON\"\n    order = pyblish.api.IntegratorOrder + 0.01\n\n    required_context_keys = [\n        \"project\", \"asset\", \"task\", \"subset\", \"version\"\n    ]\n\n    def process(self, context):\n        if not AYON_SERVER_ENABLED:\n            self.log.debug(\"AYON is not enabled. Skipping\")\n            return\n\n        # Filter instances which can be used for integration\n        filtered_instance_items = self._prepare_instances(context)\n        if not filtered_instance_items:\n            self.log.debug(\n                \"All instances were filtered. Thumbnail integration skipped.\"\n            )\n            return\n\n        project_name = context.data[\"projectName\"]\n\n        # Collect version ids from all filtered instance\n        version_ids = {\n            instance_items.version_id\n            for instance_items in filtered_instance_items\n        }\n        # Query versions\n        version_docs = get_versions(\n            project_name,\n            version_ids=version_ids,\n            hero=True,\n            fields=[\"_id\", \"type\", \"name\"]\n        )\n        # Store version by their id (converted to string)\n        version_docs_by_str_id = {\n            str(version_doc[\"_id\"]): version_doc\n            for version_doc in version_docs\n        }\n        self._integrate_thumbnails(\n            filtered_instance_items,\n            version_docs_by_str_id,\n            project_name\n        )\n\n    def _prepare_instances(self, context):\n        context_thumbnail_path = context.data.get(\"thumbnailPath\")\n        valid_context_thumbnail = bool(\n            context_thumbnail_path\n            and os.path.exists(context_thumbnail_path)\n        )\n\n        filtered_instances = []\n        for instance in context:\n            instance_label = self._get_instance_label(instance)\n            # Skip instances without published representations\n            # - there is no place where to put the thumbnail\n            published_repres = instance.data.get(\"published_representations\")\n            if not published_repres:\n                self.log.debug((\n                    \"There are no published representations\"\n                    \" on the instance {}.\"\n                ).format(instance_label))\n                continue\n\n            # Find thumbnail path on instance\n            thumbnail_path = (\n                instance.data.get(\"thumbnailPath\")\n                or self._get_instance_thumbnail_path(published_repres)\n            )\n            if thumbnail_path:\n                self.log.debug((\n                    \"Found thumbnail path for instance \\\"{}\\\".\"\n                    \" Thumbnail path: {}\"\n                ).format(instance_label, thumbnail_path))\n\n            elif valid_context_thumbnail:\n                # Use context thumbnail path if is available\n                thumbnail_path = context_thumbnail_path\n                self.log.debug((\n                    \"Using context thumbnail path for instance \\\"{}\\\".\"\n                    \" Thumbnail path: {}\"\n                ).format(instance_label, thumbnail_path))\n\n            # Skip instance if thumbnail path is not available for it\n            if not thumbnail_path:\n                self.log.debug((\n                    \"Skipping thumbnail integration for instance \\\"{}\\\".\"\n                    \" Instance and context\"\n                    \" thumbnail paths are not available.\"\n                ).format(instance_label))\n                continue\n\n            version_id = str(self._get_version_id(published_repres))\n            filtered_instances.append(\n                InstanceFilterResult(instance, thumbnail_path, version_id)\n            )\n        return filtered_instances\n\n    def _get_version_id(self, published_representations):\n        for repre_info in published_representations.values():\n            return repre_info[\"representation\"][\"parent\"]\n\n    def _get_instance_thumbnail_path(self, published_representations):\n        thumb_repre_doc = None\n        for repre_info in published_representations.values():\n            repre_doc = repre_info[\"representation\"]\n            if \"thumbnail\" in repre_doc[\"name\"].lower():\n                thumb_repre_doc = repre_doc\n                break\n\n        if thumb_repre_doc is None:\n            self.log.debug(\n                \"There is no representation with name \\\"thumbnail\\\"\"\n            )\n            return None\n\n        path = thumb_repre_doc[\"data\"][\"path\"]\n        if not os.path.exists(path):\n            self.log.warning(\n                \"Thumbnail file cannot be found. Path: {}\".format(path)\n            )\n            return None\n        return os.path.normpath(path)\n\n    def _integrate_thumbnails(\n        self,\n        filtered_instance_items,\n        version_docs_by_str_id,\n        project_name\n    ):\n        from openpype.client.server.operations import create_thumbnail\n\n        # Make sure each entity id has defined only one thumbnail id\n        thumbnail_info_by_entity_id = {}\n        for instance_item in filtered_instance_items:\n            instance, thumbnail_path, version_id = instance_item\n            instance_label = self._get_instance_label(instance)\n            version_doc = version_docs_by_str_id.get(version_id)\n            if not version_doc:\n                self.log.warning((\n                    \"Version entity for instance \\\"{}\\\" was not found.\"\n                ).format(instance_label))\n                continue\n\n            thumbnail_id = create_thumbnail(project_name, thumbnail_path)\n\n            # Set thumbnail id for version\n            thumbnail_info_by_entity_id[version_id] = {\n                \"thumbnail_id\": thumbnail_id,\n                \"entity_type\": version_doc[\"type\"],\n            }\n            if version_doc[\"type\"] == \"hero_version\":\n                version_name = \"Hero\"\n            else:\n                version_name = version_doc[\"name\"]\n            self.log.debug(\"Setting thumbnail for version \\\"{}\\\" <{}>\".format(\n                version_name, version_id\n            ))\n\n            asset_entity = instance.data[\"assetEntity\"]\n            thumbnail_info_by_entity_id[asset_entity[\"_id\"]] = {\n                \"thumbnail_id\": thumbnail_id,\n                \"entity_type\": \"asset\",\n            }\n            self.log.debug(\"Setting thumbnail for asset \\\"{}\\\" <{}>\".format(\n                asset_entity[\"name\"], version_id\n            ))\n\n        op_session = OperationsSession()\n        for entity_id, thumbnail_info in thumbnail_info_by_entity_id.items():\n            thumbnail_id = thumbnail_info[\"thumbnail_id\"]\n            op_session.update_entity(\n                project_name,\n                thumbnail_info[\"entity_type\"],\n                entity_id,\n                {\"data.thumbnail_id\": thumbnail_id}\n            )\n        op_session.commit()\n\n    def _get_instance_label(self, instance):\n        return (\n            instance.data.get(\"label\")\n            or instance.data.get(\"name\")\n            or \"N/A\"\n        )\n"
  },
  {
    "path": "openpype/plugins/publish/integrate_version_attrs.py",
    "content": "import pyblish.api\nimport ayon_api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client.operations import OperationsSession\n\n\nclass IntegrateVersionAttributes(pyblish.api.ContextPlugin):\n    \"\"\"Integrate version attributes from predefined key.\n\n    Any integration after 'IntegrateAsset' can fill 'versionAttributes' with\n    attribute key & value to be updated on created version.\n\n    The integration must make sure the attribute is available for the version\n    entity otherwise an error would be raised.\n\n    Example of 'versionAttributes':\n        {\n            \"ftrack_id\": \"0123456789-101112-131415\",\n            \"syncsketch_id\": \"987654321-012345-678910\"\n        }\n    \"\"\"\n\n    label = \"Integrate Version Attributes\"\n    order = pyblish.api.IntegratorOrder + 0.5\n\n    def process(self, context):\n        available_attributes = ayon_api.get_attributes_for_type(\"version\")\n        skipped_attributes = set()\n        project_name = context.data[\"projectName\"]\n        op_session = OperationsSession()\n        for instance in context:\n            label = self.get_instance_label(instance)\n            version_entity = instance.data.get(\"versionEntity\")\n            if not version_entity:\n                continue\n            attributes = instance.data.get(\"versionAttributes\")\n            if not attributes:\n                self.log.debug((\n                    \"Skipping instance {} because it does not specify\"\n                    \" version attributes to set.\"\n                ).format(label))\n                continue\n\n            filtered_attributes = {}\n            for attr, value in attributes.items():\n                if attr not in available_attributes:\n                    skipped_attributes.add(attr)\n                else:\n                    filtered_attributes[attr] = value\n\n            if not filtered_attributes:\n                self.log.debug((\n                    \"Skipping instance {} because all version attributes were\"\n                    \" filtered out.\"\n                ).format(label))\n                continue\n\n            self.log.debug(\"Updating attributes on version {} to {}\".format(\n                version_entity[\"_id\"], str(filtered_attributes)\n            ))\n            op_session.update_entity(\n                project_name,\n                \"version\",\n                version_entity[\"_id\"],\n                {\"attrib\": filtered_attributes}\n            )\n\n        if skipped_attributes:\n            self.log.warning((\n                \"Skipped version attributes integration because they're\"\n                \" not available on the server: {}\"\n            ).format(str(skipped_attributes)))\n\n        if len(op_session):\n            op_session.commit()\n            self.log.info(\"Updated version attributes\")\n        else:\n            self.log.debug(\"There are no version attributes to update\")\n\n    @staticmethod\n    def get_instance_label(instance):\n        return (\n            instance.data.get(\"label\")\n            or instance.data.get(\"name\")\n            or instance.data.get(\"subset\")\n            or str(instance)\n        )\n\n\n# Discover the plugin only in AYON mode\nif not AYON_SERVER_ENABLED:\n    del IntegrateVersionAttributes\n"
  },
  {
    "path": "openpype/plugins/publish/preintegrate_thumbnail_representation.py",
    "content": "\"\"\" Marks thumbnail representation for integrate to DB or not.\n\n    Some hosts produce thumbnail representation, most of them do not create\n    them explicitly, but they created during extract phase.\n\n    In some cases it might be useful to override implicit setting for host/task\n\n    This plugin needs to run after extract phase, but before integrate.py as\n    thumbnail is part of review family and integrated there.\n\n    It should be better to control integration of thumbnail in one place than\n    configure it in multiple places on host implementations.\n\"\"\"\nimport pyblish.api\n\nfrom openpype.lib.profiles_filtering import filter_profiles\n\n\nclass PreIntegrateThumbnails(pyblish.api.InstancePlugin):\n    \"\"\"Marks thumbnail representation for integrate to DB or not.\"\"\"\n\n    label = \"Override Integrate Thumbnail Representations\"\n    order = pyblish.api.IntegratorOrder - 0.1\n\n    integrate_profiles = []\n\n    def process(self, instance):\n        repres = instance.data.get(\"representations\")\n        if not repres:\n            return\n\n        thumbnail_repres = []\n        for repre in repres:\n            if \"thumbnail\" in repre.get(\"tags\", []):\n                thumbnail_repres.append(repre)\n\n        if not thumbnail_repres:\n            return\n\n        family = instance.data[\"family\"]\n        subset_name = instance.data[\"subset\"]\n        host_name = instance.context.data[\"hostName\"]\n\n        anatomy_data = instance.data[\"anatomyData\"]\n        task = anatomy_data.get(\"task\", {})\n\n        found_profile = filter_profiles(\n            self.integrate_profiles,\n            {\n                \"hosts\": host_name,\n                \"task_names\": task.get(\"name\"),\n                \"task_types\": task.get(\"type\"),\n                \"families\": family,\n                \"subsets\": subset_name,\n            },\n            logger=self.log\n        )\n\n        if not found_profile:\n            return\n\n        for thumbnail_repre in thumbnail_repres:\n            thumbnail_repre.setdefault(\"tags\", [])\n\n            if not found_profile[\"integrate_thumbnail\"]:\n                if \"delete\" not in thumbnail_repre[\"tags\"]:\n                    thumbnail_repre[\"tags\"].append(\"delete\")\n            else:\n                if \"delete\" in thumbnail_repre[\"tags\"]:\n                    thumbnail_repre[\"tags\"].remove(\"delete\")\n\n            self.log.debug(\n                \"Thumbnail repre tags {}\".format(thumbnail_repre[\"tags\"]))\n"
  },
  {
    "path": "openpype/plugins/publish/repair_unicode_strings.py",
    "content": "import os\nimport pyblish.api\n\n\nclass RepairUnicodeStrings(pyblish.api.Collector):\n    \"\"\"Validate all environment variables are string type.\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder\n    label = 'Unicode Strings'\n\n    def process(self, context):\n        for key, value in os.environ.items():\n            os.environ[str(key)] = str(value)\n"
  },
  {
    "path": "openpype/plugins/publish/validate_asset_docs.py",
    "content": "import pyblish.api\nfrom openpype.pipeline import PublishValidationError\n\n\nclass ValidateAssetDocs(pyblish.api.InstancePlugin):\n    \"\"\"Validate existence of asset documents on instances.\n\n    Without asset document it is not possible to publish the instance.\n\n    If context has set asset document the validation is skipped.\n\n    Plugin was added because there are cases when context asset is not defined\n    e.g. in tray publisher.\n    \"\"\"\n\n    label = \"Validate Asset docs\"\n    order = pyblish.api.ValidatorOrder\n\n    def process(self, instance):\n        context_asset_doc = instance.context.data.get(\"assetEntity\")\n        if context_asset_doc:\n            return\n\n        if instance.data.get(\"assetEntity\"):\n            self.log.debug(\"Instance has set asset document in its data.\")\n\n        elif instance.data.get(\"newAssetPublishing\"):\n            # skip if it is editorial\n            self.log.debug(\"Editorial instance has no need to check...\")\n\n        else:\n            raise PublishValidationError((\n                \"Instance \\\"{}\\\" doesn't have asset document \"\n                \"set which is needed for publishing.\"\n            ).format(instance.data[\"name\"]))\n"
  },
  {
    "path": "openpype/plugins/publish/validate_containers.py",
    "content": "import pyblish.api\nfrom openpype.pipeline.load import any_outdated_containers\nfrom openpype.pipeline import (\n    PublishXmlValidationError,\n    OptionalPyblishPluginMixin\n)\n\n\nclass ShowInventory(pyblish.api.Action):\n\n    label = \"Show Inventory\"\n    icon = \"briefcase\"\n    on = \"failed\"\n\n    def process(self, context, plugin):\n        from openpype.tools.utils import host_tools\n\n        host_tools.show_scene_inventory()\n\n\nclass ValidateContainers(OptionalPyblishPluginMixin,\n                         pyblish.api.ContextPlugin):\n\n    \"\"\"Containers are must be updated to latest version on publish.\"\"\"\n\n    label = \"Validate Outdated Containers\"\n    order = pyblish.api.ValidatorOrder\n    hosts = [\"maya\", \"houdini\", \"nuke\", \"harmony\", \"photoshop\", \"aftereffects\"]\n    optional = True\n    actions = [ShowInventory]\n\n    def process(self, context):\n        if not self.is_active(context.data):\n            return\n\n        if any_outdated_containers():\n            msg = \"There are outdated containers in the scene.\"\n            raise PublishXmlValidationError(self, msg)\n"
  },
  {
    "path": "openpype/plugins/publish/validate_editorial_asset_name.py",
    "content": "from pprint import pformat\n\nimport pyblish.api\n\nfrom openpype.client import get_assets, get_asset_name_identifier\n\n\nclass ValidateEditorialAssetName(pyblish.api.ContextPlugin):\n    \"\"\" Validating if editorial's asset names are not already created in db.\n\n    Checking variations of names with different size of caps or with\n    or without underscores.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    label = \"Validate Editorial Asset Name\"\n    hosts = [\n        \"hiero\",\n        \"standalonepublisher\",\n        \"resolve\",\n        \"flame\",\n        \"traypublisher\"\n    ]\n\n    def process(self, context):\n\n        asset_and_parents = self.get_parents(context)\n        self.log.debug(\"__ asset_and_parents: {}\".format(asset_and_parents))\n\n        project_name = context.data[\"projectName\"]\n        db_assets = list(get_assets(\n            project_name, fields=[\"name\", \"data.parents\"]\n        ))\n        self.log.debug(\"__ db_assets: {}\".format(db_assets))\n\n        asset_db_docs = {\n            get_asset_name_identifier(asset_doc): list(\n                asset_doc[\"data\"][\"parents\"]\n            )\n            for asset_doc in db_assets\n        }\n\n        self.log.debug(\"__ project_entities: {}\".format(\n            pformat(asset_db_docs)))\n\n        assets_missing_name = {}\n        assets_wrong_parent = {}\n        for asset in asset_and_parents.keys():\n            if asset not in asset_db_docs.keys():\n                # add to some nonexistent list for next layer of check\n                assets_missing_name[asset] = asset_and_parents[asset]\n                continue\n\n            if asset_and_parents[asset] != asset_db_docs[asset]:\n                # add to some nonexistent list for next layer of check\n                assets_wrong_parent[asset] = {\n                    \"required\": asset_and_parents[asset],\n                    \"already_in_db\": asset_db_docs[asset]\n                }\n                continue\n\n            self.log.debug(\"correct asset: {}\".format(asset))\n\n        if assets_missing_name:\n            wrong_names = {}\n            self.log.debug(\n                \">> assets_missing_name: {}\".format(assets_missing_name))\n\n            # This will create set asset names\n            asset_names = {\n                a.lower().replace(\"_\", \"\") for a in asset_db_docs\n            }\n\n            for asset in assets_missing_name:\n                _asset = asset.lower().replace(\"_\", \"\")\n                if _asset in asset_names:\n                    wrong_names[asset].update(\n                        {\n                            \"required_name\": asset,\n                            \"used_variants_in_db\": [\n                                a for a in asset_db_docs\n                                if a.lower().replace(\"_\", \"\") == _asset\n                            ]\n                        }\n                    )\n\n            if wrong_names:\n                self.log.debug(\n                    \">> wrong_names: {}\".format(wrong_names))\n                raise Exception(\n                    \"Some already existing asset name variants `{}`\".format(\n                        wrong_names))\n\n        if assets_wrong_parent:\n            self.log.debug(\n                \">> assets_wrong_parent: {}\".format(assets_wrong_parent))\n            raise Exception(\n                \"Wrong parents on assets `{}`\".format(assets_wrong_parent))\n\n    def _get_all_assets(self, input_dict):\n        \"\"\" Returns asset names in list.\n\n            List contains all asset names including parents\n        \"\"\"\n        for key in input_dict.keys():\n            # check if child key is available\n            if input_dict[key].get(\"childs\"):\n                # loop deeper\n                self._get_all_assets(\n                    input_dict[key][\"childs\"])\n            else:\n                self.all_testing_assets.append(key)\n\n    def get_parents(self, context):\n        return_dict = {}\n        for instance in context:\n            asset = instance.data[\"asset\"]\n            families = instance.data.get(\"families\", []) + [\n                instance.data[\"family\"]\n            ]\n            # filter out non-shot families\n            if \"shot\" not in families:\n                continue\n\n            parents = instance.data[\"parents\"]\n\n            return_dict[asset] = [\n                str(p[\"entity_name\"]) for p in parents\n                if p[\"entity_type\"].lower() != \"project\"\n            ]\n        return return_dict\n"
  },
  {
    "path": "openpype/plugins/publish/validate_file_saved.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import PublishValidationError\n\n\nclass ValidateCurrentSaveFile(pyblish.api.ContextPlugin):\n    \"\"\"File must be saved before publishing\"\"\"\n\n    label = \"Validate File Saved\"\n    order = pyblish.api.ValidatorOrder - 0.1\n    hosts = [\"maya\", \"houdini\", \"nuke\"]\n\n    def process(self, context):\n\n        current_file = context.data[\"currentFile\"]\n        if not current_file:\n            raise PublishValidationError(\"File not saved\")\n"
  },
  {
    "path": "openpype/plugins/publish/validate_filesequences.py",
    "content": "import pyblish.api\n\nfrom openpype.pipeline.publish import PublishValidationError\n\n\nclass ValidateFileSequences(pyblish.api.ContextPlugin):\n    \"\"\"Validates whether any file sequences were collected.\"\"\"\n\n    order = pyblish.api.ValidatorOrder\n    # Keep \"filesequence\" for backwards compatibility of older jobs\n    targets = [\"filesequence\", \"farm\"]\n    label = \"Validate File Sequences\"\n\n    def process(self, context):\n        if not context:\n            raise PublishValidationError(\"Nothing collected.\")\n"
  },
  {
    "path": "openpype/plugins/publish/validate_intent.py",
    "content": "import pyblish.api\n\nfrom openpype.lib import filter_profiles\nfrom openpype.pipeline.publish import PublishValidationError\n\n\nclass ValidateIntent(pyblish.api.ContextPlugin):\n    \"\"\"Validate intent of the publish.\n\n    It is required to fill the intent of this publish. Chech the log\n    for more details\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n\n    label = \"Validate Intent\"\n    enabled = False\n\n    # Can be modified by settings\n    profiles = [{\n        \"hosts\": [],\n        \"task_types\": [],\n        \"tasks\": [],\n        \"validate\": False\n    }]\n\n    def process(self, context):\n        # Skip if there are no profiles\n        validate = True\n        if self.profiles:\n            # Collect data from context\n            task_name = context.data.get(\"task\")\n            task_type = context.data.get(\"taskType\")\n            host_name = context.data.get(\"hostName\")\n\n            filter_data = {\n                \"hosts\": host_name,\n                \"task_types\": task_type,\n                \"tasks\": task_name\n            }\n            matching_profile = filter_profiles(\n                self.profiles, filter_data, logger=self.log\n            )\n            if matching_profile:\n                validate = matching_profile[\"validate\"]\n\n        if not validate:\n            self.log.debug((\n                \"Validation of intent was skipped.\"\n                \" Matching profile for current context disabled validation.\"\n            ))\n            return\n\n        intent = context.data.get(\"intent\") or {}\n        self.log.debug(str(intent))\n        intent_value = intent.get(\"value\")\n        if not intent_value:\n            raise PublishValidationError(\n                \"Please make sure that you select the intent of this publish.\"\n            )\n"
  },
  {
    "path": "openpype/plugins/publish/validate_publish_dir.py",
    "content": "import pyblish.api\nfrom openpype.pipeline.publish import ValidateContentsOrder\nfrom openpype.pipeline.publish import (\n    PublishXmlValidationError,\n    get_publish_template_name,\n)\n\n\nclass ValidatePublishDir(pyblish.api.InstancePlugin):\n    \"\"\"Validates if files are being published into a project directory\n\n    In specific cases ('source' template - in place publishing) source folder\n    of published items is used as a regular `publish` dir.\n    This validates if it is inside any project dir for the project.\n    (eg. files are not published from local folder, inaccessible for studio')\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Validate publish dir\"\n\n    checked_template_names = [\"source\"]\n    # validate instances might have interim family, needs to be mapped to final\n    family_mapping = {\n        \"renderLayer\": \"render\",\n        \"renderLocal\": \"render\"\n    }\n\n    def process(self, instance):\n\n        template_name = self._get_template_name_from_instance(instance)\n\n        if template_name not in self.checked_template_names:\n            return\n\n        original_dirname = instance.data.get(\"originalDirname\")\n        if not original_dirname:\n            raise PublishXmlValidationError(\n                self,\n                \"Instance meant for in place publishing.\"\n                \" Its 'originalDirname' must be collected.\"\n                \" Contact OP developer to modify collector.\"\n            )\n\n        anatomy = instance.context.data[\"anatomy\"]\n\n        # original_dirname must be convertable to rootless path\n        # in other case it is path inside of root folder for the project\n        success, _ = anatomy.find_root_template_from_path(original_dirname)\n        if not success:\n            raise PublishXmlValidationError(\n                plugin=self,\n                message=(\n                    \"Path '{}' not in project folder. Please publish from \"\n                    \"inside of project folder.\".format(original_dirname)\n                ),\n                key=\"not_in_dir\",\n                formatting_data={\"original_dirname\": original_dirname}\n            )\n\n    def _get_template_name_from_instance(self, instance):\n        \"\"\"Find template which will be used during integration.\"\"\"\n        project_name = instance.context.data[\"projectName\"]\n        host_name = instance.context.data[\"hostName\"]\n        anatomy_data = instance.data[\"anatomyData\"]\n        family = anatomy_data[\"family\"]\n        family = self.family_mapping.get(family) or family\n        task_info = anatomy_data.get(\"task\") or {}\n\n        return get_publish_template_name(\n            project_name,\n            host_name,\n            family,\n            task_name=task_info.get(\"name\"),\n            task_type=task_info.get(\"type\"),\n            project_settings=instance.context.data[\"project_settings\"],\n            logger=self.log\n        )\n"
  },
  {
    "path": "openpype/plugins/publish/validate_resources.py",
    "content": "import os\nimport pyblish.api\nfrom openpype.pipeline.publish import ValidateContentsOrder\n\n\nclass ValidateResources(pyblish.api.InstancePlugin):\n    \"\"\"Validates mapped resources.\n\n    These are external files to the current application, for example\n    these could be textures, image planes, cache files or other linked\n    media.\n\n    This validates:\n        - The resources are existing files.\n        - The resources have correctly collected the data.\n\n    \"\"\"\n\n    order = ValidateContentsOrder\n    label = \"Validate Resources\"\n\n    def process(self, instance):\n\n        for resource in instance.data.get('resources', []):\n            # Required data\n            assert \"source\" in resource, \"No source found\"\n            assert \"files\" in resource, \"No files from source\"\n            assert all(os.path.exists(f) for f in resource['files'])\n"
  },
  {
    "path": "openpype/plugins/publish/validate_unique_subsets.py",
    "content": "from collections import defaultdict\nimport pyblish.api\nfrom openpype.pipeline.publish import (\n    PublishXmlValidationError,\n)\n\n\nclass ValidateSubsetUniqueness(pyblish.api.ContextPlugin):\n    \"\"\"Validate all subset names are unique.\n\n    This only validates whether the instances currently set to publish from\n    the workfile overlap one another for the asset + subset they are publishing\n    to.\n\n    This does not perform any check against existing publishes in the database\n    since it is allowed to publish into existing subsets resulting in\n    versioning.\n\n    A subset may appear twice to publish from the workfile if one\n    of them is set to publish to another asset than the other.\n\n    \"\"\"\n\n    label = \"Validate Subset Uniqueness\"\n    order = pyblish.api.ValidatorOrder\n    families = [\"*\"]\n\n    def process(self, context):\n\n        # Find instance per (asset,subset)\n        instance_per_asset_subset = defaultdict(list)\n        for instance in context:\n\n            # Ignore disabled instances\n            if not instance.data.get('publish', True):\n                continue\n\n            # Ignore instance without asset data\n            asset = instance.data.get(\"asset\")\n            if asset is None:\n                self.log.warning(\"Instance found without `asset` data: \"\n                                 \"{}\".format(instance.name))\n                continue\n\n            # Ignore instance without subset data\n            subset = instance.data.get(\"subset\")\n            if subset is None:\n                self.log.warning(\"Instance found without `subset` data: \"\n                                 \"{}\".format(instance.name))\n                continue\n\n            instance_per_asset_subset[(asset, subset)].append(instance)\n\n        non_unique = []\n        for (asset, subset), instances in instance_per_asset_subset.items():\n\n            # A single instance per asset, subset is fine\n            if len(instances) < 2:\n                continue\n\n            non_unique.append(\"{asset} > {subset}\".format(asset=asset,\n                                                          subset=subset))\n\n        if not non_unique:\n            # All is ok\n            return\n\n        msg = (\"Instance subset names {} are not unique. \".format(non_unique) +\n               \"Please remove or rename duplicates.\")\n        formatting_data = {\n            \"non_unique\": \",\".join(non_unique)\n        }\n\n        if non_unique:\n            raise PublishXmlValidationError(self, msg,\n                                            formatting_data=formatting_data)\n"
  },
  {
    "path": "openpype/plugins/publish/validate_version.py",
    "content": "import pyblish.api\nfrom openpype.pipeline.publish import PublishValidationError\n\n\nclass ValidateVersion(pyblish.api.InstancePlugin):\n    \"\"\"Validate instance version.\n\n    OpenPype does not allow overwriting previously published versions.\n    \"\"\"\n\n    order = pyblish.api.ValidatorOrder\n\n    label = \"Validate Version\"\n    hosts = [\"nuke\", \"maya\", \"houdini\", \"blender\", \"standalonepublisher\",\n             \"photoshop\", \"aftereffects\"]\n\n    optional = False\n    active = True\n\n    def process(self, instance):\n        version = instance.data.get(\"version\")\n        latest_version = instance.data.get(\"latestVersion\")\n\n        if latest_version is not None and int(version) <= int(latest_version):\n            # TODO: Remove full non-html version upon drop of old publisher\n            msg = (\n                \"Version '{0}' from instance '{1}' that you are \"\n                \"trying to publish is lower or equal to an existing version \"\n                \"in the database. Version in database: '{2}'.\"\n                \"Please version up your workfile to a higher version number \"\n                \"than: '{2}'.\"\n            ).format(version, instance.data[\"name\"], latest_version)\n\n            msg_html = (\n                \"Version <b>{0}</b> from instance <b>{1}</b> that you are \"\n                \"trying to publish is lower or equal to an existing version \"\n                \"in the database. Version in database: <b>{2}</b>.<br><br>\"\n                \"Please version up your workfile to a higher version number \"\n                \"than: <b>{2}</b>.\"\n            ).format(version, instance.data[\"name\"], latest_version)\n            raise PublishValidationError(\n                title=\"Higher version of publish already exists\",\n                message=msg,\n                description=msg_html\n            )\n"
  },
  {
    "path": "openpype/pype_commands.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Implementation of Pype commands.\"\"\"\nimport os\nimport sys\nimport json\nimport time\nimport signal\n\n\nclass PypeCommands:\n    \"\"\"Class implementing commands used by Pype.\n\n    Most of its methods are called by :mod:`cli` module.\n    \"\"\"\n    @staticmethod\n    def launch_tray():\n        from openpype.lib import Logger\n        from openpype.tools import tray\n\n        Logger.set_process_name(\"Tray\")\n\n        tray.main()\n\n    @staticmethod\n    def launch_settings_gui(dev):\n        from openpype.tools import settings\n\n        # TODO change argument options to allow enum of user roles\n        if dev:\n            user_role = \"developer\"\n        else:\n            user_role = \"manager\"\n        settings.main(user_role)\n\n    @staticmethod\n    def add_modules(click_func):\n        \"\"\"Modules/Addons can add their cli commands dynamically.\"\"\"\n\n        from openpype.lib import Logger\n        from openpype.modules import ModulesManager\n\n        manager = ModulesManager()\n        log = Logger.get_logger(\"CLI-AddModules\")\n        for module in manager.modules:\n            try:\n                module.cli(click_func)\n\n            except Exception:\n                log.warning(\n                    \"Failed to add cli command for module \\\"{}\\\"\".format(\n                        module.name\n                    )\n                )\n        return click_func\n\n    @staticmethod\n    def launch_eventservercli(*args):\n        from openpype_modules.ftrack.ftrack_server.event_server_cli import (\n            run_event_server\n        )\n        return run_event_server(*args)\n\n    @staticmethod\n    def launch_webpublisher_webservercli(*args, **kwargs):\n        from openpype.hosts.webpublisher.webserver_service import run_webserver\n\n        return run_webserver(*args, **kwargs)\n\n    @staticmethod\n    def launch_traypublisher():\n        from openpype.tools import traypublisher\n        traypublisher.main()\n\n    @staticmethod\n    def publish(paths, targets=None, gui=False):\n        \"\"\"Start headless publishing.\n\n        Publish use json from passed paths argument.\n\n        Args:\n            paths (list): Paths to jsons.\n            targets (string): What module should be targeted\n                (to choose validator for example)\n            gui (bool): Show publish UI.\n\n        Raises:\n            RuntimeError: When there is no path to process.\n        \"\"\"\n\n        from openpype.lib import Logger\n        from openpype.lib.applications import (\n            get_app_environments_for_context,\n            LaunchTypes,\n        )\n        from openpype.modules import ModulesManager\n        from openpype.pipeline import (\n            install_openpype_plugins,\n            get_global_context,\n        )\n\n        # Register target and host\n        import pyblish.api\n        import pyblish.util\n\n        log = Logger.get_logger(\"CLI-publish\")\n\n        install_openpype_plugins()\n\n        manager = ModulesManager()\n\n        publish_paths = manager.collect_plugin_paths()[\"publish\"]\n\n        for path in publish_paths:\n            pyblish.api.register_plugin_path(path)\n\n        if not any(paths):\n            raise RuntimeError(\"No publish paths specified\")\n\n        app_full_name = os.getenv(\"AVALON_APP_NAME\")\n        if app_full_name:\n            context = get_global_context()\n            env = get_app_environments_for_context(\n                context[\"project_name\"],\n                context[\"asset_name\"],\n                context[\"task_name\"],\n                app_full_name,\n                launch_type=LaunchTypes.farm_publish,\n            )\n            os.environ.update(env)\n\n        pyblish.api.register_host(\"shell\")\n\n        if targets:\n            for target in targets:\n                print(f\"setting target: {target}\")\n                pyblish.api.register_target(target)\n        else:\n            pyblish.api.register_target(\"farm\")\n\n        os.environ[\"OPENPYPE_PUBLISH_DATA\"] = os.pathsep.join(paths)\n        os.environ[\"HEADLESS_PUBLISH\"] = 'true'  # to use in app lib\n\n        log.info(\"Running publish ...\")\n\n        plugins = pyblish.api.discover()\n        print(\"Using plugins:\")\n        for plugin in plugins:\n            print(plugin)\n\n        if gui:\n            from openpype.tools.utils.host_tools import show_publish\n            from openpype.tools.utils.lib import qt_app_context\n            with qt_app_context():\n                show_publish()\n        else:\n            # Error exit as soon as any error occurs.\n            error_format = (\"Failed {plugin.__name__}: \"\n                            \"{error} -- {error.traceback}\")\n\n            for result in pyblish.util.publish_iter():\n                if result[\"error\"]:\n                    log.error(error_format.format(**result))\n                    # uninstall()\n                    sys.exit(1)\n\n        log.info(\"Publish finished.\")\n\n    @staticmethod\n    def extractenvironments(output_json_path, project, asset, task, app,\n                            env_group):\n        \"\"\"Produces json file with environment based on project and app.\n\n        Called by Deadline plugin to propagate environment into render jobs.\n        \"\"\"\n\n        from openpype.lib.applications import (\n            get_app_environments_for_context,\n            LaunchTypes,\n        )\n\n        if all((project, asset, task, app)):\n            env = get_app_environments_for_context(\n                project,\n                asset,\n                task,\n                app,\n                env_group=env_group,\n                launch_type=LaunchTypes.farm_render\n            )\n        else:\n            env = os.environ.copy()\n\n        output_dir = os.path.dirname(output_json_path)\n        if not os.path.exists(output_dir):\n            os.makedirs(output_dir)\n\n        with open(output_json_path, \"w\") as file_stream:\n            json.dump(env, file_stream, indent=4)\n\n    @staticmethod\n    def launch_project_manager():\n        from openpype.tools import project_manager\n\n        project_manager.main()\n\n    @staticmethod\n    def contextselection(output_path, project_name, asset_name, strict):\n        from openpype.tools.context_dialog import main\n\n        main(output_path, project_name, asset_name, strict)\n\n    def validate_jsons(self):\n        pass\n\n    def run_tests(self, folder, mark, pyargs,\n                  test_data_folder, persist, app_variant, timeout, setup_only,\n                  mongo_url, app_group, dump_databases):\n        \"\"\"\n            Runs tests from 'folder'\n\n            Args:\n                 folder (str): relative path to folder with tests\n                 mark (str): label to run tests marked by it (slow etc)\n                 pyargs (str): package path to test\n                 test_data_folder (str): url to unzipped folder of test data\n                 persist (bool): True if keep test db and published after test\n                    end\n                app_variant (str): variant (eg 2020 for AE), empty if use\n                    latest installed version\n                timeout (int): explicit timeout for single test\n                setup_only (bool): if only preparation steps should be\n                    triggered, no tests (useful for debugging/development)\n                mongo_url (str): url to Openpype Mongo database\n        \"\"\"\n        print(\"run_tests\")\n        if folder:\n            folder = \" \".join(list(folder))\n        else:\n            folder = \"../tests\"\n\n        # disable warnings and show captured stdout even if success\n        args = [\n            \"--disable-pytest-warnings\",\n            \"--capture=sys\",\n            \"--print\",\n            \"-W ignore::DeprecationWarning\",\n            \"-rP\",\n            folder\n        ]\n\n        if mark:\n            args.extend([\"-m\", mark])\n\n        if pyargs:\n            args.extend([\"--pyargs\", pyargs])\n\n        if test_data_folder:\n            args.extend([\"--test_data_folder\", test_data_folder])\n\n        if persist:\n            args.extend([\"--persist\", persist])\n\n        if app_group:\n            args.extend([\"--app_group\", app_group])\n\n        if app_variant:\n            args.extend([\"--app_variant\", app_variant])\n\n        if timeout:\n            args.extend([\"--timeout\", timeout])\n\n        if setup_only:\n            args.extend([\"--setup_only\", setup_only])\n\n        if mongo_url:\n            args.extend([\"--mongo_url\", mongo_url])\n\n        if dump_databases:\n            msg = \"dump_databases format is not recognized: {}\".format(\n                dump_databases\n            )\n            assert dump_databases in [\"bson\", \"json\"], msg\n            args.extend([\"--dump_databases\", dump_databases])\n\n        print(\"run_tests args: {}\".format(args))\n        import pytest\n        pytest.main(args)\n\n    def repack_version(self, directory):\n        \"\"\"Repacking OpenPype version.\"\"\"\n        from openpype.tools.repack_version import VersionRepacker\n\n        version_packer = VersionRepacker(directory)\n        version_packer.process()\n\n    def pack_project(self, project_name, dirpath, database_only):\n        from openpype.lib.project_backpack import pack_project\n\n        if database_only and not dirpath:\n            raise ValueError((\n                \"Destination dir must be defined when using --dbonly.\"\n                \" Use '--dirpath {output dir path}' flag\"\n                \" to specify directory.\"\n            ))\n\n        pack_project(project_name, dirpath, database_only)\n\n    def unpack_project(self, zip_filepath, new_root, database_only):\n        from openpype.lib.project_backpack import unpack_project\n\n        unpack_project(zip_filepath, new_root, database_only)\n"
  },
  {
    "path": "openpype/resources/__init__.py",
    "content": "import os\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.lib.openpype_version import is_staging_enabled\n\nRESOURCES_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\ndef get_resource(*args):\n    \"\"\" Serves to simple resources access\n\n    :param *args: should contain *subfolder* names and *filename* of\n                  resource from resources folder\n    :type *args: list\n    \"\"\"\n    return os.path.normpath(os.path.join(RESOURCES_DIR, *args))\n\n\ndef get_image_path(*args):\n    \"\"\"Helper function to get images.\n\n    Args:\n        *<str>: Filepath part items.\n    \"\"\"\n    return get_resource(\"images\", *args)\n\n\ndef get_liberation_font_path(bold=False, italic=False):\n    font_name = \"LiberationSans\"\n    suffix = \"\"\n    if bold:\n        suffix += \"Bold\"\n    if italic:\n        suffix += \"Italic\"\n\n    if not suffix:\n        suffix = \"Regular\"\n\n    filename = \"{}-{}.ttf\".format(font_name, suffix)\n    font_path = get_resource(\"fonts\", font_name, filename)\n    return font_path\n\n\ndef get_openpype_production_icon_filepath():\n    filename = \"openpype_icon.png\"\n    if AYON_SERVER_ENABLED:\n        filename = \"AYON_icon.png\"\n    return get_resource(\"icons\", filename)\n\n\ndef get_openpype_staging_icon_filepath():\n    filename = \"openpype_icon_staging.png\"\n    if AYON_SERVER_ENABLED:\n        filename = \"AYON_icon_staging.png\"\n    return get_resource(\"icons\", filename)\n\n\ndef get_openpype_icon_filepath(staging=None):\n    if AYON_SERVER_ENABLED and os.getenv(\"AYON_USE_DEV\") == \"1\":\n        return get_resource(\"icons\", \"AYON_icon_dev.png\")\n\n    if staging is None:\n        staging = is_staging_enabled()\n\n    if staging:\n        return get_openpype_staging_icon_filepath()\n    return get_openpype_production_icon_filepath()\n\n\ndef get_openpype_splash_filepath(staging=None):\n    if staging is None:\n        staging = is_staging_enabled()\n\n    if AYON_SERVER_ENABLED:\n        if os.getenv(\"AYON_USE_DEV\") == \"1\":\n            splash_file_name = \"AYON_splash_dev.png\"\n        elif staging:\n            splash_file_name = \"AYON_splash_staging.png\"\n        else:\n            splash_file_name = \"AYON_splash.png\"\n    elif staging:\n        splash_file_name = \"openpype_splash_staging.png\"\n    else:\n        splash_file_name = \"openpype_splash.png\"\n    return get_resource(\"icons\", splash_file_name)\n\n\ndef pype_icon_filepath(staging=None):\n    return get_openpype_icon_filepath(staging)\n\n\ndef pype_splash_filepath(staging=None):\n    return get_openpype_splash_filepath(staging)\n"
  },
  {
    "path": "openpype/resources/fonts/LiberationSans/License.txt",
    "content": "LICENSE AGREEMENT AND LIMITED PRODUCT WARRANTY LIBERATION FONT SOFTWARE\nThis agreement governs the use of the Software and any updates to the\nSoftware, regardless of the delivery mechanism. Subject to the following\nterms, Red Hat, Inc. (\"Red Hat\") grants to the user (\"Client\") a license to\nthis collective work pursuant to the GNU General Public License v.2 with the\nexceptions set forth below and such other terms as our set forth in this End\nUser License Agreement.\n1. The Software and License Exception. LIBERATION font software  (the\n\"Software\") consists of TrueType-OpenType formatted font software for\nrendering LIBERATION typefaces in sans serif, serif, and monospaced character\nstyles. You are licensed to use, modify, copy, and distribute the Software\npursuant to the GNU General Public License v.2 with the following exceptions:  \n1) As a special exception, if you create a document which uses this font, and\nembed this font or unaltered portions of this font into the document, this\nfont does not by itself cause the resulting document to be covered by the GNU\nGeneral Public License. This exception does not however invalidate any other\nreasons why the document might be covered by the GNU General Public License.\nIf you modify this font, you may extend this exception to your version of the\nfont, but you are not obligated to do so. If you do not wish to do so, delete\nthis exception statement from your version. \n\n2) As a further exception, any distribution of the object code of the Software\nin a physical product must provide you the right to access and modify the\nsource code for the Software and to reinstall that modified version of the\nSoftware in object code form on the same physical product on which you\nreceived it.\n2. Intellectual Property Rights. The Software and each of its components,\nincluding the source code, documentation, appearance, structure and\norganization are owned by Red Hat and others and are protected under copyright\nand other laws. Title to the Software and any component, or to any copy,\nmodification, or merged portion shall remain with the aforementioned, subject\nto the applicable license. The \"LIBERATION\" trademark is a trademark of Red\nHat, Inc. in the U.S. and other countries. This agreement does not permit\nClient to distribute modified versions of the Software using Red Hat's\ntrademarks. If Client makes a redistribution of a modified version of the\nSoftware, then Client must modify the files names to remove any reference to\nthe Red Hat trademarks and must not use the Red Hat trademarks in any way to\nreference or promote the modified Software. \n3. Limited Warranty. To the maximum extent permitted under applicable law, the\nSoftware is provided and licensed \"as is\" without warranty of any kind,\nexpressed or implied, including the implied warranties of merchantability,\nnon-infringement or fitness for a particular purpose. Red Hat does not warrant\nthat the functions contained in the Software will meet Client's requirements\nor that the operation of the Software will be entirely error free or appear\nprecisely as described in the accompanying documentation. \n4. Limitation of Remedies and Liability.  To the maximum extent permitted by\napplicable law, Red Hat or any Red Hat authorized dealer will not be liable to\nClient for any incidental or consequential damages, including lost profits or\nlost savings arising out of the use or inability to use the Software, even if\nRed Hat or such dealer has been advised of the possibility of such damages. \n5. Export Control. As required by U.S. law, Client represents and warrants\nthat it: (a) understands that the Software is subject to export controls under\nthe U.S. Commerce Department's Export Administration Regulations (\"EAR\"); (b)\nis not located in a prohibited destination country under the EAR or U.S.\nsanctions regulations (currently Cuba, Iran, Iraq, Libya, North Korea, Sudan\nand Syria); (c) will not export, re-export, or transfer the Software to any\nprohibited destination, entity, or individual without the necessary export\nlicense(s) or authorizations(s) from the U.S. Government; (d) will not use or\ntransfer the Software for use in any sensitive nuclear, chemical or biological\nweapons, or missile technology end-uses unless authorized by the U.S.\nGovernment by regulation or specific license; (e) understands and agrees that\nif it is in the United States and exports or transfers the Software to\neligible end users, it will, as required by EAR Section 740.17(e), submit\nsemi-annual reports to the Commerce Department's Bureau of Industry & Security\n(BIS), which include the name and address (including country) of each\ntransferee; and (f) understands that countries other than the United States\nmay restrict the import, use, or export of encryption products and that it\nshall be solely responsible for compliance with any such import, use, or\nexport restrictions.\n6. General. If any provision of this agreement is held to be unenforceable,\nthat shall not affect the enforceability of the remaining provisions. This\nagreement shall be governed by the laws of the State of North Carolina and of\nthe United States, without regard to any conflict of laws provisions, except\nthat the United Nations Convention on the International Sale of Goods shall\nnot apply.\nCopyright  2007 Red Hat, Inc. All rights reserved. LIBERATION is a trademark\nof Red Hat, Inc.\n"
  },
  {
    "path": "openpype/resources/ftrack/sign_in_message.html",
    "content": "<html>\n    <style type=\"text/css\">\n        body {\n            background-color: #333;\n            text-align: center;\n            color: #ccc;\n            margin-top: 200px;\n        }\n        h1 {\n            font-family: \"DejaVu Sans\";\n            font-size: 36px;\n            margin: 20px 0;\n        }\n        h3 {\n            font-weight: normal;\n            font-family: \"DejaVu Sans\";\n            margin: 30px 10px;\n        }\n        em {\n            color: #fff;\n        }\n    </style>\n    <body>\n        <h1>Sign in to Ftrack was successful</h1>\n        <h3>\n            You signed in with username <em>{}</em>.\n        </h3>\n        <h3>\n            You can close this window now.\n        </h3>\n    </body>\n</html>\n"
  },
  {
    "path": "openpype/scripts/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/scripts/non_python_host_launch.py",
    "content": "\"\"\"Script wraps launch mechanism of non python host implementations.\n\nArguments passed to the script are passed to launch function in host\nimplementation. In all cases requires host app executable and may contain\nworkfile or others.\n\"\"\"\n\nimport os\nimport sys\n\n# Get current file to locate start point of sys.argv\nCURRENT_FILE = os.path.abspath(__file__)\n\n\ndef show_error_messagebox(title, message, detail_message=None):\n    \"\"\"Function will show message and process ends after closing it.\"\"\"\n    from qtpy import QtWidgets, QtCore\n    from openpype import style\n\n    app = QtWidgets.QApplication([])\n    app.setStyleSheet(style.load_stylesheet())\n\n    msgbox = QtWidgets.QMessageBox()\n    msgbox.setWindowTitle(title)\n    msgbox.setText(message)\n\n    if detail_message:\n        msgbox.setDetailedText(detail_message)\n\n    msgbox.setWindowModality(QtCore.Qt.ApplicationModal)\n    msgbox.show()\n\n    sys.exit(app.exec_())\n\n\ndef on_invalid_args(script_not_found):\n    \"\"\"Show to user message box saying that something went wrong.\n\n    Tell user that arguments to launch implementation are invalid with\n    arguments details.\n\n    Args:\n        script_not_found (bool): Use different message based on this value.\n    \"\"\"\n\n    title = \"Invalid arguments\"\n    joined_args = \", \".join(\"\\\"{}\\\"\".format(arg) for arg in sys.argv)\n    if script_not_found:\n        submsg = \"Where couldn't find script path:\\n\\\"{}\\\"\"\n    else:\n        submsg = \"Expected Host executable after script path:\\n\\\"{}\\\"\"\n\n    message = \"BUG: Got invalid arguments so can't launch Host application.\"\n    detail_message = \"Process was launched with arguments:\\n{}\\n\\n{}\".format(\n        joined_args,\n        submsg.format(CURRENT_FILE)\n    )\n\n    show_error_messagebox(title, message, detail_message)\n\n\ndef main(argv):\n    # Modify current file path to find match in sys.argv which may be different\n    #   on windows (different letter cases and slashes).\n    modified_current_file = CURRENT_FILE.replace(\"\\\\\", \"/\").lower()\n\n    # Create a copy of sys argv\n    sys_args = list(argv)\n    after_script_idx = None\n    # Find script path in sys.argv to know index of argv where host\n    #   executable should be.\n    for idx, item in enumerate(sys_args):\n        if item.replace(\"\\\\\", \"/\").lower() == modified_current_file:\n            after_script_idx = idx + 1\n            break\n\n    # Validate that there is at least one argument after script path\n    launch_args = None\n    if after_script_idx is not None:\n        launch_args = sys_args[after_script_idx:]\n\n    host_name = os.environ[\"AVALON_APP\"].lower()\n    if host_name == \"photoshop\":\n        # TODO refactor launch logic according to AE\n        from openpype.hosts.photoshop.api.lib import main\n    elif host_name == \"aftereffects\":\n        from openpype.hosts.aftereffects.api.launch_logic import main\n    elif host_name == \"harmony\":\n        from openpype.hosts.harmony.api.lib import main\n    else:\n        title = \"Unknown host name\"\n        message = (\n            \"BUG: Environment variable AVALON_APP contains unknown\"\n            \" host name \\\"{}\\\"\"\n        ).format(host_name)\n        show_error_messagebox(title, message)\n        return\n\n    if launch_args:\n        # Launch host implementation\n        main(*launch_args)\n    else:\n        # Show message box\n        on_invalid_args(after_script_idx is None)\n\n\nif __name__ == \"__main__\":\n    main(sys.argv)\n"
  },
  {
    "path": "openpype/scripts/ocio_wrapper.py",
    "content": "\"\"\"OpenColorIO Wrapper.\n\nOnly to be interpreted by Python 3. It is run in subprocess in case\nPython 2 hosts needs to use it. Or it is used as module for Python 3\nprocessing.\n\nProviding functionality:\n- get_colorspace - console command - python 2\n                 - returning all available color spaces\n                   found in input config path.\n- _get_colorspace_data - python 3 - module function\n                      - returning all available colorspaces\n                        found in input config path.\n- get_views - console command - python 2\n            - returning all available viewers\n              found in input config path.\n- _get_views_data - python 3 - module function\n                 - returning all available viewers\n                   found in input config path.\n\"\"\"\nimport os\nimport click\nimport json\nimport PyOpenColorIO as ocio\n\n\n@click.group()\ndef main():\n    pass  # noqa: WPS100\n\n\n@main.group()\ndef config():\n    \"\"\"Config related commands group\n\n    Example of use:\n    > pyton.exe ./ocio_wrapper.py config <command> *args\n    \"\"\"\n    pass  # noqa: WPS100\n\n\n@main.group()\ndef colorspace():\n    \"\"\"Colorspace related commands group\n\n    Example of use:\n    > pyton.exe ./ocio_wrapper.py config <command> *args\n    \"\"\"\n    pass  # noqa: WPS100\n\n\n@config.command(\n    name=\"get_colorspace\",\n    help=(\n        \"return all colorspaces from config file \"\n        \"--path input arg is required\"\n    )\n)\n@click.option(\"--in_path\", required=True,\n              help=\"path where to read ocio config file\",\n              type=click.Path(exists=True))\n@click.option(\"--out_path\", required=True,\n              help=\"path where to write output json file\",\n              type=click.Path())\ndef get_colorspace(in_path, out_path):\n    \"\"\"Aggregate all colorspace to file.\n\n    Python 2 wrapped console command\n\n    Args:\n        in_path (str): config file path string\n        out_path (str): temp json file path string\n\n    Example of use:\n    > pyton.exe ./ocio_wrapper.py config get_colorspace\n        --in_path=<path> --out_path=<path>\n    \"\"\"\n    out_data = _get_colorspace_data(in_path)\n\n    with open(out_path, \"w\") as f_:\n        json.dump(out_data, f_)\n\n    print(\"Colorspace data are saved to '{}'\".format(out_path))\n\n\ndef _get_colorspace_data(config_path):\n    \"\"\"Return all found colorspace data.\n\n    Args:\n        config_path (str): path string leading to config.ocio\n\n    Raises:\n        IOError: Input config does not exist.\n\n    Returns:\n        dict: aggregated available colorspaces\n    \"\"\"\n    if not os.path.isfile(config_path):\n        raise IOError(\n            \"Input path `{}` should be `config.ocio` file\".format(config_path))\n\n    config = ocio.Config().CreateFromFile(config_path)\n\n    colorspace_data = {\n        \"roles\": {},\n        \"colorspaces\": {\n            color.getName(): {\n                \"family\": color.getFamily(),\n                \"categories\": list(color.getCategories()),\n                \"aliases\": list(color.getAliases()),\n                \"equalitygroup\": color.getEqualityGroup(),\n            }\n            for color in config.getColorSpaces()\n        },\n        \"displays_views\": {\n            \"{} ({})\".format(display, view): {\n                \"display\": display,\n                \"view\": view\n            }\n            for display in config.getDisplays()\n            for view in config.getViews(display)\n        },\n        \"looks\": {}\n    }\n\n    # add looks\n    looks = config.getLooks()\n    if looks:\n        colorspace_data[\"looks\"] = {\n            look.getName(): {\"process_space\": look.getProcessSpace()}\n            for look in looks\n        }\n\n    # add roles\n    roles = config.getRoles()\n    if roles:\n        colorspace_data[\"roles\"] = {\n            role: {\"colorspace\": colorspace}\n            for (role, colorspace) in roles\n        }\n\n    return colorspace_data\n\n\n@config.command(\n    name=\"get_views\",\n    help=(\n        \"return all viewers from config file \"\n        \"--path input arg is required\"\n    )\n)\n@click.option(\"--in_path\", required=True,\n              help=\"path where to read ocio config file\",\n              type=click.Path(exists=True))\n@click.option(\"--out_path\", required=True,\n              help=\"path where to write output json file\",\n              type=click.Path())\ndef get_views(in_path, out_path):\n    \"\"\"Aggregate all viewers to file.\n\n    Python 2 wrapped console command\n\n    Args:\n        in_path (str): config file path string\n        out_path (str): temp json file path string\n\n    Example of use:\n    > pyton.exe ./ocio_wrapper.py config get_views \\\n        --in_path=<path> --out_path=<path>\n    \"\"\"\n    out_data = _get_views_data(in_path)\n\n    with open(out_path, \"w\") as f_:\n        json.dump(out_data, f_)\n\n    print(\"Viewer data are saved to '{}'\".format(out_path))\n\n\ndef _get_views_data(config_path):\n    \"\"\"Return all found viewer data.\n\n    Args:\n        config_path (str): path string leading to config.ocio\n\n    Raises:\n        IOError: Input config does not exist.\n\n    Returns:\n        dict: aggregated available viewers\n    \"\"\"\n    if not os.path.isfile(config_path):\n        raise IOError(\"Input path should be `config.ocio` file\")\n\n    config = ocio.Config().CreateFromFile(config_path)\n\n    data_ = {}\n    for display in config.getDisplays():\n        for view in config.getViews(display):\n            colorspace = config.getDisplayViewColorSpaceName(display, view)\n            # Special token.\n            # See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa\n            if colorspace == \"<USE_DISPLAY_NAME>\":\n                colorspace = display\n\n            data_[\"{}/{}\".format(display, view)] = {\n                \"display\": display,\n                \"view\": view,\n                \"colorspace\": colorspace\n            }\n\n    return data_\n\n\n@config.command(\n    name=\"get_version\",\n    help=(\n        \"return major and minor version from config file \"\n        \"--config_path input arg is required\"\n        \"--out_path input arg is required\"\n    )\n)\n@click.option(\"--config_path\", required=True,\n              help=\"path where to read ocio config file\",\n              type=click.Path(exists=True))\n@click.option(\"--out_path\", required=True,\n              help=\"path where to write output json file\",\n              type=click.Path())\ndef get_version(config_path, out_path):\n    \"\"\"Get version of config.\n\n    Python 2 wrapped console command\n\n    Args:\n        config_path (str): ocio config file path string\n        out_path (str): temp json file path string\n\n    Example of use:\n    > pyton.exe ./ocio_wrapper.py config get_version \\\n        --config_path=<path> --out_path=<path>\n    \"\"\"\n    out_data = _get_version_data(config_path)\n\n    with open(out_path, \"w\") as f_:\n        json.dump(out_data, f_)\n\n    print(\"Config version data are saved to '{}'\".format(out_path))\n\n\ndef _get_version_data(config_path):\n    \"\"\"Return major and minor version info.\n\n    Args:\n        config_path (str): path string leading to config.ocio\n\n    Raises:\n        IOError: Input config does not exist.\n\n    Returns:\n        dict: minor and major keys with values\n    \"\"\"\n    if not os.path.isfile(config_path):\n        raise IOError(\"Input path should be `config.ocio` file\")\n\n    config = ocio.Config().CreateFromFile(config_path)\n\n    return {\n        \"major\": config.getMajorVersion(),\n        \"minor\": config.getMinorVersion()\n    }\n\n\n@colorspace.command(\n    name=\"get_config_file_rules_colorspace_from_filepath\",\n    help=(\n        \"return colorspace from filepath \"\n        \"--config_path - ocio config file path (input arg is required) \"\n        \"--filepath - any file path (input arg is required) \"\n        \"--out_path - temp json file path (input arg is required)\"\n    )\n)\n@click.option(\"--config_path\", required=True,\n              help=\"path where to read ocio config file\",\n              type=click.Path(exists=True))\n@click.option(\"--filepath\", required=True,\n              help=\"path to file to get colorspace from\",\n              type=click.Path())\n@click.option(\"--out_path\", required=True,\n              help=\"path where to write output json file\",\n              type=click.Path())\ndef get_config_file_rules_colorspace_from_filepath(\n    config_path, filepath, out_path\n):\n    \"\"\"Get colorspace from file path wrapper.\n\n    Python 2 wrapped console command\n\n    Args:\n        config_path (str): config file path string\n        filepath (str): path string leading to file\n        out_path (str): temp json file path string\n\n    Example of use:\n    > pyton.exe ./ocio_wrapper.py \\\n        colorspace get_config_file_rules_colorspace_from_filepath \\\n        --config_path=<path> --filepath=<path> --out_path=<path>\n    \"\"\"\n    colorspace = _get_config_file_rules_colorspace_from_filepath(\n        config_path, filepath)\n\n    with open(out_path, \"w\") as f_:\n        json.dump(colorspace, f_)\n\n    print(\"Colorspace name is saved to '{}'\".format(out_path))\n\n\ndef _get_config_file_rules_colorspace_from_filepath(config_path, filepath):\n    \"\"\"Return found colorspace data found in v2 file rules.\n\n    Args:\n        config_path (str): path string leading to config.ocio\n        filepath (str): path string leading to v2 file rules\n\n    Raises:\n        IOError: Input config does not exist.\n\n    Returns:\n        dict: aggregated available colorspaces\n    \"\"\"\n    if not os.path.isfile(config_path):\n        raise IOError(\n            \"Input path `{}` should be `config.ocio` file\".format(config_path))\n\n    config = ocio.Config().CreateFromFile(config_path)\n\n    # TODO: use `parseColorSpaceFromString` instead if ocio v1\n    colorspace = config.getColorSpaceFromFilepath(filepath)\n\n    return colorspace\n\n\ndef _get_display_view_colorspace_name(config_path, display, view):\n    \"\"\"Returns the colorspace attribute of the (display, view) pair.\n\n    Args:\n        config_path (str): path string leading to config.ocio\n        display (str): display name e.g. \"ACES\"\n        view (str): view name e.g. \"sRGB\"\n\n\n    Raises:\n        IOError: Input config does not exist.\n\n    Returns:\n        view color space name (str) e.g. \"Output - sRGB\"\n    \"\"\"\n    if not os.path.isfile(config_path):\n        raise IOError(\"Input path should be `config.ocio` file\")\n\n    config = ocio.Config.CreateFromFile(config_path)\n    colorspace = config.getDisplayViewColorSpaceName(display, view)\n\n    return colorspace\n\n\n@config.command(\n    name=\"get_display_view_colorspace_name\",\n    help=(\n        \"return default view colorspace name \"\n        \"for the given display and view \"\n        \"--path input arg is required\"\n    )\n)\n@click.option(\"--in_path\", required=True,\n              help=\"path where to read ocio config file\",\n              type=click.Path(exists=True))\n@click.option(\"--out_path\", required=True,\n              help=\"path where to write output json file\",\n              type=click.Path())\n@click.option(\"--display\", required=True,\n              help=\"display name\",\n              type=click.STRING)\n@click.option(\"--view\", required=True,\n              help=\"view name\",\n              type=click.STRING)\ndef get_display_view_colorspace_name(in_path, out_path,\n                                     display, view):\n    \"\"\"Aggregate view colorspace name to file.\n\n    Wrapper command for processes without access to OpenColorIO\n\n    Args:\n        in_path (str): config file path string\n        out_path (str): temp json file path string\n        display (str): display name e.g. \"ACES\"\n        view (str): view name e.g. \"sRGB\"\n\n    Example of use:\n    > pyton.exe ./ocio_wrapper.py config \\\n        get_display_view_colorspace_name --in_path=<path> \\\n        --out_path=<path> --display=<display> --view=<view>\n    \"\"\"\n\n    out_data = _get_display_view_colorspace_name(in_path,\n                                                 display,\n                                                 view)\n\n    with open(out_path, \"w\") as f:\n        json.dump(out_data, f)\n\n    print(\"Display view colorspace saved to '{}'\".format(out_path))\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "openpype/scripts/otio_burnin.py",
    "content": "import os\nimport sys\nimport subprocess\nimport platform\nimport json\nimport tempfile\nfrom string import Formatter\n\nimport opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins\nfrom openpype.lib import (\n    get_ffmpeg_tool_args,\n    get_ffmpeg_codec_args,\n    get_ffmpeg_format_args,\n    convert_ffprobe_fps_value,\n)\n\nFFMPEG = (\n    '{}%(input_args)s -i \"%(input)s\" %(filters)s %(args)s%(output)s'\n).format(subprocess.list2cmdline(get_ffmpeg_tool_args(\"ffmpeg\")))\n\nDRAWTEXT = (\n    \"drawtext@'%(label)s'=fontfile='%(font)s':text=\\\\'%(text)s\\\\':\"\n    \"x=%(x)s:y=%(y)s:fontcolor=%(color)s@%(opacity).1f:fontsize=%(size)d\"\n)\nTIMECODE = (\n    \"drawtext=timecode=\\\\'%(timecode)s\\\\':text=\\\\'%(text)s\\\\'\"\n    \":timecode_rate=%(fps).2f:x=%(x)s:y=%(y)s:fontcolor=\"\n    \"%(color)s@%(opacity).1f:fontsize=%(size)d:fontfile='%(font)s'\"\n)\n\nMISSING_KEY_VALUE = \"N/A\"\nCURRENT_FRAME_KEY = \"{current_frame}\"\nCURRENT_FRAME_SPLITTER = \"_-_CURRENT_FRAME_-_\"\nTIMECODE_KEY = \"{timecode}\"\nSOURCE_TIMECODE_KEY = \"{source_timecode}\"\n\n\ndef _get_ffprobe_data(source):\n    \"\"\"Reimplemented from otio burnins to be able use full path to ffprobe\n    :param str source: source media file\n    :rtype: [{}, ...]\n    \"\"\"\n    command = get_ffmpeg_tool_args(\n        \"ffprobe\",\n        \"-v\", \"quiet\",\n        \"-print_format\", \"json\",\n        \"-show_format\",\n        \"-show_streams\",\n        source\n    )\n    kwargs = {\n        \"stdout\": subprocess.PIPE,\n    }\n    if platform.system().lower() == \"windows\":\n        kwargs[\"creationflags\"] = (\n            subprocess.CREATE_NEW_PROCESS_GROUP\n            | getattr(subprocess, \"DETACHED_PROCESS\", 0)\n            | getattr(subprocess, \"CREATE_NO_WINDOW\", 0)\n        )\n    proc = subprocess.Popen(command, **kwargs)\n    out = proc.communicate()[0]\n    if proc.returncode != 0:\n        raise RuntimeError(\"Failed to run: %s\" % command)\n    return json.loads(out)\n\n\nclass ModifiedBurnins(ffmpeg_burnins.Burnins):\n    '''\n    This is modification of OTIO FFmpeg Burnin adapter.\n    - requires FFmpeg in PATH\n\n    Offers 6 positions for burnin text. Each can be set with:\n    - static text\n    - frames\n    - timecode\n\n    Options - dictionary which sets the final look.\n    - Datatypes explanation:\n    <color> string format must be supported by FFmpeg.\n        Examples: \"#000000\", \"0x000000\", \"black\"\n    <font> must be accesible by ffmpeg = name of registered Font in system or path to font file.\n        Examples: \"Arial\", \"C:/Windows/Fonts/arial.ttf\"\n\n    - Possible keys:\n    \"opacity\" - Opacity of text - <float, Range:0-1>\n    \"bg_opacity\" - Opacity of background (box around text) - <float, Range:0-1>\n    \"bg_color\" - Background color - <color>\n    \"bg_padding\" - Background padding in pixels - <int>\n    \"x_offset\" - offsets burnin vertically by entered pixels from border - <int>\n    \"y_offset\" - offsets burnin horizontally by entered pixels from border - <int>\n    - x_offset & y_offset should be set at least to same value as bg_padding!!\n    \"font\" - Font Family for text - <font>\n    \"font_size\" - Font size in pixels - <int>\n    \"font_color\" - Color of text - <color>\n    \"frame_offset\" - Default start frame - <int>\n        - required IF start frame is not set when using frames or timecode burnins\n\n    On initializing class can be set General options through \"options_init\" arg.\n    General can be overridden when adding burnin\n\n    '''\n    TOP_CENTERED = ffmpeg_burnins.TOP_CENTERED\n    BOTTOM_CENTERED = ffmpeg_burnins.BOTTOM_CENTERED\n    TOP_LEFT = ffmpeg_burnins.TOP_LEFT\n    BOTTOM_LEFT = ffmpeg_burnins.BOTTOM_LEFT\n    TOP_RIGHT = ffmpeg_burnins.TOP_RIGHT\n    BOTTOM_RIGHT = ffmpeg_burnins.BOTTOM_RIGHT\n\n    options_init = {\n        'opacity': 1,\n        'x_offset': 5,\n        'y_offset': 5,\n        'bg_padding': 5,\n        'bg_opacity': 0.5,\n        'font_size': 42\n    }\n\n    def __init__(\n        self, source, ffprobe_data=None, options_init=None, first_frame=None\n    ):\n        if not ffprobe_data:\n            ffprobe_data = _get_ffprobe_data(source)\n\n        # Validate 'streams' before calling super to raise more specific\n        #   error\n        source_streams = ffprobe_data.get(\"streams\")\n        if not source_streams:\n            raise ValueError((\n                \"Input file \\\"{}\\\" does not contain any streams\"\n                \" with image/video content.\"\n            ).format(source))\n\n        self.ffprobe_data = ffprobe_data\n        self.first_frame = first_frame\n        self.input_args = []\n        self.cleanup_paths = []\n\n        super().__init__(source, source_streams)\n\n        if options_init:\n            self.options_init.update(options_init)\n\n    def add_text(\n        self,\n        text,\n        align,\n        frame_start=None,\n        frame_end=None,\n        options=None,\n    ):\n        \"\"\"\n        Adding static text to a filter.\n\n        :param str text: text to apply to the drawtext\n        :param enum align: alignment, must use provided enum flags\n        :param int frame_start: starting frame for burnins current frame\n        :param dict options: recommended to use TextOptions\n        \"\"\"\n        if not options:\n            options = ffmpeg_burnins.TextOptions(**self.options_init)\n\n        options = options.copy()\n        if frame_start is not None:\n            options[\"frame_offset\"] = frame_start\n\n        # `frame_end` is only for meassurements of text position\n        if frame_end is not None:\n            options[\"frame_end\"] = frame_end\n\n\n        options[\"label\"] = align\n        self._add_burnin(text, align, options, DRAWTEXT)\n\n    def add_timecode(\n        self, align, frame_start=None, frame_end=None, frame_start_tc=None,\n        text=None, options=None\n    ):\n        \"\"\"\n        Convenience method to create the frame number expression.\n\n        :param enum align: alignment, must use provided enum flags\n        :param int frame_start:  starting frame for burnins current frame\n        :param int frame_start_tc:  starting frame for burnins timecode\n        :param str text: text that will be before timecode\n        :param dict options: recommended to use TimeCodeOptions\n        \"\"\"\n        if not options:\n            options = ffmpeg_burnins.TimeCodeOptions(**self.options_init)\n\n        options = options.copy()\n        if frame_start is not None:\n            options[\"frame_offset\"] = frame_start\n\n        # `frame_end` is only for meassurements of text position\n        if frame_end is not None:\n            options[\"frame_end\"] = frame_end\n\n        if not frame_start_tc:\n            frame_start_tc = options[\"frame_offset\"]\n\n        if not text:\n            text = \"\"\n\n        if not options.get(\"fps\"):\n            options[\"fps\"] = self.frame_rate\n\n        if isinstance(frame_start_tc, str):\n            options[\"timecode\"] = frame_start_tc\n        else:\n            options[\"timecode\"] = ffmpeg_burnins._frames_to_timecode(\n                frame_start_tc,\n                self.frame_rate\n            )\n\n        self._add_burnin(text, align, options, TIMECODE)\n\n    def add_per_frame_text(\n        self,\n        text,\n        align,\n        frame_start,\n        frame_end,\n        listed_keys,\n        options=None\n    ):\n        \"\"\"Add text that changes per frame.\n\n        Args:\n            text (str): Template string with unfilled keys that are changed\n                per frame.\n            align (str): Alignment of text.\n            frame_start (int): Starting frame for burnins current frame.\n            frame_end (int): Ending frame for burnins current frame.\n            listed_keys (list): List of keys that are changed per frame.\n            options (Optional[dict]): Options to affect style of burnin.\n        \"\"\"\n\n        if not options:\n            options = ffmpeg_burnins.TimeCodeOptions(**self.options_init)\n\n        options = options.copy()\n        if frame_start is None:\n            frame_start = options[\"frame_offset\"]\n\n        # `frame_end` is only for meassurements of text position\n        if frame_end is None:\n            frame_end = options[\"frame_end\"]\n\n        fps = options.get(\"fps\")\n        if not fps:\n            fps = self.frame_rate\n\n        text_for_size = text\n        if CURRENT_FRAME_SPLITTER in text:\n            expr = self._get_current_frame_expression(frame_start, frame_end)\n            if expr is None:\n                expr = MISSING_KEY_VALUE\n                text_for_size = text_for_size.replace(\n                    CURRENT_FRAME_SPLITTER, MISSING_KEY_VALUE)\n            text = text.replace(CURRENT_FRAME_SPLITTER, expr)\n\n        # Find longest list with values\n        longest_list_len = max(\n            len(item[\"values\"]) for item in listed_keys.values()\n        )\n        # Where to store formatted values per frame by key\n        new_listed_keys = [{} for _ in range(longest_list_len)]\n        # Find the longest value per fill key.\n        #   The longest value is used to determine size of burnin box.\n        longest_value_by_key = {}\n        for key, item in listed_keys.items():\n            values = item[\"values\"]\n            # Fill the missing values from the longest list with the last\n            #   value to make sure all values have same \"frame count\"\n            last_value = values[-1] if values else \"\"\n            for _ in range(longest_list_len - len(values)):\n                values.append(last_value)\n\n            # Prepare dictionary structure for nestes values\n            # - last key is overriden on each frame loop\n            item_keys = list(item[\"keys\"])\n            fill_data = {}\n            sub_value = fill_data\n            last_item_key = item_keys.pop(-1)\n            for item_key in item_keys:\n                sub_value[item_key] = {}\n                sub_value = sub_value[item_key]\n\n            # Fill value per frame\n            key_max_len = 0\n            key_max_value = \"\"\n            for value, new_values in zip(values, new_listed_keys):\n                sub_value[last_item_key] = value\n                try:\n                    value = key.format(**sub_value)\n                except (TypeError, KeyError, ValueError):\n                    value = MISSING_KEY_VALUE\n                new_values[key] = value\n\n                value_len = len(value)\n                if value_len > key_max_len:\n                    key_max_value = value\n                    key_max_len = value_len\n\n            # Store the longest value\n            longest_value_by_key[key] = key_max_value\n\n        # Make sure the longest value of each key is replaced for text size\n        #   calculation\n        for key, value in longest_value_by_key.items():\n            text_for_size = text_for_size.replace(key, value)\n\n        # Create temp file with instructions for each frame of text\n        lines = []\n        for frame, value in enumerate(new_listed_keys):\n            seconds = float(frame) / fps\n            # Escape special character\n            new_text = text\n            for _key, _value in value.items():\n                _value = str(_value)\n                new_text = new_text.replace(_key, str(_value))\n\n            new_text = (\n                str(new_text)\n                .replace(\"\\\\\", \"\\\\\\\\\")\n                .replace(\",\", \"\\\\,\")\n                .replace(\":\", \"\\\\:\")\n            )\n            lines.append(\n                f\"{seconds} drawtext@{align} reinit text='{new_text}';\")\n\n        with tempfile.NamedTemporaryFile(mode=\"w\", delete=False) as temp:\n            path = temp.name\n            temp.write(\"\\n\".join(lines))\n\n        self.cleanup_paths.append(path)\n        self.filters[\"drawtext\"].append(\"sendcmd=f='{}'\".format(\n            path.replace(\"\\\\\", \"/\").replace(\":\", \"\\\\:\")\n        ))\n        self.add_text(text_for_size, align, frame_start, frame_end, options)\n\n    def _get_current_frame_expression(self, frame_start, frame_end):\n        if frame_start is None:\n            return None\n        return (\n            \"%{eif:n+\" + str(frame_start)\n            + \":d:\" + str(len(str(frame_end))) + \"}\"\n        )\n\n    def _add_burnin(self, text, align, options, draw):\n        \"\"\"\n        Generic method for building the filter flags.\n        :param str text: text to apply to the drawtext\n        :param enum align: alignment, must use provided enum flags\n        :param dict options:\n        \"\"\"\n\n        final_text = text\n        text_for_size = text\n        if CURRENT_FRAME_SPLITTER in text:\n            frame_start = options[\"frame_offset\"]\n            frame_end = options.get(\"frame_end\", frame_start)\n            expr = self._get_current_frame_expression(frame_start, frame_end)\n            if expr is not None:\n                max_length = len(str(frame_end))\n                # Use number '8' length times for replacement\n                size_replacement = max_length * \"8\"\n            else:\n                expr = size_replacement = MISSING_KEY_VALUE\n\n            final_text = final_text.replace(\n                CURRENT_FRAME_SPLITTER, expr\n            )\n            text_for_size = text_for_size.replace(\n                CURRENT_FRAME_SPLITTER, size_replacement\n            )\n\n        resolution = self.resolution\n        data = {\n            'text': (\n                final_text\n                .replace(\",\", r\"\\,\")\n                .replace(':', r'\\:')\n            ),\n            'color': options['font_color'],\n            'size': options['font_size']\n        }\n        timecode_text = options.get(\"timecode\") or \"\"\n        text_for_size += timecode_text\n\n        font_path = options.get(\"font\")\n        if not font_path or not os.path.exists(font_path):\n            font_path = ffmpeg_burnins.FONT\n\n        options[\"font\"] = font_path\n\n        data.update(options)\n        data.update(\n            ffmpeg_burnins._drawtext(align, resolution, text_for_size, options)\n        )\n\n        arg_font_path = (\n            font_path\n            .replace(\"\\\\\", \"\\\\\\\\\")\n            .replace(':', r'\\:')\n        )\n        data[\"font\"] = arg_font_path\n\n        self.filters['drawtext'].append(draw % data)\n\n        if options.get('bg_color') is not None:\n            box = ffmpeg_burnins.BOX % {\n                'border': options['bg_padding'],\n                'color': options['bg_color'],\n                'opacity': options['bg_opacity']\n            }\n            self.filters['drawtext'][-1] += ':%s' % box\n\n    def command(self, output=None, args=None, overwrite=False):\n        \"\"\"\n        Generate the entire FFMPEG command.\n\n        :param str output: output file\n        :param str args: additional FFMPEG arguments\n        :param bool overwrite: overwrite the output if it exists\n        :returns: completed command\n        :rtype: str\n        \"\"\"\n        output = '\"{}\"'.format(output or '')\n        if overwrite:\n            output = '-y {}'.format(output)\n\n        filters = \"\"\n        filter_string = self.filter_string\n        if filter_string:\n            with tempfile.NamedTemporaryFile(mode=\"w\", delete=False) as temp:\n                temp.write(filter_string)\n                filters_path = temp.name\n            filters = '-filter_script:v \"{}\"'.format(filters_path)\n            print(\"Filters:\", filter_string)\n            self.cleanup_paths.append(filters_path)\n\n        if self.first_frame is not None:\n            start_number_arg = \"-start_number {}\".format(self.first_frame)\n            self.input_args.append(start_number_arg)\n            if \"start_number\" not in args:\n                if not args:\n                    args = start_number_arg\n                else:\n                    args = \" \".join((start_number_arg, args))\n\n        input_args = \"\"\n        if self.input_args:\n            input_args = \" {}\".format(\" \".join(self.input_args))\n\n        return (FFMPEG % {\n            'input_args': input_args,\n            'input': self.source,\n            'output': output,\n            'args': '%s ' % args if args else '',\n            'filters': filters\n        }).strip()\n\n    def render(self, output, args=None, overwrite=False, **kwargs):\n        \"\"\"\n        Render the media to a specified destination.\n\n        :param str output: output file\n        :param str args: additional FFMPEG arguments\n        :param bool overwrite: overwrite the output if it exists\n        \"\"\"\n        if not overwrite and os.path.exists(output):\n            raise RuntimeError(\"Destination '%s' exists, please \"\n                               \"use overwrite\" % output)\n\n        is_sequence = \"%\" in output\n\n        command = self.command(\n            output=output,\n            args=args,\n            overwrite=overwrite\n        )\n        print(\"Launching command: {}\".format(command))\n\n        kwargs = {\n            \"stdout\": subprocess.PIPE,\n            \"stderr\": subprocess.PIPE,\n            \"shell\": True,\n        }\n        proc = subprocess.Popen(command, **kwargs)\n\n        _stdout, _stderr = proc.communicate()\n        if _stdout:\n            print(_stdout.decode(\"utf-8\", errors=\"backslashreplace\"))\n\n        # This will probably never happen as ffmpeg use stdout\n        if _stderr:\n            print(_stderr.decode(\"utf-8\", errors=\"backslashreplace\"))\n\n        if proc.returncode != 0:\n            raise RuntimeError(\n                \"Failed to render '{}': {}'\".format(output, command)\n            )\n        if is_sequence:\n            output = output % kwargs.get(\"duration\")\n\n        if not os.path.exists(output):\n            raise RuntimeError(\n                \"Failed to generate this f*cking file '%s'\" % output\n            )\n\n        for path in self.cleanup_paths:\n            if os.path.exists(path):\n                os.remove(path)\n\n\ndef example(input_path, output_path):\n    options_init = {\n        'opacity': 1,\n        'x_offset': 10,\n        'y_offset': 10,\n        'bg_padding': 10,\n        'bg_opacity': 0.5,\n        'font_size': 52\n    }\n    # Options init sets burnin look\n    burnin = ModifiedBurnins(input_path, options_init=options_init)\n    # Static text\n    burnin.add_text('My Text', ModifiedBurnins.TOP_CENTERED)\n    # Datetime\n    burnin.add_text('%d-%m-%y', ModifiedBurnins.TOP_RIGHT)\n    # Start render (overwrite output file if exist)\n    burnin.render(output_path, overwrite=True)\n\n\ndef prepare_fill_values(burnin_template, data):\n    \"\"\"Prepare values that will be filled instead of burnin template.\n\n    Args:\n        burnin_template (str): Burnin template string.\n        data (dict[str, Any]): Data that will be used to fill template.\n\n    Returns:\n        tuple[dict[str, dict[str, Any]], dict[str, Any], set[str]]: Filled\n            values that can be used as are, listed values that have different\n            value per frame and missing keys that are not present in data.\n    \"\"\"\n\n    fill_values = {}\n    listed_keys = {}\n    missing_keys = set()\n    for item in Formatter().parse(burnin_template):\n        _, field_name, format_spec, conversion = item\n        if not field_name:\n            continue\n        # Calculate nested keys '{project[name]}' -> ['project', 'name']\n        keys = [key.rstrip(\"]\") for key in field_name.split(\"[\")]\n        # Calculate original full key for replacement\n        conversion = \"!{}\".format(conversion) if conversion else \"\"\n        format_spec = \":{}\".format(format_spec) if format_spec else \"\"\n        orig_key = \"{{{}{}{}}}\".format(\n            field_name, conversion, format_spec)\n\n        key_value = data\n        try:\n            for key in keys:\n                key_value = key_value[key]\n\n            if isinstance(key_value, list):\n                listed_keys[orig_key] = {\n                    \"values\": key_value,\n                    \"keys\": keys}\n            else:\n                fill_values[orig_key] = orig_key.format(**data)\n        except (KeyError, TypeError):\n            missing_keys.add(orig_key)\n            continue\n    return fill_values, listed_keys, missing_keys\n\n\ndef burnins_from_data(\n    input_path, output_path, data,\n    codec_data=None, options=None, burnin_values=None, overwrite=True,\n    full_input_path=None, first_frame=None, source_ffmpeg_cmd=None\n):\n    \"\"\"This method adds burnins to video/image file based on presets setting.\n\n    Extension of output MUST be same as input. (mov -> mov, avi -> avi,...)\n\n    Args:\n        input_path (str): Full path to input file where burnins should be add.\n        output_path (str): Full path to output file where output will be\n            rendered.\n        data (dict): Data required for burnin settings (more info below).\n        codec_data (list): All codec related arguments in list.\n        options (dict): Options for burnins.\n        burnin_values (dict): Contain positioned values.\n        overwrite (bool): Output will be overwritten if already exists,\n            True by default.\n\n    Presets must be set separately. Should be dict with 2 keys:\n    - \"options\" - sets look of burnins - colors, opacity,...\n        (more info: ModifiedBurnins doc)\n                - *OPTIONAL* default values are used when not included\n    - \"burnins\" - contains dictionary with burnins settings\n                - *OPTIONAL* burnins won't be added (easier is not to use this)\n        - each key of \"burnins\" represents Alignment,\n        there are 6 possibilities:\n            TOP_LEFT        TOP_CENTERED        TOP_RIGHT\n            BOTTOM_LEFT     BOTTOM_CENTERED     BOTTOM_RIGHT\n        - value must be string with text you want to burn-in\n        - text may contain specific formatting keys (exmplained below)\n\n    Requirement of *data* keys is based on presets.\n    - \"frame_start\" - is required when \"timecode\" or \"current_frame\" ins keys\n    - \"frame_start_tc\" - when \"timecode\" should start with different frame\n    - *keys for static text*\n\n    EXAMPLE:\n    preset = {\n        \"options\": {*OPTIONS FOR LOOK*},\n        \"burnins\": {\n            \"TOP_LEFT\": \"static_text\",\n            \"TOP_RIGHT\": \"{shot}\",\n            \"BOTTOM_LEFT\": \"TC: {timecode}\",\n            \"BOTTOM_RIGHT\": \"{frame_start}{current_frame}\"\n        }\n    }\n\n    For this preset we'll need at least this data:\n    data = {\n        \"frame_start\": 1001,\n        \"shot\": \"sh0010\"\n    }\n\n    When Timecode should start from 1 then data need:\n    data = {\n        \"frame_start\": 1001,\n        \"frame_start_tc\": 1,\n        \"shot\": \"sh0010\"\n    }\n    \"\"\"\n    ffprobe_data = None\n    if full_input_path:\n        ffprobe_data = _get_ffprobe_data(full_input_path)\n\n    burnin = ModifiedBurnins(input_path, ffprobe_data, options, first_frame)\n\n    frame_start = data.get(\"frame_start\")\n    frame_end = data.get(\"frame_end\")\n    frame_start_tc = data.get('frame_start_tc', frame_start)\n\n    video_stream = None\n    for stream in burnin._streams:\n        if stream.get(\"codec_type\") == \"video\":\n            video_stream = stream\n            break\n\n    if video_stream is None:\n        raise ValueError(\"Source didn't have video stream.\")\n\n    if \"resolution_width\" not in data:\n        data[\"resolution_width\"] = video_stream.get(\n            \"width\", MISSING_KEY_VALUE)\n\n    if \"resolution_height\" not in data:\n        data[\"resolution_height\"] = video_stream.get(\n            \"height\", MISSING_KEY_VALUE)\n\n    r_frame_rate = video_stream.get(\"r_frame_rate\", \"0/0\")\n    if \"fps\" not in data:\n        data[\"fps\"] = convert_ffprobe_fps_value(r_frame_rate)\n\n    # Check frame start and add expression if is available\n    if frame_start is not None:\n        data[CURRENT_FRAME_KEY[1:-1]] = CURRENT_FRAME_SPLITTER\n\n    if frame_start_tc is not None:\n        data[TIMECODE_KEY[1:-1]] = TIMECODE_KEY\n\n    source_timecode = video_stream.get(\"timecode\")\n    if source_timecode is None:\n        source_timecode = video_stream.get(\"tags\", {}).get(\"timecode\")\n\n    # Use \"format\" key from ffprobe data\n    #   - this is used e.g. in mxf extension\n    if source_timecode is None:\n        input_format = burnin.ffprobe_data.get(\"format\") or {}\n        source_timecode = input_format.get(\"timecode\")\n        if source_timecode is None:\n            source_timecode = input_format.get(\"tags\", {}).get(\"timecode\")\n\n    if source_timecode is not None:\n        data[SOURCE_TIMECODE_KEY[1:-1]] = SOURCE_TIMECODE_KEY\n\n    clean_up_paths = []\n    for align_text, value in burnin_values.items():\n        if not value:\n            continue\n\n        if isinstance(value, dict):\n            raise TypeError((\n                \"Expected string, number or list type.\"\n                \" Got: {} - \\\"{}\\\"\"\n                \" (Make sure you have new burnin presets).\"\n            ).format(str(type(value)), str(value)))\n\n        align = None\n        align_text = align_text.strip().lower()\n        if align_text == \"top_left\":\n            align = ModifiedBurnins.TOP_LEFT\n        elif align_text == \"top_centered\":\n            align = ModifiedBurnins.TOP_CENTERED\n        elif align_text == \"top_right\":\n            align = ModifiedBurnins.TOP_RIGHT\n        elif align_text == \"bottom_left\":\n            align = ModifiedBurnins.BOTTOM_LEFT\n        elif align_text == \"bottom_centered\":\n            align = ModifiedBurnins.BOTTOM_CENTERED\n        elif align_text == \"bottom_right\":\n            align = ModifiedBurnins.BOTTOM_RIGHT\n\n        has_timecode = TIMECODE_KEY in value\n        # Replace with missing key value if frame_start_tc is not set\n        if frame_start_tc is None and has_timecode:\n            has_timecode = False\n            print(\n                \"`frame_start` and `frame_start_tc`\"\n                \" are not set in entered data.\"\n            )\n            value = value.replace(TIMECODE_KEY, MISSING_KEY_VALUE)\n\n        has_source_timecode = SOURCE_TIMECODE_KEY in value\n        if source_timecode is None and has_source_timecode:\n            has_source_timecode = False\n            print(\"Source does not have set timecode value.\")\n            value = value.replace(SOURCE_TIMECODE_KEY, MISSING_KEY_VALUE)\n\n        # Failsafe for missing keys.\n        fill_values, listed_keys, missing_keys = prepare_fill_values(\n            value, data\n        )\n\n        for key in missing_keys:\n            value = value.replace(key, MISSING_KEY_VALUE)\n\n        if listed_keys:\n            for key, key_value in fill_values.items():\n                if key == CURRENT_FRAME_KEY:\n                    key_value = CURRENT_FRAME_SPLITTER\n                value = value.replace(key, str(key_value))\n            burnin.add_per_frame_text(\n                value, align, frame_start, frame_end, listed_keys\n            )\n            continue\n\n        # Handle timecode differently\n        if has_source_timecode:\n            args = [align, frame_start, frame_end, source_timecode]\n            if not value.startswith(SOURCE_TIMECODE_KEY):\n                value_items = value.split(SOURCE_TIMECODE_KEY)\n                text = value_items[0].format(**data)\n                args.append(text)\n\n            burnin.add_timecode(*args)\n            continue\n\n        if has_timecode:\n            args = [align, frame_start, frame_end, frame_start_tc]\n            if not value.startswith(TIMECODE_KEY):\n                value_items = value.split(TIMECODE_KEY)\n                text = value_items[0].format(**data)\n                args.append(text)\n\n            burnin.add_timecode(*args)\n            continue\n\n        text = value.format(**data)\n\n        burnin.add_text(text, align, frame_start, frame_end)\n\n    ffmpeg_args = []\n    if codec_data:\n        # Use codec definition from method arguments\n        ffmpeg_args = codec_data\n        ffmpeg_args.append(\"-g 1\")\n\n    else:\n        ffmpeg_args.extend(\n            get_ffmpeg_format_args(burnin.ffprobe_data, source_ffmpeg_cmd)\n        )\n        ffmpeg_args.extend(\n            get_ffmpeg_codec_args(burnin.ffprobe_data, source_ffmpeg_cmd)\n        )\n        # Use arguments from source if are available source arguments\n        if source_ffmpeg_cmd:\n            copy_args = (\n                \"-metadata\",\n                \"-metadata:s:v:0\",\n            )\n            args = source_ffmpeg_cmd.split(\" \")\n            for idx, arg in enumerate(args):\n                if arg in copy_args:\n                    ffmpeg_args.extend([arg, args[idx + 1]])\n\n    # Use group one (same as `-intra` argument, which is deprecated)\n    ffmpeg_args_str = \" \".join(ffmpeg_args)\n    burnin.render(\n        output_path, args=ffmpeg_args_str, overwrite=overwrite, **data\n    )\n    for path in clean_up_paths:\n        os.remove(path)\n\n\nif __name__ == \"__main__\":\n    print(\"* Burnin script started\")\n    in_data_json_path = sys.argv[-1]\n    with open(in_data_json_path, \"r\") as file_stream:\n        in_data = json.load(file_stream)\n\n    burnins_from_data(\n        in_data[\"input\"],\n        in_data[\"output\"],\n        in_data[\"burnin_data\"],\n        codec_data=in_data.get(\"codec\"),\n        options=in_data.get(\"options\"),\n        burnin_values=in_data.get(\"values\"),\n        full_input_path=in_data.get(\"full_input_path\"),\n        first_frame=in_data.get(\"first_frame\"),\n        source_ffmpeg_cmd=in_data.get(\"ffmpeg_cmd\")\n    )\n    print(\"* Burnin script has finished\")\n"
  },
  {
    "path": "openpype/scripts/remote_publish.py",
    "content": "try:\n    from openpype.lib import Logger\n    from openpype.pipeline.publish.lib import remote_publish\nexcept ImportError as exc:\n    # Ensure Deadline fails by output an error that contains \"Fatal Error:\"\n    raise ImportError(\"Fatal Error: %s\" % exc)\n\n\nif __name__ == \"__main__\":\n    # Perform remote publish with thorough error checking\n    log = Logger.get_logger(__name__)\n    remote_publish(log)\n"
  },
  {
    "path": "openpype/scripts/slates/__init__.py",
    "content": "from . import slate_base\nfrom .slate_base import api\n"
  },
  {
    "path": "openpype/scripts/slates/__main__.py",
    "content": "import sys\nimport json\nfrom slate_base import api\n\n\ndef main(in_args=None):\n    data_arg = in_args[-1]\n    in_data = json.loads(data_arg)\n    api.create_slates(\n        in_data[\"fill_data\"],\n        in_data.get(\"slate_name\"),\n        in_data.get(\"slate_data\"),\n        in_data.get(\"data_output_json\")\n    )\n\n\nif __name__ == \"__main__\":\n    main(sys.argv)\n"
  },
  {
    "path": "openpype/scripts/slates/slate_base/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/scripts/slates/slate_base/api.py",
    "content": "from .font_factory import FontFactory\nfrom .base import BaseObj, load_default_style\nfrom .main_frame import MainFrame\nfrom .layer import Layer\nfrom .items import (\n    BaseItem,\n    ItemImage,\n    ItemRectangle,\n    ItemPlaceHolder,\n    ItemText,\n    ItemTable,\n    TableField\n)\nfrom .lib import create_slates\nfrom .example import example\n"
  },
  {
    "path": "openpype/scripts/slates/slate_base/base.py",
    "content": "import os\nimport re\nimport logging\nimport copy\nimport json\nfrom uuid import uuid4\n\n\ndef load_default_style():\n    cur_folder = os.path.dirname(os.path.abspath(__file__))\n    default_json_path = os.path.join(cur_folder, \"default_style.json\")\n    with open(default_json_path, \"r\") as _file:\n        data = _file.read()\n    return json.loads(data)\n\n\nclass BaseObj:\n    \"\"\"Base Object for slates.\"\"\"\n\n    obj_type = None\n    available_parents = []\n    all_style_keys = [\n        \"font-family\", \"font-size\", \"font-color\", \"font-bold\", \"font-italic\",\n        \"bg-color\", \"bg-alter-color\",\n        \"alignment-horizontal\", \"alignment-vertical\",\n        \"padding\", \"padding-left\", \"padding-right\",\n        \"padding-top\", \"padding-bottom\",\n        \"margin\", \"margin-left\", \"margin-right\",\n        \"margin-top\", \"margin-bottom\", \"width\", \"height\",\n        \"fill\", \"word-wrap\", \"ellide\", \"max-lines\"\n    ]\n    fill_data_regex = r\"{[^}]+}\"\n\n    def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None):\n        if not self.obj_type:\n            raise NotImplementedError(\n                \"Class don't have set object type <{}>\".format(\n                    self.__class__.__name__\n                )\n            )\n\n        parent_obj_type = None\n        if parent:\n            parent_obj_type = parent.obj_type\n\n        if parent_obj_type not in self.available_parents:\n            expected_parents = \", \".join(self.available_parents)\n            raise Exception((\n                \"Invalid parent <{}> for <{}>. Expected <{}>\"\n            ).format(\n                parent.__class__.__name__, self.obj_type, expected_parents\n            ))\n\n        self.parent = parent\n        self._style = style\n\n        self.id = uuid4()\n        self.name = name\n        self.items = {}\n\n        self._pos_x = pos_x or 0\n        self._pos_y = pos_y or 0\n\n        log_parts = []\n        module = self.__class__.__module__\n        if module and module != \"__main__\":\n            log_parts.append(module)\n        log_parts.append(self.__class__.__name__)\n        self.log = logging.getLogger(\".\".join(log_parts))\n\n        if parent:\n            parent.add_item(self)\n\n    def fill_data_format(self):\n        return\n\n    @property\n    def fill_data(self):\n        return self.parent.fill_data\n\n    @property\n    def main_style(self):\n        return load_default_style()\n\n    def height(self):\n        raise NotImplementedError(\n            \"Attribute `height` is not implemented for <{}>\".format(\n                self.__clas__.__name__\n            )\n        )\n\n    def width(self):\n        raise NotImplementedError(\n            \"Attribute `width` is not implemented for <{}>\".format(\n                self.__clas__.__name__\n            )\n        )\n\n    def collect_data(self):\n        return None\n\n    def find_item(self, obj_type=None, name=None):\n        obj_type_fits = False\n        name_fits = False\n        if obj_type is None or self.obj_type == obj_type:\n            obj_type_fits = True\n\n        if name is None or self.name != name:\n            name_fits = True\n\n        output = []\n        if obj_type_fits and name_fits:\n            output.append(self)\n\n        if not self.items:\n            return output\n\n        for item in self.items.values():\n            output.extend(\n                item.find_item(obj_type=obj_type, name=name)\n            )\n        return output\n\n    @property\n    def full_style(self):\n        if self.parent is not None:\n            style = dict(val for val in self.parent.full_style.items())\n        else:\n            style = self.main_style\n\n        for key, value in self._style.items():\n            if key in self.all_style_keys:\n                # TODO which variant is right?\n                style[self.obj_type][key] = value\n                # style[\"*\"][key] = value\n            else:\n                if key not in style:\n                    style[key] = {}\n\n                if isinstance(style[key], dict):\n                    style[key].update(value)\n                else:\n                    style[key] = value\n\n        return style\n\n    def get_style_for_obj_type(self, obj_type, style=None):\n        if not style:\n            style = copy.deepcopy(self.full_style)\n\n        base = style.get(\"*\") or {}\n        obj_specific = style.get(obj_type) or {}\n        name_specific = {}\n        if self.name:\n            name = str(self.name)\n            if not name.startswith(\"#\"):\n                name = \"#\" + name\n            name_specific = style.get(name) or {}\n\n        if obj_type == \"table-item\":\n            col_regex = r\"table-item-col\\[([\\d\\-, ]+)*\\]\"\n            row_regex = r\"table-item-row\\[([\\d\\-, ]+)*\\]\"\n            field_regex = (\n                r\"table-item-field\\[(([ ]+)?\\d+([ ]+)?:([ ]+)?\\d+([ ]+)?)*\\]\"\n            )\n            # STRICT field regex (not allowed spaces)\n            # fild_regex = r\"table-item-field\\[(\\d+:\\d+)*\\]\"\n\n            def get_indexes_from_regex_match(result, field=False):\n                group = result.group(1)\n                indexes = []\n                if field:\n                    return [\n                        int(part.strip()) for part in group.strip().split(\":\")\n                    ]\n\n                parts = group.strip().split(\",\")\n                for part in parts:\n                    part = part.strip()\n                    if \"-\" not in part:\n                        indexes.append(int(part))\n                        continue\n\n                    sub_parts = [\n                        int(sub.strip()) for sub in part.split(\"-\")\n                    ]\n                    if len(sub_parts) != 2:\n                        # TODO logging\n                        self.log.warning(\"Invalid range '{}'\".format(part))\n                        continue\n\n                    for idx in range(sub_parts[0], sub_parts[1]+1):\n                        indexes.append(idx)\n                return indexes\n\n            for key, value in style.items():\n                if not key.startswith(obj_type):\n                    continue\n\n                result = re.search(col_regex, key)\n                if result:\n                    indexes = get_indexes_from_regex_match(result)\n                    if self.col_idx in indexes:\n                        obj_specific.update(value)\n                    continue\n\n                result = re.search(row_regex, key)\n                if result:\n                    indexes = get_indexes_from_regex_match(result)\n                    if self.row_idx in indexes:\n                        obj_specific.update(value)\n                    continue\n\n                result = re.search(field_regex, key)\n                if result:\n                    row_idx, col_idx = get_indexes_from_regex_match(\n                        result, True\n                    )\n                    if self.col_idx == col_idx and self.row_idx == row_idx:\n                        obj_specific.update(value)\n\n        output = {}\n        output.update(base)\n        output.update(obj_specific)\n        output.update(name_specific)\n\n        return output\n\n    @property\n    def style(self):\n        return self.get_style_for_obj_type(self.obj_type)\n\n    @property\n    def item_pos_x(self):\n        if self.parent.obj_type == \"main_frame\":\n            return int(self._pos_x)\n        return 0\n\n    @property\n    def item_pos_y(self):\n        if self.parent.obj_type == \"main_frame\":\n            return int(self._pos_y)\n        return 0\n\n    @property\n    def content_pos_x(self):\n        pos_x = self.item_pos_x\n        margin = self.style[\"margin\"]\n        margin_left = self.style.get(\"margin-left\") or margin\n\n        pos_x += margin_left\n\n        return pos_x\n\n    @property\n    def content_pos_y(self):\n        pos_y = self.item_pos_y\n        margin = self.style[\"margin\"]\n        margin_top = self.style.get(\"margin-top\") or margin\n        return pos_y + margin_top\n\n    @property\n    def value_pos_x(self):\n        pos_x = int(self.content_pos_x)\n        padding = self.style[\"padding\"]\n        padding_left = self.style.get(\"padding-left\")\n        if padding_left is None:\n            padding_left = padding\n\n        pos_x += padding_left\n\n        return pos_x\n\n    @property\n    def value_pos_y(self):\n        pos_y = int(self.content_pos_y)\n        padding = self.style[\"padding\"]\n        padding_top = self.style.get(\"padding-top\")\n        if padding_top is None:\n            padding_top = padding\n\n        pos_y += padding_top\n\n        return pos_y\n\n    @property\n    def value_pos_start(self):\n        return (self.value_pos_x, self.value_pos_y)\n\n    @property\n    def value_pos_end(self):\n        pos_x, pos_y = self.value_pos_start\n        pos_x += self.width()\n        pos_y += self.height()\n        return (pos_x, pos_y)\n\n    @property\n    def content_pos_start(self):\n        return (self.content_pos_x, self.content_pos_y)\n\n    @property\n    def content_pos_end(self):\n        pos_x, pos_y = self.content_pos_start\n        pos_x += self.content_width()\n        pos_y += self.content_height()\n        return (pos_x, pos_y)\n\n    def value_width(self):\n        raise NotImplementedError(\n            \"Attribute <content_width> is not implemented <{}>\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def value_height(self):\n        raise NotImplementedError(\n            \"Attribute <content_width> is not implemented for <{}>\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def content_width(self):\n        width = self.value_width()\n        padding = self.style[\"padding\"]\n        padding_left = self.style.get(\"padding-left\")\n        if padding_left is None:\n            padding_left = padding\n\n        padding_right = self.style.get(\"padding-right\")\n        if padding_right is None:\n            padding_right = padding\n\n        return width + padding_left + padding_right\n\n    def content_height(self):\n        height = self.value_height()\n        padding = self.style[\"padding\"]\n        padding_top = self.style.get(\"padding-top\")\n        if padding_top is None:\n            padding_top = padding\n\n        padding_bottom = self.style.get(\"padding-bottom\")\n        if padding_bottom is None:\n            padding_bottom = padding\n\n        return height + padding_top + padding_bottom\n\n    def width(self):\n        width = self.content_width()\n\n        margin = self.style[\"margin\"]\n        margin_left = self.style.get(\"margin-left\") or margin\n        margin_right = self.style.get(\"margin-right\") or margin\n\n        return width + margin_left + margin_right\n\n    def height(self):\n        height = self.content_height()\n\n        margin = self.style[\"margin\"]\n        margin_top = self.style.get(\"margin-top\") or margin\n        margin_bottom = self.style.get(\"margin-bottom\") or margin\n\n        return height + margin_bottom + margin_top\n\n    def add_item(self, item):\n        self.items[item.id] = item\n        item.fill_data_format()\n\n\n    def reset(self):\n        for item in self.items.values():\n            item.reset()\n"
  },
  {
    "path": "openpype/scripts/slates/slate_base/default_style.json",
    "content": "{\n    \"*\": {\n        \"font-family\": \"arial\",\n        \"font-size\": 26,\n        \"font-color\": \"#ffffff\",\n        \"font-bold\": false,\n        \"font-italic\": false,\n        \"bg-color\": \"#0077ff\",\n        \"alignment-horizontal\": \"left\",\n        \"alignment-vertical\": \"top\",\n        \"word-wrap\": true,\n        \"ellide\": true,\n        \"max-lines\": null\n    },\n    \"layer\": {\n        \"padding\": 0,\n        \"margin\": 0\n    },\n    \"rectangle\": {\n        \"padding\": 0,\n        \"margin\": 0,\n        \"fill\": true\n    },\n    \"image\": {\n        \"padding\": 0,\n        \"margin\": 0,\n        \"fill\": true\n    },\n    \"placeholder\": {\n        \"padding\": 0,\n        \"margin\": 0,\n        \"fill\": true\n    },\n    \"main_frame\": {\n        \"padding\": 0,\n        \"margin\": 0,\n        \"bg-color\": \"#252525\"\n    },\n    \"table\": {\n        \"padding\": 0,\n        \"margin\": 0,\n        \"bg-color\": \"transparent\"\n    },\n    \"table-item\": {\n        \"padding\": 0,\n        \"margin\": 0,\n        \"bg-color\": \"#212121\",\n        \"bg-alter-color\": \"#272727\",\n        \"font-color\": \"#dcdcdc\",\n        \"font-bold\": false,\n        \"font-italic\": false,\n        \"alignment-horizontal\": \"left\",\n        \"alignment-vertical\": \"top\",\n        \"word-wrap\": false,\n        \"ellide\": true,\n        \"max-lines\": 1\n    }\n}\n"
  },
  {
    "path": "openpype/scripts/slates/slate_base/example.py",
    "content": "# import sys\n# sys.append(r\"PATH/TO/PILLOW/PACKAGE\")\n\nfrom . import api\n\n\ndef example():\n    \"\"\"Example data to demontrate function.\n\n    It is required to fill \"destination_path\", \"thumbnail_path\"\n    and \"color_bar_path\" in `example_fill_data` to be able to execute.\n    \"\"\"\n\n    example_fill_data = {\n        \"destination_path\": \"PATH/TO/OUTPUT/FILE\",\n        \"project\": {\n            \"name\": \"Testing project\"\n        },\n        \"intent\": \"WIP\",\n        \"version_name\": \"seq01_sh0100_compositing_v01\",\n        \"date\": \"2019-08-09\",\n        \"shot_type\": \"2d comp\",\n        \"submission_note\": (\n            \"Lorem ipsum dolor sit amet, consectetuer adipiscing elit.\"\n            \" Aenean commodo ligula eget dolor. Aenean massa.\"\n            \" Cum sociis natoque penatibus et magnis dis parturient montes,\"\n            \" nascetur ridiculus mus. Donec quam felis, ultricies nec,\"\n            \" pellentesque eu, pretium quis, sem. Nulla consequat massa quis\"\n            \" enim. Donec pede justo, fringilla vel,\"\n            \" aliquet nec, vulputate eget, arcu.\"\n        ),\n        \"thumbnail_path\": \"PATH/TO/THUMBNAIL/FILE\",\n        \"color_bar_path\": \"PATH/TO/COLOR/BAR/FILE\",\n        \"vendor\": \"Our Studio\",\n        \"shot_name\": \"sh0100\",\n        \"frame_start\": 1001,\n        \"frame_end\": 1004,\n        \"duration\": 3\n    }\n\n    example_presets = {\"example_HD\": {\n        \"width\": 1920,\n        \"height\": 1080,\n        \"destination_path\": \"{destination_path}\",\n        \"style\": {\n            \"*\": {\n                \"font-family\": \"arial\",\n                \"font-color\": \"#ffffff\",\n                \"font-bold\": False,\n                \"font-italic\": False,\n                \"bg-color\": \"#0077ff\",\n                \"alignment-horizontal\": \"left\",\n                \"alignment-vertical\": \"top\"\n            },\n            \"layer\": {\n                \"padding\": 0,\n                \"margin\": 0\n            },\n            \"rectangle\": {\n                \"padding\": 0,\n                \"margin\": 0,\n                \"bg-color\": \"#E9324B\",\n                \"fill\": True\n            },\n            \"main_frame\": {\n                \"padding\": 0,\n                \"margin\": 0,\n                \"bg-color\": \"#252525\"\n            },\n            \"table\": {\n                \"padding\": 0,\n                \"margin\": 0,\n                \"bg-color\": \"transparent\"\n            },\n            \"table-item\": {\n                \"padding\": 5,\n                \"padding-bottom\": 10,\n                \"margin\": 0,\n                \"bg-color\": \"#212121\",\n                \"bg-alter-color\": \"#272727\",\n                \"font-color\": \"#dcdcdc\",\n                \"font-bold\": False,\n                \"font-italic\": False,\n                \"alignment-horizontal\": \"left\",\n                \"alignment-vertical\": \"top\",\n                \"word-wrap\": False,\n                \"ellide\": True,\n                \"max-lines\": 1\n            },\n            \"table-item-col[0]\": {\n                \"font-size\": 20,\n                \"font-color\": \"#898989\",\n                \"font-bold\": True,\n                \"ellide\": False,\n                \"word-wrap\": True,\n                \"max-lines\": None\n            },\n            \"table-item-col[1]\": {\n                \"font-size\": 40,\n                \"padding-left\": 10\n            },\n            \"#colorbar\": {\n                \"bg-color\": \"#9932CC\"\n            }\n        },\n        \"items\": [{\n            \"type\": \"layer\",\n            \"direction\": 1,\n            \"name\": \"MainLayer\",\n            \"style\": {\n                \"#MainLayer\": {\n                    \"width\": 1094,\n                    \"height\": 1000,\n                    \"margin\": 25,\n                    \"padding\": 0\n                },\n                \"#LeftSide\": {\n                    \"margin-right\": 25\n                }\n            },\n            \"items\": [{\n                \"type\": \"layer\",\n                \"name\": \"LeftSide\",\n                \"items\": [{\n                    \"type\": \"layer\",\n                    \"direction\": 1,\n                    \"style\": {\n                        \"table-item\": {\n                            \"bg-color\": \"transparent\",\n                            \"padding-bottom\": 20\n                        },\n                        \"table-item-col[0]\": {\n                            \"font-size\": 20,\n                            \"font-color\": \"#898989\",\n                            \"alignment-horizontal\": \"right\"\n                        },\n                        \"table-item-col[1]\": {\n                            \"alignment-horizontal\": \"left\",\n                            \"font-bold\": True,\n                            \"font-size\": 40\n                        }\n                    },\n                    \"items\": [{\n                        \"type\": \"table\",\n                        \"values\": [\n                            [\"Show:\", \"{project[name]}\"]\n                        ],\n                        \"style\": {\n                            \"table-item-field[0:0]\": {\n                                \"width\": 150\n                            },\n                            \"table-item-field[0:1]\": {\n                                \"width\": 580\n                            }\n                        }\n                    }, {\n                        \"type\": \"table\",\n                        \"values\": [\n                            [\"Submitting For:\", \"{intent}\"]\n                        ],\n                        \"style\": {\n                            \"table-item-field[0:0]\": {\n                                \"width\": 160\n                            },\n                            \"table-item-field[0:1]\": {\n                                \"width\": 218,\n                                \"alignment-horizontal\": \"right\"\n                            }\n                        }\n                    }]\n                }, {\n                    \"type\": \"rectangle\",\n                    \"style\": {\n                        \"bg-color\": \"#bc1015\",\n                        \"width\": 1108,\n                        \"height\": 5,\n                        \"fill\": True\n                    }\n                }, {\n                    \"type\": \"table\",\n                    \"use_alternate_color\": True,\n                    \"values\": [\n                        [\"Version name:\", \"{version_name}\"],\n                        [\"Date:\", \"{date}\"],\n                        [\"Shot Types:\", \"{shot_type}\"],\n                        [\"Submission Note:\", \"{submission_note}\"]\n                    ],\n                    \"style\": {\n                        \"table-item\": {\n                            \"padding-bottom\": 20\n                        },\n                        \"table-item-field[0:1]\": {\n                            \"font-bold\": True\n                        },\n                        \"table-item-field[3:0]\": {\n                            \"word-wrap\": True,\n                            \"ellide\": True,\n                            \"max-lines\": 4\n                        },\n                        \"table-item-col[0]\": {\n                            \"alignment-horizontal\": \"right\",\n                            \"width\": 150\n                        },\n                        \"table-item-col[1]\": {\n                            \"alignment-horizontal\": \"left\",\n                            \"width\": 958\n                        }\n                    }\n                }]\n            }, {\n                \"type\": \"layer\",\n                \"name\": \"RightSide\",\n                \"items\": [{\n                    \"type\": \"placeholder\",\n                    \"name\": \"thumbnail\",\n                    \"path\": \"{thumbnail_path}\",\n                    \"style\": {\n                        \"width\": 730,\n                        \"height\": 412\n                    }\n                }, {\n                    \"type\": \"placeholder\",\n                    \"name\": \"colorbar\",\n                    \"path\": \"{color_bar_path}\",\n                    \"return_data\": True,\n                    \"style\": {\n                        \"width\": 730,\n                        \"height\": 55\n                    }\n                }, {\n                    \"type\": \"table\",\n                    \"use_alternate_color\": True,\n                    \"values\": [\n                        [\"Vendor:\", \"{vendor}\"],\n                        [\"Shot Name:\", \"{shot_name}\"],\n                        [\"Frames:\", \"{frame_start} - {frame_end} ({duration})\"]\n                    ],\n                    \"style\": {\n                        \"table-item-col[0]\": {\n                            \"alignment-horizontal\": \"left\",\n                            \"width\": 200\n                        },\n                        \"table-item-col[1]\": {\n                            \"alignment-horizontal\": \"right\",\n                            \"width\": 530,\n                            \"font-size\": 30\n                        }\n                    }\n                }]\n            }]\n        }]\n    }}\n\n    api.create_slates(example_fill_data, \"example_HD\", example_presets)\n"
  },
  {
    "path": "openpype/scripts/slates/slate_base/font_factory.py",
    "content": "import os\nimport sys\nimport collections\n\nfrom PIL import ImageFont\n\n\nclass FontFactory:\n    fonts = None\n    default = None\n\n    @classmethod\n    def get_font(cls, family, font_size=None, italic=False, bold=False):\n        if cls.fonts is None:\n            cls.load_fonts()\n\n        styles = []\n        if bold:\n            styles.append(\"Bold\")\n\n        if italic:\n            styles.append(\"Italic\")\n\n        if not styles:\n            styles.append(\"Regular\")\n\n        style = \" \".join(styles)\n        family = family.lower()\n        family_styles = cls.fonts.get(family)\n        if not family_styles:\n            return cls.default\n\n        font = family_styles.get(style)\n        if font:\n            if font_size:\n                font = font.font_variant(size=font_size)\n            return font\n\n        # Return first found\n        for font in family_styles:\n            if font_size:\n                font = font.font_variant(size=font_size)\n            return font\n\n        return cls.default\n\n    @classmethod\n    def load_fonts(cls):\n\n        cls.default = ImageFont.load_default()\n\n        available_font_ext = [\".ttf\", \".ttc\"]\n        dirs = []\n        if sys.platform == \"win32\":\n            # check the windows font repository\n            # NOTE: must use uppercase WINDIR, to work around bugs in\n            # 1.5.2's os.environ.get()\n            windir = os.environ.get(\"WINDIR\")\n            if windir:\n                dirs.append(os.path.join(windir, \"fonts\"))\n\n        elif sys.platform in (\"linux\", \"linux2\"):\n            lindirs = os.environ.get(\"XDG_DATA_DIRS\", \"\")\n            if not lindirs:\n                # According to the freedesktop spec, XDG_DATA_DIRS should\n                # default to /usr/share\n                lindirs = \"/usr/share\"\n            dirs += [\n                os.path.join(lindir, \"fonts\") for lindir in lindirs.split(\":\")\n            ]\n\n        elif sys.platform == \"darwin\":\n            dirs += [\n                \"/Library/Fonts\",\n                \"/System/Library/Fonts\",\n                os.path.expanduser(\"~/Library/Fonts\")\n            ]\n\n        available_fonts = collections.defaultdict(dict)\n        for directory in dirs:\n            for walkroot, walkdir, walkfilenames in os.walk(directory):\n                for walkfilename in walkfilenames:\n                    ext = os.path.splitext(walkfilename)[1]\n                    if ext.lower() not in available_font_ext:\n                        continue\n\n                    fontpath = os.path.join(walkroot, walkfilename)\n                    font_obj = ImageFont.truetype(fontpath)\n                    family = font_obj.font.family.lower()\n                    style = font_obj.font.style\n                    available_fonts[family][style] = font_obj\n\n        cls.fonts = available_fonts\n"
  },
  {
    "path": "openpype/scripts/slates/slate_base/items.py",
    "content": "import os\nimport re\nfrom PIL import Image\n\nfrom .base import BaseObj\nfrom .font_factory import FontFactory\n\n\nclass BaseItem(BaseObj):\n    available_parents = [\"main_frame\", \"layer\"]\n\n    @property\n    def item_pos_x(self):\n        if self.parent.obj_type == \"main_frame\":\n            return self._pos_x\n        return self.parent.child_pos_x(self.id)\n\n    @property\n    def item_pos_y(self):\n        if self.parent.obj_type == \"main_frame\":\n            return self._pos_y\n        return self.parent.child_pos_y(self.id)\n\n    def add_item(self, *args, **kwargs):\n        raise Exception(\"Can't add item to an item, use layers instead.\")\n\n    def draw(self, image, drawer):\n        raise NotImplementedError(\n            \"Method `draw` is not implemented for <{}>\".format(\n                self.__clas__.__name__\n            )\n        )\n\n\nclass ItemImage(BaseItem):\n    obj_type = \"image\"\n\n    def __init__(self, image_path, *args, **kwargs):\n        self.image_path = image_path\n        super(ItemImage, self).__init__(*args, **kwargs)\n\n    def fill_data_format(self):\n        if re.match(self.fill_data_regex, self.image_path):\n            self.image_path = self.image_path.format(**self.fill_data)\n\n    def draw(self, image, drawer):\n        source_image = Image.open(os.path.normpath(self.image_path))\n        paste_image = source_image.resize(\n            (self.value_width(), self.value_height()),\n            Image.ANTIALIAS\n        )\n        image.paste(\n            paste_image,\n            (self.value_pos_x, self.value_pos_y)\n        )\n\n    def value_width(self):\n        return int(self.style[\"width\"])\n\n    def value_height(self):\n        return int(self.style[\"height\"])\n\n\nclass ItemRectangle(BaseItem):\n    obj_type = \"rectangle\"\n\n    def draw(self, image, drawer):\n        bg_color = self.style[\"bg-color\"]\n        fill = self.style.get(\"fill\", False)\n        kwargs = {}\n        if fill:\n            kwargs[\"fill\"] = bg_color\n        else:\n            kwargs[\"outline\"] = bg_color\n\n        start_pos_x = self.value_pos_x\n        start_pos_y = self.value_pos_y\n        end_pos_x = start_pos_x + self.value_width()\n        end_pos_y = start_pos_y + self.value_height()\n        drawer.rectangle(\n            (\n                (start_pos_x, start_pos_y),\n                (end_pos_x, end_pos_y)\n            ),\n            **kwargs\n        )\n\n    def value_width(self):\n        return int(self.style[\"width\"])\n\n    def value_height(self):\n        return int(self.style[\"height\"])\n\n\nclass ItemPlaceHolder(BaseItem):\n    obj_type = \"placeholder\"\n\n    def __init__(self, image_path, *args, **kwargs):\n        self.image_path = image_path\n        super(ItemPlaceHolder, self).__init__(*args, **kwargs)\n\n    def fill_data_format(self):\n        if re.match(self.fill_data_regex, self.image_path):\n            self.image_path = self.image_path.format(**self.fill_data)\n\n    def draw(self, image, drawer):\n        bg_color = self.style[\"bg-color\"]\n\n        kwargs = {}\n        if bg_color != \"tranparent\":\n            kwargs[\"fill\"] = bg_color\n\n        start_pos_x = self.value_pos_x\n        start_pos_y = self.value_pos_y\n        end_pos_x = start_pos_x + self.value_width()\n        end_pos_y = start_pos_y + self.value_height()\n\n        drawer.rectangle(\n            (\n                (start_pos_x, start_pos_y),\n                (end_pos_x, end_pos_y)\n            ),\n            **kwargs\n        )\n\n    def value_width(self):\n        return int(self.style[\"width\"])\n\n    def value_height(self):\n        return int(self.style[\"height\"])\n\n    def collect_data(self):\n        return {\n            \"pos_x\": self.value_pos_x,\n            \"pos_y\": self.value_pos_y,\n            \"width\": self.value_width(),\n            \"height\": self.value_height(),\n            \"path\": self.image_path\n        }\n\n\nclass ItemText(BaseItem):\n    obj_type = \"text\"\n\n    def __init__(self, value, *args, **kwargs):\n        self.value = value\n        super(ItemText, self).__init__(*args, **kwargs)\n\n    def draw(self, image, drawer):\n        bg_color = self.style[\"bg-color\"]\n        if bg_color and bg_color.lower() != \"transparent\":\n            # TODO border outline styles\n            drawer.rectangle(\n                (self.content_pos_start, self.content_pos_end),\n                fill=bg_color,\n                outline=None\n            )\n\n        font_color = self.style[\"font-color\"]\n        font_family = self.style[\"font-family\"]\n        font_size = self.style[\"font-size\"]\n        font_bold = self.style.get(\"font-bold\", False)\n        font_italic = self.style.get(\"font-italic\", False)\n\n        font = FontFactory.get_font(\n            font_family, font_size, font_italic, font_bold\n        )\n        drawer.text(\n            self.value_pos_start,\n            self.value,\n            font=font,\n            fill=font_color\n        )\n\n    def value_width(self):\n        font_family = self.style[\"font-family\"]\n        font_size = self.style[\"font-size\"]\n        font_bold = self.style.get(\"font-bold\", False)\n        font_italic = self.style.get(\"font-italic\", False)\n\n        font = FontFactory.get_font(\n            font_family, font_size, font_italic, font_bold\n        )\n        width = font.getsize(self.value)[0]\n        return int(width)\n\n    def value_height(self):\n        font_family = self.style[\"font-family\"]\n        font_size = self.style[\"font-size\"]\n        font_bold = self.style.get(\"font-bold\", False)\n        font_italic = self.style.get(\"font-italic\", False)\n\n        font = FontFactory.get_font(\n            font_family, font_size, font_italic, font_bold\n        )\n        height = font.getsize(self.value)[1]\n        return int(height)\n\n\nclass ItemTable(BaseItem):\n\n    obj_type = \"table\"\n\n    def __init__(self, values, use_alternate_color=False, *args, **kwargs):\n\n        self.values_by_cords = None\n        self.prepare_values(values)\n\n        super(ItemTable, self).__init__(*args, **kwargs)\n        self.size_values = None\n        self.calculate_sizes()\n\n        self.use_alternate_color = use_alternate_color\n\n    def add_item(self, item):\n        if item.obj_type == \"table-item\":\n            return\n        super(ItemTable, self).add_item(item)\n\n    def fill_data_format(self):\n        for item in self.values:\n            item.fill_data_format()\n\n    def prepare_values(self, _values):\n        values = []\n        values_by_cords = []\n        row_count = 0\n        col_count = 0\n        for row in _values:\n            row_count += 1\n            if len(row) > col_count:\n                col_count = len(row)\n\n        for row_idx in range(row_count):\n            values_by_cords.append([])\n            for col_idx in range(col_count):\n                values_by_cords[row_idx].append([])\n                if col_idx <= len(_values[row_idx]) - 1:\n                    col = _values[row_idx][col_idx]\n                else:\n                    col = \"\"\n\n                col_item = TableField(row_idx, col_idx, col, parent=self)\n                values_by_cords[row_idx][col_idx] = col_item\n                values.append(col_item)\n\n        self.values = values\n        self.values_by_cords = values_by_cords\n\n    def calculate_sizes(self):\n        row_heights = []\n        col_widths = []\n        for row_idx, row in enumerate(self.values_by_cords):\n            row_heights.append(0)\n            for col_idx, col_item in enumerate(row):\n                if len(col_widths) < col_idx + 1:\n                    col_widths.append(0)\n\n                _width = col_widths[col_idx]\n                item_width = col_item.width()\n                if _width < item_width:\n                    col_widths[col_idx] = item_width\n\n                _height = row_heights[row_idx]\n                item_height = col_item.height()\n                if _height < item_height:\n                    row_heights[row_idx] = item_height\n\n        self.size_values = (row_heights, col_widths)\n\n    def draw(self, image, drawer):\n        bg_color = self.style[\"bg-color\"]\n        if bg_color and bg_color.lower() != \"transparent\":\n            # TODO border outline styles\n            drawer.rectangle(\n                (self.content_pos_start, self.content_pos_end),\n                fill=bg_color,\n                outline=None\n            )\n\n        for value in self.values:\n            value.draw(image, drawer)\n\n    def value_width(self):\n        row_heights, col_widths = self.size_values\n        width = 0\n        for _width in col_widths:\n            width += _width\n\n        if width != 0:\n            width -= 1\n        return width\n\n    def value_height(self):\n        row_heights, col_widths = self.size_values\n        height = 0\n        for _height in row_heights:\n            height += _height\n\n        if height != 0:\n            height -= 1\n        return height\n\n    def content_pos_info_by_cord(self, row_idx, col_idx):\n        row_heights, col_widths = self.size_values\n        pos_x = int(self.value_pos_x)\n        pos_y = int(self.value_pos_y)\n        width = 0\n        height = 0\n        for idx, value in enumerate(col_widths):\n            if col_idx == idx:\n                width = value\n                break\n            pos_x += value\n\n        for idx, value in enumerate(row_heights):\n            if row_idx == idx:\n                height = value\n                break\n            pos_y += value\n\n        return (pos_x, pos_y, width, height)\n\n\nclass TableField(BaseItem):\n\n    obj_type = \"table-item\"\n    available_parents = [\"table\"]\n    ellide_text = \"...\"\n\n    def __init__(self, row_idx, col_idx, value, *args, **kwargs):\n        super(TableField, self).__init__(*args, **kwargs)\n        self.row_idx = row_idx\n        self.col_idx = col_idx\n        self.value = value\n\n    def recalculate_by_width(self, value, max_width):\n        padding = self.style[\"padding\"]\n        padding_left = self.style.get(\"padding-left\")\n        if padding_left is None:\n            padding_left = padding\n\n        padding_right = self.style.get(\"padding-right\")\n        if padding_right is None:\n            padding_right = padding\n\n        max_width -= (padding_left + padding_right)\n\n        if not value:\n            return \"\"\n\n        word_wrap = self.style.get(\"word-wrap\")\n        ellide = self.style.get(\"ellide\")\n        max_lines = self.style.get(\"max-lines\")\n\n        font_family = self.style[\"font-family\"]\n        font_size = self.style[\"font-size\"]\n        font_bold = self.style.get(\"font-bold\", False)\n        font_italic = self.style.get(\"font-italic\", False)\n\n        font = FontFactory.get_font(\n            font_family, font_size, font_italic, font_bold\n        )\n        val_width = font.getsize(value)[0]\n        if val_width <= max_width:\n            return value\n\n        if not ellide and not word_wrap:\n            # TODO logging\n            self.log.warning((\n                \"Can't draw text because is too long with\"\n                \" `word-wrap` and `ellide` turned off <{}>\"\n            ).format(value))\n            return \"\"\n\n        elif ellide and not word_wrap:\n            max_lines = 1\n\n        words = [word for word in value.split()]\n        words_len = len(words)\n        lines = []\n        last_index = None\n        while True:\n            start_index = 0\n            if last_index is not None:\n                start_index = int(last_index) + 1\n\n            line = \"\"\n            for idx in range(start_index, words_len):\n                _word = words[idx]\n                connector = \" \"\n                if line == \"\":\n                    connector = \"\"\n\n                _line = connector.join([line, _word])\n                _line_width = font.getsize(_line)[0]\n                if _line_width > max_width:\n                    break\n                line = _line\n                last_index = idx\n\n            if line:\n                lines.append(line)\n\n            if last_index == words_len - 1:\n                break\n\n            elif last_index is None:\n                add_message = \"\"\n                if ellide:\n                    add_message = \" String was shortened to `{}`.\"\n                    line = \"\"\n                    for idx, char in enumerate(words[idx]):\n                        _line = line + char + self.ellide_text\n                        _line_width = font.getsize(_line)[0]\n                        if _line_width > max_width:\n                            if idx == 0:\n                                line = _line\n                            break\n                        line = line + char\n\n                    lines.append(line)\n                # TODO logging\n                self.log.warning((\n                    \"Font size is too big.{} <{}>\"\n                ).format(add_message, value))\n                break\n\n        output = \"\"\n        if not lines:\n            return output\n\n        over_max_lines = (max_lines and len(lines) > max_lines)\n        if not over_max_lines:\n            return \"\\n\".join([line for line in lines])\n\n        lines = [lines[idx] for idx in range(max_lines)]\n        if not ellide:\n            return \"\\n\".join(lines)\n\n        last_line = lines[-1]\n        last_line_width = font.getsize(last_line + self.ellide_text)[0]\n        if last_line_width <= max_width:\n            lines[-1] += self.ellide_text\n            return \"\\n\".join([line for line in lines])\n\n        last_line_words = last_line.split()\n        if len(last_line_words) == 1:\n            if max_lines > 1:\n                # TODO try previous line?\n                lines[-1] = self.ellide_text\n                return \"\\n\".join([line for line in lines])\n\n            line = \"\"\n            for idx, word in enumerate(last_line_words):\n                _line = line + word + self.ellide_text\n                _line_width = font.getsize(_line)[0]\n                if _line_width > max_width:\n                    if idx == 0:\n                        line = _line\n                    break\n                line = _line\n            lines[-1] = line\n\n            return \"\\n\".join([line for line in lines])\n\n        line = \"\"\n        for idx, _word in enumerate(last_line_words):\n            connector = \" \"\n            if line == \"\":\n                connector = \"\"\n\n            _line = connector.join([line, _word + self.ellide_text])\n            _line_width = font.getsize(_line)[0]\n\n            if _line_width <= max_width:\n                line = connector.join([line, _word])\n                continue\n\n            if idx != 0:\n                line += self.ellide_text\n                break\n\n            if max_lines != 1:\n                # TODO try previous line?\n                line = self.ellide_text\n                break\n\n            for idx, char in enumerate(_word):\n                _line = line + char + self.ellide_text\n                _line_width = font.getsize(_line)[0]\n                if _line_width > max_width:\n                    if idx == 0:\n                        line = _line\n                    break\n                line = line + char\n            break\n\n        lines[-1] = line\n\n        return \"\\n\".join([line for line in lines])\n\n    def fill_data_format(self):\n        value = self.value\n        if re.match(self.fill_data_regex, value):\n            value = value.format(**self.fill_data)\n\n        self.orig_value = value\n\n        max_width = self.style.get(\"max-width\")\n        max_width = self.style.get(\"width\") or max_width\n        if max_width:\n            value = self.recalculate_by_width(value, max_width)\n\n        self.value = value\n\n    def content_width(self):\n        width = self.style.get(\"width\")\n        if width:\n            return int(width)\n        return super(TableField, self).content_width()\n\n    def content_height(self):\n        return super(TableField, self).content_height()\n\n    def value_width(self):\n        if not self.value:\n            return 0\n\n        font_family = self.style[\"font-family\"]\n        font_size = self.style[\"font-size\"]\n        font_bold = self.style.get(\"font-bold\", False)\n        font_italic = self.style.get(\"font-italic\", False)\n\n        font = FontFactory.get_font(\n            font_family, font_size, font_italic, font_bold\n        )\n        width = font.getsize_multiline(self.value)[0] + 1\n\n        min_width = self.style.get(\"min-height\")\n        if min_width and min_width > width:\n            width = min_width\n\n        return int(width)\n\n    def value_height(self):\n        if not self.value:\n            return 0\n\n        height = self.style.get(\"height\")\n        if height:\n            return int(height)\n\n        font_family = self.style[\"font-family\"]\n        font_size = self.style[\"font-size\"]\n        font_bold = self.style.get(\"font-bold\", False)\n        font_italic = self.style.get(\"font-italic\", False)\n\n        font = FontFactory.get_font(\n            font_family, font_size, font_italic, font_bold\n        )\n        height = font.getsize_multiline(self.value)[1] + 1\n\n        min_height = self.style.get(\"min-height\")\n        if min_height and min_height > height:\n            height = min_height\n\n        return int(height)\n\n    @property\n    def item_pos_x(self):\n        pos_x, pos_y, width, height = (\n            self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx)\n        )\n        return pos_x\n\n    @property\n    def item_pos_y(self):\n        pos_x, pos_y, width, height = (\n            self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx)\n        )\n        return pos_y\n\n    @property\n    def value_pos_x(self):\n        pos_x, pos_y, width, height = (\n            self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx)\n        )\n        alignment_hor = self.style[\"alignment-horizontal\"].lower()\n        if alignment_hor in [\"center\", \"centre\"]:\n            pos_x += (width - self.value_width()) / 2\n\n        elif alignment_hor == \"right\":\n            pos_x += width - self.value_width()\n\n        else:\n            padding = self.style[\"padding\"]\n            padding_left = self.style.get(\"padding-left\")\n            if padding_left is None:\n                padding_left = padding\n\n            pos_x += padding_left\n\n        return int(pos_x)\n\n    @property\n    def value_pos_y(self):\n        pos_x, pos_y, width, height = (\n            self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx)\n        )\n\n        alignment_ver = self.style[\"alignment-vertical\"].lower()\n        if alignment_ver in [\"center\", \"centre\"]:\n            pos_y += (height - self.value_height()) / 2\n\n        elif alignment_ver == \"bottom\":\n            pos_y += height - self.value_height()\n\n        else:\n            padding = self.style[\"padding\"]\n            padding_top = self.style.get(\"padding-top\")\n            if padding_top is None:\n                padding_top = padding\n\n            pos_y += padding_top\n\n        return int(pos_y)\n\n    def draw(self, image, drawer):\n        pos_x, pos_y, width, height = (\n            self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx)\n        )\n        pos_start = (pos_x, pos_y)\n        pos_end = (pos_x + width, pos_y + height)\n        bg_color = self.style[\"bg-color\"]\n        if self.parent.use_alternate_color and (self.row_idx % 2) == 1:\n            bg_color = self.style[\"bg-alter-color\"]\n\n        if bg_color and bg_color.lower() != \"transparent\":\n            # TODO border outline styles\n            drawer.rectangle(\n                (pos_start, pos_end),\n                fill=bg_color,\n                outline=None\n            )\n\n        font_color = self.style[\"font-color\"]\n        font_family = self.style[\"font-family\"]\n        font_size = self.style[\"font-size\"]\n        font_bold = self.style.get(\"font-bold\", False)\n        font_italic = self.style.get(\"font-italic\", False)\n\n        font = FontFactory.get_font(\n            font_family, font_size, font_italic, font_bold\n        )\n\n        alignment_hor = self.style[\"alignment-horizontal\"].lower()\n        if alignment_hor == \"centre\":\n            alignment_hor = \"center\"\n\n        drawer.multiline_text(\n            self.value_pos_start,\n            self.value,\n            font=font,\n            fill=font_color,\n            align=alignment_hor\n        )\n"
  },
  {
    "path": "openpype/scripts/slates/slate_base/layer.py",
    "content": "from .base import BaseObj\n\n\nclass Layer(BaseObj):\n    obj_type = \"layer\"\n    available_parents = [\"main_frame\", \"layer\"]\n\n    # Direction can be 0=vertical/ 1=horizontal\n    def __init__(self, direction=0, *args, **kwargs):\n        super(Layer, self).__init__(*args, **kwargs)\n        self._direction = direction\n\n    @property\n    def item_pos_x(self):\n        if self.parent.obj_type == self.obj_type:\n            pos_x = self.parent.child_pos_x(self.id)\n        elif self.parent.obj_type == \"main_frame\":\n            pos_x = self._pos_x\n        else:\n            pos_x = self.parent.value_pos_x\n        return int(pos_x)\n\n    @property\n    def item_pos_y(self):\n        if self.parent.obj_type == self.obj_type:\n            pos_y = self.parent.child_pos_y(self.id)\n        elif self.parent.obj_type == \"main_frame\":\n            pos_y = self._pos_y\n        else:\n            pos_y = self.parent.value_pos_y\n\n        return int(pos_y)\n\n    @property\n    def direction(self):\n        if self._direction not in (0, 1):\n            self.log.warning((\n                \"Direction of Layer must be 0 or 1 \"\n                \"(0 is horizontal / 1 is vertical)! Setting to 0.\"\n            ))\n            return 0\n        return self._direction\n\n    def child_pos_x(self, item_id):\n        pos_x = self.value_pos_x\n        alignment_hor = self.style[\"alignment-horizontal\"].lower()\n\n        item = None\n        for id, _item in self.items.items():\n            if item_id == id:\n                item = _item\n                break\n\n        if self.direction == 1:\n            for id, _item in self.items.items():\n                if item_id == id:\n                    break\n\n                pos_x += _item.width()\n                if _item.obj_type not in [\"image\", \"placeholder\"]:\n                    pos_x += 1\n\n        else:\n            if alignment_hor in [\"center\", \"centre\"]:\n                pos_x += (self.content_width() - item.content_width()) / 2\n\n            elif alignment_hor == \"right\":\n                pos_x += self.content_width() - item.content_width()\n\n            else:\n                margin = self.style[\"margin\"]\n                margin_left = self.style.get(\"margin-left\") or margin\n                pos_x += margin_left\n\n        return int(pos_x)\n\n    def child_pos_y(self, item_id):\n        pos_y = self.value_pos_y\n        alignment_ver = self.style[\"alignment-horizontal\"].lower()\n\n        item = None\n        for id, _item in self.items.items():\n            if item_id == id:\n                item = _item\n                break\n\n        if self.direction != 1:\n            for id, item in self.items.items():\n                if item_id == id:\n                    break\n                pos_y += item.height()\n                if item.obj_type not in [\"image\", \"placeholder\"]:\n                    pos_y += 1\n\n        else:\n            if alignment_ver in [\"center\", \"centre\"]:\n                pos_y += (self.content_height() - item.content_height()) / 2\n\n            elif alignment_ver == \"bottom\":\n                pos_y += self.content_height() - item.content_height()\n\n        return int(pos_y)\n\n    def value_height(self):\n        height = 0\n        for item in self.items.values():\n            if self.direction == 1:\n                if height > item.height():\n                    continue\n                # times 1 because won't get object pointer but number\n                height = item.height()\n            else:\n                height += item.height()\n\n        # TODO this is not right\n        min_height = self.style.get(\"min-height\")\n        if min_height and min_height > height:\n            return min_height\n        return height\n\n    def value_width(self):\n        width = 0\n        for item in self.items.values():\n            if self.direction == 0:\n                if width > item.width():\n                    continue\n                # times 1 because won't get object pointer but number\n                width = item.width()\n            else:\n                width += item.width()\n\n        min_width = self.style.get(\"min-width\")\n        if min_width and min_width > width:\n            return min_width\n        return width\n\n    def draw(self, image, drawer):\n        for item in self.items.values():\n            item.draw(image, drawer)\n"
  },
  {
    "path": "openpype/scripts/slates/slate_base/lib.py",
    "content": "import os\nimport json\nimport logging\ntry:\n    from queue import Queue\nexcept Exception:\n    from Queue import Queue\n\nfrom .main_frame import MainFrame\nfrom .layer import Layer\nfrom .items import (\n    ItemTable, ItemImage, ItemRectangle, ItemPlaceHolder\n)\n\nlog = logging.getLogger(__name__)\n\n\nRequiredSlateKeys = [\"width\", \"height\", \"destination_path\"]\n\n\n# TODO proper documentation\ndef create_slates(\n    fill_data, slate_name=None, slate_data=None, data_output_json=None\n):\n    \"\"\"Implmentation for command line executing.\n\n    Data for slates are by defaule taken from presets. That requires to enter,\n    `slate_name`. If `slate_data` are entered then they are used.\n\n    `data_output` should be path to json file where data will be collected.\n    \"\"\"\n    if slate_data is None and slate_name is None:\n        raise TypeError(\n            \"`create_slates` expects to enter data for slates or name\"\n            \" of slate preset.\"\n        )\n\n    elif slate_data is None:\n        slate_presets = {}\n        slate_data = slate_presets.get(slate_name)\n        if slate_data is None:\n            raise ValueError(\n                \"Preset name \\\"{}\\\" was not found in slate presets.\".format(\n                    slate_name\n                )\n            )\n\n    missing_keys = []\n    for key in RequiredSlateKeys:\n        if key not in slate_data:\n            missing_keys.append(\"`{}`\".format(key))\n\n    if missing_keys:\n        log.error(\"Slate data of <{}> miss required keys: {}\".format(\n            slate_name, \", \".join(missing_keys)\n        ))\n        return False\n\n    width = slate_data[\"width\"]\n    height = slate_data[\"height\"]\n    dst_path = slate_data[\"destination_path\"]\n    style = slate_data.get(\"style\") or {}\n\n    main = MainFrame(width, height, dst_path, fill_data, style=style)\n\n    load_queue = Queue()\n    for item in slate_data[\"items\"]:\n        load_queue.put((item, main))\n\n    while not load_queue.empty():\n        item_data, parent = load_queue.get()\n\n        item_type = item_data[\"type\"].lower()\n        item_style = item_data.get(\"style\", {})\n        item_name = item_data.get(\"name\")\n\n        pos_x = item_data.get(\"pos_x\")\n        pos_y = item_data.get(\"pos_y\")\n        if parent.obj_type != \"main_frame\":\n            if pos_x or pos_y:\n                # TODO logging\n                log.warning((\n                    \"You have specified `pos_x` and `pos_y` but won't be used.\"\n                    \" Possible only if parent of an item is `main_frame`.\"\n                ))\n            pos_x = None\n            pos_y = None\n\n        kwargs = {\n            \"parent\": parent,\n            \"style\": item_style,\n            \"name\": item_name,\n            \"pos_x\": pos_x,\n            \"pos_y\": pos_y\n        }\n\n        if item_type == \"layer\":\n            direction = item_data.get(\"direction\", 0)\n            item_obj = Layer(direction, **kwargs)\n            for item in item_data.get(\"items\", []):\n                load_queue.put((item, item_obj))\n\n        elif item_type == \"table\":\n            use_alternate_color = item_data.get(\"use_alternate_color\", False)\n            values = item_data.get(\"values\") or []\n            ItemTable(values, use_alternate_color, **kwargs)\n\n        elif item_type == \"image\":\n            path = item_data[\"path\"]\n            ItemImage(path, **kwargs)\n\n        elif item_type == \"rectangle\":\n            ItemRectangle(**kwargs)\n\n        elif item_type == \"placeholder\":\n            path = item_data[\"path\"]\n            ItemPlaceHolder(path, **kwargs)\n\n        else:\n            # TODO logging\n            log.warning(\n                \"Not implemented object type `{}` - skipping\".format(item_type)\n            )\n\n    main.draw()\n    log.debug(\"Slate creation finished\")\n\n    if not data_output_json:\n        return\n\n    if not data_output_json.endswith(\".json\"):\n        raise ValueError(\"Output path must be .json file.\")\n\n    data_output_json_dir = os.path.dirname(data_output_json)\n    if not os.path.exists(data_output_json_dir):\n        log.info(\"Creating folder \\\"{}\\\"\".format(data_output_json_dir))\n        os.makedirs(data_output_json_dir)\n\n    output_data = main.collect_data()\n    with open(data_output_json, \"w\") as json_file:\n        json_file.write(json.dumps(output_data, indent=4))\n\n    log.info(\"Metadata collected in \\\"{}\\\".\".format(data_output_json))\n"
  },
  {
    "path": "openpype/scripts/slates/slate_base/main_frame.py",
    "content": "import os\nimport re\nfrom PIL import Image, ImageDraw\n\nfrom .base import BaseObj\n\n\nclass MainFrame(BaseObj):\n\n    obj_type = \"main_frame\"\n    available_parents = [None]\n\n    def __init__(\n        self, width, height, destination_path, fill_data={}, *args, **kwargs\n    ):\n        kwargs[\"parent\"] = None\n        super(MainFrame, self).__init__(*args, **kwargs)\n        self._width = width\n        self._height = height\n        self.dst_path = destination_path\n        self._fill_data = fill_data\n        self.fill_data_format()\n\n    def fill_data_format(self):\n        if re.match(self.fill_data_regex, self.dst_path):\n            self.dst_path = self.dst_path.format(**self.fill_data)\n\n    @property\n    def fill_data(self):\n        return self._fill_data\n\n    def value_width(self):\n        width = 0\n        for item in self.items.values():\n            width += item.width()\n        return width\n\n    def value_height(self):\n        height = 0\n        for item in self.items.values():\n            height += item.height()\n        return height\n\n    def width(self):\n        return self._width\n\n    def height(self):\n        return self._height\n\n    def draw(self, path=None):\n        dir_path = os.path.dirname(self.dst_path)\n        if not os.path.exists(dir_path):\n            os.makedirs(dir_path)\n\n        bg_color = self.style[\"bg-color\"]\n        image = Image.new(\"RGB\", (self.width(), self.height()), color=bg_color)\n        drawer = ImageDraw.Draw(image)\n        for item in self.items.values():\n            item.draw(image, drawer)\n\n        image.save(self.dst_path)\n        self.reset()\n\n    def collect_data(self):\n        output = {}\n        output[\"width\"] = self.width()\n        output[\"height\"] = self.height()\n        output[\"slate_path\"] = self.dst_path\n\n        placeholders = self.find_item(obj_type=\"placeholder\")\n        placeholders_data = []\n        for placeholder in placeholders:\n            placeholders_data.append(placeholder.collect_data())\n\n        output[\"placeholders\"] = placeholders_data\n\n        return output\n"
  },
  {
    "path": "openpype/settings/__init__.py",
    "content": "from .constants import (\n    GLOBAL_SETTINGS_KEY,\n    SYSTEM_SETTINGS_KEY,\n    PROJECT_SETTINGS_KEY,\n    PROJECT_ANATOMY_KEY,\n    LOCAL_SETTING_KEY,\n\n    LEGACY_SETTINGS_VERSION,\n\n    SCHEMA_KEY_SYSTEM_SETTINGS,\n    SCHEMA_KEY_PROJECT_SETTINGS,\n\n    KEY_ALLOWED_SYMBOLS,\n    KEY_REGEX\n)\nfrom .exceptions import (\n    SaveWarningExc\n)\nfrom .lib import (\n    get_general_environments,\n    get_global_settings,\n    get_system_settings,\n    get_project_settings,\n    get_current_project_settings,\n    get_anatomy_settings,\n    get_local_settings,\n)\nfrom .entities import (\n    SystemSettings,\n    ProjectSettings,\n    DefaultsNotDefined\n)\n\n\n__all__ = (\n    \"GLOBAL_SETTINGS_KEY\",\n    \"SYSTEM_SETTINGS_KEY\",\n    \"PROJECT_SETTINGS_KEY\",\n    \"PROJECT_ANATOMY_KEY\",\n    \"LOCAL_SETTING_KEY\",\n\n    \"LEGACY_SETTINGS_VERSION\",\n\n    \"SCHEMA_KEY_SYSTEM_SETTINGS\",\n    \"SCHEMA_KEY_PROJECT_SETTINGS\",\n\n    \"KEY_ALLOWED_SYMBOLS\",\n    \"KEY_REGEX\",\n\n    \"SaveWarningExc\",\n\n    \"get_general_environments\",\n    \"get_global_settings\",\n    \"get_system_settings\",\n    \"get_project_settings\",\n    \"get_current_project_settings\",\n    \"get_anatomy_settings\",\n    \"get_local_settings\",\n\n    \"SystemSettings\",\n    \"ProjectSettings\",\n    \"DefaultsNotDefined\"\n)\n"
  },
  {
    "path": "openpype/settings/ayon_settings.py",
    "content": "\"\"\"Helper functionality to convert AYON settings to OpenPype v3 settings.\n\nThe settings are converted, so we can use v3 code with AYON settings. Once\nthe code of and addon is converted to full AYON addon which expect AYON\nsettings the conversion function can be removed.\n\nThe conversion is hardcoded -> there is no other way how to achieve the result.\n\nMain entrypoints are functions:\n- convert_project_settings - convert settings to project settings\n- convert_system_settings - convert settings to system settings\n# Both getters cache values\n- get_ayon_project_settings - replacement for 'get_project_settings'\n- get_ayon_system_settings - replacement for 'get_system_settings'\n\"\"\"\nimport os\nimport collections\nimport json\nimport copy\nimport time\n\nimport six\n\nfrom openpype.client import get_ayon_server_api_connection\n\n\ndef _convert_color(color_value):\n    if isinstance(color_value, six.string_types):\n        color_value = color_value.lstrip(\"#\")\n        color_value_len = len(color_value)\n        _color_value = []\n        for idx in range(color_value_len // 2):\n            _color_value.append(int(color_value[idx:idx + 2], 16))\n        for _ in range(4 - len(_color_value)):\n            _color_value.append(255)\n        return _color_value\n\n    if isinstance(color_value, list):\n        # WARNING R,G,B can be 'int' or 'float'\n        # - 'float' variant is using 'int' for min: 0 and max: 1\n        if len(color_value) == 3:\n            # Add alpha\n            color_value.append(255)\n        else:\n            # Convert float alha to int\n            alpha = int(color_value[3] * 255)\n            if alpha > 255:\n                alpha = 255\n            elif alpha < 0:\n                alpha = 0\n            color_value[3] = alpha\n    return color_value\n\n\ndef _convert_host_imageio(host_settings):\n    if \"imageio\" not in host_settings:\n        return\n\n    # --- imageio ---\n    ayon_imageio = host_settings[\"imageio\"]\n    # TODO remove when fixed on server\n    if \"ocio_config\" in ayon_imageio[\"ocio_config\"]:\n        ayon_imageio[\"ocio_config\"][\"filepath\"] = (\n            ayon_imageio[\"ocio_config\"].pop(\"ocio_config\")\n        )\n    # Convert file rules\n    imageio_file_rules = ayon_imageio[\"file_rules\"]\n    new_rules = {}\n    for rule in imageio_file_rules[\"rules\"]:\n        name = rule.pop(\"name\")\n        new_rules[name] = rule\n    imageio_file_rules[\"rules\"] = new_rules\n\n\ndef _convert_applications_groups(groups, clear_metadata):\n    environment_key = \"environment\"\n    if isinstance(groups, dict):\n        new_groups = []\n        for name, item in groups.items():\n            item[\"name\"] = name\n            new_groups.append(item)\n        groups = new_groups\n\n    output = {}\n    group_dynamic_labels = {}\n    for group in groups:\n        group_name = group.pop(\"name\")\n        if \"label\" in group:\n            group_dynamic_labels[group_name] = group[\"label\"]\n\n        tool_group_envs = group[environment_key]\n        if isinstance(tool_group_envs, six.string_types):\n            group[environment_key] = json.loads(tool_group_envs)\n\n        variants = {}\n        variant_dynamic_labels = {}\n        for variant in group.pop(\"variants\"):\n            variant_name = variant.pop(\"name\")\n            label = variant.get(\"label\")\n            if label and label != variant_name:\n                variant_dynamic_labels[variant_name] = label\n            variant_envs = variant[environment_key]\n            if isinstance(variant_envs, six.string_types):\n                variant[environment_key] = json.loads(variant_envs)\n            variants[variant_name] = variant\n        group[\"variants\"] = variants\n\n        if not clear_metadata:\n            variants[\"__dynamic_keys_labels__\"] = variant_dynamic_labels\n        output[group_name] = group\n\n    if not clear_metadata:\n        output[\"__dynamic_keys_labels__\"] = group_dynamic_labels\n    return output\n\n\ndef _convert_applications_system_settings(\n    ayon_settings, output, clear_metadata\n):\n    # Addon settings\n    addon_settings = ayon_settings[\"applications\"]\n\n    # Remove project settings\n    addon_settings.pop(\"only_available\", None)\n\n    # Applications settings\n    ayon_apps = addon_settings[\"applications\"]\n\n    additional_apps = ayon_apps.pop(\"additional_apps\")\n    applications = _convert_applications_groups(\n        ayon_apps, clear_metadata\n    )\n    applications[\"additional_apps\"] = _convert_applications_groups(\n        additional_apps, clear_metadata\n    )\n\n    # Tools settings\n    tools = _convert_applications_groups(\n        addon_settings[\"tool_groups\"], clear_metadata\n    )\n\n    output[\"applications\"] = applications\n    output[\"tools\"] = {\"tool_groups\": tools}\n\n\ndef _convert_general(ayon_settings, output, default_settings):\n    # TODO get studio name/code\n    core_settings = ayon_settings[\"core\"]\n    environments = core_settings[\"environments\"]\n    if isinstance(environments, six.string_types):\n        environments = json.loads(environments)\n\n    general = default_settings[\"general\"]\n    general.update({\n        \"log_to_server\": False,\n        \"studio_name\": core_settings[\"studio_name\"],\n        \"studio_code\": core_settings[\"studio_code\"],\n        \"environment\": environments\n    })\n    output[\"general\"] = general\n\n\ndef _convert_kitsu_system_settings(\n    ayon_settings, output, addon_versions, default_settings\n):\n    enabled = addon_versions.get(\"kitsu\") is not None\n    kitsu_settings = default_settings[\"modules\"][\"kitsu\"]\n    kitsu_settings[\"enabled\"] = enabled\n    if enabled:\n        kitsu_settings[\"server\"] = ayon_settings[\"kitsu\"][\"server\"]\n    output[\"modules\"][\"kitsu\"] = kitsu_settings\n\n\ndef _convert_timers_manager_system_settings(\n    ayon_settings, output, addon_versions, default_settings\n):\n    enabled = addon_versions.get(\"timers_manager\") is not None\n    manager_settings = default_settings[\"modules\"][\"timers_manager\"]\n    manager_settings[\"enabled\"] = enabled\n    if enabled:\n        ayon_manager = ayon_settings[\"timers_manager\"]\n        manager_settings.update({\n            key: ayon_manager[key]\n            for key in {\n                \"auto_stop\",\n                \"full_time\",\n                \"message_time\",\n                \"disregard_publishing\"\n            }\n        })\n    output[\"modules\"][\"timers_manager\"] = manager_settings\n\n\ndef _convert_clockify_system_settings(\n    ayon_settings, output, addon_versions, default_settings\n):\n    enabled = addon_versions.get(\"clockify\") is not None\n    clockify_settings = default_settings[\"modules\"][\"clockify\"]\n    clockify_settings[\"enabled\"] = enabled\n    if enabled:\n        clockify_settings[\"workspace_name\"] = (\n            ayon_settings[\"clockify\"][\"workspace_name\"]\n        )\n    output[\"modules\"][\"clockify\"] = clockify_settings\n\n\ndef _convert_deadline_system_settings(\n    ayon_settings, output, addon_versions, default_settings\n):\n    enabled = addon_versions.get(\"deadline\") is not None\n    deadline_settings = default_settings[\"modules\"][\"deadline\"]\n    deadline_settings[\"enabled\"] = enabled\n    if enabled:\n        ayon_deadline = ayon_settings[\"deadline\"]\n        deadline_settings[\"deadline_urls\"] = {\n            item[\"name\"]: item[\"value\"]\n            for item in ayon_deadline[\"deadline_urls\"]\n        }\n\n    output[\"modules\"][\"deadline\"] = deadline_settings\n\n\ndef _convert_royalrender_system_settings(\n    ayon_settings, output, addon_versions, default_settings\n):\n    enabled = addon_versions.get(\"royalrender\") is not None\n    rr_settings = default_settings[\"modules\"][\"royalrender\"]\n    rr_settings[\"enabled\"] = enabled\n    if enabled:\n        ayon_royalrender = ayon_settings[\"royalrender\"]\n        rr_settings[\"rr_paths\"] = {\n            item[\"name\"]: item[\"value\"]\n            for item in ayon_royalrender[\"rr_paths\"]\n        }\n    output[\"modules\"][\"royalrender\"] = rr_settings\n\n\ndef _convert_modules_system(\n    ayon_settings, output, addon_versions, default_settings\n):\n    # TODO add all modules\n    # TODO add 'enabled' values\n    for func in (\n        _convert_kitsu_system_settings,\n        _convert_timers_manager_system_settings,\n        _convert_clockify_system_settings,\n        _convert_deadline_system_settings,\n        _convert_royalrender_system_settings,\n    ):\n        func(ayon_settings, output, addon_versions, default_settings)\n\n    modules_settings = output[\"modules\"]\n    for module_name in (\n        \"sync_server\",\n        \"log_viewer\",\n        \"standalonepublish_tool\",\n        \"project_manager\",\n        \"job_queue\",\n        \"avalon\",\n        \"addon_paths\",\n    ):\n        settings = default_settings[\"modules\"][module_name]\n        if \"enabled\" in settings:\n            settings[\"enabled\"] = False\n        modules_settings[module_name] = settings\n\n    for key, value in ayon_settings.items():\n        if key not in output:\n            output[key] = value\n\n        # Make sure addons have access to settings in initialization\n        # - ModulesManager passes only modules settings into initialization\n        if key not in modules_settings:\n            modules_settings[key] = value\n\n\ndef is_dev_mode_enabled():\n    \"\"\"Dev mode is enabled in AYON.\n\n    Returns:\n        bool: True if dev mode is enabled.\n    \"\"\"\n\n    return os.getenv(\"AYON_USE_DEV\") == \"1\"\n\n\ndef convert_system_settings(ayon_settings, default_settings, addon_versions):\n    default_settings = copy.deepcopy(default_settings)\n    output = {\n        \"modules\": {}\n    }\n    if \"applications\" in ayon_settings:\n        _convert_applications_system_settings(ayon_settings, output, False)\n\n    if \"core\" in ayon_settings:\n        _convert_general(ayon_settings, output, default_settings)\n\n    for key, value in ayon_settings.items():\n        if key not in output:\n            output[key] = value\n\n    for key, value in default_settings.items():\n        if key not in output:\n            output[key] = value\n\n    _convert_modules_system(\n        ayon_settings,\n        output,\n        addon_versions,\n        default_settings\n    )\n    return output\n\n\n# --------- Project settings ---------\ndef _convert_applications_project_settings(ayon_settings, output):\n    if \"applications\" not in ayon_settings:\n        return\n\n    output[\"applications\"] = {\n        \"only_available\": ayon_settings[\"applications\"][\"only_available\"]\n    }\n\n\ndef _convert_blender_project_settings(ayon_settings, output):\n    if \"blender\" not in ayon_settings:\n        return\n    ayon_blender = ayon_settings[\"blender\"]\n    _convert_host_imageio(ayon_blender)\n\n    ayon_publish = ayon_blender[\"publish\"]\n\n    for plugin in (\"ExtractThumbnail\", \"ExtractPlayblast\"):\n        plugin_settings = ayon_publish[plugin]\n        plugin_settings[\"presets\"] = json.loads(plugin_settings[\"presets\"])\n\n    output[\"blender\"] = ayon_blender\n\n\ndef _convert_celaction_project_settings(ayon_settings, output):\n    if \"celaction\" not in ayon_settings:\n        return\n\n    ayon_celaction = ayon_settings[\"celaction\"]\n    _convert_host_imageio(ayon_celaction)\n\n    output[\"celaction\"] = ayon_celaction\n\n\ndef _convert_flame_project_settings(ayon_settings, output):\n    if \"flame\" not in ayon_settings:\n        return\n\n    ayon_flame = ayon_settings[\"flame\"]\n\n    ayon_publish_flame = ayon_flame[\"publish\"]\n    # Plugin 'ExtractSubsetResources' renamed to 'ExtractProductResources'\n    if \"ExtractSubsetResources\" in ayon_publish_flame:\n        ayon_product_resources = ayon_publish_flame[\"ExtractSubsetResources\"]\n    else:\n        ayon_product_resources = (\n            ayon_publish_flame.pop(\"ExtractProductResources\"))\n        ayon_publish_flame[\"ExtractSubsetResources\"] = ayon_product_resources\n\n    # 'ExtractSubsetResources' changed model of 'export_presets_mapping'\n    # - some keys were moved under 'other_parameters'\n    new_subset_resources = {}\n    for item in ayon_product_resources.pop(\"export_presets_mapping\"):\n        name = item.pop(\"name\")\n        if \"other_parameters\" in item:\n            other_parameters = item.pop(\"other_parameters\")\n            item.update(other_parameters)\n        new_subset_resources[name] = item\n\n    ayon_product_resources[\"export_presets_mapping\"] = new_subset_resources\n\n    # 'imageio' changed model\n    # - missing subkey 'project' which is in root of 'imageio' model\n    _convert_host_imageio(ayon_flame)\n    ayon_imageio_flame = ayon_flame[\"imageio\"]\n    if \"project\" not in ayon_imageio_flame:\n        profile_mapping = ayon_imageio_flame.pop(\"profilesMapping\")\n        ayon_flame[\"imageio\"] = {\n            \"project\": ayon_imageio_flame,\n            \"profilesMapping\": profile_mapping\n        }\n\n    ayon_load_flame = ayon_flame[\"load\"]\n    for plugin_name in (\"LoadClip\", \"LoadClipBatch\"):\n        plugin_settings = ayon_load_flame[plugin_name]\n        plugin_settings[\"families\"] = plugin_settings.pop(\"product_types\")\n        plugin_settings[\"clip_name_template\"] = (\n            plugin_settings[\"clip_name_template\"]\n            .replace(\"{folder[name]}\", \"{asset}\")\n            .replace(\"{product[name]}\", \"{subset}\")\n        )\n        plugin_settings[\"layer_rename_template\"] = (\n            plugin_settings[\"layer_rename_template\"]\n            .replace(\"{folder[name]}\", \"{asset}\")\n            .replace(\"{product[name]}\", \"{subset}\")\n        )\n\n    output[\"flame\"] = ayon_flame\n\n\ndef _convert_fusion_project_settings(ayon_settings, output):\n    if \"fusion\" not in ayon_settings:\n        return\n\n    ayon_fusion = ayon_settings[\"fusion\"]\n    _convert_host_imageio(ayon_fusion)\n\n    ayon_imageio_fusion = ayon_fusion[\"imageio\"]\n\n    if \"ocioSettings\" in ayon_imageio_fusion:\n        ayon_ocio_setting = ayon_imageio_fusion.pop(\"ocioSettings\")\n        paths = ayon_ocio_setting.pop(\"ocioPathModel\")\n        for key, value in tuple(paths.items()):\n            new_value = []\n            if value:\n                new_value.append(value)\n            paths[key] = new_value\n\n        ayon_ocio_setting[\"configFilePath\"] = paths\n        ayon_imageio_fusion[\"ocio\"] = ayon_ocio_setting\n    elif \"ocio\" in ayon_imageio_fusion:\n        paths = ayon_imageio_fusion[\"ocio\"].pop(\"configFilePath\")\n        for key, value in tuple(paths.items()):\n            new_value = []\n            if value:\n                new_value.append(value)\n            paths[key] = new_value\n        ayon_imageio_fusion[\"ocio\"][\"configFilePath\"] = paths\n\n    _convert_host_imageio(ayon_imageio_fusion)\n\n    ayon_create_saver = ayon_fusion[\"create\"][\"CreateSaver\"]\n    ayon_create_saver[\"temp_rendering_path_template\"] = (\n        ayon_create_saver[\"temp_rendering_path_template\"]\n        .replace(\"{product[name]}\", \"{subset}\")\n        .replace(\"{product[type]}\", \"{family}\")\n        .replace(\"{folder[name]}\", \"{asset}\")\n        .replace(\"{task[name]}\", \"{task}\")\n    )\n\n    output[\"fusion\"] = ayon_fusion\n\n\ndef _convert_maya_project_settings(ayon_settings, output):\n    if \"maya\" not in ayon_settings:\n        return\n\n    ayon_maya = ayon_settings[\"maya\"]\n\n    # Change key of render settings\n    ayon_maya[\"RenderSettings\"] = ayon_maya.pop(\"render_settings\")\n\n    # Convert extensions mapping\n    ayon_maya[\"ext_mapping\"] = {\n        item[\"name\"]: item[\"value\"]\n        for item in ayon_maya[\"ext_mapping\"]\n    }\n\n    # Maya dirmap\n    ayon_maya_dirmap = ayon_maya.pop(\"maya_dirmap\")\n    ayon_maya_dirmap_path = ayon_maya_dirmap[\"paths\"]\n    ayon_maya_dirmap_path[\"source-path\"] = (\n        ayon_maya_dirmap_path.pop(\"source_path\")\n    )\n    ayon_maya_dirmap_path[\"destination-path\"] = (\n        ayon_maya_dirmap_path.pop(\"destination_path\")\n    )\n    ayon_maya[\"maya-dirmap\"] = ayon_maya_dirmap\n\n    # Create plugins\n    ayon_create = ayon_maya[\"create\"]\n    ayon_create_static_mesh = ayon_create[\"CreateUnrealStaticMesh\"]\n    if \"static_mesh_prefixes\" in ayon_create_static_mesh:\n        ayon_create_static_mesh[\"static_mesh_prefix\"] = (\n            ayon_create_static_mesh.pop(\"static_mesh_prefixes\")\n        )\n\n    # --- Publish (START) ---\n    ayon_publish = ayon_maya[\"publish\"]\n    try:\n        attributes = json.loads(\n            ayon_publish[\"ValidateAttributes\"][\"attributes\"]\n        )\n    except ValueError:\n        attributes = {}\n    ayon_publish[\"ValidateAttributes\"][\"attributes\"] = attributes\n\n    try:\n        SUFFIX_NAMING_TABLE = json.loads(\n            ayon_publish\n            [\"ValidateTransformNamingSuffix\"]\n            [\"SUFFIX_NAMING_TABLE\"]\n        )\n    except ValueError:\n        SUFFIX_NAMING_TABLE = {}\n    ayon_publish[\"ValidateTransformNamingSuffix\"][\"SUFFIX_NAMING_TABLE\"] = (\n        SUFFIX_NAMING_TABLE\n    )\n\n    validate_frame_range = ayon_publish[\"ValidateFrameRange\"]\n    if \"exclude_product_types\" in validate_frame_range:\n        validate_frame_range[\"exclude_families\"] = (\n            validate_frame_range.pop(\"exclude_product_types\"))\n\n    # Extract playblast capture settings\n    validate_rendern_settings = ayon_publish[\"ValidateRenderSettings\"]\n    for key in (\n        \"arnold_render_attributes\",\n        \"vray_render_attributes\",\n        \"redshift_render_attributes\",\n        \"renderman_render_attributes\",\n    ):\n        if key not in validate_rendern_settings:\n            continue\n        validate_rendern_settings[key] = [\n            [item[\"type\"], item[\"value\"]]\n            for item in validate_rendern_settings[key]\n        ]\n\n    plugin_path_attributes = ayon_publish[\"ValidatePluginPathAttributes\"]\n    plugin_path_attributes[\"attribute\"] = {\n        item[\"name\"]: item[\"value\"]\n        for item in plugin_path_attributes[\"attribute\"]\n    }\n\n    ayon_capture_preset = ayon_publish[\"ExtractPlayblast\"][\"capture_preset\"]\n    display_options = ayon_capture_preset[\"DisplayOptions\"]\n    for key in (\"background\", \"backgroundBottom\", \"backgroundTop\"):\n        display_options[key] = _convert_color(display_options[key])\n\n    for src_key, dst_key in (\n        (\"DisplayOptions\", \"Display Options\"),\n        (\"ViewportOptions\", \"Viewport Options\"),\n        (\"CameraOptions\", \"Camera Options\"),\n    ):\n        ayon_capture_preset[dst_key] = ayon_capture_preset.pop(src_key)\n\n    viewport_options = ayon_capture_preset[\"Viewport Options\"]\n    viewport_options[\"pluginObjects\"] = {\n        item[\"name\"]: item[\"value\"]\n        for item in viewport_options[\"pluginObjects\"]\n    }\n\n    ayon_playblast_settings = ayon_publish[\"ExtractPlayblast\"][\"profiles\"]\n    if ayon_playblast_settings:\n        for setting in ayon_playblast_settings:\n            capture_preset = setting[\"capture_preset\"]\n            display_options = capture_preset[\"DisplayOptions\"]\n            for key in (\"background\", \"backgroundBottom\", \"backgroundTop\"):\n                display_options[key] = _convert_color(display_options[key])\n\n            for src_key, dst_key in (\n                (\"DisplayOptions\", \"Display Options\"),\n                (\"ViewportOptions\", \"Viewport Options\"),\n                (\"CameraOptions\", \"Camera Options\"),\n            ):\n                capture_preset[dst_key] = capture_preset.pop(src_key)\n\n            viewport_options = capture_preset[\"Viewport Options\"]\n            viewport_options[\"pluginObjects\"] = {\n                item[\"name\"]: item[\"value\"]\n                for item in viewport_options[\"pluginObjects\"]\n            }\n\n    # Extract Camera Alembic bake attributes\n    try:\n        bake_attributes = json.loads(\n            ayon_publish[\"ExtractCameraAlembic\"][\"bake_attributes\"]\n        )\n    except ValueError:\n        bake_attributes = []\n    ayon_publish[\"ExtractCameraAlembic\"][\"bake_attributes\"] = bake_attributes\n\n    # --- Publish (END) ---\n    for renderer_settings in ayon_maya[\"RenderSettings\"].values():\n        if (\n            not isinstance(renderer_settings, dict)\n            or \"additional_options\" not in renderer_settings\n        ):\n            continue\n        renderer_settings[\"additional_options\"] = [\n            [item[\"attribute\"], item[\"value\"]]\n            for item in renderer_settings[\"additional_options\"]\n        ]\n\n    # Workfile build\n    ayon_workfile_build = ayon_maya[\"workfile_build\"]\n    for item in ayon_workfile_build[\"profiles\"]:\n        for key in (\"current_context\", \"linked_assets\"):\n            for subitem in item[key]:\n                if \"families\" in subitem:\n                    break\n                subitem[\"families\"] = subitem.pop(\"product_types\")\n                subitem[\"subset_name_filters\"] = subitem.pop(\n                    \"product_name_filters\")\n\n    _convert_host_imageio(ayon_maya)\n\n    ayon_maya_load = ayon_maya[\"load\"]\n    load_colors = ayon_maya_load[\"colors\"]\n    for key, color in tuple(load_colors.items()):\n        load_colors[key] = _convert_color(color)\n\n    reference_loader = ayon_maya_load[\"reference_loader\"]\n    reference_loader[\"namespace\"] = (\n        reference_loader[\"namespace\"]\n        .replace(\"{product[name]}\", \"{subset}\")\n    )\n\n    if ayon_maya_load.get(\"import_loader\"):\n        import_loader = ayon_maya_load[\"import_loader\"]\n        import_loader[\"namespace\"] = (\n            import_loader[\"namespace\"]\n            .replace(\"{product[name]}\", \"{subset}\")\n        )\n\n    output[\"maya\"] = ayon_maya\n\n\ndef _convert_3dsmax_project_settings(ayon_settings, output):\n    if \"max\" not in ayon_settings:\n        return\n\n    ayon_max = ayon_settings[\"max\"]\n    _convert_host_imageio(ayon_max)\n    if \"PointCloud\" in ayon_max:\n        point_cloud_attribute = ayon_max[\"PointCloud\"][\"attribute\"]\n        new_point_cloud_attribute = {\n            item[\"name\"]: item[\"value\"]\n            for item in point_cloud_attribute\n        }\n        ayon_max[\"PointCloud\"][\"attribute\"] = new_point_cloud_attribute\n    # --- Publish (START) ---\n    ayon_publish = ayon_max[\"publish\"]\n    if \"ValidateAttributes\" in ayon_publish:\n        try:\n            attributes = json.loads(\n                ayon_publish[\"ValidateAttributes\"][\"attributes\"]\n            )\n        except ValueError:\n            attributes = {}\n        ayon_publish[\"ValidateAttributes\"][\"attributes\"] = attributes\n\n    if \"ValidateLoadedPlugin\" in ayon_publish:\n        loaded_plugin = (\n            ayon_publish[\"ValidateLoadedPlugin\"][\"family_plugins_mapping\"]\n        )\n        for item in loaded_plugin:\n            item[\"families\"] = item.pop(\"product_types\")\n\n    output[\"max\"] = ayon_max\n\n\ndef _convert_nuke_knobs(knobs):\n    new_knobs = []\n    for knob in knobs:\n        knob_type = knob[\"type\"]\n\n        if knob_type == \"boolean\":\n            knob_type = \"bool\"\n\n        if knob_type != \"bool\":\n            value = knob[knob_type]\n        elif knob_type in knob:\n            value = knob[knob_type]\n        else:\n            value = knob[\"boolean\"]\n\n        new_knob = {\n            \"type\": knob_type,\n            \"name\": knob[\"name\"],\n        }\n        new_knobs.append(new_knob)\n\n        if knob_type == \"formatable\":\n            new_knob[\"template\"] = value[\"template\"]\n            new_knob[\"to_type\"] = value[\"to_type\"]\n            continue\n\n        value_key = \"value\"\n        if knob_type == \"expression\":\n            value_key = \"expression\"\n\n        elif knob_type == \"color_gui\":\n            value = _convert_color(value)\n\n        elif knob_type == \"vector_2d\":\n            value = [value[\"x\"], value[\"y\"]]\n\n        elif knob_type == \"vector_3d\":\n            value = [value[\"x\"], value[\"y\"], value[\"z\"]]\n\n        elif knob_type == \"box\":\n            value = [value[\"x\"], value[\"y\"], value[\"r\"], value[\"t\"]]\n\n        new_knob[value_key] = value\n    return new_knobs\n\n\ndef _convert_nuke_project_settings(ayon_settings, output):\n    if \"nuke\" not in ayon_settings:\n        return\n\n    ayon_nuke = ayon_settings[\"nuke\"]\n\n    # --- Dirmap ---\n    dirmap = ayon_nuke.pop(\"dirmap\")\n    for src_key, dst_key in (\n        (\"source_path\", \"source-path\"),\n        (\"destination_path\", \"destination-path\"),\n    ):\n        dirmap[\"paths\"][dst_key] = dirmap[\"paths\"].pop(src_key)\n    ayon_nuke[\"nuke-dirmap\"] = dirmap\n\n    # --- Load ---\n    ayon_load = ayon_nuke[\"load\"]\n    ayon_load[\"LoadClip\"][\"_representations\"] = (\n        ayon_load[\"LoadClip\"].pop(\"representations_include\")\n    )\n    ayon_load[\"LoadImage\"][\"_representations\"] = (\n        ayon_load[\"LoadImage\"].pop(\"representations_include\")\n    )\n\n    # --- Create ---\n    ayon_create = ayon_nuke[\"create\"]\n    for creator_name in (\n        \"CreateWritePrerender\",\n        \"CreateWriteImage\",\n        \"CreateWriteRender\",\n    ):\n        create_plugin_settings = ayon_create[creator_name]\n        create_plugin_settings[\"temp_rendering_path_template\"] = (\n            create_plugin_settings[\"temp_rendering_path_template\"]\n            .replace(\"{product[name]}\", \"{subset}\")\n            .replace(\"{product[type]}\", \"{family}\")\n            .replace(\"{task[name]}\", \"{task}\")\n            .replace(\"{folder[name]}\", \"{asset}\")\n        )\n        new_prenodes = {}\n        for prenode in create_plugin_settings[\"prenodes\"]:\n            name = prenode.pop(\"name\")\n            prenode[\"knobs\"] = _convert_nuke_knobs(prenode[\"knobs\"])\n            new_prenodes[name] = prenode\n\n        create_plugin_settings[\"prenodes\"] = new_prenodes\n\n    # --- Publish ---\n    ayon_publish = ayon_nuke[\"publish\"]\n    slate_mapping = ayon_publish[\"ExtractSlateFrame\"][\"key_value_mapping\"]\n    for key in tuple(slate_mapping.keys()):\n        value = slate_mapping[key]\n        slate_mapping[key] = [value[\"enabled\"], value[\"template\"]]\n\n    ayon_publish[\"ValidateKnobs\"][\"knobs\"] = json.loads(\n        ayon_publish[\"ValidateKnobs\"][\"knobs\"]\n    )\n\n    new_review_data_outputs = {}\n    outputs_settings = []\n    # Check deprecated ExtractReviewDataMov\n    # settings for backwards compatibility\n    deprecrated_review_settings = ayon_publish[\"ExtractReviewDataMov\"]\n    current_review_settings = (\n        ayon_publish.get(\"ExtractReviewIntermediates\")\n    )\n    if deprecrated_review_settings[\"enabled\"]:\n        outputs_settings = deprecrated_review_settings[\"outputs\"]\n    elif current_review_settings is None:\n        pass\n    elif current_review_settings[\"enabled\"]:\n        outputs_settings = current_review_settings[\"outputs\"]\n\n    for item in outputs_settings:\n        item_filter = item[\"filter\"]\n        if \"product_names\" in item_filter:\n            item_filter[\"subsets\"] = item_filter.pop(\"product_names\")\n            item_filter[\"families\"] = item_filter.pop(\"product_types\")\n\n        reformat_nodes_config = item.get(\"reformat_nodes_config\") or {}\n        reposition_nodes = reformat_nodes_config.get(\n            \"reposition_nodes\") or []\n\n        for reposition_node in reposition_nodes:\n            if \"knobs\" not in reposition_node:\n                continue\n            reposition_node[\"knobs\"] = _convert_nuke_knobs(\n                reposition_node[\"knobs\"]\n            )\n\n        name = item.pop(\"name\")\n        new_review_data_outputs[name] = item\n\n    if deprecrated_review_settings[\"enabled\"]:\n        deprecrated_review_settings[\"outputs\"] = new_review_data_outputs\n    elif current_review_settings[\"enabled\"]:\n        current_review_settings[\"outputs\"] = new_review_data_outputs\n\n    collect_instance_data = ayon_publish[\"CollectInstanceData\"]\n    if \"sync_workfile_version_on_product_types\" in collect_instance_data:\n        collect_instance_data[\"sync_workfile_version_on_families\"] = (\n            collect_instance_data.pop(\n                \"sync_workfile_version_on_product_types\"))\n\n    # --- ImageIO ---\n    # NOTE 'monitorOutLut' is maybe not yet in v3 (ut should be)\n    _convert_host_imageio(ayon_nuke)\n    ayon_imageio = ayon_nuke[\"imageio\"]\n\n    # workfile\n    imageio_workfile = ayon_imageio[\"workfile\"]\n    workfile_keys_mapping = (\n        (\"color_management\", \"colorManagement\"),\n        (\"native_ocio_config\", \"OCIO_config\"),\n        (\"working_space\", \"workingSpaceLUT\"),\n        (\"thumbnail_space\", \"monitorLut\"),\n    )\n    for src, dst in workfile_keys_mapping:\n        if (\n            src in imageio_workfile\n            and dst not in imageio_workfile\n        ):\n            imageio_workfile[dst] = imageio_workfile.pop(src)\n\n    # regex inputs\n    if \"regex_inputs\" in ayon_imageio:\n        ayon_imageio[\"regexInputs\"] = ayon_imageio.pop(\"regex_inputs\")\n\n    # nodes\n    ayon_imageio_nodes = ayon_imageio[\"nodes\"]\n    if \"required_nodes\" in ayon_imageio_nodes:\n        ayon_imageio_nodes[\"requiredNodes\"] = (\n            ayon_imageio_nodes.pop(\"required_nodes\"))\n    if \"override_nodes\" in ayon_imageio_nodes:\n        ayon_imageio_nodes[\"overrideNodes\"] = (\n            ayon_imageio_nodes.pop(\"override_nodes\"))\n\n    for item in ayon_imageio_nodes[\"requiredNodes\"]:\n        if \"nuke_node_class\" in item:\n            item[\"nukeNodeClass\"] = item.pop(\"nuke_node_class\")\n        item[\"knobs\"] = _convert_nuke_knobs(item[\"knobs\"])\n\n    for item in ayon_imageio_nodes[\"overrideNodes\"]:\n        if \"nuke_node_class\" in item:\n            item[\"nukeNodeClass\"] = item.pop(\"nuke_node_class\")\n        item[\"knobs\"] = _convert_nuke_knobs(item[\"knobs\"])\n\n    output[\"nuke\"] = ayon_nuke\n\n\ndef _convert_hiero_project_settings(ayon_settings, output):\n    if \"hiero\" not in ayon_settings:\n        return\n\n    ayon_hiero = ayon_settings[\"hiero\"]\n    _convert_host_imageio(ayon_hiero)\n\n    new_gui_filters = {}\n    for item in ayon_hiero.pop(\"filters\", []):\n        subvalue = {}\n        key = item[\"name\"]\n        for subitem in item[\"value\"]:\n            subvalue[subitem[\"name\"]] = subitem[\"value\"]\n        new_gui_filters[key] = subvalue\n    ayon_hiero[\"filters\"] = new_gui_filters\n\n    ayon_load_clip = ayon_hiero[\"load\"][\"LoadClip\"]\n    if \"product_types\" in ayon_load_clip:\n        ayon_load_clip[\"families\"] = ayon_load_clip.pop(\"product_types\")\n\n    ayon_load_clip = ayon_hiero[\"load\"][\"LoadClip\"]\n    ayon_load_clip[\"clip_name_template\"] = (\n        ayon_load_clip[\"clip_name_template\"]\n        .replace(\"{folder[name]}\", \"{asset}\")\n        .replace(\"{product[name]}\", \"{subset}\")\n    )\n\n    output[\"hiero\"] = ayon_hiero\n\n\ndef _convert_photoshop_project_settings(ayon_settings, output):\n    if \"photoshop\" not in ayon_settings:\n        return\n\n    ayon_photoshop = ayon_settings[\"photoshop\"]\n    _convert_host_imageio(ayon_photoshop)\n\n    ayon_publish_photoshop = ayon_photoshop[\"publish\"]\n\n    ayon_colorcoded = ayon_publish_photoshop[\"CollectColorCodedInstances\"]\n    if \"flatten_product_type_template\" in ayon_colorcoded:\n        ayon_colorcoded[\"flatten_subset_template\"] = (\n            ayon_colorcoded.pop(\"flatten_product_type_template\"))\n\n    collect_review = ayon_publish_photoshop[\"CollectReview\"]\n    if \"active\" in collect_review:\n        collect_review[\"publish\"] = collect_review.pop(\"active\")\n\n    output[\"photoshop\"] = ayon_photoshop\n\n\ndef _convert_substancepainter_project_settings(ayon_settings, output):\n    if \"substancepainter\" not in ayon_settings:\n        return\n\n    ayon_substance_painter = ayon_settings[\"substancepainter\"]\n    _convert_host_imageio(ayon_substance_painter)\n    if \"shelves\" in ayon_substance_painter:\n        shelves_items = ayon_substance_painter[\"shelves\"]\n        new_shelves_items = {\n            item[\"name\"]: item[\"value\"]\n            for item in shelves_items\n        }\n        ayon_substance_painter[\"shelves\"] = new_shelves_items\n\n    output[\"substancepainter\"] = ayon_substance_painter\n\n\ndef _convert_tvpaint_project_settings(ayon_settings, output):\n    if \"tvpaint\" not in ayon_settings:\n        return\n    ayon_tvpaint = ayon_settings[\"tvpaint\"]\n\n    _convert_host_imageio(ayon_tvpaint)\n\n    ayon_publish_settings = ayon_tvpaint[\"publish\"]\n    for plugin_name in (\n        \"ValidateProjectSettings\",\n        \"ValidateMarks\",\n        \"ValidateStartFrame\",\n        \"ValidateAssetName\",\n    ):\n        ayon_value = ayon_publish_settings[plugin_name]\n        for src_key, dst_key in (\n            (\"action_enabled\", \"optional\"),\n            (\"action_enable\", \"active\"),\n        ):\n            if src_key in ayon_value:\n                ayon_value[dst_key] = ayon_value.pop(src_key)\n\n    extract_sequence_setting = ayon_publish_settings[\"ExtractSequence\"]\n    extract_sequence_setting[\"review_bg\"] = _convert_color(\n        extract_sequence_setting[\"review_bg\"]\n    )\n\n    output[\"tvpaint\"] = ayon_tvpaint\n\n\ndef _convert_traypublisher_project_settings(ayon_settings, output):\n    if \"traypublisher\" not in ayon_settings:\n        return\n\n    ayon_traypublisher = ayon_settings[\"traypublisher\"]\n\n    _convert_host_imageio(ayon_traypublisher)\n\n    ayon_editorial_simple = (\n        ayon_traypublisher[\"editorial_creators\"][\"editorial_simple\"]\n    )\n    # Subset -> Product type conversion\n    if \"product_type_presets\" in ayon_editorial_simple:\n        family_presets = ayon_editorial_simple.pop(\"product_type_presets\")\n        for item in family_presets:\n            item[\"family\"] = item.pop(\"product_type\")\n        ayon_editorial_simple[\"family_presets\"] = family_presets\n\n    if \"shot_metadata_creator\" in ayon_editorial_simple:\n        shot_metadata_creator = ayon_editorial_simple.pop(\n            \"shot_metadata_creator\"\n        )\n        if isinstance(shot_metadata_creator[\"clip_name_tokenizer\"], dict):\n            shot_metadata_creator[\"clip_name_tokenizer\"] = [\n                {\"name\": \"_sequence_\", \"regex\": \"(sc\\\\d{3})\"},\n                {\"name\": \"_shot_\", \"regex\": \"(sh\\\\d{3})\"},\n            ]\n        ayon_editorial_simple.update(shot_metadata_creator)\n\n    ayon_editorial_simple[\"clip_name_tokenizer\"] = {\n        item[\"name\"]: item[\"regex\"]\n        for item in ayon_editorial_simple[\"clip_name_tokenizer\"]\n    }\n\n    if \"shot_subset_creator\" in ayon_editorial_simple:\n        ayon_editorial_simple.update(\n            ayon_editorial_simple.pop(\"shot_subset_creator\"))\n    for item in ayon_editorial_simple[\"shot_hierarchy\"][\"parents\"]:\n        item[\"type\"] = item.pop(\"parent_type\")\n\n    # Simple creators\n    ayon_simple_creators = ayon_traypublisher[\"simple_creators\"]\n    for item in ayon_simple_creators:\n        if \"product_type\" not in item:\n            break\n        item[\"family\"] = item.pop(\"product_type\")\n\n    shot_add_tasks = ayon_editorial_simple[\"shot_add_tasks\"]\n\n    # TODO: backward compatibility and remove in future\n    if isinstance(shot_add_tasks, dict):\n        shot_add_tasks = []\n\n    # aggregate shot_add_tasks items\n    new_shot_add_tasks = {\n        item[\"name\"]: {\"type\": item[\"task_type\"]}\n        for item in shot_add_tasks\n    }\n    ayon_editorial_simple[\"shot_add_tasks\"] = new_shot_add_tasks\n\n    output[\"traypublisher\"] = ayon_traypublisher\n\n\ndef _convert_webpublisher_project_settings(ayon_settings, output):\n    if \"webpublisher\" not in ayon_settings:\n        return\n\n    ayon_webpublisher = ayon_settings[\"webpublisher\"]\n    _convert_host_imageio(ayon_webpublisher)\n\n    ayon_publish = ayon_webpublisher[\"publish\"]\n\n    ayon_collect_files = ayon_publish[\"CollectPublishedFiles\"]\n    ayon_collect_files[\"task_type_to_family\"] = {\n        item[\"name\"]: item[\"value\"]\n        for item in ayon_collect_files[\"task_type_to_family\"]\n    }\n\n    output[\"webpublisher\"] = ayon_webpublisher\n\n\ndef _convert_deadline_project_settings(ayon_settings, output):\n    if \"deadline\" not in ayon_settings:\n        return\n\n    ayon_deadline = ayon_settings[\"deadline\"]\n\n    for key in (\"deadline_urls\",):\n        ayon_deadline.pop(key)\n\n    ayon_deadline_publish = ayon_deadline[\"publish\"]\n\n    maya_submit = ayon_deadline_publish[\"MayaSubmitDeadline\"]\n    for json_key in (\"jobInfo\", \"pluginInfo\"):\n        src_text = maya_submit.pop(json_key)\n        try:\n            value = json.loads(src_text)\n        except ValueError:\n            value = {}\n        maya_submit[json_key] = value\n\n    nuke_submit = ayon_deadline_publish[\"NukeSubmitDeadline\"]\n    nuke_submit[\"env_search_replace_values\"] = {\n        item[\"name\"]: item[\"value\"]\n        for item in nuke_submit.pop(\"env_search_replace_values\")\n    }\n    nuke_submit[\"limit_groups\"] = {\n        item[\"name\"]: item[\"value\"] for item in nuke_submit.pop(\"limit_groups\")\n    }\n\n    process_subsetted_job = ayon_deadline_publish[\"ProcessSubmittedJobOnFarm\"]\n    process_subsetted_job[\"aov_filter\"] = {\n        item[\"name\"]: item[\"value\"]\n        for item in process_subsetted_job.pop(\"aov_filter\")\n    }\n\n    output[\"deadline\"] = ayon_deadline\n\n\ndef _convert_royalrender_project_settings(ayon_settings, output):\n    if \"royalrender\" not in ayon_settings:\n        return\n    ayon_royalrender = ayon_settings[\"royalrender\"]\n    rr_paths = ayon_royalrender.get(\"selected_rr_paths\", [])\n\n    output[\"royalrender\"] = {\n        \"publish\": ayon_royalrender[\"publish\"],\n        \"rr_paths\": rr_paths,\n    }\n\n\ndef _convert_kitsu_project_settings(ayon_settings, output):\n    if \"kitsu\" not in ayon_settings:\n        return\n\n    ayon_kitsu_settings = ayon_settings[\"kitsu\"]\n    ayon_kitsu_settings.pop(\"server\")\n\n    integrate_note = ayon_kitsu_settings[\"publish\"][\"IntegrateKitsuNote\"]\n    status_change_conditions = integrate_note[\"status_change_conditions\"]\n    if \"product_type_requirements\" in status_change_conditions:\n        status_change_conditions[\"family_requirements\"] = (\n            status_change_conditions.pop(\"product_type_requirements\"))\n\n    output[\"kitsu\"] = ayon_kitsu_settings\n\n\ndef _convert_shotgrid_project_settings(ayon_settings, output):\n    if \"shotgrid\" not in ayon_settings:\n        return\n\n    ayon_shotgrid = ayon_settings[\"shotgrid\"]\n    # This means that a different variant of addon is used\n    if \"leecher_backend_url\" not in ayon_shotgrid:\n        return\n\n    for key in {\n        \"leecher_backend_url\",\n        \"filter_projects_by_login\",\n        \"shotgrid_settings\",\n        \"leecher_manager_url\",\n    }:\n        ayon_shotgrid.pop(key)\n\n    asset_field = ayon_shotgrid[\"fields\"][\"asset\"]\n    asset_field[\"type\"] = asset_field.pop(\"asset_type\")\n\n    task_field = ayon_shotgrid[\"fields\"][\"task\"]\n    if \"task\" in task_field:\n        task_field[\"step\"] = task_field.pop(\"task\")\n\n    output[\"shotgrid\"] = ayon_settings[\"shotgrid\"]\n\n\ndef _convert_slack_project_settings(ayon_settings, output):\n    if \"slack\" not in ayon_settings:\n        return\n\n    ayon_slack = ayon_settings[\"slack\"]\n    ayon_slack.pop(\"enabled\", None)\n    for profile in ayon_slack[\"publish\"][\"CollectSlackFamilies\"][\"profiles\"]:\n        profile[\"tasks\"] = profile.pop(\"task_names\")\n        profile[\"subsets\"] = profile.pop(\"subset_names\")\n\n    output[\"slack\"] = ayon_slack\n\n\ndef _convert_global_project_settings(ayon_settings, output, default_settings):\n    if \"core\" not in ayon_settings:\n        return\n\n    ayon_core = ayon_settings[\"core\"]\n\n    _convert_host_imageio(ayon_core)\n\n    for key in (\n        \"environments\",\n        \"studio_name\",\n        \"studio_code\",\n    ):\n        ayon_core.pop(key, None)\n\n    # Publish conversion\n    ayon_publish = ayon_core[\"publish\"]\n\n    ayon_collect_audio = ayon_publish[\"CollectAudio\"]\n    if \"audio_product_name\" in ayon_collect_audio:\n        ayon_collect_audio[\"audio_subset_name\"] = (\n            ayon_collect_audio.pop(\"audio_product_name\"))\n\n    for profile in ayon_publish[\"ExtractReview\"][\"profiles\"]:\n        if \"product_types\" in profile:\n            profile[\"families\"] = profile.pop(\"product_types\")\n        new_outputs = {}\n        for output_def in profile.pop(\"outputs\"):\n            name = output_def.pop(\"name\")\n            new_outputs[name] = output_def\n\n            output_def_filter = output_def[\"filter\"]\n            if \"product_names\" in output_def_filter:\n                output_def_filter[\"subsets\"] = (\n                    output_def_filter.pop(\"product_names\"))\n\n            for color_key in (\"overscan_color\", \"bg_color\"):\n                output_def[color_key] = _convert_color(output_def[color_key])\n\n            letter_box = output_def[\"letter_box\"]\n            for color_key in (\"fill_color\", \"line_color\"):\n                letter_box[color_key] = _convert_color(letter_box[color_key])\n\n            if \"output_width\" in output_def:\n                output_def[\"width\"] = output_def.pop(\"output_width\")\n\n            if \"output_height\" in output_def:\n                output_def[\"height\"] = output_def.pop(\"output_height\")\n\n        profile[\"outputs\"] = new_outputs\n\n    # ExtractThumbnail plugin\n    ayon_extract_thumbnail = ayon_publish[\"ExtractThumbnail\"]\n    # fix display and view at oiio defaults\n    ayon_default_oiio = copy.deepcopy(\n        ayon_extract_thumbnail[\"oiiotool_defaults\"])\n    display_and_view = ayon_default_oiio.pop(\"display_and_view\")\n    ayon_default_oiio[\"display\"] = display_and_view[\"display\"]\n    ayon_default_oiio[\"view\"] = display_and_view[\"view\"]\n    ayon_extract_thumbnail[\"oiiotool_defaults\"] = ayon_default_oiio\n    # fix target size\n    ayon_default_resize = copy.deepcopy(ayon_extract_thumbnail[\"target_size\"])\n    resize = ayon_default_resize.pop(\"resize\")\n    ayon_default_resize[\"width\"] = resize[\"width\"]\n    ayon_default_resize[\"height\"] = resize[\"height\"]\n    ayon_extract_thumbnail[\"target_size\"] = ayon_default_resize\n    # fix background color\n    ayon_extract_thumbnail[\"background_color\"] = _convert_color(\n        ayon_extract_thumbnail[\"background_color\"]\n    )\n\n    # ExtractOIIOTranscode plugin\n    extract_oiio_transcode = ayon_publish[\"ExtractOIIOTranscode\"]\n    extract_oiio_transcode_profiles = extract_oiio_transcode[\"profiles\"]\n    for profile in extract_oiio_transcode_profiles:\n        new_outputs = {}\n        name_counter = {}\n        if \"product_names\" in profile:\n            profile[\"subsets\"] = profile.pop(\"product_names\")\n        for profile_output in profile[\"outputs\"]:\n            if \"name\" in profile_output:\n                name = profile_output.pop(\"name\")\n            else:\n                # Backwards compatibility for setting without 'name' in model\n                name = profile_output[\"extension\"]\n                if name in new_outputs:\n                    name_counter[name] += 1\n                    name = \"{}_{}\".format(name, name_counter[name])\n                else:\n                    name_counter[name] = 0\n\n            new_outputs[name] = profile_output\n        profile[\"outputs\"] = new_outputs\n\n    # Extract Burnin plugin\n    extract_burnin = ayon_publish[\"ExtractBurnin\"]\n    extract_burnin_options = extract_burnin[\"options\"]\n    for color_key in (\"font_color\", \"bg_color\"):\n        extract_burnin_options[color_key] = _convert_color(\n            extract_burnin_options[color_key]\n        )\n\n    for profile in extract_burnin[\"profiles\"]:\n        extract_burnin_defs = profile[\"burnins\"]\n        if \"product_names\" in profile:\n            profile[\"subsets\"] = profile.pop(\"product_names\")\n            profile[\"families\"] = profile.pop(\"product_types\")\n\n        for burnin_def in extract_burnin_defs:\n            for key in (\n                \"TOP_LEFT\",\n                \"TOP_CENTERED\",\n                \"TOP_RIGHT\",\n                \"BOTTOM_LEFT\",\n                \"BOTTOM_CENTERED\",\n                \"BOTTOM_RIGHT\",\n            ):\n                burnin_def[key] = (\n                    burnin_def[key]\n                    .replace(\"{product[name]}\", \"{subset}\")\n                    .replace(\"{Product[name]}\", \"{Subset}\")\n                    .replace(\"{PRODUCT[NAME]}\", \"{SUBSET}\")\n                    .replace(\"{product[type]}\", \"{family}\")\n                    .replace(\"{Product[type]}\", \"{Family}\")\n                    .replace(\"{PRODUCT[TYPE]}\", \"{FAMILY}\")\n                    .replace(\"{folder[name]}\", \"{asset}\")\n                    .replace(\"{Folder[name]}\", \"{Asset}\")\n                    .replace(\"{FOLDER[NAME]}\", \"{ASSET}\")\n                )\n        profile[\"burnins\"] = {\n            extract_burnin_def.pop(\"name\"): extract_burnin_def\n            for extract_burnin_def in extract_burnin_defs\n        }\n\n    if \"IntegrateProductGroup\" in ayon_publish:\n        subset_group = ayon_publish.pop(\"IntegrateProductGroup\")\n        subset_group_profiles = subset_group.pop(\"product_grouping_profiles\")\n        for profile in subset_group_profiles:\n            profile[\"families\"] = profile.pop(\"product_types\")\n        subset_group[\"subset_grouping_profiles\"] = subset_group_profiles\n        ayon_publish[\"IntegrateSubsetGroup\"] = subset_group\n\n    # Cleanup plugin\n    ayon_cleanup = ayon_publish[\"CleanUp\"]\n    if \"patterns\" in ayon_cleanup:\n        ayon_cleanup[\"paterns\"] = ayon_cleanup.pop(\"patterns\")\n\n    # Project root settings - json string to dict\n    ayon_core[\"project_environments\"] = json.loads(\n        ayon_core[\"project_environments\"]\n    )\n    ayon_core[\"project_folder_structure\"] = json.dumps(json.loads(\n        ayon_core[\"project_folder_structure\"]\n    ))\n\n    # Tools settings\n    ayon_tools = ayon_core[\"tools\"]\n    ayon_create_tool = ayon_tools[\"creator\"]\n    if \"product_name_profiles\" in ayon_create_tool:\n        product_name_profiles = ayon_create_tool.pop(\"product_name_profiles\")\n        for profile in product_name_profiles:\n            profile[\"families\"] = profile.pop(\"product_types\")\n        ayon_create_tool[\"subset_name_profiles\"] = product_name_profiles\n\n    for profile in ayon_create_tool[\"subset_name_profiles\"]:\n        template = profile[\"template\"]\n        profile[\"template\"] = (\n            template\n            .replace(\"{task[name]}\", \"{task}\")\n            .replace(\"{Task[name]}\", \"{Task}\")\n            .replace(\"{TASK[NAME]}\", \"{TASK}\")\n            .replace(\"{product[type]}\", \"{family}\")\n            .replace(\"{Product[type]}\", \"{Family}\")\n            .replace(\"{PRODUCT[TYPE]}\", \"{FAMILY}\")\n            .replace(\"{folder[name]}\", \"{asset}\")\n            .replace(\"{Folder[name]}\", \"{Asset}\")\n            .replace(\"{FOLDER[NAME]}\", \"{ASSET}\")\n        )\n\n    product_smart_select_key = \"families_smart_select\"\n    if \"product_types_smart_select\" in ayon_create_tool:\n        product_smart_select_key = \"product_types_smart_select\"\n\n    new_smart_select_families = {\n        item[\"name\"]: item[\"task_names\"]\n        for item in ayon_create_tool.pop(product_smart_select_key)\n    }\n    ayon_create_tool[\"families_smart_select\"] = new_smart_select_families\n\n    ayon_loader_tool = ayon_tools[\"loader\"]\n    if \"product_type_filter_profiles\" in ayon_loader_tool:\n        product_type_filter_profiles = (\n            ayon_loader_tool.pop(\"product_type_filter_profiles\"))\n        for profile in product_type_filter_profiles:\n            profile[\"filter_families\"] = profile.pop(\"filter_product_types\")\n\n        ayon_loader_tool[\"family_filter_profiles\"] = (\n            product_type_filter_profiles)\n\n    ayon_publish_tool = ayon_tools[\"publish\"]\n    for profile in ayon_publish_tool[\"hero_template_name_profiles\"]:\n        if \"product_types\" in profile:\n            profile[\"families\"] = profile.pop(\"product_types\")\n\n    for profile in ayon_publish_tool[\"template_name_profiles\"]:\n        if \"product_types\" in profile:\n            profile[\"families\"] = profile.pop(\"product_types\")\n\n    ayon_core[\"sync_server\"] = (\n        default_settings[\"global\"][\"sync_server\"]\n    )\n    output[\"global\"] = ayon_core\n\n\ndef convert_project_settings(ayon_settings, default_settings):\n    # Missing settings\n    # - standalonepublisher\n    default_settings = copy.deepcopy(default_settings)\n    output = {}\n    exact_match = {\n        \"aftereffects\",\n        \"harmony\",\n        \"houdini\",\n        \"resolve\",\n        \"unreal\",\n    }\n    for key in exact_match:\n        if key in ayon_settings:\n            output[key] = ayon_settings[key]\n            _convert_host_imageio(output[key])\n\n    _convert_applications_project_settings(ayon_settings, output)\n    _convert_blender_project_settings(ayon_settings, output)\n    _convert_celaction_project_settings(ayon_settings, output)\n    _convert_flame_project_settings(ayon_settings, output)\n    _convert_fusion_project_settings(ayon_settings, output)\n    _convert_maya_project_settings(ayon_settings, output)\n    _convert_3dsmax_project_settings(ayon_settings, output)\n    _convert_nuke_project_settings(ayon_settings, output)\n    _convert_hiero_project_settings(ayon_settings, output)\n    _convert_photoshop_project_settings(ayon_settings, output)\n    _convert_substancepainter_project_settings(ayon_settings, output)\n    _convert_tvpaint_project_settings(ayon_settings, output)\n    _convert_traypublisher_project_settings(ayon_settings, output)\n    _convert_webpublisher_project_settings(ayon_settings, output)\n\n    _convert_deadline_project_settings(ayon_settings, output)\n    _convert_royalrender_project_settings(ayon_settings, output)\n    _convert_kitsu_project_settings(ayon_settings, output)\n    _convert_shotgrid_project_settings(ayon_settings, output)\n    _convert_slack_project_settings(ayon_settings, output)\n\n    _convert_global_project_settings(ayon_settings, output, default_settings)\n\n    for key, value in ayon_settings.items():\n        if key not in output:\n            output[key] = value\n\n    for key, value in default_settings.items():\n        if key not in output:\n            output[key] = value\n\n    return output\n\n\nclass CacheItem:\n    lifetime = 10\n\n    def __init__(self, value, outdate_time=None):\n        self._value = value\n        if outdate_time is None:\n            outdate_time = time.time() + self.lifetime\n        self._outdate_time = outdate_time\n\n    @classmethod\n    def create_outdated(cls):\n        return cls({}, 0)\n\n    def get_value(self):\n        return copy.deepcopy(self._value)\n\n    def update_value(self, value):\n        self._value = value\n        self._outdate_time = time.time() + self.lifetime\n\n    @property\n    def is_outdated(self):\n        return time.time() > self._outdate_time\n\n\nclass _AyonSettingsCache:\n    use_bundles = None\n    variant = None\n    addon_versions = CacheItem.create_outdated()\n    studio_settings = CacheItem.create_outdated()\n    cache_by_project_name = collections.defaultdict(\n        CacheItem.create_outdated)\n\n    @classmethod\n    def _use_bundles(cls):\n        if _AyonSettingsCache.use_bundles is None:\n            con = get_ayon_server_api_connection()\n            major, minor, _, _, _ = con.get_server_version_tuple()\n            use_bundles = True\n            if (major, minor) < (0, 3):\n                use_bundles = False\n            _AyonSettingsCache.use_bundles = use_bundles\n        return _AyonSettingsCache.use_bundles\n\n    @classmethod\n    def _get_variant(cls):\n        if _AyonSettingsCache.variant is None:\n            from openpype.lib.openpype_version import is_staging_enabled\n\n            variant = \"production\"\n            if is_dev_mode_enabled():\n                variant = cls._get_bundle_name()\n            elif is_staging_enabled():\n                variant = \"staging\"\n\n            # Cache variant\n            _AyonSettingsCache.variant = variant\n\n            # Set the variant to global ayon api connection\n            con = get_ayon_server_api_connection()\n            con.set_default_settings_variant(variant)\n        return _AyonSettingsCache.variant\n\n    @classmethod\n    def _get_bundle_name(cls):\n        return os.environ[\"AYON_BUNDLE_NAME\"]\n\n    @classmethod\n    def get_value_by_project(cls, project_name):\n        cache_item = _AyonSettingsCache.cache_by_project_name[project_name]\n        if cache_item.is_outdated:\n            con = get_ayon_server_api_connection()\n            if cls._use_bundles():\n                value = con.get_addons_settings(\n                    bundle_name=cls._get_bundle_name(),\n                    project_name=project_name,\n                    variant=cls._get_variant()\n                )\n            else:\n                value = con.get_addons_settings(project_name)\n            cache_item.update_value(value)\n        return cache_item.get_value()\n\n    @classmethod\n    def _get_addon_versions_from_bundle(cls):\n        con = get_ayon_server_api_connection()\n        expected_bundle = cls._get_bundle_name()\n        bundles = con.get_bundles()[\"bundles\"]\n        bundle = next(\n            (\n                bundle\n                for bundle in bundles\n                if bundle[\"name\"] == expected_bundle\n            ),\n            None\n        )\n        if bundle is not None:\n            return bundle[\"addons\"]\n        return {}\n\n    @classmethod\n    def get_addon_versions(cls):\n        cache_item = _AyonSettingsCache.addon_versions\n        if cache_item.is_outdated:\n            if cls._use_bundles():\n                addons = cls._get_addon_versions_from_bundle()\n            else:\n                con = get_ayon_server_api_connection()\n                settings_data = con.get_addons_settings(\n                    only_values=False,\n                    variant=cls._get_variant()\n                )\n                addons = settings_data[\"versions\"]\n            cache_item.update_value(addons)\n\n        return cache_item.get_value()\n\n\ndef get_ayon_project_settings(default_values, project_name):\n    ayon_settings = _AyonSettingsCache.get_value_by_project(project_name)\n    return convert_project_settings(ayon_settings, default_values)\n\n\ndef get_ayon_system_settings(default_values):\n    addon_versions = _AyonSettingsCache.get_addon_versions()\n    ayon_settings = _AyonSettingsCache.get_value_by_project(None)\n\n    return convert_system_settings(\n        ayon_settings, default_values, addon_versions\n    )\n\n\ndef get_ayon_settings(project_name=None):\n    \"\"\"AYON studio settings.\n\n    Raw AYON settings values.\n\n    Args:\n        project_name (Optional[str]): Project name.\n\n    Returns:\n        dict[str, Any]: AYON settings.\n    \"\"\"\n\n    return _AyonSettingsCache.get_value_by_project(project_name)\n"
  },
  {
    "path": "openpype/settings/constants.py",
    "content": "import re\n\n\n# Metadata keys for work with studio and project overrides\nM_OVERRIDDEN_KEY = \"__overriden_keys__\"\n# Metadata key for storing dynamic created labels\nM_DYNAMIC_KEY_LABEL = \"__dynamic_keys_labels__\"\n\nMETADATA_KEYS = frozenset([\n    M_OVERRIDDEN_KEY,\n    M_DYNAMIC_KEY_LABEL\n])\n\n# Keys where studio's system overrides are stored\nGLOBAL_SETTINGS_KEY = \"global_settings\"\nSYSTEM_SETTINGS_KEY = \"system_settings\"\nPROJECT_SETTINGS_KEY = \"project_settings\"\nPROJECT_ANATOMY_KEY = \"project_anatomy\"\nLOCAL_SETTING_KEY = \"local_settings\"\n\nLEGACY_SETTINGS_VERSION = \"legacy\"\n\n# Schema hub names\nSCHEMA_KEY_SYSTEM_SETTINGS = \"system_schema\"\nSCHEMA_KEY_PROJECT_SETTINGS = \"projects_schema\"\n\nDEFAULT_PROJECT_KEY = \"__default_project__\"\n\nKEY_ALLOWED_SYMBOLS = \"a-zA-Z0-9-_ \"\nKEY_REGEX = re.compile(r\"^[{}]+$\".format(KEY_ALLOWED_SYMBOLS))\n\n\n__all__ = (\n    \"M_OVERRIDDEN_KEY\",\n    \"M_DYNAMIC_KEY_LABEL\",\n\n    \"METADATA_KEYS\",\n\n    \"SYSTEM_SETTINGS_KEY\",\n    \"PROJECT_SETTINGS_KEY\",\n    \"PROJECT_ANATOMY_KEY\",\n    \"LOCAL_SETTING_KEY\",\n\n    \"SCHEMA_KEY_SYSTEM_SETTINGS\",\n    \"SCHEMA_KEY_PROJECT_SETTINGS\",\n\n    \"DEFAULT_PROJECT_KEY\",\n\n    \"KEY_ALLOWED_SYMBOLS\",\n    \"KEY_REGEX\"\n)\n"
  },
  {
    "path": "openpype/settings/defaults/project_anatomy/attributes.json",
    "content": "{\n    \"fps\": 25.0,\n    \"frameStart\": 1001,\n    \"frameEnd\": 1001,\n    \"clipIn\": 1,\n    \"clipOut\": 1,\n    \"handleStart\": 0,\n    \"handleEnd\": 0,\n    \"resolutionWidth\": 1920,\n    \"resolutionHeight\": 1080,\n    \"pixelAspect\": 1.0,\n    \"applications\": [\n        \"maya/2020\",\n        \"nuke/12-2\",\n        \"nukex/12-2\",\n        \"hiero/12-2\",\n        \"resolve/stable\",\n        \"houdini/18-5\",\n        \"blender/2-91\",\n        \"harmony/20\",\n        \"photoshop/2021\",\n        \"aftereffects/2021\"\n    ],\n    \"tools_env\": [],\n    \"active\": true\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_anatomy/imageio.json",
    "content": "{\n    \"hiero\": {\n        \"workfile\": {\n            \"ocioConfigName\": \"nuke-default\",\n            \"ocioconfigpath\": {\n                \"windows\": [],\n                \"darwin\": [],\n                \"linux\": []\n            },\n            \"workingSpace\": \"linear\",\n            \"sixteenBitLut\": \"sRGB\",\n            \"eightBitLut\": \"sRGB\",\n            \"floatLut\": \"linear\",\n            \"logLut\": \"Cineon\",\n            \"viewerLut\": \"sRGB\",\n            \"thumbnailLut\": \"sRGB\"\n        },\n        \"regexInputs\": {\n            \"inputs\": [\n                {\n                    \"regex\": \"[^-a-zA-Z0-9](plateRef).*(?=mp4)\",\n                    \"colorspace\": \"sRGB\"\n                }\n            ]\n        }\n    },\n    \"nuke\": {\n        \"viewer\": {\n            \"viewerProcess\": \"sRGB\"\n        },\n        \"baking\": {\n            \"viewerProcess\": \"rec709\"\n        },\n        \"workfile\": {\n            \"colorManagement\": \"Nuke\",\n            \"OCIO_config\": \"nuke-default\",\n            \"customOCIOConfigPath\": {\n                \"windows\": [],\n                \"darwin\": [],\n                \"linux\": []\n            },\n            \"workingSpaceLUT\": \"linear\",\n            \"monitorLut\": \"sRGB\",\n            \"int8Lut\": \"sRGB\",\n            \"int16Lut\": \"sRGB\",\n            \"logLut\": \"Cineon\",\n            \"floatLut\": \"linear\"\n        },\n        \"nodes\": {\n            \"requiredNodes\": [\n                {\n                    \"plugins\": [\n                        \"CreateWriteRender\"\n                    ],\n                    \"nukeNodeClass\": \"Write\",\n                    \"knobs\": [\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"file_type\",\n                            \"value\": \"exr\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"datatype\",\n                            \"value\": \"16 bit half\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"compression\",\n                            \"value\": \"Zip (1 scanline)\"\n                        },\n                        {\n                            \"type\": \"bool\",\n                            \"name\": \"autocrop\",\n                            \"value\": true\n                        },\n                        {\n                            \"type\": \"color_gui\",\n                            \"name\": \"tile_color\",\n                            \"value\": [\n                                186,\n                                35,\n                                35,\n                                255\n                            ]\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"channels\",\n                            \"value\": \"rgb\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"colorspace\",\n                            \"value\": \"linear\"\n                        },\n                        {\n                            \"type\": \"bool\",\n                            \"name\": \"create_directories\",\n                            \"value\": true\n                        }\n                    ]\n                },\n                {\n                    \"plugins\": [\n                        \"CreateWritePrerender\"\n                    ],\n                    \"nukeNodeClass\": \"Write\",\n                    \"knobs\": [\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"file_type\",\n                            \"value\": \"exr\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"datatype\",\n                            \"value\": \"16 bit half\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"compression\",\n                            \"value\": \"Zip (1 scanline)\"\n                        },\n                        {\n                            \"type\": \"bool\",\n                            \"name\": \"autocrop\",\n                            \"value\": true\n                        },\n                        {\n                            \"type\": \"color_gui\",\n                            \"name\": \"tile_color\",\n                            \"value\": [\n                                171,\n                                171,\n                                10,\n                                255\n                            ]\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"channels\",\n                            \"value\": \"rgb\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"colorspace\",\n                            \"value\": \"linear\"\n                        },\n                        {\n                            \"type\": \"bool\",\n                            \"name\": \"create_directories\",\n                            \"value\": true\n                        }\n                    ]\n                },\n                {\n                    \"plugins\": [\n                        \"CreateWriteImage\"\n                    ],\n                    \"nukeNodeClass\": \"Write\",\n                    \"knobs\": [\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"file_type\",\n                            \"value\": \"tiff\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"datatype\",\n                            \"value\": \"16 bit\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"compression\",\n                            \"value\": \"Deflate\"\n                        },\n                        {\n                            \"type\": \"color_gui\",\n                            \"name\": \"tile_color\",\n                            \"value\": [\n                                56,\n                                162,\n                                7,\n                                255\n                            ]\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"channels\",\n                            \"value\": \"rgb\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"colorspace\",\n                            \"value\": \"sRGB\"\n                        },\n                        {\n                            \"type\": \"bool\",\n                            \"name\": \"create_directories\",\n                            \"value\": true\n                        }\n                    ]\n                }\n            ],\n            \"overrideNodes\": []\n        },\n        \"regexInputs\": {\n            \"inputs\": [\n                {\n                    \"regex\": \"(beauty).*(?=.exr)\",\n                    \"colorspace\": \"linear\"\n                }\n            ]\n        }\n    },\n    \"maya\": {\n        \"colorManagementPreference_v2\": {\n            \"enabled\": true,\n            \"configFilePath\": {\n                \"windows\": [],\n                \"darwin\": [],\n                \"linux\": []\n            },\n            \"renderSpace\": \"ACEScg\",\n            \"displayName\": \"sRGB\",\n            \"viewName\": \"ACES 1.0 SDR-video\"\n        },\n        \"colorManagementPreference\": {\n            \"configFilePath\": {\n                \"windows\": [],\n                \"darwin\": [],\n                \"linux\": []\n            },\n            \"renderSpace\": \"scene-linear Rec 709/sRGB\",\n            \"viewTransform\": \"sRGB gamma\"\n        }\n    },\n    \"flame\": {\n        \"project\": {\n            \"colourPolicy\": \"ACES 1.1\",\n            \"frameDepth\": \"16-bit fp\",\n            \"fieldDominance\": \"PROGRESSIVE\"\n        },\n        \"profilesMapping\": {\n            \"inputs\": [\n                {\n                    \"flameName\": \"ACEScg\",\n                    \"ocioName\": \"ACES - ACEScg\"\n                },\n                {\n                    \"flameName\": \"Rec.709 video\",\n                    \"ocioName\": \"Output - Rec.709\"\n                }\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_anatomy/roots.json",
    "content": "{\n    \"work\": {\n        \"windows\": \"C:/projects\",\n        \"darwin\": \"/Volumes/path\",\n        \"linux\": \"/mnt/share/projects\"\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_anatomy/tasks.json",
    "content": "{\n    \"Generic\": {\n        \"short_name\": \"gener\"\n    },\n    \"Art\": {\n        \"short_name\": \"art\"\n    },\n    \"Modeling\": {\n        \"short_name\": \"mdl\"\n    },\n    \"Texture\": {\n        \"short_name\": \"tex\"\n    },\n    \"Lookdev\": {\n        \"short_name\": \"look\"\n    },\n    \"Rigging\": {\n        \"short_name\": \"rig\"\n    },\n    \"Edit\": {\n        \"short_name\": \"edit\"\n    },\n    \"Layout\": {\n        \"short_name\": \"lay\"\n    },\n    \"Setdress\": {\n        \"short_name\": \"dress\"\n    },\n    \"Animation\": {\n        \"short_name\": \"anim\"\n    },\n    \"FX\": {\n        \"short_name\": \"fx\"\n    },\n    \"Lighting\": {\n        \"short_name\": \"lgt\"\n    },\n    \"Paint\": {\n        \"short_name\": \"paint\"\n    },\n    \"Compositing\": {\n        \"short_name\": \"comp\"\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_anatomy/templates.json",
    "content": "{\n    \"defaults\": {\n        \"version_padding\": 3,\n        \"version\": \"v{version:0>{@version_padding}}\",\n        \"frame_padding\": 4,\n        \"frame\": \"{frame:0>{@frame_padding}}\"\n    },\n    \"work\": {\n        \"folder\": \"{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}\",\n        \"file\": \"{project[code]}_{asset}_{task[name]}_{@version}<_{comment}>.{ext}\",\n        \"path\": \"{@folder}/{@file}\"\n    },\n    \"render\": {\n        \"folder\": \"{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}\",\n        \"file\": \"{project[code]}_{asset}_{subset}_{@version}<_{output}><.{@frame}>.{ext}\",\n        \"path\": \"{@folder}/{@file}\"\n    },\n    \"publish\": {\n        \"folder\": \"{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}\",\n        \"file\": \"{project[code]}_{asset}_{subset}_{@version}<_{output}><.{@frame}><_{udim}>.{ext}\",\n        \"path\": \"{@folder}/{@file}\",\n        \"thumbnail\": \"{thumbnail_root}/{project[name]}/{_id}_{thumbnail_type}.{ext}\"\n    },\n    \"hero\": {\n        \"folder\": \"{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/hero\",\n        \"file\": \"{project[code]}_{asset}_{subset}_hero<_{output}><.{frame}>.{ext}\",\n        \"path\": \"{@folder}/{@file}\"\n    },\n    \"delivery\": {},\n    \"unreal\": {\n        \"folder\": \"{root[work]}/{project[name]}/unreal/{task[name]}\",\n        \"file\": \"{project[code]}_{asset}.{ext}\",\n        \"path\": \"{@folder}/{@file}\"\n    },\n    \"others\": {\n        \"maya2unreal\": {\n            \"folder\": \"{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}\",\n            \"file\": \"{subset}_{@version}<_{output}><.{@frame}>.{ext}\",\n            \"path\": \"{@folder}/{@file}\"\n        },\n        \"online\": {\n            \"folder\": \"{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}\",\n            \"file\": \"{originalBasename}<.{@frame}><_{udim}>.{ext}\",\n            \"path\": \"{@folder}/{@file}\"\n        },\n        \"tycache\": {\n            \"folder\": \"{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}\",\n            \"file\": \"{originalBasename}.{ext}\",\n            \"path\": \"{@folder}/{@file}\"\n        },\n        \"source\": {\n            \"folder\": \"{root[work]}/{originalDirname}\",\n            \"file\": \"{originalBasename}.{ext}\",\n            \"path\": \"{@folder}/{@file}\"\n        },\n        \"transient\": {\n            \"folder\": \"{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{family}/{subset}\"\n        },\n        \"__dynamic_keys_labels__\": {\n            \"maya2unreal\": \"Maya to Unreal\",\n            \"online\": \"online\",\n            \"tycache\": \"tycache\",\n            \"source\": \"source\",\n            \"transient\": \"transient\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/aftereffects.json",
    "content": "{\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"create\": {\n        \"RenderCreator\": {\n            \"default_variants\": [\n                \"Main\"\n            ],\n            \"mark_for_review\": true,\n            \"force_setting_values\": true\n        }\n    },\n    \"publish\": {\n        \"CollectReview\": {\n            \"enabled\": true\n        },\n        \"ValidateSceneSettings\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true,\n            \"skip_resolution_check\": [\n                \".*\"\n            ],\n            \"skip_timelines_check\": [\n                \".*\"\n            ]\n        },\n        \"ValidateContainers\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        }\n    },\n    \"workfile_builder\": {\n        \"create_first_version\": false,\n        \"custom_templates\": []\n    },\n    \"templated_workfile_build\": {\n        \"profiles\": []\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/applications.json",
    "content": "{\n    \"only_available\": false\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/blender.json",
    "content": "{\n    \"unit_scale_settings\": {\n        \"enabled\": true,\n        \"apply_on_opening\": false,\n        \"base_file_unit_scale\": 0.01\n    },\n    \"set_resolution_startup\": true,\n    \"set_frames_startup\": true,\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"RenderSettings\": {\n        \"default_render_image_folder\": \"renders/blender\",\n        \"aov_separator\": \"underscore\",\n        \"image_format\": \"exr\",\n        \"multilayer_exr\": true,\n        \"renderer\": \"CYCLES\",\n        \"compositing\": true,\n        \"aov_list\": [\"combined\"],\n        \"custom_passes\": []\n    },\n    \"workfile_builder\": {\n        \"create_first_version\": false,\n        \"custom_templates\": []\n    },\n    \"publish\": {\n        \"ValidateCameraZeroKeyframe\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateFileSaved\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true,\n            \"exclude_families\": []\n        },\n        \"ValidateRenderCameraIsSet\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateDeadlinePublish\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateMeshHasUvs\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshNoNegativeScale\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateTransformZero\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateNoColonsInName\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateInstanceEmpty\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ExtractBlend\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true,\n            \"families\": [\n                \"model\",\n                \"camera\",\n                \"rig\",\n                \"action\",\n                \"layout\",\n                \"blendScene\"\n            ]\n        },\n        \"ExtractFBX\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": false\n        },\n        \"ExtractModelABC\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ExtractBlendAnimation\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ExtractAnimationFBX\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": false\n        },\n        \"ExtractCamera\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ExtractCameraABC\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ExtractLayout\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": false\n        },\n        \"ExtractThumbnail\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true,\n            \"presets\": {\n                \"model\": {\n                    \"image_settings\": {\n                        \"file_format\": \"JPEG\",\n                        \"color_mode\": \"RGB\",\n                        \"quality\": 100\n                    },\n                    \"display_options\": {\n                        \"shading\": {\n                            \"light\": \"STUDIO\",\n                            \"studio_light\": \"Default\",\n                            \"type\": \"SOLID\",\n                            \"color_type\": \"OBJECT\",\n                            \"show_xray\": false,\n                            \"show_shadows\": false,\n                            \"show_cavity\": true\n                        },\n                        \"overlay\": {\n                            \"show_overlays\": false\n                        }\n                    }\n                },\n                \"rig\": {\n                    \"image_settings\": {\n                        \"file_format\": \"JPEG\",\n                        \"color_mode\": \"RGB\",\n                        \"quality\": 100\n                    },\n                    \"display_options\": {\n                        \"shading\": {\n                            \"light\": \"STUDIO\",\n                            \"studio_light\": \"Default\",\n                            \"type\": \"SOLID\",\n                            \"color_type\": \"OBJECT\",\n                            \"show_xray\": true,\n                            \"show_shadows\": false,\n                            \"show_cavity\": false\n                        },\n                        \"overlay\": {\n                            \"show_overlays\": true,\n                            \"show_ortho_grid\": false,\n                            \"show_floor\": false,\n                            \"show_axis_x\": false,\n                            \"show_axis_y\": false,\n                            \"show_axis_z\": false,\n                            \"show_text\": false,\n                            \"show_stats\": false,\n                            \"show_cursor\": false,\n                            \"show_annotation\": false,\n                            \"show_extras\": false,\n                            \"show_relationship_lines\": false,\n                            \"show_outline_selected\": false,\n                            \"show_motion_paths\": false,\n                            \"show_object_origins\": false,\n                            \"show_bones\": true\n                        }\n                    }\n                }\n            }\n        },\n        \"ExtractPlayblast\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true,\n            \"presets\": {\n                \"default\": {\n                    \"image_settings\": {\n                        \"file_format\": \"PNG\",\n                        \"color_mode\": \"RGB\",\n                        \"color_depth\": \"8\",\n                        \"compression\": 15\n                    },\n                    \"display_options\": {\n                        \"shading\": {\n                            \"type\": \"MATERIAL\",\n                            \"render_pass\": \"COMBINED\"\n                        },\n                        \"overlay\": {\n                            \"show_overlays\": false\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/celaction.json",
    "content": "{\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"workfile\": {\n        \"submission_overrides\": [\n            \"render_chunk\",\n            \"frame_range\",\n            \"resolution\"\n        ]\n    },\n    \"publish\": {\n        \"CollectRenderPath\": {\n            \"output_extension\": \"png\",\n            \"anatomy_template_key_render_files\": \"render\",\n            \"anatomy_template_key_metadata\": \"render\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/deadline.json",
    "content": "{\n    \"deadline_servers\": [],\n    \"publish\": {\n        \"CollectDefaultDeadlineServer\": {\n            \"pass_mongo_url\": true\n        },\n        \"CollectDeadlinePools\": {\n            \"primary_pool\": \"\",\n            \"secondary_pool\": \"\"\n        },\n        \"ValidateExpectedFiles\": {\n            \"enabled\": true,\n            \"active\": true,\n            \"allow_user_override\": true,\n            \"families\": [\n                \"render\"\n            ],\n            \"targets\": [\n                \"deadline\"\n            ]\n        },\n        \"MayaSubmitDeadline\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true,\n            \"tile_assembler_plugin\": \"DraftTileAssembler\",\n            \"use_published\": true,\n            \"import_reference\": false,\n            \"asset_dependencies\": true,\n            \"priority\": 50,\n            \"tile_priority\": 50,\n            \"group\": \"none\",\n            \"limit\": [],\n            \"jobInfo\": {},\n            \"pluginInfo\": {},\n            \"scene_patches\": [],\n            \"strict_error_checking\": true\n        },\n        \"HoudiniCacheSubmitDeadline\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true,\n            \"priority\": 50,\n            \"chunk_size\": 999999,\n            \"group\": \"\"\n        },\n        \"HoudiniSubmitDeadline\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true,\n            \"priority\": 50,\n            \"chunk_size\": 1,\n            \"group\": \"\",\n            \"export_priority\": 50,\n            \"export_chunk_size\": 10,\n            \"export_group\": \"\"\n        },\n        \"MaxSubmitDeadline\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true,\n            \"use_published\": true,\n            \"priority\": 50,\n            \"chunk_size\": 10,\n            \"group\": \"none\"\n        },\n        \"FusionSubmitDeadline\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true,\n            \"priority\": 50,\n            \"chunk_size\": 10,\n            \"concurrent_tasks\": 1,\n            \"group\": \"\",\n            \"plugin\": \"Fusion\"\n        },\n        \"NukeSubmitDeadline\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true,\n            \"priority\": 50,\n            \"chunk_size\": 10,\n            \"concurrent_tasks\": 1,\n            \"group\": \"\",\n            \"department\": \"\",\n            \"use_gpu\": true,\n            \"workfile_dependency\": true,\n            \"use_published_workfile\": true,\n            \"env_allowed_keys\": [],\n            \"env_search_replace_values\": {},\n            \"limit_groups\": {}\n        },\n        \"HarmonySubmitDeadline\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true,\n            \"use_published\": true,\n            \"priority\": 50,\n            \"chunk_size\": 10000,\n            \"group\": \"\",\n            \"department\": \"\"\n        },\n        \"AfterEffectsSubmitDeadline\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true,\n            \"use_published\": true,\n            \"priority\": 50,\n            \"chunk_size\": 10000,\n            \"group\": \"\",\n            \"department\": \"\",\n            \"multiprocess\": true\n        },\n        \"CelactionSubmitDeadline\": {\n            \"enabled\": true,\n            \"deadline_department\": \"\",\n            \"deadline_priority\": 50,\n            \"deadline_pool\": \"\",\n            \"deadline_pool_secondary\": \"\",\n            \"deadline_group\": \"\",\n            \"deadline_chunk_size\": 10,\n            \"deadline_job_delay\": \"00:00:00:00\"\n        },\n        \"BlenderSubmitDeadline\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true,\n            \"use_published\": true,\n            \"priority\": 50,\n            \"chunk_size\": 10,\n            \"group\": \"none\",\n            \"job_delay\": \"00:00:00:00\"\n        },\n        \"ProcessSubmittedCacheJobOnFarm\": {\n            \"enabled\": true,\n            \"deadline_department\": \"\",\n            \"deadline_pool\": \"\",\n            \"deadline_group\": \"\",\n            \"deadline_chunk_size\": 1,\n            \"deadline_priority\": 50\n        },\n        \"ProcessSubmittedJobOnFarm\": {\n            \"enabled\": true,\n            \"deadline_department\": \"\",\n            \"deadline_pool\": \"\",\n            \"deadline_group\": \"\",\n            \"deadline_chunk_size\": 1,\n            \"deadline_priority\": 50,\n            \"publishing_script\": \"\",\n            \"skip_integration_repre_list\": [],\n            \"families_transfer\": [\"render3d\", \"render2d\", \"ftrack\", \"slate\"],\n            \"aov_filter\": {\n                \"maya\": [\n                    \".*([Bb]eauty).*\"\n                ],\n                \"blender\": [\n                    \".*([Bb]eauty).*\"\n                ],\n                \"aftereffects\": [\n                    \".*\"\n                ],\n                \"celaction\": [\n                    \".*\"\n                ],\n                \"harmony\": [\n                    \".*\"\n                ],\n                \"max\": [\n                    \".*\"\n                ],\n                \"fusion\": [\n                    \".*\"\n                ]\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/equalizer.json",
    "content": "{\n    \"create\": {\n        \"CreateMatchMove\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"CameraTrack\",\n                \"ObjectTrack\",\n                \"PointTrack\",\n                \"Stabilize\",\n                \"SurveyTrack\",\n                \"UserTrack\"\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/flame.json",
    "content": "{\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"remapping\": {\n            \"rules\": []\n        },\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        },\n        \"project\": {\n            \"colourPolicy\": \"ACES 1.1\",\n            \"frameDepth\": \"16-bit fp\",\n            \"fieldDominance\": \"PROGRESSIVE\"\n        },\n        \"profilesMapping\": {\n            \"inputs\": [\n                {\n                    \"flameName\": \"ACEScg\",\n                    \"ocioName\": \"ACES - ACEScg\"\n                },\n                {\n                    \"flameName\": \"Rec.709 video\",\n                    \"ocioName\": \"Output - Rec.709\"\n                }\n            ]\n        }\n    },\n    \"create\": {\n        \"CreateShotClip\": {\n            \"hierarchy\": \"{folder}/{sequence}\",\n            \"useShotName\": true,\n            \"clipRename\": false,\n            \"clipName\": \"{sequence}{shot}\",\n            \"segmentIndex\": true,\n            \"countFrom\": 10,\n            \"countSteps\": 10,\n            \"folder\": \"shots\",\n            \"episode\": \"ep01\",\n            \"sequence\": \"a\",\n            \"track\": \"{_track_}\",\n            \"shot\": \"####\",\n            \"vSyncOn\": false,\n            \"workfileFrameStart\": 1001,\n            \"handleStart\": 5,\n            \"handleEnd\": 5,\n            \"includeHandles\": false,\n            \"retimedHandles\": true,\n            \"retimedFramerange\": true\n        }\n    },\n    \"publish\": {\n        \"CollectTimelineInstances\": {\n            \"xml_preset_attrs_from_comments\": [\n                {\n                    \"name\": \"width\",\n                    \"type\": \"number\"\n                },\n                {\n                    \"name\": \"height\",\n                    \"type\": \"number\"\n                },\n                {\n                    \"name\": \"pixelRatio\",\n                    \"type\": \"float\"\n                },\n                {\n                    \"name\": \"resizeType\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"name\": \"resizeFilter\",\n                    \"type\": \"string\"\n                }\n            ],\n            \"add_tasks\": [\n                {\n                    \"name\": \"compositing\",\n                    \"type\": \"Compositing\",\n                    \"create_batch_group\": true\n                }\n            ]\n        },\n        \"ExtractSubsetResources\": {\n            \"keep_original_representation\": false,\n            \"export_presets_mapping\": {\n                \"exr16fpdwaa\": {\n                    \"active\": true,\n                    \"export_type\": \"File Sequence\",\n                    \"ext\": \"exr\",\n                    \"xml_preset_file\": \"OpenEXR (16-bit fp DWAA).xml\",\n                    \"colorspace_out\": \"ACES - ACEScg\",\n                    \"xml_preset_dir\": \"\",\n                    \"parsed_comment_attrs\": true,\n                    \"representation_add_range\": true,\n                    \"representation_tags\": [],\n                    \"load_to_batch_group\": true,\n                    \"batch_group_loader_name\": \"LoadClipBatch\",\n                    \"filter_path_regex\": \".*\"\n                }\n            }\n        },\n        \"IntegrateBatchGroup\": {\n            \"enabled\": false\n        }\n    },\n    \"load\": {\n        \"LoadClip\": {\n            \"enabled\": true,\n            \"families\": [\n                \"render2d\",\n                \"source\",\n                \"plate\",\n                \"render\",\n                \"review\"\n            ],\n            \"reel_group_name\": \"OpenPype_Reels\",\n            \"reel_name\": \"Loaded\",\n            \"clip_name_template\": \"{asset}_{subset}<_{output}>\",\n            \"layer_rename_template\": \"{asset}_{subset}<_{output}>\",\n            \"layer_rename_patterns\": [\n                \"rgb\",\n                \"rgba\"\n            ]\n        },\n        \"LoadClipBatch\": {\n            \"enabled\": true,\n            \"families\": [\n                \"render2d\",\n                \"source\",\n                \"plate\",\n                \"render\",\n                \"review\"\n            ],\n            \"reel_name\": \"OP_LoadedReel\",\n            \"clip_name_template\": \"{batch}_{asset}_{subset}<_{output}>\",\n            \"layer_rename_template\": \"{asset}_{subset}<_{output}>\",\n            \"layer_rename_patterns\": [\n                \"rgb\",\n                \"rgba\"\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/ftrack.json",
    "content": "{\n    \"events\": {\n        \"sync_to_avalon\": {\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Administrator\",\n                \"Project manager\"\n            ]\n        },\n        \"prepare_project\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Administrator\",\n                \"Project manager\"\n            ]\n        },\n        \"sync_hier_entity_attributes\": {\n            \"enabled\": true,\n            \"interest_entity_types\": [\n                \"Shot\",\n                \"Asset Build\"\n            ],\n            \"interest_attributes\": [\n                \"frameStart\",\n                \"frameEnd\"\n            ],\n            \"action_enabled\": true,\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Administrator\",\n                \"Project Manager\"\n            ]\n        },\n        \"clone_review_session\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Administrator\",\n                \"Project Manager\"\n            ]\n        },\n        \"thumbnail_updates\": {\n            \"enabled\": true,\n            \"levels\": 1\n        },\n        \"user_assignment\": {\n            \"enabled\": true\n        },\n        \"status_update\": {\n            \"enabled\": true,\n            \"mapping\": {\n                \"In Progress\": [\n                    \"__any__\"\n                ],\n                \"Ready\": [\n                    \"Not Ready\"\n                ],\n                \"__ignore__\": [\n                    \"in progress\",\n                    \"omitted\",\n                    \"on hold\"\n                ]\n            }\n        },\n        \"status_task_to_parent\": {\n            \"enabled\": true,\n            \"parent_object_types\": [\n                \"Shot\",\n                \"Asset Build\"\n            ],\n            \"parent_status_match_all_task_statuses\": {\n                \"Completed\": [\n                    \"Approved\",\n                    \"Omitted\"\n                ]\n            },\n            \"parent_status_by_task_status\": [\n                {\n                    \"new_status\": \"In Progress\",\n                    \"task_statuses\": [\n                        \"in progress\",\n                        \"change requested\",\n                        \"retake\",\n                        \"pending review\"\n                    ]\n                }\n            ]\n        },\n        \"status_task_to_version\": {\n            \"enabled\": true,\n            \"mapping\": {},\n            \"asset_types_filter\": []\n        },\n        \"status_version_to_task\": {\n            \"enabled\": true,\n            \"mapping\": {},\n            \"asset_types_to_skip\": []\n        },\n        \"next_task_update\": {\n            \"enabled\": true,\n            \"mapping\": {\n                \"Not Ready\": \"Ready\"\n            },\n            \"ignored_statuses\": [\n                \"Omitted\"\n            ],\n            \"name_sorting\": false\n        },\n        \"transfer_values_of_hierarchical_attributes\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Administrator\",\n                \"Project manager\"\n            ]\n        },\n        \"create_daily_review_session\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Administrator\",\n                \"Project Manager\"\n            ],\n            \"cycle_enabled\": false,\n            \"cycle_hour_start\": [\n                0,\n                0,\n                0\n            ],\n            \"review_session_template\": \"{yy}{mm}{dd}\"\n        }\n    },\n    \"user_handlers\": {\n        \"application_launch_statuses\": {\n            \"enabled\": true,\n            \"ignored_statuses\": [\n                \"In Progress\",\n                \"Omitted\",\n                \"On hold\",\n                \"Approved\"\n            ],\n            \"status_change\": {\n                \"In Progress\": []\n            }\n        },\n        \"create_update_attributes\": {\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Administrator\"\n            ]\n        },\n        \"prepare_project\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Administrator\",\n                \"Project manager\"\n            ],\n            \"create_project_structure_checked\": false\n        },\n        \"clean_hierarchical_attr\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Administrator\",\n                \"Project manager\"\n            ]\n        },\n        \"delete_asset_subset\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Administrator\",\n                \"Project Manager\"\n            ]\n        },\n        \"delete_old_versions\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Project Manager\",\n                \"Administrator\"\n            ]\n        },\n        \"delivery_action\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Project Manager\",\n                \"Administrator\"\n            ]\n        },\n        \"store_thubmnail_to_avalon\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Project Manager\",\n                \"Administrator\"\n            ]\n        },\n        \"job_killer\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Administrator\"\n            ]\n        },\n        \"sync_to_avalon_local\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Pypeclub\",\n                \"Administrator\"\n            ]\n        },\n        \"fill_workfile_attribute\": {\n            \"enabled\": false,\n            \"custom_attribute_key\": \"\",\n            \"role_list\": []\n        },\n        \"seed_project\": {\n            \"enabled\": true,\n            \"role_list\": [\n                \"Pypeclub\"\n            ]\n        }\n    },\n    \"publish\": {\n        \"CollectFtrackFamily\": {\n            \"enabled\": true,\n            \"profiles\": [\n                {\n                    \"hosts\": [\n                        \"standalonepublisher\"\n                    ],\n                    \"families\": [],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": true,\n                    \"advanced_filtering\": []\n                },\n                {\n                    \"hosts\": [\n                        \"standalonepublisher\"\n                    ],\n                    \"families\": [\n                        \"matchmove\",\n                        \"shot\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": false,\n                    \"advanced_filtering\": []\n                },\n                {\n                    \"hosts\": [\n                        \"standalonepublisher\"\n                    ],\n                    \"families\": [\n                        \"plate\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": false,\n                    \"advanced_filtering\": [\n                        {\n                            \"families\": [\n                                \"clip\",\n                                \"review\"\n                            ],\n                            \"add_ftrack_family\": true\n                        }\n                    ]\n                },\n                {\n                    \"hosts\": [\n                        \"traypublisher\"\n                    ],\n                    \"families\": [],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": true,\n                    \"advanced_filtering\": []\n                },\n                {\n                    \"hosts\": [\n                        \"traypublisher\"\n                    ],\n                    \"families\": [\n                        \"matchmove\",\n                        \"shot\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": false,\n                    \"advanced_filtering\": []\n                },\n                {\n                    \"hosts\": [\n                        \"traypublisher\"\n                    ],\n                    \"families\": [\n                        \"plate\",\n                        \"review\",\n                        \"audio\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": false,\n                    \"advanced_filtering\": [\n                        {\n                            \"families\": [\n                                \"clip\",\n                                \"review\"\n                            ],\n                            \"add_ftrack_family\": true\n                        }\n                    ]\n                },\n                {\n                    \"hosts\": [\n                        \"maya\"\n                    ],\n                    \"families\": [\n                        \"model\",\n                        \"setdress\",\n                        \"animation\",\n                        \"look\",\n                        \"rig\",\n                        \"camera\",\n                        \"renderlayer\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": true,\n                    \"advanced_filtering\": []\n                },\n                {\n                    \"hosts\": [\n                        \"tvpaint\"\n                    ],\n                    \"families\": [\n                        \"renderPass\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": false,\n                    \"advanced_filtering\": []\n                },\n                {\n                    \"hosts\": [\n                        \"tvpaint\"\n                    ],\n                    \"families\": [],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": true,\n                    \"advanced_filtering\": []\n                },\n                {\n                    \"hosts\": [\n                        \"nuke\"\n                    ],\n                    \"families\": [\n                        \"write\",\n                        \"render\",\n                        \"prerender\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": false,\n                    \"advanced_filtering\": [\n                        {\n                            \"families\": [\n                                \"review\"\n                            ],\n                            \"add_ftrack_family\": true\n                        }\n                    ]\n                },\n                {\n                    \"hosts\": [\n                        \"aftereffects\"\n                    ],\n                    \"families\": [\n                        \"render\",\n                        \"workfile\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": true,\n                    \"advanced_filtering\": []\n                },\n                {\n                    \"hosts\": [\n                        \"flame\"\n                    ],\n                    \"families\": [\n                        \"plate\",\n                        \"take\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": true,\n                    \"advanced_filtering\": []\n                },\n                {\n                    \"hosts\": [\n                        \"houdini\"\n                    ],\n                    \"families\": [\n                        \"usd\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": true,\n                    \"advanced_filtering\": []\n                },\n                {\n                    \"hosts\": [\n                        \"photoshop\"\n                    ],\n                    \"families\": [\n                        \"review\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"add_ftrack_family\": true,\n                    \"advanced_filtering\": []\n                }\n            ]\n        },\n        \"CollectFtrackCustomAttributeData\": {\n            \"enabled\": false,\n            \"custom_attribute_keys\": []\n        },\n        \"IntegrateHierarchyToFtrack\": {\n            \"create_task_status_profiles\": []\n        },\n        \"IntegrateFtrackNote\": {\n            \"enabled\": true,\n            \"note_template\": \"{intent}: {comment}\",\n            \"note_labels\": []\n        },\n        \"IntegrateFtrackDescription\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true,\n            \"description_template\": \"{comment}\"\n        },\n        \"ValidateFtrackAttributes\": {\n            \"enabled\": false,\n            \"ftrack_custom_attributes\": {}\n        },\n        \"IntegrateFtrackComponentOverwrite\": {\n            \"enabled\": true\n        },\n        \"IntegrateFtrackInstance\": {\n            \"family_mapping\": {\n                \"camera\": \"cam\",\n                \"look\": \"look\",\n                \"mayaAscii\": \"scene\",\n                \"model\": \"geo\",\n                \"rig\": \"rig\",\n                \"setdress\": \"setdress\",\n                \"pointcache\": \"cache\",\n                \"render\": \"render\",\n                \"prerender\": \"render\",\n                \"render2d\": \"render\",\n                \"nukescript\": \"comp\",\n                \"write\": \"render\",\n                \"review\": \"mov\",\n                \"plate\": \"img\",\n                \"audio\": \"audio\",\n                \"workfile\": \"scene\",\n                \"animation\": \"cache\",\n                \"image\": \"img\",\n                \"reference\": \"reference\",\n                \"ass\": \"cache\",\n                \"mayaScene\": \"scene\",\n                \"camerarig\": \"rig\",\n                \"yeticache\": \"cache\",\n                \"yetiRig\": \"rig\",\n                \"xgen\": \"xgen\",\n                \"rendersetup\": \"rendersetup\",\n                \"assembly\": \"assembly\",\n                \"layout\": \"layout\",\n                \"unrealStaticMesh\": \"geo\",\n                \"vrayproxy\": \"cache\",\n                \"redshiftproxy\": \"cache\",\n                \"usd\": \"usd\"\n            },\n            \"keep_first_subset_name_for_review\": true,\n            \"asset_versions_status_profiles\": [],\n            \"additional_metadata_keys\": [],\n            \"upload_reviewable_with_origin_name\": false\n        },\n        \"IntegrateFtrackFarmStatus\": {\n            \"farm_status_profiles\": [\n                {\n                    \"hosts\": [\n                        \"celaction\"\n                    ],\n                    \"task_types\": [],\n                    \"task_names\": [],\n                    \"families\": [\n                        \"render\"\n                    ],\n                    \"subsets\": [],\n                    \"status_name\": \"Render\"\n                }\n            ]\n        },\n        \"ftrack_task_status_local_publish\": {\n            \"status_profiles\": []\n        },\n        \"ftrack_task_status_on_farm_publish\": {\n            \"status_profiles\": []\n        },\n        \"IntegrateFtrackTaskStatus\": {\n            \"after_version_statuses\": true\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/fusion.json",
    "content": "{\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"copy_fusion_settings\": {\n        \"copy_path\": \"~/.openpype/hosts/fusion/profiles\",\n        \"copy_status\": false,\n        \"force_sync\": false\n    },\n    \"hooks\": {\n        \"InstallPySideToFusion\": {\n            \"enabled\": true\n        }\n    },\n    \"create\": {\n        \"CreateSaver\": {\n            \"temp_rendering_path_template\": \"{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}\",\n            \"default_variants\": [\n                \"Main\",\n                \"Mask\"\n            ],\n            \"instance_attributes\": [\n                \"reviewable\",\n                \"farm_rendering\"\n            ],\n            \"image_format\": \"exr\",\n            \"default_frame_range_option\": \"asset_db\"\n        },\n        \"CreateImageSaver\": {\n            \"temp_rendering_path_template\": \"{workdir}/renders/fusion/{subset}/{subset}.{ext}\",\n            \"default_variants\": [\n                \"Main\",\n                \"Mask\"\n            ],\n            \"instance_attributes\": [\n                \"reviewable\",\n                \"farm_rendering\"\n            ],\n            \"image_format\": \"exr\",\n            \"default_frame\": 0\n        }\n    },\n    \"publish\": {\n        \"ValidateSaverResolution\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/global.json",
    "content": "{\n    \"version_start_category\": {\n        \"profiles\": []\n    },\n    \"imageio\": {\n        \"activate_global_color_management\": false,\n        \"ocio_config\": {\n            \"filepath\": [\n                \"{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio\",\n                \"{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio\"\n            ]\n        },\n        \"file_rules\": {\n            \"activate_global_file_rules\": false,\n            \"rules\": {\n                \"example\": {\n                    \"pattern\": \".*(beauty).*\",\n                    \"colorspace\": \"ACES - ACEScg\",\n                    \"ext\": \"exr\"\n                }\n            }\n        }\n    },\n    \"publish\": {\n        \"CollectAnatomyInstanceData\": {\n            \"follow_workfile_version\": false\n        },\n        \"CollectAudio\": {\n            \"enabled\": false,\n            \"audio_subset_name\": \"audioMain\"\n        },\n        \"CollectSceneVersion\": {\n            \"hosts\": [\n                \"aftereffects\",\n                \"blender\",\n                \"celaction\",\n                \"fusion\",\n                \"harmony\",\n                \"hiero\",\n                \"houdini\",\n                \"maya\",\n                \"nuke\",\n                \"photoshop\",\n                \"resolve\",\n                \"tvpaint\"\n            ],\n            \"skip_hosts_headless_publish\": []\n        },\n        \"collect_comment_per_instance\": {\n            \"enabled\": false,\n            \"families\": []\n        },\n        \"CollectFramesFixDef\": {\n            \"enabled\": true,\n            \"rewrite_version_enable\": true\n        },\n        \"ValidateEditorialAssetName\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateVersion\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateIntent\": {\n            \"enabled\": false,\n            \"profiles\": []\n        },\n        \"ExtractThumbnail\": {\n            \"enabled\": true,\n            \"subsets\": [],\n            \"integrate_thumbnail\": false,\n            \"background_color\": [\n                0,\n                0,\n                0,\n                255\n            ],\n            \"duration_split\": 0.5,\n            \"target_size\": {\n                \"type\": \"resize\",\n                \"width\": 1920,\n                \"height\": 1080\n            },\n            \"oiiotool_defaults\": {\n                \"type\": \"colorspace\",\n                \"colorspace\": \"color_picking\",\n                \"view\": \"sRGB\",\n                \"display\": \"default\"\n            },\n            \"ffmpeg_args\": {\n                \"input\": [\n                    \"-apply_trc gamma22\"\n                ],\n                \"output\": []\n            }\n        },\n        \"ExtractOIIOTranscode\": {\n            \"enabled\": true,\n            \"profiles\": []\n        },\n        \"ExtractReview\": {\n            \"enabled\": true,\n            \"profiles\": [\n                {\n                    \"families\": [],\n                    \"hosts\": [],\n                    \"outputs\": {\n                        \"png\": {\n                            \"ext\": \"png\",\n                            \"tags\": [\n                                \"ftrackreview\",\n                                \"kitsureview\"\n                            ],\n                            \"burnins\": [],\n                            \"ffmpeg_args\": {\n                                \"video_filters\": [],\n                                \"audio_filters\": [],\n                                \"input\": [],\n                                \"output\": []\n                            },\n                            \"filter\": {\n                                \"families\": [\n                                    \"render\",\n                                    \"review\",\n                                    \"ftrack\"\n                                ],\n                                \"subsets\": [],\n                                \"custom_tags\": [],\n                                \"single_frame_filter\": \"single_frame\"\n                            },\n                            \"overscan_crop\": \"\",\n                            \"overscan_color\": [\n                                0,\n                                0,\n                                0,\n                                255\n                            ],\n                            \"width\": 1920,\n                            \"height\": 1080,\n                            \"scale_pixel_aspect\": true,\n                            \"bg_color\": [\n                                0,\n                                0,\n                                0,\n                                0\n                            ],\n                            \"letter_box\": {\n                                \"enabled\": false,\n                                \"ratio\": 0.0,\n                                \"fill_color\": [\n                                    0,\n                                    0,\n                                    0,\n                                    255\n                                ],\n                                \"line_thickness\": 0,\n                                \"line_color\": [\n                                    255,\n                                    0,\n                                    0,\n                                    255\n                                ]\n                            }\n                        },\n                        \"h264\": {\n                            \"ext\": \"mp4\",\n                            \"tags\": [\n                                \"burnin\",\n                                \"ftrackreview\",\n                                \"kitsureview\"\n                            ],\n                            \"burnins\": [],\n                            \"ffmpeg_args\": {\n                                \"video_filters\": [],\n                                \"audio_filters\": [],\n                                \"input\": [\n                                    \"-apply_trc gamma22\"\n                                ],\n                                \"output\": [\n                                    \"-pix_fmt yuv420p\",\n                                    \"-crf 18\",\n                                    \"-intra\"\n                                ]\n                            },\n                            \"filter\": {\n                                \"families\": [\n                                    \"render\",\n                                    \"review\",\n                                    \"ftrack\"\n                                ],\n                                \"subsets\": [],\n                                \"custom_tags\": [],\n                                \"single_frame_filter\": \"multi_frame\"\n                            },\n                            \"overscan_crop\": \"\",\n                            \"overscan_color\": [\n                                0,\n                                0,\n                                0,\n                                255\n                            ],\n                            \"width\": 0,\n                            \"height\": 0,\n                            \"scale_pixel_aspect\": true,\n                            \"bg_color\": [\n                                0,\n                                0,\n                                0,\n                                0\n                            ],\n                            \"letter_box\": {\n                                \"enabled\": false,\n                                \"ratio\": 0.0,\n                                \"fill_color\": [\n                                    0,\n                                    0,\n                                    0,\n                                    255\n                                ],\n                                \"line_thickness\": 0,\n                                \"line_color\": [\n                                    255,\n                                    0,\n                                    0,\n                                    255\n                                ]\n                            }\n                        }\n                    }\n                }\n            ]\n        },\n        \"ExtractBurnin\": {\n            \"enabled\": true,\n            \"options\": {\n                \"font_size\": 42,\n                \"font_color\": [\n                    255,\n                    255,\n                    255,\n                    255\n                ],\n                \"bg_color\": [\n                    0,\n                    0,\n                    0,\n                    127\n                ],\n                \"x_offset\": 5,\n                \"y_offset\": 5,\n                \"bg_padding\": 5,\n                \"font_filepath\": {\n                    \"windows\": \"\",\n                    \"darwin\": \"\",\n                    \"linux\": \"\"\n                }\n            },\n            \"profiles\": [\n                {\n                    \"families\": [],\n                    \"hosts\": [],\n                    \"task_types\": [],\n                    \"task_names\": [],\n                    \"subsets\": [],\n                    \"burnins\": {\n                        \"burnin\": {\n                            \"TOP_LEFT\": \"{yy}-{mm}-{dd}\",\n                            \"TOP_CENTERED\": \"\",\n                            \"TOP_RIGHT\": \"{anatomy[version]}\",\n                            \"BOTTOM_LEFT\": \"{username}\",\n                            \"BOTTOM_CENTERED\": \"{asset}\",\n                            \"BOTTOM_RIGHT\": \"{frame_start}-{current_frame}-{frame_end}\",\n                            \"filter\": {\n                                \"families\": [],\n                                \"tags\": []\n                            }\n                        }\n                    }\n                },\n                {\n                    \"families\": [\n                        \"review\"\n                    ],\n                    \"hosts\": [\n                        \"maya\",\n                        \"houdini\",\n                        \"max\"\n                    ],\n                    \"task_types\": [],\n                    \"task_names\": [],\n                    \"subsets\": [],\n                    \"burnins\": {\n                        \"focal_length_burnin\": {\n                            \"TOP_LEFT\": \"{yy}-{mm}-{dd}\",\n                            \"TOP_CENTERED\": \"{focalLength:.2f} mm\",\n                            \"TOP_RIGHT\": \"{anatomy[version]}\",\n                            \"BOTTOM_LEFT\": \"{username}\",\n                            \"BOTTOM_CENTERED\": \"{asset}\",\n                            \"BOTTOM_RIGHT\": \"{frame_start}-{current_frame}-{frame_end}\",\n                            \"filter\": {\n                                \"families\": [],\n                                \"tags\": []\n                            }\n                        }\n                    }\n                }\n            ]\n        },\n        \"PreIntegrateThumbnails\": {\n            \"enabled\": true,\n            \"integrate_profiles\": []\n        },\n        \"IntegrateSubsetGroup\": {\n            \"subset_grouping_profiles\": [\n                {\n                    \"families\": [],\n                    \"hosts\": [],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"template\": \"\"\n                }\n            ]\n        },\n        \"IntegrateHeroVersion\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true,\n            \"families\": [\n                \"model\",\n                \"rig\",\n                \"look\",\n                \"pointcache\",\n                \"animation\",\n                \"setdress\",\n                \"layout\",\n                \"mayaScene\"\n            ],\n            \"template_name_profiles\": []\n        },\n        \"CleanUp\": {\n            \"paterns\": [],\n            \"remove_temp_renders\": false\n        },\n        \"CleanUpFarm\": {\n            \"enabled\": false\n        }\n    },\n    \"tools\": {\n        \"creator\": {\n            \"families_smart_select\": {\n                \"Render\": [\n                    \"light\",\n                    \"render\"\n                ],\n                \"Model\": [\n                    \"model\"\n                ],\n                \"Layout\": [\n                    \"layout\"\n                ],\n                \"Look\": [\n                    \"look\"\n                ],\n                \"Rig\": [\n                    \"rigging\",\n                    \"rig\"\n                ]\n            },\n            \"subset_name_profiles\": [\n                {\n                    \"families\": [],\n                    \"hosts\": [],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"template\": \"{family}{variant}\"\n                },\n                {\n                    \"families\": [\n                        \"workfile\"\n                    ],\n                    \"hosts\": [],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"template\": \"{family}{Task}\"\n                },\n                {\n                    \"families\": [\n                        \"render\"\n                    ],\n                    \"hosts\": [],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"template\": \"{family}{Task}{Variant}\"\n                },\n                {\n                    \"families\": [\n                        \"renderLayer\",\n                        \"renderPass\"\n                    ],\n                    \"hosts\": [\n                        \"tvpaint\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"template\": \"{family}{Task}_{Renderlayer}_{Renderpass}\"\n                },\n                {\n                    \"families\": [\n                        \"review\",\n                        \"workfile\"\n                    ],\n                    \"hosts\": [\n                        \"aftereffects\",\n                        \"tvpaint\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"template\": \"{family}{Task}\"\n                },\n                {\n                    \"families\": [\n                        \"render\"\n                    ],\n                    \"hosts\": [\n                        \"aftereffects\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"template\": \"{family}{Task}{Composition}{Variant}\"\n                },\n                {\n                    \"families\": [\n                        \"staticMesh\"\n                    ],\n                    \"hosts\": [\n                        \"maya\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"template\": \"S_{asset}{variant}\"\n                },\n                {\n                    \"families\": [\n                        \"skeletalMesh\"\n                    ],\n                    \"hosts\": [\n                        \"maya\"\n                    ],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"template\": \"SK_{asset}{variant}\"\n                }\n            ]\n        },\n        \"Workfiles\": {\n            \"workfile_template_profiles\": [\n                {\n                    \"task_types\": [],\n                    \"hosts\": [],\n                    \"workfile_template\": \"work\"\n                },\n                {\n                    \"task_types\": [],\n                    \"hosts\": [\n                        \"unreal\"\n                    ],\n                    \"workfile_template\": \"unreal\"\n                }\n            ],\n            \"last_workfile_on_startup\": [\n                {\n                    \"hosts\": [],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"enabled\": true,\n                    \"use_last_published_workfile\": false\n                }\n            ],\n            \"open_workfile_tool_on_startup\": [\n                {\n                    \"hosts\": [],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"enabled\": false\n                }\n            ],\n            \"extra_folders\": [],\n            \"workfile_lock_profiles\": []\n        },\n        \"loader\": {\n            \"family_filter_profiles\": [\n                {\n                    \"hosts\": [],\n                    \"task_types\": [],\n                    \"is_include\": true,\n                    \"filter_families\": []\n                }\n            ]\n        },\n        \"publish\": {\n            \"template_name_profiles\": [\n                {\n                    \"families\": [],\n                    \"hosts\": [],\n                    \"task_types\": [],\n                    \"task_names\": [],\n                    \"template_name\": \"publish\"\n                },\n                {\n                    \"families\": [\n                        \"review\",\n                        \"render\",\n                        \"prerender\"\n                    ],\n                    \"hosts\": [],\n                    \"task_types\": [],\n                    \"task_names\": [],\n                    \"template_name\": \"render\"\n                },\n                {\n                    \"families\": [\n                        \"staticMesh\",\n                        \"skeletalMesh\"\n                    ],\n                    \"hosts\": [\n                        \"maya\"\n                    ],\n                    \"task_types\": [],\n                    \"task_names\": [],\n                    \"template_name\": \"maya2unreal\"\n                },\n                {\n                    \"families\": [\n                        \"online\"\n                    ],\n                    \"hosts\": [\n                        \"traypublisher\"\n                    ],\n                    \"task_types\": [],\n                    \"task_names\": [],\n                    \"template_name\": \"online\"\n                },\n                {\n                    \"families\": [\n                        \"tycache\"\n                    ],\n                    \"hosts\": [\n                        \"max\"\n                    ],\n                    \"task_types\": [],\n                    \"task_names\": [],\n                    \"template_name\": \"tycache\"\n                }\n            ],\n            \"hero_template_name_profiles\": [],\n            \"custom_staging_dir_profiles\": []\n        }\n    },\n    \"project_folder_structure\": \"{\\\"__project_root__\\\": {\\\"prod\\\": {}, \\\"resources\\\": {\\\"footage\\\": {\\\"plates\\\": {}, \\\"offline\\\": {}}, \\\"audio\\\": {}, \\\"art_dept\\\": {}}, \\\"editorial\\\": {}, \\\"assets\\\": {\\\"characters\\\": {}, \\\"locations\\\": {}}, \\\"shots\\\": {}}}\",\n    \"sync_server\": {\n        \"enabled\": false,\n        \"config\": {\n            \"retry_cnt\": \"3\",\n            \"loop_delay\": \"60\",\n            \"always_accessible_on\": [],\n            \"active_site\": \"studio\",\n            \"remote_site\": \"studio\"\n        },\n        \"sites\": {}\n    },\n    \"project_plugins\": {\n        \"windows\": [],\n        \"darwin\": [],\n        \"linux\": []\n    },\n    \"project_environments\": {}\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/harmony.json",
    "content": "{\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"publish\": {\n        \"CollectPalettes\": {\n            \"allowed_tasks\": [\n                \".*\"\n            ]\n        },\n        \"ValidateAudio\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateContainers\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateSceneSettings\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true,\n            \"frame_check_filter\": [],\n            \"skip_resolution_check\": [],\n            \"skip_timelines_check\": []\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/hiero.json",
    "content": "{\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        },\n        \"workfile\": {\n            \"ocioConfigName\": \"aces_1.2\",\n            \"workingSpace\": \"role_scene_linear\",\n            \"viewerLut\": \"ACES/sRGB\",\n            \"thumbnailLut\": \"ACES/sRGB\",\n            \"monitorOutLut\": \"ACES/sRGB\",\n            \"eightBitLut\": \"role_matte_paint\",\n            \"sixteenBitLut\": \"role_texture_paint\",\n            \"logLut\": \"role_compositing_log\",\n            \"floatLut\": \"role_scene_linear\"\n        },\n        \"regexInputs\": {\n            \"inputs\": [\n                {\n                    \"regex\": \"[^-a-zA-Z0-9](plateRef).*(?=mp4)\",\n                    \"colorspace\": \"sRGB\"\n                }\n            ]\n        }\n    },\n    \"create\": {\n        \"CreateShotClip\": {\n            \"hierarchy\": \"{folder}/{sequence}\",\n            \"clipRename\": true,\n            \"clipName\": \"{track}{sequence}{shot}\",\n            \"countFrom\": 10,\n            \"countSteps\": 10,\n            \"folder\": \"shots\",\n            \"episode\": \"ep01\",\n            \"sequence\": \"sq01\",\n            \"track\": \"{_track_}\",\n            \"shot\": \"sh###\",\n            \"vSyncOn\": false,\n            \"workfileFrameStart\": 1001,\n            \"handleStart\": 10,\n            \"handleEnd\": 10\n        }\n    },\n    \"load\": {\n        \"LoadClip\": {\n            \"enabled\": true,\n            \"families\": [\n                \"render2d\",\n                \"source\",\n                \"plate\",\n                \"render\",\n                \"review\"\n            ],\n            \"clip_name_template\": \"{asset}_{subset}_{representation}\"\n        }\n    },\n    \"publish\": {\n        \"CollectInstanceVersion\": {\n            \"enabled\": false\n        },\n        \"ExtractReviewCutUpVideo\": {\n            \"enabled\": true,\n            \"tags_addition\": [\n                \"review\"\n            ]\n        },\n        \"CollectClipEffects\": {\n            \"enabled\": true,\n            \"effect_categories\": {}\n        }\n    },\n    \"filters\": {},\n    \"scriptsmenu\": {\n        \"name\": \"OpenPype Tools\",\n        \"definition\": [\n            {\n                \"type\": \"action\",\n                \"sourcetype\": \"python\",\n                \"title\": \"OpenPype Docs\",\n                \"command\": \"import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_hiero')\",\n                \"tooltip\": \"Open the OpenPype Hiero user doc page\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/houdini.json",
    "content": "{\n    \"general\": {\n        \"add_self_publish_button\": false,\n        \"update_houdini_var_context\": {\n            \"enabled\": true,\n            \"houdini_vars\":[\n                {\n                    \"var\": \"JOB\",\n                    \"value\": \"{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}\",\n                    \"is_directory\": true\n                }\n            ]\n        }\n    },\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"shelves\": [],\n    \"create\": {\n        \"CreateAlembicCamera\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateArnoldAss\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ],\n            \"ext\": \".ass\"\n        },\n        \"CreateArnoldRop\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateCompositeSequence\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateHDA\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateKarmaROP\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateMantraIFD\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateMantraROP\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreatePointCache\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateBGEO\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateRedshiftProxy\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateRedshiftROP\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateReview\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateStaticMesh\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ],\n            \"static_mesh_prefix\": \"S\",\n            \"collision_prefixes\": [\n                \"UBX\",\n                \"UCP\",\n                \"USP\",\n                \"UCX\"\n            ]\n        },\n        \"CreateUSD\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateUSDRender\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateVDBCache\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateVrayROP\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        }\n    },\n    \"publish\": {\n        \"CollectAssetHandles\": {\n            \"use_asset_handles\": true\n        },\n        \"CollectChunkSize\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"chunk_size\": 999999\n        },\n        \"ValidateContainers\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshIsStatic\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateReviewColorspace\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateSubsetName\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateUnrealStaticMeshName\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateWorkfilePaths\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"node_types\": [\n                \"file\",\n                \"alembic\"\n            ],\n            \"prohibited_vars\": [\n                \"$HIP\",\n                \"$JOB\"\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/kitsu.json",
    "content": "{\n    \"entities_naming_pattern\": {\n        \"episode\": \"E##\",\n        \"sequence\": \"SQ##\",\n        \"shot\": \"SH##\"\n    },\n    \"publish\": {\n        \"IntegrateKitsuNote\": {\n            \"set_status_note\": false,\n            \"note_status_shortname\": \"wfa\",\n            \"status_change_conditions\": {\n                \"status_conditions\": [],\n                \"family_requirements\": []\n            },\n            \"custom_comment_template\": {\n                \"enabled\": false,\n                \"comment_template\": \"{comment}\\n\\n|  |  |\\n|--|--|\\n| version| `{version}` |\\n| family | `{family}` |\\n| name | `{name}` |\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/max.json",
    "content": "{\n    \"unit_scale_settings\": {\n        \"enabled\": true,\n        \"scene_unit_scale\": \"Meters\"\n    },\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"RenderSettings\": {\n        \"default_render_image_folder\": \"renders/3dsmax\",\n        \"aov_separator\": \"underscore\",\n        \"image_format\": \"exr\",\n        \"multipass\": true\n    },\n    \"CreateReview\": {\n        \"review_width\": 1920,\n        \"review_height\": 1080,\n        \"percentSize\": 100.0,\n        \"keep_images\": false,\n        \"image_format\": \"png\",\n        \"visual_style\": \"Realistic\",\n        \"viewport_preset\": \"Quality\",\n        \"anti_aliasing\": \"None\",\n        \"vp_texture\": true\n    },\n    \"PointCloud\": {\n        \"attribute\": {\n            \"Age\": \"age\",\n            \"Radius\": \"radius\",\n            \"Position\": \"position\",\n            \"Rotation\": \"rotation\",\n            \"Scale\": \"scale\",\n            \"Velocity\": \"velocity\",\n            \"Color\": \"color\",\n            \"TextureCoordinate\": \"texcoord\",\n            \"MaterialID\": \"matid\",\n            \"custFloats\": \"custFloats\",\n            \"custVecs\": \"custVecs\"\n        }\n    },\n    \"publish\": {\n        \"ValidateInstanceInContext\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateFrameRange\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateAttributes\": {\n            \"enabled\": false,\n            \"attributes\": {}\n        },\n        \"ValidateCameraAttributes\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": false,\n            \"fov\": 45.0,\n            \"nearrange\": 0.0,\n            \"farrange\": 1000.0,\n            \"nearclip\": 1.0,\n            \"farclip\": 1000.0\n        },\n        \"ValidateLoadedPlugin\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"family_plugins_mapping\": []\n        },\n        \"ValidateRenderPasses\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ExtractModelObj\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": false\n        },\n        \"ExtractModelFbx\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": false\n        },\n        \"ExtractModelUSD\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": false\n        },\n        \"ExtractModel\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ExtractMaxSceneRaw\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/maya.json",
    "content": "{\n    \"open_workfile_post_initialization\": false,\n    \"explicit_plugins_loading\": {\n        \"enabled\": false,\n        \"plugins_to_load\": [\n            {\n                \"enabled\": false,\n                \"name\": \"AbcBullet\"\n            },\n            {\n                \"enabled\": true,\n                \"name\": \"AbcExport\"\n            },\n            {\n                \"enabled\": true,\n                \"name\": \"AbcImport\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"animImportExport\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"ArubaTessellator\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"ATFPlugin\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"atomImportExport\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"AutodeskPacketFile\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"autoLoader\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"bifmeshio\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"bifrostGraph\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"bifrostshellnode\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"bifrostvisplugin\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"blast2Cmd\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"bluePencil\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"Boss\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"bullet\"\n            },\n            {\n                \"enabled\": true,\n                \"name\": \"cacheEvaluator\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"cgfxShader\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"cleanPerFaceAssignment\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"clearcoat\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"convertToComponentTags\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"curveWarp\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"ddsFloatReader\"\n            },\n            {\n                \"enabled\": true,\n                \"name\": \"deformerEvaluator\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"dgProfiler\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"drawUfe\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"dx11Shader\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"fbxmaya\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"fltTranslator\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"freeze\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"Fur\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"gameFbxExporter\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"gameInputDevice\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"GamePipeline\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"gameVertexCount\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"geometryReport\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"geometryTools\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"glslShader\"\n            },\n            {\n                \"enabled\": true,\n                \"name\": \"GPUBuiltInDeformer\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"gpuCache\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"hairPhysicalShader\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"ik2Bsolver\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"ikSpringSolver\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"invertShape\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"lges\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"lookdevKit\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"MASH\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"matrixNodes\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"mayaCharacterization\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"mayaHIK\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"MayaMuscle\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"mayaUsdPlugin\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"mayaVnnPlugin\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"melProfiler\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"meshReorder\"\n            },\n            {\n                \"enabled\": true,\n                \"name\": \"modelingToolkit\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"mtoa\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"mtoh\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"nearestPointOnMesh\"\n            },\n            {\n                \"enabled\": true,\n                \"name\": \"objExport\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"OneClick\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"OpenEXRLoader\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"pgYetiMaya\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"pgyetiVrayMaya\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"polyBoolean\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"poseInterpolator\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"quatNodes\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"randomizerDevice\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"redshift4maya\"\n            },\n            {\n                \"enabled\": true,\n                \"name\": \"renderSetup\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"retargeterNodes\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"RokokoMotionLibrary\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"rotateHelper\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"sceneAssembly\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"shaderFXPlugin\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"shotCamera\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"snapTransform\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"stage\"\n            },\n            {\n                \"enabled\": true,\n                \"name\": \"stereoCamera\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"stlTranslator\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"studioImport\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"Substance\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"substancelink\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"substancemaya\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"substanceworkflow\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"svgFileTranslator\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"sweep\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"testify\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"tiffFloatReader\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"timeSliderBookmark\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"Turtle\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"Type\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"udpDevice\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"ufeSupport\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"Unfold3D\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"VectorRender\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"vrayformaya\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"vrayvolumegrid\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"xgenToolkit\"\n            },\n            {\n                \"enabled\": false,\n                \"name\": \"xgenVray\"\n            }\n        ]\n    },\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        },\n        \"workfile\": {\n            \"enabled\": false,\n            \"renderSpace\": \"ACES - ACEScg\",\n            \"displayName\": \"ACES\",\n            \"viewName\": \"sRGB\"\n        },\n        \"colorManagementPreference_v2\": {\n            \"enabled\": true,\n            \"renderSpace\": \"ACEScg\",\n            \"displayName\": \"sRGB\",\n            \"viewName\": \"ACES 1.0 SDR-video\"\n        },\n        \"colorManagementPreference\": {\n            \"renderSpace\": \"scene-linear Rec 709/sRGB\",\n            \"viewTransform\": \"sRGB gamma\"\n        }\n    },\n    \"mel_workspace\": \"workspace -fr \\\"shaders\\\" \\\"renderData/shaders\\\";\\nworkspace -fr \\\"images\\\" \\\"renders/maya\\\";\\nworkspace -fr \\\"particles\\\" \\\"particles\\\";\\nworkspace -fr \\\"mayaAscii\\\" \\\"\\\";\\nworkspace -fr \\\"mayaBinary\\\" \\\"\\\";\\nworkspace -fr \\\"scene\\\" \\\"\\\";\\nworkspace -fr \\\"alembicCache\\\" \\\"cache/alembic\\\";\\nworkspace -fr \\\"renderData\\\" \\\"renderData\\\";\\nworkspace -fr \\\"sourceImages\\\" \\\"sourceimages\\\";\\nworkspace -fr \\\"fileCache\\\" \\\"cache/nCache\\\";\\nworkspace -fr \\\"autoSave\\\" \\\"autosave\\\";\",\n    \"ext_mapping\": {\n        \"model\": \"ma\",\n        \"mayaAscii\": \"ma\",\n        \"camera\": \"ma\",\n        \"rig\": \"ma\",\n        \"workfile\": \"ma\",\n        \"yetiRig\": \"ma\"\n    },\n    \"maya-dirmap\": {\n        \"use_env_var_as_root\": false,\n        \"enabled\": false,\n        \"paths\": {\n            \"source-path\": [],\n            \"destination-path\": []\n        }\n    },\n    \"include_handles\": {\n        \"include_handles_default\": false,\n        \"per_task_type\": []\n    },\n    \"scriptsmenu\": {\n        \"name\": \"OpenPype Tools\",\n        \"definition\": [\n            {\n                \"type\": \"action\",\n                \"command\": \"import openpype.hosts.maya.api.commands as op_cmds; op_cmds.edit_shader_definitions()\",\n                \"sourcetype\": \"python\",\n                \"title\": \"Edit shader name definitions\",\n                \"tooltip\": \"Edit shader name definitions used in validation and renaming.\",\n                \"tags\": [\n                    \"pipeline\",\n                    \"shader\"\n                ]\n            }\n        ]\n    },\n    \"RenderSettings\": {\n        \"apply_render_settings\": true,\n        \"default_render_image_folder\": \"renders/maya\",\n        \"enable_all_lights\": true,\n        \"aov_separator\": \"underscore\",\n        \"remove_aovs\": false,\n        \"reset_current_frame\": false,\n        \"arnold_renderer\": {\n            \"image_prefix\": \"<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>\",\n            \"image_format\": \"exr\",\n            \"multilayer_exr\": true,\n            \"tiled\": true,\n            \"aov_list\": [],\n            \"additional_options\": []\n        },\n        \"vray_renderer\": {\n            \"image_prefix\": \"<scene>/<Layer>/<Layer>\",\n            \"engine\": \"1\",\n            \"image_format\": \"exr\",\n            \"aov_list\": [],\n            \"additional_options\": []\n        },\n        \"redshift_renderer\": {\n            \"image_prefix\": \"<Scene>/<RenderLayer>/<RenderLayer>\",\n            \"primary_gi_engine\": \"0\",\n            \"secondary_gi_engine\": \"0\",\n            \"image_format\": \"exr\",\n            \"multilayer_exr\": true,\n            \"force_combine\": true,\n            \"aov_list\": [],\n            \"additional_options\": []\n        },\n        \"renderman_renderer\": {\n            \"image_prefix\": \"<layer>{aov_separator}<aov>.<f4>.<ext>\",\n            \"image_dir\": \"<scene>/<layer>\",\n            \"display_filters\": [],\n            \"imageDisplay_dir\": \"<imagedir>/<layer>{aov_separator}imageDisplayFilter.<f4>.<ext>\",\n            \"sample_filters\": [],\n            \"cryptomatte_dir\": \"<imagedir>/<layer>{aov_separator}cryptomatte.<f4>.<ext>\",\n            \"watermark_dir\": \"<imagedir>/<layer>{aov_separator}watermarkFilter.<f4>.<ext>\",\n            \"additional_options\": []\n        }\n    },\n    \"create\": {\n        \"CreateLook\": {\n            \"enabled\": true,\n            \"make_tx\": true,\n            \"rs_tex\": false,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateRender\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateUnrealStaticMesh\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"\",\n                \"_Main\"\n            ],\n            \"static_mesh_prefix\": \"S\",\n            \"collision_prefixes\": [\n                \"UBX\",\n                \"UCP\",\n                \"USP\",\n                \"UCX\"\n            ]\n        },\n        \"CreateUnrealSkeletalMesh\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ],\n            \"joint_hints\": \"jnt_org\"\n        },\n        \"CreateMultiverseLook\": {\n            \"enabled\": true,\n            \"publish_mip_map\": true\n        },\n        \"CreateAnimation\": {\n            \"default_variants\": [],\n            \"step\": 1.0,\n            \"includeParentHierarchy\": false,\n            \"farm\": false,\n            \"priority\": 50,\n            \"refresh\": false,\n            \"include_user_defined_attributes\": false\n        },\n        \"CreateModel\": {\n            \"enabled\": true,\n            \"write_color_sets\": false,\n            \"write_face_sets\": false,\n            \"default_variants\": [\n                \"Main\",\n                \"Proxy\",\n                \"Sculpt\"\n            ]\n        },\n        \"CreatePointCache\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ],\n            \"step\": 1.0,\n            \"includeParentHierarchy\": false,\n            \"farm\": false,\n            \"priority\": 50,\n            \"refresh\": false,\n            \"include_user_defined_attributes\": false\n        },\n        \"CreateProxyAlembic\": {\n            \"enabled\": true,\n            \"write_color_sets\": false,\n            \"write_face_sets\": false,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateReview\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ],\n            \"useMayaTimeline\": true\n        },\n        \"CreateAss\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ],\n            \"expandProcedurals\": false,\n            \"motionBlur\": true,\n            \"motionBlurKeys\": 2,\n            \"motionBlurLength\": 0.5,\n            \"maskOptions\": false,\n            \"maskCamera\": false,\n            \"maskLight\": false,\n            \"maskShape\": false,\n            \"maskShader\": false,\n            \"maskOverride\": false,\n            \"maskDriver\": false,\n            \"maskFilter\": false,\n            \"maskOperator\": false,\n            \"maskColor_manager\": false\n        },\n        \"CreateVrayProxy\": {\n            \"enabled\": true,\n            \"vrmesh\": true,\n            \"alembic\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateMultiverseUsd\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateMultiverseUsdComp\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateMultiverseUsdOver\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateAssembly\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateCamera\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateLayout\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateMayaScene\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateRenderSetup\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateRig\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\",\n                \"Sim\",\n                \"Cloth\"\n            ]\n        },\n        \"CreateSetDress\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\",\n                \"Anim\"\n            ]\n        },\n        \"CreateVRayScene\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"CreateYetiRig\": {\n            \"enabled\": true,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        }\n    },\n    \"publish\": {\n        \"CollectMayaRender\": {\n            \"sync_workfile_version\": false\n        },\n        \"CollectFbxAnimation\": {\n            \"enabled\": true\n        },\n        \"CollectFbxCamera\": {\n            \"enabled\": false\n        },\n        \"CollectGLTF\": {\n            \"enabled\": false\n        },\n        \"ValidateInstanceInContext\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateContainers\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateFrameRange\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true,\n            \"exclude_families\": [\n                \"model\",\n                \"rig\",\n                \"staticMesh\"\n            ]\n        },\n        \"ValidateShaderName\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true,\n            \"regex\": \"(?P<asset>.*)_(.*)_SHD\"\n        },\n        \"ValidateShadingEngine\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMayaColorSpace\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateAttributes\": {\n            \"enabled\": false,\n            \"attributes\": {}\n        },\n        \"ValidateLoadedPlugin\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"whitelist_native_plugins\": false,\n            \"authorized_plugins\": []\n        },\n        \"ValidateMayaUnits\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"validate_linear_units\": true,\n            \"linear_units\": \"cm\",\n            \"validate_angular_units\": true,\n            \"angular_units\": \"deg\",\n            \"validate_fps\": true\n        },\n        \"ValidateUnrealStaticMeshName\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"validate_mesh\": false,\n            \"validate_collision\": true\n        },\n        \"ValidateCycleError\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"families\": [\n                \"rig\"\n            ]\n        },\n        \"ValidatePluginPathAttributes\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true,\n            \"attribute\": {\n                \"AlembicNode\": \"abc_File\",\n                \"VRayProxy\": \"fileName\",\n                \"RenderManArchive\": \"filename\",\n                \"pgYetiMaya\": \"cacheFileName\",\n                \"aiStandIn\": \"dso\",\n                \"RedshiftSprite\": \"tex0\",\n                \"RedshiftBokeh\": \"dofBokehImage\",\n                \"RedshiftCameraMap\": \"tex0\",\n                \"RedshiftEnvironment\": \"tex2\",\n                \"RedshiftDomeLight\": \"tex1\",\n                \"RedshiftIESLight\": \"profile\",\n                \"RedshiftLightGobo\": \"tex0\",\n                \"RedshiftNormalMap\": \"tex0\",\n                \"RedshiftProxyMesh\": \"fileName\",\n                \"RedshiftVolumeShape\": \"fileName\",\n                \"VRayTexGLSL\": \"fileName\",\n                \"VRayMtlGLSL\": \"fileName\",\n                \"VRayVRmatMtl\": \"fileName\",\n                \"VRayPtex\": \"ptexFile\",\n                \"VRayLightIESShape\": \"iesFile\",\n                \"VRayMesh\": \"materialAssignmentsFile\",\n                \"VRayMtlOSL\": \"fileName\",\n                \"VRayTexOSL\": \"fileName\",\n                \"VRayTexOCIO\": \"ocioConfigFile\",\n                \"VRaySettingsNode\": \"pmap_autoSaveFile2\",\n                \"VRayScannedMtl\": \"file\",\n                \"VRayScene\": \"parameterOverrideFilePath\",\n                \"VRayMtlMDL\": \"filename\",\n                \"VRaySimbiont\": \"file\",\n                \"dlOpenVDBShape\": \"filename\",\n                \"pgYetiMayaShape\": \"liveABCFilename\",\n                \"gpuCache\": \"cacheFileName\"\n            }\n        },\n        \"ValidateRenderSettings\": {\n            \"arnold_render_attributes\": [],\n            \"vray_render_attributes\": [],\n            \"redshift_render_attributes\": [],\n            \"renderman_render_attributes\": []\n        },\n        \"ValidateResolution\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateCurrentRenderLayerIsRenderable\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateGLSLMaterial\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateGLSLPlugin\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateRenderImageRule\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateRenderNoDefaultCameras\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateRenderSingleCamera\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateRenderLayerAOVs\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateStepSize\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateVRayDistributedRendering\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateVrayReferencedAOVs\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateVRayTranslatorEnabled\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateVrayProxy\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateVrayProxyMembers\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateYetiRenderScriptCallbacks\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateYetiRigCacheState\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateYetiRigInputShapesInInstance\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateYetiRigSettings\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateModelName\": {\n            \"enabled\": false,\n            \"database\": true,\n            \"material_file\": {\n                \"windows\": \"\",\n                \"darwin\": \"\",\n                \"linux\": \"\"\n            },\n            \"regex\": \"(.*)_(\\\\d)*_(?P<shader>.*)_(GEO)\",\n            \"top_level_regex\": \".*_GRP\"\n        },\n        \"ValidateModelContent\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"validate_top_group\": true\n        },\n        \"ValidateTransformNamingSuffix\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"SUFFIX_NAMING_TABLE\": {\n                \"mesh\": [\n                    \"_GEO\",\n                    \"_GES\",\n                    \"_GEP\",\n                    \"_OSD\"\n                ],\n                \"nurbsCurve\": [\n                    \"_CRV\"\n                ],\n                \"nurbsSurface\": [\n                    \"_NRB\"\n                ],\n                \"locator\": [\n                    \"_LOC\"\n                ],\n                \"group\": [\n                    \"_GRP\"\n                ]\n            },\n            \"ALLOW_IF_NOT_IN_SUFFIX_TABLE\": true\n        },\n        \"ValidateColorSets\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshHasOverlappingUVs\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshArnoldAttributes\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshShaderConnections\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshSingleUVSet\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshHasUVs\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshLaminaFaces\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshNgons\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshNonManifold\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshNoNegativeScale\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateMeshNonZeroEdgeLength\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshNormalsUnlocked\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshUVSetMap1\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMeshVerticesHaveEdges\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateNoAnimation\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateNoNamespace\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateNoNullTransforms\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateNoUnknownNodes\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateNodeNoGhosting\": {\n            \"enabled\": false,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateShapeDefaultNames\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateShapeRenderStats\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateShapeZero\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateTransformZero\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateUniqueNames\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateNoVRayMesh\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateUnrealMeshTriangulated\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateAlembicVisibleOnly\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ExtractProxyAlembic\": {\n            \"enabled\": true,\n            \"families\": [\n                \"proxyAbc\"\n            ]\n        },\n        \"ExtractAlembic\": {\n            \"enabled\": true,\n            \"families\": [\n                \"pointcache\",\n                \"model\",\n                \"vrayproxy.alembic\"\n            ],\n            \"flags\": [\n                \"stripNamespaces\",\n                \"writeNormals\",\n                \"worldSpace\",\n                \"uvWrite\"\n            ],\n            \"attr\": \"\",\n            \"attrPrefix\": \"\",\n            \"dataFormat\": \"ogawa\",\n            \"melPerFrameCallback\": \"\",\n            \"melPostJobCallback\": \"\",\n            \"preRollStartFrame\": 0,\n            \"pythonPerFrameCallback\": \"\",\n            \"pythonPostJobCallback\": \"\",\n            \"userAttr\": \"\",\n            \"userAttrPrefix\": \"\",\n            \"visibleOnly\": false,\n            \"overrides\": [\n                \"attr\",\n                \"attrPrefix\",\n                \"worldSpace\",\n                \"writeColorSets\",\n                \"writeNormals\",\n                \"writeFaceSets\",\n                \"renderableOnly\",\n                \"visibleOnly\"\n            ]\n        },\n        \"ExtractObj\": {\n            \"enabled\": false,\n            \"optional\": true\n        },\n        \"ValidateRigContents\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateRigJointsHidden\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateRigControllers\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateAnimatedReferenceRig\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateAnimationContent\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateOutRelatedNodeIds\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateRigControllersArnoldAttributes\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateSkeletalMeshHierarchy\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateSkeletonRigContents\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateSkeletonRigControllers\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateSkinclusterDeformerSet\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateRigOutSetNodeIds\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"allow_history_only\": false\n        },\n        \"ValidateSkeletonRigOutSetNodeIds\": {\n            \"enabled\": false,\n            \"optional\": false,\n            \"allow_history_only\": false\n        },\n        \"ValidateSkeletonRigOutputIds\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateSkeletonTopGroupHierarchy\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateCameraAttributes\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateAssemblyName\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateAssemblyNamespaces\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateAssemblyModelTransforms\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateAssRelativePaths\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateInstancerContent\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateInstancerFrameRanges\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateNoDefaultCameras\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"active\": true\n        },\n        \"ValidateUnrealUpAxis\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateCameraContents\": {\n            \"enabled\": true,\n            \"optional\": false,\n            \"validate_shapes\": true\n        },\n        \"ExtractPlayblast\": {\n            \"capture_preset\": {\n                \"Codec\": {\n                    \"compression\": \"png\",\n                    \"format\": \"image\",\n                    \"quality\": 95\n                },\n                \"Display Options\": {\n                    \"override_display\": true,\n                    \"background\": [\n                        125,\n                        125,\n                        125,\n                        255\n                    ],\n                    \"backgroundBottom\": [\n                        125,\n                        125,\n                        125,\n                        255\n                    ],\n                    \"backgroundTop\": [\n                        125,\n                        125,\n                        125,\n                        255\n                    ],\n                    \"displayGradient\": true\n                },\n                \"Generic\": {\n                    \"isolate_view\": true,\n                    \"off_screen\": true,\n                    \"pan_zoom\": false\n                },\n                \"Renderer\": {\n                    \"rendererName\": \"vp2Renderer\"\n                },\n                \"Resolution\": {\n                    \"width\": 1920,\n                    \"height\": 1080\n                },\n                \"Viewport Options\": {\n                    \"override_viewport_options\": true,\n                    \"displayLights\": \"default\",\n                    \"displayTextures\": true,\n                    \"textureMaxResolution\": 1024,\n                    \"renderDepthOfField\": true,\n                    \"shadows\": true,\n                    \"twoSidedLighting\": true,\n                    \"lineAAEnable\": true,\n                    \"multiSample\": 8,\n                    \"loadTextures\": false,\n                    \"useDefaultMaterial\": false,\n                    \"wireframeOnShaded\": false,\n                    \"xray\": false,\n                    \"jointXray\": false,\n                    \"backfaceCulling\": false,\n                    \"ssaoEnable\": false,\n                    \"ssaoAmount\": 1,\n                    \"ssaoRadius\": 16,\n                    \"ssaoFilterRadius\": 16,\n                    \"ssaoSamples\": 16,\n                    \"fogging\": false,\n                    \"hwFogFalloff\": \"0\",\n                    \"hwFogDensity\": 0.0,\n                    \"hwFogStart\": 0,\n                    \"hwFogEnd\": 100,\n                    \"hwFogAlpha\": 0,\n                    \"hwFogColorR\": 1.0,\n                    \"hwFogColorG\": 1.0,\n                    \"hwFogColorB\": 1.0,\n                    \"motionBlurEnable\": false,\n                    \"motionBlurSampleCount\": 8,\n                    \"motionBlurShutterOpenFraction\": 0.2,\n                    \"cameras\": false,\n                    \"clipGhosts\": false,\n                    \"deformers\": false,\n                    \"dimensions\": false,\n                    \"dynamicConstraints\": false,\n                    \"dynamics\": false,\n                    \"fluids\": false,\n                    \"follicles\": false,\n                    \"greasePencils\": false,\n                    \"grid\": false,\n                    \"hairSystems\": true,\n                    \"handles\": false,\n                    \"headsUpDisplay\": false,\n                    \"ikHandles\": false,\n                    \"imagePlane\": true,\n                    \"joints\": false,\n                    \"lights\": false,\n                    \"locators\": false,\n                    \"manipulators\": false,\n                    \"motionTrails\": false,\n                    \"nCloths\": false,\n                    \"nParticles\": false,\n                    \"nRigids\": false,\n                    \"controlVertices\": false,\n                    \"nurbsCurves\": false,\n                    \"hulls\": false,\n                    \"nurbsSurfaces\": false,\n                    \"particleInstancers\": false,\n                    \"pivots\": false,\n                    \"planes\": false,\n                    \"pluginShapes\": false,\n                    \"polymeshes\": true,\n                    \"strokes\": false,\n                    \"subdivSurfaces\": false,\n                    \"textures\": false,\n                    \"pluginObjects\": {\n                        \"gpuCacheDisplayFilter\": false\n                    }\n                },\n                \"Camera Options\": {\n                    \"displayGateMask\": false,\n                    \"displayResolution\": false,\n                    \"displayFilmGate\": false,\n                    \"displayFieldChart\": false,\n                    \"displaySafeAction\": false,\n                    \"displaySafeTitle\": false,\n                    \"displayFilmPivot\": false,\n                    \"displayFilmOrigin\": false,\n                    \"overscan\": 1.0\n                }\n            },\n            \"profiles\": []\n        },\n        \"ExtractMayaSceneRaw\": {\n            \"enabled\": true,\n            \"add_for_families\": [\n                \"layout\"\n            ]\n        },\n        \"ExtractCameraAlembic\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true,\n            \"bake_attributes\": []\n        },\n        \"ExtractCameraMayaScene\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true,\n            \"keep_image_planes\": false\n        },\n        \"ExtractGLB\": {\n            \"enabled\": true,\n            \"active\": true,\n            \"ogsfx_path\": \"/maya2glTF/PBR/shaders/glTF_PBR.ogsfx\"\n        },\n        \"ExtractLook\": {\n            \"maketx_arguments\": []\n        },\n        \"ExtractGPUCache\": {\n            \"enabled\": false,\n            \"families\": [\n                \"model\",\n                \"animation\",\n                \"pointcache\"\n            ],\n            \"step\": 1.0,\n            \"stepSave\": 1,\n            \"optimize\": true,\n            \"optimizationThreshold\": 40000,\n            \"optimizeAnimationsForMotionBlur\": true,\n            \"writeMaterials\": true,\n            \"useBaseTessellation\": true\n        }\n    },\n    \"load\": {\n        \"colors\": {\n            \"model\": [\n                209,\n                132,\n                30,\n                255\n            ],\n            \"rig\": [\n                59,\n                226,\n                235,\n                255\n            ],\n            \"pointcache\": [\n                94,\n                209,\n                30,\n                255\n            ],\n            \"animation\": [\n                94,\n                209,\n                30,\n                255\n            ],\n            \"ass\": [\n                249,\n                135,\n                53,\n                255\n            ],\n            \"camera\": [\n                136,\n                114,\n                244,\n                255\n            ],\n            \"fbx\": [\n                215,\n                166,\n                255,\n                255\n            ],\n            \"mayaAscii\": [\n                67,\n                174,\n                255,\n                255\n            ],\n            \"mayaScene\": [\n                67,\n                174,\n                255,\n                255\n            ],\n            \"setdress\": [\n                255,\n                250,\n                90,\n                255\n            ],\n            \"layout\": [\n                255,\n                250,\n                90,\n                255\n            ],\n            \"vdbcache\": [\n                249,\n                54,\n                0,\n                255\n            ],\n            \"vrayproxy\": [\n                255,\n                150,\n                12,\n                255\n            ],\n            \"vrayscene_layer\": [\n                255,\n                150,\n                12,\n                255\n            ],\n            \"yeticache\": [\n                99,\n                206,\n                220,\n                255\n            ],\n            \"yetiRig\": [\n                0,\n                205,\n                125,\n                255\n            ]\n        },\n        \"reference_loader\": {\n            \"namespace\": \"{asset_name}_{subset}_##_\",\n            \"group_name\": \"_GRP\",\n            \"display_handle\": true\n        },\n        \"import_loader\": {\n            \"namespace\": \"{asset_name}_{subset}_##_\",\n            \"group_name\": \"_GRP\"\n        }\n    },\n    \"workfile_build\": {\n        \"profiles\": [\n            {\n                \"task_types\": [],\n                \"tasks\": [\n                    \"Lighting\"\n                ],\n                \"current_context\": [\n                    {\n                        \"subset_name_filters\": [\n                            \".+[Mm]ain\"\n                        ],\n                        \"families\": [\n                            \"model\"\n                        ],\n                        \"repre_names\": [\n                            \"abc\",\n                            \"ma\"\n                        ],\n                        \"loaders\": [\n                            \"ReferenceLoader\"\n                        ]\n                    },\n                    {\n                        \"subset_name_filters\": [],\n                        \"families\": [\n                            \"animation\",\n                            \"pointcache\",\n                            \"proxyAbc\"\n                        ],\n                        \"repre_names\": [\n                            \"abc\"\n                        ],\n                        \"loaders\": [\n                            \"ReferenceLoader\"\n                        ]\n                    },\n                    {\n                        \"subset_name_filters\": [],\n                        \"families\": [\n                            \"rendersetup\"\n                        ],\n                        \"repre_names\": [\n                            \"json\"\n                        ],\n                        \"loaders\": [\n                            \"RenderSetupLoader\"\n                        ]\n                    },\n                    {\n                        \"subset_name_filters\": [],\n                        \"families\": [\n                            \"camera\"\n                        ],\n                        \"repre_names\": [\n                            \"abc\"\n                        ],\n                        \"loaders\": [\n                            \"ReferenceLoader\"\n                        ]\n                    }\n                ],\n                \"linked_assets\": [\n                    {\n                        \"subset_name_filters\": [],\n                        \"families\": [\n                            \"sedress\"\n                        ],\n                        \"repre_names\": [\n                            \"ma\"\n                        ],\n                        \"loaders\": [\n                            \"ReferenceLoader\"\n                        ]\n                    },\n                    {\n                        \"subset_name_filters\": [],\n                        \"families\": [\n                            \"ArnoldStandin\"\n                        ],\n                        \"repre_names\": [\n                            \"ass\"\n                        ],\n                        \"loaders\": [\n                            \"assLoader\"\n                        ]\n                    }\n                ]\n            }\n        ]\n    },\n    \"templated_workfile_build\": {\n        \"profiles\": []\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/nuke.json",
    "content": "{\n    \"general\": {\n        \"menu\": {\n            \"create\": \"ctrl+alt+c\",\n            \"publish\": \"ctrl+alt+p\",\n            \"load\": \"ctrl+alt+l\",\n            \"manage\": \"ctrl+alt+m\",\n            \"build_workfile\": \"ctrl+alt+b\"\n        }\n    },\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        },\n        \"viewer\": {\n            \"viewerProcess\": \"ACES/sRGB\"\n        },\n        \"baking\": {\n            \"viewerProcess\": \"ACES/Rec.709\"\n        },\n        \"workfile\": {\n            \"colorManagement\": \"OCIO\",\n            \"OCIO_config\": \"aces_1.2\",\n            \"workingSpaceLUT\": \"role_scene_linear\",\n            \"monitorLut\": \"ACES/sRGB\",\n            \"monitorOutLUT\": \"ACES/sRGB\",\n            \"int8Lut\": \"role_matte_paint\",\n            \"int16Lut\": \"role_texture_paint\",\n            \"logLut\": \"role_compositing_log\",\n            \"floatLut\": \"role_scene_linear\"\n        },\n        \"nodes\": {\n            \"requiredNodes\": [\n                {\n                    \"plugins\": [\n                        \"CreateWriteRender\"\n                    ],\n                    \"nukeNodeClass\": \"Write\",\n                    \"knobs\": [\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"file_type\",\n                            \"value\": \"exr\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"datatype\",\n                            \"value\": \"16 bit half\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"compression\",\n                            \"value\": \"Zip (1 scanline)\"\n                        },\n                        {\n                            \"type\": \"bool\",\n                            \"name\": \"autocrop\",\n                            \"value\": true\n                        },\n                        {\n                            \"type\": \"color_gui\",\n                            \"name\": \"tile_color\",\n                            \"value\": [\n                                186,\n                                35,\n                                35,\n                                255\n                            ]\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"channels\",\n                            \"value\": \"rgb\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"colorspace\",\n                            \"value\": \"scene_linear\"\n                        },\n                        {\n                            \"type\": \"bool\",\n                            \"name\": \"create_directories\",\n                            \"value\": true\n                        }\n                    ]\n                },\n                {\n                    \"plugins\": [\n                        \"CreateWritePrerender\"\n                    ],\n                    \"nukeNodeClass\": \"Write\",\n                    \"knobs\": [\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"file_type\",\n                            \"value\": \"exr\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"datatype\",\n                            \"value\": \"16 bit half\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"compression\",\n                            \"value\": \"Zip (1 scanline)\"\n                        },\n                        {\n                            \"type\": \"bool\",\n                            \"name\": \"autocrop\",\n                            \"value\": true\n                        },\n                        {\n                            \"type\": \"color_gui\",\n                            \"name\": \"tile_color\",\n                            \"value\": [\n                                171,\n                                171,\n                                10,\n                                255\n                            ]\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"channels\",\n                            \"value\": \"rgb\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"colorspace\",\n                            \"value\": \"scene_linear\"\n                        },\n                        {\n                            \"type\": \"bool\",\n                            \"name\": \"create_directories\",\n                            \"value\": true\n                        }\n                    ]\n                },\n                {\n                    \"plugins\": [\n                        \"CreateWriteImage\"\n                    ],\n                    \"nukeNodeClass\": \"Write\",\n                    \"knobs\": [\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"file_type\",\n                            \"value\": \"tiff\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"datatype\",\n                            \"value\": \"16 bit\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"compression\",\n                            \"value\": \"Deflate\"\n                        },\n                        {\n                            \"type\": \"color_gui\",\n                            \"name\": \"tile_color\",\n                            \"value\": [\n                                56,\n                                162,\n                                7,\n                                255\n                            ]\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"channels\",\n                            \"value\": \"rgb\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"colorspace\",\n                            \"value\": \"texture_paint\"\n                        },\n                        {\n                            \"type\": \"bool\",\n                            \"name\": \"create_directories\",\n                            \"value\": true\n                        }\n                    ]\n                }\n            ],\n            \"overrideNodes\": []\n        },\n        \"regexInputs\": {\n            \"inputs\": [\n                {\n                    \"regex\": \"(beauty).*(?=.exr)\",\n                    \"colorspace\": \"scene_linear\"\n                }\n            ]\n        }\n    },\n    \"nuke-dirmap\": {\n        \"enabled\": false,\n        \"paths\": {\n            \"source-path\": [],\n            \"destination-path\": []\n        }\n    },\n    \"scriptsmenu\": {\n        \"name\": \"OpenPype Tools\",\n        \"definition\": [\n            {\n                \"type\": \"action\",\n                \"sourcetype\": \"python\",\n                \"title\": \"OpenPype Docs\",\n                \"command\": \"import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_nuke_tut')\",\n                \"tooltip\": \"Open the OpenPype Nuke user doc page\"\n            },\n            {\n                \"type\": \"action\",\n                \"sourcetype\": \"python\",\n                \"title\": \"Set Frame Start (Read Node)\",\n                \"command\": \"from openpype.hosts.nuke.startup.frame_setting_for_read_nodes import main;main();\",\n                \"tooltip\": \"Set frame start for read node(s)\"\n            },\n            {\n                \"type\": \"action\",\n                \"sourcetype\": \"python\",\n                \"title\": \"Set non publish output for Write Node\",\n                \"command\": \"from openpype.hosts.nuke.startup.custom_write_node import main;main();\",\n                \"tooltip\": \"Open the OpenPype Nuke user doc page\"\n            }\n        ]\n    },\n    \"gizmo\": [\n        {\n            \"toolbar_menu_name\": \"OpenPype Gizmo\",\n            \"gizmo_source_dir\": {\n                \"windows\": [],\n                \"darwin\": [],\n                \"linux\": []\n            },\n            \"toolbar_icon_path\": {\n                \"windows\": \"\",\n                \"darwin\": \"\",\n                \"linux\": \"\"\n            },\n            \"gizmo_definition\": [\n                {\n                    \"gizmo_toolbar_path\": \"/path/to/menu\",\n                    \"sub_gizmo_list\": [\n                        {\n                            \"sourcetype\": \"python\",\n                            \"title\": \"Gizmo Note\",\n                            \"command\": \"nuke.nodes.StickyNote(label='You can create your own toolbar menu in the Nuke GizmoMenu of OpenPype')\",\n                            \"icon\": \"\",\n                            \"shortcut\": \"\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ],\n    \"create\": {\n        \"CreateWriteRender\": {\n            \"temp_rendering_path_template\": \"{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}\",\n            \"default_variants\": [\n                \"Main\",\n                \"Mask\"\n            ],\n            \"instance_attributes\": [\n                \"reviewable\",\n                \"farm_rendering\"\n            ],\n            \"prenodes\": {\n                \"Reformat01\": {\n                    \"nodeclass\": \"Reformat\",\n                    \"dependent\": \"\",\n                    \"knobs\": [\n                        {\n                            \"type\": \"text\",\n                            \"name\": \"resize\",\n                            \"value\": \"none\"\n                        },\n                        {\n                            \"type\": \"bool\",\n                            \"name\": \"black_outside\",\n                            \"value\": true\n                        }\n                    ]\n                }\n            }\n        },\n        \"CreateWritePrerender\": {\n            \"temp_rendering_path_template\": \"{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}\",\n            \"default_variants\": [\n                \"Key01\",\n                \"Bg01\",\n                \"Fg01\",\n                \"Branch01\",\n                \"Part01\"\n            ],\n            \"instance_attributes\": [\n                \"farm_rendering\",\n                \"use_range_limit\"\n            ],\n            \"prenodes\": {}\n        },\n        \"CreateWriteImage\": {\n            \"temp_rendering_path_template\": \"{work}/renders/nuke/{subset}/{subset}.{ext}\",\n            \"default_variants\": [\n                \"StillFrame\",\n                \"MPFrame\",\n                \"LayoutFrame\"\n            ],\n            \"instance_attributes\": [\n                \"use_range_limit\"\n            ],\n            \"prenodes\": {\n                \"FrameHold01\": {\n                    \"nodeclass\": \"FrameHold\",\n                    \"dependent\": \"\",\n                    \"knobs\": [\n                        {\n                            \"type\": \"expression\",\n                            \"name\": \"first_frame\",\n                            \"expression\": \"parent.first\"\n                        }\n                    ]\n                }\n            }\n        }\n    },\n    \"publish\": {\n        \"CollectInstanceData\": {\n            \"sync_workfile_version_on_families\": [\n                \"nukenodes\",\n                \"camera\",\n                \"gizmo\",\n                \"source\",\n                \"render\",\n                \"write\"\n            ]\n        },\n        \"ValidateCorrectAssetContext\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateContainers\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateKnobs\": {\n            \"enabled\": false,\n            \"knobs\": {\n                \"render\": {\n                    \"review\": true\n                }\n            }\n        },\n        \"ValidateOutputResolution\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateBackdrop\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateGizmo\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateScriptAttributes\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ExtractReviewData\": {\n            \"enabled\": false\n        },\n        \"ExtractReviewDataLut\": {\n            \"enabled\": false\n        },\n        \"ExtractReviewDataMov\": {\n            \"enabled\": true,\n            \"viewer_lut_raw\": false,\n            \"outputs\": {\n                \"baking\": {\n                    \"filter\": {\n                        \"task_types\": [],\n                        \"families\": [],\n                        \"subsets\": []\n                    },\n                    \"read_raw\": false,\n                    \"viewer_process_override\": \"\",\n                    \"bake_viewer_process\": true,\n                    \"bake_viewer_input_process\": true,\n                    \"reformat_nodes_config\": {\n                        \"enabled\": false,\n                        \"reposition_nodes\": [\n                            {\n                                \"node_class\": \"Reformat\",\n                                \"knobs\": [\n                                    {\n                                        \"type\": \"text\",\n                                        \"name\": \"type\",\n                                        \"value\": \"to format\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"name\": \"format\",\n                                        \"value\": \"HD_1080\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"name\": \"filter\",\n                                        \"value\": \"Lanczos6\"\n                                    },\n                                    {\n                                        \"type\": \"bool\",\n                                        \"name\": \"black_outside\",\n                                        \"value\": true\n                                    },\n                                    {\n                                        \"type\": \"bool\",\n                                        \"name\": \"pbb\",\n                                        \"value\": false\n                                    }\n                                ]\n                            }\n                        ]\n                    },\n                    \"extension\": \"mov\",\n                    \"add_custom_tags\": []\n                }\n            }\n        },\n        \"ExtractReviewIntermediates\": {\n            \"enabled\": true,\n            \"viewer_lut_raw\": false,\n            \"outputs\": {\n                \"baking\": {\n                    \"filter\": {\n                        \"task_types\": [],\n                        \"families\": [],\n                        \"subsets\": []\n                    },\n                    \"read_raw\": false,\n                    \"viewer_process_override\": \"\",\n                    \"bake_viewer_process\": true,\n                    \"bake_viewer_input_process\": true,\n                    \"reformat_nodes_config\": {\n                        \"enabled\": false,\n                        \"reposition_nodes\": [\n                            {\n                                \"node_class\": \"Reformat\",\n                                \"knobs\": [\n                                    {\n                                        \"type\": \"text\",\n                                        \"name\": \"type\",\n                                        \"value\": \"to format\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"name\": \"format\",\n                                        \"value\": \"HD_1080\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"name\": \"filter\",\n                                        \"value\": \"Lanczos6\"\n                                    },\n                                    {\n                                        \"type\": \"bool\",\n                                        \"name\": \"black_outside\",\n                                        \"value\": true\n                                    },\n                                    {\n                                        \"type\": \"bool\",\n                                        \"name\": \"pbb\",\n                                        \"value\": false\n                                    }\n                                ]\n                            }\n                        ]\n                    },\n                    \"extension\": \"mov\",\n                    \"add_custom_tags\": []\n                }\n            }\n        },\n        \"ExtractSlateFrame\": {\n            \"viewer_lut_raw\": false,\n            \"key_value_mapping\": {\n                \"f_submission_note\": [\n                    true,\n                    \"{comment}\"\n                ],\n                \"f_submitting_for\": [\n                    true,\n                    \"{intent[value]}\"\n                ],\n                \"f_vfx_scope_of_work\": [\n                    false,\n                    \"\"\n                ]\n            }\n        },\n        \"IncrementScriptVersion\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        }\n    },\n    \"load\": {\n        \"LoadImage\": {\n            \"enabled\": true,\n            \"_representations\": [],\n            \"node_name_template\": \"{class_name}_{ext}\"\n        },\n        \"LoadClip\": {\n            \"enabled\": true,\n            \"_representations\": [],\n            \"node_name_template\": \"{class_name}_{ext}\",\n            \"options_defaults\": {\n                \"start_at_workfile\": true,\n                \"add_retime\": true\n            }\n        }\n    },\n    \"workfile_builder\": {\n        \"create_first_version\": false,\n        \"custom_templates\": [],\n        \"builder_on_start\": false,\n        \"profiles\": []\n    },\n    \"templated_workfile_build\": {\n        \"profiles\": []\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/photoshop.json",
    "content": "{\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"remapping\": {\n            \"rules\": []\n        },\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"create\": {\n        \"ImageCreator\": {\n            \"enabled\": true,\n            \"active_on_create\": true,\n            \"mark_for_review\": false,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        },\n        \"AutoImageCreator\": {\n            \"enabled\": false,\n            \"active_on_create\": true,\n            \"mark_for_review\": false,\n            \"default_variant\": \"\"\n        },\n        \"ReviewCreator\": {\n            \"enabled\": true,\n            \"active_on_create\": true,\n            \"default_variant\": \"\"\n        },\n        \"WorkfileCreator\": {\n            \"enabled\": true,\n            \"active_on_create\": true,\n            \"default_variant\": \"Main\"\n        }\n    },\n    \"publish\": {\n        \"CollectColorCodedInstances\": {\n            \"enabled\": true,\n            \"create_flatten_image\": \"no\",\n            \"flatten_subset_template\": \"\",\n            \"color_code_mapping\": []\n        },\n        \"CollectReview\": {\n            \"enabled\": true\n        },\n        \"CollectVersion\": {\n            \"enabled\": false\n        },\n        \"ValidateContainers\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateNaming\": {\n            \"invalid_chars\": \"[ \\\\\\\\/+\\\\*\\\\?\\\\(\\\\)\\\\[\\\\]\\\\{\\\\}:,;]\",\n            \"replace_char\": \"_\"\n        },\n        \"ExtractImage\": {\n            \"formats\": [\n                \"png\",\n                \"jpg\"\n            ]\n        },\n        \"ExtractReview\": {\n            \"make_image_sequence\": false,\n            \"max_downscale_size\": 8192,\n            \"jpg_options\": {\n                \"tags\": [\n                    \"review\",\n                    \"ftrackreview\"\n                ]\n            },\n            \"mov_options\": {\n                \"tags\": [\n                    \"review\",\n                    \"ftrackreview\"\n                ]\n            }\n        }\n    },\n    \"workfile_builder\": {\n        \"create_first_version\": false,\n        \"custom_templates\": []\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/resolve.json",
    "content": "{\n    \"launch_openpype_menu_on_start\": false,\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"remapping\": {\n            \"rules\": []\n        },\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"create\": {\n        \"CreateShotClip\": {\n            \"hierarchy\": \"{folder}/{sequence}\",\n            \"clipRename\": true,\n            \"clipName\": \"{track}{sequence}{shot}\",\n            \"countFrom\": 10,\n            \"countSteps\": 10,\n            \"folder\": \"shots\",\n            \"episode\": \"ep01\",\n            \"sequence\": \"sq01\",\n            \"track\": \"{_track_}\",\n            \"shot\": \"sh###\",\n            \"vSyncOn\": false,\n            \"workfileFrameStart\": 1001,\n            \"handleStart\": 10,\n            \"handleEnd\": 10\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/royalrender.json",
    "content": "{\n    \"rr_paths\": [\n        \"default\"\n    ],\n    \"publish\": {\n        \"CollectSequencesFromJob\": {\n            \"review\": true\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/shotgrid.json",
    "content": "{\n    \"shotgrid_project_id\": 0,\n    \"shotgrid_server\": \"\",\n    \"event\": {\n        \"enabled\": false\n    },\n    \"fields\": {\n        \"asset\": {\n            \"type\": \"sg_asset_type\"\n        },\n        \"sequence\": {\n            \"episode_link\": \"episode\"\n        },\n        \"shot\": {\n            \"episode_link\": \"sg_episode\",\n            \"sequence_link\": \"sg_sequence\"\n        },\n        \"task\": {\n            \"step\": \"step\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/slack.json",
    "content": "{\n    \"token\": \"\",\n    \"publish\": {\n        \"CollectSlackFamilies\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"profiles\": [\n                {\n                    \"families\": [],\n                    \"hosts\": [],\n                    \"task_types\": [],\n                    \"tasks\": [],\n                    \"subsets\": [],\n                    \"review_upload_limit\": 50.0,\n                    \"channel_messages\": []\n                }\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/standalonepublisher.json",
    "content": "{\n    \"create\": {\n        \"create_workfile\": {\n            \"name\": \"workfile\",\n            \"label\": \"Workfile\",\n            \"family\": \"workfile\",\n            \"icon\": \"cube\",\n            \"defaults\": [\n                \"Main\"\n            ],\n            \"help\": \"Working scene backup\"\n        },\n        \"create_model\": {\n            \"name\": \"model\",\n            \"label\": \"Model\",\n            \"family\": \"model\",\n            \"icon\": \"cube\",\n            \"defaults\": [\n                \"Main\"\n            ],\n            \"help\": \"Polygonal static geometry\"\n        },\n        \"create_rig\": {\n            \"name\": \"rig\",\n            \"label\": \"Rig\",\n            \"family\": \"rig\",\n            \"icon\": \"wheelchair\",\n            \"defaults\": [\n                \"Main\",\n                \"Cloth\"\n            ],\n            \"help\": \"Artist-friendly rig with controls\"\n        },\n        \"create_pointcache\": {\n            \"name\": \"pointcache\",\n            \"label\": \"Pointcache\",\n            \"family\": \"pointcache\",\n            \"icon\": \"gears\",\n            \"defaults\": [\n                \"Main\"\n            ],\n            \"help\": \"Alembic pointcache for animated data\"\n        },\n        \"create_plate\": {\n            \"name\": \"plate\",\n            \"label\": \"Plate\",\n            \"family\": \"plate\",\n            \"icon\": \"camera\",\n            \"defaults\": [\n                \"Main\",\n                \"BG\",\n                \"Animatic\",\n                \"Reference\",\n                \"Offline\"\n            ],\n            \"help\": \"Footage for composting or reference\"\n        },\n        \"create_camera\": {\n            \"name\": \"camera\",\n            \"label\": \"Camera\",\n            \"family\": \"camera\",\n            \"icon\": \"camera\",\n            \"defaults\": [\n                \"Main\"\n            ],\n            \"help\": \"video-camera\"\n        },\n        \"create_editorial\": {\n            \"name\": \"editorial\",\n            \"label\": \"Editorial\",\n            \"family\": \"editorial\",\n            \"icon\": \"image\",\n            \"defaults\": [\n                \"Main\"\n            ],\n            \"help\": \"Editorial files to generate shots.\"\n        },\n        \"create_image\": {\n            \"name\": \"image\",\n            \"label\": \"Image file\",\n            \"family\": \"image\",\n            \"icon\": \"image\",\n            \"defaults\": [\n                \"Reference\",\n                \"Texture\",\n                \"ConceptArt\",\n                \"MattePaint\"\n            ],\n            \"help\": \"Holder for all kinds of image data\"\n        },\n        \"create_matchmove\": {\n            \"name\": \"matchmove\",\n            \"label\": \"Matchmove Scripts\",\n            \"family\": \"matchmove\",\n            \"icon\": \"empire\",\n            \"defaults\": [\n                \"Camera\",\n                \"Object\",\n                \"Mocap\"\n            ],\n            \"help\": \"Script exported from matchmoving application\"\n        },\n        \"create_render\": {\n            \"name\": \"render\",\n            \"label\": \"Render\",\n            \"family\": \"render\",\n            \"icon\": \"image\",\n            \"defaults\": [\n                \"Animation\",\n                \"Lighting\",\n                \"Lookdev\",\n                \"Compositing\"\n            ],\n            \"help\": \"Rendered images or video files\"\n        },\n        \"create_mov_batch\": {\n            \"name\": \"mov_batch\",\n            \"label\": \"Batch Mov\",\n            \"family\": \"render_mov_batch\",\n            \"icon\": \"image\",\n            \"defaults\": [\n                \"Main\"\n            ],\n            \"help\": \"Process multiple Mov files and publish them for layout and comp.\"\n        },\n        \"create_texture_batch\": {\n            \"name\": \"texture_batch\",\n            \"label\": \"Texture Batch\",\n            \"family\": \"texture_batch\",\n            \"icon\": \"image\",\n            \"defaults\": [\n                \"Main\"\n            ],\n            \"help\": \"Texture files with UDIM together with worfile\"\n        },\n        \"create_vdb\": {\n            \"name\": \"vdb\",\n            \"label\": \"VDB Volumetric Data\",\n            \"family\": \"vdbcache\",\n            \"icon\": \"cloud\",\n            \"defaults\": [],\n            \"help\": \"Hierarchical data structure for the efficient storage and manipulation of sparse volumetric data discretized on three-dimensional grids\"\n        },\n        \"__dynamic_keys_labels__\": {\n            \"create_workfile\": \"Workfile\",\n            \"create_model\": \"Model\",\n            \"create_rig\": \"Rig\",\n            \"create_pointcache\": \"Pointcache\",\n            \"create_plate\": \"Plate\",\n            \"create_camera\": \"Camera\",\n            \"create_editorial\": \"Editorial\",\n            \"create_image\": \"Image\",\n            \"create_matchmove\": \"Matchmove\",\n            \"create_render\": \"Render\",\n            \"create_mov_batch\": \"Batch Mov\",\n            \"create_texture_batch\": \"Batch Texture\",\n            \"create_simple_unreal_texture\": \"Simple Unreal Texture\",\n            \"create_vdb\": \"VDB Cache\"\n        }\n    },\n    \"publish\": {\n        \"CollectTextures\": {\n            \"enabled\": true,\n            \"active\": true,\n            \"main_workfile_extensions\": [\n                \"mra\"\n            ],\n            \"other_workfile_extensions\": [\n                \"spp\",\n                \"psd\"\n            ],\n            \"texture_extensions\": [\n                \"exr\",\n                \"dpx\",\n                \"jpg\",\n                \"jpeg\",\n                \"png\",\n                \"tiff\",\n                \"tga\",\n                \"gif\",\n                \"svg\"\n            ],\n            \"workfile_families\": [],\n            \"texture_families\": [],\n            \"color_space\": [\n                \"sRGB\",\n                \"Raw\",\n                \"ACEScg\"\n            ],\n            \"input_naming_patterns\": {\n                \"workfile\": [\n                    \"^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+\"\n                ],\n                \"textures\": [\n                    \"^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+\"\n                ]\n            },\n            \"input_naming_groups\": {\n                \"workfile\": [\n                    \"asset\",\n                    \"filler\",\n                    \"version\"\n                ],\n                \"textures\": [\n                    \"asset\",\n                    \"shader\",\n                    \"version\",\n                    \"channel\",\n                    \"color_space\",\n                    \"udim\"\n                ]\n            },\n            \"workfile_subset_template\": \"textures{Subset}Workfile\",\n            \"texture_subset_template\": \"textures{Subset}_{Shader}_{Channel}\"\n        },\n        \"ValidateSceneSettings\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true,\n            \"check_extensions\": [\n                \"exr\",\n                \"dpx\",\n                \"jpg\",\n                \"jpeg\",\n                \"png\",\n                \"tiff\",\n                \"tga\",\n                \"gif\",\n                \"svg\"\n            ],\n            \"families\": [\n                \"render\"\n            ],\n            \"skip_timelines_check\": []\n        },\n        \"ExtractThumbnailSP\": {\n            \"ffmpeg_args\": {\n                \"input\": [\n                    \"-apply_trc gamma22\"\n                ],\n                \"output\": []\n            }\n        },\n        \"CollectEditorial\": {\n            \"source_dir\": \"\",\n            \"extensions\": [\n                \"mov\",\n                \"mp4\"\n            ]\n        },\n        \"CollectHierarchyInstance\": {\n            \"shot_rename\": true,\n            \"shot_rename_template\": \"{project[code]}_{_sequence_}_{_shot_}\",\n            \"shot_rename_search_patterns\": {\n                \"_sequence_\": \"(sc\\\\d{3})\",\n                \"_shot_\": \"(sh\\\\d{3})\"\n            },\n            \"shot_add_hierarchy\": {\n                \"enabled\": true,\n                \"parents_path\": \"{project}/{folder}/{sequence}\",\n                \"parents\": {\n                    \"project\": \"{project[name]}\",\n                    \"sequence\": \"{_sequence_}\",\n                    \"folder\": \"shots\"\n                }\n            },\n            \"shot_add_tasks\": {}\n        },\n        \"CollectInstances\": {\n            \"custom_start_frame\": 0,\n            \"timeline_frame_start\": 900000,\n            \"timeline_frame_offset\": 0,\n            \"subsets\": {\n                \"referenceMain\": {\n                    \"family\": \"review\",\n                    \"families\": [\n                        \"clip\"\n                    ],\n                    \"extensions\": [\n                        \"mp4\"\n                    ],\n                    \"version\": 0,\n                    \"keepSequence\": false\n                },\n                \"audioMain\": {\n                    \"family\": \"audio\",\n                    \"families\": [\n                        \"clip\"\n                    ],\n                    \"extensions\": [\n                        \"wav\"\n                    ],\n                    \"version\": 0,\n                    \"keepSequence\": false\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/substancepainter.json",
    "content": "{\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"shelves\": {},\n    \"load\": {\n        \"SubstanceLoadProjectMesh\": {\n            \"project_templates\": [\n                {\n                    \"name\": \"2K(Default)\",\n                    \"default_texture_resolution\": 2048,\n                    \"import_cameras\": true,\n                    \"normal_map_format\": \"NormalMapFormat.DirectX\",\n                    \"project_workflow\": \"ProjectWorkflow.Default\",\n                    \"tangent_space_mode\": \"TangentSpace.PerFragment\",\n                    \"preserve_strokes\": true\n                },\n                {\n                    \"name\": \"2K(UV tile)\",\n                    \"default_texture_resolution\": 2048,\n                    \"import_cameras\": true,\n                    \"normal_map_format\": \"NormalMapFormat.DirectX\",\n                    \"project_workflow\": \"ProjectWorkflow.UVTile\",\n                    \"tangent_space_mode\": \"TangentSpace.PerFragment\",\n                    \"preserve_strokes\": true\n                },\n                {\n                    \"name\": \"4K(Custom)\",\n                    \"default_texture_resolution\": 4096,\n                    \"import_cameras\": true,\n                    \"normal_map_format\": \"NormalMapFormat.OpenGL\",\n                    \"project_workflow\": \"ProjectWorkflow.UVTile\",\n                    \"tangent_space_mode\": \"TangentSpace.PerFragment\",\n                    \"preserve_strokes\": true\n                }\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/traypublisher.json",
    "content": "{\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"simple_creators\": [\n        {\n            \"family\": \"workfile\",\n            \"identifier\": \"\",\n            \"label\": \"Workfile\",\n            \"icon\": \"fa.file\",\n            \"default_variants\": [\n                \"Main\"\n            ],\n            \"description\": \"Backup of a working scene\",\n            \"detailed_description\": \"Workfiles are full scenes from any application that are directly edited by artists. They represent a state of work on a task at a given point and are usually not directly referenced into other scenes.\",\n            \"allow_sequences\": false,\n            \"allow_multiple_items\": false,\n            \"allow_version_control\": false,\n            \"extensions\": [\n                \".ma\",\n                \".mb\",\n                \".nk\",\n                \".hrox\",\n                \".hip\",\n                \".hiplc\",\n                \".hipnc\",\n                \".blend\",\n                \".scn\",\n                \".tvpp\",\n                \".comp\",\n                \".zip\",\n                \".prproj\",\n                \".drp\",\n                \".psd\",\n                \".psb\",\n                \".aep\"\n            ]\n        },\n        {\n            \"family\": \"model\",\n            \"identifier\": \"\",\n            \"label\": \"Model\",\n            \"icon\": \"fa.cubes\",\n            \"default_variants\": [\n                \"Main\",\n                \"Proxy\",\n                \"Sculpt\"\n            ],\n            \"description\": \"Clean models\",\n            \"detailed_description\": \"Models should only contain geometry data, without any extras like cameras, locators or bones.\\n\\nKeep in mind that models published from tray publisher are not validated for correctness. \",\n            \"allow_sequences\": false,\n            \"allow_multiple_items\": true,\n            \"allow_version_control\": false,\n            \"extensions\": [\n                \".ma\",\n                \".mb\",\n                \".obj\",\n                \".abc\",\n                \".fbx\",\n                \".bgeo\",\n                \".bgeogz\",\n                \".bgeosc\",\n                \".usd\",\n                \".blend\"\n            ]\n        },\n        {\n            \"family\": \"pointcache\",\n            \"identifier\": \"\",\n            \"label\": \"Pointcache\",\n            \"icon\": \"fa.gears\",\n            \"default_variants\": [\n                \"Main\"\n            ],\n            \"description\": \"Geometry Caches\",\n            \"detailed_description\": \"Alembic or bgeo cache of animated data\",\n            \"allow_sequences\": true,\n            \"allow_multiple_items\": true,\n            \"allow_version_control\": false,\n            \"extensions\": [\n                \".abc\",\n                \".bgeo\",\n                \".bgeogz\",\n                \".bgeosc\"\n            ]\n        },\n        {\n            \"family\": \"plate\",\n            \"identifier\": \"\",\n            \"label\": \"Plate\",\n            \"icon\": \"mdi.camera-image\",\n            \"default_variants\": [\n                \"Main\",\n                \"BG\",\n                \"Animatic\",\n                \"Reference\",\n                \"Offline\"\n            ],\n            \"description\": \"Footage Plates\",\n            \"detailed_description\": \"Any type of image seqeuence coming from outside of the studio. Usually camera footage, but could also be animatics used for reference.\",\n            \"allow_sequences\": true,\n            \"allow_multiple_items\": true,\n            \"allow_version_control\": false,\n            \"extensions\": [\n                \".exr\",\n                \".png\",\n                \".dpx\",\n                \".jpg\",\n                \".tiff\",\n                \".tif\",\n                \".mov\",\n                \".mp4\",\n                \".avi\"\n            ]\n        },\n        {\n            \"family\": \"render\",\n            \"identifier\": \"\",\n            \"label\": \"Render\",\n            \"icon\": \"mdi.folder-multiple-image\",\n            \"default_variants\": [],\n            \"description\": \"Rendered images or video\",\n            \"detailed_description\": \"Sequence or single file renders\",\n            \"allow_sequences\": true,\n            \"allow_multiple_items\": true,\n            \"allow_version_control\": false,\n            \"extensions\": [\n                \".exr\",\n                \".png\",\n                \".dpx\",\n                \".jpg\",\n                \".jpeg\",\n                \".tiff\",\n                \".tif\",\n                \".mov\",\n                \".mp4\",\n                \".avi\"\n            ]\n        },\n        {\n            \"family\": \"camera\",\n            \"identifier\": \"\",\n            \"label\": \"Camera\",\n            \"icon\": \"fa.video-camera\",\n            \"default_variants\": [],\n            \"description\": \"3d Camera\",\n            \"detailed_description\": \"Ideally this should be only camera itself with baked animation, however, it can technically also include helper geometry.\",\n            \"allow_sequences\": false,\n            \"allow_multiple_items\": true,\n            \"allow_version_control\": false,\n            \"extensions\": [\n                \".abc\",\n                \".ma\",\n                \".hip\",\n                \".blend\",\n                \".fbx\",\n                \".usd\"\n            ]\n        },\n        {\n            \"family\": \"image\",\n            \"identifier\": \"\",\n            \"label\": \"Image\",\n            \"icon\": \"fa.image\",\n            \"default_variants\": [\n                \"Reference\",\n                \"Texture\",\n                \"Concept\",\n                \"Background\"\n            ],\n            \"description\": \"Single image\",\n            \"detailed_description\": \"Any image data can be published as image family. References, textures, concept art, matte paints. This is a fallback 2d family for everything that doesn't fit more specific family.\",\n            \"allow_sequences\": false,\n            \"allow_multiple_items\": true,\n            \"allow_version_control\": false,\n            \"extensions\": [\n                \".exr\",\n                \".jpg\",\n                \".jpeg\",\n                \".dpx\",\n                \".bmp\",\n                \".tif\",\n                \".tiff\",\n                \".png\",\n                \".psb\",\n                \".psd\"\n            ]\n        },\n        {\n            \"family\": \"vdb\",\n            \"identifier\": \"\",\n            \"label\": \"VDB Volumes\",\n            \"icon\": \"fa.cloud\",\n            \"default_variants\": [],\n            \"description\": \"Sparse volumetric data\",\n            \"detailed_description\": \"Hierarchical data structure for the efficient storage and manipulation of sparse volumetric data discretized on three-dimensional grids\",\n            \"allow_sequences\": true,\n            \"allow_multiple_items\": true,\n            \"allow_version_control\": false,\n            \"extensions\": [\n                \".vdb\"\n            ]\n        },\n        {\n            \"family\": \"matchmove\",\n            \"identifier\": \"\",\n            \"label\": \"Matchmove\",\n            \"icon\": \"fa.empire\",\n            \"default_variants\": [\n                \"Camera\",\n                \"Object\",\n                \"Mocap\"\n            ],\n            \"description\": \"Matchmoving script\",\n            \"detailed_description\": \"Script exported from matchmoving application to be later processed into a tracked camera with additional data\",\n            \"allow_sequences\": false,\n            \"allow_multiple_items\": true,\n            \"allow_version_control\": false,\n            \"extensions\": []\n        },\n        {\n            \"family\": \"rig\",\n            \"identifier\": \"\",\n            \"label\": \"Rig\",\n            \"icon\": \"fa.wheelchair\",\n            \"default_variants\": [],\n            \"description\": \"CG rig file\",\n            \"detailed_description\": \"CG rigged character or prop. Rig should be clean of any extra data and directly loadable into it's respective application\\t\",\n            \"allow_sequences\": false,\n            \"allow_multiple_items\": false,\n            \"allow_version_control\": false,\n            \"extensions\": [\n                \".ma\",\n                \".blend\",\n                \".hip\",\n                \".hda\"\n            ]\n        },\n        {\n            \"family\": \"audio\",\n            \"identifier\": \"\",\n            \"label\": \"Audio \",\n            \"icon\": \"fa5s.file-audio\",\n            \"default_variants\": [\n                \"Main\"\n            ],\n            \"description\": \"Audio product\",\n            \"detailed_description\": \"Audio files for review or final delivery\",\n            \"allow_sequences\": false,\n            \"allow_multiple_items\": false,\n            \"allow_version_control\": false,\n            \"extensions\": [\n                \".wav\"\n            ]\n        }\n    ],\n    \"editorial_creators\": {\n        \"editorial_simple\": {\n            \"default_variants\": [\n                \"Main\"\n            ],\n            \"clip_name_tokenizer\": {\n                \"_sequence_\": \"(sc\\\\d{3})\",\n                \"_shot_\": \"(sh\\\\d{3})\"\n            },\n            \"shot_rename\": {\n                \"enabled\": true,\n                \"shot_rename_template\": \"{project[code]}_{_sequence_}_{_shot_}\"\n            },\n            \"shot_hierarchy\": {\n                \"enabled\": true,\n                \"parents_path\": \"{project}/{folder}/{sequence}\",\n                \"parents\": [\n                    {\n                        \"type\": \"Project\",\n                        \"name\": \"project\",\n                        \"value\": \"{project[name]}\"\n                    },\n                    {\n                        \"type\": \"Folder\",\n                        \"name\": \"folder\",\n                        \"value\": \"shots\"\n                    },\n                    {\n                        \"type\": \"Sequence\",\n                        \"name\": \"sequence\",\n                        \"value\": \"{_sequence_}\"\n                    }\n                ]\n            },\n            \"shot_add_tasks\": {},\n            \"family_presets\": [\n                {\n                    \"family\": \"review\",\n                    \"variant\": \"Reference\",\n                    \"review\": true,\n                    \"output_file_type\": \".mp4\"\n                },\n                {\n                    \"family\": \"plate\",\n                    \"variant\": \"\",\n                    \"review\": false,\n                    \"output_file_type\": \".mov\"\n                },\n                {\n                    \"family\": \"audio\",\n                    \"variant\": \"\",\n                    \"review\": false,\n                    \"output_file_type\": \".wav\"\n                }\n            ]\n        }\n    },\n    \"create\": {\n        \"BatchMovieCreator\": {\n            \"default_variants\": [\n                \"Main\"\n            ],\n            \"default_tasks\": [\n                \"Compositing\"\n            ],\n            \"extensions\": [\n                \".mov\"\n            ]\n        }\n    },\n    \"publish\": {\n        \"CollectSequenceFrameData\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": false\n        },\n        \"ValidateFrameRange\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateExistingVersion\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/tvpaint.json",
    "content": "{\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"stop_timer_on_application_exit\": false,\n    \"create\": {\n        \"create_workfile\": {\n            \"enabled\": true,\n            \"default_variant\": \"Main\",\n            \"default_variants\": []\n        },\n        \"create_review\": {\n            \"enabled\": true,\n            \"active_on_create\": true,\n            \"default_variant\": \"Main\",\n            \"default_variants\": []\n        },\n        \"create_render_scene\": {\n            \"enabled\": true,\n            \"active_on_create\": false,\n            \"mark_for_review\": true,\n            \"default_pass_name\": \"beauty\",\n            \"default_variant\": \"Main\",\n            \"default_variants\": []\n        },\n        \"create_render_layer\": {\n            \"mark_for_review\": false,\n            \"default_pass_name\": \"beauty\",\n            \"default_variant\": \"Main\",\n            \"default_variants\": []\n        },\n        \"create_render_pass\": {\n            \"mark_for_review\": false,\n            \"default_variant\": \"Main\",\n            \"default_variants\": []\n        },\n        \"auto_detect_render\": {\n            \"enabled\": false,\n            \"allow_group_rename\": true,\n            \"group_name_template\": \"L{group_index}\",\n            \"group_idx_offset\": 10,\n            \"group_idx_padding\": 3\n        }\n    },\n    \"publish\": {\n        \"CollectRenderInstances\": {\n            \"ignore_render_pass_transparency\": false\n        },\n        \"ExtractSequence\": {\n            \"review_bg\": [\n                255,\n                255,\n                255,\n                255\n            ]\n        },\n        \"ValidateProjectSettings\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateMarks\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateStartFrame\": {\n            \"enabled\": false,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ValidateAssetName\": {\n            \"enabled\": true,\n            \"optional\": true,\n            \"active\": true\n        },\n        \"ExtractConvertToEXR\": {\n            \"enabled\": false,\n            \"replace_pngs\": true,\n            \"exr_compression\": \"ZIP\"\n        }\n    },\n    \"load\": {\n        \"LoadImage\": {\n            \"defaults\": {\n                \"stretch\": true,\n                \"timestretch\": true,\n                \"preload\": true\n            }\n        },\n        \"ImportImage\": {\n            \"defaults\": {\n                \"stretch\": true,\n                \"timestretch\": true,\n                \"preload\": true\n            }\n        }\n    },\n    \"workfile_builder\": {\n        \"create_first_version\": false,\n        \"custom_templates\": []\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/unreal.json",
    "content": "{\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"level_sequences_for_layouts\": false,\n    \"delete_unmatched_assets\": false,\n    \"render_config_path\": \"\",\n    \"preroll_frames\": 0,\n    \"render_format\": \"png\",\n    \"project_setup\": {\n        \"dev_mode\": false\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/project_settings/webpublisher.json",
    "content": "{\n    \"imageio\": {\n        \"activate_host_color_management\": true,\n        \"ocio_config\": {\n            \"override_global_config\": false,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"activate_host_rules\": false,\n            \"rules\": {}\n        }\n    },\n    \"timeout_profiles\": [\n        {\n            \"hosts\": [\n                \"photoshop\"\n            ],\n            \"task_types\": [],\n            \"timeout\": 600\n        }\n    ],\n    \"publish\": {\n        \"CollectPublishedFiles\": {\n            \"sync_next_version\": false,\n            \"task_type_to_family\": {\n                \"Animation\": [\n                    {\n                        \"is_sequence\": false,\n                        \"extensions\": [\n                            \"tvp\"\n                        ],\n                        \"families\": [],\n                        \"tags\": [],\n                        \"result_family\": \"workfile\"\n                    },\n                    {\n                        \"is_sequence\": true,\n                        \"extensions\": [\n                            \"png\",\n                            \"exr\",\n                            \"tiff\",\n                            \"tif\"\n                        ],\n                        \"families\": [\n                            \"review\"\n                        ],\n                        \"tags\": [\n                            \"review\"\n                        ],\n                        \"result_family\": \"render\"\n                    }\n                ],\n                \"Compositing\": [\n                    {\n                        \"is_sequence\": false,\n                        \"extensions\": [\n                            \"aep\"\n                        ],\n                        \"families\": [],\n                        \"tags\": [],\n                        \"result_family\": \"workfile\"\n                    },\n                    {\n                        \"is_sequence\": true,\n                        \"extensions\": [\n                            \"png\",\n                            \"exr\",\n                            \"tiff\",\n                            \"tif\"\n                        ],\n                        \"families\": [\n                            \"review\"\n                        ],\n                        \"tags\": [\n                            \"review\"\n                        ],\n                        \"result_family\": \"render\"\n                    }\n                ],\n                \"Layout\": [\n                    {\n                        \"is_sequence\": false,\n                        \"extensions\": [\n                            \"psd\"\n                        ],\n                        \"families\": [],\n                        \"tags\": [],\n                        \"result_family\": \"workfile\"\n                    },\n                    {\n                        \"is_sequence\": false,\n                        \"extensions\": [\n                            \"png\",\n                            \"jpg\",\n                            \"jpeg\",\n                            \"tiff\",\n                            \"tif\"\n                        ],\n                        \"families\": [\n                            \"review\"\n                        ],\n                        \"tags\": [\n                            \"review\"\n                        ],\n                        \"result_family\": \"image\"\n                    }\n                ],\n                \"default_task_type\": [\n                    {\n                        \"is_sequence\": false,\n                        \"extensions\": [\n                            \"tvp\",\n                            \"psd\"\n                        ],\n                        \"families\": [],\n                        \"tags\": [],\n                        \"result_family\": \"workfile\"\n                    },\n                    {\n                        \"is_sequence\": true,\n                        \"extensions\": [\n                            \"png\",\n                            \"exr\",\n                            \"tiff\",\n                            \"tif\"\n                        ],\n                        \"families\": [\n                            \"review\"\n                        ],\n                        \"tags\": [\n                            \"review\"\n                        ],\n                        \"result_family\": \"render\"\n                    }\n                ],\n                \"__dynamic_keys_labels__\": {\n                    \"default_task_type\": \"Default task type\"\n                }\n            }\n        },\n        \"CollectTVPaintInstances\": {\n            \"layer_name_regex\": \"(?P<layer>L[0-9]{3}_\\\\w+)_(?P<pass>.+)\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/system_settings/applications.json",
    "content": "{\n    \"maya\": {\n        \"enabled\": true,\n        \"label\": \"Maya\",\n        \"icon\": \"{}/app_icons/maya.png\",\n        \"host_name\": \"maya\",\n        \"environment\": {\n            \"MAYA_DISABLE_CLIC_IPM\": \"Yes\",\n            \"MAYA_DISABLE_CIP\": \"Yes\",\n            \"MAYA_DISABLE_CER\": \"Yes\",\n            \"PYMEL_SKIP_MEL_INIT\": \"Yes\",\n            \"LC_ALL\": \"C\"\n        },\n        \"variants\": {\n            \"2024\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Autodesk\\\\Maya2024\\\\bin\\\\maya.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/autodesk/maya2024/bin/maya\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {\n                    \"MAYA_VERSION\": \"2024\"\n                }\n            },\n            \"2023\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Autodesk\\\\Maya2023\\\\bin\\\\maya.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/autodesk/maya2023/bin/maya\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {\n                    \"MAYA_VERSION\": \"2023\"\n                }\n            },\n            \"2022\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Autodesk\\\\Maya2022\\\\bin\\\\maya.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/autodesk/maya2022/bin/maya\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {\n                    \"MAYA_VERSION\": \"2022\"\n                }\n            }\n        }\n    },\n    \"mayapy\": {\n        \"enabled\": true,\n        \"label\": \"MayaPy\",\n        \"icon\": \"{}/app_icons/maya.png\",\n        \"host_name\": \"maya\",\n        \"environment\": {\n            \"MAYA_DISABLE_CLIC_IPM\": \"Yes\",\n            \"MAYA_DISABLE_CIP\": \"Yes\",\n            \"MAYA_DISABLE_CER\": \"Yes\",\n            \"PYMEL_SKIP_MEL_INIT\": \"Yes\",\n            \"LC_ALL\": \"C\"\n        },\n        \"variants\": {\n            \"2024\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Autodesk\\\\Maya2024\\\\bin\\\\mayapy.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/autodesk/maya2024/bin/mayapy\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"-I\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"-I\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"2023\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Autodesk\\\\Maya2023\\\\bin\\\\mayapy.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/autodesk/maya2023/bin/mayapy\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"-I\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"-I\"\n                    ]\n                },\n                \"environment\": {}\n            }\n        }\n    },\n    \"3dsmax\": {\n        \"enabled\": true,\n        \"label\": \"3ds max\",\n        \"icon\": \"{}/app_icons/3dsmax.png\",\n        \"host_name\": \"max\",\n        \"environment\": {},\n        \"variants\": {\n            \"2023\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Autodesk\\\\3ds Max 2023\\\\3dsmax.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {\n                    \"3DSMAX_VERSION\": \"2023\"\n                }\n            }\n        }\n    },\n    \"flame\": {\n        \"enabled\": true,\n        \"label\": \"Flame\",\n        \"icon\": \"{}/app_icons/flame.png\",\n        \"host_name\": \"flame\",\n        \"environment\": {\n            \"FLAME_SCRIPT_DIRS\": {\n                \"windows\": \"\",\n                \"darwin\": \"\",\n                \"linux\": \"\"\n            },\n            \"FLAME_WIRETAP_HOSTNAME\": \"\",\n            \"FLAME_WIRETAP_VOLUME\": \"stonefs\",\n            \"FLAME_WIRETAP_GROUP\": \"staff\"\n        },\n        \"variants\": {\n            \"2021\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [],\n                    \"darwin\": [\n                        \"/opt/Autodesk/flame_2021/bin/flame.app/Contents/MacOS/startApp\"\n                    ],\n                    \"linux\": [\n                        \"/opt/Autodesk/flame_2021/bin/startApplication\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {\n                    \"OPENPYPE_FLAME_PYTHON_EXEC\": \"/opt/Autodesk/python/2021/bin/python2.7\",\n                    \"OPENPYPE_FLAME_PYTHONPATH\": \"/opt/Autodesk/flame_2021/python\",\n                    \"OPENPYPE_WIRETAP_TOOLS\": \"/opt/Autodesk/wiretap/tools/2021\"\n                }\n            },\n            \"2021_1\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [],\n                    \"darwin\": [\n                        \"/opt/Autodesk/flame_2021.1/bin/flame.app/Contents/MacOS/startApp\"\n                    ],\n                    \"linux\": [\n                        \"/opt/Autodesk/flame_2021.1/bin/startApplication\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {\n                    \"OPENPYPE_FLAME_PYTHON_EXEC\": \"/opt/Autodesk/python/2021.1/bin/python2.7\",\n                    \"OPENPYPE_FLAME_PYTHONPATH\": \"/opt/Autodesk/flame_2021.1/python\",\n                    \"OPENPYPE_WIRETAP_TOOLS\": \"/opt/Autodesk/wiretap/tools/2021.1\"\n                }\n            },\n            \"__dynamic_keys_labels__\": {\n                \"2021\": \"2021\",\n                \"2021_1\": \"2021.1\"\n            }\n        }\n    },\n    \"nuke\": {\n        \"enabled\": true,\n        \"label\": \"Nuke\",\n        \"icon\": \"{}/app_icons/nuke.png\",\n        \"host_name\": \"nuke\",\n        \"environment\": {\n            \"NUKE_PATH\": [\n                \"{NUKE_PATH}\",\n                \"{OPENPYPE_STUDIO_PLUGINS}/nuke\"\n            ]\n        },\n        \"variants\": {\n            \"13-2\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke13.2v1\\\\Nuke13.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke13.2v1/Nuke13.2\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"13-0\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke13.0v1\\\\Nuke13.0.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke13.0v1/Nuke13.0\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"12-2\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke12.2v3\\\\Nuke12.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke12.2v3Nuke12.2\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"12-0\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke12.0v1\\\\Nuke12.0.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke12.0v1/Nuke12.0\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"11-3\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke11.3v1\\\\Nuke11.3.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke11.3v5/Nuke11.3\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"11-2\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke11.2v2\\\\Nuke11.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"11-0\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke11.0v4\\\\Nuke11.0.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"__dynamic_keys_labels__\": {\n                \"13-2\": \"13.2\",\n                \"13-0\": \"13.0\",\n                \"12-2\": \"12.2\",\n                \"12-0\": \"12.0\",\n                \"11-3\": \"11.3\",\n                \"11-2\": \"11.2\",\n                \"11-0\": \"11.0\"\n            }\n        }\n    },\n    \"nukeassist\": {\n        \"enabled\": true,\n        \"label\": \"Nuke Assist\",\n        \"icon\": \"{}/app_icons/nuke.png\",\n        \"host_name\": \"nuke\",\n        \"environment\": {\n            \"NUKE_PATH\": [\n                \"{NUKE_PATH}\",\n                \"{OPENPYPE_STUDIO_PLUGINS}/nuke\"\n            ]\n        },\n        \"variants\": {\n            \"13-2\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke13.2v1\\\\Nuke13.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke13.2v1/Nuke13.2\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--nukeassist\"\n                    ],\n                    \"darwin\": [\n                        \"--nukeassist\"\n                    ],\n                    \"linux\": [\n                        \"--nukeassist\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"13-0\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke13.0v1\\\\Nuke13.0.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke13.0v1/Nuke13.0\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--nukeassist\"\n                    ],\n                    \"darwin\": [\n                        \"--nukeassist\"\n                    ],\n                    \"linux\": [\n                        \"--nukeassist\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"12-2\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke12.2v3\\\\Nuke12.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke12.2v3Nuke12.2\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--nukeassist\"\n                    ],\n                    \"darwin\": [\n                        \"--nukeassist\"\n                    ],\n                    \"linux\": [\n                        \"--nukeassist\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"12-0\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke12.0v1\\\\Nuke12.0.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke12.0v1/Nuke12.0\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--nukeassist\"\n                    ],\n                    \"darwin\": [\n                        \"--nukeassist\"\n                    ],\n                    \"linux\": [\n                        \"--nukeassist\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"11-3\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke11.3v1\\\\Nuke11.3.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke11.3v5/Nuke11.3\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--nukeassist\"\n                    ],\n                    \"darwin\": [\n                        \"--nukeassist\"\n                    ],\n                    \"linux\": [\n                        \"--nukeassist\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"11-2\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke11.2v2\\\\Nuke11.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--nukeassist\"\n                    ],\n                    \"darwin\": [\n                        \"--nukeassist\"\n                    ],\n                    \"linux\": [\n                        \"--nukeassist\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"__dynamic_keys_labels__\": {\n                \"13-2\": \"13.2\",\n                \"13-0\": \"13.0\",\n                \"12-2\": \"12.2\",\n                \"12-0\": \"12.0\",\n                \"11-3\": \"11.3\",\n                \"11-2\": \"11.2\"\n            }\n        }\n    },\n    \"nukex\": {\n        \"enabled\": true,\n        \"label\": \"Nuke X\",\n        \"icon\": \"{}/app_icons/nukex.png\",\n        \"host_name\": \"nuke\",\n        \"environment\": {\n            \"NUKE_PATH\": [\n                \"{NUKE_PATH}\",\n                \"{OPENPYPE_STUDIO_PLUGINS}/nuke\"\n            ]\n        },\n        \"variants\": {\n            \"13-2\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke13.2v1\\\\Nuke13.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke13.2v1/Nuke13.2\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--nukex\"\n                    ],\n                    \"darwin\": [\n                        \"--nukex\"\n                    ],\n                    \"linux\": [\n                        \"--nukex\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"13-0\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke13.0v1\\\\Nuke13.0.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke13.0v1/Nuke13.0\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--nukex\"\n                    ],\n                    \"darwin\": [\n                        \"--nukex\"\n                    ],\n                    \"linux\": [\n                        \"--nukex\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"12-2\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke12.2v3\\\\Nuke12.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke12.2v3Nuke12.2\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--nukex\"\n                    ],\n                    \"darwin\": [\n                        \"--nukex\"\n                    ],\n                    \"linux\": [\n                        \"--nukex\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"12-0\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke12.0v1\\\\Nuke12.0.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke12.0v1/Nuke12.0\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--nukex\"\n                    ],\n                    \"darwin\": [\n                        \"--nukex\"\n                    ],\n                    \"linux\": [\n                        \"--nukex\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"11-3\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke11.3v1\\\\Nuke11.3.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke11.3v5/Nuke11.3\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--nukex\"\n                    ],\n                    \"darwin\": [\n                        \"--nukex\"\n                    ],\n                    \"linux\": [\n                        \"--nukex\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"11-2\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke11.2v2\\\\Nuke11.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--nukex\"\n                    ],\n                    \"darwin\": [\n                        \"--nukex\"\n                    ],\n                    \"linux\": [\n                        \"--nukex\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"__dynamic_keys_labels__\": {\n                \"13-2\": \"13.2\",\n                \"13-0\": \"13.0\",\n                \"12-2\": \"12.2\",\n                \"12-0\": \"12.0\",\n                \"11-3\": \"11.3\",\n                \"11-2\": \"11.2\"\n            }\n        }\n    },\n    \"nukestudio\": {\n        \"enabled\": true,\n        \"label\": \"Nuke Studio\",\n        \"icon\": \"{}/app_icons/nukestudio.png\",\n        \"host_name\": \"hiero\",\n        \"environment\": {\n            \"WORKFILES_STARTUP\": \"0\",\n            \"TAG_ASSETBUILD_STARTUP\": \"0\"\n        },\n        \"variants\": {\n            \"13-2\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke13.2v1\\\\Nuke13.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke13.2v1/Nuke13.2\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--studio\"\n                    ],\n                    \"darwin\": [\n                        \"--studio\"\n                    ],\n                    \"linux\": [\n                        \"--studio\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"13-0\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke13.0v1\\\\Nuke13.0.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke13.0v1/Nuke13.0\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--studio\"\n                    ],\n                    \"darwin\": [\n                        \"--studio\"\n                    ],\n                    \"linux\": [\n                        \"--studio\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"12-2\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke12.2v3\\\\Nuke12.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke12.2v3Nuke12.2\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--studio\"\n                    ],\n                    \"darwin\": [\n                        \"--studio\"\n                    ],\n                    \"linux\": [\n                        \"--studio\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"12-0\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke12.0v1\\\\Nuke12.0.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke12.0v1/Nuke12.0\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--studio\"\n                    ],\n                    \"darwin\": [\n                        \"--studio\"\n                    ],\n                    \"linux\": [\n                        \"--studio\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"11-3\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke11.3v1\\\\Nuke11.3.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke11.3v5/Nuke11.3\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--studio\"\n                    ],\n                    \"darwin\": [\n                        \"--studio\"\n                    ],\n                    \"linux\": [\n                        \"--studio\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"11-2\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--studio\"\n                    ],\n                    \"darwin\": [\n                        \"--studio\"\n                    ],\n                    \"linux\": [\n                        \"--studio\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"__dynamic_keys_labels__\": {\n                \"13-2\": \"13.2\",\n                \"13-0\": \"13.0\",\n                \"12-2\": \"12.2\",\n                \"12-0\": \"12.0\",\n                \"11-3\": \"11.3\",\n                \"11-2\": \"11.2\"\n            }\n        }\n    },\n    \"hiero\": {\n        \"enabled\": true,\n        \"label\": \"Hiero\",\n        \"icon\": \"{}/app_icons/hiero.png\",\n        \"host_name\": \"hiero\",\n        \"environment\": {\n            \"WORKFILES_STARTUP\": \"0\",\n            \"TAG_ASSETBUILD_STARTUP\": \"0\"\n        },\n        \"variants\": {\n            \"13-2\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke13.2v1\\\\Nuke13.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke13.2v1/Nuke13.2\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--hiero\"\n                    ],\n                    \"darwin\": [\n                        \"--hiero\"\n                    ],\n                    \"linux\": [\n                        \"--hiero\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"13-0\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke13.0v1\\\\Nuke13.0.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke13.0v1/Nuke13.0\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--hiero\"\n                    ],\n                    \"darwin\": [\n                        \"--hiero\"\n                    ],\n                    \"linux\": [\n                        \"--hiero\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"12-2\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke12.2v3\\\\Nuke12.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke12.2v3Nuke12.2\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--hiero\"\n                    ],\n                    \"darwin\": [\n                        \"--hiero\"\n                    ],\n                    \"linux\": [\n                        \"--hiero\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"12-0\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke12.0v1\\\\Nuke12.0.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke12.0v1/Nuke12.0\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--hiero\"\n                    ],\n                    \"darwin\": [\n                        \"--hiero\"\n                    ],\n                    \"linux\": [\n                        \"--hiero\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"11-3\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke11.3v1\\\\Nuke11.3.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": [\n                        \"/usr/local/Nuke11.3v5/Nuke11.3\"\n                    ]\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--hiero\"\n                    ],\n                    \"darwin\": [\n                        \"--hiero\"\n                    ],\n                    \"linux\": [\n                        \"--hiero\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"11-2\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Nuke11.2v2\\\\Nuke11.2.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--hiero\"\n                    ],\n                    \"darwin\": [\n                        \"--hiero\"\n                    ],\n                    \"linux\": [\n                        \"--hiero\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"__dynamic_keys_labels__\": {\n                \"13-2\": \"13.2\",\n                \"13-0\": \"13.0\",\n                \"12-2\": \"12.2\",\n                \"12-0\": \"12.0\",\n                \"11-3\": \"11.3\",\n                \"11-2\": \"11.2\"\n            }\n        }\n    },\n    \"fusion\": {\n        \"enabled\": true,\n        \"label\": \"Fusion\",\n        \"icon\": \"{}/app_icons/fusion.png\",\n        \"host_name\": \"fusion\",\n        \"environment\": {\n            \"FUSION_PYTHON3_HOME\": {\n                \"windows\": \"{LOCALAPPDATA}/Programs/Python/Python36\",\n                \"darwin\": \"~/Library/Python/3.6/bin\",\n                \"linux\": \"/opt/Python/3.6/bin\"\n            }\n        },\n        \"variants\": {\n            \"18\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Blackmagic Design\\\\Fusion 18\\\\Fusion.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"17\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Blackmagic Design\\\\Fusion 17\\\\Fusion.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"16\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Blackmagic Design\\\\Fusion 16\\\\Fusion.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"9\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Blackmagic Design\\\\Fusion 9\\\\Fusion.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            }\n        }\n    },\n    \"resolve\": {\n        \"enabled\": true,\n        \"label\": \"Resolve\",\n        \"icon\": \"{}/app_icons/resolve.png\",\n        \"host_name\": \"resolve\",\n        \"environment\": {\n            \"RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR\": [],\n            \"RESOLVE_PYTHON3_HOME\": {\n                \"windows\": \"{LOCALAPPDATA}/Programs/Python/Python36\",\n                \"darwin\": \"/Library/Frameworks/Python.framework/Versions/3.6\",\n                \"linux\": \"/opt/Python/3.6\"\n            }\n        },\n        \"variants\": {\n            \"stable\": {\n                \"enabled\": true,\n                \"variant_label\": \"stable\",\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:/Program Files/Blackmagic Design/DaVinci Resolve/Resolve.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            }\n        }\n    },\n    \"houdini\": {\n        \"enabled\": true,\n        \"label\": \"Houdini\",\n        \"icon\": \"{}/app_icons/houdini.png\",\n        \"host_name\": \"houdini\",\n        \"environment\": {},\n        \"variants\": {\n            \"18-5\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Side Effects Software\\\\Houdini 18.5.499\\\\bin\\\\houdini.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"18\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"17\": {\n                \"use_python_2\": true,\n                \"executables\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"__dynamic_keys_labels__\": {\n                \"18-5\": \"18.5\",\n                \"18\": \"18\",\n                \"17\": \"17\"\n            }\n        }\n    },\n    \"blender\": {\n        \"enabled\": true,\n        \"label\": \"Blender\",\n        \"icon\": \"{}/app_icons/blender.png\",\n        \"host_name\": \"blender\",\n        \"environment\": {},\n        \"variants\": {\n            \"2-83\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Blender Foundation\\\\Blender 2.83\\\\blender.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--python-use-system-env\"\n                    ],\n                    \"darwin\": [\n                        \"--python-use-system-env\"\n                    ],\n                    \"linux\": [\n                        \"--python-use-system-env\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"2-90\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Blender Foundation\\\\Blender 2.90\\\\blender.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--python-use-system-env\"\n                    ],\n                    \"darwin\": [\n                        \"--python-use-system-env\"\n                    ],\n                    \"linux\": [\n                        \"--python-use-system-env\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"2-91\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Blender Foundation\\\\Blender 2.91\\\\blender.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [\n                        \"--python-use-system-env\"\n                    ],\n                    \"darwin\": [\n                        \"--python-use-system-env\"\n                    ],\n                    \"linux\": [\n                        \"--python-use-system-env\"\n                    ]\n                },\n                \"environment\": {}\n            },\n            \"__dynamic_keys_labels__\": {\n                \"2-83\": \"2.83\",\n                \"2-90\": \"2.90\",\n                \"2-91\": \"2.91\"\n            }\n        }\n    },\n    \"harmony\": {\n        \"enabled\": true,\n        \"label\": \"Harmony\",\n        \"icon\": \"{}/app_icons/harmony.png\",\n        \"host_name\": \"harmony\",\n        \"environment\": {\n            \"AVALON_HARMONY_WORKFILES_ON_LAUNCH\": \"1\"\n        },\n        \"variants\": {\n            \"21\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"c:\\\\Program Files (x86)\\\\Toon Boom Animation\\\\Toon Boom Harmony 21 Premium\\\\win64\\\\bin\\\\HarmonyPremium.exe\"\n                    ],\n                    \"darwin\": [\n                        \"/Applications/Toon Boom Harmony 21 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium\"\n                    ],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"20\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"c:\\\\Program Files (x86)\\\\Toon Boom Animation\\\\Toon Boom Harmony 20 Premium\\\\win64\\\\bin\\\\HarmonyPremium.exe\"\n                    ],\n                    \"darwin\": [\n                        \"/Applications/Toon Boom Harmony 20 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium\"\n                    ],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"17\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"c:\\\\Program Files (x86)\\\\Toon Boom Animation\\\\Toon Boom Harmony 17 Premium\\\\win64\\\\bin\\\\HarmonyPremium.exe\"\n                    ],\n                    \"darwin\": [\n                        \"/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium\"\n                    ],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            }\n        }\n    },\n    \"tvpaint\": {\n        \"enabled\": true,\n        \"label\": \"TVPaint\",\n        \"icon\": \"{}/app_icons/tvpaint.png\",\n        \"host_name\": \"tvpaint\",\n        \"environment\": {},\n        \"variants\": {\n            \"animation_11-64bits\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\TVPaint Developpement\\\\TVPaint Animation 11 (64bits)\\\\TVPaint Animation 11 (64bits).exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"animation_11-32bits\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files (x86)\\\\TVPaint Developpement\\\\TVPaint Animation 11 (32bits)\\\\TVPaint Animation 11 (32bits).exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"__dynamic_keys_labels__\": {\n                \"animation_11-64bits\": \"11 (64bits)\",\n                \"animation_11-32bits\": \"11 (32bits)\"\n            }\n        }\n    },\n    \"photoshop\": {\n        \"enabled\": true,\n        \"label\": \"Photoshop\",\n        \"icon\": \"{}/app_icons/photoshop.png\",\n        \"host_name\": \"photoshop\",\n        \"environment\": {\n            \"AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH\": \"1\",\n            \"WORKFILES_SAVE_AS\": \"Yes\"\n        },\n        \"variants\": {\n            \"2020\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Adobe\\\\Adobe Photoshop 2020\\\\Photoshop.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"2021\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Adobe\\\\Adobe Photoshop 2021\\\\Photoshop.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"2022\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Adobe\\\\Adobe Photoshop 2022\\\\Photoshop.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            }\n        }\n    },\n    \"aftereffects\": {\n        \"enabled\": true,\n        \"label\": \"AfterEffects\",\n        \"icon\": \"{}/app_icons/aftereffects.png\",\n        \"host_name\": \"aftereffects\",\n        \"environment\": {\n            \"AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH\": \"1\",\n            \"WORKFILES_SAVE_AS\": \"Yes\"\n        },\n        \"variants\": {\n            \"2020\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Adobe\\\\Adobe After Effects 2020\\\\Support Files\\\\AfterFX.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"2021\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Adobe\\\\Adobe After Effects 2021\\\\Support Files\\\\AfterFX.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"2022\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Adobe\\\\Adobe After Effects 2022\\\\Support Files\\\\AfterFX.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {\n                    \"MULTIPROCESS\": \"No\"\n                }\n            }\n        }\n    },\n    \"celaction\": {\n        \"enabled\": true,\n        \"label\": \"CelAction 2D\",\n        \"icon\": \"app_icons/celaction.png\",\n        \"host_name\": \"celaction\",\n        \"environment\": {\n            \"CELACTION_TEMPLATE\": \"{OPENPYPE_REPOS_ROOT}/openpype/hosts/celaction/celaction_template_scene.scn\"\n        },\n        \"variants\": {\n            \"current\": {\n                \"enabled\": true,\n                \"variant_label\": \"Current\",\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:/Program Files/CelAction/CelAction2D Studio/CelAction2D.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            }\n        }\n    },\n    \"substancepainter\": {\n        \"enabled\": true,\n        \"label\": \"Substance Painter\",\n        \"icon\": \"app_icons/substancepainter.png\",\n        \"host_name\": \"substancepainter\",\n        \"environment\": {},\n        \"variants\": {\n            \"8-2-0\": {\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Adobe\\\\Adobe Substance 3D Painter\\\\Adobe Substance 3D Painter.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"__dynamic_keys_labels__\": {\n                \"8-2-0\": \"8.2.0\"\n            }\n        }\n    },\n    \"unreal\": {\n        \"enabled\": true,\n        \"label\": \"Unreal Editor\",\n        \"icon\": \"{}/app_icons/ue4.png\",\n        \"host_name\": \"unreal\",\n        \"environment\": {\n            \"UE_PYTHONPATH\": \"{PYTHONPATH}\"\n        },\n        \"variants\": {\n            \"5-0\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Epic Games\\\\UE_5.0\\\\Engine\\\\Binaries\\\\Win64\\\\UnrealEditor.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"5-1\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\Epic Games\\\\UE_5.1\\\\Engine\\\\Binaries\\\\Win64\\\\UnrealEditor.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"__dynamic_keys_labels__\": {\n                \"5-0\": \"Unreal 5.0\",\n                \"5-1\": \"Unreal 5.1\"\n            }\n        }\n    },\n    \"djvview\": {\n        \"enabled\": true,\n        \"label\": \"DJV View\",\n        \"icon\": \"{}/app_icons/djvView.png\",\n        \"host_name\": \"\",\n        \"environment\": {},\n        \"variants\": {\n            \"1-1\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"__dynamic_keys_labels__\": {\n                \"1-1\": \"1.1\"\n            }\n        }\n    },\n    \"3dequalizer\": {\n        \"enabled\": true,\n        \"label\": \"3DEqualizer\",\n        \"icon\": \"{}/app_icons/3de4.png\",\n        \"host_name\": \"equalizer\",\n        \"heartbeat_interval\": 500,\n        \"environment\": {},\n        \"variants\": {\n            \"7-1v2\": {\n                \"use_python_2\": false,\n                \"executables\": {\n                    \"windows\": [\n                        \"C:\\\\Program Files\\\\3DE4_win64_r7.1v2\\\\bin\\\\3DE4.exe\"\n                    ],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"arguments\": {\n                    \"windows\": [],\n                    \"darwin\": [],\n                    \"linux\": []\n                },\n                \"environment\": {}\n            },\n            \"__dynamic_keys_labels__\": {\n                \"7-1v2\": \"7.1v2\"\n            }\n        }\n    },\n    \"additional_apps\": {}\n}\n"
  },
  {
    "path": "openpype/settings/defaults/system_settings/general.json",
    "content": "{\n    \"studio_name\": \"Studio name\",\n    \"studio_code\": \"stu\",\n    \"admin_password\": \"\",\n    \"environment\": {},\n    \"log_to_server\": true,\n    \"disk_mapping\": {\n        \"windows\": [],\n        \"linux\": [],\n        \"darwin\": []\n    },\n    \"local_env_white_list\": [],\n    \"openpype_path\": {\n        \"windows\": [],\n        \"darwin\": [],\n        \"linux\": []\n    },\n    \"local_openpype_path\": {\n        \"windows\": \"\",\n        \"darwin\": \"\",\n        \"linux\": \"\"\n    },\n    \"production_version\": \"\",\n    \"staging_version\": \"\",\n    \"version_check_interval\": 5\n}\n"
  },
  {
    "path": "openpype/settings/defaults/system_settings/modules.json",
    "content": "{\n    \"addon_paths\": {\n        \"windows\": [],\n        \"darwin\": [],\n        \"linux\": []\n    },\n    \"avalon\": {\n        \"AVALON_TIMEOUT\": 1000,\n        \"AVALON_THUMBNAIL_ROOT\": {\n            \"windows\": \"\",\n            \"darwin\": \"\",\n            \"linux\": \"\"\n        }\n    },\n    \"ftrack\": {\n        \"enabled\": false,\n        \"ftrack_server\": \"\",\n        \"ftrack_actions_path\": {\n            \"windows\": [],\n            \"darwin\": [],\n            \"linux\": []\n        },\n        \"ftrack_events_path\": {\n            \"windows\": [],\n            \"darwin\": [],\n            \"linux\": []\n        },\n        \"intent\": {\n            \"allow_empty_intent\": true,\n            \"empty_intent_label\": \"\",\n            \"items\": {\n                \"wip\": \"WIP\",\n                \"final\": \"Final\",\n                \"test\": \"Test\"\n            },\n            \"default\": \"\"\n        },\n        \"custom_attributes\": {\n            \"show\": {\n                \"avalon_auto_sync\": {\n                    \"write_security_roles\": [\n                        \"API\",\n                        \"Administrator\"\n                    ],\n                    \"read_security_roles\": [\n                        \"API\",\n                        \"Administrator\"\n                    ]\n                },\n                \"library_project\": {\n                    \"write_security_roles\": [\n                        \"API\",\n                        \"Administrator\"\n                    ],\n                    \"read_security_roles\": [\n                        \"API\",\n                        \"Administrator\"\n                    ]\n                },\n                \"applications\": {\n                    \"write_security_roles\": [\n                        \"API\",\n                        \"Administrator\"\n                    ],\n                    \"read_security_roles\": [\n                        \"API\",\n                        \"Administrator\"\n                    ]\n                }\n            },\n            \"is_hierarchical\": {\n                \"tools_env\": {\n                    \"write_security_roles\": [\n                        \"API\",\n                        \"Administrator\"\n                    ],\n                    \"read_security_roles\": [\n                        \"API\",\n                        \"Administrator\"\n                    ]\n                },\n                \"avalon_mongo_id\": {\n                    \"write_security_roles\": [\n                        \"API\",\n                        \"Administrator\"\n                    ],\n                    \"read_security_roles\": [\n                        \"API\",\n                        \"Administrator\"\n                    ]\n                },\n                \"fps\": {\n                    \"write_security_roles\": [],\n                    \"read_security_roles\": []\n                },\n                \"frameStart\": {\n                    \"write_security_roles\": [],\n                    \"read_security_roles\": []\n                },\n                \"frameEnd\": {\n                    \"write_security_roles\": [],\n                    \"read_security_roles\": []\n                },\n                \"clipIn\": {\n                    \"write_security_roles\": [],\n                    \"read_security_roles\": []\n                },\n                \"clipOut\": {\n                    \"write_security_roles\": [],\n                    \"read_security_roles\": []\n                },\n                \"handleStart\": {\n                    \"write_security_roles\": [],\n                    \"read_security_roles\": []\n                },\n                \"handleEnd\": {\n                    \"write_security_roles\": [],\n                    \"read_security_roles\": []\n                },\n                \"resolutionWidth\": {\n                    \"write_security_roles\": [],\n                    \"read_security_roles\": []\n                },\n                \"resolutionHeight\": {\n                    \"write_security_roles\": [],\n                    \"read_security_roles\": []\n                },\n                \"pixelAspect\": {\n                    \"write_security_roles\": [],\n                    \"read_security_roles\": []\n                }\n            }\n        }\n    },\n    \"kitsu\": {\n        \"enabled\": false,\n        \"server\": \"\"\n    },\n    \"shotgrid\": {\n        \"enabled\": false,\n        \"leecher_manager_url\": \"http://127.0.0.1:3000\",\n        \"leecher_backend_url\": \"http://127.0.0.1:8090\",\n        \"filter_projects_by_login\": true,\n        \"shotgrid_settings\": {}\n    },\n    \"timers_manager\": {\n        \"enabled\": true,\n        \"auto_stop\": true,\n        \"full_time\": 15.0,\n        \"message_time\": 0.5,\n        \"disregard_publishing\": false\n    },\n    \"clockify\": {\n        \"enabled\": false,\n        \"workspace_name\": \"\"\n    },\n    \"sync_server\": {\n        \"enabled\": false,\n        \"sites\": {}\n    },\n    \"deadline\": {\n        \"enabled\": true,\n        \"deadline_urls\": {\n            \"default\": \"http://127.0.0.1:8082\"\n        }\n    },\n    \"royalrender\": {\n        \"enabled\": false,\n        \"rr_paths\": {\n            \"default\": {\n                \"windows\": \"C:\\\\RR8\",\n                \"darwin\": \"/Volumes/share/RR8\",\n                \"linux\": \"/mnt/studio/RR8\"\n            }\n        }\n    },\n    \"log_viewer\": {\n        \"enabled\": true\n    },\n    \"standalonepublish_tool\": {\n        \"enabled\": false\n    },\n    \"project_manager\": {\n        \"enabled\": true\n    },\n    \"slack\": {\n        \"enabled\": false\n    },\n    \"job_queue\": {\n        \"server_url\": \"\",\n        \"jobs_root\": {\n            \"windows\": \"\",\n            \"darwin\": \"\",\n            \"linux\": \"\"\n        }\n    },\n    \"asset_reporter\": {\n        \"enabled\": false\n    }\n}\n"
  },
  {
    "path": "openpype/settings/defaults/system_settings/tools.json",
    "content": "{\n    \"tool_groups\": {\n        \"mtoa\": {\n            \"environment\": {\n                \"MTOA\": \"{STUDIO_SOFTWARE}/arnold/mtoa_{MAYA_VERSION}_{MTOA_VERSION}\",\n                \"MAYA_RENDER_DESC_PATH\": \"{MTOA}\",\n                \"MAYA_MODULE_PATH\": \"{MTOA}\",\n                \"ARNOLD_PLUGIN_PATH\": \"{MTOA}/shaders\",\n                \"MTOA_EXTENSIONS_PATH\": {\n                    \"darwin\": \"{MTOA}/extensions\",\n                    \"linux\": \"{MTOA}/extensions\",\n                    \"windows\": \"{MTOA}/extensions\"\n                },\n                \"MTOA_EXTENSIONS\": {\n                    \"darwin\": \"{MTOA}/extensions\",\n                    \"linux\": \"{MTOA}/extensions\",\n                    \"windows\": \"{MTOA}/extensions\"\n                },\n                \"DYLD_LIBRARY_PATH\": {\n                    \"darwin\": \"{MTOA}/bin\"\n                },\n                \"PATH\": {\n                    \"windows\": \"{PATH};{MTOA}/bin\"\n                }\n            },\n            \"variants\": {\n                \"3-2\": {\n                    \"host_names\": [],\n                    \"app_variants\": [],\n                    \"environment\": {\n                        \"MTOA_VERSION\": \"3.2\"\n                    }\n                },\n                \"3-1\": {\n                    \"host_names\": [],\n                    \"app_variants\": [],\n                    \"environment\": {\n                        \"MTOA_VERSION\": \"3.1\"\n                    }\n                },\n                \"__dynamic_keys_labels__\": {\n                    \"3-2\": \"3.2\",\n                    \"3-1\": \"3.1\"\n                }\n            }\n        },\n        \"vray\": {\n            \"environment\": {},\n            \"variants\": {}\n        },\n        \"yeti\": {\n            \"environment\": {},\n            \"variants\": {}\n        },\n        \"renderman\": {\n            \"environment\": {},\n            \"variants\": {\n                \"24-3-maya\": {\n                    \"host_names\": [\n                        \"maya\"\n                    ],\n                    \"app_variants\": [\n                        \"maya/2022\"\n                    ],\n                    \"environment\": {\n                        \"RFMTREE\": {\n                            \"windows\": \"C:\\\\Program Files\\\\Pixar\\\\RenderManForMaya-24.3\",\n                            \"darwin\": \"/Applications/Pixar/RenderManForMaya-24.3\",\n                            \"linux\": \"/opt/pixar/RenderManForMaya-24.3\"\n                        },\n                        \"RMANTREE\": {\n                            \"windows\": \"C:\\\\Program Files\\\\Pixar\\\\RenderManProServer-24.3\",\n                            \"darwin\": \"/Applications/Pixar/RenderManProServer-24.3\",\n                            \"linux\": \"/opt/pixar/RenderManProServer-24.3\"\n                        }\n                    }\n                },\n                \"__dynamic_keys_labels__\": {\n                    \"24-3-maya\": \"24.3 RFM\"\n                }\n            }\n        },\n        \"__dynamic_keys_labels__\": {\n            \"mtoa\": \"Autodesk Arnold\",\n            \"vray\": \"Chaos Group Vray\",\n            \"yeti\": \"Peregrine Labs Yeti\",\n            \"renderman\": \"Pixar Renderman\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/settings/entities/__init__.py",
    "content": "\"\"\"OpenPype Settings\n\nSettings define how openpype and it's modules behave. They became main\ncomponent of dynamism.\n\nOpenPype settings (ATM) have 3 layers:\n1.) Defaults - defined in code\n2.) Studio overrides - values that are applied on default that may modify only\n    some values or None, result can be called \"studio settings\"\n3.) Project overrides - values that are applied on studio settings, may modify\n    some values or None and may modify values that are not modified in studio\n    overrides\n\nTo be able do these overrides it is required to store metadata defying which\ndata are applied and how. Because of that it is not possible to modify\noverrides manually and expect it would work right.\n\nStructure of settings is defined with schemas. Schemas have defined structure\nand possible types with possible attributes (Schemas and their description\ncan be found in \"./schemas/README.md\").\n\nTo modify settings it's recommended to use UI settings tool which can easily\nvisuallise how values are applied.\n\nWith help of setting entities it is possible to modify settings from code.\n\nOpenPype has (ATM) 2 types of settings:\n1.) System settings - global system settings, don't have project overrides\n2.) Project settings - project specific settings\n\nStartpoint is root entity that cares about access to other setting entities\nin their scope. To be able work with entities it is required to understand\nsetting schemas and their structure. It is possible to work with dictionary\nand list entities as with standard python objects.\n\n```python\n# Create an object of system settings.\nsystem_settings = SystemSettings()\n\n# How to get value of entity\nprint(system_settings[\"general\"][\"studio_name\"].value)\n\n>>> TestStudio Name\n\n# How to set value\n# Variant 1\nsystem_settings[\"general\"][\"studio_name\"] = \"StudioidutS\"\n# Variant 2\nsystem_settings[\"general\"][\"studio_name\"].set(\"StudioidutS\")\n\nprint(system_settings[\"general\"][\"studio_name\"].value)\n>>> StudioidutS\n```\n\"\"\"\n\nfrom .exceptions import (\n    SchemaError,\n    DefaultsNotDefined,\n    StudioDefaultsNotDefined,\n    BaseInvalidValue,\n    InvalidValueType,\n    InvalidKeySymbols,\n    SchemaMissingFileInfo,\n    SchemeGroupHierarchyBug,\n    SchemaDuplicatedKeys,\n    SchemaDuplicatedEnvGroupKeys,\n    SchemaTemplateMissingKeys\n)\nfrom .lib import (\n    NOT_SET,\n    OverrideState\n)\nfrom .base_entity import (\n    BaseEntity,\n    GUIEntity,\n    BaseItemEntity,\n    ItemEntity\n)\n\nfrom .root_entities import (\n    SystemSettings,\n    ProjectSettings\n)\n\nfrom .item_entities import (\n    PathEntity,\n    ListStrictEntity\n)\n\nfrom .input_entities import (\n    EndpointEntity,\n    InputEntity,\n\n    NumberEntity,\n    BoolEntity,\n    TextEntity,\n    PathInput,\n    RawJsonEntity\n)\nfrom .color_entity import ColorEntity\nfrom .enum_entity import (\n    BaseEnumEntity,\n    EnumEntity,\n    HostsEnumEntity,\n    AppsEnumEntity,\n    ToolsEnumEntity,\n    TaskTypeEnumEntity,\n    DeadlineUrlEnumEntity,\n    AnatomyTemplatesEnumEntity,\n    ShotgridUrlEnumEntity,\n    RoyalRenderRootEnumEntity\n)\n\nfrom .list_entity import ListEntity\nfrom .dict_immutable_keys_entity import (\n    DictImmutableKeysEntity,\n    RootsDictEntity,\n    SyncServerSites\n)\nfrom .dict_mutable_keys_entity import DictMutableKeysEntity\nfrom .dict_conditional import (\n    DictConditionalEntity,\n    SyncServerProviders\n)\n\nfrom .anatomy_entities import AnatomyEntity\nfrom .op_version_entity import VersionsInputEntity\n\n__all__ = (\n    \"DefaultsNotDefined\",\n    \"StudioDefaultsNotDefined\",\n    \"BaseInvalidValue\",\n    \"InvalidValueType\",\n    \"InvalidKeySymbols\",\n    \"SchemaMissingFileInfo\",\n    \"SchemeGroupHierarchyBug\",\n    \"SchemaDuplicatedKeys\",\n    \"SchemaDuplicatedEnvGroupKeys\",\n    \"SchemaTemplateMissingKeys\",\n\n    \"NOT_SET\",\n    \"OverrideState\",\n\n    \"BaseEntity\",\n    \"GUIEntity\",\n    \"BaseItemEntity\",\n    \"ItemEntity\",\n\n    \"SystemSettings\",\n    \"ProjectSettings\",\n\n    \"PathEntity\",\n    \"ListStrictEntity\",\n\n    \"EndpointEntity\",\n    \"InputEntity\",\n\n    \"NumberEntity\",\n    \"BoolEntity\",\n    \"TextEntity\",\n    \"PathInput\",\n    \"RawJsonEntity\",\n\n    \"ColorEntity\",\n\n    \"BaseEnumEntity\",\n    \"EnumEntity\",\n    \"HostsEnumEntity\",\n    \"AppsEnumEntity\",\n    \"ToolsEnumEntity\",\n    \"TaskTypeEnumEntity\",\n    \"DeadlineUrlEnumEntity\",\n    \"ShotgridUrlEnumEntity\",\n    \"RoyalRenderRootEnumEntity\",\n    \"AnatomyTemplatesEnumEntity\",\n\n    \"ListEntity\",\n\n    \"DictImmutableKeysEntity\",\n    \"RootsDictEntity\",\n    \"SyncServerSites\",\n\n    \"DictMutableKeysEntity\",\n\n    \"DictConditionalEntity\",\n    \"SyncServerProviders\",\n\n    \"AnatomyEntity\",\n\n    \"VersionsInputEntity\",\n)\n"
  },
  {
    "path": "openpype/settings/entities/anatomy_entities.py",
    "content": "from .dict_immutable_keys_entity import DictImmutableKeysEntity\nfrom .lib import OverrideState\nfrom .exceptions import EntitySchemaError\n\n\nclass AnatomyEntity(DictImmutableKeysEntity):\n    schema_types = [\"anatomy\"]\n\n    def _update_current_metadata(self):\n        if self._override_state is OverrideState.PROJECT:\n            return {}\n        return super(AnatomyEntity, self)._update_current_metadata()\n\n    def set_override_state(self, *args, **kwargs):\n        super(AnatomyEntity, self).set_override_state(*args, **kwargs)\n        if self._override_state is OverrideState.PROJECT:\n            for child_obj in self.non_gui_children.values():\n                if not child_obj.has_project_override:\n                    self.add_to_project_override()\n                    break\n\n    def on_child_change(self, child_obj):\n        if self._override_state is OverrideState.PROJECT:\n            if not child_obj.has_project_override:\n                child_obj.add_to_project_override()\n        return super(AnatomyEntity, self).on_child_change(child_obj)\n\n    def schema_validations(self):\n        non_group_children = []\n        for key, child_obj in self.non_gui_children.items():\n            if not child_obj.is_group:\n                non_group_children.append(key)\n\n        if non_group_children:\n            _non_group_children = [\n                \"project_anatomy/{}\".format(key)\n                for key in non_group_children\n            ]\n            reason = (\n                \"Anatomy must have all children as groups.\"\n                \" Set 'is_group' to `true` on > {}\"\n            ).format(\", \".join([\n                '\"{}\"'.format(item)\n                for item in _non_group_children\n            ]))\n            raise EntitySchemaError(self, reason)\n\n        return super(AnatomyEntity, self).schema_validations()\n"
  },
  {
    "path": "openpype/settings/entities/base_entity.py",
    "content": "from uuid import uuid4\nfrom abc import ABCMeta, abstractmethod, abstractproperty\n\nimport six\n\nfrom .lib import (\n    NOT_SET,\n    OverrideState\n)\n\nfrom .exceptions import (\n    BaseInvalidValue,\n    InvalidValueType,\n    SchemeGroupHierarchyBug,\n    EntitySchemaError\n)\n\nfrom openpype.lib import Logger\n\n\n@six.add_metaclass(ABCMeta)\nclass BaseEntity:\n    \"\"\"Base entity class for Setting's item type workflow.\n\n    Args:\n        schema_data (dict): Schema data that defines entity behavior.\n    \"\"\"\n\n    def __init__(self, schema_data, *args, **kwargs):\n        self.schema_data = schema_data\n        tooltip = None\n        if schema_data:\n            tooltip = schema_data.get(\"tooltip\")\n        self.tooltip = tooltip\n\n        # Entity id\n        self._id = uuid4()\n\n    def __hash__(self):\n        \"\"\"Make entity hashable by it's id.\n\n        Helps to store entities as keys in dictionary.\n        \"\"\"\n        return self.id\n\n    @property\n    def id(self):\n        \"\"\"Unified identifier of an entity.\"\"\"\n        return self._id\n\n    @abstractproperty\n    def gui_type(self):\n        \"\"\"Is entity GUI type entity.\"\"\"\n        pass\n\n    @abstractmethod\n    def schema_validations(self):\n        \"\"\"Validation of schema.\"\"\"\n        pass\n\n\nclass GUIEntity(BaseEntity):\n    \"\"\"Entity without any specific logic that should be handled only in GUI.\"\"\"\n    gui_type = True\n\n    schema_types = [\"separator\", \"splitter\", \"label\"]\n\n    def __getitem__(self, key):\n        return self.schema_data[key]\n\n    def schema_validations(self):\n        \"\"\"TODO validate GUI schemas.\"\"\"\n        pass\n\n\nclass BaseItemEntity(BaseEntity):\n    \"\"\"Base of item entity that is not only for GUI but can modify values.\n\n    Defines minimum attributes of all entities that are not `gui_type`.\n\n    Args:\n        schema_data (dict): Schema data that defines entity behavior.\n    \"\"\"\n    gui_type = False\n\n    def __init__(self, schema_data):\n        super(BaseItemEntity, self).__init__(schema_data)\n\n        # Parent entity\n        self.parent = None\n\n        # Entity is dynamically created (in list or dict with mutable keys)\n        #   - can be also dynamically removed\n        self.is_dynamic_item = False\n\n        # Log object created on demand with `log` attribute\n        self._log = None\n\n        # Item path attribute (may be filled or be dynamic)\n        self._path = None\n\n        # These should be set on initialization and not change then\n        self.valid_value_types = getattr(self, \"valid_value_types\", NOT_SET)\n        self.value_on_not_set = getattr(self, \"value_on_not_set\", NOT_SET)\n\n        # Entity represents group entity\n        #   - all children entities will be saved on modification of overrides\n        self.is_group = False\n        # Entity's value will be stored into file with name of it's key\n        self.is_file = False\n        # Default values are not stored to an openpype file\n        # - these must not be set through schemas directly\n        self.dynamic_schema_id = None\n        self.is_dynamic_schema_node = False\n        self.is_in_dynamic_schema_node = False\n\n        # Reference to parent entity which has `is_group` == True\n        #   - stays as None if none of parents is group\n        self.group_item = None\n        # Reference to parent entity which has `is_file` == True\n        self.file_item = None\n        # Reference to `RootEntity`\n        self.root_item = None\n        # Change of value requires restart of OpenPype\n        self._require_restart_on_change = False\n\n        # Entity is in hierarchy of dynamically created entity\n        self.is_in_dynamic_item = False\n\n        # Roles of an entity\n        self.roles = None\n\n        # Key must be specified in schema data otherwise won't work as expected\n        self.require_key = True\n\n        # Key and label of an entity\n        self.key = None\n        self.label = None\n\n        # Override state defines which values are used, saved and how.\n        # TODO convert to private attribute\n        self._override_state = OverrideState.NOT_DEFINED\n        self._ignore_missing_defaults = None\n\n        # These attributes may change values during existence of an object\n        # Default value, studio override values and project override values\n        # - these should be set only with `update_default_value` etc.\n        # TODO convert to private attributes\n        self._default_value = NOT_SET\n        self._studio_override_value = NOT_SET\n        self._project_override_value = NOT_SET\n\n        # Entity has set `_default_value` (is not NOT_SET)\n        self.has_default_value = False\n\n        # Entity is marked as it contain studio override data so it's value\n        #   will be stored to studio overrides. This is relevant attribute\n        #   only if current override state is set to STUDIO.\n        self._has_studio_override = False\n        # Entity has set `_studio_override_value` (is not NOT_SET)\n        self.had_studio_override = False\n\n        # Entity is marked as it contain project override data so it's value\n        #   will be stored to project overrides. This is relevant attribute\n        #   only if current override state is set to PROJECT.\n        self._has_project_override = False\n        # Entity has set `_project_override_value` (is not NOT_SET)\n        self.had_project_override = False\n\n        self._default_log_invalid_types = True\n        self._studio_log_invalid_types = True\n        self._project_log_invalid_types = True\n\n        # Callbacks that are called on change.\n        # - main current purspose is to register GUI callbacks\n        self.on_change_callbacks = []\n\n        roles = schema_data.get(\"roles\")\n        if roles is None:\n            roles = []\n        elif not isinstance(roles, list):\n            roles = [roles]\n        self.roles = roles\n\n    @abstractmethod\n    def collect_static_entities_by_path(self):\n        \"\"\"Collect all paths of all static path entities.\n\n        Static path is entity which is not dynamic or under dynamic entity.\n        \"\"\"\n        pass\n\n    @property\n    def require_restart_on_change(self):\n        return self._require_restart_on_change\n\n    @property\n    def require_restart(self):\n        return False\n\n    @property\n    def has_studio_override(self):\n        \"\"\"Says if entity or it's children has studio overrides.\"\"\"\n        if self._override_state >= OverrideState.STUDIO:\n            return self._has_studio_override\n        return False\n\n    @property\n    def has_project_override(self):\n        \"\"\"Says if entity or it's children has project overrides.\"\"\"\n        if self._override_state >= OverrideState.PROJECT:\n            return self._has_project_override\n        return False\n\n    @property\n    def path(self):\n        \"\"\"Full path of an entity in settings hierarchy.\n\n        It is not possible to use this attribute during initialization because\n        initialization happens before entity is added to parent's children.\n        \"\"\"\n        if self._path is not None:\n            return self._path\n\n        path = self.parent.get_child_path(self)\n        if not self.is_in_dynamic_item and not self.is_dynamic_item:\n            self._path = path\n        return path\n\n    @abstractmethod\n    def get_child_path(self, child_entity):\n        \"\"\"Return path for a direct child entity.\"\"\"\n        pass\n\n    @abstractmethod\n    def get_entity_from_path(self, path):\n        \"\"\"Return system settings entity.\"\"\"\n        pass\n\n    @abstractmethod\n    def has_child_with_key(self, key):\n        \"\"\"Entity contains key as children.\"\"\"\n        pass\n\n    def schema_validations(self):\n        \"\"\"Validate schema of entity and it's hierachy.\n\n        Contain default validations same for all entities.\n        \"\"\"\n        # Entity must have defined valid value types.\n        if self.valid_value_types is NOT_SET:\n            raise EntitySchemaError(\n                self, \"Attribute `valid_value_types` is not filled.\"\n            )\n\n        # Check if entity has defined key when is required.\n        if self.require_key and not self.key:\n            error_msg = \"Missing \\\"key\\\" in schema data. {}\".format(\n                str(self.schema_data).replace(\"'\", '\"')\n            )\n            raise EntitySchemaError(self, error_msg)\n\n        # Group entity must have defined label. (UI specific)\n        # QUESTION this should not be required?\n        if not self.label and self.is_group:\n            raise EntitySchemaError(\n                self, \"Item is set as `is_group` but has empty `label`.\"\n            )\n\n        # Group item can be only once in on hierarchy branch.\n        if self.is_group and self.group_item is not None:\n            raise SchemeGroupHierarchyBug(self)\n\n        # Group item can be only once in on hierarchy branch.\n        if self.group_item is not None and self.is_dynamic_schema_node:\n            reason = (\n                \"Dynamic schema is inside grouped item {}.\"\n                \" Change group hierarchy or remove dynamic\"\n                \" schema to be able work properly.\"\n            ).format(self.group_item.path)\n            raise EntitySchemaError(self, reason)\n\n        # Dynamic items must not have defined labels. (UI specific)\n        if self.label and self.is_dynamic_item:\n            raise EntitySchemaError(\n                self, \"Item has set label but is used as dynamic item.\"\n            )\n\n        # Dynamic items or items in dynamic item must not have set `is_group`\n        if self.is_group and (self.is_dynamic_item or self.is_in_dynamic_item):\n            raise EntitySchemaError(\n                self, \"Dynamic entity has set `is_group` to true.\"\n            )\n\n        if (\n            self.require_restart_on_change\n            and (self.is_dynamic_item or self.is_in_dynamic_item)\n        ):\n            raise EntitySchemaError(\n                self, \"Dynamic entity can't require restart.\"\n            )\n\n    @abstractproperty\n    def root_key(self):\n        \"\"\"Root is represented as this dictionary key.\"\"\"\n        pass\n\n    @abstractmethod\n    def set_override_state(self, state, ignore_missing_defaults):\n        \"\"\"Set override state and trigger it on children.\n\n        Method discard all changes in hierarchy and use values, metadata\n        and all kind of values for defined override state. May be used to\n        apply updated values (default, studio overrides, project overrides).\n\n        Should start on root entity and when triggered then must be called on\n        all entities in hierarchy.\n\n        Argument `ignore_missing_defaults` should be used when entity has\n        children that are not saved or used all the time but override statu\n        must be changed and children must have any default value.\n\n        Args:\n            state (OverrideState): State to which should be data changed.\n            ignore_missing_defaults (bool): Ignore missing default values.\n                Entity won't raise `DefaultsNotDefined` and\n                `StudioDefaultsNotDefined`.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def on_change(self):\n        \"\"\"Trigger change callbacks and tell parent that has changed.\n\n        Can be any kind of change. Value has changed, has studio overrides\n        changed from True to False, etc.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def on_child_change(self, child_entity):\n        \"\"\"Triggered by children when they've changed.\n\n        Args:\n            child_entity (BaseItemEntity): Direct child entity that has\n                changed.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def set(self, value):\n        \"\"\"Set value of entity.\n\n        Args:\n            value (Any): Setter of value for this entity.\n        \"\"\"\n        pass\n\n    def is_value_valid_type(self, value):\n        \"\"\"Validate passed value type by entity's defined valid types.\n\n        Returns:\n            bool: True if value is in entity's defined types.\n        \"\"\"\n        return isinstance(value, self.valid_value_types)\n\n    def _validate_value_type(self, value):\n        \"\"\"Validate entered value.\n\n        Raises:\n            InvalidValueType: If value's type is not valid by entity's\n                definition.\n        \"\"\"\n        if self.is_value_valid_type(value):\n            return\n\n        raise InvalidValueType(self.valid_value_types, type(value), self.path)\n\n    def _convert_to_valid_type(self, value):\n        \"\"\"Private method of entity to convert value.\n\n        NOTE: Method is not abstract as more entities won't have implemented\n            logic inside.\n\n        Must return NOT_SET if can't convert the value.\n        \"\"\"\n        return NOT_SET\n\n    def convert_to_valid_type(self, value):\n        \"\"\"Check value type with possibility of conversion to valid.\n\n        If entered value has right type than is returned as it is. otherwise\n        is used privete method of entity to try convert.\n\n        Raises:\n            InvalidValueType: If value's type is not valid by entity's\n                definition and can't be converted by entity logic.\n        \"\"\"\n        #\n        if self.is_value_valid_type(value):\n            return value\n\n        new_value = self._convert_to_valid_type(value)\n        if new_value is not NOT_SET and self.is_value_valid_type(new_value):\n            return new_value\n\n        raise InvalidValueType(self.valid_value_types, type(value), self.path)\n\n    # TODO convert to private method\n    def _check_update_value(self, value, value_source, log_invalid_types=True):\n        \"\"\"Validation of value on update methods.\n\n        Update methods update data from currently saved settings so it is\n        possible to have invalid type mainly during development.\n\n        Args:\n            value (Any): Value that got to update method.\n            value_source (str): Source update method. Is used for logging and\n                is not used as part of logic (\"default\", \"studio override\",\n                \"project override\").\n\n        Returns:\n            Any: Return value itself if has valid type.\n            NOT_SET: If value has invalid type.\n        \"\"\"\n        # Nothing to validate if is NOT_SET\n        if value is NOT_SET:\n            return value\n\n        try:\n            new_value = self.convert_to_valid_type(value)\n        except BaseInvalidValue:\n            new_value = NOT_SET\n\n        if new_value is not NOT_SET:\n            return new_value\n\n        if log_invalid_types:\n            # Warning log about invalid value type.\n            self.log.warning(\n                (\n                    \"{} Got invalid value type for {} values.\"\n                    \" Expected types: {} | Got Type: {} | Value: \\\"{}\\\"\"\n                ).format(\n                    self.path, value_source,\n                    self.valid_value_types, type(value), str(value)\n                )\n            )\n        return NOT_SET\n\n    def available_for_role(self, role_name=None):\n        \"\"\"Is entity valid for role.\n\n        Args:\n            role_name (str): Name of role that will be validated. Entity's\n                `user_role` attribute is used if not defined.\n\n        Returns:\n            bool: True if is available for role.\n        \"\"\"\n        if not self.roles:\n            return True\n        if role_name is None:\n            role_name = self.user_role\n        return role_name in self.roles\n\n    @property\n    def user_role(self):\n        \"\"\"Entity is using user role.\n\n        Returns:\n            str: user role as string.\n\n        \"\"\"\n        return self.parent.user_role\n\n    @property\n    def log(self):\n        \"\"\"Auto created logger for debugging or warnings.\"\"\"\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    @abstractproperty\n    def schema_types(self):\n        pass\n\n    @abstractproperty\n    def has_unsaved_changes(self):\n        pass\n\n    @abstractmethod\n    def settings_value(self):\n        \"\"\"Value of an item without key without dynamic items.\"\"\"\n        pass\n\n    @abstractmethod\n    def collect_dynamic_schema_entities(self):\n        \"\"\"Collect entities that are on top of dynamically added schemas.\n\n        This method make sence only when defaults are saved.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def save(self):\n        \"\"\"Save data for current state.\"\"\"\n        pass\n\n    @abstractmethod\n    def _item_initialization(self):\n        \"\"\"Entity specific initialization process.\"\"\"\n        pass\n\n    @abstractproperty\n    def value(self):\n        \"\"\"Value of entity without metadata.\"\"\"\n        pass\n\n    @property\n    def _can_discard_changes(self):\n        \"\"\"Defines if `discard_changes` will be processed.\"\"\"\n        return self.has_unsaved_changes\n\n    @property\n    def _can_add_to_studio_default(self):\n        \"\"\"Defines if `add_to_studio_default` will be processed.\"\"\"\n        if self._override_state is not OverrideState.STUDIO:\n            return False\n\n        # Skip if entity is under group\n        if self.group_item is not None:\n            return False\n\n        # Skip if is group and any children is already marked with studio\n        #   overrides\n        if self.is_group and self.has_studio_override:\n            return False\n        return True\n\n    @property\n    def _can_remove_from_studio_default(self):\n        \"\"\"Defines if `remove_from_studio_default` can be processed.\"\"\"\n        if self._override_state is not OverrideState.STUDIO:\n            return False\n\n        if not self.has_studio_override:\n            return False\n        return True\n\n    @property\n    def _can_add_to_project_override(self):\n        \"\"\"Defines if `add_to_project_override` can be processed.\"\"\"\n        # Show only when project overrides are set\n        if self._override_state is not OverrideState.PROJECT:\n            return False\n\n        # Do not show on items under group item\n        if self.group_item is not None:\n            return False\n\n        # Skip if already is marked to save project overrides\n        if self.is_group and self.has_project_override:\n            return False\n        return True\n\n    @property\n    def _can_remove_from_project_override(self):\n        \"\"\"Defines if `remove_from_project_override` can be processed.\"\"\"\n        if self._override_state is not OverrideState.PROJECT:\n            return False\n\n        # Dynamic items can't have these actions\n        if self.is_dynamic_item or self.is_in_dynamic_item:\n            return False\n\n        if not self.has_project_override:\n            return False\n        return True\n\n    @property\n    def can_trigger_discard_changes(self):\n        \"\"\"Defines if can trigger `discard_changes`.\n\n        Also can be used as validation before the method is called.\n        \"\"\"\n        return self._can_discard_changes\n\n    @property\n    def can_trigger_add_to_studio_default(self):\n        \"\"\"Defines if can trigger `add_to_studio_default`.\n\n        Also can be used as validation before the method is called.\n        \"\"\"\n        if self.is_dynamic_item or self.is_in_dynamic_item:\n            return False\n        return self._can_add_to_studio_default\n\n    @property\n    def can_trigger_remove_from_studio_default(self):\n        \"\"\"Defines if can trigger `remove_from_studio_default`.\n\n        Also can be used as validation before the method is called.\n        \"\"\"\n        if self.is_dynamic_item or self.is_in_dynamic_item:\n            return False\n        return self._can_remove_from_studio_default\n\n    @property\n    def can_trigger_add_to_project_override(self):\n        \"\"\"Defines if can trigger `add_to_project_override`.\n\n        Also can be used as validation before the method is called.\n        \"\"\"\n        if self.is_dynamic_item or self.is_in_dynamic_item:\n            return False\n        return self._can_add_to_project_override\n\n    @property\n    def can_trigger_remove_from_project_override(self):\n        \"\"\"Defines if can trigger `remove_from_project_override`.\n\n        Also can be used as validation before the method is called.\n        \"\"\"\n        if self.is_dynamic_item or self.is_in_dynamic_item:\n            return False\n        return self._can_remove_from_project_override\n\n    def discard_changes(self, on_change_trigger=None):\n        \"\"\"Discard changes on entity and it's children.\n\n        Reset all values to same values as had when `set_override_state` was\n        called last time.\n\n        Must not affect `had_studio_override` value or `had_project_override`\n        value. It must be marked that there are keys/values which are not in\n        defaults or overrides.\n\n        Won't affect if will be stored as overrides if entity is under\n        group entity in hierarchy.\n\n        This is wrapper method that handles on_change callbacks only when all\n        `_discard_changes` on all children happened. That is important as\n        value changes may trigger change callbacks that must be ignored.\n        Callbacks are triggered by entity where method was called.\n\n        Args:\n            on_change_trigger (list): Callbacks of `on_change` should be stored\n                to trigger them afterwards.\n        \"\"\"\n        initialized = False\n        if on_change_trigger is None:\n            if not self.can_trigger_discard_changes:\n                return\n\n            initialized = True\n            on_change_trigger = []\n\n        self._discard_changes(on_change_trigger)\n\n        if initialized:\n            for callback in on_change_trigger:\n                callback()\n\n    @abstractmethod\n    def _discard_changes(self, on_change_trigger):\n        \"\"\"Entity's implementation to discard all changes made by user.\"\"\"\n        pass\n\n    def add_to_studio_default(self, on_change_trigger=None):\n        initialized = False\n        if on_change_trigger is None:\n            if not self.can_trigger_add_to_studio_default:\n                return\n\n            initialized = True\n            on_change_trigger = []\n\n        self._add_to_studio_default(on_change_trigger)\n\n        if initialized:\n            for callback in on_change_trigger:\n                callback()\n\n    @abstractmethod\n    def _add_to_studio_default(self, on_change_trigger):\n        \"\"\"Item's implementation to set current values as studio's overrides.\n\n        Mark item and it's children as they have studio overrides.\n        \"\"\"\n        pass\n\n    def remove_from_studio_default(self, on_change_trigger=None):\n        \"\"\"Remove studio overrides from entity and it's children.\n\n        Reset values to openpype's default and mark entity to not store values\n        as studio overrides if entity is not under group.\n\n        This is wrapper method that handles on_change callbacks only when all\n        `_remove_from_studio_default` on all children happened. That is\n        important as value changes may trigger change callbacks that must be\n        ignored. Callbacks are triggered by entity where method was called.\n\n        Args:\n            on_change_trigger (list): Callbacks of `on_change` should be stored\n                to trigger them afterwards.\n        \"\"\"\n        initialized = False\n        if on_change_trigger is None:\n            if not self.can_trigger_remove_from_studio_default:\n                return\n\n            initialized = True\n            on_change_trigger = []\n\n        self._remove_from_studio_default(on_change_trigger)\n\n        if initialized:\n            for callback in on_change_trigger:\n                callback()\n\n    @abstractmethod\n    def _remove_from_studio_default(self, on_change_trigger):\n        \"\"\"Item's implementation to remove studio overrides.\n\n        Mark item as it does not have studio overrides unset studio\n        override values.\n        \"\"\"\n        pass\n\n    def add_to_project_override(self, on_change_trigger=None):\n        initialized = False\n        if on_change_trigger is None:\n            if not self.can_trigger_add_to_project_override:\n                return\n\n            initialized = True\n            on_change_trigger = []\n\n        self._add_to_project_override(on_change_trigger)\n\n        if initialized:\n            for callback in on_change_trigger:\n                callback()\n\n    @abstractmethod\n    def _add_to_project_override(self, on_change_trigger):\n        \"\"\"Item's implementation to set values as overridden for project.\n\n        Mark item and all it's children to be stored as project overrides.\n        \"\"\"\n        pass\n\n    def remove_from_project_override(self, on_change_trigger=None):\n        \"\"\"Remove project overrides from entity and it's children.\n\n        Reset values to studio overrides or openpype's default and mark entity\n        to not store values as project overrides if entity is not under group.\n\n        This is wrapper method that handles on_change callbacks only when all\n        `_remove_from_project_override` on all children happened. That is\n        important as value changes may trigger change callbacks that must be\n        ignored. Callbacks are triggered by entity where method was called.\n\n        Args:\n            on_change_trigger (list): Callbacks of `on_change` should be stored\n                to trigger them afterwards.\n        \"\"\"\n        if self._override_state is not OverrideState.PROJECT:\n            return\n\n        initialized = False\n        if on_change_trigger is None:\n            if not self.can_trigger_remove_from_project_override:\n                return\n            initialized = True\n            on_change_trigger = []\n\n        self._remove_from_project_override(on_change_trigger)\n\n        if initialized:\n            for callback in on_change_trigger:\n                callback()\n\n    @abstractmethod\n    def _remove_from_project_override(self, on_change_trigger):\n        \"\"\"Item's implementation to remove project overrides.\n\n        Mark item as does not have project overrides. Must not change\n        `was_overridden` attribute value.\n\n        Args:\n            on_change_trigger (list): Callbacks of `on_change` should be stored\n                to trigger them afterwards.\n        \"\"\"\n        pass\n\n    def reset_callbacks(self):\n        \"\"\"Clear any registered callbacks on entity and all children.\"\"\"\n        self.on_change_callbacks = []\n\n\nclass ItemEntity(BaseItemEntity):\n    \"\"\"Item that is used as hierarchical entity.\n\n    Entity must have defined parent and can't be created outside it's parent.\n\n    Dynamically created entity is entity that can be removed from settings\n    hierarchy and it's key or existence is not defined in schemas. Are\n    created by `ListEntity` or `DictMutableKeysEntity`. Their information about\n    default value or modification is not relevant.\n\n    Args:\n        schema_data (dict): Schema data that defines entity behavior.\n        parent (BaseItemEntity): Parent entity that created this entity.\n        is_dynamic_item (bool): Entity should behave like dynamically created\n            entity.\n    \"\"\"\n    _default_label_wrap = {\n        \"use_label_wrap\": False,\n        \"collapsible\": True,\n        \"collapsed\": False\n    }\n\n    def __init__(self, schema_data, parent, is_dynamic_item=False):\n        super(ItemEntity, self).__init__(schema_data)\n\n        self.parent = parent\n        self.is_dynamic_item = is_dynamic_item\n\n        self.is_file = self.schema_data.get(\"is_file\", False)\n        # These keys have underscore as they must not be set in schemas\n        self.dynamic_schema_id = self.schema_data.get(\n            \"_dynamic_schema_id\", None\n        )\n        self.is_dynamic_schema_node = self.dynamic_schema_id is not None\n\n        self.is_group = self.schema_data.get(\"is_group\", False)\n        self.is_in_dynamic_item = bool(\n            not self.is_dynamic_item\n            and (self.parent.is_dynamic_item or self.parent.is_in_dynamic_item)\n        )\n\n        # Dynamic item can't have key defined in it-self\n        # - key is defined by it's parent\n        if self.is_dynamic_item:\n            self.require_key = False\n\n        # Root item reference\n        self.root_item = self.parent.root_item\n\n        # Item require restart on value change\n        require_restart_on_change = self.schema_data.get(\"require_restart\")\n        if (\n            require_restart_on_change is None\n            and not (self.is_dynamic_item or self.is_in_dynamic_item)\n        ):\n            require_restart_on_change = self.parent.require_restart_on_change\n        self._require_restart_on_change = require_restart_on_change\n\n        # File item reference\n        if not self.is_dynamic_schema_node:\n            self.is_in_dynamic_schema_node = (\n                self.parent.is_dynamic_schema_node\n                or self.parent.is_in_dynamic_schema_node\n            )\n\n        if (\n            not self.is_dynamic_schema_node\n            and not self.is_in_dynamic_schema_node\n        ):\n            if self.parent.is_file:\n                self.file_item = self.parent\n            elif self.parent.file_item:\n                self.file_item = self.parent.file_item\n\n        # Group item reference\n        if self.parent.is_group:\n            self.group_item = self.parent\n\n        elif self.parent.group_item is not None:\n            self.group_item = self.parent.group_item\n\n        self.key = self.schema_data.get(\"key\")\n        self.label = self.schema_data.get(\"label\")\n\n        # GUI attributes\n        _default_label_wrap = self.__class__._default_label_wrap\n        for key, value in ItemEntity._default_label_wrap.items():\n            if key not in _default_label_wrap:\n                self.log.warning(\n                    \"Class {} miss default label wrap key \\\"{}\\\"\".format(\n                        self.__class__.__name__, key\n                    )\n                )\n                _default_label_wrap[key] = value\n\n        use_label_wrap = self.schema_data.get(\"use_label_wrap\")\n        if use_label_wrap is None:\n            if not self.label:\n                use_label_wrap = False\n            else:\n                use_label_wrap = _default_label_wrap[\"use_label_wrap\"]\n        self.use_label_wrap = use_label_wrap\n\n        # Used only if `use_label_wrap` is set to True\n        self.collapsible = self.schema_data.get(\n            \"collapsible\",\n            _default_label_wrap[\"collapsible\"]\n        )\n        self.collapsed = self.schema_data.get(\n            \"collapsed\",\n            _default_label_wrap[\"collapsed\"]\n        )\n\n        self._item_initialization()\n\n    def save(self):\n        \"\"\"Call save on root item.\"\"\"\n        self.root_item.save()\n\n    @property\n    def root_key(self):\n        return self.root_item.root_key\n\n    @abstractmethod\n    def collect_dynamic_schema_entities(self, collector):\n        \"\"\"Collect entities that are on top of dynamically added schemas.\n\n        This method make sence only when defaults are saved.\n\n        Args:\n            collector(DynamicSchemaValueCollector): Object where dynamic\n                entities are stored.\n        \"\"\"\n        pass\n\n    def schema_validations(self):\n        if not self.label and self.use_label_wrap:\n            reason = (\n                \"Entity has set `use_label_wrap` to true but\"\n                \" does not have set `label`.\"\n            )\n            raise EntitySchemaError(self, reason)\n\n        if (\n            not self.is_dynamic_schema_node\n            and not self.is_in_dynamic_schema_node\n            and self.is_file\n            and self.file_item is not None\n        ):\n            reason = (\n                \"Entity has set `is_file` to true but\"\n                \" it's parent is already marked as file item.\"\n            )\n            raise EntitySchemaError(self, reason)\n\n        super(ItemEntity, self).schema_validations()\n\n    def create_schema_object(self, *args, **kwargs):\n        \"\"\"Reference method for creation of entities defined in RootEntity.\"\"\"\n        return self.schema_hub.create_schema_object(*args, **kwargs)\n\n    @property\n    def schema_hub(self):\n        return self.root_item.schema_hub\n\n    def get_entity_from_path(self, path):\n        return self.root_item.get_entity_from_path(path)\n\n    @abstractmethod\n    def update_default_value(self, parent_values, log_invalid_types=True):\n        \"\"\"Fill default values on startup or on refresh.\n\n        Default values stored in `openpype` repository should update all items\n        in schema. Each item should take values for his key and set it's value\n        or pass values down to children items.\n\n        Args:\n            parent_values (dict): Values of parent's item. But in case item is\n                used as widget, `parent_values` contain value for item.\n            log_invalid_types (bool): Log invalid type of value. Used when\n                entity can have children with same keys and different types.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def update_studio_value(self, parent_values, log_invalid_types=True):\n        \"\"\"Fill studio override values on startup or on refresh.\n\n        Set studio value if is not set to NOT_SET, in that case studio\n        overrides are not set yet.\n\n        Args:\n            parent_values (dict): Values of parent's item. But in case item is\n                used as widget, `parent_values` contain value for item.\n            log_invalid_types (bool): Log invalid type of value. Used when\n                entity can have children with same keys and different types.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def update_project_value(self, parent_values, log_invalid_types=True):\n        \"\"\"Fill project override values on startup, refresh or project change.\n\n        Set project value if is not set to NOT_SET, in that case project\n        overrides are not set yet.\n\n        Args:\n            parent_values (dict): Values of parent's item. But in case item is\n                used as widget, `parent_values` contain value for item.\n            log_invalid_types (bool): Log invalid type of value. Used when\n                entity can have children with same keys and different types.\n        \"\"\"\n        pass\n"
  },
  {
    "path": "openpype/settings/entities/color_entity.py",
    "content": "from .lib import STRING_TYPE\nfrom .input_entities import InputEntity\nfrom .exceptions import (\n    BaseInvalidValue,\n    InvalidValueType\n)\n\n\nclass ColorEntity(InputEntity):\n    schema_types = [\"color\"]\n\n    def _item_initialization(self):\n        self.valid_value_types = (list, )\n        self.use_alpha = self.schema_data.get(\"use_alpha\", True)\n        self.value_on_not_set = self.convert_to_valid_type(\n            self.schema_data.get(\"default\", [0, 0, 0, 255])\n        )\n\n    def set_override_state(self, *args, **kwargs):\n        super(ColorEntity, self).set_override_state(*args, **kwargs)\n        value = self._current_value\n        if (\n            not self.use_alpha\n            and isinstance(value, list)\n            and len(value) == 4\n        ):\n            value[3] = 255\n\n    def convert_to_valid_type(self, value):\n        \"\"\"Conversion to valid type.\n\n        Complexity of entity requires to override BaseEntity implementation.\n        \"\"\"\n        # Convertion to valid value type `list`\n        if isinstance(value, (set, tuple)):\n            value = list(value)\n\n        # Skip other validations if is not `list`\n        if not isinstance(value, list):\n            raise InvalidValueType(\n                self.valid_value_types, type(value), self.path\n            )\n\n        # Allow list of len 3 (last aplha is set to max)\n        if len(value) == 3:\n            value.append(255)\n\n        if len(value) != 4:\n            reason = \"Color entity expect 4 items in list got {}\".format(\n                len(value)\n            )\n            raise BaseInvalidValue(reason, self.path)\n\n        new_value = []\n        for item in value:\n            if not isinstance(item, int):\n                if isinstance(item, (STRING_TYPE, float)):\n                    item = int(item)\n\n            is_valid = isinstance(item, int) and -1 < item < 256\n            if not is_valid:\n                reason = (\n                    \"Color entity expect 4 integers in range 0-255 got {}\"\n                ).format(value)\n                raise BaseInvalidValue(reason, self.path)\n            new_value.append(item)\n\n        # Make sure\n        if not self.use_alpha:\n            new_value[3] = 255\n        return new_value\n"
  },
  {
    "path": "openpype/settings/entities/dict_conditional.py",
    "content": "import copy\n\nfrom .lib import (\n    OverrideState,\n    NOT_SET\n)\nfrom openpype.settings.constants import (\n    METADATA_KEYS,\n    M_OVERRIDDEN_KEY,\n    KEY_REGEX\n)\nfrom . import (\n    BaseItemEntity,\n    ItemEntity,\n    GUIEntity\n)\nfrom .exceptions import (\n    SchemaDuplicatedKeys,\n    EntitySchemaError,\n    InvalidKeySymbols\n)\n\n\nclass DictConditionalEntity(ItemEntity):\n    \"\"\"Entity represents dictionay with only one persistent key definition.\n\n    The persistent key is enumerator which define rest of children under\n    dictionary. There is not possibility of shared children.\n\n    Entity's keys can't be removed or added. But they may change based on\n    the persistent key. If you're change value manually (key by key) make sure\n    you'll change value of the persistent key as first. It is recommended to\n    use `set` method which handle this for you.\n\n    It is possible to use entity similar way as `dict` object. Returned values\n    are not real settings values but entities representing the value.\n    \"\"\"\n    schema_types = [\"dict-conditional\"]\n    _default_label_wrap = {\n        \"use_label_wrap\": False,\n        \"collapsible\": False,\n        \"collapsed\": True\n    }\n\n    def __getitem__(self, key):\n        \"\"\"Return entity inder key.\"\"\"\n        if key == self.enum_key:\n            return self.enum_entity\n        return self.non_gui_children[self.current_enum][key]\n\n    def __setitem__(self, key, value):\n        \"\"\"Set value of item under key.\"\"\"\n        if key == self.enum_key:\n            child_obj = self.enum_entity\n        else:\n            child_obj = self.non_gui_children[self.current_enum][key]\n        child_obj.set(value)\n\n    def __iter__(self):\n        \"\"\"Iter through keys.\"\"\"\n        for key in self.keys():\n            yield key\n\n    def __contains__(self, key):\n        \"\"\"Check if key is available.\"\"\"\n        if key == self.enum_key:\n            return True\n        return key in self.non_gui_children[self.current_enum]\n\n    def get(self, key, default=None):\n        \"\"\"Safe entity getter by key.\"\"\"\n        if key == self.enum_key:\n            return self.enum_entity\n        return self.non_gui_children[self.current_enum].get(key, default)\n\n    def keys(self):\n        \"\"\"Entity's keys.\"\"\"\n        keys = list(self.non_gui_children[self.current_enum].keys())\n        keys.insert(0, [self.enum_key])\n        return keys\n\n    def values(self):\n        \"\"\"Children entities.\"\"\"\n        values = [\n            self.enum_entity\n        ]\n        for child_entiy in self.non_gui_children[self.current_enum].values():\n            values.append(child_entiy)\n        return values\n\n    def items(self):\n        \"\"\"Children entities paired with their key (key, value).\"\"\"\n        items = [\n            (self.enum_key, self.enum_entity)\n        ]\n        for key, value in self.non_gui_children[self.current_enum].items():\n            items.append((key, value))\n        return items\n\n    def set(self, value):\n        \"\"\"Set value.\"\"\"\n        new_value = self.convert_to_valid_type(value)\n        # First change value of enum key if available\n        if self.enum_key in new_value:\n            self.enum_entity.set(new_value.pop(self.enum_key))\n\n        for _key, _value in new_value.items():\n            self.non_gui_children[self.current_enum][_key].set(_value)\n\n    def has_child_with_key(self, key):\n        return key in self.keys()\n\n    def _item_initialization(self):\n        self._default_metadata = NOT_SET\n        self._studio_override_metadata = NOT_SET\n        self._project_override_metadata = NOT_SET\n\n        self._ignore_child_changes = False\n\n        # `current_metadata` are still when schema is loaded\n        # - only metadata stored with dict item are gorup overrides in\n        #   M_OVERRIDDEN_KEY\n        self._current_metadata = {}\n        self._metadata_are_modified = False\n\n        # Entity must be group or in group\n        if (\n            self.group_item is None\n            and not self.is_dynamic_item\n            and not self.is_in_dynamic_item\n        ):\n            self.is_group = True\n\n        # Children are stored by key as keys are immutable and are defined by\n        # schema\n        self.valid_value_types = (dict, )\n        self.children = {}\n        self.non_gui_children = {}\n        self.gui_layout = {}\n\n        if self.is_dynamic_item:\n            self.require_key = False\n\n        self.enum_key = self.schema_data.get(\"enum_key\")\n        self.enum_label = self.schema_data.get(\"enum_label\")\n        self.enum_children = self.schema_data.get(\"enum_children\")\n        self.enum_default = self.schema_data.get(\"enum_default\")\n\n        self.enum_entity = None\n\n        # GUI attributes\n        self.enum_is_horizontal = self.schema_data.get(\n            \"enum_is_horizontal\", False\n        )\n        # `enum_on_right` can be used only if\n        self.enum_on_right = self.schema_data.get(\"enum_on_right\", False)\n\n        self.highlight_content = self.schema_data.get(\n            \"highlight_content\", False\n        )\n        self.show_borders = self.schema_data.get(\"show_borders\", True)\n\n        self._add_children()\n\n    @property\n    def current_enum(self):\n        \"\"\"Current value of enum entity.\n\n        This value define what children are used.\n        \"\"\"\n        if self.enum_entity is None:\n            return None\n        return self.enum_entity.value\n\n    def schema_validations(self):\n        \"\"\"Validation of schema data.\"\"\"\n        # Enum key must be defined\n        if self.enum_key is None:\n            raise EntitySchemaError(self, \"Key 'enum_key' is not set.\")\n\n        # Validate type of enum children\n        if not isinstance(self.enum_children, list):\n            raise EntitySchemaError(\n                self, \"Key 'enum_children' must be a list. Got: {}\".format(\n                    str(type(self.enum_children))\n                )\n            )\n\n        # Without defined enum children entity has nothing to do\n        if not self.enum_children:\n            raise EntitySchemaError(self, (\n                \"Key 'enum_children' have empty value. Entity can't work\"\n                \" without children definitions.\"\n            ))\n\n        children_def_keys = []\n        for children_def in self.enum_children:\n            if not isinstance(children_def, dict):\n                raise EntitySchemaError(self, (\n                    \"Children definition under key 'enum_children' must\"\n                    \" be a dictionary.\"\n                ))\n\n            if \"key\" not in children_def:\n                raise EntitySchemaError(self, (\n                    \"Children definition under key 'enum_children' miss\"\n                    \" 'key' definition.\"\n                ))\n            # We don't validate regex of these keys because they will be stored\n            #   as value at the end.\n            key = children_def[\"key\"]\n            if key in children_def_keys:\n                # TODO this hould probably be different exception?\n                raise SchemaDuplicatedKeys(self, key)\n            children_def_keys.append(key)\n\n        # Validate key duplications per each enum item\n        for children in self.children.values():\n            children_keys = set()\n            children_keys.add(self.enum_key)\n            for child_entity in children:\n                if not isinstance(child_entity, BaseItemEntity):\n                    continue\n                elif child_entity.key not in children_keys:\n                    children_keys.add(child_entity.key)\n                else:\n                    raise SchemaDuplicatedKeys(self, child_entity.key)\n\n        # Enum key must match key regex\n        if not KEY_REGEX.match(self.enum_key):\n            raise InvalidKeySymbols(self.path, self.enum_key)\n\n        # Validate all remaining keys with key regex\n        for children_by_key in self.non_gui_children.values():\n            for key in children_by_key.keys():\n                if not KEY_REGEX.match(key):\n                    raise InvalidKeySymbols(self.path, key)\n\n        super(DictConditionalEntity, self).schema_validations()\n        # Trigger schema validation on children entities\n        for children in self.children.values():\n            for child_obj in children:\n                child_obj.schema_validations()\n\n    def on_change(self):\n        \"\"\"Update metadata on change and pass change to parent.\"\"\"\n        self._update_current_metadata()\n\n        for callback in self.on_change_callbacks:\n            callback()\n        self.parent.on_child_change(self)\n\n    def on_child_change(self, child_obj):\n        \"\"\"Trigger on change callback if child changes are not ignored.\"\"\"\n        if self._ignore_child_changes:\n            return\n\n        if (\n            child_obj is self.enum_entity\n            or child_obj in self.children[self.current_enum]\n        ):\n            self.on_change()\n\n    def _add_children(self):\n        \"\"\"Add children from schema data and repare enum items.\n\n        Each enum item must have defined it's children. None are shared across\n        all enum items.\n\n        Nice to have: Have ability to have shared keys across all enum items.\n\n        All children are stored by their enum item.\n        \"\"\"\n        # Skip if are not defined\n        # - schema validations should raise and exception\n        if not self.enum_children or not self.enum_key:\n            return\n\n        valid_enum_items = []\n        for item in self.enum_children:\n            if isinstance(item, dict) and \"key\" in item:\n                valid_enum_items.append(item)\n\n        enum_keys = []\n        enum_items = []\n        for item in valid_enum_items:\n            item_key = item[\"key\"]\n            enum_keys.append(item_key)\n            item_label = item.get(\"label\") or item_key\n            enum_items.append({item_key: item_label})\n\n        if not enum_items:\n            return\n\n        if self.enum_default in enum_keys:\n            default_key = self.enum_default\n        else:\n            default_key = enum_keys[0]\n\n        # Create Enum child first\n        enum_key = self.enum_key or \"invalid\"\n        enum_schema = {\n            \"type\": \"enum\",\n            \"multiselection\": False,\n            \"enum_items\": enum_items,\n            \"key\": enum_key,\n            \"label\": self.enum_label,\n            \"default\": default_key\n        }\n\n        enum_entity = self.create_schema_object(enum_schema, self)\n        self.enum_entity = enum_entity\n\n        # Create children per each enum item\n        for item in valid_enum_items:\n            item_key = item[\"key\"]\n            # Make sure all keys have set value in these variables\n            # - key 'children' is optional\n            self.non_gui_children[item_key] = {}\n            self.children[item_key] = []\n            self.gui_layout[item_key] = []\n\n            children = item.get(\"children\") or []\n            for children_schema in children:\n                child_obj = self.create_schema_object(children_schema, self)\n                self.children[item_key].append(child_obj)\n                self.gui_layout[item_key].append(child_obj)\n                if isinstance(child_obj, GUIEntity):\n                    continue\n\n                self.non_gui_children[item_key][child_obj.key] = child_obj\n\n    def collect_static_entities_by_path(self):\n        if self.is_dynamic_item or self.is_in_dynamic_item:\n            return {}\n        return {self.path: self}\n\n    def get_child_path(self, child_obj):\n        \"\"\"Get hierarchical path of child entity.\n\n        Child must be entity's direct children. This must be possible to get\n        for any children even if not from current enum value.\n        \"\"\"\n        if child_obj is self.enum_entity:\n            return \"/\".join([self.path, self.enum_key])\n\n        result_key = None\n        for children in self.non_gui_children.values():\n            for key, _child_obj in children.items():\n                if _child_obj is child_obj:\n                    result_key = key\n                    break\n\n        if result_key is None:\n            raise ValueError(\"Didn't find child {}\".format(child_obj))\n\n        return \"/\".join([self.path, result_key])\n\n    def _update_current_metadata(self):\n        current_metadata = {}\n        for key, child_obj in self.non_gui_children[self.current_enum].items():\n            if self._override_state is OverrideState.DEFAULTS:\n                break\n\n            if not child_obj.is_group:\n                continue\n\n            if (\n                self._override_state is OverrideState.STUDIO\n                and not child_obj.has_studio_override\n            ):\n                continue\n\n            if (\n                self._override_state is OverrideState.PROJECT\n                and not child_obj.has_project_override\n            ):\n                continue\n\n            if M_OVERRIDDEN_KEY not in current_metadata:\n                current_metadata[M_OVERRIDDEN_KEY] = []\n            current_metadata[M_OVERRIDDEN_KEY].append(key)\n\n        # Define if current metadata are avaialble for current override state\n        metadata = NOT_SET\n        if self._override_state is OverrideState.STUDIO:\n            metadata = self._studio_override_metadata\n\n        elif self._override_state is OverrideState.PROJECT:\n            metadata = self._project_override_metadata\n\n        if metadata is NOT_SET:\n            metadata = {}\n\n        self._metadata_are_modified = current_metadata != metadata\n        self._current_metadata = current_metadata\n\n    def set_override_state(self, state, ignore_missing_defaults):\n        # Trigger override state change of root if is not same\n        if self.root_item.override_state is not state:\n            self.root_item.set_override_state(state)\n            return\n\n        # Change has/had override states\n        self._override_state = state\n        self._ignore_missing_defaults = ignore_missing_defaults\n\n        # Set override state on enum entity first\n        self.enum_entity.set_override_state(state, ignore_missing_defaults)\n\n        # Set override state on other enum children\n        # - these must not raise exception about missing defaults\n        for children_by_key in self.non_gui_children.values():\n            for child_obj in children_by_key.values():\n                child_obj.set_override_state(state, True)\n\n        self._update_current_metadata()\n\n    @property\n    def value(self):\n        output = {\n            self.enum_key: self.enum_entity.value\n        }\n        for key, child_obj in self.non_gui_children[self.current_enum].items():\n            output[key] = child_obj.value\n        return output\n\n    @property\n    def has_unsaved_changes(self):\n        if self._metadata_are_modified:\n            return True\n\n        return self._child_has_unsaved_changes\n\n    @property\n    def _child_has_unsaved_changes(self):\n        if self.enum_entity.has_unsaved_changes:\n            return True\n\n        for child_obj in self.non_gui_children[self.current_enum].values():\n            if child_obj.has_unsaved_changes:\n                return True\n        return False\n\n    @property\n    def has_studio_override(self):\n        return self._child_has_studio_override\n\n    @property\n    def _child_has_studio_override(self):\n        if self._override_state >= OverrideState.STUDIO:\n            if self.enum_entity.has_studio_override:\n                return True\n\n            for child_obj in self.non_gui_children[self.current_enum].values():\n                if child_obj.has_studio_override:\n                    return True\n        return False\n\n    @property\n    def has_project_override(self):\n        return self._child_has_project_override\n\n    @property\n    def _child_has_project_override(self):\n        if self._override_state >= OverrideState.PROJECT:\n            if self.enum_entity.has_project_override:\n                return True\n\n            for child_obj in self.non_gui_children[self.current_enum].values():\n                if child_obj.has_project_override:\n                    return True\n        return False\n\n    def collect_dynamic_schema_entities(self, collector):\n        if self.is_dynamic_schema_node:\n            collector.add_entity(self)\n\n    def settings_value(self):\n        if self._override_state is OverrideState.NOT_DEFINED:\n            return NOT_SET\n\n        if self._override_state is OverrideState.DEFAULTS:\n            children_items = [\n                (self.enum_key, self.enum_entity)\n            ]\n            for item in self.non_gui_children[self.current_enum].items():\n                children_items.append(item)\n\n            output = {}\n            for key, child_obj in children_items:\n                output[key] = child_obj.settings_value()\n            return output\n\n        if self.is_group:\n            if self._override_state is OverrideState.STUDIO:\n                if not self.has_studio_override:\n                    return NOT_SET\n            elif self._override_state is OverrideState.PROJECT:\n                if not self.has_project_override:\n                    return NOT_SET\n\n        output = {}\n        children_items = [\n            (self.enum_key, self.enum_entity)\n        ]\n        for item in self.non_gui_children[self.current_enum].items():\n            children_items.append(item)\n\n        for key, child_obj in children_items:\n            value = child_obj.settings_value()\n            if value is not NOT_SET:\n                output[key] = value\n\n        if not output:\n            return NOT_SET\n\n        output.update(self._current_metadata)\n        return output\n\n    def _prepare_value(self, value, log_invalid_types):\n        if value is NOT_SET or self.enum_key not in value:\n            return NOT_SET, NOT_SET\n\n        enum_value = value.get(self.enum_key)\n        if enum_value not in self.non_gui_children:\n            if log_invalid_types:\n                self.log.warning(\n                    \"{} Unknown enum key in default values: {}\".format(\n                        self.path, enum_value\n                    )\n                )\n            return NOT_SET, NOT_SET\n\n        # Create copy of value before poping values\n        value = copy.deepcopy(value)\n        metadata = {}\n        for key in METADATA_KEYS:\n            if key in value:\n                metadata[key] = value.pop(key)\n\n        enum_value = value.get(self.enum_key)\n\n        old_metadata = metadata.get(M_OVERRIDDEN_KEY)\n        if old_metadata:\n            old_metadata_set = set(old_metadata)\n            new_metadata = []\n            non_gui_children = self.non_gui_children[enum_value]\n            for key in non_gui_children.keys():\n                if key in old_metadata:\n                    new_metadata.append(key)\n                    old_metadata_set.remove(key)\n\n            for key in old_metadata_set:\n                new_metadata.append(key)\n            metadata[M_OVERRIDDEN_KEY] = new_metadata\n\n        return value, metadata\n\n    def update_default_value(self, value, log_invalid_types=True):\n        \"\"\"Update default values.\n\n        Not an api method, should be called by parent.\n        \"\"\"\n        self._default_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"default\", log_invalid_types\n        )\n        self.has_default_value = value is not NOT_SET\n        # TODO add value validation\n        value, metadata = self._prepare_value(value, log_invalid_types)\n        self._default_metadata = metadata\n\n        if value is NOT_SET:\n            self.enum_entity.update_default_value(value, log_invalid_types)\n            for children_by_key in self.non_gui_children.values():\n                for child_obj in children_by_key.values():\n                    child_obj.update_default_value(value, log_invalid_types)\n            return\n\n        value_keys = set(value.keys())\n        enum_value = value[self.enum_key]\n        expected_keys = set(self.non_gui_children[enum_value].keys())\n        expected_keys.add(self.enum_key)\n        unknown_keys = value_keys - expected_keys\n        if unknown_keys and log_invalid_types:\n            self.log.warning(\n                \"{} Unknown keys in default values: {}\".format(\n                    self.path,\n                    \", \".join(\"\\\"{}\\\"\".format(key) for key in unknown_keys)\n                )\n            )\n\n        self.enum_entity.update_default_value(enum_value, log_invalid_types)\n\n        for enum_key, children_by_key in self.non_gui_children.items():\n            _log_invalid_types = log_invalid_types\n            if _log_invalid_types:\n                _log_invalid_types = enum_key == enum_value\n\n            value_copy = copy.deepcopy(value)\n            for key, child_obj in children_by_key.items():\n                child_value = value_copy.get(key, NOT_SET)\n                child_obj.update_default_value(child_value, _log_invalid_types)\n\n    def update_studio_value(self, value, log_invalid_types=True):\n        \"\"\"Update studio override values.\n\n        Not an api method, should be called by parent.\n        \"\"\"\n\n        self._studio_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"studio override\", log_invalid_types\n        )\n        value, metadata = self._prepare_value(value, log_invalid_types)\n        self._studio_override_metadata = metadata\n        self.had_studio_override = metadata is not NOT_SET\n\n        if value is NOT_SET:\n            self.enum_entity.update_studio_value(value, log_invalid_types)\n            for children_by_key in self.non_gui_children.values():\n                for child_obj in children_by_key.values():\n                    child_obj.update_studio_value(value, log_invalid_types)\n            return\n\n        value_keys = set(value.keys())\n        enum_value = value[self.enum_key]\n        expected_keys = set(self.non_gui_children[enum_value])\n        expected_keys.add(self.enum_key)\n        unknown_keys = value_keys - expected_keys\n        if unknown_keys and log_invalid_types:\n            self.log.warning(\n                \"{} Unknown keys in studio overrides: {}\".format(\n                    self.path,\n                    \", \".join(\"\\\"{}\\\"\".format(key) for key in unknown_keys)\n                )\n            )\n\n        self.enum_entity.update_studio_value(enum_value, log_invalid_types)\n        for enum_key, children_by_key in self.non_gui_children.items():\n            _log_invalid_types = log_invalid_types\n            if _log_invalid_types:\n                _log_invalid_types = enum_key == enum_value\n\n            value_copy = copy.deepcopy(value)\n            for key, child_obj in children_by_key.items():\n                child_value = value_copy.get(key, NOT_SET)\n                child_obj.update_studio_value(child_value, _log_invalid_types)\n\n    def update_project_value(self, value, log_invalid_types=True):\n        \"\"\"Update project override values.\n\n        Not an api method, should be called by parent.\n        \"\"\"\n\n        self._project_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"project override\", log_invalid_types\n        )\n        value, metadata = self._prepare_value(value, log_invalid_types)\n        self._project_override_metadata = metadata\n        self.had_project_override = metadata is not NOT_SET\n\n        if value is NOT_SET:\n            self.enum_entity.update_project_value(value, log_invalid_types)\n            for children_by_key in self.non_gui_children.values():\n                for child_obj in children_by_key.values():\n                    child_obj.update_project_value(value, log_invalid_types)\n            return\n\n        value_keys = set(value.keys())\n        enum_value = value[self.enum_key]\n        expected_keys = set(self.non_gui_children[enum_value])\n        expected_keys.add(self.enum_key)\n        unknown_keys = value_keys - expected_keys\n        if unknown_keys and log_invalid_types:\n            self.log.warning(\n                \"{} Unknown keys in project overrides: {}\".format(\n                    self.path,\n                    \", \".join(\"\\\"{}\\\"\".format(key) for key in unknown_keys)\n                )\n            )\n\n        self.enum_entity.update_project_value(enum_value, log_invalid_types)\n        for enum_key, children_by_key in self.non_gui_children.items():\n            _log_invalid_types = log_invalid_types\n            if _log_invalid_types:\n                _log_invalid_types = enum_key == enum_value\n\n            value_copy = copy.deepcopy(value)\n            for key, child_obj in children_by_key.items():\n                child_value = value_copy.get(key, NOT_SET)\n                child_obj.update_project_value(child_value, _log_invalid_types)\n\n    def _discard_changes(self, on_change_trigger):\n        self._ignore_child_changes = True\n\n        self.enum_entity.discard_changes(on_change_trigger)\n        for children_by_key in self.non_gui_children.values():\n            for child_obj in children_by_key.values():\n                child_obj.discard_changes(on_change_trigger)\n\n        self._ignore_child_changes = False\n\n    def _add_to_studio_default(self, on_change_trigger):\n        self._ignore_child_changes = True\n\n        self.enum_entity.add_to_studio_default(on_change_trigger)\n        for children_by_key in self.non_gui_children.values():\n            for child_obj in children_by_key.values():\n                child_obj.add_to_studio_default(on_change_trigger)\n\n        self._ignore_child_changes = False\n\n        self._update_current_metadata()\n\n        self.parent.on_child_change(self)\n\n    def _remove_from_studio_default(self, on_change_trigger):\n        self._ignore_child_changes = True\n\n        self.enum_entity.remove_from_studio_default(on_change_trigger)\n        for children_by_key in self.non_gui_children.values():\n            for child_obj in children_by_key.values():\n                child_obj.remove_from_studio_default(on_change_trigger)\n\n        self._ignore_child_changes = False\n\n    def _add_to_project_override(self, on_change_trigger):\n        self._ignore_child_changes = True\n\n        self.enum_entity.add_to_project_override(on_change_trigger)\n        for children_by_key in self.non_gui_children.values():\n            for child_obj in children_by_key.values():\n                child_obj.add_to_project_override(on_change_trigger)\n\n        self._ignore_child_changes = False\n\n        self._update_current_metadata()\n\n        self.parent.on_child_change(self)\n\n    def _remove_from_project_override(self, on_change_trigger):\n        if self._override_state is not OverrideState.PROJECT:\n            return\n\n        self._ignore_child_changes = True\n\n        self.enum_entity.remove_from_project_override(on_change_trigger)\n        for children_by_key in self.non_gui_children.values():\n            for child_obj in children_by_key.values():\n                child_obj.remove_from_project_override(on_change_trigger)\n\n        self._ignore_child_changes = False\n\n    def reset_callbacks(self):\n        \"\"\"Reset registered callbacks on entity and children.\"\"\"\n        super(DictConditionalEntity, self).reset_callbacks()\n        for children in self.children.values():\n            for child_entity in children:\n                child_entity.reset_callbacks()\n\n\nclass SyncServerProviders(DictConditionalEntity):\n    schema_types = [\"sync-server-providers\"]\n\n    def _add_children(self):\n        self.enum_key = \"provider\"\n        self.enum_label = \"Provider\"\n\n        enum_children = self._get_enum_children()\n        if not enum_children:\n            enum_children.append({\n                \"key\": None,\n                \"label\": \"< Nothing >\"\n            })\n        self.enum_children = enum_children\n\n        super(SyncServerProviders, self)._add_children()\n\n    def _get_enum_children(self):\n        from openpype_modules import sync_server\n\n        from openpype_modules.sync_server.providers import lib as lib_providers\n\n        provider_code_to_label = {}\n        providers = lib_providers.factory.providers\n        for provider_code, provider_info in providers.items():\n            provider, _ = provider_info\n            provider_code_to_label[provider_code] = provider.LABEL\n\n        system_settings_schema = (\n            sync_server\n            .SyncServerModule\n            .get_system_settings_schema()\n        )\n\n        enum_children = []\n        for provider_code, configurables in system_settings_schema.items():\n            # any site could be exposed or vendorized by different site\n            # eg studio site content could be mapped on sftp site, single file\n            # accessible via 2 different protocols (sites)\n            configurables.append(\n                {\n                    \"type\": \"list\",\n                    \"key\": \"alternative_sites\",\n                    \"label\": \"Alternative sites\",\n                    \"object_type\": \"text\"\n                }\n            )\n            label = provider_code_to_label.get(provider_code) or provider_code\n\n            enum_children.append({\n                \"key\": provider_code,\n                \"label\": label,\n                \"children\": configurables\n            })\n        return enum_children\n"
  },
  {
    "path": "openpype/settings/entities/dict_immutable_keys_entity.py",
    "content": "import copy\nimport collections\n\nfrom .lib import (\n    WRAPPER_TYPES,\n    OverrideState,\n    NOT_SET,\n    STRING_TYPE\n)\nfrom openpype.settings.constants import (\n    METADATA_KEYS,\n    M_OVERRIDDEN_KEY,\n    KEY_REGEX\n)\nfrom . import (\n    BaseItemEntity,\n    ItemEntity,\n    BoolEntity,\n    GUIEntity\n)\nfrom .exceptions import (\n    DefaultsNotDefined,\n    SchemaDuplicatedKeys,\n    EntitySchemaError,\n    InvalidKeySymbols\n)\n\n\nclass DictImmutableKeysEntity(ItemEntity):\n    \"\"\"Entity that represents dictionary with predefined keys.\n\n    Entity's keys can't be removed or added and children type is defined in\n    schema data.\n\n    It is possible to use entity similar way as `dict` object. Returned values\n    are not real settings values but entities representing the value.\n    \"\"\"\n    schema_types = [\"dict\"]\n    _default_label_wrap = {\n        \"use_label_wrap\": True,\n        \"collapsible\": True,\n        \"collapsed\": True\n    }\n\n    def __getitem__(self, key):\n        \"\"\"Return entity inder key.\"\"\"\n        return self.non_gui_children[key]\n\n    def __setitem__(self, key, value):\n        \"\"\"Set value of item under key.\"\"\"\n        child_obj = self.non_gui_children[key]\n        child_obj.set(value)\n\n    def __iter__(self):\n        \"\"\"Iter through keys.\"\"\"\n        for key in self.keys():\n            yield key\n\n    def __contains__(self, key):\n        \"\"\"Check if key is available.\"\"\"\n        return key in self.non_gui_children\n\n    def get(self, key, default=None):\n        \"\"\"Safe entity getter by key.\"\"\"\n        return self.non_gui_children.get(key, default)\n\n    def keys(self):\n        \"\"\"Entity's keys.\"\"\"\n        return self.non_gui_children.keys()\n\n    def values(self):\n        \"\"\"Children entities.\"\"\"\n        return self.non_gui_children.values()\n\n    def items(self):\n        \"\"\"Children entities paired with their key (key, value).\"\"\"\n        return self.non_gui_children.items()\n\n    def set(self, value):\n        \"\"\"Set value.\"\"\"\n        new_value = self.convert_to_valid_type(value)\n        for _key, _value in new_value.items():\n            self.non_gui_children[_key].set(_value)\n\n    def schema_validations(self):\n        \"\"\"Validation of schema data.\"\"\"\n        children_keys = set()\n        for child_entity in self.children:\n            if not isinstance(child_entity, BaseItemEntity):\n                continue\n            elif child_entity.key not in children_keys:\n                children_keys.add(child_entity.key)\n            else:\n                raise SchemaDuplicatedKeys(self, child_entity.key)\n\n        for key in self.keys():\n            if not KEY_REGEX.match(key):\n                raise InvalidKeySymbols(self.path, key)\n\n        if self.checkbox_key:\n            checkbox_child = self.non_gui_children.get(self.checkbox_key)\n            if not checkbox_child:\n                reason = \"Checkbox children \\\"{}\\\" was not found.\".format(\n                    self.checkbox_key\n                )\n                raise EntitySchemaError(self, reason)\n\n            if not isinstance(checkbox_child, BoolEntity):\n                reason = (\n                    \"Checkbox children \\\"{}\\\" is not `boolean` type.\"\n                ).format(self.checkbox_key)\n                raise EntitySchemaError(self, reason)\n\n        super(DictImmutableKeysEntity, self).schema_validations()\n        # Trigger schema validation on children entities\n        for child_obj in self.children:\n            child_obj.schema_validations()\n\n    def on_change(self):\n        \"\"\"Update metadata on change and pass change to parent.\"\"\"\n        self._update_current_metadata()\n\n        for callback in self.on_change_callbacks:\n            callback()\n        self.parent.on_child_change(self)\n\n    def on_child_change(self, _child_obj):\n        \"\"\"Trigger on change callback if child changes are not ignored.\"\"\"\n        if not self._ignore_child_changes:\n            self.on_change()\n\n    def _add_children(self, schema_data, first=True):\n        \"\"\"Add children from schema data and separate gui wrappers.\n\n        Wrappers are stored in way so tool can create them and keep relation\n        to entities.\n\n        Args:\n            schema_data (dict): Schema data of an entity.\n            first (bool): Helper to know if was method called from inside of\n                method when handling gui wrappers.\n        \"\"\"\n        added_children = []\n        children_deque = collections.deque()\n        for _children_schema in schema_data[\"children\"]:\n            children_schemas = self.schema_hub.resolve_schema_data(\n                _children_schema\n            )\n            for children_schema in children_schemas:\n                children_deque.append(children_schema)\n\n        while children_deque:\n            children_schema = children_deque.popleft()\n            if children_schema[\"type\"] in WRAPPER_TYPES:\n                _children_schema = copy.deepcopy(children_schema)\n                wrapper_children = self._add_children(\n                    children_schema, False\n                )\n                _children_schema[\"children\"] = wrapper_children\n                added_children.append(_children_schema)\n                continue\n\n            child_obj = self.create_schema_object(children_schema, self)\n            self.children.append(child_obj)\n            added_children.append(child_obj)\n            if isinstance(child_obj, GUIEntity):\n                continue\n\n            self.non_gui_children[child_obj.key] = child_obj\n\n        if not first:\n            return added_children\n\n        for child_obj in added_children:\n            self.gui_layout.append(child_obj)\n\n    def _item_initialization(self):\n        self._default_metadata = NOT_SET\n        self._studio_override_metadata = NOT_SET\n        self._project_override_metadata = NOT_SET\n\n        self._ignore_child_changes = False\n\n        # `current_metadata` are still when schema is loaded\n        # - only metadata stored with dict item are gorup overrides in\n        #   M_OVERRIDDEN_KEY\n        self._current_metadata = {}\n        self._metadata_are_modified = False\n\n        # Children are stored by key as keys are immutable and are defined by\n        # schema\n        self.valid_value_types = (dict, )\n        self.children = []\n        self.non_gui_children = {}\n        self.gui_layout = []\n        self._add_children(self.schema_data)\n\n        if self.is_dynamic_item:\n            self.require_key = False\n\n        # GUI attributes\n        self.checkbox_key = self.schema_data.get(\"checkbox_key\")\n        self.highlight_content = self.schema_data.get(\n            \"highlight_content\", False\n        )\n        self.show_borders = self.schema_data.get(\"show_borders\", True)\n\n    def has_child_with_key(self, key):\n        return key in self.non_gui_children\n\n    def collect_static_entities_by_path(self):\n        output = {}\n        if self.is_dynamic_item or self.is_in_dynamic_item:\n            return output\n\n        output[self.path] = self\n        for children in self.non_gui_children.values():\n            result = children.collect_static_entities_by_path()\n            if result:\n                output.update(result)\n        return output\n\n    def get_child_path(self, child_obj):\n        \"\"\"Get hierarchical path of child entity.\n\n        Child must be entity's direct children.\n        \"\"\"\n        result_key = None\n        for key, _child_obj in self.non_gui_children.items():\n            if _child_obj is child_obj:\n                result_key = key\n                break\n\n        if result_key is None:\n            raise ValueError(\"Didn't find child {}\".format(child_obj))\n\n        return \"/\".join([self.path, result_key])\n\n    def _update_current_metadata(self):\n        current_metadata = {}\n        for key, child_obj in self.non_gui_children.items():\n            if self._override_state is OverrideState.DEFAULTS:\n                break\n\n            if not child_obj.is_group:\n                continue\n\n            if (\n                self._override_state is OverrideState.STUDIO\n                and not child_obj.has_studio_override\n            ):\n                continue\n\n            if (\n                self._override_state is OverrideState.PROJECT\n                and not child_obj.has_project_override\n            ):\n                continue\n\n            if M_OVERRIDDEN_KEY not in current_metadata:\n                current_metadata[M_OVERRIDDEN_KEY] = []\n            current_metadata[M_OVERRIDDEN_KEY].append(key)\n\n        # Define if current metadata are avaialble for current override state\n        metadata = NOT_SET\n        if self._override_state is OverrideState.STUDIO:\n            metadata = self._studio_override_metadata\n\n        elif self._override_state is OverrideState.PROJECT:\n            metadata = self._project_override_metadata\n\n        if metadata is NOT_SET:\n            metadata = {}\n\n        self._metadata_are_modified = current_metadata != metadata\n        self._current_metadata = current_metadata\n\n    def set_override_state(self, state, ignore_missing_defaults):\n        # Trigger override state change of root if is not same\n        if self.root_item.override_state is not state:\n            self.root_item.set_override_state(state)\n            return\n\n        # Change has/had override states\n        self._override_state = state\n        self._ignore_missing_defaults = ignore_missing_defaults\n\n        for child_obj in self.non_gui_children.values():\n            child_obj.set_override_state(state, ignore_missing_defaults)\n\n        self._update_current_metadata()\n\n    @property\n    def value(self):\n        output = {}\n        for key, child_obj in self.non_gui_children.items():\n            output[key] = child_obj.value\n        return output\n\n    @property\n    def has_unsaved_changes(self):\n        if self._metadata_are_modified:\n            return True\n\n        return self._child_has_unsaved_changes\n\n    @property\n    def _child_has_unsaved_changes(self):\n        for child_obj in self.non_gui_children.values():\n            if child_obj.has_unsaved_changes:\n                return True\n        return False\n\n    @property\n    def has_studio_override(self):\n        return self._child_has_studio_override\n\n    @property\n    def _child_has_studio_override(self):\n        if self._override_state >= OverrideState.STUDIO:\n            for child_obj in self.non_gui_children.values():\n                if child_obj.has_studio_override:\n                    return True\n        return False\n\n    @property\n    def has_project_override(self):\n        return self._child_has_project_override\n\n    @property\n    def _child_has_project_override(self):\n        if self._override_state >= OverrideState.PROJECT:\n            for child_obj in self.non_gui_children.values():\n                if child_obj.has_project_override:\n                    return True\n        return False\n\n    def collect_dynamic_schema_entities(self, collector):\n        for child_obj in self.non_gui_children.values():\n            child_obj.collect_dynamic_schema_entities(collector)\n\n        if self.is_dynamic_schema_node:\n            collector.add_entity(self)\n\n    def settings_value(self):\n        if self._override_state is OverrideState.NOT_DEFINED:\n            return NOT_SET\n\n        if self._override_state is OverrideState.DEFAULTS:\n            is_dynamic_schema_node = (\n                self.is_dynamic_schema_node or self.is_in_dynamic_schema_node\n            )\n            output = {}\n            for key, child_obj in self.non_gui_children.items():\n                if child_obj.is_dynamic_schema_node:\n                    continue\n\n                child_value = child_obj.settings_value()\n                if (\n                    not is_dynamic_schema_node\n                    and not child_obj.is_file\n                    and not child_obj.file_item\n                ):\n                    for _key, _value in child_value.items():\n                        new_key = \"/\".join([key, _key])\n                        output[new_key] = _value\n                else:\n                    output[key] = child_value\n            return output\n\n        if self.is_group:\n            if self._override_state is OverrideState.STUDIO:\n                if not self.has_studio_override:\n                    return NOT_SET\n            elif self._override_state is OverrideState.PROJECT:\n                if not self.has_project_override:\n                    return NOT_SET\n\n        output = {}\n        for key, child_obj in self.non_gui_children.items():\n            value = child_obj.settings_value()\n            if value is not NOT_SET:\n                output[key] = value\n\n        if not output:\n            return NOT_SET\n\n        output.update(self._current_metadata)\n        return output\n\n    def _prepare_value(self, value):\n        if value is NOT_SET:\n            return NOT_SET, NOT_SET\n\n        # Create copy of value before poping values\n        value = copy.deepcopy(value)\n        metadata = {}\n        for key in METADATA_KEYS:\n            if key in value:\n                metadata[key] = value.pop(key)\n\n        old_metadata = metadata.get(M_OVERRIDDEN_KEY)\n        if old_metadata:\n            old_metadata_set = set(old_metadata)\n            new_metadata = []\n            for key in self.non_gui_children.keys():\n                if key in old_metadata:\n                    new_metadata.append(key)\n                    old_metadata_set.remove(key)\n\n            for key in old_metadata_set:\n                new_metadata.append(key)\n            metadata[M_OVERRIDDEN_KEY] = new_metadata\n\n        return value, metadata\n\n    def update_default_value(self, value, log_invalid_types=True):\n        \"\"\"Update default values.\n\n        Not an api method, should be called by parent.\n        \"\"\"\n\n        self._default_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"default\", log_invalid_types\n        )\n        self.has_default_value = value is not NOT_SET\n        # TODO add value validation\n        value, metadata = self._prepare_value(value)\n        self._default_metadata = metadata\n\n        if value is NOT_SET:\n            for child_obj in self.non_gui_children.values():\n                child_obj.update_default_value(value, log_invalid_types)\n            return\n\n        value_keys = set(value.keys())\n        expected_keys = set(self.non_gui_children)\n        unknown_keys = value_keys - expected_keys\n        if unknown_keys and log_invalid_types:\n            self.log.warning(\n                \"{} Unknown keys in default values: {}\".format(\n                    self.path,\n                    \", \".join(\"\\\"{}\\\"\".format(key) for key in unknown_keys)\n                )\n            )\n\n        for key, child_obj in self.non_gui_children.items():\n            child_value = value.get(key, NOT_SET)\n            child_obj.update_default_value(child_value, log_invalid_types)\n\n    def update_studio_value(self, value, log_invalid_types=True):\n        \"\"\"Update studio override values.\n\n        Not an api method, should be called by parent.\n        \"\"\"\n\n        self._studio_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"studio override\", log_invalid_types\n        )\n        value, metadata = self._prepare_value(value)\n        self._studio_override_metadata = metadata\n        self.had_studio_override = metadata is not NOT_SET\n\n        if value is NOT_SET:\n            for child_obj in self.non_gui_children.values():\n                child_obj.update_studio_value(value, log_invalid_types)\n            return\n\n        value_keys = set(value.keys())\n        expected_keys = set(self.non_gui_children)\n        unknown_keys = value_keys - expected_keys\n        if unknown_keys and log_invalid_types:\n            self.log.warning(\n                \"{} Unknown keys in studio overrides: {}\".format(\n                    self.path,\n                    \", \".join(\"\\\"{}\\\"\".format(key) for key in unknown_keys)\n                )\n            )\n        for key, child_obj in self.non_gui_children.items():\n            child_value = value.get(key, NOT_SET)\n            child_obj.update_studio_value(child_value, log_invalid_types)\n\n    def update_project_value(self, value, log_invalid_types=True):\n        \"\"\"Update project override values.\n\n        Not an api method, should be called by parent.\n        \"\"\"\n\n        self._project_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"project override\", log_invalid_types\n        )\n        value, metadata = self._prepare_value(value)\n        self._project_override_metadata = metadata\n        self.had_project_override = metadata is not NOT_SET\n\n        if value is NOT_SET:\n            for child_obj in self.non_gui_children.values():\n                child_obj.update_project_value(value, log_invalid_types)\n            return\n\n        value_keys = set(value.keys())\n        expected_keys = set(self.non_gui_children)\n        unknown_keys = value_keys - expected_keys\n        if unknown_keys and log_invalid_types:\n            self.log.warning(\n                \"{} Unknown keys in project overrides: {}\".format(\n                    self.path,\n                    \", \".join(\"\\\"{}\\\"\".format(key) for key in unknown_keys)\n                )\n            )\n\n        for key, child_obj in self.non_gui_children.items():\n            child_value = value.get(key, NOT_SET)\n            child_obj.update_project_value(child_value, log_invalid_types)\n\n    def _discard_changes(self, on_change_trigger):\n        self._ignore_child_changes = True\n\n        for child_obj in self.non_gui_children.values():\n            child_obj.discard_changes(on_change_trigger)\n\n        self._ignore_child_changes = False\n\n    def _add_to_studio_default(self, on_change_trigger):\n        self._ignore_child_changes = True\n        for child_obj in self.non_gui_children.values():\n            child_obj.add_to_studio_default(on_change_trigger)\n        self._ignore_child_changes = False\n\n        self._update_current_metadata()\n\n        self.parent.on_child_change(self)\n\n    def _remove_from_studio_default(self, on_change_trigger):\n        self._ignore_child_changes = True\n        for child_obj in self.non_gui_children.values():\n            child_obj.remove_from_studio_default(on_change_trigger)\n        self._ignore_child_changes = False\n\n    def _add_to_project_override(self, _on_change_trigger):\n        self._ignore_child_changes = True\n        for child_obj in self.non_gui_children.values():\n            child_obj.add_to_project_override(_on_change_trigger)\n        self._ignore_child_changes = False\n\n        self._update_current_metadata()\n\n        self.parent.on_child_change(self)\n\n    def _remove_from_project_override(self, on_change_trigger):\n        if self._override_state is not OverrideState.PROJECT:\n            return\n\n        self._ignore_child_changes = True\n        for child_obj in self.non_gui_children.values():\n            child_obj.remove_from_project_override(on_change_trigger)\n        self._ignore_child_changes = False\n\n    def reset_callbacks(self):\n        \"\"\"Reset registered callbacks on entity and children.\"\"\"\n        super(DictImmutableKeysEntity, self).reset_callbacks()\n        for child_entity in self.children:\n            child_entity.reset_callbacks()\n\n\nclass RootsDictEntity(DictImmutableKeysEntity):\n    \"\"\"Entity that adds ability to fill value for roots of current project.\n\n    Value schema is defined by `object_type`.\n\n    It is not possible to change override state (Studio values will always\n    contain studio overrides and same for project). That is because roots can\n    be totally different for each project.\n    \"\"\"\n    _origin_schema_data = None\n    schema_types = [\"dict-roots\"]\n\n    def _item_initialization(self):\n        origin_schema_data = self.schema_data\n\n        self.separate_items = origin_schema_data.get(\"separate_items\", True)\n        object_type = origin_schema_data.get(\"object_type\")\n        if isinstance(object_type, STRING_TYPE):\n            object_type = {\"type\": object_type}\n        self.object_type = object_type\n\n        if self.group_item is None and not self.is_group:\n            self.is_group = True\n\n        schema_data = copy.deepcopy(self.schema_data)\n        schema_data[\"children\"] = []\n\n        self.schema_data = schema_data\n        self._origin_schema_data = origin_schema_data\n\n        self._default_value = NOT_SET\n        self._studio_value = NOT_SET\n        self._project_value = NOT_SET\n\n        super(RootsDictEntity, self)._item_initialization()\n\n    def schema_validations(self):\n        if self.object_type is None:\n            reason = (\n                \"Missing children definitions for root values\"\n                \" ('object_type' not filled).\"\n            )\n            raise EntitySchemaError(self, reason)\n\n        if not isinstance(self.object_type, dict):\n            reason = (\n                \"Children definitions for root values must be dictionary\"\n                \" ('object_type' is \\\"{}\\\").\"\n            ).format(str(type(self.object_type)))\n            raise EntitySchemaError(self, reason)\n\n        super(RootsDictEntity, self).schema_validations()\n\n    def set_override_state(self, state, ignore_missing_defaults):\n        self.children = []\n        self.non_gui_children = {}\n        self.gui_layout = []\n\n        roots_entity = self.get_entity_from_path(\n            \"project_anatomy/roots\"\n        )\n        children = []\n        first = True\n        for key in roots_entity.keys():\n            if first:\n                first = False\n            elif self.separate_items:\n                children.append({\"type\": \"separator\"})\n            child = copy.deepcopy(self.object_type)\n            child[\"key\"] = key\n            child[\"label\"] = key\n            children.append(child)\n\n        schema_data = copy.deepcopy(self.schema_data)\n        schema_data[\"children\"] = children\n\n        self._add_children(schema_data)\n\n        self._set_children_values(state, ignore_missing_defaults)\n\n        super(RootsDictEntity, self).set_override_state(\n            state, True\n        )\n\n        if state == OverrideState.STUDIO:\n            self.add_to_studio_default()\n\n        elif state == OverrideState.PROJECT:\n            self.add_to_project_override()\n\n    def on_child_change(self, child_obj):\n        if self._override_state is OverrideState.STUDIO:\n            if not child_obj.has_studio_override:\n                self.add_to_studio_default()\n\n        elif self._override_state is OverrideState.PROJECT:\n            if not child_obj.has_project_override:\n                self.add_to_project_override()\n\n        return super(RootsDictEntity, self).on_child_change(child_obj)\n\n    def _set_children_values(self, state, ignore_missing_defaults):\n        if state >= OverrideState.DEFAULTS:\n            default_value = self._default_value\n            if default_value is NOT_SET:\n                if (\n                    not ignore_missing_defaults\n                    and state > OverrideState.DEFAULTS\n                ):\n                    raise DefaultsNotDefined(self)\n                else:\n                    default_value = {}\n\n            for key, child_obj in self.non_gui_children.items():\n                child_value = default_value.get(key, NOT_SET)\n                child_obj.update_default_value(child_value)\n\n        if state >= OverrideState.STUDIO:\n            value = self._studio_value\n            if value is NOT_SET:\n                value = {}\n\n            for key, child_obj in self.non_gui_children.items():\n                child_value = value.get(key, NOT_SET)\n                child_obj.update_studio_value(child_value)\n\n        if state >= OverrideState.PROJECT:\n            value = self._project_value\n            if value is NOT_SET:\n                value = {}\n\n            for key, child_obj in self.non_gui_children.items():\n                child_value = value.get(key, NOT_SET)\n                child_obj.update_project_value(child_value)\n\n    def _update_current_metadata(self):\n        \"\"\"Override this method as this entity should not have metadata.\"\"\"\n        self._metadata_are_modified = False\n        self._current_metadata = {}\n\n    def update_default_value(self, value, log_invalid_types=True):\n        \"\"\"Update default values.\n\n        Not an api method, should be called by parent.\n        \"\"\"\n\n        self._default_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"default\", log_invalid_types\n        )\n        value, _ = self._prepare_value(value)\n\n        self._default_value = value\n        self._default_metadata = {}\n        self.has_default_value = value is not NOT_SET\n\n    def update_studio_value(self, value, log_invalid_types=True):\n        \"\"\"Update studio override values.\n\n        Not an api method, should be called by parent.\n        \"\"\"\n\n        self._studio_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"studio override\", log_invalid_types\n        )\n        value, _ = self._prepare_value(value)\n\n        self._studio_value = value\n        self._studio_override_metadata = {}\n        self.had_studio_override = value is not NOT_SET\n\n    def update_project_value(self, value, log_invalid_types=True):\n        \"\"\"Update project override values.\n\n        Not an api method, should be called by parent.\n        \"\"\"\n\n        self._project_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"project override\", log_invalid_types\n        )\n        value, _metadata = self._prepare_value(value)\n\n        self._project_value = value\n        self._project_override_metadata = {}\n        self.had_project_override = value is not NOT_SET\n\n\nclass SyncServerSites(DictImmutableKeysEntity):\n    \"\"\"Dictionary enity for sync sites.\n\n    Can be used only in project settings.\n\n    Is loading sites from system settings. Uses site name as key and by site's\n    provider loads project settings schemas calling method\n    `get_project_settings_schema` on provider.\n\n    Each provider have `enabled` boolean entity to be able know if site should\n    be enabled for the project. Enabled is by default set to False.\n    \"\"\"\n    schema_types = [\"sync-server-sites\"]\n\n    def _item_initialization(self):\n        # Make sure this is a group\n        if self.group_item is None and not self.is_group:\n            self.is_group = True\n\n        # Fake children for `dict` validations\n        self.schema_data[\"children\"] = []\n        # Site names changed or were removed\n        #   - to find out that site names was removed so project values\n        #       contain more data than should\n        self._sites_changed = False\n\n        super(SyncServerSites, self)._item_initialization()\n\n    def set_override_state(self, state, ignore_missing_defaults):\n        # Cleanup children related attributes\n        self.children = []\n        self.non_gui_children = {}\n        self.gui_layout = []\n\n        # Create copy of schema\n        schema_data = copy.deepcopy(self.schema_data)\n        # Collect children\n        children = self._get_children()\n        schema_data[\"children\"] = children\n\n        self._add_children(schema_data)\n\n        self._sites_changed = False\n        self._set_children_values(state, ignore_missing_defaults)\n\n        super(SyncServerSites, self).set_override_state(state, True)\n\n    @property\n    def has_unsaved_changes(self):\n        if self._sites_changed:\n            return True\n        return super(SyncServerSites, self).has_unsaved_changes\n\n    @property\n    def has_studio_override(self):\n        if self._sites_changed:\n            return True\n        return super(SyncServerSites, self).has_studio_override\n\n    @property\n    def has_project_override(self):\n        if self._sites_changed:\n            return True\n        return super(SyncServerSites, self).has_project_override\n\n    def _get_children(self):\n        from openpype_modules import sync_server\n\n        # Load system settings to find out all created sites\n        modules_entity = self.get_entity_from_path(\"system_settings/modules\")\n        sync_server_settings_entity = modules_entity.get(\"sync_server\")\n\n        # Get project settings configurations for all providers\n        project_settings_schema = (\n            sync_server\n            .SyncServerModule\n            .get_project_settings_schema()\n        )\n\n        children = []\n        # Add 'enabled' for each site to be able know if should be used for\n        #   the project\n        checkbox_child = {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"default\": False\n        }\n        if sync_server_settings_entity is not None:\n            sites_entity = sync_server_settings_entity[\"sites\"]\n            for site_name, provider_entity in sites_entity.items():\n                provider_name = provider_entity[\"provider\"].value\n                provider_children = copy.deepcopy(\n                    project_settings_schema.get(provider_name)\n                ) or []\n                provider_children.insert(0, copy.deepcopy(checkbox_child))\n                children.append({\n                    \"type\": \"dict\",\n                    \"key\": site_name,\n                    \"label\": site_name,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": provider_children\n                })\n\n        return children\n\n    def _set_children_values(self, state, ignore_missing_defaults):\n        current_site_names = set(self.non_gui_children.keys())\n\n        if state >= OverrideState.DEFAULTS:\n            default_value = self._default_value\n            if default_value is NOT_SET:\n                if (\n                    not ignore_missing_defaults\n                    and state > OverrideState.DEFAULTS\n                ):\n                    raise DefaultsNotDefined(self)\n                else:\n                    default_value = {}\n\n            for key, child_obj in self.non_gui_children.items():\n                child_value = default_value.get(key, NOT_SET)\n                child_obj.update_default_value(child_value)\n\n        if state >= OverrideState.STUDIO:\n            value = self._studio_value\n            if value is NOT_SET:\n                value = {}\n\n            for key, child_obj in self.non_gui_children.items():\n                child_value = value.get(key, NOT_SET)\n                child_obj.update_studio_value(child_value)\n\n            if state is OverrideState.STUDIO:\n                value_keys = set(value.keys())\n                self._sites_changed = value_keys != current_site_names\n\n        if state >= OverrideState.PROJECT:\n            value = self._project_value\n            if value is NOT_SET:\n                value = {}\n\n            for key, child_obj in self.non_gui_children.items():\n                child_value = value.get(key, NOT_SET)\n                child_obj.update_project_value(child_value)\n\n            if state is OverrideState.PROJECT:\n                value_keys = set(value.keys())\n                self._sites_changed = value_keys != current_site_names\n\n    def _update_current_metadata(self):\n        \"\"\"Override this method as this entity should not have metadata.\"\"\"\n        self._metadata_are_modified = False\n        self._current_metadata = {}\n\n    def update_default_value(self, value, log_invalid_types=True):\n        \"\"\"Update default values.\n\n        Not an api method, should be called by parent.\n        \"\"\"\n\n        self._default_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"default\", log_invalid_types\n        )\n        value, _ = self._prepare_value(value)\n\n        self._default_value = value\n        self._default_metadata = {}\n        self.has_default_value = value is not NOT_SET\n\n    def update_studio_value(self, value, log_invalid_types=True):\n        \"\"\"Update studio override values.\n\n        Not an api method, should be called by parent.\n        \"\"\"\n\n        self._studio_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"studio override\", log_invalid_types\n        )\n        value, _ = self._prepare_value(value)\n\n        self._studio_value = value\n        self._studio_override_metadata = {}\n        self.had_studio_override = value is not NOT_SET\n\n    def update_project_value(self, value, log_invalid_types=True):\n        \"\"\"Update project override values.\n\n        Not an api method, should be called by parent.\n        \"\"\"\n\n        self._project_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"project override\", log_invalid_types\n        )\n        value, _metadata = self._prepare_value(value)\n\n        self._project_value = value\n        self._project_override_metadata = {}\n        self.had_project_override = value is not NOT_SET\n"
  },
  {
    "path": "openpype/settings/entities/dict_mutable_keys_entity.py",
    "content": "import re\nimport copy\nfrom .lib import (\n    NOT_SET,\n    OverrideState\n)\nfrom . import EndpointEntity\nfrom .exceptions import (\n    DefaultsNotDefined,\n    InvalidKeySymbols,\n    StudioDefaultsNotDefined,\n    RequiredKeyModified,\n    EntitySchemaError\n)\nfrom openpype.settings.constants import (\n    METADATA_KEYS,\n    M_DYNAMIC_KEY_LABEL,\n    KEY_REGEX,\n    KEY_ALLOWED_SYMBOLS\n)\n\n\nclass DictMutableKeysEntity(EndpointEntity):\n    \"\"\"Dictionary entity that has mutable keys.\n\n    Keys of entity's children can be modified, removed or added. Children have\n    defined entity type so it is not possible to have 2 different entity types\n    as children.\n\n    TODOs:\n    - cleanup children on pop\n        - remove child's reference to parent\n        - clear callbacks\n    \"\"\"\n    schema_types = [\"dict-modifiable\"]\n    _default_label_wrap = {\n        \"use_label_wrap\": True,\n        \"collapsible\": True,\n        \"collapsed\": True\n    }\n\n    _miss_arg = object()\n\n    def __getitem__(self, key):\n        if key not in self.children_by_key:\n            self.add_key(key)\n        return self.children_by_key[key]\n\n    def __setitem__(self, key, value):\n        self.set_key_value(key, value)\n\n    def __iter__(self):\n        for key in self.keys():\n            yield key\n\n    def __contains__(self, key):\n        return key in self.children_by_key\n\n    def pop(self, key, *args, **kwargs):\n        if key in self.required_keys:\n            raise RequiredKeyModified(self.path, key)\n\n        if self._override_state is OverrideState.STUDIO:\n            self._has_studio_override = True\n        elif self._override_state is OverrideState.PROJECT:\n            self._has_project_override = True\n\n        result = self.children_by_key.pop(key, *args, **kwargs)\n        self.on_change()\n        return result\n\n    def get(self, key, default=None):\n        return self.children_by_key.get(key, default)\n\n    def keys(self):\n        return self.children_by_key.keys()\n\n    def values(self):\n        return self.children_by_key.values()\n\n    def items(self):\n        return self.children_by_key.items()\n\n    def clear(self):\n        for key in tuple(self.children_by_key.keys()):\n            self.pop(key)\n\n    def set(self, value):\n        new_value = self.convert_to_valid_type(value)\n\n        prev_keys = set(self.keys())\n\n        for _key, _value in new_value.items():\n            self.set_key_value(_key, _value)\n            if _key in prev_keys:\n                prev_keys.remove(_key)\n\n        for key in prev_keys:\n            self.pop(key)\n\n    def _convert_to_valid_type(self, value):\n        try:\n            return dict(value)\n        except Exception:\n            pass\n        return super(DictMutableKeysEntity, self)._convert_to_valid_type(value)\n\n    def set_key_value(self, key, value):\n        # TODO Check for value type if is Settings entity?\n        child_obj = self.children_by_key.get(key)\n        if not child_obj:\n            if not self.store_as_list and not KEY_REGEX.match(key):\n                raise InvalidKeySymbols(self.path, key)\n\n            child_obj = self.add_key(key)\n\n        child_obj.set(value)\n\n    def change_key(self, old_key, new_key):\n        if old_key in self.required_keys:\n            raise RequiredKeyModified(self.path, old_key)\n\n        if new_key == old_key:\n            return\n\n        if not self.store_as_list and not KEY_REGEX.match(new_key):\n            raise InvalidKeySymbols(self.path, new_key)\n\n        self.children_by_key[new_key] = self.children_by_key.pop(old_key)\n        self._on_key_label_change()\n\n    def _on_key_label_change(self):\n        if self._override_state is OverrideState.STUDIO:\n            self._has_studio_override = True\n        elif self._override_state is OverrideState.PROJECT:\n            self._has_project_override = True\n        self.on_change()\n\n    def _add_key(self, key, _ingore_key_validation=False):\n        if key in self.children_by_key:\n            self.pop(key)\n\n        if (\n            not _ingore_key_validation\n            and not self.store_as_list\n            and not KEY_REGEX.match(key)\n        ):\n            raise InvalidKeySymbols(self.path, key)\n\n        item_schema = self.item_schema\n\n        new_child = self.create_schema_object(item_schema, self, True)\n        self.children_by_key[key] = new_child\n        return new_child\n\n    def add_key(self, key):\n        new_child = self._add_key(key)\n        new_child.set_override_state(\n            self._override_state, self._ignore_missing_defaults\n        )\n        self.on_change()\n        return new_child\n\n    def change_child_key(self, child_entity, new_key):\n        old_key = None\n        for key, child in self.children_by_key.items():\n            if child is child_entity:\n                old_key = key\n                break\n\n        self.change_key(old_key, new_key)\n\n    def get_child_key(self, child_entity):\n        for key, child in self.children_by_key.items():\n            if child is child_entity:\n                return key\n        return None\n\n    # Label methods\n    def get_child_label(self, child_entity):\n        return self.children_label_by_id.get(child_entity.id)\n\n    def set_child_label(self, child_entity, label):\n        self.children_label_by_id[child_entity.id] = label\n        self._on_key_label_change()\n\n    def get_key_label(self, key):\n        child_entity = self.children_by_key[key]\n        return self.get_child_label(child_entity)\n\n    def set_key_label(self, key, label):\n        child_entity = self.children_by_key[key]\n        self.set_child_label(child_entity, label)\n\n    def has_child_with_key(self, key):\n        return key in self.children_by_key\n\n    def _item_initialization(self):\n        self._default_metadata = {}\n        self._studio_override_metadata = {}\n        self._project_override_metadata = {}\n\n        self.initial_value = None\n\n        self._ignore_child_changes = False\n\n        self.valid_value_types = (dict, )\n        self.value_on_not_set = {}\n\n        self.children_by_key = {}\n        self.children_label_by_id = {}\n\n        self.store_as_list = self.schema_data.get(\"store_as_list\") or False\n\n        self.required_keys = self.schema_data.get(\"required_keys\") or []\n        self.collapsible_key = self.schema_data.get(\"collapsible_key\") or False\n        # GUI attributes\n        self.highlight_content = (\n            self.schema_data.get(\"highlight_content\") or False\n        )\n\n        object_type = self.schema_data.get(\"object_type\") or {}\n        if not isinstance(object_type, dict):\n            # Backwards compatibility\n            object_type = {\n                \"type\": object_type\n            }\n            input_modifiers = self.schema_data.get(\"input_modifiers\") or {}\n            if input_modifiers:\n                self.log.warning((\n                    \"Used deprecated key `input_modifiers` to define item.\"\n                    \" Rather use `object_type` as dictionary with modifiers.\"\n                ))\n                object_type.update(input_modifiers)\n        self.item_schema = object_type\n\n        if self.group_item is None:\n            self.is_group = True\n\n    def schema_validations(self):\n        # Allow to have not set label if keys are collapsible\n        # - this it to bypass label validation\n        used_temp_label = False\n        if self.is_group and not self.label and self.collapsible_key:\n            used_temp_label = True\n            self.label = \"LABEL\"\n\n        super(DictMutableKeysEntity, self).schema_validations()\n        if used_temp_label:\n            self.label = None\n\n        if not self.schema_data.get(\"object_type\"):\n            reason = (\n                \"Modifiable dictionary must have specified `object_type`.\"\n            )\n            raise EntitySchemaError(self, reason)\n\n        # TODO Ability to store labels should be defined with different key\n        if self.collapsible_key and self.file_item is None:\n            reason = (\n                \"Modifiable dictionary with collapsible keys is not under\"\n                \" file item so can't store metadata.\"\n            )\n            raise EntitySchemaError(self, reason)\n\n        # Validate object type schema\n        child_validated = False\n        for child_entity in self.children_by_key.values():\n            child_entity.schema_validations()\n            child_validated = True\n            break\n\n        if not child_validated:\n            key = \"__tmp__\"\n            tmp_child = self._add_key(key)\n            tmp_child.schema_validations()\n            self.children_by_key.pop(key)\n\n    def get_child_path(self, child_obj):\n        result_key = None\n        for key, _child_obj in self.children_by_key.items():\n            if _child_obj is child_obj:\n                result_key = key\n                break\n\n        if result_key is None:\n            raise ValueError(\"Didn't find child {}\".format(child_obj))\n\n        return \"/\".join([self.path, result_key])\n\n    def on_child_change(self, _child_entity):\n        if self._ignore_child_changes:\n            return\n\n        if self._override_state is OverrideState.STUDIO:\n            self._has_studio_override = True\n        elif self._override_state is OverrideState.PROJECT:\n            self._has_project_override = True\n\n        self.on_change()\n\n    def _get_metadata_for_state(self, state):\n        if (\n            state is OverrideState.PROJECT\n            and self._project_override_value is not NOT_SET\n        ):\n            return self._project_override_metadata\n\n        if (\n            state >= OverrideState.STUDIO\n            and self._studio_override_value is not NOT_SET\n        ):\n            return self._studio_override_metadata\n\n        return self._default_metadata\n\n    def _metadata_for_current_state(self):\n        return self._get_metadata_for_state(self._override_state)\n\n    def set_override_state(self, state, ignore_missing_defaults):\n        # Trigger override state change of root if is not same\n        if self.root_item.override_state is not state:\n            self.root_item.set_override_state(state)\n            return\n\n        # TODO change metadata\n        self._override_state = state\n        self._ignore_missing_defaults = ignore_missing_defaults\n\n        # Ignore if is dynamic item and use default in that case\n        if not self.is_dynamic_item and not self.is_in_dynamic_item:\n            if state > OverrideState.DEFAULTS:\n                if (\n                    not self.has_default_value\n                    and not ignore_missing_defaults\n                ):\n                    raise DefaultsNotDefined(self)\n\n            elif state > OverrideState.STUDIO:\n                if (\n                    not self.had_studio_override\n                    and not ignore_missing_defaults\n                ):\n                    raise StudioDefaultsNotDefined(self)\n\n        if state is OverrideState.STUDIO:\n            self._has_studio_override = self.had_studio_override\n\n        elif state is OverrideState.PROJECT:\n            self._has_project_override = self.had_project_override\n            self._has_studio_override = self.had_studio_override\n\n        using_project_overrides = False\n        using_studio_overrides = False\n        using_default_values = False\n        if (\n            state is OverrideState.PROJECT\n            and self.had_project_override\n        ):\n            using_project_overrides = True\n            value = self._project_override_value\n            metadata = self._project_override_metadata\n\n        elif (\n            state >= OverrideState.STUDIO\n            and self.had_studio_override\n        ):\n            using_studio_overrides = True\n            value = self._studio_override_value\n            metadata = self._studio_override_metadata\n\n        else:\n            using_default_values = True\n            value = self._default_value\n            metadata = self._default_metadata\n\n        if value is NOT_SET:\n            using_default_values = False\n            value = self.value_on_not_set\n\n        using_values_from_state = False\n        log_invalid_types = True\n        if state is OverrideState.PROJECT:\n            log_invalid_types = self._project_log_invalid_types\n            using_values_from_state = using_project_overrides\n        elif state is OverrideState.STUDIO:\n            log_invalid_types = self._studio_log_invalid_types\n            using_values_from_state = using_studio_overrides\n        elif state is OverrideState.DEFAULTS:\n            log_invalid_types = self._default_log_invalid_types\n            using_values_from_state = using_default_values\n\n        new_value = copy.deepcopy(value)\n\n        if using_values_from_state:\n            initial_value = copy.deepcopy(value)\n            initial_value.update(metadata)\n\n        # Simulate `clear` method without triggering value change\n        for key in tuple(self.children_by_key.keys()):\n            self.children_by_key.pop(key)\n\n        for required_key in self.required_keys:\n            if required_key not in new_value:\n                new_value[required_key] = NOT_SET\n\n        # Create new children\n        children_label_by_id = {}\n        metadata_labels = metadata.get(M_DYNAMIC_KEY_LABEL) or {}\n        for _key, _value in new_value.items():\n            label = metadata_labels.get(_key)\n            if self.store_as_list or KEY_REGEX.match(_key):\n                child_entity = self._add_key(_key)\n            else:\n                # Replace invalid characters with underscore\n                # - this is safety to not break already existing settings\n                new_key = self._convert_to_regex_valid_key(_key)\n                if not using_values_from_state:\n                    child_entity = self._add_key(new_key)\n                else:\n                    child_entity = self._add_key(\n                        _key, _ingore_key_validation=True\n                    )\n                    self.change_key(_key, new_key)\n                    _key = new_key\n\n                if not label:\n                    label = metadata_labels.get(new_key)\n\n            child_entity.update_default_value(_value, log_invalid_types)\n            if using_project_overrides:\n                child_entity.update_project_value(_value, log_invalid_types)\n            elif using_studio_overrides:\n                child_entity.update_studio_value(_value, log_invalid_types)\n\n            if label:\n                children_label_by_id[child_entity.id] = label\n            child_entity.set_override_state(state, ignore_missing_defaults)\n\n        self.children_label_by_id = children_label_by_id\n\n        _settings_value = self.settings_value()\n        if using_values_from_state:\n            if _settings_value is NOT_SET:\n                initial_value = NOT_SET\n\n            elif self.store_as_list:\n                new_initial_value = []\n                for key, value in _settings_value:\n                    if key in initial_value:\n                        new_initial_value.append([key, initial_value.pop(key)])\n\n                for key, value in initial_value.items():\n                    new_initial_value.append([key, value])\n                initial_value = new_initial_value\n        else:\n            initial_value = _settings_value\n\n        self.initial_value = initial_value\n\n    def _convert_to_regex_valid_key(self, key):\n        return re.sub(\n            r\"[^{}]+\".format(KEY_ALLOWED_SYMBOLS),\n            \"_\",\n            key\n        )\n\n    def children_key_by_id(self):\n        return {\n            child_entity.id: key\n            for key, child_entity in self.children_by_key.items()\n        }\n\n    @property\n    def value(self):\n        if self.store_as_list:\n            output = []\n            for key, child_entity in self.children_by_key.items():\n                output.append([key, child_entity.value])\n            return output\n\n        output = {}\n        for key, child_entity in self.children_by_key.items():\n            output[key] = child_entity.value\n        return output\n\n    @property\n    def metadata(self):\n        output = {}\n        if not self.children_label_by_id:\n            return output\n\n        children_key_by_id = self.children_key_by_id()\n        label_metadata = {}\n        for child_id, label in self.children_label_by_id.items():\n            key = children_key_by_id.get(child_id)\n            if key:\n                label_metadata[key] = label\n\n        output[M_DYNAMIC_KEY_LABEL] = label_metadata\n\n        return output\n\n    @property\n    def has_unsaved_changes(self):\n        if (\n            self._override_state is OverrideState.PROJECT\n            and self._has_project_override != self.had_project_override\n        ):\n            return True\n\n        elif (\n            self._override_state is OverrideState.STUDIO\n            and self._has_studio_override != self.had_studio_override\n        ):\n            return True\n\n        if self._child_has_unsaved_changes:\n            return True\n\n        if self.metadata != self._metadata_for_current_state():\n            return True\n\n        if self.settings_value() != self.initial_value:\n            return True\n\n        return False\n\n    @property\n    def _child_has_unsaved_changes(self):\n        for child_obj in self.children_by_key.values():\n            if child_obj.has_unsaved_changes:\n                return True\n        return False\n\n    @property\n    def has_studio_override(self):\n        return self._has_studio_override or self._child_has_studio_override\n\n    @property\n    def _child_has_studio_override(self):\n        if self._override_state >= OverrideState.STUDIO:\n            for child_obj in self.children_by_key.values():\n                if child_obj.has_studio_override:\n                    return True\n        return False\n\n    @property\n    def has_project_override(self):\n        return self._has_project_override or self._child_has_project_override\n\n    @property\n    def _child_has_project_override(self):\n        if self._override_state >= OverrideState.PROJECT:\n            for child_obj in self.children_by_key.values():\n                if child_obj.has_project_override:\n                    return True\n        return False\n\n    def _settings_value(self):\n        if self.store_as_list:\n            output = []\n            for key, child_entity in self.children_by_key.items():\n                child_value = child_entity.settings_value()\n                output.append([key, child_value])\n            return output\n\n        output = {\n            key: child_entity.settings_value()\n            for key, child_entity in self.children_by_key.items()\n        }\n        output.update(self.metadata)\n        return output\n\n    def _prepare_value(self, value):\n        metadata = {}\n        if isinstance(value, dict):\n            for key in METADATA_KEYS:\n                if key in value:\n                    metadata[key] = value.pop(key)\n        return value, metadata\n\n    def update_default_value(self, value, log_invalid_types=True):\n        self._default_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"default\", log_invalid_types\n        )\n        has_default_value = value is not NOT_SET\n        if has_default_value:\n            for required_key in self.required_keys:\n                if required_key not in value:\n                    has_default_value = False\n                    break\n        self.has_default_value = has_default_value\n        value, metadata = self._prepare_value(value)\n        self._default_value = value\n        self._default_metadata = metadata\n\n    def update_studio_value(self, value, log_invalid_types=True):\n        self._studio_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"studio override\", log_invalid_types\n        )\n        value, metadata = self._prepare_value(value)\n        self._studio_override_value = value\n        self._studio_override_metadata = metadata\n        self.had_studio_override = value is not NOT_SET\n\n    def update_project_value(self, value, log_invalid_types=True):\n        self._project_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"project override\", log_invalid_types\n        )\n        value, metadata = self._prepare_value(value)\n        self._project_override_value = value\n        self._project_override_metadata = metadata\n        self.had_project_override = value is not NOT_SET\n\n    def _discard_changes(self, on_change_trigger):\n        if not self._can_discard_changes:\n            return\n\n        self.set_override_state(\n            self._override_state, self._ignore_missing_defaults\n        )\n        on_change_trigger.append(self.on_change)\n\n    def _add_to_studio_default(self, _on_change_trigger):\n        self._has_studio_override = True\n        self.on_change()\n\n    def _remove_from_studio_default(self, on_change_trigger):\n        if not self._can_remove_from_studio_default:\n            return\n\n        value = self._default_value\n        if value is NOT_SET:\n            value = self.value_on_not_set\n\n        new_value = copy.deepcopy(value)\n        self._ignore_child_changes = True\n\n        # Simulate `clear` method without triggering value change\n        for key in tuple(self.children_by_key.keys()):\n            self.children_by_key.pop(key)\n\n        metadata = self._get_metadata_for_state(OverrideState.DEFAULTS)\n        metadata_labels = metadata.get(M_DYNAMIC_KEY_LABEL) or {}\n        children_label_by_id = {}\n\n        # Create new children\n        for _key, _value in new_value.items():\n            new_key = self._convert_to_regex_valid_key(_key)\n            child_entity = self._add_key(new_key)\n            child_entity.update_default_value(_value)\n            label = metadata_labels.get(_key)\n            if label:\n                children_label_by_id[child_entity.id] = label\n\n            child_entity.set_override_state(\n                self._override_state, self._ignore_missing_defaults\n            )\n\n        self.children_label_by_id = children_label_by_id\n\n        self._ignore_child_changes = False\n\n        self._has_studio_override = False\n\n        on_change_trigger.append(self.on_change)\n\n    def _add_to_project_override(self, _on_change_trigger):\n        self._has_project_override = True\n        self.on_change()\n\n    def _remove_from_project_override(self, on_change_trigger):\n        if not self._can_remove_from_project_override:\n            return\n\n        log_invalid_types = True\n        if self._has_studio_override:\n            log_invalid_types = self._studio_log_invalid_types\n            value = self._studio_override_value\n        elif self.has_default_value:\n            log_invalid_types = self._default_log_invalid_types\n            value = self._default_value\n        else:\n            value = self.value_on_not_set\n\n        new_value = copy.deepcopy(value)\n\n        self._ignore_child_changes = True\n\n        # Simulate `clear` method without triggering value change\n        for key in tuple(self.children_by_key.keys()):\n            self.children_by_key.pop(key)\n\n        metadata = self._get_metadata_for_state(OverrideState.STUDIO)\n        metadata_labels = metadata.get(M_DYNAMIC_KEY_LABEL) or {}\n        children_label_by_id = {}\n\n        # Create new children\n        for _key, _value in new_value.items():\n            new_key = self._convert_to_regex_valid_key(_key)\n            child_entity = self._add_key(new_key)\n            child_entity.update_default_value(_value, log_invalid_types)\n            if self._has_studio_override:\n                child_entity.update_studio_value(_value, log_invalid_types)\n\n            label = metadata_labels.get(_key)\n            if label:\n                children_label_by_id[child_entity.id] = label\n\n            child_entity.set_override_state(\n                self._override_state, self._ignore_missing_defaults\n            )\n\n        self.children_label_by_id = children_label_by_id\n\n        self._ignore_child_changes = False\n\n        self._has_project_override = False\n\n        on_change_trigger.append(self.on_change)\n\n    def reset_callbacks(self):\n        super(DictMutableKeysEntity, self).reset_callbacks()\n        for child_entity in self.children_by_key.values():\n            child_entity.reset_callbacks()\n"
  },
  {
    "path": "openpype/settings/entities/enum_entity.py",
    "content": "import abc\nimport six\nimport copy\nfrom .input_entities import InputEntity\nfrom .exceptions import EntitySchemaError\nfrom .lib import NOT_SET, STRING_TYPE\n\n\nclass BaseEnumEntity(InputEntity):\n    def _item_initialization(self):\n        self.multiselection = True\n        self.value_on_not_set = None\n        self.enum_items = None\n        self.valid_keys = None\n        self.valid_value_types = None\n        self.placeholder = None\n\n    def schema_validations(self):\n        if not isinstance(self.enum_items, list):\n            raise EntitySchemaError(\n                self, \"Enum item must have defined `enum_items` as list.\"\n            )\n\n        enum_keys = set()\n        for item in self.enum_items:\n            key = tuple(item.keys())[0]\n            if key in enum_keys:\n                reason = 'Key \"{}\" is more than once in enum items.'.format(\n                    key\n                )\n                raise EntitySchemaError(self, reason)\n\n            enum_keys.add(key)\n\n            if not isinstance(key, STRING_TYPE):\n                reason = 'Key \"{}\" has invalid type {}, expected {}.'.format(\n                    key, type(key), STRING_TYPE\n                )\n                raise EntitySchemaError(self, reason)\n\n        super(BaseEnumEntity, self).schema_validations()\n\n    def _convert_to_valid_type(self, value):\n        if self.multiselection:\n            if isinstance(value, (set, tuple)):\n                return list(value)\n        elif isinstance(value, (int, float)):\n            return str(value)\n        return NOT_SET\n\n    def set(self, value):\n        new_value = self.convert_to_valid_type(value)\n        if self.multiselection:\n            check_values = new_value\n        else:\n            check_values = [new_value]\n\n        for item in check_values:\n            if item not in self.valid_keys:\n                raise ValueError(\n                    '{} Invalid value \"{}\". Expected one of: {}'.format(\n                        self.path, item, self.valid_keys\n                    )\n                )\n        self._current_value = new_value\n        self._on_value_change()\n\n\nclass EnumEntity(BaseEnumEntity):\n    schema_types = [\"enum\"]\n\n    def _item_initialization(self):\n        self.multiselection = self.schema_data.get(\"multiselection\", False)\n        self.enum_items = self.schema_data.get(\"enum_items\")\n        # Default is optional and non breaking attribute\n        enum_default = self.schema_data.get(\"default\")\n\n        all_keys = []\n        for item in self.enum_items or []:\n            key = tuple(item.keys())[0]\n            all_keys.append(key)\n\n        self.valid_keys = set(all_keys)\n\n        if self.multiselection:\n            self.valid_value_types = (list,)\n            value_on_not_set = []\n            if enum_default:\n                if not isinstance(enum_default, list):\n                    enum_default = [enum_default]\n\n                for item in enum_default:\n                    if item in all_keys:\n                        value_on_not_set.append(item)\n\n            self.value_on_not_set = value_on_not_set\n\n        else:\n            if isinstance(enum_default, list) and enum_default:\n                enum_default = enum_default[0]\n\n            if enum_default in self.valid_keys:\n                self.value_on_not_set = enum_default\n\n            else:\n                for key in all_keys:\n                    if self.value_on_not_set is NOT_SET:\n                        self.value_on_not_set = key\n                        break\n\n            self.valid_value_types = (STRING_TYPE,)\n\n        # GUI attribute\n        self.placeholder = self.schema_data.get(\"placeholder\")\n\n    def schema_validations(self):\n        if not self.enum_items and \"enum_items\" not in self.schema_data:\n            raise EntitySchemaError(\n                self, \"Enum item must have defined `enum_items`\"\n            )\n        super(EnumEntity, self).schema_validations()\n\n    def set_override_state(self, *args, **kwargs):\n        super(EnumEntity, self).set_override_state(*args, **kwargs)\n\n        # Make sure current value is valid\n        if self.multiselection:\n            new_value = []\n            for key in self._current_value:\n                if key in self.valid_keys:\n                    new_value.append(key)\n            self._current_value = new_value\n\n        elif self._current_value not in self.valid_keys:\n            self._current_value = self.value_on_not_set\n\n\nclass HostsEnumEntity(BaseEnumEntity):\n    \"\"\"Enumeration of host names.\n\n    Enum items are hardcoded in definition of the entity.\n\n    Hosts enum can have defined empty value as valid option which is\n    represented by empty string. Schema key to set this option is\n    `use_empty_value` (true/false). And to set label of empty value set\n    `empty_label` (string).\n\n    Enum can have single and multiselection.\n\n    NOTE:\n    Host name is not the same as application name. Host name defines\n    implementation instead of application name.\n    \"\"\"\n\n    schema_types = [\"hosts-enum\"]\n    all_host_names = [\n        \"max\",\n        \"aftereffects\",\n        \"blender\",\n        \"celaction\",\n        \"flame\",\n        \"fusion\",\n        \"harmony\",\n        \"hiero\",\n        \"houdini\",\n        \"maya\",\n        \"nuke\",\n        \"photoshop\",\n        \"resolve\",\n        \"tvpaint\",\n        \"unreal\",\n        \"standalonepublisher\",\n        \"substancepainter\",\n        \"traypublisher\",\n        \"webpublisher\",\n        \"equalizer\",\n    ]\n\n    def _item_initialization(self):\n        self.multiselection = self.schema_data.get(\"multiselection\", True)\n        use_empty_value = False\n        if not self.multiselection:\n            use_empty_value = self.schema_data.get(\n                \"use_empty_value\", use_empty_value\n            )\n        self.use_empty_value = use_empty_value\n\n        hosts_filter = self.schema_data.get(\"hosts_filter\") or []\n        self.hosts_filter = hosts_filter\n\n        custom_labels = self.schema_data.get(\"custom_labels\") or {}\n\n        host_names = copy.deepcopy(self.all_host_names)\n        if hosts_filter:\n            for host_name in tuple(host_names):\n                if host_name not in hosts_filter:\n                    host_names.remove(host_name)\n\n        if self.use_empty_value:\n            host_names.insert(0, \"\")\n            # Add default label for empty value if not available\n            if \"\" not in custom_labels:\n                custom_labels[\"\"] = \"< without host >\"\n\n        # These are hardcoded there is not list of available host in OpenPype\n        enum_items = []\n        valid_keys = set()\n        for key in host_names:\n            label = custom_labels.get(key, key)\n            valid_keys.add(key)\n            enum_items.append({key: label})\n\n        self.enum_items = enum_items\n        self.valid_keys = valid_keys\n\n        if self.multiselection:\n            self.valid_value_types = (list,)\n            self.value_on_not_set = []\n        else:\n            for key in valid_keys:\n                if self.value_on_not_set is NOT_SET:\n                    self.value_on_not_set = key\n                    break\n\n            self.valid_value_types = (STRING_TYPE,)\n\n        # GUI attribute\n        self.placeholder = self.schema_data.get(\"placeholder\")\n\n    def schema_validations(self):\n        if self.hosts_filter:\n            enum_len = len(self.enum_items)\n            if enum_len == 0 or (enum_len == 1 and self.use_empty_value):\n                joined_filters = \", \".join(\n                    ['\"{}\"'.format(item) for item in self.hosts_filter]\n                )\n                reason = (\n                    \"All host names were removed after applying\"\n                    \" host filters. {}\"\n                ).format(joined_filters)\n                raise EntitySchemaError(self, reason)\n\n            invalid_filters = set()\n            for item in self.hosts_filter:\n                if item not in self.all_host_names:\n                    invalid_filters.add(item)\n\n            if invalid_filters:\n                joined_filters = \", \".join(\n                    ['\"{}\"'.format(item) for item in self.hosts_filter]\n                )\n                expected_hosts = \", \".join(\n                    ['\"{}\"'.format(item) for item in self.all_host_names]\n                )\n                self.log.warning(\n                    (\n                        \"Host filters containt invalid host names:\"\n                        ' \"{}\" Expected values are {}'\n                    ).format(joined_filters, expected_hosts)\n                )\n\n        super(HostsEnumEntity, self).schema_validations()\n\n\nclass AppsEnumEntity(BaseEnumEntity):\n    \"\"\"Enum of applications for project anatomy attributes.\"\"\"\n\n    schema_types = [\"apps-enum\"]\n\n    def _item_initialization(self):\n        self.multiselection = True\n        self.value_on_not_set = []\n        self.enum_items = []\n        self.valid_keys = set()\n        self.valid_value_types = (list,)\n        self.placeholder = None\n\n    def _get_enum_values(self):\n        system_settings_entity = self.get_entity_from_path(\"system_settings\")\n\n        valid_keys = set()\n        enum_items_list = []\n        applications_entity = system_settings_entity[\"applications\"]\n        app_entities = {}\n        additional_app_names = set()\n        additional_apps_entity = None\n        for group_name, app_group in applications_entity.items():\n            if group_name != \"additional_apps\":\n                app_entities[group_name] = app_group\n                continue\n\n            additional_apps_entity = app_group\n            for _group_name, _group in app_group.items():\n                additional_app_names.add(_group_name)\n                app_entities[_group_name] = _group\n\n        for group_name, app_group in app_entities.items():\n            enabled_entity = app_group.get(\"enabled\")\n            if enabled_entity and not enabled_entity.value:\n                continue\n\n            if group_name in additional_app_names:\n                group_label = additional_apps_entity.get_key_label(group_name)\n                if not group_label:\n                    group_label = group_name\n            else:\n                group_label = app_group[\"label\"].value\n            variants_entity = app_group[\"variants\"]\n            for variant_name, variant_entity in variants_entity.items():\n                enabled_entity = variant_entity.get(\"enabled\")\n                if enabled_entity and not enabled_entity.value:\n                    continue\n\n                variant_label = None\n                if \"variant_label\" in variant_entity:\n                    variant_label = variant_entity[\"variant_label\"].value\n                elif hasattr(variants_entity, \"get_key_label\"):\n                    variant_label = variants_entity.get_key_label(variant_name)\n\n                if not variant_label:\n                    variant_label = variant_name\n\n                if group_label:\n                    full_label = \"{} {}\".format(group_label, variant_label)\n                else:\n                    full_label = variant_label\n\n                full_name = \"/\".join((group_name, variant_name))\n                enum_items_list.append((full_name, full_label))\n                valid_keys.add(full_name)\n\n        enum_items = []\n        for key, value in sorted(enum_items_list, key=lambda item: item[1]):\n            enum_items.append({key: value})\n        return enum_items, valid_keys\n\n    def set_override_state(self, *args, **kwargs):\n        super(AppsEnumEntity, self).set_override_state(*args, **kwargs)\n\n        self.enum_items, self.valid_keys = self._get_enum_values()\n        new_value = []\n        for key in self._current_value:\n            if key in self.valid_keys:\n                new_value.append(key)\n        self._current_value = new_value\n\n\nclass ToolsEnumEntity(BaseEnumEntity):\n    schema_types = [\"tools-enum\"]\n\n    def _item_initialization(self):\n        self.multiselection = True\n        self.value_on_not_set = []\n        self.enum_items = []\n        self.valid_keys = set()\n        self.valid_value_types = (list,)\n        self.placeholder = None\n\n    def _get_enum_values(self):\n        system_settings_entity = self.get_entity_from_path(\"system_settings\")\n\n        valid_keys = set()\n        enum_items_list = []\n        tool_groups_entity = system_settings_entity[\"tools\"][\"tool_groups\"]\n        for group_name, tool_group in tool_groups_entity.items():\n            # Try to get group label from entity\n            group_label = None\n            if hasattr(tool_groups_entity, \"get_key_label\"):\n                group_label = tool_groups_entity.get_key_label(group_name)\n\n            variants_entity = tool_group[\"variants\"]\n            for variant_name in variants_entity.keys():\n                # Prepare tool name (used as value)\n                tool_name = \"/\".join((group_name, variant_name))\n\n                # Try to get variant label from entity\n                variant_label = None\n                if hasattr(variants_entity, \"get_key_label\"):\n                    variant_label = variants_entity.get_key_label(variant_name)\n\n                # Tool label that will be shown\n                # - use tool name itself if labels are not filled\n                if group_label and variant_label:\n                    tool_label = \" \".join((group_label, variant_label))\n                else:\n                    tool_label = tool_name\n\n                enum_items_list.append((tool_name, tool_label))\n                valid_keys.add(tool_name)\n\n        enum_items = []\n        for key, value in sorted(enum_items_list, key=lambda item: item[1]):\n            enum_items.append({key: value})\n        return enum_items, valid_keys\n\n    def set_override_state(self, *args, **kwargs):\n        super(ToolsEnumEntity, self).set_override_state(*args, **kwargs)\n\n        self.enum_items, self.valid_keys = self._get_enum_values()\n        new_value = []\n        for key in self._current_value:\n            if key in self.valid_keys:\n                new_value.append(key)\n        self._current_value = new_value\n\n\nclass TaskTypeEnumEntity(BaseEnumEntity):\n    schema_types = [\"task-types-enum\"]\n\n    def _item_initialization(self):\n        self.multiselection = self.schema_data.get(\"multiselection\", True)\n        if self.multiselection:\n            self.valid_value_types = (list,)\n            self.value_on_not_set = []\n        else:\n            self.valid_value_types = (STRING_TYPE,)\n            self.value_on_not_set = \"\"\n\n        self.enum_items = []\n        self.valid_keys = set()\n        self.placeholder = None\n\n    def _get_enum_values(self):\n        anatomy_entity = self.get_entity_from_path(\n            \"project_settings/project_anatomy\"\n        )\n\n        valid_keys = set()\n        enum_items = []\n        for task_type in anatomy_entity[\"tasks\"].keys():\n            enum_items.append({task_type: task_type})\n            valid_keys.add(task_type)\n\n        return enum_items, valid_keys\n\n    def _convert_value_for_current_state(self, source_value):\n        if self.multiselection:\n            output = []\n            for key in source_value:\n                if key in self.valid_keys:\n                    output.append(key)\n            return output\n\n        if source_value not in self.valid_keys:\n            # Take first item from enum items\n            for item in self.enum_items:\n                for key in item.keys():\n                    source_value = key\n                break\n        return source_value\n\n    def set_override_state(self, *args, **kwargs):\n        super(TaskTypeEnumEntity, self).set_override_state(*args, **kwargs)\n\n        self.enum_items, self.valid_keys = self._get_enum_values()\n\n        if self.multiselection:\n            new_value = []\n            for key in self._current_value:\n                if key in self.valid_keys:\n                    new_value.append(key)\n\n            if self._current_value != new_value:\n                self.set(new_value)\n        else:\n            if not self.enum_items:\n                self.valid_keys.add(\"\")\n                self.enum_items.append({\"\": \"< Empty >\"})\n\n            for item in self.enum_items:\n                for key in item.keys():\n                    value_on_not_set = key\n                break\n\n            self.value_on_not_set = value_on_not_set\n            if (\n                self._current_value is NOT_SET\n                or self._current_value not in self.valid_keys\n            ):\n                self.set(value_on_not_set)\n\n\nclass DynamicEnumEntity(BaseEnumEntity):\n    schema_types = []\n\n    def _item_initialization(self):\n        self.multiselection = self.schema_data.get(\"multiselection\", True)\n\n        self.enum_items = []\n        self.valid_keys = set()\n\n        if self.multiselection:\n            self.valid_value_types = (list,)\n            self.value_on_not_set = []\n        else:\n            self.valid_value_types = (STRING_TYPE,)\n            self.value_on_not_set = \"\"\n\n        # GUI attribute\n        self.placeholder = self.schema_data.get(\"placeholder\")\n\n    def set_override_state(self, *args, **kwargs):\n        super(DynamicEnumEntity, self).set_override_state(*args, **kwargs)\n\n        self.enum_items, self.valid_keys = self._get_enum_values()\n        if self.multiselection:\n            new_value = []\n            for key in self._current_value:\n                if key in self.valid_keys:\n                    new_value.append(key)\n            self._current_value = new_value\n\n        else:\n            if not self.valid_keys:\n                self._current_value = \"\"\n\n            elif self._current_value not in self.valid_keys:\n                self._current_value = tuple(self.valid_keys)[0]\n\n    @abc.abstractmethod\n    def _get_enum_values(self):\n        pass\n\n\nclass DeadlineUrlEnumEntity(DynamicEnumEntity):\n    schema_types = [\"deadline_url-enum\"]\n\n    def _get_enum_values(self):\n        deadline_urls_entity = self.get_entity_from_path(\n            \"system_settings/modules/deadline/deadline_urls\"\n        )\n\n        valid_keys = set()\n        enum_items_list = []\n        for server_name, url_entity in deadline_urls_entity.items():\n            enum_items_list.append(\n                {server_name: \"{}: {}\".format(server_name, url_entity.value)}\n            )\n            valid_keys.add(server_name)\n        return enum_items_list, valid_keys\n\n\nclass RoyalRenderRootEnumEntity(DynamicEnumEntity):\n    schema_types = [\"rr_root-enum\"]\n\n    def _get_enum_values(self):\n        rr_root_entity = self.get_entity_from_path(\n            \"system_settings/modules/royalrender/rr_paths\"\n        )\n\n        valid_keys = set()\n        enum_items_list = []\n        for server_name, url_entity in rr_root_entity.items():\n            enum_items_list.append(\n                {server_name: \"{}: {}\".format(server_name, url_entity.value)}\n            )\n            valid_keys.add(server_name)\n        return enum_items_list, valid_keys\n\n\nclass ShotgridUrlEnumEntity(DynamicEnumEntity):\n    schema_types = [\"shotgrid_url-enum\"]\n\n    def _get_enum_values(self):\n        shotgrid_settings = self.get_entity_from_path(\n            \"system_settings/modules/shotgrid/shotgrid_settings\"\n        )\n\n        valid_keys = set()\n        enum_items_list = []\n        for server_name, settings in shotgrid_settings.items():\n            enum_items_list.append(\n                {\n                    server_name: \"{}: {}\".format(\n                        server_name, settings[\"shotgrid_url\"].value\n                    )\n                }\n            )\n            valid_keys.add(server_name)\n        return enum_items_list, valid_keys\n\n\nclass AnatomyTemplatesEnumEntity(BaseEnumEntity):\n    schema_types = [\"anatomy-templates-enum\"]\n\n    def _item_initialization(self):\n        self.multiselection = False\n\n        self.enum_items = []\n        self.valid_keys = set()\n\n        enum_default = self.schema_data.get(\"default\") or \"work\"\n\n        self.value_on_not_set = enum_default\n        self.valid_value_types = (STRING_TYPE,)\n\n        # GUI attribute\n        self.placeholder = self.schema_data.get(\"placeholder\")\n\n    def _get_enum_values(self):\n        templates_entity = self.get_entity_from_path(\n            \"project_anatomy/templates\"\n        )\n\n        valid_keys = set()\n        enum_items_list = []\n\n        others_entity = None\n        for key, entity in templates_entity.items():\n            # Skip defaults key\n            if key == \"defaults\":\n                continue\n\n            if key == \"others\":\n                others_entity = entity\n                continue\n\n            label = key\n            if hasattr(entity, \"label\"):\n                label = entity.label or label\n\n            enum_items_list.append({key: label})\n            valid_keys.add(key)\n\n        if others_entity is not None:\n            get_child_label_func = getattr(\n                others_entity, \"get_child_label\", None\n            )\n            for key, child_entity in others_entity.items():\n                label = key\n                if callable(get_child_label_func):\n                    label = get_child_label_func(child_entity) or label\n\n                enum_items_list.append({key: label})\n                valid_keys.add(key)\n\n        return enum_items_list, valid_keys\n\n    def set_override_state(self, *args, **kwargs):\n        super(AnatomyTemplatesEnumEntity, self).set_override_state(\n            *args, **kwargs\n        )\n\n        self.enum_items, self.valid_keys = self._get_enum_values()\n        if self._current_value not in self.valid_keys:\n            self._current_value = self.value_on_not_set\n"
  },
  {
    "path": "openpype/settings/entities/exceptions.py",
    "content": "from openpype.settings.constants import KEY_ALLOWED_SYMBOLS\n\n\nclass DefaultsNotDefined(Exception):\n    def __init__(self, obj):\n        msg = \"Default values for object are not set. {}\".format(obj.path)\n        super(DefaultsNotDefined, self).__init__(msg)\n\n\nclass StudioDefaultsNotDefined(Exception):\n    def __init__(self, obj):\n        msg = \"Studio default values for object are not set. {}\".format(\n            obj.path\n        )\n        super(StudioDefaultsNotDefined, self).__init__(msg)\n\n\nclass BaseInvalidValue(Exception):\n    def __init__(self, reason, path):\n        msg = \"Path \\\"{}\\\". {}\".format(path, reason)\n        self.msg = msg\n        super(BaseInvalidValue, self).__init__(msg)\n\n\nclass InvalidValueType(BaseInvalidValue):\n    def __init__(self, valid_types, invalid_type, path):\n        joined_types = \", \".join(\n            [str(valid_type) for valid_type in valid_types]\n        )\n        msg = \"Got invalid type \\\"{}\\\". Expected: {}\".format(\n            invalid_type, joined_types\n        )\n        super(InvalidValueType, self).__init__(msg, path)\n\n\nclass RequiredKeyModified(KeyError):\n    def __init__(self, entity_path, key):\n        msg = \"{} - Tried to modify required key \\\"{}\\\".\"\n        super(RequiredKeyModified, self).__init__(msg.format(entity_path, key))\n\n\nclass InvalidKeySymbols(KeyError):\n    def __init__(self, entity_path, key):\n        msg = \"{} - Invalid key \\\"{}\\\". Allowed symbols are {}\"\n        super(InvalidKeySymbols, self).__init__(\n            msg.format(entity_path, key, KEY_ALLOWED_SYMBOLS)\n        )\n\n\nclass SchemaError(Exception):\n    pass\n\n\nclass EntitySchemaError(SchemaError):\n    def __init__(self, entity, reason):\n        self.entity = entity\n        self.reason = reason\n        msg = \"{} {} - {}\".format(entity.__class__, entity.path, reason)\n        super(EntitySchemaError, self).__init__(msg)\n\n\nclass SchemeGroupHierarchyBug(EntitySchemaError):\n    def __init__(self, entity):\n        reason = (\n            \"Items with attribute \\\"is_group\\\" can't have another item with\"\n            \" \\\"is_group\\\" attribute as child.\"\n        )\n        super(SchemeGroupHierarchyBug, self).__init__(entity, reason)\n\n\nclass SchemaMissingFileInfo(SchemaError):\n    def __init__(self, invalid):\n        full_path_keys = []\n        for item in invalid:\n            full_path_keys.append(\"\\\"{}\\\"\".format(\"/\".join(item)))\n\n        msg = (\n            \"Schema has missing definition of output file (\\\"is_file\\\" key)\"\n            \" for keys. [{}]\"\n        ).format(\", \".join(full_path_keys))\n        super(SchemaMissingFileInfo, self).__init__(msg)\n\n\nclass SchemaDuplicatedKeys(SchemaError):\n    def __init__(self, entity, key):\n        msg = (\n            \"Schema item contain duplicated key \\\"{}\\\" in\"\n            \" one hierarchy level.\"\n        ).format(key)\n        super(SchemaDuplicatedKeys, self).__init__(entity, msg)\n\n\nclass SchemaDuplicatedEnvGroupKeys(SchemaError):\n    def __init__(self, invalid):\n        items = []\n        for key_path, keys in invalid.items():\n            joined_keys = \", \".join([\n                \"\\\"{}\\\"\".format(key) for key in keys\n            ])\n            items.append(\"\\\"{}\\\" ({})\".format(key_path, joined_keys))\n\n        msg = (\n            \"Schema items contain duplicated environment group keys. {}\"\n        ).format(\" || \".join(items))\n        super(SchemaDuplicatedEnvGroupKeys, self).__init__(msg)\n\n\nclass SchemaTemplateMissingKeys(SchemaError):\n    def __init__(self, missing_keys, required_keys, template_name=None):\n        self.missing_keys = missing_keys\n        self.required_keys = required_keys\n        if template_name:\n            msg = \"Schema template \\\"{}\\\" require more keys.\\n\".format(\n                template_name\n            )\n        else:\n            msg = \"\"\n        msg += \"Required keys: {}\\nMissing keys: {}\".format(\n            self.join_keys(required_keys),\n            self.join_keys(missing_keys)\n        )\n        super(SchemaTemplateMissingKeys, self).__init__(msg)\n\n    def join_keys(self, keys):\n        return \", \".join([\n            \"\\\"{}\\\"\".format(key) for key in keys\n        ])\n"
  },
  {
    "path": "openpype/settings/entities/input_entities.py",
    "content": "import re\nimport copy\nimport json\nfrom abc import abstractmethod\n\nfrom .base_entity import ItemEntity\nfrom .lib import (\n    NOT_SET,\n    STRING_TYPE,\n    OverrideState\n)\nfrom .exceptions import (\n    DefaultsNotDefined,\n    StudioDefaultsNotDefined,\n    EntitySchemaError\n)\n\nfrom openpype.settings.constants import METADATA_KEYS\n\n\nclass EndpointEntity(ItemEntity):\n    \"\"\"Entity that is a endpoint of settings value.\n\n    In most of cases endpoint entity does not have children entities and if has\n    then they are dynamic and can be removed/created. Is automatically set as\n    group if any parent is not, that is because of override metadata.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(EndpointEntity, self).__init__(*args, **kwargs)\n\n        if (\n            not (self.group_item is not None or self.is_group)\n            and not (self.is_dynamic_item or self.is_in_dynamic_item)\n        ):\n            self.is_group = True\n\n    def schema_validations(self):\n        \"\"\"Validation of entity schema and schema hierarchy.\"\"\"\n        # Default value when even defaults are not filled must be set\n        if self.value_on_not_set is NOT_SET:\n            reason = \"Attribute `value_on_not_set` is not filled. {}\".format(\n                self.__class__.__name__\n            )\n            raise EntitySchemaError(self, reason)\n\n        super(EndpointEntity, self).schema_validations()\n\n    def collect_dynamic_schema_entities(self, collector):\n        if self.is_dynamic_schema_node:\n            collector.add_entity(self)\n\n    @abstractmethod\n    def _settings_value(self):\n        pass\n\n    def collect_static_entities_by_path(self):\n        if self.is_dynamic_item or self.is_in_dynamic_item:\n            return {}\n        return {self.path: self}\n\n    def settings_value(self):\n        if self._override_state is OverrideState.NOT_DEFINED:\n            return NOT_SET\n\n        if self.is_group:\n            if self._override_state is OverrideState.STUDIO:\n                if not self.has_studio_override:\n                    return NOT_SET\n            elif self._override_state is OverrideState.PROJECT:\n                if not self.has_project_override:\n                    return NOT_SET\n        return self._settings_value()\n\n    def on_change(self):\n        for callback in self.on_change_callbacks:\n            callback()\n\n        if self.require_restart_on_change:\n            if self.require_restart:\n                self.root_item.add_item_require_restart(self)\n            else:\n                self.root_item.remove_item_require_restart(self)\n        self.parent.on_child_change(self)\n\n    @property\n    def require_restart(self):\n        return self.has_unsaved_changes\n\n    def update_default_value(self, value, log_invalid_types=True):\n        self._default_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"default\", log_invalid_types\n        )\n        self._default_value = value\n        self.has_default_value = value is not NOT_SET\n\n    def update_studio_value(self, value, log_invalid_types=True):\n        self._studio_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"studio override\", log_invalid_types\n        )\n        self._studio_override_value = value\n        self.had_studio_override = bool(value is not NOT_SET)\n\n    def update_project_value(self, value, log_invalid_types=True):\n        self._project_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"project override\", log_invalid_types\n        )\n        self._project_override_value = value\n        self.had_project_override = bool(value is not NOT_SET)\n\n\nclass InputEntity(EndpointEntity):\n    \"\"\"Endpoint entity without children.\"\"\"\n    def __init__(self, *args, **kwargs):\n        super(InputEntity, self).__init__(*args, **kwargs)\n        self._value_is_modified = False\n        self._current_value = NOT_SET\n\n    def __eq__(self, other):\n        if isinstance(other, ItemEntity):\n            return self.value == other.value\n        return self.value == other\n\n    def has_child_with_key(self, key):\n        return False\n\n    def get_child_path(self, child_obj):\n        raise TypeError(\"{} can't have children\".format(\n            self.__class__.__name__\n        ))\n\n    def schema_validations(self):\n        # Input entity must have file parent.\n        if (\n            not self.is_dynamic_schema_node\n            and not self.is_in_dynamic_schema_node\n            and self.file_item is None\n        ):\n            raise EntitySchemaError(self, \"Missing parent file entity.\")\n\n        super(InputEntity, self).schema_validations()\n\n    @property\n    def value(self):\n        \"\"\"Entity's value without metadata.\"\"\"\n        return self._current_value\n\n    @property\n    def require_restart(self):\n        return self._value_is_modified\n\n    def _settings_value(self):\n        return copy.deepcopy(self.value)\n\n    def set(self, value):\n        \"\"\"Change value.\"\"\"\n        self._current_value = self.convert_to_valid_type(value)\n        self._on_value_change()\n\n    def _on_value_change(self):\n        # Change has_project_override attr value\n        if self._override_state is OverrideState.PROJECT:\n            self._has_project_override = True\n\n        elif self._override_state is OverrideState.STUDIO:\n            self._has_studio_override = True\n\n        self.on_change()\n\n    def on_change(self):\n        \"\"\"Callback triggered on change.\n\n        There are cases when this method may be called from other entity.\n        \"\"\"\n        value_is_modified = None\n        if self._override_state is OverrideState.PROJECT:\n            # Only value change\n            if (\n                self._has_project_override\n                and self._project_override_value is not NOT_SET\n            ):\n                value_is_modified = (\n                    self._current_value != self._project_override_value\n                )\n\n        if (\n            self._override_state is OverrideState.STUDIO\n            or value_is_modified is None\n        ):\n            if (\n                self._has_studio_override\n                and self._studio_override_value is not NOT_SET\n            ):\n                value_is_modified = (\n                    self._current_value != self._studio_override_value\n                )\n\n        if value_is_modified is None:\n            value_is_modified = self._current_value != self._default_value\n\n        self._value_is_modified = value_is_modified\n\n        super(InputEntity, self).on_change()\n\n    def on_child_change(self, child_obj):\n        raise TypeError(\"Input entities do not contain children.\")\n\n    @property\n    def has_unsaved_changes(self):\n        if self._override_state is OverrideState.NOT_DEFINED:\n            return False\n\n        if self._value_is_modified:\n            return True\n\n        # These may be stored on value change\n        if self._override_state is OverrideState.DEFAULTS:\n            if not self.has_default_value:\n                return True\n\n        elif self._override_state is OverrideState.STUDIO:\n            if self._has_studio_override != self.had_studio_override:\n                return True\n\n            if not self._has_studio_override and not self.has_default_value:\n                return True\n\n        elif self._override_state is OverrideState.PROJECT:\n            if self._has_project_override != self.had_project_override:\n                return True\n\n            if (\n                not self._has_project_override\n                and not self._has_studio_override\n                and not self.has_default_value\n            ):\n                return True\n        return False\n\n    def set_override_state(self, state, ignore_missing_defaults):\n        # Trigger override state change of root if is not same\n        if self.root_item.override_state is not state:\n            self.root_item.set_override_state(state)\n            return\n\n        self._override_state = state\n        self._ignore_missing_defaults = ignore_missing_defaults\n        # Ignore if is dynamic item and use default in that case\n        if not self.is_dynamic_item and not self.is_in_dynamic_item:\n            if state > OverrideState.DEFAULTS:\n                if (\n                    not self.has_default_value\n                    and not ignore_missing_defaults\n                ):\n                    raise DefaultsNotDefined(self)\n\n            elif state > OverrideState.STUDIO:\n                if (\n                    not self.had_studio_override\n                    and not ignore_missing_defaults\n                ):\n                    raise StudioDefaultsNotDefined(self)\n\n        if state is OverrideState.STUDIO:\n            self._has_studio_override = (\n                self._studio_override_value is not NOT_SET\n            )\n\n        elif state is OverrideState.PROJECT:\n            self._has_project_override = (\n                self._project_override_value is not NOT_SET\n            )\n            self._has_studio_override = self.had_studio_override\n\n        value = NOT_SET\n        if state is OverrideState.PROJECT:\n            value = self._project_override_value\n\n        if value is NOT_SET and state >= OverrideState.STUDIO:\n            value = self._studio_override_value\n\n        if value is NOT_SET and state >= OverrideState.DEFAULTS:\n            value = self._default_value\n\n        if value is NOT_SET:\n            value = self.value_on_not_set\n            self.has_default_value = False\n        else:\n            self.has_default_value = True\n        self._value_is_modified = False\n\n        self._current_value = copy.deepcopy(value)\n\n    def _discard_changes(self, on_change_trigger=None):\n        if not self._can_discard_changes:\n            return\n\n        self._value_is_modified = False\n        if self._override_state >= OverrideState.PROJECT:\n            self._has_project_override = self.had_project_override\n            if self.had_project_override:\n                self._current_value = copy.deepcopy(\n                    self._project_override_value\n                )\n                on_change_trigger.append(self.on_change)\n                return\n\n        if self._override_state >= OverrideState.STUDIO:\n            self._has_studio_override = self.had_studio_override\n            if self.had_studio_override:\n                self._current_value = copy.deepcopy(\n                    self._studio_override_value\n                )\n                on_change_trigger.append(self.on_change)\n                return\n\n        if self._override_state >= OverrideState.DEFAULTS:\n            if self.has_default_value:\n                value = self._default_value\n            else:\n                value = self.value_on_not_set\n            self._current_value = copy.deepcopy(value)\n            on_change_trigger.append(self.on_change)\n            return\n\n        raise NotImplementedError(\"BUG: Unexcpected part of code.\")\n\n    def _add_to_studio_default(self, _on_change_trigger):\n        self._has_studio_override = True\n        self.on_change()\n\n    def _remove_from_studio_default(self, on_change_trigger):\n        if not self._can_remove_from_studio_default:\n            return\n\n        value = self._default_value\n        if value is NOT_SET:\n            value = self.value_on_not_set\n        self._current_value = copy.deepcopy(value)\n\n        self._has_studio_override = False\n        self._value_is_modified = False\n\n        on_change_trigger.append(self.on_change)\n\n    def _add_to_project_override(self, _on_change_trigger):\n        self._has_project_override = True\n        self.on_change()\n\n    def _remove_from_project_override(self, on_change_trigger):\n        if not self._can_remove_from_project_override:\n            return\n\n        self._has_project_override = False\n        if self._has_studio_override:\n            current_value = self._studio_override_value\n        elif self.has_default_value:\n            current_value = self._default_value\n        else:\n            current_value = self.value_on_not_set\n\n        self._current_value = copy.deepcopy(current_value)\n        on_change_trigger.append(self.on_change)\n\n\nclass NumberEntity(InputEntity):\n    schema_types = [\"number\"]\n    float_number_regex = re.compile(r\"^\\d+\\.\\d+$\")\n    int_number_regex = re.compile(r\"^\\d+$\")\n\n    def _item_initialization(self):\n        self.minimum = self.schema_data.get(\"minimum\", -99999)\n        self.maximum = self.schema_data.get(\"maximum\", 99999)\n        self.decimal = self.schema_data.get(\"decimal\", 0)\n\n        value_on_not_set = self.schema_data.get(\"default\", 0)\n        if self.decimal:\n            valid_value_types = (float, )\n            value_on_not_set = float(value_on_not_set)\n        else:\n            valid_value_types = (int, )\n            value_on_not_set = int(value_on_not_set)\n        self.valid_value_types = valid_value_types\n        self.value_on_not_set = value_on_not_set\n\n        # UI specific attributes\n        self.show_slider = self.schema_data.get(\"show_slider\", False)\n        steps = self.schema_data.get(\"steps\", None)\n        # Make sure that steps are not set to `0`\n        if steps == 0:\n            steps = None\n        self.steps = steps\n\n    def _convert_to_valid_type(self, value):\n        if isinstance(value, str):\n            new_value = None\n            if self.float_number_regex.match(value):\n                new_value = float(value)\n            elif self.int_number_regex.match(value):\n                new_value = int(value)\n\n            if new_value is not None:\n                self.log.info(\"{} - Converted str {} to {} {}\".format(\n                    self.path, value, type(new_value).__name__, new_value\n                ))\n                value = new_value\n\n        if self.decimal:\n            if isinstance(value, float):\n                return value\n            if isinstance(value, int):\n                return float(value)\n        else:\n            if isinstance(value, int):\n                return value\n            if isinstance(value, float):\n                new_value = int(value)\n                if new_value != value:\n                    self.log.info(\"{} - Converted float {} to int {}\".format(\n                        self.path, value, new_value\n                    ))\n                return new_value\n        return NOT_SET\n\n\nclass BoolEntity(InputEntity):\n    schema_types = [\"boolean\"]\n\n    def _item_initialization(self):\n        self.valid_value_types = (bool, )\n        value_on_not_set = self.convert_to_valid_type(\n            self.schema_data.get(\"default\", True)\n        )\n        self.value_on_not_set = value_on_not_set\n\n\nclass TextEntity(InputEntity):\n    schema_types = [\"text\"]\n\n    def _item_initialization(self):\n        self.valid_value_types = (STRING_TYPE, )\n        self.value_on_not_set = self.convert_to_valid_type(\n            self.schema_data.get(\"default\", \"\")\n        )\n\n        # GUI attributes\n        self.multiline = self.schema_data.get(\"multiline\", False)\n        self.placeholder_text = self.schema_data.get(\"placeholder\")\n        self.value_hints = self.schema_data.get(\"value_hints\") or []\n        self.minimum_lines_count = (\n            self.schema_data.get(\"minimum_lines_count\") or 0)\n\n    def schema_validations(self):\n        if self.multiline and self.value_hints:\n            reason = (\n                \"TextEntity entity can't use value hints\"\n                \" for multiline input (yet).\"\n            )\n            raise EntitySchemaError(self, reason)\n        super(TextEntity, self).schema_validations()\n\n    def _convert_to_valid_type(self, value):\n        # Allow numbers converted to string\n        if isinstance(value, (int, float)):\n            return str(value)\n        return NOT_SET\n\n\nclass PathInput(InputEntity):\n    schema_types = [\"path-input\"]\n\n    def _item_initialization(self):\n        self.valid_value_types = (STRING_TYPE, )\n        self.value_on_not_set = \"\"\n\n        # GUI attributes\n        self.placeholder_text = self.schema_data.get(\"placeholder\")\n\n    def set(self, value):\n        # Strip value\n        super(PathInput, self).set(value.strip())\n\n    def set_override_state(self, state, ignore_missing_defaults):\n        super(PathInput, self).set_override_state(\n            state, ignore_missing_defaults\n        )\n        # Strip current value\n        self._current_value = self._current_value.strip()\n\n\nclass RawJsonEntity(InputEntity):\n    schema_types = [\"raw-json\"]\n\n    def _item_initialization(self):\n        # Schema must define if valid value is dict or list\n        store_as_string = self.schema_data.get(\"store_as_string\", False)\n        is_list = self.schema_data.get(\"is_list\", False)\n        if is_list:\n            valid_value_types = (list, )\n            value_on_not_set = []\n        else:\n            valid_value_types = (dict, )\n            value_on_not_set = {}\n\n        self.store_as_string = store_as_string\n\n        self._is_list = is_list\n        self.valid_value_types = valid_value_types\n        self.value_on_not_set = value_on_not_set\n\n        self.default_metadata = {}\n        self.studio_override_metadata = {}\n        self.project_override_metadata = {}\n\n    @property\n    def is_list(self):\n        return self._is_list\n\n    @property\n    def is_dict(self):\n        return not self._is_list\n\n    def set(self, value):\n        new_value = self.convert_to_valid_type(value)\n\n        if isinstance(new_value, dict):\n            for key in METADATA_KEYS:\n                if key in new_value:\n                    new_value.pop(key)\n        self._current_value = new_value\n        self._on_value_change()\n\n    @property\n    def metadata(self):\n        return {}\n\n    @property\n    def has_unsaved_changes(self):\n        result = super(RawJsonEntity, self).has_unsaved_changes\n        if not result:\n            result = self.metadata != self._metadata_for_current_state()\n        return result\n\n    def _convert_to_valid_type(self, value):\n        if isinstance(value, STRING_TYPE):\n            try:\n                return json.loads(value)\n            except Exception:\n                pass\n        return super(RawJsonEntity, self)._convert_to_valid_type(value)\n\n    def _metadata_for_current_state(self):\n        if (\n            self._override_state is OverrideState.PROJECT\n            and self._project_override_value is not NOT_SET\n        ):\n            return self.project_override_metadata\n\n        if (\n            self._override_state >= OverrideState.STUDIO\n            and self._studio_override_value is not NOT_SET\n        ):\n            return self.studio_override_metadata\n\n        return self.default_metadata\n\n    def _settings_value(self):\n        value = super(RawJsonEntity, self)._settings_value()\n        if self.store_as_string:\n            return json.dumps(value)\n        return value\n\n    def _prepare_value(self, value):\n        metadata = {}\n        if isinstance(value, dict):\n            value = copy.deepcopy(value)\n            for key in METADATA_KEYS:\n                if key in value:\n                    metadata[key] = value.pop(key)\n        return value, metadata\n\n    def update_default_value(self, value, log_invalid_types=True):\n        value = self._check_update_value(value, \"default\", log_invalid_types)\n        self.has_default_value = value is not NOT_SET\n        value, metadata = self._prepare_value(value)\n        self._default_value = value\n        self.default_metadata = metadata\n\n    def update_studio_value(self, value, log_invalid_types=True):\n        value = self._check_update_value(\n            value, \"studio override\", log_invalid_types\n        )\n        self.had_studio_override = value is not NOT_SET\n        value, metadata = self._prepare_value(value)\n        self._studio_override_value = value\n        self.studio_override_metadata = metadata\n\n    def update_project_value(self, value, log_invalid_types=True):\n        value = self._check_update_value(\n            value, \"project override\", log_invalid_types\n        )\n        self.had_project_override = value is not NOT_SET\n        value, metadata = self._prepare_value(value)\n        self._project_override_value = value\n        self.project_override_metadata = metadata\n"
  },
  {
    "path": "openpype/settings/entities/item_entities.py",
    "content": "import re\n\nimport six\n\nfrom .lib import (\n    NOT_SET,\n    STRING_TYPE,\n    OverrideState\n)\nfrom .exceptions import (\n    DefaultsNotDefined,\n    StudioDefaultsNotDefined,\n    EntitySchemaError\n)\nfrom .base_entity import ItemEntity\n\n\nclass PathEntity(ItemEntity):\n    schema_types = [\"path\"]\n    platforms = (\"windows\", \"darwin\", \"linux\")\n    platform_labels_mapping = {\n        \"windows\": \"Windows\",\n        \"darwin\": \"MacOS\",\n        \"linux\": \"Linux\"\n    }\n    path_item_type_error = \"Got invalid path value type {}. Expected: {}\"\n    attribute_error_msg = (\n        \"'PathEntity' has no attribute '{}' if is not set as multiplatform\"\n    )\n\n    def __setitem__(self, *args, **kwargs):\n        return self.child_obj.__setitem__(*args, **kwargs)\n\n    def __getitem__(self, *args, **kwargs):\n        return self.child_obj.__getitem__(*args, **kwargs)\n\n    def __iter__(self):\n        return self.child_obj.__iter__()\n\n    def keys(self):\n        if not self.multiplatform:\n            raise AttributeError(self.attribute_error_msg.format(\"keys\"))\n        return self.child_obj.keys()\n\n    def values(self):\n        if not self.multiplatform:\n            raise AttributeError(self.attribute_error_msg.format(\"values\"))\n        return self.child_obj.values()\n\n    def items(self):\n        if not self.multiplatform:\n            raise AttributeError(self.attribute_error_msg.format(\"items\"))\n        return self.child_obj.items()\n\n    def has_child_with_key(self, key):\n        return self.child_obj.has_child_with_key(key)\n\n    def _item_initialization(self):\n        if self.group_item is None and not self.is_group:\n            self.is_group = True\n\n        self.multiplatform = self.schema_data.get(\"multiplatform\", False)\n        self.multipath = self.schema_data.get(\"multipath\", False)\n\n        placeholder_text = self.schema_data.get(\"placeholder\")\n\n        # Create child object\n        if not self.multiplatform and not self.multipath:\n            valid_value_types = (STRING_TYPE, )\n            item_schema = {\n                \"type\": \"path-input\",\n                \"key\": self.key,\n                \"placeholder\": placeholder_text\n            }\n\n        elif not self.multiplatform:\n            valid_value_types = (list, )\n            item_schema = {\n                \"type\": \"list\",\n                \"key\": self.key,\n                \"object_type\": {\n                    \"type\": \"path-input\",\n                    \"placeholder\": placeholder_text\n                }\n            }\n\n        else:\n            valid_value_types = (dict, )\n            item_schema = {\n                \"type\": \"dict\",\n                \"key\": self.key,\n                \"show_borders\": False,\n                \"children\": []\n            }\n            for platform_key in self.platforms:\n                platform_label = self.platform_labels_mapping[platform_key]\n                child_item = {\n                    \"key\": platform_key,\n                    \"label\": platform_label\n                }\n                if self.multipath:\n                    child_item[\"type\"] = \"list\"\n                    child_item[\"object_type\"] = {\n                        \"type\": \"path-input\",\n                        \"placeholder\": placeholder_text\n                    }\n                else:\n                    child_item[\"type\"] = \"path-input\"\n                    child_item[\"placeholder\"] = placeholder_text\n\n                item_schema[\"children\"].append(child_item)\n\n        self.valid_value_types = valid_value_types\n        self.child_obj = self.create_schema_object(item_schema, self)\n\n    def collect_static_entities_by_path(self):\n        return self.child_obj.collect_static_entities_by_path()\n\n    def get_child_path(self, _child_obj):\n        return self.path\n\n    def set(self, value):\n        self.child_obj.set(value)\n\n    def collect_dynamic_schema_entities(self, *args, **kwargs):\n        self.child_obj.collect_dynamic_schema_entities(*args, **kwargs)\n\n    def settings_value(self):\n        if self._override_state is OverrideState.NOT_DEFINED:\n            return NOT_SET\n\n        if self.is_group:\n            if self._override_state is OverrideState.STUDIO:\n                if not self.has_studio_override:\n                    return NOT_SET\n            elif self._override_state is OverrideState.PROJECT:\n                if not self.has_project_override:\n                    return NOT_SET\n\n        return self.child_obj.settings_value()\n\n    def on_change(self):\n        for callback in self.on_change_callbacks:\n            callback()\n        self.parent.on_child_change(self)\n\n    def on_child_change(self, _child_obj):\n        self.on_change()\n\n    @property\n    def has_unsaved_changes(self):\n        return self.child_obj.has_unsaved_changes\n\n    @property\n    def has_studio_override(self):\n        return self.child_obj.has_studio_override\n\n    @property\n    def has_project_override(self):\n        return self.child_obj.has_project_override\n\n    @property\n    def value(self):\n        return self.child_obj.value\n\n    def set_override_state(self, state, ignore_missing_defaults):\n        # Trigger override state change of root if is not same\n        if self.root_item.override_state is not state:\n            self.root_item.set_override_state(state)\n            return\n\n        self._override_state = state\n        self._ignore_missing_defaults = ignore_missing_defaults\n        self.child_obj.set_override_state(state, ignore_missing_defaults)\n\n    def update_default_value(self, value, log_invalid_types=True):\n        self._default_log_invalid_types = log_invalid_types\n        self.child_obj.update_default_value(value, log_invalid_types)\n\n    def update_project_value(self, value, log_invalid_types=True):\n        self._studio_log_invalid_types = log_invalid_types\n        self.child_obj.update_project_value(value, log_invalid_types)\n\n    def update_studio_value(self, value, log_invalid_types=True):\n        self._project_log_invalid_types = log_invalid_types\n        self.child_obj.update_studio_value(value, log_invalid_types)\n\n    def _discard_changes(self, *args, **kwargs):\n        self.child_obj.discard_changes(*args, **kwargs)\n\n    def _add_to_studio_default(self, *args, **kwargs):\n        self.child_obj.add_to_studio_default(*args, **kwargs)\n\n    def _remove_from_studio_default(self, *args, **kwargs):\n        self.child_obj.remove_from_studio_default(*args, **kwargs)\n\n    def _add_to_project_override(self, *args, **kwargs):\n        self.child_obj.add_to_project_override(*args, **kwargs)\n\n    def _remove_from_project_override(self, *args, **kwargs):\n        self.child_obj.remove_from_project_override(*args, **kwargs)\n\n    def reset_callbacks(self):\n        super(PathEntity, self).reset_callbacks()\n        self.child_obj.reset_callbacks()\n\n\nclass ListStrictEntity(ItemEntity):\n    schema_types = [\"list-strict\"]\n    _key_regex = re.compile(r\"[0-9]+\")\n\n    def __getitem__(self, idx):\n        if not isinstance(idx, int):\n            idx = int(idx)\n        return self.children[idx]\n\n    def __setitem__(self, idx, value):\n        if not isinstance(idx, int):\n            idx = int(idx)\n        self.children[idx].set(value)\n\n    def get(self, idx, default=None):\n        if not isinstance(idx, int):\n            idx = int(idx)\n\n        if idx < len(self.children):\n            return self.children[idx]\n        return default\n\n    def has_child_with_key(self, key):\n        if (\n            key\n            and isinstance(key, six.string_types)\n            and self._key_regex.match(key)\n        ):\n            key = int(key)\n\n        if not isinstance(key, int):\n            return False\n\n        return 0 <= key < len(self.children)\n\n    def _item_initialization(self):\n        self.valid_value_types = (list, )\n        self.require_key = True\n\n        self.initial_value = None\n\n        self._ignore_child_changes = False\n\n        # Child items\n        self.object_types = self.schema_data[\"object_types\"]\n\n        self.children = []\n        for children_schema in self.object_types:\n            child_obj = self.create_schema_object(children_schema, self, True)\n            self.children.append(child_obj)\n\n        # GUI attribute\n        self.is_horizontal = self.schema_data.get(\"horizontal\", True)\n        if self.group_item is None and not self.is_group:\n            self.is_group = True\n\n    def schema_validations(self):\n        # List entity must have file parent.\n        if (\n            not self.is_dynamic_schema_node\n            and not self.is_in_dynamic_schema_node\n            and not self.is_file\n            and self.file_item is None\n        ):\n            raise EntitySchemaError(\n                self, \"Missing file entity in hierarchy.\"\n            )\n\n        super(ListStrictEntity, self).schema_validations()\n\n    def collect_static_entities_by_path(self):\n        output = {}\n        if self.is_dynamic_item or self.is_in_dynamic_item:\n            return output\n\n        output[self.path] = self\n        for child_obj in self.children:\n            result = child_obj.collect_static_entities_by_path()\n            if result:\n                output.update(result)\n        return output\n\n    def get_child_path(self, child_obj):\n        result_idx = None\n        for idx, _child_obj in enumerate(self.children):\n            if _child_obj is child_obj:\n                result_idx = idx\n                break\n\n        if result_idx is None:\n            raise ValueError(\"Didn't find child {}\".format(child_obj))\n\n        return \"/\".join([self.path, str(result_idx)])\n\n    @property\n    def value(self):\n        output = []\n        for child_obj in self.children:\n            output.append(child_obj.value)\n        return output\n\n    def set(self, value):\n        new_value = self.convert_to_valid_type(value)\n        for idx, item in enumerate(new_value):\n            self.children[idx].set(item)\n\n    def collect_dynamic_schema_entities(self, collector):\n        if self.is_dynamic_schema_node:\n            collector.add_entity(self)\n\n    def settings_value(self):\n        if self._override_state is OverrideState.NOT_DEFINED:\n            return NOT_SET\n\n        if (\n            self.is_group\n            and self._override_state is not OverrideState.DEFAULTS\n        ):\n            if self._override_state is OverrideState.STUDIO:\n                if not self.has_studio_override:\n                    return NOT_SET\n            elif self._override_state is OverrideState.PROJECT:\n                if not self.has_project_override:\n                    return NOT_SET\n\n        output = []\n        for child_obj in self.children:\n            output.append(child_obj.settings_value())\n        return output\n\n    def on_change(self):\n        for callback in self.on_change_callbacks:\n            callback()\n        self.parent.on_child_change(self)\n\n    def on_child_change(self, _child_obj):\n        if self._ignore_child_changes:\n            return\n\n        if self._override_state is OverrideState.STUDIO:\n            self._has_studio_override = self._child_has_studio_override\n        elif self._override_state is OverrideState.PROJECT:\n            self._has_project_override = self._child_has_project_override\n\n        self.on_change()\n\n    @property\n    def has_unsaved_changes(self):\n        if self._override_state is OverrideState.NOT_DEFINED:\n            return False\n\n        if self._override_state is OverrideState.DEFAULTS:\n            if not self.has_default_value:\n                return True\n\n        elif self._override_state is OverrideState.STUDIO:\n            if self.had_studio_override != self._has_studio_override:\n                return True\n\n            if not self._has_studio_override and not self.has_default_value:\n                return True\n\n        elif self._override_state is OverrideState.PROJECT:\n            if self.had_project_override != self._has_project_override:\n                return True\n\n            if (\n                not self._has_project_override\n                and not self._has_studio_override\n                and not self.has_default_value\n            ):\n                return True\n\n        if self._child_has_unsaved_changes:\n            return True\n\n        if self.settings_value() != self.initial_value:\n            return True\n        return False\n\n    @property\n    def has_studio_override(self):\n        return self._has_studio_override or self._child_has_studio_override\n\n    @property\n    def has_project_override(self):\n        return self._has_project_override or self._child_has_project_override\n\n    @property\n    def _child_has_unsaved_changes(self):\n        for child_obj in self.children:\n            if child_obj.has_unsaved_changes:\n                return True\n        return False\n\n    @property\n    def _child_has_studio_override(self):\n        for child_obj in self.children:\n            if child_obj.has_studio_override:\n                return True\n        return False\n\n    @property\n    def _child_has_project_override(self):\n        for child_obj in self.children:\n            if child_obj.has_project_override:\n                return True\n        return False\n\n    def set_override_state(self, state, ignore_missing_defaults):\n        # Trigger override state change of root if is not same\n        if self.root_item.override_state is not state:\n            self.root_item.set_override_state(state)\n            return\n\n        self._override_state = state\n        self._ignore_missing_defaults = ignore_missing_defaults\n        # Ignore if is dynamic item and use default in that case\n        if not self.is_dynamic_item and not self.is_in_dynamic_item:\n            if state > OverrideState.DEFAULTS:\n                if (\n                    not self.has_default_value\n                    and not ignore_missing_defaults\n                ):\n                    raise DefaultsNotDefined(self)\n\n            elif state > OverrideState.STUDIO:\n                if (\n                    not self.had_studio_override\n                    and not ignore_missing_defaults\n                ):\n                    raise StudioDefaultsNotDefined(self)\n\n        for child_entity in self.children:\n            child_entity.set_override_state(state, ignore_missing_defaults)\n\n        self.initial_value = self.settings_value()\n\n    def _discard_changes(self, on_change_trigger):\n        for child_obj in self.children:\n            child_obj.discard_changes(on_change_trigger)\n\n    def _add_to_studio_default(self, _on_change_trigger):\n        self._has_studio_override = True\n        self.on_change()\n\n    def _remove_from_studio_default(self, on_change_trigger):\n        self._ignore_child_changes = True\n\n        for child_obj in self.children:\n            child_obj.remove_from_studio_default(on_change_trigger)\n\n        self._ignore_child_changes = False\n\n        self._has_studio_override = False\n\n    def _add_to_project_override(self, _on_change_trigger):\n        self._has_project_override = True\n        self.on_change()\n\n    def _remove_from_project_override(self, on_change_trigger):\n        self._ignore_child_changes = True\n\n        for child_obj in self.children:\n            child_obj.remove_from_project_override(on_change_trigger)\n\n        self._ignore_child_changes = False\n\n        self._has_project_override = False\n\n    def _check_update_value(self, value, value_type, log_invalid_types=True):\n        value = super(ListStrictEntity, self)._check_update_value(\n            value, value_type, log_invalid_types\n        )\n        if value is NOT_SET:\n            return value\n\n        child_len = len(self.children)\n        value_len = len(value)\n        if value_len == child_len:\n            return value\n\n        if log_invalid_types:\n            self.log.warning(\n                (\n                    \"{} Amount of strict list items in {} values is not same\"\n                    \" as expected. Expected {} items. Got {} items. {}\"\n                ).format(\n                    self.path, value_type,\n                    child_len, value_len, str(value)\n                )\n            )\n\n        if value_len < child_len:\n            # Fill missing values with NOT_SET\n            for _ in range(child_len - value_len):\n                value.append(NOT_SET)\n        else:\n            # Pop values that are overloaded\n            for _ in range(value_len - child_len):\n                value.pop(child_len)\n        return value\n\n    def update_default_value(self, value, log_invalid_types=True):\n        self._default_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"default\", log_invalid_types\n        )\n        self.has_default_value = value is not NOT_SET\n        if value is NOT_SET:\n            for child_obj in self.children:\n                child_obj.update_default_value(value, log_invalid_types)\n\n        else:\n            for idx, item_value in enumerate(value):\n                self.children[idx].update_default_value(\n                    item_value, log_invalid_types\n                )\n\n    def update_studio_value(self, value, log_invalid_types=True):\n        self._studio_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"studio override\", log_invalid_types\n        )\n        if value is NOT_SET:\n            for child_obj in self.children:\n                child_obj.update_studio_value(value, log_invalid_types)\n\n        else:\n            for idx, item_value in enumerate(value):\n                self.children[idx].update_studio_value(\n                    item_value, log_invalid_types\n                )\n\n    def update_project_value(self, value, log_invalid_types=True):\n        self._project_log_invalid_types = log_invalid_types\n        value = self._check_update_value(\n            value, \"project override\", log_invalid_types\n        )\n        if value is NOT_SET:\n            for child_obj in self.children:\n                child_obj.update_project_value(value, log_invalid_types)\n\n        else:\n            for idx, item_value in enumerate(value):\n                self.children[idx].update_project_value(\n                    item_value, log_invalid_types\n                )\n\n    def reset_callbacks(self):\n        super(ListStrictEntity, self).reset_callbacks()\n        for child_obj in self.children:\n            child_obj.reset_callbacks()\n"
  },
  {
    "path": "openpype/settings/entities/lib.py",
    "content": "import os\nimport re\nimport json\nimport copy\nimport inspect\nimport collections\nimport contextlib\n\nfrom .exceptions import (\n    SchemaTemplateMissingKeys,\n    SchemaDuplicatedEnvGroupKeys\n)\n\nfrom openpype.settings.constants import (\n    SYSTEM_SETTINGS_KEY,\n    PROJECT_SETTINGS_KEY,\n    SCHEMA_KEY_SYSTEM_SETTINGS,\n    SCHEMA_KEY_PROJECT_SETTINGS\n)\ntry:\n    STRING_TYPE = basestring\nexcept Exception:\n    STRING_TYPE = str\n\nWRAPPER_TYPES = [\"form\", \"collapsible-wrap\"]\nNOT_SET = type(\"NOT_SET\", (), {\"__bool__\": lambda obj: False})()\nOVERRIDE_VERSION = 1\n\nDEFAULT_VALUES_KEY = \"__default_values__\"\nTEMPLATE_METADATA_KEYS = (\n    DEFAULT_VALUES_KEY,\n)\n\nSCHEMA_EXTEND_TYPES = (\n    \"schema\", \"template\", \"schema_template\", \"dynamic_schema\"\n)\n\ntemplate_key_pattern = re.compile(r\"(\\{.*?[^{0]*\\})\")\n\n\nclass OverrideStateItem:\n    \"\"\"Object used as item for `OverrideState` enum.\n\n    Used object to be able use exact object comparison and value comparisons.\n    \"\"\"\n    values = set()\n\n    def __init__(self, value, name):\n        self.name = name\n        if value in self.__class__.values:\n            raise ValueError(\n                \"Implementation bug: Override State with same value as other.\"\n            )\n        self.__class__.values.add(value)\n        self.value = value\n\n    def __repr__(self):\n        return \"<object {}> {} {}\".format(\n            self.__class__.__name__, self.value, self.name\n        )\n\n    def __eq__(self, other):\n        \"\"\"Defines behavior for the equality operator, ==.\"\"\"\n        if isinstance(other, OverrideStateItem):\n            return self.value == other.value\n        return self.value == other\n\n    def __gt__(self, other):\n        \"\"\"Defines behavior for the greater-than operator, >.\"\"\"\n        if isinstance(other, OverrideStateItem):\n            return self.value > other.value\n        return self.value > other\n\n    def __lt__(self, other):\n        \"\"\"Defines behavior for the less-than operator, <.\"\"\"\n        if isinstance(other, OverrideStateItem):\n            return self.value < other.value\n        return self.value < other\n\n    def __le__(self, other):\n        \"\"\"Defines behavior for the less-than-or-equal-to operator, <=.\"\"\"\n        if isinstance(other, OverrideStateItem):\n            return self.value == other.value or self.value < other.value\n        return self.value == other or self.value < other\n\n    def __ge__(self, other):\n        \"\"\"Defines behavior for the greater-than-or-equal-to operator, >=.\"\"\"\n        if isinstance(other, OverrideStateItem):\n            return self.value == other.value or self.value > other.value\n        return self.value == other or self.value > other\n\n\nclass OverrideState:\n    \"\"\"Enumeration of override states.\n\n    Each state have unique value.\n\n    Currently has 4 states:\n    - NOT_DEFINED - Initial state will raise an error if want to access\n        anything in entity.\n    - DEFAULTS - Entity cares only about default values. It is not\n        possible to set higher state if any entity does not have filled\n        default value.\n    - STUDIO - First layer of overrides. Hold only studio overridden values\n        that are applied on top of defaults.\n    - PROJECT - Second layer of overrides. Hold only project overrides that are\n        applied on top of defaults and studio overrides.\n    \"\"\"\n    NOT_DEFINED = OverrideStateItem(-1, \"Not defined\")\n    DEFAULTS = OverrideStateItem(0, \"Defaults\")\n    STUDIO = OverrideStateItem(1, \"Studio overrides\")\n    PROJECT = OverrideStateItem(2, \"Project Overrides\")\n\n\nclass SchemasHub:\n    def __init__(self, schema_type, reset=True):\n        self._schema_type = schema_type\n\n        self._loaded_types = {}\n        self._gui_types = tuple()\n\n        self._crashed_on_load = {}\n        self._loaded_templates = {}\n        self._loaded_schemas = {}\n\n        # Attributes for modules settings\n        self._dynamic_schemas_defs_by_id = {}\n        self._dynamic_schemas_by_id = {}\n\n        # Store validating and validated dynamic template or schemas\n        self._validating_dynamic = set()\n        self._validated_dynamic = set()\n\n        # Trigger reset\n        if reset:\n            self.reset()\n\n    @property\n    def schema_type(self):\n        return self._schema_type\n\n    def reset(self):\n        self._load_modules_settings_defs()\n        self._load_types()\n        self._load_schemas()\n\n    def _load_modules_settings_defs(self):\n        from openpype.modules import get_module_settings_defs\n\n        module_settings_defs = get_module_settings_defs()\n        for module_settings_def_cls in module_settings_defs:\n            module_settings_def = module_settings_def_cls()\n            def_id = module_settings_def.id\n            self._dynamic_schemas_defs_by_id[def_id] = module_settings_def\n\n    @property\n    def gui_types(self):\n        return self._gui_types\n\n    def resolve_dynamic_schema(self, dynamic_key):\n        output = []\n        for def_id, def_keys in self._dynamic_schemas_by_id.items():\n            if dynamic_key in def_keys:\n                def_schema = def_keys[dynamic_key]\n                if not def_schema:\n                    continue\n\n                if isinstance(def_schema, dict):\n                    def_schema = [def_schema]\n\n                all_def_schema = []\n                for item in def_schema:\n                    items = self.resolve_schema_data(item)\n                    for _item in items:\n                        _item[\"_dynamic_schema_id\"] = def_id\n                    all_def_schema.extend(items)\n                output.extend(all_def_schema)\n        return output\n\n    def get_template_name(self, item_def, default=None):\n        \"\"\"Get template name from passed item definition.\n\n        Args:\n            item_def(dict): Definition of item with \"type\".\n            default(object): Default return value.\n        \"\"\"\n        output = default\n        if not item_def or not isinstance(item_def, dict):\n            return output\n\n        item_type = item_def.get(\"type\")\n        if item_type in (\"template\", \"schema_template\"):\n            output = item_def[\"name\"]\n        return output\n\n    def is_dynamic_template_validating(self, template_name):\n        \"\"\"Is template validating using different entity.\n\n        Returns:\n            bool: Is template validating.\n        \"\"\"\n        if template_name in self._validating_dynamic:\n            return True\n        return False\n\n    def is_dynamic_template_validated(self, template_name):\n        \"\"\"Is template already validated.\n\n        Returns:\n            bool: Is template validated.\n        \"\"\"\n\n        if template_name in self._validated_dynamic:\n            return True\n        return False\n\n    @contextlib.contextmanager\n    def validating_dynamic(self, template_name):\n        \"\"\"Template name is validating and validated.\n\n        Context manager that cares about storing template name validations of\n        template.\n\n        This is to avoid infinite loop of dynamic children validation.\n        \"\"\"\n        self._validating_dynamic.add(template_name)\n        try:\n            yield\n            self._validated_dynamic.add(template_name)\n\n        finally:\n            self._validating_dynamic.remove(template_name)\n\n    def get_schema(self, schema_name):\n        \"\"\"Get schema definition data by it's name.\n\n        Returns:\n            dict: Copy of schema loaded from json files.\n\n        Raises:\n            KeyError: When schema name is stored in loaded templates or json\n                file was not possible to parse or when schema name was not\n                found.\n        \"\"\"\n        if schema_name not in self._loaded_schemas:\n            if schema_name in self._loaded_templates:\n                raise KeyError((\n                    \"Template \\\"{}\\\" is used as `schema`\"\n                ).format(schema_name))\n\n            elif schema_name in self._crashed_on_load:\n                crashed_item = self._crashed_on_load[schema_name]\n                raise KeyError(\n                    \"Unable to parse schema file \\\"{}\\\". {}\".format(\n                        crashed_item[\"filepath\"], crashed_item[\"message\"]\n                    )\n                )\n\n            raise KeyError(\n                \"Schema \\\"{}\\\" was not found\".format(schema_name)\n            )\n        return copy.deepcopy(self._loaded_schemas[schema_name])\n\n    def get_template(self, template_name):\n        \"\"\"Get template definition data by it's name.\n\n        Returns:\n            list: Copy of template items loaded from json files.\n\n        Raises:\n            KeyError: When template name is stored in loaded schemas or json\n                file was not possible to parse or when template name was not\n                found.\n        \"\"\"\n        if template_name not in self._loaded_templates:\n            if template_name in self._loaded_schemas:\n                raise KeyError((\n                    \"Schema \\\"{}\\\" is used as `template`\"\n                ).format(template_name))\n\n            elif template_name in self._crashed_on_load:\n                crashed_item = self._crashed_on_load[template_name]\n                raise KeyError(\n                    \"Unable to parse template file \\\"{}\\\". {}\".format(\n                        crashed_item[\"filepath\"], crashed_item[\"message\"]\n                    )\n                )\n\n            raise KeyError(\n                \"Template \\\"{}\\\" was not found\".format(template_name)\n            )\n        return copy.deepcopy(self._loaded_templates[template_name])\n\n    def resolve_schema_data(self, schema_data):\n        \"\"\"Resolve single item schema data as few types can be expanded.\n\n        This is mainly for 'schema' and 'template' types. Type 'schema' does\n        not have entity representation and 'template' may contain more than one\n        output schemas.\n\n        In other cases is retuned passed schema item in list.\n\n        Goal is to have schema and template resolving at one place.\n\n        Returns:\n            list: Resolved schema data.\n        \"\"\"\n        schema_type = schema_data[\"type\"]\n        if schema_type not in SCHEMA_EXTEND_TYPES:\n            return [schema_data]\n\n        if schema_type == \"schema\":\n            return self.resolve_schema_data(\n                self.get_schema(schema_data[\"name\"])\n            )\n\n        if schema_type == \"dynamic_schema\":\n            return self.resolve_dynamic_schema(schema_data[\"name\"])\n\n        template_name = schema_data[\"name\"]\n        template_def = self.get_template(template_name)\n\n        filled_template = self._fill_template(\n            schema_data, template_def\n        )\n        new_template_def = []\n        for item in filled_template:\n            new_template_def.extend(self.resolve_schema_data(item))\n        return new_template_def\n\n    def create_schema_object(self, schema_data, *args, **kwargs):\n        \"\"\"Create entity for passed schema data.\n\n        Args:\n            schema_data(dict): Schema definition of settings entity.\n\n        Returns:\n            ItemEntity: Created entity for passed schema data item.\n\n        Raises:\n            ValueError: When 'schema', 'template' or any of wrapper types are\n                passed.\n            KeyError: When type of passed schema is not known.\n        \"\"\"\n        schema_type = schema_data[\"type\"]\n        if schema_type in (\"schema\", \"template\", \"schema_template\"):\n            raise ValueError(\n                \"Got unresolved schema data of type \\\"{}\\\"\".format(schema_type)\n            )\n\n        if schema_type in WRAPPER_TYPES:\n            raise ValueError((\n                \"Function `create_schema_object` can't create entities\"\n                \" of any wrapper type. Got type: \\\"{}\\\"\"\n            ).format(schema_type))\n\n        klass = self._loaded_types.get(schema_type)\n        if not klass:\n            raise KeyError(\"Unknown type \\\"{}\\\"\".format(schema_type))\n\n        return klass(schema_data, *args, **kwargs)\n\n    def _load_types(self):\n        \"\"\"Prepare entity types for cretion of their objects.\n\n        Currently all classes in `openpype.settings.entities` that inherited\n        from `BaseEntity` are stored as loaded types. GUI types are stored to\n        separated attribute to not mess up api access of entities.\n\n        TODOs:\n            Add more dynamic way how to add custom types from anywhere and\n            better handling of abstract classes. Skipping them is dangerous.\n        \"\"\"\n\n        from openpype.settings import entities\n\n        # Define known abstract classes\n        known_abstract_classes = (\n            entities.BaseEntity,\n            entities.BaseItemEntity,\n            entities.ItemEntity,\n            entities.EndpointEntity,\n            entities.InputEntity,\n            entities.BaseEnumEntity\n        )\n\n        self._loaded_types = {}\n        _gui_types = []\n        for attr in dir(entities):\n            item = getattr(entities, attr)\n            # Filter classes\n            if not inspect.isclass(item):\n                continue\n\n            # Skip classes that do not inherit from BaseEntity\n            if not issubclass(item, entities.BaseEntity):\n                continue\n\n            # Skip class that is abstract by design\n            if item in known_abstract_classes:\n                continue\n\n            if inspect.isabstract(item):\n                # Create an object to get crash and get traceback\n                item()\n\n            # Backwards compatibility\n            # Single entity may have multiple schema types\n            for schema_type in item.schema_types:\n                self._loaded_types[schema_type] = item\n\n            if item.gui_type:\n                _gui_types.append(item)\n        self._gui_types = tuple(_gui_types)\n\n    def _load_schemas(self):\n        \"\"\"Load schema definitions from json files.\"\"\"\n\n        # Refresh all affecting variables\n        self._crashed_on_load = {}\n        self._loaded_templates = {}\n        self._loaded_schemas = {}\n        self._dynamic_schemas_by_id = {}\n\n        dirpath = os.path.join(\n            os.path.dirname(os.path.abspath(__file__)),\n            \"schemas\",\n            self.schema_type\n        )\n        loaded_schemas = {}\n        loaded_templates = {}\n        dynamic_schemas_by_id = {}\n        for root, _, filenames in os.walk(dirpath):\n            for filename in filenames:\n                basename, ext = os.path.splitext(filename)\n                if ext != \".json\":\n                    continue\n\n                filepath = os.path.join(root, filename)\n                with open(filepath, \"r\") as json_stream:\n                    try:\n                        schema_data = json.load(json_stream)\n                    except Exception as exc:\n                        msg = str(exc)\n                        print(\"Unable to parse JSON file {}\\n{}\".format(\n                            filepath, msg\n                        ))\n                        self._crashed_on_load[basename] = {\n                            \"filepath\": filepath,\n                            \"message\": msg\n                        }\n                        continue\n\n                if basename in self._crashed_on_load:\n                    crashed_item = self._crashed_on_load[basename]\n                    raise KeyError((\n                        \"Duplicated filename \\\"{}\\\".\"\n                        \" One of them crashed on load \\\"{}\\\" {}\"\n                    ).format(\n                        filename,\n                        crashed_item[\"filepath\"],\n                        crashed_item[\"message\"]\n                    ))\n\n                if isinstance(schema_data, list):\n                    if basename in loaded_templates:\n                        raise KeyError(\n                            \"Duplicated template filename \\\"{}\\\"\".format(\n                                filename\n                            )\n                        )\n                    loaded_templates[basename] = schema_data\n                else:\n                    if basename in loaded_schemas:\n                        raise KeyError(\n                            \"Duplicated schema filename \\\"{}\\\"\".format(\n                                filename\n                            )\n                        )\n                    loaded_schemas[basename] = schema_data\n\n        defs_iter = self._dynamic_schemas_defs_by_id.items()\n        for def_id, module_settings_def in defs_iter:\n            dynamic_schemas_by_id[def_id] = (\n                module_settings_def.get_dynamic_schemas(self.schema_type)\n            )\n            module_schemas = module_settings_def.get_settings_schemas(\n                self.schema_type\n            )\n            for key, schema_data in module_schemas.items():\n                if isinstance(schema_data, list):\n                    if key in loaded_templates:\n                        raise KeyError(\n                            \"Duplicated template key \\\"{}\\\"\".format(key)\n                        )\n                    loaded_templates[key] = schema_data\n                else:\n                    if key in loaded_schemas:\n                        raise KeyError(\n                            \"Duplicated schema key \\\"{}\\\"\".format(key)\n                        )\n                    loaded_schemas[key] = schema_data\n\n        self._loaded_templates = loaded_templates\n        self._loaded_schemas = loaded_schemas\n        self._dynamic_schemas_by_id = dynamic_schemas_by_id\n\n    def get_dynamic_modules_settings_defs(self, schema_def_id):\n        return self._dynamic_schemas_defs_by_id.get(schema_def_id)\n\n    def _fill_template(self, child_data, template_def):\n        \"\"\"Fill template based on schema definition and template definition.\n\n        Based on `child_data` is `template_def` modified and result is\n        returned.\n\n        Template definition may have defined data to fill which\n        should be filled with data from child data.\n\n        Child data may contain more than one output definition of an template.\n\n        Child data can define paths to skip. Path is full path of an item\n        which won't be returned.\n\n        TODO:\n        Be able to handle wrapper items here.\n\n        Args:\n            child_data(dict): Schema data of template item.\n            template_def(dict): Template definition that will be filled with\n                child_data.\n\n        Returns:\n            list: Resolved template always returns list of schemas.\n        \"\"\"\n        template_name = child_data[\"name\"]\n\n        # Default value must be dictionary (NOT list)\n        # - empty list would not add any item if `template_data` are not filled\n        template_data = child_data.get(\"template_data\") or {}\n        if isinstance(template_data, dict):\n            template_data = [template_data]\n\n        skip_paths = child_data.get(\"skip_paths\") or []\n        if isinstance(skip_paths, STRING_TYPE):\n            skip_paths = [skip_paths]\n\n        output = []\n        for single_template_data in template_data:\n            try:\n                output.extend(self._fill_template_data(\n                    template_def, single_template_data, skip_paths\n                ))\n\n            except SchemaTemplateMissingKeys as exc:\n                raise SchemaTemplateMissingKeys(\n                    exc.missing_keys, exc.required_keys, template_name\n                )\n        return output\n\n    def _fill_template_data(\n        self,\n        template,\n        template_data,\n        skip_paths,\n        required_keys=None,\n        missing_keys=None\n    ):\n        \"\"\"Fill template values with data from schema data.\n\n        Template has more abilities than schemas. It is expected that template\n        will be used at multiple places (but may not). Schema represents\n        exactly one entity and it's children but template may represent more\n        entities.\n\n        Template can have \"keys to fill\" from their definition. Some key may be\n        required and some may be optional because template has their default\n        values defined.\n\n        Template also have ability to \"skip paths\" which means to skip entities\n        from it's content. A template can be used across multiple places with\n        different requirements.\n\n        Raises:\n            SchemaTemplateMissingKeys: When fill data do not contain all\n                required keys for template.\n        \"\"\"\n        first = False\n        if required_keys is None:\n            first = True\n\n            if \"skip_paths\" in template_data:\n                skip_paths = template_data[\"skip_paths\"]\n                if not isinstance(skip_paths, list):\n                    skip_paths = [skip_paths]\n\n            # Cleanup skip paths (skip empty values)\n            skip_paths = [path for path in skip_paths if path]\n\n            required_keys = set()\n            missing_keys = set()\n\n            # Copy template data as content may change\n            template = copy.deepcopy(template)\n\n            # Get metadata item from template\n            metadata_item = self._pop_metadata_item(template)\n\n            # Check for default values for template data\n            default_values = metadata_item.get(DEFAULT_VALUES_KEY) or {}\n\n            for key, value in default_values.items():\n                if key not in template_data:\n                    template_data[key] = value\n\n        if not template:\n            output = template\n\n        elif isinstance(template, list):\n            # Store paths by first part if path\n            # - None value says that whole key should be skipped\n            skip_paths_by_first_key = {}\n            for path in skip_paths:\n                parts = path.split(\"/\")\n                key = parts.pop(0)\n                if key not in skip_paths_by_first_key:\n                    skip_paths_by_first_key[key] = []\n\n                value = \"/\".join(parts)\n                skip_paths_by_first_key[key].append(value or None)\n\n            output = []\n            for item in template:\n                # Get skip paths for children item\n                _skip_paths = []\n                if not isinstance(item, dict):\n                    pass\n\n                elif item.get(\"type\") in WRAPPER_TYPES:\n                    _skip_paths = copy.deepcopy(skip_paths)\n\n                elif skip_paths_by_first_key:\n                    # Check if this item should be skipped\n                    key = item.get(\"key\")\n                    if key and key in skip_paths_by_first_key:\n                        _skip_paths = skip_paths_by_first_key[key]\n                        # Skip whole item if None is in skip paths value\n                        if None in _skip_paths:\n                            continue\n\n                output_item = self._fill_template_data(\n                    item,\n                    template_data,\n                    _skip_paths,\n                    required_keys,\n                    missing_keys\n                )\n                if output_item:\n                    output.append(output_item)\n\n        elif isinstance(template, dict):\n            output = {}\n            for key, value in template.items():\n                output[key] = self._fill_template_data(\n                    value,\n                    template_data,\n                    skip_paths,\n                    required_keys,\n                    missing_keys\n                )\n            if (\n                output.get(\"type\") in WRAPPER_TYPES\n                and not output.get(\"children\")\n            ):\n                return {}\n\n        elif isinstance(template, STRING_TYPE):\n            # TODO find much better way how to handle filling template data\n            template = (\n                template\n                .replace(\"{{\", \"__dbcb__\")\n                .replace(\"}}\", \"__decb__\")\n            )\n            full_replacement = False\n            for replacement_string in template_key_pattern.findall(template):\n                key = str(replacement_string[1:-1])\n                required_keys.add(key)\n                if key not in template_data:\n                    missing_keys.add(key)\n                    continue\n\n                value = template_data[key]\n                if replacement_string == template:\n                    # Replace the value with value from templates data\n                    # - with this is possible to set value with different type\n                    template = value\n                    full_replacement = True\n                else:\n                    # Only replace the key in string\n                    template = template.replace(replacement_string, value)\n\n            if not full_replacement:\n                output = (\n                    template\n                    .replace(\"__dbcb__\", \"{\")\n                    .replace(\"__decb__\", \"}\")\n                )\n            else:\n                output = template\n\n        else:\n            output = template\n\n        if first and missing_keys:\n            raise SchemaTemplateMissingKeys(missing_keys, required_keys)\n\n        return output\n\n    def _pop_metadata_item(self, template_def):\n        \"\"\"Pop template metadata from template definition.\n\n        Template metadata may define default values if are not passed from\n        schema data.\n        \"\"\"\n\n        found_idx = None\n        for idx, item in enumerate(template_def):\n            if not isinstance(item, dict):\n                continue\n\n            for key in TEMPLATE_METADATA_KEYS:\n                if key in item:\n                    found_idx = idx\n                    break\n\n            if found_idx is not None:\n                break\n\n        metadata_item = {}\n        if found_idx is not None:\n            metadata_item = template_def.pop(found_idx)\n        return metadata_item\n\n\nclass DynamicSchemaValueCollector:\n    # Map schema hub type to store keys\n    schema_hub_type_map = {\n        SCHEMA_KEY_SYSTEM_SETTINGS: SYSTEM_SETTINGS_KEY,\n        SCHEMA_KEY_PROJECT_SETTINGS: PROJECT_SETTINGS_KEY\n    }\n\n    def __init__(self, schema_hub):\n        self._schema_hub = schema_hub\n        self._dynamic_entities = []\n\n    def add_entity(self, entity):\n        self._dynamic_entities.append(entity)\n\n    def create_hierarchy(self):\n        output = collections.defaultdict(dict)\n        for entity in self._dynamic_entities:\n            output[entity.dynamic_schema_id][entity.path] = (\n                entity.settings_value()\n            )\n        return output\n\n    def save_values(self):\n        hierarchy = self.create_hierarchy()\n\n        for schema_def_id, schema_def_value in hierarchy.items():\n            schema_def = self._schema_hub.get_dynamic_modules_settings_defs(\n                schema_def_id\n            )\n            top_key = self.schema_hub_type_map.get(\n                self._schema_hub.schema_type\n            )\n            schema_def.save_defaults(top_key, schema_def_value)\n"
  },
  {
    "path": "openpype/settings/entities/list_entity.py",
    "content": "import copy\nimport six\nimport re\nfrom . import (\n    BaseEntity,\n    EndpointEntity\n)\nfrom .lib import (\n    NOT_SET,\n    OverrideState\n)\nfrom .exceptions import (\n    DefaultsNotDefined,\n    StudioDefaultsNotDefined,\n    EntitySchemaError\n)\n\n\nclass ListEntity(EndpointEntity):\n    schema_types = [\"list\"]\n    _default_label_wrap = {\n        \"use_label_wrap\": False,\n        \"collapsible\": True,\n        \"collapsed\": False\n    }\n    _key_regex = re.compile(r\"[0-9]+\")\n\n    def __iter__(self):\n        for item in self.children:\n            yield item\n\n    def __bool__(self):\n        \"\"\"Returns true because len may return 0.\"\"\"\n        return True\n\n    def __len__(self):\n        return len(self.children)\n\n    def __contains__(self, item):\n        if isinstance(item, BaseEntity):\n            for child_entity in self.children:\n                if child_entity.id == item.id:\n                    return True\n            return False\n\n        for _item in self.value:\n            if item == _item:\n                return True\n        return False\n\n    def __getitem__(self, idx):\n        if not isinstance(idx, int):\n            idx = int(idx)\n        return self.children[idx]\n\n    def __setitem__(self, idx, value):\n        if not isinstance(idx, int):\n            idx = int(idx)\n        self.children[idx].set(value)\n\n    def get(self, idx, default=None):\n        if not isinstance(idx, int):\n            idx = int(idx)\n\n        if idx < len(self.children):\n            return self.children[idx]\n        return default\n\n    def index(self, item):\n        if isinstance(item, BaseEntity):\n            for idx, child_entity in enumerate(self.children):\n                if child_entity.id == item.id:\n                    return idx\n        else:\n            for idx, _item in enumerate(self.value):\n                if item == _item:\n                    return idx\n        raise ValueError(\n            \"{} is not in {}\".format(item, self.__class__.__name__)\n        )\n\n    def append(self, item):\n        child_obj = self.add_new_item(trigger_change=False)\n        child_obj.set(item)\n        self.on_child_change(child_obj)\n\n    def extend(self, items):\n        for item in items:\n            self.append(item)\n\n    def clear(self):\n        if not self.children:\n            return\n\n        first_item = self.children.pop(0)\n        while self.children:\n            self.children.pop(0)\n        self.on_child_change(first_item)\n\n    def pop(self, idx):\n        item = self.children.pop(idx)\n        self.on_child_change(item)\n        return item\n\n    def remove(self, item):\n        try:\n            self.pop(self.index(item))\n        except ValueError:\n            raise ValueError(\"ListEntity.remove(x): x not in ListEntity\")\n\n    def insert(self, idx, item):\n        child_obj = self.add_new_item(idx, trigger_change=False)\n        child_obj.set(item)\n        self.on_child_change(child_obj)\n\n    def _add_new_item(self, idx=None):\n        child_obj = self.create_schema_object(self.item_schema, self, True)\n        if idx is None:\n            self.children.append(child_obj)\n        else:\n            self.children.insert(idx, child_obj)\n        return child_obj\n\n    def add_new_item(self, idx=None, trigger_change=True):\n        child_obj = self._add_new_item(idx)\n        child_obj.set_override_state(\n            self._override_state, self._ignore_missing_defaults\n        )\n\n        if trigger_change:\n            self.on_child_change(child_obj)\n        return child_obj\n\n    def swap_items(self, item_1, item_2):\n        index_1 = self.index(item_1)\n        index_2 = self.index(item_2)\n        self.swap_indexes(index_1, index_2)\n\n    def swap_indexes(self, index_1, index_2):\n        children_len = len(self.children)\n        if index_1 > children_len or index_2 > children_len:\n            raise IndexError(\n                \"{} index out of range\".format(self.__class__.__name__)\n            )\n        self.children[index_1], self.children[index_2] = (\n            self.children[index_2], self.children[index_1]\n        )\n        self.on_change()\n\n    def has_child_with_key(self, key):\n        if (\n            key\n            and isinstance(key, six.string_types)\n            and self._key_regex.match(key)\n        ):\n            key = int(key)\n\n        if not isinstance(key, int):\n            return False\n\n        return 0 <= key < len(self.children)\n\n    def _convert_to_valid_type(self, value):\n        if isinstance(value, (set, tuple)):\n            return list(value)\n        return NOT_SET\n\n    def _item_initialization(self):\n        self.valid_value_types = (list, )\n        self.children = []\n        self.value_on_not_set = []\n\n        self._ignore_child_changes = False\n\n        item_schema = self.schema_data[\"object_type\"]\n        if not isinstance(item_schema, dict):\n            item_schema = {\"type\": item_schema}\n\n        obj_template_name = self.schema_hub.get_template_name(item_schema)\n        _item_schemas = self.schema_hub.resolve_schema_data(item_schema)\n        if len(_item_schemas) == 1:\n            self.item_schema = _item_schemas[0]\n            if self.item_schema != item_schema:\n                if \"label\" in self.item_schema:\n                    self.item_schema.pop(\"label\")\n                self.item_schema[\"use_label_wrap\"] = False\n        else:\n            self.item_schema = _item_schemas\n\n        # Store if was used template or schema\n        self._obj_template_name = obj_template_name\n\n        if self.group_item is None:\n            self.is_group = True\n\n        # Value that was set on set_override_state\n        self.initial_value = []\n\n    def schema_validations(self):\n        if isinstance(self.item_schema, list):\n            reason = (\n                \"`ListWidget` has multiple items as object type.\"\n            )\n            raise EntitySchemaError(self, reason)\n\n        super(ListEntity, self).schema_validations()\n\n        if self.is_dynamic_item and self.use_label_wrap:\n            reason = (\n                \"`ListWidget` can't have set `use_label_wrap` to True and\"\n                \" be used as widget at the same time.\"\n            )\n            raise EntitySchemaError(self, reason)\n\n        if self.use_label_wrap and not self.label:\n            reason = (\n                \"`ListWidget` can't have set `use_label_wrap` to True and\"\n                \" not have set \\\"label\\\" key at the same time.\"\n            )\n            raise EntitySchemaError(self, reason)\n\n        # Validate object type schema\n        validate_children = True\n        for child_entity in self.children:\n            child_entity.schema_validations()\n            validate_children = False\n            break\n\n        if validate_children and self._obj_template_name:\n            _validated = self.schema_hub.is_dynamic_template_validated(\n                self._obj_template_name\n            )\n            _validating = self.schema_hub.is_dynamic_template_validating(\n                self._obj_template_name\n            )\n            validate_children = not _validated and not _validating\n\n        if not validate_children:\n            return\n\n        def _validate():\n            idx = 0\n            tmp_child = self._add_new_item(idx)\n            tmp_child.schema_validations()\n            self.children.pop(idx)\n\n        if self._obj_template_name:\n            with self.schema_hub.validating_dynamic(self._obj_template_name):\n                _validate()\n        else:\n            _validate()\n\n    def get_child_path(self, child_obj):\n        result_idx = None\n        for idx, _child_obj in enumerate(self.children):\n            if _child_obj is child_obj:\n                result_idx = idx\n                break\n\n        if result_idx is None:\n            raise ValueError(\"Didn't find child {}\".format(child_obj))\n\n        return \"/\".join([self.path, str(result_idx)])\n\n    def set(self, value):\n        new_value = self.convert_to_valid_type(value)\n        self.clear()\n        for item in new_value:\n            self.append(item)\n\n    def on_child_change(self, _child_entity):\n        if self._ignore_child_changes:\n            return\n\n        if self._override_state is OverrideState.STUDIO:\n            self._has_studio_override = True\n        elif self._override_state is OverrideState.PROJECT:\n            self._has_project_override = True\n        self.on_change()\n\n    def set_override_state(self, state, ignore_missing_defaults):\n        # Trigger override state change of root if is not same\n        if self.root_item.override_state is not state:\n            self.root_item.set_override_state(state)\n            return\n\n        self._override_state = state\n        self._ignore_missing_defaults = ignore_missing_defaults\n\n        while self.children:\n            self.children.pop(0)\n\n        # Ignore if is dynamic item and use default in that case\n        if not self.is_dynamic_item and not self.is_in_dynamic_item:\n            if state > OverrideState.DEFAULTS:\n                if (\n                    not self.has_default_value\n                    and not ignore_missing_defaults\n                ):\n                    raise DefaultsNotDefined(self)\n\n            elif state > OverrideState.STUDIO:\n                if (\n                    not self.had_studio_override\n                    and not ignore_missing_defaults\n                ):\n                    raise StudioDefaultsNotDefined(self)\n\n        value = NOT_SET\n        if self._override_state is OverrideState.PROJECT:\n            if self.had_project_override:\n                value = self._project_override_value\n            self._has_project_override = self.had_project_override\n\n        if value is NOT_SET or self._override_state is OverrideState.STUDIO:\n            if self.had_studio_override:\n                value = self._studio_override_value\n            self._has_studio_override = self.had_studio_override\n\n        if value is NOT_SET or self._override_state is OverrideState.DEFAULTS:\n            if self.has_default_value:\n                value = self._default_value\n            else:\n                value = self.value_on_not_set\n\n        for item in value:\n            child_obj = self._add_new_item()\n            child_obj.update_default_value(\n                item, self._default_log_invalid_types\n            )\n            if self._override_state is OverrideState.PROJECT:\n                if self.had_project_override:\n                    child_obj.update_project_value(\n                        item, self._project_log_invalid_types\n                    )\n                elif self.had_studio_override:\n                    child_obj.update_studio_value(\n                        item, self._studio_log_invalid_types\n                    )\n\n            elif self._override_state is OverrideState.STUDIO:\n                if self.had_studio_override:\n                    child_obj.update_studio_value(\n                        item, self._studio_log_invalid_types\n                    )\n\n        for child_obj in self.children:\n            child_obj.set_override_state(\n                self._override_state, ignore_missing_defaults\n            )\n\n        self.initial_value = self.settings_value()\n\n    @property\n    def value(self):\n        output = []\n        for child_obj in self.children:\n            output.append(child_obj.value)\n        return output\n\n    @property\n    def has_unsaved_changes(self):\n        if self._override_state is OverrideState.NOT_DEFINED:\n            return False\n\n        if self._override_state is OverrideState.DEFAULTS:\n            if not self.has_default_value:\n                return True\n\n        elif self._override_state is OverrideState.STUDIO:\n            if self.had_studio_override != self._has_studio_override:\n                return True\n\n            if not self._has_studio_override and not self.has_default_value:\n                return True\n\n        elif self._override_state is OverrideState.PROJECT:\n            if self.had_project_override != self._has_project_override:\n                return True\n\n            if (\n                not self._has_project_override\n                and not self._has_studio_override\n                and not self.has_default_value\n            ):\n                return True\n\n        if self._child_has_unsaved_changes:\n            return True\n\n        if self.settings_value() != self.initial_value:\n            return True\n        return False\n\n    @property\n    def has_studio_override(self):\n        if self._override_state >= OverrideState.STUDIO:\n            return (\n                self._has_studio_override\n                or self._child_has_studio_override\n            )\n        return False\n\n    @property\n    def has_project_override(self):\n        if self._override_state >= OverrideState.PROJECT:\n            return (\n                self._has_project_override\n                or self._child_has_project_override\n            )\n        return False\n\n    @property\n    def _child_has_unsaved_changes(self):\n        for child_obj in self.children:\n            if child_obj.has_unsaved_changes:\n                return True\n        return False\n\n    @property\n    def _child_has_studio_override(self):\n        if self._override_state >= OverrideState.STUDIO:\n            for child_obj in self.children:\n                if child_obj.has_studio_override:\n                    return True\n        return False\n\n    @property\n    def _child_has_project_override(self):\n        if self._override_state is OverrideState.PROJECT:\n            for child_obj in self.children:\n                if child_obj.has_project_override:\n                    return True\n        return False\n\n    def _settings_value(self):\n        output = []\n        for child_obj in self.children:\n            output.append(child_obj.settings_value())\n        return output\n\n    def _discard_changes(self, on_change_trigger):\n        if not self._can_discard_changes:\n            return\n\n        not_set = object()\n        value = not_set\n        if (\n            self._override_state >= OverrideState.PROJECT\n            and self.had_project_override\n        ):\n            value = copy.deepcopy(self._project_override_value)\n\n        if (\n            value is not_set\n            and self._override_state >= OverrideState.STUDIO\n            and self.had_studio_override\n        ):\n            value = copy.deepcopy(self._studio_override_value)\n\n        if value is not_set and self._override_state >= OverrideState.DEFAULTS:\n            if self.has_default_value:\n                value = copy.deepcopy(self._default_value)\n            else:\n                value = copy.deepcopy(self.value_on_not_set)\n\n        if value is not_set:\n            raise NotImplementedError(\"BUG: Unexcpected part of code.\")\n\n        self._ignore_child_changes = True\n\n        while self.children:\n            self.children.pop(0)\n\n        for item in value:\n            child_obj = self._add_new_item()\n            child_obj.update_default_value(\n                item, self._default_log_invalid_types\n            )\n            if self._override_state is OverrideState.PROJECT:\n                if self.had_project_override:\n                    child_obj.update_project_value(\n                        item, self._project_log_invalid_types\n                    )\n                elif self.had_studio_override:\n                    child_obj.update_studio_value(\n                        item, self._studio_log_invalid_types\n                    )\n\n            elif self._override_state is OverrideState.STUDIO:\n                if self.had_studio_override:\n                    child_obj.update_studio_value(\n                        item, self._studio_log_invalid_types\n                    )\n\n            child_obj.set_override_state(\n                self._override_state, self._ignore_missing_defaults\n            )\n\n        if self._override_state >= OverrideState.PROJECT:\n            self._has_project_override = self.had_project_override\n\n        if self._override_state >= OverrideState.STUDIO:\n            self._has_studio_override = self.had_studio_override\n\n        self._ignore_child_changes = False\n\n        on_change_trigger.append(self.on_change)\n\n    def _add_to_studio_default(self, _on_change_trigger):\n        self._has_studio_override = True\n        self.on_change()\n\n    def _remove_from_studio_default(self, on_change_trigger):\n        if not self._can_remove_from_studio_default:\n            return\n\n        value = self._default_value\n        if value is NOT_SET:\n            value = self.value_on_not_set\n\n        self._ignore_child_changes = True\n\n        while self.children:\n            self.children.pop(0)\n\n        for item in value:\n            child_obj = self._add_new_item()\n            child_obj.update_default_value(item)\n            child_obj.set_override_state(\n                self._override_state, self._ignore_missing_defaults\n            )\n\n        self._ignore_child_changes = False\n\n        self._has_studio_override = False\n\n        on_change_trigger.append(self.on_change)\n\n    def _add_to_project_override(self, _on_change_trigger):\n        self._has_project_override = True\n        self.on_change()\n\n    def _remove_from_project_override(self, on_change_trigger):\n        if not self._can_remove_from_project_override:\n            return\n\n        if self._has_studio_override:\n            value = self._studio_override_value\n        elif self.has_default_value:\n            value = self._default_value\n        else:\n            value = self.value_on_not_set\n\n        self._ignore_child_changes = True\n\n        while self.children:\n            self.children.pop(0)\n\n        for item in value:\n            child_obj = self._add_new_item()\n            child_obj.update_default_value(item)\n            if self._has_studio_override:\n                child_obj.update_studio_value(item)\n            child_obj.set_override_state(\n                self._override_state,\n                self._ignore_missing_defaults\n            )\n\n        self._ignore_child_changes = False\n\n        self._has_project_override = False\n\n        on_change_trigger.append(self.on_change)\n\n    def reset_callbacks(self):\n        super(ListEntity, self).reset_callbacks()\n        for child_entity in self.children:\n            child_entity.reset_callbacks()\n"
  },
  {
    "path": "openpype/settings/entities/op_version_entity.py",
    "content": "from openpype.lib.openpype_version import (\n    get_remote_versions,\n    get_OpenPypeVersion,\n    get_installed_version\n)\nfrom .input_entities import TextEntity\nfrom .lib import (\n    OverrideState,\n    NOT_SET\n)\nfrom .exceptions import BaseInvalidValue\n\n\nclass OpenPypeVersionInput(TextEntity):\n    \"\"\"Entity to store OpenPype version to use.\n\n    Settings created on another machine may affect available versions\n    on current user's machine. Text input element is provided to explicitly\n    set version not yet showing up the user's machine.\n\n    It is possible to enter empty string. In that case is used any latest\n    version. Any other string must match regex of OpenPype version semantic.\n    \"\"\"\n    def _item_initialization(self):\n        super(OpenPypeVersionInput, self)._item_initialization()\n        self.multiline = False\n        self.placeholder_text = \"Latest\"\n        self.value_hints = []\n\n    def _get_openpype_versions(self):\n        \"\"\"This is abstract method returning version hints for UI purposes.\"\"\"\n        raise NotImplementedError((\n            \"{} does not have implemented '_get_openpype_versions'\"\n        ).format(self.__class__.__name__))\n\n    def set_override_state(self, state, *args, **kwargs):\n        \"\"\"Update value hints for UI purposes.\"\"\"\n        value_hints = []\n        if state is OverrideState.STUDIO:\n            versions = self._get_openpype_versions()\n            for version in versions:\n                version_str = str(version)\n                if version_str not in value_hints:\n                    value_hints.append(version_str)\n\n        self.value_hints = value_hints\n\n        super(OpenPypeVersionInput, self).set_override_state(\n            state, *args, **kwargs\n        )\n\n    def convert_to_valid_type(self, value):\n        \"\"\"Add validation of version regex.\"\"\"\n        if value and value is not NOT_SET:\n            OpenPypeVersion = get_OpenPypeVersion()\n            if OpenPypeVersion is not None:\n                try:\n                    OpenPypeVersion(version=value)\n                except Exception:\n                    raise BaseInvalidValue(\n                        \"Value \\\"{}\\\"is not valid version format.\".format(\n                            value\n                        ),\n                        self.path\n                    )\n        return super(OpenPypeVersionInput, self).convert_to_valid_type(value)\n\n\nclass VersionsInputEntity(OpenPypeVersionInput):\n    \"\"\"Entity meant only for global settings to define production version.\"\"\"\n    schema_types = [\"versions-text\"]\n\n    def _get_openpype_versions(self):\n        versions = get_remote_versions()\n        if versions is None:\n            return []\n        versions.append(get_installed_version())\n        return sorted(versions)\n"
  },
  {
    "path": "openpype/settings/entities/root_entities.py",
    "content": "import os\nimport json\nimport copy\nimport collections\n\nfrom abc import abstractmethod\n\nfrom .base_entity import BaseItemEntity\nfrom .lib import (\n    NOT_SET,\n    WRAPPER_TYPES,\n    SCHEMA_KEY_SYSTEM_SETTINGS,\n    SCHEMA_KEY_PROJECT_SETTINGS,\n    OverrideState,\n    SchemasHub,\n    DynamicSchemaValueCollector\n)\nfrom .exceptions import (\n    SchemaError,\n    InvalidKeySymbols\n)\nfrom openpype.settings.constants import (\n    SYSTEM_SETTINGS_KEY,\n    PROJECT_SETTINGS_KEY,\n    PROJECT_ANATOMY_KEY,\n    KEY_REGEX\n)\nfrom openpype.settings.exceptions import SaveWarningExc\n\nfrom openpype.settings.lib import (\n    DEFAULTS_DIR,\n\n    get_default_settings,\n    reset_default_settings,\n\n    get_studio_system_settings_overrides,\n    get_studio_system_settings_overrides_for_version,\n    save_studio_settings,\n    get_available_studio_system_settings_overrides_versions,\n\n    get_studio_project_settings_overrides,\n    get_studio_project_settings_overrides_for_version,\n    get_studio_project_anatomy_overrides,\n    get_studio_project_anatomy_overrides_for_version,\n    get_project_settings_overrides,\n    get_project_settings_overrides_for_version,\n    get_project_anatomy_overrides,\n    save_project_settings,\n    save_project_anatomy,\n\n    get_available_project_settings_overrides_versions,\n    get_available_studio_project_settings_overrides_versions,\n    get_available_studio_project_anatomy_overrides_versions,\n\n    apply_overrides\n)\n\n\nclass RootEntity(BaseItemEntity):\n    \"\"\"Abstract class for root entities.\n\n    Root entity is top hierarchy entity without parent. Should care about\n    saving and must have access to all entities in it's scope.\n    \"\"\"\n    schema_types = [\"root\"]\n\n    def __init__(self, schema_hub, reset, main_schema_name=None):\n        self.schema_hub = schema_hub\n        if not main_schema_name:\n            main_schema_name = \"schema_main\"\n        schema_data = schema_hub.get_schema(main_schema_name)\n\n        super(RootEntity, self).__init__(schema_data)\n        self._require_restart_callbacks = []\n        self._item_ids_require_restart = set()\n        self._item_initialization()\n        if reset:\n            self.reset()\n\n    @property\n    def override_state(self):\n        \"\"\"Current OverrideState.\"\"\"\n        return self._override_state\n\n    @property\n    def require_restart(self):\n        return bool(self._item_ids_require_restart)\n\n    def add_require_restart_change_callback(self, callback):\n        self._require_restart_callbacks.append(callback)\n\n    def _on_require_restart_change(self):\n        for callback in self._require_restart_callbacks:\n            callback()\n\n    def add_item_require_restart(self, item):\n        was_empty = len(self._item_ids_require_restart) == 0\n        self._item_ids_require_restart.add(item.id)\n        if was_empty:\n            self._on_require_restart_change()\n\n    def remove_item_require_restart(self, item):\n        if item.id not in self._item_ids_require_restart:\n            return\n\n        self._item_ids_require_restart.remove(item.id)\n        if not self._item_ids_require_restart:\n            self._on_require_restart_change()\n\n    @abstractmethod\n    def reset(self):\n        \"\"\"Reset values and entities to initial state.\n\n        Reload settings and entities should reset their changes or be\n        recreated.\n        \"\"\"\n        pass\n\n    def __getitem__(self, key):\n        return self.non_gui_children[key]\n\n    def __setitem__(self, key, value):\n        self.non_gui_children[key].set(value)\n\n    def __iter__(self):\n        for key in self.keys():\n            yield key\n\n    def get(self, key, default=None):\n        return self.non_gui_children.get(key, default)\n\n    def set(self, value):\n        \"\"\"Set value.\"\"\"\n        new_value = self.convert_to_valid_type(value)\n        for _key, _value in new_value.items():\n            self.non_gui_children[_key].set(_value)\n\n    def has_child_with_key(self, key):\n        return key in self.non_gui_children\n\n    def keys(self):\n        return self.non_gui_children.keys()\n\n    def values(self):\n        return self.non_gui_children.values()\n\n    def items(self):\n        return self.non_gui_children.items()\n\n    def _add_children(self, schema_data, first=True):\n        added_children = []\n        children_deque = collections.deque()\n        for _children_schema in schema_data[\"children\"]:\n            children_schemas = self.schema_hub.resolve_schema_data(\n                _children_schema\n            )\n            for children_schema in children_schemas:\n                children_deque.append(children_schema)\n\n        while children_deque:\n            children_schema = children_deque.popleft()\n\n            if children_schema[\"type\"] in WRAPPER_TYPES:\n                _children_schema = copy.deepcopy(children_schema)\n                wrapper_children = self._add_children(\n                    children_schema[\"children\"], False\n                )\n                _children_schema[\"children\"] = wrapper_children\n                added_children.append(_children_schema)\n                continue\n\n            child_obj = self.create_schema_object(children_schema, self)\n            self.children.append(child_obj)\n            added_children.append(child_obj)\n            if isinstance(child_obj, self.schema_hub.gui_types):\n                continue\n\n            if child_obj.key in self.non_gui_children:\n                raise KeyError(\n                    \"Duplicated key \\\"{}\\\"\".format(child_obj.key)\n                )\n            self.non_gui_children[child_obj.key] = child_obj\n\n        if not first:\n            return added_children\n\n        for child_obj in added_children:\n            self.gui_layout.append(child_obj)\n\n    def _item_initialization(self):\n        # Store `self` to `root_item` for children entities\n        self.root_item = self\n\n        # Children are stored by key as keys are immutable and are defined by\n        # schema\n        self.valid_value_types = (dict, )\n\n        self.children = []\n        self.non_gui_children = {}\n        self.gui_layout = []\n\n        self._add_children(self.schema_data)\n\n        self.schema_validations()\n\n    def schema_validations(self):\n        for child_entity in self.children:\n            if child_entity.is_group:\n                reason = (\n                    \"Root entity \\\"{}\\\" has child with `is_group`\"\n                    \" attribute set to True but root can't save overrides.\"\n                ).format(self.__class__.__name__)\n                raise SchemaError(reason)\n            child_entity.schema_validations()\n\n        for key in self.non_gui_children.keys():\n            if not KEY_REGEX.match(key):\n                raise InvalidKeySymbols(self.path, key)\n\n    @abstractmethod\n    def get_entity_from_path(self, path):\n        \"\"\"Return entity matching passed path.\"\"\"\n        pass\n\n    def create_schema_object(self, schema_data, *args, **kwargs):\n        \"\"\"Create entity by entered schema data.\n\n        Available entities are loaded on first run. Children entities can call\n        this method.\n        \"\"\"\n        return self.schema_hub.create_schema_object(\n            schema_data, *args, **kwargs\n        )\n\n    def set_override_state(self, state, ignore_missing_defaults=None):\n        \"\"\"Set override state and trigger it on children.\n\n        Method will discard all changes in hierarchy and use values, metadata\n        and all kind of values for defined state.\n\n        Args:\n            state (OverrideState): State to which should be data changed.\n        \"\"\"\n        if not ignore_missing_defaults:\n            ignore_missing_defaults = False\n\n        self._override_state = state\n        for child_obj in self.non_gui_children.values():\n            child_obj.set_override_state(state, ignore_missing_defaults)\n\n    def on_change(self):\n        \"\"\"Trigger callbacks on change.\"\"\"\n        for callback in self.on_change_callbacks:\n            callback()\n\n    def on_child_change(self, _child_entity):\n        \"\"\"Whan any children has changed.\"\"\"\n        self.on_change()\n\n    def collect_static_entities_by_path(self):\n        output = {}\n        for child_obj in self.non_gui_children.values():\n            result = child_obj.collect_static_entities_by_path()\n            if result:\n                output.update(result)\n        return output\n\n    def get_child_path(self, child_entity):\n        \"\"\"Return path of children entity\"\"\"\n        for key, _child_entity in self.non_gui_children.items():\n            if _child_entity is child_entity:\n                return key\n        raise ValueError(\"Didn't find child {}\".format(child_entity))\n\n    @property\n    def value(self):\n        \"\"\"Value for current override state without metadata.\"\"\"\n        output = {}\n        for key, child_obj in self.non_gui_children.items():\n            output[key] = child_obj.value\n        return output\n\n    def collect_dynamic_schema_entities(self):\n        output = DynamicSchemaValueCollector(self.schema_hub)\n        if self._override_state is not OverrideState.DEFAULTS:\n            return output\n\n        for child_obj in self.non_gui_children.values():\n            child_obj.collect_dynamic_schema_entities(output)\n\n        return output\n\n    def settings_value(self):\n        \"\"\"Value for current override state with metadata.\n\n        This is what should be stored on save method.\n        \"\"\"\n        if self._override_state is OverrideState.NOT_DEFINED:\n            return NOT_SET\n\n        if self._override_state is not OverrideState.DEFAULTS:\n            output = {}\n            for key, child_obj in self.non_gui_children.items():\n                if child_obj.is_dynamic_schema_node:\n                    continue\n                value = child_obj.settings_value()\n                if value is not NOT_SET:\n                    output[key] = value\n            return output\n\n        output = {}\n        for key, child_obj in self.non_gui_children.items():\n            child_value = child_obj.settings_value()\n            if not child_obj.is_file and not child_obj.file_item:\n                for _key, _value in child_value.items():\n                    new_key = \"/\".join([key, _key])\n                    output[new_key] = _value\n            else:\n                output[key] = child_value\n        return output\n\n    @property\n    def has_studio_override(self):\n        \"\"\"Any children has studio override.\n\n        Return's relevant data only if override state is STUDIO or PROJECT.\n\n        Returns:\n            bool: True if any children has studio overrides.\n        \"\"\"\n        if self._override_state >= OverrideState.STUDIO:\n            for child_obj in self.non_gui_children.values():\n                if child_obj.has_studio_override:\n                    return True\n        return False\n\n    @property\n    def has_project_override(self):\n        \"\"\"Any children has project override.\n\n        Return's relevant data only if override state is PROJECT.\n\n        Returns:\n            bool: True if any children has project overrides.\n        \"\"\"\n        if self._override_state >= OverrideState.PROJECT:\n            for child_obj in self.non_gui_children.values():\n                if child_obj.has_project_override:\n                    return True\n        return False\n\n    @property\n    def has_unsaved_changes(self):\n        \"\"\"Entity contain unsaved changes.\n\n        Root on it's own can't have any modifications so looks only on\n        children.\n\n        Returns:\n            bool: True if has unsaved changes.\n        \"\"\"\n        for child_obj in self.non_gui_children.values():\n            if child_obj.has_unsaved_changes:\n                return True\n        return False\n\n    def _discard_changes(self, on_change_trigger):\n        \"\"\"Implementation of abstract method only trigger children callback.\"\"\"\n        for child_obj in self.non_gui_children.values():\n            child_obj.discard_changes(on_change_trigger)\n\n    def _add_to_studio_default(self, *args, **kwargs):\n        \"\"\"Implementation of abstract method only trigger children callback.\"\"\"\n        for child_obj in self.non_gui_children.values():\n            child_obj.add_to_studio_default(*args, **kwargs)\n\n    def _remove_from_studio_default(self, on_change_trigger):\n        \"\"\"Implementation of abstract method only trigger children callback.\"\"\"\n        for child_obj in self.non_gui_children.values():\n            child_obj.remove_from_studio_default(on_change_trigger)\n\n    def _add_to_project_override(self, *args, **kwargs):\n        \"\"\"Implementation of abstract method only trigger children callback.\"\"\"\n        for child_obj in self.non_gui_children.values():\n            child_obj.add_to_project_override(*args, **kwargs)\n\n    def _remove_from_project_override(self, on_change_trigger):\n        \"\"\"Implementation of abstract method only trigger children callback.\"\"\"\n        for child_obj in self.non_gui_children.values():\n            child_obj.remove_from_project_override(on_change_trigger)\n\n    def save(self):\n        \"\"\"Save values for current override state.\n\n        Values are stored with current values and modifications.\n        \"\"\"\n        if self._override_state is OverrideState.NOT_DEFINED:\n            raise ValueError(\n                \"Can't save if override state is set to NOT_DEFINED\"\n            )\n\n        if self._override_state is OverrideState.DEFAULTS:\n            self._save_default_values()\n            reset_default_settings()\n\n        elif self._override_state is OverrideState.STUDIO:\n            self._save_studio_values()\n\n        elif self._override_state is OverrideState.PROJECT:\n            self._save_project_values()\n\n        # Trigger reset to reload entities\n        self.reset()\n\n    @abstractmethod\n    def defaults_dir(self):\n        \"\"\"Abstract method to return directory path to defaults.\n\n        Implementation of `_save_default_values` requires defaults dir where\n        default data will be stored.\n        \"\"\"\n        pass\n\n    def _save_default_values(self):\n        \"\"\"Save default values.\n\n        Do not call this method, always use `save`. Manually called method\n        won't save current values as defaults if override state is not set to\n        DEFAULTS.\n        \"\"\"\n        settings_value = self.settings_value()\n\n        defaults_dir = self.defaults_dir()\n        for file_path, value in settings_value.items():\n            subpath = file_path + \".json\"\n\n            output_path = os.path.join(defaults_dir, subpath)\n            dirpath = os.path.dirname(output_path)\n            if not os.path.exists(dirpath):\n                os.makedirs(dirpath)\n\n            self.log.debug(\"Saving data to: {}\\n{}\".format(subpath, value))\n            data = json.dumps(value, indent=4) + \"\\n\"\n            with open(output_path, \"w\") as file_stream:\n                file_stream.write(data)\n\n        dynamic_values_item = self.collect_dynamic_schema_entities()\n        dynamic_values_item.save_values()\n\n    @abstractmethod\n    def _save_studio_values(self):\n        \"\"\"Save studio override values.\"\"\"\n        pass\n\n    @abstractmethod\n    def _save_project_values(self):\n        \"\"\"Save project override values.\"\"\"\n        pass\n\n    def is_in_defaults_state(self):\n        \"\"\"Api callback to check if current state is DEFAULTS.\"\"\"\n        return self._override_state is OverrideState.DEFAULTS\n\n    def is_in_studio_state(self):\n        \"\"\"Api callback to check if current state is STUDIO.\"\"\"\n        return self._override_state is OverrideState.STUDIO\n\n    def is_in_project_state(self):\n        \"\"\"Api callback to check if current state is PROJECT.\"\"\"\n        return self._override_state is OverrideState.PROJECT\n\n    def set_defaults_state(self):\n        \"\"\"Change override state to DEFAULTS.\"\"\"\n        self.set_override_state(OverrideState.DEFAULTS)\n\n    def set_studio_state(self):\n        \"\"\"Change override state to STUDIO.\"\"\"\n        self.set_override_state(OverrideState.STUDIO)\n\n    def set_project_state(self):\n        \"\"\"Change override state to PROJECT.\"\"\"\n        self.set_override_state(OverrideState.PROJECT)\n\n\nclass SystemSettings(RootEntity):\n    \"\"\"Root entity for system settings.\n\n    Allows to modify system settings via entity system and loaded schemas.\n\n    Args:\n        set_studio_state (bool): Set studio values on initialization. By\n            default is set to True.\n        reset (bool): Reset values on initialization. By default is set to\n            True.\n        schema_data (dict): Pass schema data to entity. This is for development\n            and debugging purposes.\n    \"\"\"\n    root_key = SYSTEM_SETTINGS_KEY\n\n    def __init__(\n        self,\n        set_studio_state=True,\n        reset=True,\n        schema_hub=None,\n        source_version=None\n    ):\n        if schema_hub is None:\n            # Load system schemas\n            schema_hub = SchemasHub(SCHEMA_KEY_SYSTEM_SETTINGS)\n\n        self._source_version = source_version\n\n        super(SystemSettings, self).__init__(schema_hub, reset)\n\n        if set_studio_state:\n            self.set_studio_state()\n\n    @property\n    def source_version(self):\n        return self._source_version\n\n    def get_entity_from_path(self, path):\n        \"\"\"Return system settings entity.\"\"\"\n        path_parts = path.split(\"/\")\n        first_part = path_parts[0]\n        output = self\n        if first_part == self.root_key:\n            path_parts.pop(0)\n\n        for path_part in path_parts:\n            output = output[path_part]\n        return output\n\n    def _reset_values(self):\n        default_value = get_default_settings()[SYSTEM_SETTINGS_KEY]\n        for key, child_obj in self.non_gui_children.items():\n            value = default_value.get(key, NOT_SET)\n            child_obj.update_default_value(value)\n\n        if self._source_version is None:\n            studio_overrides, version = get_studio_system_settings_overrides(\n                return_version=True\n            )\n            self._source_version = version\n\n        else:\n            studio_overrides = (\n                get_studio_system_settings_overrides_for_version(\n                    self._source_version\n                )\n            )\n\n        for key, child_obj in self.non_gui_children.items():\n            value = studio_overrides.get(key, NOT_SET)\n            child_obj.update_studio_value(value)\n\n    def reset(self, new_state=None, source_version=None):\n        \"\"\"Discard changes and reset entit's values.\n\n        Reload default values and studio override values and update entities.\n\n        Args:\n            new_state (OverrideState): It is possible to change override state\n                during reset. Current state is used if not defined.\n        \"\"\"\n        if new_state is None:\n            new_state = self._override_state\n\n        if new_state is OverrideState.NOT_DEFINED:\n            new_state = OverrideState.DEFAULTS\n\n        if new_state is OverrideState.PROJECT:\n            raise ValueError(\"System settings can't store poject overrides.\")\n\n        if source_version is not None:\n            self._source_version = source_version\n\n        self._reset_values()\n        self.set_override_state(new_state)\n\n    def get_available_source_versions(self, sorted=None):\n        if self.is_in_studio_state():\n            return self.get_available_studio_versions(sorted=sorted)\n        return []\n\n    def get_available_studio_versions(self, sorted=None):\n        return get_available_studio_system_settings_overrides_versions(\n            sorted=sorted\n        )\n\n    def defaults_dir(self):\n        \"\"\"Path to defaults directory.\n\n        Implementation of abstract method.\n        \"\"\"\n        return os.path.join(DEFAULTS_DIR, SYSTEM_SETTINGS_KEY)\n\n    def _save_studio_values(self):\n        settings_value = self.settings_value()\n\n        self.log.debug(\"Saving system settings: {}\".format(\n            json.dumps(settings_value, indent=4)\n        ))\n        save_studio_settings(settings_value)\n        # Reset source version after restart\n        self._source_version = None\n\n    def _save_project_values(self):\n        \"\"\"System settings can't have project overrides.\n\n        Raises:\n            ValueError: Raise when called as entity can't use or store project\n                overrides.\n        \"\"\"\n        raise ValueError(\"System settings can't save project overrides.\")\n\n\nclass ProjectSettings(RootEntity):\n    \"\"\"Root entity for project settings.\n\n    Allows to modify project settings via entity system and loaded schemas.\n\n    Args:\n        project_name (str): Project name which overrides will be loaded.\n            Use `None` to modify studio defaults.\n        change_state (bool): Set values on initialization. By\n            default is set to True.\n        reset (bool): Reset values on initialization. By default is set to\n            True.\n        schema_data (dict): Pass schema data to entity. This is for development\n            and debugging purposes.\n    \"\"\"\n    root_key = PROJECT_SETTINGS_KEY\n\n    def __init__(\n        self,\n        project_name=None,\n        change_state=True,\n        reset=True,\n        schema_hub=None,\n        source_version=None,\n        anatomy_source_version=None\n    ):\n        self._project_name = project_name\n\n        self._system_settings_entity = None\n        self._source_version = source_version\n        self._anatomy_source_version = anatomy_source_version\n\n        if schema_hub is None:\n            # Load system schemas\n            schema_hub = SchemasHub(SCHEMA_KEY_PROJECT_SETTINGS)\n\n        super(ProjectSettings, self).__init__(schema_hub, reset)\n\n        if change_state:\n            if self.project_name is None:\n                self.set_studio_state()\n            else:\n                self.set_project_state()\n\n    @property\n    def source_version(self):\n        return self._source_version\n\n    @property\n    def anatomy_source_version(self):\n        return self._anatomy_source_version\n\n    @property\n    def project_name(self):\n        return self._project_name\n\n    @project_name.setter\n    def project_name(self, project_name):\n        self.change_project(project_name)\n\n    @property\n    def system_settings_entity(self):\n        output = self._system_settings_entity\n        if output is None:\n            output = SystemSettings(set_studio_state=False)\n            self._system_settings_entity = output\n\n        if self.override_state is OverrideState.DEFAULTS:\n            if output.override_state is not OverrideState.DEFAULTS:\n                output.set_defaults_state()\n        elif self.override_state > OverrideState.DEFAULTS:\n            if output.override_state <= OverrideState.DEFAULTS:\n                try:\n                    output.set_studio_state()\n                except Exception:\n                    output.set_defaults_state()\n        return output\n\n    def get_entity_from_path(self, path):\n        path_parts = path.split(\"/\")\n        first_part = path_parts[0]\n        # TODO replace with constants\n        if first_part == \"system_settings\":\n            output = self.system_settings_entity\n            path_parts.pop(0)\n        else:\n            output = self\n            if first_part == \"project_settings\":\n                path_parts.pop(0)\n\n        for path_part in path_parts:\n            output = output[path_part]\n        return output\n\n    def change_project(self, project_name, source_version=None):\n        if project_name == self._project_name:\n            if (\n                source_version is None\n                or source_version == self._source_version\n            ):\n                if not self.is_in_project_state():\n                    self.set_project_state()\n                return\n\n        self._source_version = source_version\n        self._anatomy_source_version = None\n\n        self._set_values_for_project(project_name)\n        self.set_project_state()\n\n    def _reset_values(self):\n        default_values = {\n            PROJECT_SETTINGS_KEY: get_default_settings()[PROJECT_SETTINGS_KEY],\n            PROJECT_ANATOMY_KEY: get_default_settings()[PROJECT_ANATOMY_KEY]\n        }\n        for key, child_obj in self.non_gui_children.items():\n            value = default_values.get(key, NOT_SET)\n            child_obj.update_default_value(value)\n\n        self._set_values_for_project(self.project_name)\n\n    def _set_values_for_project(self, project_name):\n        self._project_name = project_name\n        if project_name:\n            project_settings_overrides = (\n                get_studio_project_settings_overrides()\n            )\n            project_anatomy_overrides = (\n                get_studio_project_anatomy_overrides()\n            )\n        else:\n            if self._source_version is None:\n                project_settings_overrides, version = (\n                    get_studio_project_settings_overrides(return_version=True)\n                )\n                self._source_version = version\n            else:\n                project_settings_overrides = (\n                    get_studio_project_settings_overrides_for_version(\n                        self._source_version\n                    )\n                )\n\n            if self._anatomy_source_version is None:\n                project_anatomy_overrides, anatomy_version = (\n                    get_studio_project_anatomy_overrides(return_version=True)\n                )\n                self._anatomy_source_version = anatomy_version\n            else:\n                project_anatomy_overrides = (\n                    get_studio_project_anatomy_overrides_for_version(\n                        self._anatomy_source_version\n                    )\n                )\n\n        studio_overrides = {\n            PROJECT_SETTINGS_KEY: project_settings_overrides,\n            PROJECT_ANATOMY_KEY: project_anatomy_overrides\n        }\n\n        for key, child_obj in self.non_gui_children.items():\n            value = studio_overrides.get(key, NOT_SET)\n            child_obj.update_studio_value(value)\n\n        if not project_name:\n            return\n\n        if self._source_version is None:\n            project_settings_overrides, version = (\n                get_project_settings_overrides(\n                    project_name, return_version=True\n                )\n            )\n            self._source_version = version\n        else:\n            project_settings_overrides = (\n                get_project_settings_overrides_for_version(\n                    project_name, self._source_version\n                )\n            )\n\n        project_override_value = {\n            PROJECT_SETTINGS_KEY: project_settings_overrides,\n            PROJECT_ANATOMY_KEY: get_project_anatomy_overrides(project_name)\n        }\n        for key, child_obj in self.non_gui_children.items():\n            value = project_override_value.get(key, NOT_SET)\n            child_obj.update_project_value(value)\n\n    def get_available_source_versions(self, sorted=None):\n        if self.is_in_studio_state():\n            return self.get_available_studio_versions(sorted=sorted)\n        elif self.is_in_project_state():\n            return get_available_project_settings_overrides_versions(\n                self.project_name, sorted=sorted\n            )\n        return []\n\n    def get_available_studio_versions(self, sorted=None):\n        return get_available_studio_project_settings_overrides_versions(\n            sorted=sorted\n        )\n\n    def get_available_anatomy_source_versions(self, sorted=None):\n        if self.is_in_studio_state():\n            return get_available_studio_project_anatomy_overrides_versions(\n                sorted=sorted\n            )\n        return []\n\n    def reset(self, new_state=None):\n        \"\"\"Discard changes and reset entit's values.\n\n        Reload default values and studio override values and update entities.\n\n        Args:\n            new_state (OverrideState): It is possible to change override state\n                during reset. Current state is used if not defined.\n        \"\"\"\n        if new_state is None:\n            new_state = self._override_state\n\n        if new_state is OverrideState.NOT_DEFINED:\n            new_state = OverrideState.DEFAULTS\n\n        self._system_settings_entity = None\n\n        self._reset_values()\n        self.set_override_state(new_state)\n\n    def defaults_dir(self):\n        \"\"\"Path to defaults directory.\n\n        Implementation of abstract method.\n        \"\"\"\n        return DEFAULTS_DIR\n\n    def _save_studio_values(self):\n        settings_value = self.settings_value()\n\n        self._validate_values_to_save(settings_value)\n\n        self._source_version = None\n        self._anatomy_source_version = None\n\n        self.log.debug(\"Saving project settings: {}\".format(\n            json.dumps(settings_value, indent=4)\n        ))\n        project_settings = settings_value.get(PROJECT_SETTINGS_KEY) or {}\n        project_anatomy = settings_value.get(PROJECT_ANATOMY_KEY) or {}\n\n        warnings = []\n        try:\n            save_project_settings(self.project_name, project_settings)\n        except SaveWarningExc as exc:\n            warnings.extend(exc.warnings)\n\n        try:\n            save_project_anatomy(self.project_name, project_anatomy)\n        except SaveWarningExc as exc:\n            warnings.extend(exc.warnings)\n\n        if warnings:\n            raise SaveWarningExc(warnings)\n\n    def _validate_values_to_save(self, value):\n        pass\n\n    def _save_project_values(self):\n        \"\"\"Project overrides are saved same ways as studio overrides.\"\"\"\n        self._save_studio_values()\n"
  },
  {
    "path": "openpype/settings/entities/schemas/README.md",
    "content": "# Creating GUI schemas\n\n## Basic rules\n- configurations does not define GUI, but GUI defines configurations!\n- output is always json serializable\n- GUI schema has multiple input types, all inputs are represented by a dictionary\n- each input may have \"input modifiers\" (keys in dictionary) that are required or optional\n    - only required modifier for all input items is key `\"type\"` which says what type of item it is\n- there are special keys across all inputs\n    - `\"is_file\"` - this key is for storing openpype defaults in `openpype` repo\n        - reasons of existence: developing new schemas does not require to create defaults manually\n        - key is validated, must be once in hierarchy else it won't be possible to store openpype defaults\n    - `\"is_group\"` - define that all values under key in hierarchy will be overridden if any value is modified, this information is also stored to overrides\n        - this keys is not allowed for all inputs as they may have not reason for that\n        - key is validated, can be only once in hierarchy but is not required\n- currently there are `system settings` and `project settings`\n- all entities can have set `\"tooltip\"` key with description which will be shown in UI\n\n## Inner schema\n- GUI schemas are huge json files, to be able to split whole configuration into multiple schema there's type `schema`\n- system configuration schemas are stored in `~/openpype/settings/entities/schemas/system_schema/` and project configurations in `~/openpype/settings/entities/schemas/projects_schema/`\n- each schema name is filename of json file except extension (without \".json\")\n- if content is dictionary content will be used as `schema` else will be used as `schema_template`\n\n### schema\n- can have only key `\"children\"` which is list of strings, each string should represent another schema (order matters) string represents name of the schema\n- will just paste schemas from other schema file in order of \"children\" list\n\n```\n{\n    \"type\": \"schema\",\n    \"name\": \"my_schema_name\"\n}\n```\n\n### template\n- allows to define schema \"templates\" to not duplicate same content multiple times\n- legacy name is `schema_template` (still usable)\n```javascript\n// EXAMPLE json file content (filename: example_template.json)\n[\n    {\n        \"__default_values__\": {\n            \"multipath_executables\": true\n        }\n    }, {\n        \"type\": \"raw-json\",\n        \"label\": \"{host_label} Environments\",\n        \"key\": \"{host_name}_environments\"\n    }, {\n        \"type\": \"path\",\n        \"key\": \"{host_name}_executables\",\n        \"label\": \"{host_label} - Full paths to executables\",\n        \"multiplatform\": \"{multipath_executables}\",\n        \"multipath\": true\n    }\n]\n```\n```javascript\n// EXAMPLE usage of the template in schema\n{\n    \"type\": \"dict\",\n    \"key\": \"template_examples\",\n    \"label\": \"Schema template examples\",\n    \"children\": [\n        {\n            \"type\": \"template\",\n            // filename of template (example_template.json)\n            \"name\": \"example_template\",\n            \"template_data\": {\n                \"host_label\": \"Maya 2019\",\n                \"host_name\": \"maya_2019\",\n                \"multipath_executables\": false\n            }\n        }, {\n            \"type\": \"template\",\n            \"name\": \"example_template\",\n            \"template_data\": {\n                \"host_label\": \"Maya 2020\",\n                \"host_name\": \"maya_2020\"\n            }\n        }\n    ]\n}\n```\n- item in schema mush contain `\"type\"` and `\"name\"` keys but it is also expected that `\"template_data\"` will be entered too\n- all items in the list, except `__default_values__`, will replace `schema_template` item in schema\n- template may contain another template or schema\n- it is expected that schema template will have unfilled fields as in example\n    - unfilled fields are allowed only in values of schema dictionary\n```javascript\n{\n    ...\n    // Allowed\n    \"key\": \"{to_fill}\"\n    ...\n    // Not allowed\n    \"{to_fill}\": \"value\"\n    ...\n}\n```\n- Unfilled fields can be also used for non string values(e.g. dictionary), in that case value must contain only one key and value for fill must contain right type.\n```javascript\n// Passed data\n{\n    \"executable_multiplatform\": {\n        \"type\": \"schema\",\n        \"name\": \"my_multiplatform_schema\"\n    }\n}\n// Template content\n{\n    ...\n    // Allowed\n    \"multiplatform\": \"{executable_multiplatform}\"\n    ...\n    // Not allowed\n    \"multiplatform\": \"{executable_multiplatform}_enhanced_string\"\n    ...\n}\n```\n- It is possible to define default values for unfilled fields to do so one of items in list must be dictionary with key `\"__default_values__\"` and value as dictionary with default key: values (as in example above).\n\n### dynamic_schema\n- dynamic templates that can be defined by class of `ModuleSettingsDef`\n- example:\n```\n{\n    \"type\": \"dynamic_schema\",\n    \"name\": \"project_settings/global\"\n}\n```\n- all valid `BaseModuleSettingsDef` classes where calling of `get_settings_schemas`\n    will return dictionary where is key \"project_settings/global\" with schemas\n    will extend and replace this item\n- dynamic schemas work almost the same way as templates\n    - one item can be replaced by multiple items (or by 0 items)\n- goal is to dynamically loaded settings of OpenPype addons without having\n    their schemas or default values in main repository\n    - values of these schemas are saved using the `BaseModuleSettingsDef` methods\n- easiest is to use `JsonFilesSettingsDef` which has full implementation of storing default values to json files all you have to implement is method `get_settings_root_path` which should return path to root directory where settings schema can be found and will be saved\n\n## Basic Dictionary inputs\n- these inputs wraps another inputs into {key: value} relation\n\n## dict\n- this is dictionary type wrapping more inputs with keys defined in schema\n- may be used as dynamic children (e.g. in `list` or `dict-modifiable`)\n    - in that case the only key modifier is `children` which is list of it's keys\n    - USAGE: e.g. List of dictionaries where each dictionary have same structure.\n- if is not used as dynamic children then must have defined `\"key\"` under which are it's values stored\n- may be with or without `\"label\"` (only for GUI)\n    - `\"label\"` must be set to be able mark item as group with `\"is_group\"` key set to True\n- item with label can visually wrap it's children\n    - this option is enabled by default to turn off set `\"use_label_wrap\"` to `False`\n    - label wrap is by default collapsible\n        - that can be set with key `\"collapsible\"` to `True`/`False`\n        - with key `\"collapsed\"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`)\n    - it is possible to add lighter background with `\"highlight_content\"` (Default: `False`)\n        - lighter background has limits of maximum applies after 3-4 nested highlighted items there is not much difference in the color\n    - output is dictionary `{the \"key\": children values}`\n```\n# Example\n{\n    \"key\": \"applications\",\n    \"type\": \"dict\",\n    \"label\": \"Applications\",\n    \"collapsible\": true,\n    \"highlight_content\": true,\n    \"is_group\": true,\n    \"is_file\": true,\n    \"children\": [\n        ...ITEMS...\n    ]\n}\n\n# Without label\n{\n    \"type\": \"dict\",\n    \"key\": \"global\",\n    \"children\": [\n        ...ITEMS...\n    ]\n}\n\n# When used as widget\n{\n    \"type\": \"list\",\n    \"key\": \"profiles\",\n    \"label\": \"Profiles\",\n    \"object_type\": {\n        \"type\": \"dict\",\n        \"children\": [\n            {\n                \"key\": \"families\",\n                \"label\": \"Families\",\n                \"type\": \"list\",\n                \"object_type\": \"text\"\n            }, {\n                \"key\": \"hosts\",\n                \"label\": \"Hosts\",\n                \"type\": \"list\",\n                \"object_type\": \"text\"\n            }\n            ...\n        ]\n    }\n}\n```\n\n## dict-roots\n- entity can be used only in Project settings\n- keys of dictionary are based on current project roots\n- they are not updated \"live\" it is required to save root changes and then\n    modify values on this entity\n    # TODO do live updates\n```\n{\n    \"type\": \"dict-roots\",\n    \"key\": \"roots\",\n    \"label\": \"Roots\",\n    \"object_type\": {\n        \"type\": \"path\",\n        \"multiplatform\": true,\n        \"multipath\": false\n    }\n}\n```\n\n## dict-conditional\n- is similar to `dict` but has always available one enum entity\n    - the enum entity has single selection and it's value define other children entities\n- each value of enumerator have defined children that will be used\n    - there is no way how to have shared entities across multiple enum items\n- value from enumerator is also stored next to other values\n    - to define the key under which will be enum value stored use `enum_key`\n    - `enum_key` must match key regex and any enum item can't have children with same key\n    - `enum_label` is label of the entity for UI purposes\n- enum items are define with `enum_children`\n    - it's a list where each item represents single item for the enum\n    - all items in `enum_children` must have at least `key` key which represents value stored under `enum_key`\n    - enum items can define `label` for UI purposes\n    - most important part is that item can define `children` key where are definitions of it's children (`children` value works the same way as in `dict`)\n- to set default value for `enum_key` set it with `enum_default`\n- entity must have defined `\"label\"` if is not used as widget\n- is set as group if any parent is not group (can't have children as group)\n- may be with or without `\"label\"` (only for GUI)\n    - `\"label\"` must be set to be able mark item as group with `\"is_group\"` key set to True\n- item with label can visually wrap it's children\n    - this option is enabled by default to turn off set `\"use_label_wrap\"` to `False`\n    - label wrap is by default collapsible\n        - that can be set with key `\"collapsible\"` to `True`/`False`\n        - with key `\"collapsed\"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`)\n    - it is possible to add lighter background with `\"highlight_content\"` (Default: `False`)\n        - lighter background has limits of maximum applies after 3-4 nested highlighted items there is not much difference in the color\n- for UI porposes was added `enum_is_horizontal` which will make combobox appear next to children inputs instead of on top of them (Default: `False`)\n    - this has extended ability of `enum_on_right` which will move combobox to right side next to children widgets (Default: `False`)\n- output is dictionary `{the \"key\": children values}`\n- using this type as template item for list type can be used to create infinite hierarchies\n\n```\n# Example\n{\n    \"type\": \"dict-conditional\",\n    \"key\": \"my_key\",\n    \"label\": \"My Key\",\n    \"enum_key\": \"type\",\n    \"enum_label\": \"label\",\n    \"enum_children\": [\n        # Each item must be a dictionary with 'key'\n        {\n            \"key\": \"action\",\n            \"label\": \"Action\",\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"key\",\n                    \"label\": \"Key\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"label\",\n                    \"label\": \"Label\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"command\",\n                    \"label\": \"Comand\"\n                }\n            ]\n        },\n        {\n            \"key\": \"menu\",\n            \"label\": \"Menu\",\n            \"children\": [\n                {\n                    \"key\": \"children\",\n                    \"label\": \"Children\",\n                    \"type\": \"list\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            # Separator does not have children as \"separator\" value is enough\n            \"key\": \"separator\",\n            \"label\": \"Separator\"\n        }\n    ]\n}\n```\n\nHow output of the schema could look like on save:\n```\n{\n    \"type\": \"separator\"\n}\n\n{\n    \"type\": \"action\",\n    \"key\": \"action_1\",\n    \"label\": \"Action 1\",\n    \"command\": \"run command -arg\"\n}\n\n{\n    \"type\": \"menu\",\n    \"children\": [\n        \"child_1\",\n        \"child_2\"\n    ]\n}\n```\n\n## Inputs for setting any kind of value (`Pure` inputs)\n- all inputs must have defined `\"key\"` if are not used as dynamic item\n    - they can also have defined `\"label\"`\n\n### boolean\n- simple checkbox, nothing more to set\n```\n{\n    \"type\": \"boolean\",\n    \"key\": \"my_boolean_key\",\n    \"label\": \"Do you want to use Pype?\"\n}\n```\n\n### number\n- number input, can be used for both integer and float\n    - key `\"decimal\"` defines how many decimal places will be used, 0 is for integer input (Default: `0`)\n    - key `\"minimum\"` as minimum allowed number to enter (Default: `-99999`)\n    - key `\"maximum\"` as maximum allowed number to enter (Default: `99999`)\n- key `\"steps\"` will change single step value of UI inputs (using arrows and wheel scroll)\n- for UI it is possible to show slider to enable this option set `show_slider` to `true`\n```\n{\n    \"type\": \"number\",\n    \"key\": \"fps\",\n    \"label\": \"Frame rate (FPS)\"\n    \"decimal\": 2,\n    \"minimum\": 1,\n    \"maximum\": 300000\n}\n```\n\n```\n{\n    \"type\": \"number\",\n    \"key\": \"ratio\",\n    \"label\": \"Ratio\"\n    \"decimal\": 3,\n    \"minimum\": 0,\n    \"maximum\": 1,\n    \"show_slider\": true\n}\n```\n\n### text\n- simple text input\n    - key `\"multiline\"` allows to enter multiple lines of text (Default: `False`)\n    - key `\"placeholder\"` allows to show text inside input when is empty (Default: `None`)\n    - key `\"minimum_lines_count\"` allows to define minimum size hint for UI. Can be 0-n lines.\n\n```\n{\n    \"type\": \"text\",\n    \"key\": \"deadline_pool\",\n    \"label\": \"Deadline pool\"\n}\n```\n\n### path-input\n- this input is implemented to add additional features to text input\n- this is meant to be used in proxy input `path`\n    - DO NOT USE this input in schema please\n\n### raw-json\n- a little bit enhanced text input for raw json\n- can store dictionary (`{}`) or list (`[]`) but not both\n    - by default stores dictionary to change it to list set `is_list` to `True`\n- has validations of json format\n- output can be stored as string\n    - this is to allow any keys in dictionary\n    - set key `store_as_string` to `true`\n    - code using that setting must expected that value is string and use json module to convert it to python types\n\n```\n{\n    \"type\": \"raw-json\",\n    \"key\": \"profiles\",\n    \"label\": \"Extract Review profiles\",\n    \"is_list\": true\n}\n```\n\n### enum\n- enumeration of values that are predefined in schema\n- multiselection can be allowed with setting key `\"multiselection\"` to `True` (Default: `False`)\n- values are defined under value of key `\"enum_items\"` as list\n    - each item in list is simple dictionary where value is label and key is value which will be stored\n    - should be possible to enter single dictionary if order of items doesn't matter\n- it is possible to set default selected value/s with `default` attribute\n    - it is recommended to use this option only in single selection mode\n    - at the end this option is used only when defying default settings value or in dynamic items\n\n```\n{\n    \"key\": \"tags\",\n    \"label\": \"Tags\",\n    \"type\": \"enum\",\n    \"multiselection\": true,\n    \"enum_items\": [\n        {\"burnin\": \"Add burnins\"},\n        {\"ftrackreview\": \"Add to Ftrack\"},\n        {\"delete\": \"Delete output\"},\n        {\"slate-frame\": \"Add slate frame\"},\n        {\"no-handles\": \"Skip handle frames\"}\n    ]\n}\n```\n\n### anatomy-templates-enum\n- enumeration of all available anatomy template keys\n- have only single selection mode\n- it is possible to define default value `default`\n    - `\"work\"` is used if default value is not specified\n- enum values are not updated on the fly it is required to save templates and\n    reset settings to recache values\n```\n{\n    \"key\": \"host\",\n    \"label\": \"Host name\",\n    \"type\": \"anatomy-templates-enum\",\n    \"default\": \"publish\"\n}\n```\n\n### hosts-enum\n- enumeration of available hosts\n- multiselection can be allowed with setting key `\"multiselection\"` to `True` (Default: `False`)\n- it is possible to add empty value (represented with empty string) with setting `\"use_empty_value\"` to `True` (Default: `False`)\n- it is possible to set `\"custom_labels\"` for host names where key `\"\"` is empty value (Default: `{}`)\n- to filter host names it is required to define `\"hosts_filter\"` which is list of host names that will be available\n    - do not pass empty string if `use_empty_value` is enabled\n    - ignoring host names would be more dangerous in some cases\n```\n{\n    \"key\": \"host\",\n    \"label\": \"Host name\",\n    \"type\": \"hosts-enum\",\n    \"multiselection\": false,\n    \"use_empty_value\": true,\n    \"custom_labels\": {\n        \"\": \"N/A\",\n        \"nuke\": \"Nuke\"\n    },\n    \"hosts_filter\": [\n        \"nuke\"\n    ]\n}\n```\n\n### apps-enum\n- enumeration of available application and their variants from system settings\n    - applications without host name are excluded\n- can be used only in project settings\n- has only `multiselection`\n- used only in project anatomy\n```\n{\n    \"type\": \"apps-enum\",\n    \"key\": \"applications\",\n    \"label\": \"Applications\"\n}\n```\n\n### tools-enum\n- enumeration of available tools and their variants from system settings\n- can be used only in project settings\n- has only `multiselection`\n- used only in project anatomy\n```\n{\n    \"type\": \"tools-enum\",\n    \"key\": \"tools_env\",\n    \"label\": \"Tools\"\n}\n```\n\n### task-types-enum\n- enumeration of task types from current project\n- enum values are not updated on the fly and modifications of task types on project require save and reset to be propagated to this enum\n- has set `multiselection` to `True` but can be changed to `False` in schema\n\n### deadline_url-enum\n- deadline module specific enumerator using deadline system settings to fill it's values\n- TODO: move this type to deadline module\n\n## Inputs for setting value using Pure inputs\n- these inputs also have required `\"key\"`\n- attribute `\"label\"` is required in few conditions\n    - when item is marked `as_group` or when `use_label_wrap`\n- they use Pure inputs \"as widgets\"\n\n### list\n- output is list\n- items can be added and removed\n- items in list must be the same type\n- to wrap item in collapsible widget with label on top set `use_label_wrap` to `True`\n    - when this is used `collapsible` and `collapsed` can be set (same as `dict` item does)\n- type of items is defined with key `\"object_type\"`\n- there are 2 possible ways how to set the type:\n    1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `\"type\"` (example below)\n    2.) item type name as string without modifiers (e.g. `text`)\n    3.) enhancement of 1.) there is also support of `template` type but be carefull about endless loop of templates\n        - goal of using `template` is to easily change same item definitions in multiple lists\n\n1.) with item modifiers\n```\n{\n    \"type\": \"list\",\n    \"key\": \"exclude_ports\",\n    \"label\": \"Exclude ports\",\n    \"object_type\": {\n        \"type\": \"number\", # number item type\n        \"minimum\": 1, # minimum modifier\n        \"maximum\": 65535 # maximum modifier\n    }\n}\n```\n\n2.) without modifiers\n```\n{\n    \"type\": \"list\",\n    \"key\": \"exclude_ports\",\n    \"label\": \"Exclude ports\",\n    \"object_type\": \"text\"\n}\n```\n\n3.) with template definition\n```\n# Schema of list item where template is used\n{\n    \"type\": \"list\",\n    \"key\": \"menu_items\",\n    \"label\": \"Menu Items\",\n    \"object_type\": {\n        \"type\": \"template\",\n        \"name\": \"template_object_example\"\n    }\n}\n\n# WARNING:\n#  In this example the template use itself inside which will work in `list`\n#  but may cause an issue in other entity types (e.g. `dict`).\n\n'template_object_example.json' :\n[\n    {\n        \"type\": \"dict-conditional\",\n        \"use_label_wrap\": true,\n        \"collapsible\": true,\n        \"key\": \"menu_items\",\n        \"label\": \"Menu items\",\n        \"enum_key\": \"type\",\n        \"enum_label\": \"Type\",\n        \"enum_children\": [\n            {\n                \"key\": \"action\",\n                \"label\": \"Action\",\n                \"children\": [\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"key\",\n                        \"label\": \"Key\"\n                    }\n                ]\n            },\n            {\n                \"key\": \"menu\",\n                \"label\": \"Menu\",\n                \"children\": [\n                    {\n                        \"key\": \"children\",\n                        \"label\": \"Children\",\n                        \"type\": \"list\",\n                        \"object_type\": {\n                            \"type\": \"template\",\n                            \"name\": \"template_object_example\"\n                        }\n                    }\n                ]\n            }\n        ]\n    }\n]\n```\n\n### dict-modifiable\n- one of dictionary inputs, this is only used as value input\n- items in this input can be removed and added same way as in `list` input\n- value items in dictionary must be the same type\n- type of items is defined with key `\"object_type\"`\n- required keys may be defined under `\"required_keys\"`\n    - required keys must be defined as a list (e.g. `[\"key_1\"]`) and are moved to the top\n    - these keys can't be removed or edited (it is possible to edit label if item is collapsible)\n- there are 2 possible ways how to set the type:\n    1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `\"type\"` (example below)\n    2.) item type name as string without modifiers (e.g. `text`)\n- this input can be collapsible\n    - that can be set with key `\"collapsible\"` as `True`/`False` (Default: `True`)\n        - with key `\"collapsed\"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`)\n\n1.) with item modifiers\n```\n{\n    \"type\": \"dict-modifiable\",\n    \"object_type\": {\n        \"type\": \"number\",\n        \"minimum\": 0,\n        \"maximum\": 300\n    },\n    \"is_group\": true,\n    \"key\": \"templates_mapping\",\n    \"label\": \"Deadline - Templates mapping\",\n    \"is_file\": true\n}\n```\n\n2.) without modifiers\n```\n{\n    \"type\": \"dict-modifiable\",\n    \"object_type\": \"text\",\n    \"is_group\": true,\n    \"key\": \"templates_mapping\",\n    \"label\": \"Deadline - Templates mapping\",\n    \"is_file\": true\n}\n```\n\n### path\n- input for paths, use `path-input` internally\n- has 2 input modifiers `\"multiplatform\"` and `\"multipath\"`\n    - `\"multiplatform\"` - adds `\"windows\"`, `\"linux\"` and `\"darwin\"` path inputs result is dictionary\n    - `\"multipath\"` - it is possible to enter multiple paths\n    - if both are enabled result is dictionary with lists\n\n```\n{\n    \"type\": \"path\",\n    \"key\": \"ffmpeg_path\",\n    \"label\": \"FFmpeg path\",\n    \"multiplatform\": true,\n    \"multipath\": true\n}\n```\n\n### list-strict\n- input for strict number of items in list\n- each child item can be different type with different possible modifiers\n- it is possible to display them in horizontal or vertical layout\n    - key `\"horizontal\"` as `True`/`False` (Default: `True`)\n- each child may have defined `\"label\"` which is shown next to input\n    - label does not reflect modifications or overrides (TODO)\n- children item are defined under key `\"object_types\"` which is list of dictionaries\n    - key `\"children\"` is not used because is used for hierarchy validations in schema\n- USAGE: For colors, transformations, etc. Custom number and different modifiers\n  give ability to define if color is HUE or RGB, 0-255, 0-1, 0-100 etc.\n\n```\n{\n    \"type\": \"list-strict\",\n    \"key\": \"color\",\n    \"label\": \"Color\",\n    \"object_types\": [\n        {\n            \"label\": \"Red\",\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"maximum\": 255,\n            \"decimal\": 0\n        }, {\n            \"label\": \"Green\",\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"maximum\": 255,\n            \"decimal\": 0\n        }, {\n            \"label\": \"Blue\",\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"maximum\": 255,\n            \"decimal\": 0\n        }, {\n            \"label\": \"Alpha\",\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"maximum\": 1,\n            \"decimal\": 6\n        }\n    ]\n}\n```\n\n### color\n- preimplemented entity to store and load color values\n- entity store and expect list of 4 integers in range 0-255\n    - integers represents rgba [Red, Green, Blue, Alpha]\n\n```\n{\n    \"type\": \"color\",\n    \"key\": \"bg_color\",\n    \"label\": \"Background Color\"\n}\n```\n\n## Noninteractive widgets\n- have nothing to do with data\n\n### label\n- add label with note or explanations\n- it is possible to use html tags inside the label\n- set `work_wrap` to `true`/`false` if you want to enable word wrapping in UI (default: `false`)\n\n```\n{\n    \"type\": \"label\",\n    \"label\": \"<span style=\\\"color:#FF0000\\\";>RED LABEL:</span> Normal label\"\n}\n```\n\n### separator\n- legacy name is `splitter` (still usable)\n- visual separator of items (more divider than separator)\n\n```\n{\n    \"type\": \"separator\"\n}\n```\n\n## Anatomy\nAnatomy represents data stored on project document.\n\n### anatomy\n- entity works similarly to `dict`\n- anatomy has always all keys overridden with overrides\n    - overrides are not applied as all anatomy data must be available from project document\n    - all children must be groups\n\n## Proxy wrappers\n- should wraps multiple inputs only visually\n- these does not have `\"key\"` key and do not allow to have `\"is_file\"` or `\"is_group\"` modifiers enabled\n- can't be used as widget (first item in e.g. `list`, `dict-modifiable`, etc.)\n\n### form\n- wraps inputs into form look layout\n- should be used only for Pure inputs\n\n```\n{\n    \"type\": \"dict-form\",\n    \"children\": [\n        {\n            \"type\": \"text\",\n            \"key\": \"deadline_department\",\n            \"label\": \"Deadline apartment\"\n        }, {\n            \"type\": \"number\",\n            \"key\": \"deadline_priority\",\n            \"label\": \"Deadline priority\"\n        }, {\n           ...\n        }\n    ]\n}\n```\n\n\n### collapsible-wrap\n- wraps inputs into collapsible widget\n    - looks like `dict` but does not hold `\"key\"`\n- should be used only for Pure inputs\n\n```\n{\n    \"type\": \"collapsible-wrap\",\n    \"label\": \"Collapsible example\"\n    \"children\": [\n        {\n            \"type\": \"text\",\n            \"key\": \"_example_input_collapsible\",\n            \"label\": \"Example input in collapsible wrapper\"\n        }, {\n           ...\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_main.json",
    "content": "{\n    \"key\": \"project\",\n    \"type\": \"dict\",\n    \"children\": [\n        {\n            \"type\": \"anatomy\",\n            \"key\": \"project_anatomy\",\n            \"label\": \"Anatomy\",\n            \"children\": [\n                {\n                    \"key\": \"roots\",\n                    \"label\": \"Roots\",\n                    \"type\": \"dict-modifiable\",\n                    \"is_file\": true,\n                    \"is_group\": true,\n                    \"expandable\": false,\n                    \"object_type\": {\n                        \"type\": \"path\",\n                        \"multiplatform\": true\n                    }\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_anatomy_templates\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_anatomy_attributes\"\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"key\": \"tasks\",\n                    \"label\": \"Task types\",\n                    \"is_file\": true,\n                    \"is_group\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"short_name\",\n                                \"label\": \"Short name\"\n                            }\n                        ]\n                    }\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_anatomy_imageio\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"project_settings\",\n            \"children\": [\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_global\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_ftrack\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_shotgrid\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_kitsu\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_deadline\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_royalrender\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_slack\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_applications\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_max\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_maya\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_nuke\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_fusion\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_hiero\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_houdini\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_blender\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_aftereffects\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_photoshop\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_substancepainter\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_harmony\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_tvpaint\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_celaction\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_flame\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_resolve\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_standalonepublisher\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_traypublisher\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_webpublisher\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_unreal\"\n                },\n                {\n                    \"type\": \"schema\",\n                    \"name\": \"schema_project_equalizer\"\n                },\n                {\n                    \"type\": \"dynamic_schema\",\n                    \"name\": \"project_settings/global\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"aftereffects\",\n    \"label\": \"AfterEffects\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (OCIO managed)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_ocio\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"create\",\n            \"label\": \"Creator plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"RenderCreator\",\n                    \"label\": \"Create render\",\n                    \"children\": [\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default Variants\",\n                            \"object_type\": \"text\",\n                            \"docstring\": \"Fill default variant(s) (like 'Main' or 'Default') used in subset name creation.\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"mark_for_review\",\n                            \"label\": \"Review\",\n                            \"default\": true\n                         },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"force_setting_values\",\n                            \"label\": \"Force resolution and duration values from Asset\",\n                            \"default\": true\n                         }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectReview\",\n                    \"label\": \"Collect Review\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\",\n                            \"default\": true\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ValidateSceneSettings\",\n                    \"label\": \"Validate Scene Settings\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Validate if FPS and Resolution match shot data\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"skip_resolution_check\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Skip Resolution Check for Tasks\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"skip_timelines_check\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Skip Timeline Check  for Tasks\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"docstring\": \"Check if loaded container in scene are latest versions.\",\n                            \"key\": \"ValidateContainers\",\n                            \"label\": \"ValidateContainers\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_workfile_options\",\n            \"skip_paths\": [\n                \"workfile_builder/builder_on_start\",\n                \"workfile_builder/profiles\"\n            ]\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_templated_workfile_build\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_applications.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"applications\",\n    \"label\": \"Applications\",\n    \"collapsible\": true,\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"only_available\",\n            \"label\": \"Show only available applications\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_blender.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"blender\",\n    \"label\": \"Blender\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"unit_scale_settings\",\n            \"type\": \"dict\",\n            \"label\": \"Set Unit Scale\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"key\": \"apply_on_opening\",\n                    \"type\": \"boolean\",\n                    \"label\": \"Apply on Opening Existing Files\"\n                },\n                {\n                    \"key\": \"base_file_unit_scale\",\n                    \"type\": \"number\",\n                    \"label\": \"Base File Unit Scale\",\n                    \"decimal\": 10\n                }\n            ]\n        },\n        {\n            \"key\": \"set_resolution_startup\",\n            \"type\": \"boolean\",\n            \"label\": \"Set Resolution on Startup\"\n        },\n        {\n            \"key\": \"set_frames_startup\",\n            \"type\": \"boolean\",\n            \"label\": \"Set Start/End Frames and FPS on Startup\"\n        },\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (OCIO managed)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_ocio\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"RenderSettings\",\n            \"label\": \"Render Settings\",\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"default_render_image_folder\",\n                    \"label\": \"Default render image folder\"\n                },\n                {\n                    \"key\": \"aov_separator\",\n                    \"label\": \"AOV Separator Character\",\n                    \"type\": \"enum\",\n                    \"multiselection\": false,\n                    \"defaults\": \"underscore\",\n                    \"enum_items\": [\n                        {\"dash\": \"- (dash)\"},\n                        {\"underscore\": \"_ (underscore)\"},\n                        {\"dot\": \". (dot)\"}\n                    ]\n                },\n                {\n                    \"key\": \"image_format\",\n                    \"label\": \"Output Image Format\",\n                    \"type\": \"enum\",\n                    \"multiselection\": false,\n                    \"defaults\": \"exr\",\n                    \"enum_items\": [\n                        {\"exr\": \"OpenEXR\"},\n                        {\"bmp\": \"BMP\"},\n                        {\"rgb\": \"Iris\"},\n                        {\"png\": \"PNG\"},\n                        {\"jpg\": \"JPEG\"},\n                        {\"jp2\": \"JPEG 2000\"},\n                        {\"tga\": \"Targa\"},\n                        {\"tif\": \"TIFF\"}\n                    ]\n                },\n                {\n                    \"key\": \"multilayer_exr\",\n                    \"type\": \"boolean\",\n                    \"label\": \"Multilayer (EXR)\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Note: Multilayer EXR is only used when output format type set to EXR.\"\n                },\n                {\n                    \"key\": \"renderer\",\n                    \"label\": \"Renderer\",\n                    \"type\": \"enum\",\n                    \"multiselection\": false,\n                    \"defaults\": \"CYCLES\",\n                    \"enum_items\": [\n                        {\"CYCLES\": \"Cycles\"},\n                        {\"BLENDER_EEVEE\": \"Eevee\"}\n                    ]\n                },\n                {\n                    \"key\": \"compositing\",\n                    \"type\": \"boolean\",\n                    \"label\": \"Enable Compositing\"\n                },\n                {\n                    \"key\": \"aov_list\",\n                    \"label\": \"AOVs to create\",\n                    \"type\": \"enum\",\n                    \"multiselection\": true,\n                    \"defaults\": \"empty\",\n                    \"enum_items\": [\n                        {\"combined\": \"Combined\"},\n                        {\"z\": \"Z\"},\n                        {\"mist\": \"Mist\"},\n                        {\"normal\": \"Normal\"},\n                        {\"position\": \"Position (Cycles Only)\"},\n                        {\"vector\": \"Vector (Cycles Only)\"},\n                        {\"uv\": \"UV (Cycles Only)\"},\n                        {\"denoising\": \"Denoising Data (Cycles Only)\"},\n                        {\"object_index\": \"Object Index (Cycles Only)\"},\n                        {\"material_index\": \"Material Index (Cycles Only)\"},\n                        {\"sample_count\": \"Sample Count (Cycles Only)\"},\n                        {\"diffuse_light\": \"Diffuse Light/Direct\"},\n                        {\"diffuse_indirect\": \"Diffuse Indirect (Cycles Only)\"},\n                        {\"diffuse_color\": \"Diffuse Color\"},\n                        {\"specular_light\": \"Specular (Glossy) Light/Direct\"},\n                        {\"specular_indirect\": \"Specular (Glossy) Indirect (Cycles Only)\"},\n                        {\"specular_color\": \"Specular (Glossy) Color\"},\n                        {\"transmission_light\": \"Transmission Light/Direct (Cycles Only)\"},\n                        {\"transmission_indirect\": \"Transmission Indirect (Cycles Only)\"},\n                        {\"transmission_color\": \"Transmission Color (Cycles Only)\"},\n                        {\"volume_light\": \"Volume Light/Direct\"},\n                        {\"volume_indirect\": \"Volume Indirect (Cycles Only)\"},\n                        {\"emission\": \"Emission\"},\n                        {\"environment\": \"Environment\"},\n                        {\"shadow\": \"Shadow/Shadow Catcher\"},\n                        {\"ao\": \"Ambient Occlusion\"},\n                        {\"bloom\": \"Bloom (Eevee Only)\"},\n                        {\"transparent\": \"Transparent (Eevee Only)\"},\n                        {\"cryptomatte_object\": \"Cryptomatte Object\"},\n                        {\"cryptomatte_material\": \"Cryptomatte Material\"},\n                        {\"cryptomatte_asset\": \"Cryptomatte Asset\"},\n                        {\"cryptomatte_accurate\": \"Cryptomatte Accurate Mode (Eevee Only)\"}\n                    ]\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Add custom AOVs. They are added to the view layer and in the Compositing Nodetree,\\nbut they need to be added manually to the Shader Nodetree.\"\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"store_as_list\": true,\n                    \"key\": \"custom_passes\",\n                    \"label\": \"Custom Passes\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"type\",\n                                \"label\": \"Type\",\n                                \"type\": \"enum\",\n                                \"multiselection\": false,\n                                \"default\": \"COLOR\",\n                                \"enum_items\": [\n                                    {\"COLOR\": \"Color\"},\n                                    {\"VALUE\": \"Value\"}\n                                ]\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_workfile_options\",\n            \"skip_paths\": [\n                \"workfile_builder/builder_on_start\",\n                \"workfile_builder/profiles\"\n            ]\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_blender_publish\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"celaction\",\n    \"label\": \"CelAction\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (derived to OCIO)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_derived\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"workfile\",\n            \"label\": \"Workfile\",\n            \"children\": [\n                {\n                    \"key\": \"submission_overrides\",\n                    \"label\": \"Submission workfile overrides\",\n                    \"type\": \"enum\",\n                    \"multiselection\": true,\n                    \"enum_items\": [\n                        {\n                            \"render_chunk\": \"Pass chunk size\"\n                        },\n                        {\n                            \"frame_range\": \"Pass frame range\"\n                        },\n                        {\n                            \"resolution\": \"Pass resolution\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectRenderPath\",\n                    \"label\": \"CollectRenderPath\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"output_extension\",\n                            \"label\": \"Output render file extension\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"anatomy_template_key_render_files\",\n                            \"label\": \"Anatomy template key: render files\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"anatomy_template_key_metadata\",\n                            \"label\": \"Anatomy template key: metadata job file\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"deadline\",\n    \"label\": \"Deadline\",\n    \"collapsible\": true,\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"deadline_url-enum\",\n            \"key\": \"deadline_servers\",\n            \"label\": \"Deadline Webservice URLs\",\n            \"multiselect\": true\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectDefaultDeadlineServer\",\n                    \"label\": \"Default Deadline Webservice\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"pass_mongo_url\",\n                            \"label\": \"Pass Mongo url to job\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectDeadlinePools\",\n                    \"label\": \"Default Deadline Pools\",\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"primary_pool\",\n                            \"label\": \"Primary Pool\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"secondary_pool\",\n                            \"label\": \"Secondary Pool\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ValidateExpectedFiles\",\n                    \"label\": \"Validate Expected Files\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Validate if all expected files were rendered\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"allow_user_override\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Allow user change frame range\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"families\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Trigger on families\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"targets\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Trigger for plugins\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"MayaSubmitDeadline\",\n                    \"label\": \"Maya Submit to Deadline\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"enum\",\n                            \"key\": \"tile_assembler_plugin\",\n                            \"label\": \"Tile Assembler Plugin\",\n                            \"multiselection\": false,\n                            \"enum_items\": [\n                                {\n                                    \"DraftTileAssembler\": \"Draft Tile Assembler\"\n                                },\n                                {\n                                    \"OpenPypeTileAssembler\": \"OpenPype Tile Assembler\"\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"use_published\",\n                            \"label\": \"Use Published scene\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"import_reference\",\n                            \"label\": \"Use Scene with Imported Reference\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"asset_dependencies\",\n                            \"label\": \"Use Asset dependencies\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"priority\",\n                            \"label\": \"Priority\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"tile_priority\",\n                            \"label\": \"Tile Assembler Priority\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"group\",\n                            \"label\": \"Group Name\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"limit\",\n                            \"label\": \"Limit Groups\",\n                            \"object_type\": \"text\"\n                        },\n                        {\n                            \"type\": \"raw-json\",\n                            \"key\": \"jobInfo\",\n                            \"label\": \"Additional JobInfo data\"\n                        },\n                        {\n                            \"type\": \"raw-json\",\n                            \"key\": \"pluginInfo\",\n                            \"label\": \"Additional PluginInfo data\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"scene_patches\",\n                            \"label\": \"Scene patches\",\n                            \"required_keys\": [\"name\", \"regex\", \"line\"],\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"name\",\n                                        \"label\": \"Patch name\",\n                                        \"type\": \"text\"\n                                    }, {\n                                        \"key\": \"regex\",\n                                        \"label\": \"Patch regex\",\n                                        \"type\": \"text\"\n                                    }, {\n                                        \"key\": \"line\",\n                                        \"label\": \"Patch line\",\n                                        \"type\": \"text\"\n                                    }\n                                ]\n\n                            }\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"strict_error_checking\",\n                            \"label\": \"Strict Error Checking\",\n                            \"default\": true\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"HoudiniCacheSubmitDeadline\",\n                    \"label\": \"Houdini Submit cache to deadline\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"priority\",\n                            \"label\": \"Priority\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"chunk_size\",\n                            \"label\": \"Chunk Size\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"group\",\n                            \"label\": \"Group Name\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"HoudiniSubmitDeadline\",\n                    \"label\": \"Houdini Submit render to deadline\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"priority\",\n                            \"label\": \"Priority\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"chunk_size\",\n                            \"label\": \"Chunk Size\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"group\",\n                            \"label\": \"Group Name\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"export_priority\",\n                            \"label\": \"Export Priority\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"export_chunk_size\",\n                            \"label\": \"Export Chunk Size\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"export_group\",\n                            \"label\": \"Export Group\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"MaxSubmitDeadline\",\n                    \"label\": \"3dsMax Submit to Deadline\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"use_published\",\n                            \"label\": \"Use Published scene\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"priority\",\n                            \"label\": \"Priority\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"chunk_size\",\n                            \"label\": \"Frame per Task\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"group\",\n                            \"label\": \"Group Name\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"FusionSubmitDeadline\",\n                    \"label\": \"Fusion submit to Deadline\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"priority\",\n                            \"label\": \"Priority\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"chunk_size\",\n                            \"label\": \"Frame per Task\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"concurrent_tasks\",\n                            \"label\": \"Number of concurrent tasks\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"group\",\n                            \"label\": \"Group Name\"\n                        },\n                        {\n                            \"type\": \"enum\",\n                            \"key\": \"plugin\",\n                            \"label\": \"Deadline Plugin\",\n                            \"enum_items\": [\n                                {\"Fusion\": \"Fusion\"},\n                                {\"FusionCmd\": \"FusionCmd\"}\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"NukeSubmitDeadline\",\n                    \"label\": \"Nuke Submit to Deadline\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"priority\",\n                            \"label\": \"Priority\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"chunk_size\",\n                            \"label\": \"Chunk Size\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"concurrent_tasks\",\n                            \"label\": \"Number of concurrent tasks\"\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"group\",\n                            \"label\": \"Group\"\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"department\",\n                            \"label\": \"Department\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"use_gpu\",\n                            \"label\": \"Use GPU\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"workfile_dependency\",\n                            \"label\": \"Workfile Dependency\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"use_published_workfile\",\n                            \"label\": \"Use Published Workfile\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"env_allowed_keys\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Allowed environment keys\"\n                        },\n                        {\n                            \"type\": \"dict-modifiable\",\n                            \"key\": \"env_search_replace_values\",\n                            \"label\": \"Search & replace in environment values\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"dict-modifiable\",\n                            \"key\": \"limit_groups\",\n                            \"label\": \"Limit Groups\",\n                            \"object_type\": {\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"HarmonySubmitDeadline\",\n                    \"label\": \"Harmony Submit to Deadline\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"use_published\",\n                            \"label\": \"Use Published scene\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"priority\",\n                            \"label\": \"Priority\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"chunk_size\",\n                            \"label\": \"Chunk Size\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"group\",\n                            \"label\": \"Group\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"department\",\n                            \"label\": \"Department\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"AfterEffectsSubmitDeadline\",\n                    \"label\": \"After Effects Submit to Deadline\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"use_published\",\n                            \"label\": \"Use Published scene\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"priority\",\n                            \"label\": \"Priority\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"chunk_size\",\n                            \"label\": \"Chunk Size\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"group\",\n                            \"label\": \"Group\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"department\",\n                            \"label\": \"Department\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"multiprocess\",\n                            \"label\": \"Multiprocess\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"CelactionSubmitDeadline\",\n                    \"label\": \"Celaction Submit Deadline\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"deadline_department\",\n                            \"label\": \"Deadline apartment\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"deadline_priority\",\n                            \"label\": \"Deadline priority\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"deadline_pool\",\n                            \"label\": \"Deadline pool\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"deadline_pool_secondary\",\n                            \"label\": \"Deadline pool (secondary)\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"deadline_group\",\n                            \"label\": \"Deadline Group\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"deadline_chunk_size\",\n                            \"label\": \"Deadline Chunk size\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"deadline_job_delay\",\n                            \"label\": \"Delay job (timecode dd:hh:mm:ss)\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"BlenderSubmitDeadline\",\n                    \"label\": \"Blender Submit to Deadline\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"use_published\",\n                            \"label\": \"Use Published scene\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"priority\",\n                            \"label\": \"Priority\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"chunk_size\",\n                            \"label\": \"Frame per Task\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"group\",\n                            \"label\": \"Group Name\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"job_delay\",\n                            \"label\": \"Delay job (timecode dd:hh:mm:ss)\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ProcessSubmittedCacheJobOnFarm\",\n                    \"label\": \"ProcessSubmittedCacheJobOnFarm\",\n                    \"checkbox_key\": \"enabled\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"deadline_department\",\n                            \"label\": \"Deadline department\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"deadline_pool\",\n                            \"label\": \"Deadline Pool\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"deadline_group\",\n                            \"label\": \"Deadline Group\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"deadline_chunk_size\",\n                            \"label\": \"Deadline Chunk Size\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"deadline_priority\",\n                            \"label\": \"Deadline Priotity\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ProcessSubmittedJobOnFarm\",\n                    \"label\": \"ProcessSubmittedJobOnFarm\",\n                    \"checkbox_key\": \"enabled\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"deadline_department\",\n                            \"label\": \"Deadline department\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"deadline_pool\",\n                            \"label\": \"Deadline Pool\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"deadline_group\",\n                            \"label\": \"Deadline Group\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"deadline_chunk_size\",\n                            \"label\": \"Deadline Chunk Size\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"deadline_priority\",\n                            \"label\": \"Deadline Priotity\"\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"publishing_script\",\n                            \"label\": \"Publishing script path\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"skip_integration_repre_list\",\n                            \"label\": \"Skip integration of representation with ext\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"families_transfer\",\n                            \"label\": \"List of family names to transfer\\nto generated instances (AOVs for example).\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"dict-modifiable\",\n                            \"docstring\": \"Regular expression to filter for which subset review should be created in publish job.\",\n                            \"key\": \"aov_filter\",\n                            \"label\": \"Reviewable subsets filter\",\n                            \"object_type\": {\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_equalizer.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"equalizer\",\n    \"label\": \"3DEqualizer\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_equalizer_create\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_flame.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"flame\",\n    \"label\": \"Flame\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (remapped to OCIO)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_remapped\"\n                },\n                {\n                    \"key\": \"project\",\n                    \"type\": \"dict\",\n                    \"label\": \"Project\",\n                    \"collapsible\": false,\n                    \"children\": [\n                        {\n                            \"type\": \"form\",\n                            \"children\": [\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"colourPolicy\",\n                                    \"label\": \"Colour Policy (name or path)\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"frameDepth\",\n                                    \"label\": \"Image Depth\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"fieldDominance\",\n                                    \"label\": \"Field Dominance\"\n                                }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Profile names mapping settings is deprecated use <a href=\\\"settings://project_settings/flame/imageio/remapping\\\"><b>./imagio/remapping</b></a> instead\"\n                },\n                {\n                    \"key\": \"profilesMapping\",\n                    \"type\": \"dict\",\n                    \"label\": \"Profile names mapping [deprecated]\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"inputs\",\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"flameName\",\n                                        \"label\": \"Flame name\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"ocioName\",\n                                        \"label\": \"OCIO name\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"create\",\n            \"label\": \"Create plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CreateShotClip\",\n                    \"label\": \"Create Shot Clip\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                          \"type\": \"collapsible-wrap\",\n                          \"label\": \"Shot Hierarchy And Rename Settings\",\n                          \"collapsible\": false,\n                          \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"hierarchy\",\n                                \"label\": \"Shot parent hierarchy\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"useShotName\",\n                                \"label\": \"Use Shot Name\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"clipRename\",\n                                \"label\": \"Rename clips\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"clipName\",\n                                \"label\": \"Clip name template\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"segmentIndex\",\n                                \"label\": \"Accept segment order\"\n                            },\n                            {\n                                \"type\": \"number\",\n                                \"key\": \"countFrom\",\n                                \"label\": \"Count sequence from\"\n                            },\n                            {\n                                \"type\": \"number\",\n                                \"key\": \"countSteps\",\n                                \"label\": \"Stepping number\"\n                            }\n                          ]\n                        },\n                        {\n                          \"type\": \"collapsible-wrap\",\n                          \"label\": \"Shot Template Keywords\",\n                          \"collapsible\": false,\n                          \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"folder\",\n                                \"label\": \"{folder}\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"episode\",\n                                \"label\": \"{episode}\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"sequence\",\n                                \"label\": \"{sequence}\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"track\",\n                                \"label\": \"{track}\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"shot\",\n                                \"label\": \"{shot}\"\n                            }\n                          ]\n                        },\n                        {\n                          \"type\": \"collapsible-wrap\",\n                          \"label\": \"Vertical Synchronization Of Attributes\",\n                          \"collapsible\": false,\n                          \"children\": [\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"vSyncOn\",\n                                \"label\": \"Enable Vertical Sync\"\n                            }\n                          ]\n                        },\n                        {\n                          \"type\": \"collapsible-wrap\",\n                          \"label\": \"Shot Attributes\",\n                          \"collapsible\": false,\n                          \"children\": [\n                            {\n                                \"type\": \"number\",\n                                \"key\": \"workfileFrameStart\",\n                                \"label\": \"Workfiles Start Frame\"\n                            },\n                            {\n                                \"type\": \"number\",\n                                \"key\": \"handleStart\",\n                                \"label\": \"Handle start (head)\"\n                            },\n                            {\n                                \"type\": \"number\",\n                                \"key\": \"handleEnd\",\n                                \"label\": \"Handle end (tail)\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"includeHandles\",\n                                \"label\": \"Enable handles including\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"retimedHandles\",\n                                \"label\": \"Enable retimed handles\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"retimedFramerange\",\n                                \"label\": \"Enable retimed shot frameranges\"\n                            }\n                          ]\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectTimelineInstances\",\n                    \"label\": \"Collect Timeline Instances\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"collapsible-wrap\",\n                            \"label\": \"XML presets attributes parsable from segment comments\",\n                            \"collapsible\": true,\n                            \"collapsed\": true,\n                            \"children\": [\n                                {\n                                    \"type\": \"list\",\n                                    \"key\": \"xml_preset_attrs_from_comments\",\n                                    \"object_type\": {\n                                        \"type\": \"dict\",\n                                        \"children\": [\n                                            {\n                                                \"type\": \"text\",\n                                                \"key\": \"name\",\n                                                \"label\": \"Attribute name\"\n                                            },\n                                            {\n                                                \"key\": \"type\",\n                                                \"label\": \"Attribute type\",\n                                                \"type\": \"enum\",\n                                                \"default\": \"number\",\n                                                \"enum_items\": [\n                                                    {\n                                                        \"number\": \"number\"\n                                                    },\n                                                    {\n                                                        \"float\": \"float\"\n                                                    },\n                                                    {\n                                                        \"string\": \"string\"\n                                                    }\n                                                ]\n                                            }\n                                        ]\n                                    }\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"collapsible-wrap\",\n                            \"label\": \"Add tasks\",\n                            \"collapsible\": true,\n                            \"collapsed\": true,\n                            \"children\": [\n                                {\n                                    \"type\": \"list\",\n                                    \"key\": \"add_tasks\",\n                                    \"object_type\": {\n                                        \"type\": \"dict\",\n                                        \"children\": [\n                                            {\n                                                \"type\": \"text\",\n                                                \"key\": \"name\",\n                                                \"label\": \"Task name\"\n                                            },\n                                            {\n                                                \"key\": \"type\",\n                                                \"label\": \"Task type\",\n                                                \"multiselection\": false,\n                                                \"type\": \"task-types-enum\"\n                                            },\n                                            {\n                                                \"type\": \"boolean\",\n                                                \"key\": \"create_batch_group\",\n                                                \"label\": \"Create batch group\"\n                                            }\n                                        ]\n                                    }\n                                }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ExtractSubsetResources\",\n                    \"label\": \"Extract Subset Resources\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"keep_original_representation\",\n                            \"label\": \"Publish clip's original media\"\n                        },\n                        {\n                            \"key\": \"export_presets_mapping\",\n                            \"label\": \"Export presets mapping\",\n                            \"type\": \"dict-modifiable\",\n                            \"highlight_content\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"active\",\n                                        \"label\": \"Is active\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"separator\"\n                                    },\n                                    {\n                                        \"key\": \"export_type\",\n                                        \"label\": \"Eport clip type\",\n                                        \"type\": \"enum\",\n                                        \"default\": \"Sequence Publish\",\n                                        \"enum_items\": [\n                                            {\n                                                \"Movie\": \"Movie\"\n                                            },\n                                            {\n                                                \"File Sequence\": \"File Sequence\"\n                                            },\n                                            {\n                                                \"Sequence Publish\": \"Sequence Publish\"\n                                            }\n                                        ]\n                                    },\n                                    {\n                                        \"key\": \"ext\",\n                                        \"label\": \"Output extension\",\n                                        \"type\": \"text\",\n                                        \"default\": \"exr\"\n                                    },\n                                    {\n                                        \"key\": \"xml_preset_file\",\n                                        \"label\": \"XML preset file (with ext)\",\n                                        \"type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"colorspace_out\",\n                                        \"label\": \"Output color\",\n                                        \"type\": \"text\",\n                                        \"default\": \"linear\"\n                                    },\n                                    {\n                                        \"type\": \"collapsible-wrap\",\n                                        \"label\": \"Other parameters\",\n                                        \"collapsible\": true,\n                                        \"collapsed\": true,\n                                        \"children\": [\n                                            {\n                                                \"key\": \"xml_preset_dir\",\n                                                \"label\": \"XML preset folder (optional)\",\n                                                \"type\": \"text\"\n                                            },\n                                            {\n                                                \"type\": \"separator\"\n                                            },\n                                            {\n                                                \"type\": \"boolean\",\n                                                \"key\": \"parsed_comment_attrs\",\n                                                \"label\": \"Include parsed attributes from comments\",\n                                                \"default\": false\n\n                                            },\n                                            {\n                                                \"type\": \"separator\"\n                                            },\n                                            {\n                                                \"type\": \"collapsible-wrap\",\n                                                \"label\": \"Representation\",\n                                                \"collapsible\": true,\n                                                \"collapsed\": true,\n                                                \"children\": [\n                                                    {\n                                                        \"type\": \"boolean\",\n                                                        \"key\": \"representation_add_range\",\n                                                        \"label\": \"Add frame range to representation\"\n                                                    },\n                                                    {\n                                                        \"type\": \"list\",\n                                                        \"key\": \"representation_tags\",\n                                                        \"label\": \"Add representation tags\",\n                                                        \"object_type\": {\n                                                            \"type\": \"text\",\n                                                            \"multiline\": false\n                                                        }\n                                                    }\n                                                ]\n                                            },\n                                            {\n                                                \"type\": \"collapsible-wrap\",\n                                                \"label\": \"Loading during publish\",\n                                                \"collapsible\": true,\n                                                \"collapsed\": true,\n                                                \"children\": [\n                                                    {\n                                                        \"type\": \"boolean\",\n                                                        \"key\": \"load_to_batch_group\",\n                                                        \"label\": \"Load to batch group reel\",\n                                                        \"default\": false\n                                                    },\n                                                    {\n                                                        \"type\": \"text\",\n                                                        \"key\": \"batch_group_loader_name\",\n                                                        \"label\": \"Use loader name\"\n                                                    }\n                                                ]\n                                            }\n\n                                        ]\n                                    },\n                                    {\n                                        \"type\": \"collapsible-wrap\",\n                                        \"label\": \"Filtering\",\n                                        \"collapsible\": true,\n                                        \"collapsed\": true,\n                                        \"children\": [\n                                            {\n                                                \"key\": \"filter_path_regex\",\n                                                \"label\": \"Regex in clip path\",\n                                                \"type\": \"text\",\n                                                \"default\": \".*\"\n                                            }\n                                        ]\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"IntegrateBatchGroup\",\n                    \"label\": \"IntegrateBatchGroup\",\n                    \"is_group\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"load\",\n            \"label\": \"Loader plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"LoadClip\",\n                    \"label\": \"Load Clip\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"families\",\n                            \"label\": \"Families\",\n                            \"object_type\": \"text\"\n                        },\n                        {\n                            \"type\": \"separator\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"reel_group_name\",\n                            \"label\": \"Reel group name\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"reel_name\",\n                            \"label\": \"Reel name\"\n                        },\n                        {\n                            \"type\": \"separator\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"clip_name_template\",\n                            \"label\": \"Clip name template\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"layer_rename_template\",\n                            \"label\": \"Layer name template\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"layer_rename_patterns\",\n                            \"label\": \"Layer rename patters\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"LoadClipBatch\",\n                    \"label\": \"Load as clip to current batch\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"families\",\n                            \"label\": \"Families\",\n                            \"object_type\": \"text\"\n                        },\n                        {\n                            \"type\": \"separator\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"reel_name\",\n                            \"label\": \"Reel name\"\n                        },\n                        {\n                            \"type\": \"separator\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"clip_name_template\",\n                            \"label\": \"Clip name template\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"layer_rename_template\",\n                            \"label\": \"Layer name template\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"layer_rename_patterns\",\n                            \"label\": \"Layer rename patters\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"ftrack\",\n    \"label\": \"Ftrack\",\n    \"collapsible\": true,\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"dict\",\n            \"key\": \"events\",\n            \"label\": \"Server Actions/Events\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"sync_to_avalon\",\n                    \"label\": \"Sync to avalon\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Allow name and hierarchy change only if following statuses are on all children tasks\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"prepare_project\",\n                    \"label\": \"Prepare Project\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"sync_hier_entity_attributes\",\n                    \"label\": \"Sync Hierarchical and Entity Attributes\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"interest_entity_types\",\n                            \"label\": \"Entity types of interest\",\n                            \"object_type\": {\n                                \"type\": \"text\",\n                                \"multiline\": false\n                            }\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"interest_attributes\",\n                            \"label\": \"Attributes to sync\",\n                            \"object_type\": {\n                                \"type\": \"text\",\n                                \"multiline\": false\n                            }\n                        },\n                        {\n                            \"type\": \"separator\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"action_enabled\",\n                            \"label\": \"Enable Action\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles for action\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"clone_review_session\",\n                    \"label\": \"Clone Review Session\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles for action\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"thumbnail_updates\",\n                    \"label\": \"Update Hierarchy thumbnails\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Push thumbnail from version, up through multiple hierarchy levels.\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"levels\",\n                            \"label\": \"Levels\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"user_assignment\",\n                    \"label\": \"Run script on user assignments\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"status_update\",\n                    \"label\": \"Update status on task action\",\n                    \"is_group\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"key\": \"mapping\",\n                            \"type\": \"dict-modifiable\",\n                            \"object_type\": {\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"status_task_to_parent\",\n                    \"label\": \"Sync status from Task to Parent\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"List of parent object types where this is triggered (\\\"Shot\\\", \\\"Asset Build\\\", etc.). Skipped if list is empty.\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"object_type\": \"text\",\n                            \"key\": \"parent_object_types\",\n                            \"label\": \"Object types\"\n                        },\n                        {\n                            \"key\": \"parent_status_match_all_task_statuses\",\n                            \"type\": \"dict-modifiable\",\n                            \"label\": \"Change parent if all tasks match\",\n                            \"object_type\": {\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"parent_status_by_task_status\",\n                            \"label\": \"Change parent status if a single task matches\",\n                            \"use_label_wrap\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"text\",\n                                        \"label\": \"New parent status\",\n                                        \"key\": \"new_status\"\n                                    },\n                                    {\n                                        \"type\": \"separator\"\n                                    },\n                                    {\n                                        \"type\": \"list\",\n                                        \"label\": \"Task status\",\n                                        \"key\": \"task_statuses\",\n                                        \"object_type\": \"text\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"status_task_to_version\",\n                    \"label\": \"Sync status from Task to Version\",\n                    \"is_group\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"dict-modifiable\",\n                            \"key\": \"mapping\",\n                            \"object_type\":\n                            {\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Limit<b/> status changes to entered asset types. Limitation is ignored if nothing is entered.\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"asset_types_filter\",\n                            \"label\": \"Asset types (short)\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"status_version_to_task\",\n                    \"label\": \"Sync status from Version to Task\",\n                    \"is_group\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Change Task status based on a changed Version status.</b><br/>Version's new status on the <b>left</b> will trigger a change of a task status to the first available from the list on <b>right</b>.<br/> - if no status from the list is available it will use the same status as the version.\"\n                        },\n                        {\n                            \"type\": \"dict-modifiable\",\n                            \"key\": \"mapping\",\n                            \"object_type\": {\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"separator\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Disable<b/> event if status was changed on specific Asset type.\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"label\": \"Asset types (short)\",\n                            \"key\": \"asset_types_to_skip\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"next_task_update\",\n                    \"is_group\": true,\n                    \"label\": \"Update status on next task\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Change status on next task by <b>task types order</b> when task status state changed to \\\"Done\\\". All tasks with same Task type must be \\\"Done\\\".\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Mapping of next task status changes <b>From</b> -> <b>To</b>.\"\n                        },\n                        {\n                            \"type\": \"dict-modifiable\",\n                            \"key\": \"mapping\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"separator\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Status names that are ignored on \\\"Done\\\" check (e.g. \\\"Omitted\\\").\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"ignored_statuses\",\n                            \"object_type\": \"text\"\n                        },\n                        {\n                            \"type\": \"separator\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Allow to break rule that all tasks with same Task type must be \\\"Done\\\" and change statuses with same type tasks ordered by name.\"\n                        },\n                        {\n                            \"label\": \"Name sorting\",\n                            \"type\": \"boolean\",\n                            \"key\": \"name_sorting\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"transfer_values_of_hierarchical_attributes\",\n                    \"label\": \"Action to transfer hierarchical attribute values\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"create_daily_review_session\",\n                    \"label\": \"Create daily review session\",\n                    \"type\": \"dict\",\n                    \"is_group\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\",\n                            \"use_label_wrap\": true\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"cycle_enabled\",\n                            \"label\": \"Run automatically every day\"\n                        },\n                        {\n                            \"type\": \"separator\"\n                        },\n                        {\n                            \"type\": \"list-strict\",\n                            \"key\": \"cycle_hour_start\",\n                            \"label\": \"Create daily review session at\",\n                            \"tooltip\": \"This may take affect on next day\",\n                            \"object_types\": [\n                                {\n                                    \"label\": \"H:\",\n                                    \"type\": \"number\",\n                                    \"minimum\": 0,\n                                    \"maximum\": 23,\n                                    \"decimal\": 0\n                                }, {\n                                    \"label\": \"M:\",\n                                    \"type\": \"number\",\n                                    \"minimum\": 0,\n                                    \"maximum\": 59,\n                                    \"decimal\": 0\n                                }, {\n                                    \"label\": \"S:\",\n                                    \"type\": \"number\",\n                                    \"minimum\": 0,\n                                    \"maximum\": 59,\n                                    \"decimal\": 0\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"This can't be overriden per project and any change will take effect on the next day or on restart of event server.\"\n                        },\n                        {\n                            \"type\": \"separator\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"review_session_template\",\n                            \"label\": \"ReviewSession template\",\n                            \"placeholder\": \"Default: {yy}{mm}{dd}\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Possible formatting keys in template:<br/>- \\\"project_name\\\" - &lt;Name of project&gt;<br/>- \\\"d\\\" - &lt;Day of month number&gt; in shortest possible way.<br/>- \\\"dd\\\" - &lt;Day of month number&gt; with 2 digits.<br/>- \\\"ddd\\\" - &lt;Week day name&gt; shortened week day. e.g.: `Mon`, ...<br/>- \\\"dddd\\\" - &lt;Week day name&gt; full name of week day. e.g.: `Monday`, ...<br/>- \\\"m\\\" - &lt;Month number&gt; in shortest possible way. e.g.: `1` if January<br/>- \\\"mm\\\" - &lt;Month number&gt; with 2 digits.<br/>- \\\"mmm\\\" - &lt;Month name&gt; shortened month name. e.g.: `Jan`, ...<br/>- \\\"mmmm\\\" -&lt;Month name&gt; full month name. e.g.: `January`, ...<br/>- \\\"yy\\\" - &lt;Year number&gt; shortened year. e.g.: `19`, `20`, ...<br/>- \\\"yyyy\\\" - &lt;Year number&gt; full year. e.g.: `2019`, `2020`, ...\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"user_handlers\",\n            \"label\": \"User Actions/Events\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"application_launch_statuses\",\n                    \"is_group\": true,\n                    \"label\": \"Application - Status change on launch\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Do not change status if current status is:</b>\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"ignored_statuses\",\n                            \"object_type\": \"text\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Change task's status to <b>left side</b> if current task status is in list on <b>right side</b>.\"\n                        },\n                        {\n                            \"type\": \"dict-modifiable\",\n                            \"key\": \"status_change\",\n                            \"object_type\": {\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"create_update_attributes\",\n                    \"label\": \"Create/Update Avalon Attributes\",\n                    \"children\": [\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"prepare_project\",\n                    \"label\": \"Prepare Project\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        },\n                        {\n                          \"type\": \"separator\"\n                        },\n                        {\n                          \"type\": \"label\",\n                          \"label\": \"Check \\\"Create project structure\\\" by default\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"create_project_structure_checked\",\n                            \"label\": \"Checked\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"clean_hierarchical_attr\",\n                    \"label\": \"Clean hierarchical custom attributes\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"delete_asset_subset\",\n                    \"label\": \"Delete Asset/Subsets\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"delete_old_versions\",\n                    \"label\": \"Delete old versions\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"delivery_action\",\n                    \"label\": \"Delivery\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"store_thubmnail_to_avalon\",\n                    \"label\": \"Store Thumbnails to avalon\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"job_killer\",\n                    \"label\": \"Job Killer\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"sync_to_avalon_local\",\n                    \"label\": \"Sync to avalon (local) - For development\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"fill_workfile_attribute\",\n                    \"label\": \"Fill workfile Custom attribute\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Custom attribute must be <b>Text</b> type added to <b>Task</b> entity type\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"custom_attribute_key\",\n                            \"label\": \"Custom attribute key\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"seed_project\",\n                    \"label\": \"Seed Debug Project\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"role_list\",\n                            \"label\": \"Roles\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"CollectFtrackFamily\",\n                    \"label\": \"Collect Ftrack Family\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"collapsible\": true,\n                            \"key\": \"profiles\",\n                            \"label\": \"Profiles\",\n                            \"use_label_wrap\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"hosts\",\n                                        \"label\": \"Host names\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"families\",\n                                        \"label\": \"Families\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"task_types\",\n                                        \"label\": \"Task types\",\n                                        \"type\": \"task-types-enum\"\n                                    },\n                                    {\n                                        \"key\": \"tasks\",\n                                        \"label\": \"Task names\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"separator\"\n                                    },\n                                    {\n                                        \"key\": \"add_ftrack_family\",\n                                        \"label\": \"Add Ftrack Family\",\n                                        \"type\": \"boolean\"\n                                    },\n                                    {\n                                        \"type\": \"list\",\n                                        \"collapsible\": true,\n                                        \"key\": \"advanced_filtering\",\n                                        \"label\": \"Advanced adding if additional families present\",\n                                        \"use_label_wrap\": true,\n                                        \"object_type\": {\n                                            \"type\": \"dict\",\n                                            \"children\": [\n                                                {\n                                                    \"key\": \"families\",\n                                                    \"label\": \"Additional Families\",\n                                                    \"type\": \"list\",\n                                                    \"object_type\": \"text\"\n                                                },\n                                                {\n                                                    \"key\": \"add_ftrack_family\",\n                                                    \"label\": \"Add Ftrack Family\",\n                                                    \"type\": \"boolean\"\n                                                }\n                                            ]\n                                        }\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"CollectFtrackCustomAttributeData\",\n                    \"label\": \"Collect Custom Attribute Data\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Collect custom attributes from ftrack for ftrack entities that can be used in some templates during publishing.\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"custom_attribute_keys\",\n                            \"label\": \"Custom attribute keys\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"IntegrateHierarchyToFtrack\",\n                    \"label\": \"Integrate Hierarchy to ftrack\",\n                    \"is_group\": true,\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Set task status on new task creation. Ftrack's default status is used otherwise.\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"create_task_status_profiles\",\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"task_types\",\n                                        \"label\": \"Task types\",\n                                        \"type\": \"task-types-enum\"\n                                    },\n                                    {\n                                        \"key\": \"task_names\",\n                                        \"label\": \"Task names\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"status_name\",\n                                        \"label\": \"Status name\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"IntegrateFtrackNote\",\n                    \"label\": \"IntegrateFtrackNote\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Template may contain formatting keys <b>intent</b>, <b>comment</b>, <b>host_name</b>, <b>app_name</b>, <b>app_label</b>, <b>published_paths</b> and <b>source</b>.\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"note_template\",\n                            \"label\": \"Note template\",\n                            \"multiline\": true\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"object_type\": \"text\",\n                            \"key\": \"note_labels\",\n                            \"label\": \"Note labels\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"IntegrateFtrackDescription\",\n                    \"label\": \"Integrate Ftrack Description\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Add description to integrated AssetVersion.\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Template may contain formatting keys <b>intent</b> and <b>comment</b>.\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"description_template\",\n                            \"label\": \"Description template\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"ValidateFtrackAttributes\",\n                    \"label\": \"ValidateFtrackAttributes\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"raw-json\",\n                            \"key\": \"ftrack_custom_attributes\",\n                            \"label\": \"Custom attributes to validate\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"IntegrateFtrackComponentOverwrite\",\n                    \"label\": \"IntegrateFtrackComponentOverwrite\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"IntegrateFtrackInstance\",\n                    \"label\": \"Integrate Ftrack Instance\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"dict-modifiable\",\n                            \"key\": \"family_mapping\",\n                            \"label\": \"Family Mapping\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"keep_first_subset_name_for_review\",\n                            \"label\": \"Make subset name as first asset name\",\n                            \"default\": true\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"collapsible\": true,\n                            \"key\": \"asset_versions_status_profiles\",\n                            \"label\": \"AssetVersion status on publish\",\n                            \"use_label_wrap\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"hosts\",\n                                        \"label\": \"Host names\",\n                                        \"type\": \"hosts-enum\",\n                                        \"multiselection\": true\n                                    },\n                                    {\n                                        \"key\": \"task_types\",\n                                        \"label\": \"Task types\",\n                                        \"type\": \"task-types-enum\"\n                                    },\n                                    {\n                                        \"key\": \"family\",\n                                        \"label\": \"Family\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"separator\"\n                                    },\n                                    {\n                                        \"key\": \"status\",\n                                        \"label\": \"Status name\",\n                                        \"type\": \"text\"\n                                    }\n                                ]\n                            }\n                        },\n                        {\n                            \"key\": \"additional_metadata_keys\",\n                            \"label\": \"Additional metadata keys on components\",\n                            \"type\": \"enum\",\n                            \"multiselection\": true,\n                            \"enum_items\": [\n                                {\"openpype_version\": \"OpenPype version\"},\n                                {\"frame_start\": \"Frame start\"},\n                                {\"frame_end\": \"Frame end\"},\n                                {\"duration\": \"Duration\"},\n                                {\"width\": \"Resolution width\"},\n                                {\"height\": \"Resolution height\"},\n                                {\"fps\": \"FPS\"},\n                                {\"code\": \"Codec\"}\n                            ]\n                        },\n                        {\n                            \"type\": \"separator\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"upload_reviewable_with_origin_name\",\n                            \"label\": \"Upload reviewable with origin name\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Note:</b> Reviewable will be uploaded twice into ftrack when enabled. One with original name and second with required 'ftrackreview-mp4'. That may cause dramatic increase of ftrack storage usage.\"\n                        },\n                        {\n                            \"type\": \"separator\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"IntegrateFtrackFarmStatus\",\n                    \"label\": \"Ftrack Status To Farm\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Change status of task when it's subset is submitted to farm\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"collapsible\": true,\n                            \"key\": \"farm_status_profiles\",\n                            \"label\": \"Profiles\",\n                            \"use_label_wrap\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"hosts\",\n                                        \"label\": \"Host names\",\n                                        \"type\": \"hosts-enum\",\n                                        \"multiselection\": true\n                                    },\n                                    {\n                                        \"key\": \"task_types\",\n                                        \"label\": \"Task types\",\n                                        \"type\": \"task-types-enum\"\n                                    },\n                                    {\n                                        \"key\": \"task_names\",\n                                        \"label\": \"Task names\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"families\",\n                                        \"label\": \"Families\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"subsets\",\n                                        \"label\": \"Subset names\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"separator\"\n                                    },\n                                    {\n                                        \"key\": \"status_name\",\n                                        \"label\": \"Status name\",\n                                        \"type\": \"text\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"ftrack_task_status_local_publish\",\n                    \"label\": \"Ftrack Status Local Integration\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Change status of task when is integrated locally\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"collapsible\": true,\n                            \"key\": \"status_profiles\",\n                            \"label\": \"Profiles\",\n                            \"use_label_wrap\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"host_names\",\n                                        \"label\": \"Host names\",\n                                        \"type\": \"hosts-enum\",\n                                        \"multiselection\": true\n                                    },\n                                    {\n                                        \"key\": \"task_types\",\n                                        \"label\": \"Task types\",\n                                        \"type\": \"task-types-enum\"\n                                    },\n                                    {\n                                        \"key\": \"task_names\",\n                                        \"label\": \"Task names\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"families\",\n                                        \"label\": \"Families\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"subset_names\",\n                                        \"label\": \"Subset names\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"separator\"\n                                    },\n                                    {\n                                        \"key\": \"status_name\",\n                                        \"label\": \"Status name\",\n                                        \"type\": \"text\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"ftrack_task_status_on_farm_publish\",\n                    \"label\": \"Ftrack Status On Farm\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Change status of task when it's subset is integrated on farm\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"collapsible\": true,\n                            \"key\": \"status_profiles\",\n                            \"label\": \"Profiles\",\n                            \"use_label_wrap\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"host_names\",\n                                        \"label\": \"Host names\",\n                                        \"type\": \"hosts-enum\",\n                                        \"multiselection\": true\n                                    },\n                                    {\n                                        \"key\": \"task_types\",\n                                        \"label\": \"Task types\",\n                                        \"type\": \"task-types-enum\"\n                                    },\n                                    {\n                                        \"key\": \"task_names\",\n                                        \"label\": \"Task names\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"families\",\n                                        \"label\": \"Families\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"subset_names\",\n                                        \"label\": \"Subset names\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"separator\"\n                                    },\n                                    {\n                                        \"key\": \"status_name\",\n                                        \"label\": \"Status name\",\n                                        \"type\": \"text\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"IntegrateFtrackTaskStatus\",\n                    \"label\": \"Integrate Ftrack Task Status\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Apply collected task statuses. This plugin can run before or after version integration. Some status automations may conflict with status changes on versions because of wrong order.\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"after_version_statuses\",\n                            \"label\": \"After version integration\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"fusion\",\n    \"label\": \"Fusion\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (OCIO managed)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_ocio\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"copy_fusion_settings\",\n            \"collapsible\": true,\n            \"label\": \"Local Fusion profile settings\",\n            \"children\": [\n                {\n                    \"key\": \"copy_path\",\n                    \"type\": \"path\",\n                    \"label\": \"Local Fusion profile directory\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"copy_status\",\n                    \"label\": \"Copy profile on first launch\"\n                },\n                {\n                    \"key\":\"force_sync\",\n                    \"type\": \"boolean\",\n                    \"label\": \"Resync profile on each launch\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"hooks\",\n            \"label\": \"Hooks\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"InstallPySideToFusion\",\n                    \"label\": \"Install PySide2\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"create\",\n            \"label\": \"Creator plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CreateSaver\",\n                    \"label\": \"Create Render Saver\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"temp_rendering_path_template\",\n                            \"label\": \"Temporary rendering path template\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default variants\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"key\": \"instance_attributes\",\n                            \"label\": \"Instance attributes\",\n                            \"type\": \"enum\",\n                            \"multiselection\": true,\n                            \"enum_items\": [\n                                {\n                                    \"reviewable\": \"Reviewable\"\n                                },\n                                {\n                                    \"farm_rendering\": \"Farm rendering\"\n                                }\n                            ]\n                        },\n                        {\n                            \"key\": \"image_format\",\n                            \"label\": \"Output Image Format\",\n                            \"type\": \"enum\",\n                            \"multiselect\": false,\n                            \"enum_items\": [\n                                {\"exr\": \"exr\"},\n                                {\"tga\": \"tga\"},\n                                {\"png\": \"png\"},\n                                {\"tif\": \"tif\"},\n                                {\"jpg\": \"jpg\"}\n                            ]\n                        },\n                        {\n                            \"key\": \"default_frame_range_option\",\n                            \"label\": \"Default frame range source\",\n                            \"type\": \"enum\",\n                            \"multiselect\": false,\n                            \"enum_items\": [\n                                {\"asset_db\": \"Current asset context\"},\n                                {\"render_range\": \"From render in/out\"},\n                                {\"comp_range\": \"From composition timeline\"}\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CreateImageSaver\",\n                    \"label\": \"Create Image Saver\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"temp_rendering_path_template\",\n                            \"label\": \"Temporary rendering path template\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default variants\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"key\": \"instance_attributes\",\n                            \"label\": \"Instance attributes\",\n                            \"type\": \"enum\",\n                            \"multiselection\": true,\n                            \"enum_items\": [\n                                {\n                                    \"reviewable\": \"Reviewable\"\n                                },\n                                {\n                                    \"farm_rendering\": \"Farm rendering\"\n                                }\n                            ]\n                        },\n                        {\n                            \"key\": \"image_format\",\n                            \"label\": \"Output Image Format\",\n                            \"type\": \"enum\",\n                            \"multiselect\": false,\n                            \"enum_items\": [\n                                {\"exr\": \"exr\"},\n                                {\"tga\": \"tga\"},\n                                {\"png\": \"png\"},\n                                {\"tif\": \"tif\"},\n                                {\"jpg\": \"jpg\"}\n                            ]\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"default_frame\",\n                            \"label\": \"Default rendered frame\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"key\": \"ValidateSaverResolution\",\n                            \"label\": \"Validate Saver Resolution\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_global.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"global\",\n    \"label\": \"Global\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"dict\",\n            \"key\": \"version_start_category\",\n            \"label\": \"Version Start\",\n            \"collapsible\": true,\n            \"collapsible_key\": true,\n            \"children\": [\n                {\n                  \"type\": \"list\",\n                  \"collapsible\": true,\n                  \"key\": \"profiles\",\n                  \"label\": \"Profiles\",\n                  \"object_type\": {\n                      \"type\": \"dict\",\n                      \"children\": [\n                          {\n                              \"key\": \"host_names\",\n                              \"label\": \"Host names\",\n                              \"type\": \"hosts-enum\",\n                              \"multiselection\": true\n                          },\n                          {\n                              \"key\": \"task_types\",\n                              \"label\": \"Task types\",\n                              \"type\": \"task-types-enum\"\n                          },\n                          {\n                              \"key\": \"task_names\",\n                              \"label\": \"Task names\",\n                              \"type\": \"list\",\n                              \"object_type\": \"text\"\n                          },\n                          {\n                              \"key\": \"families\",\n                              \"label\": \"Families\",\n                              \"type\": \"list\",\n                              \"object_type\": \"text\"\n                          },\n                          {\n                              \"key\": \"subsets\",\n                              \"label\": \"Subset names\",\n                              \"type\": \"list\",\n                              \"object_type\": \"text\"\n                          },\n                          {\n                              \"key\": \"version_start\",\n                              \"label\": \"Version Start\",\n                              \"type\": \"number\",\n                              \"minimum\": 0\n                          }\n                      ]\n                  }\n                }\n            ]\n        },\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"label\",\n                    \"label\": \"It's important to note that once color management is activated on a project, all hosts will be color managed by default. <br>The OpenColorIO (OCIO) config file is used either from the global settings or from the host's overrides. It's worth <br>noting that the order of the defined configuration paths matters, with higher priority given to paths listed earlier in <br>the configuration list.<br><br>To avoid potential issues, ensure that the OCIO configuration path is not an absolute path and includes at least <br>the root token (Anatomy). This helps ensure that the configuration path remains valid across different environments and <br>avoids any hard-coding of paths that may be specific to one particular system.<br><br><b><a href='https://ayon.ynput.io/docs/admin_colorspace' style=\\\"color:#00d6a1\\\";>Related documentation.</a></b>\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"activate_global_color_management\",\n                    \"label\": \"Enable Color Management\"\n                },\n                {\n                    \"key\": \"ocio_config\",\n                    \"type\": \"dict\",\n                    \"label\": \"OCIO config\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"path\",\n                            \"key\": \"filepath\",\n                            \"label\": \"Config path\",\n                            \"multiplatform\": false,\n                            \"multipath\": true\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"file_rules\",\n                    \"type\": \"dict\",\n                    \"label\": \"File Rules (OCIO v1 only)\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"activate_global_file_rules\",\n                            \"label\": \"Enable File Rules\"\n                        },\n                        {\n                            \"key\": \"rules\",\n                            \"label\": \"Rules\",\n                            \"type\": \"dict-modifiable\",\n                            \"highlight_content\": true,\n                            \"collapsible\": false,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"pattern\",\n                                        \"label\": \"Regex pattern\",\n                                        \"type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"colorspace\",\n                                        \"label\": \"Colorspace name\",\n                                        \"type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"ext\",\n                                        \"label\": \"File extension\",\n                                        \"type\": \"text\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                }\n\n            ]\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_global_publish\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_global_tools\"\n        },\n        {\n            \"type\": \"raw-json\",\n            \"label\": \"Project Folder Structure\",\n            \"key\": \"project_folder_structure\",\n            \"use_label_wrap\": true,\n            \"store_as_string\": true\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_project_syncserver\"\n        },\n        {\n            \"key\": \"project_plugins\",\n            \"type\": \"path\",\n            \"label\": \"Additional Project Plugin Paths\",\n            \"multiplatform\": true,\n            \"multipath\": true,\n            \"use_label_wrap\": true\n        },\n        {\n            \"key\": \"project_environments\",\n            \"type\": \"raw-json\",\n            \"label\": \"Additional Project Environments (set on application launch)\",\n            \"use_label_wrap\": true\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"harmony\",\n    \"label\": \"Harmony\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (OCIO managed)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_ocio\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectPalettes\",\n                    \"label\": \"Collect Palettes\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Set regular expressions to filter triggering on specific task names. '.*' means on all.\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"allowed_tasks\",\n                            \"label\": \"Allowed tasks\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"docstring\": \"Check if scene contains audio track.\",\n                            \"key\": \"ValidateAudio\",\n                            \"label\": \"ValidateAudio\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"docstring\": \"Check if loaded container is scene are latest versions.\",\n                            \"key\": \"ValidateContainers\",\n                            \"label\": \"ValidateContainers\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ValidateSceneSettings\",\n                    \"label\": \"Validate Scene Settings\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Validate if FrameStart, FrameEnd and Resolution match shot data in DB.\\n Use regular expressions to limit validations only on particular asset or task names.\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"frame_check_filter\",\n                            \"label\": \"Skip Frame check for Assets with\",\n                            \"object_type\": \"text\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"skip_resolution_check\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Skip Resolution Check for Tasks\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"skip_timelines_check\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Skip Timeline Check for Tasks\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"hiero\",\n    \"label\": \"Hiero\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (OCIO managed)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_ocio\"\n                },\n                {\n                    \"key\": \"workfile\",\n                    \"type\": \"dict\",\n                    \"label\": \"Workfile\",\n                    \"collapsible\": false,\n                    \"children\": [\n                        {\n                            \"type\": \"form\",\n                            \"children\": [\n                                {\n                                    \"type\": \"enum\",\n                                    \"key\": \"ocioConfigName\",\n                                    \"label\": \"OpenColorIO Config\",\n                                    \"enum_items\": [\n                                        {\n                                            \"nuke-default\": \"nuke-default\"\n                                        },\n                                        {\n                                            \"aces_1.0.3\": \"aces_1.0.3 (12)\"\n                                        },\n                                        {\n                                            \"aces_1.1\": \"aces_1.1 (12, 13)\"\n                                        },\n                                        {\n                                            \"aces_1.2\": \"aces_1.2 (13, 14)\"\n                                        },\n                                        {\n                                            \"studio-config-v1.0.0_aces-v1.3_ocio-v2.1\": \"studio-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)\"\n                                        },\n                                        {\n                                            \"cg-config-v1.0.0_aces-v1.3_ocio-v2.1\": \"cg-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)\"\n                                        }\n                                    ]\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"workingSpace\",\n                                    \"label\": \"Working Space\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"viewerLut\",\n                                    \"label\": \"Viewer\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"thumbnailLut\",\n                                    \"label\": \"Thumbnails\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"monitorOutLut\",\n                                    \"label\": \"Monitor\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"eightBitLut\",\n                                    \"label\": \"8 Bit Files\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"sixteenBitLut\",\n                                    \"label\": \"16 Bit Files\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"logLut\",\n                                    \"label\": \"Log Files\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"floatLut\",\n                                    \"label\": \"Floating Point Files\"\n                                }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"regexInputs\",\n                    \"type\": \"dict\",\n                    \"label\": \"Colorspace on Inputs by regex detection\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"inputs\",\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"regex\",\n                                        \"label\": \"Regex\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"colorspace\",\n                                        \"label\": \"Colorspace\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"create\",\n            \"label\": \"Create plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CreateShotClip\",\n                    \"label\": \"Create Shot Clip\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"collapsible-wrap\",\n                            \"label\": \"Shot Hierarchy And Rename Settings\",\n                            \"collapsible\": false,\n                            \"children\": [\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"hierarchy\",\n                                    \"label\": \"Shot parent hierarchy\"\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"clipRename\",\n                                    \"label\": \"Rename clips\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"clipName\",\n                                    \"label\": \"Clip name template\"\n                                },\n                                {\n                                    \"type\": \"number\",\n                                    \"key\": \"countFrom\",\n                                    \"label\": \"Count sequence from\"\n                                },\n                                {\n                                    \"type\": \"number\",\n                                    \"key\": \"countSteps\",\n                                    \"label\": \"Stepping number\"\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"collapsible-wrap\",\n                            \"label\": \"Shot Template Keywords\",\n                            \"collapsible\": false,\n                            \"children\": [\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"folder\",\n                                    \"label\": \"{folder}\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"episode\",\n                                    \"label\": \"{episode}\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"sequence\",\n                                    \"label\": \"{sequence}\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"track\",\n                                    \"label\": \"{track}\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"shot\",\n                                    \"label\": \"{shot}\"\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"collapsible-wrap\",\n                            \"label\": \"Vertical Synchronization Of Attributes\",\n                            \"collapsible\": false,\n                            \"children\": [\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"vSyncOn\",\n                                    \"label\": \"Enable Vertical Sync\"\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"collapsible-wrap\",\n                            \"label\": \"Shot Attributes\",\n                            \"collapsible\": false,\n                            \"children\": [\n                                {\n                                    \"type\": \"number\",\n                                    \"key\": \"workfileFrameStart\",\n                                    \"label\": \"Workfiles Start Frame\"\n                                },\n                                {\n                                    \"type\": \"number\",\n                                    \"key\": \"handleStart\",\n                                    \"label\": \"Handle start (head)\"\n                                },\n                                {\n                                    \"type\": \"number\",\n                                    \"key\": \"handleEnd\",\n                                    \"label\": \"Handle end (tail)\"\n                                }\n                            ]\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"load\",\n            \"label\": \"Loader plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"LoadClip\",\n                    \"label\": \"Load Clip\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"families\",\n                            \"label\": \"Families\",\n                            \"object_type\": \"text\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"clip_name_template\",\n                            \"label\": \"Clip name template\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"CollectInstanceVersion\",\n                    \"label\": \"Collect Instance Version\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"ExtractReviewCutUpVideo\",\n                    \"label\": \"Extract Review Cut Up Video\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"object_type\": \"text\",\n                            \"key\": \"tags_addition\",\n                            \"label\": \"Tags addition\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"CollectClipEffects\",\n                    \"label\": \"Collect Clip Effects\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"dict-modifiable\",\n                            \"key\": \"effect_categories\",\n                            \"label\": \"Effect Categories\",\n                            \"object_type\": {\n                                \"type\": \"list\",\n                                \"key\": \"effects_classes\",\n                                \"object_type\": \"text\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_publish_gui_filter\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_scriptsmenu\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"houdini\",\n    \"label\": \"Houdini\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_houdini_general\"\n        },\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (OCIO managed)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_ocio\"\n                }\n            ]\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_houdini_scriptshelf\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_houdini_create\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_houdini_publish\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"kitsu\",\n    \"label\": \"Kitsu\",\n    \"collapsible\": true,\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"dict\",\n            \"key\": \"entities_naming_pattern\",\n            \"label\": \"Entities naming pattern\",\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"episode\",\n                    \"label\": \"Episode:\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"sequence\",\n                    \"label\": \"Sequence:\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"shot\",\n                    \"label\": \"Shot:\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Integrator\"\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"IntegrateKitsuNote\",\n                    \"label\": \"Integrate Kitsu Note\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"set_status_note\",\n                            \"label\": \"Set status with note\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"note_status_shortname\",\n                            \"label\": \"Note shortname\"\n                        },\n                        {\n                            \"type\": \"dict\",\n                            \"collapsible\": true,\n                            \"key\": \"status_change_conditions\",\n                            \"label\": \"Status change conditions\",\n                            \"children\": [\n                                 {\n                                    \"type\": \"list\",\n                                    \"key\": \"status_conditions\",\n                                    \"label\": \"Status conditions\",\n                                    \"object_type\": {\n                                        \"type\": \"dict\",\n                                        \"key\": \"condition_dict\",\n                                        \"children\": [\n                                            {\n                                                \"type\": \"enum\",\n                                                \"key\": \"condition\",\n                                                \"label\": \"Condition\",\n                                                \"enum_items\": [\n                                                    {\"equal\": \"Equal\"},\n                                                    {\"not_equal\": \"Not equal\"}\n                                                ]\n                                            },\n                                            {\n                                                \"type\": \"text\",\n                                                \"key\": \"short_name\",\n                                                \"label\": \"Short name\"\n                                            }\n                                        ]\n                                    }\n                                },\n                                {\n                                    \"type\": \"list\",\n                                    \"key\": \"family_requirements\",\n                                    \"label\": \"Family requirements\",\n                                    \"object_type\": {\n                                        \"type\": \"dict\",\n                                        \"key\": \"requirement_dict\",\n                                        \"children\": [\n                                            {\n                                                \"type\": \"enum\",\n                                                \"key\": \"condition\",\n                                                \"label\": \"Condition\",\n                                                \"enum_items\": [\n                                                    {\"equal\": \"Equal\"},\n                                                    {\"not_equal\": \"Not equal\"}\n                                                ]\n                                            },\n                                            {\n                                                \"type\": \"text\",\n                                                \"key\": \"family\",\n                                                \"label\": \"Family\"\n                                            }\n                                        ]\n                                    }\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"dict\",\n                            \"collapsible\": true,\n                            \"checkbox_key\": \"enabled\",\n                            \"key\": \"custom_comment_template\",\n                            \"label\": \"Custom Comment Template\",\n                            \"children\": [\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"enabled\",\n                                    \"label\": \"Enabled\"\n                                },\n                                {\n                                    \"type\": \"label\",\n                                    \"label\": \"Kitsu supports markdown and here you can create a custom comment template.<br/>You can use data from your publishing instance's data.\"\n                                },\n                                {\n                                    \"key\": \"comment_template\",\n                                    \"type\": \"text\",\n                                    \"multiline\": true,\n                                    \"label\": \"Custom comment\"\n                                }\n                            ]\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_max.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"max\",\n    \"label\": \"Max\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"unit_scale_settings\",\n            \"type\": \"dict\",\n            \"label\": \"Set Unit Scale\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"key\": \"scene_unit_scale\",\n                    \"label\": \"Scene Unit Scale\",\n                    \"type\": \"enum\",\n                    \"multiselection\": false,\n                    \"defaults\": \"exr\",\n                    \"enum_items\": [\n                      {\"Millimeters\": \"mm\"},\n                      {\"Centimeters\": \"cm\"},\n                      {\"Meters\":  \"m\"},\n                      {\"Kilometers\":  \"km\"}\n                    ]\n                }\n            ]\n        },\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (OCIO managed)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_ocio\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"RenderSettings\",\n            \"label\": \"Render Settings\",\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"default_render_image_folder\",\n                    \"label\": \"Default render image folder\"\n                },\n                {\n                    \"key\": \"aov_separator\",\n                    \"label\": \"AOV Separator character\",\n                    \"type\": \"enum\",\n                    \"multiselection\": false,\n                    \"default\": \"underscore\",\n                    \"enum_items\": [\n                        {\"dash\":  \"- (dash)\"},\n                        {\"underscore\":  \"_ (underscore)\"},\n                        {\"dot\": \". (dot)\"}\n                    ]\n                },\n                {\n                  \"key\": \"image_format\",\n                  \"label\": \"Output Image Format\",\n                  \"type\": \"enum\",\n                  \"multiselection\": false,\n                  \"defaults\": \"exr\",\n                  \"enum_items\": [\n                    {\"bmp\": \"bmp\"},\n                    {\"exr\":  \"exr\"},\n                    {\"tif\":  \"tif\"},\n                    {\"tiff\": \"tiff\"},\n                    {\"jpg\": \"jpg\"},\n                    {\"png\":  \"png\"},\n                    {\"tga\":  \"tga\"},\n                    {\"dds\":  \"dds\"}\n                  ]\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"multipass\",\n                    \"label\": \"multipass\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateReview\",\n            \"label\": \"Create Review\",\n            \"children\": [\n                {\n                    \"type\": \"number\",\n                    \"key\": \"review_width\",\n                    \"label\": \"Review Width\"\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"review_height\",\n                    \"label\": \"Review Height\"\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"percentSize\",\n                    \"label\": \"Percent of Output\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"keep_images\",\n                    \"label\": \"Keep Image Sequences\"\n                },\n                {\n                  \"key\": \"image_format\",\n                  \"label\": \"Image Format Options\",\n                  \"type\": \"enum\",\n                  \"multiselection\": false,\n                  \"defaults\": \"exr\",\n                  \"enum_items\": [\n                    {\"exr\": \"exr\"},\n                    {\"jpg\": \"jpg\"},\n                    {\"png\": \"png\"},\n                    {\"tga\": \"tga\"}\n                  ]\n                },\n                {\n                    \"key\": \"visual_style\",\n                    \"label\": \"Preference\",\n                    \"type\": \"enum\",\n                    \"multiselection\": false,\n                    \"defaults\": \"Realistic\",\n                    \"enum_items\": [\n                      {\"Realistic\": \"Realistic\"},\n                      {\"Shaded\": \"Shaded\"},\n                      {\"Facets\": \"Facets\"},\n                      {\"ConsistentColors\": \"ConsistentColors\"},\n                      {\"HiddenLine\": \"HiddenLine\"},\n                      {\"Wireframe\": \"Wireframe\"},\n                      {\"BoundingBox\": \"BoundingBox\"},\n                      {\"Ink\": \"Ink\"},\n                      {\"ColorInk\": \"ColorInk\"},\n                      {\"Acrylic\": \"Acrylic\"},\n                      {\"Tech\": \"Tech\"},\n                      {\"Graphite\": \"Graphite\"},\n                      {\"ColorPencil\": \"ColorPencil\"},\n                      {\"Pastel\": \"Pastel\"},\n                      {\"Clay\": \"Clay\"},\n                      {\"ModelAssist\": \"ModelAssist\"}\n                    ]\n                },\n                {\n                    \"key\": \"viewport_preset\",\n                    \"label\": \"Pre-View Preset\",\n                    \"type\": \"enum\",\n                    \"multiselection\": false,\n                    \"defaults\": \"Quality\",\n                    \"enum_items\": [\n                      {\"Quality\": \"Quality\"},\n                      {\"Standard\": \"Standard\"},\n                      {\"Performance\": \"Performance\"},\n                      {\"DXMode\": \"DXMode\"},\n                      {\"Customize\": \"Customize\"}\n                    ]\n                },\n                {\n                    \"key\": \"anti_aliasing\",\n                    \"label\": \"Anti-aliasing Quality\",\n                    \"type\": \"enum\",\n                    \"multiselection\": false,\n                    \"defaults\": \"None\",\n                    \"enum_items\": [\n                      {\"None\": \"None\"},\n                      {\"2X\": \"2X\"},\n                      {\"4X\": \"4X\"},\n                      {\"8X\": \"8X\"}\n                    ]\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"vp_texture\",\n                    \"label\": \"Viewport Texture\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"PointCloud\",\n            \"label\": \"Point Cloud\",\n            \"children\": [\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Define the channel attribute names before exporting as PRT\"\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"collapsible\": true,\n                    \"key\": \"attribute\",\n                    \"label\": \"Channel Attribute\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"text\"\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_max_publish\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_maya.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"maya\",\n    \"label\": \"Maya\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"open_workfile_post_initialization\",\n            \"label\": \"Open Workfile Post Initialization\"\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"explicit_plugins_loading\",\n            \"label\": \"Explicit Plugins Loading\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"plugins_to_load\",\n                    \"label\": \"Plugins To Load\",\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                          {\n                              \"type\": \"boolean\",\n                              \"key\": \"enabled\",\n                              \"label\": \"Enabled\"\n                          },\n                          {\n                              \"type\": \"text\",\n                              \"key\": \"name\",\n                              \"label\": \"Name\"\n                          }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (OCIO managed)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_ocio\"\n                },\n                {\n                    \"key\": \"workfile\",\n                    \"type\": \"dict\",\n                    \"label\": \"Workfile\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"renderSpace\",\n                            \"label\": \"Rendering Space\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"displayName\",\n                            \"label\": \"Display\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"viewName\",\n                            \"label\": \"View\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"collapsible-wrap\",\n                    \"label\": \"<b>[Deprecated] please migrate all to 'Workfile' and enable it.</b>\",\n                    \"collapsible\": true,\n                    \"collapsed\": true,\n                    \"children\": [\n                        {\n                            \"key\": \"colorManagementPreference_v2\",\n                            \"type\": \"dict\",\n                            \"label\": \"[DEPRECATED] Color Management Preference v2 (Maya 2022+)\",\n                            \"collapsible\": true,\n                            \"checkbox_key\": \"enabled\",\n                            \"children\": [\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"enabled\",\n                                    \"label\": \"Use Color Management Preference v2\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"renderSpace\",\n                                    \"label\": \"Rendering Space\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"displayName\",\n                                    \"label\": \"Display\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"viewName\",\n                                    \"label\": \"View\"\n                                }\n                            ]\n                        },\n                        {\n                            \"key\": \"colorManagementPreference\",\n                            \"type\": \"dict\",\n                            \"label\": \"[DEPRECATED] Color Management Preference (legacy)\",\n                            \"collapsible\": true,\n                            \"children\": [\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"renderSpace\",\n                                    \"label\": \"Rendering Space\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"viewTransform\",\n                                    \"label\": \"Viewer Transform (workfile/viewName)\"\n                                }\n                            ]\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"text\",\n            \"multiline\" : true,\n            \"use_label_wrap\": true,\n            \"key\": \"mel_workspace\",\n            \"label\": \"Maya MEL Workspace\"\n         },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"ext_mapping\",\n            \"label\": \"Extension Mapping\",\n            \"use_label_wrap\": true,\n            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n        },\n        {\n           \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"maya-dirmap\",\n            \"label\": \"Maya Directory Mapping\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"use_env_var_as_root\",\n                    \"label\": \"Use env var placeholder in referenced paths\",\n                    \"docstring\": \"Use ${} placeholder instead of absolute value of a root in referenced filepaths.\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"paths\",\n                    \"children\": [\n                        {\n                            \"type\": \"list\",\n                            \"object_type\": \"text\",\n                            \"key\": \"source-path\",\n                            \"label\": \"Source Path\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"object_type\": \"text\",\n                            \"key\": \"destination-path\",\n                            \"label\": \"Destination Path\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"include_handles\",\n            \"collapsible\": true,\n            \"label\": \"Include/Exclude Handles in default playback & render range\",\n            \"children\": [\n                {\n                    \"key\": \"include_handles_default\",\n                    \"label\": \"Include handles by default\",\n                    \"type\": \"boolean\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"per_task_type\",\n                    \"label\": \"Include/exclude handles by task type\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"task-types-enum\",\n                                \"key\": \"task_type\",\n                                \"label\": \"Task types\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"include_handles\",\n                                \"label\": \"Include handles\"\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_scriptsmenu\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_maya_render_settings\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_maya_create\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_maya_publish\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_maya_load\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_workfile_build\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_templated_workfile_build\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"nuke\",\n    \"label\": \"Nuke\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"general\",\n            \"label\": \"General\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"menu\",\n                    \"label\": \"OpenPype Menu shortcuts\",\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"create\",\n                            \"label\": \"Create...\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"publish\",\n                            \"label\": \"Publish...\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"load\",\n                            \"label\": \"Load...\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"manage\",\n                            \"label\": \"Manage...\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"build_workfile\",\n                            \"label\": \"Build Workfile\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_nuke_imageio\"\n        },\n        {\n           \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"nuke-dirmap\",\n            \"label\": \"Nuke Directory Mapping\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"paths\",\n                    \"children\": [\n                        {\n                            \"type\": \"list\",\n                            \"object_type\": \"text\",\n                            \"key\": \"source-path\",\n                            \"label\": \"Source Path\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"object_type\": \"text\",\n                            \"key\": \"destination-path\",\n                            \"label\": \"Destination Path\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_scriptsmenu\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_nuke_scriptsgizmo\"\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"create\",\n            \"label\": \"Creator plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CreateWriteRender\",\n                    \"label\": \"CreateWriteRender\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"temp_rendering_path_template\",\n                            \"label\": \"Temporary rendering path template\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default variants\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"schema_template\",\n                            \"name\": \"template_nuke_write_attrs\"\n                        },\n                        {\n                            \"key\": \"prenodes\",\n                            \"label\": \"Pre write nodes\",\n                            \"type\": \"dict-modifiable\",\n                            \"highlight_content\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"nodeclass\",\n                                        \"label\": \"Node class\",\n                                        \"type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"dependent\",\n                                        \"label\": \"Outside node dependency\",\n                                        \"type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"schema_template\",\n                                        \"name\": \"template_nuke_knob_inputs\",\n                                        \"template_data\": [\n                                            {\n                                                \"label\": \"Node knobs\",\n                                                \"key\": \"knobs\"\n                                            }\n                                        ]\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CreateWritePrerender\",\n                    \"label\": \"CreateWritePrerender\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"temp_rendering_path_template\",\n                            \"label\": \"Temporary rendering path template\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default variants\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"schema_template\",\n                            \"name\": \"template_nuke_write_attrs\"\n                        },\n                        {\n                            \"key\": \"prenodes\",\n                            \"label\": \"Pre write nodes\",\n                            \"type\": \"dict-modifiable\",\n                            \"highlight_content\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"nodeclass\",\n                                        \"label\": \"Node class\",\n                                        \"type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"dependent\",\n                                        \"label\": \"Outside node dependency\",\n                                        \"type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"schema_template\",\n                                        \"name\": \"template_nuke_knob_inputs\",\n                                        \"template_data\": [\n                                            {\n                                                \"label\": \"Node knobs\",\n                                                \"key\": \"knobs\"\n                                            }\n                                        ]\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CreateWriteImage\",\n                    \"label\": \"CreateWriteImage\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"temp_rendering_path_template\",\n                            \"label\": \"Temporary rendering path template\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default variants\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"schema_template\",\n                            \"name\": \"template_nuke_write_attrs\"\n                        },\n                        {\n                            \"key\": \"prenodes\",\n                            \"label\": \"Pre write nodes\",\n                            \"type\": \"dict-modifiable\",\n                            \"highlight_content\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"nodeclass\",\n                                        \"label\": \"Node class\",\n                                        \"type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"dependent\",\n                                        \"label\": \"Outside node dependency\",\n                                        \"type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"schema_template\",\n                                        \"name\": \"template_nuke_knob_inputs\",\n                                        \"template_data\": [\n                                            {\n                                                \"label\": \"Node knobs\",\n                                                \"key\": \"knobs\"\n                                            }\n                                        ]\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_nuke_publish\",\n            \"template_data\": []\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_nuke_load\",\n            \"template_data\": []\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_workfile_options\"\n        },\n        {\n            \"type\": \"label\",\n            \"label\": \"^ Settings and for <span style=\\\"color:#FF0000\\\";><b>Workfile Builder</b></span> is deprecated and will be soon removed.  <br> Please use <b>Template Workfile Build Settings</b> instead.\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_templated_workfile_build\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"photoshop\",\n    \"label\": \"Photoshop\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (remapped to OCIO)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_remapped\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"create\",\n            \"label\": \"Creator plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ImageCreator\",\n                    \"label\": \"Create Image\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Manually create instance from layer or group of layers. \\n Separate review could be created for this image to be sent to Asset Management System.\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active_on_create\",\n                            \"label\": \"Active by default\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"mark_for_review\",\n                            \"label\": \"Review by default\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default Variants\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"AutoImageCreator\",\n                    \"label\": \"Create Flatten Image\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Auto create image for all visible layers, used for simplified processing. \\n Separate review could be created for this image to be sent to Asset Management System.\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active_on_create\",\n                            \"label\": \"Active by default\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"mark_for_review\",\n                            \"label\": \"Review by default\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"default_variant\",\n                            \"label\": \"Default variant\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ReviewCreator\",\n                    \"label\": \"Create Review\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Auto create review instance containing all published image instances or visible layers if no image instance.\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\",\n                            \"default\": true\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active_on_create\",\n                            \"label\": \"Active by default\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"default_variant\",\n                            \"label\": \"Default variant\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"WorkfileCreator\",\n                    \"label\": \"Create Workfile\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Auto create workfile instance\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active_on_create\",\n                            \"label\": \"Active by default\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"default_variant\",\n                            \"label\": \"Default variant\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"is_group\": true,\n                    \"key\": \"CollectColorCodedInstances\",\n                    \"label\": \"Collect Color Coded Instances\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Set color for publishable layers, set its resulting family and template for subset name. \\nCan create flatten image from published instances.(Applicable only for remote publishing!)\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\",\n                            \"default\": true\n                        },\n                        {\n                            \"key\": \"create_flatten_image\",\n                            \"label\": \"Create flatten image\",\n                            \"type\": \"enum\",\n                            \"multiselection\": false,\n                            \"enum_items\": [\n                                { \"flatten_with_images\": \"Flatten with images\" },\n                                { \"flatten_only\": \"Flatten only\" },\n                                { \"no\": \"No\" }\n                            ]\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"flatten_subset_template\",\n                            \"label\": \"Subset template for flatten image\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"color_code_mapping\",\n                            \"label\": \"Color code mappings\",\n                            \"use_label_wrap\": false,\n                            \"collapsible\": false,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"list\",\n                                        \"key\": \"color_code\",\n                                        \"label\": \"Color codes for layers\",\n                                        \"type\": \"enum\",\n                                        \"multiselection\": true,\n                                        \"enum_items\": [\n                                            { \"red\": \"red\" },\n                                            { \"orange\": \"orange\" },\n                                            { \"yellowColor\": \"yellow\" },\n                                            { \"grain\": \"green\" },\n                                            { \"blue\": \"blue\" },\n                                            { \"violet\": \"violet\" },\n                                            { \"gray\": \"gray\" }\n                                        ]\n                                    },\n                                    {\n                                        \"type\": \"list\",\n                                        \"key\": \"layer_name_regex\",\n                                        \"label\": \"Layer name regex\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"splitter\"\n                                    },\n                                    {\n                                        \"key\": \"family\",\n                                        \"label\": \"Resulting family\",\n                                        \"type\": \"enum\",\n                                        \"enum_items\": [\n                                            {\n                                                \"image\": \"image\"\n                                            }\n                                        ]\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"subset_template_name\",\n                                        \"label\": \"Subset template name\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectReview\",\n                    \"label\": \"Collect Review\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\",\n                            \"default\": true\n                        }\n                    ]\n                 },\n                 {\n                    \"type\": \"dict\",\n                    \"key\": \"CollectVersion\",\n                    \"label\": \"Collect Version\",\n                     \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Synchronize version for image and review instances by workfile version.\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"key\": \"ValidateContainers\",\n                            \"label\": \"ValidateContainers\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ValidateNaming\",\n                    \"label\": \"Validate naming of subsets and layers\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Subset cannot contain invalid characters or extract to file would fail\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"invalid_chars\",\n                            \"label\": \"Regex pattern of invalid characters\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"replace_char\",\n                            \"label\": \"Replacement character\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ExtractImage\",\n                    \"label\": \"Extract Image\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Currently only jpg and png are supported\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"formats\",\n                            \"label\": \"Extract Formats\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ExtractReview\",\n                    \"label\": \"Extract Review\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"make_image_sequence\",\n                            \"label\": \"Makes an image sequence instead of a flatten image\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"max_downscale_size\",\n                            \"label\": \"Maximum size of sources for review\",\n                            \"tooltip\": \"FFMpeg can only handle limited resolution for creation of review and/or thumbnail\",\n                            \"minimum\": 300,\n                            \"maximum\": 16384,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"type\": \"dict\",\n                            \"collapsible\": false,\n                            \"key\": \"jpg_options\",\n                            \"label\": \"Extracted jpg Options\",\n                            \"children\": [\n                                {\n                                    \"type\": \"schema\",\n                                    \"name\": \"schema_representation_tags\"\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"dict\",\n                            \"collapsible\": false,\n                            \"key\": \"mov_options\",\n                            \"label\": \"Extracted mov Options\",\n                            \"children\": [\n                                {\n                                    \"type\": \"schema\",\n                                    \"name\": \"schema_representation_tags\"\n                                }\n                            ]\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_workfile_options\",\n            \"skip_paths\": [\n                \"workfile_builder/builder_on_start\",\n                \"workfile_builder/profiles\"\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"resolve\",\n    \"label\": \"DaVinci Resolve\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"launch_openpype_menu_on_start\",\n            \"label\": \"Launch OpenPype menu on start of Resolve\"\n        },\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (remapped to OCIO)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_remapped\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"create\",\n            \"label\": \"Creator plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CreateShotClip\",\n                    \"label\": \"Create Shot Clip\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                          \"type\": \"collapsible-wrap\",\n                          \"label\": \"Shot Hierarchy And Rename Settings\",\n                          \"collapsible\": false,\n                          \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"hierarchy\",\n                                \"label\": \"Shot parent hierarchy\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"clipRename\",\n                                \"label\": \"Rename clips\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"clipName\",\n                                \"label\": \"Clip name template\"\n                            },\n                            {\n                                \"type\": \"number\",\n                                \"key\": \"countFrom\",\n                                \"label\": \"Count sequence from\"\n                            },\n                            {\n                                \"type\": \"number\",\n                                \"key\": \"countSteps\",\n                                \"label\": \"Stepping number\"\n                            }\n                          ]\n                        },\n                        {\n                          \"type\": \"collapsible-wrap\",\n                          \"label\": \"Shot Template Keywords\",\n                          \"collapsible\": false,\n                          \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"folder\",\n                                \"label\": \"{folder}\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"episode\",\n                                \"label\": \"{episode}\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"sequence\",\n                                \"label\": \"{sequence}\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"track\",\n                                \"label\": \"{track}\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"shot\",\n                                \"label\": \"{shot}\"\n                            }\n                          ]\n                        },\n                        {\n                          \"type\": \"collapsible-wrap\",\n                          \"label\": \"Vertical Synchronization Of Attributes\",\n                          \"collapsible\": false,\n                          \"children\": [\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"vSyncOn\",\n                                \"label\": \"Enable Vertical Sync\"\n                            }\n                          ]\n                        },\n                        {\n                          \"type\": \"collapsible-wrap\",\n                          \"label\": \"Shot Attributes\",\n                          \"collapsible\": false,\n                          \"children\": [\n                            {\n                                \"type\": \"number\",\n                                \"key\": \"workfileFrameStart\",\n                                \"label\": \"Workfiles Start Frame\"\n                            },\n                            {\n                                \"type\": \"number\",\n                                \"key\": \"handleStart\",\n                                \"label\": \"Handle start (head)\"\n                            },\n                            {\n                                \"type\": \"number\",\n                                \"key\": \"handleEnd\",\n                                \"label\": \"Handle end (tail)\"\n                            }\n                          ]\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_royalrender.json",
    "content": "{\n  \"type\": \"dict\",\n  \"key\": \"royalrender\",\n  \"label\": \"Royal Render\",\n  \"collapsible\": true,\n  \"is_file\": true,\n  \"children\": [\n    {\n            \"type\": \"rr_root-enum\",\n            \"key\": \"rr_paths\",\n            \"label\": \"Royal Render Roots\",\n            \"multiselect\": true\n    },\n    {\n      \"type\": \"dict\",\n      \"collapsible\": true,\n      \"key\": \"publish\",\n      \"label\": \"Publish plugins\",\n      \"children\": [\n        {\n            \"type\": \"label\",\n            \"label\": \"Collectors\"\n        },\n        {\n          \"type\": \"dict\",\n          \"collapsible\": true,\n          \"key\": \"CollectSequencesFromJob\",\n          \"label\": \"Collect Sequences from the Job\",\n          \"children\": [\n            {\n              \"type\": \"boolean\",\n              \"key\": \"review\",\n              \"label\": \"Generate reviews from sequences\"\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_shotgrid.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"shotgrid\",\n    \"label\": \"Shotgrid\",\n    \"collapsible\": true,\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"number\",\n            \"key\": \"shotgrid_project_id\",\n            \"label\": \"Shotgrid project id\"\n        },\n        {\n            \"type\": \"shotgrid_url-enum\",\n            \"key\": \"shotgrid_server\",\n            \"label\": \"Shotgrid Server\",\n            \"multiselection\": false\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"event\",\n            \"label\": \"Event Handler\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"fields\",\n            \"label\": \"Fields Template\",\n            \"collapsible\": true,\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"asset\",\n                    \"label\": \"Asset\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"type\",\n                            \"label\": \"Asset Type\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"sequence\",\n                    \"label\": \"Sequence\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"episode_link\",\n                            \"label\": \"Episode link\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"shot\",\n                    \"label\": \"Shot\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"episode_link\",\n                            \"label\": \"Episode link\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"sequence_link\",\n                            \"label\": \"Sequence link\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"task\",\n                    \"label\": \"Task\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"step\",\n                            \"label\": \"Step link\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_slack.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"slack\",\n    \"label\": \"Slack\",\n    \"collapsible\": true,\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"text\",\n            \"key\": \"token\",\n            \"label\": \"Auth Token\"\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Fill combination of families, task names and hosts when to send notification\"\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"CollectSlackFamilies\",\n                    \"label\": \"Notification to Slack\",\n                    \"use_label_wrap\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"collapsible\": true,\n                            \"key\": \"profiles\",\n                            \"label\": \"Profiles\",\n                            \"use_label_wrap\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"families\",\n                                        \"label\": \"Families\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"hosts-enum\",\n                                        \"key\": \"hosts\",\n                                        \"label\": \"Host names\",\n                                        \"multiselection\": true\n                                    },\n                                    {\n                                        \"key\": \"task_types\",\n                                        \"label\": \"Task types\",\n                                        \"type\": \"task-types-enum\"\n                                    },\n                                    {\n                                        \"key\": \"tasks\",\n                                        \"label\": \"Task names\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"subsets\",\n                                        \"label\": \"Subset names\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"review_upload_limit\",\n                                        \"label\": \"Upload review maximum file size (MB)\",\n                                        \"decimal\": 2,\n                                        \"default\": 50,\n                                        \"minimum\": 0,\n                                        \"maximum\": 1000000\n                                    },\n                                    {\n                                        \"type\": \"separator\"\n                                    },\n                                    {\n                                        \"key\": \"channel_messages\",\n                                        \"label\": \"Messages to channels\",\n                                        \"type\": \"list\",\n                                        \"use_label_wrap\": true,\n                                        \"object_type\": {\n                                            \"type\": \"dict\",\n                                            \"children\": [\n                                                {\n                                                    \"type\": \"list\",\n                                                    \"object_type\": \"text\",\n                                                    \"key\": \"channels\",\n                                                    \"label\": \"Channels\"\n                                                },\n                                                {\n                                                    \"type\": \"boolean\",\n                                                    \"key\": \"upload_thumbnail\",\n                                                    \"label\": \"Upload thumbnail\"\n                                                },\n                                                {\n                                                    \"type\": \"boolean\",\n                                                    \"key\": \"upload_review\",\n                                                    \"label\": \"Upload review\"\n                                                },\n                                                {\n                                                    \"type\": \"text\",\n                                                    \"multiline\": true,\n                                                    \"key\": \"message\",\n                                                    \"label\": \"Message\"\n                                                }\n                                            ]\n                                        }\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"standalonepublisher\",\n    \"label\": \"Standalone Publisher\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"dict-modifiable\",\n            \"collapsible\": true,\n            \"key\": \"create\",\n            \"label\": \"Creator plugins\",\n            \"collapsible_key\": true,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"children\": [\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"name\",\n                        \"label\": \"Name\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"label\",\n                        \"label\": \"Label\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"family\",\n                        \"label\": \"Family\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"icon\",\n                        \"label\": \"Icon\"\n                    },\n                    {\n                        \"type\": \"list\",\n                        \"key\": \"defaults\",\n                        \"label\": \"Defaults\",\n                        \"object_type\": {\n                            \"type\": \"text\"\n                        }\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"help\",\n                        \"label\": \"Help\"\n                    }\n                ]\n            }\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectTextures\",\n                    \"label\": \"Collect Textures\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"main_workfile_extensions\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Main workfile extensions\"\n                        },\n                        {\n                            \"key\": \"other_workfile_extensions\",\n                            \"label\": \"Support workfile extensions\",\n                            \"type\": \"list\",\n                            \"object_type\": \"text\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"texture_extensions\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Texture extensions\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"workfile_families\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Additional families for workfile\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"texture_families\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Additional families for textures\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"color_space\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Color spaces\"\n                        },\n                        {\n                            \"type\": \"dict\",\n                            \"collapsible\": false,\n                            \"key\": \"input_naming_patterns\",\n                            \"label\": \"Regex patterns for naming conventions\",\n                            \"children\": [\n                                {\n                                    \"type\": \"label\",\n                                    \"label\": \"Add regex groups matching expected name\"\n                                },\n                                {\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\",\n                                    \"key\": \"workfile\",\n                                    \"label\": \"Workfile naming pattern\"\n                                },\n                                {\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\",\n                                    \"key\": \"textures\",\n                                    \"label\": \"Textures naming pattern\"\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"dict\",\n                            \"collapsible\": false,\n                            \"key\": \"input_naming_groups\",\n                            \"label\": \"Group order for regex patterns\",\n                            \"children\": [\n                                {\n                                    \"type\": \"label\",\n                                    \"label\": \"Add names of matched groups in correct order. Available values: ('filler', 'asset', 'shader', 'version', 'channel', 'color_space', 'udim')\"\n                                },\n                                {\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\",\n                                    \"key\": \"workfile\",\n                                    \"label\": \"Workfile group positions\"\n                                },\n                                {\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\",\n                                    \"key\": \"textures\",\n                                    \"label\": \"Textures group positions\"\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"workfile_subset_template\",\n                            \"label\": \"Subset name template for workfile\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"texture_subset_template\",\n                            \"label\": \"Subset name template for textures\"\n                        }\n                    ]\n                },\n               {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ValidateSceneSettings\",\n                    \"label\": \"Validate Scene Settings\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active\",\n                            \"label\": \"Active\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Validate if frame range in DB matches number of published files\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"check_extensions\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Check Frame Range for Extensions\"\n                        },\n                        {\n                            \"key\": \"families\",\n                            \"label\": \"Families\",\n                            \"type\": \"list\",\n                            \"object_type\": \"text\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"skip_timelines_check\",\n                            \"object_type\": \"text\",\n                            \"label\": \"Skip Frame Range check for Tasks\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ExtractThumbnailSP\",\n                    \"label\": \"ExtractThumbnailSP\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"dict\",\n                            \"collapsible\": false,\n                            \"key\": \"ffmpeg_args\",\n                            \"label\": \"ffmpeg_args\",\n                            \"children\": [\n                                {\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\",\n                                    \"key\": \"input\",\n                                    \"label\": \"input\"\n                                },\n                                {\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\",\n                                    \"key\": \"output\",\n                                    \"label\": \"output\"\n                                }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectEditorial\",\n                    \"label\": \"Collect Editorial\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"source_dir\",\n                            \"label\": \"Editorial resources pointer\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"extensions\",\n                            \"label\": \"Accepted extensions\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectHierarchyInstance\",\n                    \"label\": \"Collect Instance Hierarchy\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"shot_rename\",\n                            \"label\": \"Shot Rename\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"shot_rename_template\",\n                            \"label\": \"Shot rename template\"\n                        },\n                        {\n                            \"key\": \"shot_rename_search_patterns\",\n                            \"label\": \"Shot renaming paterns search\",\n                            \"type\": \"dict-modifiable\",\n                            \"highlight_content\": true,\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"dict\",\n                            \"key\": \"shot_add_hierarchy\",\n                            \"label\": \"Shot hierarchy\",\n                            \"checkbox_key\": \"enabled\",\n                            \"children\": [\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"enabled\",\n                                    \"label\": \"Enabled\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"parents_path\",\n                                    \"label\": \"Parents path template\"\n                                },\n                                {\n                                    \"key\": \"parents\",\n                                    \"label\": \"Parents\",\n                                    \"type\": \"dict-modifiable\",\n                                    \"highlight_content\": true,\n                                    \"object_type\": {\n                                        \"type\": \"text\"\n                                    }\n                                }\n                            ]\n                        },\n                        {\n                            \"key\": \"shot_add_tasks\",\n                            \"label\": \"Add tasks to shot\",\n                            \"type\": \"dict-modifiable\",\n                            \"highlight_content\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"task-types-enum\",\n                                        \"key\": \"type\",\n                                        \"label\": \"Task type\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectInstances\",\n                    \"label\": \"Collect Clip Instances\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"custom_start_frame\",\n                            \"label\": \"Custom start frame\",\n                            \"default\": 0,\n                            \"minimum\": 1,\n                            \"maximum\": 100000\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"timeline_frame_start\",\n                            \"label\": \"Timeline start frame\",\n                            \"default\": 90000,\n                            \"minimum\": 0,\n                            \"maximum\": 10000000\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"timeline_frame_offset\",\n                            \"label\": \"Timeline frame offset\",\n                            \"default\": 0,\n                            \"minimum\": -1000000,\n                            \"maximum\": 1000000\n                        },\n                        {\n                            \"key\": \"subsets\",\n                            \"label\": \"Subsets\",\n                            \"type\": \"dict-modifiable\",\n                            \"highlight_content\": true,\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"family\",\n                                        \"label\": \"Family\"\n                                    },\n                                    {\n                                        \"type\": \"list\",\n                                        \"key\": \"families\",\n                                        \"label\": \"Families\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"splitter\"\n                                    },\n                                    {\n                                        \"type\": \"list\",\n                                        \"key\": \"extensions\",\n                                        \"label\": \"Extensions\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"version\",\n                                        \"label\": \"Version lock\",\n                                        \"type\": \"number\",\n                                        \"default\": 0,\n                                        \"minimum\": 0,\n                                        \"maximum\": 10\n                                    }\n                                    ,\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"keepSequence\",\n                                        \"label\": \"Keep sequence if used for review\",\n                                        \"default\": false\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"substancepainter\",\n    \"label\": \"Substance Painter\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (OCIO managed)\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_ocio\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"shelves\",\n            \"label\": \"Shelves\",\n            \"use_label_wrap\": true,\n            \"object_type\": {\n                \"type\": \"text\"\n            }\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"load\",\n            \"label\": \"Loaders\",\n            \"use_label_wrap\": true,\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"SubstanceLoadProjectMesh\",\n                    \"label\": \"Load Mesh\",\n                    \"children\": [\n                        {\n                            \"type\": \"list\",\n                            \"collapsible\": true,\n                            \"key\": \"project_templates\",\n                            \"label\": \"Project Templates\",\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"name\",\n                                        \"label\": \"Name\"\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"default_texture_resolution\",\n                                        \"label\": \"Document Resolution\"\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"import_cameras\",\n                                        \"label\": \"Import Cameras\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"normal_map_format\",\n                                        \"label\": \"Normal Map Format\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"project_workflow\",\n                                        \"label\": \"UV Tile Settings\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"tangent_space_mode\",\n                                        \"label\": \"Normal Map Format\"\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"preserve_strokes\",\n                                        \"label\": \"Preserve Strokes\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                }\n\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"sync_server\",\n    \"label\": \"Site Sync (beta testing)\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"config\",\n            \"label\": \"Config\",\n            \"collapsible\": true,\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"retry_cnt\",\n                    \"label\": \"Retry Count\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"loop_delay\",\n                    \"label\": \"Loop Delay\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"always_accessible_on\",\n                    \"label\": \"Always accessible on sites\",\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"active_site\",\n                    \"label\": \"User Default Active Site\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"remote_site\",\n                    \"label\": \"User Default Remote Site\"\n                }\n            ]\n        },\n        {\n            \"type\": \"sync-server-sites\",\n            \"collapsible\": true,\n            \"key\": \"sites\",\n            \"label\": \"Sites\",\n            \"collapsible_key\": false\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"traypublisher\",\n    \"label\": \"Tray Publisher\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (derived to OCIO)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_derived\"\n                }\n            ]\n        },\n        {\n            \"type\": \"list\",\n            \"collapsible\": true,\n            \"key\": \"simple_creators\",\n            \"label\": \"Simple Create Plugins\",\n            \"use_label_wrap\": true,\n            \"collapsible_key\": true,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"children\": [\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"family\",\n                        \"label\": \"Family\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"identifier\",\n                        \"label\": \"Identifier\",\n                        \"placeholder\": \"< Use 'Family' >\",\n                        \"tooltip\": \"All creators must have unique identifier.\\nBy default is used 'family' but if you need to have more creators with same families\\nyou have to set identifier too.\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"label\",\n                        \"label\": \"Label\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"icon\",\n                        \"label\": \"Icon\"\n                    },\n                    {\n                        \"type\": \"list\",\n                        \"key\": \"default_variants\",\n                        \"label\": \"Default variants\",\n                        \"object_type\": {\n                            \"type\": \"text\"\n                        }\n                    },\n                    {\n                        \"type\": \"separator\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"description\",\n                        \"label\": \"Description\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"detailed_description\",\n                        \"label\": \"Detailed Description\",\n                        \"multiline\": true\n                    },\n                    {\n                        \"type\": \"separator\"\n                    },\n                    {\n                        \"key\": \"allow_sequences\",\n                        \"label\": \"Allow sequences\",\n                        \"type\": \"boolean\"\n                    },\n                    {\n                        \"key\": \"allow_multiple_items\",\n                        \"label\": \"Allow multiple items\",\n                        \"type\": \"boolean\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"key\": \"allow_version_control\",\n                        \"label\": \"Allow version control\",\n                        \"default\": false\n                    },\n                    {\n                        \"type\": \"list\",\n                        \"key\": \"extensions\",\n                        \"label\": \"Extensions\",\n                        \"use_label_wrap\": true,\n                        \"collapsible_key\": true,\n                        \"collapsed\": false,\n                        \"object_type\": \"text\"\n                    }\n                ]\n            }\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"editorial_creators\",\n            \"label\": \"Editorial creator plugins\",\n            \"use_label_wrap\": true,\n            \"collapsible_key\": true,\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"editorial_simple\",\n                    \"label\": \"Editorial simple creator\",\n                    \"use_label_wrap\": true,\n                    \"collapsible_key\": true,\n                    \"children\": [\n\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default variants\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"collapsible-wrap\",\n                            \"label\": \"Shot metadata creator\",\n                            \"collapsible\": true,\n                            \"collapsed\": true,\n                            \"children\": [\n                                {\n                                    \"key\": \"clip_name_tokenizer\",\n                                    \"label\": \"Clip name tokenizer\",\n                                    \"type\": \"dict-modifiable\",\n                                    \"highlight_content\": true,\n                                    \"tooltip\": \"Using Regex expression to create tokens. \\nThose can be used later in \\\"Shot rename\\\" creator \\nor \\\"Shot hierarchy\\\". \\n\\nTokens should be decorated with \\\"_\\\" on each side\",\n                                    \"object_type\": {\n                                        \"type\": \"text\"\n                                    }\n                                },\n                                {\n                                    \"type\": \"dict\",\n                                    \"key\": \"shot_rename\",\n                                    \"label\": \"Shot rename\",\n                                    \"checkbox_key\": \"enabled\",\n                                    \"children\": [\n                                        {\n                                            \"type\": \"boolean\",\n                                            \"key\": \"enabled\",\n                                            \"label\": \"Enabled\"\n                                        },\n                                        {\n                                            \"type\": \"text\",\n                                            \"key\": \"shot_rename_template\",\n                                            \"label\": \"Shot rename template\",\n                                            \"tooltip\":\"Template only supports Anatomy keys and Tokens \\nfrom \\\"Clip name tokenizer\\\"\"\n                                        }\n                                    ]\n                                },\n                                {\n                                    \"type\": \"dict\",\n                                    \"key\": \"shot_hierarchy\",\n                                    \"label\": \"Shot hierarchy\",\n                                    \"checkbox_key\": \"enabled\",\n                                    \"children\": [\n                                        {\n                                            \"type\": \"boolean\",\n                                            \"key\": \"enabled\",\n                                            \"label\": \"Enabled\"\n                                        },\n                                        {\n                                            \"type\": \"text\",\n                                            \"key\": \"parents_path\",\n                                            \"label\": \"Parents path template\",\n                                            \"tooltip\": \"Using keys from \\\"Token to parent convertor\\\" or tokens directly\"\n                                        },\n                                        {\n                                            \"key\": \"parents\",\n                                            \"label\": \"Token to parent convertor\",\n                                            \"type\": \"list\",\n                                            \"highlight_content\": true,\n                                            \"tooltip\": \"The left side is key to be used in template. \\nThe right is value build from Tokens comming from \\n\\\"Clip name tokenizer\\\"\",\n                                            \"object_type\": {\n                                                \"type\": \"dict\",\n                                                \"children\": [\n                                                    {\n                                                        \"type\": \"enum\",\n                                                        \"key\": \"type\",\n                                                        \"label\": \"Parent type\",\n                                                        \"enum_items\": [\n                                                            {\"Project\": \"Project\"},\n                                                            {\"Folder\": \"Folder\"},\n                                                            {\"Episode\": \"Episode\"},\n                                                            {\"Sequence\": \"Sequence\"}\n                                                        ]\n                                                    },\n                                                    {\n                                                        \"type\": \"text\",\n                                                        \"key\": \"name\",\n                                                        \"label\": \"Parent token name\",\n                                                        \"tooltip\": \"Unique name used in \\\"Parent path template\\\"\"\n                                                    },\n                                                    {\n                                                        \"type\": \"text\",\n                                                        \"key\": \"value\",\n                                                        \"label\": \"Parent name value\",\n                                                        \"tooltip\": \"Template where any text, Anatomy keys and Tokens could be used\"\n                                                    }\n                                                ]\n                                            }\n                                        }\n                                    ]\n                                },\n                                {\n                                    \"key\": \"shot_add_tasks\",\n                                    \"label\": \"Add tasks to shot\",\n                                    \"type\": \"dict-modifiable\",\n                                    \"highlight_content\": true,\n                                    \"object_type\": {\n                                        \"type\": \"dict\",\n                                        \"children\": [\n                                            {\n                                                \"type\": \"task-types-enum\",\n                                                \"key\": \"type\",\n                                                \"label\": \"Task type\",\n                                                \"multiselection\": false\n                                            }\n                                        ]\n                                    }\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"collapsible-wrap\",\n                            \"label\": \"Shot's subset creator\",\n                            \"collapsible\": true,\n                            \"collapsed\": true,\n                            \"children\": [\n                                {\n                                    \"type\": \"list\",\n                                    \"key\": \"family_presets\",\n                                    \"label\": \"Family presets\",\n                                    \"object_type\": {\n                                        \"type\": \"dict\",\n                                        \"children\": [\n                                            {\n                                                \"type\": \"enum\",\n                                                \"key\": \"family\",\n                                                \"label\": \"Family\",\n                                                \"enum_items\": [\n                                                    {\"review\": \"review\"},\n                                                    {\"plate\": \"plate\"},\n                                                    {\"audio\": \"audio\"}\n                                                ]\n                                            },\n                                            {\n                                                \"type\": \"text\",\n                                                \"key\": \"variant\",\n                                                \"label\": \"Variant\",\n                                                \"placeholder\": \"< Inherited >\"\n                                            },\n                                            {\n                                                \"type\": \"boolean\",\n                                                \"key\": \"review\",\n                                                \"label\": \"Review\",\n                                                \"default\": true\n                                            },\n                                            {\n                                                \"type\": \"enum\",\n                                                \"key\": \"output_file_type\",\n                                                \"label\": \"Integrating file type\",\n                                                \"enum_items\": [\n                                                    {\".mp4\": \"MP4\"},\n                                                    {\".mov\": \"MOV\"},\n                                                    {\".wav\": \"WAV\"}\n                                                ]\n                                            }\n                                        ]\n                                    }\n                                }\n                            ]\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"key\": \"create\",\n            \"label\": \"Create plugins\",\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"BatchMovieCreator\",\n                    \"label\": \"Batch Movie Creator\",\n                    \"collapsible_key\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Allows to publish multiple video files in one go. <br />Name of matching asset is parsed from file names ('asset.mov', 'asset_v001.mov', 'my_asset_to_publish.mov')\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default variants\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_tasks\",\n                            \"label\": \"Default tasks\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"extensions\",\n                            \"label\": \"Extensions\",\n                            \"use_label_wrap\": true,\n                            \"collapsible_key\": true,\n                            \"collapsed\": false,\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_validate_plugin\",\n                    \"template_data\": [\n                        {\n                            \"key\": \"CollectSequenceFrameData\",\n                            \"label\": \"Collect Original Sequence Frame Data\"\n                        },\n                        {\n                            \"key\": \"ValidateFrameRange\",\n                            \"label\": \"Validate frame range\"\n                        },\n                        {\n                            \"key\": \"ValidateExistingVersion\",\n                            \"label\": \"Validate Existing Version\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"tvpaint\",\n    \"label\": \"TVPaint\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (derived to OCIO)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_derived\"\n                }\n            ]\n        },\n        {\n            \"type\": \"boolean\",\n            \"key\": \"stop_timer_on_application_exit\",\n            \"label\": \"Stop timer on application exit\"\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"create\",\n            \"label\": \"Create plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"create_workfile\",\n                    \"label\": \"Create Workfile\",\n                    \"is_group\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"default_variant\",\n                            \"label\": \"Default variant\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default variants\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"create_review\",\n                    \"label\": \"Create Review\",\n                    \"is_group\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active_on_create\",\n                            \"label\": \"Active by default\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"default_variant\",\n                            \"label\": \"Default variant\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default variants\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"create_render_scene\",\n                    \"label\": \"Create Render Scene\",\n                    \"is_group\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"active_on_create\",\n                            \"label\": \"Active by default\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"mark_for_review\",\n                            \"label\": \"Review by default\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"default_pass_name\",\n                            \"label\": \"Default beauty pass\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"default_variant\",\n                            \"label\": \"Default variant\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default variants\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"create_render_layer\",\n                    \"label\": \"Create Render Layer\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"mark_for_review\",\n                            \"label\": \"Review by default\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"default_pass_name\",\n                            \"label\": \"Default beauty pass\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"default_variant\",\n                            \"label\": \"Default variant\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default variants\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"create_render_pass\",\n                    \"label\": \"Create Render Pass\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"mark_for_review\",\n                            \"label\": \"Review by default\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"default_variant\",\n                            \"label\": \"Default variant\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"default_variants\",\n                            \"label\": \"Default variants\",\n                            \"object_type\": {\n                                \"type\": \"text\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"auto_detect_render\",\n                    \"label\": \"Auto-Detect Create Render\",\n                    \"is_group\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"The creator tries to auto-detect Render Layers and Render Passes in scene. For Render Layers is used group name as a variant and for Render Passes is used TVPaint layer name.<br/><br/>Group names can be renamed by their used order in scene. The renaming template where can be used <b>{group_index}</b> formatting key which is filled by \\\"used position index of group\\\".<br/>- Template: <b>L{group_index}</b><br/>- Group offset: <b>10</b><br/>- Group padding: <b>3</b><br/>Would create group names \\\"<b>L010</b>\\\", \\\"<b>L020</b>\\\", ...\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"allow_group_rename\",\n                            \"label\": \"Allow group rename\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"group_name_template\",\n                            \"label\": \"Group name template\"\n                        },\n                        {\n                            \"key\": \"group_idx_offset\",\n                            \"label\": \"Group index Offset\",\n                            \"type\": \"number\",\n                            \"decimal\": 0,\n                            \"minimum\": 1\n                        },\n                        {\n                            \"key\": \"group_idx_padding\",\n                            \"type\": \"number\",\n                            \"label\": \"Group index Padding\",\n                            \"decimal\": 0,\n                            \"minimum\": 1\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectRenderInstances\",\n                    \"label\": \"Collect Render Instances\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"ignore_render_pass_transparency\",\n                            \"label\": \"Ignore Render Pass opacity\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ExtractSequence\",\n                    \"label\": \"ExtractSequence\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Review BG color</b> is used for whole scene review and for thumbnails.\"\n                        },\n                        {\n                            \"type\": \"color\",\n                            \"key\": \"review_bg\",\n                            \"label\": \"Review BG color\",\n                            \"use_alpha\": false\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"key\": \"ValidateProjectSettings\",\n                            \"label\": \"ValidateProjectSettings\",\n                            \"docstring\": \"Validate if FPS and Resolution match shot data\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"key\": \"ValidateMarks\",\n                            \"label\": \"Validate MarkIn/Out\",\n                            \"docstring\": \"Validate MarkIn/Out match Frame start/end on shot data\"\n                        }\n                    ]\n                },\n                {\n                  \"type\": \"schema_template\",\n                  \"name\": \"template_publish_plugin\",\n                  \"template_data\": [\n                      {\n                          \"key\": \"ValidateStartFrame\",\n                          \"label\": \"Validate Scene Start Frame\",\n                          \"docstring\": \"Validate first frame of scene is set to '0'.\"\n                      }\n                  ]\n                },\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"key\": \"ValidateAssetName\",\n                            \"label\": \"ValidateAssetName\",\n                            \"docstring\": \"Validate if shot on instances metadata is same as workfiles shot\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"ExtractConvertToEXR\",\n                    \"label\": \"Extract Convert To EXR\",\n                    \"is_group\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>WARNING:</b> This plugin does not work on MacOS (using OIIO tool).\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"replace_pngs\",\n                            \"label\": \"Replace source PNG\"\n                        },\n                        {\n                            \"type\": \"enum\",\n                            \"key\": \"exr_compression\",\n                            \"label\": \"EXR Compression\",\n                            \"multiselection\": false,\n                            \"enum_items\": [\n                                {\"ZIP\": \"ZIP\"},\n                                {\"ZIPS\": \"ZIPS\"},\n                                {\"DWAA\": \"DWAA\"},\n                                {\"DWAB\": \"DWAB\"},\n                                {\"PIZ\": \"PIZ\"},\n                                {\"RLE\": \"RLE\"},\n                                {\"PXR24\": \"PXR24\"},\n                                {\"B44\": \"B44\"},\n                                {\"B44A\": \"B44A\"},\n                                {\"none\": \"None\"}\n                            ]\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"load\",\n            \"label\": \"Loader plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"LoadImage\",\n                    \"label\": \"Load Image\",\n                    \"children\": [\n                        {\n                            \"key\": \"defaults\",\n                            \"type\": \"dict\",\n                            \"children\": [\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"stretch\",\n                                    \"label\": \"Stretch\"\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"timestretch\",\n                                    \"label\": \"TimeStretch\"\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"preload\",\n                                    \"label\": \"Preload\"\n                                }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ImportImage\",\n                    \"label\": \"Import Image\",\n                    \"children\": [\n                        {\n                            \"key\": \"defaults\",\n                            \"type\": \"dict\",\n                            \"children\": [\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"stretch\",\n                                    \"label\": \"Stretch\"\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"timestretch\",\n                                    \"label\": \"TimeStretch\"\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"preload\",\n                                    \"label\": \"Preload\"\n                                }\n                            ]\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_workfile_options\",\n            \"skip_paths\": [\n                \"workfile_builder/builder_on_start\",\n                \"workfile_builder/profiles\"\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"unreal\",\n    \"label\": \"Unreal Engine\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (OCIO managed)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_ocio\"\n                }\n            ]\n        },\n        {\n            \"type\": \"boolean\",\n            \"key\": \"level_sequences_for_layouts\",\n            \"label\": \"Generate level sequences when loading layouts\"\n        },\n        {\n            \"type\": \"boolean\",\n            \"key\": \"delete_unmatched_assets\",\n            \"label\": \"Delete assets that are not matched\"\n        },\n        {\n            \"type\": \"text\",\n            \"key\": \"render_config_path\",\n            \"label\": \"Render Config Path\"\n        },\n        {\n            \"type\": \"number\",\n            \"key\": \"preroll_frames\",\n            \"label\": \"Pre-roll frames\"\n        },\n        {\n            \"key\": \"render_format\",\n            \"label\": \"Render format\",\n            \"type\": \"enum\",\n            \"multiselection\": false,\n            \"enum_items\": [\n                {\"png\": \"PNG\"},\n                {\"exr\": \"EXR\"},\n                {\"jpg\": \"JPG\"},\n                {\"bmp\": \"BMP\"}\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"project_setup\",\n            \"label\": \"Project Setup\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"dev_mode\",\n                    \"label\": \"Dev mode\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"webpublisher\",\n    \"label\": \"Web Publisher\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"imageio\",\n            \"type\": \"dict\",\n            \"label\": \"Color Management (derived to OCIO)\",\n            \"collapsible\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"template_host_color_management_derived\"\n                }\n            ]\n        },\n        {\n            \"type\": \"list\",\n            \"collapsible\": true,\n            \"use_label_wrap\": true,\n            \"key\": \"timeout_profiles\",\n            \"label\": \"Timeout profiles\",\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"children\": [\n                    {\n                        \"key\": \"hosts\",\n                        \"label\": \"Host names\",\n                        \"type\": \"hosts-enum\",\n                        \"multiselection\": true\n                    },\n                    {\n                        \"key\": \"task_types\",\n                        \"label\": \"Task types\",\n                        \"type\": \"task-types-enum\",\n                        \"multiselection\": true\n                    },\n                    {\n                        \"type\": \"separator\"\n                    },\n                    {\n                        \"type\": \"number\",\n                        \"key\": \"timeout\",\n                        \"label\": \"Timeout (sec)\"\n                    }\n                ]\n            }\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"publish\",\n            \"label\": \"Publish plugins\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectPublishedFiles\",\n                    \"label\": \"Collect Published Files\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Select if all versions of published items should be kept same. (As max(published) + 1.)\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"sync_next_version\",\n                            \"label\": \"Sync next publish version\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Configure resulting family and tags on representation based on uploaded file and task. <br>Eg. '.png' is uploaded >> create instance of 'render' family<br>'Create review' in Tags >> mark representation to create review from.\"\n                        },\n                        {\n                            \"type\": \"dict-modifiable\",\n                            \"collapsible\": true,\n                            \"key\": \"task_type_to_family\",\n                            \"label\": \"Task type to family mapping\",\n                            \"collapsible_key\": true,\n                            \"object_type\": {\n                                \"type\": \"list\",\n                                \"collapsible\": true,\n                                \"key\": \"task_type\",\n                                \"collapsible_key\": true,\n                                \"object_type\": {\n                                    \"type\": \"dict\",\n                                    \"children\": [\n                                        {\n                                            \"type\": \"boolean\",\n                                            \"key\": \"is_sequence\",\n                                            \"label\": \"Is Sequence\"\n                                        },\n                                        {\n                                            \"type\": \"list\",\n                                            \"key\": \"extensions\",\n                                            \"label\": \"Extensions\",\n                                            \"object_type\": \"text\"\n                                        },\n                                        {\n                                            \"type\": \"separator\"\n                                        },\n                                        {\n                                            \"type\": \"list\",\n                                            \"key\": \"families\",\n                                            \"label\": \"Families\",\n                                            \"object_type\": \"text\"\n                                        },\n                                        {\n                                            \"type\": \"schema\",\n                                            \"name\": \"schema_representation_tags\"\n                                        },\n                                        {\n                                            \"type\": \"text\",\n                                            \"key\": \"result_family\",\n                                            \"label\": \"Resulting family\"\n                                        }\n                                    ]\n                                }\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"CollectTVPaintInstances\",\n                    \"label\": \"Collect TVPaint Instances\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Regex helps to extract render layer and pass names from TVPaint layer name.<br>The regex must contain named groups <b>'layer'</b> and <b>'pass'</b> which are used for creation of RenderPass instances.<hr><br>Example layer name: <b>\\\"L001_Person_Hand\\\"</b><br>Example regex: <b>\\\"(?P&lt;layer&gt;L[0-9]{3}_\\\\w+)_(?P&lt;pass&gt;.+)\\\"</b><br>Extracted layer: <b>\\\"L001_Person\\\"</b><br>Extracted pass: <b>\\\"Hand\\\"</b>\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"layer_name_regex\",\n                            \"label\": \"Layer name regex\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"attributes\",\n    \"label\": \"Attributes\",\n    \"is_file\": true,\n    \"is_group\": true,\n    \"children\": [\n        {\n            \"type\": \"number\",\n            \"key\": \"fps\",\n            \"label\": \"Frame Rate\",\n            \"decimal\": 3,\n            \"minimum\": 0\n        },\n        {\n            \"type\": \"number\",\n            \"key\": \"frameStart\",\n            \"label\": \"Frame Start\",\n            \"maximum\": 999999999\n        },\n        {\n            \"type\": \"number\",\n            \"key\": \"frameEnd\",\n            \"label\": \"Frame End\",\n            \"maximum\": 999999999\n        },\n        {\n            \"type\": \"number\",\n            \"key\": \"clipIn\",\n            \"label\": \"Clip In\",\n            \"maximum\": 999999999\n        },\n        {\n            \"type\": \"number\",\n            \"key\": \"clipOut\",\n            \"label\": \"Clip Out\",\n            \"maximum\": 999999999\n        },\n        {\n            \"type\": \"number\",\n            \"key\": \"handleStart\",\n            \"label\": \"Handle Start\"\n        },\n        {\n            \"type\": \"number\",\n            \"key\": \"handleEnd\",\n            \"label\": \"Handle End\"\n        },\n        {\n            \"type\": \"number\",\n            \"key\": \"resolutionWidth\",\n            \"label\": \"Resolution Width\"\n        },\n        {\n            \"type\": \"number\",\n            \"key\": \"resolutionHeight\",\n            \"label\": \"Resolution Height\"\n        },\n        {\n            \"type\": \"number\",\n            \"key\": \"pixelAspect\",\n            \"label\": \"Pixel Aspect Ratio\",\n            \"decimal\": 2,\n            \"minimum\": 0\n        },\n        {\n            \"type\": \"apps-enum\",\n            \"key\": \"applications\",\n            \"label\": \"Applications\"\n        },\n        {\n            \"type\": \"tools-enum\",\n            \"key\": \"tools_env\",\n            \"label\": \"Tools\"\n        },\n        {\n            \"type\": \"boolean\",\n            \"key\": \"active\",\n            \"label\": \"Active Project\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"imageio\",\n    \"label\": \"Color Management and Output Formats (Deprecated)\",\n    \"is_file\": true,\n    \"is_group\": true,\n    \"children\": [\n        {\n            \"type\": \"label\",\n            \"label\": \"These settings are deprecated and have moved to: <i>project_settings/{app}/imageio</i>.<br>You can right click to copy each host's values and paste them to apply to each host as needed.<br>Changing these values here will not do anything.\"\n        },\n        {\n            \"key\": \"hiero\",\n            \"type\": \"dict\",\n            \"label\": \"Hiero\",\n            \"children\": [\n                {\n                    \"key\": \"workfile\",\n                    \"type\": \"dict\",\n                    \"label\": \"Workfile\",\n                    \"collapsible\": false,\n                    \"children\": [\n                        {\n                            \"type\": \"form\",\n                            \"children\": [\n                                {\n                                    \"type\": \"enum\",\n                                    \"key\": \"ocioConfigName\",\n                                    \"label\": \"OpenColorIO Config\",\n                                    \"enum_items\": [\n                                        {\n                                            \"nuke-default\": \"nuke-default\"\n                                        },\n                                        {\n                                            \"aces_1.0.3\": \"aces_1.0.3\"\n                                        },\n                                        {\n                                            \"aces_1.1\": \"aces_1.1\"\n                                        },\n                                        {\n                                            \"custom\": \"custom\"\n                                        }\n                                    ]\n                                },\n                                {\n                                    \"type\": \"path\",\n                                    \"key\": \"ocioconfigpath\",\n                                    \"label\": \"Custom OCIO path\",\n                                    \"multiplatform\": true,\n                                    \"multipath\": true\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"workingSpace\",\n                                    \"label\": \"Working Space\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"sixteenBitLut\",\n                                    \"label\": \"16 Bit Files\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"eightBitLut\",\n                                    \"label\": \"8 Bit Files\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"floatLut\",\n                                    \"label\": \"Floating Point Files\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"logLut\",\n                                    \"label\": \"Log Files\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"viewerLut\",\n                                    \"label\": \"Viewer\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"thumbnailLut\",\n                                    \"label\": \"Thumbnails\"\n                                }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"regexInputs\",\n                    \"type\": \"dict\",\n                    \"label\": \"Colorspace on Inputs by regex detection\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"inputs\",\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"regex\",\n                                        \"label\": \"Regex\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"colorspace\",\n                                        \"label\": \"Colorspace\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"key\": \"nuke\",\n            \"type\": \"dict\",\n            \"label\": \"Nuke\",\n            \"children\": [\n                {\n                    \"key\": \"viewer\",\n                    \"type\": \"dict\",\n                    \"label\": \"Viewer\",\n                    \"collapsible\": false,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"viewerProcess\",\n                            \"label\": \"Viewer Process\"\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"baking\",\n                    \"type\": \"dict\",\n                    \"label\": \"Extract-review baking profile\",\n                    \"collapsible\": false,\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"viewerProcess\",\n                            \"label\": \"Viewer Process\"\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"workfile\",\n                    \"type\": \"dict\",\n                    \"label\": \"Workfile\",\n                    \"collapsible\": false,\n                    \"children\": [\n                        {\n                            \"type\": \"form\",\n                            \"children\": [\n                                {\n                                    \"type\": \"enum\",\n                                    \"key\": \"colorManagement\",\n                                    \"label\": \"color management\",\n                                    \"enum_items\": [\n                                        {\n                                            \"Nuke\": \"Nuke\"\n                                        },\n                                        {\n                                            \"OCIO\": \"OCIO\"\n                                        }\n                                    ]\n                                },\n                                {\n                                    \"type\": \"enum\",\n                                    \"key\": \"OCIO_config\",\n                                    \"label\": \"OpenColorIO Config\",\n                                    \"enum_items\": [\n                                        {\n                                            \"nuke-default\": \"nuke-default\"\n                                        },\n                                        {\n                                            \"spi-vfx\": \"spi-vfx\"\n                                        },\n                                        {\n                                            \"spi-anim\": \"spi-anim\"\n                                        },\n                                        {\n                                            \"aces_0.1.1\": \"aces_0.1.1\"\n                                        },\n                                        {\n                                            \"aces_0.7.1\": \"aces_0.7.1\"\n                                        },\n                                        {\n                                            \"aces_1.0.1\": \"aces_1.0.1\"\n                                        },\n                                        {\n                                            \"aces_1.0.3\": \"aces_1.0.3\"\n                                        },\n                                        {\n                                            \"aces_1.1\": \"aces_1.1\"\n                                        },\n                                        {\n                                            \"aces_1.2\": \"aces_1.2\"\n                                        },\n                                        {\n                                            \"custom\": \"custom\"\n                                        }\n                                    ]\n                                },\n                                {\n                                    \"type\": \"path\",\n                                    \"key\": \"customOCIOConfigPath\",\n                                    \"label\": \"Custom OCIO config path\",\n                                    \"multiplatform\": true,\n                                    \"multipath\": true\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"workingSpaceLUT\",\n                                    \"label\": \"Working Space\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"monitorLut\",\n                                    \"label\": \"monitor\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"int8Lut\",\n                                    \"label\": \"8-bit files\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"int16Lut\",\n                                    \"label\": \"16-bit files\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"logLut\",\n                                    \"label\": \"log files\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"floatLut\",\n                                    \"label\": \"float files\"\n                                }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"nodes\",\n                    \"type\": \"dict\",\n                    \"label\": \"Nodes\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"key\": \"requiredNodes\",\n                            \"type\": \"list\",\n                            \"label\": \"Plugin required\",\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"list\",\n                                        \"key\": \"plugins\",\n                                        \"label\": \"Used in plugins\",\n                                        \"object_type\": {\n                                            \"type\": \"text\",\n                                            \"key\": \"pluginClass\"\n                                        }\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"nukeNodeClass\",\n                                        \"label\": \"Nuke Node Class\"\n                                    },\n                                    {\n                                        \"type\": \"schema_template\",\n                                        \"name\": \"template_nuke_knob_inputs\",\n                                        \"template_data\": [\n                                            {\n                                                \"label\": \"Knobs\",\n                                                \"key\": \"knobs\"\n                                            }\n                                        ]\n                                    }\n\n                                ]\n                            }\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"overrideNodes\",\n                            \"label\": \"Plugin's node overrides\",\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"list\",\n                                        \"key\": \"plugins\",\n                                        \"label\": \"Used in plugins\",\n                                        \"object_type\": {\n                                            \"type\": \"text\",\n                                            \"key\": \"pluginClass\"\n                                        }\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"nukeNodeClass\",\n                                        \"label\": \"Nuke Node Class\"\n                                    },\n                                    {\n                                        \"key\": \"subsets\",\n                                        \"label\": \"Subsets\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"type\": \"schema_template\",\n                                        \"name\": \"template_nuke_knob_inputs\",\n                                        \"template_data\": [\n                                            {\n                                                \"label\": \"Knobs overrides\",\n                                                \"key\": \"knobs\"\n                                            }\n                                        ]\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"regexInputs\",\n                    \"type\": \"dict\",\n                    \"label\": \"Colorspace on Inputs by regex detection\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"inputs\",\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"regex\",\n                                        \"label\": \"Regex\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"colorspace\",\n                                        \"label\": \"Colorspace\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"key\": \"maya\",\n            \"type\": \"dict\",\n            \"label\": \"Maya\",\n            \"children\": [\n                {\n                    \"key\": \"colorManagementPreference_v2\",\n                    \"type\": \"dict\",\n                    \"label\": \"Color Management Preference v2 (Maya 2022+)\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Use Color Management Preference v2\"\n                        },\n                        {\n                            \"type\": \"path\",\n                            \"key\": \"configFilePath\",\n                            \"label\": \"OCIO Config File Path\",\n                            \"multiplatform\": true,\n                            \"multipath\": true\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"renderSpace\",\n                            \"label\": \"Rendering Space\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"displayName\",\n                            \"label\": \"Display\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"viewName\",\n                            \"label\": \"View\"\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"colorManagementPreference\",\n                    \"type\": \"dict\",\n                    \"label\": \"Color Management Preference (legacy)\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"path\",\n                            \"key\": \"configFilePath\",\n                            \"label\": \"OCIO Config File Path\",\n                            \"multiplatform\": true,\n                            \"multipath\": true\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"renderSpace\",\n                            \"label\": \"Rendering Space\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"viewTransform\",\n                            \"label\": \"Viewer Transform\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"key\": \"flame\",\n            \"type\": \"dict\",\n            \"label\": \"Flame & Flare\",\n            \"children\": [\n                {\n                    \"key\": \"project\",\n                    \"type\": \"dict\",\n                    \"label\": \"Project\",\n                    \"collapsible\": false,\n                    \"children\": [\n                        {\n                            \"type\": \"form\",\n                            \"children\": [\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"colourPolicy\",\n                                    \"label\": \"Colour Policy (name or path)\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"frameDepth\",\n                                    \"label\": \"Image Depth\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"fieldDominance\",\n                                    \"label\": \"Field Dominance\"\n                                }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"profilesMapping\",\n                    \"type\": \"dict\",\n                    \"label\": \"Profile names mapping\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"list\",\n                            \"key\": \"inputs\",\n                            \"object_type\": {\n                                \"type\": \"dict\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"flameName\",\n                                        \"label\": \"Flame name\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"ocioName\",\n                                        \"label\": \"OCIO name\"\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_templates.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"templates\",\n    \"label\": \"Templates\",\n    \"collapsible\": true,\n    \"collapsible_key\": true,\n    \"is_file\": true,\n    \"is_group\": true,\n    \"children\": [\n        {\n            \"type\": \"dict\",\n            \"key\": \"defaults\",\n            \"children\": [\n                {\n                    \"type\": \"label\",\n                    \"label\": \"The list of existing placeholders is available here:<br> https://openpype.io/docs/admin_settings_project_anatomy/#available-template-keys \"\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"version_padding\",\n                    \"label\": \"Version Padding\",\n                    \"minimum\": 1,\n                    \"maximum\": 10\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"version\",\n                    \"label\": \"Version\"\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"frame_padding\",\n                    \"label\": \"Frame Padding\",\n                    \"minimum\": 1,\n                    \"maximum\": 10\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"frame\",\n                    \"label\": \"Frame\"\n                }\n            ]\n        },\n        {\n            \"type\": \"separator\"\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"work\",\n            \"label\": \"Work\",\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"folder\",\n                    \"label\": \"Folder\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"file\",\n                    \"label\": \"File\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"path\",\n                    \"label\": \"Path\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"render\",\n            \"label\": \"Render\",\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"folder\",\n                    \"label\": \"Folder\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"file\",\n                    \"label\": \"File\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"path\",\n                    \"label\": \"Path\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"publish\",\n            \"label\": \"Publish\",\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"folder\",\n                    \"label\": \"Folder\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"file\",\n                    \"label\": \"File\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"path\",\n                    \"label\": \"Path\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"thumbnail\",\n                    \"label\": \"Thumbnail\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"hero\",\n            \"label\": \"Hero\",\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"folder\",\n                    \"label\": \"Folder\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"file\",\n                    \"label\": \"File\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"path\",\n                    \"label\": \"Path\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"delivery\",\n            \"label\": \"Delivery\",\n            \"object_type\": \"text\"\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"unreal\",\n            \"label\": \"Unreal\",\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"folder\",\n                    \"label\": \"Folder\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"file\",\n                    \"label\": \"File\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"path\",\n                    \"label\": \"Path\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"others\",\n            \"label\": \"Others\",\n            \"collapsible_key\": true,\n            \"object_type\": {\n                \"type\": \"dict-modifiable\",\n                \"object_type\": \"text\"\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"publish\",\n    \"label\": \"Publish plugins\",\n    \"children\": [\n        {\n            \"type\": \"label\",\n            \"label\": \"Validators\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_publish_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"ValidateCameraZeroKeyframe\",\n                    \"label\": \"Validate Camera Zero Keyframe\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ValidateFileSaved\",\n            \"label\": \"Validate File Saved\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"key\": \"exclude_families\",\n                    \"label\": \"Exclude Families\",\n                    \"type\": \"list\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            \"type\": \"collapsible-wrap\",\n            \"label\": \"Model\",\n            \"children\": [\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"key\": \"ValidateMeshHasUvs\",\n                            \"label\": \"Validate Mesh Has UVs\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshNoNegativeScale\",\n                            \"label\": \"Validate Mesh No Negative Scale\"\n                        },\n                        {\n                            \"key\": \"ValidateTransformZero\",\n                            \"label\": \"Validate Transform Zero\"\n                        },\n                        {\n                            \"key\": \"ValidateNoColonsInName\",\n                            \"label\": \"Validate No Colons In Name\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"collapsible-wrap\",\n            \"label\": \"BlendScene\",\n            \"children\": [\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"key\": \"ValidateInstanceEmpty\",\n                            \"label\": \"Validate Instance is not Empty\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"collapsible-wrap\",\n            \"label\": \"Render\",\n            \"children\": [\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"type\": \"dict\",\n                            \"collapsible\": true,\n                            \"key\": \"ValidateRenderCameraIsSet\",\n                            \"label\": \"Validate Render Camera Is Set\",\n                            \"checkbox_key\": \"enabled\",\n                            \"children\": [\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"enabled\",\n                                    \"label\": \"Enabled\"\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"optional\",\n                                    \"label\": \"Optional\"\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"active\",\n                                    \"label\": \"Active\"\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"dict\",\n                            \"collapsible\": true,\n                            \"key\": \"ValidateDeadlinePublish\",\n                            \"label\": \"Validate Render Output for Deadline\",\n                            \"checkbox_key\": \"enabled\",\n                            \"children\": [\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"enabled\",\n                                    \"label\": \"Enabled\"\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"optional\",\n                                    \"label\": \"Optional\"\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"key\": \"active\",\n                                    \"label\": \"Active\"\n                                }\n                            ]\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"splitter\"\n        },\n        {\n            \"type\": \"label\",\n            \"label\": \"Extractors\"\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractBlend\",\n            \"label\": \"Extract Blend\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                },\n                {\n                    \"key\": \"families\",\n                    \"label\": \"Families\",\n                    \"type\": \"list\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_publish_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"ExtractModelABC\",\n                    \"label\": \"Extract ABC (model)\"\n                },\n                {\n                    \"key\": \"ExtractFBX\",\n                    \"label\": \"Extract FBX (model and rig)\"\n                },\n                {\n                    \"key\": \"ExtractBlendAnimation\",\n                    \"label\": \"Extract Animation as Blend\"\n                },\n                {\n                    \"key\": \"ExtractAnimationFBX\",\n                    \"label\": \"Extract Animation as FBX\"\n                },\n                {\n                    \"key\": \"ExtractCamera\",\n                    \"label\": \"Extract Camera as FBX\"\n                },\n                {\n                    \"key\": \"ExtractCameraABC\",\n                    \"label\": \"Extract Camera as ABC\"\n                },\n                {\n                    \"key\": \"ExtractLayout\",\n                    \"label\": \"Extract Layout as JSON\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractThumbnail\",\n            \"label\": \"ExtractThumbnail\",\n            \"checkbox_key\": \"enabled\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                },\n                {\n                    \"type\": \"raw-json\",\n                    \"key\": \"presets\",\n                    \"label\": \"Presets\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractPlayblast\",\n            \"label\": \"ExtractPlayblast\",\n            \"checkbox_key\": \"enabled\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                },\n                {\n                    \"type\": \"raw-json\",\n                    \"key\": \"presets\",\n                    \"label\": \"Presets\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_equalizer_create.json",
    "content": "{\n  \"type\": \"dict\",\n  \"collapsible\": true,\n  \"key\": \"create\",\n  \"label\": \"Creator plugins\",\n  \"children\": [\n    {\n      \"type\": \"dict\",\n      \"collapsible\": true,\n      \"key\": \"CreateMatchMove\",\n      \"label\": \"Create Match Move\",\n      \"checkbox_key\": \"enabled\",\n      \"children\": [\n        {\n          \"type\": \"boolean\",\n          \"key\": \"enabled\",\n          \"label\": \"Enabled\"\n        },\n        {\n          \"type\": \"list\",\n          \"key\": \"default_variants\",\n          \"label\": \"Default Variants\",\n          \"object_type\": \"text\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"publish\",\n    \"label\": \"Publish plugins\",\n    \"children\": [\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CollectAnatomyInstanceData\",\n            \"label\": \"Collect Anatomy Instance Data\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"follow_workfile_version\",\n                    \"label\": \"Follow workfile version\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"CollectAudio\",\n            \"label\": \"Collect Audio\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"key\": \"audio_subset_name\",\n                    \"label\": \"Name of audio variant\",\n                    \"type\": \"text\",\n                    \"placeholder\": \"audioMain\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CollectSceneVersion\",\n            \"label\": \"Collect Version from Workfile\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"key\": \"hosts\",\n                    \"label\": \"Host names\",\n                    \"type\": \"hosts-enum\",\n                    \"multiselection\": true\n                },\n                {\n                    \"key\": \"skip_hosts_headless_publish\",\n                    \"label\": \"Skip for host if headless publish\",\n                    \"type\": \"hosts-enum\",\n                    \"multiselection\": true\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"collect_comment_per_instance\",\n            \"label\": \"Collect comment per instance\",\n            \"checkbox_key\": \"enabled\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"key\": \"families\",\n                    \"label\": \"Families\",\n                    \"type\": \"list\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"CollectFramesFixDef\",\n            \"label\": \"Collect Frames to Fix\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"rewrite_version_enable\",\n                    \"label\": \"Show 'Rewrite latest version' toggle\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"ValidateEditorialAssetName\",\n            \"label\": \"Validate Editorial Asset Name\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_publish_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"ValidateVersion\",\n                    \"label\": \"Validate Version\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"label\": \"Validate Intent\",\n            \"key\": \"ValidateIntent\",\n            \"is_group\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Validate if Publishing intent was selected. It is possible to disable validation for specific publishing context with profiles.\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"collapsible\": true,\n                    \"key\": \"profiles\",\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"hosts\",\n                                \"label\": \"Host names\",\n                                \"type\": \"hosts-enum\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\",\n                                \"type\": \"task-types-enum\"\n                            },\n                            {\n                                \"key\": \"tasks\",\n                                \"label\": \"Task names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"key\": \"validate\",\n                                \"label\": \"Validate\",\n                                \"type\": \"boolean\"\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"ExtractThumbnail\",\n            \"label\": \"ExtractThumbnail\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"object_type\": \"text\",\n                    \"key\": \"subsets\",\n                    \"label\": \"Subsets\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"integrate_thumbnail\",\n                    \"label\": \"Integrate thumbnail as representation\"\n                },\n                {\n                    \"type\": \"dict-conditional\",\n                    \"use_label_wrap\": false,\n                    \"collapsible\": false,\n                    \"key\": \"target_size\",\n                    \"label\": \"Target size\",\n                    \"enum_key\": \"type\",\n                    \"enum_label\": \"Type\",\n                    \"enum_children\": [\n                        {\n                            \"key\": \"source\",\n                            \"label\": \"Image source\",\n                            \"children\": [\n                                {\n                                    \"type\": \"label\",\n                                    \"label\": \"Image size will be inherited from source image.\"\n                                }\n                            ]\n                        },\n                        {\n                            \"key\": \"resize\",\n                            \"label\": \"Resize\",\n                            \"children\": [\n                                {\n                                    \"type\": \"label\",\n                                    \"label\": \"Image will be resized to specified size.\"\n                                },\n                                {\n                                    \"type\": \"number\",\n                                    \"key\": \"width\",\n                                    \"label\": \"Width\",\n                                    \"decimal\": 0,\n                                    \"minimum\": 0,\n                                    \"maximum\": 99999\n                                },\n                                {\n                                    \"type\": \"number\",\n                                    \"key\": \"height\",\n                                    \"label\": \"Height\",\n                                    \"decimal\": 0,\n                                    \"minimum\": 0,\n                                    \"maximum\": 99999\n                                }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Background color\",\n                    \"key\": \"background_color\"\n                },\n                {\n                    \"key\": \"duration_split\",\n                    \"label\": \"Duration split ratio\",\n                    \"type\": \"number\",\n                    \"decimal\": 1,\n                    \"default\": 0.5,\n                    \"minimum\": 0,\n                    \"maximum\": 1\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"oiiotool_defaults\",\n                    \"label\": \"OIIOtool defaults\",\n                    \"children\": [\n                        {\n                            \"type\": \"enum\",\n                            \"key\": \"type\",\n                            \"label\": \"Target type\",\n                            \"enum_items\": [\n                                { \"colorspace\": \"Colorspace\" },\n                                { \"display_and_view\": \"Display & View\" }\n                            ]\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"colorspace\",\n                            \"label\": \"Colorspace\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"view\",\n                            \"label\": \"View\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"display\",\n                            \"label\": \"Display\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"ffmpeg_args\",\n                    \"children\": [\n                        {\n                            \"type\": \"list\",\n                            \"object_type\": \"text\",\n                            \"key\": \"input\",\n                            \"label\": \"FFmpeg input arguments\"\n                        },\n                        {\n                            \"type\": \"list\",\n                            \"object_type\": \"text\",\n                            \"key\": \"output\",\n                            \"label\": \"FFmpeg output arguments\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractOIIOTranscode\",\n            \"label\": \"Extract OIIO Transcode\",\n            \"checkbox_key\": \"enabled\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Configure Output Definition(s) for new representation(s). \\nEmpty 'Extension' denotes keeping source extension. \\nName(key) of output definition will be used as new representation name \\nunless 'passthrough' value is used to keep existing name. \\nFill either 'Colorspace' (for target colorspace) or \\nboth 'Display' and 'View' (for display and viewer colorspaces).\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"profiles\",\n                    \"label\": \"Profiles\",\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"families\",\n                                \"label\": \"Families\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"key\": \"hosts\",\n                                \"label\": \"Host names\",\n                                \"type\": \"hosts-enum\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\",\n                                \"type\": \"task-types-enum\"\n                            },\n                            {\n                                \"key\": \"task_names\",\n                                \"label\": \"Task names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"key\": \"subsets\",\n                                \"label\": \"Subset names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"delete_original\",\n                                \"label\": \"Delete Original Representation\"\n                            },\n                            {\n                                \"type\": \"splitter\"\n                            },\n                            {\n                                \"key\": \"outputs\",\n                                \"label\": \"Output Definitions\",\n                                \"type\": \"dict-modifiable\",\n                                \"highlight_content\": true,\n                                \"object_type\": {\n                                    \"type\": \"dict\",\n                                    \"children\": [\n                                        {\n                                            \"key\": \"extension\",\n                                            \"label\": \"Extension\",\n                                            \"type\": \"text\"\n                                        },\n                                        {\n                                            \"type\": \"enum\",\n                                            \"key\": \"transcoding_type\",\n                                            \"label\": \"Transcoding type\",\n                                            \"enum_items\": [\n                                                { \"colorspace\": \"Use Colorspace\" },\n                                                { \"display\": \"Use Display&View\" }\n                                            ]\n                                        },\n                                        {\n                                            \"key\": \"colorspace\",\n                                            \"label\": \"Colorspace\",\n                                            \"type\": \"text\"\n                                        },\n                                        {\n                                            \"key\": \"display\",\n                                            \"label\": \"Display\",\n                                            \"type\": \"text\"\n                                        },\n                                        {\n                                            \"key\": \"view\",\n                                            \"label\": \"View\",\n                                            \"type\": \"text\"\n                                        },\n                                        {\n                                            \"key\": \"oiiotool_args\",\n                                            \"label\": \"OIIOtool arguments\",\n                                            \"type\": \"dict\",\n                                            \"highlight_content\": true,\n                                            \"children\": [\n                                                {\n                                                    \"key\": \"additional_command_args\",\n                                                    \"label\": \"Arguments\",\n                                                    \"type\": \"list\",\n                                                    \"object_type\": \"text\"\n                                                }\n                                            ]\n                                        },\n                                        {\n                                            \"type\": \"schema\",\n                                            \"name\": \"schema_representation_tags\"\n                                        },\n                                        {\n                                            \"key\": \"custom_tags\",\n                                            \"label\": \"Custom Tags\",\n                                            \"type\": \"list\",\n                                            \"object_type\": \"text\"\n                                        }\n                                    ]\n                                }\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractReview\",\n            \"label\": \"ExtractReview\",\n            \"checkbox_key\": \"enabled\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"profiles\",\n                    \"label\": \"Profiles\",\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"families\",\n                                \"label\": \"Families\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"hosts-enum\",\n                                \"key\": \"hosts\",\n                                \"label\": \"Hosts\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"type\": \"splitter\"\n                            },\n                            {\n                                \"key\": \"outputs\",\n                                \"label\": \"Output Definitions\",\n                                \"type\": \"dict-modifiable\",\n                                \"highlight_content\": true,\n                                \"object_type\": {\n                                    \"type\": \"dict\",\n                                    \"children\": [\n                                        {\n                                            \"key\": \"ext\",\n                                            \"label\": \"Output extension\",\n                                            \"type\": \"text\"\n                                        },\n                                        {\n                                            \"type\": \"schema\",\n                                            \"name\": \"schema_representation_tags\"\n                                        },\n                                        {\n                                            \"key\": \"burnins\",\n                                            \"label\": \"Link to a burnin by name\",\n                                            \"type\": \"list\",\n                                            \"object_type\": \"text\"\n                                        },\n                                        {\n                                            \"key\": \"ffmpeg_args\",\n                                            \"label\": \"FFmpeg arguments\",\n                                            \"type\": \"dict\",\n                                            \"highlight_content\": true,\n                                            \"children\": [\n                                                {\n                                                    \"key\": \"video_filters\",\n                                                    \"label\": \"Video filters\",\n                                                    \"type\": \"list\",\n                                                    \"object_type\": \"text\"\n                                                },\n                                                {\n                                                    \"type\": \"splitter\"\n                                                },\n                                                {\n                                                    \"key\": \"audio_filters\",\n                                                    \"label\": \"Audio filters\",\n                                                    \"type\": \"list\",\n                                                    \"object_type\": \"text\"\n                                                },\n                                                {\n                                                    \"type\": \"splitter\"\n                                                },\n                                                {\n                                                    \"key\": \"input\",\n                                                    \"label\": \"Input arguments\",\n                                                    \"type\": \"list\",\n                                                    \"object_type\": \"text\"\n                                                },\n                                                {\n                                                    \"type\": \"splitter\"\n                                                },\n                                                {\n                                                    \"key\": \"output\",\n                                                    \"label\": \"Output arguments\",\n                                                    \"type\": \"list\",\n                                                    \"object_type\": \"text\"\n                                                }\n                                            ]\n                                        },\n                                        {\n                                            \"key\": \"filter\",\n                                            \"label\": \"Additional output filtering\",\n                                            \"type\": \"dict\",\n                                            \"highlight_content\": true,\n                                            \"children\": [\n                                                {\n                                                    \"key\": \"families\",\n                                                    \"label\": \"Families\",\n                                                    \"type\": \"list\",\n                                                    \"object_type\": \"text\"\n                                                },\n                                                {\n                                                    \"type\": \"separator\"\n                                                },\n                                                {\n                                                    \"key\": \"subsets\",\n                                                    \"label\": \"Subsets\",\n                                                    \"type\": \"list\",\n                                                    \"object_type\": \"text\"\n                                                },\n                                                {\n                                                    \"type\": \"separator\"\n                                                },\n                                                {\n                                                    \"key\": \"custom_tags\",\n                                                    \"label\": \"Custom Tags\",\n                                                    \"type\": \"list\",\n                                                    \"object_type\": \"text\"\n                                                },\n                                                {\n                                                    \"type\": \"label\",\n                                                    \"label\": \"Use output <b>always</b> / only if input <b>is 1 frame</b> image / only if has <b>2+ frames</b> or <b>is video</b>\"\n                                                },\n                                                {\n                                                    \"type\": \"enum\",\n                                                    \"key\": \"single_frame_filter\",\n                                                    \"default\": \"everytime\",\n                                                    \"enum_items\": [\n                                                        {\"everytime\": \"Always\"},\n                                                        {\"single_frame\": \"Only if input has 1 image frame\"},\n                                                        {\"multi_frame\": \"Only if input is video or sequence of frames\"}\n                                                    ]\n                                                }\n                                            ]\n                                        },\n                                        {\n                                            \"type\": \"separator\"\n                                        },\n                                        {\n                                            \"type\": \"label\",\n                                            \"label\": \"Crop input overscan. See the documentation for more information.\"\n                                        },\n                                        {\n                                            \"type\": \"text\",\n                                            \"key\": \"overscan_crop\",\n                                            \"label\": \"Overscan crop\"\n                                        },\n                                        {\n                                            \"type\": \"label\",\n                                            \"label\": \"Overscan color is used when input aspect ratio is not same as output aspect ratio.\"\n                                        },\n                                        {\n                                            \"type\": \"color\",\n                                            \"label\": \"Overscan color\",\n                                            \"key\": \"overscan_color\",\n                                            \"use_alpha\": false\n                                        },\n                                        {\n                                            \"type\": \"label\",\n                                            \"label\": \"Width and Height must be both set to higher value than 0 else source resolution is used.\"\n                                        },\n                                        {\n                                            \"key\": \"width\",\n                                            \"label\": \"Output width\",\n                                            \"type\": \"number\",\n                                            \"default\": 0,\n                                            \"minimum\": 0,\n                                            \"maximum\": 100000\n                                        },\n                                        {\n                                            \"key\": \"height\",\n                                            \"label\": \"Output height\",\n                                            \"type\": \"number\",\n                                            \"default\": 0,\n                                            \"minimum\": 0,\n                                            \"maximum\": 100000\n                                        },\n                                        {\n                                            \"type\": \"label\",\n                                            \"label\": \"Rescale input when it's pixel aspect ratio is not 1. Usefull for anamorph reviews.\"\n                                        },\n                                        {\n                                            \"key\": \"scale_pixel_aspect\",\n                                            \"label\": \"Scale pixel aspect\",\n                                            \"type\": \"boolean\"\n                                        },\n                                        {\n                                            \"type\": \"label\",\n                                            \"label\": \"Background color is used only when input have transparency and Alpha is higher than 0.\"\n                                        },\n                                        {\n                                            \"type\": \"color\",\n                                            \"label\": \"Background color\",\n                                            \"key\": \"bg_color\"\n                                        },\n                                        {\n                                            \"key\": \"letter_box\",\n                                            \"label\": \"Letter box\",\n                                            \"type\": \"dict\",\n                                            \"checkbox_key\": \"enabled\",\n                                            \"children\": [\n                                                {\n                                                    \"type\": \"boolean\",\n                                                    \"key\": \"enabled\",\n                                                    \"label\": \"Enabled\",\n                                                    \"default\": false\n                                                },\n                                                {\n                                                    \"key\": \"ratio\",\n                                                    \"label\": \"Ratio\",\n                                                    \"type\": \"number\",\n                                                    \"decimal\": 4,\n                                                    \"default\": 0,\n                                                    \"minimum\": 0,\n                                                    \"maximum\": 10000\n                                                },\n                                                {\n                                                    \"type\": \"color\",\n                                                    \"label\": \"Fill Color\",\n                                                    \"key\": \"fill_color\"\n                                                },\n                                                {\n                                                    \"key\": \"line_thickness\",\n                                                    \"label\": \"Line Thickness\",\n                                                    \"type\": \"number\",\n                                                    \"minimum\": 0,\n                                                    \"maximum\": 1000\n                                                },\n                                                {\n                                                    \"type\": \"color\",\n                                                    \"label\": \"Line Color\",\n                                                    \"key\": \"line_color\"\n                                                }\n                                            ]\n                                        }\n                                    ]\n                                }\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractBurnin\",\n            \"label\": \"ExtractBurnin\",\n            \"checkbox_key\": \"enabled\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"options\",\n                    \"label\": \"Burnin formatting options\",\n                    \"children\": [\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"font_size\",\n                            \"label\": \"Font size\",\n                            \"minimum\": 0\n                        },\n                        {\n                            \"type\": \"color\",\n                            \"key\": \"font_color\",\n                            \"label\": \"Font Color\"\n                        },\n                        {\n                            \"type\": \"color\",\n                            \"key\": \"bg_color\",\n                            \"label\": \"Background Color\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"x_offset\",\n                            \"label\": \"X Offset\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"y_offset\",\n                            \"label\": \"Y Offset\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"bg_padding\",\n                            \"label\": \"Padding aroung text\"\n                        },\n                        {\n                            \"type\": \"path\",\n                            \"key\": \"font_filepath\",\n                            \"label\": \"Font file path\",\n                            \"multipath\": false,\n                            \"multiplatform\": true\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"separator\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"profiles\",\n                    \"label\": \"Profiles\",\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"families\",\n                                \"label\": \"Families\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"key\": \"hosts\",\n                                \"label\": \"Host names\",\n                                \"type\": \"hosts-enum\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\",\n                                \"type\": \"task-types-enum\"\n                            },\n                            {\n                                \"key\": \"task_names\",\n                                \"label\": \"Task names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"key\": \"subsets\",\n                                \"label\": \"Subset names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"splitter\"\n                            },\n                            {\n                                \"key\": \"burnins\",\n                                \"label\": \"Burnins\",\n                                \"type\": \"dict-modifiable\",\n                                \"highlight_content\": true,\n                                \"collapsible\": false,\n                                \"object_type\": {\n                                    \"type\": \"dict\",\n                                    \"children\": [\n                                        {\n                                            \"key\": \"TOP_LEFT\",\n                                            \"label\": \"Top Left\",\n                                            \"type\": \"text\"\n                                        },\n                                        {\n                                            \"key\": \"TOP_CENTERED\",\n                                            \"label\": \"Top Centered\",\n                                            \"type\": \"text\"\n                                        },\n                                        {\n                                            \"key\": \"TOP_RIGHT\",\n                                            \"label\": \"top Right\",\n                                            \"type\": \"text\"\n                                        },\n                                        {\n                                            \"key\": \"BOTTOM_LEFT\",\n                                            \"label\": \"Bottom Left\",\n                                            \"type\": \"text\"\n                                        },\n                                        {\n                                            \"key\": \"BOTTOM_CENTERED\",\n                                            \"label\": \"Bottom Centered\",\n                                            \"type\": \"text\"\n                                        },\n                                        {\n                                            \"key\": \"BOTTOM_RIGHT\",\n                                            \"label\": \"BottomRight\",\n                                            \"type\": \"text\"\n                                        },\n                                        {\n                                            \"key\": \"filter\",\n                                            \"label\": \"Additional filtering\",\n                                            \"type\": \"dict\",\n                                            \"highlight_content\": true,\n                                            \"children\": [\n                                                {\n                                                    \"key\": \"families\",\n                                                    \"label\": \"Families\",\n                                                    \"type\": \"list\",\n                                                    \"object_type\": \"text\"\n                                                },\n                                                {\n                                                    \"type\": \"schema\",\n                                                    \"name\": \"schema_representation_tags\"\n                                                }\n                                            ]\n                                        }\n                                    ]\n                                }\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"PreIntegrateThumbnails\",\n            \"label\": \"Override Integrate Thumbnail Representations\",\n            \"is_group\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Explicitly set if Thumbnail representation should be integrated into DB.<br> If no matching profile set, existing state from Host implementation is kept.\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"integrate_profiles\",\n                    \"label\": \"Integrate profiles\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"families\",\n                                \"label\": \"Families\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"hosts-enum\",\n                                \"key\": \"hosts\",\n                                \"label\": \"Hosts\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\",\n                                \"type\": \"task-types-enum\"\n                            },\n                            {\n                                \"key\": \"task_names\",\n                                \"label\": \"Task names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"key\": \"subsets\",\n                                \"label\": \"Subset names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"integrate_thumbnail\",\n                                \"label\": \"Integrate thumbnail\"\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"IntegrateSubsetGroup\",\n            \"label\": \"Integrate Subset Group\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"list\",\n                    \"key\": \"subset_grouping_profiles\",\n                    \"label\": \"Subset grouping profiles\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"label\",\n                                \"label\": \"Set all published instances as a part of specific group named according to 'Template'. <br>Implemented all variants of placeholders [{task},{family},{host},{subset},{renderlayer}]\"\n                            },\n                            {\n                                \"key\": \"families\",\n                                \"label\": \"Families\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"hosts-enum\",\n                                \"key\": \"hosts\",\n                                \"label\": \"Hosts\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\",\n                                \"type\": \"task-types-enum\"\n                            },\n                            {\n                                \"key\": \"tasks\",\n                                \"label\": \"Task names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"template\",\n                                \"label\": \"Template\"\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"IntegrateHeroVersion\",\n            \"label\": \"IntegrateHeroVersion\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                },\n                {\n                    \"key\": \"families\",\n                    \"label\": \"Families\",\n                    \"type\": \"list\",\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"<b>NOTE:</b> Hero publish template profiles settings were moved to <a href=\\\"settings://project_settings/global/tools/publish/hero_template_name_profiles\\\"><b>Tools/Publish/Hero template name profiles</b></a>. Please move values there.\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CleanUp\",\n            \"label\": \"Clean Up\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"list\",\n                    \"key\": \"paterns\",\n                    \"label\": \"Paterrns (regex)\",\n                    \"object_type\": {\n                        \"type\": \"text\"\n                    }\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"remove_temp_renders\",\n                    \"label\": \"Remove Temp renders\",\n                    \"default\": false\n                }\n            ]\n        },\n        {\n          \"type\": \"dict\",\n          \"collapsible\": false,\n          \"key\": \"CleanUpFarm\",\n          \"label\": \"Clean Up Farm\",\n          \"is_group\": true,\n          \"checkbox_key\": \"enabled\",\n          \"children\": [\n              {\n                  \"type\": \"boolean\",\n                  \"key\": \"enabled\",\n                  \"label\": \"Enabled\"\n              }\n          ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"tools\",\n    \"label\": \"Tools\",\n    \"children\": [\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"creator\",\n            \"label\": \"Creator\",\n            \"children\": [\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"collapsible\": true,\n                    \"key\": \"families_smart_select\",\n                    \"label\": \"Families smart select\",\n                    \"object_type\": {\n                        \"type\": \"list\",\n                        \"object_type\": \"text\"\n                    }\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"subset_name_profiles\",\n                    \"label\": \"Subset name profiles\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"families\",\n                                \"label\": \"Families\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"hosts-enum\",\n                                \"key\": \"hosts\",\n                                \"label\": \"Hosts\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\",\n                                \"type\": \"task-types-enum\"\n                            },\n                            {\n                                \"key\": \"tasks\",\n                                \"label\": \"Task names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"template\",\n                                \"label\": \"Template\"\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"Workfiles\",\n            \"label\": \"Workfiles\",\n            \"children\": [\n                {\n                    \"type\": \"list\",\n                    \"key\": \"workfile_template_profiles\",\n                    \"label\": \"Workfile template profiles\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\",\n                                \"type\": \"task-types-enum\"\n                            },\n                            {\n                                \"type\": \"hosts-enum\",\n                                \"key\": \"hosts\",\n                                \"label\": \"Hosts\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"type\": \"splitter\"\n                            },\n                            {\n                                \"key\": \"workfile_template\",\n                                \"label\": \"Workfile template\",\n                                \"type\": \"anatomy-templates-enum\",\n                                \"multiselection\": false\n                            }\n                        ]\n                    }\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"last_workfile_on_startup\",\n                    \"label\": \"Open last workfiles on launch\",\n                    \"is_group\": true,\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"hosts-enum\",\n                                \"key\": \"hosts\",\n                                \"label\": \"Hosts\",\n                                \"multiselection\": true,\n                                \"hosts_filter\": [\n                                      \"aftereffects\",\n                                      \"blender\",\n                                      \"celaction\",\n                                      \"fusion\",\n                                      \"harmony\",\n                                      \"hiero\",\n                                      \"houdini\",\n                                      \"maya\",\n                                      \"nuke\",\n                                      \"photoshop\",\n                                      \"resolve\",\n                                      \"tvpaint\",\n                                      \"unreal\"\n                                  ]\n                            },\n                            {\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\",\n                                \"type\": \"task-types-enum\"\n                            },\n                            {\n                                \"key\": \"tasks\",\n                                \"label\": \"Task names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"splitter\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"enabled\",\n                                \"label\": \"Enabled\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"use_last_published_workfile\",\n                                \"label\": \"Use last published workfile\"\n                            }\n                        ]\n                    }\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"open_workfile_tool_on_startup\",\n                    \"label\": \"Open workfile tool on launch\",\n                    \"is_group\": true,\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"hosts-enum\",\n                                \"key\": \"hosts\",\n                                \"label\": \"Hosts\",\n                                \"multiselection\": true,\n                                \"hosts_filter\": [\n                                    \"nuke\"\n                                ]\n                            },\n                            {\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\",\n                                \"type\": \"list\",\n                                \"object_type\": \"task-types-enum\"\n                            },\n                            {\n                                \"key\": \"tasks\",\n                                \"label\": \"Task names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"splitter\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"enabled\",\n                                \"label\": \"Enabled\"\n                            }\n                        ]\n                    }\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"extra_folders\",\n                    \"label\": \"Extra work folders\",\n                    \"collapsible\": true,\n                    \"use_label_wrap\": true,\n                    \"is_group\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"hosts-enum\",\n                                \"key\": \"hosts\",\n                                \"label\": \"Hosts\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"type\": \"task-types-enum\",\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\"\n                            },\n                            {\n                                \"label\": \"Task names\",\n                                \"key\": \"task_names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"splitter\"\n                            },\n                            {\n                                \"type\": \"label\",\n                                \"label\": \"Folders will be created in directory next to workfile. Items may contain nested directories (e.g. <b>resources/images</b>).\"\n                            },\n                            {\n                                \"key\": \"folders\",\n                                \"label\": \"Folders\",\n                                \"type\": \"list\",\n                                \"highlight_content\": true,\n                                \"collapsible\": false,\n                                \"object_type\": \"text\"\n                            }\n                        ]\n                    }\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"workfile_lock_profiles\",\n                    \"label\": \"Workfile lock profiles\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"hosts-enum\",\n                                \"key\": \"host_name\",\n                                \"label\": \"Hosts\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"type\": \"splitter\"\n                            },\n                            {\n                                \"key\": \"enabled\",\n                                \"label\": \"Enabled\",\n                                \"type\": \"boolean\"\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"loader\",\n            \"label\": \"Loader\",\n            \"children\": [\n                {\n                    \"type\": \"list\",\n                    \"key\": \"family_filter_profiles\",\n                    \"label\": \"Family filtering\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"hosts-enum\",\n                                \"key\": \"hosts\",\n                                \"label\": \"Hosts\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"type\": \"task-types-enum\",\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"is_include\",\n                                \"label\": \"Exclude / Include\"\n                            },\n                            {\n                                \"type\": \"template\",\n                                \"name\": \"template_publish_families\",\n                                \"template_data\": {\n                                    \"key\": \"filter_families\",\n                                    \"label\": \"Filter families\",\n                                    \"multiselection\": true\n                                }\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"publish\",\n            \"label\": \"Publish\",\n            \"children\": [\n                {\n                    \"type\": \"list\",\n                    \"key\": \"template_name_profiles\",\n                    \"label\": \"Template name profiles\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"families\",\n                                \"label\": \"Families\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"hosts-enum\",\n                                \"key\": \"hosts\",\n                                \"label\": \"Hosts\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\",\n                                \"type\": \"task-types-enum\"\n                            },\n                            {\n                                \"key\": \"task_names\",\n                                \"label\": \"Task names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"template_name\",\n                                \"label\": \"Template name\"\n                            }\n                        ]\n                    }\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"hero_template_name_profiles\",\n                    \"label\": \"Hero template name profiles\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"families\",\n                                \"label\": \"Families\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"hosts-enum\",\n                                \"key\": \"hosts\",\n                                \"label\": \"Hosts\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\",\n                                \"type\": \"task-types-enum\"\n                            },\n                            {\n                                \"key\": \"task_names\",\n                                \"label\": \"Task names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"template_name\",\n                                \"label\": \"Template name\",\n                                \"tooltip\": \"Name of template from Anatomy templates\"\n                            }\n                        ]\n                    }\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"custom_staging_dir_profiles\",\n                    \"label\": \"Custom Staging Dir Profiles\",\n                    \"use_label_wrap\": true,\n                    \"docstring\": \"Profiles to specify special location and persistence for staging dir. Could be used in Creators and Publish phase!\",\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"active\",\n                                \"label\": \"Is active\",\n                                \"default\": true\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"key\": \"hosts\",\n                                \"label\": \"Host names\",\n                                \"type\": \"hosts-enum\",\n                                \"multiselection\": true\n                            },\n                            {\n                                \"key\": \"task_types\",\n                                \"label\": \"Task types\",\n                                \"type\": \"task-types-enum\"\n                            },\n                            {\n                                \"key\": \"task_names\",\n                                \"label\": \"Task names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"key\": \"families\",\n                                \"label\": \"Families\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"key\": \"subsets\",\n                                \"label\": \"Subset names\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"key\": \"custom_staging_dir_persistent\",\n                                \"label\": \"Custom Staging Folder Persistent\",\n                                \"type\": \"boolean\",\n                                \"default\": false\n                            },\n                            {\n                                \"key\": \"template_name\",\n                                \"label\": \"Template Name\",\n                                \"type\": \"text\",\n                                \"placeholder\": \"transient\"\n                            }\n                        ]\n                    }\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"create\",\n    \"label\": \"Creator plugins\",\n    \"children\": [\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_create_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"CreateAlembicCamera\",\n                    \"label\": \"Create Alembic Camera\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateArnoldAss\",\n            \"label\": \"Create Arnold Ass\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"default_variants\",\n                    \"label\": \"Default Variants\",\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"type\": \"enum\",\n                    \"key\": \"ext\",\n                    \"label\": \"Default Output Format (extension)\",\n                    \"multiselection\": false,\n                    \"enum_items\": [\n                        {\n                            \".ass\": \".ass\"\n                        },\n                        {\n                            \".ass.gz\": \".ass.gz (gzipped)\"\n                        }\n                    ]\n                }\n            ]\n\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_create_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"CreateArnoldRop\",\n                    \"label\": \"Create Arnold ROP\"\n                },\n                {\n                    \"key\": \"CreateCompositeSequence\",\n                    \"label\": \"Create Composite (Image Sequence)\"\n                },\n                {\n                    \"key\": \"CreateHDA\",\n                    \"label\": \"Create Houdini Digital Asset\"\n                },\n                {\n                    \"key\": \"CreateKarmaROP\",\n                    \"label\": \"Create Karma ROP\"\n                },\n                {\n                    \"key\": \"CreateMantraIFD\",\n                    \"label\": \"Create Mantra IFD\"\n                },\n                {\n                    \"key\": \"CreateMantraROP\",\n                    \"label\": \"Create Mantra ROP\"\n                },\n                {\n                    \"key\": \"CreatePointCache\",\n                    \"label\": \"Create PointCache (Abc)\"\n                },\n                {\n                    \"key\": \"CreateBGEO\",\n                    \"label\": \"Create PointCache (Bgeo)\"\n                },\n                {\n                    \"key\": \"CreateRedshiftProxy\",\n                    \"label\": \"Create Redshift Proxy\"\n                },\n                {\n                    \"key\": \"CreateRedshiftROP\",\n                    \"label\": \"Create Redshift ROP\"\n                },\n                {\n                    \"key\": \"CreateReview\",\n                    \"label\": \"Create Review\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateStaticMesh\",\n            \"label\": \"Create Static Mesh\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"default_variants\",\n                    \"label\": \"Default Variants\",\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"static_mesh_prefix\",\n                    \"label\": \"Static Mesh Prefix\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"collision_prefixes\",\n                    \"label\": \"Collision Mesh Prefixes\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_create_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"CreateUSD\",\n                    \"label\": \"Create USD (experimental)\"\n                },\n                {\n                    \"key\": \"CreateUSDRender\",\n                    \"label\": \"Create USD render (experimental)\"\n                },\n                {\n                    \"key\": \"CreateVDBCache\",\n                    \"label\": \"Create VDB Cache\"\n                },\n                {\n                    \"key\": \"CreateVrayROP\",\n                    \"label\": \"Create VRay ROP\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"general\",\n    \"label\": \"General\",\n    \"collapsible\": true,\n    \"is_group\": true,\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"add_self_publish_button\",\n            \"label\": \"Add Self Publish Button\"\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"update_houdini_var_context\",\n            \"label\": \"Update Houdini Vars on context change\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Sync vars with context changes.<br>If a value is treated as a directory on update it will be ensured the folder exists\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"houdini_vars\",\n                    \"label\": \"Houdini Vars\",\n                    \"collapsible\": false,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"var\",\n                                \"label\": \"Var\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"value\",\n                                \"label\": \"Value\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"is_directory\",\n                                \"label\": \"Treat as directory\"\n                            }\n                        ]\n                    }\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json",
    "content": "{\n  \"type\": \"dict\",\n  \"collapsible\": true,\n  \"key\": \"publish\",\n  \"label\": \"Publish plugins\",\n  \"children\": [\n    {\n      \"type\":\"label\",\n      \"label\":\"Collectors\"\n    },\n    {\n      \"type\": \"dict\",\n      \"collapsible\": true,\n      \"key\": \"CollectAssetHandles\",\n      \"label\": \"Collect Asset Handles\",\n      \"children\": [\n        {\n          \"type\": \"label\",\n          \"label\": \"Disable this if you want the publisher to ignore start and end handles specified in the asset data for publish instances\"\n        },\n        {\n            \"type\": \"boolean\",\n            \"key\": \"use_asset_handles\",\n            \"label\": \"Use asset handles\"\n        }\n      ]\n    },\n    {\n      \"type\": \"dict\",\n      \"collapsible\": true,\n      \"checkbox_key\": \"enabled\",\n      \"key\": \"CollectChunkSize\",\n      \"label\": \"Collect Chunk Size\",\n      \"is_group\": true,\n      \"children\": [\n        {\n          \"type\": \"boolean\",\n          \"key\": \"enabled\",\n          \"label\": \"Enabled\"\n        },\n        {\n          \"type\": \"boolean\",\n          \"key\": \"optional\",\n          \"label\": \"Optional\"\n        },\n        {\n            \"type\": \"number\",\n            \"key\": \"chunk_size\",\n            \"label\": \"Frames Per Task\"\n        }\n      ]\n    },\n    {\n        \"type\": \"label\",\n        \"label\": \"Validators\"\n    },\n    {\n      \"type\": \"schema_template\",\n      \"name\": \"template_publish_plugin\",\n      \"template_data\": [\n          {\n              \"key\": \"ValidateContainers\",\n              \"label\": \"Validate Containers\"\n          },\n          {\n            \"key\": \"ValidateMeshIsStatic\",\n            \"label\": \"Validate Mesh is Static\"\n          },\n          {\n            \"key\": \"ValidateReviewColorspace\",\n            \"label\": \"Validate Review Colorspace\"\n          },\n          {\n            \"key\": \"ValidateSubsetName\",\n            \"label\": \"Validate Subset Name\"\n          },\n          {\n            \"key\": \"ValidateUnrealStaticMeshName\",\n            \"label\": \"Validate Unreal Static Mesh Name\"\n          }\n      ]\n    },\n    {\n      \"type\": \"dict\",\n      \"collapsible\": true,\n      \"checkbox_key\": \"enabled\",\n      \"key\": \"ValidateWorkfilePaths\",\n      \"label\": \"Validate Workfile Paths\",\n      \"is_group\": true,\n      \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"boolean\",\n            \"key\": \"optional\",\n            \"label\": \"Optional\"\n        },\n        {\n            \"key\": \"node_types\",\n            \"label\": \"Node types\",\n            \"type\": \"list\",\n            \"object_type\": \"text\"\n        },\n        {\n            \"key\": \"prohibited_vars\",\n            \"label\": \"Prohibited variables\",\n            \"type\": \"list\",\n            \"object_type\": \"text\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json",
    "content": "{\n    \"type\": \"list\",\n    \"key\": \"shelves\",\n    \"label\": \"Shelves Manager\",\n    \"is_group\": true,\n    \"use_label_wrap\": true,\n    \"object_type\": {\n        \"type\": \"dict-conditional\",\n        \"enum_key\": \"options\",\n        \"enum_label\": \"Options\",\n        \"enum_children\": [\n            {\n\n                \"key\": \"add_shelf_file\",\n                \"label\": \"Add a .shelf file\",\n                \"children\": [\n                    {\n                        \"type\": \"dict\",\n                        \"key\": \"add_shelf_file\",\n                        \"label\": \"Add a .shelf file\",\n                        \"children\": [\n                            {\n                                \"type\": \"path\",\n                                \"key\": \"shelf_set_source_path\",\n                                \"label\": \"Shelf Set Path\",\n                                \"multipath\": false,\n                                \"multiplatform\": true\n                            }\n                        ]\n                    }\n                ]\n            },\n            {\n                \"key\": \"add_set_and_definitions\",\n                \"label\": \"Add Shelf Set Name and Shelves Definitions\",\n                \"children\": [\n                    {\n                        \"key\": \"add_set_and_definitions\",\n                        \"label\": \"Add Shelf Set Name and Shelves Definitions\",\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"shelf_set_name\",\n                                \"label\": \"Shelf Set Name\"\n                            },\n                            {\n                                \"type\": \"list\",\n                                \"key\": \"shelf_definition\",\n                                \"label\": \"Shelves Definitions\",\n                                \"use_label_wrap\": true,\n                                \"object_type\": {\n                                    \"type\": \"dict\",\n                                    \"children\": [\n                                        {\n                                            \"type\": \"text\",\n                                            \"key\": \"shelf_name\",\n                                            \"label\": \"Shelf Name\"\n                                        },\n                                        {\n                                            \"type\": \"list\",\n                                            \"key\": \"tools_list\",\n                                            \"label\": \"Tools\",\n                                            \"use_label_wrap\": true,\n                                            \"object_type\": {\n                                                \"type\": \"dict\",\n                                                \"children\": [\n                                                    {\n                                                        \"type\": \"label\",\n                                                        \"label\": \"Name and Script Path are mandatory.\"\n                                                    },\n                                                    {\n                                                        \"type\": \"text\",\n                                                        \"key\": \"label\",\n                                                        \"label\": \"Name\"\n                                                    },\n                                                    {\n                                                        \"type\": \"path\",\n                                                        \"key\": \"script\",\n                                                        \"label\": \"Script\"\n                                                    },\n                                                    {\n                                                        \"type\": \"path\",\n                                                        \"key\": \"icon\",\n                                                        \"label\": \"Icon\"\n                                                    },\n                                                    {\n                                                        \"type\": \"text\",\n                                                        \"key\": \"help\",\n                                                        \"label\": \"Help\"\n                                                    }\n                                                ]\n                                            }\n                                        }\n                                    ]\n                                }\n                            }\n                        ]\n                    }\n                ]\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"publish\",\n    \"label\": \"Publish plugins\",\n    \"children\": [\n      {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"checkbox_key\": \"enabled\",\n        \"key\": \"ValidateInstanceInContext\",\n        \"label\": \"Validate Instance In Context\",\n        \"is_group\": true,\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"optional\",\n                \"label\": \"Optional\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"active\",\n                \"label\": \"Active\"\n            }\n        ]\n      },\n      {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"checkbox_key\": \"enabled\",\n        \"key\": \"ValidateFrameRange\",\n        \"label\": \"Validate Frame Range\",\n        \"is_group\": true,\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"optional\",\n                \"label\": \"Optional\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"active\",\n                \"label\": \"Active\"\n            }\n        ]\n      },\n      {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"key\": \"ValidateAttributes\",\n        \"label\": \"ValidateAttributes\",\n        \"checkbox_key\": \"enabled\",\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"raw-json\",\n                \"key\": \"attributes\",\n                \"label\": \"Attributes\"\n            }\n        ]\n      },\n      {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"checkbox_key\": \"enabled\",\n        \"key\": \"ValidateCameraAttributes\",\n        \"label\": \"Validate Camera Attributes\",\n        \"is_group\": true,\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"optional\",\n                \"label\": \"Optional\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"active\",\n                \"label\": \"Active\"\n            },\n            {\n                \"type\": \"number\",\n                \"key\": \"fov\",\n                \"label\": \"Focal Length\",\n                \"decimal\": 1,\n                \"minimum\": 0,\n                \"maximum\": 100.0\n            },\n            {\n                \"type\": \"label\",\n                \"label\": \"If the value of the camera attributes set to 0, the system automatically skips checking it\"\n            },\n            {\n                \"type\": \"number\",\n                \"key\": \"nearrange\",\n                \"label\": \"Near Range\",\n                \"decimal\": 1,\n                \"minimum\": 0,\n                \"maximum\": 100.0\n            },\n            {\n                \"type\": \"number\",\n                \"key\": \"farrange\",\n                \"label\": \"Far Range\",\n                \"decimal\": 1,\n                \"minimum\": 0,\n                \"maximum\": 2000.0\n            },\n            {\n                \"type\": \"number\",\n                \"key\": \"nearclip\",\n                \"label\": \"Near Clip\",\n                \"decimal\": 1,\n                \"minimum\": 0,\n                \"maximum\": 100.0\n            },\n            {\n                \"type\": \"number\",\n                \"key\": \"farclip\",\n                \"label\": \"Far Clip\",\n                \"decimal\": 1,\n                \"minimum\": 0,\n                \"maximum\": 2000.0\n            }\n        ]\n      },\n\n      {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"key\": \"ValidateLoadedPlugin\",\n        \"label\": \"Validate Loaded Plugin\",\n        \"checkbox_key\": \"enabled\",\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"optional\",\n                \"label\": \"Optional\"\n            },\n            {\n                \"type\": \"list\",\n                \"collapsible\": true,\n                \"key\": \"family_plugins_mapping\",\n                \"label\": \"Family Plugins Mapping\",\n                \"use_label_wrap\": true,\n                \"object_type\": {\n                    \"type\": \"dict\",\n                    \"children\": [\n                        {\n                            \"key\": \"families\",\n                            \"label\": \"Famiies\",\n                            \"type\": \"list\",\n                            \"object_type\": \"text\"\n                        },\n                        {\n                            \"key\": \"plugins\",\n                            \"label\": \"Plugins\",\n                            \"type\": \"list\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                }\n            }\n        ]\n      },\n      {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"checkbox_key\": \"enabled\",\n        \"key\": \"ValidateRenderPasses\",\n        \"label\": \"Validate Render Passes\",\n        \"is_group\": true,\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"optional\",\n                \"label\": \"Optional\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"active\",\n                \"label\": \"Active\"\n            }\n        ]\n      },\n      {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"checkbox_key\": \"enabled\",\n        \"key\": \"ExtractModelObj\",\n        \"label\": \"Extract Obj\",\n        \"is_group\": true,\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"optional\",\n                \"label\": \"Optional\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"active\",\n                \"label\": \"Active\"\n            }\n        ]\n      },\n      {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"checkbox_key\": \"enabled\",\n        \"key\": \"ExtractModelFbx\",\n        \"label\": \"Extract FBX\",\n        \"is_group\": true,\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"optional\",\n                \"label\": \"Optional\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"active\",\n                \"label\": \"Active\"\n            }\n        ]\n      },\n      {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"checkbox_key\": \"enabled\",\n        \"key\": \"ExtractModelUSD\",\n        \"label\": \"Extract Geometry (USD)\",\n        \"is_group\": true,\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"optional\",\n                \"label\": \"Optional\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"active\",\n                \"label\": \"Active\"\n            }\n        ]\n      },\n      {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"checkbox_key\": \"enabled\",\n        \"key\": \"ExtractModel\",\n        \"label\": \"Extract Geometry (Alembic)\",\n        \"is_group\": true,\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"optional\",\n                \"label\": \"Optional\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"active\",\n                \"label\": \"Active\"\n            }\n        ]\n      },\n      {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"checkbox_key\": \"enabled\",\n        \"key\": \"ExtractMaxSceneRaw\",\n        \"label\": \"Extract Max Scene (Raw)\",\n        \"is_group\": true,\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"optional\",\n                \"label\": \"Optional\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"active\",\n                \"label\": \"Active\"\n            }\n        ]\n      }\n    ]\n  }\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"ExtractPlayblast\",\n    \"label\": \"Extract Playblast settings\",\n    \"children\": [\n        {\n            \"type\": \"dict\",\n            \"key\": \"capture_preset\",\n            \"label\": \"DEPRECATED! Please use \\\"Profiles\\\" below.\",\n            \"collapsed\": false,\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"Codec\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Codec</b>\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"compression\",\n                            \"label\": \"Encoding\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"format\",\n                            \"label\": \"Format\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"quality\",\n                            \"label\": \"Quality\",\n                            \"decimal\": 0,\n                            \"minimum\": 0,\n                            \"maximum\": 100\n                        },\n\n                        {\n                            \"type\": \"splitter\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"Display Options\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Display Options</b>\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"override_display\",\n                            \"label\": \"Override display options\"\n                        },\n                        {\n                            \"type\": \"color\",\n                            \"key\": \"background\",\n                            \"label\": \"Background Color:    \"\n                        },\n                        {\n                            \"type\": \"color\",\n                            \"key\": \"backgroundBottom\",\n                            \"label\": \"Background Bottom:    \"\n                        },\n                        {\n                            \"type\": \"color\",\n                            \"key\": \"backgroundTop\",\n                            \"label\": \"Background Top:    \"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"displayGradient\",\n                            \"label\": \"Display background gradient\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"Generic\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Generic</b>\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"isolate_view\",\n                            \"label\": \"            Isolate view\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"off_screen\",\n                            \"label\": \"            Off Screen\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"pan_zoom\",\n                            \"label\": \"            2D Pan/Zoom\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"Renderer\",\n                    \"children\": [\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Renderer</b>\"\n                        },\n                        {\n                            \"type\": \"enum\",\n                            \"key\": \"rendererName\",\n                            \"label\": \"Renderer name\",\n                            \"enum_items\": [\n                                { \"vp2Renderer\": \"Viewport 2.0\" }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"Resolution\",\n                    \"children\": [\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Resolution</b>\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"width\",\n                            \"label\": \"                  Width\",\n                            \"decimal\": 0,\n                            \"minimum\": 0,\n                            \"maximum\": 99999\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"height\",\n                            \"label\": \"Height\",\n                            \"decimal\": 0,\n                            \"minimum\": 0,\n                            \"maximum\": 99999\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"Viewport Options\",\n                    \"label\": \"Viewport Options\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"override_viewport_options\",\n                            \"label\": \"Override Viewport Options\"\n                        },\n                        {\n                            \"type\": \"enum\",\n                            \"key\": \"displayLights\",\n                            \"label\": \"Display Lights\",\n                            \"enum_items\": [\n                                { \"default\": \"Default Lighting\"},\n                                { \"all\": \"All Lights\"},\n                                { \"selected\": \"Selected Lights\"},\n                                { \"flat\": \"Flat Lighting\"},\n                                { \"none\": \"No Lights\"}\n                            ]\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"displayTextures\",\n                            \"label\": \"Display Textures\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"textureMaxResolution\",\n                            \"label\": \"Texture Clamp Resolution\",\n                            \"decimal\": 0\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Display</b>\"\n                        },\n                        {\n                            \"type\":\"boolean\",\n                            \"key\": \"renderDepthOfField\",\n                            \"label\": \"Depth of Field\"\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"shadows\",\n                            \"label\": \"Display Shadows\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"twoSidedLighting\",\n                            \"label\": \"Two Sided Lighting\"\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"lineAAEnable\",\n                            \"label\": \"Enable Anti-Aliasing\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"multiSample\",\n                            \"label\": \"Anti Aliasing Samples\",\n                            \"decimal\": 0,\n                            \"minimum\": 0,\n                            \"maximum\": 32\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"loadTextures\",\n                            \"label\": \"Load Textures\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"useDefaultMaterial\",\n                            \"label\": \"Use Default Material\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"wireframeOnShaded\",\n                            \"label\": \"Wireframe On Shaded\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"xray\",\n                            \"label\": \"X-Ray\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"jointXray\",\n                            \"label\": \"X-Ray Joints\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"backfaceCulling\",\n                            \"label\": \"Backface Culling\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"ssaoEnable\",\n                            \"label\": \"Screen Space Ambient Occlusion\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"ssaoAmount\",\n                            \"label\": \"SSAO Amount\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"ssaoRadius\",\n                            \"label\": \"SSAO Radius\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"ssaoFilterRadius\",\n                            \"label\": \"SSAO Filter Radius\",\n                            \"decimal\": 0,\n                            \"minimum\": 1,\n                            \"maximum\": 32\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"ssaoSamples\",\n                            \"label\": \"SSAO Samples\",\n                            \"decimal\": 0,\n                            \"minimum\": 8,\n                            \"maximum\": 32\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"fogging\",\n                            \"label\": \"Enable Hardware Fog\"\n                        },\n                        {\n                            \"type\": \"enum\",\n                            \"key\": \"hwFogFalloff\",\n                            \"label\": \"Hardware Falloff\",\n                            \"enum_items\": [\n                                { \"0\": \"Linear\"},\n                                { \"1\": \"Exponential\"},\n                                { \"2\": \"Exponential Squared\"}\n                            ]\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"hwFogDensity\",\n                            \"label\": \"Fog Density\",\n                            \"decimal\": 2,\n                            \"minimum\": 0,\n                            \"maximum\": 1\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"hwFogStart\",\n                            \"label\": \"Fog Start\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"hwFogEnd\",\n                            \"label\": \"Fog End\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"hwFogAlpha\",\n                            \"label\": \"Fog Alpha\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"hwFogColorR\",\n                            \"label\": \"Fog Color R\",\n                            \"decimal\": 2,\n                            \"minimum\": 0,\n                            \"maximum\": 1\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"hwFogColorG\",\n                            \"label\": \"Fog Color G\",\n                            \"decimal\": 2,\n                            \"minimum\": 0,\n                            \"maximum\": 1\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"hwFogColorB\",\n                            \"label\": \"Fog Color B\",\n                            \"decimal\": 2,\n                            \"minimum\": 0,\n                            \"maximum\": 1\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"motionBlurEnable\",\n                            \"label\": \"Enable Motion Blur\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"motionBlurSampleCount\",\n                            \"label\": \"Motion Blur Sample Count\",\n                            \"decimal\": 0,\n                            \"minimum\": 8,\n                            \"maximum\": 32\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"motionBlurShutterOpenFraction\",\n                            \"label\": \"Shutter Open Fraction\",\n                            \"decimal\": 3,\n                            \"minimum\": 0.01,\n                            \"maximum\": 32\n                        },\n                        {\n                            \"type\": \"splitter\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"<b>Show</b>\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"cameras\",\n                            \"label\": \"Cameras\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"clipGhosts\",\n                            \"label\": \"Clip Ghosts\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"deformers\",\n                            \"label\": \"Deformers\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"dimensions\",\n                            \"label\": \"Dimensions\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"dynamicConstraints\",\n                            \"label\": \"Dynamic Constraints\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"dynamics\",\n                            \"label\": \"Dynamics\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"fluids\",\n                            \"label\": \"Fluids\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"follicles\",\n                            \"label\": \"Follicles\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"greasePencils\",\n                            \"label\": \"Grease Pencil\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"grid\",\n                            \"label\": \"Grid\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"hairSystems\",\n                            \"label\": \"Hair Systems\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"handles\",\n                            \"label\": \"Handles\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"headsUpDisplay\",\n                            \"label\": \"HUD\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"ikHandles\",\n                            \"label\": \"IK Handles\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"imagePlane\",\n                            \"label\": \"Image Planes\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"joints\",\n                            \"label\": \"Joints\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"lights\",\n                            \"label\": \"Lights\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"locators\",\n                            \"label\": \"Locators\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"manipulators\",\n                            \"label\": \"Manipulators\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"motionTrails\",\n                            \"label\": \"Motion Trails\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"nCloths\",\n                            \"label\": \"nCloths\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"nParticles\",\n                            \"label\": \"nParticles\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"nRigids\",\n                            \"label\": \"nRigids\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"controlVertices\",\n                            \"label\": \"NURBS CVs\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"nurbsCurves\",\n                            \"label\": \"NURBS Curves\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"hulls\",\n                            \"label\": \"NURBS Hulls\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"nurbsSurfaces\",\n                            \"label\": \"NURBS Surfaces\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"particleInstancers\",\n                            \"label\": \"Particle Instancers\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"pivots\",\n                            \"label\": \"Pivots\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"planes\",\n                            \"label\": \"Planes\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"pluginShapes\",\n                            \"label\": \"Plugin Shapes\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"polymeshes\",\n                            \"label\": \"Polygons\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"strokes\",\n                            \"label\": \"Strokes\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"subdivSurfaces\",\n                            \"label\": \"Subdiv Surfaces\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"textures\",\n                            \"label\": \"Texture Placements\"\n                        },\n                        {\n                            \"type\": \"dict-modifiable\",\n                            \"key\": \"pluginObjects\",\n                            \"label\": \"Plugin Objects\",\n                            \"object_type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"Camera Options\",\n                    \"label\": \"Camera Options\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"displayGateMask\",\n                            \"label\": \"Display Gate Mask\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"displayResolution\",\n                            \"label\": \"Display Resolution\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"displayFilmGate\",\n                            \"label\": \"Display Film Gate\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"displayFieldChart\",\n                            \"label\": \"Display Field Chart\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"displaySafeAction\",\n                            \"label\": \"Display Safe Action\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"displaySafeTitle\",\n                            \"label\": \"Display Safe Title\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"displayFilmPivot\",\n                            \"label\": \"Display Film Pivot\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"displayFilmOrigin\",\n                            \"label\": \"Display Film Origin\"\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"overscan\",\n                            \"label\": \"Overscan\",\n                            \"decimal\": 1,\n                            \"minimum\": 0,\n                            \"maximum\": 10\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"list\",\n            \"key\": \"profiles\",\n            \"label\": \"Profiles\",\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"children\": [\n                    {\n                        \"key\": \"task_types\",\n                        \"label\": \"Task types\",\n                        \"type\": \"task-types-enum\"\n                    },\n                    {\n                        \"key\": \"task_names\",\n                        \"label\": \"Task names\",\n                        \"type\": \"list\",\n                        \"object_type\": \"text\"\n                    },\n                    {\n                        \"key\": \"subsets\",\n                        \"label\": \"Subset names\",\n                        \"type\": \"list\",\n                        \"object_type\": \"text\"\n                    },\n                    {\n                        \"type\": \"splitter\"\n                    },\n                    {\n                        \"type\": \"dict\",\n                        \"key\": \"capture_preset\",\n                        \"children\": [\n                            {\n                                \"type\": \"dict\",\n                                \"key\": \"Codec\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"label\",\n                                        \"label\": \"<b>Codec</b>\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"compression\",\n                                        \"label\": \"Encoding\",\n                                        \"default\": \"png\"\n                                    },\n                                    {\n                                        \"type\": \"text\",\n                                        \"key\": \"format\",\n                                        \"label\": \"Format\",\n                                        \"default\": \"image\"\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"quality\",\n                                        \"label\": \"Quality\",\n                                        \"decimal\": 0,\n                                        \"minimum\": 0,\n                                        \"maximum\": 100,\n                                        \"default\": 95\n                                    },\n                                    {\n                                        \"type\": \"splitter\"\n                                    }\n                                ]\n                            },\n                            {\n                                \"type\": \"dict\",\n                                \"key\": \"Display Options\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"label\",\n                                        \"label\": \"<b>Display Options</b>\"\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"override_display\",\n                                        \"label\": \"Override display options\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"color\",\n                                        \"key\": \"background\",\n                                        \"label\": \"Background Color:    \",\n                                        \"default\": [125, 125, 125, 255]\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"displayGradient\",\n                                        \"label\": \"Display background gradient\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"color\",\n                                        \"key\": \"backgroundBottom\",\n                                        \"label\": \"Background Bottom:    \",\n                                        \"default\": [125, 125, 125, 255]\n                                    },\n                                    {\n                                        \"type\": \"color\",\n                                        \"key\": \"backgroundTop\",\n                                        \"label\": \"Background Top:    \",\n                                        \"default\": [125, 125, 125, 255]\n                                    }\n                                ]\n                            },\n                            {\n                                \"type\": \"splitter\"\n                            },\n                            {\n                                \"type\": \"dict\",\n                                \"key\": \"Generic\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"label\",\n                                        \"label\": \"<b>Generic</b>\"\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"isolate_view\",\n                                        \"label\": \"            Isolate view\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"off_screen\",\n                                        \"label\": \"            Off Screen\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"pan_zoom\",\n                                        \"label\": \"            2D Pan/Zoom\",\n                                        \"default\": false\n                                    }\n                                ]\n                            },\n                            {\n                                \"type\": \"splitter\"\n                            },\n                            {\n                                \"type\": \"dict\",\n                                \"key\": \"Renderer\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"label\",\n                                        \"label\": \"<b>Renderer</b>\"\n                                    },\n                                    {\n                                        \"type\": \"enum\",\n                                        \"key\": \"rendererName\",\n                                        \"label\": \"Renderer name\",\n                                        \"enum_items\": [\n                                            { \"vp2Renderer\": \"Viewport 2.0\" }\n                                        ],\n                                        \"default\": \"vp2Renderer\"\n                                    }\n                                ]\n                            },\n                            {\n                                \"type\": \"dict\",\n                                \"key\": \"Resolution\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"splitter\"\n                                    },\n                                    {\n                                        \"type\": \"label\",\n                                        \"label\": \"<b>Resolution</b>\"\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"width\",\n                                        \"label\": \"                  Width\",\n                                        \"decimal\": 0,\n                                        \"minimum\": 0,\n                                        \"maximum\": 99999,\n                                        \"default\": 0\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"height\",\n                                        \"label\": \"Height\",\n                                        \"decimal\": 0,\n                                        \"minimum\": 0,\n                                        \"maximum\": 99999,\n                                        \"default\": 0\n                                    }\n                                ]\n                            },\n                            {\n                                \"type\": \"splitter\"\n                            },\n                            {\n                                \"type\": \"dict\",\n                                \"collapsible\": true,\n                                \"key\": \"Viewport Options\",\n                                \"label\": \"Viewport Options\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"override_viewport_options\",\n                                        \"label\": \"Override Viewport Options\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"enum\",\n                                        \"key\": \"displayLights\",\n                                        \"label\": \"Display Lights\",\n                                        \"enum_items\": [\n                                            { \"default\": \"Default Lighting\"},\n                                            { \"all\": \"All Lights\"},\n                                            { \"selected\": \"Selected Lights\"},\n                                            { \"flat\": \"Flat Lighting\"},\n                                            { \"nolights\": \"No Lights\"}\n                                        ],\n                                        \"default\": \"default\"\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"displayTextures\",\n                                        \"label\": \"Display Textures\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"textureMaxResolution\",\n                                        \"label\": \"Texture Clamp Resolution\",\n                                        \"decimal\": 0,\n                                        \"default\": 1024\n                                    },\n                                    {\n                                        \"type\": \"splitter\"\n                                    },\n                                    {\n                                        \"type\": \"label\",\n                                        \"label\": \"<b>Display</b>\"\n                                    },\n                                    {\n                                        \"type\":\"boolean\",\n                                        \"key\": \"renderDepthOfField\",\n                                        \"label\": \"Depth of Field\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"splitter\"\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"shadows\",\n                                        \"label\": \"Display Shadows\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"twoSidedLighting\",\n                                        \"label\": \"Two Sided Lighting\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"splitter\"\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"lineAAEnable\",\n                                        \"label\": \"Enable Anti-Aliasing\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"multiSample\",\n                                        \"label\": \"Anti Aliasing Samples\",\n                                        \"decimal\": 0,\n                                        \"minimum\": 0,\n                                        \"maximum\": 32,\n                                        \"default\": 8\n                                    },\n                                    {\n                                        \"type\": \"splitter\"\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"loadTextures\",\n                                        \"label\": \"Load Textures\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"useDefaultMaterial\",\n                                        \"label\": \"Use Default Material\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"wireframeOnShaded\",\n                                        \"label\": \"Wireframe On Shaded\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"xray\",\n                                        \"label\": \"X-Ray\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"jointXray\",\n                                        \"label\": \"X-Ray Joints\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"backfaceCulling\",\n                                        \"label\": \"Backface Culling\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"ssaoEnable\",\n                                        \"label\": \"Screen Space Ambient Occlusion\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"ssaoAmount\",\n                                        \"label\": \"SSAO Amount\",\n                                        \"default\": 1\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"ssaoRadius\",\n                                        \"label\": \"SSAO Radius\",\n                                        \"default\": 16\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"ssaoFilterRadius\",\n                                        \"label\": \"SSAO Filter Radius\",\n                                        \"decimal\": 0,\n                                        \"minimum\": 1,\n                                        \"maximum\": 32,\n                                        \"default\": 16\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"ssaoSamples\",\n                                        \"label\": \"SSAO Samples\",\n                                        \"decimal\": 0,\n                                        \"minimum\": 8,\n                                        \"maximum\": 32,\n                                        \"default\": 16\n                                    },\n                                    {\n                                        \"type\": \"splitter\"\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"fogging\",\n                                        \"label\": \"Enable Hardware Fog\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"enum\",\n                                        \"key\": \"hwFogFalloff\",\n                                        \"label\": \"Hardware Falloff\",\n                                        \"enum_items\": [\n                                            { \"0\": \"Linear\"},\n                                            { \"1\": \"Exponential\"},\n                                            { \"2\": \"Exponential Squared\"}\n                                        ],\n                                        \"default\": \"0\"\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"hwFogDensity\",\n                                        \"label\": \"Fog Density\",\n                                        \"decimal\": 2,\n                                        \"minimum\": 0,\n                                        \"maximum\": 1,\n                                        \"default\": 0\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"hwFogStart\",\n                                        \"label\": \"Fog Start\",\n                                        \"default\": 0\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"hwFogEnd\",\n                                        \"label\": \"Fog End\",\n                                        \"default\": 100\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"hwFogAlpha\",\n                                        \"label\": \"Fog Alpha\",\n                                        \"default\": 0\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"hwFogColorR\",\n                                        \"label\": \"Fog Color R\",\n                                        \"decimal\": 2,\n                                        \"minimum\": 0,\n                                        \"maximum\": 1,\n                                        \"default\": 1\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"hwFogColorG\",\n                                        \"label\": \"Fog Color G\",\n                                        \"decimal\": 2,\n                                        \"minimum\": 0,\n                                        \"maximum\": 1,\n                                        \"default\": 1\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"hwFogColorB\",\n                                        \"label\": \"Fog Color B\",\n                                        \"decimal\": 2,\n                                        \"minimum\": 0,\n                                        \"maximum\": 1,\n                                        \"default\": 1\n                                    },\n                                    {\n                                        \"type\": \"splitter\"\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"motionBlurEnable\",\n                                        \"label\": \"Enable Motion Blur\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"motionBlurSampleCount\",\n                                        \"label\": \"Motion Blur Sample Count\",\n                                        \"decimal\": 0,\n                                        \"minimum\": 8,\n                                        \"maximum\": 32,\n                                        \"default\": 8\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"motionBlurShutterOpenFraction\",\n                                        \"label\": \"Shutter Open Fraction\",\n                                        \"decimal\": 3,\n                                        \"minimum\": 0.01,\n                                        \"maximum\": 32,\n                                        \"default\": 0.2\n                                    },\n                                    {\n                                        \"type\": \"splitter\"\n                                    },\n                                    {\n                                        \"type\": \"label\",\n                                        \"label\": \"<b>Show</b>\"\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"cameras\",\n                                        \"label\": \"Cameras\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"clipGhosts\",\n                                        \"label\": \"Clip Ghosts\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"deformers\",\n                                        \"label\": \"Deformers\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"dimensions\",\n                                        \"label\": \"Dimensions\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"dynamicConstraints\",\n                                        \"label\": \"Dynamic Constraints\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"dynamics\",\n                                        \"label\": \"Dynamics\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"fluids\",\n                                        \"label\": \"Fluids\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"follicles\",\n                                        \"label\": \"Follicles\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"greasePencils\",\n                                        \"label\": \"Grease Pencil\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"grid\",\n                                        \"label\": \"Grid\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"hairSystems\",\n                                        \"label\": \"Hair Systems\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"handles\",\n                                        \"label\": \"Handles\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"headsUpDisplay\",\n                                        \"label\": \"HUD\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"ikHandles\",\n                                        \"label\": \"IK Handles\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"imagePlane\",\n                                        \"label\": \"Image Planes\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"joints\",\n                                        \"label\": \"Joints\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"lights\",\n                                        \"label\": \"Lights\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"locators\",\n                                        \"label\": \"Locators\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"manipulators\",\n                                        \"label\": \"Manipulators\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"motionTrails\",\n                                        \"label\": \"Motion Trails\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"nCloths\",\n                                        \"label\": \"nCloths\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"nParticles\",\n                                        \"label\": \"nParticles\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"nRigids\",\n                                        \"label\": \"nRigids\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"controlVertices\",\n                                        \"label\": \"NURBS CVs\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"nurbsCurves\",\n                                        \"label\": \"NURBS Curves\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"hulls\",\n                                        \"label\": \"NURBS Hulls\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"nurbsSurfaces\",\n                                        \"label\": \"NURBS Surfaces\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"particleInstancers\",\n                                        \"label\": \"Particle Instancers\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"pivots\",\n                                        \"label\": \"Pivots\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"planes\",\n                                        \"label\": \"Planes\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"pluginShapes\",\n                                        \"label\": \"Plugin Shapes\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"polymeshes\",\n                                        \"label\": \"Polygons\",\n                                        \"default\": true\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"strokes\",\n                                        \"label\": \"Strokes\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"subdivSurfaces\",\n                                        \"label\": \"Subdiv Surfaces\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"textures\",\n                                        \"label\": \"Texture Placements\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"dict-modifiable\",\n                                        \"key\": \"pluginObjects\",\n                                        \"label\": \"Plugin Objects\",\n                                        \"object_type\": \"boolean\"\n                                    }\n                                ]\n                            },\n                            {\n                                \"type\": \"dict\",\n                                \"collapsible\": true,\n                                \"key\": \"Camera Options\",\n                                \"label\": \"Camera Options\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"displayGateMask\",\n                                        \"label\": \"Display Gate Mask\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"displayResolution\",\n                                        \"label\": \"Display Resolution\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"displayFilmGate\",\n                                        \"label\": \"Display Film Gate\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"displayFieldChart\",\n                                        \"label\": \"Display Field Chart\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"displaySafeAction\",\n                                        \"label\": \"Display Safe Action\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"displaySafeTitle\",\n                                        \"label\": \"Display Safe Title\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"displayFilmPivot\",\n                                        \"label\": \"Display Film Pivot\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"displayFilmOrigin\",\n                                        \"label\": \"Display Film Origin\",\n                                        \"default\": false\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"overscan\",\n                                        \"label\": \"Overscan\",\n                                        \"decimal\": 1,\n                                        \"minimum\": 0,\n                                        \"maximum\": 10,\n                                        \"default\": 1\n                                    }\n                                ]\n                            }\n                        ]\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"create\",\n    \"label\": \"Creator plugins\",\n    \"children\": [\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateLook\",\n            \"label\": \"Create Look\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"make_tx\",\n                    \"label\": \"Make tx files\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"rs_tex\",\n                    \"label\": \"Make Redshift texture files\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"default_variants\",\n                    \"label\": \"Default Variants\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_create_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"CreateRender\",\n                    \"label\": \"Create Render\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateUnrealStaticMesh\",\n            \"label\": \"Create Unreal - Static Mesh\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"default_variants\",\n                    \"label\": \"Default Variants\",\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"static_mesh_prefix\",\n                    \"label\": \"Static Mesh Prefix\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"collision_prefixes\",\n                    \"label\": \"Collision Mesh Prefixes\",\n                    \"object_type\": \"text\"\n                }\n            ]\n\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateUnrealSkeletalMesh\",\n            \"label\": \"Create Unreal - Skeletal Mesh\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"default_variants\",\n                    \"label\": \"Default Variants\",\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"joint_hints\",\n                    \"label\": \"Joint root hint\"\n                }\n            ]\n\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateMultiverseLook\",\n            \"label\": \"Create Multiverse Look\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"publish_mip_map\",\n                    \"label\": \"Publish Mip Maps\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateAnimation\",\n            \"label\": \"Create Animation\",\n            \"children\": [\n                {\n                    \"type\": \"label\",\n                    \"label\": \"This plugin is not optional due to implicit creation through loading the \\\"rig\\\" family.\\nThis family is also hidden from creation due to complexity in setup.\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"default_variants\",\n                    \"label\": \"Default Variants\",\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"includeParentHierarchy\",\n                    \"label\": \"Include Parent Hierarchy\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"farm\",\n                    \"label\": \"Submit to the Farm\"\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"priority\",\n                    \"label\": \"Farm Job Priority\",\n                    \"minimum\": 0\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"refresh\",\n                    \"label\": \"Refresh\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"include_user_defined_attributes\",\n                    \"label\": \"Include User Defined Attributes\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateModel\",\n            \"label\": \"Create Model\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"write_color_sets\",\n                    \"label\": \"Write Color Sets\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"write_face_sets\",\n                    \"label\": \"Write Face Sets\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"default_variants\",\n                    \"label\": \"Default Variants\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreatePointCache\",\n            \"label\": \"Create Point Cache\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"default_variants\",\n                    \"label\": \"Default Variants\",\n                    \"object_type\": \"text\"\n                },\n                {\n                  \"type\": \"number\",\n                  \"key\": \"step\",\n                  \"label\": \"Step default\",\n                  \"minimum\": 0.0,\n                  \"decimal\": 4\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"includeParentHierarchy\",\n                    \"label\": \"Include Parent Hierarchy default\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"farm\",\n                    \"label\": \"Farm default\"\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"priority\",\n                    \"label\": \"Priority default\",\n                    \"minimum\": 0\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"refresh\",\n                    \"label\": \"Refresh default\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"include_user_defined_attributes\",\n                    \"label\": \"Include User Defined Attributes\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateProxyAlembic\",\n            \"label\": \"Create Proxy Alembic\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"write_color_sets\",\n                    \"label\": \"Write Color Sets\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"write_face_sets\",\n                    \"label\": \"Write Face Sets\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"default_variants\",\n                    \"label\": \"Default Variants\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateReview\",\n            \"label\": \"Create Review\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"default_variants\",\n                    \"label\": \"Default Variants\",\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"useMayaTimeline\",\n                    \"label\": \"Use Maya Timeline for Frame Range.\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateAss\",\n            \"label\": \"Create Ass\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"default_variants\",\n                    \"label\": \"Default Variants\",\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"expandProcedurals\",\n                    \"label\": \"Expand Procedurals\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"motionBlur\",\n                    \"label\": \"Motion Blur\"\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"motionBlurKeys\",\n                    \"label\": \"Motion Blur Keys\",\n                    \"minimum\": 0\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"motionBlurLength\",\n                    \"label\": \"Motion Blur Length\",\n                    \"decimal\": 3\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"maskOptions\",\n                    \"label\": \"Export Options\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"maskCamera\",\n                    \"label\": \"Export Cameras\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"maskLight\",\n                    \"label\": \"Export Lights\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"maskShape\",\n                    \"label\": \"Export Shapes\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"maskShader\",\n                    \"label\": \"Export Shaders\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"maskOverride\",\n                    \"label\": \"Export Override Nodes\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"maskDriver\",\n                    \"label\": \"Export Drivers\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"maskFilter\",\n                    \"label\": \"Export Filters\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"maskOperator\",\n                    \"label\": \"Export Operators\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"maskColor_manager\",\n                    \"label\": \"Export Color Managers\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CreateVrayProxy\",\n            \"label\": \"Create VRay Proxy\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"vrmesh\",\n                    \"label\": \"VrMesh\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"alembic\",\n                    \"label\": \"Alembic\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"default_variants\",\n                    \"label\": \"Default Variants\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_create_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"CreateMultiverseUsd\",\n                    \"label\": \"Create Multiverse USD\"\n                },\n                {\n                    \"key\": \"CreateMultiverseUsdComp\",\n                    \"label\": \"Create Multiverse USD Composition\"\n                },\n                {\n                    \"key\": \"CreateMultiverseUsdOver\",\n                    \"label\": \"Create Multiverse USD Override\"\n                },\n                {\n                    \"key\": \"CreateAssembly\",\n                    \"label\": \"Create Assembly\"\n                },\n                {\n                    \"key\": \"CreateCamera\",\n                    \"label\": \"Create Camera\"\n                },\n                {\n                    \"key\": \"CreateLayout\",\n                    \"label\": \"Create Layout\"\n                },\n                {\n                    \"key\": \"CreateMayaScene\",\n                    \"label\": \"Create Maya Scene\"\n                },\n                {\n                    \"key\": \"CreateRenderSetup\",\n                    \"label\": \"Create Render Setup\"\n                },\n                {\n                    \"key\": \"CreateRig\",\n                    \"label\": \"Create Rig\"\n                },\n                {\n                    \"key\": \"CreateSetDress\",\n                    \"label\": \"Create Set Dress\"\n                },\n                {\n                    \"key\": \"CreateVRayScene\",\n                    \"label\": \"Create VRay Scene\"\n                },\n                {\n                    \"key\": \"CreateYetiRig\",\n                    \"label\": \"Create Yeti Rig\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"load\",\n    \"label\": \"Loader plugins\",\n    \"children\": [\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"colors\",\n            \"label\": \"Loaded Subsets Outliner Colors\",\n            \"children\": [\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Model:\",\n                    \"key\": \"model\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Rig:\",\n                    \"key\": \"rig\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Pointcache:\",\n                    \"key\": \"pointcache\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Animation:\",\n                    \"key\": \"animation\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Arnold Standin:\",\n                    \"key\": \"ass\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Camera:\",\n                    \"key\": \"camera\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"FBX:\",\n                    \"key\": \"fbx\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Maya Ascii:\",\n                    \"key\": \"mayaAscii\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Maya Scene:\",\n                    \"key\": \"mayaScene\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Set Dress:\",\n                    \"key\": \"setdress\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Layout:\",\n                    \"key\": \"layout\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"VDB Cache:\",\n                    \"key\": \"vdbcache\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Vray Proxy:\",\n                    \"key\": \"vrayproxy\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Vray Scene:\",\n                    \"key\": \"vrayscene_layer\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Yeti Cache:\",\n                    \"key\": \"yeticache\"\n                },\n                {\n                    \"type\": \"color\",\n                    \"label\": \"Yeti Rig:\",\n                    \"key\": \"yetiRig\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"reference_loader\",\n            \"label\": \"Reference Loader\",\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"label\": \"Namespace\",\n                    \"key\": \"namespace\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"label\": \"Group name\",\n                    \"key\": \"group_name\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Here's a link to the doc where you can find explanations about customing the naming of referenced assets: https://openpype.io/docs/admin_hosts_maya#load-plugins\"\n                },\n                {\n                    \"type\": \"separator\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"display_handle\",\n                    \"label\": \"Display Handle On Load References\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"import_loader\",\n            \"label\": \"Import Loader\",\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"label\": \"Namespace\",\n                    \"key\": \"namespace\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"label\": \"Group name\",\n                    \"key\": \"group_name\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Here's a link to the doc where you can find explanations about customing the naming of referenced assets: https://openpype.io/docs/admin_hosts_maya#load-plugins\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"publish\",\n    \"label\": \"Publish plugins\",\n    \"children\": [\n        {\n            \"type\": \"label\",\n            \"label\": \"Collectors\"\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CollectMayaRender\",\n            \"label\": \"Collect Render Layers\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"sync_workfile_version\",\n                    \"label\": \"Sync render version with workfile\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CollectFbxAnimation\",\n            \"label\": \"Collect Fbx Animation\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CollectFbxCamera\",\n            \"label\": \"Collect Camera for FBX export\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CollectGLTF\",\n            \"label\": \"Collect Assets for GLTF/GLB export\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                }\n            ]\n        },\n        {\n            \"type\": \"splitter\"\n        },\n        {\n            \"type\": \"label\",\n            \"label\": \"Validators\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_publish_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"ValidateInstanceInContext\",\n                    \"label\": \"Validate Instance In Context\"\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_publish_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"ValidateContainers\",\n                    \"label\": \"ValidateContainers\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ValidateFrameRange\",\n            \"label\": \"Validate Frame Range\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"key\": \"exclude_families\",\n                    \"label\": \"Exclude Families\",\n                    \"type\": \"list\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ValidateShaderName\",\n            \"label\": \"ValidateShaderName\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Shader name regex can use named capture group <b>asset</b> to validate against current asset name.<p><b>Example:</b><br/><code>^.*(?P=&lt;asset&gt;.+)_SHD</code></p>\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"regex\",\n                    \"label\": \"Validation regex\"\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_publish_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"ValidateShadingEngine\",\n                    \"label\": \"Validate Look Shading Engine Naming\"\n                },\n                {\n                    \"key\": \"ValidateMayaColorSpace\",\n                    \"label\": \"ValidateMayaColorSpace\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ValidateAttributes\",\n            \"label\": \"ValidateAttributes\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"raw-json\",\n                    \"key\": \"attributes\",\n                    \"label\": \"Attributes\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ValidateLoadedPlugin\",\n            \"label\": \"Validate Loaded Plugin\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"whitelist_native_plugins\",\n                    \"label\": \"Whitelist Maya Native Plugins\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"authorized_plugins\",\n                    \"label\": \"Authorized plugins\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ValidateMayaUnits\",\n            \"label\": \"Validate Maya Units\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"validate_linear_units\",\n                    \"label\": \"Validate linear units\"\n                },\n                {\n                    \"key\": \"linear_units\",\n                    \"label\": \"Linear units\",\n                    \"type\": \"enum\",\n                    \"multiselection\": false,\n                    \"defaults\": \"cm\",\n                    \"enum_items\": [\n                        {\"mm\": \"millimeter\"},\n                        {\"cm\": \"centimeter\"},\n                        {\"m\": \"meter\"},\n                        {\"km\": \"kilometer\"},\n                        {\"in\": \"inch\"},\n                        {\"ft\": \"foot\"},\n                        {\"yd\": \"yard\"},\n                        {\"mi\": \"mile\"}\n                    ]\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"validate_angular_units\",\n                    \"label\": \"Validate angular units\"\n                },\n                {\n                    \"key\": \"angular_units\",\n                    \"label\": \"Angular units\",\n                    \"type\": \"enum\",\n                    \"multiselection\": false,\n                    \"defaults\": \"cm\",\n                    \"enum_items\": [\n                        {\"deg\": \"degree\"},\n                        {\"rad\": \"radian\"}\n                    ]\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"validate_fps\",\n                    \"label\": \"Validate fps\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ValidateUnrealStaticMeshName\",\n            \"label\": \"Validate Unreal Static Mesh Name\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"validate_mesh\",\n                    \"label\": \"Validate mesh Names \"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"validate_collision\",\n                    \"label\": \"Validate collision names\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"ValidateCycleError\",\n            \"label\": \"Validate Cycle Error\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"key\": \"families\",\n                    \"label\": \"Families\",\n                    \"type\": \"list\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"ValidatePluginPathAttributes\",\n            \"label\": \"Plug-in Path Attributes\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Fill in the node types and attributes you want to validate. <p>e.g. <b>AlembicNode.abc_file</b>, the node type is <b>AlembicNode</b> and the node attribute is <b>abc_file</b>\"\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"collapsible\": true,\n                    \"key\": \"attribute\",\n                    \"label\": \"File Attribute\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"text\"\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ValidateRenderSettings\",\n            \"label\": \"ValidateRenderSettings\",\n            \"children\": [\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"store_as_list\": true,\n                    \"key\": \"arnold_render_attributes\",\n                    \"label\": \"Arnold Render Attributes\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"list\",\n                        \"object_type\": \"text\"\n                    }\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"store_as_list\": true,\n                    \"key\": \"vray_render_attributes\",\n                    \"label\": \"Vray Render Attributes\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"list\",\n                        \"object_type\": \"text\"\n                    }\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"store_as_list\": true,\n                    \"key\": \"redshift_render_attributes\",\n                    \"label\": \"Redshift Render Attributes\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"list\",\n                        \"object_type\": \"text\"\n                    }\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"store_as_list\": true,\n                    \"key\": \"renderman_render_attributes\",\n                    \"label\": \"Renderman Render Attributes\",\n                    \"use_label_wrap\": true,\n                    \"object_type\": {\n                        \"type\": \"list\",\n                        \"object_type\": \"text\"\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_publish_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"ValidateResolution\",\n                    \"label\": \"Validate Resolution Settings\"\n                },\n                {\n                    \"key\": \"ValidateCurrentRenderLayerIsRenderable\",\n                    \"label\": \"Validate Current Render Layer Has Renderable Camera\"\n                },\n                {\n                    \"key\": \"ValidateGLSLMaterial\",\n                    \"label\": \"Validate GLSL Material\"\n                },\n                {\n                    \"key\": \"ValidateGLSLPlugin\",\n                    \"label\": \"Validate GLSL Plugin\"\n                },\n                {\n                    \"key\": \"ValidateRenderImageRule\",\n                    \"label\": \"Validate Images File Rule (Workspace)\"\n                },\n                {\n                    \"key\": \"ValidateRenderNoDefaultCameras\",\n                    \"label\": \"Validate No Default Cameras Renderable\"\n                },\n                {\n                    \"key\": \"ValidateRenderSingleCamera\",\n                    \"label\": \"Validate Render Single Camera\"\n                },\n                {\n                    \"key\": \"ValidateRenderLayerAOVs\",\n                    \"label\": \"Validate Render Passes / AOVs Are Registered\"\n                },\n                {\n                    \"key\": \"ValidateStepSize\",\n                    \"label\": \"Validate Step Size\"\n                },\n                {\n                    \"key\": \"ValidateVRayDistributedRendering\",\n                    \"label\": \"VRay Distributed Rendering\"\n                },\n                {\n                    \"key\": \"ValidateVrayReferencedAOVs\",\n                    \"label\": \"VRay Referenced AOVs\"\n                },\n                {\n                    \"key\": \"ValidateVRayTranslatorEnabled\",\n                    \"label\": \"VRay Translator Settings\"\n                },\n                {\n                    \"key\": \"ValidateVrayProxy\",\n                    \"label\": \"VRay Proxy Settings\"\n                },\n                {\n                    \"key\": \"ValidateVrayProxyMembers\",\n                    \"label\": \"VRay Proxy Members\"\n                },\n                {\n                    \"key\": \"ValidateYetiRenderScriptCallbacks\",\n                    \"label\": \"Yeti Render Script Callbacks\"\n                },\n                {\n                    \"key\": \"ValidateYetiRigCacheState\",\n                    \"label\": \"Yeti Rig Cache State\"\n                },\n                {\n                    \"key\": \"ValidateYetiRigInputShapesInInstance\",\n                    \"label\": \"Yeti Rig Input Shapes In Instance\"\n                },\n                {\n                    \"key\": \"ValidateYetiRigSettings\",\n                    \"label\": \"Yeti Rig Settings\"\n                }\n            ]\n        },\n        {\n            \"type\": \"collapsible-wrap\",\n            \"label\": \"Model\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ValidateModelName\",\n                    \"label\": \"Validate Model Name\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"database\",\n                            \"label\": \"Use database shader name definitions\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Path to material file defining list of material names to check. This is material name per line simple text file.<br/>It will be checked against named group <b>shader</b> in your <em>Validation regex</em>.<p>For example: <br/> <code>^.*(?P=&lt;shader&gt;.+)_GEO</code></p>This is used instead of database definitions if they are disabled.\"\n                        },\n                        {\n                            \"type\": \"path\",\n                            \"key\": \"material_file\",\n                            \"label\": \"Material File\",\n                            \"multiplatform\": true,\n                            \"multipath\": false\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"regex\",\n                            \"label\": \"Validation regex\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Regex for validating name of top level group name.<br/>You can use named capturing groups:<br/><code>(?P&lt;asset&gt;.*)</code> for Asset name<br/><code>(?P&lt;subset&gt;.*)</code> for Subset<br/><code>(?P&lt;project&gt;.*)</code> for project<br/><p>For example to check for asset in name so <code>*_some_asset_name_GRP</code> is valid, use:<br/><code>.*?_(?P&lt;asset&gt;.*)_GEO</code>\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"top_level_regex\",\n                            \"label\": \"Top level group name regex\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ValidateModelContent\",\n                    \"label\": \"Validate Model Content\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"validate_top_group\",\n                            \"label\": \"Validate one top group\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ValidateTransformNamingSuffix\",\n                    \"label\": \"ValidateTransformNamingSuffix\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"label\",\n                            \"label\": \"Validates transform suffix based on the type of its children shapes.\"\n                        },\n                        {\n                            \"type\": \"raw-json\",\n                            \"key\": \"SUFFIX_NAMING_TABLE\",\n                            \"label\": \"Suffix Naming Table\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"ALLOW_IF_NOT_IN_SUFFIX_TABLE\",\n                            \"label\": \"Allow if suffix not in table\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"key\": \"ValidateColorSets\",\n                            \"label\": \"ValidateColorSets\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshHasOverlappingUVs\",\n                            \"label\": \"ValidateMeshHasOverlappingUVs\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshArnoldAttributes\",\n                            \"label\": \"ValidateMeshArnoldAttributes\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshShaderConnections\",\n                            \"label\": \"ValidateMeshShaderConnections\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshSingleUVSet\",\n                            \"label\": \"ValidateMeshSingleUVSet\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshHasUVs\",\n                            \"label\": \"ValidateMeshHasUVs\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshLaminaFaces\",\n                            \"label\": \"ValidateMeshLaminaFaces\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshNgons\",\n                            \"label\": \"ValidateMeshNgons\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshNonManifold\",\n                            \"label\": \"ValidateMeshNonManifold\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshNoNegativeScale\",\n                            \"label\": \"Validate Mesh No Negative Scale\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshNonZeroEdgeLength\",\n                            \"label\": \"Validate Mesh Edge Length Non Zero\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshNormalsUnlocked\",\n                            \"label\": \"ValidateMeshNormalsUnlocked\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshUVSetMap1\",\n                            \"label\": \"ValidateMeshUVSetMap1\",\n                            \"docstring\": \"Validate model's default uv set exists and is named 'map1'.<br><br>In Maya meshes by default have a uv set named 'map1' that cannot be deleted. It can be renamed, however,<br> introducing some issues with some renderers. As such we ensure the first (default) UV set index is named 'map1'.\"\n                        },\n                        {\n                            \"key\": \"ValidateMeshVerticesHaveEdges\",\n                            \"label\": \"ValidateMeshVerticesHaveEdges\"\n                        },\n                        {\n                            \"key\": \"ValidateNoAnimation\",\n                            \"label\": \"ValidateNoAnimation\",\n                            \"docstring\": \"Ensure no keyframes on nodes in the Instance.<br>Even though a Model would extract without animCurves correctly this avoids getting different <br> output from a model when extracted from a different frame than the first frame. (Might be overly restrictive though).\"\n                        },\n                        {\n                            \"key\": \"ValidateNoNamespace\",\n                            \"label\": \"ValidateNoNamespace\"\n                        },\n                        {\n                            \"key\": \"ValidateNoNullTransforms\",\n                            \"label\": \"ValidateNoNullTransforms\"\n                        },\n                        {\n                            \"key\": \"ValidateNoUnknownNodes\",\n                            \"label\": \"ValidateNoUnknownNodes\"\n                        },\n                        {\n                            \"key\": \"ValidateNodeNoGhosting\",\n                            \"label\": \"ValidateNodeNoGhosting\"\n                        },\n                        {\n                            \"key\": \"ValidateShapeDefaultNames\",\n                            \"label\": \"ValidateShapeDefaultNames\"\n                        },\n                        {\n                            \"key\": \"ValidateShapeRenderStats\",\n                            \"label\": \"ValidateShapeRenderStats\"\n                        },\n                        {\n                            \"key\": \"ValidateShapeZero\",\n                            \"label\": \"ValidateShapeZero\"\n                        },\n                        {\n                            \"key\": \"ValidateTransformZero\",\n                            \"label\": \"ValidateTransformZero\"\n                        },\n                        {\n                            \"key\": \"ValidateUniqueNames\",\n                            \"label\": \"ValidateUniqueNames\"\n                        },\n                        {\n                            \"key\": \"ValidateNoVRayMesh\",\n                            \"label\": \"Validate No V-Ray Proxies (VRayMesh)\"\n                        },\n                        {\n                            \"key\": \"ValidateUnrealMeshTriangulated\",\n                            \"label\": \"Validate if Mesh is Triangulated\"\n                        },\n                        {\n                            \"key\": \"ValidateAlembicVisibleOnly\",\n                            \"label\": \"Validate Alembic visible node\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Extractors\"\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ExtractProxyAlembic\",\n                    \"label\": \"Extract Proxy Alembic\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"key\": \"families\",\n                            \"label\": \"Families\",\n                            \"type\": \"list\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"key\": \"ExtractObj\",\n                    \"label\": \"Extract OBJ\",\n                    \"checkbox_key\": \"enabled\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"collapsible-wrap\",\n            \"label\": \"Rig\",\n            \"children\": [\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_publish_plugin\",\n                    \"template_data\": [\n                        {\n                            \"key\": \"ValidateRigContents\",\n                            \"label\": \"Validate Rig Contents\"\n                        },\n                        {\n                            \"key\": \"ValidateRigJointsHidden\",\n                            \"label\": \"Validate Rig Joints Hidden\"\n                        },\n                        {\n                            \"key\": \"ValidateRigControllers\",\n                            \"label\": \"Validate Rig Controllers\"\n                        },\n                        {\n                            \"key\": \"ValidateAnimatedReferenceRig\",\n                            \"label\": \"Validate Animated Reference Rig\"\n                        },\n                        {\n                            \"key\": \"ValidateAnimationContent\",\n                            \"label\": \"Validate Animation Content\"\n                        },\n                        {\n                            \"key\": \"ValidateOutRelatedNodeIds\",\n                            \"label\": \"Validate Animation Out Set Related Node Ids\"\n                        },\n                        {\n                            \"key\": \"ValidateRigControllersArnoldAttributes\",\n                            \"label\": \"Validate Rig Controllers (Arnold Attributes)\"\n                        },\n                        {\n                            \"key\": \"ValidateSkeletalMeshHierarchy\",\n                            \"label\": \"Validate Skeletal Mesh Top Node\"\n                        },\n                        {\n                            \"key\": \"ValidateSkeletonRigContents\",\n                            \"label\": \"Validate Skeleton Rig Contents\"\n                        },\n                        {\n                            \"key\": \"ValidateSkeletonRigControllers\",\n                            \"label\": \"Validate Skeleton Rig Controllers\"\n                        },\n                        {\n                            \"key\": \"ValidateSkinclusterDeformerSet\",\n                            \"label\": \"Validate Skincluster Deformer Relationships\"\n                        },\n                        {\n                            \"key\": \"ValidateSkeletonRigOutputIds\",\n                            \"label\": \"Validate Skeleton Rig Output Ids\"\n                        },\n                        {\n                            \"key\": \"ValidateSkeletonTopGroupHierarchy\",\n                            \"label\": \"Validate Skeleton Top Group Hierarchy\"\n                        }\n                    ]\n                },\n\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"ValidateRigOutSetNodeIds\",\n                    \"label\": \"Validate Rig Out Set Node Ids\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"allow_history_only\",\n                            \"label\": \"Allow history only\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"checkbox_key\": \"enabled\",\n                    \"key\": \"ValidateSkeletonRigOutSetNodeIds\",\n                    \"label\": \"Validate Skeleton Rig Out Set Node Ids\",\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"enabled\",\n                            \"label\": \"Enabled\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"optional\",\n                            \"label\": \"Optional\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"allow_history_only\",\n                            \"label\": \"Allow history only\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_publish_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"ValidateCameraAttributes\",\n                    \"label\": \"Validate Camera Attributes\",\n                    \"docstring\": \"\"\n                },\n                {\n                    \"key\": \"ValidateAssemblyName\",\n                    \"label\": \"Validate Assembly Name\"\n                },\n                {\n                    \"key\": \"ValidateAssemblyNamespaces\",\n                    \"label\": \"Validate Assembly Namespaces\"\n                },\n                {\n                    \"key\": \"ValidateAssemblyModelTransforms\",\n                    \"label\": \"Validate Assembly Model Transforms\"\n                },\n                {\n                    \"key\": \"ValidateAssRelativePaths\",\n                    \"label\": \"ValidateAssRelativePaths\"\n                },\n                {\n                    \"key\": \"ValidateInstancerContent\",\n                    \"label\": \"Validate Instancer Content\"\n                },\n                {\n                    \"key\": \"ValidateInstancerFrameRanges\",\n                    \"label\": \"Validate Instancer Cache Frame Ranges\"\n                },\n                {\n                    \"key\": \"ValidateNoDefaultCameras\",\n                    \"label\": \"Validate No Default Cameras\"\n                },\n                {\n                    \"key\": \"ValidateUnrealUpAxis\",\n                    \"label\": \"Validate Unreal Up-Axis check\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ValidateCameraContents\",\n            \"label\": \"Validate Camera Content\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"validate_shapes\",\n                    \"label\": \"Validate presence of shapes\"\n                }\n            ]\n        },\n        {\n            \"type\": \"splitter\"\n        },\n        {\n            \"type\": \"label\",\n            \"label\": \"Extractors\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_maya_capture\"\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractMayaSceneRaw\",\n            \"label\": \"Maya Scene (Raw)\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Add loaded instances to those published families:\"\n                },\n                {\n                    \"key\": \"add_for_families\",\n                    \"label\": \"Families\",\n                    \"type\": \"list\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractAlembic\",\n            \"label\": \"Extract Pointcache/Animation\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"key\": \"families\",\n                    \"label\": \"Families\",\n                    \"type\": \"list\",\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Export Defaults\"\n                },\n                {\n                    \"type\": \"enum\",\n                    \"key\": \"flags\",\n                    \"multiselection\": true,\n                    \"label\": \"Export Flags\",\n                    \"enum_items\": [\n                        {\"autoSubd\": \"autoSubd\"},\n                        {\"dontSkipUnwrittenFrames\": \"dontSkipUnwrittenFrames\"},\n                        {\"eulerFilter\": \"eulerFilter\"},\n                        {\"noNormals\": \"noNormals\"},\n                        {\"preRoll\": \"preRoll\"},\n                        {\"renderableOnly\": \"renderableOnly\"},\n                        {\"stripNamespaces\": \"stripNamespaces\"},\n                        {\"uvWrite\": \"uvWrite\"},\n                        {\"uvsOnly\": \"uvsOnly\"},\n                        {\"verbose\": \"verbose\"},\n                        {\"wholeFrameGeo\": \"wholeFrameGeo\"},\n                        {\"worldSpace\": \"worldSpace\"},\n                        {\"writeColorSets\": \"writeColorSets\"},\n                        {\"writeFaceSets\": \"writeFaceSets\"},\n                        {\"writeNormals\": \"writeNormals\"},\n                        {\"writeUVSets\": \"writeUVSets\"},\n                        {\"writeVisibility\": \"writeVisibility\"}\n                    ]\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"attr\",\n                    \"label\": \"Custom Attributes\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"attrPrefix\",\n                    \"label\": \"Custom Attributes Prefix\"\n                },\n                {\n                    \"type\": \"enum\",\n                    \"key\": \"dataFormat\",\n                    \"label\": \"Data Format\",\n                    \"enum_items\": [\n                        {\n                            \"ogawa\": \"ogawa\"\n                        },\n                        {\n                            \"HDF\": \"HDF\"\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"melPerFrameCallback\",\n                    \"label\": \"melPerFrameCallback\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"melPostJobCallback\",\n                    \"label\": \"melPostJobCallback\"\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"preRollStartFrame\",\n                    \"label\": \"Pre Roll Start Frame\",\n                    \"minimum\": 0\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"pythonPerFrameCallback\",\n                    \"label\": \"pythonPerFrameCallback\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"pythonPostJobCallback\",\n                    \"label\": \"pythonPostJobCallback\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"userAttr\",\n                    \"label\": \"userAttr\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"userAttrPrefix\",\n                    \"label\": \"userAttrPrefix\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"visibleOnly\",\n                    \"label\": \"Visible Only\"\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"These attributes are exposed to the user when publishing with default values from above.\"\n                },\n                {\n                    \"type\": \"enum\",\n                    \"key\": \"overrides\",\n                    \"multiselection\": true,\n                    \"label\": \"Exposed Overrides\",\n                    \"enum_items\": [\n                        {\"attr\": \"Custom Attributes\"},\n                        {\"attrPrefix\": \"Custom Attributes Prefix\"},\n                        {\"autoSubd\": \"autoSubd\"},\n                        {\"dataFormat\": \"dataFormat\"},\n                        {\"dontSkipUnwrittenFrames\": \"dontSkipUnwrittenFrames\"},\n                        {\"eulerFilter\": \"eulerFilter\"},\n                        {\"melPerFrameCallback\": \"melPerFrameCallback\"},\n                        {\"melPostJobCallback\": \"melPostJobCallback\"},\n                        {\"noNormals\": \"noNormals\"},\n                        {\"preRoll\": \"preRoll\"},\n                        {\"preRollStartFrame\": \"preRollStartFrame\"},\n                        {\"pythonPerFrameCallback\": \"pythonPerFrameCallback\"},\n                        {\"pythonPostJobCallback\": \"pythonPostJobCallback\"},\n                        {\"renderableOnly\": \"renderableOnly\"},\n                        {\"stripNamespaces\": \"stripNamespaces\"},\n                        {\"userAttr\": \"userAttr\"},\n                        {\"userAttrPrefix\": \"userAttrPrefix\"},\n                        {\"uvWrite\": \"uvWrite\"},\n                        {\"uvsOnly\": \"uvsOnly\"},\n                        {\"verbose\": \"verbose\"},\n                        {\"visibleOnly\": \"visibleOnly\"},\n                        {\"wholeFrameGeo\": \"wholeFrameGeo\"},\n                        {\"worldSpace\": \"worldSpace\"},\n                        {\"writeColorSets\": \"writeColorSets\"},\n                        {\"writeCreases\": \"writeCreases\"},\n                        {\"writeFaceSets\": \"writeFaceSets\"},\n                        {\"writeNormals\": \"writeNormals\"},\n                        {\"writeUVSets\": \"writeUVSets\"},\n                        {\"writeVisibility\": \"writeVisibility\"}\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractCameraAlembic\",\n            \"label\": \"Extract camera to Alembic\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"List of attributes that will be added to the baked alembic camera. Needs to be written in python list syntax.<p>For example: <br/> <code>[\\\"attributeName\\\", \\\"anotherAttribute\\\"]</code></p>\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                },\n                {\n                    \"type\": \"raw-json\",\n                    \"key\": \"bake_attributes\",\n                    \"label\": \"Bake Attributes\",\n                    \"is_list\": true\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractCameraMayaScene\",\n            \"label\": \"Extract camera to Maya scene\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"keep_image_planes\",\n                    \"label\": \"Export Image planes\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractGLB\",\n            \"label\": \"Extract GLB\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"ogsfx_path\",\n                    \"label\": \"GLSL Shader Directory\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractLook\",\n            \"label\": \"Extract Look\",\n            \"children\": [\n                {\n                    \"type\": \"list\",\n                    \"key\": \"maketx_arguments\",\n                    \"label\": \"Extra arguments for maketx command line\",\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"argument\",\n                                \"label\": \"Argument\",\n                                \"type\": \"text\"\n                            },\n                            {\n                                \"key\": \"parameters\",\n                                \"label\": \"Parameters\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractGPUCache\",\n            \"label\": \"Extract GPU Cache\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"key\": \"families\",\n                    \"label\": \"Families\",\n                    \"type\": \"list\",\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"key\": \"step\",\n                    \"label\": \"Step\",\n                    \"type\": \"number\",\n                    \"decimal\": 4,\n                    \"minimum\": 1\n                },\n                {\n                    \"key\": \"stepSave\",\n                    \"label\": \"Step Save\",\n                    \"type\": \"number\",\n                    \"minimum\": 1\n                },\n                {\n                    \"key\": \"optimize\",\n                    \"label\": \"Optimize Hierarchy\",\n                    \"type\": \"boolean\"\n                },\n                {\n                    \"key\": \"optimizationThreshold\",\n                    \"label\": \"Optimization Threshold\",\n                    \"type\": \"number\",\n                    \"minimum\": 1\n                },\n                {\n                    \"key\": \"optimizeAnimationsForMotionBlur\",\n                    \"label\": \"Optimize Animations For Motion Blur\",\n                    \"type\": \"boolean\"\n                },\n                {\n                    \"key\": \"writeMaterials\",\n                    \"label\": \"Write Materials\",\n                    \"type\": \"boolean\"\n                },\n                {\n                    \"key\": \"useBaseTessellation\",\n                    \"label\": \"User Base Tesselation\",\n                    \"type\": \"boolean\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"RenderSettings\",\n    \"label\": \"Render Settings\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"apply_render_settings\",\n            \"label\": \"Apply Render Settings on creation\"\n        },\n        {\n            \"type\": \"text\",\n            \"key\": \"default_render_image_folder\",\n            \"label\": \"Default render image folder. This setting can be\\noverwritten by custom staging directory profile;\\n\\\"project_settings/global/tools/publish\\n/custom_staging_dir_profiles\\\".\"\n        },\n        {\n          \"type\": \"boolean\",\n          \"key\": \"enable_all_lights\",\n          \"label\": \"Include all lights in Render Setup Layers by default\"\n        },\n        {\n            \"key\": \"aov_separator\",\n            \"label\": \"AOV Separator character\",\n            \"type\": \"enum\",\n            \"multiselection\": false,\n            \"default\": \"underscore\",\n            \"enum_items\": [\n                {\"dash\":  \"- (dash)\"},\n                {\"underscore\":  \"_ (underscore)\"},\n                {\"dot\": \". (dot)\"}\n            ]\n        },\n        {\n          \"key\": \"remove_aovs\",\n          \"label\": \"Remove existing AOVs\",\n          \"type\": \"boolean\"\n        },\n        {\n          \"key\": \"reset_current_frame\",\n          \"label\": \"Reset Current Frame\",\n          \"type\": \"boolean\"\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"arnold_renderer\",\n            \"label\": \"Arnold Renderer\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                  \"key\": \"image_prefix\",\n                  \"label\": \"Image prefix template\",\n                  \"type\": \"text\"\n                },\n                {\n                  \"key\": \"image_format\",\n                  \"label\": \"Output Image Format\",\n                  \"type\": \"enum\",\n                  \"multiselection\": false,\n                  \"defaults\": \"exr\",\n                  \"enum_items\": [\n                    {\"jpeg\": \"jpeg\"},\n                    {\"png\": \"png\"},\n                    {\"deepexr\":  \"deep exr\"},\n                    {\"tif\":  \"tif\"},\n                    {\"exr\": \"exr\"},\n                    {\"maya\": \"maya\"},\n                    {\"mtoa_shaders\":  \"mtoa_shaders\"}\n                  ]\n                },\n                {\n                  \"key\": \"multilayer_exr\",\n                  \"label\": \"Multilayer (exr)\",\n                  \"type\": \"boolean\"\n                },\n                {\n                  \"key\": \"tiled\",\n                  \"label\": \"Tiled (tif, exr)\",\n                  \"type\": \"boolean\"\n                },\n                {\n                  \"key\": \"aov_list\",\n                  \"label\": \"AOVs to create\",\n                  \"type\": \"enum\",\n                  \"multiselection\": true,\n                  \"defaults\": \"empty\",\n                  \"enum_items\": [\n                    {\"empty\": \"< empty >\"},\n                    {\"ID\": \"ID\"},\n                    {\"N\": \"N\"},\n                    {\"P\": \"P\"},\n                    {\"Pref\": \"Pref\"},\n                    {\"RGBA\": \"RGBA\"},\n                    {\"Z\": \"Z\"},\n                    {\"albedo\": \"albedo\"},\n                    {\"background\": \"background\"},\n                    {\"coat\": \"coat\"},\n                    {\"coat_albedo\": \"coat_albedo\"},\n                    {\"coat_direct\": \"coat_direct\"},\n                    {\"coat_indirect\": \"coat_indirect\"},\n                    {\"cputime\": \"cputime\"},\n                    {\"crypto_asset\": \"crypto_asset\"},\n                    {\"crypto_material\": \"cypto_material\"},\n                    {\"crypto_object\": \"crypto_object\"},\n                    {\"diffuse\": \"diffuse\"},\n                    {\"diffuse_albedo\": \"diffuse_albedo\"},\n                    {\"diffuse_direct\": \"diffuse_direct\"},\n                    {\"diffuse_indirect\": \"diffuse_indirect\"},\n                    {\"direct\": \"direct\"},\n                    {\"emission\": \"emission\"},\n                    {\"highlight\": \"highlight\"},\n                    {\"indirect\": \"indirect\"},\n                    {\"motionvector\": \"motionvector\"},\n                    {\"opacity\": \"opacity\"},\n                    {\"raycount\": \"raycount\"},\n                    {\"rim_light\": \"rim_light\"},\n                    {\"shadow\": \"shadow\"},\n                    {\"shadow_diff\": \"shadow_diff\"},\n                    {\"shadow_mask\": \"shadow_mask\"},\n                    {\"shadow_matte\": \"shadow_matte\"},\n                    {\"sheen\": \"sheen\"},\n                    {\"sheen_albedo\": \"sheen_albedo\"},\n                    {\"sheen_direct\": \"sheen_direct\"},\n                    {\"sheen_indirect\": \"sheen_indirect\"},\n                    {\"specular\": \"specular\"},\n                    {\"specular_albedo\": \"specular_albedo\"},\n                    {\"specular_direct\": \"specular_direct\"},\n                    {\"specular_indirect\": \"specular_indirect\"},\n                    {\"sss\": \"sss\"},\n                    {\"sss_albedo\": \"sss_albedo\"},\n                    {\"sss_direct\": \"sss_direct\"},\n                    {\"sss_indirect\": \"sss_indirect\"},\n                    {\"transmission\": \"transmission\"},\n                    {\"transmission_albedo\": \"transmission_albedo\"},\n                    {\"transmission_direct\": \"transmission_direct\"},\n                    {\"transmission_indirect\": \"transmission_indirect\"},\n                    {\"volume\": \"volume\"},\n                    {\"volume_Z\": \"volume_Z\"},\n                    {\"volume_albedo\": \"volume_albedo\"},\n                    {\"volume_direct\": \"volume_direct\"},\n                    {\"volume_indirect\": \"volume_indirect\"},\n                    {\"volume_opacity\": \"volume_opacity\"}\n                  ]\n                },\n                {\n                  \"type\": \"label\",\n                  \"label\": \"Add additional options - put attribute and value, like <code>defaultArnoldRenderOptions.AASamples</code> = <code>4</code>\"\n                },\n                {\n                  \"type\": \"dict-modifiable\",\n                  \"store_as_list\": true,\n                  \"key\": \"additional_options\",\n                  \"label\": \"Additional Renderer Options\",\n                  \"use_label_wrap\": true,\n                  \"object_type\": {\n                        \"type\": \"text\"\n                  }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"vray_renderer\",\n            \"label\": \"V-Ray Renderer\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                  \"key\": \"image_prefix\",\n                  \"label\": \"Image prefix template\",\n                  \"type\": \"text\"\n                },\n                {\n                  \"key\": \"engine\",\n                  \"label\": \"Production Engine\",\n                  \"type\": \"enum\",\n                  \"multiselection\": false,\n                  \"defaults\": \"1\",\n                  \"enum_items\": [\n                    {\"1\":  \"V-Ray\"},\n                    {\"2\":  \"V-Ray GPU\"}\n                  ]\n                },\n                {\n                  \"key\": \"image_format\",\n                  \"label\": \"Output Image Format\",\n                  \"type\": \"enum\",\n                  \"multiselection\": false,\n                  \"defaults\": \"exr\",\n                  \"enum_items\": [\n                    {\"png\": \"png\"},\n                    {\"jpg\": \"jpg\"},\n                    {\"vrimg\":  \"vrimg\"},\n                    {\"hdr\":  \"hdr\"},\n                    {\"exr\": \"exr\"},\n                    {\"exr (multichannel)\": \"exr (multichannel)\"},\n                    {\"exr (deep)\":  \"exr (deep)\"},\n                    {\"tga\":  \"tga\"},\n                    {\"bmp\":  \"bmp\"},\n                    {\"sgi\":  \"sgi\"}\n                  ]\n                },\n                {\n                  \"key\": \"aov_list\",\n                  \"label\": \"AOVs to create\",\n                  \"type\": \"enum\",\n                  \"multiselection\": true,\n                  \"defaults\": \"empty\",\n                  \"enum_items\": [\n                    {\"empty\": \"< empty >\"},\n                    {\"atmosphereChannel\": \"atmosphereChannel\"},\n                    {\"backgroundChannel\": \"backgroundChannel\"},\n                    {\"bumpNormalsChannel\": \"bumpNormalsChannel\"},\n                    {\"causticsChannel\": \"causticsChannel\"},\n                    {\"coatFilterChannel\": \"coatFilterChannel\"},\n                    {\"coatGlossinessChannel\": \"coatGlossinessChannel\"},\n                    {\"coatReflectionChannel\": \"coatReflectionChannel\"},\n                    {\"vrayCoatChannel\": \"vrayCoatChannel\"},\n                    {\"CoverageChannel\": \"CoverageChannel\"},\n                    {\"cryptomatteChannel\": \"cryptomatteChannel\"},\n                    {\"customColor\": \"customColor\"},\n                    {\"drBucketChannel\": \"drBucketChannel\"},\n                    {\"denoiserChannel\": \"denoiserChannel\"},\n                    {\"diffuseChannel\": \"diffuseChannel\"},\n                    {\"ExtraTexElement\": \"ExtraTexElement\"},\n                    {\"giChannel\": \"giChannel\"},\n                    {\"LightMixElement\": \"LightMixElement\"},\n                    {\"LightSelectElement\": \"LightSelectElement\"},\n                    {\"lightingChannel\": \"lightingChannel\"},\n                    {\"LightingAnalysisChannel\": \"LightingAnalysisChannel\"},\n                    {\"materialIDChannel\": \"materialIDChannel\"},\n                    {\"MaterialSelectElement\": \"MaterialSelectElement\"},\n                    {\"matteShadowChannel\": \"matteShadowChannel\"},\n                    {\"metalnessChannel\": \"metalnessChannel\"},\n                    {\"MultiMatteElement\": \"MultiMatteElement\"},\n                    {\"multimatteIDChannel\": \"multimatteIDChannel\"},\n                    {\"noiseLevelChannel\": \"noiseLevelChannel\"},\n                    {\"normalsChannel\": \"normalsChannel\"},\n                    {\"nodeIDChannel\": \"nodeIDChannel\"},\n                    {\"objectSelectChannel\": \"objectSelectChannel\"},\n                    {\"rawCoatFilterChannel\": \"rawCoatFilterChannel\"},\n                    {\"rawCoatReflectionChannel\": \"rawCoatReflectionChannel\"},\n                    {\"rawDiffuseFilterChannel\": \"rawDiffuseFilterChannel\"},\n                    {\"rawGiChannel\": \"rawGiChannel\"},\n                    {\"rawLightChannel\": \"rawLightChannel\"},\n                    {\"rawReflectionChannel\": \"rawReflectionChannel\"},\n                    {\"rawReflectionFilterChannel\": \"rawReflectionFilterChannel\"},\n                    {\"rawRefractionChannel\": \"rawRefractionChannel\"},\n                    {\"rawRefractionFilterChannel\": \"rawRefractionFilterChannel\"},\n                    {\"rawShadowChannel\": \"rawShadowChannel\"},\n                    {\"rawSheenFilterChannel\": \"rawSheenFilterChannel\"},\n                    {\"rawSheenReflectionChannel\": \"rawSheenReflectionChannel\"},\n                    {\"rawTotalLightChannel\": \"rawTotalLightChannel\"},\n                    {\"reflectIORChannel\": \"reflectIORChannel\"},\n                    {\"reflectChannel\": \"reflectChannel\"},\n                    {\"reflectionFilterChannel\": \"reflectionFilterChannel\"},\n                    {\"reflectGlossinessChannel\": \"reflectGlossinessChannel\"},\n                    {\"refractChannel\": \"refractChannel\"},\n                    {\"refractionFilterChannel\": \"refractionFilterChannel\"},\n                    {\"refractGlossinessChannel\": \"refractGlossinessChannel\"},\n                    {\"renderIDChannel\": \"renderIDChannel\"},\n                    {\"FastSSS2Channel\": \"FastSSS2Channel\"},\n                    {\"sampleRateChannel\": \"sampleRateChannel\"},\n                    {\"samplerInfo\": \"samplerInfo\"},\n                    {\"selfIllumChannel\": \"selfIllumChannel\"},\n                    {\"shadowChannel\": \"shadowChannel\"},\n                    {\"sheenFilterChannel\": \"sheenFilterChannel\"},\n                    {\"sheenGlossinessChannel\": \"sheenGlossinessChannel\"},\n                    {\"sheenReflectionChannel\": \"sheenReflectionChannel\"},\n                    {\"vraySheenChannel\": \"vraySheenChannel\"},\n                    {\"specularChannel\": \"specularChannel\"},\n                    {\"Toon\": \"Toon\"},\n                    {\"toonLightingChannel\": \"toonLightingChannel\"},\n                    {\"toonSpecularChannel\": \"toonSpecularChannel\"},\n                    {\"totalLightChannel\": \"totalLightChannel\"},\n                    {\"unclampedColorChannel\": \"unclampedColorChannel\"},\n                    {\"VRScansPaintMaskChannel\": \"VRScansPaintMaskChannel\"},\n                    {\"VRScansZoneMaskChannel\": \"VRScansZoneMaskChannel\"},\n                    {\"velocityChannel\": \"velocityChannel\"},\n                    {\"zdepthChannel\": \"zdepthChannel\"}\n                  ]\n                },\n                {\n                  \"type\": \"label\",\n                  \"label\": \"Add additional options - put attribute and value, like <code>vraySettings.aaFilterSize</code> = <code>1.5</code>\"\n                },\n                {\n                  \"type\": \"dict-modifiable\",\n                  \"store_as_list\": true,\n                  \"key\": \"additional_options\",\n                  \"label\": \"Additional Renderer Options\",\n                  \"use_label_wrap\": true,\n                  \"object_type\": {\n                        \"type\": \"text\"\n                  }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"redshift_renderer\",\n            \"label\": \"Redshift Renderer\",\n            \"is_group\": true,\n            \"children\": [\n              {\n                  \"key\": \"image_prefix\",\n                  \"label\": \"Image prefix template\",\n                  \"type\": \"text\"\n              },\n              {\n                  \"key\": \"primary_gi_engine\",\n                  \"label\": \"Primary GI Engine\",\n                  \"type\": \"enum\",\n                  \"multiselection\": false,\n                  \"defaults\": \"0\",\n                  \"enum_items\": [\n                    {\"0\":  \"None\"},\n                    {\"3\":  \"Irradiance Cache\"},\n                    {\"4\":  \"Brute Force\"}\n                  ]\n              },\n              {\n                  \"key\": \"secondary_gi_engine\",\n                  \"label\": \"Secondary GI Engine\",\n                  \"type\": \"enum\",\n                  \"multiselection\": false,\n                  \"defaults\": \"0\",\n                  \"enum_items\": [\n                    {\"0\":  \"None\"},\n                    {\"2\": \"Irradiance Point Cloud\"},\n                    {\"4\":  \"Brute Force\"}\n                  ]\n              },\n              {\n                  \"key\": \"image_format\",\n                  \"label\": \"Output Image Format\",\n                  \"type\": \"enum\",\n                  \"multiselection\": false,\n                  \"defaults\": \"exr\",\n                  \"enum_items\": [\n                    {\"iff\": \"Maya IFF\"},\n                    {\"exr\": \"OpenEXR\"},\n                    {\"tif\":  \"TIFF\"},\n                    {\"png\":  \"PNG\"},\n                    {\"tga\": \"Targa\"},\n                    {\"jpg\":  \"JPEG\"}\n                  ]\n              },\n              {\n                  \"key\": \"multilayer_exr\",\n                  \"label\": \"Multilayer (exr)\",\n                  \"type\": \"boolean\"\n              },\n              {\n                  \"key\": \"force_combine\",\n                  \"label\": \"Force combine beauty and AOVs\",\n                  \"type\": \"boolean\"\n              },\n              {\n                  \"key\": \"aov_list\",\n                  \"label\": \"AOVs to create\",\n                  \"type\": \"enum\",\n                  \"multiselection\": true,\n                  \"defaults\": \"empty\",\n                  \"enum_items\": [\n                    {\"empty\": \"< none >\"},\n                    {\"Ambient Occlusion\": \"Ambient Occlusion\"},\n                    {\"Background\": \"Background\"},\n                    {\"Beauty\": \"Beauty\"},\n                    {\"Bump Normals\": \"Bump Normals\"},\n                    {\"Caustics\": \"Caustics\"},\n                    {\"Caustics Raw\": \"Caustics Raw\"},\n                    {\"Cryptomatte\": \"Cryptomatte\"},\n                    {\"Custom\": \"Custom\"},\n                    {\"Depth\": \"Depth\"},\n                    {\"Diffuse Filter\": \"Diffuse Filter\"},\n                    {\"Diffuse Lighting\": \"Diffuse Lighting\"},\n                    {\"Diffuse Lighting Raw\": \"Diffuse Lighting Raw\"},\n                    {\"Emission\": \"Emission\"},\n                    {\"Global Illumination\": \"Global Illumination\"},\n                    {\"Global Illumination Raw\": \"Global Illumination Raw\"},\n                    {\"Matte\": \"Matte\"},\n                    {\"Motion Vectors\": \"Motion Vectors\"},\n                    {\"Normals\": \"Normals\"},\n                    {\"ObjectID\": \"ObjectID\"},\n                    {\"Object-Space Bump Normals\": \"Object-Space Bump Normals\"},\n                    {\"Object-Space Positions\": \"Object-Space Positions\"},\n                    {\"Puzzle Matte\":  \"Puzzle Matte\"},\n                    {\"Reflections\": \"Reflections\"},\n                    {\"Reflections Filter\": \"Reflections Filter\"},\n                    {\"Reflections Raw\": \"Reflections Raw\"},\n                    {\"Refractions\": \"Refractions\"},\n                    {\"Refractions Filter\": \"Refractions Filter\"},\n                    {\"Refractions Raw\": \"Refractions Filter\"},\n                    {\"Shadows\": \"Shadows\"},\n                    {\"SpecularLighting\": \"Specular Lighting\"},\n                    {\"Sub Surface Scatter\": \"Sub Surface Scatter\"},\n                    {\"Sub Surface Scatter Raw\": \"Sub Surface Scatter Raw\"},\n                    {\"Total Diffuse Lighting Raw\": \"Total Diffuse Lighting Raw\"},\n                    {\"Total Translucency Filter\": \"Total Translucency Filter\"},\n                    {\"Translucency Filter\": \"Translucency Filter\"},\n                    {\"Translucency Lighting Raw\": \"Translucency Lighting Raw\"},\n                    {\"Volume Fog Emission\": \"Volume Fog Emission\"},\n                    {\"Volume Fog Tint\": \"Volume Fog Tint\"},\n                    {\"Volume Lighting\": \"Volume Lighting\"},\n                    {\"World Position\": \"World Position\"}\n                  ]\n              },\n              {\n                  \"type\": \"label\",\n                  \"label\": \"Add additional options - put attribute and value, like <code>redshiftOptions.reflectionMaxTraceDepth</code> = <code>3</code>\"\n              },\n              {\n                  \"type\": \"dict-modifiable\",\n                  \"store_as_list\": true,\n                  \"key\": \"additional_options\",\n                  \"label\": \"Additional Renderer Options\",\n                  \"use_label_wrap\": true,\n                  \"object_type\": {\n                        \"type\": \"text\"\n                  }\n              }\n            ]\n        },\n        {\n          \"type\": \"dict\",\n          \"collapsible\": true,\n          \"key\": \"renderman_renderer\",\n          \"label\": \"Renderman Renderer\",\n          \"is_group\": true,\n          \"children\": [\n            {\n                \"key\": \"image_prefix\",\n                \"label\": \"Image prefix template\",\n                \"type\": \"text\"\n            },\n            {\n              \"key\": \"image_dir\",\n              \"label\": \"Image Output Directory\",\n              \"type\": \"text\"\n            },\n            {\n              \"key\": \"display_filters\",\n              \"label\": \"Display Filters\",\n              \"type\": \"enum\",\n              \"multiselection\": true,\n              \"defaults\": \"empty\",\n              \"enum_items\": [\n                {\"PxrBackgroundDisplayFilter\": \"PxrBackgroundDisplayFilter\"},\n                {\"PxrCopyAOVDisplayFilter\": \"PxrCopyAOVDisplayFilter\"},\n                {\"PxrEdgeDetect\":\"PxrEdgeDetect\"},\n                {\"PxrFilmicTonemapperDisplayFilter\": \"PxrFilmicTonemapperDisplayFilter\"},\n                {\"PxrGradeDisplayFilter\": \"PxrGradeDisplayFilter\"},\n                {\"PxrHalfBufferErrorFilter\": \"PxrHalfBufferErrorFilter\"},\n                {\"PxrImageDisplayFilter\": \"PxrImageDisplayFilter\"},\n                {\"PxrLightSaturation\": \"PxrLightSaturation\"},\n                {\"PxrShadowDisplayFilter\": \"PxrShadowDisplayFilter\"},\n                {\"PxrStylizedHatching\": \"PxrStylizedHatching\"},\n                {\"PxrStylizedLines\": \"PxrStylizedLines\"},\n                {\"PxrStylizedToon\": \"PxrStylizedToon\"},\n                {\"PxrWhitePointDisplayFilter\": \"PxrWhitePointDisplayFilter\"}\n              ]\n            },\n            {\n              \"key\": \"imageDisplay_dir\",\n              \"label\": \"Image Display Filter Directory\",\n              \"type\": \"text\"\n            },\n            {\n              \"key\": \"sample_filters\",\n              \"label\": \"Sample Filters\",\n              \"type\": \"enum\",\n              \"multiselection\": true,\n              \"defaults\": \"empty\",\n              \"enum_items\": [\n                {\"PxrBackgroundSampleFilter\": \"PxrBackgroundSampleFilter\"},\n                {\"PxrCopyAOVSampleFilter\": \"PxrCopyAOVSampleFilter\"},\n                {\"PxrCryptomatte\": \"PxrCryptomatte\"},\n                {\"PxrFilmicTonemapperSampleFilter\": \"PxrFilmicTonemapperSampleFilter\"},\n                {\"PxrGradeSampleFilter\": \"PxrGradeSampleFilter\"},\n                {\"PxrShadowFilter\": \"PxrShadowFilter\"},\n                {\"PxrWatermarkFilter\": \"PxrWatermarkFilter\"},\n                {\"PxrWhitePointSampleFilter\": \"PxrWhitePointSampleFilter\"}\n              ]\n            },\n            {\n              \"key\": \"cryptomatte_dir\",\n              \"label\": \"Cryptomatte Output Directory\",\n              \"type\": \"text\"\n            },\n            {\n              \"key\": \"watermark_dir\",\n              \"label\": \"Watermark Filter Directory\",\n              \"type\": \"text\"\n            },\n            {\n                \"type\": \"label\",\n                \"label\": \"Add additional options - put attribute and value, like <code>Ci</code>\"\n            },\n            {\n                \"type\": \"dict-modifiable\",\n                \"store_as_list\": true,\n                \"key\": \"additional_options\",\n                \"label\": \"Additional Renderer Options\",\n                \"use_label_wrap\": true,\n                \"object_type\": {\n                      \"type\": \"text\"\n                }\n            }\n          ]\n      }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json",
    "content": "{\n    \"key\": \"imageio\",\n    \"type\": \"dict\",\n    \"label\": \"Color Management (OCIO managed)\",\n    \"collapsible\": true,\n    \"is_group\": true,\n    \"children\": [\n        {\n            \"type\": \"template\",\n            \"name\": \"template_host_color_management_ocio\"\n        },\n        {\n            \"key\": \"viewer\",\n            \"type\": \"dict\",\n            \"label\": \"Viewer\",\n            \"collapsible\": false,\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"viewerProcess\",\n                    \"label\": \"Viewer Process\"\n                }\n            ]\n        },\n        {\n            \"key\": \"baking\",\n            \"type\": \"dict\",\n            \"label\": \"Extract-review baking profile\",\n            \"collapsible\": false,\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"viewerProcess\",\n                    \"label\": \"Viewer Process\"\n                }\n            ]\n        },\n        {\n            \"key\": \"workfile\",\n            \"type\": \"dict\",\n            \"label\": \"Workfile\",\n            \"collapsible\": false,\n            \"children\": [\n                {\n                    \"type\": \"form\",\n                    \"children\": [\n                        {\n                            \"type\": \"enum\",\n                            \"key\": \"colorManagement\",\n                            \"label\": \"color management\",\n                            \"enum_items\": [\n                                {\n                                    \"Nuke\": \"Nuke\"\n                                },\n                                {\n                                    \"OCIO\": \"OCIO\"\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"enum\",\n                            \"key\": \"OCIO_config\",\n                            \"label\": \"OpenColorIO Config\",\n                            \"enum_items\": [\n                                {\n                                    \"nuke-default\": \"nuke-default\"\n                                },\n                                {\n                                    \"spi-vfx\": \"spi-vfx (11)\"\n                                },\n                                {\n                                    \"spi-anim\": \"spi-anim (11)\"\n                                },\n                                {\n                                    \"aces_0.1.1\": \"aces_0.1.1 (11)\"\n                                },\n                                {\n                                    \"aces_0.7.1\": \"aces_0.7.1 (11)\"\n                                },\n                                {\n                                    \"aces_1.0.1\": \"aces_1.0.1 (11)\"\n                                },\n                                {\n                                    \"aces_1.0.3\": \"aces_1.0.3 (11, 12)\"\n                                },\n                                {\n                                    \"aces_1.1\": \"aces_1.1 (12, 13)\"\n                                },\n                                {\n                                    \"aces_1.2\": \"aces_1.2 (13, 14)\"\n                                },\n                                {\n                                    \"studio-config-v1.0.0_aces-v1.3_ocio-v2.1\": \"studio-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)\"\n                                },\n                                {\n                                    \"cg-config-v1.0.0_aces-v1.3_ocio-v2.1\": \"cg-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)\"\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"workingSpaceLUT\",\n                            \"label\": \"Working Space\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"monitorLut\",\n                            \"label\": \"Thumbnails\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"monitorOutLUT\",\n                            \"label\": \"Monitor Out\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"int8Lut\",\n                            \"label\": \"8-bit files\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"int16Lut\",\n                            \"label\": \"16-bit files\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"logLut\",\n                            \"label\": \"log files\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"floatLut\",\n                            \"label\": \"float files\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"key\": \"nodes\",\n            \"type\": \"dict\",\n            \"label\": \"Nodes\",\n            \"collapsible\": true,\n            \"children\": [\n                {\n                    \"key\": \"requiredNodes\",\n                    \"type\": \"list\",\n                    \"label\": \"Plugin required\",\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"list\",\n                                \"key\": \"plugins\",\n                                \"label\": \"Used in plugins\",\n                                \"object_type\": {\n                                    \"type\": \"text\",\n                                    \"key\": \"pluginClass\"\n                                }\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"nukeNodeClass\",\n                                \"label\": \"Nuke Node Class\"\n                            },\n                            {\n                                \"type\": \"schema_template\",\n                                \"name\": \"template_nuke_knob_inputs\",\n                                \"template_data\": [\n                                    {\n                                        \"label\": \"Knobs\",\n                                        \"key\": \"knobs\"\n                                    }\n                                ]\n                            }\n\n                        ]\n                    }\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"overrideNodes\",\n                    \"label\": \"Plugin's node overrides\",\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"list\",\n                                \"key\": \"plugins\",\n                                \"label\": \"Used in plugins\",\n                                \"object_type\": {\n                                    \"type\": \"text\",\n                                    \"key\": \"pluginClass\"\n                                }\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"nukeNodeClass\",\n                                \"label\": \"Nuke Node Class\"\n                            },\n                            {\n                                \"key\": \"subsets\",\n                                \"label\": \"Subsets\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"type\": \"schema_template\",\n                                \"name\": \"template_nuke_knob_inputs\",\n                                \"template_data\": [\n                                    {\n                                        \"label\": \"Knobs overrides\",\n                                        \"key\": \"knobs\"\n                                    }\n                                ]\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"key\": \"regexInputs\",\n            \"type\": \"dict\",\n            \"label\": \"Colorspace on Inputs by regex detection\",\n            \"collapsible\": true,\n            \"children\": [\n                {\n                    \"type\": \"list\",\n                    \"key\": \"inputs\",\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"regex\",\n                                \"label\": \"Regex\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"colorspace\",\n                                \"label\": \"Colorspace\"\n                            }\n                        ]\n                    }\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"load\",\n    \"label\": \"Loader plugins\",\n    \"children\": [\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_loader_plugin_nuke\",\n            \"template_data\": [\n                {\n                    \"key\": \"LoadImage\",\n                    \"label\": \"Image Loader\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"LoadClip\",\n            \"label\": \"Clip Loader\",\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"_representations\",\n                    \"label\": \"Representations\",\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"node_name_template\",\n                    \"label\": \"Node name template\"\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"type\": \"dict\",\n                    \"collapsible\": false,\n                    \"key\": \"options_defaults\",\n                    \"label\": \"Loader option defaults\",\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"start_at_workfile\",\n                            \"label\": \"Start at worfile beggining\"\n                        },\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"add_retime\",\n                            \"label\": \"Add retime\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"publish\",\n    \"label\": \"Publish plugins\",\n    \"children\": [\n        {\n            \"type\": \"label\",\n            \"label\": \"Collectors\"\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"CollectInstanceData\",\n            \"label\": \"CollectInstanceData\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"enum\",\n                    \"key\": \"sync_workfile_version_on_families\",\n                    \"label\": \"Sync workfile version for families\",\n                    \"multiselection\": true,\n                    \"enum_items\": [\n                        {\n                            \"nukenodes\": \"nukenodes\"\n                        },\n                        {\n                            \"model\": \"model\"\n                        },\n                        {\n                            \"camera\": \"camera\"\n                        },\n                        {\n                            \"gizmo\": \"gizmo\"\n                        },\n                        {\n                            \"source\": \"source\"\n                        },\n                        {\n                            \"prerender\": \"prerender\"\n                        },\n                        {\n                            \"render\": \"render\"\n                        },\n                        {\n                            \"write\": \"write\"\n                        }\n                    ]\n                }\n            ]\n        },\n                {\n            \"type\": \"splitter\"\n        },\n        {\n            \"type\": \"label\",\n            \"label\": \"Validators\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_publish_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"ValidateCorrectAssetContext\",\n                    \"label\": \"Validate Correct Asset Name\"\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_publish_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"ValidateContainers\",\n                    \"label\": \"Validate Containers\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"ValidateKnobs\",\n            \"label\": \"Validate Knobs\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"raw-json\",\n                    \"key\": \"knobs\",\n                    \"label\": \"Knobs\"\n                }\n            ]\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_publish_plugin\",\n            \"template_data\": [\n                {\n                    \"key\": \"ValidateOutputResolution\",\n                    \"label\": \"Validate Output Resolution\"\n                },\n                {\n                    \"key\": \"ValidateBackdrop\",\n                    \"label\": \"Validate Backdrop\"\n                },\n                {\n                    \"key\": \"ValidateGizmo\",\n                    \"label\": \"Validate Gizmo (Group)\"\n                },\n                {\n                    \"key\": \"ValidateScriptAttributes\",\n                    \"label\": \"Validate workfile attributes\"\n                }\n            ]\n        },\n                {\n            \"type\": \"splitter\"\n        },\n        {\n            \"type\": \"label\",\n            \"label\": \"Extractors\"\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"ExtractReviewData\",\n            \"label\": \"ExtractReviewData\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"ExtractReviewDataLut\",\n            \"label\": \"ExtractReviewDataLut\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"ExtractReviewDataMov\",\n            \"label\": \"ExtractReviewDataMov\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"viewer_lut_raw\",\n                    \"label\": \"Viewer LUT raw\"\n                },\n                {\n                    \"key\": \"outputs\",\n                    \"label\": \"Output Definitions\",\n                    \"type\": \"dict-modifiable\",\n                    \"highlight_content\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"dict\",\n                                \"collapsible\": false,\n                                \"key\": \"filter\",\n                                \"label\": \"Filtering\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"task_types\",\n                                        \"label\": \"Task types\",\n                                        \"type\": \"task-types-enum\"\n                                    },\n                                    {\n                                        \"key\": \"families\",\n                                        \"label\": \"Families\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"subsets\",\n                                        \"label\": \"Subsets\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    }\n                                ]\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"read_raw\",\n                                \"label\": \"Read colorspace RAW\",\n                                \"default\": false\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"viewer_process_override\",\n                                \"label\": \"Viewer Process colorspace profile override\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"bake_viewer_process\",\n                                \"label\": \"Bake Viewer Process\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"bake_viewer_input_process\",\n                                \"label\": \"Bake Viewer Input Process (LUTs)\"\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"key\": \"reformat_nodes_config\",\n                                \"type\": \"dict\",\n                                \"label\": \"Reformat Nodes\",\n                                \"collapsible\": true,\n                                \"checkbox_key\": \"enabled\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"enabled\",\n                                        \"label\": \"Enabled\"\n                                    },\n                                    {\n                                        \"type\": \"label\",\n                                        \"label\": \"Reposition knobs supported only.<br/>You can add multiple reformat nodes <br/>and set their knobs. Order of reformat <br/>nodes is important. First reformat node <br/>will be applied first and last reformat <br/>node will be applied last.\"\n                                    },\n                                    {\n                                        \"key\": \"reposition_nodes\",\n                                        \"type\": \"list\",\n                                        \"label\": \"Reposition nodes\",\n                                        \"object_type\": {\n                                            \"type\": \"dict\",\n                                            \"children\": [\n                                                {\n                                                    \"key\": \"node_class\",\n                                                    \"label\": \"Node class\",\n                                                    \"type\": \"text\"\n                                                },\n                                                {\n                                                    \"type\": \"schema_template\",\n                                                    \"name\": \"template_nuke_knob_inputs\",\n                                                    \"template_data\": [\n                                                        {\n                                                            \"label\": \"Node knobs\",\n                                                            \"key\": \"knobs\"\n                                                        }\n                                                    ]\n                                                }\n                                            ]\n                                        }\n                                    }\n                                ]\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"extension\",\n                                \"label\": \"Write node file type\"\n                            },\n                            {\n                                \"key\": \"add_custom_tags\",\n                                \"label\": \"Add custom tags\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            }\n                        ]\n                    }\n                }\n\n            ]\n        },\n        {\n            \"type\": \"label\",\n            \"label\": \"^ Settings and for <span style=\\\"color:#FF0000\\\";><b>ExtractReviewDataMov</b></span> is deprecated and will be soon removed.  <br> Please use <b>ExtractReviewIntermediates</b> instead.\"\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"ExtractReviewIntermediates\",\n            \"label\": \"ExtractReviewIntermediates\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"viewer_lut_raw\",\n                    \"label\": \"Viewer LUT raw\"\n                },\n                {\n                    \"key\": \"outputs\",\n                    \"label\": \"Output Definitions\",\n                    \"type\": \"dict-modifiable\",\n                    \"highlight_content\": true,\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"type\": \"dict\",\n                                \"collapsible\": false,\n                                \"key\": \"filter\",\n                                \"label\": \"Filtering\",\n                                \"children\": [\n                                    {\n                                        \"key\": \"task_types\",\n                                        \"label\": \"Task types\",\n                                        \"type\": \"task-types-enum\"\n                                    },\n                                    {\n                                        \"key\": \"families\",\n                                        \"label\": \"Families\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    },\n                                    {\n                                        \"key\": \"subsets\",\n                                        \"label\": \"Subsets\",\n                                        \"type\": \"list\",\n                                        \"object_type\": \"text\"\n                                    }\n                                ]\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"read_raw\",\n                                \"label\": \"Read colorspace RAW\",\n                                \"default\": false\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"viewer_process_override\",\n                                \"label\": \"Viewer Process colorspace profile override\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"bake_viewer_process\",\n                                \"label\": \"Bake Viewer Process\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"bake_viewer_input_process\",\n                                \"label\": \"Bake Viewer Input Process (LUTs)\"\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"key\": \"reformat_nodes_config\",\n                                \"type\": \"dict\",\n                                \"label\": \"Reformat Nodes\",\n                                \"collapsible\": true,\n                                \"checkbox_key\": \"enabled\",\n                                \"children\": [\n                                    {\n                                        \"type\": \"boolean\",\n                                        \"key\": \"enabled\",\n                                        \"label\": \"Enabled\"\n                                    },\n                                    {\n                                        \"type\": \"label\",\n                                        \"label\": \"Reposition knobs supported only.<br/>You can add multiple reformat nodes <br/>and set their knobs. Order of reformat <br/>nodes is important. First reformat node <br/>will be applied first and last reformat <br/>node will be applied last.\"\n                                    },\n                                    {\n                                        \"key\": \"reposition_nodes\",\n                                        \"type\": \"list\",\n                                        \"label\": \"Reposition nodes\",\n                                        \"object_type\": {\n                                            \"type\": \"dict\",\n                                            \"children\": [\n                                                {\n                                                    \"key\": \"node_class\",\n                                                    \"label\": \"Node class\",\n                                                    \"type\": \"text\"\n                                                },\n                                                {\n                                                    \"type\": \"schema_template\",\n                                                    \"name\": \"template_nuke_knob_inputs\",\n                                                    \"template_data\": [\n                                                        {\n                                                            \"label\": \"Node knobs\",\n                                                            \"key\": \"knobs\"\n                                                        }\n                                                    ]\n                                                }\n                                            ]\n                                        }\n                                    }\n                                ]\n                            },\n                            {\n                                \"type\": \"separator\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"extension\",\n                                \"label\": \"Write node file type\"\n                            },\n                            {\n                                \"key\": \"add_custom_tags\",\n                                \"label\": \"Add custom tags\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            }\n                        ]\n                    }\n                }\n\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"key\": \"ExtractSlateFrame\",\n            \"label\": \"ExtractSlateFrame\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"viewer_lut_raw\",\n                    \"label\": \"Viewer LUT raw\"\n                },\n                {\n                    \"type\": \"separator\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Fill specific slate node values with templates. Uncheck the checkbox to not change the value.\",\n                    \"word_wrap\": true\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"key_value_mapping\",\n                    \"children\": [\n                        {\n                            \"type\": \"list-strict\",\n                            \"key\": \"f_submission_note\",\n                            \"label\": \"Submission Note:\",\n                            \"object_types\": [\n                                {\n                                    \"type\": \"boolean\"\n                                },\n                                {\n                                    \"type\": \"text\"\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"list-strict\",\n                            \"key\": \"f_submitting_for\",\n                            \"label\": \"Submission For:\",\n                            \"object_types\": [\n                                {\n                                    \"type\": \"boolean\"\n                                },\n                                {\n                                    \"type\": \"text\"\n                                }\n                            ]\n                        },\n                        {\n                            \"type\": \"list-strict\",\n                            \"key\": \"f_vfx_scope_of_work\",\n                            \"label\": \"VFX Scope Of Work:\",\n                            \"object_types\": [\n                                {\n                                    \"type\": \"boolean\"\n                                },\n                                {\n                                    \"type\": \"text\"\n                                }\n                            ]\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"splitter\"\n        },\n        {\n            \"type\": \"label\",\n            \"label\": \"Integrators\"\n        },\n        {\n            \"type\": \"dict\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"key\": \"IncrementScriptVersion\",\n            \"label\": \"IncrementScriptVersion\",\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"optional\",\n                    \"label\": \"Optional\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"active\",\n                    \"label\": \"Active\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json",
    "content": "{\n    \"type\": \"list\",\n    \"key\": \"gizmo\",\n    \"label\": \"Gizmo Menu\",\n    \"is_group\": true,\n    \"use_label_wrap\": true,\n    \"object_type\": {\n        \"type\": \"dict\",\n        \"children\": [\n            {\n                \"type\": \"text\",\n                \"key\": \"toolbar_menu_name\",\n                \"label\": \"Toolbar Menu Name\"\n            },\n            {\n                \"type\": \"path\",\n                \"key\": \"gizmo_source_dir\",\n                \"label\": \"Gizmo directory path\",\n                \"multipath\": true,\n                \"multiplatform\": true\n            },\n            {\n                \"type\": \"collapsible-wrap\",\n                \"label\": \"Options\",\n                \"collapsible\": true,\n                \"collapsed\": true,\n                \"children\": [\n                    {\n                        \"type\": \"path\",\n                        \"key\": \"toolbar_icon_path\",\n                        \"label\": \"Toolbar Icon Path\",\n                        \"multipath\": false,\n                        \"multiplatform\": true\n                    },\n                    {\n                        \"type\": \"splitter\"\n                    },\n                    {\n                        \"type\": \"list\",\n                        \"key\": \"gizmo_definition\",\n                        \"label\": \"Gizmo definitions\",\n                        \"use_label_wrap\": true,\n                        \"object_type\": {\n                            \"type\": \"dict\",\n                            \"children\": [\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"gizmo_toolbar_path\",\n                                    \"label\": \"Gizmo Menu Path\"\n                                },\n                                {\n                                    \"type\": \"list\",\n                                    \"key\": \"sub_gizmo_list\",\n                                    \"label\": \"Sub Gizmo List\",\n                                    \"use_label_wrap\": true,\n                                    \"object_type\": {\n                                        \"type\": \"dict-conditional\",\n                                        \"enum_key\": \"sourcetype\",\n                                        \"enum_label\": \"Type of usage\",\n                                        \"enum_children\": [\n                                            {\n                                                \"key\": \"python\",\n                                                \"label\": \"Python\",\n                                                \"children\": [\n                                                    {\n                                                        \"type\": \"text\",\n                                                        \"key\": \"title\",\n                                                        \"label\": \"Title\"\n                                                    },\n                                                    {\n                                                        \"type\": \"text\",\n                                                        \"key\": \"command\",\n                                                        \"label\": \"Python command\"\n                                                    },\n                                                    {\n                                                        \"type\": \"text\",\n                                                        \"key\": \"icon\",\n                                                        \"label\": \"Icon Path\"\n                                                    },\n                                                    {\n                                                        \"type\": \"text\",\n                                                        \"key\": \"shortcut\",\n                                                        \"label\": \"Hotkey\"\n                                                    }\n                                                ]\n                                            },\n                                            {\n                                                \"key\": \"file\",\n                                                \"label\": \"File\",\n                                                \"children\": [\n                                                    {\n                                                        \"type\": \"text\",\n                                                        \"key\": \"title\",\n                                                        \"label\": \"Title\"\n                                                    },\n                                                    {\n                                                        \"type\": \"text\",\n                                                        \"key\": \"file_name\",\n                                                        \"label\": \"Gizmo file name\"\n                                                    },\n                                                    {\n                                                        \"type\": \"text\",\n                                                        \"key\": \"shortcut\",\n                                                        \"label\": \"Hotkey\"\n                                                    }\n                                                ]\n                                            },\n                                            {\n                                                \"key\": \"separator\",\n                                                \"label\": \"Separator\",\n                                                \"children\": [\n                                                {\n                                                    \"type\": \"text\",\n                                                    \"key\": \"gizmo_toolbar_path\",\n                                                    \"label\": \"Toolbar path\"\n                                                }\n                                                ]\n                                            }\n                                        ]\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                ]\n            }\n        ]\n    }\n}"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_publish_gui_filter.json",
    "content": "{\n    \"type\": \"dict-modifiable\",\n    \"collapsible\": true,\n    \"key\": \"filters\",\n    \"label\": \"Publish GUI Filters\",\n    \"object_type\": {\n        \"type\": \"raw-json\"\n    }\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_representation_tags.json",
    "content": "{\n    \"key\": \"tags\",\n    \"label\": \"Tags\",\n    \"type\": \"enum\",\n    \"multiselection\": true,\n    \"enum_items\": [\n        {\n            \"burnin\": \"Add burnins\"\n        },\n        {\n            \"review\": \"Create review\"\n        },\n        {\n            \"ftrackreview\": \"Add review to Ftrack\"\n        },\n        {\n            \"shotgridreview\": \"Add review to Shotgrid\"\n        },\n        {\n            \"kitsureview\": \"Add review to Kitsu\"\n        },\n        {\n            \"delete\": \"Delete output\"\n        },\n        {\n            \"slate-frame\": \"Add slate frame\"\n        },\n        {\n            \"no-handles\": \"Skip handle frames\"\n        },\n        {\n            \"sequence\": \"Output as image sequence\"\n        },\n        {\n            \"no-audio\": \"Do not add audio\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_scriptsmenu.json",
    "content": "{\n  \"type\": \"dict\",\n  \"collapsible\": true,\n  \"key\": \"scriptsmenu\",\n  \"label\": \"Scripts Menu Definition\",\n  \"children\": [\n    {\n      \"type\": \"text\",\n      \"key\": \"name\",\n      \"label\": \"Menu Name\"\n    },\n    {\n      \"type\": \"splitter\"\n    },\n    {\n      \"type\": \"raw-json\",\n      \"key\": \"definition\",\n      \"label\": \"Menu definition\",\n      \"is_list\": true\n    }\n  ]\n}"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"templated_workfile_build\",\n    \"label\": \"Templated Workfile Build Settings\",\n    \"children\": [\n        {\n            \"type\": \"list\",\n            \"key\": \"profiles\",\n            \"label\": \"Profiles\",\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"children\": [\n                    {\n                        \"key\": \"task_types\",\n                        \"label\": \"Task types\",\n                        \"type\": \"task-types-enum\"\n                    },\n                    {\n                        \"key\": \"task_names\",\n                        \"label\": \"Task names\",\n                        \"type\": \"list\",\n                        \"object_type\": \"text\"\n                    },\n                    {\n                        \"key\": \"path\",\n                        \"label\": \"Path to template\",\n                        \"type\": \"path\",\n                        \"multiplatform\": false,\n                        \"multipath\": false\n                    },\n                    {\n                        \"key\": \"keep_placeholder\",\n                        \"label\": \"Keep placeholders\",\n                        \"type\": \"boolean\",\n                        \"default\": true\n                    },\n                    {\n                        \"key\": \"create_first_version\",\n                        \"label\": \"Create first version\",\n                        \"type\": \"boolean\",\n                        \"default\": true\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json",
    "content": "{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"workfile_build\",\n    \"label\": \"Workfile Build Settings\",\n    \"children\": [\n        {\n            \"type\": \"list\",\n            \"key\": \"profiles\",\n            \"label\": \"Profiles\",\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"children\": [\n                    {\n                        \"key\": \"task_types\",\n                        \"label\": \"Task types\",\n                        \"type\": \"task-types-enum\"\n                    },\n                    {\n                        \"key\": \"tasks\",\n                        \"label\": \"Task names\",\n                        \"type\": \"list\",\n                        \"object_type\": \"text\"\n                    },\n                    {\n                        \"type\": \"splitter\"\n                    },\n                    {\n                        \"key\": \"current_context\",\n                        \"label\": \"<b>Current Context</b>\",\n                        \"type\": \"list\",\n                        \"highlight_content\": true,\n                        \"object_type\": {\n                            \"type\": \"dict\",\n                            \"children\": [\n                                {\n                                    \"key\": \"subset_name_filters\",\n                                    \"label\": \"Subset name Filters\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                },\n                                {\n                                    \"key\": \"families\",\n                                    \"label\": \"Families\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                },\n                                {\n                                    \"key\": \"repre_names\",\n                                    \"label\": \"Repre Names\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                },\n                                {\n                                    \"key\": \"loaders\",\n                                    \"label\": \"Loaders\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                }\n                            ]\n                        }\n                    },\n                    {\n                        \"key\": \"linked_assets\",\n                        \"label\": \"<b>Linked Assets</b>\",\n                        \"type\": \"list\",\n                        \"highlight_content\": true,\n                        \"object_type\": {\n                            \"type\": \"dict\",\n                            \"children\": [\n                                {\n                                    \"key\": \"subset_name_filters\",\n                                    \"label\": \"Subset name Filters\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                },\n                                {\n                                    \"key\": \"families\",\n                                    \"label\": \"Families\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                },\n                                {\n                                    \"key\": \"repre_names\",\n                                    \"label\": \"Repre Names\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                },\n                                {\n                                    \"key\": \"loaders\",\n                                    \"label\": \"Loaders\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                }\n                            ]\n                        }\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_colorspace_remapping.json",
    "content": "[\n    {\n        \"key\": \"remapping\",\n        \"type\": \"dict\",\n        \"label\": \"Remapping colorspace names\",\n        \"collapsible\": true,\n        \"children\": [\n            {\n                \"type\": \"list\",\n                \"key\": \"rules\",\n                \"object_type\": {\n                    \"type\": \"dict\",\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"host_native_name\",\n                            \"label\": \"Application native colorspace name\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"ocio_name\",\n                            \"label\": \"OCIO colorspace name\"\n                        }\n                    ]\n                }\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json",
    "content": "[\n    {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"key\": \"{key}\",\n        \"label\": \"{label}\",\n        \"checkbox_key\": \"enabled\",\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"list\",\n                \"key\": \"default_variants\",\n                \"label\": \"Default Variants\",\n                \"object_type\": \"text\"\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_derived.json",
    "content": "[\n    {\n        \"type\": \"label\",\n        \"label\": \"The application does not include any built-in color management capabilities, OpenPype offers a solution <br>to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management <br>system from file paths, using File Rules feature only during Publishing.<br><br><b><a href='https://ayon.ynput.io/docs/admin_colorspace#derived-colorspace' style=\\\"color:#00d6a1\\\";>Related documentation.</a></b>\"\n    },\n    {\n        \"type\": \"boolean\",\n        \"key\": \"activate_host_color_management\",\n        \"label\": \"Enable Color Management\"\n    },\n    {\n        \"type\": \"template\",\n        \"name\": \"template_imageio_config\"\n    },\n    {\n        \"type\": \"template\",\n        \"name\": \"template_imageio_file_rules\"\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_ocio.json",
    "content": "[\n    {\n        \"type\": \"label\",\n        \"label\": \"Colorspace management for the application can be controlled through OpenPype settings.  <br>Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile. <br>Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.<br><br><b><a href='https://ayon.ynput.io/docs/admin_colorspace#remapped-internal-colorspace' style=\\\"color:#00d6a1\\\";>Related documentation.</a></b>\"\n    },\n    {\n        \"type\": \"boolean\",\n        \"key\": \"activate_host_color_management\",\n        \"label\": \"Enable Color Management\"\n    },\n    {\n        \"type\": \"template\",\n        \"name\": \"template_imageio_config\"\n    },\n    {\n        \"type\": \"template\",\n        \"name\": \"template_imageio_file_rules\"\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_remapped.json",
    "content": "[\n    {\n        \"type\": \"label\",\n        \"label\": \"The application includes internal color management functionality, but it does not offer external control <br>over this feature. To address this limitation, OpenPype uses mapping rules to remap the native <br>colorspace names used in the internal color management system to the OpenColorIO (OCIO) <br>color management system. Remapping feature is used in Publishing and Loading procedures.<br><br><b><a href='https://ayon.ynput.io/docs/admin_colorspace#remapped-internal-colorspace' style=\\\"color:#00d6a1\\\";>Related documentation.</a></b>.\"\n    },\n    {\n        \"type\": \"boolean\",\n        \"key\": \"activate_host_color_management\",\n        \"label\": \"Enable Color Management\"\n    },\n    {\n        \"type\": \"template\",\n        \"name\": \"template_colorspace_remapping\"\n    },\n    {\n        \"type\": \"template\",\n        \"name\": \"template_imageio_config\"\n    },\n    {\n        \"type\": \"template\",\n        \"name\": \"template_imageio_file_rules\"\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_config.json",
    "content": "[\n    {\n        \"key\": \"ocio_config\",\n        \"type\": \"dict\",\n        \"label\": \"OCIO config\",\n        \"collapsible\": true,\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"override_global_config\",\n                \"label\": \"Override global OCIO config\"\n            },\n            {\n                \"type\": \"path\",\n                \"key\": \"filepath\",\n                \"label\": \"Config path\",\n                \"multiplatform\": false,\n                \"multipath\": true\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_file_rules.json",
    "content": "[\n    {\n        \"key\": \"file_rules\",\n        \"type\": \"dict\",\n        \"label\": \"File Rules (OCIO v1 only)\",\n        \"collapsible\": true,\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"activate_host_rules\",\n                \"label\": \"Activate Host File Rules\"\n            },\n            {\n                \"key\": \"rules\",\n                \"label\": \"Rules\",\n                \"type\": \"dict-modifiable\",\n                \"highlight_content\": true,\n                \"collapsible\": false,\n                \"object_type\": {\n                    \"type\": \"dict\",\n                    \"children\": [\n                        {\n                            \"key\": \"pattern\",\n                            \"label\": \"Regex pattern\",\n                            \"type\": \"text\"\n                        },\n                        {\n                            \"key\": \"colorspace\",\n                            \"label\": \"Colorspace name\",\n                            \"type\": \"text\"\n                        },\n                        {\n                            \"key\": \"ext\",\n                            \"label\": \"File extension\",\n                            \"type\": \"text\"\n                        }\n                    ]\n                }\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_loader_plugin_nuke.json",
    "content": "[\n    {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"key\": \"{key}\",\n        \"label\": \"{label}\",\n        \"checkbox_key\": \"enabled\",\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"list\",\n                \"key\": \"_representations\",\n                \"label\": \"Representations\",\n                \"object_type\": \"text\"\n            },\n            {\n                \"type\": \"text\",\n                \"key\": \"node_name_template\",\n                \"label\": \"Node name template\"\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json",
    "content": "[\n    {\n        \"type\": \"collapsible-wrap\",\n        \"label\": \"{label}\",\n        \"collapsible\": true,\n        \"collapsed\": true,\n        \"children\": [{\n            \"type\": \"list\",\n            \"key\": \"{key}\",\n            \"object_type\": {\n                \"type\": \"dict-conditional\",\n                \"enum_key\": \"type\",\n                \"enum_label\": \"Type\",\n                \"enum_children\": [\n                    {\n                        \"key\": \"text\",\n                        \"label\": \"Text\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"name\",\n                                \"label\": \"Name\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"value\",\n                                \"label\": \"Value\"\n                            }\n                        ]\n                    },\n                    {\n                        \"key\": \"expression\",\n                        \"label\": \"Expression\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"name\",\n                                \"label\": \"Name\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"expression\",\n                                \"label\": \"Expression\"\n                            }\n                        ]\n                    },\n                    {\n                        \"key\": \"formatable\",\n                        \"label\": \"Formate from template\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"name\",\n                                \"label\": \"Name\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"template\",\n                                \"label\": \"Template\",\n                                \"placeholder\": \"{{key}} or {{key}};{{key}}\"\n                            },\n                            {\n                                \"type\": \"enum\",\n                                \"key\": \"to_type\",\n                                \"label\": \"Knob type\",\n                                \"enum_items\": [\n                                    {\n                                        \"text\": \"Text\"\n                                    },\n                                    {\n                                        \"number\": \"Number\"\n                                    },\n                                    {\n                                        \"decimal_number\": \"Decimal number\"\n                                    },\n                                    {\n                                        \"2d_vector\": \"2D vector\"\n                                    }\n                                ]\n                            }\n                        ]\n                    },\n                    {\n                        \"key\": \"color_gui\",\n                        \"label\": \"Color GUI\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"name\",\n                                \"label\": \"Name\"\n                            },\n                            {\n                                \"type\": \"color\",\n                                \"key\": \"value\",\n                                \"label\": \"Value\",\n                                \"use_alpha\": false\n                            }\n                        ]\n                    },\n                    {\n                        \"key\": \"bool\",\n                        \"label\": \"Boolean\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"name\",\n                                \"label\": \"Name\"\n                            },\n                            {\n                                \"type\": \"boolean\",\n                                \"key\": \"value\",\n                                \"label\": \"Value\"\n                            }\n                        ]\n                    },\n                    {\n                        \"key\": \"number\",\n                        \"label\": \"Number\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"name\",\n                                \"label\": \"Name\"\n                            },\n                            {\n                                \"type\": \"number\",\n                                \"key\": \"value\",\n                                \"default\": 1,\n                                \"decimal\": 0,\n                                \"maximum\": 99999999\n                            }\n\n                        ]\n                    },\n                    {\n                        \"key\": \"decimal_number\",\n                        \"label\": \"Decimal number\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"name\",\n                                \"label\": \"Name\"\n                            },\n                            {\n                                \"type\": \"number\",\n                                \"key\": \"value\",\n                                \"default\": 1,\n                                \"decimal\": 4,\n                                \"maximum\": 99999999\n                            }\n\n                        ]\n                    },\n                    {\n                        \"key\": \"2d_vector\",\n                        \"label\": \"2D vector\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"name\",\n                                \"label\": \"Name\"\n                            },\n                            {\n                                \"type\": \"list-strict\",\n                                \"key\": \"value\",\n                                \"label\": \"Value\",\n                                \"object_types\": [\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"x\",\n                                        \"default\": 1,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"y\",\n                                        \"default\": 1,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    }\n                                ]\n                            }\n                        ]\n                    },\n                    {\n                        \"key\": \"3d_vector\",\n                        \"label\": \"3D vector\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"name\",\n                                \"label\": \"Name\"\n                            },\n                            {\n                                \"type\": \"list-strict\",\n                                \"key\": \"value\",\n                                \"label\": \"Value\",\n                                \"object_types\": [\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"x\",\n                                        \"default\": 1,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"y\",\n                                        \"default\": 1,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"z\",\n                                        \"default\": 1,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    }\n                                ]\n                            }\n                        ]\n                    },\n                    {\n                        \"key\": \"color\",\n                        \"label\": \"Color\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"name\",\n                                \"label\": \"Name\"\n                            },\n                            {\n                                \"type\": \"list-strict\",\n                                \"key\": \"value\",\n                                \"label\": \"Value\",\n                                \"object_types\": [\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"r\",\n                                        \"default\": 1,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"g\",\n                                        \"default\": 1,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"b\",\n                                        \"default\": 1,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"a\",\n                                        \"default\": 1,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    }\n                                ]\n                            }\n                        ]\n                    },\n                    {\n                        \"key\": \"box\",\n                        \"label\": \"Box\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"name\",\n                                \"label\": \"Name\"\n                            },\n                            {\n                                \"type\": \"list-strict\",\n                                \"key\": \"value\",\n                                \"label\": \"Value\",\n                                \"object_types\": [\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"x\",\n                                        \"default\": 0,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"y\",\n                                        \"default\": 0,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"r\",\n                                        \"default\": 1920,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    },\n                                    {\n                                        \"type\": \"number\",\n                                        \"key\": \"t\",\n                                        \"default\": 1080,\n                                        \"decimal\": 4,\n                                        \"maximum\": 99999999\n                                    }\n                                ]\n                            }\n                        ]\n                    },\n                    {\n                        \"key\": \"__legacy__\",\n                        \"label\": \"_ Legacy type _\",\n                        \"children\": [\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"name\",\n                                \"label\": \"Name\"\n                            },\n                            {\n                                \"type\": \"text\",\n                                \"key\": \"value\",\n                                \"label\": \"Value\"\n                            }\n                        ]\n                    }\n                ]\n            }\n        }]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_write_attrs.json",
    "content": "[\n    {\n        \"key\": \"instance_attributes\",\n        \"label\": \"Instance attributes\",\n        \"type\": \"enum\",\n        \"multiselection\": true,\n        \"enum_items\": [\n            {\n                \"reviewable\": \"Reviewable\"\n            },\n            {\n                \"farm_rendering\": \"Farm rendering\"\n            },\n            {\n                \"use_range_limit\": \"Use range limit\"\n            },\n            {\n                \"ordered\": \"Defined order\"\n            },\n            {\n                \"channels\": \"Channels override\"\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json",
    "content": "[\n    {\n        \"__default_values__\": {\n            \"multiselection\": true\n        }\n    },\n    {\n        \"key\": \"{key}\",\n        \"label\": \"{label}\",\n        \"multiselection\": \"{multiselection}\",\n        \"type\": \"enum\",\n        \"enum_items\": [\n             {\"action\": \"action\"},\n             {\"animation\": \"animation\"},\n             {\"assembly\": \"assembly\"},\n             {\"audio\": \"audio\"},\n             {\"backgroundComp\": \"backgroundComp\"},\n             {\"backgroundLayout\": \"backgroundLayout\"},\n             {\"camera\": \"camera\"},\n             {\"editorial\": \"editorial\"},\n             {\"gizmo\": \"gizmo\"},\n             {\"image\": \"image\"},\n             {\"layout\": \"layout\"},\n             {\"look\": \"look\"},\n             {\"matchmove\": \"matchmove\"},\n             {\"mayaScene\": \"mayaScene\"},\n             {\"model\": \"model\"},\n             {\"nukenodes\": \"nukenodes\"},\n             {\"plate\": \"plate\"},\n             {\"pointcache\": \"pointcache\"},\n             {\"proxyAbc\": \"proxyAbc\"},\n             {\"prerender\": \"prerender\"},\n             {\"redshiftproxy\": \"redshiftproxy\"},\n             {\"reference\": \"reference\"},\n             {\"render\": \"render\"},\n             {\"review\": \"review\"},\n             {\"rig\": \"rig\"},\n             {\"setdress\": \"setdress\"},\n             {\"take\": \"take\"},\n             {\"usdShade\": \"usdShade\"},\n             {\"vdbcache\": \"vdbcache\"},\n             {\"vrayproxy\": \"vrayproxy\"},\n             {\"workfile\": \"workfile\"},\n             {\"xgen\": \"xgen\"},\n             {\"yetiRig\": \"yetiRig\"},\n             {\"yeticache\": \"yeticache\"}\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_publish_plugin.json",
    "content": "[\n    {\n        \"__default_values__\": {\n            \"docstring\": \"\"\n        }\n    },\n    {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"key\": \"{key}\",\n        \"label\": \"{label}\",\n        \"checkbox_key\": \"enabled\",\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"optional\",\n                \"label\": \"Optional\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"active\",\n                \"label\": \"Active\"\n            },\n            {\n                \"type\": \"label\",\n                \"label\": \"{docstring}\"\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_validate_plugin.json",
    "content": "[\n    {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"key\": \"{key}\",\n        \"label\": \"{label}\",\n        \"checkbox_key\": \"enabled\",\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"optional\",\n                \"label\": \"Optional\"\n            },\n            {\n                \"type\": \"boolean\",\n                \"key\": \"active\",\n                \"label\": \"Active\"\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_builder_simple.json",
    "content": "[\n    {\n        \"type\": \"dict\",\n        \"collapsible\": true,\n        \"key\": \"workfile_builder\",\n        \"label\": \"Workfile Builder\",\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"create_first_version\",\n                \"label\": \"Create first workfile\",\n                \"default\": false\n            },\n            {\n                \"type\": \"path\",\n                \"key\": \"template_path\",\n                \"label\": \"First workfile template\",\n                \"multiplatform\": true,\n                \"multipath\": false\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_options.json",
    "content": "[{\n    \"type\": \"dict\",\n    \"collapsible\": true,\n    \"key\": \"workfile_builder\",\n    \"label\": \"Workfile Builder\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"create_first_version\",\n            \"label\": \"Create first workfile\",\n            \"default\": false\n        },\n        {\n            \"type\": \"list\",\n            \"key\": \"custom_templates\",\n            \"label\": \"Custom templates\",\n            \"is_group\": true,\n            \"use_label_wrap\": true,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"children\": [\n                    {\n                        \"type\": \"task-types-enum\",\n                        \"key\": \"task_types\",\n                        \"label\": \"Task types\"\n                    },\n                    {\n                        \"type\": \"splitter\"\n                    },\n                    {\n                        \"type\": \"label\",\n                        \"label\": \"Absolute path to workfile template or OpenPype Anatomy text is accepted.\"\n                    },\n                    {\n                        \"type\": \"path\",\n                        \"key\": \"path\",\n                        \"label\": \"Path\",\n                        \"multiplatform\": true,\n                        \"multipath\": false\n                    }\n                ]\n            }\n        },\n        {\n            \"type\": \"boolean\",\n            \"key\": \"builder_on_start\",\n            \"label\": \"Run Builder Profiles on first launch\",\n            \"default\": false\n        },\n        {\n            \"type\": \"list\",\n            \"key\": \"profiles\",\n            \"label\": \"Profiles\",\n            \"use_label_wrap\": true,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"children\": [\n                    {\n                        \"key\": \"task_types\",\n                        \"label\": \"Task types\",\n                        \"type\": \"task-types-enum\"\n                    },\n                    {\n                        \"key\": \"tasks\",\n                        \"label\": \"Task names\",\n                        \"type\": \"list\",\n                        \"object_type\": \"text\"\n                    },\n                    {\n                        \"type\": \"splitter\"\n                    },\n                    {\n                        \"key\": \"current_context\",\n                        \"label\": \"<b>Current Context</b>\",\n                        \"type\": \"list\",\n                        \"highlight_content\": true,\n                        \"object_type\": {\n                            \"type\": \"dict\",\n                            \"children\": [\n                                {\n                                    \"key\": \"subset_name_filters\",\n                                    \"label\": \"Subset name Filters\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                },\n                                {\n                                    \"key\": \"families\",\n                                    \"label\": \"Families\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                },\n                                {\n                                    \"key\": \"repre_names\",\n                                    \"label\": \"Repre Names\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                },\n                                {\n                                    \"key\": \"loaders\",\n                                    \"label\": \"Loaders\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                }\n                            ]\n                        }\n                    },\n                    {\n                        \"type\": \"separator\"\n                    },\n                    {\n                        \"key\": \"linked_assets\",\n                        \"label\": \"<b>Linked Assets/Shots</b>\",\n                        \"type\": \"list\",\n                        \"highlight_content\": true,\n                        \"object_type\": {\n                            \"type\": \"dict\",\n                            \"children\": [\n                                {\n                                    \"key\": \"subset_name_filters\",\n                                    \"label\": \"Subset name Filters\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                },\n                                {\n                                    \"key\": \"families\",\n                                    \"label\": \"Families\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                },\n                                {\n                                    \"key\": \"repre_names\",\n                                    \"label\": \"Repre Names\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                },\n                                {\n                                    \"key\": \"loaders\",\n                                    \"label\": \"Loaders\",\n                                    \"type\": \"list\",\n                                    \"object_type\": \"text\"\n                                }\n                            ]\n                        }\n                    }\n                ]\n            }\n        }\n    ]\n}\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/example_infinite_hierarchy.json",
    "content": "[\n    {\n        \"type\": \"dict-conditional\",\n        \"use_label_wrap\": true,\n        \"collapsible\": true,\n        \"key\": \"menu_items\",\n        \"label\": \"Menu items\",\n        \"enum_key\": \"type\",\n        \"enum_label\": \"Type\",\n        \"enum_children\": [\n            {\n                \"key\": \"action\",\n                \"label\": \"Action\",\n                \"children\": [\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"key\",\n                        \"label\": \"Key\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"label\",\n                        \"label\": \"Label\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"command\",\n                        \"label\": \"Comand\"\n                    }\n                ]\n            },\n            {\n                \"key\": \"menu\",\n                \"label\": \"Menu\",\n                \"children\": [\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"label\",\n                        \"label\": \"Label\"\n                    },\n                    {\n                        \"key\": \"children\",\n                        \"label\": \"Children\",\n                        \"type\": \"list\",\n                        \"object_type\": {\n                            \"type\": \"template\",\n                            \"name\": \"example_infinite_hierarchy\"\n                        }\n                    }\n                ]\n            },\n            {\n                \"key\": \"separator\",\n                \"label\": \"Separator\"\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/example_schema.json",
    "content": "{\n    \"key\": \"example_dict\",\n    \"label\": \"Examples\",\n    \"type\": \"dict\",\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"color\",\n            \"label\": \"Color input\",\n            \"type\": \"color\"\n        },\n        {\n            \"type\": \"dict-conditional\",\n            \"key\": \"overridden_value\",\n            \"label\": \"Overridden value\",\n            \"enum_key\": \"overridden\",\n            \"enum_is_horizontal\": true,\n            \"enum_children\": [\n                {\n                    \"key\": \"overridden\",\n                    \"label\": \"Override value\",\n                    \"children\": [\n                        {\n                            \"type\": \"number\",\n                            \"key\": \"value\",\n                            \"label\": \"value\"\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"inherit\",\n                    \"label\": \"Inherit value\",\n                    \"children\": []\n                }\n            ]\n        },\n        {\n            \"type\": \"dict-conditional\",\n            \"use_label_wrap\": true,\n            \"collapsible\": true,\n            \"key\": \"menu_items\",\n            \"label\": \"Menu items\",\n            \"enum_key\": \"type\",\n            \"enum_label\": \"Type\",\n            \"enum_children\": [\n                {\n                    \"key\": \"action\",\n                    \"label\": \"Action\",\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"key\",\n                            \"label\": \"Key\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"label\",\n                            \"label\": \"Label\"\n                        },\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"command\",\n                            \"label\": \"Comand\"\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"menu\",\n                    \"label\": \"Menu\",\n                    \"children\": [\n                        {\n                            \"key\": \"children\",\n                            \"label\": \"Children\",\n                            \"type\": \"list\",\n                            \"object_type\": \"text\"\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"separator\",\n                    \"label\": \"Separator\"\n                }\n            ]\n        },\n        {\n            \"type\": \"list\",\n            \"use_label_wrap\": true,\n            \"collapsible\": true,\n            \"key\": \"infinite_hierarchy\",\n            \"label\": \"Infinite list template hierarchy\",\n            \"object_type\": {\n                \"type\": \"template\",\n                \"name\": \"example_infinite_hierarchy\"\n            }\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"template_exaples\",\n            \"label\": \"Schema template examples\",\n            \"children\": [\n                {\n                    \"type\": \"template\",\n                    \"name\": \"example_template\",\n                    \"template_data\": {\n                        \"host_label\": \"Application 1\",\n                        \"host_name\": \"app_1\",\n                        \"multipath_executables\": false\n                    }\n                },\n                {\n                    \"type\": \"template\",\n                    \"name\": \"example_template\",\n                    \"template_data\": {\n                        \"host_label\": \"Application 2\",\n                        \"host_name\": \"app_2\"\n                    }\n                }\n            ]\n        },\n        {\n            \"key\": \"dict_wrapper\",\n            \"type\": \"dict\",\n            \"children\": [\n                {\n                    \"type\": \"enum\",\n                    \"key\": \"test_enum_singleselection\",\n                    \"label\": \"Enum Single Selection\",\n                    \"enum_items\": [\n                        { \"value_1\": \"Label 1\" },\n                        { \"value_2\": \"Label 2\" },\n                        { \"value_3\": \"Label 3\" }\n                    ]\n                },\n                {\n                    \"type\": \"enum\",\n                    \"key\": \"test_enum_multiselection\",\n                    \"label\": \"Enum Multi Selection\",\n                    \"multiselection\": true,\n                    \"enum_items\": [\n                        { \"value_1\": \"Label 1\" },\n                        { \"value_2\": \"Label 2\" },\n                        { \"value_3\": \"Label 3\" }\n                    ]\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"bool\",\n                    \"label\": \"Boolean checkbox\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"NOTE: This is label\"\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"integer\",\n                    \"label\": \"Integer\",\n                    \"decimal\": 0,\n                    \"minimum\": 0,\n                    \"maximum\": 10\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"float\",\n                    \"label\": \"Float (2 decimals)\",\n                    \"decimal\": 2,\n                    \"minimum\": -10,\n                    \"maximum\": -5\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"number_with_slider\",\n                    \"label\": \"Number with slider\",\n                    \"decimal\": 2,\n                    \"minimum\": 0.0,\n                    \"maximum\": 1.0,\n                    \"show_slider\": true\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"singleline_text\",\n                    \"label\": \"Singleline text\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"multiline_text\",\n                    \"label\": \"Multiline text\",\n                    \"multiline\": true\n                },\n                {\n                    \"type\": \"raw-json\",\n                    \"key\": \"raw_json\",\n                    \"label\": \"Raw json input\"\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"list_item_of_multiline_texts\",\n                    \"label\": \"List of multiline texts\",\n                    \"object_type\": {\n                        \"type\": \"text\",\n                        \"multiline\": true\n                    }\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"list_item_of_floats\",\n                    \"label\": \"List of floats\",\n                    \"object_type\": {\n                        \"type\": \"number\",\n                        \"decimal\": 3,\n                        \"minimum\": 1000,\n                        \"maximum\": 2000\n                    }\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"key\": \"modifiable_dict_of_integers\",\n                    \"label\": \"Modifiable dict of integers\",\n                    \"object_type\": {\n                        \"type\": \"number\",\n                        \"decimal\": 0,\n                        \"minimum\": 10,\n                        \"maximum\": 100\n                    }\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"key\": \"modifiable_dict_with_required_keys\",\n                    \"label\": \"Modifiable dict with required keys\",\n                    \"required_keys\": [\n                        \"key_1\",\n                        \"key_2\"\n                    ],\n                    \"object_type\": \"text\"\n                },\n                {\n                    \"type\": \"list-strict\",\n                    \"key\": \"strict_list_labels_horizontal\",\n                    \"label\": \"StrictList-labels-horizontal (color)\",\n                    \"object_types\": [\n                        {\n                            \"label\": \"Red\",\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 255,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"label\": \"Green\",\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 255,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"label\": \"Blue\",\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 255,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"label\": \"Alpha\",\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 1,\n                            \"decimal\": 6\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"list-strict\",\n                    \"key\": \"strict_list_labels_vertical\",\n                    \"label\": \"StrictList-labels-vertical (color)\",\n                    \"horizontal\": false,\n                    \"object_types\": [\n                        {\n                            \"label\": \"Red\",\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 255,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"label\": \"Green\",\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 255,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"label\": \"Blue\",\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 255,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"label\": \"Alpha\",\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 1,\n                            \"decimal\": 6\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"list-strict\",\n                    \"key\": \"strict_list_nolabels_horizontal\",\n                    \"label\": \"StrictList-nolabels-horizontal (color)\",\n                    \"object_types\": [\n                        {\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 255,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 255,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 255,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 1,\n                            \"decimal\": 6\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"list-strict\",\n                    \"key\": \"strict_list_nolabels_vertical\",\n                    \"label\": \"StrictList-nolabels-vertical (color)\",\n                    \"horizontal\": false,\n                    \"object_types\": [\n                        {\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 255,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 255,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 255,\n                            \"decimal\": 0\n                        },\n                        {\n                            \"type\": \"number\",\n                            \"minimum\": 0,\n                            \"maximum\": 1,\n                            \"decimal\": 6\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"list\",\n                    \"key\": \"dict_item\",\n                    \"label\": \"DictItem in List\",\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"families\",\n                                \"label\": \"Families\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            },\n                            {\n                                \"key\": \"hosts\",\n                                \"label\": \"Hosts\",\n                                \"type\": \"list\",\n                                \"object_type\": \"text\"\n                            }\n                        ]\n                    }\n                },\n                {\n                    \"type\": \"path\",\n                    \"key\": \"single_path_input\",\n                    \"label\": \"Single path input\",\n                    \"multiplatform\": false,\n                    \"multipath\": false\n                },\n                {\n                    \"type\": \"path\",\n                    \"key\": \"multi_path_input\",\n                    \"label\": \"Multi path input\",\n                    \"multiplatform\": false,\n                    \"multipath\": true\n                },\n                {\n                    \"type\": \"path\",\n                    \"key\": \"single_os_specific_path_input\",\n                    \"label\": \"Single OS specific path input\",\n                    \"multiplatform\": true,\n                    \"multipath\": false\n                },\n                {\n                    \"type\": \"path\",\n                    \"key\": \"multi_os_specific_path_input\",\n                    \"label\": \"Multi OS specific path input\",\n                    \"multiplatform\": true,\n                    \"multipath\": true\n                },\n                {\n                    \"key\": \"collapsible\",\n                    \"type\": \"dict\",\n                    \"label\": \"collapsible dictionary\",\n                    \"collapsible\": true,\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"_nothing\",\n                            \"label\": \"Exmaple input\"\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"collapsible_expanded\",\n                    \"type\": \"dict\",\n                    \"label\": \"collapsible dictionary, expanded on creation\",\n                    \"collapsible\": true,\n                    \"collapsed\": false,\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"_nothing\",\n                            \"label\": \"Exmaple input\"\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"not_collapsible\",\n                    \"type\": \"dict\",\n                    \"label\": \"Not collapsible\",\n                    \"collapsible\": false,\n                    \"is_group\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"boolean\",\n                            \"key\": \"_nothing\",\n                            \"label\": \"Exmaple input\"\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"nested_dict_lvl1\",\n                    \"type\": \"dict\",\n                    \"label\": \"Nested dictionary (level 1)\",\n                    \"children\": [\n                        {\n                            \"key\": \"nested_dict_lvl2\",\n                            \"type\": \"dict\",\n                            \"label\": \"Nested dictionary (level 2)\",\n                            \"is_group\": true,\n                            \"children\": [\n                                {\n                                    \"key\": \"nested_dict_lvl3\",\n                                    \"type\": \"dict\",\n                                    \"label\": \"Nested dictionary (level 3)\",\n                                    \"children\": [\n                                        {\n                                            \"type\": \"boolean\",\n                                            \"key\": \"_nothing\",\n                                            \"label\": \"Exmaple input\"\n                                        }\n                                    ]\n                                },\n                                {\n                                    \"key\": \"nested_dict_lvl3_2\",\n                                    \"type\": \"dict\",\n                                    \"label\": \"Nested dictionary (level 3) (2)\",\n                                    \"children\": [\n                                        {\n                                            \"type\": \"text\",\n                                            \"key\": \"_nothing\",\n                                            \"label\": \"Exmaple input\"\n                                        },\n                                        {\n                                            \"type\": \"text\",\n                                            \"key\": \"_nothing2\",\n                                            \"label\": \"Exmaple input 2\"\n                                        }\n                                    ]\n                                }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"key\": \"form_examples\",\n                    \"type\": \"dict\",\n                    \"label\": \"Form examples\",\n                    \"children\": [\n                        {\n                            \"key\": \"inputs_without_form_example\",\n                            \"type\": \"dict\",\n                            \"label\": \"Inputs without form\",\n                            \"children\": [\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"_nothing_1\",\n                                    \"label\": \"Example label\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"_nothing_2\",\n                                    \"label\": \"Example label ####\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"key\": \"_nothing_3\",\n                                    \"label\": \"Example label ########\"\n                                }\n                            ]\n                        },\n                        {\n                            \"key\": \"inputs_with_form_example\",\n                            \"type\": \"dict\",\n                            \"label\": \"Inputs with form\",\n                            \"children\": [\n                                {\n                                    \"type\": \"form\",\n                                    \"children\": [\n                                        {\n                                            \"type\": \"text\",\n                                            \"key\": \"_nothing_1\",\n                                            \"label\": \"Example label\"\n                                        },\n                                        {\n                                            \"type\": \"text\",\n                                            \"key\": \"_nothing_2\",\n                                            \"label\": \"Example label ####\"\n                                        },\n                                        {\n                                            \"type\": \"text\",\n                                            \"key\": \"_nothing_3\",\n                                            \"label\": \"Example label ########\"\n                                        }\n                                    ]\n                                }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"collapsible-wrap\",\n                    \"label\": \"Collapsible Wrapper without key\",\n                    \"children\": [\n                        {\n                            \"type\": \"text\",\n                            \"key\": \"_example_input_collapsible\",\n                            \"label\": \"Example input in collapsible wrapper\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/example_template.json",
    "content": "[\n    {\n        \"__default_values__\": {\n            \"multipath_executables\": true\n        }\n    },\n    {\n        \"type\": \"raw-json\",\n        \"label\": \"{host_label} Environments\",\n        \"key\": \"{host_name}_environments\"\n    },\n    {\n        \"type\": \"path\",\n        \"key\": \"{host_name}_executables\",\n        \"label\": \"{host_label} - Full paths to executables\",\n        \"multiplatform\": \"{multipath_executables}\",\n        \"multipath\": true\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_3dequalizer.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"3dequalizer\",\n    \"label\": \"3DEqualizer\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n          \"type\": \"boolean\",\n          \"key\": \"enabled\",\n          \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        }, {\n            \"type\": \"number\",\n            \"key\": \"heartbeat_interval\",\n            \"label\": \"Qt Heartbeat Interval (ms)\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\"\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_3dsmax.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"3dsmax\",\n    \"label\": \"Autodesk 3ds Max\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\"\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"aftereffects\",\n    \"label\": \"Adobe AfterEffects\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\",\n                        \"skip_paths\": [\"use_python_2\"]\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"blender\",\n    \"label\": \"Blender\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\",\n                        \"skip_paths\": [\"use_python_2\"]\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_celaction.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"celaction\",\n    \"label\": \"CelAction2D\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"variants\",\n            \"children\": [\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_host_variant\",\n                    \"template_data\": [\n                        {\n                            \"app_variant_label\": \"Current\",\n                            \"app_variant\": \"current\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_djv.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"djvview\",\n    \"label\": \"DJV View\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\"\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_flame.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"flame\",\n    \"label\": \"Autodesk Flame\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\"\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"fusion\",\n    \"label\": \"Blackmagic Fusion\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\",\n                        \"skip_paths\": [\"use_python_2\"]\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"harmony\",\n    \"label\": \"Toon Boom Harmony\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\",\n                        \"skip_paths\": [\"use_python_2\"]\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_houdini.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"houdini\",\n    \"label\": \"SideFX Houdini\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\"\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_maya.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"maya\",\n    \"label\": \"Autodesk Maya\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\"\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_mayapy.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"mayapy\",\n    \"label\": \"Autodesk MayaPy\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\"\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"photoshop\",\n    \"label\": \"Adobe Photoshop\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\",\n                        \"skip_paths\": [\"use_python_2\"]\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_resolve.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"resolve\",\n    \"label\": \"Blackmagic DaVinci Resolve\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"variants\",\n            \"children\": [\n                {\n                    \"type\": \"schema_template\",\n                    \"name\": \"template_host_variant\",\n                    \"template_data\": [\n                        {\n                            \"app_variant_label\": \"stable\",\n                            \"app_variant\": \"stable\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_substancepainter.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"substancepainter\",\n    \"label\": \"Substance Painter\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\",\n                        \"skip_paths\": [\"use_python_2\"]\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"tvpaint\",\n    \"label\": \"TVPaint\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\",\n                        \"skip_paths\": [\"use_python_2\"]\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"unreal\",\n    \"label\": \"Unreal Editor\",\n    \"collapsible\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"schema_template\",\n            \"name\": \"template_host_unchangables\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\"\n        },\n        {\n            \"type\": \"dict-modifiable\",\n            \"key\": \"variants\",\n            \"collapsible_key\": true,\n            \"use_label_wrap\": false,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"collapsible\": true,\n                \"children\": [\n                    {\n                        \"type\": \"schema_template\",\n                        \"name\": \"template_host_variant_items\"\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json",
    "content": "[\n    {\n        \"type\": \"text\",\n        \"key\": \"label\",\n        \"label\": \"Label\",\n        \"placeholder\": \"Host label (without any version)\",\n        \"roles\": [\"developer\"]\n    },\n    {\n        \"type\": \"text\",\n        \"key\": \"icon\",\n        \"label\": \"Icon\",\n        \"placeholder\": \"Host icon path template\",\n        \"roles\": [\"developer\"]\n    },\n    {\n        \"type\": \"hosts-enum\",\n        \"key\": \"host_name\",\n        \"label\": \"Host implementation\",\n        \"multiselection\": false,\n        \"use_empty_value\": true,\n        \"roles\": [\"developer\"]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json",
    "content": "[\n    {\n        \"__default_values__\": {\n            \"variant_skip_paths\": null\n        }\n    },\n    {\n        \"type\": \"dict\",\n        \"key\": \"{app_variant}\",\n        \"label\": \"{app_variant_label}\",\n        \"collapsible\": true,\n        \"checkbox_key\": \"enabled\",\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"text\",\n                \"key\": \"variant_label\",\n                \"label\": \"Variant label\",\n                \"placeholder\": \"< {app_variant} >\"\n            },\n            {\n                \"type\": \"schema_template\",\n                \"name\": \"template_host_variant_items\",\n                \"skip_paths\": \"{variant_skip_paths}\"\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant_items.json",
    "content": "[\n    {\n        \"type\": \"boolean\",\n        \"key\": \"use_python_2\",\n        \"label\": \"Use Python 2\",\n        \"default\": false\n    },\n    {\n        \"type\": \"path\",\n        \"key\": \"executables\",\n        \"label\": \"Executables\",\n        \"multiplatform\": true,\n        \"multipath\": true,\n        \"placeholder\": \"Executable path\"\n    },\n    {\n        \"key\": \"separator\",\n        \"type\":\"separator\"\n    },\n    {\n        \"type\": \"dict\",\n        \"key\": \"arguments\",\n        \"label\": \"Arguments\",\n        \"use_label_wrap\": false,\n        \"children\": [\n            {\n                \"key\": \"windows\",\n                \"label\": \"Windows\",\n                \"type\": \"list\",\n                \"object_type\": \"text\"\n            },\n            {\n                \"key\": \"darwin\",\n                \"label\": \"MacOS\",\n                \"type\": \"list\",\n                \"object_type\": \"text\"\n            },\n            {\n                \"key\": \"linux\",\n                \"label\": \"Linux\",\n                \"type\": \"list\",\n                \"object_type\": \"text\"\n            }\n        ]\n    },\n    {\n        \"key\": \"environment\",\n        \"label\": \"Environment\",\n        \"type\": \"raw-json\"\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/host_settings/template_nuke.json",
    "content": "[\n    {\n        \"type\": \"dict\",\n        \"key\": \"{nuke_type}\",\n        \"label\": \"Foundry {nuke_label}\",\n        \"collapsible\": true,\n        \"checkbox_key\": \"enabled\",\n        \"children\": [\n            {\n                \"type\": \"boolean\",\n                \"key\": \"enabled\",\n                \"label\": \"Enabled\"\n            },\n            {\n                \"type\": \"schema_template\",\n                \"name\": \"template_host_unchangables\"\n            },\n            {\n                \"key\": \"environment\",\n                \"label\": \"Environment\",\n                \"type\": \"raw-json\"\n            },\n            {\n                \"type\": \"dict-modifiable\",\n                \"key\": \"variants\",\n                \"collapsible_key\": true,\n                \"use_label_wrap\": false,\n                \"object_type\": {\n                    \"type\": \"dict\",\n                    \"collapsible\": true,\n                    \"children\": [\n                        {\n                            \"type\": \"schema_template\",\n                            \"name\": \"template_host_variant_items\"\n                        }\n                    ]\n                }\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"ftrack\",\n    \"label\": \"Ftrack\",\n    \"collapsible\": true,\n    \"require_restart\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"text\",\n            \"key\": \"ftrack_server\",\n            \"label\": \"Server\"\n        },\n        {\n            \"type\": \"splitter\"\n        },\n        {\n            \"type\": \"label\",\n            \"label\": \"Additional Ftrack event handlers paths\"\n        },\n        {\n            \"type\": \"path\",\n            \"key\": \"ftrack_actions_path\",\n            \"label\": \"User paths\",\n            \"use_label_wrap\": true,\n            \"multipath\": true,\n            \"multiplatform\": true\n        },\n        {\n            \"type\": \"path\",\n            \"key\": \"ftrack_events_path\",\n            \"label\": \"Server paths\",\n            \"use_label_wrap\": true,\n            \"multipath\": true,\n            \"multiplatform\": true\n        },\n        {\n            \"type\": \"separator\"\n        },\n        {\n            \"key\": \"intent\",\n            \"type\": \"dict\",\n            \"label\": \"Intent\",\n            \"collapsible_key\": true,\n            \"is_group\": true,\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"allow_empty_intent\",\n                    \"label\": \"Allow empty intent\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"empty_intent_label\",\n                    \"label\": \"Empty item label\",\n                    \"placeholder\": \"< Not set >\"\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"object_type\": \"text\",\n                    \"key\": \"items\"\n                },\n                {\n                    \"type\": \"separator\"\n                },\n                {\n                    \"key\": \"default\",\n                    \"type\": \"text\",\n                    \"label\": \"Default Intent\",\n                    \"placeholder\": \"< First available >\"\n                },\n                {\n                    \"type\": \"separator\"\n                }\n            ]\n        },\n        {\n            \"key\": \"custom_attributes\",\n            \"label\": \"Custom Attributes\",\n            \"type\": \"dict\",\n            \"children\": [\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"show\",\n                    \"label\": \"Project Custom attributes\",\n                    \"children\": [\n                        {\n                            \"type\": \"schema_template\",\n                            \"name\": \"template_custom_attribute\",\n                            \"template_data\": [\n                                {\n                                    \"key\": \"avalon_auto_sync\"\n                                },\n                                {\n                                    \"key\": \"library_project\"\n                                },\n                                {\n                                    \"key\": \"applications\"\n                                }\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"type\": \"dict\",\n                    \"key\": \"is_hierarchical\",\n                    \"label\": \"Hierarchical Attributes\",\n                    \"children\": [\n                        {\n                            \"type\": \"schema_template\",\n                            \"name\": \"template_custom_attribute\",\n                            \"template_data\": [\n                                {\n                                    \"key\": \"tools_env\"\n                                },\n                                {\n                                    \"key\": \"avalon_mongo_id\"\n                                },\n                                {\n                                    \"key\": \"fps\"\n                                },\n                                {\n                                    \"key\": \"frameStart\"\n                                },\n                                {\n                                    \"key\": \"frameEnd\"\n                                },\n                                {\n                                    \"key\": \"clipIn\"\n                                },\n                                {\n                                    \"key\": \"clipOut\"\n                                },\n                                {\n                                    \"key\": \"handleStart\"\n                                },\n                                {\n                                    \"key\": \"handleEnd\"\n                                },\n                                {\n                                    \"key\": \"resolutionWidth\"\n                                },\n                                {\n                                    \"key\": \"resolutionHeight\"\n                                },\n                                {\n                                    \"key\": \"pixelAspect\"\n                                }\n                            ]\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"kitsu\",\n    \"label\": \"Kitsu\",\n    \"collapsible\": true,\n    \"require_restart\": true,\n    \"checkbox_key\": \"enabled\",\n    \"children\": [\n        {\n            \"type\": \"boolean\",\n            \"key\": \"enabled\",\n            \"label\": \"Enabled\"\n        },\n        {\n            \"type\": \"text\",\n            \"key\": \"server\",\n            \"label\": \"Server\"\n        },\n        {\n            \"type\": \"splitter\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/module_settings/template_custom_attribute.json",
    "content": "[\n    {\n        \"key\": \"{key}\",\n        \"label\": \"{key}\",\n        \"type\": \"dict\",\n        \"children\": [\n            {\n                \"key\": \"write_security_roles\",\n                \"label\": \"Write roles\",\n                \"type\": \"list\",\n                \"object_type\": \"text\"\n            },\n            {\n                \"key\": \"read_security_roles\",\n                \"label\": \"Read roles\",\n                \"type\": \"list\",\n                \"object_type\": \"text\"\n            }\n        ]\n    }\n]\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/schema_applications.json",
    "content": "{\n  \"key\": \"applications\",\n  \"type\": \"dict\",\n  \"label\": \"Applications\",\n  \"collapsible\": true,\n  \"is_file\": true,\n  \"children\": [\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_maya\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_mayapy\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_3dsmax\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_flame\"\n    },\n    {\n      \"type\": \"schema_template\",\n      \"name\": \"template_nuke\",\n      \"template_data\": {\n        \"nuke_type\": \"nuke\",\n        \"nuke_label\": \"Nuke\"\n      }\n    },\n    {\n      \"type\": \"schema_template\",\n      \"name\": \"template_nuke\",\n      \"template_data\": {\n        \"nuke_type\": \"nukeassist\",\n        \"nuke_label\": \"Nuke Assist\"\n      }\n    },\n    {\n      \"type\": \"schema_template\",\n      \"name\": \"template_nuke\",\n      \"template_data\": {\n        \"nuke_type\": \"nukex\",\n        \"nuke_label\": \"Nuke X\"\n      }\n    },\n    {\n      \"type\": \"schema_template\",\n      \"name\": \"template_nuke\",\n      \"template_data\": {\n        \"nuke_type\": \"nukestudio\",\n        \"nuke_label\": \"Nuke Studio\"\n      }\n    },\n    {\n      \"type\": \"schema_template\",\n      \"name\": \"template_nuke\",\n      \"template_data\": {\n        \"nuke_type\": \"hiero\",\n        \"nuke_label\": \"Hiero\"\n      }\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_fusion\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_resolve\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_houdini\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_blender\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_harmony\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_tvpaint\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_photoshop\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_aftereffects\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_celaction\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_substancepainter\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_unreal\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_djv\"\n    },\n    {\n      \"type\": \"schema\",\n      \"name\": \"schema_3dequalizer\"\n    },\n    {\n      \"type\": \"dict-modifiable\",\n      \"key\": \"additional_apps\",\n      \"label\": \"Additional\",\n      \"collapsible\": true,\n      \"collapsible_key\": true,\n      \"object_type\": {\n          \"type\": \"dict\",\n          \"children\": [\n              {\n                  \"type\": \"boolean\",\n                  \"key\": \"enabled\",\n                  \"label\": \"Enabled\"\n              },\n              {\n                  \"type\": \"schema_template\",\n                  \"name\": \"template_host_unchangables\",\n                  \"skip_paths\": [\"host_name\", \"label\"]\n              },\n              {\n                  \"key\": \"environment\",\n                  \"label\": \"Environment\",\n                  \"type\": \"raw-json\"\n              },\n              {\n                  \"type\": \"dict-modifiable\",\n                  \"key\": \"variants\",\n                  \"collapsible_key\": true,\n                  \"use_label_wrap\": false,\n                  \"object_type\": {\n                      \"type\": \"dict\",\n                      \"collapsible\": true,\n                      \"children\": [\n                          {\n                              \"type\": \"schema_template\",\n                              \"name\": \"template_host_variant_items\"\n                          }\n                      ]\n                  }\n              }\n          ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/schema_general.json",
    "content": "{\n    \"key\": \"general\",\n    \"type\": \"dict\",\n    \"label\": \"General\",\n    \"collapsible\": true,\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"key\": \"studio_name\",\n            \"type\": \"text\",\n            \"label\": \"Studio Name\"\n        },\n        {\n            \"key\": \"studio_code\",\n            \"type\": \"text\",\n            \"label\": \"Studio Short Code\"\n        },\n        {\n            \"type\": \"splitter\"\n        },\n        {\n            \"type\": \"label\",\n            \"label\": \"This is <b>NOT a securely stored password!</b> It only acts as a simple barrier to stop users from accessing studio wide settings.\"\n        },\n        {\n            \"type\": \"text\",\n            \"key\": \"admin_password\",\n            \"label\": \"Admin password\"\n        },\n        {\n            \"type\": \"splitter\"\n        },\n        {\n            \"key\": \"environment\",\n            \"label\": \"Environment\",\n            \"type\": \"raw-json\",\n            \"require_restart\": true\n        },\n        {\n            \"type\": \"splitter\"\n        },\n        {\n            \"type\": \"boolean\",\n            \"key\": \"log_to_server\",\n            \"label\": \"Log to mongo\"\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"disk_mapping\",\n            \"label\": \"Disk mapping\",\n            \"is_group\": true,\n            \"use_label_wrap\": false,\n            \"collapsible\": false,\n            \"children\": [\n                {\n                    \"key\": \"windows\",\n                    \"label\": \"Windows\",\n                    \"type\": \"list\",\n                    \"object_type\": {\n                        \"type\": \"list-strict\",\n                        \"key\": \"item\",\n                         \"object_types\": [\n                            {\n                                \"label\": \"Source\",\n                                \"type\": \"path\"\n                            },\n                            {\n                                \"label\": \"Destination\",\n                                \"type\": \"path\"\n                            }\n                        ]\n                    }\n                },\n                {\n                    \"key\": \"linux\",\n                    \"label\": \"Linux\",\n                    \"type\": \"list\",\n                    \"object_type\": {\n                        \"type\": \"list-strict\",\n                        \"key\": \"item\",\n                         \"object_types\": [\n                            {\n                                \"label\": \"Source\",\n                                \"type\": \"path\"\n                            },\n                            {\n                                \"label\": \"Destination\",\n                                \"type\": \"path\"\n                            }\n                        ]\n                    }\n                },\n                {\n                    \"key\": \"darwin\",\n                    \"label\": \"MacOS\",\n                    \"type\": \"list\",\n                    \"object_type\": {\n                        \"type\": \"list-strict\",\n                        \"key\": \"item\",\n                         \"object_types\": [\n                            {\n                                \"label\": \"Source\",\n                                \"type\": \"path\"\n                            },\n                            {\n                                \"label\": \"Destination\",\n                                \"type\": \"path\"\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"splitter\"\n        },\n        {\n            \"type\": \"list\",\n            \"key\": \"local_env_white_list\",\n            \"label\": \"Local overrides of environment variable keys\",\n            \"tooltip\": \"Environment variable keys that can be changed per machine using Local settings UI.\\nKey changes are applied only on applications and tools environments.\",\n            \"use_label_wrap\": true,\n            \"object_type\": \"text\"\n        },\n        {\n            \"type\": \"splitter\"\n        },\n        {\n            \"type\": \"collapsible-wrap\",\n            \"label\": \"OpenPype deployment control\",\n            \"collapsible\": true,\n            \"children\": [\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Define location accessible by artist machine to check for zip updates with Openpype code.\"\n                },\n                {\n                    \"type\": \"path\",\n                    \"key\": \"openpype_path\",\n                    \"label\": \"Versions Repository\",\n                    \"multiplatform\": true,\n                    \"multipath\": true,\n                    \"require_restart\": true\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Define custom location for artist machine where to unzip versions of Openpype code. By default it is in user app data folder.\"\n                },\n                {\n                    \"type\": \"path\",\n                    \"key\": \"local_openpype_path\",\n                    \"label\": \"Custom Local Versions Folder\",\n                    \"multiplatform\": true,\n                    \"multipath\": false,\n                    \"require_restart\": true\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Define explicit OpenPype version that should be used. Keep empty to use latest available version.\"\n                },\n                {\n                    \"type\": \"versions-text\",\n                    \"key\": \"production_version\",\n                    \"label\": \"Production version\"\n                },\n                {\n                    \"type\": \"versions-text\",\n                    \"key\": \"staging_version\",\n                    \"label\": \"Staging version\"\n                },\n                {\n                    \"type\": \"splitter\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Trigger validation if running OpenPype is using studio defined version each 'n' <b>minutes</b>. Validation happens in OpenPype tray application.\"\n                },\n                {\n                    \"type\": \"number\",\n                    \"key\": \"version_check_interval\",\n                    \"label\": \"Version check interval\",\n                    \"minimum\": 0\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/schema_main.json",
    "content": "{\n    \"key\": \"system\",\n    \"type\": \"dict\",\n    \"children\": [\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_general\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_modules\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_applications\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_tools\"\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/schema_modules.json",
    "content": "{\n    \"key\": \"modules\",\n    \"type\": \"dict\",\n    \"label\": \"Modules\",\n    \"collapsible\": true,\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"path\",\n            \"key\": \"addon_paths\",\n            \"label\": \"OpenPype AddOn Paths\",\n            \"use_label_wrap\": true,\n            \"multiplatform\": true,\n            \"multipath\": true,\n            \"require_restart\": true\n        },\n        {\n            \"type\": \"separator\"\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"avalon\",\n            \"label\": \"Avalon\",\n            \"collapsible\": true,\n            \"require_restart\": true,\n            \"children\": [\n                {\n                    \"type\": \"number\",\n                    \"key\": \"AVALON_TIMEOUT\",\n                    \"minimum\": 0,\n                    \"label\": \"Avalon Mongo Timeout (ms)\",\n                    \"steps\": 100\n                },\n                {\n                    \"type\": \"path\",\n                    \"label\": \"Thumbnail Storage Location\",\n                    \"key\": \"AVALON_THUMBNAIL_ROOT\",\n                    \"multiplatform\": true,\n                    \"multipath\": false\n                }\n            ]\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_ftrack\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_kitsu\"\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"shotgrid\",\n            \"label\": \"Shotgrid\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"leecher_manager_url\",\n                    \"label\": \"Shotgrid Leecher Manager URL\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"leecher_backend_url\",\n                    \"label\": \"Shotgrid Leecher Backend URL\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"filter_projects_by_login\",\n                    \"label\": \"Filter projects by SG login\"\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"key\": \"shotgrid_settings\",\n                    \"label\": \"Shotgrid Servers\",\n                    \"object_type\": {\n                        \"type\": \"dict\",\n                        \"children\": [\n                            {\n                                \"key\": \"shotgrid_url\",\n                                \"label\": \"Server URL\",\n                                \"type\": \"text\"\n                            },\n                            {\n                                \"key\": \"shotgrid_script_name\",\n                                \"label\": \"Script Name\",\n                                \"type\": \"text\"\n                            },\n                            {\n                                \"key\": \"shotgrid_script_key\",\n                                \"label\": \"Script api key\",\n                                \"type\": \"text\"\n                            }\n                        ]\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"timers_manager\",\n            \"label\": \"Timers Manager\",\n            \"collapsible\": true,\n            \"require_restart\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"auto_stop\",\n                    \"label\": \"Auto stop timer\"\n                },\n                {\n                    \"type\": \"number\",\n                    \"decimal\": 2,\n                    \"key\": \"full_time\",\n                    \"label\": \"Max idle time\"\n                },\n                {\n                    \"type\": \"number\",\n                    \"decimal\": 2,\n                    \"key\": \"message_time\",\n                    \"label\": \"When dialog will show\"\n                },\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"disregard_publishing\",\n                    \"label\": \"Disregard Publishing\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"clockify\",\n            \"label\": \"Clockify\",\n            \"collapsible\": true,\n            \"require_restart\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"workspace_name\",\n                    \"label\": \"Workspace name\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"sync_server\",\n            \"label\": \"Site Sync\",\n            \"collapsible\": true,\n            \"require_restart\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"collapsible\": true,\n                    \"key\": \"sites\",\n                    \"label\": \"Sites\",\n                    \"collapsible_key\": false,\n                    \"object_type\":\n                    {\n                        \"type\": \"sync-server-providers\"\n                    }\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"deadline\",\n            \"label\": \"Deadline\",\n            \"require_restart\": true,\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"object_type\": \"text\",\n                    \"key\": \"deadline_urls\",\n                    \"required_keys\": [\"default\"],\n                    \"label\": \"Deadline Webservice URLs\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"royalrender\",\n            \"label\": \"Royal Render\",\n            \"require_restart\": true,\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                },\n                {\n                    \"type\": \"dict-modifiable\",\n                    \"object_type\": {\n                        \"type\": \"path\",\n                        \"multiplatform\": true\n                    },\n                    \"key\": \"rr_paths\",\n                    \"required_keys\": [\"default\"],\n                    \"label\": \"Royal Render Root Paths\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"log_viewer\",\n            \"label\": \"Logging\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"standalonepublish_tool\",\n            \"label\": \"Standalone Publish\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"project_manager\",\n            \"label\": \"Project Manager (beta)\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"slack\",\n            \"label\": \"Slack Notifications\",\n            \"collapsible\": true,\n            \"require_restart\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                }\n            ]\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"job_queue\",\n            \"label\": \"Job Queue\",\n            \"require_restart\": true,\n            \"collapsible\": true,\n            \"children\": [\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Address of machine where job queue server is running.\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"server_url\",\n                    \"label\": \"Server Rest URL\"\n                },\n                {\n                    \"type\": \"separator\"\n                },\n                {\n                    \"type\": \"label\",\n                    \"label\": \"Jobs root is used as temporary directory for workers where source is copied and render output can be stored.\"\n                },\n                {\n                    \"key\": \"jobs_root\",\n                    \"label\": \"Jobs root\",\n                    \"type\": \"path\",\n                    \"multipath\": false,\n                    \"multiplatform\": true\n                }\n            ]\n        },\n        {\n            \"type\": \"dynamic_schema\",\n            \"name\": \"system_settings/modules\"\n        },\n        {\n            \"type\": \"dict\",\n            \"key\": \"asset_reporter\",\n            \"label\": \"Asset Usage Reporter\",\n            \"collapsible\": true,\n            \"checkbox_key\": \"enabled\",\n            \"children\": [\n                {\n                    \"type\": \"boolean\",\n                    \"key\": \"enabled\",\n                    \"label\": \"Enabled\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/entities/schemas/system_schema/schema_tools.json",
    "content": "{\n    \"type\": \"dict\",\n    \"key\": \"tools\",\n    \"collapsible\": true,\n    \"is_file\": true,\n    \"children\": [\n        {\n            \"type\": \"dict-modifiable\",\n            \"label\": \"Tools\",\n            \"key\": \"tool_groups\",\n            \"collapsible_key\": true,\n            \"object_type\": {\n                \"type\": \"dict\",\n                \"children\": [\n                    {\n                        \"key\": \"environment\",\n                        \"label\": \"Environments\",\n                        \"type\": \"raw-json\"\n                    },\n                    {\n                        \"type\": \"separator\"\n                    },\n                    {\n                        \"type\": \"dict-modifiable\",\n                        \"key\": \"variants\",\n                        \"collapsible_key\": true,\n                        \"object_type\": {\n                            \"type\": \"dict\",\n                            \"children\": [\n                                {\n                                    \"key\": \"host_names\",\n                                    \"label\": \"Hosts\",\n                                    \"type\": \"hosts-enum\",\n                                    \"multiselection\": true\n                                },\n                                {\n                                    \"key\": \"app_variants\",\n                                    \"label\": \"Applications\",\n                                    \"type\": \"apps-enum\",\n                                    \"multiselection\": true,\n                                    \"tooltip\": \"Applications are not \\\"live\\\" and may require to Save and refresh settings UI to update values.\"\n                                },\n                                {\n                                    \"type\": \"separator\"\n                                },\n                                {\n                                    \"key\": \"environment\",\n                                    \"label\": \"Environments\",\n                                    \"type\": \"raw-json\"\n                                }\n                            ]\n                        }\n                    }\n                ]\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/settings/exceptions.py",
    "content": "class SaveSettingsValidation(Exception):\n    pass\n\n\nclass SaveWarningExc(SaveSettingsValidation):\n    def __init__(self, warnings):\n        if isinstance(warnings, str):\n            warnings = [warnings]\n        self.warnings = warnings\n        msg = \" | \".join(warnings)\n        super(SaveWarningExc, self).__init__(msg)\n"
  },
  {
    "path": "openpype/settings/handlers.py",
    "content": "import os\nimport json\nimport copy\nimport collections\nimport datetime\nfrom abc import ABCMeta, abstractmethod\nimport six\n\nimport openpype.version\nfrom openpype.client.mongo import (\n    OpenPypeMongoConnection,\n    get_project_connection,\n)\nfrom openpype.client.entities import get_project\nfrom openpype.lib.pype_info import get_workstation_info\n\n\nfrom .constants import (\n    GLOBAL_SETTINGS_KEY,\n    SYSTEM_SETTINGS_KEY,\n    PROJECT_SETTINGS_KEY,\n    PROJECT_ANATOMY_KEY,\n    LOCAL_SETTING_KEY,\n    M_OVERRIDDEN_KEY,\n\n    LEGACY_SETTINGS_VERSION\n)\n\n\nclass SettingsStateInfo:\n    \"\"\"Helper state information about some settings state.\n\n    Is used to hold information about last saved and last opened UI. Keep\n    information about the time when that happened and on which machine under\n    which user and on which openpype version.\n\n    To create currrent machine and time information use 'create_new' method.\n    \"\"\"\n\n    timestamp_format = \"%Y-%m-%d %H:%M:%S.%f\"\n\n    def __init__(\n        self,\n        openpype_version,\n        settings_type,\n        project_name,\n        timestamp,\n        hostname,\n        hostip,\n        username,\n        system_name,\n        local_id\n    ):\n        self.openpype_version = openpype_version\n        self.settings_type = settings_type\n        self.project_name = project_name\n\n        timestamp_obj = None\n        if timestamp:\n            timestamp_obj = datetime.datetime.strptime(\n                timestamp, self.timestamp_format\n            )\n        self.timestamp = timestamp\n        self.timestamp_obj = timestamp_obj\n        self.hostname = hostname\n        self.hostip = hostip\n        self.username = username\n        self.system_name = system_name\n        self.local_id = local_id\n\n    def copy(self):\n        return self.from_data(self.to_data())\n\n    @classmethod\n    def create_new(\n        cls, openpype_version, settings_type=None, project_name=None\n    ):\n        \"\"\"Create information about this machine for current time.\"\"\"\n\n        from openpype.lib.pype_info import get_workstation_info\n\n        now = datetime.datetime.now()\n        workstation_info = get_workstation_info()\n\n        return cls(\n            openpype_version,\n            settings_type,\n            project_name,\n            now.strftime(cls.timestamp_format),\n            workstation_info[\"hostname\"],\n            workstation_info[\"hostip\"],\n            workstation_info[\"username\"],\n            workstation_info[\"system_name\"],\n            workstation_info[\"local_id\"]\n        )\n\n    @classmethod\n    def from_data(cls, data):\n        \"\"\"Create object from data.\"\"\"\n\n        return cls(\n            data[\"openpype_version\"],\n            data[\"settings_type\"],\n            data[\"project_name\"],\n            data[\"timestamp\"],\n            data[\"hostname\"],\n            data[\"hostip\"],\n            data[\"username\"],\n            data[\"system_name\"],\n            data[\"local_id\"]\n        )\n\n    def to_data(self):\n        data = self.to_document_data()\n        data.update({\n            \"openpype_version\": self.openpype_version,\n            \"settings_type\": self.settings_type,\n            \"project_name\": self.project_name\n        })\n        return data\n\n    @classmethod\n    def create_new_empty(cls, openpype_version, settings_type=None):\n        return cls(\n            openpype_version,\n            settings_type,\n            None,\n            None,\n            None,\n            None,\n            None,\n            None,\n            None\n        )\n\n    @classmethod\n    def from_document(cls, openpype_version, settings_type, document):\n        document = document or {}\n        project_name = document.get(\"project_name\")\n        last_saved_info = document.get(\"last_saved_info\")\n        if last_saved_info:\n            copy_last_saved_info = copy.deepcopy(last_saved_info)\n            copy_last_saved_info.update({\n                \"openpype_version\": openpype_version,\n                \"settings_type\": settings_type,\n                \"project_name\": project_name,\n            })\n            return cls.from_data(copy_last_saved_info)\n        return cls(\n            openpype_version,\n            settings_type,\n            project_name,\n            None,\n            None,\n            None,\n            None,\n            None,\n            None\n        )\n\n    def to_document_data(self):\n        return {\n            \"timestamp\": self.timestamp,\n            \"hostname\": self.hostname,\n            \"hostip\": self.hostip,\n            \"username\": self.username,\n            \"system_name\": self.system_name,\n            \"local_id\": self.local_id,\n        }\n\n    def __eq__(self, other):\n        if not isinstance(other, SettingsStateInfo):\n            return False\n\n        if other.timestamp_obj != self.timestamp_obj:\n            return False\n\n        return (\n            self.openpype_version == other.openpype_version\n            and self.hostname == other.hostname\n            and self.hostip == other.hostip\n            and self.username == other.username\n            and self.system_name == other.system_name\n            and self.local_id == other.local_id\n        )\n\n\n@six.add_metaclass(ABCMeta)\nclass SettingsHandler(object):\n    global_keys = {\n        \"openpype_path\",\n        \"local_openpype_path\",\n        \"admin_password\",\n        \"log_to_server\",\n        \"disk_mapping\",\n        \"production_version\",\n        \"staging_version\"\n    }\n\n    @abstractmethod\n    def save_studio_settings(self, data):\n        \"\"\"Save studio overrides of system settings.\n\n        Do not use to store whole system settings data with defaults but only\n        it's overrides with metadata defining how overrides should be applied\n        in load function. For loading should be used function\n        `studio_system_settings`.\n\n        Args:\n            data(dict): Data of studio overrides with override metadata.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def save_project_settings(self, project_name, overrides):\n        \"\"\"Save studio overrides of project settings.\n\n        Data are saved for specific project or as defaults for all projects.\n\n        Do not use to store whole project settings data with defaults but only\n        it's overrides with metadata defining how overrides should be applied\n        in load function. For loading should be used function\n        `get_studio_project_settings_overrides` for global project settings\n        and `get_project_settings_overrides` for project specific settings.\n\n        Args:\n            project_name(str, null): Project name for which overrides are\n                or None for global settings.\n            data(dict): Data of project overrides with override metadata.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def save_project_anatomy(self, project_name, anatomy_data):\n        \"\"\"Save studio overrides of project anatomy data.\n\n        Args:\n            project_name(str, null): Project name for which overrides are\n                or None for global settings.\n            data(dict): Data of project overrides with override metadata.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def save_change_log(self, project_name, changes, settings_type):\n        \"\"\"Stores changes to settings to separate logging collection.\n\n        Args:\n            project_name(str, null): Project name for which overrides are\n                or None for global settings.\n            changes(dict): Data of project overrides with override metadata.\n            settings_type (str): system|project|anatomy\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_studio_system_settings_overrides(self, return_version):\n        \"\"\"Studio overrides of system settings.\"\"\"\n        pass\n\n    @abstractmethod\n    def get_studio_project_settings_overrides(self, return_version):\n        \"\"\"Studio overrides of default project settings.\"\"\"\n        pass\n\n    @abstractmethod\n    def get_studio_project_anatomy_overrides(self, return_version):\n        \"\"\"Studio overrides of default project anatomy data.\"\"\"\n        pass\n\n    @abstractmethod\n    def get_project_settings_overrides(self, project_name, return_version):\n        \"\"\"Studio overrides of project settings for specific project.\n\n        Args:\n            project_name(str): Name of project for which data should be loaded.\n            return_version(bool): Version string will be added to output.\n\n        Returns:\n            dict: Only overrides for entered project, may be empty dictionary.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_project_anatomy_overrides(self, project_name, return_version):\n        \"\"\"Studio overrides of project anatomy for specific project.\n\n        Args:\n            project_name(str): Name of project for which data should be loaded.\n            return_version(bool): Version string will be added to output.\n\n        Returns:\n            dict: Only overrides for entered project, may be empty dictionary.\n        \"\"\"\n        pass\n\n    # Getters for specific version overrides\n    @abstractmethod\n    def get_studio_system_settings_overrides_for_version(self, version):\n        \"\"\"Studio system settings overrides for specific version.\n\n        Args:\n            version(str): OpenPype version for which settings should be\n                returned.\n\n        Returns:\n            None: If the version does not have system settings overrides.\n            dict: Document with overrides data.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_studio_project_anatomy_overrides_for_version(self, version):\n        \"\"\"Studio project anatomy overrides for specific version.\n\n        Args:\n            version(str): OpenPype version for which settings should be\n                returned.\n\n        Returns:\n            None: If the version does not have system settings overrides.\n            dict: Document with overrides data.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_studio_project_settings_overrides_for_version(self, version):\n        \"\"\"Studio project settings overrides for specific version.\n\n        Args:\n            version(str): OpenPype version for which settings should be\n                returned.\n\n        Returns:\n            None: If the version does not have system settings overrides.\n            dict: Document with overrides data.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_project_settings_overrides_for_version(\n        self, project_name, version\n    ):\n        \"\"\"Studio project settings overrides for specific project and version.\n\n        Args:\n            project_name(str): Name of project for which the overrides should\n                be loaded.\n            version(str): OpenPype version for which settings should be\n                returned.\n\n        Returns:\n            None: If the version does not have system settings overrides.\n            dict: Document with overrides data.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_global_settings(self):\n        \"\"\"Studio global settings available across versions.\n\n        Output must contain all keys from 'global_keys'. If value is not set\n        the output value should be 'None'.\n\n        Returns:\n            Dict[str, Any]: Global settings same across versions.\n        \"\"\"\n\n        pass\n\n    # Clear methods - per version\n    # - clearing may be helpfull when a version settings were created for\n    #   testing purposes\n    @abstractmethod\n    def clear_studio_system_settings_overrides_for_version(self, version):\n        \"\"\"Remove studio system settings overrides for specific version.\n\n        If version is not available then skip processing.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def clear_studio_project_settings_overrides_for_version(self, version):\n        \"\"\"Remove studio project settings overrides for specific version.\n\n        If version is not available then skip processing.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def clear_studio_project_anatomy_overrides_for_version(self, version):\n        \"\"\"Remove studio project anatomy overrides for specific version.\n\n        If version is not available then skip processing.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def clear_project_settings_overrides_for_version(\n        self, version, project_name\n    ):\n        \"\"\"Remove studio project settings overrides for project and version.\n\n        If version is not available then skip processing.\n        \"\"\"\n        pass\n\n    # Get versions that are available for each type of settings\n    @abstractmethod\n    def get_available_studio_system_settings_overrides_versions(\n        self, sorted=None\n    ):\n        \"\"\"OpenPype versions that have any studio system settings overrides.\n\n        Returns:\n            list<str>: OpenPype versions strings.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_available_studio_project_anatomy_overrides_versions(\n        self, sorted=None\n    ):\n        \"\"\"OpenPype versions that have any studio project anatomy overrides.\n\n        Returns:\n            List[str]: OpenPype versions strings.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_available_studio_project_settings_overrides_versions(\n        self, sorted=None\n    ):\n        \"\"\"OpenPype versions that have any studio project settings overrides.\n\n        Returns:\n            List[str]: OpenPype versions strings.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_available_project_settings_overrides_versions(\n        self, project_name, sorted=None\n    ):\n        \"\"\"OpenPype versions that have any project settings overrides.\n\n        Args:\n            project_name(str): Name of project.\n\n        Returns:\n            List[str]: OpenPype versions strings.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_system_last_saved_info(self):\n        \"\"\"State of last system settings overrides at the moment when called.\n\n        This method must provide most recent data so using cached data is not\n        the way.\n\n        Returns:\n            SettingsStateInfo: Information about system settings overrides.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_project_last_saved_info(self, project_name):\n        \"\"\"State of last project settings overrides at the moment when called.\n\n        This method must provide most recent data so using cached data is not\n        the way.\n\n        Args:\n            project_name (Union[None, str]): Project name for which state\n                should be returned.\n\n        Returns:\n            SettingsStateInfo: Information about project settings overrides.\n        \"\"\"\n\n        pass\n\n    # UI related calls\n    @abstractmethod\n    def get_last_opened_info(self):\n        \"\"\"Get information about last opened UI.\n\n        Last opened UI is empty if there is noone who would have opened UI at\n        the moment when called.\n\n        Returns:\n            Union[None, SettingsStateInfo]: Information about machine who had\n                opened Settings UI.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def opened_settings_ui(self):\n        \"\"\"Callback called when settings UI is opened.\n\n        Information about this machine must be available when\n        'get_last_opened_info' is called from anywhere until\n        'closed_settings_ui' is called again.\n\n        Returns:\n            SettingsStateInfo: Object representing information about this\n                machine. Must be passed to 'closed_settings_ui' when finished.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def closed_settings_ui(self, info_obj):\n        \"\"\"Callback called when settings UI is closed.\n\n        From the moment this method is called the information about this\n        machine is removed and no more available when 'get_last_opened_info'\n        is called.\n\n        Callback should validate if this machine is still stored as opened ui\n        before changing any value.\n\n        Args:\n            info_obj (SettingsStateInfo): Object created when\n                'opened_settings_ui' was called.\n        \"\"\"\n\n        pass\n\n\n@six.add_metaclass(ABCMeta)\nclass LocalSettingsHandler:\n    \"\"\"Handler that should handle about storing and loading of local settings.\n\n    Local settings are \"workstation\" specific modifications that modify how\n    system and project settings look on the workstation and only there.\n    \"\"\"\n    @abstractmethod\n    def save_local_settings(self, data):\n        \"\"\"Save local data of local settings.\n\n        Args:\n            data(dict): Data of local data with override metadata.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_local_settings(self):\n        \"\"\"Studio overrides of system settings.\"\"\"\n        pass\n\n\nclass CacheValues:\n    cache_lifetime = 10\n\n    def __init__(self):\n        self.data = None\n        self.creation_time = None\n        self.version = None\n        self.last_saved_info = None\n\n    def data_copy(self):\n        if not self.data:\n            return {}\n        return copy.deepcopy(self.data)\n\n    def update_data(self, data, version):\n        self.data = data\n        self.creation_time = datetime.datetime.now()\n        self.version = version\n\n    def update_last_saved_info(self, last_saved_info):\n        self.last_saved_info = last_saved_info\n\n    def update_from_document(self, document, version):\n        data = {}\n        if document:\n            if \"data\" in document:\n                data = document[\"data\"]\n            elif \"value\" in document:\n                value = document[\"value\"]\n                if value:\n                    data = json.loads(value)\n\n        self.data = data\n        self.version = version\n\n    def to_json_string(self):\n        return json.dumps(self.data or {})\n\n    @property\n    def is_outdated(self):\n        if self.creation_time is None:\n            return True\n        delta = (datetime.datetime.now() - self.creation_time).seconds\n        return delta > self.cache_lifetime\n\n    def set_outdated(self):\n        self.create_time = None\n\n\nclass MongoSettingsHandler(SettingsHandler):\n    \"\"\"Settings handler that use mongo for storing and loading of settings.\"\"\"\n    key_suffix = \"_versioned\"\n    _version_order_key = \"versions_order\"\n    _all_versions_keys = \"all_versions\"\n\n    def __init__(self):\n        # Get mongo connection\n        settings_collection = OpenPypeMongoConnection.get_mongo_client()\n\n        self._anatomy_keys = None\n        self._attribute_keys = None\n\n        self._version_order_checked = False\n\n        self._system_settings_key = SYSTEM_SETTINGS_KEY + self.key_suffix\n        self._project_settings_key = PROJECT_SETTINGS_KEY + self.key_suffix\n        self._project_anatomy_key = PROJECT_ANATOMY_KEY + self.key_suffix\n        self._current_version = openpype.version.__version__\n\n        database_name = os.environ[\"OPENPYPE_DATABASE_NAME\"]\n        # TODO modify to not use hardcoded keys\n        collection_name = \"settings\"\n\n        self.settings_collection = settings_collection\n\n        self.database_name = database_name\n        self.collection_name = collection_name\n\n        self.collection = settings_collection[database_name][collection_name]\n\n        self.global_settings_cache = CacheValues()\n        self.system_settings_cache = CacheValues()\n        self.project_settings_cache = collections.defaultdict(CacheValues)\n        self.project_anatomy_cache = collections.defaultdict(CacheValues)\n\n    def _prepare_project_settings_keys(self):\n        from .entities import ProjectSettings\n        # Prepare anatomy keys and attribute keys\n        # NOTE this is cached on first import\n        # - keys may change only on schema change which should not happen\n        #   during production\n        project_settings_root = ProjectSettings(\n            reset=False, change_state=False\n        )\n        anatomy_entity = project_settings_root[\"project_anatomy\"]\n        anatomy_keys = set(anatomy_entity.keys())\n        anatomy_keys.remove(\"attributes\")\n        attribute_keys = set(anatomy_entity[\"attributes\"].keys())\n\n        self._anatomy_keys = anatomy_keys\n        self._attribute_keys = attribute_keys\n\n    @property\n    def anatomy_keys(self):\n        if self._anatomy_keys is None:\n            self._prepare_project_settings_keys()\n        return self._anatomy_keys\n\n    @property\n    def attribute_keys(self):\n        if self._attribute_keys is None:\n            self._prepare_project_settings_keys()\n        return self._attribute_keys\n\n    def get_global_settings_doc(self):\n        if self.global_settings_cache.is_outdated:\n            global_settings_doc = self.collection.find_one({\n                \"type\": GLOBAL_SETTINGS_KEY\n            }) or {}\n            self.global_settings_cache.update_data(global_settings_doc, None)\n        return self.global_settings_cache.data_copy()\n\n    def get_global_settings(self):\n        global_settings_doc = self.get_global_settings_doc()\n        global_settings = global_settings_doc.get(\"data\", {})\n        return {\n            key: global_settings[key]\n            for key in self.global_keys\n            if key in global_settings\n        }\n\n    def _extract_global_settings(self, data):\n        \"\"\"Extract global settings data from system settings overrides.\n\n        This is now limited to \"general\" key in system settings which must be\n        set as group in schemas.\n\n        Returns:\n            dict: Global settings extracted from system settings data.\n        \"\"\"\n        output = {}\n        if \"general\" not in data:\n            return output\n\n        general_data = data[\"general\"]\n\n        # Add predefined keys to global settings if are set\n        for key in self.global_keys:\n            if key not in general_data:\n                continue\n            # Pop key from values\n            output[key] = general_data.pop(key)\n            # Pop key from overridden metadata\n            if (\n                M_OVERRIDDEN_KEY in general_data\n                and key in general_data[M_OVERRIDDEN_KEY]\n            ):\n                general_data[M_OVERRIDDEN_KEY].remove(key)\n        return output\n\n    def _apply_global_settings(\n        self, system_settings_document, globals_document\n    ):\n        \"\"\"Apply global settings data to system settings.\n\n        Applification is skipped if document with global settings is not\n        available or does not have set data in.\n\n        System settings document is \"faked\" like it exists if global document\n        has set values.\n\n        Args:\n            system_settings_document (dict): System settings document from\n                MongoDB.\n            globals_document (dict): Global settings document from MongoDB.\n\n        Returns:\n            Merged document which has applied global settings data.\n        \"\"\"\n        # Skip if globals document is not available\n        if (\n            not globals_document\n            or \"data\" not in globals_document\n            or not globals_document[\"data\"]\n        ):\n            return system_settings_document\n\n        globals_data = globals_document[\"data\"]\n        # Check if data contain any key from predefined keys\n        any_key_found = False\n        if globals_data:\n            for key in self.global_keys:\n                if key in globals_data:\n                    any_key_found = True\n                    break\n\n        # Skip if any key from predefined key was not found in globals\n        if not any_key_found:\n            return system_settings_document\n\n        # \"Fake\" system settings document if document does not exist\n        # - global settings document may exist but system settings not yet\n        if not system_settings_document:\n            system_settings_document = {}\n\n        if \"data\" in system_settings_document:\n            system_settings_data = system_settings_document[\"data\"]\n        else:\n            system_settings_data = {}\n            system_settings_document[\"data\"] = system_settings_data\n\n        if \"general\" in system_settings_data:\n            system_general = system_settings_data[\"general\"]\n        else:\n            system_general = {}\n            system_settings_data[\"general\"] = system_general\n\n        overridden_keys = system_general.get(M_OVERRIDDEN_KEY) or []\n        for key in self.global_keys:\n            if key not in globals_data:\n                continue\n\n            system_general[key] = globals_data[key]\n            if key not in overridden_keys:\n                overridden_keys.append(key)\n\n        if overridden_keys:\n            system_general[M_OVERRIDDEN_KEY] = overridden_keys\n\n        return system_settings_document\n\n    def save_studio_settings(self, data):\n        \"\"\"Save studio overrides of system settings.\n\n        Do not use to store whole system settings data with defaults but only\n        it's overrides with metadata defining how overrides should be applied\n        in load function. For loading should be used function\n        `studio_system_settings`.\n\n        Args:\n            data(dict): Data of studio overrides with override metadata.\n        \"\"\"\n        # Update cache\n        self.system_settings_cache.update_data(data, self._current_version)\n\n        last_saved_info = SettingsStateInfo.create_new(\n            self._current_version,\n            SYSTEM_SETTINGS_KEY\n        )\n        self.system_settings_cache.update_last_saved_info(\n            last_saved_info\n        )\n\n        # Get copy of just updated cache\n        system_settings_data = self.system_settings_cache.data_copy()\n\n        # Extract global settings from system settings\n        global_settings = self._extract_global_settings(\n            system_settings_data\n        )\n        self.global_settings_cache.update_data(\n            global_settings,\n            None\n        )\n\n        system_settings_doc = self.collection.find_one(\n            {\n                \"type\": self._system_settings_key,\n                \"version\": self._current_version\n            },\n            {\"_id\": True}\n        )\n\n        # Store system settings\n        new_system_settings_doc = {\n            \"type\": self._system_settings_key,\n            \"version\": self._current_version,\n            \"data\": system_settings_data,\n            \"last_saved_info\": last_saved_info.to_document_data()\n        }\n        if not system_settings_doc:\n            self.collection.insert_one(new_system_settings_doc)\n        else:\n            self.collection.update_one(\n                {\"_id\": system_settings_doc[\"_id\"]},\n                {\"$set\": new_system_settings_doc}\n            )\n\n        # Store global settings\n        self.collection.replace_one(\n            {\n                \"type\": GLOBAL_SETTINGS_KEY\n            },\n            {\n                \"type\": GLOBAL_SETTINGS_KEY,\n                \"data\": global_settings\n            },\n            upsert=True\n        )\n\n    def save_project_settings(self, project_name, overrides):\n        \"\"\"Save studio overrides of project settings.\n\n        Data are saved for specific project or as defaults for all projects.\n\n        Do not use to store whole project settings data with defaults but only\n        it's overrides with metadata defining how overrides should be applied\n        in load function. For loading should be used function\n        `get_studio_project_settings_overrides` for global project settings\n        and `get_project_settings_overrides` for project specific settings.\n\n        Args:\n            project_name(str, null): Project name for which overrides are\n                or None for global settings.\n            data(dict): Data of project overrides with override metadata.\n        \"\"\"\n        data_cache = self.project_settings_cache[project_name]\n        data_cache.update_data(overrides, self._current_version)\n\n        last_saved_info = SettingsStateInfo.create_new(\n            self._current_version,\n            PROJECT_SETTINGS_KEY,\n            project_name\n        )\n\n        data_cache.update_last_saved_info(last_saved_info)\n\n        self._save_project_data(\n            project_name,\n            self._project_settings_key,\n            data_cache,\n            last_saved_info\n        )\n\n    def save_project_anatomy(self, project_name, anatomy_data):\n        \"\"\"Save studio overrides of project anatomy data.\n\n        Args:\n            project_name(str, null): Project name for which overrides are\n                or None for global settings.\n            data(dict): Data of project overrides with override metadata.\n        \"\"\"\n        data_cache = self.project_anatomy_cache[project_name]\n        data_cache.update_data(anatomy_data, self._current_version)\n\n        if project_name is not None:\n            self._save_project_anatomy_data(project_name, data_cache)\n\n        else:\n            last_saved_info = SettingsStateInfo.create_new(\n                self._current_version,\n                PROJECT_ANATOMY_KEY,\n                project_name\n            )\n            self._save_project_data(\n                project_name,\n                self._project_anatomy_key,\n                data_cache,\n                last_saved_info\n            )\n\n    @classmethod\n    def prepare_mongo_update_dict(cls, in_data):\n        data = {}\n        for key, value in in_data.items():\n            if not isinstance(value, dict):\n                data[key] = value\n                continue\n\n            new_value = cls.prepare_mongo_update_dict(value)\n            for _key, _value in new_value.items():\n                new_key = \".\".join((key, _key))\n                data[new_key] = _value\n\n        return data\n\n    def save_change_log(self, project_name, changes, settings_type):\n        \"\"\"Log all settings changes to separate collection\"\"\"\n        if not changes:\n            return\n\n        if settings_type == \"project\" and not project_name:\n            project_name = \"default\"\n\n        host_info = get_workstation_info()\n\n        document = {\n            \"local_id\": host_info[\"local_id\"],\n            \"username\": host_info[\"username\"],\n            \"hostname\": host_info[\"hostname\"],\n            \"hostip\": host_info[\"hostip\"],\n            \"system_name\": host_info[\"system_name\"],\n            \"date_created\": datetime.datetime.now(),\n            \"project\": project_name,\n            \"settings_type\": settings_type,\n            \"changes\": changes\n        }\n        collection_name = \"settings_log\"\n        collection = (self.settings_collection[self.database_name]\n                                              [collection_name])\n        collection.insert_one(document)\n\n    def _save_project_anatomy_data(self, project_name, data_cache):\n        # Create copy of data as they will be modified during save\n        new_data = data_cache.data_copy()\n\n        # Prepare avalon project document\n        project_doc = get_project(project_name)\n        if not project_doc:\n            raise ValueError((\n                \"Project document of project \\\"{}\\\" does not exists.\"\n                \" Create project first.\"\n            ).format(project_name))\n\n        collection = get_project_connection(project_name)\n        # Project's data\n        update_dict_data = {}\n        project_doc_data = project_doc.get(\"data\") or {}\n        attributes = new_data.pop(\"attributes\")\n        _applications = attributes.pop(\"applications\", None) or []\n        for key, value in attributes.items():\n            if (\n                key in project_doc_data\n                and project_doc_data[key] == value\n            ):\n                continue\n            update_dict_data[key] = value\n\n        update_dict_config = {}\n\n        applications = []\n        for application in _applications:\n            if not application:\n                continue\n            if isinstance(application, six.string_types):\n                applications.append({\"name\": application})\n\n        new_data[\"apps\"] = applications\n\n        for key, value in new_data.items():\n            project_doc_value = project_doc.get(key)\n            if key in project_doc and project_doc_value == value:\n                continue\n            update_dict_config[key] = value\n\n        if not update_dict_data and not update_dict_config:\n            return\n\n        data_changes = self.prepare_mongo_update_dict(update_dict_data)\n\n        # Update dictionary of changes that will be changed in mongo\n        update_dict = {}\n        for key, value in data_changes.items():\n            new_key = \"data.{}\".format(key)\n            update_dict[new_key] = value\n\n        for key, value in update_dict_config.items():\n            new_key = \"config.{}\".format(key)\n            update_dict[new_key] = value\n\n        collection.update_one(\n            {\"type\": \"project\"},\n            {\"$set\": update_dict}\n        )\n\n    def _save_project_data(\n        self, project_name, doc_type, data_cache, last_saved_info\n    ):\n        is_default = bool(project_name is None)\n        query_filter = {\n            \"type\": doc_type,\n            \"is_default\": is_default,\n            \"version\": self._current_version\n        }\n\n        new_project_settings_doc = {\n            \"type\": doc_type,\n            \"data\": data_cache.data,\n            \"is_default\": is_default,\n            \"version\": self._current_version,\n            \"last_saved_info\": last_saved_info.to_data()\n        }\n\n        if not is_default:\n            query_filter[\"project_name\"] = project_name\n            new_project_settings_doc[\"project_name\"] = project_name\n\n        project_settings_doc = self.collection.find_one(\n            query_filter,\n            {\"_id\": True}\n        )\n        if project_settings_doc:\n            self.collection.update_one(\n                {\"_id\": project_settings_doc[\"_id\"]},\n                {\"$set\": new_project_settings_doc}\n            )\n        else:\n            self.collection.insert_one(new_project_settings_doc)\n\n    def _get_versions_order_doc(self, projection=None):\n        # TODO cache\n        return self.collection.find_one(\n            {\"type\": self._version_order_key},\n            projection\n        ) or {}\n\n    def _check_version_order(self):\n        \"\"\"This method will work only in OpenPype process.\n\n        Will create/update mongo document where OpenPype versions are stored\n        in semantic version order.\n\n        This document can be then used to find closes version of settings in\n        processes where 'OpenPypeVersion' is not available.\n        \"\"\"\n        # Do this step only once\n        if self._version_order_checked:\n            return\n        self._version_order_checked = True\n\n        from openpype.lib.openpype_version import get_OpenPypeVersion\n\n        OpenPypeVersion = get_OpenPypeVersion()\n        # Skip if 'OpenPypeVersion' is not available\n        if OpenPypeVersion is None:\n            return\n\n        # Query document holding sorted list of version strings\n        doc = self._get_versions_order_doc()\n        if not doc:\n            doc = {\"type\": self._version_order_key}\n\n        if self._all_versions_keys not in doc:\n            doc[self._all_versions_keys] = []\n\n        # Skip if current version is already available\n        if self._current_version in doc[self._all_versions_keys]:\n            return\n\n        if self._current_version not in doc[self._all_versions_keys]:\n            # Add all versions into list\n            all_objected_versions = [\n                OpenPypeVersion(version=self._current_version)\n            ]\n            for version_str in doc[self._all_versions_keys]:\n                all_objected_versions.append(\n                    OpenPypeVersion(version=version_str)\n                )\n\n            doc[self._all_versions_keys] = [\n                str(version) for version in sorted(all_objected_versions)\n            ]\n\n        self.collection.replace_one(\n            {\"type\": self._version_order_key},\n            doc,\n            upsert=True\n        )\n\n    def find_closest_version_for_projects(self, project_names):\n        output = {\n            project_name: None\n            for project_name in project_names\n        }\n        from openpype.lib.openpype_version import get_OpenPypeVersion\n        OpenPypeVersion = get_OpenPypeVersion()\n        if OpenPypeVersion is None:\n            return output\n\n        versioned_doc = self._get_versions_order_doc()\n\n        settings_ids = []\n        for project_name in project_names:\n            if project_name is None:\n                doc_filter = {\"is_default\": True}\n            else:\n                doc_filter = {\"project_name\": project_name}\n            settings_id = self._find_closest_settings_id(\n                self._project_settings_key,\n                PROJECT_SETTINGS_KEY,\n                doc_filter,\n                versioned_doc\n            )\n            if settings_id:\n                settings_ids.append(settings_id)\n\n        if settings_ids:\n            docs = self.collection.find(\n                {\"_id\": {\"$in\": settings_ids}},\n                {\"version\": True, \"project_name\": True}\n            )\n            for doc in docs:\n                project_name = doc.get(\"project_name\")\n                version = doc.get(\"version\", LEGACY_SETTINGS_VERSION)\n                output[project_name] = version\n        return output\n\n    def _find_closest_settings_id(\n        self, key, legacy_key, additional_filters=None, versioned_doc=None\n    ):\n        \"\"\"Try to find closes available versioned settings for settings key.\n\n        This method should be used only if settings for current OpenPype\n        version are not available.\n\n        Args:\n            key(str): Settings key under which are settings stored (\"type\").\n            legacy_key(str): Settings key under which were stored not versioned\n                settings.\n            additional_filters(dict): Additional filters of document. Used\n                for project specific settings.\n        \"\"\"\n        # Trigger check of versions\n        self._check_version_order()\n\n        doc_filters = {\n            \"type\": {\"$in\": [key, legacy_key]}\n        }\n        if additional_filters:\n            doc_filters.update(additional_filters)\n\n        # Query base data of each settings doc\n        other_versions = self.collection.find(\n            doc_filters,\n            {\n                \"_id\": True,\n                \"version\": True,\n                \"type\": True\n            }\n        )\n        # Query doc with list of sorted versions\n        if versioned_doc is None:\n            versioned_doc = self._get_versions_order_doc()\n\n        # Separate queried docs\n        legacy_settings_doc = None\n        versioned_settings_by_version = {}\n        for doc in other_versions:\n            if doc[\"type\"] == legacy_key:\n                legacy_settings_doc = doc\n            elif doc[\"type\"] == key:\n                if doc[\"version\"] == self._current_version:\n                    return doc[\"_id\"]\n                versioned_settings_by_version[doc[\"version\"]] = doc\n\n        versions_in_doc = versioned_doc.get(self._all_versions_keys) or []\n        # Cases when only legacy settings can be used\n        if (\n            # There are not versioned documents yet\n            not versioned_settings_by_version\n            # Versioned document is not available at all\n            # - this can happen only if old build of OpenPype was used\n            or not versioned_doc\n            # Current OpenPype version is not available\n            # - something went really wrong when this happens\n            or self._current_version not in versions_in_doc\n        ):\n            if not legacy_settings_doc:\n                return None\n            return legacy_settings_doc[\"_id\"]\n\n        # Separate versions to lower and higher and keep their order\n        lower_versions = []\n        higher_versions = []\n        before = True\n        for version_str in versions_in_doc:\n            if version_str == self._current_version:\n                before = False\n            elif before:\n                lower_versions.append(version_str)\n            else:\n                higher_versions.append(version_str)\n\n        # Use legacy settings doc as source document\n        src_doc_id = None\n        if legacy_settings_doc:\n            src_doc_id = legacy_settings_doc[\"_id\"]\n\n        # Find highest version which has available settings\n        if lower_versions:\n            for version_str in reversed(lower_versions):\n                doc = versioned_settings_by_version.get(version_str)\n                if doc:\n                    src_doc_id = doc[\"_id\"]\n                    break\n\n        # Use versions with higher version only if there are not legacy\n        #   settings and there are not any versions before\n        if src_doc_id is None and higher_versions:\n            for version_str in higher_versions:\n                doc = versioned_settings_by_version.get(version_str)\n                if doc:\n                    src_doc_id = doc[\"_id\"]\n                    break\n\n        return src_doc_id\n\n    def _find_closest_settings(\n        self, key, legacy_key, additional_filters=None, versioned_doc=None\n    ):\n        doc_id = self._find_closest_settings_id(\n            key, legacy_key, additional_filters, versioned_doc\n        )\n        if doc_id is None:\n            return None\n        return self.collection.find_one({\"_id\": doc_id})\n\n    def _find_closest_system_settings(self):\n        return self._find_closest_settings(\n            self._system_settings_key,\n            SYSTEM_SETTINGS_KEY\n        )\n\n    def _find_closest_project_settings(self, project_name):\n        if project_name is None:\n            additional_filters = {\"is_default\": True}\n        else:\n            additional_filters = {\"project_name\": project_name}\n\n        return self._find_closest_settings(\n            self._project_settings_key,\n            PROJECT_SETTINGS_KEY,\n            additional_filters\n        )\n\n    def _find_closest_project_anatomy(self):\n        additional_filters = {\"is_default\": True}\n        return self._find_closest_settings(\n            self._project_anatomy_key,\n            PROJECT_ANATOMY_KEY,\n            additional_filters\n        )\n\n    def _get_studio_system_settings_overrides_for_version(self, version=None):\n        # QUESTION cache?\n        if version == LEGACY_SETTINGS_VERSION:\n            return self.collection.find_one({\n                \"type\": SYSTEM_SETTINGS_KEY\n            })\n\n        if version is None:\n            version = self._current_version\n\n        return self.collection.find_one({\n            \"type\": self._system_settings_key,\n            \"version\": version\n        })\n\n    def _get_project_settings_overrides_for_version(\n        self, project_name, version=None\n    ):\n        # QUESTION cache?\n        if version == LEGACY_SETTINGS_VERSION:\n            document_filter = {\n                \"type\": PROJECT_SETTINGS_KEY\n            }\n\n        else:\n            if version is None:\n                version = self._current_version\n\n            document_filter = {\n                \"type\": self._project_settings_key,\n                \"version\": version\n            }\n\n        if project_name is None:\n            document_filter[\"is_default\"] = True\n        else:\n            document_filter[\"project_name\"] = project_name\n        return self.collection.find_one(document_filter)\n\n    def _get_project_anatomy_overrides_for_version(self, version=None):\n        # QUESTION cache?\n        if version == LEGACY_SETTINGS_VERSION:\n            return self.collection.find_one({\n                \"type\": PROJECT_ANATOMY_KEY,\n                \"is_default\": True\n            })\n\n        if version is None:\n            version = self._current_version\n\n        return self.collection.find_one({\n            \"type\": self._project_anatomy_key,\n            \"is_default\": True,\n            \"version\": version\n        })\n\n    def get_studio_system_settings_overrides(self, return_version):\n        \"\"\"Studio overrides of system settings.\"\"\"\n        if self.system_settings_cache.is_outdated:\n            globals_document = self.get_global_settings_doc()\n            document, version = self._get_system_settings_overrides_doc()\n\n            last_saved_info = SettingsStateInfo.from_document(\n                version, SYSTEM_SETTINGS_KEY, document\n            )\n            merged_document = self._apply_global_settings(\n                document, globals_document\n            )\n\n            self.system_settings_cache.update_from_document(\n                merged_document, version\n            )\n            self.system_settings_cache.update_last_saved_info(\n                last_saved_info\n            )\n\n        cache = self.system_settings_cache\n        data = cache.data_copy()\n        if return_version:\n            return data, cache.version\n        return data\n\n    def _get_system_settings_overrides_doc(self):\n        document = (\n            self._get_studio_system_settings_overrides_for_version()\n        )\n        if document is None:\n            document = self._find_closest_system_settings()\n\n        version = None\n        if document:\n            if document[\"type\"] == self._system_settings_key:\n                version = document[\"version\"]\n            else:\n                version = LEGACY_SETTINGS_VERSION\n\n        return document, version\n\n    def get_system_last_saved_info(self):\n        # Make sure settings are recaches\n        self.system_settings_cache.set_outdated()\n        self.get_studio_system_settings_overrides(False)\n\n        return self.system_settings_cache.last_saved_info.copy()\n\n    def _get_project_settings_overrides(self, project_name, return_version):\n        if self.project_settings_cache[project_name].is_outdated:\n            document, version = self._get_project_settings_overrides_doc(\n                project_name\n            )\n            self.project_settings_cache[project_name].update_from_document(\n                document, version\n            )\n            last_saved_info = SettingsStateInfo.from_document(\n                version, PROJECT_SETTINGS_KEY, document\n            )\n            self.project_settings_cache[project_name].update_last_saved_info(\n                last_saved_info\n            )\n\n        cache = self.project_settings_cache[project_name]\n        data = cache.data_copy()\n        if return_version:\n            return data, cache.version\n        return data\n\n    def _get_project_settings_overrides_doc(self, project_name):\n        document = self._get_project_settings_overrides_for_version(\n            project_name\n        )\n        if document is None:\n            document = self._find_closest_project_settings(project_name)\n\n        version = None\n        if document:\n            if document[\"type\"] == self._project_settings_key:\n                version = document[\"version\"]\n            else:\n                version = LEGACY_SETTINGS_VERSION\n\n        return document, version\n\n    def get_project_last_saved_info(self, project_name):\n        # Make sure settings are recaches\n        self.project_settings_cache[project_name].set_outdated()\n        self._get_project_settings_overrides(project_name, False)\n\n        return self.project_settings_cache[project_name].last_saved_info.copy()\n\n    def get_studio_project_settings_overrides(self, return_version):\n        \"\"\"Studio overrides of default project settings.\"\"\"\n        return self._get_project_settings_overrides(None, return_version)\n\n    def get_project_settings_overrides(self, project_name, return_version):\n        \"\"\"Studio overrides of project settings for specific project.\n\n        Args:\n            project_name(str): Name of project for which data should be loaded.\n\n        Returns:\n            dict: Only overrides for entered project, may be empty dictionary.\n        \"\"\"\n        if not project_name:\n            if return_version:\n                return {}, None\n            return {}\n        return self._get_project_settings_overrides(\n            project_name, return_version\n        )\n\n    def project_doc_to_anatomy_data(self, project_doc):\n        \"\"\"Convert project document to anatomy data.\n\n        Probably should fill missing keys and values.\n        \"\"\"\n        if not project_doc:\n            return {}\n\n        attributes = {}\n        project_doc_data = project_doc.get(\"data\") or {}\n        for key in self.attribute_keys:\n            value = project_doc_data.get(key)\n            if value is not None:\n                attributes[key] = value\n\n        project_doc_config = project_doc.get(\"config\") or {}\n\n        app_names = set()\n        if not project_doc_config or \"apps\" not in project_doc_config:\n            set_applications = False\n        else:\n            set_applications = True\n            for app_item in project_doc_config[\"apps\"]:\n                if not app_item:\n                    continue\n                app_name = app_item.get(\"name\")\n                if app_name:\n                    app_names.add(app_name)\n\n        if set_applications:\n            attributes[\"applications\"] = list(app_names)\n\n        output = {\"attributes\": attributes}\n        for key in self.anatomy_keys:\n            value = project_doc_config.get(key)\n            if value is not None:\n                output[key] = value\n\n        return output\n\n    def _get_project_anatomy_overrides(self, project_name, return_version):\n        if self.project_anatomy_cache[project_name].is_outdated:\n            if project_name is None:\n                document = self._get_project_anatomy_overrides_for_version()\n                if document is None:\n                    document = self._find_closest_project_anatomy()\n\n                version = None\n                if document:\n                    if document[\"type\"] == self._project_anatomy_key:\n                        version = document[\"version\"]\n                    else:\n                        version = LEGACY_SETTINGS_VERSION\n                self.project_anatomy_cache[project_name].update_from_document(\n                    document, version\n                )\n\n            else:\n                project_doc = get_project(project_name)\n                self.project_anatomy_cache[project_name].update_data(\n                    self.project_doc_to_anatomy_data(project_doc),\n                    self._current_version\n                )\n\n        cache = self.project_anatomy_cache[project_name]\n        data = cache.data_copy()\n        if return_version:\n            return data, cache.version\n        return data\n\n    def get_studio_project_anatomy_overrides(self, return_version):\n        \"\"\"Studio overrides of default project anatomy data.\"\"\"\n        return self._get_project_anatomy_overrides(None, return_version)\n\n    def get_project_anatomy_overrides(self, project_name):\n        \"\"\"Studio overrides of project anatomy for specific project.\n\n        Args:\n            project_name(str): Name of project for which data should be loaded.\n\n        Returns:\n            dict: Only overrides for entered project, may be empty dictionary.\n        \"\"\"\n        if not project_name:\n            return {}\n        return self._get_project_anatomy_overrides(project_name, False)\n\n    # Implementations of abstract methods to get overrides for version\n    def get_studio_system_settings_overrides_for_version(self, version):\n        doc = self._get_studio_system_settings_overrides_for_version(version)\n        if not doc:\n            return doc\n        return doc[\"data\"]\n\n    def get_studio_project_anatomy_overrides_for_version(self, version):\n        doc = self._get_project_anatomy_overrides_for_version(version)\n        if not doc:\n            return doc\n        return doc[\"data\"]\n\n    def get_studio_project_settings_overrides_for_version(self, version):\n        doc = self._get_project_settings_overrides_for_version(None, version)\n        if not doc:\n            return doc\n        return doc[\"data\"]\n\n    def get_project_settings_overrides_for_version(\n        self, project_name, version\n    ):\n        doc = self._get_project_settings_overrides_for_version(\n            project_name, version\n        )\n        if not doc:\n            return doc\n        return doc[\"data\"]\n\n    # Implementations of abstract methods to clear overrides for version\n    def clear_studio_system_settings_overrides_for_version(self, version):\n        self.collection.delete_one({\n            \"type\": self._system_settings_key,\n            \"version\": version\n        })\n\n    def clear_studio_project_settings_overrides_for_version(self, version):\n        self.collection.delete_one({\n            \"type\": self._project_settings_key,\n            \"version\": version,\n            \"is_default\": True\n        })\n\n    def clear_studio_project_anatomy_overrides_for_version(self, version):\n        self.collection.delete_one({\n            \"type\": self._project_anatomy_key,\n            \"version\": version\n        })\n\n    def clear_project_settings_overrides_for_version(\n        self, version, project_name\n    ):\n        self.collection.delete_one({\n            \"type\": self._project_settings_key,\n            \"version\": version,\n            \"project_name\": project_name\n        })\n\n    def _sort_versions(self, versions):\n        \"\"\"Sort versions.\n\n        WARNING:\n        This method does not handle all possible issues so it should not be\n        used in logic which determine which settings are used. Is used for\n        sorting of available versions.\n        \"\"\"\n        if not versions:\n            return []\n\n        set_versions = set(versions)\n        contain_legacy = LEGACY_SETTINGS_VERSION in set_versions\n        if contain_legacy:\n            set_versions.remove(LEGACY_SETTINGS_VERSION)\n\n        from openpype.lib.openpype_version import get_OpenPypeVersion\n\n        OpenPypeVersion = get_OpenPypeVersion()\n\n        # Skip if 'OpenPypeVersion' is not available\n        if OpenPypeVersion is not None:\n            obj_versions = sorted(\n                [OpenPypeVersion(version=version) for version in set_versions]\n            )\n            sorted_versions = [str(version) for version in obj_versions]\n            if contain_legacy:\n                sorted_versions.insert(0, LEGACY_SETTINGS_VERSION)\n            return sorted_versions\n\n        doc = self._get_versions_order_doc()\n        all_versions = doc.get(self._all_versions_keys)\n        if not all_versions:\n            return list(sorted(versions))\n\n        sorted_versions = []\n        for version in all_versions:\n            if version in set_versions:\n                set_versions.remove(version)\n                sorted_versions.append(version)\n\n        for version in sorted(set_versions):\n            sorted_versions.insert(0, version)\n\n        if contain_legacy:\n            sorted_versions.insert(0, LEGACY_SETTINGS_VERSION)\n        return sorted_versions\n\n    # Get available versions for settings type\n    def get_available_studio_system_settings_overrides_versions(\n        self, sorted=None\n    ):\n        docs = self.collection.find(\n            {\"type\": {\n                \"$in\": [self._system_settings_key, SYSTEM_SETTINGS_KEY]\n            }},\n            {\"type\": True, \"version\": True}\n        )\n        output = set()\n        for doc in docs:\n            if doc[\"type\"] == self._system_settings_key:\n                output.add(doc[\"version\"])\n            else:\n                output.add(LEGACY_SETTINGS_VERSION)\n        if not sorted:\n            return output\n        return self._sort_versions(output)\n\n    def get_available_studio_project_anatomy_overrides_versions(\n        self, sorted=None\n    ):\n        docs = self.collection.find(\n            {\"type\": {\n                \"$in\": [self._project_anatomy_key, PROJECT_ANATOMY_KEY]\n            }},\n            {\"type\": True, \"version\": True}\n        )\n        output = set()\n        for doc in docs:\n            if doc[\"type\"] == self._project_anatomy_key:\n                output.add(doc[\"version\"])\n            else:\n                output.add(LEGACY_SETTINGS_VERSION)\n        if not sorted:\n            return output\n        return self._sort_versions(output)\n\n    def get_available_studio_project_settings_overrides_versions(\n        self, sorted=None\n    ):\n        docs = self.collection.find(\n            {\n                \"is_default\": True,\n                \"type\": {\n                    \"$in\": [self._project_settings_key, PROJECT_SETTINGS_KEY]\n                }\n            },\n            {\"type\": True, \"version\": True}\n        )\n        output = set()\n        for doc in docs:\n            if doc[\"type\"] == self._project_settings_key:\n                output.add(doc[\"version\"])\n            else:\n                output.add(LEGACY_SETTINGS_VERSION)\n        if not sorted:\n            return output\n        return self._sort_versions(output)\n\n    def get_available_project_settings_overrides_versions(\n        self, project_name, sorted=None\n    ):\n        docs = self.collection.find(\n            {\n                \"project_name\": project_name,\n                \"type\": {\n                    \"$in\": [self._project_settings_key, PROJECT_SETTINGS_KEY]\n                }\n            },\n            {\"type\": True, \"version\": True}\n        )\n        output = set()\n        for doc in docs:\n            if doc[\"type\"] == self._project_settings_key:\n                output.add(doc[\"version\"])\n            else:\n                output.add(LEGACY_SETTINGS_VERSION)\n        if not sorted:\n            return output\n        return self._sort_versions(output)\n\n    def get_last_opened_info(self):\n        doc = self.collection.find_one({\n            \"type\": \"last_opened_settings_ui\",\n            \"version\": self._current_version\n        }) or {}\n        info_data = doc.get(\"info\")\n        if not info_data:\n            return None\n\n        # Fill not available information\n        info_data[\"openpype_version\"] = self._current_version\n        info_data[\"settings_type\"] = None\n        info_data[\"project_name\"] = None\n        return SettingsStateInfo.from_data(info_data)\n\n    def opened_settings_ui(self):\n        doc_filter = {\n            \"type\": \"last_opened_settings_ui\",\n            \"version\": self._current_version\n        }\n\n        opened_info = SettingsStateInfo.create_new(self._current_version)\n        new_doc_data = copy.deepcopy(doc_filter)\n        new_doc_data[\"info\"] = opened_info.to_document_data()\n\n        doc = self.collection.find_one(\n            doc_filter,\n            {\"_id\": True}\n        )\n        if doc:\n            self.collection.update_one(\n                {\"_id\": doc[\"_id\"]},\n                {\"$set\": new_doc_data}\n            )\n        else:\n            self.collection.insert_one(new_doc_data)\n        return opened_info\n\n    def closed_settings_ui(self, info_obj):\n        doc_filter = {\n            \"type\": \"last_opened_settings_ui\",\n            \"version\": self._current_version\n        }\n        doc = self.collection.find_one(doc_filter) or {}\n        info_data = doc.get(\"info\")\n        if not info_data:\n            return\n\n        info_data[\"openpype_version\"] = self._current_version\n        info_data[\"settings_type\"] = None\n        info_data[\"project_name\"] = None\n        current_info = SettingsStateInfo.from_data(info_data)\n        if current_info == info_obj:\n            self.collection.update_one(\n                {\"_id\": doc[\"_id\"]},\n                {\"$set\": {\"info\": None}}\n            )\n\n\nclass MongoLocalSettingsHandler(LocalSettingsHandler):\n    \"\"\"Settings handler that use mongo for store and load local settings.\n\n    Data have 2 query criteria. First is key \"type\" stored in constant\n    `LOCAL_SETTING_KEY`. Second is key \"site_id\" which value can be obstained\n    with `get_local_site_id` function.\n    \"\"\"\n\n    def __init__(self, local_site_id=None):\n        # Get mongo connection\n        from openpype.lib import get_local_site_id\n\n        if local_site_id is None:\n            local_site_id = get_local_site_id()\n        settings_collection = OpenPypeMongoConnection.get_mongo_client()\n\n        # TODO prepare version of pype\n        # - pype version should define how are settings saved and loaded\n\n        database_name = os.environ[\"OPENPYPE_DATABASE_NAME\"]\n        # TODO modify to not use hardcoded keys\n        collection_name = \"settings\"\n\n        self.settings_collection = settings_collection\n\n        self.database_name = database_name\n        self.collection_name = collection_name\n\n        self.collection = settings_collection[database_name][collection_name]\n\n        self.local_site_id = local_site_id\n\n        self.local_settings_cache = CacheValues()\n\n    def save_local_settings(self, data):\n        \"\"\"Save local settings.\n\n        Args:\n            data(dict): Data of studio overrides with override metadata.\n        \"\"\"\n        data = data or {}\n\n        self.local_settings_cache.update_data(data, None)\n\n        self.collection.replace_one(\n            {\n                \"type\": LOCAL_SETTING_KEY,\n                \"site_id\": self.local_site_id\n            },\n            {\n                \"type\": LOCAL_SETTING_KEY,\n                \"site_id\": self.local_site_id,\n                \"data\": self.local_settings_cache.data\n            },\n            upsert=True\n        )\n\n    def get_local_settings(self):\n        \"\"\"Local settings for local site id.\"\"\"\n        if self.local_settings_cache.is_outdated:\n            document = self.collection.find_one({\n                \"type\": LOCAL_SETTING_KEY,\n                \"site_id\": self.local_site_id\n            })\n\n            self.local_settings_cache.update_from_document(document, None)\n\n        return self.local_settings_cache.data_copy()\n"
  },
  {
    "path": "openpype/settings/lib.py",
    "content": "import os\nimport json\nimport functools\nimport logging\nimport platform\nimport copy\n\nfrom openpype import AYON_SERVER_ENABLED\n\nfrom .exceptions import (\n    SaveWarningExc\n)\nfrom .constants import (\n    M_OVERRIDDEN_KEY,\n\n    METADATA_KEYS,\n\n    SYSTEM_SETTINGS_KEY,\n    PROJECT_SETTINGS_KEY,\n    PROJECT_ANATOMY_KEY,\n    DEFAULT_PROJECT_KEY\n)\n\nfrom .ayon_settings import (\n    get_ayon_project_settings,\n    get_ayon_system_settings\n)\n\nlog = logging.getLogger(__name__)\n\n# Py2 + Py3 json decode exception\nJSON_EXC = getattr(json.decoder, \"JSONDecodeError\", ValueError)\n\n\n# Path to default settings\nDEFAULTS_DIR = os.path.join(\n    os.path.dirname(os.path.abspath(__file__)),\n    \"defaults\"\n)\n\n# Variable where cache of default settings are stored\n_DEFAULT_SETTINGS = None\n\n# Handler of studio overrides\n_SETTINGS_HANDLER = None\n\n# Handler of local settings\n_LOCAL_SETTINGS_HANDLER = None\n\n\ndef clear_metadata_from_settings(values):\n    \"\"\"Remove all metadata keys from loaded settings.\"\"\"\n    if isinstance(values, dict):\n        for key in tuple(values.keys()):\n            if key in METADATA_KEYS:\n                values.pop(key)\n            else:\n                clear_metadata_from_settings(values[key])\n    elif isinstance(values, list):\n        for item in values:\n            clear_metadata_from_settings(item)\n\n\ndef calculate_changes(old_value, new_value):\n    changes = {}\n    for key, value in new_value.items():\n        if key not in old_value:\n            changes[key] = value\n            continue\n\n        _value = old_value[key]\n        if isinstance(value, dict) and isinstance(_value, dict):\n            _changes = calculate_changes(_value, value)\n            if _changes:\n                changes[key] = _changes\n            continue\n\n        if _value != value:\n            changes[key] = value\n    return changes\n\n\ndef create_settings_handler():\n    if AYON_SERVER_ENABLED:\n        raise RuntimeError(\"Mongo settings handler was triggered in AYON mode\")\n    from .handlers import MongoSettingsHandler\n    # Handler can't be created in global space on initialization but only when\n    # needed. Plus here may be logic: Which handler is used (in future).\n    return MongoSettingsHandler()\n\n\ndef create_local_settings_handler():\n    if AYON_SERVER_ENABLED:\n        raise RuntimeError(\"Mongo settings handler was triggered in AYON mode\")\n    from .handlers import MongoLocalSettingsHandler\n    return MongoLocalSettingsHandler()\n\n\ndef require_handler(func):\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs):\n        global _SETTINGS_HANDLER\n        if _SETTINGS_HANDLER is None:\n            _SETTINGS_HANDLER = create_settings_handler()\n        return func(*args, **kwargs)\n    return wrapper\n\n\ndef require_local_handler(func):\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs):\n        global _LOCAL_SETTINGS_HANDLER\n        if _LOCAL_SETTINGS_HANDLER is None:\n            _LOCAL_SETTINGS_HANDLER = create_local_settings_handler()\n        return func(*args, **kwargs)\n    return wrapper\n\n\n@require_handler\ndef get_system_last_saved_info():\n    return _SETTINGS_HANDLER.get_system_last_saved_info()\n\n\n@require_handler\ndef get_project_last_saved_info(project_name):\n    return _SETTINGS_HANDLER.get_project_last_saved_info(project_name)\n\n\n@require_handler\ndef get_last_opened_info():\n    return _SETTINGS_HANDLER.get_last_opened_info()\n\n\n@require_handler\ndef opened_settings_ui():\n    return _SETTINGS_HANDLER.opened_settings_ui()\n\n\n@require_handler\ndef closed_settings_ui(info_obj):\n    return _SETTINGS_HANDLER.closed_settings_ui(info_obj)\n\n\n@require_handler\ndef save_studio_settings(data):\n    \"\"\"Save studio overrides of system settings.\n\n    Triggers callbacks on modules that want to know about system settings\n    changes.\n\n    Callbacks are triggered on all modules. They must check if their enabled\n    value has changed.\n\n    For saving of data cares registered Settings handler.\n\n    Warning messages are not logged as module raising them should log it within\n    it's logger.\n\n    Args:\n        data(dict): Overrides data with metadata defying studio overrides.\n\n    Raises:\n        SaveWarningExc: If any module raises the exception.\n    \"\"\"\n    # Notify Pype modules\n    from openpype.modules import ModulesManager, ISettingsChangeListener\n\n    old_data = get_system_settings()\n    default_values = get_default_settings()[SYSTEM_SETTINGS_KEY]\n    new_data = apply_overrides(default_values, copy.deepcopy(data))\n    new_data_with_metadata = copy.deepcopy(new_data)\n    clear_metadata_from_settings(new_data)\n\n    changes = calculate_changes(old_data, new_data)\n    modules_manager = ModulesManager(new_data)\n\n    warnings = []\n    for module in modules_manager.get_enabled_modules():\n        if isinstance(module, ISettingsChangeListener):\n            try:\n                module.on_system_settings_save(\n                    old_data, new_data, changes, new_data_with_metadata\n                )\n            except SaveWarningExc as exc:\n                warnings.extend(exc.warnings)\n\n    _SETTINGS_HANDLER.save_change_log(None, changes, \"system\")\n    _SETTINGS_HANDLER.save_studio_settings(data)\n    if warnings:\n        raise SaveWarningExc(warnings)\n\n\n@require_handler\ndef save_project_settings(project_name, overrides):\n    \"\"\"Save studio overrides of project settings.\n\n    Old value, new value and changes are passed to enabled modules that want to\n    know about settings changes.\n\n    For saving of data cares registered Settings handler.\n\n    Warning messages are not logged as module raising them should log it within\n    it's logger.\n\n    Args:\n        project_name (str): Project name for which overrides are passed.\n            Default project's value is None.\n        overrides(dict): Overrides data with metadata defying studio overrides.\n\n    Raises:\n        SaveWarningExc: If any module raises the exception.\n    \"\"\"\n    # Notify Pype modules\n    from openpype.modules import ModulesManager, ISettingsChangeListener\n\n    default_values = get_default_settings()[PROJECT_SETTINGS_KEY]\n    if project_name:\n        old_data = get_project_settings(project_name)\n\n        studio_overrides = get_studio_project_settings_overrides()\n        studio_values = apply_overrides(default_values, studio_overrides)\n        clear_metadata_from_settings(studio_values)\n        new_data = apply_overrides(studio_values, copy.deepcopy(overrides))\n\n    else:\n        old_data = get_default_project_settings(exclude_locals=True)\n        new_data = apply_overrides(default_values, copy.deepcopy(overrides))\n\n    new_data_with_metadata = copy.deepcopy(new_data)\n    clear_metadata_from_settings(new_data)\n\n    changes = calculate_changes(old_data, new_data)\n    modules_manager = ModulesManager()\n    warnings = []\n    for module in modules_manager.get_enabled_modules():\n        if isinstance(module, ISettingsChangeListener):\n            try:\n                module.on_project_settings_save(\n                    old_data,\n                    new_data,\n                    project_name,\n                    changes,\n                    new_data_with_metadata\n                )\n            except SaveWarningExc as exc:\n                warnings.extend(exc.warnings)\n    _SETTINGS_HANDLER.save_change_log(project_name, changes, \"project\")\n    _SETTINGS_HANDLER.save_project_settings(project_name, overrides)\n\n    if warnings:\n        raise SaveWarningExc(warnings)\n\n\n@require_handler\ndef save_project_anatomy(project_name, anatomy_data):\n    \"\"\"Save studio overrides of project anatomy.\n\n    Old value, new value and changes are passed to enabled modules that want to\n    know about settings changes.\n\n    For saving of data cares registered Settings handler.\n\n    Warning messages are not logged as module raising them should log it within\n    it's logger.\n\n    Args:\n        project_name (str): Project name for which overrides are passed.\n            Default project's value is None.\n        overrides(dict): Overrides data with metadata defying studio overrides.\n\n    Raises:\n        SaveWarningExc: If any module raises the exception.\n    \"\"\"\n    # Notify Pype modules\n    from openpype.modules import ModulesManager, ISettingsChangeListener\n\n    default_values = get_default_settings()[PROJECT_ANATOMY_KEY]\n    if project_name:\n        old_data = get_anatomy_settings(project_name)\n\n        studio_overrides = get_studio_project_settings_overrides()\n        studio_values = apply_overrides(default_values, studio_overrides)\n        clear_metadata_from_settings(studio_values)\n        new_data = apply_overrides(studio_values, copy.deepcopy(anatomy_data))\n\n    else:\n        old_data = get_default_anatomy_settings(exclude_locals=True)\n        new_data = apply_overrides(default_values, copy.deepcopy(anatomy_data))\n\n    new_data_with_metadata = copy.deepcopy(new_data)\n    clear_metadata_from_settings(new_data)\n\n    changes = calculate_changes(old_data, new_data)\n    modules_manager = ModulesManager()\n    warnings = []\n    for module in modules_manager.get_enabled_modules():\n        if isinstance(module, ISettingsChangeListener):\n            try:\n                module.on_project_anatomy_save(\n                    old_data,\n                    new_data,\n                    changes,\n                    project_name,\n                    new_data_with_metadata\n                )\n            except SaveWarningExc as exc:\n                warnings.extend(exc.warnings)\n\n    _SETTINGS_HANDLER.save_change_log(project_name, changes, \"anatomy\")\n    _SETTINGS_HANDLER.save_project_anatomy(project_name, anatomy_data)\n\n    if warnings:\n        raise SaveWarningExc(warnings)\n\n\ndef _system_settings_backwards_compatible_conversion(studio_overrides):\n    # Backwards compatibility of tools 3.9.1 - 3.9.2 to keep\n    #   \"tools\" environments\n    if (\n        \"tools\" in studio_overrides\n        and \"tool_groups\" in studio_overrides[\"tools\"]\n    ):\n        tool_groups = studio_overrides[\"tools\"][\"tool_groups\"]\n        for tool_group, group_value in tool_groups.items():\n            if tool_group in METADATA_KEYS:\n                continue\n\n            variants = group_value.get(\"variants\")\n            if not variants:\n                continue\n\n            for key in set(variants.keys()):\n                if key in METADATA_KEYS:\n                    continue\n\n                variant_value = variants[key]\n                if \"environment\" not in variant_value:\n                    variants[key] = {\n                        \"environment\": variant_value\n                    }\n\n\ndef _project_anatomy_backwards_compatible_conversion(project_anatomy):\n    # Backwards compatibility of node settings in Nuke 3.9.x - 3.10.0\n    # - source PR - https://github.com/pypeclub/OpenPype/pull/3143\n    value = project_anatomy\n    for key in (\"imageio\", \"nuke\", \"nodes\", \"requiredNodes\"):\n        if key not in value:\n            return\n        value = value[key]\n\n    for item in value:\n        for node in item.get(\"knobs\") or []:\n            if \"type\" in node:\n                break\n            node[\"type\"] = \"__legacy__\"\n\n\n@require_handler\ndef get_studio_system_settings_overrides(return_version=False):\n    output = _SETTINGS_HANDLER.get_studio_system_settings_overrides(\n        return_version\n    )\n    value = output\n    if return_version:\n        value, version = output\n    _system_settings_backwards_compatible_conversion(value)\n    return output\n\n\n@require_handler\ndef get_studio_project_settings_overrides(return_version=False):\n    return _SETTINGS_HANDLER.get_studio_project_settings_overrides(\n        return_version\n    )\n\n\n@require_handler\ndef get_studio_project_anatomy_overrides(return_version=False):\n    return _SETTINGS_HANDLER.get_studio_project_anatomy_overrides(\n        return_version\n    )\n\n\n@require_handler\ndef get_project_settings_overrides(project_name, return_version=False):\n    return _SETTINGS_HANDLER.get_project_settings_overrides(\n        project_name, return_version\n    )\n\n\n@require_handler\ndef get_project_anatomy_overrides(project_name):\n    output = _SETTINGS_HANDLER.get_project_anatomy_overrides(project_name)\n    _project_anatomy_backwards_compatible_conversion(output)\n    return output\n\n\n@require_handler\ndef get_studio_system_settings_overrides_for_version(version):\n    return (\n        _SETTINGS_HANDLER\n        .get_studio_system_settings_overrides_for_version(version)\n    )\n\n\n@require_handler\ndef get_studio_project_anatomy_overrides_for_version(version):\n    return (\n        _SETTINGS_HANDLER\n        .get_studio_project_anatomy_overrides_for_version(version)\n    )\n\n\n@require_handler\ndef get_studio_project_settings_overrides_for_version(version):\n    return (\n        _SETTINGS_HANDLER\n        .get_studio_project_settings_overrides_for_version(version)\n    )\n\n\n@require_handler\ndef get_project_settings_overrides_for_version(\n    project_name, version\n):\n    return (\n        _SETTINGS_HANDLER\n        .get_project_settings_overrides_for_version(project_name, version)\n    )\n\n\n@require_handler\ndef get_available_studio_system_settings_overrides_versions(sorted=None):\n    return (\n        _SETTINGS_HANDLER\n        .get_available_studio_system_settings_overrides_versions(\n            sorted=sorted\n        )\n    )\n\n\n@require_handler\ndef get_available_studio_project_anatomy_overrides_versions(sorted=None):\n    return (\n        _SETTINGS_HANDLER\n        .get_available_studio_project_anatomy_overrides_versions(\n            sorted=sorted\n        )\n    )\n\n\n@require_handler\ndef get_available_studio_project_settings_overrides_versions(sorted=None):\n    return (\n        _SETTINGS_HANDLER\n        .get_available_studio_project_settings_overrides_versions(\n            sorted=sorted\n        )\n    )\n\n\n@require_handler\ndef get_available_project_settings_overrides_versions(\n    project_name, sorted=None\n):\n    return (\n        _SETTINGS_HANDLER\n        .get_available_project_settings_overrides_versions(\n            project_name, sorted=sorted\n        )\n    )\n\n\n@require_handler\ndef find_closest_version_for_projects(project_names):\n    return (\n        _SETTINGS_HANDLER\n        .find_closest_version_for_projects(project_names)\n    )\n\n\n@require_handler\ndef clear_studio_system_settings_overrides_for_version(version):\n    return (\n        _SETTINGS_HANDLER\n        .clear_studio_system_settings_overrides_for_version(version)\n    )\n\n\n@require_handler\ndef clear_studio_project_settings_overrides_for_version(version):\n    return (\n        _SETTINGS_HANDLER\n        .clear_studio_project_settings_overrides_for_version(version)\n    )\n\n\n@require_handler\ndef clear_studio_project_anatomy_overrides_for_version(version):\n    return (\n        _SETTINGS_HANDLER\n        .clear_studio_project_anatomy_overrides_for_version(version)\n    )\n\n\n@require_handler\ndef clear_project_settings_overrides_for_version(\n    version, project_name\n):\n    return _SETTINGS_HANDLER.clear_project_settings_overrides_for_version(\n        version, project_name\n    )\n\n\n@require_local_handler\ndef save_local_settings(data):\n    return _LOCAL_SETTINGS_HANDLER.save_local_settings(data)\n\n\n@require_local_handler\ndef _get_local_settings():\n    return _LOCAL_SETTINGS_HANDLER.get_local_settings()\n\n\ndef get_local_settings():\n    if not AYON_SERVER_ENABLED:\n        return _get_local_settings()\n    # TODO implement ayon implementation\n    return {}\n\n\ndef load_openpype_default_settings():\n    \"\"\"Load openpype default settings.\"\"\"\n    return load_jsons_from_dir(DEFAULTS_DIR)\n\n\ndef reset_default_settings():\n    \"\"\"Reset cache of default settings. Can't be used now.\"\"\"\n    global _DEFAULT_SETTINGS\n    _DEFAULT_SETTINGS = None\n\n\ndef _get_default_settings():\n    from openpype.modules import get_module_settings_defs\n\n    defaults = load_openpype_default_settings()\n\n    module_settings_defs = get_module_settings_defs()\n    for module_settings_def_cls in module_settings_defs:\n        module_settings_def = module_settings_def_cls()\n        system_defaults = module_settings_def.get_defaults(\n            SYSTEM_SETTINGS_KEY\n        ) or {}\n        for path, value in system_defaults.items():\n            if not path:\n                continue\n\n            subdict = defaults[\"system_settings\"]\n            path_items = list(path.split(\"/\"))\n            last_key = path_items.pop(-1)\n            for key in path_items:\n                subdict = subdict[key]\n            subdict[last_key] = value\n\n        project_defaults = module_settings_def.get_defaults(\n            PROJECT_SETTINGS_KEY\n        ) or {}\n        for path, value in project_defaults.items():\n            if not path:\n                continue\n\n            subdict = defaults\n            path_items = list(path.split(\"/\"))\n            last_key = path_items.pop(-1)\n            for key in path_items:\n                subdict = subdict[key]\n            subdict[last_key] = value\n\n    return defaults\n\n\ndef get_default_settings():\n    \"\"\"Get default settings.\n\n    Todo:\n        Cache loaded defaults.\n\n    Returns:\n        dict: Loaded default settings.\n    \"\"\"\n    global _DEFAULT_SETTINGS\n    if _DEFAULT_SETTINGS is None:\n        _DEFAULT_SETTINGS = _get_default_settings()\n    return copy.deepcopy(_DEFAULT_SETTINGS)\n\n\ndef load_json_file(fpath):\n    # Load json data\n    try:\n        with open(fpath, \"r\") as opened_file:\n            return json.load(opened_file)\n\n    except JSON_EXC:\n        log.warning(\n            \"File has invalid json format \\\"{}\\\"\".format(fpath),\n            exc_info=True\n        )\n    return {}\n\n\ndef load_jsons_from_dir(path, *args, **kwargs):\n    \"\"\"Load all .json files with content from entered folder path.\n\n    Data are loaded recursively from a directory and recreate the\n    hierarchy as a dictionary.\n\n    Entered path hierarchy:\n    |_ folder1\n    | |_ data1.json\n    |_ folder2\n      |_ subfolder1\n        |_ data2.json\n\n    Will result in:\n    ```javascript\n    {\n        \"folder1\": {\n            \"data1\": \"CONTENT OF FILE\"\n        },\n        \"folder2\": {\n            \"subfolder1\": {\n                \"data2\": \"CONTENT OF FILE\"\n            }\n        }\n    }\n    ```\n\n    Args:\n        path (str): Path to the root folder where the json hierarchy starts.\n\n    Returns:\n        dict: Loaded data.\n    \"\"\"\n    output = {}\n\n    path = os.path.normpath(path)\n    if not os.path.exists(path):\n        # TODO warning\n        return output\n\n    sub_keys = list(kwargs.pop(\"subkeys\", args))\n    for sub_key in tuple(sub_keys):\n        _path = os.path.join(path, sub_key)\n        if not os.path.exists(_path):\n            break\n\n        path = _path\n        sub_keys.pop(0)\n\n    base_len = len(path) + 1\n    for base, _directories, filenames in os.walk(path):\n        base_items_str = base[base_len:]\n        if not base_items_str:\n            base_items = []\n        else:\n            base_items = base_items_str.split(os.path.sep)\n\n        for filename in filenames:\n            basename, ext = os.path.splitext(filename)\n            if ext == \".json\":\n                full_path = os.path.join(base, filename)\n                value = load_json_file(full_path)\n                dict_keys = base_items + [basename]\n                output = subkey_merge(output, value, dict_keys)\n\n    for sub_key in sub_keys:\n        output = output[sub_key]\n    return output\n\n\ndef subkey_merge(_dict, value, keys):\n    key = keys.pop(0)\n    if not keys:\n        _dict[key] = value\n        return _dict\n\n    if key not in _dict:\n        _dict[key] = {}\n    _dict[key] = subkey_merge(_dict[key], value, keys)\n\n    return _dict\n\n\ndef merge_overrides(source_dict, override_dict):\n    \"\"\"Merge data from override_dict to source_dict.\"\"\"\n\n    if M_OVERRIDDEN_KEY in override_dict:\n        overridden_keys = set(override_dict.pop(M_OVERRIDDEN_KEY))\n    else:\n        overridden_keys = set()\n\n    for key, value in override_dict.items():\n        if (key in overridden_keys or key not in source_dict):\n            source_dict[key] = value\n\n        elif isinstance(value, dict) and isinstance(source_dict[key], dict):\n            source_dict[key] = merge_overrides(source_dict[key], value)\n\n        else:\n            source_dict[key] = value\n    return source_dict\n\n\ndef apply_overrides(source_data, override_data):\n    if not override_data:\n        return source_data\n    _source_data = copy.deepcopy(source_data)\n    return merge_overrides(_source_data, override_data)\n\n\ndef apply_local_settings_on_system_settings(system_settings, local_settings):\n    \"\"\"Apply local settings on studio system settings.\n\n    ATM local settings can modify only application executables. Executable\n    values are not overridden but prepended.\n    \"\"\"\n    if not local_settings or \"applications\" not in local_settings:\n        return\n\n    current_platform = platform.system().lower()\n    apps_settings = system_settings[\"applications\"]\n    additional_apps = apps_settings[\"additional_apps\"]\n    for app_group_name, value in local_settings[\"applications\"].items():\n        if not value:\n            continue\n\n        if (\n            app_group_name not in apps_settings\n            and app_group_name not in additional_apps\n        ):\n            continue\n\n        if app_group_name in apps_settings:\n            variants = apps_settings[app_group_name][\"variants\"]\n\n        else:\n            variants = (\n                apps_settings[\"additional_apps\"][app_group_name][\"variants\"]\n            )\n\n        for app_name, app_value in value.items():\n            if (\n                not app_value\n                or app_name not in variants\n                or \"executables\" not in variants[app_name]\n            ):\n                continue\n\n            executable = app_value.get(\"executable\")\n            if not executable:\n                continue\n            platform_executables = variants[app_name][\"executables\"].get(\n                current_platform\n            )\n            # TODO This is temporary fix until launch arguments will be stored\n            #   per platform and not per executable.\n            # - local settings store only executable\n            new_executables = [executable]\n            new_executables.extend(platform_executables)\n            variants[app_name][\"executables\"] = new_executables\n\n\ndef apply_local_settings_on_anatomy_settings(\n    anatomy_settings, local_settings, project_name, site_name=None\n):\n    \"\"\"Apply local settings on anatomy settings.\n\n    ATM local settings can modify project roots. Project name is required as\n    local settings have data stored data by project's name.\n\n    Local settings override root values in this order:\n    1.) Check if local settings contain overrides for default project and\n        apply it's values on roots if there are any.\n    2.) If passed `project_name` is not None then check project specific\n        overrides in local settings for the project and apply it's value on\n        roots if there are any.\n\n    NOTE: Root values of default project from local settings are always applied\n        if are set.\n\n    Args:\n        anatomy_settings (dict): Data for anatomy settings.\n        local_settings (dict): Data of local settings.\n        project_name (str): Name of project for which anatomy data are.\n    \"\"\"\n    if not local_settings:\n        return\n\n    local_project_settings = local_settings.get(\"projects\") or {}\n\n    # Check for roots existence in local settings first\n    roots_project_locals = (\n        local_project_settings\n        .get(project_name, {})\n    )\n    roots_default_locals = (\n        local_project_settings\n        .get(DEFAULT_PROJECT_KEY, {})\n    )\n\n    # Skip rest of processing if roots are not set\n    if not roots_project_locals and not roots_default_locals:\n        return\n\n    # Get active site from settings\n    if site_name is None:\n        if project_name:\n            project_settings = get_project_settings(project_name)\n        else:\n            project_settings = get_default_project_settings()\n        site_name = (\n            project_settings[\"global\"][\"sync_server\"][\"config\"][\"active_site\"]\n        )\n\n    # QUESTION should raise an exception?\n    if not site_name:\n        return\n\n    # Combine roots from local settings\n    roots_locals = roots_default_locals.get(site_name) or {}\n    roots_locals.update(roots_project_locals.get(site_name) or {})\n    # Skip processing if roots for current active site are not available in\n    #   local settings\n    if not roots_locals:\n        return\n\n    current_platform = platform.system().lower()\n\n    root_data = anatomy_settings[\"roots\"]\n    for root_name, path in roots_locals.items():\n        if root_name not in root_data:\n            continue\n        anatomy_settings[\"roots\"][root_name][current_platform] = (\n            path\n        )\n\n\ndef get_site_local_overrides(project_name, site_name, local_settings=None):\n    \"\"\"Site overrides from local settings for passet project and site name.\n\n    Args:\n        project_name (str): For which project are overrides.\n        site_name (str): For which site are overrides needed.\n        local_settings (dict): Preloaded local settings. They are loaded\n            automatically if not passed.\n    \"\"\"\n    # Check if local settings were passed\n    if local_settings is None:\n        local_settings = get_local_settings()\n\n    output = {}\n\n    # Skip if local settings are empty\n    if not local_settings:\n        return output\n\n    local_project_settings = local_settings.get(\"projects\") or {}\n\n    # Prepare overrides for entered project and for default project\n    project_locals = None\n    if project_name:\n        project_locals = local_project_settings.get(project_name)\n    default_project_locals = local_project_settings.get(DEFAULT_PROJECT_KEY)\n\n    # First load and use local settings from default project\n    if default_project_locals and site_name in default_project_locals:\n        output.update(default_project_locals[site_name])\n\n    # Apply project specific local settings if there are any\n    if project_locals and site_name in project_locals:\n        output.update(project_locals[site_name])\n\n    return output\n\n\ndef apply_local_settings_on_project_settings(\n    project_settings, local_settings, project_name\n):\n    \"\"\"Apply local settings on project settings.\n\n    Currently is modifying active site and remote site in sync server.\n\n    Args:\n        project_settings (dict): Data for project settings.\n        local_settings (dict): Data of local settings.\n        project_name (str): Name of project for which settings data are.\n    \"\"\"\n    if not local_settings:\n        return\n\n    local_project_settings = local_settings.get(\"projects\")\n    if not local_project_settings:\n        return\n\n    project_locals = local_project_settings.get(project_name) or {}\n    default_locals = local_project_settings.get(DEFAULT_PROJECT_KEY) or {}\n    active_site = (\n        project_locals.get(\"active_site\")\n        or default_locals.get(\"active_site\")\n    )\n    remote_site = (\n        project_locals.get(\"remote_site\")\n        or default_locals.get(\"remote_site\")\n    )\n\n    sync_server_config = project_settings[\"global\"][\"sync_server\"][\"config\"]\n    if active_site:\n        sync_server_config[\"active_site\"] = active_site\n\n    if remote_site:\n        sync_server_config[\"remote_site\"] = remote_site\n\n\ndef _get_system_settings(clear_metadata=True, exclude_locals=None):\n    \"\"\"System settings with applied studio overrides.\"\"\"\n    default_values = get_default_settings()[SYSTEM_SETTINGS_KEY]\n    studio_values = get_studio_system_settings_overrides()\n    result = apply_overrides(default_values, studio_values)\n\n    # Clear overrides metadata from settings\n    if clear_metadata:\n        clear_metadata_from_settings(result)\n\n    # Apply local settings\n    # Default behavior is based on `clear_metadata` value\n    if exclude_locals is None:\n        exclude_locals = not clear_metadata\n\n    if not exclude_locals:\n        # TODO local settings may be required to apply for environments\n        local_settings = get_local_settings()\n        apply_local_settings_on_system_settings(result, local_settings)\n\n    return result\n\n\ndef get_default_project_settings(clear_metadata=True, exclude_locals=None):\n    \"\"\"Project settings with applied studio's default project overrides.\"\"\"\n    default_values = get_default_settings()[PROJECT_SETTINGS_KEY]\n    studio_values = get_studio_project_settings_overrides()\n    result = apply_overrides(default_values, studio_values)\n    # Clear overrides metadata from settings\n    if clear_metadata:\n        clear_metadata_from_settings(result)\n\n    # Apply local settings\n    if exclude_locals is None:\n        exclude_locals = not clear_metadata\n\n    if not exclude_locals:\n        local_settings = get_local_settings()\n        apply_local_settings_on_project_settings(\n            result, local_settings, None\n        )\n    return result\n\n\ndef get_default_anatomy_settings(clear_metadata=True, exclude_locals=None):\n    \"\"\"Project anatomy data with applied studio's default project overrides.\"\"\"\n    default_values = get_default_settings()[PROJECT_ANATOMY_KEY]\n    studio_values = get_studio_project_anatomy_overrides()\n\n    result = apply_overrides(default_values, studio_values)\n    # Clear overrides metadata from settings\n    if clear_metadata:\n        clear_metadata_from_settings(result)\n\n    # Apply local settings\n    if exclude_locals is None:\n        exclude_locals = not clear_metadata\n\n    if not exclude_locals:\n        local_settings = get_local_settings()\n        apply_local_settings_on_anatomy_settings(\n            result, local_settings, None\n        )\n    return result\n\n\ndef get_anatomy_settings(\n    project_name, site_name=None, clear_metadata=True, exclude_locals=None\n):\n    \"\"\"Project anatomy data with applied studio and project overrides.\"\"\"\n    if not project_name:\n        raise ValueError(\n            \"Must enter project name. Call \"\n            \"`get_default_anatomy_settings` to get project defaults.\"\n        )\n\n    studio_overrides = get_default_anatomy_settings(False)\n    project_overrides = get_project_anatomy_overrides(\n        project_name\n    )\n    result = copy.deepcopy(studio_overrides)\n    if project_overrides:\n        for key, value in project_overrides.items():\n            result[key] = value\n\n    # Clear overrides metadata from settings\n    if clear_metadata:\n        clear_metadata_from_settings(result)\n\n    # Apply local settings\n    if exclude_locals is None:\n        exclude_locals = not clear_metadata\n\n    if not exclude_locals:\n        local_settings = get_local_settings()\n        apply_local_settings_on_anatomy_settings(\n            result, local_settings, project_name, site_name\n        )\n\n    return result\n\n\ndef _get_project_settings(\n    project_name, clear_metadata=True, exclude_locals=None\n):\n    \"\"\"Project settings with applied studio and project overrides.\"\"\"\n    if not project_name:\n        raise ValueError(\n            \"Must enter project name.\"\n            \" Call `get_default_project_settings` to get project defaults.\"\n        )\n\n    studio_overrides = get_default_project_settings(False)\n    project_overrides = get_project_settings_overrides(\n        project_name\n    )\n\n    result = apply_overrides(studio_overrides, project_overrides)\n\n    # Clear overrides metadata from settings\n    if clear_metadata:\n        clear_metadata_from_settings(result)\n\n    # Apply local settings\n    if exclude_locals is None:\n        exclude_locals = not clear_metadata\n\n    if not exclude_locals:\n        local_settings = get_local_settings()\n        apply_local_settings_on_project_settings(\n            result, local_settings, project_name\n        )\n\n    return result\n\n\ndef get_current_project_settings():\n    \"\"\"Project settings for current context project.\n\n    Project name should be stored in environment variable `AVALON_PROJECT`.\n    This function should be used only in host context where environment\n    variable must be set and should not happen that any part of process will\n    change the value of the enviornment variable.\n    \"\"\"\n    project_name = os.environ.get(\"AVALON_PROJECT\")\n    if not project_name:\n        raise ValueError(\n            \"Missing context project in environemt variable `AVALON_PROJECT`.\"\n        )\n    return get_project_settings(project_name)\n\n\n@require_handler\ndef _get_global_settings():\n    default_settings = load_openpype_default_settings()\n    default_values = default_settings[\"system_settings\"][\"general\"]\n    studio_values = _SETTINGS_HANDLER.get_global_settings()\n    return {\n        key: studio_values.get(key, default_values.get(key))\n        for key in _SETTINGS_HANDLER.global_keys\n    }\n\n\ndef get_global_settings():\n    if not AYON_SERVER_ENABLED:\n        return _get_global_settings()\n    default_settings = load_openpype_default_settings()\n    return default_settings[\"system_settings\"][\"general\"]\n\n\ndef _get_general_environments():\n    \"\"\"Get general environments.\n\n    Function is implemented to be able load general environments without using\n    `get_default_settings`.\n    \"\"\"\n    # Use only openpype defaults.\n    # - prevent to use `get_system_settings` where `get_default_settings`\n    #   is used\n    default_values = load_openpype_default_settings()\n    system_settings = default_values[\"system_settings\"]\n    studio_overrides = get_studio_system_settings_overrides()\n\n    result = apply_overrides(system_settings, studio_overrides)\n    environments = result[\"general\"][\"environment\"]\n\n    clear_metadata_from_settings(environments)\n\n    whitelist_envs = result[\"general\"].get(\"local_env_white_list\")\n    if whitelist_envs:\n        local_settings = get_local_settings()\n        local_envs = local_settings.get(\"environments\") or {}\n        for key, value in local_envs.items():\n            if key in whitelist_envs and key in environments:\n                environments[key] = value\n\n    return environments\n\n\ndef get_general_environments():\n    if not AYON_SERVER_ENABLED:\n        return _get_general_environments()\n    value = get_system_settings()\n    return value[\"general\"][\"environment\"]\n\n\ndef get_system_settings(*args, **kwargs):\n    if not AYON_SERVER_ENABLED:\n        return _get_system_settings(*args, **kwargs)\n\n    default_settings = get_default_settings()[SYSTEM_SETTINGS_KEY]\n    return get_ayon_system_settings(default_settings)\n\n\ndef get_project_settings(project_name, *args, **kwargs):\n    if not AYON_SERVER_ENABLED:\n        return _get_project_settings(project_name, *args, **kwargs)\n\n    default_settings = get_default_settings()[PROJECT_SETTINGS_KEY]\n    return get_ayon_project_settings(default_settings, project_name)\n"
  },
  {
    "path": "openpype/settings/local_settings.md",
    "content": "# Structure of local settings\n- local settings do not have any validation schemas right now this should help to see what is stored to local settings and how it works\n- they are stored by identifier site_id which should be unified identifier of workstation\n- all keys may and may not available on load\n- contain main categories: `general`, `applications`, `projects`\n\n## Categories\n### General\n- ATM contain only label of site\n```json\n{\n    \"general\": {\n        \"site_label\": \"MySite\"\n    }\n}\n```\n\n### Applications\n- modifications of application executables\n- output should match application groups and variants\n```json\n{\n    \"applications\": {\n        \"<app group>\": {\n            \"<app name>\": {\n                \"executable\": \"/my/path/to/nuke_12_2\"\n            }\n        }\n    }\n}\n```\n\n### Projects\n- project specific modifications\n- default project is stored under constant key defined in `pype.settings.contants`\n```json\n{\n    \"projects\": {\n        \"<project name>\": {\n            \"active_site\": \"<name of active site>\",\n            \"remote_site\": \"<name of remote site>\",\n            \"roots\": {\n                \"<site name>\": {\n                    \"<root name>\": \"<root dir path>\"\n                }\n            }\n        }\n    }\n}\n```\n\n## Final document\n```json\n{\n    \"_id\": \"<ObjectId(...)>\",\n    \"site_id\": \"<site id>\",\n    \"general\": {\n        \"site_label\": \"MySite\"\n    },\n    \"applications\": {\n        \"<app group>\": {\n            \"<app name>\": {\n                \"executable\": \"<path to app executable>\"\n            }\n        }\n    },\n    \"projects\": {\n        \"<project name>\": {\n            \"active_site\": \"<name of active site>\",\n            \"remote_site\": \"<name of remote site>\",\n            \"roots\": {\n                \"<site name>\": {\n                    \"<root name>\": \"<root dir path>\"\n                }\n            }\n        }\n    }\n}\n```\n"
  },
  {
    "path": "openpype/style/__init__.py",
    "content": "import os\nimport copy\nimport json\nimport collections\nimport six\n\nfrom openpype import resources\n\nfrom .color_defs import parse_color\n\ncurrent_dir = os.path.dirname(os.path.abspath(__file__))\n\n\nclass _Cache:\n    stylesheet = None\n    font_ids = None\n\n    tools_icon_color = None\n    default_entity_icon_color = None\n    disabled_entity_icon_color = None\n    deprecated_entity_font_color = None\n\n    colors_data = None\n    objected_colors = None\n\n\ndef get_style_image_path(image_name):\n    # All filenames are lowered\n    image_name = image_name.lower()\n    # Male sure filename has png extension\n    if not image_name.endswith(\".png\"):\n        image_name += \".png\"\n    filepath = os.path.join(current_dir, \"images\", image_name)\n    if os.path.exists(filepath):\n        return filepath\n    return None\n\n\ndef _get_colors_raw_data():\n    \"\"\"Read data file with stylesheet fill values.\n\n    Returns:\n        dict: Loaded data for stylesheet.\n    \"\"\"\n    data_path = os.path.join(current_dir, \"data.json\")\n    with open(data_path, \"r\") as data_stream:\n        data = json.load(data_stream)\n    return data\n\n\ndef get_colors_data():\n    \"\"\"Only color data from stylesheet data.\"\"\"\n    if _Cache.colors_data is None:\n        data = _get_colors_raw_data()\n        color_data = data.get(\"color\") or {}\n        _Cache.colors_data = color_data\n    return copy.deepcopy(_Cache.colors_data)\n\n\ndef _convert_color_values_to_objects(value):\n    \"\"\"Parse all string values in dictionary to Color definitions.\n\n    Recursive function calling itself if value is dictionary.\n\n    Args:\n        value (dict, str): String is parsed into color definition object and\n            dictionary is passed into this function.\n\n    Raises:\n        TypeError: If value in color data do not contain string of dictionary.\n    \"\"\"\n    if isinstance(value, dict):\n        output = {}\n        for _key, _value in value.items():\n            output[_key] = _convert_color_values_to_objects(_value)\n        return output\n\n    if not isinstance(value, six.string_types):\n        raise TypeError((\n            \"Unexpected type in colors data '{}'. Expected 'str' or 'dict'.\"\n        ).format(str(type(value))))\n    return parse_color(value)\n\n\ndef get_objected_colors(*keys):\n    \"\"\"Colors parsed from stylesheet data into color definitions.\n\n    You can pass multiple arguments to get a key from the data dict's colors.\n    Because this functions returns a deep copy of the cached data this allows\n    a much smaller dataset to be copied and thus result in a faster function.\n    It is however a micro-optimization in the area of 0.001s and smaller.\n\n    For example:\n        >>> get_colors_data()           # copy of full colors dict\n        >>> get_colors_data(\"font\")\n        >>> get_colors_data(\"loader\", \"asset-view\")\n\n    Args:\n        *keys: Each key argument will return a key nested deeper in the\n            objected colors data.\n\n    Returns:\n        Any: Parsed color objects by keys in data.\n    \"\"\"\n    if _Cache.objected_colors is None:\n        colors_data = get_colors_data()\n        output = {}\n        for key, value in colors_data.items():\n            output[key] = _convert_color_values_to_objects(value)\n\n        _Cache.objected_colors = output\n\n    output = _Cache.objected_colors\n    for key in keys:\n        output = output[key]\n    return copy.deepcopy(output)\n\n\ndef _load_stylesheet():\n    \"\"\"Load strylesheet and trigger all related callbacks.\n\n    Style require more than a stylesheet string. Stylesheet string\n    contains paths to resources which must be registered into Qt application\n    and load fonts used in stylesheets.\n\n    Also replace values from stylesheet data into stylesheet text.\n    \"\"\"\n    from . import qrc_resources\n\n    qrc_resources.qInitResources()\n\n    style_path = os.path.join(current_dir, \"style.css\")\n    with open(style_path, \"r\") as style_file:\n        stylesheet = style_file.read()\n\n    data = _get_colors_raw_data()\n\n    data_deque = collections.deque()\n    for item in data.items():\n        data_deque.append(item)\n\n    fill_data = {}\n    while data_deque:\n        key, value = data_deque.popleft()\n        if isinstance(value, dict):\n            for sub_key, sub_value in value.items():\n                new_key = \"{}:{}\".format(key, sub_key)\n                data_deque.append((new_key, sub_value))\n            continue\n        fill_data[key] = value\n\n    for key, value in fill_data.items():\n        replacement_key = \"{\" + key + \"}\"\n        stylesheet = stylesheet.replace(replacement_key, value)\n    return stylesheet\n\n\ndef _load_font():\n    \"\"\"Load and register fonts into Qt application.\"\"\"\n    from qtpy import QtGui\n\n    # Check if font ids are still loaded\n    if _Cache.font_ids is not None:\n        for font_id in tuple(_Cache.font_ids):\n            font_families = QtGui.QFontDatabase.applicationFontFamilies(\n                font_id\n            )\n            # Reset font if font id is not available\n            if not font_families:\n                _Cache.font_ids = None\n                break\n\n    if _Cache.font_ids is None:\n        _Cache.font_ids = []\n        fonts_dirpath = os.path.join(current_dir, \"fonts\")\n        font_dirs = []\n        font_dirs.append(os.path.join(fonts_dirpath, \"Noto_Sans\"))\n        font_dirs.append(os.path.join(\n            fonts_dirpath,\n            \"Noto_Sans_Mono\",\n            \"static\",\n            \"NotoSansMono\"\n        ))\n\n        loaded_fonts = []\n        for font_dir in font_dirs:\n            for filename in os.listdir(font_dir):\n                if os.path.splitext(filename)[1] not in [\".ttf\"]:\n                    continue\n                full_path = os.path.join(font_dir, filename)\n                font_id = QtGui.QFontDatabase.addApplicationFont(full_path)\n                _Cache.font_ids.append(font_id)\n                font_families = QtGui.QFontDatabase.applicationFontFamilies(\n                    font_id\n                )\n                loaded_fonts.extend(font_families)\n        print(\"Registered font families: {}\".format(\", \".join(loaded_fonts)))\n\n\ndef load_stylesheet():\n    \"\"\"Load and return OpenPype Qt stylesheet.\"\"\"\n\n    if _Cache.stylesheet is None:\n        _Cache.stylesheet = _load_stylesheet()\n    _load_font()\n    return _Cache.stylesheet\n\n\ndef get_app_icon_path():\n    \"\"\"Path to OpenPype icon.\"\"\"\n    return resources.get_openpype_icon_filepath()\n\n\ndef app_icon_path():\n    # Backwards compatibility\n    return get_app_icon_path()\n\n\ndef get_default_tools_icon_color():\n    \"\"\"Default color used in tool icons.\n\n    Color must be possible to parse using QColor.\n\n    Returns:\n        str: Color as a string.\n    \"\"\"\n    if _Cache.tools_icon_color is None:\n        color_data = get_colors_data()\n        _Cache.tools_icon_color = color_data[\"icon-tools\"]\n    return _Cache.tools_icon_color\n\n\ndef get_default_entity_icon_color():\n    \"\"\"Default color of entities icons.\n\n    Color must be possible to parse using QColor.\n\n    Returns:\n        str: Color as a string.\n    \"\"\"\n    if _Cache.default_entity_icon_color is None:\n        color_data = get_colors_data()\n        _Cache.default_entity_icon_color = color_data[\"icon-entity-default\"]\n    return _Cache.default_entity_icon_color\n\n\ndef get_disabled_entity_icon_color():\n    \"\"\"Default color of entities icons.\n\n    TODO: Find more suitable function name.\n\n    Color must be possible to parse using QColor.\n\n    Returns:\n        str: Color as a string.\n    \"\"\"\n    if _Cache.disabled_entity_icon_color is None:\n        color_data = get_colors_data()\n        _Cache.disabled_entity_icon_color = color_data[\"icon-entity-disabled\"]\n    return _Cache.disabled_entity_icon_color\n\n\ndef get_deprecated_entity_font_color():\n    \"\"\"Font color for deprecated entities.\n\n    Color must be possible to parse using QColor.\n\n    Returns:\n        str: Color as a string.\n    \"\"\"\n    if _Cache.deprecated_entity_font_color is None:\n        color_data = get_colors_data()\n        _Cache.deprecated_entity_font_color = (\n            color_data[\"font-entity-deprecated\"]\n        )\n    return _Cache.deprecated_entity_font_color\n"
  },
  {
    "path": "openpype/style/color_defs.py",
    "content": "\"\"\"Color definitions that can be used to parse strings for stylesheet.\n\nEach definition must have available method `get_qcolor` which should return\n`QtGui.QColor` representation of the color.\n\n# TODO create abstract class to force this method implementation\n\nUsage: Some colors may be not be used only in stylesheet but is required to\nuse them in code too. To not hardcode these color values into code it is better\nto use same colors that are available fro stylesheets.\n\nIt is possible that some colors may not be used in stylesheet at all and thei\ndefinition is used only in code.\n\"\"\"\n\nimport re\n\n\ndef parse_color(value):\n    \"\"\"Parse string value of color to one of objected representation.\n\n    Args:\n        value(str): Color definition usable in stylesheet.\n    \"\"\"\n    modified_value = value.strip().lower()\n    if modified_value.startswith(\"hsla\"):\n        return HSLAColor(value)\n\n    if modified_value.startswith(\"hsl\"):\n        return HSLColor(value)\n\n    if modified_value.startswith(\"#\"):\n        return HEXColor(value)\n\n    if modified_value.startswith(\"rgba\"):\n        return RGBAColor(value)\n\n    if modified_value.startswith(\"rgb\"):\n        return RGBColor(value)\n    return UnknownColor(value)\n\n\ndef create_qcolor(*args):\n    \"\"\"Create QtGui.QColor object.\n\n    Args:\n        *args (tuple): It is possible to pass initialization arguments for\n            Qcolor.\n    \"\"\"\n    from qtpy import QtGui\n\n    return QtGui.QColor(*args)\n\n\ndef min_max_check(value, min_value, max_value):\n    \"\"\"Validate number value if is in passed range.\n\n    Args:\n        value (int, float): Value which is validated.\n        min_value (int, float): Minimum possible value. Validation is skipped\n            if passed value is None.\n        max_value (int, float): Maximum possible value. Validation is skipped\n            if passed value is None.\n\n    Raises:\n        ValueError: When 'value' is out of specified range.\n    \"\"\"\n    if min_value is not None and value < min_value:\n        raise ValueError(\"Minimum expected value is '{}' got '{}'\".format(\n            min_value, value\n        ))\n\n    if max_value is not None and value > max_value:\n        raise ValueError(\"Maximum expected value is '{}' got '{}'\".format(\n            min_value, value\n        ))\n\n\ndef int_validation(value, min_value=None, max_value=None):\n    \"\"\"Validation of integer value within range.\n\n    Args:\n        value (int): Validated value.\n        min_value (int): Minimum possible value.\n        max_value (int): Maximum possible value.\n\n    Raises:\n        TypeError: If 'value' is not 'int' type.\n    \"\"\"\n    if not isinstance(value, int):\n        raise TypeError((\n            \"Invalid type of hue expected 'int' got {}\"\n        ).format(str(type(value))))\n\n    min_max_check(value, min_value, max_value)\n\n\ndef float_validation(value, min_value=None, max_value=None):\n    \"\"\"Validation of float value within range.\n\n    Args:\n        value (float): Validated value.\n        min_value (float): Minimum possible value.\n        max_value (float): Maximum possible value.\n\n    Raises:\n        TypeError: If 'value' is not 'float' type.\n    \"\"\"\n    if not isinstance(value, float):\n        raise TypeError((\n            \"Invalid type of hue expected 'int' got {}\"\n        ).format(str(type(value))))\n\n    min_max_check(value, min_value, max_value)\n\n\nclass UnknownColor:\n    \"\"\"Color from stylesheet data without known color definition.\n\n    This is backup for unknown color definitions which may be for example\n    constants or definition not yet defined by class.\n    \"\"\"\n    def __init__(self, value):\n        self.value = value\n\n    def get_qcolor(self):\n        return create_qcolor(self.value)\n\n\nclass HEXColor:\n    \"\"\"Hex color definition.\n\n    Hex color is defined by '#' and 3 or 6 hex values (0-F).\n\n    Examples:\n        \"#fff\"\n        \"#f3f3f3\"\n    \"\"\"\n    regex = re.compile(r\"[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$\")\n\n    def __init__(self, color_string):\n        red, green, blue = self.hex_to_rgb(color_string)\n\n        self._color_string = color_string\n        self._red = red\n        self._green = green\n        self._blue = blue\n\n    @property\n    def red(self):\n        return self._red\n\n    @property\n    def green(self):\n        return self._green\n\n    @property\n    def blue(self):\n        return self._blue\n\n    def to_stylesheet_str(self):\n        return self._color_string\n\n    @classmethod\n    def hex_to_rgb(cls, value):\n        \"\"\"Convert hex value to rgb.\"\"\"\n        hex_value = value.lstrip(\"#\")\n        if not cls.regex.match(hex_value):\n            raise ValueError(\"\\\"{}\\\" is not a valid HEX code.\".format(value))\n\n        output = []\n        if len(hex_value) == 3:\n            for char in hex_value:\n                output.append(int(char * 2, 16))\n        else:\n            for idx in range(3):\n                start_idx = idx * 2\n                output.append(int(hex_value[start_idx:start_idx + 2], 16))\n        return output\n\n    def get_qcolor(self):\n        return create_qcolor(self.red, self.green, self.blue)\n\n\nclass RGBColor:\n    \"\"\"Color defined by red green and blue values.\n\n    Each color has possible integer range 0-255.\n\n    Examples:\n        \"rgb(255, 127, 0)\"\n    \"\"\"\n    def __init__(self, value):\n        modified_color = value.lower().strip()\n        content = modified_color.rstrip(\")\").lstrip(\"rgb(\")\n        red_str, green_str, blue_str = (\n            item.strip() for item in content.split(\",\")\n        )\n        red = int(red_str)\n        green = int(green_str)\n        blue = int(blue_str)\n\n        int_validation(red, 0, 255)\n        int_validation(green, 0, 255)\n        int_validation(blue, 0, 255)\n\n        self._red = red\n        self._green = green\n        self._blue = blue\n\n    @property\n    def red(self):\n        return self._red\n\n    @property\n    def green(self):\n        return self._green\n\n    @property\n    def blue(self):\n        return self._blue\n\n    def get_qcolor(self):\n        return create_qcolor(self.red, self.green, self.blue)\n\n\nclass RGBAColor:\n    \"\"\"Color defined by red green, blue and alpha values.\n\n    Each color has possible integer range 0-255.\n\n    Examples:\n        \"rgba(255, 127, 0, 127)\"\n    \"\"\"\n    def __init__(self, value):\n        modified_color = value.lower().strip()\n        content = modified_color.rstrip(\")\").lstrip(\"rgba(\")\n        red_str, green_str, blue_str, alpha_str = (\n            item.strip() for item in content.split(\",\")\n        )\n        red = int(red_str)\n        green = int(green_str)\n        blue = int(blue_str)\n        if \".\" in alpha_str:\n            alpha = int(float(alpha_str) * 100)\n        else:\n            alpha = int(alpha_str)\n\n        int_validation(red, 0, 255)\n        int_validation(green, 0, 255)\n        int_validation(blue, 0, 255)\n        int_validation(alpha, 0, 255)\n\n        self._red = red\n        self._green = green\n        self._blue = blue\n        self._alpha = alpha\n\n    @property\n    def red(self):\n        return self._red\n\n    @property\n    def green(self):\n        return self._green\n\n    @property\n    def blue(self):\n        return self._blue\n\n    @property\n    def alpha(self):\n        return self._alpha\n\n    def get_qcolor(self):\n        return create_qcolor(self.red, self.green, self.blue, self.alpha)\n\n\nclass HSLColor:\n    \"\"\"Color defined by hue, saturation and light values.\n\n    Hue is defined as integer in rage 0-360. Saturation and light can be\n    defined as float or percent value.\n\n    Examples:\n        \"hsl(27, 0.7, 0.3)\"\n        \"hsl(27, 70%, 30%)\"\n    \"\"\"\n    def __init__(self, value):\n        modified_color = value.lower().strip()\n        content = modified_color.rstrip(\")\").lstrip(\"hsl(\")\n        hue_str, sat_str, light_str = (\n            item.strip() for item in content.split(\",\")\n        )\n        hue = int(hue_str) % 360\n        if \"%\" in sat_str:\n            sat = float(sat_str.rstrip(\"%\")) / 100\n        else:\n            sat = float(sat_str)\n\n        if \"%\" in light_str:\n            light = float(light_str.rstrip(\"%\")) / 100\n        else:\n            light = float(light_str)\n\n        int_validation(hue, 0, 360)\n        float_validation(sat, 0, 1)\n        float_validation(light, 0, 1)\n\n        self._hue = hue\n        self._saturation = sat\n        self._light = light\n\n    @property\n    def hue(self):\n        return self._hue\n\n    @property\n    def saturation(self):\n        return self._saturation\n\n    @property\n    def light(self):\n        return self._light\n\n    def get_qcolor(self):\n        color = create_qcolor()\n        color.setHslF(self.hue / 360, self.saturation, self.light)\n        return color\n\n\nclass HSLAColor:\n    \"\"\"Color defined by hue, saturation, light and alpha values.\n\n    Hue is defined as integer in rage 0-360. Saturation and light can be\n    defined as float (0-1 range) or percent value(0-100%). And alpha\n    as float (0-1 range).\n\n    Examples:\n        \"hsla(27, 0.7, 0.3, 0.5)\"\n        \"hsla(27, 70%, 30%, 0.5)\"\n    \"\"\"\n    def __init__(self, value):\n        modified_color = value.lower().strip()\n        content = modified_color.rstrip(\")\").lstrip(\"hsla(\")\n        hue_str, sat_str, light_str, alpha_str = (\n            item.strip() for item in content.split(\",\")\n        )\n        hue = int(hue_str) % 360\n        if \"%\" in sat_str:\n            sat = float(sat_str.rstrip(\"%\")) / 100\n        else:\n            sat = float(sat_str)\n\n        if \"%\" in light_str:\n            light = float(light_str.rstrip(\"%\")) / 100\n        else:\n            light = float(light_str)\n\n        alpha = float(alpha_str)\n\n        int_validation(hue, 0, 360)\n        float_validation(sat, 0, 1)\n        float_validation(light, 0, 1)\n        float_validation(alpha, 0, 1)\n\n        self._hue = hue\n        self._saturation = sat\n        self._light = light\n        self._alpha = alpha\n\n    @property\n    def hue(self):\n        return self._hue\n\n    @property\n    def saturation(self):\n        return self._saturation\n\n    @property\n    def light(self):\n        return self._light\n\n    @property\n    def alpha(self):\n        return self._alpha\n\n    def get_qcolor(self):\n        color = create_qcolor()\n        color.setHslF(self.hue / 360, self.saturation, self.light, self.alpha)\n        return color\n"
  },
  {
    "path": "openpype/style/data.json",
    "content": "{\n    \"palette\": {\n        \"grey-base\": \"#2C313A\",\n        \"grey-light\": \"#373D48\",\n        \"grey-lighter\": \"#434a56\",\n        \"grey-lightest\": \"#4E5565\",\n        \"grey-input\": \"#353A45\",\n        \"grey-dark\": \"#21252B\",\n\n        \"text-darker\": \"#99A3B2\",\n        \"text-base\": \"#D3D8DE\",\n        \"text-lighter\": \"#F0F2F5\",\n\n        \"blue-base\": \"hsl(200, 60%, 60%)\",\n        \"blue-light\": \"hsl(200, 80%, 80%)\",\n\n        \"green-base\": \"hsl(155, 55%, 55%)\",\n        \"green-light\": \"hsl(155, 80%, 80%)\"\n    },\n    \"color\": {\n        \"font\": \"#D3D8DE\",\n        \"font-hover\": \"#F0F2F5\",\n        \"font-disabled\": \"#5b6779\",\n        \"font-view-selection\": \"#ffffff\",\n        \"font-view-hover\": \"#F0F2F5\",\n\n        \"bg\": \"#2C313A\",\n        \"bg-inputs\": \"#21252B\",\n        \"bg-buttons\": \"rgb(67, 74, 86)\",\n        \"bg-buttons-hover\": \"rgb(81, 86, 97)\",\n        \"bg-inputs-disabled\": \"#2C313A\",\n        \"bg-buttons-disabled\": \"#434a56\",\n\n        \"bg-splitter\": \"#434a56\",\n        \"bg-splitter-hover\": \"rgba(168, 175, 189, 0.3)\",\n\n        \"bg-menu-separator\": \"rgba(75, 83, 98, 127)\",\n\n        \"bg-scroll-handle\": \"#4B5362\",\n\n        \"bg-view\": \"#21252B\",\n        \"bg-view-header\": \"#373D48\",\n        \"bg-view-hover\": \"rgba(168, 175, 189, .3)\",\n        \"bg-view-alternate\": \"rgb(36, 42, 50)\",\n        \"bg-view-disabled\": \"#2C313A\",\n        \"bg-view-alternate-disabled\": \"#2C313A\",\n        \"bg-view-selection\": \"rgba(92, 173, 214, .4)\",\n        \"bg-view-selection-hover\": \"rgba(92, 173, 214, .8)\",\n\n        \"border\": \"#373D48\",\n        \"border-hover\": \"rgb(92, 99, 111)\",\n        \"border-focus\": \"rgb(92, 173, 214)\",\n\n        \"restart-btn-bg\": \"#458056\",\n\n        \"delete-btn-bg\": \"rgb(201, 54, 54)\",\n        \"delete-btn-bg-disabled\": \"rgba(201, 54, 54, 64)\",\n\n        \"icon-tools\": \"#ffffff\",\n        \"icon-alert-tools\": \"#AA5050\",\n        \"icon-entity-default\": \"#bfccd6\",\n        \"icon-entity-disabled\": \"#808080\",\n        \"font-entity-deprecated\": \"#666666\",\n        \"overlay-messages\": {\n            \"close-btn\": \"#D3D8DE\",\n            \"bg-success\": \"#458056\",\n            \"bg-success-hover\": \"#55a066\",\n            \"bg-error\": \"#AD2E2E\",\n            \"bg-error-hover\": \"#C93636\",\n            \"bg-info\": \"rgb(63, 98, 121)\",\n            \"bg-info-hover\": \"rgb(81, 146, 181)\"\n        },\n        \"tab-widget\": {\n            \"bg\": \"#21252B\",\n            \"bg-selected\": \"#434a56\",\n            \"bg-hover\": \"#373D48\",\n            \"color\": \"#99A3B2\",\n            \"color-selected\": \"#F0F2F5\",\n            \"color-hover\": \"#F0F2F5\"\n        },\n        \"nice-checkbox\": {\n            \"bg-checked\": \"#56a06f\",\n            \"bg-unchecked\": \"#21252B\",\n            \"bg-checker\": \"#D3D8DE\",\n            \"bg-checker-hover\": \"#F0F2F5\"\n        },\n        \"loader\": {\n            \"asset-view\": {\n                \"selected\": \"rgba(168, 175, 189, 0.6)\",\n                \"hover\": \"rgba(168, 175, 189, 0.3)\",\n                \"selected-hover\": \"rgba(168, 175, 189, 0.7)\"\n            }\n        },\n        \"publisher\": {\n            \"error\": \"#AA5050\",\n            \"crash\": \"#FF6432\",\n            \"success\": \"#458056\",\n            \"warning\": \"#ffc671\",\n            \"progress\": \"rgb(194, 226, 236)\",\n            \"tab-bg\": \"#16191d\",\n            \"list-view-group\": {\n                \"bg\": \"#434a56\",\n                \"bg-hover\": \"rgba(168, 175, 189, 0.3)\",\n                \"bg-selected-hover\": \"rgba(92, 173, 214, 0.4)\",\n                \"bg-expander\": \"#2C313A\",\n                \"bg-expander-hover\": \"#2d6c9f\",\n                \"bg-expander-selected-hover\": \"#3784c5\"\n            }\n        },\n        \"settings\": {\n            \"invalid-light\": \"#C93636\",\n            \"invalid-dark\": \"#AD2E2E\",\n\n            \"modified-light\": \"#46b1f3\",\n            \"modified-mid\": \"#189AEA\",\n            \"modified-dark\": \"#106AA2\",\n\n            \"studio-light\": \"#73C990\",\n            \"studio-dark\": \"#56a06f\",\n            \"studio-label-hover\": \"#FFFFFF\",\n\n            \"project-light\": \"#FFA64D\",\n            \"project-mid\": \"#FF8C1A\",\n            \"project-dark\": \"#E67300\",\n\n            \"label-fg\": \"#969b9e\",\n            \"label-fg-hover\": \"#b8c1c5\",\n\n            \"breadcrumbs-btn-bg\": \"rgba(127, 127, 127, 60)\",\n            \"breadcrumbs-btn-bg-hover\": \"rgba(127, 127, 127, 90)\",\n\n            \"content-highlighted\": \"rgba(19, 26, 32, 15)\",\n            \"focus-border\": \"#839caf\",\n            \"image-btn\": \"#bfccd6\",\n            \"image-btn-hover\": \"#189aea\",\n            \"image-btn-disabled\": \"#bfccd6\",\n            \"version-exists\": \"#458056\",\n            \"version-not-found\": \"#ffc671\",\n\n            \"source-version\": \"#D3D8DE\",\n            \"source-version-outdated\": \"#ffc671\"\n        }\n    }\n}\n"
  },
  {
    "path": "openpype/style/fonts/Noto_Sans/OFL.txt",
    "content": "Copyright 2012 Google Inc. All Rights Reserved.\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttp://scripts.sil.org/OFL\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "openpype/style/fonts/Noto_Sans_Mono/OFL.txt",
    "content": "Copyright 2012 Google Inc. All Rights Reserved.\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttp://scripts.sil.org/OFL\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "openpype/style/fonts/Noto_Sans_Mono/README.txt",
    "content": "Noto Sans Mono Variable Font\n============================\n\nThis download contains Noto Sans Mono as both a variable font and static fonts.\n\nNoto Sans Mono is a variable font with these axes:\n  wdth\n  wght\n\nThis means all the styles are contained in a single file:\n  NotoSansMono-VariableFont_wdth,wght.ttf\n\nIf your app fully supports variable fonts, you can now pick intermediate styles\nthat aren’t available as static fonts. Not all apps support variable fonts, and\nin those cases you can use the static font files for Noto Sans Mono:\n  static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Thin.ttf\n  static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-ExtraLight.ttf\n  static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Light.ttf\n  static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Regular.ttf\n  static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Medium.ttf\n  static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-SemiBold.ttf\n  static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Bold.ttf\n  static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-ExtraBold.ttf\n  static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Black.ttf\n  static/NotoSansMono_Condensed/NotoSansMono_Condensed-Thin.ttf\n  static/NotoSansMono_Condensed/NotoSansMono_Condensed-ExtraLight.ttf\n  static/NotoSansMono_Condensed/NotoSansMono_Condensed-Light.ttf\n  static/NotoSansMono_Condensed/NotoSansMono_Condensed-Regular.ttf\n  static/NotoSansMono_Condensed/NotoSansMono_Condensed-Medium.ttf\n  static/NotoSansMono_Condensed/NotoSansMono_Condensed-SemiBold.ttf\n  static/NotoSansMono_Condensed/NotoSansMono_Condensed-Bold.ttf\n  static/NotoSansMono_Condensed/NotoSansMono_Condensed-ExtraBold.ttf\n  static/NotoSansMono_Condensed/NotoSansMono_Condensed-Black.ttf\n  static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Thin.ttf\n  static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-ExtraLight.ttf\n  static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Light.ttf\n  static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Regular.ttf\n  static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Medium.ttf\n  static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-SemiBold.ttf\n  static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Bold.ttf\n  static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-ExtraBold.ttf\n  static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Black.ttf\n  static/NotoSansMono/NotoSansMono-Thin.ttf\n  static/NotoSansMono/NotoSansMono-ExtraLight.ttf\n  static/NotoSansMono/NotoSansMono-Light.ttf\n  static/NotoSansMono/NotoSansMono-Regular.ttf\n  static/NotoSansMono/NotoSansMono-Medium.ttf\n  static/NotoSansMono/NotoSansMono-SemiBold.ttf\n  static/NotoSansMono/NotoSansMono-Bold.ttf\n  static/NotoSansMono/NotoSansMono-ExtraBold.ttf\n  static/NotoSansMono/NotoSansMono-Black.ttf\n\nGet started\n-----------\n\n1. Install the font files you want to use\n\n2. Use your app's font picker to view the font family and all the\navailable styles\n\nLearn more about variable fonts\n-------------------------------\n\n  https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts\n  https://variablefonts.typenetwork.com\n  https://medium.com/variable-fonts\n\nIn desktop apps\n\n  https://theblog.adobe.com/can-variable-fonts-illustrator-cc\n  https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts\n\nOnline\n\n  https://developers.google.com/fonts/docs/getting_started\n  https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide\n  https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts\n\nInstalling fonts\n\n  MacOS: https://support.apple.com/en-us/HT201749\n  Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux\n  Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows\n\nAndroid Apps\n\n  https://developers.google.com/fonts/docs/android\n  https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts\n\nLicense\n-------\nPlease read the full license text (OFL.txt) to understand the permissions,\nrestrictions and requirements for usage, redistribution, and modification.\n\nYou can use them freely in your products & projects - print or digital,\ncommercial or otherwise.\n\nThis isn't legal advice, please consider consulting a lawyer and see the full\nlicense for all details.\n"
  },
  {
    "path": "openpype/style/pyqt5_resources.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Resource object code\n#\n# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)\n#\n# WARNING! All changes made in this file will be lost!\n\nfrom PyQt5 import QtCore\n\n\nqt_resource_data = b\"\\\n\\x00\\x00\\x07\\xad\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x07\\x00\\x00\\x00\\x0a\\x08\\x06\\x00\\x00\\x00\\x78\\xcc\\x44\\x0d\\\n\\x00\\x00\\x05\\x52\\x69\\x54\\x58\\x74\\x58\\x4d\\x4c\\x3a\\x63\\x6f\\x6d\\x2e\\\n\\x61\\x64\\x6f\\x62\\x65\\x2e\\x78\\x6d\\x70\\x00\\x00\\x00\\x00\\x00\\x3c\\x3f\\\n\\x78\\x70\\x61\\x63\\x6b\\x65\\x74\\x20\\x62\\x65\\x67\\x69\\x6e\\x3d\\x22\\xef\\\n\\xbb\\xbf\\x22\\x20\\x69\\x64\\x3d\\x22\\x57\\x35\\x4d\\x30\\x4d\\x70\\x43\\x65\\\n\\x68\\x69\\x48\\x7a\\x72\\x65\\x53\\x7a\\x4e\\x54\\x63\\x7a\\x6b\\x63\\x39\\x64\\\n\\x22\\x3f\\x3e\\x0a\\x3c\\x78\\x3a\\x78\\x6d\\x70\\x6d\\x65\\x74\\x61\\x20\\x78\\\n\\x6d\\x6c\\x6e\\x73\\x3a\\x78\\x3d\\x22\\x61\\x64\\x6f\\x62\\x65\\x3a\\x6e\\x73\\\n\\x3a\\x6d\\x65\\x74\\x61\\x2f\\x22\\x20\\x78\\x3a\\x78\\x6d\\x70\\x74\\x6b\\x3d\\\n\\x22\\x58\\x4d\\x50\\x20\\x43\\x6f\\x72\\x65\\x20\\x35\\x2e\\x35\\x2e\\x30\\x22\\\n\\x3e\\x0a\\x20\\x3c\\x72\\x64\\x66\\x3a\\x52\\x44\\x46\\x20\\x78\\x6d\\x6c\\x6e\\\n\\x73\\x3a\\x72\\x64\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x77\\x77\\\n\\x77\\x2e\\x77\\x33\\x2e\\x6f\\x72\\x67\\x2f\\x31\\x39\\x39\\x39\\x2f\\x30\\x32\\\n\\x2f\\x32\\x32\\x2d\\x72\\x64\\x66\\x2d\\x73\\x79\\x6e\\x74\\x61\\x78\\x2d\\x6e\\\n\\x73\\x23\\x22\\x3e\\x0a\\x20\\x20\\x3c\\x72\\x64\\x66\\x3a\\x44\\x65\\x73\\x63\\\n\\x72\\x69\\x70\\x74\\x69\\x6f\\x6e\\x20\\x72\\x64\\x66\\x3a\\x61\\x62\\x6f\\x75\\\n\\x74\\x3d\\x22\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x64\\\n\\x63\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x70\\x75\\x72\\x6c\\x2e\\x6f\\\n\\x72\\x67\\x2f\\x64\\x63\\x2f\\x65\\x6c\\x65\\x6d\\x65\\x6e\\x74\\x73\\x2f\\x31\\\n\\x2e\\x31\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x65\\\n\\x78\\x69\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\\n\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x65\\x78\\x69\\x66\\x2f\\x31\\x2e\\\n\\x30\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x74\\x69\\\n\\x66\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\\n\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x74\\x69\\x66\\x66\\x2f\\x31\\x2e\\x30\\\n\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x70\\x68\\x6f\\\n\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\\n\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x70\\x68\\x6f\\x74\\\n\\x6f\\x73\\x68\\x6f\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\\n\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x78\\x6d\\x70\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\\n\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x78\\\n\\x61\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\\n\\x6e\\x73\\x3a\\x78\\x6d\\x70\\x4d\\x4d\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\\n\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x78\\x61\\\n\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x6d\\x6d\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\\n\\x6d\\x6c\\x6e\\x73\\x3a\\x73\\x74\\x45\\x76\\x74\\x3d\\x22\\x68\\x74\\x74\\x70\\\n\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\\n\\x78\\x61\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x73\\x54\\x79\\x70\\x65\\x2f\\x52\\x65\\\n\\x73\\x6f\\x75\\x72\\x63\\x65\\x45\\x76\\x65\\x6e\\x74\\x23\\x22\\x0a\\x20\\x20\\\n\\x20\\x65\\x78\\x69\\x66\\x3a\\x50\\x69\\x78\\x65\\x6c\\x58\\x44\\x69\\x6d\\x65\\\n\\x6e\\x73\\x69\\x6f\\x6e\\x3d\\x22\\x37\\x22\\x0a\\x20\\x20\\x20\\x65\\x78\\x69\\\n\\x66\\x3a\\x50\\x69\\x78\\x65\\x6c\\x59\\x44\\x69\\x6d\\x65\\x6e\\x73\\x69\\x6f\\\n\\x6e\\x3d\\x22\\x31\\x30\\x22\\x0a\\x20\\x20\\x20\\x65\\x78\\x69\\x66\\x3a\\x43\\\n\\x6f\\x6c\\x6f\\x72\\x53\\x70\\x61\\x63\\x65\\x3d\\x22\\x31\\x22\\x0a\\x20\\x20\\\n\\x20\\x74\\x69\\x66\\x66\\x3a\\x49\\x6d\\x61\\x67\\x65\\x57\\x69\\x64\\x74\\x68\\\n\\x3d\\x22\\x37\\x22\\x0a\\x20\\x20\\x20\\x74\\x69\\x66\\x66\\x3a\\x49\\x6d\\x61\\\n\\x67\\x65\\x4c\\x65\\x6e\\x67\\x74\\x68\\x3d\\x22\\x31\\x30\\x22\\x0a\\x20\\x20\\\n\\x20\\x74\\x69\\x66\\x66\\x3a\\x52\\x65\\x73\\x6f\\x6c\\x75\\x74\\x69\\x6f\\x6e\\\n\\x55\\x6e\\x69\\x74\\x3d\\x22\\x32\\x22\\x0a\\x20\\x20\\x20\\x74\\x69\\x66\\x66\\\n\\x3a\\x58\\x52\\x65\\x73\\x6f\\x6c\\x75\\x74\\x69\\x6f\\x6e\\x3d\\x22\\x37\\x32\\\n\\x2e\\x30\\x22\\x0a\\x20\\x20\\x20\\x74\\x69\\x66\\x66\\x3a\\x59\\x52\\x65\\x73\\\n\\x6f\\x6c\\x75\\x74\\x69\\x6f\\x6e\\x3d\\x22\\x37\\x32\\x2e\\x30\\x22\\x0a\\x20\\\n\\x20\\x20\\x70\\x68\\x6f\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3a\\x43\\x6f\\x6c\\x6f\\\n\\x72\\x4d\\x6f\\x64\\x65\\x3d\\x22\\x33\\x22\\x0a\\x20\\x20\\x20\\x70\\x68\\x6f\\\n\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3a\\x49\\x43\\x43\\x50\\x72\\x6f\\x66\\x69\\x6c\\\n\\x65\\x3d\\x22\\x73\\x52\\x47\\x42\\x20\\x49\\x45\\x43\\x36\\x31\\x39\\x36\\x36\\\n\\x2d\\x32\\x2e\\x31\\x22\\x0a\\x20\\x20\\x20\\x78\\x6d\\x70\\x3a\\x4d\\x6f\\x64\\\n\\x69\\x66\\x79\\x44\\x61\\x74\\x65\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x30\\x35\\\n\\x2d\\x33\\x31\\x54\\x31\\x32\\x3a\\x34\\x33\\x3a\\x33\\x35\\x2b\\x30\\x32\\x3a\\\n\\x30\\x30\\x22\\x0a\\x20\\x20\\x20\\x78\\x6d\\x70\\x3a\\x4d\\x65\\x74\\x61\\x64\\\n\\x61\\x74\\x61\\x44\\x61\\x74\\x65\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x30\\x35\\\n\\x2d\\x33\\x31\\x54\\x31\\x32\\x3a\\x34\\x33\\x3a\\x33\\x35\\x2b\\x30\\x32\\x3a\\\n\\x30\\x30\\x22\\x3e\\x0a\\x20\\x20\\x20\\x3c\\x64\\x63\\x3a\\x74\\x69\\x74\\x6c\\\n\\x65\\x3e\\x0a\\x20\\x20\\x20\\x20\\x3c\\x72\\x64\\x66\\x3a\\x41\\x6c\\x74\\x3e\\\n\\x0a\\x20\\x20\\x20\\x20\\x20\\x3c\\x72\\x64\\x66\\x3a\\x6c\\x69\\x20\\x78\\x6d\\\n\\x6c\\x3a\\x6c\\x61\\x6e\\x67\\x3d\\x22\\x78\\x2d\\x64\\x65\\x66\\x61\\x75\\x6c\\\n\\x74\\x22\\x3e\\x62\\x72\\x61\\x6e\\x63\\x68\\x5f\\x63\\x6c\\x6f\\x73\\x65\\x3c\\\n\\x2f\\x72\\x64\\x66\\x3a\\x6c\\x69\\x3e\\x0a\\x20\\x20\\x20\\x20\\x3c\\x2f\\x72\\\n\\x64\\x66\\x3a\\x41\\x6c\\x74\\x3e\\x0a\\x20\\x20\\x20\\x3c\\x2f\\x64\\x63\\x3a\\\n\\x74\\x69\\x74\\x6c\\x65\\x3e\\x0a\\x20\\x20\\x20\\x3c\\x78\\x6d\\x70\\x4d\\x4d\\\n\\x3a\\x48\\x69\\x73\\x74\\x6f\\x72\\x79\\x3e\\x0a\\x20\\x20\\x20\\x20\\x3c\\x72\\\n\\x64\\x66\\x3a\\x53\\x65\\x71\\x3e\\x0a\\x20\\x20\\x20\\x20\\x20\\x3c\\x72\\x64\\\n\\x66\\x3a\\x6c\\x69\\x0a\\x20\\x20\\x20\\x20\\x20\\x20\\x73\\x74\\x45\\x76\\x74\\\n\\x3a\\x61\\x63\\x74\\x69\\x6f\\x6e\\x3d\\x22\\x70\\x72\\x6f\\x64\\x75\\x63\\x65\\\n\\x64\\x22\\x0a\\x20\\x20\\x20\\x20\\x20\\x20\\x73\\x74\\x45\\x76\\x74\\x3a\\x73\\\n\\x6f\\x66\\x74\\x77\\x61\\x72\\x65\\x41\\x67\\x65\\x6e\\x74\\x3d\\x22\\x41\\x66\\\n\\x66\\x69\\x6e\\x69\\x74\\x79\\x20\\x44\\x65\\x73\\x69\\x67\\x6e\\x65\\x72\\x20\\\n\\x31\\x2e\\x39\\x2e\\x32\\x22\\x0a\\x20\\x20\\x20\\x20\\x20\\x20\\x73\\x74\\x45\\\n\\x76\\x74\\x3a\\x77\\x68\\x65\\x6e\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x30\\x35\\\n\\x2d\\x33\\x31\\x54\\x31\\x32\\x3a\\x34\\x33\\x3a\\x33\\x35\\x2b\\x30\\x32\\x3a\\\n\\x30\\x30\\x22\\x2f\\x3e\\x0a\\x20\\x20\\x20\\x20\\x3c\\x2f\\x72\\x64\\x66\\x3a\\\n\\x53\\x65\\x71\\x3e\\x0a\\x20\\x20\\x20\\x3c\\x2f\\x78\\x6d\\x70\\x4d\\x4d\\x3a\\\n\\x48\\x69\\x73\\x74\\x6f\\x72\\x79\\x3e\\x0a\\x20\\x20\\x3c\\x2f\\x72\\x64\\x66\\\n\\x3a\\x44\\x65\\x73\\x63\\x72\\x69\\x70\\x74\\x69\\x6f\\x6e\\x3e\\x0a\\x20\\x3c\\\n\\x2f\\x72\\x64\\x66\\x3a\\x52\\x44\\x46\\x3e\\x0a\\x3c\\x2f\\x78\\x3a\\x78\\x6d\\\n\\x70\\x6d\\x65\\x74\\x61\\x3e\\x0a\\x3c\\x3f\\x78\\x70\\x61\\x63\\x6b\\x65\\x74\\\n\\x20\\x65\\x6e\\x64\\x3d\\x22\\x72\\x22\\x3f\\x3e\\x24\\xe1\\x35\\x97\\x00\\x00\\\n\\x01\\x83\\x69\\x43\\x43\\x50\\x73\\x52\\x47\\x42\\x20\\x49\\x45\\x43\\x36\\x31\\\n\\x39\\x36\\x36\\x2d\\x32\\x2e\\x31\\x00\\x00\\x28\\x91\\x75\\x91\\xcf\\x2b\\x44\\\n\\x51\\x14\\xc7\\x3f\\x66\\x68\\xfc\\x18\\x8d\\x62\\x61\\x31\\x65\\x12\\x16\\x42\\\n\\x83\\x12\\x1b\\x8b\\x99\\x18\\x0a\\x8b\\x99\\x51\\x7e\\x6d\\x66\\x9e\\x79\\x33\\\n\\x6a\\xde\\x78\\xbd\\x37\\xd2\\x64\\xab\\x6c\\xa7\\x28\\xb1\\xf1\\x6b\\xc1\\x5f\\\n\\xc0\\x56\\x59\\x2b\\x45\\xa4\\x64\\xa7\\xac\\x89\\x0d\\x7a\\xce\\x9b\\x51\\x23\\\n\\x99\\x73\\x3b\\xf7\\x7c\\xee\\xf7\\xde\\x73\\xba\\xf7\\x5c\\x70\\x44\\xd3\\x8a\\\n\\x66\\x56\\xfa\\x41\\xcb\\x64\\x8d\\x70\\x28\\xe0\\x9b\\x99\\x9d\\xf3\\xb9\\x9e\\\n\\xa8\\xa2\\x85\\x1a\\x3a\\xf1\\xc6\\x14\\x53\\x9f\\x8c\\x8c\\x46\\x29\\x6b\\xef\\\n\\xb7\\x54\\xd8\\xf1\\xba\\xdb\\xae\\x55\\xfe\\xdc\\xbf\\x56\\xb7\\x98\\x30\\x15\\\n\\xa8\\xa8\\x16\\x1e\\x56\\x74\\x23\\x2b\\x3c\\x26\\x3c\\xb1\\x9a\\xd5\\x6d\\xde\\\n\\x12\\x6e\\x52\\x52\\xb1\\x45\\xe1\\x13\\xe1\\x2e\\x43\\x2e\\x28\\x7c\\x63\\xeb\\\n\\xf1\\x22\\x3f\\xdb\\x9c\\x2c\\xf2\\xa7\\xcd\\x46\\x34\\x1c\\x04\\x47\\x83\\xb0\\\n\\x2f\\xf9\\x8b\\xe3\\xbf\\x58\\x49\\x19\\x9a\\xb0\\xbc\\x9c\\x36\\x2d\\xbd\\xa2\\\n\\xfc\\xdc\\xc7\\x7e\\x89\\x3b\\x91\\x99\\x8e\\x48\\x6c\\x15\\xf7\\x62\\x12\\x26\\\n\\x44\\x00\\x1f\\xe3\\x8c\\x10\\x64\\x80\\x5e\\x86\\x64\\x1e\\xa0\\x9b\\x3e\\x7a\\\n\\x64\\x45\\x99\\x7c\\x7f\\x21\\x7f\\x8a\\x65\\xc9\\x55\\x64\\xd6\\xc9\\x61\\xb0\\\n\\x44\\x92\\x14\\x59\\xba\\x44\\x5d\\x91\\xea\\x09\\x89\\xaa\\xe8\\x09\\x19\\x69\\\n\\x72\\x76\\xff\\xff\\xf6\\xd5\\x54\\xfb\\xfb\\x8a\\xd5\\xdd\\x01\\xa8\\x7a\\xb4\\\n\\xac\\xd7\\x76\\x70\\x6d\\xc2\\x57\\xde\\xb2\\x3e\\x0e\\x2c\\xeb\\xeb\\x10\\x9c\\\n\\x0f\\x70\\x9e\\x29\\xe5\\x2f\\xef\\xc3\\xe0\\x9b\\xe8\\xf9\\x92\\xd6\\xb6\\x07\\\n\\x9e\\x75\\x38\\xbd\\x28\\x69\\xf1\\x6d\\x38\\xdb\\x80\\xe6\\x7b\\x3d\\x66\\xc4\\\n\\x0a\\x92\\x53\\xdc\\xa1\\xaa\\xf0\\x72\\x0c\\xf5\\xb3\\xd0\\x78\\x05\\xb5\\xf3\\\n\\xc5\\x9e\\xfd\\xec\\x73\\x74\\x07\\xd1\\x35\\xf9\\xaa\\x4b\\xd8\\xd9\\x85\\x0e\\\n\\x39\\xef\\x59\\xf8\\x06\\x8e\\xfd\\x67\\xf8\\xfd\\x8a\\x18\\x97\\x00\\x00\\x00\\\n\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\\n\\x9c\\x18\\x00\\x00\\x00\\x72\\x49\\x44\\x41\\x54\\x18\\x95\\x6d\\xcf\\x31\\x0a\\\n\\xc2\\x50\\x14\\x44\\xd1\\xe8\\x02\\xb4\\x57\\x08\\xd6\\x49\\x61\\x99\\x4a\\x43\\\n\\x74\\x15\\x82\\xab\\x49\\x36\\x28\\xee\\x40\\x04\\xdb\\xa8\\x95\\x58\\x78\\x2c\\\n\\xf2\\x09\\xe1\\xf3\\x07\\xa6\\x9a\\xfb\\xe0\\xbe\\x0c\\x1b\\xb4\\x58\\x64\\x71\\\n\\x70\\x30\\xe4\\x82\\x55\\x0a\\x38\\xe3\\x8b\\x1b\\x8a\\x14\\x70\\xc4\\x1b\\x3d\\\n\\x76\\x29\\x60\\x8b\\x07\\x3e\\xa8\\xe6\\xd1\\xfe\\x0b\\x9d\\x85\\x8e\\x57\\x0d\\\n\\x5e\\x78\\xa2\\x9e\\x0e\\xa7\\x20\\x74\\x47\\x39\\x1d\\xf6\\xe1\\x95\\x2b\\xd6\\\n\\xb1\\x44\\x8e\\x0e\\xcb\\x58\\xf0\\x0f\\x52\\x8a\\x79\\x18\\xdc\\xe2\\x02\\x70\\\n\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x07\\x06\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x0a\\x00\\x00\\x00\\x07\\x08\\x06\\x00\\x00\\x00\\x31\\xac\\xdc\\x63\\\n\\x00\\x00\\x04\\xb0\\x69\\x54\\x58\\x74\\x58\\x4d\\x4c\\x3a\\x63\\x6f\\x6d\\x2e\\\n\\x61\\x64\\x6f\\x62\\x65\\x2e\\x78\\x6d\\x70\\x00\\x00\\x00\\x00\\x00\\x3c\\x3f\\\n\\x78\\x70\\x61\\x63\\x6b\\x65\\x74\\x20\\x62\\x65\\x67\\x69\\x6e\\x3d\\x22\\xef\\\n\\xbb\\xbf\\x22\\x20\\x69\\x64\\x3d\\x22\\x57\\x35\\x4d\\x30\\x4d\\x70\\x43\\x65\\\n\\x68\\x69\\x48\\x7a\\x72\\x65\\x53\\x7a\\x4e\\x54\\x63\\x7a\\x6b\\x63\\x39\\x64\\\n\\x22\\x3f\\x3e\\x0a\\x3c\\x78\\x3a\\x78\\x6d\\x70\\x6d\\x65\\x74\\x61\\x20\\x78\\\n\\x6d\\x6c\\x6e\\x73\\x3a\\x78\\x3d\\x22\\x61\\x64\\x6f\\x62\\x65\\x3a\\x6e\\x73\\\n\\x3a\\x6d\\x65\\x74\\x61\\x2f\\x22\\x20\\x78\\x3a\\x78\\x6d\\x70\\x74\\x6b\\x3d\\\n\\x22\\x58\\x4d\\x50\\x20\\x43\\x6f\\x72\\x65\\x20\\x35\\x2e\\x35\\x2e\\x30\\x22\\\n\\x3e\\x0a\\x20\\x3c\\x72\\x64\\x66\\x3a\\x52\\x44\\x46\\x20\\x78\\x6d\\x6c\\x6e\\\n\\x73\\x3a\\x72\\x64\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x77\\x77\\\n\\x77\\x2e\\x77\\x33\\x2e\\x6f\\x72\\x67\\x2f\\x31\\x39\\x39\\x39\\x2f\\x30\\x32\\\n\\x2f\\x32\\x32\\x2d\\x72\\x64\\x66\\x2d\\x73\\x79\\x6e\\x74\\x61\\x78\\x2d\\x6e\\\n\\x73\\x23\\x22\\x3e\\x0a\\x20\\x20\\x3c\\x72\\x64\\x66\\x3a\\x44\\x65\\x73\\x63\\\n\\x72\\x69\\x70\\x74\\x69\\x6f\\x6e\\x20\\x72\\x64\\x66\\x3a\\x61\\x62\\x6f\\x75\\\n\\x74\\x3d\\x22\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x65\\\n\\x78\\x69\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\\n\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x65\\x78\\x69\\x66\\x2f\\x31\\x2e\\\n\\x30\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x74\\x69\\\n\\x66\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\\n\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x74\\x69\\x66\\x66\\x2f\\x31\\x2e\\x30\\\n\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x70\\x68\\x6f\\\n\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\\n\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x70\\x68\\x6f\\x74\\\n\\x6f\\x73\\x68\\x6f\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\\n\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x78\\x6d\\x70\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\\n\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x78\\\n\\x61\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\\n\\x6e\\x73\\x3a\\x78\\x6d\\x70\\x4d\\x4d\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\\n\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x78\\x61\\\n\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x6d\\x6d\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\\n\\x6d\\x6c\\x6e\\x73\\x3a\\x73\\x74\\x45\\x76\\x74\\x3d\\x22\\x68\\x74\\x74\\x70\\\n\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\\n\\x78\\x61\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x73\\x54\\x79\\x70\\x65\\x2f\\x52\\x65\\\n\\x73\\x6f\\x75\\x72\\x63\\x65\\x45\\x76\\x65\\x6e\\x74\\x23\\x22\\x0a\\x20\\x20\\\n\\x20\\x65\\x78\\x69\\x66\\x3a\\x50\\x69\\x78\\x65\\x6c\\x58\\x44\\x69\\x6d\\x65\\\n\\x6e\\x73\\x69\\x6f\\x6e\\x3d\\x22\\x31\\x30\\x22\\x0a\\x20\\x20\\x20\\x65\\x78\\\n\\x69\\x66\\x3a\\x50\\x69\\x78\\x65\\x6c\\x59\\x44\\x69\\x6d\\x65\\x6e\\x73\\x69\\\n\\x6f\\x6e\\x3d\\x22\\x37\\x22\\x0a\\x20\\x20\\x20\\x65\\x78\\x69\\x66\\x3a\\x43\\\n\\x6f\\x6c\\x6f\\x72\\x53\\x70\\x61\\x63\\x65\\x3d\\x22\\x31\\x22\\x0a\\x20\\x20\\\n\\x20\\x74\\x69\\x66\\x66\\x3a\\x49\\x6d\\x61\\x67\\x65\\x57\\x69\\x64\\x74\\x68\\\n\\x3d\\x22\\x31\\x30\\x22\\x0a\\x20\\x20\\x20\\x74\\x69\\x66\\x66\\x3a\\x49\\x6d\\\n\\x61\\x67\\x65\\x4c\\x65\\x6e\\x67\\x74\\x68\\x3d\\x22\\x37\\x22\\x0a\\x20\\x20\\\n\\x20\\x74\\x69\\x66\\x66\\x3a\\x52\\x65\\x73\\x6f\\x6c\\x75\\x74\\x69\\x6f\\x6e\\\n\\x55\\x6e\\x69\\x74\\x3d\\x22\\x32\\x22\\x0a\\x20\\x20\\x20\\x74\\x69\\x66\\x66\\\n\\x3a\\x58\\x52\\x65\\x73\\x6f\\x6c\\x75\\x74\\x69\\x6f\\x6e\\x3d\\x22\\x37\\x32\\\n\\x2e\\x30\\x22\\x0a\\x20\\x20\\x20\\x74\\x69\\x66\\x66\\x3a\\x59\\x52\\x65\\x73\\\n\\x6f\\x6c\\x75\\x74\\x69\\x6f\\x6e\\x3d\\x22\\x37\\x32\\x2e\\x30\\x22\\x0a\\x20\\\n\\x20\\x20\\x70\\x68\\x6f\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3a\\x43\\x6f\\x6c\\x6f\\\n\\x72\\x4d\\x6f\\x64\\x65\\x3d\\x22\\x33\\x22\\x0a\\x20\\x20\\x20\\x70\\x68\\x6f\\\n\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3a\\x49\\x43\\x43\\x50\\x72\\x6f\\x66\\x69\\x6c\\\n\\x65\\x3d\\x22\\x73\\x52\\x47\\x42\\x20\\x49\\x45\\x43\\x36\\x31\\x39\\x36\\x36\\\n\\x2d\\x32\\x2e\\x31\\x22\\x0a\\x20\\x20\\x20\\x78\\x6d\\x70\\x3a\\x4d\\x6f\\x64\\\n\\x69\\x66\\x79\\x44\\x61\\x74\\x65\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x30\\x35\\\n\\x2d\\x33\\x31\\x54\\x31\\x32\\x3a\\x33\\x30\\x3a\\x31\\x31\\x2b\\x30\\x32\\x3a\\\n\\x30\\x30\\x22\\x0a\\x20\\x20\\x20\\x78\\x6d\\x70\\x3a\\x4d\\x65\\x74\\x61\\x64\\\n\\x61\\x74\\x61\\x44\\x61\\x74\\x65\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x30\\x35\\\n\\x2d\\x33\\x31\\x54\\x31\\x32\\x3a\\x33\\x30\\x3a\\x31\\x31\\x2b\\x30\\x32\\x3a\\\n\\x30\\x30\\x22\\x3e\\x0a\\x20\\x20\\x20\\x3c\\x78\\x6d\\x70\\x4d\\x4d\\x3a\\x48\\\n\\x69\\x73\\x74\\x6f\\x72\\x79\\x3e\\x0a\\x20\\x20\\x20\\x20\\x3c\\x72\\x64\\x66\\\n\\x3a\\x53\\x65\\x71\\x3e\\x0a\\x20\\x20\\x20\\x20\\x20\\x3c\\x72\\x64\\x66\\x3a\\\n\\x6c\\x69\\x0a\\x20\\x20\\x20\\x20\\x20\\x20\\x73\\x74\\x45\\x76\\x74\\x3a\\x61\\\n\\x63\\x74\\x69\\x6f\\x6e\\x3d\\x22\\x70\\x72\\x6f\\x64\\x75\\x63\\x65\\x64\\x22\\\n\\x0a\\x20\\x20\\x20\\x20\\x20\\x20\\x73\\x74\\x45\\x76\\x74\\x3a\\x73\\x6f\\x66\\\n\\x74\\x77\\x61\\x72\\x65\\x41\\x67\\x65\\x6e\\x74\\x3d\\x22\\x41\\x66\\x66\\x69\\\n\\x6e\\x69\\x74\\x79\\x20\\x44\\x65\\x73\\x69\\x67\\x6e\\x65\\x72\\x20\\x31\\x2e\\\n\\x39\\x2e\\x32\\x22\\x0a\\x20\\x20\\x20\\x20\\x20\\x20\\x73\\x74\\x45\\x76\\x74\\\n\\x3a\\x77\\x68\\x65\\x6e\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x30\\x35\\x2d\\x33\\\n\\x31\\x54\\x31\\x32\\x3a\\x33\\x30\\x3a\\x31\\x31\\x2b\\x30\\x32\\x3a\\x30\\x30\\\n\\x22\\x2f\\x3e\\x0a\\x20\\x20\\x20\\x20\\x3c\\x2f\\x72\\x64\\x66\\x3a\\x53\\x65\\\n\\x71\\x3e\\x0a\\x20\\x20\\x20\\x3c\\x2f\\x78\\x6d\\x70\\x4d\\x4d\\x3a\\x48\\x69\\\n\\x73\\x74\\x6f\\x72\\x79\\x3e\\x0a\\x20\\x20\\x3c\\x2f\\x72\\x64\\x66\\x3a\\x44\\\n\\x65\\x73\\x63\\x72\\x69\\x70\\x74\\x69\\x6f\\x6e\\x3e\\x0a\\x20\\x3c\\x2f\\x72\\\n\\x64\\x66\\x3a\\x52\\x44\\x46\\x3e\\x0a\\x3c\\x2f\\x78\\x3a\\x78\\x6d\\x70\\x6d\\\n\\x65\\x74\\x61\\x3e\\x0a\\x3c\\x3f\\x78\\x70\\x61\\x63\\x6b\\x65\\x74\\x20\\x65\\\n\\x6e\\x64\\x3d\\x22\\x72\\x22\\x3f\\x3e\\x85\\x9d\\x9f\\x08\\x00\\x00\\x01\\x83\\\n\\x69\\x43\\x43\\x50\\x73\\x52\\x47\\x42\\x20\\x49\\x45\\x43\\x36\\x31\\x39\\x36\\\n\\x36\\x2d\\x32\\x2e\\x31\\x00\\x00\\x28\\x91\\x75\\x91\\xcf\\x2b\\x44\\x51\\x14\\\n\\xc7\\x3f\\x66\\x68\\xfc\\x18\\x8d\\x62\\x61\\x31\\x65\\x12\\x16\\x42\\x83\\x12\\\n\\x1b\\x8b\\x99\\x18\\x0a\\x8b\\x99\\x51\\x7e\\x6d\\x66\\x9e\\x79\\x33\\x6a\\xde\\\n\\x78\\xbd\\x37\\xd2\\x64\\xab\\x6c\\xa7\\x28\\xb1\\xf1\\x6b\\xc1\\x5f\\xc0\\x56\\\n\\x59\\x2b\\x45\\xa4\\x64\\xa7\\xac\\x89\\x0d\\x7a\\xce\\x9b\\x51\\x23\\x99\\x73\\\n\\x3b\\xf7\\x7c\\xee\\xf7\\xde\\x73\\xba\\xf7\\x5c\\x70\\x44\\xd3\\x8a\\x66\\x56\\\n\\xfa\\x41\\xcb\\x64\\x8d\\x70\\x28\\xe0\\x9b\\x99\\x9d\\xf3\\xb9\\x9e\\xa8\\xa2\\\n\\x85\\x1a\\x3a\\xf1\\xc6\\x14\\x53\\x9f\\x8c\\x8c\\x46\\x29\\x6b\\xef\\xb7\\x54\\\n\\xd8\\xf1\\xba\\xdb\\xae\\x55\\xfe\\xdc\\xbf\\x56\\xb7\\x98\\x30\\x15\\xa8\\xa8\\\n\\x16\\x1e\\x56\\x74\\x23\\x2b\\x3c\\x26\\x3c\\xb1\\x9a\\xd5\\x6d\\xde\\x12\\x6e\\\n\\x52\\x52\\xb1\\x45\\xe1\\x13\\xe1\\x2e\\x43\\x2e\\x28\\x7c\\x63\\xeb\\xf1\\x22\\\n\\x3f\\xdb\\x9c\\x2c\\xf2\\xa7\\xcd\\x46\\x34\\x1c\\x04\\x47\\x83\\xb0\\x2f\\xf9\\\n\\x8b\\xe3\\xbf\\x58\\x49\\x19\\x9a\\xb0\\xbc\\x9c\\x36\\x2d\\xbd\\xa2\\xfc\\xdc\\\n\\xc7\\x7e\\x89\\x3b\\x91\\x99\\x8e\\x48\\x6c\\x15\\xf7\\x62\\x12\\x26\\x44\\x00\\\n\\x1f\\xe3\\x8c\\x10\\x64\\x80\\x5e\\x86\\x64\\x1e\\xa0\\x9b\\x3e\\x7a\\x64\\x45\\\n\\x99\\x7c\\x7f\\x21\\x7f\\x8a\\x65\\xc9\\x55\\x64\\xd6\\xc9\\x61\\xb0\\x44\\x92\\\n\\x14\\x59\\xba\\x44\\x5d\\x91\\xea\\x09\\x89\\xaa\\xe8\\x09\\x19\\x69\\x72\\x76\\\n\\xff\\xff\\xf6\\xd5\\x54\\xfb\\xfb\\x8a\\xd5\\xdd\\x01\\xa8\\x7a\\xb4\\xac\\xd7\\\n\\x76\\x70\\x6d\\xc2\\x57\\xde\\xb2\\x3e\\x0e\\x2c\\xeb\\xeb\\x10\\x9c\\x0f\\x70\\\n\\x9e\\x29\\xe5\\x2f\\xef\\xc3\\xe0\\x9b\\xe8\\xf9\\x92\\xd6\\xb6\\x07\\x9e\\x75\\\n\\x38\\xbd\\x28\\x69\\xf1\\x6d\\x38\\xdb\\x80\\xe6\\x7b\\x3d\\x66\\xc4\\x0a\\x92\\\n\\x53\\xdc\\xa1\\xaa\\xf0\\x72\\x0c\\xf5\\xb3\\xd0\\x78\\x05\\xb5\\xf3\\xc5\\x9e\\\n\\xfd\\xec\\x73\\x74\\x07\\xd1\\x35\\xf9\\xaa\\x4b\\xd8\\xd9\\x85\\x0e\\x39\\xef\\\n\\x59\\xf8\\x06\\x8e\\xfd\\x67\\xf8\\xfd\\x8a\\x18\\x97\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x6d\\x49\\x44\\x41\\x54\\x18\\x95\\x75\\xcf\\xc1\\x09\\xc2\\x50\\\n\\x10\\x84\\xe1\\xd7\\x85\\x07\\x9b\\xd0\\x43\\x40\\xd2\\x82\\x78\\x14\\x7b\\x30\\\n\\x57\\x21\\x8d\\x84\\x60\\x3f\\x62\\x4b\\x7a\\x48\\xcc\\x97\\x83\\xfb\\x30\\x04\\\n\\xdf\\x9c\\x86\\x7f\\x67\\x99\\xdd\\x84\\x0d\\xaa\\x54\\x10\\x6a\\x6c\\x13\\x1e\\\n\\xbe\\xba\\xfe\\x09\\x35\\x31\\x7b\\xe6\\x8d\\x0f\\x26\\x1c\\x17\\xa1\\x53\\xb0\\\n\\x11\\x87\\x0c\\x2f\\x01\\x07\\xec\\xb0\\x0f\\x3f\\xe1\\xbc\\xae\\x69\\xa3\\xe6\\\n\\x85\\x77\\xf8\\x5b\\xe9\\xf0\\xbb\\x9f\\xfa\\xd2\\x83\\x39\\xdc\\xa3\\x5b\\xf3\\\n\\x19\\x2e\\xa8\\x89\\xb5\\x30\\xf7\\x43\\xa0\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\\n\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x01\\xdc\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x30\\x00\\x00\\x00\\x30\\x08\\x06\\x00\\x00\\x00\\x57\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x8e\\x49\\x44\\x41\\x54\\x68\\x81\\xed\\\n\\x9a\\xaf\\x4e\\xc3\\x50\\x14\\x87\\xbf\\x6e\\x1d\\x0a\\x1c\\x41\\x1e\\x83\\x04\\\n\\xc4\\x0c\\x6a\\x41\\x10\\x04\\x82\\x80\\x9e\\xe7\\x05\\x78\\x80\\x21\\x78\\x01\\\n\\x5e\\x00\\x8f\\xc2\\x00\\x72\\x41\\x90\\x3d\\xc2\\x40\\x31\\x73\\xe4\\x32\\x14\\\n\\xc1\\xec\\x4f\\x82\\x68\\x1b\\xb6\\x65\\xed\\x28\\xeb\\x76\\xda\\xe5\\x7e\\xae\\\n\\xf7\\x5c\\xf1\\xfb\\xda\\x7b\\x6f\\x9a\\xdc\\xe3\\x11\\xa2\\xaa\\xdb\\xc0\\x35\\\n\\x50\\x03\\xf6\\x81\\x0a\\xf9\\x62\\x00\\xb4\\x81\\x16\\x70\\x23\\x22\\x3d\\x00\\\n\\x0f\\x40\\x55\\x8f\\x81\\x7b\\x60\\xc7\\x2c\\x5e\\x3a\\xba\\x40\\x5d\\x44\\x5e\\\n\\xbc\\xf0\\xcd\\xbf\\x51\\x9c\\xf0\\x11\\x5d\\x60\\xaf\\x44\\xb0\\x6c\\x8a\\x16\\\n\\x1e\\x82\\xcc\\x0d\\x9f\\x60\\xcd\\x4f\\x33\\x5a\\x71\\x98\\xbf\\x52\\x9e\\x7a\\\n\\xae\\xf9\\x04\\x1b\\x76\\x9c\\x91\\x88\\xf8\\x2b\\x0a\\x94\\x0a\\x55\\x1d\\x32\\\n\\x29\\x71\\x50\\x22\\x7f\\xa7\\x4d\\x1a\\x2a\\x25\\xeb\\x04\\x8b\\xe2\\x04\\xac\\\n\\x71\\x02\\xd6\\x38\\x01\\x6b\\x9c\\x80\\x35\\x4e\\xc0\\x1a\\x27\\x60\\xcd\\xdc\\\n\\xdf\\x66\\x55\\x3d\\x01\\x0e\\x81\\xcd\\xe5\\xc7\\x99\\x60\\x08\\xbc\\x03\\x4f\\\n\\x22\\xf2\\x1d\\x37\\x29\\x56\\x40\\x55\\x7d\\xe0\\x01\\x38\\xcf\\x3e\\x5b\\x2a\\\n\\x3a\\xaa\\x7a\\x2a\\x22\\x1f\\xb3\\x8a\\x49\\x4b\\xe8\\x0a\\xfb\\xf0\\x00\\xbb\\\n\\xc0\\x5d\\x5c\\x31\\x49\\xe0\\x2c\\xfb\\x2c\\xff\\xe6\\x48\\x55\\xb7\\x66\\x15\\\n\\x0a\\xbf\\x89\\x93\\x04\\x9e\\x57\\x96\\x62\\x3e\\xaf\\x22\\xf2\\x35\\xab\\x90\\\n\\x24\\x70\\x0b\\x3c\\x2e\\x27\\x4f\\x2a\\x3a\\xc0\\x65\\x5c\\x31\\xf6\\x14\\x12\\\n\\x91\\x21\\x70\\x51\\xd8\\x63\\x34\\x42\\x44\\x9a\\x40\\x33\\xc3\\x60\\x99\\xb2\\\n\\xd6\\x9b\\xb8\\x10\\x38\\x01\\x6b\\x9c\\x80\\x35\\x4e\\xc0\\x1a\\x27\\x60\\x8d\\\n\\x13\\xb0\\x66\\x2d\\x04\\x06\\xd6\\x21\\x16\\xa0\\xef\\x13\\x5c\\xdf\\x57\\xc7\\\n\\x06\\xcb\\xe1\\x6d\\x60\\x1e\\x99\\xbe\\x66\\x6d\\xfb\\x04\\xbd\\x07\\xd5\\x39\\\n\\x13\\xf3\\x4a\\xab\\xf8\\xad\\x06\\x61\\xd7\\x47\\x3d\\x1c\\x28\\x0a\\x51\\xb3\\\n\\x47\\xcf\\x8b\\x46\\xc2\\x2f\\xd1\\xe0\\xb7\\xdd\\x66\\xc3\\x28\\x5c\\x1c\\x7d\\\n\\x26\\xdb\\x6d\\x3e\\x01\\x7e\\x00\\x25\\xf8\\x5a\\x43\\x55\\x4e\\x3a\\x7f\\x00\\\n\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x01\\xef\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x30\\x00\\x00\\x00\\x30\\x08\\x06\\x00\\x00\\x00\\x57\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\xa1\\x49\\x44\\x41\\x54\\x68\\x81\\xed\\\n\\x9a\\xbf\\x4e\\xc2\\x50\\x14\\x87\\xbf\\x96\\xe2\\xa4\\x9b\\x71\\xbc\\x8b\\x1b\\\n\\xea\\xc0\\xe2\\x44\\x1c\\x8c\\x83\\x83\\xd1\\x81\\x89\\x84\\xd1\\x17\\xf0\\x01\\\n\\x70\\xc0\\x07\\xf0\\x05\\x1c\\x49\\x9c\\xba\\xa8\\x23\\x71\\x30\\x3c\\x02\\x76\\\n\\x92\\xe5\\x8e\\x04\\x27\\xe3\\xc2\\x9f\\xc4\\xa1\\x6d\\x04\\x42\\x8b\\x95\\xc2\\\n\\xa1\\xe4\\x7e\\x5b\\x7b\\xee\\xf0\\xfb\\x9a\\x7b\\x6f\\x9a\\x9c\\x63\\x11\\x50\\\n\\x75\\xbd\\x5d\\xe0\\x16\\x28\\x01\\x87\\x40\\x9e\\xf5\\x62\\x00\\xb4\\x81\\x16\\\n\\x50\\x6f\\x94\\x0b\\x3d\\x00\\x0b\\xa0\\xea\\x7a\\xa7\\xc0\\x23\\xb0\\x27\\x16\\\n\\x2f\\x19\\x5d\\xa0\\xd2\\x28\\x17\\x5e\\xad\\xe0\\xcb\\xbf\\x93\\x9d\\xf0\\x21\\\n\\x5d\\xe0\\xc0\\xc6\\xdf\\x36\\x59\\x0b\\x0f\\x7e\\xe6\\x9a\\x83\\xbf\\xe7\\xa7\\\n\\x19\\xad\\x38\\xcc\\x5f\\xc9\\x4d\\x3d\\x97\\x1c\\xfc\\x03\\x3b\\xce\\xa8\\x51\\\n\\x2e\\x38\\x2b\\x0a\\x94\\x88\\xaa\\xeb\\x0d\\x99\\x94\\x38\\xb2\\x59\\xbf\\xdb\\\n\\x26\\x09\\x79\\x5b\\x3a\\xc1\\xa2\\x18\\x01\\x69\\x8c\\x80\\x34\\x46\\x40\\x1a\\\n\\x23\\x20\\x8d\\x11\\x90\\xc6\\x08\\x48\\x33\\xf7\\xb7\\x59\\x6b\\x7d\\x06\\x1c\\\n\\x03\\xdb\\xcb\\x8f\\x33\\xc1\\x10\\xf0\\x80\\x67\\xa5\\xd4\\x77\\xd4\\xa2\\x48\\\n\\x01\\xad\\xb5\\x03\\xb8\\xc0\\x65\\xfa\\xd9\\x12\\xd1\\xd1\\x5a\\x9f\\x2b\\xa5\\\n\\x3e\\x66\\x15\\xe3\\xb6\\xd0\\x0d\\xf2\\xe1\\x01\\xf6\\x81\\x87\\xa8\\x62\\x9c\\\n\\xc0\\x45\\xfa\\x59\\xfe\\xcd\\x89\\xd6\\x7a\\x67\\x56\\x21\\xf3\\x87\\x38\\x4e\\\n\\xe0\\x65\\x65\\x29\\xe6\\xf3\\xa6\\x94\\xfa\\x9a\\x55\\x88\\x13\\xb8\\x07\\x9e\\\n\\x96\\x93\\x27\\x11\\x1d\\xe0\\x3a\\xaa\\x18\\x79\\x0b\\x29\\xa5\\x86\\xc0\\x55\\\n\\x66\\xaf\\xd1\\x10\\xa5\\x54\\x13\\x68\\xa6\\x18\\x2c\\x55\\x36\\xfa\\x10\\x67\\\n\\x02\\x23\\x20\\x8d\\x11\\x90\\xc6\\x08\\x48\\x63\\x04\\xa4\\x31\\x02\\xd2\\x6c\\\n\\x84\\xc0\\x40\\x3a\\xc4\\x02\\xf4\\x1d\\xfc\\xf6\\x7d\\x71\\xec\\x65\\x2e\\xe8\\\n\\x06\\xae\\x23\\xd3\\x6d\\xd6\\xb6\\x83\\x3f\\x7b\\x50\\x9c\\xb3\\x70\\x5d\\x69\\\n\\xd9\\x40\\x1d\\xbf\\x6d\\x9f\\x35\\xba\\xc0\\x9d\\x1d\\x4c\\x7d\\x54\\xc8\\x96\\\n\\x44\\x38\\xec\\xd1\\xb3\\xc2\\x37\\xc1\\xd0\\x47\\x8d\\xdf\\x71\\x9b\\x2d\\xa1\\\n\\x70\\x51\\xf4\\x99\\x1c\\xb7\\xf9\\x04\\xf8\\x01\\x6f\\xed\\x58\\x63\\x2d\\xfd\\\n\\xb2\\x59\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\xa5\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce\\x7c\\x4e\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\x9c\\x53\\x34\\xfc\\x5d\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x0b\\x02\\x04\\x6d\\\n\\x98\\x1b\\x69\\x00\\x00\\x00\\x29\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\xc0\\\n\\x00\\x8c\\x0c\\x0c\\xff\\xcf\\xa3\\x08\\x18\\x32\\x32\\x30\\x20\\x0b\\x32\\x1a\\\n\\x32\\x30\\x30\\x42\\x98\\x10\\x41\\x46\\x43\\x14\\x13\\x50\\xb5\\xa3\\x01\\x00\\\n\\xd6\\x10\\x07\\xd2\\x2f\\x48\\xdf\\x4a\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\\n\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\xa0\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x14\\x1c\\x1f\\x24\\\n\\xc6\\x09\\x17\\x00\\x00\\x00\\x24\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\x40\\\n\\x05\\xff\\xcf\\xc3\\x58\\x4c\\xc8\\x5c\\x26\\x64\\x59\\x26\\x64\\xc5\\x70\\x0e\\\n\\xa3\\x21\\x9c\\xc3\\x68\\x88\\x61\\x1a\\x0a\\x00\\x00\\x6d\\x84\\x09\\x75\\x37\\\n\\x9e\\xd9\\x23\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\xa5\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce\\x7c\\x4e\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\x9c\\x53\\x34\\xfc\\x5d\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x0b\\x02\\x04\\x6d\\\n\\x98\\x1b\\x69\\x00\\x00\\x00\\x29\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\xc0\\\n\\x00\\x8c\\x0c\\x0c\\xff\\xcf\\xa3\\x08\\x18\\x32\\x32\\x30\\x20\\x0b\\x32\\x1a\\\n\\x32\\x30\\x30\\x42\\x98\\x10\\x41\\x46\\x43\\x14\\x13\\x50\\xb5\\xa3\\x01\\x00\\\n\\xd6\\x10\\x07\\xd2\\x2f\\x48\\xdf\\x4a\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\\n\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x01\\x69\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x30\\x00\\x00\\x00\\x30\\x08\\x06\\x00\\x00\\x00\\x57\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x1b\\x49\\x44\\x41\\x54\\x68\\x81\\xed\\\n\\xda\\xb1\\x6d\\xc2\\x40\\x14\\x87\\xf1\\xcf\\xc7\\x91\\x09\\x50\\x86\\x70\\x42\\\n\\x41\\x4f\\xc5\\x0a\\xae\\x90\\xbc\\x0a\\x29\\xc8\\x2a\\x96\\x52\\x79\\x05\\x2a\\\n\\x46\\x20\\x1e\\xc2\\x82\\x05\\x48\\x90\\x52\\xdc\\x59\\x01\\x4b\\x51\\x62\\x45\\\n\\xe2\\xef\\x93\\xde\\xaf\\xb3\\x45\\xf1\\x3e\\xcb\\xa6\\xb9\\x97\\x11\\x95\\x75\\\n\\x33\\x03\\x5e\\x80\\x25\\xf0\\x0c\\x4c\\x19\\x97\\x0f\\xe0\\x00\\xec\\x81\\x6d\\\n\\x55\\xe4\\x47\\x80\\x0c\\xa0\\xac\\x9b\\x15\\xf0\\x06\\x3c\\xca\\xc6\\x1b\\xa6\\\n\\x05\\xd6\\x55\\x91\\xef\\xb2\\xf8\\xe4\\xdf\\x49\\x67\\xf8\\x4e\\x0b\\x3c\\x39\\\n\\xc2\\x6b\\x93\\xda\\xf0\\x10\\x66\\xde\\x78\\xc2\\x3b\\xdf\\x77\\xb9\\xf3\\x30\\\n\\x7f\\x35\\xe9\\x5d\\x2f\\x3d\\xe1\\x83\\xbd\\x76\\xa9\\x8a\\xdc\\xdf\\x69\\xa0\\\n\\x41\\xca\\xba\\xf9\\xe4\\x36\\x62\\xee\\x18\\xdf\\xbf\\xcd\\x10\\x53\\xa7\\x9e\\\n\\xe0\\xbf\\x2c\\x40\\xcd\\x02\\xd4\\x2c\\x40\\xcd\\x02\\xd4\\x2c\\x40\\xcd\\x02\\\n\\xd4\\x2c\\x40\\xcd\\x02\\xd4\\x2c\\x40\\xcd\\x02\\xd4\\x2c\\x40\\xcd\\x02\\xd4\\\n\\x2c\\x40\\xcd\\x02\\xd4\\x2c\\x40\\xcd\\x02\\xd4\\x2c\\x40\\xcd\\x11\\x4e\\xc0\\\n\\x53\\x75\\xf6\\x84\\xe3\\xfb\\xc5\\xd5\\xcd\\x49\\x3c\\x0d\\x1c\\xa3\\xfe\\x31\\\n\\xeb\\xc1\\x13\\x76\\x0f\\x16\\xbf\\xfc\\x70\\xac\\xf6\\x0e\\xd8\\x12\\x8e\\xed\\\n\\x53\\xd3\\x02\\xaf\\x2e\\x6e\\x7d\\xac\\x49\\x2b\\xa2\\x5b\\xf6\\x38\\x66\\xdd\\\n\\x9d\\xb8\\xf4\\xb1\\xe1\\x7b\\xdd\\xe6\\x41\\x34\\xdc\\x4f\\xce\\xdc\\xae\\xdb\\\n\\x9c\\x00\\xbe\\x00\\x9f\\xf6\\x34\\x3e\\x36\\x4f\\x37\\x81\\x00\\x00\\x00\\x00\\\n\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce\\x7c\\x4e\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x08\\x15\\x3b\\xdc\\\n\\x3b\\x0c\\x9b\\x00\\x00\\x00\\x2a\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\xc0\\\n\\x00\\x8c\\x0c\\x0c\\x73\\x3e\\x20\\x0b\\xa4\\x08\\x30\\x32\\x30\\x20\\x0b\\xa6\\\n\\x08\\x30\\x30\\x30\\x42\\x98\\x10\\xc1\\x14\\x01\\x14\\x13\\x50\\xb5\\xa3\\x01\\\n\\x00\\xc6\\xb9\\x07\\x90\\x5d\\x66\\x1f\\x83\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\\n\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce\\x7c\\x4e\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x08\\x15\\x3b\\xdc\\\n\\x3b\\x0c\\x9b\\x00\\x00\\x00\\x2a\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\xc0\\\n\\x00\\x8c\\x0c\\x0c\\x73\\x3e\\x20\\x0b\\xa4\\x08\\x30\\x32\\x30\\x20\\x0b\\xa6\\\n\\x08\\x30\\x30\\x30\\x42\\x98\\x10\\xc1\\x14\\x01\\x14\\x13\\x50\\xb5\\xa3\\x01\\\n\\x00\\xc6\\xb9\\x07\\x90\\x5d\\x66\\x1f\\x83\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\\n\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x04\\x33\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x30\\x00\\x00\\x00\\x30\\x08\\x06\\x00\\x00\\x00\\x57\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x03\\xe5\\x49\\x44\\x41\\x54\\x68\\x81\\xed\\\n\\x9a\\x4d\\x88\\x1c\\x45\\x14\\xc7\\x7f\\x33\\x3b\\x89\\x2e\\x18\\xf1\\x33\\x1e\\\n\\x92\\x8b\\x8b\\xa2\\x26\\x06\\x14\\x2f\\xa2\\x39\\x88\\x15\\x89\\xa9\\x18\\x15\\\n\\xd1\\xca\\x06\\x0f\\x91\\x05\\x3d\\x88\\x62\\x6e\\x42\\xc0\\x1c\\x12\\xc4\\x83\\\n\\x07\\x15\\x0d\\x42\\x14\\x89\\x04\\xa2\\x96\\x44\\x59\\x22\\x2f\\x46\\x7d\\x8a\\\n\\xb0\\x22\\x08\\x1e\\x34\\xbb\\x22\\x46\\x8c\\x2c\\x88\\xba\\xc4\\x88\\x82\\x5f\\\n\\xc9\\x46\\x3d\\x54\\x0f\\x8c\\xbd\\xdd\\x5d\\xdd\\xd3\\x61\\xa7\\x07\\xfc\\xdd\\\n\\xa6\\xfa\\xd5\\xab\\xf7\\xea\\xf3\\xdf\\xd5\\xd3\\x22\\xc1\\x58\\x77\\x11\\xb0\\\n\\x03\\x58\\x0b\\x5c\\x0d\\x2c\\xa1\\x59\\x9c\\x02\\xa6\\x81\\x29\\x60\\xa7\\x8a\\\n\\x3f\\x0e\\xd0\\x02\\x30\\xd6\\xdd\\x0c\\xbc\\x02\\x2c\\x1f\\x58\\x78\\xd5\\x98\\\n\\x03\\xb6\\xa8\\xf8\\xf7\\x5b\\x49\\xcf\\xcf\\x30\\x3c\\xc1\\x77\\x99\\x03\\x56\\\n\\xb7\\x09\\xd3\\x66\\xd8\\x82\\x87\\x10\\xf3\\x63\\x1d\\xc2\\x9c\\x4f\\x73\\x7a\\\n\\x91\\x83\\x29\\xcb\\x48\\xea\\xf7\\xda\\x0e\\x61\\xc1\\xf6\\x72\\x5a\\xc5\\x77\\\n\\x16\\x29\\xa0\\x4a\\x18\\xeb\\xe6\\xf9\\x6f\\x12\\x6b\\xda\\x34\\x6f\\xb7\\xa9\\\n\\xc2\\x92\\xf6\\xa0\\x23\\xa8\\xcb\\xff\\x09\\x0c\\x9a\\xa1\\x4f\\xa0\\xa9\\xbb\\\n\\xcd\\x3d\\xc0\\x5d\\xc0\\x4b\\x2a\\xfe\\xdd\\x22\\xdb\\xc6\\x8d\\x80\\xb1\\xee\\\n\\x11\\xc0\\x03\\xe3\\xc0\\x61\\x63\\xdd\\x6e\\x63\\x5d\\xee\\x4e\\xd9\\xa8\\x04\\\n\\x8c\\x75\\xe3\\xc0\\x53\\x3d\\x45\\x2d\\xe0\\x41\\xe0\\x85\\xbc\\x3a\\x8d\\x99\\\n\\x42\\xc6\\xba\\x75\\xc0\\xcb\\x24\\x02\\x33\\xc5\\x56\\x63\\xdd\\x91\\xac\\x7a\\\n\\x8d\\x18\\x01\\x63\\xdd\\x75\\xc0\\x1b\\xc0\\xd2\\x02\\xb3\\x0d\\x59\\x85\\x03\\\n\\x4f\\xc0\\x58\\x77\\x19\\x20\\xc0\\xb2\\x88\\xe9\\x8b\\x59\\x85\\x03\\x4d\\xc0\\\n\\x58\\x77\\x09\\x70\\x98\\xb8\\x1a\\x7e\\x52\\xc5\\xbf\\x9a\\xf5\\x60\\x60\\x09\\\n\\x18\\xeb\\x96\\x01\\x87\\x80\\xb1\\x88\\xe9\\x3e\\xe0\\xd1\\xbc\\x87\\x03\\x49\\\n\\xc0\\x58\\xb7\\x14\\x78\\x13\\xb8\\x36\\x62\\xfa\\x36\\x30\\xa1\\xe2\\xff\\xc9\\\n\\x33\\x58\\xf4\\x04\\x8c\\x75\\x6d\\x42\\xaf\\x9a\\x88\\xe9\\x27\\xc0\\xdd\\x2a\\\n\\x7e\\xbe\\xc8\\x68\\x10\\x23\\xf0\\x34\\xe0\\x22\\x36\\x5f\\x01\\x1b\\x55\\xfc\\\n\\x6f\\x31\\x67\\xa5\\x12\\x30\\xd6\\xb5\\x92\\x9e\\xab\\x85\\xb1\\x6e\\x3b\\xf0\\\n\\x70\\xc4\\xec\\x7b\\x60\\x7d\\xf7\\xd6\\x21\\x46\\x61\\x50\\x49\\xe0\\xdb\\x80\\\n\\x3f\\x80\\x69\\x63\\xdd\\x9a\\x52\\x91\\x66\\xfb\\x9a\\x00\\x1e\\x8f\\x98\\xfd\\\n\\x02\\xdc\\xaa\\xe2\\xbf\\x2d\\xeb\\x37\\x37\\x81\\x44\\x7f\\x4c\\x12\\x8e\\xf6\\\n\\xb3\\x80\\xab\\x80\\xf7\\x8c\\x75\\x57\\x94\\x75\\xde\\xe3\\x6b\\x13\\xb0\\x27\\\n\\x62\\xf6\\x17\\x70\\x87\\x8a\\xff\\xbc\\x8a\\xef\\xa2\\x11\\x78\\x0e\\xd8\\x94\\\n\\x2a\\x5b\\x0e\\xa8\\xb1\\xee\\xd2\\xb2\\x0d\\x18\\xeb\\x6e\\x00\\x5e\\x63\\xe1\\\n\\x0b\\x79\\x2f\\x7f\\x03\\xf7\\xaa\\xf8\\x0f\\xcb\\xfa\\xed\\x92\\x99\\x40\\xd2\\\n\\x63\\x0f\\xe4\\xd4\\x59\\x41\\x18\\x89\\x15\\x31\\xe7\\xc6\\xba\\x55\\xc0\\x41\\\n\\x60\\x34\\x62\\xfa\\x90\\x8a\\x3f\\x10\\xf3\\x97\\x45\\xde\\x08\\x5c\\x10\\xa9\\\n\\x37\\x46\\x48\\xe2\\xe2\\x3c\\x03\\x63\\xdd\\x4a\\xc2\\x29\\x1b\\xf3\\xb5\\x4b\\\n\\xc5\\x3f\\x1f\\xb1\\xc9\\x25\\x2f\\x81\\xfd\\x84\\x7d\\xb8\\x88\\x2b\\x81\\x77\\\n\\x8c\\x75\\xe7\\xa5\\x1f\\x18\\xeb\\xce\\x27\\x04\\xbf\\x32\\xe2\\x63\\x8f\\x8a\\\n\\xdf\\x11\\x8d\\xb2\\x80\\xcc\\x04\\x54\\xfc\\x29\\x82\\xfa\\xcb\\x94\\xb0\\x3d\\\n\\x5c\\x03\\x1c\\x32\\xd6\\x9d\\xd3\\x2d\\x30\\xd6\\x8d\\x12\\xa6\\xcd\\xaa\\x48\\\n\\xdd\\x49\\x82\\xd6\\xaf\\x45\\xee\\x22\\x56\\xf1\\x27\\x80\\x5b\\x80\\xa3\\x11\\\n\\x1f\\xd7\\x03\\x07\\x8d\\x75\\xa3\\xc6\\xba\\x11\\xc2\\x82\\xbd\\x31\\x52\\x67\\\n\\x0a\\x18\\x57\\xf1\\xb5\\x6f\\x00\\x0b\\xcf\\x01\\x15\\xff\\x23\\xb0\\x0e\\x98\\\n\\x8d\\xf8\\xb9\\x09\\x38\\x40\\xd8\\x2a\\xd3\\x3b\\x57\\x9a\\x69\\xe0\\x76\\x15\\\n\\xff\\x67\\xc9\\x18\\x0b\\x89\\x9e\\xae\\x2a\\x7e\\x96\\xa0\\x5b\\x7e\\x88\\x98\\\n\\x6e\\x00\\x26\\x22\\x36\\xb3\\x84\\x83\\xea\\xe7\\x72\\xe1\\xc5\\x29\\x25\\x0f\\\n\\x54\\xfc\\xd7\\x84\\x91\\xf8\\xa9\\x46\\x5b\\x27\\x08\\x12\\xe1\\xbb\\x1a\\x3e\\\n\\x16\\x50\\x5a\\xdf\\xa8\\xf8\\x19\\x60\\x3d\\xf0\\x6b\\x1f\\xed\\xfc\\x0e\\xdc\\\n\\xa6\\xe2\\xbf\\xec\\xa3\\x6e\\x21\\x95\\x04\\x9a\\x8a\\xff\\x14\\xd8\\x98\\x04\\\n\\x54\\x96\\x79\\x60\\xb3\\x8a\\xff\\xb8\\x4a\\x5b\\x65\\xa9\\xac\\x30\\x55\\xfc\\\n\\x14\\x70\\x27\\x41\\xbb\\x94\\xe1\\x7e\\x15\\xff\\x56\\xd5\\x76\\xca\\xd2\\x97\\\n\\x44\\x4e\\x6e\\xcb\\x36\\x13\\x7a\\xb7\\x88\\xed\\x2a\\x7e\\x6f\\x3f\\x6d\\x94\\\n\\xa5\\x6f\\x8d\\xaf\\xe2\\x27\\x81\\xad\\x04\\x21\\x96\\xc5\\xb3\\x2a\\xfe\\x89\\\n\\x7e\\xfd\\x97\\xa5\\xd6\\x4b\\x8a\\x8a\\xdf\\x0f\\xdc\\xc7\\xc2\\x35\\xb1\\x1b\\\n\\xd8\\x56\\xc7\\x77\\x59\\x6a\\xdf\\xcc\\xa9\\xf8\\x7d\\xc6\\xba\\x8f\\x08\\x07\\\n\\xd8\\xb9\\xc0\\x07\\xc9\\x3a\\x59\\x14\\xce\\xc8\\xd5\\xa2\\x8a\\xff\\x06\\x78\\\n\\xe6\\x4c\\xf8\\xaa\\x4a\\x9b\\xf0\\x05\\x7c\\x58\\x39\\xd9\\x21\\x68\\x93\\xde\\\n\\xfb\\x99\\x91\\xe4\\x6b\\x60\\x13\\x49\\xbf\\xd5\\x4d\\x77\\x08\\xca\\x30\\x7d\\\n\\xc1\\x54\\xf4\\xfa\\xd7\\x24\\xa6\\xda\\xc0\\x4e\\xc2\\x67\\xfb\\x61\\x63\\x0e\\\n\\xd8\\xd5\\x4e\\xee\\x5f\\xb6\\x30\\x5c\\x49\\x74\\xff\\xec\\x71\\x7c\\x04\\xe0\\\n\\xd8\\xd1\\x99\\x63\\x63\\x97\\xaf\\xde\\x0b\\x9c\\x4d\\xf8\\xf0\\x7d\\x21\\xcd\\\n\\x9b\\x46\\x27\\x81\\xcf\\x80\\xd7\\x01\\xa7\\xe2\\xbf\\x00\\xf8\\x17\\x5d\\x81\\\n\\x0b\\x38\\xb3\\xfa\\x20\\x9c\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\\n\\x60\\x82\\\n\\x00\\x00\\x01\\xfc\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x30\\x00\\x00\\x00\\x30\\x08\\x06\\x00\\x00\\x00\\x57\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\xae\\x49\\x44\\x41\\x54\\x68\\x81\\xed\\\n\\x9a\\xbd\\x4a\\x03\\x41\\x14\\x46\\x4f\\x36\\x1b\\xb0\\xd0\\x4a\\xf1\\x01\\x14\\\n\\xab\\x68\\x91\\xc6\\x2a\\x58\\xb8\\x16\\xb2\\x88\\x76\\x0b\\xe9\\x7d\\x01\\x1f\\\n\\x40\\x8b\\xf8\\x00\\xbe\\x80\\x85\\x9d\\x30\\x62\\xa3\\x32\\x55\\xc6\\x42\\xf2\\\n\\x02\\x42\\x92\\x46\\x83\\x7d\\x48\\x27\\x36\\xf9\\x01\\x8b\\xdd\\x40\\x12\\xb2\\\n\\x89\\x6b\\x7e\\x66\\x37\\xcc\\xe9\\x76\\xef\\x14\\xdf\\x59\\xee\\x0c\\x0b\\x77\\\n\\x52\\x04\\x38\\xae\\xb7\\x01\\x5c\\x01\\x79\\x60\\x17\\xc8\\x10\\x2f\\xda\\x40\\\n\\x05\\x28\\x03\\x45\\x25\\x45\\x13\\x20\\x05\\xe0\\xb8\\xde\\x21\\x70\\x0f\\x6c\\\n\\x6a\\x8b\\x17\\x8d\\x06\\x50\\x50\\x52\\xbc\\xa6\\x82\\x2f\\x5f\\x25\\x39\\xe1\\\n\\x7b\\x34\\x80\\xac\\x85\\xdf\\x36\\x49\\x0b\\x0f\\x7e\\xe6\\x4b\\x1b\\xbf\\xe7\\\n\\x87\\xe9\\x2e\\x38\\xcc\\x5f\\x49\\x0f\\x3d\\xe7\\x6d\\xfc\\x0d\\xdb\\x4f\\x57\\\n\\x49\\x61\\x2f\\x28\\x50\\x24\\x1c\\xd7\\xeb\\x30\\x28\\xb1\\x67\\x11\\xbf\\xd3\\\n\\x26\\x0a\\x19\\x4b\\x77\\x82\\x69\\x31\\x02\\xba\\x31\\x02\\xba\\x31\\x02\\xba\\\n\\x31\\x02\\xba\\x31\\x02\\xba\\x31\\x02\\xba\\x99\\xf8\\xdb\\xec\\xb8\\xde\\x11\\\n\\xb0\\x0f\\xac\\xce\\x3f\\xce\\x00\\x1d\\xa0\\x06\\x3c\\x2b\\x29\\x7e\\xc2\\x16\\\n\\x85\\x0a\\x38\\xae\\x67\\x03\\x8f\\xc0\\xe9\\xec\\xb3\\x45\\xa2\\xee\\xb8\\xde\\\n\\xb1\\x92\\xe2\\x73\\x54\\x71\\x5c\\x0b\\x5d\\xa0\\x3f\\x3c\\xc0\\x36\\x70\\x1b\\\n\\x56\\x1c\\x27\\x70\\x32\\xfb\\x2c\\xff\\xe6\\xc0\\x71\\xbd\\xb5\\x51\\x85\\xc4\\\n\\x6f\\xe2\\x71\\x02\\x2f\\x0b\\x4b\\x31\\x99\\x37\\x25\\xc5\\xf7\\xa8\\xc2\\x38\\\n\\x81\\x1b\\xe0\\x69\\x3e\\x79\\x22\\x51\\x07\\xce\\xc3\\x8a\\xa1\\xa7\\x90\\x92\\\n\\xa2\\x03\\x9c\\x25\\xf6\\x18\\xed\\xa1\\xa4\\x28\\x01\\xa5\\x19\\x06\\x9b\\x29\\\n\\x4b\\xbd\\x89\\x13\\x81\\x11\\xd0\\x8d\\x11\\xd0\\x8d\\x11\\xd0\\x8d\\x11\\xd0\\\n\\x8d\\x11\\xd0\\xcd\\x52\\x08\\xb4\\x75\\x87\\x98\\x82\\x96\\x8d\\x3f\\xbe\\xcf\\\n\\xf5\\xbd\\x4c\\x07\\xd3\\xc0\\x38\\x32\\x3c\\x66\\xad\\xd8\\xf8\\x77\\x0f\\x72\\\n\\x13\\x16\\xc6\\x95\\xb2\\x05\\x14\\xf1\\xc7\\xf6\\x49\\xa3\\x01\\x5c\\x5b\\xc1\\\n\\xad\\x8f\\x02\\xc9\\x92\\xe8\\x5d\\xf6\\x68\\xa6\\x01\\xbe\\x3e\\xaa\\x5f\\x5b\\\n\\x3b\\xd9\\x3b\\x60\\x05\\x7f\\xf0\\xbd\\x4e\\xfc\\xda\\xa8\\x05\\xbc\\x03\\x0f\\\n\\x80\\xa7\\xa4\\xa8\\x01\\xfc\\x02\\x51\\xab\\x5c\\x8a\\x3f\\xde\\xe3\\x59\\x00\\\n\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x14\\x1f\\x20\\xb9\\\n\\x8d\\x77\\xe9\\x00\\x00\\x00\\x2a\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\xc0\\\n\\x06\\xe6\\x7c\\x60\\x60\\x60\\x42\\x30\\xa1\\x1c\\x08\\x93\\x81\\x81\\x09\\xc1\\\n\\x64\\x60\\x60\\x62\\x60\\x48\\x11\\x40\\xe2\\x20\\x73\\x19\\x90\\x8d\\x40\\x02\\\n\\x00\\x23\\xed\\x08\\xaf\\x64\\x9f\\x0f\\x15\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\\n\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x07\\x30\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x0a\\x00\\x00\\x00\\x07\\x08\\x06\\x00\\x00\\x00\\x31\\xac\\xdc\\x63\\\n\\x00\\x00\\x04\\xb0\\x69\\x54\\x58\\x74\\x58\\x4d\\x4c\\x3a\\x63\\x6f\\x6d\\x2e\\\n\\x61\\x64\\x6f\\x62\\x65\\x2e\\x78\\x6d\\x70\\x00\\x00\\x00\\x00\\x00\\x3c\\x3f\\\n\\x78\\x70\\x61\\x63\\x6b\\x65\\x74\\x20\\x62\\x65\\x67\\x69\\x6e\\x3d\\x22\\xef\\\n\\xbb\\xbf\\x22\\x20\\x69\\x64\\x3d\\x22\\x57\\x35\\x4d\\x30\\x4d\\x70\\x43\\x65\\\n\\x68\\x69\\x48\\x7a\\x72\\x65\\x53\\x7a\\x4e\\x54\\x63\\x7a\\x6b\\x63\\x39\\x64\\\n\\x22\\x3f\\x3e\\x0a\\x3c\\x78\\x3a\\x78\\x6d\\x70\\x6d\\x65\\x74\\x61\\x20\\x78\\\n\\x6d\\x6c\\x6e\\x73\\x3a\\x78\\x3d\\x22\\x61\\x64\\x6f\\x62\\x65\\x3a\\x6e\\x73\\\n\\x3a\\x6d\\x65\\x74\\x61\\x2f\\x22\\x20\\x78\\x3a\\x78\\x6d\\x70\\x74\\x6b\\x3d\\\n\\x22\\x58\\x4d\\x50\\x20\\x43\\x6f\\x72\\x65\\x20\\x35\\x2e\\x35\\x2e\\x30\\x22\\\n\\x3e\\x0a\\x20\\x3c\\x72\\x64\\x66\\x3a\\x52\\x44\\x46\\x20\\x78\\x6d\\x6c\\x6e\\\n\\x73\\x3a\\x72\\x64\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x77\\x77\\\n\\x77\\x2e\\x77\\x33\\x2e\\x6f\\x72\\x67\\x2f\\x31\\x39\\x39\\x39\\x2f\\x30\\x32\\\n\\x2f\\x32\\x32\\x2d\\x72\\x64\\x66\\x2d\\x73\\x79\\x6e\\x74\\x61\\x78\\x2d\\x6e\\\n\\x73\\x23\\x22\\x3e\\x0a\\x20\\x20\\x3c\\x72\\x64\\x66\\x3a\\x44\\x65\\x73\\x63\\\n\\x72\\x69\\x70\\x74\\x69\\x6f\\x6e\\x20\\x72\\x64\\x66\\x3a\\x61\\x62\\x6f\\x75\\\n\\x74\\x3d\\x22\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x65\\\n\\x78\\x69\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\\n\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x65\\x78\\x69\\x66\\x2f\\x31\\x2e\\\n\\x30\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x74\\x69\\\n\\x66\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\\n\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x74\\x69\\x66\\x66\\x2f\\x31\\x2e\\x30\\\n\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x70\\x68\\x6f\\\n\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\\n\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x70\\x68\\x6f\\x74\\\n\\x6f\\x73\\x68\\x6f\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\\n\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x78\\x6d\\x70\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\\n\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x78\\\n\\x61\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\\n\\x6e\\x73\\x3a\\x78\\x6d\\x70\\x4d\\x4d\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\\n\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x78\\x61\\\n\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x6d\\x6d\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\\n\\x6d\\x6c\\x6e\\x73\\x3a\\x73\\x74\\x45\\x76\\x74\\x3d\\x22\\x68\\x74\\x74\\x70\\\n\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\\n\\x78\\x61\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x73\\x54\\x79\\x70\\x65\\x2f\\x52\\x65\\\n\\x73\\x6f\\x75\\x72\\x63\\x65\\x45\\x76\\x65\\x6e\\x74\\x23\\x22\\x0a\\x20\\x20\\\n\\x20\\x65\\x78\\x69\\x66\\x3a\\x50\\x69\\x78\\x65\\x6c\\x58\\x44\\x69\\x6d\\x65\\\n\\x6e\\x73\\x69\\x6f\\x6e\\x3d\\x22\\x31\\x30\\x22\\x0a\\x20\\x20\\x20\\x65\\x78\\\n\\x69\\x66\\x3a\\x50\\x69\\x78\\x65\\x6c\\x59\\x44\\x69\\x6d\\x65\\x6e\\x73\\x69\\\n\\x6f\\x6e\\x3d\\x22\\x37\\x22\\x0a\\x20\\x20\\x20\\x65\\x78\\x69\\x66\\x3a\\x43\\\n\\x6f\\x6c\\x6f\\x72\\x53\\x70\\x61\\x63\\x65\\x3d\\x22\\x31\\x22\\x0a\\x20\\x20\\\n\\x20\\x74\\x69\\x66\\x66\\x3a\\x49\\x6d\\x61\\x67\\x65\\x57\\x69\\x64\\x74\\x68\\\n\\x3d\\x22\\x31\\x30\\x22\\x0a\\x20\\x20\\x20\\x74\\x69\\x66\\x66\\x3a\\x49\\x6d\\\n\\x61\\x67\\x65\\x4c\\x65\\x6e\\x67\\x74\\x68\\x3d\\x22\\x37\\x22\\x0a\\x20\\x20\\\n\\x20\\x74\\x69\\x66\\x66\\x3a\\x52\\x65\\x73\\x6f\\x6c\\x75\\x74\\x69\\x6f\\x6e\\\n\\x55\\x6e\\x69\\x74\\x3d\\x22\\x32\\x22\\x0a\\x20\\x20\\x20\\x74\\x69\\x66\\x66\\\n\\x3a\\x58\\x52\\x65\\x73\\x6f\\x6c\\x75\\x74\\x69\\x6f\\x6e\\x3d\\x22\\x37\\x32\\\n\\x2e\\x30\\x22\\x0a\\x20\\x20\\x20\\x74\\x69\\x66\\x66\\x3a\\x59\\x52\\x65\\x73\\\n\\x6f\\x6c\\x75\\x74\\x69\\x6f\\x6e\\x3d\\x22\\x37\\x32\\x2e\\x30\\x22\\x0a\\x20\\\n\\x20\\x20\\x70\\x68\\x6f\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3a\\x43\\x6f\\x6c\\x6f\\\n\\x72\\x4d\\x6f\\x64\\x65\\x3d\\x22\\x33\\x22\\x0a\\x20\\x20\\x20\\x70\\x68\\x6f\\\n\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3a\\x49\\x43\\x43\\x50\\x72\\x6f\\x66\\x69\\x6c\\\n\\x65\\x3d\\x22\\x73\\x52\\x47\\x42\\x20\\x49\\x45\\x43\\x36\\x31\\x39\\x36\\x36\\\n\\x2d\\x32\\x2e\\x31\\x22\\x0a\\x20\\x20\\x20\\x78\\x6d\\x70\\x3a\\x4d\\x6f\\x64\\\n\\x69\\x66\\x79\\x44\\x61\\x74\\x65\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x30\\x35\\\n\\x2d\\x33\\x31\\x54\\x31\\x32\\x3a\\x33\\x33\\x3a\\x31\\x34\\x2b\\x30\\x32\\x3a\\\n\\x30\\x30\\x22\\x0a\\x20\\x20\\x20\\x78\\x6d\\x70\\x3a\\x4d\\x65\\x74\\x61\\x64\\\n\\x61\\x74\\x61\\x44\\x61\\x74\\x65\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x30\\x35\\\n\\x2d\\x33\\x31\\x54\\x31\\x32\\x3a\\x33\\x33\\x3a\\x31\\x34\\x2b\\x30\\x32\\x3a\\\n\\x30\\x30\\x22\\x3e\\x0a\\x20\\x20\\x20\\x3c\\x78\\x6d\\x70\\x4d\\x4d\\x3a\\x48\\\n\\x69\\x73\\x74\\x6f\\x72\\x79\\x3e\\x0a\\x20\\x20\\x20\\x20\\x3c\\x72\\x64\\x66\\\n\\x3a\\x53\\x65\\x71\\x3e\\x0a\\x20\\x20\\x20\\x20\\x20\\x3c\\x72\\x64\\x66\\x3a\\\n\\x6c\\x69\\x0a\\x20\\x20\\x20\\x20\\x20\\x20\\x73\\x74\\x45\\x76\\x74\\x3a\\x61\\\n\\x63\\x74\\x69\\x6f\\x6e\\x3d\\x22\\x70\\x72\\x6f\\x64\\x75\\x63\\x65\\x64\\x22\\\n\\x0a\\x20\\x20\\x20\\x20\\x20\\x20\\x73\\x74\\x45\\x76\\x74\\x3a\\x73\\x6f\\x66\\\n\\x74\\x77\\x61\\x72\\x65\\x41\\x67\\x65\\x6e\\x74\\x3d\\x22\\x41\\x66\\x66\\x69\\\n\\x6e\\x69\\x74\\x79\\x20\\x44\\x65\\x73\\x69\\x67\\x6e\\x65\\x72\\x20\\x31\\x2e\\\n\\x39\\x2e\\x32\\x22\\x0a\\x20\\x20\\x20\\x20\\x20\\x20\\x73\\x74\\x45\\x76\\x74\\\n\\x3a\\x77\\x68\\x65\\x6e\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x30\\x35\\x2d\\x33\\\n\\x31\\x54\\x31\\x32\\x3a\\x33\\x33\\x3a\\x31\\x34\\x2b\\x30\\x32\\x3a\\x30\\x30\\\n\\x22\\x2f\\x3e\\x0a\\x20\\x20\\x20\\x20\\x3c\\x2f\\x72\\x64\\x66\\x3a\\x53\\x65\\\n\\x71\\x3e\\x0a\\x20\\x20\\x20\\x3c\\x2f\\x78\\x6d\\x70\\x4d\\x4d\\x3a\\x48\\x69\\\n\\x73\\x74\\x6f\\x72\\x79\\x3e\\x0a\\x20\\x20\\x3c\\x2f\\x72\\x64\\x66\\x3a\\x44\\\n\\x65\\x73\\x63\\x72\\x69\\x70\\x74\\x69\\x6f\\x6e\\x3e\\x0a\\x20\\x3c\\x2f\\x72\\\n\\x64\\x66\\x3a\\x52\\x44\\x46\\x3e\\x0a\\x3c\\x2f\\x78\\x3a\\x78\\x6d\\x70\\x6d\\\n\\x65\\x74\\x61\\x3e\\x0a\\x3c\\x3f\\x78\\x70\\x61\\x63\\x6b\\x65\\x74\\x20\\x65\\\n\\x6e\\x64\\x3d\\x22\\x72\\x22\\x3f\\x3e\\x48\\x8b\\x5b\\x5e\\x00\\x00\\x01\\x83\\\n\\x69\\x43\\x43\\x50\\x73\\x52\\x47\\x42\\x20\\x49\\x45\\x43\\x36\\x31\\x39\\x36\\\n\\x36\\x2d\\x32\\x2e\\x31\\x00\\x00\\x28\\x91\\x75\\x91\\xcf\\x2b\\x44\\x51\\x14\\\n\\xc7\\x3f\\x66\\x68\\xfc\\x18\\x8d\\x62\\x61\\x31\\x65\\x12\\x16\\x42\\x83\\x12\\\n\\x1b\\x8b\\x99\\x18\\x0a\\x8b\\x99\\x51\\x7e\\x6d\\x66\\x9e\\x79\\x33\\x6a\\xde\\\n\\x78\\xbd\\x37\\xd2\\x64\\xab\\x6c\\xa7\\x28\\xb1\\xf1\\x6b\\xc1\\x5f\\xc0\\x56\\\n\\x59\\x2b\\x45\\xa4\\x64\\xa7\\xac\\x89\\x0d\\x7a\\xce\\x9b\\x51\\x23\\x99\\x73\\\n\\x3b\\xf7\\x7c\\xee\\xf7\\xde\\x73\\xba\\xf7\\x5c\\x70\\x44\\xd3\\x8a\\x66\\x56\\\n\\xfa\\x41\\xcb\\x64\\x8d\\x70\\x28\\xe0\\x9b\\x99\\x9d\\xf3\\xb9\\x9e\\xa8\\xa2\\\n\\x85\\x1a\\x3a\\xf1\\xc6\\x14\\x53\\x9f\\x8c\\x8c\\x46\\x29\\x6b\\xef\\xb7\\x54\\\n\\xd8\\xf1\\xba\\xdb\\xae\\x55\\xfe\\xdc\\xbf\\x56\\xb7\\x98\\x30\\x15\\xa8\\xa8\\\n\\x16\\x1e\\x56\\x74\\x23\\x2b\\x3c\\x26\\x3c\\xb1\\x9a\\xd5\\x6d\\xde\\x12\\x6e\\\n\\x52\\x52\\xb1\\x45\\xe1\\x13\\xe1\\x2e\\x43\\x2e\\x28\\x7c\\x63\\xeb\\xf1\\x22\\\n\\x3f\\xdb\\x9c\\x2c\\xf2\\xa7\\xcd\\x46\\x34\\x1c\\x04\\x47\\x83\\xb0\\x2f\\xf9\\\n\\x8b\\xe3\\xbf\\x58\\x49\\x19\\x9a\\xb0\\xbc\\x9c\\x36\\x2d\\xbd\\xa2\\xfc\\xdc\\\n\\xc7\\x7e\\x89\\x3b\\x91\\x99\\x8e\\x48\\x6c\\x15\\xf7\\x62\\x12\\x26\\x44\\x00\\\n\\x1f\\xe3\\x8c\\x10\\x64\\x80\\x5e\\x86\\x64\\x1e\\xa0\\x9b\\x3e\\x7a\\x64\\x45\\\n\\x99\\x7c\\x7f\\x21\\x7f\\x8a\\x65\\xc9\\x55\\x64\\xd6\\xc9\\x61\\xb0\\x44\\x92\\\n\\x14\\x59\\xba\\x44\\x5d\\x91\\xea\\x09\\x89\\xaa\\xe8\\x09\\x19\\x69\\x72\\x76\\\n\\xff\\xff\\xf6\\xd5\\x54\\xfb\\xfb\\x8a\\xd5\\xdd\\x01\\xa8\\x7a\\xb4\\xac\\xd7\\\n\\x76\\x70\\x6d\\xc2\\x57\\xde\\xb2\\x3e\\x0e\\x2c\\xeb\\xeb\\x10\\x9c\\x0f\\x70\\\n\\x9e\\x29\\xe5\\x2f\\xef\\xc3\\xe0\\x9b\\xe8\\xf9\\x92\\xd6\\xb6\\x07\\x9e\\x75\\\n\\x38\\xbd\\x28\\x69\\xf1\\x6d\\x38\\xdb\\x80\\xe6\\x7b\\x3d\\x66\\xc4\\x0a\\x92\\\n\\x53\\xdc\\xa1\\xaa\\xf0\\x72\\x0c\\xf5\\xb3\\xd0\\x78\\x05\\xb5\\xf3\\xc5\\x9e\\\n\\xfd\\xec\\x73\\x74\\x07\\xd1\\x35\\xf9\\xaa\\x4b\\xd8\\xd9\\x85\\x0e\\x39\\xef\\\n\\x59\\xf8\\x06\\x8e\\xfd\\x67\\xf8\\xfd\\x8a\\x18\\x97\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x97\\x49\\x44\\x41\\x54\\x18\\x95\\x6d\\xcf\\xb1\\x6a\\x02\\x41\\\n\\x14\\x85\\xe1\\x6f\\xb7\\xb6\\xd0\\x27\\x48\\x3d\\x56\\x69\\x03\\xb1\\xb4\\x48\\\n\\x3b\\x6c\\xa5\\xf1\\x39\\xf6\\x59\\x02\\x56\\x42\\xba\\x61\\x0a\\x0b\\x3b\\x1b\\\n\\x1b\\x6b\\x41\\x18\\x02\\x29\\x6d\\xe3\\xbe\\x82\\xcd\\x06\\x16\\xd9\\xdb\\xdd\\\n\\x9f\\xff\\x5c\\xee\\xa9\\x62\\x2a\\x13\\x4c\\x73\\x13\\x6e\\x46\\x26\\xa6\\xf2\\\n\\x82\\xae\\x46\\x8b\\xdf\\x98\\xca\\xfb\\x88\\xb4\\xc0\\x0f\\xda\\x1a\\x5b\\x74\\\n\\xd8\\xc7\\x54\\xc2\\x40\\x9a\\x63\\x8f\\x3f\\x7c\\x55\\x3d\\x7c\\xc5\\x09\\x77\\\n\\xbc\\xa1\\xc2\\x19\\x33\\x2c\\x72\\x13\\x2e\\xd5\\xe0\\xc2\\x12\\x07\\x5c\\x51\\\n\\x23\\xe0\\x23\\x37\\xe1\\xa8\\x4f\\x0e\\x7f\\xda\\x60\\xd7\\xaf\\x9f\\xb9\\x09\\\n\\xdf\\x63\\x05\\xff\\xe5\\x75\\x4c\\x65\\xf5\\xcc\\x1f\\x0d\\x33\\x2c\\x83\\xb6\\\n\\x06\\x44\\x83\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x03\\xfb\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x30\\x00\\x00\\x00\\x30\\x08\\x06\\x00\\x00\\x00\\x57\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x03\\xad\\x49\\x44\\x41\\x54\\x68\\x81\\xed\\\n\\x9a\\x4f\\xa8\\x15\\x55\\x1c\\xc7\\x3f\\xf7\\xbe\\xab\\xf2\\x20\\xa3\\x22\\x6d\\\n\\xa1\\x7c\\x21\\x09\\x2a\\x4b\\x28\\xda\\x44\\xb9\\x88\\x52\\x4c\\xcc\\x6a\\x51\\\n\\xf9\\xa4\\x85\\xf1\\xa0\\x16\\x51\\xe4\\x2e\\x10\\x74\\x51\\x44\\x8b\\x16\\x15\\\n\\x25\\x81\\xb5\\x28\\x04\\xad\\xc0\\xe2\\x61\\x64\\xf6\\x97\\xe0\\x45\\x10\\xb4\\\n\\xa9\\x27\\x44\\x45\\xc4\\x17\\xa2\\x12\\x35\\x0a\\x2a\\xff\\x3c\\xab\\xc5\\x99\\\n\\x5b\\xd7\\x79\\x33\\x73\\xce\\xdc\\x6b\\x6f\\xee\\x05\\x3f\\xbb\\x39\\xf3\\x3b\\\n\\xbf\\xf3\\xfb\\x9d\\x33\\xe7\\x9c\\xef\\x9c\\x99\\x16\\x19\\xb6\\x2f\\x06\\x76\\\n\\x00\\xab\\x81\\xab\\x81\\x05\\x0c\\x17\\xa7\\x80\\x19\\x60\\x1a\\x78\\x4c\\xd2\\\n\\x11\\x80\\x16\\x80\\xed\\x9b\\x81\\xbd\\xc0\\xd2\\xc6\\xc2\\xab\\xc7\\x61\\x60\\\n\\xb3\\xa4\\x0f\\x5b\\x59\\xcf\\x1f\\x62\\x74\\x82\\xef\\x72\\x18\\xb8\\xaa\\x4d\\\n\\x78\\x6c\\x46\\x2d\\x78\\x08\\x31\\x6f\\xef\\x10\\x9e\\xf9\\x3c\\xa7\\xe7\\x39\\\n\\x98\\x54\\xc6\\x72\\xd7\\xab\\x3b\\x84\\x09\\xdb\\xcb\\x69\\x49\\x9d\\x79\\x0a\\\n\\xa8\\x16\\xb6\\x67\\x39\\x33\\x89\\x55\\x6d\\x86\\x6f\\xb5\\xa9\\xc3\\x82\\x76\\\n\\xd3\\x11\\x0c\\xca\\xb9\\x04\\x9a\\xe6\\x5c\\x02\\xff\\x07\\xb6\\xef\\xb6\\xbd\\\n\\xd7\\xf6\\xda\\x98\\xed\\xd0\\x25\\x60\\xfb\\x11\\xe0\\x75\\x60\\x02\\x38\\x68\\\n\\x7b\\xa7\\xed\\xd2\\x95\\x72\\xa8\\x12\\xb0\\x3d\\x01\\x3c\\xdd\\x53\\xd4\\x02\\\n\\x1e\\x04\\x5e\\x2c\\xab\\x33\\x34\\x1b\\x96\\xed\\x35\\xc0\\x2b\\x64\\x02\\x33\\\n\\xc7\\x16\\xdb\\x5f\\x16\\xd5\\x1b\\x8a\\x11\\xb0\\x7d\\x1d\\xf0\\x06\\xb0\\xb0\\\n\\xc2\\x6c\\x7d\\x51\\x61\\xe3\\x09\\xd8\\xbe\\x0c\\x78\\x1b\\x58\\x1c\\x31\\x7d\\\n\\xa9\\xa8\\xb0\\xd1\\x04\\x6c\\x5f\\x02\\x1c\\x24\\xae\\x86\\x9f\\x92\\xf4\\x6a\\\n\\xd1\\x8d\\xc6\\x12\\xb0\\xbd\\x18\\x38\\x00\\xac\\x88\\x98\\xee\\x06\\x1e\\x2d\\\n\\xbb\\xd9\\x48\\x02\\xb6\\x17\\x02\\x6f\\x02\\xd7\\x46\\x4c\\xdf\\x01\\x26\\x25\\\n\\xfd\\x5d\\x66\\x30\\xef\\x09\\xd8\\x6e\\x13\\x7a\\xf5\\x96\\x88\\xe9\\x67\\xc0\\\n\\x5d\\x92\\x66\\xab\\x8c\\x9a\\x18\\x81\\x67\\x80\\x7b\\x22\\x36\\x5f\\x03\\x1b\\\n\\x24\\xfd\\x1e\\x73\\x96\\x94\\x80\\xed\\x56\\xd6\\x73\\x03\\x61\\x7b\\x1b\\xf0\\\n\\x70\\xc4\\xec\\x47\\x60\\x5d\\xf7\\xd4\\x21\\x46\\x65\\x50\\x59\\xe0\\x5b\\x81\\\n\\x3f\\x81\\x19\\xdb\\xab\\x92\\x22\\x2d\\xf6\\x35\\x09\\x3c\\x11\\x31\\xfb\\x15\\\n\\xb8\\x55\\xd2\\xf7\\xa9\\x7e\\x4b\\x13\\xc8\\xf4\\xc7\\x14\\x61\\x6b\\x5f\\x04\\\n\\x5c\\x09\\xbc\\x6f\\xfb\\xf2\\x54\\xe7\\x3d\\xbe\\x36\\x02\\xbb\\x22\\x66\\x27\\\n\\x80\\x3b\\x24\\x7d\\x51\\xc7\\x77\\xd5\\x08\\x3c\\x0f\\x6c\\xcc\\x95\\x2d\\x05\\\n\\x3e\\xb0\\x7d\\x69\\x6a\\x03\\xb6\\x6f\\x00\\x5e\\x63\\xee\\x0b\\x79\\x2f\\x7f\\\n\\x01\\xf7\\x4a\\xfa\\x38\\xd5\\x6f\\x97\\xc2\\x04\\xb2\\x1e\\x7b\\xa0\\xa4\\xce\\\n\\x32\\xc2\\x48\\x2c\\x8b\\x39\\xb7\\xbd\\x12\\xd8\\x0f\\x8c\\x47\\x4c\\x1f\\x92\\\n\\xb4\\x2f\\xe6\\xaf\\x88\\xb2\\x11\\xb8\\x28\\x52\\x6f\\x05\\x21\\x89\\x25\\x65\\\n\\x06\\xb6\\x97\\x13\\x76\\xd9\\x98\\xaf\\xc7\\x25\\xbd\\x10\\xb1\\x29\\xa5\\x2c\\\n\\x81\\x3d\\x84\\x75\\xb8\\x8a\\x2b\\x80\\x77\\x6d\\x5f\\x90\\xbf\\x61\\xfb\\x42\\\n\\x42\\xf0\\xcb\\x23\\x3e\\x76\\x49\\xda\\x11\\x8d\\xb2\\x82\\xc2\\x04\\x24\\x9d\\\n\\x22\\xa8\\xbf\\x42\\x09\\xdb\\xc3\\x35\\xc0\\x01\\xdb\\xe7\\x75\\x0b\\x6c\\x8f\\\n\\x13\\x1e\\x9b\\x95\\x91\\xba\\x53\\x04\\xad\\x3f\\x10\\xa5\\x93\\x58\\xd2\\x31\\\n\\x60\\x2d\\xf0\\x4d\\xc4\\xc7\\xf5\\xc0\\x7e\\xdb\\xe3\\xb6\\xc7\\x08\\x13\\xf6\\\n\\xc6\\x48\\x9d\\x69\\x60\\x42\\xd2\\xc0\\x27\\x80\\x95\\xfb\\x80\\xa4\\x9f\\x81\\\n\\x35\\x80\\x23\\x7e\\x6e\\x02\\xf6\\x11\\x96\\xca\\xfc\\xca\\x95\\x67\\x06\\xb8\\\n\\x5d\\xd2\\xf1\\xc4\\x18\\x2b\\x89\\xee\\xae\\x92\\x4c\\xd0\\x2d\\x3f\\x45\\x4c\\\n\\xd7\\x03\\x93\\x11\\x1b\\x13\\x36\\xaa\\x5f\\xd2\\xc2\\x8b\\x93\\x24\\x0f\\x24\\\n\\x7d\\x4b\\x18\\x89\\xa3\\x03\\xb4\\x75\\x8c\\x20\\x11\\x7e\\x18\\xc0\\xc7\\x1c\\\n\\x92\\xf5\\x8d\\xa4\\x43\\xc0\\x3a\\xe0\\xb7\\x3e\\xda\\xf9\\x03\\xb8\\x4d\\xd2\\\n\\x57\\x7d\\xd4\\xad\\xa4\\x96\\x40\\x93\\xf4\\x39\\xb0\\x21\\x0b\\x28\\x95\\x59\\\n\\x60\\x93\\xa4\\x4f\\xeb\\xb4\\x95\\x4a\\x6d\\x85\\x29\\x69\\x1a\\xb8\\x93\\xa0\\\n\\x5d\\x52\\xb8\\x5f\\xd2\\x5b\\x75\\xdb\\x49\\xa5\\x2f\\x89\\x2c\\xe9\\x3d\\x60\\\n\\x13\\xa1\\x77\\xab\\xd8\\x26\\xe9\\xe5\\x7e\\xda\\x48\\xa5\\x6f\\x8d\\x2f\\x69\\\n\\x0a\\xd8\\x42\\x10\\x62\\x45\\x3c\\x27\\xe9\\xc9\\x7e\\xfd\\xa7\\x32\\xd0\\x4b\\\n\\x8a\\xa4\\x3d\\xc0\\x7d\\xcc\\x9d\\x13\\x3b\\x81\\xad\\x83\\xf8\\x4e\\x65\\xe0\\\n\\x93\\x39\\x49\\xbb\\x6d\\x7f\\x42\\xd8\\xc0\\xce\\x07\\x3e\\xca\\xe6\\xc9\\xbc\\\n\\x70\\x56\\x8e\\x16\\x25\\x7d\\x07\\x3c\\x7b\\x36\\x7c\\xd5\\xa5\\x4d\\xf8\\x02\\\n\\x3e\\xaa\\x9c\\xec\\x10\\xb4\\x49\\xef\\xf9\\xcc\\x58\\xf6\\x35\\x70\\x18\\xc9\\\n\\xbf\\xd5\\xcd\\x74\\x08\\xca\\x30\\x7f\\xc0\\x54\\xf5\\xfa\\x37\\x4c\\x4c\\x8f\\\n\\xfe\\xaf\\x06\\xd9\\xf9\\xcb\\xe6\\xac\\x60\\x54\\xe8\\xfe\\xec\\x71\\xe4\\xdf\\\n\\x8f\\x09\\xd9\\x48\\x6c\\xe7\\xbf\\xdf\\x6d\\xaa\\xce\\xea\\x9b\\xe0\\x24\\x67\\\n\\xfe\\x6e\\x73\\x14\\xe0\\x1f\\x0a\\x43\\x12\\x6b\\x4f\\xfd\\x3f\\x13\\x00\\x00\\\n\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x01\\x5b\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x30\\x00\\x00\\x00\\x30\\x08\\x06\\x00\\x00\\x00\\x57\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x0d\\x49\\x44\\x41\\x54\\x68\\x81\\xed\\\n\\xda\\xb1\\x6d\\x02\\x41\\x10\\x46\\xe1\\x77\\xc7\\xe2\\x0a\\x2c\\x87\\xd3\\x00\\\n\\x38\\x20\\x27\\x72\\x17\\x14\\x83\\x03\\x37\\xe3\\x2e\\x1c\\x51\\x02\\x34\\x30\\\n\\x21\\xc2\\x0d\\x60\\x90\\x1c\\xec\\x9e\\x0c\\x27\\x59\\xf6\\x09\\x89\\xff\\x56\\\n\\x9a\\x2f\\x63\\x45\\x30\\x0f\\x0e\\x92\\x9d\\x86\\xc2\\xdd\\x1f\\x81\\x57\\x60\\\n\\x09\\xcc\\x81\\x29\\xe3\\xf2\\x05\\x6c\\x81\\x0d\\xf0\\x66\\x66\\x07\\x80\\x06\\\n\\xc0\\xdd\\x5f\\x80\\x77\\xe0\\x49\\x36\\xde\\x30\\x7b\\x60\\x65\\x66\\x1f\\x4d\\\n\\xf9\\xe4\\x77\\xd4\\x33\\x7c\\x67\\x0f\\xcc\\x5a\\xf2\\x63\\x53\\xdb\\xf0\\x90\\\n\\x67\\x5e\\x27\\xf2\\x33\\xdf\\x77\\xbe\\xf3\\x30\\xff\\x35\\xe9\\xbd\\x5e\\x26\\\n\\xf2\\x0f\\xf6\\xd2\\xd9\\xcc\\xd2\\x9d\\x06\\x1a\\xc4\\xdd\\x4f\\x5c\\x47\\x3c\\\n\\xb7\\x8c\\xef\\xdf\\x66\\x88\\x69\\xab\\x9e\\xe0\\x56\\x11\\xa0\\x16\\x01\\x6a\\\n\\x11\\xa0\\x16\\x01\\x6a\\x11\\xa0\\x16\\x01\\x6a\\x11\\xa0\\x16\\x01\\x6a\\x11\\\n\\xa0\\x16\\x01\\x6a\\x11\\xa0\\x16\\x01\\x6a\\x11\\xa0\\x16\\x01\\x6a\\x11\\xa0\\\n\\x16\\x01\\x6a\\x11\\xa0\\xd6\\x92\\x6f\\xc0\\x6b\\x75\\x4c\\xe4\\xeb\\xfb\\xc5\\\n\\xc5\\xe1\\xa4\\xdc\\x06\\x8e\\x51\\xff\\x9a\\x75\\x9b\\xc8\\xbb\\x07\\x8b\\x3f\\\n\\xde\\x38\\x56\\x9b\\xfa\\x57\\x0d\\xca\\xd6\\xc7\\xaa\\x1c\\xd4\\xa2\\x5b\\xf6\\\n\\x38\\x34\\xdd\\x49\\xf9\\x26\\xd6\\xfc\\xac\\xdb\\x3c\\x88\\x86\\xfb\\xcd\\x91\\\n\\xeb\\x75\\x9b\\x4f\\x80\\x6f\\x56\\x01\\x36\\x1e\\x77\\x0d\\xa5\\x42\\x00\\x00\\\n\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x07\\xdd\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x07\\x00\\x00\\x00\\x0a\\x08\\x06\\x00\\x00\\x00\\x78\\xcc\\x44\\x0d\\\n\\x00\\x00\\x05\\x52\\x69\\x54\\x58\\x74\\x58\\x4d\\x4c\\x3a\\x63\\x6f\\x6d\\x2e\\\n\\x61\\x64\\x6f\\x62\\x65\\x2e\\x78\\x6d\\x70\\x00\\x00\\x00\\x00\\x00\\x3c\\x3f\\\n\\x78\\x70\\x61\\x63\\x6b\\x65\\x74\\x20\\x62\\x65\\x67\\x69\\x6e\\x3d\\x22\\xef\\\n\\xbb\\xbf\\x22\\x20\\x69\\x64\\x3d\\x22\\x57\\x35\\x4d\\x30\\x4d\\x70\\x43\\x65\\\n\\x68\\x69\\x48\\x7a\\x72\\x65\\x53\\x7a\\x4e\\x54\\x63\\x7a\\x6b\\x63\\x39\\x64\\\n\\x22\\x3f\\x3e\\x0a\\x3c\\x78\\x3a\\x78\\x6d\\x70\\x6d\\x65\\x74\\x61\\x20\\x78\\\n\\x6d\\x6c\\x6e\\x73\\x3a\\x78\\x3d\\x22\\x61\\x64\\x6f\\x62\\x65\\x3a\\x6e\\x73\\\n\\x3a\\x6d\\x65\\x74\\x61\\x2f\\x22\\x20\\x78\\x3a\\x78\\x6d\\x70\\x74\\x6b\\x3d\\\n\\x22\\x58\\x4d\\x50\\x20\\x43\\x6f\\x72\\x65\\x20\\x35\\x2e\\x35\\x2e\\x30\\x22\\\n\\x3e\\x0a\\x20\\x3c\\x72\\x64\\x66\\x3a\\x52\\x44\\x46\\x20\\x78\\x6d\\x6c\\x6e\\\n\\x73\\x3a\\x72\\x64\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x77\\x77\\\n\\x77\\x2e\\x77\\x33\\x2e\\x6f\\x72\\x67\\x2f\\x31\\x39\\x39\\x39\\x2f\\x30\\x32\\\n\\x2f\\x32\\x32\\x2d\\x72\\x64\\x66\\x2d\\x73\\x79\\x6e\\x74\\x61\\x78\\x2d\\x6e\\\n\\x73\\x23\\x22\\x3e\\x0a\\x20\\x20\\x3c\\x72\\x64\\x66\\x3a\\x44\\x65\\x73\\x63\\\n\\x72\\x69\\x70\\x74\\x69\\x6f\\x6e\\x20\\x72\\x64\\x66\\x3a\\x61\\x62\\x6f\\x75\\\n\\x74\\x3d\\x22\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x64\\\n\\x63\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x70\\x75\\x72\\x6c\\x2e\\x6f\\\n\\x72\\x67\\x2f\\x64\\x63\\x2f\\x65\\x6c\\x65\\x6d\\x65\\x6e\\x74\\x73\\x2f\\x31\\\n\\x2e\\x31\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x65\\\n\\x78\\x69\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\\n\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x65\\x78\\x69\\x66\\x2f\\x31\\x2e\\\n\\x30\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x74\\x69\\\n\\x66\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\\n\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x74\\x69\\x66\\x66\\x2f\\x31\\x2e\\x30\\\n\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x70\\x68\\x6f\\\n\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\\n\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x70\\x68\\x6f\\x74\\\n\\x6f\\x73\\x68\\x6f\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\\n\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x78\\x6d\\x70\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\\n\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x78\\\n\\x61\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\x6d\\x6c\\\n\\x6e\\x73\\x3a\\x78\\x6d\\x70\\x4d\\x4d\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\\n\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x78\\x61\\\n\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x6d\\x6d\\x2f\\x22\\x0a\\x20\\x20\\x20\\x20\\x78\\\n\\x6d\\x6c\\x6e\\x73\\x3a\\x73\\x74\\x45\\x76\\x74\\x3d\\x22\\x68\\x74\\x74\\x70\\\n\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\\n\\x78\\x61\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x73\\x54\\x79\\x70\\x65\\x2f\\x52\\x65\\\n\\x73\\x6f\\x75\\x72\\x63\\x65\\x45\\x76\\x65\\x6e\\x74\\x23\\x22\\x0a\\x20\\x20\\\n\\x20\\x65\\x78\\x69\\x66\\x3a\\x50\\x69\\x78\\x65\\x6c\\x58\\x44\\x69\\x6d\\x65\\\n\\x6e\\x73\\x69\\x6f\\x6e\\x3d\\x22\\x37\\x22\\x0a\\x20\\x20\\x20\\x65\\x78\\x69\\\n\\x66\\x3a\\x50\\x69\\x78\\x65\\x6c\\x59\\x44\\x69\\x6d\\x65\\x6e\\x73\\x69\\x6f\\\n\\x6e\\x3d\\x22\\x31\\x30\\x22\\x0a\\x20\\x20\\x20\\x65\\x78\\x69\\x66\\x3a\\x43\\\n\\x6f\\x6c\\x6f\\x72\\x53\\x70\\x61\\x63\\x65\\x3d\\x22\\x31\\x22\\x0a\\x20\\x20\\\n\\x20\\x74\\x69\\x66\\x66\\x3a\\x49\\x6d\\x61\\x67\\x65\\x57\\x69\\x64\\x74\\x68\\\n\\x3d\\x22\\x37\\x22\\x0a\\x20\\x20\\x20\\x74\\x69\\x66\\x66\\x3a\\x49\\x6d\\x61\\\n\\x67\\x65\\x4c\\x65\\x6e\\x67\\x74\\x68\\x3d\\x22\\x31\\x30\\x22\\x0a\\x20\\x20\\\n\\x20\\x74\\x69\\x66\\x66\\x3a\\x52\\x65\\x73\\x6f\\x6c\\x75\\x74\\x69\\x6f\\x6e\\\n\\x55\\x6e\\x69\\x74\\x3d\\x22\\x32\\x22\\x0a\\x20\\x20\\x20\\x74\\x69\\x66\\x66\\\n\\x3a\\x58\\x52\\x65\\x73\\x6f\\x6c\\x75\\x74\\x69\\x6f\\x6e\\x3d\\x22\\x37\\x32\\\n\\x2e\\x30\\x22\\x0a\\x20\\x20\\x20\\x74\\x69\\x66\\x66\\x3a\\x59\\x52\\x65\\x73\\\n\\x6f\\x6c\\x75\\x74\\x69\\x6f\\x6e\\x3d\\x22\\x37\\x32\\x2e\\x30\\x22\\x0a\\x20\\\n\\x20\\x20\\x70\\x68\\x6f\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3a\\x43\\x6f\\x6c\\x6f\\\n\\x72\\x4d\\x6f\\x64\\x65\\x3d\\x22\\x33\\x22\\x0a\\x20\\x20\\x20\\x70\\x68\\x6f\\\n\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3a\\x49\\x43\\x43\\x50\\x72\\x6f\\x66\\x69\\x6c\\\n\\x65\\x3d\\x22\\x73\\x52\\x47\\x42\\x20\\x49\\x45\\x43\\x36\\x31\\x39\\x36\\x36\\\n\\x2d\\x32\\x2e\\x31\\x22\\x0a\\x20\\x20\\x20\\x78\\x6d\\x70\\x3a\\x4d\\x6f\\x64\\\n\\x69\\x66\\x79\\x44\\x61\\x74\\x65\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x30\\x35\\\n\\x2d\\x33\\x31\\x54\\x31\\x32\\x3a\\x34\\x33\\x3a\\x30\\x39\\x2b\\x30\\x32\\x3a\\\n\\x30\\x30\\x22\\x0a\\x20\\x20\\x20\\x78\\x6d\\x70\\x3a\\x4d\\x65\\x74\\x61\\x64\\\n\\x61\\x74\\x61\\x44\\x61\\x74\\x65\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x30\\x35\\\n\\x2d\\x33\\x31\\x54\\x31\\x32\\x3a\\x34\\x33\\x3a\\x30\\x39\\x2b\\x30\\x32\\x3a\\\n\\x30\\x30\\x22\\x3e\\x0a\\x20\\x20\\x20\\x3c\\x64\\x63\\x3a\\x74\\x69\\x74\\x6c\\\n\\x65\\x3e\\x0a\\x20\\x20\\x20\\x20\\x3c\\x72\\x64\\x66\\x3a\\x41\\x6c\\x74\\x3e\\\n\\x0a\\x20\\x20\\x20\\x20\\x20\\x3c\\x72\\x64\\x66\\x3a\\x6c\\x69\\x20\\x78\\x6d\\\n\\x6c\\x3a\\x6c\\x61\\x6e\\x67\\x3d\\x22\\x78\\x2d\\x64\\x65\\x66\\x61\\x75\\x6c\\\n\\x74\\x22\\x3e\\x62\\x72\\x61\\x6e\\x63\\x68\\x5f\\x63\\x6c\\x6f\\x73\\x65\\x3c\\\n\\x2f\\x72\\x64\\x66\\x3a\\x6c\\x69\\x3e\\x0a\\x20\\x20\\x20\\x20\\x3c\\x2f\\x72\\\n\\x64\\x66\\x3a\\x41\\x6c\\x74\\x3e\\x0a\\x20\\x20\\x20\\x3c\\x2f\\x64\\x63\\x3a\\\n\\x74\\x69\\x74\\x6c\\x65\\x3e\\x0a\\x20\\x20\\x20\\x3c\\x78\\x6d\\x70\\x4d\\x4d\\\n\\x3a\\x48\\x69\\x73\\x74\\x6f\\x72\\x79\\x3e\\x0a\\x20\\x20\\x20\\x20\\x3c\\x72\\\n\\x64\\x66\\x3a\\x53\\x65\\x71\\x3e\\x0a\\x20\\x20\\x20\\x20\\x20\\x3c\\x72\\x64\\\n\\x66\\x3a\\x6c\\x69\\x0a\\x20\\x20\\x20\\x20\\x20\\x20\\x73\\x74\\x45\\x76\\x74\\\n\\x3a\\x61\\x63\\x74\\x69\\x6f\\x6e\\x3d\\x22\\x70\\x72\\x6f\\x64\\x75\\x63\\x65\\\n\\x64\\x22\\x0a\\x20\\x20\\x20\\x20\\x20\\x20\\x73\\x74\\x45\\x76\\x74\\x3a\\x73\\\n\\x6f\\x66\\x74\\x77\\x61\\x72\\x65\\x41\\x67\\x65\\x6e\\x74\\x3d\\x22\\x41\\x66\\\n\\x66\\x69\\x6e\\x69\\x74\\x79\\x20\\x44\\x65\\x73\\x69\\x67\\x6e\\x65\\x72\\x20\\\n\\x31\\x2e\\x39\\x2e\\x32\\x22\\x0a\\x20\\x20\\x20\\x20\\x20\\x20\\x73\\x74\\x45\\\n\\x76\\x74\\x3a\\x77\\x68\\x65\\x6e\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x30\\x35\\\n\\x2d\\x33\\x31\\x54\\x31\\x32\\x3a\\x34\\x33\\x3a\\x30\\x39\\x2b\\x30\\x32\\x3a\\\n\\x30\\x30\\x22\\x2f\\x3e\\x0a\\x20\\x20\\x20\\x20\\x3c\\x2f\\x72\\x64\\x66\\x3a\\\n\\x53\\x65\\x71\\x3e\\x0a\\x20\\x20\\x20\\x3c\\x2f\\x78\\x6d\\x70\\x4d\\x4d\\x3a\\\n\\x48\\x69\\x73\\x74\\x6f\\x72\\x79\\x3e\\x0a\\x20\\x20\\x3c\\x2f\\x72\\x64\\x66\\\n\\x3a\\x44\\x65\\x73\\x63\\x72\\x69\\x70\\x74\\x69\\x6f\\x6e\\x3e\\x0a\\x20\\x3c\\\n\\x2f\\x72\\x64\\x66\\x3a\\x52\\x44\\x46\\x3e\\x0a\\x3c\\x2f\\x78\\x3a\\x78\\x6d\\\n\\x70\\x6d\\x65\\x74\\x61\\x3e\\x0a\\x3c\\x3f\\x78\\x70\\x61\\x63\\x6b\\x65\\x74\\\n\\x20\\x65\\x6e\\x64\\x3d\\x22\\x72\\x22\\x3f\\x3e\\x58\\xad\\xf2\\x80\\x00\\x00\\\n\\x01\\x83\\x69\\x43\\x43\\x50\\x73\\x52\\x47\\x42\\x20\\x49\\x45\\x43\\x36\\x31\\\n\\x39\\x36\\x36\\x2d\\x32\\x2e\\x31\\x00\\x00\\x28\\x91\\x75\\x91\\xcf\\x2b\\x44\\\n\\x51\\x14\\xc7\\x3f\\x66\\x68\\xfc\\x18\\x8d\\x62\\x61\\x31\\x65\\x12\\x16\\x42\\\n\\x83\\x12\\x1b\\x8b\\x99\\x18\\x0a\\x8b\\x99\\x51\\x7e\\x6d\\x66\\x9e\\x79\\x33\\\n\\x6a\\xde\\x78\\xbd\\x37\\xd2\\x64\\xab\\x6c\\xa7\\x28\\xb1\\xf1\\x6b\\xc1\\x5f\\\n\\xc0\\x56\\x59\\x2b\\x45\\xa4\\x64\\xa7\\xac\\x89\\x0d\\x7a\\xce\\x9b\\x51\\x23\\\n\\x99\\x73\\x3b\\xf7\\x7c\\xee\\xf7\\xde\\x73\\xba\\xf7\\x5c\\x70\\x44\\xd3\\x8a\\\n\\x66\\x56\\xfa\\x41\\xcb\\x64\\x8d\\x70\\x28\\xe0\\x9b\\x99\\x9d\\xf3\\xb9\\x9e\\\n\\xa8\\xa2\\x85\\x1a\\x3a\\xf1\\xc6\\x14\\x53\\x9f\\x8c\\x8c\\x46\\x29\\x6b\\xef\\\n\\xb7\\x54\\xd8\\xf1\\xba\\xdb\\xae\\x55\\xfe\\xdc\\xbf\\x56\\xb7\\x98\\x30\\x15\\\n\\xa8\\xa8\\x16\\x1e\\x56\\x74\\x23\\x2b\\x3c\\x26\\x3c\\xb1\\x9a\\xd5\\x6d\\xde\\\n\\x12\\x6e\\x52\\x52\\xb1\\x45\\xe1\\x13\\xe1\\x2e\\x43\\x2e\\x28\\x7c\\x63\\xeb\\\n\\xf1\\x22\\x3f\\xdb\\x9c\\x2c\\xf2\\xa7\\xcd\\x46\\x34\\x1c\\x04\\x47\\x83\\xb0\\\n\\x2f\\xf9\\x8b\\xe3\\xbf\\x58\\x49\\x19\\x9a\\xb0\\xbc\\x9c\\x36\\x2d\\xbd\\xa2\\\n\\xfc\\xdc\\xc7\\x7e\\x89\\x3b\\x91\\x99\\x8e\\x48\\x6c\\x15\\xf7\\x62\\x12\\x26\\\n\\x44\\x00\\x1f\\xe3\\x8c\\x10\\x64\\x80\\x5e\\x86\\x64\\x1e\\xa0\\x9b\\x3e\\x7a\\\n\\x64\\x45\\x99\\x7c\\x7f\\x21\\x7f\\x8a\\x65\\xc9\\x55\\x64\\xd6\\xc9\\x61\\xb0\\\n\\x44\\x92\\x14\\x59\\xba\\x44\\x5d\\x91\\xea\\x09\\x89\\xaa\\xe8\\x09\\x19\\x69\\\n\\x72\\x76\\xff\\xff\\xf6\\xd5\\x54\\xfb\\xfb\\x8a\\xd5\\xdd\\x01\\xa8\\x7a\\xb4\\\n\\xac\\xd7\\x76\\x70\\x6d\\xc2\\x57\\xde\\xb2\\x3e\\x0e\\x2c\\xeb\\xeb\\x10\\x9c\\\n\\x0f\\x70\\x9e\\x29\\xe5\\x2f\\xef\\xc3\\xe0\\x9b\\xe8\\xf9\\x92\\xd6\\xb6\\x07\\\n\\x9e\\x75\\x38\\xbd\\x28\\x69\\xf1\\x6d\\x38\\xdb\\x80\\xe6\\x7b\\x3d\\x66\\xc4\\\n\\x0a\\x92\\x53\\xdc\\xa1\\xaa\\xf0\\x72\\x0c\\xf5\\xb3\\xd0\\x78\\x05\\xb5\\xf3\\\n\\xc5\\x9e\\xfd\\xec\\x73\\x74\\x07\\xd1\\x35\\xf9\\xaa\\x4b\\xd8\\xd9\\x85\\x0e\\\n\\x39\\xef\\x59\\xf8\\x06\\x8e\\xfd\\x67\\xf8\\xfd\\x8a\\x18\\x97\\x00\\x00\\x00\\\n\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\\n\\x9c\\x18\\x00\\x00\\x00\\xa2\\x49\\x44\\x41\\x54\\x18\\x95\\x55\\xcf\\xb1\\x4a\\\n\\xc3\\x31\\x00\\xc4\\xe1\\x2f\\xff\\xb9\\x93\\xa3\\x93\\xb8\\xa5\\x8b\\x0f\\x20\\\n\\x55\\x44\\x10\\x5c\\x3a\\x84\\x2c\\x1d\\x5c\\x7c\\x0f\\xb7\\x8e\\x3e\\x4a\\x88\\\n\\xa3\\xb8\\x08\\x6d\\x05\\xbb\\x77\\xc8\\xea\\xe2\\x0b\\x74\\x6f\\xe9\\xd2\\x42\\\n\\x7a\\x70\\x70\\xf0\\xe3\\x0e\\x2e\\xa4\\xd2\\xae\\xf0\\x8a\\xf7\\x9a\\xe3\\x56\\\n\\xa7\\x01\\xd7\\x78\\xc3\\x32\\x95\\x76\\x79\\x06\\x6b\\x8e\\xdf\\x78\\xc1\\x18\\\n\\xbf\\xa9\\xb4\\xf1\\x09\\x86\\x53\\x48\\xa5\\x3d\\xe2\\x03\\x3b\\x4c\\x6b\\x8e\\\n\\xab\\xd0\\xcf\\xa4\\xd2\\x6e\\xf0\\x89\\x0b\\xdc\\x0f\\xce\\xb5\\x3f\\x3a\\x20\\\n\\x0c\\x5d\\xeb\\x01\\x3f\\x18\\xe1\\xa9\\xe6\\xb8\\x1e\\x8e\\x60\\x86\\x2f\\x6c\\\n\\x71\\x5b\\x73\\x5c\\x40\\x48\\xa5\\xdd\\x61\\x81\\x0d\\x9e\\x6b\\x8e\\xff\\xfd\\\n\\xcf\\x3f\\xcc\\x31\\xe9\\x01\\x1c\\x00\\x73\\x52\\x2d\\x71\\xe4\\x4a\\x1b\\x69\\\n\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\x9e\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce\\x7c\\x4e\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x08\\x15\\x0f\\xfd\\\n\\x8f\\xf8\\x2e\\x00\\x00\\x00\\x22\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\xc0\\\n\\x0d\\xfe\\x9f\\x87\\xb1\\x18\\x91\\x05\\x18\\x0d\\xe1\\x42\\x48\\x2a\\x0c\\x19\\\n\\x18\\x18\\x91\\x05\\x10\\x2a\\xd1\\x00\\x00\\xca\\xb5\\x07\\xd2\\x76\\xbb\\xb2\\\n\\xc5\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x14\\x1d\\x00\\xb0\\\n\\xd5\\x35\\xa3\\x00\\x00\\x00\\x2a\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\xc0\\\n\\x06\\xfe\\x9f\\x67\\x60\\x60\\x42\\x30\\xa1\\x1c\\x08\\x93\\x81\\x81\\x09\\xc1\\\n\\x64\\x60\\x60\\x62\\x60\\x60\\x34\\x44\\xe2\\x20\\x73\\x19\\x90\\x8d\\x40\\x02\\\n\\x00\\x64\\x40\\x09\\x75\\x86\\xb3\\xad\\x9c\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\\n\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\xa5\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce\\x7c\\x4e\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\x9c\\x53\\x34\\xfc\\x5d\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x0b\\x02\\x04\\x6d\\\n\\x98\\x1b\\x69\\x00\\x00\\x00\\x29\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\xc0\\\n\\x00\\x8c\\x0c\\x0c\\xff\\xcf\\xa3\\x08\\x18\\x32\\x32\\x30\\x20\\x0b\\x32\\x1a\\\n\\x32\\x30\\x30\\x42\\x98\\x10\\x41\\x46\\x43\\x14\\x13\\x50\\xb5\\xa3\\x01\\x00\\\n\\xd6\\x10\\x07\\xd2\\x2f\\x48\\xdf\\x4a\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\\n\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\x9e\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce\\x7c\\x4e\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x08\\x15\\x0f\\xfd\\\n\\x8f\\xf8\\x2e\\x00\\x00\\x00\\x22\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\xc0\\\n\\x0d\\xfe\\x9f\\x87\\xb1\\x18\\x91\\x05\\x18\\x0d\\xe1\\x42\\x48\\x2a\\x0c\\x19\\\n\\x18\\x18\\x91\\x05\\x10\\x2a\\xd1\\x00\\x00\\xca\\xb5\\x07\\xd2\\x76\\xbb\\xb2\\\n\\xc5\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x01\\x57\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x30\\x00\\x00\\x00\\x30\\x08\\x06\\x00\\x00\\x00\\x57\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x09\\x49\\x44\\x41\\x54\\x68\\x81\\xed\\\n\\xda\\xcd\\x6d\\xc2\\x40\\x14\\x45\\xe1\\xf3\\x8c\\x49\\x05\\x51\\x9a\\x48\\x36\\\n\\xec\\x59\\xd1\\x05\\xc5\\x90\\x45\\x6a\\xa3\\x04\\x52\\x04\\x88\\x34\\x60\\x82\\\n\\x6e\\x16\\x33\\xf9\\xb1\\xa5\\x28\\x44\\x48\\x5c\\x5b\\x7a\\xdf\\x8e\\xc1\\x8b\\\n\\x77\\x8c\\xcd\\x66\\x26\\xa8\\x24\\xdd\\x03\\xcf\\xc0\\x12\\x78\\x02\\xe6\\x8c\\\n\\xcb\\x09\\xd8\\x01\\x5b\\xe0\\x25\\x22\\x8e\\x5f\\xdf\\x48\\x5a\\x49\\xda\\x6b\\\n\\x3a\\xf6\\x92\\x56\\x00\\xa1\\x72\\xe7\\x5f\\x81\\x07\\xc7\\x6d\\xbd\\xc2\\x01\\\n\\x78\\x6c\\x28\\x8f\\xcd\\xd4\\x86\\x87\\x32\\xf3\\xa6\\xa5\\x3c\\xf3\\x43\\xe7\\\n\\x1b\\x0f\\x73\\xa9\\xd9\\xe0\\xf3\\x32\\x24\\x75\\xf4\\x5f\\xd8\\x73\\x44\\xb4\\\n\\x37\\x1c\\xea\\x62\\x92\\xde\\xe9\\x47\\x9c\\x1a\\xc6\\xf7\\x6f\\xf3\\x1f\\xf3\\\n\\xc6\\x3d\\xc1\\xb5\\x32\\xc0\\x2d\\x03\\xdc\\x32\\xc0\\x2d\\x03\\xdc\\x32\\xc0\\\n\\x2d\\x03\\xdc\\x32\\xc0\\x2d\\x03\\xdc\\x32\\xc0\\x2d\\x03\\xdc\\x32\\xc0\\x2d\\\n\\x03\\xdc\\x32\\xc0\\x2d\\x03\\xdc\\x32\\xc0\\x2d\\x03\\xdc\\x32\\xc0\\xad\\xa1\\\n\\xec\\x80\\x4f\\x55\\xd7\\x52\\xb6\\xef\\x17\\x3f\\x16\\x67\\x75\\x37\\x70\\x8c\\\n\\x86\\xdb\\xac\\xbb\\x96\\x72\\xf6\\x60\\xf1\\xc7\\x85\\x63\\xb5\\x9d\\xfe\\x51\\\n\\x83\\x7a\\xea\\x63\\x5d\\x17\\xa6\\xe2\\x00\\xac\\x23\\xe2\\x18\\x9f\\x2b\\xf5\\\n\\x97\\xd8\\xf0\\x7d\\xdc\\xe6\\xce\\x34\\xdc\\x6f\\x3a\\xfa\\xc7\\x6d\\xde\\x00\\\n\\x3e\\x00\\x47\\xd7\\xea\\xb1\\xad\\x69\\xe1\\xd6\\x00\\x00\\x00\\x00\\x49\\x45\\\n\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x04\\x12\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x30\\x00\\x00\\x00\\x30\\x08\\x06\\x00\\x00\\x00\\x57\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x03\\xc4\\x49\\x44\\x41\\x54\\x68\\x81\\xed\\\n\\x9a\\x5f\\x88\\x94\\x55\\x18\\xc6\\x7f\\x33\\x3b\\x1a\\x0b\\x19\\x15\\x66\\x17\\\n\\xca\\x03\\x49\\x50\\x4d\\x09\\x4a\\x37\\x51\\x5e\\x44\\x29\\x26\\x66\\x05\\x5b\\\n\\xb9\\xd2\\x82\\xb1\\x50\\x17\\x91\\x24\\x74\\x11\\x08\\x7a\\xa1\\x44\\x17\\x5d\\\n\\x54\\x94\\x04\\xd6\\x45\\xe1\\xa2\\x15\\x4c\\xb1\\x18\\x99\\xfd\\x25\\xd8\\x08\\\n\\x82\\x6e\\x6a\\x5d\\xa4\\x22\\xe2\\x81\\xa8\\x96\\xd5\\x28\\xe8\\x9f\\xae\\xd5\\\n\\xc5\\xf9\\xb6\\xc6\\xd9\\xf9\\xbe\\x73\\x66\\xc6\\x76\\x66\\xc0\\xdf\\xdd\\x9c\\\n\\xef\\x3d\\xef\\x79\\x9f\\x73\\xe6\\x9c\\xf3\\x7e\\xdf\\x39\\x25\\x32\\x46\\x6a\\\n\\x53\\x4b\\x81\\xdd\\xc0\\x5a\\xe0\\x3a\\x60\\x11\\xbd\\xc5\\x69\\x60\\x12\\x98\\\n\\x00\\xf6\\x8c\\x0d\\x55\\x67\\x00\\x4a\\x00\\x23\\xb5\\xa9\\x5b\\x80\\x43\\xc0\\\n\\xb2\\xae\\x85\\xd7\\x1a\\xd3\\xc0\\xd6\\xb1\\xa1\\xea\\x07\\xa5\\xac\\xe7\\x8f\\\n\\xd1\\x3f\\xc1\\xcf\\x31\\x0d\\x5c\\x5b\\x26\\xfc\\x6d\\xfa\\x2d\\x78\\x08\\x31\\\n\\xef\\xaa\\x10\\xfe\\xf3\\x8d\\x9c\\x59\\xe0\\x60\\x52\\x19\\x68\\xf8\\xbd\\xb6\\\n\\x42\\x98\\xb0\\xf5\\x9c\\x19\\x1b\\xaa\\x56\\x16\\x28\\xa0\\x96\\x18\\xa9\\x4d\\\n\\xcd\\x72\\xb6\\x88\\x55\\x65\\x7a\\x6f\\xb5\\x69\\x85\\x45\\xe5\\x6e\\x47\\xd0\\\n\\x29\\xe7\\x05\\x74\\x9b\\xf3\\x02\\xfe\\x0f\\x6c\\xdf\\x63\\xfb\\x90\\xed\\xf5\\\n\\x31\\xdb\\x9e\\x13\\x60\\xfb\\x11\\xe0\\x35\\x60\\x18\\x38\\x6a\\x7b\\x9f\\xed\\\n\\xdc\\x95\\xb2\\xa7\\x04\\xd8\\x1e\\x06\\x9e\\xaa\\x2b\\x2a\\x01\\x0f\\x01\\x2f\\\n\\xe4\\xd5\\xe9\\x19\\x01\\xb6\\xd7\\x01\\x2f\\x93\\x25\\x98\\x0d\\x6c\\xb3\\xfd\\\n\\x68\\xb3\\x7a\\x3d\\x21\\xc0\\xf6\\xf5\\xc0\\xeb\\xc0\\xe2\\x02\\xb3\\x8d\\xcd\\\n\\x0a\\xbb\\x2e\\xc0\\xf6\\x95\\xc0\\x5b\\xc0\\x92\\x88\\xe9\\x8b\\xcd\\x0a\\xbb\\\n\\x2a\\xc0\\xf6\\xe5\\xc0\\x51\\xe2\\xd9\\xf0\\x93\\x92\\x5e\\x69\\xf6\\xa0\\x6b\\\n\\x02\\x6c\\x2f\\x01\\x8e\\x00\\x2b\\x23\\xa6\\x07\\x80\\xc7\\xf2\\x1e\\x76\\x45\\\n\\x80\\xed\\xc5\\xc0\\x1b\\xc0\\x9a\\x88\\xe9\\xdb\\xc0\\xa8\\xa4\\xbf\\xf3\\x0c\\\n\\x16\\x5c\\x80\\xed\\x32\\xa1\\x57\\x6f\\x8d\\x98\\x7e\\x0a\\xdc\\x2d\\x69\\xb6\\\n\\xc8\\xa8\\x1b\\x23\\xf0\\x34\\x70\\x6f\\xc4\\xe6\\x4b\\x60\\x93\\xa4\\x5f\\x63\\\n\\xce\\x92\\x04\\xd8\\x2e\\x65\\x3d\\xd7\\x11\\xb6\\x77\\x02\\xdb\\x23\\x66\\xdf\\\n\\x03\\x1b\\x24\\xcd\\xa4\\xf8\\x2c\\x0c\\x2a\\x0b\\x7c\\x07\\xf0\\x3b\\x30\\x69\\\n\\x7b\\x55\\x52\\xa4\\xcd\\x7d\\x8d\\x02\\x8f\\x47\\xcc\\x7e\\x06\\x6e\\x93\\xf4\\\n\\x6d\\xaa\\xdf\\x5c\\x01\\x59\\xfe\\x31\\x4e\\xd8\\xda\\x2f\\x00\\xae\\x01\\xde\\\n\\xb3\\x7d\\x55\\xaa\\xf3\\x3a\\x5f\\x9b\\x81\\xfd\\x11\\xb3\\x3f\\x81\\x3b\\x25\\\n\\x7d\\xde\\x8a\\xef\\xa2\\x11\\x78\\x0e\\xd8\\xdc\\x50\\xb6\\x0c\\x78\\xdf\\xf6\\\n\\x15\\xa9\\x0d\\xd8\\xbe\\x11\\x78\\x95\\xf9\\x2f\\xe4\\xf5\\xfc\\x05\\xdc\\x27\\\n\\xe9\\xa3\\x54\\xbf\\x73\\x34\\x15\\x90\\xf5\\xd8\\x83\\x39\\x75\\x96\\x13\\x46\\\n\\x62\\x79\\xcc\\xb9\\xed\\x2a\\x70\\x18\\x18\\x8c\\x98\\x3e\\x2c\\xa9\\x16\\xf3\\\n\\xd7\\x8c\\xbc\\x11\\xb8\\x34\\x52\\x6f\\x25\\x41\\xc4\\x65\\x79\\x06\\xb6\\x57\\\n\\x10\\x76\\xd9\\x98\\xaf\\xbd\\x92\\x9e\\x8f\\xd8\\xe4\\x92\\x27\\xe0\\x20\\x61\\\n\\x1d\\x2e\\xe2\\x6a\\xe0\\x1d\\xdb\\x17\\x37\\x3e\\xb0\\x7d\\x09\\x21\\xf8\\x15\\\n\\x11\\x1f\\xfb\\x25\\xed\\x8e\\x46\\x59\\x40\\x53\\x01\\x92\\x4e\\x13\\xb2\\xbf\\\n\\x2f\\x22\\xf5\\x57\\x03\\x47\\x6c\\x5f\\x38\\x57\\x60\\x7b\\x90\\xf0\\xb7\\xa9\\\n\\x46\\xea\\x8e\\x13\\x72\\xfd\\x8e\\xc8\\x9d\\xc4\\x92\\x4e\\x02\\xeb\\x81\\xaf\\\n\\x22\\x3e\\x6e\\x00\\x0e\\xdb\\x1e\\xb4\\x3d\\x40\\x98\\xb0\\x37\\x45\\xea\\x4c\\\n\\x00\\xc3\\x92\\x3a\\xfe\\x02\\x58\\xb8\\x0f\\x48\\xfa\\x11\\x58\\x07\\x38\\xe2\\\n\\xe7\\x66\\xa0\\x46\\x58\\x2a\\x1b\\x57\\xae\\x46\\x26\\x81\\x3b\\x24\\xfd\\x91\\\n\\x18\\x63\\x21\\xd1\\xdd\\x55\\x92\\x09\\x79\\xcb\\x0f\\x11\\xd3\\x8d\\xc0\\x68\\\n\\xc4\\xc6\\x84\\x8d\\xea\\xa7\\xb4\\xf0\\xe2\\x24\\xa5\\x07\\x92\\xbe\\x26\\x8c\\\n\\xc4\\x89\\x0e\\xda\\x3a\\x49\\x48\\x11\\xbe\\xeb\\xc0\\xc7\\x3c\\x92\\xf3\\x1b\\\n\\x49\\xc7\\x80\\x0d\\xc0\\x2f\\x6d\\xb4\\xf3\\x1b\\x70\\xbb\\xa4\\xe3\\x6d\\xd4\\\n\\x2d\\xa4\\xa5\\x04\\x4d\\xd2\\x67\\xc0\\xa6\\x2c\\xa0\\x54\\x66\\x81\\x2d\\x92\\\n\\x3e\\x69\\xa5\\xad\\x54\\x5a\\xce\\x30\\x25\\x4d\\x00\\x77\\x11\\x72\\x97\\x14\\\n\\x1e\\x90\\xf4\\x66\\xab\\xed\\xa4\\xd2\\x56\\x8a\\x2c\\xe9\\x5d\\x60\\x0b\\xa1\\\n\\x77\\x8b\\xd8\\x29\\xe9\\xa5\\x76\\xda\\x48\\xa5\\xed\\x1c\\x5f\\xd2\\x38\\xb0\\\n\\x8d\\x90\\x88\\x35\\xe3\\x59\\x49\\x4f\\xb4\\xeb\\x3f\\x95\\x8e\\x5e\\x52\\x24\\\n\\x1d\\x04\\xee\\x67\\xfe\\x9c\\xd8\\x07\\xec\\xe8\\xc4\\x77\\x2a\\x1d\\x1f\\x25\\\n\\x49\\x3a\\x60\\xfb\\x63\\xc2\\x06\\x76\\x11\\xf0\\x61\\x36\\x4f\\x16\\x84\\x73\\\n\\x72\\x16\\x26\\xe9\\x1b\\xe0\\x99\\x73\\xe1\\xab\\x55\\xca\\x84\\x13\\xf0\\x7e\\\n\\xe5\\x54\\x85\\x90\\x9b\\xd4\\x7f\\x9f\\x19\\xc8\\x4e\\x03\\x7b\\x91\\xc6\\xb7\\\n\\xba\\xc9\\x0a\\x21\\x33\\x6c\\xfc\\xc0\\x54\\xf4\\xfa\\xd7\\x4b\\x4c\\x94\\x81\\\n\\x3d\\x84\\x63\\xfb\\x7e\\x63\\x1a\\xd8\\x5b\\xce\\x6e\\x7d\\x6c\\xa5\\xbf\\x44\\\n\\xcc\\x5d\\xf6\\x98\\xf9\\xf7\\x30\\x21\\xbb\\xf4\\xb1\\x8b\\xff\\xae\\xdb\\x14\\\n\\x7d\\xab\\xef\\x06\\xa7\\x38\\xfb\\xba\\xcd\\x09\\x80\\x7f\\x00\\xc4\\x1e\\x10\\\n\\x29\\x33\\x5b\\x85\\xf7\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\\n\\x82\\\n\\x00\\x00\\x01\\xe1\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x30\\x00\\x00\\x00\\x30\\x08\\x06\\x00\\x00\\x00\\x57\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x93\\x49\\x44\\x41\\x54\\x68\\x81\\xed\\\n\\x9a\\x3b\\x4e\\xc3\\x40\\x10\\x86\\xbf\\x71\\x1c\\x2a\\xe8\\x10\\xe5\\x36\\x94\\\n\\xd0\\xa4\\xa1\\x8a\\x28\\x22\\x0a\\x0a\\x44\\x9f\\x9e\\x0b\\x70\\x80\\x50\\x70\\\n\\x01\\x2e\\xc0\\x15\\x68\\x80\\x13\\xa0\\x1c\\x21\\x50\\x91\\x66\\xbb\\x44\\xa1\\\n\\x42\\x34\\x79\\x68\\x28\\x6c\\x1e\\xb1\\xfc\\x48\\x08\\xc9\\xda\\xd2\\x7e\\x9d\\\n\\x77\\x5c\\xfc\\x9f\\xb3\\x1e\\x39\\xda\\x11\\x62\\x54\\x75\\x17\\xb8\\x02\\x9a\\\n\\xc0\\x21\\x50\\xa7\\x5c\\x4c\\x80\\x1e\\xd0\\x05\\xae\\x45\\x64\\xf4\\x5d\\x51\\\n\\xd5\\x96\\xaa\\x0e\\xb4\\x3a\\x0c\\x54\\xb5\\x05\\x20\\x1a\\x3d\\xf9\\x67\\x60\\\n\\xcf\\xc5\\x63\\x5d\\x81\\x21\\x70\\x10\\x10\\x6d\\x9b\\xaa\\x85\\x87\\x28\\x73\\\n\\x27\\x24\\xda\\xf3\\x49\\x66\\x1b\\x0e\\xb3\\x28\\xb5\\xc4\\x75\\x53\\x54\\x75\\\n\\xcc\\xfc\\x0b\\x3b\\x13\\x91\\x70\\x83\\xa1\\x16\\x46\\x55\\xa7\\xcc\\x4b\\x4c\\\n\\x02\\xca\\xd7\\x6d\\x96\\xa1\\x1e\\xb8\\x4e\\xb0\\x2a\\x5e\\xc0\\x35\\x5e\\xc0\\\n\\x35\\x5e\\xc0\\x35\\x5e\\xc0\\x35\\x5e\\xc0\\x35\\x5e\\xc0\\x35\\x85\\x9f\\xcd\\\n\\xd6\\xda\\x13\\xe0\\x08\\xd8\\x5e\\x7f\\x9c\\x39\\xa6\\xc0\\x0b\\xf0\\x60\\x8c\\\n\\xf9\\xc8\\xba\\x49\\x54\\x55\\x13\\x6b\\x33\\x11\\x09\\xad\\xb5\\x21\\x70\\x07\\\n\\x9c\\xaf\\x31\\xe4\\x22\\xf4\\x81\\x53\\x63\\xcc\\x6b\\xca\\xff\\x81\\xdc\\x2d\\\n\\x74\\x89\\xfb\\xf0\\x00\\xfb\\xc0\\x6d\\x56\\x31\\x4f\\xe0\\xec\\xff\\xb3\\xfc\\\n\\x99\\x63\\x6b\\xed\\x4e\\x5a\\xa1\\xf2\\x2f\\x71\\x9e\\xc0\\xe3\\xc6\\x52\\x14\\\n\\xf3\\x64\\x8c\\x79\\x4f\\x2b\\xe4\\x09\\xdc\\x00\\xf7\\xeb\\xc9\\xb3\\x14\\x7d\\\n\\xe0\\x22\\xab\\x98\\xd9\\x85\\xbe\\x2e\\xca\\xd4\\x46\\xd3\\xba\\x50\\xa1\\x40\\\n\\x99\\x58\\xb6\\x8d\\x56\\x02\\x2f\\xe0\\x1a\\x2f\\xe0\\x1a\\x2f\\xe0\\x1a\\x2f\\\n\\xe0\\x1a\\x2f\\xe0\\x1a\\x2f\\xe0\\x9a\\x80\\xe8\\x04\\xbc\\xaa\\x8c\\x43\\xa2\\\n\\xe3\\xfb\\xc6\\xaf\\xc5\\x5a\\xfc\\xd9\\x5a\\x46\\x92\\xc7\\xac\\xbd\\x90\\x68\\\n\\xf6\\xa0\\x51\\x70\\x63\\x59\\xe9\\x56\\x7f\\xd4\\x20\\x9e\\xfa\\x68\\xc7\\x0b\\\n\\x55\\x61\\x08\\xb4\\x45\\x64\\x24\\x5f\\x2b\\xf1\\x2f\\xd1\\xe1\\x67\\xdc\\x66\\\n\\xcb\\x51\\xb8\\x2c\\xc6\\xcc\\x8f\\xdb\\xbc\\x01\\x7c\\x02\\x6d\\x77\\x23\\xb3\\\n\\xd4\\x95\\x53\\x76\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\\n\\x00\\x00\\x01\\x76\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x30\\x00\\x00\\x00\\x30\\x08\\x06\\x00\\x00\\x00\\x57\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x28\\x49\\x44\\x41\\x54\\x68\\x81\\xed\\\n\\xda\\xb1\\x4a\\xc3\\x50\\x14\\x87\\xf1\\x2f\\x37\\xb7\\xe0\\xae\\xf8\\x00\\x82\\\n\\x53\\x75\\xe8\\xde\\xc9\\x6c\\x79\\x80\\x40\\x1f\\x46\\x87\\xfa\\x22\\x6e\\x42\\\n\\xdc\\xb3\\xc5\\xa9\\x2f\\x20\\xb4\\x5d\\x3a\\x74\\x0f\\x7d\\x82\\x6a\\xc1\\xe1\\\n\\xa6\\x50\\xb3\\x68\\x10\\xfa\\xcf\\x85\\xf3\\xdb\\x52\\x3a\\x9c\\xaf\\xdc\\x66\\\n\\x39\\x37\\xa1\\x95\\xe5\\xc5\\x15\\xf0\\x04\\x4c\\x81\\x3b\\x60\\xc4\\xb0\\x7c\\\n\\x02\\x4b\\x60\\x01\\xcc\\xeb\\xaa\\xdc\\x01\\x24\\x00\\x59\\x5e\\x3c\\x00\\xaf\\\n\\xc0\\xb5\\x6c\\xbc\\x7e\\x1a\\x60\\x56\\x57\\xe5\\x7b\\xd2\\xfe\\xf2\\x2b\\xe2\\\n\\x19\\xfe\\xa8\\x01\\xc6\\x8e\\x70\\x6c\\x62\\x1b\\x1e\\xc2\\xcc\\x8f\\x9e\\x70\\\n\\xe6\\xbb\\x0e\\x67\\x1e\\xe6\\xaf\\xd2\\xce\\xf3\\xd4\\x13\\xfe\\xb0\\xa7\\x0e\\\n\\x75\\x55\\xfa\\x33\\x0d\\xd4\\x4b\\x96\\x17\\x5f\\xfc\\x8c\\xb8\\x77\\x0c\\xef\\\n\\x6d\\xd3\\xc7\\xc8\\xa9\\x27\\xf8\\x2f\\x0b\\x50\\xb3\\x00\\x35\\x0b\\x50\\xb3\\\n\\x00\\x35\\x0b\\x50\\xb3\\x00\\x35\\x0b\\x50\\xb3\\x00\\x35\\x0b\\x50\\xb3\\x00\\\n\\x35\\x0b\\x50\\xb3\\x00\\x35\\x0b\\x50\\xb3\\x00\\x35\\x0b\\x50\\xb3\\x00\\x35\\\n\\x0b\\x50\\x73\\x84\\x0d\\x78\\xac\\xf6\\x9e\\xb0\\xbe\\x9f\\x9c\\x7c\\x98\\xb6\\\n\\xdb\\xc0\\x21\\xea\\xae\\x59\\x97\\x9e\\x70\\xf7\\x60\\xf2\\xcb\\x17\\x87\\x6a\\\n\\xe1\\x80\\x39\\x61\\x6d\\x1f\\x9b\\x06\\x78\\x76\\xed\\xad\\x8f\\x19\\x71\\x45\\\n\\x1c\\x2f\\x7b\\xec\\x52\\x80\\xed\\x66\\xb5\\xbd\\xb9\\x1d\\xbf\\x00\\x17\\x84\\\n\\xc5\\xf7\\x25\\xc3\\x3b\\x46\\x7b\\xe0\\x03\\x78\\x03\\x8a\\xba\\x2a\\xd7\\x00\\\n\\xdf\\xa4\\xb5\\x36\\xa2\\xca\\x99\\x74\\x47\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\\n\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\xa0\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x14\\x1f\\x0d\\xfc\\\n\\x52\\x2b\\x9c\\x00\\x00\\x00\\x24\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\x40\\\n\\x05\\x73\\x3e\\xc0\\x58\\x4c\\xc8\\x5c\\x26\\x64\\x59\\x26\\x64\\xc5\\x70\\x4e\\\n\\x8a\\x00\\x9c\\x93\\x22\\x80\\x61\\x1a\\x0a\\x00\\x00\\x29\\x95\\x08\\xaf\\x88\\\n\\xac\\xba\\x34\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x05\\x7e\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x08\\x06\\x00\\x00\\x00\\x1f\\x15\\xc4\\x89\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x05\\x17\\x69\\x54\\x58\\x74\\x58\\x4d\\x4c\\\n\\x3a\\x63\\x6f\\x6d\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x78\\x6d\\x70\\x00\\x00\\\n\\x00\\x00\\x00\\x3c\\x3f\\x78\\x70\\x61\\x63\\x6b\\x65\\x74\\x20\\x62\\x65\\x67\\\n\\x69\\x6e\\x3d\\x22\\xef\\xbb\\xbf\\x22\\x20\\x69\\x64\\x3d\\x22\\x57\\x35\\x4d\\\n\\x30\\x4d\\x70\\x43\\x65\\x68\\x69\\x48\\x7a\\x72\\x65\\x53\\x7a\\x4e\\x54\\x63\\\n\\x7a\\x6b\\x63\\x39\\x64\\x22\\x3f\\x3e\\x20\\x3c\\x78\\x3a\\x78\\x6d\\x70\\x6d\\\n\\x65\\x74\\x61\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x78\\x3d\\x22\\x61\\x64\\x6f\\\n\\x62\\x65\\x3a\\x6e\\x73\\x3a\\x6d\\x65\\x74\\x61\\x2f\\x22\\x20\\x78\\x3a\\x78\\\n\\x6d\\x70\\x74\\x6b\\x3d\\x22\\x41\\x64\\x6f\\x62\\x65\\x20\\x58\\x4d\\x50\\x20\\\n\\x43\\x6f\\x72\\x65\\x20\\x37\\x2e\\x31\\x2d\\x63\\x30\\x30\\x30\\x20\\x37\\x39\\\n\\x2e\\x37\\x61\\x37\\x61\\x32\\x33\\x36\\x2c\\x20\\x32\\x30\\x32\\x31\\x2f\\x30\\\n\\x38\\x2f\\x31\\x32\\x2d\\x30\\x30\\x3a\\x32\\x35\\x3a\\x32\\x30\\x20\\x20\\x20\\\n\\x20\\x20\\x20\\x20\\x20\\x22\\x3e\\x20\\x3c\\x72\\x64\\x66\\x3a\\x52\\x44\\x46\\\n\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x72\\x64\\x66\\x3d\\x22\\x68\\x74\\x74\\x70\\\n\\x3a\\x2f\\x2f\\x77\\x77\\x77\\x2e\\x77\\x33\\x2e\\x6f\\x72\\x67\\x2f\\x31\\x39\\\n\\x39\\x39\\x2f\\x30\\x32\\x2f\\x32\\x32\\x2d\\x72\\x64\\x66\\x2d\\x73\\x79\\x6e\\\n\\x74\\x61\\x78\\x2d\\x6e\\x73\\x23\\x22\\x3e\\x20\\x3c\\x72\\x64\\x66\\x3a\\x44\\\n\\x65\\x73\\x63\\x72\\x69\\x70\\x74\\x69\\x6f\\x6e\\x20\\x72\\x64\\x66\\x3a\\x61\\\n\\x62\\x6f\\x75\\x74\\x3d\\x22\\x22\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x78\\x6d\\\n\\x70\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\\n\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x78\\x61\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x22\\\n\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\x64\\x63\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\\n\\x2f\\x2f\\x70\\x75\\x72\\x6c\\x2e\\x6f\\x72\\x67\\x2f\\x64\\x63\\x2f\\x65\\x6c\\\n\\x65\\x6d\\x65\\x6e\\x74\\x73\\x2f\\x31\\x2e\\x31\\x2f\\x22\\x20\\x78\\x6d\\x6c\\\n\\x6e\\x73\\x3a\\x78\\x6d\\x70\\x4d\\x4d\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\\n\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x78\\x61\\\n\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x6d\\x6d\\x2f\\x22\\x20\\x78\\x6d\\x6c\\x6e\\x73\\\n\\x3a\\x73\\x74\\x45\\x76\\x74\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\x2f\\x2f\\x6e\\\n\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x78\\x61\\x70\\x2f\\\n\\x31\\x2e\\x30\\x2f\\x73\\x54\\x79\\x70\\x65\\x2f\\x52\\x65\\x73\\x6f\\x75\\x72\\\n\\x63\\x65\\x45\\x76\\x65\\x6e\\x74\\x23\\x22\\x20\\x78\\x6d\\x6c\\x6e\\x73\\x3a\\\n\\x70\\x68\\x6f\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3d\\x22\\x68\\x74\\x74\\x70\\x3a\\\n\\x2f\\x2f\\x6e\\x73\\x2e\\x61\\x64\\x6f\\x62\\x65\\x2e\\x63\\x6f\\x6d\\x2f\\x70\\\n\\x68\\x6f\\x74\\x6f\\x73\\x68\\x6f\\x70\\x2f\\x31\\x2e\\x30\\x2f\\x22\\x20\\x78\\\n\\x6d\\x70\\x3a\\x43\\x72\\x65\\x61\\x74\\x6f\\x72\\x54\\x6f\\x6f\\x6c\\x3d\\x22\\\n\\x41\\x64\\x6f\\x62\\x65\\x20\\x50\\x68\\x6f\\x74\\x6f\\x73\\x68\\x6f\\x70\\x20\\\n\\x32\\x32\\x2e\\x35\\x20\\x28\\x57\\x69\\x6e\\x64\\x6f\\x77\\x73\\x29\\x22\\x20\\\n\\x78\\x6d\\x70\\x3a\\x43\\x72\\x65\\x61\\x74\\x65\\x44\\x61\\x74\\x65\\x3d\\x22\\\n\\x32\\x30\\x32\\x31\\x2d\\x31\\x31\\x2d\\x31\\x30\\x54\\x31\\x37\\x3a\\x33\\x39\\\n\\x3a\\x30\\x32\\x2b\\x30\\x31\\x3a\\x30\\x30\\x22\\x20\\x78\\x6d\\x70\\x3a\\x4d\\\n\\x65\\x74\\x61\\x64\\x61\\x74\\x61\\x44\\x61\\x74\\x65\\x3d\\x22\\x32\\x30\\x32\\\n\\x31\\x2d\\x31\\x31\\x2d\\x31\\x30\\x54\\x31\\x37\\x3a\\x33\\x39\\x3a\\x30\\x32\\\n\\x2b\\x30\\x31\\x3a\\x30\\x30\\x22\\x20\\x78\\x6d\\x70\\x3a\\x4d\\x6f\\x64\\x69\\\n\\x66\\x79\\x44\\x61\\x74\\x65\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x31\\x31\\x2d\\\n\\x31\\x30\\x54\\x31\\x37\\x3a\\x33\\x39\\x3a\\x30\\x32\\x2b\\x30\\x31\\x3a\\x30\\\n\\x30\\x22\\x20\\x64\\x63\\x3a\\x66\\x6f\\x72\\x6d\\x61\\x74\\x3d\\x22\\x69\\x6d\\\n\\x61\\x67\\x65\\x2f\\x70\\x6e\\x67\\x22\\x20\\x78\\x6d\\x70\\x4d\\x4d\\x3a\\x49\\\n\\x6e\\x73\\x74\\x61\\x6e\\x63\\x65\\x49\\x44\\x3d\\x22\\x78\\x6d\\x70\\x2e\\x69\\\n\\x69\\x64\\x3a\\x66\\x31\\x37\\x65\\x62\\x62\\x32\\x33\\x2d\\x65\\x36\\x32\\x61\\\n\\x2d\\x33\\x39\\x34\\x36\\x2d\\x61\\x39\\x37\\x35\\x2d\\x64\\x34\\x66\\x36\\x61\\\n\\x62\\x34\\x34\\x64\\x34\\x30\\x39\\x22\\x20\\x78\\x6d\\x70\\x4d\\x4d\\x3a\\x44\\\n\\x6f\\x63\\x75\\x6d\\x65\\x6e\\x74\\x49\\x44\\x3d\\x22\\x78\\x6d\\x70\\x2e\\x64\\\n\\x69\\x64\\x3a\\x66\\x31\\x37\\x65\\x62\\x62\\x32\\x33\\x2d\\x65\\x36\\x32\\x61\\\n\\x2d\\x33\\x39\\x34\\x36\\x2d\\x61\\x39\\x37\\x35\\x2d\\x64\\x34\\x66\\x36\\x61\\\n\\x62\\x34\\x34\\x64\\x34\\x30\\x39\\x22\\x20\\x78\\x6d\\x70\\x4d\\x4d\\x3a\\x4f\\\n\\x72\\x69\\x67\\x69\\x6e\\x61\\x6c\\x44\\x6f\\x63\\x75\\x6d\\x65\\x6e\\x74\\x49\\\n\\x44\\x3d\\x22\\x78\\x6d\\x70\\x2e\\x64\\x69\\x64\\x3a\\x66\\x31\\x37\\x65\\x62\\\n\\x62\\x32\\x33\\x2d\\x65\\x36\\x32\\x61\\x2d\\x33\\x39\\x34\\x36\\x2d\\x61\\x39\\\n\\x37\\x35\\x2d\\x64\\x34\\x66\\x36\\x61\\x62\\x34\\x34\\x64\\x34\\x30\\x39\\x22\\\n\\x20\\x70\\x68\\x6f\\x74\\x6f\\x73\\x68\\x6f\\x70\\x3a\\x43\\x6f\\x6c\\x6f\\x72\\\n\\x4d\\x6f\\x64\\x65\\x3d\\x22\\x33\\x22\\x20\\x70\\x68\\x6f\\x74\\x6f\\x73\\x68\\\n\\x6f\\x70\\x3a\\x49\\x43\\x43\\x50\\x72\\x6f\\x66\\x69\\x6c\\x65\\x3d\\x22\\x73\\\n\\x52\\x47\\x42\\x20\\x49\\x45\\x43\\x36\\x31\\x39\\x36\\x36\\x2d\\x32\\x2e\\x31\\\n\\x22\\x3e\\x20\\x3c\\x78\\x6d\\x70\\x4d\\x4d\\x3a\\x48\\x69\\x73\\x74\\x6f\\x72\\\n\\x79\\x3e\\x20\\x3c\\x72\\x64\\x66\\x3a\\x53\\x65\\x71\\x3e\\x20\\x3c\\x72\\x64\\\n\\x66\\x3a\\x6c\\x69\\x20\\x73\\x74\\x45\\x76\\x74\\x3a\\x61\\x63\\x74\\x69\\x6f\\\n\\x6e\\x3d\\x22\\x63\\x72\\x65\\x61\\x74\\x65\\x64\\x22\\x20\\x73\\x74\\x45\\x76\\\n\\x74\\x3a\\x69\\x6e\\x73\\x74\\x61\\x6e\\x63\\x65\\x49\\x44\\x3d\\x22\\x78\\x6d\\\n\\x70\\x2e\\x69\\x69\\x64\\x3a\\x66\\x31\\x37\\x65\\x62\\x62\\x32\\x33\\x2d\\x65\\\n\\x36\\x32\\x61\\x2d\\x33\\x39\\x34\\x36\\x2d\\x61\\x39\\x37\\x35\\x2d\\x64\\x34\\\n\\x66\\x36\\x61\\x62\\x34\\x34\\x64\\x34\\x30\\x39\\x22\\x20\\x73\\x74\\x45\\x76\\\n\\x74\\x3a\\x77\\x68\\x65\\x6e\\x3d\\x22\\x32\\x30\\x32\\x31\\x2d\\x31\\x31\\x2d\\\n\\x31\\x30\\x54\\x31\\x37\\x3a\\x33\\x39\\x3a\\x30\\x32\\x2b\\x30\\x31\\x3a\\x30\\\n\\x30\\x22\\x20\\x73\\x74\\x45\\x76\\x74\\x3a\\x73\\x6f\\x66\\x74\\x77\\x61\\x72\\\n\\x65\\x41\\x67\\x65\\x6e\\x74\\x3d\\x22\\x41\\x64\\x6f\\x62\\x65\\x20\\x50\\x68\\\n\\x6f\\x74\\x6f\\x73\\x68\\x6f\\x70\\x20\\x32\\x32\\x2e\\x35\\x20\\x28\\x57\\x69\\\n\\x6e\\x64\\x6f\\x77\\x73\\x29\\x22\\x2f\\x3e\\x20\\x3c\\x2f\\x72\\x64\\x66\\x3a\\\n\\x53\\x65\\x71\\x3e\\x20\\x3c\\x2f\\x78\\x6d\\x70\\x4d\\x4d\\x3a\\x48\\x69\\x73\\\n\\x74\\x6f\\x72\\x79\\x3e\\x20\\x3c\\x2f\\x72\\x64\\x66\\x3a\\x44\\x65\\x73\\x63\\\n\\x72\\x69\\x70\\x74\\x69\\x6f\\x6e\\x3e\\x20\\x3c\\x2f\\x72\\x64\\x66\\x3a\\x52\\\n\\x44\\x46\\x3e\\x20\\x3c\\x2f\\x78\\x3a\\x78\\x6d\\x70\\x6d\\x65\\x74\\x61\\x3e\\\n\\x20\\x3c\\x3f\\x78\\x70\\x61\\x63\\x6b\\x65\\x74\\x20\\x65\\x6e\\x64\\x3d\\x22\\\n\\x72\\x22\\x3f\\x3e\\x07\\x62\\x0c\\x81\\x00\\x00\\x00\\x0d\\x49\\x44\\x41\\x54\\\n\\x08\\x1d\\x63\\xf8\\xff\\xff\\x3f\\x03\\x00\\x08\\xfc\\x02\\xfe\\xe6\\x0c\\xff\\\n\\xab\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\x9f\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce\\x7c\\x4e\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x08\\x14\\x1f\\xf9\\\n\\x23\\xd9\\x0b\\x00\\x00\\x00\\x23\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\xc0\\\n\\x0d\\xe6\\x7c\\x80\\xb1\\x18\\x91\\x05\\x52\\x04\\xe0\\x42\\x08\\x15\\x29\\x02\\\n\\x0c\\x0c\\x8c\\xc8\\x02\\x08\\x95\\x68\\x00\\x00\\xac\\xac\\x07\\x90\\x4e\\x65\\\n\\x34\\xac\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\xa0\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x14\\x1c\\x1f\\x24\\\n\\xc6\\x09\\x17\\x00\\x00\\x00\\x24\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\x40\\\n\\x05\\xff\\xcf\\xc3\\x58\\x4c\\xc8\\x5c\\x26\\x64\\x59\\x26\\x64\\xc5\\x70\\x0e\\\n\\xa3\\x21\\x9c\\xc3\\x68\\x88\\x61\\x1a\\x0a\\x00\\x00\\x6d\\x84\\x09\\x75\\x37\\\n\\x9e\\xd9\\x23\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x14\\x1d\\x00\\xb0\\\n\\xd5\\x35\\xa3\\x00\\x00\\x00\\x2a\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\xc0\\\n\\x06\\xfe\\x9f\\x67\\x60\\x60\\x42\\x30\\xa1\\x1c\\x08\\x93\\x81\\x81\\x09\\xc1\\\n\\x64\\x60\\x60\\x62\\x60\\x60\\x34\\x44\\xe2\\x20\\x73\\x19\\x90\\x8d\\x40\\x02\\\n\\x00\\x64\\x40\\x09\\x75\\x86\\xb3\\xad\\x9c\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\\n\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce\\x7c\\x4e\\\n\\x00\\x00\\x00\\x01\\x73\\x52\\x47\\x42\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02\\x62\\x4b\\x47\\x44\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09\\x70\\\n\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07\\x74\\x49\\x4d\\x45\\x07\\xdc\\x08\\x17\\x08\\x15\\x3b\\xdc\\\n\\x3b\\x0c\\x9b\\x00\\x00\\x00\\x2a\\x49\\x44\\x41\\x54\\x08\\xd7\\x63\\x60\\xc0\\\n\\x00\\x8c\\x0c\\x0c\\x73\\x3e\\x20\\x0b\\xa4\\x08\\x30\\x32\\x30\\x20\\x0b\\xa6\\\n\\x08\\x30\\x30\\x30\\x42\\x98\\x10\\xc1\\x14\\x01\\x14\\x13\\x50\\xb5\\xa3\\x01\\\n\\x00\\xc6\\xb9\\x07\\x90\\x5d\\x66\\x1f\\x83\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\\n\\x44\\xae\\x42\\x60\\x82\\\n\\x00\\x00\\x03\\xff\\\n\\x89\\\n\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0d\\x49\\x48\\x44\\x52\\x00\\\n\\x00\\x00\\x30\\x00\\x00\\x00\\x30\\x08\\x06\\x00\\x00\\x00\\x57\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09\\x70\\x48\\x59\\x73\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x03\\xb1\\x49\\x44\\x41\\x54\\x68\\x81\\xed\\\n\\x9a\\x4f\\x68\\x1e\\x45\\x18\\xc6\\x7f\\x9b\\x26\\x85\\x82\\x15\\x15\\xab\\x42\\\n\\xcb\\x03\\x06\\x05\\xa9\\x0a\\x8a\\xb7\\x52\\x3c\\xd4\\x96\\xaa\\xb5\\xe0\\x41\\\n\\xad\\xc5\\x43\\x25\\xa0\\x07\\x51\\xcc\\x4d\\x28\\xb4\\x07\\x45\\x3c\\x78\\xb0\\\n\\xa0\\x52\\x50\\x0f\\x8a\\x50\\xf5\\x50\\xa5\\x28\\xc6\\xbf\\xa8\\x34\\x20\\x08\\\n\\x9e\\x4c\\x41\\xc5\\x83\\x3c\\x20\\xd2\\x90\\x2a\\x0a\\xfe\\x69\\x92\\x3a\\x1e\\\n\\x66\\x8d\\x5f\\xd6\\xdd\\x9d\\xfd\\xf6\\x8b\\xd9\\x2f\\xd0\\xdf\\xed\\x9b\\x79\\\n\\xe7\\x9d\\xe7\\x9d\\xd9\\x99\\x79\\x67\\xbf\\xcd\\xc8\\x09\\x21\\x5c\\x0a\\x1c\\\n\\x06\\xb6\\x03\\xd7\\x01\\x63\\x0c\\x17\\x0b\\xc0\\x0c\\x30\\x0d\\x3c\\x9e\\x65\\\n\\xd9\\xdc\\x52\\x4d\\x08\\x61\\x47\\x08\\xe1\\x74\\x58\\x3b\\x9c\\x0e\\x21\\xec\\\n\\x00\\xc8\\x42\\x1c\\xf9\\x53\\xc0\\x65\\x5d\\x0c\\xeb\\x00\\xcc\\x02\\xd7\\x8e\\\n\\x10\\x1f\\x9b\\xb5\\x26\\x1e\\xa2\\xe6\\x43\\xa3\\xc4\\x67\\xbe\\xc8\\xb9\\x55\\\n\\x16\\xd3\\x94\\x75\\x85\\xdf\\xdb\\xb3\\x10\\xc2\\x3c\\xcb\\x17\\xec\\xb9\\x2c\\\n\\xcb\\x46\\x57\\x51\\x54\\x63\\x42\\x08\\x8b\\x2c\\x0f\\x62\\x61\\x84\\xe1\\xdb\\\n\\x6d\\xfa\\x61\\x6c\\xa4\\x6b\\x05\\x83\\x72\\x3e\\x80\\xae\\x39\\x1f\\xc0\\xff\\\n\\x81\\xed\\xbb\\x6d\\xbf\\x66\\x7b\\x57\\xca\\x36\\x0b\\x21\\x84\\x42\\x59\\xa7\\\n\\xdb\\xa8\\xed\\x47\\x81\\x23\\xf9\\xcf\\x00\\x1c\\x05\\x26\\x25\\x2d\\x94\\x6c\\\n\\xa3\\xc3\\x35\\x03\\xb6\\xef\\x05\\x9e\\xe9\\x29\\xca\\x80\\x87\\x80\\x17\\xab\\\n\\xda\\x0c\\xcd\\x81\\x65\\x7b\\x27\\xf0\\x0a\\x51\\x74\\x91\\x03\\xb6\\xbf\\x2a\\\n\\x6b\\x37\\x14\\x33\\x60\\xfb\\x26\\xe0\\x4d\\x60\\x7d\\x8d\\xd9\\x6d\\x65\\x85\\\n\\x9d\\x07\\x60\\xfb\\x2a\\xe0\\x5d\\x60\\x63\\xc2\\xf4\\xa5\\xb2\\xc2\\x4e\\x03\\\n\\xb0\\x7d\\x39\\xf0\\x3e\\xe9\\x6c\\xf8\\x69\\x49\\xaf\\x97\\x55\\x74\\x16\\x80\\\n\\xed\\x8d\\xc0\\x14\\x30\\x9e\\x30\\x7d\\x15\\x78\\xac\\xaa\\xb2\\x93\\x00\\x6c\\\n\\xaf\\x07\\xde\\x02\\x6e\\x4c\\x98\\xbe\\x07\\x4c\\x48\\x2a\\x6e\\xf5\\x4b\\xac\\\n\\x7a\\x00\\xb6\\x47\\x88\\xa3\\x7a\\x4b\\xc2\\xf4\\x0b\\xe0\\x2e\\x49\\x8b\\x75\\\n\\x46\\x5d\\xcc\\xc0\\x11\\xe0\\x9e\\x84\\xcd\\xb7\\xc0\\x1e\\x49\\xbf\\xa5\\x9c\\\n\\x35\\x0a\\xc0\\x76\\x96\\x8f\\xdc\\x40\\xd8\\x3e\\x08\\x3c\\x92\\x30\\xfb\\x11\\\n\\xd8\\x2d\\x69\\x2e\\x61\\x07\\x24\\x02\\xc8\\x85\\x4f\\x02\\x7f\\x00\\x33\\xb6\\\n\\xaf\\x6f\\xa4\\xb4\\xdc\\xd7\\x04\\xf0\\x64\\xc2\\xec\\x17\\xe0\\x56\\x49\\xdf\\\n\\x37\\xf5\\x5b\\x99\\x0b\\xd9\\x1e\\x03\\x8e\\x03\\x7b\\x7b\\xea\\x66\\x81\\x9b\\\n\\x25\\x7d\\xd3\\xb4\\x03\\x00\\xdb\\x7b\\x89\\x8b\\xb6\\x78\\xa7\\xed\\xe5\\x2c\\\n\\x71\\xe4\\x3f\\xab\\x32\\xe8\\x37\\x17\\x7a\\x8e\\xe5\\xe2\\x21\\xee\\xd7\\x1f\\\n\\xdb\\xbe\\xb2\\x56\\x71\\x0f\\xb6\\xb7\\x01\\x6f\\x14\\x3b\\x2e\\xf0\\x17\\x70\\\n\\x5f\\x9d\\xf8\\x2a\\x4a\\x03\\xc8\\x47\\xec\\xc1\\x8a\\x36\\x9b\\x81\\x8f\\x6c\\\n\\x6f\\x4e\\x39\\xb7\\xbd\\x15\\x78\\x1b\\xd8\\x90\\x30\\x7d\\x58\\xd2\\xf1\\x94\\\n\\xbf\\x32\\xaa\\x66\\xe0\\x92\\x44\\xbb\\x71\\x62\\x10\\x9b\\xaa\\x0c\\x6c\\x6f\\\n\\x21\\x9e\\xb2\\x29\\x5f\\x4f\\x48\\x3a\\x9a\\xb0\\xa9\\xa4\\x2a\\x80\\x63\\xc4\\\n\\x7d\\xb8\\x8e\\x6b\\x80\\x0f\\x6c\\x5f\\x54\\xac\\xb0\\x7d\\x31\\x51\\xfc\\x96\\\n\\x84\\x8f\\x17\\x24\\x1d\\x4e\\xaa\\xac\\xa1\\x34\\x00\\x49\\x0b\\xc4\\xec\\xaf\\\n\\x34\\x85\\xed\\xe1\\x06\\x60\\xca\\xf6\\x05\\xff\\x14\\xd8\\xde\\x40\\x7c\\x6c\\\n\\xb6\\x26\\xda\\x9e\\x20\\xe6\\xfa\\x03\\x51\\x7b\\x23\\xcb\\x93\\xad\\x93\\xc0\\\n\\xd5\\x09\\x3f\\x9f\\x02\\xb7\\x03\\xf3\\xc4\\xdd\\xa6\\xb8\\xf8\\x8b\\x4c\\x03\\\n\\xbb\\x24\\xfd\\xd9\\x8f\\xd8\\xb2\\x5d\\x28\\x79\\xa5\\xb4\\x2d\\x62\\x10\\x4a\\\n\\xf8\\x9f\\x22\\x1e\\x42\\x13\\x09\\xbb\\x19\\xe2\\x56\\xfc\\x73\\x23\\xd5\\x3d\\\n\\xb4\\x0a\\x00\\x96\\x72\\xf6\\x93\\xc0\\x15\\xfd\\x76\\x5a\\xc0\\xc0\\x36\\x49\\\n\\x3f\\xb4\\x69\\xdc\\xfa\\x4e\\x2c\\xe9\\x3b\\x60\\x27\\x70\\xa6\\x4d\\xc7\\x39\\\n\\x3f\\x11\\x0f\\xaa\\x56\\xe2\\xab\\x68\\x9c\\xdf\\x48\\x3a\\x05\\xec\\x06\\x7e\\\n\\x6d\\xd1\\xcf\\xef\\xc0\\x1d\\x92\\xbe\\x6e\\xd1\\xb6\\x96\\xbe\\x12\\x34\\x49\\\n\\x5f\\x02\\x7b\\x72\\x41\\x4d\\x59\\x04\\xf6\\x49\\xfa\\xbc\\x9f\\xbe\\x9a\\xd2\\\n\\x77\\x86\\x29\\x69\\x1a\\xb8\\x93\\x98\\xbb\\x34\\xe1\\x01\\x49\\xef\\xf4\\xdb\\\n\\x4f\\x53\\x5a\\xa5\\xc8\\x92\\x3e\\x04\\xf6\\x11\\x47\\xb7\\x8e\\x83\\x92\\x5e\\\n\\x6e\\xd3\\x47\\x53\\x5a\\xe7\\xf8\\x92\\x4e\\x00\\x07\\x88\\x89\\x58\\x19\\xcf\\\n\\x4a\\x7a\\xaa\\xad\\xff\\xa6\\x0c\\x74\\x49\\x91\\x74\\x0c\\xb8\\x9f\\xff\\xae\\\n\\x89\\xe7\\x81\\xc9\\x41\\x7c\\x37\\x65\\x45\\xde\\x8d\\xda\\x1e\\x27\\x9e\\xbe\\\n\\x17\\x02\\x9f\\xe4\\xeb\\x64\\xc5\\x69\\x7d\\x90\\x0d\\x0b\\x55\\x07\\xd9\\x42\\\n\\x37\\x72\\x56\\x84\\xf9\\x51\\x62\\x6e\\xd2\\xfb\\x7e\\x66\\x5d\\x1e\\xe9\\x30\\\n\\x52\\xbc\\xd5\\xcd\\x8c\\x12\\x33\\xc3\\xe2\\x0b\\xa6\\xba\\xeb\\xdf\\x30\\x31\\\n\\xbd\\xf6\\x3f\\x35\\xc8\\xbf\\xfa\\xd8\\x9f\\x17\\xac\\x15\\x66\\x81\\xfd\\x59\\\n\\x96\\xcd\\x2d\\xfd\\x99\\x90\\xcf\\xc4\\x21\\xfe\\xfd\\xdc\\xa6\\xee\\x5d\\x7d\\\n\\x17\\xcc\\xb3\\xfc\\x73\\x9b\\x33\\x00\\x7f\\x03\\xd9\\x1a\\xfb\\xdb\\xbb\\xa7\\\n\\x8f\\x07\\x00\\x00\\x00\\x00\\x49\\x45\\x4e\\x44\\xae\\x42\\x60\\x82\\\n\"\n\nqt_resource_name = b\"\\\n\\x00\\x08\\\n\\x06\\xc5\\x8e\\xa5\\\n\\x00\\x6f\\\n\\x00\\x70\\x00\\x65\\x00\\x6e\\x00\\x70\\x00\\x79\\x00\\x70\\x00\\x65\\\n\\x00\\x06\\\n\\x07\\x03\\x7d\\xc3\\\n\\x00\\x69\\\n\\x00\\x6d\\x00\\x61\\x00\\x67\\x00\\x65\\x00\\x73\\\n\\x00\\x11\\\n\\x0b\\xda\\x30\\xa7\\\n\\x00\\x62\\\n\\x00\\x72\\x00\\x61\\x00\\x6e\\x00\\x63\\x00\\x68\\x00\\x5f\\x00\\x63\\x00\\x6c\\x00\\x6f\\x00\\x73\\x00\\x65\\x00\\x64\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\\n\\x00\\x0f\\\n\\x06\\x53\\x25\\xa7\\\n\\x00\\x62\\\n\\x00\\x72\\x00\\x61\\x00\\x6e\\x00\\x63\\x00\\x68\\x00\\x5f\\x00\\x6f\\x00\\x70\\x00\\x65\\x00\\x6e\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x1a\\\n\\x01\\x87\\xae\\x67\\\n\\x00\\x63\\\n\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x69\\x00\\x6e\\x00\\x64\\x00\\x65\\x00\\x74\\x00\\x65\\x00\\x72\\x00\\x6d\\\n\\x00\\x69\\x00\\x6e\\x00\\x61\\x00\\x74\\x00\\x65\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x20\\\n\\x0f\\xd4\\x1b\\xc7\\\n\\x00\\x63\\\n\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x69\\x00\\x6e\\x00\\x64\\x00\\x65\\x00\\x74\\x00\\x65\\x00\\x72\\x00\\x6d\\\n\\x00\\x69\\x00\\x6e\\x00\\x61\\x00\\x74\\x00\\x65\\x00\\x5f\\x00\\x68\\x00\\x6f\\x00\\x76\\x00\\x65\\x00\\x72\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x1b\\\n\\x03\\x5a\\x32\\x27\\\n\\x00\\x63\\\n\\x00\\x6f\\x00\\x6d\\x00\\x62\\x00\\x6f\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x5f\\x00\\x64\\x00\\x69\\\n\\x00\\x73\\x00\\x61\\x00\\x62\\x00\\x6c\\x00\\x65\\x00\\x64\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x0f\\\n\\x02\\x9f\\x05\\x87\\\n\\x00\\x72\\\n\\x00\\x69\\x00\\x67\\x00\\x68\\x00\\x74\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x0e\\\n\\x04\\xa2\\xfc\\xa7\\\n\\x00\\x64\\\n\\x00\\x6f\\x00\\x77\\x00\\x6e\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x1c\\\n\\x0e\\x3c\\xde\\x07\\\n\\x00\\x63\\\n\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x75\\x00\\x6e\\x00\\x63\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x65\\\n\\x00\\x64\\x00\\x5f\\x00\\x68\\x00\\x6f\\x00\\x76\\x00\\x65\\x00\\x72\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x12\\\n\\x01\\x2e\\x03\\x27\\\n\\x00\\x63\\\n\\x00\\x6f\\x00\\x6d\\x00\\x62\\x00\\x6f\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x2e\\x00\\x70\\x00\\x6e\\\n\\x00\\x67\\\n\\x00\\x15\\\n\\x03\\x27\\x72\\x67\\\n\\x00\\x63\\\n\\x00\\x6f\\x00\\x6d\\x00\\x62\\x00\\x6f\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x5f\\x00\\x6f\\x00\\x6e\\\n\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x1d\\\n\\x09\\x07\\x81\\x07\\\n\\x00\\x63\\\n\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x63\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x65\\x00\\x64\\x00\\x5f\\\n\\x00\\x64\\x00\\x69\\x00\\x73\\x00\\x61\\x00\\x62\\x00\\x6c\\x00\\x65\\x00\\x64\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x23\\\n\\x06\\xf2\\x1a\\x47\\\n\\x00\\x63\\\n\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x69\\x00\\x6e\\x00\\x64\\x00\\x65\\x00\\x74\\x00\\x65\\x00\\x72\\x00\\x6d\\\n\\x00\\x69\\x00\\x6e\\x00\\x61\\x00\\x74\\x00\\x65\\x00\\x5f\\x00\\x64\\x00\\x69\\x00\\x73\\x00\\x61\\x00\\x62\\x00\\x6c\\x00\\x65\\x00\\x64\\x00\\x2e\\x00\\x70\\\n\\x00\\x6e\\x00\\x67\\\n\\x00\\x17\\\n\\x0c\\x65\\xce\\x07\\\n\\x00\\x6c\\\n\\x00\\x65\\x00\\x66\\x00\\x74\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x5f\\x00\\x64\\x00\\x69\\x00\\x73\\x00\\x61\\x00\\x62\\x00\\x6c\\\n\\x00\\x65\\x00\\x64\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x12\\\n\\x05\\x8f\\x9d\\x07\\\n\\x00\\x62\\\n\\x00\\x72\\x00\\x61\\x00\\x6e\\x00\\x63\\x00\\x68\\x00\\x5f\\x00\\x6f\\x00\\x70\\x00\\x65\\x00\\x6e\\x00\\x5f\\x00\\x6f\\x00\\x6e\\x00\\x2e\\x00\\x70\\x00\\x6e\\\n\\x00\\x67\\\n\\x00\\x14\\\n\\x07\\xec\\xd1\\xc7\\\n\\x00\\x63\\\n\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x63\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x65\\x00\\x64\\x00\\x2e\\\n\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x16\\\n\\x01\\x75\\xcc\\x87\\\n\\x00\\x63\\\n\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x75\\x00\\x6e\\x00\\x63\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x65\\\n\\x00\\x64\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x14\\\n\\x04\\x5e\\x2d\\xa7\\\n\\x00\\x62\\\n\\x00\\x72\\x00\\x61\\x00\\x6e\\x00\\x63\\x00\\x68\\x00\\x5f\\x00\\x63\\x00\\x6c\\x00\\x6f\\x00\\x73\\x00\\x65\\x00\\x64\\x00\\x5f\\x00\\x6f\\x00\\x6e\\x00\\x2e\\\n\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x0f\\\n\\x01\\x73\\x8b\\x07\\\n\\x00\\x75\\\n\\x00\\x70\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x5f\\x00\\x6f\\x00\\x6e\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x11\\\n\\x00\\xb8\\x8c\\x07\\\n\\x00\\x6c\\\n\\x00\\x65\\x00\\x66\\x00\\x74\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x5f\\x00\\x6f\\x00\\x6e\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\\n\\x00\\x11\\\n\\x01\\x1f\\xc3\\x87\\\n\\x00\\x64\\\n\\x00\\x6f\\x00\\x77\\x00\\x6e\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x5f\\x00\\x6f\\x00\\x6e\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\\n\\x00\\x0c\\\n\\x06\\xe6\\xe6\\x67\\\n\\x00\\x75\\\n\\x00\\x70\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x1c\\\n\\x08\\x3f\\xda\\x67\\\n\\x00\\x63\\\n\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x75\\x00\\x6e\\x00\\x63\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x65\\\n\\x00\\x64\\x00\\x5f\\x00\\x66\\x00\\x6f\\x00\\x63\\x00\\x75\\x00\\x73\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x1a\\\n\\x03\\x0e\\xe4\\x87\\\n\\x00\\x63\\\n\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x63\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x65\\x00\\x64\\x00\\x5f\\\n\\x00\\x68\\x00\\x6f\\x00\\x76\\x00\\x65\\x00\\x72\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x20\\\n\\x09\\xd7\\x1f\\xa7\\\n\\x00\\x63\\\n\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x69\\x00\\x6e\\x00\\x64\\x00\\x65\\x00\\x74\\x00\\x65\\x00\\x72\\x00\\x6d\\\n\\x00\\x69\\x00\\x6e\\x00\\x61\\x00\\x74\\x00\\x65\\x00\\x5f\\x00\\x66\\x00\\x6f\\x00\\x63\\x00\\x75\\x00\\x73\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x1f\\\n\\x0a\\xae\\x27\\x47\\\n\\x00\\x63\\\n\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x75\\x00\\x6e\\x00\\x63\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x65\\\n\\x00\\x64\\x00\\x5f\\x00\\x64\\x00\\x69\\x00\\x73\\x00\\x61\\x00\\x62\\x00\\x6c\\x00\\x65\\x00\\x64\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x18\\\n\\x03\\x8e\\xde\\x67\\\n\\x00\\x72\\\n\\x00\\x69\\x00\\x67\\x00\\x68\\x00\\x74\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x5f\\x00\\x64\\x00\\x69\\x00\\x73\\x00\\x61\\x00\\x62\\\n\\x00\\x6c\\x00\\x65\\x00\\x64\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x0f\\\n\\x0c\\xe2\\x68\\x67\\\n\\x00\\x74\\\n\\x00\\x72\\x00\\x61\\x00\\x6e\\x00\\x73\\x00\\x70\\x00\\x61\\x00\\x72\\x00\\x65\\x00\\x6e\\x00\\x74\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x15\\\n\\x0f\\xf3\\xc0\\x07\\\n\\x00\\x75\\\n\\x00\\x70\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x5f\\x00\\x64\\x00\\x69\\x00\\x73\\x00\\x61\\x00\\x62\\x00\\x6c\\x00\\x65\\x00\\x64\\\n\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x12\\\n\\x03\\x8d\\x04\\x47\\\n\\x00\\x72\\\n\\x00\\x69\\x00\\x67\\x00\\x68\\x00\\x74\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x5f\\x00\\x6f\\x00\\x6e\\x00\\x2e\\x00\\x70\\x00\\x6e\\\n\\x00\\x67\\\n\\x00\\x0e\\\n\\x0e\\xde\\xfa\\xc7\\\n\\x00\\x6c\\\n\\x00\\x65\\x00\\x66\\x00\\x74\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x17\\\n\\x0c\\xab\\x51\\x07\\\n\\x00\\x64\\\n\\x00\\x6f\\x00\\x77\\x00\\x6e\\x00\\x5f\\x00\\x61\\x00\\x72\\x00\\x72\\x00\\x6f\\x00\\x77\\x00\\x5f\\x00\\x64\\x00\\x69\\x00\\x73\\x00\\x61\\x00\\x62\\x00\\x6c\\\n\\x00\\x65\\x00\\x64\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\\x00\\x1a\\\n\\x05\\x11\\xe0\\xe7\\\n\\x00\\x63\\\n\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x62\\x00\\x6f\\x00\\x78\\x00\\x5f\\x00\\x63\\x00\\x68\\x00\\x65\\x00\\x63\\x00\\x6b\\x00\\x65\\x00\\x64\\x00\\x5f\\\n\\x00\\x66\\x00\\x6f\\x00\\x63\\x00\\x75\\x00\\x73\\x00\\x2e\\x00\\x70\\x00\\x6e\\x00\\x67\\\n\"\n\nqt_resource_struct_v1 = b\"\\\n\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\\n\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x02\\\n\\x00\\x00\\x00\\x16\\x00\\x02\\x00\\x00\\x00\\x20\\x00\\x00\\x00\\x03\\\n\\x00\\x00\\x03\\xaa\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x33\\x3b\\\n\\x00\\x00\\x03\\xd2\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x33\\xe5\\\n\\x00\\x00\\x01\\xb4\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x15\\xf1\\\n\\x00\\x00\\x03\\x86\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x32\\x99\\\n\\x00\\x00\\x03\\x26\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x29\\x59\\\n\\x00\\x00\\x00\\x74\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x0e\\xbb\\\n\\x00\\x00\\x01\\x30\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x13\\x37\\\n\\x00\\x00\\x04\\x56\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x36\\x8b\\\n\\x00\\x00\\x01\\xde\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x16\\x9b\\\n\\x00\\x00\\x00\\xf4\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x12\\x8e\\\n\\x00\\x00\\x05\\xa4\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x44\\xc9\\\n\\x00\\x00\\x05\\x1a\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x3e\\x00\\\n\\x00\\x00\\x03\\x58\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x2a\\xb8\\\n\\x00\\x00\\x01\\x54\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x13\\xdb\\\n\\x00\\x00\\x06\\x24\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x46\\xc1\\\n\\x00\\x00\\x02\\xce\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1e\\x26\\\n\\x00\\x00\\x00\\x50\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x07\\xb1\\\n\\x00\\x00\\x03\\xfa\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x34\\x8e\\\n\\x00\\x00\\x02\\x4e\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1b\\x7c\\\n\\x00\\x00\\x02\\xf8\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x25\\x5a\\\n\\x00\\x00\\x04\\x18\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x35\\x30\\\n\\x00\\x00\\x02\\x0e\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x17\\x45\\\n\\x00\\x00\\x04\\x90\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x3a\\xa1\\\n\\x00\\x00\\x04\\xd6\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x3c\\x86\\\n\\x00\\x00\\x00\\x28\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\\n\\x00\\x00\\x02\\x9a\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1d\\x7c\\\n\\x00\\x00\\x05\\xf0\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x46\\x17\\\n\\x00\\x00\\x05\\x50\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x3e\\xa4\\\n\\x00\\x00\\x01\\x76\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x14\\x84\\\n\\x00\\x00\\x05\\xce\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x45\\x6d\\\n\\x00\\x00\\x00\\xae\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x10\\x9b\\\n\\x00\\x00\\x05\\x74\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x44\\x26\\\n\"\n\nqt_resource_struct_v2 = b\"\\\n\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\\n\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\\n\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x02\\\n\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\\n\\x00\\x00\\x00\\x16\\x00\\x02\\x00\\x00\\x00\\x20\\x00\\x00\\x00\\x03\\\n\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\\n\\x00\\x00\\x03\\xaa\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x33\\x3b\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xdd\\\n\\x00\\x00\\x03\\xd2\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x33\\xe5\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xdb\\\n\\x00\\x00\\x01\\xb4\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x15\\xf1\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xd9\\\n\\x00\\x00\\x03\\x86\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x32\\x99\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xe0\\\n\\x00\\x00\\x03\\x26\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x29\\x59\\\n\\x00\\x00\\x01\\x7d\\x0a\\x8c\\xfa\\xc7\\\n\\x00\\x00\\x00\\x74\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x0e\\xbb\\\n\\x00\\x00\\x01\\x7d\\x0a\\x8c\\xfa\\xc5\\\n\\x00\\x00\\x01\\x30\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x13\\x37\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xdd\\\n\\x00\\x00\\x04\\x56\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x36\\x8b\\\n\\x00\\x00\\x01\\x7d\\x0a\\x8c\\xfa\\xc4\\\n\\x00\\x00\\x01\\xde\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x16\\x9b\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xda\\\n\\x00\\x00\\x00\\xf4\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x12\\x8e\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xd9\\\n\\x00\\x00\\x05\\xa4\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x44\\xc9\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xde\\\n\\x00\\x00\\x05\\x1a\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x3e\\x00\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xde\\\n\\x00\\x00\\x03\\x58\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x2a\\xb8\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xd7\\\n\\x00\\x00\\x01\\x54\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x13\\xdb\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xda\\\n\\x00\\x00\\x06\\x24\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x46\\xc1\\\n\\x00\\x00\\x01\\x7d\\x0a\\x8c\\xfa\\xc4\\\n\\x00\\x00\\x02\\xce\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1e\\x26\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xd8\\\n\\x00\\x00\\x00\\x50\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x07\\xb1\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xd8\\\n\\x00\\x00\\x03\\xfa\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x34\\x8e\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xdf\\\n\\x00\\x00\\x02\\x4e\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1b\\x7c\\\n\\x00\\x00\\x01\\x7d\\x0a\\x8c\\xfa\\xc5\\\n\\x00\\x00\\x02\\xf8\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x25\\x5a\\\n\\x00\\x00\\x01\\x7d\\x0a\\x8c\\xfa\\xc2\\\n\\x00\\x00\\x04\\x18\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x35\\x30\\\n\\x00\\x00\\x01\\x7d\\x0a\\x8c\\xfa\\xc8\\\n\\x00\\x00\\x02\\x0e\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x17\\x45\\\n\\x00\\x00\\x01\\x7d\\x0a\\x8c\\xfa\\xc3\\\n\\x00\\x00\\x04\\x90\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x3a\\xa1\\\n\\x00\\x00\\x01\\x7d\\x0a\\x8c\\xfa\\xc6\\\n\\x00\\x00\\x04\\xd6\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x3c\\x86\\\n\\x00\\x00\\x01\\x7d\\x0a\\x8c\\xfa\\xc7\\\n\\x00\\x00\\x00\\x28\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xd7\\\n\\x00\\x00\\x02\\x9a\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1d\\x7c\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xdc\\\n\\x00\\x00\\x05\\xf0\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x46\\x17\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xdb\\\n\\x00\\x00\\x05\\x50\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x3e\\xa4\\\n\\x00\\x00\\x01\\x7d\\x0a\\xb7\\x38\\x27\\\n\\x00\\x00\\x01\\x76\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x14\\x84\\\n\\x00\\x00\\x01\\x7d\\x0a\\x8c\\xfa\\xc9\\\n\\x00\\x00\\x05\\xce\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x45\\x6d\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xdc\\\n\\x00\\x00\\x00\\xae\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x10\\x9b\\\n\\x00\\x00\\x01\\x7d\\x0a\\x8c\\xfa\\xc6\\\n\\x00\\x00\\x05\\x74\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x44\\x26\\\n\\x00\\x00\\x01\\x7b\\xe9\\x78\\x46\\xdf\\\n\"\n\n\nqt_version = [int(v) for v in QtCore.qVersion().split('.')]\nif qt_version < [5, 8, 0]:\n    rcc_version = 1\n    qt_resource_struct = qt_resource_struct_v1\nelse:\n    rcc_version = 2\n    qt_resource_struct = qt_resource_struct_v2\n\n\ndef qInitResources():\n    QtCore.qRegisterResourceData(\n        rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data\n    )\n\n\ndef qCleanupResources():\n    QtCore.qUnregisterResourceData(\n        rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data\n    )\n"
  },
  {
    "path": "openpype/style/pyside2_resources.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Resource object code\n#\n# Created: Wed Nov 10 17:40:15 2021\n#      by: The Resource Compiler for PySide2 (Qt v5.12.5)\n#\n# WARNING! All changes made in this file will be lost!\n\nfrom PySide2 import QtCore\n\n\nqt_resource_data = b\"\\\n\\x00\\x00\\x00\\xa5\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\x9cS4\\xfc]\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x0b\\x02\\x04m\\\n\\x98\\x1bi\\x00\\x00\\x00)IDAT\\x08\\xd7c`\\xc0\\\n\\x00\\x8c\\x0c\\x0c\\xff\\xcf\\xa3\\x08\\x18220 \\x0b2\\x1a\\\n200B\\x98\\x10AFC\\x14\\x13P\\xb5\\xa3\\x01\\x00\\\n\\xd6\\x10\\x07\\xd2/H\\xdfJ\\x00\\x00\\x00\\x00IEND\\\n\\xaeB`\\x82\\\n\\x00\\x00\\x07\\xad\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x07\\x00\\x00\\x00\\x0a\\x08\\x06\\x00\\x00\\x00x\\xccD\\x0d\\\n\\x00\\x00\\x05RiTXtXML:com.\\\nadobe.xmp\\x00\\x00\\x00\\x00\\x00<?\\\nxpacket begin=\\x22\\xef\\\n\\xbb\\xbf\\x22 id=\\x22W5M0MpCe\\\nhiHzreSzNTczkc9d\\\n\\x22?>\\x0a<x:xmpmeta x\\\nmlns:x=\\x22adobe:ns\\\n:meta/\\x22 x:xmptk=\\\n\\x22XMP Core 5.5.0\\x22\\\n>\\x0a <rdf:RDF xmln\\\ns:rdf=\\x22http://ww\\\nw.w3.org/1999/02\\\n/22-rdf-syntax-n\\\ns#\\x22>\\x0a  <rdf:Desc\\\nription rdf:abou\\\nt=\\x22\\x22\\x0a    xmlns:d\\\nc=\\x22http://purl.o\\\nrg/dc/elements/1\\\n.1/\\x22\\x0a    xmlns:e\\\nxif=\\x22http://ns.a\\\ndobe.com/exif/1.\\\n0/\\x22\\x0a    xmlns:ti\\\nff=\\x22http://ns.ad\\\nobe.com/tiff/1.0\\\n/\\x22\\x0a    xmlns:pho\\\ntoshop=\\x22http://n\\\ns.adobe.com/phot\\\noshop/1.0/\\x22\\x0a    \\\nxmlns:xmp=\\x22http:\\\n//ns.adobe.com/x\\\nap/1.0/\\x22\\x0a    xml\\\nns:xmpMM=\\x22http:/\\\n/ns.adobe.com/xa\\\np/1.0/mm/\\x22\\x0a    x\\\nmlns:stEvt=\\x22http\\\n://ns.adobe.com/\\\nxap/1.0/sType/Re\\\nsourceEvent#\\x22\\x0a  \\\n exif:PixelXDime\\\nnsion=\\x227\\x22\\x0a   exi\\\nf:PixelYDimensio\\\nn=\\x2210\\x22\\x0a   exif:C\\\nolorSpace=\\x221\\x22\\x0a  \\\n tiff:ImageWidth\\\n=\\x227\\x22\\x0a   tiff:Ima\\\ngeLength=\\x2210\\x22\\x0a  \\\n tiff:Resolution\\\nUnit=\\x222\\x22\\x0a   tiff\\\n:XResolution=\\x2272\\\n.0\\x22\\x0a   tiff:YRes\\\nolution=\\x2272.0\\x22\\x0a \\\n  photoshop:Colo\\\nrMode=\\x223\\x22\\x0a   pho\\\ntoshop:ICCProfil\\\ne=\\x22sRGB IEC61966\\\n-2.1\\x22\\x0a   xmp:Mod\\\nifyDate=\\x222021-05\\\n-31T12:43:35+02:\\\n00\\x22\\x0a   xmp:Metad\\\nataDate=\\x222021-05\\\n-31T12:43:35+02:\\\n00\\x22>\\x0a   <dc:titl\\\ne>\\x0a    <rdf:Alt>\\\n\\x0a     <rdf:li xm\\\nl:lang=\\x22x-defaul\\\nt\\x22>branch_close<\\\n/rdf:li>\\x0a    </r\\\ndf:Alt>\\x0a   </dc:\\\ntitle>\\x0a   <xmpMM\\\n:History>\\x0a    <r\\\ndf:Seq>\\x0a     <rd\\\nf:li\\x0a      stEvt\\\n:action=\\x22produce\\\nd\\x22\\x0a      stEvt:s\\\noftwareAgent=\\x22Af\\\nfinity Designer \\\n1.9.2\\x22\\x0a      stE\\\nvt:when=\\x222021-05\\\n-31T12:43:35+02:\\\n00\\x22/>\\x0a    </rdf:\\\nSeq>\\x0a   </xmpMM:\\\nHistory>\\x0a  </rdf\\\n:Description>\\x0a <\\\n/rdf:RDF>\\x0a</x:xm\\\npmeta>\\x0a<?xpacket\\\n end=\\x22r\\x22?>$\\xe15\\x97\\x00\\x00\\\n\\x01\\x83iCCPsRGB IEC61\\\n966-2.1\\x00\\x00(\\x91u\\x91\\xcf+D\\\nQ\\x14\\xc7?fh\\xfc\\x18\\x8dba1e\\x12\\x16B\\\n\\x83\\x12\\x1b\\x8b\\x99\\x18\\x0a\\x8b\\x99Q~mf\\x9ey3\\\nj\\xdex\\xbd7\\xd2d\\xabl\\xa7(\\xb1\\xf1k\\xc1_\\\n\\xc0VY+E\\xa4d\\xa7\\xac\\x89\\x0dz\\xce\\x9bQ#\\\n\\x99s;\\xf7|\\xee\\xf7\\xdes\\xba\\xf7\\x5cpD\\xd3\\x8a\\\nfV\\xfaA\\xcbd\\x8dp(\\xe0\\x9b\\x99\\x9d\\xf3\\xb9\\x9e\\\n\\xa8\\xa2\\x85\\x1a:\\xf1\\xc6\\x14S\\x9f\\x8c\\x8cF)k\\xef\\\n\\xb7T\\xd8\\xf1\\xba\\xdb\\xaeU\\xfe\\xdc\\xbfV\\xb7\\x980\\x15\\\n\\xa8\\xa8\\x16\\x1eVt#+<&<\\xb1\\x9a\\xd5m\\xde\\\n\\x12nRR\\xb1E\\xe1\\x13\\xe1.C.(|c\\xeb\\\n\\xf1\\x22?\\xdb\\x9c,\\xf2\\xa7\\xcdF4\\x1c\\x04G\\x83\\xb0\\\n/\\xf9\\x8b\\xe3\\xbfXI\\x19\\x9a\\xb0\\xbc\\x9c6-\\xbd\\xa2\\\n\\xfc\\xdc\\xc7~\\x89;\\x91\\x99\\x8eHl\\x15\\xf7b\\x12&\\\nD\\x00\\x1f\\xe3\\x8c\\x10d\\x80^\\x86d\\x1e\\xa0\\x9b>z\\\ndE\\x99|\\x7f!\\x7f\\x8ae\\xc9Ud\\xd6\\xc9a\\xb0\\\nD\\x92\\x14Y\\xbaD]\\x91\\xea\\x09\\x89\\xaa\\xe8\\x09\\x19i\\\nrv\\xff\\xff\\xf6\\xd5T\\xfb\\xfb\\x8a\\xd5\\xdd\\x01\\xa8z\\xb4\\\n\\xac\\xd7vpm\\xc2W\\xde\\xb2>\\x0e,\\xeb\\xeb\\x10\\x9c\\\n\\x0fp\\x9e)\\xe5/\\xef\\xc3\\xe0\\x9b\\xe8\\xf9\\x92\\xd6\\xb6\\x07\\\n\\x9eu8\\xbd(i\\xf1m8\\xdb\\x80\\xe6{=f\\xc4\\\n\\x0a\\x92S\\xdc\\xa1\\xaa\\xf0r\\x0c\\xf5\\xb3\\xd0x\\x05\\xb5\\xf3\\\n\\xc5\\x9e\\xfd\\xecst\\x07\\xd15\\xf9\\xaaK\\xd8\\xd9\\x85\\x0e\\\n9\\xefY\\xf8\\x06\\x8e\\xfdg\\xf8\\xfd\\x8a\\x18\\x97\\x00\\x00\\x00\\\n\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\\n\\x9c\\x18\\x00\\x00\\x00rIDAT\\x18\\x95m\\xcf1\\x0a\\\n\\xc2P\\x14D\\xd1\\xe8\\x02\\xb4W\\x08\\xd6Ia\\x99JC\\\nt\\x15\\x82\\xabI6(\\xee@\\x04\\xdb\\xa8\\x95Xx,\\\n\\xf2\\x09\\xe1\\xf3\\x07\\xa6\\x9a\\xfb\\xe0\\xbe\\x0c\\x1b\\xb4Xdq\\\np0\\xe4\\x82U\\x0a8\\xe3\\x8b\\x1b\\x8a\\x14p\\xc4\\x1b=\\\nv)`\\x8b\\x07>\\xa8\\xe6\\xd1\\xfe\\x0b\\x9d\\x85\\x8eW\\x0d\\\n^x\\xa2\\x9e\\x0e\\xa7 tG9\\x1d\\xf6\\xe1\\x95+\\xd6\\\n\\xb1D\\x8e\\x0e\\xcbX\\xf0\\x0fR\\x8ay\\x18\\xdc\\xe2\\x02p\\\n\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x043\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x03\\xe5IDATh\\x81\\xed\\\n\\x9aM\\x88\\x1cE\\x14\\xc7\\x7f3;\\x89.\\x18\\xf13\\x1e\\\n\\x92\\x8b\\x8b\\xa2&\\x06\\x14/\\xa29\\x88\\x15\\x89\\xa9\\x18\\x15\\\n\\xd1\\xca\\x06\\x0f\\x91\\x05=\\x88bnB\\xc0\\x1c\\x12\\xc4\\x83\\\n\\x07\\x15\\x0dB\\x14\\x89\\x04\\xa2\\x96DY\\x22/F}\\x8a\\\n\\xb0\\x22\\x08\\x1e4\\xbb\\x22F\\x8c,\\x88\\xba\\xc4\\x88\\x82_\\\n\\xc9F=T\\x0f\\x8c\\xbd\\xdd]\\xdd\\xd3a\\xa7\\x07\\xfc\\xdd\\\n\\xa6\\xfa\\xd5\\xab\\xf7\\xea\\xf3\\xdf\\xd5\\xd3\\x22\\xc1Xw\\x11\\xb0\\\n\\x03X\\x0b\\x5c\\x0d,\\xa1Y\\x9c\\x02\\xa6\\x81)`\\xa7\\x8a\\\n?\\x0e\\xd0\\x020\\xd6\\xdd\\x0c\\xbc\\x02,\\x1fXx\\xd5\\x98\\\n\\x03\\xb6\\xa8\\xf8\\xf7[I\\xcf\\xcf0<\\xc1w\\x99\\x03V\\\n\\xb7\\x09\\xd3f\\xd8\\x82\\x87\\x10\\xf3c\\x1d\\xc2\\x9cOsz\\\n\\x91\\x83)\\xcbH\\xea\\xf7\\xda\\x0ea\\xc1\\xf6rZ\\xc5w\\\n\\x16)\\xa0J\\x18\\xeb\\xe6\\xf9o\\x12k\\xda4o\\xb7\\xa9\\\n\\xc2\\x92\\xf6\\xa0#\\xa8\\xcb\\xff\\x09\\x0c\\x9a\\xa1O\\xa0\\xa9\\xbb\\\n\\xcd=\\xc0]\\xc0K*\\xfe\\xdd\\x22\\xdb\\xc6\\x8d\\x80\\xb1\\xee\\\n\\x11\\xc0\\x03\\xe3\\xc0ac\\xddnc]\\xeeN\\xd9\\xa8\\x04\\\n\\x8cu\\xe3\\xc0S=E-\\xe0A\\xe0\\x85\\xbc:\\x8d\\x99\\\nB\\xc6\\xbau\\xc0\\xcb$\\x023\\xc5Vc\\xdd\\x91\\xacz\\\n\\x8d\\x18\\x01c\\xddu\\xc0\\x1b\\xc0\\xd2\\x02\\xb3\\x0dY\\x85\\x03\\\nO\\xc0Xw\\x19 \\xc0\\xb2\\x88\\xe9\\x8bY\\x85\\x03M\\xc0\\\nXw\\x09p\\x98\\xb8\\x1a~R\\xc5\\xbf\\x9a\\xf5``\\x09\\\n\\x18\\xeb\\x96\\x01\\x87\\x80\\xb1\\x88\\xe9>\\xe0\\xd1\\xbc\\x87\\x03I\\\n\\xc0X\\xb7\\x14x\\x13\\xb86b\\xfa60\\xa1\\xe2\\xff\\xc9\\\n3X\\xf4\\x04\\x8cumB\\xaf\\x9a\\x88\\xe9'\\xc0\\xdd*\\\n~\\xbe\\xc8h\\x10#\\xf04\\xe0\\x226_\\x01\\x1bU\\xfc\\\no1g\\xa5\\x120\\xd6\\xb5\\x92\\x9e\\xab\\x85\\xb1n;\\xf0\\\np\\xc4\\xec{`}\\xf7\\xd6!FaPI\\xe0\\xdb\\x80\\\n?\\x80ic\\xdd\\x9aR\\x91f\\xfb\\x9a\\x00\\x1e\\x8f\\x98\\xfd\\\n\\x02\\xdc\\xaa\\xe2\\xbf-\\xeb77\\x81D\\x7fL\\x12\\x8e\\xf6\\\n\\xb3\\x80\\xab\\x80\\xf7\\x8cuW\\x94u\\xde\\xe3k\\x13\\xb0'\\\nb\\xf6\\x17p\\x87\\x8a\\xff\\xbc\\x8a\\xef\\xa2\\x11x\\x0e\\xd8\\x94\\\n*[\\x0e\\xa8\\xb1\\xee\\xd2\\xb2\\x0d\\x18\\xebn\\x00^c\\xe1\\\n\\x0by/\\x7f\\x03\\xf7\\xaa\\xf8\\x0f\\xcb\\xfa\\xed\\x92\\x99@\\xd2\\\nc\\x0f\\xe4\\xd4YA\\x18\\x89\\x151\\xe7\\xc6\\xbaU\\xc0A\\\n`4b\\xfa\\x90\\x8a?\\x10\\xf3\\x97E\\xde\\x08\\x5c\\x10\\xa9\\\n7FH\\xe2\\xe2<\\x03c\\xddJ\\xc2)\\x1b\\xf3\\xb5K\\\n\\xc5?\\x1f\\xb1\\xc9%/\\x81\\xfd\\x84}\\xb8\\x88+\\x81w\\\n\\x8cu\\xe7\\xa5\\x1f\\x18\\xeb\\xce'\\x04\\xbf2\\xe2c\\x8f\\x8a\\\n\\xdf\\x11\\x8d\\xb2\\x80\\xcc\\x04T\\xfc)\\x82\\xfa\\xcb\\x94\\xb0=\\\n\\x5c\\x03\\x1c2\\xd6\\x9d\\xd3-0\\xd6\\x8d\\x12\\xa6\\xcd\\xaaH\\\n\\xddI\\x82\\xd6\\xafE\\xee\\x22V\\xf1'\\x80[\\x80\\xa3\\x11\\\n\\x1f\\xd7\\x03\\x07\\x8du\\xa3\\xc6\\xba\\x11\\xc2\\x82\\xbd1Rg\\\n\\x0a\\x18W\\xf1\\xb5o\\x00\\x0b\\xcf\\x01\\x15\\xff#\\xb0\\x0e\\x98\\\n\\x8d\\xf8\\xb9\\x098@\\xd8*\\xd3;W\\x9ai\\xe0v\\x15\\\n\\xffg\\xc9\\x18\\x0b\\x89\\x9e\\xae*~\\x96\\xa0[~\\x88\\x98\\\nn\\x00&\\x226\\xb3\\x84\\x83\\xea\\xe7r\\xe1\\xc5)%\\x0f\\\nT\\xfc\\xd7\\x84\\x91\\xf8\\xa9F['\\x08\\x12\\xe1\\xbb\\x1a>\\\n\\x16PZ\\xdf\\xa8\\xf8\\x19`=\\xf0k\\x1f\\xed\\xfc\\x0e\\xdc\\\n\\xa6\\xe2\\xbf\\xec\\xa3n!\\x95\\x04\\x9a\\x8a\\xff\\x14\\xd8\\x98\\x04\\\nT\\x96y`\\xb3\\x8a\\xff\\xb8J[e\\xa9\\xac0U\\xfc\\\n\\x14p'A\\xbb\\x94\\xe1~\\x15\\xffV\\xd5v\\xca\\xd2\\x97\\\nDNn\\xcb6\\x13z\\xb7\\x88\\xed*~o?m\\x94\\\n\\xa5o\\x8d\\xaf\\xe2'\\x81\\xad\\x04!\\x96\\xc5\\xb3*\\xfe\\x89\\\n~\\xfd\\x97\\xa5\\xd6K\\x8a\\x8a\\xdf\\x0f\\xdc\\xc7\\xc25\\xb1\\x1b\\\n\\xd8V\\xc7wYj\\xdf\\xcc\\xa9\\xf8}\\xc6\\xba\\x8f\\x08\\x07\\\n\\xd8\\xb9\\xc0\\x07\\xc9:Y\\x14\\xce\\xc8\\xd5\\xa2\\x8a\\xff\\x06x\\\n\\xe6L\\xf8\\xaaJ\\x9b\\xf0\\x05|X9\\xd9!h\\x93\\xde\\\n\\xfb\\x99\\x91\\xe4k`\\x13I\\xbf\\xd5Mw\\x08\\xca0}\\\n\\xc1T\\xf4\\xfa\\xd7$\\xa6\\xda\\xc0N\\xc2g\\xfbac\\x0e\\\n\\xd8\\xd5N\\xee_\\xb60\\x5cIt\\xff\\xecq|\\x04\\xe0\\\n\\xd8\\xd1\\x99cc\\x97\\xaf\\xde\\x0b\\x9cM\\xf8\\xf0}!\\xcd\\\n\\x9bF'\\x81\\xcf\\x80\\xd7\\x01\\xa7\\xe2\\xbf\\x00\\xf8\\x17]\\x81\\\n\\x0b8\\xb3\\xfa \\x9c\\x00\\x00\\x00\\x00IEND\\xaeB\\\n`\\x82\\\n\\x00\\x00\\x01W\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x09IDATh\\x81\\xed\\\n\\xda\\xcdm\\xc2@\\x14E\\xe1\\xf3\\x8cI\\x05Q\\x9aH6\\\n\\xecY\\xd1\\x05\\xc5\\x90Ej\\xa3\\x04R\\x04\\x884`\\x82\\\nn\\x163\\xf9\\xb1\\xa5(DH\\x5c[z\\xdf\\x8e\\xc1\\x8b\\\nw\\x8c\\xcdf&\\xa8$\\xdd\\x03\\xcf\\xc0\\x12x\\x02\\xe6\\x8c\\\n\\xcb\\x09\\xd8\\x01[\\xe0%\\x22\\x8e_\\xdfHZI\\xdak\\\n:\\xf6\\x92V\\x00\\xa1r\\xe7_\\x81\\x07\\xc7m\\xbd\\xc2\\x01\\\nxl(\\x8f\\xcd\\xd4\\x86\\x872\\xf3\\xa6\\xa5<\\xf3C\\xe7\\\n\\x1b\\x0fs\\xa9\\xd9\\xe0\\xf32$u\\xf4_\\xd8sD\\xb4\\\n7\\x1c\\xeab\\x92\\xde\\xe9G\\x9c\\x1a\\xc6\\xf7o\\xf3\\x1f\\xf3\\\n\\xc6=\\xc1\\xb52\\xc0-\\x03\\xdc2\\xc0-\\x03\\xdc2\\xc0\\\n-\\x03\\xdc2\\xc0-\\x03\\xdc2\\xc0-\\x03\\xdc2\\xc0-\\\n\\x03\\xdc2\\xc0-\\x03\\xdc2\\xc0-\\x03\\xdc2\\xc0\\xad\\xa1\\\n\\xec\\x80OU\\xd7R\\xb6\\xef\\x17?\\x16gu7p\\x8c\\\n\\x86\\xdb\\xac\\xbb\\x96r\\xf6`\\xf1\\xc7\\x85c\\xb5\\x9d\\xfeQ\\\n\\x83z\\xeac]\\x17\\xa6\\xe2\\x00\\xac#\\xe2\\x18\\x9f+\\xf5\\\n\\x97\\xd8\\xf0}\\xdc\\xe6\\xce4\\xdco:\\xfa\\xc7m\\xde\\x00\\\n>\\x00G\\xd7\\xea\\xb1\\xadi\\xe1\\xd6\\x00\\x00\\x00\\x00IE\\\nND\\xaeB`\\x82\\\n\\x00\\x00\\x01\\xfc\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\xaeIDATh\\x81\\xed\\\n\\x9a\\xbdJ\\x03A\\x14FO6\\x1b\\xb0\\xd0J\\xf1\\x01\\x14\\\n\\xabh\\x91\\xc6*X\\xb8\\x16\\xb2\\x88v\\x0b\\xe9}\\x01\\x1f\\\n@\\x8b\\xf8\\x00\\xbe\\x80\\x85\\x9d0b\\xa32U\\xc6B\\xf2\\\n\\x02B\\x92F\\x83}H'6\\xf9\\x01\\x8b\\xdd@\\x12\\xb2\\\n\\x89k~f7\\xcc\\xe9v\\xef\\x14\\xdfY\\xee\\x0c\\x0bw\\\nR\\x048\\xae\\xb7\\x01\\x5c\\x01y`\\x17\\xc8\\x10/\\xda@\\\n\\x05(\\x03E%E\\x13 \\x05\\xe0\\xb8\\xde!p\\x0fl\\\nj\\x8b\\x17\\x8d\\x06PPR\\xbc\\xa6\\x82/_%9\\xe1\\\n{4\\x80\\xac\\x85\\xdf6I\\x0b\\x0f~\\xe6K\\x1b\\xbf\\xe7\\\n\\x87\\xe9.8\\xcc_I\\x0f=\\xe7m\\xfc\\x0d\\xdbOW\\\nIa/(P$\\x1c\\xd7\\xeb0(\\xb1g\\x11\\xbf\\xd3\\\n&\\x0a\\x19Kw\\x82i1\\x02\\xba1\\x02\\xba1\\x02\\xba\\\n1\\x02\\xba1\\x02\\xba1\\x02\\xba\\x99\\xf8\\xdb\\xec\\xb8\\xde\\x11\\\n\\xb0\\x0f\\xac\\xce?\\xce\\x00\\x1d\\xa0\\x06<+)~\\xc2\\x16\\\n\\x85\\x0a8\\xaeg\\x03\\x8f\\xc0\\xe9\\xec\\xb3E\\xa2\\xee\\xb8\\xde\\\n\\xb1\\x92\\xe2sTq\\x5c\\x0b]\\xa0?<\\xc06p\\x1b\\\nV\\x1c'p2\\xfb,\\xff\\xe6\\xc0q\\xbd\\xb5Q\\x85\\xc4\\\no\\xe2q\\x02/\\x0bK1\\x997%\\xc5\\xf7\\xa8\\xc28\\\n\\x81\\x1b\\xe0i>y\\x22Q\\x07\\xce\\xc3\\x8a\\xa1\\xa7\\x90\\x92\\\n\\xa2\\x03\\x9c%\\xf6\\x18\\xed\\xa1\\xa4(\\x01\\xa5\\x19\\x06\\x9b)\\\nK\\xbd\\x89\\x13\\x81\\x11\\xd0\\x8d\\x11\\xd0\\x8d\\x11\\xd0\\x8d\\x11\\xd0\\\n\\x8d\\x11\\xd0\\xcdR\\x08\\xb4u\\x87\\x98\\x82\\x96\\x8d?\\xbe\\xcf\\\n\\xf5\\xbdL\\x07\\xd3\\xc082<f\\xad\\xd8\\xf8w\\x0fr\\\n\\x13\\x16\\xc6\\x95\\xb2\\x05\\x14\\xf1\\xc7\\xf6I\\xa3\\x01\\x5c[\\xc1\\\n\\xad\\x8f\\x02\\xc9\\x92\\xe8]\\xf6h\\xa6\\x01\\xbe>\\xaa_[\\\n;\\xd9;`\\x05\\x7f\\xf0\\xbdN\\xfc\\xda\\xa8\\x05\\xbc\\x03\\x0f\\\n\\x80\\xa7\\xa4\\xa8\\x01\\xfc\\x02Q\\xab\\x5c\\x8a?\\xde\\xe3Y\\x00\\\n\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x08\\x15;\\xdc\\\n;\\x0c\\x9b\\x00\\x00\\x00*IDAT\\x08\\xd7c`\\xc0\\\n\\x00\\x8c\\x0c\\x0cs> \\x0b\\xa4\\x08020 \\x0b\\xa6\\\n\\x08000B\\x98\\x10\\xc1\\x14\\x01\\x14\\x13P\\xb5\\xa3\\x01\\\n\\x00\\xc6\\xb9\\x07\\x90]f\\x1f\\x83\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x01i\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x1bIDATh\\x81\\xed\\\n\\xda\\xb1m\\xc2@\\x14\\x87\\xf1\\xcf\\xc7\\x91\\x09P\\x86pB\\\nAO\\xc5\\x0a\\xae\\x90\\xbc\\x0a)\\xc8*\\x96Ry\\x05*\\\nF \\x1e\\xc2\\x82\\x05H\\x90R\\xdcY\\x01KQbE\\\n\\xe2\\xef\\x93\\xde\\xaf\\xb3E\\xf1>\\xcb\\xa6\\xb9\\x97\\x11\\x95u\\\n3\\x03^\\x80%\\xf0\\x0cL\\x19\\x97\\x0f\\xe0\\x00\\xec\\x81m\\\nU\\xe4G\\x80\\x0c\\xa0\\xac\\x9b\\x15\\xf0\\x06<\\xca\\xc6\\x1b\\xa6\\\n\\x05\\xd6U\\x91\\xef\\xb2\\xf8\\xe4\\xdfIg\\xf8N\\x0b<9\\\n\\xc2k\\x93\\xda\\xf0\\x10f\\xdex\\xc2;\\xdfw\\xb9\\xf30\\\n\\x7f5\\xe9]/=\\xe1\\x83\\xbdv\\xa9\\x8a\\xdc\\xdfi\\xa0\\\nA\\xca\\xba\\xf9\\xe46b\\xee\\x18\\xdf\\xbf\\xcd\\x10S\\xa7\\x9e\\\n\\xe0\\xbf,@\\xcd\\x02\\xd4,@\\xcd\\x02\\xd4,@\\xcd\\x02\\\n\\xd4,@\\xcd\\x02\\xd4,@\\xcd\\x02\\xd4,@\\xcd\\x02\\xd4\\\n,@\\xcd\\x02\\xd4,@\\xcd\\x02\\xd4,@\\xcd\\x11N\\xc0\\\nSu\\xf6\\x84\\xe3\\xfb\\xc5\\xd5\\xcdI<\\x0d\\x1c\\xa3\\xfe1\\\n\\xeb\\xc1\\x13v\\x0f\\x16\\xbf\\xfcp\\xac\\xf6\\x0e\\xd8\\x12\\x8e\\xed\\\nS\\xd3\\x02\\xaf.n}\\xacI+\\xa2[\\xf68f\\xdd\\\n\\x9d\\xb8\\xf4\\xb1\\xe1{\\xdd\\xe6A4\\xdcO\\xce\\xdc\\xae\\xdb\\\n\\x9c\\x00\\xbe\\x00\\x9f\\xf64>6O7\\x81\\x00\\x00\\x00\\x00\\\nIEND\\xaeB`\\x82\\\n\\x00\\x00\\x03\\xfb\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x03\\xadIDATh\\x81\\xed\\\n\\x9aO\\xa8\\x15U\\x1c\\xc7?\\xf7\\xbe\\xab\\xf2 \\xa3\\x22m\\\n\\xa1|!\\x09*K(\\xdaD\\xb9\\x88RL\\xccjQ\\\n\\xf9\\xa4\\x85\\xf1\\xa0\\x16Q\\xe4.\\x10tQD\\x8b\\x16\\x15\\\n%\\x81\\xb5(\\x04\\xad\\xc0\\xe2ad\\xf6\\x97\\xe0E\\x10\\xb4\\\n\\xa9'DE\\xc4\\x17\\xa2\\x125\\x0a*\\xff<\\xab\\xc5\\x99\\\n[\\xd7y3s\\xce\\xdcko\\xee\\x05?\\xbb9\\xf3;\\\n\\xbf\\xf3\\xfb\\x9d3\\xe7\\x9c\\xef\\x9c\\x99\\x16\\x19\\xb6/\\x06v\\\n\\x00\\xab\\x81\\xab\\x81\\x05\\x0c\\x17\\xa7\\x80\\x19`\\x1axL\\xd2\\\n\\x11\\x80\\x16\\x80\\xed\\x9b\\x81\\xbd\\xc0\\xd2\\xc6\\xc2\\xab\\xc7a`\\\n\\xb3\\xa4\\x0f[Y\\xcf\\x1fbt\\x82\\xefr\\x18\\xb8\\xaaM\\\nxlF-x\\x081o\\xef\\x10\\x9e\\xf9<\\xa7\\xe79\\\n\\x98T\\xc6r\\xd7\\xab;\\x84\\x09\\xdb\\xcbiI\\x9dy\\x0a\\\n\\xa8\\x16\\xb6g93\\x89Um\\x86o\\xb5\\xa9\\xc3\\x82v\\\n\\xd3\\x11\\x0c\\xca\\xb9\\x04\\x9a\\xe6\\x5c\\x02\\xff\\x07\\xb6\\xef\\xb6\\xbd\\\n\\xd7\\xf6\\xda\\x98\\xed\\xd0%`\\xfb\\x11\\xe0u`\\x028h\\\n{\\xa7\\xed\\xd2\\x95r\\xa8\\x12\\xb0=\\x01<\\xddS\\xd4\\x02\\\n\\x1e\\x04^,\\xab34\\x1b\\x96\\xed5\\xc0+d\\x023\\\n\\xc7\\x16\\xdb_\\x16\\xd5\\x1b\\x8a\\x11\\xb0}\\x1d\\xf0\\x06\\xb0\\xb0\\\n\\xc2l}Qa\\xe3\\x09\\xd8\\xbe\\x0cx\\x1bX\\x1c1}\\\n\\xa9\\xa8\\xb0\\xd1\\x04l_\\x02\\x1c$\\xae\\x86\\x9f\\x92\\xf4j\\\n\\xd1\\x8d\\xc6\\x12\\xb0\\xbd\\x188\\x00\\xac\\x88\\x98\\xee\\x06\\x1e-\\\n\\xbb\\xd9H\\x02\\xb6\\x17\\x02o\\x02\\xd7FL\\xdf\\x01&%\\\n\\xfd]f0\\xef\\x09\\xd8n\\x13z\\xf5\\x96\\x88\\xe9g\\xc0\\\n]\\x92f\\xab\\x8c\\x9a\\x18\\x81g\\x80{\\x226_\\x03\\x1b\\\n$\\xfd\\x1es\\x96\\x94\\x80\\xedV\\xd6s\\x03a{\\x1b\\xf0\\\np\\xc4\\xecG`]\\xf7\\xd4!FePY\\xe0[\\x81\\\n?\\x81\\x19\\xdb\\xab\\x92\\x22-\\xf65\\x09<\\x111\\xfb\\x15\\\n\\xb8U\\xd2\\xf7\\xa9~K\\x13\\xc8\\xf4\\xc7\\x14ak_\\x04\\\n\\x5c\\x09\\xbco\\xfb\\xf2T\\xe7=\\xbe6\\x02\\xbb\\x22f'\\\n\\x80;$}Q\\xc7w\\xd5\\x08<\\x0fl\\xcc\\x95-\\x05\\\n>\\xb0}ij\\x03\\xb6o\\x00^c\\xee\\x0by/\\x7f\\\n\\x01\\xf7J\\xfa8\\xd5o\\x97\\xc2\\x04\\xb2\\x1e{\\xa0\\xa4\\xce\\\n2\\xc2H,\\x8b9\\xb7\\xbd\\x12\\xd8\\x0f\\x8cGL\\x1f\\x92\\\n\\xb4/\\xe6\\xaf\\x88\\xb2\\x11\\xb8(Ro\\x05!\\x89%e\\\n\\x06\\xb6\\x97\\x13v\\xd9\\x98\\xaf\\xc7%\\xbd\\x10\\xb1)\\xa5,\\\n\\x81=\\x84u\\xb8\\x8a+\\x80wm_\\x90\\xbfa\\xfbB\\\nB\\xf0\\xcb#>vI\\xda\\x11\\x8d\\xb2\\x82\\xc2\\x04$\\x9d\\\n\\x22\\xa8\\xbfB\\x09\\xdb\\xc35\\xc0\\x01\\xdb\\xe7u\\x0bl\\x8f\\\n\\x13\\x1e\\x9b\\x95\\x91\\xbaS\\x04\\xad?\\x10\\xa5\\x93X\\xd21\\\n`-\\xf0M\\xc4\\xc7\\xf5\\xc0~\\xdb\\xe3\\xb6\\xc7\\x08\\x13\\xf6\\\n\\xc6H\\x9di`B\\xd2\\xc0'\\x80\\x95\\xfb\\x80\\xa4\\x9f\\x81\\\n5\\x80#~n\\x02\\xf6\\x11\\x96\\xca\\xfc\\xca\\x95g\\x06\\xb8\\\n]\\xd2\\xf1\\xc4\\x18+\\x89\\xee\\xae\\x92L\\xd0-?EL\\\n\\xd7\\x03\\x93\\x11\\x1b\\x136\\xaa_\\xd2\\xc2\\x8b\\x93$\\x0f$\\\n}K\\x18\\x89\\xa3\\x03\\xb4u\\x8c \\x11~\\x18\\xc0\\xc7\\x1c\\\n\\x92\\xf5\\x8d\\xa4C\\xc0:\\xe0\\xb7>\\xda\\xf9\\x03\\xb8M\\xd2\\\nW}\\xd4\\xad\\xa4\\x96@\\x93\\xf49\\xb0!\\x0b(\\x95Y\\\n`\\x93\\xa4O\\xeb\\xb4\\x95Jm\\x85)i\\x1a\\xb8\\x93\\xa0\\\n]R\\xb8_\\xd2[u\\xdbI\\xa5/\\x89,\\xe9=`\\\n\\x13\\xa1w\\xab\\xd8&\\xe9\\xe5~\\xdaH\\xa5o\\x8d/i\\\n\\x0a\\xd8B\\x10bE<'\\xe9\\xc9~\\xfd\\xa72\\xd0K\\\n\\x8a\\xa4=\\xc0}\\xcc\\x9d\\x13;\\x81\\xad\\x83\\xf8Ne\\xe0\\\n\\x939I\\xbbm\\x7fB\\xd8\\xc0\\xce\\x07>\\xca\\xe6\\xc9\\xbc\\\npV\\x8e\\x16%}\\x07<{6|\\xd5\\xa5M\\xf8\\x02\\\n>\\xaa\\x9c\\xec\\x10\\xb4I\\xef\\xf9\\xccX\\xf65p\\x18\\xc9\\\n\\xbf\\xd5\\xcdt\\x08\\xca0\\x7f\\xc0T\\xf5\\xfa7LL\\x8f\\\n\\xfe\\xaf\\x06\\xd9\\xf9\\xcb\\xe6\\xac`T\\xe8\\xfe\\xecq\\xe4\\xdf\\\n\\x8f\\x09\\xd9Hl\\xe7\\xbf\\xdfm\\xaa\\xce\\xea\\x9b\\xe0$g\\\n\\xfens\\x14\\xe0\\x1f\\x0aC\\x12kO\\xfd?\\x13\\x00\\x00\\\n\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x03\\xff\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x03\\xb1IDATh\\x81\\xed\\\n\\x9aOh\\x1eE\\x18\\xc6\\x7f\\x9b&\\x85\\x82\\x15\\x15\\xabB\\\n\\xcb\\x03\\x06\\x05\\xa9\\x0a\\x8a\\xb7R<\\xd4\\x96\\xaa\\xb5\\xe0A\\\n\\xad\\xc5C%\\xa0\\x07Q\\xccM(\\xb4\\x07E<x\\xb0\\\n\\xa0RP\\x0f\\x8aP\\xf5P\\xa5(\\xc6\\xbf\\xa84 \\x08\\\n\\x9eLA\\xc5\\x83< \\xd2\\x90*\\x0a\\xfei\\x92:\\x1e\\\nf\\x8d_\\xd6\\xdd\\x9d\\xfd\\xf6\\x8b\\xd9/\\xd0\\xdf\\xed\\x9by\\\n\\xe7\\x9d\\xe7\\x9d\\xd9\\x99yg\\xbf\\xcd\\xc8\\x09!\\x5c\\x0a\\x1c\\\n\\x06\\xb6\\x03\\xd7\\x01c\\x0c\\x17\\x0b\\xc0\\x0c0\\x0d<\\x9ee\\\n\\xd9\\xdcRM\\x08aG\\x08\\xe1tX;\\x9c\\x0e!\\xec\\\n\\x00\\xc8B\\x1c\\xf9S\\xc0e]\\x0c\\xeb\\x00\\xcc\\x02\\xd7\\x8e\\\n\\x10\\x1f\\x9b\\xb5&\\x1e\\xa2\\xe6C\\xa3\\xc4g\\xbe\\xc8\\xb9U\\\n\\x16\\xd3\\x94u\\x85\\xdf\\xdb\\xb3\\x10\\xc2<\\xcb\\x17\\xec\\xb9,\\\n\\xcbFWQTcB\\x08\\x8b,\\x0fba\\x84\\xe1\\xdb\\\nm\\xfaal\\xa4k\\x05\\x83r>\\x80\\xae9\\x1f\\xc0\\xff\\\n\\x81\\xed\\xbbm\\xbff{W\\xca6\\x0b!\\x84BY\\xa7\\\n\\xdb\\xa8\\xedG\\x81#\\xf9\\xcf\\x00\\x1c\\x05&%-\\x94l\\\n\\xa3\\xc35\\x03\\xb6\\xef\\x05\\x9e\\xe9)\\xca\\x80\\x87\\x80\\x17\\xab\\\n\\xda\\x0c\\xcd\\x81e{'\\xf0\\x0aQt\\x91\\x03\\xb6\\xbf*\\\nk7\\x143`\\xfb&\\xe0M`}\\x8d\\xd9me\\x85\\\n\\x9d\\x07`\\xfb*\\xe0]`c\\xc2\\xf4\\xa5\\xb2\\xc2N\\x03\\\n\\xb0}9\\xf0>\\xe9l\\xf8iI\\xaf\\x97Ut\\x16\\x80\\\n\\xed\\x8d\\xc0\\x140\\x9e0}\\x15x\\xac\\xaa\\xb2\\x93\\x00l\\\n\\xaf\\x07\\xde\\x02nL\\x98\\xbe\\x07LH*n\\xf5K\\xac\\\nz\\x00\\xb6G\\x88\\xa3zK\\xc2\\xf4\\x0b\\xe0.I\\x8bu\\\nF]\\xcc\\xc0\\x11\\xe0\\x9e\\x84\\xcd\\xb7\\xc0\\x1eI\\xbf\\xa5\\x9c\\\n5\\x0a\\xc0v\\x96\\x8f\\xdc@\\xd8>\\x08<\\x920\\xfb\\x11\\\n\\xd8-i.a\\x07$\\x02\\xc8\\x85O\\x02\\x7f\\x003\\xb6\\\n\\xafo\\xa4\\xb4\\xdc\\xd7\\x04\\xf0d\\xc2\\xec\\x17\\xe0VI\\xdf\\\n7\\xf5[\\x99\\x0b\\xd9\\x1e\\x03\\x8e\\x03{{\\xeaf\\x81\\x9b\\\n%}\\xd3\\xb4\\x03\\x00\\xdb{\\x89\\x8b\\xb6x\\xa7\\xed\\xe5,\\\nq\\xe4?\\xab2\\xe87\\x17z\\x8e\\xe5\\xe2!\\xee\\xd7\\x1f\\\n\\xdb\\xbe\\xb2Vq\\x0f\\xb6\\xb7\\x01o\\x14;.\\xf0\\x17p\\\n_\\x9d\\xf8*J\\x03\\xc8G\\xec\\xc1\\x8a6\\x9b\\x81\\x8fl\\\noN9\\xb7\\xbd\\x15x\\x1b\\xd8\\x900}X\\xd2\\xf1\\x94\\\n\\xbf2\\xaaf\\xe0\\x92D\\xbbqb\\x10\\x9b\\xaa\\x0clo\\\n!\\x9e\\xb2)_OH:\\x9a\\xb0\\xa9\\xa4*\\x80c\\xc4\\\n}\\xb8\\x8ek\\x80\\x0fl_T\\xac\\xb0}1Q\\xfc\\x96\\\n\\x84\\x8f\\x17$\\x1dN\\xaa\\xac\\xa14\\x00I\\x0b\\xc4\\xec\\xaf\\\n4\\x85\\xed\\xe1\\x06`\\xca\\xf6\\x05\\xff\\x14\\xd8\\xde@|l\\\n\\xb6&\\xda\\x9e \\xe6\\xfa\\x03Q{#\\xcb\\x93\\xad\\x93\\xc0\\\n\\xd5\\x09?\\x9f\\x02\\xb7\\x03\\xf3\\xc4\\xdd\\xa6\\xb8\\xf8\\x8bL\\x03\\\n\\xbb$\\xfd\\xd9\\x8f\\xd8\\xb2](y\\xa5\\xb4-b\\x10J\\\n\\xf8\\x9f\\x22\\x1eB\\x13\\x09\\xbb\\x19\\xe2V\\xfcs#\\xd5=\\\n\\xb4\\x0a\\x00\\x96r\\xf6\\x93\\xc0\\x15\\xfdvZ\\xc0\\xc06I\\\n?\\xb4i\\xdc\\xfaN,\\xe9;`'p\\xa6M\\xc79\\\n?\\x11\\x0f\\xaaV\\xe2\\xabh\\x9c\\xdfH:\\x05\\xec\\x06~\\\nm\\xd1\\xcf\\xef\\xc0\\x1d\\x92\\xben\\xd1\\xb6\\x96\\xbe\\x124I\\\n_\\x02{rAMY\\x04\\xf6I\\xfa\\xbc\\x9f\\xbe\\x9a\\xd2\\\nw\\x86)i\\x1a\\xb8\\x93\\x98\\xbb4\\xe1\\x01I\\xef\\xf4\\xdb\\\nOSZ\\xa5\\xc8\\x92>\\x04\\xf6\\x11G\\xb7\\x8e\\x83\\x92^\\\nn\\xd3GSZ\\xe7\\xf8\\x92N\\x00\\x07\\x88\\x89X\\x19\\xcf\\\nJz\\xaa\\xad\\xff\\xa6\\x0ctI\\x91t\\x0c\\xb8\\x9f\\xff\\xae\\\n\\x89\\xe7\\x81\\xc9A|7eE\\xde\\x8d\\xda\\x1e'\\x9e\\xbe\\\n\\x17\\x02\\x9f\\xe4\\xebd\\xc5i}\\x90\\x0d\\x0bU\\x07\\xd9B\\\n7rV\\x84\\xf9Qbn\\xd2\\xfb~f]\\x1e\\xe90\\\nR\\xbc\\xd5\\xcd\\x8c\\x123\\xc3\\xe2\\x0b\\xa6\\xba\\xeb\\xdf01\\\n\\xbd\\xf6?5\\xc8\\xbf\\xfa\\xd8\\x9f\\x17\\xac\\x15f\\x81\\xfdY\\\n\\x96\\xcd-\\xfd\\x99\\x90\\xcf\\xc4!\\xfe\\xfd\\xdc\\xa6\\xee]}\\\n\\x17\\xcc\\xb3\\xfcs\\x9b3\\x00\\x7f\\x03\\xd9\\x1a\\xfb\\xdb\\xbb\\xa7\\\n\\x8f\\x07\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa0\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x14\\x1f\\x0d\\xfc\\\nR+\\x9c\\x00\\x00\\x00$IDAT\\x08\\xd7c`@\\\n\\x05s>\\xc0XL\\xc8\\x5c&dY&d\\xc5pN\\\n\\x8a\\x00\\x9c\\x93\\x22\\x80a\\x1a\\x0a\\x00\\x00)\\x95\\x08\\xaf\\x88\\\n\\xac\\xba4\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x08\\x15;\\xdc\\\n;\\x0c\\x9b\\x00\\x00\\x00*IDAT\\x08\\xd7c`\\xc0\\\n\\x00\\x8c\\x0c\\x0cs> \\x0b\\xa4\\x08020 \\x0b\\xa6\\\n\\x08000B\\x98\\x10\\xc1\\x14\\x01\\x14\\x13P\\xb5\\xa3\\x01\\\n\\x00\\xc6\\xb9\\x07\\x90]f\\x1f\\x83\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa0\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x14\\x1c\\x1f$\\\n\\xc6\\x09\\x17\\x00\\x00\\x00$IDAT\\x08\\xd7c`@\\\n\\x05\\xff\\xcf\\xc3XL\\xc8\\x5c&dY&d\\xc5p\\x0e\\\n\\xa3!\\x9c\\xc3h\\x88a\\x1a\\x0a\\x00\\x00m\\x84\\x09u7\\\n\\x9e\\xd9#\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x01[\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x0dIDATh\\x81\\xed\\\n\\xda\\xb1m\\x02A\\x10F\\xe1w\\xc7\\xe2\\x0a,\\x87\\xd3\\x00\\\n8 'r\\x17\\x14\\x83\\x037\\xe3.\\x1cQ\\x0240\\\n!\\xc2\\x0d`\\x90\\x1c\\xec\\x9e\\x0c'Y\\xf6\\x09\\x89\\xffV\\\n\\x9a/cE0\\x0f\\x0e\\x92\\x9d\\x86\\xc2\\xdd\\x1f\\x81W`\\\n\\x09\\xcc\\x81)\\xe3\\xf2\\x05l\\x81\\x0d\\xf0ff\\x07\\x80\\x06\\\n\\xc0\\xdd_\\x80w\\xe0I6\\xde0{`ef\\x1fM\\\n\\xf9\\xe4w\\xd43|g\\x0f\\xccZ\\xf2cS\\xdb\\xf0\\x90\\\ng^'\\xf23\\xdfw\\xbe\\xf30\\xff5\\xe9\\xbd^&\\\n\\xf2\\x0f\\xf6\\xd2\\xd9\\xcc\\xd2\\x9d\\x06\\x1a\\xc4\\xddO\\x5cG<\\\n\\xb7\\x8c\\xef\\xdff\\x88i\\xab\\x9e\\xe0V\\x11\\xa0\\x16\\x01j\\\n\\x11\\xa0\\x16\\x01j\\x11\\xa0\\x16\\x01j\\x11\\xa0\\x16\\x01j\\x11\\\n\\xa0\\x16\\x01j\\x11\\xa0\\x16\\x01j\\x11\\xa0\\x16\\x01j\\x11\\xa0\\\n\\x16\\x01j\\x11\\xa0\\xd6\\x92o\\xc0kuL\\xe4\\xeb\\xfb\\xc5\\\n\\xc5\\xe1\\xa4\\xdc\\x06\\x8eQ\\xff\\x9au\\x9b\\xc8\\xbb\\x07\\x8b?\\\n\\xde8V\\x9b\\xfaW\\x0d\\xca\\xd6\\xc7\\xaa\\x1c\\xd4\\xa2[\\xf6\\\n84\\xddI\\xf9&\\xd6\\xfc\\xac\\xdb<\\x88\\x86\\xfb\\xcd\\x91\\\n\\xebu\\x9bO\\x80oV\\x016\\x1ew\\x0d\\xa5B\\x00\\x00\\\n\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x08\\x15;\\xdc\\\n;\\x0c\\x9b\\x00\\x00\\x00*IDAT\\x08\\xd7c`\\xc0\\\n\\x00\\x8c\\x0c\\x0cs> \\x0b\\xa4\\x08020 \\x0b\\xa6\\\n\\x08000B\\x98\\x10\\xc1\\x14\\x01\\x14\\x13P\\xb5\\xa3\\x01\\\n\\x00\\xc6\\xb9\\x07\\x90]f\\x1f\\x83\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x00\\x9f\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x08\\x14\\x1f\\xf9\\\n#\\xd9\\x0b\\x00\\x00\\x00#IDAT\\x08\\xd7c`\\xc0\\\n\\x0d\\xe6|\\x80\\xb1\\x18\\x91\\x05R\\x04\\xe0B\\x08\\x15)\\x02\\\n\\x0c\\x0c\\x8c\\xc8\\x02\\x08\\x95h\\x00\\x00\\xac\\xac\\x07\\x90Ne\\\n4\\xac\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x07\\xdd\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x07\\x00\\x00\\x00\\x0a\\x08\\x06\\x00\\x00\\x00x\\xccD\\x0d\\\n\\x00\\x00\\x05RiTXtXML:com.\\\nadobe.xmp\\x00\\x00\\x00\\x00\\x00<?\\\nxpacket begin=\\x22\\xef\\\n\\xbb\\xbf\\x22 id=\\x22W5M0MpCe\\\nhiHzreSzNTczkc9d\\\n\\x22?>\\x0a<x:xmpmeta x\\\nmlns:x=\\x22adobe:ns\\\n:meta/\\x22 x:xmptk=\\\n\\x22XMP Core 5.5.0\\x22\\\n>\\x0a <rdf:RDF xmln\\\ns:rdf=\\x22http://ww\\\nw.w3.org/1999/02\\\n/22-rdf-syntax-n\\\ns#\\x22>\\x0a  <rdf:Desc\\\nription rdf:abou\\\nt=\\x22\\x22\\x0a    xmlns:d\\\nc=\\x22http://purl.o\\\nrg/dc/elements/1\\\n.1/\\x22\\x0a    xmlns:e\\\nxif=\\x22http://ns.a\\\ndobe.com/exif/1.\\\n0/\\x22\\x0a    xmlns:ti\\\nff=\\x22http://ns.ad\\\nobe.com/tiff/1.0\\\n/\\x22\\x0a    xmlns:pho\\\ntoshop=\\x22http://n\\\ns.adobe.com/phot\\\noshop/1.0/\\x22\\x0a    \\\nxmlns:xmp=\\x22http:\\\n//ns.adobe.com/x\\\nap/1.0/\\x22\\x0a    xml\\\nns:xmpMM=\\x22http:/\\\n/ns.adobe.com/xa\\\np/1.0/mm/\\x22\\x0a    x\\\nmlns:stEvt=\\x22http\\\n://ns.adobe.com/\\\nxap/1.0/sType/Re\\\nsourceEvent#\\x22\\x0a  \\\n exif:PixelXDime\\\nnsion=\\x227\\x22\\x0a   exi\\\nf:PixelYDimensio\\\nn=\\x2210\\x22\\x0a   exif:C\\\nolorSpace=\\x221\\x22\\x0a  \\\n tiff:ImageWidth\\\n=\\x227\\x22\\x0a   tiff:Ima\\\ngeLength=\\x2210\\x22\\x0a  \\\n tiff:Resolution\\\nUnit=\\x222\\x22\\x0a   tiff\\\n:XResolution=\\x2272\\\n.0\\x22\\x0a   tiff:YRes\\\nolution=\\x2272.0\\x22\\x0a \\\n  photoshop:Colo\\\nrMode=\\x223\\x22\\x0a   pho\\\ntoshop:ICCProfil\\\ne=\\x22sRGB IEC61966\\\n-2.1\\x22\\x0a   xmp:Mod\\\nifyDate=\\x222021-05\\\n-31T12:43:09+02:\\\n00\\x22\\x0a   xmp:Metad\\\nataDate=\\x222021-05\\\n-31T12:43:09+02:\\\n00\\x22>\\x0a   <dc:titl\\\ne>\\x0a    <rdf:Alt>\\\n\\x0a     <rdf:li xm\\\nl:lang=\\x22x-defaul\\\nt\\x22>branch_close<\\\n/rdf:li>\\x0a    </r\\\ndf:Alt>\\x0a   </dc:\\\ntitle>\\x0a   <xmpMM\\\n:History>\\x0a    <r\\\ndf:Seq>\\x0a     <rd\\\nf:li\\x0a      stEvt\\\n:action=\\x22produce\\\nd\\x22\\x0a      stEvt:s\\\noftwareAgent=\\x22Af\\\nfinity Designer \\\n1.9.2\\x22\\x0a      stE\\\nvt:when=\\x222021-05\\\n-31T12:43:09+02:\\\n00\\x22/>\\x0a    </rdf:\\\nSeq>\\x0a   </xmpMM:\\\nHistory>\\x0a  </rdf\\\n:Description>\\x0a <\\\n/rdf:RDF>\\x0a</x:xm\\\npmeta>\\x0a<?xpacket\\\n end=\\x22r\\x22?>X\\xad\\xf2\\x80\\x00\\x00\\\n\\x01\\x83iCCPsRGB IEC61\\\n966-2.1\\x00\\x00(\\x91u\\x91\\xcf+D\\\nQ\\x14\\xc7?fh\\xfc\\x18\\x8dba1e\\x12\\x16B\\\n\\x83\\x12\\x1b\\x8b\\x99\\x18\\x0a\\x8b\\x99Q~mf\\x9ey3\\\nj\\xdex\\xbd7\\xd2d\\xabl\\xa7(\\xb1\\xf1k\\xc1_\\\n\\xc0VY+E\\xa4d\\xa7\\xac\\x89\\x0dz\\xce\\x9bQ#\\\n\\x99s;\\xf7|\\xee\\xf7\\xdes\\xba\\xf7\\x5cpD\\xd3\\x8a\\\nfV\\xfaA\\xcbd\\x8dp(\\xe0\\x9b\\x99\\x9d\\xf3\\xb9\\x9e\\\n\\xa8\\xa2\\x85\\x1a:\\xf1\\xc6\\x14S\\x9f\\x8c\\x8cF)k\\xef\\\n\\xb7T\\xd8\\xf1\\xba\\xdb\\xaeU\\xfe\\xdc\\xbfV\\xb7\\x980\\x15\\\n\\xa8\\xa8\\x16\\x1eVt#+<&<\\xb1\\x9a\\xd5m\\xde\\\n\\x12nRR\\xb1E\\xe1\\x13\\xe1.C.(|c\\xeb\\\n\\xf1\\x22?\\xdb\\x9c,\\xf2\\xa7\\xcdF4\\x1c\\x04G\\x83\\xb0\\\n/\\xf9\\x8b\\xe3\\xbfXI\\x19\\x9a\\xb0\\xbc\\x9c6-\\xbd\\xa2\\\n\\xfc\\xdc\\xc7~\\x89;\\x91\\x99\\x8eHl\\x15\\xf7b\\x12&\\\nD\\x00\\x1f\\xe3\\x8c\\x10d\\x80^\\x86d\\x1e\\xa0\\x9b>z\\\ndE\\x99|\\x7f!\\x7f\\x8ae\\xc9Ud\\xd6\\xc9a\\xb0\\\nD\\x92\\x14Y\\xbaD]\\x91\\xea\\x09\\x89\\xaa\\xe8\\x09\\x19i\\\nrv\\xff\\xff\\xf6\\xd5T\\xfb\\xfb\\x8a\\xd5\\xdd\\x01\\xa8z\\xb4\\\n\\xac\\xd7vpm\\xc2W\\xde\\xb2>\\x0e,\\xeb\\xeb\\x10\\x9c\\\n\\x0fp\\x9e)\\xe5/\\xef\\xc3\\xe0\\x9b\\xe8\\xf9\\x92\\xd6\\xb6\\x07\\\n\\x9eu8\\xbd(i\\xf1m8\\xdb\\x80\\xe6{=f\\xc4\\\n\\x0a\\x92S\\xdc\\xa1\\xaa\\xf0r\\x0c\\xf5\\xb3\\xd0x\\x05\\xb5\\xf3\\\n\\xc5\\x9e\\xfd\\xecst\\x07\\xd15\\xf9\\xaaK\\xd8\\xd9\\x85\\x0e\\\n9\\xefY\\xf8\\x06\\x8e\\xfdg\\xf8\\xfd\\x8a\\x18\\x97\\x00\\x00\\x00\\\n\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\\n\\x9c\\x18\\x00\\x00\\x00\\xa2IDAT\\x18\\x95U\\xcf\\xb1J\\\n\\xc31\\x00\\xc4\\xe1/\\xff\\xb9\\x93\\xa3\\x93\\xb8\\xa5\\x8b\\x0f \\\nUD\\x10\\x5c:\\x84,\\x1d\\x5c|\\x0f\\xb7\\x8e>J\\x88\\\n\\xa3\\xb8\\x08m\\x05\\xbbw\\xc8\\xea\\xe2\\x0bto\\xe9\\xd2B\\\nzpp\\xf0\\xe3\\x0e.\\xa4\\xd2\\xae\\xf0\\x8a\\xf7\\x9a\\xe3V\\\n\\xa7\\x01\\xd7x\\xc32\\x95vy\\x06k\\x8e\\xdfx\\xc1\\x18\\\n\\xbf\\xa9\\xb4\\xf1\\x09\\x86SH\\xa5=\\xe2\\x03;Lk\\x8e\\\n\\xab\\xd0\\xcf\\xa4\\xd2n\\xf0\\x89\\x0b\\xdc\\x0f\\xce\\xb5?: \\\n\\x0c]\\xeb\\x01?\\x18\\xe1\\xa9\\xe6\\xb8\\x1e\\x8e`\\x86/l\\\nq[s\\x5c@H\\xa5\\xdda\\x81\\x0d\\x9ek\\x8e\\xff\\xfd\\\n\\xcf?\\xcc1\\xe9\\x01\\x1c\\x00sR-q\\xe4J\\x1bi\\\n\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x07\\x06\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x0a\\x00\\x00\\x00\\x07\\x08\\x06\\x00\\x00\\x001\\xac\\xdcc\\\n\\x00\\x00\\x04\\xb0iTXtXML:com.\\\nadobe.xmp\\x00\\x00\\x00\\x00\\x00<?\\\nxpacket begin=\\x22\\xef\\\n\\xbb\\xbf\\x22 id=\\x22W5M0MpCe\\\nhiHzreSzNTczkc9d\\\n\\x22?>\\x0a<x:xmpmeta x\\\nmlns:x=\\x22adobe:ns\\\n:meta/\\x22 x:xmptk=\\\n\\x22XMP Core 5.5.0\\x22\\\n>\\x0a <rdf:RDF xmln\\\ns:rdf=\\x22http://ww\\\nw.w3.org/1999/02\\\n/22-rdf-syntax-n\\\ns#\\x22>\\x0a  <rdf:Desc\\\nription rdf:abou\\\nt=\\x22\\x22\\x0a    xmlns:e\\\nxif=\\x22http://ns.a\\\ndobe.com/exif/1.\\\n0/\\x22\\x0a    xmlns:ti\\\nff=\\x22http://ns.ad\\\nobe.com/tiff/1.0\\\n/\\x22\\x0a    xmlns:pho\\\ntoshop=\\x22http://n\\\ns.adobe.com/phot\\\noshop/1.0/\\x22\\x0a    \\\nxmlns:xmp=\\x22http:\\\n//ns.adobe.com/x\\\nap/1.0/\\x22\\x0a    xml\\\nns:xmpMM=\\x22http:/\\\n/ns.adobe.com/xa\\\np/1.0/mm/\\x22\\x0a    x\\\nmlns:stEvt=\\x22http\\\n://ns.adobe.com/\\\nxap/1.0/sType/Re\\\nsourceEvent#\\x22\\x0a  \\\n exif:PixelXDime\\\nnsion=\\x2210\\x22\\x0a   ex\\\nif:PixelYDimensi\\\non=\\x227\\x22\\x0a   exif:C\\\nolorSpace=\\x221\\x22\\x0a  \\\n tiff:ImageWidth\\\n=\\x2210\\x22\\x0a   tiff:Im\\\nageLength=\\x227\\x22\\x0a  \\\n tiff:Resolution\\\nUnit=\\x222\\x22\\x0a   tiff\\\n:XResolution=\\x2272\\\n.0\\x22\\x0a   tiff:YRes\\\nolution=\\x2272.0\\x22\\x0a \\\n  photoshop:Colo\\\nrMode=\\x223\\x22\\x0a   pho\\\ntoshop:ICCProfil\\\ne=\\x22sRGB IEC61966\\\n-2.1\\x22\\x0a   xmp:Mod\\\nifyDate=\\x222021-05\\\n-31T12:30:11+02:\\\n00\\x22\\x0a   xmp:Metad\\\nataDate=\\x222021-05\\\n-31T12:30:11+02:\\\n00\\x22>\\x0a   <xmpMM:H\\\nistory>\\x0a    <rdf\\\n:Seq>\\x0a     <rdf:\\\nli\\x0a      stEvt:a\\\nction=\\x22produced\\x22\\\n\\x0a      stEvt:sof\\\ntwareAgent=\\x22Affi\\\nnity Designer 1.\\\n9.2\\x22\\x0a      stEvt\\\n:when=\\x222021-05-3\\\n1T12:30:11+02:00\\\n\\x22/>\\x0a    </rdf:Se\\\nq>\\x0a   </xmpMM:Hi\\\nstory>\\x0a  </rdf:D\\\nescription>\\x0a </r\\\ndf:RDF>\\x0a</x:xmpm\\\neta>\\x0a<?xpacket e\\\nnd=\\x22r\\x22?>\\x85\\x9d\\x9f\\x08\\x00\\x00\\x01\\x83\\\niCCPsRGB IEC6196\\\n6-2.1\\x00\\x00(\\x91u\\x91\\xcf+DQ\\x14\\\n\\xc7?fh\\xfc\\x18\\x8dba1e\\x12\\x16B\\x83\\x12\\\n\\x1b\\x8b\\x99\\x18\\x0a\\x8b\\x99Q~mf\\x9ey3j\\xde\\\nx\\xbd7\\xd2d\\xabl\\xa7(\\xb1\\xf1k\\xc1_\\xc0V\\\nY+E\\xa4d\\xa7\\xac\\x89\\x0dz\\xce\\x9bQ#\\x99s\\\n;\\xf7|\\xee\\xf7\\xdes\\xba\\xf7\\x5cpD\\xd3\\x8afV\\\n\\xfaA\\xcbd\\x8dp(\\xe0\\x9b\\x99\\x9d\\xf3\\xb9\\x9e\\xa8\\xa2\\\n\\x85\\x1a:\\xf1\\xc6\\x14S\\x9f\\x8c\\x8cF)k\\xef\\xb7T\\\n\\xd8\\xf1\\xba\\xdb\\xaeU\\xfe\\xdc\\xbfV\\xb7\\x980\\x15\\xa8\\xa8\\\n\\x16\\x1eVt#+<&<\\xb1\\x9a\\xd5m\\xde\\x12n\\\nRR\\xb1E\\xe1\\x13\\xe1.C.(|c\\xeb\\xf1\\x22\\\n?\\xdb\\x9c,\\xf2\\xa7\\xcdF4\\x1c\\x04G\\x83\\xb0/\\xf9\\\n\\x8b\\xe3\\xbfXI\\x19\\x9a\\xb0\\xbc\\x9c6-\\xbd\\xa2\\xfc\\xdc\\\n\\xc7~\\x89;\\x91\\x99\\x8eHl\\x15\\xf7b\\x12&D\\x00\\\n\\x1f\\xe3\\x8c\\x10d\\x80^\\x86d\\x1e\\xa0\\x9b>zdE\\\n\\x99|\\x7f!\\x7f\\x8ae\\xc9Ud\\xd6\\xc9a\\xb0D\\x92\\\n\\x14Y\\xbaD]\\x91\\xea\\x09\\x89\\xaa\\xe8\\x09\\x19irv\\\n\\xff\\xff\\xf6\\xd5T\\xfb\\xfb\\x8a\\xd5\\xdd\\x01\\xa8z\\xb4\\xac\\xd7\\\nvpm\\xc2W\\xde\\xb2>\\x0e,\\xeb\\xeb\\x10\\x9c\\x0fp\\\n\\x9e)\\xe5/\\xef\\xc3\\xe0\\x9b\\xe8\\xf9\\x92\\xd6\\xb6\\x07\\x9eu\\\n8\\xbd(i\\xf1m8\\xdb\\x80\\xe6{=f\\xc4\\x0a\\x92\\\nS\\xdc\\xa1\\xaa\\xf0r\\x0c\\xf5\\xb3\\xd0x\\x05\\xb5\\xf3\\xc5\\x9e\\\n\\xfd\\xecst\\x07\\xd15\\xf9\\xaaK\\xd8\\xd9\\x85\\x0e9\\xef\\\nY\\xf8\\x06\\x8e\\xfdg\\xf8\\xfd\\x8a\\x18\\x97\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00mIDAT\\x18\\x95u\\xcf\\xc1\\x09\\xc2P\\\n\\x10\\x84\\xe1\\xd7\\x85\\x07\\x9b\\xd0C@\\xd2\\x82x\\x14{0\\\nW!\\x8d\\x84`?bKzH\\xcc\\x97\\x83\\xfb0\\x04\\\n\\xdf\\x9c\\x86\\x7fg\\x99\\xdd\\x84\\x0d\\xaaT\\x10jl\\x13\\x1e\\\n\\xbe\\xba\\xfe\\x0951{\\xe6\\x8d\\x0f&\\x1c\\x17\\xa1S\\xb0\\\n\\x11\\x87\\x0c/\\x01\\x07\\xec\\xb0\\x0f?\\xe1\\xbc\\xaei\\xa3\\xe6\\\n\\x85w\\xf8[\\xe9\\xf0\\xbb\\x9f\\xfa\\xd2\\x839\\xdc\\xa3[\\xf3\\\n\\x19.\\xa8\\x89\\xb50\\xf7C\\xa0\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x14\\x1f \\xb9\\\n\\x8dw\\xe9\\x00\\x00\\x00*IDAT\\x08\\xd7c`\\xc0\\\n\\x06\\xe6|```B0\\xa1\\x1c\\x08\\x93\\x81\\x81\\x09\\xc1\\\nd``b`H\\x11@\\xe2 s\\x19\\x90\\x8d@\\x02\\\n\\x00#\\xed\\x08\\xafd\\x9f\\x0f\\x15\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x14\\x1d\\x00\\xb0\\\n\\xd55\\xa3\\x00\\x00\\x00*IDAT\\x08\\xd7c`\\xc0\\\n\\x06\\xfe\\x9fg``B0\\xa1\\x1c\\x08\\x93\\x81\\x81\\x09\\xc1\\\nd``b``4D\\xe2 s\\x19\\x90\\x8d@\\x02\\\n\\x00d@\\x09u\\x86\\xb3\\xad\\x9c\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x01v\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01(IDATh\\x81\\xed\\\n\\xda\\xb1J\\xc3P\\x14\\x87\\xf1/7\\xb7\\xe0\\xae\\xf8\\x00\\x82\\\nSu\\xe8\\xde\\xc9ly\\x80@\\x1fF\\x87\\xfa\\x22nB\\\n\\xdc\\xb3\\xc5\\xa9/ \\xb4]:t\\x0f}\\x82j\\xc1\\xe1\\\n\\xa6P\\xb3h\\x10\\xfa\\xcf\\x85\\xf3\\xdbR:\\x9c\\xaf\\xdcf\\\n97\\xa1\\x95\\xe5\\xc5\\x15\\xf0\\x04L\\x81;`\\xc4\\xb0|\\\n\\x02K`\\x01\\xcc\\xeb\\xaa\\xdc\\x01$\\x00Y^<\\x00\\xaf\\\n\\xc0\\xb5l\\xbc~\\x1a`VW\\xe5{\\xd2\\xfe\\xf2+\\xe2\\\n\\x19\\xfe\\xa8\\x01\\xc6\\x8eplb\\x1b\\x1e\\xc2\\xcc\\x8f\\x9ep\\\n\\xe6\\xbb\\x0eg\\x1e\\xe6\\xaf\\xd2\\xce\\xf3\\xd4\\x13\\xfe\\xb0\\xa7\\x0e\\\nuU\\xfa3\\x0d\\xd4K\\x96\\x17_\\xfc\\x8c\\xb8w\\x0c\\xef\\\nm\\xd3\\xc7\\xc8\\xa9'\\xf8/\\x0bP\\xb3\\x005\\x0bP\\xb3\\\n\\x005\\x0bP\\xb3\\x005\\x0bP\\xb3\\x005\\x0bP\\xb3\\x00\\\n5\\x0bP\\xb3\\x005\\x0bP\\xb3\\x005\\x0bP\\xb3\\x005\\\n\\x0bPs\\x84\\x0dx\\xac\\xf6\\x9e\\xb0\\xbe\\x9f\\x9c|\\x98\\xb6\\\n\\xdb\\xc0!\\xea\\xaeY\\x97\\x9ep\\xf7`\\xf2\\xcb\\x17\\x87j\\\n\\xe1\\x809am\\x1f\\x9b\\x06xv\\xed\\xad\\x8f\\x19qE\\\n\\x1c/{\\xecR\\x80\\xedf\\xb5\\xbd\\xb9\\x1d\\xbf\\x00\\x17\\x84\\\n\\xc5\\xf7%\\xc3;F{\\xe0\\x03x\\x03\\x8a\\xba*\\xd7\\x00\\\n\\xdf\\xa4\\xb56\\xa2\\xca\\x99tG\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x14\\x1d\\x00\\xb0\\\n\\xd55\\xa3\\x00\\x00\\x00*IDAT\\x08\\xd7c`\\xc0\\\n\\x06\\xfe\\x9fg``B0\\xa1\\x1c\\x08\\x93\\x81\\x81\\x09\\xc1\\\nd``b``4D\\xe2 s\\x19\\x90\\x8d@\\x02\\\n\\x00d@\\x09u\\x86\\xb3\\xad\\x9c\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa0\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x14\\x1c\\x1f$\\\n\\xc6\\x09\\x17\\x00\\x00\\x00$IDAT\\x08\\xd7c`@\\\n\\x05\\xff\\xcf\\xc3XL\\xc8\\x5c&dY&d\\xc5p\\x0e\\\n\\xa3!\\x9c\\xc3h\\x88a\\x1a\\x0a\\x00\\x00m\\x84\\x09u7\\\n\\x9e\\xd9#\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa5\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\x9cS4\\xfc]\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x0b\\x02\\x04m\\\n\\x98\\x1bi\\x00\\x00\\x00)IDAT\\x08\\xd7c`\\xc0\\\n\\x00\\x8c\\x0c\\x0c\\xff\\xcf\\xa3\\x08\\x18220 \\x0b2\\x1a\\\n200B\\x98\\x10AFC\\x14\\x13P\\xb5\\xa3\\x01\\x00\\\n\\xd6\\x10\\x07\\xd2/H\\xdfJ\\x00\\x00\\x00\\x00IEND\\\n\\xaeB`\\x82\\\n\\x00\\x00\\x00\\x9e\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x08\\x15\\x0f\\xfd\\\n\\x8f\\xf8.\\x00\\x00\\x00\\x22IDAT\\x08\\xd7c`\\xc0\\\n\\x0d\\xfe\\x9f\\x87\\xb1\\x18\\x91\\x05\\x18\\x0d\\xe1BH*\\x0c\\x19\\\n\\x18\\x18\\x91\\x05\\x10*\\xd1\\x00\\x00\\xca\\xb5\\x07\\xd2v\\xbb\\xb2\\\n\\xc5\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x01\\xef\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\xa1IDATh\\x81\\xed\\\n\\x9a\\xbfN\\xc2P\\x14\\x87\\xbf\\x96\\xe2\\xa4\\x9bq\\xbc\\x8b\\x1b\\\n\\xea\\xc0\\xe2D\\x1c\\x8c\\x83\\x83\\xd1\\x81\\x89\\x84\\xd1\\x17\\xf0\\x01\\\np\\xc0\\x07\\xf0\\x05\\x1cI\\x9c\\xba\\xa8#q0<\\x02v\\\n\\x92\\xe5\\x8e\\x04'\\xe3\\xc2\\x9f\\xc4\\xa1m\\x04B\\x8b\\x95\\xc2\\\n\\xa1\\xe4~[{\\xee\\xf0\\xfb\\x9a{o\\x9a\\x9cc\\x11P\\\nu\\xbd]\\xe0\\x16(\\x01\\x87@\\x9e\\xf5b\\x00\\xb4\\x81\\x16\\\nPo\\x94\\x0b=\\x00\\x0b\\xa0\\xeaz\\xa7\\xc0#\\xb0'\\x16\\\n/\\x19]\\xa0\\xd2(\\x17^\\xad\\xe0\\xcb\\xbf\\x93\\x9d\\xf0!\\\n]\\xe0\\xc0\\xc6\\xdf6Y\\x0b\\x0f~\\xe6\\x9a\\x83\\xbf\\xe7\\xa7\\\n\\x19\\xad8\\xcc_\\xc9M=\\x97\\x1c\\xfc\\x03;\\xce\\xa8Q\\\n.8+\\x0a\\x94\\x88\\xaa\\xeb\\x0d\\x99\\x948\\xb2Y\\xbf\\xdb\\\n&\\x09y[:\\xc1\\xa2\\x18\\x01i\\x8c\\x804F@\\x1a\\\n# \\x8d\\x11\\x90\\xc6\\x08H3\\xf7\\xb7Yk}\\x06\\x1c\\\n\\x03\\xdb\\xcb\\x8f3\\xc1\\x10\\xf0\\x80g\\xa5\\xd4w\\xd4\\xa2H\\\n\\x01\\xad\\xb5\\x03\\xb8\\xc0e\\xfa\\xd9\\x12\\xd1\\xd1Z\\x9f+\\xa5\\\n>f\\x15\\xe3\\xb6\\xd0\\x0d\\xf2\\xe1\\x01\\xf6\\x81\\x87\\xa8b\\x9c\\\n\\xc0E\\xfaY\\xfe\\xcd\\x89\\xd6zgV!\\xf3\\x878N\\\n\\xe0ee)\\xe6\\xf3\\xa6\\x94\\xfa\\x9aU\\x88\\x13\\xb8\\x07\\x9e\\\n\\x96\\x93'\\x11\\x1d\\xe0:\\xaa\\x18y\\x0b)\\xa5\\x86\\xc0U\\\nf\\xaf\\xd1\\x10\\xa5T\\x13h\\xa6\\x18,U6\\xfa\\x10g\\\n\\x02# \\x8d\\x11\\x90\\xc6\\x08Hc\\x04\\xa41\\x02\\xd2l\\\n\\x84\\xc0@:\\xc4\\x02\\xf4\\x1d\\xfc\\xf6}q\\xece.\\xe8\\\n\\x06\\xae#\\xd3m\\xd6\\xb6\\x83?{P\\x9c\\xb3p]i\\\n\\xd9@\\x1d\\xbfm\\x9f5\\xba\\xc0\\x9d\\x1dL}T\\xc8\\x96\\\nD8\\xec\\xd1\\xb3\\xc27\\xc1\\xd0G\\x8d\\xdfq\\x9b-\\xa1\\\npQ\\xf4\\x99\\x1c\\xb7\\xf9\\x04\\xf8\\x01o\\xedXc-\\xfd\\\n\\xb2Y\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x070\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x0a\\x00\\x00\\x00\\x07\\x08\\x06\\x00\\x00\\x001\\xac\\xdcc\\\n\\x00\\x00\\x04\\xb0iTXtXML:com.\\\nadobe.xmp\\x00\\x00\\x00\\x00\\x00<?\\\nxpacket begin=\\x22\\xef\\\n\\xbb\\xbf\\x22 id=\\x22W5M0MpCe\\\nhiHzreSzNTczkc9d\\\n\\x22?>\\x0a<x:xmpmeta x\\\nmlns:x=\\x22adobe:ns\\\n:meta/\\x22 x:xmptk=\\\n\\x22XMP Core 5.5.0\\x22\\\n>\\x0a <rdf:RDF xmln\\\ns:rdf=\\x22http://ww\\\nw.w3.org/1999/02\\\n/22-rdf-syntax-n\\\ns#\\x22>\\x0a  <rdf:Desc\\\nription rdf:abou\\\nt=\\x22\\x22\\x0a    xmlns:e\\\nxif=\\x22http://ns.a\\\ndobe.com/exif/1.\\\n0/\\x22\\x0a    xmlns:ti\\\nff=\\x22http://ns.ad\\\nobe.com/tiff/1.0\\\n/\\x22\\x0a    xmlns:pho\\\ntoshop=\\x22http://n\\\ns.adobe.com/phot\\\noshop/1.0/\\x22\\x0a    \\\nxmlns:xmp=\\x22http:\\\n//ns.adobe.com/x\\\nap/1.0/\\x22\\x0a    xml\\\nns:xmpMM=\\x22http:/\\\n/ns.adobe.com/xa\\\np/1.0/mm/\\x22\\x0a    x\\\nmlns:stEvt=\\x22http\\\n://ns.adobe.com/\\\nxap/1.0/sType/Re\\\nsourceEvent#\\x22\\x0a  \\\n exif:PixelXDime\\\nnsion=\\x2210\\x22\\x0a   ex\\\nif:PixelYDimensi\\\non=\\x227\\x22\\x0a   exif:C\\\nolorSpace=\\x221\\x22\\x0a  \\\n tiff:ImageWidth\\\n=\\x2210\\x22\\x0a   tiff:Im\\\nageLength=\\x227\\x22\\x0a  \\\n tiff:Resolution\\\nUnit=\\x222\\x22\\x0a   tiff\\\n:XResolution=\\x2272\\\n.0\\x22\\x0a   tiff:YRes\\\nolution=\\x2272.0\\x22\\x0a \\\n  photoshop:Colo\\\nrMode=\\x223\\x22\\x0a   pho\\\ntoshop:ICCProfil\\\ne=\\x22sRGB IEC61966\\\n-2.1\\x22\\x0a   xmp:Mod\\\nifyDate=\\x222021-05\\\n-31T12:33:14+02:\\\n00\\x22\\x0a   xmp:Metad\\\nataDate=\\x222021-05\\\n-31T12:33:14+02:\\\n00\\x22>\\x0a   <xmpMM:H\\\nistory>\\x0a    <rdf\\\n:Seq>\\x0a     <rdf:\\\nli\\x0a      stEvt:a\\\nction=\\x22produced\\x22\\\n\\x0a      stEvt:sof\\\ntwareAgent=\\x22Affi\\\nnity Designer 1.\\\n9.2\\x22\\x0a      stEvt\\\n:when=\\x222021-05-3\\\n1T12:33:14+02:00\\\n\\x22/>\\x0a    </rdf:Se\\\nq>\\x0a   </xmpMM:Hi\\\nstory>\\x0a  </rdf:D\\\nescription>\\x0a </r\\\ndf:RDF>\\x0a</x:xmpm\\\neta>\\x0a<?xpacket e\\\nnd=\\x22r\\x22?>H\\x8b[^\\x00\\x00\\x01\\x83\\\niCCPsRGB IEC6196\\\n6-2.1\\x00\\x00(\\x91u\\x91\\xcf+DQ\\x14\\\n\\xc7?fh\\xfc\\x18\\x8dba1e\\x12\\x16B\\x83\\x12\\\n\\x1b\\x8b\\x99\\x18\\x0a\\x8b\\x99Q~mf\\x9ey3j\\xde\\\nx\\xbd7\\xd2d\\xabl\\xa7(\\xb1\\xf1k\\xc1_\\xc0V\\\nY+E\\xa4d\\xa7\\xac\\x89\\x0dz\\xce\\x9bQ#\\x99s\\\n;\\xf7|\\xee\\xf7\\xdes\\xba\\xf7\\x5cpD\\xd3\\x8afV\\\n\\xfaA\\xcbd\\x8dp(\\xe0\\x9b\\x99\\x9d\\xf3\\xb9\\x9e\\xa8\\xa2\\\n\\x85\\x1a:\\xf1\\xc6\\x14S\\x9f\\x8c\\x8cF)k\\xef\\xb7T\\\n\\xd8\\xf1\\xba\\xdb\\xaeU\\xfe\\xdc\\xbfV\\xb7\\x980\\x15\\xa8\\xa8\\\n\\x16\\x1eVt#+<&<\\xb1\\x9a\\xd5m\\xde\\x12n\\\nRR\\xb1E\\xe1\\x13\\xe1.C.(|c\\xeb\\xf1\\x22\\\n?\\xdb\\x9c,\\xf2\\xa7\\xcdF4\\x1c\\x04G\\x83\\xb0/\\xf9\\\n\\x8b\\xe3\\xbfXI\\x19\\x9a\\xb0\\xbc\\x9c6-\\xbd\\xa2\\xfc\\xdc\\\n\\xc7~\\x89;\\x91\\x99\\x8eHl\\x15\\xf7b\\x12&D\\x00\\\n\\x1f\\xe3\\x8c\\x10d\\x80^\\x86d\\x1e\\xa0\\x9b>zdE\\\n\\x99|\\x7f!\\x7f\\x8ae\\xc9Ud\\xd6\\xc9a\\xb0D\\x92\\\n\\x14Y\\xbaD]\\x91\\xea\\x09\\x89\\xaa\\xe8\\x09\\x19irv\\\n\\xff\\xff\\xf6\\xd5T\\xfb\\xfb\\x8a\\xd5\\xdd\\x01\\xa8z\\xb4\\xac\\xd7\\\nvpm\\xc2W\\xde\\xb2>\\x0e,\\xeb\\xeb\\x10\\x9c\\x0fp\\\n\\x9e)\\xe5/\\xef\\xc3\\xe0\\x9b\\xe8\\xf9\\x92\\xd6\\xb6\\x07\\x9eu\\\n8\\xbd(i\\xf1m8\\xdb\\x80\\xe6{=f\\xc4\\x0a\\x92\\\nS\\xdc\\xa1\\xaa\\xf0r\\x0c\\xf5\\xb3\\xd0x\\x05\\xb5\\xf3\\xc5\\x9e\\\n\\xfd\\xecst\\x07\\xd15\\xf9\\xaaK\\xd8\\xd9\\x85\\x0e9\\xef\\\nY\\xf8\\x06\\x8e\\xfdg\\xf8\\xfd\\x8a\\x18\\x97\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x97IDAT\\x18\\x95m\\xcf\\xb1j\\x02A\\\n\\x14\\x85\\xe1o\\xb7\\xb6\\xd0'H=Vi\\x03\\xb1\\xb4H\\\n;l\\xa5\\xf19\\xf6Y\\x02VB\\xbaa\\x0a\\x0b;\\x1b\\\n\\x1bkA\\x18\\x02)m\\xe3\\xbe\\x82\\xcd\\x06\\x16\\xd9\\xdb\\xdd\\\n\\x9f\\xff\\x5c\\xee\\xa9b*\\x13Ls\\x13nF&\\xa6\\xf2\\\n\\x82\\xaeF\\x8b\\xdf\\x98\\xca\\xfb\\x88\\xb4\\xc0\\x0f\\xda\\x1a[t\\\n\\xd8\\xc7T\\xc2@\\x9ac\\x8f?|U=|\\xc5\\x09w\\\n\\xbc\\xa1\\xc2\\x193,r\\x13.\\xd5\\xe0\\xc2\\x12\\x07\\x5cQ\\\n#\\xe0#7\\xe1\\xa8O\\x0e\\x7f\\xda`\\xd7\\xaf\\x9f\\xb9\\x09\\\n\\xdfc\\x05\\xff\\xe5uLe\\xf5\\xcc\\x1f\\x0d3,\\x83\\xb6\\\n\\x06D\\x83\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x01\\xdc\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x8eIDATh\\x81\\xed\\\n\\x9a\\xafN\\xc3P\\x14\\x87\\xbfn\\x1d\\x0a\\x1cA\\x1e\\x83\\x04\\\n\\xc4\\x0cjA\\x10\\x04\\x82\\x80\\x9e\\xe7\\x05x\\x80!x\\x01\\\n^\\x00\\x8f\\xc2\\x00rA\\x90=\\xc2@1s\\xe42\\x14\\\n\\xc1\\xecO\\x82h\\x1b\\xb6e\\xed(\\xebv\\xda\\xe5~\\xae\\\n\\xf7\\x5c\\xf1\\xfb\\xda{o\\x9a\\xdc\\xe3\\x11\\xa2\\xaa\\xdb\\xc05\\\nP\\x03\\xf6\\x81\\x0a\\xf9b\\x00\\xb4\\x81\\x16p#\\x22=\\x00\\\n\\x0f@U\\x8f\\x81{`\\xc7,^:\\xba@]D^\\\n\\xbc\\xf0\\xcd\\xbfQ\\x9c\\xf0\\x11]`\\xafD\\xb0l\\x8a\\x16\\\n\\x1e\\x82\\xcc\\x0d\\x9f`\\xcdO3Zq\\x98\\xbfR\\x9ez\\\n\\xae\\xf9\\x04\\x1bv\\x9c\\x91\\x88\\xf8+\\x0a\\x94\\x0aU\\x1d2\\\n)qP\\x22\\x7f\\xa7M\\x1a*%\\xeb\\x04\\x8b\\xe2\\x04\\xac\\\nq\\x02\\xd68\\x01k\\x9c\\x805N\\xc0\\x1a'`\\xcd\\xdc\\\n\\xdffU=\\x01\\x0e\\x81\\xcd\\xe5\\xc7\\x99`\\x08\\xbc\\x03O\\\n\\x22\\xf2\\x1d7)V@U}\\xe0\\x018\\xcf>[*\\\n:\\xaaz*\\x22\\x1f\\xb3\\x8aIK\\xe8\\x0a\\xfb\\xf0\\x00\\xbb\\\n\\xc0]\\x5c1I\\xe0,\\xfb,\\xff\\xe6HU\\xb7f\\x15\\\n\\x0a\\xbf\\x89\\x93\\x04\\x9eW\\x96b>\\xaf\\x22\\xf25\\xab\\x90\\\n$p\\x0b<.'O*:\\xc0e\\x5c1\\xf6\\x14\\x12\\\n\\x91!pQ\\xd8c4BD\\x9a@3\\xc3`\\x99\\xb2\\\n\\xd6\\x9b\\xb8\\x108\\x01k\\x9c\\x805N\\xc0\\x1a'`\\x8d\\\n\\x13\\xb0f-\\x04\\x06\\xd6!\\x16\\xa0\\xef\\x13\\x5c\\xdfW\\xc7\\\n\\x06\\xcb\\xe1m`\\x1e\\x99\\xbefm\\xfb\\x04\\xbd\\x07\\xd59\\\n\\x13\\xf3J\\xab\\xf8\\xad\\x06a\\xd7G=\\x1c(\\x0aQ\\xb3\\\nG\\xcf\\x8bF\\xc2/\\xd1\\xe0\\xb7\\xddf\\xc3(\\x5c\\x1c}\\\n&\\xdbm>\\x01~\\x00%\\xf8ZCUN:\\x7f\\x00\\\n\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x05~\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x08\\x06\\x00\\x00\\x00\\x1f\\x15\\xc4\\x89\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x05\\x17iTXtXML\\\n:com.adobe.xmp\\x00\\x00\\\n\\x00\\x00\\x00<?xpacket beg\\\nin=\\x22\\xef\\xbb\\xbf\\x22 id=\\x22W5M\\\n0MpCehiHzreSzNTc\\\nzkc9d\\x22?> <x:xmpm\\\neta xmlns:x=\\x22ado\\\nbe:ns:meta/\\x22 x:x\\\nmptk=\\x22Adobe XMP \\\nCore 7.1-c000 79\\\n.7a7a236, 2021/0\\\n8/12-00:25:20   \\\n     \\x22> <rdf:RDF\\\n xmlns:rdf=\\x22http\\\n://www.w3.org/19\\\n99/02/22-rdf-syn\\\ntax-ns#\\x22> <rdf:D\\\nescription rdf:a\\\nbout=\\x22\\x22 xmlns:xm\\\np=\\x22http://ns.ado\\\nbe.com/xap/1.0/\\x22\\\n xmlns:dc=\\x22http:\\\n//purl.org/dc/el\\\nements/1.1/\\x22 xml\\\nns:xmpMM=\\x22http:/\\\n/ns.adobe.com/xa\\\np/1.0/mm/\\x22 xmlns\\\n:stEvt=\\x22http://n\\\ns.adobe.com/xap/\\\n1.0/sType/Resour\\\nceEvent#\\x22 xmlns:\\\nphotoshop=\\x22http:\\\n//ns.adobe.com/p\\\nhotoshop/1.0/\\x22 x\\\nmp:CreatorTool=\\x22\\\nAdobe Photoshop \\\n22.5 (Windows)\\x22 \\\nxmp:CreateDate=\\x22\\\n2021-11-10T17:39\\\n:02+01:00\\x22 xmp:M\\\netadataDate=\\x22202\\\n1-11-10T17:39:02\\\n+01:00\\x22 xmp:Modi\\\nfyDate=\\x222021-11-\\\n10T17:39:02+01:0\\\n0\\x22 dc:format=\\x22im\\\nage/png\\x22 xmpMM:I\\\nnstanceID=\\x22xmp.i\\\nid:f17ebb23-e62a\\\n-3946-a975-d4f6a\\\nb44d409\\x22 xmpMM:D\\\nocumentID=\\x22xmp.d\\\nid:f17ebb23-e62a\\\n-3946-a975-d4f6a\\\nb44d409\\x22 xmpMM:O\\\nriginalDocumentI\\\nD=\\x22xmp.did:f17eb\\\nb23-e62a-3946-a9\\\n75-d4f6ab44d409\\x22\\\n photoshop:Color\\\nMode=\\x223\\x22 photosh\\\nop:ICCProfile=\\x22s\\\nRGB IEC61966-2.1\\\n\\x22> <xmpMM:Histor\\\ny> <rdf:Seq> <rd\\\nf:li stEvt:actio\\\nn=\\x22created\\x22 stEv\\\nt:instanceID=\\x22xm\\\np.iid:f17ebb23-e\\\n62a-3946-a975-d4\\\nf6ab44d409\\x22 stEv\\\nt:when=\\x222021-11-\\\n10T17:39:02+01:0\\\n0\\x22 stEvt:softwar\\\neAgent=\\x22Adobe Ph\\\notoshop 22.5 (Wi\\\nndows)\\x22/> </rdf:\\\nSeq> </xmpMM:His\\\ntory> </rdf:Desc\\\nription> </rdf:R\\\nDF> </x:xmpmeta>\\\n <?xpacket end=\\x22\\\nr\\x22?>\\x07b\\x0c\\x81\\x00\\x00\\x00\\x0dIDAT\\\n\\x08\\x1dc\\xf8\\xff\\xff?\\x03\\x00\\x08\\xfc\\x02\\xfe\\xe6\\x0c\\xff\\\n\\xab\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\x9e\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x08\\x15\\x0f\\xfd\\\n\\x8f\\xf8.\\x00\\x00\\x00\\x22IDAT\\x08\\xd7c`\\xc0\\\n\\x0d\\xfe\\x9f\\x87\\xb1\\x18\\x91\\x05\\x18\\x0d\\xe1BH*\\x0c\\x19\\\n\\x18\\x18\\x91\\x05\\x10*\\xd1\\x00\\x00\\xca\\xb5\\x07\\xd2v\\xbb\\xb2\\\n\\xc5\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x01\\xe1\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x93IDATh\\x81\\xed\\\n\\x9a;N\\xc3@\\x10\\x86\\xbfq\\x1c*\\xe8\\x10\\xe56\\x94\\\n\\xd0\\xa4\\xa1\\x8a(\\x22\\x0a\\x0aD\\x9f\\x9e\\x0bp\\x80Pp\\\n\\x01.\\xc0\\x15h\\x80\\x13\\xa0\\x1c!P\\x91f\\xbbD\\xa1\\\nB4yh(l\\x1e\\xb1\\xfcH\\x08\\xc9\\xda\\xd2~\\x9d\\\nw\\x5c\\xfc\\x9f\\xb3\\x1e9\\xda\\x11bTu\\x17\\xb8\\x02\\x9a\\\n\\xc0!P\\xa7\\x5cL\\x80\\x1e\\xd0\\x05\\xaeEd\\xf4]Q\\\n\\xd5\\x96\\xaa\\x0e\\xb4:\\x0cT\\xb5\\x05 \\x1a=\\xf9g`\\\n\\xcf\\xc5c]\\x81!p\\x10\\x10m\\x9b\\xaa\\x85\\x87(s\\\n'$\\xda\\xf3If\\x1b\\x0e\\xb3(\\xb5\\xc4uSTu\\\n\\xcc\\xfc\\x0b;\\x13\\x91p\\x83\\xa1\\x16FU\\xa7\\xccKL\\\n\\x02\\xca\\xd7m\\x96\\xa1\\x1e\\xb8N\\xb0*^\\xc05^\\xc0\\\n5^\\xc05^\\xc05^\\xc05^\\xc05\\x85\\x9f\\xcd\\\n\\xd6\\xda\\x13\\xe0\\x08\\xd8^\\x7f\\x9c9\\xa6\\xc0\\x0b\\xf0`\\x8c\\\n\\xf9\\xc8\\xbaITU\\x13k3\\x11\\x09\\xad\\xb5!p\\x07\\\n\\x9c\\xaf1\\xe4\\x22\\xf4\\x81Sc\\xcck\\xca\\xff\\x81\\xdc-\\\nt\\x89\\xfb\\xf0\\x00\\xfb\\xc0mV1O\\xe0\\xec\\xff\\xb3\\xfc\\\n\\x99ck\\xedNZ\\xa1\\xf2/q\\x9e\\xc0\\xe3\\xc6R\\x14\\\n\\xf3d\\x8cyO+\\xe4\\x09\\xdc\\x00\\xf7\\xeb\\xc9\\xb3\\x14}\\\n\\xe0\\x22\\xab\\x98\\xd9\\x85\\xbe.\\xca\\xd4F\\xd3\\xbaP\\xa1@\\\n\\x99X\\xb6\\x8dV\\x02/\\xe0\\x1a/\\xe0\\x1a/\\xe0\\x1a/\\\n\\xe0\\x1a/\\xe0\\x1a/\\xe0\\x9a\\x80\\xe8\\x04\\xbc\\xaa\\x8cC\\xa2\\\n\\xe3\\xfb\\xc6\\xaf\\xc5Z\\xfc\\xd9ZF\\x92\\xc7\\xac\\xbd\\x90h\\\n\\xf6\\xa0QpcY\\xe9V\\x7f\\xd4 \\x9e\\xfah\\xc7\\x0b\\\nUa\\x08\\xb4Ed$_+\\xf1/\\xd1\\xe1g\\xdcf\\\n\\xcbQ\\xb8,\\xc6\\xcc\\x8f\\xdb\\xbc\\x01|\\x02mw#\\xb3\\\n\\xd4\\x95Sv\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\\n\\x00\\x00\\x00\\xa5\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\x9cS4\\xfc]\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x0b\\x02\\x04m\\\n\\x98\\x1bi\\x00\\x00\\x00)IDAT\\x08\\xd7c`\\xc0\\\n\\x00\\x8c\\x0c\\x0c\\xff\\xcf\\xa3\\x08\\x18220 \\x0b2\\x1a\\\n200B\\x98\\x10AFC\\x14\\x13P\\xb5\\xa3\\x01\\x00\\\n\\xd6\\x10\\x07\\xd2/H\\xdfJ\\x00\\x00\\x00\\x00IEND\\\n\\xaeB`\\x82\\\n\\x00\\x00\\x04\\x12\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x03\\xc4IDATh\\x81\\xed\\\n\\x9a_\\x88\\x94U\\x18\\xc6\\x7f3;\\x1a\\x0b\\x19\\x15f\\x17\\\n\\xca\\x03IPM\\x09J7Q^D)&f\\x05[\\\n\\xb9\\xd2\\x82\\xb1P\\x17\\x91$t\\x11\\x08z\\xa1D\\x17]\\\nT\\x94\\x04\\xd6E\\xe1\\xa2\\x15L\\xb1\\x18\\x99\\xfd%\\xd8\\x08\\\n\\x82nj]\\xa4\\x22\\xe2\\x81\\xa8\\x96\\xd5(\\xe8\\x9f\\xae\\xd5\\\n\\xc5\\xf9\\xb6\\xc6\\xd9\\xf9\\xbesf\\xc6vf\\xc0\\xdf\\xdd\\x9c\\\n\\xef=\\xefy\\x9fs\\xe6\\x9c\\xf3~\\xdf9%2Fj\\\nSK\\x81\\xdd\\xc0Z\\xe0:`\\x11\\xbd\\xc5i`\\x12\\x98\\\n\\x00\\xf6\\x8c\\x0dUg\\x00J\\x00#\\xb5\\xa9[\\x80C\\xc0\\\n\\xb2\\xae\\x85\\xd7\\x1a\\xd3\\xc0\\xd6\\xb1\\xa1\\xea\\x07\\xa5\\xac\\xe7\\x8f\\\n\\xd1?\\xc1\\xcf1\\x0d\\x5c[&\\xfcm\\xfa-x\\x081\\\n\\xef\\xaa\\x10\\xfe\\xf3\\x8d\\x9cY\\xe0`R\\x19h\\xf8\\xbd\\xb6\\\nB\\x98\\xb0\\xf5\\x9c\\x19\\x1b\\xaaV\\x16(\\xa0\\x96\\x18\\xa9M\\\n\\xcdr\\xb6\\x88Uezo\\xb5i\\x85E\\xe5nG\\xd0\\\n)\\xe7\\x05t\\x9b\\xf3\\x02\\xfe\\x0fl\\xdfc\\xfb\\x90\\xed\\xf5\\\n1\\xdb\\x9e\\x13`\\xfb\\x11\\xe05`\\x188j{\\x9f\\xed\\\n\\xdc\\x95\\xb2\\xa7\\x04\\xd8\\x1e\\x06\\x9e\\xaa+*\\x01\\x0f\\x01/\\\n\\xe4\\xd5\\xe9\\x19\\x01\\xb6\\xd7\\x01/\\x93%\\x98\\x0dl\\xb3\\xfd\\\nh\\xb3z=!\\xc0\\xf6\\xf5\\xc0\\xeb\\xc0\\xe2\\x02\\xb3\\x8d\\xcd\\\n\\x0a\\xbb.\\xc0\\xf6\\x95\\xc0[\\xc0\\x92\\x88\\xe9\\x8b\\xcd\\x0a\\xbb\\\n*\\xc0\\xf6\\xe5\\xc0Q\\xe2\\xd9\\xf0\\x93\\x92^i\\xf6\\xa0k\\\n\\x02l/\\x01\\x8e\\x00+#\\xa6\\x07\\x80\\xc7\\xf2\\x1evE\\\n\\x80\\xed\\xc5\\xc0\\x1b\\xc0\\x9a\\x88\\xe9\\xdb\\xc0\\xa8\\xa4\\xbf\\xf3\\x0c\\\n\\x16\\x5c\\x80\\xed2\\xa1Wo\\x8d\\x98~\\x0a\\xdc-i\\xb6\\\n\\xc8\\xa8\\x1b#\\xf04po\\xc4\\xe6K`\\x93\\xa4_c\\\n\\xce\\x92\\x04\\xd8.e=\\xd7\\x11\\xb6w\\x02\\xdb#f\\xdf\\\n\\x03\\x1b$\\xcd\\xa4\\xf8,\\x0c*\\x0b|\\x07\\xf0;0i\\\n{UR\\xa4\\xcd}\\x8d\\x02\\x8fG\\xcc~\\x06n\\x93\\xf4\\\nm\\xaa\\xdf\\x5c\\x01Y\\xfe1N\\xd8\\xda/\\x00\\xae\\x01\\xde\\\n\\xb3}U\\xaa\\xf3:_\\x9b\\x81\\xfd\\x11\\xb3?\\x81;%\\\n}\\xde\\x8a\\xef\\xa2\\x11x\\x0e\\xd8\\xdcP\\xb6\\x0cx\\xdf\\xf6\\\n\\x15\\xa9\\x0d\\xd8\\xbe\\x11x\\x95\\xf9/\\xe4\\xf5\\xfc\\x05\\xdc'\\\n\\xe9\\xa3T\\xbfs4\\x15\\x90\\xf5\\xd8\\x839u\\x96\\x13F\\\nby\\xcc\\xb9\\xed*p\\x18\\x18\\x8c\\x98>,\\xa9\\x16\\xf3\\\n\\xd7\\x8c\\xbc\\x11\\xb84Ro%A\\xc4ey\\x06\\xb6W\\\n\\x10v\\xd9\\x98\\xaf\\xbd\\x92\\x9e\\x8f\\xd8\\xe4\\x92'\\xe0 a\\\n\\x1d.\\xe2j\\xe0\\x1d\\xdb\\x177>\\xb0}\\x09!\\xf8\\x15\\\n\\x11\\x1f\\xfb%\\xed\\x8eFY@S\\x01\\x92N\\x13\\xb2\\xbf\\\n/\\x22\\xf5W\\x03Gl_8W`{\\x90\\xf0\\xb7\\xa9\\\nF\\xea\\x8e\\x13r\\xfd\\x8e\\xc8\\x9d\\xc4\\x92N\\x02\\xeb\\x81\\xaf\\\n\\x22>n\\x00\\x0e\\xdb\\x1e\\xb4=@\\x98\\xb07E\\xeaL\\\n\\x00\\xc3\\x92:\\xfe\\x02X\\xb8\\x0fH\\xfa\\x11X\\x078\\xe2\\\n\\xe7f\\xa0FX*\\x1bW\\xaeF&\\x81;$\\xfd\\x91\\\n\\x18c!\\xd1\\xddU\\x92\\x09y\\xcb\\x0f\\x11\\xd3\\x8d\\xc0h\\\n\\xc4\\xc6\\x84\\x8d\\xea\\xa7\\xb4\\xf0\\xe2$\\xa5\\x07\\x92\\xbe&\\x8c\\\n\\xc4\\x89\\x0e\\xda:IH\\x11\\xbe\\xeb\\xc0\\xc7<\\x92\\xf3\\x1b\\\nI\\xc7\\x80\\x0d\\xc0/m\\xb4\\xf3\\x1bp\\xbb\\xa4\\xe3m\\xd4\\\n-\\xa4\\xa5\\x04M\\xd2g\\xc0\\xa6,\\xa0Tf\\x81-\\x92\\\n>i\\xa5\\xadTZ\\xce0%M\\x00w\\x11r\\x97\\x14\\\n\\x1e\\x90\\xf4f\\xab\\xed\\xa4\\xd2V\\x8a,\\xe9]`\\x0b\\xa1\\\nw\\x8b\\xd8)\\xe9\\xa5v\\xdaH\\xa5\\xed\\x1c_\\xd28\\xb0\\\n\\x8d\\x90\\x885\\xe3YIO\\xb4\\xeb?\\x95\\x8e^R$\\\n\\x1d\\x04\\xeeg\\xfe\\x9c\\xd8\\x07\\xec\\xe8\\xc4w*\\x1d\\x1f%\\\nI:`\\xfbc\\xc2\\x06v\\x11\\xf0a6O\\x16\\x84s\\\nr\\x16&\\xe9\\x1b\\xe0\\x99s\\xe1\\xabU\\xca\\x84\\x13\\xf0~\\\n\\xe5T\\x85\\x90\\x9b\\xd4\\x7f\\x9f\\x19\\xc8N\\x03{\\x91\\xc6\\xb7\\\n\\xba\\xc9\\x0a!3l\\xfc\\xc0T\\xf4\\xfa\\xd7KL\\x94\\x81\\\n=\\x84c\\xfb~c\\x1a\\xd8[\\xcen}l\\xa5\\xbfD\\\n\\xcc]\\xf6\\x98\\xf9\\xf70!\\xbb\\xf4\\xb1\\x8b\\xff\\xae\\xdb\\x14\\\n}\\xab\\xef\\x06\\xa78\\xfb\\xba\\xcd\\x09\\x80\\x7f\\x00\\xc4\\x1e\\x10\\\n)3[\\x85\\xf7\\x00\\x00\\x00\\x00IEND\\xaeB`\\\n\\x82\\\n\"\n\nqt_resource_name = b\"\\\n\\x00\\x08\\\n\\x06\\xc5\\x8e\\xa5\\\n\\x00o\\\n\\x00p\\x00e\\x00n\\x00p\\x00y\\x00p\\x00e\\\n\\x00\\x06\\\n\\x07\\x03}\\xc3\\\n\\x00i\\\n\\x00m\\x00a\\x00g\\x00e\\x00s\\\n\\x00\\x0e\\\n\\x04\\xa2\\xfc\\xa7\\\n\\x00d\\\n\\x00o\\x00w\\x00n\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x11\\\n\\x0b\\xda0\\xa7\\\n\\x00b\\\n\\x00r\\x00a\\x00n\\x00c\\x00h\\x00_\\x00c\\x00l\\x00o\\x00s\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\\n\\x00\\x1d\\\n\\x09\\x07\\x81\\x07\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\x00d\\x00_\\\n\\x00d\\x00i\\x00s\\x00a\\x00b\\x00l\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x1c\\\n\\x08?\\xdag\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00u\\x00n\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\\n\\x00d\\x00_\\x00f\\x00o\\x00c\\x00u\\x00s\\x00.\\x00p\\x00n\\x00g\\\n\\x00#\\\n\\x06\\xf2\\x1aG\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00i\\x00n\\x00d\\x00e\\x00t\\x00e\\x00r\\x00m\\\n\\x00i\\x00n\\x00a\\x00t\\x00e\\x00_\\x00d\\x00i\\x00s\\x00a\\x00b\\x00l\\x00e\\x00d\\x00.\\x00p\\\n\\x00n\\x00g\\\n\\x00\\x12\\\n\\x01.\\x03'\\\n\\x00c\\\n\\x00o\\x00m\\x00b\\x00o\\x00b\\x00o\\x00x\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00.\\x00p\\x00n\\\n\\x00g\\\n\\x00\\x1c\\\n\\x0e<\\xde\\x07\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00u\\x00n\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\\n\\x00d\\x00_\\x00h\\x00o\\x00v\\x00e\\x00r\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x14\\\n\\x07\\xec\\xd1\\xc7\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\x00d\\x00.\\\n\\x00p\\x00n\\x00g\\\n\\x00\\x1a\\\n\\x05\\x11\\xe0\\xe7\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\x00d\\x00_\\\n\\x00f\\x00o\\x00c\\x00u\\x00s\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x18\\\n\\x03\\x8e\\xdeg\\\n\\x00r\\\n\\x00i\\x00g\\x00h\\x00t\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00d\\x00i\\x00s\\x00a\\x00b\\\n\\x00l\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x15\\\n\\x03'rg\\\n\\x00c\\\n\\x00o\\x00m\\x00b\\x00o\\x00b\\x00o\\x00x\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00o\\x00n\\\n\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x12\\\n\\x03\\x8d\\x04G\\\n\\x00r\\\n\\x00i\\x00g\\x00h\\x00t\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00o\\x00n\\x00.\\x00p\\x00n\\\n\\x00g\\\n\\x00\\x16\\\n\\x01u\\xcc\\x87\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00u\\x00n\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\\n\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x17\\\n\\x0c\\xabQ\\x07\\\n\\x00d\\\n\\x00o\\x00w\\x00n\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00d\\x00i\\x00s\\x00a\\x00b\\x00l\\\n\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x15\\\n\\x0f\\xf3\\xc0\\x07\\\n\\x00u\\\n\\x00p\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00d\\x00i\\x00s\\x00a\\x00b\\x00l\\x00e\\x00d\\\n\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x14\\\n\\x04^-\\xa7\\\n\\x00b\\\n\\x00r\\x00a\\x00n\\x00c\\x00h\\x00_\\x00c\\x00l\\x00o\\x00s\\x00e\\x00d\\x00_\\x00o\\x00n\\x00.\\\n\\x00p\\x00n\\x00g\\\n\\x00\\x0f\\\n\\x06S%\\xa7\\\n\\x00b\\\n\\x00r\\x00a\\x00n\\x00c\\x00h\\x00_\\x00o\\x00p\\x00e\\x00n\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x17\\\n\\x0ce\\xce\\x07\\\n\\x00l\\\n\\x00e\\x00f\\x00t\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00d\\x00i\\x00s\\x00a\\x00b\\x00l\\\n\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x0e\\\n\\x0e\\xde\\xfa\\xc7\\\n\\x00l\\\n\\x00e\\x00f\\x00t\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x1f\\\n\\x0a\\xae'G\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00u\\x00n\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\\n\\x00d\\x00_\\x00d\\x00i\\x00s\\x00a\\x00b\\x00l\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x11\\\n\\x00\\xb8\\x8c\\x07\\\n\\x00l\\\n\\x00e\\x00f\\x00t\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00o\\x00n\\x00.\\x00p\\x00n\\x00g\\\n\\\n\\x00\\x0f\\\n\\x02\\x9f\\x05\\x87\\\n\\x00r\\\n\\x00i\\x00g\\x00h\\x00t\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x1b\\\n\\x03Z2'\\\n\\x00c\\\n\\x00o\\x00m\\x00b\\x00o\\x00b\\x00o\\x00x\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00d\\x00i\\\n\\x00s\\x00a\\x00b\\x00l\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x0f\\\n\\x01s\\x8b\\x07\\\n\\x00u\\\n\\x00p\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00o\\x00n\\x00.\\x00p\\x00n\\x00g\\\n\\x00 \\\n\\x0f\\xd4\\x1b\\xc7\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00i\\x00n\\x00d\\x00e\\x00t\\x00e\\x00r\\x00m\\\n\\x00i\\x00n\\x00a\\x00t\\x00e\\x00_\\x00h\\x00o\\x00v\\x00e\\x00r\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x12\\\n\\x05\\x8f\\x9d\\x07\\\n\\x00b\\\n\\x00r\\x00a\\x00n\\x00c\\x00h\\x00_\\x00o\\x00p\\x00e\\x00n\\x00_\\x00o\\x00n\\x00.\\x00p\\x00n\\\n\\x00g\\\n\\x00\\x1a\\\n\\x01\\x87\\xaeg\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00i\\x00n\\x00d\\x00e\\x00t\\x00e\\x00r\\x00m\\\n\\x00i\\x00n\\x00a\\x00t\\x00e\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x0f\\\n\\x0c\\xe2hg\\\n\\x00t\\\n\\x00r\\x00a\\x00n\\x00s\\x00p\\x00a\\x00r\\x00e\\x00n\\x00t\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x0c\\\n\\x06\\xe6\\xe6g\\\n\\x00u\\\n\\x00p\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00.\\x00p\\x00n\\x00g\\\n\\x00 \\\n\\x09\\xd7\\x1f\\xa7\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00i\\x00n\\x00d\\x00e\\x00t\\x00e\\x00r\\x00m\\\n\\x00i\\x00n\\x00a\\x00t\\x00e\\x00_\\x00f\\x00o\\x00c\\x00u\\x00s\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x11\\\n\\x01\\x1f\\xc3\\x87\\\n\\x00d\\\n\\x00o\\x00w\\x00n\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00o\\x00n\\x00.\\x00p\\x00n\\x00g\\\n\\\n\\x00\\x1a\\\n\\x03\\x0e\\xe4\\x87\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\x00d\\x00_\\\n\\x00h\\x00o\\x00v\\x00e\\x00r\\x00.\\x00p\\x00n\\x00g\\\n\"\n\nqt_resource_struct = b\"\\\n\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\\n\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x02\\\n\\x00\\x00\\x00\\x16\\x00\\x02\\x00\\x00\\x00 \\x00\\x00\\x00\\x03\\\n\\x00\\x00\\x04\\x1e\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x000\\x5c\\\n\\x00\\x00\\x05\\xfc\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00F\\x05\\\n\\x00\\x00\\x01<\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x0f\\xec\\\n\\x00\\x00\\x04\\xa6\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x002S\\\n\\x00\\x00\\x02\\x9c\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1b\\xf7\\\n\\x00\\x00\\x05:\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00<\\x1c\\\n\\x00\\x00\\x04F\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x001\\x06\\\n\\x00\\x00\\x06$\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00F\\xae\\\n\\x00\\x00\\x02B\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1a\\xa9\\\n\\x00\\x00\\x04j\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x001\\xaa\\\n\\x00\\x00\\x02r\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1bS\\\n\\x00\\x00\\x02\\x0c\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1a\\x05\\\n\\x00\\x00\\x032\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1e\\xa3\\\n\\x00\\x00\\x00(\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\\n\\x00\\x00\\x01\\xd2\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x16\\x02\\\n\\x00\\x00\\x05\\x10\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x004\\xe8\\\n\\x00\\x00\\x03`\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00&\\x84\\\n\\x00\\x00\\x05\\x98\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00C~\\\n\\x00\\x00\\x00\\xf0\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x0d\\xec\\\n\\x00\\x00\\x01\\xa4\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x12\\x03\\\n\\x00\\x00\\x00\\xb2\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x0c\\x91\\\n\\x00\\x00\\x00r\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x08Z\\\n\\x00\\x00\\x05\\xb6\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00D \\\n\\x00\\x00\\x03\\xda\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00.\\xe2\\\n\\x00\\x00\\x00J\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\xa9\\\n\\x00\\x00\\x03\\x84\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00-\\x8e\\\n\\x00\\x00\\x02\\xce\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1dV\\\n\\x00\\x00\\x05t\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00=\\xfc\\\n\\x00\\x00\\x01f\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x10\\x96\\\n\\x00\\x00\\x03\\xb8\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00.8\\\n\\x00\\x00\\x04\\xca\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x002\\xf5\\\n\\x00\\x00\\x03\\x02\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1e\\x00\\\n\"\n\n\ndef qInitResources():\n    QtCore.qRegisterResourceData(\n        0x01, qt_resource_struct, qt_resource_name, qt_resource_data\n    )\n\n\ndef qCleanupResources():\n    QtCore.qUnregisterResourceData(\n        0x01, qt_resource_struct, qt_resource_name, qt_resource_data\n    )\n"
  },
  {
    "path": "openpype/style/pyside6_resources.py",
    "content": "# Resource object code (Python 3)\n# Created by: object code\n# Created by: The Resource Compiler for Qt version 6.4.1\n# WARNING! All changes made in this file will be lost!\n\nfrom PySide6 import QtCore\n\nqt_resource_data = b\"\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x14\\x1f \\xb9\\\n\\x8dw\\xe9\\x00\\x00\\x00*IDAT\\x08\\xd7c`\\xc0\\\n\\x06\\xe6|```B0\\xa1\\x1c\\x08\\x93\\x81\\x81\\x09\\xc1\\\nd``b`H\\x11@\\xe2 s\\x19\\x90\\x8d@\\x02\\\n\\x00#\\xed\\x08\\xafd\\x9f\\x0f\\x15\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x04\\x12\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x03\\xc4IDATh\\x81\\xed\\\n\\x9a_\\x88\\x94U\\x18\\xc6\\x7f3;\\x1a\\x0b\\x19\\x15f\\x17\\\n\\xca\\x03IPM\\x09J7Q^D)&f\\x05[\\\n\\xb9\\xd2\\x82\\xb1P\\x17\\x91$t\\x11\\x08z\\xa1D\\x17]\\\nT\\x94\\x04\\xd6E\\xe1\\xa2\\x15L\\xb1\\x18\\x99\\xfd%\\xd8\\x08\\\n\\x82nj]\\xa4\\x22\\xe2\\x81\\xa8\\x96\\xd5(\\xe8\\x9f\\xae\\xd5\\\n\\xc5\\xf9\\xb6\\xc6\\xd9\\xf9\\xbesf\\xc6vf\\xc0\\xdf\\xdd\\x9c\\\n\\xef=\\xefy\\x9fs\\xe6\\x9c\\xf3~\\xdf9%2Fj\\\nSK\\x81\\xdd\\xc0Z\\xe0:`\\x11\\xbd\\xc5i`\\x12\\x98\\\n\\x00\\xf6\\x8c\\x0dUg\\x00J\\x00#\\xb5\\xa9[\\x80C\\xc0\\\n\\xb2\\xae\\x85\\xd7\\x1a\\xd3\\xc0\\xd6\\xb1\\xa1\\xea\\x07\\xa5\\xac\\xe7\\x8f\\\n\\xd1?\\xc1\\xcf1\\x0d\\x5c[&\\xfcm\\xfa-x\\x081\\\n\\xef\\xaa\\x10\\xfe\\xf3\\x8d\\x9cY\\xe0`R\\x19h\\xf8\\xbd\\xb6\\\nB\\x98\\xb0\\xf5\\x9c\\x19\\x1b\\xaaV\\x16(\\xa0\\x96\\x18\\xa9M\\\n\\xcdr\\xb6\\x88Uezo\\xb5i\\x85E\\xe5nG\\xd0\\\n)\\xe7\\x05t\\x9b\\xf3\\x02\\xfe\\x0fl\\xdfc\\xfb\\x90\\xed\\xf5\\\n1\\xdb\\x9e\\x13`\\xfb\\x11\\xe05`\\x188j{\\x9f\\xed\\\n\\xdc\\x95\\xb2\\xa7\\x04\\xd8\\x1e\\x06\\x9e\\xaa+*\\x01\\x0f\\x01/\\\n\\xe4\\xd5\\xe9\\x19\\x01\\xb6\\xd7\\x01/\\x93%\\x98\\x0dl\\xb3\\xfd\\\nh\\xb3z=!\\xc0\\xf6\\xf5\\xc0\\xeb\\xc0\\xe2\\x02\\xb3\\x8d\\xcd\\\n\\x0a\\xbb.\\xc0\\xf6\\x95\\xc0[\\xc0\\x92\\x88\\xe9\\x8b\\xcd\\x0a\\xbb\\\n*\\xc0\\xf6\\xe5\\xc0Q\\xe2\\xd9\\xf0\\x93\\x92^i\\xf6\\xa0k\\\n\\x02l/\\x01\\x8e\\x00+#\\xa6\\x07\\x80\\xc7\\xf2\\x1evE\\\n\\x80\\xed\\xc5\\xc0\\x1b\\xc0\\x9a\\x88\\xe9\\xdb\\xc0\\xa8\\xa4\\xbf\\xf3\\x0c\\\n\\x16\\x5c\\x80\\xed2\\xa1Wo\\x8d\\x98~\\x0a\\xdc-i\\xb6\\\n\\xc8\\xa8\\x1b#\\xf04po\\xc4\\xe6K`\\x93\\xa4_c\\\n\\xce\\x92\\x04\\xd8.e=\\xd7\\x11\\xb6w\\x02\\xdb#f\\xdf\\\n\\x03\\x1b$\\xcd\\xa4\\xf8,\\x0c*\\x0b|\\x07\\xf0;0i\\\n{UR\\xa4\\xcd}\\x8d\\x02\\x8fG\\xcc~\\x06n\\x93\\xf4\\\nm\\xaa\\xdf\\x5c\\x01Y\\xfe1N\\xd8\\xda/\\x00\\xae\\x01\\xde\\\n\\xb3}U\\xaa\\xf3:_\\x9b\\x81\\xfd\\x11\\xb3?\\x81;%\\\n}\\xde\\x8a\\xef\\xa2\\x11x\\x0e\\xd8\\xdcP\\xb6\\x0cx\\xdf\\xf6\\\n\\x15\\xa9\\x0d\\xd8\\xbe\\x11x\\x95\\xf9/\\xe4\\xf5\\xfc\\x05\\xdc'\\\n\\xe9\\xa3T\\xbfs4\\x15\\x90\\xf5\\xd8\\x839u\\x96\\x13F\\\nby\\xcc\\xb9\\xed*p\\x18\\x18\\x8c\\x98>,\\xa9\\x16\\xf3\\\n\\xd7\\x8c\\xbc\\x11\\xb84Ro%A\\xc4ey\\x06\\xb6W\\\n\\x10v\\xd9\\x98\\xaf\\xbd\\x92\\x9e\\x8f\\xd8\\xe4\\x92'\\xe0 a\\\n\\x1d.\\xe2j\\xe0\\x1d\\xdb\\x177>\\xb0}\\x09!\\xf8\\x15\\\n\\x11\\x1f\\xfb%\\xed\\x8eFY@S\\x01\\x92N\\x13\\xb2\\xbf\\\n/\\x22\\xf5W\\x03Gl_8W`{\\x90\\xf0\\xb7\\xa9\\\nF\\xea\\x8e\\x13r\\xfd\\x8e\\xc8\\x9d\\xc4\\x92N\\x02\\xeb\\x81\\xaf\\\n\\x22>n\\x00\\x0e\\xdb\\x1e\\xb4=@\\x98\\xb07E\\xeaL\\\n\\x00\\xc3\\x92:\\xfe\\x02X\\xb8\\x0fH\\xfa\\x11X\\x078\\xe2\\\n\\xe7f\\xa0FX*\\x1bW\\xaeF&\\x81;$\\xfd\\x91\\\n\\x18c!\\xd1\\xddU\\x92\\x09y\\xcb\\x0f\\x11\\xd3\\x8d\\xc0h\\\n\\xc4\\xc6\\x84\\x8d\\xea\\xa7\\xb4\\xf0\\xe2$\\xa5\\x07\\x92\\xbe&\\x8c\\\n\\xc4\\x89\\x0e\\xda:IH\\x11\\xbe\\xeb\\xc0\\xc7<\\x92\\xf3\\x1b\\\nI\\xc7\\x80\\x0d\\xc0/m\\xb4\\xf3\\x1bp\\xbb\\xa4\\xe3m\\xd4\\\n-\\xa4\\xa5\\x04M\\xd2g\\xc0\\xa6,\\xa0Tf\\x81-\\x92\\\n>i\\xa5\\xadTZ\\xce0%M\\x00w\\x11r\\x97\\x14\\\n\\x1e\\x90\\xf4f\\xab\\xed\\xa4\\xd2V\\x8a,\\xe9]`\\x0b\\xa1\\\nw\\x8b\\xd8)\\xe9\\xa5v\\xdaH\\xa5\\xed\\x1c_\\xd28\\xb0\\\n\\x8d\\x90\\x885\\xe3YIO\\xb4\\xeb?\\x95\\x8e^R$\\\n\\x1d\\x04\\xeeg\\xfe\\x9c\\xd8\\x07\\xec\\xe8\\xc4w*\\x1d\\x1f%\\\nI:`\\xfbc\\xc2\\x06v\\x11\\xf0a6O\\x16\\x84s\\\nr\\x16&\\xe9\\x1b\\xe0\\x99s\\xe1\\xabU\\xca\\x84\\x13\\xf0~\\\n\\xe5T\\x85\\x90\\x9b\\xd4\\x7f\\x9f\\x19\\xc8N\\x03{\\x91\\xc6\\xb7\\\n\\xba\\xc9\\x0a!3l\\xfc\\xc0T\\xf4\\xfa\\xd7KL\\x94\\x81\\\n=\\x84c\\xfb~c\\x1a\\xd8[\\xcen}l\\xa5\\xbfD\\\n\\xcc]\\xf6\\x98\\xf9\\xf70!\\xbb\\xf4\\xb1\\x8b\\xff\\xae\\xdb\\x14\\\n}\\xab\\xef\\x06\\xa78\\xfb\\xba\\xcd\\x09\\x80\\x7f\\x00\\xc4\\x1e\\x10\\\n)3[\\x85\\xf7\\x00\\x00\\x00\\x00IEND\\xaeB`\\\n\\x82\\\n\\x00\\x00\\x01\\xef\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\xa1IDATh\\x81\\xed\\\n\\x9a\\xbfN\\xc2P\\x14\\x87\\xbf\\x96\\xe2\\xa4\\x9bq\\xbc\\x8b\\x1b\\\n\\xea\\xc0\\xe2D\\x1c\\x8c\\x83\\x83\\xd1\\x81\\x89\\x84\\xd1\\x17\\xf0\\x01\\\np\\xc0\\x07\\xf0\\x05\\x1cI\\x9c\\xba\\xa8#q0<\\x02v\\\n\\x92\\xe5\\x8e\\x04'\\xe3\\xc2\\x9f\\xc4\\xa1m\\x04B\\x8b\\x95\\xc2\\\n\\xa1\\xe4~[{\\xee\\xf0\\xfb\\x9a{o\\x9a\\x9cc\\x11P\\\nu\\xbd]\\xe0\\x16(\\x01\\x87@\\x9e\\xf5b\\x00\\xb4\\x81\\x16\\\nPo\\x94\\x0b=\\x00\\x0b\\xa0\\xeaz\\xa7\\xc0#\\xb0'\\x16\\\n/\\x19]\\xa0\\xd2(\\x17^\\xad\\xe0\\xcb\\xbf\\x93\\x9d\\xf0!\\\n]\\xe0\\xc0\\xc6\\xdf6Y\\x0b\\x0f~\\xe6\\x9a\\x83\\xbf\\xe7\\xa7\\\n\\x19\\xad8\\xcc_\\xc9M=\\x97\\x1c\\xfc\\x03;\\xce\\xa8Q\\\n.8+\\x0a\\x94\\x88\\xaa\\xeb\\x0d\\x99\\x948\\xb2Y\\xbf\\xdb\\\n&\\x09y[:\\xc1\\xa2\\x18\\x01i\\x8c\\x804F@\\x1a\\\n# \\x8d\\x11\\x90\\xc6\\x08H3\\xf7\\xb7Yk}\\x06\\x1c\\\n\\x03\\xdb\\xcb\\x8f3\\xc1\\x10\\xf0\\x80g\\xa5\\xd4w\\xd4\\xa2H\\\n\\x01\\xad\\xb5\\x03\\xb8\\xc0e\\xfa\\xd9\\x12\\xd1\\xd1Z\\x9f+\\xa5\\\n>f\\x15\\xe3\\xb6\\xd0\\x0d\\xf2\\xe1\\x01\\xf6\\x81\\x87\\xa8b\\x9c\\\n\\xc0E\\xfaY\\xfe\\xcd\\x89\\xd6zgV!\\xf3\\x878N\\\n\\xe0ee)\\xe6\\xf3\\xa6\\x94\\xfa\\x9aU\\x88\\x13\\xb8\\x07\\x9e\\\n\\x96\\x93'\\x11\\x1d\\xe0:\\xaa\\x18y\\x0b)\\xa5\\x86\\xc0U\\\nf\\xaf\\xd1\\x10\\xa5T\\x13h\\xa6\\x18,U6\\xfa\\x10g\\\n\\x02# \\x8d\\x11\\x90\\xc6\\x08Hc\\x04\\xa41\\x02\\xd2l\\\n\\x84\\xc0@:\\xc4\\x02\\xf4\\x1d\\xfc\\xf6}q\\xece.\\xe8\\\n\\x06\\xae#\\xd3m\\xd6\\xb6\\x83?{P\\x9c\\xb3p]i\\\n\\xd9@\\x1d\\xbfm\\x9f5\\xba\\xc0\\x9d\\x1dL}T\\xc8\\x96\\\nD8\\xec\\xd1\\xb3\\xc27\\xc1\\xd0G\\x8d\\xdfq\\x9b-\\xa1\\\npQ\\xf4\\x99\\x1c\\xb7\\xf9\\x04\\xf8\\x01o\\xedXc-\\xfd\\\n\\xb2Y\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x08\\x15;\\xdc\\\n;\\x0c\\x9b\\x00\\x00\\x00*IDAT\\x08\\xd7c`\\xc0\\\n\\x00\\x8c\\x0c\\x0cs> \\x0b\\xa4\\x08020 \\x0b\\xa6\\\n\\x08000B\\x98\\x10\\xc1\\x14\\x01\\x14\\x13P\\xb5\\xa3\\x01\\\n\\x00\\xc6\\xb9\\x07\\x90]f\\x1f\\x83\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa5\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\x9cS4\\xfc]\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x0b\\x02\\x04m\\\n\\x98\\x1bi\\x00\\x00\\x00)IDAT\\x08\\xd7c`\\xc0\\\n\\x00\\x8c\\x0c\\x0c\\xff\\xcf\\xa3\\x08\\x18220 \\x0b2\\x1a\\\n200B\\x98\\x10AFC\\x14\\x13P\\xb5\\xa3\\x01\\x00\\\n\\xd6\\x10\\x07\\xd2/H\\xdfJ\\x00\\x00\\x00\\x00IEND\\\n\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa5\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\x9cS4\\xfc]\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x0b\\x02\\x04m\\\n\\x98\\x1bi\\x00\\x00\\x00)IDAT\\x08\\xd7c`\\xc0\\\n\\x00\\x8c\\x0c\\x0c\\xff\\xcf\\xa3\\x08\\x18220 \\x0b2\\x1a\\\n200B\\x98\\x10AFC\\x14\\x13P\\xb5\\xa3\\x01\\x00\\\n\\xd6\\x10\\x07\\xd2/H\\xdfJ\\x00\\x00\\x00\\x00IEND\\\n\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa5\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\x9cS4\\xfc]\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x0b\\x02\\x04m\\\n\\x98\\x1bi\\x00\\x00\\x00)IDAT\\x08\\xd7c`\\xc0\\\n\\x00\\x8c\\x0c\\x0c\\xff\\xcf\\xa3\\x08\\x18220 \\x0b2\\x1a\\\n200B\\x98\\x10AFC\\x14\\x13P\\xb5\\xa3\\x01\\x00\\\n\\xd6\\x10\\x07\\xd2/H\\xdfJ\\x00\\x00\\x00\\x00IEND\\\n\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa0\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x14\\x1c\\x1f$\\\n\\xc6\\x09\\x17\\x00\\x00\\x00$IDAT\\x08\\xd7c`@\\\n\\x05\\xff\\xcf\\xc3XL\\xc8\\x5c&dY&d\\xc5p\\x0e\\\n\\xa3!\\x9c\\xc3h\\x88a\\x1a\\x0a\\x00\\x00m\\x84\\x09u7\\\n\\x9e\\xd9#\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x08\\x15;\\xdc\\\n;\\x0c\\x9b\\x00\\x00\\x00*IDAT\\x08\\xd7c`\\xc0\\\n\\x00\\x8c\\x0c\\x0cs> \\x0b\\xa4\\x08020 \\x0b\\xa6\\\n\\x08000B\\x98\\x10\\xc1\\x14\\x01\\x14\\x13P\\xb5\\xa3\\x01\\\n\\x00\\xc6\\xb9\\x07\\x90]f\\x1f\\x83\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x01i\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x1bIDATh\\x81\\xed\\\n\\xda\\xb1m\\xc2@\\x14\\x87\\xf1\\xcf\\xc7\\x91\\x09P\\x86pB\\\nAO\\xc5\\x0a\\xae\\x90\\xbc\\x0a)\\xc8*\\x96Ry\\x05*\\\nF \\x1e\\xc2\\x82\\x05H\\x90R\\xdcY\\x01KQbE\\\n\\xe2\\xef\\x93\\xde\\xaf\\xb3E\\xf1>\\xcb\\xa6\\xb9\\x97\\x11\\x95u\\\n3\\x03^\\x80%\\xf0\\x0cL\\x19\\x97\\x0f\\xe0\\x00\\xec\\x81m\\\nU\\xe4G\\x80\\x0c\\xa0\\xac\\x9b\\x15\\xf0\\x06<\\xca\\xc6\\x1b\\xa6\\\n\\x05\\xd6U\\x91\\xef\\xb2\\xf8\\xe4\\xdfIg\\xf8N\\x0b<9\\\n\\xc2k\\x93\\xda\\xf0\\x10f\\xdex\\xc2;\\xdfw\\xb9\\xf30\\\n\\x7f5\\xe9]/=\\xe1\\x83\\xbdv\\xa9\\x8a\\xdc\\xdfi\\xa0\\\nA\\xca\\xba\\xf9\\xe46b\\xee\\x18\\xdf\\xbf\\xcd\\x10S\\xa7\\x9e\\\n\\xe0\\xbf,@\\xcd\\x02\\xd4,@\\xcd\\x02\\xd4,@\\xcd\\x02\\\n\\xd4,@\\xcd\\x02\\xd4,@\\xcd\\x02\\xd4,@\\xcd\\x02\\xd4\\\n,@\\xcd\\x02\\xd4,@\\xcd\\x02\\xd4,@\\xcd\\x11N\\xc0\\\nSu\\xf6\\x84\\xe3\\xfb\\xc5\\xd5\\xcdI<\\x0d\\x1c\\xa3\\xfe1\\\n\\xeb\\xc1\\x13v\\x0f\\x16\\xbf\\xfcp\\xac\\xf6\\x0e\\xd8\\x12\\x8e\\xed\\\nS\\xd3\\x02\\xaf.n}\\xacI+\\xa2[\\xf68f\\xdd\\\n\\x9d\\xb8\\xf4\\xb1\\xe1{\\xdd\\xe6A4\\xdcO\\xce\\xdc\\xae\\xdb\\\n\\x9c\\x00\\xbe\\x00\\x9f\\xf64>6O7\\x81\\x00\\x00\\x00\\x00\\\nIEND\\xaeB`\\x82\\\n\\x00\\x00\\x07\\x06\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x0a\\x00\\x00\\x00\\x07\\x08\\x06\\x00\\x00\\x001\\xac\\xdcc\\\n\\x00\\x00\\x04\\xb0iTXtXML:com.\\\nadobe.xmp\\x00\\x00\\x00\\x00\\x00<?\\\nxpacket begin=\\x22\\xef\\\n\\xbb\\xbf\\x22 id=\\x22W5M0MpCe\\\nhiHzreSzNTczkc9d\\\n\\x22?>\\x0a<x:xmpmeta x\\\nmlns:x=\\x22adobe:ns\\\n:meta/\\x22 x:xmptk=\\\n\\x22XMP Core 5.5.0\\x22\\\n>\\x0a <rdf:RDF xmln\\\ns:rdf=\\x22http://ww\\\nw.w3.org/1999/02\\\n/22-rdf-syntax-n\\\ns#\\x22>\\x0a  <rdf:Desc\\\nription rdf:abou\\\nt=\\x22\\x22\\x0a    xmlns:e\\\nxif=\\x22http://ns.a\\\ndobe.com/exif/1.\\\n0/\\x22\\x0a    xmlns:ti\\\nff=\\x22http://ns.ad\\\nobe.com/tiff/1.0\\\n/\\x22\\x0a    xmlns:pho\\\ntoshop=\\x22http://n\\\ns.adobe.com/phot\\\noshop/1.0/\\x22\\x0a    \\\nxmlns:xmp=\\x22http:\\\n//ns.adobe.com/x\\\nap/1.0/\\x22\\x0a    xml\\\nns:xmpMM=\\x22http:/\\\n/ns.adobe.com/xa\\\np/1.0/mm/\\x22\\x0a    x\\\nmlns:stEvt=\\x22http\\\n://ns.adobe.com/\\\nxap/1.0/sType/Re\\\nsourceEvent#\\x22\\x0a  \\\n exif:PixelXDime\\\nnsion=\\x2210\\x22\\x0a   ex\\\nif:PixelYDimensi\\\non=\\x227\\x22\\x0a   exif:C\\\nolorSpace=\\x221\\x22\\x0a  \\\n tiff:ImageWidth\\\n=\\x2210\\x22\\x0a   tiff:Im\\\nageLength=\\x227\\x22\\x0a  \\\n tiff:Resolution\\\nUnit=\\x222\\x22\\x0a   tiff\\\n:XResolution=\\x2272\\\n.0\\x22\\x0a   tiff:YRes\\\nolution=\\x2272.0\\x22\\x0a \\\n  photoshop:Colo\\\nrMode=\\x223\\x22\\x0a   pho\\\ntoshop:ICCProfil\\\ne=\\x22sRGB IEC61966\\\n-2.1\\x22\\x0a   xmp:Mod\\\nifyDate=\\x222021-05\\\n-31T12:30:11+02:\\\n00\\x22\\x0a   xmp:Metad\\\nataDate=\\x222021-05\\\n-31T12:30:11+02:\\\n00\\x22>\\x0a   <xmpMM:H\\\nistory>\\x0a    <rdf\\\n:Seq>\\x0a     <rdf:\\\nli\\x0a      stEvt:a\\\nction=\\x22produced\\x22\\\n\\x0a      stEvt:sof\\\ntwareAgent=\\x22Affi\\\nnity Designer 1.\\\n9.2\\x22\\x0a      stEvt\\\n:when=\\x222021-05-3\\\n1T12:30:11+02:00\\\n\\x22/>\\x0a    </rdf:Se\\\nq>\\x0a   </xmpMM:Hi\\\nstory>\\x0a  </rdf:D\\\nescription>\\x0a </r\\\ndf:RDF>\\x0a</x:xmpm\\\neta>\\x0a<?xpacket e\\\nnd=\\x22r\\x22?>\\x85\\x9d\\x9f\\x08\\x00\\x00\\x01\\x83\\\niCCPsRGB IEC6196\\\n6-2.1\\x00\\x00(\\x91u\\x91\\xcf+DQ\\x14\\\n\\xc7?fh\\xfc\\x18\\x8dba1e\\x12\\x16B\\x83\\x12\\\n\\x1b\\x8b\\x99\\x18\\x0a\\x8b\\x99Q~mf\\x9ey3j\\xde\\\nx\\xbd7\\xd2d\\xabl\\xa7(\\xb1\\xf1k\\xc1_\\xc0V\\\nY+E\\xa4d\\xa7\\xac\\x89\\x0dz\\xce\\x9bQ#\\x99s\\\n;\\xf7|\\xee\\xf7\\xdes\\xba\\xf7\\x5cpD\\xd3\\x8afV\\\n\\xfaA\\xcbd\\x8dp(\\xe0\\x9b\\x99\\x9d\\xf3\\xb9\\x9e\\xa8\\xa2\\\n\\x85\\x1a:\\xf1\\xc6\\x14S\\x9f\\x8c\\x8cF)k\\xef\\xb7T\\\n\\xd8\\xf1\\xba\\xdb\\xaeU\\xfe\\xdc\\xbfV\\xb7\\x980\\x15\\xa8\\xa8\\\n\\x16\\x1eVt#+<&<\\xb1\\x9a\\xd5m\\xde\\x12n\\\nRR\\xb1E\\xe1\\x13\\xe1.C.(|c\\xeb\\xf1\\x22\\\n?\\xdb\\x9c,\\xf2\\xa7\\xcdF4\\x1c\\x04G\\x83\\xb0/\\xf9\\\n\\x8b\\xe3\\xbfXI\\x19\\x9a\\xb0\\xbc\\x9c6-\\xbd\\xa2\\xfc\\xdc\\\n\\xc7~\\x89;\\x91\\x99\\x8eHl\\x15\\xf7b\\x12&D\\x00\\\n\\x1f\\xe3\\x8c\\x10d\\x80^\\x86d\\x1e\\xa0\\x9b>zdE\\\n\\x99|\\x7f!\\x7f\\x8ae\\xc9Ud\\xd6\\xc9a\\xb0D\\x92\\\n\\x14Y\\xbaD]\\x91\\xea\\x09\\x89\\xaa\\xe8\\x09\\x19irv\\\n\\xff\\xff\\xf6\\xd5T\\xfb\\xfb\\x8a\\xd5\\xdd\\x01\\xa8z\\xb4\\xac\\xd7\\\nvpm\\xc2W\\xde\\xb2>\\x0e,\\xeb\\xeb\\x10\\x9c\\x0fp\\\n\\x9e)\\xe5/\\xef\\xc3\\xe0\\x9b\\xe8\\xf9\\x92\\xd6\\xb6\\x07\\x9eu\\\n8\\xbd(i\\xf1m8\\xdb\\x80\\xe6{=f\\xc4\\x0a\\x92\\\nS\\xdc\\xa1\\xaa\\xf0r\\x0c\\xf5\\xb3\\xd0x\\x05\\xb5\\xf3\\xc5\\x9e\\\n\\xfd\\xecst\\x07\\xd15\\xf9\\xaaK\\xd8\\xd9\\x85\\x0e9\\xef\\\nY\\xf8\\x06\\x8e\\xfdg\\xf8\\xfd\\x8a\\x18\\x97\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00mIDAT\\x18\\x95u\\xcf\\xc1\\x09\\xc2P\\\n\\x10\\x84\\xe1\\xd7\\x85\\x07\\x9b\\xd0C@\\xd2\\x82x\\x14{0\\\nW!\\x8d\\x84`?bKzH\\xcc\\x97\\x83\\xfb0\\x04\\\n\\xdf\\x9c\\x86\\x7fg\\x99\\xdd\\x84\\x0d\\xaaT\\x10jl\\x13\\x1e\\\n\\xbe\\xba\\xfe\\x0951{\\xe6\\x8d\\x0f&\\x1c\\x17\\xa1S\\xb0\\\n\\x11\\x87\\x0c/\\x01\\x07\\xec\\xb0\\x0f?\\xe1\\xbc\\xaei\\xa3\\xe6\\\n\\x85w\\xf8[\\xe9\\xf0\\xbb\\x9f\\xfa\\xd2\\x839\\xdc\\xa3[\\xf3\\\n\\x19.\\xa8\\x89\\xb50\\xf7C\\xa0\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x14\\x1d\\x00\\xb0\\\n\\xd55\\xa3\\x00\\x00\\x00*IDAT\\x08\\xd7c`\\xc0\\\n\\x06\\xfe\\x9fg``B0\\xa1\\x1c\\x08\\x93\\x81\\x81\\x09\\xc1\\\nd``b``4D\\xe2 s\\x19\\x90\\x8d@\\x02\\\n\\x00d@\\x09u\\x86\\xb3\\xad\\x9c\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x07\\xad\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x07\\x00\\x00\\x00\\x0a\\x08\\x06\\x00\\x00\\x00x\\xccD\\x0d\\\n\\x00\\x00\\x05RiTXtXML:com.\\\nadobe.xmp\\x00\\x00\\x00\\x00\\x00<?\\\nxpacket begin=\\x22\\xef\\\n\\xbb\\xbf\\x22 id=\\x22W5M0MpCe\\\nhiHzreSzNTczkc9d\\\n\\x22?>\\x0a<x:xmpmeta x\\\nmlns:x=\\x22adobe:ns\\\n:meta/\\x22 x:xmptk=\\\n\\x22XMP Core 5.5.0\\x22\\\n>\\x0a <rdf:RDF xmln\\\ns:rdf=\\x22http://ww\\\nw.w3.org/1999/02\\\n/22-rdf-syntax-n\\\ns#\\x22>\\x0a  <rdf:Desc\\\nription rdf:abou\\\nt=\\x22\\x22\\x0a    xmlns:d\\\nc=\\x22http://purl.o\\\nrg/dc/elements/1\\\n.1/\\x22\\x0a    xmlns:e\\\nxif=\\x22http://ns.a\\\ndobe.com/exif/1.\\\n0/\\x22\\x0a    xmlns:ti\\\nff=\\x22http://ns.ad\\\nobe.com/tiff/1.0\\\n/\\x22\\x0a    xmlns:pho\\\ntoshop=\\x22http://n\\\ns.adobe.com/phot\\\noshop/1.0/\\x22\\x0a    \\\nxmlns:xmp=\\x22http:\\\n//ns.adobe.com/x\\\nap/1.0/\\x22\\x0a    xml\\\nns:xmpMM=\\x22http:/\\\n/ns.adobe.com/xa\\\np/1.0/mm/\\x22\\x0a    x\\\nmlns:stEvt=\\x22http\\\n://ns.adobe.com/\\\nxap/1.0/sType/Re\\\nsourceEvent#\\x22\\x0a  \\\n exif:PixelXDime\\\nnsion=\\x227\\x22\\x0a   exi\\\nf:PixelYDimensio\\\nn=\\x2210\\x22\\x0a   exif:C\\\nolorSpace=\\x221\\x22\\x0a  \\\n tiff:ImageWidth\\\n=\\x227\\x22\\x0a   tiff:Ima\\\ngeLength=\\x2210\\x22\\x0a  \\\n tiff:Resolution\\\nUnit=\\x222\\x22\\x0a   tiff\\\n:XResolution=\\x2272\\\n.0\\x22\\x0a   tiff:YRes\\\nolution=\\x2272.0\\x22\\x0a \\\n  photoshop:Colo\\\nrMode=\\x223\\x22\\x0a   pho\\\ntoshop:ICCProfil\\\ne=\\x22sRGB IEC61966\\\n-2.1\\x22\\x0a   xmp:Mod\\\nifyDate=\\x222021-05\\\n-31T12:43:35+02:\\\n00\\x22\\x0a   xmp:Metad\\\nataDate=\\x222021-05\\\n-31T12:43:35+02:\\\n00\\x22>\\x0a   <dc:titl\\\ne>\\x0a    <rdf:Alt>\\\n\\x0a     <rdf:li xm\\\nl:lang=\\x22x-defaul\\\nt\\x22>branch_close<\\\n/rdf:li>\\x0a    </r\\\ndf:Alt>\\x0a   </dc:\\\ntitle>\\x0a   <xmpMM\\\n:History>\\x0a    <r\\\ndf:Seq>\\x0a     <rd\\\nf:li\\x0a      stEvt\\\n:action=\\x22produce\\\nd\\x22\\x0a      stEvt:s\\\noftwareAgent=\\x22Af\\\nfinity Designer \\\n1.9.2\\x22\\x0a      stE\\\nvt:when=\\x222021-05\\\n-31T12:43:35+02:\\\n00\\x22/>\\x0a    </rdf:\\\nSeq>\\x0a   </xmpMM:\\\nHistory>\\x0a  </rdf\\\n:Description>\\x0a <\\\n/rdf:RDF>\\x0a</x:xm\\\npmeta>\\x0a<?xpacket\\\n end=\\x22r\\x22?>$\\xe15\\x97\\x00\\x00\\\n\\x01\\x83iCCPsRGB IEC61\\\n966-2.1\\x00\\x00(\\x91u\\x91\\xcf+D\\\nQ\\x14\\xc7?fh\\xfc\\x18\\x8dba1e\\x12\\x16B\\\n\\x83\\x12\\x1b\\x8b\\x99\\x18\\x0a\\x8b\\x99Q~mf\\x9ey3\\\nj\\xdex\\xbd7\\xd2d\\xabl\\xa7(\\xb1\\xf1k\\xc1_\\\n\\xc0VY+E\\xa4d\\xa7\\xac\\x89\\x0dz\\xce\\x9bQ#\\\n\\x99s;\\xf7|\\xee\\xf7\\xdes\\xba\\xf7\\x5cpD\\xd3\\x8a\\\nfV\\xfaA\\xcbd\\x8dp(\\xe0\\x9b\\x99\\x9d\\xf3\\xb9\\x9e\\\n\\xa8\\xa2\\x85\\x1a:\\xf1\\xc6\\x14S\\x9f\\x8c\\x8cF)k\\xef\\\n\\xb7T\\xd8\\xf1\\xba\\xdb\\xaeU\\xfe\\xdc\\xbfV\\xb7\\x980\\x15\\\n\\xa8\\xa8\\x16\\x1eVt#+<&<\\xb1\\x9a\\xd5m\\xde\\\n\\x12nRR\\xb1E\\xe1\\x13\\xe1.C.(|c\\xeb\\\n\\xf1\\x22?\\xdb\\x9c,\\xf2\\xa7\\xcdF4\\x1c\\x04G\\x83\\xb0\\\n/\\xf9\\x8b\\xe3\\xbfXI\\x19\\x9a\\xb0\\xbc\\x9c6-\\xbd\\xa2\\\n\\xfc\\xdc\\xc7~\\x89;\\x91\\x99\\x8eHl\\x15\\xf7b\\x12&\\\nD\\x00\\x1f\\xe3\\x8c\\x10d\\x80^\\x86d\\x1e\\xa0\\x9b>z\\\ndE\\x99|\\x7f!\\x7f\\x8ae\\xc9Ud\\xd6\\xc9a\\xb0\\\nD\\x92\\x14Y\\xbaD]\\x91\\xea\\x09\\x89\\xaa\\xe8\\x09\\x19i\\\nrv\\xff\\xff\\xf6\\xd5T\\xfb\\xfb\\x8a\\xd5\\xdd\\x01\\xa8z\\xb4\\\n\\xac\\xd7vpm\\xc2W\\xde\\xb2>\\x0e,\\xeb\\xeb\\x10\\x9c\\\n\\x0fp\\x9e)\\xe5/\\xef\\xc3\\xe0\\x9b\\xe8\\xf9\\x92\\xd6\\xb6\\x07\\\n\\x9eu8\\xbd(i\\xf1m8\\xdb\\x80\\xe6{=f\\xc4\\\n\\x0a\\x92S\\xdc\\xa1\\xaa\\xf0r\\x0c\\xf5\\xb3\\xd0x\\x05\\xb5\\xf3\\\n\\xc5\\x9e\\xfd\\xecst\\x07\\xd15\\xf9\\xaaK\\xd8\\xd9\\x85\\x0e\\\n9\\xefY\\xf8\\x06\\x8e\\xfdg\\xf8\\xfd\\x8a\\x18\\x97\\x00\\x00\\x00\\\n\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\\n\\x9c\\x18\\x00\\x00\\x00rIDAT\\x18\\x95m\\xcf1\\x0a\\\n\\xc2P\\x14D\\xd1\\xe8\\x02\\xb4W\\x08\\xd6Ia\\x99JC\\\nt\\x15\\x82\\xabI6(\\xee@\\x04\\xdb\\xa8\\x95Xx,\\\n\\xf2\\x09\\xe1\\xf3\\x07\\xa6\\x9a\\xfb\\xe0\\xbe\\x0c\\x1b\\xb4Xdq\\\np0\\xe4\\x82U\\x0a8\\xe3\\x8b\\x1b\\x8a\\x14p\\xc4\\x1b=\\\nv)`\\x8b\\x07>\\xa8\\xe6\\xd1\\xfe\\x0b\\x9d\\x85\\x8eW\\x0d\\\n^x\\xa2\\x9e\\x0e\\xa7 tG9\\x1d\\xf6\\xe1\\x95+\\xd6\\\n\\xb1D\\x8e\\x0e\\xcbX\\xf0\\x0fR\\x8ay\\x18\\xdc\\xe2\\x02p\\\n\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\x9f\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x08\\x14\\x1f\\xf9\\\n#\\xd9\\x0b\\x00\\x00\\x00#IDAT\\x08\\xd7c`\\xc0\\\n\\x0d\\xe6|\\x80\\xb1\\x18\\x91\\x05R\\x04\\xe0B\\x08\\x15)\\x02\\\n\\x0c\\x0c\\x8c\\xc8\\x02\\x08\\x95h\\x00\\x00\\xac\\xac\\x07\\x90Ne\\\n4\\xac\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x070\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x0a\\x00\\x00\\x00\\x07\\x08\\x06\\x00\\x00\\x001\\xac\\xdcc\\\n\\x00\\x00\\x04\\xb0iTXtXML:com.\\\nadobe.xmp\\x00\\x00\\x00\\x00\\x00<?\\\nxpacket begin=\\x22\\xef\\\n\\xbb\\xbf\\x22 id=\\x22W5M0MpCe\\\nhiHzreSzNTczkc9d\\\n\\x22?>\\x0a<x:xmpmeta x\\\nmlns:x=\\x22adobe:ns\\\n:meta/\\x22 x:xmptk=\\\n\\x22XMP Core 5.5.0\\x22\\\n>\\x0a <rdf:RDF xmln\\\ns:rdf=\\x22http://ww\\\nw.w3.org/1999/02\\\n/22-rdf-syntax-n\\\ns#\\x22>\\x0a  <rdf:Desc\\\nription rdf:abou\\\nt=\\x22\\x22\\x0a    xmlns:e\\\nxif=\\x22http://ns.a\\\ndobe.com/exif/1.\\\n0/\\x22\\x0a    xmlns:ti\\\nff=\\x22http://ns.ad\\\nobe.com/tiff/1.0\\\n/\\x22\\x0a    xmlns:pho\\\ntoshop=\\x22http://n\\\ns.adobe.com/phot\\\noshop/1.0/\\x22\\x0a    \\\nxmlns:xmp=\\x22http:\\\n//ns.adobe.com/x\\\nap/1.0/\\x22\\x0a    xml\\\nns:xmpMM=\\x22http:/\\\n/ns.adobe.com/xa\\\np/1.0/mm/\\x22\\x0a    x\\\nmlns:stEvt=\\x22http\\\n://ns.adobe.com/\\\nxap/1.0/sType/Re\\\nsourceEvent#\\x22\\x0a  \\\n exif:PixelXDime\\\nnsion=\\x2210\\x22\\x0a   ex\\\nif:PixelYDimensi\\\non=\\x227\\x22\\x0a   exif:C\\\nolorSpace=\\x221\\x22\\x0a  \\\n tiff:ImageWidth\\\n=\\x2210\\x22\\x0a   tiff:Im\\\nageLength=\\x227\\x22\\x0a  \\\n tiff:Resolution\\\nUnit=\\x222\\x22\\x0a   tiff\\\n:XResolution=\\x2272\\\n.0\\x22\\x0a   tiff:YRes\\\nolution=\\x2272.0\\x22\\x0a \\\n  photoshop:Colo\\\nrMode=\\x223\\x22\\x0a   pho\\\ntoshop:ICCProfil\\\ne=\\x22sRGB IEC61966\\\n-2.1\\x22\\x0a   xmp:Mod\\\nifyDate=\\x222021-05\\\n-31T12:33:14+02:\\\n00\\x22\\x0a   xmp:Metad\\\nataDate=\\x222021-05\\\n-31T12:33:14+02:\\\n00\\x22>\\x0a   <xmpMM:H\\\nistory>\\x0a    <rdf\\\n:Seq>\\x0a     <rdf:\\\nli\\x0a      stEvt:a\\\nction=\\x22produced\\x22\\\n\\x0a      stEvt:sof\\\ntwareAgent=\\x22Affi\\\nnity Designer 1.\\\n9.2\\x22\\x0a      stEvt\\\n:when=\\x222021-05-3\\\n1T12:33:14+02:00\\\n\\x22/>\\x0a    </rdf:Se\\\nq>\\x0a   </xmpMM:Hi\\\nstory>\\x0a  </rdf:D\\\nescription>\\x0a </r\\\ndf:RDF>\\x0a</x:xmpm\\\neta>\\x0a<?xpacket e\\\nnd=\\x22r\\x22?>H\\x8b[^\\x00\\x00\\x01\\x83\\\niCCPsRGB IEC6196\\\n6-2.1\\x00\\x00(\\x91u\\x91\\xcf+DQ\\x14\\\n\\xc7?fh\\xfc\\x18\\x8dba1e\\x12\\x16B\\x83\\x12\\\n\\x1b\\x8b\\x99\\x18\\x0a\\x8b\\x99Q~mf\\x9ey3j\\xde\\\nx\\xbd7\\xd2d\\xabl\\xa7(\\xb1\\xf1k\\xc1_\\xc0V\\\nY+E\\xa4d\\xa7\\xac\\x89\\x0dz\\xce\\x9bQ#\\x99s\\\n;\\xf7|\\xee\\xf7\\xdes\\xba\\xf7\\x5cpD\\xd3\\x8afV\\\n\\xfaA\\xcbd\\x8dp(\\xe0\\x9b\\x99\\x9d\\xf3\\xb9\\x9e\\xa8\\xa2\\\n\\x85\\x1a:\\xf1\\xc6\\x14S\\x9f\\x8c\\x8cF)k\\xef\\xb7T\\\n\\xd8\\xf1\\xba\\xdb\\xaeU\\xfe\\xdc\\xbfV\\xb7\\x980\\x15\\xa8\\xa8\\\n\\x16\\x1eVt#+<&<\\xb1\\x9a\\xd5m\\xde\\x12n\\\nRR\\xb1E\\xe1\\x13\\xe1.C.(|c\\xeb\\xf1\\x22\\\n?\\xdb\\x9c,\\xf2\\xa7\\xcdF4\\x1c\\x04G\\x83\\xb0/\\xf9\\\n\\x8b\\xe3\\xbfXI\\x19\\x9a\\xb0\\xbc\\x9c6-\\xbd\\xa2\\xfc\\xdc\\\n\\xc7~\\x89;\\x91\\x99\\x8eHl\\x15\\xf7b\\x12&D\\x00\\\n\\x1f\\xe3\\x8c\\x10d\\x80^\\x86d\\x1e\\xa0\\x9b>zdE\\\n\\x99|\\x7f!\\x7f\\x8ae\\xc9Ud\\xd6\\xc9a\\xb0D\\x92\\\n\\x14Y\\xbaD]\\x91\\xea\\x09\\x89\\xaa\\xe8\\x09\\x19irv\\\n\\xff\\xff\\xf6\\xd5T\\xfb\\xfb\\x8a\\xd5\\xdd\\x01\\xa8z\\xb4\\xac\\xd7\\\nvpm\\xc2W\\xde\\xb2>\\x0e,\\xeb\\xeb\\x10\\x9c\\x0fp\\\n\\x9e)\\xe5/\\xef\\xc3\\xe0\\x9b\\xe8\\xf9\\x92\\xd6\\xb6\\x07\\x9eu\\\n8\\xbd(i\\xf1m8\\xdb\\x80\\xe6{=f\\xc4\\x0a\\x92\\\nS\\xdc\\xa1\\xaa\\xf0r\\x0c\\xf5\\xb3\\xd0x\\x05\\xb5\\xf3\\xc5\\x9e\\\n\\xfd\\xecst\\x07\\xd15\\xf9\\xaaK\\xd8\\xd9\\x85\\x0e9\\xef\\\nY\\xf8\\x06\\x8e\\xfdg\\xf8\\xfd\\x8a\\x18\\x97\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x97IDAT\\x18\\x95m\\xcf\\xb1j\\x02A\\\n\\x14\\x85\\xe1o\\xb7\\xb6\\xd0'H=Vi\\x03\\xb1\\xb4H\\\n;l\\xa5\\xf19\\xf6Y\\x02VB\\xbaa\\x0a\\x0b;\\x1b\\\n\\x1bkA\\x18\\x02)m\\xe3\\xbe\\x82\\xcd\\x06\\x16\\xd9\\xdb\\xdd\\\n\\x9f\\xff\\x5c\\xee\\xa9b*\\x13Ls\\x13nF&\\xa6\\xf2\\\n\\x82\\xaeF\\x8b\\xdf\\x98\\xca\\xfb\\x88\\xb4\\xc0\\x0f\\xda\\x1a[t\\\n\\xd8\\xc7T\\xc2@\\x9ac\\x8f?|U=|\\xc5\\x09w\\\n\\xbc\\xa1\\xc2\\x193,r\\x13.\\xd5\\xe0\\xc2\\x12\\x07\\x5cQ\\\n#\\xe0#7\\xe1\\xa8O\\x0e\\x7f\\xda`\\xd7\\xaf\\x9f\\xb9\\x09\\\n\\xdfc\\x05\\xff\\xe5uLe\\xf5\\xcc\\x1f\\x0d3,\\x83\\xb6\\\n\\x06D\\x83\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x03\\xff\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x03\\xb1IDATh\\x81\\xed\\\n\\x9aOh\\x1eE\\x18\\xc6\\x7f\\x9b&\\x85\\x82\\x15\\x15\\xabB\\\n\\xcb\\x03\\x06\\x05\\xa9\\x0a\\x8a\\xb7R<\\xd4\\x96\\xaa\\xb5\\xe0A\\\n\\xad\\xc5C%\\xa0\\x07Q\\xccM(\\xb4\\x07E<x\\xb0\\\n\\xa0RP\\x0f\\x8aP\\xf5P\\xa5(\\xc6\\xbf\\xa84 \\x08\\\n\\x9eLA\\xc5\\x83< \\xd2\\x90*\\x0a\\xfei\\x92:\\x1e\\\nf\\x8d_\\xd6\\xdd\\x9d\\xfd\\xf6\\x8b\\xd9/\\xd0\\xdf\\xed\\x9by\\\n\\xe7\\x9d\\xe7\\x9d\\xd9\\x99yg\\xbf\\xcd\\xc8\\x09!\\x5c\\x0a\\x1c\\\n\\x06\\xb6\\x03\\xd7\\x01c\\x0c\\x17\\x0b\\xc0\\x0c0\\x0d<\\x9ee\\\n\\xd9\\xdcRM\\x08aG\\x08\\xe1tX;\\x9c\\x0e!\\xec\\\n\\x00\\xc8B\\x1c\\xf9S\\xc0e]\\x0c\\xeb\\x00\\xcc\\x02\\xd7\\x8e\\\n\\x10\\x1f\\x9b\\xb5&\\x1e\\xa2\\xe6C\\xa3\\xc4g\\xbe\\xc8\\xb9U\\\n\\x16\\xd3\\x94u\\x85\\xdf\\xdb\\xb3\\x10\\xc2<\\xcb\\x17\\xec\\xb9,\\\n\\xcbFWQTcB\\x08\\x8b,\\x0fba\\x84\\xe1\\xdb\\\nm\\xfaal\\xa4k\\x05\\x83r>\\x80\\xae9\\x1f\\xc0\\xff\\\n\\x81\\xed\\xbbm\\xbff{W\\xca6\\x0b!\\x84BY\\xa7\\\n\\xdb\\xa8\\xedG\\x81#\\xf9\\xcf\\x00\\x1c\\x05&%-\\x94l\\\n\\xa3\\xc35\\x03\\xb6\\xef\\x05\\x9e\\xe9)\\xca\\x80\\x87\\x80\\x17\\xab\\\n\\xda\\x0c\\xcd\\x81e{'\\xf0\\x0aQt\\x91\\x03\\xb6\\xbf*\\\nk7\\x143`\\xfb&\\xe0M`}\\x8d\\xd9me\\x85\\\n\\x9d\\x07`\\xfb*\\xe0]`c\\xc2\\xf4\\xa5\\xb2\\xc2N\\x03\\\n\\xb0}9\\xf0>\\xe9l\\xf8iI\\xaf\\x97Ut\\x16\\x80\\\n\\xed\\x8d\\xc0\\x140\\x9e0}\\x15x\\xac\\xaa\\xb2\\x93\\x00l\\\n\\xaf\\x07\\xde\\x02nL\\x98\\xbe\\x07LH*n\\xf5K\\xac\\\nz\\x00\\xb6G\\x88\\xa3zK\\xc2\\xf4\\x0b\\xe0.I\\x8bu\\\nF]\\xcc\\xc0\\x11\\xe0\\x9e\\x84\\xcd\\xb7\\xc0\\x1eI\\xbf\\xa5\\x9c\\\n5\\x0a\\xc0v\\x96\\x8f\\xdc@\\xd8>\\x08<\\x920\\xfb\\x11\\\n\\xd8-i.a\\x07$\\x02\\xc8\\x85O\\x02\\x7f\\x003\\xb6\\\n\\xafo\\xa4\\xb4\\xdc\\xd7\\x04\\xf0d\\xc2\\xec\\x17\\xe0VI\\xdf\\\n7\\xf5[\\x99\\x0b\\xd9\\x1e\\x03\\x8e\\x03{{\\xeaf\\x81\\x9b\\\n%}\\xd3\\xb4\\x03\\x00\\xdb{\\x89\\x8b\\xb6x\\xa7\\xed\\xe5,\\\nq\\xe4?\\xab2\\xe87\\x17z\\x8e\\xe5\\xe2!\\xee\\xd7\\x1f\\\n\\xdb\\xbe\\xb2Vq\\x0f\\xb6\\xb7\\x01o\\x14;.\\xf0\\x17p\\\n_\\x9d\\xf8*J\\x03\\xc8G\\xec\\xc1\\x8a6\\x9b\\x81\\x8fl\\\noN9\\xb7\\xbd\\x15x\\x1b\\xd8\\x900}X\\xd2\\xf1\\x94\\\n\\xbf2\\xaaf\\xe0\\x92D\\xbbqb\\x10\\x9b\\xaa\\x0clo\\\n!\\x9e\\xb2)_OH:\\x9a\\xb0\\xa9\\xa4*\\x80c\\xc4\\\n}\\xb8\\x8ek\\x80\\x0fl_T\\xac\\xb0}1Q\\xfc\\x96\\\n\\x84\\x8f\\x17$\\x1dN\\xaa\\xac\\xa14\\x00I\\x0b\\xc4\\xec\\xaf\\\n4\\x85\\xed\\xe1\\x06`\\xca\\xf6\\x05\\xff\\x14\\xd8\\xde@|l\\\n\\xb6&\\xda\\x9e \\xe6\\xfa\\x03Q{#\\xcb\\x93\\xad\\x93\\xc0\\\n\\xd5\\x09?\\x9f\\x02\\xb7\\x03\\xf3\\xc4\\xdd\\xa6\\xb8\\xf8\\x8bL\\x03\\\n\\xbb$\\xfd\\xd9\\x8f\\xd8\\xb2](y\\xa5\\xb4-b\\x10J\\\n\\xf8\\x9f\\x22\\x1eB\\x13\\x09\\xbb\\x19\\xe2V\\xfcs#\\xd5=\\\n\\xb4\\x0a\\x00\\x96r\\xf6\\x93\\xc0\\x15\\xfdvZ\\xc0\\xc06I\\\n?\\xb4i\\xdc\\xfaN,\\xe9;`'p\\xa6M\\xc79\\\n?\\x11\\x0f\\xaaV\\xe2\\xabh\\x9c\\xdfH:\\x05\\xec\\x06~\\\nm\\xd1\\xcf\\xef\\xc0\\x1d\\x92\\xben\\xd1\\xb6\\x96\\xbe\\x124I\\\n_\\x02{rAMY\\x04\\xf6I\\xfa\\xbc\\x9f\\xbe\\x9a\\xd2\\\nw\\x86)i\\x1a\\xb8\\x93\\x98\\xbb4\\xe1\\x01I\\xef\\xf4\\xdb\\\nOSZ\\xa5\\xc8\\x92>\\x04\\xf6\\x11G\\xb7\\x8e\\x83\\x92^\\\nn\\xd3GSZ\\xe7\\xf8\\x92N\\x00\\x07\\x88\\x89X\\x19\\xcf\\\nJz\\xaa\\xad\\xff\\xa6\\x0ctI\\x91t\\x0c\\xb8\\x9f\\xff\\xae\\\n\\x89\\xe7\\x81\\xc9A|7eE\\xde\\x8d\\xda\\x1e'\\x9e\\xbe\\\n\\x17\\x02\\x9f\\xe4\\xebd\\xc5i}\\x90\\x0d\\x0bU\\x07\\xd9B\\\n7rV\\x84\\xf9Qbn\\xd2\\xfb~f]\\x1e\\xe90\\\nR\\xbc\\xd5\\xcd\\x8c\\x123\\xc3\\xe2\\x0b\\xa6\\xba\\xeb\\xdf01\\\n\\xbd\\xf6?5\\xc8\\xbf\\xfa\\xd8\\x9f\\x17\\xac\\x15f\\x81\\xfdY\\\n\\x96\\xcd-\\xfd\\x99\\x90\\xcf\\xc4!\\xfe\\xfd\\xdc\\xa6\\xee]}\\\n\\x17\\xcc\\xb3\\xfcs\\x9b3\\x00\\x7f\\x03\\xd9\\x1a\\xfb\\xdb\\xbb\\xa7\\\n\\x8f\\x07\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x01[\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x0dIDATh\\x81\\xed\\\n\\xda\\xb1m\\x02A\\x10F\\xe1w\\xc7\\xe2\\x0a,\\x87\\xd3\\x00\\\n8 'r\\x17\\x14\\x83\\x037\\xe3.\\x1cQ\\x0240\\\n!\\xc2\\x0d`\\x90\\x1c\\xec\\x9e\\x0c'Y\\xf6\\x09\\x89\\xffV\\\n\\x9a/cE0\\x0f\\x0e\\x92\\x9d\\x86\\xc2\\xdd\\x1f\\x81W`\\\n\\x09\\xcc\\x81)\\xe3\\xf2\\x05l\\x81\\x0d\\xf0ff\\x07\\x80\\x06\\\n\\xc0\\xdd_\\x80w\\xe0I6\\xde0{`ef\\x1fM\\\n\\xf9\\xe4w\\xd43|g\\x0f\\xccZ\\xf2cS\\xdb\\xf0\\x90\\\ng^'\\xf23\\xdfw\\xbe\\xf30\\xff5\\xe9\\xbd^&\\\n\\xf2\\x0f\\xf6\\xd2\\xd9\\xcc\\xd2\\x9d\\x06\\x1a\\xc4\\xddO\\x5cG<\\\n\\xb7\\x8c\\xef\\xdff\\x88i\\xab\\x9e\\xe0V\\x11\\xa0\\x16\\x01j\\\n\\x11\\xa0\\x16\\x01j\\x11\\xa0\\x16\\x01j\\x11\\xa0\\x16\\x01j\\x11\\\n\\xa0\\x16\\x01j\\x11\\xa0\\x16\\x01j\\x11\\xa0\\x16\\x01j\\x11\\xa0\\\n\\x16\\x01j\\x11\\xa0\\xd6\\x92o\\xc0kuL\\xe4\\xeb\\xfb\\xc5\\\n\\xc5\\xe1\\xa4\\xdc\\x06\\x8eQ\\xff\\x9au\\x9b\\xc8\\xbb\\x07\\x8b?\\\n\\xde8V\\x9b\\xfaW\\x0d\\xca\\xd6\\xc7\\xaa\\x1c\\xd4\\xa2[\\xf6\\\n84\\xddI\\xf9&\\xd6\\xfc\\xac\\xdb<\\x88\\x86\\xfb\\xcd\\x91\\\n\\xebu\\x9bO\\x80oV\\x016\\x1ew\\x0d\\xa5B\\x00\\x00\\\n\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x05~\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x08\\x06\\x00\\x00\\x00\\x1f\\x15\\xc4\\x89\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x05\\x17iTXtXML\\\n:com.adobe.xmp\\x00\\x00\\\n\\x00\\x00\\x00<?xpacket beg\\\nin=\\x22\\xef\\xbb\\xbf\\x22 id=\\x22W5M\\\n0MpCehiHzreSzNTc\\\nzkc9d\\x22?> <x:xmpm\\\neta xmlns:x=\\x22ado\\\nbe:ns:meta/\\x22 x:x\\\nmptk=\\x22Adobe XMP \\\nCore 7.1-c000 79\\\n.7a7a236, 2021/0\\\n8/12-00:25:20   \\\n     \\x22> <rdf:RDF\\\n xmlns:rdf=\\x22http\\\n://www.w3.org/19\\\n99/02/22-rdf-syn\\\ntax-ns#\\x22> <rdf:D\\\nescription rdf:a\\\nbout=\\x22\\x22 xmlns:xm\\\np=\\x22http://ns.ado\\\nbe.com/xap/1.0/\\x22\\\n xmlns:dc=\\x22http:\\\n//purl.org/dc/el\\\nements/1.1/\\x22 xml\\\nns:xmpMM=\\x22http:/\\\n/ns.adobe.com/xa\\\np/1.0/mm/\\x22 xmlns\\\n:stEvt=\\x22http://n\\\ns.adobe.com/xap/\\\n1.0/sType/Resour\\\nceEvent#\\x22 xmlns:\\\nphotoshop=\\x22http:\\\n//ns.adobe.com/p\\\nhotoshop/1.0/\\x22 x\\\nmp:CreatorTool=\\x22\\\nAdobe Photoshop \\\n22.5 (Windows)\\x22 \\\nxmp:CreateDate=\\x22\\\n2021-11-10T17:39\\\n:02+01:00\\x22 xmp:M\\\netadataDate=\\x22202\\\n1-11-10T17:39:02\\\n+01:00\\x22 xmp:Modi\\\nfyDate=\\x222021-11-\\\n10T17:39:02+01:0\\\n0\\x22 dc:format=\\x22im\\\nage/png\\x22 xmpMM:I\\\nnstanceID=\\x22xmp.i\\\nid:f17ebb23-e62a\\\n-3946-a975-d4f6a\\\nb44d409\\x22 xmpMM:D\\\nocumentID=\\x22xmp.d\\\nid:f17ebb23-e62a\\\n-3946-a975-d4f6a\\\nb44d409\\x22 xmpMM:O\\\nriginalDocumentI\\\nD=\\x22xmp.did:f17eb\\\nb23-e62a-3946-a9\\\n75-d4f6ab44d409\\x22\\\n photoshop:Color\\\nMode=\\x223\\x22 photosh\\\nop:ICCProfile=\\x22s\\\nRGB IEC61966-2.1\\\n\\x22> <xmpMM:Histor\\\ny> <rdf:Seq> <rd\\\nf:li stEvt:actio\\\nn=\\x22created\\x22 stEv\\\nt:instanceID=\\x22xm\\\np.iid:f17ebb23-e\\\n62a-3946-a975-d4\\\nf6ab44d409\\x22 stEv\\\nt:when=\\x222021-11-\\\n10T17:39:02+01:0\\\n0\\x22 stEvt:softwar\\\neAgent=\\x22Adobe Ph\\\notoshop 22.5 (Wi\\\nndows)\\x22/> </rdf:\\\nSeq> </xmpMM:His\\\ntory> </rdf:Desc\\\nription> </rdf:R\\\nDF> </x:xmpmeta>\\\n <?xpacket end=\\x22\\\nr\\x22?>\\x07b\\x0c\\x81\\x00\\x00\\x00\\x0dIDAT\\\n\\x08\\x1dc\\xf8\\xff\\xff?\\x03\\x00\\x08\\xfc\\x02\\xfe\\xe6\\x0c\\xff\\\n\\xab\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x08\\x15;\\xdc\\\n;\\x0c\\x9b\\x00\\x00\\x00*IDAT\\x08\\xd7c`\\xc0\\\n\\x00\\x8c\\x0c\\x0cs> \\x0b\\xa4\\x08020 \\x0b\\xa6\\\n\\x08000B\\x98\\x10\\xc1\\x14\\x01\\x14\\x13P\\xb5\\xa3\\x01\\\n\\x00\\xc6\\xb9\\x07\\x90]f\\x1f\\x83\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x043\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x03\\xe5IDATh\\x81\\xed\\\n\\x9aM\\x88\\x1cE\\x14\\xc7\\x7f3;\\x89.\\x18\\xf13\\x1e\\\n\\x92\\x8b\\x8b\\xa2&\\x06\\x14/\\xa29\\x88\\x15\\x89\\xa9\\x18\\x15\\\n\\xd1\\xca\\x06\\x0f\\x91\\x05=\\x88bnB\\xc0\\x1c\\x12\\xc4\\x83\\\n\\x07\\x15\\x0dB\\x14\\x89\\x04\\xa2\\x96DY\\x22/F}\\x8a\\\n\\xb0\\x22\\x08\\x1e4\\xbb\\x22F\\x8c,\\x88\\xba\\xc4\\x88\\x82_\\\n\\xc9F=T\\x0f\\x8c\\xbd\\xdd]\\xdd\\xd3a\\xa7\\x07\\xfc\\xdd\\\n\\xa6\\xfa\\xd5\\xab\\xf7\\xea\\xf3\\xdf\\xd5\\xd3\\x22\\xc1Xw\\x11\\xb0\\\n\\x03X\\x0b\\x5c\\x0d,\\xa1Y\\x9c\\x02\\xa6\\x81)`\\xa7\\x8a\\\n?\\x0e\\xd0\\x020\\xd6\\xdd\\x0c\\xbc\\x02,\\x1fXx\\xd5\\x98\\\n\\x03\\xb6\\xa8\\xf8\\xf7[I\\xcf\\xcf0<\\xc1w\\x99\\x03V\\\n\\xb7\\x09\\xd3f\\xd8\\x82\\x87\\x10\\xf3c\\x1d\\xc2\\x9cOsz\\\n\\x91\\x83)\\xcbH\\xea\\xf7\\xda\\x0ea\\xc1\\xf6rZ\\xc5w\\\n\\x16)\\xa0J\\x18\\xeb\\xe6\\xf9o\\x12k\\xda4o\\xb7\\xa9\\\n\\xc2\\x92\\xf6\\xa0#\\xa8\\xcb\\xff\\x09\\x0c\\x9a\\xa1O\\xa0\\xa9\\xbb\\\n\\xcd=\\xc0]\\xc0K*\\xfe\\xdd\\x22\\xdb\\xc6\\x8d\\x80\\xb1\\xee\\\n\\x11\\xc0\\x03\\xe3\\xc0ac\\xddnc]\\xeeN\\xd9\\xa8\\x04\\\n\\x8cu\\xe3\\xc0S=E-\\xe0A\\xe0\\x85\\xbc:\\x8d\\x99\\\nB\\xc6\\xbau\\xc0\\xcb$\\x023\\xc5Vc\\xdd\\x91\\xacz\\\n\\x8d\\x18\\x01c\\xddu\\xc0\\x1b\\xc0\\xd2\\x02\\xb3\\x0dY\\x85\\x03\\\nO\\xc0Xw\\x19 \\xc0\\xb2\\x88\\xe9\\x8bY\\x85\\x03M\\xc0\\\nXw\\x09p\\x98\\xb8\\x1a~R\\xc5\\xbf\\x9a\\xf5``\\x09\\\n\\x18\\xeb\\x96\\x01\\x87\\x80\\xb1\\x88\\xe9>\\xe0\\xd1\\xbc\\x87\\x03I\\\n\\xc0X\\xb7\\x14x\\x13\\xb86b\\xfa60\\xa1\\xe2\\xff\\xc9\\\n3X\\xf4\\x04\\x8cumB\\xaf\\x9a\\x88\\xe9'\\xc0\\xdd*\\\n~\\xbe\\xc8h\\x10#\\xf04\\xe0\\x226_\\x01\\x1bU\\xfc\\\no1g\\xa5\\x120\\xd6\\xb5\\x92\\x9e\\xab\\x85\\xb1n;\\xf0\\\np\\xc4\\xec{`}\\xf7\\xd6!FaPI\\xe0\\xdb\\x80\\\n?\\x80ic\\xdd\\x9aR\\x91f\\xfb\\x9a\\x00\\x1e\\x8f\\x98\\xfd\\\n\\x02\\xdc\\xaa\\xe2\\xbf-\\xeb77\\x81D\\x7fL\\x12\\x8e\\xf6\\\n\\xb3\\x80\\xab\\x80\\xf7\\x8cuW\\x94u\\xde\\xe3k\\x13\\xb0'\\\nb\\xf6\\x17p\\x87\\x8a\\xff\\xbc\\x8a\\xef\\xa2\\x11x\\x0e\\xd8\\x94\\\n*[\\x0e\\xa8\\xb1\\xee\\xd2\\xb2\\x0d\\x18\\xebn\\x00^c\\xe1\\\n\\x0by/\\x7f\\x03\\xf7\\xaa\\xf8\\x0f\\xcb\\xfa\\xed\\x92\\x99@\\xd2\\\nc\\x0f\\xe4\\xd4YA\\x18\\x89\\x151\\xe7\\xc6\\xbaU\\xc0A\\\n`4b\\xfa\\x90\\x8a?\\x10\\xf3\\x97E\\xde\\x08\\x5c\\x10\\xa9\\\n7FH\\xe2\\xe2<\\x03c\\xddJ\\xc2)\\x1b\\xf3\\xb5K\\\n\\xc5?\\x1f\\xb1\\xc9%/\\x81\\xfd\\x84}\\xb8\\x88+\\x81w\\\n\\x8cu\\xe7\\xa5\\x1f\\x18\\xeb\\xce'\\x04\\xbf2\\xe2c\\x8f\\x8a\\\n\\xdf\\x11\\x8d\\xb2\\x80\\xcc\\x04T\\xfc)\\x82\\xfa\\xcb\\x94\\xb0=\\\n\\x5c\\x03\\x1c2\\xd6\\x9d\\xd3-0\\xd6\\x8d\\x12\\xa6\\xcd\\xaaH\\\n\\xddI\\x82\\xd6\\xafE\\xee\\x22V\\xf1'\\x80[\\x80\\xa3\\x11\\\n\\x1f\\xd7\\x03\\x07\\x8du\\xa3\\xc6\\xba\\x11\\xc2\\x82\\xbd1Rg\\\n\\x0a\\x18W\\xf1\\xb5o\\x00\\x0b\\xcf\\x01\\x15\\xff#\\xb0\\x0e\\x98\\\n\\x8d\\xf8\\xb9\\x098@\\xd8*\\xd3;W\\x9ai\\xe0v\\x15\\\n\\xffg\\xc9\\x18\\x0b\\x89\\x9e\\xae*~\\x96\\xa0[~\\x88\\x98\\\nn\\x00&\\x226\\xb3\\x84\\x83\\xea\\xe7r\\xe1\\xc5)%\\x0f\\\nT\\xfc\\xd7\\x84\\x91\\xf8\\xa9F['\\x08\\x12\\xe1\\xbb\\x1a>\\\n\\x16PZ\\xdf\\xa8\\xf8\\x19`=\\xf0k\\x1f\\xed\\xfc\\x0e\\xdc\\\n\\xa6\\xe2\\xbf\\xec\\xa3n!\\x95\\x04\\x9a\\x8a\\xff\\x14\\xd8\\x98\\x04\\\nT\\x96y`\\xb3\\x8a\\xff\\xb8J[e\\xa9\\xac0U\\xfc\\\n\\x14p'A\\xbb\\x94\\xe1~\\x15\\xffV\\xd5v\\xca\\xd2\\x97\\\nDNn\\xcb6\\x13z\\xb7\\x88\\xed*~o?m\\x94\\\n\\xa5o\\x8d\\xaf\\xe2'\\x81\\xad\\x04!\\x96\\xc5\\xb3*\\xfe\\x89\\\n~\\xfd\\x97\\xa5\\xd6K\\x8a\\x8a\\xdf\\x0f\\xdc\\xc7\\xc25\\xb1\\x1b\\\n\\xd8V\\xc7wYj\\xdf\\xcc\\xa9\\xf8}\\xc6\\xba\\x8f\\x08\\x07\\\n\\xd8\\xb9\\xc0\\x07\\xc9:Y\\x14\\xce\\xc8\\xd5\\xa2\\x8a\\xff\\x06x\\\n\\xe6L\\xf8\\xaaJ\\x9b\\xf0\\x05|X9\\xd9!h\\x93\\xde\\\n\\xfb\\x99\\x91\\xe4k`\\x13I\\xbf\\xd5Mw\\x08\\xca0}\\\n\\xc1T\\xf4\\xfa\\xd7$\\xa6\\xda\\xc0N\\xc2g\\xfbac\\x0e\\\n\\xd8\\xd5N\\xee_\\xb60\\x5cIt\\xff\\xecq|\\x04\\xe0\\\n\\xd8\\xd1\\x99cc\\x97\\xaf\\xde\\x0b\\x9cM\\xf8\\xf0}!\\xcd\\\n\\x9bF'\\x81\\xcf\\x80\\xd7\\x01\\xa7\\xe2\\xbf\\x00\\xf8\\x17]\\x81\\\n\\x0b8\\xb3\\xfa \\x9c\\x00\\x00\\x00\\x00IEND\\xaeB\\\n`\\x82\\\n\\x00\\x00\\x00\\xa0\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x14\\x1c\\x1f$\\\n\\xc6\\x09\\x17\\x00\\x00\\x00$IDAT\\x08\\xd7c`@\\\n\\x05\\xff\\xcf\\xc3XL\\xc8\\x5c&dY&d\\xc5p\\x0e\\\n\\xa3!\\x9c\\xc3h\\x88a\\x1a\\x0a\\x00\\x00m\\x84\\x09u7\\\n\\x9e\\xd9#\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x01\\xdc\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x8eIDATh\\x81\\xed\\\n\\x9a\\xafN\\xc3P\\x14\\x87\\xbfn\\x1d\\x0a\\x1cA\\x1e\\x83\\x04\\\n\\xc4\\x0cjA\\x10\\x04\\x82\\x80\\x9e\\xe7\\x05x\\x80!x\\x01\\\n^\\x00\\x8f\\xc2\\x00rA\\x90=\\xc2@1s\\xe42\\x14\\\n\\xc1\\xecO\\x82h\\x1b\\xb6e\\xed(\\xebv\\xda\\xe5~\\xae\\\n\\xf7\\x5c\\xf1\\xfb\\xda{o\\x9a\\xdc\\xe3\\x11\\xa2\\xaa\\xdb\\xc05\\\nP\\x03\\xf6\\x81\\x0a\\xf9b\\x00\\xb4\\x81\\x16p#\\x22=\\x00\\\n\\x0f@U\\x8f\\x81{`\\xc7,^:\\xba@]D^\\\n\\xbc\\xf0\\xcd\\xbfQ\\x9c\\xf0\\x11]`\\xafD\\xb0l\\x8a\\x16\\\n\\x1e\\x82\\xcc\\x0d\\x9f`\\xcdO3Zq\\x98\\xbfR\\x9ez\\\n\\xae\\xf9\\x04\\x1bv\\x9c\\x91\\x88\\xf8+\\x0a\\x94\\x0aU\\x1d2\\\n)qP\\x22\\x7f\\xa7M\\x1a*%\\xeb\\x04\\x8b\\xe2\\x04\\xac\\\nq\\x02\\xd68\\x01k\\x9c\\x805N\\xc0\\x1a'`\\xcd\\xdc\\\n\\xdffU=\\x01\\x0e\\x81\\xcd\\xe5\\xc7\\x99`\\x08\\xbc\\x03O\\\n\\x22\\xf2\\x1d7)V@U}\\xe0\\x018\\xcf>[*\\\n:\\xaaz*\\x22\\x1f\\xb3\\x8aIK\\xe8\\x0a\\xfb\\xf0\\x00\\xbb\\\n\\xc0]\\x5c1I\\xe0,\\xfb,\\xff\\xe6HU\\xb7f\\x15\\\n\\x0a\\xbf\\x89\\x93\\x04\\x9eW\\x96b>\\xaf\\x22\\xf25\\xab\\x90\\\n$p\\x0b<.'O*:\\xc0e\\x5c1\\xf6\\x14\\x12\\\n\\x91!pQ\\xd8c4BD\\x9a@3\\xc3`\\x99\\xb2\\\n\\xd6\\x9b\\xb8\\x108\\x01k\\x9c\\x805N\\xc0\\x1a'`\\x8d\\\n\\x13\\xb0f-\\x04\\x06\\xd6!\\x16\\xa0\\xef\\x13\\x5c\\xdfW\\xc7\\\n\\x06\\xcb\\xe1m`\\x1e\\x99\\xbefm\\xfb\\x04\\xbd\\x07\\xd59\\\n\\x13\\xf3J\\xab\\xf8\\xad\\x06a\\xd7G=\\x1c(\\x0aQ\\xb3\\\nG\\xcf\\x8bF\\xc2/\\xd1\\xe0\\xb7\\xddf\\xc3(\\x5c\\x1c}\\\n&\\xdbm>\\x01~\\x00%\\xf8ZCUN:\\x7f\\x00\\\n\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x01\\xfc\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\xaeIDATh\\x81\\xed\\\n\\x9a\\xbdJ\\x03A\\x14FO6\\x1b\\xb0\\xd0J\\xf1\\x01\\x14\\\n\\xabh\\x91\\xc6*X\\xb8\\x16\\xb2\\x88v\\x0b\\xe9}\\x01\\x1f\\\n@\\x8b\\xf8\\x00\\xbe\\x80\\x85\\x9d0b\\xa32U\\xc6B\\xf2\\\n\\x02B\\x92F\\x83}H'6\\xf9\\x01\\x8b\\xdd@\\x12\\xb2\\\n\\x89k~f7\\xcc\\xe9v\\xef\\x14\\xdfY\\xee\\x0c\\x0bw\\\nR\\x048\\xae\\xb7\\x01\\x5c\\x01y`\\x17\\xc8\\x10/\\xda@\\\n\\x05(\\x03E%E\\x13 \\x05\\xe0\\xb8\\xde!p\\x0fl\\\nj\\x8b\\x17\\x8d\\x06PPR\\xbc\\xa6\\x82/_%9\\xe1\\\n{4\\x80\\xac\\x85\\xdf6I\\x0b\\x0f~\\xe6K\\x1b\\xbf\\xe7\\\n\\x87\\xe9.8\\xcc_I\\x0f=\\xe7m\\xfc\\x0d\\xdbOW\\\nIa/(P$\\x1c\\xd7\\xeb0(\\xb1g\\x11\\xbf\\xd3\\\n&\\x0a\\x19Kw\\x82i1\\x02\\xba1\\x02\\xba1\\x02\\xba\\\n1\\x02\\xba1\\x02\\xba1\\x02\\xba\\x99\\xf8\\xdb\\xec\\xb8\\xde\\x11\\\n\\xb0\\x0f\\xac\\xce?\\xce\\x00\\x1d\\xa0\\x06<+)~\\xc2\\x16\\\n\\x85\\x0a8\\xaeg\\x03\\x8f\\xc0\\xe9\\xec\\xb3E\\xa2\\xee\\xb8\\xde\\\n\\xb1\\x92\\xe2sTq\\x5c\\x0b]\\xa0?<\\xc06p\\x1b\\\nV\\x1c'p2\\xfb,\\xff\\xe6\\xc0q\\xbd\\xb5Q\\x85\\xc4\\\no\\xe2q\\x02/\\x0bK1\\x997%\\xc5\\xf7\\xa8\\xc28\\\n\\x81\\x1b\\xe0i>y\\x22Q\\x07\\xce\\xc3\\x8a\\xa1\\xa7\\x90\\x92\\\n\\xa2\\x03\\x9c%\\xf6\\x18\\xed\\xa1\\xa4(\\x01\\xa5\\x19\\x06\\x9b)\\\nK\\xbd\\x89\\x13\\x81\\x11\\xd0\\x8d\\x11\\xd0\\x8d\\x11\\xd0\\x8d\\x11\\xd0\\\n\\x8d\\x11\\xd0\\xcdR\\x08\\xb4u\\x87\\x98\\x82\\x96\\x8d?\\xbe\\xcf\\\n\\xf5\\xbdL\\x07\\xd3\\xc082<f\\xad\\xd8\\xf8w\\x0fr\\\n\\x13\\x16\\xc6\\x95\\xb2\\x05\\x14\\xf1\\xc7\\xf6I\\xa3\\x01\\x5c[\\xc1\\\n\\xad\\x8f\\x02\\xc9\\x92\\xe8]\\xf6h\\xa6\\x01\\xbe>\\xaa_[\\\n;\\xd9;`\\x05\\x7f\\xf0\\xbdN\\xfc\\xda\\xa8\\x05\\xbc\\x03\\x0f\\\n\\x80\\xa7\\xa4\\xa8\\x01\\xfc\\x02Q\\xab\\x5c\\x8a?\\xde\\xe3Y\\x00\\\n\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\x9e\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x08\\x15\\x0f\\xfd\\\n\\x8f\\xf8.\\x00\\x00\\x00\\x22IDAT\\x08\\xd7c`\\xc0\\\n\\x0d\\xfe\\x9f\\x87\\xb1\\x18\\x91\\x05\\x18\\x0d\\xe1BH*\\x0c\\x19\\\n\\x18\\x18\\x91\\x05\\x10*\\xd1\\x00\\x00\\xca\\xb5\\x07\\xd2v\\xbb\\xb2\\\n\\xc5\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa6\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x14\\x1d\\x00\\xb0\\\n\\xd55\\xa3\\x00\\x00\\x00*IDAT\\x08\\xd7c`\\xc0\\\n\\x06\\xfe\\x9fg``B0\\xa1\\x1c\\x08\\x93\\x81\\x81\\x09\\xc1\\\nd``b``4D\\xe2 s\\x19\\x90\\x8d@\\x02\\\n\\x00d@\\x09u\\x86\\xb3\\xad\\x9c\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\\x00\\x00\\x00\\x9e\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x09\\x00\\x00\\x00\\x06\\x08\\x04\\x00\\x00\\x00\\xbb\\xce|N\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x08\\x15\\x0f\\xfd\\\n\\x8f\\xf8.\\x00\\x00\\x00\\x22IDAT\\x08\\xd7c`\\xc0\\\n\\x0d\\xfe\\x9f\\x87\\xb1\\x18\\x91\\x05\\x18\\x0d\\xe1BH*\\x0c\\x19\\\n\\x18\\x18\\x91\\x05\\x10*\\xd1\\x00\\x00\\xca\\xb5\\x07\\xd2v\\xbb\\xb2\\\n\\xc5\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x07\\xdd\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x07\\x00\\x00\\x00\\x0a\\x08\\x06\\x00\\x00\\x00x\\xccD\\x0d\\\n\\x00\\x00\\x05RiTXtXML:com.\\\nadobe.xmp\\x00\\x00\\x00\\x00\\x00<?\\\nxpacket begin=\\x22\\xef\\\n\\xbb\\xbf\\x22 id=\\x22W5M0MpCe\\\nhiHzreSzNTczkc9d\\\n\\x22?>\\x0a<x:xmpmeta x\\\nmlns:x=\\x22adobe:ns\\\n:meta/\\x22 x:xmptk=\\\n\\x22XMP Core 5.5.0\\x22\\\n>\\x0a <rdf:RDF xmln\\\ns:rdf=\\x22http://ww\\\nw.w3.org/1999/02\\\n/22-rdf-syntax-n\\\ns#\\x22>\\x0a  <rdf:Desc\\\nription rdf:abou\\\nt=\\x22\\x22\\x0a    xmlns:d\\\nc=\\x22http://purl.o\\\nrg/dc/elements/1\\\n.1/\\x22\\x0a    xmlns:e\\\nxif=\\x22http://ns.a\\\ndobe.com/exif/1.\\\n0/\\x22\\x0a    xmlns:ti\\\nff=\\x22http://ns.ad\\\nobe.com/tiff/1.0\\\n/\\x22\\x0a    xmlns:pho\\\ntoshop=\\x22http://n\\\ns.adobe.com/phot\\\noshop/1.0/\\x22\\x0a    \\\nxmlns:xmp=\\x22http:\\\n//ns.adobe.com/x\\\nap/1.0/\\x22\\x0a    xml\\\nns:xmpMM=\\x22http:/\\\n/ns.adobe.com/xa\\\np/1.0/mm/\\x22\\x0a    x\\\nmlns:stEvt=\\x22http\\\n://ns.adobe.com/\\\nxap/1.0/sType/Re\\\nsourceEvent#\\x22\\x0a  \\\n exif:PixelXDime\\\nnsion=\\x227\\x22\\x0a   exi\\\nf:PixelYDimensio\\\nn=\\x2210\\x22\\x0a   exif:C\\\nolorSpace=\\x221\\x22\\x0a  \\\n tiff:ImageWidth\\\n=\\x227\\x22\\x0a   tiff:Ima\\\ngeLength=\\x2210\\x22\\x0a  \\\n tiff:Resolution\\\nUnit=\\x222\\x22\\x0a   tiff\\\n:XResolution=\\x2272\\\n.0\\x22\\x0a   tiff:YRes\\\nolution=\\x2272.0\\x22\\x0a \\\n  photoshop:Colo\\\nrMode=\\x223\\x22\\x0a   pho\\\ntoshop:ICCProfil\\\ne=\\x22sRGB IEC61966\\\n-2.1\\x22\\x0a   xmp:Mod\\\nifyDate=\\x222021-05\\\n-31T12:43:09+02:\\\n00\\x22\\x0a   xmp:Metad\\\nataDate=\\x222021-05\\\n-31T12:43:09+02:\\\n00\\x22>\\x0a   <dc:titl\\\ne>\\x0a    <rdf:Alt>\\\n\\x0a     <rdf:li xm\\\nl:lang=\\x22x-defaul\\\nt\\x22>branch_close<\\\n/rdf:li>\\x0a    </r\\\ndf:Alt>\\x0a   </dc:\\\ntitle>\\x0a   <xmpMM\\\n:History>\\x0a    <r\\\ndf:Seq>\\x0a     <rd\\\nf:li\\x0a      stEvt\\\n:action=\\x22produce\\\nd\\x22\\x0a      stEvt:s\\\noftwareAgent=\\x22Af\\\nfinity Designer \\\n1.9.2\\x22\\x0a      stE\\\nvt:when=\\x222021-05\\\n-31T12:43:09+02:\\\n00\\x22/>\\x0a    </rdf:\\\nSeq>\\x0a   </xmpMM:\\\nHistory>\\x0a  </rdf\\\n:Description>\\x0a <\\\n/rdf:RDF>\\x0a</x:xm\\\npmeta>\\x0a<?xpacket\\\n end=\\x22r\\x22?>X\\xad\\xf2\\x80\\x00\\x00\\\n\\x01\\x83iCCPsRGB IEC61\\\n966-2.1\\x00\\x00(\\x91u\\x91\\xcf+D\\\nQ\\x14\\xc7?fh\\xfc\\x18\\x8dba1e\\x12\\x16B\\\n\\x83\\x12\\x1b\\x8b\\x99\\x18\\x0a\\x8b\\x99Q~mf\\x9ey3\\\nj\\xdex\\xbd7\\xd2d\\xabl\\xa7(\\xb1\\xf1k\\xc1_\\\n\\xc0VY+E\\xa4d\\xa7\\xac\\x89\\x0dz\\xce\\x9bQ#\\\n\\x99s;\\xf7|\\xee\\xf7\\xdes\\xba\\xf7\\x5cpD\\xd3\\x8a\\\nfV\\xfaA\\xcbd\\x8dp(\\xe0\\x9b\\x99\\x9d\\xf3\\xb9\\x9e\\\n\\xa8\\xa2\\x85\\x1a:\\xf1\\xc6\\x14S\\x9f\\x8c\\x8cF)k\\xef\\\n\\xb7T\\xd8\\xf1\\xba\\xdb\\xaeU\\xfe\\xdc\\xbfV\\xb7\\x980\\x15\\\n\\xa8\\xa8\\x16\\x1eVt#+<&<\\xb1\\x9a\\xd5m\\xde\\\n\\x12nRR\\xb1E\\xe1\\x13\\xe1.C.(|c\\xeb\\\n\\xf1\\x22?\\xdb\\x9c,\\xf2\\xa7\\xcdF4\\x1c\\x04G\\x83\\xb0\\\n/\\xf9\\x8b\\xe3\\xbfXI\\x19\\x9a\\xb0\\xbc\\x9c6-\\xbd\\xa2\\\n\\xfc\\xdc\\xc7~\\x89;\\x91\\x99\\x8eHl\\x15\\xf7b\\x12&\\\nD\\x00\\x1f\\xe3\\x8c\\x10d\\x80^\\x86d\\x1e\\xa0\\x9b>z\\\ndE\\x99|\\x7f!\\x7f\\x8ae\\xc9Ud\\xd6\\xc9a\\xb0\\\nD\\x92\\x14Y\\xbaD]\\x91\\xea\\x09\\x89\\xaa\\xe8\\x09\\x19i\\\nrv\\xff\\xff\\xf6\\xd5T\\xfb\\xfb\\x8a\\xd5\\xdd\\x01\\xa8z\\xb4\\\n\\xac\\xd7vpm\\xc2W\\xde\\xb2>\\x0e,\\xeb\\xeb\\x10\\x9c\\\n\\x0fp\\x9e)\\xe5/\\xef\\xc3\\xe0\\x9b\\xe8\\xf9\\x92\\xd6\\xb6\\x07\\\n\\x9eu8\\xbd(i\\xf1m8\\xdb\\x80\\xe6{=f\\xc4\\\n\\x0a\\x92S\\xdc\\xa1\\xaa\\xf0r\\x0c\\xf5\\xb3\\xd0x\\x05\\xb5\\xf3\\\n\\xc5\\x9e\\xfd\\xecst\\x07\\xd15\\xf9\\xaaK\\xd8\\xd9\\x85\\x0e\\\n9\\xefY\\xf8\\x06\\x8e\\xfdg\\xf8\\xfd\\x8a\\x18\\x97\\x00\\x00\\x00\\\n\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\\n\\x9c\\x18\\x00\\x00\\x00\\xa2IDAT\\x18\\x95U\\xcf\\xb1J\\\n\\xc31\\x00\\xc4\\xe1/\\xff\\xb9\\x93\\xa3\\x93\\xb8\\xa5\\x8b\\x0f \\\nUD\\x10\\x5c:\\x84,\\x1d\\x5c|\\x0f\\xb7\\x8e>J\\x88\\\n\\xa3\\xb8\\x08m\\x05\\xbbw\\xc8\\xea\\xe2\\x0bto\\xe9\\xd2B\\\nzpp\\xf0\\xe3\\x0e.\\xa4\\xd2\\xae\\xf0\\x8a\\xf7\\x9a\\xe3V\\\n\\xa7\\x01\\xd7x\\xc32\\x95vy\\x06k\\x8e\\xdfx\\xc1\\x18\\\n\\xbf\\xa9\\xb4\\xf1\\x09\\x86SH\\xa5=\\xe2\\x03;Lk\\x8e\\\n\\xab\\xd0\\xcf\\xa4\\xd2n\\xf0\\x89\\x0b\\xdc\\x0f\\xce\\xb5?: \\\n\\x0c]\\xeb\\x01?\\x18\\xe1\\xa9\\xe6\\xb8\\x1e\\x8e`\\x86/l\\\nq[s\\x5c@H\\xa5\\xdda\\x81\\x0d\\x9ek\\x8e\\xff\\xfd\\\n\\xcf?\\xcc1\\xe9\\x01\\x1c\\x00sR-q\\xe4J\\x1bi\\\n\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x03\\xfb\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x03\\xadIDATh\\x81\\xed\\\n\\x9aO\\xa8\\x15U\\x1c\\xc7?\\xf7\\xbe\\xab\\xf2 \\xa3\\x22m\\\n\\xa1|!\\x09*K(\\xdaD\\xb9\\x88RL\\xccjQ\\\n\\xf9\\xa4\\x85\\xf1\\xa0\\x16Q\\xe4.\\x10tQD\\x8b\\x16\\x15\\\n%\\x81\\xb5(\\x04\\xad\\xc0\\xe2ad\\xf6\\x97\\xe0E\\x10\\xb4\\\n\\xa9'DE\\xc4\\x17\\xa2\\x125\\x0a*\\xff<\\xab\\xc5\\x99\\\n[\\xd7y3s\\xce\\xdcko\\xee\\x05?\\xbb9\\xf3;\\\n\\xbf\\xf3\\xfb\\x9d3\\xe7\\x9c\\xef\\x9c\\x99\\x16\\x19\\xb6/\\x06v\\\n\\x00\\xab\\x81\\xab\\x81\\x05\\x0c\\x17\\xa7\\x80\\x19`\\x1axL\\xd2\\\n\\x11\\x80\\x16\\x80\\xed\\x9b\\x81\\xbd\\xc0\\xd2\\xc6\\xc2\\xab\\xc7a`\\\n\\xb3\\xa4\\x0f[Y\\xcf\\x1fbt\\x82\\xefr\\x18\\xb8\\xaaM\\\nxlF-x\\x081o\\xef\\x10\\x9e\\xf9<\\xa7\\xe79\\\n\\x98T\\xc6r\\xd7\\xab;\\x84\\x09\\xdb\\xcbiI\\x9dy\\x0a\\\n\\xa8\\x16\\xb6g93\\x89Um\\x86o\\xb5\\xa9\\xc3\\x82v\\\n\\xd3\\x11\\x0c\\xca\\xb9\\x04\\x9a\\xe6\\x5c\\x02\\xff\\x07\\xb6\\xef\\xb6\\xbd\\\n\\xd7\\xf6\\xda\\x98\\xed\\xd0%`\\xfb\\x11\\xe0u`\\x028h\\\n{\\xa7\\xed\\xd2\\x95r\\xa8\\x12\\xb0=\\x01<\\xddS\\xd4\\x02\\\n\\x1e\\x04^,\\xab34\\x1b\\x96\\xed5\\xc0+d\\x023\\\n\\xc7\\x16\\xdb_\\x16\\xd5\\x1b\\x8a\\x11\\xb0}\\x1d\\xf0\\x06\\xb0\\xb0\\\n\\xc2l}Qa\\xe3\\x09\\xd8\\xbe\\x0cx\\x1bX\\x1c1}\\\n\\xa9\\xa8\\xb0\\xd1\\x04l_\\x02\\x1c$\\xae\\x86\\x9f\\x92\\xf4j\\\n\\xd1\\x8d\\xc6\\x12\\xb0\\xbd\\x188\\x00\\xac\\x88\\x98\\xee\\x06\\x1e-\\\n\\xbb\\xd9H\\x02\\xb6\\x17\\x02o\\x02\\xd7FL\\xdf\\x01&%\\\n\\xfd]f0\\xef\\x09\\xd8n\\x13z\\xf5\\x96\\x88\\xe9g\\xc0\\\n]\\x92f\\xab\\x8c\\x9a\\x18\\x81g\\x80{\\x226_\\x03\\x1b\\\n$\\xfd\\x1es\\x96\\x94\\x80\\xedV\\xd6s\\x03a{\\x1b\\xf0\\\np\\xc4\\xecG`]\\xf7\\xd4!FePY\\xe0[\\x81\\\n?\\x81\\x19\\xdb\\xab\\x92\\x22-\\xf65\\x09<\\x111\\xfb\\x15\\\n\\xb8U\\xd2\\xf7\\xa9~K\\x13\\xc8\\xf4\\xc7\\x14ak_\\x04\\\n\\x5c\\x09\\xbco\\xfb\\xf2T\\xe7=\\xbe6\\x02\\xbb\\x22f'\\\n\\x80;$}Q\\xc7w\\xd5\\x08<\\x0fl\\xcc\\x95-\\x05\\\n>\\xb0}ij\\x03\\xb6o\\x00^c\\xee\\x0by/\\x7f\\\n\\x01\\xf7J\\xfa8\\xd5o\\x97\\xc2\\x04\\xb2\\x1e{\\xa0\\xa4\\xce\\\n2\\xc2H,\\x8b9\\xb7\\xbd\\x12\\xd8\\x0f\\x8cGL\\x1f\\x92\\\n\\xb4/\\xe6\\xaf\\x88\\xb2\\x11\\xb8(Ro\\x05!\\x89%e\\\n\\x06\\xb6\\x97\\x13v\\xd9\\x98\\xaf\\xc7%\\xbd\\x10\\xb1)\\xa5,\\\n\\x81=\\x84u\\xb8\\x8a+\\x80wm_\\x90\\xbfa\\xfbB\\\nB\\xf0\\xcb#>vI\\xda\\x11\\x8d\\xb2\\x82\\xc2\\x04$\\x9d\\\n\\x22\\xa8\\xbfB\\x09\\xdb\\xc35\\xc0\\x01\\xdb\\xe7u\\x0bl\\x8f\\\n\\x13\\x1e\\x9b\\x95\\x91\\xbaS\\x04\\xad?\\x10\\xa5\\x93X\\xd21\\\n`-\\xf0M\\xc4\\xc7\\xf5\\xc0~\\xdb\\xe3\\xb6\\xc7\\x08\\x13\\xf6\\\n\\xc6H\\x9di`B\\xd2\\xc0'\\x80\\x95\\xfb\\x80\\xa4\\x9f\\x81\\\n5\\x80#~n\\x02\\xf6\\x11\\x96\\xca\\xfc\\xca\\x95g\\x06\\xb8\\\n]\\xd2\\xf1\\xc4\\x18+\\x89\\xee\\xae\\x92L\\xd0-?EL\\\n\\xd7\\x03\\x93\\x11\\x1b\\x136\\xaa_\\xd2\\xc2\\x8b\\x93$\\x0f$\\\n}K\\x18\\x89\\xa3\\x03\\xb4u\\x8c \\x11~\\x18\\xc0\\xc7\\x1c\\\n\\x92\\xf5\\x8d\\xa4C\\xc0:\\xe0\\xb7>\\xda\\xf9\\x03\\xb8M\\xd2\\\nW}\\xd4\\xad\\xa4\\x96@\\x93\\xf49\\xb0!\\x0b(\\x95Y\\\n`\\x93\\xa4O\\xeb\\xb4\\x95Jm\\x85)i\\x1a\\xb8\\x93\\xa0\\\n]R\\xb8_\\xd2[u\\xdbI\\xa5/\\x89,\\xe9=`\\\n\\x13\\xa1w\\xab\\xd8&\\xe9\\xe5~\\xdaH\\xa5o\\x8d/i\\\n\\x0a\\xd8B\\x10bE<'\\xe9\\xc9~\\xfd\\xa72\\xd0K\\\n\\x8a\\xa4=\\xc0}\\xcc\\x9d\\x13;\\x81\\xad\\x83\\xf8Ne\\xe0\\\n\\x939I\\xbbm\\x7fB\\xd8\\xc0\\xce\\x07>\\xca\\xe6\\xc9\\xbc\\\npV\\x8e\\x16%}\\x07<{6|\\xd5\\xa5M\\xf8\\x02\\\n>\\xaa\\x9c\\xec\\x10\\xb4I\\xef\\xf9\\xccX\\xf65p\\x18\\xc9\\\n\\xbf\\xd5\\xcdt\\x08\\xca0\\x7f\\xc0T\\xf5\\xfa7LL\\x8f\\\n\\xfe\\xaf\\x06\\xd9\\xf9\\xcb\\xe6\\xac`T\\xe8\\xfe\\xecq\\xe4\\xdf\\\n\\x8f\\x09\\xd9Hl\\xe7\\xbf\\xdfm\\xaa\\xce\\xea\\x9b\\xe0$g\\\n\\xfens\\x14\\xe0\\x1f\\x0aC\\x12kO\\xfd?\\x13\\x00\\x00\\\n\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x00\\xa0\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x00\\x06\\x00\\x00\\x00\\x09\\x08\\x04\\x00\\x00\\x00\\xbb\\x93\\x95\\x16\\\n\\x00\\x00\\x00\\x01sRGB\\x00\\xae\\xce\\x1c\\xe9\\x00\\x00\\x00\\\n\\x02bKGD\\x00\\xff\\x87\\x8f\\xcc\\xbf\\x00\\x00\\x00\\x09p\\\nHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\x01\\x00\\x9a\\x9c\\x18\\\n\\x00\\x00\\x00\\x07tIME\\x07\\xdc\\x08\\x17\\x14\\x1f\\x0d\\xfc\\\nR+\\x9c\\x00\\x00\\x00$IDAT\\x08\\xd7c`@\\\n\\x05s>\\xc0XL\\xc8\\x5c&dY&d\\xc5pN\\\n\\x8a\\x00\\x9c\\x93\\x22\\x80a\\x1a\\x0a\\x00\\x00)\\x95\\x08\\xaf\\x88\\\n\\xac\\xba4\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\x00\\x00\\x01\\xe1\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x93IDATh\\x81\\xed\\\n\\x9a;N\\xc3@\\x10\\x86\\xbfq\\x1c*\\xe8\\x10\\xe56\\x94\\\n\\xd0\\xa4\\xa1\\x8a(\\x22\\x0a\\x0aD\\x9f\\x9e\\x0bp\\x80Pp\\\n\\x01.\\xc0\\x15h\\x80\\x13\\xa0\\x1c!P\\x91f\\xbbD\\xa1\\\nB4yh(l\\x1e\\xb1\\xfcH\\x08\\xc9\\xda\\xd2~\\x9d\\\nw\\x5c\\xfc\\x9f\\xb3\\x1e9\\xda\\x11bTu\\x17\\xb8\\x02\\x9a\\\n\\xc0!P\\xa7\\x5cL\\x80\\x1e\\xd0\\x05\\xaeEd\\xf4]Q\\\n\\xd5\\x96\\xaa\\x0e\\xb4:\\x0cT\\xb5\\x05 \\x1a=\\xf9g`\\\n\\xcf\\xc5c]\\x81!p\\x10\\x10m\\x9b\\xaa\\x85\\x87(s\\\n'$\\xda\\xf3If\\x1b\\x0e\\xb3(\\xb5\\xc4uSTu\\\n\\xcc\\xfc\\x0b;\\x13\\x91p\\x83\\xa1\\x16FU\\xa7\\xccKL\\\n\\x02\\xca\\xd7m\\x96\\xa1\\x1e\\xb8N\\xb0*^\\xc05^\\xc0\\\n5^\\xc05^\\xc05^\\xc05^\\xc05\\x85\\x9f\\xcd\\\n\\xd6\\xda\\x13\\xe0\\x08\\xd8^\\x7f\\x9c9\\xa6\\xc0\\x0b\\xf0`\\x8c\\\n\\xf9\\xc8\\xbaITU\\x13k3\\x11\\x09\\xad\\xb5!p\\x07\\\n\\x9c\\xaf1\\xe4\\x22\\xf4\\x81Sc\\xcck\\xca\\xff\\x81\\xdc-\\\nt\\x89\\xfb\\xf0\\x00\\xfb\\xc0mV1O\\xe0\\xec\\xff\\xb3\\xfc\\\n\\x99ck\\xedNZ\\xa1\\xf2/q\\x9e\\xc0\\xe3\\xc6R\\x14\\\n\\xf3d\\x8cyO+\\xe4\\x09\\xdc\\x00\\xf7\\xeb\\xc9\\xb3\\x14}\\\n\\xe0\\x22\\xab\\x98\\xd9\\x85\\xbe.\\xca\\xd4F\\xd3\\xbaP\\xa1@\\\n\\x99X\\xb6\\x8dV\\x02/\\xe0\\x1a/\\xe0\\x1a/\\xe0\\x1a/\\\n\\xe0\\x1a/\\xe0\\x1a/\\xe0\\x9a\\x80\\xe8\\x04\\xbc\\xaa\\x8cC\\xa2\\\n\\xe3\\xfb\\xc6\\xaf\\xc5Z\\xfc\\xd9ZF\\x92\\xc7\\xac\\xbd\\x90h\\\n\\xf6\\xa0QpcY\\xe9V\\x7f\\xd4 \\x9e\\xfah\\xc7\\x0b\\\nUa\\x08\\xb4Ed$_+\\xf1/\\xd1\\xe1g\\xdcf\\\n\\xcbQ\\xb8,\\xc6\\xcc\\x8f\\xdb\\xbc\\x01|\\x02mw#\\xb3\\\n\\xd4\\x95Sv\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\\\n\\\n\\x00\\x00\\x01W\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01\\x09IDATh\\x81\\xed\\\n\\xda\\xcdm\\xc2@\\x14E\\xe1\\xf3\\x8cI\\x05Q\\x9aH6\\\n\\xecY\\xd1\\x05\\xc5\\x90Ej\\xa3\\x04R\\x04\\x884`\\x82\\\nn\\x163\\xf9\\xb1\\xa5(DH\\x5c[z\\xdf\\x8e\\xc1\\x8b\\\nw\\x8c\\xcdf&\\xa8$\\xdd\\x03\\xcf\\xc0\\x12x\\x02\\xe6\\x8c\\\n\\xcb\\x09\\xd8\\x01[\\xe0%\\x22\\x8e_\\xdfHZI\\xdak\\\n:\\xf6\\x92V\\x00\\xa1r\\xe7_\\x81\\x07\\xc7m\\xbd\\xc2\\x01\\\nxl(\\x8f\\xcd\\xd4\\x86\\x872\\xf3\\xa6\\xa5<\\xf3C\\xe7\\\n\\x1b\\x0fs\\xa9\\xd9\\xe0\\xf32$u\\xf4_\\xd8sD\\xb4\\\n7\\x1c\\xeab\\x92\\xde\\xe9G\\x9c\\x1a\\xc6\\xf7o\\xf3\\x1f\\xf3\\\n\\xc6=\\xc1\\xb52\\xc0-\\x03\\xdc2\\xc0-\\x03\\xdc2\\xc0\\\n-\\x03\\xdc2\\xc0-\\x03\\xdc2\\xc0-\\x03\\xdc2\\xc0-\\\n\\x03\\xdc2\\xc0-\\x03\\xdc2\\xc0-\\x03\\xdc2\\xc0\\xad\\xa1\\\n\\xec\\x80OU\\xd7R\\xb6\\xef\\x17?\\x16gu7p\\x8c\\\n\\x86\\xdb\\xac\\xbb\\x96r\\xf6`\\xf1\\xc7\\x85c\\xb5\\x9d\\xfeQ\\\n\\x83z\\xeac]\\x17\\xa6\\xe2\\x00\\xac#\\xe2\\x18\\x9f+\\xf5\\\n\\x97\\xd8\\xf0}\\xdc\\xe6\\xce4\\xdco:\\xfa\\xc7m\\xde\\x00\\\n>\\x00G\\xd7\\xea\\xb1\\xadi\\xe1\\xd6\\x00\\x00\\x00\\x00IE\\\nND\\xaeB`\\x82\\\n\\x00\\x00\\x01v\\\n\\x89\\\nPNG\\x0d\\x0a\\x1a\\x0a\\x00\\x00\\x00\\x0dIHDR\\x00\\\n\\x00\\x000\\x00\\x00\\x000\\x08\\x06\\x00\\x00\\x00W\\x02\\xf9\\x87\\\n\\x00\\x00\\x00\\x09pHYs\\x00\\x00\\x0b\\x13\\x00\\x00\\x0b\\x13\\\n\\x01\\x00\\x9a\\x9c\\x18\\x00\\x00\\x01(IDATh\\x81\\xed\\\n\\xda\\xb1J\\xc3P\\x14\\x87\\xf1/7\\xb7\\xe0\\xae\\xf8\\x00\\x82\\\nSu\\xe8\\xde\\xc9ly\\x80@\\x1fF\\x87\\xfa\\x22nB\\\n\\xdc\\xb3\\xc5\\xa9/ \\xb4]:t\\x0f}\\x82j\\xc1\\xe1\\\n\\xa6P\\xb3h\\x10\\xfa\\xcf\\x85\\xf3\\xdbR:\\x9c\\xaf\\xdcf\\\n97\\xa1\\x95\\xe5\\xc5\\x15\\xf0\\x04L\\x81;`\\xc4\\xb0|\\\n\\x02K`\\x01\\xcc\\xeb\\xaa\\xdc\\x01$\\x00Y^<\\x00\\xaf\\\n\\xc0\\xb5l\\xbc~\\x1a`VW\\xe5{\\xd2\\xfe\\xf2+\\xe2\\\n\\x19\\xfe\\xa8\\x01\\xc6\\x8eplb\\x1b\\x1e\\xc2\\xcc\\x8f\\x9ep\\\n\\xe6\\xbb\\x0eg\\x1e\\xe6\\xaf\\xd2\\xce\\xf3\\xd4\\x13\\xfe\\xb0\\xa7\\x0e\\\nuU\\xfa3\\x0d\\xd4K\\x96\\x17_\\xfc\\x8c\\xb8w\\x0c\\xef\\\nm\\xd3\\xc7\\xc8\\xa9'\\xf8/\\x0bP\\xb3\\x005\\x0bP\\xb3\\\n\\x005\\x0bP\\xb3\\x005\\x0bP\\xb3\\x005\\x0bP\\xb3\\x00\\\n5\\x0bP\\xb3\\x005\\x0bP\\xb3\\x005\\x0bP\\xb3\\x005\\\n\\x0bPs\\x84\\x0dx\\xac\\xf6\\x9e\\xb0\\xbe\\x9f\\x9c|\\x98\\xb6\\\n\\xdb\\xc0!\\xea\\xaeY\\x97\\x9ep\\xf7`\\xf2\\xcb\\x17\\x87j\\\n\\xe1\\x809am\\x1f\\x9b\\x06xv\\xed\\xad\\x8f\\x19qE\\\n\\x1c/{\\xecR\\x80\\xedf\\xb5\\xbd\\xb9\\x1d\\xbf\\x00\\x17\\x84\\\n\\xc5\\xf7%\\xc3;F{\\xe0\\x03x\\x03\\x8a\\xba*\\xd7\\x00\\\n\\xdf\\xa4\\xb56\\xa2\\xca\\x99tG\\x00\\x00\\x00\\x00IEN\\\nD\\xaeB`\\x82\\\n\"\n\nqt_resource_name = b\"\\\n\\x00\\x08\\\n\\x06\\xc5\\x8e\\xa5\\\n\\x00o\\\n\\x00p\\x00e\\x00n\\x00p\\x00y\\x00p\\x00e\\\n\\x00\\x06\\\n\\x07\\x03}\\xc3\\\n\\x00i\\\n\\x00m\\x00a\\x00g\\x00e\\x00s\\\n\\x00\\x17\\\n\\x0ce\\xce\\x07\\\n\\x00l\\\n\\x00e\\x00f\\x00t\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00d\\x00i\\x00s\\x00a\\x00b\\x00l\\\n\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x1a\\\n\\x03\\x0e\\xe4\\x87\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\x00d\\x00_\\\n\\x00h\\x00o\\x00v\\x00e\\x00r\\x00.\\x00p\\x00n\\x00g\\\n\\x00 \\\n\\x0f\\xd4\\x1b\\xc7\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00i\\x00n\\x00d\\x00e\\x00t\\x00e\\x00r\\x00m\\\n\\x00i\\x00n\\x00a\\x00t\\x00e\\x00_\\x00h\\x00o\\x00v\\x00e\\x00r\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x15\\\n\\x03'rg\\\n\\x00c\\\n\\x00o\\x00m\\x00b\\x00o\\x00b\\x00o\\x00x\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00o\\x00n\\\n\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x11\\\n\\x01\\x1f\\xc3\\x87\\\n\\x00d\\\n\\x00o\\x00w\\x00n\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00o\\x00n\\x00.\\x00p\\x00n\\x00g\\\n\\\n\\x00\\x0e\\\n\\x04\\xa2\\xfc\\xa7\\\n\\x00d\\\n\\x00o\\x00w\\x00n\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x1b\\\n\\x03Z2'\\\n\\x00c\\\n\\x00o\\x00m\\x00b\\x00o\\x00b\\x00o\\x00x\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00d\\x00i\\\n\\x00s\\x00a\\x00b\\x00l\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x0f\\\n\\x02\\x9f\\x05\\x87\\\n\\x00r\\\n\\x00i\\x00g\\x00h\\x00t\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x12\\\n\\x01.\\x03'\\\n\\x00c\\\n\\x00o\\x00m\\x00b\\x00o\\x00b\\x00o\\x00x\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00.\\x00p\\x00n\\\n\\x00g\\\n\\x00\\x1c\\\n\\x0e<\\xde\\x07\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00u\\x00n\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\\n\\x00d\\x00_\\x00h\\x00o\\x00v\\x00e\\x00r\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x0f\\\n\\x06S%\\xa7\\\n\\x00b\\\n\\x00r\\x00a\\x00n\\x00c\\x00h\\x00_\\x00o\\x00p\\x00e\\x00n\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x0e\\\n\\x0e\\xde\\xfa\\xc7\\\n\\x00l\\\n\\x00e\\x00f\\x00t\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x11\\\n\\x0b\\xda0\\xa7\\\n\\x00b\\\n\\x00r\\x00a\\x00n\\x00c\\x00h\\x00_\\x00c\\x00l\\x00o\\x00s\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\\n\\x00\\x15\\\n\\x0f\\xf3\\xc0\\x07\\\n\\x00u\\\n\\x00p\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00d\\x00i\\x00s\\x00a\\x00b\\x00l\\x00e\\x00d\\\n\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x12\\\n\\x05\\x8f\\x9d\\x07\\\n\\x00b\\\n\\x00r\\x00a\\x00n\\x00c\\x00h\\x00_\\x00o\\x00p\\x00e\\x00n\\x00_\\x00o\\x00n\\x00.\\x00p\\x00n\\\n\\x00g\\\n\\x00\\x1a\\\n\\x05\\x11\\xe0\\xe7\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\x00d\\x00_\\\n\\x00f\\x00o\\x00c\\x00u\\x00s\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x16\\\n\\x01u\\xcc\\x87\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00u\\x00n\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\\n\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x0f\\\n\\x0c\\xe2hg\\\n\\x00t\\\n\\x00r\\x00a\\x00n\\x00s\\x00p\\x00a\\x00r\\x00e\\x00n\\x00t\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x17\\\n\\x0c\\xabQ\\x07\\\n\\x00d\\\n\\x00o\\x00w\\x00n\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00d\\x00i\\x00s\\x00a\\x00b\\x00l\\\n\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x1d\\\n\\x09\\x07\\x81\\x07\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\x00d\\x00_\\\n\\x00d\\x00i\\x00s\\x00a\\x00b\\x00l\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x12\\\n\\x03\\x8d\\x04G\\\n\\x00r\\\n\\x00i\\x00g\\x00h\\x00t\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00o\\x00n\\x00.\\x00p\\x00n\\\n\\x00g\\\n\\x00\\x1a\\\n\\x01\\x87\\xaeg\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00i\\x00n\\x00d\\x00e\\x00t\\x00e\\x00r\\x00m\\\n\\x00i\\x00n\\x00a\\x00t\\x00e\\x00.\\x00p\\x00n\\x00g\\\n\\x00#\\\n\\x06\\xf2\\x1aG\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00i\\x00n\\x00d\\x00e\\x00t\\x00e\\x00r\\x00m\\\n\\x00i\\x00n\\x00a\\x00t\\x00e\\x00_\\x00d\\x00i\\x00s\\x00a\\x00b\\x00l\\x00e\\x00d\\x00.\\x00p\\\n\\x00n\\x00g\\\n\\x00\\x0c\\\n\\x06\\xe6\\xe6g\\\n\\x00u\\\n\\x00p\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x11\\\n\\x00\\xb8\\x8c\\x07\\\n\\x00l\\\n\\x00e\\x00f\\x00t\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00o\\x00n\\x00.\\x00p\\x00n\\x00g\\\n\\\n\\x00\\x0f\\\n\\x01s\\x8b\\x07\\\n\\x00u\\\n\\x00p\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00o\\x00n\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x14\\\n\\x04^-\\xa7\\\n\\x00b\\\n\\x00r\\x00a\\x00n\\x00c\\x00h\\x00_\\x00c\\x00l\\x00o\\x00s\\x00e\\x00d\\x00_\\x00o\\x00n\\x00.\\\n\\x00p\\x00n\\x00g\\\n\\x00\\x14\\\n\\x07\\xec\\xd1\\xc7\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\x00d\\x00.\\\n\\x00p\\x00n\\x00g\\\n\\x00\\x18\\\n\\x03\\x8e\\xdeg\\\n\\x00r\\\n\\x00i\\x00g\\x00h\\x00t\\x00_\\x00a\\x00r\\x00r\\x00o\\x00w\\x00_\\x00d\\x00i\\x00s\\x00a\\x00b\\\n\\x00l\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\\x00 \\\n\\x09\\xd7\\x1f\\xa7\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00i\\x00n\\x00d\\x00e\\x00t\\x00e\\x00r\\x00m\\\n\\x00i\\x00n\\x00a\\x00t\\x00e\\x00_\\x00f\\x00o\\x00c\\x00u\\x00s\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x1c\\\n\\x08?\\xdag\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00u\\x00n\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\\n\\x00d\\x00_\\x00f\\x00o\\x00c\\x00u\\x00s\\x00.\\x00p\\x00n\\x00g\\\n\\x00\\x1f\\\n\\x0a\\xae'G\\\n\\x00c\\\n\\x00h\\x00e\\x00c\\x00k\\x00b\\x00o\\x00x\\x00_\\x00u\\x00n\\x00c\\x00h\\x00e\\x00c\\x00k\\x00e\\\n\\x00d\\x00_\\x00d\\x00i\\x00s\\x00a\\x00b\\x00l\\x00e\\x00d\\x00.\\x00p\\x00n\\x00g\\\n\"\n\nqt_resource_struct = b\"\\\n\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\\n\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\\n\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x02\\\n\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\\n\\x00\\x00\\x00\\x16\\x00\\x02\\x00\\x00\\x00 \\x00\\x00\\x00\\x03\\\n\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\\n\\x00\\x00\\x04\\xb8\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x008:\\\n\\x00\\x00\\x01{\\xe9xF\\xdd\\\n\\x00\\x00\\x01\\x0c\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x07]\\\n\\x00\\x00\\x01{\\xe9xF\\xdb\\\n\\x00\\x00\\x01\\xb6\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x09\\xfc\\\n\\x00\\x00\\x01{\\xe9xF\\xd9\\\n\\x00\\x00\\x04\\xe0\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x008\\xe4\\\n\\x00\\x00\\x01{\\xe9xF\\xe0\\\n\\x00\\x00\\x03 \\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00'R\\\n\\x00\\x00\\x01}\\x0f$Y\\x81\\\n\\x00\\x00\\x04\\x14\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x003\\xb8\\\n\\x00\\x00\\x01}\\x0f$Y~\\\n\\x00\\x00\\x01\\x92\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x09X\\\n\\x00\\x00\\x01{\\xe9xF\\xdd\\\n\\x00\\x00\\x00\\x5c\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\xaa\\\n\\x00\\x00\\x01}\\x0f$Y~\\\n\\x00\\x00\\x00\\xdc\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x06\\xb3\\\n\\x00\\x00\\x01{\\xe9xF\\xda\\\n\\x00\\x00\\x01V\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x08\\xaf\\\n\\x00\\x00\\x01{\\xe9xF\\xd9\\\n\\x00\\x00\\x03\\xea\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x003\\x14\\\n\\x00\\x00\\x01{\\xe9xF\\xde\\\n\\x00\\x00\\x05`\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00Ef\\\n\\x00\\x00\\x01{\\xe9xF\\xde\\\n\\x00\\x00\\x05\\x04\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x009\\x86\\\n\\x00\\x00\\x01{\\xe9xF\\xd7\\\n\\x00\\x00\\x014\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x08\\x06\\\n\\x00\\x00\\x01{\\xe9xF\\xda\\\n\\x00\\x00\\x02\\xe6\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00#O\\\n\\x00\\x00\\x01}\\x0f$Y}\\\n\\x00\\x00\\x02\\xbc\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1c\\x1b\\\n\\x00\\x00\\x01{\\xe9xF\\xd8\\\n\\x00\\x00\\x02\\x1e\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x0c\\x13\\\n\\x00\\x00\\x01{\\xe9xF\\xd8\\\n\\x00\\x00\\x04\\x9a\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x007\\x98\\\n\\x00\\x00\\x01{\\xe9xF\\xdf\\\n\\x00\\x00\\x04N\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x005\\x98\\\n\\x00\\x00\\x01}\\x0f$Y\\x7f\\\n\\x00\\x00\\x052\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00Ag\\\n\\x00\\x00\\x01}\\x0f$Y|\\\n\\x00\\x00\\x05\\xdc\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00G\\xef\\\n\\x00\\x00\\x01}\\x0f$Y\\x82\\\n\\x00\\x00\\x03\\xaa\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00.\\xdd\\\n\\x00\\x00\\x01}\\x0f$Y}\\\n\\x00\\x00\\x05\\x96\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00F\\x0a\\\n\\x00\\x00\\x01}\\x0f$Y\\x80\\\n\\x00\\x00\\x06\\x1a\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00IJ\\\n\\x00\\x00\\x01}\\x0f$Y\\x81\\\n\\x00\\x00\\x02d\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x13\\xc7\\\n\\x00\\x00\\x01{\\xe9xF\\xd7\\\n\\x00\\x00\\x00(\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\\n\\x00\\x00\\x01{\\xe9xF\\xdc\\\n\\x00\\x00\\x03v\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00.3\\\n\\x00\\x00\\x01{\\xe9xF\\xdb\\\n\\x00\\x00\\x03R\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00(\\xb1\\\n\\x00\\x00\\x01}\\x0f$k\\xb6\\\n\\x00\\x00\\x01\\xe0\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x0a\\xa6\\\n\\x00\\x00\\x01}\\x0f$Y\\x82\\\n\\x00\\x00\\x02B\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x13\\x1d\\\n\\x00\\x00\\x01{\\xe9xF\\xdc\\\n\\x00\\x00\\x00\\x96\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x04\\xc0\\\n\\x00\\x00\\x01}\\x0f$Y\\x80\\\n\\x00\\x00\\x02\\x8c\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x1bx\\\n\\x00\\x00\\x01{\\xe9xF\\xdf\\\n\"\n\n\ndef qInitResources():\n    QtCore.qRegisterResourceData(\n        0x03, qt_resource_struct, qt_resource_name, qt_resource_data)\n\n\ndef qCleanupResources():\n    QtCore.qUnregisterResourceData(\n        0x03, qt_resource_struct, qt_resource_name, qt_resource_data)\n"
  },
  {
    "path": "openpype/style/qrc_resources.py",
    "content": "import qtpy\n\n\ninitialized = False\nresources = None\nif qtpy.API == \"pyside6\":\n    from . import pyside6_resources as resources\nelif qtpy.API == \"pyside2\":\n    from . import pyside2_resources as resources\nelif qtpy.API == \"pyqt5\":\n    from . import pyqt5_resources as resources\n\n\ndef qInitResources():\n    global resources\n    global initialized\n    if resources is not None and not initialized:\n        initialized = True\n        resources.qInitResources()\n\n\ndef qCleanupResources():\n    global resources\n    global initialized\n    if resources is not None:\n        initialized = False\n        resources.qCleanupResources()\n\n\n__all__ = (\n    \"resources\",\n    \"qInitResources\",\n    \"qCleanupResources\"\n)\n"
  },
  {
    "path": "openpype/style/resources.qrc",
    "content": "<RCC>\n    <qresource prefix=\"/openpype\">\n        <file>images/combobox_arrow.png</file>\n        <file>images/combobox_arrow_disabled.png</file>\n        <file>images/branch_closed.png</file>\n        <file>images/branch_closed_on.png</file>\n        <file>images/branch_open.png</file>\n        <file>images/branch_open_on.png</file>\n        <file>images/combobox_arrow_on.png</file>\n        <file>images/down_arrow.png</file>\n        <file>images/down_arrow_disabled.png</file>\n        <file>images/down_arrow_on.png</file>\n        <file>images/left_arrow.png</file>\n        <file>images/left_arrow_disabled.png</file>\n        <file>images/left_arrow_on.png</file>\n        <file>images/right_arrow.png</file>\n        <file>images/right_arrow_disabled.png</file>\n        <file>images/right_arrow_on.png</file>\n        <file>images/up_arrow.png</file>\n        <file>images/up_arrow_disabled.png</file>\n        <file>images/up_arrow_on.png</file>\n        <file>images/checkbox_checked.png</file>\n        <file>images/checkbox_checked_hover.png</file>\n        <file>images/checkbox_checked_focus.png</file>\n        <file>images/checkbox_checked_disabled.png</file>\n        <file>images/checkbox_unchecked.png</file>\n        <file>images/checkbox_unchecked_hover.png</file>\n        <file>images/checkbox_unchecked_focus.png</file>\n        <file>images/checkbox_unchecked_disabled.png</file>\n        <file>images/checkbox_indeterminate.png</file>\n        <file>images/checkbox_indeterminate_hover.png</file>\n        <file>images/checkbox_indeterminate_focus.png</file>\n        <file>images/checkbox_indeterminate_disabled.png</file>\n        <file>images/transparent.png</file>\n    </qresource>\n</RCC>\n"
  },
  {
    "path": "openpype/style/style.css",
    "content": "/*\nEnabled vs Disabled logic in most of stylesheets\n\n- global font color\n    Enabled - should be same globalle except placeholders\n    Disabled - font color is greyed out\n\n- global active/hover\n   Enabled - color motive of borders and bg color\n       - combobox, slider, views, buttons, checkbox, radiobox, inputs\n\n- QLineEdit, QTextEdit, QPlainTextEdit, QAbstractSpinBox\n    Enabled - bg has lighter or darked color\n    Disabled - bg has same color as background\n\n- QComboBox, QPushButton, QToolButton\n    Enabled - slightly lighter color\n    Disabled - even lighter color\n*/\n\n* {\n    font-size: 10pt;\n    font-family: \"Noto Sans\";\n    font-weight: 450;\n    outline: none;\n}\n\nQWidget {\n    color: {color:font};\n    background: {color:bg};\n    border-radius: 0px;\n}\n\nQWidget:disabled {\n    color: {color:font-disabled};\n}\n\n/* Some DCCs have set borders to solid color */\nQScrollArea {\n    border: none;\n}\n\nQLabel {\n    background: transparent;\n}\n\n/* Inputs */\nQAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit {\n    border: 1px solid {color:border};\n    border-radius: 0.2em;\n    background: {color:bg-inputs};\n    padding: 0.1em;\n}\n\nQAbstractSpinBox:disabled, QLineEdit:disabled, QPlainTextEdit:disabled, QTextEdit:disabled {\n    background: {color:bg-inputs-disabled};\n}\nQAbstractSpinBox:hover, QLineEdit:hover, QPlainTextEdit:hover, QTextEdit:hover{\n    border-color: {color:border-hover};\n}\nQAbstractSpinBox:focus, QLineEdit:focus, QPlainTextEdit:focus, QTextEdit:focus{\n    border-color: {color:border-focus};\n}\n\nQAbstractSpinBox:up-button {\n    margin: 0px;\n    background-color: transparent;\n    subcontrol-origin: border;\n    subcontrol-position: top right;\n    border-top-right-radius: 0.3em;\n    border-top: 0px solid transparent;\n    border-right: 0px solid transparent;\n    border-left: 1px solid {color:border};\n    border-bottom: 1px solid {color:border};\n}\n\nQAbstractSpinBox:down-button {\n    margin: 0px;\n    background-color: transparent;\n    subcontrol-origin: border;\n    subcontrol-position: bottom right;\n    border-bottom-right-radius: 0.3em;\n    border-bottom: 0px solid transparent;\n    border-right: 0px solid transparent;\n    border-left: 1px solid {color:border};\n    border-top: 1px solid {color:border};\n}\n\nQAbstractSpinBox:up-button:focus, QAbstractSpinBox:down-button:focus {\n    border-color: {color:border-focus};\n}\nQAbstractSpinBox::up-arrow, QAbstractSpinBox::up-arrow:off {\n    image: url(:/openpype/images/up_arrow.png);\n    width: 0.5em;\n    height: 1em;\n    border-width: 1px;\n}\nQAbstractSpinBox::up-arrow:hover {\n    image: url(:/openpype/images/up_arrow_on.png);\n    bottom: 1;\n}\nQAbstractSpinBox::up-arrow:disabled {\n    image: url(:/openpype/images/up_arrow_disabled.png);\n}\nQAbstractSpinBox::up-arrow:pressed {\n    image: url(:/openpype/images/up_arrow_on.png);\n    bottom: 0;\n}\n\nQAbstractSpinBox::down-arrow, QAbstractSpinBox::down-arrow:off {\n    image: url(:/openpype/images/down_arrow.png);\n    width: 0.5em;\n    height: 1em;\n    border-width: 1px;\n}\nQAbstractSpinBox::down-arrow:hover {\n    image: url(:/openpype/images/down_arrow_on.png);\n    bottom: 1;\n}\nQAbstractSpinBox::down-arrow:disabled {\n    image: url(:/openpype/images/down_arrow_disabled.png);\n}\nQAbstractSpinBox::down-arrow:hover:pressed {\n    image: url(:/openpype/images/down_arrow_on.png);\n    bottom: 0;\n}\n\n/* Buttons */\nQPushButton {\n    text-align:center center;\n    border: 0px solid transparent;\n    border-radius: 0.2em;\n    padding: 3px 5px 3px 5px;\n    background: {color:bg-buttons};\n    min-width: 0px; /* Substance Painter fix */\n}\n\nQPushButton:hover {\n    background: {color:bg-buttons-hover};\n    color: {color:font-hover};\n}\n\nQPushButton:pressed {}\n\nQPushButton:disabled {\n    background: {color:bg-buttons-disabled};\n}\n\nQPushButton::menu-indicator  {\n    subcontrol-origin: padding;\n    subcontrol-position: right;\n    width: 8px;\n    height: 8px;\n    padding-right: 5px;\n}\n\nQPushButton[state=\"error\"] {\n    background: {color:publisher:error};\n}\n\nQToolButton {\n    border: 0px solid transparent;\n    background: {color:bg-buttons};\n    border-radius: 0.2em;\n    padding: 2px;\n}\n\nQToolButton:hover {\n    background: {color:bg-buttons-hover};\n    color: {color:font-hover};\n}\n\nQToolButton:disabled {\n    background: {color:bg-buttons-disabled};\n}\n\nQToolButton[popupMode=\"1\"], QToolButton[popupMode=\"MenuButtonPopup\"] {\n    /* make way for the popup button */\n    padding-right: 20px;\n}\n\nQToolButton::menu-button {\n    width: 16px;\n    background: transparent;\n    border: 1px solid transparent;\n    border-left: 1px solid qlineargradient(x1:0, y1:0, x2:0, y2:1, stop: 0 transparent, stop:0.2 {color:font}, stop:0.8 {color:font}, stop: 1 transparent);\n    padding: 3px 0px 3px 0px;\n    border-radius: 0;\n}\n\nQToolButton::menu-arrow {\n    /* Offset arrow a little bit to center. */\n    left: 1px; top: 1px;\n}\n\nQToolButton::menu-arrow:open {\n    /* Don't offset arrow on open. */\n    left: 0px; top: 0px;\n}\n\n/* QMenu */\nQMenu {\n    border: 1px solid #555555;\n    background: {color:bg-inputs};\n}\n\nQMenu::icon {\n    padding-left: 7px;\n}\n\nQMenu::item {\n    padding: 6px 25px 6px 10px;\n}\n\nQMenu::item:selected {\n    background: {color:bg-view-hover};\n}\n\nQMenu::item:selected:hover {\n    background: {color:bg-view-hover};\n}\n\nQMenu::right-arrow {\n    min-width: 10px;\n}\nQMenu::separator {\n    background: {color:bg-menu-separator};\n    height: 2px;\n    margin-right: 5px;\n}\n\n/* Combobox */\nQComboBox {\n    border: 1px solid {color:border};\n    border-radius: 0.2em;\n    padding: 1px 3px 1px 3px;\n    background: {color:bg-inputs};\n}\nQComboBox:hover {\n    border-color: {color:border-hover};\n}\nQComboBox:disabled {\n    background: {color:bg-inputs-disabled};\n}\n\n/* QComboBox must have explicitly set Styled delegate! */\nQComboBox QAbstractItemView {\n    border: 1px solid {color:border};\n\tbackground: {color:bg-inputs};\n}\n\nQComboBox QAbstractItemView::item:selected {\n    background: {color:bg-view-hover};\n    color: {color:font};\n    padding-left: 0px;\n}\n\nQComboBox QAbstractItemView::item:selected:hover {\n    background: {color:bg-view-hover};\n}\n\nQComboBox::drop-down {\n    subcontrol-origin: padding;\n    subcontrol-position: center right;\n    width: 15px;\n    border-style: none;\n    border-left-style: solid;\n    border-left-color: {color:border};\n    border-left-width: 1px;\n}\nQComboBox::down-arrow, QComboBox::down-arrow:on, QComboBox::down-arrow:hover, QComboBox::down-arrow:focus\n{\n    image: url(:/openpype/images/combobox_arrow.png);\n}\n\n/* Splitter */\nQSplitter::handle {\n    border: 3px solid transparent;\n}\n\nQSplitter::handle:horizontal, QSplitter::handle:vertical, QSplitter::handle:horizontal:hover, QSplitter::handle:vertical:hover {\n    /* must be single like because of Nuke*/\n    background: transparent;\n}\n\n/* SLider */\nQSlider::groove {\n    border: 1px solid #464b54;\n    border-radius: 0.3em;\n    background: {color:bg-inputs};\n}\nQSlider::groove:horizontal {\n    height: 8px;\n}\n\nQSlider::groove:vertical {\n    width: 8px;\n}\n\nQSlider::groove:hover {\n    border-color: {color:border-hover};\n}\nQSlider::groove:disabled {\n    background: {color:bg-inputs-disabled};\n}\nQSlider::groove:focus {\n    border-color: {color:border-focus};\n}\nQSlider::handle {\n    /* must be single like because of Nuke*/\n    background: qlineargradient(x1: 0, y1: 0.5, x2: 1, y2: 0.5,stop: 0 {palette:blue-base},stop: 1 {palette:green-base});\n    border: 1px solid #5c5c5c;\n    width: 10px;\n    height: 10px;\n\n    border-radius: 5px;\n}\n\nQSlider::handle:horizontal {\n    margin: -2px 0;\n}\nQSlider::handle:vertical {\n    margin: 0 -2px;\n}\n\nQSlider::handle:disabled {\n    /* must be single like because of Nuke*/\n    background: qlineargradient(x1:0, y1:0,x2:1, y2:1,stop:0 {color:bg-buttons},stop:1 {color:bg-buttons-disabled});\n}\n\n/* Tab widget*/\nQTabWidget::pane {\n    border-top-style: none;\n}\n\n/* move to the right to not mess with borders of widget underneath */\nQTabWidget::tab-bar {\n    alignment: left;\n}\n\n/* avoid QTabBar overrides in Substance Painter */\nQTabBar {\n    text-transform: none;\n    font-weight: normal;\n}\n\nQTabBar::tab {\n    text-transform: none;\n    font-weight: normal;\n    border-top: 1px solid {color:border};\n    border-left: 1px solid {color:border};\n    border-right: 1px solid {color:border};\n    padding: 5px;\n    background: {color:tab-widget:bg};\n    color: {color:tab-widget:color};\n}\n\nQTabBar::tab:selected {\n    border-left-color: {color:tab-widget:bg-selected};\n    border-right-color: {color:tab-widget:bg-selected};\n    border-top-color: {color:border-focus};\n    background: {color:tab-widget:bg-selected};\n    color: {color:tab-widget:color-selected};\n}\n\nQTabBar::tab:!selected {}\nQTabBar::tab:!selected:hover {\n    background: {color:tab-widget:bg-hover};\n    color: {color:tab-widget:color-hover};\n}\nQTabBar::tab:first {}\nQTabBar::tab:first:selected {}\nQTabBar::tab:last:!selected {\n    border-right: 1px solid {color:border};\n}\nQTabBar::tab:last:selected {}\nQTabBar::tab:only-one {}\n\nQHeaderView {\n    border: 0px solid {color:border};\n    border-radius: 0px;\n    margin: 0px;\n    padding: 0px;\n}\n\nQHeaderView::section  {\n    background: {color:bg-view-header};\n    padding: 4px;\n    border-top: 0px; /* Substance Painter fix */\n    border-right: 1px solid {color:bg-view};\n    border-radius: 0px;\n    text-align: center;\n    color: {color:font};\n    font-weight: bold;\n}\nQHeaderView::section:first {\n    border-left: none;\n}\nQHeaderView::section:last {\n    border-right: none;\n}\nQHeaderView::section:only-one {\n    border-left: none;\n    border-right: none;\n}\n\nQHeaderView::down-arrow {\n    image: url(:/openpype/images/down_arrow.png);\n    padding-right: 4px;\n    subcontrol-origin: padding;\n    subcontrol-position: center right;\n}\n\nQHeaderView::up-arrow {\n    image: url(:/openpype/images/up_arrow.png);\n    padding-right: 4px;\n    subcontrol-origin: padding;\n    subcontrol-position: center right;\n}\n\n/* Checkboxes */\nQCheckBox {\n    background: transparent;\n}\n\nQCheckBox::indicator {\n    width: 16px;\n    height: 16px;\n}\n\nQAbstractItemView::indicator:checked, QCheckBox::indicator:checked {\n    image: url(:/openpype/images/checkbox_checked.png);\n}\nQAbstractItemView::indicator:checked:focus, QCheckBox::indicator:checked:focus {\n    image: url(:/openpype/images/checkbox_checked_focus.png);\n}\nQAbstractItemView::indicator:checked:hover, QAbstractItemView::indicator:checked:pressed, QCheckBox::indicator:checked:hover, QCheckBox::indicator:checked:pressed {\n    image: url(:/openpype/images/checkbox_checked_hover.png);\n}\nQAbstractItemView::indicator:checked:disabled, QCheckBox::indicator:checked:disabled {\n    image: url(:/openpype/images/checkbox_checked_disabled.png);\n}\n\nQAbstractItemView::indicator:unchecked, QCheckBox::indicator:unchecked {\n    image: url(:/openpype/images/checkbox_unchecked.png);\n}\nQAbstractItemView::indicator:unchecked:focus, QCheckBox::indicator:unchecked:focus {\n    image: url(:/openpype/images/checkbox_unchecked_focus.png);\n}\nQAbstractItemView::indicator:unchecked:hover, QAbstractItemView::indicator:unchecked:pressed, QCheckBox::indicator:unchecked:hover, QCheckBox::indicator:unchecked:pressed {\n    image: url(:/openpype/images/checkbox_unchecked_hover.png);\n}\nQAbstractItemView::indicator:unchecked:disabled, QCheckBox::indicator:unchecked:disabled {\n    image: url(:/openpype/images/checkbox_unchecked_disabled.png);\n}\n\nQAbstractItemView::indicator:indeterminate, QCheckBox::indicator:indeterminate {\n    image: url(:/openpype/images/checkbox_indeterminate.png);\n}\nQAbstractItemView::indicator:indeterminate:focus, QCheckBox::indicator:indeterminate:focus {\n    image: url(:/openpype/images/checkbox_indeterminate_focus.png);\n}\nQAbstractItemView::indicator:indeterminate:hover, QAbstractItemView::indicator:indeterminate:pressed, QCheckBox::indicator:indeterminate:hover, QCheckBox::indicator:indeterminate:pressed {\n    image: url(:/openpype/images/checkbox_indeterminate_hover.png);\n}\nQAbstractItemView::indicator:indeterminate:disabled, QCheckBox::indicator:indeterminate:disabled {\n    image: url(:/openpype/images/checkbox_indeterminate_disabled.png);\n}\n\n/* Views QListView QTreeView QTableView */\nQAbstractItemView {\n    border: 0px solid {color:border};\n    border-radius: 0px;\n    background: {color:bg-view};\n    alternate-background-color: {color:bg-view-alternate};\n    /* Mac shows selection color on branches. */\n    selection-background-color: transparent;\n}\n\nQAbstractItemView::item {\n    /* `border: none` hide outline of selected item. */\n    border: none;\n}\n\nQAbstractItemView:disabled{\n    background: {color:bg-view-disabled};\n    alternate-background-color: {color:bg-view-alternate-disabled};\n    border: 1px solid {color:border};\n}\n\nQAbstractItemView::item:hover {\n    background: {color:bg-view-hover};\n}\n\nQAbstractItemView::item:selected {\n    background: {color:bg-view-selection};\n    color: {color:font-view-selection};\n}\n\nQAbstractItemView::item:selected:active {\n    color: {color:font-view-selection};\n}\n\n/* Same as selected but give ability to easy change it */\nQAbstractItemView::item:selected:!active {\n    background: {color:bg-view-selection};\n    color: {color:font-view-selection};\n}\n\nQAbstractItemView::item:selected:hover {\n    background: {color:bg-view-selection-hover};\n}\n\n/* Row colors (alternate colors) are from left - right */\nQTreeView::branch {\n    background: {color:bg-view};\n}\nQTreeView::branch:hover {\n    background: {color:bg-view};\n}\nQTreeView::branch:selected {\n    background: {color:bg-view};\n}\n\nQAbstractItemView::branch:open:has-children:!has-siblings,\nQAbstractItemView::branch:open:has-children:has-siblings {\n    border-image: none;\n    image: url(:/openpype/images/branch_open.png);\n    background: {color:bg-view};\n}\nQAbstractItemView::branch:open:has-children:!has-siblings:hover,\nQAbstractItemView::branch:open:has-children:has-siblings:hover {\n    border-image: none;\n    image: url(:/openpype/images/branch_open_on.png);\n    background: {color:bg-view};\n}\n\nQAbstractItemView::branch:has-children:!has-siblings:closed,\nQAbstractItemView::branch:closed:has-children:has-siblings {\n    border-image: none;\n    image: url(:/openpype/images/branch_closed.png);\n    background: {color:bg-view};\n}\nQAbstractItemView::branch:has-children:!has-siblings:closed:hover,\nQAbstractItemView::branch:closed:has-children:has-siblings:hover {\n    border-image: none;\n    image: url(:/openpype/images/branch_closed_on.png);\n    background: {color:bg-view};\n}\n\nQAbstractItemView::branch:has-siblings:!adjoins-item {\n    border-image: none;\n    image: url(:/openpype/images/transparent.png);\n    background: {color:bg-view};\n}\n\nQAbstractItemView::branch:has-siblings:adjoins-item {\n    border-image: none;\n    image: url(:/openpype/images/transparent.png);\n    background: {color:bg-view};\n}\n\nQAbstractItemView::branch:!has-children:!has-siblings:adjoins-item {\n    border-image: none;\n    image: url(:/openpype/images/transparent.png);\n    background: {color:bg-view};\n}\n\nCompleterView {\n    border: 1px solid #555555;\n    background: {color:bg-inputs};\n}\n\nCompleterView::item:selected {\n    background: {color:bg-view-hover};\n}\n\nCompleterView::item:selected:hover {\n    background: {color:bg-view-hover};\n}\n\nCompleterView::right-arrow {\n    min-width: 10px;\n}\nCompleterView::separator {\n    background: {color:bg-menu-separator};\n    height: 2px;\n    margin-right: 5px;\n}\n\n/* Progress bar */\nQProgressBar {\n    border: 1px solid {color:border};\n    font-weight: bold;\n    text-align: center;\n}\n\nQProgressBar:horizontal {\n    height: 20px;\n}\nQProgressBar:vertical {\n    width: 20px;\n}\n\nQProgressBar::chunk {\n    /* must be single like because of Nuke*/\n    background: qlineargradient(x1: 0, y1: 0.5,x2: 1, y2: 0.5,stop: 0 {palette:blue-base},stop: 1 {palette:green-base});\n}\n\n/* Scroll bars */\nQScrollBar {\n    background: {color:bg-inputs};\n    border-radius: 4px;\n    border: 1px transparent {color:bg-inputs};\n}\n\nQScrollBar:horizontal {\n    height: 15px;\n    margin: 3px 3px 3px 6px;\n}\n\nQScrollBar:vertical {\n    width: 15px;\n    margin: 6px 3px 3px 3px;\n}\n\nQScrollBar::handle {\n    background: {color:bg-scroll-handle};\n    border-radius: 4px;\n}\n\nQScrollBar::handle:horizontal {\n    min-width: 5px;\n}\n\nQScrollBar::handle:vertical {\n    min-height: 5px;\n}\n\nQScrollBar::add-line:horizontal {\n    margin: 0px 3px 0px 3px;\n    width: 0px;\n    height: 0px;\n    subcontrol-position: right;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::sub-line:horizontal {\n    margin: 0px 3px 0px 3px;\n    height: 0px;\n    width: 0px;\n    subcontrol-position: left;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on {\n    height: 0px;\n    width: 0px;\n    subcontrol-position: right;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on {\n    height: 0px;\n    width: 0px;\n    subcontrol-position: left;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal {\n    background: none;\n}\n\nQScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {\n    background: none;\n}\n\nQScrollBar::sub-line:vertical {\n    margin: 3px 0px 3px 0px;\n    height: 0px;\n    width: 0px;\n    subcontrol-position: top;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::add-line:vertical {\n    margin: 3px 0px 3px 0px;\n    height: 0px;\n    width: 0px;\n    subcontrol-position: bottom;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on {\n    subcontrol-position: top;\n    subcontrol-origin: margin;\n}\n\n\nQScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on {\n    subcontrol-position: bottom;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {\n    background: none;\n}\n\n\nQScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {\n    background: none;\n}\n\n/* Messages overlay */\nOverlayMessageWidget {\n    border-radius: 0.2em;\n    background: {color:overlay-messages:bg-success};\n}\n\nOverlayMessageWidget:hover {\n    background: {color:overlay-messages:bg-success-hover};\n}\n\nOverlayMessageWidget[type=\"error\"] {\n    background: {color:overlay-messages:bg-error};\n}\nOverlayMessageWidget[type=\"error\"]:hover {\n    background: {color:overlay-messages:bg-error-hover};\n}\n\nOverlayMessageWidget[type=\"info\"] {\n    background: {color:overlay-messages:bg-info};\n}\nOverlayMessageWidget[type=\"info\"]:hover {\n    background: {color:overlay-messages:bg-info-hover};\n}\n\nOverlayMessageWidget QWidget {\n    background: transparent;\n}\n\n/* Password dialog*/\n#PasswordBtn {\n    border: none;\n    padding:0.1em;\n    background: transparent;\n}\n\n#PasswordBtn:hover {\n    background: {color:bg-buttons};\n}\n\n#RememberCheckbox {\n    spacing: 0.5em;\n}\n\n/* Project Manager stylesheets */\n#HierarchyView::item {\n    padding-top: 3px;\n    padding-bottom: 3px;\n    padding-right: 3px;\n}\n\n#InfoText {\n    padding-left: 0px;\n    padding-top: 0px;\n    padding-right: 20px;\n    background: transparent;\n    border: none;\n}\n\n#TypeEditor, #ToolEditor, #NameEditor, #NumberEditor {\n    background: transparent;\n    border-radius: 0.2em;\n}\n\n#TypeEditor:focus, #ToolEditor:focus, #NameEditor:focus, #NumberEditor:focus {\n    background: {color:bg-inputs};\n}\n\n#CompleterView {\n    border: 1px solid {color:border};\n    background: {color:bg-inputs};\n}\n\n#CompleterView::item {\n    background: {color:bg-view-hover};\n    color: {color:font};\n    padding-left: 0px;\n}\n\n#CompleterView::item:hover {\n    background: {color:bg-view-hover};\n}\n\n#DeleteButton {\n    background: {color:delete-btn-bg};\n}\n#DeleteButton:disabled {\n    background: {color:delete-btn-bg-disabled};\n}\n\n/* Launcher specific stylesheets */\n#IconView[mode=\"icon\"] {\n    /* font size can't be set on items */\n    font-size: 9pt;\n    border: 0px;\n    padding: 0px;\n    margin: 0px;\n}\n\n#IconView[mode=\"icon\"]::item  {\n    margin-top: 6px;\n    border: 0px;\n}\n\n#IconView[mode=\"icon\"]::item:hover {\n    background: rgba(0, 0, 0, 0);\n    color: {color:font-hover};\n}\n\n#IconView[mode=\"icon\"]::icon {\n    top: 3px;\n}\n\n/* Standalone publisher */\n\n#ComponentItem {\n    background: transparent;\n}\n\n#ComponentFrame {\n    border: 1px solid {color:border};\n    border-radius: 0.1em;\n}\n\n/* Subset Manager */\n#SubsetManagerDetailsText {}\n#SubsetManagerDetailsText[state=\"invalid\"] {\n    border: 1px solid #ff0000;\n}\n\n/* Creator */\n#CreatorsView::item {\n    padding: 1px 5px;\n}\n\n#CreatorFamilyLabel {\n    font-size: 10pt;\n    font-weight: bold;\n}\n\n/* Scene Inventory */\n#ButtonWithMenu {\n    padding-right: 16px;\n    border: 1px solid #4A4949;\n    border-radius: 2px;\n}\n#ButtonWithMenu::menu-button {\n    border: 1px solid #4A4949;\n    width: 12px;\n    border-top-left-radius: 0px;\n    border-top-right-radius: 2px;\n    border-bottom-right-radius: 2px;\n    border-bottom-left-radius: 0px;\n}\n\n#ButtonWithMenu[state=\"1\"], #ButtonWithMenu[state=\"1\"]::menu-button, #ButtonWithMenu[state=\"1\"]::menu-button:hover {\n    border-color: green;\n}\n\n/* Python console interpreter */\n#PythonInterpreterOutput, #PythonCodeEditor {\n    font-family: \"Noto Sans Mono\";\n    border-radius: 0px;\n}\n\n#SubsetView::item, #RepresentationView:item {\n    padding: 5px 1px;\n    border: 0px;\n}\n\n#OptionalActionBody, #OptionalActionOption {\n    background: transparent;\n}\n\n#OptionalActionBody[state=\"hover\"], #OptionalActionOption[state=\"hover\"] {\n    background: {color:bg-view-hover};\n}\n\n/* Publisher UI (Create/Publish) */\n#PublishWindow QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit {\n    padding: 1px;\n}\n#PublishWindow QComboBox {\n    padding: 1px 1px 1px 0.2em;\n}\nPublisherTabsWidget {\n    background: {color:publisher:tab-bg};\n}\n\nPublisherTabBtn {\n    border-radius: 0px;\n    background: {color:bg-inputs};\n    font-size: 9pt;\n    font-weight: regular;\n    padding: 0.5em 1em 0.5em 1em;\n}\n\nPublisherTabBtn:disabled {\n    background: {color:bg-inputs};\n}\n\nPublisherTabBtn:hover {\n    background: {color:bg-buttons};\n}\n\nPublisherTabBtn[active=\"1\"] {\n    background: {color:bg};\n}\nPublisherTabBtn[active=\"1\"]:hover {\n    background: {color:bg};\n}\n\nPixmapButton{\n    border: 0px solid transparent;\n    border-radius: 0.2em;\n    background: {color:bg-buttons};\n}\nPixmapButton:hover {\n    background: {color:bg-buttons-hover};\n}\nPixmapButton:disabled {\n    background: {color:bg-buttons-disabled};\n}\n\n#ThumbnailPixmapHoverButton {\n    font-size: 11pt;\n    background: {color:bg-view};\n}\n#ThumbnailPixmapHoverButton:hover {\n    background: {color:bg-buttons-hover};\n}\n\n#CreatorDetailedDescription {\n    padding-left: 5px;\n    padding-right: 5px;\n    padding-top: 5px;\n    background: transparent;\n    border: 1px solid {color:border};\n}\n\n#CreateDialogHelpButton {\n    background: {color:bg-buttons};\n    border-top-left-radius: 0.2em;\n    border-bottom-left-radius: 0.2em;\n    border-top-right-radius: 0;\n    border-bottom-right-radius: 0;\n    font-weight: bold;\n}\n\n#CreateDialogHelpButton:hover {\n    background: {color:bg-buttons-hover};\n}\n#CreateDialogHelpButton QWidget {\n    background: transparent;\n}\n\n#PublishLogConsole {\n    font-family: \"Noto Sans Mono\";\n}\n#VariantInputsWidget QLineEdit {\n    border-bottom-right-radius: 0px;\n    border-top-right-radius: 0px;\n}\n#VariantInputsWidget QToolButton {\n    border-bottom-left-radius: 0px;\n    border-top-left-radius: 0px;\n    padding-top: 0.5em;\n    padding-bottom: 0.5em;\n    width: 0.5em;\n}\n#VariantInput[state=\"new\"], #VariantInput[state=\"new\"]:focus, #VariantInput[state=\"new\"]:hover {\n    border-color: {color:publisher:success};\n}\n#VariantInput[state=\"invalid\"], #VariantInput[state=\"invalid\"]:focus, #VariantInput[state=\"invalid\"]:hover {\n    border-color: {color:publisher:error};\n}\n\n#VariantInput[state=\"empty\"], #VariantInput[state=\"empty\"]:focus, #VariantInput[state=\"empty\"]:hover {\n    border-color: {color:bg-inputs};\n}\n\n#VariantInput[state=\"exists\"], #VariantInput[state=\"exists\"]:focus, #VariantInput[state=\"exists\"]:hover {\n    border-color: #4E76BB;\n}\n\n#MultipleItemView {\n    background: transparent;\n    border: none;\n}\n\n#MultipleItemView:item {\n    background: {color:bg-view-selection};\n    border-radius: 0.4em;\n}\n\n#InstanceListView::item {\n    border-radius: 0.3em;\n    margin: 1px;\n}\n#InstanceListGroupWidget {\n    border: none;\n    background: transparent;\n}\n\n#CardViewWidget {\n    background: {color:bg-buttons};\n    border-radius: 0.2em;\n}\n#CardViewWidget:hover {\n    background: {color:bg-buttons-hover};\n}\n#CardViewWidget[state=\"selected\"] {\n    background: {color:bg-view-selection};\n}\n\n#ListViewSubsetName[state=\"invalid\"] {\n    color: {color:publisher:error};\n}\n\n#PublishInfoFrame {\n    background: {color:bg};\n    border-radius: 0.3em;\n}\n#PublishInfoFrame[state=\"0\"] {\n  background: {color:publisher:success};\n}\n\n#PublishInfoFrame[state=\"1\"] {\n\tbackground: {color:publisher:crash};\n}\n\n#PublishInfoFrame[state=\"2\"] {\n\tbackground: {color:publisher:warning};\n}\n\n#PublishInfoFrame[state=\"3\"], #PublishInfoFrame[state=\"4\"] {\n    background: {color:publisher:progress};\n}\n\n#PublishInfoFrame QLabel {\n    color: black;\n    font-style: bold;\n}\n\n#PublishReportHeader {\n    font-size: 14pt;\n    font-weight: bold;\n}\n\n#PublishInfoMainLabel {\n    font-size: 12pt;\n}\n\n#PublishContextLabel {\n    font-size: 13pt;\n}\n\nValidationArtistMessage QLabel {\n    font-size: 20pt;\n    font-weight: bold;\n}\n\n#ValidationActionButton {\n    border-radius: 0.2em;\n    padding: 4px 6px 4px 6px;\n    background: {color:bg-buttons};\n}\n\n#ValidationActionButton:hover {\n    background: {color:bg-buttons-hover};\n    color: {color:font-hover};\n}\n\n#ValidationActionButton:disabled {\n    background: {color:bg-buttons-disabled};\n}\n\n#ValidationErrorTitleFrame {\n    border-radius: 0.2em;\n    background: {color:bg-buttons};\n}\n\n#ValidationErrorTitleFrame:hover {\n    background: {color:bg-buttons-hover};\n}\n\n#ValidationErrorTitleFrame[selected=\"1\"] {\n    background: {color:bg-view-selection};\n}\n\n#ValidationErrorInstanceList {\n    border-radius: 0;\n}\n\n#ValidationErrorInstanceList::item {\n    border-bottom: 1px solid {color:border};\n    border-left: 1px solid {color:border};\n}\n\n#PublishInstancesDetails {\n    border: 1px solid {color:border};\n    border-radius: 0.3em;\n}\n\n#InstancesLogsView {\n    border: 1px solid {color:border};\n    background: {color:bg-view};\n    border-radius: 0.3em;\n}\n\n#PublishLogMessage {\n    font-family: \"Noto Sans Mono\";\n}\n\n#PublishInstanceLogsLabel {\n    font-weight: bold;\n}\n\n#PublishCrashMainLabel{\n    font-weight: bold;\n    font-size: 16pt;\n}\n\n#PublishCrashReportLabel {\n    font-weight: bold;\n    font-size: 13pt;\n}\n\n#AssetNameInputWidget {\n    background: {color:bg-inputs};\n    border: 1px solid {color:border};\n    border-radius: 0.2em;\n}\n\n#AssetNameInputWidget QWidget {\n    background: transparent;\n}\n\n#AssetNameInputButton {\n    border-bottom-left-radius: 0px;\n    border-top-left-radius: 0px;\n    padding: 0px;\n    qproperty-iconSize: 11px 11px;\n    border-left: 1px solid {color:border};\n    border-right: none;\n    border-top: none;\n    border-bottom: none;\n}\n\n#AssetNameInput {\n    border-bottom-right-radius: 0px;\n    border-top-right-radius: 0px;\n    border: none;\n}\n\n#AssetNameInputWidget:hover {\n    border-color: {color:border-hover};\n}\n#AssetNameInputWidget:focus{\n    border-color: {color:border-focus};\n}\n#AssetNameInputWidget:disabled {\n    background: {color:bg-inputs-disabled};\n}\n\n#TasksCombobox[state=\"invalid\"], #AssetNameInputWidget[state=\"invalid\"], #AssetNameInputButton[state=\"invalid\"] {\n    border-color: {color:publisher:error};\n}\n\n#PublishProgressBar[state=\"1\"]::chunk, #PublishProgressBar[state=\"4\"]::chunk {\n    background: {color:bg-buttons};\n}\n\n#PublishDetailViews {\n    background: transparent;\n}\n#PublishDetailViews::item {\n    margin: 1px 0px 1px 0px;\n}\n#PublishCommentInput {\n    padding: 0.2em;\n}\n#FamilyIconLabel {\n    font-size: 14pt;\n}\n#ArrowBtn, #ArrowBtn:disabled, #ArrowBtn:hover {\n    background: transparent;\n}\n\nCreateNextPageOverlay {\n    font-size: 32pt;\n}\n\n/* Settings - NOT USED YET\n- we need to define font family for settings UI */\n\n#SettingsMainWidget {\n    background: #141a1f;\n}\n/* Change focus borders. */\n#SettingsMainWidget QAbstractSpinBox:focus, #SettingsMainWidget QLineEdit:focus, #SettingsMainWidget QPlainTextEdit:focus, #SettingsMainWidget QTextEdit:focus {\n    border-color: {color:settings:focus-border};\n}\n/* Modify tab widget for settings */\n#SettingsMainWidget QTabWidget::pane {\n    border-top-style: none;\n}\n\n#SettingsMainWidget QTabBar {\n   background: transparent;\n}\n\n#SettingsMainWidget QTabBar::tab {\n    border: none;\n    border-top-left-radius: 4px;\n    border-top-right-radius: 4px;\n    padding: 5px;\n}\n\n#SettingsMainWidget QTabBar::tab:selected {\n    background: {color:bg};\n    border-color: #9B9B9B;\n    border-bottom-color: #C2C7CB;\n}\n\n#SettingsMainWidget QTabBar::tab:!selected {\n    margin-top: 2px;\n    background: #21252B;\n}\n\n#SettingsMainWidget QTabBar::tab:!selected:hover {\n    background: #333840;\n}\n\n#SettingsMainWidget QTabBar::tab:first:selected {\n    margin-left: 0;\n}\n\n#SettingsMainWidget QTabBar::tab:last:selected {\n    margin-right: 0;\n}\n\n#SettingsMainWidget QTabBar::tab:only-one {\n    margin: 0;\n}\n\n#SettingsToolIconBtn {\n    border: 0px solid #bfccd6;\n    background-color: transparent;\n}\n\n#SettingsToolBtn {\n    border: 1px solid #bfccd6;\n    border-radius: 10px;\n    background-color: transparent;\n}\n\n#SettingsToolBtn:hover {\n    border-color: #189aea;\n    color: {color:settings:modified-light};\n    background-color: transparent;\n}\n#SettingsToolBtn:disabled {\n    background-color: #464b54;\n}\n\n#ExpandToggleBtn {\n    background: transparent;\n}\n\n#SettingsLabel {\n    background: transparent;\n    color: {color:settings:label-fg};\n}\n#SettingsLabel:hover {color: {color:settings:label-fg-hover};}\n\n#ExpandLabel {\n    font-weight: bold;\n    color: {color:settings:label-fg};\n}\n#ExpandLabel:hover {\n    color: {color:settings:label-fg-hover};\n}\n\n#ExpandLabel[state=\"studio\"], #SettingsLabel[state=\"studio\"] {\n    color: {color:settings:studio-light};\n}\n#ExpandLabel[state=\"studio\"]:hover, #SettingsLabel[state=\"studio\"]:hover {\n    color: {color:settings:studio-label-hover};\n}\n#ExpandLabel[state=\"modified\"], #SettingsLabel[state=\"modified\"] {\n    color: {color:settings:modified-mid};\n}\n#ExpandLabel[state=\"modified\"]:hover, #SettingsLabel[state=\"modified\"]:hover {\n    color: {color:settings:modified-light};\n}\n#ExpandLabel[state=\"overridden-modified\"], #SettingsLabel[state=\"overridden-modified\"] {\n    color: {color:settings:modified-mid};\n}\n#ExpandLabel[state=\"overridden-modified\"]:hover, #SettingsLabel[state=\"overridden-modified\"]:hover {\n    color: {color:settings:modified-light};\n}\n#ExpandLabel[state=\"overridden\"], #SettingsLabel[state=\"overridden\"] {\n    color: {color:settings:project-mid};\n}\n#ExpandLabel[state=\"overridden\"]:hover, #SettingsLabel[state=\"overridden\"]:hover {\n    color: {color:settings:project-light};\n}\n#ExpandLabel[state=\"invalid\"], #SettingsLabel[state=\"invalid\"] {\n    color:{color:settings:invalid-dark};\n}\n#ExpandLabel[state=\"invalid\"]:hover, #SettingsLabel[state=\"invalid\"]:hover {\n    color: {color:settings:invalid-dark};\n}\n#SettingsOutdatedSourceVersion {\n    color: {color:settings:source-version-outdated};\n}\n#SourceVersionLabel {\n    padding-left: 3px;\n    padding-right: 3px;\n}\n\n#SourceVersionLabel[state=\"same\"] {\n    color: {color:settings:source-version};\n}\n#SourceVersionLabel[state=\"different\"] {\n    color: {color:settings:source-version-outdated};\n}\n\n/* TODO Replace these with explicit widget types if possible */\n#SettingsMainWidget QWidget[input-state=\"modified\"] {\n    border-color: {color:settings:modified-mid};\n}\n#SettingsMainWidget QWidget[input-state=\"overridden-modified\"] {\n    border-color: {color:settings:modified-mid};\n}\n#SettingsMainWidget QWidget[input-state=\"overridden\"] {\n    border-color: {color:settings:project-mid};\n}\n#SettingsMainWidget QWidget[input-state=\"invalid\"] {\n    border-color: {color:settings:invalid-dark};\n}\n\n#SettingsFooter {\n    border-top: 1px solid #21252B;\n}\n\n#ProjectListWidget QLabel {\n    background: transparent;\n    font-weight: bold;\n}\n\n#ProjectListContentWidget {\n    background: {color:bg-view};\n}\n\n#MultiSelectionComboBox {\n    font-size: 12px;\n}\n\n#DictKey[state=\"modified\"] {border-color: {color:settings:modified-mid};}\n#DictKey[state=\"invalid\"] {border-color: {color:settings:invalid-dark};}\n\n#ContentWidget {\n    background-color: transparent;\n}\n#ContentWidget[content_state=\"highlighted\"] {\n    background-color: {color:settings:content-highlighted};\n}\n\n#SideLineWidget {\n    background-color: #333942;\n    border-style: solid;\n    border-color: #4e5254;\n    border-left-width: 3px;\n    border-bottom-width: 0px;\n    border-right-width: 0px;\n    border-top-width: 0px;\n}\n\n#SideLineWidget:hover {\n    border-color: #7d8386;\n}\n\n#SideLineWidget[state=\"child-studio\"] {border-color: {color:settings:studio-dark};}\n#SideLineWidget[state=\"child-studio\"]:hover {border-color: {color:settings:studio-light};}\n\n#SideLineWidget[state=\"child-modified\"] {border-color: {color:settings:modified-dark};}\n#SideLineWidget[state=\"child-modified\"]:hover {border-color: {color:settings:modified-mid};}\n\n#SideLineWidget[state=\"child-invalid\"] {border-color: {color:settings:invalid-dark};}\n#SideLineWidget[state=\"child-invalid\"]:hover {border-color: {color:settings:invalid-light};}\n\n#SideLineWidget[state=\"child-overridden\"] {border-color: {color:settings:project-dark};}\n#SideLineWidget[state=\"child-overridden\"]:hover {border-color: {color:settings:project-mid};}\n\n#SideLineWidget[state=\"child-overridden-modified\"] {border-color: {color:settings:modified-dark};}\n#SideLineWidget[state=\"child-overridden-modified\"]:hover {border-color: {color:settings:modified-mid};}\n\n#DictAsWidgetBody {\n    background: transparent;\n}\n#DictAsWidgetBody[show_borders=\"1\"] {\n    border: 1px solid #4e5254;\n    border-radius: 5px;\n}\n\n#OpenPypeVersionLabel[state=\"success\"] {\n    color: {color:settings:version-exists};\n}\n\n#OpenPypeVersionLabel[state=\"warning\"] {\n    color: {color:settings:version-not-found};\n}\n\n#ShadowWidget {\n    font-size: 36pt;\n}\n\n#OverlayFrame {\n    background: rgba(0, 0, 0, 127);\n}\n\n#OverlayFrameLabel {\n    font-size: 15pt;\n}\n\n#BreadcrumbsPathInput {\n    padding: 2px;\n    font-size: 9pt;\n}\n\n#BreadcrumbsButton {\n    padding-right: 12px;\n    font-size: 9pt;\n    background: transparent;\n}\n\n#BreadcrumbsButton[empty=\"1\"] {\n    padding-right: 0px;\n}\n\n#BreadcrumbsButton::menu-button {\n    border: none;\n    width: 12px;\n    background: {color:settings:breadcrumbs-btn-bg};\n}\n#BreadcrumbsButton::menu-button:hover {\n    background: {color:settings:breadcrumbs-btn-bg-hover};\n}\n\n#BreadcrumbsPanel {\n    border: 1px solid #4e5254;\n    border-radius: 5px;\n    background: #21252B;\n}\n\n/* Workfiles */\n#WorkfilesPublishedContextSelect {\n    background: rgba(0, 0, 0, 127);\n}\n#WorkfilesPublishedContextSelect QLabel {\n    font-size: 17pt;\n}\n\n/* Tray */\n#TrayRestartButton {\n    background: {color:restart-btn-bg};\n}\n\n/* Tray publisher */\n#ChooseProjectLabel {\n    font-size: 15pt;\n    font-weight: 750;\n}\n#ChooseProjectFrame {\n    border-radius: 10px;\n}\n#ChooseProjectView {\n    background: transparent;\n}\n\n/* Globally used names */\n#ValidatedLineEdit[state=\"valid\"], #ValidatedLineEdit[state=\"valid\"]:focus, #ValidatedLineEdit[state=\"valid\"]:hover {\n    border-color: {color:publisher:success};\n}\n#ValidatedLineEdit[state=\"invalid\"], #ValidatedLineEdit[state=\"invalid\"]:focus, #ValidatedLineEdit[state=\"invalid\"]:hover {\n    border-color: {color:publisher:error};\n}\n\n#Separator {\n    background: {color:bg-menu-separator};\n}\n\n#IconButton {\n    padding: 4px 4px 4px 4px;\n}\n\n#NiceCheckbox {\n    /* Default size hint of NiceCheckbox is defined by font size. */\n    font-size: 7pt;\n}\n\n#ImageButton {\n    padding: 0;\n    background: transparent;\n    font-size: 11pt;\n}\n\n#ImageButton:disabled {\n    background: {color:bg-buttons-disabled};\n}\n\n/* Input field that looks like disabled\n- QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit\n- usage: QLineEdit that is not editable but has selectable color\n */\n#LikeDisabledInput {\n    background: {color:bg-inputs-disabled};\n}\n#LikeDisabledInput:hover {\n    border-color: {color:border};\n}\n#LikeDisabledInput:focus {\n    border-color: {color:border};\n}\n\n/* Attribute Definition widgets */\nAttributeDefinitionsWidget QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit {\n    padding: 1px;\n}\nAttributeDefinitionsWidget QComboBox {\n    padding: 1px 1px 1px 0.2em;\n}\nInViewButton, InViewButton:disabled {\n    background: transparent;\n}\nInViewButton:hover {\n    background: rgba(255, 255, 255, 37);\n}\nSupportLabel {\n    color: {color:font-disabled};\n}\n"
  },
  {
    "path": "openpype/tests/README.md",
    "content": "Tests for Pype\n--------------\nTrigger by:\n    `pype test --pype`"
  },
  {
    "path": "openpype/tests/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/tests/lib.py",
    "content": "import os\nimport sys\nimport shutil\nimport tempfile\nimport contextlib\n\nimport pyblish\nimport pyblish.plugin\nfrom pyblish.vendor import six\n\n\n# Setup\nHOST = 'python'\nFAMILY = 'test.family'\n\nREGISTERED = pyblish.plugin.registered_paths()\nPACKAGEPATH = pyblish.lib.main_package_path()\nENVIRONMENT = os.environ.get(\"PYBLISHPLUGINPATH\", \"\")\nPLUGINPATH = os.path.join(PACKAGEPATH, '..', 'tests', 'plugins')\n\n\ndef setup():\n    pyblish.plugin.deregister_all_paths()\n\n\ndef setup_empty():\n    \"\"\"Disable all plug-ins\"\"\"\n    setup()\n    pyblish.plugin.deregister_all_plugins()\n    pyblish.plugin.deregister_all_paths()\n    pyblish.plugin.deregister_all_hosts()\n    pyblish.plugin.deregister_all_callbacks()\n    pyblish.plugin.deregister_all_targets()\n    pyblish.api.deregister_all_discovery_filters()\n\n\ndef teardown():\n    \"\"\"Restore previously REGISTERED paths\"\"\"\n\n    pyblish.plugin.deregister_all_paths()\n    for path in REGISTERED:\n        pyblish.plugin.register_plugin_path(path)\n\n    os.environ[\"PYBLISHPLUGINPATH\"] = ENVIRONMENT\n    pyblish.api.deregister_all_plugins()\n    pyblish.api.deregister_all_hosts()\n    pyblish.api.deregister_all_discovery_filters()\n    pyblish.api.deregister_test()\n    pyblish.api.__init__()\n\n\n@contextlib.contextmanager\ndef captured_stdout():\n    \"\"\"Temporarily reassign stdout to a local variable\"\"\"\n    try:\n        sys.stdout = six.StringIO()\n        yield sys.stdout\n    finally:\n        sys.stdout = sys.__stdout__\n\n\n@contextlib.contextmanager\ndef captured_stderr():\n    \"\"\"Temporarily reassign stderr to a local variable\"\"\"\n    try:\n        sys.stderr = six.StringIO()\n        yield sys.stderr\n    finally:\n        sys.stderr = sys.__stderr__\n\n\n@contextlib.contextmanager\ndef tempdir():\n    \"\"\"Provide path to temporary directory\"\"\"\n    try:\n        tempdir = tempfile.mkdtemp()\n        yield tempdir\n    finally:\n        shutil.rmtree(tempdir)\n\n\ndef is_in_tests():\n    \"\"\"Returns if process is running in automatic tests mode.\n\n    In tests mode different source DB is used, some plugins might be disabled\n    etc.\n    \"\"\"\n    return os.environ.get(\"IS_TEST\") == '1'\n"
  },
  {
    "path": "openpype/tests/mongo_performance.py",
    "content": "import pymongo\nimport bson\nimport random\nfrom datetime import datetime\nimport os\n\n\nclass TestPerformance():\n    '''\n        Class for testing performance of representation and their 'files'\n        parts.\n        Discussion is if embedded array:\n                            'files' : [ {'_id': '1111', 'path':'....},\n                                        {'_id'...}]\n                     OR documents:\n                            'files' : {\n                                            '1111': {'path':'....'},\n                                            '2222': {'path':'...'}\n                                        }\n                     is faster.\n\n        Current results:\n            without additional partial index documents is 3x faster\n            With index is array 50x faster then document\n\n        Partial index something like:\n        db.getCollection('performance_test').createIndex\n            ({'files._id': 1},\n            {partialFilterExpresion: {'files': {'$exists': true}}})\n        !DIDNT work for me, had to create manually in Compass\n\n    '''\n\n    MONGO_URL = 'mongodb://localhost:27017'\n    MONGO_DB = 'performance_test'\n    MONGO_COLLECTION = 'performance_test'\n\n    MAX_FILE_SIZE_B = 5000\n    MAX_NUMBER_OF_SITES = 50\n    ROOT_DIR = \"C:/projects\"\n\n    inserted_ids = []\n\n    def __init__(self, version='array'):\n        '''\n            It creates and fills collection, based on value of 'version'.\n\n        :param version: 'array' - files as embedded array,\n                        'doc' - as document\n        '''\n        self.client = pymongo.MongoClient(self.MONGO_URL)\n        self.db = self.client[self.MONGO_DB]\n        self.collection_name = self.MONGO_COLLECTION\n\n        self.version = version\n\n        if self.version != 'array':\n            self.collection_name = self.MONGO_COLLECTION + '_doc'\n\n        self.collection = self.db[self.collection_name]\n\n        self.ids = []  # for testing\n        self.inserted_ids = []\n\n    def prepare(self, no_of_records=100000, create_files=False):\n        '''\n            Produce 'no_of_records' of representations with 'files' segment.\n            It depends on 'version' value in constructor, 'arrray' or 'doc'\n        :return:\n        '''\n        print('Purging {} collection'.format(self.collection_name))\n        self.collection.delete_many({})\n\n        id = bson.objectid.ObjectId()\n\n        insert_recs = []\n        for i in range(no_of_records):\n            file_id = bson.objectid.ObjectId()\n            file_id2 = bson.objectid.ObjectId()\n            file_id3 = bson.objectid.ObjectId()\n\n            self.inserted_ids.extend([file_id, file_id2, file_id3])\n            version_str = \"v{:03d}\".format(i + 1)\n            file_name = \"test_Cylinder_workfileLookdev_{}.mb\".\\\n                format(version_str)\n\n            document = {\"files\": self.get_files(self.version, i + 1,\n                                                file_id, file_id2, file_id3,\n                                                create_files)\n                        ,\n                        \"context\": {\n                            \"subset\": \"workfileLookdev\",\n                            \"username\": \"petrk\",\n                            \"task\": \"lookdev\",\n                            \"family\": \"workfile\",\n                            \"hierarchy\": \"Assets\",\n                            \"project\": {\"code\": \"test\", \"name\": \"Test\"},\n                            \"version\": i + 1,\n                            \"asset\": \"Cylinder\",\n                            \"representation\": \"mb\",\n                            \"root\": self.ROOT_DIR\n                        },\n                        \"dependencies\": [],\n                        \"name\": \"mb\",\n                        \"parent\": {\"oid\": '{}'.format(id)},\n                        \"data\": {\n                            \"path\": \"C:\\\\projects\\\\test_performance\\\\Assets\\\\Cylinder\\\\publish\\\\workfile\\\\workfileLookdev\\\\{}\\\\{}\".format(version_str, file_name),  # noqa: E501\n                            \"template\": \"{root[work]}\\\\{project[name]}\\\\{hierarchy}\\\\{asset}\\\\publish\\\\{family}\\\\{subset}\\\\v{version:0>3}\\\\{project[code]}_{asset}_{subset}_v{version:0>3}<_{output}><.{frame:0>4}>.{representation}\"  # noqa: E501\n                        },\n                        \"type\": \"representation\",\n                        \"schema\": \"openpype:representation-2.0\"\n                        }\n\n            insert_recs.append(document)\n\n        print('Prepared {} records in {} collection'.\n              format(no_of_records, self.collection_name))\n\n        self.collection.insert_many(insert_recs)\n        # TODO refactore to produce real array and not needeing ugly regex\n        self.collection.insert_one({\"inserted_id\": self.inserted_ids})\n        print('-' * 50)\n\n    def run(self, queries=1000, loops=3):\n        '''\n            Run X'queries' that are searching collection Y'loops' times\n        :param queries: how many times do ..find(...)\n        :param loops:  loop of testing X queries\n        :return: None\n        '''\n        print('Testing version {} on {}'.format(self.version,\n                                                self.collection_name))\n        print('Queries rung {} in {} loops'.format(queries, loops))\n\n        inserted_ids = list(self.collection.\n                            find({\"inserted_id\": {\"$exists\": True}}))\n        import re\n        self.ids = re.findall(\"'[0-9a-z]*'\", str(inserted_ids))\n\n        import time\n\n        found_cnt = 0\n        for _ in range(loops):\n            print('Starting loop {}'.format(_))\n            start = time.time()\n            for _ in range(queries):\n                # val = random.choice(self.ids)\n                # val = val.replace(\"'\", '')\n                val = random.randint(0, 50)\n                print(val)\n\n                if (self.version == 'array'):\n                    # prepared for partial index, without 'files': exists\n                    # wont engage\n                    found = self.collection.\\\n                        find({'files': {\"$exists\": True},\n                              'files.sites.name': \"local_{}\".format(val)}).\\\n                        count()\n                else:\n                    key = \"files.{}\".format(val)\n                    found = self.collection.find_one({key: {\"$exists\": True}})\n                print(\"found {} records\".format(found))\n                # if found:\n                #     found_cnt += len(list(found))\n\n            end = time.time()\n            print('duration per loop {}'.format(end - start))\n            print(\"found_cnt {}\".format(found_cnt))\n\n    def get_files(self, mode, i, file_id, file_id2, file_id3,\n                  create_files=False):\n        '''\n            Wrapper to decide if 'array' or document version should be used\n        :param mode: 'array'|'doc'\n        :param i: step number\n        :param file_id: ObjectId of first dummy file\n        :param file_id2: ..\n        :param file_id3: ..\n        :return:\n        '''\n        if mode == 'array':\n            return self.get_files_array(i, file_id, file_id2, file_id3,\n                                        create_files)\n        else:\n            return self.get_files_doc(i, file_id, file_id2, file_id3)\n\n    def get_files_array(self, i, file_id, file_id2, file_id3,\n                        create_files=False):\n        ret = [\n            {\n                 \"path\": \"{root[work]}\" + \"{root[work]}/test_performance/Assets/Cylinder/publish/workfile/workfileLookdev/v{:03d}/test_Cylinder_A_workfileLookdev_v{:03d}.dat\".format(i, i),  # noqa: E501\n                 \"_id\": '{}'.format(file_id),\n                 \"hash\": \"temphash\",\n                 \"sites\": self.get_sites(self.MAX_NUMBER_OF_SITES),\n                 \"size\": random.randint(0, self.MAX_FILE_SIZE_B)\n            },\n            {\n                \"path\": \"{root[work]}\" + \"/test_performance/Assets/Cylinder/publish/workfile/workfileLookdev/v{:03d}/test_Cylinder_B_workfileLookdev_v{:03d}.dat\".format(i, i),  # noqa: E501\n                \"_id\": '{}'.format(file_id2),\n                \"hash\": \"temphash\",\n                \"sites\": self.get_sites(self.MAX_NUMBER_OF_SITES),\n                \"size\": random.randint(0, self.MAX_FILE_SIZE_B)\n            },\n            {\n                \"path\": \"{root[work]}\" + \"/test_performance/Assets/Cylinder/publish/workfile/workfileLookdev/v{:03d}/test_Cylinder_C_workfileLookdev_v{:03d}.dat\".format(i, i),  # noqa: E501\n                \"_id\": '{}'.format(file_id3),\n                \"hash\": \"temphash\",\n                \"sites\": self.get_sites(self.MAX_NUMBER_OF_SITES),\n                \"size\": random.randint(0, self.MAX_FILE_SIZE_B)\n            }\n\n            ]\n        if create_files:\n            for f in ret:\n                path = f.get(\"path\").replace(\"{root[work]}\", self.ROOT_DIR)\n                os.makedirs(os.path.dirname(path), exist_ok=True)\n                with open(path, 'wb') as fp:\n                    fp.write(os.urandom(f.get(\"size\")))\n\n        return ret\n\n    def get_files_doc(self, i, file_id, file_id2, file_id3):\n        ret = {}\n        ret['{}'.format(file_id)] = {\n            \"path\": \"{root[work]}\" +\n                    \"/test_performance/Assets/Cylinder/publish/workfile/workfileLookdev/\"  # noqa: E501\n                    \"v{:03d}/test_CylinderA_workfileLookdev_v{:03d}.mb\".format(i, i),  # noqa: E501\n            \"hash\": \"temphash\",\n            \"sites\": [\"studio\"],\n            \"size\": 87236\n        }\n\n        ret['{}'.format(file_id2)] = {\n            \"path\": \"{root[work]}\" +\n                    \"/test_performance/Assets/Cylinder/publish/workfile/workfileLookdev/\"  # noqa: E501\n                    \"v{:03d}/test_CylinderB_workfileLookdev_v{:03d}.mb\".format(i, i),  # noqa: E501\n            \"hash\": \"temphash\",\n            \"sites\": [\"studio\"],\n            \"size\": 87236\n        }\n        ret['{}'.format(file_id3)] = {\n            \"path\": \"{root[work]}\" +\n                    \"/test_performance/Assets/Cylinder/publish/workfile/workfileLookdev/\"  # noqa: E501\n                    \"v{:03d}/test_CylinderC_workfileLookdev_v{:03d}.mb\".format(i, i),  # noqa: E501\n            \"hash\": \"temphash\",\n            \"sites\": [\"studio\"],\n            \"size\": 87236\n        }\n\n        return ret\n\n    def get_sites(self, number_of_sites=50):\n        \"\"\"\n            Return array of sites declaration.\n            Currently on 1st site has \"created_dt\" fillled, which should\n            trigger upload to 'gdrive' site.\n            'gdrive' site is appended, its destination for syncing for\n            Sync Server\n        Args:\n            number_of_sites:\n\n        Returns:\n\n        \"\"\"\n        sites = []\n        for i in range(number_of_sites):\n            site = {'name': \"local_{}\".format(i)}\n            # do not create null 'created_dt' field, Mongo doesnt like it\n            if i == 0:\n                site['created_dt'] = datetime.now()\n\n            sites.append(site)\n\n        sites.append({'name': \"gdrive\"})\n\n        return sites\n\n\nif __name__ == '__main__':\n    tp = TestPerformance('array')\n    tp.prepare(no_of_records=10000, create_files=True)\n    # tp.run(10, 3)\n\n    # print('-'*50)\n    #\n    # tp = TestPerformance('doc')\n    # tp.prepare()  # enable to prepare data\n    # tp.run(1000, 3)\n"
  },
  {
    "path": "openpype/tests/test_avalon_plugin_presets.py",
    "content": "from openpype.pipeline import (\n    install_host,\n    LegacyCreator,\n    register_creator_plugin,\n    discover_creator_plugins,\n)\n\n\nclass MyTestCreator(LegacyCreator):\n\n    my_test_property = \"A\"\n\n    def __init__(self, name, asset, options=None, data=None):\n        super(MyTestCreator, self).__init__(self, name, asset,\n                                            options=None, data=None)\n\n\n# this is hack like no other - we need to inject our own avalon host\n# and bypass all its validation. Avalon hosts are modules that needs\n# `ls` callable as attribute. Voila:\nclass Test:\n    __name__ = \"test\"\n    ls = len\n\n    @staticmethod\n    def install():\n        register_creator_plugin(MyTestCreator)\n\n\ndef test_avalon_plugin_presets(monkeypatch, printer):\n    install_host(Test)\n\n    plugins = discover_creator_plugins()\n    printer(\"Test if we got our test plugin\")\n    assert MyTestCreator in plugins\n    for p in plugins:\n        if p.__name__ == \"MyTestCreator\":\n            printer(\"Test if we have overridden existing property\")\n            assert p.my_test_property == \"B\"\n            printer(\"Test if we have overridden superclass property\")\n            assert p.active is False\n            printer(\"Test if we have added new property\")\n            assert p.new_property == \"new\"\n"
  },
  {
    "path": "openpype/tests/test_lib_restructuralization.py",
    "content": "# Test for backward compatibility of restructure of lib.py into lib library\n# Contains simple imports that should still work\n\n\ndef test_backward_compatibility(printer):\n    printer(\"Test if imports still work\")\n    try:\n        from openpype.lib import execute_hook\n        from openpype.lib import PypeHook\n\n        from openpype.lib import ApplicationLaunchFailed\n\n        from openpype.lib import get_ffmpeg_tool_path\n        from openpype.lib import get_last_version_from_path\n        from openpype.lib import get_paths_from_environ\n        from openpype.lib import get_version_from_path\n        from openpype.lib import version_up\n\n        from openpype.lib import get_ffprobe_streams\n\n        from openpype.lib import source_hash\n        from openpype.lib import run_subprocess\n\n    except ImportError as e:\n        raise\n"
  },
  {
    "path": "openpype/tests/test_pyblish_filter.py",
    "content": "import os\nimport pyblish.api\nimport pyblish.util\nimport pyblish.plugin\nfrom openpype.pipeline.publish.lib import filter_pyblish_plugins\nfrom . import lib\n\n\ndef test_pyblish_plugin_filter_modifier(printer, monkeypatch):\n    \"\"\"\n    Test if pyblish filter can filter and modify plugins on-the-fly.\n    \"\"\"\n\n    lib.setup_empty()\n    monkeypatch.setitem(os.environ, 'PYBLISHPLUGINPATH', '')\n    plugins = pyblish.api.registered_plugins()\n    printer(\"Test if we have no registered plugins\")\n    assert len(plugins) == 0\n    paths = pyblish.api.registered_paths()\n    printer(\"Test if we have no registered plugin paths\")\n    assert len(paths) == 0\n\n    class MyTestPlugin(pyblish.api.InstancePlugin):\n        my_test_property = 1\n        label = \"Collect Renderable Camera(s)\"\n        hosts = [\"test\"]\n        families = [\"default\"]\n\n    pyblish.api.register_host(\"test\")\n    pyblish.api.register_plugin(MyTestPlugin)\n    pyblish.api.register_discovery_filter(filter_pyblish_plugins)\n    plugins = pyblish.api.discover()\n\n    printer(\"Test if only one plugin was discovered\")\n    assert len(plugins) == 1\n    printer(\"Test if properties are modified correctly\")\n    assert plugins[0].label == \"loaded from preset\"\n    assert plugins[0].families == [\"changed\", \"by\", \"preset\"]\n    assert plugins[0].optional is True\n\n    lib.teardown()\n\n\ndef test_pyblish_plugin_filter_removal(monkeypatch):\n    \"\"\" Test that plugin can be removed by filter \"\"\"\n    lib.setup_empty()\n    monkeypatch.setitem(os.environ, 'PYBLISHPLUGINPATH', '')\n    plugins = pyblish.api.registered_plugins()\n\n    class MyTestRemovedPlugin(pyblish.api.InstancePlugin):\n        my_test_property = 1\n        label = \"Collect Renderable Camera(s)\"\n        hosts = [\"test\"]\n        families = [\"default\"]\n\n    pyblish.api.register_host(\"test\")\n    pyblish.api.register_plugin(MyTestRemovedPlugin)\n    pyblish.api.register_discovery_filter(filter_pyblish_plugins)\n    plugins = pyblish.api.discover()\n    assert len(plugins) == 0\n"
  },
  {
    "path": "openpype/tools/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/tools/adobe_webserver/app.py",
    "content": "\"\"\"This Webserver tool is python 3 specific.\n\nDon't import directly to avalon.tools or implementation of Python 2 hosts\nwould break.\n\"\"\"\nimport os\nimport logging\nimport urllib\nimport threading\nimport asyncio\nimport socket\n\nfrom aiohttp import web\n\nfrom wsrpc_aiohttp import (\n    WSRPCClient\n)\n\nfrom openpype.pipeline import get_global_context\n\nlog = logging.getLogger(__name__)\n\n\nclass WebServerTool:\n    \"\"\"\n        Basic POC implementation of asychronic websocket RPC server.\n        Uses class in external_app_1.py to mimic implementation for single\n        external application.\n        'test_client' folder contains two test implementations of client\n    \"\"\"\n    _instance = None\n\n    def __init__(self):\n        WebServerTool._instance = self\n\n        self.client = None\n        self.handlers = {}\n        self.on_stop_callbacks = []\n\n        port = None\n        host_name = \"localhost\"\n        websocket_url = os.getenv(\"WEBSOCKET_URL\")\n        if websocket_url:\n            parsed = urllib.parse.urlparse(websocket_url)\n            port = parsed.port\n            host_name = parsed.netloc.split(\":\")[0]\n        if not port:\n            port = 8098  # fallback\n\n        self.port = port\n        self.host_name = host_name\n\n        self.app = web.Application()\n\n        # add route with multiple methods for single \"external app\"\n        self.webserver_thread = WebServerThread(self, self.port)\n\n    def add_route(self, *args, **kwargs):\n        self.app.router.add_route(*args, **kwargs)\n\n    def add_static(self, *args, **kwargs):\n        self.app.router.add_static(*args, **kwargs)\n\n    def start_server(self):\n        if self.webserver_thread and not self.webserver_thread.is_alive():\n            self.webserver_thread.start()\n\n    def stop_server(self):\n        self.stop()\n\n    async def send_context_change(self, host):\n        \"\"\"\n            Calls running webserver to inform about context change\n\n            Used when new PS/AE should be triggered,\n            but one already running, without\n            this publish would point to old context.\n        \"\"\"\n        client = WSRPCClient(os.getenv(\"WEBSOCKET_URL\"),\n                             loop=asyncio.get_event_loop())\n        await client.connect()\n\n        context = get_global_context()\n        project = context[\"project_name\"]\n        asset = context[\"asset_name\"]\n        task = context[\"task_name\"]\n        log.info(\"Sending context change to {}-{}-{}\".format(project,\n                                                             asset,\n                                                             task))\n\n        await client.call('{}.set_context'.format(host),\n                          project=project, asset=asset, task=task)\n        await client.close()\n\n    def port_occupied(self, host_name, port):\n        \"\"\"\n            Check if 'url' is already occupied.\n\n            This could mean, that app is already running and we are trying open it\n            again. In that case, use existing running webserver.\n            Check here is easier than capturing exception from thread.\n        \"\"\"\n        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n        result = True\n        try:\n            sock.bind((host_name, port))\n            result = False\n        except:\n            print(\"Port is in use\")\n\n        return result\n\n    def call(self, func):\n        log.debug(\"websocket.call {}\".format(func))\n        future = asyncio.run_coroutine_threadsafe(\n            func,\n            self.webserver_thread.loop\n        )\n        result = future.result()\n        return result\n\n    @staticmethod\n    def get_instance():\n        if WebServerTool._instance is None:\n            WebServerTool()\n        return WebServerTool._instance\n\n    @property\n    def is_running(self):\n        if not self.webserver_thread:\n            return False\n        return self.webserver_thread.is_running\n\n    def stop(self):\n        if not self.is_running:\n            return\n        try:\n            log.debug(\"Stopping websocket server\")\n            self.webserver_thread.is_running = False\n            self.webserver_thread.stop()\n        except Exception:\n            log.warning(\n                \"Error has happened during Killing websocket server\",\n                exc_info=True\n            )\n\n    def thread_stopped(self):\n        for callback in self.on_stop_callbacks:\n            callback()\n\n\nclass WebServerThread(threading.Thread):\n    \"\"\" Listener for websocket rpc requests.\n\n        It would be probably better to \"attach\" this to main thread (as for\n        example Harmony needs to run something on main thread), but currently\n        it creates separate thread and separate asyncio event loop\n    \"\"\"\n    def __init__(self, module, port):\n        super(WebServerThread, self).__init__()\n\n        self.is_running = False\n        self.port = port\n        self.module = module\n        self.loop = None\n        self.runner = None\n        self.site = None\n        self.tasks = []\n\n    def run(self):\n        self.is_running = True\n\n        try:\n            log.info(\"Starting web server\")\n            self.loop = asyncio.new_event_loop()  # create new loop for thread\n            asyncio.set_event_loop(self.loop)\n\n            self.loop.run_until_complete(self.start_server())\n\n            websocket_url = \"ws://localhost:{}/ws\".format(self.port)\n\n            log.debug(\n                \"Running Websocket server on URL: \\\"{}\\\"\".format(websocket_url)\n            )\n\n            asyncio.ensure_future(self.check_shutdown(), loop=self.loop)\n            self.loop.run_forever()\n        except Exception:\n            self.is_running = False\n            log.warning(\n                \"Websocket Server service has failed\", exc_info=True\n            )\n            raise\n        finally:\n            self.loop.close()  # optional\n\n            self.is_running = False\n            self.module.thread_stopped()\n            log.info(\"Websocket server stopped\")\n\n    async def start_server(self):\n        \"\"\" Starts runner and TCPsite \"\"\"\n        self.runner = web.AppRunner(self.module.app)\n        await self.runner.setup()\n        self.site = web.TCPSite(self.runner, 'localhost', self.port)\n        await self.site.start()\n\n    def stop(self):\n        \"\"\"Sets is_running flag to false, 'check_shutdown' shuts server down\"\"\"\n        self.is_running = False\n\n    async def check_shutdown(self):\n        \"\"\" Future that is running and checks if server should be running\n            periodically.\n        \"\"\"\n        while self.is_running:\n            while self.tasks:\n                task = self.tasks.pop(0)\n                log.debug(\"waiting for task {}\".format(task))\n                await task\n                log.debug(\"returned value {}\".format(task.result))\n\n            await asyncio.sleep(0.5)\n\n        log.debug(\"Starting shutdown\")\n        await self.site.stop()\n        log.debug(\"Site stopped\")\n        await self.runner.cleanup()\n        log.debug(\"Runner stopped\")\n        tasks = [task for task in asyncio.all_tasks() if\n                 task is not asyncio.current_task()]\n        list(map(lambda task: task.cancel(), tasks))  # cancel all the tasks\n        results = await asyncio.gather(*tasks, return_exceptions=True)\n        log.debug(f'Finished awaiting cancelled tasks, results: {results}...')\n        await self.loop.shutdown_asyncgens()\n        # to really make sure everything else has time to stop\n        await asyncio.sleep(0.07)\n        self.loop.stop()\n"
  },
  {
    "path": "openpype/tools/adobe_webserver/readme.txt",
    "content": "Adobe webserver\n---------------\nAiohttp (Asyncio) based websocket server used for communication with host\napplications, currently only for Adobe (but could be used for any non python\nDCC which has websocket client).\n\nThis webserver is started in spawned Python process that opens DCC during\nits launch, waits for connection from DCC and handles communication going\nforward. Server is closed before Python process is killed.\n\n(Different from `openpype/modules/webserver` as that one is running in Tray,\nthis one is running in spawn Python process.)"
  },
  {
    "path": "openpype/tools/assetlinks/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/tools/assetlinks/widgets.py",
    "content": "import collections\nfrom openpype.client import (\n    get_versions,\n    get_subsets,\n    get_assets,\n    get_output_link_versions,\n)\n\nfrom qtpy import QtWidgets\n\n\nclass SimpleLinkView(QtWidgets.QWidget):\n    def __init__(self, dbcon, parent):\n        super(SimpleLinkView, self).__init__(parent=parent)\n        self.dbcon = dbcon\n\n        # TODO: display selected target\n\n        in_text = QtWidgets.QLabel(\"Inputs\")\n        in_view = QtWidgets.QListWidget(parent=self)\n        out_text = QtWidgets.QLabel(\"Outputs\")\n        out_view = QtWidgets.QListWidget(parent=self)\n\n        layout = QtWidgets.QGridLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(in_text, 0, 0)\n        layout.addWidget(in_view, 1, 0)\n        layout.addWidget(out_text, 0, 1)\n        layout.addWidget(out_view, 1, 1)\n\n        self._in_view = in_view\n        self._out_view = out_view\n        self._version_doc_to_process = None\n\n    @property\n    def project_name(self):\n        return self.dbcon.current_project()\n\n    def clear(self):\n        self._in_view.clear()\n        self._out_view.clear()\n\n    def set_version(self, version_doc):\n        self.clear()\n        self._version_doc_to_process = version_doc\n        if version_doc and self.isVisible():\n            self._fill_values()\n\n    def showEvent(self, event):\n        super(SimpleLinkView, self).showEvent(event)\n        self._fill_values()\n\n    def _fill_values(self):\n        if self._version_doc_to_process is None:\n            return\n        version_doc = self._version_doc_to_process\n        self._version_doc_to_process = None\n        self._fill_inputs(version_doc)\n        self._fill_outputs(version_doc)\n\n    def _fill_inputs(self, version_doc):\n        version_ids = set()\n        for link in version_doc[\"data\"].get(\"inputLinks\", []):\n            # Backwards compatibility for \"input\" key used as \"id\"\n            if \"id\" not in link:\n                link_id = link[\"input\"]\n            else:\n                link_id = link[\"id\"]\n            version_ids.add(link_id)\n\n        version_docs = list(get_versions(\n            self.project_name,\n            version_ids=version_ids,\n            fields=[\"name\", \"parent\"]\n        ))\n\n        versions_by_subset_id = collections.defaultdict(list)\n        for version_doc in version_docs:\n            subset_id = version_doc[\"parent\"]\n            versions_by_subset_id[subset_id].append(version_doc)\n\n        subset_docs = []\n        if versions_by_subset_id:\n            subset_docs = list(get_subsets(\n                self.project_name,\n                subset_ids=versions_by_subset_id.keys(),\n                fields=[\"_id\", \"name\", \"parent\"]\n            ))\n\n        asset_docs = []\n        subsets_by_asset_id = collections.defaultdict(list)\n        if subset_docs:\n            for subset_doc in subset_docs:\n                asset_id = subset_doc[\"parent\"]\n                subsets_by_asset_id[asset_id].append(subset_doc)\n\n            asset_docs = list(get_assets(\n                self.project_name,\n                asset_ids=subsets_by_asset_id.keys(),\n                fields=[\"_id\", \"name\"]\n            ))\n\n        for asset_doc in asset_docs:\n            asset_id = asset_doc[\"_id\"]\n            for subset_doc in subsets_by_asset_id[asset_id]:\n                subset_id = subset_doc[\"_id\"]\n                for version_doc in versions_by_subset_id[subset_id]:\n                    self._in_view.addItem(\"{} {} v{:0>3}\".format(\n                        asset_doc[\"name\"],\n                        subset_doc[\"name\"],\n                        version_doc[\"name\"],\n                    ))\n\n    def _fill_outputs(self, version_doc):\n        version_docs = list(get_output_link_versions(\n            self.project_name,\n            version_doc[\"_id\"],\n            fields=[\"name\", \"parent\"]\n        ))\n        versions_by_subset_id = collections.defaultdict(list)\n        for version_doc in version_docs:\n            subset_id = version_doc[\"parent\"]\n            versions_by_subset_id[subset_id].append(version_doc)\n\n        subset_docs = []\n        if versions_by_subset_id:\n            subset_docs = list(get_subsets(\n                self.project_name,\n                subset_ids=versions_by_subset_id.keys(),\n                fields=[\"_id\", \"name\", \"parent\"]\n            ))\n\n        asset_docs = []\n        subsets_by_asset_id = collections.defaultdict(list)\n        if subset_docs:\n            for subset_doc in subset_docs:\n                asset_id = subset_doc[\"parent\"]\n                subsets_by_asset_id[asset_id].append(subset_doc)\n\n            asset_docs = list(get_assets(\n                self.project_name,\n                asset_ids=subsets_by_asset_id.keys(),\n                fields=[\"_id\", \"name\"]\n            ))\n\n        for asset_doc in asset_docs:\n            asset_id = asset_doc[\"_id\"]\n            for subset_doc in subsets_by_asset_id[asset_id]:\n                subset_id = subset_doc[\"_id\"]\n                for version_doc in versions_by_subset_id[subset_id]:\n                    self._out_view.addItem(\"{} {} v{:0>3}\".format(\n                        asset_doc[\"name\"],\n                        subset_doc[\"name\"],\n                        version_doc[\"name\"],\n                    ))\n"
  },
  {
    "path": "openpype/tools/attribute_defs/__init__.py",
    "content": "from .widgets import (\n    create_widget_for_attr_def,\n    AttributeDefinitionsWidget,\n)\n\nfrom .dialog import (\n    AttributeDefinitionsDialog,\n)\n\n\n__all__ = (\n    \"create_widget_for_attr_def\",\n    \"AttributeDefinitionsWidget\",\n\n    \"AttributeDefinitionsDialog\",\n)\n"
  },
  {
    "path": "openpype/tools/attribute_defs/dialog.py",
    "content": "from qtpy import QtWidgets\n\nfrom .widgets import AttributeDefinitionsWidget\n\n\nclass AttributeDefinitionsDialog(QtWidgets.QDialog):\n    def __init__(self, attr_defs, parent=None):\n        super(AttributeDefinitionsDialog, self).__init__(parent)\n\n        attrs_widget = AttributeDefinitionsWidget(attr_defs, self)\n\n        btns_widget = QtWidgets.QWidget(self)\n        ok_btn = QtWidgets.QPushButton(\"OK\", btns_widget)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", btns_widget)\n\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(ok_btn, 0)\n        btns_layout.addWidget(cancel_btn, 0)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(attrs_widget, 0)\n        main_layout.addStretch(1)\n        main_layout.addWidget(btns_widget, 0)\n\n        ok_btn.clicked.connect(self.accept)\n        cancel_btn.clicked.connect(self.reject)\n\n        self._attrs_widget = attrs_widget\n\n    def get_values(self):\n        return self._attrs_widget.current_value()\n"
  },
  {
    "path": "openpype/tools/attribute_defs/files_widget.py",
    "content": "import os\nimport collections\nimport uuid\nimport json\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.lib import FileDefItem\nfrom openpype.tools.utils import (\n    paint_image_with_color,\n    ClickableLabel,\n)\n# TODO change imports\nfrom openpype.tools.resources import get_image\nfrom openpype.tools.utils import (\n    IconButton,\n    PixmapLabel\n)\n\nITEM_ID_ROLE = QtCore.Qt.UserRole + 1\nITEM_LABEL_ROLE = QtCore.Qt.UserRole + 2\nITEM_ICON_ROLE = QtCore.Qt.UserRole + 3\nFILENAMES_ROLE = QtCore.Qt.UserRole + 4\nDIRPATH_ROLE = QtCore.Qt.UserRole + 5\nIS_DIR_ROLE = QtCore.Qt.UserRole + 6\nIS_SEQUENCE_ROLE = QtCore.Qt.UserRole + 7\nEXT_ROLE = QtCore.Qt.UserRole + 8\n\n\ndef convert_bytes_to_json(bytes_value):\n    if isinstance(bytes_value, QtCore.QByteArray):\n        # Raw data are already QByteArray and we don't have to load them\n        encoded_data = bytes_value\n    else:\n        encoded_data = QtCore.QByteArray.fromRawData(bytes_value)\n    stream = QtCore.QDataStream(encoded_data, QtCore.QIODevice.ReadOnly)\n    text = stream.readQString()\n    try:\n        return json.loads(text)\n    except Exception:\n        return None\n\n\ndef convert_data_to_bytes(data):\n    bytes_value = QtCore.QByteArray()\n    stream = QtCore.QDataStream(bytes_value, QtCore.QIODevice.WriteOnly)\n    stream.writeQString(json.dumps(data))\n    return bytes_value\n\n\nclass SupportLabel(QtWidgets.QLabel):\n    pass\n\n\nclass DropEmpty(QtWidgets.QWidget):\n    _empty_extensions = \"Any file\"\n\n    def __init__(self, single_item, allow_sequences, extensions_label, parent):\n        super(DropEmpty, self).__init__(parent)\n\n        drop_label_widget = QtWidgets.QLabel(\"Drag & Drop files here\", self)\n\n        items_label_widget = SupportLabel(self)\n        items_label_widget.setWordWrap(True)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addSpacing(20)\n        layout.addWidget(\n            drop_label_widget, 0, alignment=QtCore.Qt.AlignCenter\n        )\n        layout.addSpacing(30)\n        layout.addStretch(1)\n        layout.addWidget(\n            items_label_widget, 0, alignment=QtCore.Qt.AlignCenter\n        )\n        layout.addSpacing(10)\n\n        for widget in (\n            drop_label_widget,\n            items_label_widget,\n        ):\n            widget.setAlignment(QtCore.Qt.AlignCenter)\n            widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        update_size_timer = QtCore.QTimer()\n        update_size_timer.setInterval(10)\n        update_size_timer.setSingleShot(True)\n\n        update_size_timer.timeout.connect(self._on_update_size_timer)\n\n        self._update_size_timer = update_size_timer\n\n        if extensions_label and not extensions_label.startswith(\" \"):\n            extensions_label = \" \" + extensions_label\n\n        self._single_item = single_item\n        self._extensions_label = extensions_label\n        self._allow_sequences = allow_sequences\n        self._allowed_extensions = set()\n        self._allow_folders = None\n\n        self._drop_label_widget = drop_label_widget\n        self._items_label_widget = items_label_widget\n\n        self.set_allow_folders(False)\n\n    def set_extensions(self, extensions):\n        if extensions:\n            extensions = {\n                ext.replace(\".\", \"\")\n                for ext in extensions\n            }\n        if extensions == self._allowed_extensions:\n            return\n        self._allowed_extensions = extensions\n\n        self._update_items_label()\n\n    def set_allow_folders(self, allowed):\n        if self._allow_folders == allowed:\n            return\n\n        self._allow_folders = allowed\n        self._update_items_label()\n\n    def _update_items_label(self):\n        allowed_items = []\n        if self._allow_folders:\n            allowed_items.append(\"folder\")\n\n        if self._allowed_extensions:\n            allowed_items.append(\"file\")\n            if self._allow_sequences:\n                allowed_items.append(\"sequence\")\n\n        if not self._single_item:\n            allowed_items = [item + \"s\" for item in allowed_items]\n\n        if not allowed_items:\n            self._drop_label_widget.setVisible(False)\n            self._items_label_widget.setText(\n                \"It is not allowed to add anything here!\"\n            )\n            return\n\n        self._drop_label_widget.setVisible(True)\n        items_label = \"Multiple \"\n        if self._single_item:\n            items_label = \"Single \"\n\n        if len(allowed_items) == 1:\n            extensions_label = allowed_items[0]\n        elif len(allowed_items) == 2:\n            extensions_label = \" or \".join(allowed_items)\n        else:\n            last_item = allowed_items.pop(-1)\n            new_last_item = \" or \".join([last_item, allowed_items.pop(-1)])\n            allowed_items.append(new_last_item)\n            extensions_label = \", \".join(allowed_items)\n\n        allowed_items_label = extensions_label\n\n        items_label += allowed_items_label\n        label_tooltip = None\n        if self._allowed_extensions:\n            items_label += \" of\\n{}\".format(\n                \", \".join(sorted(self._allowed_extensions))\n            )\n\n        if self._extensions_label:\n            label_tooltip = items_label\n            items_label = self._extensions_label\n\n        if self._items_label_widget.text() == items_label:\n            return\n\n        self._items_label_widget.setToolTip(label_tooltip)\n        self._items_label_widget.setText(items_label)\n        self._update_size_timer.start()\n\n    def resizeEvent(self, event):\n        super(DropEmpty, self).resizeEvent(event)\n        self._update_size_timer.start()\n\n    def _on_update_size_timer(self):\n        \"\"\"Recalculate height of label with extensions.\n\n        Dynamic QLabel with word wrap does not handle properly it's sizeHint\n        calculations on show. This way it is recalculated. It is good practice\n        to trigger this method with small offset using '_update_size_timer'.\n        \"\"\"\n\n        width = self._items_label_widget.width()\n        height = self._items_label_widget.heightForWidth(width)\n        self._items_label_widget.setMinimumHeight(height)\n        self._items_label_widget.updateGeometry()\n\n    def paintEvent(self, event):\n        super(DropEmpty, self).paintEvent(event)\n\n        pen = QtGui.QPen()\n        pen.setBrush(QtCore.Qt.darkGray)\n        pen.setStyle(QtCore.Qt.DashLine)\n        pen.setWidth(1)\n\n        content_margins = self.layout().contentsMargins()\n        rect = self.rect()\n        left_m = content_margins.left() + pen.width()\n        top_m = content_margins.top() + pen.width()\n        new_rect = QtCore.QRect(\n            left_m,\n            top_m,\n            (\n                rect.width()\n                - (left_m + content_margins.right() + pen.width())\n            ),\n            (\n                rect.height()\n                - (top_m + content_margins.bottom() + pen.width())\n            )\n        )\n\n        painter = QtGui.QPainter(self)\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n        painter.setPen(pen)\n        painter.drawRect(new_rect)\n\n\nclass FilesModel(QtGui.QStandardItemModel):\n    def __init__(self, single_item, allow_sequences):\n        super(FilesModel, self).__init__()\n\n        self._id = str(uuid.uuid4())\n        self._single_item = single_item\n        self._multivalue = False\n        self._allow_sequences = allow_sequences\n\n        self._items_by_id = {}\n        self._file_items_by_id = {}\n        self._filenames_by_dirpath = collections.defaultdict(set)\n        self._items_by_dirpath = collections.defaultdict(list)\n\n        self.rowsAboutToBeRemoved.connect(self._on_about_to_be_removed)\n        self.rowsInserted.connect(self._on_insert)\n\n    @property\n    def id(self):\n        return self._id\n\n    def _on_about_to_be_removed(self, parent_index, start, end):\n        \"\"\"Make sure that removed items are removed from items mapping.\n\n        Connected with '_on_insert'. When user drag item and drop it to same\n        view the item is actually removed and creted again but it happens in\n        inner calls of Qt.\n        \"\"\"\n\n        for row in range(start, end + 1):\n            index = self.index(row, 0, parent_index)\n            item_id = index.data(ITEM_ID_ROLE)\n            if item_id is not None:\n                self._items_by_id.pop(item_id, None)\n\n    def _on_insert(self, parent_index, start, end):\n        \"\"\"Make sure new added items are stored in items mapping.\n\n        Connected to '_on_about_to_be_removed'. Some items are not created\n        using '_create_item' but are recreated using Qt. So the item is not in\n        mapping and if it would it would not lead to same item pointer.\n        \"\"\"\n\n        for row in range(start, end + 1):\n            index = self.index(start, end, parent_index)\n            item_id = index.data(ITEM_ID_ROLE)\n            if item_id not in self._items_by_id:\n                self._items_by_id[item_id] = self.item(row)\n\n    def set_multivalue(self, multivalue):\n        \"\"\"Disable filtering.\"\"\"\n\n        if self._multivalue == multivalue:\n            return\n        self._multivalue = multivalue\n\n    def add_filepaths(self, items):\n        if not items:\n            return\n\n        if self._multivalue:\n            _items = []\n            for item in items:\n                if isinstance(item, (tuple, list, set)):\n                    _items.extend(item)\n                else:\n                    _items.append(item)\n            items = _items\n\n        file_items = FileDefItem.from_value(items, self._allow_sequences)\n        if not file_items:\n            return\n\n        if not self._multivalue and self._single_item:\n            file_items = [file_items[0]]\n            current_ids = list(self._file_items_by_id.keys())\n            if current_ids:\n                self.remove_item_by_ids(current_ids)\n\n        new_model_items = []\n        for file_item in file_items:\n            item_id, model_item = self._create_item(file_item)\n            new_model_items.append(model_item)\n            self._file_items_by_id[item_id] = file_item\n            self._items_by_id[item_id] = model_item\n\n        if new_model_items:\n            roow_item = self.invisibleRootItem()\n            roow_item.appendRows(new_model_items)\n\n    def remove_item_by_ids(self, item_ids):\n        if not item_ids:\n            return\n\n        items = []\n        for item_id in set(item_ids):\n            if item_id not in self._items_by_id:\n                continue\n            item = self._items_by_id.pop(item_id)\n            self._file_items_by_id.pop(item_id)\n            items.append(item)\n\n        if items:\n            for item in items:\n                self.removeRows(item.row(), 1)\n\n    def get_file_item_by_id(self, item_id):\n        return self._file_items_by_id.get(item_id)\n\n    def _create_item(self, file_item):\n        if file_item.is_dir:\n            icon_pixmap = paint_image_with_color(\n                get_image(filename=\"folder.png\"), QtCore.Qt.white\n            )\n        else:\n            icon_pixmap = paint_image_with_color(\n                get_image(filename=\"file.png\"), QtCore.Qt.white\n            )\n\n        item = QtGui.QStandardItem()\n        item_id = str(uuid.uuid4())\n        item.setData(item_id, ITEM_ID_ROLE)\n        item.setData(file_item.label or \"< empty >\", ITEM_LABEL_ROLE)\n        item.setData(file_item.filenames, FILENAMES_ROLE)\n        item.setData(file_item.directory, DIRPATH_ROLE)\n        item.setData(icon_pixmap, ITEM_ICON_ROLE)\n        item.setData(file_item.lower_ext, EXT_ROLE)\n        item.setData(file_item.is_dir, IS_DIR_ROLE)\n        item.setData(file_item.is_sequence, IS_SEQUENCE_ROLE)\n\n        return item_id, item\n\n    def mimeData(self, indexes):\n        item_ids = [\n            index.data(ITEM_ID_ROLE)\n            for index in indexes\n        ]\n\n        item_ids_data = convert_data_to_bytes(item_ids)\n        mime_data = super(FilesModel, self).mimeData(indexes)\n        mime_data.setData(\"files_widget/internal_move\", item_ids_data)\n\n        file_items = []\n        for item_id in item_ids:\n            file_item = self.get_file_item_by_id(item_id)\n            if file_item:\n                file_items.append(file_item.to_dict())\n\n        full_item_data = convert_data_to_bytes({\n            \"items\": file_items,\n            \"id\": self._id\n        })\n        mime_data.setData(\"files_widget/full_data\", full_item_data)\n        return mime_data\n\n    def dropMimeData(self, mime_data, action, row, col, index):\n        item_ids = convert_bytes_to_json(\n            mime_data.data(\"files_widget/internal_move\")\n        )\n        if item_ids is None:\n            return False\n\n        # Find matching item after which will be items moved\n        #   - store item before moved items are removed\n        root = self.invisibleRootItem()\n        if row >= 0:\n            src_item = self.item(row)\n        else:\n            src_item_id = index.data(ITEM_ID_ROLE)\n            src_item = self._items_by_id.get(src_item_id)\n\n        src_row = None\n        if src_item:\n            src_row = src_item.row()\n\n        # Take out items that should be moved\n        items = []\n        for item_id in item_ids:\n            item = self._items_by_id.get(item_id)\n            if item:\n                self.takeRow(item.row())\n                items.append(item)\n\n        # Skip if there are not items that can be moved\n        if not items:\n            return False\n\n        # Calculate row where items should be inserted\n        row_count = root.rowCount()\n        if src_row is None:\n            src_row = row_count\n\n        if src_row > row_count:\n            src_row = row_count\n\n        root.insertRow(src_row, items)\n        return True\n\n\nclass FilesProxyModel(QtCore.QSortFilterProxyModel):\n    def __init__(self, *args, **kwargs):\n        super(FilesProxyModel, self).__init__(*args, **kwargs)\n        self._allow_folders = False\n        self._allowed_extensions = None\n        self._multivalue = False\n\n    def set_multivalue(self, multivalue):\n        \"\"\"Disable filtering.\"\"\"\n\n        if self._multivalue == multivalue:\n            return\n        self._multivalue = multivalue\n        self.invalidateFilter()\n\n    def set_allow_folders(self, allow=None):\n        if allow is None:\n            allow = not self._allow_folders\n\n        if allow == self._allow_folders:\n            return\n        self._allow_folders = allow\n        self.invalidateFilter()\n\n    def set_allowed_extensions(self, extensions=None):\n        if extensions is not None:\n            _extensions = set()\n            for ext in set(extensions):\n                if not ext.startswith(\".\"):\n                    ext = \".{}\".format(ext)\n                _extensions.add(ext.lower())\n            extensions = _extensions\n\n        if self._allowed_extensions != extensions:\n            self._allowed_extensions = extensions\n            self.invalidateFilter()\n\n    def are_valid_files(self, filepaths):\n        for filepath in filepaths:\n            if os.path.isfile(filepath):\n                _, ext = os.path.splitext(filepath)\n                if ext.lower() in self._allowed_extensions:\n                    return True\n\n            elif self._allow_folders:\n                return True\n        return False\n\n    def filter_valid_files(self, filepaths):\n        filtered_paths = []\n        for filepath in filepaths:\n            if os.path.isfile(filepath):\n                _, ext = os.path.splitext(filepath)\n                if ext.lower() in self._allowed_extensions:\n                    filtered_paths.append(filepath)\n\n            elif self._allow_folders:\n                filtered_paths.append(filepath)\n        return filtered_paths\n\n    def filterAcceptsRow(self, row, parent_index):\n        # Skip filtering if multivalue is set\n        if self._multivalue:\n            return True\n\n        model = self.sourceModel()\n        index = model.index(row, self.filterKeyColumn(), parent_index)\n        # First check if item is folder and if folders are enabled\n        if index.data(IS_DIR_ROLE):\n            if not self._allow_folders:\n                return False\n            return True\n\n        # Check if there are any allowed extensions\n        if self._allowed_extensions is None:\n            return False\n\n        if index.data(EXT_ROLE) not in self._allowed_extensions:\n            return False\n        return True\n\n    def lessThan(self, left, right):\n        left_comparison = left.data(DIRPATH_ROLE)\n        right_comparison = right.data(DIRPATH_ROLE)\n        if left_comparison == right_comparison:\n            left_comparison = left.data(ITEM_LABEL_ROLE)\n            right_comparison = right.data(ITEM_LABEL_ROLE)\n\n        if sorted((left_comparison, right_comparison))[0] == left_comparison:\n            return True\n        return False\n\n\nclass ItemWidget(QtWidgets.QWidget):\n    context_menu_requested = QtCore.Signal(QtCore.QPoint)\n\n    def __init__(\n        self, item_id, label, pixmap_icon, is_sequence, multivalue, parent=None\n    ):\n        self._item_id = item_id\n\n        super(ItemWidget, self).__init__(parent)\n\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        icon_widget = PixmapLabel(pixmap_icon, self)\n        label_widget = QtWidgets.QLabel(label, self)\n\n        label_size_hint = label_widget.sizeHint()\n        height = label_size_hint.height()\n        actions_menu_pix = paint_image_with_color(\n            get_image(filename=\"menu.png\"), QtCore.Qt.white\n        )\n\n        split_btn = ClickableLabel(self)\n        split_btn.setFixedSize(height, height)\n        split_btn.setPixmap(actions_menu_pix)\n        if multivalue:\n            split_btn.setVisible(False)\n        else:\n            split_btn.setVisible(is_sequence)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(5, 5, 5, 5)\n        layout.addWidget(icon_widget, 0)\n        layout.addWidget(label_widget, 1)\n        layout.addWidget(split_btn, 0)\n\n        split_btn.clicked.connect(self._on_actions_clicked)\n\n        self._icon_widget = icon_widget\n        self._label_widget = label_widget\n        self._split_btn = split_btn\n        self._actions_menu_pix = actions_menu_pix\n        self._last_scaled_pix_height = None\n\n    def _update_btn_size(self):\n        label_size_hint = self._label_widget.sizeHint()\n        height = label_size_hint.height()\n        if height == self._last_scaled_pix_height:\n            return\n        self._last_scaled_pix_height = height\n        self._split_btn.setFixedSize(height, height)\n        pix = self._actions_menu_pix.scaled(\n            height, height,\n            QtCore.Qt.KeepAspectRatio,\n            QtCore.Qt.SmoothTransformation\n        )\n        self._split_btn.setPixmap(pix)\n\n    def showEvent(self, event):\n        super(ItemWidget, self).showEvent(event)\n        self._update_btn_size()\n\n    def resizeEvent(self, event):\n        super(ItemWidget, self).resizeEvent(event)\n        self._update_btn_size()\n\n    def _on_actions_clicked(self):\n        pos = self._split_btn.rect().bottomLeft()\n        point = self._split_btn.mapToGlobal(pos)\n        self.context_menu_requested.emit(point)\n\n\nclass InViewButton(IconButton):\n    pass\n\n\nclass FilesView(QtWidgets.QListView):\n    \"\"\"View showing instances and their groups.\"\"\"\n\n    remove_requested = QtCore.Signal()\n    context_menu_requested = QtCore.Signal(QtCore.QPoint)\n\n    def __init__(self, *args, **kwargs):\n        super(FilesView, self).__init__(*args, **kwargs)\n\n        self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\n        self.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection\n        )\n        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        self.setAcceptDrops(True)\n        self.setDragEnabled(True)\n        self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)\n\n        remove_btn = InViewButton(self)\n        pix_enabled = paint_image_with_color(\n            get_image(filename=\"delete.png\"), QtCore.Qt.white\n        )\n        pix_disabled = paint_image_with_color(\n            get_image(filename=\"delete.png\"), QtCore.Qt.gray\n        )\n        icon = QtGui.QIcon(pix_enabled)\n        icon.addPixmap(pix_disabled, QtGui.QIcon.Disabled, QtGui.QIcon.Off)\n        remove_btn.setIcon(icon)\n        remove_btn.setEnabled(False)\n\n        remove_btn.clicked.connect(self._on_remove_clicked)\n        self.customContextMenuRequested.connect(self._on_context_menu_request)\n\n        self._remove_btn = remove_btn\n        self._multivalue = False\n\n    def setSelectionModel(self, *args, **kwargs):\n        \"\"\"Catch selection model set to register signal callback.\n\n        Selection model is not available during initialization.\n        \"\"\"\n\n        super(FilesView, self).setSelectionModel(*args, **kwargs)\n        selection_model = self.selectionModel()\n        selection_model.selectionChanged.connect(self._on_selection_change)\n\n    def set_multivalue(self, multivalue):\n        \"\"\"Disable remove button on multivalue.\"\"\"\n\n        self._multivalue = multivalue\n        self._remove_btn.setVisible(not multivalue)\n\n    def update_remove_btn_visibility(self):\n        model = self.model()\n        visible = False\n        if not self._multivalue and model:\n            visible = model.rowCount() > 0\n        self._remove_btn.setVisible(visible)\n\n    def has_selected_item_ids(self):\n        \"\"\"Is any index selected.\"\"\"\n        for index in self.selectionModel().selectedIndexes():\n            instance_id = index.data(ITEM_ID_ROLE)\n            if instance_id is not None:\n                return True\n        return False\n\n    def get_selected_item_ids(self):\n        \"\"\"Ids of selected instances.\"\"\"\n\n        selected_item_ids = set()\n        for index in self.selectionModel().selectedIndexes():\n            instance_id = index.data(ITEM_ID_ROLE)\n            if instance_id is not None:\n                selected_item_ids.add(instance_id)\n        return selected_item_ids\n\n    def has_selected_sequence(self):\n        for index in self.selectionModel().selectedIndexes():\n            if index.data(IS_SEQUENCE_ROLE):\n                return True\n        return False\n\n    def event(self, event):\n        if event.type() == QtCore.QEvent.KeyPress:\n            if (\n                event.key() == QtCore.Qt.Key_Delete\n                and self.has_selected_item_ids()\n            ):\n                self.remove_requested.emit()\n                return True\n\n        return super(FilesView, self).event(event)\n\n    def _on_context_menu_request(self, pos):\n        index = self.indexAt(pos)\n        if index.isValid():\n            point = self.viewport().mapToGlobal(pos)\n            self.context_menu_requested.emit(point)\n\n    def _on_selection_change(self):\n        self._remove_btn.setEnabled(self.has_selected_item_ids())\n\n    def _on_remove_clicked(self):\n        self.remove_requested.emit()\n\n    def _update_remove_btn(self):\n        \"\"\"Position remove button to bottom right.\"\"\"\n\n        viewport = self.viewport()\n        height = viewport.height()\n        pos_x = viewport.width() - self._remove_btn.width() - 5\n        pos_y = height - self._remove_btn.height() - 5\n        self._remove_btn.move(max(0, pos_x), max(0, pos_y))\n\n    def resizeEvent(self, event):\n        super(FilesView, self).resizeEvent(event)\n        self._update_remove_btn()\n\n    def showEvent(self, event):\n        super(FilesView, self).showEvent(event)\n        self._update_remove_btn()\n        self.update_remove_btn_visibility()\n\n\nclass FilesWidget(QtWidgets.QFrame):\n    value_changed = QtCore.Signal()\n\n    def __init__(self, single_item, allow_sequences, extensions_label, parent):\n        super(FilesWidget, self).__init__(parent)\n        self.setAcceptDrops(True)\n\n        empty_widget = DropEmpty(\n            single_item, allow_sequences, extensions_label, self\n        )\n\n        files_model = FilesModel(single_item, allow_sequences)\n        files_proxy_model = FilesProxyModel()\n        files_proxy_model.setSourceModel(files_model)\n        files_view = FilesView(self)\n        files_view.setModel(files_proxy_model)\n\n        layout = QtWidgets.QStackedLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setStackingMode(QtWidgets.QStackedLayout.StackAll)\n        layout.addWidget(empty_widget)\n        layout.addWidget(files_view)\n        layout.setCurrentWidget(empty_widget)\n\n        files_proxy_model.rowsInserted.connect(self._on_rows_inserted)\n        files_proxy_model.rowsRemoved.connect(self._on_rows_removed)\n        files_view.remove_requested.connect(self._on_remove_requested)\n        files_view.context_menu_requested.connect(\n            self._on_context_menu_requested\n        )\n\n        self._in_set_value = False\n        self._single_item = single_item\n        self._multivalue = False\n\n        self._empty_widget = empty_widget\n        self._files_model = files_model\n        self._files_proxy_model = files_proxy_model\n        self._files_view = files_view\n\n        self._widgets_by_id = {}\n\n        self._layout = layout\n\n    def _set_multivalue(self, multivalue):\n        if self._multivalue is multivalue:\n            return\n        self._multivalue = multivalue\n        self._files_view.set_multivalue(multivalue)\n        self._files_model.set_multivalue(multivalue)\n        self._files_proxy_model.set_multivalue(multivalue)\n        self.setEnabled(not multivalue)\n\n    def set_value(self, value, multivalue):\n        self._in_set_value = True\n\n        widget_ids = set(self._widgets_by_id.keys())\n        self._remove_item_by_ids(widget_ids)\n\n        self._set_multivalue(multivalue)\n\n        self._add_filepaths(value)\n\n        self._in_set_value = False\n\n    def current_value(self):\n        model = self._files_proxy_model\n        item_ids = set()\n        for row in range(model.rowCount()):\n            index = model.index(row, 0)\n            item_ids.add(index.data(ITEM_ID_ROLE))\n\n        file_items = []\n        for item_id in item_ids:\n            file_item = self._files_model.get_file_item_by_id(item_id)\n            if file_item is not None:\n                file_items.append(file_item.to_dict())\n\n        if not self._single_item:\n            return file_items\n        if file_items:\n            return file_items[0]\n\n        empty_item = FileDefItem.create_empty_item()\n        return empty_item.to_dict()\n\n    def set_filters(self, folders_allowed, exts_filter):\n        self._files_proxy_model.set_allow_folders(folders_allowed)\n        self._files_proxy_model.set_allowed_extensions(exts_filter)\n        self._empty_widget.set_extensions(exts_filter)\n        self._empty_widget.set_allow_folders(folders_allowed)\n\n    def _on_rows_inserted(self, parent_index, start_row, end_row):\n        for row in range(start_row, end_row + 1):\n            index = self._files_proxy_model.index(row, 0, parent_index)\n            item_id = index.data(ITEM_ID_ROLE)\n            if item_id in self._widgets_by_id:\n                continue\n            label = index.data(ITEM_LABEL_ROLE)\n            pixmap_icon = index.data(ITEM_ICON_ROLE)\n            is_sequence = index.data(IS_SEQUENCE_ROLE)\n\n            widget = ItemWidget(\n                item_id,\n                label,\n                pixmap_icon,\n                is_sequence,\n                self._multivalue\n            )\n            widget.context_menu_requested.connect(\n                self._on_context_menu_requested\n            )\n            self._files_view.setIndexWidget(index, widget)\n            self._files_proxy_model.setData(\n                index, widget.sizeHint(), QtCore.Qt.SizeHintRole\n            )\n            self._widgets_by_id[item_id] = widget\n\n        if not self._in_set_value:\n            self.value_changed.emit()\n\n        self._update_visibility()\n\n    def _on_rows_removed(self, parent_index, start_row, end_row):\n        available_item_ids = set()\n        for row in range(self._files_proxy_model.rowCount()):\n            index = self._files_proxy_model.index(row, 0)\n            item_id = index.data(ITEM_ID_ROLE)\n            available_item_ids.add(index.data(ITEM_ID_ROLE))\n\n        widget_ids = set(self._widgets_by_id.keys())\n        for item_id in available_item_ids:\n            if item_id in widget_ids:\n                widget_ids.remove(item_id)\n\n        for item_id in widget_ids:\n            widget = self._widgets_by_id.pop(item_id)\n            widget.setVisible(False)\n            widget.deleteLater()\n\n        if not self._in_set_value:\n            self.value_changed.emit()\n        self._update_visibility()\n\n    def _on_split_request(self):\n        if self._multivalue:\n            return\n\n        item_ids = self._files_view.get_selected_item_ids()\n        if not item_ids:\n            return\n\n        for item_id in item_ids:\n            file_item = self._files_model.get_file_item_by_id(item_id)\n            if not file_item:\n                return\n\n            new_items = file_item.split_sequence()\n            self._add_filepaths(new_items)\n        self._remove_item_by_ids(item_ids)\n\n    def _on_remove_requested(self):\n        if self._multivalue:\n            return\n\n        items_to_delete = self._files_view.get_selected_item_ids()\n        if items_to_delete:\n            self._remove_item_by_ids(items_to_delete)\n\n    def _on_context_menu_requested(self, pos):\n        if self._multivalue:\n            return\n\n        menu = QtWidgets.QMenu(self._files_view)\n\n        if self._files_view.has_selected_sequence():\n            split_action = QtWidgets.QAction(\"Split sequence\", menu)\n            split_action.triggered.connect(self._on_split_request)\n            menu.addAction(split_action)\n\n        remove_action = QtWidgets.QAction(\"Remove\", menu)\n        remove_action.triggered.connect(self._on_remove_requested)\n        menu.addAction(remove_action)\n\n        menu.popup(pos)\n\n    def dragEnterEvent(self, event):\n        if self._multivalue:\n            return\n\n        mime_data = event.mimeData()\n        if mime_data.hasUrls():\n            filepaths = []\n            for url in mime_data.urls():\n                filepath = url.toLocalFile()\n                if os.path.exists(filepath):\n                    filepaths.append(filepath)\n\n            if self._files_proxy_model.are_valid_files(filepaths):\n                event.setDropAction(QtCore.Qt.CopyAction)\n                event.accept()\n\n        full_data_value = mime_data.data(\"files_widget/full_data\")\n        if self._handle_full_data_drag(full_data_value):\n            event.setDropAction(QtCore.Qt.CopyAction)\n            event.accept()\n\n    def dragLeaveEvent(self, event):\n        event.accept()\n\n    def dropEvent(self, event):\n        if self._multivalue:\n            return\n\n        mime_data = event.mimeData()\n        if mime_data.hasUrls():\n            event.accept()\n            filepaths = []\n            for url in mime_data.urls():\n                filepath = url.toLocalFile()\n                if os.path.exists(filepath):\n                    filepaths.append(filepath)\n\n            # Filter filepaths before passing it to model\n            filepaths = self._files_proxy_model.filter_valid_files(filepaths)\n            if filepaths:\n                self._add_filepaths(filepaths)\n\n        if self._handle_full_data_drop(\n            mime_data.data(\"files_widget/full_data\")\n        ):\n            event.setDropAction(QtCore.Qt.CopyAction)\n            event.accept()\n\n        super(FilesWidget, self).dropEvent(event)\n\n    def _handle_full_data_drag(self, value):\n        if value is None:\n            return False\n\n        full_data = convert_bytes_to_json(value)\n        if full_data is None:\n            return False\n\n        if full_data[\"id\"] == self._files_model.id:\n            return False\n        return True\n\n    def _handle_full_data_drop(self, value):\n        if value is None:\n            return False\n\n        full_data = convert_bytes_to_json(value)\n        if full_data is None:\n            return False\n\n        if full_data[\"id\"] == self._files_model.id:\n            return False\n\n        for item in full_data[\"items\"]:\n            filepaths = [\n                os.path.join(item[\"directory\"], filename)\n                for filename in item[\"filenames\"]\n            ]\n            filepaths = self._files_proxy_model.filter_valid_files(filepaths)\n            if filepaths:\n                self._add_filepaths(filepaths)\n\n        if self._copy_modifiers_enabled():\n            return False\n        return True\n\n    def _copy_modifiers_enabled(self):\n        if (\n            QtWidgets.QApplication.keyboardModifiers()\n            & QtCore.Qt.ControlModifier\n        ):\n            return True\n        return False\n\n    def _add_filepaths(self, filepaths):\n        self._files_model.add_filepaths(filepaths)\n\n    def _remove_item_by_ids(self, item_ids):\n        self._files_model.remove_item_by_ids(item_ids)\n\n    def _update_visibility(self):\n        files_exists = self._files_proxy_model.rowCount() > 0\n        if files_exists:\n            current_widget = self._files_view\n        else:\n            current_widget = self._empty_widget\n        self._layout.setCurrentWidget(current_widget)\n        self._files_view.update_remove_btn_visibility()\n"
  },
  {
    "path": "openpype/tools/attribute_defs/widgets.py",
    "content": "import copy\n\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.lib.attribute_definitions import (\n    AbstractAttrDef,\n    UnknownDef,\n    HiddenDef,\n    NumberDef,\n    TextDef,\n    EnumDef,\n    BoolDef,\n    FileDef,\n    UIDef,\n    UISeparatorDef,\n    UILabelDef\n)\nfrom openpype.tools.utils import (\n    CustomTextComboBox,\n    FocusSpinBox,\n    FocusDoubleSpinBox,\n    MultiSelectionComboBox,\n)\nfrom openpype.widgets.nice_checkbox import NiceCheckbox\n\nfrom .files_widget import FilesWidget\n\n\ndef create_widget_for_attr_def(attr_def, parent=None):\n    widget = _create_widget_for_attr_def(attr_def, parent)\n    if attr_def.hidden:\n        widget.setVisible(False)\n\n    if attr_def.disabled:\n        widget.setEnabled(False)\n    return widget\n\n\ndef _create_widget_for_attr_def(attr_def, parent=None):\n    if not isinstance(attr_def, AbstractAttrDef):\n        raise TypeError(\"Unexpected type \\\"{}\\\" expected \\\"{}\\\"\".format(\n            str(type(attr_def)), AbstractAttrDef\n        ))\n\n    if isinstance(attr_def, NumberDef):\n        return NumberAttrWidget(attr_def, parent)\n\n    if isinstance(attr_def, TextDef):\n        return TextAttrWidget(attr_def, parent)\n\n    if isinstance(attr_def, EnumDef):\n        return EnumAttrWidget(attr_def, parent)\n\n    if isinstance(attr_def, BoolDef):\n        return BoolAttrWidget(attr_def, parent)\n\n    if isinstance(attr_def, UnknownDef):\n        return UnknownAttrWidget(attr_def, parent)\n\n    if isinstance(attr_def, HiddenDef):\n        return HiddenAttrWidget(attr_def, parent)\n\n    if isinstance(attr_def, FileDef):\n        return FileAttrWidget(attr_def, parent)\n\n    if isinstance(attr_def, UISeparatorDef):\n        return SeparatorAttrWidget(attr_def, parent)\n\n    if isinstance(attr_def, UILabelDef):\n        return LabelAttrWidget(attr_def, parent)\n\n    raise ValueError(\"Unknown attribute definition \\\"{}\\\"\".format(\n        str(type(attr_def))\n    ))\n\n\nclass AttributeDefinitionsWidget(QtWidgets.QWidget):\n    \"\"\"Create widgets for attribute definitions in grid layout.\n\n    Widget creates input widgets for passed attribute definitions.\n\n    Widget can't handle multiselection values.\n    \"\"\"\n\n    def __init__(self, attr_defs=None, parent=None):\n        super(AttributeDefinitionsWidget, self).__init__(parent)\n\n        self._widgets = []\n        self._current_keys = set()\n\n        self.set_attr_defs(attr_defs)\n\n    def clear_attr_defs(self):\n        \"\"\"Remove all existing widgets and reset layout if needed.\"\"\"\n        self._widgets = []\n        self._current_keys = set()\n\n        layout = self.layout()\n        if layout is not None:\n            if layout.count() == 0:\n                return\n\n            while layout.count():\n                item = layout.takeAt(0)\n                widget = item.widget()\n                if widget:\n                    widget.setVisible(False)\n                    widget.deleteLater()\n\n            layout.deleteLater()\n\n        new_layout = QtWidgets.QGridLayout()\n        new_layout.setColumnStretch(0, 0)\n        new_layout.setColumnStretch(1, 1)\n        self.setLayout(new_layout)\n\n    def set_attr_defs(self, attr_defs):\n        \"\"\"Replace current attribute definitions with passed.\"\"\"\n        self.clear_attr_defs()\n        if attr_defs:\n            self.add_attr_defs(attr_defs)\n\n    def add_attr_defs(self, attr_defs):\n        \"\"\"Add attribute definitions to current.\"\"\"\n        layout = self.layout()\n\n        row = 0\n        for attr_def in attr_defs:\n            if attr_def.is_value_def:\n                if attr_def.key in self._current_keys:\n                    raise KeyError(\n                        \"Duplicated key \\\"{}\\\"\".format(attr_def.key))\n\n                self._current_keys.add(attr_def.key)\n            widget = create_widget_for_attr_def(attr_def, self)\n            self._widgets.append(widget)\n\n            if attr_def.hidden:\n                continue\n\n            expand_cols = 2\n            if attr_def.is_value_def and attr_def.is_label_horizontal:\n                expand_cols = 1\n\n            col_num = 2 - expand_cols\n\n            if attr_def.is_value_def and attr_def.label:\n                label_widget = QtWidgets.QLabel(attr_def.label, self)\n                tooltip = attr_def.tooltip\n                if tooltip:\n                    label_widget.setToolTip(tooltip)\n                if attr_def.is_label_horizontal:\n                    label_widget.setAlignment(\n                        QtCore.Qt.AlignRight\n                        | QtCore.Qt.AlignVCenter\n                    )\n                layout.addWidget(\n                    label_widget, row, 0, 1, expand_cols\n                )\n                if not attr_def.is_label_horizontal:\n                    row += 1\n\n            layout.addWidget(\n                widget, row, col_num, 1, expand_cols\n            )\n            row += 1\n\n    def set_value(self, value):\n        new_value = copy.deepcopy(value)\n        unused_keys = set(new_value.keys())\n        for widget in self._widgets:\n            attr_def = widget.attr_def\n            if attr_def.key not in new_value:\n                continue\n            unused_keys.remove(attr_def.key)\n\n            widget_value = new_value[attr_def.key]\n            if widget_value is None:\n                widget_value = copy.deepcopy(attr_def.default)\n            widget.set_value(widget_value)\n\n    def current_value(self):\n        output = {}\n        for widget in self._widgets:\n            attr_def = widget.attr_def\n            if not isinstance(attr_def, UIDef):\n                output[attr_def.key] = widget.current_value()\n\n        return output\n\n\nclass _BaseAttrDefWidget(QtWidgets.QWidget):\n    # Type 'object' may not work with older PySide versions\n    value_changed = QtCore.Signal(object, str)\n\n    def __init__(self, attr_def, parent):\n        super(_BaseAttrDefWidget, self).__init__(parent)\n\n        self.attr_def = attr_def\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n\n        self.main_layout = main_layout\n\n        self._ui_init()\n\n    def _ui_init(self):\n        raise NotImplementedError(\n            \"Method '_ui_init' is not implemented. {}\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def current_value(self):\n        raise NotImplementedError(\n            \"Method 'current_value' is not implemented. {}\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def set_value(self, value, multivalue=False):\n        raise NotImplementedError(\n            \"Method 'set_value' is not implemented. {}\".format(\n                self.__class__.__name__\n            )\n        )\n\n\nclass SeparatorAttrWidget(_BaseAttrDefWidget):\n    def _ui_init(self):\n        input_widget = QtWidgets.QWidget(self)\n        input_widget.setObjectName(\"Separator\")\n        input_widget.setMinimumHeight(2)\n        input_widget.setMaximumHeight(2)\n\n        self._input_widget = input_widget\n\n        self.main_layout.addWidget(input_widget, 0)\n\n\nclass LabelAttrWidget(_BaseAttrDefWidget):\n    def _ui_init(self):\n        input_widget = QtWidgets.QLabel(self)\n        label = self.attr_def.label\n        if label:\n            input_widget.setText(str(label))\n\n        self._input_widget = input_widget\n\n        self.main_layout.addWidget(input_widget, 0)\n\n\nclass ClickableLineEdit(QtWidgets.QLineEdit):\n    clicked = QtCore.Signal()\n\n    def __init__(self, text, parent):\n        super(ClickableLineEdit, self).__init__(parent)\n        self.setText(text)\n        self.setReadOnly(True)\n\n        self._mouse_pressed = False\n\n    def mousePressEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self._mouse_pressed = True\n        super(ClickableLineEdit, self).mousePressEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        if self._mouse_pressed:\n            self._mouse_pressed = False\n            if self.rect().contains(event.pos()):\n                self.clicked.emit()\n\n        super(ClickableLineEdit, self).mouseReleaseEvent(event)\n\n\nclass NumberAttrWidget(_BaseAttrDefWidget):\n    def _ui_init(self):\n        decimals = self.attr_def.decimals\n        if decimals > 0:\n            input_widget = FocusDoubleSpinBox(self)\n            input_widget.setDecimals(decimals)\n        else:\n            input_widget = FocusSpinBox(self)\n\n        if self.attr_def.tooltip:\n            input_widget.setToolTip(self.attr_def.tooltip)\n\n        input_widget.setMinimum(self.attr_def.minimum)\n        input_widget.setMaximum(self.attr_def.maximum)\n        input_widget.setValue(self.attr_def.default)\n\n        input_widget.setButtonSymbols(\n            QtWidgets.QAbstractSpinBox.ButtonSymbols.NoButtons\n        )\n        input_line_edit = input_widget.lineEdit()\n        input_widget.installEventFilter(self)\n\n        multisel_widget = ClickableLineEdit(\"< Multiselection >\", self)\n        multisel_widget.setVisible(False)\n\n        input_widget.valueChanged.connect(self._on_value_change)\n        multisel_widget.clicked.connect(self._on_multi_click)\n\n        self._input_widget = input_widget\n        self._input_line_edit = input_line_edit\n        self._multisel_widget = multisel_widget\n        self._last_multivalue = None\n        self._multivalue = False\n\n        self.main_layout.addWidget(input_widget, 0)\n        self.main_layout.addWidget(multisel_widget, 0)\n\n    def eventFilter(self, obj, event):\n        if (\n            self._multivalue\n            and obj is self._input_widget\n            and event.type() == QtCore.QEvent.FocusOut\n        ):\n            self._set_multiselection_visible(True)\n        return False\n\n    def current_value(self):\n        return self._input_widget.value()\n\n    def set_value(self, value, multivalue=False):\n        self._last_multivalue = None\n        if multivalue:\n            set_value = set(value)\n            if None in set_value:\n                set_value.remove(None)\n                set_value.add(self.attr_def.default)\n\n            if len(set_value) > 1:\n                self._last_multivalue = next(iter(set_value), None)\n                self._set_multiselection_visible(True)\n                self._multivalue = True\n                return\n            value = tuple(set_value)[0]\n\n        self._multivalue = False\n        self._set_multiselection_visible(False)\n\n        if self.current_value != value:\n            self._input_widget.setValue(value)\n\n    def _on_value_change(self, new_value):\n        self._multivalue = False\n        self.value_changed.emit(new_value, self.attr_def.id)\n\n    def _on_multi_click(self):\n        self._set_multiselection_visible(False, True)\n\n    def _set_multiselection_visible(self, visible, change_focus=False):\n        self._input_widget.setVisible(not visible)\n        self._multisel_widget.setVisible(visible)\n        if visible:\n            return\n\n        # Change value once user clicked on the input field\n        if self._last_multivalue is None:\n            value = self.attr_def.default\n        else:\n            value = self._last_multivalue\n        self._input_widget.blockSignals(True)\n        self._input_widget.setValue(value)\n        self._input_widget.blockSignals(False)\n        if not change_focus:\n            return\n        # Change focus to input field and move cursor to the end\n        self._input_widget.setFocus(QtCore.Qt.MouseFocusReason)\n        self._input_line_edit.setCursorPosition(\n            len(self._input_line_edit.text())\n        )\n\n\nclass TextAttrWidget(_BaseAttrDefWidget):\n    def _ui_init(self):\n        # TODO Solve how to handle regex\n        # self.attr_def.regex\n\n        self.multiline = self.attr_def.multiline\n        if self.multiline:\n            input_widget = QtWidgets.QPlainTextEdit(self)\n        else:\n            input_widget = QtWidgets.QLineEdit(self)\n\n        if (\n            self.attr_def.placeholder\n            and hasattr(input_widget, \"setPlaceholderText\")\n        ):\n            input_widget.setPlaceholderText(self.attr_def.placeholder)\n\n        if self.attr_def.tooltip:\n            input_widget.setToolTip(self.attr_def.tooltip)\n\n        if self.attr_def.default:\n            if self.multiline:\n                input_widget.setPlainText(self.attr_def.default)\n            else:\n                input_widget.setText(self.attr_def.default)\n\n        input_widget.textChanged.connect(self._on_value_change)\n\n        self._input_widget = input_widget\n\n        self.main_layout.addWidget(input_widget, 0)\n\n    def _on_value_change(self):\n        if self.multiline:\n            new_value = self._input_widget.toPlainText()\n        else:\n            new_value = self._input_widget.text()\n        self.value_changed.emit(new_value, self.attr_def.id)\n\n    def current_value(self):\n        if self.multiline:\n            return self._input_widget.toPlainText()\n        return self._input_widget.text()\n\n    def set_value(self, value, multivalue=False):\n        block_signals = False\n        if multivalue:\n            set_value = set(value)\n            if None in set_value:\n                set_value.remove(None)\n                set_value.add(self.attr_def.default)\n\n            if len(set_value) == 1:\n                value = tuple(set_value)[0]\n            else:\n                block_signals = True\n                value = \"< Multiselection >\"\n\n        if value != self.current_value():\n            if block_signals:\n                self._input_widget.blockSignals(True)\n            if self.multiline:\n                self._input_widget.setPlainText(value)\n            else:\n                self._input_widget.setText(value)\n            if block_signals:\n                self._input_widget.blockSignals(False)\n\n\nclass BoolAttrWidget(_BaseAttrDefWidget):\n    def _ui_init(self):\n        input_widget = NiceCheckbox(parent=self)\n        input_widget.setChecked(self.attr_def.default)\n\n        if self.attr_def.tooltip:\n            input_widget.setToolTip(self.attr_def.tooltip)\n\n        input_widget.stateChanged.connect(self._on_value_change)\n\n        self._input_widget = input_widget\n\n        self.main_layout.addWidget(input_widget, 0)\n        self.main_layout.addStretch(1)\n\n    def _on_value_change(self):\n        new_value = self._input_widget.isChecked()\n        self.value_changed.emit(new_value, self.attr_def.id)\n\n    def current_value(self):\n        return self._input_widget.isChecked()\n\n    def set_value(self, value, multivalue=False):\n        if multivalue:\n            set_value = set(value)\n            if None in set_value:\n                set_value.remove(None)\n                set_value.add(self.attr_def.default)\n\n            if len(set_value) > 1:\n                self._input_widget.blockSignals(True)\n                self._input_widget.setCheckState(QtCore.Qt.PartiallyChecked)\n                self._input_widget.blockSignals(False)\n                return\n            value = tuple(set_value)[0]\n\n        if value != self.current_value():\n            self._input_widget.setChecked(value)\n\n\nclass EnumAttrWidget(_BaseAttrDefWidget):\n    def __init__(self, *args, **kwargs):\n        self._multivalue = False\n        super(EnumAttrWidget, self).__init__(*args, **kwargs)\n\n    @property\n    def multiselection(self):\n        return self.attr_def.multiselection\n\n    def _ui_init(self):\n        if self.multiselection:\n            input_widget = MultiSelectionComboBox(self)\n\n        else:\n            input_widget = CustomTextComboBox(self)\n            combo_delegate = QtWidgets.QStyledItemDelegate(input_widget)\n            input_widget.setItemDelegate(combo_delegate)\n            self._combo_delegate = combo_delegate\n\n        if self.attr_def.tooltip:\n            input_widget.setToolTip(self.attr_def.tooltip)\n\n        for item in self.attr_def.items:\n            input_widget.addItem(item[\"label\"], item[\"value\"])\n\n        idx = input_widget.findData(self.attr_def.default)\n        if idx >= 0:\n            input_widget.setCurrentIndex(idx)\n\n        if self.multiselection:\n            input_widget.value_changed.connect(self._on_value_change)\n        else:\n            input_widget.currentIndexChanged.connect(self._on_value_change)\n\n        self._input_widget = input_widget\n\n        self.main_layout.addWidget(input_widget, 0)\n\n    def _on_value_change(self):\n        new_value = self.current_value()\n        if self._multivalue:\n            self._multivalue = False\n            self._input_widget.set_custom_text(None)\n        self.value_changed.emit(new_value, self.attr_def.id)\n\n    def current_value(self):\n        if self.multiselection:\n            return self._input_widget.value()\n        idx = self._input_widget.currentIndex()\n        return self._input_widget.itemData(idx)\n\n    def _multiselection_multivalue_prep(self, values):\n        final = None\n        multivalue = False\n        for value in values:\n            value = set(value)\n            if final is None:\n                final = value\n            elif multivalue or final != value:\n                final |= value\n                multivalue = True\n        return list(final), multivalue\n\n    def set_value(self, value, multivalue=False):\n        if multivalue:\n            if self.multiselection:\n                value, multivalue = self._multiselection_multivalue_prep(\n                    value)\n            else:\n                set_value = set(value)\n                if len(set_value) == 1:\n                    multivalue = False\n                    value = tuple(set_value)[0]\n\n        if self.multiselection:\n            self._input_widget.blockSignals(True)\n            self._input_widget.set_value(value)\n            self._input_widget.blockSignals(False)\n\n        elif not multivalue:\n            idx = self._input_widget.findData(value)\n            cur_idx = self._input_widget.currentIndex()\n            if idx != cur_idx and idx >= 0:\n                self._input_widget.setCurrentIndex(idx)\n\n        custom_text = None\n        if multivalue:\n            custom_text = \"< Multiselection >\"\n        self._input_widget.set_custom_text(custom_text)\n        self._multivalue = multivalue\n\n\nclass UnknownAttrWidget(_BaseAttrDefWidget):\n    def _ui_init(self):\n        input_widget = QtWidgets.QLabel(self)\n        self._value = self.attr_def.default\n        input_widget.setText(str(self._value))\n\n        self._input_widget = input_widget\n\n        self.main_layout.addWidget(input_widget, 0)\n\n    def current_value(self):\n        raise ValueError(\n            \"{} can't hold real value.\".format(self.__class__.__name__)\n        )\n\n    def set_value(self, value, multivalue=False):\n        if multivalue:\n            set_value = set(value)\n            if len(set_value) == 1:\n                value = tuple(set_value)[0]\n            else:\n                value = \"< Multiselection >\"\n\n        str_value = str(value)\n        if str_value != self._value:\n            self._value = str_value\n            self._input_widget.setText(str_value)\n\n\nclass HiddenAttrWidget(_BaseAttrDefWidget):\n    def _ui_init(self):\n        self.setVisible(False)\n        self._value = self.attr_def.default\n        self._multivalue = False\n\n    def setVisible(self, visible):\n        if visible:\n            visible = False\n        super(HiddenAttrWidget, self).setVisible(visible)\n\n    def current_value(self):\n        if self._multivalue:\n            raise ValueError(\"{} can't output for multivalue.\".format(\n                self.__class__.__name__\n            ))\n        return self._value\n\n    def set_value(self, value, multivalue=False):\n        self._value = copy.deepcopy(value)\n        self._multivalue = multivalue\n\n\nclass FileAttrWidget(_BaseAttrDefWidget):\n    def _ui_init(self):\n        input_widget = FilesWidget(\n            self.attr_def.single_item,\n            self.attr_def.allow_sequences,\n            self.attr_def.extensions_label,\n            self\n        )\n\n        if self.attr_def.tooltip:\n            input_widget.setToolTip(self.attr_def.tooltip)\n\n        input_widget.set_filters(\n            self.attr_def.folders, self.attr_def.extensions\n        )\n\n        input_widget.value_changed.connect(self._on_value_change)\n\n        self._input_widget = input_widget\n\n        self.main_layout.addWidget(input_widget, 0)\n\n    def _on_value_change(self):\n        new_value = self.current_value()\n        self.value_changed.emit(new_value, self.attr_def.id)\n\n    def current_value(self):\n        return self._input_widget.current_value()\n\n    def set_value(self, value, multivalue=False):\n        self._input_widget.set_value(value, multivalue)\n"
  },
  {
    "path": "openpype/tools/ayon_launcher/abstract.py",
    "content": "from abc import ABCMeta, abstractmethod\n\nimport six\n\n\n@six.add_metaclass(ABCMeta)\nclass AbstractLauncherCommon(object):\n    @abstractmethod\n    def register_event_callback(self, topic, callback):\n        \"\"\"Register event callback.\n\n        Listen for events with given topic.\n\n        Args:\n            topic (str): Name of topic.\n            callback (Callable): Callback that will be called when event\n                is triggered.\n        \"\"\"\n\n        pass\n\n\nclass AbstractLauncherBackend(AbstractLauncherCommon):\n    @abstractmethod\n    def emit_event(self, topic, data=None, source=None):\n        \"\"\"Emit event.\n\n        Args:\n            topic (str): Event topic used for callbacks filtering.\n            data (Optional[dict[str, Any]]): Event data.\n            source (Optional[str]): Event source.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_project_settings(self, project_name):\n        \"\"\"Project settings for current project.\n\n        Args:\n            project_name (Union[str, None]): Project name.\n\n        Returns:\n            dict[str, Any]: Project settings.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_project_entity(self, project_name):\n        \"\"\"Get project entity by name.\n\n        Args:\n            project_name (str): Project name.\n\n        Returns:\n            dict[str, Any]: Project entity data.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_folder_entity(self, project_name, folder_id):\n        \"\"\"Get folder entity by id.\n\n        Args:\n            project_name (str): Project name.\n            folder_id (str): Folder id.\n\n        Returns:\n            dict[str, Any]: Folder entity data.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_task_entity(self, project_name, task_id):\n        \"\"\"Get task entity by id.\n\n        Args:\n            project_name (str): Project name.\n            task_id (str): Task id.\n\n        Returns:\n            dict[str, Any]: Task entity data.\n        \"\"\"\n\n        pass\n\n\nclass AbstractLauncherFrontEnd(AbstractLauncherCommon):\n    # Entity items for UI\n    @abstractmethod\n    def get_project_items(self, sender=None):\n        \"\"\"Project items for all projects.\n\n        This function may trigger events 'projects.refresh.started' and\n        'projects.refresh.finished' which will contain 'sender' value in data.\n        That may help to avoid re-refresh of project items in UI elements.\n\n        Args:\n            sender (str): Who requested folder items.\n\n        Returns:\n            list[ProjectItem]: Minimum possible information needed\n                for visualisation of folder hierarchy.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_folder_items(self, project_name, sender=None):\n        \"\"\"Folder items to visualize project hierarchy.\n\n        This function may trigger events 'folders.refresh.started' and\n        'folders.refresh.finished' which will contain 'sender' value in data.\n        That may help to avoid re-refresh of folder items in UI elements.\n\n        Args:\n            project_name (str): Project name.\n            sender (str): Who requested folder items.\n\n        Returns:\n            list[FolderItem]: Minimum possible information needed\n                for visualisation of folder hierarchy.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_task_items(self, project_name, folder_id, sender=None):\n        \"\"\"Task items.\n\n        This function may trigger events 'tasks.refresh.started' and\n        'tasks.refresh.finished' which will contain 'sender' value in data.\n        That may help to avoid re-refresh of task items in UI elements.\n\n        Args:\n            project_name (str): Project name.\n            folder_id (str): Folder ID for which are tasks requested.\n            sender (str): Who requested folder items.\n\n        Returns:\n            list[TaskItem]: Minimum possible information needed\n                for visualisation of tasks.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_selected_project_name(self):\n        \"\"\"Selected project name.\n\n        Returns:\n            Union[str, None]: Selected project name.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_selected_folder_id(self):\n        \"\"\"Selected folder id.\n\n        Returns:\n            Union[str, None]: Selected folder id.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_selected_task_id(self):\n        \"\"\"Selected task id.\n\n        Returns:\n            Union[str, None]: Selected task id.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_selected_task_name(self):\n        \"\"\"Selected task name.\n\n        Returns:\n            Union[str, None]: Selected task name.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_selected_context(self):\n        \"\"\"Get whole selected context.\n\n        Example:\n            {\n                \"project_name\": self.get_selected_project_name(),\n                \"folder_id\": self.get_selected_folder_id(),\n                \"task_id\": self.get_selected_task_id(),\n                \"task_name\": self.get_selected_task_name(),\n            }\n\n        Returns:\n            dict[str, Union[str, None]]: Selected context.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_selected_project(self, project_name):\n        \"\"\"Change selected folder.\n\n        Args:\n            project_name (Union[str, None]): Project nameor None if no project\n                is selected.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_selected_folder(self, folder_id):\n        \"\"\"Change selected folder.\n\n        Args:\n            folder_id (Union[str, None]): Folder id or None if no folder\n                is selected.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_selected_task(self, task_id, task_name):\n        \"\"\"Change selected task.\n\n        Args:\n            task_id (Union[str, None]): Task id or None if no task\n                is selected.\n            task_name (Union[str, None]): Task name or None if no task\n                is selected.\n        \"\"\"\n\n        pass\n\n    # Actions\n    @abstractmethod\n    def get_action_items(self, project_name, folder_id, task_id):\n        \"\"\"Get action items for given context.\n\n        Args:\n            project_name (Union[str, None]): Project name.\n            folder_id (Union[str, None]): Folder id.\n            task_id (Union[str, None]): Task id.\n\n        Returns:\n            list[ActionItem]: List of action items that should be shown\n                for given context.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def trigger_action(self, project_name, folder_id, task_id, action_id):\n        \"\"\"Trigger action on given context.\n\n        Args:\n            project_name (Union[str, None]): Project name.\n            folder_id (Union[str, None]): Folder id.\n            task_id (Union[str, None]): Task id.\n            action_id (str): Action identifier.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_application_force_not_open_workfile(\n        self, project_name, folder_id, task_id, action_ids, enabled\n    ):\n        \"\"\"This is application action related to force not open last workfile.\n\n        Args:\n            project_name (Union[str, None]): Project name.\n            folder_id (Union[str, None]): Folder id.\n            task_id (Union[str, None]): Task id.\n            action_id (Iterable[str]): Action identifiers.\n            enabled (bool): New value of force not open workfile.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def refresh(self):\n        \"\"\"Refresh everything, models, ui etc.\n\n        Triggers 'controller.refresh.started' event at the beginning and\n        'controller.refresh.finished' at the end.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def refresh_actions(self):\n        \"\"\"Refresh actions and all related data.\n\n        Triggers 'controller.refresh.actions.started' event at the beginning\n        and 'controller.refresh.actions.finished' at the end.\n        \"\"\"\n\n        pass\n"
  },
  {
    "path": "openpype/tools/ayon_launcher/control.py",
    "content": "from openpype.lib import Logger\nfrom openpype.lib.events import QueuedEventSystem\nfrom openpype.settings import get_project_settings\nfrom openpype.tools.ayon_utils.models import ProjectsModel, HierarchyModel\n\nfrom .abstract import AbstractLauncherFrontEnd, AbstractLauncherBackend\nfrom .models import LauncherSelectionModel, ActionsModel\n\n\nclass BaseLauncherController(\n    AbstractLauncherFrontEnd, AbstractLauncherBackend\n):\n    def __init__(self):\n        self._project_settings = {}\n        self._event_system = None\n        self._log = None\n\n        self._selection_model = LauncherSelectionModel(self)\n        self._projects_model = ProjectsModel(self)\n        self._hierarchy_model = HierarchyModel(self)\n        self._actions_model = ActionsModel(self)\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    @property\n    def event_system(self):\n        \"\"\"Inner event system for workfiles tool controller.\n\n        Is used for communication with UI. Event system is created on demand.\n\n        Returns:\n            QueuedEventSystem: Event system which can trigger callbacks\n                for topics.\n        \"\"\"\n\n        if self._event_system is None:\n            self._event_system = QueuedEventSystem()\n        return self._event_system\n\n    # ---------------------------------\n    # Implementation of abstract methods\n    # ---------------------------------\n    # Events system\n    def emit_event(self, topic, data=None, source=None):\n        \"\"\"Use implemented event system to trigger event.\"\"\"\n\n        if data is None:\n            data = {}\n        self.event_system.emit(topic, data, source)\n\n    def register_event_callback(self, topic, callback):\n        self.event_system.add_callback(topic, callback)\n\n    # Entity items for UI\n    def get_project_items(self, sender=None):\n        return self._projects_model.get_project_items(sender)\n\n    def get_folder_items(self, project_name, sender=None):\n        return self._hierarchy_model.get_folder_items(project_name, sender)\n\n    def get_task_items(self, project_name, folder_id, sender=None):\n        return self._hierarchy_model.get_task_items(\n            project_name, folder_id, sender)\n\n    # Project settings for applications actions\n    def get_project_settings(self, project_name):\n        if project_name in self._project_settings:\n            return self._project_settings[project_name]\n        settings = get_project_settings(project_name)\n        self._project_settings[project_name] = settings\n        return settings\n\n    # Entity for backend\n    def get_project_entity(self, project_name):\n        return self._projects_model.get_project_entity(project_name)\n\n    def get_folder_entity(self, project_name, folder_id):\n        return self._hierarchy_model.get_folder_entity(\n            project_name, folder_id)\n\n    def get_task_entity(self, project_name, task_id):\n        return self._hierarchy_model.get_task_entity(project_name, task_id)\n\n    # Selection methods\n    def get_selected_project_name(self):\n        return self._selection_model.get_selected_project_name()\n\n    def set_selected_project(self, project_name):\n        self._selection_model.set_selected_project(project_name)\n\n    def get_selected_folder_id(self):\n        return self._selection_model.get_selected_folder_id()\n\n    def set_selected_folder(self, folder_id):\n        self._selection_model.set_selected_folder(folder_id)\n\n    def get_selected_task_id(self):\n        return self._selection_model.get_selected_task_id()\n\n    def get_selected_task_name(self):\n        return self._selection_model.get_selected_task_name()\n\n    def set_selected_task(self, task_id, task_name):\n        self._selection_model.set_selected_task(task_id, task_name)\n\n    def get_selected_context(self):\n        return {\n            \"project_name\": self.get_selected_project_name(),\n            \"folder_id\": self.get_selected_folder_id(),\n            \"task_id\": self.get_selected_task_id(),\n            \"task_name\": self.get_selected_task_name(),\n        }\n\n    # Actions\n    def get_action_items(self, project_name, folder_id, task_id):\n        return self._actions_model.get_action_items(\n            project_name, folder_id, task_id)\n\n    def set_application_force_not_open_workfile(\n        self, project_name, folder_id, task_id, action_ids, enabled\n    ):\n        self._actions_model.set_application_force_not_open_workfile(\n            project_name, folder_id, task_id, action_ids, enabled\n        )\n\n    def trigger_action(self, project_name, folder_id, task_id, identifier):\n        self._actions_model.trigger_action(\n            project_name, folder_id, task_id, identifier)\n\n    # General methods\n    def refresh(self):\n        self._emit_event(\"controller.refresh.started\")\n\n        self._project_settings = {}\n\n        self._projects_model.reset()\n        self._hierarchy_model.reset()\n\n        self._actions_model.refresh()\n        self._projects_model.refresh()\n\n        self._emit_event(\"controller.refresh.finished\")\n\n    def refresh_actions(self):\n        self._emit_event(\"controller.refresh.actions.started\")\n\n        # Refresh project settings (used for actions discovery)\n        self._project_settings = {}\n        # Refresh projects - they define applications\n        self._projects_model.reset()\n        # Refresh actions\n        self._actions_model.refresh()\n\n        self._emit_event(\"controller.refresh.actions.finished\")\n\n    def _emit_event(self, topic, data=None):\n        self.emit_event(topic, data, \"controller\")\n"
  },
  {
    "path": "openpype/tools/ayon_launcher/models/__init__.py",
    "content": "from .actions import ActionsModel\nfrom .selection import LauncherSelectionModel\n\n\n__all__ = (\n    \"ActionsModel\",\n    \"LauncherSelectionModel\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_launcher/models/actions.py",
    "content": "import os\n\nfrom openpype import resources\nfrom openpype.lib import Logger, OpenPypeSettingsRegistry\nfrom openpype.pipeline.actions import (\n    discover_launcher_actions,\n    LauncherAction,\n)\n\n\n# class Action:\n#     def __init__(self, label, icon=None, identifier=None):\n#         self._label = label\n#         self._icon = icon\n#         self._callbacks = []\n#         self._identifier = identifier or uuid.uuid4().hex\n#         self._checked = True\n#         self._checkable = False\n#\n#     def set_checked(self, checked):\n#         self._checked = checked\n#\n#     def set_checkable(self, checkable):\n#         self._checkable = checkable\n#\n#     def set_label(self, label):\n#         self._label = label\n#\n#     def add_callback(self, callback):\n#         self._callbacks = callback\n#\n#\n# class Menu:\n#     def __init__(self, label, icon=None):\n#         self.label = label\n#         self.icon = icon\n#         self._actions = []\n#\n#     def add_action(self, action):\n#         self._actions.append(action)\n\n\nclass ApplicationAction(LauncherAction):\n    \"\"\"Action to launch an application.\n\n    Application action based on 'ApplicationManager' system.\n\n    Handling of applications in launcher is not ideal and should be completely\n    redone from scratch. This is just a temporary solution to keep backwards\n    compatibility with OpenPype launcher.\n\n    Todos:\n        Move handling of errors to frontend.\n    \"\"\"\n\n    # Application object\n    application = None\n    # Action attributes\n    name = None\n    label = None\n    label_variant = None\n    group = None\n    icon = None\n    color = None\n    order = 0\n    data = {}\n    project_settings = {}\n    project_entities = {}\n\n    _log = None\n    required_session_keys = (\n        \"AVALON_PROJECT\",\n        \"AVALON_ASSET\",\n        \"AVALON_TASK\"\n    )\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    def is_compatible(self, session):\n        for key in self.required_session_keys:\n            if not session.get(key):\n                return False\n\n        project_name = session[\"AVALON_PROJECT\"]\n        project_entity = self.project_entities[project_name]\n        apps = project_entity[\"attrib\"].get(\"applications\")\n        if not apps or self.application.full_name not in apps:\n            return False\n\n        project_settings = self.project_settings[project_name]\n        only_available = project_settings[\"applications\"][\"only_available\"]\n        if only_available and not self.application.find_executable():\n            return False\n        return True\n\n    def _show_message_box(self, title, message, details=None):\n        from qtpy import QtWidgets, QtGui\n        from openpype import style\n\n        dialog = QtWidgets.QMessageBox()\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        dialog.setWindowIcon(icon)\n        dialog.setStyleSheet(style.load_stylesheet())\n        dialog.setWindowTitle(title)\n        dialog.setText(message)\n        if details:\n            dialog.setDetailedText(details)\n        dialog.exec_()\n\n    def process(self, session, **kwargs):\n        \"\"\"Process the full Application action\"\"\"\n\n        from openpype.lib import (\n            ApplictionExecutableNotFound,\n            ApplicationLaunchFailed,\n        )\n\n        project_name = session[\"AVALON_PROJECT\"]\n        asset_name = session[\"AVALON_ASSET\"]\n        task_name = session[\"AVALON_TASK\"]\n        try:\n            self.application.launch(\n                project_name=project_name,\n                asset_name=asset_name,\n                task_name=task_name,\n                **self.data\n            )\n\n        except ApplictionExecutableNotFound as exc:\n            details = exc.details\n            msg = exc.msg\n            log_msg = str(msg)\n            if details:\n                log_msg += \"\\n\" + details\n            self.log.warning(log_msg)\n            self._show_message_box(\n                \"Application executable not found\", msg, details\n            )\n\n        except ApplicationLaunchFailed as exc:\n            msg = str(exc)\n            self.log.warning(msg, exc_info=True)\n            self._show_message_box(\"Application launch failed\", msg)\n\n\nclass ActionItem:\n    \"\"\"Item representing single action to trigger.\n\n    Todos:\n        Get rid of application specific logic.\n\n    Args:\n        identifier (str): Unique identifier of action item.\n        label (str): Action label.\n        variant_label (Union[str, None]): Variant label, full label is\n            concatenated with space. Actions are grouped under single\n            action if it has same 'label' and have set 'variant_label'.\n        icon (dict[str, str]): Icon definition.\n        order (int): Action ordering.\n        is_application (bool): Is action application action.\n        force_not_open_workfile (bool): Force not open workfile. Application\n            related.\n        full_label (Optional[str]): Full label, if not set it is generated\n            from 'label' and 'variant_label'.\n    \"\"\"\n\n    def __init__(\n        self,\n        identifier,\n        label,\n        variant_label,\n        icon,\n        order,\n        is_application,\n        force_not_open_workfile,\n        full_label=None\n    ):\n        self.identifier = identifier\n        self.label = label\n        self.variant_label = variant_label\n        self.icon = icon\n        self.order = order\n        self.is_application = is_application\n        self.force_not_open_workfile = force_not_open_workfile\n        self._full_label = full_label\n\n    def copy(self):\n        return self.from_data(self.to_data())\n\n    @property\n    def full_label(self):\n        if self._full_label is None:\n            if self.variant_label:\n                self._full_label = \" \".join([self.label, self.variant_label])\n            else:\n                self._full_label = self.label\n        return self._full_label\n\n    def to_data(self):\n        return {\n            \"identifier\": self.identifier,\n            \"label\": self.label,\n            \"variant_label\": self.variant_label,\n            \"icon\": self.icon,\n            \"order\": self.order,\n            \"is_application\": self.is_application,\n            \"force_not_open_workfile\": self.force_not_open_workfile,\n            \"full_label\": self._full_label,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        return cls(**data)\n\n\ndef get_action_icon(action):\n    \"\"\"Get action icon info.\n\n    Args:\n        action (LacunherAction): Action instance.\n\n    Returns:\n        dict[str, str]: Icon info.\n    \"\"\"\n\n    icon = action.icon\n    if not icon:\n        return {\n            \"type\": \"awesome-font\",\n            \"name\": \"fa.cube\",\n            \"color\": \"white\"\n        }\n\n    if isinstance(icon, dict):\n        return icon\n\n    icon_path = resources.get_resource(icon)\n    if not os.path.exists(icon_path):\n        try:\n            icon_path = icon.format(resources.RESOURCES_DIR)\n        except Exception:\n            pass\n\n    if os.path.exists(icon_path):\n        return {\n            \"type\": \"path\",\n            \"path\": icon_path,\n        }\n\n    return {\n        \"type\": \"awesome-font\",\n        \"name\": icon,\n        \"color\": action.color or \"white\"\n    }\n\n\nclass ActionsModel:\n    \"\"\"Actions model.\n\n    Args:\n        controller (AbstractLauncherBackend): Controller instance.\n    \"\"\"\n\n    _not_open_workfile_reg_key = \"force_not_open_workfile\"\n\n    def __init__(self, controller):\n        self._controller = controller\n\n        self._log = None\n\n        self._discovered_actions = None\n        self._actions = None\n        self._action_items = {}\n\n        self._launcher_tool_reg = OpenPypeSettingsRegistry(\"launcher_tool\")\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    def refresh(self):\n        self._discovered_actions = None\n        self._actions = None\n        self._action_items = {}\n\n        self._controller.emit_event(\"actions.refresh.started\")\n        self._get_action_objects()\n        self._controller.emit_event(\"actions.refresh.finished\")\n\n    def get_action_items(self, project_name, folder_id, task_id):\n        \"\"\"Get actions for project.\n\n        Args:\n            project_name (Union[str, None]): Project name.\n            folder_id (Union[str, None]): Folder id.\n            task_id (Union[str, None]): Task id.\n\n        Returns:\n            list[ActionItem]: List of actions.\n        \"\"\"\n\n        not_open_workfile_actions = self._get_no_last_workfile_for_context(\n            project_name, folder_id, task_id)\n        session = self._prepare_session(project_name, folder_id, task_id)\n        output = []\n        action_items = self._get_action_items(project_name)\n        for identifier, action in self._get_action_objects().items():\n            if not action.is_compatible(session):\n                continue\n\n            action_item = action_items[identifier]\n            # Handling of 'force_not_open_workfile' for applications\n            if action_item.is_application:\n                action_item = action_item.copy()\n                action_item.force_not_open_workfile = (\n                    not_open_workfile_actions.get(identifier, False)\n                )\n\n            output.append(action_item)\n        return output\n\n    def set_application_force_not_open_workfile(\n        self, project_name, folder_id, task_id, action_ids, enabled\n    ):\n        no_workfile_reg_data = self._get_no_last_workfile_reg_data()\n        project_data = no_workfile_reg_data.setdefault(project_name, {})\n        folder_data = project_data.setdefault(folder_id, {})\n        task_data = folder_data.setdefault(task_id, {})\n        for action_id in action_ids:\n            task_data[action_id] = enabled\n        self._launcher_tool_reg.set_item(\n            self._not_open_workfile_reg_key, no_workfile_reg_data\n        )\n\n    def trigger_action(self, project_name, folder_id, task_id, identifier):\n        session = self._prepare_session(project_name, folder_id, task_id)\n        failed = False\n        error_message = None\n        action_label = identifier\n        action_items = self._get_action_items(project_name)\n        try:\n            action = self._actions[identifier]\n            action_item = action_items[identifier]\n            action_label = action_item.full_label\n            self._controller.emit_event(\n                \"action.trigger.started\",\n                {\n                    \"identifier\": identifier,\n                    \"full_label\": action_label,\n                }\n            )\n            if isinstance(action, ApplicationAction):\n                per_action = self._get_no_last_workfile_for_context(\n                    project_name, folder_id, task_id\n                )\n                force_not_open_workfile = per_action.get(identifier, False)\n                if force_not_open_workfile:\n                    action.data[\"start_last_workfile\"] = False\n                else:\n                    action.data.pop(\"start_last_workfile\", None)\n            action.process(session)\n        except Exception as exc:\n            self.log.warning(\"Action trigger failed.\", exc_info=True)\n            failed = True\n            error_message = str(exc)\n\n        self._controller.emit_event(\n            \"action.trigger.finished\",\n            {\n                \"identifier\": identifier,\n                \"failed\": failed,\n                \"error_message\": error_message,\n                \"full_label\": action_label,\n            }\n        )\n\n    def _get_no_last_workfile_reg_data(self):\n        try:\n            no_workfile_reg_data = self._launcher_tool_reg.get_item(\n                self._not_open_workfile_reg_key)\n        except ValueError:\n            no_workfile_reg_data = {}\n            self._launcher_tool_reg.set_item(\n                self._not_open_workfile_reg_key, no_workfile_reg_data)\n        return no_workfile_reg_data\n\n    def _get_no_last_workfile_for_context(\n        self, project_name, folder_id, task_id\n    ):\n        not_open_workfile_reg_data = self._get_no_last_workfile_reg_data()\n        return (\n            not_open_workfile_reg_data\n            .get(project_name, {})\n            .get(folder_id, {})\n            .get(task_id, {})\n        )\n\n    def _prepare_session(self, project_name, folder_id, task_id):\n        folder_path = None\n        if folder_id:\n            folder = self._controller.get_folder_entity(\n                project_name, folder_id)\n            if folder:\n                folder_path = folder[\"path\"]\n\n        task_name = None\n        if task_id:\n            task = self._controller.get_task_entity(project_name, task_id)\n            if task:\n                task_name = task[\"name\"]\n\n        return {\n            \"AVALON_PROJECT\": project_name,\n            \"AVALON_ASSET\": folder_path,\n            \"AVALON_TASK\": task_name,\n        }\n\n    def _get_discovered_action_classes(self):\n        if self._discovered_actions is None:\n            self._discovered_actions = (\n                discover_launcher_actions()\n                + self._get_applications_action_classes()\n            )\n        return self._discovered_actions\n\n    def _get_action_objects(self):\n        if self._actions is None:\n            actions = {}\n            for cls in self._get_discovered_action_classes():\n                obj = cls()\n                identifier = getattr(obj, \"identifier\", None)\n                if identifier is None:\n                    identifier = cls.__name__\n                actions[identifier] = obj\n            self._actions = actions\n        return self._actions\n\n    def _get_action_items(self, project_name):\n        action_items = self._action_items.get(project_name)\n        if action_items is not None:\n            return action_items\n\n        project_entity = None\n        if project_name:\n            project_entity = self._controller.get_project_entity(project_name)\n        project_settings = self._controller.get_project_settings(project_name)\n\n        action_items = {}\n        for identifier, action in self._get_action_objects().items():\n            is_application = isinstance(action, ApplicationAction)\n            if is_application:\n                action.project_entities[project_name] = project_entity\n                action.project_settings[project_name] = project_settings\n            label = action.label or identifier\n            variant_label = getattr(action, \"label_variant\", None)\n            icon = get_action_icon(action)\n            item = ActionItem(\n                identifier,\n                label,\n                variant_label,\n                icon,\n                action.order,\n                is_application,\n                False\n            )\n            action_items[identifier] = item\n        self._action_items[project_name] = action_items\n        return action_items\n\n    def _get_applications_action_classes(self):\n        from openpype.lib.applications import (\n            CUSTOM_LAUNCH_APP_GROUPS,\n            ApplicationManager,\n        )\n\n        actions = []\n\n        manager = ApplicationManager()\n        for full_name, application in manager.applications.items():\n            if (\n                application.group.name in CUSTOM_LAUNCH_APP_GROUPS\n                or not application.enabled\n            ):\n                continue\n\n            action = type(\n                \"app_{}\".format(full_name),\n                (ApplicationAction,),\n                {\n                    \"identifier\": \"application.{}\".format(full_name),\n                    \"application\": application,\n                    \"name\": application.name,\n                    \"label\": application.group.label,\n                    \"label_variant\": application.label,\n                    \"group\": None,\n                    \"icon\": application.icon,\n                    \"color\": getattr(application, \"color\", None),\n                    \"order\": getattr(application, \"order\", None) or 0,\n                    \"data\": {}\n                }\n            )\n            actions.append(action)\n        return actions\n"
  },
  {
    "path": "openpype/tools/ayon_launcher/models/selection.py",
    "content": "class LauncherSelectionModel(object):\n    \"\"\"Model handling selection changes.\n\n    Triggering events:\n    - \"selection.project.changed\"\n    - \"selection.folder.changed\"\n    - \"selection.task.changed\"\n    \"\"\"\n\n    event_source = \"launcher.selection.model\"\n\n    def __init__(self, controller):\n        self._controller = controller\n\n        self._project_name = None\n        self._folder_id = None\n        self._task_name = None\n        self._task_id = None\n\n    def get_selected_project_name(self):\n        return self._project_name\n\n    def set_selected_project(self, project_name):\n        if project_name == self._project_name:\n            return\n\n        self._project_name = project_name\n        self._controller.emit_event(\n            \"selection.project.changed\",\n            {\"project_name\": project_name},\n            self.event_source\n        )\n\n    def get_selected_folder_id(self):\n        return self._folder_id\n\n    def set_selected_folder(self, folder_id):\n        if folder_id == self._folder_id:\n            return\n\n        self._folder_id = folder_id\n        self._controller.emit_event(\n            \"selection.folder.changed\",\n            {\n                \"project_name\": self._project_name,\n                \"folder_id\": folder_id,\n            },\n            self.event_source\n        )\n\n    def get_selected_task_name(self):\n        return self._task_name\n\n    def get_selected_task_id(self):\n        return self._task_id\n\n    def set_selected_task(self, task_id, task_name):\n        if task_id == self._task_id:\n            return\n\n        self._task_name = task_name\n        self._task_id = task_id\n        self._controller.emit_event(\n            \"selection.task.changed\",\n            {\n                \"project_name\": self._project_name,\n                \"folder_id\": self._folder_id,\n                \"task_name\": task_name,\n                \"task_id\": task_id,\n            },\n            self.event_source\n        )\n"
  },
  {
    "path": "openpype/tools/ayon_launcher/ui/__init__.py",
    "content": "from .window import LauncherWindow\n\n\n__all__ = (\n    \"LauncherWindow\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_launcher/ui/actions_widget.py",
    "content": "import time\nimport collections\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.tools.flickcharm import FlickCharm\nfrom openpype.tools.ayon_utils.widgets import get_qt_icon\n\nfrom .resources import get_options_image_path\n\nANIMATION_LEN = 7\n\nACTION_ID_ROLE = QtCore.Qt.UserRole + 1\nACTION_IS_APPLICATION_ROLE = QtCore.Qt.UserRole + 2\nACTION_IS_GROUP_ROLE = QtCore.Qt.UserRole + 3\nACTION_SORT_ROLE = QtCore.Qt.UserRole + 4\nANIMATION_START_ROLE = QtCore.Qt.UserRole + 5\nANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 6\nFORCE_NOT_OPEN_WORKFILE_ROLE = QtCore.Qt.UserRole + 7\n\n\ndef _variant_label_sort_getter(action_item):\n    \"\"\"Get variant label value for sorting.\n\n    Make sure the output value is a string.\n\n    Args:\n        action_item (ActionItem): Action item.\n\n    Returns:\n        str: Variant label or empty string.\n    \"\"\"\n\n    return action_item.variant_label or \"\"\n\n\nclass ActionsQtModel(QtGui.QStandardItemModel):\n    \"\"\"Qt model for actions.\n\n    Args:\n        controller (AbstractLauncherFrontEnd): Controller instance.\n    \"\"\"\n\n    refreshed = QtCore.Signal()\n\n    def __init__(self, controller):\n        super(ActionsQtModel, self).__init__()\n\n        controller.register_event_callback(\n            \"selection.project.changed\",\n            self._on_selection_project_changed,\n        )\n        controller.register_event_callback(\n            \"selection.folder.changed\",\n            self._on_selection_folder_changed,\n        )\n        controller.register_event_callback(\n            \"selection.task.changed\",\n            self._on_selection_task_changed,\n        )\n\n        self._controller = controller\n\n        self._items_by_id = {}\n        self._action_items_by_id = {}\n        self._groups_by_id = {}\n\n        self._selected_project_name = None\n        self._selected_folder_id = None\n        self._selected_task_id = None\n\n    def get_selected_project_name(self):\n        return self._selected_project_name\n\n    def get_selected_folder_id(self):\n        return self._selected_folder_id\n\n    def get_selected_task_id(self):\n        return self._selected_task_id\n\n    def get_group_items(self, action_id):\n        return self._groups_by_id[action_id]\n\n    def get_item_by_id(self, action_id):\n        return self._items_by_id.get(action_id)\n\n    def get_action_item_by_id(self, action_id):\n        return self._action_items_by_id.get(action_id)\n\n    def _clear_items(self):\n        self._items_by_id = {}\n        self._action_items_by_id = {}\n        self._groups_by_id = {}\n        root = self.invisibleRootItem()\n        root.removeRows(0, root.rowCount())\n\n    def refresh(self):\n        items = self._controller.get_action_items(\n            self._selected_project_name,\n            self._selected_folder_id,\n            self._selected_task_id,\n        )\n        if not items:\n            self._clear_items()\n            self.refreshed.emit()\n            return\n\n        root_item = self.invisibleRootItem()\n\n        all_action_items_info = []\n        items_by_label = collections.defaultdict(list)\n        for item in items:\n            if not item.variant_label:\n                all_action_items_info.append((item, False))\n            else:\n                items_by_label[item.label].append(item)\n\n        groups_by_id = {}\n        for action_items in items_by_label.values():\n            action_items.sort(key=_variant_label_sort_getter, reverse=True)\n            first_item = next(iter(action_items))\n            all_action_items_info.append((first_item, len(action_items) > 1))\n            groups_by_id[first_item.identifier] = action_items\n\n        new_items = []\n        items_by_id = {}\n        action_items_by_id = {}\n        for action_item_info in all_action_items_info:\n            action_item, is_group = action_item_info\n            icon = get_qt_icon(action_item.icon)\n            if is_group:\n                label = action_item.label\n            else:\n                label = action_item.full_label\n\n            item = self._items_by_id.get(action_item.identifier)\n            if item is None:\n                item = QtGui.QStandardItem()\n                item.setData(action_item.identifier, ACTION_ID_ROLE)\n                new_items.append(item)\n\n            item.setFlags(QtCore.Qt.ItemIsEnabled)\n            item.setData(label, QtCore.Qt.DisplayRole)\n            item.setData(icon, QtCore.Qt.DecorationRole)\n            item.setData(is_group, ACTION_IS_GROUP_ROLE)\n            item.setData(action_item.order, ACTION_SORT_ROLE)\n            item.setData(\n                action_item.is_application, ACTION_IS_APPLICATION_ROLE)\n            item.setData(\n                action_item.force_not_open_workfile,\n                FORCE_NOT_OPEN_WORKFILE_ROLE)\n            items_by_id[action_item.identifier] = item\n            action_items_by_id[action_item.identifier] = action_item\n\n        if new_items:\n            root_item.appendRows(new_items)\n\n        to_remove = set(self._items_by_id.keys()) - set(items_by_id.keys())\n        for identifier in to_remove:\n            item = self._items_by_id.pop(identifier)\n            self._action_items_by_id.pop(identifier)\n            root_item.removeRow(item.row())\n\n        self._groups_by_id = groups_by_id\n        self._items_by_id = items_by_id\n        self._action_items_by_id = action_items_by_id\n        self.refreshed.emit()\n\n    def _on_selection_project_changed(self, event):\n        self._selected_project_name = event[\"project_name\"]\n        self._selected_folder_id = None\n        self._selected_task_id = None\n        self.refresh()\n\n    def _on_selection_folder_changed(self, event):\n        self._selected_project_name = event[\"project_name\"]\n        self._selected_folder_id = event[\"folder_id\"]\n        self._selected_task_id = None\n        self.refresh()\n\n    def _on_selection_task_changed(self, event):\n        self._selected_project_name = event[\"project_name\"]\n        self._selected_folder_id = event[\"folder_id\"]\n        self._selected_task_id = event[\"task_id\"]\n        self.refresh()\n\n\nclass ActionDelegate(QtWidgets.QStyledItemDelegate):\n    _cached_extender = {}\n\n    def __init__(self, *args, **kwargs):\n        super(ActionDelegate, self).__init__(*args, **kwargs)\n        self._anim_start_color = QtGui.QColor(178, 255, 246)\n        self._anim_end_color = QtGui.QColor(5, 44, 50)\n\n    def _draw_animation(self, painter, option, index):\n        grid_size = option.widget.gridSize()\n        x_offset = int(\n            (grid_size.width() / 2)\n            - (option.rect.width() / 2)\n        )\n        item_x = option.rect.x() - x_offset\n        rect_offset = grid_size.width() / 20\n        size = grid_size.width() - (rect_offset * 2)\n        anim_rect = QtCore.QRect(\n            item_x + rect_offset,\n            option.rect.y() + rect_offset,\n            size,\n            size\n        )\n\n        painter.save()\n\n        painter.setBrush(QtCore.Qt.transparent)\n\n        gradient = QtGui.QConicalGradient()\n        gradient.setCenter(QtCore.QPointF(anim_rect.center()))\n        gradient.setColorAt(0, self._anim_start_color)\n        gradient.setColorAt(1, self._anim_end_color)\n\n        time_diff = time.time() - index.data(ANIMATION_START_ROLE)\n\n        # Repeat 4 times\n        part_anim = 2.5\n        part_time = time_diff % part_anim\n        offset = (part_time / part_anim) * 360\n        angle = (offset + 90) % 360\n\n        gradient.setAngle(-angle)\n\n        pen = QtGui.QPen(QtGui.QBrush(gradient), rect_offset)\n        pen.setCapStyle(QtCore.Qt.RoundCap)\n        painter.setPen(pen)\n        painter.drawArc(\n            anim_rect,\n            -16 * (angle + 10),\n            -16 * offset\n        )\n\n        painter.restore()\n\n    @classmethod\n    def _get_extender_pixmap(cls, size):\n        pix = cls._cached_extender.get(size)\n        if pix is not None:\n            return pix\n        pix = QtGui.QPixmap(get_options_image_path()).scaled(\n            size, size,\n            QtCore.Qt.KeepAspectRatio,\n            QtCore.Qt.SmoothTransformation\n        )\n        cls._cached_extender[size] = pix\n        return pix\n\n    def paint(self, painter, option, index):\n        painter.setRenderHints(\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n        )\n\n        if index.data(ANIMATION_STATE_ROLE):\n            self._draw_animation(painter, option, index)\n\n        super(ActionDelegate, self).paint(painter, option, index)\n\n        if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE):\n            rect = QtCore.QRectF(\n                option.rect.x(), option.rect.height(), 5, 5)\n            painter.setPen(QtCore.Qt.NoPen)\n            painter.setBrush(QtGui.QColor(200, 0, 0))\n            painter.drawEllipse(rect)\n\n        if not index.data(ACTION_IS_GROUP_ROLE):\n            return\n\n        grid_size = option.widget.gridSize()\n        x_offset = int(\n            (grid_size.width() / 2)\n            - (option.rect.width() / 2)\n        )\n        item_x = option.rect.x() - x_offset\n\n        tenth_size = int(grid_size.width() / 10)\n        extender_size = int(tenth_size * 2.4)\n\n        extender_x = item_x + tenth_size\n        extender_y = option.rect.y() + tenth_size\n\n        pix = self._get_extender_pixmap(extender_size)\n        painter.drawPixmap(extender_x, extender_y, pix)\n\n\nclass ActionsWidget(QtWidgets.QWidget):\n    def __init__(self, controller, parent):\n        super(ActionsWidget, self).__init__(parent)\n\n        self._controller = controller\n\n        view = QtWidgets.QListView(self)\n        view.setProperty(\"mode\", \"icon\")\n        view.setObjectName(\"IconView\")\n        view.setViewMode(QtWidgets.QListView.IconMode)\n        view.setResizeMode(QtWidgets.QListView.Adjust)\n        view.setSelectionMode(QtWidgets.QListView.NoSelection)\n        view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\n        view.setWrapping(True)\n        view.setGridSize(QtCore.QSize(70, 75))\n        view.setIconSize(QtCore.QSize(30, 30))\n        view.setSpacing(0)\n        view.setWordWrap(True)\n\n        # Make view flickable\n        flick = FlickCharm(parent=view)\n        flick.activateOn(view)\n\n        model = ActionsQtModel(controller)\n\n        proxy_model = QtCore.QSortFilterProxyModel()\n        proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n        proxy_model.setSortRole(ACTION_SORT_ROLE)\n\n        proxy_model.setSourceModel(model)\n        view.setModel(proxy_model)\n\n        delegate = ActionDelegate(self)\n        view.setItemDelegate(delegate)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(view)\n\n        animation_timer = QtCore.QTimer()\n        animation_timer.setInterval(40)\n        animation_timer.timeout.connect(self._on_animation)\n\n        view.clicked.connect(self._on_clicked)\n        view.customContextMenuRequested.connect(self._on_context_menu)\n        model.refreshed.connect(self._on_model_refresh)\n\n        self._animated_items = set()\n        self._animation_timer = animation_timer\n\n        self._context_menu = None\n\n        self._flick = flick\n        self._view = view\n        self._model = model\n        self._proxy_model = proxy_model\n\n        self._set_row_height(1)\n\n    def refresh(self):\n        self._model.refresh()\n\n    def _set_row_height(self, rows):\n        self.setMinimumHeight(rows * 75)\n\n    def _on_model_refresh(self):\n        self._proxy_model.sort(0)\n\n    def _on_animation(self):\n        time_now = time.time()\n        for action_id in tuple(self._animated_items):\n            item = self._model.get_item_by_id(action_id)\n            if item is None:\n                self._animated_items.discard(action_id)\n                continue\n\n            start_time = item.data(ANIMATION_START_ROLE)\n            if start_time is None or (time_now - start_time) > ANIMATION_LEN:\n                item.setData(0, ANIMATION_STATE_ROLE)\n                self._animated_items.discard(action_id)\n\n        if not self._animated_items:\n            self._animation_timer.stop()\n\n        self.update()\n\n    def _start_animation(self, index):\n        # Offset refresh timout\n        model_index = self._proxy_model.mapToSource(index)\n        if not model_index.isValid():\n            return\n        action_id = model_index.data(ACTION_ID_ROLE)\n        self._model.setData(model_index, time.time(), ANIMATION_START_ROLE)\n        self._model.setData(model_index, 1, ANIMATION_STATE_ROLE)\n        self._animated_items.add(action_id)\n        self._animation_timer.start()\n\n    def _on_context_menu(self, point):\n        \"\"\"Creates menu to force skip opening last workfile.\"\"\"\n        index = self._view.indexAt(point)\n        if not index.isValid():\n            return\n\n        if not index.data(ACTION_IS_APPLICATION_ROLE):\n            return\n\n        menu = QtWidgets.QMenu(self._view)\n        checkbox = QtWidgets.QCheckBox(\n            \"Skip opening last workfile.\", menu)\n        if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE):\n            checkbox.setChecked(True)\n\n        action_id = index.data(ACTION_ID_ROLE)\n        is_group = index.data(ACTION_IS_GROUP_ROLE)\n        if is_group:\n            action_items = self._model.get_group_items(action_id)\n        else:\n            action_items = [self._model.get_action_item_by_id(action_id)]\n        action_ids = {action_item.identifier for action_item in action_items}\n        checkbox.stateChanged.connect(\n            lambda: self._on_checkbox_changed(\n                action_ids, checkbox.isChecked()\n            )\n        )\n        action = QtWidgets.QWidgetAction(menu)\n        action.setDefaultWidget(checkbox)\n\n        menu.addAction(action)\n\n        self._context_menu = menu\n        global_point = self.mapToGlobal(point)\n        menu.exec_(global_point)\n        self._context_menu = None\n\n    def _on_checkbox_changed(self, action_ids, is_checked):\n        if self._context_menu is not None:\n            self._context_menu.close()\n\n        project_name = self._model.get_selected_project_name()\n        folder_id = self._model.get_selected_folder_id()\n        task_id = self._model.get_selected_task_id()\n        self._controller.set_application_force_not_open_workfile(\n            project_name, folder_id, task_id, action_ids, is_checked)\n        self._model.refresh()\n\n    def _on_clicked(self, index):\n        if not index or not index.isValid():\n            return\n\n        is_group = index.data(ACTION_IS_GROUP_ROLE)\n        action_id = index.data(ACTION_ID_ROLE)\n\n        project_name = self._model.get_selected_project_name()\n        folder_id = self._model.get_selected_folder_id()\n        task_id = self._model.get_selected_task_id()\n\n        if not is_group:\n            self._controller.trigger_action(\n                project_name, folder_id, task_id, action_id\n            )\n            self._start_animation(index)\n            return\n\n        action_items = self._model.get_group_items(action_id)\n\n        menu = QtWidgets.QMenu(self)\n        actions_mapping = {}\n\n        for action_item in action_items:\n            menu_action = QtWidgets.QAction(action_item.full_label)\n            menu.addAction(menu_action)\n            actions_mapping[menu_action] = action_item\n\n        result = menu.exec_(QtGui.QCursor.pos())\n        if not result:\n            return\n\n        action_item = actions_mapping[result]\n\n        self._controller.trigger_action(\n            project_name, folder_id, task_id, action_item.identifier\n        )\n        self._start_animation(index)\n"
  },
  {
    "path": "openpype/tools/ayon_launcher/ui/hierarchy_page.py",
    "content": "import qtawesome\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.tools.utils import (\n    PlaceholderLineEdit,\n    SquareButton,\n    RefreshButton,\n)\nfrom openpype.tools.ayon_utils.widgets import (\n    ProjectsCombobox,\n    FoldersWidget,\n    TasksWidget,\n)\n\n\nclass HierarchyPage(QtWidgets.QWidget):\n    def __init__(self, controller, parent):\n        super(HierarchyPage, self).__init__(parent)\n\n        # Header\n        header_widget = QtWidgets.QWidget(self)\n\n        btn_back_icon = qtawesome.icon(\"fa.angle-left\", color=\"white\")\n        btn_back = SquareButton(header_widget)\n        btn_back.setIcon(btn_back_icon)\n\n        projects_combobox = ProjectsCombobox(controller, header_widget)\n\n        refresh_btn = RefreshButton(header_widget)\n\n        header_layout = QtWidgets.QHBoxLayout(header_widget)\n        header_layout.setContentsMargins(0, 0, 0, 0)\n        header_layout.addWidget(btn_back, 0)\n        header_layout.addWidget(projects_combobox, 1)\n        header_layout.addWidget(refresh_btn, 0)\n\n        # Body - Folders + Tasks selection\n        content_body = QtWidgets.QSplitter(self)\n        content_body.setContentsMargins(0, 0, 0, 0)\n        content_body.setSizePolicy(\n            QtWidgets.QSizePolicy.Expanding,\n            QtWidgets.QSizePolicy.Expanding\n        )\n        content_body.setOrientation(QtCore.Qt.Horizontal)\n\n        # - Folders widget with filter\n        folders_wrapper = QtWidgets.QWidget(content_body)\n\n        folders_filter_text = PlaceholderLineEdit(folders_wrapper)\n        folders_filter_text.setPlaceholderText(\"Filter folders...\")\n\n        folders_widget = FoldersWidget(controller, folders_wrapper)\n\n        folders_wrapper_layout = QtWidgets.QVBoxLayout(folders_wrapper)\n        folders_wrapper_layout.setContentsMargins(0, 0, 0, 0)\n        folders_wrapper_layout.addWidget(folders_filter_text, 0)\n        folders_wrapper_layout.addWidget(folders_widget, 1)\n\n        # - Tasks widget\n        tasks_widget = TasksWidget(controller, content_body)\n\n        content_body.addWidget(folders_wrapper)\n        content_body.addWidget(tasks_widget)\n        content_body.setStretchFactor(0, 100)\n        content_body.setStretchFactor(1, 65)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(header_widget, 0)\n        main_layout.addWidget(content_body, 1)\n\n        btn_back.clicked.connect(self._on_back_clicked)\n        refresh_btn.clicked.connect(self._on_refreh_clicked)\n        folders_filter_text.textChanged.connect(self._on_filter_text_changed)\n\n        self._is_visible = False\n        self._controller = controller\n\n        self._btn_back = btn_back\n        self._projects_combobox = projects_combobox\n        self._folders_widget = folders_widget\n        self._tasks_widget = tasks_widget\n\n        # Post init\n        projects_combobox.set_listen_to_selection_change(self._is_visible)\n\n    def set_page_visible(self, visible, project_name=None):\n        if self._is_visible == visible:\n            return\n        self._is_visible = visible\n        self._projects_combobox.set_listen_to_selection_change(visible)\n        if visible and project_name:\n            self._projects_combobox.set_selection(project_name)\n\n    def refresh(self):\n        self._folders_widget.refresh()\n        self._tasks_widget.refresh()\n\n    def _on_back_clicked(self):\n        self._controller.set_selected_project(None)\n\n    def _on_refreh_clicked(self):\n        self._controller.refresh()\n\n    def _on_filter_text_changed(self, text):\n        self._folders_widget.set_name_filter(text)\n"
  },
  {
    "path": "openpype/tools/ayon_launcher/ui/projects_widget.py",
    "content": "from qtpy import QtWidgets, QtCore\n\nfrom openpype.tools.flickcharm import FlickCharm\nfrom openpype.tools.utils import PlaceholderLineEdit, RefreshButton\nfrom openpype.tools.ayon_utils.widgets import (\n    ProjectsQtModel,\n    ProjectSortFilterProxy,\n)\nfrom openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER\n\n\nclass ProjectIconView(QtWidgets.QListView):\n    \"\"\"Styled ListView that allows to toggle between icon and list mode.\n\n    Toggling between the two modes is done by Right Mouse Click.\n    \"\"\"\n\n    IconMode = 0\n    ListMode = 1\n\n    def __init__(self, parent=None, mode=ListMode):\n        super(ProjectIconView, self).__init__(parent=parent)\n\n        # Workaround for scrolling being super slow or fast when\n        # toggling between the two visual modes\n        self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)\n        self.setObjectName(\"IconView\")\n\n        self._mode = None\n        self.set_mode(mode)\n\n    def set_mode(self, mode):\n        if mode == self._mode:\n            return\n\n        self._mode = mode\n\n        if mode == self.IconMode:\n            self.setViewMode(QtWidgets.QListView.IconMode)\n            self.setResizeMode(QtWidgets.QListView.Adjust)\n            self.setWrapping(True)\n            self.setWordWrap(True)\n            self.setGridSize(QtCore.QSize(151, 90))\n            self.setIconSize(QtCore.QSize(50, 50))\n            self.setSpacing(0)\n            self.setAlternatingRowColors(False)\n\n            self.setProperty(\"mode\", \"icon\")\n            self.style().polish(self)\n\n            self.verticalScrollBar().setSingleStep(30)\n\n        elif self.ListMode:\n            self.setProperty(\"mode\", \"list\")\n            self.style().polish(self)\n\n            self.setViewMode(QtWidgets.QListView.ListMode)\n            self.setResizeMode(QtWidgets.QListView.Adjust)\n            self.setWrapping(False)\n            self.setWordWrap(False)\n            self.setIconSize(QtCore.QSize(20, 20))\n            self.setGridSize(QtCore.QSize(100, 25))\n            self.setSpacing(0)\n            self.setAlternatingRowColors(False)\n\n            self.verticalScrollBar().setSingleStep(34)\n\n    def mousePressEvent(self, event):\n        if event.button() == QtCore.Qt.RightButton:\n            self.set_mode(int(not self._mode))\n        return super(ProjectIconView, self).mousePressEvent(event)\n\n\nclass ProjectsWidget(QtWidgets.QWidget):\n    \"\"\"Projects Page\"\"\"\n\n    refreshed = QtCore.Signal()\n\n    def __init__(self, controller, parent=None):\n        super(ProjectsWidget, self).__init__(parent=parent)\n\n        header_widget = QtWidgets.QWidget(self)\n\n        projects_filter_text = PlaceholderLineEdit(header_widget)\n        projects_filter_text.setPlaceholderText(\"Filter projects...\")\n\n        refresh_btn = RefreshButton(header_widget)\n\n        header_layout = QtWidgets.QHBoxLayout(header_widget)\n        header_layout.setContentsMargins(0, 0, 0, 0)\n        header_layout.addWidget(projects_filter_text, 1)\n        header_layout.addWidget(refresh_btn, 0)\n\n        projects_view = ProjectIconView(parent=self)\n        projects_view.setSelectionMode(QtWidgets.QListView.NoSelection)\n        flick = FlickCharm(parent=self)\n        flick.activateOn(projects_view)\n        projects_model = ProjectsQtModel(controller)\n        projects_proxy_model = ProjectSortFilterProxy()\n        projects_proxy_model.setSourceModel(projects_model)\n\n        projects_view.setModel(projects_proxy_model)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(header_widget, 0)\n        main_layout.addWidget(projects_view, 1)\n\n        projects_view.clicked.connect(self._on_view_clicked)\n        projects_model.refreshed.connect(self.refreshed)\n        projects_filter_text.textChanged.connect(\n            self._on_project_filter_change)\n        refresh_btn.clicked.connect(self._on_refresh_clicked)\n\n        controller.register_event_callback(\n            \"projects.refresh.finished\",\n            self._on_projects_refresh_finished\n        )\n\n        self._controller = controller\n\n        self._projects_view = projects_view\n        self._projects_model = projects_model\n        self._projects_proxy_model = projects_proxy_model\n\n    def has_content(self):\n        \"\"\"Model has at least one project.\n\n        Returns:\n             bool: True if there is any content in the model.\n        \"\"\"\n\n        return self._projects_model.has_content()\n\n    def _on_view_clicked(self, index):\n        if not index.isValid():\n            return\n        model = index.model()\n        flags = model.flags(index)\n        if not flags & QtCore.Qt.ItemIsEnabled:\n            return\n        project_name = index.data(QtCore.Qt.DisplayRole)\n        self._controller.set_selected_project(project_name)\n\n    def _on_project_filter_change(self, text):\n        self._projects_proxy_model.setFilterFixedString(text)\n\n    def _on_refresh_clicked(self):\n        self._controller.refresh()\n\n    def _on_projects_refresh_finished(self, event):\n        if event[\"sender\"] != PROJECTS_MODEL_SENDER:\n            self._projects_model.refresh()\n"
  },
  {
    "path": "openpype/tools/ayon_launcher/ui/resources/__init__.py",
    "content": "import os\n\nRESOURCES_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\ndef get_options_image_path():\n    return os.path.join(RESOURCES_DIR, \"options.png\")\n"
  },
  {
    "path": "openpype/tools/ayon_launcher/ui/window.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import style\nfrom openpype import resources\n\nfrom openpype.tools.ayon_launcher.control import BaseLauncherController\n\nfrom .projects_widget import ProjectsWidget\nfrom .hierarchy_page import HierarchyPage\nfrom .actions_widget import ActionsWidget\n\n\nclass LauncherWindow(QtWidgets.QWidget):\n    \"\"\"Launcher interface\"\"\"\n    message_interval = 5000\n    refresh_interval = 10000\n    page_side_anim_interval = 250\n\n    def __init__(self, controller=None, parent=None):\n        super(LauncherWindow, self).__init__(parent)\n\n        if controller is None:\n            controller = BaseLauncherController()\n\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n        self.setWindowTitle(\"Launcher\")\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n        self.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)\n\n        self.setStyleSheet(style.load_stylesheet())\n\n        # Allow minimize\n        self.setWindowFlags(\n            QtCore.Qt.Window\n            | QtCore.Qt.CustomizeWindowHint\n            | QtCore.Qt.WindowTitleHint\n            | QtCore.Qt.WindowMinimizeButtonHint\n            | QtCore.Qt.WindowCloseButtonHint\n        )\n\n        self._controller = controller\n\n        # Main content - Pages & Actions\n        content_body = QtWidgets.QSplitter(self)\n\n        # Pages\n        pages_widget = QtWidgets.QWidget(content_body)\n\n        # - First page - Projects\n        projects_page = ProjectsWidget(controller, pages_widget)\n\n        # - Second page - Hierarchy (folders & tasks)\n        hierarchy_page = HierarchyPage(controller, pages_widget)\n\n        pages_layout = QtWidgets.QHBoxLayout(pages_widget)\n        pages_layout.setContentsMargins(0, 0, 0, 0)\n        pages_layout.addWidget(projects_page, 1)\n        pages_layout.addWidget(hierarchy_page, 1)\n\n        # Actions\n        actions_widget = ActionsWidget(controller, content_body)\n\n        # Vertically split Pages and Actions\n        content_body.setContentsMargins(0, 0, 0, 0)\n        content_body.setSizePolicy(\n            QtWidgets.QSizePolicy.Expanding,\n            QtWidgets.QSizePolicy.Expanding\n        )\n        content_body.setOrientation(QtCore.Qt.Vertical)\n        content_body.addWidget(pages_widget)\n        content_body.addWidget(actions_widget)\n\n        # Set useful default sizes and set stretch\n        # for the pages so that is the only one that\n        # stretches on UI resize.\n        content_body.setStretchFactor(0, 10)\n        content_body.setSizes([580, 160])\n\n        # Footer\n        footer_widget = QtWidgets.QWidget(self)\n\n        # - Message label\n        message_label = QtWidgets.QLabel(footer_widget)\n\n        # action_history = ActionHistory(footer_widget)\n        # action_history.setStatusTip(\"Show Action History\")\n\n        footer_layout = QtWidgets.QHBoxLayout(footer_widget)\n        footer_layout.setContentsMargins(0, 0, 0, 0)\n        footer_layout.addWidget(message_label, 1)\n        # footer_layout.addWidget(action_history, 0)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(content_body, 1)\n        layout.addWidget(footer_widget, 0)\n\n        message_timer = QtCore.QTimer()\n        message_timer.setInterval(self.message_interval)\n        message_timer.setSingleShot(True)\n\n        actions_refresh_timer = QtCore.QTimer()\n        actions_refresh_timer.setInterval(self.refresh_interval)\n\n        page_slide_anim = QtCore.QVariantAnimation(self)\n        page_slide_anim.setDuration(self.page_side_anim_interval)\n        page_slide_anim.setStartValue(0.0)\n        page_slide_anim.setEndValue(1.0)\n        page_slide_anim.setEasingCurve(QtCore.QEasingCurve.OutQuad)\n\n        projects_page.refreshed.connect(self._on_projects_refresh)\n        message_timer.timeout.connect(self._on_message_timeout)\n        actions_refresh_timer.timeout.connect(\n            self._on_actions_refresh_timeout)\n        page_slide_anim.valueChanged.connect(\n            self._on_page_slide_value_changed)\n        page_slide_anim.finished.connect(self._on_page_slide_finished)\n\n        controller.register_event_callback(\n            \"selection.project.changed\",\n            self._on_project_selection_change,\n        )\n        controller.register_event_callback(\n            \"action.trigger.started\",\n            self._on_action_trigger_started,\n        )\n        controller.register_event_callback(\n            \"action.trigger.finished\",\n            self._on_action_trigger_finished,\n        )\n\n        self._controller = controller\n\n        self._is_on_projects_page = True\n        self._window_is_active = False\n        self._refresh_on_activate = False\n        self._selected_project_name = None\n\n        self._pages_widget = pages_widget\n        self._pages_layout = pages_layout\n        self._projects_page = projects_page\n        self._hierarchy_page = hierarchy_page\n        self._actions_widget = actions_widget\n\n        self._message_label = message_label\n        # self._action_history = action_history\n\n        self._message_timer = message_timer\n        self._actions_refresh_timer = actions_refresh_timer\n        self._page_slide_anim = page_slide_anim\n\n        hierarchy_page.setVisible(not self._is_on_projects_page)\n        self.resize(520, 740)\n\n    def showEvent(self, event):\n        super(LauncherWindow, self).showEvent(event)\n        self._window_is_active = True\n        if not self._actions_refresh_timer.isActive():\n            self._actions_refresh_timer.start()\n        self._controller.refresh()\n\n    def closeEvent(self, event):\n        super(LauncherWindow, self).closeEvent(event)\n        self._window_is_active = False\n        self._actions_refresh_timer.stop()\n\n    def changeEvent(self, event):\n        if event.type() in (\n            QtCore.QEvent.Type.WindowStateChange,\n            QtCore.QEvent.ActivationChange,\n        ):\n            is_active = self.isActiveWindow() and not self.isMinimized()\n            self._window_is_active = is_active\n            if is_active and self._refresh_on_activate:\n                self._refresh_on_activate = False\n                self._on_actions_refresh_timeout()\n                self._actions_refresh_timer.start()\n\n        super(LauncherWindow, self).changeEvent(event)\n\n    def _on_actions_refresh_timeout(self):\n        # Stop timer if widget is not visible\n        if self._window_is_active:\n            self._controller.refresh_actions()\n        else:\n            self._refresh_on_activate = True\n\n    def _echo(self, message):\n        self._message_label.setText(str(message))\n        self._message_timer.start()\n\n    def _on_message_timeout(self):\n        self._message_label.setText(\"\")\n\n    def _on_project_selection_change(self, event):\n        project_name = event[\"project_name\"]\n        self._selected_project_name = project_name\n        if not project_name:\n            self._go_to_projects_page()\n\n        elif self._is_on_projects_page:\n            self._go_to_hierarchy_page(project_name)\n\n    def _on_projects_refresh(self):\n        # There is nothing to do, we're on projects page\n        if self._is_on_projects_page:\n            return\n\n        # No projects were found -> go back to projects page\n        if not self._projects_page.has_content():\n            self._go_to_projects_page()\n            return\n\n        self._hierarchy_page.refresh()\n        self._actions_widget.refresh()\n\n    def _on_action_trigger_started(self, event):\n        self._echo(\"Running action: {}\".format(event[\"full_label\"]))\n\n    def _on_action_trigger_finished(self, event):\n        if not event[\"failed\"]:\n            return\n        self._echo(\"Failed: {}\".format(event[\"error_message\"]))\n\n    def _is_page_slide_anim_running(self):\n        return (\n            self._page_slide_anim.state() == QtCore.QAbstractAnimation.Running\n        )\n\n    def _go_to_projects_page(self):\n        if self._is_on_projects_page:\n            return\n        self._is_on_projects_page = True\n        self._hierarchy_page.set_page_visible(False)\n\n        self._start_page_slide_animation()\n\n    def _go_to_hierarchy_page(self, project_name):\n        if not self._is_on_projects_page:\n            return\n        self._is_on_projects_page = False\n        self._hierarchy_page.set_page_visible(True, project_name)\n\n        self._start_page_slide_animation()\n\n    def _start_page_slide_animation(self):\n        if self._is_on_projects_page:\n            direction = QtCore.QAbstractAnimation.Backward\n        else:\n            direction = QtCore.QAbstractAnimation.Forward\n        self._page_slide_anim.setDirection(direction)\n        if self._is_page_slide_anim_running():\n            return\n\n        layout_spacing = self._pages_layout.spacing()\n        if self._is_on_projects_page:\n            hierarchy_geo = self._hierarchy_page.geometry()\n            projects_geo = QtCore.QRect(hierarchy_geo)\n            projects_geo.moveRight(\n                hierarchy_geo.left() - (layout_spacing + 1))\n\n            self._projects_page.setVisible(True)\n\n        else:\n            projects_geo = self._projects_page.geometry()\n            hierarchy_geo = QtCore.QRect(projects_geo)\n            hierarchy_geo.moveLeft(projects_geo.right() + layout_spacing)\n            self._hierarchy_page.setVisible(True)\n\n        while self._pages_layout.count():\n            self._pages_layout.takeAt(0)\n\n        self._projects_page.setGeometry(projects_geo)\n        self._hierarchy_page.setGeometry(hierarchy_geo)\n\n        self._page_slide_anim.start()\n\n    def _on_page_slide_value_changed(self, value):\n        layout_spacing = self._pages_layout.spacing()\n        content_width = self._pages_widget.width() - layout_spacing\n        content_height = self._pages_widget.height()\n\n        # Visible widths of other widgets\n        hierarchy_width = int(content_width * value)\n\n        hierarchy_geo = QtCore.QRect(\n            content_width - hierarchy_width, 0, content_width, content_height\n        )\n        projects_geo = QtCore.QRect(hierarchy_geo)\n        projects_geo.moveRight(hierarchy_geo.left() - (layout_spacing + 1))\n\n        self._projects_page.setGeometry(projects_geo)\n        self._hierarchy_page.setGeometry(hierarchy_geo)\n\n    def _on_page_slide_finished(self):\n        self._pages_layout.addWidget(self._projects_page, 1)\n        self._pages_layout.addWidget(self._hierarchy_page, 1)\n        self._projects_page.setVisible(self._is_on_projects_page)\n        self._hierarchy_page.setVisible(not self._is_on_projects_page)\n\n    # def _on_history_action(self, history_data):\n    #     action, session = history_data\n    #     app = QtWidgets.QApplication.instance()\n    #     modifiers = app.keyboardModifiers()\n    #\n    #     is_control_down = QtCore.Qt.ControlModifier & modifiers\n    #     if is_control_down:\n    #         # Revert to that \"session\" location\n    #         self.set_session(session)\n    #     else:\n    #         # User is holding control, rerun the action\n    #         self.run_action(action, session=session)\n"
  },
  {
    "path": "openpype/tools/ayon_loader/__init__.py",
    "content": "from .control import LoaderController\n\n\n__all__ = (\n    \"LoaderController\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_loader/abstract.py",
    "content": "from abc import ABCMeta, abstractmethod\nimport six\n\nfrom openpype.lib.attribute_definitions import (\n    AbstractAttrDef,\n    serialize_attr_defs,\n    deserialize_attr_defs,\n)\n\n\nclass ProductTypeItem:\n    \"\"\"Item representing product type.\n\n    Args:\n        name (str): Product type name.\n        icon (dict[str, Any]): Product type icon definition.\n        checked (bool): Is product type checked for filtering.\n    \"\"\"\n\n    def __init__(self, name, icon, checked):\n        self.name = name\n        self.icon = icon\n        self.checked = checked\n\n    def to_data(self):\n        return {\n            \"name\": self.name,\n            \"icon\": self.icon,\n            \"checked\": self.checked,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        return cls(**data)\n\n\nclass ProductItem:\n    \"\"\"Product item with it versions.\n\n    Args:\n        product_id (str): Product id.\n        product_type (str): Product type.\n        product_name (str): Product name.\n        product_icon (dict[str, Any]): Product icon definition.\n        product_type_icon (dict[str, Any]): Product type icon definition.\n        product_in_scene (bool): Is product in scene (only when used in DCC).\n        group_name (str): Group name.\n        folder_id (str): Folder id.\n        folder_label (str): Folder label.\n        version_items (dict[str, VersionItem]): Version items by id.\n    \"\"\"\n\n    def __init__(\n        self,\n        product_id,\n        product_type,\n        product_name,\n        product_icon,\n        product_type_icon,\n        product_in_scene,\n        group_name,\n        folder_id,\n        folder_label,\n        version_items,\n    ):\n        self.product_id = product_id\n        self.product_type = product_type\n        self.product_name = product_name\n        self.product_icon = product_icon\n        self.product_type_icon = product_type_icon\n        self.product_in_scene = product_in_scene\n        self.group_name = group_name\n        self.folder_id = folder_id\n        self.folder_label = folder_label\n        self.version_items = version_items\n\n    def to_data(self):\n        return {\n            \"product_id\": self.product_id,\n            \"product_type\": self.product_type,\n            \"product_name\": self.product_name,\n            \"product_icon\": self.product_icon,\n            \"product_type_icon\": self.product_type_icon,\n            \"product_in_scene\": self.product_in_scene,\n            \"group_name\": self.group_name,\n            \"folder_id\": self.folder_id,\n            \"folder_label\": self.folder_label,\n            \"version_items\": {\n                version_id: version_item.to_data()\n                for version_id, version_item in self.version_items.items()\n            },\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        version_items = {\n            version_id: VersionItem.from_data(version)\n            for version_id, version in data[\"version_items\"].items()\n        }\n        data[\"version_items\"] = version_items\n        return cls(**data)\n\n\nclass VersionItem:\n    \"\"\"Version item.\n\n    Object have implemented comparison operators to be sortable.\n\n    Args:\n        version_id (str): Version id.\n        version (int): Version. Can be negative when is hero version.\n        is_hero (bool): Is hero version.\n        product_id (str): Product id.\n        thumbnail_id (Union[str, None]): Thumbnail id.\n        published_time (Union[str, None]): Published time in format\n            '%Y%m%dT%H%M%SZ'.\n        author (Union[str, None]): Author.\n        frame_range (Union[str, None]): Frame range.\n        duration (Union[int, None]): Duration.\n        handles (Union[str, None]): Handles.\n        step (Union[int, None]): Step.\n        comment (Union[str, None]): Comment.\n        source (Union[str, None]): Source.\n    \"\"\"\n\n    def __init__(\n        self,\n        version_id,\n        version,\n        is_hero,\n        product_id,\n        thumbnail_id,\n        published_time,\n        author,\n        frame_range,\n        duration,\n        handles,\n        step,\n        comment,\n        source,\n    ):\n        self.version_id = version_id\n        self.product_id = product_id\n        self.thumbnail_id = thumbnail_id\n        self.version = version\n        self.is_hero = is_hero\n        self.published_time = published_time\n        self.author = author\n        self.frame_range = frame_range\n        self.duration = duration\n        self.handles = handles\n        self.step = step\n        self.comment = comment\n        self.source = source\n\n    def __eq__(self, other):\n        if not isinstance(other, VersionItem):\n            return False\n        return (\n            self.is_hero == other.is_hero\n            and self.version == other.version\n            and self.version_id == other.version_id\n            and self.product_id == other.product_id\n        )\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    def __gt__(self, other):\n        if not isinstance(other, VersionItem):\n            return False\n        if (\n            other.version == self.version\n            and self.is_hero\n        ):\n            return True\n        return other.version < self.version\n\n    def to_data(self):\n        return {\n            \"version_id\": self.version_id,\n            \"product_id\": self.product_id,\n            \"thumbnail_id\": self.thumbnail_id,\n            \"version\": self.version,\n            \"is_hero\": self.is_hero,\n            \"published_time\": self.published_time,\n            \"author\": self.author,\n            \"frame_range\": self.frame_range,\n            \"duration\": self.duration,\n            \"handles\": self.handles,\n            \"step\": self.step,\n            \"comment\": self.comment,\n            \"source\": self.source,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        return cls(**data)\n\n\nclass RepreItem:\n    \"\"\"Representation item.\n\n    Args:\n        representation_id (str): Representation id.\n        representation_name (str): Representation name.\n        representation_icon (dict[str, Any]): Representation icon definition.\n        product_name (str): Product name.\n        folder_label (str): Folder label.\n    \"\"\"\n\n    def __init__(\n        self,\n        representation_id,\n        representation_name,\n        representation_icon,\n        product_name,\n        folder_label\n    ):\n        self.representation_id = representation_id\n        self.representation_name = representation_name\n        self.representation_icon = representation_icon\n        self.product_name = product_name\n        self.folder_label = folder_label\n\n    def to_data(self):\n        return {\n            \"representation_id\": self.representation_id,\n            \"representation_name\": self.representation_name,\n            \"representation_icon\": self.representation_icon,\n            \"product_name\": self.product_name,\n            \"folder_label\": self.folder_label,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        return cls(**data)\n\n\nclass ActionItem:\n    \"\"\"Action item that can be triggered.\n\n    Action item is defined for a specific context. To trigger the action\n    use 'identifier' and context, it necessary also use 'options'.\n\n    Args:\n        identifier (str): Action identifier.\n        label (str): Action label.\n        icon (dict[str, Any]): Action icon definition.\n        tooltip (str): Action tooltip.\n        options (Union[list[AbstractAttrDef], list[qargparse.QArgument]]):\n            Action options. Note: 'qargparse' is considered as deprecated.\n        order (int): Action order.\n        project_name (str): Project name.\n        folder_ids (list[str]): Folder ids.\n        product_ids (list[str]): Product ids.\n        version_ids (list[str]): Version ids.\n        representation_ids (list[str]): Representation ids.\n    \"\"\"\n\n    def __init__(\n        self,\n        identifier,\n        label,\n        icon,\n        tooltip,\n        options,\n        order,\n        project_name,\n        folder_ids,\n        product_ids,\n        version_ids,\n        representation_ids,\n    ):\n        self.identifier = identifier\n        self.label = label\n        self.icon = icon\n        self.tooltip = tooltip\n        self.options = options\n        self.order = order\n        self.project_name = project_name\n        self.folder_ids = folder_ids\n        self.product_ids = product_ids\n        self.version_ids = version_ids\n        self.representation_ids = representation_ids\n\n    def _options_to_data(self):\n        options = self.options\n        if not options:\n            return options\n        if isinstance(options[0], AbstractAttrDef):\n            return serialize_attr_defs(options)\n        # NOTE: Data conversion is not used by default in loader tool. But for\n        #   future development of detached UI tools it would be better to be\n        #   prepared for it.\n        raise NotImplementedError(\n            \"{}.to_data is not implemented. Use Attribute definitions\"\n            \" from 'openpype.lib' instead of 'qargparse'.\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def to_data(self):\n        options = self._options_to_data()\n        return {\n            \"identifier\": self.identifier,\n            \"label\": self.label,\n            \"icon\": self.icon,\n            \"tooltip\": self.tooltip,\n            \"options\": options,\n            \"order\": self.order,\n            \"project_name\": self.project_name,\n            \"folder_ids\": self.folder_ids,\n            \"product_ids\": self.product_ids,\n            \"version_ids\": self.version_ids,\n            \"representation_ids\": self.representation_ids,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        options = data[\"options\"]\n        if options:\n            options = deserialize_attr_defs(options)\n        data[\"options\"] = options\n        return cls(**data)\n\n\n@six.add_metaclass(ABCMeta)\nclass _BaseLoaderController(object):\n    \"\"\"Base loader controller abstraction.\n\n    Abstract base class that is required for both frontend and backed.\n    \"\"\"\n\n    @abstractmethod\n    def get_current_context(self):\n        \"\"\"Current context is a context of the current scene.\n\n        Example output:\n            {\n                \"project_name\": \"MyProject\",\n                \"folder_id\": \"0011223344-5566778-99\",\n                \"task_name\": \"Compositing\",\n            }\n\n        Returns:\n            dict[str, Union[str, None]]: Context data.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def reset(self):\n        \"\"\"Reset all cached data to reload everything.\n\n        Triggers events \"controller.reset.started\" and\n        \"controller.reset.finished\".\n        \"\"\"\n\n        pass\n\n    # Model wrappers\n    @abstractmethod\n    def get_folder_items(self, project_name, sender=None):\n        \"\"\"Folder items for a project.\n\n        Args:\n            project_name (str): Project name.\n            sender (Optional[str]): Sender who requested the name.\n\n        Returns:\n            list[FolderItem]: Folder items for the project.\n        \"\"\"\n\n        pass\n\n    # Expected selection helpers\n    @abstractmethod\n    def get_expected_selection_data(self):\n        \"\"\"Full expected selection information.\n\n        Expected selection is a selection that may not be yet selected in UI\n        e.g. because of refreshing, this data tell the UI what should be\n        selected when they finish their refresh.\n\n        Returns:\n            dict[str, Any]: Expected selection data.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_expected_selection(self, project_name, folder_id):\n        \"\"\"Set expected selection.\n\n        Args:\n            project_name (str): Name of project to be selected.\n            folder_id (str): Id of folder to be selected.\n        \"\"\"\n\n        pass\n\n\nclass BackendLoaderController(_BaseLoaderController):\n    \"\"\"Backend loader controller abstraction.\n\n    What backend logic requires from a controller for proper logic.\n    \"\"\"\n\n    @abstractmethod\n    def emit_event(self, topic, data=None, source=None):\n        \"\"\"Emit event with a certain topic, data and source.\n\n        The event should be sent to both frontend and backend.\n\n        Args:\n            topic (str): Event topic name.\n            data (Optional[dict[str, Any]]): Event data.\n            source (Optional[str]): Event source.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_loaded_product_ids(self):\n        \"\"\"Return set of loaded product ids.\n\n        Returns:\n            set[str]: Set of loaded product ids.\n        \"\"\"\n\n        pass\n\n\nclass FrontendLoaderController(_BaseLoaderController):\n    @abstractmethod\n    def register_event_callback(self, topic, callback):\n        \"\"\"Register callback for an event topic.\n\n        Args:\n            topic (str): Event topic name.\n            callback (func): Callback triggered when the event is emitted.\n        \"\"\"\n\n        pass\n\n    # Expected selection helpers\n    @abstractmethod\n    def expected_project_selected(self, project_name):\n        \"\"\"Expected project was selected in frontend.\n\n        Args:\n            project_name (str): Project name.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def expected_folder_selected(self, folder_id):\n        \"\"\"Expected folder was selected in frontend.\n\n        Args:\n            folder_id (str): Folder id.\n        \"\"\"\n\n        pass\n\n    # Model wrapper calls\n    @abstractmethod\n    def get_project_items(self, sender=None):\n        \"\"\"Items for all projects available on server.\n\n        Triggers event topics \"projects.refresh.started\" and\n        \"projects.refresh.finished\" with data:\n            {\n                \"sender\": sender\n            }\n\n        Notes:\n            Filtering of projects is done in UI.\n\n        Args:\n            sender (Optional[str]): Sender who requested the items.\n\n        Returns:\n            list[ProjectItem]: List of project items.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_product_items(self, project_name, folder_ids, sender=None):\n        \"\"\"Product items for folder ids.\n\n        Triggers event topics \"products.refresh.started\" and\n        \"products.refresh.finished\" with data:\n            {\n                \"project_name\": project_name,\n                \"folder_ids\": folder_ids,\n                \"sender\": sender\n            }\n\n        Args:\n            project_name (str): Project name.\n            folder_ids (Iterable[str]): Folder ids.\n            sender (Optional[str]): Sender who requested the items.\n\n        Returns:\n            list[ProductItem]: List of product items.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_product_item(self, project_name, product_id):\n        \"\"\"Receive single product item.\n\n        Args:\n            project_name (str): Project name.\n            product_id (str): Product id.\n\n        Returns:\n             Union[ProductItem, None]: Product info or None if not found.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_product_type_items(self, project_name):\n        \"\"\"Product type items for a project.\n\n        Product types have defined if are checked for filtering or not.\n\n        Args:\n            project_name (Union[str, None]): Project name.\n\n        Returns:\n            list[ProductTypeItem]: List of product type items for a project.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_representation_items(\n        self, project_name, version_ids, sender=None\n    ):\n        \"\"\"Representation items for version ids.\n\n        Triggers event topics \"model.representations.refresh.started\" and\n        \"model.representations.refresh.finished\" with data:\n            {\n                \"project_name\": project_name,\n                \"version_ids\": version_ids,\n                \"sender\": sender\n            }\n\n        Args:\n            project_name (str): Project name.\n            version_ids (Iterable[str]): Version ids.\n            sender (Optional[str]): Sender who requested the items.\n\n        Returns:\n            list[RepreItem]: List of representation items.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_version_thumbnail_ids(self, project_name, version_ids):\n        \"\"\"Get thumbnail ids for version ids.\n\n        Args:\n            project_name (str): Project name.\n            version_ids (Iterable[str]): Version ids.\n\n        Returns:\n            dict[str, Union[str, Any]]: Thumbnail id by version id.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_folder_thumbnail_ids(self, project_name, folder_ids):\n        \"\"\"Get thumbnail ids for folder ids.\n\n        Args:\n            project_name (str): Project name.\n            folder_ids (Iterable[str]): Folder ids.\n\n        Returns:\n            dict[str, Union[str, Any]]: Thumbnail id by folder id.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_versions_representation_count(\n        self, project_name, version_ids, sender=None\n    ):\n        \"\"\"\n        Args:\n            project_name (str): Project name.\n            version_ids (Iterable[str]): Version ids.\n            sender (Optional[str]): Sender who requested the items.\n\n        Returns:\n            dict[str, int]: Representation count by version id.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_thumbnail_path(self, project_name, thumbnail_id):\n        \"\"\"Get thumbnail path for thumbnail id.\n\n        This method should get a path to a thumbnail based on thumbnail id.\n        Which probably means to download the thumbnail from server and store\n        it locally.\n\n        Args:\n            project_name (str): Project name.\n            thumbnail_id (str): Thumbnail id.\n\n        Returns:\n            Union[str, None]: Thumbnail path or None if not found.\n        \"\"\"\n\n        pass\n\n    # Selection model wrapper calls\n    @abstractmethod\n    def get_selected_project_name(self):\n        \"\"\"Get selected project name.\n\n        The information is based on last selection from UI.\n\n        Returns:\n            Union[str, None]: Selected project name.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_selected_folder_ids(self):\n        \"\"\"Get selected folder ids.\n\n        The information is based on last selection from UI.\n\n        Returns:\n            list[str]: Selected folder ids.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_selected_version_ids(self):\n        \"\"\"Get selected version ids.\n\n        The information is based on last selection from UI.\n\n        Returns:\n            list[str]: Selected version ids.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_selected_representation_ids(self):\n        \"\"\"Get selected representation ids.\n\n        The information is based on last selection from UI.\n\n        Returns:\n            list[str]: Selected representation ids.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_selected_project(self, project_name):\n        \"\"\"Set selected project.\n\n        Project selection changed in UI. Method triggers event with topic\n        \"selection.project.changed\" with data:\n            {\n                \"project_name\": self._project_name\n            }\n\n        Args:\n            project_name (Union[str, None]): Selected project name.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_selected_folders(self, folder_ids):\n        \"\"\"Set selected folders.\n\n        Folder selection changed in UI. Method triggers event with topic\n        \"selection.folders.changed\" with data:\n            {\n                \"project_name\": project_name,\n                \"folder_ids\": folder_ids\n            }\n\n        Args:\n            folder_ids (Iterable[str]): Selected folder ids.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_selected_versions(self, version_ids):\n        \"\"\"Set selected versions.\n\n        Version selection changed in UI. Method triggers event with topic\n        \"selection.versions.changed\" with data:\n            {\n                \"project_name\": project_name,\n                \"folder_ids\": folder_ids,\n                \"version_ids\": version_ids\n            }\n\n        Args:\n            version_ids (Iterable[str]): Selected version ids.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_selected_representations(self, repre_ids):\n        \"\"\"Set selected representations.\n\n        Representation selection changed in UI. Method triggers event with\n        topic \"selection.representations.changed\" with data:\n            {\n                \"project_name\": project_name,\n                \"folder_ids\": folder_ids,\n                \"version_ids\": version_ids,\n                \"representation_ids\": representation_ids\n            }\n\n        Args:\n            repre_ids (Iterable[str]): Selected representation ids.\n        \"\"\"\n\n        pass\n\n    # Load action items\n    @abstractmethod\n    def get_versions_action_items(self, project_name, version_ids):\n        \"\"\"Action items for versions selection.\n\n        Args:\n            project_name (str): Project name.\n            version_ids (Iterable[str]): Version ids.\n\n        Returns:\n            list[ActionItem]: List of action items.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_representations_action_items(\n        self, project_name, representation_ids\n    ):\n        \"\"\"Action items for representations selection.\n\n        Args:\n            project_name (str): Project name.\n            representation_ids (Iterable[str]): Representation ids.\n\n        Returns:\n            list[ActionItem]: List of action items.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def trigger_action_item(\n        self,\n        identifier,\n        options,\n        project_name,\n        version_ids,\n        representation_ids\n    ):\n        \"\"\"Trigger action item.\n\n        Triggers event \"load.started\" with data:\n            {\n                \"identifier\": identifier,\n                \"id\": <Random UUID>,\n            }\n\n        And triggers \"load.finished\" with data:\n            {\n                \"identifier\": identifier,\n                \"id\": <Random UUID>,\n                \"error_info\": [...],\n            }\n\n        Args:\n            identifier (str): Action identifier.\n            options (dict[str, Any]): Action option values from UI.\n            project_name (str): Project name.\n            version_ids (Iterable[str]): Version ids.\n            representation_ids (Iterable[str]): Representation ids.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def change_products_group(self, project_name, product_ids, group_name):\n        \"\"\"Change group of products.\n\n        Triggers event \"products.group.changed\" with data:\n            {\n                \"project_name\": project_name,\n                \"folder_ids\": folder_ids,\n                \"product_ids\": product_ids,\n                \"group_name\": group_name,\n            }\n\n        Args:\n            project_name (str): Project name.\n            product_ids (Iterable[str]): Product ids.\n            group_name (str): New group name.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def fill_root_in_source(self, source):\n        \"\"\"Fill root in source path.\n\n        Args:\n            source (Union[str, None]): Source of a published version. Usually\n                rootless workfile path.\n        \"\"\"\n\n        pass\n\n    # NOTE: Methods 'is_loaded_products_supported' and\n    #   'is_standard_projects_filter_enabled' are both based on being in host\n    #   or not. Maybe we could implement only single method 'is_in_host'?\n    @abstractmethod\n    def is_loaded_products_supported(self):\n        \"\"\"Is capable to get information about loaded products.\n\n        Returns:\n            bool: True if it is supported.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def is_standard_projects_filter_enabled(self):\n        \"\"\"Is standard projects filter enabled.\n\n        This is used for filtering out when loader tool is used in a host. In\n        that case only current project and library projects should be shown.\n\n        Returns:\n            bool: Frontend should filter out non-library projects, except\n                current context project.\n        \"\"\"\n\n        pass\n\n    # Site sync functions\n    @abstractmethod\n    def is_site_sync_enabled(self, project_name=None):\n        \"\"\"Is site sync enabled.\n\n        Site sync addon can be enabled but can be disabled per project.\n\n        When asked for enabled state without project name, it should return\n            True if site sync addon is available and enabled.\n\n        Args:\n            project_name (Optional[str]): Project name.\n\n        Returns:\n            bool: True if site sync is enabled.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_active_site_icon_def(self, project_name):\n        \"\"\"Active site icon definition.\n\n        Args:\n            project_name (Union[str, None]): Project name.\n\n        Returns:\n            Union[dict[str, Any], None]: Icon definition or None if site sync\n                is not enabled for the project.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_remote_site_icon_def(self, project_name):\n        \"\"\"Remote site icon definition.\n\n        Args:\n            project_name (Union[str, None]): Project name.\n\n        Returns:\n            Union[dict[str, Any], None]: Icon definition or None if site sync\n                is not enabled for the project.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_version_sync_availability(self, project_name, version_ids):\n        \"\"\"Version sync availability.\n\n        Args:\n            project_name (str): Project name.\n            version_ids (Iterable[str]): Version ids.\n\n        Returns:\n            dict[str, tuple[int, int]]: Sync availability by version id.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_representations_sync_status(\n        self, project_name, representation_ids\n    ):\n        \"\"\"Representations sync status.\n\n        Args:\n            project_name (str): Project name.\n            representation_ids (Iterable[str]): Representation ids.\n\n        Returns:\n            dict[str, tuple[int, int]]: Sync status by representation id.\n        \"\"\"\n\n        pass\n"
  },
  {
    "path": "openpype/tools/ayon_loader/control.py",
    "content": "import logging\nimport uuid\n\nimport ayon_api\n\nfrom openpype.lib.events import QueuedEventSystem\nfrom openpype.pipeline import Anatomy, get_current_context\nfrom openpype.host import ILoadHost\nfrom openpype.tools.ayon_utils.models import (\n    ProjectsModel,\n    HierarchyModel,\n    NestedCacheItem,\n    CacheItem,\n    ThumbnailsModel,\n)\n\nfrom .abstract import BackendLoaderController, FrontendLoaderController\nfrom .models import (\n    SelectionModel,\n    ProductsModel,\n    LoaderActionsModel,\n    SiteSyncModel\n)\n\n\nclass ExpectedSelection:\n    def __init__(self, controller):\n        self._project_name = None\n        self._folder_id = None\n\n        self._project_selected = True\n        self._folder_selected = True\n\n        self._controller = controller\n\n    def _emit_change(self):\n        self._controller.emit_event(\n            \"expected_selection_changed\",\n            self.get_expected_selection_data(),\n        )\n\n    def set_expected_selection(self, project_name, folder_id):\n        self._project_name = project_name\n        self._folder_id = folder_id\n\n        self._project_selected = False\n        self._folder_selected = False\n        self._emit_change()\n\n    def get_expected_selection_data(self):\n        project_current = False\n        folder_current = False\n        if not self._project_selected:\n            project_current = True\n        elif not self._folder_selected:\n            folder_current = True\n        return {\n            \"project\": {\n                \"name\": self._project_name,\n                \"current\": project_current,\n                \"selected\": self._project_selected,\n            },\n            \"folder\": {\n                \"id\": self._folder_id,\n                \"current\": folder_current,\n                \"selected\": self._folder_selected,\n            },\n        }\n\n    def is_expected_project_selected(self, project_name):\n        return project_name == self._project_name and self._project_selected\n\n    def is_expected_folder_selected(self, folder_id):\n        return folder_id == self._folder_id and self._folder_selected\n\n    def expected_project_selected(self, project_name):\n        if project_name != self._project_name:\n            return False\n        self._project_selected = True\n        self._emit_change()\n        return True\n\n    def expected_folder_selected(self, folder_id):\n        if folder_id != self._folder_id:\n            return False\n        self._folder_selected = True\n        self._emit_change()\n        return True\n\n\nclass LoaderController(BackendLoaderController, FrontendLoaderController):\n    \"\"\"\n\n    Args:\n        host (Optional[AbstractHost]): Host object. Defaults to None.\n    \"\"\"\n\n    def __init__(self, host=None):\n        self._log = None\n        self._host = host\n\n        self._event_system = self._create_event_system()\n\n        self._project_anatomy_cache = NestedCacheItem(\n            levels=1, lifetime=60)\n        self._loaded_products_cache = CacheItem(\n            default_factory=set, lifetime=60)\n\n        self._selection_model = SelectionModel(self)\n        self._expected_selection = ExpectedSelection(self)\n        self._projects_model = ProjectsModel(self)\n        self._hierarchy_model = HierarchyModel(self)\n        self._products_model = ProductsModel(self)\n        self._loader_actions_model = LoaderActionsModel(self)\n        self._thumbnails_model = ThumbnailsModel()\n        self._site_sync_model = SiteSyncModel(self)\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = logging.getLogger(self.__class__.__name__)\n        return self._log\n\n    # ---------------------------------\n    # Implementation of abstract methods\n    # ---------------------------------\n    # Events system\n    def emit_event(self, topic, data=None, source=None):\n        \"\"\"Use implemented event system to trigger event.\"\"\"\n\n        if data is None:\n            data = {}\n        self._event_system.emit(topic, data, source)\n\n    def register_event_callback(self, topic, callback):\n        self._event_system.add_callback(topic, callback)\n\n    def reset(self):\n        self._emit_event(\"controller.reset.started\")\n\n        project_name = self.get_selected_project_name()\n        folder_ids = self.get_selected_folder_ids()\n\n        self._project_anatomy_cache.reset()\n        self._loaded_products_cache.reset()\n\n        self._products_model.reset()\n        self._hierarchy_model.reset()\n        self._loader_actions_model.reset()\n        self._projects_model.reset()\n        self._thumbnails_model.reset()\n        self._site_sync_model.reset()\n\n        self._projects_model.refresh()\n\n        if not project_name and not folder_ids:\n            context = self.get_current_context()\n            project_name = context[\"project_name\"]\n            folder_id = context[\"folder_id\"]\n            self.set_expected_selection(project_name, folder_id)\n\n        self._emit_event(\"controller.reset.finished\")\n\n    # Expected selection helpers\n    def get_expected_selection_data(self):\n        return self._expected_selection.get_expected_selection_data()\n\n    def set_expected_selection(self, project_name, folder_id):\n        self._expected_selection.set_expected_selection(\n            project_name, folder_id\n        )\n\n    def expected_project_selected(self, project_name):\n        self._expected_selection.expected_project_selected(project_name)\n\n    def expected_folder_selected(self, folder_id):\n        self._expected_selection.expected_folder_selected(folder_id)\n\n    # Entity model wrappers\n    def get_project_items(self, sender=None):\n        return self._projects_model.get_project_items(sender)\n\n    def get_folder_items(self, project_name, sender=None):\n        return self._hierarchy_model.get_folder_items(project_name, sender)\n\n    def get_product_items(self, project_name, folder_ids, sender=None):\n        return self._products_model.get_product_items(\n            project_name, folder_ids, sender)\n\n    def get_product_item(self, project_name, product_id):\n        return self._products_model.get_product_item(\n            project_name, product_id\n        )\n\n    def get_product_type_items(self, project_name):\n        return self._products_model.get_product_type_items(project_name)\n\n    def get_representation_items(\n        self, project_name, version_ids, sender=None\n    ):\n        return self._products_model.get_repre_items(\n            project_name, version_ids, sender\n        )\n\n    def get_versions_representation_count(\n        self, project_name, version_ids, sender=None\n    ):\n        return self._products_model.get_versions_repre_count(\n            project_name, version_ids, sender\n        )\n\n    def get_folder_thumbnail_ids(self, project_name, folder_ids):\n        return self._thumbnails_model.get_folder_thumbnail_ids(\n            project_name, folder_ids\n        )\n\n    def get_version_thumbnail_ids(self, project_name, version_ids):\n        return self._thumbnails_model.get_version_thumbnail_ids(\n            project_name, version_ids\n        )\n\n    def get_thumbnail_path(self, project_name, thumbnail_id):\n        return self._thumbnails_model.get_thumbnail_path(\n            project_name, thumbnail_id\n        )\n\n    def change_products_group(self, project_name, product_ids, group_name):\n        self._products_model.change_products_group(\n            project_name, product_ids, group_name\n        )\n\n    def get_versions_action_items(self, project_name, version_ids):\n        return self._loader_actions_model.get_versions_action_items(\n            project_name, version_ids)\n\n    def get_representations_action_items(\n            self, project_name, representation_ids):\n        action_items = (\n            self._loader_actions_model.get_representations_action_items(\n                project_name, representation_ids)\n        )\n\n        action_items.extend(self._site_sync_model.get_site_sync_action_items(\n            project_name, representation_ids)\n        )\n\n        return action_items\n\n    def trigger_action_item(\n        self,\n        identifier,\n        options,\n        project_name,\n        version_ids,\n        representation_ids\n    ):\n        if self._site_sync_model.is_site_sync_action(identifier):\n            self._site_sync_model.trigger_action_item(\n                identifier,\n                project_name,\n                representation_ids\n            )\n            return\n\n        self._loader_actions_model.trigger_action_item(\n            identifier,\n            options,\n            project_name,\n            version_ids,\n            representation_ids\n        )\n\n    # Selection model wrappers\n    def get_selected_project_name(self):\n        return self._selection_model.get_selected_project_name()\n\n    def set_selected_project(self, project_name):\n        self._selection_model.set_selected_project(project_name)\n\n    # Selection model wrappers\n    def get_selected_folder_ids(self):\n        return self._selection_model.get_selected_folder_ids()\n\n    def set_selected_folders(self, folder_ids):\n        self._selection_model.set_selected_folders(folder_ids)\n\n    def get_selected_version_ids(self):\n        return self._selection_model.get_selected_version_ids()\n\n    def set_selected_versions(self, version_ids):\n        self._selection_model.set_selected_versions(version_ids)\n\n    def get_selected_representation_ids(self):\n        return self._selection_model.get_selected_representation_ids()\n\n    def set_selected_representations(self, repre_ids):\n        self._selection_model.set_selected_representations(repre_ids)\n\n    def fill_root_in_source(self, source):\n        project_name = self.get_selected_project_name()\n        anatomy = self._get_project_anatomy(project_name)\n        if anatomy is None:\n            return source\n\n        try:\n            return anatomy.fill_root(source)\n        except Exception:\n            return source\n\n    def get_current_context(self):\n        if self._host is None:\n            return {\n                \"project_name\": None,\n                \"folder_id\": None,\n                \"task_name\": None,\n            }\n        if hasattr(self._host, \"get_current_context\"):\n            context = self._host.get_current_context()\n        else:\n            context = get_current_context()\n        folder_id = None\n        project_name = context.get(\"project_name\")\n        asset_name = context.get(\"asset_name\")\n        if project_name and asset_name:\n            folder = ayon_api.get_folder_by_path(\n                project_name, asset_name, fields=[\"id\"]\n            )\n            if folder:\n                folder_id = folder[\"id\"]\n        return {\n            \"project_name\": project_name,\n            \"folder_id\": folder_id,\n            \"task_name\": context.get(\"task_name\"),\n        }\n\n    def get_loaded_product_ids(self):\n        if self._host is None:\n            return set()\n\n        context = self.get_current_context()\n        project_name = context[\"project_name\"]\n        if not project_name:\n            return set()\n\n        if not self._loaded_products_cache.is_valid:\n            if isinstance(self._host, ILoadHost):\n                containers = self._host.get_containers()\n            else:\n                containers = self._host.ls()\n            repre_ids = set()\n            for container in containers:\n                repre_id = container.get(\"representation\")\n                # Ignore invalid representation ids.\n                # - invalid representation ids may be available if e.g. is\n                #   opened scene from OpenPype whe 'ObjectId' was used instead\n                #   of 'uuid'.\n                # NOTE: Server call would crash if there is any invalid id.\n                #   That would cause crash we won't get any information.\n                try:\n                    uuid.UUID(repre_id)\n                    repre_ids.add(repre_id)\n                except ValueError:\n                    pass\n\n            product_ids = self._products_model.get_product_ids_by_repre_ids(\n                project_name, repre_ids\n            )\n            self._loaded_products_cache.update_data(product_ids)\n        return self._loaded_products_cache.get_data()\n\n    def is_site_sync_enabled(self, project_name=None):\n        return self._site_sync_model.is_site_sync_enabled(project_name)\n\n    def get_active_site_icon_def(self, project_name):\n        return self._site_sync_model.get_active_site_icon_def(project_name)\n\n    def get_remote_site_icon_def(self, project_name):\n        return self._site_sync_model.get_remote_site_icon_def(project_name)\n\n    def get_version_sync_availability(self, project_name, version_ids):\n        return self._site_sync_model.get_version_sync_availability(\n            project_name, version_ids\n        )\n\n    def get_representations_sync_status(\n        self, project_name, representation_ids\n    ):\n        return self._site_sync_model.get_representations_sync_status(\n            project_name, representation_ids\n        )\n\n    def is_loaded_products_supported(self):\n        return self._host is not None\n\n    def is_standard_projects_filter_enabled(self):\n        return self._host is not None\n\n    def _get_project_anatomy(self, project_name):\n        if not project_name:\n            return None\n        cache = self._project_anatomy_cache[project_name]\n        if not cache.is_valid:\n            cache.update_data(Anatomy(project_name))\n        return cache.get_data()\n\n    def _create_event_system(self):\n        return QueuedEventSystem()\n\n    def _emit_event(self, topic, data=None):\n        self._event_system.emit(topic, data or {}, \"controller\")\n"
  },
  {
    "path": "openpype/tools/ayon_loader/models/__init__.py",
    "content": "from .selection import SelectionModel\nfrom .products import ProductsModel\nfrom .actions import LoaderActionsModel\nfrom .site_sync import SiteSyncModel\n\n\n__all__ = (\n    \"SelectionModel\",\n    \"ProductsModel\",\n    \"LoaderActionsModel\",\n    \"SiteSyncModel\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_loader/models/actions.py",
    "content": "import sys\nimport traceback\nimport inspect\nimport copy\nimport collections\nimport uuid\n\nfrom openpype.client import (\n    get_project,\n    get_assets,\n    get_subsets,\n    get_versions,\n    get_representations,\n)\nfrom openpype.pipeline.load import (\n    discover_loader_plugins,\n    SubsetLoaderPlugin,\n    filter_repre_contexts_by_loader,\n    get_loader_identifier,\n    load_with_repre_context,\n    load_with_subset_context,\n    load_with_subset_contexts,\n    LoadError,\n    IncompatibleLoaderError,\n)\nfrom openpype.tools.ayon_utils.models import NestedCacheItem\nfrom openpype.tools.ayon_loader.abstract import ActionItem\n\nACTIONS_MODEL_SENDER = \"actions.model\"\nNOT_SET = object()\n\n\nclass LoaderActionsModel:\n    \"\"\"Model for loader actions.\n\n    This is probably only part of models that requires to use codebase from\n    'openpype.client' because of backwards compatibility with loaders logic\n    which are expecting mongo documents.\n\n    TODOs:\n        Deprecate 'qargparse' usage in loaders and implement conversion\n            of 'ActionItem' to data (and 'from_data').\n        Use controller to get entities (documents) -> possible only when\n            loaders are able to handle AYON vs. OpenPype logic.\n        Add missing site sync logic, and if possible remove it from loaders.\n        Implement loader actions to replace load plugins.\n        Ask loader actions to return action items instead of guessing them.\n    \"\"\"\n\n    # Cache loader plugins for some time\n    # NOTE Set to '0' for development\n    loaders_cache_lifetime = 30\n\n    def __init__(self, controller):\n        self._controller = controller\n        self._current_context_project = NOT_SET\n        self._loaders_by_identifier = NestedCacheItem(\n            levels=1, lifetime=self.loaders_cache_lifetime)\n        self._product_loaders = NestedCacheItem(\n            levels=1, lifetime=self.loaders_cache_lifetime)\n        self._repre_loaders = NestedCacheItem(\n            levels=1, lifetime=self.loaders_cache_lifetime)\n\n    def reset(self):\n        \"\"\"Reset the model with all cached items.\"\"\"\n\n        self._current_context_project = NOT_SET\n        self._loaders_by_identifier.reset()\n        self._product_loaders.reset()\n        self._repre_loaders.reset()\n\n    def get_versions_action_items(self, project_name, version_ids):\n        \"\"\"Get action items for given version ids.\n\n        Args:\n            project_name (str): Project name.\n            version_ids (Iterable[str]): Version ids.\n\n        Returns:\n            list[ActionItem]: List of action items.\n        \"\"\"\n\n        (\n            version_context_by_id,\n            repre_context_by_id\n        ) = self._contexts_for_versions(\n            project_name,\n            version_ids\n        )\n        return self._get_action_items_for_contexts(\n            project_name,\n            version_context_by_id,\n            repre_context_by_id\n        )\n\n    def get_representations_action_items(\n        self, project_name, representation_ids\n    ):\n        \"\"\"Get action items for given representation ids.\n\n        Args:\n            project_name (str): Project name.\n            representation_ids (Iterable[str]): Representation ids.\n\n        Returns:\n            list[ActionItem]: List of action items.\n        \"\"\"\n\n        (\n            product_context_by_id,\n            repre_context_by_id\n        ) = self._contexts_for_representations(\n            project_name,\n            representation_ids\n        )\n        return self._get_action_items_for_contexts(\n            project_name,\n            product_context_by_id,\n            repre_context_by_id\n        )\n\n    def trigger_action_item(\n        self,\n        identifier,\n        options,\n        project_name,\n        version_ids,\n        representation_ids\n    ):\n        \"\"\"Trigger action by identifier.\n\n        Triggers the action by identifier for given contexts.\n\n        Triggers events \"load.started\" and \"load.finished\". Finished event\n            also contains \"error_info\" key with error information if any\n            happened.\n\n        Args:\n            identifier (str): Loader identifier.\n            options (dict[str, Any]): Loader option values.\n            project_name (str): Project name.\n            version_ids (Iterable[str]): Version ids.\n            representation_ids (Iterable[str]): Representation ids.\n        \"\"\"\n\n        event_data = {\n            \"identifier\": identifier,\n            \"id\": uuid.uuid4().hex,\n        }\n        self._controller.emit_event(\n            \"load.started\",\n            event_data,\n            ACTIONS_MODEL_SENDER,\n        )\n        loader = self._get_loader_by_identifier(project_name, identifier)\n        if representation_ids is not None:\n            error_info = self._trigger_representation_loader(\n                loader,\n                options,\n                project_name,\n                representation_ids,\n            )\n        elif version_ids is not None:\n            error_info = self._trigger_version_loader(\n                loader,\n                options,\n                project_name,\n                version_ids,\n            )\n        else:\n            raise NotImplementedError(\n                \"Invalid arguments to trigger action item\")\n\n        event_data[\"error_info\"] = error_info\n        self._controller.emit_event(\n            \"load.finished\",\n            event_data,\n            ACTIONS_MODEL_SENDER,\n        )\n\n    def _get_current_context_project(self):\n        \"\"\"Get current context project name.\n\n        The value is based on controller (host) and cached.\n\n        Returns:\n            Union[str, None]: Current context project.\n        \"\"\"\n\n        if self._current_context_project is NOT_SET:\n            context = self._controller.get_current_context()\n            self._current_context_project = context[\"project_name\"]\n        return self._current_context_project\n\n    def _get_action_label(self, loader, representation=None):\n        \"\"\"Pull label info from loader class.\n\n        Args:\n            loader (LoaderPlugin): Plugin class.\n            representation (Optional[dict[str, Any]]): Representation data.\n\n        Returns:\n            str: Action label.\n        \"\"\"\n\n        label = getattr(loader, \"label\", None)\n        if label is None:\n            label = loader.__name__\n        if representation:\n            # Add the representation as suffix\n            label = \"{} ({})\".format(label, representation[\"name\"])\n        return label\n\n    def _get_action_icon(self, loader):\n        \"\"\"Pull icon info from loader class.\n\n        Args:\n            loader (LoaderPlugin): Plugin class.\n\n        Returns:\n            Union[dict[str, Any], None]: Icon definition based on\n                loader plugin.\n        \"\"\"\n\n        # Support font-awesome icons using the `.icon` and `.color`\n        # attributes on plug-ins.\n        icon = getattr(loader, \"icon\", None)\n        if icon is not None and not isinstance(icon, dict):\n            icon = {\n                \"type\": \"awesome-font\",\n                \"name\": icon,\n                \"color\": getattr(loader, \"color\", None) or \"white\"\n            }\n        return icon\n\n    def _get_action_tooltip(self, loader):\n        \"\"\"Pull tooltip info from loader class.\n\n        Args:\n            loader (LoaderPlugin): Plugin class.\n\n        Returns:\n            str: Action tooltip.\n        \"\"\"\n\n        # Add tooltip and statustip from Loader docstring\n        return inspect.getdoc(loader)\n\n    def _filter_loaders_by_tool_name(self, project_name, loaders):\n        \"\"\"Filter loaders by tool name.\n\n        Tool names are based on OpenPype tools loader tool and library\n        loader tool. The new tool merged both into one tool and the difference\n        is based only on current project name.\n\n        Args:\n            project_name (str): Project name.\n            loaders (list[LoaderPlugin]): List of loader plugins.\n\n        Returns:\n            list[LoaderPlugin]: Filtered list of loader plugins.\n        \"\"\"\n\n        # Keep filtering by tool name\n        # - if current context project name is same as project name we do\n        #   expect the tool is used as OpenPype loader tool, otherwise\n        #   as library loader tool.\n        if project_name == self._get_current_context_project():\n            tool_name = \"loader\"\n        else:\n            tool_name = \"library_loader\"\n        filtered_loaders = []\n        for loader in loaders:\n            tool_names = getattr(loader, \"tool_names\", None)\n            if (\n                tool_names is None\n                or \"*\" in tool_names\n                or tool_name in tool_names\n            ):\n                filtered_loaders.append(loader)\n        return filtered_loaders\n\n    def _create_loader_action_item(\n        self,\n        loader,\n        contexts,\n        project_name,\n        folder_ids=None,\n        product_ids=None,\n        version_ids=None,\n        representation_ids=None,\n        repre_name=None,\n    ):\n        label = self._get_action_label(loader)\n        if repre_name:\n            label = \"{} ({})\".format(label, repre_name)\n        return ActionItem(\n            get_loader_identifier(loader),\n            label=label,\n            icon=self._get_action_icon(loader),\n            tooltip=self._get_action_tooltip(loader),\n            options=loader.get_options(contexts),\n            order=loader.order,\n            project_name=project_name,\n            folder_ids=folder_ids,\n            product_ids=product_ids,\n            version_ids=version_ids,\n            representation_ids=representation_ids,\n        )\n\n    def _get_loaders(self, project_name):\n        \"\"\"Loaders with loaded settings for a project.\n\n        Questions:\n            Project name is required because of settings. Should we actually\n                pass in current project name instead of project name where\n                we want to show loaders for?\n\n        Returns:\n            tuple[list[SubsetLoaderPlugin], list[LoaderPlugin]]: Discovered\n                loader plugins.\n        \"\"\"\n\n        loaders_by_identifier_c = self._loaders_by_identifier[project_name]\n        product_loaders_c = self._product_loaders[project_name]\n        repre_loaders_c = self._repre_loaders[project_name]\n        if loaders_by_identifier_c.is_valid:\n            return product_loaders_c.get_data(), repre_loaders_c.get_data()\n\n        # Get all representation->loader combinations available for the\n        # index under the cursor, so we can list the user the options.\n        available_loaders = self._filter_loaders_by_tool_name(\n            project_name, discover_loader_plugins(project_name)\n        )\n\n        repre_loaders = []\n        product_loaders = []\n        loaders_by_identifier = {}\n        for loader_cls in available_loaders:\n            if not loader_cls.enabled:\n                continue\n\n            identifier = get_loader_identifier(loader_cls)\n            loaders_by_identifier[identifier] = loader_cls\n            if issubclass(loader_cls, SubsetLoaderPlugin):\n                product_loaders.append(loader_cls)\n            else:\n                repre_loaders.append(loader_cls)\n\n        loaders_by_identifier_c.update_data(loaders_by_identifier)\n        product_loaders_c.update_data(product_loaders)\n        repre_loaders_c.update_data(repre_loaders)\n        return product_loaders, repre_loaders\n\n    def _get_loader_by_identifier(self, project_name, identifier):\n        if not self._loaders_by_identifier[project_name].is_valid:\n            self._get_loaders(project_name)\n        loaders_by_identifier_c = self._loaders_by_identifier[project_name]\n        loaders_by_identifier = loaders_by_identifier_c.get_data()\n        return loaders_by_identifier.get(identifier)\n\n    def _actions_sorter(self, action_item):\n        \"\"\"Sort the Loaders by their order and then their name.\n\n        Returns:\n            tuple[int, str]: Sort keys.\n        \"\"\"\n\n        return action_item.order, action_item.label\n\n    def _get_version_docs(self, project_name, version_ids):\n        \"\"\"Get version documents for given version ids.\n\n        This function also handles hero versions and copies data from\n        source version to it.\n\n        Todos:\n            Remove this function when this is completely rewritten to\n                use AYON calls.\n        \"\"\"\n\n        version_docs = list(get_versions(\n            project_name, version_ids=version_ids, hero=True\n        ))\n        hero_versions_by_src_id = collections.defaultdict(list)\n        src_hero_version = set()\n        for version_doc in version_docs:\n            if version_doc[\"type\"] != \"hero\":\n                continue\n            version_id = \"\"\n            src_hero_version.add(version_id)\n            hero_versions_by_src_id[version_id].append(version_doc)\n\n        src_versions = []\n        if src_hero_version:\n            src_versions = get_versions(project_name, version_ids=version_ids)\n        for src_version in src_versions:\n            src_version_id = src_version[\"_id\"]\n            for hero_version in hero_versions_by_src_id[src_version_id]:\n                hero_version[\"data\"] = copy.deepcopy(src_version[\"data\"])\n\n        return version_docs\n\n    def _contexts_for_versions(self, project_name, version_ids):\n        \"\"\"Get contexts for given version ids.\n\n        Prepare version contexts for 'SubsetLoaderPlugin' and representation\n        contexts for 'LoaderPlugin' for all children representations of\n        given versions.\n\n        This method is very similar to '_contexts_for_representations' but the\n        queries of documents are called in a different order.\n\n        Args:\n            project_name (str): Project name.\n            version_ids (Iterable[str]): Version ids.\n\n        Returns:\n            tuple[list[dict[str, Any]], list[dict[str, Any]]]: Version and\n                representation contexts.\n        \"\"\"\n\n        # TODO fix hero version\n        version_context_by_id = {}\n        repre_context_by_id = {}\n        if not project_name and not version_ids:\n            return version_context_by_id, repre_context_by_id\n\n        version_docs = self._get_version_docs(project_name, version_ids)\n        version_docs_by_id = {}\n        version_docs_by_product_id = collections.defaultdict(list)\n        for version_doc in version_docs:\n            version_id = version_doc[\"_id\"]\n            product_id = version_doc[\"parent\"]\n            version_docs_by_id[version_id] = version_doc\n            version_docs_by_product_id[product_id].append(version_doc)\n\n        _product_ids = set(version_docs_by_product_id.keys())\n        _product_docs = get_subsets(project_name, subset_ids=_product_ids)\n        product_docs_by_id = {p[\"_id\"]: p for p in _product_docs}\n\n        _folder_ids = {p[\"parent\"] for p in product_docs_by_id.values()}\n        _folder_docs = get_assets(project_name, asset_ids=_folder_ids)\n        folder_docs_by_id = {f[\"_id\"]: f for f in _folder_docs}\n\n        project_doc = get_project(project_name)\n        project_doc[\"code\"] = project_doc[\"data\"][\"code\"]\n\n        for version_doc in version_docs:\n            version_id = version_doc[\"_id\"]\n            product_id = version_doc[\"parent\"]\n            product_doc = product_docs_by_id[product_id]\n            folder_id = product_doc[\"parent\"]\n            folder_doc = folder_docs_by_id[folder_id]\n            version_context_by_id[version_id] = {\n                \"project\": project_doc,\n                \"asset\": folder_doc,\n                \"subset\": product_doc,\n                \"version\": version_doc,\n            }\n\n        repre_docs = get_representations(\n            project_name, version_ids=version_ids)\n        for repre_doc in repre_docs:\n            version_id = repre_doc[\"parent\"]\n            version_doc = version_docs_by_id[version_id]\n            product_id = version_doc[\"parent\"]\n            product_doc = product_docs_by_id[product_id]\n            folder_id = product_doc[\"parent\"]\n            folder_doc = folder_docs_by_id[folder_id]\n\n            repre_context_by_id[repre_doc[\"_id\"]] = {\n                \"project\": project_doc,\n                \"asset\": folder_doc,\n                \"subset\": product_doc,\n                \"version\": version_doc,\n                \"representation\": repre_doc,\n            }\n\n        return version_context_by_id, repre_context_by_id\n\n    def _contexts_for_representations(self, project_name, repre_ids):\n        \"\"\"Get contexts for given representation ids.\n\n        Prepare version contexts for 'SubsetLoaderPlugin' and representation\n        contexts for 'LoaderPlugin' for all children representations of\n        given versions.\n\n        This method is very similar to '_contexts_for_versions' but the\n        queries of documents are called in a different order.\n\n        Args:\n            project_name (str): Project name.\n            repre_ids (Iterable[str]): Representation ids.\n\n        Returns:\n            tuple[list[dict[str, Any]], list[dict[str, Any]]]: Version and\n                representation contexts.\n        \"\"\"\n\n        product_context_by_id = {}\n        repre_context_by_id = {}\n        if not project_name and not repre_ids:\n            return product_context_by_id, repre_context_by_id\n\n        repre_docs = list(get_representations(\n            project_name, representation_ids=repre_ids\n        ))\n        version_ids = {r[\"parent\"] for r in repre_docs}\n        version_docs = self._get_version_docs(project_name, version_ids)\n        version_docs_by_id = {\n            v[\"_id\"]: v for v in version_docs\n        }\n\n        product_ids = {v[\"parent\"] for v in version_docs_by_id.values()}\n        product_docs = get_subsets(project_name, subset_ids=product_ids)\n        product_docs_by_id = {\n            p[\"_id\"]: p for p in product_docs\n        }\n\n        folder_ids = {p[\"parent\"] for p in product_docs_by_id.values()}\n        folder_docs = get_assets(project_name, asset_ids=folder_ids)\n        folder_docs_by_id = {\n            f[\"_id\"]: f for f in folder_docs\n        }\n\n        project_doc = get_project(project_name)\n        project_doc[\"code\"] = project_doc[\"data\"][\"code\"]\n\n        for product_id, product_doc in product_docs_by_id.items():\n            folder_id = product_doc[\"parent\"]\n            folder_doc = folder_docs_by_id[folder_id]\n            product_context_by_id[product_id] = {\n                \"project\": project_doc,\n                \"asset\": folder_doc,\n                \"subset\": product_doc,\n            }\n\n        for repre_doc in repre_docs:\n            version_id = repre_doc[\"parent\"]\n            version_doc = version_docs_by_id[version_id]\n            product_id = version_doc[\"parent\"]\n            product_doc = product_docs_by_id[product_id]\n            folder_id = product_doc[\"parent\"]\n            folder_doc = folder_docs_by_id[folder_id]\n\n            repre_context_by_id[repre_doc[\"_id\"]] = {\n                \"project\": project_doc,\n                \"asset\": folder_doc,\n                \"subset\": product_doc,\n                \"version\": version_doc,\n                \"representation\": repre_doc,\n            }\n        return product_context_by_id, repre_context_by_id\n\n    def _get_action_items_for_contexts(\n        self,\n        project_name,\n        version_context_by_id,\n        repre_context_by_id\n    ):\n        \"\"\"Prepare action items based on contexts.\n\n        Actions are prepared based on discovered loader plugins and contexts.\n        The context must be valid for the loader plugin.\n\n        Args:\n            project_name (str): Project name.\n            version_context_by_id (dict[str, dict[str, Any]]): Version\n                contexts by version id.\n            repre_context_by_id (dict[str, dict[str, Any]]): Representation\n        \"\"\"\n\n        action_items = []\n        if not version_context_by_id and not repre_context_by_id:\n            return action_items\n\n        product_loaders, repre_loaders = self._get_loaders(project_name)\n\n        repre_contexts_by_name = collections.defaultdict(list)\n        for repre_context in repre_context_by_id.values():\n            repre_name = repre_context[\"representation\"][\"name\"]\n            repre_contexts_by_name[repre_name].append(repre_context)\n\n        for loader in repre_loaders:\n            # # do not allow download whole repre, select specific repre\n            # if tools_lib.is_sync_loader(loader):\n            #     continue\n\n            for repre_name, repre_contexts in repre_contexts_by_name.items():\n                filtered_repre_contexts = filter_repre_contexts_by_loader(\n                    repre_contexts, loader)\n                if not filtered_repre_contexts:\n                    continue\n\n                repre_ids = set()\n                repre_version_ids = set()\n                repre_product_ids = set()\n                repre_folder_ids = set()\n                for repre_context in filtered_repre_contexts:\n                    repre_ids.add(repre_context[\"representation\"][\"_id\"])\n                    repre_product_ids.add(repre_context[\"subset\"][\"_id\"])\n                    repre_version_ids.add(repre_context[\"version\"][\"_id\"])\n                    repre_folder_ids.add(repre_context[\"asset\"][\"_id\"])\n\n                item = self._create_loader_action_item(\n                    loader,\n                    repre_contexts,\n                    project_name=project_name,\n                    folder_ids=repre_folder_ids,\n                    product_ids=repre_product_ids,\n                    version_ids=repre_version_ids,\n                    representation_ids=repre_ids,\n                    repre_name=repre_name,\n                )\n                action_items.append(item)\n\n        # Subset Loaders.\n        version_ids = set(version_context_by_id.keys())\n        product_folder_ids = set()\n        product_ids = set()\n        for product_context in version_context_by_id.values():\n            product_ids.add(product_context[\"subset\"][\"_id\"])\n            product_folder_ids.add(product_context[\"asset\"][\"_id\"])\n\n        version_contexts = list(version_context_by_id.values())\n        for loader in product_loaders:\n            item = self._create_loader_action_item(\n                loader,\n                version_contexts,\n                project_name=project_name,\n                folder_ids=product_folder_ids,\n                product_ids=product_ids,\n                version_ids=version_ids,\n            )\n            action_items.append(item)\n\n        action_items.sort(key=self._actions_sorter)\n        return action_items\n\n    def _trigger_version_loader(\n        self,\n        loader,\n        options,\n        project_name,\n        version_ids,\n    ):\n        \"\"\"Trigger version loader.\n\n        This triggers 'load' method of 'SubsetLoaderPlugin' for given version\n        ids.\n\n        Note:\n            Even when the plugin is 'SubsetLoaderPlugin' it actually expects\n                versions and should be named 'VersionLoaderPlugin'. Because it\n                is planned to refactor load system and introduce\n                'LoaderAction' plugins it is not relevant to change it\n                anymore.\n\n        Args:\n            loader (SubsetLoaderPlugin): Loader plugin to use.\n            options (dict): Option values for loader.\n            project_name (str): Project name.\n            version_ids (Iterable[str]): Version ids.\n        \"\"\"\n\n        project_doc = get_project(project_name)\n        project_doc[\"code\"] = project_doc[\"data\"][\"code\"]\n\n        version_docs = self._get_version_docs(project_name, version_ids)\n        product_ids = {v[\"parent\"] for v in version_docs}\n        product_docs = get_subsets(project_name, subset_ids=product_ids)\n        product_docs_by_id = {f[\"_id\"]: f for f in product_docs}\n        folder_ids = {p[\"parent\"] for p in product_docs_by_id.values()}\n        folder_docs = get_assets(project_name, asset_ids=folder_ids)\n        folder_docs_by_id = {f[\"_id\"]: f for f in folder_docs}\n        product_contexts = []\n        for version_doc in version_docs:\n            product_id = version_doc[\"parent\"]\n            product_doc = product_docs_by_id[product_id]\n            folder_id = product_doc[\"parent\"]\n            folder_doc = folder_docs_by_id[folder_id]\n            product_contexts.append({\n                \"project\": project_doc,\n                \"asset\": folder_doc,\n                \"subset\": product_doc,\n                \"version\": version_doc,\n            })\n\n        return self._load_products_by_loader(\n            loader, product_contexts, options\n        )\n\n    def _trigger_representation_loader(\n        self,\n        loader,\n        options,\n        project_name,\n        representation_ids,\n    ):\n        \"\"\"Trigger representation loader.\n\n        This triggers 'load' method of 'LoaderPlugin' for given representation\n            ids. For that are prepared contexts for each representation, with\n            all parent documents.\n\n        Args:\n            loader (LoaderPlugin): Loader plugin to use.\n            options (dict): Option values for loader.\n            project_name (str): Project name.\n            representation_ids (Iterable[str]): Representation ids.\n        \"\"\"\n\n        project_doc = get_project(project_name)\n        project_doc[\"code\"] = project_doc[\"data\"][\"code\"]\n        repre_docs = list(get_representations(\n            project_name, representation_ids=representation_ids\n        ))\n        version_ids = {r[\"parent\"] for r in repre_docs}\n        version_docs = self._get_version_docs(project_name, version_ids)\n        version_docs_by_id = {v[\"_id\"]: v for v in version_docs}\n        product_ids = {v[\"parent\"] for v in version_docs_by_id.values()}\n        product_docs = get_subsets(project_name, subset_ids=product_ids)\n        product_docs_by_id = {p[\"_id\"]: p for p in product_docs}\n        folder_ids = {p[\"parent\"] for p in product_docs_by_id.values()}\n        folder_docs = get_assets(project_name, asset_ids=folder_ids)\n        folder_docs_by_id = {f[\"_id\"]: f for f in folder_docs}\n        repre_contexts = []\n        for repre_doc in repre_docs:\n            version_id = repre_doc[\"parent\"]\n            version_doc = version_docs_by_id[version_id]\n            product_id = version_doc[\"parent\"]\n            product_doc = product_docs_by_id[product_id]\n            folder_id = product_doc[\"parent\"]\n            folder_doc = folder_docs_by_id[folder_id]\n            repre_contexts.append({\n                \"project\": project_doc,\n                \"asset\": folder_doc,\n                \"subset\": product_doc,\n                \"version\": version_doc,\n                \"representation\": repre_doc,\n            })\n\n        return self._load_representations_by_loader(\n            loader, repre_contexts, options\n        )\n\n    def _load_representations_by_loader(self, loader, repre_contexts, options):\n        \"\"\"Loops through list of repre_contexts and loads them with one loader\n\n        Args:\n            loader (LoaderPlugin): Loader plugin to use.\n            repre_contexts (list[dict]): Full info about selected\n                representations, containing repre, version, subset, asset and\n                project documents.\n            options (dict): Data from options.\n        \"\"\"\n\n        error_info = []\n        for repre_context in repre_contexts:\n            version_doc = repre_context[\"version\"]\n            if version_doc[\"type\"] == \"hero_version\":\n                version_name = \"Hero\"\n            else:\n                version_name = version_doc.get(\"name\")\n            try:\n                load_with_repre_context(\n                    loader,\n                    repre_context,\n                    options=options\n                )\n\n            except IncompatibleLoaderError as exc:\n                print(exc)\n                error_info.append((\n                    \"Incompatible Loader\",\n                    None,\n                    repre_context[\"representation\"][\"name\"],\n                    repre_context[\"subset\"][\"name\"],\n                    version_name\n                ))\n\n            except Exception as exc:\n                formatted_traceback = None\n                if not isinstance(exc, LoadError):\n                    exc_type, exc_value, exc_traceback = sys.exc_info()\n                    formatted_traceback = \"\".join(traceback.format_exception(\n                        exc_type, exc_value, exc_traceback\n                    ))\n\n                error_info.append((\n                    str(exc),\n                    formatted_traceback,\n                    repre_context[\"representation\"][\"name\"],\n                    repre_context[\"subset\"][\"name\"],\n                    version_name\n                ))\n        return error_info\n\n    def _load_products_by_loader(self, loader, version_contexts, options):\n        \"\"\"Triggers load with SubsetLoader type of loaders.\n\n        Warning:\n            Plugin is named 'SubsetLoader' but version is passed to context\n                too.\n\n        Args:\n            loader (SubsetLoder): Loader used to load.\n            version_contexts (list[dict[str, Any]]): For context for each\n                version.\n            options (dict[str, Any]): Options for loader that user could fill.\n        \"\"\"\n\n        error_info = []\n        if loader.is_multiple_contexts_compatible:\n            subset_names = []\n            for context in version_contexts:\n                subset_name = context.get(\"subset\", {}).get(\"name\") or \"N/A\"\n                subset_names.append(subset_name)\n            try:\n                load_with_subset_contexts(\n                    loader,\n                    version_contexts,\n                    options=options\n                )\n\n            except Exception as exc:\n                formatted_traceback = None\n                if not isinstance(exc, LoadError):\n                    exc_type, exc_value, exc_traceback = sys.exc_info()\n                    formatted_traceback = \"\".join(traceback.format_exception(\n                        exc_type, exc_value, exc_traceback\n                    ))\n                error_info.append((\n                    str(exc),\n                    formatted_traceback,\n                    None,\n                    \", \".join(subset_names),\n                    None\n                ))\n        else:\n            for version_context in version_contexts:\n                subset_name = (\n                    version_context.get(\"subset\", {}).get(\"name\") or \"N/A\"\n                )\n                try:\n                    load_with_subset_context(\n                        loader,\n                        version_context,\n                        options=options\n                    )\n\n                except Exception as exc:\n                    formatted_traceback = None\n                    if not isinstance(exc, LoadError):\n                        exc_type, exc_value, exc_traceback = sys.exc_info()\n                        formatted_traceback = \"\".join(\n                            traceback.format_exception(\n                                exc_type, exc_value, exc_traceback\n                            )\n                        )\n\n                    error_info.append((\n                        str(exc),\n                        formatted_traceback,\n                        None,\n                        subset_name,\n                        None\n                    ))\n\n        return error_info\n"
  },
  {
    "path": "openpype/tools/ayon_loader/models/products.py",
    "content": "import collections\nimport contextlib\n\nimport arrow\nimport ayon_api\nfrom ayon_api.operations import OperationsSession\n\nfrom openpype.style import get_default_entity_icon_color\nfrom openpype.tools.ayon_utils.models import NestedCacheItem\nfrom openpype.tools.ayon_loader.abstract import (\n    ProductTypeItem,\n    ProductItem,\n    VersionItem,\n    RepreItem,\n)\n\nPRODUCTS_MODEL_SENDER = \"products.model\"\n\n\ndef version_item_from_entity(version):\n    version_attribs = version[\"attrib\"]\n    frame_start = version_attribs.get(\"frameStart\")\n    frame_end = version_attribs.get(\"frameEnd\")\n    handle_start = version_attribs.get(\"handleStart\")\n    handle_end = version_attribs.get(\"handleEnd\")\n    step = version_attribs.get(\"step\")\n    comment = version_attribs.get(\"comment\")\n    source = version_attribs.get(\"source\")\n\n    frame_range = None\n    duration = None\n    handles = None\n    if frame_start is not None and frame_end is not None:\n        # Remove superfluous zeros from numbers (3.0 -> 3) to improve\n        # readability for most frame ranges\n        frame_start = int(frame_start)\n        frame_end = int(frame_end)\n        frame_range = \"{}-{}\".format(frame_start, frame_end)\n        duration = frame_end - frame_start + 1\n\n    if handle_start is not None and handle_end is not None:\n        handles = \"{}-{}\".format(int(handle_start), int(handle_end))\n\n    # NOTE There is also 'updatedAt', should be used that instead?\n    # TODO skip conversion - converting to '%Y%m%dT%H%M%SZ' is because\n    #   'PrettyTimeDelegate' expects it\n    created_at = arrow.get(version[\"createdAt\"]).to(\"local\")\n    published_time = created_at.strftime(\"%Y%m%dT%H%M%SZ\")\n    author = version[\"author\"]\n    version_num = version[\"version\"]\n    is_hero = version_num < 0\n\n    return VersionItem(\n        version_id=version[\"id\"],\n        version=version_num,\n        is_hero=is_hero,\n        product_id=version[\"productId\"],\n        thumbnail_id=version[\"thumbnailId\"],\n        published_time=published_time,\n        author=author,\n        frame_range=frame_range,\n        duration=duration,\n        handles=handles,\n        step=step,\n        comment=comment,\n        source=source,\n    )\n\n\ndef product_item_from_entity(\n    product_entity,\n    version_entities,\n    product_type_items_by_name,\n    folder_label,\n    product_in_scene,\n):\n    product_attribs = product_entity[\"attrib\"]\n    group = product_attribs.get(\"productGroup\")\n    product_type = product_entity[\"productType\"]\n    product_type_item = product_type_items_by_name.get(product_type)\n    # NOTE This is needed for cases when products were not created on server\n    #   using api functions. In that case product type item may not be\n    #   available and we need to create a default.\n    if product_type_item is None:\n        product_type_item = create_default_product_type_item(product_type)\n        # Cache the item for future use\n        product_type_items_by_name[product_type] = product_type_item\n\n    product_type_icon = product_type_item.icon\n\n    product_icon = {\n        \"type\": \"awesome-font\",\n        \"name\": \"fa.file-o\",\n        \"color\": get_default_entity_icon_color(),\n    }\n    version_items = {\n        version_entity[\"id\"]: version_item_from_entity(version_entity)\n        for version_entity in version_entities\n    }\n\n    return ProductItem(\n        product_id=product_entity[\"id\"],\n        product_type=product_type,\n        product_name=product_entity[\"name\"],\n        product_icon=product_icon,\n        product_type_icon=product_type_icon,\n        product_in_scene=product_in_scene,\n        group_name=group,\n        folder_id=product_entity[\"folderId\"],\n        folder_label=folder_label,\n        version_items=version_items,\n    )\n\n\ndef product_type_item_from_data(product_type_data):\n    # TODO implement icon implementation\n    # icon = product_type_data[\"icon\"]\n    # color = product_type_data[\"color\"]\n    icon = {\n        \"type\": \"awesome-font\",\n        \"name\": \"fa.folder\",\n        \"color\": \"#0091B2\",\n    }\n    # TODO implement checked logic\n    return ProductTypeItem(product_type_data[\"name\"], icon, True)\n\n\ndef create_default_product_type_item(product_type):\n    icon = {\n        \"type\": \"awesome-font\",\n        \"name\": \"fa.folder\",\n        \"color\": \"#0091B2\",\n    }\n    return ProductTypeItem(product_type, icon, True)\n\n\nclass ProductsModel:\n    \"\"\"Model for products, version and representation.\n\n    All of the entities are product based. This model prepares data for UI\n    and caches it for faster access.\n\n    Note:\n        Data are not used for actions model because that would require to\n            break OpenPype compatibility of 'LoaderPlugin's.\n    \"\"\"\n\n    lifetime = 60  # In seconds (minute by default)\n\n    def __init__(self, controller):\n        self._controller = controller\n\n        # Mapping helpers\n        # NOTE - mapping must be cleaned up with cache cleanup\n        self._product_item_by_id = collections.defaultdict(dict)\n        self._version_item_by_id = collections.defaultdict(dict)\n        self._product_folder_ids_mapping = collections.defaultdict(dict)\n\n        # Cache helpers\n        self._product_type_items_cache = NestedCacheItem(\n            levels=1, default_factory=list, lifetime=self.lifetime)\n        self._product_items_cache = NestedCacheItem(\n            levels=2, default_factory=dict, lifetime=self.lifetime)\n        self._repre_items_cache = NestedCacheItem(\n            levels=2, default_factory=dict, lifetime=self.lifetime)\n\n    def reset(self):\n        \"\"\"Reset model with all cached data.\"\"\"\n\n        self._product_item_by_id.clear()\n        self._version_item_by_id.clear()\n        self._product_folder_ids_mapping.clear()\n\n        self._product_type_items_cache.reset()\n        self._product_items_cache.reset()\n        self._repre_items_cache.reset()\n\n    def get_product_type_items(self, project_name):\n        \"\"\"Product type items for project.\n\n        Args:\n            project_name (Union[str, None]): Project name.\n\n        Returns:\n            list[ProductTypeItem]: Product type items.\n        \"\"\"\n\n        if not project_name:\n            return []\n\n        cache = self._product_type_items_cache[project_name]\n        if not cache.is_valid:\n            product_types = ayon_api.get_project_product_types(project_name)\n            cache.update_data([\n                product_type_item_from_data(product_type)\n                for product_type in product_types\n            ])\n        return cache.get_data()\n\n    def get_product_items(self, project_name, folder_ids, sender):\n        \"\"\"Product items with versions for project and folder ids.\n\n        Product items also contain version items. They're directly connected\n        to product items in the UI and the separation is not needed.\n\n        Args:\n            project_name (Union[str, None]): Project name.\n            folder_ids (Iterable[str]): Folder ids.\n            sender (Union[str, None]): Who triggered the method.\n\n        Returns:\n            list[ProductItem]: Product items.\n        \"\"\"\n\n        if not project_name or not folder_ids:\n            return []\n\n        project_cache = self._product_items_cache[project_name]\n        output = []\n        folder_ids_to_update = set()\n        for folder_id in folder_ids:\n            cache = project_cache[folder_id]\n            if cache.is_valid:\n                output.extend(cache.get_data().values())\n            else:\n                folder_ids_to_update.add(folder_id)\n\n        self._refresh_product_items(\n            project_name, folder_ids_to_update, sender)\n\n        for folder_id in folder_ids_to_update:\n            cache = project_cache[folder_id]\n            output.extend(cache.get_data().values())\n        return output\n\n    def get_product_item(self, project_name, product_id):\n        \"\"\"Get product item based on passed product id.\n\n        This method is using cached items, but if cache is not valid it also\n        can query the item.\n\n        Args:\n            project_name (Union[str, None]): Where to look for product.\n            product_id (Union[str, None]): Product id to receive.\n\n        Returns:\n            Union[ProductItem, None]: Product item or 'None' if not found.\n        \"\"\"\n\n        if not any((project_name, product_id)):\n            return None\n\n        product_items_by_id = self._product_item_by_id[project_name]\n        product_item = product_items_by_id.get(product_id)\n        if product_item is not None:\n            return product_item\n        for product_item in self._query_product_items_by_ids(\n            project_name, product_ids=[product_id]\n        ).values():\n            return product_item\n\n    def get_product_ids_by_repre_ids(self, project_name, repre_ids):\n        \"\"\"Get product ids based on passed representation ids.\n\n        Args:\n            project_name (str): Where to look for representations.\n            repre_ids (Iterable[str]): Representation ids.\n\n        Returns:\n            set[str]: Product ids for passed representation ids.\n        \"\"\"\n\n        # TODO look out how to use single server call\n        if not repre_ids:\n            return set()\n        repres = ayon_api.get_representations(\n            project_name, repre_ids, fields=[\"versionId\"]\n        )\n        version_ids = {repre[\"versionId\"] for repre in repres}\n        if not version_ids:\n            return set()\n        versions = ayon_api.get_versions(\n            project_name, version_ids=version_ids, fields=[\"productId\"]\n        )\n        return {v[\"productId\"] for v in versions}\n\n    def get_repre_items(self, project_name, version_ids, sender):\n        \"\"\"Get representation items for passed version ids.\n\n        Args:\n            project_name (str): Project name.\n            version_ids (Iterable[str]): Version ids.\n            sender (Union[str, None]): Who triggered the method.\n\n        Returns:\n            list[RepreItem]: Representation items.\n        \"\"\"\n\n        output = []\n        if not any((project_name, version_ids)):\n            return output\n\n        invalid_version_ids = set()\n        project_cache = self._repre_items_cache[project_name]\n        for version_id in version_ids:\n            version_cache = project_cache[version_id]\n            if version_cache.is_valid:\n                output.extend(version_cache.get_data().values())\n            else:\n                invalid_version_ids.add(version_id)\n\n        if invalid_version_ids:\n            self.refresh_representation_items(\n                project_name, invalid_version_ids, sender\n            )\n\n        for version_id in invalid_version_ids:\n            version_cache = project_cache[version_id]\n            output.extend(version_cache.get_data().values())\n\n        return output\n\n    def get_versions_repre_count(self, project_name, version_ids, sender):\n        \"\"\"Get representation count for passed version ids.\n\n        Args:\n            project_name (str): Project name.\n            version_ids (Iterable[str]): Version ids.\n            sender (Union[str, None]): Who triggered the method.\n\n        Returns:\n            dict[str, int]: Number of representations by version id.\n        \"\"\"\n\n        output = {}\n        if not any((project_name, version_ids)):\n            return output\n\n        invalid_version_ids = set()\n        project_cache = self._repre_items_cache[project_name]\n        for version_id in version_ids:\n            version_cache = project_cache[version_id]\n            if version_cache.is_valid:\n                output[version_id] = len(version_cache.get_data())\n            else:\n                invalid_version_ids.add(version_id)\n\n        if invalid_version_ids:\n            self.refresh_representation_items(\n                project_name, invalid_version_ids, sender\n            )\n\n        for version_id in invalid_version_ids:\n            version_cache = project_cache[version_id]\n            output[version_id] = len(version_cache.get_data())\n\n        return output\n\n    def change_products_group(self, project_name, product_ids, group_name):\n        \"\"\"Change group name for passed product ids.\n\n        Group name is stored in 'attrib' of product entity and is used in UI\n        to group items.\n\n        Method triggers \"products.group.changed\" event with data:\n            {\n                \"project_name\": project_name,\n                \"folder_ids\": folder_ids,\n                \"product_ids\": product_ids,\n                \"group_name\": group_name\n            }\n\n        Args:\n            project_name (str): Project name.\n            product_ids (Iterable[str]): Product ids to change group name for.\n            group_name (str): Group name to set.\n        \"\"\"\n\n        if not product_ids:\n            return\n\n        product_items = self._get_product_items_by_id(\n            project_name, product_ids\n        )\n        if not product_items:\n            return\n\n        session = OperationsSession()\n        folder_ids = set()\n        for product_item in product_items.values():\n            session.update_entity(\n                project_name,\n                \"product\",\n                product_item.product_id,\n                {\"attrib\": {\"productGroup\": group_name}}\n            )\n            folder_ids.add(product_item.folder_id)\n            product_item.group_name = group_name\n\n        session.commit()\n        self._controller.emit_event(\n            \"products.group.changed\",\n            {\n                \"project_name\": project_name,\n                \"folder_ids\": folder_ids,\n                \"product_ids\": product_ids,\n                \"group_name\": group_name,\n            },\n            PRODUCTS_MODEL_SENDER\n        )\n\n    def _get_product_items_by_id(self, project_name, product_ids):\n        product_item_by_id = self._product_item_by_id[project_name]\n        missing_product_ids = set()\n        output = {}\n        for product_id in product_ids:\n            product_item = product_item_by_id.get(product_id)\n            if product_item is not None:\n                output[product_id] = product_item\n            else:\n                missing_product_ids.add(product_id)\n\n        output.update(\n            self._query_product_items_by_ids(\n                project_name, missing_product_ids\n            )\n        )\n        return output\n\n    def _get_version_items_by_id(self, project_name, version_ids):\n        version_item_by_id = self._version_item_by_id[project_name]\n        missing_version_ids = set()\n        output = {}\n        for version_id in version_ids:\n            version_item = version_item_by_id.get(version_id)\n            if version_item is not None:\n                output[version_id] = version_item\n            else:\n                missing_version_ids.add(version_id)\n\n        output.update(\n            self._query_version_items_by_ids(\n                project_name, missing_version_ids\n            )\n        )\n        return output\n\n    def _create_product_items(\n        self,\n        project_name,\n        products,\n        versions,\n        folder_items=None,\n        product_type_items=None,\n    ):\n        if folder_items is None:\n            folder_items = self._controller.get_folder_items(project_name)\n\n        if product_type_items is None:\n            product_type_items = self.get_product_type_items(project_name)\n\n        loaded_product_ids = self._controller.get_loaded_product_ids()\n\n        versions_by_product_id = collections.defaultdict(list)\n        for version in versions:\n            versions_by_product_id[version[\"productId\"]].append(version)\n        product_type_items_by_name = {\n            product_type_item.name: product_type_item\n            for product_type_item in product_type_items\n        }\n        output = {}\n        for product in products:\n            product_id = product[\"id\"]\n            folder_id = product[\"folderId\"]\n            folder_item = folder_items.get(folder_id)\n            if not folder_item:\n                continue\n            versions = versions_by_product_id[product_id]\n            if not versions:\n                continue\n            product_item = product_item_from_entity(\n                product,\n                versions,\n                product_type_items_by_name,\n                folder_item.label,\n                product_id in loaded_product_ids,\n            )\n            output[product_id] = product_item\n        return output\n\n    def _query_product_items_by_ids(\n        self,\n        project_name,\n        folder_ids=None,\n        product_ids=None,\n        folder_items=None\n    ):\n        \"\"\"Query product items.\n\n        This method does get from, or store to, cache attributes.\n\n        One of 'product_ids' or 'folder_ids' must be passed to the method.\n\n        Args:\n            project_name (str): Project name.\n            folder_ids (Optional[Iterable[str]]): Folder ids under which are\n                products.\n            product_ids (Optional[Iterable[str]]): Product ids to use.\n            folder_items (Optional[Dict[str, FolderItem]]): Prepared folder\n                items from controller.\n\n        Returns:\n            dict[str, ProductItem]: Product items by product id.\n        \"\"\"\n\n        if not folder_ids and not product_ids:\n            return {}\n\n        kwargs = {}\n        if folder_ids is not None:\n            kwargs[\"folder_ids\"] = folder_ids\n\n        if product_ids is not None:\n            kwargs[\"product_ids\"] = product_ids\n\n        products = list(ayon_api.get_products(project_name, **kwargs))\n        product_ids = {product[\"id\"] for product in products}\n\n        versions = ayon_api.get_versions(\n            project_name, product_ids=product_ids\n        )\n\n        return self._create_product_items(\n            project_name, products, versions, folder_items=folder_items\n        )\n\n    def _query_version_items_by_ids(self, project_name, version_ids):\n        versions = list(ayon_api.get_versions(\n            project_name, version_ids=version_ids\n        ))\n        product_ids = {version[\"productId\"] for version in versions}\n        products = list(ayon_api.get_products(\n            project_name, product_ids=product_ids\n        ))\n        product_items = self._create_product_items(\n            project_name, products, versions\n        )\n        version_items = {}\n        for product_item in product_items.values():\n            version_items.update(product_item.version_items)\n        return version_items\n\n    def _clear_product_version_items(self, project_name, folder_ids):\n        \"\"\"Clear product and version items from memory.\n\n        When products are re-queried for a folders, the old product and version\n        items in '_product_item_by_id' and '_version_item_by_id' should\n        be cleaned up from memory. And mapping in stored in\n        '_product_folder_ids_mapping' is not relevant either.\n\n        Args:\n            project_name (str): Name of project.\n            folder_ids (Iterable[str]): Folder ids which are being refreshed.\n        \"\"\"\n\n        project_mapping = self._product_folder_ids_mapping[project_name]\n        if not project_mapping:\n            return\n\n        product_item_by_id = self._product_item_by_id[project_name]\n        version_item_by_id = self._version_item_by_id[project_name]\n        for folder_id in folder_ids:\n            product_ids = project_mapping.pop(folder_id, None)\n            if not product_ids:\n                continue\n\n            for product_id in product_ids:\n                product_item = product_item_by_id.pop(product_id, None)\n                if product_item is None:\n                    continue\n                for version_item in product_item.version_items.values():\n                    version_item_by_id.pop(version_item.version_id, None)\n\n    def _refresh_product_items(self, project_name, folder_ids, sender):\n        \"\"\"Refresh product items and store them in cache.\n\n        Args:\n            project_name (str): Name of project.\n            folder_ids (Iterable[str]): Folder ids which are being refreshed.\n            sender (Union[str, None]): Who triggered the refresh.\n        \"\"\"\n\n        if not project_name or not folder_ids:\n            return\n\n        self._clear_product_version_items(project_name, folder_ids)\n\n        project_mapping = self._product_folder_ids_mapping[project_name]\n        product_item_by_id = self._product_item_by_id[project_name]\n        version_item_by_id = self._version_item_by_id[project_name]\n\n        for folder_id in folder_ids:\n            project_mapping[folder_id] = set()\n\n        with self._product_refresh_event_manager(\n            project_name, folder_ids, sender\n        ):\n            folder_items = self._controller.get_folder_items(project_name)\n            items_by_folder_id = {\n                folder_id: {}\n                for folder_id in folder_ids\n            }\n            product_items_by_id = self._query_product_items_by_ids(\n                project_name,\n                folder_ids=folder_ids,\n                folder_items=folder_items\n            )\n            for product_id, product_item in product_items_by_id.items():\n                folder_id = product_item.folder_id\n                items_by_folder_id[product_item.folder_id][product_id] = (\n                    product_item\n                )\n\n                project_mapping[folder_id].add(product_id)\n                product_item_by_id[product_id] = product_item\n                for version_id, version_item in (\n                    product_item.version_items.items()\n                ):\n                    version_item_by_id[version_id] = version_item\n\n            project_cache = self._product_items_cache[project_name]\n            for folder_id, product_items in items_by_folder_id.items():\n                project_cache[folder_id].update_data(product_items)\n\n    @contextlib.contextmanager\n    def _product_refresh_event_manager(\n        self, project_name, folder_ids, sender\n    ):\n        self._controller.emit_event(\n            \"products.refresh.started\",\n            {\n                \"project_name\": project_name,\n                \"folder_ids\": folder_ids,\n                \"sender\": sender,\n            },\n            PRODUCTS_MODEL_SENDER\n        )\n        try:\n            yield\n\n        finally:\n            self._controller.emit_event(\n                \"products.refresh.finished\",\n                {\n                    \"project_name\": project_name,\n                    \"folder_ids\": folder_ids,\n                    \"sender\": sender,\n                },\n                PRODUCTS_MODEL_SENDER\n            )\n\n    def refresh_representation_items(\n        self, project_name, version_ids, sender\n    ):\n        if not any((project_name, version_ids)):\n            return\n        self._controller.emit_event(\n            \"model.representations.refresh.started\",\n            {\n                \"project_name\": project_name,\n                \"version_ids\": version_ids,\n                \"sender\": sender,\n            },\n            PRODUCTS_MODEL_SENDER\n        )\n        failed = False\n        try:\n            self._refresh_representation_items(project_name, version_ids)\n        except Exception:\n            # TODO add more information about failed refresh\n            failed = True\n\n        self._controller.emit_event(\n            \"model.representations.refresh.finished\",\n            {\n                \"project_name\": project_name,\n                \"version_ids\": version_ids,\n                \"sender\": sender,\n                \"failed\": failed,\n            },\n            PRODUCTS_MODEL_SENDER\n        )\n\n    def _refresh_representation_items(self, project_name, version_ids):\n        representations = list(ayon_api.get_representations(\n            project_name,\n            version_ids=version_ids,\n            fields=[\"id\", \"name\", \"versionId\"]\n        ))\n\n        version_items_by_id = self._get_version_items_by_id(\n            project_name, version_ids\n        )\n        product_ids = {\n            version_item.product_id\n            for version_item in version_items_by_id.values()\n        }\n        product_items_by_id = self._get_product_items_by_id(\n            project_name, product_ids\n        )\n        repre_icon = {\n            \"type\": \"awesome-font\",\n            \"name\": \"fa.file-o\",\n            \"color\": get_default_entity_icon_color(),\n        }\n        repre_items_by_version_id = collections.defaultdict(dict)\n        for representation in representations:\n            version_id = representation[\"versionId\"]\n            version_item = version_items_by_id.get(version_id)\n            if version_item is None:\n                continue\n            product_item = product_items_by_id.get(version_item.product_id)\n            if product_item is None:\n                continue\n            repre_id = representation[\"id\"]\n            repre_item = RepreItem(\n                repre_id,\n                representation[\"name\"],\n                repre_icon,\n                product_item.product_name,\n                product_item.folder_label,\n            )\n            repre_items_by_version_id[version_id][repre_id] = repre_item\n\n        project_cache = self._repre_items_cache[project_name]\n        for version_id, repre_items in repre_items_by_version_id.items():\n            version_cache = project_cache[version_id]\n            version_cache.update_data(repre_items)\n"
  },
  {
    "path": "openpype/tools/ayon_loader/models/selection.py",
    "content": "class SelectionModel(object):\n    \"\"\"Model handling selection changes.\n\n    Triggering events:\n    - \"selection.project.changed\"\n    - \"selection.folders.changed\"\n    - \"selection.versions.changed\"\n    \"\"\"\n\n    event_source = \"selection.model\"\n\n    def __init__(self, controller):\n        self._controller = controller\n\n        self._project_name = None\n        self._folder_ids = set()\n        self._version_ids = set()\n        self._representation_ids = set()\n\n    def get_selected_project_name(self):\n        return self._project_name\n\n    def set_selected_project(self, project_name):\n        if self._project_name == project_name:\n            return\n\n        self._project_name = project_name\n        self._controller.emit_event(\n            \"selection.project.changed\",\n            {\"project_name\": self._project_name},\n            self.event_source\n        )\n\n    def get_selected_folder_ids(self):\n        return self._folder_ids\n\n    def set_selected_folders(self, folder_ids):\n        if folder_ids == self._folder_ids:\n            return\n\n        self._folder_ids = folder_ids\n        self._controller.emit_event(\n            \"selection.folders.changed\",\n            {\n                \"project_name\": self._project_name,\n                \"folder_ids\": folder_ids,\n            },\n            self.event_source\n        )\n\n    def get_selected_version_ids(self):\n        return self._version_ids\n\n    def set_selected_versions(self, version_ids):\n        if version_ids == self._version_ids:\n            return\n\n        self._version_ids = version_ids\n        self._controller.emit_event(\n            \"selection.versions.changed\",\n            {\n                \"project_name\": self._project_name,\n                \"folder_ids\": self._folder_ids,\n                \"version_ids\": self._version_ids,\n            },\n            self.event_source\n        )\n\n    def get_selected_representation_ids(self):\n        return self._representation_ids\n\n    def set_selected_representations(self, repre_ids):\n        if repre_ids == self._representation_ids:\n            return\n\n        self._representation_ids = repre_ids\n        self._controller.emit_event(\n            \"selection.representations.changed\",\n            {\n                \"project_name\": self._project_name,\n                \"folder_ids\": self._folder_ids,\n                \"version_ids\": self._version_ids,\n                \"representation_ids\": self._representation_ids,\n            }\n        )\n"
  },
  {
    "path": "openpype/tools/ayon_loader/models/site_sync.py",
    "content": "import collections\n\nfrom openpype.lib import Logger\nfrom openpype.client.entities import get_representations\nfrom openpype.client import get_linked_representation_id\nfrom openpype.modules import ModulesManager\nfrom openpype.tools.ayon_utils.models import NestedCacheItem\nfrom openpype.tools.ayon_loader.abstract import ActionItem\n\nDOWNLOAD_IDENTIFIER = \"sitesync.download\"\nUPLOAD_IDENTIFIER = \"sitesync.upload\"\nREMOVE_IDENTIFIER = \"sitesync.remove\"\n\nlog = Logger.get_logger(__name__)\n\n\ndef _default_version_availability():\n    return 0, 0\n\n\ndef _default_repre_status():\n    return 0.0, 0.0\n\n\nclass SiteSyncModel:\n    \"\"\"Model handling site sync logic.\n\n    Model cares about handling of site sync functionality. All public\n    functions should be possible to call even if site sync is not available.\n    \"\"\"\n\n    lifetime = 60  # In seconds (minute by default)\n    status_lifetime = 20\n\n    def __init__(self, controller):\n        self._controller = controller\n\n        self._site_icons = None\n        self._site_sync_enabled_cache = NestedCacheItem(\n            levels=1, lifetime=self.lifetime\n        )\n        self._active_site_cache = NestedCacheItem(\n            levels=1, lifetime=self.lifetime\n        )\n        self._remote_site_cache = NestedCacheItem(\n            levels=1, lifetime=self.lifetime\n        )\n        self._version_availability_cache = NestedCacheItem(\n            levels=2,\n            default_factory=_default_version_availability,\n            lifetime=self.status_lifetime\n        )\n        self._repre_status_cache = NestedCacheItem(\n            levels=2,\n            default_factory=_default_repre_status,\n            lifetime=self.status_lifetime\n        )\n\n        manager = ModulesManager()\n        self._site_sync_addon = manager.get(\"sync_server\")\n\n    def reset(self):\n        self._site_icons = None\n        self._site_sync_enabled_cache.reset()\n        self._active_site_cache.reset()\n        self._remote_site_cache.reset()\n        self._version_availability_cache.reset()\n        self._repre_status_cache.reset()\n\n    def is_site_sync_enabled(self, project_name=None):\n        \"\"\"Site sync is enabled for a project.\n\n        Returns false if site sync addon is not available or enabled\n            or project has disabled it.\n\n        Args:\n            project_name (Union[str, None]): Project name. If project name\n                is 'None', True is returned if site sync addon\n                is available and enabled.\n\n        Returns:\n            bool: Site sync is enabled.\n        \"\"\"\n\n        if not self._is_site_sync_addon_enabled():\n            return False\n        cache = self._site_sync_enabled_cache[project_name]\n        if not cache.is_valid:\n            enabled = True\n            if project_name:\n                enabled = self._site_sync_addon.is_project_enabled(\n                    project_name, single=True\n                )\n            cache.update_data(enabled)\n        return cache.get_data()\n\n    def get_active_site(self, project_name):\n        \"\"\"Active site name for a project.\n\n        Args:\n            project_name (str): Project name.\n\n        Returns:\n            Union[str, None]: Remote site name.\n        \"\"\"\n\n        cache = self._active_site_cache[project_name]\n        if not cache.is_valid:\n            site_name = None\n            if project_name and self._is_site_sync_addon_enabled():\n                site_name = self._site_sync_addon.get_active_site(project_name)\n            cache.update_data(site_name)\n        return cache.get_data()\n\n    def get_remote_site(self, project_name):\n        \"\"\"Remote site name for a project.\n\n        Args:\n            project_name (str): Project name.\n\n        Returns:\n            Union[str, None]: Remote site name.\n        \"\"\"\n\n        cache = self._remote_site_cache[project_name]\n        if not cache.is_valid:\n            site_name = None\n            if project_name and self._is_site_sync_addon_enabled():\n                site_name = self._site_sync_addon.get_remote_site(project_name)\n            cache.update_data(site_name)\n        return cache.get_data()\n\n    def get_active_site_icon_def(self, project_name):\n        \"\"\"Active site icon definition.\n\n        Args:\n            project_name (Union[str, None]): Name of project.\n\n        Returns:\n            Union[dict[str, Any], None]: Site icon definition.\n        \"\"\"\n\n        if not project_name or not self.is_site_sync_enabled(project_name):\n            return None\n        active_site = self.get_active_site(project_name)\n        return self._get_site_icon_def(project_name, active_site)\n\n    def get_remote_site_icon_def(self, project_name):\n        \"\"\"Remote site icon definition.\n\n        Args:\n            project_name (Union[str, None]): Name of project.\n\n        Returns:\n            Union[dict[str, Any], None]: Site icon definition.\n        \"\"\"\n\n        if not project_name or not self.is_site_sync_enabled(project_name):\n            return None\n        remote_site = self.get_remote_site(project_name)\n        return self._get_site_icon_def(project_name, remote_site)\n\n    def _get_site_icon_def(self, project_name, site_name):\n        # use different icon for studio even if provider is 'local_drive'\n        if site_name == self._site_sync_addon.DEFAULT_SITE:\n            provider = \"studio\"\n        else:\n            provider = self._get_provider_for_site(project_name, site_name)\n        return self._get_provider_icon(provider)\n\n    def get_version_sync_availability(self, project_name, version_ids):\n        \"\"\"Returns how many representations are available on sites.\n\n        Returned value `{version_id: (4, 6)}` denotes that locally are\n            available 4 and remotely 6 representation.\n        NOTE: Available means they were synced to site.\n\n        Returns:\n            dict[str, tuple[int, int]]\n        \"\"\"\n\n        if not self.is_site_sync_enabled(project_name):\n            return {\n                version_id: _default_version_availability()\n                for version_id in version_ids\n            }\n\n        output = {}\n        project_cache = self._version_availability_cache[project_name]\n        invalid_ids = set()\n        for version_id in version_ids:\n            repre_cache = project_cache[version_id]\n            if repre_cache.is_valid:\n                output[version_id] = repre_cache.get_data()\n            else:\n                invalid_ids.add(version_id)\n\n        if invalid_ids:\n            self._refresh_version_availability(\n                project_name, invalid_ids\n            )\n            for version_id in invalid_ids:\n                version_cache = project_cache[version_id]\n                output[version_id] = version_cache.get_data()\n        return output\n\n    def get_representations_sync_status(\n        self, project_name, representation_ids\n    ):\n        \"\"\"\n\n        Args:\n            project_name (str): Project name.\n            representation_ids (Iterable[str]): Representation ids.\n\n        Returns:\n            dict[str, tuple[float, float]]\n        \"\"\"\n\n        if not self.is_site_sync_enabled(project_name):\n            return {\n                repre_id: _default_repre_status()\n                for repre_id in representation_ids\n            }\n\n        output = {}\n        project_cache = self._repre_status_cache[project_name]\n        invalid_ids = set()\n        for repre_id in representation_ids:\n            repre_cache = project_cache[repre_id]\n            if repre_cache.is_valid:\n                output[repre_id] = repre_cache.get_data()\n            else:\n                invalid_ids.add(repre_id)\n\n        if invalid_ids:\n            self._refresh_representations_sync_status(\n                project_name, invalid_ids\n            )\n            for repre_id in invalid_ids:\n                repre_cache = project_cache[repre_id]\n                output[repre_id] = repre_cache.get_data()\n        return output\n\n    def get_site_sync_action_items(self, project_name, representation_ids):\n        \"\"\"\n\n        Args:\n             project_name (str): Project name.\n             representation_ids (Iterable[str]): Representation ids.\n\n        Returns:\n            list[ActionItem]: Actions that can be shown in loader.\n        \"\"\"\n\n        if not self.is_site_sync_enabled(project_name):\n            return []\n\n        repres_status = self.get_representations_sync_status(\n            project_name, representation_ids\n        )\n\n        repre_ids_per_identifier = collections.defaultdict(set)\n        for repre_id in representation_ids:\n            repre_status = repres_status[repre_id]\n            local_status, remote_status = repre_status\n\n            if local_status:\n                repre_ids_per_identifier[UPLOAD_IDENTIFIER].add(repre_id)\n                repre_ids_per_identifier[REMOVE_IDENTIFIER].add(repre_id)\n\n            if remote_status:\n                repre_ids_per_identifier[DOWNLOAD_IDENTIFIER].add(repre_id)\n\n        action_items = []\n        for identifier, repre_ids in repre_ids_per_identifier.items():\n            if identifier == DOWNLOAD_IDENTIFIER:\n                action_items.append(self._create_download_action_item(\n                    project_name, repre_ids\n                ))\n            elif identifier == UPLOAD_IDENTIFIER:\n                action_items.append(self._create_upload_action_item(\n                    project_name, repre_ids\n                ))\n            elif identifier == REMOVE_IDENTIFIER:\n                action_items.append(self._create_delete_action_item(\n                    project_name, repre_ids\n                ))\n\n        return action_items\n\n    def is_site_sync_action(self, identifier):\n        \"\"\"Should be `identifier` handled by SiteSync.\n\n        Args:\n            identifier (str): Action identifier.\n\n        Returns:\n            bool: Should action be handled by SiteSync.\n        \"\"\"\n\n        return identifier in {\n            UPLOAD_IDENTIFIER,\n            DOWNLOAD_IDENTIFIER,\n            REMOVE_IDENTIFIER,\n        }\n\n    def trigger_action_item(\n        self,\n        identifier,\n        project_name,\n        representation_ids\n    ):\n        \"\"\"Resets status for site_name or remove local files.\n\n        Args:\n            identifier (str): Action identifier.\n            project_name (str): Project name.\n            representation_ids (Iterable[str]): Representation ids.\n        \"\"\"\n\n        active_site = self.get_active_site(project_name)\n        remote_site = self.get_remote_site(project_name)\n\n        repre_docs = list(get_representations(\n            project_name, representation_ids=representation_ids\n        ))\n        families_per_repre_id = {\n            item[\"_id\"]: item[\"context\"][\"family\"]\n            for item in repre_docs\n        }\n\n        for repre_id in representation_ids:\n            family = families_per_repre_id[repre_id]\n            if identifier == DOWNLOAD_IDENTIFIER:\n                self._add_site(\n                    project_name, repre_id, active_site, family\n                )\n\n            elif identifier == UPLOAD_IDENTIFIER:\n                self._add_site(\n                    project_name, repre_id, remote_site, family\n                )\n\n            elif identifier == REMOVE_IDENTIFIER:\n                self._site_sync_addon.remove_site(\n                    project_name,\n                    repre_id,\n                    active_site,\n                    remove_local_files=True\n                )\n\n    def _is_site_sync_addon_enabled(self):\n        \"\"\"\n        Returns:\n            bool: Site sync addon is enabled.\n        \"\"\"\n\n        if self._site_sync_addon is None:\n            return False\n        return self._site_sync_addon.enabled\n\n    def _get_provider_for_site(self, project_name, site_name):\n        \"\"\"Provider for a site.\n\n        Args:\n            project_name (str): Project name.\n            site_name (str): Site name.\n\n        Returns:\n            Union[str, None]: Provider name.\n        \"\"\"\n\n        if not self._is_site_sync_addon_enabled():\n            return None\n        return self._site_sync_addon.get_provider_for_site(\n            project_name, site_name\n        )\n\n    def _get_provider_icon(self, provider):\n        \"\"\"site provider icons.\n\n        Returns:\n            Union[dict[str, Any], None]: Icon of site provider.\n        \"\"\"\n\n        if not provider:\n            return None\n\n        if self._site_icons is None:\n            self._site_icons = self._site_sync_addon.get_site_icons()\n        return self._site_icons.get(provider)\n\n    def _refresh_version_availability(self, project_name, version_ids):\n        if not project_name or not version_ids:\n            return\n        project_cache = self._version_availability_cache[project_name]\n\n        avail_by_id = self._site_sync_addon.get_version_availability(\n            project_name,\n            version_ids,\n            self.get_active_site(project_name),\n            self.get_remote_site(project_name),\n        )\n        for version_id in version_ids:\n            status = avail_by_id.get(version_id)\n            if status is None:\n                status = _default_version_availability()\n            project_cache[version_id].update_data(status)\n\n    def _refresh_representations_sync_status(\n        self, project_name, representation_ids\n    ):\n        if not project_name or not representation_ids:\n            return\n        project_cache = self._repre_status_cache[project_name]\n        status_by_repre_id = (\n            self._site_sync_addon.get_representations_sync_state(\n                project_name,\n                representation_ids,\n                self.get_active_site(project_name),\n                self.get_remote_site(project_name),\n            )\n        )\n        for repre_id in representation_ids:\n            status = status_by_repre_id.get(repre_id)\n            if status is None:\n                status = _default_repre_status()\n            project_cache[repre_id].update_data(status)\n\n    def _create_download_action_item(self, project_name, representation_ids):\n        return self._create_action_item(\n            project_name,\n            representation_ids,\n            DOWNLOAD_IDENTIFIER,\n            \"Download\",\n            \"Mark representation for download locally\",\n            \"fa.download\"\n        )\n\n    def _create_upload_action_item(self, project_name, representation_ids):\n        return self._create_action_item(\n            project_name,\n            representation_ids,\n            UPLOAD_IDENTIFIER,\n            \"Upload\",\n            \"Mark representation for upload remotely\",\n            \"fa.upload\"\n        )\n\n    def _create_delete_action_item(self, project_name, representation_ids):\n        return self._create_action_item(\n            project_name,\n            representation_ids,\n            REMOVE_IDENTIFIER,\n            \"Remove from local\",\n            \"Remove local synchronization\",\n            \"fa.trash\"\n        )\n\n    def _create_action_item(\n        self,\n        project_name,\n        representation_ids,\n        identifier,\n        label,\n        tooltip,\n        icon_name\n    ):\n        return ActionItem(\n            identifier,\n            label,\n            icon={\n                \"type\": \"awesome-font\",\n                \"name\": icon_name,\n                \"color\": \"#999999\"\n            },\n            tooltip=tooltip,\n            options={},\n            order=1,\n            project_name=project_name,\n            folder_ids=[],\n            product_ids=[],\n            version_ids=[],\n            representation_ids=representation_ids,\n        )\n\n    def _add_site(self, project_name, repre_id, site_name, family):\n        self._site_sync_addon.add_site(\n            project_name, repre_id, site_name, force=True\n        )\n\n        # TODO this should happen in site sync addon\n        if family != \"workfile\":\n            return\n\n        links = get_linked_representation_id(\n            project_name,\n            repre_id=repre_id,\n            link_type=\"reference\"\n        )\n        for link_repre_id in links:\n            try:\n                print(\"Adding {} to linked representation: {}\".format(\n                    site_name, link_repre_id))\n                self._site_sync_addon.add_site(\n                    project_name,\n                    link_repre_id,\n                    site_name,\n                    force=False\n                )\n            except Exception:\n                # do not add/reset working site for references\n                log.debug(\"Site present\", exc_info=True)\n"
  },
  {
    "path": "openpype/tools/ayon_loader/ui/__init__.py",
    "content": "from .window import LoaderWindow\n\n\n__all__ = (\n    \"LoaderWindow\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_loader/ui/actions_utils.py",
    "content": "import uuid\n\nfrom qtpy import QtWidgets, QtGui\nimport qtawesome\n\nfrom openpype.lib.attribute_definitions import AbstractAttrDef\nfrom openpype.tools.attribute_defs import AttributeDefinitionsDialog\nfrom openpype.tools.utils.widgets import (\n    OptionalMenu,\n    OptionalAction,\n    OptionDialog,\n)\nfrom openpype.tools.ayon_utils.widgets import get_qt_icon\n\n\ndef show_actions_menu(action_items, global_point, one_item_selected, parent):\n    selected_action_item = None\n    selected_options = None\n\n    if not action_items:\n        menu = QtWidgets.QMenu(parent)\n        action = _get_no_loader_action(menu, one_item_selected)\n        menu.addAction(action)\n        menu.exec_(global_point)\n        return selected_action_item, selected_options\n\n    menu = OptionalMenu(parent)\n\n    action_items_by_id = {}\n    for action_item in action_items:\n        item_id = uuid.uuid4().hex\n        action_items_by_id[item_id] = action_item\n        item_options = action_item.options\n        icon = get_qt_icon(action_item.icon)\n        use_option = bool(item_options)\n        action = OptionalAction(\n            action_item.label,\n            icon,\n            use_option,\n            menu\n        )\n        if use_option:\n            # Add option box tip\n            action.set_option_tip(item_options)\n\n        tip = action_item.tooltip\n        if tip:\n            action.setToolTip(tip)\n            action.setStatusTip(tip)\n\n        action.setData(item_id)\n\n        menu.addAction(action)\n\n    action = menu.exec_(global_point)\n    if action is not None:\n        item_id = action.data()\n        selected_action_item = action_items_by_id.get(item_id)\n\n    if selected_action_item is not None:\n        selected_options = _get_options(action, selected_action_item, parent)\n\n    return selected_action_item, selected_options\n\n\ndef _get_options(action, action_item, parent):\n    \"\"\"Provides dialog to select value from loader provided options.\n\n    Loader can provide static or dynamically created options based on\n    AttributeDefinitions, and for backwards compatibility qargparse.\n\n    Args:\n        action (OptionalAction) - Action object in menu.\n        action_item (ActionItem) - Action item with context information.\n        parent (QtCore.QObject) - Parent object for dialog.\n\n    Returns:\n        Union[dict[str, Any], None]: Selected value from attributes or\n            'None' if dialog was cancelled.\n    \"\"\"\n\n    # Pop option dialog\n    options = action_item.options\n    if not getattr(action, \"optioned\", False) or not options:\n        return {}\n\n    if isinstance(options[0], AbstractAttrDef):\n        qargparse_options = False\n        dialog = AttributeDefinitionsDialog(options, parent)\n    else:\n        qargparse_options = True\n        dialog = OptionDialog(parent)\n        dialog.create(options)\n\n    dialog.setWindowTitle(action.label + \" Options\")\n\n    if not dialog.exec_():\n        return None\n\n    # Get option\n    if qargparse_options:\n        return dialog.parse()\n    return dialog.get_values()\n\n\ndef _get_no_loader_action(menu, one_item_selected):\n    \"\"\"Creates dummy no loader option in 'menu'\"\"\"\n\n    if one_item_selected:\n        submsg = \"this version.\"\n    else:\n        submsg = \"your selection.\"\n    msg = \"No compatible loaders for {}\".format(submsg)\n    icon = qtawesome.icon(\n        \"fa.exclamation\",\n        color=QtGui.QColor(255, 51, 0)\n    )\n    return QtWidgets.QAction(icon, (\"*\" + msg), menu)\n"
  },
  {
    "path": "openpype/tools/ayon_loader/ui/folders_widget.py",
    "content": "import qtpy\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.tools.utils import (\n    RecursiveSortFilterProxyModel,\n    DeselectableTreeView,\n)\nfrom openpype.style import get_objected_colors\n\nfrom openpype.tools.ayon_utils.widgets import (\n    FoldersQtModel,\n    FOLDERS_MODEL_SENDER_NAME,\n)\nfrom openpype.tools.ayon_utils.widgets.folders_widget import FOLDER_ID_ROLE\n\nif qtpy.API == \"pyside\":\n    from PySide.QtGui import QStyleOptionViewItemV4\nelif qtpy.API == \"pyqt4\":\n    from PyQt4.QtGui import QStyleOptionViewItemV4\n\nUNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 50\n\n\nclass UnderlinesFolderDelegate(QtWidgets.QItemDelegate):\n    \"\"\"Item delegate drawing bars under folder label.\n\n    This is used in loader tool. Multiselection of folders\n    may group products by name under colored groups. Selected color groups are\n    then propagated back to selected folders as underlines.\n    \"\"\"\n    bar_height = 3\n\n    def __init__(self, *args, **kwargs):\n        super(UnderlinesFolderDelegate, self).__init__(*args, **kwargs)\n        colors = get_objected_colors(\"loader\", \"asset-view\")\n        self._selected_color = colors[\"selected\"].get_qcolor()\n        self._hover_color = colors[\"hover\"].get_qcolor()\n        self._selected_hover_color = colors[\"selected-hover\"].get_qcolor()\n\n    def sizeHint(self, option, index):\n        \"\"\"Add bar height to size hint.\"\"\"\n        result = super(UnderlinesFolderDelegate, self).sizeHint(option, index)\n        height = result.height()\n        result.setHeight(height + self.bar_height)\n\n        return result\n\n    def paint(self, painter, option, index):\n        \"\"\"Replicate painting of an item and draw color bars if needed.\"\"\"\n        # Qt4 compat\n        if qtpy.API in (\"pyside\", \"pyqt4\"):\n            option = QStyleOptionViewItemV4(option)\n\n        painter.save()\n\n        item_rect = QtCore.QRect(option.rect)\n        item_rect.setHeight(option.rect.height() - self.bar_height)\n\n        subset_colors = index.data(UNDERLINE_COLORS_ROLE) or []\n\n        subset_colors_width = 0\n        if subset_colors:\n            subset_colors_width = option.rect.width() / len(subset_colors)\n\n        subset_rects = []\n        counter = 0\n        for subset_c in subset_colors:\n            new_color = None\n            new_rect = None\n            if subset_c:\n                new_color = QtGui.QColor(subset_c)\n\n                new_rect = QtCore.QRect(\n                    option.rect.left() + (counter * subset_colors_width),\n                    option.rect.top() + (\n                        option.rect.height() - self.bar_height\n                    ),\n                    subset_colors_width,\n                    self.bar_height\n                )\n            subset_rects.append((new_color, new_rect))\n            counter += 1\n\n        # Background\n        if option.state & QtWidgets.QStyle.State_Selected:\n            if len(subset_colors) == 0:\n                item_rect.setTop(item_rect.top() + (self.bar_height / 2))\n\n            if option.state & QtWidgets.QStyle.State_MouseOver:\n                bg_color = self._selected_hover_color\n            else:\n                bg_color = self._selected_color\n        else:\n            item_rect.setTop(item_rect.top() + (self.bar_height / 2))\n            if option.state & QtWidgets.QStyle.State_MouseOver:\n                bg_color = self._hover_color\n            else:\n                bg_color = QtGui.QColor()\n                bg_color.setAlpha(0)\n\n        # When not needed to do a rounded corners (easier and without\n        #   painter restore):\n        painter.fillRect(\n            option.rect,\n            QtGui.QBrush(bg_color)\n        )\n\n        if option.state & QtWidgets.QStyle.State_Selected:\n            for color, subset_rect in subset_rects:\n                if not color or not subset_rect:\n                    continue\n                painter.fillRect(subset_rect, QtGui.QBrush(color))\n\n        # Icon\n        icon_index = index.model().index(\n            index.row(), index.column(), index.parent()\n        )\n        # - Default icon_rect if not icon\n        icon_rect = QtCore.QRect(\n            item_rect.left(),\n            item_rect.top(),\n            # To make sure it's same size all the time\n            option.rect.height() - self.bar_height,\n            option.rect.height() - self.bar_height\n        )\n        icon = index.model().data(icon_index, QtCore.Qt.DecorationRole)\n\n        if icon:\n            mode = QtGui.QIcon.Normal\n            if not (option.state & QtWidgets.QStyle.State_Enabled):\n                mode = QtGui.QIcon.Disabled\n            elif option.state & QtWidgets.QStyle.State_Selected:\n                mode = QtGui.QIcon.Selected\n\n            if isinstance(icon, QtGui.QPixmap):\n                icon = QtGui.QIcon(icon)\n                option.decorationSize = icon.size() / icon.devicePixelRatio()\n\n            elif isinstance(icon, QtGui.QColor):\n                pixmap = QtGui.QPixmap(option.decorationSize)\n                pixmap.fill(icon)\n                icon = QtGui.QIcon(pixmap)\n\n            elif isinstance(icon, QtGui.QImage):\n                icon = QtGui.QIcon(QtGui.QPixmap.fromImage(icon))\n                option.decorationSize = icon.size() / icon.devicePixelRatio()\n\n            elif isinstance(icon, QtGui.QIcon):\n                state = QtGui.QIcon.Off\n                if option.state & QtWidgets.QStyle.State_Open:\n                    state = QtGui.QIcon.On\n                actual_size = option.icon.actualSize(\n                    option.decorationSize, mode, state\n                )\n                option.decorationSize = QtCore.QSize(\n                    min(option.decorationSize.width(), actual_size.width()),\n                    min(option.decorationSize.height(), actual_size.height())\n                )\n\n            state = QtGui.QIcon.Off\n            if option.state & QtWidgets.QStyle.State_Open:\n                state = QtGui.QIcon.On\n\n            icon.paint(\n                painter, icon_rect,\n                QtCore.Qt.AlignLeft, mode, state\n            )\n\n        # Text\n        text_rect = QtCore.QRect(\n            icon_rect.left() + icon_rect.width() + 2,\n            item_rect.top(),\n            item_rect.width(),\n            item_rect.height()\n        )\n\n        painter.drawText(\n            text_rect, QtCore.Qt.AlignVCenter,\n            index.data(QtCore.Qt.DisplayRole)\n        )\n\n        painter.restore()\n\n\nclass LoaderFoldersModel(FoldersQtModel):\n    def __init__(self, *args, **kwargs):\n        super(LoaderFoldersModel, self).__init__(*args, **kwargs)\n\n        self._colored_items = set()\n\n    def _fill_item_data(self, item, folder_item):\n        \"\"\"\n\n        Args:\n            item (QtGui.QStandardItem): Item to fill data.\n            folder_item (FolderItem): Folder item.\n        \"\"\"\n\n        super(LoaderFoldersModel, self)._fill_item_data(item, folder_item)\n\n    def set_merged_products_selection(self, items):\n        changes = {\n            folder_id: None\n            for folder_id in self._colored_items\n        }\n\n        all_folder_ids = set()\n        for item in items:\n            folder_ids = item[\"folder_ids\"]\n            all_folder_ids.update(folder_ids)\n\n        for folder_id in all_folder_ids:\n            changes[folder_id] = []\n\n        for item in items:\n            item_color = item[\"color\"]\n            item_folder_ids = item[\"folder_ids\"]\n            for folder_id in all_folder_ids:\n                folder_color = (\n                    item_color\n                    if folder_id in item_folder_ids\n                    else None\n                )\n                changes[folder_id].append(folder_color)\n\n        for folder_id, color_value in changes.items():\n            item = self._items_by_id.get(folder_id)\n            if item is not None:\n                item.setData(color_value, UNDERLINE_COLORS_ROLE)\n\n        self._colored_items = all_folder_ids\n\n\nclass LoaderFoldersWidget(QtWidgets.QWidget):\n    \"\"\"Folders widget.\n\n    Widget that handles folders view, model and selection.\n\n    Expected selection handling is disabled by default. If enabled, the\n    widget will handle the expected in predefined way. Widget is listening\n    to event 'expected_selection_changed' with expected event data below,\n    the same data must be available when called method\n    'get_expected_selection_data' on controller.\n\n    {\n        \"folder\": {\n            \"current\": bool,               # Folder is what should be set now\n            \"folder_id\": Union[str, None], # Folder id that should be selected\n        },\n        ...\n    }\n\n    Selection is confirmed by calling method 'expected_folder_selected' on\n    controller.\n\n\n    Args:\n        controller (AbstractWorkfilesFrontend): The control object.\n        parent (QtWidgets.QWidget): The parent widget.\n    \"\"\"\n\n    refreshed = QtCore.Signal()\n\n    def __init__(self, controller, parent):\n        super(LoaderFoldersWidget, self).__init__(parent)\n\n        folders_view = DeselectableTreeView(self)\n        folders_view.setHeaderHidden(True)\n        folders_view.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection)\n\n        folders_model = LoaderFoldersModel(controller)\n        folders_proxy_model = RecursiveSortFilterProxyModel()\n        folders_proxy_model.setSourceModel(folders_model)\n        folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        folders_label_delegate = UnderlinesFolderDelegate(folders_view)\n\n        folders_view.setModel(folders_proxy_model)\n        folders_view.setItemDelegate(folders_label_delegate)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(folders_view, 1)\n\n        controller.register_event_callback(\n            \"selection.project.changed\",\n            self._on_project_selection_change,\n        )\n        controller.register_event_callback(\n            \"folders.refresh.finished\",\n            self._on_folders_refresh_finished\n        )\n        controller.register_event_callback(\n            \"controller.refresh.finished\",\n            self._on_controller_refresh\n        )\n        controller.register_event_callback(\n            \"expected_selection_changed\",\n            self._on_expected_selection_change\n        )\n\n        selection_model = folders_view.selectionModel()\n        selection_model.selectionChanged.connect(self._on_selection_change)\n\n        folders_model.refreshed.connect(self._on_model_refresh)\n\n        self._controller = controller\n        self._folders_view = folders_view\n        self._folders_model = folders_model\n        self._folders_proxy_model = folders_proxy_model\n        self._folders_label_delegate = folders_label_delegate\n\n        self._expected_selection = None\n\n    def set_name_filter(self, name):\n        \"\"\"Set filter of folder name.\n\n        Args:\n            name (str): The string filter.\n        \"\"\"\n\n        self._folders_proxy_model.setFilterFixedString(name)\n\n    def set_merged_products_selection(self, items):\n        \"\"\"\n\n        Args:\n            items (list[dict[str, Any]]): List of merged items with folder\n                ids.\n        \"\"\"\n\n        self._folders_model.set_merged_products_selection(items)\n\n    def refresh(self):\n        self._folders_model.refresh()\n\n    def _on_project_selection_change(self, event):\n        project_name = event[\"project_name\"]\n        self._set_project_name(project_name)\n\n    def _set_project_name(self, project_name):\n        self._folders_model.set_project_name(project_name)\n\n    def _clear(self):\n        self._folders_model.clear()\n\n    def _on_folders_refresh_finished(self, event):\n        if event[\"sender\"] != FOLDERS_MODEL_SENDER_NAME:\n            self._set_project_name(event[\"project_name\"])\n\n    def _on_controller_refresh(self):\n        self._update_expected_selection()\n\n    def _on_model_refresh(self):\n        if self._expected_selection:\n            self._set_expected_selection()\n        self._folders_proxy_model.sort(0)\n        self.refreshed.emit()\n\n    def _get_selected_item_ids(self):\n        selection_model = self._folders_view.selectionModel()\n        item_ids = []\n        for index in selection_model.selectedIndexes():\n            item_id = index.data(FOLDER_ID_ROLE)\n            if item_id is not None:\n                item_ids.append(item_id)\n        return item_ids\n\n    def _on_selection_change(self):\n        item_ids = self._get_selected_item_ids()\n        self._controller.set_selected_folders(item_ids)\n\n    # Expected selection handling\n    def _on_expected_selection_change(self, event):\n        self._update_expected_selection(event.data)\n\n    def _update_expected_selection(self, expected_data=None):\n        if expected_data is None:\n            expected_data = self._controller.get_expected_selection_data()\n\n        folder_data = expected_data.get(\"folder\")\n        if not folder_data or not folder_data[\"current\"]:\n            return\n\n        folder_id = folder_data[\"id\"]\n        self._expected_selection = folder_id\n        if not self._folders_model.is_refreshing:\n            self._set_expected_selection()\n\n    def _set_expected_selection(self):\n        folder_id = self._expected_selection\n        selected_ids = self._get_selected_item_ids()\n        self._expected_selection = None\n        skip_selection = (\n            folder_id is None\n            or (\n                folder_id in selected_ids\n                and len(selected_ids) == 1\n            )\n        )\n        if not skip_selection:\n            index = self._folders_model.get_index_by_id(folder_id)\n            if index.isValid():\n                proxy_index = self._folders_proxy_model.mapFromSource(index)\n                self._folders_view.setCurrentIndex(proxy_index)\n        self._controller.expected_folder_selected(folder_id)\n"
  },
  {
    "path": "openpype/tools/ayon_loader/ui/info_widget.py",
    "content": "import datetime\n\nfrom qtpy import QtWidgets\n\nfrom openpype.tools.utils.lib import format_version\n\n\nclass VersionTextEdit(QtWidgets.QTextEdit):\n    \"\"\"QTextEdit that displays version specific information.\n\n    This also overrides the context menu to add actions like copying\n    source path to clipboard or copying the raw data of the version\n    to clipboard.\n\n    \"\"\"\n    def __init__(self, controller, parent):\n        super(VersionTextEdit, self).__init__(parent=parent)\n\n        self._version_item = None\n        self._product_item = None\n\n        self._controller = controller\n\n        # Reset\n        self.set_current_item()\n\n    def set_current_item(self, product_item=None, version_item=None):\n        \"\"\"\n\n        Args:\n            product_item (Union[ProductItem, None]): Product item.\n            version_item (Union[VersionItem, None]): Version item to display.\n        \"\"\"\n\n        self._product_item = product_item\n        self._version_item = version_item\n\n        if version_item is None:\n            # Reset state to empty\n            self.setText(\"\")\n            return\n\n        version_label = format_version(abs(version_item.version))\n        if version_item.version < 0:\n            version_label = \"Hero version {}\".format(version_label)\n\n        # Define readable creation timestamp\n        created = version_item.published_time\n        created = datetime.datetime.strptime(created, \"%Y%m%dT%H%M%SZ\")\n        created = datetime.datetime.strftime(created, \"%b %d %Y %H:%M\")\n\n        comment = version_item.comment or \"No comment\"\n        source = version_item.source or \"No source\"\n\n        self.setHtml(\n            (\n                \"<h2>{product_name}</h2>\"\n                \"<h3>{version_label}</h3>\"\n                \"<b>Comment</b><br>\"\n                \"{comment}<br><br>\"\n\n                \"<b>Created</b><br>\"\n                \"{created}<br><br>\"\n\n                \"<b>Source</b><br>\"\n                \"{source}\"\n            ).format(\n                product_name=product_item.product_name,\n                version_label=version_label,\n                comment=comment,\n                created=created,\n                source=source,\n            )\n        )\n\n    def contextMenuEvent(self, event):\n        \"\"\"Context menu with additional actions\"\"\"\n        menu = self.createStandardContextMenu()\n\n        # Add additional actions when any text, so we can assume\n        #   the version is set.\n        source = None\n        if self._version_item is not None:\n            source = self._version_item.source\n\n        if source:\n            menu.addSeparator()\n            action = QtWidgets.QAction(\n                \"Copy source path to clipboard\", menu\n            )\n            action.triggered.connect(self._on_copy_source)\n            menu.addAction(action)\n\n        menu.exec_(event.globalPos())\n\n    def _on_copy_source(self):\n        \"\"\"Copy formatted source path to clipboard.\"\"\"\n\n        source = self._version_item.source\n        if not source:\n            return\n\n        filled_source = self._controller.fill_root_in_source(source)\n        clipboard = QtWidgets.QApplication.clipboard()\n        clipboard.setText(filled_source)\n\n\nclass InfoWidget(QtWidgets.QWidget):\n    \"\"\"A Widget that display information about a specific version\"\"\"\n    def __init__(self, controller, parent):\n        super(InfoWidget, self).__init__(parent=parent)\n\n        label_widget = QtWidgets.QLabel(\"Version Info\", self)\n        info_text_widget = VersionTextEdit(controller, self)\n        info_text_widget.setReadOnly(True)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(label_widget, 0)\n        layout.addWidget(info_text_widget, 1)\n\n        self._controller = controller\n\n        self._info_text_widget = info_text_widget\n        self._label_widget = label_widget\n\n    def set_selected_version_info(self, project_name, items):\n        if not items or not project_name:\n            self._info_text_widget.set_current_item()\n            return\n        first_item = next(iter(items))\n        product_item = self._controller.get_product_item(\n            project_name,\n            first_item[\"product_id\"],\n        )\n        version_id = first_item[\"version_id\"]\n        version_item = None\n        if product_item is not None:\n            version_item = product_item.version_items.get(version_id)\n\n        self._info_text_widget.set_current_item(product_item, version_item)\n"
  },
  {
    "path": "openpype/tools/ayon_loader/ui/product_group_dialog.py",
    "content": "from qtpy import QtWidgets\n\nfrom openpype.tools.utils import PlaceholderLineEdit\n\n\nclass ProductGroupDialog(QtWidgets.QDialog):\n    def __init__(self, controller, parent):\n        super(ProductGroupDialog, self).__init__(parent)\n        self.setWindowTitle(\"Grouping products\")\n        self.setMinimumWidth(250)\n        self.setModal(True)\n\n        main_label = QtWidgets.QLabel(\"Group Name\", self)\n\n        group_name_input = PlaceholderLineEdit(self)\n        group_name_input.setPlaceholderText(\"Remain blank to ungroup..\")\n\n        group_btn = QtWidgets.QPushButton(\"Apply\", self)\n        group_btn.setAutoDefault(True)\n        group_btn.setDefault(True)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(main_label, 0)\n        layout.addWidget(group_name_input, 0)\n        layout.addWidget(group_btn, 0)\n\n        group_btn.clicked.connect(self._on_apply_click)\n\n        self._project_name = None\n        self._product_ids = set()\n\n        self._controller = controller\n        self._group_btn = group_btn\n        self._group_name_input = group_name_input\n\n    def set_product_ids(self, project_name, product_ids):\n        self._project_name = project_name\n        self._product_ids = product_ids\n\n    def _on_apply_click(self):\n        group_name = self._group_name_input.text().strip() or None\n        self._controller.change_products_group(\n            self._project_name, self._product_ids, group_name\n        )\n        self.close()\n"
  },
  {
    "path": "openpype/tools/ayon_loader/ui/product_types_widget.py",
    "content": "from qtpy import QtWidgets, QtGui, QtCore\n\nfrom openpype.tools.ayon_utils.widgets import get_qt_icon\n\nPRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 1\n\n\nclass ProductTypesQtModel(QtGui.QStandardItemModel):\n    refreshed = QtCore.Signal()\n    filter_changed = QtCore.Signal()\n\n    def __init__(self, controller):\n        super(ProductTypesQtModel, self).__init__()\n        self._controller = controller\n\n        self._refreshing = False\n        self._bulk_change = False\n        self._items_by_name = {}\n\n    def is_refreshing(self):\n        return self._refreshing\n\n    def get_filter_info(self):\n        \"\"\"Product types filtering info.\n\n        Returns:\n            dict[str, bool]: Filtering value by product type name. False value\n                means to hide product type.\n        \"\"\"\n\n        return {\n            name: item.checkState() == QtCore.Qt.Checked\n            for name, item in self._items_by_name.items()\n        }\n\n    def refresh(self, project_name):\n        self._refreshing = True\n        product_type_items = self._controller.get_product_type_items(\n            project_name)\n\n        items_to_remove = set(self._items_by_name.keys())\n        new_items = []\n        for product_type_item in product_type_items:\n            name = product_type_item.name\n            items_to_remove.discard(name)\n            item = self._items_by_name.get(product_type_item.name)\n            if item is None:\n                item = QtGui.QStandardItem(name)\n                item.setData(name, PRODUCT_TYPE_ROLE)\n                item.setEditable(False)\n                item.setCheckable(True)\n                new_items.append(item)\n                self._items_by_name[name] = item\n\n            item.setCheckState(\n                QtCore.Qt.Checked\n                if product_type_item.checked\n                else QtCore.Qt.Unchecked\n            )\n            icon = get_qt_icon(product_type_item.icon)\n            item.setData(icon, QtCore.Qt.DecorationRole)\n\n        root_item = self.invisibleRootItem()\n        if new_items:\n            root_item.appendRows(new_items)\n\n        for name in items_to_remove:\n            item = self._items_by_name.pop(name)\n            root_item.removeRow(item.row())\n\n        self._refreshing = False\n        self.refreshed.emit()\n\n    def setData(self, index, value, role=None):\n        checkstate_changed = False\n        if role is None:\n            role = QtCore.Qt.EditRole\n        elif role == QtCore.Qt.CheckStateRole:\n            checkstate_changed = True\n        output = super(ProductTypesQtModel, self).setData(index, value, role)\n        if checkstate_changed and not self._bulk_change:\n            self.filter_changed.emit()\n        return output\n\n    def change_state_for_all(self, checked):\n        if self._items_by_name:\n            self.change_states(checked, self._items_by_name.keys())\n\n    def change_states(self, checked, product_types):\n        product_types = set(product_types)\n        if not product_types:\n            return\n\n        if checked is None:\n            state = None\n        elif checked:\n            state = QtCore.Qt.Checked\n        else:\n            state = QtCore.Qt.Unchecked\n\n        self._bulk_change = True\n\n        changed = False\n        for product_type in product_types:\n            item = self._items_by_name.get(product_type)\n            if item is None:\n                continue\n            new_state = state\n            item_checkstate = item.checkState()\n            if new_state is None:\n                if item_checkstate == QtCore.Qt.Checked:\n                    new_state = QtCore.Qt.Unchecked\n                else:\n                    new_state = QtCore.Qt.Checked\n            elif item_checkstate == new_state:\n                continue\n            changed = True\n            item.setCheckState(new_state)\n\n        self._bulk_change = False\n\n        if changed:\n            self.filter_changed.emit()\n\n\nclass ProductTypesView(QtWidgets.QListView):\n    filter_changed = QtCore.Signal()\n\n    def __init__(self, controller, parent):\n        super(ProductTypesView, self).__init__(parent)\n\n        self.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection\n        )\n        self.setAlternatingRowColors(True)\n        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n\n        product_types_model = ProductTypesQtModel(controller)\n        product_types_proxy_model = QtCore.QSortFilterProxyModel()\n        product_types_proxy_model.setSourceModel(product_types_model)\n\n        self.setModel(product_types_proxy_model)\n\n        product_types_model.refreshed.connect(self._on_refresh_finished)\n        product_types_model.filter_changed.connect(self._on_filter_change)\n        self.customContextMenuRequested.connect(self._on_context_menu)\n\n        controller.register_event_callback(\n            \"selection.project.changed\",\n            self._on_project_change\n        )\n\n        self._controller = controller\n\n        self._product_types_model = product_types_model\n        self._product_types_proxy_model = product_types_proxy_model\n\n    def get_filter_info(self):\n        return self._product_types_model.get_filter_info()\n\n    def _on_project_change(self, event):\n        project_name = event[\"project_name\"]\n        self._product_types_model.refresh(project_name)\n\n    def _on_refresh_finished(self):\n        self.filter_changed.emit()\n\n    def _on_filter_change(self):\n        if not self._product_types_model.is_refreshing():\n            self.filter_changed.emit()\n\n    def _change_selection_state(self, checkstate):\n        selection_model = self.selectionModel()\n        product_types = {\n            index.data(PRODUCT_TYPE_ROLE)\n            for index in selection_model.selectedIndexes()\n        }\n        product_types.discard(None)\n        self._product_types_model.change_states(checkstate, product_types)\n\n    def _on_enable_all(self):\n        self._product_types_model.change_state_for_all(True)\n\n    def _on_disable_all(self):\n        self._product_types_model.change_state_for_all(False)\n\n    def _on_context_menu(self, pos):\n        menu = QtWidgets.QMenu(self)\n\n        # Add enable all action\n        action_check_all = QtWidgets.QAction(menu)\n        action_check_all.setText(\"Enable All\")\n        action_check_all.triggered.connect(self._on_enable_all)\n        # Add disable all action\n        action_uncheck_all = QtWidgets.QAction(menu)\n        action_uncheck_all.setText(\"Disable All\")\n        action_uncheck_all.triggered.connect(self._on_disable_all)\n\n        menu.addAction(action_check_all)\n        menu.addAction(action_uncheck_all)\n\n        # Get mouse position\n        global_pos = self.viewport().mapToGlobal(pos)\n        menu.exec_(global_pos)\n\n    def event(self, event):\n        if event.type() == QtCore.QEvent.KeyPress:\n            if event.key() == QtCore.Qt.Key_Space:\n                self._change_selection_state(None)\n                return True\n\n            if event.key() == QtCore.Qt.Key_Backspace:\n                self._change_selection_state(False)\n                return True\n\n            if event.key() == QtCore.Qt.Key_Return:\n                self._change_selection_state(True)\n                return True\n\n        return super(ProductTypesView, self).event(event)\n"
  },
  {
    "path": "openpype/tools/ayon_loader/ui/products_delegates.py",
    "content": "import numbers\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.tools.utils.lib import format_version\n\nfrom .products_model import (\n    PRODUCT_ID_ROLE,\n    VERSION_NAME_EDIT_ROLE,\n    VERSION_ID_ROLE,\n    PRODUCT_IN_SCENE_ROLE,\n    ACTIVE_SITE_ICON_ROLE,\n    REMOTE_SITE_ICON_ROLE,\n    REPRESENTATIONS_COUNT_ROLE,\n    SYNC_ACTIVE_SITE_AVAILABILITY,\n    SYNC_REMOTE_SITE_AVAILABILITY,\n)\n\n\nclass VersionComboBox(QtWidgets.QComboBox):\n    value_changed = QtCore.Signal(str)\n\n    def __init__(self, product_id, parent):\n        super(VersionComboBox, self).__init__(parent)\n        self._product_id = product_id\n        self._items_by_id = {}\n\n        self._current_id = None\n\n        self.currentIndexChanged.connect(self._on_index_change)\n\n    def update_versions(self, version_items, current_version_id):\n        model = self.model()\n        root_item = model.invisibleRootItem()\n        version_items = list(reversed(version_items))\n        version_ids = [\n            version_item.version_id\n            for version_item in version_items\n        ]\n        if current_version_id not in version_ids and version_ids:\n            current_version_id = version_ids[0]\n        self._current_id = current_version_id\n\n        to_remove = set(self._items_by_id.keys()) - set(version_ids)\n        for item_id in to_remove:\n            item = self._items_by_id.pop(item_id)\n            root_item.removeRow(item.row())\n\n        for idx, version_item in enumerate(version_items):\n            version_id = version_item.version_id\n\n            item = self._items_by_id.get(version_id)\n            if item is None:\n                label = format_version(\n                    abs(version_item.version), version_item.is_hero\n                )\n                item = QtGui.QStandardItem(label)\n                item.setData(version_id, QtCore.Qt.UserRole)\n                self._items_by_id[version_id] = item\n\n            if item.row() != idx:\n                root_item.insertRow(idx, item)\n\n        index = version_ids.index(current_version_id)\n        if self.currentIndex() != index:\n            self.setCurrentIndex(index)\n\n    def _on_index_change(self):\n        idx = self.currentIndex()\n        value = self.itemData(idx)\n        if value == self._current_id:\n            return\n        self._current_id = value\n        self.value_changed.emit(self._product_id)\n\n\nclass VersionDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"A delegate that display version integer formatted as version string.\"\"\"\n\n    version_changed = QtCore.Signal()\n\n    def __init__(self, *args, **kwargs):\n        super(VersionDelegate, self).__init__(*args, **kwargs)\n        self._editor_by_product_id = {}\n\n    def displayText(self, value, locale):\n        if not isinstance(value, numbers.Integral):\n            return \"N/A\"\n        return format_version(abs(value), value < 0)\n\n    def paint(self, painter, option, index):\n        fg_color = index.data(QtCore.Qt.ForegroundRole)\n        if fg_color:\n            if isinstance(fg_color, QtGui.QBrush):\n                fg_color = fg_color.color()\n            elif isinstance(fg_color, QtGui.QColor):\n                pass\n            else:\n                fg_color = None\n\n        if not fg_color:\n            return super(VersionDelegate, self).paint(painter, option, index)\n\n        if option.widget:\n            style = option.widget.style()\n        else:\n            style = QtWidgets.QApplication.style()\n\n        style.drawControl(\n            style.CE_ItemViewItem, option, painter, option.widget\n        )\n\n        painter.save()\n\n        text = self.displayText(\n            index.data(QtCore.Qt.DisplayRole), option.locale\n        )\n        pen = painter.pen()\n        pen.setColor(fg_color)\n        painter.setPen(pen)\n\n        text_rect = style.subElementRect(style.SE_ItemViewItemText, option)\n        text_margin = style.proxy().pixelMetric(\n            style.PM_FocusFrameHMargin, option, option.widget\n        ) + 1\n\n        painter.drawText(\n            text_rect.adjusted(text_margin, 0, - text_margin, 0),\n            option.displayAlignment,\n            text\n        )\n\n        painter.restore()\n\n    def createEditor(self, parent, option, index):\n        product_id = index.data(PRODUCT_ID_ROLE)\n        if not product_id:\n            return\n\n        editor = VersionComboBox(product_id, parent)\n        self._editor_by_product_id[product_id] = editor\n        editor.value_changed.connect(self._on_editor_change)\n\n        return editor\n\n    def _on_editor_change(self, product_id):\n        editor = self._editor_by_product_id[product_id]\n\n        # Update model data\n        self.commitData.emit(editor)\n        # Display model data\n        self.version_changed.emit()\n\n    def setEditorData(self, editor, index):\n        editor.clear()\n\n        # Current value of the index\n        versions = index.data(VERSION_NAME_EDIT_ROLE) or []\n        version_id = index.data(VERSION_ID_ROLE)\n        editor.update_versions(versions, version_id)\n\n    def setModelData(self, editor, model, index):\n        \"\"\"Apply the integer version back in the model\"\"\"\n\n        version_id = editor.itemData(editor.currentIndex())\n        model.setData(index, version_id, VERSION_NAME_EDIT_ROLE)\n\n\nclass LoadedInSceneDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Delegate for Loaded in Scene state columns.\n\n    Shows \"Yes\" or \"No\" for 1 or 0 values, or \"N/A\" for other values.\n    Colorizes green or dark grey based on values.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(LoadedInSceneDelegate, self).__init__(*args, **kwargs)\n        self._colors = {\n            1: QtGui.QColor(80, 170, 80),\n            0: QtGui.QColor(90, 90, 90),\n        }\n        self._default_color = QtGui.QColor(90, 90, 90)\n\n    def displayText(self, value, locale):\n        if value == 0:\n            return \"No\"\n        elif value == 1:\n            return \"Yes\"\n        return \"N/A\"\n\n    def initStyleOption(self, option, index):\n        super(LoadedInSceneDelegate, self).initStyleOption(option, index)\n\n        # Colorize based on value\n        value = index.data(PRODUCT_IN_SCENE_ROLE)\n        color = self._colors.get(value, self._default_color)\n        option.palette.setBrush(QtGui.QPalette.Text, color)\n\n\nclass SiteSyncDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Paints icons and downloaded representation ration for both sites.\"\"\"\n\n    def paint(self, painter, option, index):\n        super(SiteSyncDelegate, self).paint(painter, option, index)\n        option = QtWidgets.QStyleOptionViewItem(option)\n        option.showDecorationSelected = True\n\n        active_icon = index.data(ACTIVE_SITE_ICON_ROLE)\n        remote_icon = index.data(REMOTE_SITE_ICON_ROLE)\n\n        availability_active = \"{}/{}\".format(\n            index.data(SYNC_ACTIVE_SITE_AVAILABILITY),\n            index.data(REPRESENTATIONS_COUNT_ROLE)\n        )\n        availability_remote = \"{}/{}\".format(\n            index.data(SYNC_REMOTE_SITE_AVAILABILITY),\n            index.data(REPRESENTATIONS_COUNT_ROLE)\n        )\n\n        if availability_active is None or availability_remote is None:\n            return\n\n        items_to_draw = [\n            (value, icon)\n            for value, icon in (\n                (availability_active, active_icon),\n                (availability_remote, remote_icon),\n            )\n            if icon\n        ]\n        if not items_to_draw:\n            return\n\n        icon_size = QtCore.QSize(24, 24)\n        padding = 10\n        pos_x = option.rect.x()\n\n        item_width = int(option.rect.width() / len(items_to_draw))\n        if item_width < 1:\n            item_width = 0\n\n        for value, icon in items_to_draw:\n            item_rect = QtCore.QRect(\n                pos_x,\n                option.rect.y(),\n                item_width,\n                option.rect.height()\n            )\n            # Prepare pos_x for next item\n            pos_x = item_rect.x() + item_rect.width()\n\n            pixmap = icon.pixmap(icon.actualSize(icon_size))\n            point = QtCore.QPoint(\n                item_rect.x() + padding,\n                item_rect.y() + ((item_rect.height() - pixmap.height()) * 0.5)\n            )\n            painter.drawPixmap(point, pixmap)\n\n            icon_offset = icon_size.width() + (padding * 2)\n            text_rect = QtCore.QRect(item_rect)\n            text_rect.setLeft(text_rect.left() + icon_offset)\n            if text_rect.width() < 1:\n                continue\n\n            painter.drawText(\n                text_rect,\n                option.displayAlignment,\n                value\n            )\n\n    def displayText(self, value, locale):\n        pass\n"
  },
  {
    "path": "openpype/tools/ayon_loader/ui/products_model.py",
    "content": "import collections\n\nimport qtawesome\nfrom qtpy import QtGui, QtCore\n\nfrom openpype.style import get_default_entity_icon_color\nfrom openpype.tools.ayon_utils.widgets import get_qt_icon\n\nPRODUCTS_MODEL_SENDER_NAME = \"qt_products_model\"\n\nGROUP_TYPE_ROLE = QtCore.Qt.UserRole + 1\nMERGED_COLOR_ROLE = QtCore.Qt.UserRole + 2\nFOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 3\nFOLDER_ID_ROLE = QtCore.Qt.UserRole + 4\nPRODUCT_ID_ROLE = QtCore.Qt.UserRole + 5\nPRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 6\nPRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 7\nPRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 8\nPRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 9\nVERSION_ID_ROLE = QtCore.Qt.UserRole + 10\nVERSION_HERO_ROLE = QtCore.Qt.UserRole + 11\nVERSION_NAME_ROLE = QtCore.Qt.UserRole + 12\nVERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13\nVERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14\nVERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 15\nVERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 16\nVERSION_DURATION_ROLE = QtCore.Qt.UserRole + 17\nVERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 18\nVERSION_STEP_ROLE = QtCore.Qt.UserRole + 19\nVERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 20\nVERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 21\nACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 22\nREMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23\nREPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 24\nSYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 25\nSYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 26\n\n\nclass ProductsModel(QtGui.QStandardItemModel):\n    refreshed = QtCore.Signal()\n    version_changed = QtCore.Signal()\n    column_labels = [\n        \"Product name\",\n        \"Product type\",\n        \"Folder\",\n        \"Version\",\n        \"Time\",\n        \"Author\",\n        \"Frames\",\n        \"Duration\",\n        \"Handles\",\n        \"Step\",\n        \"In scene\",\n        \"Availability\",\n    ]\n    merged_items_colors = [\n        (\"#{0:02x}{1:02x}{2:02x}\".format(*c), QtGui.QColor(*c))\n        for c in [\n            (55, 161, 222),   # Light Blue\n            (231, 176, 0),    # Yellow\n            (154, 13, 255),   # Purple\n            (130, 184, 30),   # Light Green\n            (211, 79, 63),    # Light Red\n            (179, 181, 182),  # Grey\n            (194, 57, 179),   # Pink\n            (0, 120, 215),    # Dark Blue\n            (0, 204, 106),    # Dark Green\n            (247, 99, 12),    # Orange\n        ]\n    ]\n\n    version_col = column_labels.index(\"Version\")\n    published_time_col = column_labels.index(\"Time\")\n    folders_label_col = column_labels.index(\"Folder\")\n    in_scene_col = column_labels.index(\"In scene\")\n    site_sync_avail_col = column_labels.index(\"Availability\")\n\n    def __init__(self, controller):\n        super(ProductsModel, self).__init__()\n        self.setColumnCount(len(self.column_labels))\n        for idx, label in enumerate(self.column_labels):\n            self.setHeaderData(idx, QtCore.Qt.Horizontal, label)\n        self._controller = controller\n\n        # Variables to store 'QStandardItem'\n        self._items_by_id = {}\n        self._group_items_by_name = {}\n        self._merged_items_by_id = {}\n\n        # product item objects (they have version information)\n        self._product_items_by_id = {}\n        self._grouping_enabled = True\n        self._reset_merge_color = False\n        self._color_iterator = self._color_iter()\n        self._group_icon = None\n\n        self._last_project_name = None\n        self._last_folder_ids = []\n\n    def get_product_item_indexes(self):\n        return [\n            item.index()\n            for item in self._items_by_id.values()\n        ]\n\n    def get_product_item_by_id(self, product_id):\n        \"\"\"\n\n        Args:\n            product_id (str): Product id.\n\n        Returns:\n            Union[ProductItem, None]: Product item with version information.\n        \"\"\"\n\n        return self._product_items_by_id.get(product_id)\n\n    def set_enable_grouping(self, enable_grouping):\n        if enable_grouping is self._grouping_enabled:\n            return\n        self._grouping_enabled = enable_grouping\n        # Ignore change if groups are not available\n        self.refresh(self._last_project_name, self._last_folder_ids)\n\n    def flags(self, index):\n        # Make the version column editable\n        if index.column() == self.version_col and index.data(PRODUCT_ID_ROLE):\n            return (\n                QtCore.Qt.ItemIsEnabled\n                | QtCore.Qt.ItemIsSelectable\n                | QtCore.Qt.ItemIsEditable\n            )\n        if index.column() != 0:\n            index = self.index(index.row(), 0, index.parent())\n        return super(ProductsModel, self).flags(index)\n\n    def data(self, index, role=None):\n        if role is None:\n            role = QtCore.Qt.DisplayRole\n\n        if not index.isValid():\n            return None\n\n        col = index.column()\n        if col == 0:\n            return super(ProductsModel, self).data(index, role)\n\n        if role == QtCore.Qt.DecorationRole:\n            if col == 1:\n                role = PRODUCT_TYPE_ICON_ROLE\n            else:\n                return None\n\n        if (\n            role == VERSION_NAME_EDIT_ROLE\n            or (role == QtCore.Qt.EditRole and col == self.version_col)\n        ):\n            index = self.index(index.row(), 0, index.parent())\n            product_id = index.data(PRODUCT_ID_ROLE)\n            product_item = self._product_items_by_id.get(product_id)\n            if product_item is None:\n                return None\n            return list(product_item.version_items.values())\n\n        if role == QtCore.Qt.EditRole:\n            return None\n\n        if role == QtCore.Qt.DisplayRole:\n            if not index.data(PRODUCT_ID_ROLE):\n                return None\n            if col == self.version_col:\n                role = VERSION_NAME_ROLE\n            elif col == 1:\n                role = PRODUCT_TYPE_ROLE\n            elif col == 2:\n                role = FOLDER_LABEL_ROLE\n            elif col == 4:\n                role = VERSION_PUBLISH_TIME_ROLE\n            elif col == 5:\n                role = VERSION_AUTHOR_ROLE\n            elif col == 6:\n                role = VERSION_FRAME_RANGE_ROLE\n            elif col == 7:\n                role = VERSION_DURATION_ROLE\n            elif col == 8:\n                role = VERSION_HANDLES_ROLE\n            elif col == 9:\n                role = VERSION_STEP_ROLE\n            elif col == 10:\n                role = PRODUCT_IN_SCENE_ROLE\n            elif col == 11:\n                role = VERSION_AVAILABLE_ROLE\n            else:\n                return None\n\n        index = self.index(index.row(), 0, index.parent())\n\n        return super(ProductsModel, self).data(index, role)\n\n    def setData(self, index, value, role=None):\n        if not index.isValid():\n            return False\n\n        if role is None:\n            role = QtCore.Qt.EditRole\n\n        col = index.column()\n        if col == self.version_col and role == QtCore.Qt.EditRole:\n            role = VERSION_NAME_EDIT_ROLE\n\n        if role == VERSION_NAME_EDIT_ROLE:\n            if col != 0:\n                index = self.index(index.row(), 0, index.parent())\n            product_id = index.data(PRODUCT_ID_ROLE)\n            product_item = self._product_items_by_id[product_id]\n            final_version_item = None\n            for v_id, version_item in product_item.version_items.items():\n                if v_id == value:\n                    final_version_item = version_item\n                    break\n\n            if final_version_item is None:\n                return False\n            if index.data(VERSION_ID_ROLE) == final_version_item.version_id:\n                return True\n            item = self.itemFromIndex(index)\n            self._set_version_data_to_product_item(item, final_version_item)\n            self.version_changed.emit()\n            return True\n        return super(ProductsModel, self).setData(index, value, role)\n\n    def _get_next_color(self):\n        return next(self._color_iterator)\n\n    def _color_iter(self):\n        while True:\n            for color in self.merged_items_colors:\n                if self._reset_merge_color:\n                    self._reset_merge_color = False\n                    break\n                yield color\n\n    def _clear(self):\n        root_item = self.invisibleRootItem()\n        root_item.removeRows(0, root_item.rowCount())\n\n        self._items_by_id = {}\n        self._group_items_by_name = {}\n        self._merged_items_by_id = {}\n        self._product_items_by_id = {}\n        self._reset_merge_color = True\n\n    def _get_group_icon(self):\n        if self._group_icon is None:\n            self._group_icon = qtawesome.icon(\n                \"fa.object-group\",\n                color=get_default_entity_icon_color()\n            )\n        return self._group_icon\n\n    def _get_group_model_item(self, group_name):\n        model_item = self._group_items_by_name.get(group_name)\n        if model_item is None:\n            model_item = QtGui.QStandardItem(group_name)\n            model_item.setData(\n                self._get_group_icon(), QtCore.Qt.DecorationRole\n            )\n            model_item.setData(0, GROUP_TYPE_ROLE)\n            model_item.setEditable(False)\n            model_item.setColumnCount(self.columnCount())\n            self._group_items_by_name[group_name] = model_item\n        return model_item\n\n    def _get_merged_model_item(self, path, count, hex_color):\n        model_item = self._merged_items_by_id.get(path)\n        if model_item is None:\n            model_item = QtGui.QStandardItem()\n            model_item.setData(1, GROUP_TYPE_ROLE)\n            model_item.setData(hex_color, MERGED_COLOR_ROLE)\n            model_item.setEditable(False)\n            model_item.setColumnCount(self.columnCount())\n            self._merged_items_by_id[path] = model_item\n        label = \"{} ({})\".format(path, count)\n        model_item.setData(label, QtCore.Qt.DisplayRole)\n        return model_item\n\n    def _set_version_data_to_product_item(self, model_item, version_item):\n        \"\"\"\n\n        Args:\n            model_item (QtGui.QStandardItem): Item which should have values\n                from version item.\n            version_item (VersionItem): Item from entities model with\n                information about version.\n        \"\"\"\n\n        model_item.setData(version_item.version_id, VERSION_ID_ROLE)\n        model_item.setData(version_item.version, VERSION_NAME_ROLE)\n        model_item.setData(version_item.version_id, VERSION_ID_ROLE)\n        model_item.setData(version_item.is_hero, VERSION_HERO_ROLE)\n        model_item.setData(\n            version_item.published_time, VERSION_PUBLISH_TIME_ROLE\n        )\n        model_item.setData(version_item.author, VERSION_AUTHOR_ROLE)\n        model_item.setData(version_item.frame_range, VERSION_FRAME_RANGE_ROLE)\n        model_item.setData(version_item.duration, VERSION_DURATION_ROLE)\n        model_item.setData(version_item.handles, VERSION_HANDLES_ROLE)\n        model_item.setData(version_item.step, VERSION_STEP_ROLE)\n        model_item.setData(\n            version_item.thumbnail_id, VERSION_THUMBNAIL_ID_ROLE)\n\n        # TODO call site sync methods for all versions at once\n        project_name = self._last_project_name\n        version_id = version_item.version_id\n        repre_count = self._controller.get_versions_representation_count(\n            project_name, [version_id]\n        )[version_id]\n        active, remote = self._controller.get_version_sync_availability(\n            project_name, [version_id]\n        )[version_id]\n\n        model_item.setData(repre_count, REPRESENTATIONS_COUNT_ROLE)\n        model_item.setData(active, SYNC_ACTIVE_SITE_AVAILABILITY)\n        model_item.setData(remote, SYNC_REMOTE_SITE_AVAILABILITY)\n\n    def _get_product_model_item(\n        self,\n        product_item,\n        active_site_icon,\n        remote_site_icon\n    ):\n        model_item = self._items_by_id.get(product_item.product_id)\n        versions = list(product_item.version_items.values())\n        versions.sort()\n        last_version = versions[-1]\n        if model_item is None:\n            product_id = product_item.product_id\n            model_item = QtGui.QStandardItem(product_item.product_name)\n            model_item.setEditable(False)\n            icon = get_qt_icon(product_item.product_icon)\n            product_type_icon = get_qt_icon(product_item.product_type_icon)\n            model_item.setColumnCount(self.columnCount())\n            model_item.setData(icon, QtCore.Qt.DecorationRole)\n            model_item.setData(product_id, PRODUCT_ID_ROLE)\n            model_item.setData(product_item.product_name, PRODUCT_NAME_ROLE)\n            model_item.setData(product_item.product_type, PRODUCT_TYPE_ROLE)\n            model_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE)\n            model_item.setData(product_item.folder_id, FOLDER_ID_ROLE)\n\n            self._product_items_by_id[product_id] = product_item\n            self._items_by_id[product_id] = model_item\n\n        model_item.setData(product_item.folder_label, FOLDER_LABEL_ROLE)\n        in_scene = 1 if product_item.product_in_scene else 0\n        model_item.setData(in_scene, PRODUCT_IN_SCENE_ROLE)\n\n        model_item.setData(active_site_icon, ACTIVE_SITE_ICON_ROLE)\n        model_item.setData(remote_site_icon, REMOTE_SITE_ICON_ROLE)\n\n        self._set_version_data_to_product_item(model_item, last_version)\n        return model_item\n\n    def get_last_project_name(self):\n        return self._last_project_name\n\n    def refresh(self, project_name, folder_ids):\n        self._clear()\n\n        self._last_project_name = project_name\n        self._last_folder_ids = folder_ids\n\n        active_site_icon_def = self._controller.get_active_site_icon_def(\n            project_name\n        )\n        remote_site_icon_def = self._controller.get_remote_site_icon_def(\n            project_name\n        )\n        active_site_icon = get_qt_icon(active_site_icon_def)\n        remote_site_icon = get_qt_icon(remote_site_icon_def)\n\n        product_items = self._controller.get_product_items(\n            project_name,\n            folder_ids,\n            sender=PRODUCTS_MODEL_SENDER_NAME\n        )\n        product_items_by_id = {\n            product_item.product_id: product_item\n            for product_item in product_items\n        }\n\n        # Prepare product groups\n        product_name_matches_by_group = collections.defaultdict(dict)\n        for product_item in product_items_by_id.values():\n            group_name = None\n            if self._grouping_enabled:\n                group_name = product_item.group_name\n\n            product_name = product_item.product_name\n            group = product_name_matches_by_group[group_name]\n            if product_name not in group:\n                group[product_name] = [product_item]\n                continue\n            group[product_name].append(product_item)\n\n        group_names = set(product_name_matches_by_group.keys())\n\n        root_item = self.invisibleRootItem()\n        new_root_items = []\n        merged_paths = set()\n        for group_name in group_names:\n            key_parts = []\n            if group_name:\n                key_parts.append(group_name)\n\n            groups = product_name_matches_by_group[group_name]\n            merged_product_items = {}\n            top_items = []\n            group_product_types = set()\n            for product_name, product_items in groups.items():\n                group_product_types |= {p.product_type for p in product_items}\n                if len(product_items) == 1:\n                    top_items.append(product_items[0])\n                else:\n                    path = \"/\".join(key_parts + [product_name])\n                    merged_paths.add(path)\n                    merged_product_items[path] = (\n                        product_name,\n                        product_items,\n                    )\n\n            parent_item = None\n            if group_name:\n                parent_item = self._get_group_model_item(group_name)\n                parent_item.setData(\n                    \"|\".join(group_product_types), PRODUCT_TYPE_ROLE)\n\n            new_items = []\n            if parent_item is not None and parent_item.row() < 0:\n                new_root_items.append(parent_item)\n\n            for product_item in top_items:\n                item = self._get_product_model_item(\n                    product_item,\n                    active_site_icon,\n                    remote_site_icon,\n                )\n                new_items.append(item)\n\n            for path_info in merged_product_items.values():\n                product_name, product_items = path_info\n                (merged_color_hex, merged_color_qt) = self._get_next_color()\n                merged_color = qtawesome.icon(\n                    \"fa.circle\", color=merged_color_qt)\n                merged_item = self._get_merged_model_item(\n                    product_name, len(product_items), merged_color_hex)\n                merged_item.setData(merged_color, QtCore.Qt.DecorationRole)\n                new_items.append(merged_item)\n\n                merged_product_types = set()\n                new_merged_items = []\n                for product_item in product_items:\n                    item = self._get_product_model_item(\n                        product_item,\n                        active_site_icon,\n                        remote_site_icon,\n                    )\n                    new_merged_items.append(item)\n                    merged_product_types.add(product_item.product_type)\n\n                merged_item.setData(\n                    \"|\".join(merged_product_types), PRODUCT_TYPE_ROLE)\n                if new_merged_items:\n                    merged_item.appendRows(new_merged_items)\n\n            if not new_items:\n                continue\n\n            if parent_item is None:\n                new_root_items.extend(new_items)\n            else:\n                parent_item.appendRows(new_items)\n\n        if new_root_items:\n            root_item.appendRows(new_root_items)\n\n        self.refreshed.emit()\n    # ---------------------------------\n    #   This implementation does not call '_clear' at the start\n    #       but is more complex and probably slower\n    # ---------------------------------\n    # def _remove_items(self, items):\n    #     if not items:\n    #         return\n    #     root_item = self.invisibleRootItem()\n    #     for item in items:\n    #         row = item.row()\n    #         if row < 0:\n    #             continue\n    #         parent = item.parent()\n    #         if parent is None:\n    #             parent = root_item\n    #         parent.removeRow(row)\n    #\n    # def _remove_group_items(self, group_names):\n    #     group_items = [\n    #         self._group_items_by_name.pop(group_name)\n    #         for group_name in group_names\n    #     ]\n    #     self._remove_items(group_items)\n    #\n    # def _remove_merged_items(self, paths):\n    #     merged_items = [\n    #         self._merged_items_by_id.pop(path)\n    #         for path in paths\n    #     ]\n    #     self._remove_items(merged_items)\n    #\n    # def _remove_product_items(self, product_ids):\n    #     product_items = []\n    #     for product_id in product_ids:\n    #         self._product_items_by_id.pop(product_id)\n    #         product_items.append(self._items_by_id.pop(product_id))\n    #     self._remove_items(product_items)\n    #\n    # def _add_to_new_items(self, item, parent_item, new_items, root_item):\n    #     if item.row() < 0:\n    #         new_items.append(item)\n    #     else:\n    #         item_parent = item.parent()\n    #         if item_parent is not parent_item:\n    #             if item_parent is None:\n    #                 item_parent = root_item\n    #             item_parent.takeRow(item.row())\n    #             new_items.append(item)\n\n    # def refresh(self, project_name, folder_ids):\n    #     product_items = self._controller.get_product_items(\n    #         project_name,\n    #         folder_ids,\n    #         sender=PRODUCTS_MODEL_SENDER_NAME\n    #     )\n    #     product_items_by_id = {\n    #         product_item.product_id: product_item\n    #         for product_item in product_items\n    #     }\n    #     # Remove product items that are not available\n    #     product_ids_to_remove = (\n    #         set(self._items_by_id.keys()) - set(product_items_by_id.keys())\n    #     )\n    #     self._remove_product_items(product_ids_to_remove)\n    #\n    #     # Prepare product groups\n    #     product_name_matches_by_group = collections.defaultdict(dict)\n    #     for product_item in product_items_by_id.values():\n    #         group_name = None\n    #         if self._grouping_enabled:\n    #             group_name = product_item.group_name\n    #\n    #         product_name = product_item.product_name\n    #         group = product_name_matches_by_group[group_name]\n    #         if product_name not in group:\n    #             group[product_name] = [product_item]\n    #             continue\n    #         group[product_name].append(product_item)\n    #\n    #     group_names = set(product_name_matches_by_group.keys())\n    #\n    #     root_item = self.invisibleRootItem()\n    #     new_root_items = []\n    #     merged_paths = set()\n    #     for group_name in group_names:\n    #         key_parts = []\n    #         if group_name:\n    #             key_parts.append(group_name)\n    #\n    #         groups = product_name_matches_by_group[group_name]\n    #         merged_product_items = {}\n    #         top_items = []\n    #         for product_name, product_items in groups.items():\n    #             if len(product_items) == 1:\n    #                 top_items.append(product_items[0])\n    #             else:\n    #                 path = \"/\".join(key_parts + [product_name])\n    #                 merged_paths.add(path)\n    #                 merged_product_items[path] = product_items\n    #\n    #         parent_item = None\n    #         if group_name:\n    #             parent_item = self._get_group_model_item(group_name)\n    #\n    #         new_items = []\n    #         if parent_item is not None and parent_item.row() < 0:\n    #             new_root_items.append(parent_item)\n    #\n    #         for product_item in top_items:\n    #             item = self._get_product_model_item(product_item)\n    #             self._add_to_new_items(\n    #                 item, parent_item, new_items, root_item\n    #             )\n    #\n    #         for path, product_items in merged_product_items.items():\n    #             merged_item = self._get_merged_model_item(path)\n    #             self._add_to_new_items(\n    #                 merged_item, parent_item, new_items, root_item\n    #             )\n    #\n    #             new_merged_items = []\n    #             for product_item in product_items:\n    #                 item = self._get_product_model_item(product_item)\n    #                 self._add_to_new_items(\n    #                     item, merged_item, new_merged_items, root_item\n    #                 )\n    #\n    #             if new_merged_items:\n    #                 merged_item.appendRows(new_merged_items)\n    #\n    #         if not new_items:\n    #             continue\n    #\n    #         if parent_item is not None:\n    #             parent_item.appendRows(new_items)\n    #             continue\n    #\n    #         new_root_items.extend(new_items)\n    #\n    #     root_item.appendRows(new_root_items)\n    #\n    #     merged_item_ids_to_remove = (\n    #         set(self._merged_items_by_id.keys()) - merged_paths\n    #     )\n    #     group_names_to_remove = (\n    #         set(self._group_items_by_name.keys()) - set(group_names)\n    #     )\n    #     self._remove_merged_items(merged_item_ids_to_remove)\n    #     self._remove_group_items(group_names_to_remove)\n"
  },
  {
    "path": "openpype/tools/ayon_loader/ui/products_widget.py",
    "content": "import collections\n\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.tools.utils import (\n    RecursiveSortFilterProxyModel,\n    DeselectableTreeView,\n)\nfrom openpype.tools.utils.delegates import PrettyTimeDelegate\n\nfrom .products_model import (\n    ProductsModel,\n    PRODUCTS_MODEL_SENDER_NAME,\n    PRODUCT_TYPE_ROLE,\n    GROUP_TYPE_ROLE,\n    MERGED_COLOR_ROLE,\n    FOLDER_ID_ROLE,\n    PRODUCT_ID_ROLE,\n    VERSION_ID_ROLE,\n    VERSION_THUMBNAIL_ID_ROLE,\n)\nfrom .products_delegates import (\n    VersionDelegate,\n    LoadedInSceneDelegate,\n    SiteSyncDelegate\n)\nfrom .actions_utils import show_actions_menu\n\n\nclass ProductsProxyModel(RecursiveSortFilterProxyModel):\n    def __init__(self, parent=None):\n        super(ProductsProxyModel, self).__init__(parent)\n\n        self._product_type_filters = {}\n        self._ascending_sort = True\n\n    def set_product_type_filters(self, product_type_filters):\n        self._product_type_filters = product_type_filters\n        self.invalidateFilter()\n\n    def filterAcceptsRow(self, source_row, source_parent):\n        source_model = self.sourceModel()\n        index = source_model.index(source_row, 0, source_parent)\n        product_types_s = source_model.data(index, PRODUCT_TYPE_ROLE)\n        product_types = []\n        if product_types_s:\n            product_types = product_types_s.split(\"|\")\n\n        for product_type in product_types:\n            if not self._product_type_filters.get(product_type, True):\n                return False\n        return super(ProductsProxyModel, self).filterAcceptsRow(\n            source_row, source_parent)\n\n    def lessThan(self, left, right):\n        l_model = left.model()\n        r_model = right.model()\n        left_group_type = l_model.data(left, GROUP_TYPE_ROLE)\n        right_group_type = r_model.data(right, GROUP_TYPE_ROLE)\n        # Groups are always on top, merged product types are below\n        #   and items without group at the bottom\n        # QUESTION Do we need to do it this way?\n        if left_group_type != right_group_type:\n            if left_group_type is None:\n                output = False\n            elif right_group_type is None:\n                output = True\n            else:\n                output = left_group_type < right_group_type\n            if not self._ascending_sort:\n                output = not output\n            return output\n        return super(ProductsProxyModel, self).lessThan(left, right)\n\n    def sort(self, column, order=None):\n        if order is None:\n            order = QtCore.Qt.AscendingOrder\n        self._ascending_sort = order == QtCore.Qt.AscendingOrder\n        super(ProductsProxyModel, self).sort(column, order)\n\n\nclass ProductsWidget(QtWidgets.QWidget):\n    refreshed = QtCore.Signal()\n    merged_products_selection_changed = QtCore.Signal()\n    selection_changed = QtCore.Signal()\n    version_changed = QtCore.Signal()\n    default_widths = (\n        200,  # Product name\n        90,   # Product type\n        130,  # Folder label\n        60,   # Version\n        125,  # Time\n        75,   # Author\n        75,   # Frames\n        60,   # Duration\n        55,   # Handles\n        10,   # Step\n        25,   # Loaded in scene\n        65,   # Site sync info\n    )\n\n    def __init__(self, controller, parent):\n        super(ProductsWidget, self).__init__(parent)\n\n        self._controller = controller\n\n        products_view = DeselectableTreeView(self)\n        # TODO - define custom object name in style\n        products_view.setObjectName(\"SubsetView\")\n        products_view.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection\n        )\n        products_view.setAllColumnsShowFocus(True)\n        # TODO - add context menu\n        products_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        products_view.setSortingEnabled(True)\n        # Sort by product type\n        products_view.sortByColumn(1, QtCore.Qt.AscendingOrder)\n        products_view.setAlternatingRowColors(True)\n\n        products_model = ProductsModel(controller)\n        products_proxy_model = ProductsProxyModel()\n        products_proxy_model.setSourceModel(products_model)\n\n        products_view.setModel(products_proxy_model)\n\n        for idx, width in enumerate(self.default_widths):\n            products_view.setColumnWidth(idx, width)\n\n        version_delegate = VersionDelegate()\n        products_view.setItemDelegateForColumn(\n            products_model.version_col, version_delegate)\n\n        time_delegate = PrettyTimeDelegate()\n        products_view.setItemDelegateForColumn(\n            products_model.published_time_col, time_delegate)\n\n        in_scene_delegate = LoadedInSceneDelegate()\n        products_view.setItemDelegateForColumn(\n            products_model.in_scene_col, in_scene_delegate)\n\n        site_sync_delegate = SiteSyncDelegate()\n        products_view.setItemDelegateForColumn(\n            products_model.site_sync_avail_col, site_sync_delegate)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(products_view, 1)\n\n        products_proxy_model.rowsInserted.connect(self._on_rows_inserted)\n        products_proxy_model.rowsMoved.connect(self._on_rows_moved)\n        products_model.refreshed.connect(self._on_refresh)\n        products_view.customContextMenuRequested.connect(\n            self._on_context_menu)\n        products_view.selectionModel().selectionChanged.connect(\n            self._on_selection_change)\n        products_model.version_changed.connect(self._on_version_change)\n\n        controller.register_event_callback(\n            \"selection.folders.changed\",\n            self._on_folders_selection_change,\n        )\n        controller.register_event_callback(\n            \"products.refresh.finished\",\n            self._on_products_refresh_finished\n        )\n        controller.register_event_callback(\n            \"products.group.changed\",\n            self._on_group_changed\n        )\n\n        self._products_view = products_view\n        self._products_model = products_model\n        self._products_proxy_model = products_proxy_model\n\n        self._version_delegate = version_delegate\n        self._time_delegate = time_delegate\n        self._in_scene_delegate = in_scene_delegate\n        self._site_sync_delegate = site_sync_delegate\n\n        self._selected_project_name = None\n        self._selected_folder_ids = set()\n\n        self._selected_merged_products = []\n        self._selected_versions_info = []\n\n        # Set initial state of widget\n        # - Hide folders column\n        self._update_folders_label_visible()\n        # - Hide in scene column if is not supported (this won't change)\n        products_view.setColumnHidden(\n            products_model.in_scene_col,\n            not controller.is_loaded_products_supported()\n        )\n        self._set_site_sync_visibility(\n            self._controller.is_site_sync_enabled()\n        )\n\n    def set_name_filter(self, name):\n        \"\"\"Set filter of product name.\n\n        Args:\n            name (str): The string filter.\n        \"\"\"\n\n        self._products_proxy_model.setFilterFixedString(name)\n\n    def set_product_type_filter(self, product_type_filters):\n        \"\"\"\n\n        Args:\n            product_type_filters (dict[str, bool]): The filter of product\n                types.\n        \"\"\"\n\n        self._products_proxy_model.set_product_type_filters(\n            product_type_filters\n        )\n\n    def set_enable_grouping(self, enable_grouping):\n        self._products_model.set_enable_grouping(enable_grouping)\n\n    def get_selected_merged_products(self):\n        return self._selected_merged_products\n\n    def get_selected_version_info(self):\n        return self._selected_versions_info\n\n    def refresh(self):\n        self._refresh_model()\n\n    def _set_site_sync_visibility(self, site_sync_enabled):\n        self._products_view.setColumnHidden(\n            self._products_model.site_sync_avail_col,\n            not site_sync_enabled\n        )\n\n    def _fill_version_editor(self):\n        model = self._products_proxy_model\n        index_queue = collections.deque()\n        for row in range(model.rowCount()):\n            index_queue.append((row, None))\n\n        version_col = self._products_model.version_col\n        while index_queue:\n            (row, parent_index) = index_queue.popleft()\n            args = [row, 0]\n            if parent_index is not None:\n                args.append(parent_index)\n            index = model.index(*args)\n            rows = model.rowCount(index)\n            for row in range(rows):\n                index_queue.append((row, index))\n\n            product_id = model.data(index, PRODUCT_ID_ROLE)\n            if product_id is not None:\n                args[1] = version_col\n                v_index = model.index(*args)\n                self._products_view.openPersistentEditor(v_index)\n\n    def _on_refresh(self):\n        self._fill_version_editor()\n        self.refreshed.emit()\n\n    def _on_rows_inserted(self):\n        self._fill_version_editor()\n\n    def _on_rows_moved(self):\n        self._fill_version_editor()\n\n    def _refresh_model(self):\n        self._products_model.refresh(\n            self._selected_project_name,\n            self._selected_folder_ids\n        )\n\n    def _on_context_menu(self, point):\n        selection_model = self._products_view.selectionModel()\n        model = self._products_view.model()\n        project_name = self._products_model.get_last_project_name()\n\n        version_ids = set()\n        indexes_queue = collections.deque()\n        indexes_queue.extend(selection_model.selectedIndexes())\n        while indexes_queue:\n            index = indexes_queue.popleft()\n            for row in range(model.rowCount(index)):\n                child_index = model.index(row, 0, index)\n                indexes_queue.append(child_index)\n            version_id = model.data(index, VERSION_ID_ROLE)\n            if version_id is not None:\n                version_ids.add(version_id)\n\n        action_items = self._controller.get_versions_action_items(\n            project_name, version_ids)\n\n        # Prepare global point where to show the menu\n        global_point = self._products_view.mapToGlobal(point)\n\n        result = show_actions_menu(\n            action_items,\n            global_point,\n            len(version_ids) == 1,\n            self\n        )\n        action_item, options = result\n        if action_item is None or options is None:\n            return\n\n        self._controller.trigger_action_item(\n            action_item.identifier,\n            options,\n            action_item.project_name,\n            version_ids=action_item.version_ids,\n            representation_ids=action_item.representation_ids,\n        )\n\n    def _on_selection_change(self):\n        selected_merged_products = []\n        selection_model = self._products_view.selectionModel()\n        model = self._products_view.model()\n        indexes_queue = collections.deque()\n        indexes_queue.extend(selection_model.selectedIndexes())\n\n        # Helper for 'version_items' to avoid duplicated items\n        all_product_ids = set()\n        selected_version_ids = set()\n        # Version items contains information about selected version items\n        selected_versions_info = []\n        while indexes_queue:\n            index = indexes_queue.popleft()\n            if index.column() != 0:\n                continue\n\n            group_type = model.data(index, GROUP_TYPE_ROLE)\n            if group_type is None:\n                product_id = model.data(index, PRODUCT_ID_ROLE)\n                # Skip duplicates - when group and item are selected the item\n                #   would be in the loop multiple times\n                if product_id in all_product_ids:\n                    continue\n\n                all_product_ids.add(product_id)\n\n                version_id = model.data(index, VERSION_ID_ROLE)\n                selected_version_ids.add(version_id)\n\n                thumbnail_id = model.data(index, VERSION_THUMBNAIL_ID_ROLE)\n                selected_versions_info.append({\n                    \"folder_id\": model.data(index, FOLDER_ID_ROLE),\n                    \"product_id\": product_id,\n                    \"version_id\": version_id,\n                    \"thumbnail_id\": thumbnail_id,\n                })\n                continue\n\n            if group_type == 0:\n                for row in range(model.rowCount(index)):\n                    child_index = model.index(row, 0, index)\n                    indexes_queue.append(child_index)\n                continue\n\n            if group_type != 1:\n                continue\n\n            item_folder_ids = set()\n            for row in range(model.rowCount(index)):\n                child_index = model.index(row, 0, index)\n                indexes_queue.append(child_index)\n\n                folder_id = model.data(child_index, FOLDER_ID_ROLE)\n                item_folder_ids.add(folder_id)\n\n            if not item_folder_ids:\n                continue\n\n            hex_color = model.data(index, MERGED_COLOR_ROLE)\n            item_data = {\n                \"color\": hex_color,\n                \"folder_ids\": item_folder_ids\n            }\n            selected_merged_products.append(item_data)\n\n        prev_selected_merged_products = self._selected_merged_products\n        self._selected_merged_products = selected_merged_products\n        self._selected_versions_info = selected_versions_info\n\n        if selected_merged_products != prev_selected_merged_products:\n            self.merged_products_selection_changed.emit()\n        self.selection_changed.emit()\n        self._controller.set_selected_versions(selected_version_ids)\n\n    def _on_version_change(self):\n        self._on_selection_change()\n\n    def _on_folders_selection_change(self, event):\n        project_name = event[\"project_name\"]\n        site_sync_enabled = self._controller.is_site_sync_enabled(\n            project_name\n        )\n        self._set_site_sync_visibility(site_sync_enabled)\n        self._selected_project_name = project_name\n        self._selected_folder_ids = event[\"folder_ids\"]\n        self._refresh_model()\n        self._update_folders_label_visible()\n\n    def _update_folders_label_visible(self):\n        folders_label_hidden = len(self._selected_folder_ids) <= 1\n        self._products_view.setColumnHidden(\n            self._products_model.folders_label_col,\n            folders_label_hidden\n        )\n\n    def _on_products_refresh_finished(self, event):\n        if event[\"sender\"] != PRODUCTS_MODEL_SENDER_NAME:\n            self._refresh_model()\n\n    def _on_group_changed(self, event):\n        if event[\"project_name\"] != self._selected_project_name:\n            return\n        folder_ids = event[\"folder_ids\"]\n        if not set(folder_ids).intersection(set(self._selected_folder_ids)):\n            return\n        self.refresh()\n"
  },
  {
    "path": "openpype/tools/ayon_loader/ui/repres_widget.py",
    "content": "import collections\n\nfrom qtpy import QtWidgets, QtGui, QtCore\nimport qtawesome\n\nfrom openpype.style import get_default_entity_icon_color\nfrom openpype.tools.ayon_utils.widgets import get_qt_icon\nfrom openpype.tools.utils import DeselectableTreeView\n\nfrom .actions_utils import show_actions_menu\n\nREPRESENTAION_NAME_ROLE = QtCore.Qt.UserRole + 1\nREPRESENTATION_ID_ROLE = QtCore.Qt.UserRole + 2\nPRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 3\nFOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 4\nGROUP_TYPE_ROLE = QtCore.Qt.UserRole + 5\nACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 6\nREMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 7\nSYNC_ACTIVE_SITE_PROGRESS = QtCore.Qt.UserRole + 8\nSYNC_REMOTE_SITE_PROGRESS = QtCore.Qt.UserRole + 9\n\n\nclass RepresentationsModel(QtGui.QStandardItemModel):\n    refreshed = QtCore.Signal()\n    colums_info = [\n        (\"Name\", 120),\n        (\"Product name\", 125),\n        (\"Folder\", 125),\n        (\"Active site\", 85),\n        (\"Remote site\", 85)\n    ]\n    column_labels = [label for label, _ in colums_info]\n    column_widths = [width for _, width in colums_info]\n    folder_column = column_labels.index(\"Product name\")\n    active_site_column = column_labels.index(\"Active site\")\n    remote_site_column = column_labels.index(\"Remote site\")\n\n    def __init__(self, controller):\n        super(RepresentationsModel, self).__init__()\n\n        self.setColumnCount(len(self.column_labels))\n\n        for idx, label in enumerate(self.column_labels):\n            self.setHeaderData(idx, QtCore.Qt.Horizontal, label)\n\n        controller.register_event_callback(\n            \"selection.project.changed\",\n            self._on_project_change\n        )\n        controller.register_event_callback(\n            \"selection.versions.changed\",\n            self._on_version_change\n        )\n        self._selected_project_name = None\n        self._selected_version_ids = None\n\n        self._group_icon = None\n\n        self._items_by_id = {}\n        self._groups_items_by_name = {}\n\n        self._controller = controller\n\n    def refresh(self):\n        repre_items = self._controller.get_representation_items(\n            self._selected_project_name, self._selected_version_ids\n        )\n        self._fill_items(repre_items, self._selected_project_name)\n        self.refreshed.emit()\n\n    def data(self, index, role=None):\n        if role is None:\n            role = QtCore.Qt.DisplayRole\n\n        col = index.column()\n        if col != 0:\n            if role == QtCore.Qt.DecorationRole:\n                if col == 3:\n                    role = ACTIVE_SITE_ICON_ROLE\n                elif col == 4:\n                    role = REMOTE_SITE_ICON_ROLE\n                else:\n                    return None\n\n            if role == QtCore.Qt.DisplayRole:\n                if col == 1:\n                    role = PRODUCT_NAME_ROLE\n                elif col == 2:\n                    role = FOLDER_LABEL_ROLE\n                elif col == 3:\n                    role = SYNC_ACTIVE_SITE_PROGRESS\n                elif col == 4:\n                    role = SYNC_REMOTE_SITE_PROGRESS\n\n            index = self.index(index.row(), 0, index.parent())\n        return super(RepresentationsModel, self).data(index, role)\n\n    def setData(self, index, value, role=None):\n        if role is None:\n            role = QtCore.Qt.EditRole\n        return super(RepresentationsModel, self).setData(index, value, role)\n\n    def _clear_items(self):\n        self._items_by_id = {}\n        root_item = self.invisibleRootItem()\n        root_item.removeRows(0, root_item.rowCount())\n\n    def _get_repre_item(\n        self,\n        repre_item,\n        active_site_icon,\n        remote_site_icon,\n        repres_sync_status\n    ):\n        repre_id = repre_item.representation_id\n        repre_name = repre_item.representation_name\n        repre_icon = repre_item.representation_icon\n        item = self._items_by_id.get(repre_id)\n        is_new_item = False\n        if item is None:\n            is_new_item = True\n            item = QtGui.QStandardItem()\n            self._items_by_id[repre_id] = item\n            item.setColumnCount(self.columnCount())\n            item.setEditable(False)\n\n        sync_status = repres_sync_status[repre_id]\n        active_progress, remote_progress = sync_status\n\n        active_site_progress = \"{}%\".format(int(active_progress * 100))\n        remote_site_progress = \"{}%\".format(int(remote_progress * 100))\n\n        icon = get_qt_icon(repre_icon)\n        item.setData(repre_name, QtCore.Qt.DisplayRole)\n        item.setData(icon, QtCore.Qt.DecorationRole)\n        item.setData(repre_name, REPRESENTAION_NAME_ROLE)\n        item.setData(repre_id, REPRESENTATION_ID_ROLE)\n        item.setData(repre_item.product_name, PRODUCT_NAME_ROLE)\n        item.setData(repre_item.folder_label, FOLDER_LABEL_ROLE)\n        item.setData(active_site_icon, ACTIVE_SITE_ICON_ROLE)\n        item.setData(remote_site_icon, REMOTE_SITE_ICON_ROLE)\n        item.setData(active_site_progress, SYNC_ACTIVE_SITE_PROGRESS)\n        item.setData(remote_site_progress, SYNC_REMOTE_SITE_PROGRESS)\n        return is_new_item, item\n\n    def _get_group_icon(self):\n        if self._group_icon is None:\n            self._group_icon = qtawesome.icon(\n                \"fa.folder\",\n                color=get_default_entity_icon_color()\n            )\n        return self._group_icon\n\n    def _get_group_item(self, repre_name):\n        item = self._groups_items_by_name.get(repre_name)\n        if item is not None:\n            return False, item\n\n        # TODO add color\n        item = QtGui.QStandardItem()\n        item.setColumnCount(self.columnCount())\n        item.setData(repre_name, QtCore.Qt.DisplayRole)\n        item.setData(self._get_group_icon(), QtCore.Qt.DecorationRole)\n        item.setData(0, GROUP_TYPE_ROLE)\n        item.setEditable(False)\n        self._groups_items_by_name[repre_name] = item\n        return True, item\n\n    def _fill_items(self, repre_items, project_name):\n        active_site_icon_def = self._controller.get_active_site_icon_def(\n            project_name\n        )\n        remote_site_icon_def = self._controller.get_remote_site_icon_def(\n            project_name\n        )\n        active_site_icon = get_qt_icon(active_site_icon_def)\n        remote_site_icon = get_qt_icon(remote_site_icon_def)\n\n        items_to_remove = set(self._items_by_id.keys())\n        repre_items_by_name = collections.defaultdict(list)\n        repre_ids = set()\n        for repre_item in repre_items:\n            repre_ids.add(repre_item.representation_id)\n            items_to_remove.discard(repre_item.representation_id)\n            repre_name = repre_item.representation_name\n            repre_items_by_name[repre_name].append(repre_item)\n\n        repres_sync_status = self._controller.get_representations_sync_status(\n            project_name, repre_ids\n        )\n\n        root_item = self.invisibleRootItem()\n        for repre_id in items_to_remove:\n            item = self._items_by_id.pop(repre_id)\n            parent_item = item.parent()\n            if parent_item is None:\n                parent_item = root_item\n            parent_item.removeRow(item.row())\n\n        group_names = set()\n        new_root_items = []\n        for repre_name, repre_name_items in repre_items_by_name.items():\n            group_item = None\n            parent_is_group = False\n            if len(repre_name_items) > 1:\n                group_names.add(repre_name)\n                is_new_group, group_item = self._get_group_item(repre_name)\n                if is_new_group:\n                    new_root_items.append(group_item)\n                parent_is_group = True\n\n            new_group_items = []\n            for repre_item in repre_name_items:\n                is_new_item, item = self._get_repre_item(\n                    repre_item,\n                    active_site_icon,\n                    remote_site_icon,\n                    repres_sync_status\n                )\n                item_parent = item.parent()\n                if item_parent is None:\n                    item_parent = root_item\n\n                if not is_new_item:\n                    if parent_is_group:\n                        if item_parent is group_item:\n                            continue\n                    elif item_parent is root_item:\n                        continue\n                    item_parent.takeRow(item.row())\n                    is_new_item = True\n\n                if is_new_item:\n                    new_group_items.append(item)\n\n            if not new_group_items:\n                continue\n\n            if group_item is not None:\n                group_item.appendRows(new_group_items)\n            else:\n                new_root_items.extend(new_group_items)\n\n        if new_root_items:\n            root_item.appendRows(new_root_items)\n\n        for group_name in set(self._groups_items_by_name) - group_names:\n            item = self._groups_items_by_name.pop(group_name)\n            parent_item = item.parent()\n            if parent_item is None:\n                parent_item = root_item\n            parent_item.removeRow(item.row())\n\n    def _on_project_change(self, event):\n        self._selected_project_name = event[\"project_name\"]\n\n    def _on_version_change(self, event):\n        self._selected_version_ids = event[\"version_ids\"]\n        self.refresh()\n\n\nclass RepresentationsWidget(QtWidgets.QWidget):\n    def __init__(self, controller, parent):\n        super(RepresentationsWidget, self).__init__(parent)\n\n        repre_view = DeselectableTreeView(self)\n        repre_view.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection\n        )\n        repre_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        repre_view.setSortingEnabled(True)\n        repre_view.setAlternatingRowColors(True)\n\n        repre_model = RepresentationsModel(controller)\n        repre_proxy_model = QtCore.QSortFilterProxyModel()\n        repre_proxy_model.setSourceModel(repre_model)\n        repre_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n        repre_view.setModel(repre_proxy_model)\n\n        for idx, width in enumerate(repre_model.column_widths):\n            repre_view.setColumnWidth(idx, width)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(repre_view, 1)\n\n        repre_view.customContextMenuRequested.connect(\n            self._on_context_menu)\n        repre_view.selectionModel().selectionChanged.connect(\n            self._on_selection_change)\n        repre_model.refreshed.connect(self._on_model_refresh)\n\n        controller.register_event_callback(\n            \"selection.project.changed\",\n            self._on_project_change\n        )\n        controller.register_event_callback(\n            \"selection.folders.changed\",\n            self._on_folder_change\n        )\n\n        self._controller = controller\n        self._selected_project_name = None\n        self._selected_multiple_folders = None\n\n        self._repre_view = repre_view\n        self._repre_model = repre_model\n        self._repre_proxy_model = repre_proxy_model\n\n        self._set_site_sync_visibility(\n            self._controller.is_site_sync_enabled()\n        )\n        self._set_multiple_folders_selected(False)\n\n    def refresh(self):\n        self._repre_model.refresh()\n\n    def _on_folder_change(self, event):\n        self._set_multiple_folders_selected(len(event[\"folder_ids\"]) > 1)\n\n    def _on_project_change(self, event):\n        self._selected_project_name = event[\"project_name\"]\n        site_sync_enabled = self._controller.is_site_sync_enabled(\n            self._selected_project_name\n        )\n        self._set_site_sync_visibility(site_sync_enabled)\n\n    def _set_site_sync_visibility(self, site_sync_enabled):\n        self._repre_view.setColumnHidden(\n            self._repre_model.active_site_column,\n            not site_sync_enabled\n        )\n        self._repre_view.setColumnHidden(\n            self._repre_model.remote_site_column,\n            not site_sync_enabled\n        )\n\n    def _set_multiple_folders_selected(self, selected_multiple_folders):\n        if selected_multiple_folders == self._selected_multiple_folders:\n            return\n        self._selected_multiple_folders = selected_multiple_folders\n        self._repre_view.setColumnHidden(\n            self._repre_model.folder_column,\n            not self._selected_multiple_folders\n        )\n\n    def _on_model_refresh(self):\n        self._repre_proxy_model.sort(0)\n\n    def _get_selected_repre_indexes(self):\n        selection_model = self._repre_view.selectionModel()\n        model = self._repre_view.model()\n        indexes_queue = collections.deque()\n        indexes_queue.extend(selection_model.selectedIndexes())\n\n        selected_indexes = []\n        while indexes_queue:\n            index = indexes_queue.popleft()\n            if index.column() != 0:\n                continue\n\n            group_type = model.data(index, GROUP_TYPE_ROLE)\n            if group_type is None:\n                selected_indexes.append(index)\n\n            elif group_type == 0:\n                for row in range(model.rowCount(index)):\n                    child_index = model.index(row, 0, index)\n                    indexes_queue.append(child_index)\n\n        return selected_indexes\n\n    def _get_selected_repre_ids(self):\n        repre_ids = {\n            index.data(REPRESENTATION_ID_ROLE)\n            for index in self._get_selected_repre_indexes()\n        }\n        repre_ids.discard(None)\n        return repre_ids\n\n    def _on_selection_change(self):\n        selected_repre_ids = self._get_selected_repre_ids()\n        self._controller.set_selected_representations(selected_repre_ids)\n\n    def _on_context_menu(self, point):\n        repre_ids = self._get_selected_repre_ids()\n        action_items = self._controller.get_representations_action_items(\n            self._selected_project_name, repre_ids\n        )\n        global_point = self._repre_view.mapToGlobal(point)\n        result = show_actions_menu(\n            action_items,\n            global_point,\n            len(repre_ids) == 1,\n            self\n        )\n        action_item, options = result\n        if action_item is None or options is None:\n            return\n\n        self._controller.trigger_action_item(\n            action_item.identifier,\n            options,\n            action_item.project_name,\n            version_ids=action_item.version_ids,\n            representation_ids=action_item.representation_ids,\n        )\n"
  },
  {
    "path": "openpype/tools/ayon_loader/ui/window.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.resources import get_openpype_icon_filepath\nfrom openpype.style import load_stylesheet\nfrom openpype.tools.utils import (\n    PlaceholderLineEdit,\n    ErrorMessageBox,\n    ThumbnailPainterWidget,\n    RefreshButton,\n    GoToCurrentButton,\n)\nfrom openpype.tools.utils.lib import center_window\nfrom openpype.tools.ayon_utils.widgets import ProjectsCombobox\nfrom openpype.tools.ayon_loader.control import LoaderController\n\nfrom .folders_widget import LoaderFoldersWidget\nfrom .products_widget import ProductsWidget\nfrom .product_types_widget import ProductTypesView\nfrom .product_group_dialog import ProductGroupDialog\nfrom .info_widget import InfoWidget\nfrom .repres_widget import RepresentationsWidget\n\n\nclass LoadErrorMessageBox(ErrorMessageBox):\n    def __init__(self, messages, parent=None):\n        self._messages = messages\n        super(LoadErrorMessageBox, self).__init__(\"Loading failed\", parent)\n\n    def _create_top_widget(self, parent_widget):\n        label_widget = QtWidgets.QLabel(parent_widget)\n        label_widget.setText(\n            \"<span style='font-size:18pt;'>Failed to load items</span>\"\n        )\n        return label_widget\n\n    def _get_report_data(self):\n        report_data = []\n        for exc_msg, tb_text, repre, product, version in self._messages:\n            report_message = (\n                \"During load error happened on Product: \\\"{product}\\\"\"\n                \" Representation: \\\"{repre}\\\" Version: {version}\"\n                \"\\n\\nError message: {message}\"\n            ).format(\n                product=product,\n                repre=repre,\n                version=version,\n                message=exc_msg\n            )\n            if tb_text:\n                report_message += \"\\n\\n{}\".format(tb_text)\n            report_data.append(report_message)\n        return report_data\n\n    def _create_content(self, content_layout):\n        item_name_template = (\n            \"<span style='font-weight:bold;'>Product:</span> {}<br>\"\n            \"<span style='font-weight:bold;'>Version:</span> {}<br>\"\n            \"<span style='font-weight:bold;'>Representation:</span> {}<br>\"\n        )\n        exc_msg_template = \"<span style='font-weight:bold'>{}</span>\"\n\n        for exc_msg, tb_text, repre, product, version in self._messages:\n            line = self._create_line()\n            content_layout.addWidget(line)\n\n            item_name = item_name_template.format(product, version, repre)\n            item_name_widget = QtWidgets.QLabel(\n                item_name.replace(\"\\n\", \"<br>\"), self\n            )\n            item_name_widget.setWordWrap(True)\n            content_layout.addWidget(item_name_widget)\n\n            exc_msg = exc_msg_template.format(exc_msg.replace(\"\\n\", \"<br>\"))\n            message_label_widget = QtWidgets.QLabel(exc_msg, self)\n            message_label_widget.setWordWrap(True)\n            content_layout.addWidget(message_label_widget)\n\n            if tb_text:\n                line = self._create_line()\n                tb_widget = self._create_traceback_widget(tb_text, self)\n                content_layout.addWidget(line)\n                content_layout.addWidget(tb_widget)\n\n\nclass RefreshHandler:\n    def __init__(self):\n        self._project_refreshed = False\n        self._folders_refreshed = False\n        self._products_refreshed = False\n\n    @property\n    def project_refreshed(self):\n        return self._products_refreshed\n\n    @property\n    def folders_refreshed(self):\n        return self._folders_refreshed\n\n    @property\n    def products_refreshed(self):\n        return self._products_refreshed\n\n    def reset(self):\n        self._project_refreshed = False\n        self._folders_refreshed = False\n        self._products_refreshed = False\n\n    def set_project_refreshed(self):\n        self._project_refreshed = True\n\n    def set_folders_refreshed(self):\n        self._folders_refreshed = True\n\n    def set_products_refreshed(self):\n        self._products_refreshed = True\n\n\nclass LoaderWindow(QtWidgets.QWidget):\n    def __init__(self, controller=None, parent=None):\n        super(LoaderWindow, self).__init__(parent)\n\n        icon = QtGui.QIcon(get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n        self.setWindowTitle(\"AYON Loader\")\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n        self.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)\n        self.setWindowFlags(self.windowFlags() | QtCore.Qt.Window)\n\n        if controller is None:\n            controller = LoaderController()\n\n        main_splitter = QtWidgets.QSplitter(self)\n\n        context_splitter = QtWidgets.QSplitter(main_splitter)\n        context_splitter.setOrientation(QtCore.Qt.Vertical)\n\n        # Context selection widget\n        context_widget = QtWidgets.QWidget(context_splitter)\n\n        context_top_widget = QtWidgets.QWidget(context_widget)\n        projects_combobox = ProjectsCombobox(\n            controller,\n            context_top_widget,\n            handle_expected_selection=True\n        )\n        projects_combobox.set_select_item_visible(True)\n        projects_combobox.set_libraries_separator_visible(True)\n        projects_combobox.set_standard_filter_enabled(\n            controller.is_standard_projects_filter_enabled()\n        )\n\n        go_to_current_btn = GoToCurrentButton(context_top_widget)\n        refresh_btn = RefreshButton(context_top_widget)\n\n        context_top_layout = QtWidgets.QHBoxLayout(context_top_widget)\n        context_top_layout.setContentsMargins(0, 0, 0, 0,)\n        context_top_layout.addWidget(projects_combobox, 1)\n        context_top_layout.addWidget(go_to_current_btn, 0)\n        context_top_layout.addWidget(refresh_btn, 0)\n\n        folders_filter_input = PlaceholderLineEdit(context_widget)\n        folders_filter_input.setPlaceholderText(\"Folder name filter...\")\n\n        folders_widget = LoaderFoldersWidget(controller, context_widget)\n\n        product_types_widget = ProductTypesView(controller, context_splitter)\n\n        context_layout = QtWidgets.QVBoxLayout(context_widget)\n        context_layout.setContentsMargins(0, 0, 0, 0)\n        context_layout.addWidget(context_top_widget, 0)\n        context_layout.addWidget(folders_filter_input, 0)\n        context_layout.addWidget(folders_widget, 1)\n\n        context_splitter.addWidget(context_widget)\n        context_splitter.addWidget(product_types_widget)\n        context_splitter.setStretchFactor(0, 65)\n        context_splitter.setStretchFactor(1, 35)\n\n        # Product + version selection item\n        products_wrap_widget = QtWidgets.QWidget(main_splitter)\n\n        products_inputs_widget = QtWidgets.QWidget(products_wrap_widget)\n\n        products_filter_input = PlaceholderLineEdit(products_inputs_widget)\n        products_filter_input.setPlaceholderText(\"Product name filter...\")\n        product_group_checkbox = QtWidgets.QCheckBox(\n            \"Enable grouping\", products_inputs_widget)\n        product_group_checkbox.setChecked(True)\n\n        products_widget = ProductsWidget(controller, products_wrap_widget)\n\n        products_inputs_layout = QtWidgets.QHBoxLayout(products_inputs_widget)\n        products_inputs_layout.setContentsMargins(0, 0, 0, 0)\n        products_inputs_layout.addWidget(products_filter_input, 1)\n        products_inputs_layout.addWidget(product_group_checkbox, 0)\n\n        products_wrap_layout = QtWidgets.QVBoxLayout(products_wrap_widget)\n        products_wrap_layout.setContentsMargins(0, 0, 0, 0)\n        products_wrap_layout.addWidget(products_inputs_widget, 0)\n        products_wrap_layout.addWidget(products_widget, 1)\n\n        right_panel_splitter = QtWidgets.QSplitter(main_splitter)\n        right_panel_splitter.setOrientation(QtCore.Qt.Vertical)\n\n        thumbnails_widget = ThumbnailPainterWidget(right_panel_splitter)\n        thumbnails_widget.set_use_checkboard(False)\n\n        info_widget = InfoWidget(controller, right_panel_splitter)\n\n        repre_widget = RepresentationsWidget(controller, right_panel_splitter)\n\n        right_panel_splitter.addWidget(thumbnails_widget)\n        right_panel_splitter.addWidget(info_widget)\n        right_panel_splitter.addWidget(repre_widget)\n\n        right_panel_splitter.setStretchFactor(0, 1)\n        right_panel_splitter.setStretchFactor(1, 1)\n        right_panel_splitter.setStretchFactor(2, 2)\n\n        main_splitter.addWidget(context_splitter)\n        main_splitter.addWidget(products_wrap_widget)\n        main_splitter.addWidget(right_panel_splitter)\n\n        main_splitter.setStretchFactor(0, 4)\n        main_splitter.setStretchFactor(1, 6)\n        main_splitter.setStretchFactor(2, 1)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.addWidget(main_splitter)\n\n        show_timer = QtCore.QTimer()\n        show_timer.setInterval(1)\n\n        show_timer.timeout.connect(self._on_show_timer)\n\n        projects_combobox.refreshed.connect(self._on_projects_refresh)\n        folders_widget.refreshed.connect(self._on_folders_refresh)\n        products_widget.refreshed.connect(self._on_products_refresh)\n        folders_filter_input.textChanged.connect(\n            self._on_folder_filter_change\n        )\n        product_types_widget.filter_changed.connect(\n            self._on_product_type_filter_change\n        )\n        products_filter_input.textChanged.connect(\n            self._on_product_filter_change\n        )\n        product_group_checkbox.stateChanged.connect(\n            self._on_product_group_change\n        )\n        products_widget.merged_products_selection_changed.connect(\n            self._on_merged_products_selection_change\n        )\n        products_widget.selection_changed.connect(\n            self._on_products_selection_change\n        )\n        go_to_current_btn.clicked.connect(\n            self._on_go_to_current_context_click\n        )\n        refresh_btn.clicked.connect(\n            self._on_refresh_click\n        )\n        controller.register_event_callback(\n            \"load.finished\",\n            self._on_load_finished,\n        )\n        controller.register_event_callback(\n            \"selection.project.changed\",\n            self._on_project_selection_changed,\n        )\n        controller.register_event_callback(\n            \"selection.folders.changed\",\n            self._on_folders_selection_changed,\n        )\n        controller.register_event_callback(\n            \"selection.versions.changed\",\n            self._on_versions_selection_changed,\n        )\n        controller.register_event_callback(\n            \"controller.reset.started\",\n            self._on_controller_reset_start,\n        )\n        controller.register_event_callback(\n            \"controller.reset.finished\",\n            self._on_controller_reset_finish,\n        )\n\n        self._group_dialog = ProductGroupDialog(controller, self)\n\n        self._main_splitter = main_splitter\n\n        self._go_to_current_btn = go_to_current_btn\n        self._refresh_btn = refresh_btn\n        self._projects_combobox = projects_combobox\n\n        self._folders_filter_input = folders_filter_input\n        self._folders_widget = folders_widget\n\n        self._product_types_widget = product_types_widget\n\n        self._products_filter_input = products_filter_input\n        self._product_group_checkbox = product_group_checkbox\n        self._products_widget = products_widget\n\n        self._right_panel_splitter = right_panel_splitter\n        self._thumbnails_widget = thumbnails_widget\n        self._info_widget = info_widget\n        self._repre_widget = repre_widget\n\n        self._controller = controller\n        self._refresh_handler = RefreshHandler()\n        self._first_show = True\n        self._reset_on_show = True\n        self._show_counter = 0\n        self._show_timer = show_timer\n        self._selected_project_name = None\n        self._selected_folder_ids = set()\n        self._selected_version_ids = set()\n\n        self._products_widget.set_enable_grouping(\n            self._product_group_checkbox.isChecked()\n        )\n\n    def refresh(self):\n        self._reset_on_show = False\n        self._controller.reset()\n\n    def showEvent(self, event):\n        super(LoaderWindow, self).showEvent(event)\n\n        if self._first_show:\n            self._on_first_show()\n\n        self._show_timer.start()\n\n    def closeEvent(self, event):\n        super(LoaderWindow, self).closeEvent(event)\n        # Deselect project so current context will be selected\n        #   on next 'showEvent'\n        self._controller.set_selected_project(None)\n        self._reset_on_show = True\n\n    def keyPressEvent(self, event):\n        modifiers = event.modifiers()\n        ctrl_pressed = QtCore.Qt.ControlModifier & modifiers\n\n        # Grouping products on pressing Ctrl + G\n        if (\n            ctrl_pressed\n            and event.key() == QtCore.Qt.Key_G\n            and not event.isAutoRepeat()\n        ):\n            self._show_group_dialog()\n            event.setAccepted(True)\n            return\n\n        super(LoaderWindow, self).keyPressEvent(event)\n\n    def _on_first_show(self):\n        self._first_show = False\n        # width, height = 1800, 900\n        width, height = 1500, 750\n\n        self.resize(width, height)\n\n        mid_width = int(width / 1.8)\n        sides_width = int((width - mid_width) * 0.5)\n        self._main_splitter.setSizes(\n            [sides_width, mid_width, sides_width]\n        )\n\n        thumbnail_height = int(height / 3.6)\n        info_height = int((height - thumbnail_height) * 0.5)\n        self._right_panel_splitter.setSizes(\n            [thumbnail_height, info_height, info_height]\n        )\n        self.setStyleSheet(load_stylesheet())\n        center_window(self)\n\n    def _on_show_timer(self):\n        if self._show_counter < 2:\n            self._show_counter += 1\n            return\n\n        self._show_counter = 0\n        self._show_timer.stop()\n\n        if self._reset_on_show:\n            self.refresh()\n\n    def _show_group_dialog(self):\n        project_name = self._projects_combobox.get_selected_project_name()\n        if not project_name:\n            return\n\n        product_ids = {\n            i[\"product_id\"]\n            for i in self._products_widget.get_selected_version_info()\n        }\n        if not product_ids:\n            return\n\n        self._group_dialog.set_product_ids(project_name, product_ids)\n        self._group_dialog.show()\n\n    def _on_folder_filter_change(self, text):\n        self._folders_widget.set_name_filter(text)\n\n    def _on_product_group_change(self):\n        self._products_widget.set_enable_grouping(\n            self._product_group_checkbox.isChecked()\n        )\n\n    def _on_product_filter_change(self, text):\n        self._products_widget.set_name_filter(text)\n\n    def _on_product_type_filter_change(self):\n        self._products_widget.set_product_type_filter(\n            self._product_types_widget.get_filter_info()\n        )\n\n    def _on_merged_products_selection_change(self):\n        items = self._products_widget.get_selected_merged_products()\n        self._folders_widget.set_merged_products_selection(items)\n\n    def _on_products_selection_change(self):\n        items = self._products_widget.get_selected_version_info()\n        self._info_widget.set_selected_version_info(\n            self._projects_combobox.get_selected_project_name(),\n            items\n        )\n\n    def _on_go_to_current_context_click(self):\n        context = self._controller.get_current_context()\n        self._controller.set_expected_selection(\n            context[\"project_name\"],\n            context[\"folder_id\"],\n        )\n\n    def _on_refresh_click(self):\n        self._controller.reset()\n\n    def _on_controller_reset_start(self):\n        self._refresh_handler.reset()\n\n    def _on_controller_reset_finish(self):\n        context = self._controller.get_current_context()\n        project_name = context[\"project_name\"]\n        self._go_to_current_btn.setVisible(bool(project_name))\n        self._projects_combobox.set_current_context_project(project_name)\n        if not self._refresh_handler.project_refreshed:\n            self._projects_combobox.refresh()\n\n    def _on_load_finished(self, event):\n        error_info = event[\"error_info\"]\n        if not error_info:\n            return\n\n        box = LoadErrorMessageBox(error_info, self)\n        box.show()\n\n    def _on_project_selection_changed(self, event):\n        self._selected_project_name = event[\"project_name\"]\n\n    def _on_folders_selection_changed(self, event):\n        self._selected_folder_ids = set(event[\"folder_ids\"])\n        self._update_thumbnails()\n\n    def _on_versions_selection_changed(self, event):\n        self._selected_version_ids = set(event[\"version_ids\"])\n        self._update_thumbnails()\n\n    def _update_thumbnails(self):\n        project_name = self._selected_project_name\n        thumbnail_ids = set()\n        if self._selected_version_ids:\n            thumbnail_id_by_entity_id = (\n                self._controller.get_version_thumbnail_ids(\n                    project_name,\n                    self._selected_version_ids\n                )\n            )\n            thumbnail_ids = set(thumbnail_id_by_entity_id.values())\n        elif self._selected_folder_ids:\n            thumbnail_id_by_entity_id = (\n                self._controller.get_folder_thumbnail_ids(\n                    project_name,\n                    self._selected_folder_ids\n                )\n            )\n            thumbnail_ids = set(thumbnail_id_by_entity_id.values())\n\n        thumbnail_ids.discard(None)\n\n        if not thumbnail_ids:\n            self._thumbnails_widget.set_current_thumbnails(None)\n            return\n\n        thumbnail_paths = set()\n        for thumbnail_id in thumbnail_ids:\n            thumbnail_path = self._controller.get_thumbnail_path(\n                project_name, thumbnail_id)\n            thumbnail_paths.add(thumbnail_path)\n        thumbnail_paths.discard(None)\n        self._thumbnails_widget.set_current_thumbnail_paths(thumbnail_paths)\n\n    def _on_projects_refresh(self):\n        self._refresh_handler.set_project_refreshed()\n        if not self._refresh_handler.folders_refreshed:\n            self._folders_widget.refresh()\n\n    def _on_folders_refresh(self):\n        self._refresh_handler.set_folders_refreshed()\n        if not self._refresh_handler.products_refreshed:\n            self._products_widget.refresh()\n\n    def _on_products_refresh(self):\n        self._refresh_handler.set_products_refreshed()\n"
  },
  {
    "path": "openpype/tools/ayon_push_to_project/__init__.py",
    "content": "from .control import PushToContextController\n\n\n__all__ = (\n    \"PushToContextController\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_push_to_project/control.py",
    "content": "import threading\n\nfrom openpype.client import (\n    get_asset_by_id,\n    get_subset_by_id,\n    get_version_by_id,\n    get_representations,\n)\nfrom openpype.settings import get_project_settings\nfrom openpype.lib import prepare_template_data\nfrom openpype.lib.events import QueuedEventSystem\nfrom openpype.pipeline.create import get_subset_name_template\nfrom openpype.tools.ayon_utils.models import ProjectsModel, HierarchyModel\n\nfrom .models import (\n    PushToProjectSelectionModel,\n    UserPublishValuesModel,\n    IntegrateModel,\n)\n\n\nclass PushToContextController:\n    def __init__(self, project_name=None, version_id=None):\n        self._event_system = self._create_event_system()\n\n        self._projects_model = ProjectsModel(self)\n        self._hierarchy_model = HierarchyModel(self)\n        self._integrate_model = IntegrateModel(self)\n\n        self._selection_model = PushToProjectSelectionModel(self)\n        self._user_values = UserPublishValuesModel(self)\n\n        self._src_project_name = None\n        self._src_version_id = None\n        self._src_asset_doc = None\n        self._src_subset_doc = None\n        self._src_version_doc = None\n        self._src_label = None\n\n        self._submission_enabled = False\n        self._process_thread = None\n        self._process_item_id = None\n\n        self.set_source(project_name, version_id)\n\n    # Events system\n    def emit_event(self, topic, data=None, source=None):\n        \"\"\"Use implemented event system to trigger event.\"\"\"\n\n        if data is None:\n            data = {}\n        self._event_system.emit(topic, data, source)\n\n    def register_event_callback(self, topic, callback):\n        self._event_system.add_callback(topic, callback)\n\n    def set_source(self, project_name, version_id):\n        \"\"\"Set source project and version.\n\n        Args:\n            project_name (Union[str, None]): Source project name.\n            version_id (Union[str, None]): Source version id.\n        \"\"\"\n\n        if (\n            project_name == self._src_project_name\n            and version_id == self._src_version_id\n        ):\n            return\n\n        self._src_project_name = project_name\n        self._src_version_id = version_id\n        self._src_label = None\n        asset_doc = None\n        subset_doc = None\n        version_doc = None\n        if project_name and version_id:\n            version_doc = get_version_by_id(project_name, version_id)\n\n        if version_doc:\n            subset_doc = get_subset_by_id(project_name, version_doc[\"parent\"])\n\n        if subset_doc:\n            asset_doc = get_asset_by_id(project_name, subset_doc[\"parent\"])\n\n        self._src_asset_doc = asset_doc\n        self._src_subset_doc = subset_doc\n        self._src_version_doc = version_doc\n        if asset_doc:\n            self._user_values.set_new_folder_name(asset_doc[\"name\"])\n            variant = self._get_src_variant()\n            if variant:\n                self._user_values.set_variant(variant)\n\n            comment = version_doc[\"data\"].get(\"comment\")\n            if comment:\n                self._user_values.set_comment(comment)\n\n        self._emit_event(\n            \"source.changed\",\n            {\n                \"project_name\": project_name,\n                \"version_id\": version_id\n            }\n        )\n\n    def get_source_label(self):\n        \"\"\"Get source label.\n\n        Returns:\n            str: Label describing source project and version as path.\n        \"\"\"\n\n        if self._src_label is None:\n            self._src_label = self._prepare_source_label()\n        return self._src_label\n\n    def get_project_items(self, sender=None):\n        return self._projects_model.get_project_items(sender)\n\n    def get_folder_items(self, project_name, sender=None):\n        return self._hierarchy_model.get_folder_items(project_name, sender)\n\n    def get_task_items(self, project_name, folder_id, sender=None):\n        return self._hierarchy_model.get_task_items(\n            project_name, folder_id, sender\n        )\n\n    def get_user_values(self):\n        return self._user_values.get_data()\n\n    def set_user_value_folder_name(self, folder_name):\n        self._user_values.set_new_folder_name(folder_name)\n        self._invalidate()\n\n    def set_user_value_variant(self, variant):\n        self._user_values.set_variant(variant)\n        self._invalidate()\n\n    def set_user_value_comment(self, comment):\n        self._user_values.set_comment(comment)\n        self._invalidate()\n\n    def set_selected_project(self, project_name):\n        self._selection_model.set_selected_project(project_name)\n        self._invalidate()\n\n    def set_selected_folder(self, folder_id):\n        self._selection_model.set_selected_folder(folder_id)\n        self._invalidate()\n\n    def set_selected_task(self, task_id, task_name):\n        self._selection_model.set_selected_task(task_id, task_name)\n\n    def get_process_item_status(self, item_id):\n        return self._integrate_model.get_item_status(item_id)\n\n    # Processing methods\n    def submit(self, wait=True):\n        if not self._submission_enabled:\n            return\n\n        if self._process_thread is not None:\n            return\n\n        item_id = self._integrate_model.create_process_item(\n            self._src_project_name,\n            self._src_version_id,\n            self._selection_model.get_selected_project_name(),\n            self._selection_model.get_selected_folder_id(),\n            self._selection_model.get_selected_task_name(),\n            self._user_values.variant,\n            comment=self._user_values.comment,\n            new_folder_name=self._user_values.new_folder_name,\n            dst_version=1\n        )\n\n        self._process_item_id = item_id\n        self._emit_event(\"submit.started\")\n        if wait:\n            self._submit_callback()\n            self._process_item_id = None\n            return item_id\n\n        thread = threading.Thread(target=self._submit_callback)\n        self._process_thread = thread\n        thread.start()\n        return item_id\n\n    def wait_for_process_thread(self):\n        if self._process_thread is None:\n            return\n        self._process_thread.join()\n        self._process_thread = None\n\n    def _prepare_source_label(self):\n        if not self._src_project_name or not self._src_version_id:\n            return \"Source is not defined\"\n\n        asset_doc = self._src_asset_doc\n        if not asset_doc:\n            return \"Source is invalid\"\n\n        folder_path_parts = list(asset_doc[\"data\"][\"parents\"])\n        folder_path_parts.append(asset_doc[\"name\"])\n        folder_path = \"/\".join(folder_path_parts)\n        subset_doc = self._src_subset_doc\n        version_doc = self._src_version_doc\n        return \"Source: {}/{}/{}/v{:0>3}\".format(\n            self._src_project_name,\n            folder_path,\n            subset_doc[\"name\"],\n            version_doc[\"name\"]\n        )\n\n    def _get_task_info_from_repre_docs(self, asset_doc, repre_docs):\n        asset_tasks = asset_doc[\"data\"].get(\"tasks\") or {}\n        found_comb = []\n        for repre_doc in repre_docs:\n            context = repre_doc[\"context\"]\n            task_info = context.get(\"task\")\n            if task_info is None:\n                continue\n\n            task_name = None\n            task_type = None\n            if isinstance(task_info, str):\n                task_name = task_info\n                asset_task_info = asset_tasks.get(task_info) or {}\n                task_type = asset_task_info.get(\"type\")\n\n            elif isinstance(task_info, dict):\n                task_name = task_info.get(\"name\")\n                task_type = task_info.get(\"type\")\n\n            if task_name and task_type:\n                return task_name, task_type\n\n            if task_name:\n                found_comb.append((task_name, task_type))\n\n        for task_name, task_type in found_comb:\n            return task_name, task_type\n        return None, None\n\n    def _get_src_variant(self):\n        project_name = self._src_project_name\n        version_doc = self._src_version_doc\n        asset_doc = self._src_asset_doc\n        repre_docs = get_representations(\n            project_name, version_ids=[version_doc[\"_id\"]]\n        )\n        task_name, task_type = self._get_task_info_from_repre_docs(\n            asset_doc, repre_docs\n        )\n\n        project_settings = get_project_settings(project_name)\n        subset_doc = self._src_subset_doc\n        family = subset_doc[\"data\"].get(\"family\")\n        if not family:\n            family = subset_doc[\"data\"][\"families\"][0]\n        template = get_subset_name_template(\n            self._src_project_name,\n            family,\n            task_name,\n            task_type,\n            None,\n            project_settings=project_settings\n        )\n        template_low = template.lower()\n        variant_placeholder = \"{variant}\"\n        if (\n            variant_placeholder not in template_low\n            or (not task_name and \"{task\" in template_low)\n        ):\n            return \"\"\n\n        idx = template_low.index(variant_placeholder)\n        template_s = template[:idx]\n        template_e = template[idx + len(variant_placeholder):]\n        fill_data = prepare_template_data({\n            \"family\": family,\n            \"task\": task_name\n        })\n        try:\n            subset_s = template_s.format(**fill_data)\n            subset_e = template_e.format(**fill_data)\n        except Exception as exc:\n            print(\"Failed format\", exc)\n            return \"\"\n\n        subset_name = self._src_subset_doc[\"name\"]\n        if (\n            (subset_s and not subset_name.startswith(subset_s))\n            or (subset_e and not subset_name.endswith(subset_e))\n        ):\n            return \"\"\n\n        if subset_s:\n            subset_name = subset_name[len(subset_s):]\n        if subset_e:\n            subset_name = subset_name[:len(subset_e)]\n        return subset_name\n\n    def _check_submit_validations(self):\n        if not self._user_values.is_valid:\n            return False\n\n        if not self._selection_model.get_selected_project_name():\n            return False\n\n        if (\n            not self._user_values.new_folder_name\n            and not self._selection_model.get_selected_folder_id()\n        ):\n            return False\n        return True\n\n    def _invalidate(self):\n        submission_enabled = self._check_submit_validations()\n        if submission_enabled == self._submission_enabled:\n            return\n        self._submission_enabled = submission_enabled\n        self._emit_event(\n            \"submission.enabled.changed\",\n            {\"enabled\": submission_enabled}\n        )\n\n    def _submit_callback(self):\n        process_item_id = self._process_item_id\n        if process_item_id is None:\n            return\n        self._integrate_model.integrate_item(process_item_id)\n        self._emit_event(\"submit.finished\", {})\n        if process_item_id == self._process_item_id:\n            self._process_item_id = None\n\n    def _emit_event(self, topic, data=None):\n        if data is None:\n            data = {}\n        self.emit_event(topic, data, \"controller\")\n\n    def _create_event_system(self):\n        return QueuedEventSystem()\n"
  },
  {
    "path": "openpype/tools/ayon_push_to_project/main.py",
    "content": "import click\n\nfrom openpype.tools.utils import get_openpype_qt_app\nfrom openpype.tools.ayon_push_to_project.ui import PushToContextSelectWindow\n\n\ndef main_show(project_name, version_id):\n    app = get_openpype_qt_app()\n\n    window = PushToContextSelectWindow()\n    window.show()\n    window.set_source(project_name, version_id)\n\n    app.exec_()\n\n\n@click.command()\n@click.option(\"--project\", help=\"Source project name\")\n@click.option(\"--version\", help=\"Source version id\")\ndef main(project, version):\n    \"\"\"Run PushToProject tool to integrate version in different project.\n\n    Args:\n        project (str): Source project name.\n        version (str): Version id.\n    \"\"\"\n\n    main_show(project, version)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "openpype/tools/ayon_push_to_project/models/__init__.py",
    "content": "from .selection import PushToProjectSelectionModel\nfrom .user_values import UserPublishValuesModel\nfrom .integrate import IntegrateModel\n\n\n__all__ = (\n    \"PushToProjectSelectionModel\",\n    \"UserPublishValuesModel\",\n    \"IntegrateModel\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_push_to_project/models/integrate.py",
    "content": "import os\nimport re\nimport copy\nimport socket\nimport itertools\nimport datetime\nimport sys\nimport traceback\nimport uuid\n\nfrom bson.objectid import ObjectId\n\nfrom openpype.client import (\n    get_project,\n    get_assets,\n    get_asset_by_id,\n    get_subset_by_id,\n    get_subset_by_name,\n    get_version_by_id,\n    get_last_version_by_subset_id,\n    get_version_by_name,\n    get_representations,\n)\nfrom openpype.client.operations import (\n    OperationsSession,\n    new_asset_document,\n    new_subset_document,\n    new_version_doc,\n    new_representation_doc,\n    prepare_version_update_data,\n    prepare_representation_update_data,\n)\nfrom openpype.modules import ModulesManager\nfrom openpype.lib import (\n    StringTemplate,\n    get_openpype_username,\n    get_formatted_current_time,\n    source_hash,\n)\n\nfrom openpype.lib.file_transaction import FileTransaction\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import Anatomy\nfrom openpype.pipeline.version_start import get_versioning_start\nfrom openpype.pipeline.template_data import get_template_data\nfrom openpype.pipeline.publish import get_publish_template_name\nfrom openpype.pipeline.create import get_subset_name\n\nUNKNOWN = object()\n\n\nclass PushToProjectError(Exception):\n    pass\n\n\nclass FileItem(object):\n    def __init__(self, path):\n        self.path = path\n\n    @property\n    def is_valid_file(self):\n        return os.path.exists(self.path) and os.path.isfile(self.path)\n\n\nclass SourceFile(FileItem):\n    def __init__(self, path, frame=None, udim=None):\n        super(SourceFile, self).__init__(path)\n        self.frame = frame\n        self.udim = udim\n\n    def __repr__(self):\n        subparts = [self.__class__.__name__]\n        if self.frame is not None:\n            subparts.append(\"frame: {}\".format(self.frame))\n        if self.udim is not None:\n            subparts.append(\"UDIM: {}\".format(self.udim))\n\n        return \"<{}> '{}'\".format(\" - \".join(subparts), self.path)\n\n\nclass ResourceFile(FileItem):\n    def __init__(self, path, relative_path):\n        super(ResourceFile, self).__init__(path)\n        self.relative_path = relative_path\n\n    def __repr__(self):\n        return \"<{}> '{}'\".format(self.__class__.__name__, self.relative_path)\n\n    @property\n    def is_valid_file(self):\n        if not self.relative_path:\n            return False\n        return super(ResourceFile, self).is_valid_file\n\n\nclass ProjectPushItem:\n    def __init__(\n        self,\n        src_project_name,\n        src_version_id,\n        dst_project_name,\n        dst_folder_id,\n        dst_task_name,\n        variant,\n        comment,\n        new_folder_name,\n        dst_version,\n        item_id=None,\n    ):\n        if not item_id:\n            item_id = uuid.uuid4().hex\n        self.src_project_name = src_project_name\n        self.src_version_id = src_version_id\n        self.dst_project_name = dst_project_name\n        self.dst_folder_id = dst_folder_id\n        self.dst_task_name = dst_task_name\n        self.dst_version = dst_version\n        self.variant = variant\n        self.new_folder_name = new_folder_name\n        self.comment = comment or \"\"\n        self.item_id = item_id\n        self._repr_value = None\n\n    @property\n    def _repr(self):\n        if not self._repr_value:\n            self._repr_value = \"|\".join([\n                self.src_project_name,\n                self.src_version_id,\n                self.dst_project_name,\n                str(self.dst_folder_id),\n                str(self.new_folder_name),\n                str(self.dst_task_name),\n                str(self.dst_version)\n            ])\n        return self._repr_value\n\n    def __repr__(self):\n        return \"<{} - {}>\".format(self.__class__.__name__, self._repr)\n\n    def to_data(self):\n        return {\n            \"src_project_name\": self.src_project_name,\n            \"src_version_id\": self.src_version_id,\n            \"dst_project_name\": self.dst_project_name,\n            \"dst_folder_id\": self.dst_folder_id,\n            \"dst_task_name\": self.dst_task_name,\n            \"dst_version\": self.dst_version,\n            \"variant\": self.variant,\n            \"comment\": self.comment,\n            \"new_folder_name\": self.new_folder_name,\n            \"item_id\": self.item_id,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        return cls(**data)\n\n\nclass StatusMessage:\n    def __init__(self, message, level):\n        self.message = message\n        self.level = level\n\n    def __str__(self):\n        return \"{}: {}\".format(self.level.upper(), self.message)\n\n    def __repr__(self):\n        return \"<{} - {}> {}\".format(\n            self.__class__.__name__, self.level.upper, self.message\n        )\n\n\nclass ProjectPushItemStatus:\n    def __init__(\n        self,\n        started=False,\n        failed=False,\n        finished=False,\n        fail_reason=None,\n        full_traceback=None\n    ):\n        self.started = started\n        self.failed = failed\n        self.finished = finished\n        self.fail_reason = fail_reason\n        self.full_traceback = full_traceback\n\n    def set_failed(self, fail_reason, exc_info=None):\n        \"\"\"Set status as failed.\n\n        Attribute 'fail_reason' can change automatically based on passed value.\n        Reason is unset if 'failed' is 'False' and is set do default reason if\n        is set to 'True' and reason is not set.\n\n        Args:\n            fail_reason (str): Reason why failed.\n            exc_info(tuple): Exception info.\n        \"\"\"\n\n        failed = True\n        if not fail_reason and not exc_info:\n            failed = False\n\n        full_traceback = None\n        if exc_info is not None:\n            full_traceback = \"\".join(traceback.format_exception(*exc_info))\n            if not fail_reason:\n                fail_reason = \"Failed without specified reason\"\n\n        self.failed = failed\n        self.fail_reason = fail_reason or None\n        self.full_traceback = full_traceback\n\n    def to_data(self):\n        return {\n            \"started\": self.started,\n            \"failed\": self.failed,\n            \"finished\": self.finished,\n            \"fail_reason\": self.fail_reason,\n            \"full_traceback\": self.full_traceback,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        return cls(**data)\n\n\nclass ProjectPushRepreItem:\n    \"\"\"Representation item.\n\n    Representation item based on representation document and project roots.\n\n    Representation document may have reference to:\n    - source files: Files defined with publish template\n    - resource files: Files that should be in publish directory\n        but filenames are not template based.\n\n    Args:\n        repre_doc (Dict[str, Ant]): Representation document.\n        roots (Dict[str, str]): Project roots (based on project anatomy).\n    \"\"\"\n\n    def __init__(self, repre_doc, roots):\n        self._repre_doc = repre_doc\n        self._roots = roots\n        self._src_files = None\n        self._resource_files = None\n        self._frame = UNKNOWN\n\n    @property\n    def repre_doc(self):\n        return self._repre_doc\n\n    @property\n    def src_files(self):\n        if self._src_files is None:\n            self.get_source_files()\n        return self._src_files\n\n    @property\n    def resource_files(self):\n        if self._resource_files is None:\n            self.get_source_files()\n        return self._resource_files\n\n    @staticmethod\n    def _clean_path(path):\n        new_value = path.replace(\"\\\\\", \"/\")\n        while \"//\" in new_value:\n            new_value = new_value.replace(\"//\", \"/\")\n        return new_value\n\n    @staticmethod\n    def _get_relative_path(path, src_dirpath):\n        dirpath, basename = os.path.split(path)\n        if not dirpath.lower().startswith(src_dirpath.lower()):\n            return None\n\n        relative_dir = dirpath[len(src_dirpath):].lstrip(\"/\")\n        if relative_dir:\n            relative_path = \"/\".join([relative_dir, basename])\n        else:\n            relative_path = basename\n        return relative_path\n\n    @property\n    def frame(self):\n        \"\"\"First frame of representation files.\n\n        This value will be in representation document context if is sequence.\n\n        Returns:\n            Union[int, None]: First frame in representation files based on\n                source files or None if frame is not part of filename.\n        \"\"\"\n\n        if self._frame is UNKNOWN:\n            frame = None\n            for src_file in self.src_files:\n                src_frame = src_file.frame\n                if (\n                    src_frame is not None\n                    and (frame is None or src_frame < frame)\n                ):\n                    frame = src_frame\n            self._frame = frame\n        return self._frame\n\n    @staticmethod\n    def validate_source_files(src_files, resource_files):\n        if not src_files:\n            raise AssertionError((\n                \"Couldn't figure out source files from representation.\"\n                \" Found resource files {}\"\n            ).format(\", \".join(str(i) for i in resource_files)))\n\n        invalid_items = [\n            item\n            for item in itertools.chain(src_files, resource_files)\n            if not item.is_valid_file\n        ]\n        if invalid_items:\n            raise AssertionError((\n                \"Source files that were not found on disk: {}\"\n            ).format(\", \".join(str(i) for i in invalid_items)))\n\n    def get_source_files(self):\n        if self._src_files is not None:\n            return self._src_files, self._resource_files\n\n        repre_context = self._repre_doc[\"context\"]\n        if \"frame\" in repre_context or \"udim\" in repre_context:\n            src_files, resource_files = self._get_source_files_with_frames()\n        else:\n            src_files, resource_files = self._get_source_files()\n\n        self.validate_source_files(src_files, resource_files)\n\n        self._src_files = src_files\n        self._resource_files = resource_files\n        return self._src_files, self._resource_files\n\n    def _get_source_files_with_frames(self):\n        frame_placeholder = \"__frame__\"\n        udim_placeholder = \"__udim__\"\n        src_files = []\n        resource_files = []\n        template = self._repre_doc[\"data\"][\"template\"]\n        # Remove padding from 'udim' and 'frame' formatting keys\n        # - \"{frame:0>4}\" -> \"{frame}\"\n        for key in (\"udim\", \"frame\"):\n            sub_part = \"{\" + key + \"[^}]*}\"\n            replacement = \"{{{}}}\".format(key)\n            template = re.sub(sub_part, replacement, template)\n\n        repre_context = self._repre_doc[\"context\"]\n        fill_repre_context = copy.deepcopy(repre_context)\n        if \"frame\" in fill_repre_context:\n            fill_repre_context[\"frame\"] = frame_placeholder\n\n        if \"udim\" in fill_repre_context:\n            fill_repre_context[\"udim\"] = udim_placeholder\n\n        fill_roots = fill_repre_context[\"root\"]\n        for root_name in tuple(fill_roots.keys()):\n            fill_roots[root_name] = \"{{root[{}]}}\".format(root_name)\n        repre_path = StringTemplate.format_template(\n            template, fill_repre_context)\n        repre_path = self._clean_path(repre_path)\n        src_dirpath, src_basename = os.path.split(repre_path)\n        src_basename = (\n            re.escape(src_basename)\n            .replace(frame_placeholder, \"(?P<frame>[0-9]+)\")\n            .replace(udim_placeholder, \"(?P<udim>[0-9]+)\")\n        )\n        src_basename_regex = re.compile(\"^{}$\".format(src_basename))\n        for file_info in self._repre_doc[\"files\"]:\n            filepath_template = self._clean_path(file_info[\"path\"])\n            filepath = self._clean_path(\n                filepath_template.format(root=self._roots)\n            )\n            dirpath, basename = os.path.split(filepath_template)\n            if (\n                dirpath.lower() != src_dirpath.lower()\n                or not src_basename_regex.match(basename)\n            ):\n                relative_path = self._get_relative_path(filepath, src_dirpath)\n                resource_files.append(ResourceFile(filepath, relative_path))\n                continue\n\n            filepath = os.path.join(src_dirpath, basename)\n            frame = None\n            udim = None\n            for item in src_basename_regex.finditer(basename):\n                group_name = item.lastgroup\n                value = item.group(group_name)\n                if group_name == \"frame\":\n                    frame = int(value)\n                elif group_name == \"udim\":\n                    udim = value\n\n            src_files.append(SourceFile(filepath, frame, udim))\n\n        return src_files, resource_files\n\n    def _get_source_files(self):\n        src_files = []\n        resource_files = []\n        template = self._repre_doc[\"data\"][\"template\"]\n        repre_context = self._repre_doc[\"context\"]\n        fill_repre_context = copy.deepcopy(repre_context)\n        fill_roots = fill_repre_context[\"root\"]\n        for root_name in tuple(fill_roots.keys()):\n            fill_roots[root_name] = \"{{root[{}]}}\".format(root_name)\n        repre_path = StringTemplate.format_template(template,\n                                                    fill_repre_context)\n        repre_path = self._clean_path(repre_path)\n        src_dirpath = os.path.dirname(repre_path)\n        for file_info in self._repre_doc[\"files\"]:\n            filepath_template = self._clean_path(file_info[\"path\"])\n            filepath = self._clean_path(\n                filepath_template.format(root=self._roots))\n\n            if filepath_template.lower() == repre_path.lower():\n                src_files.append(\n                    SourceFile(repre_path.format(root=self._roots))\n                )\n            else:\n                relative_path = self._get_relative_path(\n                    filepath_template, src_dirpath\n                )\n                resource_files.append(\n                    ResourceFile(filepath, relative_path)\n                )\n        return src_files, resource_files\n\n\nclass ProjectPushItemProcess:\n    \"\"\"\n    Args:\n        model (IntegrateModel): Model which is processing item.\n        item (ProjectPushItem): Item which is being processed.\n    \"\"\"\n\n    # TODO where to get host?!!!\n    host_name = \"republisher\"\n\n    def __init__(self, model, item):\n        self._model = model\n        self._item = item\n\n        self._src_asset_doc = None\n        self._src_subset_doc = None\n        self._src_version_doc = None\n        self._src_repre_items = None\n\n        self._project_doc = None\n        self._anatomy = None\n        self._asset_doc = None\n        self._created_asset_doc = None\n        self._task_info = None\n        self._subset_doc = None\n        self._version_doc = None\n\n        self._family = None\n        self._subset_name = None\n\n        self._project_settings = None\n        self._template_name = None\n\n        self._status = ProjectPushItemStatus()\n        self._operations = OperationsSession()\n        self._file_transaction = FileTransaction()\n\n        self._messages = []\n\n    @property\n    def item_id(self):\n        return self._item.item_id\n\n    @property\n    def started(self):\n        return self._status.started\n\n    def get_status_data(self):\n        return self._status.to_data()\n\n    def integrate(self):\n        self._status.started = True\n        try:\n            self._log_info(\"Process started\")\n            self._fill_source_variables()\n            self._log_info(\"Source entities were found\")\n            self._fill_destination_project()\n            self._log_info(\"Destination project was found\")\n            self._fill_or_create_destination_asset()\n            self._log_info(\"Destination asset was determined\")\n            self._determine_family()\n            self._determine_publish_template_name()\n            self._determine_subset_name()\n            self._make_sure_subset_exists()\n            self._make_sure_version_exists()\n            self._log_info(\"Prerequirements were prepared\")\n            self._integrate_representations()\n            self._log_info(\"Integration finished\")\n\n        except PushToProjectError as exc:\n            if not self._status.failed:\n                self._status.set_failed(str(exc))\n\n        except Exception as exc:\n            _exc, _value, _tb = sys.exc_info()\n            self._status.set_failed(\n                \"Unhandled error happened: {}\".format(str(exc)),\n                (_exc, _value, _tb)\n            )\n\n        finally:\n            self._status.finished = True\n            self._emit_event(\n                \"push.finished.changed\",\n                {\n                    \"finished\": True,\n                    \"item_id\": self.item_id,\n                }\n            )\n\n    def _emit_event(self, topic, data):\n        self._model.emit_event(topic, data)\n\n    # Loggin helpers\n    # TODO better logging\n    def _add_message(self, message, level):\n        message_obj = StatusMessage(message, level)\n        self._messages.append(message_obj)\n        self._emit_event(\n            \"push.message.added\",\n            {\n                \"message\": message,\n                \"level\": level,\n                \"item_id\": self.item_id,\n            }\n        )\n        print(message_obj)\n        return message_obj\n\n    def _log_debug(self, message):\n        return self._add_message(message, \"debug\")\n\n    def _log_info(self, message):\n        return self._add_message(message, \"info\")\n\n    def _log_warning(self, message):\n        return self._add_message(message, \"warning\")\n\n    def _log_error(self, message):\n        return self._add_message(message, \"error\")\n\n    def _log_critical(self, message):\n        return self._add_message(message, \"critical\")\n\n    def _fill_source_variables(self):\n        src_project_name = self._item.src_project_name\n        src_version_id = self._item.src_version_id\n\n        project_doc = get_project(src_project_name)\n        if not project_doc:\n            self._status.set_failed(\n                f\"Source project \\\"{src_project_name}\\\" was not found\"\n            )\n\n            self._emit_event(\n                \"push.failed.changed\",\n                {\"item_id\": self.item_id}\n            )\n            raise PushToProjectError(self._status.fail_reason)\n\n        self._log_debug(f\"Project '{src_project_name}' found\")\n\n        version_doc = get_version_by_id(src_project_name, src_version_id)\n        if not version_doc:\n            self._status.set_failed((\n                f\"Source version with id \\\"{src_version_id}\\\"\"\n                f\" was not found in project \\\"{src_project_name}\\\"\"\n            ))\n            raise PushToProjectError(self._status.fail_reason)\n\n        subset_id = version_doc[\"parent\"]\n        subset_doc = get_subset_by_id(src_project_name, subset_id)\n        if not subset_doc:\n            self._status.set_failed((\n                f\"Could find subset with id \\\"{subset_id}\\\"\"\n                f\" in project \\\"{src_project_name}\\\"\"\n            ))\n            raise PushToProjectError(self._status.fail_reason)\n\n        asset_id = subset_doc[\"parent\"]\n        asset_doc = get_asset_by_id(src_project_name, asset_id)\n        if not asset_doc:\n            self._status.set_failed((\n                f\"Could find asset with id \\\"{asset_id}\\\"\"\n                f\" in project \\\"{src_project_name}\\\"\"\n            ))\n            raise PushToProjectError(self._status.fail_reason)\n\n        anatomy = Anatomy(src_project_name)\n\n        repre_docs = get_representations(\n            src_project_name,\n            version_ids=[src_version_id]\n        )\n        repre_items = [\n            ProjectPushRepreItem(repre_doc, anatomy.roots)\n            for repre_doc in repre_docs\n        ]\n        self._log_debug((\n            f\"Found {len(repre_items)} representations on\"\n            f\" version {src_version_id} in project '{src_project_name}'\"\n        ))\n        if not repre_items:\n            self._status.set_failed(\n                \"Source version does not have representations\"\n                f\" (Version id: {src_version_id})\"\n            )\n            raise PushToProjectError(self._status.fail_reason)\n\n        self._src_asset_doc = asset_doc\n        self._src_subset_doc = subset_doc\n        self._src_version_doc = version_doc\n        self._src_repre_items = repre_items\n\n    def _fill_destination_project(self):\n        # --- Destination entities ---\n        dst_project_name = self._item.dst_project_name\n        # Validate project existence\n        dst_project_doc = get_project(dst_project_name)\n        if not dst_project_doc:\n            self._status.set_failed(\n                f\"Destination project '{dst_project_name}' was not found\"\n            )\n            raise PushToProjectError(self._status.fail_reason)\n\n        self._log_debug(\n            f\"Destination project '{dst_project_name}' found\"\n        )\n        self._project_doc = dst_project_doc\n        self._anatomy = Anatomy(dst_project_name)\n        self._project_settings = get_project_settings(\n            self._item.dst_project_name\n        )\n\n    def _create_asset(\n        self,\n        src_asset_doc,\n        project_doc,\n        parent_asset_doc,\n        asset_name\n    ):\n        parent_id = None\n        parents = []\n        tools = []\n        if parent_asset_doc:\n            parent_id = parent_asset_doc[\"_id\"]\n            parents = list(parent_asset_doc[\"data\"][\"parents\"])\n            parents.append(parent_asset_doc[\"name\"])\n            _tools = parent_asset_doc[\"data\"].get(\"tools_env\")\n            if _tools:\n                tools = list(_tools)\n\n        asset_name_low = asset_name.lower()\n        other_asset_docs = get_assets(\n            project_doc[\"name\"], fields=[\"_id\", \"name\", \"data.visualParent\"]\n        )\n        for other_asset_doc in other_asset_docs:\n            other_name = other_asset_doc[\"name\"]\n            other_parent_id = other_asset_doc[\"data\"].get(\"visualParent\")\n            if other_name.lower() != asset_name_low:\n                continue\n\n            if other_parent_id != parent_id:\n                self._status.set_failed((\n                    f\"Asset with name \\\"{other_name}\\\" already\"\n                    \" exists in different hierarchy.\"\n                ))\n                raise PushToProjectError(self._status.fail_reason)\n\n            self._log_debug((\n                f\"Found already existing asset with name \\\"{other_name}\\\"\"\n                f\" which match requested name \\\"{asset_name}\\\"\"\n            ))\n            return get_asset_by_id(project_doc[\"name\"], other_asset_doc[\"_id\"])\n\n        data_keys = (\n            \"clipIn\",\n            \"clipOut\",\n            \"frameStart\",\n            \"frameEnd\",\n            \"handleStart\",\n            \"handleEnd\",\n            \"resolutionWidth\",\n            \"resolutionHeight\",\n            \"fps\",\n            \"pixelAspect\",\n        )\n        asset_data = {\n            \"visualParent\": parent_id,\n            \"parents\": parents,\n            \"tasks\": {},\n            \"tools_env\": tools\n        }\n        src_asset_data = src_asset_doc[\"data\"]\n        for key in data_keys:\n            if key in src_asset_data:\n                asset_data[key] = src_asset_data[key]\n\n        asset_doc = new_asset_document(\n            asset_name,\n            project_doc[\"_id\"],\n            parent_id,\n            parents,\n            data=asset_data\n        )\n        self._operations.create_entity(\n            project_doc[\"name\"],\n            asset_doc[\"type\"],\n            asset_doc\n        )\n        self._log_info(\n            f\"Creating new asset with name \\\"{asset_name}\\\"\"\n        )\n        self._created_asset_doc = asset_doc\n        return asset_doc\n\n    def _fill_or_create_destination_asset(self):\n        dst_project_name = self._item.dst_project_name\n        dst_folder_id = self._item.dst_folder_id\n        dst_task_name = self._item.dst_task_name\n        new_folder_name = self._item.new_folder_name\n        if not dst_folder_id and not new_folder_name:\n            self._status.set_failed(\n                \"Push item does not have defined destination asset\"\n            )\n            raise PushToProjectError(self._status.fail_reason)\n\n        # Get asset document\n        parent_asset_doc = None\n        if dst_folder_id:\n            parent_asset_doc = get_asset_by_id(\n                self._item.dst_project_name, self._item.dst_folder_id\n            )\n            if not parent_asset_doc:\n                self._status.set_failed(\n                    f\"Could find asset with id \\\"{dst_folder_id}\\\"\"\n                    f\" in project \\\"{dst_project_name}\\\"\"\n                )\n                raise PushToProjectError(self._status.fail_reason)\n\n        if not new_folder_name:\n            asset_doc = parent_asset_doc\n        else:\n            asset_doc = self._create_asset(\n                self._src_asset_doc,\n                self._project_doc,\n                parent_asset_doc,\n                new_folder_name\n            )\n        self._asset_doc = asset_doc\n        if not dst_task_name:\n            self._task_info = {}\n            return\n\n        asset_path_parts = list(asset_doc[\"data\"][\"parents\"])\n        asset_path_parts.append(asset_doc[\"name\"])\n        asset_path = \"/\".join(asset_path_parts)\n        asset_tasks = asset_doc.get(\"data\", {}).get(\"tasks\") or {}\n        task_info = asset_tasks.get(dst_task_name)\n        if not task_info:\n            self._status.set_failed(\n                f\"Could find task with name \\\"{dst_task_name}\\\"\"\n                f\" on asset \\\"{asset_path}\\\"\"\n                f\" in project \\\"{dst_project_name}\\\"\"\n            )\n            raise PushToProjectError(self._status.fail_reason)\n\n        # Create copy of task info to avoid changing data in asset document\n        task_info = copy.deepcopy(task_info)\n        task_info[\"name\"] = dst_task_name\n        # Fill rest of task information based on task type\n        task_type = task_info[\"type\"]\n        task_type_info = self._project_doc[\"config\"][\"tasks\"].get(\n            task_type, {})\n        task_info.update(task_type_info)\n        self._task_info = task_info\n\n    def _determine_family(self):\n        subset_doc = self._src_subset_doc\n        family = subset_doc[\"data\"].get(\"family\")\n        families = subset_doc[\"data\"].get(\"families\")\n        if not family and families:\n            family = families[0]\n\n        if not family:\n            self._status.set_failed(\n                \"Couldn't figure out family from source subset\"\n            )\n            raise PushToProjectError(self._status.fail_reason)\n\n        self._log_debug(\n            f\"Publishing family is '{family}' (Based on source subset)\"\n        )\n        self._family = family\n\n    def _determine_publish_template_name(self):\n        template_name = get_publish_template_name(\n            self._item.dst_project_name,\n            self.host_name,\n            self._family,\n            self._task_info.get(\"name\"),\n            self._task_info.get(\"type\"),\n            project_settings=self._project_settings\n        )\n        self._log_debug(\n            f\"Using template '{template_name}' for integration\"\n        )\n        self._template_name = template_name\n\n    def _determine_subset_name(self):\n        family = self._family\n        asset_doc = self._asset_doc\n        task_info = self._task_info\n        subset_name = get_subset_name(\n            family,\n            self._item.variant,\n            task_info.get(\"name\"),\n            asset_doc,\n            project_name=self._item.dst_project_name,\n            host_name=self.host_name,\n            project_settings=self._project_settings\n        )\n        self._log_info(\n            f\"Push will be integrating to subset with name '{subset_name}'\"\n        )\n        self._subset_name = subset_name\n\n    def _make_sure_subset_exists(self):\n        project_name = self._item.dst_project_name\n        asset_id = self._asset_doc[\"_id\"]\n        subset_name = self._subset_name\n        family = self._family\n        subset_doc = get_subset_by_name(project_name, subset_name, asset_id)\n        if subset_doc:\n            self._subset_doc = subset_doc\n            return subset_doc\n\n        data = {\n            \"families\": [family]\n        }\n        subset_doc = new_subset_document(\n            subset_name, family, asset_id, data\n        )\n        self._operations.create_entity(project_name, \"subset\", subset_doc)\n        self._subset_doc = subset_doc\n\n    def _make_sure_version_exists(self):\n        \"\"\"Make sure version document exits in database.\"\"\"\n\n        project_name = self._item.dst_project_name\n        version = self._item.dst_version\n        src_version_doc = self._src_version_doc\n        subset_doc = self._subset_doc\n        subset_id = subset_doc[\"_id\"]\n        src_data = src_version_doc[\"data\"]\n        families = subset_doc[\"data\"].get(\"families\")\n        if not families:\n            families = [subset_doc[\"data\"][\"family\"]]\n\n        version_data = {\n            \"families\": list(families),\n            \"fps\": src_data.get(\"fps\"),\n            \"source\": src_data.get(\"source\"),\n            \"machine\": socket.gethostname(),\n            \"comment\": self._item.comment or \"\",\n            \"author\": get_openpype_username(),\n            \"time\": get_formatted_current_time(),\n        }\n        if version is None:\n            last_version_doc = get_last_version_by_subset_id(\n                project_name, subset_id\n            )\n            if last_version_doc:\n                version = int(last_version_doc[\"name\"]) + 1\n            else:\n                version = get_versioning_start(\n                    project_name,\n                    self.host_name,\n                    task_name=self._task_info[\"name\"],\n                    task_type=self._task_info[\"type\"],\n                    family=families[0],\n                    subset=subset_doc[\"name\"]\n                )\n\n        existing_version_doc = get_version_by_name(\n            project_name, version, subset_id\n        )\n        # Update existing version\n        if existing_version_doc:\n            version_doc = new_version_doc(\n                version, subset_id, version_data, existing_version_doc[\"_id\"]\n            )\n            update_data = prepare_version_update_data(\n                existing_version_doc, version_doc\n            )\n            if update_data:\n                self._operations.update_entity(\n                    project_name,\n                    \"version\",\n                    existing_version_doc[\"_id\"],\n                    update_data\n                )\n            self._version_doc = version_doc\n\n            return\n\n        version_doc = new_version_doc(\n            version, subset_id, version_data\n        )\n        self._operations.create_entity(project_name, \"version\", version_doc)\n\n        self._version_doc = version_doc\n\n    def _integrate_representations(self):\n        try:\n            self._real_integrate_representations()\n        except Exception:\n            self._operations.clear()\n            self._file_transaction.rollback()\n            raise\n\n    def _real_integrate_representations(self):\n        version_doc = self._version_doc\n        version_id = version_doc[\"_id\"]\n        existing_repres = get_representations(\n            self._item.dst_project_name,\n            version_ids=[version_id]\n        )\n        existing_repres_by_low_name = {\n            repre_doc[\"name\"].lower(): repre_doc\n            for repre_doc in existing_repres\n        }\n        template_name = self._template_name\n        anatomy = self._anatomy\n        formatting_data = get_template_data(\n            self._project_doc,\n            self._asset_doc,\n            self._task_info.get(\"name\"),\n            self.host_name\n        )\n        formatting_data.update({\n            \"subset\": self._subset_name,\n            \"family\": self._family,\n            \"version\": version_doc[\"name\"]\n        })\n\n        path_template = anatomy.templates[template_name][\"path\"].replace(\n            \"\\\\\", \"/\"\n        )\n        file_template = StringTemplate(\n            anatomy.templates[template_name][\"file\"]\n        )\n        self._log_info(\"Preparing files to transfer\")\n        processed_repre_items = self._prepare_file_transactions(\n            anatomy, template_name, formatting_data, file_template\n        )\n        self._file_transaction.process()\n        self._log_info(\"Preparing database changes\")\n        self._prepare_database_operations(\n            version_id,\n            processed_repre_items,\n            path_template,\n            existing_repres_by_low_name\n        )\n        self._log_info(\"Finalization\")\n        self._operations.commit()\n        self._file_transaction.finalize()\n\n    def _prepare_file_transactions(\n        self, anatomy, template_name, formatting_data, file_template\n    ):\n        processed_repre_items = []\n        for repre_item in self._src_repre_items:\n            repre_doc = repre_item.repre_doc\n            repre_name = repre_doc[\"name\"]\n            repre_format_data = copy.deepcopy(formatting_data)\n            repre_format_data[\"representation\"] = repre_name\n            for src_file in repre_item.src_files:\n                ext = os.path.splitext(src_file.path)[-1]\n                repre_format_data[\"ext\"] = ext[1:]\n                break\n\n            # Re-use 'output' from source representation\n            repre_output_name = repre_doc[\"context\"].get(\"output\")\n            if repre_output_name is not None:\n                repre_format_data[\"output\"] = repre_output_name\n\n            template_obj = anatomy.templates_obj[template_name][\"folder\"]\n            folder_path = template_obj.format_strict(formatting_data)\n            repre_context = folder_path.used_values\n            folder_path_rootless = folder_path.rootless\n            repre_filepaths = []\n            published_path = None\n            for src_file in repre_item.src_files:\n                file_data = copy.deepcopy(repre_format_data)\n                frame = src_file.frame\n                if frame is not None:\n                    file_data[\"frame\"] = frame\n\n                udim = src_file.udim\n                if udim is not None:\n                    file_data[\"udim\"] = udim\n\n                filename = file_template.format_strict(file_data)\n                dst_filepath = os.path.normpath(\n                    os.path.join(folder_path, filename)\n                )\n                dst_rootless_path = os.path.normpath(\n                    os.path.join(folder_path_rootless, filename)\n                )\n                if published_path is None or frame == repre_item.frame:\n                    published_path = dst_filepath\n                    repre_context.update(filename.used_values)\n\n                repre_filepaths.append((dst_filepath, dst_rootless_path))\n                self._file_transaction.add(src_file.path, dst_filepath)\n\n            for resource_file in repre_item.resource_files:\n                dst_filepath = os.path.normpath(\n                    os.path.join(folder_path, resource_file.relative_path)\n                )\n                dst_rootless_path = os.path.normpath(\n                    os.path.join(\n                        folder_path_rootless, resource_file.relative_path\n                    )\n                )\n                repre_filepaths.append((dst_filepath, dst_rootless_path))\n                self._file_transaction.add(resource_file.path, dst_filepath)\n            processed_repre_items.append(\n                (repre_item, repre_filepaths, repre_context, published_path)\n            )\n        return processed_repre_items\n\n    def _prepare_database_operations(\n        self,\n        version_id,\n        processed_repre_items,\n        path_template,\n        existing_repres_by_low_name\n    ):\n        modules_manager = ModulesManager()\n        sync_server_module = modules_manager.get(\"sync_server\")\n        if sync_server_module is None or not sync_server_module.enabled:\n            sites = [{\n                \"name\": \"studio\",\n                \"created_dt\": datetime.datetime.now()\n            }]\n        else:\n            sites = sync_server_module.compute_resource_sync_sites(\n                project_name=self._item.dst_project_name\n            )\n\n        added_repre_names = set()\n        for item in processed_repre_items:\n            (repre_item, repre_filepaths, repre_context, published_path) = item\n            repre_name = repre_item.repre_doc[\"name\"]\n            added_repre_names.add(repre_name.lower())\n            new_repre_data = {\n                \"path\": published_path,\n                \"template\": path_template\n            }\n            new_repre_files = []\n            for (path, rootless_path) in repre_filepaths:\n                new_repre_files.append({\n                    \"_id\": ObjectId(),\n                    \"path\": rootless_path,\n                    \"size\": os.path.getsize(path),\n                    \"hash\": source_hash(path),\n                    \"sites\": sites\n                })\n\n            existing_repre = existing_repres_by_low_name.get(\n                repre_name.lower()\n            )\n            entity_id = None\n            if existing_repre:\n                entity_id = existing_repre[\"_id\"]\n            new_repre_doc = new_representation_doc(\n                repre_name,\n                version_id,\n                repre_context,\n                data=new_repre_data,\n                entity_id=entity_id\n            )\n            new_repre_doc[\"files\"] = new_repre_files\n            if not existing_repre:\n                self._operations.create_entity(\n                    self._item.dst_project_name,\n                    new_repre_doc[\"type\"],\n                    new_repre_doc\n                )\n            else:\n                update_data = prepare_representation_update_data(\n                    existing_repre, new_repre_doc\n                )\n                if update_data:\n                    self._operations.update_entity(\n                        self._item.dst_project_name,\n                        new_repre_doc[\"type\"],\n                        new_repre_doc[\"_id\"],\n                        update_data\n                    )\n\n        existing_repre_names = set(existing_repres_by_low_name.keys())\n        for repre_name in (existing_repre_names - added_repre_names):\n            repre_doc = existing_repres_by_low_name[repre_name]\n            self._operations.update_entity(\n                self._item.dst_project_name,\n                repre_doc[\"type\"],\n                repre_doc[\"_id\"],\n                {\"type\": \"archived_representation\"}\n            )\n\n\nclass IntegrateModel:\n    def __init__(self, controller):\n        self._controller = controller\n        self._process_items = {}\n\n    def reset(self):\n        self._process_items = {}\n\n    def emit_event(self, topic, data=None, source=None):\n        self._controller.emit_event(topic, data, source)\n\n    def create_process_item(\n        self,\n        src_project_name,\n        src_version_id,\n        dst_project_name,\n        dst_folder_id,\n        dst_task_name,\n        variant,\n        comment,\n        new_folder_name,\n        dst_version,\n    ):\n        \"\"\"Create new item for integration.\n\n        Args:\n            src_project_name (str): Source project name.\n            src_version_id (str): Source version id.\n            dst_project_name (str): Destination project name.\n            dst_folder_id (str): Destination folder id.\n            dst_task_name (str): Destination task name.\n            variant (str): Variant name.\n            comment (Union[str, None]): Comment.\n            new_folder_name (Union[str, None]): New folder name.\n            dst_version (int): Destination version number.\n\n        Returns:\n            str: Item id. The id can be used to trigger integration or get\n                status information.\n        \"\"\"\n\n        item = ProjectPushItem(\n            src_project_name,\n            src_version_id,\n            dst_project_name,\n            dst_folder_id,\n            dst_task_name,\n            variant,\n            comment=comment,\n            new_folder_name=new_folder_name,\n            dst_version=dst_version\n        )\n        process_item = ProjectPushItemProcess(self, item)\n        self._process_items[item.item_id] = process_item\n        return item.item_id\n\n    def integrate_item(self, item_id):\n        \"\"\"Start integration of item.\n\n        Args:\n            item_id (str): Item id which should be integrated.\n        \"\"\"\n\n        item = self._process_items.get(item_id)\n        if item is None or item.started:\n            return\n        item.integrate()\n\n    def get_item_status(self, item_id):\n        \"\"\"Status of an item.\n\n        Args:\n            item_id (str): Item id for which status should be returned.\n\n        Returns:\n            dict[str, Any]: Status data.\n        \"\"\"\n\n        item = self._process_items.get(item_id)\n        if item is not None:\n            return item.get_status_data()\n        return None\n"
  },
  {
    "path": "openpype/tools/ayon_push_to_project/models/selection.py",
    "content": "class PushToProjectSelectionModel(object):\n    \"\"\"Model handling selection changes.\n\n    Triggering events:\n    - \"selection.project.changed\"\n    - \"selection.folder.changed\"\n    - \"selection.task.changed\"\n    \"\"\"\n\n    event_source = \"push-to-project.selection.model\"\n\n    def __init__(self, controller):\n        self._controller = controller\n\n        self._project_name = None\n        self._folder_id = None\n        self._task_name = None\n        self._task_id = None\n\n    def get_selected_project_name(self):\n        return self._project_name\n\n    def set_selected_project(self, project_name):\n        if project_name == self._project_name:\n            return\n\n        self._project_name = project_name\n        self._controller.emit_event(\n            \"selection.project.changed\",\n            {\"project_name\": project_name},\n            self.event_source\n        )\n\n    def get_selected_folder_id(self):\n        return self._folder_id\n\n    def set_selected_folder(self, folder_id):\n        if folder_id == self._folder_id:\n            return\n\n        self._folder_id = folder_id\n        self._controller.emit_event(\n            \"selection.folder.changed\",\n            {\n                \"project_name\": self._project_name,\n                \"folder_id\": folder_id,\n            },\n            self.event_source\n        )\n\n    def get_selected_task_name(self):\n        return self._task_name\n\n    def get_selected_task_id(self):\n        return self._task_id\n\n    def set_selected_task(self, task_id, task_name):\n        if task_id == self._task_id:\n            return\n\n        self._task_name = task_name\n        self._task_id = task_id\n        self._controller.emit_event(\n            \"selection.task.changed\",\n            {\n                \"project_name\": self._project_name,\n                \"folder_id\": self._folder_id,\n                \"task_name\": task_name,\n                \"task_id\": task_id,\n            },\n            self.event_source\n        )\n"
  },
  {
    "path": "openpype/tools/ayon_push_to_project/models/user_values.py",
    "content": "import re\n\nfrom openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS\n\n\nclass UserPublishValuesModel:\n    \"\"\"Helper object to validate values required for push to different project.\n\n    Args:\n        controller (PushToContextController): Event system to catch\n            and emit events.\n    \"\"\"\n\n    folder_name_regex = re.compile(\"^[a-zA-Z0-9_.]+$\")\n    variant_regex = re.compile(\"^[{}]+$\".format(SUBSET_NAME_ALLOWED_SYMBOLS))\n\n    def __init__(self, controller):\n        self._controller = controller\n        self._new_folder_name = None\n        self._variant = None\n        self._comment = None\n        self._is_variant_valid = False\n        self._is_new_folder_name_valid = False\n\n        self.set_new_folder_name(\"\")\n        self.set_variant(\"\")\n        self.set_comment(\"\")\n\n    @property\n    def new_folder_name(self):\n        return self._new_folder_name\n\n    @property\n    def variant(self):\n        return self._variant\n\n    @property\n    def comment(self):\n        return self._comment\n\n    @property\n    def is_variant_valid(self):\n        return self._is_variant_valid\n\n    @property\n    def is_new_folder_name_valid(self):\n        return self._is_new_folder_name_valid\n\n    @property\n    def is_valid(self):\n        return self.is_variant_valid and self.is_new_folder_name_valid\n\n    def get_data(self):\n        return {\n            \"new_folder_name\": self._new_folder_name,\n            \"variant\": self._variant,\n            \"comment\": self._comment,\n            \"is_variant_valid\": self._is_variant_valid,\n            \"is_new_folder_name_valid\": self._is_new_folder_name_valid,\n            \"is_valid\": self.is_valid\n        }\n\n    def set_variant(self, variant):\n        if variant == self._variant:\n            return\n\n        self._variant = variant\n        is_valid = False\n        if variant:\n            is_valid = self.variant_regex.match(variant) is not None\n        self._is_variant_valid = is_valid\n\n        self._controller.emit_event(\n            \"variant.changed\",\n            {\n                \"variant\": variant,\n                \"is_valid\": self._is_variant_valid,\n            },\n            \"user_values\"\n        )\n\n    def set_new_folder_name(self, folder_name):\n        if self._new_folder_name == folder_name:\n            return\n\n        self._new_folder_name = folder_name\n        is_valid = True\n        if folder_name:\n            is_valid = (\n                self.folder_name_regex.match(folder_name) is not None\n            )\n        self._is_new_folder_name_valid = is_valid\n        self._controller.emit_event(\n            \"new_folder_name.changed\",\n            {\n                \"new_folder_name\": self._new_folder_name,\n                \"is_valid\": self._is_new_folder_name_valid,\n            },\n            \"user_values\"\n        )\n\n    def set_comment(self, comment):\n        if comment == self._comment:\n            return\n        self._comment = comment\n        self._controller.emit_event(\n            \"comment.changed\",\n            {\"comment\": comment},\n            \"user_values\"\n        )\n"
  },
  {
    "path": "openpype/tools/ayon_push_to_project/ui/__init__.py",
    "content": "from .window import PushToContextSelectWindow\n\n\n__all__ = (\n    \"PushToContextSelectWindow\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_push_to_project/ui/window.py",
    "content": "from qtpy import QtWidgets, QtGui, QtCore\n\nfrom openpype.style import load_stylesheet, get_app_icon_path\nfrom openpype.tools.utils import (\n    PlaceholderLineEdit,\n    SeparatorWidget,\n    set_style_property,\n)\nfrom openpype.tools.ayon_utils.widgets import (\n    ProjectsCombobox,\n    FoldersWidget,\n    TasksWidget,\n)\nfrom openpype.tools.ayon_push_to_project.control import (\n    PushToContextController,\n)\n\n\nclass PushToContextSelectWindow(QtWidgets.QWidget):\n    def __init__(self, controller=None):\n        super(PushToContextSelectWindow, self).__init__()\n        if controller is None:\n            controller = PushToContextController()\n        self._controller = controller\n\n        self.setWindowTitle(\"Push to project (select context)\")\n        self.setWindowIcon(QtGui.QIcon(get_app_icon_path()))\n\n        main_context_widget = QtWidgets.QWidget(self)\n\n        header_widget = QtWidgets.QWidget(main_context_widget)\n\n        header_label = QtWidgets.QLabel(\n            controller.get_source_label(),\n            header_widget\n        )\n\n        header_layout = QtWidgets.QHBoxLayout(header_widget)\n        header_layout.setContentsMargins(0, 0, 0, 0)\n        header_layout.addWidget(header_label)\n\n        main_splitter = QtWidgets.QSplitter(\n            QtCore.Qt.Horizontal, main_context_widget\n        )\n\n        context_widget = QtWidgets.QWidget(main_splitter)\n\n        projects_combobox = ProjectsCombobox(controller, context_widget)\n        projects_combobox.set_select_item_visible(True)\n        projects_combobox.set_standard_filter_enabled(True)\n\n        context_splitter = QtWidgets.QSplitter(\n            QtCore.Qt.Vertical, context_widget\n        )\n\n        folders_widget = FoldersWidget(controller, context_splitter)\n        folders_widget.set_deselectable(True)\n        tasks_widget = TasksWidget(controller, context_splitter)\n\n        context_splitter.addWidget(folders_widget)\n        context_splitter.addWidget(tasks_widget)\n\n        context_layout = QtWidgets.QVBoxLayout(context_widget)\n        context_layout.setContentsMargins(0, 0, 0, 0)\n        context_layout.addWidget(projects_combobox, 0)\n        context_layout.addWidget(context_splitter, 1)\n\n        # --- Inputs widget ---\n        inputs_widget = QtWidgets.QWidget(main_splitter)\n\n        folder_name_input = PlaceholderLineEdit(inputs_widget)\n        folder_name_input.setPlaceholderText(\"< Name of new folder >\")\n        folder_name_input.setObjectName(\"ValidatedLineEdit\")\n\n        variant_input = PlaceholderLineEdit(inputs_widget)\n        variant_input.setPlaceholderText(\"< Variant >\")\n        variant_input.setObjectName(\"ValidatedLineEdit\")\n\n        comment_input = PlaceholderLineEdit(inputs_widget)\n        comment_input.setPlaceholderText(\"< Publish comment >\")\n\n        inputs_layout = QtWidgets.QFormLayout(inputs_widget)\n        inputs_layout.setContentsMargins(0, 0, 0, 0)\n        inputs_layout.addRow(\"New folder name\", folder_name_input)\n        inputs_layout.addRow(\"Variant\", variant_input)\n        inputs_layout.addRow(\"Comment\", comment_input)\n\n        main_splitter.addWidget(context_widget)\n        main_splitter.addWidget(inputs_widget)\n\n        # --- Buttons widget ---\n        btns_widget = QtWidgets.QWidget(self)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", btns_widget)\n        publish_btn = QtWidgets.QPushButton(\"Publish\", btns_widget)\n\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(cancel_btn, 0)\n        btns_layout.addWidget(publish_btn, 0)\n\n        sep_1 = SeparatorWidget(parent=main_context_widget)\n        sep_2 = SeparatorWidget(parent=main_context_widget)\n        main_context_layout = QtWidgets.QVBoxLayout(main_context_widget)\n        main_context_layout.addWidget(header_widget, 0)\n        main_context_layout.addWidget(sep_1, 0)\n        main_context_layout.addWidget(main_splitter, 1)\n        main_context_layout.addWidget(sep_2, 0)\n        main_context_layout.addWidget(btns_widget, 0)\n\n        # NOTE This was added in hurry\n        # - should be reorganized and changed styles\n        overlay_widget = QtWidgets.QFrame(self)\n        overlay_widget.setObjectName(\"OverlayFrame\")\n\n        overlay_label = QtWidgets.QLabel(overlay_widget)\n        overlay_label.setAlignment(QtCore.Qt.AlignCenter)\n\n        overlay_btns_widget = QtWidgets.QWidget(overlay_widget)\n        overlay_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        # Add try again button (requires changes in controller)\n        overlay_try_btn = QtWidgets.QPushButton(\n            \"Try again\", overlay_btns_widget\n        )\n        overlay_close_btn = QtWidgets.QPushButton(\n            \"Close\", overlay_btns_widget\n        )\n\n        overlay_btns_layout = QtWidgets.QHBoxLayout(overlay_btns_widget)\n        overlay_btns_layout.addStretch(1)\n        overlay_btns_layout.addWidget(overlay_try_btn, 0)\n        overlay_btns_layout.addWidget(overlay_close_btn, 0)\n        overlay_btns_layout.addStretch(1)\n\n        overlay_layout = QtWidgets.QVBoxLayout(overlay_widget)\n        overlay_layout.addWidget(overlay_label, 0)\n        overlay_layout.addWidget(overlay_btns_widget, 0)\n        overlay_layout.setAlignment(QtCore.Qt.AlignCenter)\n\n        main_layout = QtWidgets.QStackedLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(main_context_widget)\n        main_layout.addWidget(overlay_widget)\n        main_layout.setStackingMode(QtWidgets.QStackedLayout.StackAll)\n        main_layout.setCurrentWidget(main_context_widget)\n\n        show_timer = QtCore.QTimer()\n        show_timer.setInterval(0)\n\n        main_thread_timer = QtCore.QTimer()\n        main_thread_timer.setInterval(10)\n\n        user_input_changed_timer = QtCore.QTimer()\n        user_input_changed_timer.setInterval(200)\n        user_input_changed_timer.setSingleShot(True)\n\n        main_thread_timer.timeout.connect(self._on_main_thread_timer)\n        show_timer.timeout.connect(self._on_show_timer)\n        user_input_changed_timer.timeout.connect(self._on_user_input_timer)\n        folder_name_input.textChanged.connect(self._on_new_asset_change)\n        variant_input.textChanged.connect(self._on_variant_change)\n        comment_input.textChanged.connect(self._on_comment_change)\n\n        publish_btn.clicked.connect(self._on_select_click)\n        cancel_btn.clicked.connect(self._on_close_click)\n        overlay_close_btn.clicked.connect(self._on_close_click)\n        overlay_try_btn.clicked.connect(self._on_try_again_click)\n\n        controller.register_event_callback(\n            \"new_folder_name.changed\",\n            self._on_controller_new_asset_change\n        )\n        controller.register_event_callback(\n            \"variant.changed\", self._on_controller_variant_change\n        )\n        controller.register_event_callback(\n            \"comment.changed\", self._on_controller_comment_change\n        )\n        controller.register_event_callback(\n            \"submission.enabled.changed\", self._on_submission_change\n        )\n        controller.register_event_callback(\n            \"source.changed\", self._on_controller_source_change\n        )\n        controller.register_event_callback(\n            \"submit.started\", self._on_controller_submit_start\n        )\n        controller.register_event_callback(\n            \"submit.finished\", self._on_controller_submit_end\n        )\n        controller.register_event_callback(\n            \"push.message.added\", self._on_push_message\n        )\n\n        self._main_layout = main_layout\n\n        self._main_context_widget = main_context_widget\n\n        self._header_label = header_label\n        self._main_splitter = main_splitter\n\n        self._projects_combobox = projects_combobox\n        self._folders_widget = folders_widget\n        self._tasks_widget = tasks_widget\n\n        self._variant_input = variant_input\n        self._folder_name_input = folder_name_input\n        self._comment_input = comment_input\n\n        self._publish_btn = publish_btn\n\n        self._overlay_widget = overlay_widget\n        self._overlay_close_btn = overlay_close_btn\n        self._overlay_try_btn = overlay_try_btn\n        self._overlay_label = overlay_label\n\n        self._user_input_changed_timer = user_input_changed_timer\n        # Store current value on input text change\n        #   The value is unset when is passed to controller\n        # The goal is to have controll over changes happened during user change\n        #   in UI and controller auto-changes\n        self._variant_input_text = None\n        self._new_folder_name_input_text = None\n        self._comment_input_text = None\n\n        self._first_show = True\n        self._show_timer = show_timer\n        self._show_counter = 0\n\n        self._main_thread_timer = main_thread_timer\n        self._main_thread_timer_can_stop = True\n        self._last_submit_message = None\n        self._process_item_id = None\n\n        self._variant_is_valid = None\n        self._folder_is_valid = None\n\n        publish_btn.setEnabled(False)\n        overlay_close_btn.setVisible(False)\n        overlay_try_btn.setVisible(False)\n\n    # Support of public api function of controller\n    def set_source(self, project_name, version_id):\n        \"\"\"Set source project and version.\n\n        Call the method on controller.\n\n        Args:\n            project_name (Union[str, None]): Name of project.\n            version_id (Union[str, None]): Version id.\n        \"\"\"\n\n        self._controller.set_source(project_name, version_id)\n\n    def showEvent(self, event):\n        super(PushToContextSelectWindow, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self._on_first_show()\n\n    def refresh(self):\n        user_values = self._controller.get_user_values()\n        new_folder_name = user_values[\"new_folder_name\"]\n        variant = user_values[\"variant\"]\n        self._folder_name_input.setText(new_folder_name or \"\")\n        self._variant_input.setText(variant or \"\")\n        self._invalidate_variant(user_values[\"is_variant_valid\"])\n        self._invalidate_new_folder_name(\n            new_folder_name, user_values[\"is_new_folder_name_valid\"]\n        )\n\n        self._projects_combobox.refresh()\n\n    def _on_first_show(self):\n        width = 740\n        height = 640\n        inputs_width = 360\n        self.setStyleSheet(load_stylesheet())\n        self.resize(width, height)\n        self._main_splitter.setSizes([width - inputs_width, inputs_width])\n        self._show_timer.start()\n\n    def _on_show_timer(self):\n        if self._show_counter < 3:\n            self._show_counter += 1\n            return\n        self._show_timer.stop()\n\n        self._show_counter = 0\n\n        self.refresh()\n\n    def _on_new_asset_change(self, text):\n        self._new_folder_name_input_text = text\n        self._user_input_changed_timer.start()\n\n    def _on_variant_change(self, text):\n        self._variant_input_text = text\n        self._user_input_changed_timer.start()\n\n    def _on_comment_change(self, text):\n        self._comment_input_text = text\n        self._user_input_changed_timer.start()\n\n    def _on_user_input_timer(self):\n        folder_name = self._new_folder_name_input_text\n        if folder_name is not None:\n            self._new_folder_name_input_text = None\n            self._controller.set_user_value_folder_name(folder_name)\n\n        variant = self._variant_input_text\n        if variant is not None:\n            self._variant_input_text = None\n            self._controller.set_user_value_variant(variant)\n\n        comment = self._comment_input_text\n        if comment is not None:\n            self._comment_input_text = None\n            self._controller.set_user_value_comment(comment)\n\n    def _on_controller_new_asset_change(self, event):\n        folder_name = event[\"new_folder_name\"]\n        if (\n            self._new_folder_name_input_text is None\n            and folder_name != self._folder_name_input.text()\n        ):\n            self._folder_name_input.setText(folder_name)\n\n        self._invalidate_new_folder_name(folder_name, event[\"is_valid\"])\n\n    def _on_controller_variant_change(self, event):\n        is_valid = event[\"is_valid\"]\n        variant = event[\"variant\"]\n        if (\n            self._variant_input_text is None\n            and variant != self._variant_input.text()\n        ):\n            self._variant_input.setText(variant)\n\n        self._invalidate_variant(is_valid)\n\n    def _on_controller_comment_change(self, event):\n        comment = event[\"comment\"]\n        if (\n            self._comment_input_text is None\n            and comment != self._comment_input.text()\n        ):\n            self._comment_input.setText(comment)\n\n    def _on_controller_source_change(self):\n        self._header_label.setText(self._controller.get_source_label())\n\n    def _invalidate_new_folder_name(self, folder_name, is_valid):\n        self._tasks_widget.setVisible(not folder_name)\n        if self._folder_is_valid is is_valid:\n            return\n        self._folder_is_valid = is_valid\n        state = \"\"\n        if folder_name:\n            if is_valid is True:\n                state = \"valid\"\n            elif is_valid is False:\n                state = \"invalid\"\n        set_style_property(\n            self._folder_name_input, \"state\", state\n        )\n\n    def _invalidate_variant(self, is_valid):\n        if self._variant_is_valid is is_valid:\n            return\n        self._variant_is_valid = is_valid\n        state = \"valid\" if is_valid else \"invalid\"\n        set_style_property(self._variant_input, \"state\", state)\n\n    def _on_submission_change(self, event):\n        self._publish_btn.setEnabled(event[\"enabled\"])\n\n    def _on_close_click(self):\n        self.close()\n\n    def _on_select_click(self):\n        self._process_item_id = self._controller.submit(wait=False)\n\n    def _on_try_again_click(self):\n        self._process_item_id = None\n        self._last_submit_message = None\n\n        self._overlay_close_btn.setVisible(False)\n        self._overlay_try_btn.setVisible(False)\n        self._main_layout.setCurrentWidget(self._main_context_widget)\n\n    def _on_main_thread_timer(self):\n        if self._last_submit_message:\n            self._overlay_label.setText(self._last_submit_message)\n            self._last_submit_message = None\n\n        process_status = self._controller.get_process_item_status(\n            self._process_item_id\n        )\n        push_failed = process_status[\"failed\"]\n        fail_traceback = process_status[\"full_traceback\"]\n        if self._main_thread_timer_can_stop:\n            self._main_thread_timer.stop()\n            self._overlay_close_btn.setVisible(True)\n            if push_failed and not fail_traceback:\n                self._overlay_try_btn.setVisible(True)\n\n        if push_failed:\n            message = \"Push Failed:\\n{}\".format(process_status[\"fail_reason\"])\n            if fail_traceback:\n                message += \"\\n{}\".format(fail_traceback)\n            self._overlay_label.setText(message)\n            set_style_property(self._overlay_close_btn, \"state\", \"error\")\n\n        if self._main_thread_timer_can_stop:\n            # Join thread in controller\n            self._controller.wait_for_process_thread()\n            # Reset process item to None\n            self._process_item_id = None\n\n    def _on_controller_submit_start(self):\n        self._main_thread_timer_can_stop = False\n        self._main_thread_timer.start()\n        self._main_layout.setCurrentWidget(self._overlay_widget)\n        self._overlay_label.setText(\"Submittion started\")\n\n    def _on_controller_submit_end(self):\n        self._main_thread_timer_can_stop = True\n\n    def _on_push_message(self, event):\n        self._last_submit_message = event[\"message\"]\n"
  },
  {
    "path": "openpype/tools/ayon_sceneinventory/__init__.py",
    "content": "from .control import SceneInventoryController\n\n\n__all__ = (\n    \"SceneInventoryController\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_sceneinventory/control.py",
    "content": "import ayon_api\n\nfrom openpype.lib.events import QueuedEventSystem\nfrom openpype.host import ILoadHost\nfrom openpype.pipeline import (\n    registered_host,\n    get_current_context,\n)\nfrom openpype.tools.ayon_utils.models import HierarchyModel\n\nfrom .models import SiteSyncModel\n\n\nclass SceneInventoryController:\n    \"\"\"This is a temporary controller for AYON.\n\n    Goal of this temporary controller is to provide a way to get current\n    context instead of using 'AvalonMongoDB' object (or 'legacy_io').\n\n    Also provides (hopefully) cleaner api for site sync.\n    \"\"\"\n\n    def __init__(self, host=None):\n        if host is None:\n            host = registered_host()\n        self._host = host\n        self._current_context = None\n        self._current_project = None\n        self._current_folder_id = None\n        self._current_folder_set = False\n\n        self._site_sync_model = SiteSyncModel(self)\n        # Switch dialog requirements\n        self._hierarchy_model = HierarchyModel(self)\n        self._event_system = self._create_event_system()\n\n    def emit_event(self, topic, data=None, source=None):\n        if data is None:\n            data = {}\n        self._event_system.emit(topic, data, source)\n\n    def register_event_callback(self, topic, callback):\n        self._event_system.add_callback(topic, callback)\n\n    def reset(self):\n        self._current_context = None\n        self._current_project = None\n        self._current_folder_id = None\n        self._current_folder_set = False\n\n        self._site_sync_model.reset()\n        self._hierarchy_model.reset()\n\n    def get_current_context(self):\n        if self._current_context is None:\n            if hasattr(self._host, \"get_current_context\"):\n                self._current_context = self._host.get_current_context()\n            else:\n                self._current_context = get_current_context()\n        return self._current_context\n\n    def get_current_project_name(self):\n        if self._current_project is None:\n            self._current_project = self.get_current_context()[\"project_name\"]\n        return self._current_project\n\n    def get_current_folder_id(self):\n        if self._current_folder_set:\n            return self._current_folder_id\n\n        context = self.get_current_context()\n        project_name = context[\"project_name\"]\n        folder_name = context.get(\"asset_name\")\n        folder_id = None\n        if folder_name:\n            folder = ayon_api.get_folder_by_path(project_name, folder_name)\n            if folder:\n                folder_id = folder[\"id\"]\n\n        self._current_folder_id = folder_id\n        self._current_folder_set = True\n        return self._current_folder_id\n\n    def get_containers(self):\n        host = self._host\n        if isinstance(host, ILoadHost):\n            return list(host.get_containers())\n        elif hasattr(host, \"ls\"):\n            return list(host.ls())\n        return []\n\n    # Site Sync methods\n    def is_sync_server_enabled(self):\n        return self._site_sync_model.is_sync_server_enabled()\n\n    def get_sites_information(self):\n        return self._site_sync_model.get_sites_information()\n\n    def get_site_provider_icons(self):\n        return self._site_sync_model.get_site_provider_icons()\n\n    def get_representations_site_progress(self, representation_ids):\n        return self._site_sync_model.get_representations_site_progress(\n            representation_ids\n        )\n\n    def resync_representations(self, representation_ids, site_type):\n        return self._site_sync_model.resync_representations(\n            representation_ids, site_type\n        )\n\n    # Switch dialog methods\n    def get_folder_items(self, project_name, sender=None):\n        return self._hierarchy_model.get_folder_items(project_name, sender)\n\n    def get_folder_label(self, folder_id):\n        if not folder_id:\n            return None\n        project_name = self.get_current_project_name()\n        folder_item = self._hierarchy_model.get_folder_item(\n            project_name, folder_id)\n        if folder_item is None:\n            return None\n        return folder_item.label\n\n    def _create_event_system(self):\n        return QueuedEventSystem()\n"
  },
  {
    "path": "openpype/tools/ayon_sceneinventory/model.py",
    "content": "import collections\nimport re\nimport logging\nimport uuid\nimport copy\n\nfrom collections import defaultdict\n\nfrom qtpy import QtCore, QtGui\nimport qtawesome\n\nfrom openpype.client import (\n    get_assets,\n    get_subsets,\n    get_versions,\n    get_last_version_by_subset_id,\n    get_representations,\n)\nfrom openpype.pipeline import (\n    get_current_project_name,\n    schema,\n    HeroVersionType,\n)\nfrom openpype.style import get_default_entity_icon_color\nfrom openpype.tools.utils.models import TreeModel, Item\nfrom openpype.tools.ayon_utils.widgets import get_qt_icon\n\n\ndef walk_hierarchy(node):\n    \"\"\"Recursively yield group node.\"\"\"\n    for child in node.children():\n        if child.get(\"isGroupNode\"):\n            yield child\n\n        for _child in walk_hierarchy(child):\n            yield _child\n\n\nclass InventoryModel(TreeModel):\n    \"\"\"The model for the inventory\"\"\"\n\n    Columns = [\n        \"Name\",\n        \"version\",\n        \"count\",\n        \"family\",\n        \"group\",\n        \"loader\",\n        \"objectName\",\n        \"active_site\",\n        \"remote_site\",\n    ]\n    active_site_col = Columns.index(\"active_site\")\n    remote_site_col = Columns.index(\"remote_site\")\n\n    OUTDATED_COLOR = QtGui.QColor(235, 30, 30)\n    CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30)\n    GRAYOUT_COLOR = QtGui.QColor(160, 160, 160)\n\n    UniqueRole = QtCore.Qt.UserRole + 2     # unique label role\n\n    def __init__(self, controller, parent=None):\n        super(InventoryModel, self).__init__(parent)\n        self.log = logging.getLogger(self.__class__.__name__)\n\n        self._controller = controller\n\n        self._hierarchy_view = False\n\n        self._default_icon_color = get_default_entity_icon_color()\n\n        site_icons = self._controller.get_site_provider_icons()\n\n        self._site_icons = {\n            provider: get_qt_icon(icon_def)\n            for provider, icon_def in site_icons.items()\n        }\n\n    def outdated(self, item):\n        value = item.get(\"version\")\n        if isinstance(value, HeroVersionType):\n            return False\n\n        if item.get(\"version\") == item.get(\"highest_version\"):\n            return False\n        return True\n\n    def data(self, index, role):\n        if not index.isValid():\n            return\n\n        item = index.internalPointer()\n\n        if role == QtCore.Qt.FontRole:\n            # Make top-level entries bold\n            if item.get(\"isGroupNode\") or item.get(\"isNotSet\"):  # group-item\n                font = QtGui.QFont()\n                font.setBold(True)\n                return font\n\n        if role == QtCore.Qt.ForegroundRole:\n            # Set the text color to the OUTDATED_COLOR when the\n            # collected version is not the same as the highest version\n            key = self.Columns[index.column()]\n            if key == \"version\":  # version\n                if item.get(\"isGroupNode\"):  # group-item\n                    if self.outdated(item):\n                        return self.OUTDATED_COLOR\n\n                    if self._hierarchy_view:\n                        # If current group is not outdated, check if any\n                        # outdated children.\n                        for _node in walk_hierarchy(item):\n                            if self.outdated(_node):\n                                return self.CHILD_OUTDATED_COLOR\n                else:\n\n                    if self._hierarchy_view:\n                        # Although this is not a group item, we still need\n                        # to distinguish which one contain outdated child.\n                        for _node in walk_hierarchy(item):\n                            if self.outdated(_node):\n                                return self.CHILD_OUTDATED_COLOR.darker(150)\n\n                    return self.GRAYOUT_COLOR\n\n            if key == \"Name\" and not item.get(\"isGroupNode\"):\n                return self.GRAYOUT_COLOR\n\n        # Add icons\n        if role == QtCore.Qt.DecorationRole:\n            if index.column() == 0:\n                # Override color\n                color = item.get(\"color\", self._default_icon_color)\n                if item.get(\"isGroupNode\"):  # group-item\n                    return qtawesome.icon(\"fa.folder\", color=color)\n                if item.get(\"isNotSet\"):\n                    return qtawesome.icon(\"fa.exclamation-circle\", color=color)\n\n                return qtawesome.icon(\"fa.file-o\", color=color)\n\n            if index.column() == 3:\n                # Family icon\n                return item.get(\"familyIcon\", None)\n\n            column_name = self.Columns[index.column()]\n\n            if column_name == \"group\" and item.get(\"group\"):\n                return qtawesome.icon(\"fa.object-group\",\n                                      color=get_default_entity_icon_color())\n\n            if item.get(\"isGroupNode\"):\n                if column_name == \"active_site\":\n                    provider = item.get(\"active_site_provider\")\n                    return self._site_icons.get(provider)\n\n                if column_name == \"remote_site\":\n                    provider = item.get(\"remote_site_provider\")\n                    return self._site_icons.get(provider)\n\n        if role == QtCore.Qt.DisplayRole and item.get(\"isGroupNode\"):\n            column_name = self.Columns[index.column()]\n            progress = None\n            if column_name == \"active_site\":\n                progress = item.get(\"active_site_progress\", 0)\n            elif column_name == \"remote_site\":\n                progress = item.get(\"remote_site_progress\", 0)\n            if progress is not None:\n                return \"{}%\".format(max(progress, 0) * 100)\n\n        if role == self.UniqueRole:\n            return item[\"representation\"] + item.get(\"objectName\", \"<none>\")\n\n        return super(InventoryModel, self).data(index, role)\n\n    def set_hierarchy_view(self, state):\n        \"\"\"Set whether to display subsets in hierarchy view.\"\"\"\n        state = bool(state)\n\n        if state != self._hierarchy_view:\n            self._hierarchy_view = state\n\n    def refresh(self, selected=None, containers=None):\n        \"\"\"Refresh the model\"\"\"\n\n        # for debugging or testing, injecting items from outside\n        if containers is None:\n            containers = self._controller.get_containers()\n\n        self.clear()\n        if not selected or not self._hierarchy_view:\n            self._add_containers(containers)\n            return\n\n        # Filter by cherry-picked items\n        self._add_containers((\n            container\n            for container in containers\n            if container[\"objectName\"] in selected\n        ))\n\n    def _add_containers(self, containers, parent=None):\n        \"\"\"Add the items to the model.\n\n        The items should be formatted similar to `api.ls()` returns, an item\n        is then represented as:\n            {\"filename_v001.ma\": [full/filename/of/loaded/filename_v001.ma,\n                                  full/filename/of/loaded/filename_v001.ma],\n             \"nodetype\" : \"reference\",\n             \"node\": \"referenceNode1\"}\n\n        Note: When performing an additional call to `add_items` it will *not*\n            group the new items with previously existing item groups of the\n            same type.\n\n        Args:\n            containers (generator): Container items.\n            parent (Item, optional): Set this item as parent for the added\n              items when provided. Defaults to the root of the model.\n\n        Returns:\n            node.Item: root node which has children added based on the data\n        \"\"\"\n\n        project_name = get_current_project_name()\n\n        self.beginResetModel()\n\n        # Group by representation\n        grouped = defaultdict(lambda: {\"containers\": list()})\n        for container in containers:\n            repre_id = container[\"representation\"]\n            grouped[repre_id][\"containers\"].append(container)\n\n        (\n            repres_by_id,\n            versions_by_id,\n            products_by_id,\n            folders_by_id,\n        ) = self._query_entities(project_name, set(grouped.keys()))\n        # Add to model\n        not_found = defaultdict(list)\n        not_found_ids = []\n        for repre_id, group_dict in sorted(grouped.items()):\n            group_containers = group_dict[\"containers\"]\n            representation = repres_by_id.get(repre_id)\n            if not representation:\n                not_found[\"representation\"].extend(group_containers)\n                not_found_ids.append(repre_id)\n                continue\n\n            version = versions_by_id.get(representation[\"parent\"])\n            if not version:\n                not_found[\"version\"].extend(group_containers)\n                not_found_ids.append(repre_id)\n                continue\n\n            product = products_by_id.get(version[\"parent\"])\n            if not product:\n                not_found[\"product\"].extend(group_containers)\n                not_found_ids.append(repre_id)\n                continue\n\n            folder = folders_by_id.get(product[\"parent\"])\n            if not folder:\n                not_found[\"folder\"].extend(group_containers)\n                not_found_ids.append(repre_id)\n                continue\n\n            group_dict.update({\n                \"representation\": representation,\n                \"version\": version,\n                \"subset\": product,\n                \"asset\": folder\n            })\n\n        for _repre_id in not_found_ids:\n            grouped.pop(_repre_id)\n\n        for where, group_containers in not_found.items():\n            # create the group header\n            group_node = Item()\n            name = \"< NOT FOUND - {} >\".format(where)\n            group_node[\"Name\"] = name\n            group_node[\"representation\"] = name\n            group_node[\"count\"] = len(group_containers)\n            group_node[\"isGroupNode\"] = False\n            group_node[\"isNotSet\"] = True\n\n            self.add_child(group_node, parent=parent)\n\n            for container in group_containers:\n                item_node = Item()\n                item_node.update(container)\n                item_node[\"Name\"] = container.get(\"objectName\", \"NO NAME\")\n                item_node[\"isNotFound\"] = True\n                self.add_child(item_node, parent=group_node)\n\n        # TODO Use product icons\n        family_icon = qtawesome.icon(\n            \"fa.folder\", color=\"#0091B2\"\n        )\n        # Prepare site sync specific data\n        progress_by_id = self._controller.get_representations_site_progress(\n            set(grouped.keys())\n        )\n        sites_info = self._controller.get_sites_information()\n\n        for repre_id, group_dict in sorted(grouped.items()):\n            group_containers = group_dict[\"containers\"]\n            representation = group_dict[\"representation\"]\n            version = group_dict[\"version\"]\n            subset = group_dict[\"subset\"]\n            asset = group_dict[\"asset\"]\n\n            # Get the primary family\n            maj_version, _ = schema.get_schema_version(subset[\"schema\"])\n            if maj_version < 3:\n                src_doc = version\n            else:\n                src_doc = subset\n\n            prim_family = src_doc[\"data\"].get(\"family\")\n            if not prim_family:\n                families = src_doc[\"data\"].get(\"families\")\n                if families:\n                    prim_family = families[0]\n\n            # Store the highest available version so the model can know\n            # whether current version is currently up-to-date.\n            highest_version = get_last_version_by_subset_id(\n                project_name, version[\"parent\"]\n            )\n\n            # create the group header\n            group_node = Item()\n            group_node[\"Name\"] = \"{}_{}: ({})\".format(\n                asset[\"name\"], subset[\"name\"], representation[\"name\"]\n            )\n            group_node[\"representation\"] = repre_id\n            group_node[\"version\"] = version[\"name\"]\n            group_node[\"highest_version\"] = highest_version[\"name\"]\n            group_node[\"family\"] = prim_family or \"\"\n            group_node[\"familyIcon\"] = family_icon\n            group_node[\"count\"] = len(group_containers)\n            group_node[\"isGroupNode\"] = True\n            group_node[\"group\"] = subset[\"data\"].get(\"subsetGroup\")\n\n            # Site sync specific data\n            progress = progress_by_id[repre_id]\n            group_node.update(sites_info)\n            group_node[\"active_site_progress\"] = progress[\"active_site\"]\n            group_node[\"remote_site_progress\"] = progress[\"remote_site\"]\n\n            self.add_child(group_node, parent=parent)\n\n            for container in group_containers:\n                item_node = Item()\n                item_node.update(container)\n\n                # store the current version on the item\n                item_node[\"version\"] = version[\"name\"]\n\n                # Remapping namespace to item name.\n                # Noted that the name key is capital \"N\", by doing this, we\n                # can view namespace in GUI without changing container data.\n                item_node[\"Name\"] = container[\"namespace\"]\n\n                self.add_child(item_node, parent=group_node)\n\n        self.endResetModel()\n\n        return self._root_item\n\n    def _query_entities(self, project_name, repre_ids):\n        \"\"\"Query entities for representations from containers.\n\n        Returns:\n            tuple[dict, dict, dict, dict]: Representation, version, product\n                and folder documents by id.\n        \"\"\"\n\n        repres_by_id = {}\n        versions_by_id = {}\n        products_by_id = {}\n        folders_by_id = {}\n        output = (\n            repres_by_id,\n            versions_by_id,\n            products_by_id,\n            folders_by_id,\n        )\n\n        filtered_repre_ids = set()\n        for repre_id in repre_ids:\n            # Filter out invalid representation ids\n            # NOTE: This is added because scenes from OpenPype did contain\n            #   ObjectId from mongo.\n            try:\n                uuid.UUID(repre_id)\n                filtered_repre_ids.add(repre_id)\n            except ValueError:\n                continue\n        if not filtered_repre_ids:\n            return output\n\n        repre_docs = get_representations(project_name, repre_ids)\n        repres_by_id.update({\n            repre_doc[\"_id\"]: repre_doc\n            for repre_doc in repre_docs\n        })\n        version_ids = {\n            repre_doc[\"parent\"] for repre_doc in repres_by_id.values()\n        }\n        if not version_ids:\n            return output\n\n        version_docs = get_versions(project_name, version_ids, hero=True)\n        versions_by_id.update({\n            version_doc[\"_id\"]: version_doc\n            for version_doc in version_docs\n        })\n        hero_versions_by_subversion_id = collections.defaultdict(list)\n        for version_doc in versions_by_id.values():\n            if version_doc[\"type\"] != \"hero_version\":\n                continue\n            subversion = version_doc[\"version_id\"]\n            hero_versions_by_subversion_id[subversion].append(version_doc)\n\n        if hero_versions_by_subversion_id:\n            subversion_ids = set(\n                hero_versions_by_subversion_id.keys()\n            )\n            subversion_docs = get_versions(project_name, subversion_ids)\n            for subversion_doc in subversion_docs:\n                subversion_id = subversion_doc[\"_id\"]\n                subversion_ids.discard(subversion_id)\n                h_version_docs = hero_versions_by_subversion_id[subversion_id]\n                for version_doc in h_version_docs:\n                    version_doc[\"name\"] = HeroVersionType(\n                        subversion_doc[\"name\"]\n                    )\n                    version_doc[\"data\"] = copy.deepcopy(\n                        subversion_doc[\"data\"]\n                    )\n\n            for subversion_id in subversion_ids:\n                h_version_docs = hero_versions_by_subversion_id[subversion_id]\n                for version_doc in h_version_docs:\n                    versions_by_id.pop(version_doc[\"_id\"])\n\n        product_ids = {\n            version_doc[\"parent\"]\n            for version_doc in versions_by_id.values()\n        }\n        if not product_ids:\n            return output\n        product_docs = get_subsets(project_name, product_ids)\n        products_by_id.update({\n            product_doc[\"_id\"]: product_doc\n            for product_doc in product_docs\n        })\n        folder_ids = {\n            product_doc[\"parent\"]\n            for product_doc in products_by_id.values()\n        }\n        if not folder_ids:\n            return output\n\n        folder_docs = get_assets(project_name, folder_ids)\n        folders_by_id.update({\n            folder_doc[\"_id\"]: folder_doc\n            for folder_doc in folder_docs\n        })\n        return output\n\n\nclass FilterProxyModel(QtCore.QSortFilterProxyModel):\n    \"\"\"Filter model to where key column's value is in the filtered tags\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(FilterProxyModel, self).__init__(*args, **kwargs)\n        self._filter_outdated = False\n        self._hierarchy_view = False\n\n    def filterAcceptsRow(self, row, parent):\n        model = self.sourceModel()\n        source_index = model.index(row, self.filterKeyColumn(), parent)\n\n        # Always allow bottom entries (individual containers), since their\n        # parent group hidden if it wouldn't have been validated.\n        rows = model.rowCount(source_index)\n        if not rows:\n            return True\n\n        # Filter by regex\n        if hasattr(self, \"filterRegExp\"):\n            regex = self.filterRegExp()\n        else:\n            regex = self.filterRegularExpression()\n        pattern = regex.pattern()\n        if pattern:\n            pattern = re.escape(pattern)\n\n            if not self._matches(row, parent, pattern):\n                return False\n\n        if self._filter_outdated:\n            # When filtering to outdated we filter the up to date entries\n            # thus we \"allow\" them when they are outdated\n            if not self._is_outdated(row, parent):\n                return False\n\n        return True\n\n    def set_filter_outdated(self, state):\n        \"\"\"Set whether to show the outdated entries only.\"\"\"\n        state = bool(state)\n\n        if state != self._filter_outdated:\n            self._filter_outdated = bool(state)\n            self.invalidateFilter()\n\n    def set_hierarchy_view(self, state):\n        state = bool(state)\n\n        if state != self._hierarchy_view:\n            self._hierarchy_view = state\n\n    def _is_outdated(self, row, parent):\n        \"\"\"Return whether row is outdated.\n\n        A row is considered outdated if it has \"version\" and \"highest_version\"\n        data and in the internal data structure, and they are not of an\n        equal value.\n\n        \"\"\"\n        def outdated(node):\n            version = node.get(\"version\", None)\n            highest = node.get(\"highest_version\", None)\n\n            # Always allow indices that have no version data at all\n            if version is None and highest is None:\n                return True\n\n            # If either a version or highest is present but not the other\n            # consider the item invalid.\n            if not self._hierarchy_view:\n                # Skip this check if in hierarchy view, or the child item\n                # node will be hidden even it's actually outdated.\n                if version is None or highest is None:\n                    return False\n            return version != highest\n\n        index = self.sourceModel().index(row, self.filterKeyColumn(), parent)\n\n        # The scene contents are grouped by \"representation\", e.g. the same\n        # \"representation\" loaded twice is grouped under the same header.\n        # Since the version check filters these parent groups we skip that\n        # check for the individual children.\n        has_parent = index.parent().isValid()\n        if has_parent and not self._hierarchy_view:\n            return True\n\n        # Filter to those that have the different version numbers\n        node = index.internalPointer()\n        if outdated(node):\n            return True\n\n        if self._hierarchy_view:\n            for _node in walk_hierarchy(node):\n                if outdated(_node):\n                    return True\n\n        return False\n\n    def _matches(self, row, parent, pattern):\n        \"\"\"Return whether row matches regex pattern.\n\n        Args:\n            row (int): row number in model\n            parent (QtCore.QModelIndex): parent index\n            pattern (regex.pattern): pattern to check for in key\n\n        Returns:\n            bool\n\n        \"\"\"\n        model = self.sourceModel()\n        column = self.filterKeyColumn()\n        role = self.filterRole()\n\n        def matches(row, parent, pattern):\n            index = model.index(row, column, parent)\n            key = model.data(index, role)\n            if re.search(pattern, key, re.IGNORECASE):\n                return True\n\n        if matches(row, parent, pattern):\n            return True\n\n        # Also allow if any of the children matches\n        source_index = model.index(row, column, parent)\n        rows = model.rowCount(source_index)\n\n        if any(\n            matches(idx, source_index, pattern)\n            for idx in range(rows)\n        ):\n            return True\n\n        if not self._hierarchy_view:\n            return False\n\n        for idx in range(rows):\n            child_index = model.index(idx, column, source_index)\n            child_rows = model.rowCount(child_index)\n            return any(\n                self._matches(child_idx, child_index, pattern)\n                for child_idx in range(child_rows)\n            )\n\n        return True\n"
  },
  {
    "path": "openpype/tools/ayon_sceneinventory/models/__init__.py",
    "content": "from .site_sync import SiteSyncModel\n\n\n__all__ = (\n    \"SiteSyncModel\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_sceneinventory/models/site_sync.py",
    "content": "from openpype.client import get_representations\nfrom openpype.modules import ModulesManager\n\nNOT_SET = object()\n\n\nclass SiteSyncModel:\n    def __init__(self, controller):\n        self._controller = controller\n\n        self._sync_server_module = NOT_SET\n        self._sync_server_enabled = None\n        self._active_site = NOT_SET\n        self._remote_site = NOT_SET\n        self._active_site_provider = NOT_SET\n        self._remote_site_provider = NOT_SET\n\n    def reset(self):\n        self._sync_server_module = NOT_SET\n        self._sync_server_enabled = None\n        self._active_site = NOT_SET\n        self._remote_site = NOT_SET\n        self._active_site_provider = NOT_SET\n        self._remote_site_provider = NOT_SET\n\n    def is_sync_server_enabled(self):\n        \"\"\"Site sync is enabled.\n\n        Returns:\n            bool: Is enabled or not.\n        \"\"\"\n\n        self._cache_sync_server_module()\n        return self._sync_server_enabled\n\n    def get_site_provider_icons(self):\n        \"\"\"Icon paths per provider.\n\n        Returns:\n            dict[str, str]: Path by provider name.\n        \"\"\"\n\n        if not self.is_sync_server_enabled():\n            return {}\n        site_sync_addon = self._get_sync_server_module()\n        return site_sync_addon.get_site_icons()\n\n    def get_sites_information(self):\n        return {\n            \"active_site\": self._get_active_site(),\n            \"active_site_provider\": self._get_active_site_provider(),\n            \"remote_site\": self._get_remote_site(),\n            \"remote_site_provider\": self._get_remote_site_provider()\n        }\n\n    def get_representations_site_progress(self, representation_ids):\n        \"\"\"Get progress of representations sync.\"\"\"\n\n        representation_ids = set(representation_ids)\n        output = {\n            repre_id: {\n                \"active_site\": 0,\n                \"remote_site\": 0,\n            }\n            for repre_id in representation_ids\n        }\n        if not self.is_sync_server_enabled():\n            return output\n\n        project_name = self._controller.get_current_project_name()\n        site_sync = self._get_sync_server_module()\n        repre_docs = get_representations(project_name, representation_ids)\n        active_site = self._get_active_site()\n        remote_site = self._get_remote_site()\n\n        for repre_doc in repre_docs:\n            repre_output = output[repre_doc[\"_id\"]]\n            result = site_sync.get_progress_for_repre(\n                repre_doc, active_site, remote_site\n            )\n            repre_output[\"active_site\"] = result[active_site]\n            repre_output[\"remote_site\"] = result[remote_site]\n\n        return output\n\n    def resync_representations(self, representation_ids, site_type):\n        \"\"\"\n\n        Args:\n            representation_ids (Iterable[str]): Representation ids.\n            site_type (Literal[active_site, remote_site]): Site type.\n        \"\"\"\n\n        project_name = self._controller.get_current_project_name()\n        site_sync = self._get_sync_server_module()\n        active_site = self._get_active_site()\n        remote_site = self._get_remote_site()\n        progress = self.get_representations_site_progress(\n            representation_ids\n        )\n        for repre_id in representation_ids:\n            repre_progress = progress.get(repre_id)\n            if not repre_progress:\n                continue\n\n            if site_type == \"active_site\":\n                # check opposite from added site, must be 1 or unable to sync\n                check_progress = repre_progress[\"remote_site\"]\n                site = active_site\n            else:\n                check_progress = repre_progress[\"active_site\"]\n                site = remote_site\n\n            if check_progress == 1:\n                site_sync.add_site(\n                    project_name, repre_id, site, force=True\n                )\n\n    def _get_sync_server_module(self):\n        self._cache_sync_server_module()\n        return self._sync_server_module\n\n    def _cache_sync_server_module(self):\n        if self._sync_server_module is not NOT_SET:\n            return self._sync_server_module\n        manager = ModulesManager()\n        site_sync = manager.modules_by_name.get(\"sync_server\")\n        sync_enabled = site_sync is not None and site_sync.enabled\n        self._sync_server_module = site_sync\n        self._sync_server_enabled = sync_enabled\n\n    def _get_active_site(self):\n        if self._active_site is NOT_SET:\n            self._cache_sites()\n        return self._active_site\n\n    def _get_remote_site(self):\n        if self._remote_site is NOT_SET:\n            self._cache_sites()\n        return self._remote_site\n\n    def _get_active_site_provider(self):\n        if self._active_site_provider is NOT_SET:\n            self._cache_sites()\n        return self._active_site_provider\n\n    def _get_remote_site_provider(self):\n        if self._remote_site_provider is NOT_SET:\n            self._cache_sites()\n        return self._remote_site_provider\n\n    def _cache_sites(self):\n        active_site = None\n        remote_site = None\n        active_site_provider = None\n        remote_site_provider = None\n        if self.is_sync_server_enabled():\n            site_sync = self._get_sync_server_module()\n            project_name = self._controller.get_current_project_name()\n            active_site = site_sync.get_active_site(project_name)\n            remote_site = site_sync.get_remote_site(project_name)\n            active_site_provider = \"studio\"\n            remote_site_provider = \"studio\"\n            if active_site != \"studio\":\n                active_site_provider = site_sync.get_provider_for_site(\n                    project_name, active_site\n                )\n            if remote_site != \"studio\":\n                remote_site_provider = site_sync.get_provider_for_site(\n                    project_name, remote_site\n                )\n\n        self._active_site = active_site\n        self._remote_site = remote_site\n        self._active_site_provider = active_site_provider\n        self._remote_site_provider = remote_site_provider\n"
  },
  {
    "path": "openpype/tools/ayon_sceneinventory/switch_dialog/__init__.py",
    "content": "from .dialog import SwitchAssetDialog\n\n\n__all__ = (\n    \"SwitchAssetDialog\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py",
    "content": "import collections\nimport logging\n\nfrom qtpy import QtWidgets, QtCore\nimport qtawesome\n\nfrom openpype.client import (\n    get_assets,\n    get_subset_by_name,\n    get_subsets,\n    get_versions,\n    get_hero_versions,\n    get_last_versions,\n    get_representations,\n)\nfrom openpype.pipeline.load import (\n    discover_loader_plugins,\n    switch_container,\n    get_repres_contexts,\n    loaders_from_repre_context,\n    LoaderSwitchNotImplementedError,\n    IncompatibleLoaderError,\n    LoaderNotFoundError\n)\n\nfrom .widgets import (\n    ButtonWithMenu,\n    SearchComboBox\n)\nfrom .folders_input import FoldersField\n\nlog = logging.getLogger(\"SwitchAssetDialog\")\n\n\nclass ValidationState:\n    def __init__(self):\n        self.folder_ok = True\n        self.product_ok = True\n        self.repre_ok = True\n\n    @property\n    def all_ok(self):\n        return (\n            self.folder_ok\n            and self.product_ok\n            and self.repre_ok\n        )\n\n\nclass SwitchAssetDialog(QtWidgets.QDialog):\n    \"\"\"Widget to support asset switching\"\"\"\n\n    MIN_WIDTH = 550\n\n    switched = QtCore.Signal()\n\n    def __init__(self, controller, parent=None, items=None):\n        super(SwitchAssetDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"Switch selected items ...\")\n\n        # Force and keep focus dialog\n        self.setModal(True)\n\n        folders_field = FoldersField(controller, self)\n        products_combox = SearchComboBox(self)\n        repres_combobox = SearchComboBox(self)\n\n        products_combox.set_placeholder(\"<product>\")\n        repres_combobox.set_placeholder(\"<representation>\")\n\n        folder_label = QtWidgets.QLabel(self)\n        product_label = QtWidgets.QLabel(self)\n        repre_label = QtWidgets.QLabel(self)\n\n        current_folder_btn = QtWidgets.QPushButton(\"Use current folder\", self)\n\n        accept_icon = qtawesome.icon(\"fa.check\", color=\"white\")\n        accept_btn = ButtonWithMenu(self)\n        accept_btn.setIcon(accept_icon)\n\n        main_layout = QtWidgets.QGridLayout(self)\n        # Folder column\n        main_layout.addWidget(current_folder_btn, 0, 0)\n        main_layout.addWidget(folders_field, 1, 0)\n        main_layout.addWidget(folder_label, 2, 0)\n        # Product column\n        main_layout.addWidget(products_combox, 1, 1)\n        main_layout.addWidget(product_label, 2, 1)\n        # Representation column\n        main_layout.addWidget(repres_combobox, 1, 2)\n        main_layout.addWidget(repre_label, 2, 2)\n        # Btn column\n        main_layout.addWidget(accept_btn, 1, 3)\n        main_layout.setColumnStretch(0, 1)\n        main_layout.setColumnStretch(1, 1)\n        main_layout.setColumnStretch(2, 1)\n        main_layout.setColumnStretch(3, 0)\n\n        show_timer = QtCore.QTimer()\n        show_timer.setInterval(0)\n        show_timer.setSingleShot(False)\n\n        show_timer.timeout.connect(self._on_show_timer)\n        folders_field.value_changed.connect(\n            self._combobox_value_changed\n        )\n        products_combox.currentIndexChanged.connect(\n            self._combobox_value_changed\n        )\n        repres_combobox.currentIndexChanged.connect(\n            self._combobox_value_changed\n        )\n        accept_btn.clicked.connect(self._on_accept)\n        current_folder_btn.clicked.connect(self._on_current_folder)\n\n        self._show_timer = show_timer\n        self._show_counter = 0\n\n        self._current_folder_btn = current_folder_btn\n\n        self._folders_field = folders_field\n        self._products_combox = products_combox\n        self._representations_box = repres_combobox\n\n        self._folder_label = folder_label\n        self._product_label = product_label\n        self._repre_label = repre_label\n\n        self._accept_btn = accept_btn\n\n        self.setMinimumWidth(self.MIN_WIDTH)\n\n        # Set default focus to accept button so you don't directly type in\n        # first asset field, this also allows to see the placeholder value.\n        accept_btn.setFocus()\n\n        self._folder_docs_by_id = {}\n        self._product_docs_by_id = {}\n        self._version_docs_by_id = {}\n        self._repre_docs_by_id = {}\n\n        self._missing_folder_ids = set()\n        self._missing_product_ids = set()\n        self._missing_version_ids = set()\n        self._missing_repre_ids = set()\n        self._missing_docs = False\n\n        self._inactive_folder_ids = set()\n        self._inactive_product_ids = set()\n        self._inactive_repre_ids = set()\n\n        self._init_folder_id = None\n        self._init_product_name = None\n        self._init_repre_name = None\n\n        self._fill_check = False\n\n        self._project_name = controller.get_current_project_name()\n        self._folder_id = controller.get_current_folder_id()\n\n        self._current_folder_btn.setEnabled(self._folder_id is not None)\n\n        self._controller = controller\n\n        self._items = items\n        self._prepare_content_data()\n\n    def showEvent(self, event):\n        super(SwitchAssetDialog, self).showEvent(event)\n        self._show_timer.start()\n\n    def refresh(self, init_refresh=False):\n        \"\"\"Build the need comboboxes with content\"\"\"\n        if not self._fill_check and not init_refresh:\n            return\n\n        self._fill_check = False\n\n        validation_state = ValidationState()\n        self._folders_field.refresh()\n        # Set other comboboxes to empty if any document is missing or\n        #   any folder of loaded representations is archived.\n        self._is_folder_ok(validation_state)\n        if validation_state.folder_ok:\n            product_values = self._get_product_box_values()\n            self._fill_combobox(product_values, \"product\")\n            self._is_product_ok(validation_state)\n\n        if validation_state.folder_ok and validation_state.product_ok:\n            repre_values = sorted(self._representations_box_values())\n            self._fill_combobox(repre_values, \"repre\")\n            self._is_repre_ok(validation_state)\n\n        # Fill comboboxes with values\n        self.set_labels()\n\n        self.apply_validations(validation_state)\n\n        self._build_loaders_menu()\n\n        if init_refresh:\n            # pre select context if possible\n            self._folders_field.set_selected_item(self._init_folder_id)\n            self._products_combox.set_valid_value(self._init_product_name)\n            self._representations_box.set_valid_value(self._init_repre_name)\n\n        self._fill_check = True\n\n    def set_labels(self):\n        folder_label = self._folders_field.get_selected_folder_label()\n        product_label = self._products_combox.get_valid_value()\n        repre_label = self._representations_box.get_valid_value()\n\n        default = \"*No changes\"\n        self._folder_label.setText(folder_label or default)\n        self._product_label.setText(product_label or default)\n        self._repre_label.setText(repre_label or default)\n\n    def apply_validations(self, validation_state):\n        error_msg = \"*Please select\"\n        error_sheet = \"border: 1px solid red;\"\n\n        product_sheet = None\n        repre_sheet = None\n        accept_state = \"\"\n        if validation_state.folder_ok is False:\n            self._folder_label.setText(error_msg)\n        elif validation_state.product_ok is False:\n            product_sheet = error_sheet\n            self._product_label.setText(error_msg)\n        elif validation_state.repre_ok is False:\n            repre_sheet = error_sheet\n            self._repre_label.setText(error_msg)\n\n        if validation_state.all_ok:\n            accept_state = \"1\"\n\n        self._folders_field.set_valid(validation_state.folder_ok)\n        self._products_combox.setStyleSheet(product_sheet or \"\")\n        self._representations_box.setStyleSheet(repre_sheet or \"\")\n\n        self._accept_btn.setEnabled(validation_state.all_ok)\n        self._set_style_property(self._accept_btn, \"state\", accept_state)\n\n    def find_last_versions(self, product_ids):\n        project_name = self._project_name\n        return get_last_versions(\n            project_name,\n            subset_ids=product_ids,\n            fields=[\"_id\", \"parent\", \"type\"]\n        )\n\n    def _on_show_timer(self):\n        if self._show_counter == 2:\n            self._show_timer.stop()\n            self.refresh(True)\n        else:\n            self._show_counter += 1\n\n    def _prepare_content_data(self):\n        repre_ids = {\n            item[\"representation\"]\n            for item in self._items\n        }\n\n        project_name = self._project_name\n        repres = list(get_representations(\n            project_name,\n            representation_ids=repre_ids,\n            archived=True,\n        ))\n        repres_by_id = {str(repre[\"_id\"]): repre for repre in repres}\n\n        content_repre_docs_by_id = {}\n        inactive_repre_ids = set()\n        missing_repre_ids = set()\n        version_ids = set()\n        for repre_id in repre_ids:\n            repre_doc = repres_by_id.get(repre_id)\n            if repre_doc is None:\n                missing_repre_ids.add(repre_id)\n            elif repres_by_id[repre_id][\"type\"] == \"archived_representation\":\n                inactive_repre_ids.add(repre_id)\n                version_ids.add(repre_doc[\"parent\"])\n            else:\n                content_repre_docs_by_id[repre_id] = repre_doc\n                version_ids.add(repre_doc[\"parent\"])\n\n        version_docs = get_versions(\n            project_name,\n            version_ids=version_ids,\n            hero=True\n        )\n        content_version_docs_by_id = {}\n        for version_doc in version_docs:\n            version_id = version_doc[\"_id\"]\n            content_version_docs_by_id[version_id] = version_doc\n\n        missing_version_ids = set()\n        product_ids = set()\n        for version_id in version_ids:\n            version_doc = content_version_docs_by_id.get(version_id)\n            if version_doc is None:\n                missing_version_ids.add(version_id)\n            else:\n                product_ids.add(version_doc[\"parent\"])\n\n        product_docs = get_subsets(\n            project_name, subset_ids=product_ids, archived=True\n        )\n        product_docs_by_id = {sub[\"_id\"]: sub for sub in product_docs}\n\n        folder_ids = set()\n        inactive_product_ids = set()\n        missing_product_ids = set()\n        content_product_docs_by_id = {}\n        for product_id in product_ids:\n            product_doc = product_docs_by_id.get(product_id)\n            if product_doc is None:\n                missing_product_ids.add(product_id)\n            elif product_doc[\"type\"] == \"archived_subset\":\n                folder_ids.add(product_doc[\"parent\"])\n                inactive_product_ids.add(product_id)\n            else:\n                folder_ids.add(product_doc[\"parent\"])\n                content_product_docs_by_id[product_id] = product_doc\n\n        folder_docs = get_assets(\n            project_name, asset_ids=folder_ids, archived=True\n        )\n        folder_docs_by_id = {\n            folder_doc[\"_id\"]: folder_doc\n            for folder_doc in folder_docs\n        }\n\n        missing_folder_ids = set()\n        inactive_folder_ids = set()\n        content_folder_docs_by_id = {}\n        for folder_id in folder_ids:\n            folder_doc = folder_docs_by_id.get(folder_id)\n            if folder_doc is None:\n                missing_folder_ids.add(folder_id)\n            elif folder_doc[\"type\"] == \"archived_asset\":\n                inactive_folder_ids.add(folder_id)\n            else:\n                content_folder_docs_by_id[folder_id] = folder_doc\n\n        # stash context values, works only for single representation\n        init_folder_id = None\n        init_product_name = None\n        init_repre_name = None\n        if len(repres) == 1:\n            init_repre_doc = repres[0]\n            init_version_doc = content_version_docs_by_id.get(\n                init_repre_doc[\"parent\"])\n            init_product_doc = None\n            init_folder_doc = None\n            if init_version_doc:\n                init_product_doc = content_product_docs_by_id.get(\n                    init_version_doc[\"parent\"]\n                )\n            if init_product_doc:\n                init_folder_doc = content_folder_docs_by_id.get(\n                    init_product_doc[\"parent\"]\n                )\n            if init_folder_doc:\n                init_repre_name = init_repre_doc[\"name\"]\n                init_product_name = init_product_doc[\"name\"]\n                init_folder_id = init_folder_doc[\"_id\"]\n\n        self._init_folder_id = init_folder_id\n        self._init_product_name = init_product_name\n        self._init_repre_name = init_repre_name\n\n        self._folder_docs_by_id = content_folder_docs_by_id\n        self._product_docs_by_id = content_product_docs_by_id\n        self._version_docs_by_id = content_version_docs_by_id\n        self._repre_docs_by_id = content_repre_docs_by_id\n\n        self._missing_folder_ids = missing_folder_ids\n        self._missing_product_ids = missing_product_ids\n        self._missing_version_ids = missing_version_ids\n        self._missing_repre_ids = missing_repre_ids\n        self._missing_docs = (\n            bool(missing_folder_ids)\n            or bool(missing_version_ids)\n            or bool(missing_product_ids)\n            or bool(missing_repre_ids)\n        )\n\n        self._inactive_folder_ids = inactive_folder_ids\n        self._inactive_product_ids = inactive_product_ids\n        self._inactive_repre_ids = inactive_repre_ids\n\n    def _combobox_value_changed(self, *args, **kwargs):\n        self.refresh()\n\n    def _build_loaders_menu(self):\n        repre_ids = self._get_current_output_repre_ids()\n        loaders = self._get_loaders(repre_ids)\n        # Get and destroy the action group\n        self._accept_btn.clear_actions()\n\n        if not loaders:\n            return\n\n        # Build new action group\n        group = QtWidgets.QActionGroup(self._accept_btn)\n\n        for loader in loaders:\n            # Label\n            label = getattr(loader, \"label\", None)\n            if label is None:\n                label = loader.__name__\n\n            action = group.addAction(label)\n            # action = QtWidgets.QAction(label)\n            action.setData(loader)\n\n            # Support font-awesome icons using the `.icon` and `.color`\n            # attributes on plug-ins.\n            icon = getattr(loader, \"icon\", None)\n            if icon is not None:\n                try:\n                    key = \"fa.{0}\".format(icon)\n                    color = getattr(loader, \"color\", \"white\")\n                    action.setIcon(qtawesome.icon(key, color=color))\n\n                except Exception as exc:\n                    print(\"Unable to set icon for loader {}: {}\".format(\n                        loader, str(exc)\n                    ))\n\n            self._accept_btn.add_action(action)\n\n        group.triggered.connect(self._on_action_clicked)\n\n    def _on_action_clicked(self, action):\n        loader_plugin = action.data()\n        self._trigger_switch(loader_plugin)\n\n    def _get_loaders(self, repre_ids):\n        repre_contexts = None\n        if repre_ids:\n            repre_contexts = get_repres_contexts(repre_ids)\n\n        if not repre_contexts:\n            return list()\n\n        available_loaders = []\n        for loader_plugin in discover_loader_plugins():\n            # Skip loaders without switch method\n            if not hasattr(loader_plugin, \"switch\"):\n                continue\n\n            # Skip utility loaders\n            if (\n                hasattr(loader_plugin, \"is_utility\")\n                and loader_plugin.is_utility\n            ):\n                continue\n            available_loaders.append(loader_plugin)\n\n        loaders = None\n        for repre_context in repre_contexts.values():\n            _loaders = set(loaders_from_repre_context(\n                available_loaders, repre_context\n            ))\n            if loaders is None:\n                loaders = _loaders\n            else:\n                loaders = _loaders.intersection(loaders)\n\n            if not loaders:\n                break\n\n        if loaders is None:\n            loaders = []\n        else:\n            loaders = list(loaders)\n\n        return loaders\n\n    def _fill_combobox(self, values, combobox_type):\n        if combobox_type == \"product\":\n            combobox_widget = self._products_combox\n        elif combobox_type == \"repre\":\n            combobox_widget = self._representations_box\n        else:\n            return\n        selected_value = combobox_widget.get_valid_value()\n\n        # Fill combobox\n        if values is not None:\n            combobox_widget.populate(list(sorted(values)))\n            if selected_value and selected_value in values:\n                index = None\n                for idx in range(combobox_widget.count()):\n                    if selected_value == str(combobox_widget.itemText(idx)):\n                        index = idx\n                        break\n                if index is not None:\n                    combobox_widget.setCurrentIndex(index)\n\n    def _set_style_property(self, widget, name, value):\n        cur_value = widget.property(name)\n        if cur_value == value:\n            return\n        widget.setProperty(name, value)\n        widget.style().polish(widget)\n\n    def _get_current_output_repre_ids(self):\n        # NOTE hero versions are not used because it is expected that\n        # hero version has same representations as latests\n        selected_folder_id = self._folders_field.get_selected_folder_id()\n        selected_product_name = self._products_combox.currentText()\n        selected_repre = self._representations_box.currentText()\n\n        # Nothing is selected\n        # [ ] [ ] [ ]\n        if (\n            not selected_folder_id\n            and not selected_product_name\n            and not selected_repre\n        ):\n            return list(self._repre_docs_by_id.keys())\n\n        # Everything is selected\n        # [x] [x] [x]\n        if selected_folder_id and selected_product_name and selected_repre:\n            return self._get_current_output_repre_ids_xxx(\n                selected_folder_id, selected_product_name, selected_repre\n            )\n\n        # [x] [x] [ ]\n        # If folder and product is selected\n        if selected_folder_id and selected_product_name:\n            return self._get_current_output_repre_ids_xxo(\n                selected_folder_id, selected_product_name\n            )\n\n        # [x] [ ] [x]\n        # If folder and repre is selected\n        if selected_folder_id and selected_repre:\n            return self._get_current_output_repre_ids_xox(\n                selected_folder_id, selected_repre\n            )\n\n        # [x] [ ] [ ]\n        # If folder and product is selected\n        if selected_folder_id:\n            return self._get_current_output_repre_ids_xoo(selected_folder_id)\n\n        # [ ] [x] [x]\n        if selected_product_name and selected_repre:\n            return self._get_current_output_repre_ids_oxx(\n                selected_product_name, selected_repre\n            )\n\n        # [ ] [x] [ ]\n        if selected_product_name:\n            return self._get_current_output_repre_ids_oxo(\n                selected_product_name\n            )\n\n        # [ ] [ ] [x]\n        return self._get_current_output_repre_ids_oox(selected_repre)\n\n    def _get_current_output_repre_ids_xxx(\n        self, folder_id, selected_product_name, selected_repre\n    ):\n        project_name = self._project_name\n        product_doc = get_subset_by_name(\n            project_name,\n            selected_product_name,\n            folder_id,\n            fields=[\"_id\"]\n        )\n\n        product_id = product_doc[\"_id\"]\n        last_versions_by_product_id = self.find_last_versions([product_id])\n        version_doc = last_versions_by_product_id.get(product_id)\n        if not version_doc:\n            return []\n\n        repre_docs = get_representations(\n            project_name,\n            version_ids=[version_doc[\"_id\"]],\n            representation_names=[selected_repre],\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_current_output_repre_ids_xxo(self, folder_id, product_name):\n        project_name = self._project_name\n        product_doc = get_subset_by_name(\n            project_name,\n            product_name,\n            folder_id,\n            fields=[\"_id\"]\n        )\n        if not product_doc:\n            return []\n\n        repre_names = set()\n        for repre_doc in self._repre_docs_by_id.values():\n            repre_names.add(repre_doc[\"name\"])\n\n        # TODO where to take version ids?\n        version_ids = []\n        repre_docs = get_representations(\n            project_name,\n            representation_names=repre_names,\n            version_ids=version_ids,\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_current_output_repre_ids_xox(self, folder_id, selected_repre):\n        product_names = {\n            product_doc[\"name\"]\n            for product_doc in self._product_docs_by_id.values()\n        }\n\n        project_name = self._project_name\n        product_docs = get_subsets(\n            project_name,\n            asset_ids=[folder_id],\n            subset_names=product_names,\n            fields=[\"_id\", \"name\"]\n        )\n        product_name_by_id = {\n            product_doc[\"_id\"]: product_doc[\"name\"]\n            for product_doc in product_docs\n        }\n        product_ids = list(product_name_by_id.keys())\n        last_versions_by_product_id = self.find_last_versions(product_ids)\n        last_version_id_by_product_name = {}\n        for product_id, last_version in last_versions_by_product_id.items():\n            product_name = product_name_by_id[product_id]\n            last_version_id_by_product_name[product_name] = (\n                last_version[\"_id\"]\n            )\n\n        repre_docs = get_representations(\n            project_name,\n            version_ids=last_version_id_by_product_name.values(),\n            representation_names=[selected_repre],\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_current_output_repre_ids_xoo(self, folder_id):\n        project_name = self._project_name\n        repres_by_product_name = collections.defaultdict(set)\n        for repre_doc in self._repre_docs_by_id.values():\n            version_doc = self._version_docs_by_id[repre_doc[\"parent\"]]\n            product_doc = self._product_docs_by_id[version_doc[\"parent\"]]\n            product_name = product_doc[\"name\"]\n            repres_by_product_name[product_name].add(repre_doc[\"name\"])\n\n        product_docs = list(get_subsets(\n            project_name,\n            asset_ids=[folder_id],\n            subset_names=repres_by_product_name.keys(),\n            fields=[\"_id\", \"name\"]\n        ))\n        product_name_by_id = {\n            product_doc[\"_id\"]: product_doc[\"name\"]\n            for product_doc in product_docs\n        }\n        product_ids = list(product_name_by_id.keys())\n        last_versions_by_product_id = self.find_last_versions(product_ids)\n        last_version_id_by_product_name = {}\n        for product_id, last_version in last_versions_by_product_id.items():\n            product_name = product_name_by_id[product_id]\n            last_version_id_by_product_name[product_name] = (\n                last_version[\"_id\"]\n            )\n\n        repre_names_by_version_id = {}\n        for product_name, repre_names in repres_by_product_name.items():\n            version_id = last_version_id_by_product_name.get(product_name)\n            # This should not happen but why to crash?\n            if version_id is not None:\n                repre_names_by_version_id[version_id] = list(repre_names)\n\n        repre_docs = get_representations(\n            project_name,\n            names_by_version_ids=repre_names_by_version_id,\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_current_output_repre_ids_oxx(\n        self, product_name, selected_repre\n    ):\n        project_name = self._project_name\n        product_docs = get_subsets(\n            project_name,\n            asset_ids=self._folder_docs_by_id.keys(),\n            subset_names=[product_name],\n            fields=[\"_id\"]\n        )\n        product_ids = [product_doc[\"_id\"] for product_doc in product_docs]\n        last_versions_by_product_id = self.find_last_versions(product_ids)\n        last_version_ids = [\n            last_version[\"_id\"]\n            for last_version in last_versions_by_product_id.values()\n        ]\n        repre_docs = get_representations(\n            project_name,\n            version_ids=last_version_ids,\n            representation_names=[selected_repre],\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_current_output_repre_ids_oxo(self, product_name):\n        project_name = self._project_name\n        product_docs = get_subsets(\n            project_name,\n            asset_ids=self._folder_docs_by_id.keys(),\n            subset_names=[product_name],\n            fields=[\"_id\", \"parent\"]\n        )\n        product_docs_by_id = {\n            product_doc[\"_id\"]: product_doc\n            for product_doc in product_docs\n        }\n        if not product_docs:\n            return list()\n\n        last_versions_by_product_id = self.find_last_versions(\n            product_docs_by_id.keys()\n        )\n\n        product_id_by_version_id = {}\n        for product_id, last_version in last_versions_by_product_id.items():\n            version_id = last_version[\"_id\"]\n            product_id_by_version_id[version_id] = product_id\n\n        if not product_id_by_version_id:\n            return list()\n\n        repre_names_by_folder_id = collections.defaultdict(set)\n        for repre_doc in self._repre_docs_by_id.values():\n            version_doc = self._version_docs_by_id[repre_doc[\"parent\"]]\n            product_doc = self._product_docs_by_id[version_doc[\"parent\"]]\n            folder_doc = self._folder_docs_by_id[product_doc[\"parent\"]]\n            folder_id = folder_doc[\"_id\"]\n            repre_names_by_folder_id[folder_id].add(repre_doc[\"name\"])\n\n        repre_names_by_version_id = {}\n        for last_version_id, product_id in product_id_by_version_id.items():\n            product_doc = product_docs_by_id[product_id]\n            folder_id = product_doc[\"parent\"]\n            repre_names = repre_names_by_folder_id.get(folder_id)\n            if not repre_names:\n                continue\n            repre_names_by_version_id[last_version_id] = repre_names\n\n        repre_docs = get_representations(\n            project_name,\n            names_by_version_ids=repre_names_by_version_id,\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_current_output_repre_ids_oox(self, selected_repre):\n        project_name = self._project_name\n        repre_docs = get_representations(\n            project_name,\n            representation_names=[selected_repre],\n            version_ids=self._version_docs_by_id.keys(),\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_product_box_values(self):\n        project_name = self._project_name\n        selected_folder_id = self._folders_field.get_selected_folder_id()\n        if selected_folder_id:\n            folder_ids = [selected_folder_id]\n        else:\n            folder_ids = list(self._folder_docs_by_id.keys())\n\n        product_docs = get_subsets(\n            project_name,\n            asset_ids=folder_ids,\n            fields=[\"parent\", \"name\"]\n        )\n\n        product_names_by_parent_id = collections.defaultdict(set)\n        for product_doc in product_docs:\n            product_names_by_parent_id[product_doc[\"parent\"]].add(\n                product_doc[\"name\"]\n            )\n\n        possible_product_names = None\n        for product_names in product_names_by_parent_id.values():\n            if possible_product_names is None:\n                possible_product_names = product_names\n            else:\n                possible_product_names = possible_product_names.intersection(\n                    product_names)\n\n            if not possible_product_names:\n                break\n\n        if not possible_product_names:\n            return []\n        return list(possible_product_names)\n\n    def _representations_box_values(self):\n        # NOTE hero versions are not used because it is expected that\n        # hero version has same representations as latests\n        project_name = self._project_name\n        selected_folder_id = self._folders_field.get_selected_folder_id()\n        selected_product_name = self._products_combox.currentText()\n\n        # If nothing is selected\n        # [ ] [ ] [?]\n        if not selected_folder_id and not selected_product_name:\n            # Find all representations of selection's products\n            possible_repres = get_representations(\n                project_name,\n                version_ids=self._version_docs_by_id.keys(),\n                fields=[\"parent\", \"name\"]\n            )\n\n            possible_repres_by_parent = collections.defaultdict(set)\n            for repre in possible_repres:\n                possible_repres_by_parent[repre[\"parent\"]].add(repre[\"name\"])\n\n            output_repres = None\n            for repre_names in possible_repres_by_parent.values():\n                if output_repres is None:\n                    output_repres = repre_names\n                else:\n                    output_repres = (output_repres & repre_names)\n\n                if not output_repres:\n                    break\n\n            return list(output_repres or list())\n\n        # [x] [x] [?]\n        if selected_folder_id and selected_product_name:\n            product_doc = get_subset_by_name(\n                project_name,\n                selected_product_name,\n                selected_folder_id,\n                fields=[\"_id\"]\n            )\n\n            product_id = product_doc[\"_id\"]\n            last_versions_by_product_id = self.find_last_versions([product_id])\n            version_doc = last_versions_by_product_id.get(product_id)\n            repre_docs = get_representations(\n                project_name,\n                version_ids=[version_doc[\"_id\"]],\n                fields=[\"name\"]\n            )\n            return [\n                repre_doc[\"name\"]\n                for repre_doc in repre_docs\n            ]\n\n        # [x] [ ] [?]\n        # If only folder is selected\n        if selected_folder_id:\n            # Filter products by names from content\n            product_names = {\n                product_doc[\"name\"]\n                for product_doc in self._product_docs_by_id.values()\n            }\n\n            product_docs = get_subsets(\n                project_name,\n                asset_ids=[selected_folder_id],\n                subset_names=product_names,\n                fields=[\"_id\"]\n            )\n            product_ids = {\n                product_doc[\"_id\"]\n                for product_doc in product_docs\n            }\n            if not product_ids:\n                return list()\n\n            last_versions_by_product_id = self.find_last_versions(product_ids)\n            product_id_by_version_id = {}\n            for product_id, last_version in (\n                last_versions_by_product_id.items()\n            ):\n                version_id = last_version[\"_id\"]\n                product_id_by_version_id[version_id] = product_id\n\n            if not product_id_by_version_id:\n                return list()\n\n            repre_docs = list(get_representations(\n                project_name,\n                version_ids=product_id_by_version_id.keys(),\n                fields=[\"name\", \"parent\"]\n            ))\n            if not repre_docs:\n                return list()\n\n            repre_names_by_parent = collections.defaultdict(set)\n            for repre_doc in repre_docs:\n                repre_names_by_parent[repre_doc[\"parent\"]].add(\n                    repre_doc[\"name\"]\n                )\n\n            available_repres = None\n            for repre_names in repre_names_by_parent.values():\n                if available_repres is None:\n                    available_repres = repre_names\n                    continue\n\n                available_repres = available_repres.intersection(repre_names)\n\n            return list(available_repres)\n\n        # [ ] [x] [?]\n        product_docs = list(get_subsets(\n            project_name,\n            asset_ids=self._folder_docs_by_id.keys(),\n            subset_names=[selected_product_name],\n            fields=[\"_id\", \"parent\"]\n        ))\n        if not product_docs:\n            return list()\n\n        product_docs_by_id = {\n            product_doc[\"_id\"]: product_doc\n            for product_doc in product_docs\n        }\n        last_versions_by_product_id = self.find_last_versions(\n            product_docs_by_id.keys()\n        )\n\n        product_id_by_version_id = {}\n        for product_id, last_version in last_versions_by_product_id.items():\n            version_id = last_version[\"_id\"]\n            product_id_by_version_id[version_id] = product_id\n\n        if not product_id_by_version_id:\n            return list()\n\n        repre_docs = list(\n            get_representations(\n                project_name,\n                version_ids=product_id_by_version_id.keys(),\n                fields=[\"name\", \"parent\"]\n            )\n        )\n        if not repre_docs:\n            return list()\n\n        repre_names_by_folder_id = collections.defaultdict(set)\n        for repre_doc in repre_docs:\n            product_id = product_id_by_version_id[repre_doc[\"parent\"]]\n            folder_id = product_docs_by_id[product_id][\"parent\"]\n            repre_names_by_folder_id[folder_id].add(repre_doc[\"name\"])\n\n        available_repres = None\n        for repre_names in repre_names_by_folder_id.values():\n            if available_repres is None:\n                available_repres = repre_names\n                continue\n\n            available_repres = available_repres.intersection(repre_names)\n\n        return list(available_repres)\n\n    def _is_folder_ok(self, validation_state):\n        selected_folder_id = self._folders_field.get_selected_folder_id()\n        if (\n            selected_folder_id is None\n            and (self._missing_docs or self._inactive_folder_ids)\n        ):\n            validation_state.folder_ok = False\n\n    def _is_product_ok(self, validation_state):\n        selected_folder_id = self._folders_field.get_selected_folder_id()\n        selected_product_name = self._products_combox.get_valid_value()\n\n        # [?] [x] [?]\n        # If product is selected then must be ok\n        if selected_product_name is not None:\n            return\n\n        # [ ] [ ] [?]\n        if selected_folder_id is None:\n            # If there were archived products and folder is not selected\n            if self._inactive_product_ids:\n                validation_state.product_ok = False\n            return\n\n        # [x] [ ] [?]\n        project_name = self._project_name\n        product_docs = get_subsets(\n            project_name, asset_ids=[selected_folder_id], fields=[\"name\"]\n        )\n\n        product_names = set(\n            product_doc[\"name\"]\n            for product_doc in product_docs\n        )\n\n        for product_doc in self._product_docs_by_id.values():\n            if product_doc[\"name\"] not in product_names:\n                validation_state.product_ok = False\n                break\n\n    def _is_repre_ok(self, validation_state):\n        selected_folder_id = self._folders_field.get_selected_folder_id()\n        selected_product_name = self._products_combox.get_valid_value()\n        selected_repre = self._representations_box.get_valid_value()\n\n        # [?] [?] [x]\n        # If product is selected then must be ok\n        if selected_repre is not None:\n            return\n\n        # [ ] [ ] [ ]\n        if selected_folder_id is None and selected_product_name is None:\n            if (\n                self._inactive_repre_ids\n                or self._missing_version_ids\n                or self._missing_repre_ids\n            ):\n                validation_state.repre_ok = False\n            return\n\n        # [x] [x] [ ]\n        project_name = self._project_name\n        if (\n            selected_folder_id is not None\n            and selected_product_name is not None\n        ):\n            product_doc = get_subset_by_name(\n                project_name,\n                selected_product_name,\n                selected_folder_id,\n                fields=[\"_id\"]\n            )\n            product_id = product_doc[\"_id\"]\n            last_versions_by_product_id = self.find_last_versions([product_id])\n            last_version = last_versions_by_product_id.get(product_id)\n            if not last_version:\n                validation_state.repre_ok = False\n                return\n\n            repre_docs = get_representations(\n                project_name,\n                version_ids=[last_version[\"_id\"]],\n                fields=[\"name\"]\n            )\n\n            repre_names = set(\n                repre_doc[\"name\"]\n                for repre_doc in repre_docs\n            )\n            for repre_doc in self._repre_docs_by_id.values():\n                if repre_doc[\"name\"] not in repre_names:\n                    validation_state.repre_ok = False\n                    break\n            return\n\n        # [x] [ ] [ ]\n        if selected_folder_id is not None:\n            product_docs = list(get_subsets(\n                project_name,\n                asset_ids=[selected_folder_id],\n                fields=[\"_id\", \"name\"]\n            ))\n\n            product_name_by_id = {}\n            product_ids = set()\n            for product_doc in product_docs:\n                product_id = product_doc[\"_id\"]\n                product_ids.add(product_id)\n                product_name_by_id[product_id] = product_doc[\"name\"]\n\n            last_versions_by_product_id = self.find_last_versions(product_ids)\n\n            product_id_by_version_id = {}\n            for product_id, last_version in (\n                last_versions_by_product_id.items()\n            ):\n                version_id = last_version[\"_id\"]\n                product_id_by_version_id[version_id] = product_id\n\n            repre_docs = get_representations(\n                project_name,\n                version_ids=product_id_by_version_id.keys(),\n                fields=[\"name\", \"parent\"]\n            )\n            repres_by_product_name = collections.defaultdict(set)\n            for repre_doc in repre_docs:\n                product_id = product_id_by_version_id[repre_doc[\"parent\"]]\n                product_name = product_name_by_id[product_id]\n                repres_by_product_name[product_name].add(repre_doc[\"name\"])\n\n            for repre_doc in self._repre_docs_by_id.values():\n                version_doc = self._version_docs_by_id[repre_doc[\"parent\"]]\n                product_doc = self._product_docs_by_id[version_doc[\"parent\"]]\n                repre_names = repres_by_product_name[product_doc[\"name\"]]\n                if repre_doc[\"name\"] not in repre_names:\n                    validation_state.repre_ok = False\n                    break\n            return\n\n        # [ ] [x] [ ]\n        # Product documents\n        product_docs = get_subsets(\n            project_name,\n            asset_ids=self._folder_docs_by_id.keys(),\n            subset_names=[selected_product_name],\n            fields=[\"_id\", \"name\", \"parent\"]\n        )\n        product_docs_by_id = {}\n        for product_doc in product_docs:\n            product_docs_by_id[product_doc[\"_id\"]] = product_doc\n\n        last_versions_by_product_id = self.find_last_versions(\n            product_docs_by_id.keys()\n        )\n        product_id_by_version_id = {}\n        for product_id, last_version in last_versions_by_product_id.items():\n            version_id = last_version[\"_id\"]\n            product_id_by_version_id[version_id] = product_id\n\n        repre_docs = get_representations(\n            project_name,\n            version_ids=product_id_by_version_id.keys(),\n            fields=[\"name\", \"parent\"]\n        )\n        repres_by_folder_id = collections.defaultdict(set)\n        for repre_doc in repre_docs:\n            product_id = product_id_by_version_id[repre_doc[\"parent\"]]\n            folder_id = product_docs_by_id[product_id][\"parent\"]\n            repres_by_folder_id[folder_id].add(repre_doc[\"name\"])\n\n        for repre_doc in self._repre_docs_by_id.values():\n            version_doc = self._version_docs_by_id[repre_doc[\"parent\"]]\n            product_doc = self._product_docs_by_id[version_doc[\"parent\"]]\n            folder_id = product_doc[\"parent\"]\n            repre_names = repres_by_folder_id[folder_id]\n            if repre_doc[\"name\"] not in repre_names:\n                validation_state.repre_ok = False\n                break\n\n    def _on_current_folder(self):\n        # Set initial folder as current.\n        folder_id = self._controller.get_current_folder_id()\n        if not folder_id:\n            return\n\n        selected_folder_id = self._folders_field.get_selected_folder_id()\n        if folder_id == selected_folder_id:\n            return\n\n        self._folders_field.set_selected_item(folder_id)\n        self._combobox_value_changed()\n\n    def _on_accept(self):\n        self._trigger_switch()\n\n    def _trigger_switch(self, loader=None):\n        # Use None when not a valid value or when placeholder value\n        selected_folder_id = self._folders_field.get_selected_folder_id()\n        selected_product_name = self._products_combox.get_valid_value()\n        selected_representation = self._representations_box.get_valid_value()\n\n        project_name = self._project_name\n        if selected_folder_id:\n            folder_ids = {selected_folder_id}\n        else:\n            folder_ids = set(self._folder_docs_by_id.keys())\n\n        product_names = None\n        if selected_product_name:\n            product_names = [selected_product_name]\n\n        product_docs = list(get_subsets(\n            project_name,\n            subset_names=product_names,\n            asset_ids=folder_ids\n        ))\n        product_ids = set()\n        product_docs_by_parent_and_name = collections.defaultdict(dict)\n        for product_doc in product_docs:\n            product_ids.add(product_doc[\"_id\"])\n            folder_id = product_doc[\"parent\"]\n            name = product_doc[\"name\"]\n            product_docs_by_parent_and_name[folder_id][name] = product_doc\n\n        # versions\n        _version_docs = get_versions(project_name, subset_ids=product_ids)\n        version_docs = list(reversed(\n            sorted(_version_docs, key=lambda item: item[\"name\"])\n        ))\n\n        hero_version_docs = list(get_hero_versions(\n            project_name, subset_ids=product_ids\n        ))\n\n        version_ids = set()\n        version_docs_by_parent_id_and_name = collections.defaultdict(dict)\n        for version_doc in version_docs:\n            version_ids.add(version_doc[\"_id\"])\n            product_id = version_doc[\"parent\"]\n            name = version_doc[\"name\"]\n            version_docs_by_parent_id_and_name[product_id][name] = version_doc\n\n        hero_version_docs_by_parent_id = {}\n        for hero_version_doc in hero_version_docs:\n            version_ids.add(hero_version_doc[\"_id\"])\n            parent_id = hero_version_doc[\"parent\"]\n            hero_version_docs_by_parent_id[parent_id] = hero_version_doc\n\n        repre_docs = get_representations(\n            project_name, version_ids=version_ids\n        )\n        repre_docs_by_parent_id_by_name = collections.defaultdict(dict)\n        for repre_doc in repre_docs:\n            parent_id = repre_doc[\"parent\"]\n            name = repre_doc[\"name\"]\n            repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc\n\n        for container in self._items:\n            self._switch_container(\n                container,\n                loader,\n                selected_folder_id,\n                selected_product_name,\n                selected_representation,\n                product_docs_by_parent_and_name,\n                version_docs_by_parent_id_and_name,\n                hero_version_docs_by_parent_id,\n                repre_docs_by_parent_id_by_name,\n            )\n\n        self.switched.emit()\n\n        self.close()\n\n    def _switch_container(\n        self,\n        container,\n        loader,\n        selected_folder_id,\n        selected_product_name,\n        selected_representation,\n        product_docs_by_parent_and_name,\n        version_docs_by_parent_id_and_name,\n        hero_version_docs_by_parent_id,\n        repre_docs_by_parent_id_by_name,\n    ):\n        container_repre_id = container[\"representation\"]\n        container_repre = self._repre_docs_by_id[container_repre_id]\n        container_repre_name = container_repre[\"name\"]\n        container_version_id = container_repre[\"parent\"]\n\n        container_version = self._version_docs_by_id[container_version_id]\n\n        container_product_id = container_version[\"parent\"]\n        container_product = self._product_docs_by_id[container_product_id]\n        container_product_name = container_product[\"name\"]\n\n        container_folder_id = container_product[\"parent\"]\n\n        if selected_folder_id:\n            folder_id = selected_folder_id\n        else:\n            folder_id = container_folder_id\n\n        products_by_name = product_docs_by_parent_and_name[folder_id]\n        if selected_product_name:\n            product_doc = products_by_name[selected_product_name]\n        else:\n            product_doc = products_by_name[container_product[\"name\"]]\n\n        repre_doc = None\n        product_id = product_doc[\"_id\"]\n        if container_version[\"type\"] == \"hero_version\":\n            hero_version = hero_version_docs_by_parent_id.get(\n                product_id\n            )\n            if hero_version:\n                _repres = repre_docs_by_parent_id_by_name.get(\n                    hero_version[\"_id\"]\n                )\n                if selected_representation:\n                    repre_doc = _repres.get(selected_representation)\n                else:\n                    repre_doc = _repres.get(container_repre_name)\n\n        if not repre_doc:\n            version_docs_by_name = (\n                version_docs_by_parent_id_and_name[product_id]\n            )\n            # If asset or subset are selected for switching, we use latest\n            # version else we try to keep the current container version.\n            version_name = None\n            if (\n                selected_folder_id in (None, container_folder_id)\n                and selected_product_name in (None, container_product_name)\n            ):\n                version_name = container_version.get(\"name\")\n\n            version_doc = None\n            if version_name is not None:\n                version_doc = version_docs_by_name.get(version_name)\n\n            if version_doc is None:\n                version_name = max(version_docs_by_name)\n                version_doc = version_docs_by_name[version_name]\n\n            version_id = version_doc[\"_id\"]\n            repres_by_name = repre_docs_by_parent_id_by_name[version_id]\n            if selected_representation:\n                repre_doc = repres_by_name[selected_representation]\n            else:\n                repre_doc = repres_by_name[container_repre_name]\n\n        error = None\n        try:\n            switch_container(container, repre_doc, loader)\n        except (\n            LoaderSwitchNotImplementedError,\n            IncompatibleLoaderError,\n            LoaderNotFoundError,\n        ) as exc:\n            error = str(exc)\n        except Exception:\n            error = (\n                \"Switch asset failed. \"\n                \"Search console log for more details.\"\n            )\n        if error is not None:\n            log.warning((\n                \"Couldn't switch asset.\"\n                \"See traceback for more information.\"\n            ), exc_info=True)\n            dialog = QtWidgets.QMessageBox(self)\n            dialog.setWindowTitle(\"Switch asset failed\")\n            dialog.setText(error)\n            dialog.exec_()\n"
  },
  {
    "path": "openpype/tools/ayon_sceneinventory/switch_dialog/folders_input.py",
    "content": "from qtpy import QtWidgets, QtCore\nimport qtawesome\n\nfrom openpype.tools.utils import (\n    PlaceholderLineEdit,\n    BaseClickableFrame,\n    set_style_property,\n)\nfrom openpype.tools.ayon_utils.widgets import FoldersWidget\n\nNOT_SET = object()\n\n\nclass ClickableLineEdit(QtWidgets.QLineEdit):\n    \"\"\"QLineEdit capturing left mouse click.\n\n    Triggers `clicked` signal on mouse click.\n    \"\"\"\n    clicked = QtCore.Signal()\n\n    def __init__(self, *args, **kwargs):\n        super(ClickableLineEdit, self).__init__(*args, **kwargs)\n        self.setReadOnly(True)\n        self._mouse_pressed = False\n\n    def mousePressEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self._mouse_pressed = True\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        if self._mouse_pressed:\n            self._mouse_pressed = False\n            if self.rect().contains(event.pos()):\n                self.clicked.emit()\n        event.accept()\n\n    def mouseDoubleClickEvent(self, event):\n        event.accept()\n\n\nclass ControllerWrap:\n    def __init__(self, controller):\n        self._controller = controller\n        self._selected_folder_id = None\n\n    def emit_event(self, *args, **kwargs):\n        self._controller.emit_event(*args, **kwargs)\n\n    def register_event_callback(self, *args, **kwargs):\n        self._controller.register_event_callback(*args, **kwargs)\n\n    def get_current_project_name(self):\n        return self._controller.get_current_project_name()\n\n    def get_folder_items(self, *args, **kwargs):\n        return self._controller.get_folder_items(*args, **kwargs)\n\n    def set_selected_folder(self, folder_id):\n        self._selected_folder_id = folder_id\n\n    def get_selected_folder_id(self):\n        return self._selected_folder_id\n\n\nclass FoldersDialog(QtWidgets.QDialog):\n    \"\"\"Dialog to select asset for a context of instance.\"\"\"\n\n    def __init__(self, controller, parent):\n        super(FoldersDialog, self).__init__(parent)\n        self.setWindowTitle(\"Select folder\")\n\n        filter_input = PlaceholderLineEdit(self)\n        filter_input.setPlaceholderText(\"Filter folders..\")\n\n        controller_wrap = ControllerWrap(controller)\n        folders_widget = FoldersWidget(controller_wrap, self)\n        folders_widget.set_deselectable(True)\n\n        ok_btn = QtWidgets.QPushButton(\"OK\", self)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", self)\n\n        btns_layout = QtWidgets.QHBoxLayout()\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(ok_btn)\n        btns_layout.addWidget(cancel_btn)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(filter_input, 0)\n        layout.addWidget(folders_widget, 1)\n        layout.addLayout(btns_layout, 0)\n\n        folders_widget.double_clicked.connect(self._on_ok_clicked)\n        folders_widget.refreshed.connect(self._on_folders_refresh)\n        filter_input.textChanged.connect(self._on_filter_change)\n        ok_btn.clicked.connect(self._on_ok_clicked)\n        cancel_btn.clicked.connect(self._on_cancel_clicked)\n\n        self._filter_input = filter_input\n        self._ok_btn = ok_btn\n        self._cancel_btn = cancel_btn\n\n        self._folders_widget = folders_widget\n        self._controller_wrap = controller_wrap\n\n        # Set selected folder only when user confirms the dialog\n        self._selected_folder_id = None\n        self._selected_folder_label = None\n\n        self._folder_id_to_select = NOT_SET\n\n        self._first_show = True\n        self._default_height = 500\n\n    def showEvent(self, event):\n        \"\"\"Refresh asset model on show.\"\"\"\n        super(FoldersDialog, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self._on_first_show()\n\n    def refresh(self):\n        project_name = self._controller_wrap.get_current_project_name()\n        self._folders_widget.set_project_name(project_name)\n\n    def _on_first_show(self):\n        center = self.rect().center()\n        size = self.size()\n        size.setHeight(self._default_height)\n\n        self.resize(size)\n        new_pos = self.mapToGlobal(center)\n        new_pos.setX(new_pos.x() - int(self.width() / 2))\n        new_pos.setY(new_pos.y() - int(self.height() / 2))\n        self.move(new_pos)\n\n    def _on_folders_refresh(self):\n        if self._folder_id_to_select is NOT_SET:\n            return\n        self._folders_widget.set_selected_folder(self._folder_id_to_select)\n        self._folder_id_to_select = NOT_SET\n\n    def _on_filter_change(self, text):\n        \"\"\"Trigger change of filter of folders.\"\"\"\n\n        self._folders_widget.set_name_filter(text)\n\n    def _on_cancel_clicked(self):\n        self.done(0)\n\n    def _on_ok_clicked(self):\n        self._selected_folder_id = (\n            self._folders_widget.get_selected_folder_id()\n        )\n        self._selected_folder_label = (\n            self._folders_widget.get_selected_folder_label()\n        )\n        self.done(1)\n\n    def set_selected_folder(self, folder_id):\n        \"\"\"Change preselected folder before showing the dialog.\n\n        This also resets model and clean filter.\n        \"\"\"\n\n        if (\n            self._folders_widget.is_refreshing\n            or self._folders_widget.get_project_name() is None\n        ):\n            self._folder_id_to_select = folder_id\n        else:\n            self._folders_widget.set_selected_folder(folder_id)\n\n    def get_selected_folder_id(self):\n        \"\"\"Get selected folder id.\n\n        Returns:\n            Union[str, None]: Selected folder id or None if nothing\n                is selected.\n        \"\"\"\n        return self._selected_folder_id\n\n    def get_selected_folder_label(self):\n        return self._selected_folder_label\n\n\nclass FoldersField(BaseClickableFrame):\n    \"\"\"Field where asset name of selected instance/s is showed.\n\n    Click on the field will trigger `FoldersDialog`.\n    \"\"\"\n    value_changed = QtCore.Signal()\n\n    def __init__(self, controller, parent):\n        super(FoldersField, self).__init__(parent)\n        self.setObjectName(\"AssetNameInputWidget\")\n\n        # Don't use 'self' for parent!\n        # - this widget has specific styles\n        dialog = FoldersDialog(controller, parent)\n\n        name_input = ClickableLineEdit(self)\n        name_input.setObjectName(\"AssetNameInput\")\n\n        icon = qtawesome.icon(\"fa.window-maximize\", color=\"white\")\n        icon_btn = QtWidgets.QPushButton(self)\n        icon_btn.setIcon(icon)\n        icon_btn.setObjectName(\"AssetNameInputButton\")\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(0)\n        layout.addWidget(name_input, 1)\n        layout.addWidget(icon_btn, 0)\n\n        # Make sure all widgets are vertically extended to highest widget\n        for widget in (\n            name_input,\n            icon_btn\n        ):\n            w_size_policy = widget.sizePolicy()\n            w_size_policy.setVerticalPolicy(\n                QtWidgets.QSizePolicy.MinimumExpanding)\n            widget.setSizePolicy(w_size_policy)\n\n        size_policy = self.sizePolicy()\n        size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Maximum)\n        self.setSizePolicy(size_policy)\n\n        name_input.clicked.connect(self._mouse_release_callback)\n        icon_btn.clicked.connect(self._mouse_release_callback)\n        dialog.finished.connect(self._on_dialog_finish)\n\n        self._controller = controller\n        self._dialog = dialog\n        self._name_input = name_input\n        self._icon_btn = icon_btn\n\n        self._selected_folder_id = None\n        self._selected_folder_label = None\n        self._selected_items = []\n        self._is_valid = True\n\n    def refresh(self):\n        self._dialog.refresh()\n\n    def is_valid(self):\n        \"\"\"Is asset valid.\"\"\"\n        return self._is_valid\n\n    def get_selected_folder_id(self):\n        \"\"\"Selected asset names.\"\"\"\n        return self._selected_folder_id\n\n    def get_selected_folder_label(self):\n        return self._selected_folder_label\n\n    def set_text(self, text):\n        \"\"\"Set text in text field.\n\n        Does not change selected items (assets).\n        \"\"\"\n        self._name_input.setText(text)\n\n    def set_valid(self, is_valid):\n        state = \"\"\n        if not is_valid:\n            state = \"invalid\"\n        self._set_state_property(state)\n\n    def set_selected_item(self, folder_id=None, folder_label=None):\n        \"\"\"Set folder for selection.\n\n        Args:\n            folder_id (Optional[str]): Folder id to select.\n            folder_label (Optional[str]): Folder label.\n        \"\"\"\n\n        self._selected_folder_id = folder_id\n        if not folder_id:\n            folder_label = None\n        elif folder_id and not folder_label:\n            folder_label = self._controller.get_folder_label(folder_id)\n        self._selected_folder_label = folder_label\n        self.set_text(folder_label if folder_label else \"<folder>\")\n\n    def _on_dialog_finish(self, result):\n        if not result:\n            return\n\n        folder_id = self._dialog.get_selected_folder_id()\n        folder_label = self._dialog.get_selected_folder_label()\n        self.set_selected_item(folder_id, folder_label)\n\n        self.value_changed.emit()\n\n    def _mouse_release_callback(self):\n        self._dialog.set_selected_folder(self._selected_folder_id)\n        self._dialog.open()\n\n    def _set_state_property(self, state):\n        set_style_property(self, \"state\", state)\n        set_style_property(self._name_input, \"state\", state)\n        set_style_property(self._icon_btn, \"state\", state)\n"
  },
  {
    "path": "openpype/tools/ayon_sceneinventory/switch_dialog/widgets.py",
    "content": "from qtpy import QtWidgets, QtCore\n\nfrom openpype import style\n\n\nclass ButtonWithMenu(QtWidgets.QToolButton):\n    def __init__(self, parent=None):\n        super(ButtonWithMenu, self).__init__(parent)\n\n        self.setObjectName(\"ButtonWithMenu\")\n\n        self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)\n        menu = QtWidgets.QMenu(self)\n\n        self.setMenu(menu)\n\n        self._menu = menu\n        self._actions = []\n\n    def menu(self):\n        return self._menu\n\n    def clear_actions(self):\n        if self._menu is not None:\n            self._menu.clear()\n        self._actions = []\n\n    def add_action(self, action):\n        self._actions.append(action)\n        self._menu.addAction(action)\n\n    def _on_action_trigger(self):\n        action = self.sender()\n        if action not in self._actions:\n            return\n        action.trigger()\n\n\nclass SearchComboBox(QtWidgets.QComboBox):\n    \"\"\"Searchable ComboBox with empty placeholder value as first value\"\"\"\n\n    def __init__(self, parent):\n        super(SearchComboBox, self).__init__(parent)\n\n        self.setEditable(True)\n        self.setInsertPolicy(QtWidgets.QComboBox.NoInsert)\n\n        combobox_delegate = QtWidgets.QStyledItemDelegate(self)\n        self.setItemDelegate(combobox_delegate)\n\n        completer = self.completer()\n        completer.setCompletionMode(\n            QtWidgets.QCompleter.PopupCompletion\n        )\n        completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        completer_view = completer.popup()\n        completer_view.setObjectName(\"CompleterView\")\n        completer_delegate = QtWidgets.QStyledItemDelegate(completer_view)\n        completer_view.setItemDelegate(completer_delegate)\n        completer_view.setStyleSheet(style.load_stylesheet())\n\n        self._combobox_delegate = combobox_delegate\n\n        self._completer_delegate = completer_delegate\n        self._completer = completer\n\n    def set_placeholder(self, placeholder):\n        self.lineEdit().setPlaceholderText(placeholder)\n\n    def populate(self, items):\n        self.clear()\n        self.addItems([\"\"])     # ensure first item is placeholder\n        self.addItems(items)\n\n    def get_valid_value(self):\n        \"\"\"Return the current text if it's a valid value else None\n\n        Note: The empty placeholder value is valid and returns as \"\"\n\n        \"\"\"\n\n        text = self.currentText()\n        lookup = set(self.itemText(i) for i in range(self.count()))\n        if text not in lookup:\n            return None\n\n        return text or None\n\n    def set_valid_value(self, value):\n        \"\"\"Try to locate 'value' and pre-select it in dropdown.\"\"\"\n        index = self.findText(value)\n        if index > -1:\n            self.setCurrentIndex(index)\n"
  },
  {
    "path": "openpype/tools/ayon_sceneinventory/view.py",
    "content": "import uuid\nimport collections\nimport logging\nimport itertools\nfrom functools import partial\n\nfrom qtpy import QtWidgets, QtCore\nimport qtawesome\n\nfrom openpype.client import (\n    get_version_by_id,\n    get_versions,\n    get_hero_versions,\n    get_representation_by_id,\n    get_representations,\n)\nfrom openpype import style\nfrom openpype.pipeline import (\n    HeroVersionType,\n    update_container,\n    remove_container,\n    discover_inventory_actions,\n)\nfrom openpype.tools.utils.lib import (\n    iter_model_rows,\n    format_version\n)\n\nfrom .switch_dialog import SwitchAssetDialog\nfrom .model import InventoryModel\n\n\nDEFAULT_COLOR = \"#fb9c15\"\n\nlog = logging.getLogger(\"SceneInventory\")\n\n\nclass SceneInventoryView(QtWidgets.QTreeView):\n    data_changed = QtCore.Signal()\n    hierarchy_view_changed = QtCore.Signal(bool)\n\n    def __init__(self, controller, parent):\n        super(SceneInventoryView, self).__init__(parent=parent)\n\n        # view settings\n        self.setIndentation(12)\n        self.setAlternatingRowColors(True)\n        self.setSortingEnabled(True)\n        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)\n        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n\n        self.customContextMenuRequested.connect(self._show_right_mouse_menu)\n\n        self._hierarchy_view = False\n        self._selected = None\n\n        self._controller = controller\n\n    def _set_hierarchy_view(self, enabled):\n        if enabled == self._hierarchy_view:\n            return\n        self._hierarchy_view = enabled\n        self.hierarchy_view_changed.emit(enabled)\n\n    def _enter_hierarchy(self, items):\n        self._selected = set(i[\"objectName\"] for i in items)\n        self._set_hierarchy_view(True)\n        self.data_changed.emit()\n        self.expandToDepth(1)\n        self.setStyleSheet(\"\"\"\n        QTreeView {\n             border-color: #fb9c15;\n        }\n        \"\"\")\n\n    def _leave_hierarchy(self):\n        self._set_hierarchy_view(False)\n        self.data_changed.emit()\n        self.setStyleSheet(\"QTreeView {}\")\n\n    def _build_item_menu_for_selection(self, items, menu):\n        # Exclude items that are \"NOT FOUND\" since setting versions, updating\n        # and removal won't work for those items.\n        items = [item for item in items if not item.get(\"isNotFound\")]\n        if not items:\n            return\n\n        # An item might not have a representation, for example when an item\n        # is listed as \"NOT FOUND\"\n        repre_ids = set()\n        for item in items:\n            repre_id = item[\"representation\"]\n            try:\n                uuid.UUID(repre_id)\n                repre_ids.add(repre_id)\n            except ValueError:\n                pass\n\n        project_name = self._controller.get_current_project_name()\n        repre_docs = get_representations(\n            project_name, representation_ids=repre_ids, fields=[\"parent\"]\n        )\n\n        version_ids = {\n            repre_doc[\"parent\"]\n            for repre_doc in repre_docs\n        }\n\n        loaded_versions = get_versions(\n            project_name, version_ids=version_ids, hero=True\n        )\n\n        loaded_hero_versions = []\n        versions_by_parent_id = collections.defaultdict(list)\n        subset_ids = set()\n        for version in loaded_versions:\n            if version[\"type\"] == \"hero_version\":\n                loaded_hero_versions.append(version)\n            else:\n                parent_id = version[\"parent\"]\n                versions_by_parent_id[parent_id].append(version)\n                subset_ids.add(parent_id)\n\n        all_versions = get_versions(\n            project_name, subset_ids=subset_ids, hero=True\n        )\n        hero_versions = []\n        versions = []\n        for version in all_versions:\n            if version[\"type\"] == \"hero_version\":\n                hero_versions.append(version)\n            else:\n                versions.append(version)\n\n        has_loaded_hero_versions = len(loaded_hero_versions) > 0\n        has_available_hero_version = len(hero_versions) > 0\n        has_outdated = False\n\n        for version in versions:\n            parent_id = version[\"parent\"]\n            current_versions = versions_by_parent_id[parent_id]\n            for current_version in current_versions:\n                if current_version[\"name\"] < version[\"name\"]:\n                    has_outdated = True\n                    break\n\n            if has_outdated:\n                break\n\n        switch_to_versioned = None\n        if has_loaded_hero_versions:\n            def _on_switch_to_versioned(items):\n                repre_ids = {\n                    item[\"representation\"]\n                    for item in items\n                }\n\n                repre_docs = get_representations(\n                    project_name,\n                    representation_ids=repre_ids,\n                    fields=[\"parent\"]\n                )\n\n                version_ids = set()\n                version_id_by_repre_id = {}\n                for repre_doc in repre_docs:\n                    version_id = repre_doc[\"parent\"]\n                    repre_id = str(repre_doc[\"_id\"])\n                    version_id_by_repre_id[repre_id] = version_id\n                    version_ids.add(version_id)\n\n                hero_versions = get_hero_versions(\n                    project_name,\n                    version_ids=version_ids,\n                    fields=[\"version_id\"]\n                )\n\n                hero_src_version_ids = set()\n                for hero_version in hero_versions:\n                    version_id = hero_version[\"version_id\"]\n                    hero_src_version_ids.add(version_id)\n                    hero_version_id = hero_version[\"_id\"]\n                    for _repre_id, current_version_id in (\n                        version_id_by_repre_id.items()\n                    ):\n                        if current_version_id == hero_version_id:\n                            version_id_by_repre_id[_repre_id] = version_id\n\n                version_docs = get_versions(\n                    project_name,\n                    version_ids=hero_src_version_ids,\n                    fields=[\"name\"]\n                )\n                version_name_by_id = {}\n                for version_doc in version_docs:\n                    version_name_by_id[version_doc[\"_id\"]] = \\\n                        version_doc[\"name\"]\n\n                # Specify version per item to update to\n                update_items = []\n                update_versions = []\n                for item in items:\n                    repre_id = item[\"representation\"]\n                    version_id = version_id_by_repre_id.get(repre_id)\n                    version_name = version_name_by_id.get(version_id)\n                    if version_name is not None:\n                        update_items.append(item)\n                        update_versions.append(version_name)\n                self._update_containers(update_items, update_versions)\n\n            update_icon = qtawesome.icon(\n                \"fa.asterisk\",\n                color=DEFAULT_COLOR\n            )\n            switch_to_versioned = QtWidgets.QAction(\n                update_icon,\n                \"Switch to versioned\",\n                menu\n            )\n            switch_to_versioned.triggered.connect(\n                lambda: _on_switch_to_versioned(items)\n            )\n\n        update_to_latest_action = None\n        if has_outdated or has_loaded_hero_versions:\n            update_icon = qtawesome.icon(\n                \"fa.angle-double-up\",\n                color=DEFAULT_COLOR\n            )\n            update_to_latest_action = QtWidgets.QAction(\n                update_icon,\n                \"Update to latest\",\n                menu\n            )\n            update_to_latest_action.triggered.connect(\n                lambda: self._update_containers(items, version=-1)\n            )\n\n        change_to_hero = None\n        if has_available_hero_version:\n            # TODO change icon\n            change_icon = qtawesome.icon(\n                \"fa.asterisk\",\n                color=\"#00b359\"\n            )\n            change_to_hero = QtWidgets.QAction(\n                change_icon,\n                \"Change to hero\",\n                menu\n            )\n            change_to_hero.triggered.connect(\n                lambda: self._update_containers(items,\n                                                version=HeroVersionType(-1))\n            )\n\n        # set version\n        set_version_icon = qtawesome.icon(\"fa.hashtag\", color=DEFAULT_COLOR)\n        set_version_action = QtWidgets.QAction(\n            set_version_icon,\n            \"Set version\",\n            menu\n        )\n        set_version_action.triggered.connect(\n            lambda: self._show_version_dialog(items))\n\n        # switch folder\n        switch_folder_icon = qtawesome.icon(\"fa.sitemap\", color=DEFAULT_COLOR)\n        switch_folder_action = QtWidgets.QAction(\n            switch_folder_icon,\n            \"Switch Folder\",\n            menu\n        )\n        switch_folder_action.triggered.connect(\n            lambda: self._show_switch_dialog(items))\n\n        # remove\n        remove_icon = qtawesome.icon(\"fa.remove\", color=DEFAULT_COLOR)\n        remove_action = QtWidgets.QAction(remove_icon, \"Remove items\", menu)\n        remove_action.triggered.connect(\n            lambda: self._show_remove_warning_dialog(items))\n\n        # add the actions\n        if switch_to_versioned:\n            menu.addAction(switch_to_versioned)\n\n        if update_to_latest_action:\n            menu.addAction(update_to_latest_action)\n\n        if change_to_hero:\n            menu.addAction(change_to_hero)\n\n        menu.addAction(set_version_action)\n        menu.addAction(switch_folder_action)\n\n        menu.addSeparator()\n\n        menu.addAction(remove_action)\n\n        self._handle_sync_server(menu, repre_ids)\n\n    def _handle_sync_server(self, menu, repre_ids):\n        \"\"\"Adds actions for download/upload when SyncServer is enabled\n\n        Args:\n            menu (OptionMenu)\n            repre_ids (list) of object_ids\n\n        Returns:\n            (OptionMenu)\n        \"\"\"\n\n        if not self._controller.is_sync_server_enabled():\n            return\n\n        menu.addSeparator()\n\n        download_icon = qtawesome.icon(\"fa.download\", color=DEFAULT_COLOR)\n        download_active_action = QtWidgets.QAction(\n            download_icon,\n            \"Download\",\n            menu\n        )\n        download_active_action.triggered.connect(\n            lambda: self._add_sites(repre_ids, \"active_site\"))\n\n        upload_icon = qtawesome.icon(\"fa.upload\", color=DEFAULT_COLOR)\n        upload_remote_action = QtWidgets.QAction(\n            upload_icon,\n            \"Upload\",\n            menu\n        )\n        upload_remote_action.triggered.connect(\n            lambda: self._add_sites(repre_ids, \"remote_site\"))\n\n        menu.addAction(download_active_action)\n        menu.addAction(upload_remote_action)\n\n    def _add_sites(self, repre_ids, site_type):\n        \"\"\"(Re)sync all 'repre_ids' to specific site.\n\n        It checks if opposite site has fully available content to limit\n        accidents. (ReSync active when no remote >> losing active content)\n\n        Args:\n            repre_ids (list)\n            site_type (Literal[active_site, remote_site]): Site type.\n        \"\"\"\n\n        self._controller.resync_representations(repre_ids, site_type)\n\n        self.data_changed.emit()\n\n    def _build_item_menu(self, items=None):\n        \"\"\"Create menu for the selected items\"\"\"\n\n        if not items:\n            items = []\n\n        menu = QtWidgets.QMenu(self)\n\n        # add the actions\n        self._build_item_menu_for_selection(items, menu)\n\n        # These two actions should be able to work without selection\n        # expand all items\n        expandall_action = QtWidgets.QAction(menu, text=\"Expand all items\")\n        expandall_action.triggered.connect(self.expandAll)\n\n        # collapse all items\n        collapse_action = QtWidgets.QAction(menu, text=\"Collapse all items\")\n        collapse_action.triggered.connect(self.collapseAll)\n\n        menu.addAction(expandall_action)\n        menu.addAction(collapse_action)\n\n        custom_actions = self._get_custom_actions(containers=items)\n        if custom_actions:\n            submenu = QtWidgets.QMenu(\"Actions\", self)\n            for action in custom_actions:\n                color = action.color or DEFAULT_COLOR\n                icon = qtawesome.icon(\"fa.%s\" % action.icon, color=color)\n                action_item = QtWidgets.QAction(icon, action.label, submenu)\n                action_item.triggered.connect(\n                    partial(self._process_custom_action, action, items))\n\n                submenu.addAction(action_item)\n\n            menu.addMenu(submenu)\n\n        # go back to flat view\n        back_to_flat_action = None\n        if self._hierarchy_view:\n            back_to_flat_icon = qtawesome.icon(\"fa.list\", color=DEFAULT_COLOR)\n            back_to_flat_action = QtWidgets.QAction(\n                back_to_flat_icon,\n                \"Back to Full-View\",\n                menu\n            )\n            back_to_flat_action.triggered.connect(self._leave_hierarchy)\n\n        # send items to hierarchy view\n        enter_hierarchy_icon = qtawesome.icon(\"fa.indent\", color=\"#d8d8d8\")\n        enter_hierarchy_action = QtWidgets.QAction(\n            enter_hierarchy_icon,\n            \"Cherry-Pick (Hierarchy)\",\n            menu\n        )\n        enter_hierarchy_action.triggered.connect(\n            lambda: self._enter_hierarchy(items))\n\n        if items:\n            menu.addAction(enter_hierarchy_action)\n\n        if back_to_flat_action is not None:\n            menu.addAction(back_to_flat_action)\n\n        return menu\n\n    def _get_custom_actions(self, containers):\n        \"\"\"Get the registered Inventory Actions\n\n        Args:\n            containers(list): collection of containers\n\n        Returns:\n            list: collection of filter and initialized actions\n        \"\"\"\n\n        def sorter(Plugin):\n            \"\"\"Sort based on order attribute of the plugin\"\"\"\n            return Plugin.order\n\n        # Fedd an empty dict if no selection, this will ensure the compat\n        # lookup always work, so plugin can interact with Scene Inventory\n        # reversely.\n        containers = containers or [dict()]\n\n        # Check which action will be available in the menu\n        Plugins = discover_inventory_actions()\n        compatible = [p() for p in Plugins if\n                      any(p.is_compatible(c) for c in containers)]\n\n        return sorted(compatible, key=sorter)\n\n    def _process_custom_action(self, action, containers):\n        \"\"\"Run action and if results are returned positive update the view\n\n        If the result is list or dict, will select view items by the result.\n\n        Args:\n            action (InventoryAction): Inventory Action instance\n            containers (list): Data of currently selected items\n\n        Returns:\n            None\n        \"\"\"\n\n        result = action.process(containers)\n        if result:\n            self.data_changed.emit()\n\n            if isinstance(result, (list, set)):\n                self._select_items_by_action(result)\n\n            if isinstance(result, dict):\n                self._select_items_by_action(\n                    result[\"objectNames\"], result[\"options\"]\n                )\n\n    def _select_items_by_action(self, object_names, options=None):\n        \"\"\"Select view items by the result of action\n\n        Args:\n            object_names (list or set): A list/set of container object name\n            options (dict): GUI operation options.\n\n        Returns:\n            None\n\n        \"\"\"\n        options = options or dict()\n\n        if options.get(\"clear\", True):\n            self.clearSelection()\n\n        object_names = set(object_names)\n        if (\n            self._hierarchy_view\n            and not self._selected.issuperset(object_names)\n        ):\n            # If any container not in current cherry-picked view, update\n            # view before selecting them.\n            self._selected.update(object_names)\n            self.data_changed.emit()\n\n        model = self.model()\n        selection_model = self.selectionModel()\n\n        select_mode = {\n            \"select\": QtCore.QItemSelectionModel.Select,\n            \"deselect\": QtCore.QItemSelectionModel.Deselect,\n            \"toggle\": QtCore.QItemSelectionModel.Toggle,\n        }[options.get(\"mode\", \"select\")]\n\n        for index in iter_model_rows(model, 0):\n            item = index.data(InventoryModel.ItemRole)\n            if item.get(\"isGroupNode\"):\n                continue\n\n            name = item.get(\"objectName\")\n            if name in object_names:\n                self.scrollTo(index)  # Ensure item is visible\n                flags = select_mode | QtCore.QItemSelectionModel.Rows\n                selection_model.select(index, flags)\n\n                object_names.remove(name)\n\n            if len(object_names) == 0:\n                break\n\n    def _show_right_mouse_menu(self, pos):\n        \"\"\"Display the menu when at the position of the item clicked\"\"\"\n\n        globalpos = self.viewport().mapToGlobal(pos)\n\n        if not self.selectionModel().hasSelection():\n            print(\"No selection\")\n            # Build menu without selection, feed an empty list\n            menu = self._build_item_menu()\n            menu.exec_(globalpos)\n            return\n\n        active = self.currentIndex()  # index under mouse\n        active = active.sibling(active.row(), 0)  # get first column\n\n        # move index under mouse\n        indices = self.get_indices()\n        if active in indices:\n            indices.remove(active)\n\n        indices.append(active)\n\n        # Extend to the sub-items\n        all_indices = self._extend_to_children(indices)\n        items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices\n                 if i.parent().isValid()]\n\n        if self._hierarchy_view:\n            # Ensure no group item\n            items = [n for n in items if not n.get(\"isGroupNode\")]\n\n        menu = self._build_item_menu(items)\n        menu.exec_(globalpos)\n\n    def get_indices(self):\n        \"\"\"Get the selected rows\"\"\"\n        selection_model = self.selectionModel()\n        return selection_model.selectedRows()\n\n    def _extend_to_children(self, indices):\n        \"\"\"Extend the indices to the children indices.\n\n        Top-level indices are extended to its children indices. Sub-items\n        are kept as is.\n\n        Args:\n            indices (list): The indices to extend.\n\n        Returns:\n            list: The children indices\n\n        \"\"\"\n        def get_children(i):\n            model = i.model()\n            rows = model.rowCount(parent=i)\n            for row in range(rows):\n                child = model.index(row, 0, parent=i)\n                yield child\n\n        subitems = set()\n        for i in indices:\n            valid_parent = i.parent().isValid()\n            if valid_parent and i not in subitems:\n                subitems.add(i)\n\n                if self._hierarchy_view:\n                    # Assume this is a group item\n                    for child in get_children(i):\n                        subitems.add(child)\n            else:\n                # is top level item\n                for child in get_children(i):\n                    subitems.add(child)\n\n        return list(subitems)\n\n    def _show_version_dialog(self, items):\n        \"\"\"Create a dialog with the available versions for the selected file\n\n        Args:\n            items (list): list of items to run the \"set_version\" for\n\n        Returns:\n            None\n        \"\"\"\n\n        active = items[-1]\n\n        project_name = self._controller.get_current_project_name()\n        # Get available versions for active representation\n        repre_doc = get_representation_by_id(\n            project_name,\n            active[\"representation\"],\n            fields=[\"parent\"]\n        )\n\n        repre_version_doc = get_version_by_id(\n            project_name,\n            repre_doc[\"parent\"],\n            fields=[\"parent\"]\n        )\n\n        version_docs = list(get_versions(\n            project_name,\n            subset_ids=[repre_version_doc[\"parent\"]],\n            hero=True\n        ))\n        hero_version = None\n        standard_versions = []\n        for version_doc in version_docs:\n            if version_doc[\"type\"] == \"hero_version\":\n                hero_version = version_doc\n            else:\n                standard_versions.append(version_doc)\n        versions = list(reversed(\n            sorted(standard_versions, key=lambda item: item[\"name\"])\n        ))\n        if hero_version:\n            _version_id = hero_version[\"version_id\"]\n            for _version in versions:\n                if _version[\"_id\"] != _version_id:\n                    continue\n\n                hero_version[\"name\"] = HeroVersionType(\n                    _version[\"name\"]\n                )\n                hero_version[\"data\"] = _version[\"data\"]\n                break\n\n        # Get index among the listed versions\n        current_item = None\n        current_version = active[\"version\"]\n        if isinstance(current_version, HeroVersionType):\n            current_item = hero_version\n        else:\n            for version in versions:\n                if version[\"name\"] == current_version:\n                    current_item = version\n                    break\n\n        all_versions = []\n        if hero_version:\n            all_versions.append(hero_version)\n        all_versions.extend(versions)\n\n        if current_item:\n            index = all_versions.index(current_item)\n        else:\n            index = 0\n\n        versions_by_label = dict()\n        labels = []\n        for version in all_versions:\n            is_hero = version[\"type\"] == \"hero_version\"\n            label = format_version(version[\"name\"], is_hero)\n            labels.append(label)\n            versions_by_label[label] = version[\"name\"]\n\n        label, state = QtWidgets.QInputDialog.getItem(\n            self,\n            \"Set version..\",\n            \"Set version number to\",\n            labels,\n            current=index,\n            editable=False\n        )\n        if not state:\n            return\n\n        if label:\n            version = versions_by_label[label]\n            self._update_containers(items, version)\n\n    def _show_switch_dialog(self, items):\n        \"\"\"Display Switch dialog\"\"\"\n        dialog = SwitchAssetDialog(self._controller, self, items)\n        dialog.switched.connect(self.data_changed.emit)\n        dialog.show()\n\n    def _show_remove_warning_dialog(self, items):\n        \"\"\"Prompt a dialog to inform the user the action will remove items\"\"\"\n\n        accept = QtWidgets.QMessageBox.Ok\n        buttons = accept | QtWidgets.QMessageBox.Cancel\n\n        state = QtWidgets.QMessageBox.question(\n            self,\n            \"Are you sure?\",\n            \"Are you sure you want to remove {} item(s)\".format(len(items)),\n            buttons=buttons,\n            defaultButton=accept\n        )\n\n        if state != accept:\n            return\n\n        for item in items:\n            remove_container(item)\n        self.data_changed.emit()\n\n    def _show_version_error_dialog(self, version, items):\n        \"\"\"Shows QMessageBox when version switch doesn't work\n\n            Args:\n                version: str or int or None\n        \"\"\"\n        if version == -1:\n            version_str = \"latest\"\n        elif isinstance(version, HeroVersionType):\n            version_str = \"hero\"\n        elif isinstance(version, int):\n            version_str = \"v{:03d}\".format(version)\n        else:\n            version_str = version\n\n        dialog = QtWidgets.QMessageBox(self)\n        dialog.setIcon(QtWidgets.QMessageBox.Warning)\n        dialog.setStyleSheet(style.load_stylesheet())\n        dialog.setWindowTitle(\"Update failed\")\n\n        switch_btn = dialog.addButton(\n            \"Switch Folder\",\n            QtWidgets.QMessageBox.ActionRole\n        )\n        switch_btn.clicked.connect(lambda: self._show_switch_dialog(items))\n\n        dialog.addButton(QtWidgets.QMessageBox.Cancel)\n\n        msg = (\n            \"Version update to '{}' failed as representation doesn't exist.\"\n            \"\\n\\nPlease update to version with a valid representation\"\n            \" OR \\n use 'Switch Folder' button to change folder.\"\n        ).format(version_str)\n        dialog.setText(msg)\n        dialog.exec_()\n\n    def update_all(self):\n        \"\"\"Update all items that are currently 'outdated' in the view\"\"\"\n        # Get the source model through the proxy model\n        model = self.model().sourceModel()\n\n        # Get all items from outdated groups\n        outdated_items = []\n        for index in iter_model_rows(model,\n                                     column=0,\n                                     include_root=False):\n            item = index.data(model.ItemRole)\n\n            if not item.get(\"isGroupNode\"):\n                continue\n\n            # Only the group nodes contain the \"highest_version\" data and as\n            # such we find only the groups and take its children.\n            if not model.outdated(item):\n                continue\n\n            # Collect all children which we want to update\n            children = item.children()\n            outdated_items.extend(children)\n\n        if not outdated_items:\n            log.info(\"Nothing to update.\")\n            return\n\n        # Trigger update to latest\n        self._update_containers(outdated_items, version=-1)\n\n    def _update_containers(self, items, version):\n        \"\"\"Helper to update items to given version (or version per item)\n\n        If at least one item is specified this will always try to refresh\n        the inventory even if errors occurred on any of the items.\n\n        Arguments:\n            items (list): Items to update\n            version (int or list): Version to set to.\n                This can be a list specifying a version for each item.\n                Like `update_container` version -1 sets the latest version\n                and HeroTypeVersion instances set the hero version.\n\n        \"\"\"\n\n        if isinstance(version, (list, tuple)):\n            # We allow a unique version to be specified per item. In that case\n            # the length must match with the items\n            assert len(items) == len(version), (\n                \"Number of items mismatches number of versions: \"\n                \"{} items - {} versions\".format(len(items), len(version))\n            )\n            versions = version\n        else:\n            # Repeat the same version infinitely\n            versions = itertools.repeat(version)\n\n        # Trigger update to latest\n        try:\n            for item, item_version in zip(items, versions):\n                try:\n                    update_container(item, item_version)\n                except AssertionError:\n                    self._show_version_error_dialog(item_version, [item])\n                    log.warning(\"Update failed\", exc_info=True)\n        finally:\n            # Always update the scene inventory view, even if errors occurred\n            self.data_changed.emit()\n"
  },
  {
    "path": "openpype/tools/ayon_sceneinventory/window.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\nimport qtawesome\n\nfrom openpype import style, resources\nfrom openpype.tools.utils.delegates import VersionDelegate\nfrom openpype.tools.utils.lib import (\n    preserve_expanded_rows,\n    preserve_selection,\n)\nfrom openpype.tools.ayon_sceneinventory import SceneInventoryController\n\nfrom .model import (\n    InventoryModel,\n    FilterProxyModel\n)\nfrom .view import SceneInventoryView\n\n\nclass ControllerVersionDelegate(VersionDelegate):\n    \"\"\"Version delegate that uses controller to get project.\n\n    Original VersionDelegate is using 'AvalonMongoDB' object instead. Don't\n    worry about the variable name, object is stored to '_dbcon' attribute.\n    \"\"\"\n\n    def get_project_name(self):\n        self._dbcon.get_current_project_name()\n\n\nclass SceneInventoryWindow(QtWidgets.QDialog):\n    \"\"\"Scene Inventory window\"\"\"\n\n    def __init__(self, controller=None, parent=None):\n        super(SceneInventoryWindow, self).__init__(parent)\n\n        if controller is None:\n            controller = SceneInventoryController()\n\n        project_name = controller.get_current_project_name()\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n        self.setWindowTitle(\"Scene Inventory - {}\".format(project_name))\n        self.setObjectName(\"SceneInventory\")\n\n        self.resize(1100, 480)\n\n        # region control\n\n        filter_label = QtWidgets.QLabel(\"Search\", self)\n        text_filter = QtWidgets.QLineEdit(self)\n\n        outdated_only_checkbox = QtWidgets.QCheckBox(\n            \"Filter to outdated\", self\n        )\n        outdated_only_checkbox.setToolTip(\"Show outdated files only\")\n        outdated_only_checkbox.setChecked(False)\n\n        icon = qtawesome.icon(\"fa.arrow-up\", color=\"white\")\n        update_all_button = QtWidgets.QPushButton(self)\n        update_all_button.setToolTip(\"Update all outdated to latest version\")\n        update_all_button.setIcon(icon)\n\n        icon = qtawesome.icon(\"fa.refresh\", color=\"white\")\n        refresh_button = QtWidgets.QPushButton(self)\n        refresh_button.setToolTip(\"Refresh\")\n        refresh_button.setIcon(icon)\n\n        control_layout = QtWidgets.QHBoxLayout()\n        control_layout.addWidget(filter_label)\n        control_layout.addWidget(text_filter)\n        control_layout.addWidget(outdated_only_checkbox)\n        control_layout.addWidget(update_all_button)\n        control_layout.addWidget(refresh_button)\n\n        model = InventoryModel(controller)\n        proxy = FilterProxyModel()\n        proxy.setSourceModel(model)\n        proxy.setDynamicSortFilter(True)\n        proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        view = SceneInventoryView(controller, self)\n        view.setModel(proxy)\n\n        sync_enabled = controller.is_sync_server_enabled()\n        view.setColumnHidden(model.active_site_col, not sync_enabled)\n        view.setColumnHidden(model.remote_site_col, not sync_enabled)\n\n        # set some nice default widths for the view\n        view.setColumnWidth(0, 250)  # name\n        view.setColumnWidth(1, 55)   # version\n        view.setColumnWidth(2, 55)   # count\n        view.setColumnWidth(3, 150)  # family\n        view.setColumnWidth(4, 120)  # group\n        view.setColumnWidth(5, 150)  # loader\n\n        # apply delegates\n        version_delegate = ControllerVersionDelegate(controller, self)\n        column = model.Columns.index(\"version\")\n        view.setItemDelegateForColumn(column, version_delegate)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addLayout(control_layout)\n        layout.addWidget(view)\n\n        show_timer = QtCore.QTimer()\n        show_timer.setInterval(0)\n        show_timer.setSingleShot(False)\n\n        # signals\n        show_timer.timeout.connect(self._on_show_timer)\n        text_filter.textChanged.connect(self._on_text_filter_change)\n        outdated_only_checkbox.stateChanged.connect(\n            self._on_outdated_state_change\n        )\n        view.hierarchy_view_changed.connect(\n            self._on_hierarchy_view_change\n        )\n        view.data_changed.connect(self._on_refresh_request)\n        refresh_button.clicked.connect(self._on_refresh_request)\n        update_all_button.clicked.connect(self._on_update_all)\n\n        self._show_timer = show_timer\n        self._show_counter = 0\n        self._controller = controller\n        self._update_all_button = update_all_button\n        self._outdated_only_checkbox = outdated_only_checkbox\n        self._view = view\n        self._model = model\n        self._proxy = proxy\n        self._version_delegate = version_delegate\n\n        self._first_show = True\n        self._first_refresh = True\n\n    def showEvent(self, event):\n        super(SceneInventoryWindow, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self.setStyleSheet(style.load_stylesheet())\n\n        self._show_counter = 0\n        self._show_timer.start()\n\n    def keyPressEvent(self, event):\n        \"\"\"Custom keyPressEvent.\n\n        Override keyPressEvent to do nothing so that Maya's panels won't\n        take focus when pressing \"SHIFT\" whilst mouse is over viewport or\n        outliner. This way users don't accidentally perform Maya commands\n        whilst trying to name an instance.\n\n        \"\"\"\n\n    def _on_refresh_request(self):\n        \"\"\"Signal callback to trigger 'refresh' without any arguments.\"\"\"\n\n        self.refresh()\n\n    def refresh(self, containers=None):\n        self._first_refresh = False\n        self._controller.reset()\n        with preserve_expanded_rows(\n            tree_view=self._view,\n            role=self._model.UniqueRole\n        ):\n            with preserve_selection(\n                tree_view=self._view,\n                role=self._model.UniqueRole,\n                current_index=False\n            ):\n                kwargs = {\"containers\": containers}\n                # TODO do not touch view's inner attribute\n                if self._view._hierarchy_view:\n                    kwargs[\"selected\"] = self._view._selected\n                self._model.refresh(**kwargs)\n\n    def _on_show_timer(self):\n        if self._show_counter < 3:\n            self._show_counter += 1\n            return\n        self._show_timer.stop()\n        self.refresh()\n\n    def _on_hierarchy_view_change(self, enabled):\n        self._proxy.set_hierarchy_view(enabled)\n        self._model.set_hierarchy_view(enabled)\n\n    def _on_text_filter_change(self, text_filter):\n        if hasattr(self._proxy, \"setFilterRegExp\"):\n            self._proxy.setFilterRegExp(text_filter)\n        else:\n            self._proxy.setFilterRegularExpression(text_filter)\n\n    def _on_outdated_state_change(self):\n        self._proxy.set_filter_outdated(\n            self._outdated_only_checkbox.isChecked()\n        )\n\n    def _on_update_all(self):\n        self._view.update_all()\n"
  },
  {
    "path": "openpype/tools/ayon_utils/models/__init__.py",
    "content": "\"\"\"Backend models that can be used in controllers.\"\"\"\n\nfrom .cache import CacheItem, NestedCacheItem\nfrom .projects import (\n    ProjectItem,\n    ProjectsModel,\n    PROJECTS_MODEL_SENDER,\n)\nfrom .hierarchy import (\n    FolderItem,\n    TaskItem,\n    HierarchyModel,\n    HIERARCHY_MODEL_SENDER,\n)\nfrom .thumbnails import ThumbnailsModel\nfrom .selection import HierarchyExpectedSelection\n\n\n__all__ = (\n    \"CacheItem\",\n    \"NestedCacheItem\",\n\n    \"ProjectItem\",\n    \"ProjectsModel\",\n    \"PROJECTS_MODEL_SENDER\",\n\n    \"FolderItem\",\n    \"TaskItem\",\n    \"HierarchyModel\",\n    \"HIERARCHY_MODEL_SENDER\",\n\n    \"ThumbnailsModel\",\n\n    \"HierarchyExpectedSelection\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_utils/models/cache.py",
    "content": "import time\nimport collections\n\nInitInfo = collections.namedtuple(\n    \"InitInfo\",\n    [\"default_factory\", \"lifetime\"]\n)\n\n\ndef _default_factory_func():\n    return None\n\n\nclass CacheItem:\n    \"\"\"Simple cache item with lifetime and default value.\n\n    Args:\n        default_factory (Optional[callable]): Function that returns default\n            value used on init and on reset.\n        lifetime (Optional[int]): Lifetime of the cache data in seconds.\n    \"\"\"\n\n    def __init__(self, default_factory=None, lifetime=None):\n        if lifetime is None:\n            lifetime = 120\n        self._lifetime = lifetime\n        self._last_update = None\n        if default_factory is None:\n            default_factory = _default_factory_func\n        self._default_factory = default_factory\n        self._data = default_factory()\n\n    @property\n    def is_valid(self):\n        \"\"\"Is cache valid to use.\n\n        Return:\n            bool: True if cache is valid, False otherwise.\n        \"\"\"\n\n        if self._last_update is None:\n            return False\n\n        return (time.time() - self._last_update) < self._lifetime\n\n    def set_lifetime(self, lifetime):\n        \"\"\"Change lifetime of cache item.\n\n        Args:\n            lifetime (int): Lifetime of the cache data in seconds.\n        \"\"\"\n\n        self._lifetime = lifetime\n\n    def set_invalid(self):\n        \"\"\"Set cache as invalid.\"\"\"\n\n        self._last_update = None\n\n    def reset(self):\n        \"\"\"Set cache as invalid and reset data.\"\"\"\n\n        self._last_update = None\n        self._data = self._default_factory()\n\n    def get_data(self):\n        \"\"\"Receive cached data.\n\n        Returns:\n            Any: Any data that are cached.\n        \"\"\"\n\n        return self._data\n\n    def update_data(self, data):\n        self._data = data\n        self._last_update = time.time()\n\n\nclass NestedCacheItem:\n    \"\"\"Helper for cached items stored in nested structure.\n\n    Example:\n        >>> cache = NestedCacheItem(levels=2, default_factory=lambda: 0)\n        >>> cache[\"a\"][\"b\"].is_valid\n        False\n        >>> cache[\"a\"][\"b\"].get_data()\n        0\n        >>> cache[\"a\"][\"b\"] = 1\n        >>> cache[\"a\"][\"b\"].is_valid\n        True\n        >>> cache[\"a\"][\"b\"].get_data()\n        1\n        >>> cache.reset()\n        >>> cache[\"a\"][\"b\"].is_valid\n        False\n\n    Args:\n        levels (int): Number of nested levels where read cache is stored.\n        default_factory (Optional[callable]): Function that returns default\n            value used on init and on reset.\n        lifetime (Optional[int]): Lifetime of the cache data in seconds.\n        _init_info (Optional[InitInfo]): Private argument. Init info for\n            nested cache where created from parent item.\n    \"\"\"\n\n    def __init__(\n        self, levels=1, default_factory=None, lifetime=None, _init_info=None\n    ):\n        if levels < 1:\n            raise ValueError(\"Nested levels must be greater than 0\")\n        self._data_by_key = {}\n        if _init_info is None:\n            _init_info = InitInfo(default_factory, lifetime)\n        self._init_info = _init_info\n        self._levels = levels\n\n    def __getitem__(self, key):\n        \"\"\"Get cached data.\n\n        Args:\n            key (str): Key of the cache item.\n\n        Returns:\n            Union[NestedCacheItem, CacheItem]: Cache item.\n        \"\"\"\n\n        cache = self._data_by_key.get(key)\n        if cache is None:\n            if self._levels > 1:\n                cache = NestedCacheItem(\n                    levels=self._levels - 1,\n                    _init_info=self._init_info\n                )\n            else:\n                cache = CacheItem(\n                    self._init_info.default_factory,\n                    self._init_info.lifetime\n                )\n            self._data_by_key[key] = cache\n        return cache\n\n    def __setitem__(self, key, value):\n        \"\"\"Update cached data.\n\n        Args:\n            key (str): Key of the cache item.\n            value (Any): Any data that are cached.\n        \"\"\"\n\n        if self._levels > 1:\n            raise AttributeError((\n                \"{} does not support '__setitem__'. Lower nested level by {}\"\n            ).format(self.__class__.__name__, self._levels - 1))\n        cache = self[key]\n        cache.update_data(value)\n\n    def get(self, key):\n        \"\"\"Get cached data.\n\n        Args:\n            key (str): Key of the cache item.\n\n        Returns:\n            Union[NestedCacheItem, CacheItem]: Cache item.\n        \"\"\"\n\n        return self[key]\n\n    def cached_count(self):\n        \"\"\"Amount of cached items.\n\n        Returns:\n            int: Amount of cached items.\n        \"\"\"\n\n        return len(self._data_by_key)\n\n    def clear_key(self, key):\n        \"\"\"Clear cached item by key.\n\n        Args:\n            key (str): Key of the cache item.\n        \"\"\"\n\n        self._data_by_key.pop(key, None)\n\n    def clear_invalid(self):\n        \"\"\"Clear all invalid cache items.\n\n        Note:\n            To clear all cache items use 'reset'.\n        \"\"\"\n\n        changed = {}\n        children_are_nested = self._levels > 1\n        for key, cache in tuple(self._data_by_key.items()):\n            if children_are_nested:\n                output = cache.clear_invalid()\n                if output:\n                    changed[key] = output\n                if not cache.cached_count():\n                    self._data_by_key.pop(key)\n            elif not cache.is_valid:\n                changed[key] = cache.get_data()\n                self._data_by_key.pop(key)\n        return changed\n\n    def reset(self):\n        \"\"\"Reset cache.\n\n        Note:\n            To clear only invalid cache items use 'clear_invalid'.\n        \"\"\"\n\n        self._data_by_key = {}\n\n    def set_lifetime(self, lifetime):\n        \"\"\"Change lifetime of all children cache items.\n\n        Args:\n            lifetime (int): Lifetime of the cache data in seconds.\n        \"\"\"\n\n        self._init_info.lifetime = lifetime\n        for cache in self._data_by_key.values():\n            cache.set_lifetime(lifetime)\n\n    @property\n    def is_valid(self):\n        \"\"\"Raise reasonable error when called on wront level.\n\n        Raises:\n            AttributeError: If called on nested cache item.\n        \"\"\"\n\n        raise AttributeError((\n            \"{} does not support 'is_valid'. Lower nested level by '{}'\"\n        ).format(self.__class__.__name__, self._levels))\n"
  },
  {
    "path": "openpype/tools/ayon_utils/models/hierarchy.py",
    "content": "import collections\nimport contextlib\nfrom abc import ABCMeta, abstractmethod\n\nimport ayon_api\nimport six\n\nfrom openpype.style import get_default_entity_icon_color\n\nfrom .cache import NestedCacheItem\n\nHIERARCHY_MODEL_SENDER = \"hierarchy.model\"\n\n\n@six.add_metaclass(ABCMeta)\nclass AbstractHierarchyController:\n    @abstractmethod\n    def emit_event(self, topic, data, source):\n        pass\n\n\nclass FolderItem:\n    \"\"\"Item representing folder entity on a server.\n\n    Folder can be a child of another folder or a project.\n\n    Args:\n        entity_id (str): Folder id.\n        parent_id (Union[str, None]): Parent folder id. If 'None' then project\n            is parent.\n        name (str): Name of folder.\n        path (str): Folder path.\n        folder_type (str): Type of folder.\n        label (Union[str, None]): Folder label.\n        icon (Union[dict[str, Any], None]): Icon definition.\n    \"\"\"\n\n    def __init__(\n        self, entity_id, parent_id, name, path, folder_type, label, icon\n    ):\n        self.entity_id = entity_id\n        self.parent_id = parent_id\n        self.name = name\n        self.path = path\n        self.folder_type = folder_type\n        self.label = label or name\n        if not icon:\n            icon = {\n                \"type\": \"awesome-font\",\n                \"name\": \"fa.folder\",\n                \"color\": get_default_entity_icon_color()\n            }\n        self.icon = icon\n\n    def to_data(self):\n        \"\"\"Converts folder item to data.\n\n        Returns:\n            dict[str, Any]: Folder item data.\n        \"\"\"\n\n        return {\n            \"entity_id\": self.entity_id,\n            \"parent_id\": self.parent_id,\n            \"name\": self.name,\n            \"path\": self.path,\n            \"folder_type\": self.folder_type,\n            \"label\": self.label,\n            \"icon\": self.icon,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        \"\"\"Re-creates folder item from data.\n\n        Args:\n            data (dict[str, Any]): Folder item data.\n\n        Returns:\n            FolderItem: Folder item.\n        \"\"\"\n\n        return cls(**data)\n\n\nclass TaskItem:\n    \"\"\"Task item representing task entity on a server.\n\n    Task is child of a folder.\n\n    Task item has label that is used for display in UI. The label is by\n        default using task name and type.\n\n    Args:\n        task_id (str): Task id.\n        name (str): Name of task.\n        task_type (str): Type of task.\n        parent_id (str): Parent folder id.\n        icon (Union[dict[str, Any], None]): Icon definitions.\n    \"\"\"\n\n    def __init__(\n        self, task_id, name, task_type, parent_id, icon\n    ):\n        self.task_id = task_id\n        self.name = name\n        self.task_type = task_type\n        self.parent_id = parent_id\n        if icon is None:\n            icon = {\n                \"type\": \"awesome-font\",\n                \"name\": \"fa.male\",\n                \"color\": get_default_entity_icon_color()\n            }\n        self.icon = icon\n\n        self._label = None\n\n    @property\n    def id(self):\n        \"\"\"Alias for task_id.\n\n        Returns:\n            str: Task id.\n        \"\"\"\n\n        return self.task_id\n\n    @property\n    def label(self):\n        \"\"\"Label of task item for UI.\n\n        Returns:\n            str: Label of task item.\n        \"\"\"\n\n        if self._label is None:\n            self._label = \"{} ({})\".format(self.name, self.task_type)\n        return self._label\n\n    def to_data(self):\n        \"\"\"Converts task item to data.\n\n        Returns:\n            dict[str, Any]: Task item data.\n        \"\"\"\n\n        return {\n            \"task_id\": self.task_id,\n            \"name\": self.name,\n            \"parent_id\": self.parent_id,\n            \"task_type\": self.task_type,\n            \"icon\": self.icon,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        \"\"\"Re-create task item from data.\n\n        Args:\n            data (dict[str, Any]): Task item data.\n\n        Returns:\n            TaskItem: Task item.\n        \"\"\"\n\n        return cls(**data)\n\n\ndef _get_task_items_from_tasks(tasks):\n    \"\"\"\n\n    Returns:\n        TaskItem: Task item.\n    \"\"\"\n\n    output = []\n    for task in tasks:\n        folder_id = task[\"folderId\"]\n        output.append(TaskItem(\n            task[\"id\"],\n            task[\"name\"],\n            task[\"type\"],\n            folder_id,\n            None\n        ))\n    return output\n\n\ndef _get_folder_item_from_hierarchy_item(item):\n    name = item[\"name\"]\n    path_parts = list(item[\"parents\"])\n    path_parts.append(name)\n\n    return FolderItem(\n        item[\"id\"],\n        item[\"parentId\"],\n        name,\n        \"/\".join(path_parts),\n        item[\"folderType\"],\n        item[\"label\"],\n        None,\n    )\n\n\ndef _get_folder_item_from_entity(entity):\n    name = entity[\"name\"]\n    return FolderItem(\n        entity[\"id\"],\n        entity[\"parentId\"],\n        name,\n        entity[\"path\"],\n        entity[\"folderType\"],\n        entity[\"label\"] or name,\n        None,\n    )\n\n\nclass HierarchyModel(object):\n    \"\"\"Model for project hierarchy items.\n\n    Hierarchy items are folders and tasks. Folders can have as parent another\n    folder or project. Tasks can have as parent only folder.\n    \"\"\"\n    lifetime = 60  # A minute\n\n    def __init__(self, controller):\n        self._folders_items = NestedCacheItem(\n            levels=1, default_factory=dict, lifetime=self.lifetime)\n        self._folders_by_id = NestedCacheItem(\n            levels=2, default_factory=dict, lifetime=self.lifetime)\n\n        self._task_items = NestedCacheItem(\n            levels=2, default_factory=dict, lifetime=self.lifetime)\n        self._tasks_by_id = NestedCacheItem(\n            levels=2, default_factory=dict, lifetime=self.lifetime)\n\n        self._folders_refreshing = set()\n        self._tasks_refreshing = set()\n        self._controller = controller\n\n    def reset(self):\n        self._folders_items.reset()\n        self._folders_by_id.reset()\n\n        self._task_items.reset()\n        self._tasks_by_id.reset()\n\n    def refresh_project(self, project_name):\n        \"\"\"Force to refresh folder items for a project.\n\n        Args:\n            project_name (str): Name of project to refresh.\n        \"\"\"\n\n        self._refresh_folders_cache(project_name)\n\n    def get_folder_items(self, project_name, sender):\n        \"\"\"Get folder items by project name.\n\n        The folders are cached per project name. If the cache is not valid\n        then the folders are queried from server.\n\n        Args:\n            project_name (str): Name of project where to look for folders.\n            sender (Union[str, None]): Who requested the folder ids.\n\n        Returns:\n            dict[str, FolderItem]: Folder items by id.\n        \"\"\"\n\n        if not self._folders_items[project_name].is_valid:\n            self._refresh_folders_cache(project_name, sender)\n        return self._folders_items[project_name].get_data()\n\n    def get_folder_items_by_id(self, project_name, folder_ids):\n        \"\"\"Get folder items by ids.\n\n        This function will query folders if they are not in cache. But the\n        queried items are not added to cache back.\n\n        Args:\n            project_name (str): Name of project where to look for folders.\n            folder_ids (Iterable[str]): Folder ids.\n\n        Returns:\n            dict[str, Union[FolderItem, None]]: Folder items by id.\n        \"\"\"\n\n        folder_ids = set(folder_ids)\n        if self._folders_items[project_name].is_valid:\n            cache_data = self._folders_items[project_name].get_data()\n            return {\n                folder_id: cache_data.get(folder_id)\n                for folder_id in folder_ids\n            }\n        folders = ayon_api.get_folders(\n            project_name,\n            folder_ids=folder_ids,\n            fields=[\"id\", \"name\", \"label\", \"parentId\", \"path\", \"folderType\"]\n        )\n        # Make sure all folder ids are in output\n        output = {folder_id: None for folder_id in folder_ids}\n        output.update({\n            folder[\"id\"]: _get_folder_item_from_entity(folder)\n            for folder in folders\n        })\n        return output\n\n    def get_folder_item(self, project_name, folder_id):\n        \"\"\"Get folder items by id.\n\n        This function will query folder if they is not in cache. But the\n        queried items are not added to cache back.\n\n        Args:\n            project_name (str): Name of project where to look for folders.\n            folder_id (str): Folder id.\n\n        Returns:\n            Union[FolderItem, None]: Folder item.\n        \"\"\"\n        items = self.get_folder_items_by_id(\n            project_name, [folder_id]\n        )\n        return items.get(folder_id)\n\n    def get_task_items(self, project_name, folder_id, sender):\n        if not project_name or not folder_id:\n            return []\n\n        task_cache = self._task_items[project_name][folder_id]\n        if not task_cache.is_valid:\n            self._refresh_tasks_cache(project_name, folder_id, sender)\n        return task_cache.get_data()\n\n    def get_folder_entities(self, project_name, folder_ids):\n        \"\"\"Get folder entities by ids.\n\n        Args:\n            project_name (str): Project name.\n            folder_ids (Iterable[str]): Folder ids.\n\n        Returns:\n            dict[str, Any]: Folder entities by id.\n        \"\"\"\n\n        output = {}\n        folder_ids = set(folder_ids)\n        if not project_name or not folder_ids:\n            return output\n\n        folder_ids_to_query = set()\n        for folder_id in folder_ids:\n            cache = self._folders_by_id[project_name][folder_id]\n            if cache.is_valid:\n                output[folder_id] = cache.get_data()\n            elif folder_id:\n                folder_ids_to_query.add(folder_id)\n            else:\n                output[folder_id] = None\n        self._query_folder_entities(project_name, folder_ids_to_query)\n        for folder_id in folder_ids_to_query:\n            cache = self._folders_by_id[project_name][folder_id]\n            output[folder_id] = cache.get_data()\n        return output\n\n    def get_folder_entity(self, project_name, folder_id):\n        output = self.get_folder_entities(project_name, {folder_id})\n        return output[folder_id]\n\n    def get_task_entities(self, project_name, task_ids):\n        output = {}\n        task_ids = set(task_ids)\n        if not project_name or not task_ids:\n            return output\n\n        task_ids_to_query = set()\n        for task_id in task_ids:\n            cache = self._tasks_by_id[project_name][task_id]\n            if cache.is_valid:\n                output[task_id] = cache.get_data()\n            elif task_id:\n                task_ids_to_query.add(task_id)\n            else:\n                output[task_id] = None\n        self._query_task_entities(project_name, task_ids_to_query)\n        for task_id in task_ids_to_query:\n            cache = self._tasks_by_id[project_name][task_id]\n            output[task_id] = cache.get_data()\n        return output\n\n    def get_task_entity(self, project_name, task_id):\n        output = self.get_task_entities(project_name, {task_id})\n        return output[task_id]\n\n    @contextlib.contextmanager\n    def _folder_refresh_event_manager(self, project_name, sender):\n        self._folders_refreshing.add(project_name)\n        self._controller.emit_event(\n            \"folders.refresh.started\",\n            {\"project_name\": project_name, \"sender\": sender},\n            HIERARCHY_MODEL_SENDER\n        )\n        try:\n            yield\n\n        finally:\n            self._controller.emit_event(\n                \"folders.refresh.finished\",\n                {\"project_name\": project_name, \"sender\": sender},\n                HIERARCHY_MODEL_SENDER\n            )\n            self._folders_refreshing.remove(project_name)\n\n    @contextlib.contextmanager\n    def _task_refresh_event_manager(\n        self, project_name, folder_id, sender\n    ):\n        self._tasks_refreshing.add(folder_id)\n        self._controller.emit_event(\n            \"tasks.refresh.started\",\n            {\n                \"project_name\": project_name,\n                \"folder_id\": folder_id,\n                \"sender\": sender,\n            },\n            HIERARCHY_MODEL_SENDER\n        )\n        try:\n            yield\n\n        finally:\n            self._controller.emit_event(\n                \"tasks.refresh.finished\",\n                {\n                    \"project_name\": project_name,\n                    \"folder_id\": folder_id,\n                    \"sender\": sender,\n                },\n                HIERARCHY_MODEL_SENDER\n            )\n            self._tasks_refreshing.discard(folder_id)\n\n    def _refresh_folders_cache(self, project_name, sender=None):\n        if project_name in self._folders_refreshing:\n            return\n\n        with self._folder_refresh_event_manager(project_name, sender):\n            folder_items = self._query_folders(project_name)\n            self._folders_items[project_name].update_data(folder_items)\n\n    def _query_folders(self, project_name):\n        hierarchy = ayon_api.get_folders_hierarchy(project_name)\n\n        folder_items = {}\n        hierachy_queue = collections.deque(hierarchy[\"hierarchy\"])\n        while hierachy_queue:\n            item = hierachy_queue.popleft()\n            folder_item = _get_folder_item_from_hierarchy_item(item)\n            folder_items[folder_item.entity_id] = folder_item\n            hierachy_queue.extend(item[\"children\"] or [])\n        return folder_items\n\n    def _query_folder_entities(self, project_name, folder_ids):\n        if not project_name or not folder_ids:\n            return\n        project_cache = self._folders_by_id[project_name]\n        folders = ayon_api.get_folders(project_name, folder_ids=folder_ids)\n        for folder in folders:\n            folder_id = folder[\"id\"]\n            project_cache[folder_id].update_data(folder)\n\n    def _query_task_entities(self, project_name, task_ids):\n        if not project_name or not task_ids:\n            return\n\n        project_cache = self._tasks_by_id[project_name]\n        tasks = ayon_api.get_tasks(project_name, task_ids=task_ids)\n        for task in tasks:\n            task_id = task[\"id\"]\n            project_cache[task_id].update_data(task)\n\n    def _refresh_tasks_cache(self, project_name, folder_id, sender=None):\n        if folder_id in self._tasks_refreshing:\n            return\n\n        with self._task_refresh_event_manager(\n            project_name, folder_id, sender\n        ):\n            task_items = self._query_tasks(project_name, folder_id)\n            self._task_items[project_name][folder_id] = task_items\n\n    def _query_tasks(self, project_name, folder_id):\n        tasks = list(ayon_api.get_tasks(\n            project_name,\n            folder_ids=[folder_id],\n            fields={\"id\", \"name\", \"label\", \"folderId\", \"type\"}\n        ))\n        return _get_task_items_from_tasks(tasks)\n"
  },
  {
    "path": "openpype/tools/ayon_utils/models/projects.py",
    "content": "import contextlib\nfrom abc import ABCMeta, abstractmethod\n\nimport ayon_api\nimport six\n\nfrom openpype.style import get_default_entity_icon_color\n\nfrom .cache import CacheItem\n\nPROJECTS_MODEL_SENDER = \"projects.model\"\n\n\n@six.add_metaclass(ABCMeta)\nclass AbstractHierarchyController:\n    @abstractmethod\n    def emit_event(self, topic, data, source):\n        pass\n\n\nclass ProjectItem:\n    \"\"\"Item representing folder entity on a server.\n\n    Folder can be a child of another folder or a project.\n\n    Args:\n        name (str): Project name.\n        active (Union[str, None]): Parent folder id. If 'None' then project\n            is parent.\n    \"\"\"\n\n    def __init__(self, name, active, is_library, icon=None):\n        self.name = name\n        self.active = active\n        self.is_library = is_library\n        if icon is None:\n            icon = {\n                \"type\": \"awesome-font\",\n                \"name\": \"fa.book\" if is_library else \"fa.map\",\n                \"color\": get_default_entity_icon_color(),\n            }\n        self.icon = icon\n\n    def to_data(self):\n        \"\"\"Converts folder item to data.\n\n        Returns:\n            dict[str, Any]: Folder item data.\n        \"\"\"\n\n        return {\n            \"name\": self.name,\n            \"active\": self.active,\n            \"is_library\": self.is_library,\n            \"icon\": self.icon,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        \"\"\"Re-creates folder item from data.\n\n        Args:\n            data (dict[str, Any]): Folder item data.\n\n        Returns:\n            FolderItem: Folder item.\n        \"\"\"\n\n        return cls(**data)\n\n\ndef _get_project_items_from_entitiy(projects):\n    \"\"\"\n\n    Args:\n        projects (list[dict[str, Any]]): List of projects.\n\n    Returns:\n        ProjectItem: Project item.\n    \"\"\"\n\n    return [\n        ProjectItem(project[\"name\"], project[\"active\"], project[\"library\"])\n        for project in projects\n    ]\n\n\nclass ProjectsModel(object):\n    def __init__(self, controller):\n        self._projects_cache = CacheItem(default_factory=list)\n        self._project_items_by_name = {}\n        self._projects_by_name = {}\n\n        self._is_refreshing = False\n        self._controller = controller\n\n    def reset(self):\n        self._projects_cache.reset()\n        self._project_items_by_name = {}\n        self._projects_by_name = {}\n\n    def refresh(self):\n        self._refresh_projects_cache()\n\n    def get_project_items(self, sender):\n        \"\"\"\n\n        Args:\n            sender (str): Name of sender who asked for items.\n\n        Returns:\n            Union[list[ProjectItem], None]: List of project items, or None\n                if model is refreshing.\n        \"\"\"\n\n        if not self._projects_cache.is_valid:\n            return self._refresh_projects_cache(sender)\n        return self._projects_cache.get_data()\n\n    def get_project_entity(self, project_name):\n        if project_name not in self._projects_by_name:\n            entity = None\n            if project_name:\n                entity = ayon_api.get_project(project_name)\n            self._projects_by_name[project_name] = entity\n        return self._projects_by_name[project_name]\n\n    @contextlib.contextmanager\n    def _project_refresh_event_manager(self, sender):\n        self._is_refreshing = True\n        self._controller.emit_event(\n            \"projects.refresh.started\",\n            {\"sender\": sender},\n            PROJECTS_MODEL_SENDER\n        )\n        try:\n            yield\n\n        finally:\n            self._controller.emit_event(\n                \"projects.refresh.finished\",\n                {\"sender\": sender},\n                PROJECTS_MODEL_SENDER\n            )\n            self._is_refreshing = False\n\n    def _refresh_projects_cache(self, sender=None):\n        if self._is_refreshing:\n            return None\n\n        with self._project_refresh_event_manager(sender):\n            project_items = self._query_projects()\n            self._projects_cache.update_data(project_items)\n        return self._projects_cache.get_data()\n\n    def _query_projects(self):\n        projects = ayon_api.get_projects(fields=[\"name\", \"active\", \"library\"])\n        return _get_project_items_from_entitiy(projects)\n"
  },
  {
    "path": "openpype/tools/ayon_utils/models/selection.py",
    "content": "class _ExampleController:\n    def emit_event(self, topic, data, **kwargs):\n        pass\n\n\nclass HierarchyExpectedSelection:\n    \"\"\"Base skeleton of expected selection model.\n\n    Expected selection model holds information about which entities should be\n    selected. The order of selection is very important as change of project\n    will affect what folders are available in folders UI and so on. Because\n    of that should expected selection model know what is current entity\n    to select.\n\n    If any of 'handle_project', 'handle_folder' or 'handle_task' is set to\n    'False' expected selection data won't contain information about the\n    entity type at all. Also if project is not handled then it is not\n    necessary to call 'expected_project_selected'. Same goes for folder and\n    task.\n\n    Model is triggering event with 'expected_selection_changed' topic and\n    data > data structure is matching 'get_expected_selection_data' method.\n\n    Questions:\n        Require '_ExampleController' as abstraction?\n\n    Args:\n        controller (Any): Controller object. ('_ExampleController')\n        handle_project (bool): Project can be considered as can have expected\n            selection.\n        handle_folder (bool): Folder can be considered as can have expected\n            selection.\n        handle_task (bool): Task can be considered as can have expected\n            selection.\n    \"\"\"\n\n    def __init__(\n        self,\n        controller,\n        handle_project=True,\n        handle_folder=True,\n        handle_task=True\n    ):\n        self._project_name = None\n        self._folder_id = None\n        self._task_name = None\n\n        self._project_selected = True\n        self._folder_selected = True\n        self._task_selected = True\n\n        self._controller = controller\n\n        self._handle_project = handle_project\n        self._handle_folder = handle_folder\n        self._handle_task = handle_task\n\n    def set_expected_selection(\n        self,\n        project_name=None,\n        folder_id=None,\n        task_name=None\n    ):\n        \"\"\"Sets expected selection.\n\n        Args:\n            project_name (Optional[str]): Project name.\n            folder_id (Optional[str]): Folder id.\n            task_name (Optional[str]): Task name.\n        \"\"\"\n\n        self._project_name = project_name\n        self._folder_id = folder_id\n        self._task_name = task_name\n\n        self._project_selected = not self._handle_project\n        self._folder_selected = not self._handle_folder\n        self._task_selected = not self._handle_task\n        self._emit_change()\n\n    def get_expected_selection_data(self):\n        project_current = False\n        folder_current = False\n        task_current = False\n        if not self._project_selected:\n            project_current = True\n        elif not self._folder_selected:\n            folder_current = True\n        elif not self._task_selected:\n            task_current = True\n        data = {}\n        if self._handle_project:\n            data[\"project\"] = {\n                \"name\": self._project_name,\n                \"current\": project_current,\n                \"selected\": self._project_selected,\n            }\n        if self._handle_folder:\n            data[\"folder\"] = {\n                \"id\": self._folder_id,\n                \"current\": folder_current,\n                \"selected\": self._folder_selected,\n            }\n        if self._handle_task:\n            data[\"task\"] = {\n                \"name\": self._task_name,\n                \"current\": task_current,\n                \"selected\": self._task_selected,\n            }\n\n        return data\n\n    def is_expected_project_selected(self, project_name):\n        if not self._handle_project:\n            return True\n        return project_name == self._project_name and self._project_selected\n\n    def is_expected_folder_selected(self, folder_id):\n        if not self._handle_folder:\n            return True\n        return folder_id == self._folder_id and self._folder_selected\n\n    def expected_project_selected(self, project_name):\n        \"\"\"UI selected requested project.\n\n        Other entity types can be requested for selection.\n\n        Args:\n            project_name (str): Name of project.\n        \"\"\"\n\n        if project_name != self._project_name:\n            return False\n        self._project_selected = True\n        self._emit_change()\n        return True\n\n    def expected_folder_selected(self, folder_id):\n        \"\"\"UI selected requested folder.\n\n        Other entity types can be requested for selection.\n\n        Args:\n            folder_id (str): Folder id.\n        \"\"\"\n\n        if folder_id != self._folder_id:\n            return False\n        self._folder_selected = True\n        self._emit_change()\n        return True\n\n    def expected_task_selected(self, folder_id, task_name):\n        \"\"\"UI selected requested task.\n\n        Other entity types can be requested for selection.\n\n        Because task name is not unique across project a folder id is also\n        required to confirm the right task has been selected.\n\n        Args:\n            folder_id (str): Folder id.\n            task_name (str): Task name.\n        \"\"\"\n\n        if self._folder_id != folder_id:\n            return False\n\n        if task_name != self._task_name:\n            return False\n        self._task_selected = True\n        self._emit_change()\n        return True\n\n    def _emit_change(self):\n        self._controller.emit_event(\n            \"expected_selection_changed\",\n            self.get_expected_selection_data(),\n        )\n"
  },
  {
    "path": "openpype/tools/ayon_utils/models/thumbnails.py",
    "content": "import collections\n\nimport ayon_api\n\nfrom openpype.client.server.thumbnails import AYONThumbnailCache\n\nfrom .cache import NestedCacheItem\n\n\nclass ThumbnailsModel:\n    entity_cache_lifetime = 240  # In seconds\n\n    def __init__(self):\n        self._thumbnail_cache = AYONThumbnailCache()\n        self._paths_cache = collections.defaultdict(dict)\n        self._folders_cache = NestedCacheItem(\n            levels=2, lifetime=self.entity_cache_lifetime)\n        self._versions_cache = NestedCacheItem(\n            levels=2, lifetime=self.entity_cache_lifetime)\n\n    def reset(self):\n        self._paths_cache = collections.defaultdict(dict)\n        self._folders_cache.reset()\n        self._versions_cache.reset()\n\n    def get_thumbnail_path(self, project_name, thumbnail_id):\n        return self._get_thumbnail_path(project_name, thumbnail_id)\n\n    def get_folder_thumbnail_ids(self, project_name, folder_ids):\n        project_cache = self._folders_cache[project_name]\n        output = {}\n        missing_cache = set()\n        for folder_id in folder_ids:\n            cache = project_cache[folder_id]\n            if cache.is_valid:\n                output[folder_id] = cache.get_data()\n            else:\n                missing_cache.add(folder_id)\n        self._query_folder_thumbnail_ids(project_name, missing_cache)\n        for folder_id in missing_cache:\n            cache = project_cache[folder_id]\n            output[folder_id] = cache.get_data()\n        return output\n\n    def get_version_thumbnail_ids(self, project_name, version_ids):\n        project_cache = self._versions_cache[project_name]\n        output = {}\n        missing_cache = set()\n        for version_id in version_ids:\n            cache = project_cache[version_id]\n            if cache.is_valid:\n                output[version_id] = cache.get_data()\n            else:\n                missing_cache.add(version_id)\n        self._query_version_thumbnail_ids(project_name, missing_cache)\n        for version_id in missing_cache:\n            cache = project_cache[version_id]\n            output[version_id] = cache.get_data()\n        return output\n\n    def _get_thumbnail_path(self, project_name, thumbnail_id):\n        if not thumbnail_id:\n            return None\n\n        project_cache = self._paths_cache[project_name]\n        if thumbnail_id in project_cache:\n            return project_cache[thumbnail_id]\n\n        filepath = self._thumbnail_cache.get_thumbnail_filepath(\n            project_name, thumbnail_id\n        )\n        if filepath is not None:\n            project_cache[thumbnail_id] = filepath\n            return filepath\n\n        # 'ayon_api' had a bug, public function\n        #   'get_thumbnail_by_id' did not return output of\n        #   'ServerAPI' method.\n        con = ayon_api.get_server_api_connection()\n        result = con.get_thumbnail_by_id(project_name, thumbnail_id)\n        if result is None:\n            pass\n\n        elif result.is_valid:\n            filepath = self._thumbnail_cache.store_thumbnail(\n                project_name,\n                thumbnail_id,\n                result.content,\n                result.content_type\n            )\n        project_cache[thumbnail_id] = filepath\n        return filepath\n\n    def _query_folder_thumbnail_ids(self, project_name, folder_ids):\n        if not project_name or not folder_ids:\n            return\n\n        folders = ayon_api.get_folders(\n            project_name,\n            folder_ids=folder_ids,\n            fields=[\"id\", \"thumbnailId\"]\n        )\n        project_cache = self._folders_cache[project_name]\n        for folder in folders:\n            project_cache[folder[\"id\"]] = folder[\"thumbnailId\"]\n\n    def _query_version_thumbnail_ids(self, project_name, version_ids):\n        if not project_name or not version_ids:\n            return\n\n        versions = ayon_api.get_versions(\n            project_name,\n            version_ids=version_ids,\n            fields=[\"id\", \"thumbnailId\"]\n        )\n        project_cache = self._versions_cache[project_name]\n        for version in versions:\n            project_cache[version[\"id\"]] = version[\"thumbnailId\"]\n"
  },
  {
    "path": "openpype/tools/ayon_utils/widgets/__init__.py",
    "content": "from .projects_widget import (\n    # ProjectsWidget,\n    ProjectsCombobox,\n    ProjectsQtModel,\n    ProjectSortFilterProxy,\n)\n\nfrom .folders_widget import (\n    FoldersWidget,\n    FoldersQtModel,\n    FOLDERS_MODEL_SENDER_NAME,\n)\n\nfrom .tasks_widget import (\n    TasksWidget,\n    TasksQtModel,\n    TASKS_MODEL_SENDER_NAME,\n)\nfrom .utils import (\n    get_qt_icon,\n    RefreshThread,\n)\n\n\n__all__ = (\n    # \"ProjectsWidget\",\n    \"ProjectsCombobox\",\n    \"ProjectsQtModel\",\n    \"ProjectSortFilterProxy\",\n\n    \"FoldersWidget\",\n    \"FoldersQtModel\",\n    \"FOLDERS_MODEL_SENDER_NAME\",\n\n    \"TasksWidget\",\n    \"TasksQtModel\",\n    \"TASKS_MODEL_SENDER_NAME\",\n\n    \"get_qt_icon\",\n    \"RefreshThread\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_utils/widgets/folders_widget.py",
    "content": "import collections\n\nfrom qtpy import QtWidgets, QtGui, QtCore\n\nfrom openpype.tools.utils import (\n    RecursiveSortFilterProxyModel,\n    TreeView,\n)\n\nfrom .utils import RefreshThread, get_qt_icon\n\nFOLDERS_MODEL_SENDER_NAME = \"qt_folders_model\"\nFOLDER_ID_ROLE = QtCore.Qt.UserRole + 1\nFOLDER_NAME_ROLE = QtCore.Qt.UserRole + 2\nFOLDER_PATH_ROLE = QtCore.Qt.UserRole + 3\nFOLDER_TYPE_ROLE = QtCore.Qt.UserRole + 4\n\n\nclass FoldersQtModel(QtGui.QStandardItemModel):\n    \"\"\"Folders model which cares about refresh of folders.\n\n    Args:\n        controller (AbstractWorkfilesFrontend): The control object.\n    \"\"\"\n\n    refreshed = QtCore.Signal()\n\n    def __init__(self, controller):\n        super(FoldersQtModel, self).__init__()\n\n        self._controller = controller\n        self._items_by_id = {}\n        self._parent_id_by_id = {}\n\n        self._refresh_threads = {}\n        self._current_refresh_thread = None\n        self._last_project_name = None\n\n        self._has_content = False\n        self._is_refreshing = False\n\n    @property\n    def is_refreshing(self):\n        \"\"\"Model is refreshing.\n\n        Returns:\n            bool: True if model is refreshing.\n        \"\"\"\n        return self._is_refreshing\n\n    @property\n    def has_content(self):\n        \"\"\"Has at least one folder.\n\n        Returns:\n            bool: True if model has at least one folder.\n        \"\"\"\n\n        return self._has_content\n\n    def refresh(self):\n        \"\"\"Refresh folders for last selected project.\n\n        Force to update folders model from controller. This may or may not\n        trigger query from server, that's based on controller's cache.\n        \"\"\"\n\n        self.set_project_name(self._last_project_name)\n\n    def _clear_items(self):\n        self._items_by_id = {}\n        self._parent_id_by_id = {}\n        self._has_content = False\n        root_item = self.invisibleRootItem()\n        root_item.removeRows(0, root_item.rowCount())\n\n    def get_index_by_id(self, item_id):\n        \"\"\"Get index by folder id.\n\n        Returns:\n            QtCore.QModelIndex: Index of the folder. Can be invalid if folder\n                is not available.\n        \"\"\"\n        item = self._items_by_id.get(item_id)\n        if item is None:\n            return QtCore.QModelIndex()\n        return self.indexFromItem(item)\n\n    def get_project_name(self):\n        \"\"\"Project name which model currently use.\n\n        Returns:\n            Union[str, None]: Currently used project name.\n        \"\"\"\n\n        return self._last_project_name\n\n    def set_project_name(self, project_name):\n        \"\"\"Refresh folders items.\n\n        Refresh start thread because it can cause that controller can\n        start query from database if folders are not cached.\n        \"\"\"\n\n        if not project_name:\n            self._last_project_name = project_name\n            self._fill_items({})\n            self._current_refresh_thread = None\n            return\n\n        self._is_refreshing = True\n\n        if self._last_project_name != project_name:\n            self._clear_items()\n        self._last_project_name = project_name\n\n        thread = self._refresh_threads.get(project_name)\n        if thread is not None:\n            self._current_refresh_thread = thread\n            return\n\n        thread = RefreshThread(\n            project_name,\n            self._controller.get_folder_items,\n            project_name,\n            FOLDERS_MODEL_SENDER_NAME\n        )\n        self._current_refresh_thread = thread\n        self._refresh_threads[thread.id] = thread\n        thread.refresh_finished.connect(self._on_refresh_thread)\n        thread.start()\n\n    def _on_refresh_thread(self, thread_id):\n        \"\"\"Callback when refresh thread is finished.\n\n        Technically can be running multiple refresh threads at the same time,\n        to avoid using values from wrong thread, we check if thread id is\n        current refresh thread id.\n\n        Folders are stored by id.\n\n        Args:\n            thread_id (str): Thread id.\n        \"\"\"\n\n        # Make sure to remove thread from '_refresh_threads' dict\n        thread = self._refresh_threads.pop(thread_id)\n        if (\n            self._current_refresh_thread is None\n            or thread_id != self._current_refresh_thread.id\n        ):\n            return\n\n        self._fill_items(thread.get_result())\n        self._current_refresh_thread = None\n\n    def _fill_item_data(self, item, folder_item):\n        \"\"\"\n\n        Args:\n            item (QtGui.QStandardItem): Item to fill data.\n            folder_item (FolderItem): Folder item.\n        \"\"\"\n\n        icon = get_qt_icon(folder_item.icon)\n        item.setData(folder_item.entity_id, FOLDER_ID_ROLE)\n        item.setData(folder_item.name, FOLDER_NAME_ROLE)\n        item.setData(folder_item.path, FOLDER_PATH_ROLE)\n        item.setData(folder_item.folder_type, FOLDER_TYPE_ROLE)\n        item.setData(folder_item.label, QtCore.Qt.DisplayRole)\n        item.setData(icon, QtCore.Qt.DecorationRole)\n\n    def _fill_items(self, folder_items_by_id):\n        if not folder_items_by_id:\n            if folder_items_by_id is not None:\n                self._clear_items()\n            self._is_refreshing = False\n            self.refreshed.emit()\n            return\n\n        self._has_content = True\n\n        folder_ids = set(folder_items_by_id)\n        ids_to_remove = set(self._items_by_id) - folder_ids\n\n        folder_items_by_parent = collections.defaultdict(dict)\n        for folder_item in folder_items_by_id.values():\n            (\n                folder_items_by_parent\n                [folder_item.parent_id]\n                [folder_item.entity_id]\n            ) = folder_item\n\n        hierarchy_queue = collections.deque()\n        hierarchy_queue.append((self.invisibleRootItem(), None))\n\n        # Keep pointers to removed items until the refresh finishes\n        #   - some children of the items could be moved and reused elsewhere\n        removed_items = []\n        while hierarchy_queue:\n            item = hierarchy_queue.popleft()\n            parent_item, parent_id = item\n            folder_items = folder_items_by_parent[parent_id]\n\n            items_by_id = {}\n            folder_ids_to_add = set(folder_items)\n            for row_idx in reversed(range(parent_item.rowCount())):\n                child_item = parent_item.child(row_idx)\n                child_id = child_item.data(FOLDER_ID_ROLE)\n                if child_id in ids_to_remove:\n                    removed_items.append(parent_item.takeRow(row_idx))\n                else:\n                    items_by_id[child_id] = child_item\n\n            new_items = []\n            for item_id in folder_ids_to_add:\n                folder_item = folder_items[item_id]\n                item = items_by_id.get(item_id)\n                if item is None:\n                    is_new = True\n                    item = QtGui.QStandardItem()\n                    item.setEditable(False)\n                else:\n                    is_new = self._parent_id_by_id[item_id] != parent_id\n\n                self._fill_item_data(item, folder_item)\n                if is_new:\n                    new_items.append(item)\n                self._items_by_id[item_id] = item\n                self._parent_id_by_id[item_id] = parent_id\n\n                hierarchy_queue.append((item, item_id))\n\n            if new_items:\n                parent_item.appendRows(new_items)\n\n        for item_id in ids_to_remove:\n            self._items_by_id.pop(item_id)\n            self._parent_id_by_id.pop(item_id)\n\n        self._is_refreshing = False\n        self.refreshed.emit()\n\n\nclass FoldersWidget(QtWidgets.QWidget):\n    \"\"\"Folders widget.\n\n    Widget that handles folders view, model and selection.\n\n    Expected selection handling is disabled by default. If enabled, the\n    widget will handle the expected in predefined way. Widget is listening\n    to event 'expected_selection_changed' with expected event data below,\n    the same data must be available when called method\n    'get_expected_selection_data' on controller.\n\n    {\n        \"folder\": {\n            \"current\": bool,               # Folder is what should be set now\n            \"folder_id\": Union[str, None], # Folder id that should be selected\n        },\n        ...\n    }\n\n    Selection is confirmed by calling method 'expected_folder_selected' on\n    controller.\n\n\n    Args:\n        controller (AbstractWorkfilesFrontend): The control object.\n        parent (QtWidgets.QWidget): The parent widget.\n        handle_expected_selection (bool): If True, the widget will handle\n            the expected selection. Defaults to False.\n    \"\"\"\n\n    double_clicked = QtCore.Signal(QtGui.QMouseEvent)\n    selection_changed = QtCore.Signal()\n    refreshed = QtCore.Signal()\n\n    def __init__(self, controller, parent, handle_expected_selection=False):\n        super(FoldersWidget, self).__init__(parent)\n\n        folders_view = TreeView(self)\n        folders_view.setHeaderHidden(True)\n\n        folders_model = FoldersQtModel(controller)\n        folders_proxy_model = RecursiveSortFilterProxyModel()\n        folders_proxy_model.setSourceModel(folders_model)\n        folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        folders_view.setModel(folders_proxy_model)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(folders_view, 1)\n\n        controller.register_event_callback(\n            \"selection.project.changed\",\n            self._on_project_selection_change,\n        )\n        controller.register_event_callback(\n            \"folders.refresh.finished\",\n            self._on_folders_refresh_finished\n        )\n        controller.register_event_callback(\n            \"controller.refresh.finished\",\n            self._on_controller_refresh\n        )\n        controller.register_event_callback(\n            \"expected_selection_changed\",\n            self._on_expected_selection_change\n        )\n\n        selection_model = folders_view.selectionModel()\n        selection_model.selectionChanged.connect(self._on_selection_change)\n        folders_view.double_clicked.connect(self.double_clicked)\n        folders_model.refreshed.connect(self._on_model_refresh)\n\n        self._controller = controller\n        self._folders_view = folders_view\n        self._folders_model = folders_model\n        self._folders_proxy_model = folders_proxy_model\n\n        self._handle_expected_selection = handle_expected_selection\n        self._expected_selection = None\n\n    @property\n    def is_refreshing(self):\n        \"\"\"Model is refreshing.\n\n        Returns:\n            bool: True if model is refreshing.\n        \"\"\"\n\n        return self._folders_model.is_refreshing\n\n    @property\n    def has_content(self):\n        \"\"\"Has at least one folder.\n\n        Returns:\n            bool: True if model has at least one folder.\n        \"\"\"\n\n        return self._folders_model.has_content\n\n    def set_name_filter(self, name):\n        \"\"\"Set filter of folder name.\n\n        Args:\n            name (str): The string filter.\n        \"\"\"\n\n        self._folders_proxy_model.setFilterFixedString(name)\n\n    def refresh(self):\n        \"\"\"Refresh folders model.\n\n        Force to update folders model from controller.\n        \"\"\"\n\n        self._folders_model.refresh()\n\n    def get_project_name(self):\n        \"\"\"Project name in which folders widget currently is.\n\n        Returns:\n            Union[str, None]: Currently used project name.\n        \"\"\"\n\n        return self._folders_model.get_project_name()\n\n    def set_project_name(self, project_name):\n        \"\"\"Set project name.\n\n        Do not use this method when controller is handling selection of\n        project using 'selection.project.changed' event.\n\n        Args:\n            project_name (str): Project name.\n        \"\"\"\n\n        self._folders_model.set_project_name(project_name)\n\n    def get_selected_folder_id(self):\n        \"\"\"Get selected folder id.\n\n        Returns:\n            Union[str, None]: Folder id which is selected.\n        \"\"\"\n\n        return self._get_selected_item_id()\n\n    def get_selected_folder_label(self):\n        \"\"\"Selected folder label.\n\n        Returns:\n            Union[str, None]: Selected folder label.\n        \"\"\"\n\n        item_id = self._get_selected_item_id()\n        return self.get_folder_label(item_id)\n\n    def get_folder_label(self, folder_id):\n        \"\"\"Folder label for a given folder id.\n\n        Returns:\n            Union[str, None]: Folder label.\n        \"\"\"\n\n        index = self._folders_model.get_index_by_id(folder_id)\n        if index.isValid():\n            return index.data(QtCore.Qt.DisplayRole)\n        return None\n\n    def set_selected_folder(self, folder_id):\n        \"\"\"Change selection.\n\n        Args:\n            folder_id (Union[str, None]): Folder id or None to deselect.\n        \"\"\"\n\n        if folder_id is None:\n            self._folders_view.clearSelection()\n            return True\n\n        if folder_id == self._get_selected_item_id():\n            return True\n        index = self._folders_model.get_index_by_id(folder_id)\n        if not index.isValid():\n            return False\n\n        proxy_index = self._folders_proxy_model.mapFromSource(index)\n        if not proxy_index.isValid():\n            return False\n\n        selection_model = self._folders_view.selectionModel()\n        selection_model.setCurrentIndex(\n            proxy_index, QtCore.QItemSelectionModel.SelectCurrent\n        )\n        return True\n\n    def set_deselectable(self, enabled):\n        \"\"\"Set deselectable mode.\n\n        Items in view can be deselected.\n\n        Args:\n            enabled (bool): Enable deselectable mode.\n        \"\"\"\n\n        self._folders_view.set_deselectable(enabled)\n\n    def _get_selected_index(self):\n        return self._folders_model.get_index_by_id(\n            self.get_selected_folder_id()\n        )\n\n    def _on_project_selection_change(self, event):\n        project_name = event[\"project_name\"]\n        self.set_project_name(project_name)\n\n    def _on_folders_refresh_finished(self, event):\n        if event[\"sender\"] != FOLDERS_MODEL_SENDER_NAME:\n            self.set_project_name(event[\"project_name\"])\n\n    def _on_controller_refresh(self):\n        self._update_expected_selection()\n\n    def _on_model_refresh(self):\n        if self._expected_selection:\n            self._set_expected_selection()\n        self._folders_proxy_model.sort(0)\n        self.refreshed.emit()\n\n    def _get_selected_item_id(self):\n        selection_model = self._folders_view.selectionModel()\n        for index in selection_model.selectedIndexes():\n            item_id = index.data(FOLDER_ID_ROLE)\n            if item_id is not None:\n                return item_id\n        return None\n\n    def _on_selection_change(self):\n        item_id = self._get_selected_item_id()\n        self._controller.set_selected_folder(item_id)\n        self.selection_changed.emit()\n\n    # Expected selection handling\n    def _on_expected_selection_change(self, event):\n        self._update_expected_selection(event.data)\n\n    def _update_expected_selection(self, expected_data=None):\n        if not self._handle_expected_selection:\n            return\n\n        if expected_data is None:\n            expected_data = self._controller.get_expected_selection_data()\n\n        folder_data = expected_data.get(\"folder\")\n        if not folder_data or not folder_data[\"current\"]:\n            return\n\n        folder_id = folder_data[\"id\"]\n        self._expected_selection = folder_id\n        if not self._folders_model.is_refreshing:\n            self._set_expected_selection()\n\n    def _set_expected_selection(self):\n        if not self._handle_expected_selection:\n            return\n\n        folder_id = self._expected_selection\n        self._expected_selection = None\n        if folder_id is not None:\n            self.set_selected_folder(folder_id)\n        self._controller.expected_folder_selected(folder_id)\n"
  },
  {
    "path": "openpype/tools/ayon_utils/widgets/projects_widget.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER\nfrom .utils import RefreshThread, get_qt_icon\n\nPROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1\nPROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2\nPROJECT_IS_LIBRARY_ROLE = QtCore.Qt.UserRole + 3\nPROJECT_IS_CURRENT_ROLE = QtCore.Qt.UserRole + 4\nLIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 5\n\n\nclass ProjectsQtModel(QtGui.QStandardItemModel):\n    refreshed = QtCore.Signal()\n\n    def __init__(self, controller):\n        super(ProjectsQtModel, self).__init__()\n        self._controller = controller\n\n        self._project_items = {}\n        self._has_libraries = False\n\n        self._empty_item = None\n        self._empty_item_added = False\n\n        self._select_item = None\n        self._select_item_added = False\n        self._select_item_visible = None\n\n        self._libraries_sep_item = None\n        self._libraries_sep_item_added = False\n        self._libraries_sep_item_visible = False\n\n        self._current_context_project = None\n\n        self._selected_project = None\n\n        self._refresh_thread = None\n\n    @property\n    def is_refreshing(self):\n        return self._refresh_thread is not None\n\n    def refresh(self):\n        self._refresh()\n\n    def has_content(self):\n        return len(self._project_items) > 0\n\n    def set_select_item_visible(self, visible):\n        if self._select_item_visible is visible:\n            return\n        self._select_item_visible = visible\n\n        if self._selected_project is None:\n            self._add_select_item()\n\n    def set_libraries_separator_visible(self, visible):\n        if self._libraries_sep_item_visible is visible:\n            return\n        self._libraries_sep_item_visible = visible\n\n    def set_selected_project(self, project_name):\n        if not self._select_item_visible:\n            return\n\n        self._selected_project = project_name\n        if project_name is None:\n            self._add_select_item()\n        else:\n            self._remove_select_item()\n\n    def set_current_context_project(self, project_name):\n        if project_name == self._current_context_project:\n            return\n        self._unset_current_context_project(self._current_context_project)\n        self._current_context_project = project_name\n        self._set_current_context_project(project_name)\n\n    def _set_current_context_project(self, project_name):\n        item = self._project_items.get(project_name)\n        if item is None:\n            return\n        item.setData(True, PROJECT_IS_CURRENT_ROLE)\n\n    def _unset_current_context_project(self, project_name):\n        item = self._project_items.get(project_name)\n        if item is None:\n            return\n        item.setData(False, PROJECT_IS_CURRENT_ROLE)\n\n    def _add_empty_item(self):\n        if self._empty_item_added:\n            return\n        self._empty_item_added = True\n        item = self._get_empty_item()\n        root_item = self.invisibleRootItem()\n        root_item.appendRow(item)\n\n    def _remove_empty_item(self):\n        if not self._empty_item_added:\n            return\n        self._empty_item_added = False\n        root_item = self.invisibleRootItem()\n        item = self._get_empty_item()\n        root_item.takeRow(item.row())\n\n    def _get_empty_item(self):\n        if self._empty_item is None:\n            item = QtGui.QStandardItem(\"< No projects >\")\n            item.setFlags(QtCore.Qt.NoItemFlags)\n            self._empty_item = item\n        return self._empty_item\n\n    def _get_library_sep_item(self):\n        if self._libraries_sep_item is not None:\n            return self._libraries_sep_item\n\n        item = QtGui.QStandardItem()\n        item.setData(\"Libraries\", QtCore.Qt.DisplayRole)\n        item.setData(True, LIBRARY_PROJECT_SEPARATOR_ROLE)\n        item.setFlags(QtCore.Qt.NoItemFlags)\n        self._libraries_sep_item = item\n        return item\n\n    def _add_library_sep_item(self):\n        if (\n            not self._libraries_sep_item_visible\n            or self._libraries_sep_item_added\n        ):\n            return\n        self._libraries_sep_item_added = True\n        item = self._get_library_sep_item()\n        root_item = self.invisibleRootItem()\n        root_item.appendRow(item)\n\n    def _remove_library_sep_item(self):\n        if (\n            not self._libraries_sep_item_added\n        ):\n            return\n        self._libraries_sep_item_added = False\n        item = self._get_library_sep_item()\n        root_item = self.invisibleRootItem()\n        root_item.takeRow(item.row())\n\n    def _add_select_item(self):\n        if self._select_item_added:\n            return\n        self._select_item_added = True\n        item = self._get_select_item()\n        root_item = self.invisibleRootItem()\n        root_item.appendRow(item)\n\n    def _remove_select_item(self):\n        if not self._select_item_added:\n            return\n        self._select_item_added = False\n        root_item = self.invisibleRootItem()\n        item = self._get_select_item()\n        root_item.takeRow(item.row())\n\n    def _get_select_item(self):\n        if self._select_item is None:\n            item = QtGui.QStandardItem(\"< Select project >\")\n            item.setEditable(False)\n            self._select_item = item\n        return self._select_item\n\n    def _refresh(self):\n        if self._refresh_thread is not None:\n            return\n\n        refresh_thread = RefreshThread(\n            \"projects\", self._query_project_items\n        )\n        refresh_thread.refresh_finished.connect(self._refresh_finished)\n\n        self._refresh_thread = refresh_thread\n        refresh_thread.start()\n\n    def _query_project_items(self):\n        return self._controller.get_project_items(\n            sender=PROJECTS_MODEL_SENDER\n        )\n\n    def _refresh_finished(self):\n        # TODO check if failed\n        result = self._refresh_thread.get_result()\n        if result is not None:\n            self._fill_items(result)\n\n        self._refresh_thread = None\n        if result is None:\n            self._refresh()\n        else:\n            self.refreshed.emit()\n\n    def _fill_items(self, project_items):\n        new_project_names = {\n            project_item.name\n            for project_item in project_items\n        }\n\n        # Handle \"Select item\" visibility\n        if self._select_item_visible:\n            # Add select project. if previously selected project is not in\n            #   project items\n            if self._selected_project not in new_project_names:\n                self._add_select_item()\n            else:\n                self._remove_select_item()\n\n        root_item = self.invisibleRootItem()\n\n        items_to_remove = set(self._project_items.keys()) - new_project_names\n        for project_name in items_to_remove:\n            item = self._project_items.pop(project_name)\n            root_item.takeRow(item.row())\n\n        has_library_project = False\n        new_items = []\n        for project_item in project_items:\n            project_name = project_item.name\n            item = self._project_items.get(project_name)\n            if project_item.is_library:\n                has_library_project = True\n            if item is None:\n                item = QtGui.QStandardItem()\n                item.setEditable(False)\n                new_items.append(item)\n            icon = get_qt_icon(project_item.icon)\n            item.setData(project_name, QtCore.Qt.DisplayRole)\n            item.setData(icon, QtCore.Qt.DecorationRole)\n            item.setData(project_name, PROJECT_NAME_ROLE)\n            item.setData(project_item.active, PROJECT_IS_ACTIVE_ROLE)\n            item.setData(project_item.is_library, PROJECT_IS_LIBRARY_ROLE)\n            is_current = project_name == self._current_context_project\n            item.setData(is_current, PROJECT_IS_CURRENT_ROLE)\n            self._project_items[project_name] = item\n\n        self._set_current_context_project(self._current_context_project)\n\n        self._has_libraries = has_library_project\n\n        if new_items:\n            root_item.appendRows(new_items)\n\n        if self.has_content():\n            # Make sure \"No projects\" item is removed\n            self._remove_empty_item()\n            if has_library_project:\n                self._add_library_sep_item()\n            else:\n                self._remove_library_sep_item()\n        else:\n            # Keep only \"No projects\" item\n            self._add_empty_item()\n            self._remove_select_item()\n            self._remove_library_sep_item()\n\n\nclass ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):\n    def __init__(self, *args, **kwargs):\n        super(ProjectSortFilterProxy, self).__init__(*args, **kwargs)\n        self._filter_inactive = True\n        self._filter_standard = False\n        self._filter_library = False\n        self._sort_by_type = True\n        # Disable case sensitivity\n        self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n    def _type_sort(self, l_index, r_index):\n        if not self._sort_by_type:\n            return None\n\n        l_is_library = l_index.data(PROJECT_IS_LIBRARY_ROLE)\n        r_is_library = r_index.data(PROJECT_IS_LIBRARY_ROLE)\n        # Both hare project items\n        if l_is_library is not None and r_is_library is not None:\n            if l_is_library is r_is_library:\n                return None\n            if l_is_library:\n                return False\n            return True\n\n        if l_index.data(LIBRARY_PROJECT_SEPARATOR_ROLE):\n            if r_is_library is None:\n                return False\n            return r_is_library\n\n        if r_index.data(LIBRARY_PROJECT_SEPARATOR_ROLE):\n            if l_is_library is None:\n                return True\n            return l_is_library\n        return None\n\n    def lessThan(self, left_index, right_index):\n        # Current project always on top\n        # - make sure this is always first, before any other sorting\n        #   e.g. type sort would move the item lower\n        if left_index.data(PROJECT_IS_CURRENT_ROLE):\n            return True\n        if right_index.data(PROJECT_IS_CURRENT_ROLE):\n            return False\n\n        # Library separator should be before library projects\n        result = self._type_sort(left_index, right_index)\n        if result is not None:\n            return result\n\n        if left_index.data(PROJECT_NAME_ROLE) is None:\n            return True\n\n        if right_index.data(PROJECT_NAME_ROLE) is None:\n            return False\n\n        left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE)\n        right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE)\n        if right_is_active == left_is_active:\n            return super(ProjectSortFilterProxy, self).lessThan(\n                left_index, right_index\n            )\n\n        if left_is_active:\n            return True\n        return False\n\n    def filterAcceptsRow(self, source_row, source_parent):\n        index = self.sourceModel().index(source_row, 0, source_parent)\n        project_name = index.data(PROJECT_NAME_ROLE)\n        if project_name is None:\n            return True\n\n        string_pattern = self.filterRegularExpression().pattern()\n        if string_pattern:\n            return string_pattern.lower() in project_name.lower()\n\n        # Current project keep always visible\n        default = super(ProjectSortFilterProxy, self).filterAcceptsRow(\n            source_row, source_parent\n        )\n        if not default:\n            return default\n\n        # Make sure current project is visible\n        if index.data(PROJECT_IS_CURRENT_ROLE):\n            return True\n\n        if (\n            self._filter_inactive\n            and not index.data(PROJECT_IS_ACTIVE_ROLE)\n        ):\n            return False\n\n        if (\n            self._filter_standard\n            and not index.data(PROJECT_IS_LIBRARY_ROLE)\n        ):\n            return False\n\n        if (\n            self._filter_library\n            and index.data(PROJECT_IS_LIBRARY_ROLE)\n        ):\n            return False\n        return True\n\n    def _custom_index_filter(self, index):\n        return bool(index.data(PROJECT_IS_ACTIVE_ROLE))\n\n    def is_active_filter_enabled(self):\n        return self._filter_inactive\n\n    def set_active_filter_enabled(self, enabled):\n        if self._filter_inactive == enabled:\n            return\n        self._filter_inactive = enabled\n        self.invalidateFilter()\n\n    def set_library_filter_enabled(self, enabled):\n        if self._filter_library == enabled:\n            return\n        self._filter_library = enabled\n        self.invalidateFilter()\n\n    def set_standard_filter_enabled(self, enabled):\n        if self._filter_standard == enabled:\n            return\n        self._filter_standard = enabled\n        self.invalidateFilter()\n\n    def set_sort_by_type(self, enabled):\n        if self._sort_by_type is enabled:\n            return\n        self._sort_by_type = enabled\n        self.invalidate()\n\n\nclass ProjectsCombobox(QtWidgets.QWidget):\n    refreshed = QtCore.Signal()\n    selection_changed = QtCore.Signal()\n\n    def __init__(self, controller, parent, handle_expected_selection=False):\n        super(ProjectsCombobox, self).__init__(parent)\n\n        projects_combobox = QtWidgets.QComboBox(self)\n        combobox_delegate = QtWidgets.QStyledItemDelegate(projects_combobox)\n        projects_combobox.setItemDelegate(combobox_delegate)\n        projects_model = ProjectsQtModel(controller)\n        projects_proxy_model = ProjectSortFilterProxy()\n        projects_proxy_model.setSourceModel(projects_model)\n        projects_combobox.setModel(projects_proxy_model)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(projects_combobox, 1)\n\n        projects_model.refreshed.connect(self._on_model_refresh)\n\n        controller.register_event_callback(\n            \"projects.refresh.finished\",\n            self._on_projects_refresh_finished\n        )\n        controller.register_event_callback(\n            \"controller.refresh.finished\",\n            self._on_controller_refresh\n        )\n        controller.register_event_callback(\n            \"expected_selection_changed\",\n            self._on_expected_selection_change\n        )\n\n        projects_combobox.currentIndexChanged.connect(\n            self._on_current_index_changed\n        )\n\n        self._controller = controller\n        self._listen_selection_change = True\n        self._select_item_visible = False\n\n        self._handle_expected_selection = handle_expected_selection\n        self._expected_selection = None\n\n        self._projects_combobox = projects_combobox\n        self._projects_model = projects_model\n        self._projects_proxy_model = projects_proxy_model\n        self._combobox_delegate = combobox_delegate\n\n    def refresh(self):\n        self._projects_model.refresh()\n\n    def set_selection(self, project_name):\n        \"\"\"Set selection to a given project.\n\n        Selection change is ignored if project is not found.\n\n        Args:\n            project_name (str): Name of project.\n\n        Returns:\n            bool: True if selection was changed, False otherwise. NOTE:\n                Selection may not be changed if project is not found, or if\n                project is already selected.\n        \"\"\"\n\n        idx = self._projects_combobox.findData(\n            project_name, PROJECT_NAME_ROLE)\n        if idx < 0:\n            return False\n        if idx != self._projects_combobox.currentIndex():\n            self._projects_combobox.setCurrentIndex(idx)\n            return True\n        return False\n\n    def set_listen_to_selection_change(self, listen):\n        \"\"\"Disable listening to changes of the selection.\n\n        Because combobox is triggering selection change when it's model\n        is refreshed, it's necessary to disable listening to selection for\n        some cases, e.g. when is on a different page of UI and should be just\n        refreshed.\n\n        Args:\n            listen (bool): Enable or disable listening to selection changes.\n        \"\"\"\n\n        self._listen_selection_change = listen\n\n    def get_selected_project_name(self):\n        \"\"\"Name of selected project.\n\n        Returns:\n            Union[str, None]: Name of selected project, or None if no project\n        \"\"\"\n\n        idx = self._projects_combobox.currentIndex()\n        if idx < 0:\n            return None\n        return self._projects_combobox.itemData(idx, PROJECT_NAME_ROLE)\n\n    def set_current_context_project(self, project_name):\n        self._projects_model.set_current_context_project(project_name)\n        self._projects_proxy_model.invalidateFilter()\n\n    def set_select_item_visible(self, visible):\n        self._select_item_visible = visible\n        self._projects_model.set_select_item_visible(visible)\n        self._update_select_item_visiblity()\n\n    def set_libraries_separator_visible(self, visible):\n        self._projects_model.set_libraries_separator_visible(visible)\n\n    def is_active_filter_enabled(self):\n        return self._projects_proxy_model.is_active_filter_enabled()\n\n    def set_active_filter_enabled(self, enabled):\n        return self._projects_proxy_model.set_active_filter_enabled(enabled)\n\n    def set_standard_filter_enabled(self, enabled):\n        return self._projects_proxy_model.set_standard_filter_enabled(enabled)\n\n    def set_library_filter_enabled(self, enabled):\n        return self._projects_proxy_model.set_library_filter_enabled(enabled)\n\n    def _update_select_item_visiblity(self, **kwargs):\n        if not self._select_item_visible:\n            return\n        if \"project_name\" not in kwargs:\n            project_name = self.get_selected_project_name()\n        else:\n            project_name = kwargs.get(\"project_name\")\n\n        # Hide the item if a project is selected\n        self._projects_model.set_selected_project(project_name)\n\n    def _on_current_index_changed(self, idx):\n        if not self._listen_selection_change:\n            return\n        project_name = self._projects_combobox.itemData(\n            idx, PROJECT_NAME_ROLE)\n        self._update_select_item_visiblity(project_name=project_name)\n        self._controller.set_selected_project(project_name)\n        self.selection_changed.emit()\n\n    def _on_model_refresh(self):\n        self._projects_proxy_model.sort(0)\n        self._projects_proxy_model.invalidateFilter()\n        if self._expected_selection:\n            self._set_expected_selection()\n        self._update_select_item_visiblity()\n        self.refreshed.emit()\n\n    def _on_projects_refresh_finished(self, event):\n        if event[\"sender\"] != PROJECTS_MODEL_SENDER:\n            self._projects_model.refresh()\n\n    def _on_controller_refresh(self):\n        self._update_expected_selection()\n\n    # Expected selection handling\n    def _on_expected_selection_change(self, event):\n        self._update_expected_selection(event.data)\n\n    def _set_expected_selection(self):\n        if not self._handle_expected_selection:\n            return\n        project_name = self._expected_selection\n        if project_name is not None:\n            if project_name != self.get_selected_project_name():\n                self.set_selection(project_name)\n            else:\n                # Fake project change\n                self._on_current_index_changed(\n                    self._projects_combobox.currentIndex()\n                )\n\n        self._controller.expected_project_selected(project_name)\n\n    def _update_expected_selection(self, expected_data=None):\n        if not self._handle_expected_selection:\n            return\n        if expected_data is None:\n            expected_data = self._controller.get_expected_selection_data()\n\n        project_data = expected_data.get(\"project\")\n        if (\n            not project_data\n            or not project_data[\"current\"]\n            or project_data[\"selected\"]\n        ):\n            return\n        self._expected_selection = project_data[\"name\"]\n        if not self._projects_model.is_refreshing:\n            self._set_expected_selection()\n\n\nclass ProjectsWidget(QtWidgets.QWidget):\n    # TODO implement\n    pass\n"
  },
  {
    "path": "openpype/tools/ayon_utils/widgets/tasks_widget.py",
    "content": "from qtpy import QtWidgets, QtGui, QtCore\n\nfrom openpype.style import get_disabled_entity_icon_color\nfrom openpype.tools.utils import DeselectableTreeView\n\nfrom .utils import RefreshThread, get_qt_icon\n\nTASKS_MODEL_SENDER_NAME = \"qt_tasks_model\"\nITEM_ID_ROLE = QtCore.Qt.UserRole + 1\nPARENT_ID_ROLE = QtCore.Qt.UserRole + 2\nITEM_NAME_ROLE = QtCore.Qt.UserRole + 3\nTASK_TYPE_ROLE = QtCore.Qt.UserRole + 4\n\n\nclass TasksQtModel(QtGui.QStandardItemModel):\n    \"\"\"Tasks model which cares about refresh of tasks by folder id.\n\n    Args:\n        controller (AbstractWorkfilesFrontend): The control object.\n    \"\"\"\n\n    refreshed = QtCore.Signal()\n\n    def __init__(self, controller):\n        super(TasksQtModel, self).__init__()\n\n        self._controller = controller\n\n        self._items_by_name = {}\n        self._has_content = False\n        self._is_refreshing = False\n\n        self._invalid_selection_item_used = False\n        self._invalid_selection_item = None\n        self._empty_tasks_item_used = False\n        self._empty_tasks_item = None\n\n        self._last_project_name = None\n        self._last_folder_id = None\n\n        self._refresh_threads = {}\n        self._current_refresh_thread = None\n\n        # Initial state\n        self._add_invalid_selection_item()\n\n    def _clear_items(self):\n        self._items_by_name = {}\n        self._has_content = False\n        self._remove_invalid_items()\n        root_item = self.invisibleRootItem()\n        root_item.removeRows(0, root_item.rowCount())\n\n    def refresh(self):\n        \"\"\"Refresh tasks for last project and folder.\"\"\"\n\n        self._refresh(self._last_project_name, self._last_folder_id)\n\n    def set_context(self, project_name, folder_id):\n        \"\"\"Set context for which should be tasks showed.\n\n        Args:\n            project_name (Union[str]): Name of project.\n            folder_id (Union[str, None]): Folder id.\n        \"\"\"\n\n        self._refresh(project_name, folder_id)\n\n    def get_index_by_name(self, task_name):\n        \"\"\"Find item by name and return its index.\n\n        Returns:\n            QtCore.QModelIndex: Index of item. Is invalid if task is not\n                found by name.\n        \"\"\"\n\n        item = self._items_by_name.get(task_name)\n        if item is None:\n            return QtCore.QModelIndex()\n        return self.indexFromItem(item)\n\n    def get_last_project_name(self):\n        \"\"\"Get last refreshed project name.\n\n        Returns:\n            Union[str, None]: Project name.\n        \"\"\"\n\n        return self._last_project_name\n\n    def get_last_folder_id(self):\n        \"\"\"Get last refreshed folder id.\n\n        Returns:\n            Union[str, None]: Folder id.\n        \"\"\"\n\n        return self._last_folder_id\n\n    def set_selected_project(self, project_name):\n        self._selected_project_name = project_name\n\n    def _get_invalid_selection_item(self):\n        if self._invalid_selection_item is None:\n            item = QtGui.QStandardItem(\"Select a folder\")\n            item.setFlags(QtCore.Qt.NoItemFlags)\n            icon = get_qt_icon({\n                \"type\": \"awesome-font\",\n                \"name\": \"fa.times\",\n                \"color\": get_disabled_entity_icon_color(),\n            })\n            item.setData(icon, QtCore.Qt.DecorationRole)\n            self._invalid_selection_item = item\n        return self._invalid_selection_item\n\n    def _get_empty_task_item(self):\n        if self._empty_tasks_item is None:\n            item = QtGui.QStandardItem(\"No task\")\n            icon = get_qt_icon({\n                \"type\": \"awesome-font\",\n                \"name\": \"fa.exclamation-circle\",\n                \"color\": get_disabled_entity_icon_color(),\n            })\n            item.setData(icon, QtCore.Qt.DecorationRole)\n            item.setFlags(QtCore.Qt.NoItemFlags)\n            self._empty_tasks_item = item\n        return self._empty_tasks_item\n\n    def _add_invalid_item(self, item):\n        self._clear_items()\n        root_item = self.invisibleRootItem()\n        root_item.appendRow(item)\n\n    def _remove_invalid_item(self, item):\n        root_item = self.invisibleRootItem()\n        root_item.takeRow(item.row())\n\n    def _remove_invalid_items(self):\n        self._remove_invalid_selection_item()\n        self._remove_empty_task_item()\n\n    def _add_invalid_selection_item(self):\n        if not self._invalid_selection_item_used:\n            self._add_invalid_item(self._get_invalid_selection_item())\n            self._invalid_selection_item_used = True\n\n    def _remove_invalid_selection_item(self):\n        if self._invalid_selection_item:\n            self._remove_invalid_item(self._get_invalid_selection_item())\n            self._invalid_selection_item_used = False\n\n    def _add_empty_task_item(self):\n        if not self._empty_tasks_item_used:\n            self._add_invalid_item(self._get_empty_task_item())\n            self._empty_tasks_item_used = True\n\n    def _remove_empty_task_item(self):\n        if self._empty_tasks_item_used:\n            self._remove_invalid_item(self._get_empty_task_item())\n            self._empty_tasks_item_used = False\n\n    def _refresh(self, project_name, folder_id):\n        self._is_refreshing = True\n        self._last_project_name = project_name\n        self._last_folder_id = folder_id\n        if not folder_id:\n            self._add_invalid_selection_item()\n            self._current_refresh_thread = None\n            self._is_refreshing = False\n            self.refreshed.emit()\n            return\n\n        thread = self._refresh_threads.get(folder_id)\n        if thread is not None:\n            self._current_refresh_thread = thread\n            return\n        thread = RefreshThread(\n            folder_id,\n            self._controller.get_task_items,\n            project_name,\n            folder_id\n        )\n        self._current_refresh_thread = thread\n        self._refresh_threads[thread.id] = thread\n        thread.refresh_finished.connect(self._on_refresh_thread)\n        thread.start()\n\n    def _fill_data_from_thread(self, thread):\n        task_items = thread.get_result()\n        # Task items are refreshed\n        if task_items is None:\n            return\n\n        # No tasks are available on folder\n        if not task_items:\n            self._add_empty_task_item()\n            return\n        self._remove_invalid_items()\n\n        new_items = []\n        new_names = set()\n        for task_item in task_items:\n            name = task_item.name\n            new_names.add(name)\n            item = self._items_by_name.get(name)\n            if item is None:\n                item = QtGui.QStandardItem()\n                item.setEditable(False)\n                new_items.append(item)\n                self._items_by_name[name] = item\n\n            # TODO cache locally\n            icon = get_qt_icon(task_item.icon)\n            item.setData(task_item.label, QtCore.Qt.DisplayRole)\n            item.setData(name, ITEM_NAME_ROLE)\n            item.setData(task_item.id, ITEM_ID_ROLE)\n            item.setData(task_item.parent_id, PARENT_ID_ROLE)\n            item.setData(icon, QtCore.Qt.DecorationRole)\n\n        root_item = self.invisibleRootItem()\n\n        for name in set(self._items_by_name) - new_names:\n            item = self._items_by_name.pop(name)\n            root_item.removeRow(item.row())\n\n        if new_items:\n            root_item.appendRows(new_items)\n\n    def _on_refresh_thread(self, thread_id):\n        \"\"\"Callback when refresh thread is finished.\n\n        Technically can be running multiple refresh threads at the same time,\n        to avoid using values from wrong thread, we check if thread id is\n        current refresh thread id.\n\n        Tasks are stored by name, so if a folder has same task name as\n        previously selected folder it keeps the selection.\n\n        Args:\n            thread_id (str): Thread id.\n        \"\"\"\n\n        # Make sure to remove thread from '_refresh_threads' dict\n        thread = self._refresh_threads.pop(thread_id)\n        if (\n            self._current_refresh_thread is None\n            or thread_id != self._current_refresh_thread.id\n        ):\n            return\n\n        self._fill_data_from_thread(thread)\n\n        root_item = self.invisibleRootItem()\n        self._has_content = root_item.rowCount() > 0\n        self._current_refresh_thread = None\n        self._is_refreshing = False\n        self.refreshed.emit()\n\n    @property\n    def is_refreshing(self):\n        \"\"\"Model is refreshing.\n\n        Returns:\n            bool: Model is refreshing\n        \"\"\"\n\n        return self._is_refreshing\n\n    @property\n    def has_content(self):\n        \"\"\"Model has content.\n\n        Returns:\n            bools: Have at least one task.\n        \"\"\"\n\n        return self._has_content\n\n    def headerData(self, section, orientation, role):\n        # Show nice labels in the header\n        if (\n            role == QtCore.Qt.DisplayRole\n            and orientation == QtCore.Qt.Horizontal\n        ):\n            if section == 0:\n                return \"Tasks\"\n\n        return super(TasksQtModel, self).headerData(\n            section, orientation, role\n        )\n\n\nclass TasksWidget(QtWidgets.QWidget):\n    \"\"\"Tasks widget.\n\n    Widget that handles tasks view, model and selection.\n\n    Args:\n        controller (AbstractWorkfilesFrontend): Workfiles controller.\n        parent (QtWidgets.QWidget): Parent widget.\n        handle_expected_selection (Optional[bool]): Handle expected selection.\n    \"\"\"\n\n    refreshed = QtCore.Signal()\n    selection_changed = QtCore.Signal()\n\n    def __init__(self, controller, parent, handle_expected_selection=False):\n        super(TasksWidget, self).__init__(parent)\n\n        tasks_view = DeselectableTreeView(self)\n        tasks_view.setIndentation(0)\n\n        tasks_model = TasksQtModel(controller)\n        tasks_proxy_model = QtCore.QSortFilterProxyModel()\n        tasks_proxy_model.setSourceModel(tasks_model)\n        tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        tasks_view.setModel(tasks_proxy_model)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(tasks_view, 1)\n\n        controller.register_event_callback(\n            \"tasks.refresh.finished\",\n            self._on_tasks_refresh_finished\n        )\n        controller.register_event_callback(\n            \"selection.folder.changed\",\n            self._folder_selection_changed\n        )\n        controller.register_event_callback(\n            \"expected_selection_changed\",\n            self._on_expected_selection_change\n        )\n\n        selection_model = tasks_view.selectionModel()\n        selection_model.selectionChanged.connect(self._on_selection_change)\n\n        tasks_model.refreshed.connect(self._on_tasks_model_refresh)\n\n        self._controller = controller\n        self._tasks_view = tasks_view\n        self._tasks_model = tasks_model\n        self._tasks_proxy_model = tasks_proxy_model\n\n        self._selected_folder_id = None\n\n        self._handle_expected_selection = handle_expected_selection\n        self._expected_selection_data = None\n\n    def refresh(self):\n        \"\"\"Refresh folders for last selected project.\n\n        Force to update folders model from controller. This may or may not\n        trigger query from server, that's based on controller's cache.\n        \"\"\"\n\n        self._tasks_model.refresh()\n\n    def _on_tasks_refresh_finished(self, event):\n        \"\"\"Tasks were refreshed in controller.\n\n        Ignore if refresh was triggered by tasks model, or refreshed folder is\n        not the same as currently selected folder.\n\n        Args:\n            event (Event): Event object.\n        \"\"\"\n\n        # Refresh only if current folder id is the same\n        if (\n            event[\"sender\"] == TASKS_MODEL_SENDER_NAME\n            or event[\"folder_id\"] != self._selected_folder_id\n        ):\n            return\n        self._tasks_model.set_context(\n            event[\"project_name\"], self._selected_folder_id\n        )\n\n    def _folder_selection_changed(self, event):\n        self._selected_folder_id = event[\"folder_id\"]\n        self._tasks_model.set_context(\n            event[\"project_name\"], self._selected_folder_id\n        )\n\n    def _on_tasks_model_refresh(self):\n        if not self._set_expected_selection():\n            self._on_selection_change()\n        self._tasks_proxy_model.sort(0)\n        self.refreshed.emit()\n\n    def _get_selected_item_ids(self):\n        selection_model = self._tasks_view.selectionModel()\n        for index in selection_model.selectedIndexes():\n            task_id = index.data(ITEM_ID_ROLE)\n            task_name = index.data(ITEM_NAME_ROLE)\n            parent_id = index.data(PARENT_ID_ROLE)\n            if task_name is not None:\n                return parent_id, task_id, task_name\n        return self._selected_folder_id, None, None\n\n    def _on_selection_change(self):\n        # Don't trigger task change during refresh\n        #   - a task was deselected if that happens\n        #   - can cause crash triggered during tasks refreshing\n        if self._tasks_model.is_refreshing:\n            return\n\n        parent_id, task_id, task_name = self._get_selected_item_ids()\n        self._controller.set_selected_task(task_id, task_name)\n        self.selection_changed.emit()\n\n    # Expected selection handling\n    def _on_expected_selection_change(self, event):\n        self._update_expected_selection(event.data)\n\n    def _set_expected_selection(self):\n        if not self._handle_expected_selection:\n            return False\n\n        if self._expected_selection_data is None:\n            return False\n        folder_id = self._expected_selection_data[\"folder_id\"]\n        task_name = self._expected_selection_data[\"task_name\"]\n        self._expected_selection_data = None\n        model_folder_id = self._tasks_model.get_last_folder_id()\n        if folder_id != model_folder_id:\n            return False\n        if task_name is not None:\n            index = self._tasks_model.get_index_by_name(task_name)\n            if index.isValid():\n                proxy_index = self._tasks_proxy_model.mapFromSource(index)\n                self._tasks_view.setCurrentIndex(proxy_index)\n        self._controller.expected_task_selected(folder_id, task_name)\n        return True\n\n    def _update_expected_selection(self, expected_data=None):\n        if not self._handle_expected_selection:\n            return\n        if expected_data is None:\n            expected_data = self._controller.get_expected_selection_data()\n        folder_data = expected_data.get(\"folder\")\n        task_data = expected_data.get(\"task\")\n        if (\n            not folder_data\n            or not task_data\n            or not task_data[\"current\"]\n        ):\n            return\n        folder_id = folder_data[\"id\"]\n        self._expected_selection_data = {\n            \"task_name\": task_data[\"name\"],\n            \"folder_id\": folder_id,\n        }\n        model_folder_id = self._tasks_model.get_last_folder_id()\n        if folder_id != model_folder_id or self._tasks_model.is_refreshing:\n            return\n        self._set_expected_selection()\n"
  },
  {
    "path": "openpype/tools/ayon_utils/widgets/utils.py",
    "content": "import os\nfrom functools import partial\n\nfrom qtpy import QtCore, QtGui\n\nfrom openpype.tools.utils.lib import get_qta_icon_by_name_and_color\n\n\nclass RefreshThread(QtCore.QThread):\n    refresh_finished = QtCore.Signal(str)\n\n    def __init__(self, thread_id, func, *args, **kwargs):\n        super(RefreshThread, self).__init__()\n        self._id = thread_id\n        self._callback = partial(func, *args, **kwargs)\n        self._exception = None\n        self._result = None\n        self.finished.connect(self._on_finish_callback)\n\n    @property\n    def id(self):\n        return self._id\n\n    @property\n    def failed(self):\n        return self._exception is not None\n\n    def run(self):\n        try:\n            self._result = self._callback()\n        except Exception as exc:\n            self._exception = exc\n\n    def get_result(self):\n        return self._result\n\n    def _on_finish_callback(self):\n        \"\"\"Trigger custom signal with thread id.\n\n        Listening for 'finished' signal we make sure that execution of thread\n            finished and QThread object can be safely deleted.\n        \"\"\"\n\n        self.refresh_finished.emit(self.id)\n\n\nclass _IconsCache:\n    \"\"\"Cache for icons.\"\"\"\n\n    _cache = {}\n    _default = None\n\n    @classmethod\n    def _get_cache_key(cls, icon_def):\n        parts = []\n        icon_type = icon_def[\"type\"]\n        if icon_type == \"path\":\n            parts = [icon_type, icon_def[\"path\"]]\n\n        elif icon_type == \"awesome-font\":\n            parts = [icon_type, icon_def[\"name\"], icon_def[\"color\"]]\n        return \"|\".join(parts)\n\n    @classmethod\n    def get_icon(cls, icon_def):\n        if not icon_def:\n            return None\n        icon_type = icon_def[\"type\"]\n        cache_key = cls._get_cache_key(icon_def)\n        cache = cls._cache.get(cache_key)\n        if cache is not None:\n            return cache\n\n        icon = None\n        if icon_type == \"path\":\n            path = icon_def[\"path\"]\n            if os.path.exists(path):\n                icon = QtGui.QIcon(path)\n\n        elif icon_type == \"awesome-font\":\n            icon_name = icon_def[\"name\"]\n            icon_color = icon_def[\"color\"]\n            icon = get_qta_icon_by_name_and_color(icon_name, icon_color)\n            if icon is None:\n                icon = get_qta_icon_by_name_and_color(\n                    \"fa.{}\".format(icon_name), icon_color)\n        if icon is None:\n            icon = cls.get_default()\n        cls._cache[cache_key] = icon\n        return icon\n\n    @classmethod\n    def get_default(cls):\n        pix = QtGui.QPixmap(1, 1)\n        pix.fill(QtCore.Qt.transparent)\n        return QtGui.QIcon(pix)\n\n\ndef get_qt_icon(icon_def):\n    \"\"\"Returns icon from cache or creates new one.\n\n    Args:\n        icon_def (dict[str, Any]): Icon definition.\n\n    Returns:\n        QtGui.QIcon: Icon.\n    \"\"\"\n\n    return _IconsCache.get_icon(icon_def)\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/tools/ayon_workfiles/abstract.py",
    "content": "import os\nfrom abc import ABCMeta, abstractmethod\n\nimport six\nfrom openpype.style import get_default_entity_icon_color\n\n\nclass WorkfileInfo:\n    \"\"\"Information about workarea file with possible additional from database.\n\n    Args:\n        folder_id (str): Folder id.\n        task_id (str): Task id.\n        filepath (str): Filepath.\n        filesize (int): File size.\n        creation_time (int): Creation time (timestamp).\n        modification_time (int): Modification time (timestamp).\n        note (str): Note.\n    \"\"\"\n\n    def __init__(\n        self,\n        folder_id,\n        task_id,\n        filepath,\n        filesize,\n        creation_time,\n        modification_time,\n        note,\n    ):\n        self.folder_id = folder_id\n        self.task_id = task_id\n        self.filepath = filepath\n        self.filesize = filesize\n        self.creation_time = creation_time\n        self.modification_time = modification_time\n        self.note = note\n\n    def to_data(self):\n        \"\"\"Converts WorkfileInfo item to data.\n\n        Returns:\n            dict[str, Any]: Folder item data.\n        \"\"\"\n\n        return {\n            \"folder_id\": self.folder_id,\n            \"task_id\": self.task_id,\n            \"filepath\": self.filepath,\n            \"filesize\": self.filesize,\n            \"creation_time\": self.creation_time,\n            \"modification_time\": self.modification_time,\n            \"note\": self.note,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        \"\"\"Re-creates WorkfileInfo item from data.\n\n        Args:\n            data (dict[str, Any]): Workfile info item data.\n\n        Returns:\n            WorkfileInfo: Workfile info item.\n        \"\"\"\n\n        return cls(**data)\n\n\nclass FolderItem:\n    \"\"\"Item representing folder entity on a server.\n\n    Folder can be a child of another folder or a project.\n\n    Args:\n        entity_id (str): Folder id.\n        parent_id (Union[str, None]): Parent folder id. If 'None' then project\n            is parent.\n        name (str): Name of folder.\n        label (str): Folder label.\n        icon_name (str): Name of icon from font awesome.\n        icon_color (str): Hex color string that will be used for icon.\n    \"\"\"\n\n    def __init__(\n        self, entity_id, parent_id, name, label, icon_name, icon_color\n    ):\n        self.entity_id = entity_id\n        self.parent_id = parent_id\n        self.name = name\n        self.icon_name = icon_name or \"fa.folder\"\n        self.icon_color = icon_color or get_default_entity_icon_color()\n        self.label = label or name\n\n    def to_data(self):\n        \"\"\"Converts folder item to data.\n\n        Returns:\n            dict[str, Any]: Folder item data.\n        \"\"\"\n\n        return {\n            \"entity_id\": self.entity_id,\n            \"parent_id\": self.parent_id,\n            \"name\": self.name,\n            \"label\": self.label,\n            \"icon_name\": self.icon_name,\n            \"icon_color\": self.icon_color,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        \"\"\"Re-creates folder item from data.\n\n        Args:\n            data (dict[str, Any]): Folder item data.\n\n        Returns:\n            FolderItem: Folder item.\n        \"\"\"\n\n        return cls(**data)\n\n\nclass TaskItem:\n    \"\"\"Task item representing task entity on a server.\n\n    Task is child of a folder.\n\n    Task item has label that is used for display in UI. The label is by\n        default using task name and type.\n\n    Args:\n        task_id (str): Task id.\n        name (str): Name of task.\n        task_type (str): Type of task.\n        parent_id (str): Parent folder id.\n        icon_name (str): Name of icon from font awesome.\n        icon_color (str): Hex color string that will be used for icon.\n    \"\"\"\n\n    def __init__(\n        self, task_id, name, task_type, parent_id, icon_name, icon_color\n    ):\n        self.task_id = task_id\n        self.name = name\n        self.task_type = task_type\n        self.parent_id = parent_id\n        self.icon_name = icon_name or \"fa.male\"\n        self.icon_color = icon_color or get_default_entity_icon_color()\n        self._label = None\n\n    @property\n    def id(self):\n        \"\"\"Alias for task_id.\n\n        Returns:\n            str: Task id.\n        \"\"\"\n\n        return self.task_id\n\n    @property\n    def label(self):\n        \"\"\"Label of task item for UI.\n\n        Returns:\n            str: Label of task item.\n        \"\"\"\n\n        if self._label is None:\n            self._label = \"{} ({})\".format(self.name, self.task_type)\n        return self._label\n\n    def to_data(self):\n        \"\"\"Converts task item to data.\n\n        Returns:\n            dict[str, Any]: Task item data.\n        \"\"\"\n\n        return {\n            \"task_id\": self.task_id,\n            \"name\": self.name,\n            \"parent_id\": self.parent_id,\n            \"task_type\": self.task_type,\n            \"icon_name\": self.icon_name,\n            \"icon_color\": self.icon_color,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        \"\"\"Re-create task item from data.\n\n        Args:\n            data (dict[str, Any]): Task item data.\n\n        Returns:\n            TaskItem: Task item.\n        \"\"\"\n\n        return cls(**data)\n\n\nclass FileItem:\n    \"\"\"File item that represents a file.\n\n    Can be used for both Workarea and Published workfile. Workarea file\n    will always exist on disk which is not the case for Published workfile.\n\n    Args:\n        dirpath (str): Directory path of file.\n        filename (str): Filename.\n        modified (float): Modified timestamp.\n        representation_id (Optional[str]): Representation id of published\n            workfile.\n        filepath (Optional[str]): Prepared filepath.\n        exists (Optional[bool]): If file exists on disk.\n    \"\"\"\n\n    def __init__(\n        self,\n        dirpath,\n        filename,\n        modified,\n        representation_id=None,\n        filepath=None,\n        exists=None\n    ):\n        self.filename = filename\n        self.dirpath = dirpath\n        self.modified = modified\n        self.representation_id = representation_id\n        self._filepath = filepath\n        self._exists = exists\n\n    @property\n    def filepath(self):\n        \"\"\"Filepath of file.\n\n        Returns:\n            str: Full path to a file.\n        \"\"\"\n\n        if self._filepath is None:\n            self._filepath = os.path.join(self.dirpath, self.filename)\n        return self._filepath\n\n    @property\n    def exists(self):\n        \"\"\"File is available.\n\n        Returns:\n            bool: If file exists on disk.\n        \"\"\"\n\n        if self._exists is None:\n            self._exists = os.path.exists(self.filepath)\n        return self._exists\n\n    def to_data(self):\n        \"\"\"Converts file item to data.\n\n        Returns:\n            dict[str, Any]: File item data.\n        \"\"\"\n\n        return {\n            \"filename\": self.filename,\n            \"dirpath\": self.dirpath,\n            \"modified\": self.modified,\n            \"representation_id\": self.representation_id,\n            \"filepath\": self.filepath,\n            \"exists\": self.exists,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        \"\"\"Re-creates file item from data.\n\n        Args:\n            data (dict[str, Any]): File item data.\n\n        Returns:\n            FileItem: File item.\n        \"\"\"\n\n        required_keys = {\n            \"filename\",\n            \"dirpath\",\n            \"modified\",\n            \"representation_id\"\n        }\n        missing_keys = required_keys - set(data.keys())\n        if missing_keys:\n            raise KeyError(\"Missing keys: {}\".format(missing_keys))\n\n        return cls(**{\n            key: data[key]\n            for key in required_keys\n        })\n\n\nclass WorkareaFilepathResult:\n    \"\"\"Result of workarea file formatting.\n\n    Args:\n        root (str): Root path of workarea.\n        filename (str): Filename.\n        exists (bool): True if file exists.\n        filepath (str): Filepath. If not provided it will be constructed\n            from root and filename.\n    \"\"\"\n\n    def __init__(self, root, filename, exists, filepath=None):\n        if not filepath and root and filename:\n            filepath = os.path.join(root, filename)\n        self.root = root\n        self.filename = filename\n        self.exists = exists\n        self.filepath = filepath\n\n\n@six.add_metaclass(ABCMeta)\nclass AbstractWorkfilesCommon(object):\n    @abstractmethod\n    def is_host_valid(self):\n        \"\"\"Host is valid for workfiles tool work.\n\n        Returns:\n            bool: True if host is valid.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_workfile_extensions(self):\n        \"\"\"Get possible workfile extensions.\n\n        Defined by host implementation.\n\n        Returns:\n            Iterable[str]: List of extensions.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def is_save_enabled(self):\n        \"\"\"Is workfile save enabled.\n\n        Returns:\n            bool: True if save is enabled.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_save_enabled(self, enabled):\n        \"\"\"Enable or disabled workfile save.\n\n        Args:\n            enabled (bool): Enable save workfile when True.\n        \"\"\"\n\n        pass\n\n\nclass AbstractWorkfilesBackend(AbstractWorkfilesCommon):\n    # Current context\n    @abstractmethod\n    def get_host_name(self):\n        \"\"\"Name of host.\n\n        Returns:\n            str: Name of host.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_current_project_name(self):\n        \"\"\"Project name from current context of host.\n\n        Returns:\n            str: Name of project.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_current_folder_id(self):\n        \"\"\"Folder id from current context of host.\n\n        Returns:\n            Union[str, None]: Folder id or None if host does not have\n                any context.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_current_task_name(self):\n        \"\"\"Task name from current context of host.\n\n        Returns:\n            Union[str, None]: Task name or None if host does not have\n                any context.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_current_workfile(self):\n        \"\"\"Current workfile from current context of host.\n\n        Returns:\n            Union[str, None]: Path to workfile or None if host does\n                not have opened specific file.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def project_anatomy(self):\n        \"\"\"Project anatomy for current project.\n\n        Returns:\n            Anatomy: Project anatomy.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def project_settings(self):\n        \"\"\"Project settings for current project.\n\n        Returns:\n            dict[str, Any]: Project settings.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_project_entity(self, project_name):\n        \"\"\"Get project entity by name.\n\n        Args:\n            project_name (str): Project name.\n\n        Returns:\n            dict[str, Any]: Project entity data.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_folder_entity(self, project_name, folder_id):\n        \"\"\"Get folder entity by id.\n\n        Args:\n            project_name (str): Project name.\n            folder_id (str): Folder id.\n\n        Returns:\n            dict[str, Any]: Folder entity data.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_task_entity(self, project_name, task_id):\n        \"\"\"Get task entity by id.\n\n        Args:\n            project_name (str): Project name.\n            task_id (str): Task id.\n\n        Returns:\n            dict[str, Any]: Task entity data.\n        \"\"\"\n\n        pass\n\n    def emit_event(self, topic, data=None, source=None):\n        \"\"\"Emit event.\n\n        Args:\n            topic (str): Event topic used for callbacks filtering.\n            data (Optional[dict[str, Any]]): Event data.\n            source (Optional[str]): Event source.\n        \"\"\"\n\n        pass\n\n\nclass AbstractWorkfilesFrontend(AbstractWorkfilesCommon):\n    \"\"\"UI controller abstraction that is used for workfiles tool frontend.\n\n    Abstraction to provide data for UI and to handle UI events.\n\n    Provide access to abstract backend data, like folders and tasks. Cares\n    about handling of selection, keep information about current UI selection\n    and have ability to tell what selection should UI show.\n\n    Selection is separated into 2 parts, first is what UI elements tell\n    about selection, and second is what UI should show as selected.\n    \"\"\"\n\n    @abstractmethod\n    def register_event_callback(self, topic, callback):\n        \"\"\"Register event callback.\n\n        Listen for events with given topic.\n\n        Args:\n            topic (str): Name of topic.\n            callback (Callable): Callback that will be called when event\n                is triggered.\n        \"\"\"\n\n        pass\n\n    # Host information\n    @abstractmethod\n    def get_workfile_extensions(self):\n        \"\"\"Each host can define extensions that can be used for workfile.\n\n        Returns:\n            List[str]: File extensions that can be used as workfile for\n                current host.\n        \"\"\"\n\n        pass\n\n    # Selection information\n    @abstractmethod\n    def get_selected_folder_id(self):\n        \"\"\"Currently selected folder id.\n\n        Returns:\n            Union[str, None]: Folder id or None if no folder is selected.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_selected_folder(self, folder_id):\n        \"\"\"Change selected folder.\n\n        This deselects currently selected task.\n\n        Args:\n            folder_id (Union[str, None]): Folder id or None if no folder\n                is selected.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_selected_task_id(self):\n        \"\"\"Currently selected task id.\n\n        Returns:\n            Union[str, None]: Task id or None if no folder is selected.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_selected_task_name(self):\n        \"\"\"Currently selected task name.\n\n        Returns:\n            Union[str, None]: Task name or None if no folder is selected.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_selected_task(self, task_id, task_name):\n        \"\"\"Change selected task.\n\n        Args:\n            task_id (Union[str, None]): Task id or None if no task\n                is selected.\n            task_name (Union[str, None]): Task name or None if no task\n                is selected.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_selected_workfile_path(self):\n        \"\"\"Currently selected workarea workile.\n\n        Returns:\n            Union[str, None]: Selected workfile path.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_selected_workfile_path(self, path):\n        \"\"\"Change selected workfile path.\n\n        Args:\n            path (Union[str, None]): Selected workfile path.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_selected_representation_id(self):\n        \"\"\"Currently selected workfile representation id.\n\n        Returns:\n            Union[str, None]: Representation id or None if no representation\n                is selected.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_selected_representation_id(self, representation_id):\n        \"\"\"Change selected representation.\n\n        Args:\n            representation_id (Union[str, None]): Selected workfile\n                representation id.\n        \"\"\"\n\n        pass\n\n    def get_selected_context(self):\n        \"\"\"Obtain selected context.\n\n        Returns:\n            dict[str, Union[str, None]]: Selected context.\n        \"\"\"\n\n        return {\n            \"folder_id\": self.get_selected_folder_id(),\n            \"task_id\": self.get_selected_task_id(),\n            \"task_name\": self.get_selected_task_name(),\n            \"workfile_path\": self.get_selected_workfile_path(),\n            \"representation_id\": self.get_selected_representation_id(),\n        }\n\n    # Expected selection\n    # - expected selection is used to restore selection after refresh\n    #   or when current context should be used\n    @abstractmethod\n    def set_expected_selection(\n        self,\n        folder_id,\n        task_name,\n        workfile_name=None,\n        representation_id=None\n    ):\n        \"\"\"Define what should be selected in UI.\n\n        Expected selection provide a way to define/change selection of\n        sequential UI elements. For example, if folder and task should be\n        selected a task element should wait until folder element has selected\n        folder.\n\n        Triggers 'expected_selection.changed' event.\n\n        Args:\n            folder_id (str): Folder id.\n            task_name (str): Task name.\n            workfile_name (Optional[str]): Workfile name. Used for workarea\n                files UI element.\n            representation_id (Optional[str]): Representation id. Used for\n                published filed UI element.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_expected_selection_data(self):\n        \"\"\"Data of expected selection.\n\n        TODOs:\n            Return defined object instead of dict.\n\n        Returns:\n            dict[str, Any]: Expected selection data.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def expected_folder_selected(self, folder_id):\n        \"\"\"Expected folder was selected in UI.\n\n        Args:\n            folder_id (str): Folder id which was selected.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def expected_task_selected(self, folder_id, task_name):\n        \"\"\"Expected task was selected in UI.\n\n        Args:\n            folder_id (str): Folder id under which task is.\n            task_name (str): Task name which was selected.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def expected_representation_selected(\n        self, folder_id, task_name, representation_id\n    ):\n        \"\"\"Expected representation was selected in UI.\n\n        Args:\n            folder_id (str): Folder id under which representation is.\n            task_name (str): Task name under which representation is.\n            representation_id (str): Representation id which was selected.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def expected_workfile_selected(self, folder_id, task_name, workfile_name):\n        \"\"\"Expected workfile was selected in UI.\n\n        Args:\n            folder_id (str): Folder id under which workfile is.\n            task_name (str): Task name under which workfile is.\n            workfile_name (str): Workfile filename which was selected.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def go_to_current_context(self):\n        \"\"\"Set expected selection to current context.\"\"\"\n\n        pass\n\n    # Model functions\n    @abstractmethod\n    def get_folder_items(self, project_name, sender):\n        \"\"\"Folder items to visualize project hierarchy.\n\n        This function may trigger events 'folders.refresh.started' and\n        'folders.refresh.finished' which will contain 'sender' value in data.\n        That may help to avoid re-refresh of folder items in UI elements.\n\n        Args:\n            project_name (str): Project name for which are folders requested.\n            sender (str): Who requested folder items.\n\n        Returns:\n            list[FolderItem]: Minimum possible information needed\n                for visualisation of folder hierarchy.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_task_items(self, project_name, folder_id, sender):\n        \"\"\"Task items.\n\n        This function may trigger events 'tasks.refresh.started' and\n        'tasks.refresh.finished' which will contain 'sender' value in data.\n        That may help to avoid re-refresh of task items in UI elements.\n\n        Args:\n            project_name (str): Project name for which are tasks requested.\n            folder_id (str): Folder ID for which are tasks requested.\n            sender (str): Who requested folder items.\n\n        Returns:\n            list[TaskItem]: Minimum possible information needed\n                for visualisation of tasks.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def has_unsaved_changes(self):\n        \"\"\"Has host unsaved change in currently running session.\n\n        Returns:\n            bool: Has unsaved changes.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_workarea_dir_by_context(self, folder_id, task_id):\n        \"\"\"Get workarea directory by context.\n\n        Args:\n            folder_id (str): Folder id.\n            task_id (str): Task id.\n\n        Returns:\n            str: Workarea directory.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_workarea_file_items(self, folder_id, task_id):\n        \"\"\"Get workarea file items.\n\n        Args:\n            folder_id (str): Folder id.\n            task_id (str): Task id.\n\n        Returns:\n            list[FileItem]: List of workarea file items.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_workarea_save_as_data(self, folder_id, task_id):\n        \"\"\"Prepare data for Save As operation.\n\n        Todos:\n            Return defined object instead of dict.\n\n        Args:\n            folder_id (str): Folder id.\n            task_id (str): Task id.\n\n        Returns:\n            dict[str, Any]: Data for Save As operation.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def fill_workarea_filepath(\n        self,\n        folder_id,\n        task_id,\n        extension,\n        use_last_version,\n        version,\n        comment,\n    ):\n        \"\"\"Calculate workfile path for passed context.\n\n        Args:\n            folder_id (str): Folder id.\n            task_id (str): Task id.\n            extension (str): File extension.\n            use_last_version (bool): Use last version.\n            version (int): Version used if 'use_last_version' if 'False'.\n            comment (str): User's comment (subversion).\n\n        Returns:\n            WorkareaFilepathResult: Result of the operation.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_published_file_items(self, folder_id, task_id):\n        \"\"\"Get published file items.\n\n        Args:\n            folder_id (str): Folder id.\n            task_id (Union[str, None]): Task id.\n\n        Returns:\n            list[FileItem]: List of published file items.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_workfile_info(self, folder_id, task_id, filepath):\n        \"\"\"Workfile info from database.\n\n        Args:\n            folder_id (str): Folder id.\n            task_id (str): Task id.\n            filepath (str): Workfile path.\n\n        Returns:\n            Union[WorkfileInfo, None]: Workfile info or None if was passed\n                invalid context.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def save_workfile_info(self, folder_id, task_id, filepath, note):\n        \"\"\"Save workfile info to database.\n\n        At this moment the only information which can be saved about\n            workfile is 'note'.\n\n        When 'note' is 'None' it is only validated if workfile info exists,\n            and if not then creates one with empty note.\n\n        Args:\n            folder_id (str): Folder id.\n            task_id (str): Task id.\n            filepath (str): Workfile path.\n            note (Union[str, None]): Note.\n        \"\"\"\n\n        pass\n\n    # General commands\n    @abstractmethod\n    def reset(self):\n        \"\"\"Reset everything, models, ui etc.\n\n        Triggers 'controller.reset.started' event at the beginning and\n        'controller.reset.finished' at the end.\n        \"\"\"\n\n        pass\n\n    # Controller actions\n    @abstractmethod\n    def open_workfile(self, folder_id, task_id, filepath):\n        \"\"\"Open a workfile for context.\n\n        Args:\n            folder_id (str): Folder id.\n            task_id (str): Task id.\n            filepath (str): Workfile path.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def save_current_workfile(self):\n        \"\"\"Save state of current workfile.\"\"\"\n\n        pass\n\n    @abstractmethod\n    def save_as_workfile(\n        self,\n        folder_id,\n        task_id,\n        workdir,\n        filename,\n        template_key,\n    ):\n        \"\"\"Save current state of workfile to workarea.\n\n        Args:\n            folder_id (str): Folder id.\n            task_id (str): Task id.\n            workdir (str): Workarea directory.\n            filename (str): Workarea filename.\n            template_key (str): Template key used to get the workdir\n                and filename.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def copy_workfile_representation(\n        self,\n        representation_id,\n        representation_filepath,\n        folder_id,\n        task_id,\n        workdir,\n        filename,\n        template_key,\n    ):\n        \"\"\"Action to copy published workfile representation to workarea.\n\n        Triggers 'copy_representation.started' event on start and\n        'copy_representation.finished' event with '{\"failed\": bool}'.\n\n        Args:\n            representation_id (str): Representation id.\n            representation_filepath (str): Path to representation file.\n            folder_id (str): Folder id.\n            task_id (str): Task id.\n            workdir (str): Workarea directory.\n            filename (str): Workarea filename.\n            template_key (str): Template key.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def duplicate_workfile(self, src_filepath, workdir, filename):\n        \"\"\"Duplicate workfile.\n\n        Workfiles is not opened when done.\n\n        Args:\n            src_filepath (str): Source workfile path.\n            workdir (str): Destination workdir.\n            filename (str): Destination filename.\n        \"\"\"\n\n        pass\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/control.py",
    "content": "import os\nimport shutil\n\nimport ayon_api\n\nfrom openpype.client import get_asset_by_id\nfrom openpype.host import IWorkfileHost\nfrom openpype.lib import Logger, emit_event\nfrom openpype.lib.events import QueuedEventSystem\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import Anatomy, registered_host\nfrom openpype.pipeline.context_tools import (\n    change_current_context,\n    get_current_host_name,\n    get_global_context,\n)\nfrom openpype.pipeline.workfile import create_workdir_extra_folders\n\nfrom openpype.tools.ayon_utils.models import (\n    HierarchyModel,\n    HierarchyExpectedSelection,\n    ProjectsModel,\n)\n\nfrom .abstract import (\n    AbstractWorkfilesFrontend,\n    AbstractWorkfilesBackend,\n)\nfrom .models import SelectionModel, WorkfilesModel\n\n\nclass WorkfilesToolExpectedSelection(HierarchyExpectedSelection):\n    def __init__(self, controller):\n        super(WorkfilesToolExpectedSelection, self).__init__(\n            controller,\n            handle_project=False,\n            handle_folder=True,\n            handle_task=True,\n        )\n\n        self._workfile_name = None\n        self._representation_id = None\n\n        self._workfile_selected = True\n        self._representation_selected = True\n\n    def set_expected_selection(\n        self,\n        project_name=None,\n        folder_id=None,\n        task_name=None,\n        workfile_name=None,\n        representation_id=None,\n    ):\n        self._workfile_name = workfile_name\n        self._representation_id = representation_id\n\n        self._workfile_selected = False\n        self._representation_selected = False\n\n        super(WorkfilesToolExpectedSelection, self).set_expected_selection(\n            project_name,\n            folder_id,\n            task_name,\n        )\n\n    def get_expected_selection_data(self):\n        data = super(\n            WorkfilesToolExpectedSelection, self\n        ).get_expected_selection_data()\n\n        _is_current = (\n            self._project_selected\n            and self._folder_selected\n            and self._task_selected\n        )\n        workfile_is_current = False\n        repre_is_current = False\n        if _is_current:\n            workfile_is_current = not self._workfile_selected\n            repre_is_current = not self._representation_selected\n\n        data[\"workfile\"] = {\n            \"name\": self._workfile_name,\n            \"current\": workfile_is_current,\n            \"selected\": self._workfile_selected,\n        }\n        data[\"representation\"] = {\n            \"id\": self._representation_id,\n            \"current\": repre_is_current,\n            \"selected\": self._workfile_selected,\n        }\n        return data\n\n    def is_expected_workfile_selected(self, workfile_name):\n        return (\n            workfile_name == self._workfile_name\n            and self._workfile_selected\n        )\n\n    def is_expected_representation_selected(self, representation_id):\n        return (\n            representation_id == self._representation_id\n            and self._representation_selected\n        )\n\n    def expected_workfile_selected(self, folder_id, task_name, workfile_name):\n        if folder_id != self._folder_id:\n            return False\n\n        if task_name != self._task_name:\n            return False\n\n        if workfile_name != self._workfile_name:\n            return False\n        self._workfile_selected = True\n        self._emit_change()\n        return True\n\n    def expected_representation_selected(\n        self, folder_id, task_name, representation_id\n    ):\n        if folder_id != self._folder_id:\n            return False\n\n        if task_name != self._task_name:\n            return False\n\n        if representation_id != self._representation_id:\n            return False\n        self._representation_selected = True\n        self._emit_change()\n        return True\n\n\nclass BaseWorkfileController(\n    AbstractWorkfilesFrontend, AbstractWorkfilesBackend\n):\n    def __init__(self, host=None):\n        if host is None:\n            host = registered_host()\n\n        host_is_valid = False\n        if host is not None:\n            missing_methods = (\n                IWorkfileHost.get_missing_workfile_methods(host)\n            )\n            host_is_valid = len(missing_methods) == 0\n\n        self._host = host\n        self._host_is_valid = host_is_valid\n\n        self._project_anatomy = None\n        self._project_settings = None\n        self._event_system = None\n        self._log = None\n\n        self._current_project_name = None\n        self._current_folder_name = None\n        self._current_folder_id = None\n        self._current_task_name = None\n        self._save_is_enabled = True\n\n        # Expected selected folder and task\n        self._expected_selection = self._create_expected_selection_obj()\n        self._selection_model = self._create_selection_model()\n        self._projects_model = self._create_projects_model()\n        self._hierarchy_model = self._create_hierarchy_model()\n        self._workfiles_model = self._create_workfiles_model()\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(\"WorkfilesUI\")\n        return self._log\n\n    def is_host_valid(self):\n        return self._host_is_valid\n\n    def _create_expected_selection_obj(self):\n        return WorkfilesToolExpectedSelection(self)\n\n    def _create_projects_model(self):\n        return ProjectsModel(self)\n\n    def _create_selection_model(self):\n        return SelectionModel(self)\n\n    def _create_hierarchy_model(self):\n        return HierarchyModel(self)\n\n    def _create_workfiles_model(self):\n        return WorkfilesModel(self)\n\n    @property\n    def event_system(self):\n        \"\"\"Inner event system for workfiles tool controller.\n\n        Is used for communication with UI. Event system is created on demand.\n\n        Returns:\n            QueuedEventSystem: Event system which can trigger callbacks\n                for topics.\n        \"\"\"\n\n        if self._event_system is None:\n            self._event_system = QueuedEventSystem()\n        return self._event_system\n\n    # ----------------------------------------------------\n    # Implementation of methods required for backend logic\n    # ----------------------------------------------------\n    @property\n    def project_settings(self):\n        if self._project_settings is None:\n            self._project_settings = get_project_settings(\n                self.get_current_project_name())\n        return self._project_settings\n\n    @property\n    def project_anatomy(self):\n        if self._project_anatomy is None:\n            self._project_anatomy = Anatomy(self.get_current_project_name())\n        return self._project_anatomy\n\n    def get_project_entity(self, project_name):\n        return self._projects_model.get_project_entity(\n            project_name)\n\n    def get_folder_entity(self, project_name, folder_id):\n        return self._hierarchy_model.get_folder_entity(\n            project_name, folder_id)\n\n    def get_task_entity(self, project_name, task_id):\n        return self._hierarchy_model.get_task_entity(\n            project_name, task_id)\n\n    # ---------------------------------\n    # Implementation of abstract methods\n    # ---------------------------------\n    def emit_event(self, topic, data=None, source=None):\n        \"\"\"Use implemented event system to trigger event.\"\"\"\n\n        if data is None:\n            data = {}\n        self.event_system.emit(topic, data, source)\n\n    def register_event_callback(self, topic, callback):\n        self.event_system.add_callback(topic, callback)\n\n    def is_save_enabled(self):\n        \"\"\"Is workfile save enabled.\n\n        Returns:\n            bool: True if save is enabled.\n        \"\"\"\n\n        return self._save_is_enabled\n\n    def set_save_enabled(self, enabled):\n        \"\"\"Enable or disabled workfile save.\n\n        Args:\n            enabled (bool): Enable save workfile when True.\n        \"\"\"\n\n        if self._save_is_enabled == enabled:\n            return\n\n        self._save_is_enabled = enabled\n        self._emit_event(\n            \"workfile_save_enable.changed\",\n            {\"enabled\": enabled}\n        )\n\n    # Host information\n    def get_workfile_extensions(self):\n        host = self._host\n        if isinstance(host, IWorkfileHost):\n            return host.get_workfile_extensions()\n        return host.file_extensions()\n\n    def has_unsaved_changes(self):\n        host = self._host\n        if isinstance(host, IWorkfileHost):\n            return host.workfile_has_unsaved_changes()\n        return host.has_unsaved_changes()\n\n    # Current context\n    def get_host_name(self):\n        host = self._host\n        if isinstance(host, IWorkfileHost):\n            return host.name\n        return get_current_host_name()\n\n    def _get_host_current_context(self):\n        if hasattr(self._host, \"get_current_context\"):\n            return self._host.get_current_context()\n        return get_global_context()\n\n    def get_current_project_name(self):\n        return self._current_project_name\n\n    def get_current_folder_id(self):\n        return self._current_folder_id\n\n    def get_current_task_name(self):\n        return self._current_task_name\n\n    def get_current_workfile(self):\n        host = self._host\n        if isinstance(host, IWorkfileHost):\n            return host.get_current_workfile()\n        return host.current_file()\n\n    # Selection information\n    def get_selected_folder_id(self):\n        return self._selection_model.get_selected_folder_id()\n\n    def set_selected_folder(self, folder_id):\n        self._selection_model.set_selected_folder(folder_id)\n\n    def get_selected_task_id(self):\n        return self._selection_model.get_selected_task_id()\n\n    def get_selected_task_name(self):\n        return self._selection_model.get_selected_task_name()\n\n    def set_selected_task(self, task_id, task_name):\n        return self._selection_model.set_selected_task(task_id, task_name)\n\n    def get_selected_workfile_path(self):\n        return self._selection_model.get_selected_workfile_path()\n\n    def set_selected_workfile_path(self, path):\n        self._selection_model.set_selected_workfile_path(path)\n\n    def get_selected_representation_id(self):\n        return self._selection_model.get_selected_representation_id()\n\n    def set_selected_representation_id(self, representation_id):\n        self._selection_model.set_selected_representation_id(\n            representation_id)\n\n    def set_expected_selection(\n        self,\n        folder_id,\n        task_name,\n        workfile_name=None,\n        representation_id=None\n    ):\n        self._expected_selection.set_expected_selection(\n            self.get_current_project_name(),\n            folder_id,\n            task_name,\n            workfile_name,\n            representation_id\n        )\n        self._trigger_expected_selection_changed()\n\n    def expected_folder_selected(self, folder_id):\n        if self._expected_selection.expected_folder_selected(folder_id):\n            self._trigger_expected_selection_changed()\n\n    def expected_task_selected(self, folder_id, task_name):\n        if self._expected_selection.expected_task_selected(\n            folder_id, task_name\n        ):\n            self._trigger_expected_selection_changed()\n\n    def expected_workfile_selected(self, folder_id, task_name, workfile_name):\n        if self._expected_selection.expected_workfile_selected(\n            folder_id, task_name, workfile_name\n        ):\n            self._trigger_expected_selection_changed()\n\n    def expected_representation_selected(\n        self, folder_id, task_name, representation_id\n    ):\n        if self._expected_selection.expected_representation_selected(\n            folder_id, task_name, representation_id\n        ):\n            self._trigger_expected_selection_changed()\n\n    def get_expected_selection_data(self):\n        return self._expected_selection.get_expected_selection_data()\n\n    def go_to_current_context(self):\n        self.set_expected_selection(\n            self._current_folder_id, self._current_task_name\n        )\n\n    # Model functions\n    def get_folder_items(self, project_name, sender=None):\n        return self._hierarchy_model.get_folder_items(project_name, sender)\n\n    def get_task_items(self, project_name, folder_id, sender=None):\n        return self._hierarchy_model.get_task_items(\n            project_name, folder_id, sender\n        )\n\n    def get_workarea_dir_by_context(self, folder_id, task_id):\n        return self._workfiles_model.get_workarea_dir_by_context(\n            folder_id, task_id)\n\n    def get_workarea_file_items(self, folder_id, task_id):\n        return self._workfiles_model.get_workarea_file_items(\n            folder_id, task_id)\n\n    def get_workarea_save_as_data(self, folder_id, task_id):\n        return self._workfiles_model.get_workarea_save_as_data(\n            folder_id, task_id)\n\n    def fill_workarea_filepath(\n        self,\n        folder_id,\n        task_id,\n        extension,\n        use_last_version,\n        version,\n        comment,\n    ):\n        return self._workfiles_model.fill_workarea_filepath(\n            folder_id,\n            task_id,\n            extension,\n            use_last_version,\n            version,\n            comment,\n        )\n\n    def get_published_file_items(self, folder_id, task_id):\n        task_name = None\n        if task_id:\n            task = self.get_task_entity(\n                self.get_current_project_name(), task_id\n            )\n            task_name = task.get(\"name\")\n\n        return self._workfiles_model.get_published_file_items(\n            folder_id, task_name)\n\n    def get_workfile_info(self, folder_id, task_id, filepath):\n        return self._workfiles_model.get_workfile_info(\n            folder_id, task_id, filepath\n        )\n\n    def save_workfile_info(self, folder_id, task_id, filepath, note):\n        self._workfiles_model.save_workfile_info(\n            folder_id, task_id, filepath, note\n        )\n\n    def reset(self):\n        if not self._host_is_valid:\n            self._emit_event(\"controller.reset.started\")\n            self._emit_event(\"controller.reset.finished\")\n            return\n        expected_folder_id = self.get_selected_folder_id()\n        expected_task_name = self.get_selected_task_name()\n        expected_work_path = self.get_selected_workfile_path()\n        expected_repre_id = self.get_selected_representation_id()\n        expected_work_name = None\n        if expected_work_path:\n            expected_work_name = os.path.basename(expected_work_path)\n\n        self._emit_event(\"controller.reset.started\")\n\n        context = self._get_host_current_context()\n\n        project_name = context[\"project_name\"]\n        folder_name = context[\"asset_name\"]\n        task_name = context[\"task_name\"]\n        current_file = self.get_current_workfile()\n        folder_id = None\n        if folder_name:\n            folder = ayon_api.get_folder_by_path(project_name, folder_name)\n            if folder:\n                folder_id = folder[\"id\"]\n\n        self._project_settings = None\n        self._project_anatomy = None\n\n        self._current_project_name = project_name\n        self._current_folder_name = folder_name\n        self._current_folder_id = folder_id\n        self._current_task_name = task_name\n\n        self._projects_model.reset()\n        self._hierarchy_model.reset()\n\n        if not expected_folder_id:\n            expected_folder_id = folder_id\n            expected_task_name = task_name\n            if current_file:\n                expected_work_name = os.path.basename(current_file)\n\n        self._emit_event(\"controller.reset.finished\")\n\n        self._expected_selection.set_expected_selection(\n            project_name,\n            expected_folder_id,\n            expected_task_name,\n            expected_work_name,\n            expected_repre_id,\n        )\n\n    # Controller actions\n    def open_workfile(self, folder_id, task_id, filepath):\n        self._emit_event(\"open_workfile.started\")\n\n        failed = False\n        try:\n            self._open_workfile(folder_id, task_id, filepath)\n\n        except Exception:\n            failed = True\n            self.log.warning(\"Open of workfile failed\", exc_info=True)\n\n        self._emit_event(\n            \"open_workfile.finished\",\n            {\"failed\": failed},\n        )\n\n    def save_current_workfile(self):\n        current_file = self.get_current_workfile()\n        self._host_save_workfile(current_file)\n\n    def save_as_workfile(\n        self,\n        folder_id,\n        task_id,\n        workdir,\n        filename,\n        template_key,\n    ):\n        self._emit_event(\"save_as.started\")\n\n        failed = False\n        try:\n            self._save_as_workfile(\n                folder_id,\n                task_id,\n                workdir,\n                filename,\n                template_key,\n            )\n        except Exception:\n            failed = True\n            self.log.warning(\"Save as failed\", exc_info=True)\n\n        self._emit_event(\n            \"save_as.finished\",\n            {\"failed\": failed},\n        )\n\n    def copy_workfile_representation(\n        self,\n        representation_id,\n        representation_filepath,\n        folder_id,\n        task_id,\n        workdir,\n        filename,\n        template_key,\n    ):\n        self._emit_event(\"copy_representation.started\")\n\n        failed = False\n        try:\n            self._save_as_workfile(\n                folder_id,\n                task_id,\n                workdir,\n                filename,\n                template_key,\n                src_filepath=representation_filepath\n            )\n        except Exception:\n            failed = True\n            self.log.warning(\n                \"Copy of workfile representation failed\", exc_info=True\n            )\n\n        self._emit_event(\n            \"copy_representation.finished\",\n            {\"failed\": failed},\n        )\n\n    def duplicate_workfile(self, src_filepath, workdir, filename):\n        self._emit_event(\"workfile_duplicate.started\")\n\n        failed = False\n        try:\n            dst_filepath = os.path.join(workdir, filename)\n            shutil.copy(src_filepath, dst_filepath)\n        except Exception:\n            failed = True\n            self.log.warning(\"Duplication of workfile failed\", exc_info=True)\n\n        self._emit_event(\n            \"workfile_duplicate.finished\",\n            {\"failed\": failed},\n        )\n\n    # Helper host methods that resolve 'IWorkfileHost' interface\n    def _host_open_workfile(self, filepath):\n        host = self._host\n        if isinstance(host, IWorkfileHost):\n            host.open_workfile(filepath)\n        else:\n            host.open_file(filepath)\n\n    def _host_save_workfile(self, filepath):\n        host = self._host\n        if isinstance(host, IWorkfileHost):\n            host.save_workfile(filepath)\n        else:\n            host.save_file(filepath)\n\n    def _emit_event(self, topic, data=None):\n        self.emit_event(topic, data, \"controller\")\n\n    # Expected selection\n    # - expected selection is used to restore selection after refresh\n    #   or when current context should be used\n    def _trigger_expected_selection_changed(self):\n        self._emit_event(\n            \"expected_selection_changed\",\n            self._expected_selection.get_expected_selection_data(),\n        )\n\n    def _get_event_context_data(\n        self, project_name, folder_id, task_id, folder=None, task=None\n    ):\n        if folder is None:\n            folder = self.get_folder_entity(project_name, folder_id)\n        if task is None:\n            task = self.get_task_entity(project_name, task_id)\n        # NOTE keys should be OpenPype compatible\n        return {\n            \"project_name\": project_name,\n            \"folder_id\": folder_id,\n            \"asset_id\": folder_id,\n            \"asset_name\": folder[\"name\"],\n            \"task_id\": task_id,\n            \"task_name\": task[\"name\"],\n            \"host_name\": self.get_host_name(),\n        }\n\n    def _open_workfile(self, folder_id, task_id, filepath):\n        project_name = self.get_current_project_name()\n        event_data = self._get_event_context_data(\n            project_name, folder_id, task_id\n        )\n        event_data[\"filepath\"] = filepath\n\n        emit_event(\"workfile.open.before\", event_data, source=\"workfiles.tool\")\n\n        # Change context\n        task_name = event_data[\"task_name\"]\n        if (\n            folder_id != self.get_current_folder_id()\n            or task_name != self.get_current_task_name()\n        ):\n            # Use OpenPype asset-like object\n            asset_doc = get_asset_by_id(\n                event_data[\"project_name\"],\n                event_data[\"folder_id\"],\n            )\n            change_current_context(\n                asset_doc,\n                event_data[\"task_name\"]\n            )\n\n        self._host_open_workfile(filepath)\n\n        emit_event(\"workfile.open.after\", event_data, source=\"workfiles.tool\")\n\n    def _save_as_workfile(\n        self,\n        folder_id,\n        task_id,\n        workdir,\n        filename,\n        template_key,\n        src_filepath=None,\n    ):\n        # Trigger before save event\n        project_name = self.get_current_project_name()\n        folder = self.get_folder_entity(project_name, folder_id)\n        task = self.get_task_entity(project_name, task_id)\n        task_name = task[\"name\"]\n\n        # QUESTION should the data be different for 'before' and 'after'?\n        event_data = self._get_event_context_data(\n            project_name, folder_id, task_id, folder, task\n        )\n        event_data.update({\n            \"filename\": filename,\n            \"workdir_path\": workdir,\n        })\n\n        emit_event(\"workfile.save.before\", event_data, source=\"workfiles.tool\")\n\n        # Create workfiles root folder\n        if not os.path.exists(workdir):\n            self.log.debug(\"Initializing work directory: %s\", workdir)\n            os.makedirs(workdir)\n\n        # Change context\n        if (\n            folder_id != self.get_current_folder_id()\n            or task_name != self.get_current_task_name()\n        ):\n            # Use OpenPype asset-like object\n            asset_doc = get_asset_by_id(project_name, folder[\"id\"])\n            change_current_context(\n                asset_doc,\n                task[\"name\"],\n                template_key=template_key\n            )\n\n        # Save workfile\n        dst_filepath = os.path.join(workdir, filename)\n        if src_filepath:\n            shutil.copyfile(src_filepath, dst_filepath)\n            self._host_open_workfile(dst_filepath)\n        else:\n            self._host_save_workfile(dst_filepath)\n\n        # Make sure workfile info exists\n        self.save_workfile_info(folder_id, task_id, dst_filepath, None)\n\n        # Create extra folders\n        create_workdir_extra_folders(\n            workdir,\n            self.get_host_name(),\n            task[\"taskType\"],\n            task_name,\n            project_name\n        )\n\n        # Trigger after save events\n        emit_event(\"workfile.save.after\", event_data, source=\"workfiles.tool\")\n        self.reset()\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/models/__init__.py",
    "content": "from .selection import SelectionModel\nfrom .workfiles import WorkfilesModel\n\n\n__all__ = (\n    \"SelectionModel\",\n    \"WorkfilesModel\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/models/selection.py",
    "content": "class SelectionModel(object):\n    \"\"\"Model handling selection changes.\n\n    Triggering events:\n    - \"selection.folder.changed\"\n    - \"selection.task.changed\"\n    - \"selection.workarea.changed\"\n    - \"selection.representation.changed\"\n    \"\"\"\n\n    event_source = \"selection.model\"\n\n    def __init__(self, controller):\n        self._controller = controller\n\n        self._folder_id = None\n        self._task_name = None\n        self._task_id = None\n        self._workfile_path = None\n        self._representation_id = None\n\n    def get_selected_folder_id(self):\n        return self._folder_id\n\n    def set_selected_folder(self, folder_id):\n        if folder_id == self._folder_id:\n            return\n\n        self._folder_id = folder_id\n        self._controller.emit_event(\n            \"selection.folder.changed\",\n            {\n                \"project_name\": self._controller.get_current_project_name(),\n                \"folder_id\": folder_id\n            },\n            self.event_source\n        )\n\n    def get_selected_task_name(self):\n        return self._task_name\n\n    def get_selected_task_id(self):\n        return self._task_id\n\n    def set_selected_task(self, task_id, task_name):\n        if task_id == self._task_id:\n            return\n\n        self._task_name = task_name\n        self._task_id = task_id\n        self._controller.emit_event(\n            \"selection.task.changed\",\n            {\n                \"project_name\": self._controller.get_current_project_name(),\n                \"folder_id\": self._folder_id,\n                \"task_name\": task_name,\n                \"task_id\": task_id\n            },\n            self.event_source\n        )\n\n    def get_selected_workfile_path(self):\n        return self._workfile_path\n\n    def set_selected_workfile_path(self, path):\n        if path == self._workfile_path:\n            return\n\n        self._workfile_path = path\n        self._controller.emit_event(\n            \"selection.workarea.changed\",\n            {\n                \"project_name\": self._controller.get_current_project_name(),\n                \"path\": path,\n                \"folder_id\": self._folder_id,\n                \"task_name\": self._task_name,\n                \"task_id\": self._task_id,\n            },\n            self.event_source\n        )\n\n    def get_selected_representation_id(self):\n        return self._representation_id\n\n    def set_selected_representation_id(self, representation_id):\n        if representation_id == self._representation_id:\n            return\n        self._representation_id = representation_id\n        self._controller.emit_event(\n            \"selection.representation.changed\",\n            {\n                \"project_name\": self._controller.get_current_project_name(),\n                \"representation_id\": representation_id,\n            },\n            self.event_source\n        )\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/models/workfiles.py",
    "content": "import os\nimport re\nimport copy\n\nimport arrow\nimport ayon_api\nfrom ayon_api.operations import OperationsSession\n\nfrom openpype.client import get_project\nfrom openpype.client.operations import (\n    prepare_workfile_info_update_data,\n)\nfrom openpype.pipeline.template_data import (\n    get_template_data,\n)\nfrom openpype.pipeline.workfile import (\n    get_workdir_with_workdir_data,\n    get_workfile_template_key,\n    get_last_workfile_with_version,\n)\nfrom openpype.pipeline.version_start import get_versioning_start\nfrom openpype.tools.ayon_workfiles.abstract import (\n    WorkareaFilepathResult,\n    FileItem,\n    WorkfileInfo,\n)\n\n\ndef get_folder_template_data(folder):\n    if not folder:\n        return {}\n    parts = folder[\"path\"].split(\"/\")\n    parts.pop(-1)\n    hierarchy = \"/\".join(parts)\n    return {\n        \"asset\": folder[\"name\"],\n        \"folder\": {\n            \"name\": folder[\"name\"],\n            \"type\": folder[\"folderType\"],\n            \"path\": folder[\"path\"],\n        },\n        \"hierarchy\": hierarchy,\n    }\n\n\ndef get_task_template_data(project_entity, task):\n    if not task:\n        return {}\n    short_name = None\n    task_type_name = task[\"taskType\"]\n    for task_type_info in project_entity[\"taskTypes\"]:\n        if task_type_info[\"name\"] == task_type_name:\n            short_name = task_type_info[\"shortName\"]\n            break\n\n    return {\n        \"task\": {\n            \"name\": task[\"name\"],\n            \"type\": task_type_name,\n            \"short\": short_name,\n        }\n    }\n\n\nclass CommentMatcher(object):\n    \"\"\"Use anatomy and work file data to parse comments from filenames\"\"\"\n    def __init__(self, extensions, file_template, data):\n        self.fname_regex = None\n\n        if \"{comment}\" not in file_template:\n            # Don't look for comment if template doesn't allow it\n            return\n\n        # Create a regex group for extensions\n        any_extension = \"(?:{})\".format(\n            \"|\".join(re.escape(ext.lstrip(\".\")) for ext in extensions)\n        )\n\n        # Use placeholders that will never be in the filename\n        temp_data = copy.deepcopy(data)\n        temp_data[\"comment\"] = \"<<comment>>\"\n        temp_data[\"version\"] = \"<<version>>\"\n        temp_data[\"ext\"] = \"<<ext>>\"\n\n        fname_pattern = file_template.format_strict(temp_data)\n        fname_pattern = re.escape(fname_pattern)\n\n        # Replace comment and version with something we can match with regex\n        replacements = {\n            \"<<comment>>\": \"(.+)\",\n            \"<<version>>\": \"[0-9]+\",\n            \"<<ext>>\": any_extension,\n        }\n        for src, dest in replacements.items():\n            fname_pattern = fname_pattern.replace(re.escape(src), dest)\n\n        # Match from beginning to end of string to be safe\n        fname_pattern = \"^{}$\".format(fname_pattern)\n\n        self.fname_regex = re.compile(fname_pattern)\n\n    def parse_comment(self, filepath):\n        \"\"\"Parse the {comment} part from a filename\"\"\"\n        if not self.fname_regex:\n            return\n\n        fname = os.path.basename(filepath)\n        match = self.fname_regex.match(fname)\n        if match:\n            return match.group(1)\n\n\nclass WorkareaModel:\n    \"\"\"Workfiles model looking for workfiles in workare folder.\n\n    Workarea folder is usually task and host specific, defined by\n    anatomy templates. Is looking for files with extensions defined\n    by host integration.\n    \"\"\"\n\n    def __init__(self, controller):\n        self._controller = controller\n        extensions = None\n        if controller.is_host_valid():\n            extensions = controller.get_workfile_extensions()\n        self._extensions = extensions\n        self._base_data = None\n        self._fill_data_by_folder_id = {}\n        self._task_data_by_folder_id = {}\n        self._workdir_by_context = {}\n\n    @property\n    def project_name(self):\n        return self._controller.get_current_project_name()\n\n    def reset(self):\n        self._base_data = None\n        self._fill_data_by_folder_id = {}\n        self._task_data_by_folder_id = {}\n\n    def _get_base_data(self):\n        if self._base_data is None:\n            base_data = get_template_data(get_project(self.project_name))\n            base_data[\"app\"] = self._controller.get_host_name()\n            self._base_data = base_data\n        return copy.deepcopy(self._base_data)\n\n    def _get_folder_data(self, folder_id):\n        fill_data = self._fill_data_by_folder_id.get(folder_id)\n        if fill_data is None:\n            folder = self._controller.get_folder_entity(\n                self.project_name, folder_id\n            )\n            fill_data = get_folder_template_data(folder)\n            self._fill_data_by_folder_id[folder_id] = fill_data\n        return copy.deepcopy(fill_data)\n\n    def _get_task_data(self, project_entity, folder_id, task_id):\n        task_data = self._task_data_by_folder_id.setdefault(folder_id, {})\n        if task_id not in task_data:\n            task = self._controller.get_task_entity(\n                self.project_name, task_id\n            )\n            if task:\n                task_data[task_id] = get_task_template_data(\n                    project_entity, task)\n        return copy.deepcopy(task_data[task_id])\n\n    def _prepare_fill_data(self, folder_id, task_id):\n        if not folder_id or not task_id:\n            return {}\n\n        base_data = self._get_base_data()\n        project_name = base_data[\"project\"][\"name\"]\n        folder_data = self._get_folder_data(folder_id)\n        project_entity = self._controller.get_project_entity(project_name)\n        task_data = self._get_task_data(project_entity, folder_id, task_id)\n\n        base_data.update(folder_data)\n        base_data.update(task_data)\n\n        return base_data\n\n    def get_workarea_dir_by_context(self, folder_id, task_id):\n        if not folder_id or not task_id:\n            return None\n        folder_mapping = self._workdir_by_context.setdefault(folder_id, {})\n        workdir = folder_mapping.get(task_id)\n        if workdir is not None:\n            return workdir\n\n        workdir_data = self._prepare_fill_data(folder_id, task_id)\n\n        workdir = get_workdir_with_workdir_data(\n            workdir_data,\n            self.project_name,\n            anatomy=self._controller.project_anatomy,\n        )\n        folder_mapping[task_id] = workdir\n        return workdir\n\n    def get_file_items(self, folder_id, task_id):\n        items = []\n        if not folder_id or not task_id:\n            return items\n\n        workdir = self.get_workarea_dir_by_context(folder_id, task_id)\n        if not os.path.exists(workdir):\n            return items\n\n        for filename in os.listdir(workdir):\n            filepath = os.path.join(workdir, filename)\n            if not os.path.isfile(filepath):\n                continue\n\n            ext = os.path.splitext(filename)[1].lower()\n            if ext not in self._extensions:\n                continue\n\n            modified = os.path.getmtime(filepath)\n            items.append(\n                FileItem(workdir, filename, modified)\n            )\n        return items\n\n    def _get_template_key(self, fill_data):\n        task_type = fill_data.get(\"task\", {}).get(\"type\")\n        # TODO cache\n        return get_workfile_template_key(\n            task_type,\n            self._controller.get_host_name(),\n            project_name=self.project_name\n        )\n\n    def _get_last_workfile_version(\n        self, workdir, file_template, fill_data, extensions\n    ):\n        version = get_last_workfile_with_version(\n            workdir, str(file_template), fill_data, extensions\n        )[1]\n\n        if version is None:\n            task_info = fill_data.get(\"task\", {})\n            version = get_versioning_start(\n                self.project_name,\n                self._controller.get_host_name(),\n                task_name=task_info.get(\"name\"),\n                task_type=task_info.get(\"type\"),\n                family=\"workfile\",\n                project_settings=self._controller.project_settings,\n            )\n        else:\n            version += 1\n        return version\n\n    def _get_comments_from_root(\n        self,\n        file_template,\n        extensions,\n        fill_data,\n        root,\n        current_filename,\n    ):\n        current_comment = None\n        comment_hints = set()\n        filenames = []\n        if root and os.path.exists(root):\n            for filename in os.listdir(root):\n                path = os.path.join(root, filename)\n                if not os.path.isfile(path):\n                    continue\n\n                ext = os.path.splitext(filename)[-1].lower()\n                if ext in extensions:\n                    filenames.append(filename)\n\n        if not filenames:\n            return comment_hints, current_comment\n\n        matcher = CommentMatcher(extensions, file_template, fill_data)\n\n        for filename in filenames:\n            comment = matcher.parse_comment(filename)\n            if comment:\n                comment_hints.add(comment)\n                if filename == current_filename:\n                    current_comment = comment\n\n        return list(comment_hints), current_comment\n\n    def _get_workdir(self, anatomy, template_key, fill_data):\n        template_info = anatomy.templates_obj[template_key]\n        directory_template = template_info[\"folder\"]\n        return directory_template.format_strict(fill_data).normalized()\n\n    def get_workarea_save_as_data(self, folder_id, task_id):\n        folder = None\n        task = None\n        if folder_id:\n            folder = self._controller.get_folder_entity(\n                self.project_name, folder_id\n            )\n        if task_id:\n            task = self._controller.get_task_entity(\n                self.project_name, task_id\n            )\n\n        if not folder or not task:\n            return {\n                \"template_key\": None,\n                \"template_has_version\": None,\n                \"template_has_comment\": None,\n                \"ext\": None,\n                \"workdir\": None,\n                \"comment\": None,\n                \"comment_hints\": None,\n                \"last_version\": None,\n                \"extensions\": None,\n            }\n\n        anatomy = self._controller.project_anatomy\n        fill_data = self._prepare_fill_data(folder_id, task_id)\n        template_key = self._get_template_key(fill_data)\n\n        current_workfile = self._controller.get_current_workfile()\n        current_filename = None\n        current_ext = None\n        if current_workfile:\n            current_filename = os.path.basename(current_workfile)\n            current_ext = os.path.splitext(current_filename)[1].lower()\n\n        extensions = self._extensions\n        if not current_ext and extensions:\n            current_ext = tuple(extensions)[0]\n\n        workdir = self._get_workdir(anatomy, template_key, fill_data)\n\n        template_info = anatomy.templates_obj[template_key]\n        file_template = template_info[\"file\"]\n\n        comment_hints, comment = self._get_comments_from_root(\n            file_template,\n            extensions,\n            fill_data,\n            workdir,\n            current_filename,\n        )\n        last_version = self._get_last_workfile_version(\n            workdir, file_template, fill_data, extensions)\n        str_file_template = str(file_template)\n        template_has_version = \"{version\" in str_file_template\n        template_has_comment = \"{comment\" in str_file_template\n\n        return {\n            \"template_key\": template_key,\n            \"template_has_version\": template_has_version,\n            \"template_has_comment\": template_has_comment,\n            \"ext\": current_ext,\n            \"workdir\": workdir,\n            \"comment\": comment,\n            \"comment_hints\": comment_hints,\n            \"last_version\": last_version,\n            \"extensions\": extensions,\n        }\n\n    def fill_workarea_filepath(\n        self,\n        folder_id,\n        task_id,\n        extension,\n        use_last_version,\n        version,\n        comment,\n    ):\n        anatomy = self._controller.project_anatomy\n        fill_data = self._prepare_fill_data(folder_id, task_id)\n        template_key = self._get_template_key(fill_data)\n\n        workdir = self._get_workdir(anatomy, template_key, fill_data)\n\n        template_info = anatomy.templates_obj[template_key]\n        file_template = template_info[\"file\"]\n\n        if use_last_version:\n            version = self._get_last_workfile_version(\n                workdir, file_template, fill_data, self._extensions\n            )\n        fill_data[\"version\"] = version\n        fill_data[\"ext\"] = extension.lstrip(\".\")\n\n        if comment:\n            fill_data[\"comment\"] = comment\n\n        filename = file_template.format(fill_data)\n        if not filename.solved:\n            filename = None\n\n        exists = False\n        if filename:\n            filepath = os.path.join(workdir, filename)\n            exists = os.path.exists(filepath)\n\n        return WorkareaFilepathResult(\n            workdir,\n            filename,\n            exists\n        )\n\n\nclass WorkfileEntitiesModel:\n    \"\"\"Workfile entities model.\n\n    Args:\n        control (AbstractWorkfileController): Controller object.\n    \"\"\"\n\n    def __init__(self, controller):\n        self._controller = controller\n        self._cache = {}\n        self._items = {}\n\n    def _get_workfile_info_identifier(\n        self, folder_id, task_id, rootless_path\n    ):\n        return \"_\".join([folder_id, task_id, rootless_path])\n\n    def _get_rootless_path(self, filepath):\n        anatomy = self._controller.project_anatomy\n\n        workdir, filename = os.path.split(filepath)\n        success, rootless_dir = anatomy.find_root_template_from_path(workdir)\n        return \"/\".join([\n            os.path.normpath(rootless_dir).replace(\"\\\\\", \"/\"),\n            filename\n        ])\n\n    def _prepare_workfile_info_item(\n        self, folder_id, task_id, workfile_info, filepath\n    ):\n        note = \"\"\n        if workfile_info:\n            note = workfile_info[\"attrib\"].get(\"description\") or \"\"\n\n        filestat = os.stat(filepath)\n        return WorkfileInfo(\n            folder_id,\n            task_id,\n            filepath,\n            filesize=filestat.st_size,\n            creation_time=filestat.st_ctime,\n            modification_time=filestat.st_mtime,\n            note=note\n        )\n\n    def _get_workfile_info(self, folder_id, task_id, identifier):\n        workfile_info = self._cache.get(identifier)\n        if workfile_info is not None:\n            return workfile_info\n\n        for workfile_info in ayon_api.get_workfiles_info(\n            self._controller.get_current_project_name(),\n            task_ids=[task_id],\n            fields=[\"id\", \"path\", \"attrib\"],\n        ):\n            workfile_identifier = self._get_workfile_info_identifier(\n                folder_id, task_id, workfile_info[\"path\"]\n            )\n            self._cache[workfile_identifier] = workfile_info\n        return self._cache.get(identifier)\n\n    def get_workfile_info(\n        self, folder_id, task_id, filepath, rootless_path=None\n    ):\n        if not folder_id or not task_id or not filepath:\n            return None\n\n        if rootless_path is None:\n            rootless_path = self._get_rootless_path(filepath)\n\n        identifier = self._get_workfile_info_identifier(\n            folder_id, task_id, rootless_path)\n        item = self._items.get(identifier)\n        if item is None:\n            workfile_info = self._get_workfile_info(\n                folder_id, task_id, identifier\n            )\n            item = self._prepare_workfile_info_item(\n                folder_id, task_id, workfile_info, filepath\n            )\n            self._items[identifier] = item\n        return item\n\n    def save_workfile_info(self, folder_id, task_id, filepath, note):\n        rootless_path = self._get_rootless_path(filepath)\n        identifier = self._get_workfile_info_identifier(\n            folder_id, task_id, rootless_path\n        )\n        workfile_info = self._get_workfile_info(\n            folder_id, task_id, identifier\n        )\n        if not workfile_info:\n            self._cache[identifier] = self._create_workfile_info_entity(\n                task_id, rootless_path, note or \"\")\n            self._items.pop(identifier, None)\n            return\n\n        if note is None:\n            return\n\n        new_workfile_info = copy.deepcopy(workfile_info)\n        attrib = new_workfile_info.setdefault(\"attrib\", {})\n        attrib[\"description\"] = note\n        update_data = prepare_workfile_info_update_data(\n            workfile_info, new_workfile_info\n        )\n        self._cache[identifier] = new_workfile_info\n        self._items.pop(identifier, None)\n        if not update_data:\n            return\n\n        project_name = self._controller.get_current_project_name()\n\n        session = OperationsSession()\n        session.update_entity(\n            project_name, \"workfile\", workfile_info[\"id\"], update_data\n        )\n        session.commit()\n\n    def _create_workfile_info_entity(self, task_id, rootless_path, note):\n        extension = os.path.splitext(rootless_path)[1]\n\n        project_name = self._controller.get_current_project_name()\n\n        workfile_info = {\n            \"path\": rootless_path,\n            \"taskId\": task_id,\n            \"attrib\": {\n                \"extension\": extension,\n                \"description\": note\n            }\n        }\n\n        session = OperationsSession()\n        session.create_entity(project_name, \"workfile\", workfile_info)\n        session.commit()\n        return workfile_info\n\n\nclass PublishWorkfilesModel:\n    \"\"\"Model for handling of published workfiles.\n\n    Todos:\n        Cache workfiles products and representations for some time.\n            Note Representations won't change. Only what can change are\n                versions.\n    \"\"\"\n\n    def __init__(self, controller):\n        self._controller = controller\n        self._cached_extensions = None\n        self._cached_repre_extensions = None\n\n    @property\n    def _extensions(self):\n        if self._cached_extensions is None:\n            exts = self._controller.get_workfile_extensions() or []\n            self._cached_extensions = exts\n        return self._cached_extensions\n\n    @property\n    def _repre_extensions(self):\n        if self._cached_repre_extensions is None:\n            self._cached_repre_extensions = {\n                ext.lstrip(\".\") for ext in self._extensions\n            }\n        return self._cached_repre_extensions\n\n    def _file_item_from_representation(\n        self, repre_entity, project_anatomy, task_name=None\n    ):\n        if task_name is not None:\n            task_info = repre_entity[\"context\"].get(\"task\")\n            if not task_info or task_info[\"name\"] != task_name:\n                return None\n\n        # Filter by extension\n        extensions = self._repre_extensions\n        workfile_path = None\n        for repre_file in repre_entity[\"files\"]:\n            ext = (\n                os.path.splitext(repre_file[\"name\"])[1]\n                .lower()\n                .lstrip(\".\")\n            )\n            if ext in extensions:\n                workfile_path = repre_file[\"path\"]\n                break\n\n        if not workfile_path:\n            return None\n\n        try:\n            workfile_path = workfile_path.format(\n                root=project_anatomy.roots)\n        except Exception as exc:\n            print(\"Failed to format workfile path: {}\".format(exc))\n\n        dirpath, filename = os.path.split(workfile_path)\n        created_at = arrow.get(repre_entity[\"createdAt\"]).to(\"local\")\n        return FileItem(\n            dirpath,\n            filename,\n            created_at.float_timestamp,\n            repre_entity[\"id\"]\n        )\n\n    def get_file_items(self, folder_id, task_name):\n        # TODO refactor to use less server API calls\n        project_name = self._controller.get_current_project_name()\n        # Get subset docs of asset\n        product_entities = ayon_api.get_products(\n            project_name,\n            folder_ids=[folder_id],\n            product_types=[\"workfile\"],\n            fields=[\"id\", \"name\"]\n        )\n\n        output = []\n        product_ids = {product[\"id\"] for product in product_entities}\n        if not product_ids:\n            return output\n\n        # Get version docs of subsets with their families\n        version_entities = ayon_api.get_versions(\n            project_name,\n            product_ids=product_ids,\n            fields=[\"id\", \"productId\"]\n        )\n        version_ids = {version[\"id\"] for version in version_entities}\n        if not version_ids:\n            return output\n\n        # Query representations of filtered versions and add filter for\n        #   extension\n        repre_entities = ayon_api.get_representations(\n            project_name,\n            version_ids=version_ids\n        )\n        project_anatomy = self._controller.project_anatomy\n\n        # Filter queried representations by task name if task is set\n        file_items = []\n        for repre_entity in repre_entities:\n            file_item = self._file_item_from_representation(\n                repre_entity, project_anatomy, task_name\n            )\n            if file_item is not None:\n                file_items.append(file_item)\n\n        return file_items\n\n\nclass WorkfilesModel:\n    \"\"\"Workfiles model.\"\"\"\n\n    def __init__(self, controller):\n        self._controller = controller\n\n        self._entities_model = WorkfileEntitiesModel(controller)\n        self._workarea_model = WorkareaModel(controller)\n        self._published_model = PublishWorkfilesModel(controller)\n\n    def get_workfile_info(self, folder_id, task_id, filepath):\n        return self._entities_model.get_workfile_info(\n            folder_id, task_id, filepath\n        )\n\n    def save_workfile_info(self, folder_id, task_id, filepath, note):\n        self._entities_model.save_workfile_info(\n            folder_id, task_id, filepath, note\n        )\n\n    def get_workarea_dir_by_context(self, folder_id, task_id):\n        \"\"\"Workarea dir for passed context.\n\n        The directory path is based on project anatomy templates.\n\n        Args:\n            folder_id (str): Folder id.\n            task_id (str): Task id.\n\n        Returns:\n            Union[str, None]: Workarea dir path or None for invalid context.\n        \"\"\"\n\n        return self._workarea_model.get_workarea_dir_by_context(\n            folder_id, task_id)\n\n    def get_workarea_file_items(self, folder_id, task_id):\n        \"\"\"Workfile items for passed context from workarea.\n\n        Args:\n            folder_id (Union[str, None]): Folder id.\n            task_id (Union[str, None]): Task id.\n\n        Returns:\n            list[FileItem]: List of file items matching workarea of passed\n                context.\n        \"\"\"\n\n        return self._workarea_model.get_file_items(folder_id, task_id)\n\n    def get_workarea_save_as_data(self, folder_id, task_id):\n        return self._workarea_model.get_workarea_save_as_data(\n            folder_id, task_id)\n\n    def fill_workarea_filepath(self, *args, **kwargs):\n        return self._workarea_model.fill_workarea_filepath(\n            *args, **kwargs\n        )\n\n    def get_published_file_items(self, folder_id, task_name):\n        \"\"\"Published workfiles for passed context.\n\n        Args:\n            folder_id (str): Folder id.\n            task_name (str): Task name.\n\n        Returns:\n            list[FileItem]: List of files for published workfiles.\n        \"\"\"\n\n        return self._published_model.get_file_items(folder_id, task_name)\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/widgets/__init__.py",
    "content": "from .window import WorkfilesToolWindow\n\n\n__all__ = (\n    \"WorkfilesToolWindow\",\n)\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/widgets/constants.py",
    "content": "from qtpy import QtCore\n\n\nITEM_ID_ROLE = QtCore.Qt.UserRole + 1\nPARENT_ID_ROLE = QtCore.Qt.UserRole + 2\nITEM_NAME_ROLE = QtCore.Qt.UserRole + 3\nTASK_TYPE_ROLE = QtCore.Qt.UserRole + 4\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/widgets/files_widget.py",
    "content": "import os\n\nimport qtpy\nfrom qtpy import QtWidgets, QtCore\n\nfrom .save_as_dialog import SaveAsDialog\nfrom .files_widget_workarea import WorkAreaFilesWidget\nfrom .files_widget_published import PublishedFilesWidget\n\n\nclass FilesWidget(QtWidgets.QWidget):\n    \"\"\"A widget displaying files that allows to save and open files.\n\n    Args:\n        controller (AbstractWorkfilesFrontend): The control object.\n        parent (QtWidgets.QWidget): The parent widget.\n    \"\"\"\n\n    def __init__(self, controller, parent):\n        super(FilesWidget, self).__init__(parent)\n\n        files_widget = QtWidgets.QStackedWidget(self)\n        workarea_widget = WorkAreaFilesWidget(controller, files_widget)\n        published_widget = PublishedFilesWidget(controller, files_widget)\n        files_widget.addWidget(workarea_widget)\n        files_widget.addWidget(published_widget)\n\n        btns_widget = QtWidgets.QWidget(self)\n\n        workarea_btns_widget = QtWidgets.QWidget(btns_widget)\n        workarea_btn_open = QtWidgets.QPushButton(\n            \"Open\", workarea_btns_widget)\n        workarea_btn_browse = QtWidgets.QPushButton(\n            \"Browse\", workarea_btns_widget)\n        workarea_btn_save = QtWidgets.QPushButton(\n            \"Save As\", workarea_btns_widget)\n\n        workarea_btns_layout = QtWidgets.QHBoxLayout(workarea_btns_widget)\n        workarea_btns_layout.setContentsMargins(0, 0, 0, 0)\n        workarea_btns_layout.addWidget(workarea_btn_open, 1)\n        workarea_btns_layout.addWidget(workarea_btn_browse, 1)\n        workarea_btns_layout.addWidget(workarea_btn_save, 1)\n\n        published_btns_widget = QtWidgets.QWidget(btns_widget)\n        published_btn_copy_n_open = QtWidgets.QPushButton(\n            \"Copy && Open\", published_btns_widget\n        )\n        published_btn_change_context = QtWidgets.QPushButton(\n            \"Choose different context\", published_btns_widget\n        )\n        published_btn_cancel = QtWidgets.QPushButton(\n            \"Cancel\", published_btns_widget\n        )\n\n        published_btns_layout = QtWidgets.QHBoxLayout(published_btns_widget)\n        published_btns_layout.setContentsMargins(0, 0, 0, 0)\n        published_btns_layout.addWidget(published_btn_copy_n_open, 1)\n        published_btns_layout.addWidget(published_btn_change_context, 1)\n        published_btns_layout.addWidget(published_btn_cancel, 1)\n\n        btns_layout = QtWidgets.QVBoxLayout(btns_widget)\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n        btns_layout.addWidget(workarea_btns_widget, 1)\n        btns_layout.addWidget(published_btns_widget, 1)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(files_widget, 1)\n        main_layout.addWidget(btns_widget, 0)\n\n        controller.register_event_callback(\n            \"selection.workarea.changed\",\n            self._on_workarea_path_changed\n        )\n        controller.register_event_callback(\n            \"selection.representation.changed\",\n            self._on_published_repre_changed\n        )\n        controller.register_event_callback(\n            \"selection.task.changed\",\n            self._on_task_changed\n        )\n        controller.register_event_callback(\n            \"copy_representation.finished\",\n            self._on_copy_representation_finished,\n        )\n        controller.register_event_callback(\n            \"workfile_save_enable.changed\",\n            self._on_workfile_save_enabled_change,\n        )\n\n        workarea_widget.open_current_requested.connect(\n            self._on_current_open_requests)\n        workarea_widget.duplicate_requested.connect(\n            self._on_duplicate_request)\n        workarea_btn_open.clicked.connect(self._on_workarea_open_clicked)\n        workarea_btn_browse.clicked.connect(self._on_workarea_browse_clicked)\n        workarea_btn_save.clicked.connect(self._on_workarea_save_clicked)\n\n        published_widget.save_as_requested.connect(self._on_save_as_request)\n        published_btn_copy_n_open.clicked.connect(\n            self._on_published_save_clicked)\n        published_btn_change_context.clicked.connect(\n            self._on_published_change_context_clicked)\n        published_btn_cancel.clicked.connect(\n            self._on_published_cancel_clicked)\n\n        self._selected_folder_id = None\n        self._selected_task_id = None\n        self._selected_task_name = None\n\n        self._pre_select_folder_id = None\n        self._pre_select_task_name = None\n\n        self._select_context_mode = False\n        self._valid_selected_context = False\n        self._valid_representation_id = False\n        self._tmp_text_filter = None\n        self._is_save_enabled = True\n\n        self._controller = controller\n        self._files_widget = files_widget\n        self._workarea_widget = workarea_widget\n        self._published_widget = published_widget\n        self._workarea_btns_widget = workarea_btns_widget\n        self._published_btns_widget = published_btns_widget\n\n        self._workarea_btn_open = workarea_btn_open\n        self._workarea_btn_browse = workarea_btn_browse\n        self._workarea_btn_save = workarea_btn_save\n\n        self._published_widget = published_widget\n        self._published_btn_copy_n_open = published_btn_copy_n_open\n        self._published_btn_change_context = published_btn_change_context\n        self._published_btn_cancel = published_btn_cancel\n\n        # Initial setup\n        workarea_btn_open.setEnabled(False)\n        published_btn_copy_n_open.setEnabled(False)\n        published_btn_change_context.setEnabled(False)\n        published_btn_cancel.setVisible(False)\n\n    def set_published_mode(self, published_mode):\n        # Make sure context selection is disabled\n        self._set_select_contex_mode(False)\n        # Change current widget\n        self._files_widget.setCurrentWidget((\n            self._published_widget\n            if published_mode\n            else self._workarea_widget\n        ))\n        # Pass the mode to the widgets, so they can start/stop handle events\n        self._workarea_widget.set_published_mode(published_mode)\n        self._published_widget.set_published_mode(published_mode)\n\n        # Change available buttons\n        self._workarea_btns_widget.setVisible(not published_mode)\n        self._published_btns_widget.setVisible(published_mode)\n\n    def set_text_filter(self, text_filter):\n        if self._select_context_mode:\n            self._tmp_text_filter = text_filter\n            return\n        self._workarea_widget.set_text_filter(text_filter)\n        self._published_widget.set_text_filter(text_filter)\n\n    def _exec_save_as_dialog(self):\n        \"\"\"Show SaveAs dialog using currently selected context.\n\n        Returns:\n            Union[dict[str, Any], None]: Result of the dialog.\n        \"\"\"\n\n        dialog = SaveAsDialog(self._controller, self)\n        dialog.update_context()\n        dialog.exec_()\n        return dialog.get_result()\n\n    # -------------------------------------------------------------\n    # Workarea workfiles\n    # -------------------------------------------------------------\n    def _open_workfile(self, folder_id, task_name, filepath):\n        if self._controller.has_unsaved_changes():\n            result = self._save_changes_prompt()\n            if result is None:\n                return\n\n            if result:\n                self._controller.save_current_workfile()\n        self._controller.open_workfile(folder_id, task_name, filepath)\n\n    def _on_workarea_open_clicked(self):\n        path = self._workarea_widget.get_selected_path()\n        if not path:\n            return\n        folder_id = self._selected_folder_id\n        task_id = self._selected_task_id\n        self._open_workfile(folder_id, task_id, path)\n\n    def _on_current_open_requests(self):\n        self._on_workarea_open_clicked()\n\n    def _on_duplicate_request(self):\n        filepath = self._workarea_widget.get_selected_path()\n        if filepath is None:\n            return\n\n        result = self._exec_save_as_dialog()\n        if result is None:\n            return\n        self._controller.duplicate_workfile(\n            filepath,\n            result[\"workdir\"],\n            result[\"filename\"]\n        )\n\n    def _on_workarea_browse_clicked(self):\n        extnsions = self._controller.get_workfile_extensions()\n        ext_filter = \"Work File (*{0})\".format(\n            \" *\".join(extnsions)\n        )\n        dir_key = \"directory\"\n        if qtpy.API in (\"pyside\", \"pyside2\", \"pyside6\"):\n            dir_key = \"dir\"\n\n        selected_context = self._controller.get_selected_context()\n        workfile_root = self._controller.get_workarea_dir_by_context(\n            selected_context[\"folder_id\"], selected_context[\"task_id\"]\n        )\n        # Find existing directory of workfile root\n        #   - Qt will use 'cwd' instead, if path does not exist, which may lead\n        #       to igniter directory\n        while workfile_root:\n            if os.path.exists(workfile_root):\n                break\n            workfile_root = os.path.dirname(workfile_root)\n\n        kwargs = {\n            \"caption\": \"Work Files\",\n            \"filter\": ext_filter,\n            dir_key: workfile_root\n        }\n\n        filepath = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]\n        if not filepath:\n            return\n\n        folder_id = self._selected_folder_id\n        task_id = self._selected_task_id\n        self._open_workfile(folder_id, task_id, filepath)\n\n    def _on_workarea_save_clicked(self):\n        result = self._exec_save_as_dialog()\n        if result is None:\n            return\n        self._controller.save_as_workfile(\n            result[\"folder_id\"],\n            result[\"task_id\"],\n            result[\"workdir\"],\n            result[\"filename\"],\n            result[\"template_key\"],\n        )\n\n    def _on_workarea_path_changed(self, event):\n        valid_path = event[\"path\"] is not None\n        self._workarea_btn_open.setEnabled(valid_path)\n\n    # -------------------------------------------------------------\n    # Published workfiles\n    # -------------------------------------------------------------\n    def _update_published_btns_state(self):\n        enabled = (\n            self._valid_representation_id\n            and self._valid_selected_context\n            and self._is_save_enabled\n        )\n        self._published_btn_copy_n_open.setEnabled(enabled)\n        self._published_btn_change_context.setEnabled(enabled)\n\n    def _update_workarea_btns_state(self):\n        enabled = self._is_save_enabled\n        self._workarea_btn_save.setEnabled(enabled)\n\n    def _on_published_repre_changed(self, event):\n        self._valid_representation_id = event[\"representation_id\"] is not None\n        self._update_published_btns_state()\n\n    def _on_task_changed(self, event):\n        self._selected_folder_id = event[\"folder_id\"]\n        self._selected_task_id = event[\"task_id\"]\n        self._selected_task_name = event[\"task_name\"]\n        self._valid_selected_context = (\n            self._selected_folder_id is not None\n            and self._selected_task_id is not None\n        )\n        self._update_published_btns_state()\n\n    def _on_published_save_clicked(self):\n        result = self._exec_save_as_dialog()\n        if result is None:\n            return\n\n        repre_info = self._published_widget.get_selected_repre_info()\n        self._controller.copy_workfile_representation(\n            repre_info[\"representation_id\"],\n            repre_info[\"filepath\"],\n            result[\"folder_id\"],\n            result[\"task_id\"],\n            result[\"workdir\"],\n            result[\"filename\"],\n            result[\"template_key\"],\n        )\n\n    def _on_save_as_request(self):\n        self._on_published_save_clicked()\n\n    def _set_select_contex_mode(self, enabled):\n        if self._select_context_mode is enabled:\n            return\n\n        if enabled:\n            self._pre_select_folder_id = self._selected_folder_id\n            self._pre_select_task_name = self._selected_task_name\n        else:\n            self._pre_select_folder_id = None\n            self._pre_select_task_name = None\n        self._select_context_mode = enabled\n        self._published_btn_cancel.setVisible(enabled)\n        self._published_btn_change_context.setVisible(not enabled)\n        self._published_widget.set_select_context_mode(enabled)\n\n        if not enabled and self._tmp_text_filter is not None:\n            self.set_text_filter(self._tmp_text_filter)\n            self._tmp_text_filter = None\n\n    def _on_published_change_context_clicked(self):\n        self._set_select_contex_mode(True)\n\n    def _should_set_pre_select_context(self):\n        if self._pre_select_folder_id is None:\n            return False\n        if self._pre_select_folder_id != self._selected_folder_id:\n            return True\n        if self._pre_select_task_name is None:\n            return False\n        return self._pre_select_task_name != self._selected_task_name\n\n    def _on_published_cancel_clicked(self):\n        folder_id = self._pre_select_folder_id\n        task_name = self._pre_select_task_name\n        representation_id = self._published_widget.get_selected_repre_id()\n        should_change_selection = self._should_set_pre_select_context()\n        self._set_select_contex_mode(False)\n        if should_change_selection:\n            self._controller.set_expected_selection(\n                folder_id, task_name, representation_id=representation_id\n            )\n\n    def _on_copy_representation_finished(self, event):\n        \"\"\"Callback for when copy representation is finished.\n\n        Make sure that select context mode is disabled when representation\n        copy is finished.\n\n        Args:\n            event (Event): Event object.\n        \"\"\"\n\n        if not event[\"failed\"]:\n            self._set_select_contex_mode(False)\n\n    def _on_workfile_save_enabled_change(self, event):\n        enabled = event[\"enabled\"]\n        self._is_save_enabled = enabled\n        self._update_published_btns_state()\n        self._update_workarea_btns_state()\n\n    def _save_changes_prompt(self):\n        \"\"\"Ask user if wants to save changes to current file.\n\n        Returns:\n            Union[bool, None]: True if user wants to save changes, False if\n                user does not want to save changes, None if user cancels\n                operation.\n        \"\"\"\n        messagebox = QtWidgets.QMessageBox(parent=self)\n        messagebox.setWindowFlags(\n            messagebox.windowFlags() | QtCore.Qt.FramelessWindowHint\n        )\n        messagebox.setIcon(QtWidgets.QMessageBox.Warning)\n        messagebox.setWindowTitle(\"Unsaved Changes!\")\n        messagebox.setText(\n            \"There are unsaved changes to the current file.\"\n            \"\\nDo you want to save the changes?\"\n        )\n        messagebox.setStandardButtons(\n            QtWidgets.QMessageBox.Yes\n            | QtWidgets.QMessageBox.No\n            | QtWidgets.QMessageBox.Cancel\n        )\n\n        result = messagebox.exec_()\n        if result == QtWidgets.QMessageBox.Yes:\n            return True\n        if result == QtWidgets.QMessageBox.No:\n            return False\n        return None\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/widgets/files_widget_published.py",
    "content": "import qtawesome\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.style import (\n    get_default_entity_icon_color,\n    get_disabled_entity_icon_color,\n)\nfrom openpype.tools.utils import TreeView\nfrom openpype.tools.utils.delegates import PrettyTimeDelegate\n\nfrom .utils import BaseOverlayFrame\n\n\nREPRE_ID_ROLE = QtCore.Qt.UserRole + 1\nFILEPATH_ROLE = QtCore.Qt.UserRole + 2\nDATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3\n\n\nclass PublishedFilesModel(QtGui.QStandardItemModel):\n    \"\"\"A model for displaying files.\n\n    Args:\n        controller (AbstractWorkfilesFrontend): The control object.\n    \"\"\"\n\n    def __init__(self, controller):\n        super(PublishedFilesModel, self).__init__()\n\n        self.setColumnCount(2)\n\n        self.setHeaderData(0, QtCore.Qt.Horizontal, \"Name\")\n        self.setHeaderData(1, QtCore.Qt.Horizontal, \"Date Modified\")\n\n        controller.register_event_callback(\n            \"selection.task.changed\",\n            self._on_task_changed\n        )\n        controller.register_event_callback(\n            \"selection.folder.changed\",\n            self._on_folder_changed\n        )\n\n        self._file_icon = qtawesome.icon(\n            \"fa.file-o\",\n            color=get_default_entity_icon_color()\n        )\n        self._controller = controller\n        self._items_by_id = {}\n        self._missing_context_item = None\n        self._missing_context_used = False\n        self._empty_root_item = None\n        self._empty_item_used = False\n\n        self._published_mode = False\n        self._context_select_mode = False\n\n        self._last_folder_id = None\n        self._last_task_id = None\n\n        self._add_empty_item()\n\n    def set_published_mode(self, published_mode):\n        if self._published_mode == published_mode:\n            return\n        self._published_mode = published_mode\n        if published_mode:\n            self._fill_items()\n        elif self._context_select_mode:\n            self.set_select_context_mode(False)\n\n    def set_select_context_mode(self, select_mode):\n        if self._context_select_mode is select_mode:\n            return\n        self._context_select_mode = select_mode\n        if not select_mode and self._published_mode:\n            self._fill_items()\n\n    def get_index_by_representation_id(self, representation_id):\n        item = self._items_by_id.get(representation_id)\n        if item is None:\n            return QtCore.QModelIndex()\n        return self.indexFromItem(item)\n\n    def refresh(self):\n        if self._published_mode:\n            self._fill_items()\n\n    def _clear_items(self):\n        self._remove_missing_context_item()\n        self._remove_empty_item()\n        if self._items_by_id:\n            root = self.invisibleRootItem()\n            root.removeRows(0, root.rowCount())\n            self._items_by_id = {}\n\n    def _get_missing_context_item(self):\n        if self._missing_context_item is None:\n            message = \"Select folder\"\n            item = QtGui.QStandardItem(message)\n            icon = qtawesome.icon(\n                \"fa.times\",\n                color=get_disabled_entity_icon_color()\n            )\n            item.setData(icon, QtCore.Qt.DecorationRole)\n            item.setFlags(QtCore.Qt.NoItemFlags)\n            item.setColumnCount(self.columnCount())\n            self._missing_context_item = item\n        return self._missing_context_item\n\n    def _add_missing_context_item(self):\n        if self._missing_context_used:\n            return\n        self._clear_items()\n        root_item = self.invisibleRootItem()\n        root_item.appendRow(self._get_missing_context_item())\n        self._missing_context_used = True\n\n    def _remove_missing_context_item(self):\n        if not self._missing_context_used:\n            return\n        root_item = self.invisibleRootItem()\n        root_item.takeRow(self._missing_context_item.row())\n        self._missing_context_used = False\n\n    def _get_empty_root_item(self):\n        if self._empty_root_item is None:\n            message = \"Didn't find any published workfiles.\"\n            item = QtGui.QStandardItem(message)\n            icon = qtawesome.icon(\n                \"fa.times\",\n                color=get_disabled_entity_icon_color()\n            )\n            item.setData(icon, QtCore.Qt.DecorationRole)\n            item.setFlags(QtCore.Qt.NoItemFlags)\n            item.setColumnCount(self.columnCount())\n            self._empty_root_item = item\n        return self._empty_root_item\n\n    def _add_empty_item(self):\n        if self._empty_item_used:\n            return\n        self._clear_items()\n        root_item = self.invisibleRootItem()\n        root_item.appendRow(self._get_empty_root_item())\n        self._empty_item_used = True\n\n    def _remove_empty_item(self):\n        if not self._empty_item_used:\n            return\n        root_item = self.invisibleRootItem()\n        root_item.takeRow(self._empty_root_item.row())\n        self._empty_item_used = False\n\n    def _on_folder_changed(self, event):\n        self._last_folder_id = event[\"folder_id\"]\n        if self._context_select_mode:\n            return\n\n        if self._published_mode:\n            self._fill_items()\n\n    def _on_task_changed(self, event):\n        self._last_folder_id = event[\"folder_id\"]\n        self._last_task_id = event[\"task_id\"]\n        if self._context_select_mode:\n            return\n\n        if self._published_mode:\n            self._fill_items()\n\n    def _fill_items(self):\n        folder_id = self._last_folder_id\n        task_id = self._last_task_id\n        if not folder_id:\n            self._add_missing_context_item()\n            return\n\n        file_items = self._controller.get_published_file_items(\n            folder_id, task_id\n        )\n        root_item = self.invisibleRootItem()\n        if not file_items:\n            self._add_empty_item()\n            return\n        self._remove_empty_item()\n        self._remove_missing_context_item()\n\n        items_to_remove = set(self._items_by_id.keys())\n        new_items = []\n        for file_item in file_items:\n            repre_id = file_item.representation_id\n            if repre_id in self._items_by_id:\n                items_to_remove.discard(repre_id)\n                item = self._items_by_id[repre_id]\n            else:\n                item = QtGui.QStandardItem()\n                new_items.append(item)\n                item.setColumnCount(self.columnCount())\n                item.setData(self._file_icon, QtCore.Qt.DecorationRole)\n                item.setData(file_item.filename, QtCore.Qt.DisplayRole)\n                item.setData(repre_id, REPRE_ID_ROLE)\n\n            if file_item.exists:\n                flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable\n            else:\n                flags = QtCore.Qt.NoItemFlags\n\n            item.setFlags(flags)\n            item.setData(file_item.filepath, FILEPATH_ROLE)\n            item.setData(file_item.modified, DATE_MODIFIED_ROLE)\n\n            self._items_by_id[repre_id] = item\n\n        if new_items:\n            root_item.appendRows(new_items)\n\n        for repre_id in items_to_remove:\n            item = self._items_by_id.pop(repre_id)\n            root_item.removeRow(item.row())\n\n        if root_item.rowCount() == 0:\n            self._add_empty_item()\n\n    def flags(self, index):\n        # Use flags of first column for all columns\n        if index.column() != 0:\n            index = self.index(index.row(), 0, index.parent())\n        return super(PublishedFilesModel, self).flags(index)\n\n    def data(self, index, role=None):\n        if role is None:\n            role = QtCore.Qt.DisplayRole\n\n        # Handle roles for first column\n        if index.column() == 1:\n            if role == QtCore.Qt.DecorationRole:\n                return None\n\n            if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):\n                role = DATE_MODIFIED_ROLE\n            index = self.index(index.row(), 0, index.parent())\n\n        return super(PublishedFilesModel, self).data(index, role)\n\n\nclass SelectContextOverlay(BaseOverlayFrame):\n    \"\"\"Overlay for files view when user should select context.\n\n    Todos:\n        The look of this overlay should be improved, it is \"not nice\" now.\n    \"\"\"\n\n    def __init__(self, parent):\n        super(SelectContextOverlay, self).__init__(parent)\n\n        label_widget = QtWidgets.QLabel(\n            \"Please choose context on the left<br/>&lt\",\n            self\n        )\n        label_widget.setAlignment(QtCore.Qt.AlignCenter)\n        label_widget.setObjectName(\"OverlayFrameLabel\")\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter)\n\n        label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n\nclass PublishedFilesWidget(QtWidgets.QWidget):\n    \"\"\"Published workfiles widget.\n\n    Args:\n        controller (AbstractWorkfilesFrontend): The control object.\n        parent (QtWidgets.QWidget): The parent widget.\n    \"\"\"\n\n    selection_changed = QtCore.Signal()\n    save_as_requested = QtCore.Signal()\n\n    def __init__(self, controller, parent):\n        super(PublishedFilesWidget, self).__init__(parent)\n\n        view = TreeView(self)\n        view.setSortingEnabled(True)\n        view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        # Smaller indentation\n        view.setIndentation(0)\n\n        model = PublishedFilesModel(controller)\n        proxy_model = QtCore.QSortFilterProxyModel()\n        proxy_model.setSourceModel(model)\n        proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n        proxy_model.setDynamicSortFilter(True)\n\n        view.setModel(proxy_model)\n\n        time_delegate = PrettyTimeDelegate()\n        view.setItemDelegateForColumn(1, time_delegate)\n\n        # Default to a wider first filename column it is what we mostly care\n        # about and the date modified is relatively small anyway.\n        view.setColumnWidth(0, 330)\n\n        select_overlay = SelectContextOverlay(view)\n        select_overlay.setVisible(False)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(view, 1)\n\n        selection_model = view.selectionModel()\n        selection_model.selectionChanged.connect(self._on_selection_change)\n        view.double_clicked.connect(self._on_mouse_double_click)\n\n        controller.register_event_callback(\n            \"expected_selection_changed\",\n            self._on_expected_selection_change\n        )\n\n        self._view = view\n        self._select_overlay = select_overlay\n        self._model = model\n        self._proxy_model = proxy_model\n        self._time_delegate = time_delegate\n        self._controller = controller\n\n    def set_published_mode(self, published_mode):\n        self._model.set_published_mode(published_mode)\n\n    def set_select_context_mode(self, select_mode):\n        self._model.set_select_context_mode(select_mode)\n        self._select_overlay.setVisible(select_mode)\n\n    def set_text_filter(self, text_filter):\n        self._proxy_model.setFilterFixedString(text_filter)\n\n    def get_selected_repre_info(self):\n        selection_model = self._view.selectionModel()\n        representation_id = None\n        filepath = None\n        for index in selection_model.selectedIndexes():\n            representation_id = index.data(REPRE_ID_ROLE)\n            filepath = index.data(FILEPATH_ROLE)\n\n        return {\n            \"representation_id\": representation_id,\n            \"filepath\": filepath,\n        }\n\n    def get_selected_repre_id(self):\n        return self.get_selected_repre_info()[\"representation_id\"]\n\n    def _on_selection_change(self):\n        repre_id = self.get_selected_repre_id()\n        self._controller.set_selected_representation_id(repre_id)\n\n    def _on_mouse_double_click(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self.save_as_requested.emit()\n\n    def _on_expected_selection_change(self, event):\n        repre_info = event[\"representation\"]\n        if not repre_info[\"current\"]:\n            return\n\n        self._model.refresh()\n\n        representation_id = repre_info[\"id\"]\n        selected_repre_id = self.get_selected_repre_id()\n        if (\n            representation_id is not None\n            and representation_id != selected_repre_id\n        ):\n            index = self._model.get_index_by_representation_id(\n                representation_id)\n            if index.isValid():\n                proxy_index = self._proxy_model.mapFromSource(index)\n                self._view.setCurrentIndex(proxy_index)\n\n        self._controller.expected_representation_selected(\n            event[\"folder\"][\"id\"], event[\"task\"][\"name\"], representation_id\n        )\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py",
    "content": "import qtawesome\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.style import (\n    get_default_entity_icon_color,\n    get_disabled_entity_icon_color,\n)\nfrom openpype.tools.utils import TreeView\nfrom openpype.tools.utils.delegates import PrettyTimeDelegate\n\nFILENAME_ROLE = QtCore.Qt.UserRole + 1\nFILEPATH_ROLE = QtCore.Qt.UserRole + 2\nDATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3\n\n\nclass WorkAreaFilesModel(QtGui.QStandardItemModel):\n    \"\"\"A model for workare workfiles.\n\n    Args:\n        controller (AbstractWorkfilesFrontend): The control object.\n    \"\"\"\n\n    def __init__(self, controller):\n        super(WorkAreaFilesModel, self).__init__()\n\n        self.setColumnCount(2)\n\n        self.setHeaderData(0, QtCore.Qt.Horizontal, \"Name\")\n        self.setHeaderData(1, QtCore.Qt.Horizontal, \"Date Modified\")\n\n        controller.register_event_callback(\n            \"selection.folder.changed\",\n            self._on_folder_changed\n        )\n        controller.register_event_callback(\n            \"selection.task.changed\",\n            self._on_task_changed\n        )\n        controller.register_event_callback(\n            \"workfile_duplicate.finished\",\n            self._on_duplicate_finished\n        )\n        controller.register_event_callback(\n            \"save_as.finished\",\n            self._on_save_as_finished\n        )\n\n        self._file_icon = qtawesome.icon(\n            \"fa.file-o\",\n            color=get_default_entity_icon_color()\n        )\n        self._controller = controller\n        self._items_by_filename = {}\n        self._missing_context_item = None\n        self._missing_context_used = False\n        self._empty_root_item = None\n        self._empty_item_used = False\n        self._published_mode = False\n        self._selected_folder_id = None\n        self._selected_task_id = None\n\n        self._add_missing_context_item()\n\n    def get_index_by_filename(self, filename):\n        item = self._items_by_filename.get(filename)\n        if item is None:\n            return QtCore.QModelIndex()\n        return self.indexFromItem(item)\n\n    def refresh(self):\n        if not self._published_mode:\n            self._fill_items()\n\n    def _get_missing_context_item(self):\n        if self._missing_context_item is None:\n            message = \"Select folder and task\"\n            item = QtGui.QStandardItem(message)\n            icon = qtawesome.icon(\n                \"fa.times\",\n                color=get_disabled_entity_icon_color()\n            )\n            item.setData(icon, QtCore.Qt.DecorationRole)\n            item.setFlags(QtCore.Qt.NoItemFlags)\n            item.setColumnCount(self.columnCount())\n            self._missing_context_item = item\n        return self._missing_context_item\n\n    def _clear_items(self):\n        self._remove_missing_context_item()\n        self._remove_empty_item()\n        if self._items_by_filename:\n            root = self.invisibleRootItem()\n            root.removeRows(0, root.rowCount())\n            self._items_by_filename = {}\n\n    def _add_missing_context_item(self):\n        if self._missing_context_used:\n            return\n        self._clear_items()\n        root_item = self.invisibleRootItem()\n        root_item.appendRow(self._get_missing_context_item())\n        self._missing_context_used = True\n\n    def _remove_missing_context_item(self):\n        if not self._missing_context_used:\n            return\n        root_item = self.invisibleRootItem()\n        root_item.takeRow(self._missing_context_item.row())\n        self._missing_context_used = False\n\n    def _get_empty_root_item(self):\n        if self._empty_root_item is None:\n            message = \"Work Area is empty..\"\n            item = QtGui.QStandardItem(message)\n            icon = qtawesome.icon(\n                \"fa.exclamation-circle\",\n                color=get_disabled_entity_icon_color()\n            )\n            item.setData(icon, QtCore.Qt.DecorationRole)\n            item.setFlags(QtCore.Qt.NoItemFlags)\n            item.setColumnCount(self.columnCount())\n            self._empty_root_item = item\n        return self._empty_root_item\n\n    def _add_empty_item(self):\n        if self._empty_item_used:\n            return\n        self._clear_items()\n        root_item = self.invisibleRootItem()\n        root_item.appendRow(self._get_empty_root_item())\n        self._empty_item_used = True\n\n    def _remove_empty_item(self):\n        if not self._empty_item_used:\n            return\n        root_item = self.invisibleRootItem()\n        root_item.takeRow(self._empty_root_item.row())\n        self._empty_item_used = False\n\n    def _on_folder_changed(self, event):\n        self._selected_folder_id = event[\"folder_id\"]\n        if not self._published_mode:\n            self._fill_items()\n\n    def _on_task_changed(self, event):\n        self._selected_folder_id = event[\"folder_id\"]\n        self._selected_task_id = event[\"task_id\"]\n        if not self._published_mode:\n            self._fill_items()\n\n    def _on_duplicate_finished(self, event):\n        if event[\"failed\"]:\n            return\n\n        if not self._published_mode:\n            self._fill_items()\n\n    def _on_save_as_finished(self, event):\n        if event[\"failed\"]:\n            return\n\n        if not self._published_mode:\n            self._fill_items()\n\n    def _fill_items(self):\n        folder_id = self._selected_folder_id\n        task_id = self._selected_task_id\n        if not folder_id or not task_id:\n            self._add_missing_context_item()\n            return\n\n        file_items = self._controller.get_workarea_file_items(\n            folder_id, task_id\n        )\n        root_item = self.invisibleRootItem()\n        if not file_items:\n            self._add_empty_item()\n            return\n        self._remove_empty_item()\n        self._remove_missing_context_item()\n\n        items_to_remove = set(self._items_by_filename.keys())\n        new_items = []\n        for file_item in file_items:\n            filename = file_item.filename\n            if filename in self._items_by_filename:\n                items_to_remove.discard(filename)\n                item = self._items_by_filename[filename]\n            else:\n                item = QtGui.QStandardItem()\n                new_items.append(item)\n                item.setColumnCount(self.columnCount())\n                item.setFlags(\n                    QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable\n                )\n                item.setData(self._file_icon, QtCore.Qt.DecorationRole)\n                item.setData(file_item.filename, QtCore.Qt.DisplayRole)\n                item.setData(file_item.filename, FILENAME_ROLE)\n\n            item.setData(file_item.filepath, FILEPATH_ROLE)\n            item.setData(file_item.modified, DATE_MODIFIED_ROLE)\n\n            self._items_by_filename[file_item.filename] = item\n\n        if new_items:\n            root_item.appendRows(new_items)\n\n        for filename in items_to_remove:\n            item = self._items_by_filename.pop(filename)\n            root_item.removeRow(item.row())\n\n        if root_item.rowCount() == 0:\n            self._add_empty_item()\n\n    def flags(self, index):\n        # Use flags of first column for all columns\n        if index.column() != 0:\n            index = self.index(index.row(), 0, index.parent())\n        return super(WorkAreaFilesModel, self).flags(index)\n\n    def data(self, index, role=None):\n        if role is None:\n            role = QtCore.Qt.DisplayRole\n\n        # Handle roles for first column\n        if index.column() == 1:\n            if role == QtCore.Qt.DecorationRole:\n                return None\n\n            if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):\n                role = DATE_MODIFIED_ROLE\n            index = self.index(index.row(), 0, index.parent())\n\n        return super(WorkAreaFilesModel, self).data(index, role)\n\n    def set_published_mode(self, published_mode):\n        if self._published_mode == published_mode:\n            return\n        self._published_mode = published_mode\n        if not published_mode:\n            self._fill_items()\n\n\nclass WorkAreaFilesWidget(QtWidgets.QWidget):\n    \"\"\"Workarea files widget.\n\n    Args:\n        controller (AbstractWorkfilesFrontend): The control object.\n        parent (QtWidgets.QWidget): The parent widget.\n    \"\"\"\n\n    selection_changed = QtCore.Signal()\n    open_current_requested = QtCore.Signal()\n    duplicate_requested = QtCore.Signal()\n\n    def __init__(self, controller, parent):\n        super(WorkAreaFilesWidget, self).__init__(parent)\n\n        view = TreeView(self)\n        view.setSortingEnabled(True)\n        view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        # Smaller indentation\n        view.setIndentation(0)\n\n        model = WorkAreaFilesModel(controller)\n        proxy_model = QtCore.QSortFilterProxyModel()\n        proxy_model.setSourceModel(model)\n        proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n        proxy_model.setDynamicSortFilter(True)\n\n        view.setModel(proxy_model)\n\n        time_delegate = PrettyTimeDelegate()\n        view.setItemDelegateForColumn(1, time_delegate)\n\n        # Default to a wider first filename column it is what we mostly care\n        # about and the date modified is relatively small anyway.\n        view.setColumnWidth(0, 330)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(view, 1)\n\n        selection_model = view.selectionModel()\n        selection_model.selectionChanged.connect(self._on_selection_change)\n        view.double_clicked.connect(self._on_mouse_double_click)\n        view.customContextMenuRequested.connect(self._on_context_menu)\n\n        controller.register_event_callback(\n            \"expected_selection_changed\",\n            self._on_expected_selection_change\n        )\n\n        self._view = view\n        self._model = model\n        self._proxy_model = proxy_model\n        self._time_delegate = time_delegate\n        self._controller = controller\n\n        self._published_mode = False\n\n    def set_published_mode(self, published_mode):\n        \"\"\"Set the published mode.\n\n        Widget should ignore most of events when in published mode is enabled.\n\n        Args:\n            published_mode (bool): The published mode.\n        \"\"\"\n\n        self._model.set_published_mode(published_mode)\n        self._published_mode = published_mode\n\n    def set_text_filter(self, text_filter):\n        \"\"\"Set the text filter.\n\n        Args:\n            text_filter (str): The text filter.\n        \"\"\"\n\n        self._proxy_model.setFilterFixedString(text_filter)\n\n    def _get_selected_info(self):\n        selection_model = self._view.selectionModel()\n        filepath = None\n        filename = None\n        for index in selection_model.selectedIndexes():\n            filepath = index.data(FILEPATH_ROLE)\n            filename = index.data(FILENAME_ROLE)\n        return {\n            \"filepath\": filepath,\n            \"filename\": filename,\n        }\n\n    def get_selected_path(self):\n        \"\"\"Selected filepath.\n\n        Returns:\n            Union[str, None]: The selected filepath or None if nothing is\n                selected.\n        \"\"\"\n        return self._get_selected_info()[\"filepath\"]\n\n    def _on_selection_change(self):\n        filepath = self.get_selected_path()\n        self._controller.set_selected_workfile_path(filepath)\n\n    def _on_mouse_double_click(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self.open_current_requested.emit()\n\n    def _on_context_menu(self, point):\n        index = self._view.indexAt(point)\n        if not index.isValid():\n            return\n\n        if not index.flags() & QtCore.Qt.ItemIsEnabled:\n            return\n\n        menu = QtWidgets.QMenu(self)\n\n        # Duplicate\n        action = QtWidgets.QAction(\"Duplicate\", menu)\n        tip = \"Duplicate selected file.\"\n        action.setToolTip(tip)\n        action.setStatusTip(tip)\n        action.triggered.connect(self._on_duplicate_pressed)\n        menu.addAction(action)\n\n        # Show the context action menu\n        global_point = self._view.mapToGlobal(point)\n        _ = menu.exec_(global_point)\n\n    def _on_duplicate_pressed(self):\n        self.duplicate_requested.emit()\n\n    def _on_expected_selection_change(self, event):\n        workfile_info = event[\"workfile\"]\n        if not workfile_info[\"current\"]:\n            return\n\n        self._model.refresh()\n\n        workfile_name = workfile_info[\"name\"]\n        if (\n            workfile_name is not None\n            and workfile_name != self._get_selected_info()[\"filename\"]\n        ):\n            index = self._model.get_index_by_filename(workfile_name)\n            if index.isValid():\n                proxy_index = self._proxy_model.mapFromSource(index)\n                self._view.setCurrentIndex(proxy_index)\n\n        self._controller.expected_workfile_selected(\n            event[\"folder\"][\"id\"], event[\"task\"][\"name\"], workfile_name\n        )\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/widgets/save_as_dialog.py",
    "content": "from qtpy import QtWidgets, QtCore\n\nfrom openpype.tools.utils import PlaceholderLineEdit\n\n\nclass SubversionLineEdit(QtWidgets.QWidget):\n    \"\"\"QLineEdit with QPushButton for drop down selection of list of strings\"\"\"\n\n    text_changed = QtCore.Signal(str)\n\n    def __init__(self, *args, **kwargs):\n        super(SubversionLineEdit, self).__init__(*args, **kwargs)\n\n        input_field = PlaceholderLineEdit(self)\n        menu_btn = QtWidgets.QPushButton(self)\n        menu_btn.setFixedWidth(18)\n\n        menu = QtWidgets.QMenu(self)\n        menu_btn.setMenu(menu)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(3)\n\n        layout.addWidget(input_field, 1)\n        layout.addWidget(menu_btn, 0)\n\n        input_field.textChanged.connect(self.text_changed)\n\n        self.setFocusProxy(input_field)\n\n        self._input_field = input_field\n        self._menu_btn = menu_btn\n        self._menu = menu\n\n    def set_placeholder(self, placeholder):\n        self._input_field.setPlaceholderText(placeholder)\n\n    def set_text(self, text):\n        self._input_field.setText(text)\n\n    def set_values(self, values):\n        self._update(values)\n\n    def _on_button_clicked(self):\n        self._menu.exec_()\n\n    def _on_action_clicked(self, action):\n        self._input_field.setText(action.text())\n\n    def _update(self, values):\n        \"\"\"Create optional predefined subset names\n\n        Args:\n            default_names(list): all predefined names\n\n        Returns:\n             None\n        \"\"\"\n\n        menu = self._menu\n        button = self._menu_btn\n\n        state = any(values)\n        button.setEnabled(state)\n        if state is False:\n            return\n\n        # Include an empty string\n        values = [\"\"] + sorted(values)\n\n        # Get and destroy the action group\n        group = button.findChild(QtWidgets.QActionGroup)\n        if group:\n            group.deleteLater()\n\n        # Build new action group\n        group = QtWidgets.QActionGroup(button)\n        for name in values:\n            action = group.addAction(name)\n            menu.addAction(action)\n\n        group.triggered.connect(self._on_action_clicked)\n\n\nclass SaveAsDialog(QtWidgets.QDialog):\n    \"\"\"Save as dialog to define a unique filename inside workdir.\n\n    The filename is calculated in controller where UI sends values from\n    dialog inputs.\n\n    Args:\n        controller (AbstractWorkfilesFrontend): The control object.\n    \"\"\"\n\n    def __init__(self, controller, parent):\n        super(SaveAsDialog, self).__init__(parent=parent)\n        self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)\n\n        self._controller = controller\n\n        self._folder_id = None\n        self._task_id = None\n        self._last_version = None\n        self._template_key = None\n        self._comment_value = None\n        self._version_value = None\n        self._ext_value = None\n        self._filename = None\n        self._workdir = None\n\n        self._result = None\n\n        # Btns widget\n        btns_widget = QtWidgets.QWidget(self)\n\n        btn_ok = QtWidgets.QPushButton(\"Ok\", btns_widget)\n        btn_cancel = QtWidgets.QPushButton(\"Cancel\", btns_widget)\n\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.addWidget(btn_ok)\n        btns_layout.addWidget(btn_cancel)\n\n        # Inputs widget\n        inputs_widget = QtWidgets.QWidget(self)\n\n        # Version widget\n        version_widget = QtWidgets.QWidget(inputs_widget)\n\n        # Version number input\n        version_input = QtWidgets.QSpinBox(version_widget)\n        version_input.setMinimum(1)\n        version_input.setMaximum(9999)\n\n        # Last version checkbox\n        last_version_check = QtWidgets.QCheckBox(\n            \"Next Available Version\", version_widget\n        )\n        last_version_check.setChecked(True)\n\n        version_layout = QtWidgets.QHBoxLayout(version_widget)\n        version_layout.setContentsMargins(0, 0, 0, 0)\n        version_layout.addWidget(version_input)\n        version_layout.addWidget(last_version_check)\n\n        # Preview widget\n        preview_widget = QtWidgets.QLabel(\"Preview filename\", inputs_widget)\n        preview_widget.setWordWrap(True)\n\n        # Subversion input\n        subversion_input = SubversionLineEdit(inputs_widget)\n        subversion_input.set_placeholder(\"Will be part of filename.\")\n\n        # Extensions combobox\n        extension_combobox = QtWidgets.QComboBox(inputs_widget)\n        # Add styled delegate to use stylesheets\n        extension_delegate = QtWidgets.QStyledItemDelegate()\n        extension_combobox.setItemDelegate(extension_delegate)\n\n        version_label = QtWidgets.QLabel(\"Version:\", inputs_widget)\n        subversion_label = QtWidgets.QLabel(\"Subversion:\", inputs_widget)\n        extension_label = QtWidgets.QLabel(\"Extension:\", inputs_widget)\n        preview_label = QtWidgets.QLabel(\"Preview:\", inputs_widget)\n\n        # Build inputs\n        inputs_layout = QtWidgets.QGridLayout(inputs_widget)\n        inputs_layout.addWidget(version_label, 0, 0)\n        inputs_layout.addWidget(version_widget, 0, 1)\n        inputs_layout.addWidget(subversion_label, 1, 0)\n        inputs_layout.addWidget(subversion_input, 1, 1)\n        inputs_layout.addWidget(extension_label, 2, 0)\n        inputs_layout.addWidget(extension_combobox, 2, 1)\n        inputs_layout.addWidget(preview_label, 3, 0)\n        inputs_layout.addWidget(preview_widget, 3, 1)\n\n        # Build layout\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(inputs_widget)\n        main_layout.addWidget(btns_widget)\n\n        # Signal callback registration\n        version_input.valueChanged.connect(self._on_version_spinbox_change)\n        last_version_check.stateChanged.connect(\n            self._on_version_checkbox_change\n        )\n\n        subversion_input.text_changed.connect(self._on_comment_change)\n        extension_combobox.currentIndexChanged.connect(\n            self._on_extension_change)\n\n        btn_ok.pressed.connect(self._on_ok_pressed)\n        btn_cancel.pressed.connect(self._on_cancel_pressed)\n\n        # Store objects\n        self._inputs_layout = inputs_layout\n\n        self._btn_ok = btn_ok\n        self._btn_cancel = btn_cancel\n\n        self._version_widget = version_widget\n\n        self._version_input = version_input\n        self._last_version_check = last_version_check\n\n        self._extension_delegate = extension_delegate\n        self._extension_combobox = extension_combobox\n        self._subversion_input = subversion_input\n        self._preview_widget = preview_widget\n\n        self._version_label = version_label\n        self._subversion_label = subversion_label\n        self._extension_label = extension_label\n        self._preview_label = preview_label\n\n        # Post init setup\n\n        # Allow \"Enter\" key to accept the save.\n        btn_ok.setDefault(True)\n\n        # Disable version input if last version is checked\n        version_input.setEnabled(not last_version_check.isChecked())\n\n        # Force default focus to comment, some hosts didn't automatically\n        # apply focus to this line edit (e.g. Houdini)\n        subversion_input.setFocus()\n\n    def get_result(self):\n        return self._result\n\n    def update_context(self):\n        # Add version only if template contains version key\n        # - since the version can be padded with \"{version:0>4}\" we only search\n        #   for \"{version\".\n        selected_context = self._controller.get_selected_context()\n        folder_id = selected_context[\"folder_id\"]\n        task_id = selected_context[\"task_id\"]\n        data = self._controller.get_workarea_save_as_data(folder_id, task_id)\n        last_version = data[\"last_version\"]\n        comment = data[\"comment\"]\n        comment_hints = data[\"comment_hints\"]\n\n        template_has_version = data[\"template_has_version\"]\n        template_has_comment = data[\"template_has_comment\"]\n\n        self._folder_id = folder_id\n        self._task_id = task_id\n        self._workdir = data[\"workdir\"]\n        self._comment_value = data[\"comment\"]\n        self._ext_value = data[\"ext\"]\n        self._template_key = data[\"template_key\"]\n        self._last_version = data[\"last_version\"]\n\n        self._extension_combobox.clear()\n        self._extension_combobox.addItems(data[\"extensions\"])\n\n        self._version_input.setValue(last_version)\n\n        vw_idx = self._inputs_layout.indexOf(self._version_widget)\n        self._version_label.setVisible(template_has_version)\n        self._version_widget.setVisible(template_has_version)\n        if template_has_version:\n            if vw_idx == -1:\n                self._inputs_layout.addWidget(self._version_label, 0, 0)\n                self._inputs_layout.addWidget(self._version_widget, 0, 1)\n        elif vw_idx != -1:\n            self._inputs_layout.takeAt(vw_idx)\n            self._inputs_layout.takeAt(\n                self._inputs_layout.indexOf(self._version_label)\n            )\n\n        cw_idx = self._inputs_layout.indexOf(self._subversion_input)\n        self._subversion_label.setVisible(template_has_comment)\n        self._subversion_input.setVisible(template_has_comment)\n        if template_has_comment:\n            if cw_idx == -1:\n                self._inputs_layout.addWidget(self._subversion_label, 1, 0)\n                self._inputs_layout.addWidget(self._subversion_input, 1, 1)\n        elif cw_idx != -1:\n            self._inputs_layout.takeAt(cw_idx)\n            self._inputs_layout.takeAt(\n                self._inputs_layout.indexOf(self._subversion_label)\n            )\n\n        if template_has_comment:\n            self._subversion_input.set_text(comment or \"\")\n            self._subversion_input.set_values(comment_hints)\n        self._update_filename()\n\n    def _on_version_spinbox_change(self, value):\n        if value == self._version_value:\n            return\n        self._version_value = value\n        if not self._last_version_check.isChecked():\n            self._update_filename()\n\n    def _on_version_checkbox_change(self):\n        use_last_version = self._last_version_check.isChecked()\n        self._version_input.setEnabled(not use_last_version)\n        if use_last_version:\n            self._version_input.blockSignals(True)\n            self._version_input.setValue(self._last_version)\n            self._version_input.blockSignals(False)\n        self._update_filename()\n\n    def _on_comment_change(self, text):\n        if self._comment_value == text:\n            return\n        self._comment_value = text\n        self._update_filename()\n\n    def _on_extension_change(self):\n        ext = self._extension_combobox.currentText()\n        if ext == self._ext_value:\n            return\n        self._ext_value = ext\n        self._update_filename()\n\n    def _on_ok_pressed(self):\n        self._result = {\n            \"filename\": self._filename,\n            \"workdir\": self._workdir,\n            \"folder_id\": self._folder_id,\n            \"task_id\": self._task_id,\n            \"template_key\": self._template_key,\n        }\n        self.close()\n\n    def _on_cancel_pressed(self):\n        self.close()\n\n    def _update_filename(self):\n        result = self._controller.fill_workarea_filepath(\n            self._folder_id,\n            self._task_id,\n            self._ext_value,\n            self._last_version_check.isChecked(),\n            self._version_value,\n            self._comment_value,\n        )\n        self._filename = result.filename\n        self._btn_ok.setEnabled(not result.exists)\n\n        if result.exists:\n            self._preview_widget.setText((\n                \"<font color='red'>Cannot create \\\"{}\\\" because file exists!\"\n                \"</font>\"\n            ).format(result.filename))\n        else:\n            self._preview_widget.setText(\n                \"<font color='green'>{}</font>\".format(result.filename)\n            )\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/widgets/side_panel.py",
    "content": "import datetime\n\nfrom qtpy import QtWidgets, QtCore\n\n\ndef file_size_to_string(file_size):\n    size = 0\n    size_ending_mapping = {\n        \"KB\": 1024 ** 1,\n        \"MB\": 1024 ** 2,\n        \"GB\": 1024 ** 3\n    }\n    ending = \"B\"\n    for _ending, _size in size_ending_mapping.items():\n        if file_size < _size:\n            break\n        size = file_size / _size\n        ending = _ending\n    return \"{:.2f} {}\".format(size, ending)\n\n\nclass SidePanelWidget(QtWidgets.QWidget):\n    \"\"\"Details about selected workfile.\n\n    Todos:\n        At this moment only shows created and modified date of file\n            or its size.\n\n    Args:\n        controller (AbstractWorkfilesFrontend): The control object.\n        parent (QtWidgets.QWidget): The parent widget.\n    \"\"\"\n\n    published_workfile_message = (\n        \"<b>INFO</b>: Opened published workfiles will be stored in\"\n        \" temp directory on your machine. Current temp size: <b>{}</b>.\"\n    )\n\n    def __init__(self, controller, parent):\n        super(SidePanelWidget, self).__init__(parent)\n\n        details_label = QtWidgets.QLabel(\"Details\", self)\n        details_input = QtWidgets.QPlainTextEdit(self)\n        details_input.setReadOnly(True)\n\n        artist_note_widget = QtWidgets.QWidget(self)\n        note_label = QtWidgets.QLabel(\"Artist note\", artist_note_widget)\n        note_input = QtWidgets.QPlainTextEdit(artist_note_widget)\n        btn_note_save = QtWidgets.QPushButton(\"Save note\", artist_note_widget)\n\n        artist_note_layout = QtWidgets.QVBoxLayout(artist_note_widget)\n        artist_note_layout.setContentsMargins(0, 0, 0, 0)\n        artist_note_layout.addWidget(note_label, 0)\n        artist_note_layout.addWidget(note_input, 1)\n        artist_note_layout.addWidget(\n            btn_note_save, 0, alignment=QtCore.Qt.AlignRight\n        )\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(details_label, 0)\n        main_layout.addWidget(details_input, 1)\n        main_layout.addWidget(artist_note_widget, 1)\n\n        note_input.textChanged.connect(self._on_note_change)\n        btn_note_save.clicked.connect(self._on_save_click)\n\n        controller.register_event_callback(\n            \"selection.workarea.changed\", self._on_selection_change\n        )\n\n        self._details_input = details_input\n        self._artist_note_widget = artist_note_widget\n        self._note_input = note_input\n        self._btn_note_save = btn_note_save\n\n        self._folder_id = None\n        self._task_id = None\n        self._filepath = None\n        self._orig_note = \"\"\n        self._controller = controller\n\n        self._set_context(None, None, None)\n\n    def set_published_mode(self, published_mode):\n        \"\"\"Change published mode.\n\n        Args:\n            published_mode (bool): Published mode enabled.\n        \"\"\"\n\n        self._artist_note_widget.setVisible(not published_mode)\n\n    def _on_selection_change(self, event):\n        folder_id = event[\"folder_id\"]\n        task_id = event[\"task_id\"]\n        filepath = event[\"path\"]\n\n        self._set_context(folder_id, task_id, filepath)\n\n    def _on_note_change(self):\n        text = self._note_input.toPlainText()\n        self._btn_note_save.setEnabled(self._orig_note != text)\n\n    def _on_save_click(self):\n        note = self._note_input.toPlainText()\n        self._controller.save_workfile_info(\n            self._folder_id,\n            self._task_id,\n            self._filepath,\n            note\n        )\n        self._orig_note = note\n        self._btn_note_save.setEnabled(False)\n\n    def _set_context(self, folder_id, task_id, filepath):\n        workfile_info = None\n        # Check if folder, task and file are selected\n        if bool(folder_id) and bool(task_id) and bool(filepath):\n            workfile_info = self._controller.get_workfile_info(\n                folder_id, task_id, filepath\n            )\n        enabled = workfile_info is not None\n\n        self._details_input.setEnabled(enabled)\n        self._note_input.setEnabled(enabled)\n        self._btn_note_save.setEnabled(enabled)\n\n        self._folder_id = folder_id\n        self._task_id = task_id\n        self._filepath = filepath\n\n        # Disable inputs and remove texts if any required arguments are\n        #   missing\n        if not enabled:\n            self._orig_note = \"\"\n            self._details_input.setPlainText(\"\")\n            self._note_input.setPlainText(\"\")\n            return\n\n        note = workfile_info.note\n        size_value = file_size_to_string(workfile_info.filesize)\n\n        # Append html string\n        datetime_format = \"%b %d %Y %H:%M:%S\"\n        creation_time = datetime.datetime.fromtimestamp(\n            workfile_info.creation_time)\n        modification_time = datetime.datetime.fromtimestamp(\n            workfile_info.modification_time)\n        lines = (\n            \"<b>Size:</b>\",\n            size_value,\n            \"<b>Created:</b>\",\n            creation_time.strftime(datetime_format),\n            \"<b>Modified:</b>\",\n            modification_time.strftime(datetime_format)\n        )\n        self._orig_note = note\n        self._note_input.setPlainText(note)\n\n        # Set as empty string\n        self._details_input.setPlainText(\"\")\n        self._details_input.appendHtml(\"<br>\".join(lines))\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/widgets/utils.py",
    "content": "from qtpy import QtWidgets, QtCore\n\n\nclass BaseOverlayFrame(QtWidgets.QFrame):\n    \"\"\"Base frame for overlay widgets.\n\n    Has implemented automated resize and event filtering.\n    \"\"\"\n\n    def __init__(self, parent):\n        super(BaseOverlayFrame, self).__init__(parent)\n        self.setObjectName(\"OverlayFrame\")\n\n        self._parent = parent\n\n    def setVisible(self, visible):\n        super(BaseOverlayFrame, self).setVisible(visible)\n        if visible:\n            self._parent.installEventFilter(self)\n            self.resize(self._parent.size())\n        else:\n            self._parent.removeEventFilter(self)\n\n    def eventFilter(self, obj, event):\n        if event.type() == QtCore.QEvent.Resize:\n            self.resize(obj.size())\n\n        return super(BaseOverlayFrame, self).eventFilter(obj, event)\n"
  },
  {
    "path": "openpype/tools/ayon_workfiles/widgets/window.py",
    "content": "from qtpy import QtCore, QtWidgets, QtGui\n\nfrom openpype import style, resources\nfrom openpype.tools.utils import (\n    PlaceholderLineEdit,\n    MessageOverlayObject,\n)\n\nfrom openpype.tools.ayon_utils.widgets import FoldersWidget, TasksWidget\nfrom openpype.tools.ayon_workfiles.control import BaseWorkfileController\nfrom openpype.tools.utils import GoToCurrentButton, RefreshButton\n\nfrom .side_panel import SidePanelWidget\nfrom .files_widget import FilesWidget\nfrom .utils import BaseOverlayFrame\n\n\nclass InvalidHostOverlay(BaseOverlayFrame):\n    def __init__(self, parent):\n        super(InvalidHostOverlay, self).__init__(parent)\n\n        label_widget = QtWidgets.QLabel(\n            (\n                \"Workfiles tool is not supported in this host/DCCs.\"\n                \"<br/><br/>This may be caused by a bug.\"\n                \" Please contact your TD for more information.\"\n            ),\n            self\n        )\n        label_widget.setAlignment(QtCore.Qt.AlignCenter)\n        label_widget.setObjectName(\"OverlayFrameLabel\")\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addStretch(2)\n        layout.addWidget(label_widget, 0, QtCore.Qt.AlignCenter)\n        layout.addStretch(3)\n\n        label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n\nclass WorkfilesToolWindow(QtWidgets.QWidget):\n    \"\"\"WorkFiles Window.\n\n    Main windows of workfiles tool.\n\n    Args:\n        controller (AbstractWorkfilesFrontend): Frontend controller.\n        parent (Optional[QtWidgets.QWidget]): Parent widget.\n    \"\"\"\n\n    title = \"Work Files\"\n\n    def __init__(self, controller=None, parent=None):\n        super(WorkfilesToolWindow, self).__init__(parent=parent)\n\n        if controller is None:\n            controller = BaseWorkfileController()\n\n        self.setWindowTitle(self.title)\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n        flags = self.windowFlags() | QtCore.Qt.Window\n        self.setWindowFlags(flags)\n\n        self._default_window_flags = flags\n\n        self._folders_widget = None\n        self._folder_filter_input = None\n\n        self._files_widget = None\n\n        self._first_show = True\n        self._controller_refreshed = False\n        self._context_to_set = None\n        # Host validation should happen only once\n        self._host_is_valid = None\n\n        self._controller = controller\n\n        # Create pages widget and set it as central widget\n        pages_widget = QtWidgets.QStackedWidget(self)\n\n        home_page_widget = QtWidgets.QWidget(pages_widget)\n        home_body_widget = QtWidgets.QWidget(home_page_widget)\n\n        col_1_widget = self._create_col_1_widget(controller, parent)\n        tasks_widget = TasksWidget(\n            controller, home_body_widget, handle_expected_selection=True\n        )\n        col_3_widget = self._create_col_3_widget(controller, home_body_widget)\n        side_panel = SidePanelWidget(controller, home_body_widget)\n\n        pages_widget.addWidget(home_page_widget)\n\n        # Build home\n        home_page_layout = QtWidgets.QVBoxLayout(home_page_widget)\n        home_page_layout.addWidget(home_body_widget)\n\n        # Build home - body\n        body_layout = QtWidgets.QVBoxLayout(home_body_widget)\n        split_widget = QtWidgets.QSplitter(home_body_widget)\n        split_widget.addWidget(col_1_widget)\n        split_widget.addWidget(tasks_widget)\n        split_widget.addWidget(col_3_widget)\n        split_widget.addWidget(side_panel)\n        split_widget.setSizes([255, 160, 455, 175])\n\n        body_layout.addWidget(split_widget)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.addWidget(pages_widget, 1)\n\n        overlay_messages_widget = MessageOverlayObject(self)\n        overlay_invalid_host = InvalidHostOverlay(self)\n        overlay_invalid_host.setVisible(False)\n\n        first_show_timer = QtCore.QTimer()\n        first_show_timer.setSingleShot(True)\n        first_show_timer.setInterval(50)\n\n        first_show_timer.timeout.connect(self._on_first_show)\n\n        controller.register_event_callback(\n            \"save_as.finished\",\n            self._on_save_as_finished,\n        )\n        controller.register_event_callback(\n            \"copy_representation.finished\",\n            self._on_copy_representation_finished,\n        )\n        controller.register_event_callback(\n            \"workfile_duplicate.finished\",\n            self._on_duplicate_finished\n        )\n        controller.register_event_callback(\n            \"open_workfile.finished\",\n            self._on_open_finished\n        )\n        controller.register_event_callback(\n            \"controller.reset.started\",\n            self._on_controller_refresh_started,\n        )\n        controller.register_event_callback(\n            \"controller.reset.finished\",\n            self._on_controller_refresh_finished,\n        )\n\n        self._overlay_messages_widget = overlay_messages_widget\n        self._overlay_invalid_host = overlay_invalid_host\n        self._home_page_widget = home_page_widget\n        self._pages_widget = pages_widget\n        self._home_body_widget = home_body_widget\n        self._split_widget = split_widget\n\n        self._tasks_widget = tasks_widget\n        self._side_panel = side_panel\n\n        self._first_show_timer = first_show_timer\n\n        self._post_init()\n\n    def _post_init(self):\n        self._on_published_checkbox_changed()\n\n        # Force focus on the open button by default, required for Houdini.\n        self._files_widget.setFocus()\n\n        self.resize(1200, 600)\n\n    def _create_col_1_widget(self, controller, parent):\n        col_widget = QtWidgets.QWidget(parent)\n        header_widget = QtWidgets.QWidget(col_widget)\n\n        folder_filter_input = PlaceholderLineEdit(header_widget)\n        folder_filter_input.setPlaceholderText(\"Filter folders..\")\n\n        go_to_current_btn = GoToCurrentButton(header_widget)\n        refresh_btn = RefreshButton(header_widget)\n\n        folder_widget = FoldersWidget(\n            controller, col_widget, handle_expected_selection=True\n        )\n\n        header_layout = QtWidgets.QHBoxLayout(header_widget)\n        header_layout.setContentsMargins(0, 0, 0, 0)\n        header_layout.addWidget(folder_filter_input, 1)\n        header_layout.addWidget(go_to_current_btn, 0)\n        header_layout.addWidget(refresh_btn, 0)\n\n        col_layout = QtWidgets.QVBoxLayout(col_widget)\n        col_layout.setContentsMargins(0, 0, 0, 0)\n        col_layout.addWidget(header_widget, 0)\n        col_layout.addWidget(folder_widget, 1)\n\n        folder_filter_input.textChanged.connect(self._on_folder_filter_change)\n        go_to_current_btn.clicked.connect(self._on_go_to_current_clicked)\n        refresh_btn.clicked.connect(self._on_refresh_clicked)\n\n        self._folder_filter_input = folder_filter_input\n        self._folders_widget = folder_widget\n\n        return col_widget\n\n    def _create_col_3_widget(self, controller, parent):\n        col_widget = QtWidgets.QWidget(parent)\n\n        header_widget = QtWidgets.QWidget(col_widget)\n\n        files_filter_input = PlaceholderLineEdit(header_widget)\n        files_filter_input.setPlaceholderText(\"Filter files..\")\n\n        published_checkbox = QtWidgets.QCheckBox(\"Published\", header_widget)\n        published_checkbox.setToolTip(\"Show published workfiles\")\n\n        header_layout = QtWidgets.QHBoxLayout(header_widget)\n        header_layout.setContentsMargins(0, 0, 0, 0)\n        header_layout.addWidget(files_filter_input, 1)\n        header_layout.addWidget(published_checkbox, 0)\n\n        files_widget = FilesWidget(controller, col_widget)\n\n        col_layout = QtWidgets.QVBoxLayout(col_widget)\n        col_layout.setContentsMargins(0, 0, 0, 0)\n        col_layout.addWidget(header_widget, 0)\n        col_layout.addWidget(files_widget, 1)\n\n        files_filter_input.textChanged.connect(\n            self._on_file_text_filter_change)\n        published_checkbox.stateChanged.connect(\n            self._on_published_checkbox_changed\n        )\n\n        self._files_filter_input = files_filter_input\n        self._published_checkbox = published_checkbox\n\n        self._files_widget = files_widget\n\n        return col_widget\n\n    def set_window_on_top(self, on_top):\n        \"\"\"Set window on top of other windows.\n\n        Args:\n            on_top (bool): Show on top of other windows.\n        \"\"\"\n\n        flags = self._default_window_flags\n        if on_top:\n            flags |= QtCore.Qt.WindowStaysOnTopHint\n        if self.windowFlags() != flags:\n            self.setWindowFlags(flags)\n\n    def ensure_visible(self, use_context=True, save=True, on_top=False):\n        \"\"\"Ensure the window is visible.\n\n        This method expects arguments for compatibility with previous variant\n            of Workfiles tool.\n\n        Args:\n            use_context (Optional[bool]): DEPRECATED: This argument is\n                ignored.\n            save (Optional[bool]): Allow to save workfiles.\n            on_top (Optional[bool]): Show on top of other windows.\n        \"\"\"\n\n        save = True if save is None else save\n        on_top = False if on_top is None else on_top\n\n        is_visible = self.isVisible()\n        self._controller.set_save_enabled(save)\n        self.set_window_on_top(on_top)\n\n        self.show()\n        self.raise_()\n        self.activateWindow()\n        if is_visible:\n            self.refresh()\n\n    def refresh(self):\n        \"\"\"Trigger refresh of workfiles tool controller.\"\"\"\n\n        self._controller.reset()\n\n    def showEvent(self, event):\n        super(WorkfilesToolWindow, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self._first_show_timer.start()\n            self.setStyleSheet(style.load_stylesheet())\n\n    def keyPressEvent(self, event):\n        \"\"\"Custom keyPressEvent.\n\n        Override keyPressEvent to do nothing so that Maya's panels won't\n        take focus when pressing \"SHIFT\" whilst mouse is over viewport or\n        outliner. This way users don't accidentally perform Maya commands\n        whilst trying to name an instance.\n        \"\"\"\n\n        pass\n\n    def _on_first_show(self):\n        if not self._controller_refreshed:\n            self.refresh()\n\n    def _on_file_text_filter_change(self, text):\n        self._files_widget.set_text_filter(text)\n\n    def _on_published_checkbox_changed(self):\n        \"\"\"Publish mode changed.\n\n        Tell children widgets about it so they can handle the mode.\n        \"\"\"\n\n        published_mode = self._published_checkbox.isChecked()\n        self._files_widget.set_published_mode(published_mode)\n        self._side_panel.set_published_mode(published_mode)\n\n    def _on_folder_filter_change(self, text):\n        self._folders_widget.set_name_filter(text)\n\n    def _on_go_to_current_clicked(self):\n        self._controller.go_to_current_context()\n\n    def _on_refresh_clicked(self):\n        self.refresh()\n\n    def _on_controller_refresh_started(self):\n        self._controller_refreshed = True\n\n    def _on_controller_refresh_finished(self):\n        if self._host_is_valid is None:\n            self._host_is_valid = self._controller.is_host_valid()\n            self._overlay_invalid_host.setVisible(not self._host_is_valid)\n\n        if not self._host_is_valid:\n            return\n\n        self._folders_widget.set_project_name(\n            self._controller.get_current_project_name()\n        )\n\n    def _on_save_as_finished(self, event):\n        if event[\"failed\"]:\n            self._overlay_messages_widget.add_message(\n                \"Failed to save workfile\",\n                \"error\",\n            )\n        else:\n            self._overlay_messages_widget.add_message(\n                \"Workfile saved\"\n            )\n\n    def _on_copy_representation_finished(self, event):\n        if event[\"failed\"]:\n            self._overlay_messages_widget.add_message(\n                \"Failed to copy published workfile\",\n                \"error\",\n            )\n        else:\n            self._overlay_messages_widget.add_message(\n                \"Publish workfile saved\"\n            )\n\n    def _on_duplicate_finished(self, event):\n        if event[\"failed\"]:\n            self._overlay_messages_widget.add_message(\n                \"Failed to duplicate workfile\",\n                \"error\",\n            )\n        else:\n            self._overlay_messages_widget.add_message(\n                \"Workfile duplicated\"\n            )\n\n    def _on_open_finished(self, event):\n        if event[\"failed\"]:\n            self._overlay_messages_widget.add_message(\n                \"Failed to open workfile\",\n                \"error\",\n            )\n        else:\n            self.close()\n"
  },
  {
    "path": "openpype/tools/context_dialog/__init__.py",
    "content": "from openpype import AYON_SERVER_ENABLED\n\nif AYON_SERVER_ENABLED:\n    from ._ayon_window import ContextDialog, main\nelse:\n    from ._openpype_window import ContextDialog, main\n\n\n__all__ = (\n    \"ContextDialog\",\n    \"main\",\n)\n"
  },
  {
    "path": "openpype/tools/context_dialog/_ayon_window.py",
    "content": "import os\nimport json\n\nimport ayon_api\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import style\nfrom openpype.lib.events import QueuedEventSystem\nfrom openpype.tools.ayon_utils.models import (\n    ProjectsModel,\n    HierarchyModel,\n)\nfrom openpype.tools.ayon_utils.widgets import (\n    ProjectsCombobox,\n    FoldersWidget,\n    TasksWidget,\n)\nfrom openpype.tools.utils.lib import (\n    center_window,\n    get_openpype_qt_app,\n)\n\n\nclass SelectionModel(object):\n    \"\"\"Model handling selection changes.\n\n    Triggering events:\n    - \"selection.project.changed\"\n    - \"selection.folder.changed\"\n    - \"selection.task.changed\"\n    \"\"\"\n\n    event_source = \"selection.model\"\n\n    def __init__(self, controller):\n        self._controller = controller\n\n        self._project_name = None\n        self._folder_id = None\n        self._task_id = None\n        self._task_name = None\n\n    def get_selected_project_name(self):\n        return self._project_name\n\n    def set_selected_project(self, project_name):\n        self._project_name = project_name\n        self._controller.emit_event(\n            \"selection.project.changed\",\n            {\"project_name\": project_name},\n            self.event_source\n        )\n\n    def get_selected_folder_id(self):\n        return self._folder_id\n\n    def set_selected_folder(self, folder_id):\n        if folder_id == self._folder_id:\n            return\n        self._folder_id = folder_id\n        self._controller.emit_event(\n            \"selection.folder.changed\",\n            {\n                \"project_name\": self._project_name,\n                \"folder_id\": folder_id,\n            },\n            self.event_source\n        )\n\n    def get_selected_task_name(self):\n        return self._task_name\n\n    def get_selected_task_id(self):\n        return self._task_id\n\n    def set_selected_task(self, task_id, task_name):\n        if task_id == self._task_id:\n            return\n\n        self._task_name = task_name\n        self._task_id = task_id\n        self._controller.emit_event(\n            \"selection.task.changed\",\n            {\n                \"project_name\": self._project_name,\n                \"folder_id\": self._folder_id,\n                \"task_name\": task_name,\n                \"task_id\": task_id,\n            },\n            self.event_source\n        )\n\n\nclass ExpectedSelection:\n    def __init__(self, controller):\n        self._project_name = None\n        self._folder_id = None\n\n        self._project_selected = True\n        self._folder_selected = True\n\n        self._controller = controller\n\n    def _emit_change(self):\n        self._controller.emit_event(\n            \"expected_selection_changed\",\n            self.get_expected_selection_data(),\n        )\n\n    def set_expected_selection(self, project_name, folder_id):\n        self._project_name = project_name\n        self._folder_id = folder_id\n\n        self._project_selected = False\n        self._folder_selected = False\n        self._emit_change()\n\n    def get_expected_selection_data(self):\n        project_current = False\n        folder_current = False\n        if not self._project_selected:\n            project_current = True\n        elif not self._folder_selected:\n            folder_current = True\n        return {\n            \"project\": {\n                \"name\": self._project_name,\n                \"current\": project_current,\n                \"selected\": self._project_selected,\n            },\n            \"folder\": {\n                \"id\": self._folder_id,\n                \"current\": folder_current,\n                \"selected\": self._folder_selected,\n            },\n        }\n\n    def is_expected_project_selected(self, project_name):\n        return project_name == self._project_name and self._project_selected\n\n    def is_expected_folder_selected(self, folder_id):\n        return folder_id == self._folder_id and self._folder_selected\n\n    def expected_project_selected(self, project_name):\n        if project_name != self._project_name:\n            return False\n        self._project_selected = True\n        self._emit_change()\n        return True\n\n    def expected_folder_selected(self, folder_id):\n        if folder_id != self._folder_id:\n            return False\n        self._folder_selected = True\n        self._emit_change()\n        return True\n\n\nclass ContextDialogController:\n    def __init__(self):\n        self._event_system = None\n\n        self._projects_model = ProjectsModel(self)\n        self._hierarchy_model = HierarchyModel(self)\n        self._selection_model = SelectionModel(self)\n        self._expected_selection = ExpectedSelection(self)\n\n        self._confirmed = False\n        self._is_strict = False\n        self._output_path = None\n\n        self._initial_project_name = None\n        self._initial_folder_id = None\n        self._initial_folder_label = None\n        self._initial_project_found = True\n        self._initial_folder_found = True\n        self._initial_tasks_found = True\n\n    def reset(self):\n        self._emit_event(\"controller.reset.started\")\n\n        self._confirmed = False\n        self._output_path = None\n\n        self._initial_project_name = None\n        self._initial_folder_id = None\n        self._initial_folder_label = None\n        self._initial_project_found = True\n        self._initial_folder_found = True\n        self._initial_tasks_found = True\n\n        self._projects_model.reset()\n        self._hierarchy_model.reset()\n\n        self._emit_event(\"controller.reset.finished\")\n\n    def refresh(self):\n        self._emit_event(\"controller.refresh.started\")\n\n        self._projects_model.reset()\n        self._hierarchy_model.reset()\n\n        self._emit_event(\"controller.refresh.finished\")\n\n    # Event handling\n    def emit_event(self, topic, data=None, source=None):\n        \"\"\"Use implemented event system to trigger event.\"\"\"\n\n        if data is None:\n            data = {}\n        self._get_event_system().emit(topic, data, source)\n\n    def register_event_callback(self, topic, callback):\n        self._get_event_system().add_callback(topic, callback)\n\n    def set_output_json_path(self, output_path):\n        self._output_path = output_path\n\n    def is_strict(self):\n        return self._is_strict\n\n    def set_strict(self, enabled):\n        if self._is_strict is enabled:\n            return\n        self._is_strict = enabled\n        self._emit_event(\"strict.changed\", {\"strict\": enabled})\n\n    # Data model functions\n    def get_project_items(self, sender=None):\n        return self._projects_model.get_project_items(sender)\n\n    def get_folder_items(self, project_name, sender=None):\n        return self._hierarchy_model.get_folder_items(project_name, sender)\n\n    def get_task_items(self, project_name, folder_id, sender=None):\n        return self._hierarchy_model.get_task_items(\n            project_name, folder_id, sender\n        )\n\n    # Expected selection helpers\n    def set_expected_selection(self, project_name, folder_id):\n        return self._expected_selection.set_expected_selection(\n            project_name, folder_id\n        )\n\n    def get_expected_selection_data(self):\n        return self._expected_selection.get_expected_selection_data()\n\n    def expected_project_selected(self, project_name):\n        self._expected_selection.expected_project_selected(project_name)\n\n    def expected_folder_selected(self, folder_id):\n        self._expected_selection.expected_folder_selected(folder_id)\n\n    # Selection handling\n    def get_selected_project_name(self):\n        return self._selection_model.get_selected_project_name()\n\n    def set_selected_project(self, project_name):\n        self._selection_model.set_selected_project(project_name)\n\n    def get_selected_folder_id(self):\n        return self._selection_model.get_selected_folder_id()\n\n    def set_selected_folder(self, folder_id):\n        self._selection_model.set_selected_folder(folder_id)\n\n    def get_selected_task_name(self):\n        return self._selection_model.get_selected_task_name()\n\n    def get_selected_task_id(self):\n        return self._selection_model.get_selected_task_id()\n\n    def set_selected_task(self, task_id, task_name):\n        self._selection_model.set_selected_task(task_id, task_name)\n\n    def is_initial_context_valid(self):\n        return self._initial_folder_found and self._initial_project_found\n\n    def set_initial_context(self, project_name=None, asset_name=None):\n        result = self._prepare_initial_context(project_name, asset_name)\n\n        self._initial_project_name = project_name\n        self._initial_folder_id = result[\"folder_id\"]\n        self._initial_folder_label = result[\"folder_label\"]\n        self._initial_project_found = result[\"project_found\"]\n        self._initial_folder_found = result[\"folder_found\"]\n        self._initial_tasks_found = result[\"tasks_found\"]\n        self._emit_event(\n            \"initial.context.changed\",\n            self.get_initial_context()\n        )\n\n    def get_initial_context(self):\n        return {\n            \"project_name\": self._initial_project_name,\n            \"folder_id\": self._initial_folder_id,\n            \"folder_label\": self._initial_folder_label,\n            \"project_found\": self._initial_project_found,\n            \"folder_found\": self._initial_folder_found,\n            \"tasks_found\": self._initial_tasks_found,\n            \"valid\": (\n                self._initial_project_found\n                and self._initial_folder_found\n                and self._initial_tasks_found\n            )\n        }\n\n    # Result of this tool\n    def get_selected_context(self):\n        project_name = None\n        folder_id = None\n        task_id = None\n        task_name = None\n        folder_path = None\n        folder_name = None\n        if self._confirmed:\n            project_name = self.get_selected_project_name()\n            folder_id = self.get_selected_folder_id()\n            task_id = self.get_selected_task_id()\n            task_name = self.get_selected_task_name()\n\n        folder_item = None\n        if folder_id:\n            folder_item = self._hierarchy_model.get_folder_item(\n                project_name, folder_id)\n\n        if folder_item:\n            folder_path = folder_item.path\n            folder_name = folder_item.name\n        return {\n            \"project\": project_name,\n            \"project_name\": project_name,\n            \"asset\": folder_name,\n            \"folder_id\": folder_id,\n            \"folder_path\": folder_path,\n            \"task\": task_name,\n            \"task_name\": task_name,\n            \"task_id\": task_id,\n            \"initial_context_valid\": self.is_initial_context_valid(),\n        }\n\n    def confirm_selection(self):\n        self._confirmed = True\n\n    def store_output(self):\n        if not self._output_path:\n            return\n\n        dirpath = os.path.dirname(self._output_path)\n        os.makedirs(dirpath, exist_ok=True)\n        with open(self._output_path, \"w\") as stream:\n            json.dump(self.get_selected_context(), stream, indent=4)\n\n    def _prepare_initial_context(self, project_name, asset_name):\n        project_found = True\n        output = {\n            \"project_found\": project_found,\n            \"folder_id\": None,\n            \"folder_label\": None,\n            \"folder_found\": True,\n            \"tasks_found\": True,\n        }\n        if project_name is None:\n            asset_name = None\n        else:\n            project = ayon_api.get_project(project_name)\n            project_found = project is not None\n        output[\"project_found\"] = project_found\n        if not project_found or not asset_name:\n            return output\n\n        output[\"folder_label\"] = asset_name\n\n        folder_id = None\n        folder_found = False\n        # First try to find by path\n        folder = ayon_api.get_folder_by_path(project_name, asset_name)\n        # Try to find by name if folder was not found by path\n        #   - prevent to query by name if 'asset_name' contains '/'\n        if not folder and \"/\" not in asset_name:\n            folder = next(\n                ayon_api.get_folders(\n                    project_name, folder_names=[asset_name], fields=[\"id\"]),\n                None\n            )\n\n        if folder:\n            folder_id = folder[\"id\"]\n            folder_found = True\n\n        output[\"folder_id\"] = folder_id\n        output[\"folder_found\"] = folder_found\n        if not folder_found:\n            return output\n\n        tasks = list(ayon_api.get_tasks(\n            project_name, folder_ids=[folder_id], fields=[\"id\"]\n        ))\n        output[\"tasks_found\"] = bool(tasks)\n        return output\n\n    def _get_event_system(self):\n        \"\"\"Inner event system for workfiles tool controller.\n\n        Is used for communication with UI. Event system is created on demand.\n\n        Returns:\n            QueuedEventSystem: Event system which can trigger callbacks\n                for topics.\n        \"\"\"\n\n        if self._event_system is None:\n            self._event_system = QueuedEventSystem()\n        return self._event_system\n\n    def _emit_event(self, topic, data=None):\n        self.emit_event(topic, data, \"controller\")\n\n\nclass InvalidContextOverlay(QtWidgets.QFrame):\n    confirmed = QtCore.Signal()\n\n    def __init__(self, parent):\n        super(InvalidContextOverlay, self).__init__(parent)\n        self.setObjectName(\"OverlayFrame\")\n\n        mid_widget = QtWidgets.QWidget(self)\n        label_widget = QtWidgets.QLabel(\n            \"Requested context was not found...\",\n            mid_widget\n        )\n\n        confirm_btn = QtWidgets.QPushButton(\"Close\", mid_widget)\n\n        mid_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        mid_layout = QtWidgets.QVBoxLayout(mid_widget)\n        mid_layout.setContentsMargins(0, 0, 0, 0)\n        mid_layout.addWidget(label_widget, 0)\n        mid_layout.addSpacing(30)\n        mid_layout.addWidget(confirm_btn, 0)\n\n        main_layout = QtWidgets.QGridLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(mid_widget, 1, 1)\n        main_layout.setRowStretch(0, 1)\n        main_layout.setRowStretch(1, 0)\n        main_layout.setRowStretch(2, 1)\n        main_layout.setColumnStretch(0, 1)\n        main_layout.setColumnStretch(1, 0)\n        main_layout.setColumnStretch(2, 1)\n\n        confirm_btn.clicked.connect(self.confirmed)\n\n        self._label_widget = label_widget\n        self._confirm_btn = confirm_btn\n\n    def set_context(\n        self,\n        project_name,\n        folder_label,\n        project_found,\n        folder_found,\n        tasks_found,\n    ):\n        lines = []\n        if not project_found:\n            lines.extend([\n                \"Requested project '{}' was not found...\".format(\n                    project_name),\n            ])\n\n        elif not folder_found:\n            lines.extend([\n                \"Requested folder was not found...\",\n                \"\",\n                \"Project: {}\".format(project_name),\n                \"Folder: {}\".format(folder_label),\n            ])\n        elif not tasks_found:\n            lines.extend([\n                \"Requested folder does not have any tasks...\",\n                \"\",\n                \"Project: {}\".format(project_name),\n                \"Folder: {}\".format(folder_label),\n            ])\n        else:\n            lines.append(\"Requested context was not found...\")\n        self._label_widget.setText(\"<br/>\".join(lines))\n\n\nclass ContextDialog(QtWidgets.QDialog):\n    \"\"\"Dialog to select a context.\n\n    Context has 3 parts:\n    - Project\n    - Asset\n    - Task\n\n    It is possible to predefine project and asset. In that case their widgets\n    will have passed preselected values and will be disabled.\n    \"\"\"\n    def __init__(self, controller=None, parent=None):\n        super(ContextDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"Select Context\")\n        self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))\n\n        if controller is None:\n            controller = ContextDialogController()\n\n        # Enable minimize and maximize for app\n        window_flags = QtCore.Qt.Window\n        if not parent:\n            window_flags |= QtCore.Qt.WindowStaysOnTopHint\n        self.setWindowFlags(window_flags)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        # UI initialization\n        main_splitter = QtWidgets.QSplitter(self)\n\n        # Left side widget contains project combobox and asset widget\n        left_side_widget = QtWidgets.QWidget(main_splitter)\n\n        project_combobox = ProjectsCombobox(\n            controller,\n            parent=left_side_widget,\n            handle_expected_selection=True\n        )\n        project_combobox.set_select_item_visible(True)\n\n        # Assets widget\n        folders_widget = FoldersWidget(\n            controller,\n            parent=left_side_widget,\n            handle_expected_selection=True\n        )\n\n        left_side_layout = QtWidgets.QVBoxLayout(left_side_widget)\n        left_side_layout.setContentsMargins(0, 0, 0, 0)\n        left_side_layout.addWidget(project_combobox, 0)\n        left_side_layout.addWidget(folders_widget, 1)\n\n        # Right side of window contains only tasks\n        tasks_widget = TasksWidget(controller, parent=main_splitter)\n\n        # Add widgets to main splitter\n        main_splitter.addWidget(left_side_widget)\n        main_splitter.addWidget(tasks_widget)\n\n        # Set stretch of both sides\n        main_splitter.setStretchFactor(0, 7)\n        main_splitter.setStretchFactor(1, 3)\n\n        # Add confimation button to bottom right\n        ok_btn = QtWidgets.QPushButton(\"OK\", self)\n\n        buttons_layout = QtWidgets.QHBoxLayout()\n        buttons_layout.setContentsMargins(0, 0, 0, 0)\n        buttons_layout.addStretch(1)\n        buttons_layout.addWidget(ok_btn, 0)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(main_splitter, 1)\n        main_layout.addLayout(buttons_layout, 0)\n\n        overlay_widget = InvalidContextOverlay(self)\n        overlay_widget.setVisible(False)\n\n        ok_btn.clicked.connect(self._on_ok_click)\n        project_combobox.refreshed.connect(self._on_projects_refresh)\n        overlay_widget.confirmed.connect(self._on_overlay_confirm)\n\n        controller.register_event_callback(\n            \"selection.project.changed\",\n            self._on_project_selection_change\n        )\n        controller.register_event_callback(\n            \"selection.folder.changed\",\n            self._on_folder_selection_change\n        )\n        controller.register_event_callback(\n            \"selection.task.changed\",\n            self._on_task_selection_change\n        )\n        controller.register_event_callback(\n            \"initial.context.changed\",\n            self._on_init_context_change\n        )\n        controller.register_event_callback(\n            \"strict.changed\",\n            self._on_strict_changed\n        )\n        controller.register_event_callback(\n            \"controller.reset.finished\",\n            self._on_controller_reset\n        )\n        controller.register_event_callback(\n            \"controller.refresh.finished\",\n            self._on_controller_refresh\n        )\n\n        # Set stylehseet and resize window on first show\n        self._first_show = True\n        self._visible = False\n\n        self._controller = controller\n\n        self._project_combobox = project_combobox\n        self._folders_widget = folders_widget\n        self._tasks_widget = tasks_widget\n\n        self._ok_btn = ok_btn\n\n        self._overlay_widget = overlay_widget\n\n        self._apply_strict_changes(self.is_strict())\n\n    def is_strict(self):\n        return self._controller.is_strict()\n\n    def showEvent(self, event):\n        \"\"\"Override show event to do some callbacks.\"\"\"\n        super(ContextDialog, self).showEvent(event)\n        self._visible = True\n\n        if self._first_show:\n            self._first_show = False\n            # Set stylesheet and resize\n            self.setStyleSheet(style.load_stylesheet())\n            self.resize(600, 700)\n            center_window(self)\n        self._controller.refresh()\n\n        initial_context = self._controller.get_initial_context()\n        self._set_init_context(initial_context)\n        self._overlay_widget.resize(self.size())\n\n    def resizeEvent(self, event):\n        super(ContextDialog, self).resizeEvent(event)\n        self._overlay_widget.resize(self.size())\n\n    def closeEvent(self, event):\n        \"\"\"Ignore close event if is in strict state and context is not done.\"\"\"\n        if self.is_strict() and not self._ok_btn.isEnabled():\n            # Allow to close window when initial context is not valid\n            if self._controller.is_initial_context_valid():\n                event.ignore()\n                return\n\n        if self.is_strict():\n            self._confirm_selection()\n        self._visible = False\n        super(ContextDialog, self).closeEvent(event)\n\n    def set_strict(self, enabled):\n        \"\"\"Change strictness of dialog.\"\"\"\n\n        self._controller.set_strict(enabled)\n\n    def refresh(self):\n        \"\"\"Refresh all widget one by one.\n\n        When asset refresh is triggered we have to wait when is done so\n        this method continues with `_on_asset_widget_refresh_finished`.\n        \"\"\"\n\n        self._controller.reset()\n\n    def get_context(self):\n        \"\"\"Result of dialog.\"\"\"\n        return self._controller.get_selected_context()\n\n    def set_context(self, project_name=None, asset_name=None):\n        \"\"\"Set context which will be used and locked in dialog.\"\"\"\n\n        self._controller.set_initial_context(project_name, asset_name)\n\n    def _on_projects_refresh(self):\n        initial_context = self._controller.get_initial_context()\n        self._controller.set_expected_selection(\n            initial_context[\"project_name\"],\n            initial_context[\"folder_id\"]\n        )\n\n    def _on_overlay_confirm(self):\n        self.close()\n\n    def _on_ok_click(self):\n        # Store values to output\n        self._confirm_selection()\n        # Close dialog\n        self.accept()\n\n    def _confirm_selection(self):\n        self._controller.confirm_selection()\n\n    def _on_project_selection_change(self, event):\n        self._on_selection_change(\n            event[\"project_name\"],\n        )\n\n    def _on_folder_selection_change(self, event):\n        self._on_selection_change(\n            event[\"project_name\"],\n            event[\"folder_id\"],\n        )\n\n    def _on_task_selection_change(self, event):\n        self._on_selection_change(\n            event[\"project_name\"],\n            event[\"folder_id\"],\n            event[\"task_name\"],\n        )\n\n    def _on_selection_change(\n        self, project_name, folder_id=None, task_name=None\n    ):\n        self._validate_strict(project_name, folder_id, task_name)\n\n    def _on_init_context_change(self, event):\n        self._set_init_context(event.data)\n        if self._visible:\n            self._controller.set_expected_selection(\n                event[\"project_name\"], event[\"folder_id\"]\n            )\n\n    def _set_init_context(self, init_context):\n        project_name = init_context[\"project_name\"]\n        if not init_context[\"valid\"]:\n            self._overlay_widget.setVisible(True)\n            self._overlay_widget.set_context(\n                project_name,\n                init_context[\"folder_label\"],\n                init_context[\"project_found\"],\n                init_context[\"folder_found\"],\n                init_context[\"tasks_found\"]\n            )\n            return\n\n        self._overlay_widget.setVisible(False)\n        if project_name:\n            self._project_combobox.setEnabled(False)\n            if init_context[\"folder_id\"]:\n                self._folders_widget.setEnabled(False)\n        else:\n            self._project_combobox.setEnabled(True)\n            self._folders_widget.setEnabled(True)\n\n    def _on_strict_changed(self, event):\n        self._apply_strict_changes(event[\"strict\"])\n\n    def _on_controller_reset(self):\n        self._apply_strict_changes(self.is_strict())\n        self._project_combobox.refresh()\n\n    def _on_controller_refresh(self):\n        self._project_combobox.refresh()\n\n    def _apply_strict_changes(self, is_strict):\n        if not is_strict:\n            if not self._ok_btn.isEnabled():\n                self._ok_btn.setEnabled(True)\n            return\n        context = self._controller.get_selected_context()\n        self._validate_strict(\n            context[\"project_name\"],\n            context[\"folder_id\"],\n            context[\"task_name\"]\n        )\n\n    def _validate_strict(self, project_name, folder_id, task_name):\n        if not self.is_strict():\n            return\n\n        enabled = True\n        if not project_name or not folder_id or not task_name:\n            enabled = False\n        self._ok_btn.setEnabled(enabled)\n\n\ndef main(\n    path_to_store,\n    project_name=None,\n    asset_name=None,\n    strict=True\n):\n    # Run Qt application\n    app = get_openpype_qt_app()\n    controller = ContextDialogController()\n    controller.set_strict(strict)\n    controller.set_initial_context(project_name, asset_name)\n    controller.set_output_json_path(path_to_store)\n    window = ContextDialog(controller=controller)\n    window.show()\n    app.exec_()\n    controller.store_output()\n"
  },
  {
    "path": "openpype/tools/context_dialog/_openpype_window.py",
    "content": "import os\nimport json\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import style\nfrom openpype.pipeline import AvalonMongoDB\nfrom openpype.tools.utils.lib import center_window, get_openpype_qt_app\nfrom openpype.tools.utils.assets_widget import SingleSelectAssetsWidget\nfrom openpype.tools.utils.constants import (\n    PROJECT_NAME_ROLE\n)\nfrom openpype.tools.utils.tasks_widget import TasksWidget\nfrom openpype.tools.utils.models import (\n    ProjectModel,\n    ProjectSortFilterProxy\n)\n\n\nclass ContextDialog(QtWidgets.QDialog):\n    \"\"\"Dialog to select a context.\n\n    Context has 3 parts:\n    - Project\n    - Asset\n    - Task\n\n    It is possible to predefine project and asset. In that case their widgets\n    will have passed preselected values and will be disabled.\n    \"\"\"\n    def __init__(self, parent=None):\n        super(ContextDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"Select Context\")\n        self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))\n\n        # Enable minimize and maximize for app\n        window_flags = QtCore.Qt.Window\n        if not parent:\n            window_flags |= QtCore.Qt.WindowStaysOnTopHint\n        self.setWindowFlags(window_flags)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        dbcon = AvalonMongoDB()\n\n        # UI initialization\n        main_splitter = QtWidgets.QSplitter(self)\n\n        # Left side widget contains project combobox and asset widget\n        left_side_widget = QtWidgets.QWidget(main_splitter)\n\n        project_combobox = QtWidgets.QComboBox(left_side_widget)\n        # Styled delegate to propagate stylessheet\n        project_delegate = QtWidgets.QStyledItemDelegate(project_combobox)\n        project_combobox.setItemDelegate(project_delegate)\n        # Project model with only active projects without default item\n        project_model = ProjectModel(\n            dbcon,\n            only_active=True,\n            add_default_project=False\n        )\n        # Sorting proxy model\n        project_proxy = ProjectSortFilterProxy()\n        project_proxy.setSourceModel(project_model)\n        project_combobox.setModel(project_proxy)\n\n        # Assets widget\n        assets_widget = SingleSelectAssetsWidget(\n            dbcon, parent=left_side_widget\n        )\n\n        left_side_layout = QtWidgets.QVBoxLayout(left_side_widget)\n        left_side_layout.setContentsMargins(0, 0, 0, 0)\n        left_side_layout.addWidget(project_combobox)\n        left_side_layout.addWidget(assets_widget)\n\n        # Right side of window contains only tasks\n        tasks_widget = TasksWidget(dbcon, main_splitter)\n\n        # Add widgets to main splitter\n        main_splitter.addWidget(left_side_widget)\n        main_splitter.addWidget(tasks_widget)\n\n        # Set stretch of both sides\n        main_splitter.setStretchFactor(0, 7)\n        main_splitter.setStretchFactor(1, 3)\n\n        # Add confimation button to bottom right\n        ok_btn = QtWidgets.QPushButton(\"OK\", self)\n\n        buttons_layout = QtWidgets.QHBoxLayout()\n        buttons_layout.setContentsMargins(0, 0, 0, 0)\n        buttons_layout.addStretch(1)\n        buttons_layout.addWidget(ok_btn, 0)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(main_splitter, 1)\n        main_layout.addLayout(buttons_layout, 0)\n\n        # Timer which will trigger asset refresh\n        # - this is needed because asset widget triggers\n        #   finished refresh before hides spin box so we need to trigger\n        #   refreshing in small offset if we want re-refresh asset widget\n        assets_timer = QtCore.QTimer()\n        assets_timer.setInterval(50)\n        assets_timer.setSingleShot(True)\n\n        assets_timer.timeout.connect(self._on_asset_refresh_timer)\n\n        project_combobox.currentIndexChanged.connect(\n            self._on_project_combo_change\n        )\n        assets_widget.selection_changed.connect(self._on_asset_change)\n        assets_widget.refresh_triggered.connect(self._on_asset_refresh_trigger)\n        assets_widget.refreshed.connect(self._on_asset_widget_refresh_finished)\n        tasks_widget.task_changed.connect(self._on_task_change)\n        ok_btn.clicked.connect(self._on_ok_click)\n\n        self._dbcon = dbcon\n\n        self._project_combobox = project_combobox\n        self._project_model = project_model\n        self._project_proxy = project_proxy\n        self._project_delegate = project_delegate\n\n        self._assets_widget = assets_widget\n\n        self._tasks_widget = tasks_widget\n\n        self._ok_btn = ok_btn\n\n        self._strict = False\n\n        # Values set by `set_context` method\n        self._set_context_project = None\n        self._set_context_asset = None\n\n        # Requirements for asset widget refresh\n        self._assets_timer = assets_timer\n        self._rerefresh_assets = True\n        self._assets_refreshing = False\n\n        # Set stylehseet and resize window on first show\n        self._first_show = True\n\n        # Helper attributes for handling of refresh\n        self._ignore_value_changes = False\n        self._refresh_on_next_show = True\n\n        # Output of dialog\n        self._context_to_store = {\n            \"project\": None,\n            \"asset\": None,\n            \"task\": None\n        }\n\n    def closeEvent(self, event):\n        \"\"\"Ignore close event if is in strict state and context is not done.\"\"\"\n        if self._strict and not self._ok_btn.isEnabled():\n            event.ignore()\n            return\n\n        if self._strict:\n            self._confirm_values()\n        super(ContextDialog, self).closeEvent(event)\n\n    def set_strict(self, strict):\n        \"\"\"Change strictness of dialog.\"\"\"\n        self._strict = strict\n        self._validate_strict()\n\n    def _set_refresh_on_next_show(self):\n        \"\"\"Refresh will be called on next showEvent.\n\n        If window is already visible then just execute refresh.\n        \"\"\"\n        self._refresh_on_next_show = True\n        if self.isVisible():\n            self.refresh()\n\n    def _refresh_assets(self):\n        \"\"\"Trigger refreshing of asset widget.\n\n        This will set mart to rerefresh asset when current refreshing is done\n        or do it immidietely if asset widget is not refreshing at the time.\n        \"\"\"\n        if self._assets_refreshing:\n            self._rerefresh_assets = True\n        else:\n            self._on_asset_refresh_timer()\n\n    def showEvent(self, event):\n        \"\"\"Override show event to do some callbacks.\"\"\"\n        super(ContextDialog, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            # Set stylesheet and resize\n            self.setStyleSheet(style.load_stylesheet())\n            self.resize(600, 700)\n            center_window(self)\n\n        if self._refresh_on_next_show:\n            self.refresh()\n\n    def refresh(self):\n        \"\"\"Refresh all widget one by one.\n\n        When asset refresh is triggered we have to wait when is done so\n        this method continues with `_on_asset_widget_refresh_finished`.\n        \"\"\"\n        # Change state of refreshing (no matter how refresh was called)\n        self._refresh_on_next_show = False\n\n        # Ignore changes of combobox and asset widget\n        self._ignore_value_changes = True\n\n        # Get current project name to be able set it afterwards\n        select_project_name = self._dbcon.Session.get(\"AVALON_PROJECT\")\n        # Trigger project refresh\n        self._project_model.refresh()\n        # Sort projects\n        self._project_proxy.sort(0)\n\n        # Disable combobox if project was passed to `set_context`\n        if self._set_context_project:\n            select_project_name = self._set_context_project\n            self._project_combobox.setEnabled(False)\n        else:\n            # Find new project to select\n            self._project_combobox.setEnabled(True)\n            if (\n                select_project_name is None\n                and self._project_proxy.rowCount() > 0\n            ):\n                index = self._project_proxy.index(0, 0)\n                select_project_name = index.data(PROJECT_NAME_ROLE)\n\n        self._ignore_value_changes = False\n\n        idx = self._project_combobox.findText(select_project_name)\n        if idx >= 0:\n            self._project_combobox.setCurrentIndex(idx)\n        self._dbcon.Session[\"AVALON_PROJECT\"] = (\n            self._project_combobox.currentText()\n        )\n\n        # Trigger asset refresh\n        self._refresh_assets()\n\n    def _on_asset_refresh_timer(self):\n        \"\"\"This is only way how to trigger refresh asset widget.\n\n        Use `_refresh_assets` method to refresh asset widget.\n        \"\"\"\n        self._assets_widget.refresh()\n\n    def _on_asset_widget_refresh_finished(self):\n        \"\"\"Catch when asset widget finished refreshing.\"\"\"\n        # If should refresh again then skip all other callbacks and trigger\n        #   assets timer directly.\n        self._assets_refreshing = False\n        if self._rerefresh_assets:\n            self._rerefresh_assets = False\n            self._assets_timer.start()\n            return\n\n        self._ignore_value_changes = True\n        if self._set_context_asset:\n            self._dbcon.Session[\"AVALON_ASSET\"] = self._set_context_asset\n            self._assets_widget.setEnabled(False)\n            self._assets_widget.select_asset_by_name(self._set_context_asset)\n            self._set_asset_to_tasks_widget()\n        else:\n            self._assets_widget.setEnabled(True)\n            self._assets_widget.set_current_asset_btn_visibility(False)\n\n        # Refresh tasks\n        self._tasks_widget.refresh()\n\n        self._ignore_value_changes = False\n\n        self._validate_strict()\n\n    def _on_project_combo_change(self):\n        if self._ignore_value_changes:\n            return\n        project_name = self._project_combobox.currentText()\n\n        if self._dbcon.Session.get(\"AVALON_PROJECT\") == project_name:\n            return\n\n        self._dbcon.Session[\"AVALON_PROJECT\"] = project_name\n\n        self._refresh_assets()\n        self._validate_strict()\n\n    def _on_asset_refresh_trigger(self):\n        self._assets_refreshing = True\n        self._on_asset_change()\n\n    def _on_asset_change(self):\n        \"\"\"Selected assets have changed\"\"\"\n        if self._ignore_value_changes:\n            return\n        self._set_asset_to_tasks_widget()\n\n    def _on_task_change(self):\n        self._validate_strict()\n\n    def _set_asset_to_tasks_widget(self):\n        asset_id = self._assets_widget.get_selected_asset_id()\n\n        self._tasks_widget.set_asset_id(asset_id)\n\n    def _confirm_values(self):\n        \"\"\"Store values to output.\"\"\"\n        self._context_to_store[\"project\"] = self.get_selected_project()\n        self._context_to_store[\"asset\"] = self.get_selected_asset()\n        self._context_to_store[\"task\"] = self.get_selected_task()\n\n    def _on_ok_click(self):\n        # Store values to output\n        self._confirm_values()\n        # Close dialog\n        self.accept()\n\n    def get_selected_project(self):\n        \"\"\"Get selected project.\"\"\"\n        return self._project_combobox.currentText()\n\n    def get_selected_asset(self):\n        \"\"\"Currently selected asset in asset widget.\"\"\"\n        return self._assets_widget.get_selected_asset_name()\n\n    def get_selected_task(self):\n        \"\"\"Currently selected task.\"\"\"\n        return self._tasks_widget.get_selected_task_name()\n\n    def _validate_strict(self):\n        if not self._strict:\n            if not self._ok_btn.isEnabled():\n                self._ok_btn.setEnabled(True)\n            return\n\n        enabled = True\n        if not self._set_context_project and not self.get_selected_project():\n            enabled = False\n        elif not self._set_context_asset and not self.get_selected_asset():\n            enabled = False\n        elif not self.get_selected_task():\n            enabled = False\n        self._ok_btn.setEnabled(enabled)\n\n    def set_context(self, project_name=None, asset_name=None):\n        \"\"\"Set context which will be used and locked in dialog.\"\"\"\n        if project_name is None:\n            asset_name = None\n\n        self._set_context_project = project_name\n        self._set_context_asset = asset_name\n\n        self._context_to_store[\"project\"] = project_name\n        self._context_to_store[\"asset\"] = asset_name\n\n        self._set_refresh_on_next_show()\n\n    def get_context(self):\n        \"\"\"Result of dialog.\"\"\"\n        return self._context_to_store\n\n\ndef main(\n    path_to_store,\n    project_name=None,\n    asset_name=None,\n    strict=True\n):\n    # Run Qt application\n    app = get_openpype_qt_app()\n    window = ContextDialog()\n    window.set_strict(strict)\n    window.set_context(project_name, asset_name)\n    window.show()\n    app.exec_()\n\n    # Get result from window\n    data = window.get_context()\n\n    # Make sure json filepath directory exists\n    file_dir = os.path.dirname(path_to_store)\n    if not os.path.exists(file_dir):\n        os.makedirs(file_dir)\n\n    # Store result into json file\n    with open(path_to_store, \"w\") as stream:\n        json.dump(data, stream)\n"
  },
  {
    "path": "openpype/tools/creator/__init__.py",
    "content": "from .window import (\n    show,\n    CreatorWindow\n)\n\n__all__ = (\n    \"show\",\n    \"CreatorWindow\"\n)\n"
  },
  {
    "path": "openpype/tools/creator/constants.py",
    "content": "from qtpy import QtCore\n\n\nFAMILY_ROLE = QtCore.Qt.UserRole + 1\nITEM_ID_ROLE = QtCore.Qt.UserRole + 2\n\nSEPARATOR = \"---\"\nSEPARATORS = {\"---\", \"---separator---\"}\n"
  },
  {
    "path": "openpype/tools/creator/model.py",
    "content": "import uuid\nfrom qtpy import QtGui, QtCore\n\nfrom openpype.pipeline import discover_legacy_creator_plugins\n\nfrom . constants import (\n    FAMILY_ROLE,\n    ITEM_ID_ROLE\n)\n\n\nclass CreatorsModel(QtGui.QStandardItemModel):\n    def __init__(self, *args, **kwargs):\n        super(CreatorsModel, self).__init__(*args, **kwargs)\n\n        self._creators_by_id = {}\n\n    def reset(self):\n        # TODO change to refresh when clearing is not needed\n        self.clear()\n        self._creators_by_id = {}\n\n        items = []\n        creators = discover_legacy_creator_plugins()\n        for creator in creators:\n            if not creator.enabled:\n                continue\n            item_id = str(uuid.uuid4())\n            self._creators_by_id[item_id] = creator\n\n            label = creator.label or creator.family\n            item = QtGui.QStandardItem(label)\n            item.setEditable(False)\n            item.setData(item_id, ITEM_ID_ROLE)\n            item.setData(creator.family, FAMILY_ROLE)\n            items.append(item)\n\n        if not items:\n            item = QtGui.QStandardItem(\"No registered families\")\n            item.setEnabled(False)\n            item.setData(False, QtCore.Qt.ItemIsEnabled)\n            items.append(item)\n\n        items.sort(key=lambda item: item.text())\n        self.invisibleRootItem().appendRows(items)\n\n    def get_creator_by_id(self, item_id):\n        return self._creators_by_id.get(item_id)\n\n    def get_indexes_by_family(self, family):\n        indexes = []\n        for row in range(self.rowCount()):\n            index = self.index(row, 0)\n            item_id = index.data(ITEM_ID_ROLE)\n            creator_plugin = self._creators_by_id.get(item_id)\n            if creator_plugin and (\n                creator_plugin.label.lower() == family.lower()\n                or creator_plugin.family.lower() == family.lower()\n            ):\n                indexes.append(index)\n        return indexes\n"
  },
  {
    "path": "openpype/tools/creator/widgets.py",
    "content": "import re\nimport inspect\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nimport qtawesome\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS\nfrom openpype.tools.utils import ErrorMessageBox\n\nif hasattr(QtGui, \"QRegularExpressionValidator\"):\n    RegularExpressionValidatorClass = QtGui.QRegularExpressionValidator\n    RegularExpressionClass = QtCore.QRegularExpression\nelse:\n    RegularExpressionValidatorClass = QtGui.QRegExpValidator\n    RegularExpressionClass = QtCore.QRegExp\n\n\nclass CreateErrorMessageBox(ErrorMessageBox):\n    def __init__(\n        self,\n        family,\n        subset_name,\n        asset_name,\n        exc_msg,\n        formatted_traceback,\n        parent\n    ):\n        self._family = family\n        self._subset_name = subset_name\n        self._asset_name = asset_name\n        self._exc_msg = exc_msg\n        self._formatted_traceback = formatted_traceback\n        super(CreateErrorMessageBox, self).__init__(\"Creation failed\", parent)\n\n    def _create_top_widget(self, parent_widget):\n        label_widget = QtWidgets.QLabel(parent_widget)\n        label_widget.setText(\n            \"<span style='font-size:18pt;'>Failed to create</span>\"\n        )\n        return label_widget\n\n    def _get_report_data(self):\n        report_message = (\n            \"Failed to create {subset_label}: \\\"{subset}\\\"\"\n            \" {family_label}: \\\"{family}\\\"\"\n            \" in Asset: \\\"{asset}\\\"\"\n            \"\\n\\nError: {message}\"\n        ).format(\n            subset_label=\"Product\" if AYON_SERVER_ENABLED else \"Subset\",\n            family_label=\"Type\" if AYON_SERVER_ENABLED else \"Family\",\n            subset=self._subset_name,\n            family=self._family,\n            asset=self._asset_name,\n            message=self._exc_msg\n        )\n        if self._formatted_traceback:\n            report_message += \"\\n\\n{}\".format(self._formatted_traceback)\n        return [report_message]\n\n    def _create_content(self, content_layout):\n        item_name_template = (\n            \"<span style='font-weight:bold;'>{}:</span> {{}}<br>\"\n            \"<span style='font-weight:bold;'>{}:</span> {{}}<br>\"\n            \"<span style='font-weight:bold;'>{}:</span> {{}}<br>\"\n        ).format(\n            \"Product type\" if AYON_SERVER_ENABLED else \"Family\",\n            \"Product name\" if AYON_SERVER_ENABLED else \"Subset\",\n            \"Folder\" if AYON_SERVER_ENABLED else \"Asset\"\n        )\n        exc_msg_template = \"<span style='font-weight:bold'>{}</span>\"\n\n        line = self._create_line()\n        content_layout.addWidget(line)\n\n        item_name_widget = QtWidgets.QLabel(self)\n        item_name_widget.setText(\n            item_name_template.format(\n                self._family, self._subset_name, self._asset_name\n            )\n        )\n        content_layout.addWidget(item_name_widget)\n\n        message_label_widget = QtWidgets.QLabel(self)\n        message_label_widget.setText(\n            exc_msg_template.format(self.convert_text_for_html(self._exc_msg))\n        )\n        content_layout.addWidget(message_label_widget)\n\n        if self._formatted_traceback:\n            line_widget = self._create_line()\n            tb_widget = self._create_traceback_widget(\n                self._formatted_traceback\n            )\n            content_layout.addWidget(line_widget)\n            content_layout.addWidget(tb_widget)\n\n\nclass SubsetNameValidator(RegularExpressionValidatorClass):\n    invalid = QtCore.Signal(set)\n    pattern = \"^[{}]*$\".format(SUBSET_NAME_ALLOWED_SYMBOLS)\n\n    def __init__(self):\n        reg = RegularExpressionClass(self.pattern)\n        super(SubsetNameValidator, self).__init__(reg)\n\n    def validate(self, text, pos):\n        results = super(SubsetNameValidator, self).validate(text, pos)\n        if results[0] == self.Invalid:\n            self.invalid.emit(self.invalid_chars(text))\n        return results\n\n    def invalid_chars(self, text):\n        invalid = set()\n        re_valid = re.compile(self.pattern)\n        for char in text:\n            if char == \" \":\n                invalid.add(\"' '\")\n                continue\n            if not re_valid.match(char):\n                invalid.add(char)\n        return invalid\n\n\nclass VariantLineEdit(QtWidgets.QLineEdit):\n    report = QtCore.Signal(str)\n    colors = {\n        \"empty\": (QtGui.QColor(\"#78879b\"), \"\"),\n        \"exists\": (QtGui.QColor(\"#4E76BB\"), \"border-color: #4E76BB;\"),\n        \"new\": (QtGui.QColor(\"#7AAB8F\"), \"border-color: #7AAB8F;\"),\n    }\n\n    def __init__(self, *args, **kwargs):\n        super(VariantLineEdit, self).__init__(*args, **kwargs)\n\n        validator = SubsetNameValidator()\n        self.setValidator(validator)\n        self.setToolTip(\"Only alphanumeric characters (A-Z a-z 0-9), \"\n                        \"'_' and '.' are allowed.\")\n\n        self._status_color = self.colors[\"empty\"][0]\n\n        anim = QtCore.QPropertyAnimation()\n        anim.setTargetObject(self)\n        anim.setPropertyName(b\"status_color\")\n        anim.setEasingCurve(QtCore.QEasingCurve.InCubic)\n        anim.setDuration(300)\n        anim.setStartValue(QtGui.QColor(\"#C84747\"))  # `Invalid` status color\n        self.animation = anim\n\n        validator.invalid.connect(self.on_invalid)\n\n    def on_invalid(self, invalid):\n        message = \"Invalid character: %s\" % \", \".join(invalid)\n        self.report.emit(message)\n        self.animation.stop()\n        self.animation.start()\n\n    def as_empty(self):\n        self._set_border(\"empty\")\n        self.report.emit(\"Empty {} name ..\".format(\n            \"product\" if AYON_SERVER_ENABLED else \"subset\"\n        ))\n\n    def as_exists(self):\n        self._set_border(\"exists\")\n        self.report.emit(\"Existing {}, appending next version.\".format(\n            \"product\" if AYON_SERVER_ENABLED else \"subset\"\n        ))\n\n    def as_new(self):\n        self._set_border(\"new\")\n        self.report.emit(\"New {}, creating first version.\".format(\n            \"product\" if AYON_SERVER_ENABLED else \"subset\"\n        ))\n\n    def _set_border(self, status):\n        qcolor, style = self.colors[status]\n        self.animation.setEndValue(qcolor)\n        self.setStyleSheet(style)\n\n    def _get_status_color(self):\n        return self._status_color\n\n    def _set_status_color(self, color):\n        self._status_color = color\n        self.setStyleSheet(\"border-color: %s;\" % color.name())\n\n    status_color = QtCore.Property(\n        QtGui.QColor, _get_status_color, _set_status_color\n    )\n\n\nclass FamilyDescriptionWidget(QtWidgets.QWidget):\n    \"\"\"A family description widget.\n\n    Shows a family icon, family name and a help description.\n    Used in creator header.\n\n     _________________\n    |  ____           |\n    | |icon| FAMILY   |\n    | |____| help     |\n    |_________________|\n\n    \"\"\"\n\n    SIZE = 35\n\n    def __init__(self, parent=None):\n        super(FamilyDescriptionWidget, self).__init__(parent=parent)\n\n        icon_label = QtWidgets.QLabel(self)\n        icon_label.setSizePolicy(\n            QtWidgets.QSizePolicy.Maximum,\n            QtWidgets.QSizePolicy.Maximum\n        )\n\n        # Add 4 pixel padding to avoid icon being cut off\n        icon_label.setFixedWidth(self.SIZE + 4)\n        icon_label.setFixedHeight(self.SIZE + 4)\n\n        label_layout = QtWidgets.QVBoxLayout()\n        label_layout.setSpacing(0)\n\n        family_label = QtWidgets.QLabel(self)\n        family_label.setObjectName(\"CreatorFamilyLabel\")\n        family_label.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft)\n\n        help_label = QtWidgets.QLabel(self)\n        help_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)\n\n        label_layout.addWidget(family_label)\n        label_layout.addWidget(help_label)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(5)\n        layout.addWidget(icon_label)\n        layout.addLayout(label_layout)\n\n        self._help_label = help_label\n        self._family_label = family_label\n        self._icon_label = icon_label\n\n    def set_item(self, creator_plugin):\n        \"\"\"Update elements to display information of a family item.\n\n        Args:\n            item (dict): A family item as registered with name, help and icon\n\n        Returns:\n            None\n\n        \"\"\"\n        if not creator_plugin:\n            self._icon_label.setPixmap(None)\n            self._family_label.setText(\"\")\n            self._help_label.setText(\"\")\n            return\n\n        # Support a font-awesome icon\n        icon_name = getattr(creator_plugin, \"icon\", None) or \"info-circle\"\n        try:\n            icon = qtawesome.icon(\"fa.{}\".format(icon_name), color=\"white\")\n            pixmap = icon.pixmap(self.SIZE, self.SIZE)\n        except Exception:\n            print(\"BUG: Couldn't load icon \\\"fa.{}\\\"\".format(str(icon_name)))\n            # Create transparent pixmap\n            pixmap = QtGui.QPixmap()\n            pixmap.fill(QtCore.Qt.transparent)\n        pixmap = pixmap.scaled(self.SIZE, self.SIZE)\n\n        # Parse a clean line from the Creator's docstring\n        docstring = inspect.getdoc(creator_plugin)\n        creator_help = docstring.splitlines()[0] if docstring else \"\"\n\n        self._icon_label.setPixmap(pixmap)\n        self._family_label.setText(creator_plugin.family)\n        self._help_label.setText(creator_help)\n"
  },
  {
    "path": "openpype/tools/creator/window.py",
    "content": "import sys\nimport traceback\nimport re\n\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.client import get_asset_by_name, get_subsets\nfrom openpype import style\nfrom openpype.settings import get_current_project_settings\nfrom openpype.tools.utils.lib import qt_app_context\nfrom openpype.pipeline import (\n    get_current_project_name,\n    get_current_asset_name,\n    get_current_task_name,\n)\nfrom openpype.pipeline.create import (\n    SUBSET_NAME_ALLOWED_SYMBOLS,\n    legacy_create,\n    CreatorError,\n)\n\nfrom .model import CreatorsModel\nfrom .widgets import (\n    CreateErrorMessageBox,\n    VariantLineEdit,\n    FamilyDescriptionWidget\n)\nfrom .constants import (\n    ITEM_ID_ROLE,\n    SEPARATOR,\n    SEPARATORS\n)\n\nmodule = sys.modules[__name__]\nmodule.window = None\n\n\nclass CreatorWindow(QtWidgets.QDialog):\n    def __init__(self, parent=None):\n        super(CreatorWindow, self).__init__(parent)\n        self.setWindowTitle(\"Instance Creator\")\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n        if not parent:\n            self.setWindowFlags(\n                self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint\n            )\n\n        creator_info = FamilyDescriptionWidget(self)\n\n        creators_model = CreatorsModel()\n\n        creators_proxy = QtCore.QSortFilterProxyModel()\n        creators_proxy.setSourceModel(creators_model)\n\n        creators_view = QtWidgets.QListView(self)\n        creators_view.setObjectName(\"CreatorsView\")\n        creators_view.setModel(creators_proxy)\n\n        asset_name_input = QtWidgets.QLineEdit(self)\n        variant_input = VariantLineEdit(self)\n        subset_name_input = QtWidgets.QLineEdit(self)\n        subset_name_input.setEnabled(False)\n\n        subset_button = QtWidgets.QPushButton()\n        subset_button.setFixedWidth(18)\n        subset_menu = QtWidgets.QMenu(subset_button)\n        subset_button.setMenu(subset_menu)\n\n        name_layout = QtWidgets.QHBoxLayout()\n        name_layout.addWidget(variant_input)\n        name_layout.addWidget(subset_button)\n        name_layout.setSpacing(3)\n        name_layout.setContentsMargins(0, 0, 0, 0)\n\n        body_layout = QtWidgets.QVBoxLayout()\n        body_layout.setContentsMargins(0, 0, 0, 0)\n\n        body_layout.addWidget(creator_info, 0)\n        body_layout.addWidget(QtWidgets.QLabel(\"Family\", self), 0)\n        body_layout.addWidget(creators_view, 1)\n        body_layout.addWidget(QtWidgets.QLabel(\"Asset\", self), 0)\n        body_layout.addWidget(asset_name_input, 0)\n        body_layout.addWidget(QtWidgets.QLabel(\"Subset\", self), 0)\n        body_layout.addLayout(name_layout, 0)\n        body_layout.addWidget(subset_name_input, 0)\n\n        useselection_chk = QtWidgets.QCheckBox(\"Use selection\", self)\n        useselection_chk.setCheckState(QtCore.Qt.Checked)\n\n        create_btn = QtWidgets.QPushButton(\"Create\", self)\n        # Need to store error_msg to prevent garbage collection\n        msg_label = QtWidgets.QLabel(self)\n\n        footer_layout = QtWidgets.QVBoxLayout()\n        footer_layout.addWidget(create_btn, 0)\n        footer_layout.addWidget(msg_label, 0)\n        footer_layout.setContentsMargins(0, 0, 0, 0)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addLayout(body_layout, 1)\n        layout.addWidget(useselection_chk, 0, QtCore.Qt.AlignLeft)\n        layout.addLayout(footer_layout, 0)\n\n        msg_timer = QtCore.QTimer()\n        msg_timer.setSingleShot(True)\n        msg_timer.setInterval(5000)\n\n        validation_timer = QtCore.QTimer()\n        validation_timer.setSingleShot(True)\n        validation_timer.setInterval(300)\n\n        msg_timer.timeout.connect(self._on_msg_timer)\n        validation_timer.timeout.connect(self._on_validation_timer)\n\n        create_btn.clicked.connect(self._on_create)\n        variant_input.returnPressed.connect(self._on_create)\n        variant_input.textChanged.connect(self._on_data_changed)\n        variant_input.report.connect(self.echo)\n        asset_name_input.textChanged.connect(self._on_data_changed)\n        creators_view.selectionModel().currentChanged.connect(\n            self._on_selection_changed\n        )\n\n        # Store valid states and\n        self._is_valid = False\n        create_btn.setEnabled(self._is_valid)\n\n        self._first_show = True\n\n        # Message dialog when something goes wrong during creation\n        self._message_dialog = None\n\n        self._creator_info = creator_info\n        self._create_btn = create_btn\n        self._useselection_chk = useselection_chk\n        self._variant_input = variant_input\n        self._subset_name_input = subset_name_input\n        self._asset_name_input = asset_name_input\n\n        self._creators_model = creators_model\n        self._creators_proxy = creators_proxy\n        self._creators_view = creators_view\n\n        self._subset_btn = subset_button\n        self._subset_menu = subset_menu\n\n        self._msg_label = msg_label\n\n        self._validation_timer = validation_timer\n        self._msg_timer = msg_timer\n\n        # Defaults\n        self.resize(300, 500)\n        variant_input.setFocus()\n\n    def _set_valid_state(self, valid):\n        if self._is_valid == valid:\n            return\n        self._is_valid = valid\n        self._create_btn.setEnabled(valid)\n\n    def _build_menu(self, default_names=None):\n        \"\"\"Create optional predefined subset names\n\n        Args:\n            default_names(list): all predefined names\n\n        Returns:\n             None\n        \"\"\"\n        if not default_names:\n            default_names = []\n\n        menu = self._subset_menu\n        button = self._subset_btn\n\n        # Get and destroy the action group\n        group = button.findChild(QtWidgets.QActionGroup)\n        if group:\n            group.deleteLater()\n\n        state = any(default_names)\n        button.setEnabled(state)\n        if state is False:\n            return\n\n        # Build new action group\n        group = QtWidgets.QActionGroup(button)\n        for name in default_names:\n            if name in SEPARATORS:\n                menu.addSeparator()\n                continue\n            action = group.addAction(name)\n            menu.addAction(action)\n\n        group.triggered.connect(self._on_action_clicked)\n\n    def _on_action_clicked(self, action):\n        self._variant_input.setText(action.text())\n\n    def _on_data_changed(self, *args):\n        # Set invalid state until it's reconfirmed to be valid by the\n        # scheduled callback so any form of creation is held back until\n        # valid again\n        self._set_valid_state(False)\n\n        self._validation_timer.start()\n\n    def _on_validation_timer(self):\n        index = self._creators_view.currentIndex()\n        item_id = index.data(ITEM_ID_ROLE)\n        creator_plugin = self._creators_model.get_creator_by_id(item_id)\n        user_input_text = self._variant_input.text()\n        asset_name = self._asset_name_input.text()\n\n        # Early exit if no asset name\n        if not asset_name:\n            self._build_menu()\n            self.echo(\"Asset name is required ..\")\n            self._set_valid_state(False)\n            return\n\n        project_name = get_current_project_name()\n        asset_doc = None\n        if creator_plugin:\n            # Get the asset from the database which match with the name\n            asset_doc = get_asset_by_name(\n                project_name, asset_name, fields=[\"_id\"]\n            )\n\n        # Get plugin\n        if not asset_doc or not creator_plugin:\n            subset_name = user_input_text\n            self._build_menu()\n\n            if not creator_plugin:\n                self.echo(\"No registered families ..\")\n            else:\n                self.echo(\"Asset '%s' not found ..\" % asset_name)\n            self._set_valid_state(False)\n            return\n\n        asset_id = asset_doc[\"_id\"]\n        task_name = get_current_task_name()\n\n        # Calculate subset name with Creator plugin\n        subset_name = creator_plugin.get_subset_name(\n            user_input_text, task_name, asset_id, project_name\n        )\n        # Force replacement of prohibited symbols\n        # QUESTION should Creator care about this and here should be only\n        #   validated with schema regex?\n\n        # Allow curly brackets in subset name for dynamic keys\n        curly_left = \"__cbl__\"\n        curly_right = \"__cbr__\"\n        tmp_subset_name = (\n            subset_name\n            .replace(\"{\", curly_left)\n            .replace(\"}\", curly_right)\n        )\n        # Replace prohibited symbols\n        tmp_subset_name = re.sub(\n            \"[^{}]+\".format(SUBSET_NAME_ALLOWED_SYMBOLS),\n            \"\",\n            tmp_subset_name\n        )\n        subset_name = (\n            tmp_subset_name\n            .replace(curly_left, \"{\")\n            .replace(curly_right, \"}\")\n        )\n        self._subset_name_input.setText(subset_name)\n\n        # Get all subsets of the current asset\n        subset_docs = get_subsets(\n            project_name, asset_ids=[asset_id], fields=[\"name\"]\n        )\n        existing_subset_names = {\n            subset_doc[\"name\"]\n            for subset_doc in subset_docs\n        }\n        existing_subset_names_low = set(\n            _name.lower()\n            for _name in existing_subset_names\n        )\n\n        # Defaults to dropdown\n        defaults = []\n        # Check if Creator plugin has set defaults\n        if (\n            creator_plugin.defaults\n            and isinstance(creator_plugin.defaults, (list, tuple, set))\n        ):\n            defaults = list(creator_plugin.defaults)\n\n        # Replace\n        compare_regex = re.compile(re.sub(\n            user_input_text, \"(.+)\", subset_name, flags=re.IGNORECASE\n        ))\n        subset_hints = set()\n        if user_input_text:\n            for _name in existing_subset_names:\n                _result = compare_regex.search(_name)\n                if _result:\n                    subset_hints |= set(_result.groups())\n\n        if subset_hints:\n            if defaults:\n                defaults.append(SEPARATOR)\n            defaults.extend(subset_hints)\n        self._build_menu(defaults)\n\n        # Indicate subset existence\n        if not user_input_text:\n            self._variant_input.as_empty()\n        elif subset_name.lower() in existing_subset_names_low:\n            # validate existence of subset name with lowered text\n            #   - \"renderMain\" vs. \"rensermain\" mean same path item for\n            #   windows\n            self._variant_input.as_exists()\n        else:\n            self._variant_input.as_new()\n\n        # Update the valid state\n        valid = subset_name.strip() != \"\"\n\n        self._set_valid_state(valid)\n\n    def _on_selection_changed(self, old_idx, new_idx):\n        index = self._creators_view.currentIndex()\n        item_id = index.data(ITEM_ID_ROLE)\n\n        creator_plugin = self._creators_model.get_creator_by_id(item_id)\n\n        self._creator_info.set_item(creator_plugin)\n\n        if creator_plugin is None:\n            return\n\n        default = None\n        if hasattr(creator_plugin, \"get_default_variant\"):\n            default = creator_plugin.get_default_variant()\n\n        if not default:\n            if (\n                creator_plugin.defaults\n                and isinstance(creator_plugin.defaults, list)\n            ):\n                default = creator_plugin.defaults[0]\n            else:\n                default = \"Default\"\n\n        self._variant_input.setText(default)\n\n        self._on_data_changed()\n\n    def keyPressEvent(self, event):\n        \"\"\"Custom keyPressEvent.\n\n        Override keyPressEvent to do nothing so that Maya's panels won't\n        take focus when pressing \"SHIFT\" whilst mouse is over viewport or\n        outliner. This way users don't accidentally perform Maya commands\n        whilst trying to name an instance.\n\n        \"\"\"\n        pass\n\n    def showEvent(self, event):\n        super(CreatorWindow, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self.setStyleSheet(style.load_stylesheet())\n\n    def refresh(self):\n        self._asset_name_input.setText(get_current_asset_name())\n\n        self._creators_model.reset()\n\n        pype_project_setting = (\n            get_current_project_settings()\n            [\"global\"]\n            [\"tools\"]\n            [\"creator\"]\n            [\"families_smart_select\"]\n        )\n        current_index = None\n        family = None\n        task_name = get_current_task_name() or None\n        lowered_task_name = task_name.lower()\n        if task_name:\n            for _family, _task_names in pype_project_setting.items():\n                _low_task_names = {name.lower() for name in _task_names}\n                for _task_name in _low_task_names:\n                    if _task_name in lowered_task_name:\n                        family = _family\n                        break\n                if family:\n                    break\n\n        if family:\n            indexes = self._creators_model.get_indexes_by_family(family)\n            if indexes:\n                index = indexes[0]\n                current_index = self._creators_proxy.mapFromSource(index)\n\n        if current_index is None or not current_index.isValid():\n            current_index = self._creators_proxy.index(0, 0)\n\n        self._creators_view.setCurrentIndex(current_index)\n\n    def _on_create(self):\n        # Do not allow creation in an invalid state\n        if not self._is_valid:\n            return\n\n        index = self._creators_view.currentIndex()\n        item_id = index.data(ITEM_ID_ROLE)\n        creator_plugin = self._creators_model.get_creator_by_id(item_id)\n        if creator_plugin is None:\n            return\n\n        subset_name = self._subset_name_input.text()\n        asset_name = self._asset_name_input.text()\n        use_selection = self._useselection_chk.isChecked()\n\n        variant = self._variant_input.text()\n\n        error_info = None\n        try:\n            legacy_create(\n                creator_plugin,\n                subset_name,\n                asset_name,\n                options={\"useSelection\": use_selection},\n                data={\"variant\": variant}\n            )\n\n        except CreatorError as exc:\n            self.echo(\"Creator error: {}\".format(str(exc)))\n            error_info = (str(exc), None)\n\n        except Exception as exc:\n            self.echo(\"Program error: %s\" % str(exc))\n\n            exc_type, exc_value, exc_traceback = sys.exc_info()\n            formatted_traceback = \"\".join(traceback.format_exception(\n                exc_type, exc_value, exc_traceback\n            ))\n            error_info = (str(exc), formatted_traceback)\n\n        if error_info:\n            box = CreateErrorMessageBox(\n                creator_plugin.family,\n                subset_name,\n                asset_name,\n                *error_info,\n                parent=self\n            )\n            box.show()\n            # Store dialog so is not garbage collected before is shown\n            self._message_dialog = box\n\n        else:\n            self.echo(\"Created %s ..\" % subset_name)\n\n    def _on_msg_timer(self):\n        self._msg_label.setText(\"\")\n\n    def echo(self, message):\n        self._msg_label.setText(str(message))\n        self._msg_timer.start()\n\n\ndef show(parent=None):\n    \"\"\"Display asset creator GUI\n\n    Arguments:\n        debug (bool, optional): Run loader in debug-mode,\n            defaults to False\n        parent (QtCore.QObject, optional): When provided parent the interface\n            to this QObject.\n\n    \"\"\"\n\n    try:\n        module.window.close()\n        del(module.window)\n    except (AttributeError, RuntimeError):\n        pass\n\n    with qt_app_context():\n        window = CreatorWindow(parent)\n        window.refresh()\n        window.show()\n\n        module.window = window\n\n        # Pull window to the front.\n        module.window.raise_()\n        module.window.activateWindow()\n"
  },
  {
    "path": "openpype/tools/experimental_tools/__init__.py",
    "content": "from .tools_def import (\n    ExperimentalTools,\n    LOCAL_EXPERIMENTAL_KEY\n)\n\nfrom .dialog import ExperimentalToolsDialog\n\n\n__all__ = (\n    \"ExperimentalTools\",\n    \"LOCAL_EXPERIMENTAL_KEY\",\n\n    \"ExperimentalToolsDialog\"\n)\n"
  },
  {
    "path": "openpype/tools/experimental_tools/dialog.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.style import (\n    load_stylesheet,\n    app_icon_path\n)\n\nfrom .tools_def import ExperimentalTools\n\n\nclass ToolButton(QtWidgets.QPushButton):\n    triggered = QtCore.Signal(str)\n\n    def __init__(self, identifier, *args, **kwargs):\n        super(ToolButton, self).__init__(*args, **kwargs)\n        self._identifier = identifier\n\n        self.clicked.connect(self._on_click)\n\n    def _on_click(self):\n        self.triggered.emit(self._identifier)\n\n\nclass ExperimentalToolsDialog(QtWidgets.QDialog):\n    refresh_interval = 3000\n\n    def __init__(self, parent=None):\n        super(ExperimentalToolsDialog, self).__init__(parent)\n        app_label = \"AYON\" if AYON_SERVER_ENABLED else \"OpenPype\"\n        self.setWindowTitle(\"{} Experimental tools\".format(app_label))\n        icon = QtGui.QIcon(app_icon_path())\n        self.setWindowIcon(icon)\n        self.setStyleSheet(load_stylesheet())\n\n        # Widgets for cases there are not available experimental tools\n        empty_widget = QtWidgets.QWidget(self)\n\n        empty_label = QtWidgets.QLabel(\n            \"There are no experimental tools available...\", empty_widget\n        )\n\n        empty_btns_layout = QtWidgets.QHBoxLayout()\n        ok_btn = QtWidgets.QPushButton(\"OK\", empty_widget)\n\n        empty_btns_layout.setContentsMargins(0, 0, 0, 0)\n        empty_btns_layout.addStretch(1)\n        empty_btns_layout.addWidget(ok_btn, 0)\n\n        empty_layout = QtWidgets.QVBoxLayout(empty_widget)\n        empty_layout.setContentsMargins(0, 0, 0, 0)\n        empty_layout.addWidget(empty_label)\n        empty_layout.addStretch(1)\n        empty_layout.addLayout(empty_btns_layout)\n\n        # Content of Experimental tools\n\n        # Layout where buttons are added\n        content_layout = QtWidgets.QVBoxLayout()\n        content_layout.setContentsMargins(0, 0, 0, 0)\n\n        # Separator line\n        separator_widget = QtWidgets.QWidget(self)\n        separator_widget.setObjectName(\"Separator\")\n        separator_widget.setMinimumHeight(2)\n        separator_widget.setMaximumHeight(2)\n\n        # Label describing how to turn off tools\n        tool_btns_widget = QtWidgets.QWidget(self)\n        tool_btns_label = QtWidgets.QLabel(\n            (\n                \"You can enable these features in\"\n                \"<br><b>{} tray -> Settings -> Experimental tools</b>\"\n            ).format(app_label),\n            tool_btns_widget\n        )\n        tool_btns_label.setAlignment(QtCore.Qt.AlignCenter)\n\n        tool_btns_layout = QtWidgets.QVBoxLayout(tool_btns_widget)\n        tool_btns_layout.setContentsMargins(0, 0, 0, 0)\n        tool_btns_layout.addLayout(content_layout)\n        tool_btns_layout.addStretch(1)\n        tool_btns_layout.addWidget(separator_widget, 0)\n        tool_btns_layout.addWidget(tool_btns_label, 0)\n\n        experimental_tools = ExperimentalTools(\n            parent_widget=parent, refresh=False\n        )\n\n        # Main layout\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(empty_widget, 1)\n        layout.addWidget(tool_btns_widget, 1)\n\n        refresh_timer = QtCore.QTimer()\n        refresh_timer.setInterval(self.refresh_interval)\n        refresh_timer.timeout.connect(self._on_refresh_timeout)\n\n        ok_btn.clicked.connect(self._on_ok_click)\n\n        self._empty_widget = empty_widget\n        self._tool_btns_widget = tool_btns_widget\n        self._content_layout = content_layout\n\n        self._experimental_tools = experimental_tools\n        self._buttons_by_tool_identifier = {}\n\n        self._refresh_timer = refresh_timer\n\n        # Is dialog first shown\n        self._first_show = True\n        # Trigger refresh when window gets activity\n        self._refresh_on_active = True\n        # Is window active\n        self._window_is_active = False\n\n    def refresh(self):\n        app_label = \"AYON\" if AYON_SERVER_ENABLED else \"OpenPype\"\n        self._experimental_tools.refresh_availability()\n\n        buttons_to_remove = set(self._buttons_by_tool_identifier.keys())\n        tools = self._experimental_tools.get_tools_for_host()\n        for idx, tool in enumerate(tools):\n            identifier = tool.identifier\n            if identifier in buttons_to_remove:\n                buttons_to_remove.remove(identifier)\n                is_new = False\n                button = self._buttons_by_tool_identifier[identifier]\n            else:\n                is_new = True\n                button = ToolButton(identifier, self._tool_btns_widget)\n                button.triggered.connect(self._on_btn_trigger)\n                self._buttons_by_tool_identifier[identifier] = button\n                self._content_layout.insertWidget(idx, button)\n\n            if button.text() != tool.label:\n                button.setText(tool.label)\n\n            if tool.enabled:\n                button.setToolTip(tool.tooltip)\n\n            elif is_new or button.isEnabled():\n                button.setToolTip((\n                    \"You can enable this tool in local settings.\"\n                    \"\\n\\n{} Tray > Settings > Experimental Tools\"\n                ).format(app_label))\n\n            if tool.enabled != button.isEnabled():\n                button.setEnabled(tool.enabled)\n\n        for identifier in buttons_to_remove:\n            button = self._buttons_by_tool_identifier.pop(identifier)\n            button.setVisible(False)\n            idx = self._content_layout.indexOf(button)\n            self._content_layout.takeAt(idx)\n            button.deleteLater()\n\n        self._set_visibility()\n\n    def _is_content_visible(self):\n        return len(self._buttons_by_tool_identifier) > 0\n\n    def _set_visibility(self):\n        content_visible = self._is_content_visible()\n        self._tool_btns_widget.setVisible(content_visible)\n        self._empty_widget.setVisible(not content_visible)\n\n    def _on_ok_click(self):\n        self.close()\n\n    def _on_btn_trigger(self, identifier):\n        tool = self._experimental_tools.tools_by_identifier.get(identifier)\n        if tool is not None:\n            tool.execute()\n\n    def showEvent(self, event):\n        super(ExperimentalToolsDialog, self).showEvent(event)\n\n        if self._refresh_on_active:\n            # Start/Restart timer\n            self._refresh_timer.start()\n            # Refresh\n            self.refresh()\n\n        elif not self._refresh_timer.isActive():\n            self._refresh_timer.start()\n\n        if self._first_show:\n            self._first_show = False\n            # Set stylesheet\n            self.setStyleSheet(load_stylesheet())\n            # Resize dialog if there is not content\n            if not self._is_content_visible():\n                size = self.size()\n                size.setWidth(size.width() + size.width() / 3)\n                self.resize(size)\n\n    def changeEvent(self, event):\n        if event.type() == QtCore.QEvent.ActivationChange:\n            self._window_is_active = self.isActiveWindow()\n            if self._window_is_active and self._refresh_on_active:\n                self._refresh_timer.start()\n                self.refresh()\n\n        super(ExperimentalToolsDialog, self).changeEvent(event)\n\n    def _on_refresh_timeout(self):\n        # Stop timer if window is not visible\n        if not self.isVisible():\n            self._refresh_on_active = True\n            self._refresh_timer.stop()\n\n        # Skip refreshing if window is not active\n        elif not self._window_is_active:\n            self._refresh_on_active = True\n\n        # Window is active and visible so we're refreshing buttons\n        else:\n            self.refresh()\n"
  },
  {
    "path": "openpype/tools/experimental_tools/tools_def.py",
    "content": "import os\nfrom openpype.settings import get_local_settings\n\n# Constant key under which local settings are stored\nLOCAL_EXPERIMENTAL_KEY = \"experimental_tools\"\n\n\nclass ExperimentalTool(object):\n    \"\"\"Definition of experimental tool.\n\n    Definition is used in local settings.\n\n    Args:\n        identifier (str): String identifier of tool (unique).\n        label (str): Label shown in UI.\n    \"\"\"\n    def __init__(self, identifier, label, tooltip):\n        self.identifier = identifier\n        self.label = label\n        self.tooltip = tooltip\n        self._enabled = True\n\n    @property\n    def enabled(self):\n        \"\"\"Is tool enabled and button is clickable.\"\"\"\n        return self._enabled\n\n    def set_enabled(self, enabled=True):\n        \"\"\"Change if tool is enabled.\"\"\"\n        self._enabled = enabled\n\n\nclass ExperimentalHostTool(ExperimentalTool):\n    \"\"\"Definition of experimental tool.\n\n    Definition is used in local settings and in experimental tools dialog.\n\n    Args:\n        identifier (str): String identifier of tool (unique).\n        label (str): Label shown in UI.\n        callback (function): Callback for UI button.\n        tooltip (str): Tooltip showed on button.\n        hosts_filter (list): List of host names for which is tool available.\n            Some tools may not be available in all hosts.\n    \"\"\"\n    def __init__(\n        self, identifier, label, tooltip, callback, hosts_filter=None\n    ):\n        super(ExperimentalHostTool, self).__init__(identifier, label, tooltip)\n        self.callback = callback\n        self.hosts_filter = hosts_filter\n        self._enabled = True\n\n    def is_available_for_host(self, host_name):\n        if self.hosts_filter:\n            return host_name in self.hosts_filter\n        return True\n\n    def execute(self, *args, **kwargs):\n        \"\"\"Trigger registered callback.\"\"\"\n        self.callback(*args, **kwargs)\n\n\nclass ExperimentalTools:\n    \"\"\"Wrapper around experimental tools.\n\n    To add/remove experimental tool just add/remove tool to\n    `experimental_tools` variable in __init__ function.\n\n    --- Example tool (callback will just print on click) ---\n    def example_callback(*args):\n        print(\"Triggered tool\")\n\n    experimental_tools = [\n        ExperimentalHostTool(\n            \"example\",\n            \"Example experimental tool\",\n            example_callback,\n            \"Example tool tooltip.\"\n        )\n    ]\n    ---\n    \"\"\"\n    def __init__(self, parent_widget=None, refresh=True):\n        # Definition of experimental tools\n        experimental_tools = [\n            ExperimentalHostTool(\n                \"publisher\",\n                \"New publisher\",\n                \"Combined creation and publishing into one tool.\",\n                self._show_publisher,\n                hosts_filter=[\"blender\", \"maya\", \"nuke\", \"celaction\", \"flame\",\n                              \"fusion\", \"harmony\", \"hiero\", \"resolve\",\n                              \"tvpaint\", \"unreal\"]\n            )\n        ]\n\n        # Store tools by identifier\n        tools_by_identifier = {}\n        for tool in experimental_tools:\n            if tool.identifier in tools_by_identifier:\n                raise KeyError((\n                    \"Duplicated experimental tool identifier \\\"{}\\\"\"\n                ).format(tool.identifier))\n            tools_by_identifier[tool.identifier] = tool\n\n        self._tools_by_identifier = tools_by_identifier\n        self._tools = experimental_tools\n\n        self._parent_widget = parent_widget\n        self._publisher_tool = None\n\n        if refresh:\n            self.refresh_availability()\n\n    @property\n    def tools(self):\n        \"\"\"Tools in list.\n\n        Returns:\n            list: Tools filtered by host name if filtering was enabled\n                on initialization.\n        \"\"\"\n        return self._tools\n\n    @property\n    def tools_by_identifier(self):\n        \"\"\"Tools by their identifier.\n\n        Returns:\n            dict: Tools by identifier filtered by host name if filtering\n                was enabled on initialization.\n        \"\"\"\n        return self._tools_by_identifier\n\n    def get(self, tool_identifier):\n        \"\"\"Get tool by identifier.\"\"\"\n        return self.tools_by_identifier.get(tool_identifier)\n\n    def get_tools_for_host(self, host_name=None):\n        if not host_name:\n            host_name = os.environ.get(\"AVALON_APP\")\n        tools = []\n        for tool in self.tools:\n            if (\n                isinstance(tool, ExperimentalHostTool)\n                and tool.is_available_for_host(host_name)\n            ):\n                tools.append(tool)\n        return tools\n\n    def refresh_availability(self):\n        \"\"\"Reload local settings and check if any tool changed ability.\"\"\"\n        local_settings = get_local_settings()\n        experimental_settings = (\n            local_settings.get(LOCAL_EXPERIMENTAL_KEY)\n        ) or {}\n\n        for identifier, eperimental_tool in self.tools_by_identifier.items():\n            enabled = experimental_settings.get(identifier, False)\n            eperimental_tool.set_enabled(enabled)\n\n    def _show_publisher(self):\n        if self._publisher_tool is None:\n            from openpype.tools.publisher.window import PublisherWindow\n\n            self._publisher_tool = PublisherWindow(\n                parent=self._parent_widget\n            )\n\n        self._publisher_tool.show()\n"
  },
  {
    "path": "openpype/tools/flickcharm.py",
    "content": "\"\"\"\nThis based on the flickcharm-python code from:\n    https://code.google.com/archive/p/flickcharm-python/\n\nWhich states:\n    This is a Python (PyQt) port of Ariya Hidayat's elegant FlickCharm\n    hack which adds kinetic scrolling to any scrollable Qt widget.\n\n    Licensed under GNU GPL version 2 or later.\n\nIt has been altered to fix edge cases where clicks and drags would not\npropagate correctly under some conditions. It also allows a small \"dead zone\"\nthreshold in which it will still propagate the user pressed click if he or she\ntravelled only very slightly with the cursor.\n\n\"\"\"\n\nimport copy\nfrom qtpy import QtWidgets, QtCore, QtGui\n\n\nclass FlickData(object):\n    Steady = 0\n    Pressed = 1\n    ManualScroll = 2\n    AutoScroll = 3\n    Stop = 4\n\n    def __init__(self):\n        self.state = FlickData.Steady\n        self.widget = None\n        self.pressPos = QtCore.QPoint(0, 0)\n        self.offset = QtCore.QPoint(0, 0)\n        self.dragPos = QtCore.QPoint(0, 0)\n        self.speed = QtCore.QPoint(0, 0)\n        self.travelled = 0\n        self.ignored = []\n\n\nclass FlickCharm(QtCore.QObject):\n    \"\"\"Make scrollable widgets flickable.\n\n    For example:\n        charm = FlickCharm()\n        charm.activateOn(widget)\n\n    It can `activateOn` multiple widgets with a single FlickCharm instance.\n    Be aware that the FlickCharm object must be kept around for it not\n    to get garbage collected and losing the flickable behavior.\n\n    Flick away!\n\n    \"\"\"\n\n    def __init__(self, parent=None):\n        super(FlickCharm, self).__init__(parent=parent)\n\n        self.flickData = {}\n        self.ticker = QtCore.QBasicTimer()\n\n        # The flick button to use\n        self.button = QtCore.Qt.LeftButton\n\n        # The time taken per update tick of flicking behavior\n        self.tick_time = 20\n\n        # Allow a item click/press directly when AutoScroll is slower than\n        # this threshold velocity\n        self.click_in_autoscroll_threshold = 10\n\n        # Allow an item click/press to propagate as opposed to scrolling\n        # when the cursor travelled less than this amount of pixels\n        # Note: back & forth motion increases the value too\n        self.travel_threshold = 20\n\n        self.max_speed = 64  # max scroll speed\n        self.drag = 1  # higher drag will stop autoscroll faster\n\n    def activateOn(self, widget):\n        viewport = widget.viewport()\n        viewport.installEventFilter(self)\n        widget.installEventFilter(self)\n        self.flickData[viewport] = FlickData()\n        self.flickData[viewport].widget = widget\n        self.flickData[viewport].state = FlickData.Steady\n\n    def deactivateFrom(self, widget):\n\n        viewport = widget.viewport()\n        viewport.removeEventFilter(self)\n        widget.removeEventFilter(self)\n        self.flickData.pop(viewport)\n\n    def eventFilter(self, obj, event):\n\n        if not obj.isWidgetType():\n            return False\n\n        eventType = event.type()\n        if eventType != QtCore.QEvent.MouseButtonPress and \\\n                eventType != QtCore.QEvent.MouseButtonRelease and \\\n                eventType != QtCore.QEvent.MouseMove:\n            return False\n\n        if event.modifiers() != QtCore.Qt.NoModifier:\n            return False\n\n        if obj not in self.flickData:\n            return False\n\n        data = self.flickData[obj]\n        found, newIgnored = removeAll(data.ignored, event)\n        if found:\n            data.ignored = newIgnored\n            return False\n\n        if data.state == FlickData.Steady:\n            if eventType == QtCore.QEvent.MouseButtonPress:\n                if event.buttons() == self.button:\n                    self._set_press_pos_and_offset(event, data)\n                    data.state = FlickData.Pressed\n                    return True\n\n        elif data.state == FlickData.Pressed:\n            if eventType == QtCore.QEvent.MouseButtonRelease:\n                # User didn't actually scroll but clicked in\n                # the widget. Let the original press and release\n                # event be evaluated on the Widget\n                data.state = FlickData.Steady\n                event1 = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress,\n                                           data.pressPos,\n                                           QtCore.Qt.LeftButton,\n                                           QtCore.Qt.LeftButton,\n                                           QtCore.Qt.NoModifier)\n                # Copy the current event\n                event2 = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonRelease,\n                                           event.pos(),\n                                           event.button(),\n                                           event.buttons(),\n                                           event.modifiers())\n                data.ignored.append(event1)\n                data.ignored.append(event2)\n                QtWidgets.QApplication.postEvent(obj, event1)\n                QtWidgets.QApplication.postEvent(obj, event2)\n                return True\n            elif eventType == QtCore.QEvent.MouseMove:\n                data.state = FlickData.ManualScroll\n                data.dragPos = QtGui.QCursor.pos()\n                if not self.ticker.isActive():\n                    self.ticker.start(self.tick_time, self)\n                return True\n\n        elif data.state == FlickData.ManualScroll:\n            if eventType == QtCore.QEvent.MouseMove:\n                pos = event.pos()\n                delta = pos - data.pressPos\n                data.travelled += delta.manhattanLength()\n                setScrollOffset(data.widget, data.offset - delta)\n                return True\n            elif eventType == QtCore.QEvent.MouseButtonRelease:\n\n                if data.travelled <= self.travel_threshold:\n                    # If the user travelled less than the threshold\n                    # don't go into autoscroll mode but assume the user\n                    # intended to click instead\n                    return self._propagate_click(obj, event, data)\n\n                data.state = FlickData.AutoScroll\n                return True\n\n        elif data.state == FlickData.AutoScroll:\n            if eventType == QtCore.QEvent.MouseButtonPress:\n\n                # Allow pressing when auto scroll is already slower than\n                # the click in autoscroll threshold\n                velocity = data.speed.manhattanLength()\n                if velocity <= self.click_in_autoscroll_threshold:\n                    self._set_press_pos_and_offset(event, data)\n                    data.state = FlickData.Pressed\n                else:\n                    data.state = FlickData.Stop\n\n                data.speed = QtCore.QPoint(0, 0)\n                return True\n            elif eventType == QtCore.QEvent.MouseButtonRelease:\n                data.state = FlickData.Steady\n                data.speed = QtCore.QPoint(0, 0)\n                return True\n\n        elif data.state == FlickData.Stop:\n            if eventType == QtCore.QEvent.MouseButtonRelease:\n                data.state = FlickData.Steady\n\n                # If the user had a very limited scroll smaller than the\n                # threshold consider it a regular press and release.\n                if data.travelled < self.travel_threshold:\n                    return self._propagate_click(obj, event, data)\n\n                return True\n            elif eventType == QtCore.QEvent.MouseMove:\n                # Reset the press position and offset to allow us to \"continue\"\n                # the scroll from the new point the user clicked and then held\n                # down to continue scrolling after AutoScroll.\n                self._set_press_pos_and_offset(event, data)\n                data.state = FlickData.ManualScroll\n\n                data.dragPos = QtGui.QCursor.pos()\n                if not self.ticker.isActive():\n                    self.ticker.start(self.tick_time, self)\n                return True\n\n        return False\n\n    def _set_press_pos_and_offset(self, event, data):\n        \"\"\"Store current event position on Press\"\"\"\n        data.state = FlickData.Pressed\n        data.pressPos = copy.copy(event.pos())\n        data.offset = scrollOffset(data.widget)\n        data.travelled = 0\n\n    def _propagate_click(self, obj, event, data):\n        \"\"\"Propagate from Pressed state with MouseButtonRelease event.\n\n        Use only on button release in certain states to propagate a click,\n        for example when the user dragged only a slight distance under the\n        travel threshold.\n\n        \"\"\"\n\n        data.state = FlickData.Pressed\n        data.pressPos = copy.copy(event.pos())\n        data.offset = scrollOffset(data.widget)\n        data.travelled = 0\n        self.eventFilter(obj, event)\n        return True\n\n    def timerEvent(self, event):\n\n        count = 0\n        for data in self.flickData.values():\n            if data.state == FlickData.ManualScroll:\n                count += 1\n                cursorPos = QtGui.QCursor.pos()\n                data.speed = cursorPos - data.dragPos\n                data.dragPos = cursorPos\n            elif data.state == FlickData.AutoScroll:\n                count += 1\n                data.speed = deaccelerate(data.speed,\n                                          a=self.drag,\n                                          maxVal=self.max_speed)\n                p = scrollOffset(data.widget)\n                new_p = p - data.speed\n                setScrollOffset(data.widget, new_p)\n\n                if scrollOffset(data.widget) == p:\n                    # If this scroll resulted in no change on the widget\n                    # we reached the end of the list and set the speed to\n                    # zero.\n                    data.speed = QtCore.QPoint(0, 0)\n\n                if data.speed == QtCore.QPoint(0, 0):\n                    data.state = FlickData.Steady\n\n        if count == 0:\n            self.ticker.stop()\n\n        super(FlickCharm, self).timerEvent(event)\n\n\ndef scrollOffset(widget):\n    x = widget.horizontalScrollBar().value()\n    y = widget.verticalScrollBar().value()\n    return QtCore.QPoint(x, y)\n\n\ndef setScrollOffset(widget, p):\n    widget.horizontalScrollBar().setValue(p.x())\n    widget.verticalScrollBar().setValue(p.y())\n\n\ndef deaccelerate(speed, a=1, maxVal=64):\n\n    x = max(min(speed.x(), maxVal), -maxVal)\n    y = max(min(speed.y(), maxVal), -maxVal)\n    if x > 0:\n        x = max(0, x - a)\n    elif x < 0:\n        x = min(0, x + a)\n    if y > 0:\n        y = max(0, y - a)\n    elif y < 0:\n        y = min(0, y + a)\n    return QtCore.QPoint(x, y)\n\n\ndef removeAll(list, val):\n    found = False\n    ret = []\n    for element in list:\n        if element == val:\n            found = True\n        else:\n            ret.append(element)\n    return found, ret\n"
  },
  {
    "path": "openpype/tools/launcher/__init__.py",
    "content": "from .window import LauncherWindow\nfrom . import actions\n\n__all__ = [\n    \"LauncherWindow\",\n    \"actions\"\n]\n"
  },
  {
    "path": "openpype/tools/launcher/actions.py",
    "content": "from qtpy import QtWidgets, QtGui\n\nfrom openpype import style\nfrom openpype import resources\nfrom openpype.lib import (\n    Logger,\n    ApplictionExecutableNotFound,\n    ApplicationLaunchFailed\n)\nfrom openpype.pipeline import LauncherAction\n\n\n# TODO move to 'openpype.pipeline.actions'\n# - remove Qt related stuff and implement exceptions to show error in launcher\nclass ApplicationAction(LauncherAction):\n    \"\"\"Pype's application launcher\n\n    Application action based on pype's ApplicationManager system.\n    \"\"\"\n\n    # Application object\n    application = None\n    # Action attributes\n    name = None\n    label = None\n    label_variant = None\n    group = None\n    icon = None\n    color = None\n    order = 0\n    data = {}\n\n    _log = None\n    required_session_keys = (\n        \"AVALON_PROJECT\",\n        \"AVALON_ASSET\",\n        \"AVALON_TASK\"\n    )\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    def is_compatible(self, session):\n        for key in self.required_session_keys:\n            if key not in session:\n                return False\n        return True\n\n    def _show_message_box(self, title, message, details=None):\n        dialog = QtWidgets.QMessageBox()\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        dialog.setWindowIcon(icon)\n        dialog.setStyleSheet(style.load_stylesheet())\n        dialog.setWindowTitle(title)\n        dialog.setText(message)\n        if details:\n            dialog.setDetailedText(details)\n        dialog.exec_()\n\n    def process(self, session, **kwargs):\n        \"\"\"Process the full Application action\"\"\"\n\n        project_name = session[\"AVALON_PROJECT\"]\n        asset_name = session[\"AVALON_ASSET\"]\n        task_name = session[\"AVALON_TASK\"]\n        try:\n            self.application.launch(\n                project_name=project_name,\n                asset_name=asset_name,\n                task_name=task_name,\n                **self.data\n            )\n\n        except ApplictionExecutableNotFound as exc:\n            details = exc.details\n            msg = exc.msg\n            log_msg = str(msg)\n            if details:\n                log_msg += \"\\n\" + details\n            self.log.warning(log_msg)\n            self._show_message_box(\n                \"Application executable not found\", msg, details\n            )\n\n        except ApplicationLaunchFailed as exc:\n            msg = str(exc)\n            self.log.warning(msg, exc_info=True)\n            self._show_message_box(\"Application launch failed\", msg)\n"
  },
  {
    "path": "openpype/tools/launcher/constants.py",
    "content": "from qtpy import QtCore\n\n\nACTION_ROLE = QtCore.Qt.UserRole\nGROUP_ROLE = QtCore.Qt.UserRole + 1\nVARIANT_GROUP_ROLE = QtCore.Qt.UserRole + 2\nACTION_ID_ROLE = QtCore.Qt.UserRole + 3\nANIMATION_START_ROLE = QtCore.Qt.UserRole + 4\nANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 5\nFORCE_NOT_OPEN_WORKFILE_ROLE = QtCore.Qt.UserRole + 6\nACTION_TOOLTIP_ROLE = QtCore.Qt.UserRole + 7\n\n# Animation length in seconds\nANIMATION_LEN = 7\n"
  },
  {
    "path": "openpype/tools/launcher/delegates.py",
    "content": "import time\nfrom qtpy import QtCore, QtWidgets, QtGui\nfrom .constants import (\n    ANIMATION_START_ROLE,\n    ANIMATION_STATE_ROLE,\n    FORCE_NOT_OPEN_WORKFILE_ROLE\n)\n\n\nclass ActionDelegate(QtWidgets.QStyledItemDelegate):\n    extender_lines = 2\n    extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100, 160))\n    extender_fg = QtGui.QColor(255, 255, 255, 160)\n\n    def __init__(self, group_roles, *args, **kwargs):\n        super(ActionDelegate, self).__init__(*args, **kwargs)\n        self.group_roles = group_roles\n        self._anim_start_color = QtGui.QColor(178, 255, 246)\n        self._anim_end_color = QtGui.QColor(5, 44, 50)\n\n    def _draw_animation(self, painter, option, index):\n        grid_size = option.widget.gridSize()\n        x_offset = int(\n            (grid_size.width() / 2)\n            - (option.rect.width() / 2)\n        )\n        item_x = option.rect.x() - x_offset\n        rect_offset = grid_size.width() / 20\n        size = grid_size.width() - (rect_offset * 2)\n        anim_rect = QtCore.QRect(\n            item_x + rect_offset,\n            option.rect.y() + rect_offset,\n            size,\n            size\n        )\n\n        painter.save()\n\n        painter.setBrush(QtCore.Qt.transparent)\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n\n        gradient = QtGui.QConicalGradient()\n        gradient.setCenter(anim_rect.center())\n        gradient.setColorAt(0, self._anim_start_color)\n        gradient.setColorAt(1, self._anim_end_color)\n\n        time_diff = time.time() - index.data(ANIMATION_START_ROLE)\n\n        # Repeat 4 times\n        part_anim = 2.5\n        part_time = time_diff % part_anim\n        offset = (part_time / part_anim) * 360\n        angle = (offset + 90) % 360\n\n        gradient.setAngle(-angle)\n\n        pen = QtGui.QPen(QtGui.QBrush(gradient), rect_offset)\n        pen.setCapStyle(QtCore.Qt.RoundCap)\n        painter.setPen(pen)\n        painter.drawArc(\n            anim_rect,\n            -16 * (angle + 10),\n            -16 * offset\n        )\n\n        painter.restore()\n\n    def paint(self, painter, option, index):\n        if index.data(ANIMATION_STATE_ROLE):\n            self._draw_animation(painter, option, index)\n\n        super(ActionDelegate, self).paint(painter, option, index)\n\n        if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE):\n            rect = QtCore.QRectF(option.rect.x(), option.rect.height(),\n                                 5, 5)\n            painter.setPen(QtCore.Qt.transparent)\n            painter.setBrush(QtGui.QColor(200, 0, 0))\n            painter.drawEllipse(rect)\n\n            painter.setBrush(self.extender_bg_brush)\n\n        is_group = False\n        for group_role in self.group_roles:\n            is_group = index.data(group_role)\n            if is_group:\n                break\n        if not is_group:\n            return\n\n        grid_size = option.widget.gridSize()\n        x_offset = int(\n            (grid_size.width() / 2)\n            - (option.rect.width() / 2)\n        )\n        item_x = option.rect.x() - x_offset\n\n        tenth_width = int(grid_size.width() / 10)\n        tenth_height = int(grid_size.height() / 10)\n\n        extender_width = tenth_width * 2\n        extender_height = tenth_height * 2\n\n        exteder_rect = QtCore.QRectF(\n            item_x + tenth_width,\n            option.rect.y() + tenth_height,\n            extender_width,\n            extender_height\n        )\n        path = QtGui.QPainterPath()\n        path.addRoundedRect(exteder_rect, 2, 2)\n\n        painter.fillPath(path, self.extender_bg_brush)\n\n        painter.setPen(self.extender_fg)\n        painter.drawPath(path)\n\n        divider = (2 * self.extender_lines) + 1\n        extender_offset = int(extender_width / 6)\n        line_height = round(extender_height / divider)\n        line_width = extender_width - (extender_offset * 2) + 1\n        pos_x = exteder_rect.x() + extender_offset\n        pos_y = exteder_rect.y() + line_height\n        for _ in range(self.extender_lines):\n            line_rect = QtCore.QRectF(\n                pos_x, pos_y, line_width, line_height\n            )\n            painter.fillRect(line_rect, self.extender_fg)\n            pos_y += 2 * line_height\n"
  },
  {
    "path": "openpype/tools/launcher/lib.py",
    "content": "import os\nfrom qtpy import QtGui\nimport qtawesome\nfrom openpype import resources\n\nICON_CACHE = {}\nNOT_FOUND = type(\"NotFound\", (object, ), {})\n\n\ndef get_action_icon(action):\n    icon_name = action.icon\n    if not icon_name:\n        return None\n\n    global ICON_CACHE\n\n    icon = ICON_CACHE.get(icon_name)\n    if icon is NOT_FOUND:\n        return None\n    elif icon:\n        return icon\n\n    icon_path = resources.get_resource(icon_name)\n    if not os.path.exists(icon_path):\n        icon_path = icon_name.format(resources.RESOURCES_DIR)\n\n    if os.path.exists(icon_path):\n        icon = QtGui.QIcon(icon_path)\n        ICON_CACHE[icon_name] = icon\n        return icon\n\n    try:\n        icon_color = getattr(action, \"color\", None) or \"white\"\n        icon = qtawesome.icon(\n            \"fa.{}\".format(icon_name), color=icon_color\n        )\n\n    except Exception:\n        ICON_CACHE[icon_name] = NOT_FOUND\n        print(\"Can't load icon \\\"{}\\\"\".format(icon_name))\n\n    return icon\n\n\ndef get_action_label(action):\n    label = getattr(action, \"label\", None)\n    if not label:\n        return action.name\n\n    label_variant = getattr(action, \"label_variant\", None)\n    if not label_variant:\n        return label\n    return \" \".join([label, label_variant])\n"
  },
  {
    "path": "openpype/tools/launcher/models.py",
    "content": "import re\nimport uuid\nimport copy\nimport logging\nimport collections\nimport time\n\nimport appdirs\nfrom qtpy import QtCore, QtGui\nimport qtawesome\n\nfrom openpype.client import (\n    get_projects,\n    get_project,\n    get_assets,\n)\nfrom openpype.lib import JSONSettingRegistry\nfrom openpype.lib.applications import (\n    CUSTOM_LAUNCH_APP_GROUPS,\n    ApplicationManager\n)\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import discover_launcher_actions\nfrom openpype.tools.utils.lib import (\n    DynamicQThread,\n    get_project_icon,\n)\nfrom openpype.tools.utils.assets_widget import (\n    AssetModel,\n    ASSET_NAME_ROLE\n)\nfrom openpype.tools.utils.tasks_widget import (\n    TasksModel,\n    TasksProxyModel,\n    TASK_TYPE_ROLE,\n    TASK_ASSIGNEE_ROLE\n)\n\nfrom . import lib\nfrom .constants import (\n    ACTION_ROLE,\n    GROUP_ROLE,\n    VARIANT_GROUP_ROLE,\n    ACTION_ID_ROLE,\n    FORCE_NOT_OPEN_WORKFILE_ROLE\n)\nfrom .actions import ApplicationAction\n\nlog = logging.getLogger(__name__)\n\n# Must be different than roles in default asset model\nASSET_TASK_TYPES_ROLE = QtCore.Qt.UserRole + 10\nASSET_ASSIGNEE_ROLE = QtCore.Qt.UserRole + 11\n\n\nclass ActionModel(QtGui.QStandardItemModel):\n    def __init__(self, dbcon, parent=None):\n        super(ActionModel, self).__init__(parent=parent)\n        self.dbcon = dbcon\n\n        self.application_manager = ApplicationManager()\n\n        self.default_icon = qtawesome.icon(\"fa.cube\", color=\"white\")\n        # Cache of available actions\n        self._registered_actions = list()\n        self.items_by_id = {}\n        path = appdirs.user_data_dir(\"openpype\", \"pypeclub\")\n        self.launcher_registry = JSONSettingRegistry(\"launcher\", path)\n\n        try:\n            _ = self.launcher_registry.get_item(\"force_not_open_workfile\")\n        except ValueError:\n            self.launcher_registry.set_item(\"force_not_open_workfile\", [])\n\n    def discover(self):\n        \"\"\"Set up Actions cache. Run this for each new project.\"\"\"\n        # Discover all registered actions\n        actions = discover_launcher_actions()\n\n        # Get available project actions and the application actions\n        app_actions = self.get_application_actions()\n        actions.extend(app_actions)\n\n        self._registered_actions = actions\n\n        self.filter_actions()\n\n    def get_application_actions(self):\n        actions = []\n        if not self.dbcon.current_project():\n            return actions\n\n        project_name = self.dbcon.active_project()\n        project_doc = get_project(project_name, fields=[\"config.apps\"])\n        if not project_doc:\n            return actions\n\n        project_settings = get_project_settings(project_name)\n        only_available = project_settings[\"applications\"][\"only_available\"]\n        self.application_manager.refresh()\n        for app_def in project_doc[\"config\"][\"apps\"]:\n            app_name = app_def[\"name\"]\n            app = self.application_manager.applications.get(app_name)\n            if not app or not app.enabled:\n                continue\n\n            if app.group.name in CUSTOM_LAUNCH_APP_GROUPS:\n                continue\n\n            if only_available and not app.find_executable():\n                continue\n\n            # Get from app definition, if not there from app in project\n            action = type(\n                \"app_{}\".format(app_name),\n                (ApplicationAction,),\n                {\n                    \"application\": app,\n                    \"name\": app.name,\n                    \"label\": app.group.label,\n                    \"label_variant\": app.label,\n                    \"group\": None,\n                    \"icon\": app.icon,\n                    \"color\": getattr(app, \"color\", None),\n                    \"order\": getattr(app, \"order\", None) or 0,\n                    \"data\": {}\n                }\n            )\n\n            actions.append(action)\n        return actions\n\n    def get_icon(self, action, skip_default=False):\n        icon = lib.get_action_icon(action)\n        if not icon and not skip_default:\n            return self.default_icon\n        return icon\n\n    def filter_actions(self):\n        self.items_by_id.clear()\n        # Validate actions based on compatibility\n        self.clear()\n\n        actions = self.filter_compatible_actions(self._registered_actions)\n\n        single_actions = []\n        varianted_actions = collections.defaultdict(list)\n        grouped_actions = collections.defaultdict(list)\n        for action in actions:\n            # Groups\n            group_name = getattr(action, \"group\", None)\n\n            # Label variants\n            label = getattr(action, \"label\", None)\n            label_variant = getattr(action, \"label_variant\", None)\n            if label_variant and not label:\n                print((\n                    \"Invalid action \\\"{}\\\" has set `label_variant` to \\\"{}\\\"\"\n                    \", but doesn't have set `label` attribute\"\n                ).format(action.name, label_variant))\n                action.label_variant = None\n                label_variant = None\n\n            if group_name:\n                grouped_actions[group_name].append(action)\n\n            elif label_variant:\n                varianted_actions[label].append(action)\n            else:\n                single_actions.append(action)\n\n        items_by_order = collections.defaultdict(list)\n        for label, actions in tuple(varianted_actions.items()):\n            if len(actions) == 1:\n                varianted_actions.pop(label)\n                single_actions.append(actions[0])\n                continue\n\n            icon = None\n            order = None\n            for action in actions:\n                if icon is None:\n                    _icon = lib.get_action_icon(action)\n                    if _icon:\n                        icon = _icon\n\n                if order is None or action.order < order:\n                    order = action.order\n\n            if icon is None:\n                icon = self.default_icon\n\n            item = QtGui.QStandardItem(icon, label)\n            item.setData(label, QtCore.Qt.ToolTipRole)\n            item.setData(actions, ACTION_ROLE)\n            item.setData(True, VARIANT_GROUP_ROLE)\n            items_by_order[order].append(item)\n\n        for action in single_actions:\n            icon = self.get_icon(action)\n            label = lib.get_action_label(action)\n            item = QtGui.QStandardItem(icon, label)\n            item.setData(label, QtCore.Qt.ToolTipRole)\n            item.setData(action, ACTION_ROLE)\n            items_by_order[action.order].append(item)\n\n        for group_name, actions in grouped_actions.items():\n            icon = None\n            order = None\n            for action in actions:\n                if order is None or action.order < order:\n                    order = action.order\n\n                if icon is None:\n                    _icon = lib.get_action_icon(action)\n                    if _icon:\n                        icon = _icon\n\n            if icon is None:\n                icon = self.default_icon\n\n            item = QtGui.QStandardItem(icon, group_name)\n            item.setData(actions, ACTION_ROLE)\n            item.setData(True, GROUP_ROLE)\n\n            items_by_order[order].append(item)\n\n        self.beginResetModel()\n\n        stored = self.launcher_registry.get_item(\"force_not_open_workfile\")\n        items = []\n        for order in sorted(items_by_order.keys()):\n            for item in items_by_order[order]:\n                item_id = str(uuid.uuid4())\n                item.setData(item_id, ACTION_ID_ROLE)\n\n                if self.is_force_not_open_workfile(item,\n                                                   stored):\n                    self.change_action_item(item, True)\n\n                self.items_by_id[item_id] = item\n                items.append(item)\n\n        self.invisibleRootItem().appendRows(items)\n\n        self.endResetModel()\n\n    def filter_compatible_actions(self, actions):\n        \"\"\"Collect all actions which are compatible with the environment\n\n        Each compatible action will be translated to a dictionary to ensure\n        the action can be visualized in the launcher.\n\n        Args:\n            actions (list): list of classes\n\n        Returns:\n            list: collection of dictionaries sorted on order int he\n        \"\"\"\n\n        compatible = []\n        _session = copy.deepcopy(self.dbcon.Session)\n        session = {\n            key: value\n            for key, value in _session.items()\n            if value\n        }\n\n        for action in actions:\n            if action().is_compatible(session):\n                compatible.append(action)\n\n        # Sort by order and name\n        return sorted(\n            compatible,\n            key=lambda action: (action.order, lib.get_action_label(action))\n        )\n\n    def update_force_not_open_workfile_settings(self, is_checked, action_id):\n        \"\"\"Store/remove config for forcing to skip opening last workfile.\n\n        Args:\n            is_checked (bool): True to add, False to remove\n            action_id (str)\n        \"\"\"\n        action_item = self.items_by_id.get(action_id)\n        if not action_item:\n            return\n\n        actions = action_item.data(ACTION_ROLE)\n        if not isinstance(actions, list):\n            actions = [actions]\n\n        action_actions_data = [\n            self._prepare_compare_data(action)\n            for action in actions\n        ]\n\n        stored = self.launcher_registry.get_item(\"force_not_open_workfile\")\n        for actual_data in action_actions_data:\n            if is_checked:\n                stored.append(actual_data)\n            else:\n                final_values = []\n                for config in stored:\n                    if config != actual_data:\n                        final_values.append(config)\n                stored = final_values\n\n        self.launcher_registry.set_item(\"force_not_open_workfile\", stored)\n        self.launcher_registry._get_item.cache_clear()\n        self.change_action_item(action_item, is_checked)\n\n    def change_action_item(self, item, checked):\n        \"\"\"Modifies tooltip and sets if opening of last workfile forbidden\"\"\"\n        tooltip = item.data(QtCore.Qt.ToolTipRole)\n        if checked:\n            tooltip += \" (Not opening last workfile)\"\n\n        item.setData(tooltip, QtCore.Qt.ToolTipRole)\n        item.setData(checked, FORCE_NOT_OPEN_WORKFILE_ROLE)\n\n    def is_application_action(self, action):\n        \"\"\"Checks if item is of a ApplicationAction type\n\n        Args:\n            action (action)\n        \"\"\"\n        if isinstance(action, list) and action:\n            action = action[0]\n\n        return ApplicationAction in action.__bases__\n\n    def is_force_not_open_workfile(self, item, stored):\n        \"\"\"Checks if application for task is marked to not open workfile\n\n        There might be specific tasks where is unwanted to open workfile right\n        always (broken file, low performance). This allows artist to mark to\n        skip opening for combination (project, asset, task_name, app)\n\n        Args:\n            item (QStandardItem)\n            stored (list) of dict\n        \"\"\"\n\n        actions = item.data(ACTION_ROLE)\n        if not isinstance(actions, list):\n            actions = [actions]\n\n        if not self.is_application_action(actions[0]):\n            return False\n\n        action_actions_data = [\n            self._prepare_compare_data(action)\n            for action in actions\n        ]\n        for config in stored:\n            if config in action_actions_data:\n                return True\n        return False\n\n    def _prepare_compare_data(self, action):\n        compare_data = {}\n        if action and action.label:\n            compare_data = {\n                \"app_label\": action.label.lower(),\n                \"project_name\": self.dbcon.Session[\"AVALON_PROJECT\"],\n                \"asset\": self.dbcon.Session[\"AVALON_ASSET\"],\n                \"task_name\": self.dbcon.Session[\"AVALON_TASK\"]\n            }\n        return compare_data\n\n\nclass LauncherModel(QtCore.QObject):\n    # Refresh interval of projects\n    refresh_interval = 10000\n\n    # Signals\n    # Current project has changed\n    project_changed = QtCore.Signal(str)\n    # Filters has changed (any)\n    filters_changed = QtCore.Signal()\n\n    # Projects were refreshed\n    projects_refreshed = QtCore.Signal()\n\n    # Signals ONLY for assets model!\n    # - other objects should listen to asset model signals\n    # Asset refresh started\n    assets_refresh_started = QtCore.Signal()\n    # Assets refresh finished\n    assets_refreshed = QtCore.Signal()\n\n    # Refresh timer timeout\n    #   - give ability to tell parent window that this timer still runs\n    timer_timeout = QtCore.Signal()\n\n    # Duplication from AssetsModel with \"data.tasks\"\n    _asset_projection = {\n        \"name\": 1,\n        \"parent\": 1,\n        \"data.visualParent\": 1,\n        \"data.label\": 1,\n        \"data.icon\": 1,\n        \"data.color\": 1,\n        \"data.tasks\": 1\n    }\n\n    def __init__(self, dbcon):\n        super(LauncherModel, self).__init__()\n        # Refresh timer\n        #   - should affect only projects\n        refresh_timer = QtCore.QTimer()\n        refresh_timer.setInterval(self.refresh_interval)\n        refresh_timer.timeout.connect(self._on_timeout)\n\n        self._refresh_timer = refresh_timer\n\n        # Launcher is active\n        self._active = False\n\n        # Global data\n        self._dbcon = dbcon\n        # Available project names\n        self._project_names = set()\n        self._project_docs_by_name = {}\n\n        # Context data\n        self._asset_docs = []\n        self._asset_docs_by_id = {}\n        self._asset_filter_data_by_id = {}\n        self._assignees = set()\n        self._task_types = set()\n\n        # Filters\n        self._asset_name_filter = \"\"\n        self._assignee_filters = set()\n        self._task_type_filters = set()\n\n        # Last project for which were assets queried\n        self._last_project_name = None\n        # Asset refresh thread is running\n        self._refreshing_assets = False\n        # Asset refresh thread\n        self._asset_refresh_thread = None\n\n    def _on_timeout(self):\n        \"\"\"Refresh timer timeout.\"\"\"\n        if self._active:\n            self.timer_timeout.emit()\n            self.refresh_projects()\n\n    def set_active(self, active):\n        \"\"\"Window change active state.\"\"\"\n        self._active = active\n\n    def start_refresh_timer(self, trigger=False):\n        \"\"\"Start refresh timer.\"\"\"\n        self._refresh_timer.start()\n        if trigger:\n            self._on_timeout()\n\n    def stop_refresh_timer(self):\n        \"\"\"Stop refresh timer.\"\"\"\n        self._refresh_timer.stop()\n\n    @property\n    def project_name(self):\n        \"\"\"Current project name.\"\"\"\n        return self._dbcon.current_project()\n\n    @property\n    def refreshing_assets(self):\n        \"\"\"Refreshing thread is running.\"\"\"\n        return self._refreshing_assets\n\n    @property\n    def asset_docs(self):\n        \"\"\"Access to asset docs.\"\"\"\n        return self._asset_docs\n\n    @property\n    def project_names(self):\n        \"\"\"Available project names.\"\"\"\n        return self._project_names\n\n    def get_project_doc(self, project_name):\n        return self._project_docs_by_name.get(project_name)\n\n    @property\n    def asset_filter_data_by_id(self):\n        \"\"\"Prepared filter data by asset id.\"\"\"\n        return self._asset_filter_data_by_id\n\n    @property\n    def assignees(self):\n        \"\"\"All assignees for all assets in current project.\"\"\"\n        return self._assignees\n\n    @property\n    def task_types(self):\n        \"\"\"All task types for all assets in current project.\n\n        TODO: This could be maybe taken from project document where are all\n        task types...\n        \"\"\"\n        return self._task_types\n\n    @property\n    def task_type_filters(self):\n        \"\"\"Currently set task type filters.\"\"\"\n        return self._task_type_filters\n\n    @property\n    def assignee_filters(self):\n        \"\"\"Currently set assignee filters.\"\"\"\n        return self._assignee_filters\n\n    @property\n    def asset_name_filter(self):\n        \"\"\"Asset name filter (can be used as regex filter).\"\"\"\n        return self._asset_name_filter\n\n    def get_asset_doc(self, asset_id):\n        \"\"\"Get single asset document by id.\"\"\"\n        return self._asset_docs_by_id.get(asset_id)\n\n    def set_project_name(self, project_name):\n        \"\"\"Change project name and refresh asset documents.\"\"\"\n        if project_name == self.project_name:\n            return\n        self._dbcon.Session[\"AVALON_PROJECT\"] = project_name\n        self.project_changed.emit(project_name)\n\n        self.refresh_assets(force=True)\n\n    def refresh(self):\n        \"\"\"Trigger refresh of whole model.\"\"\"\n        self.refresh_projects()\n        self.refresh_assets(force=False)\n\n    def refresh_projects(self):\n        \"\"\"Refresh projects.\"\"\"\n        current_project = self.project_name\n        project_names = set()\n        project_docs_by_name = {}\n        for project_doc in get_projects():\n            project_name = project_doc[\"name\"]\n            project_names.add(project_name)\n            project_docs_by_name[project_name] = project_doc\n\n        self._project_docs_by_name = project_docs_by_name\n        self._project_names = project_names\n        self.projects_refreshed.emit()\n        if (\n            current_project is not None\n            and current_project not in project_names\n        ):\n            self.set_project_name(None)\n\n    def _set_asset_docs(self, asset_docs=None):\n        \"\"\"Set asset documents and all related data.\n\n        Method extract and prepare data needed for assets and tasks widget and\n        prepare filtering data.\n        \"\"\"\n        if asset_docs is None:\n            asset_docs = []\n\n        all_task_types = set()\n        all_assignees = set()\n        asset_docs_by_id = {}\n        asset_filter_data_by_id = {}\n        for asset_doc in asset_docs:\n            task_types = set()\n            assignees = set()\n            asset_id = asset_doc[\"_id\"]\n            asset_docs_by_id[asset_id] = asset_doc\n            asset_tasks = asset_doc.get(\"data\", {}).get(\"tasks\")\n            asset_filter_data_by_id[asset_id] = {\n                \"assignees\": assignees,\n                \"task_types\": task_types\n            }\n            if not asset_tasks:\n                continue\n\n            for task_data in asset_tasks.values():\n                task_assignees = set()\n                _task_assignees = task_data.get(\"assignees\")\n                if _task_assignees:\n                    for assignee in _task_assignees:\n                        task_assignees.add(assignee[\"username\"])\n\n                task_type = task_data.get(\"type\")\n                if task_assignees:\n                    assignees |= set(task_assignees)\n                if task_type:\n                    task_types.add(task_type)\n\n            all_task_types |= task_types\n            all_assignees |= assignees\n\n        self._asset_docs_by_id = asset_docs_by_id\n        self._asset_docs = asset_docs\n        self._asset_filter_data_by_id = asset_filter_data_by_id\n        self._assignees = all_assignees\n        self._task_types = all_task_types\n\n        self.assets_refreshed.emit()\n\n    def set_task_type_filter(self, task_types):\n        \"\"\"Change task type filter.\n\n        Args:\n            task_types (set): Set of task types that should be visible.\n                Pass empty set to turn filter off.\n        \"\"\"\n        self._task_type_filters = task_types\n        self.filters_changed.emit()\n\n    def set_assignee_filter(self, assignees):\n        \"\"\"Change assignees filter.\n\n        Args:\n            assignees (set): Set of assignees that should be visible.\n                Pass empty set to turn filter off.\n        \"\"\"\n        self._assignee_filters = assignees\n        self.filters_changed.emit()\n\n    def set_asset_name_filter(self, text_filter):\n        \"\"\"Change asset name filter.\n\n        Args:\n            text_filter (str): Asset name filter. Pass empty string to\n            turn filter off.\n        \"\"\"\n        self._asset_name_filter = text_filter\n        self.filters_changed.emit()\n\n    def refresh_assets(self, force=True):\n        \"\"\"Refresh assets.\"\"\"\n        self.assets_refresh_started.emit()\n\n        if self.project_name is None:\n            self._set_asset_docs()\n            return\n\n        if (\n            not force\n            and self._last_project_name == self.project_name\n        ):\n            return\n\n        self._stop_fetch_thread()\n\n        self._refreshing_assets = True\n        self._last_project_name = self.project_name\n        self._asset_refresh_thread = DynamicQThread(self._refresh_assets)\n        self._asset_refresh_thread.start()\n\n    def _stop_fetch_thread(self):\n        self._refreshing_assets = False\n        if self._asset_refresh_thread is not None:\n            while self._asset_refresh_thread.isRunning():\n                # TODO this is blocking UI should be done in a different way\n                time.sleep(0.01)\n            self._asset_refresh_thread = None\n\n    def _refresh_assets(self):\n        asset_docs = list(get_assets(\n            self._last_project_name, fields=self._asset_projection.keys()\n        ))\n        if not self._refreshing_assets:\n            return\n        self._refreshing_assets = False\n        self._set_asset_docs(asset_docs)\n\n\nclass LauncherTasksProxyModel(TasksProxyModel):\n    \"\"\"Tasks proxy model with more filtering.\n\n    TODO:\n    This can be (with few modifications) used in default tasks widget too.\n    \"\"\"\n    def __init__(self, launcher_model, *args, **kwargs):\n        self._launcher_model = launcher_model\n        super(LauncherTasksProxyModel, self).__init__(*args, **kwargs)\n\n        launcher_model.filters_changed.connect(self._on_filter_change)\n\n        self._task_types_filter = set()\n        self._assignee_filter = set()\n\n    def _on_filter_change(self):\n        self._task_types_filter = self._launcher_model.task_type_filters\n        self._assignee_filter = self._launcher_model.assignee_filters\n        self.invalidateFilter()\n\n    def filterAcceptsRow(self, row, parent):\n        if not self._task_types_filter and not self._assignee_filter:\n            return True\n\n        model = self.sourceModel()\n        source_index = model.index(row, self.filterKeyColumn(), parent)\n        if not source_index.isValid():\n            return False\n\n        # Check current index itself\n        if self._task_types_filter:\n            task_type = model.data(source_index, TASK_TYPE_ROLE)\n            if task_type not in self._task_types_filter:\n                return False\n\n        if self._assignee_filter:\n            assignee = model.data(source_index, TASK_ASSIGNEE_ROLE)\n            if not self._assignee_filter.intersection(assignee):\n                return False\n        return True\n\n\nclass LauncherTaskModel(TasksModel):\n    def __init__(self, launcher_model, *args, **kwargs):\n        self._launcher_model = launcher_model\n        super(LauncherTaskModel, self).__init__(*args, **kwargs)\n\n    def _refresh_project_doc(self):\n        self._project_doc = self._launcher_model.get_project_doc(\n            self._launcher_model.project_name\n        )\n\n    def set_asset_id(self, asset_id):\n        asset_doc = None\n        if self._context_is_valid():\n            asset_doc = self._launcher_model.get_asset_doc(asset_id)\n        self._set_asset(asset_doc)\n\n\nclass AssetRecursiveSortFilterModel(QtCore.QSortFilterProxyModel):\n    def __init__(self, launcher_model, *args, **kwargs):\n        self._launcher_model = launcher_model\n\n        super(AssetRecursiveSortFilterModel, self).__init__(*args, **kwargs)\n\n        launcher_model.filters_changed.connect(self._on_filter_change)\n        self._name_filter = \"\"\n        self._task_types_filter = set()\n        self._assignee_filter = set()\n\n    def _on_filter_change(self):\n        self._name_filter = self._launcher_model.asset_name_filter\n        self._task_types_filter = self._launcher_model.task_type_filters\n        self._assignee_filter = self._launcher_model.assignee_filters\n        self.invalidateFilter()\n\n    def filterAcceptsRow(self, row, parent):\n        if (\n            not self._name_filter\n            and not self._task_types_filter\n            and not self._assignee_filter\n        ):\n            return True\n\n        model = self.sourceModel()\n        source_index = model.index(row, self.filterKeyColumn(), parent)\n        if not source_index.isValid():\n            return False\n\n        # Check current index itself\n        valid = True\n        if self._name_filter:\n            name = model.data(source_index, ASSET_NAME_ROLE)\n            if (\n                name is None\n                or not re.search(self._name_filter, name, re.IGNORECASE)\n            ):\n                valid = False\n\n        if valid and self._task_types_filter:\n            task_types = model.data(source_index, ASSET_TASK_TYPES_ROLE)\n            if not self._task_types_filter.intersection(task_types):\n                valid = False\n\n        if valid and self._assignee_filter:\n            assignee = model.data(source_index, ASSET_ASSIGNEE_ROLE)\n            if not self._assignee_filter.intersection(assignee):\n                valid = False\n\n        if valid:\n            return True\n\n        # Check children\n        rows = model.rowCount(source_index)\n        for child_row in range(rows):\n            if self.filterAcceptsRow(child_row, source_index):\n                return True\n        return False\n\n\nclass LauncherAssetsModel(AssetModel):\n    def __init__(self, launcher_model, dbcon, parent=None):\n        self._launcher_model = launcher_model\n        # Make sure that variable is available (even if is in AssetModel)\n        self._last_project_name = None\n\n        super(LauncherAssetsModel, self).__init__(dbcon, parent)\n\n        launcher_model.project_changed.connect(self._on_project_change)\n        launcher_model.assets_refresh_started.connect(\n            self._on_launcher_refresh_start\n        )\n        launcher_model.assets_refreshed.connect(self._on_launcher_refresh)\n\n    def _on_launcher_refresh_start(self):\n        self._refreshing = True\n        project_name = self._launcher_model.project_name\n        if self._last_project_name != project_name:\n            self._clear_items()\n            self._last_project_name = project_name\n\n    def _on_launcher_refresh(self):\n        self._fill_assets(self._launcher_model.asset_docs)\n        self._refreshing = False\n        self.refreshed.emit(bool(self._items_by_asset_id))\n\n    def _fill_assets(self, *args, **kwargs):\n        super(LauncherAssetsModel, self)._fill_assets(*args, **kwargs)\n        asset_filter_data_by_id = self._launcher_model.asset_filter_data_by_id\n        for asset_id, item in self._items_by_asset_id.items():\n            filter_data = asset_filter_data_by_id.get(asset_id)\n\n            assignees = filter_data[\"assignees\"]\n            task_types = filter_data[\"task_types\"]\n\n            item.setData(assignees, ASSET_ASSIGNEE_ROLE)\n            item.setData(task_types, ASSET_TASK_TYPES_ROLE)\n\n    def _on_project_change(self):\n        self._clear_items()\n\n    def refresh(self, *args, **kwargs):\n        raise ValueError(\"This is a bug!\")\n\n    def stop_refresh(self, *args, **kwargs):\n        raise ValueError(\"This is a bug!\")\n\n\nclass ProjectModel(QtGui.QStandardItemModel):\n    \"\"\"List of projects\"\"\"\n\n    def __init__(self, launcher_model, parent=None):\n        super(ProjectModel, self).__init__(parent=parent)\n\n        self._launcher_model = launcher_model\n        self._project_names = set()\n\n        launcher_model.projects_refreshed.connect(self._on_refresh)\n\n    def _on_refresh(self):\n        project_names = set(self._launcher_model.project_names)\n        origin_project_names = set(self._project_names)\n        self._project_names = project_names\n\n        project_names_to_remove = origin_project_names - project_names\n        if project_names_to_remove:\n            row_counts = {}\n            continuous = None\n            for row in range(self.rowCount()):\n                index = self.index(row, 0)\n                index_name = index.data(QtCore.Qt.DisplayRole)\n                if index_name in project_names_to_remove:\n                    if continuous is None:\n                        continuous = row\n                        row_counts[continuous] = 0\n                    row_counts[continuous] += 1\n                else:\n                    continuous = None\n\n            for row in reversed(sorted(row_counts.keys())):\n                count = row_counts[row]\n                self.removeRows(row, count)\n\n        continuous = None\n        row_counts = {}\n        for idx, project_name in enumerate(sorted(project_names)):\n            if project_name in origin_project_names:\n                continuous = None\n                continue\n\n            if continuous is None:\n                continuous = idx\n                row_counts[continuous] = []\n\n            row_counts[continuous].append(project_name)\n\n        for row in reversed(sorted(row_counts.keys())):\n            items = []\n            for project_name in row_counts[row]:\n                project_doc = self._launcher_model.get_project_doc(\n                    project_name\n                )\n                icon = get_project_icon(project_doc)\n                item = QtGui.QStandardItem(icon, project_name)\n                items.append(item)\n\n            self.invisibleRootItem().insertRows(row, items)\n"
  },
  {
    "path": "openpype/tools/launcher/widgets.py",
    "content": "import copy\nimport time\nimport collections\nfrom qtpy import QtWidgets, QtCore, QtGui\nimport qtawesome\n\nfrom openpype.tools.flickcharm import FlickCharm\nfrom openpype.tools.utils.assets_widget import SingleSelectAssetsWidget\nfrom openpype.tools.utils.tasks_widget import TasksWidget\n\nfrom .delegates import ActionDelegate\nfrom . import lib\nfrom .models import (\n    ActionModel,\n    ProjectModel,\n    LauncherAssetsModel,\n    AssetRecursiveSortFilterModel,\n    LauncherTaskModel,\n    LauncherTasksProxyModel\n)\nfrom .actions import ApplicationAction\nfrom .constants import (\n    ACTION_ROLE,\n    GROUP_ROLE,\n    VARIANT_GROUP_ROLE,\n    ACTION_ID_ROLE,\n    ANIMATION_START_ROLE,\n    ANIMATION_STATE_ROLE,\n    ANIMATION_LEN,\n    FORCE_NOT_OPEN_WORKFILE_ROLE\n)\n\n\nclass ProjectBar(QtWidgets.QWidget):\n    def __init__(self, launcher_model, parent=None):\n        super(ProjectBar, self).__init__(parent)\n\n        project_combobox = QtWidgets.QComboBox(self)\n        # Change delegate so stylysheets are applied\n        project_delegate = QtWidgets.QStyledItemDelegate(project_combobox)\n        project_combobox.setItemDelegate(project_delegate)\n        model = ProjectModel(launcher_model)\n        project_combobox.setModel(model)\n        project_combobox.setRootModelIndex(QtCore.QModelIndex())\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(project_combobox)\n\n        self.setSizePolicy(\n            QtWidgets.QSizePolicy.MinimumExpanding,\n            QtWidgets.QSizePolicy.Maximum\n        )\n\n        self._launcher_model = launcher_model\n        self.project_delegate = project_delegate\n        self.project_combobox = project_combobox\n        self._model = model\n\n        # Signals\n        self.project_combobox.currentIndexChanged.connect(self.on_index_change)\n        launcher_model.project_changed.connect(self._on_project_change)\n\n        # Set current project by default if it's set.\n        project_name = launcher_model.project_name\n        if project_name:\n            self.set_project(project_name)\n\n    def _on_project_change(self, project_name):\n        if self.get_current_project() == project_name:\n            return\n        self.set_project(project_name)\n\n    def get_current_project(self):\n        return self.project_combobox.currentText()\n\n    def set_project(self, project_name):\n        index = self.project_combobox.findText(project_name)\n        if index < 0:\n            # Try refresh combobox model\n            self._launcher_model.refresh_projects()\n            index = self.project_combobox.findText(project_name)\n\n        if index >= 0:\n            self.project_combobox.setCurrentIndex(index)\n\n    def on_index_change(self, idx):\n        if not self.isVisible():\n            return\n\n        project_name = self.get_current_project()\n        self._launcher_model.set_project_name(project_name)\n\n\nclass LauncherTaskWidget(TasksWidget):\n    def __init__(self, launcher_model, *args, **kwargs):\n        self._launcher_model = launcher_model\n\n        super(LauncherTaskWidget, self).__init__(*args, **kwargs)\n\n    def _create_source_model(self):\n        return LauncherTaskModel(self._launcher_model, self._dbcon)\n\n    def _create_proxy_model(self, source_model):\n        proxy = LauncherTasksProxyModel(self._launcher_model)\n        proxy.setSourceModel(source_model)\n        return proxy\n\n\nclass LauncherAssetsWidget(SingleSelectAssetsWidget):\n    def __init__(self, launcher_model, *args, **kwargs):\n        self._launcher_model = launcher_model\n\n        super(LauncherAssetsWidget, self).__init__(*args, **kwargs)\n\n        launcher_model.assets_refresh_started.connect(self._on_refresh_start)\n\n        self.set_current_asset_btn_visibility(False)\n\n    def _on_refresh_start(self):\n        self._set_loading_state(loading=True, empty=True)\n        self.refresh_triggered.emit()\n\n    @property\n    def refreshing(self):\n        return self._model.refreshing\n\n    def refresh(self):\n        self._launcher_model.refresh_assets(force=True)\n\n    def stop_refresh(self):\n        raise ValueError(\"bug stop_refresh called\")\n\n    def _refresh_model(self, clear=False):\n        raise ValueError(\"bug _refresh_model called\")\n\n    def _create_source_model(self):\n        model = LauncherAssetsModel(self._launcher_model, self.dbcon)\n        model.refreshed.connect(self._on_model_refresh)\n        return model\n\n    def _create_proxy_model(self, source_model):\n        proxy = AssetRecursiveSortFilterModel(self._launcher_model)\n        proxy.setSourceModel(source_model)\n        proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)\n        proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n        return proxy\n\n    def _on_model_refresh(self, has_item):\n        self._proxy.sort(0)\n        self._set_loading_state(loading=False, empty=not has_item)\n        self.refreshed.emit()\n\n    def _on_filter_text_change(self, new_text):\n        self._launcher_model.set_asset_name_filter(new_text)\n\n\nclass ActionBar(QtWidgets.QWidget):\n    \"\"\"Launcher interface\"\"\"\n\n    action_clicked = QtCore.Signal(object)\n\n    def __init__(self, launcher_model, dbcon, parent=None):\n        super(ActionBar, self).__init__(parent)\n\n        self._launcher_model = launcher_model\n        self.dbcon = dbcon\n\n        view = QtWidgets.QListView(self)\n        view.setProperty(\"mode\", \"icon\")\n        view.setObjectName(\"IconView\")\n        view.setViewMode(QtWidgets.QListView.IconMode)\n        view.setResizeMode(QtWidgets.QListView.Adjust)\n        view.setSelectionMode(QtWidgets.QListView.NoSelection)\n        view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\n        view.setWrapping(True)\n        view.setGridSize(QtCore.QSize(70, 75))\n        view.setIconSize(QtCore.QSize(30, 30))\n        view.setSpacing(0)\n        view.setWordWrap(True)\n\n        model = ActionModel(self.dbcon, self)\n        view.setModel(model)\n\n        # TODO better group delegate\n        delegate = ActionDelegate(\n            [GROUP_ROLE, VARIANT_GROUP_ROLE],\n            self\n        )\n        view.setItemDelegate(delegate)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(view)\n\n        self.model = model\n        self.view = view\n\n        self._animated_items = set()\n\n        animation_timer = QtCore.QTimer()\n        animation_timer.setInterval(50)\n        animation_timer.timeout.connect(self._on_animation)\n        self._animation_timer = animation_timer\n\n        # Make view flickable\n        flick = FlickCharm(parent=view)\n        flick.activateOn(view)\n\n        self.set_row_height(1)\n\n        launcher_model.projects_refreshed.connect(self._on_projects_refresh)\n        view.clicked.connect(self.on_clicked)\n        view.customContextMenuRequested.connect(self.on_context_menu)\n\n        self._context_menu = None\n        self._discover_on_menu = False\n\n    def discover_actions(self):\n        if self._context_menu is not None:\n            self._discover_on_menu = True\n            return\n\n        if self._animation_timer.isActive():\n            self._animation_timer.stop()\n        self.model.discover()\n\n    def filter_actions(self):\n        if self._animation_timer.isActive():\n            self._animation_timer.stop()\n        self.model.filter_actions()\n\n    def set_row_height(self, rows):\n        self.setMinimumHeight(rows * 75)\n\n    def _on_projects_refresh(self):\n        self.discover_actions()\n\n    def _on_animation(self):\n        time_now = time.time()\n        for action_id in tuple(self._animated_items):\n            item = self.model.items_by_id.get(action_id)\n            if not item:\n                self._animated_items.remove(action_id)\n                continue\n\n            start_time = item.data(ANIMATION_START_ROLE)\n            if (time_now - start_time) > ANIMATION_LEN:\n                item.setData(0, ANIMATION_STATE_ROLE)\n                self._animated_items.remove(action_id)\n\n        if not self._animated_items:\n            self._animation_timer.stop()\n\n        self.update()\n\n    def _start_animation(self, index):\n        # Offset refresh timout\n        self._launcher_model.start_refresh_timer()\n        action_id = index.data(ACTION_ID_ROLE)\n        item = self.model.items_by_id.get(action_id)\n        if item:\n            item.setData(time.time(), ANIMATION_START_ROLE)\n            item.setData(1, ANIMATION_STATE_ROLE)\n            self._animated_items.add(action_id)\n            self._animation_timer.start()\n\n    def on_context_menu(self, point):\n        \"\"\"Creates menu to force skip opening last workfile.\"\"\"\n        index = self.view.indexAt(point)\n        if not index.isValid():\n            return\n\n        action_item = index.data(ACTION_ROLE)\n        if not self.model.is_application_action(action_item):\n            return\n\n        menu = QtWidgets.QMenu(self.view)\n        checkbox = QtWidgets.QCheckBox(\"Skip opening last workfile.\",\n                                       menu)\n        if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE):\n            checkbox.setChecked(True)\n\n        action_id = index.data(ACTION_ID_ROLE)\n        checkbox.stateChanged.connect(\n            lambda: self.on_checkbox_changed(checkbox.isChecked(),\n                                             action_id))\n        action = QtWidgets.QWidgetAction(menu)\n        action.setDefaultWidget(checkbox)\n\n        menu.addAction(action)\n\n        self._context_menu = menu\n        global_point = self.mapToGlobal(point)\n        menu.exec_(global_point)\n        self._context_menu = None\n        if self._discover_on_menu:\n            self._discover_on_menu = False\n            self.discover_actions()\n\n    def on_checkbox_changed(self, is_checked, action_id):\n        self.model.update_force_not_open_workfile_settings(is_checked,\n                                                           action_id)\n        self.view.update()\n        if self._context_menu is not None:\n            self._context_menu.close()\n\n    def on_clicked(self, index):\n        if not index or not index.isValid():\n            return\n\n        is_group = index.data(GROUP_ROLE)\n        is_variant_group = index.data(VARIANT_GROUP_ROLE)\n        force_not_open_workfile = index.data(FORCE_NOT_OPEN_WORKFILE_ROLE)\n        if not is_group and not is_variant_group:\n            action = index.data(ACTION_ROLE)\n            # Change data of application action\n            if issubclass(action, ApplicationAction):\n                if force_not_open_workfile:\n                    action.data[\"start_last_workfile\"] = False\n                else:\n                    action.data.pop(\"start_last_workfile\", None)\n            self._start_animation(index)\n            self.action_clicked.emit(action)\n            return\n\n        # Offset refresh timout\n        self._launcher_model.start_refresh_timer()\n\n        actions = index.data(ACTION_ROLE)\n\n        menu = QtWidgets.QMenu(self)\n        actions_mapping = {}\n\n        if is_variant_group:\n            for action in actions:\n                menu_action = QtWidgets.QAction(\n                    lib.get_action_label(action)\n                )\n                menu.addAction(menu_action)\n                actions_mapping[menu_action] = action\n        else:\n            by_variant_label = collections.defaultdict(list)\n            orders = []\n            for action in actions:\n                # Label variants\n                label = getattr(action, \"label\", None)\n                label_variant = getattr(action, \"label_variant\", None)\n                if label_variant and not label:\n                    label_variant = None\n\n                if not label_variant:\n                    orders.append(action)\n                    continue\n\n                if label not in orders:\n                    orders.append(label)\n                by_variant_label[label].append(action)\n\n            for action_item in orders:\n                actions = by_variant_label.get(action_item)\n                if not actions:\n                    action = action_item\n                elif len(actions) == 1:\n                    action = actions[0]\n                else:\n                    action = None\n\n                if action:\n                    menu_action = QtWidgets.QAction(\n                        lib.get_action_label(action)\n                    )\n                    menu.addAction(menu_action)\n                    actions_mapping[menu_action] = action\n                    continue\n\n                sub_menu = QtWidgets.QMenu(label, menu)\n                for action in actions:\n                    menu_action = QtWidgets.QAction(\n                        lib.get_action_label(action)\n                    )\n                    sub_menu.addAction(menu_action)\n                    actions_mapping[menu_action] = action\n\n                menu.addMenu(sub_menu)\n\n        result = menu.exec_(QtGui.QCursor.pos())\n        if not result:\n            return\n\n        action = actions_mapping[result]\n        if issubclass(action, ApplicationAction):\n            if force_not_open_workfile:\n                action.data[\"start_last_workfile\"] = False\n            else:\n                action.data.pop(\"start_last_workfile\", None)\n\n        self._start_animation(index)\n        self.action_clicked.emit(action)\n\n\nclass ActionHistory(QtWidgets.QPushButton):\n    trigger_history = QtCore.Signal(tuple)\n\n    def __init__(self, parent=None):\n        super(ActionHistory, self).__init__(parent=parent)\n\n        self.max_history = 15\n\n        self.setFixedWidth(25)\n        self.setFixedHeight(25)\n\n        self.setIcon(qtawesome.icon(\"fa.history\", color=\"#CCCCCC\"))\n        self.setIconSize(QtCore.QSize(15, 15))\n\n        self._history = []\n        self.clicked.connect(self.show_history)\n\n    def show_history(self):\n        # Show history popup\n        if not self._history:\n            return\n\n        widget = QtWidgets.QListWidget()\n        widget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)\n        widget.setStyleSheet(\"\"\"\n        * {\n            font-family: \"Courier New\";\n        }\n        \"\"\")\n\n        largest_label_num_chars = 0\n        largest_action_label = max(len(x[0].label) for x in self._history)\n        action_session_role = QtCore.Qt.UserRole + 1\n\n        for action, session in reversed(self._history):\n            project = session.get(\"AVALON_PROJECT\")\n            asset = session.get(\"AVALON_ASSET\")\n            task = session.get(\"AVALON_TASK\")\n            breadcrumb = \" > \".join(x for x in [project, asset, task] if x)\n\n            m = \"{{action:{0}}} | {{breadcrumb}}\".format(largest_action_label)\n            label = m.format(action=action.label, breadcrumb=breadcrumb)\n\n            icon = lib.get_action_icon(action)\n            item = QtWidgets.QListWidgetItem(icon, label)\n            item.setData(action_session_role, (action, session))\n\n            largest_label_num_chars = max(largest_label_num_chars, len(label))\n\n            widget.addItem(item)\n\n        # Show history\n        dialog = QtWidgets.QDialog(parent=self)\n        dialog.setWindowTitle(\"Action History\")\n        dialog.setWindowFlags(\n            QtCore.Qt.FramelessWindowHint | QtCore.Qt.Popup\n        )\n        dialog.setSizePolicy(\n            QtWidgets.QSizePolicy.Ignored,\n            QtWidgets.QSizePolicy.Ignored\n        )\n\n        layout = QtWidgets.QVBoxLayout(dialog)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(widget)\n\n        def on_clicked(index):\n            data = index.data(action_session_role)\n            self.trigger_history.emit(data)\n            dialog.close()\n\n        widget.clicked.connect(on_clicked)\n\n        # padding + icon + text\n        width = 40 + (largest_label_num_chars * 7)\n        entry_height = 21\n        height = entry_height * len(self._history)\n\n        point = QtGui.QCursor().pos()\n        dialog.setGeometry(\n            point.x() - width,\n            point.y() - height,\n            width,\n            height\n        )\n        dialog.exec_()\n\n        self.widget_popup = widget\n\n    def add_action(self, action, session):\n        key = (action, copy.deepcopy(session))\n\n        # Remove entry if already exists\n        if key in self._history:\n            self._history.remove(key)\n\n        self._history.append(key)\n\n        # Slice the end of the list if we exceed the max history\n        if len(self._history) > self.max_history:\n            self._history = self._history[-self.max_history:]\n\n    def clear_history(self):\n        self._history.clear()\n\n\nclass SlidePageWidget(QtWidgets.QStackedWidget):\n    \"\"\"Stacked widget that nicely slides between its pages\"\"\"\n\n    directions = {\n        \"left\": QtCore.QPoint(-1, 0),\n        \"right\": QtCore.QPoint(1, 0),\n        \"up\": QtCore.QPoint(0, 1),\n        \"down\": QtCore.QPoint(0, -1)\n    }\n\n    def slide_view(self, index, direction=\"right\"):\n        if self.currentIndex() == index:\n            return\n\n        offset_direction = self.directions.get(direction)\n        if offset_direction is None:\n            print(\"BUG: invalid slide direction: {}\".format(direction))\n            return\n\n        width = self.frameRect().width()\n        height = self.frameRect().height()\n        offset = QtCore.QPoint(\n            offset_direction.x() * width,\n            offset_direction.y() * height\n        )\n\n        new_page = self.widget(index)\n        new_page.setGeometry(0, 0, width, height)\n        curr_pos = new_page.pos()\n        new_page.move(curr_pos + offset)\n        new_page.show()\n        new_page.raise_()\n\n        current_page = self.currentWidget()\n\n        b_pos = QtCore.QByteArray(b\"pos\")\n\n        anim_old = QtCore.QPropertyAnimation(current_page, b_pos, self)\n        anim_old.setDuration(250)\n        anim_old.setStartValue(curr_pos)\n        anim_old.setEndValue(curr_pos - offset)\n        anim_old.setEasingCurve(QtCore.QEasingCurve.OutQuad)\n\n        anim_new = QtCore.QPropertyAnimation(new_page, b_pos, self)\n        anim_new.setDuration(250)\n        anim_new.setStartValue(curr_pos + offset)\n        anim_new.setEndValue(curr_pos)\n        anim_new.setEasingCurve(QtCore.QEasingCurve.OutQuad)\n\n        anim_group = QtCore.QParallelAnimationGroup(self)\n        anim_group.addAnimation(anim_old)\n        anim_group.addAnimation(anim_new)\n\n        def slide_finished():\n            self.setCurrentWidget(new_page)\n\n        anim_group.finished.connect(slide_finished)\n        anim_group.start()\n"
  },
  {
    "path": "openpype/tools/launcher/window.py",
    "content": "import copy\nimport logging\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import style\nfrom openpype import resources\nfrom openpype.pipeline import AvalonMongoDB\n\nimport qtawesome\nfrom .models import (\n    LauncherModel,\n    ProjectModel\n)\nfrom .lib import get_action_label\nfrom .widgets import (\n    ProjectBar,\n    ActionBar,\n    ActionHistory,\n    SlidePageWidget,\n    LauncherAssetsWidget,\n    LauncherTaskWidget\n)\n\nfrom openpype.tools.flickcharm import FlickCharm\n\n\nclass ProjectIconView(QtWidgets.QListView):\n    \"\"\"Styled ListView that allows to toggle between icon and list mode.\n\n    Toggling between the two modes is done by Right Mouse Click.\n\n    \"\"\"\n\n    IconMode = 0\n    ListMode = 1\n\n    def __init__(self, parent=None, mode=ListMode):\n        super(ProjectIconView, self).__init__(parent=parent)\n\n        # Workaround for scrolling being super slow or fast when\n        # toggling between the two visual modes\n        self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)\n        self.setObjectName(\"IconView\")\n\n        self._mode = None\n        self.set_mode(mode)\n\n    def set_mode(self, mode):\n        if mode == self._mode:\n            return\n\n        self._mode = mode\n\n        if mode == self.IconMode:\n            self.setViewMode(QtWidgets.QListView.IconMode)\n            self.setResizeMode(QtWidgets.QListView.Adjust)\n            self.setWrapping(True)\n            self.setWordWrap(True)\n            self.setGridSize(QtCore.QSize(151, 90))\n            self.setIconSize(QtCore.QSize(50, 50))\n            self.setSpacing(0)\n            self.setAlternatingRowColors(False)\n\n            self.setProperty(\"mode\", \"icon\")\n            self.style().polish(self)\n\n            self.verticalScrollBar().setSingleStep(30)\n\n        elif self.ListMode:\n            self.setProperty(\"mode\", \"list\")\n            self.style().polish(self)\n\n            self.setViewMode(QtWidgets.QListView.ListMode)\n            self.setResizeMode(QtWidgets.QListView.Adjust)\n            self.setWrapping(False)\n            self.setWordWrap(False)\n            self.setIconSize(QtCore.QSize(20, 20))\n            self.setGridSize(QtCore.QSize(100, 25))\n            self.setSpacing(0)\n            self.setAlternatingRowColors(False)\n\n            self.verticalScrollBar().setSingleStep(33.33)\n\n    def mousePressEvent(self, event):\n        if event.button() == QtCore.Qt.RightButton:\n            self.set_mode(int(not self._mode))\n        return super(ProjectIconView, self).mousePressEvent(event)\n\n\nclass ProjectsPanel(QtWidgets.QWidget):\n    \"\"\"Projects Page\"\"\"\n    def __init__(self, launcher_model, parent=None):\n        super(ProjectsPanel, self).__init__(parent=parent)\n\n        view = ProjectIconView(parent=self)\n        view.setSelectionMode(QtWidgets.QListView.NoSelection)\n        flick = FlickCharm(parent=self)\n        flick.activateOn(view)\n        model = ProjectModel(launcher_model)\n        view.setModel(model)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(view)\n\n        view.clicked.connect(self.on_clicked)\n\n        self._model = model\n        self.view = view\n        self._launcher_model = launcher_model\n\n    def on_clicked(self, index):\n        if index.isValid():\n            project_name = index.data(QtCore.Qt.DisplayRole)\n            self._launcher_model.set_project_name(project_name)\n\n\nclass AssetsPanel(QtWidgets.QWidget):\n    \"\"\"Assets page\"\"\"\n    back_clicked = QtCore.Signal()\n    session_changed = QtCore.Signal()\n\n    def __init__(self, launcher_model, dbcon, parent=None):\n        super(AssetsPanel, self).__init__(parent=parent)\n\n        self.dbcon = dbcon\n\n        # Project bar\n        btn_back_icon = qtawesome.icon(\"fa.angle-left\", color=\"white\")\n        btn_back = QtWidgets.QPushButton(self)\n        btn_back.setIcon(btn_back_icon)\n\n        project_bar = ProjectBar(launcher_model, self)\n\n        project_bar_layout = QtWidgets.QHBoxLayout()\n        project_bar_layout.setContentsMargins(0, 0, 0, 0)\n        project_bar_layout.setSpacing(4)\n        project_bar_layout.addWidget(btn_back)\n        project_bar_layout.addWidget(project_bar)\n\n        # Assets widget\n        assets_widget = LauncherAssetsWidget(\n            launcher_model, dbcon=self.dbcon, parent=self\n        )\n        # Make assets view flickable\n        assets_widget.activate_flick_charm()\n\n        # Tasks widget\n        tasks_widget = LauncherTaskWidget(launcher_model, self.dbcon, self)\n\n        # Body\n        body = QtWidgets.QSplitter(self)\n        body.setContentsMargins(0, 0, 0, 0)\n        body.setSizePolicy(\n            QtWidgets.QSizePolicy.Expanding,\n            QtWidgets.QSizePolicy.Expanding\n        )\n        body.setOrientation(QtCore.Qt.Horizontal)\n        body.addWidget(assets_widget)\n        body.addWidget(tasks_widget)\n        body.setStretchFactor(0, 100)\n        body.setStretchFactor(1, 65)\n\n        # main layout\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addLayout(project_bar_layout)\n        layout.addWidget(body)\n\n        # signals\n        launcher_model.project_changed.connect(self._on_project_changed)\n        assets_widget.selection_changed.connect(self._on_asset_changed)\n        assets_widget.refreshed.connect(self._on_asset_changed)\n        tasks_widget.task_changed.connect(self._on_task_change)\n\n        btn_back.clicked.connect(self.back_clicked)\n\n        self.project_bar = project_bar\n        self.assets_widget = assets_widget\n        self._tasks_widget = tasks_widget\n        self._btn_back = btn_back\n\n        self._launcher_model = launcher_model\n\n    def select_asset(self, asset_name):\n        self.assets_widget.select_asset_by_name(asset_name)\n\n    def showEvent(self, event):\n        super(AssetsPanel, self).showEvent(event)\n\n        # Change size of a btn\n        # WARNING does not handle situation if combobox is bigger\n        btn_size = self.project_bar.height()\n        self._btn_back.setFixedSize(QtCore.QSize(btn_size, btn_size))\n\n    def select_task_name(self, task_name):\n        self._on_asset_changed()\n        self._tasks_widget.select_task_name(task_name)\n\n    def _on_project_changed(self):\n        self.session_changed.emit()\n\n    def _on_asset_changed(self):\n        \"\"\"Callback on asset selection changed\n\n        This updates the task view.\n        \"\"\"\n\n        # Check asset on current index and selected assets\n        asset_id = self.assets_widget.get_selected_asset_id()\n        asset_name = self.assets_widget.get_selected_asset_name()\n\n        self.dbcon.Session[\"AVALON_TASK\"] = None\n        self.dbcon.Session[\"AVALON_ASSET\"] = asset_name\n\n        self.session_changed.emit()\n\n        self._tasks_widget.set_asset_id(asset_id)\n\n    def _on_task_change(self):\n        task_name = self._tasks_widget.get_selected_task_name()\n        self.dbcon.Session[\"AVALON_TASK\"] = task_name\n        self.session_changed.emit()\n\n\nclass LauncherWindow(QtWidgets.QDialog):\n    \"\"\"Launcher interface\"\"\"\n    message_timeout = 5000\n\n    def __init__(self, parent=None):\n        super(LauncherWindow, self).__init__(parent)\n\n        self.log = logging.getLogger(\n            \".\".join([__name__, self.__class__.__name__])\n        )\n        self.dbcon = AvalonMongoDB()\n\n        self.setWindowTitle(\"Launcher\")\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n        self.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)\n\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n        self.setStyleSheet(style.load_stylesheet())\n\n        # Allow minimize\n        self.setWindowFlags(\n            QtCore.Qt.Window\n            | QtCore.Qt.CustomizeWindowHint\n            | QtCore.Qt.WindowTitleHint\n            | QtCore.Qt.WindowMinimizeButtonHint\n            | QtCore.Qt.WindowCloseButtonHint\n        )\n\n        launcher_model = LauncherModel(self.dbcon)\n\n        project_panel = ProjectsPanel(launcher_model)\n        asset_panel = AssetsPanel(launcher_model, self.dbcon)\n\n        page_slider = SlidePageWidget()\n        page_slider.addWidget(project_panel)\n        page_slider.addWidget(asset_panel)\n\n        # actions\n        actions_bar = ActionBar(launcher_model, self.dbcon, self)\n\n        # statusbar\n        message_label = QtWidgets.QLabel(self)\n\n        action_history = ActionHistory(self)\n        action_history.setStatusTip(\"Show Action History\")\n\n        status_layout = QtWidgets.QHBoxLayout()\n        status_layout.addWidget(message_label, 1)\n        status_layout.addWidget(action_history, 0)\n\n        # Vertically split Pages and Actions\n        body = QtWidgets.QSplitter(self)\n        body.setContentsMargins(0, 0, 0, 0)\n        body.setSizePolicy(\n            QtWidgets.QSizePolicy.Expanding,\n            QtWidgets.QSizePolicy.Expanding\n        )\n        body.setOrientation(QtCore.Qt.Vertical)\n        body.addWidget(page_slider)\n        body.addWidget(actions_bar)\n\n        # Set useful default sizes and set stretch\n        # for the pages so that is the only one that\n        # stretches on UI resize.\n        body.setStretchFactor(0, 10)\n        body.setSizes([580, 160])\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(body)\n        layout.addLayout(status_layout)\n\n        message_timer = QtCore.QTimer()\n        message_timer.setInterval(self.message_timeout)\n        message_timer.setSingleShot(True)\n\n        message_timer.timeout.connect(self._on_message_timeout)\n\n        # signals\n        actions_bar.action_clicked.connect(self.on_action_clicked)\n        action_history.trigger_history.connect(self.on_history_action)\n        launcher_model.project_changed.connect(self.on_project_change)\n        launcher_model.timer_timeout.connect(self._on_refresh_timeout)\n        asset_panel.back_clicked.connect(self.on_back_clicked)\n        asset_panel.session_changed.connect(self.on_session_changed)\n\n        self.resize(520, 740)\n\n        self._page = 0\n\n        self._message_timer = message_timer\n\n        self._launcher_model = launcher_model\n\n        self._message_label = message_label\n        self.project_panel = project_panel\n        self.asset_panel = asset_panel\n        self.actions_bar = actions_bar\n        self.action_history = action_history\n        self.page_slider = page_slider\n\n    def showEvent(self, event):\n        self._launcher_model.set_active(True)\n        self._launcher_model.start_refresh_timer(True)\n\n        super(LauncherWindow, self).showEvent(event)\n\n    def _on_refresh_timeout(self):\n        # Stop timer if widget is not visible\n        if not self.isVisible():\n            self._launcher_model.stop_refresh_timer()\n\n    def changeEvent(self, event):\n        if event.type() == QtCore.QEvent.ActivationChange:\n            self._launcher_model.set_active(self.isActiveWindow())\n        super(LauncherWindow, self).changeEvent(event)\n\n    def set_page(self, page):\n        current = self.page_slider.currentIndex()\n        if current == page and self._page == page:\n            return\n\n        direction = \"right\" if page > current else \"left\"\n        self._page = page\n        self.page_slider.slide_view(page, direction=direction)\n\n    def _on_message_timeout(self):\n        self._message_label.setText(\"\")\n\n    def echo(self, message):\n        self._message_label.setText(str(message))\n        self._message_timer.start()\n        self.log.debug(message)\n\n    def on_session_changed(self):\n        self.filter_actions()\n\n    def discover_actions(self):\n        self.actions_bar.discover_actions()\n\n    def filter_actions(self):\n        self.actions_bar.filter_actions()\n\n    def on_project_change(self, project_name):\n        # Update the Action plug-ins available for the current project\n        self.set_page(1)\n        self.discover_actions()\n\n    def on_back_clicked(self):\n        self._launcher_model.set_project_name(None)\n        self.set_page(0)\n        self.discover_actions()\n\n    def on_action_clicked(self, action):\n        self.echo(\"Running action: {}\".format(get_action_label(action)))\n        self.run_action(action)\n\n    def on_history_action(self, history_data):\n        action, session = history_data\n        app = QtWidgets.QApplication.instance()\n        modifiers = app.keyboardModifiers()\n\n        is_control_down = QtCore.Qt.ControlModifier & modifiers\n        if is_control_down:\n            # Revert to that \"session\" location\n            self.set_session(session)\n        else:\n            # User is holding control, rerun the action\n            self.run_action(action, session=session)\n\n    def run_action(self, action, session=None):\n        if session is None:\n            session = copy.deepcopy(self.dbcon.Session)\n\n        filtered_session = {\n            key: value\n            for key, value in session.items()\n            if value\n        }\n        # Add to history\n        self.action_history.add_action(action, filtered_session)\n\n        # Process the Action\n        try:\n            action().process(filtered_session)\n        except Exception as exc:\n            self.log.warning(\"Action launch failed.\", exc_info=True)\n            self.echo(\"Failed: {}\".format(str(exc)))\n\n    def set_session(self, session):\n        project_name = session.get(\"AVALON_PROJECT\")\n        asset_name = session.get(\"AVALON_ASSET\")\n        task_name = session.get(\"AVALON_TASK\")\n\n        if project_name:\n            # Force the \"in project\" view.\n            self.page_slider.slide_view(1, direction=\"right\")\n            index = self.asset_panel.project_bar.project_combobox.findText(\n                project_name\n            )\n            if index >= 0:\n                self.asset_panel.project_bar.project_combobox.setCurrentIndex(\n                    index\n                )\n\n        if asset_name:\n            self.asset_panel.select_asset(asset_name)\n\n        if task_name:\n            # requires a forced refresh first\n            self.asset_panel.select_task_name(task_name)\n"
  },
  {
    "path": "openpype/tools/libraryloader/__init__.py",
    "content": "from .app import (\n    LibraryLoaderWindow,\n    show,\n    cli\n)\n\n__all__ = [\n    \"LibraryLoaderWindow\",\n    \"show\",\n    \"cli\",\n]\n"
  },
  {
    "path": "openpype/tools/libraryloader/__main__.py",
    "content": "from . import cli\n\nif __name__ == '__main__':\n    import sys\n    sys.exit(cli(sys.argv[1:]))\n"
  },
  {
    "path": "openpype/tools/libraryloader/app.py",
    "content": "import sys\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import style\nfrom openpype.client import get_projects, get_project\nfrom openpype.pipeline import AvalonMongoDB\nfrom openpype.tools.utils import lib as tools_lib\nfrom openpype.tools.loader.widgets import (\n    ThumbnailWidget,\n    VersionWidget,\n    FamilyListView,\n    RepresentationWidget,\n    SubsetWidget\n)\nfrom openpype.tools.utils.assets_widget import MultiSelectAssetsWidget\n\nfrom openpype.modules import ModulesManager\n\nmodule = sys.modules[__name__]\nmodule.window = None\n\n\nclass LibraryLoaderWindow(QtWidgets.QDialog):\n    \"\"\"Asset library loader interface\"\"\"\n\n    tool_title = \"Library Loader 0.5\"\n    tool_name = \"library_loader\"\n\n    message_timeout = 5000\n\n    def __init__(\n        self, parent=None, show_projects=False, show_libraries=True\n    ):\n        super(LibraryLoaderWindow, self).__init__(parent)\n\n        # Window modifications\n        self.setWindowTitle(self.tool_title)\n        window_flags = QtCore.Qt.Window\n        if not parent:\n            window_flags |= QtCore.Qt.WindowStaysOnTopHint\n        self.setWindowFlags(window_flags)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        icon = QtGui.QIcon(style.app_icon_path())\n        self.setWindowIcon(icon)\n\n        self._first_show = True\n        self._initial_refresh = False\n        self._ignore_project_change = False\n\n        dbcon = AvalonMongoDB()\n        dbcon.install()\n        dbcon.Session[\"AVALON_PROJECT\"] = None\n\n        self.dbcon = dbcon\n\n        self.show_projects = show_projects\n        self.show_libraries = show_libraries\n\n        # Groups config\n        self.groups_config = tools_lib.GroupsConfig(dbcon)\n        self.family_config_cache = tools_lib.FamilyConfigCache(dbcon)\n\n        # UI initialization\n        main_splitter = QtWidgets.QSplitter(self)\n\n        # --- Left part ---\n        left_side_splitter = QtWidgets.QSplitter(main_splitter)\n        left_side_splitter.setOrientation(QtCore.Qt.Vertical)\n\n        # Project combobox\n        projects_combobox = QtWidgets.QComboBox(left_side_splitter)\n        combobox_delegate = QtWidgets.QStyledItemDelegate(self)\n        projects_combobox.setItemDelegate(combobox_delegate)\n\n        # Assets widget\n        assets_widget = MultiSelectAssetsWidget(\n            dbcon, parent=left_side_splitter\n        )\n\n        # Families widget\n        families_filter_view = FamilyListView(\n            dbcon, self.family_config_cache, left_side_splitter\n        )\n        left_side_splitter.addWidget(projects_combobox)\n        left_side_splitter.addWidget(assets_widget)\n        left_side_splitter.addWidget(families_filter_view)\n        left_side_splitter.setStretchFactor(1, 65)\n        left_side_splitter.setStretchFactor(2, 35)\n\n        # --- Middle part ---\n        # Subsets widget\n        subsets_widget = SubsetWidget(\n            dbcon,\n            self.groups_config,\n            self.family_config_cache,\n            tool_name=self.tool_name,\n            parent=self\n        )\n\n        # --- Right part ---\n        thumb_ver_splitter = QtWidgets.QSplitter(main_splitter)\n        thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical)\n\n        thumbnail_widget = ThumbnailWidget(dbcon, parent=thumb_ver_splitter)\n        version_info_widget = VersionWidget(dbcon, parent=thumb_ver_splitter)\n\n        thumb_ver_splitter.addWidget(thumbnail_widget)\n        thumb_ver_splitter.addWidget(version_info_widget)\n\n        thumb_ver_splitter.setStretchFactor(0, 30)\n        thumb_ver_splitter.setStretchFactor(1, 35)\n\n        manager = ModulesManager()\n        sync_server = manager.modules_by_name.get(\"sync_server\")\n        sync_server_enabled = (\n            sync_server is not None\n            and sync_server.enabled\n        )\n\n        repres_widget = None\n        if sync_server_enabled:\n            repres_widget = RepresentationWidget(\n                dbcon, self.tool_name, parent=thumb_ver_splitter\n            )\n            thumb_ver_splitter.addWidget(repres_widget)\n\n        main_splitter.addWidget(left_side_splitter)\n        main_splitter.addWidget(subsets_widget)\n        main_splitter.addWidget(thumb_ver_splitter)\n        if sync_server_enabled:\n            main_splitter.setSizes([250, 1000, 550])\n        else:\n            main_splitter.setSizes([250, 850, 200])\n\n        # --- Footer ---\n        footer_widget = QtWidgets.QWidget(self)\n        footer_widget.setFixedHeight(20)\n\n        message_label = QtWidgets.QLabel(footer_widget)\n\n        footer_layout = QtWidgets.QVBoxLayout(footer_widget)\n        footer_layout.setContentsMargins(0, 0, 0, 0)\n        footer_layout.addWidget(message_label)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(main_splitter)\n        layout.addWidget(footer_widget)\n\n        self.data = {\n            \"state\": {\n                \"assetIds\": None\n            }\n        }\n\n        message_timer = QtCore.QTimer()\n        message_timer.setInterval(self.message_timeout)\n        message_timer.setSingleShot(True)\n\n        message_timer.timeout.connect(self._on_message_timeout)\n\n        families_filter_view.active_changed.connect(\n            self._on_family_filter_change\n        )\n        assets_widget.selection_changed.connect(self.on_assetschanged)\n        assets_widget.refresh_triggered.connect(self.on_assetschanged)\n        subsets_widget.active_changed.connect(self.on_subsetschanged)\n        subsets_widget.version_changed.connect(self.on_versionschanged)\n        subsets_widget.refreshed.connect(self._on_subset_refresh)\n        projects_combobox.currentTextChanged.connect(self.on_project_change)\n\n        self.sync_server = sync_server\n        self._sync_server_enabled = sync_server_enabled\n\n        self._combobox_delegate = combobox_delegate\n        self._projects_combobox = projects_combobox\n        self._assets_widget = assets_widget\n        self._families_filter_view = families_filter_view\n\n        self._subsets_widget = subsets_widget\n\n        self._version_info_widget = version_info_widget\n        self._thumbnail_widget = thumbnail_widget\n        self._repres_widget = repres_widget\n\n        self._message_label = message_label\n        self._message_timer = message_timer\n\n    def showEvent(self, event):\n        super(LibraryLoaderWindow, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self.setStyleSheet(style.load_stylesheet())\n            if self._sync_server_enabled:\n                self.resize(1800, 900)\n            else:\n                self.resize(1300, 700)\n\n            tools_lib.center_window(self)\n\n        if not self._initial_refresh:\n            self._initial_refresh = True\n            self.refresh()\n\n    def _set_projects(self):\n        # Store current project\n        old_project_name = self.current_project\n\n        self._ignore_project_change = True\n\n        # Cleanup\n        self._projects_combobox.clear()\n\n        # Fill combobox with projects\n        select_project_item = QtGui.QStandardItem(\"< Select project >\")\n        select_project_item.setData(None, QtCore.Qt.UserRole + 1)\n\n        combobox_items = [select_project_item]\n\n        project_names = self.get_filtered_projects()\n\n        for project_name in sorted(project_names):\n            item = QtGui.QStandardItem(project_name)\n            item.setData(project_name, QtCore.Qt.UserRole + 1)\n            combobox_items.append(item)\n\n        root_item = self._projects_combobox.model().invisibleRootItem()\n        root_item.appendRows(combobox_items)\n\n        index = 0\n        self._ignore_project_change = False\n\n        if old_project_name:\n            index = self._projects_combobox.findText(\n                old_project_name, QtCore.Qt.MatchFixedString\n            )\n\n        self._projects_combobox.setCurrentIndex(index)\n\n    def get_filtered_projects(self):\n        projects = list()\n        for project in get_projects(fields=[\"name\", \"data.library_project\"]):\n            is_library = project.get(\"data\", {}).get(\"library_project\", False)\n            if (\n                (is_library and self.show_libraries) or\n                (not is_library and self.show_projects)\n            ):\n                projects.append(project[\"name\"])\n\n        return projects\n\n    def on_project_change(self):\n        if self._ignore_project_change:\n            return\n\n        row = self._projects_combobox.currentIndex()\n        index = self._projects_combobox.model().index(row, 0)\n        project_name = index.data(QtCore.Qt.UserRole + 1)\n\n        self.dbcon.Session[\"AVALON_PROJECT\"] = project_name\n\n        self._subsets_widget.on_project_change(project_name)\n        if self._repres_widget:\n            self._repres_widget.on_project_change(project_name)\n\n        self.family_config_cache.refresh()\n        self.groups_config.refresh()\n\n        self._refresh_assets()\n        self._assetschanged()\n\n        project_name = self.dbcon.active_project() or \"No project selected\"\n        title = \"{} - {}\".format(self.tool_title, project_name)\n        self.setWindowTitle(title)\n\n    @property\n    def current_project(self):\n        return self.dbcon.active_project() or None\n\n    # -------------------------------\n    # Delay calling blocking methods\n    # -------------------------------\n\n    def refresh(self):\n        self.echo(\"Fetching results..\")\n        tools_lib.schedule(self._refresh, 50, channel=\"mongo\")\n\n    def on_assetschanged(self, *args):\n        self.echo(\"Fetching asset..\")\n        tools_lib.schedule(self._assetschanged, 50, channel=\"mongo\")\n\n    def on_subsetschanged(self, *args):\n        self.echo(\"Fetching subset..\")\n        tools_lib.schedule(self._subsetschanged, 50, channel=\"mongo\")\n\n    def on_versionschanged(self, *args):\n        self.echo(\"Fetching version..\")\n        tools_lib.schedule(self._versionschanged, 150, channel=\"mongo\")\n\n    def _on_subset_refresh(self, has_item):\n        self._subsets_widget.set_loading_state(\n            loading=False, empty=not has_item\n        )\n        families = self._subsets_widget.get_subsets_families()\n        self._families_filter_view.set_enabled_families(families)\n\n    # ------------------------------\n    def set_context(self, context, refresh=True):\n        \"\"\"Set the selection in the interface using a context.\n        The context must contain `asset` data by name.\n\n        Args:\n            context (dict): The context to apply.\n        Returns:\n            None\n        \"\"\"\n\n        asset_name = context.get(\"asset\", None)\n        if asset_name is None:\n            return\n\n        if refresh:\n            self._refresh_assets()\n\n        self._assets_widget.select_asset_by_name(asset_name)\n\n    def _on_family_filter_change(self, families):\n        self._subsets_widget.set_family_filters(families)\n\n    def _refresh(self):\n        if not self._initial_refresh:\n            self._initial_refresh = True\n        self._set_projects()\n\n    def _refresh_assets(self):\n        \"\"\"Load assets from database\"\"\"\n        if self.current_project is not None:\n            # Ensure a project is loaded\n            project_doc = get_project(self.current_project, fields=[\"_id\"])\n            assert project_doc, \"This is a bug\"\n\n        self._families_filter_view.set_enabled_families(set())\n        self._families_filter_view.refresh()\n\n        self._assets_widget.stop_refresh()\n        self._assets_widget.refresh()\n        self._assets_widget.setFocus()\n\n    def clear_assets_underlines(self):\n        last_asset_ids = self.data[\"state\"][\"assetIds\"]\n        if last_asset_ids:\n            self._assets_widget.clear_underlines()\n\n    def _assetschanged(self):\n        \"\"\"Selected assets have changed\"\"\"\n        subsets_model = self._subsets_widget.model\n\n        subsets_model.clear()\n        self.clear_assets_underlines()\n\n        if not self.dbcon.Session.get(\"AVALON_PROJECT\"):\n            self._subsets_widget.set_loading_state(\n                loading=False,\n                empty=True\n            )\n            return\n\n        asset_ids = self._assets_widget.get_selected_asset_ids()\n\n        # Start loading\n        self._subsets_widget.set_loading_state(\n            loading=bool(asset_ids),\n            empty=True\n        )\n\n        subsets_model.set_assets(asset_ids)\n        self._subsets_widget.view.setColumnHidden(\n            subsets_model.Columns.index(\"asset\"),\n            len(asset_ids) < 2\n        )\n\n        # Clear the version information on asset change\n        self._version_info_widget.set_version(None)\n        self._thumbnail_widget.set_thumbnail(\"asset\", asset_ids)\n\n        self.data[\"state\"][\"assetIds\"] = asset_ids\n\n        # reset repre list\n        if self._repres_widget:\n            self._repres_widget.set_version_ids([])\n\n    def _subsetschanged(self):\n        asset_ids = self.data[\"state\"][\"assetIds\"]\n        # Skip setting colors if not asset multiselection\n        if not asset_ids or len(asset_ids) < 2:\n            self._versionschanged()\n            return\n\n        selected_subsets = self._subsets_widget.get_selected_merge_items()\n\n        asset_colors = {}\n        asset_ids = []\n        for subset_node in selected_subsets:\n            asset_ids.extend(subset_node.get(\"assetIds\", []))\n        asset_ids = set(asset_ids)\n\n        for subset_node in selected_subsets:\n            for asset_id in asset_ids:\n                if asset_id not in asset_colors:\n                    asset_colors[asset_id] = []\n\n                color = None\n                if asset_id in subset_node.get(\"assetIds\", []):\n                    color = subset_node[\"subsetColor\"]\n\n                asset_colors[asset_id].append(color)\n\n        self._assets_widget.set_underline_colors(asset_colors)\n\n        # Set version in Version Widget\n        self._versionschanged()\n\n    def _versionschanged(self):\n        items = self._subsets_widget.get_selected_subsets()\n        version_doc = None\n        version_docs = []\n        for item in items:\n            doc = item[\"version_document\"]\n            version_docs.append(doc)\n            if version_doc is None:\n                version_doc = doc\n\n        self._version_info_widget.set_version(version_doc)\n\n        thumbnail_src_ids = [\n            version_doc[\"_id\"]\n            for version_doc in version_docs\n        ]\n        src_type = \"version\"\n        if not thumbnail_src_ids:\n            src_type = \"asset\"\n            thumbnail_src_ids = self._assets_widget.get_selected_asset_ids()\n\n        self._thumbnail_widget.set_thumbnail(src_type, thumbnail_src_ids)\n\n        version_ids = [doc[\"_id\"] for doc in version_docs or []]\n        if self._repres_widget:\n            self._repres_widget.set_version_ids(version_ids)\n\n    def _on_message_timeout(self):\n        self._message_label.setText(\"\")\n\n    def echo(self, message):\n        self._message_label.setText(str(message))\n        print(message)\n        self._message_timer.start()\n\n    def closeEvent(self, event):\n        # Kill on holding SHIFT\n        modifiers = QtWidgets.QApplication.queryKeyboardModifiers()\n        shift_pressed = QtCore.Qt.ShiftModifier & modifiers\n\n        if shift_pressed:\n            print(\"Force quitted..\")\n            self.setAttribute(QtCore.Qt.WA_DeleteOnClose)\n\n        print(\"Good bye\")\n        return super(LibraryLoaderWindow, self).closeEvent(event)\n\n\ndef show(debug=False, parent=None, show_projects=False, show_libraries=True):\n    \"\"\"Display Loader GUI\n\n    Arguments:\n        debug (bool, optional): Run loader in debug-mode,\n            defaults to False\n        parent (QtCore.QObject, optional): The Qt object to parent to.\n        use_context (bool): Whether to apply the current context upon launch\n\n    \"\"\"\n    # Remember window\n    if module.window is not None:\n        try:\n            module.window.show()\n\n            # If the window is minimized then unminimize it.\n            if module.window.windowState() & QtCore.Qt.WindowMinimized:\n                module.window.setWindowState(QtCore.Qt.WindowActive)\n\n            # Raise and activate the window\n            module.window.raise_()             # for MacOS\n            module.window.activateWindow()     # for Windows\n            module.window.refresh()\n            return\n        except RuntimeError as e:\n            if not e.message.rstrip().endswith(\"already deleted.\"):\n                raise\n\n            # Garbage collected\n            module.window = None\n\n    if debug:\n        import traceback\n        sys.excepthook = lambda typ, val, tb: traceback.print_last()\n\n    with tools_lib.qt_app_context():\n        window = LibraryLoaderWindow(\n            parent, show_projects, show_libraries\n        )\n        window.show()\n\n        module.window = window\n\n\ndef cli(args):\n\n    import argparse\n\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"project\")\n\n    show(show_projects=True, show_libraries=True)\n"
  },
  {
    "path": "openpype/tools/loader/__init__.py",
    "content": "from .app import (\n    LoaderWindow,\n    show,\n    cli,\n)\n\n__all__ = (\n    \"LoaderWindow\",\n    \"show\",\n    \"cli\",\n)\n"
  },
  {
    "path": "openpype/tools/loader/__main__.py",
    "content": "\"\"\"Main entrypoint for standalone debugging\n\n    Used for running 'avalon.tool.loader.__main__' as a module (-m), useful for\n    debugging without need to start host.\n\n    Modify AVALON_MONGO accordingly\n\"\"\"\nimport os\nimport sys\nfrom . import cli\n\n\ndef my_exception_hook(exctype, value, traceback):\n    # Print the error and traceback\n    print(exctype, value, traceback)\n    # Call the normal Exception hook after\n    sys._excepthook(exctype, value, traceback)\n    sys.exit(1)\n\n\nif __name__ == '__main__':\n    os.environ[\"OPENPYPE_MONGO\"] = \"mongodb://localhost:27017\"\n    os.environ[\"AVALON_DB\"] = \"avalon\"\n    os.environ[\"AVALON_TIMEOUT\"] = \"1000\"\n    os.environ[\"OPENPYPE_DEBUG\"] = \"1\"\n    os.environ[\"AVALON_ASSET\"] = \"Jungle\"\n\n    # Set the exception hook to our wrapping function\n    sys.excepthook = my_exception_hook\n\n    sys.exit(cli(sys.argv[1:]))\n"
  },
  {
    "path": "openpype/tools/loader/app.py",
    "content": "import sys\nimport traceback\n\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.client import get_projects, get_project\nfrom openpype import style\nfrom openpype.lib import register_event_callback\nfrom openpype.pipeline import (\n    install_openpype_plugins,\n    legacy_io,\n)\nfrom openpype.tools.utils import (\n    lib,\n    PlaceholderLineEdit\n)\nfrom openpype.tools.utils.assets_widget import MultiSelectAssetsWidget\n\nfrom .widgets import (\n    SubsetWidget,\n    VersionWidget,\n    FamilyListView,\n    ThumbnailWidget,\n    RepresentationWidget,\n    OverlayFrame\n)\n\nfrom openpype.modules import ModulesManager\n\nmodule = sys.modules[__name__]\nmodule.window = None\n\n\nclass LoaderWindow(QtWidgets.QDialog):\n    \"\"\"Asset loader interface\"\"\"\n\n    tool_name = \"loader\"\n    message_timeout = 5000\n\n    def __init__(self, parent=None):\n        super(LoaderWindow, self).__init__(parent)\n        title = \"Asset Loader 2.1\"\n        project_name = legacy_io.active_project()\n        if project_name:\n            title += \" - {}\".format(project_name)\n        self.setWindowTitle(title)\n\n        # Groups config\n        self.groups_config = lib.GroupsConfig(legacy_io)\n        self.family_config_cache = lib.FamilyConfigCache(legacy_io)\n\n        # Enable minimize and maximize for app\n        window_flags = QtCore.Qt.Window\n        if not parent:\n            window_flags |= QtCore.Qt.WindowStaysOnTopHint\n        self.setWindowFlags(window_flags)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        main_splitter = QtWidgets.QSplitter(self)\n\n        # --- Left part ---\n        left_side_splitter = QtWidgets.QSplitter(main_splitter)\n        left_side_splitter.setOrientation(QtCore.Qt.Vertical)\n\n        # Assets widget\n        assets_widget = MultiSelectAssetsWidget(\n            legacy_io, parent=left_side_splitter\n        )\n        assets_widget.set_current_asset_btn_visibility(True)\n\n        # Families widget\n        families_filter_view = FamilyListView(\n            legacy_io, self.family_config_cache, left_side_splitter\n        )\n        left_side_splitter.addWidget(assets_widget)\n        left_side_splitter.addWidget(families_filter_view)\n        left_side_splitter.setStretchFactor(0, 65)\n        left_side_splitter.setStretchFactor(1, 35)\n\n        # --- Middle part ---\n        # Subsets widget\n        subsets_widget = SubsetWidget(\n            legacy_io,\n            self.groups_config,\n            self.family_config_cache,\n            tool_name=self.tool_name,\n            parent=main_splitter\n        )\n\n        # --- Right part ---\n        thumb_ver_splitter = QtWidgets.QSplitter(main_splitter)\n        thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical)\n\n        thumbnail_widget = ThumbnailWidget(\n            legacy_io, parent=thumb_ver_splitter\n        )\n        version_info_widget = VersionWidget(\n            legacy_io, parent=thumb_ver_splitter\n        )\n\n        thumb_ver_splitter.addWidget(thumbnail_widget)\n        thumb_ver_splitter.addWidget(version_info_widget)\n\n        thumb_ver_splitter.setStretchFactor(0, 30)\n        thumb_ver_splitter.setStretchFactor(1, 35)\n\n        manager = ModulesManager()\n        sync_server = manager.modules_by_name.get(\"sync_server\")\n        sync_server_enabled = False\n        if sync_server is not None:\n            sync_server_enabled = sync_server.enabled\n\n        repres_widget = None\n        if sync_server_enabled:\n            repres_widget = RepresentationWidget(\n                legacy_io, self.tool_name, parent=thumb_ver_splitter\n            )\n            thumb_ver_splitter.addWidget(repres_widget)\n\n        main_splitter.addWidget(left_side_splitter)\n        main_splitter.addWidget(subsets_widget)\n        main_splitter.addWidget(thumb_ver_splitter)\n\n        if sync_server_enabled:\n            main_splitter.setSizes([250, 1000, 550])\n        else:\n            main_splitter.setSizes([250, 850, 200])\n\n        footer_widget = QtWidgets.QWidget(self)\n\n        message_label = QtWidgets.QLabel(footer_widget)\n\n        footer_layout = QtWidgets.QHBoxLayout(footer_widget)\n        footer_layout.setContentsMargins(0, 0, 0, 0)\n        footer_layout.addWidget(message_label, 1)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(main_splitter, 1)\n        layout.addWidget(footer_widget, 0)\n\n        self.data = {\n            \"state\": {\n                \"assetIds\": None\n            }\n        }\n\n        overlay_frame = OverlayFrame(\"Loading...\", self)\n        overlay_frame.setVisible(False)\n\n        message_timer = QtCore.QTimer()\n        message_timer.setInterval(self.message_timeout)\n        message_timer.setSingleShot(True)\n\n        message_timer.timeout.connect(self._on_message_timeout)\n\n        families_filter_view.active_changed.connect(\n            self._on_family_filter_change\n        )\n        assets_widget.selection_changed.connect(self.on_assetschanged)\n        assets_widget.refresh_triggered.connect(self.on_assetschanged)\n        subsets_widget.active_changed.connect(self.on_subsetschanged)\n        subsets_widget.version_changed.connect(self.on_versionschanged)\n        subsets_widget.refreshed.connect(self._on_subset_refresh)\n\n        subsets_widget.load_started.connect(self._on_load_start)\n        subsets_widget.load_ended.connect(self._on_load_end)\n        if repres_widget:\n            repres_widget.load_started.connect(self._on_load_start)\n            repres_widget.load_ended.connect(self._on_load_end)\n\n        self._sync_server_enabled = sync_server_enabled\n\n        self._assets_widget = assets_widget\n        self._families_filter_view = families_filter_view\n\n        self._subsets_widget = subsets_widget\n\n        self._version_info_widget = version_info_widget\n        self._thumbnail_widget = thumbnail_widget\n        self._repres_widget = repres_widget\n\n        self._message_label = message_label\n        self._message_timer = message_timer\n\n        # TODO add overlay using stack widget\n        self._overlay_frame = overlay_frame\n\n        self.family_config_cache.refresh()\n        self.groups_config.refresh()\n\n        self._refresh()\n        self._assetschanged()\n\n        self._first_show = True\n\n        register_event_callback(\"taskChanged\", self.on_context_task_change)\n\n    def resizeEvent(self, event):\n        super(LoaderWindow, self).resizeEvent(event)\n        self._overlay_frame.resize(self.size())\n\n    def moveEvent(self, event):\n        super(LoaderWindow, self).moveEvent(event)\n        self._overlay_frame.move(0, 0)\n\n    def showEvent(self, event):\n        super(LoaderWindow, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self.setStyleSheet(style.load_stylesheet())\n            if self._sync_server_enabled:\n                self.resize(1800, 900)\n            else:\n                self.resize(1300, 700)\n            lib.center_window(self)\n\n    # -------------------------------\n    # Delay calling blocking methods\n    # -------------------------------\n\n    def refresh(self):\n        self.echo(\"Fetching results..\")\n        lib.schedule(self._refresh, 50, channel=\"mongo\")\n\n    def on_assetschanged(self, *args):\n        self.echo(\"Fetching hierarchy..\")\n        lib.schedule(self._assetschanged, 50, channel=\"mongo\")\n\n    def on_subsetschanged(self, *args):\n        self.echo(\"Fetching subset..\")\n        lib.schedule(self._subsetschanged, 50, channel=\"mongo\")\n\n    def on_versionschanged(self, *args):\n        self.echo(\"Fetching version..\")\n        lib.schedule(self._versionschanged, 150, channel=\"mongo\")\n\n    def set_context(self, context, refresh=True):\n        self.echo(\"Setting context: {}\".format(context))\n        lib.schedule(lambda: self._set_context(context, refresh=refresh),\n                     50, channel=\"mongo\")\n\n    def _on_load_start(self):\n        # Show overlay and process events so it's repainted\n        self._overlay_frame.setVisible(True)\n        QtWidgets.QApplication.processEvents()\n\n    def _hide_overlay(self):\n        self._overlay_frame.setVisible(False)\n\n    def _on_subset_refresh(self, has_item):\n        self._subsets_widget.set_loading_state(\n            loading=False, empty=not has_item\n        )\n        families = self._subsets_widget.get_subsets_families()\n        self._families_filter_view.set_enabled_families(families)\n\n    def _on_load_end(self):\n        # Delay hiding as click events happened during loading should be\n        #   blocked\n        QtCore.QTimer.singleShot(100, self._hide_overlay)\n\n    # ------------------------------\n    def _on_family_filter_change(self, families):\n        self._subsets_widget.set_family_filters(families)\n\n    def on_context_task_change(self, *args, **kwargs):\n        # Refresh families config\n        self._families_filter_view.refresh()\n        # Change to context asset on context change\n        self._assets_widget.select_asset_by_name(\n            legacy_io.Session[\"AVALON_ASSET\"]\n        )\n\n    def _refresh(self):\n        \"\"\"Load assets from database\"\"\"\n\n        # Ensure a project is loaded\n        project_name = legacy_io.active_project()\n        project_doc = get_project(project_name, fields=[\"_id\"])\n        assert project_doc, \"Project was not found! This is a bug\"\n\n        self._assets_widget.refresh()\n        self._assets_widget.setFocus()\n\n        self._families_filter_view.refresh()\n\n    def clear_assets_underlines(self):\n        \"\"\"Clear colors from asset data to remove colored underlines\n        When multiple assets are selected colored underlines mark which asset\n        own selected subsets. These colors must be cleared from asset data\n        on selection change so they match current selection.\n        \"\"\"\n        # TODO do not touch inner attributes of asset widget\n        self._assets_widget.clear_underlines()\n\n    def _assetschanged(self):\n        \"\"\"Selected assets have changed\"\"\"\n        subsets_widget = self._subsets_widget\n        # TODO do not touch subset widget inner attributes\n        subsets_model = subsets_widget.model\n\n        subsets_model.clear()\n        self.clear_assets_underlines()\n\n        asset_ids = self._assets_widget.get_selected_asset_ids()\n        # Start loading\n        subsets_widget.set_loading_state(\n            loading=bool(asset_ids),\n            empty=True\n        )\n\n        subsets_model.set_assets(asset_ids)\n        subsets_widget.view.setColumnHidden(\n            subsets_model.Columns.index(\"asset\"),\n            len(asset_ids) < 2\n        )\n\n        # Clear the version information on asset change\n        self._thumbnail_widget.set_thumbnail(\"asset\", asset_ids)\n        self._version_info_widget.set_version(None)\n\n        self.data[\"state\"][\"assetIds\"] = asset_ids\n\n        # reset repre list\n        if self._repres_widget is not None:\n            self._repres_widget.set_version_ids([])\n\n    def _subsetschanged(self):\n        asset_ids = self.data[\"state\"][\"assetIds\"]\n        # Skip setting colors if not asset multiselection\n        if not asset_ids or len(asset_ids) < 2:\n            self.clear_assets_underlines()\n            self._versionschanged()\n            return\n\n        selected_subsets = self._subsets_widget.get_selected_merge_items()\n\n        asset_colors = {}\n        asset_ids = []\n        for subset_node in selected_subsets:\n            asset_ids.extend(subset_node.get(\"assetIds\", []))\n        asset_ids = set(asset_ids)\n\n        for subset_node in selected_subsets:\n            for asset_id in asset_ids:\n                if asset_id not in asset_colors:\n                    asset_colors[asset_id] = []\n\n                color = None\n                if asset_id in subset_node.get(\"assetIds\", []):\n                    color = subset_node[\"subsetColor\"]\n\n                asset_colors[asset_id].append(color)\n\n        self._assets_widget.set_underline_colors(asset_colors)\n\n        # Set version in Version Widget\n        self._versionschanged()\n\n    def _versionschanged(self):\n        items = self._subsets_widget.get_selected_subsets()\n        version_doc = None\n        version_docs = []\n        for item in items:\n            doc = item[\"version_document\"]\n            version_docs.append(doc)\n            if version_doc is None:\n                version_doc = doc\n\n        self._version_info_widget.set_version(version_doc)\n\n        thumbnail_src_ids = [\n            version_doc[\"_id\"]\n            for version_doc in version_docs\n        ]\n        source_type = \"version\"\n        if not thumbnail_src_ids:\n            source_type = \"asset\"\n            thumbnail_src_ids = self._assets_widget.get_selected_asset_ids()\n\n        self._thumbnail_widget.set_thumbnail(source_type, thumbnail_src_ids)\n\n        if self._repres_widget is not None:\n            version_ids = [doc[\"_id\"] for doc in version_docs]\n            self._repres_widget.set_version_ids(version_ids)\n\n            # self._repres_widget.change_visibility(\"subset\", len(rows) > 1)\n            # self._repres_widget.change_visibility(\n            #     \"asset\", len(asset_docs) > 1\n            # )\n\n    def _set_context(self, context, refresh=True):\n        \"\"\"Set the selection in the interface using a context.\n\n        The context must contain `asset` data by name.\n\n        Args:\n            context (dict): The context to apply.\n            refrest (bool): Trigger refresh on context set.\n        \"\"\"\n\n        asset = context.get(\"asset\", None)\n        if asset is None:\n            return\n\n        if refresh:\n            self._refresh()\n\n        self._assets_widget.select_asset_by_name(asset)\n\n    def _on_message_timeout(self):\n        self._message_label.setText(\"\")\n\n    def echo(self, message):\n        self._message_label.setText(str(message))\n        print(message)\n        self._message_timer.start()\n\n    def closeEvent(self, event):\n        # Kill on holding SHIFT\n        modifiers = QtWidgets.QApplication.queryKeyboardModifiers()\n        shift_pressed = QtCore.Qt.ShiftModifier & modifiers\n\n        if shift_pressed:\n            print(\"Force quit..\")\n            self.setAttribute(QtCore.Qt.WA_DeleteOnClose)\n\n        print(\"Good bye\")\n        return super(LoaderWindow, self).closeEvent(event)\n\n    def keyPressEvent(self, event):\n        modifiers = event.modifiers()\n        ctrl_pressed = QtCore.Qt.ControlModifier & modifiers\n\n        # Grouping subsets on pressing Ctrl + G\n        if (ctrl_pressed and event.key() == QtCore.Qt.Key_G and\n                not event.isAutoRepeat()):\n            self.show_grouping_dialog()\n            return\n\n        super(LoaderWindow, self).keyPressEvent(event)\n        event.setAccepted(True)  # Avoid interfering other widgets\n\n    def show_grouping_dialog(self):\n        subsets = self._subsets_widget\n        if not subsets.is_groupable():\n            self.echo(\"Grouping not enabled.\")\n            return\n\n        selected = self._subsets_widget.get_selected_subsets()\n        if not selected:\n            self.echo(\"No selected subset.\")\n            return\n\n        dialog = SubsetGroupingDialog(\n            items=selected, groups_config=self.groups_config, parent=self\n        )\n        dialog.grouped.connect(self._assetschanged)\n        dialog.show()\n\n\nclass SubsetGroupingDialog(QtWidgets.QDialog):\n    grouped = QtCore.Signal()\n\n    def __init__(self, items, groups_config, parent=None):\n        super(SubsetGroupingDialog, self).__init__(parent=parent)\n        self.setWindowTitle(\"Grouping Subsets\")\n        self.setMinimumWidth(250)\n        self.setModal(True)\n\n        self.items = items\n        self.groups_config = groups_config\n        # TODO do not touch inner attributes\n        self.subsets = parent._subsets_widget\n        self.asset_ids = parent.data[\"state\"][\"assetIds\"]\n\n        name = PlaceholderLineEdit(self)\n        name.setPlaceholderText(\"Remain blank to ungroup..\")\n\n        # Menu for pre-defined subset groups\n        name_button = QtWidgets.QPushButton()\n        name_button.setFixedWidth(18)\n        name_button.setFixedHeight(20)\n        name_menu = QtWidgets.QMenu(name_button)\n        name_button.setMenu(name_menu)\n\n        name_layout = QtWidgets.QHBoxLayout()\n        name_layout.addWidget(name)\n        name_layout.addWidget(name_button)\n        name_layout.setContentsMargins(0, 0, 0, 0)\n\n        group_btn = QtWidgets.QPushButton(\"Apply\")\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(QtWidgets.QLabel(\"Group Name\"))\n        layout.addLayout(name_layout)\n        layout.addWidget(group_btn)\n\n        group_btn.clicked.connect(self.on_group)\n        group_btn.setAutoDefault(True)\n        group_btn.setDefault(True)\n\n        self.name = name\n        self.name_menu = name_menu\n\n        self._build_menu()\n\n    def _build_menu(self):\n        menu = self.name_menu\n        button = menu.parent()\n        # Get and destroy the action group\n        group = button.findChild(QtWidgets.QActionGroup)\n        if group:\n            group.deleteLater()\n\n        active_groups = self.groups_config.active_groups(self.asset_ids)\n\n        # Build new action group\n        group = QtWidgets.QActionGroup(button)\n        group_names = list()\n        for data in sorted(active_groups, key=lambda x: x[\"order\"]):\n            name = data[\"name\"]\n            if name in group_names:\n                continue\n            group_names.append(name)\n            icon = data[\"icon\"]\n\n            action = group.addAction(name)\n            action.setIcon(icon)\n            menu.addAction(action)\n\n        group.triggered.connect(self._on_action_clicked)\n        button.setEnabled(not menu.isEmpty())\n\n    def _on_action_clicked(self, action):\n        self.name.setText(action.text())\n\n    def on_group(self):\n        name = self.name.text().strip()\n        self.subsets.group_subsets(name, self.asset_ids, self.items)\n\n        with lib.preserve_selection(tree_view=self.subsets.view,\n                                    current_index=False):\n            self.grouped.emit()\n            self.close()\n\n\ndef show(debug=False, parent=None, use_context=False):\n    \"\"\"Display Loader GUI\n\n    Arguments:\n        debug (bool, optional): Run loader in debug-mode,\n            defaults to False\n        parent (QtCore.QObject, optional): The Qt object to parent to.\n        use_context (bool): Whether to apply the current context upon launch\n\n    \"\"\"\n\n    # Remember window\n    if module.window is not None:\n        try:\n            module.window.show()\n\n            # If the window is minimized then unminimize it.\n            if module.window.windowState() & QtCore.Qt.WindowMinimized:\n                module.window.setWindowState(QtCore.Qt.WindowActive)\n\n            # Raise and activate the window\n            module.window.raise_()             # for MacOS\n            module.window.activateWindow()     # for Windows\n            module.window.refresh()\n            return\n        except (AttributeError, RuntimeError):\n            # Garbage collected\n            module.window = None\n\n    if debug:\n        sys.excepthook = lambda typ, val, tb: traceback.print_last()\n\n        legacy_io.install()\n\n        any_project = next(\n            project for project in get_projects(fields=[\"name\"])\n        )\n\n        legacy_io.Session[\"AVALON_PROJECT\"] = any_project[\"name\"]\n        module.project = any_project[\"name\"]\n\n    with lib.qt_app_context():\n        window = LoaderWindow(parent)\n        window.show()\n\n        if use_context:\n            context = {\"asset\": legacy_io.Session[\"AVALON_ASSET\"]}\n            window.set_context(context, refresh=True)\n        else:\n            window.refresh()\n\n        module.window = window\n\n        # Pull window to the front.\n        module.window.raise_()\n        module.window.activateWindow()\n\n\ndef cli(args):\n\n    import argparse\n\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"project\")\n\n    args = parser.parse_args(args)\n    project = args.project\n\n    print(\"Entering Project: %s\" % project)\n\n    legacy_io.install()\n\n    # Store settings\n    legacy_io.Session[\"AVALON_PROJECT\"] = project\n\n    install_openpype_plugins(project)\n\n    show()\n"
  },
  {
    "path": "openpype/tools/loader/delegates.py",
    "content": "from qtpy import QtWidgets, QtGui, QtCore\n\n\nclass LoadedInSceneDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Delegate for Loaded in Scene state columns.\n\n    Shows \"yes\" or \"no\" for True or False values\n    Colorizes green or dark grey based on True or False values\n\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(LoadedInSceneDelegate, self).__init__(*args, **kwargs)\n        self._colors = {\n            True: QtGui.QColor(80, 170, 80),\n            False: QtGui.QColor(90, 90, 90)\n        }\n\n    def displayText(self, value, locale):\n        return \"yes\" if value else \"no\"\n\n    def initStyleOption(self, option, index):\n        super(LoadedInSceneDelegate, self).initStyleOption(option, index)\n\n        # Colorize based on value\n        value = index.data(QtCore.Qt.DisplayRole)\n        color = self._colors[bool(value)]\n        option.palette.setBrush(QtGui.QPalette.Text, color)\n"
  },
  {
    "path": "openpype/tools/loader/lib.py",
    "content": "import inspect\nfrom qtpy import QtGui\nimport qtawesome\n\nfrom openpype.lib.attribute_definitions import AbstractAttrDef\nfrom openpype.tools.attribute_defs import AttributeDefinitionsDialog\nfrom openpype.tools.utils.widgets import (\n    OptionalAction,\n    OptionDialog\n)\n\n\ndef change_visibility(model, view, column_name, visible):\n    \"\"\"\n        Hides or shows particular 'column_name'.\n\n        \"asset\" and \"subset\" columns should be visible only in multiselect\n    \"\"\"\n    index = model.Columns.index(column_name)\n    view.setColumnHidden(index, not visible)\n\n\ndef get_options(action, loader, parent, repre_contexts):\n    \"\"\"Provides dialog to select value from loader provided options.\n\n        Loader can provide static or dynamically created options based on\n        qargparse variants.\n\n        Args:\n            action (OptionalAction) - action in menu\n            loader (cls of api.Loader) - not initialized yet\n            parent (Qt element to parent dialog to)\n            repre_contexts (list) of dict with full info about selected repres\n        Returns:\n            (dict) - selected value from OptionDialog\n            None when dialog was closed or cancelled, in all other cases {}\n              if no options\n    \"\"\"\n\n    # Pop option dialog\n    options = {}\n    loader_options = loader.get_options(repre_contexts)\n    if not getattr(action, \"optioned\", False) or not loader_options:\n        return options\n\n    if isinstance(loader_options[0], AbstractAttrDef):\n        qargparse_options = False\n        dialog = AttributeDefinitionsDialog(loader_options, parent)\n    else:\n        qargparse_options = True\n        dialog = OptionDialog(parent)\n        dialog.create(loader_options)\n\n    dialog.setWindowTitle(action.label + \" Options\")\n\n    if not dialog.exec_():\n        return None\n\n    # Get option\n    if qargparse_options:\n        return dialog.parse()\n    return dialog.get_values()\n\n\ndef add_representation_loaders_to_menu(loaders, menu, repre_contexts):\n    \"\"\"\n        Loops through provider loaders and adds them to 'menu'.\n\n        Expects loaders sorted in requested order.\n        Expects loaders de-duplicated if wanted.\n\n        Args:\n            loaders(tuple): representation - loader\n            menu (OptionalMenu):\n            repre_contexts (dict): full info about representations (contains\n                their repre_doc, asset_doc, subset_doc, version_doc),\n                keys are repre_ids\n\n        Returns:\n            menu (OptionalMenu): with new items\n    \"\"\"\n    # List the available loaders\n    for representation, loader in loaders:\n        label = None\n        repre_context = None\n        if representation:\n            label = representation.get(\"custom_label\")\n            repre_context = repre_contexts[representation[\"_id\"]]\n\n        if not label:\n            label = get_label_from_loader(loader, representation)\n\n        icon = get_icon_from_loader(loader)\n\n        loader_options = loader.get_options([repre_context])\n\n        use_option = bool(loader_options)\n        action = OptionalAction(label, icon, use_option, menu)\n        if use_option:\n            # Add option box tip\n            action.set_option_tip(loader_options)\n\n        action.setData((representation, loader))\n\n        # Add tooltip and statustip from Loader docstring\n        tip = inspect.getdoc(loader)\n        if tip:\n            action.setToolTip(tip)\n            action.setStatusTip(tip)\n\n        menu.addAction(action)\n\n    return menu\n\n\ndef remove_tool_name_from_loaders(available_loaders, tool_name):\n    if not tool_name:\n        return available_loaders\n    filtered_loaders = []\n    for loader in available_loaders:\n        if hasattr(loader, \"tool_names\"):\n            if not (\"*\" in loader.tool_names or\n                    tool_name in loader.tool_names):\n                continue\n        filtered_loaders.append(loader)\n    return filtered_loaders\n\n\ndef get_icon_from_loader(loader):\n    \"\"\"Pull icon info from loader class\"\"\"\n    # Support font-awesome icons using the `.icon` and `.color`\n    # attributes on plug-ins.\n    icon = getattr(loader, \"icon\", None)\n    if icon is not None:\n        try:\n            key = \"fa.{0}\".format(icon)\n            color = getattr(loader, \"color\", \"white\")\n            icon = qtawesome.icon(key, color=color)\n        except Exception as e:\n            print(\"Unable to set icon for loader \"\n                  \"{}: {}\".format(loader, e))\n            icon = None\n    return icon\n\n\ndef get_label_from_loader(loader, representation=None):\n    \"\"\"Pull label info from loader class\"\"\"\n    label = getattr(loader, \"label\", None)\n    if label is None:\n        label = loader.__name__\n    if representation:\n        # Add the representation as suffix\n        label = \"{0} ({1})\".format(label, representation['name'])\n    return label\n\n\ndef get_no_loader_action(menu, one_item_selected=False):\n    \"\"\"Creates dummy no loader option in 'menu'\"\"\"\n    submsg = \"your selection.\"\n    if one_item_selected:\n        submsg = \"this version.\"\n    msg = \"No compatible loaders for {}\".format(submsg)\n    print(msg)\n    icon = qtawesome.icon(\n        \"fa.exclamation\",\n        color=QtGui.QColor(255, 51, 0)\n    )\n    action = OptionalAction((\"*\" + msg), icon, False, menu)\n    return action\n\n\ndef sort_loaders(loaders, custom_sorter=None):\n    def sorter(value):\n        \"\"\"Sort the Loaders by their order and then their name\"\"\"\n        Plugin = value[1]\n        return Plugin.order, Plugin.__name__\n\n    if not custom_sorter:\n        custom_sorter = sorter\n\n    return sorted(loaders, key=custom_sorter)\n"
  },
  {
    "path": "openpype/tools/loader/model.py",
    "content": "import copy\nimport re\nimport math\nimport time\nfrom uuid import uuid4\n\nfrom qtpy import QtCore, QtGui\nimport qtawesome\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_assets,\n    get_subsets,\n    get_last_versions,\n    get_versions,\n    get_hero_versions,\n    get_version_by_name,\n    get_representations\n)\nfrom openpype.pipeline import (\n    registered_host,\n    HeroVersionType,\n    schema,\n)\n\nfrom openpype.style import get_default_entity_icon_color\nfrom openpype.tools.utils.models import TreeModel, Item\nfrom openpype.tools.utils import lib\nfrom openpype.host import ILoadHost\n\nfrom openpype.modules import ModulesManager\nfrom openpype.tools.utils.constants import (\n    LOCAL_PROVIDER_ROLE,\n    REMOTE_PROVIDER_ROLE,\n    LOCAL_AVAILABILITY_ROLE,\n    REMOTE_AVAILABILITY_ROLE\n)\n\nITEM_ID_ROLE = QtCore.Qt.UserRole + 90\n\n\ndef is_filtering_recursible():\n    \"\"\"Does Qt binding support recursive filtering for QSortFilterProxyModel?\n\n    (NOTE) Recursive filtering was introduced in Qt 5.10.\n\n    \"\"\"\n    return hasattr(QtCore.QSortFilterProxyModel,\n                   \"setRecursiveFilteringEnabled\")\n\n\nclass BaseRepresentationModel(object):\n    \"\"\"Methods for SyncServer useful in multiple models\"\"\"\n    # Cheap & hackish way how to avoid refreshing of whole sync server module\n    #   on each selection change\n    _last_project = None\n    _modules_manager = None\n    _last_project_cache = 0\n    _last_manager_cache = 0\n    _max_project_cache_time = 30\n    _max_manager_cache_time = 60\n\n    def reset_sync_server(self, project_name=None):\n        \"\"\"Sets/Resets sync server vars after every change (refresh.)\"\"\"\n        repre_icons = {}\n        sync_server = None\n        sync_server_enabled = False\n        active_site = active_provider = None\n        remote_site = remote_provider = None\n\n        if not project_name:\n            project_name = self.dbcon.active_project()\n        else:\n            self.dbcon.Session[\"AVALON_PROJECT\"] = project_name\n\n        if not project_name:\n            self.repre_icons = repre_icons\n            self.sync_server = sync_server\n            self.sync_server_enabled = sync_server_enabled\n            self.active_site = active_site\n            self.active_provider = active_provider\n            self.remote_site = remote_site\n            self.remote_provider = remote_provider\n            return\n\n        now_time = time.time()\n        project_cache_diff = now_time - self._last_project_cache\n        if project_cache_diff > self._max_project_cache_time:\n            self._last_project = None\n\n        if project_name == self._last_project:\n            return\n\n        self._last_project = project_name\n        self._last_project_cache = now_time\n\n        manager_cache_diff = now_time - self._last_manager_cache\n        if manager_cache_diff > self._max_manager_cache_time:\n            self._modules_manager = None\n\n        if self._modules_manager is None:\n            self._modules_manager = ModulesManager()\n            self._last_manager_cache = now_time\n\n        sync_server = self._modules_manager.modules_by_name.get(\"sync_server\")\n        if (\n            sync_server is not None\n            and sync_server.enabled\n            and sync_server.is_project_enabled(project_name, single=True)\n        ):\n            sync_server_enabled = True\n            active_site = sync_server.get_active_site(project_name)\n            active_provider = sync_server.get_provider_for_site(\n                project_name, active_site)\n            if active_site == 'studio':  # for studio use explicit icon\n                active_provider = 'studio'\n\n            remote_site = sync_server.get_remote_site(project_name)\n            remote_provider = sync_server.get_provider_for_site(\n                project_name, remote_site)\n            if remote_site == 'studio':  # for studio use explicit icon\n                remote_provider = 'studio'\n\n            repre_icons = lib.get_repre_icons()\n\n        self.repre_icons = repre_icons\n        self.sync_server = sync_server\n        self.sync_server_enabled = sync_server_enabled\n        self.active_site = active_site\n        self.active_provider = active_provider\n        self.remote_site = remote_site\n        self.remote_provider = remote_provider\n\n\nclass SubsetsModel(BaseRepresentationModel, TreeModel):\n    doc_fetched = QtCore.Signal()\n    refreshed = QtCore.Signal(bool)\n\n    Columns = [\n        \"subset\",\n        \"asset\",\n        \"family\",\n        \"version\",\n        \"time\",\n        \"author\",\n        \"frames\",\n        \"duration\",\n        \"handles\",\n        \"step\",\n        \"loaded_in_scene\",\n        \"repre_info\"\n    ]\n\n    column_labels_mapping = {\n        \"subset\": \"Product\" if AYON_SERVER_ENABLED else \"Subset\",\n        \"asset\": \"Folder\" if AYON_SERVER_ENABLED else \"Asset\",\n        \"family\": \"Product type\" if AYON_SERVER_ENABLED else \"Family\",\n        \"version\": \"Version\",\n        \"time\": \"Time\",\n        \"author\": \"Author\",\n        \"frames\": \"Frames\",\n        \"duration\": \"Duration\",\n        \"handles\": \"Handles\",\n        \"step\": \"Step\",\n        \"loaded_in_scene\": \"In scene\",\n        \"repre_info\": \"Availability\"\n    }\n\n    SortAscendingRole = QtCore.Qt.UserRole + 2\n    SortDescendingRole = QtCore.Qt.UserRole + 3\n    merged_subset_colors = [\n        (55, 161, 222),  # Light Blue\n        (231, 176, 0),  # Yellow\n        (154, 13, 255),  # Purple\n        (130, 184, 30),  # Light Green\n        (211, 79, 63),  # Light Red\n        (179, 181, 182),  # Grey\n        (194, 57, 179),  # Pink\n        (0, 120, 215),  # Dark Blue\n        (0, 204, 106),  # Dark Green\n        (247, 99, 12),  # Orange\n    ]\n    not_last_hero_brush = QtGui.QBrush(QtGui.QColor(254, 121, 121))\n\n    # Should be minimum of required asset document keys\n    asset_doc_projection = {\n        \"name\": 1,\n        \"label\": 1\n    }\n    # Should be minimum of required subset document keys\n    subset_doc_projection = {\n        \"name\": 1,\n        \"parent\": 1,\n        \"schema\": 1,\n        \"data.families\": 1,\n        \"data.subsetGroup\": 1\n    }\n\n    def __init__(\n        self,\n        dbcon,\n        groups_config,\n        family_config_cache,\n        grouping=True,\n        parent=None,\n        asset_doc_projection=None,\n        subset_doc_projection=None\n    ):\n        super(SubsetsModel, self).__init__(parent=parent)\n\n        self.dbcon = dbcon\n\n        # Projections for Mongo queries\n        # - let ability to modify them if used in tools that require more than\n        #   defaults\n        if asset_doc_projection:\n            self.asset_doc_projection = asset_doc_projection\n\n        if subset_doc_projection:\n            self.subset_doc_projection = subset_doc_projection\n\n        self.repre_icons = {}\n        self.sync_server = None\n        self.sync_server_enabled = False\n        self.active_site = self.active_provider = None\n\n        self.columns_index = dict(\n            (key, idx) for idx, key in enumerate(self.Columns)\n        )\n        self._asset_ids = None\n\n        self.groups_config = groups_config\n        self.family_config_cache = family_config_cache\n        self._sorter = None\n        self._grouping = grouping\n        self._icons = {\n            \"subset\": qtawesome.icon(\n                \"fa.file-o\",\n                color=get_default_entity_icon_color()\n            )\n        }\n        self._items_by_id = {}\n\n        self._doc_fetching_thread = None\n        self._doc_fetching_stop = False\n        self._doc_payload = {}\n\n        self._host = registered_host()\n        self._loaded_representation_ids = set()\n\n        # Refresh loaded scene containers only every 3 seconds at most\n        self._host_loaded_refresh_timeout = 3\n        self._host_loaded_refresh_time = 0\n\n        self.doc_fetched.connect(self._on_doc_fetched)\n        self.refresh()\n\n    def get_item_by_id(self, item_id):\n        return self._items_by_id.get(item_id)\n\n    def add_child(self, new_item, *args, **kwargs):\n        item_id = str(uuid4())\n        new_item[\"id\"] = item_id\n        self._items_by_id[item_id] = new_item\n        super(SubsetsModel, self).add_child(new_item, *args, **kwargs)\n\n    def set_assets(self, asset_ids):\n        self._asset_ids = asset_ids\n        self.refresh()\n\n    def set_grouping(self, state):\n        self._grouping = state\n        self._on_doc_fetched()\n\n    def get_subsets_families(self):\n        return self._doc_payload.get(\"subset_families\") or set()\n\n    def setData(self, index, value, role=QtCore.Qt.EditRole):\n        # Trigger additional edit when `version` column changed\n        # because it also updates the information in other columns\n        if index.column() == self.columns_index[\"version\"]:\n            item = index.internalPointer()\n            subset_id = item[\"_id\"]\n            if isinstance(value, HeroVersionType):\n                version_doc = self._get_hero_version(subset_id)\n\n            else:\n                project_name = self.dbcon.active_project()\n                version_doc = get_version_by_name(\n                    project_name, value, subset_id\n                )\n\n                # update availability on active site when version changes\n                if self.sync_server_enabled and version_doc:\n                    repres_info = list(\n                        self.sync_server.get_repre_info_for_versions(\n                            project_name,\n                            [version_doc[\"_id\"]],\n                            self.active_site,\n                            self.remote_site\n                        )\n                    )\n                    if repres_info:\n                        version_doc[\"data\"].update(\n                            self._get_repre_dict(repres_info[0]))\n\n            self.set_version(index, version_doc)\n\n        return super(SubsetsModel, self).setData(index, value, role)\n\n    def _get_hero_version(self, subset_id):\n        project_name = self.dbcon.active_project()\n        version_docs = get_versions(\n            project_name, subset_ids=[subset_id], hero=True\n        )\n        standard_versions = []\n        hero_version_doc = None\n        for version_doc in version_docs:\n            if version_doc[\"type\"] == \"hero_version\":\n                hero_version_doc = version_doc\n                continue\n            standard_versions.append(version_doc)\n\n        src_version_id = hero_version_doc[\"version_id\"]\n        src_version = None\n        is_from_latest = True\n        for version_doc in reversed(sorted(\n            standard_versions, key=lambda item: item[\"name\"]\n        )):\n            if version_doc[\"_id\"] == src_version_id:\n                src_version = version_doc\n                break\n            is_from_latest = False\n\n        hero_version_doc[\"data\"] = src_version[\"data\"]\n        hero_version_doc[\"name\"] = src_version[\"name\"]\n        hero_version_doc[\"is_from_latest\"] = is_from_latest\n        return hero_version_doc\n\n    def set_version(self, index, version):\n        \"\"\"Update the version data of the given index.\n\n        Arguments:\n            index (QtCore.QModelIndex): The model index.\n            version (dict) Version document in the database.\n\n        \"\"\"\n\n        assert isinstance(index, QtCore.QModelIndex)\n        if not index.isValid():\n            return\n\n        item = index.internalPointer()\n\n        assert version[\"parent\"] == item[\"_id\"], (\n            \"Version does not belong to subset\"\n        )\n\n        # Get the data from the version\n        version_data = version.get(\"data\", dict())\n\n        # Compute frame ranges (if data is present)\n        frame_start = version_data.get(\n            \"frameStart\",\n            # backwards compatibility\n            version_data.get(\"startFrame\", None)\n        )\n        frame_end = version_data.get(\n            \"frameEnd\",\n            # backwards compatibility\n            version_data.get(\"endFrame\", None)\n        )\n\n        handles_label = None\n        handle_start = version_data.get(\"handleStart\", None)\n        handle_end = version_data.get(\"handleEnd\", None)\n        if handle_start is not None and handle_end is not None:\n            handles_label = \"{}-{}\".format(str(handle_start), str(handle_end))\n\n        if frame_start is not None and frame_end is not None:\n            # Remove superfluous zeros from numbers (3.0 -> 3) to improve\n            # readability for most frame ranges\n            start_clean = (\"%f\" % frame_start).rstrip(\"0\").rstrip(\".\")\n            end_clean = (\"%f\" % frame_end).rstrip(\"0\").rstrip(\".\")\n            frames = \"{0}-{1}\".format(start_clean, end_clean)\n            duration = frame_end - frame_start + 1\n        else:\n            frames = None\n            duration = None\n\n        schema_maj_version, _ = schema.get_schema_version(item[\"schema\"])\n        if schema_maj_version < 3:\n            families = version_data.get(\"families\", [None])\n        else:\n            families = item[\"data\"][\"families\"]\n\n        family = None\n        if families:\n            family = families[0]\n\n        family_config = self.family_config_cache.family_config(family)\n\n        item.update({\n            \"version\": version[\"name\"],\n            \"version_document\": version,\n            \"author\": version_data.get(\"author\", None),\n            \"time\": version_data.get(\"time\", None),\n            \"family\": family,\n            \"familyLabel\": family_config.get(\"label\", family),\n            \"familyIcon\": family_config.get(\"icon\", None),\n            \"families\": set(families),\n            \"frameStart\": frame_start,\n            \"frameEnd\": frame_end,\n            \"duration\": duration,\n            \"handles\": handles_label,\n            \"frames\": frames,\n            \"step\": version_data.get(\"step\", None),\n        })\n\n        repre_info = version_data.get(\"repre_info\")\n        if repre_info:\n            item[\"repre_info\"] = repre_info\n\n    def _fetch(self):\n        project_name = self.dbcon.active_project()\n        asset_docs = get_assets(\n            project_name,\n            asset_ids=self._asset_ids,\n            fields=self.asset_doc_projection.keys()\n        )\n\n        asset_docs_by_id = {\n            asset_doc[\"_id\"]: asset_doc\n            for asset_doc in asset_docs\n        }\n\n        subset_docs_by_id = {}\n        subset_docs = get_subsets(\n            project_name,\n            asset_ids=self._asset_ids,\n            fields=self.subset_doc_projection.keys()\n        )\n\n        subset_families = set()\n        for subset_doc in subset_docs:\n            if self._doc_fetching_stop:\n                return\n\n            families = subset_doc.get(\"data\", {}).get(\"families\")\n            if families:\n                subset_families.add(families[0])\n\n            subset_docs_by_id[subset_doc[\"_id\"]] = subset_doc\n\n        subset_ids = list(subset_docs_by_id.keys())\n        last_versions_by_subset_id = get_last_versions(\n            project_name,\n            subset_ids,\n            active=True,\n            fields=[\"_id\", \"parent\", \"name\", \"type\", \"data\", \"schema\"]\n        )\n\n        hero_versions = get_hero_versions(project_name, subset_ids=subset_ids)\n        missing_versions = []\n        for hero_version in hero_versions:\n            version_id = hero_version[\"version_id\"]\n            if version_id not in last_versions_by_subset_id:\n                missing_versions.append(version_id)\n\n        missing_versions_by_id = {}\n        if missing_versions:\n            missing_version_docs = get_versions(\n                project_name, version_ids=missing_versions\n            )\n            missing_versions_by_id = {\n                missing_version_doc[\"_id\"]: missing_version_doc\n                for missing_version_doc in missing_version_docs\n            }\n\n        for hero_version in hero_versions:\n            version_id = hero_version[\"version_id\"]\n            subset_id = hero_version[\"parent\"]\n\n            version_doc = last_versions_by_subset_id.get(subset_id)\n            if version_doc is None:\n                version_doc = missing_versions_by_id.get(version_id)\n                if version_doc is None:\n                    continue\n\n            hero_version[\"data\"] = version_doc[\"data\"]\n            hero_version[\"name\"] = HeroVersionType(version_doc[\"name\"])\n            # Add information if hero version is from latest version\n            hero_version[\"is_from_latest\"] = version_id == version_doc[\"_id\"]\n\n            last_versions_by_subset_id[subset_id] = hero_version\n\n        # Check loaded subsets\n        loaded_subset_ids = set()\n        ids = self._loaded_representation_ids\n        if ids:\n            if self._doc_fetching_stop:\n                return\n\n            # Get subset ids from loaded representations in workfile\n            # todo: optimize with aggregation query to distinct subset id\n            representations = get_representations(project_name,\n                                                  representation_ids=ids,\n                                                  fields=[\"parent\"])\n            version_ids = set(repre[\"parent\"] for repre in representations)\n            versions = get_versions(project_name,\n                                    version_ids=version_ids,\n                                    fields=[\"parent\"])\n            loaded_subset_ids = set(version[\"parent\"] for version in versions)\n\n        if self._doc_fetching_stop:\n            return\n\n        repre_info_by_version_id = {}\n        if self.sync_server_enabled:\n            versions_by_id = {}\n            for _subset_id, doc in last_versions_by_subset_id.items():\n                versions_by_id[doc[\"_id\"]] = doc\n\n            repres_info = self.sync_server.get_repre_info_for_versions(\n                project_name,\n                list(versions_by_id.keys()),\n                self.active_site,\n                self.remote_site\n            )\n            for repre_info in repres_info:\n                if self._doc_fetching_stop:\n                    return\n\n                version_id = repre_info[\"_id\"]\n                doc = versions_by_id[version_id]\n                doc[\"active_provider\"] = self.active_provider\n                doc[\"remote_provider\"] = self.remote_provider\n                repre_info_by_version_id[version_id] = repre_info\n\n        self._doc_payload = {\n            \"asset_docs_by_id\": asset_docs_by_id,\n            \"subset_docs_by_id\": subset_docs_by_id,\n            \"subset_families\": subset_families,\n            \"last_versions_by_subset_id\": last_versions_by_subset_id,\n            \"repre_info_by_version_id\": repre_info_by_version_id,\n            \"subsets_loaded_by_id\": loaded_subset_ids\n        }\n\n        self.doc_fetched.emit()\n\n    def fetch_subset_and_version(self):\n        \"\"\"Query all subsets and latest versions from aggregation\n        (NOTE) The returned version documents are NOT the real version\n            document, it's generated from the MongoDB's aggregation so\n            some of the first level field may not be presented.\n        \"\"\"\n        self._doc_payload = {}\n        self._doc_fetching_stop = False\n        self._doc_fetching_thread = lib.create_qthread(self._fetch)\n        self._doc_fetching_thread.start()\n\n    def stop_fetch_thread(self):\n        if self._doc_fetching_thread is not None:\n            self._doc_fetching_stop = True\n            while self._doc_fetching_thread.isRunning():\n                pass\n\n    def refresh(self):\n        self.stop_fetch_thread()\n        self.clear()\n        self._items_by_id = {}\n        self.reset_sync_server()\n\n        if not self._asset_ids:\n            self.doc_fetched.emit()\n            return\n\n        # Collect scene container representations to compare loaded state\n        # This runs in the main thread because it involves the host DCC\n        if self._host:\n            time_since_refresh = time.time() - self._host_loaded_refresh_time\n            if time_since_refresh > self._host_loaded_refresh_timeout:\n                if isinstance(self._host, ILoadHost):\n                    containers = self._host.get_containers()\n                else:\n                    containers = self._host.ls()\n\n                repre_ids = {con.get(\"representation\") for con in containers}\n                self._loaded_representation_ids = repre_ids\n                self._host_loaded_refresh_time = time.time()\n\n        self.fetch_subset_and_version()\n\n    def _on_doc_fetched(self):\n        self.clear()\n        self._items_by_id = {}\n        self.beginResetModel()\n\n        asset_docs_by_id = self._doc_payload.get(\n            \"asset_docs_by_id\"\n        )\n        subset_docs_by_id = self._doc_payload.get(\n            \"subset_docs_by_id\"\n        )\n        last_versions_by_subset_id = self._doc_payload.get(\n            \"last_versions_by_subset_id\"\n        )\n\n        repre_info_by_version_id = self._doc_payload.get(\n            \"repre_info_by_version_id\"\n        )\n\n        subsets_loaded_by_id = self._doc_payload.get(\n            \"subsets_loaded_by_id\"\n        )\n\n        if (\n            asset_docs_by_id is None\n            or subset_docs_by_id is None\n            or last_versions_by_subset_id is None\n            or len(self._asset_ids) == 0\n        ):\n            self.endResetModel()\n            self.refreshed.emit(False)\n            return\n\n        self._fill_subset_items(\n            asset_docs_by_id,\n            subset_docs_by_id,\n            last_versions_by_subset_id,\n            repre_info_by_version_id,\n            subsets_loaded_by_id\n        )\n        self.endResetModel()\n        self.refreshed.emit(True)\n\n    def create_multiasset_group(\n        self, subset_name, asset_ids, subset_counter, parent_item=None\n    ):\n        subset_color = self.merged_subset_colors[\n            subset_counter % len(self.merged_subset_colors)\n        ]\n        merge_group = Item()\n        merge_group.update({\n            \"subset\": \"{} ({})\".format(subset_name, len(asset_ids)),\n            \"isMerged\": True,\n            \"subsetColor\": subset_color,\n            \"assetIds\": list(asset_ids),\n            \"icon\": qtawesome.icon(\n                \"fa.circle\",\n                color=\"#{0:02x}{1:02x}{2:02x}\".format(*subset_color)\n            )\n        })\n\n        self.add_child(merge_group, parent_item)\n\n        return merge_group\n\n    def _fill_subset_items(\n        self,\n        asset_docs_by_id,\n        subset_docs_by_id,\n        last_versions_by_subset_id,\n        repre_info_by_version_id,\n        subsets_loaded_by_id\n    ):\n        _groups_tuple = self.groups_config.split_subsets_for_groups(\n            subset_docs_by_id.values(), self._grouping\n        )\n        groups, subset_docs_without_group, subset_docs_by_group = _groups_tuple\n\n        group_item_by_name = {}\n        for group_data in groups:\n            group_name = group_data[\"name\"]\n            group_item = Item()\n            group_item.update({\n                \"subset\": group_name,\n                \"isGroup\": True\n            })\n            group_item.update(group_data)\n\n            self.add_child(group_item)\n\n            group_item_by_name[group_name] = {\n                \"item\": group_item,\n                \"index\": self.index(group_item.row(), 0)\n            }\n\n        def _add_subset_item(subset_doc, parent_item, parent_index):\n            last_version = last_versions_by_subset_id.get(\n                subset_doc[\"_id\"]\n            )\n            # do not show subset without version\n            if not last_version:\n                return\n\n            data = copy.deepcopy(subset_doc)\n            data[\"subset\"] = subset_doc[\"name\"]\n\n            asset_id = subset_doc[\"parent\"]\n            data[\"asset\"] = asset_docs_by_id[asset_id][\"name\"]\n\n            data[\"last_version\"] = last_version\n            data[\"loaded_in_scene\"] = subset_doc[\"_id\"] in subsets_loaded_by_id\n\n            # Sync server data\n            data.update(\n                self._get_last_repre_info(repre_info_by_version_id,\n                                          last_version[\"_id\"]))\n\n            item = Item()\n            item.update(data)\n            self.add_child(item, parent_item)\n\n            index = self.index(item.row(), 0, parent_index)\n            self.set_version(index, last_version)\n\n        subset_counter = 0\n        for group_name, subset_docs_by_name in subset_docs_by_group.items():\n            parent_item = group_item_by_name[group_name][\"item\"]\n            parent_index = group_item_by_name[group_name][\"index\"]\n            for subset_name in sorted(subset_docs_by_name.keys()):\n                subset_docs = subset_docs_by_name[subset_name]\n                asset_ids = [\n                    subset_doc[\"parent\"] for subset_doc in subset_docs\n                ]\n                if len(subset_docs) > 1:\n                    _parent_item = self.create_multiasset_group(\n                        subset_name, asset_ids, subset_counter, parent_item\n                    )\n                    _parent_index = self.index(\n                        _parent_item.row(), 0, parent_index\n                    )\n                    subset_counter += 1\n                else:\n                    _parent_item = parent_item\n                    _parent_index = parent_index\n\n                for subset_doc in subset_docs:\n                    _add_subset_item(subset_doc,\n                                     parent_item=_parent_item,\n                                     parent_index=_parent_index)\n\n        for subset_name in sorted(subset_docs_without_group.keys()):\n            subset_docs = subset_docs_without_group[subset_name]\n            asset_ids = [subset_doc[\"parent\"] for subset_doc in subset_docs]\n            parent_item = None\n            parent_index = None\n            if len(subset_docs) > 1:\n                parent_item = self.create_multiasset_group(\n                    subset_name, asset_ids, subset_counter\n                )\n                parent_index = self.index(parent_item.row(), 0)\n                subset_counter += 1\n\n            for subset_doc in subset_docs:\n                _add_subset_item(subset_doc,\n                                 parent_item=parent_item,\n                                 parent_index=parent_index)\n\n    def data(self, index, role):\n        if not index.isValid():\n            return\n\n        item = index.internalPointer()\n        if role == ITEM_ID_ROLE:\n            return item[\"id\"]\n\n        if role == self.SortDescendingRole:\n            if item.get(\"isGroup\"):\n                # Ensure groups be on top when sorting by descending order\n                prefix = \"2\"\n                order = item[\"order\"]\n            else:\n                if item.get(\"isMerged\"):\n                    prefix = \"1\"\n                else:\n                    prefix = \"0\"\n                order = str(super(SubsetsModel, self).data(\n                    index, QtCore.Qt.DisplayRole\n                ))\n            return prefix + order\n\n        if role == self.SortAscendingRole:\n            if item.get(\"isGroup\"):\n                # Ensure groups be on top when sorting by ascending order\n                prefix = \"0\"\n                order = item[\"order\"]\n            else:\n                if item.get(\"isMerged\"):\n                    prefix = \"1\"\n                else:\n                    prefix = \"2\"\n                order = str(super(SubsetsModel, self).data(\n                    index, QtCore.Qt.DisplayRole\n                ))\n            return prefix + order\n\n        if role == QtCore.Qt.DisplayRole:\n            if index.column() == self.columns_index[\"family\"]:\n                # Show familyLabel instead of family\n                return item.get(\"familyLabel\", None)\n\n        elif role == QtCore.Qt.DecorationRole:\n\n            # Add icon to subset column\n            if index.column() == self.columns_index[\"subset\"]:\n                if item.get(\"isGroup\") or item.get(\"isMerged\"):\n                    return item[\"icon\"]\n                else:\n                    return self._icons[\"subset\"]\n\n            # Add icon to family column\n            if index.column() == self.columns_index[\"family\"]:\n                return item.get(\"familyIcon\", None)\n\n        elif role == QtCore.Qt.ForegroundRole:\n            version_doc = item.get(\"version_document\")\n            if version_doc and version_doc.get(\"type\") == \"hero_version\":\n                if not version_doc[\"is_from_latest\"]:\n                    return self.not_last_hero_brush\n\n        elif role == LOCAL_AVAILABILITY_ROLE:\n            if not item.get(\"isGroup\"):\n                return item.get(\"repre_info_local\")\n            else:\n                return None\n\n        elif role == REMOTE_AVAILABILITY_ROLE:\n            if not item.get(\"isGroup\"):\n                return item.get(\"repre_info_remote\")\n            else:\n                return None\n\n        elif role == LOCAL_PROVIDER_ROLE:\n            return self.active_provider\n\n        elif role == REMOTE_PROVIDER_ROLE:\n            return self.remote_provider\n\n        return super(SubsetsModel, self).data(index, role)\n\n    def flags(self, index):\n        flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable\n\n        # Make the version column editable\n        if index.column() == self.columns_index[\"version\"]:\n            flags |= QtCore.Qt.ItemIsEditable\n\n        return flags\n\n    def headerData(self, section, orientation, role):\n        \"\"\"Remap column names to labels\"\"\"\n        if role == QtCore.Qt.DisplayRole:\n            if section < len(self.Columns):\n                key = self.Columns[section]\n                return self.column_labels_mapping.get(key) or key\n\n        super(TreeModel, self).headerData(section, orientation, role)\n\n    def _get_last_repre_info(self, repre_info_by_version_id, last_version_id):\n        data = {}\n        if repre_info_by_version_id:\n            repre_info = repre_info_by_version_id.get(last_version_id)\n            return self._get_repre_dict(repre_info)\n\n        return data\n\n    def _get_repre_dict(self, repre_info):\n        \"\"\"Returns str representation of availability\"\"\"\n        data = {}\n        if repre_info:\n            repres_str = \"{}/{}\".format(\n                int(math.floor(float(repre_info['avail_repre_local']))),\n                int(math.floor(float(repre_info['repre_count']))))\n\n            data[\"repre_info_local\"] = repres_str\n\n            repres_str = \"{}/{}\".format(\n                int(math.floor(float(repre_info['avail_repre_remote']))),\n                int(math.floor(float(repre_info['repre_count']))))\n\n            data[\"repre_info_remote\"] = repres_str\n\n        return data\n\n\nclass GroupMemberFilterProxyModel(QtCore.QSortFilterProxyModel):\n    \"\"\"Provide the feature of filtering group by the acceptance of members\n\n    The subset group nodes will not be filtered directly, the group node's\n    acceptance depends on it's child subsets' acceptance.\n\n    \"\"\"\n\n    if is_filtering_recursible():\n        def _is_group_acceptable(self, index, node):\n            # (NOTE) With the help of `RecursiveFiltering` feature from\n            #        Qt 5.10, group always not be accepted by default.\n            return False\n        filter_accepts_group = _is_group_acceptable\n\n    else:\n        # Patch future function\n        setRecursiveFilteringEnabled = (lambda *args: None)\n\n        def _is_group_acceptable(self, index, model):\n            # (NOTE) This is not recursive.\n            for child_row in range(model.rowCount(index)):\n                if self.filterAcceptsRow(child_row, index):\n                    return True\n            return False\n        filter_accepts_group = _is_group_acceptable\n\n    def __init__(self, *args, **kwargs):\n        super(GroupMemberFilterProxyModel, self).__init__(*args, **kwargs)\n        self.setRecursiveFilteringEnabled(True)\n\n\nclass SubsetFilterProxyModel(GroupMemberFilterProxyModel):\n    def filterAcceptsRow(self, row, parent):\n        model = self.sourceModel()\n        index = model.index(row, self.filterKeyColumn(), parent)\n        item = index.internalPointer()\n        if item.get(\"isGroup\"):\n            return self.filter_accepts_group(index, model)\n        return super(\n            SubsetFilterProxyModel, self\n        ).filterAcceptsRow(row, parent)\n\n\nclass FamiliesFilterProxyModel(GroupMemberFilterProxyModel):\n    \"\"\"Filters to specified families\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(FamiliesFilterProxyModel, self).__init__(*args, **kwargs)\n        self._families = set()\n\n    def familyFilter(self):\n        return self._families\n\n    def setFamiliesFilter(self, values):\n        \"\"\"Set the families to include\"\"\"\n        assert isinstance(values, (tuple, list, set))\n        self._families = set(values)\n        self.invalidateFilter()\n\n    def filterAcceptsRow(self, row=0, parent=None):\n        if not self._families:\n            return False\n\n        model = self.sourceModel()\n        index = model.index(row, 0, parent=parent or QtCore.QModelIndex())\n\n        # Ensure index is valid\n        if not index.isValid() or index is None:\n            return True\n\n        # Get the item data and validate\n        item = model.data(index, TreeModel.ItemRole)\n\n        if item.get(\"isGroup\"):\n            return self.filter_accepts_group(index, model)\n\n        family = item.get(\"family\")\n        if not family:\n            return True\n\n        # We want to keep the families which are not in the list\n        return family in self._families\n\n    def sort(self, column, order):\n        proxy = self.sourceModel()\n        model = proxy.sourceModel()\n        # We need to know the sorting direction for pinning groups on top\n        if order == QtCore.Qt.AscendingOrder:\n            self.setSortRole(model.SortAscendingRole)\n        else:\n            self.setSortRole(model.SortDescendingRole)\n\n        super(FamiliesFilterProxyModel, self).sort(column, order)\n\n\nclass RepresentationSortProxyModel(GroupMemberFilterProxyModel):\n    \"\"\"To properly sort progress string\"\"\"\n    def lessThan(self, left, right):\n        source_model = self.sourceModel()\n        progress_indexes = [source_model.Columns.index(\"active_site\"),\n                            source_model.Columns.index(\"remote_site\")]\n        if left.column() in progress_indexes:\n            left_data = self.sourceModel().data(left, QtCore.Qt.DisplayRole)\n            right_data = self.sourceModel().data(right, QtCore.Qt.DisplayRole)\n            left_val = re.sub(\"[^0-9]\", '', left_data)\n            right_val = re.sub(\"[^0-9]\", '', right_data)\n\n            return int(left_val) < int(right_val)\n\n        return super(RepresentationSortProxyModel, self).lessThan(left, right)\n\n\nclass RepresentationModel(TreeModel, BaseRepresentationModel):\n    doc_fetched = QtCore.Signal()\n    refreshed = QtCore.Signal(bool)\n\n    SiteNameRole = QtCore.Qt.UserRole + 2\n    ProgressRole = QtCore.Qt.UserRole + 3\n    SiteSideRole = QtCore.Qt.UserRole + 4\n    IdRole = QtCore.Qt.UserRole + 5\n    ContextRole = QtCore.Qt.UserRole + 6\n\n    Columns = [\n        \"name\",\n        \"subset\",\n        \"asset\",\n        \"active_site\",\n        \"remote_site\"\n    ]\n\n    column_labels_mapping = {\n        \"name\": \"Name\",\n        \"subset\": \"Subset\",\n        \"asset\": \"Asset\",\n        \"active_site\": \"Active\",\n        \"remote_site\": \"Remote\"\n    }\n\n    repre_projection = {\n        \"_id\": 1,\n        \"name\": 1,\n        \"context.subset\": 1,\n        \"context.asset\": 1,\n        \"context.version\": 1,\n        \"context.representation\": 1,\n        'files.sites': 1\n    }\n\n    def __init__(self, dbcon, header):\n        super(RepresentationModel, self).__init__()\n        self.dbcon = dbcon\n        self._data = []\n        self._header = header\n        self._version_ids = []\n\n        manager = ModulesManager()\n        active_site = remote_site = None\n        active_provider = remote_provider = None\n        sync_server = manager.modules_by_name.get(\"sync_server\")\n        sync_server_enabled = (\n            sync_server is not None\n            and sync_server.enabled\n        )\n\n        project_name = dbcon.current_project()\n        if sync_server_enabled and project_name:\n            active_site = sync_server.get_active_site(project_name)\n            remote_site = sync_server.get_remote_site(project_name)\n\n            # TODO refactor\n            active_provider = sync_server.get_provider_for_site(\n                project_name, active_site\n            )\n            if active_site == 'studio':\n                active_provider = 'studio'\n\n            remote_provider = sync_server.get_provider_for_site(\n                project_name, remote_site\n            )\n\n            if remote_site == 'studio':\n                remote_provider = 'studio'\n\n        self.sync_server = sync_server\n        self.sync_server_enabled = sync_server_enabled\n        self.active_site = active_site\n        self.active_provider = active_provider\n        self.remote_site = remote_site\n        self.remote_provider = remote_provider\n\n        self.doc_fetched.connect(self._on_doc_fetched)\n\n        self._docs = {}\n        self._icons = lib.get_repre_icons()\n        self._icons[\"repre\"] = qtawesome.icon(\n            \"fa.file-o\",\n            color=get_default_entity_icon_color()\n        )\n        self._items_by_id = {}\n\n    def set_version_ids(self, version_ids):\n        self._version_ids = version_ids\n        self.refresh()\n\n    def data(self, index, role):\n        item = index.internalPointer()\n\n        if role == ITEM_ID_ROLE:\n            return item[\"id\"]\n\n        if role == self.IdRole:\n            return item.get(\"_id\")\n\n        if role == QtCore.Qt.DecorationRole:\n            # Add icon to subset column\n            if index.column() == self.Columns.index(\"name\"):\n                if item.get(\"isMerged\"):\n                    return item[\"icon\"]\n                return self._icons[\"repre\"]\n\n        active_index = self.Columns.index(\"active_site\")\n        remote_index = self.Columns.index(\"remote_site\")\n        if role == QtCore.Qt.DisplayRole:\n            progress = None\n            label = ''\n            if index.column() == active_index:\n                progress = item.get(\"active_site_progress\", 0)\n            elif index.column() == remote_index:\n                progress = item.get(\"remote_site_progress\", 0)\n\n            if progress is not None:\n                # site added, sync in progress\n                progress_str = \"not avail.\"\n                if progress >= 0:\n                    if progress == 0 and item.get(\"isMerged\"):\n                        progress_str = \"not avail.\"\n                    else:\n                        progress_str = \"{}% {}\".format(\n                            int(progress * 100), label\n                        )\n\n                return progress_str\n\n        if role == QtCore.Qt.DecorationRole:\n            if index.column() == active_index:\n                return item.get(\"active_site_icon\", None)\n            if index.column() == remote_index:\n                return item.get(\"remote_site_icon\", None)\n\n        if role == self.SiteNameRole:\n            if index.column() == active_index:\n                return item.get(\"active_site_name\", None)\n            if index.column() == remote_index:\n                return item.get(\"remote_site_name\", None)\n\n        if role == self.SiteSideRole:\n            if index.column() == active_index:\n                return \"active\"\n            if index.column() == remote_index:\n                return \"remote\"\n\n        if role == self.ProgressRole:\n            if index.column() == active_index:\n                return item.get(\"active_site_progress\", 0)\n            if index.column() == remote_index:\n                return item.get(\"remote_site_progress\", 0)\n\n        return super(RepresentationModel, self).data(index, role)\n\n    def _on_doc_fetched(self):\n        self.clear()\n        self.beginResetModel()\n        subsets = set()\n        assets = set()\n        repre_groups = {}\n        repre_groups_items = {}\n        group = None\n        self._items_by_id = {}\n        for doc in self._docs:\n            if len(self._version_ids) > 1:\n                group = repre_groups.get(doc[\"name\"])\n                if not group:\n                    group_item = Item()\n                    item_id = str(uuid4())\n                    group_item.update({\n                        \"id\": item_id,\n                        \"_id\": doc[\"_id\"],\n                        \"name\": doc[\"name\"],\n                        \"isMerged\": True,\n                        \"active_site_name\": self.active_site,\n                        \"remote_site_name\": self.remote_site,\n                        \"icon\": qtawesome.icon(\n                            \"fa.folder\",\n                            color=get_default_entity_icon_color()\n                        )\n                    })\n                    self._items_by_id[item_id] = group_item\n                    self.add_child(group_item, None)\n                    repre_groups[doc[\"name\"]] = group_item\n                    repre_groups_items[doc[\"name\"]] = 0\n                    group = group_item\n\n            progress = {\n                self.active_site: 0,\n                self.remote_site: 0,\n            }\n            if self.sync_server_enabled:\n                progress = self.sync_server.get_progress_for_repre(\n                    doc,\n                    self.active_site,\n                    self.remote_site)\n\n            active_site_icon = self._icons.get(self.active_provider)\n            remote_site_icon = self._icons.get(self.remote_provider)\n\n            item_id = str(uuid4())\n            data = {\n                \"id\": item_id,\n                \"_id\": doc[\"_id\"],\n                \"name\": doc[\"name\"],\n                \"subset\": doc[\"context\"][\"subset\"],\n                \"asset\": doc[\"context\"][\"asset\"],\n                \"isMerged\": False,\n\n                \"active_site_icon\": active_site_icon,\n                \"remote_site_icon\": remote_site_icon,\n                \"active_site_name\": self.active_site,\n                \"remote_site_name\": self.remote_site,\n                \"active_site_progress\": progress[self.active_site],\n                \"remote_site_progress\": progress[self.remote_site]\n            }\n            subsets.add(doc[\"context\"][\"subset\"])\n            assets.add(doc[\"context\"][\"subset\"])\n\n            item = Item()\n            item.update(data)\n            self._items_by_id[item_id] = item\n\n            current_progress = {\n                'active_site_progress': progress[self.active_site],\n                'remote_site_progress': progress[self.remote_site]\n            }\n            if group:\n                group = self._sum_group_progress(\n                    doc[\"name\"], group, current_progress, repre_groups_items\n                )\n\n            self.add_child(item, group)\n\n        # finalize group average progress\n        for group_name, group in repre_groups.items():\n            items_cnt = repre_groups_items[group_name]\n            active_progress = group.get(\"active_site_progress\", 0)\n            group[\"active_site_progress\"] = active_progress / items_cnt\n            remote_progress = group.get(\"remote_site_progress\", 0)\n            group[\"remote_site_progress\"] = remote_progress / items_cnt\n\n        self.endResetModel()\n        self.refreshed.emit(False)\n\n    def get_item_by_id(self, item_id):\n        return self._items_by_id.get(item_id)\n\n    def refresh(self):\n        project_name = self.dbcon.current_project()\n        if not project_name:\n            return\n\n        repre_docs = []\n        if self._version_ids:\n            # Simple find here for now, expected to receive lower number of\n            # representations and logic could be in Python\n            repre_docs = list(get_representations(\n                project_name,\n                version_ids=self._version_ids,\n                fields=self.repre_projection.keys()\n            ))\n\n        self._docs = repre_docs\n\n        self.doc_fetched.emit()\n\n    def _sum_group_progress(\n        self, repre_name, group, current_item_progress, repre_groups_items\n    ):\n        \"\"\"Update final group progress\n\n        Called after every item in group is added\n\n        Args:\n            repre_name(string)\n            group(dict): info about group of selected items\n            current_item_progress(dict): {'active_site_progress': XX,\n                                          'remote_site_progress': YY}\n            repre_groups_items(dict)\n        Returns:\n            (dict): updated group info\n        \"\"\"\n        repre_groups_items[repre_name] += 1\n\n        for key, progress in current_item_progress.items():\n            group[key] = (group.get(key, 0) + max(progress, 0))\n\n        return group\n"
  },
  {
    "path": "openpype/tools/loader/widgets.py",
    "content": "import os\nimport sys\nimport datetime\nimport pprint\nimport traceback\nimport collections\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.client import (\n    get_subset_families,\n    get_subset_by_id,\n    get_subsets,\n    get_version_by_id,\n    get_versions,\n    get_representations,\n    get_thumbnail_id_from_source,\n    get_thumbnail,\n)\nfrom openpype.client.operations import OperationsSession, REMOVED_VALUE\nfrom openpype.pipeline import HeroVersionType, Anatomy\nfrom openpype.pipeline.thumbnail import get_thumbnail_binary\nfrom openpype.pipeline.load import (\n    discover_loader_plugins,\n    SubsetLoaderPlugin,\n    loaders_from_repre_context,\n    get_repres_contexts,\n    get_subset_contexts,\n    load_with_repre_context,\n    load_with_subset_context,\n    load_with_subset_contexts,\n    LoadError,\n    IncompatibleLoaderError,\n)\nfrom openpype.tools.utils import (\n    ErrorMessageBox,\n    lib as tools_lib\n)\nfrom openpype.tools.utils.lib import checkstate_int_to_enum\nfrom openpype.tools.utils.delegates import (\n    VersionDelegate,\n    PrettyTimeDelegate\n)\nfrom openpype.tools.utils.widgets import (\n    OptionalMenu,\n    PlaceholderLineEdit\n)\nfrom openpype.tools.utils.views import (\n    TreeViewSpinner,\n    DeselectableTreeView\n)\nfrom openpype.tools.utils.constants import (\n    LOCAL_PROVIDER_ROLE,\n    REMOTE_PROVIDER_ROLE,\n    LOCAL_AVAILABILITY_ROLE,\n    REMOTE_AVAILABILITY_ROLE,\n)\nfrom openpype.tools.assetlinks.widgets import SimpleLinkView\n\nfrom .model import (\n    SubsetsModel,\n    SubsetFilterProxyModel,\n    FamiliesFilterProxyModel,\n    RepresentationModel,\n    RepresentationSortProxyModel,\n    ITEM_ID_ROLE\n)\nfrom . import lib\nfrom .delegates import LoadedInSceneDelegate\n\n\nclass OverlayFrame(QtWidgets.QFrame):\n    def __init__(self, label, parent):\n        super(OverlayFrame, self).__init__(parent)\n\n        label_widget = QtWidgets.QLabel(label, self)\n        label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter)\n\n        self.label_widget = label_widget\n\n        self.setStyleSheet((\n            \"background: rgba(0, 0, 0, 127);\"\n            \"font-size: 60pt;\"\n        ))\n\n    def set_label(self, label):\n        self.label_widget.setText(label)\n\n\nclass LoadErrorMessageBox(ErrorMessageBox):\n    def __init__(self, messages, parent=None):\n        self._messages = messages\n        super(LoadErrorMessageBox, self).__init__(\"Loading failed\", parent)\n\n    def _create_top_widget(self, parent_widget):\n        label_widget = QtWidgets.QLabel(parent_widget)\n        label_widget.setText(\n            \"<span style='font-size:18pt;'>Failed to load items</span>\"\n        )\n        return label_widget\n\n    def _get_report_data(self):\n        report_data = []\n        for exc_msg, tb_text, repre, subset, version in self._messages:\n            report_message = (\n                \"During load error happened on Subset: \\\"{subset}\\\"\"\n                \" Representation: \\\"{repre}\\\" Version: {version}\"\n                \"\\n\\nError message: {message}\"\n            ).format(\n                subset=subset,\n                repre=repre,\n                version=version,\n                message=exc_msg\n            )\n            if tb_text:\n                report_message += \"\\n\\n{}\".format(tb_text)\n            report_data.append(report_message)\n        return report_data\n\n    def _create_content(self, content_layout):\n        item_name_template = (\n            \"<span style='font-weight:bold;'>Subset:</span> {}<br>\"\n            \"<span style='font-weight:bold;'>Version:</span> {}<br>\"\n            \"<span style='font-weight:bold;'>Representation:</span> {}<br>\"\n        )\n        exc_msg_template = \"<span style='font-weight:bold'>{}</span>\"\n\n        for exc_msg, tb_text, repre, subset, version in self._messages:\n            line = self._create_line()\n            content_layout.addWidget(line)\n\n            item_name = item_name_template.format(subset, version, repre)\n            item_name_widget = QtWidgets.QLabel(\n                item_name.replace(\"\\n\", \"<br>\"), self\n            )\n            item_name_widget.setWordWrap(True)\n            content_layout.addWidget(item_name_widget)\n\n            exc_msg = exc_msg_template.format(exc_msg.replace(\"\\n\", \"<br>\"))\n            message_label_widget = QtWidgets.QLabel(exc_msg, self)\n            message_label_widget.setWordWrap(True)\n            content_layout.addWidget(message_label_widget)\n\n            if tb_text:\n                line = self._create_line()\n                tb_widget = self._create_traceback_widget(tb_text, self)\n                content_layout.addWidget(line)\n                content_layout.addWidget(tb_widget)\n\n\nclass SubsetWidget(QtWidgets.QWidget):\n    \"\"\"A widget that lists the published subsets for an asset\"\"\"\n\n    active_changed = QtCore.Signal()    # active index changed\n    version_changed = QtCore.Signal()   # version state changed for a subset\n    load_started = QtCore.Signal()\n    load_ended = QtCore.Signal()\n    refreshed = QtCore.Signal(bool)\n\n    default_widths = (\n        (\"subset\", 200),\n        (\"asset\", 130),\n        (\"family\", 90),\n        (\"version\", 60),\n        (\"time\", 125),\n        (\"author\", 75),\n        (\"frames\", 75),\n        (\"duration\", 60),\n        (\"handles\", 55),\n        (\"step\", 10),\n        (\"loaded_in_scene\", 25),\n        (\"repre_info\", 65)\n    )\n\n    def __init__(\n        self,\n        dbcon,\n        groups_config,\n        family_config_cache,\n        enable_grouping=True,\n        tool_name=None,\n        parent=None\n    ):\n        super(SubsetWidget, self).__init__(parent=parent)\n\n        self.dbcon = dbcon\n        self.tool_name = tool_name\n\n        model = SubsetsModel(\n            dbcon,\n            groups_config,\n            family_config_cache,\n            grouping=enable_grouping\n        )\n        proxy = SubsetFilterProxyModel()\n        proxy.setSourceModel(model)\n        proxy.setDynamicSortFilter(True)\n        proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        family_proxy = FamiliesFilterProxyModel()\n        family_proxy.setSourceModel(proxy)\n\n        subset_filter = PlaceholderLineEdit(self)\n        subset_filter.setPlaceholderText(\"Filter subsets..\")\n\n        group_checkbox = QtWidgets.QCheckBox(\"Enable Grouping\", self)\n        group_checkbox.setChecked(enable_grouping)\n\n        top_bar_layout = QtWidgets.QHBoxLayout()\n        top_bar_layout.addWidget(subset_filter)\n        top_bar_layout.addWidget(group_checkbox)\n\n        view = TreeViewSpinner(self)\n        view.setModel(family_proxy)\n        view.setObjectName(\"SubsetView\")\n        view.setIndentation(20)\n        view.setAllColumnsShowFocus(True)\n        view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)\n        view.setSortingEnabled(True)\n        view.sortByColumn(1, QtCore.Qt.AscendingOrder)\n        view.setAlternatingRowColors(True)\n\n        # Set view delegates\n        version_delegate = VersionDelegate(self.dbcon, view)\n        column = model.Columns.index(\"version\")\n        view.setItemDelegateForColumn(column, version_delegate)\n\n        time_delegate = PrettyTimeDelegate(view)\n        column = model.Columns.index(\"time\")\n        view.setItemDelegateForColumn(column, time_delegate)\n\n        avail_delegate = AvailabilityDelegate(self.dbcon, view)\n        column = model.Columns.index(\"repre_info\")\n        view.setItemDelegateForColumn(column, avail_delegate)\n\n        loaded_in_scene_delegate = LoadedInSceneDelegate(view)\n        column = model.Columns.index(\"loaded_in_scene\")\n        view.setItemDelegateForColumn(column, loaded_in_scene_delegate)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addLayout(top_bar_layout)\n        layout.addWidget(view)\n\n        # settings and connections\n        for column_name, width in self.default_widths:\n            idx = model.Columns.index(column_name)\n            view.setColumnWidth(idx, width)\n\n        self.model = model\n        self.view = view\n\n        self.on_project_change(dbcon.current_project())\n\n        view.customContextMenuRequested.connect(self.on_context_menu)\n\n        selection = view.selectionModel()\n        selection.selectionChanged.connect(self.active_changed)\n\n        version_delegate.version_changed.connect(self.version_changed)\n\n        group_checkbox.stateChanged.connect(self.set_grouping)\n\n        subset_filter.textChanged.connect(self._subset_changed)\n\n        model.refreshed.connect(self.refreshed)\n\n        self.proxy = proxy\n        self.family_proxy = family_proxy\n\n        self._subset_filter = subset_filter\n        self._group_checkbox = group_checkbox\n\n        self._version_delegate = version_delegate\n        self._time_delegate = time_delegate\n\n        self.model.refresh()\n\n    def get_subsets_families(self):\n        return self.model.get_subsets_families()\n\n    def set_family_filters(self, families):\n        self.family_proxy.setFamiliesFilter(families)\n\n    def is_groupable(self):\n        return self._group_checkbox.isChecked()\n\n    def set_grouping(self, state):\n        with tools_lib.preserve_selection(tree_view=self.view,\n                                          current_index=False):\n            self.model.set_grouping(state)\n\n    def _subset_changed(self, text):\n        if hasattr(self.proxy, \"setFilterRegExp\"):\n            self.proxy.setFilterRegExp(text)\n        else:\n            self.proxy.setFilterRegularExpression(text)\n        self.view.expandAll()\n\n    def set_loading_state(self, loading, empty):\n        view = self.view\n\n        if view.is_loading != loading:\n            if loading:\n                view.spinner.repaintNeeded.connect(view.viewport().update)\n            else:\n                view.spinner.repaintNeeded.disconnect()\n\n        view.is_loading = loading\n        view.is_empty = empty\n\n    def _repre_contexts_for_loaders_filter(self, items):\n        version_docs_by_id = {\n            item[\"version_document\"][\"_id\"]: item[\"version_document\"]\n            for item in items\n        }\n        version_docs_by_subset_id = collections.defaultdict(list)\n        for item in items:\n            subset_id = item[\"version_document\"][\"parent\"]\n            version_docs_by_subset_id[subset_id].append(\n                item[\"version_document\"]\n            )\n\n        project_name = self.dbcon.active_project()\n        subset_docs = list(get_subsets(\n            project_name,\n            subset_ids=version_docs_by_subset_id.keys(),\n            fields=[\"schema\", \"data.families\"]\n        ))\n        subset_docs_by_id = {\n            subset_doc[\"_id\"]: subset_doc\n            for subset_doc in subset_docs\n        }\n        version_ids = list(version_docs_by_id.keys())\n        repre_docs = get_representations(\n            project_name,\n            version_ids=version_ids,\n            fields=[\"name\", \"parent\", \"data\", \"context\"]\n        )\n\n        repre_docs_by_version_id = {\n            version_id: []\n            for version_id in version_ids\n        }\n        repre_context_by_id = {}\n        for repre_doc in repre_docs:\n            version_id = repre_doc[\"parent\"]\n            repre_docs_by_version_id[version_id].append(repre_doc)\n\n            version_doc = version_docs_by_id[version_id]\n            repre_context_by_id[repre_doc[\"_id\"]] = {\n                \"representation\": repre_doc,\n                \"version\": version_doc,\n                \"subset\": subset_docs_by_id[version_doc[\"parent\"]]\n            }\n        return repre_context_by_id, repre_docs_by_version_id\n\n    def on_project_change(self, project_name):\n        \"\"\"\n            Called on each project change in parent widget.\n\n            Checks if Sync Server is enabled for a project, pushes changes to\n            model.\n        \"\"\"\n        enabled = False\n        if project_name:\n            self.model.reset_sync_server(project_name)\n            sync_server = self.model.sync_server\n            if sync_server:\n                enabled = sync_server.is_project_enabled(project_name,\n                                                         single=True)\n\n        lib.change_visibility(self.model, self.view, \"repre_info\", enabled)\n\n    def get_selected_items(self):\n        selection_model = self.view.selectionModel()\n        indexes = selection_model.selectedIndexes()\n\n        item_ids = set()\n        for index in indexes:\n            item_id = index.data(ITEM_ID_ROLE)\n            if item_id is not None:\n                item_ids.add(item_id)\n\n        output = []\n        for item_id in item_ids:\n            item = self.model.get_item_by_id(item_id)\n            if item is not None:\n                output.append(item)\n        return output\n\n    def get_selected_merge_items(self):\n        output = []\n        items = collections.deque(self.get_selected_items())\n\n        item_ids = set()\n        while items:\n            item = items.popleft()\n            if item.get(\"isGroup\"):\n                for child in item.children():\n                    items.appendleft(child)\n\n            elif item.get(\"isMerged\"):\n                item_id = item[\"id\"]\n                if item_id not in item_ids:\n                    item_ids.add(item_id)\n                    output.append(item)\n\n        return output\n\n    def get_selected_subsets(self):\n        output = []\n        items = collections.deque(self.get_selected_items())\n\n        item_ids = set()\n        while items:\n            item = items.popleft()\n            if item.get(\"isGroup\") or item.get(\"isMerged\"):\n                for child in item.children():\n                    items.appendleft(child)\n            else:\n                item_id = item[\"id\"]\n                if item_id not in item_ids:\n                    item_ids.add(item_id)\n                    output.append(item)\n        return output\n\n    def on_context_menu(self, point):\n        \"\"\"Shows menu with loader actions on Right-click.\n\n        Registered actions are filtered by selection and help of\n        `loaders_from_representation` from avalon api. Intersection of actions\n        is shown when more subset is selected. When there are not available\n        actions for selected subsets then special action is shown (works as\n        info message to user): \"*No compatible loaders for your selection\"\n\n        \"\"\"\n\n        point_index = self.view.indexAt(point)\n        if not point_index.isValid():\n            return\n\n        # Get selected subsets without groups\n        items = self.get_selected_subsets()\n\n        # Get all representation->loader combinations available for the\n        # index under the cursor, so we can list the user the options.\n        project_name = self.dbcon.active_project()\n        available_loaders = discover_loader_plugins(project_name)\n        if self.tool_name:\n            available_loaders = lib.remove_tool_name_from_loaders(\n                available_loaders, self.tool_name\n            )\n\n        repre_loaders = []\n        subset_loaders = []\n        for loader in available_loaders:\n            if not loader.enabled:\n                continue\n            # Skip if its a SubsetLoader.\n            if issubclass(loader, SubsetLoaderPlugin):\n                subset_loaders.append(loader)\n            else:\n                repre_loaders.append(loader)\n\n        loaders = list()\n\n        # Bool if is selected only one subset\n        one_item_selected = (len(items) == 1)\n\n        # Prepare variables for multiple selected subsets\n        first_loaders = []\n        found_combinations = None\n\n        is_first = True\n        repre_context_by_id, repre_docs_by_version_id = (\n            self._repre_contexts_for_loaders_filter(items)\n        )\n        for item in items:\n            _found_combinations = []\n            version_id = item[\"version_document\"][\"_id\"]\n            repre_docs = repre_docs_by_version_id[version_id]\n            for repre_doc in repre_docs:\n                repre_context = repre_context_by_id[repre_doc[\"_id\"]]\n                for loader in loaders_from_repre_context(\n                    repre_loaders,\n                    repre_context\n                ):\n                    # do not allow download whole repre, select specific repre\n                    if tools_lib.is_sync_loader(loader):\n                        continue\n\n                    # skip multiple select variant if one is selected\n                    if one_item_selected:\n                        loaders.append((repre_doc, loader))\n                        continue\n\n                    # store loaders of first subset\n                    if is_first:\n                        first_loaders.append((repre_doc, loader))\n\n                    # store combinations to compare with other subsets\n                    _found_combinations.append(\n                        (repre_doc[\"name\"].lower(), loader)\n                    )\n\n            # skip multiple select variant if one is selected\n            if one_item_selected:\n                continue\n\n            is_first = False\n            # Store first combinations to compare\n            if found_combinations is None:\n                found_combinations = _found_combinations\n            # Intersect found combinations with all previous subsets\n            else:\n                found_combinations = list(\n                    set(found_combinations) & set(_found_combinations)\n                )\n\n        if not one_item_selected:\n            # Filter loaders from first subset by intersected combinations\n            for repre, loader in first_loaders:\n                if (repre[\"name\"].lower(), loader) not in found_combinations:\n                    continue\n\n                loaders.append((repre, loader))\n\n        # Subset Loaders.\n        for loader in subset_loaders:\n            loaders.append((None, loader))\n\n        loaders = lib.sort_loaders(loaders)\n\n        # Prepare menu content based on selected items\n        menu = OptionalMenu(self)\n        if not loaders:\n            action = lib.get_no_loader_action(menu, one_item_selected)\n            menu.addAction(action)\n        else:\n            repre_contexts = get_repres_contexts(\n                repre_context_by_id.keys(), self.dbcon)\n\n            menu = lib.add_representation_loaders_to_menu(\n                loaders, menu, repre_contexts)\n\n        # Show the context action menu\n        global_point = self.view.mapToGlobal(point)\n        action = menu.exec_(global_point)\n        if not action or not action.data():\n            return\n\n        # Find the representation name and loader to trigger\n        action_representation, loader = action.data()\n\n        self.load_started.emit()\n\n        if issubclass(loader, SubsetLoaderPlugin):\n            subset_ids = []\n            subset_version_docs = {}\n            for item in items:\n                subset_id = item[\"version_document\"][\"parent\"]\n                subset_ids.append(subset_id)\n                subset_version_docs[subset_id] = item[\"version_document\"]\n\n            # get contexts only for selected menu option\n            subset_contexts_by_id = get_subset_contexts(subset_ids, self.dbcon)\n            subset_contexts = list(subset_contexts_by_id.values())\n            options = lib.get_options(action, loader, self, subset_contexts)\n\n            error_info = _load_subsets_by_loader(\n                loader, subset_contexts, options, subset_version_docs\n            )\n\n        else:\n            representation_name = action_representation[\"name\"]\n\n            # Run the loader for all selected indices, for those that have the\n            # same representation available\n\n            # Trigger\n            project_name = self.dbcon.active_project()\n            subset_name_by_version_id = dict()\n            for item in items:\n                version_id = item[\"version_document\"][\"_id\"]\n                subset_name_by_version_id[version_id] = item[\"subset\"]\n\n            version_ids = set(subset_name_by_version_id.keys())\n            repre_docs = get_representations(\n                project_name,\n                representation_names=[representation_name],\n                version_ids=version_ids,\n                fields=[\"_id\", \"parent\"]\n            )\n\n            repre_ids = []\n            for repre_doc in repre_docs:\n                repre_ids.append(repre_doc[\"_id\"])\n\n                # keep only version ids without representation with that name\n                version_id = repre_doc[\"parent\"]\n                version_ids.discard(version_id)\n\n            if version_ids:\n                # report versions that didn't have valid representation\n                joined_subset_names = \", \".join([\n                    '\"{}\"'.format(subset_name_by_version_id[version_id])\n                    for version_id in version_ids\n                ])\n                self.echo(\"Subsets {} don't have representation '{}'\".format(\n                    joined_subset_names, representation_name\n                ))\n\n            # get contexts only for selected menu option\n            repre_contexts = get_repres_contexts(repre_ids, self.dbcon)\n            options = lib.get_options(\n                action, loader, self, list(repre_contexts.values())\n            )\n            error_info = _load_representations_by_loader(\n                loader, repre_contexts, options=options\n            )\n\n        self.load_ended.emit()\n\n        if error_info:\n            box = LoadErrorMessageBox(error_info, self)\n            box.show()\n\n    def group_subsets(self, name, asset_ids, items):\n        subset_ids = {\n            item[\"_id\"]\n            for item in items\n            if item.get(\"_id\")\n        }\n        if not subset_ids:\n            return\n\n        if name:\n            self.echo(\"Group subsets to '%s'..\" % name)\n        else:\n            self.echo(\"Ungroup subsets..\")\n\n        project_name = self.dbcon.active_project()\n        op_session = OperationsSession()\n        for subset_id in subset_ids:\n            op_session.update_entity(\n                project_name,\n                \"subset\",\n                subset_id,\n                {\"data.subsetGroup\": name or REMOVED_VALUE}\n            )\n\n        op_session.commit()\n\n    def echo(self, message):\n        print(message)\n\n\nclass VersionTextEdit(QtWidgets.QTextEdit):\n    \"\"\"QTextEdit that displays version specific information.\n\n    This also overrides the context menu to add actions like copying\n    source path to clipboard or copying the raw data of the version\n    to clipboard.\n\n    \"\"\"\n    def __init__(self, dbcon, parent=None):\n        super(VersionTextEdit, self).__init__(parent=parent)\n        self.dbcon = dbcon\n\n        self.data = {\n            \"source\": None,\n            \"raw\": None\n        }\n        self._anatomy = None\n\n        # Reset\n        self.set_version(None)\n\n    def set_version(self, version_doc=None, version_id=None):\n        # TODO expect only filling data (do not query them here!)\n        if not version_doc and not version_id:\n            # Reset state to empty\n            self.data = {\n                \"source\": None,\n                \"raw\": None,\n            }\n            self.setText(\"\")\n            self.setEnabled(True)\n            return\n\n        self.setEnabled(True)\n\n        print(\"Querying..\")\n\n        project_name = self.dbcon.active_project()\n        if not version_doc:\n            version_doc = get_version_by_id(project_name, version_id)\n            assert version_doc, \"Not a valid version id\"\n\n        if version_doc[\"type\"] == \"hero_version\":\n            _version_doc = get_version_by_id(\n                project_name, version_doc[\"version_id\"]\n            )\n            version_doc[\"data\"] = _version_doc[\"data\"]\n            version_doc[\"name\"] = HeroVersionType(\n                _version_doc[\"name\"]\n            )\n\n        subset = get_subset_by_id(project_name, version_doc[\"parent\"])\n        assert subset, \"No valid subset parent for version\"\n\n        # Define readable creation timestamp\n        created = version_doc[\"data\"][\"time\"]\n        created = datetime.datetime.strptime(created, \"%Y%m%dT%H%M%SZ\")\n        created = datetime.datetime.strftime(created, \"%b %d %Y %H:%M\")\n\n        comment = version_doc[\"data\"].get(\"comment\", None) or \"No comment\"\n\n        source = version_doc[\"data\"].get(\"source\", None)\n        source_label = source if source else \"No source\"\n\n        # Store source and raw data\n        self.data[\"source\"] = source\n        self.data[\"raw\"] = version_doc\n\n        if version_doc[\"type\"] == \"hero_version\":\n            version_name = \"hero\"\n        else:\n            version_name = tools_lib.format_version(version_doc[\"name\"])\n\n        data = {\n            \"subset\": subset[\"name\"],\n            \"version\": version_name,\n            \"comment\": comment,\n            \"created\": created,\n            \"source\": source_label\n        }\n\n        self.setHtml((\n            \"<h2>{subset}</h2>\"\n            \"<h3>{version}</h3>\"\n            \"<b>Comment</b><br>\"\n            \"{comment}<br><br>\"\n\n            \"<b>Created</b><br>\"\n            \"{created}<br><br>\"\n\n            \"<b>Source</b><br>\"\n            \"{source}\"\n        ).format(**data))\n\n    def contextMenuEvent(self, event):\n        \"\"\"Context menu with additional actions\"\"\"\n        menu = self.createStandardContextMenu()\n\n        # Add additional actions when any text so we can assume\n        # the version is set.\n        if self.toPlainText().strip():\n            menu.addSeparator()\n            action = QtWidgets.QAction(\n                \"Copy source path to clipboard\", menu\n            )\n            action.triggered.connect(self.on_copy_source)\n            menu.addAction(action)\n\n            action = QtWidgets.QAction(\n                \"Copy raw data to clipboard\", menu\n            )\n            action.triggered.connect(self.on_copy_raw)\n            menu.addAction(action)\n\n        menu.exec_(event.globalPos())\n\n    def on_copy_source(self):\n        \"\"\"Copy formatted source path to clipboard\"\"\"\n        source = self.data.get(\"source\", None)\n        if not source:\n            return\n\n        project_name = self.dbcon.current_project()\n        if self._anatomy is None or self._anatomy.project_name != project_name:\n            self._anatomy = Anatomy(project_name)\n\n        path = source.format(root=self._anatomy.roots)\n        clipboard = QtWidgets.QApplication.clipboard()\n        clipboard.setText(path)\n\n    def on_copy_raw(self):\n        \"\"\"Copy raw version data to clipboard\n\n        The data is string formatted with `pprint.pformat`.\n\n        \"\"\"\n        raw = self.data.get(\"raw\", None)\n        if not raw:\n            return\n\n        raw_text = pprint.pformat(raw)\n        clipboard = QtWidgets.QApplication.clipboard()\n        clipboard.setText(raw_text)\n\n\nclass ThumbnailWidget(QtWidgets.QLabel):\n    aspect_ratio = (16, 9)\n    max_width = 300\n\n    def __init__(self, dbcon, parent=None):\n        super(ThumbnailWidget, self).__init__(parent)\n        self.dbcon = dbcon\n\n        self.current_thumb_id = None\n        self.current_thumbnail = None\n\n        self.setAlignment(QtCore.Qt.AlignCenter)\n\n        # TODO get res path much better way\n        default_pix_path = os.path.join(\n            os.path.dirname(os.path.abspath(__file__)),\n            \"images\",\n            \"default_thumbnail.png\"\n        )\n        self.default_pix = QtGui.QPixmap(default_pix_path)\n        self.set_pixmap()\n\n    def height(self):\n        width = self.width()\n        asp_w, asp_h = self.aspect_ratio\n\n        return (width / asp_w) * asp_h\n\n    def width(self):\n        width = super(ThumbnailWidget, self).width()\n        if width > self.max_width:\n            width = self.max_width\n        return width\n\n    def set_pixmap(self, pixmap=None):\n        if not pixmap:\n            pixmap = self.default_pix\n            self.current_thumb_id = None\n\n        self.current_thumbnail = pixmap\n\n        pixmap = self.scale_pixmap(pixmap)\n        self.setPixmap(pixmap)\n\n    def resizeEvent(self, _event):\n        if not self.current_thumbnail:\n            return\n        cur_pix = self.scale_pixmap(self.current_thumbnail)\n        self.setPixmap(cur_pix)\n\n    def scale_pixmap(self, pixmap):\n        return pixmap.scaled(\n            self.width(),\n            self.height(),\n            QtCore.Qt.KeepAspectRatio,\n            QtCore.Qt.SmoothTransformation\n        )\n\n    def set_thumbnail(self, src_type, doc_ids):\n        if not doc_ids:\n            self.set_pixmap()\n            return\n\n        src_id = doc_ids[0]\n\n        project_name = self.dbcon.active_project()\n        thumbnail_id = get_thumbnail_id_from_source(\n            project_name,\n            src_type,\n            src_id,\n        )\n        if thumbnail_id == self.current_thumb_id:\n            if self.current_thumbnail is None:\n                self.set_pixmap()\n            return\n\n        self.current_thumb_id = thumbnail_id\n        if not thumbnail_id:\n            self.set_pixmap()\n            return\n\n        thumbnail_ent = get_thumbnail(\n            project_name, thumbnail_id, src_type, src_id\n        )\n        if not thumbnail_ent:\n            return\n\n        thumbnail_bin = get_thumbnail_binary(\n            thumbnail_ent, \"thumbnail\", self.dbcon\n        )\n        if not thumbnail_bin:\n            self.set_pixmap()\n            return\n\n        thumbnail = QtGui.QPixmap()\n        thumbnail.loadFromData(thumbnail_bin)\n\n        self.set_pixmap(thumbnail)\n\n\nclass VersionWidget(QtWidgets.QWidget):\n    \"\"\"A Widget that display information about a specific version\"\"\"\n    def __init__(self, dbcon, parent=None):\n        super(VersionWidget, self).__init__(parent=parent)\n\n        data = VersionTextEdit(dbcon, self)\n        data.setReadOnly(True)\n\n        depend_widget = SimpleLinkView(dbcon, self)\n\n        tab = QtWidgets.QTabWidget()\n        tab.addTab(data, \"Version Info\")\n        tab.addTab(depend_widget, \"Dependency\")\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(tab)\n\n        self.data = data\n        self.depend_widget = depend_widget\n\n    def set_version(self, version_doc):\n        self.data.set_version(version_doc)\n        self.depend_widget.set_version(version_doc)\n\n\nclass FamilyModel(QtGui.QStandardItemModel):\n    def __init__(self, dbcon, family_config_cache):\n        super(FamilyModel, self).__init__()\n\n        self.dbcon = dbcon\n        self.family_config_cache = family_config_cache\n\n        self._items_by_family = {}\n\n    def refresh(self):\n        families = set()\n        project_name = self.dbcon.current_project()\n        if project_name:\n            families = get_subset_families(project_name)\n\n        root_item = self.invisibleRootItem()\n\n        for family in tuple(self._items_by_family.keys()):\n            if family not in families:\n                item = self._items_by_family.pop(family)\n                root_item.removeRow(item.row())\n\n        self.family_config_cache.refresh()\n\n        new_items = []\n        for family in families:\n            family_config = self.family_config_cache.family_config(family)\n            label = family_config.get(\"label\", family)\n            icon = family_config.get(\"icon\", None)\n\n            if family_config.get(\"state\", True):\n                state = QtCore.Qt.Checked\n            else:\n                state = QtCore.Qt.Unchecked\n\n            if family not in self._items_by_family:\n                item = QtGui.QStandardItem(label)\n                item.setFlags(\n                    QtCore.Qt.ItemIsEnabled\n                    | QtCore.Qt.ItemIsSelectable\n                    | QtCore.Qt.ItemIsUserCheckable\n                )\n                new_items.append(item)\n                self._items_by_family[family] = item\n\n            else:\n                item = self._items_by_family[label]\n                item.setData(label, QtCore.Qt.DisplayRole)\n\n            item.setCheckState(state)\n\n            if icon:\n                item.setIcon(icon)\n\n        if new_items:\n            root_item.appendRows(new_items)\n\n\nclass FamilyProxyFiler(QtCore.QSortFilterProxyModel):\n    def __init__(self, *args, **kwargs):\n        super(FamilyProxyFiler, self).__init__(*args, **kwargs)\n\n        self._filtering_enabled = False\n        self._enabled_families = set()\n\n    def set_enabled_families(self, families):\n        if self._enabled_families == families:\n            return\n\n        self._enabled_families = families\n        if self._filtering_enabled:\n            self.invalidateFilter()\n\n    def is_filter_enabled(self):\n        return self._filtering_enabled\n\n    def set_filter_enabled(self, enabled=None):\n        if enabled is None:\n            enabled = not self._filtering_enabled\n        elif self._filtering_enabled == enabled:\n            return\n\n        self._filtering_enabled = enabled\n        self.invalidateFilter()\n\n    def filterAcceptsRow(self, row, parent):\n        if not self._filtering_enabled:\n            return True\n\n        if not self._enabled_families:\n            return False\n\n        index = self.sourceModel().index(row, self.filterKeyColumn(), parent)\n        if index.data(QtCore.Qt.DisplayRole) in self._enabled_families:\n            return True\n        return False\n\n\nclass FamilyListView(QtWidgets.QListView):\n    active_changed = QtCore.Signal(list)\n\n    def __init__(self, dbcon, family_config_cache, parent=None):\n        super(FamilyListView, self).__init__(parent=parent)\n\n        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)\n        self.setAlternatingRowColors(True)\n        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n\n        family_model = FamilyModel(dbcon, family_config_cache)\n        proxy_model = FamilyProxyFiler()\n        proxy_model.setDynamicSortFilter(True)\n        proxy_model.setSourceModel(family_model)\n\n        self.setModel(proxy_model)\n\n        family_model.dataChanged.connect(self._on_data_change)\n        self.customContextMenuRequested.connect(self._on_context_menu)\n\n        self._family_model = family_model\n        self._proxy_model = proxy_model\n\n    def set_enabled_families(self, families):\n        self._proxy_model.set_enabled_families(families)\n\n        self.set_enabled_family_filtering(True)\n\n    def set_enabled_family_filtering(self, enabled=None):\n        self._proxy_model.set_filter_enabled(enabled)\n\n    def refresh(self):\n        self._family_model.refresh()\n\n        self.active_changed.emit(self.get_enabled_families())\n\n    def get_enabled_families(self):\n        \"\"\"Return the checked family items\"\"\"\n        model = self._family_model\n        checked_families = []\n        for row in range(model.rowCount()):\n            index = model.index(row, 0)\n            checked = checkstate_int_to_enum(\n                index.data(QtCore.Qt.CheckStateRole)\n            )\n            if checked == QtCore.Qt.Checked:\n                family = index.data(QtCore.Qt.DisplayRole)\n                checked_families.append(family)\n\n        return checked_families\n\n    def set_all_unchecked(self):\n        self._set_checkstates(False, self._get_all_indexes())\n\n    def set_all_checked(self):\n        self._set_checkstates(True, self._get_all_indexes())\n\n    def _get_all_indexes(self):\n        indexes = []\n        model = self._family_model\n        for row in range(model.rowCount()):\n            index = model.index(row, 0)\n            indexes.append(index)\n        return indexes\n\n    def _set_checkstates(self, checked, indexes):\n        if not indexes:\n            return\n\n        if checked is None:\n            state = None\n        elif checked:\n            state = QtCore.Qt.Checked\n        else:\n            state = QtCore.Qt.Unchecked\n\n        self.blockSignals(True)\n\n        for index in indexes:\n            index_state = checkstate_int_to_enum(\n                index.data(QtCore.Qt.CheckStateRole)\n            )\n            if index_state == state:\n                continue\n\n            new_state = state\n            if new_state is None:\n                if index_state in QtCore.Qt.Checked:\n                    new_state = QtCore.Qt.Unchecked\n                else:\n                    new_state = QtCore.Qt.Checked\n\n            index.model().setData(index, new_state, QtCore.Qt.CheckStateRole)\n\n        self.blockSignals(False)\n\n        self.active_changed.emit(self.get_enabled_families())\n\n    def _change_selection_state(self, checked):\n        indexes = self.selectionModel().selectedIndexes()\n        self._set_checkstates(checked, indexes)\n\n    def _on_data_change(self, *_args):\n        self.active_changed.emit(self.get_enabled_families())\n\n    def _on_context_menu(self, pos):\n        \"\"\"Build RMB menu under mouse at current position (within widget)\"\"\"\n        menu = QtWidgets.QMenu(self)\n\n        # Add enable all action\n        action_check_all = QtWidgets.QAction(menu)\n        action_check_all.setText(\"Enable All\")\n        action_check_all.triggered.connect(self.set_all_checked)\n        # Add disable all action\n        action_uncheck_all = QtWidgets.QAction(menu)\n        action_uncheck_all.setText(\"Disable All\")\n        action_uncheck_all.triggered.connect(self.set_all_unchecked)\n\n        menu.addAction(action_check_all)\n        menu.addAction(action_uncheck_all)\n\n        # Get mouse position\n        global_pos = self.viewport().mapToGlobal(pos)\n        menu.exec_(global_pos)\n\n    def event(self, event):\n        if not event.type() == QtCore.QEvent.KeyPress:\n            pass\n\n        elif event.key() == QtCore.Qt.Key_Space:\n            self._change_selection_state(None)\n            return True\n\n        elif event.key() == QtCore.Qt.Key_Backspace:\n            self._change_selection_state(False)\n            return True\n\n        elif event.key() == QtCore.Qt.Key_Return:\n            self._change_selection_state(True)\n            return True\n\n        return super(FamilyListView, self).event(event)\n\n\nclass RepresentationWidget(QtWidgets.QWidget):\n    load_started = QtCore.Signal()\n    load_ended = QtCore.Signal()\n\n    default_widths = (\n        (\"name\", 120),\n        (\"subset\", 125),\n        (\"asset\", 125),\n        (\"active_site\", 85),\n        (\"remote_site\", 85)\n    )\n\n    commands = {'active': 'Download', 'remote': 'Upload'}\n\n    def __init__(self, dbcon, tool_name=None, parent=None):\n        super(RepresentationWidget, self).__init__(parent=parent)\n        self.dbcon = dbcon\n        self.tool_name = tool_name\n\n        headers = [item[0] for item in self.default_widths]\n\n        model = RepresentationModel(self.dbcon, headers)\n\n        proxy_model = RepresentationSortProxyModel(self)\n        proxy_model.setSourceModel(model)\n\n        label = QtWidgets.QLabel(\"Representations\", self)\n\n        tree_view = DeselectableTreeView(parent=self)\n        tree_view.setObjectName(\"RepresentationView\")\n        tree_view.setModel(proxy_model)\n        tree_view.setAllColumnsShowFocus(True)\n        tree_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        tree_view.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection)\n        tree_view.setSortingEnabled(True)\n        tree_view.sortByColumn(1, QtCore.Qt.AscendingOrder)\n        tree_view.setAlternatingRowColors(True)\n        tree_view.setIndentation(20)\n        tree_view.collapseAll()\n\n        for column_name, width in self.default_widths:\n            idx = model.Columns.index(column_name)\n            tree_view.setColumnWidth(idx, width)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(label)\n        layout.addWidget(tree_view)\n\n        # self.itemChanged.connect(self._on_item_changed)\n        tree_view.customContextMenuRequested.connect(self.on_context_menu)\n\n        self.tree_view = tree_view\n        self.model = model\n        self.proxy_model = proxy_model\n\n        self.sync_server_enabled = False\n\n        self.on_project_change(dbcon.current_project())\n\n        self.model.refresh()\n\n    def on_project_change(self, project_name):\n        \"\"\"\n            Called on each project change in parent widget.\n\n            Checks if Sync Server is enabled for a project, pushes changes to\n            model.\n        \"\"\"\n        enabled = False\n        if project_name:\n            self.model.reset_sync_server(project_name)\n            sync_server = self.model.sync_server\n            if sync_server:\n                enabled = sync_server.is_project_enabled(project_name,\n                                                         single=True)\n\n        self.sync_server_enabled = enabled\n        lib.change_visibility(self.model, self.tree_view,\n                              \"active_site\", enabled)\n        lib.change_visibility(self.model, self.tree_view,\n                              \"remote_site\", enabled)\n\n    def _repre_contexts_for_loaders_filter(self, items):\n        repre_ids = []\n        for item in items:\n            repre_ids.append(item[\"_id\"])\n\n        project_name = self.dbcon.active_project()\n        repre_docs = list(get_representations(\n            project_name,\n            representation_ids=repre_ids,\n            fields=[\"name\", \"parent\", \"data\", \"context\"]\n        ))\n\n        version_ids = [\n            repre_doc[\"parent\"]\n            for repre_doc in repre_docs\n        ]\n        version_docs = get_versions(\n            project_name,\n            version_ids=version_ids,\n            hero=True\n        )\n\n        version_docs_by_id = {}\n        version_docs_by_subset_id = collections.defaultdict(list)\n        for version_doc in version_docs:\n            version_id = version_doc[\"_id\"]\n            subset_id = version_doc[\"parent\"]\n            version_docs_by_id[version_id] = version_doc\n            version_docs_by_subset_id[subset_id].append(version_doc)\n\n        subset_docs = list(get_subsets(\n            project_name,\n            subset_ids=version_docs_by_subset_id.keys(),\n            fields=[\"schema\", \"data.families\"]\n        ))\n        subset_docs_by_id = {\n            subset_doc[\"_id\"]: subset_doc\n            for subset_doc in subset_docs\n        }\n        repre_context_by_id = {}\n        for repre_doc in repre_docs:\n            version_id = repre_doc[\"parent\"]\n\n            version_doc = version_docs_by_id[version_id]\n            repre_context_by_id[repre_doc[\"_id\"]] = {\n                \"representation\": repre_doc,\n                \"version\": version_doc,\n                \"subset\": subset_docs_by_id[version_doc[\"parent\"]]\n            }\n        return repre_context_by_id\n\n    def get_selected_items(self):\n        selection_model = self.tree_view.selectionModel()\n        indexes = selection_model.selectedIndexes()\n\n        item_ids = set()\n        for index in indexes:\n            item_id = index.data(ITEM_ID_ROLE)\n            if item_id is not None:\n                item_ids.add(item_id)\n\n        output = []\n        for item_id in item_ids:\n            item = self.model.get_item_by_id(item_id)\n            if item is not None:\n                output.append(item)\n        return output\n\n    def get_selected_repre_items(self):\n        output = []\n        items = collections.deque(self.get_selected_items())\n\n        item_ids = set()\n        while items:\n            item = items.popleft()\n            if item.get(\"isGroup\") or item.get(\"isMerged\"):\n                for child in item.children():\n                    items.appendleft(child)\n            else:\n                item_id = item[\"id\"]\n                if item_id not in item_ids:\n                    item_ids.add(item_id)\n                    output.append(item)\n        return output\n\n    def on_context_menu(self, point):\n        \"\"\"Shows menu with loader actions on Right-click.\n\n        Registered actions are filtered by selection and help of\n        `loaders_from_representation` from avalon api. Intersection of actions\n        is shown when more subset is selected. When there are not available\n        actions for selected subsets then special action is shown (works as\n        info message to user): \"*No compatible loaders for your selection\"\n\n        \"\"\"\n        point_index = self.tree_view.indexAt(point)\n        if not point_index.isValid():\n            return\n\n        # Get selected subsets without groups\n        selection = self.tree_view.selectionModel()\n        rows = selection.selectedRows(column=0)\n\n        items = self.get_selected_repre_items()\n        selected_side = self._get_selected_side(point_index, rows)\n        # Get all representation->loader combinations available for the\n        # index under the cursor, so we can list the user the options.\n        project_name = self.dbcon.active_project()\n        available_loaders = discover_loader_plugins(project_name)\n\n        filtered_loaders = []\n        for loader in available_loaders:\n            if not loader.enabled:\n                continue\n            # Skip subset loaders\n            if issubclass(loader, SubsetLoaderPlugin):\n                continue\n\n            if (\n                tools_lib.is_sync_loader(loader)\n                and not self.sync_server_enabled\n            ):\n                continue\n\n            filtered_loaders.append(loader)\n\n        if self.tool_name:\n            filtered_loaders = lib.remove_tool_name_from_loaders(\n                filtered_loaders, self.tool_name\n            )\n\n        loaders = list()\n        already_added_loaders = set()\n        label_already_in_menu = set()\n\n        repre_context_by_id = (\n            self._repre_contexts_for_loaders_filter(items)\n        )\n\n        for item in items:\n            repre_context = repre_context_by_id[item[\"_id\"]]\n            for loader in loaders_from_repre_context(\n                filtered_loaders,\n                repre_context\n            ):\n                if tools_lib.is_sync_loader(loader):\n                    both_unavailable = (\n                        item[\"active_site_progress\"] <= 0\n                        and item[\"remote_site_progress\"] <= 0\n                    )\n                    if both_unavailable:\n                        continue\n\n                    for selected_side in self.commands.keys():\n                        item = item.copy()\n                        item[\"custom_label\"] = None\n                        label = None\n                        selected_site_progress = item.get(\n                            \"{}_site_progress\".format(selected_side), -1)\n\n                        # only remove if actually present\n                        if tools_lib.is_remove_site_loader(loader):\n                            label = \"Remove {}\".format(selected_side)\n                            if selected_site_progress < 1:\n                                continue\n\n                        if tools_lib.is_add_site_loader(loader):\n                            label = self.commands[selected_side]\n                            if selected_site_progress >= 0:\n                                label = 'Re-{} {}'.format(label, selected_side)\n\n                        if not label:\n                            continue\n\n                        item[\"selected_side\"] = selected_side\n                        item[\"custom_label\"] = label\n\n                        if label not in label_already_in_menu:\n                            loaders.append((item, loader))\n                            already_added_loaders.add(loader)\n                            label_already_in_menu.add(label)\n\n                else:\n                    item = item.copy()\n                    item[\"custom_label\"] = None\n\n                    if loader not in already_added_loaders:\n                        loaders.append((item, loader))\n                        already_added_loaders.add(loader)\n\n        loaders = lib.sort_loaders(loaders)\n\n        menu = OptionalMenu(self)\n        if not loaders:\n            action = lib.get_no_loader_action(menu)\n            menu.addAction(action)\n        else:\n            repre_contexts = get_repres_contexts(\n                repre_context_by_id.keys(), self.dbcon)\n            menu = lib.add_representation_loaders_to_menu(loaders, menu,\n                                                          repre_contexts)\n\n        self._process_action(items, menu, point)\n\n    def _process_action(self, items, menu, point):\n        \"\"\"Show the context action menu and process selected\n\n        Args:\n            items(dict): menu items\n            menu(OptionalMenu)\n            point(PointIndex)\n        \"\"\"\n        global_point = self.tree_view.mapToGlobal(point)\n        action = menu.exec_(global_point)\n\n        if not action or not action.data():\n            return\n\n        self.load_started.emit()\n\n        # Find the representation name and loader to trigger\n        action_representation, loader = action.data()\n        repre_ids = []\n        data_by_repre_id = {}\n        selected_side = action_representation.get(\"selected_side\")\n        site_name = \"{}_site_name\".format(selected_side)\n\n        is_sync_loader = tools_lib.is_sync_loader(loader)\n        for item in items:\n            repre_id = item[\"_id\"]\n            repre_ids.append(repre_id)\n            if not is_sync_loader:\n                continue\n\n            data_site_name = item.get(site_name)\n            if not data_site_name:\n                continue\n\n            data_by_repre_id[repre_id] = {\n                \"site_name\": data_site_name\n            }\n\n        repre_contexts = get_repres_contexts(repre_ids, self.dbcon)\n        options = lib.get_options(action, loader, self,\n                                  list(repre_contexts.values()))\n\n        errors = _load_representations_by_loader(\n            loader, repre_contexts,\n            options=options, data_by_repre_id=data_by_repre_id)\n\n        self.model.refresh()\n\n        self.load_ended.emit()\n\n        if errors:\n            box = LoadErrorMessageBox(errors, self)\n            box.show()\n\n    def _get_optional_labels(self, loaders, selected_side):\n        \"\"\"Each loader could have specific label\n\n            Args:\n                loaders (tuple of dict, dict): (item, loader)\n                selected_side(string): active or remote\n\n            Returns:\n                (dict) {loader: string}\n        \"\"\"\n        optional_labels = {}\n        if selected_side:\n            if selected_side == 'active':\n                txt = \"Localize\"\n            else:\n                txt = \"Sync to Remote\"\n            optional_labels = {loader: txt for _, loader in loaders\n                               if tools_lib.is_sync_loader(loader)}\n        return optional_labels\n\n    def _get_selected_side(self, point_index, rows):\n        \"\"\"Returns active/remote label according to column in 'point_index'\"\"\"\n        selected_side = None\n        if self.sync_server_enabled:\n            if rows:\n                source_index = self.proxy_model.mapToSource(point_index)\n                selected_side = self.model.data(source_index,\n                                                self.model.SiteSideRole)\n        return selected_side\n\n    def set_version_ids(self, version_ids):\n        self.model.set_version_ids(version_ids)\n\n    def _set_download(self):\n        pass\n\n    def change_visibility(self, column_name, visible):\n        \"\"\"\n            Hides or shows particular 'column_name'.\n\n            \"asset\" and \"subset\" columns should be visible only in multiselect\n        \"\"\"\n        lib.change_visibility(self.model, self.tree_view, column_name, visible)\n\n\ndef _load_representations_by_loader(loader, repre_contexts,\n                                    options,\n                                    data_by_repre_id=None):\n    \"\"\"Loops through list of repre_contexts and loads them with one loader\n\n        Args:\n            loader (cls of LoaderPlugin) - not initialized yet\n            repre_contexts (dicts) - full info about selected representations\n                (containing repre_doc, version_doc, subset_doc, project info)\n            options (dict) - qargparse arguments to fill OptionDialog\n            data_by_repre_id (dict) - additional data applicable on top of\n                options to provide dynamic values\n    \"\"\"\n    error_info = []\n\n    if options is None:  # not load when cancelled\n        return\n\n    for repre_context in repre_contexts.values():\n        version_doc = repre_context[\"version\"]\n        if version_doc[\"type\"] == \"hero_version\":\n            version_name = \"Hero\"\n        else:\n            version_name = version_doc.get(\"name\")\n        try:\n            if data_by_repre_id:\n                repre_id = repre_context[\"representation\"][\"_id\"]\n                data = data_by_repre_id.get(repre_id)\n                options.update(data)\n            load_with_repre_context(\n                loader,\n                repre_context,\n                options=options\n            )\n\n        except IncompatibleLoaderError as exc:\n            print(exc)\n            error_info.append((\n                \"Incompatible Loader\",\n                None,\n                repre_context[\"representation\"][\"name\"],\n                repre_context[\"subset\"][\"name\"],\n                version_name\n            ))\n\n        except Exception as exc:\n            formatted_traceback = None\n            if not isinstance(exc, LoadError):\n                exc_type, exc_value, exc_traceback = sys.exc_info()\n                formatted_traceback = \"\".join(traceback.format_exception(\n                    exc_type, exc_value, exc_traceback\n                ))\n\n            error_info.append((\n                str(exc),\n                formatted_traceback,\n                repre_context[\"representation\"][\"name\"],\n                repre_context[\"subset\"][\"name\"],\n                version_name\n            ))\n    return error_info\n\n\ndef _load_subsets_by_loader(loader, subset_contexts, options,\n                            subset_version_docs=None):\n    \"\"\"\n        Triggers load with SubsetLoader type of loaders\n\n        Args:\n            loader (SubsetLoder):\n            subset_contexts (list):\n            options (dict):\n            subset_version_docs (dict): {subset_id: version_doc}\n    \"\"\"\n    error_info = []\n\n    if options is None:  # not load when cancelled\n        return error_info\n\n    if loader.is_multiple_contexts_compatible:\n        subset_names = []\n        for context in subset_contexts:\n            subset_name = context.get(\"subset\", {}).get(\"name\") or \"N/A\"\n            subset_names.append(subset_name)\n\n            context[\"version\"] = subset_version_docs[context[\"subset\"][\"_id\"]]\n        try:\n            load_with_subset_contexts(\n                loader,\n                subset_contexts,\n                options=options\n            )\n\n        except Exception as exc:\n            formatted_traceback = None\n            if not isinstance(exc, LoadError):\n                exc_type, exc_value, exc_traceback = sys.exc_info()\n                formatted_traceback = \"\".join(traceback.format_exception(\n                    exc_type, exc_value, exc_traceback\n                ))\n            error_info.append((\n                str(exc),\n                formatted_traceback,\n                None,\n                \", \".join(subset_names),\n                None\n            ))\n    else:\n        for subset_context in subset_contexts:\n            subset_name = subset_context.get(\"subset\", {}).get(\"name\") or \"N/A\"\n\n            version_doc = subset_version_docs[subset_context[\"subset\"][\"_id\"]]\n            subset_context[\"version\"] = version_doc\n            try:\n                load_with_subset_context(\n                    loader,\n                    subset_context,\n                    options=options\n                )\n\n            except Exception as exc:\n                formatted_traceback = None\n                if not isinstance(exc, LoadError):\n                    exc_type, exc_value, exc_traceback = sys.exc_info()\n                    formatted_traceback = \"\".join(traceback.format_exception(\n                        exc_type, exc_value, exc_traceback\n                    ))\n\n                error_info.append((\n                    str(exc),\n                    formatted_traceback,\n                    None,\n                    subset_name,\n                    None\n                ))\n\n    return error_info\n\n\nclass AvailabilityDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"\n        Prints icons and downloaded representation ration for both sides.\n    \"\"\"\n\n    def __init__(self, dbcon, parent=None):\n        super(AvailabilityDelegate, self).__init__(parent)\n        self.icons = tools_lib.get_repre_icons()\n\n    def paint(self, painter, option, index):\n        super(AvailabilityDelegate, self).paint(painter, option, index)\n        option = QtWidgets.QStyleOptionViewItem(option)\n        option.showDecorationSelected = True\n\n        provider_active = index.data(LOCAL_PROVIDER_ROLE)\n        provider_remote = index.data(REMOTE_PROVIDER_ROLE)\n\n        availability_active = index.data(LOCAL_AVAILABILITY_ROLE)\n        availability_remote = index.data(REMOTE_AVAILABILITY_ROLE)\n\n        if not availability_active or not availability_remote:  # group lines\n            return\n\n        idx = 0\n        height = width = 24\n        for value, provider in [(availability_active, provider_active),\n                                (availability_remote, provider_remote)]:\n            icon = self.icons.get(provider)\n            if not icon:\n                continue\n\n            pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(height, width)))\n            padding = 10 + (70 * idx)\n            point = QtCore.QPoint(option.rect.x() + padding,\n                                  option.rect.y() +\n                                  (option.rect.height() - pixmap.height()) / 2)\n            painter.drawPixmap(point, pixmap)\n\n            text_rect = option.rect.translated(padding + width + 10, 0)\n            painter.drawText(\n                text_rect,\n                option.displayAlignment,\n                value\n            )\n\n            idx += 1\n\n    def displayText(self, value, locale):\n        pass\n"
  },
  {
    "path": "openpype/tools/project_manager/__init__.py",
    "content": "\"\"\"Project Manager tool\n\nPurpose of the tool is to be able create and modify hierarchy under project\nready for OpenPype pipeline usage. Tool also give ability to create new\nprojects.\n\n# Brief info\nProject hierarchy consist of two types \"asset\" and \"task\". Assets can be\nchildren of Project or other Asset. Task can be children of Asset.\n\nIt is not possible to have duplicated Asset name across whole project.\nIt is not possible to have duplicated Task name under one Asset.\n\nAsset can't be moved or renamed if has or it's children has published content.\n\nDeleted assets are not deleted from database but their type is changed to\n\"archived_asset\".\n\nTool allows to modify Asset attributes like frame start/end, fps, etc.\n\"\"\"\n\nfrom .project_manager import (\n    ProjectManagerWindow,\n    main\n)\n\n\n__all__ = (\n    \"ProjectManagerWindow\",\n    \"main\"\n)\n"
  },
  {
    "path": "openpype/tools/project_manager/__main__.py",
    "content": "from project_manager import main\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "openpype/tools/project_manager/project_manager/__init__.py",
    "content": "__all__ = (\n    \"IDENTIFIER_ROLE\",\n    \"PROJECT_NAME_ROLE\",\n\n    \"HierarchyView\",\n\n    \"ProjectModel\",\n    \"ProjectProxyFilter\",\n    \"CreateProjectDialog\",\n\n    \"HierarchyModel\",\n    \"HierarchySelectionModel\",\n    \"BaseItem\",\n    \"RootItem\",\n    \"ProjectItem\",\n    \"AssetItem\",\n    \"TaskItem\",\n\n    \"ProjectManagerWindow\",\n    \"main\"\n)\n\n\nfrom .constants import (\n    IDENTIFIER_ROLE,\n    PROJECT_NAME_ROLE\n)\nfrom .widgets import CreateProjectDialog\nfrom .view import HierarchyView\nfrom .model import (\n    ProjectModel,\n    ProjectProxyFilter,\n\n    HierarchyModel,\n    HierarchySelectionModel,\n    BaseItem,\n    RootItem,\n    ProjectItem,\n    AssetItem,\n    TaskItem\n)\nfrom .window import ProjectManagerWindow\n\n\ndef main():\n    import sys\n    from qtpy import QtWidgets\n\n    app = QtWidgets.QApplication([])\n\n    window = ProjectManagerWindow()\n    window.show()\n\n    sys.exit(app.exec_())\n"
  },
  {
    "path": "openpype/tools/project_manager/project_manager/constants.py",
    "content": "import re\nfrom qtpy import QtCore\n\n\n# Item identifier (unique ID - uuid4 is used)\nIDENTIFIER_ROLE = QtCore.Qt.UserRole + 1\n# Item has duplicated name (Asset and Task items)\nDUPLICATED_ROLE = QtCore.Qt.UserRole + 2\n# It is possible to move and rename items\n# - that is disabled if e.g. Asset has published content\nHIERARCHY_CHANGE_ABLE_ROLE = QtCore.Qt.UserRole + 3\n# Item is marked for deletion\n# - item will be deleted after hitting save\nREMOVED_ROLE = QtCore.Qt.UserRole + 4\n# Item type in string\nITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5\n# Item has opened editor (per column)\nEDITOR_OPENED_ROLE = QtCore.Qt.UserRole + 6\n\n# Role for project model\nPROJECT_NAME_ROLE = QtCore.Qt.UserRole + 7\n\n# Allowed symbols for any name\nNAME_ALLOWED_SYMBOLS = \"a-zA-Z0-9_\"\nNAME_REGEX = re.compile(\"^[\" + NAME_ALLOWED_SYMBOLS + \"]*$\")\n"
  },
  {
    "path": "openpype/tools/project_manager/project_manager/delegates.py",
    "content": "from qtpy import QtWidgets, QtCore\n\nfrom .widgets import (\n    NameTextEdit,\n    FilterComboBox,\n    SpinBoxScrollFixed,\n    DoubleSpinBoxScrollFixed\n)\nfrom .multiselection_combobox import MultiSelectionComboBox\n\n\nclass ResizeEditorDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Implementation of private method from QStyledItemDelegate.\n\n    Force editor to resize into item size.\n    \"\"\"\n    @staticmethod\n    def _q_smart_min_size(editor):\n        min_size_hint = editor.minimumSizeHint()\n        size_policy = editor.sizePolicy()\n        width = 0\n        height = 0\n        if size_policy.horizontalPolicy() != QtWidgets.QSizePolicy.Ignored:\n            if (\n                size_policy.horizontalPolicy()\n                & QtWidgets.QSizePolicy.ShrinkFlag\n            ):\n                width = min_size_hint.width()\n            else:\n                width = max(\n                    editor.sizeHint().width(),\n                    min_size_hint.width()\n                )\n\n        if size_policy.verticalPolicy() != QtWidgets.QSizePolicy.Ignored:\n            if size_policy.verticalPolicy() & QtWidgets.QSizePolicy.ShrinkFlag:\n                height = min_size_hint.height()\n            else:\n                height = max(\n                    editor.sizeHint().height(),\n                    min_size_hint.height()\n                )\n\n        output = QtCore.QSize(width, height).boundedTo(editor.maximumSize())\n        min_size = editor.minimumSize()\n        if min_size.width() > 0:\n            output.setWidth(min_size.width())\n        if min_size.height() > 0:\n            output.setHeight(min_size.height())\n\n        return output.expandedTo(QtCore.QSize(0, 0))\n\n    def updateEditorGeometry(self, editor, option, index):\n        self.initStyleOption(option, index)\n\n        option.showDecorationSelected = editor.style().styleHint(\n            QtWidgets.QStyle.SH_ItemView_ShowDecorationSelected, None, editor\n        )\n\n        widget = option.widget\n\n        style = widget.style() if widget else QtWidgets.QApplication.style()\n        geo = style.subElementRect(\n            QtWidgets.QStyle.SE_ItemViewItemText, option, widget\n        )\n        delta = self._q_smart_min_size(editor).width() - geo.width()\n        if delta > 0:\n            if editor.layoutDirection() == QtCore.Qt.RightToLeft:\n                geo.adjust(-delta, 0, 0, 0)\n            else:\n                geo.adjust(0, 0, delta, 0)\n        editor.setGeometry(geo)\n\n\nclass NumberDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Delegate for number attributes.\n\n    Editor correspond passed arguments.\n\n    Args:\n        minimum(int, float): Minimum possible value.\n        maximum(int, float): Maximum possible value.\n        decimals(int): How many decimal points can be used. Float will be used\n            as value if is higher than 0.\n    \"\"\"\n    def __init__(self, minimum, maximum, decimals, step, *args, **kwargs):\n        super(NumberDelegate, self).__init__(*args, **kwargs)\n        self.minimum = minimum\n        self.maximum = maximum\n        self.decimals = decimals\n        self.step = step\n\n    def createEditor(self, parent, option, index):\n        if self.decimals > 0:\n            editor = DoubleSpinBoxScrollFixed(parent)\n            editor.setSingleStep(self.step)\n            editor.setDecimals(self.decimals)\n        else:\n            editor = SpinBoxScrollFixed(parent)\n\n        editor.setObjectName(\"NumberEditor\")\n        # Set min/max\n        editor.setMinimum(self.minimum)\n        editor.setMaximum(self.maximum)\n        # Hide spinbox buttons\n        editor.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n\n        # Try to set value from item\n        value = index.data(QtCore.Qt.EditRole)\n        if value is not None:\n            try:\n                if isinstance(value, str):\n                    value = float(value)\n                editor.setValue(value)\n\n            except Exception:\n                print(\"Couldn't set invalid value \\\"{}\\\"\".format(str(value)))\n\n        return editor\n\n\nclass NameDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Specific delegate for \"name\" key.\"\"\"\n\n    def createEditor(self, parent, option, index):\n        editor = NameTextEdit(parent)\n        editor.setObjectName(\"NameEditor\")\n        value = index.data(QtCore.Qt.EditRole)\n        if value is not None:\n            editor.setText(str(value))\n        return editor\n\n\nclass TypeDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Specific delegate for \"type\" key.\n\n    It is expected that will be used only for TaskItem which has modifiable\n    type. Type values are defined with cached project document.\n\n    Args:\n        project_doc_cache(ProjectDocCache): Project cache shared across all\n            delegates (kind of a struct pointer).\n    \"\"\"\n\n    def __init__(self, project_doc_cache, *args, **kwargs):\n        self._project_doc_cache = project_doc_cache\n        super(TypeDelegate, self).__init__(*args, **kwargs)\n\n    def createEditor(self, parent, option, index):\n        \"\"\"Editor is using filtrable combobox.\n\n        Editor should not be possible to create new items or set values that\n        are not in this method.\n        \"\"\"\n        editor = FilterComboBox(parent)\n        editor.setObjectName(\"TypeEditor\")\n        editor.style().polish(editor)\n        if not self._project_doc_cache.project_doc:\n            return editor\n\n        task_type_defs = self._project_doc_cache.project_doc[\"config\"][\"tasks\"]\n        editor.addItems(list(task_type_defs.keys()))\n\n        return editor\n\n    def setEditorData(self, editor, index):\n        value = index.data(QtCore.Qt.EditRole)\n        index = editor.findText(value)\n        if index >= 0:\n            editor.setCurrentIndex(index)\n\n    def setModelData(self, editor, model, index):\n        editor.value_cleanup()\n        super(TypeDelegate, self).setModelData(editor, model, index)\n\n\nclass ToolsDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Specific delegate for \"tools_env\" key.\n\n    Expected that editor will be used only on AssetItem which is the only item\n    that can have `tools_env` (except project).\n\n    Delegate requires tools cache which is shared across all ToolsDelegate\n    objects.\n\n    Args:\n        tools_cache (ToolsCache): Possible values of tools.\n    \"\"\"\n\n    def __init__(self, tools_cache, *args, **kwargs):\n        self._tools_cache = tools_cache\n        super(ToolsDelegate, self).__init__(*args, **kwargs)\n\n    def createEditor(self, parent, option, index):\n        editor = MultiSelectionComboBox(parent)\n        editor.setObjectName(\"ToolEditor\")\n        if not self._tools_cache.tools_data:\n            return editor\n\n        for key, label in self._tools_cache.tools_data:\n            editor.addItem(label, key)\n\n        return editor\n\n    def setEditorData(self, editor, index):\n        value = index.data(QtCore.Qt.EditRole)\n        editor.set_value(value)\n\n    def setModelData(self, editor, model, index):\n        model.setData(index, editor.value(), QtCore.Qt.EditRole)\n\n    def displayText(self, value, locale):\n        if value:\n            return \", \".join(value)\n        else:\n            return\n"
  },
  {
    "path": "openpype/tools/project_manager/project_manager/model.py",
    "content": "import collections\nimport copy\nimport json\nfrom uuid import uuid4\n\nfrom pymongo import UpdateOne, DeleteOne\n\nfrom qtpy import QtCore, QtGui\n\nfrom openpype.client import (\n    get_projects,\n    get_project,\n    get_assets,\n    get_asset_ids_with_subsets,\n)\nfrom openpype.client.operations import CURRENT_ASSET_DOC_SCHEMA\nfrom openpype.lib import Logger\n\nfrom .constants import (\n    IDENTIFIER_ROLE,\n    ITEM_TYPE_ROLE,\n    DUPLICATED_ROLE,\n    HIERARCHY_CHANGE_ABLE_ROLE,\n    REMOVED_ROLE,\n    EDITOR_OPENED_ROLE,\n    PROJECT_NAME_ROLE\n)\nfrom .style import ResourceCache\n\n\nclass ProjectModel(QtGui.QStandardItemModel):\n    \"\"\"Load possible projects to modify from MongoDB.\n\n    Mongo collection must contain project document with \"type\" \"project\" and\n    matching \"name\" value with name of collection.\n    \"\"\"\n\n    def __init__(self, dbcon, *args, **kwargs):\n        self.dbcon = dbcon\n\n        self._items_by_name = {}\n\n        super(ProjectModel, self).__init__(*args, **kwargs)\n\n    def refresh(self):\n        \"\"\"Reload projects.\"\"\"\n        self.dbcon.Session[\"AVALON_PROJECT\"] = None\n\n        new_project_items = []\n\n        if None not in self._items_by_name:\n            none_project = QtGui.QStandardItem(\"< Select Project >\")\n            self._items_by_name[None] = none_project\n            new_project_items.append(none_project)\n\n        project_names = set()\n        for project_doc in get_projects(fields=[\"name\"]):\n            project_name = project_doc.get(\"name\")\n            if not project_name:\n                continue\n\n            project_names.add(project_name)\n            if project_name not in self._items_by_name:\n                project_item = QtGui.QStandardItem(project_name)\n                project_item.setData(project_name, PROJECT_NAME_ROLE)\n\n                self._items_by_name[project_name] = project_item\n                new_project_items.append(project_item)\n\n        root_item = self.invisibleRootItem()\n        for project_name in tuple(self._items_by_name.keys()):\n            if project_name is None or project_name in project_names:\n                continue\n            project_item = self._items_by_name.pop(project_name)\n            root_item.removeRow(project_item.row())\n\n        if new_project_items:\n            root_item.appendRows(new_project_items)\n\n\nclass ProjectProxyFilter(QtCore.QSortFilterProxyModel):\n    \"\"\"Filters default project item.\"\"\"\n    def __init__(self, *args, **kwargs):\n        super(ProjectProxyFilter, self).__init__(*args, **kwargs)\n        self._filter_default = False\n\n    def lessThan(self, left, right):\n        if left.data(PROJECT_NAME_ROLE) is None:\n            return True\n        if right.data(PROJECT_NAME_ROLE) is None:\n            return False\n        return super(ProjectProxyFilter, self).lessThan(left, right)\n\n    def set_filter_default(self, enabled=True):\n        \"\"\"Set if filtering of default item is enabled.\"\"\"\n        if enabled == self._filter_default:\n            return\n        self._filter_default = enabled\n        self.invalidateFilter()\n\n    def filterAcceptsRow(self, row, parent):\n        if not self._filter_default:\n            return True\n\n        model = self.sourceModel()\n        source_index = model.index(row, self.filterKeyColumn(), parent)\n        return source_index.data(PROJECT_NAME_ROLE) is not None\n\n\nclass HierarchySelectionModel(QtCore.QItemSelectionModel):\n    \"\"\"Selection model with defined allowed multiselection columns.\n\n    This model allows to select multiple rows and enter one of their\n    editors to edit value of all selected rows.\n    \"\"\"\n\n    def __init__(self, multiselection_columns, *args, **kwargs):\n        super(HierarchySelectionModel, self).__init__(*args, **kwargs)\n        self.multiselection_columns = multiselection_columns\n\n    def setCurrentIndex(self, index, command):\n        if index.column() in self.multiselection_columns:\n            if (\n                command & QtCore.QItemSelectionModel.Clear\n                and command & QtCore.QItemSelectionModel.Select\n            ):\n                command = QtCore.QItemSelectionModel.NoUpdate\n        super(HierarchySelectionModel, self).setCurrentIndex(index, command)\n\n\nclass HierarchyModel(QtCore.QAbstractItemModel):\n    \"\"\"Main model for hierarchy modification and value changes.\n\n    Main part of ProjectManager.\n\n    Model should be able to load existing entities, create new, handle their\n    validations like name duplication and validate if is possible to save its\n    data.\n\n    Args:\n        dbcon (AvalonMongoDB): Connection to MongoDB with set AVALON_PROJECT in\n            its Session to current project.\n    \"\"\"\n\n    # Definition of all possible columns with their labels in default order\n    # - order is important as column names are used as keys for column indexes\n    _columns_def = [\n        (\"name\", \"Name\"),\n        (\"type\", \"Type\"),\n        (\"fps\", \"FPS\"),\n        (\"frameStart\", \"Frame start\"),\n        (\"frameEnd\", \"Frame end\"),\n        (\"handleStart\", \"Handle start\"),\n        (\"handleEnd\", \"Handle end\"),\n        (\"resolutionWidth\", \"Width\"),\n        (\"resolutionHeight\", \"Height\"),\n        (\"clipIn\", \"Clip in\"),\n        (\"clipOut\", \"Clip out\"),\n        (\"pixelAspect\", \"Pixel aspect\"),\n        (\"tools_env\", \"Tools\")\n    ]\n    # Columns allowing multiselection in edit mode\n    # - gives ability to set all of keys below on multiple items at once\n    multiselection_columns = {\n        \"frameStart\",\n        \"frameEnd\",\n        \"fps\",\n        \"resolutionWidth\",\n        \"resolutionHeight\",\n        \"handleStart\",\n        \"handleEnd\",\n        \"clipIn\",\n        \"clipOut\",\n        \"pixelAspect\",\n        \"tools_env\"\n    }\n    columns = [\n        item[0]\n        for item in _columns_def\n    ]\n    columns_len = len(columns)\n    column_labels = {\n        idx: item[1]\n        for idx, item in enumerate(_columns_def)\n    }\n\n    index_moved = QtCore.Signal(QtCore.QModelIndex)\n    project_changed = QtCore.Signal()\n\n    def __init__(self, dbcon, parent=None):\n        super(HierarchyModel, self).__init__(parent)\n\n        self.multiselection_column_indexes = {\n            self.columns.index(key)\n            for key in self.multiselection_columns\n        }\n\n        self._log = None\n        # TODO Reset them on project change\n        self._current_project = None\n        self._root_item = None\n        self._items_by_id = {}\n        self._asset_items_by_name = collections.defaultdict(set)\n        self.dbcon = dbcon\n\n        self._reset_root_item()\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(\"ProjectManagerModel\")\n        return self._log\n\n    @property\n    def items_by_id(self):\n        return self._items_by_id\n\n    def _reset_root_item(self):\n        \"\"\"Removes all previous content related to model.\"\"\"\n        self._root_item = RootItem(self)\n\n    def refresh_project(self):\n        \"\"\"Reload project data and discard unsaved changes.\"\"\"\n        self.set_project(self._current_project, True)\n\n    @property\n    def project_item(self):\n        \"\"\"Access to current project item.\n\n        Model can have 0-1 ProjectItems at once.\n        \"\"\"\n        output = None\n        for row in range(self._root_item.rowCount()):\n            item = self._root_item.child(row)\n            if isinstance(item, ProjectItem):\n                output = item\n                break\n        return output\n\n    def set_project(self, project_name, force=False):\n        \"\"\"Change project and discard unsaved changes.\n\n        Args:\n            project_name(str): New project name. Or None if just clearing\n                content.\n            force(bool): Force to change project even if project name is same\n                as current project.\n        \"\"\"\n        if self._current_project == project_name and not force:\n            return\n\n        # Reset attributes\n        self._items_by_id.clear()\n        self._asset_items_by_name.clear()\n\n        self.clear()\n\n        self._current_project = project_name\n\n        # Skip if project is None\n        if not project_name:\n            return\n\n        # Find project'd document\n        project_doc = get_project(\n            project_name,\n            fields=list(ProjectItem.query_projection.keys())\n        )\n\n        # Skip if project document does not exist\n        # - this shouldn't happen using only UI elements\n        if not project_doc:\n            return\n\n        # Create project item\n        project_item = ProjectItem(project_doc)\n        self.add_item(project_item)\n\n        # Query all assets of the project\n        asset_docs = get_assets(\n            project_name, fields=AssetItem.query_projection.keys()\n        )\n        asset_docs_by_id = {\n            asset_doc[\"_id\"]: asset_doc\n            for asset_doc in asset_docs\n        }\n\n        # Check if asset have published content and prepare booleans\n        #   if asset item can be modified (name and hierarchy change)\n        # - the same must be applied to all it's parents\n        asset_ids = list(asset_docs_by_id.keys())\n        asset_ids_with_subsets = []\n        if asset_ids:\n            asset_ids_with_subsets = get_asset_ids_with_subsets(\n                project_name, asset_ids=asset_ids\n            )\n\n        asset_modifiable = {\n            asset_id: asset_id not in asset_ids_with_subsets\n            for asset_id in asset_docs_by_id.keys()\n        }\n\n        # Store assets by their visual parent to be able create their hierarchy\n        asset_docs_by_parent_id = collections.defaultdict(list)\n        for asset_doc in asset_docs_by_id.values():\n            parent_id = asset_doc[\"data\"].get(\"visualParent\")\n            asset_docs_by_parent_id[parent_id].append(asset_doc)\n\n        appending_queue = collections.deque()\n        appending_queue.append((None, project_item))\n\n        asset_items_by_id = {}\n        non_modifiable_items = set()\n        while appending_queue:\n            parent_id, parent_item = appending_queue.popleft()\n            asset_docs = asset_docs_by_parent_id.get(parent_id) or []\n\n            new_items = []\n            for asset_doc in sorted(asset_docs, key=lambda item: item[\"name\"]):\n                # Create new Item\n                new_item = AssetItem(asset_doc)\n                # Store item to be added under parent in bulk\n                new_items.append(new_item)\n\n                # Store item by id for task processing\n                asset_id = asset_doc[\"_id\"]\n                if not asset_modifiable[asset_id]:\n                    non_modifiable_items.add(new_item.id)\n\n                asset_items_by_id[asset_id] = new_item\n                # Add item to appending queue\n                appending_queue.append((asset_id, new_item))\n\n            if new_items:\n                self.add_items(new_items, parent_item)\n\n        # Handle Asset's that are not modifiable\n        # - pass the information to all it's parents\n        non_modifiable_queue = collections.deque()\n        for item_id in non_modifiable_items:\n            non_modifiable_queue.append(item_id)\n\n        while non_modifiable_queue:\n            item_id = non_modifiable_queue.popleft()\n            item = self._items_by_id[item_id]\n            item.setData(False, HIERARCHY_CHANGE_ABLE_ROLE)\n\n            parent = item.parent()\n            if (\n                isinstance(parent, AssetItem)\n                and parent.id not in non_modifiable_items\n            ):\n                non_modifiable_items.add(parent.id)\n                non_modifiable_queue.append(parent.id)\n\n        # Add task items\n        for asset_id, asset_item in asset_items_by_id.items():\n            asset_doc = asset_docs_by_id[asset_id]\n            asset_tasks = asset_doc[\"data\"][\"tasks\"]\n            if not asset_tasks:\n                continue\n\n            task_items = []\n            for task_name in sorted(asset_tasks.keys()):\n                _task_data = copy.deepcopy(asset_tasks[task_name])\n                _task_data[\"name\"] = task_name\n                task_item = TaskItem(_task_data)\n                task_items.append(task_item)\n\n            self.add_items(task_items, asset_item)\n\n        # Emit that project was successfully changed\n        self.project_changed.emit()\n\n    def rowCount(self, parent=None):\n        \"\"\"Number of rows for passed parent.\"\"\"\n        if parent is None or not parent.isValid():\n            parent_item = self._root_item\n        else:\n            parent_item = parent.internalPointer()\n        return parent_item.rowCount()\n\n    def columnCount(self, *args, **kwargs):\n        \"\"\"Number of columns is static for this model.\"\"\"\n        return self.columns_len\n\n    def data(self, index, role):\n        \"\"\"Access data for passed index and it's role.\n\n        Model is using principles implemented in BaseItem so converts passed\n        index column into key and ask item to return value for passed role.\n        \"\"\"\n        if not index.isValid():\n            return None\n\n        column = index.column()\n        key = self.columns[column]\n\n        item = index.internalPointer()\n        return item.data(role, key)\n\n    def setData(self, index, value, role=QtCore.Qt.EditRole):\n        \"\"\"Store data to passed index under role.\n\n        Pass values to corresponding item and behave by it's result.\n        \"\"\"\n        if not index.isValid():\n            return False\n\n        item = index.internalPointer()\n        column = index.column()\n        key = self.columns[column]\n        # Capture asset name changes for duplcated asset names validation.\n        if (\n            key == \"name\"\n            and role in (QtCore.Qt.EditRole, QtCore.Qt.DisplayRole)\n        ):\n            self._rename_asset(item, value)\n\n        # Pass values to item and by result emi dataChanged signal or not\n        result = item.setData(value, role, key)\n        if result:\n            self.dataChanged.emit(index, index, [role])\n\n        return result\n\n    def headerData(self, section, orientation, role):\n        \"\"\"Header labels.\"\"\"\n        if role == QtCore.Qt.DisplayRole:\n            if section < self.columnCount():\n                return self.column_labels[section]\n\n        return super(HierarchyModel, self).headerData(\n            section, orientation, role\n        )\n\n    def flags(self, index):\n        \"\"\"Index flags are defined by corresponding item.\"\"\"\n        item = index.internalPointer()\n        if item is None:\n            return QtCore.Qt.NoItemFlags\n        column = index.column()\n        key = self.columns[column]\n        return item.flags(key)\n\n    def parent(self, index=None):\n        \"\"\"Parent for passed index as QModelIndex.\n\n        Args:\n            index(QModelIndex): Parent index. Root item is used if not passed.\n        \"\"\"\n        if not index.isValid():\n            return QtCore.QModelIndex()\n\n        item = index.internalPointer()\n        parent_item = item.parent()\n\n        # If it has no parents we return invalid\n        if not parent_item or parent_item is self._root_item:\n            return QtCore.QModelIndex()\n\n        return self.createIndex(parent_item.row(), 0, parent_item)\n\n    def index(self, row, column, parent=None):\n        \"\"\"Return index for row/column under parent.\n\n        Args:\n            row(int): Row number.\n            column(int): Column number.\n            parent(QModelIndex): Parent index. Root item is used if not passed.\n        \"\"\"\n        parent_item = None\n        if parent is not None and parent.isValid():\n            parent_item = parent.internalPointer()\n\n        return self.index_from_item(row, column, parent_item)\n\n    def index_for_item(self, item, column=0):\n        \"\"\"Index for passed item.\n\n        This is for cases that index operations are required on specific item.\n\n        Args:\n            item(BaseItem): Item from model that will be converted to\n                corresponding QModelIndex.\n            column(int): Which column will be part of returned index. By\n                default is used column 0.\n        \"\"\"\n        return self.index_from_item(\n            item.row(), column, item.parent()\n        )\n\n    def index_from_item(self, row, column, parent=None):\n        \"\"\"Index for passed row, column and parent item.\n\n        Same implementation as `index` method but \"parent\" is one of\n        BaseItem objects instead of QModelIndex.\n\n        Args:\n            row(int): Row number.\n            column(int): Column number.\n            parent(BaseItem): Parent item. Root item is used if not passed.\n        \"\"\"\n        if parent is None:\n            parent = self._root_item\n\n        child_item = parent.child(row)\n        if child_item:\n            return self.createIndex(row, column, child_item)\n\n        return QtCore.QModelIndex()\n\n    def add_new_asset(self, source_index):\n        \"\"\"Create new asset item in hierarchy.\n\n        Args:\n            source_index(QModelIndex): Parent under which new asset will be\n                added.\n        \"\"\"\n        item_id = source_index.data(IDENTIFIER_ROLE)\n        item = self.items_by_id[item_id]\n\n        if isinstance(item, TaskItem):\n            item = item.parent()\n\n        if isinstance(item, (RootItem, ProjectItem)):\n            name = \"ep\"\n            new_row = None\n        elif isinstance(item, AssetItem):\n            name = None\n            new_row = item.rowCount()\n        else:\n            return\n\n        asset_data = {}\n        if name:\n            asset_data[\"name\"] = name\n\n        new_child = AssetItem(asset_data)\n\n        result = self.add_item(new_child, item, new_row)\n        if result is not None:\n            # WARNING Expecting result is index for column 0 (\"name\")\n            new_name = result.data(QtCore.Qt.EditRole)\n            self._validate_asset_duplicity(new_name)\n\n        return result\n\n    def add_new_task(self, parent_index):\n        \"\"\"Create new TaskItem under passed parent index or it's parent.\n\n        Args:\n            parent_index(QModelIndex): Index of parent AssetItem under which\n                will be task added. If index represents TaskItem it's parent is\n                used as parent.\n        \"\"\"\n        item_id = parent_index.data(IDENTIFIER_ROLE)\n        item = self.items_by_id[item_id]\n\n        if isinstance(item, TaskItem):\n            parent = item.parent()\n        else:\n            parent = item\n\n        if not isinstance(parent, AssetItem):\n            return None\n\n        new_child = TaskItem()\n        return self.add_item(new_child, parent)\n\n    def add_items(self, items, parent=None, start_row=None):\n        \"\"\"Add new items with definition of QAbstractItemModel.\n\n        Trigger `beginInsertRows` and `endInsertRows` to trigger proper\n        callbacks in view or proxy model.\n\n        Args:\n            items(list[BaseItem]): List of item that will be inserted in model.\n            parent(RootItem, ProjectItem, AssetItem): Parent of items under\n                which will be items added. Root item is used if not passed.\n            start_row(int): Define to which row will be items added. Next\n                available row of parent is used if not passed.\n        \"\"\"\n        if parent is None:\n            parent = self._root_item\n\n        if parent.data(REMOVED_ROLE):\n            return []\n\n        if start_row is None:\n            start_row = parent.rowCount()\n\n        end_row = start_row + len(items) - 1\n\n        parent_index = self.index_from_item(parent.row(), 0, parent.parent())\n\n        self.beginInsertRows(parent_index, start_row, end_row)\n\n        for idx, item in enumerate(items):\n            row = start_row + idx\n            if item.parent() is not parent:\n                item.set_parent(parent)\n\n            parent.add_child(item, row)\n\n            if isinstance(item, AssetItem):\n                name = item.data(QtCore.Qt.EditRole, \"name\")\n                self._asset_items_by_name[name].add(item.id)\n\n            if item.id not in self._items_by_id:\n                self._items_by_id[item.id] = item\n\n        self.endInsertRows()\n\n        indexes = []\n        for row in range(start_row, end_row + 1):\n            indexes.append(\n                self.index_from_item(row, 0, parent)\n            )\n        return indexes\n\n    def add_item(self, item, parent=None, row=None):\n        \"\"\"Add single item into model.\"\"\"\n        result = self.add_items([item], parent, row)\n        if result:\n            return result[0]\n        return None\n\n    def remove_delete_flag(self, item_ids, with_children=True):\n        \"\"\"Remove deletion flag from items with matching ids.\n\n        The flag is also removed from all parents of passed children as it\n        wouldn't make sense to not to do so.\n\n        Children of passed item ids are by default also unset for deletion.\n\n        Args:\n            list(uuid4): Ids of model items where remove flag should be unset.\n            with_children(bool): Unset remove flag also on all children of\n                passed items.\n        \"\"\"\n        items_by_id = {}\n        for item_id in item_ids:\n            if item_id in items_by_id:\n                continue\n\n            item = self.items_by_id[item_id]\n            if isinstance(item, (AssetItem, TaskItem)):\n                items_by_id[item_id] = item\n\n        for item in tuple(items_by_id.values()):\n            parent = item.parent()\n            while True:\n                if not isinstance(parent, (AssetItem, TaskItem)):\n                    break\n\n                if parent.id not in items_by_id:\n                    items_by_id[parent.id] = parent\n\n                parent = parent.parent()\n\n            if not with_children:\n                continue\n\n            def _children_recursion(_item):\n                if not isinstance(_item, AssetItem):\n                    return\n\n                for row in range(_item.rowCount()):\n                    _child_item = _item.child(row)\n                    if _child_item.id in items_by_id:\n                        continue\n\n                    items_by_id[_child_item.id] = _child_item\n                    _children_recursion(_child_item)\n\n            _children_recursion(item)\n\n        for item in items_by_id.values():\n            if item.data(REMOVED_ROLE):\n                item.setData(False, REMOVED_ROLE)\n                if isinstance(item, AssetItem):\n                    name = item.data(QtCore.Qt.EditRole, \"name\")\n                    self._asset_items_by_name[name].add(item.id)\n                    self._validate_asset_duplicity(name)\n\n    def delete_index(self, index):\n        \"\"\"Delete item of the index from model.\"\"\"\n        return self.delete_indexes([index])\n\n    def delete_indexes(self, indexes):\n        \"\"\"Delete items from model.\"\"\"\n        items_by_id = {}\n        processed_ids = set()\n        for index in indexes:\n            if not index.isValid():\n                continue\n\n            item_id = index.data(IDENTIFIER_ROLE)\n            # There may be indexes for multiple columns\n            if item_id not in processed_ids:\n                processed_ids.add(item_id)\n\n                item = self._items_by_id[item_id]\n                if isinstance(item, (TaskItem, AssetItem)):\n                    items_by_id[item_id] = item\n\n        if not items_by_id:\n            return\n\n        for item in items_by_id.values():\n            self._remove_item(item)\n\n    def _remove_item(self, item):\n        \"\"\"Remove item from model or mark item for deletion.\n\n        Deleted items are using definition of QAbstractItemModel which call\n        `beginRemoveRows` and `endRemoveRows` to trigger proper view and proxy\n        model callbacks.\n\n        Item is not just removed but is checked if can be removed from model or\n        just mark it for deletion for save.\n\n        First of all will find all related children and based on their\n        attributes define if can be removed.\n        \"\"\"\n        # Skip if item is already marked for deletion\n        is_removed = item.data(REMOVED_ROLE)\n        if is_removed:\n            return\n\n        parent = item.parent()\n\n        # Find all descendants and store them by parent id\n        all_descendants = collections.defaultdict(dict)\n        all_descendants[parent.id][item.id] = item\n\n        def _fill_children(_all_descendants, cur_item, parent_item=None):\n            if parent_item is not None:\n                _all_descendants[parent_item.id][cur_item.id] = cur_item\n\n            if isinstance(cur_item, TaskItem):\n                was_removed = cur_item.data(REMOVED_ROLE)\n                task_removed = True\n                if not was_removed and parent_item is not None:\n                    task_removed = parent_item.data(REMOVED_ROLE)\n                if not was_removed:\n                    cur_item.setData(task_removed, REMOVED_ROLE)\n                return task_removed\n\n            remove_item = True\n            task_children = []\n            for row in range(cur_item.rowCount()):\n                child_item = cur_item.child(row)\n                if isinstance(child_item, TaskItem):\n                    task_children.append(child_item)\n                    continue\n\n                if not _fill_children(_all_descendants, child_item, cur_item):\n                    remove_item = False\n\n            if remove_item:\n                cur_item.setData(True, REMOVED_ROLE)\n                if isinstance(cur_item, AssetItem):\n                    self._rename_asset(cur_item, None)\n\n            # Process tasks as last because their logic is based on parent\n            # - tasks may be processed before parent check all asset children\n            for task_item in task_children:\n                _fill_children(_all_descendants, task_item, cur_item)\n            return remove_item\n\n        _fill_children(all_descendants, item)\n\n        modified_children = []\n        while all_descendants:\n            for parent_id in tuple(all_descendants.keys()):\n                children = all_descendants[parent_id]\n                if not children:\n                    all_descendants.pop(parent_id)\n                    continue\n\n                parent_children = {}\n                all_without_children = True\n                for child_id in tuple(children.keys()):\n                    if child_id in all_descendants:\n                        all_without_children = False\n                        break\n                    parent_children[child_id] = children[child_id]\n\n                if not all_without_children:\n                    continue\n\n                # Row ranges of items to remove\n                # - store tuples of row \"start\", \"end\" (can be the same)\n                row_ranges = []\n                # Predefine start, end variables\n                start_row = end_row = None\n                chilren_by_row = {}\n                parent_item = self._items_by_id[parent_id]\n                for row in range(parent_item.rowCount()):\n                    child_item = parent_item.child(row)\n                    child_id = child_item.id\n                    # Not sure if this can happen\n                    # TODO validate this line it seems dangerous as start/end\n                    #   row is not changed\n                    if child_id not in children:\n                        continue\n\n                    chilren_by_row[row] = child_item\n                    children.pop(child_item.id)\n\n                    removed_mark = child_item.data(REMOVED_ROLE)\n                    if not removed_mark or not child_item.is_new:\n                        # Skip row sequence store child for later processing\n                        #   and store current start/end row range\n                        modified_children.append(child_item)\n                        if end_row is not None:\n                            row_ranges.append((start_row, end_row))\n                        start_row = end_row = None\n                        continue\n\n                    end_row = row\n                    if start_row is None:\n                        start_row = row\n\n                if end_row is not None:\n                    row_ranges.append((start_row, end_row))\n\n                if not row_ranges:\n                    continue\n\n                # Remove items from model\n                parent_index = self.index_for_item(parent_item)\n                for start, end in row_ranges:\n                    self.beginRemoveRows(parent_index, start, end)\n\n                    for idx in range(start, end + 1):\n                        child_item = chilren_by_row[idx]\n                        # Force name validation\n                        if isinstance(child_item, AssetItem):\n                            self._rename_asset(child_item, None)\n                        child_item.set_parent(None)\n                        self._items_by_id.pop(child_item.id)\n\n                    self.endRemoveRows()\n\n        # Trigger data change to repaint items\n        # - `BackgroundRole` is random role without any specific reason\n        for item in modified_children:\n            s_index = self.index_for_item(item)\n            e_index = self.index_for_item(item, column=self.columns_len - 1)\n            self.dataChanged.emit(s_index, e_index, [QtCore.Qt.BackgroundRole])\n\n    def _rename_asset(self, asset_item, new_name):\n        if not isinstance(asset_item, AssetItem):\n            return\n\n        prev_name = asset_item.data(QtCore.Qt.EditRole, \"name\")\n        if prev_name == new_name:\n            return\n\n        if asset_item.id in self._asset_items_by_name[prev_name]:\n            self._asset_items_by_name[prev_name].remove(asset_item.id)\n\n        self._validate_asset_duplicity(prev_name)\n\n        if new_name is None:\n            return\n        self._asset_items_by_name[new_name].add(asset_item.id)\n\n        self._validate_asset_duplicity(new_name)\n\n    def _validate_asset_duplicity(self, name):\n        if name not in self._asset_items_by_name:\n            return\n\n        item_ids = self._asset_items_by_name[name]\n        if not item_ids:\n            self._asset_items_by_name.pop(name)\n\n        elif len(item_ids) == 1:\n            for item_id in item_ids:\n                item = self._items_by_id[item_id]\n            index = self.index_for_item(item)\n            self.setData(index, False, DUPLICATED_ROLE)\n\n        else:\n            for item_id in item_ids:\n                item = self._items_by_id[item_id]\n                index = self.index_for_item(item)\n                self.setData(index, True, DUPLICATED_ROLE)\n\n    def _move_horizontal_single(self, index, direction):\n        if not index.isValid():\n            return\n\n        item_id = index.data(IDENTIFIER_ROLE)\n        if item_id is None:\n            return\n\n        item = self._items_by_id[item_id]\n        if isinstance(item, (RootItem, ProjectItem)):\n            return\n\n        if item.data(REMOVED_ROLE):\n            return\n\n        if (\n            isinstance(item, AssetItem)\n            and not item.data(HIERARCHY_CHANGE_ABLE_ROLE)\n        ):\n            return\n\n        if abs(direction) != 1:\n            return\n\n        # Move under parent of parent\n        src_row = item.row()\n        src_parent = item.parent()\n        src_parent_index = self.index_from_item(\n            src_parent.row(), 0, src_parent.parent()\n        )\n\n        dst_row = None\n        dst_parent = None\n\n        if direction == -1:\n            if isinstance(src_parent, (RootItem, ProjectItem)):\n                return\n            dst_parent = src_parent.parent()\n            dst_row = src_parent.row() + 1\n\n        # Move under parent before or after if before is None\n        elif direction == 1:\n            src_row_count = src_parent.rowCount()\n            if src_row_count == 1:\n                return\n\n            item_row = item.row()\n            dst_parent = None\n            for row in reversed(range(item_row)):\n                _item = src_parent.child(row)\n                if not isinstance(_item, AssetItem):\n                    continue\n\n                if _item.data(REMOVED_ROLE):\n                    continue\n\n                dst_parent = _item\n                break\n\n            _next_row = item_row + 1\n            if dst_parent is None and _next_row < src_row_count:\n                for row in range(_next_row, src_row_count):\n                    _item = src_parent.child(row)\n                    if not isinstance(_item, AssetItem):\n                        continue\n\n                    if _item.data(REMOVED_ROLE):\n                        continue\n\n                    dst_parent = _item\n                    break\n\n            if dst_parent is None:\n                return\n\n            dst_row = dst_parent.rowCount()\n\n        if src_parent is dst_parent:\n            return\n\n        if (\n            isinstance(item, TaskItem)\n            and not isinstance(dst_parent, AssetItem)\n        ):\n            return\n\n        dst_parent_index = self.index_from_item(\n            dst_parent.row(), 0, dst_parent.parent()\n        )\n\n        self.beginMoveRows(\n            src_parent_index,\n            src_row,\n            src_row,\n            dst_parent_index,\n            dst_row\n        )\n        src_parent.remove_child(item)\n        dst_parent.add_child(item)\n        item.set_parent(dst_parent)\n        dst_parent.move_to(item, dst_row)\n\n        self.endMoveRows()\n\n        new_index = self.index(dst_row, index.column(), dst_parent_index)\n        self.index_moved.emit(new_index)\n\n    def move_horizontal(self, indexes, direction):\n        if not indexes:\n            return\n\n        if isinstance(indexes, QtCore.QModelIndex):\n            indexes = [indexes]\n\n        if len(indexes) == 1:\n            self._move_horizontal_single(indexes[0], direction)\n            return\n\n        items_by_id = {}\n        for index in indexes:\n            item_id = index.data(IDENTIFIER_ROLE)\n            item = self._items_by_id[item_id]\n            if isinstance(item, (RootItem, ProjectItem)):\n                continue\n\n            if (\n                direction == -1\n                and isinstance(item.parent(), (RootItem, ProjectItem))\n            ):\n                continue\n\n            items_by_id[item_id] = item\n\n        if not items_by_id:\n            return\n\n        parents_by_id = {}\n        items_ids_by_parent_id = collections.defaultdict(set)\n        skip_ids = set(items_by_id.keys())\n        for item_id, item in tuple(items_by_id.items()):\n            item_parent = item.parent()\n\n            parent_ids = set()\n            skip_item = False\n            parent = item_parent\n            while parent is not None:\n                if parent.id in skip_ids:\n                    skip_item = True\n                    skip_ids |= parent_ids\n                    break\n                parent_ids.add(parent.id)\n                parent = parent.parent()\n\n            if skip_item:\n                items_by_id.pop(item_id)\n            else:\n                parents_by_id[item_parent.id] = item_parent\n                items_ids_by_parent_id[item_parent.id].add(item_id)\n\n        if direction == 1:\n            for parent_id, parent in parents_by_id.items():\n                items_ids = items_ids_by_parent_id[parent_id]\n                if len(items_ids) == parent.rowCount():\n                    for item_id in items_ids:\n                        items_by_id.pop(item_id)\n\n        items = tuple(items_by_id.values())\n        if direction == -1:\n            items = reversed(items)\n\n        for item in items:\n            index = self.index_for_item(item)\n            self._move_horizontal_single(index, direction)\n\n    def _move_vertical_single(self, index, direction):\n        if not index.isValid():\n            return\n\n        item_id = index.data(IDENTIFIER_ROLE)\n        item = self._items_by_id[item_id]\n        if isinstance(item, (RootItem, ProjectItem)):\n            return\n\n        if item.data(REMOVED_ROLE):\n            return\n\n        if (\n            isinstance(item, AssetItem)\n            and not item.data(HIERARCHY_CHANGE_ABLE_ROLE)\n        ):\n            return\n\n        if abs(direction) != 1:\n            return\n\n        src_parent = item.parent()\n        if not isinstance(src_parent, AssetItem):\n            return\n\n        src_parent_index = self.index_from_item(\n            src_parent.row(), 0, src_parent.parent()\n        )\n        source_row = item.row()\n\n        parent_items = []\n        parent = src_parent\n        while True:\n            parent = parent.parent()\n            parent_items.insert(0, parent)\n            if isinstance(parent, ProjectItem):\n                break\n\n        dst_parent = None\n        # Down\n        if direction == 1:\n            current_idxs = []\n            current_max_idxs = []\n            for parent_item in parent_items:\n                current_max_idxs.append(parent_item.rowCount())\n                if not isinstance(parent_item, ProjectItem):\n                    current_idxs.append(parent_item.row())\n            current_idxs.append(src_parent.row())\n            indexes_len = len(current_idxs)\n\n            while True:\n                def _update_parents(idx, top=True):\n                    if idx < 0:\n                        return False\n\n                    if current_max_idxs[idx] == current_idxs[idx]:\n                        if not _update_parents(idx - 1, False):\n                            return False\n\n                        parent = parent_items[idx]\n                        row_count = 0\n                        if parent is not None:\n                            row_count = parent.rowCount()\n                        current_max_idxs[idx] = row_count\n                        current_idxs[idx] = 0\n                        return True\n\n                    if top:\n                        return True\n\n                    current_idxs[idx] += 1\n                    parent_item = parent_items[idx]\n                    new_item = parent_item.child(current_idxs[idx])\n                    parent_items[idx + 1] = new_item\n\n                    return True\n\n                updated = _update_parents(indexes_len - 1)\n                if not updated:\n                    return\n\n                start = current_idxs[-1]\n                end = current_max_idxs[-1]\n                current_idxs[-1] = current_max_idxs[-1]\n                parent = parent_items[-1]\n                for row in range(start, end):\n                    child_item = parent.child(row)\n                    if (\n                        child_item is src_parent\n                        or child_item.data(REMOVED_ROLE)\n                        or not isinstance(child_item, AssetItem)\n                    ):\n                        continue\n\n                    dst_parent = child_item\n                    destination_row = 0\n                    break\n\n                if dst_parent is not None:\n                    break\n\n        # Up\n        elif direction == -1:\n            current_idxs = []\n            for parent_item in parent_items:\n                if not isinstance(parent_item, ProjectItem):\n                    current_idxs.append(parent_item.row())\n            current_idxs.append(src_parent.row())\n\n            max_idxs = [0 for _ in current_idxs]\n            indexes_len = len(current_idxs)\n\n            while True:\n                if current_idxs == max_idxs:\n                    return\n\n                def _update_parents(_current_idx, top=True):\n                    if _current_idx < 0:\n                        return False\n\n                    if current_idxs[_current_idx] == 0:\n                        if not _update_parents(_current_idx - 1, False):\n                            return False\n\n                        parent = parent_items[_current_idx]\n                        row_count = 0\n                        if parent is not None:\n                            row_count = parent.rowCount()\n                        current_idxs[_current_idx] = row_count\n                        return True\n                    if top:\n                        return True\n\n                    current_idxs[_current_idx] -= 1\n                    parent_item = parent_items[_current_idx]\n                    new_item = parent_item.child(current_idxs[_current_idx])\n                    parent_items[_current_idx + 1] = new_item\n\n                    return True\n\n                updated = _update_parents(indexes_len - 1)\n                if not updated:\n                    return\n\n                parent_item = parent_items[-1]\n                row_count = current_idxs[-1]\n                current_idxs[-1] = 0\n                for row in reversed(range(row_count)):\n                    child_item = parent_item.child(row)\n                    if (\n                        child_item is src_parent\n                        or child_item.data(REMOVED_ROLE)\n                        or not isinstance(child_item, AssetItem)\n                    ):\n                        continue\n\n                    dst_parent = child_item\n                    destination_row = dst_parent.rowCount()\n                    break\n\n                if dst_parent is not None:\n                    break\n\n        if dst_parent is None:\n            return\n\n        dst_parent_index = self.index_from_item(\n            dst_parent.row(), 0, dst_parent.parent()\n        )\n\n        self.beginMoveRows(\n            src_parent_index,\n            source_row,\n            source_row,\n            dst_parent_index,\n            destination_row\n        )\n\n        if src_parent is dst_parent:\n            dst_parent.move_to(item, destination_row)\n\n        else:\n            src_parent.remove_child(item)\n            dst_parent.add_child(item)\n            item.set_parent(dst_parent)\n            dst_parent.move_to(item, destination_row)\n\n        self.endMoveRows()\n\n        new_index = self.index(\n            destination_row, index.column(), dst_parent_index\n        )\n        self.index_moved.emit(new_index)\n\n    def move_vertical(self, indexes, direction):\n        \"\"\"Move item vertically in model to matching parent if possible.\n\n        If passed indexes contain items that has parent<->child relation at any\n        hierarchy level only the top parent is actually moved.\n\n        Example (items marked with star are passed in `indexes`):\n        - shots*\n            - ep01\n                - ep01_sh0010*\n                - ep01_sh0020*\n        In this case only `shots` item will be moved vertically and\n        both \"ep01_sh0010\" \"ep01_sh0020\" will stay as children of \"ep01\".\n\n        Args:\n            indexes(list[QModelIndex]): Indexes that should be moved\n                vertically.\n            direction(int): Which way will be moved -1 or 1 to determine.\n        \"\"\"\n        if not indexes:\n            return\n\n        # Convert single index to list of indexes\n        if isinstance(indexes, QtCore.QModelIndex):\n            indexes = [indexes]\n\n        # Just process single index\n        if len(indexes) == 1:\n            self._move_vertical_single(indexes[0], direction)\n            return\n\n        items_by_id = {}\n        for index in indexes:\n            item_id = index.data(IDENTIFIER_ROLE)\n            items_by_id[item_id] = self._items_by_id[item_id]\n\n        skip_ids = set(items_by_id.keys())\n        for item_id, item in tuple(items_by_id.items()):\n            parent = item.parent()\n            parent_ids = set()\n            skip_item = False\n            while parent is not None:\n                if parent.id in skip_ids:\n                    skip_item = True\n                    skip_ids |= parent_ids\n                    break\n                parent_ids.add(parent.id)\n                parent = parent.parent()\n\n            if skip_item:\n                items_by_id.pop(item_id)\n\n        items = tuple(items_by_id.values())\n        if direction == 1:\n            items = reversed(items)\n\n        for item in items:\n            index = self.index_for_item(item)\n            self._move_vertical_single(index, direction)\n\n    def child_removed(self, child):\n        \"\"\"Callback for removed child.\"\"\"\n        self._items_by_id.pop(child.id, None)\n\n    def column_name(self, column):\n        \"\"\"Return column key by index\"\"\"\n        if column < len(self.columns):\n            return self.columns[column]\n        return None\n\n    def clear(self):\n        \"\"\"Reset model.\"\"\"\n        self.beginResetModel()\n        self._reset_root_item()\n        self.endResetModel()\n\n    def save(self):\n        \"\"\"Save all changes from current project manager session.\n\n        Will create new asset documents, update existing and asset documents\n        marked for deletion are removed from mongo if has published content or\n        their type is changed to `archived_asset` to not loose their data.\n        \"\"\"\n        # Check if all items are valid before save\n        all_valid = True\n        for item in self._items_by_id.values():\n            if not item.is_valid:\n                all_valid = False\n                break\n\n        if not all_valid:\n            return\n\n        # Check project item and do not save without it\n        project_item = None\n        for _project_item in self._root_item.children():\n            project_item = _project_item\n\n        if not project_item:\n            return\n\n        project_name = project_item.name\n        project_col = self.dbcon.database[project_name]\n\n        # Process asset items per one hierarchical level.\n        # - new assets are inserted per one parent\n        # - update and delete data are stored and processed at once at the end\n        to_process = collections.deque()\n        to_process.append(project_item)\n\n        created_count = 0\n        updated_count = 0\n        removed_count = 0\n        bulk_writes = []\n        while to_process:\n            parent = to_process.popleft()\n            insert_list = []\n            for item in parent.children():\n                if not isinstance(item, AssetItem):\n                    continue\n\n                to_process.append(item)\n\n                if item.is_new:\n                    insert_list.append(item)\n\n                elif item.data(REMOVED_ROLE):\n                    removed_count += 1\n                    if item.data(HIERARCHY_CHANGE_ABLE_ROLE):\n                        bulk_writes.append(DeleteOne(\n                            {\"_id\": item.asset_id}\n                        ))\n                    else:\n                        bulk_writes.append(UpdateOne(\n                            {\"_id\": item.asset_id},\n                            {\"$set\": {\"type\": \"archived_asset\"}}\n                        ))\n\n                else:\n                    update_data = item.update_data()\n                    if update_data:\n                        updated_count += 1\n                        bulk_writes.append(UpdateOne(\n                            {\"_id\": item.asset_id},\n                            update_data\n                        ))\n\n            if insert_list:\n                new_docs = []\n                for item in insert_list:\n                    new_docs.append(item.to_doc())\n\n                result = project_col.insert_many(new_docs)\n                for idx, mongo_id in enumerate(result.inserted_ids):\n                    created_count += 1\n                    insert_list[idx].mongo_id = mongo_id\n\n        if sum([created_count, updated_count, removed_count]) == 0:\n            self.log.info(\"Nothing has changed\")\n            return\n\n        if bulk_writes:\n            project_col.bulk_write(bulk_writes)\n\n        self.log.info((\n            \"Save finished.\"\n            \" Created {} | Updated {} | Removed {} asset documents\"\n        ).format(created_count, updated_count, removed_count))\n\n        self.refresh_project()\n\n    def copy_mime_data(self, indexes):\n        items = []\n        processed_ids = set()\n        for index in indexes:\n            if not index.isValid():\n                continue\n            item_id = index.data(IDENTIFIER_ROLE)\n            if item_id in processed_ids:\n                continue\n            processed_ids.add(item_id)\n            item = self._items_by_id[item_id]\n            items.append(item)\n\n        parent_item = None\n        for item in items:\n            if not isinstance(item, TaskItem):\n                raise ValueError(\"Can copy only tasks\")\n\n            if parent_item is None:\n                parent_item = item.parent()\n            elif item.parent() is not parent_item:\n                raise ValueError(\"Can copy only tasks from same parent\")\n\n        data = []\n        for task_item in items:\n            data.append(task_item.to_json_data())\n\n        encoded_data = QtCore.QByteArray()\n        stream = QtCore.QDataStream(encoded_data, QtCore.QIODevice.WriteOnly)\n        stream.writeQString(json.dumps(data))\n        mimedata = QtCore.QMimeData()\n        mimedata.setData(\"application/copy_task\", encoded_data)\n        return mimedata\n\n    def _paste_mime_data(self, item, mime_data):\n        if not isinstance(item, (AssetItem, TaskItem)):\n            return\n\n        raw_data = mime_data.data(\"application/copy_task\")\n        if isinstance(raw_data, QtCore.QByteArray):\n            # Raw data are already QByteArrat and we don't have to load them\n            encoded_data = raw_data\n        else:\n            encoded_data = QtCore.QByteArray.fromRawData(raw_data)\n        stream = QtCore.QDataStream(encoded_data, QtCore.QIODevice.ReadOnly)\n        text = stream.readQString()\n        try:\n            data = json.loads(text)\n        except Exception:\n            data = []\n\n        if not data:\n            return\n\n        if isinstance(item, TaskItem):\n            parent = item.parent()\n        else:\n            parent = item\n\n        for task_item_data in data:\n            task_data = {}\n            for name, data in task_item_data.items():\n                task_data = data\n                task_data[\"name\"] = name\n\n            task_item = TaskItem(task_data, True)\n            self.add_item(task_item, parent)\n\n    def paste(self, indexes, mime_data):\n\n        # Get the selected Assets uniquely\n        items = set()\n        for index in indexes:\n            if not index.isValid():\n                return\n            item_id = index.data(IDENTIFIER_ROLE)\n            item = self._items_by_id[item_id]\n\n            # Do not copy into the Task Item so get parent Asset instead\n            if isinstance(item, TaskItem):\n                item = item.parent()\n\n            items.add(item)\n\n        for item in items:\n            self._paste_mime_data(item, mime_data)\n\n\nclass BaseItem:\n    \"\"\"Base item for HierarchyModel.\n\n    Is not meant to be used as real item but as superclass for all items used\n    in HierarchyModel.\n\n    TODO cleanup some attributes and methods related only to AssetItem and\n    TaskItem.\n    \"\"\"\n    columns = []\n    # Use `set` for faster result\n    editable_columns = set()\n\n    _name_icons = None\n    _is_duplicated = False\n    item_type = \"base\"\n\n    _None = object()\n\n    def __init__(self, data=None):\n        self._id = uuid4()\n        self._children = list()\n        self._parent = None\n\n        self._data = {\n            key: None\n            for key in self.columns\n        }\n        self._global_data = {}\n        self._source_data = data\n        if data:\n            for key, value in data.items():\n                if key in self.columns:\n                    self._data[key] = value\n\n    def name_icon(self):\n        \"\"\"Icon shown next to name.\n\n        Item must imlpement this method to change it.\n        \"\"\"\n        return None\n\n    @property\n    def is_valid(self):\n        return not self._is_duplicated\n\n    def model(self):\n        return self._parent.model()\n\n    def move_to(self, item, row):\n        idx = self._children.index(item)\n        if idx == row:\n            return\n\n        self._children.pop(idx)\n        self._children.insert(row, item)\n\n    def _get_global_data(self, role):\n        \"\"\"Global data getter without column specification.\"\"\"\n        if role == ITEM_TYPE_ROLE:\n            return self.item_type\n\n        if role == IDENTIFIER_ROLE:\n            return self._id\n\n        if role == DUPLICATED_ROLE:\n            return self._is_duplicated\n\n        if role == REMOVED_ROLE:\n            return False\n\n        return self._global_data.get(role, self._None)\n\n    def _set_global_data(self, value, role):\n        self._global_data[role] = value\n        return True\n\n    def data(self, role, key=None):\n        value = self._get_global_data(role)\n        if value is not self._None:\n            return value\n\n        if key not in self.columns:\n            return None\n\n        if role == QtCore.Qt.ForegroundRole:\n            if key == \"name\" and not self.is_valid:\n                return ResourceCache.colors[\"warning\"]\n            return None\n\n        if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):\n            value = self._data[key]\n            if value is None:\n                value = self.parent().data(role, key)\n            return value\n\n        if role == QtCore.Qt.DecorationRole and key == \"name\":\n            return self.name_icon()\n        return None\n\n    def setData(self, value, role, key=None):\n        if role == DUPLICATED_ROLE:\n            if value == self._is_duplicated:\n                return False\n\n            self._is_duplicated = value\n            return True\n\n        if role == QtCore.Qt.EditRole:\n            if key in self.editable_columns:\n                self._data[key] = value\n                # must return true if successful\n                return True\n\n        return self._set_global_data(value, role)\n\n    @property\n    def id(self):\n        return self._id\n\n    @property\n    def is_new(self):\n        return False\n\n    def rowCount(self):\n        return len(self._children)\n\n    def child(self, row):\n        if -1 < row < self.rowCount():\n            return self._children[row]\n        return None\n\n    def children(self):\n        return self._children\n\n    def child_row(self, child):\n        if child not in self._children:\n            return -1\n        return self._children.index(child)\n\n    def parent(self):\n        return self._parent\n\n    def set_parent(self, parent):\n        if parent is self._parent:\n            return\n\n        if self._parent:\n            self._parent.remove_child(self)\n        self._parent = parent\n\n    def row(self):\n        if self._parent is not None:\n            return self._parent.child_row(self)\n        return -1\n\n    def add_child(self, item, row=None):\n        if item in self._children:\n            return\n\n        row_count = self.rowCount()\n        if row is None or row == row_count:\n            self._children.append(item)\n            return\n\n        if row > row_count or row < 0:\n            raise ValueError(\n                \"Invalid row number {} expected range 0 - {}\".format(\n                    row, row_count\n                )\n            )\n\n        self._children.insert(row, item)\n\n    def remove_child(self, item):\n        if item in self._children:\n            self._children.remove(item)\n\n    def flags(self, key):\n        flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable\n        if key in self.editable_columns:\n            flags |= QtCore.Qt.ItemIsEditable\n        return flags\n\n\nclass RootItem(BaseItem):\n    \"\"\"Invisible root item used as base item for model.\"\"\"\n    item_type = \"root\"\n\n    def __init__(self, model):\n        super(RootItem, self).__init__()\n        self._model = model\n\n    def model(self):\n        return self._model\n\n    def flags(self, *args, **kwargs):\n        return QtCore.Qt.NoItemFlags\n\n\nclass ProjectItem(BaseItem):\n    \"\"\"Item representing project document in Mongo.\n\n    Item is used only to read it's data. It is not possible to modify them.\n    \"\"\"\n    item_type = \"project\"\n\n    columns = {\n        \"name\",\n        \"type\",\n        \"frameStart\",\n        \"frameEnd\",\n        \"fps\",\n        \"resolutionWidth\",\n        \"resolutionHeight\",\n        \"handleStart\",\n        \"handleEnd\",\n        \"clipIn\",\n        \"clipOut\",\n        \"pixelAspect\",\n        \"tools_env\",\n    }\n    query_projection = {\n        \"_id\": 1,\n        \"name\": 1,\n        \"type\": 1,\n\n        \"data.frameStart\": 1,\n        \"data.frameEnd\": 1,\n        \"data.fps\": 1,\n        \"data.resolutionWidth\": 1,\n        \"data.resolutionHeight\": 1,\n        \"data.handleStart\": 1,\n        \"data.handleEnd\": 1,\n        \"data.clipIn\": 1,\n        \"data.clipOut\": 1,\n        \"data.pixelAspect\": 1,\n        \"data.tools_env\": 1\n    }\n\n    def __init__(self, project_doc):\n        self._mongo_id = project_doc[\"_id\"]\n\n        data = self.data_from_doc(project_doc)\n        super(ProjectItem, self).__init__(data)\n\n    @property\n    def project_id(self):\n        \"\"\"Project Mongo ID.\"\"\"\n        return self._mongo_id\n\n    @property\n    def asset_id(self):\n        \"\"\"Should not be implemented.\n\n        TODO Remove this method from ProjectItem.\n        \"\"\"\n        return None\n\n    @property\n    def name(self):\n        \"\"\"Project name\"\"\"\n        return self._data[\"name\"]\n\n    def child_parents(self):\n        \"\"\"Used by children AssetItems for filling `data.parents` key.\"\"\"\n        return []\n\n    @classmethod\n    def data_from_doc(cls, project_doc):\n        \"\"\"Convert document data into item data.\n\n        Project data are used as default value for it's children.\n        \"\"\"\n        data = {\n            \"name\": project_doc[\"name\"],\n            \"type\": project_doc[\"type\"]\n        }\n        doc_data = project_doc.get(\"data\") or {}\n        for key in cls.columns:\n            if key in data:\n                continue\n\n            data[key] = doc_data.get(key)\n\n        return data\n\n    def flags(self, *args, **kwargs):\n        \"\"\"Project is enabled and selectable.\"\"\"\n        return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable\n\n\nclass AssetItem(BaseItem):\n    \"\"\"Item represent asset document.\n\n    Item have ability to set all required and optional data for OpenPype\n    workflow. Some of them are not modifiable in specific cases e.g. when asset\n    has published content it is not possible to change it's name or parent.\n    \"\"\"\n    item_type = \"asset\"\n\n    columns = {\n        \"name\",\n        \"type\",\n        \"fps\",\n        \"frameStart\",\n        \"frameEnd\",\n        \"resolutionWidth\",\n        \"resolutionHeight\",\n        \"handleStart\",\n        \"handleEnd\",\n        \"clipIn\",\n        \"clipOut\",\n        \"pixelAspect\",\n        \"tools_env\"\n    }\n    editable_columns = {\n        \"name\",\n        \"frameStart\",\n        \"frameEnd\",\n        \"fps\",\n        \"resolutionWidth\",\n        \"resolutionHeight\",\n        \"handleStart\",\n        \"handleEnd\",\n        \"clipIn\",\n        \"clipOut\",\n        \"pixelAspect\",\n        \"tools_env\"\n    }\n    query_projection = {\n        \"_id\": 1,\n        \"name\": 1,\n        \"schema\": 1,\n        \"type\": 1,\n        \"parent\": 1,\n\n        \"data.visualParent\": 1,\n        \"data.parents\": 1,\n\n        \"data.tasks\": 1,\n\n        \"data.frameStart\": 1,\n        \"data.frameEnd\": 1,\n        \"data.fps\": 1,\n        \"data.resolutionWidth\": 1,\n        \"data.resolutionHeight\": 1,\n        \"data.handleStart\": 1,\n        \"data.handleEnd\": 1,\n        \"data.clipIn\": 1,\n        \"data.clipOut\": 1,\n        \"data.pixelAspect\": 1,\n        \"data.tools_env\": 1,\n    }\n\n    def __init__(self, asset_doc):\n        if not asset_doc:\n            asset_doc = {}\n        self.mongo_id = asset_doc.get(\"_id\")\n        self._project_id = None\n        self._edited_columns = {\n            column_name: False\n            for column_name in self.editable_columns\n        }\n\n        # Item data\n        self._hierarchy_changes_enabled = True\n        self._removed = False\n\n        # Task children duplication variables\n        self._task_items_by_name = collections.defaultdict(list)\n        self._task_name_by_item_id = {}\n        self._duplicated_task_names = set()\n\n        # Copy of original document\n        self._origin_asset_doc = copy.deepcopy(asset_doc)\n\n        data = self.data_from_doc(asset_doc)\n\n        self._origin_data = copy.deepcopy(data)\n\n        super(AssetItem, self).__init__(data)\n\n    @property\n    def project_id(self):\n        \"\"\"Access to project \"parent\" id which is always set.\"\"\"\n        if self._project_id is None:\n            self._project_id = self.parent().project_id\n        return self._project_id\n\n    @property\n    def asset_id(self):\n        \"\"\"Property access to mongo id.\"\"\"\n        return self.mongo_id\n\n    @property\n    def is_new(self):\n        \"\"\"Item was created during current project manager session.\"\"\"\n        return self.asset_id is None\n\n    @property\n    def is_valid(self):\n        \"\"\"Item is invalid for saving.\"\"\"\n        if self._is_duplicated or not self._data[\"name\"]:\n            return False\n        return True\n\n    @property\n    def name(self):\n        \"\"\"Asset name.\n\n        Returns:\n            str: If name is set.\n            None: If name is not yet set in that case is AssetItem marked as\n                invalid.\n        \"\"\"\n        return self._data[\"name\"]\n\n    def child_parents(self):\n        \"\"\"Children AssetItem can use this method to get it's parent names.\n\n        This is used for `data.parents` key on document.\n        \"\"\"\n        parents = self.parent().child_parents()\n        parents.append(self.name)\n        return parents\n\n    def to_doc(self):\n        \"\"\"Convert item to Mongo document matching asset schema.\n\n        Method does no validate if item is valid or children are valid.\n\n        Returns:\n            dict: Document with all related data about asset item also\n                contains task children.\n        \"\"\"\n        tasks = {}\n        for item in self.children():\n            if isinstance(item, TaskItem):\n                tasks.update(item.to_doc_data())\n\n        doc_data = {\n            \"parents\": self.parent().child_parents(),\n            \"visualParent\": self.parent().asset_id,\n            \"tasks\": tasks\n        }\n        schema_name = (\n            self._origin_asset_doc.get(\"schema\")\n            or CURRENT_ASSET_DOC_SCHEMA\n        )\n\n        doc = {\n            \"name\": self.data(QtCore.Qt.EditRole, \"name\"),\n            \"type\": self.data(QtCore.Qt.EditRole, \"type\"),\n            \"schema\": schema_name,\n            \"data\": doc_data,\n            \"parent\": self.project_id\n        }\n        if self.mongo_id:\n            doc[\"_id\"] = self.mongo_id\n\n        for key in self._data.keys():\n            if key in doc:\n                continue\n            # Use `data` method to get inherited values\n            doc_data[key] = self.data(QtCore.Qt.EditRole, key)\n\n        return doc\n\n    def update_data(self):\n        \"\"\"Changes dictionary ready for Mongo's update.\n\n        Method should be used on save. There is not other usage of this method.\n\n        # Example\n        ```python\n        {\n            \"$set\": {\n                \"name\": \"new_name\"\n            }\n        }\n        ```\n\n        Returns:\n            dict: May be empty if item was not changed.\n        \"\"\"\n        if not self.mongo_id:\n            return {}\n\n        document = self.to_doc()\n\n        changes = {}\n\n        for key, value in document.items():\n            if key in (\"data\", \"_id\"):\n                continue\n\n            if (\n                key in self._origin_asset_doc\n                and self._origin_asset_doc[key] == value\n            ):\n                continue\n\n            changes[key] = value\n\n        if \"data\" not in self._origin_asset_doc:\n            changes[\"data\"] = document[\"data\"]\n        else:\n            origin_data = self._origin_asset_doc[\"data\"]\n\n            for key, value in document[\"data\"].items():\n                if key in origin_data and origin_data[key] == value:\n                    continue\n                _key = \"data.{}\".format(key)\n                changes[_key] = value\n\n        if changes:\n            return {\"$set\": changes}\n        return {}\n\n    @classmethod\n    def data_from_doc(cls, asset_doc):\n        \"\"\"Convert asset document from Mongo to item data.\"\"\"\n        # Minimum required data for cases that it is new AssetItem without doc\n        data = {\n            \"name\": None,\n            \"type\": \"asset\"\n        }\n        if asset_doc:\n            for key in data.keys():\n                if key in asset_doc:\n                    data[key] = asset_doc[key]\n\n        doc_data = asset_doc.get(\"data\") or {}\n        for key in cls.columns:\n            if key in data:\n                continue\n\n            data[key] = doc_data.get(key)\n\n        return data\n\n    def name_icon(self):\n        \"\"\"Icon shown next to name.\"\"\"\n        if self.__class__._name_icons is None:\n            self.__class__._name_icons = ResourceCache.get_icons()[\"asset\"]\n\n        if self._removed:\n            icon_type = \"removed\"\n        elif not self.is_valid:\n            icon_type = \"invalid\"\n        elif self.is_new:\n            icon_type = \"new\"\n        else:\n            icon_type = \"default\"\n        return self.__class__._name_icons[icon_type]\n\n    def _get_global_data(self, role):\n        \"\"\"Global data getter without column specification.\"\"\"\n        if role == HIERARCHY_CHANGE_ABLE_ROLE:\n            return self._hierarchy_changes_enabled\n\n        if role == REMOVED_ROLE:\n            return self._removed\n\n        if role == QtCore.Qt.ToolTipRole:\n            name = self.data(QtCore.Qt.EditRole, \"name\")\n            if not name:\n                return \"Name is not set\"\n\n            elif self._is_duplicated:\n                return \"Duplicated asset name \\\"{}\\\"\".format(name)\n            return None\n\n        return super(AssetItem, self)._get_global_data(role)\n\n    def data(self, role, key=None):\n        if role == EDITOR_OPENED_ROLE:\n            if key not in self._edited_columns:\n                return False\n            return self._edited_columns[key]\n\n        if role == QtCore.Qt.DisplayRole and self._edited_columns.get(key):\n            return \"\"\n\n        return super(AssetItem, self).data(role, key)\n\n    def setData(self, value, role, key=None):\n        # Store information that column has opened editor\n        # - DisplayRole for the column will return empty string\n        if role == EDITOR_OPENED_ROLE:\n            if key not in self._edited_columns:\n                return False\n            self._edited_columns[key] = value\n            return True\n\n        if role == REMOVED_ROLE:\n            self._removed = value\n            return True\n\n        # This can be set only on project load (or save)\n        if role == HIERARCHY_CHANGE_ABLE_ROLE:\n            if self._hierarchy_changes_enabled == value:\n                return False\n            self._hierarchy_changes_enabled = value\n            return True\n\n        # Do not allow to change name if item is marked to not be able do any\n        #   hierarchical changes.\n        if (\n            role == QtCore.Qt.EditRole\n            and key == \"name\"\n            and not self._hierarchy_changes_enabled\n        ):\n            return False\n        return super(AssetItem, self).setData(value, role, key)\n\n    def flags(self, key):\n        if key == \"name\":\n            flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable\n            if self._hierarchy_changes_enabled:\n                flags |= QtCore.Qt.ItemIsEditable\n            return flags\n        return super(AssetItem, self).flags(key)\n\n    def _add_task(self, item):\n        name = item.data(QtCore.Qt.EditRole, \"name\").lower()\n        item_id = item.data(IDENTIFIER_ROLE)\n\n        self._task_name_by_item_id[item_id] = name\n        self._task_items_by_name[name].append(item)\n        if len(self._task_items_by_name[name]) > 1:\n            self._duplicated_task_names.add(name)\n            for _item in self._task_items_by_name[name]:\n                _item.setData(True, DUPLICATED_ROLE)\n        elif item.data(DUPLICATED_ROLE):\n            item.setData(False, DUPLICATED_ROLE)\n\n    def _remove_task(self, item):\n        # This method is probably obsolete with changed logic and added\n        #   `on_task_remove_state_change` method.\n        item_id = item.data(IDENTIFIER_ROLE)\n        if item_id not in self._task_name_by_item_id:\n            return\n\n        name = self._task_name_by_item_id.pop(item_id)\n        self._task_items_by_name[name].remove(item)\n        if not self._task_items_by_name[name]:\n            self._task_items_by_name.pop(name)\n\n        elif len(self._task_items_by_name[name]) == 1:\n            self._duplicated_task_names.remove(name)\n            for _item in self._task_items_by_name[name]:\n                _item.setData(False, DUPLICATED_ROLE)\n\n    def _rename_task(self, item):\n        # Skip processing if item is marked for removing\n        # - item is not in any of attributes below\n        if item.data(REMOVED_ROLE):\n            return\n\n        new_name = item.data(QtCore.Qt.EditRole, \"name\").lower()\n        item_id = item.data(IDENTIFIER_ROLE)\n        prev_name = self._task_name_by_item_id[item_id]\n        if new_name == prev_name:\n            return\n\n        # Remove from previous name mapping\n        self._task_items_by_name[prev_name].remove(item)\n        if not self._task_items_by_name[prev_name]:\n            self._task_items_by_name.pop(prev_name)\n\n        elif len(self._task_items_by_name[prev_name]) == 1:\n            self._duplicated_task_names.remove(prev_name)\n            for _item in self._task_items_by_name[prev_name]:\n                _item.setData(False, DUPLICATED_ROLE)\n\n        # Add to new name mapping\n        self._task_items_by_name[new_name].append(item)\n        if len(self._task_items_by_name[new_name]) > 1:\n            self._duplicated_task_names.add(new_name)\n            for _item in self._task_items_by_name[new_name]:\n                _item.setData(True, DUPLICATED_ROLE)\n        else:\n            item.setData(False, DUPLICATED_ROLE)\n\n        self._task_name_by_item_id[item_id] = new_name\n\n    def on_task_name_change(self, task_item):\n        \"\"\"Method called from TaskItem children on name change.\n\n        Helps to handle duplicated task name validations.\n        \"\"\"\n\n        self._rename_task(task_item)\n\n    def on_task_remove_state_change(self, task_item):\n        \"\"\"Method called from children TaskItem to handle name duplications.\n\n        Method is called when TaskItem children is marked for deletion or\n        deletion was reversed.\n\n        Name is removed/added to task item mapping attribute and removed/added\n        to `_task_items_by_name` used for determination of duplicated tasks.\n        \"\"\"\n        is_removed = task_item.data(REMOVED_ROLE)\n        item_id = task_item.data(IDENTIFIER_ROLE)\n        if is_removed:\n            name = self._task_name_by_item_id.pop(item_id)\n            self._task_items_by_name[name].remove(task_item)\n\n        else:\n            name = task_item.data(QtCore.Qt.EditRole, \"name\").lower()\n            self._task_name_by_item_id[item_id] = name\n            self._task_items_by_name[name].append(task_item)\n\n        # Remove from previous name mapping\n        if not self._task_items_by_name[name]:\n            self._task_items_by_name.pop(name)\n\n        elif len(self._task_items_by_name[name]) == 1:\n            if name in self._duplicated_task_names:\n                self._duplicated_task_names.remove(name)\n            task_item.setData(False, DUPLICATED_ROLE)\n\n        else:\n            self._duplicated_task_names.add(name)\n            for _item in self._task_items_by_name[name]:\n                _item.setData(True, DUPLICATED_ROLE)\n\n    def add_child(self, item, row=None):\n        \"\"\"Add new children.\n\n        Args:\n            item(AssetItem, TaskItem): New added item.\n            row(int): Optionally can be passed on which row (index) should be\n                children added.\n        \"\"\"\n        if item in self._children:\n            return\n\n        super(AssetItem, self).add_child(item, row)\n\n        # Call inner method for checking task name duplications\n        if isinstance(item, TaskItem):\n            self._add_task(item)\n\n    def remove_child(self, item):\n        \"\"\"Remove one of children from AssetItem children.\n\n        Skipped if item is not children of item.\n\n        Args:\n            item(AssetItem, TaskItem): Child item.\n        \"\"\"\n        if item not in self._children:\n            return\n\n        # Call inner method to remove task from registered task name\n        #   validations.\n        if isinstance(item, TaskItem):\n            self._remove_task(item)\n\n        super(AssetItem, self).remove_child(item)\n\n\nclass TaskItem(BaseItem):\n    \"\"\"Item representing Task item on Asset document.\n\n    Always should be AssetItem children and never should have any other\n    children.\n\n    It's name value should be validated with it's parent which only knows if\n    has same name as other sibling under same parent.\n    \"\"\"\n\n    # String representation of item\n    item_type = \"task\"\n\n    columns = {\n        \"name\",\n        \"type\"\n    }\n    editable_columns = {\n        \"name\",\n        \"type\"\n    }\n\n    def __init__(self, data=None, is_new=None):\n        self._removed = False\n        if is_new is None:\n            is_new = data is None\n        self._is_new = is_new\n        if data is None:\n            data = {}\n\n        self._edited_columns = {\n            column_name: False\n            for column_name in self.editable_columns\n        }\n        self._origin_data = copy.deepcopy(data)\n        super(TaskItem, self).__init__(data)\n\n    @property\n    def is_new(self):\n        \"\"\"Task was created during current project manager session.\"\"\"\n        return self._is_new\n\n    @property\n    def is_valid(self):\n        \"\"\"Task valid for saving.\"\"\"\n        if self._is_duplicated or not self._data[\"type\"]:\n            return False\n        if not self.data(QtCore.Qt.EditRole, \"name\"):\n            return False\n        return True\n\n    def name_icon(self):\n        \"\"\"Icon shown next to name.\"\"\"\n        if self.__class__._name_icons is None:\n            self.__class__._name_icons = ResourceCache.get_icons()[\"task\"]\n\n        if self._removed:\n            icon_type = \"removed\"\n        elif not self.is_valid:\n            icon_type = \"invalid\"\n        elif self.is_new:\n            icon_type = \"new\"\n        else:\n            icon_type = \"default\"\n        return self.__class__._name_icons[icon_type]\n\n    def add_child(self, item, row=None):\n        \"\"\"Reimplement `add_child` to avoid adding items under task.\"\"\"\n        raise AssertionError(\"BUG: Can't add children to Task\")\n\n    def _get_global_data(self, role):\n        \"\"\"Global data getter without column specification.\"\"\"\n        if role == REMOVED_ROLE:\n            return self._removed\n\n        if role == QtCore.Qt.ToolTipRole:\n            if not self._data[\"type\"]:\n                return \"Type is not set\"\n\n            name = self.data(QtCore.Qt.EditRole, \"name\")\n            if not name:\n                return \"Name is not set\"\n\n            elif self._is_duplicated:\n                return \"Duplicated task name \\\"{}\".format(name)\n            return None\n\n        return super(TaskItem, self)._get_global_data(role)\n\n    def to_doc_data(self):\n        \"\"\"Data for Asset document.\n\n        Returns:\n            dict: May be empty if task is marked as removed or with single key\n                dict with name as key and task data as value.\n        \"\"\"\n        if self._removed:\n            return {}\n        data = copy.deepcopy(self._data)\n        data.pop(\"name\")\n        name = self.data(QtCore.Qt.EditRole, \"name\")\n        return {\n            name: data\n        }\n\n    def data(self, role, key=None):\n        if role == EDITOR_OPENED_ROLE:\n            if key not in self._edited_columns:\n                return False\n            return self._edited_columns[key]\n\n        # Return empty string if column is edited\n        if role == QtCore.Qt.DisplayRole and self._edited_columns.get(key):\n            return \"\"\n\n        if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):\n            if key == \"type\":\n                return self._data[\"type\"]\n\n            # Always require task type filled\n            if key == \"name\":\n                if not self._data[\"type\"]:\n                    if role == QtCore.Qt.DisplayRole:\n                        return \"< Select Type >\"\n                    if role == QtCore.Qt.EditRole:\n                        return \"\"\n                else:\n                    return self._data[key] or self._data[\"type\"]\n\n        return super(TaskItem, self).data(role, key)\n\n    def setData(self, value, role, key=None):\n        # Store information that item on a column is edited\n        #   - DisplayRole will return empty string in that case\n        if role == EDITOR_OPENED_ROLE:\n            if key not in self._edited_columns:\n                return False\n            self._edited_columns[key] = value\n            return True\n\n        if role == REMOVED_ROLE:\n            # Skip value change if is same as already set value\n            if value == self._removed:\n                return False\n            self._removed = value\n            self.parent().on_task_remove_state_change(self)\n            return True\n\n        # Convert empty string to None on EditRole\n        if (\n            role == QtCore.Qt.EditRole\n            and key == \"name\"\n            and not value\n        ):\n            value = None\n\n        result = super(TaskItem, self).setData(value, role, key)\n\n        if role == QtCore.Qt.EditRole:\n            # Trigger task name change of parent AssetItem\n            if (\n                key == \"name\"\n                or (key == \"type\" and not self._data[\"name\"])\n            ):\n                self.parent().on_task_name_change(self)\n\n        return result\n\n    def to_json_data(self):\n        \"\"\"Convert json data without parent reference.\n\n        Method used for mime data on copy/paste\n        \"\"\"\n        return self.to_doc_data()\n"
  },
  {
    "path": "openpype/tools/project_manager/project_manager/multiselection_combobox.py",
    "content": "from qtpy import QtCore, QtWidgets\n\nfrom openpype.tools.utils.lib import (\n    checkstate_int_to_enum,\n    checkstate_enum_to_int,\n)\nfrom openpype.tools.utils.constants import (\n    CHECKED_INT,\n    UNCHECKED_INT,\n    ITEM_IS_USER_TRISTATE,\n)\n\n\nclass ComboItemDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"\n    Helper styled delegate (mostly based on existing private Qt's\n    delegate used by the QtWidgets.QComboBox). Used to style the popup like a\n    list view (e.g windows style).\n    \"\"\"\n\n    def paint(self, painter, option, index):\n        option = QtWidgets.QStyleOptionViewItem(option)\n        option.showDecorationSelected = True\n\n        # option.state &= (\n        #     ~QtWidgets.QStyle.State_HasFocus\n        #     & ~QtWidgets.QStyle.State_MouseOver\n        # )\n        super(ComboItemDelegate, self).paint(painter, option, index)\n\n\nclass MultiSelectionComboBox(QtWidgets.QComboBox):\n    value_changed = QtCore.Signal()\n    ignored_keys = {\n        QtCore.Qt.Key_Up,\n        QtCore.Qt.Key_Down,\n        QtCore.Qt.Key_PageDown,\n        QtCore.Qt.Key_PageUp,\n        QtCore.Qt.Key_Home,\n        QtCore.Qt.Key_End\n    }\n\n    def __init__(self, parent=None, **kwargs):\n        super(MultiSelectionComboBox, self).__init__(parent=parent, **kwargs)\n        self.setObjectName(\"MultiSelectionComboBox\")\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        self._popup_is_shown = False\n        self._block_mouse_release_timer = QtCore.QTimer(self, singleShot=True)\n        self._initial_mouse_pos = None\n        self._delegate = ComboItemDelegate(self)\n        self.setItemDelegate(self._delegate)\n\n    def mousePressEvent(self, event):\n        \"\"\"Reimplemented.\"\"\"\n        self._popup_is_shown = False\n        super(MultiSelectionComboBox, self).mousePressEvent(event)\n        if self._popup_is_shown:\n            self._initial_mouse_pos = self.mapToGlobal(event.pos())\n            self._block_mouse_release_timer.start(\n                QtWidgets.QApplication.doubleClickInterval()\n            )\n\n    def showPopup(self):\n        \"\"\"Reimplemented.\"\"\"\n        super(MultiSelectionComboBox, self).showPopup()\n        view = self.view()\n        view.installEventFilter(self)\n        view.viewport().installEventFilter(self)\n        self._popup_is_shown = True\n\n    def hidePopup(self):\n        \"\"\"Reimplemented.\"\"\"\n        self.view().removeEventFilter(self)\n        self.view().viewport().removeEventFilter(self)\n        self._popup_is_shown = False\n        self._initial_mouse_pos = None\n        super(MultiSelectionComboBox, self).hidePopup()\n        self.view().clearFocus()\n\n    def _event_popup_shown(self, obj, event):\n        if not self._popup_is_shown:\n            return\n\n        current_index = self.view().currentIndex()\n        model = self.model()\n\n        if event.type() == QtCore.QEvent.MouseMove:\n            if (\n                self.view().isVisible()\n                and self._initial_mouse_pos is not None\n                and self._block_mouse_release_timer.isActive()\n            ):\n                diff = obj.mapToGlobal(event.pos()) - self._initial_mouse_pos\n                if diff.manhattanLength() > 9:\n                    self._block_mouse_release_timer.stop()\n            return\n\n        index_flags = current_index.flags()\n        state = checkstate_int_to_enum(\n            current_index.data(QtCore.Qt.CheckStateRole)\n        )\n        new_state = None\n\n        if event.type() == QtCore.QEvent.MouseButtonRelease:\n            if (\n                self._block_mouse_release_timer.isActive()\n                or not current_index.isValid()\n                or not self.view().isVisible()\n                or not self.view().rect().contains(event.pos())\n                or not index_flags & QtCore.Qt.ItemIsSelectable\n                or not index_flags & QtCore.Qt.ItemIsEnabled\n                or not index_flags & QtCore.Qt.ItemIsUserCheckable\n            ):\n                return\n\n            if state == QtCore.Qt.Unchecked:\n                new_state = CHECKED_INT\n            else:\n                new_state = UNCHECKED_INT\n\n        elif event.type() == QtCore.QEvent.KeyPress:\n            # TODO: handle QtCore.Qt.Key_Enter, Key_Return?\n            if event.key() == QtCore.Qt.Key_Space:\n                # toggle the current items check state\n                if (\n                    index_flags & QtCore.Qt.ItemIsUserCheckable\n                    and index_flags & ITEM_IS_USER_TRISTATE\n                ):\n                    new_state = (checkstate_enum_to_int(state) + 1) % 3\n\n                elif index_flags & QtCore.Qt.ItemIsUserCheckable:\n                    if state != QtCore.Qt.Checked:\n                        new_state = CHECKED_INT\n                    else:\n                        new_state = UNCHECKED_INT\n\n        if new_state is not None:\n            model.setData(current_index, new_state, QtCore.Qt.CheckStateRole)\n            self.view().update(current_index)\n            self.value_changed.emit()\n            return True\n\n    def eventFilter(self, obj, event):\n        \"\"\"Reimplemented.\"\"\"\n        result = self._event_popup_shown(obj, event)\n        if result is not None:\n            return result\n\n        return super(MultiSelectionComboBox, self).eventFilter(obj, event)\n\n    def addItem(self, *args, **kwargs):\n        idx = self.count()\n        super(MultiSelectionComboBox, self).addItem(*args, **kwargs)\n        self.model().item(idx).setCheckable(True)\n\n    def paintEvent(self, event):\n        \"\"\"Reimplemented.\"\"\"\n        painter = QtWidgets.QStylePainter(self)\n        option = QtWidgets.QStyleOptionComboBox()\n        self.initStyleOption(option)\n        painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, option)\n\n        # draw the icon and text\n        items = self.checked_items_text()\n        if not items:\n            return\n\n        text_rect = self.style().subControlRect(\n            QtWidgets.QStyle.CC_ComboBox,\n            option,\n            QtWidgets.QStyle.SC_ComboBoxEditField\n        )\n        text = \", \".join(items)\n        new_text = self.fontMetrics().elidedText(\n            text, QtCore.Qt.ElideRight, text_rect.width()\n        )\n        painter.drawText(\n            text_rect,\n            QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,\n            new_text\n        )\n\n    def setItemCheckState(self, index, state):\n        self.setItemData(index, state, QtCore.Qt.CheckStateRole)\n\n    def set_value(self, values):\n        for idx in range(self.count()):\n            value = self.itemData(idx, role=QtCore.Qt.UserRole)\n            if value in values:\n                check_state = CHECKED_INT\n            else:\n                check_state = UNCHECKED_INT\n            self.setItemData(idx, check_state, QtCore.Qt.CheckStateRole)\n\n    def value(self):\n        items = list()\n        for idx in range(self.count()):\n            state = checkstate_int_to_enum(\n                self.itemData(idx, role=QtCore.Qt.CheckStateRole)\n            )\n            if state == QtCore.Qt.Checked:\n                items.append(\n                    self.itemData(idx, role=QtCore.Qt.UserRole)\n                )\n        return items\n\n    def checked_items_text(self):\n        items = list()\n        for idx in range(self.count()):\n            state = checkstate_int_to_enum(\n                self.itemData(idx, role=QtCore.Qt.CheckStateRole)\n            )\n            if state == QtCore.Qt.Checked:\n                items.append(self.itemText(idx))\n        return items\n\n    def wheelEvent(self, event):\n        event.ignore()\n\n    def keyPressEvent(self, event):\n        if (\n            event.key() == QtCore.Qt.Key_Down\n            and event.modifiers() & QtCore.Qt.AltModifier\n        ):\n            return self.showPopup()\n\n        if event.key() in self.ignored_keys:\n            return event.ignore()\n\n        return super(MultiSelectionComboBox, self).keyPressEvent(event)\n"
  },
  {
    "path": "openpype/tools/project_manager/project_manager/style.py",
    "content": "import os\nfrom qtpy import QtGui\n\nimport qtawesome\nfrom openpype.tools.utils import paint_image_with_color\n\n\nclass ResourceCache:\n    # TODO use colors from OpenPype style\n    colors = {\n        \"standard\": \"#bfccd6\",\n        \"disabled\": \"#969696\",\n        \"new\": \"#2d9a4c\",\n        \"warning\": \"#c83232\"\n    }\n    icons = None\n\n    @classmethod\n    def get_icon(cls, *keys):\n        output = cls.get_icons()\n        for key in keys:\n            output = output[key]\n        return output\n\n    @classmethod\n    def get_icons(cls):\n        if cls.icons is None:\n            cls.icons = {\n                \"asset\": {\n                    \"default\": qtawesome.icon(\n                        \"fa.folder\",\n                        color=cls.colors[\"standard\"]\n                    ),\n                    \"new\": qtawesome.icon(\n                        \"fa.folder\",\n                        color=cls.colors[\"new\"]\n                    ),\n                    \"invalid\": qtawesome.icon(\n                        \"fa.exclamation-triangle\",\n                        color=cls.colors[\"warning\"]\n                    ),\n                    \"removed\": qtawesome.icon(\n                        \"fa.trash\",\n                        color=cls.colors[\"warning\"]\n                    )\n                },\n                \"task\": {\n                    \"default\": qtawesome.icon(\n                        \"fa.check-circle-o\",\n                        color=cls.colors[\"standard\"]\n                    ),\n                    \"new\": qtawesome.icon(\n                        \"fa.check-circle\",\n                        color=cls.colors[\"new\"]\n                    ),\n                    \"invalid\": qtawesome.icon(\n                        \"fa.exclamation-circle\",\n                        color=cls.colors[\"warning\"]\n                    ),\n                    \"removed\": qtawesome.icon(\n                        \"fa.trash\",\n                        color=cls.colors[\"warning\"]\n                    )\n                },\n                \"refresh\": qtawesome.icon(\n                    \"fa.refresh\",\n                    color=cls.colors[\"standard\"],\n                    color_disabled=cls.colors[\"disabled\"]\n                ),\n                \"remove\": cls.get_remove_icon()\n            }\n        return cls.icons\n\n    @classmethod\n    def get_color(cls, color_name):\n        return cls.colors[color_name]\n\n    @classmethod\n    def get_remove_icon(cls):\n        src_image = get_remove_image()\n        normal_pix = paint_image_with_color(\n            src_image,\n            QtGui.QColor(cls.colors[\"standard\"])\n        )\n        disabled_pix = paint_image_with_color(\n            src_image,\n            QtGui.QColor(cls.colors[\"disabled\"])\n        )\n        icon = QtGui.QIcon(normal_pix)\n        icon.addPixmap(disabled_pix, QtGui.QIcon.Disabled, QtGui.QIcon.On)\n        icon.addPixmap(disabled_pix, QtGui.QIcon.Disabled, QtGui.QIcon.Off)\n        return icon\n\n\ndef get_remove_image():\n    image_path = os.path.join(\n        os.path.dirname(os.path.abspath(__file__)),\n        \"images\",\n        \"bin.png\"\n    )\n    return QtGui.QImage(image_path)\n"
  },
  {
    "path": "openpype/tools/project_manager/project_manager/view.py",
    "content": "import collections\nfrom queue import Queue\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.client import get_project\nfrom .delegates import (\n    NumberDelegate,\n    NameDelegate,\n    TypeDelegate,\n    ToolsDelegate\n)\n\nfrom openpype.lib import ApplicationManager\nfrom .constants import (\n    REMOVED_ROLE,\n    IDENTIFIER_ROLE,\n    ITEM_TYPE_ROLE,\n    HIERARCHY_CHANGE_ABLE_ROLE,\n    EDITOR_OPENED_ROLE\n)\n\n\nclass NameDef:\n    pass\n\n\nclass NumberDef:\n    def __init__(self, minimum=None, maximum=None, decimals=None, step=None):\n        self.minimum = 0 if minimum is None else minimum\n        self.maximum = 999999999 if maximum is None else maximum\n        self.decimals = 0 if decimals is None else decimals\n        self.step = 1 if decimals is None else step\n\n\nclass TypeDef:\n    pass\n\n\nclass ToolsDef:\n    pass\n\n\nclass ProjectDocCache:\n    def __init__(self, dbcon):\n        self.dbcon = dbcon\n        self.project_doc = None\n\n    def set_project(self, project_name):\n        self.project_doc = None\n\n        if project_name:\n            self.project_doc = get_project(project_name)\n\n\nclass ToolsCache:\n    def __init__(self):\n        self.tools_data = []\n\n    def refresh(self):\n        app_manager = ApplicationManager()\n        tools_data = []\n        for tool_name, tool in app_manager.tools.items():\n            tools_data.append(\n                (tool_name, tool.label)\n            )\n        self.tools_data = tools_data\n\n\nclass HierarchyView(QtWidgets.QTreeView):\n    \"\"\"A tree view that deselects on clicking on an empty area in the view\"\"\"\n    column_delegate_defs = {\n        \"name\": NameDef(),\n        \"type\": TypeDef(),\n        \"frameStart\": NumberDef(0),\n        \"frameEnd\": NumberDef(0),\n        \"fps\": NumberDef(1, decimals=3, step=1),\n        \"resolutionWidth\": NumberDef(0),\n        \"resolutionHeight\": NumberDef(0),\n        \"handleStart\": NumberDef(0),\n        \"handleEnd\": NumberDef(0),\n        \"clipIn\": NumberDef(1),\n        \"clipOut\": NumberDef(1),\n        \"pixelAspect\": NumberDef(0, decimals=2, step=0.01),\n        \"tools_env\": ToolsDef()\n    }\n\n    columns_sizes = {\n        \"default\": {\n            \"stretch\": QtWidgets.QHeaderView.ResizeToContents\n        },\n        \"name\": {\n            \"stretch\": QtWidgets.QHeaderView.Interactive,\n            \"width\": 260\n        },\n        \"type\": {\n            \"stretch\": QtWidgets.QHeaderView.Interactive,\n            \"width\": 140\n        },\n        \"fps\": {\n            \"stretch\": QtWidgets.QHeaderView.Interactive,\n            \"width\": 65\n        },\n        \"tools_env\": {\n            \"stretch\": QtWidgets.QHeaderView.Interactive,\n            \"width\": 200\n        }\n    }\n    persistent_columns = {\n        \"type\",\n        \"frameStart\",\n        \"frameEnd\",\n        \"fps\",\n        \"resolutionWidth\",\n        \"resolutionHeight\",\n        \"handleStart\",\n        \"handleEnd\",\n        \"clipIn\",\n        \"clipOut\",\n        \"pixelAspect\",\n        \"tools_env\"\n    }\n\n    def __init__(self, dbcon, source_model, parent):\n        super(HierarchyView, self).__init__(parent)\n\n        self.setObjectName(\"HierarchyView\")\n\n        # Direct access to model\n        self._source_model = source_model\n        self._editors_mapping = {}\n        self._persisten_editors = set()\n        # Access to parent because of `show_message` method\n        self._parent = parent\n\n        project_doc_cache = ProjectDocCache(dbcon)\n        tools_cache = ToolsCache()\n\n        main_delegate = QtWidgets.QStyledItemDelegate()\n        self.setItemDelegate(main_delegate)\n        self.setAlternatingRowColors(True)\n        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)\n        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        self.setEditTriggers(QtWidgets.QAbstractItemView.AllEditTriggers)\n\n        column_delegates = {}\n        column_key_to_index = {}\n        for key, item_type in self.column_delegate_defs.items():\n            if isinstance(item_type, NameDef):\n                delegate = NameDelegate()\n\n            elif isinstance(item_type, NumberDef):\n                delegate = NumberDelegate(\n                    item_type.minimum,\n                    item_type.maximum,\n                    item_type.decimals,\n                    item_type.step\n                )\n\n            elif isinstance(item_type, TypeDef):\n                delegate = TypeDelegate(project_doc_cache)\n\n            elif isinstance(item_type, ToolsDef):\n                delegate = ToolsDelegate(tools_cache)\n\n            column = self._source_model.columns.index(key)\n            self.setItemDelegateForColumn(column, delegate)\n            column_delegates[key] = delegate\n            column_key_to_index[key] = column\n\n        source_model.index_moved.connect(self._on_rows_moved)\n        self.customContextMenuRequested.connect(self._on_context_menu)\n        self._source_model.project_changed.connect(self._on_project_reset)\n\n        self._project_doc_cache = project_doc_cache\n        self._tools_cache = tools_cache\n\n        self._delegate = main_delegate\n        self._column_delegates = column_delegates\n        self._column_key_to_index = column_key_to_index\n\n    def header_init(self):\n        header = self.header()\n\n        default_behavior = self.columns_sizes[\"default\"]\n        widths_by_idx = {}\n        for idx in range(header.count()):\n            key = self._source_model.columns[idx]\n            behavior = self.columns_sizes.get(key, default_behavior)\n            if behavior is None:\n                continue\n            logical_index = header.logicalIndex(idx)\n            stretch = behavior[\"stretch\"]\n            header.setSectionResizeMode(logical_index, stretch)\n            width = behavior.get(\"width\")\n            if width is not None:\n                widths_by_idx[idx] = width\n\n        for idx, width in widths_by_idx.items():\n            self.setColumnWidth(idx, width)\n\n    def set_project(self, project_name, force=False):\n        # Trigger helpers first\n        self._project_doc_cache.set_project(project_name)\n        self._tools_cache.refresh()\n\n        # Trigger update of model after all data for delegates are filled\n        self._source_model.set_project(project_name, force)\n\n    def _on_project_reset(self):\n        self.header_init()\n\n        self.collapseAll()\n\n        project_item = self._source_model.project_item\n        if project_item:\n            index = self._source_model.index_for_item(project_item)\n            self.expand(index)\n\n    def _on_rows_moved(self, index):\n        parent_index = index.parent()\n        if not self.isExpanded(parent_index):\n            self.expand(parent_index)\n\n    def commitData(self, editor):\n        super(HierarchyView, self).commitData(editor)\n        current_index = self.currentIndex()\n        column = current_index.column()\n        row = current_index.row()\n        skipped_index = None\n        # Change column from \"type\" to \"name\"\n        if column == 1:\n            new_index = self._source_model.index(\n                current_index.row(),\n                0,\n                current_index.parent()\n            )\n            self.setCurrentIndex(new_index)\n        elif column > 0:\n            indexes = []\n            for index in self.selectedIndexes():\n                if index.column() == column:\n                    if index.row() == row:\n                        skipped_index = index\n                    else:\n                        indexes.append(index)\n\n            if skipped_index is not None:\n                value = current_index.data(QtCore.Qt.EditRole)\n                for index in indexes:\n                    index.model().setData(index, value, QtCore.Qt.EditRole)\n\n        # Update children data\n        self.updateEditorData()\n\n    def _deselect_editor(self, editor):\n        if editor:\n            if isinstance(\n                editor, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)\n            ):\n                line_edit = editor.findChild(QtWidgets.QLineEdit)\n                line_edit.deselect()\n\n            elif isinstance(editor, QtWidgets.QLineEdit):\n                editor.deselect()\n\n    def edit(self, index, *args, **kwargs):\n        result = super(HierarchyView, self).edit(index, *args, **kwargs)\n        if result:\n            # Mark index to not return text for DisplayRole\n            editor = self.indexWidget(index)\n            if (\n                editor not in self._persisten_editors\n                and editor not in self._editors_mapping\n            ):\n                self._editors_mapping[editor] = index\n                self._source_model.setData(index, True, EDITOR_OPENED_ROLE)\n            # Deselect content of editor\n            # QUESTION not sure if we want do this all the time\n            self._deselect_editor(editor)\n        return result\n\n    def closeEditor(self, editor, hint):\n        if (\n            editor not in self._persisten_editors\n            and editor in self._editors_mapping\n        ):\n            index = self._editors_mapping.pop(editor)\n            self._source_model.setData(index, False, EDITOR_OPENED_ROLE)\n        super(HierarchyView, self).closeEditor(editor, hint)\n\n    def openPersistentEditor(self, index):\n        self._source_model.setData(index, True, EDITOR_OPENED_ROLE)\n        super(HierarchyView, self).openPersistentEditor(index)\n        editor = self.indexWidget(index)\n        self._persisten_editors.add(editor)\n        self._deselect_editor(editor)\n\n    def closePersistentEditor(self, index):\n        self._source_model.setData(index, False, EDITOR_OPENED_ROLE)\n        editor = self.indexWidget(index)\n        self._persisten_editors.remove(editor)\n        super(HierarchyView, self).closePersistentEditor(index)\n\n    def rowsInserted(self, parent_index, start, end):\n        super(HierarchyView, self).rowsInserted(parent_index, start, end)\n\n\n        # Expand parent on insert\n        if not self.isExpanded(parent_index):\n            self.expand(parent_index)\n\n    def mousePressEvent(self, event):\n        index = self.indexAt(event.pos())\n        if not index.isValid():\n            # clear the selection\n            self.clearSelection()\n            # clear the current index\n            self.setCurrentIndex(QtCore.QModelIndex())\n\n        super(HierarchyView, self).mousePressEvent(event)\n\n    def keyPressEvent(self, event):\n        call_super = False\n        if event.key() == QtCore.Qt.Key_Delete:\n            self._delete_items()\n\n        elif event.matches(QtGui.QKeySequence.Copy):\n            self._copy_items()\n\n        elif event.matches(QtGui.QKeySequence.Paste):\n            self._paste_items()\n\n        elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):\n            mdfs = event.modifiers()\n            if mdfs == (QtCore.Qt.ShiftModifier | QtCore.Qt.ControlModifier):\n                self._on_ctrl_shift_enter_pressed()\n            elif mdfs == QtCore.Qt.ShiftModifier:\n                self._on_shift_enter_pressed()\n            else:\n                if self.state() == HierarchyView.NoState:\n                    self._on_enter_pressed()\n\n        elif event.modifiers() == QtCore.Qt.ControlModifier:\n            if event.key() == QtCore.Qt.Key_Left:\n                self._on_left_ctrl_pressed()\n            elif event.key() == QtCore.Qt.Key_Right:\n                self._on_right_ctrl_pressed()\n            elif event.key() == QtCore.Qt.Key_Up:\n                self._on_up_ctrl_pressed()\n            elif event.key() == QtCore.Qt.Key_Down:\n                self._on_down_ctrl_pressed()\n        else:\n            call_super = True\n\n        if call_super:\n            super(HierarchyView, self).keyPressEvent(event)\n        else:\n            event.accept()\n\n    def _copy_items(self, indexes=None):\n        clipboard = QtWidgets.QApplication.clipboard()\n        try:\n            if indexes is None:\n                indexes = self.selectedIndexes()\n            mime_data = self._source_model.copy_mime_data(indexes)\n\n            clipboard.setMimeData(mime_data)\n            self._show_message(\"Tasks copied\")\n        except ValueError as exc:\n            # Change clipboard to contain empty data\n            empty_mime_data = QtCore.QMimeData()\n            clipboard.setMimeData(empty_mime_data)\n            self._show_message(str(exc))\n\n    def _paste_items(self):\n        mime_data = QtWidgets.QApplication.clipboard().mimeData()\n        rows = self.selectionModel().selectedRows()\n        self._source_model.paste(rows, mime_data)\n\n    def _delete_items(self, indexes=None):\n        if indexes is None:\n            indexes = self.selectedIndexes()\n        self._source_model.delete_indexes(indexes)\n\n    def _on_ctrl_shift_enter_pressed(self):\n        self.add_task_and_edit()\n\n    def add_asset(self, parent_index=None):\n        if parent_index is None:\n            parent_index = self.currentIndex()\n\n        if not parent_index.isValid():\n            return\n\n        # Stop editing\n        self.setState(HierarchyView.NoState)\n        QtWidgets.QApplication.processEvents()\n\n        return self._source_model.add_new_asset(parent_index)\n\n    def add_task(self, parent_index=None):\n        if parent_index is None:\n            parent_index = self.currentIndex()\n\n        if not parent_index.isValid():\n            return\n\n        return self._source_model.add_new_task(parent_index)\n\n    def _add_asset_action(self):\n        self._add_asset_and_edit()\n\n    def _add_asset_and_edit(self, parent_index=None):\n        new_index = self.add_asset(parent_index)\n        if new_index is None:\n            return\n\n        # Change current index\n        self.selectionModel().setCurrentIndex(\n            new_index,\n            QtCore.QItemSelectionModel.Clear\n            | QtCore.QItemSelectionModel.Select\n        )\n        # Start editing\n        self.edit(new_index)\n\n    def _add_task_action(self):\n        self.add_task_and_edit()\n\n    def add_task_and_edit(self):\n        new_index = self.add_task()\n        if new_index is None:\n            return\n\n        # Stop editing\n        self.setState(HierarchyView.NoState)\n        QtWidgets.QApplication.processEvents()\n\n        # TODO change hardcoded column index to coded\n        task_type_index = self._source_model.index(\n            new_index.row(), 1, new_index.parent()\n        )\n        # Change current index\n        self.selectionModel().setCurrentIndex(\n            task_type_index,\n            QtCore.QItemSelectionModel.Clear\n            | QtCore.QItemSelectionModel.Select\n        )\n        # Start editing\n        self.edit(task_type_index)\n\n    def _on_shift_enter_pressed(self):\n        parent_index = self.currentIndex()\n        if not parent_index.isValid():\n            return\n\n        if parent_index.data(ITEM_TYPE_ROLE) == \"asset\":\n            parent_index = parent_index.parent()\n        self._add_asset_and_edit(parent_index)\n\n    def _on_up_ctrl_pressed(self):\n        indexes = self.selectedIndexes()\n        self._source_model.move_vertical(indexes, -1)\n\n    def _on_down_ctrl_pressed(self):\n        indexes = self.selectedIndexes()\n        self._source_model.move_vertical(indexes, 1)\n\n    def _on_left_ctrl_pressed(self):\n        indexes = self.selectedIndexes()\n        self._source_model.move_horizontal(indexes, -1)\n\n    def _on_right_ctrl_pressed(self):\n        indexes = self.selectedIndexes()\n        self._source_model.move_horizontal(indexes, 1)\n\n    def _on_enter_pressed(self):\n        index = self.currentIndex()\n        if (\n            index.isValid()\n            and index.flags() & QtCore.Qt.ItemIsEditable\n        ):\n            self.edit(index)\n\n    def _remove_delete_flag(self, item_ids):\n        \"\"\"Remove deletion flag on items marked for deletion.\"\"\"\n        self._source_model.remove_delete_flag(item_ids)\n\n    def _expand_items(self, indexes):\n        \"\"\"Expand multiple items with all it's children.\n\n        Args:\n            indexes (list): List of QModelIndex that should be expanded.\n        \"\"\"\n        process_queue = Queue()\n        for index in indexes:\n            if index.column() == 0:\n                process_queue.put(index)\n\n        item_ids = set()\n        # Use deque as expanding not visible items as first is faster\n        indexes_deque = collections.deque()\n        while not process_queue.empty():\n            index = process_queue.get()\n            item_id = index.data(IDENTIFIER_ROLE)\n            if item_id in item_ids:\n                continue\n            item_ids.add(item_id)\n\n            indexes_deque.append(index)\n\n            for row in range(self._source_model.rowCount(index)):\n                process_queue.put(self._source_model.index(\n                    row, 0, index\n                ))\n\n        while indexes_deque:\n            self.expand(indexes_deque.pop())\n\n    def _collapse_items(self, indexes):\n        \"\"\"Collapse multiple items with all it's children.\n\n        Args:\n            indexes (list): List of QModelIndex that should be collapsed.\n        \"\"\"\n        item_ids = set()\n        process_queue = Queue()\n        for index in indexes:\n            if index.column() == 0:\n                process_queue.put(index)\n\n        while not process_queue.empty():\n            index = process_queue.get()\n            item_id = index.data(IDENTIFIER_ROLE)\n            if item_id in item_ids:\n                continue\n            item_ids.add(item_id)\n\n            self.collapse(index)\n\n            for row in range(self._source_model.rowCount(index)):\n                process_queue.put(self._source_model.index(\n                    row, 0, index\n                ))\n\n    def _show_message(self, message):\n        \"\"\"Show message to user.\"\"\"\n        self._parent.show_message(message)\n\n    def _on_context_menu(self, point):\n        \"\"\"Context menu on right click.\n\n        Currently is menu shown only on \"name\" column.\n        \"\"\"\n        index = self.indexAt(point)\n        column = index.column()\n        if column != 0:\n            return\n\n        actions = []\n\n        context_menu = QtWidgets.QMenu(self)\n\n        indexes = self.selectedIndexes()\n\n        items_by_id = {}\n        for index in indexes:\n            if index.column() != column:\n                continue\n\n            item_id = index.data(IDENTIFIER_ROLE)\n            items_by_id[item_id] = self._source_model.items_by_id[item_id]\n\n        item_ids = tuple(items_by_id.keys())\n        if len(item_ids) == 1:\n            item = items_by_id[item_ids[0]]\n            item_type = item.data(ITEM_TYPE_ROLE)\n            if item_type in (\"asset\", \"project\"):\n                add_asset_action = QtWidgets.QAction(\"Add Asset\", context_menu)\n                add_asset_action.triggered.connect(\n                    self._add_asset_action\n                )\n                actions.append(add_asset_action)\n\n            if item_type in (\"asset\", \"task\"):\n                add_task_action = QtWidgets.QAction(\"Add Task\", context_menu)\n                add_task_action.triggered.connect(\n                    self._add_task_action\n                )\n                actions.append(add_task_action)\n\n        # Remove delete tag on items\n        removed_item_ids = []\n        show_delete_items = False\n        for item_id, item in items_by_id.items():\n            if item.data(REMOVED_ROLE):\n                removed_item_ids.append(item_id)\n            elif (\n                not show_delete_items\n                and item.data(ITEM_TYPE_ROLE) != \"project\"\n                and item.data(HIERARCHY_CHANGE_ABLE_ROLE)\n            ):\n                show_delete_items = True\n\n        if show_delete_items:\n            action = QtWidgets.QAction(\"Delete items\", context_menu)\n            action.triggered.connect(\n                lambda: self._delete_items()\n            )\n            actions.append(action)\n\n        if removed_item_ids:\n            action = QtWidgets.QAction(\"Keep items\", context_menu)\n            action.triggered.connect(\n                lambda: self._remove_delete_flag(removed_item_ids)\n            )\n            actions.append(action)\n\n        # Collapse/Expand action\n        show_collapse_expand_action = False\n        for item_id in item_ids:\n            item = items_by_id[item_id]\n            item_type = item.data(ITEM_TYPE_ROLE)\n            if item_type != \"task\":\n                show_collapse_expand_action = True\n                break\n\n        if show_collapse_expand_action:\n            expand_action = QtWidgets.QAction(\"Expand all\", context_menu)\n            collapse_action = QtWidgets.QAction(\"Collapse all\", context_menu)\n            expand_action.triggered.connect(\n                lambda: self._expand_items(indexes)\n            )\n            collapse_action.triggered.connect(\n                lambda: self._collapse_items(indexes)\n            )\n            actions.append(expand_action)\n            actions.append(collapse_action)\n\n        if not actions:\n            return\n\n        for action in actions:\n            context_menu.addAction(action)\n\n        global_point = self.viewport().mapToGlobal(point)\n        context_menu.exec_(global_point)\n"
  },
  {
    "path": "openpype/tools/project_manager/project_manager/widgets.py",
    "content": "import re\nimport platform\n\nfrom openpype.client import get_projects, create_project\nfrom .constants import (\n    NAME_ALLOWED_SYMBOLS,\n    NAME_REGEX\n)\nfrom openpype.client.operations import (\n    PROJECT_NAME_ALLOWED_SYMBOLS,\n    PROJECT_NAME_REGEX,\n    OperationsSession,\n)\nfrom openpype.style import load_stylesheet\nfrom openpype.pipeline import AvalonMongoDB\nfrom openpype.tools.utils import (\n    PlaceholderLineEdit,\n    get_warning_pixmap,\n    PixmapLabel,\n)\nfrom openpype.settings.lib import get_default_anatomy_settings\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\n\nclass NameTextEdit(QtWidgets.QLineEdit):\n    def __init__(self, *args, **kwargs):\n        super(NameTextEdit, self).__init__(*args, **kwargs)\n\n        self.textChanged.connect(self._on_text_change)\n\n    def _on_text_change(self, text):\n        if NAME_REGEX.match(text):\n            return\n\n        idx = self.cursorPosition()\n        before_text = text[0:idx]\n        after_text = text[idx:len(text)]\n        sub_regex = \"[^{}]+\".format(NAME_ALLOWED_SYMBOLS)\n        new_before_text = re.sub(sub_regex, \"\", before_text)\n        new_after_text = re.sub(sub_regex, \"\", after_text)\n        idx -= len(before_text) - len(new_before_text)\n\n        self.setText(new_before_text + new_after_text)\n        self.setCursorPosition(idx)\n\n\nclass FilterComboBox(QtWidgets.QComboBox):\n    def __init__(self, parent=None):\n        super(FilterComboBox, self).__init__(parent)\n\n        self._last_value = None\n\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n        self.setEditable(True)\n        combobox_delegate = QtWidgets.QStyledItemDelegate(self)\n        self.setItemDelegate(combobox_delegate)\n\n        filter_proxy_model = QtCore.QSortFilterProxyModel(self)\n        filter_proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)\n        filter_proxy_model.setSourceModel(self.model())\n\n        completer = QtWidgets.QCompleter(filter_proxy_model, self)\n        completer.setCompletionMode(\n            QtWidgets.QCompleter.UnfilteredPopupCompletion\n        )\n        self.setCompleter(completer)\n\n        completer_view = completer.popup()\n        completer_view.setObjectName(\"CompleterView\")\n        delegate = QtWidgets.QStyledItemDelegate(completer_view)\n        completer_view.setItemDelegate(delegate)\n        completer_view.setStyleSheet(load_stylesheet())\n\n        self.lineEdit().textEdited.connect(\n            filter_proxy_model.setFilterFixedString\n        )\n        completer.activated.connect(self.on_completer_activated)\n\n        self._completer = completer\n        self._filter_proxy_model = filter_proxy_model\n\n    def focusInEvent(self, event):\n        super(FilterComboBox, self).focusInEvent(event)\n        self._last_value = self.lineEdit().text()\n        self.lineEdit().selectAll()\n\n    def value_cleanup(self):\n        text = self.lineEdit().text()\n        idx = self.findText(text)\n        if idx < 0:\n            count = self._completer.completionModel().rowCount()\n            if count > 0:\n                index = self._completer.completionModel().index(0, 0)\n                text = index.data(QtCore.Qt.DisplayRole)\n                idx = self.findText(text)\n            elif self._last_value is not None:\n                idx = self.findText(self._last_value)\n\n        if idx < 0:\n            idx = 0\n        self.setCurrentIndex(idx)\n\n    def on_completer_activated(self, text):\n        if text:\n            index = self.findText(text)\n            self.setCurrentIndex(index)\n\n    def keyPressEvent(self, event):\n        if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):\n            self.value_cleanup()\n\n        super(FilterComboBox, self).keyPressEvent(event)\n\n    def setModel(self, model):\n        super(FilterComboBox, self).setModel(model)\n        self._filter_proxy_model.setSourceModel(model)\n        self._completer.setModel(self._filter_proxy_model)\n\n    def setModelColumn(self, column):\n        self._completer.setCompletionColumn(column)\n        self._filter_proxy_model.setFilterKeyColumn(column)\n        super(FilterComboBox, self).setModelColumn(column)\n\n\nclass CreateProjectDialog(QtWidgets.QDialog):\n    def __init__(self, parent=None, dbcon=None):\n        super(CreateProjectDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"Create Project\")\n\n        self.allowed_regex = \"[^{}]+\".format(PROJECT_NAME_ALLOWED_SYMBOLS)\n\n        if dbcon is None:\n            dbcon = AvalonMongoDB()\n\n        self.dbcon = dbcon\n        self._ignore_code_change = False\n        self._project_name_is_valid = False\n        self._project_code_is_valid = False\n        self._project_code_value = None\n\n        project_names, project_codes = self._get_existing_projects()\n\n        inputs_widget = QtWidgets.QWidget(self)\n        project_name_input = QtWidgets.QLineEdit(inputs_widget)\n        project_code_input = QtWidgets.QLineEdit(inputs_widget)\n        project_width_input = NumScrollWidget(0, 9999999)\n        project_height_input = NumScrollWidget(0, 9999999)\n        project_fps_input = FloatScrollWidget(1, 9999999, decimals=3, step=1)\n        project_aspect_input = FloatScrollWidget(\n            0, 9999999, decimals=2, step=0.1\n        )\n        project_frame_start_input = NumScrollWidget(-9999999, 9999999)\n        project_frame_end_input = NumScrollWidget(-9999999, 9999999)\n\n        default_project_data = self.get_default_attributes()\n        project_width_input.setValue(default_project_data[\"resolutionWidth\"])\n        project_height_input.setValue(default_project_data[\"resolutionHeight\"])\n        project_fps_input.setValue(default_project_data[\"fps\"])\n        project_aspect_input.setValue(default_project_data[\"pixelAspect\"])\n        project_frame_start_input.setValue(default_project_data[\"frameStart\"])\n        project_frame_end_input.setValue(default_project_data[\"frameEnd\"])\n\n        library_project_input = QtWidgets.QCheckBox(inputs_widget)\n\n        inputs_layout = QtWidgets.QFormLayout(inputs_widget)\n        if platform.system() == \"Darwin\":\n            inputs_layout.setFieldGrowthPolicy(\n                QtWidgets.QFormLayout.AllNonFixedFieldsGrow\n            )\n        inputs_layout.setContentsMargins(0, 0, 0, 0)\n        inputs_layout.addRow(\"Project name:\", project_name_input)\n        inputs_layout.addRow(\"Project code:\", project_code_input)\n        inputs_layout.addRow(\"Library project:\", library_project_input)\n        inputs_layout.addRow(\"Width:\", project_width_input)\n        inputs_layout.addRow(\"Height:\", project_height_input)\n        inputs_layout.addRow(\"FPS:\", project_fps_input)\n        inputs_layout.addRow(\"Aspect:\", project_aspect_input)\n        inputs_layout.addRow(\"Frame Start:\", project_frame_start_input)\n        inputs_layout.addRow(\"Frame End:\", project_frame_end_input)\n\n        project_name_label = QtWidgets.QLabel(self)\n        project_code_label = QtWidgets.QLabel(self)\n\n        btns_widget = QtWidgets.QWidget(self)\n        ok_btn = QtWidgets.QPushButton(\"Ok\", btns_widget)\n        ok_btn.setEnabled(False)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", btns_widget)\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(ok_btn)\n        btns_layout.addWidget(cancel_btn)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(inputs_widget, 0)\n        main_layout.addWidget(project_name_label, 1)\n        main_layout.addWidget(project_code_label, 1)\n        main_layout.addStretch(1)\n        main_layout.addWidget(btns_widget, 0)\n\n        project_name_input.textChanged.connect(self._on_project_name_change)\n        project_code_input.textChanged.connect(self._on_project_code_change)\n        ok_btn.clicked.connect(self._on_ok_clicked)\n        cancel_btn.clicked.connect(self._on_cancel_clicked)\n\n        self.invalid_project_names = project_names\n        self.invalid_project_codes = project_codes\n\n        self.project_name_label = project_name_label\n        self.project_code_label = project_code_label\n\n        self.project_name_input = project_name_input\n        self.project_code_input = project_code_input\n        self.library_project_input = library_project_input\n        self.project_width_input = project_width_input\n        self.project_height_input = project_height_input\n        self.project_fps_input = project_fps_input\n        self.project_aspect_input = project_aspect_input\n        self.project_frame_start_input = project_frame_start_input\n        self.project_frame_end_input = project_frame_end_input\n\n        self.ok_btn = ok_btn\n\n    @property\n    def project_name(self):\n        return self.project_name_input.text()\n\n    def get_default_attributes(self):\n        settings = get_default_anatomy_settings()\n        return settings[\"attributes\"]\n\n    def _on_project_name_change(self, value):\n        if self._project_code_value is None:\n            self._ignore_code_change = True\n            self.project_code_input.setText(value.lower())\n            self._ignore_code_change = False\n\n        self._update_valid_project_name(value)\n\n    def _on_project_code_change(self, value):\n        if not value:\n            value = None\n\n        self._update_valid_project_code(value)\n\n        if not self._ignore_code_change:\n            self._project_code_value = value\n\n    def _update_valid_project_name(self, value):\n        message = \"\"\n        is_valid = True\n        if not value:\n            message = \"Project name is empty\"\n            is_valid = False\n\n        elif value in self.invalid_project_names:\n            message = 'Project name \"{}\" already exist'.format(value)\n            is_valid = False\n\n        elif not PROJECT_NAME_REGEX.match(value):\n            message = (\n                'Project name \"{}\" contain not supported symbols'\n            ).format(value)\n            is_valid = False\n\n        self._project_name_is_valid = is_valid\n        self.project_name_label.setText(message)\n        self.project_name_label.setVisible(bool(message))\n        self._enable_button()\n\n    def _update_valid_project_code(self, value):\n        message = \"\"\n        is_valid = True\n        if not value:\n            message = \"Project code is empty\"\n            is_valid = False\n\n        elif value in self.invalid_project_names:\n            message = 'Project code \"{}\" already exist'.format(value)\n            is_valid = False\n\n        elif not PROJECT_NAME_REGEX.match(value):\n            message = (\n                'Project code \"{}\" contain not supported symbols'\n            ).format(value)\n            is_valid = False\n\n        self._project_code_is_valid = is_valid\n        self.project_code_label.setText(message)\n        self._enable_button()\n\n    def _enable_button(self):\n        self.ok_btn.setEnabled(\n            self._project_name_is_valid and self._project_code_is_valid\n        )\n\n    def _on_cancel_clicked(self):\n        self.done(0)\n\n    def _on_ok_clicked(self):\n        if not self._project_name_is_valid or not self._project_code_is_valid:\n            return\n\n        project_name = self.project_name_input.text()\n        project_code = self.project_code_input.text()\n        project_width = self.project_width_input.value()\n        project_height = self.project_height_input.value()\n        project_fps = self.project_fps_input.value()\n        project_aspect = self.project_aspect_input.value()\n        project_frame_start = self.project_frame_start_input.value()\n        project_frame_end = self.project_frame_end_input.value()\n\n        library_project = self.library_project_input.isChecked()\n        project_doc = create_project(\n            project_name,\n            project_code,\n            library_project,\n        )\n        update_data = {\n            \"data.resolutionWidth\": project_width,\n            \"data.resolutionHeight\": project_height,\n            \"data.fps\": project_fps,\n            \"data.pixelAspect\": project_aspect,\n            \"data.frameStart\": project_frame_start,\n            \"data.frameEnd\": project_frame_end,\n        }\n        session = OperationsSession()\n        session.update_entity(\n            project_name,\n            project_doc[\"type\"],\n            project_doc[\"_id\"],\n            update_data,\n        )\n        session.commit()\n        self.done(1)\n\n    def _get_existing_projects(self):\n        project_names = set()\n        project_codes = set()\n        for project_doc in get_projects(\n            inactive=True, fields=[\"name\", \"data.code\"]\n        ):\n            project_name = project_doc.get(\"name\")\n            if not project_name:\n                continue\n\n            project_names.add(project_name)\n            project_code = project_doc.get(\"data\", {}).get(\"code\")\n            if not project_code:\n                project_code = project_name\n\n            project_codes.add(project_code)\n        return project_names, project_codes\n\n\nclass ProjectManagerPixmapLabel(PixmapLabel):\n    def _get_pix_size(self):\n        size = self.fontMetrics().height() * 4\n        return size, size\n\n\nclass ConfirmProjectDeletion(QtWidgets.QDialog):\n    \"\"\"Dialog which confirms deletion of a project.\"\"\"\n\n    def __init__(self, project_name, parent):\n        super(ConfirmProjectDeletion, self).__init__(parent)\n\n        self.setWindowTitle(\"Delete project?\")\n\n        top_widget = QtWidgets.QWidget(self)\n\n        warning_pixmap = get_warning_pixmap()\n        warning_icon_label = ProjectManagerPixmapLabel(\n            warning_pixmap, top_widget\n        )\n\n        message_label = QtWidgets.QLabel(top_widget)\n        message_label.setWordWrap(True)\n        message_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)\n        message_label.setText(\n            (\n                \"<b>WARNING: This cannot be undone.</b><br/><br/>\"\n                'Project <b>\"{}\"</b> with all related data will be'\n                \" permanently removed from the database.\"\n                \" (This action won't remove any files on disk.)\"\n            ).format(project_name)\n        )\n\n        top_layout = QtWidgets.QHBoxLayout(top_widget)\n        top_layout.setContentsMargins(0, 0, 0, 0)\n        top_layout.addWidget(\n            warning_icon_label, 0, QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter\n        )\n        top_layout.addWidget(message_label, 1)\n\n        question_label = QtWidgets.QLabel(\"<b>Are you sure?</b>\", self)\n\n        confirm_input = PlaceholderLineEdit(self)\n        confirm_input.setPlaceholderText(\n            'Type \"{}\" to confirm...'.format(project_name)\n        )\n\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", self)\n        cancel_btn.setToolTip(\"Cancel deletion of the project\")\n        confirm_btn = QtWidgets.QPushButton(\"Permanently Delete Project\", self)\n        confirm_btn.setObjectName(\"DeleteButton\")\n        confirm_btn.setEnabled(False)\n        confirm_btn.setToolTip(\"Confirm deletion\")\n\n        btns_layout = QtWidgets.QHBoxLayout()\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(cancel_btn, 0)\n        btns_layout.addWidget(confirm_btn, 0)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(top_widget, 0)\n        layout.addStretch(1)\n        layout.addWidget(question_label, 0)\n        layout.addWidget(confirm_input, 0)\n        layout.addLayout(btns_layout)\n\n        cancel_btn.clicked.connect(self._on_cancel_click)\n        confirm_btn.clicked.connect(self._on_confirm_click)\n        confirm_input.textChanged.connect(self._on_confirm_text_change)\n        confirm_input.returnPressed.connect(self._on_enter_clicked)\n\n        self._cancel_btn = cancel_btn\n        self._confirm_btn = confirm_btn\n        self._confirm_input = confirm_input\n        self._result = 0\n        self._project_name = project_name\n\n        self.setMinimumWidth(480)\n        self.setMaximumWidth(650)\n        self.setMaximumHeight(250)\n\n    def exec_(self, *args, **kwargs):\n        super(ConfirmProjectDeletion, self).exec_(*args, **kwargs)\n        return self._result\n\n    def showEvent(self, event):\n        \"\"\"Reset result on show.\"\"\"\n        super(ConfirmProjectDeletion, self).showEvent(event)\n        self._result = 0\n        minimum_size_hint = self.minimumSizeHint()\n        self.resize(self.width(), minimum_size_hint.height() + 30)\n\n    def result(self):\n        \"\"\"Get result of dialog 1 for confirm 0 for cancel.\"\"\"\n        return self._result\n\n    def _on_cancel_click(self):\n        self.close()\n\n    def _on_confirm_click(self):\n        self._result = 1\n        self.close()\n\n    def _on_enter_clicked(self):\n        if self._confirm_btn.isEnabled():\n            self._on_confirm_click()\n\n    def _on_confirm_text_change(self):\n        enabled = self._confirm_input.text() == self._project_name\n        self._confirm_btn.setEnabled(enabled)\n\n\nclass SpinBoxScrollFixed(QtWidgets.QSpinBox):\n    \"\"\"QSpinBox which only allow edits change with scroll wheel when active\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(SpinBoxScrollFixed, self).__init__(*args, **kwargs)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n    def wheelEvent(self, event):\n        if not self.hasFocus():\n            event.ignore()\n        else:\n            super(SpinBoxScrollFixed, self).wheelEvent(event)\n\n\nclass DoubleSpinBoxScrollFixed(QtWidgets.QDoubleSpinBox):\n    \"\"\"QDoubleSpinBox which only allow edits with scroll wheel when active\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(DoubleSpinBoxScrollFixed, self).__init__(*args, **kwargs)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n    def wheelEvent(self, event):\n        if not self.hasFocus():\n            event.ignore()\n        else:\n            super(DoubleSpinBoxScrollFixed, self).wheelEvent(event)\n\n\nclass NumScrollWidget(SpinBoxScrollFixed):\n    def __init__(self, minimum, maximum):\n        super(NumScrollWidget, self).__init__()\n        self.setMaximum(maximum)\n        self.setMinimum(minimum)\n        self.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n\n\nclass FloatScrollWidget(DoubleSpinBoxScrollFixed):\n    def __init__(self, minimum, maximum, decimals, step=None):\n        super(FloatScrollWidget, self).__init__()\n        self.setMaximum(maximum)\n        self.setMinimum(minimum)\n        self.setDecimals(decimals)\n        if step is not None:\n            self.setSingleStep(step)\n        self.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n"
  },
  {
    "path": "openpype/tools/project_manager/project_manager/window.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import resources\nfrom openpype.style import load_stylesheet\nfrom openpype.widgets import PasswordDialog\nfrom openpype.lib import is_admin_password_required, Logger\nfrom openpype.pipeline import AvalonMongoDB\nfrom openpype.pipeline.project_folders import create_project_folders\n\nfrom . import (\n    ProjectModel,\n    ProjectProxyFilter,\n\n    HierarchyModel,\n    HierarchySelectionModel,\n    HierarchyView,\n\n    CreateProjectDialog,\n    PROJECT_NAME_ROLE\n)\nfrom .widgets import ConfirmProjectDeletion\nfrom .style import ResourceCache\n\n\nclass ProjectManagerWindow(QtWidgets.QWidget):\n    \"\"\"Main widget of Project Manager tool.\"\"\"\n\n    def __init__(self, parent=None):\n        super(ProjectManagerWindow, self).__init__(parent)\n\n        self.log = Logger.get_logger(self.__class__.__name__)\n\n        self._initial_reset = False\n        self._password_dialog = None\n        self._user_passed = False\n\n        self.setWindowTitle(\"OpenPype Project Manager\")\n        self.setWindowIcon(QtGui.QIcon(resources.get_openpype_icon_filepath()))\n\n        # Top part of window\n        top_part_widget = QtWidgets.QWidget(self)\n\n        # Project selection\n        project_widget = QtWidgets.QWidget(top_part_widget)\n\n        dbcon = AvalonMongoDB()\n\n        project_model = ProjectModel(dbcon)\n        project_proxy = ProjectProxyFilter()\n        project_proxy.setSourceModel(project_model)\n        project_proxy.setDynamicSortFilter(True)\n\n        project_combobox = QtWidgets.QComboBox(project_widget)\n        project_combobox.setSizeAdjustPolicy(\n            QtWidgets.QComboBox.AdjustToContents\n        )\n        project_combobox.setModel(project_proxy)\n        project_combobox.setRootModelIndex(QtCore.QModelIndex())\n        style_delegate = QtWidgets.QStyledItemDelegate()\n        project_combobox.setItemDelegate(style_delegate)\n\n        refresh_projects_btn = QtWidgets.QPushButton(project_widget)\n        refresh_projects_btn.setIcon(ResourceCache.get_icon(\"refresh\"))\n        refresh_projects_btn.setToolTip(\"Refresh projects\")\n        refresh_projects_btn.setObjectName(\"IconBtn\")\n\n        create_project_btn = QtWidgets.QPushButton(\n            \"Create project...\", project_widget\n        )\n        create_folders_btn = QtWidgets.QPushButton(\n            ResourceCache.get_icon(\"asset\", \"default\"),\n            \"Create Starting Folders\",\n            project_widget\n        )\n        create_folders_btn.setEnabled(False)\n\n        remove_projects_btn = QtWidgets.QPushButton(\n            \"Delete project\", project_widget\n        )\n        remove_projects_btn.setIcon(ResourceCache.get_icon(\"remove\"))\n        remove_projects_btn.setObjectName(\"IconBtn\")\n\n        project_layout = QtWidgets.QHBoxLayout(project_widget)\n        project_layout.setContentsMargins(0, 0, 0, 0)\n        project_layout.addWidget(project_combobox, 0)\n        project_layout.addWidget(refresh_projects_btn, 0)\n        project_layout.addWidget(create_project_btn, 0)\n        project_layout.addWidget(create_folders_btn)\n        project_layout.addStretch(1)\n        project_layout.addWidget(remove_projects_btn)\n\n        # Helper buttons\n        helper_btns_widget = QtWidgets.QWidget(top_part_widget)\n\n        helper_label = QtWidgets.QLabel(\"Add:\", helper_btns_widget)\n        add_asset_btn = QtWidgets.QPushButton(\n            ResourceCache.get_icon(\"asset\", \"default\"),\n            \"Asset\",\n            helper_btns_widget\n        )\n        add_task_btn = QtWidgets.QPushButton(\n            ResourceCache.get_icon(\"task\", \"default\"),\n            \"Task\",\n            helper_btns_widget\n        )\n        add_asset_btn.setObjectName(\"IconBtn\")\n        add_asset_btn.setEnabled(False)\n        add_task_btn.setObjectName(\"IconBtn\")\n        add_task_btn.setEnabled(False)\n\n        helper_btns_layout = QtWidgets.QHBoxLayout(helper_btns_widget)\n        helper_btns_layout.setContentsMargins(0, 0, 0, 0)\n        helper_btns_layout.addWidget(helper_label)\n        helper_btns_layout.addWidget(add_asset_btn)\n        helper_btns_layout.addWidget(add_task_btn)\n        helper_btns_layout.addStretch(1)\n\n        # Add widgets to top widget layout\n        top_part_layout = QtWidgets.QVBoxLayout(top_part_widget)\n        top_part_layout.setContentsMargins(0, 0, 0, 0)\n        top_part_layout.addWidget(project_widget)\n        top_part_layout.addWidget(helper_btns_widget)\n\n        hierarchy_model = HierarchyModel(dbcon)\n\n        hierarchy_view = HierarchyView(dbcon, hierarchy_model, self)\n        hierarchy_view.setModel(hierarchy_model)\n\n        _selection_model = HierarchySelectionModel(\n            hierarchy_model.multiselection_column_indexes\n        )\n        _selection_model.setModel(hierarchy_view.model())\n        hierarchy_view.setSelectionModel(_selection_model)\n\n        buttons_widget = QtWidgets.QWidget(self)\n\n        message_label = QtWidgets.QLabel(buttons_widget)\n        save_btn = QtWidgets.QPushButton(\"Save\", buttons_widget)\n        save_btn.setEnabled(False)\n\n        buttons_layout = QtWidgets.QHBoxLayout(buttons_widget)\n        buttons_layout.setContentsMargins(0, 0, 0, 0)\n        buttons_layout.addWidget(message_label)\n        buttons_layout.addStretch(1)\n        buttons_layout.addWidget(save_btn)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(top_part_widget)\n        main_layout.addWidget(hierarchy_view)\n        main_layout.addWidget(buttons_widget)\n\n        refresh_projects_btn.clicked.connect(self._on_project_refresh)\n        create_project_btn.clicked.connect(self._on_project_create)\n        create_folders_btn.clicked.connect(self._on_create_folders)\n        remove_projects_btn.clicked.connect(self._on_remove_project)\n        project_combobox.currentIndexChanged.connect(self._on_project_change)\n        save_btn.clicked.connect(self._on_save_click)\n        add_asset_btn.clicked.connect(self._on_add_asset)\n        add_task_btn.clicked.connect(self._on_add_task)\n\n        self._dbcon = dbcon\n        self._project_model = project_model\n        self._project_proxy_model = project_proxy\n\n        self.hierarchy_view = hierarchy_view\n        self.hierarchy_model = hierarchy_model\n\n        self.message_label = message_label\n\n        self._refresh_projects_btn = refresh_projects_btn\n        self._project_combobox = project_combobox\n        self._create_project_btn = create_project_btn\n        self._create_folders_btn = create_folders_btn\n        self._remove_projects_btn = remove_projects_btn\n        self._save_btn = save_btn\n\n        self._add_asset_btn = add_asset_btn\n        self._add_task_btn = add_task_btn\n\n        self.resize(1200, 600)\n        self.setStyleSheet(load_stylesheet())\n\n    def _set_project(self, project_name=None, force=False):\n        self._create_folders_btn.setEnabled(project_name is not None)\n        self._remove_projects_btn.setEnabled(project_name is not None)\n        self._add_asset_btn.setEnabled(project_name is not None)\n        self._add_task_btn.setEnabled(project_name is not None)\n        self._save_btn.setEnabled(project_name is not None)\n        self._project_proxy_model.set_filter_default(project_name is not None)\n        self.hierarchy_view.set_project(project_name, force)\n\n    def _current_project(self):\n        row = self._project_combobox.currentIndex()\n        if row < 0:\n            return None\n        index = self._project_proxy_model.index(row, 0)\n        return index.data(PROJECT_NAME_ROLE)\n\n    def showEvent(self, event):\n        super(ProjectManagerWindow, self).showEvent(event)\n\n        if not self._initial_reset:\n            self.reset()\n\n        font_size = self._refresh_projects_btn.fontMetrics().height()\n        icon_size = QtCore.QSize(font_size, font_size)\n        self._refresh_projects_btn.setIconSize(icon_size)\n        self._add_asset_btn.setIconSize(icon_size)\n        self._add_task_btn.setIconSize(icon_size)\n\n    def refresh_projects(self, project_name=None):\n        if project_name is None:\n            if self._project_combobox.count() > 0:\n                project_name = self._project_combobox.currentText()\n\n        self._project_model.refresh()\n        self._project_proxy_model.sort(0, QtCore.Qt.AscendingOrder)\n\n        if self._project_combobox.count() == 0:\n            return self._set_project()\n\n        if project_name:\n            row = self._project_combobox.findText(project_name)\n            if row >= 0:\n                self._project_combobox.setCurrentIndex(row)\n\n        selected_project = self._current_project()\n        self._set_project(selected_project, True)\n\n    def _on_project_change(self):\n        selected_project = self._current_project()\n        self._set_project(selected_project, False)\n\n    def _on_project_refresh(self):\n        self.refresh_projects()\n\n    def _on_save_click(self):\n        self.hierarchy_model.save()\n\n    def _on_add_asset(self):\n        self.hierarchy_view.add_asset()\n\n    def _on_add_task(self):\n        self.hierarchy_view.add_task_and_edit()\n\n    def _on_create_folders(self):\n        project_name = self._current_project()\n        if not project_name:\n            return\n\n        result = QtWidgets.QMessageBox.question(\n            self,\n            \"OpenPype Project Manager\",\n            \"Confirm to create starting project folders?\",\n            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No\n        )\n        if result == QtWidgets.QMessageBox.Yes:\n            try:\n                # Invoking OpenPype API to create the project folders\n                create_project_folders(project_name)\n            except Exception as exc:\n                self.log.warning(\n                    \"Cannot create starting folders: {}\".format(exc),\n                    exc_info=True\n                )\n\n    def _on_remove_project(self):\n        project_name = self._current_project()\n        dialog = ConfirmProjectDeletion(project_name, self)\n        result = dialog.exec_()\n        if result != 1:\n            return\n\n        database = self._dbcon.database\n        if project_name in database.collection_names():\n            collection = database[project_name]\n            collection.drop()\n        self.refresh_projects()\n\n    def show_message(self, message):\n        # TODO add nicer message pop\n        self.message_label.setText(message)\n\n    def _on_project_create(self):\n        dialog = CreateProjectDialog(self)\n        dialog.exec_()\n        if dialog.result() != 1:\n            return\n\n        project_name = dialog.project_name\n        self.show_message(\"Created project \\\"{}\\\"\".format(project_name))\n        self.refresh_projects(project_name)\n\n    def _show_password_dialog(self):\n        if self._password_dialog:\n            self._password_dialog.open()\n\n    def _on_password_dialog_close(self, password_passed):\n        # Store result for future settings reset\n        self._user_passed = password_passed\n        # Remove reference to password dialog\n        self._password_dialog = None\n        if password_passed:\n            self.reset()\n        else:\n            self.close()\n\n    def reset(self):\n        if self._password_dialog:\n            return\n\n        if not self._user_passed:\n            self._user_passed = not is_admin_password_required()\n\n        if not self._user_passed:\n            self.setEnabled(False)\n            # Avoid doubled dialog\n            dialog = PasswordDialog(self)\n            dialog.setModal(True)\n            dialog.finished.connect(self._on_password_dialog_close)\n\n            self._password_dialog = dialog\n\n            QtCore.QTimer.singleShot(100, self._show_password_dialog)\n\n            return\n\n        self.setEnabled(True)\n\n        # Mark as was reset\n        if not self._initial_reset:\n            self._initial_reset = True\n\n        self.refresh_projects()\n"
  },
  {
    "path": "openpype/tools/publisher/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/tools/publisher/app.py",
    "content": "from .window import PublisherWindow\n\n\nclass _WindowCache:\n    window = None\n\n\ndef show(parent=None):\n    window = _WindowCache.window\n    if window is None:\n        window = PublisherWindow(parent)\n        _WindowCache.window = window\n\n    window.show()\n    window.activateWindow()\n\n    return window\n"
  },
  {
    "path": "openpype/tools/publisher/constants.py",
    "content": "from qtpy import QtCore, QtGui\n\n# ID of context item in instance view\nCONTEXT_ID = \"context\"\nCONTEXT_LABEL = \"Context\"\n# Not showed anywhere - used as identifier\nCONTEXT_GROUP = \"__ContextGroup__\"\n\nCONVERTOR_ITEM_GROUP = \"Incompatible subsets\"\n\n# Allowed symbols for subset name (and variant)\n# - characters, numbers, unsercore and dash\nVARIANT_TOOLTIP = (\n    \"Variant may contain alphabetical characters (a-Z)\"\n    \"\\nnumerical characters (0-9) dot (\\\".\\\") or underscore (\\\"_\\\").\"\n)\n\nINPUTS_LAYOUT_HSPACING = 4\nINPUTS_LAYOUT_VSPACING = 2\n\n# Roles for instance views\nINSTANCE_ID_ROLE = QtCore.Qt.UserRole + 1\nSORT_VALUE_ROLE = QtCore.Qt.UserRole + 2\nIS_GROUP_ROLE = QtCore.Qt.UserRole + 3\nCREATOR_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 4\nCREATOR_THUMBNAIL_ENABLED_ROLE = QtCore.Qt.UserRole + 5\nFAMILY_ROLE = QtCore.Qt.UserRole + 6\nGROUP_ROLE = QtCore.Qt.UserRole + 7\nCONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 8\nCREATOR_SORT_ROLE = QtCore.Qt.UserRole + 9\n\nResetKeySequence = QtGui.QKeySequence(\n    QtCore.Qt.ControlModifier | QtCore.Qt.Key_R\n)\n\n__all__ = (\n    \"CONTEXT_ID\",\n    \"CONTEXT_LABEL\",\n\n    \"VARIANT_TOOLTIP\",\n\n    \"INPUTS_LAYOUT_HSPACING\",\n    \"INPUTS_LAYOUT_VSPACING\",\n\n    \"INSTANCE_ID_ROLE\",\n    \"SORT_VALUE_ROLE\",\n    \"IS_GROUP_ROLE\",\n    \"CREATOR_IDENTIFIER_ROLE\",\n    \"CREATOR_THUMBNAIL_ENABLED_ROLE\",\n    \"CREATOR_SORT_ROLE\",\n    \"FAMILY_ROLE\",\n    \"GROUP_ROLE\",\n    \"CONVERTER_IDENTIFIER_ROLE\",\n\n    \"ResetKeySequence\",\n)\n"
  },
  {
    "path": "openpype/tools/publisher/control.py",
    "content": "import os\nimport copy\nimport logging\nimport traceback\nimport collections\nimport uuid\nimport tempfile\nimport shutil\nimport inspect\nfrom abc import ABCMeta, abstractmethod\n\nimport six\nimport arrow\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_assets,\n    get_asset_by_id,\n    get_subsets,\n    get_asset_name_identifier,\n)\nfrom openpype.lib.events import EventSystem\nfrom openpype.lib.attribute_definitions import (\n    UIDef,\n    serialize_attr_defs,\n    deserialize_attr_defs,\n)\nfrom openpype.pipeline import (\n    PublishValidationError,\n    KnownPublishError,\n    registered_host,\n    get_process_id,\n    OptionalPyblishPluginMixin,\n)\nfrom openpype.pipeline.create import (\n    CreateContext,\n    AutoCreator,\n    HiddenCreator,\n    Creator,\n)\nfrom openpype.pipeline.create.context import (\n    CreatorsOperationFailed,\n    ConvertorsOperationFailed,\n)\nfrom openpype.pipeline.publish import get_publish_instance_label\n\n# Define constant for plugin orders offset\nPLUGIN_ORDER_OFFSET = 0.5\n\n\nclass CardMessageTypes:\n    standard = None\n    info = \"info\"\n    error = \"error\"\n\n\nclass MainThreadItem:\n    \"\"\"Callback with args and kwargs.\"\"\"\n\n    def __init__(self, callback, *args, **kwargs):\n        self.callback = callback\n        self.args = args\n        self.kwargs = kwargs\n\n    def process(self):\n        self.callback(*self.args, **self.kwargs)\n\n\nclass AssetDocsCache:\n    \"\"\"Cache asset documents for creation part.\"\"\"\n\n    projection = {\n        \"_id\": True,\n        \"name\": True,\n        \"data.visualParent\": True,\n        \"data.tasks\": True\n    }\n    if AYON_SERVER_ENABLED:\n        projection[\"data.parents\"] = True\n\n    def __init__(self, controller):\n        self._controller = controller\n        self._asset_docs = None\n        self._asset_docs_hierarchy = None\n        self._task_names_by_asset_name = {}\n        self._asset_docs_by_name = {}\n        self._full_asset_docs_by_name = {}\n\n    def reset(self):\n        self._asset_docs = None\n        self._asset_docs_hierarchy = None\n        self._task_names_by_asset_name = {}\n        self._asset_docs_by_name = {}\n        self._full_asset_docs_by_name = {}\n\n    def _query(self):\n        if self._asset_docs is not None:\n            return\n\n        project_name = self._controller.project_name\n        asset_docs = list(get_assets(\n            project_name, fields=self.projection.keys()\n        ))\n        asset_docs_by_name = {}\n        task_names_by_asset_name = {}\n        for asset_doc in asset_docs:\n            if \"data\" not in asset_doc:\n                asset_doc[\"data\"] = {\"tasks\": {}, \"visualParent\": None}\n            elif \"tasks\" not in asset_doc[\"data\"]:\n                asset_doc[\"data\"][\"tasks\"] = {}\n\n            asset_name = get_asset_name_identifier(asset_doc)\n            asset_tasks = asset_doc[\"data\"][\"tasks\"]\n            task_names_by_asset_name[asset_name] = list(asset_tasks.keys())\n            asset_docs_by_name[asset_name] = asset_doc\n\n        self._asset_docs = asset_docs\n        self._asset_docs_by_name = asset_docs_by_name\n        self._task_names_by_asset_name = task_names_by_asset_name\n\n    def get_asset_docs(self):\n        self._query()\n        return copy.deepcopy(self._asset_docs)\n\n    def get_asset_hierarchy(self):\n        \"\"\"Prepare asset documents into hierarchy.\n\n        Convert ObjectId to string. Asset id is not used during whole\n        process of publisher but asset name is used rather.\n\n        Returns:\n            Dict[Union[str, None]: Any]: Mapping of parent id to it's children.\n                Top level assets have parent id 'None'.\n        \"\"\"\n\n        if self._asset_docs_hierarchy is None:\n            _queue = collections.deque(self.get_asset_docs())\n\n            output = collections.defaultdict(list)\n            while _queue:\n                asset_doc = _queue.popleft()\n                asset_doc[\"_id\"] = str(asset_doc[\"_id\"])\n                parent_id = asset_doc[\"data\"][\"visualParent\"]\n                if parent_id is not None:\n                    parent_id = str(parent_id)\n                    asset_doc[\"data\"][\"visualParent\"] = parent_id\n                output[parent_id].append(asset_doc)\n            self._asset_docs_hierarchy = output\n        return copy.deepcopy(self._asset_docs_hierarchy)\n\n    def get_task_names_by_asset_name(self):\n        self._query()\n        return copy.deepcopy(self._task_names_by_asset_name)\n\n    def get_asset_by_name(self, asset_name):\n        self._query()\n        asset_doc = self._asset_docs_by_name.get(asset_name)\n        if asset_doc is None:\n            return None\n        return copy.deepcopy(asset_doc)\n\n    def get_full_asset_by_name(self, asset_name):\n        self._query()\n        if asset_name not in self._full_asset_docs_by_name:\n            asset_doc = self._asset_docs_by_name.get(asset_name)\n            project_name = self._controller.project_name\n            full_asset_doc = get_asset_by_id(project_name, asset_doc[\"_id\"])\n            self._full_asset_docs_by_name[asset_name] = full_asset_doc\n        return copy.deepcopy(self._full_asset_docs_by_name[asset_name])\n\n\nclass PublishReportMaker:\n    \"\"\"Report for single publishing process.\n\n    Report keeps current state of publishing and currently processed plugin.\n    \"\"\"\n\n    def __init__(self, controller):\n        self.controller = controller\n        self._create_discover_result = None\n        self._convert_discover_result = None\n        self._publish_discover_result = None\n\n        self._plugin_data_by_id = {}\n        self._current_plugin = None\n        self._current_plugin_data = {}\n        self._all_instances_by_id = {}\n        self._current_context = None\n\n    def reset(self, context, create_context):\n        \"\"\"Reset report and clear all data.\"\"\"\n\n        self._create_discover_result = create_context.creator_discover_result\n        self._convert_discover_result = (\n            create_context.convertor_discover_result\n        )\n        self._publish_discover_result = create_context.publish_discover_result\n\n        self._plugin_data_by_id = {}\n        self._current_plugin = None\n        self._current_plugin_data = {}\n        self._all_instances_by_id = {}\n        self._current_context = context\n\n        for plugin in create_context.publish_plugins_mismatch_targets:\n            plugin_data = self._add_plugin_data_item(plugin)\n            plugin_data[\"skipped\"] = True\n\n    def add_plugin_iter(self, plugin, context):\n        \"\"\"Add report about single iteration of plugin.\"\"\"\n        for instance in context:\n            self._all_instances_by_id[instance.id] = instance\n\n        if self._current_plugin_data:\n            self._current_plugin_data[\"passed\"] = True\n\n        self._current_plugin = plugin\n        self._current_plugin_data = self._add_plugin_data_item(plugin)\n\n    def _add_plugin_data_item(self, plugin):\n        if plugin.id in self._plugin_data_by_id:\n            # A plugin would be processed more than once. What can cause it:\n            #   - there is a bug in controller\n            #   - plugin class is imported into multiple files\n            #       - this can happen even with base classes from 'pyblish'\n            raise ValueError(\n                \"Plugin '{}' is already stored\".format(str(plugin)))\n\n        plugin_data_item = self._create_plugin_data_item(plugin)\n        self._plugin_data_by_id[plugin.id] = plugin_data_item\n\n        return plugin_data_item\n\n    def _create_plugin_data_item(self, plugin):\n        label = None\n        if hasattr(plugin, \"label\"):\n            label = plugin.label\n\n        return {\n            \"id\": plugin.id,\n            \"name\": plugin.__name__,\n            \"label\": label,\n            \"order\": plugin.order,\n            \"targets\": list(plugin.targets),\n            \"instances_data\": [],\n            \"actions_data\": [],\n            \"skipped\": False,\n            \"passed\": False\n        }\n\n    def set_plugin_skipped(self):\n        \"\"\"Set that current plugin has been skipped.\"\"\"\n        self._current_plugin_data[\"skipped\"] = True\n\n    def add_result(self, result):\n        \"\"\"Handle result of one plugin and it's instance.\"\"\"\n\n        instance = result[\"instance\"]\n        instance_id = None\n        if instance is not None:\n            instance_id = instance.id\n        self._current_plugin_data[\"instances_data\"].append({\n            \"id\": instance_id,\n            \"logs\": self._extract_instance_log_items(result),\n            \"process_time\": result[\"duration\"]\n        })\n\n    def add_action_result(self, action, result):\n        \"\"\"Add result of single action.\"\"\"\n        plugin = result[\"plugin\"]\n\n        store_item = self._plugin_data_by_id.get(plugin.id)\n        if store_item is None:\n            store_item = self._add_plugin_data_item(plugin)\n\n        action_name = action.__name__\n        action_label = action.label or action_name\n        log_items = self._extract_log_items(result)\n        store_item[\"actions_data\"].append({\n            \"success\": result[\"success\"],\n            \"name\": action_name,\n            \"label\": action_label,\n            \"logs\": log_items\n        })\n\n    def get_report(self, publish_plugins=None):\n        \"\"\"Report data with all details of current state.\"\"\"\n\n        now = arrow.utcnow().to(\"local\")\n        instances_details = {}\n        for instance in self._all_instances_by_id.values():\n            instances_details[instance.id] = self._extract_instance_data(\n                instance, instance in self._current_context\n            )\n\n        plugins_data_by_id = copy.deepcopy(\n            self._plugin_data_by_id\n        )\n\n        # Ensure the current plug-in is marked as `passed` in the result\n        # so that it shows on reports for paused publishes\n        if self._current_plugin is not None:\n            current_plugin_data = plugins_data_by_id.get(\n                self._current_plugin.id\n            )\n            if current_plugin_data and not current_plugin_data[\"passed\"]:\n                current_plugin_data[\"passed\"] = True\n\n        if publish_plugins:\n            for plugin in publish_plugins:\n                if plugin.id not in plugins_data_by_id:\n                    plugins_data_by_id[plugin.id] = \\\n                        self._create_plugin_data_item(plugin)\n\n        reports = []\n        if self._create_discover_result is not None:\n            reports.append(self._create_discover_result)\n\n        if self._convert_discover_result is not None:\n            reports.append(self._convert_discover_result)\n\n        if self._publish_discover_result is not None:\n            reports.append(self._publish_discover_result)\n\n        crashed_file_paths = {}\n        for report in reports:\n            items = report.crashed_file_paths.items()\n            for filepath, exc_info in items:\n                crashed_file_paths[filepath] = \"\".join(\n                    traceback.format_exception(*exc_info)\n                )\n\n        return {\n            \"plugins_data\": list(plugins_data_by_id.values()),\n            \"instances\": instances_details,\n            \"context\": self._extract_context_data(self._current_context),\n            \"crashed_file_paths\": crashed_file_paths,\n            \"id\": uuid.uuid4().hex,\n            \"created_at\": now.isoformat(),\n            \"report_version\": \"1.0.1\",\n        }\n\n    def _extract_context_data(self, context):\n        context_label = \"Context\"\n        if context is not None:\n            context_label = context.data.get(\"label\")\n        return {\n            \"label\": context_label\n        }\n\n    def _extract_instance_data(self, instance, exists):\n        return {\n            \"name\": instance.data.get(\"name\"),\n            \"label\": get_publish_instance_label(instance),\n            \"family\": instance.data[\"family\"],\n            \"families\": instance.data.get(\"families\") or [],\n            \"exists\": exists,\n            \"creator_identifier\": instance.data.get(\"creator_identifier\"),\n            \"instance_id\": instance.data.get(\"instance_id\"),\n        }\n\n    def _extract_instance_log_items(self, result):\n        instance = result[\"instance\"]\n        instance_id = None\n        if instance:\n            instance_id = instance.id\n\n        log_items = self._extract_log_items(result)\n        for item in log_items:\n            item[\"instance_id\"] = instance_id\n        return log_items\n\n    def _extract_log_items(self, result):\n        output = []\n        records = result.get(\"records\") or []\n        for record in records:\n            record_exc_info = record.exc_info\n            if record_exc_info is not None:\n                record_exc_info = \"\".join(\n                    traceback.format_exception(*record_exc_info)\n                )\n\n            try:\n                msg = record.getMessage()\n            except Exception:\n                msg = str(record.msg)\n\n            output.append({\n                \"type\": \"record\",\n                \"msg\": msg,\n                \"name\": record.name,\n                \"lineno\": record.lineno,\n                \"levelno\": record.levelno,\n                \"levelname\": record.levelname,\n                \"threadName\": record.threadName,\n                \"filename\": record.filename,\n                \"pathname\": record.pathname,\n                \"msecs\": record.msecs,\n                \"exc_info\": record_exc_info\n            })\n\n        exception = result.get(\"error\")\n        if exception:\n            fname, line_no, func, exc = exception.traceback\n\n            # Conversion of exception into string may crash\n            try:\n                msg = str(exception)\n            except BaseException:\n                msg = (\n                    \"Publisher Controller: ERROR\"\n                    \" - Failed to get exception message\"\n                )\n\n            # Action result does not have 'is_validation_error'\n            is_validation_error = result.get(\"is_validation_error\", False)\n            output.append({\n                \"type\": \"error\",\n                \"is_validation_error\": is_validation_error,\n                \"msg\": msg,\n                \"filename\": str(fname),\n                \"lineno\": str(line_no),\n                \"func\": str(func),\n                \"traceback\": exception.formatted_traceback\n            })\n\n        return output\n\n\nclass PublishPluginsProxy:\n    \"\"\"Wrapper around publish plugin.\n\n    Prepare mapping for publish plugins and actions. Also can create\n    serializable data for plugin actions so UI don't have to have access to\n    them.\n\n    This object is created in process where publishing is actually running.\n\n    Notes:\n        Actions have id but single action can be used on multiple plugins so\n            to run an action is needed combination of plugin and action.\n\n    Args:\n        plugins [List[pyblish.api.Plugin]]: Discovered plugins that will be\n            processed.\n    \"\"\"\n\n    def __init__(self, plugins):\n        plugins_by_id = {}\n        actions_by_plugin_id = {}\n        action_ids_by_plugin_id = {}\n        for plugin in plugins:\n            plugin_id = plugin.id\n            plugins_by_id[plugin_id] = plugin\n\n            action_ids = []\n            actions_by_id = {}\n            action_ids_by_plugin_id[plugin_id] = action_ids\n            actions_by_plugin_id[plugin_id] = actions_by_id\n\n            actions = getattr(plugin, \"actions\", None) or []\n            for action in actions:\n                action_id = action.id\n                action_ids.append(action_id)\n                actions_by_id[action_id] = action\n\n        self._plugins_by_id = plugins_by_id\n        self._actions_by_plugin_id = actions_by_plugin_id\n        self._action_ids_by_plugin_id = action_ids_by_plugin_id\n\n    def get_action(self, plugin_id, action_id):\n        return self._actions_by_plugin_id[plugin_id][action_id]\n\n    def get_plugin(self, plugin_id):\n        return self._plugins_by_id[plugin_id]\n\n    def get_plugin_id(self, plugin):\n        \"\"\"Get id of plugin based on plugin object.\n\n        It's used for validation errors report.\n\n        Args:\n            plugin (pyblish.api.Plugin): Publish plugin for which id should be\n                returned.\n\n        Returns:\n            str: Plugin id.\n        \"\"\"\n\n        return plugin.id\n\n    def get_plugin_action_items(self, plugin_id):\n        \"\"\"Get plugin action items for plugin by its id.\n\n        Args:\n            plugin_id (str): Publish plugin id.\n\n        Returns:\n            List[PublishPluginActionItem]: Items with information about publish\n                plugin actions.\n        \"\"\"\n\n        return [\n            self._create_action_item(\n                self.get_action(plugin_id, action_id), plugin_id\n            )\n            for action_id in self._action_ids_by_plugin_id[plugin_id]\n        ]\n\n    def _create_action_item(self, action, plugin_id):\n        label = action.label or action.__name__\n        icon = getattr(action, \"icon\", None)\n        return PublishPluginActionItem(\n            action.id,\n            plugin_id,\n            action.active,\n            action.on,\n            label,\n            icon\n        )\n\n\nclass PublishPluginActionItem:\n    \"\"\"Representation of publish plugin action.\n\n    Data driven object which is used as proxy for controller and UI.\n\n    Args:\n        action_id (str): Action id.\n        plugin_id (str): Plugin id.\n        active (bool): Action is active.\n        on_filter (str): Actions have 'on' attribte which define when can be\n            action triggered (e.g. 'all', 'failed', ...).\n        label (str): Action's label.\n        icon (Union[str, None]) Action's icon.\n    \"\"\"\n\n    def __init__(self, action_id, plugin_id, active, on_filter, label, icon):\n        self.action_id = action_id\n        self.plugin_id = plugin_id\n        self.active = active\n        self.on_filter = on_filter\n        self.label = label\n        self.icon = icon\n\n    def to_data(self):\n        \"\"\"Serialize object to dictionary.\n\n        Returns:\n            Dict[str, Union[str,bool,None]]: Serialized object.\n        \"\"\"\n\n        return {\n            \"action_id\": self.action_id,\n            \"plugin_id\": self.plugin_id,\n            \"active\": self.active,\n            \"on_filter\": self.on_filter,\n            \"label\": self.label,\n            \"icon\": self.icon\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        \"\"\"Create object from data.\n\n        Args:\n            data (Dict[str, Union[str,bool,None]]): Data used to recreate\n                object.\n\n        Returns:\n            PublishPluginActionItem: Object created using data.\n        \"\"\"\n\n        return cls(**data)\n\n\nclass ValidationErrorItem:\n    \"\"\"Data driven validation error item.\n\n    Prepared data container with information about validation error and it's\n    source plugin.\n\n    Can be converted to raw data and recreated should be used for controller\n    and UI connection.\n\n    Args:\n        instance_id (str): Id of pyblish instance to which is validation error\n            connected.\n        instance_label (str): Prepared instance label.\n        plugin_id (str): Id of pyblish Plugin which triggered the validation\n            error. Id is generated using 'PublishPluginsProxy'.\n    \"\"\"\n\n    def __init__(\n        self,\n        instance_id,\n        instance_label,\n        plugin_id,\n        context_validation,\n        title,\n        description,\n        detail\n    ):\n        self.instance_id = instance_id\n        self.instance_label = instance_label\n        self.plugin_id = plugin_id\n        self.context_validation = context_validation\n        self.title = title\n        self.description = description\n        self.detail = detail\n\n    def to_data(self):\n        \"\"\"Serialize object to dictionary.\n\n        Returns:\n            Dict[str, Union[str, bool, None]]: Serialized object data.\n        \"\"\"\n\n        return {\n            \"instance_id\": self.instance_id,\n            \"instance_label\": self.instance_label,\n            \"plugin_id\": self.plugin_id,\n            \"context_validation\": self.context_validation,\n            \"title\": self.title,\n            \"description\": self.description,\n            \"detail\": self.detail,\n        }\n\n    @classmethod\n    def from_result(cls, plugin_id, error, instance):\n        \"\"\"Create new object based on resukt from controller.\n\n        Returns:\n            ValidationErrorItem: New object with filled data.\n        \"\"\"\n\n        instance_label = None\n        instance_id = None\n        if instance is not None:\n            instance_label = (\n                instance.data.get(\"label\") or instance.data.get(\"name\")\n            )\n            instance_id = instance.id\n\n        return cls(\n            instance_id,\n            instance_label,\n            plugin_id,\n            instance is None,\n            error.title,\n            error.description,\n            error.detail,\n        )\n\n    @classmethod\n    def from_data(cls, data):\n        return cls(**data)\n\n\nclass PublishValidationErrorsReport:\n    \"\"\"Publish validation errors report that can be parsed to raw data.\n\n    Args:\n        error_items (List[ValidationErrorItem]): List of validation errors.\n        plugin_action_items (Dict[str, PublishPluginActionItem]): Action items\n            by plugin id.\n    \"\"\"\n\n    def __init__(self, error_items, plugin_action_items):\n        self._error_items = error_items\n        self._plugin_action_items = plugin_action_items\n\n    def __iter__(self):\n        for item in self._error_items:\n            yield item\n\n    def group_items_by_title(self):\n        \"\"\"Group errors by plugin and their titles.\n\n        Items are grouped by plugin and title -> same title from different\n        plugin is different item. Items are ordered by plugin order.\n\n        Returns:\n            List[Dict[str, Any]]: List where each item title, instance\n                information related to title and possible plugin actions.\n        \"\"\"\n\n        ordered_plugin_ids = []\n        error_items_by_plugin_id = collections.defaultdict(list)\n        for error_item in self._error_items:\n            plugin_id = error_item.plugin_id\n            if plugin_id not in ordered_plugin_ids:\n                ordered_plugin_ids.append(plugin_id)\n            error_items_by_plugin_id[plugin_id].append(error_item)\n\n        grouped_error_items = []\n        for plugin_id in ordered_plugin_ids:\n            plugin_action_items = self._plugin_action_items[plugin_id]\n            error_items = error_items_by_plugin_id[plugin_id]\n\n            titles = []\n            error_items_by_title = collections.defaultdict(list)\n            for error_item in error_items:\n                title = error_item.title\n                if title not in titles:\n                    titles.append(error_item.title)\n                error_items_by_title[title].append(error_item)\n\n            for title in titles:\n                grouped_error_items.append({\n                    \"id\": uuid.uuid4().hex,\n                    \"plugin_id\": plugin_id,\n                    \"plugin_action_items\": list(plugin_action_items),\n                    \"error_items\": error_items_by_title[title],\n                    \"title\": title\n                })\n        return grouped_error_items\n\n    def to_data(self):\n        \"\"\"Serialize object to dictionary.\n\n        Returns:\n            Dict[str, Any]: Serialized data.\n        \"\"\"\n\n        error_items = [\n            item.to_data()\n            for item in self._error_items\n        ]\n\n        plugin_action_items = {\n            plugin_id: [\n                action_item.to_data()\n                for action_item in action_items\n            ]\n            for plugin_id, action_items in self._plugin_action_items.items()\n        }\n\n        return {\n            \"error_items\": error_items,\n            \"plugin_action_items\": plugin_action_items\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        \"\"\"Recreate object from data.\n\n        Args:\n            data (dict[str, Any]): Data to recreate object. Can be created\n                using 'to_data' method.\n\n        Returns:\n            PublishValidationErrorsReport: New object based on data.\n        \"\"\"\n\n        error_items = [\n            ValidationErrorItem.from_data(error_item)\n            for error_item in data[\"error_items\"]\n        ]\n        plugin_action_items = [\n            PublishPluginActionItem.from_data(action_item)\n            for action_item in data[\"plugin_action_items\"]\n        ]\n        return cls(error_items, plugin_action_items)\n\n\nclass PublishValidationErrors:\n    \"\"\"Object to keep track about validation errors by plugin.\"\"\"\n\n    def __init__(self):\n        self._plugins_proxy = None\n        self._error_items = []\n        self._plugin_action_items = {}\n\n    def __bool__(self):\n        return self.has_errors\n\n    @property\n    def has_errors(self):\n        \"\"\"At least one error was added.\"\"\"\n\n        return bool(self._error_items)\n\n    def reset(self, plugins_proxy):\n        \"\"\"Reset object to default state.\n\n        Args:\n            plugins_proxy (PublishPluginsProxy): Proxy which store plugins,\n                actions by ids and create mapping of action ids by plugin ids.\n        \"\"\"\n\n        self._plugins_proxy = plugins_proxy\n        self._error_items = []\n        self._plugin_action_items = {}\n\n    def create_report(self):\n        \"\"\"Create report based on currently existing errors.\n\n        Returns:\n            PublishValidationErrorsReport: Validation error report with all\n                error information and publish plugin action items.\n        \"\"\"\n\n        return PublishValidationErrorsReport(\n            self._error_items, self._plugin_action_items\n        )\n\n    def add_error(self, plugin, error, instance):\n        \"\"\"Add error from pyblish result.\n\n        Args:\n            plugin (pyblish.api.Plugin): Plugin which triggered error.\n            error (ValidationException): Validation error.\n            instance (Union[pyblish.api.Instance, None]): Instance on which was\n                error raised or None if was raised on context.\n        \"\"\"\n\n        # Make sure the cached report is cleared\n        plugin_id = self._plugins_proxy.get_plugin_id(plugin)\n        if not error.title:\n            if hasattr(plugin, \"label\") and plugin.label:\n                plugin_label = plugin.label\n            else:\n                plugin_label = plugin.__name__\n            error.title = plugin_label\n\n        self._error_items.append(\n            ValidationErrorItem.from_result(plugin_id, error, instance)\n        )\n        if plugin_id in self._plugin_action_items:\n            return\n\n        plugin_actions = self._plugins_proxy.get_plugin_action_items(\n            plugin_id\n        )\n        self._plugin_action_items[plugin_id] = plugin_actions\n\n\nclass CreatorType:\n    def __init__(self, name):\n        self.name = name\n\n    def __str__(self):\n        return self.name\n\n    def __eq__(self, other):\n        return self.name == str(other)\n\n    def __ne__(self, other):\n        # This is implemented only because of Python 2\n        return not self == other\n\n\nclass CreatorTypes:\n    base = CreatorType(\"base\")\n    auto = CreatorType(\"auto\")\n    hidden = CreatorType(\"hidden\")\n    artist = CreatorType(\"artist\")\n\n    @classmethod\n    def from_str(cls, value):\n        for creator_type in (\n            cls.base,\n            cls.auto,\n            cls.hidden,\n            cls.artist\n        ):\n            if value == creator_type:\n                return creator_type\n        raise ValueError(\"Unknown type \\\"{}\\\"\".format(str(value)))\n\n\nclass CreatorItem:\n    \"\"\"Wrapper around Creator plugin.\n\n    Object can be serialized and recreated.\n    \"\"\"\n\n    def __init__(\n        self,\n        identifier,\n        creator_type,\n        family,\n        label,\n        group_label,\n        icon,\n        description,\n        detailed_description,\n        default_variant,\n        default_variants,\n        create_allow_context_change,\n        create_allow_thumbnail,\n        show_order,\n        pre_create_attributes_defs,\n    ):\n        self.identifier = identifier\n        self.creator_type = creator_type\n        self.family = family\n        self.label = label\n        self.group_label = group_label\n        self.icon = icon\n        self.description = description\n        self.detailed_description = detailed_description\n        self.default_variant = default_variant\n        self.default_variants = default_variants\n        self.create_allow_context_change = create_allow_context_change\n        self.create_allow_thumbnail = create_allow_thumbnail\n        self.show_order = show_order\n        self.pre_create_attributes_defs = pre_create_attributes_defs\n\n    def get_group_label(self):\n        return self.group_label\n\n    @classmethod\n    def from_creator(cls, creator):\n        if isinstance(creator, AutoCreator):\n            creator_type = CreatorTypes.auto\n        elif isinstance(creator, HiddenCreator):\n            creator_type = CreatorTypes.hidden\n        elif isinstance(creator, Creator):\n            creator_type = CreatorTypes.artist\n        else:\n            creator_type = CreatorTypes.base\n\n        description = None\n        detail_description = None\n        default_variant = None\n        default_variants = None\n        pre_create_attr_defs = None\n        create_allow_context_change = None\n        create_allow_thumbnail = None\n        show_order = creator.order\n        if creator_type is CreatorTypes.artist:\n            description = creator.get_description()\n            detail_description = creator.get_detail_description()\n            default_variant = creator.get_default_variant()\n            default_variants = creator.get_default_variants()\n            pre_create_attr_defs = creator.get_pre_create_attr_defs()\n            create_allow_context_change = creator.create_allow_context_change\n            create_allow_thumbnail = creator.create_allow_thumbnail\n            show_order = creator.show_order\n\n        identifier = creator.identifier\n        return cls(\n            identifier,\n            creator_type,\n            creator.family,\n            creator.label or identifier,\n            creator.get_group_label(),\n            creator.get_icon(),\n            description,\n            detail_description,\n            default_variant,\n            default_variants,\n            create_allow_context_change,\n            create_allow_thumbnail,\n            show_order,\n            pre_create_attr_defs,\n        )\n\n    def to_data(self):\n        pre_create_attributes_defs = None\n        if self.pre_create_attributes_defs is not None:\n            pre_create_attributes_defs = serialize_attr_defs(\n                self.pre_create_attributes_defs\n            )\n\n        return {\n            \"identifier\": self.identifier,\n            \"creator_type\": str(self.creator_type),\n            \"family\": self.family,\n            \"label\": self.label,\n            \"group_label\": self.group_label,\n            \"icon\": self.icon,\n            \"description\": self.description,\n            \"detailed_description\": self.detailed_description,\n            \"default_variant\": self.default_variant,\n            \"default_variants\": self.default_variants,\n            \"create_allow_context_change\": self.create_allow_context_change,\n            \"create_allow_thumbnail\": self.create_allow_thumbnail,\n            \"show_order\": self.show_order,\n            \"pre_create_attributes_defs\": pre_create_attributes_defs,\n        }\n\n    @classmethod\n    def from_data(cls, data):\n        pre_create_attributes_defs = data[\"pre_create_attributes_defs\"]\n        if pre_create_attributes_defs is not None:\n            data[\"pre_create_attributes_defs\"] = deserialize_attr_defs(\n                pre_create_attributes_defs\n            )\n\n        data[\"creator_type\"] = CreatorTypes.from_str(data[\"creator_type\"])\n        return cls(**data)\n\n\n@six.add_metaclass(ABCMeta)\nclass AbstractPublisherController(object):\n    \"\"\"Publisher tool controller.\n\n    Define what must be implemented to be able use Publisher functionality.\n\n    Goal is to have \"data driven\" controller that can be used to control UI\n    running in different process. That lead to some disadvantages like UI can't\n    access objects directly but by using wrappers that can be serialized.\n    \"\"\"\n\n    @property\n    @abstractmethod\n    def log(self):\n        \"\"\"Controller's logger object.\n\n        Returns:\n            logging.Logger: Logger object that can be used for logging.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def event_system(self):\n        \"\"\"Inner event system for publisher controller.\"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def project_name(self):\n        \"\"\"Current context project name.\n\n        Returns:\n            str: Name of project.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def current_asset_name(self):\n        \"\"\"Current context asset name.\n\n        Returns:\n            Union[str, None]: Name of asset.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def current_task_name(self):\n        \"\"\"Current context task name.\n\n        Returns:\n            Union[str, None]: Name of task.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def host_context_has_changed(self):\n        \"\"\"Host context changed after last reset.\n\n        'CreateContext' has this option available using 'context_has_changed'.\n\n        Returns:\n            bool: Context has changed.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def host_is_valid(self):\n        \"\"\"Host is valid for creation part.\n\n        Host must have implemented certain functionality to be able create\n        in Publisher tool.\n\n        Returns:\n            bool: Host can handle creation of instances.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def instances(self):\n        \"\"\"Collected/created instances.\n\n        Returns:\n            List[CreatedInstance]: List of created instances.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_context_title(self):\n        \"\"\"Get context title for artist shown at the top of main window.\n\n        Returns:\n            Union[str, None]: Context title for window or None. In case of None\n                a warning is displayed (not nice for artists).\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_asset_docs(self):\n        pass\n\n    @abstractmethod\n    def get_asset_hierarchy(self):\n        pass\n\n    @abstractmethod\n    def get_task_names_by_asset_names(self, asset_names):\n        pass\n\n    @abstractmethod\n    def get_existing_subset_names(self, asset_name):\n        pass\n\n    @abstractmethod\n    def reset(self):\n        \"\"\"Reset whole controller.\n\n        This should reset create context, publish context and all variables\n        that are related to it.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_creator_attribute_definitions(self, instances):\n        pass\n\n    @abstractmethod\n    def get_publish_attribute_definitions(self, instances, include_context):\n        pass\n\n    @abstractmethod\n    def get_creator_icon(self, identifier):\n        \"\"\"Receive creator's icon by identifier.\n\n        Args:\n            identifier (str): Creator's identifier.\n\n        Returns:\n            Union[str, None]: Creator's icon string.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_subset_name(\n        self,\n        creator_identifier,\n        variant,\n        task_name,\n        asset_name,\n        instance_id=None\n    ):\n        \"\"\"Get subset name based on passed data.\n\n        Args:\n            creator_identifier (str): Identifier of creator which should be\n                responsible for subset name creation.\n            variant (str): Variant value from user's input.\n            task_name (str): Name of task for which is instance created.\n            asset_name (str): Name of asset for which is instance created.\n            instance_id (Union[str, None]): Existing instance id when subset\n                name is updated.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def create(\n        self, creator_identifier, subset_name, instance_data, options\n    ):\n        \"\"\"Trigger creation by creator identifier.\n\n        Should also trigger refresh of instanes.\n\n        Args:\n            creator_identifier (str): Identifier of Creator plugin.\n            subset_name (str): Calculated subset name.\n            instance_data (Dict[str, Any]): Base instance data with variant,\n                asset name and task name.\n            options (Dict[str, Any]): Data from pre-create attributes.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def save_changes(self):\n        \"\"\"Save changes in create context.\n\n        Save can crash because of unexpected errors.\n\n        Returns:\n            bool: Save was successful.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def remove_instances(self, instance_ids):\n        \"\"\"Remove list of instances from create context.\"\"\"\n        # TODO expect instance ids\n\n        pass\n\n    @property\n    @abstractmethod\n    def publish_has_started(self):\n        \"\"\"Has publishing finished.\n\n        Returns:\n            bool: If publishing finished and all plugins were iterated.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def publish_has_finished(self):\n        \"\"\"Has publishing finished.\n\n        Returns:\n            bool: If publishing finished and all plugins were iterated.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def publish_is_running(self):\n        \"\"\"Publishing is running right now.\n\n        Returns:\n            bool: If publishing is in progress.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def publish_has_validated(self):\n        \"\"\"Publish validation passed.\n\n        Returns:\n            bool: If publishing passed last possible validation order.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def publish_has_crashed(self):\n        \"\"\"Publishing crashed for any reason.\n\n        Returns:\n            bool: Publishing crashed.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def publish_has_validation_errors(self):\n        \"\"\"During validation happened at least one validation error.\n\n        Returns:\n            bool: Validation error was raised during validation.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def publish_max_progress(self):\n        \"\"\"Get maximum possible progress number.\n\n        Returns:\n            int: Number that can be used as 100% of publish progress bar.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def publish_progress(self):\n        \"\"\"Current progress number.\n\n        Returns:\n            int: Current progress value from 0 to 'publish_max_progress'.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def publish_error_msg(self):\n        \"\"\"Current error message which cause fail of publishing.\n\n        Returns:\n            Union[str, None]: Message which will be showed to artist or\n                None.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_publish_report(self):\n        pass\n\n    @abstractmethod\n    def get_validation_errors(self):\n        pass\n\n    @abstractmethod\n    def publish(self):\n        \"\"\"Trigger publishing without any order limitations.\"\"\"\n\n        pass\n\n    @abstractmethod\n    def validate(self):\n        \"\"\"Trigger publishing which will stop after validation order.\"\"\"\n\n        pass\n\n    @abstractmethod\n    def stop_publish(self):\n        \"\"\"Stop publishing can be also used to pause publishing.\n\n        Pause of publishing is possible only if all plugins successfully\n        finished.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def run_action(self, plugin_id, action_id):\n        \"\"\"Trigger pyblish action on a plugin.\n\n        Args:\n            plugin_id (str): Id of publish plugin.\n            action_id (str): Id of publish action.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def convertor_items(self):\n        pass\n\n    @abstractmethod\n    def trigger_convertor_items(self, convertor_identifiers):\n        pass\n\n    @abstractmethod\n    def get_thumbnail_paths_for_instances(self, instance_ids):\n        pass\n\n    @abstractmethod\n    def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping):\n        pass\n\n    @abstractmethod\n    def set_comment(self, comment):\n        \"\"\"Set comment on pyblish context.\n\n        Set \"comment\" key on current pyblish.api.Context data.\n\n        Args:\n            comment (str): Artist's comment.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def emit_card_message(\n        self, message, message_type=CardMessageTypes.standard\n    ):\n        \"\"\"Emit a card message which can have a lifetime.\n\n        This is for UI purposes. Method can be extended to more arguments\n        in future e.g. different message timeout or type (color).\n\n        Args:\n            message (str): Message that will be showed.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_thumbnail_temp_dir_path(self):\n        \"\"\"Return path to directory where thumbnails can be temporary stored.\n\n        Returns:\n            str: Path to a directory.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def clear_thumbnail_temp_dir_path(self):\n        \"\"\"Remove content of thumbnail temp directory.\"\"\"\n\n        pass\n\n\nclass BasePublisherController(AbstractPublisherController):\n    \"\"\"Implement common logic for controllers.\n\n    Implement event system, logger and common attributes. Attributes are\n    triggering value changes so anyone can listen to their topics.\n\n    Prepare implementation for creator items. Controller must implement just\n    their filling by '_collect_creator_items'.\n\n    All prepared implementation is based on calling super '__init__'.\n    \"\"\"\n\n    def __init__(self):\n        self._log = None\n        self._event_system = None\n\n        # Host is valid for creation\n        self._host_is_valid = False\n\n        # Any other exception that happened during publishing\n        self._publish_error_msg = None\n        # Publishing is in progress\n        self._publish_is_running = False\n        # Publishing is over validation order\n        self._publish_has_validated = False\n\n        self._publish_has_validation_errors = False\n        self._publish_has_crashed = False\n        # All publish plugins are processed\n        self._publish_has_started = False\n        self._publish_has_finished = False\n        self._publish_max_progress = 0\n        self._publish_progress = 0\n\n        # Controller must '_collect_creator_items' to fill the value\n        self._creator_items = None\n\n    @property\n    def log(self):\n        \"\"\"Controller's logger object.\n\n        Returns:\n            logging.Logger: Logger object that can be used for logging.\n        \"\"\"\n\n        if self._log is None:\n            self._log = logging.getLogger(self.__class__.__name__)\n        return self._log\n\n    @property\n    def event_system(self):\n        \"\"\"Inner event system for publisher controller.\n\n        Is used for communication with UI. Event system is autocreated.\n\n        Known topics:\n            \"show.detailed.help\" - Detailed help requested (UI related).\n            \"show.card.message\" - Show card message request (UI related).\n            \"instances.refresh.finished\" - Instances are refreshed.\n            \"plugins.refresh.finished\" - Plugins refreshed.\n            \"publish.reset.finished\" - Reset finished.\n            \"controller.reset.started\" - Controller reset started.\n            \"controller.reset.finished\" - Controller reset finished.\n            \"publish.process.started\" - Publishing started. Can be started from\n                paused state.\n            \"publish.process.stopped\" - Publishing stopped/paused process.\n            \"publish.process.plugin.changed\" - Plugin state has changed.\n            \"publish.process.instance.changed\" - Instance state has changed.\n            \"publish.has_validated.changed\" - Attr 'publish_has_validated'\n                changed.\n            \"publish.is_running.changed\" - Attr 'publish_is_running' changed.\n            \"publish.has_crashed.changed\" - Attr 'publish_has_crashed' changed.\n            \"publish.publish_error.changed\" - Attr 'publish_error'\n            \"publish.has_validation_errors.changed\" - Attr\n                'has_validation_errors' changed.\n            \"publish.max_progress.changed\" - Attr 'publish_max_progress'\n                changed.\n            \"publish.progress.changed\" - Attr 'publish_progress' changed.\n            \"publish.host_is_valid.changed\" - Attr 'host_is_valid' changed.\n            \"publish.finished.changed\" - Attr 'publish_has_finished' changed.\n\n        Returns:\n            EventSystem: Event system which can trigger callbacks for topics.\n        \"\"\"\n\n        if self._event_system is None:\n            self._event_system = EventSystem()\n        return self._event_system\n\n    def _emit_event(self, topic, data=None):\n        if data is None:\n            data = {}\n        self.event_system.emit(topic, data, \"controller\")\n\n    def _get_host_is_valid(self):\n        return self._host_is_valid\n\n    def _set_host_is_valid(self, value):\n        if self._host_is_valid != value:\n            self._host_is_valid = value\n            self._emit_event(\n                \"publish.host_is_valid.changed\", {\"value\": value}\n            )\n\n    def _get_publish_has_started(self):\n        return self._publish_has_started\n\n    def _set_publish_has_started(self, value):\n        if value != self._publish_has_started:\n            self._publish_has_started = value\n\n    def _get_publish_has_finished(self):\n        return self._publish_has_finished\n\n    def _set_publish_has_finished(self, value):\n        if self._publish_has_finished != value:\n            self._publish_has_finished = value\n            self._emit_event(\"publish.finished.changed\", {\"value\": value})\n\n    def _get_publish_is_running(self):\n        return self._publish_is_running\n\n    def _set_publish_is_running(self, value):\n        if self._publish_is_running != value:\n            self._publish_is_running = value\n            self._emit_event(\"publish.is_running.changed\", {\"value\": value})\n\n    def _get_publish_has_validated(self):\n        return self._publish_has_validated\n\n    def _set_publish_has_validated(self, value):\n        if self._publish_has_validated != value:\n            self._publish_has_validated = value\n            self._emit_event(\n                \"publish.has_validated.changed\", {\"value\": value}\n            )\n\n    def _get_publish_has_crashed(self):\n        return self._publish_has_crashed\n\n    def _set_publish_has_crashed(self, value):\n        if self._publish_has_crashed != value:\n            self._publish_has_crashed = value\n            self._emit_event(\"publish.has_crashed.changed\", {\"value\": value})\n\n    def _get_publish_has_validation_errors(self):\n        return self._publish_has_validation_errors\n\n    def _set_publish_has_validation_errors(self, value):\n        if self._publish_has_validation_errors != value:\n            self._publish_has_validation_errors = value\n            self._emit_event(\n                \"publish.has_validation_errors.changed\",\n                {\"value\": value}\n            )\n\n    def _get_publish_max_progress(self):\n        return self._publish_max_progress\n\n    def _set_publish_max_progress(self, value):\n        if self._publish_max_progress != value:\n            self._publish_max_progress = value\n            self._emit_event(\"publish.max_progress.changed\", {\"value\": value})\n\n    def _get_publish_progress(self):\n        return self._publish_progress\n\n    def _set_publish_progress(self, value):\n        if self._publish_progress != value:\n            self._publish_progress = value\n            self._emit_event(\"publish.progress.changed\", {\"value\": value})\n\n    def _get_publish_error_msg(self):\n        return self._publish_error_msg\n\n    def _set_publish_error_msg(self, value):\n        if self._publish_error_msg != value:\n            self._publish_error_msg = value\n            self._emit_event(\"publish.publish_error.changed\", {\"value\": value})\n\n    host_is_valid = property(\n        _get_host_is_valid, _set_host_is_valid\n    )\n    publish_has_started = property(\n        _get_publish_has_started, _set_publish_has_started\n    )\n    publish_has_finished = property(\n        _get_publish_has_finished, _set_publish_has_finished\n    )\n    publish_is_running = property(\n        _get_publish_is_running, _set_publish_is_running\n    )\n    publish_has_validated = property(\n        _get_publish_has_validated, _set_publish_has_validated\n    )\n    publish_has_crashed = property(\n        _get_publish_has_crashed, _set_publish_has_crashed\n    )\n    publish_has_validation_errors = property(\n        _get_publish_has_validation_errors, _set_publish_has_validation_errors\n    )\n    publish_max_progress = property(\n        _get_publish_max_progress, _set_publish_max_progress\n    )\n    publish_progress = property(\n        _get_publish_progress, _set_publish_progress\n    )\n    publish_error_msg = property(\n        _get_publish_error_msg, _set_publish_error_msg\n    )\n\n    def _reset_attributes(self):\n        \"\"\"Reset most of attributes that can be reset.\"\"\"\n\n        self.publish_is_running = False\n        self.publish_has_started = False\n        self.publish_has_validated = False\n        self.publish_has_crashed = False\n        self.publish_has_validation_errors = False\n        self.publish_has_finished = False\n\n        self.publish_error_msg = None\n        self.publish_progress = 0\n\n    @property\n    def creator_items(self):\n        \"\"\"Creators that can be shown in create dialog.\"\"\"\n        if self._creator_items is None:\n            self._creator_items = self._collect_creator_items()\n        return self._creator_items\n\n    @abstractmethod\n    def _collect_creator_items(self):\n        \"\"\"Receive CreatorItems to work with.\n\n        Returns:\n            Dict[str, CreatorItem]: Creator items by their identifier.\n        \"\"\"\n\n        pass\n\n    def get_creator_icon(self, identifier):\n        \"\"\"Function to receive icon for creator identifier.\n\n        Args:\n            str: Creator's identifier for which should be icon returned.\n        \"\"\"\n\n        creator_item = self.creator_items.get(identifier)\n        if creator_item is not None:\n            return creator_item.icon\n        return None\n\n    def get_thumbnail_temp_dir_path(self):\n        \"\"\"Return path to directory where thumbnails can be temporary stored.\n\n        Returns:\n            str: Path to a directory.\n        \"\"\"\n\n        return os.path.join(\n            tempfile.gettempdir(),\n            \"publisher_thumbnails\",\n            get_process_id()\n        )\n\n    def clear_thumbnail_temp_dir_path(self):\n        \"\"\"Remove content of thumbnail temp directory.\"\"\"\n\n        dirpath = self.get_thumbnail_temp_dir_path()\n        if os.path.exists(dirpath):\n            shutil.rmtree(dirpath)\n\n\nclass PublisherController(BasePublisherController):\n    \"\"\"Middleware between UI, CreateContext and publish Context.\n\n    Handle both creation and publishing parts.\n\n    Args:\n        headless (bool): Headless publishing. ATM not implemented or used.\n    \"\"\"\n\n    _log = None\n\n    def __init__(self, headless=False):\n        super(PublisherController, self).__init__()\n\n        self._host = registered_host()\n        self._headless = headless\n\n        self._create_context = CreateContext(\n            self._host, headless=headless, reset=False\n        )\n\n        self._publish_plugins_proxy = None\n\n        # pyblish.api.Context\n        self._publish_context = None\n        # Pyblish report\n        self._publish_report = PublishReportMaker(self)\n        # Store exceptions of validation error\n        self._publish_validation_errors = PublishValidationErrors()\n\n        # Publishing should stop at validation stage\n        self._publish_up_validation = False\n        # This information is not much important for controller but for widget\n        #   which can change (and set) the comment.\n        self._publish_comment_is_set = False\n\n        # Validation order\n        # - plugin with order same or higher than this value is extractor or\n        #   higher\n        self._validation_order = (\n            pyblish.api.ValidatorOrder + PLUGIN_ORDER_OFFSET\n        )\n\n        # Plugin iterator\n        self._main_thread_iter = None\n\n        # State flags to prevent executing method which is already in progress\n        self._resetting_plugins = False\n        self._resetting_instances = False\n\n        # Cacher of avalon documents\n        self._asset_docs_cache = AssetDocsCache(self)\n\n    @property\n    def project_name(self):\n        \"\"\"Current project context defined by host.\n\n        Returns:\n            str: Project name.\n        \"\"\"\n\n        return self._create_context.get_current_project_name()\n\n    @property\n    def current_asset_name(self):\n        \"\"\"Current context asset name defined by host.\n\n        Returns:\n            Union[str, None]: Asset name or None if asset is not set.\n        \"\"\"\n\n        return self._create_context.get_current_asset_name()\n\n    @property\n    def current_task_name(self):\n        \"\"\"Current context task name defined by host.\n\n        Returns:\n            Union[str, None]: Task name or None if task is not set.\n        \"\"\"\n\n        return self._create_context.get_current_task_name()\n\n    @property\n    def host_context_has_changed(self):\n        return self._create_context.context_has_changed\n\n    @property\n    def instances(self):\n        \"\"\"Current instances in create context.\"\"\"\n        return self._create_context.instances_by_id\n\n    @property\n    def convertor_items(self):\n        return self._create_context.convertor_items_by_id\n\n    @property\n    def _creators(self):\n        \"\"\"All creators loaded in create context.\"\"\"\n\n        return self._create_context.creators\n\n    @property\n    def _publish_plugins(self):\n        \"\"\"Publish plugins.\"\"\"\n        return self._create_context.publish_plugins\n\n    # --- Publish specific callbacks ---\n    def get_asset_docs(self):\n        \"\"\"Get asset documents from cache for whole project.\"\"\"\n        return self._asset_docs_cache.get_asset_docs()\n\n    def get_context_title(self):\n        \"\"\"Get context title for artist shown at the top of main window.\"\"\"\n\n        context_title = None\n        if hasattr(self._host, \"get_context_title\"):\n            context_title = self._host.get_context_title()\n\n        if context_title is None:\n            context_title = os.environ.get(\"AVALON_APP_NAME\")\n            if context_title is None:\n                context_title = os.environ.get(\"AVALON_APP\")\n\n        return context_title\n\n    def get_asset_hierarchy(self):\n        \"\"\"Prepare asset documents into hierarchy.\"\"\"\n\n        return self._asset_docs_cache.get_asset_hierarchy()\n\n    def get_task_names_by_asset_names(self, asset_names):\n        \"\"\"Prepare task names by asset name.\"\"\"\n        task_names_by_asset_name = (\n            self._asset_docs_cache.get_task_names_by_asset_name()\n        )\n        result = {}\n        for asset_name in asset_names:\n            result[asset_name] = set(\n                task_names_by_asset_name.get(asset_name) or []\n            )\n        return result\n\n    def get_existing_subset_names(self, asset_name):\n        project_name = self.project_name\n        asset_doc = self._asset_docs_cache.get_asset_by_name(asset_name)\n        if not asset_doc:\n            return None\n\n        asset_id = asset_doc[\"_id\"]\n        subset_docs = get_subsets(\n            project_name, asset_ids=[asset_id], fields=[\"name\"]\n        )\n        return {\n            subset_doc[\"name\"]\n            for subset_doc in subset_docs\n        }\n\n    def reset(self):\n        \"\"\"Reset everything related to creation and publishing.\"\"\"\n        self.stop_publish()\n\n        self._emit_event(\"controller.reset.started\")\n\n        self.host_is_valid = self._create_context.host_is_valid\n\n        self._create_context.reset_preparation()\n\n        # Reset avalon context\n        self._create_context.reset_current_context()\n\n        self._asset_docs_cache.reset()\n\n        self._reset_plugins()\n        # Publish part must be reset after plugins\n        self._reset_publish()\n        self._reset_instances()\n\n        self._create_context.reset_finalization()\n\n        self._emit_event(\"controller.reset.finished\")\n\n        self.emit_card_message(\"Refreshed..\")\n\n    def _reset_plugins(self):\n        \"\"\"Reset to initial state.\"\"\"\n        if self._resetting_plugins:\n            return\n\n        self._resetting_plugins = True\n\n        self._create_context.reset_plugins()\n        # Reset creator items\n        self._creator_items = None\n\n        self._resetting_plugins = False\n\n        self._emit_event(\"plugins.refresh.finished\")\n\n    def _collect_creator_items(self):\n        # TODO add crashed initialization of create plugins to report\n        output = {}\n        for identifier, creator in self._create_context.creators.items():\n            try:\n                output[identifier] = CreatorItem.from_creator(creator)\n            except Exception:\n                self.log.error(\n                    \"Failed to create creator item for '%s'\",\n                    identifier,\n                    exc_info=True\n                )\n\n        return output\n\n    def _reset_instances(self):\n        \"\"\"Reset create instances.\"\"\"\n        if self._resetting_instances:\n            return\n\n        self._resetting_instances = True\n\n        self._create_context.reset_context_data()\n        with self._create_context.bulk_instances_collection():\n            try:\n                self._create_context.reset_instances()\n            except CreatorsOperationFailed as exc:\n                self._emit_event(\n                    \"instances.collection.failed\",\n                    {\n                        \"title\": \"Instance collection failed\",\n                        \"failed_info\": exc.failed_info\n                    }\n                )\n\n            try:\n                self._create_context.find_convertor_items()\n            except ConvertorsOperationFailed as exc:\n                self._emit_event(\n                    \"convertors.find.failed\",\n                    {\n                        \"title\": \"Collection of unsupported subset failed\",\n                        \"failed_info\": exc.failed_info\n                    }\n                )\n\n            try:\n                self._create_context.execute_autocreators()\n\n            except CreatorsOperationFailed as exc:\n                self._emit_event(\n                    \"instances.create.failed\",\n                    {\n                        \"title\": \"AutoCreation failed\",\n                        \"failed_info\": exc.failed_info\n                    }\n                )\n\n        self._resetting_instances = False\n\n        self._on_create_instance_change()\n\n    def get_thumbnail_paths_for_instances(self, instance_ids):\n        thumbnail_paths_by_instance_id = (\n            self._create_context.thumbnail_paths_by_instance_id\n        )\n        return {\n            instance_id: thumbnail_paths_by_instance_id.get(instance_id)\n            for instance_id in instance_ids\n        }\n\n    def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping):\n        thumbnail_paths_by_instance_id = (\n            self._create_context.thumbnail_paths_by_instance_id\n        )\n        for instance_id, thumbnail_path in thumbnail_path_mapping.items():\n            thumbnail_paths_by_instance_id[instance_id] = thumbnail_path\n\n        self._emit_event(\n            \"instance.thumbnail.changed\",\n            {\n                \"mapping\": thumbnail_path_mapping\n            }\n        )\n\n    def emit_card_message(\n        self, message, message_type=CardMessageTypes.standard\n    ):\n        self._emit_event(\n            \"show.card.message\",\n            {\n                \"message\": message,\n                \"message_type\": message_type\n            }\n        )\n\n    def get_creator_attribute_definitions(self, instances):\n        \"\"\"Collect creator attribute definitions for multuple instances.\n\n        Args:\n            instances(List[CreatedInstance]): List of created instances for\n                which should be attribute definitions returned.\n        \"\"\"\n\n        # NOTE it would be great if attrdefs would have hash method implemented\n        #   so they could be used as keys in dictionary\n        output = []\n        _attr_defs = {}\n        for instance in instances:\n            for attr_def in instance.creator_attribute_defs:\n                found_idx = None\n                for idx, _attr_def in _attr_defs.items():\n                    if attr_def == _attr_def:\n                        found_idx = idx\n                        break\n\n                value = None\n                if attr_def.is_value_def:\n                    value = instance.creator_attributes[attr_def.key]\n                if found_idx is None:\n                    idx = len(output)\n                    output.append((attr_def, [instance], [value]))\n                    _attr_defs[idx] = attr_def\n                else:\n                    item = output[found_idx]\n                    item[1].append(instance)\n                    item[2].append(value)\n        return output\n\n    def get_publish_attribute_definitions(self, instances, include_context):\n        \"\"\"Collect publish attribute definitions for passed instances.\n\n        Args:\n            instances(list<CreatedInstance>): List of created instances for\n                which should be attribute definitions returned.\n            include_context(bool): Add context specific attribute definitions.\n        \"\"\"\n\n        _tmp_items = []\n        if include_context:\n            _tmp_items.append(self._create_context)\n\n        for instance in instances:\n            _tmp_items.append(instance)\n\n        all_defs_by_plugin_name = {}\n        all_plugin_values = {}\n        for item in _tmp_items:\n            for plugin_name, attr_val in item.publish_attributes.items():\n                attr_defs = attr_val.attr_defs\n                if not attr_defs:\n                    continue\n\n                if plugin_name not in all_defs_by_plugin_name:\n                    all_defs_by_plugin_name[plugin_name] = attr_val.attr_defs\n\n                if plugin_name not in all_plugin_values:\n                    all_plugin_values[plugin_name] = {}\n\n                plugin_values = all_plugin_values[plugin_name]\n\n                for attr_def in attr_defs:\n                    if isinstance(attr_def, UIDef):\n                        continue\n                    if attr_def.key not in plugin_values:\n                        plugin_values[attr_def.key] = []\n                    attr_values = plugin_values[attr_def.key]\n\n                    value = attr_val[attr_def.key]\n                    attr_values.append((item, value))\n\n        output = []\n        for plugin in self._create_context.plugins_with_defs:\n            plugin_name = plugin.__name__\n            if plugin_name not in all_defs_by_plugin_name:\n                continue\n            output.append((\n                plugin_name,\n                all_defs_by_plugin_name[plugin_name],\n                all_plugin_values\n            ))\n        return output\n\n    def get_subset_name(\n        self,\n        creator_identifier,\n        variant,\n        task_name,\n        asset_name,\n        instance_id=None\n    ):\n        \"\"\"Get subset name based on passed data.\n\n        Args:\n            creator_identifier (str): Identifier of creator which should be\n                responsible for subset name creation.\n            variant (str): Variant value from user's input.\n            task_name (str): Name of task for which is instance created.\n            asset_name (str): Name of asset for which is instance created.\n            instance_id (Union[str, None]): Existing instance id when subset\n                name is updated.\n        \"\"\"\n\n        creator = self._creators[creator_identifier]\n        project_name = self.project_name\n        asset_doc = self._asset_docs_cache.get_full_asset_by_name(asset_name)\n        instance = None\n        if instance_id:\n            instance = self.instances[instance_id]\n\n        return creator.get_subset_name(\n            variant, task_name, asset_doc, project_name, instance=instance\n        )\n\n    def trigger_convertor_items(self, convertor_identifiers):\n        \"\"\"Trigger legacy item convertors.\n\n        This functionality requires to save and reset CreateContext. The reset\n        is needed so Creators can collect converted items.\n\n        Args:\n            convertor_identifiers (list[str]): Identifiers of convertor\n                plugins.\n        \"\"\"\n\n        success = True\n        try:\n            self._create_context.run_convertors(convertor_identifiers)\n\n        except ConvertorsOperationFailed as exc:\n            success = False\n            self._emit_event(\n                \"convertors.convert.failed\",\n                {\n                    \"title\": \"Conversion failed\",\n                    \"failed_info\": exc.failed_info\n                }\n            )\n\n        if success:\n            self.emit_card_message(\"Conversion finished\")\n        else:\n            self.emit_card_message(\"Conversion failed\", CardMessageTypes.error)\n\n        self.reset()\n\n    def create(\n        self, creator_identifier, subset_name, instance_data, options\n    ):\n        \"\"\"Trigger creation and refresh of instances in UI.\"\"\"\n\n        success = True\n        try:\n            self._create_context.create_with_unified_error(\n                creator_identifier, subset_name, instance_data, options\n            )\n\n        except CreatorsOperationFailed as exc:\n            success = False\n            self._emit_event(\n                \"instances.create.failed\",\n                {\n                    \"title\": \"Creation failed\",\n                    \"failed_info\": exc.failed_info\n                }\n            )\n\n        self._on_create_instance_change()\n        return success\n\n    def save_changes(self, show_message=True):\n        \"\"\"Save changes happened during creation.\n\n        Trigger save of changes using host api. This functionality does not\n        validate anything. It is required to do checks before this method is\n        called to be able to give user actionable response e.g. check of\n        context using 'host_context_has_changed'.\n\n        Args:\n            show_message (bool): Show message that changes were\n                saved successfully.\n\n        Returns:\n            bool: Save of changes was successful.\n        \"\"\"\n\n        if not self._create_context.host_is_valid:\n            # TODO remove\n            # Fake success save when host is not valid for CreateContext\n            #   this is for testing as experimental feature\n            return True\n\n        try:\n            self._create_context.save_changes()\n            if show_message:\n                self.emit_card_message(\"Saved changes..\")\n            return True\n\n        except CreatorsOperationFailed as exc:\n            self._emit_event(\n                \"instances.save.failed\",\n                {\n                    \"title\": \"Instances save failed\",\n                    \"failed_info\": exc.failed_info\n                }\n            )\n\n        return False\n\n    def remove_instances(self, instance_ids):\n        \"\"\"Remove instances based on instance ids.\n\n        Args:\n            instance_ids (List[str]): List of instance ids to remove.\n        \"\"\"\n\n        # QUESTION Expect that instances are really removed? In that case reset\n        #    is not required.\n        self._remove_instances_from_context(instance_ids)\n\n        self._on_create_instance_change()\n\n    def _remove_instances_from_context(self, instance_ids):\n        instances_by_id = self._create_context.instances_by_id\n        instances = [\n            instances_by_id[instance_id]\n            for instance_id in instance_ids\n        ]\n        try:\n            self._create_context.remove_instances(instances)\n        except CreatorsOperationFailed as exc:\n            self._emit_event(\n                \"instances.remove.failed\",\n                {\n                    \"title\": \"Instance removement failed\",\n                    \"failed_info\": exc.failed_info\n                }\n            )\n\n    def _on_create_instance_change(self):\n        self._emit_event(\"instances.refresh.finished\")\n\n    def get_publish_report(self):\n        return self._publish_report.get_report(self._publish_plugins)\n\n    def get_validation_errors(self):\n        return self._publish_validation_errors.create_report()\n\n    def _reset_publish(self):\n        self._reset_attributes()\n\n        self._publish_up_validation = False\n        self._publish_comment_is_set = False\n\n        self._main_thread_iter = self._publish_iterator()\n        self._publish_context = pyblish.api.Context()\n        # Make sure \"comment\" is set on publish context\n        self._publish_context.data[\"comment\"] = \"\"\n        # Add access to create context during publishing\n        # - must not be used for changing CreatedInstances during publishing!\n        # QUESTION\n        # - pop the key after first collector using it would be safest option?\n        self._publish_context.data[\"create_context\"] = self._create_context\n\n        self._publish_plugins_proxy = PublishPluginsProxy(\n            self._publish_plugins\n        )\n\n        self._publish_report.reset(self._publish_context, self._create_context)\n        self._publish_validation_errors.reset(self._publish_plugins_proxy)\n\n        self.publish_max_progress = len(self._publish_plugins)\n\n        self._emit_event(\"publish.reset.finished\")\n\n    def set_comment(self, comment):\n        \"\"\"Set comment from ui to pyblish context.\n\n        This should be called always before publishing is started but should\n        happen only once on first publish start thus variable\n        '_publish_comment_is_set' is used to keep track about the information.\n        \"\"\"\n\n        if not self._publish_comment_is_set:\n            self._publish_context.data[\"comment\"] = comment\n            self._publish_comment_is_set = True\n\n    def publish(self):\n        \"\"\"Run publishing.\n\n        Make sure all changes are saved before method is called (Call\n        'save_changes' and check output).\n        \"\"\"\n\n        self._publish_up_validation = False\n        self._start_publish()\n\n    def validate(self):\n        \"\"\"Run publishing and stop after Validation.\n\n        Make sure all changes are saved before method is called (Call\n        'save_changes' and check output).\n        \"\"\"\n\n        if self.publish_has_validated:\n            return\n        self._publish_up_validation = True\n        self._start_publish()\n\n    def _start_publish(self):\n        \"\"\"Start or continue in publishing.\"\"\"\n        if self.publish_is_running:\n            return\n\n        self.publish_is_running = True\n        self.publish_has_started = True\n\n        self._emit_event(\"publish.process.started\")\n\n        self._publish_next_process()\n\n    def _stop_publish(self):\n        \"\"\"Stop or pause publishing.\"\"\"\n        self.publish_is_running = False\n\n        self._emit_event(\"publish.process.stopped\")\n\n    def stop_publish(self):\n        \"\"\"Stop publishing process (any reason).\"\"\"\n\n        if self.publish_is_running:\n            self._stop_publish()\n\n    def run_action(self, plugin_id, action_id):\n        # TODO handle result in UI\n        plugin = self._publish_plugins_proxy.get_plugin(plugin_id)\n        action = self._publish_plugins_proxy.get_action(plugin_id, action_id)\n\n        result = pyblish.plugin.process(\n            plugin, self._publish_context, None, action.id\n        )\n        exception = result.get(\"error\")\n        if exception:\n            self._emit_event(\n                \"publish.action.failed\",\n                {\n                    \"title\": \"Action failed\",\n                    \"message\": \"Action failed.\",\n                    \"traceback\": \"\".join(\n                        traceback.format_exception(\n                            type(exception),\n                            exception,\n                            exception.__traceback__\n                        )\n                    ),\n                    \"label\": action.__name__,\n                    \"identifier\": action.id\n                }\n            )\n\n        self._publish_report.add_action_result(action, result)\n\n        self.emit_card_message(\"Action finished.\")\n\n    def _publish_next_process(self):\n        # Validations of progress before using iterator\n        # - same conditions may be inside iterator but they may be used\n        #   only in specific cases (e.g. when it happens for a first time)\n\n        # There are validation errors and validation is passed\n        # - can't do any progree\n        if (\n            self.publish_has_validated\n            and self.publish_has_validation_errors\n        ):\n            item = MainThreadItem(self.stop_publish)\n\n        # Any unexpected error happened\n        # - everything should stop\n        elif self.publish_has_crashed:\n            item = MainThreadItem(self.stop_publish)\n\n        # Everything is ok so try to get new processing item\n        else:\n            item = next(self._main_thread_iter)\n\n        self._process_main_thread_item(item)\n\n    def _process_main_thread_item(self, item):\n        item()\n\n    def _is_publish_plugin_active(self, plugin):\n        \"\"\"Decide if publish plugin is active.\n\n        This is hack because 'active' is mis-used in mixin\n        'OptionalPyblishPluginMixin' where 'active' is used for default value\n        of optional plugins. Because of that is 'active' state of plugin\n        which inherit from 'OptionalPyblishPluginMixin' ignored. That affects\n        headless publishing inside host, potentially remote publishing.\n\n        We have to change that to match pyblish base, but we can do that\n        only when all hosts use Publisher because the change requires\n        change of settings schemas.\n\n        Args:\n            plugin (pyblish.Plugin): Plugin which should be checked if is\n                active.\n\n        Returns:\n            bool: Is plugin active.\n        \"\"\"\n\n        if plugin.active:\n            return True\n\n        if not plugin.optional:\n            return False\n\n        if OptionalPyblishPluginMixin in inspect.getmro(plugin):\n            return True\n        return False\n\n    def _publish_iterator(self):\n        \"\"\"Main logic center of publishing.\n\n        Iterator returns `MainThreadItem` objects with callbacks that should be\n        processed in main thread (threaded in future?). Cares about changing\n        states of currently processed publish plugin and instance. Also\n        change state of processed orders like validation order has passed etc.\n\n        Also stops publishing, if should stop on validation.\n        \"\"\"\n\n        for idx, plugin in enumerate(self._publish_plugins):\n            self._publish_progress = idx\n\n            # Check if plugin is over validation order\n            if not self.publish_has_validated:\n                self.publish_has_validated = (\n                    plugin.order >= self._validation_order\n                )\n\n            # Stop if plugin is over validation order and process\n            #   should process up to validation.\n            if self._publish_up_validation and self.publish_has_validated:\n                yield MainThreadItem(self.stop_publish)\n\n            # Stop if validation is over and validation errors happened\n            if (\n                self.publish_has_validated\n                and self.publish_has_validation_errors\n            ):\n                yield MainThreadItem(self.stop_publish)\n\n            # Add plugin to publish report\n            self._publish_report.add_plugin_iter(\n                plugin, self._publish_context)\n\n            # WARNING This is hack fix for optional plugins\n            if not self._is_publish_plugin_active(plugin):\n                self._publish_report.set_plugin_skipped()\n                continue\n\n            # Trigger callback that new plugin is going to be processed\n            plugin_label = plugin.__name__\n            if hasattr(plugin, \"label\") and plugin.label:\n                plugin_label = plugin.label\n            self._emit_event(\n                \"publish.process.plugin.changed\",\n                {\"plugin_label\": plugin_label}\n            )\n\n            # Plugin is instance plugin\n            if plugin.__instanceEnabled__:\n                instances = pyblish.logic.instances_by_plugin(\n                    self._publish_context, plugin\n                )\n                if not instances:\n                    self._publish_report.set_plugin_skipped()\n                    continue\n\n                for instance in instances:\n                    if instance.data.get(\"publish\") is False:\n                        continue\n\n                    instance_label = (\n                        instance.data.get(\"label\")\n                        or instance.data[\"name\"]\n                    )\n                    self._emit_event(\n                        \"publish.process.instance.changed\",\n                        {\"instance_label\": instance_label}\n                    )\n\n                    yield MainThreadItem(\n                        self._process_and_continue, plugin, instance\n                    )\n            else:\n                families = collect_families_from_instances(\n                    self._publish_context, only_active=True\n                )\n                plugins = pyblish.logic.plugins_by_families(\n                    [plugin], families\n                )\n                if plugins:\n                    instance_label = (\n                        self._publish_context.data.get(\"label\")\n                        or self._publish_context.data.get(\"name\")\n                        or \"Context\"\n                    )\n                    self._emit_event(\n                        \"publish.process.instance.changed\",\n                        {\"instance_label\": instance_label}\n                    )\n                    yield MainThreadItem(\n                        self._process_and_continue, plugin, None\n                    )\n                else:\n                    self._publish_report.set_plugin_skipped()\n\n        # Cleanup of publishing process\n        self.publish_has_finished = True\n        self.publish_progress = self.publish_max_progress\n        yield MainThreadItem(self.stop_publish)\n\n    def _add_validation_error(self, result):\n        self.publish_has_validation_errors = True\n        self._publish_validation_errors.add_error(\n            result[\"plugin\"],\n            result[\"error\"],\n            result[\"instance\"]\n        )\n\n    def _process_and_continue(self, plugin, instance):\n        result = pyblish.plugin.process(\n            plugin, self._publish_context, instance\n        )\n\n        exception = result.get(\"error\")\n        if exception:\n            has_validation_error = False\n            if (\n                isinstance(exception, PublishValidationError)\n                and not self.publish_has_validated\n            ):\n                has_validation_error = True\n                self._add_validation_error(result)\n\n            else:\n                if isinstance(exception, KnownPublishError):\n                    msg = str(exception)\n                else:\n                    msg = (\n                        \"Something went wrong. Send report\"\n                        \" to your supervisor or Ynput team.\"\n                    )\n                self.publish_error_msg = msg\n                self.publish_has_crashed = True\n\n            result[\"is_validation_error\"] = has_validation_error\n\n        self._publish_report.add_result(result)\n\n        self._publish_next_process()\n\n\ndef collect_families_from_instances(instances, only_active=False):\n    \"\"\"Collect all families for passed publish instances.\n\n    Args:\n        instances(list<pyblish.api.Instance>): List of publish instances from\n            which are families collected.\n        only_active(bool): Return families only for active instances.\n\n    Returns:\n        list[str]: Families available on instances.\n    \"\"\"\n\n    all_families = set()\n    for instance in instances:\n        if only_active:\n            if instance.data.get(\"publish\") is False:\n                continue\n        family = instance.data.get(\"family\")\n        if family:\n            all_families.add(family)\n\n        families = instance.data.get(\"families\") or tuple()\n        for family in families:\n            all_families.add(family)\n\n    return list(all_families)\n"
  },
  {
    "path": "openpype/tools/publisher/control_qt.py",
    "content": "import collections\nfrom abc import abstractmethod, abstractproperty\n\nfrom qtpy import QtCore\n\nfrom openpype.lib.events import Event\nfrom openpype.pipeline.create import CreatedInstance\n\nfrom .control import (\n    MainThreadItem,\n    PublisherController,\n    BasePublisherController,\n)\n\n\nclass MainThreadProcess(QtCore.QObject):\n    \"\"\"Qt based main thread process executor.\n\n    Has timer which controls each 50ms if there is new item to process.\n\n    This approach gives ability to update UI meanwhile plugin is in progress.\n    \"\"\"\n\n    count_timeout = 2\n\n    def __init__(self):\n        super(MainThreadProcess, self).__init__()\n        self._items_to_process = collections.deque()\n\n        timer = QtCore.QTimer()\n        timer.setInterval(0)\n\n        timer.timeout.connect(self._execute)\n\n        self._timer = timer\n        self._switch_counter = self.count_timeout\n\n    def process(self, func, *args, **kwargs):\n        item = MainThreadItem(func, *args, **kwargs)\n        self.add_item(item)\n\n    def add_item(self, item):\n        self._items_to_process.append(item)\n\n    def _execute(self):\n        if not self._items_to_process:\n            return\n\n        if self._switch_counter > 0:\n            self._switch_counter -= 1\n            return\n\n        self._switch_counter = self.count_timeout\n\n        item = self._items_to_process.popleft()\n        item.process()\n\n    def start(self):\n        if not self._timer.isActive():\n            self._timer.start()\n\n    def stop(self):\n        if self._timer.isActive():\n            self._timer.stop()\n\n    def clear(self):\n        if self._timer.isActive():\n            self._timer.stop()\n        self._items_to_process = collections.deque()\n\n\nclass QtPublisherController(PublisherController):\n    def __init__(self, *args, **kwargs):\n        self._main_thread_processor = MainThreadProcess()\n\n        super(QtPublisherController, self).__init__(*args, **kwargs)\n\n        self.event_system.add_callback(\n            \"publish.process.started\", self._qt_on_publish_start\n        )\n        self.event_system.add_callback(\n            \"publish.process.stopped\", self._qt_on_publish_stop\n        )\n\n    def _reset_publish(self):\n        super(QtPublisherController, self)._reset_publish()\n        self._main_thread_processor.clear()\n\n    def _process_main_thread_item(self, item):\n        self._main_thread_processor.add_item(item)\n\n    def _qt_on_publish_start(self):\n        self._main_thread_processor.start()\n\n    def _qt_on_publish_stop(self):\n        self._main_thread_processor.stop()\n\n\nclass QtRemotePublishController(BasePublisherController):\n    \"\"\"Abstract Remote controller for Qt UI.\n\n    This controller should be used in process where UI is running and should\n    listen and ask for data on a client side.\n\n    All objects that are used during UI processing should be able to convert\n    on client side to json serializable data and then recreated here. Keep in\n    mind that all changes made here should be send back to client controller\n    before critical actions.\n\n    ATM Was not tested and will require some changes. All code written here is\n    based on theoretical idea how it could work.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        self._created_instances = {}\n        self._thumbnail_paths_by_instance_id = None\n\n    def _reset_attributes(self):\n        super()._reset_attributes()\n        self._thumbnail_paths_by_instance_id = None\n\n    @abstractmethod\n    def _get_serialized_instances(self):\n        \"\"\"Receive serialized instances from client process.\n\n        Returns:\n            List[Dict[str, Any]]: Serialized instances.\n        \"\"\"\n\n        pass\n\n    def _on_create_instance_change(self):\n        serialized_instances = self._get_serialized_instances()\n\n        created_instances = {}\n        for serialized_data in serialized_instances:\n            item = CreatedInstance.deserialize_on_remote(serialized_data)\n            created_instances[item.id] = item\n\n        self._created_instances = created_instances\n        self._emit_event(\"instances.refresh.finished\")\n\n    def remote_events_handler(self, event_data):\n        event = Event.from_data(event_data)\n\n        # Topics that cause \"replication\" of controller changes\n        if event.topic == \"publish.max_progress.changed\":\n            self.publish_max_progress = event[\"value\"]\n            return\n\n        if event.topic == \"publish.progress.changed\":\n            self.publish_progress = event[\"value\"]\n            return\n\n        if event.topic == \"publish.has_validated.changed\":\n            self.publish_has_validated = event[\"value\"]\n            return\n\n        if event.topic == \"publish.is_running.changed\":\n            self.publish_is_running = event[\"value\"]\n            return\n\n        if event.topic == \"publish.publish_error.changed\":\n            self.publish_error_msg = event[\"value\"]\n            return\n\n        if event.topic == \"publish.has_crashed.changed\":\n            self.publish_has_crashed = event[\"value\"]\n            return\n\n        if event.topic == \"publish.has_validation_errors.changed\":\n            self.publish_has_validation_errors = event[\"value\"]\n            return\n\n        if event.topic == \"publish.finished.changed\":\n            self.publish_has_finished = event[\"value\"]\n            return\n\n        if event.topic == \"publish.host_is_valid.changed\":\n            self.host_is_valid = event[\"value\"]\n            return\n\n        # Don't skip because UI want know about it too\n        if event.topic == \"instance.thumbnail.changed\":\n            for instance_id, path in event[\"mapping\"].items():\n                self.thumbnail_paths_by_instance_id[instance_id] = path\n\n        # Topics that can be just passed by because are not affecting\n        #   controller itself\n        # - \"show.card.message\"\n        # - \"show.detailed.help\"\n        # - \"publish.reset.finished\"\n        # - \"instances.refresh.finished\"\n        # - \"plugins.refresh.finished\"\n        # - \"controller.reset.finished\"\n        # - \"publish.process.started\"\n        # - \"publish.process.stopped\"\n        # - \"publish.process.plugin.changed\"\n        # - \"publish.process.instance.changed\"\n        self.event_system.emit_event(event)\n\n    @abstractproperty\n    def project_name(self):\n        \"\"\"Current context project name from client.\n\n        Returns:\n            str: Name of project.\n        \"\"\"\n\n        pass\n\n    @abstractproperty\n    def current_asset_name(self):\n        \"\"\"Current context asset name from client.\n\n        Returns:\n            Union[str, None]: Name of asset.\n        \"\"\"\n\n        pass\n\n    @abstractproperty\n    def current_task_name(self):\n        \"\"\"Current context task name from client.\n\n        Returns:\n            Union[str, None]: Name of task.\n        \"\"\"\n\n        pass\n\n    @property\n    def instances(self):\n        \"\"\"Collected/created instances.\n\n        Returns:\n            List[CreatedInstance]: List of created instances.\n        \"\"\"\n\n        return self._created_instances\n\n    def get_context_title(self):\n        \"\"\"Get context title for artist shown at the top of main window.\n\n        Returns:\n            Union[str, None]: Context title for window or None. In case of None\n                a warning is displayed (not nice for artists).\n        \"\"\"\n\n        pass\n\n    def get_asset_docs(self):\n        pass\n\n    def get_asset_hierarchy(self):\n        pass\n\n    def get_task_names_by_asset_names(self, asset_names):\n        pass\n\n    def get_existing_subset_names(self, asset_name):\n        pass\n\n    @property\n    def thumbnail_paths_by_instance_id(self):\n        if self._thumbnail_paths_by_instance_id is None:\n            self._thumbnail_paths_by_instance_id = (\n                self._collect_thumbnail_paths_by_instance_id()\n            )\n        return self._thumbnail_paths_by_instance_id\n\n    def get_thumbnail_path_for_instance(self, instance_id):\n        return self.thumbnail_paths_by_instance_id.get(instance_id)\n\n    def set_thumbnail_path_for_instance(self, instance_id, thumbnail_path):\n        self._set_thumbnail_path_on_context(self, instance_id, thumbnail_path)\n\n    @abstractmethod\n    def _collect_thumbnail_paths_by_instance_id(self):\n        \"\"\"Collect thumbnail paths by instance id in remote controller.\n\n        These should be collected from 'CreatedContext' there.\n\n        Returns:\n            Dict[str, str]: Mapping of thumbnail path by instance id.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def _set_thumbnail_path_on_context(self, instance_id, thumbnail_path):\n        \"\"\"Send change of thumbnail path in remote controller.\n\n        That should trigger event 'instance.thumbnail.changed' which is\n        captured and handled in default implementation in this class.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def get_subset_name(\n        self,\n        creator_identifier,\n        variant,\n        task_name,\n        asset_name,\n        instance_id=None\n    ):\n        \"\"\"Get subset name based on passed data.\n\n        Args:\n            creator_identifier (str): Identifier of creator which should be\n                responsible for subset name creation.\n            variant (str): Variant value from user's input.\n            task_name (str): Name of task for which is instance created.\n            asset_name (str): Name of asset for which is instance created.\n            instance_id (Union[str, None]): Existing instance id when subset\n                name is updated.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def create(\n        self, creator_identifier, subset_name, instance_data, options\n    ):\n        \"\"\"Trigger creation by creator identifier.\n\n        Should also trigger refresh of instanes.\n\n        Args:\n            creator_identifier (str): Identifier of Creator plugin.\n            subset_name (str): Calculated subset name.\n            instance_data (Dict[str, Any]): Base instance data with variant,\n                asset name and task name.\n            options (Dict[str, Any]): Data from pre-create attributes.\n        \"\"\"\n\n        pass\n\n    def _get_instance_changes_for_client(self):\n        \"\"\"Preimplemented method to receive instance changes for client.\"\"\"\n\n        created_instance_changes = {}\n        for instance_id, instance in self._created_instances.items():\n            created_instance_changes[instance_id] = (\n                instance.remote_changes()\n            )\n        return created_instance_changes\n\n    @abstractmethod\n    def _send_instance_changes_to_client(self):\n        instance_changes = self._get_instance_changes_for_client()\n        # Implement to send 'instance_changes' value to client\n\n    @abstractmethod\n    def save_changes(self):\n        \"\"\"Save changes happened during creation.\"\"\"\n\n        self._send_instance_changes_to_client()\n\n    @abstractmethod\n    def remove_instances(self, instance_ids):\n        \"\"\"Remove list of instances from create context.\"\"\"\n        # TODO add Args:\n\n        pass\n\n    @abstractmethod\n    def get_publish_report(self):\n        pass\n\n    @abstractmethod\n    def get_validation_errors(self):\n        pass\n\n    @abstractmethod\n    def reset(self):\n        \"\"\"Reset whole controller.\n\n        This should reset create context, publish context and all variables\n        that are related to it.\n        \"\"\"\n\n        self._send_instance_changes_to_client()\n        pass\n\n    @abstractmethod\n    def publish(self):\n        \"\"\"Trigger publishing without any order limitations.\"\"\"\n\n        self._send_instance_changes_to_client()\n        pass\n\n    @abstractmethod\n    def validate(self):\n        \"\"\"Trigger publishing which will stop after validation order.\"\"\"\n\n        self._send_instance_changes_to_client()\n        pass\n\n    @abstractmethod\n    def stop_publish(self):\n        \"\"\"Stop publishing can be also used to pause publishing.\n\n        Pause of publishing is possible only if all plugins successfully\n        finished.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def run_action(self, plugin_id, action_id):\n        \"\"\"Trigger pyblish action on a plugin.\n\n        Args:\n            plugin_id (str): Id of publish plugin.\n            action_id (str): Id of publish action.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def set_comment(self, comment):\n        \"\"\"Set comment on pyblish context.\n\n        Set \"comment\" key on current pyblish.api.Context data.\n\n        Args:\n            comment (str): Artist's comment.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def emit_card_message(self, message):\n        \"\"\"Emit a card message which can have a lifetime.\n\n        This is for UI purposes. Method can be extended to more arguments\n        in future e.g. different message timeout or type (color).\n\n        Args:\n            message (str): Message that will be showed.\n        \"\"\"\n\n        pass\n"
  },
  {
    "path": "openpype/tools/publisher/publish_report_viewer/__init__.py",
    "content": "from qtpy import QtWidgets\n\nfrom .report_items import (\n    PublishReport\n)\nfrom .widgets import (\n    PublishReportViewerWidget\n)\n\nfrom .window import (\n    PublishReportViewerWindow\n)\n\n\n__all__ = (\n    \"PublishReport\",\n\n    \"PublishReportViewerWidget\",\n\n    \"PublishReportViewerWindow\",\n\n    \"main\",\n)\n\n\ndef main():\n    app = QtWidgets.QApplication([])\n    window = PublishReportViewerWindow()\n    window.show()\n    return app.exec_()\n"
  },
  {
    "path": "openpype/tools/publisher/publish_report_viewer/constants.py",
    "content": "from qtpy import QtCore\n\n\nITEM_ID_ROLE = QtCore.Qt.UserRole + 1\nITEM_IS_GROUP_ROLE = QtCore.Qt.UserRole + 2\nITEM_LABEL_ROLE = QtCore.Qt.UserRole + 3\nITEM_ERRORED_ROLE = QtCore.Qt.UserRole + 4\nPLUGIN_SKIPPED_ROLE = QtCore.Qt.UserRole + 5\nPLUGIN_PASSED_ROLE = QtCore.Qt.UserRole + 6\nINSTANCE_REMOVED_ROLE = QtCore.Qt.UserRole + 7\n\n\n__all__ = (\n    \"ITEM_ID_ROLE\",\n    \"ITEM_IS_GROUP_ROLE\",\n    \"ITEM_LABEL_ROLE\",\n    \"ITEM_ERRORED_ROLE\",\n    \"PLUGIN_SKIPPED_ROLE\",\n    \"INSTANCE_REMOVED_ROLE\"\n)\n"
  },
  {
    "path": "openpype/tools/publisher/publish_report_viewer/delegates.py",
    "content": "import collections\nfrom qtpy import QtWidgets, QtCore, QtGui\nfrom .constants import (\n    ITEM_IS_GROUP_ROLE,\n    ITEM_ERRORED_ROLE,\n    PLUGIN_SKIPPED_ROLE,\n    PLUGIN_PASSED_ROLE,\n    INSTANCE_REMOVED_ROLE\n)\n\ncolors = {\n    \"error\": QtGui.QColor(\"#ff4a4a\"),\n    \"warning\": QtGui.QColor(\"#ff9900\"),\n    \"ok\": QtGui.QColor(\"#77AE24\"),\n    \"active\": QtGui.QColor(\"#99CEEE\"),\n    \"idle\": QtCore.Qt.white,\n    \"inactive\": QtGui.QColor(\"#888\"),\n    \"hover\": QtGui.QColor(255, 255, 255, 5),\n    \"selected\": QtGui.QColor(255, 255, 255, 10),\n    \"outline\": QtGui.QColor(\"#333\"),\n    \"group\": QtGui.QColor(\"#21252B\"),\n    \"group-hover\": QtGui.QColor(\"#3c3c3c\"),\n    \"group-selected-hover\": QtGui.QColor(\"#555555\")\n}\n\n\nclass GroupItemDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Generic delegate for instance header\"\"\"\n\n    _item_icons_by_name_and_size = collections.defaultdict(dict)\n\n    _minus_pixmaps = {}\n    _plus_pixmaps = {}\n    _path_stroker = None\n\n    _item_pix_offset_ratio = 1.0 / 5.0\n    _item_border_size = 1.0 / 7.0\n    _group_pix_offset_ratio = 1.0 / 3.0\n    _group_pix_stroke_size_ratio = 1.0 / 7.0\n\n    @classmethod\n    def _get_path_stroker(cls):\n        if cls._path_stroker is None:\n            path_stroker = QtGui.QPainterPathStroker()\n            path_stroker.setCapStyle(QtCore.Qt.RoundCap)\n            path_stroker.setJoinStyle(QtCore.Qt.RoundJoin)\n\n            cls._path_stroker = path_stroker\n        return cls._path_stroker\n\n    @classmethod\n    def _get_plus_pixmap(cls, size):\n        pix = cls._minus_pixmaps.get(size)\n        if pix is not None:\n            return pix\n\n        pix = QtGui.QPixmap(size, size)\n        pix.fill(QtCore.Qt.transparent)\n\n        offset = int(size * cls._group_pix_offset_ratio)\n        pnt_1 = QtCore.QPoint(offset, int(size / 2))\n        pnt_2 = QtCore.QPoint(size - offset, int(size / 2))\n        pnt_3 = QtCore.QPoint(int(size / 2), offset)\n        pnt_4 = QtCore.QPoint(int(size / 2), size - offset)\n        path_1 = QtGui.QPainterPath(pnt_1)\n        path_1.lineTo(pnt_2)\n        path_2 = QtGui.QPainterPath(pnt_3)\n        path_2.lineTo(pnt_4)\n\n        path_stroker = cls._get_path_stroker()\n        path_stroker.setWidth(size * cls._group_pix_stroke_size_ratio)\n        stroked_path_1 = path_stroker.createStroke(path_1)\n        stroked_path_2 = path_stroker.createStroke(path_2)\n\n        pix = QtGui.QPixmap(size, size)\n        pix.fill(QtCore.Qt.transparent)\n\n        painter = QtGui.QPainter(pix)\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n        painter.setPen(QtCore.Qt.transparent)\n        painter.setBrush(QtCore.Qt.white)\n        painter.drawPath(stroked_path_1)\n        painter.drawPath(stroked_path_2)\n        painter.end()\n\n        cls._minus_pixmaps[size] = pix\n\n        return pix\n\n    @classmethod\n    def _get_minus_pixmap(cls, size):\n        pix = cls._plus_pixmaps.get(size)\n        if pix is not None:\n            return pix\n\n        offset = int(size * cls._group_pix_offset_ratio)\n        pnt_1 = QtCore.QPoint(offset, int(size / 2))\n        pnt_2 = QtCore.QPoint(size - offset, int(size / 2))\n        path = QtGui.QPainterPath(pnt_1)\n        path.lineTo(pnt_2)\n        path_stroker = cls._get_path_stroker()\n        path_stroker.setWidth(size * cls._group_pix_stroke_size_ratio)\n        stroked_path = path_stroker.createStroke(path)\n\n        pix = QtGui.QPixmap(size, size)\n        pix.fill(QtCore.Qt.transparent)\n\n        painter = QtGui.QPainter(pix)\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n        painter.setPen(QtCore.Qt.transparent)\n        painter.setBrush(QtCore.Qt.white)\n        painter.drawPath(stroked_path)\n        painter.end()\n\n        cls._plus_pixmaps[size] = pix\n\n        return pix\n\n    @classmethod\n    def _get_icon_color(cls, name):\n        if name == \"error\":\n            return QtGui.QColor(colors[\"error\"])\n        return QtGui.QColor(QtCore.Qt.white)\n\n    @classmethod\n    def _get_icon(cls, name, size):\n        icons_by_size = cls._item_icons_by_name_and_size[name]\n        if icons_by_size and size in icons_by_size:\n            return icons_by_size[size]\n\n        offset = int(size * cls._item_pix_offset_ratio)\n        offset_size = size - (2 * offset)\n        pix = QtGui.QPixmap(size, size)\n        pix.fill(QtCore.Qt.transparent)\n\n        painter = QtGui.QPainter(pix)\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n\n        draw_ellipse = True\n        if name == \"error\":\n            color = QtGui.QColor(colors[\"error\"])\n            painter.setPen(QtCore.Qt.NoPen)\n            painter.setBrush(color)\n\n        elif name == \"skipped\":\n            color = QtGui.QColor(QtCore.Qt.white)\n            pen = QtGui.QPen(color)\n            pen.setWidth(int(size * cls._item_border_size))\n            painter.setPen(pen)\n            painter.setBrush(QtCore.Qt.transparent)\n\n        elif name == \"passed\":\n            color = QtGui.QColor(colors[\"ok\"])\n            painter.setPen(QtCore.Qt.NoPen)\n            painter.setBrush(color)\n\n        elif name == \"removed\":\n            draw_ellipse = False\n\n            offset = offset * 1.5\n            p1 = QtCore.QPoint(offset, offset)\n            p2 = QtCore.QPoint(size - offset, size - offset)\n            p3 = QtCore.QPoint(offset, size - offset)\n            p4 = QtCore.QPoint(size - offset, offset)\n\n            pen = QtGui.QPen(QtCore.Qt.white)\n            pen.setWidth(offset_size / 4)\n            pen.setCapStyle(QtCore.Qt.RoundCap)\n            painter.setPen(pen)\n            painter.setBrush(QtCore.Qt.transparent)\n            painter.drawLine(p1, p2)\n            painter.drawLine(p3, p4)\n\n        else:\n            color = QtGui.QColor(QtCore.Qt.white)\n            painter.setPen(QtCore.Qt.NoPen)\n            painter.setBrush(color)\n\n        if draw_ellipse:\n            painter.drawEllipse(offset, offset, offset_size, offset_size)\n\n        painter.end()\n\n        cls._item_icons_by_name_and_size[name][size] = pix\n\n        return pix\n\n    def paint(self, painter, option, index):\n        if index.data(ITEM_IS_GROUP_ROLE):\n            self.group_item_paint(painter, option, index)\n        else:\n            self.item_paint(painter, option, index)\n\n    def item_paint(self, painter, option, index):\n        self.initStyleOption(option, index)\n\n        widget = option.widget\n        if widget:\n            style = widget.style()\n        else:\n            style = QtWidgets.QApplicaion.style()\n\n        style.proxy().drawPrimitive(\n            QtWidgets.QStyle.PE_PanelItemViewItem, option, painter, widget\n        )\n        _rect = style.proxy().subElementRect(\n            QtWidgets.QStyle.SE_ItemViewItemText, option, widget\n        )\n        bg_rect = QtCore.QRectF(option.rect)\n        bg_rect.setY(_rect.y())\n        bg_rect.setHeight(_rect.height())\n\n        expander_rect = QtCore.QRectF(bg_rect)\n        expander_rect.setWidth(expander_rect.height() + 5)\n\n        label_rect = QtCore.QRectF(\n            expander_rect.x() + expander_rect.width(),\n            expander_rect.y(),\n            bg_rect.width() - expander_rect.width(),\n            expander_rect.height()\n        )\n\n        icon_size = expander_rect.height()\n        if index.data(ITEM_ERRORED_ROLE):\n            expander_icon = self._get_icon(\"error\", icon_size)\n        elif index.data(PLUGIN_SKIPPED_ROLE):\n            expander_icon = self._get_icon(\"skipped\", icon_size)\n        elif index.data(PLUGIN_PASSED_ROLE):\n            expander_icon = self._get_icon(\"passed\", icon_size)\n        elif index.data(INSTANCE_REMOVED_ROLE):\n            expander_icon = self._get_icon(\"removed\", icon_size)\n        else:\n            expander_icon = self._get_icon(\"\", icon_size)\n\n        label = index.data(QtCore.Qt.DisplayRole)\n        label = option.fontMetrics.elidedText(\n            label, QtCore.Qt.ElideRight, label_rect.width()\n        )\n\n        painter.save()\n        # Draw icon\n        pix_point = QtCore.QPoint(\n            expander_rect.center().x() - int(expander_icon.width() / 2),\n            expander_rect.top()\n        )\n        painter.drawPixmap(pix_point, expander_icon)\n\n        # Draw label\n        painter.setFont(option.font)\n        painter.drawText(label_rect, QtCore.Qt.AlignVCenter, label)\n\n        # Ok, we're done, tidy up.\n        painter.restore()\n\n    def group_item_paint(self, painter, option, index):\n        \"\"\"Paint text\n         _\n        My label\n        \"\"\"\n        self.initStyleOption(option, index)\n\n        widget = option.widget\n        if widget:\n            style = widget.style()\n        else:\n            style = QtWidgets.QApplicaion.style()\n        _rect = style.proxy().subElementRect(\n            QtWidgets.QStyle.SE_ItemViewItemText, option, widget\n        )\n\n        bg_rect = QtCore.QRectF(option.rect)\n        bg_rect.setY(_rect.y())\n        bg_rect.setHeight(_rect.height())\n\n        expander_height = bg_rect.height()\n        expander_width = expander_height + 5\n        expander_y_offset = expander_height % 2\n        expander_height -= expander_y_offset\n        expander_rect = QtCore.QRectF(\n            bg_rect.x(),\n            bg_rect.y() + expander_y_offset,\n            expander_width,\n            expander_height\n        )\n\n        label_rect = QtCore.QRectF(\n            bg_rect.x() + expander_width,\n            bg_rect.y(),\n            bg_rect.width() - expander_width,\n            bg_rect.height()\n        )\n\n        bg_path = QtGui.QPainterPath()\n        radius = (bg_rect.height() / 2) - 0.01\n        bg_path.addRoundedRect(bg_rect, radius, radius)\n\n        painter.fillPath(bg_path, colors[\"group\"])\n\n        selected = option.state & QtWidgets.QStyle.State_Selected\n        hovered = option.state & QtWidgets.QStyle.State_MouseOver\n\n        if selected and hovered:\n            painter.fillPath(bg_path, colors[\"selected\"])\n        elif hovered:\n            painter.fillPath(bg_path, colors[\"hover\"])\n\n        expanded = self.parent().isExpanded(index)\n        if expanded:\n            expander_icon = self._get_minus_pixmap(expander_height)\n        else:\n            expander_icon = self._get_plus_pixmap(expander_height)\n\n        label = index.data(QtCore.Qt.DisplayRole)\n        label = option.fontMetrics.elidedText(\n            label, QtCore.Qt.ElideRight, label_rect.width()\n        )\n\n        # Maintain reference to state, so we can restore it once we're done\n        painter.save()\n        pix_point = QtCore.QPoint(\n            expander_rect.center().x() - int(expander_icon.width() / 2),\n            expander_rect.top()\n        )\n        painter.drawPixmap(pix_point, expander_icon)\n\n        # Draw label\n        painter.setFont(option.font)\n        painter.drawText(label_rect, QtCore.Qt.AlignVCenter, label)\n\n        # Ok, we're done, tidy up.\n        painter.restore()\n"
  },
  {
    "path": "openpype/tools/publisher/publish_report_viewer/model.py",
    "content": "import uuid\nfrom qtpy import QtCore, QtGui\n\nimport pyblish.api\n\nfrom openpype.tools.utils.lib import html_escape\nfrom .constants import (\n    ITEM_ID_ROLE,\n    ITEM_IS_GROUP_ROLE,\n    ITEM_LABEL_ROLE,\n    ITEM_ERRORED_ROLE,\n    PLUGIN_SKIPPED_ROLE,\n    PLUGIN_PASSED_ROLE,\n    INSTANCE_REMOVED_ROLE\n)\n\n\nclass InstancesModel(QtGui.QStandardItemModel):\n    def __init__(self, *args, **kwargs):\n        super(InstancesModel, self).__init__(*args, **kwargs)\n\n        self._items_by_id = {}\n        self._plugin_items_by_id = {}\n\n    def get_items_by_id(self):\n        return self._items_by_id\n\n    def set_report(self, report_item):\n        root_item = self.invisibleRootItem()\n        if root_item.rowCount() > 0:\n            root_item.removeRows(0, root_item.rowCount())\n        self._items_by_id.clear()\n        self._plugin_items_by_id.clear()\n        if not report_item:\n            return\n\n        families = set(report_item.instance_items_by_family.keys())\n        families.remove(None)\n        all_families = list(sorted(families))\n        all_families.insert(0, None)\n\n        family_items = []\n        for family in all_families:\n            items = []\n            instance_items = report_item.instance_items_by_family[family]\n            all_removed = True\n            for instance_item in instance_items:\n                src_instance_label = instance_item.label\n                if src_instance_label is None:\n                    # Do not cause UI crash if label is 'None'\n                    src_instance_label = \"No label\"\n                instance_label = html_escape(src_instance_label)\n\n                item = QtGui.QStandardItem(src_instance_label)\n                item.setData(instance_label, ITEM_LABEL_ROLE)\n                item.setData(instance_item.errored, ITEM_ERRORED_ROLE)\n                item.setData(instance_item.id, ITEM_ID_ROLE)\n                item.setData(instance_item.removed, INSTANCE_REMOVED_ROLE)\n                if all_removed and not instance_item.removed:\n                    all_removed = False\n                item.setData(False, ITEM_IS_GROUP_ROLE)\n                items.append(item)\n                self._items_by_id[instance_item.id] = item\n                self._plugin_items_by_id[instance_item.id] = item\n\n            if family is None:\n                family_items.extend(items)\n                continue\n\n            family_item = QtGui.QStandardItem(family)\n            family_item.setData(family, ITEM_LABEL_ROLE)\n            family_item.setFlags(QtCore.Qt.ItemIsEnabled)\n            family_id = uuid.uuid4()\n            family_item.setData(family_id, ITEM_ID_ROLE)\n            family_item.setData(all_removed, INSTANCE_REMOVED_ROLE)\n            family_item.setData(True, ITEM_IS_GROUP_ROLE)\n            family_item.appendRows(items)\n            family_items.append(family_item)\n            self._items_by_id[family_id] = family_item\n\n        root_item.appendRows(family_items)\n\n\nclass InstanceProxyModel(QtCore.QSortFilterProxyModel):\n    def __init__(self, *args, **kwargs):\n        super(InstanceProxyModel, self).__init__(*args, **kwargs)\n\n        self._ignore_removed = True\n\n    @property\n    def ignore_removed(self):\n        return self._ignore_removed\n\n    def set_ignore_removed(self, value):\n        if value == self._ignore_removed:\n            return\n        self._ignore_removed = value\n\n        if self.sourceModel():\n            self.invalidateFilter()\n\n    def filterAcceptsRow(self, row, parent):\n        source_index = self.sourceModel().index(row, 0, parent)\n        if self._ignore_removed and source_index.data(INSTANCE_REMOVED_ROLE):\n            return False\n        return True\n\n\nclass PluginsModel(QtGui.QStandardItemModel):\n    order_label_mapping = (\n        (pyblish.api.CollectorOrder + 0.5, \"Collect\"),\n        (pyblish.api.ValidatorOrder + 0.5, \"Validate\"),\n        (pyblish.api.ExtractorOrder + 0.5, \"Extract\"),\n        (pyblish.api.IntegratorOrder + 0.5, \"Integrate\"),\n        (None, \"Other\")\n    )\n\n    def __init__(self, *args, **kwargs):\n        super(PluginsModel, self).__init__(*args, **kwargs)\n\n        self._items_by_id = {}\n        self._plugin_items_by_id = {}\n\n    def get_items_by_id(self):\n        return self._items_by_id\n\n    def set_report(self, report_item):\n        root_item = self.invisibleRootItem()\n        if root_item.rowCount() > 0:\n            root_item.removeRows(0, root_item.rowCount())\n        self._items_by_id.clear()\n        self._plugin_items_by_id.clear()\n        if not report_item:\n            return\n\n        labels_iter = iter(self.order_label_mapping)\n        cur_order, cur_label = next(labels_iter)\n        cur_plugin_items = []\n\n        plugin_items_by_group_labels = []\n        plugin_items_by_group_labels.append((cur_label, cur_plugin_items))\n        for plugin_id in report_item.plugins_id_order:\n            plugin_item = report_item.plugins_items_by_id[plugin_id]\n            if cur_order is not None and plugin_item.order >= cur_order:\n                cur_order, cur_label = next(labels_iter)\n                cur_plugin_items = []\n                plugin_items_by_group_labels.append(\n                    (cur_label, cur_plugin_items)\n                )\n\n            cur_plugin_items.append(plugin_item)\n\n        group_items = []\n        for group_label, plugin_items in plugin_items_by_group_labels:\n            group_id = uuid.uuid4()\n            group_item = QtGui.QStandardItem(group_label)\n            group_item.setData(group_label, ITEM_LABEL_ROLE)\n            group_item.setData(group_id, ITEM_ID_ROLE)\n            group_item.setData(True, ITEM_IS_GROUP_ROLE)\n            group_item.setFlags(QtCore.Qt.ItemIsEnabled)\n            group_items.append(group_item)\n\n            self._items_by_id[group_id] = group_item\n\n            if not plugin_items:\n                continue\n\n            items = []\n            for plugin_item in plugin_items:\n                label = plugin_item.label or plugin_item.name\n                item = QtGui.QStandardItem(label)\n                item.setData(False, ITEM_IS_GROUP_ROLE)\n                item.setData(plugin_item.label, ITEM_LABEL_ROLE)\n                item.setData(plugin_item.id, ITEM_ID_ROLE)\n                item.setData(plugin_item.skipped, PLUGIN_SKIPPED_ROLE)\n                item.setData(plugin_item.passed, PLUGIN_PASSED_ROLE)\n                item.setData(plugin_item.errored, ITEM_ERRORED_ROLE)\n                items.append(item)\n                self._items_by_id[plugin_item.id] = item\n                self._plugin_items_by_id[plugin_item.id] = item\n            group_item.appendRows(items)\n\n        root_item.appendRows(group_items)\n\n\nclass PluginProxyModel(QtCore.QSortFilterProxyModel):\n    def __init__(self, *args, **kwargs):\n        super(PluginProxyModel, self).__init__(*args, **kwargs)\n\n        self._ignore_skipped = True\n\n    @property\n    def ignore_skipped(self):\n        return self._ignore_skipped\n\n    def set_ignore_skipped(self, value):\n        if value == self._ignore_skipped:\n            return\n        self._ignore_skipped = value\n\n        if self.sourceModel():\n            self.invalidateFilter()\n\n    def filterAcceptsRow(self, row, parent):\n        model = self.sourceModel()\n        source_index = model.index(row, 0, parent)\n        if source_index.data(ITEM_IS_GROUP_ROLE):\n            return model.rowCount(source_index) > 0\n\n        if self._ignore_skipped and source_index.data(PLUGIN_SKIPPED_ROLE):\n            return False\n        return True\n"
  },
  {
    "path": "openpype/tools/publisher/publish_report_viewer/report_items.py",
    "content": "import uuid\nimport collections\nimport copy\n\n\nclass PluginItem:\n    def __init__(self, plugin_data):\n        self._id = uuid.uuid4()\n\n        self.name = plugin_data[\"name\"]\n        self.label = plugin_data[\"label\"]\n        self.order = plugin_data[\"order\"]\n        self.skipped = plugin_data[\"skipped\"]\n        self.passed = plugin_data[\"passed\"]\n\n        errored = False\n        for instance_data in plugin_data[\"instances_data\"]:\n            for log_item in instance_data[\"logs\"]:\n                errored = log_item[\"type\"] == \"error\"\n                if errored:\n                    break\n            if errored:\n                break\n\n        self.errored = errored\n\n    @property\n    def id(self):\n        return self._id\n\n\nclass InstanceItem:\n    def __init__(self, instance_id, instance_data, logs_by_instance_id):\n        self._id = instance_id\n        self.label = instance_data.get(\"label\") or instance_data.get(\"name\")\n        self.family = instance_data.get(\"family\")\n        self.removed = not instance_data.get(\"exists\", True)\n\n        logs = logs_by_instance_id.get(instance_id) or []\n        errored = False\n        for log_item in logs:\n            if log_item.errored:\n                errored = True\n                break\n\n        self.errored = errored\n\n    @property\n    def id(self):\n        return self._id\n\n\nclass LogItem:\n    def __init__(self, log_item_data, plugin_id, instance_id):\n        self._instance_id = instance_id\n        self._plugin_id = plugin_id\n        self._errored = log_item_data[\"type\"] == \"error\"\n        self.data = log_item_data\n\n    def __getitem__(self, key):\n        return self.data[key]\n\n    @property\n    def errored(self):\n        return self._errored\n\n    @property\n    def instance_id(self):\n        return self._instance_id\n\n    @property\n    def plugin_id(self):\n        return self._plugin_id\n\n\nclass PublishReport:\n    def __init__(self, report_data):\n        data = copy.deepcopy(report_data)\n\n        context_data = data[\"context\"]\n        context_data[\"name\"] = \"context\"\n        context_data[\"label\"] = context_data.get(\"label\") or \"Context\"\n\n        logs = []\n        plugins_items_by_id = {}\n        for plugin_data in data[\"plugins_data\"]:\n            item = PluginItem(plugin_data)\n            plugins_items_by_id[item.id] = item\n            for instance_data_item in plugin_data[\"instances_data\"]:\n                instance_id = instance_data_item[\"id\"]\n                for log_item_data in instance_data_item[\"logs\"]:\n                    log_item = LogItem(\n                        copy.deepcopy(log_item_data), item.id, instance_id\n                    )\n                    logs.append(log_item)\n        sorted_plugins = sorted(\n            plugins_items_by_id.values(),\n            key=lambda item: item.order\n        )\n        plugins_id_order = [\n            plugin_item.id\n            for plugin_item in sorted_plugins\n        ]\n\n        logs_by_instance_id = collections.defaultdict(list)\n        for log_item in logs:\n            logs_by_instance_id[log_item.instance_id].append(log_item)\n\n        instance_items_by_id = {}\n        instance_items_by_family = {}\n        context_item = InstanceItem(None, context_data, logs_by_instance_id)\n        instance_items_by_id[context_item.id] = context_item\n        instance_items_by_family[context_item.family] = [context_item]\n\n        for instance_id, instance_data in data[\"instances\"].items():\n            item = InstanceItem(\n                instance_id, instance_data, logs_by_instance_id\n            )\n            instance_items_by_id[item.id] = item\n            if item.family not in instance_items_by_family:\n                instance_items_by_family[item.family] = []\n            instance_items_by_family[item.family].append(item)\n\n        self.instance_items_by_id = instance_items_by_id\n        self.instance_items_by_family = instance_items_by_family\n\n        self.plugins_id_order = plugins_id_order\n        self.plugins_items_by_id = plugins_items_by_id\n\n        self.logs = logs\n\n        self.crashed_plugin_paths = report_data[\"crashed_file_paths\"]\n"
  },
  {
    "path": "openpype/tools/publisher/publish_report_viewer/widgets.py",
    "content": "from math import ceil\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.widgets.nice_checkbox import NiceCheckbox\n\n# from openpype.tools.utils import DeselectableTreeView\nfrom .constants import (\n    ITEM_ID_ROLE,\n    ITEM_IS_GROUP_ROLE\n)\nfrom .delegates import GroupItemDelegate\nfrom .model import (\n    InstancesModel,\n    InstanceProxyModel,\n    PluginsModel,\n    PluginProxyModel\n)\nfrom .report_items import PublishReport\n\nFILEPATH_ROLE = QtCore.Qt.UserRole + 1\nTRACEBACK_ROLE = QtCore.Qt.UserRole + 2\nIS_DETAIL_ITEM_ROLE = QtCore.Qt.UserRole + 3\n\n\nclass PluginLoadReportModel(QtGui.QStandardItemModel):\n    def set_report(self, report):\n        parent = self.invisibleRootItem()\n        parent.removeRows(0, parent.rowCount())\n\n        if report is None:\n            return\n\n        new_items = []\n        new_items_by_filepath = {}\n        for filepath in report.crashed_plugin_paths.keys():\n            item = QtGui.QStandardItem(filepath)\n            new_items.append(item)\n            new_items_by_filepath[filepath] = item\n\n        if not new_items:\n            return\n\n        parent.appendRows(new_items)\n        for filepath, item in new_items_by_filepath.items():\n            traceback_txt = report.crashed_plugin_paths[filepath]\n            detail_item = QtGui.QStandardItem()\n            detail_item.setData(filepath, FILEPATH_ROLE)\n            detail_item.setData(traceback_txt, TRACEBACK_ROLE)\n            detail_item.setData(True, IS_DETAIL_ITEM_ROLE)\n            item.appendRow(detail_item)\n\n\nclass DetailWidget(QtWidgets.QTextEdit):\n    def __init__(self, text, *args, **kwargs):\n        super(DetailWidget, self).__init__(*args, **kwargs)\n\n        self.setReadOnly(True)\n        self.setHtml(text)\n        self.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)\n        self.setWordWrapMode(\n            QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere\n        )\n\n    def sizeHint(self):\n        content_margins = (\n            self.contentsMargins().top()\n            + self.contentsMargins().bottom()\n        )\n        size = self.document().documentLayout().documentSize().toSize()\n        size.setHeight(size.height() + content_margins)\n        return size\n\n\nclass PluginLoadReportWidget(QtWidgets.QWidget):\n    def __init__(self, parent):\n        super(PluginLoadReportWidget, self).__init__(parent)\n\n        view = QtWidgets.QTreeView(self)\n        view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\n        view.setTextElideMode(QtCore.Qt.ElideLeft)\n        view.setHeaderHidden(True)\n        view.setAlternatingRowColors(True)\n        view.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)\n\n        model = PluginLoadReportModel()\n        view.setModel(model)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(view, 1)\n\n        view.expanded.connect(self._on_expand)\n\n        self._view = view\n        self._model = model\n        self._widgets_by_filepath = {}\n\n    def _on_expand(self, index):\n        for row in range(self._model.rowCount(index)):\n            child_index = self._model.index(row, index.column(), index)\n            self._create_widget(child_index)\n\n    def showEvent(self, event):\n        super(PluginLoadReportWidget, self).showEvent(event)\n        self._update_widgets_size_hints()\n\n    def resizeEvent(self, event):\n        super(PluginLoadReportWidget, self).resizeEvent(event)\n        self._update_widgets_size_hints()\n\n    def _update_widgets_size_hints(self):\n        for item in self._widgets_by_filepath.values():\n            widget, index = item\n            if not widget.isVisible():\n                continue\n            self._model.setData(\n                index, widget.sizeHint(), QtCore.Qt.SizeHintRole\n            )\n\n    def _create_widget(self, index):\n        if not index.data(IS_DETAIL_ITEM_ROLE):\n            return\n\n        filepath = index.data(FILEPATH_ROLE)\n        if filepath in self._widgets_by_filepath:\n            return\n\n        traceback_txt = index.data(TRACEBACK_ROLE)\n        detail_text = (\n            \"<b>Filepath:</b><br/>\"\n            \"{}<br/><br/>\"\n            \"<b>Traceback:</b><br/>\"\n            \"{}\"\n        ).format(filepath, traceback_txt.replace(\"\\n\", \"<br/>\"))\n        widget = DetailWidget(detail_text, self)\n        self._view.setIndexWidget(index, widget)\n        self._widgets_by_filepath[filepath] = (widget, index)\n\n    def set_report(self, report):\n        self._widgets_by_filepath = {}\n        self._model.set_report(report)\n\n\nclass ZoomPlainText(QtWidgets.QPlainTextEdit):\n    min_point_size = 1.0\n    max_point_size = 200.0\n\n    def __init__(self, *args, **kwargs):\n        super(ZoomPlainText, self).__init__(*args, **kwargs)\n\n        anim_timer = QtCore.QTimer()\n        anim_timer.setInterval(20)\n\n        anim_timer.timeout.connect(self._scaling_callback)\n\n        self._anim_timer = anim_timer\n        self._scheduled_scalings = 0\n        self._point_size = None\n\n    def wheelEvent(self, event):\n        modifiers = QtWidgets.QApplication.keyboardModifiers()\n        if modifiers != QtCore.Qt.ControlModifier:\n            super(ZoomPlainText, self).wheelEvent(event)\n            return\n\n        if hasattr(event, \"angleDelta\"):\n            delta = event.angleDelta().y()\n        else:\n            delta = event.delta()\n        degrees = float(delta) / 8\n        steps = int(ceil(degrees / 5))\n        self._scheduled_scalings += steps\n        if (self._scheduled_scalings * steps < 0):\n            self._scheduled_scalings = steps\n\n        self._anim_timer.start()\n\n    def _scaling_callback(self):\n        if self._scheduled_scalings == 0:\n            self._anim_timer.stop()\n            return\n\n        factor = 1.0 + (self._scheduled_scalings / 300)\n        font = self.font()\n\n        if self._point_size is None:\n            point_size = font.pointSizeF()\n        else:\n            point_size = self._point_size\n\n        point_size *= factor\n        min_hit = False\n        max_hit = False\n        if point_size < self.min_point_size:\n            point_size = self.min_point_size\n            min_hit = True\n        elif point_size > self.max_point_size:\n            point_size = self.max_point_size\n            max_hit = True\n\n        self._point_size = point_size\n\n        font.setPointSizeF(point_size)\n        # Using 'self.setFont(font)' would not be propagated when stylesheets\n        #   are applied on this widget\n        self.setStyleSheet(\"font-size: {}pt\".format(font.pointSize()))\n\n        if (\n            (max_hit and self._scheduled_scalings > 0)\n            or (min_hit and self._scheduled_scalings < 0)\n        ):\n            self._scheduled_scalings = 0\n\n        elif self._scheduled_scalings > 0:\n            self._scheduled_scalings -= 1\n        else:\n            self._scheduled_scalings += 1\n\n\nclass DetailsWidget(QtWidgets.QWidget):\n    def __init__(self, parent):\n        super(DetailsWidget, self).__init__(parent)\n\n        output_widget = ZoomPlainText(self)\n        output_widget.setObjectName(\"PublishLogConsole\")\n        output_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(output_widget)\n\n        self._output_widget = output_widget\n        self._report_item = None\n        self._instance_filter = set()\n        self._plugin_filter = set()\n\n    def clear(self):\n        self._output_widget.setPlainText(\"\")\n\n    def set_report(self, report):\n        self._report_item = report\n        self._plugin_filter = set()\n        self._instance_filter = set()\n        self._update_logs()\n\n    def set_plugin_filter(self, plugin_filter):\n        self._plugin_filter = plugin_filter\n        self._update_logs()\n\n    def set_instance_filter(self, instance_filter):\n        self._instance_filter = instance_filter\n        self._update_logs()\n\n    def _update_logs(self):\n        if not self._report_item:\n            self._output_widget.setPlainText(\"\")\n            return\n\n        filtered_logs = []\n        for log in self._report_item.logs:\n            if (\n                self._instance_filter\n                and log.instance_id not in self._instance_filter\n            ):\n                continue\n\n            if (\n                self._plugin_filter\n                and log.plugin_id not in self._plugin_filter\n            ):\n                continue\n            filtered_logs.append(log)\n\n        self._set_logs(filtered_logs)\n\n    def _set_logs(self, logs):\n        lines = []\n        for log in logs:\n            if log[\"type\"] == \"record\":\n                message = \"{}: {}\".format(log[\"levelname\"], log[\"msg\"])\n\n                lines.append(message)\n                exc_info = log[\"exc_info\"]\n                if exc_info:\n                    lines.append(exc_info)\n\n            elif log[\"type\"] == \"error\":\n                lines.append(log[\"traceback\"])\n\n            else:\n                print(log[\"type\"])\n\n        text = \"\\n\".join(lines)\n        self._output_widget.setPlainText(text)\n\n\nclass DeselectableTreeView(QtWidgets.QTreeView):\n    \"\"\"A tree view that deselects on clicking on an empty area in the view\"\"\"\n\n    def mousePressEvent(self, event):\n        index = self.indexAt(event.pos())\n        clear_selection = False\n        if not index.isValid():\n            modifiers = QtWidgets.QApplication.keyboardModifiers()\n            if modifiers == QtCore.Qt.ShiftModifier:\n                return\n            elif modifiers == QtCore.Qt.ControlModifier:\n                return\n            clear_selection = True\n        else:\n            indexes = self.selectedIndexes()\n            if len(indexes) == 1 and index in indexes:\n                clear_selection = True\n\n        if clear_selection:\n            # clear the selection\n            self.clearSelection()\n            # clear the current index\n            self.setCurrentIndex(QtCore.QModelIndex())\n            event.accept()\n            return\n\n        QtWidgets.QTreeView.mousePressEvent(self, event)\n\n\nclass DetailsPopup(QtWidgets.QDialog):\n    closed = QtCore.Signal()\n\n    def __init__(self, parent, center_widget):\n        super(DetailsPopup, self).__init__(parent)\n        self.setWindowTitle(\"Report Details\")\n        layout = QtWidgets.QHBoxLayout(self)\n\n        self._center_widget = center_widget\n        self._first_show = True\n        self._layout = layout\n\n    def showEvent(self, event):\n        layout = self.layout()\n        layout.insertWidget(0, self._center_widget)\n        super(DetailsPopup, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self.resize(700, 400)\n\n    def closeEvent(self, event):\n        super(DetailsPopup, self).closeEvent(event)\n        self.closed.emit()\n\n\nclass PublishReportViewerWidget(QtWidgets.QFrame):\n    def __init__(self, parent=None):\n        super(PublishReportViewerWidget, self).__init__(parent)\n\n        instances_model = InstancesModel()\n        instances_proxy = InstanceProxyModel()\n        instances_proxy.setSourceModel(instances_model)\n\n        plugins_model = PluginsModel()\n        plugins_proxy = PluginProxyModel()\n        plugins_proxy.setSourceModel(plugins_model)\n\n        removed_instances_check = NiceCheckbox(parent=self)\n        removed_instances_check.setChecked(instances_proxy.ignore_removed)\n        removed_instances_label = QtWidgets.QLabel(\n            \"Hide removed instances\", self\n        )\n\n        removed_instances_layout = QtWidgets.QHBoxLayout()\n        removed_instances_layout.setContentsMargins(0, 0, 0, 0)\n        removed_instances_layout.addWidget(removed_instances_check, 0)\n        removed_instances_layout.addWidget(removed_instances_label, 1)\n\n        instances_view = DeselectableTreeView(self)\n        instances_view.setObjectName(\"PublishDetailViews\")\n        instances_view.setModel(instances_proxy)\n        instances_view.setIndentation(0)\n        instances_view.setHeaderHidden(True)\n        instances_view.setEditTriggers(\n            QtWidgets.QAbstractItemView.NoEditTriggers)\n        instances_view.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection)\n        instances_view.setExpandsOnDoubleClick(False)\n\n        instances_delegate = GroupItemDelegate(instances_view)\n        instances_view.setItemDelegate(instances_delegate)\n\n        skipped_plugins_check = NiceCheckbox(parent=self)\n        skipped_plugins_check.setChecked(plugins_proxy.ignore_skipped)\n        skipped_plugins_label = QtWidgets.QLabel(\"Hide skipped plugins\", self)\n\n        skipped_plugins_layout = QtWidgets.QHBoxLayout()\n        skipped_plugins_layout.setContentsMargins(0, 0, 0, 0)\n        skipped_plugins_layout.addWidget(skipped_plugins_check, 0)\n        skipped_plugins_layout.addWidget(skipped_plugins_label, 1)\n\n        plugins_view = DeselectableTreeView(self)\n        plugins_view.setObjectName(\"PublishDetailViews\")\n        plugins_view.setModel(plugins_proxy)\n        plugins_view.setIndentation(0)\n        plugins_view.setHeaderHidden(True)\n        plugins_view.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection)\n        plugins_view.setEditTriggers(\n            QtWidgets.QAbstractItemView.NoEditTriggers)\n        plugins_view.setExpandsOnDoubleClick(False)\n\n        plugins_delegate = GroupItemDelegate(plugins_view)\n        plugins_view.setItemDelegate(plugins_delegate)\n\n        details_widget = QtWidgets.QWidget(self)\n        details_tab_widget = QtWidgets.QTabWidget(details_widget)\n        details_popup_btn = QtWidgets.QPushButton(\"PopUp\", details_widget)\n\n        details_layout = QtWidgets.QVBoxLayout(details_widget)\n        details_layout.setContentsMargins(0, 0, 0, 0)\n        details_layout.addWidget(details_tab_widget, 1)\n        details_layout.addWidget(details_popup_btn, 0)\n\n        details_popup = DetailsPopup(self, details_tab_widget)\n\n        logs_text_widget = DetailsWidget(details_tab_widget)\n        plugin_load_report_widget = PluginLoadReportWidget(details_tab_widget)\n\n        details_tab_widget.addTab(logs_text_widget, \"Logs\")\n        details_tab_widget.addTab(plugin_load_report_widget, \"Crashed plugins\")\n\n        middle_widget = QtWidgets.QWidget(self)\n        middle_layout = QtWidgets.QGridLayout(middle_widget)\n        middle_layout.setContentsMargins(0, 0, 0, 0)\n        # Row 1\n        middle_layout.addLayout(removed_instances_layout, 0, 0)\n        middle_layout.addLayout(skipped_plugins_layout, 0, 1)\n        # Row 2\n        middle_layout.addWidget(instances_view, 1, 0)\n        middle_layout.addWidget(plugins_view, 1, 1)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(middle_widget, 0)\n        layout.addWidget(details_widget, 1)\n\n        instances_view.selectionModel().selectionChanged.connect(\n            self._on_instance_change\n        )\n        instances_view.clicked.connect(self._on_instance_view_clicked)\n        plugins_view.clicked.connect(self._on_plugin_view_clicked)\n        plugins_view.selectionModel().selectionChanged.connect(\n            self._on_plugin_change\n        )\n\n        skipped_plugins_check.stateChanged.connect(\n            self._on_skipped_plugin_check\n        )\n        removed_instances_check.stateChanged.connect(\n            self._on_removed_instances_check\n        )\n        details_popup_btn.clicked.connect(self._on_details_popup)\n        details_popup.closed.connect(self._on_popup_close)\n\n        self._ignore_selection_changes = False\n        self._report_item = None\n        self._logs_text_widget = logs_text_widget\n        self._plugin_load_report_widget = plugin_load_report_widget\n\n        self._removed_instances_check = removed_instances_check\n        self._instances_view = instances_view\n        self._instances_model = instances_model\n        self._instances_proxy = instances_proxy\n\n        self._instances_delegate = instances_delegate\n        self._plugins_delegate = plugins_delegate\n\n        self._skipped_plugins_check = skipped_plugins_check\n        self._plugins_view = plugins_view\n        self._plugins_model = plugins_model\n        self._plugins_proxy = plugins_proxy\n\n        self._details_widget = details_widget\n        self._details_tab_widget = details_tab_widget\n        self._details_popup = details_popup\n\n    def _on_instance_view_clicked(self, index):\n        if not index.isValid() or not index.data(ITEM_IS_GROUP_ROLE):\n            return\n\n        if self._instances_view.isExpanded(index):\n            self._instances_view.collapse(index)\n        else:\n            self._instances_view.expand(index)\n\n    def _on_plugin_view_clicked(self, index):\n        if not index.isValid() or not index.data(ITEM_IS_GROUP_ROLE):\n            return\n\n        if self._plugins_view.isExpanded(index):\n            self._plugins_view.collapse(index)\n        else:\n            self._plugins_view.expand(index)\n\n    def set_report_data(self, report_data):\n        report = PublishReport(report_data)\n        self.set_report(report)\n\n    def set_report(self, report):\n        self._ignore_selection_changes = True\n\n        self._report_item = report\n\n        self._instances_model.set_report(report)\n        self._plugins_model.set_report(report)\n        self._logs_text_widget.set_report(report)\n        self._plugin_load_report_widget.set_report(report)\n\n        self._ignore_selection_changes = False\n\n        self._instances_view.expandAll()\n        self._plugins_view.expandAll()\n\n    def _on_instance_change(self, *_args):\n        if self._ignore_selection_changes:\n            return\n\n        instance_ids = set()\n        for index in self._instances_view.selectedIndexes():\n            if index.isValid():\n                instance_ids.add(index.data(ITEM_ID_ROLE))\n\n        self._logs_text_widget.set_instance_filter(instance_ids)\n\n    def _on_plugin_change(self, *_args):\n        if self._ignore_selection_changes:\n            return\n\n        plugin_ids = set()\n        for index in self._plugins_view.selectedIndexes():\n            if index.isValid():\n                plugin_ids.add(index.data(ITEM_ID_ROLE))\n\n        self._logs_text_widget.set_plugin_filter(plugin_ids)\n\n    def _on_skipped_plugin_check(self):\n        self._plugins_proxy.set_ignore_skipped(\n            self._skipped_plugins_check.isChecked()\n        )\n\n    def _on_removed_instances_check(self):\n        self._instances_proxy.set_ignore_removed(\n            self._removed_instances_check.isChecked()\n        )\n\n    def _on_details_popup(self):\n        self._details_widget.setVisible(False)\n        self._details_popup.show()\n\n    def _on_popup_close(self):\n        self._details_widget.setVisible(True)\n        layout = self._details_widget.layout()\n        layout.insertWidget(0, self._details_tab_widget)\n\n    def close_details_popup(self):\n        if self._details_popup.isVisible():\n            self._details_popup.close()\n"
  },
  {
    "path": "openpype/tools/publisher/publish_report_viewer/window.py",
    "content": "import os\nimport json\nimport six\nimport uuid\n\nimport appdirs\nimport arrow\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import style\nfrom openpype.resources import get_openpype_icon_filepath\nfrom openpype.tools import resources\nfrom openpype.tools.utils import (\n    IconButton,\n    paint_image_with_color\n)\n\nfrom openpype.tools.utils.delegates import PrettyTimeDelegate\n\nif __package__:\n    from .widgets import PublishReportViewerWidget\n    from .report_items import PublishReport\nelse:\n    from widgets import PublishReportViewerWidget\n    from report_items import PublishReport\n\n\nITEM_ID_ROLE = QtCore.Qt.UserRole + 1\nITEM_CREATED_AT_ROLE = QtCore.Qt.UserRole + 2\n\n\ndef get_reports_dir():\n    \"\"\"Root directory where publish reports are stored for next session.\n\n    Returns:\n        str: Path to directory where reports are stored.\n    \"\"\"\n\n    report_dir = os.path.join(\n        appdirs.user_data_dir(\"openpype\", \"pypeclub\"),\n        \"publish_report_viewer\"\n    )\n    if not os.path.exists(report_dir):\n        os.makedirs(report_dir)\n    return report_dir\n\n\nclass PublishReportItem:\n    \"\"\"Report item representing one file in report directory.\"\"\"\n\n    def __init__(self, content):\n        changed = self._fix_content(content)\n\n        report_path = os.path.join(get_reports_dir(), content[\"id\"])\n        file_modified = None\n        if os.path.exists(report_path):\n            file_modified = os.path.getmtime(report_path)\n\n        created_at_obj = arrow.get(content[\"created_at\"]).to(\"local\")\n        created_at = created_at_obj.float_timestamp\n\n        self.content = content\n        self.report_path = report_path\n        self.file_modified = file_modified\n        self.created_at = float(created_at)\n        self._loaded_label = content.get(\"label\")\n        self._changed = changed\n        self.publish_report = PublishReport(content)\n\n    @property\n    def version(self):\n        \"\"\"Publish report version.\n\n        Returns:\n            str: Publish report version.\n        \"\"\"\n        return self.content[\"report_version\"]\n\n    @property\n    def id(self):\n        \"\"\"Publish report id.\n\n        Returns:\n            str: Publish report id.\n        \"\"\"\n\n        return self.content[\"id\"]\n\n    def get_label(self):\n        \"\"\"Publish report label.\n\n        Returns:\n            str: Publish report label showed in UI.\n        \"\"\"\n\n        return self.content.get(\"label\") or \"Unfilled label\"\n\n    def set_label(self, label):\n        \"\"\"Set publish report label.\n\n        Args:\n            label (str): New publish report label.\n        \"\"\"\n\n        if not label:\n            self.content.pop(\"label\", None)\n        self.content[\"label\"] = label\n\n    label = property(get_label, set_label)\n\n    @property\n    def loaded_label(self):\n        return self._loaded_label\n\n    def mark_as_changed(self):\n        \"\"\"Mark report as changed.\"\"\"\n\n        self._changed = True\n\n    def save(self):\n        \"\"\"Save publish report to file.\"\"\"\n\n        save = False\n        if (\n            self._changed\n            or self._loaded_label != self.label\n            or not os.path.exists(self.report_path)\n            or self.file_modified != os.path.getmtime(self.report_path)\n        ):\n            save = True\n\n        if not save:\n            return\n\n        with open(self.report_path, \"w\") as stream:\n            json.dump(self.content, stream)\n\n        self._loaded_label = self.content.get(\"label\")\n        self._changed = False\n        self.file_modified = os.path.getmtime(self.report_path)\n\n    @classmethod\n    def from_filepath(cls, filepath):\n        \"\"\"Create report item from file.\n\n        Args:\n            filepath (str): Path to report file. Content must be json.\n\n        Returns:\n            PublishReportItem: Report item.\n        \"\"\"\n\n        if not os.path.exists(filepath):\n            return None\n\n        try:\n            with open(filepath, \"r\") as stream:\n                content = json.load(stream)\n\n            file_modified = os.path.getmtime(filepath)\n            changed = cls._fix_content(content, file_modified=file_modified)\n            obj = cls(content)\n            if changed:\n                obj.mark_as_changed()\n            return obj\n\n        except Exception:\n            return None\n\n    def remove_file(self):\n        \"\"\"Remove report file.\"\"\"\n\n        if os.path.exists(self.report_path):\n            os.remove(self.report_path)\n\n    def update_file_content(self):\n        \"\"\"Update report content in file.\"\"\"\n\n        if not os.path.exists(self.report_path):\n            return\n\n        file_modified = os.path.getmtime(self.report_path)\n        if file_modified == self.file_modified:\n            return\n\n        with open(self.report_path, \"r\") as stream:\n            content = json.load(self.content, stream)\n\n        item_id = content.get(\"id\")\n        version = content.get(\"report_version\")\n        if not item_id:\n            item_id = str(uuid.uuid4())\n            content[\"id\"] = item_id\n\n        if not version:\n            version = \"0.0.1\"\n            content[\"report_version\"] = version\n\n        self.content = content\n        self.file_modified = file_modified\n\n    @classmethod\n    def _fix_content(cls, content, file_modified=None):\n        \"\"\"Fix content for backward compatibility of older report items.\n\n        Args:\n            content (dict[str, Any]): Report content.\n            file_modified (Optional[float]): File modification time.\n\n        Returns:\n            bool: True if content was changed, False otherwise.\n        \"\"\"\n\n        # Fix created_at key\n        changed = cls._fix_created_at(content, file_modified)\n\n        # NOTE backward compatibility for 'id' and 'report_version' is from\n        #    28.10.2022 https://github.com/ynput/OpenPype/pull/4040\n        # We can probably safely remove it\n\n        # Fix missing 'id'\n        item_id = content.get(\"id\")\n        if not item_id:\n            item_id = str(uuid.uuid4())\n            changed = True\n            content[\"id\"] = item_id\n\n        # Fix missing 'report_version'\n        if not content.get(\"report_version\"):\n            changed = True\n            content[\"report_version\"] = \"0.0.1\"\n        return changed\n\n    @classmethod\n    def _fix_created_at(cls, content, file_modified):\n        # Key 'create_at' was added in report version 1.0.1\n        created_at = content.get(\"created_at\")\n        if created_at:\n            return False\n\n        # Auto fix 'created_at', use file modification time if it is not set\n        #   or current time if modification could not be received.\n        if file_modified is not None:\n            created_at_obj = arrow.Arrow.fromtimestamp(file_modified)\n        else:\n            created_at_obj = arrow.utcnow()\n        content[\"created_at\"] = created_at_obj.to(\"local\").isoformat()\n        return True\n\n\nclass PublisherReportHandler:\n    \"\"\"Class handling storing publish report items.\"\"\"\n\n    def __init__(self):\n        self._reports = None\n        self._reports_by_id = {}\n\n    def reset(self):\n        self._reports = None\n        self._reports_by_id = {}\n\n    def list_reports(self):\n        if self._reports is not None:\n            return self._reports\n\n        reports = []\n        reports_by_id = {}\n        report_dir = get_reports_dir()\n        for filename in os.listdir(report_dir):\n            ext = os.path.splitext(filename)[-1]\n            if ext == \".json\":\n                continue\n            filepath = os.path.join(report_dir, filename)\n            item = PublishReportItem.from_filepath(filepath)\n            if item is not None:\n                reports.append(item)\n                reports_by_id[item.id] = item\n\n        self._reports = reports\n        self._reports_by_id = reports_by_id\n        return reports\n\n    def remove_report_item(self, item_id):\n        \"\"\"Remove report item by id.\n\n        Remove from cache and also remove the file with the content.\n\n        Args:\n            item_id (str): Report item id.\n        \"\"\"\n\n        item = self._reports_by_id.get(item_id)\n        if item:\n            try:\n                item.remove_file()\n                self._reports_by_id.get(item_id)\n            except Exception:\n                pass\n\n\nclass LoadedFilesModel(QtGui.QStandardItemModel):\n    header_labels = (\"Reports\", \"Created\")\n\n    def __init__(self, *args, **kwargs):\n        super(LoadedFilesModel, self).__init__(*args, **kwargs)\n\n        # Column count must be set before setting header data\n        self.setColumnCount(len(self.header_labels))\n        for col, label in enumerate(self.header_labels):\n            self.setHeaderData(col, QtCore.Qt.Horizontal, label)\n\n        self._items_by_id = {}\n        self._report_items_by_id = {}\n\n        self._handler = PublisherReportHandler()\n\n        self._loading_registry = False\n\n    def refresh(self):\n        root_item = self.invisibleRootItem()\n        if root_item.rowCount() > 0:\n            root_item.removeRows(0, root_item.rowCount())\n        self._items_by_id = {}\n        self._report_items_by_id = {}\n\n        self._handler.reset()\n\n        new_items = []\n        for report_item in self._handler.list_reports():\n            item = self._create_item(report_item)\n            self._report_items_by_id[report_item.id] = report_item\n            self._items_by_id[report_item.id] = item\n            new_items.append(item)\n\n        if new_items:\n            root_item = self.invisibleRootItem()\n            root_item.appendRows(new_items)\n\n    def data(self, index, role=None):\n        if role is None:\n            role = QtCore.Qt.DisplayRole\n\n        col = index.column()\n        if col == 1:\n            if role in (\n                QtCore.Qt.DisplayRole, QtCore.Qt.InitialSortOrderRole\n            ):\n                role = ITEM_CREATED_AT_ROLE\n\n        if col != 0:\n            index = self.index(index.row(), 0, index.parent())\n\n        return super(LoadedFilesModel, self).data(index, role)\n\n    def setData(self, index, value, role=None):\n        if role is None:\n            role = QtCore.Qt.EditRole\n\n        if role == QtCore.Qt.EditRole:\n            item_id = index.data(ITEM_ID_ROLE)\n            report_item = self._report_items_by_id.get(item_id)\n            if report_item is not None:\n                report_item.label = value\n                report_item.save()\n                value = report_item.label\n\n        return super(LoadedFilesModel, self).setData(index, value, role)\n\n    def flags(self, index):\n        # Allow editable flag only for first column\n        if index.column() > 0:\n            return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled\n        return super(LoadedFilesModel, self).flags(index)\n\n    def _create_item(self, report_item):\n        if report_item.id in self._items_by_id:\n            return None\n\n        item = QtGui.QStandardItem(report_item.label)\n        item.setColumnCount(self.columnCount())\n        item.setData(report_item.id, ITEM_ID_ROLE)\n        item.setData(report_item.created_at, ITEM_CREATED_AT_ROLE)\n\n        return item\n\n    def add_filepaths(self, filepaths):\n        if not filepaths:\n            return\n\n        if isinstance(filepaths, six.string_types):\n            filepaths = [filepaths]\n\n        filtered_paths = []\n        for filepath in filepaths:\n            normalized_path = os.path.normpath(filepath)\n            if (\n                os.path.exists(normalized_path)\n                and normalized_path not in filtered_paths\n            ):\n                filtered_paths.append(normalized_path)\n\n        if not filtered_paths:\n            return\n\n        new_items = []\n        for normalized_path in filtered_paths:\n            report_item = PublishReportItem.from_filepath(normalized_path)\n            if report_item is None:\n                continue\n\n            # Skip already added report items\n            # QUESTION: Should we replace existing or skip the item?\n            if report_item.id in self._items_by_id:\n                continue\n\n            if not report_item.loaded_label:\n                report_item.label = (\n                    os.path.splitext(os.path.basename(filepath))[0]\n                )\n\n            item = self._create_item(report_item)\n            if item is None:\n                continue\n\n            new_items.append(item)\n            report_item.save()\n            self._items_by_id[report_item.id] = item\n            self._report_items_by_id[report_item.id] = report_item\n\n        if new_items:\n            root_item = self.invisibleRootItem()\n            root_item.appendRows(new_items)\n\n    def remove_item_by_id(self, item_id):\n        self._handler.remove_report_item(item_id)\n\n        self._report_items_by_id.pop(item_id, None)\n        item = self._items_by_id.pop(item_id, None)\n        if item is not None:\n            parent = self.invisibleRootItem()\n            parent.removeRow(item.row())\n\n    def get_report_by_id(self, item_id):\n        report_item = self._report_items_by_id.get(item_id)\n        if report_item:\n            return report_item.publish_report\n        return None\n\n\nclass LoadedFilesView(QtWidgets.QTreeView):\n    selection_changed = QtCore.Signal()\n\n    def __init__(self, *args, **kwargs):\n        super(LoadedFilesView, self).__init__(*args, **kwargs)\n        self.setEditTriggers(\n            QtWidgets.QAbstractItemView.EditKeyPressed\n            | QtWidgets.QAbstractItemView.SelectedClicked\n            | QtWidgets.QAbstractItemView.DoubleClicked\n        )\n        self.setIndentation(0)\n        self.setAlternatingRowColors(True)\n        self.setSortingEnabled(True)\n\n        model = LoadedFilesModel()\n        proxy_model = QtCore.QSortFilterProxyModel()\n        proxy_model.setSourceModel(model)\n        self.setModel(proxy_model)\n\n        time_delegate = PrettyTimeDelegate()\n        self.setItemDelegateForColumn(1, time_delegate)\n\n        self.sortByColumn(1, QtCore.Qt.AscendingOrder)\n\n        remove_btn = IconButton(self)\n        remove_icon_path = resources.get_icon_path(\"delete\")\n        loaded_remove_image = QtGui.QImage(remove_icon_path)\n        pix = paint_image_with_color(loaded_remove_image, QtCore.Qt.white)\n        icon = QtGui.QIcon(pix)\n        remove_btn.setIcon(icon)\n\n        model.rowsInserted.connect(self._on_rows_inserted)\n        remove_btn.clicked.connect(self._on_remove_clicked)\n        self.selectionModel().selectionChanged.connect(\n            self._on_selection_change\n        )\n\n        self._model = model\n        self._proxy_model = proxy_model\n        self._time_delegate = time_delegate\n        self._remove_btn = remove_btn\n\n    def _update_remove_btn(self):\n        viewport = self.viewport()\n        height = viewport.height() + self.header().height()\n        pos_x = viewport.width() - self._remove_btn.width() - 5\n        pos_y = height - self._remove_btn.height() - 5\n        self._remove_btn.move(max(0, pos_x), max(0, pos_y))\n\n    def _on_rows_inserted(self):\n        header = self.header()\n        header.resizeSections(QtWidgets.QHeaderView.ResizeToContents)\n        self._update_remove_btn()\n\n    def resizeEvent(self, event):\n        super(LoadedFilesView, self).resizeEvent(event)\n        self._update_remove_btn()\n\n    def showEvent(self, event):\n        super(LoadedFilesView, self).showEvent(event)\n        self._model.refresh()\n        header = self.header()\n        header.resizeSections(QtWidgets.QHeaderView.ResizeToContents)\n        self._update_remove_btn()\n\n    def _on_selection_change(self):\n        self.selection_changed.emit()\n\n    def add_filepaths(self, filepaths):\n        self._model.add_filepaths(filepaths)\n        self._fill_selection()\n\n    def remove_item_by_id(self, item_id):\n        self._model.remove_item_by_id(item_id)\n        self._fill_selection()\n\n    def _on_remove_clicked(self):\n        index = self.currentIndex()\n        item_id = index.data(ITEM_ID_ROLE)\n        self.remove_item_by_id(item_id)\n\n    def _fill_selection(self):\n        index = self.currentIndex()\n        if index.isValid():\n            return\n\n        model = self.model()\n        index = model.index(0, 0)\n        if index.isValid():\n            self.setCurrentIndex(index)\n\n    def get_current_report(self):\n        index = self.currentIndex()\n        item_id = index.data(ITEM_ID_ROLE)\n        return self._model.get_report_by_id(item_id)\n\n\nclass LoadedFilesWidget(QtWidgets.QWidget):\n    report_changed = QtCore.Signal()\n\n    def __init__(self, parent):\n        super(LoadedFilesWidget, self).__init__(parent)\n\n        self.setAcceptDrops(True)\n\n        view = LoadedFilesView(self)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(view, 1)\n\n        view.selection_changed.connect(self._on_report_change)\n\n        self._view = view\n\n    def dragEnterEvent(self, event):\n        mime_data = event.mimeData()\n        if mime_data.hasUrls():\n            event.setDropAction(QtCore.Qt.CopyAction)\n            event.accept()\n\n    def dragLeaveEvent(self, event):\n        event.accept()\n\n    def dropEvent(self, event):\n        mime_data = event.mimeData()\n        if mime_data.hasUrls():\n            filepaths = []\n            for url in mime_data.urls():\n                filepath = url.toLocalFile()\n                ext = os.path.splitext(filepath)[-1]\n                if os.path.exists(filepath) and ext == \".json\":\n                    filepaths.append(filepath)\n            self._add_filepaths(filepaths)\n        event.accept()\n\n    def _on_report_change(self):\n        self.report_changed.emit()\n\n    def _add_filepaths(self, filepaths):\n        self._view.add_filepaths(filepaths)\n\n    def get_current_report(self):\n        return self._view.get_current_report()\n\n\nclass PublishReportViewerWindow(QtWidgets.QWidget):\n    default_width = 1200\n    default_height = 600\n\n    def __init__(self, parent=None):\n        super(PublishReportViewerWindow, self).__init__(parent)\n        self.setWindowTitle(\"Publish report viewer\")\n        icon = QtGui.QIcon(get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n\n        body = QtWidgets.QSplitter(self)\n        body.setContentsMargins(0, 0, 0, 0)\n        body.setSizePolicy(\n            QtWidgets.QSizePolicy.Expanding,\n            QtWidgets.QSizePolicy.Expanding\n        )\n        body.setOrientation(QtCore.Qt.Horizontal)\n\n        loaded_files_widget = LoadedFilesWidget(body)\n        main_widget = PublishReportViewerWidget(body)\n\n        body.addWidget(loaded_files_widget)\n        body.addWidget(main_widget)\n        body.setStretchFactor(0, 70)\n        body.setStretchFactor(1, 65)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.addWidget(body, 1)\n\n        loaded_files_widget.report_changed.connect(self._on_report_change)\n\n        self._loaded_files_widget = loaded_files_widget\n        self._main_widget = main_widget\n\n        self.resize(self.default_width, self.default_height)\n        self.setStyleSheet(style.load_stylesheet())\n\n    def _on_report_change(self):\n        report = self._loaded_files_widget.get_current_report()\n        self.set_report(report)\n\n    def set_report(self, report_data):\n        self._main_widget.set_report(report_data)\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/__init__.py",
    "content": "from .icons import (\n    get_icon_path,\n    get_pixmap,\n    get_icon\n)\nfrom .widgets import (\n    SaveBtn,\n    ResetBtn,\n    StopBtn,\n    ValidateBtn,\n    PublishBtn,\n    CreateNextPageOverlay,\n)\nfrom .help_widget import (\n    HelpButton,\n    HelpDialog,\n)\nfrom .publish_frame import PublishFrame\nfrom .tabs_widget import PublisherTabsWidget\nfrom .overview_widget import OverviewWidget\nfrom .report_page import ReportPageWidget\n\n\n__all__ = (\n    \"get_icon_path\",\n    \"get_pixmap\",\n    \"get_icon\",\n\n    \"SaveBtn\",\n    \"ResetBtn\",\n    \"StopBtn\",\n    \"ValidateBtn\",\n    \"PublishBtn\",\n    \"CreateNextPageOverlay\",\n\n    \"HelpButton\",\n    \"HelpDialog\",\n\n    \"PublishFrame\",\n\n    \"PublisherTabsWidget\",\n    \"OverviewWidget\",\n    \"ReportPageWidget\",\n)\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/assets_widget.py",
    "content": "import collections\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.tools.utils import (\n    PlaceholderLineEdit,\n    RecursiveSortFilterProxyModel,\n    get_asset_icon,\n)\nfrom openpype.tools.utils.assets_widget import (\n    SingleSelectAssetsWidget,\n    ASSET_ID_ROLE,\n    ASSET_NAME_ROLE,\n    ASSET_PATH_ROLE,\n)\n\n\nclass CreateWidgetAssetsWidget(SingleSelectAssetsWidget):\n    current_context_required = QtCore.Signal()\n    header_height_changed = QtCore.Signal(int)\n\n    def __init__(self, controller, parent):\n        self._controller = controller\n        super(CreateWidgetAssetsWidget, self).__init__(None, parent)\n\n        self.set_refresh_btn_visibility(False)\n        self.set_current_asset_btn_visibility(False)\n\n        self._last_selection = None\n        self._enabled = None\n\n        self._last_filter_height = None\n\n    def get_selected_asset_name(self):\n        if AYON_SERVER_ENABLED:\n            selection_model = self._view.selectionModel()\n            indexes = selection_model.selectedRows()\n            for index in indexes:\n                return index.data(ASSET_PATH_ROLE)\n            return None\n        return super(CreateWidgetAssetsWidget, self).get_selected_asset_name()\n\n    def _check_header_height(self):\n        \"\"\"Catch header height changes.\n\n        Label on top of creaters should have same height so Creators view has\n        same offset.\n        \"\"\"\n        height = self.header_widget.height()\n        if height != self._last_filter_height:\n            self._last_filter_height = height\n            self.header_height_changed.emit(height)\n\n    def resizeEvent(self, event):\n        super(CreateWidgetAssetsWidget, self).resizeEvent(event)\n        self._check_header_height()\n\n    def showEvent(self, event):\n        super(CreateWidgetAssetsWidget, self).showEvent(event)\n        self._check_header_height()\n\n    def _on_current_asset_click(self):\n        self.current_context_required.emit()\n\n    def set_enabled(self, enabled):\n        if self._enabled == enabled:\n            return\n        self._enabled = enabled\n        if not enabled:\n            self._last_selection = self.get_selected_asset_id()\n            self._clear_selection()\n        elif self._last_selection is not None:\n            self.select_asset(self._last_selection)\n\n    def _select_indexes(self, *args, **kwargs):\n        super(CreateWidgetAssetsWidget, self)._select_indexes(*args, **kwargs)\n        if self._enabled:\n            return\n        self._last_selection = self.get_selected_asset_id()\n        self._clear_selection()\n\n    def update_current_asset(self):\n        # Hide set current asset if there is no one\n        asset_name = self._get_current_session_asset()\n        self.set_current_asset_btn_visibility(bool(asset_name))\n\n    def _get_current_session_asset(self):\n        return self._controller.current_asset_name\n\n    def _create_source_model(self):\n        return AssetsHierarchyModel(self._controller)\n\n    def _refresh_model(self):\n        self._model.reset()\n        self._on_model_refresh(self._model.rowCount() > 0)\n\n\nclass AssetsHierarchyModel(QtGui.QStandardItemModel):\n    \"\"\"Assets hierarchy model.\n\n    For selecting asset for which an instance should be created.\n\n    Uses controller to load asset hierarchy. All asset documents are stored by\n    their parents.\n    \"\"\"\n\n    def __init__(self, controller):\n        super(AssetsHierarchyModel, self).__init__()\n        self._controller = controller\n\n        self._items_by_name = {}\n        self._items_by_path = {}\n        self._items_by_asset_id = {}\n\n    def reset(self):\n        self.clear()\n\n        self._items_by_name = {}\n        self._items_by_path = {}\n        self._items_by_asset_id = {}\n        assets_by_parent_id = self._controller.get_asset_hierarchy()\n\n        items_by_name = {}\n        items_by_path = {}\n        items_by_asset_id = {}\n        _queue = collections.deque()\n        _queue.append((self.invisibleRootItem(), None, None))\n        while _queue:\n            parent_item, parent_id, parent_path = _queue.popleft()\n            children = assets_by_parent_id.get(parent_id)\n            if not children:\n                continue\n\n            children_by_name = {\n                child[\"name\"]: child\n                for child in children\n            }\n            items = []\n            for name in sorted(children_by_name.keys()):\n                child = children_by_name[name]\n                child_id = child[\"_id\"]\n                if parent_path:\n                    child_path = \"{}/{}\".format(parent_path, name)\n                else:\n                    child_path = \"/{}\".format(name)\n\n                has_children = bool(assets_by_parent_id.get(child_id))\n                icon = get_asset_icon(child, has_children)\n\n                item = QtGui.QStandardItem(name)\n                item.setFlags(\n                    QtCore.Qt.ItemIsEnabled\n                    | QtCore.Qt.ItemIsSelectable\n                )\n                item.setData(icon, QtCore.Qt.DecorationRole)\n                item.setData(child_id, ASSET_ID_ROLE)\n                item.setData(name, ASSET_NAME_ROLE)\n                item.setData(child_path, ASSET_PATH_ROLE)\n\n                items_by_name[name] = item\n                items_by_path[child_path] = item\n                items_by_asset_id[child_id] = item\n                items.append(item)\n                _queue.append((item, child_id, child_path))\n\n            parent_item.appendRows(items)\n\n        self._items_by_name = items_by_name\n        self._items_by_path = items_by_path\n        self._items_by_asset_id = items_by_asset_id\n\n    def get_index_by_asset_id(self, asset_id):\n        item = self._items_by_asset_id.get(asset_id)\n        if item is not None:\n            return item.index()\n        return QtCore.QModelIndex()\n\n    def get_index_by_asset_name(self, asset_name):\n        item = None\n        if AYON_SERVER_ENABLED:\n            item = self._items_by_path.get(asset_name)\n\n        if item is None:\n            item = self._items_by_name.get(asset_name)\n\n        if item is None:\n            return QtCore.QModelIndex()\n        return item.index()\n\n    def name_is_valid(self, item_name):\n        if AYON_SERVER_ENABLED and item_name in self._items_by_path:\n            return True\n        return item_name in self._items_by_name\n\n\nclass AssetDialogView(QtWidgets.QTreeView):\n    double_clicked = QtCore.Signal(QtCore.QModelIndex)\n\n    def mouseDoubleClickEvent(self, event):\n        index = self.indexAt(event.pos())\n        if index.isValid():\n            self.double_clicked.emit(index)\n            event.accept()\n\n\nclass AssetsDialog(QtWidgets.QDialog):\n    \"\"\"Dialog to select asset for a context of instance.\"\"\"\n\n    def __init__(self, controller, parent):\n        super(AssetsDialog, self).__init__(parent)\n        self.setWindowTitle(\"Select asset\")\n\n        model = AssetsHierarchyModel(controller)\n        proxy_model = RecursiveSortFilterProxyModel()\n        proxy_model.setSourceModel(model)\n        proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        filter_input = PlaceholderLineEdit(self)\n        filter_input.setPlaceholderText(\"Filter {}..\".format(\n            \"folders\" if AYON_SERVER_ENABLED else \"assets\"))\n\n        asset_view = AssetDialogView(self)\n        asset_view.setModel(proxy_model)\n        asset_view.setHeaderHidden(True)\n        asset_view.setFrameShape(QtWidgets.QFrame.NoFrame)\n        asset_view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\n        asset_view.setAlternatingRowColors(True)\n        asset_view.setSelectionBehavior(QtWidgets.QTreeView.SelectRows)\n        asset_view.setAllColumnsShowFocus(True)\n\n        ok_btn = QtWidgets.QPushButton(\"OK\", self)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", self)\n\n        btns_layout = QtWidgets.QHBoxLayout()\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(ok_btn)\n        btns_layout.addWidget(cancel_btn)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(filter_input, 0)\n        layout.addWidget(asset_view, 1)\n        layout.addLayout(btns_layout, 0)\n\n        controller.event_system.add_callback(\n            \"controller.reset.finished\", self._on_controller_reset\n        )\n\n        asset_view.double_clicked.connect(self._on_ok_clicked)\n        filter_input.textChanged.connect(self._on_filter_change)\n        ok_btn.clicked.connect(self._on_ok_clicked)\n        cancel_btn.clicked.connect(self._on_cancel_clicked)\n\n        self._filter_input = filter_input\n        self._ok_btn = ok_btn\n        self._cancel_btn = cancel_btn\n\n        self._model = model\n        self._proxy_model = proxy_model\n\n        self._asset_view = asset_view\n\n        self._selected_asset = None\n        # Soft refresh is enabled\n        # - reset will happen at all cost if soft reset is enabled\n        # - adds ability to call reset on multiple places without repeating\n        self._soft_reset_enabled = True\n\n        self._first_show = True\n        self._default_height = 500\n\n    def _on_first_show(self):\n        center = self.rect().center()\n        size = self.size()\n        size.setHeight(self._default_height)\n\n        self.resize(size)\n        new_pos = self.mapToGlobal(center)\n        new_pos.setX(new_pos.x() - int(self.width() / 2))\n        new_pos.setY(new_pos.y() - int(self.height() / 2))\n        self.move(new_pos)\n\n    def _on_controller_reset(self):\n        # Change reset enabled so model is reset on show event\n        self._soft_reset_enabled = True\n\n    def showEvent(self, event):\n        \"\"\"Refresh asset model on show.\"\"\"\n        super(AssetsDialog, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self._on_first_show()\n        # Refresh on show\n        self.reset(False)\n\n    def reset(self, force=True):\n        \"\"\"Reset asset model.\"\"\"\n        if not force and not self._soft_reset_enabled:\n            return\n\n        if self._soft_reset_enabled:\n            self._soft_reset_enabled = False\n\n        self._model.reset()\n\n    def name_is_valid(self, name):\n        \"\"\"Is asset name valid.\n\n        Args:\n            name(str): Asset name that should be checked.\n        \"\"\"\n        # Make sure we're reset\n        self.reset(False)\n        # Valid the name by model\n        return self._model.name_is_valid(name)\n\n    def _on_filter_change(self, text):\n        \"\"\"Trigger change of filter of assets.\"\"\"\n        self._proxy_model.setFilterFixedString(text)\n\n    def _on_cancel_clicked(self):\n        self.done(0)\n\n    def _on_ok_clicked(self):\n        index = self._asset_view.currentIndex()\n        asset_name = None\n        if index.isValid():\n            if AYON_SERVER_ENABLED:\n                asset_name = index.data(ASSET_PATH_ROLE)\n            else:\n                asset_name = index.data(ASSET_NAME_ROLE)\n        self._selected_asset = asset_name\n        self.done(1)\n\n    def set_selected_assets(self, asset_names):\n        \"\"\"Change preselected asset before showing the dialog.\n\n        This also resets model and clean filter.\n        \"\"\"\n        self.reset(False)\n        self._asset_view.collapseAll()\n        self._filter_input.setText(\"\")\n\n        indexes = []\n        for asset_name in asset_names:\n            index = self._model.get_index_by_asset_name(asset_name)\n            if index.isValid():\n                indexes.append(index)\n\n        if not indexes:\n            return\n\n        index_deque = collections.deque()\n        for index in indexes:\n            index_deque.append(index)\n\n        all_indexes = []\n        while index_deque:\n            index = index_deque.popleft()\n            all_indexes.append(index)\n\n            parent_index = index.parent()\n            if parent_index.isValid():\n                index_deque.append(parent_index)\n\n        for index in all_indexes:\n            proxy_index = self._proxy_model.mapFromSource(index)\n            self._asset_view.expand(proxy_index)\n\n    def get_selected_asset(self):\n        \"\"\"Get selected asset name.\"\"\"\n        return self._selected_asset\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/border_label_widget.py",
    "content": "# -*- coding: utf-8 -*-\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.style import get_objected_colors\n\n\nclass _VLineWidget(QtWidgets.QWidget):\n    \"\"\"Widget drawing 1px wide vertical line.\n\n    ```  │  ```\n\n    Line is drawn in the middle of widget.\n\n    It is expected that parent widget will set width.\n    \"\"\"\n    def __init__(self, color, line_size, left, parent):\n        super(_VLineWidget, self).__init__(parent)\n        self._color = color\n        self._left = left\n        self._line_size = line_size\n\n    def set_line_size(self, line_size):\n        self._line_size = line_size\n\n    def paintEvent(self, event):\n        if not self.isVisible():\n            return\n\n        pos_x = self._line_size * 0.5\n        if not self._left:\n            pos_x = self.width() - pos_x\n\n        painter = QtGui.QPainter(self)\n        painter.setRenderHints(\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n        )\n\n        if self._color:\n            pen = QtGui.QPen(self._color)\n        else:\n            pen = painter.pen()\n        pen.setWidth(self._line_size)\n        painter.setPen(pen)\n        painter.setBrush(QtCore.Qt.transparent)\n        painter.drawRect(\n            QtCore.QRectF(\n                pos_x,\n                -self._line_size,\n                pos_x + (self.width() * 2),\n                self.height() + (self._line_size * 2)\n            )\n        )\n        painter.end()\n\n\nclass _HBottomLineWidget(QtWidgets.QWidget):\n    \"\"\"Widget drawing 1px wide vertical line with side lines going upwards.\n\n    ```└─────────────┘```\n\n    Corners may have curve set by radius (`set_radius`). Radius should expect\n    height of widget.\n\n    Bottom line is drawn at the bottom of widget. If radius is 0 then height\n    of widget should be 1px.\n\n    It is expected that parent widget will set height and radius.\n    \"\"\"\n    def __init__(self, color, line_size, parent):\n        super(_HBottomLineWidget, self).__init__(parent)\n        self._color = color\n        self._radius = 0\n        self._line_size = line_size\n\n    def set_radius(self, radius):\n        self._radius = radius\n\n    def set_line_size(self, line_size):\n        self._line_size = line_size\n\n    def paintEvent(self, event):\n        if not self.isVisible():\n            return\n\n        x_offset = self._line_size * 0.5\n        rect = QtCore.QRectF(\n            x_offset,\n            -self._radius,\n            self.width() - (2 * x_offset),\n            (self.height() + self._radius) - x_offset\n        )\n        painter = QtGui.QPainter(self)\n        painter.setRenderHints(\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n        )\n\n        if self._color:\n            pen = QtGui.QPen(self._color)\n        else:\n            pen = painter.pen()\n        pen.setWidth(self._line_size)\n        painter.setPen(pen)\n        painter.setBrush(QtCore.Qt.transparent)\n        if self._radius:\n            painter.drawRoundedRect(rect, self._radius, self._radius)\n        else:\n            painter.drawRect(rect)\n        painter.end()\n\n\nclass _HTopCornerLineWidget(QtWidgets.QWidget):\n    \"\"\"Widget drawing 1px wide horizontal line with side line going downwards.\n\n    ```────────┐```\n          or\n    ```┌───────```\n\n    Horizontal line is drawn in the middle of widget.\n\n    Widget represents left or right corner. Corner may have curve set by\n    radius (`set_radius`). Radius should expect height of widget (maximum half\n    height of widget).\n\n    It is expected that parent widget will set height and radius.\n    \"\"\"\n\n    def __init__(self, color, line_size, left_side, parent):\n        super(_HTopCornerLineWidget, self).__init__(parent)\n        self._left_side = left_side\n        self._line_size = line_size\n        self._color = color\n        self._radius = 0\n\n    def set_radius(self, radius):\n        self._radius = radius\n\n    def set_line_size(self, line_size):\n        self._line_size = line_size\n\n    def paintEvent(self, event):\n        if not self.isVisible():\n            return\n\n        pos_y = self.height() * 0.5\n        x_offset = self._line_size * 0.5\n        if self._left_side:\n            rect = QtCore.QRectF(\n                x_offset,\n                pos_y,\n                self.width() + self._radius + x_offset,\n                self.height()\n            )\n        else:\n            rect = QtCore.QRectF(\n                (-self._radius),\n                pos_y,\n                (self.width() + self._radius) - x_offset,\n                self.height()\n            )\n\n        painter = QtGui.QPainter(self)\n        painter.setRenderHints(\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n        )\n        if self._color:\n            pen = QtGui.QPen(self._color)\n        else:\n            pen = painter.pen()\n        pen.setWidth(self._line_size)\n        painter.setPen(pen)\n        painter.setBrush(QtCore.Qt.transparent)\n        if self._radius:\n            painter.drawRoundedRect(rect, self._radius, self._radius)\n        else:\n            painter.drawRect(rect)\n        painter.end()\n\n\nclass BorderedLabelWidget(QtWidgets.QFrame):\n    \"\"\"Draws borders around widget with label in the middle of top.\n\n    ┌─────── Label ────────┐\n    │                      │\n    │                      │\n    │       CONTENT        │\n    │                      │\n    │                      │\n    └──────────────────────┘\n    \"\"\"\n    def __init__(self, label, parent):\n        super(BorderedLabelWidget, self).__init__(parent)\n        color_value = get_objected_colors(\"border\")\n        color = None\n        if color_value:\n            color = color_value.get_qcolor()\n\n        line_size = 1\n\n        top_left_w = _HTopCornerLineWidget(color, line_size, True, self)\n        top_right_w = _HTopCornerLineWidget(color, line_size, False, self)\n\n        label_widget = QtWidgets.QLabel(label, self)\n\n        top_layout = QtWidgets.QHBoxLayout()\n        top_layout.setContentsMargins(0, 0, 0, 0)\n        top_layout.setSpacing(5)\n        top_layout.addWidget(top_left_w, 1)\n        top_layout.addWidget(label_widget, 0)\n        top_layout.addWidget(top_right_w, 1)\n\n        left_w = _VLineWidget(color, line_size, True, self)\n        right_w = _VLineWidget(color, line_size, False, self)\n\n        bottom_w = _HBottomLineWidget(color, line_size, self)\n\n        center_layout = QtWidgets.QHBoxLayout()\n        center_layout.setContentsMargins(5, 5, 5, 5)\n\n        layout = QtWidgets.QGridLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(0)\n\n        layout.addLayout(top_layout, 0, 0, 1, 3)\n\n        layout.addWidget(left_w, 1, 0)\n        layout.addLayout(center_layout, 1, 1)\n        layout.addWidget(right_w, 1, 2)\n\n        layout.addWidget(bottom_w, 2, 0, 1, 3)\n\n        layout.setColumnStretch(1, 1)\n        layout.setRowStretch(1, 1)\n\n        self._widget = None\n\n        self._radius = 0\n        self._line_size = line_size\n\n        self._top_left_w = top_left_w\n        self._top_right_w = top_right_w\n        self._left_w = left_w\n        self._right_w = right_w\n        self._bottom_w = bottom_w\n        self._label_widget = label_widget\n        self._center_layout = center_layout\n\n    def set_content_margins(self, value):\n        \"\"\"Set margins around content.\"\"\"\n        self._center_layout.setContentsMargins(\n            value, value, value, value\n        )\n\n    def set_line_size(self, line_size):\n        if self._line_size == line_size:\n            return\n        self._line_size = line_size\n        for widget in (\n            self._top_left_w,\n            self._top_right_w,\n            self._left_w,\n            self._right_w,\n            self._bottom_w\n        ):\n            widget.set_line_size(line_size)\n        self._recalculate_sizes()\n\n    def showEvent(self, event):\n        super(BorderedLabelWidget, self).showEvent(event)\n        self._recalculate_sizes()\n\n    def _recalculate_sizes(self):\n        height = self._label_widget.height()\n        radius = int((height + (height % 2)) / 2)\n        self._radius = radius\n\n        radius_size = self._line_size + 1\n        if radius_size < radius:\n            radius_size = radius\n\n        if radius:\n            side_width = self._line_size + radius\n        else:\n            side_width = self._line_size + 1\n\n        # Don't use fixed width/height as that would set also set\n        #   the other size (When fixed width is set then is also set\n        #   fixed height).\n        self._left_w.setMinimumWidth(side_width)\n        self._left_w.setMaximumWidth(side_width)\n        self._right_w.setMinimumWidth(side_width)\n        self._right_w.setMaximumWidth(side_width)\n        self._bottom_w.setMinimumHeight(radius_size)\n        self._bottom_w.setMaximumHeight(radius_size)\n        self._bottom_w.set_radius(radius)\n        self._top_right_w.set_radius(radius)\n        self._top_left_w.set_radius(radius)\n        if self._widget:\n            self._widget.update()\n\n    def set_center_widget(self, widget):\n        \"\"\"Set content widget and add it to center.\"\"\"\n        while self._center_layout.count():\n            item = self._center_layout.takeAt(0)\n            widget = item.widget()\n            if widget:\n                widget.deleteLater()\n\n        self._widget = widget\n        if isinstance(widget, QtWidgets.QLayout):\n            self._center_layout.addLayout(widget)\n        else:\n            self._center_layout.addWidget(widget)\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/card_view_widgets.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Card view instance with more information about each instance.\n\nInstances are grouped under groups. Groups are defined by `creator_label`\nattribute on instance (Group defined by creator).\n\nOnly one item can be selected at a time.\n\n```\n<i> : Icon. Can have Warning icon when context is not right\n┌──────────────────────┐\n│  Context             │\n│ <Group 1> ────────── │\n│ <i> <Instance 1>  [x]│\n│ <i> <Instance 2>  [x]│\n│ <Group 2> ────────── │\n│ <i> <Instance 3>  [x]│\n│ ...                  │\n└──────────────────────┘\n```\n\"\"\"\n\nimport re\nimport collections\n\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.widgets.nice_checkbox import NiceCheckbox\n\nfrom openpype.tools.utils import BaseClickableFrame\nfrom openpype.tools.utils.lib import html_escape\nfrom .widgets import (\n    AbstractInstanceView,\n    ContextWarningLabel,\n    IconValuePixmapLabel,\n    PublishPixmapLabel\n)\nfrom ..constants import (\n    CONTEXT_ID,\n    CONTEXT_LABEL,\n    CONTEXT_GROUP,\n    CONVERTOR_ITEM_GROUP,\n)\n\n\nclass SelectionTypes:\n    clear = \"clear\"\n    extend = \"extend\"\n    extend_to = \"extend_to\"\n\n\nclass BaseGroupWidget(QtWidgets.QWidget):\n    selected = QtCore.Signal(str, str, str)\n    removed_selected = QtCore.Signal()\n\n    def __init__(self, group_name, parent):\n        super(BaseGroupWidget, self).__init__(parent)\n\n        label_widget = QtWidgets.QLabel(group_name, self)\n\n        line_widget = QtWidgets.QWidget(self)\n        line_widget.setObjectName(\"Separator\")\n        line_widget.setMinimumHeight(2)\n        line_widget.setMaximumHeight(2)\n\n        label_layout = QtWidgets.QHBoxLayout()\n        label_layout.setAlignment(QtCore.Qt.AlignVCenter)\n        label_layout.setSpacing(10)\n        label_layout.setContentsMargins(0, 0, 0, 0)\n        label_layout.addWidget(label_widget, 0)\n        label_layout.addWidget(line_widget, 1)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addLayout(label_layout, 0)\n\n        self._group = group_name\n\n        self._widgets_by_id = {}\n        self._ordered_item_ids = []\n\n        self._label_widget = label_widget\n        self._content_layout = layout\n\n    @property\n    def group_name(self):\n        \"\"\"Group which widget represent.\n\n        Returns:\n            str: Name of group.\n        \"\"\"\n\n        return self._group\n\n    def get_widget_by_item_id(self, item_id):\n        \"\"\"Get instance widget by its id.\"\"\"\n\n        return self._widgets_by_id.get(item_id)\n\n    def get_selected_item_ids(self):\n        \"\"\"Selected instance ids.\n\n        Returns:\n            Set[str]: Instance ids that are selected.\n        \"\"\"\n\n        return {\n            instance_id\n            for instance_id, widget in self._widgets_by_id.items()\n            if widget.is_selected\n        }\n\n    def get_selected_widgets(self):\n        \"\"\"Access to widgets marked as selected.\n\n        Returns:\n            List[InstanceCardWidget]: Instance widgets that are selected.\n        \"\"\"\n\n        return [\n            widget\n            for instance_id, widget in self._widgets_by_id.items()\n            if widget.is_selected\n        ]\n\n    def get_ordered_widgets(self):\n        \"\"\"Get instance ids in order as are shown in ui.\n\n        Returns:\n            List[str]: Instance ids.\n        \"\"\"\n\n        return [\n            self._widgets_by_id[instance_id]\n            for instance_id in self._ordered_item_ids\n        ]\n\n    def _remove_all_except(self, item_ids):\n        item_ids = set(item_ids)\n        # Remove instance widgets that are not in passed instances\n        for item_id in tuple(self._widgets_by_id.keys()):\n            if item_id in item_ids:\n                continue\n\n            widget = self._widgets_by_id.pop(item_id)\n            if widget.is_selected:\n                self.removed_selected.emit()\n\n            widget.setVisible(False)\n            self._content_layout.removeWidget(widget)\n            widget.deleteLater()\n\n    def _update_ordered_item_ids(self):\n        ordered_item_ids = []\n        for idx in range(self._content_layout.count()):\n            if idx > 0:\n                item = self._content_layout.itemAt(idx)\n                widget = item.widget()\n                if widget is not None:\n                    ordered_item_ids.append(widget.id)\n\n        self._ordered_item_ids = ordered_item_ids\n\n    def _on_widget_selection(self, instance_id, group_id, selection_type):\n        self.selected.emit(instance_id, group_id, selection_type)\n\n    def set_active_toggle_enabled(self, enabled):\n        for widget in self._widgets_by_id.values():\n            if isinstance(widget, InstanceCardWidget):\n                widget.set_active_toggle_enabled(enabled)\n\n\nclass ConvertorItemsGroupWidget(BaseGroupWidget):\n    def update_items(self, items_by_id):\n        items_by_label = collections.defaultdict(list)\n        for item in items_by_id.values():\n            items_by_label[item.label].append(item)\n\n        # Remove instance widgets that are not in passed instances\n        self._remove_all_except(items_by_id.keys())\n\n        # Sort instances by subset name\n        sorted_labels = list(sorted(items_by_label.keys()))\n\n        # Add new instances to widget\n        widget_idx = 1\n        for label in sorted_labels:\n            for item in items_by_label[label]:\n                if item.id in self._widgets_by_id:\n                    widget = self._widgets_by_id[item.id]\n                    widget.update_item(item)\n                else:\n                    widget = ConvertorItemCardWidget(item, self)\n                    widget.selected.connect(self._on_widget_selection)\n                    self._widgets_by_id[item.id] = widget\n                    self._content_layout.insertWidget(widget_idx, widget)\n                widget_idx += 1\n\n        self._update_ordered_item_ids()\n\n\nclass InstanceGroupWidget(BaseGroupWidget):\n    \"\"\"Widget wrapping instances under group.\"\"\"\n\n    active_changed = QtCore.Signal(str, str, bool)\n\n    def __init__(self, group_icons, *args, **kwargs):\n        super(InstanceGroupWidget, self).__init__(*args, **kwargs)\n\n        self._group_icons = group_icons\n\n    def update_icons(self, group_icons):\n        self._group_icons = group_icons\n\n    def update_instance_values(self):\n        \"\"\"Trigger update on instance widgets.\"\"\"\n\n        for widget in self._widgets_by_id.values():\n            widget.update_instance_values()\n\n    def update_instances(self, instances):\n        \"\"\"Update instances for the group.\n\n        Args:\n            instances(list<CreatedInstance>): List of instances in\n                CreateContext.\n        \"\"\"\n\n        # Store instances by id and by subset name\n        instances_by_id = {}\n        instances_by_subset_name = collections.defaultdict(list)\n        for instance in instances:\n            instances_by_id[instance.id] = instance\n            subset_name = instance[\"subset\"]\n            instances_by_subset_name[subset_name].append(instance)\n\n        # Remove instance widgets that are not in passed instances\n        self._remove_all_except(instances_by_id.keys())\n\n        # Sort instances by subset name\n        sorted_subset_names = list(sorted(instances_by_subset_name.keys()))\n\n        # Add new instances to widget\n        widget_idx = 1\n        for subset_names in sorted_subset_names:\n            for instance in instances_by_subset_name[subset_names]:\n                if instance.id in self._widgets_by_id:\n                    widget = self._widgets_by_id[instance.id]\n                    widget.update_instance(instance)\n                else:\n                    group_icon = self._group_icons[instance.creator_identifier]\n                    widget = InstanceCardWidget(\n                        instance, group_icon, self\n                    )\n                    widget.selected.connect(self._on_widget_selection)\n                    widget.active_changed.connect(self._on_active_changed)\n                    self._widgets_by_id[instance.id] = widget\n                    self._content_layout.insertWidget(widget_idx, widget)\n                widget_idx += 1\n\n        self._update_ordered_item_ids()\n\n    def _on_active_changed(self, instance_id, value):\n        self.active_changed.emit(self.group_name, instance_id, value)\n\n\nclass CardWidget(BaseClickableFrame):\n    \"\"\"Clickable card used as bigger button.\"\"\"\n\n    selected = QtCore.Signal(str, str, str)\n    # Group identifier of card\n    # - this must be set because if send when mouse is released with card id\n    _group_identifier = None\n\n    def __init__(self, parent):\n        super(CardWidget, self).__init__(parent)\n        self.setObjectName(\"CardViewWidget\")\n\n        self._selected = False\n        self._id = None\n\n    @property\n    def id(self):\n        \"\"\"Id of card.\"\"\"\n\n        return self._id\n\n    @property\n    def is_selected(self):\n        \"\"\"Is card selected.\"\"\"\n        return self._selected\n\n    def set_selected(self, selected):\n        \"\"\"Set card as selected.\"\"\"\n        if selected == self._selected:\n            return\n        self._selected = selected\n        state = \"selected\" if selected else \"\"\n        self.setProperty(\"state\", state)\n        self.style().polish(self)\n\n    def _mouse_release_callback(self):\n        \"\"\"Trigger selected signal.\"\"\"\n\n        modifiers = QtWidgets.QApplication.keyboardModifiers()\n        selection_type = SelectionTypes.clear\n        if bool(modifiers & QtCore.Qt.ShiftModifier):\n            selection_type = SelectionTypes.extend_to\n\n        elif bool(modifiers & QtCore.Qt.ControlModifier):\n            selection_type = SelectionTypes.extend\n\n        self.selected.emit(self._id, self._group_identifier, selection_type)\n\n\nclass ContextCardWidget(CardWidget):\n    \"\"\"Card for global context.\n\n    Is not visually under group widget and is always at the top of card view.\n    \"\"\"\n\n    def __init__(self, parent):\n        super(ContextCardWidget, self).__init__(parent)\n\n        self._id = CONTEXT_ID\n        self._group_identifier = CONTEXT_GROUP\n\n        icon_widget = PublishPixmapLabel(None, self)\n        icon_widget.setObjectName(\"FamilyIconLabel\")\n\n        label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self)\n\n        icon_layout = QtWidgets.QHBoxLayout()\n        icon_layout.setContentsMargins(5, 5, 5, 5)\n        icon_layout.addWidget(icon_widget)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 2, 10, 2)\n        layout.addLayout(icon_layout, 0)\n        layout.addWidget(label_widget, 1)\n\n        self._icon_widget = icon_widget\n        self._label_widget = label_widget\n\n\nclass ConvertorItemCardWidget(CardWidget):\n    \"\"\"Card for global context.\n\n    Is not visually under group widget and is always at the top of card view.\n    \"\"\"\n\n    def __init__(self, item, parent):\n        super(ConvertorItemCardWidget, self).__init__(parent)\n\n        self._id = item.id\n        self.identifier = item.identifier\n        self._group_identifier = CONVERTOR_ITEM_GROUP\n\n        icon_widget = IconValuePixmapLabel(\"fa.magic\", self)\n        icon_widget.setObjectName(\"FamilyIconLabel\")\n\n        label_widget = QtWidgets.QLabel(item.label, self)\n\n        icon_layout = QtWidgets.QHBoxLayout()\n        icon_layout.setContentsMargins(10, 5, 5, 5)\n        icon_layout.addWidget(icon_widget)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 2, 10, 2)\n        layout.addLayout(icon_layout, 0)\n        layout.addWidget(label_widget, 1)\n\n        self._icon_widget = icon_widget\n        self._label_widget = label_widget\n\n    def update_instance_values(self):\n        pass\n\n\nclass InstanceCardWidget(CardWidget):\n    \"\"\"Card widget representing instance.\"\"\"\n\n    active_changed = QtCore.Signal(str, bool)\n\n    def __init__(self, instance, group_icon, parent):\n        super(InstanceCardWidget, self).__init__(parent)\n\n        self._id = instance.id\n        self._group_identifier = instance.group_label\n        self._group_icon = group_icon\n\n        self.instance = instance\n\n        self._last_subset_name = None\n        self._last_variant = None\n        self._last_label = None\n\n        icon_widget = IconValuePixmapLabel(group_icon, self)\n        icon_widget.setObjectName(\"FamilyIconLabel\")\n        context_warning = ContextWarningLabel(self)\n\n        icon_layout = QtWidgets.QHBoxLayout()\n        icon_layout.setContentsMargins(10, 5, 5, 5)\n        icon_layout.addWidget(icon_widget)\n        icon_layout.addWidget(context_warning)\n\n        label_widget = QtWidgets.QLabel(self)\n        active_checkbox = NiceCheckbox(parent=self)\n\n        expand_btn = QtWidgets.QToolButton(self)\n        # Not yet implemented\n        expand_btn.setVisible(False)\n        expand_btn.setObjectName(\"ArrowBtn\")\n        expand_btn.setArrowType(QtCore.Qt.DownArrow)\n        expand_btn.setMaximumWidth(14)\n        expand_btn.setEnabled(False)\n\n        detail_widget = QtWidgets.QWidget(self)\n        detail_widget.setVisible(False)\n        self.detail_widget = detail_widget\n\n        top_layout = QtWidgets.QHBoxLayout()\n        top_layout.addLayout(icon_layout, 0)\n        top_layout.addWidget(label_widget, 1)\n        top_layout.addWidget(context_warning, 0)\n        top_layout.addWidget(active_checkbox, 0)\n        top_layout.addWidget(expand_btn, 0)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 2, 10, 2)\n        layout.addLayout(top_layout)\n        layout.addWidget(detail_widget)\n\n        active_checkbox.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        expand_btn.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        active_checkbox.stateChanged.connect(self._on_active_change)\n        expand_btn.clicked.connect(self._on_expend_clicked)\n\n        self._icon_widget = icon_widget\n        self._label_widget = label_widget\n        self._context_warning = context_warning\n        self._active_checkbox = active_checkbox\n        self._expand_btn = expand_btn\n\n        self.update_instance_values()\n\n    def set_active_toggle_enabled(self, enabled):\n        self._active_checkbox.setEnabled(enabled)\n\n    @property\n    def is_active(self):\n        return self._active_checkbox.isChecked()\n\n    def set_active(self, new_value):\n        \"\"\"Set instance as active.\"\"\"\n        checkbox_value = self._active_checkbox.isChecked()\n        instance_value = self.instance[\"active\"]\n\n        # First change instance value and them change checkbox\n        # - prevent to trigger `active_changed` signal\n        if instance_value != new_value:\n            self.instance[\"active\"] = new_value\n\n        if checkbox_value != new_value:\n            self._active_checkbox.setChecked(new_value)\n\n    def update_instance(self, instance):\n        \"\"\"Update instance object and update UI.\"\"\"\n        self.instance = instance\n        self.update_instance_values()\n\n    def _validate_context(self):\n        valid = self.instance.has_valid_context\n        self._icon_widget.setVisible(valid)\n        self._context_warning.setVisible(not valid)\n\n    def _update_subset_name(self):\n        variant = self.instance[\"variant\"]\n        subset_name = self.instance[\"subset\"]\n        label = self.instance.label\n        if (\n            variant == self._last_variant\n            and subset_name == self._last_subset_name\n            and label == self._last_label\n        ):\n            return\n\n        self._last_variant = variant\n        self._last_subset_name = subset_name\n        self._last_label = label\n        # Make `variant` bold\n        label = html_escape(self.instance.label)\n        found_parts = set(re.findall(variant, label, re.IGNORECASE))\n        if found_parts:\n            for part in found_parts:\n                replacement = \"<b>{}</b>\".format(part)\n                label = label.replace(part, replacement)\n\n        self._label_widget.setText(label)\n        # HTML text will cause that label start catch mouse clicks\n        # - disabling with changing interaction flag\n        self._label_widget.setTextInteractionFlags(\n            QtCore.Qt.NoTextInteraction\n        )\n\n    def update_instance_values(self):\n        \"\"\"Update instance data\"\"\"\n        self._update_subset_name()\n        self.set_active(self.instance[\"active\"])\n        self._validate_context()\n\n    def _set_expanded(self, expanded=None):\n        if expanded is None:\n            expanded = not self.detail_widget.isVisible()\n        self.detail_widget.setVisible(expanded)\n\n    def _on_active_change(self):\n        new_value = self._active_checkbox.isChecked()\n        old_value = self.instance[\"active\"]\n        if new_value == old_value:\n            return\n\n        self.instance[\"active\"] = new_value\n        self.active_changed.emit(self._id, new_value)\n\n    def _on_expend_clicked(self):\n        self._set_expanded()\n\n\nclass InstanceCardView(AbstractInstanceView):\n    \"\"\"Publish access to card view.\n\n    Wrapper of all widgets in card view.\n    \"\"\"\n\n    def __init__(self, controller, parent):\n        super(InstanceCardView, self).__init__(parent)\n\n        self._controller = controller\n\n        scroll_area = QtWidgets.QScrollArea(self)\n        scroll_area.setWidgetResizable(True)\n        scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n        scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)\n        scrollbar_bg = scroll_area.verticalScrollBar().parent()\n        if scrollbar_bg:\n            scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        scroll_area.setViewportMargins(0, 0, 0, 0)\n\n        content_widget = QtWidgets.QWidget(scroll_area)\n\n        scroll_area.setWidget(content_widget)\n\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.setContentsMargins(0, 0, 0, 0)\n        content_layout.addStretch(1)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(scroll_area)\n\n        self._scroll_area = scroll_area\n        self._content_layout = content_layout\n        self._content_widget = content_widget\n\n        self._context_widget = None\n        self._convertor_items_group = None\n        self._active_toggle_enabled = True\n        self._widgets_by_group = {}\n        self._ordered_groups = []\n\n        self._explicitly_selected_instance_ids = []\n        self._explicitly_selected_groups = []\n\n        self.setSizePolicy(\n            QtWidgets.QSizePolicy.Minimum,\n            self.sizePolicy().verticalPolicy()\n        )\n\n    def sizeHint(self):\n        \"\"\"Modify sizeHint based on visibility of scroll bars.\"\"\"\n        # Calculate width hint by content widget and vertical scroll bar\n        scroll_bar = self._scroll_area.verticalScrollBar()\n        width = (\n            self._content_widget.sizeHint().width()\n            + scroll_bar.sizeHint().width()\n        )\n\n        result = super(InstanceCardView, self).sizeHint()\n        result.setWidth(width)\n        return result\n\n    def _toggle_instances(self, value):\n        if not self._active_toggle_enabled:\n            return\n\n        widgets = self._get_selected_widgets()\n        changed = False\n        for widget in widgets:\n            if not isinstance(widget, InstanceCardWidget):\n                continue\n\n            is_active = widget.is_active\n            if value == -1:\n                widget.set_active(not is_active)\n                changed = True\n                continue\n\n            _value = bool(value)\n            if is_active is not _value:\n                widget.set_active(_value)\n                changed = True\n\n        if changed:\n            self.active_changed.emit()\n\n    def keyPressEvent(self, event):\n        if event.key() == QtCore.Qt.Key_Space:\n            self._toggle_instances(-1)\n            return True\n\n        elif event.key() == QtCore.Qt.Key_Backspace:\n            self._toggle_instances(0)\n            return True\n\n        elif event.key() == QtCore.Qt.Key_Return:\n            self._toggle_instances(1)\n            return True\n\n        return super(InstanceCardView, self).keyPressEvent(event)\n\n    def _get_selected_widgets(self):\n        output = []\n        if (\n            self._context_widget is not None\n            and self._context_widget.is_selected\n        ):\n            output.append(self._context_widget)\n\n        if self._convertor_items_group is not None:\n            output.extend(self._convertor_items_group.get_selected_widgets())\n\n        for group_widget in self._widgets_by_group.values():\n            for widget in group_widget.get_selected_widgets():\n                output.append(widget)\n        return output\n\n    def _get_selected_instance_ids(self):\n        output = []\n        if (\n            self._context_widget is not None\n            and self._context_widget.is_selected\n        ):\n            output.append(CONTEXT_ID)\n\n        if self._convertor_items_group is not None:\n            output.extend(self._convertor_items_group.get_selected_item_ids())\n\n        for group_widget in self._widgets_by_group.values():\n            output.extend(group_widget.get_selected_item_ids())\n        return output\n\n    def refresh(self):\n        \"\"\"Refresh instances in view based on CreatedContext.\"\"\"\n\n        self._make_sure_context_widget_exists()\n\n        self._update_convertor_items_group()\n\n        # Prepare instances by group and identifiers by group\n        instances_by_group = collections.defaultdict(list)\n        identifiers_by_group = collections.defaultdict(set)\n        for instance in self._controller.instances.values():\n            group_name = instance.group_label\n            instances_by_group[group_name].append(instance)\n            identifiers_by_group[group_name].add(\n                instance.creator_identifier\n            )\n\n        # Remove groups that were not found in apassed instances\n        for group_name in tuple(self._widgets_by_group.keys()):\n            if group_name in instances_by_group:\n                continue\n\n            widget = self._widgets_by_group.pop(group_name)\n            widget.setVisible(False)\n            self._content_layout.removeWidget(widget)\n            widget.deleteLater()\n\n            if group_name in self._explicitly_selected_groups:\n                self._explicitly_selected_groups.remove(group_name)\n\n        # Sort groups\n        sorted_group_names = list(sorted(instances_by_group.keys()))\n\n        # Keep track of widget indexes\n        # - we start with 1 because Context item as at the top\n        widget_idx = 1\n        if self._convertor_items_group is not None:\n            widget_idx += 1\n\n        for group_name in sorted_group_names:\n            group_icons = {\n                identifier: self._controller.get_creator_icon(identifier)\n                for identifier in identifiers_by_group[group_name]\n            }\n            if group_name in self._widgets_by_group:\n                group_widget = self._widgets_by_group[group_name]\n                group_widget.update_icons(group_icons)\n\n            else:\n                group_widget = InstanceGroupWidget(\n                    group_icons, group_name, self._content_widget\n                )\n                group_widget.active_changed.connect(self._on_active_changed)\n                group_widget.selected.connect(self._on_widget_selection)\n                self._content_layout.insertWidget(widget_idx, group_widget)\n                self._widgets_by_group[group_name] = group_widget\n\n            widget_idx += 1\n            group_widget.update_instances(\n                instances_by_group[group_name]\n            )\n            group_widget.set_active_toggle_enabled(\n                self._active_toggle_enabled\n            )\n\n        self._update_ordered_group_names()\n\n    def has_items(self):\n        if self._convertor_items_group is not None:\n            return True\n        if self._widgets_by_group:\n            return True\n        return False\n\n    def _update_ordered_group_names(self):\n        ordered_group_names = [CONTEXT_GROUP]\n        for idx in range(self._content_layout.count()):\n            if idx > 0:\n                item = self._content_layout.itemAt(idx)\n                group_widget = item.widget()\n                if group_widget is not None:\n                    ordered_group_names.append(group_widget.group_name)\n\n        self._ordered_groups = ordered_group_names\n\n    def _make_sure_context_widget_exists(self):\n        # Create context item if is not already existing\n        # - this must be as first thing to do as context item should be at the\n        #   top\n        if self._context_widget is not None:\n            return\n\n        widget = ContextCardWidget(self._content_widget)\n        widget.selected.connect(self._on_widget_selection)\n\n        self._context_widget = widget\n\n        self.selection_changed.emit()\n        self._content_layout.insertWidget(0, widget)\n\n    def _update_convertor_items_group(self):\n        convertor_items = self._controller.convertor_items\n        if not convertor_items and self._convertor_items_group is None:\n            return\n\n        if not convertor_items:\n            self._convertor_items_group.setVisible(False)\n            self._content_layout.removeWidget(self._convertor_items_group)\n            self._convertor_items_group.deleteLater()\n            self._convertor_items_group = None\n            return\n\n        if self._convertor_items_group is None:\n            group_widget = ConvertorItemsGroupWidget(\n                CONVERTOR_ITEM_GROUP, self._content_widget\n            )\n            group_widget.selected.connect(self._on_widget_selection)\n            self._content_layout.insertWidget(1, group_widget)\n            self._convertor_items_group = group_widget\n\n        self._convertor_items_group.update_items(convertor_items)\n\n    def refresh_instance_states(self):\n        \"\"\"Trigger update of instances on group widgets.\"\"\"\n        for widget in self._widgets_by_group.values():\n            widget.update_instance_values()\n\n    def _on_active_changed(self, group_name, instance_id, value):\n        group_widget = self._widgets_by_group[group_name]\n        instance_widget = group_widget.get_widget_by_item_id(instance_id)\n        if instance_widget.is_selected:\n            for widget in self._get_selected_widgets():\n                if isinstance(widget, InstanceCardWidget):\n                    widget.set_active(value)\n        else:\n            self._select_item_clear(instance_id, group_name, instance_widget)\n            self.selection_changed.emit()\n        self.active_changed.emit()\n\n    def _on_widget_selection(self, instance_id, group_name, selection_type):\n        \"\"\"Select specific item by instance id.\n\n        Pass `CONTEXT_ID` as instance id and empty string as group to select\n        global context item.\n        \"\"\"\n        if instance_id == CONTEXT_ID:\n            new_widget = self._context_widget\n\n        else:\n            if group_name == CONVERTOR_ITEM_GROUP:\n                group_widget = self._convertor_items_group\n            else:\n                group_widget = self._widgets_by_group[group_name]\n            new_widget = group_widget.get_widget_by_item_id(instance_id)\n\n        if selection_type == SelectionTypes.clear:\n            self._select_item_clear(instance_id, group_name, new_widget)\n        elif selection_type == SelectionTypes.extend:\n            self._select_item_extend(instance_id, group_name, new_widget)\n        elif selection_type == SelectionTypes.extend_to:\n            self._select_item_extend_to(instance_id, group_name, new_widget)\n\n        self.selection_changed.emit()\n\n    def _select_item_clear(self, instance_id, group_name, new_widget):\n        \"\"\"Select specific item by instance id and clear previous selection.\n\n        Pass `CONTEXT_ID` as instance id and empty string as group to select\n        global context item.\n        \"\"\"\n\n        selected_widgets = self._get_selected_widgets()\n        for widget in selected_widgets:\n            if widget.id != instance_id:\n                widget.set_selected(False)\n\n        self._explicitly_selected_groups = [group_name]\n        self._explicitly_selected_instance_ids = [instance_id]\n\n        if new_widget is not None:\n            new_widget.set_selected(True)\n\n    def _select_item_extend(self, instance_id, group_name, new_widget):\n        \"\"\"Add/Remove single item to/from current selection.\n\n        If item is already selected the selection is removed.\n        \"\"\"\n\n        self._explicitly_selected_instance_ids = (\n            self._get_selected_instance_ids()\n        )\n        if new_widget.is_selected:\n            self._explicitly_selected_instance_ids.remove(instance_id)\n            new_widget.set_selected(False)\n            remove_group = False\n            if instance_id == CONTEXT_ID:\n                remove_group = True\n            else:\n                if group_name == CONVERTOR_ITEM_GROUP:\n                    group_widget = self._convertor_items_group\n                else:\n                    group_widget = self._widgets_by_group[group_name]\n                if not group_widget.get_selected_widgets():\n                    remove_group = True\n\n            if remove_group:\n                self._explicitly_selected_groups.remove(group_name)\n            return\n\n        self._explicitly_selected_instance_ids.append(instance_id)\n        if group_name in self._explicitly_selected_groups:\n            self._explicitly_selected_groups.remove(group_name)\n        self._explicitly_selected_groups.append(group_name)\n        new_widget.set_selected(True)\n\n    def _select_item_extend_to(self, instance_id, group_name, new_widget):\n        \"\"\"Extend selected items to specific instance id.\n\n        This method is handling Shift+click selection of widgets. Selection\n        is not stored to explicit selection items. That's because user can\n        shift select again and it should use last explicit selected item as\n        source item for selection.\n\n        Items selected via this function can get to explicit selection only if\n        selection is extended by one specific item ('_select_item_extend').\n        From that moment the selection is locked to new last explicit selected\n        item.\n\n        It's required to traverse through group widgets in their UI order and\n        through their instances in UI order. All explicitly selected items\n        must not change their selection state during this function. Passed\n        instance id can be above or under last selected item so a start item\n        and end item must be found to be able know which direction is selection\n        happening.\n        \"\"\"\n\n        # Start group name (in '_ordered_groups')\n        start_group = None\n        # End group name (in '_ordered_groups')\n        end_group = None\n        # Instance id of first selected item\n        start_instance_id = None\n        # Instance id of last selected item\n        end_instance_id = None\n\n        # Get previously selected group by explicit selected groups\n        previous_group = None\n        if self._explicitly_selected_groups:\n            previous_group = self._explicitly_selected_groups[-1]\n\n        # Find last explicitly selected instance id\n        previous_last_selected_id = None\n        if self._explicitly_selected_instance_ids:\n            previous_last_selected_id = (\n                self._explicitly_selected_instance_ids[-1]\n            )\n\n        # If last instance id was not found or available then last selected\n        #   group is also invalid.\n        # NOTE: This probably never happen?\n        if previous_last_selected_id is None:\n            previous_group = None\n\n        # Check if previously selected group is available and find out if\n        #   new instance group is above or under previous selection\n        # - based on these information are start/end group/instance filled\n        if previous_group in self._ordered_groups:\n            new_idx = self._ordered_groups.index(group_name)\n            prev_idx = self._ordered_groups.index(previous_group)\n            if new_idx < prev_idx:\n                start_group = group_name\n                end_group = previous_group\n                start_instance_id = instance_id\n                end_instance_id = previous_last_selected_id\n            else:\n                start_group = previous_group\n                end_group = group_name\n                start_instance_id = previous_last_selected_id\n                end_instance_id = instance_id\n\n        # If start group is not set then use context item group name\n        if start_group is None:\n            start_group = CONTEXT_GROUP\n\n        # If start instance id is not filled then use context id (similar to\n        #   group)\n        if start_instance_id is None:\n            start_instance_id = CONTEXT_ID\n\n        # If end group is not defined then use passed group name\n        #   - this can be happen when previous group was not selected\n        #   - when this happens the selection will probably happen from context\n        #       item to item selected by user\n        if end_group is None:\n            end_group = group_name\n\n        # If end instance is not filled then use instance selected by user\n        if end_instance_id is None:\n            end_instance_id = instance_id\n\n        # Start and end group are the same\n        # - a different logic is needed in that case\n        same_group = start_group == end_group\n\n        # Process known information and change selection of items\n        passed_start_group = False\n        passed_end_group = False\n        # Go through ordered groups (from top to bottom) and change selection\n        for name in self._ordered_groups:\n            # Prepare sorted instance widgets\n            if name == CONTEXT_GROUP:\n                sorted_widgets = [self._context_widget]\n            else:\n                if name == CONVERTOR_ITEM_GROUP:\n                    group_widget = self._convertor_items_group\n                else:\n                    group_widget = self._widgets_by_group[name]\n                sorted_widgets = group_widget.get_ordered_widgets()\n\n            # Change selection based on explicit selection if start group\n            #   was not passed yet\n            if not passed_start_group:\n                if name != start_group:\n                    for widget in sorted_widgets:\n                        widget.set_selected(\n                            widget.id in self._explicitly_selected_instance_ids\n                        )\n                    continue\n\n            # Change selection based on explicit selection if end group\n            #   already passed\n            if passed_end_group:\n                for widget in sorted_widgets:\n                    widget.set_selected(\n                        widget.id in self._explicitly_selected_instance_ids\n                    )\n                continue\n\n            # Start group is already passed and end group was not yet hit\n            if same_group:\n                passed_start_group = True\n                passed_end_group = True\n                passed_start_instance = False\n                passed_end_instance = False\n                for widget in sorted_widgets:\n                    if not passed_start_instance:\n                        if widget.id in (start_instance_id, end_instance_id):\n                            if widget.id != start_instance_id:\n                                # Swap start/end instance if start instance is\n                                #   after end\n                                # - fix 'passed_end_instance' check\n                                start_instance_id, end_instance_id = (\n                                    end_instance_id, start_instance_id\n                                )\n                            passed_start_instance = True\n\n                    # Find out if widget should be selected\n                    select = False\n                    if passed_end_instance:\n                        select = False\n\n                    elif passed_start_instance:\n                        select = True\n\n                    # Check if instance is in explicitly selected items if\n                    #   should ont be selected\n                    if (\n                        not select\n                        and widget.id in self._explicitly_selected_instance_ids\n                    ):\n                        select = True\n\n                    widget.set_selected(select)\n\n                    if (\n                        not passed_end_instance\n                        and widget.id == end_instance_id\n                    ):\n                        passed_end_instance = True\n\n            elif name == start_group:\n                # First group from which selection should start\n                # - look for start instance first from which the selection\n                #   should happen\n                passed_start_group = True\n                passed_start_instance = False\n                for widget in sorted_widgets:\n                    if widget.id == start_instance_id:\n                        passed_start_instance = True\n\n                    select = False\n                    # Check if passed start instance or instance is\n                    #   in explicitly selected items to be selected\n                    if (\n                        passed_start_instance\n                        or widget.id in self._explicitly_selected_instance_ids\n                    ):\n                        select = True\n                    widget.set_selected(select)\n\n            elif name == end_group:\n                # Last group where selection should happen\n                # - look for end instance first after which the selection\n                #   should stop\n                passed_end_group = True\n                passed_end_instance = False\n                for widget in sorted_widgets:\n                    select = False\n                    # Check if not yet passed end instance or if instance is\n                    #   in explicitly selected items to be selected\n                    if (\n                        not passed_end_instance\n                        or widget.id in self._explicitly_selected_instance_ids\n                    ):\n                        select = True\n\n                    widget.set_selected(select)\n\n                    if widget.id == end_instance_id:\n                        passed_end_instance = True\n\n            else:\n                # Just select everything between start and end group\n                for widget in sorted_widgets:\n                    widget.set_selected(True)\n\n    def get_selected_items(self):\n        \"\"\"Get selected instance ids and context.\"\"\"\n\n        convertor_identifiers = []\n        instances = []\n        selected_widgets = self._get_selected_widgets()\n\n        context_selected = False\n        for widget in selected_widgets:\n            if widget is self._context_widget:\n                context_selected = True\n\n            elif isinstance(widget, InstanceCardWidget):\n                instances.append(widget.id)\n\n            elif isinstance(widget, ConvertorItemCardWidget):\n                convertor_identifiers.append(widget.identifier)\n\n        return instances, context_selected, convertor_identifiers\n\n    def set_selected_items(\n        self, instance_ids, context_selected, convertor_identifiers\n    ):\n        s_instance_ids = set(instance_ids)\n        s_convertor_identifiers = set(convertor_identifiers)\n        cur_ids, cur_context, cur_convertor_identifiers = (\n            self.get_selected_items()\n        )\n        if (\n            set(cur_ids) == s_instance_ids\n            and cur_context == context_selected\n            and set(cur_convertor_identifiers) == s_convertor_identifiers\n        ):\n            return\n\n        selected_groups = []\n        selected_instances = []\n        if context_selected:\n            selected_groups.append(CONTEXT_GROUP)\n            selected_instances.append(CONTEXT_ID)\n\n        self._context_widget.set_selected(context_selected)\n\n        for group_name in self._ordered_groups:\n            if group_name == CONTEXT_GROUP:\n                continue\n\n            is_convertor_group = group_name == CONVERTOR_ITEM_GROUP\n            if is_convertor_group:\n                group_widget = self._convertor_items_group\n            else:\n                group_widget = self._widgets_by_group[group_name]\n\n            group_selected = False\n            for widget in group_widget.get_ordered_widgets():\n                select = False\n                if is_convertor_group:\n                    is_in = widget.identifier in s_convertor_identifiers\n                else:\n                    is_in = widget.id in s_instance_ids\n                if is_in:\n                    selected_instances.append(widget.id)\n                    group_selected = True\n                    select = True\n                widget.set_selected(select)\n\n            if group_selected:\n                selected_groups.append(group_name)\n\n        self._explicitly_selected_groups = selected_groups\n        self._explicitly_selected_instance_ids = selected_instances\n\n    def set_active_toggle_enabled(self, enabled):\n        if self._active_toggle_enabled is enabled:\n            return\n        self._active_toggle_enabled = enabled\n        for group_widget in self._widgets_by_group.values():\n            group_widget.set_active_toggle_enabled(enabled)\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/create_widget.py",
    "content": "import re\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.pipeline.create import (\n    SUBSET_NAME_ALLOWED_SYMBOLS,\n    PRE_CREATE_THUMBNAIL_KEY,\n    DEFAULT_VARIANT_VALUE,\n    TaskNotSetError,\n)\n\nfrom .thumbnail_widget import ThumbnailWidget\nfrom .widgets import (\n    IconValuePixmapLabel,\n    CreateBtn,\n)\nfrom .assets_widget import CreateWidgetAssetsWidget\nfrom .tasks_widget import CreateWidgetTasksWidget\nfrom .precreate_widget import PreCreateWidget\nfrom ..constants import (\n    VARIANT_TOOLTIP,\n    FAMILY_ROLE,\n    CREATOR_IDENTIFIER_ROLE,\n    CREATOR_THUMBNAIL_ENABLED_ROLE,\n    CREATOR_SORT_ROLE,\n    INPUTS_LAYOUT_HSPACING,\n    INPUTS_LAYOUT_VSPACING,\n)\n\nSEPARATORS = (\"---separator---\", \"---\")\n\n\nclass ResizeControlWidget(QtWidgets.QWidget):\n    resized = QtCore.Signal()\n\n    def resizeEvent(self, event):\n        super(ResizeControlWidget, self).resizeEvent(event)\n        self.resized.emit()\n\n\n# TODO add creator identifier/label to details\nclass CreatorShortDescWidget(QtWidgets.QWidget):\n    def __init__(self, parent=None):\n        super(CreatorShortDescWidget, self).__init__(parent=parent)\n\n        # --- Short description widget ---\n        icon_widget = IconValuePixmapLabel(None, self)\n        icon_widget.setObjectName(\"FamilyIconLabel\")\n\n        # --- Short description inputs ---\n        short_desc_input_widget = QtWidgets.QWidget(self)\n\n        family_label = QtWidgets.QLabel(short_desc_input_widget)\n        family_label.setAlignment(\n            QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft\n        )\n\n        description_label = QtWidgets.QLabel(short_desc_input_widget)\n        description_label.setAlignment(\n            QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft\n        )\n\n        short_desc_input_layout = QtWidgets.QVBoxLayout(\n            short_desc_input_widget\n        )\n        short_desc_input_layout.setSpacing(0)\n        short_desc_input_layout.addWidget(family_label)\n        short_desc_input_layout.addWidget(description_label)\n        # --------------------------------\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(icon_widget, 0)\n        layout.addWidget(short_desc_input_widget, 1)\n        # --------------------------------\n\n        self._icon_widget = icon_widget\n        self._family_label = family_label\n        self._description_label = description_label\n\n    def set_creator_item(self, creator_item=None):\n        if not creator_item:\n            self._icon_widget.set_icon_def(None)\n            self._family_label.setText(\"\")\n            self._description_label.setText(\"\")\n            return\n\n        plugin_icon = creator_item.icon\n        description = creator_item.description or \"\"\n\n        self._icon_widget.set_icon_def(plugin_icon)\n        self._family_label.setText(\"<b>{}</b>\".format(creator_item.family))\n        self._family_label.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)\n        self._description_label.setText(description)\n\n\nclass CreatorsProxyModel(QtCore.QSortFilterProxyModel):\n    def lessThan(self, left, right):\n        l_show_order = left.data(CREATOR_SORT_ROLE)\n        r_show_order = right.data(CREATOR_SORT_ROLE)\n        if l_show_order == r_show_order:\n            return super(CreatorsProxyModel, self).lessThan(left, right)\n        return l_show_order < r_show_order\n\n\nclass CreateWidget(QtWidgets.QWidget):\n    def __init__(self, controller, parent=None):\n        super(CreateWidget, self).__init__(parent)\n\n        self._controller = controller\n\n        self._asset_name = None\n        self._subset_names = None\n        self._selected_creator = None\n\n        self._prereq_available = False\n\n        name_pattern = \"^[{}]*$\".format(SUBSET_NAME_ALLOWED_SYMBOLS)\n        self._name_pattern = name_pattern\n        self._compiled_name_pattern = re.compile(name_pattern)\n\n        main_splitter_widget = QtWidgets.QSplitter(self)\n\n        context_widget = QtWidgets.QWidget(main_splitter_widget)\n\n        assets_widget = CreateWidgetAssetsWidget(controller, context_widget)\n        tasks_widget = CreateWidgetTasksWidget(controller, context_widget)\n\n        context_layout = QtWidgets.QVBoxLayout(context_widget)\n        context_layout.setContentsMargins(0, 0, 0, 0)\n        context_layout.setSpacing(0)\n        context_layout.addWidget(assets_widget, 2)\n        context_layout.addWidget(tasks_widget, 1)\n\n        # --- Creators view ---\n        creators_widget = QtWidgets.QWidget(main_splitter_widget)\n\n        creator_short_desc_widget = CreatorShortDescWidget(creators_widget)\n\n        attr_separator_widget = QtWidgets.QWidget(creators_widget)\n        attr_separator_widget.setObjectName(\"Separator\")\n        attr_separator_widget.setMinimumHeight(1)\n        attr_separator_widget.setMaximumHeight(1)\n\n        creators_splitter = QtWidgets.QSplitter(creators_widget)\n\n        creators_view_widget = QtWidgets.QWidget(creators_splitter)\n\n        creator_view_label = QtWidgets.QLabel(\n            \"Choose publish type\", creators_view_widget\n        )\n\n        creators_view = QtWidgets.QListView(creators_view_widget)\n        creators_model = QtGui.QStandardItemModel()\n        creators_sort_model = CreatorsProxyModel()\n        creators_sort_model.setSourceModel(creators_model)\n        creators_view.setModel(creators_sort_model)\n\n        creators_view_layout = QtWidgets.QVBoxLayout(creators_view_widget)\n        creators_view_layout.setContentsMargins(0, 0, 0, 0)\n        creators_view_layout.addWidget(creator_view_label, 0)\n        creators_view_layout.addWidget(creators_view, 1)\n\n        # --- Creator attr defs ---\n        creators_attrs_widget = QtWidgets.QWidget(creators_splitter)\n\n        # Top part - variant / subset name + thumbnail\n        creators_attrs_top = QtWidgets.QWidget(creators_attrs_widget)\n\n        # Basics - variant / subset name\n        creator_basics_widget = ResizeControlWidget(creators_attrs_top)\n\n        variant_subset_label = QtWidgets.QLabel(\n            \"Create options\", creator_basics_widget\n        )\n\n        variant_subset_widget = QtWidgets.QWidget(creator_basics_widget)\n        # Variant and subset input\n        variant_widget = ResizeControlWidget(variant_subset_widget)\n        variant_widget.setObjectName(\"VariantInputsWidget\")\n\n        variant_input = QtWidgets.QLineEdit(variant_widget)\n        variant_input.setObjectName(\"VariantInput\")\n        variant_input.setToolTip(VARIANT_TOOLTIP)\n\n        variant_hints_btn = QtWidgets.QToolButton(variant_widget)\n        variant_hints_btn.setArrowType(QtCore.Qt.DownArrow)\n        variant_hints_btn.setIconSize(QtCore.QSize(12, 12))\n\n        variant_hints_menu = QtWidgets.QMenu(variant_widget)\n        variant_hints_group = QtWidgets.QActionGroup(variant_hints_menu)\n\n        variant_layout = QtWidgets.QHBoxLayout(variant_widget)\n        variant_layout.setContentsMargins(0, 0, 0, 0)\n        variant_layout.setSpacing(0)\n        variant_layout.addWidget(variant_input, 1)\n        variant_layout.addWidget(variant_hints_btn, 0, QtCore.Qt.AlignVCenter)\n\n        subset_name_input = QtWidgets.QLineEdit(variant_subset_widget)\n        subset_name_input.setEnabled(False)\n\n        variant_subset_layout = QtWidgets.QFormLayout(variant_subset_widget)\n        variant_subset_layout.setContentsMargins(0, 0, 0, 0)\n        variant_subset_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING)\n        variant_subset_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING)\n        variant_subset_layout.addRow(\"Variant\", variant_widget)\n        variant_subset_layout.addRow(\n            \"Product\" if AYON_SERVER_ENABLED else \"Subset\",\n            subset_name_input)\n\n        creator_basics_layout = QtWidgets.QVBoxLayout(creator_basics_widget)\n        creator_basics_layout.setContentsMargins(0, 0, 0, 0)\n        creator_basics_layout.addWidget(variant_subset_label, 0)\n        creator_basics_layout.addWidget(variant_subset_widget, 0)\n\n        thumbnail_widget = ThumbnailWidget(controller, creators_attrs_top)\n\n        creators_attrs_top_layout = QtWidgets.QHBoxLayout(creators_attrs_top)\n        creators_attrs_top_layout.setContentsMargins(0, 0, 0, 0)\n        creators_attrs_top_layout.addWidget(creator_basics_widget, 1)\n        creators_attrs_top_layout.addWidget(thumbnail_widget, 0)\n\n        # Precreate attributes widget\n        pre_create_widget = PreCreateWidget(creators_attrs_widget)\n\n        # Create button\n        create_btn_wrapper = QtWidgets.QWidget(creators_attrs_widget)\n        create_btn = CreateBtn(create_btn_wrapper)\n        create_btn.setEnabled(False)\n\n        create_btn_wrap_layout = QtWidgets.QHBoxLayout(create_btn_wrapper)\n        create_btn_wrap_layout.setContentsMargins(0, 0, 0, 0)\n        create_btn_wrap_layout.addStretch(1)\n        create_btn_wrap_layout.addWidget(create_btn, 0)\n\n        creators_attrs_layout = QtWidgets.QVBoxLayout(creators_attrs_widget)\n        creators_attrs_layout.setContentsMargins(0, 0, 0, 0)\n        creators_attrs_layout.addWidget(creators_attrs_top, 0)\n        creators_attrs_layout.addWidget(pre_create_widget, 1)\n        creators_attrs_layout.addWidget(create_btn_wrapper, 0)\n\n        creators_splitter.addWidget(creators_view_widget)\n        creators_splitter.addWidget(creators_attrs_widget)\n        creators_splitter.setStretchFactor(0, 1)\n        creators_splitter.setStretchFactor(1, 2)\n\n        creators_layout = QtWidgets.QVBoxLayout(creators_widget)\n        creators_layout.setContentsMargins(0, 0, 0, 0)\n        creators_layout.addWidget(creator_short_desc_widget, 0)\n        creators_layout.addWidget(attr_separator_widget, 0)\n        creators_layout.addWidget(creators_splitter, 1)\n        # ------------\n\n        # --- Detailed information about creator ---\n        # Detailed description of creator\n        # TODO this has no way how can be showed now\n\n        # -------------------------------------------\n        main_splitter_widget.addWidget(context_widget)\n        main_splitter_widget.addWidget(creators_widget)\n        main_splitter_widget.setStretchFactor(0, 1)\n        main_splitter_widget.setStretchFactor(1, 3)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(main_splitter_widget, 1)\n\n        prereq_timer = QtCore.QTimer()\n        prereq_timer.setInterval(50)\n        prereq_timer.setSingleShot(True)\n\n        prereq_timer.timeout.connect(self._invalidate_prereq)\n\n        create_btn.clicked.connect(self._on_create)\n        variant_widget.resized.connect(self._on_variant_widget_resize)\n        creator_basics_widget.resized.connect(self._on_creator_basics_resize)\n        variant_input.returnPressed.connect(self._on_create)\n        variant_input.textChanged.connect(self._on_variant_change)\n        creators_view.selectionModel().currentChanged.connect(\n            self._on_creator_item_change\n        )\n        variant_hints_btn.clicked.connect(self._on_variant_btn_click)\n        variant_hints_menu.triggered.connect(self._on_variant_action)\n        assets_widget.selection_changed.connect(self._on_asset_change)\n        assets_widget.current_context_required.connect(\n            self._on_current_session_context_request\n        )\n        tasks_widget.task_changed.connect(self._on_task_change)\n        thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)\n        thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear)\n\n        controller.event_system.add_callback(\n            \"main.window.closed\", self._on_main_window_close\n        )\n        controller.event_system.add_callback(\n            \"plugins.refresh.finished\", self._on_plugins_refresh\n        )\n\n        self._main_splitter_widget = main_splitter_widget\n\n        self._creators_splitter = creators_splitter\n\n        self._context_widget = context_widget\n        self._assets_widget = assets_widget\n        self._tasks_widget = tasks_widget\n\n        self.subset_name_input = subset_name_input\n\n        self.variant_input = variant_input\n        self.variant_hints_btn = variant_hints_btn\n        self.variant_hints_menu = variant_hints_menu\n        self.variant_hints_group = variant_hints_group\n\n        self._creators_model = creators_model\n        self._creators_sort_model = creators_sort_model\n        self._creators_view = creators_view\n        self._create_btn = create_btn\n\n        self._creator_short_desc_widget = creator_short_desc_widget\n        self._creator_basics_widget = creator_basics_widget\n        self._thumbnail_widget = thumbnail_widget\n        self._pre_create_widget = pre_create_widget\n        self._attr_separator_widget = attr_separator_widget\n\n        self._prereq_timer = prereq_timer\n        self._first_show = True\n        self._last_thumbnail_path = None\n\n        self._last_current_context_asset = None\n        self._last_current_context_task = None\n        self._use_current_context = True\n\n    @property\n    def current_asset_name(self):\n        return self._controller.current_asset_name\n\n    @property\n    def current_task_name(self):\n        return self._controller.current_task_name\n\n    def _context_change_is_enabled(self):\n        return self._context_widget.isEnabled()\n\n    def _get_asset_name(self):\n        asset_name = None\n        if self._context_change_is_enabled():\n            asset_name = self._assets_widget.get_selected_asset_name()\n\n        if asset_name is None:\n            asset_name = self.current_asset_name\n        return asset_name or None\n\n    def _get_task_name(self):\n        task_name = None\n        if self._context_change_is_enabled():\n            # Don't use selection of task if asset is not set\n            asset_name = self._assets_widget.get_selected_asset_name()\n            if asset_name:\n                task_name = self._tasks_widget.get_selected_task_name()\n\n        if not task_name:\n            task_name = self.current_task_name\n        return task_name\n\n    def _set_context_enabled(self, enabled):\n        self._assets_widget.set_enabled(enabled)\n        self._tasks_widget.set_enabled(enabled)\n        check_prereq = self._context_widget.isEnabled() != enabled\n        self._context_widget.setEnabled(enabled)\n        if check_prereq:\n            self._invalidate_prereq()\n\n    def _on_main_window_close(self):\n        \"\"\"Publisher window was closed.\"\"\"\n\n        # Use current context on next refresh\n        self._use_current_context = True\n\n    def refresh(self):\n        current_asset_name = self._controller.current_asset_name\n        current_task_name = self._controller.current_task_name\n\n        # Get context before refresh to keep selection of asset and\n        #   task widgets\n        asset_name = self._get_asset_name()\n        task_name = self._get_task_name()\n\n        # Replace by current context if last loaded context was\n        #   'current context' before reset\n        if (\n            self._use_current_context\n            or (\n                self._last_current_context_asset\n                and asset_name == self._last_current_context_asset\n                and task_name == self._last_current_context_task\n            )\n        ):\n            asset_name = current_asset_name\n            task_name = current_task_name\n\n        # Store values for future refresh\n        self._last_current_context_asset = current_asset_name\n        self._last_current_context_task = current_task_name\n        self._use_current_context = False\n\n        self._prereq_available = False\n\n        # Disable context widget so refresh of asset will use context asset\n        #   name\n        self._set_context_enabled(False)\n\n        self._assets_widget.refresh()\n\n        # Refresh data before update of creators\n        self._refresh_asset()\n        # Then refresh creators which may trigger callbacks using refreshed\n        #   data\n        self._refresh_creators()\n\n        self._assets_widget.update_current_asset()\n        self._assets_widget.select_asset_by_name(asset_name)\n        self._tasks_widget.set_asset_name(asset_name)\n        self._tasks_widget.select_task_name(task_name)\n\n        self._invalidate_prereq_deffered()\n\n    def _invalidate_prereq_deffered(self):\n        self._prereq_timer.start()\n\n    def _invalidate_prereq(self):\n        prereq_available = True\n        creator_btn_tooltips = []\n\n        available_creators = self._creators_model.rowCount() > 0\n        if available_creators != self._creators_view.isEnabled():\n            self._creators_view.setEnabled(available_creators)\n\n        if not available_creators:\n            prereq_available = False\n            creator_btn_tooltips.append(\"Creator is not selected\")\n\n        if (\n            self._context_change_is_enabled()\n            and self._get_asset_name() is None\n        ):\n            # QUESTION how to handle invalid asset?\n            prereq_available = False\n            creator_btn_tooltips.append(\"Context is not selected\")\n\n        if prereq_available != self._prereq_available:\n            self._prereq_available = prereq_available\n\n            self._create_btn.setEnabled(prereq_available)\n\n            self.variant_input.setEnabled(prereq_available)\n            self.variant_hints_btn.setEnabled(prereq_available)\n\n        tooltip = \"\"\n        if creator_btn_tooltips:\n            tooltip = \"\\n\".join(creator_btn_tooltips)\n        self._create_btn.setToolTip(tooltip)\n\n        self._on_variant_change()\n\n    def _refresh_asset(self):\n        asset_name = self._get_asset_name()\n\n        # Skip if asset did not change\n        if self._asset_name and self._asset_name == asset_name:\n            return\n\n        # Make sure `_asset_name` and `_subset_names` variables are reset\n        self._asset_name = asset_name\n        self._subset_names = None\n        if asset_name is None:\n            return\n\n        subset_names = self._controller.get_existing_subset_names(asset_name)\n\n        self._subset_names = subset_names\n        if subset_names is None:\n            self.subset_name_input.setText(\"< Asset is not set >\")\n\n    def _refresh_creators(self):\n        # Refresh creators and add their families to list\n        existing_items = {}\n        old_creators = set()\n        for row in range(self._creators_model.rowCount()):\n            item = self._creators_model.item(row, 0)\n            identifier = item.data(CREATOR_IDENTIFIER_ROLE)\n            existing_items[identifier] = item\n            old_creators.add(identifier)\n\n        # Add new families\n        new_creators = set()\n        creator_items_by_identifier = self._controller.creator_items\n        for identifier, creator_item in creator_items_by_identifier.items():\n            if creator_item.creator_type != \"artist\":\n                continue\n\n            # TODO add details about creator\n            new_creators.add(identifier)\n            if identifier in existing_items:\n                is_new = False\n                item = existing_items[identifier]\n            else:\n                is_new = True\n                item = QtGui.QStandardItem()\n                item.setFlags(\n                    QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable\n                )\n\n            item.setData(creator_item.label, QtCore.Qt.DisplayRole)\n            item.setData(creator_item.show_order, CREATOR_SORT_ROLE)\n            item.setData(identifier, CREATOR_IDENTIFIER_ROLE)\n            item.setData(\n                creator_item.create_allow_thumbnail,\n                CREATOR_THUMBNAIL_ENABLED_ROLE\n            )\n            item.setData(creator_item.family, FAMILY_ROLE)\n            if is_new:\n                self._creators_model.appendRow(item)\n\n        # Remove families that are no more available\n        for identifier in (old_creators - new_creators):\n            item = existing_items[identifier]\n            self._creators_model.takeRow(item.row())\n\n        if self._creators_model.rowCount() < 1:\n            return\n\n        self._creators_sort_model.sort(0)\n        # Make sure there is a selection\n        indexes = self._creators_view.selectedIndexes()\n        if not indexes:\n            index = self._creators_sort_model.index(0, 0)\n            self._creators_view.setCurrentIndex(index)\n        else:\n            index = indexes[0]\n\n        identifier = index.data(CREATOR_IDENTIFIER_ROLE)\n        create_item = creator_items_by_identifier.get(identifier)\n\n        self._set_creator(create_item)\n\n    def _on_plugins_refresh(self):\n        # Trigger refresh only if is visible\n        self.refresh()\n\n    def _on_asset_change(self):\n        self._refresh_asset()\n\n        asset_name = self._assets_widget.get_selected_asset_name()\n        self._tasks_widget.set_asset_name(asset_name)\n        if self._context_change_is_enabled():\n            self._invalidate_prereq_deffered()\n\n    def _on_task_change(self):\n        if self._context_change_is_enabled():\n            self._invalidate_prereq_deffered()\n\n    def _on_thumbnail_create(self, thumbnail_path):\n        self._last_thumbnail_path = thumbnail_path\n        self._thumbnail_widget.set_current_thumbnails([thumbnail_path])\n\n    def _on_thumbnail_clear(self):\n        self._last_thumbnail_path = None\n\n    def _on_current_session_context_request(self):\n        self._assets_widget.set_current_session_asset()\n        task_name = self.current_task_name\n        if task_name:\n            self._tasks_widget.select_task_name(task_name)\n\n    def _on_creator_item_change(self, new_index, _old_index):\n        identifier = None\n        if new_index.isValid():\n            identifier = new_index.data(CREATOR_IDENTIFIER_ROLE)\n        self._set_creator_by_identifier(identifier)\n\n    def _set_creator_detailed_text(self, creator_item):\n        # TODO implement\n        description = \"\"\n        if creator_item is not None:\n            description = creator_item.detailed_description or description\n        self._controller.event_system.emit(\n            \"show.detailed.help\",\n            {\n                \"message\": description\n            },\n            \"create.widget\"\n        )\n\n    def _set_creator_by_identifier(self, identifier):\n        creator_item = self._controller.creator_items.get(identifier)\n        self._set_creator(creator_item)\n\n    def _set_creator(self, creator_item):\n        \"\"\"Set current creator item.\n\n        Args:\n            creator_item (CreatorItem): Item representing creator that can be\n                triggered by artist.\n        \"\"\"\n\n        self._creator_short_desc_widget.set_creator_item(creator_item)\n        self._set_creator_detailed_text(creator_item)\n        self._pre_create_widget.set_creator_item(creator_item)\n\n        self._selected_creator = creator_item\n\n        if not creator_item:\n            self._set_context_enabled(False)\n            return\n\n        if (\n            creator_item.create_allow_context_change\n            != self._context_change_is_enabled()\n        ):\n            self._set_context_enabled(creator_item.create_allow_context_change)\n            self._refresh_asset()\n\n        self._thumbnail_widget.setVisible(\n            creator_item.create_allow_thumbnail\n        )\n\n        default_variants = creator_item.default_variants\n        if not default_variants:\n            default_variants = [DEFAULT_VARIANT_VALUE]\n\n        default_variant = creator_item.default_variant\n        if not default_variant:\n            default_variant = default_variants[0]\n\n        for action in tuple(self.variant_hints_menu.actions()):\n            self.variant_hints_menu.removeAction(action)\n            action.deleteLater()\n\n        for variant in default_variants:\n            if variant in SEPARATORS:\n                self.variant_hints_menu.addSeparator()\n            elif variant:\n                self.variant_hints_menu.addAction(variant)\n\n        variant_text = default_variant or DEFAULT_VARIANT_VALUE\n        # Make sure subset name is updated to new plugin\n        if variant_text == self.variant_input.text():\n            self._on_variant_change()\n        else:\n            self.variant_input.setText(variant_text)\n\n    def _on_variant_widget_resize(self):\n        self.variant_hints_btn.setFixedHeight(self.variant_input.height())\n\n    def _on_variant_btn_click(self):\n        pos = self.variant_hints_btn.rect().bottomLeft()\n        point = self.variant_hints_btn.mapToGlobal(pos)\n        self.variant_hints_menu.popup(point)\n\n    def _on_variant_action(self, action):\n        value = action.text()\n        if self.variant_input.text() != value:\n            self.variant_input.setText(value)\n\n    def _on_variant_change(self, variant_value=None):\n        if not self._prereq_available:\n            return\n\n        # This should probably never happen?\n        if not self._selected_creator:\n            if self.subset_name_input.text():\n                self.subset_name_input.setText(\"\")\n            return\n\n        if variant_value is None:\n            variant_value = self.variant_input.text()\n\n        if not self._compiled_name_pattern.match(variant_value):\n            self._create_btn.setEnabled(False)\n            self._set_variant_state_property(\"invalid\")\n            self.subset_name_input.setText(\"< Invalid variant >\")\n            return\n\n        if not self._context_change_is_enabled():\n            self._create_btn.setEnabled(True)\n            self._set_variant_state_property(\"\")\n            self.subset_name_input.setText(\"< Valid variant >\")\n            return\n\n        asset_name = self._get_asset_name()\n        task_name = self._get_task_name()\n        creator_idenfier = self._selected_creator.identifier\n        # Calculate subset name with Creator plugin\n        try:\n            subset_name = self._controller.get_subset_name(\n                creator_idenfier, variant_value, task_name, asset_name\n            )\n        except TaskNotSetError:\n            self._create_btn.setEnabled(False)\n            self._set_variant_state_property(\"invalid\")\n            self.subset_name_input.setText(\"< Missing task >\")\n            return\n\n        self.subset_name_input.setText(subset_name)\n\n        self._create_btn.setEnabled(True)\n        self._validate_subset_name(subset_name, variant_value)\n\n    def _validate_subset_name(self, subset_name, variant_value):\n        # Get all subsets of the current asset\n        if self._subset_names:\n            existing_subset_names = set(self._subset_names)\n        else:\n            existing_subset_names = set()\n        existing_subset_names_low = set(\n            _name.lower()\n            for _name in existing_subset_names\n        )\n\n        # Replace\n        compare_regex = re.compile(re.sub(\n            variant_value, \"(.+)\", subset_name, flags=re.IGNORECASE\n        ))\n        variant_hints = set()\n        if variant_value:\n            for _name in existing_subset_names:\n                _result = compare_regex.search(_name)\n                if _result:\n                    variant_hints |= set(_result.groups())\n\n        # Remove previous hints from menu\n        for action in tuple(self.variant_hints_group.actions()):\n            self.variant_hints_group.removeAction(action)\n            self.variant_hints_menu.removeAction(action)\n            action.deleteLater()\n\n        # Add separator if there are hints and menu already has actions\n        if variant_hints and self.variant_hints_menu.actions():\n            self.variant_hints_menu.addSeparator()\n\n        # Add hints to actions\n        for variant_hint in variant_hints:\n            action = self.variant_hints_menu.addAction(variant_hint)\n            self.variant_hints_group.addAction(action)\n\n        # Indicate subset existence\n        if not variant_value:\n            property_value = \"empty\"\n\n        elif subset_name.lower() in existing_subset_names_low:\n            # validate existence of subset name with lowered text\n            #   - \"renderMain\" vs. \"rendermain\" mean same path item for\n            #   windows\n            property_value = \"exists\"\n        else:\n            property_value = \"new\"\n\n        self._set_variant_state_property(property_value)\n\n        variant_is_valid = variant_value.strip() != \"\"\n        if variant_is_valid != self._create_btn.isEnabled():\n            self._create_btn.setEnabled(variant_is_valid)\n\n    def _set_variant_state_property(self, state):\n        current_value = self.variant_input.property(\"state\")\n        if current_value != state:\n            self.variant_input.setProperty(\"state\", state)\n            self.variant_input.style().polish(self.variant_input)\n\n    def _on_first_show(self):\n        width = self.width()\n        part = int(width / 4)\n        rem_width = width - part\n        self._main_splitter_widget.setSizes([part, rem_width])\n        rem_width = rem_width - part\n        self._creators_splitter.setSizes([part, rem_width])\n\n    def showEvent(self, event):\n        super(CreateWidget, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self._on_first_show()\n\n    def _on_creator_basics_resize(self):\n        self._thumbnail_widget.set_height(\n            self._creator_basics_widget.sizeHint().height()\n        )\n\n    def _on_create(self):\n        indexes = self._creators_view.selectedIndexes()\n        if not indexes or len(indexes) > 1:\n            return\n\n        if not self._create_btn.isEnabled():\n            return\n\n        index = indexes[0]\n        creator_identifier = index.data(CREATOR_IDENTIFIER_ROLE)\n        family = index.data(FAMILY_ROLE)\n        variant = self.variant_input.text()\n        # Care about subset name only if context change is enabled\n        subset_name = None\n        asset_name = None\n        task_name = None\n        if self._context_change_is_enabled():\n            subset_name = self.subset_name_input.text()\n            asset_name = self._get_asset_name()\n            task_name = self._get_task_name()\n\n        pre_create_data = self._pre_create_widget.current_value()\n        if index.data(CREATOR_THUMBNAIL_ENABLED_ROLE):\n            pre_create_data[PRE_CREATE_THUMBNAIL_KEY] = (\n                self._last_thumbnail_path\n            )\n\n        # Where to define these data?\n        # - what data show be stored?\n        if AYON_SERVER_ENABLED:\n            asset_key = \"folderPath\"\n        else:\n            asset_key = \"asset\"\n\n        instance_data = {\n            asset_key: asset_name,\n            \"task\": task_name,\n            \"variant\": variant,\n            \"family\": family\n        }\n\n        success = self._controller.create(\n            creator_identifier,\n            subset_name,\n            instance_data,\n            pre_create_data\n        )\n\n        if success:\n            self._set_creator(self._selected_creator)\n            self.variant_input.setText(variant)\n            self._controller.emit_card_message(\"Creation finished...\")\n            self._last_thumbnail_path = None\n            self._thumbnail_widget.set_current_thumbnails()\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/help_widget.py",
    "content": "try:\n    import commonmark\nexcept Exception:\n    commonmark = None\n\nfrom qtpy import QtWidgets, QtCore\n\n\nclass HelpButton(QtWidgets.QPushButton):\n    \"\"\"Button used to trigger help dialog.\"\"\"\n\n    def __init__(self, parent):\n        super(HelpButton, self).__init__(parent)\n        self.setObjectName(\"CreateDialogHelpButton\")\n        self.setText(\"?\")\n\n\nclass HelpWidget(QtWidgets.QWidget):\n    \"\"\"Widget showing help for single functionality.\"\"\"\n\n    def __init__(self, parent):\n        super(HelpWidget, self).__init__(parent)\n\n        # TODO add hints what to help with?\n        detail_description_input = QtWidgets.QTextEdit(self)\n        detail_description_input.setObjectName(\"CreatorDetailedDescription\")\n        detail_description_input.setTextInteractionFlags(\n            QtCore.Qt.TextBrowserInteraction\n        )\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n        main_layout.addWidget(detail_description_input, 1)\n\n        self._detail_description_input = detail_description_input\n\n        self.set_detailed_text()\n\n    def set_detailed_text(self, text=None):\n        if not text:\n            text = \"We didn't prepare help for this part...\"\n\n        if commonmark:\n            html = commonmark.commonmark(text)\n            self._detail_description_input.setHtml(html)\n        elif hasattr(self._detail_description_input, \"setMarkdown\"):\n            self._detail_description_input.setMarkdown(text)\n        else:\n            self._detail_description_input.setText(text)\n\n\nclass HelpDialog(QtWidgets.QDialog):\n    default_width = 530\n    default_height = 340\n\n    def __init__(self, controller, parent):\n        super(HelpDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"Help dialog\")\n\n        help_content = HelpWidget(self)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.addWidget(help_content, 1)\n\n        controller.event_system.add_callback(\n            \"show.detailed.help\", self._on_help_request\n        )\n\n        self._controller = controller\n\n        self._help_content = help_content\n\n    def _on_help_request(self, event):\n        message = event.get(\"message\")\n        self.set_detailed_text(message)\n\n    def set_detailed_text(self, text=None):\n        self._help_content.set_detailed_text(text)\n\n    def showEvent(self, event):\n        super(HelpDialog, self).showEvent(event)\n        self.resize(self.default_width, self.default_height)\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/icons.py",
    "content": "import os\n\nfrom qtpy import QtGui\n\n\ndef get_icon_path(icon_name=None, filename=None):\n    \"\"\"Path to image in './images' folder.\"\"\"\n    if icon_name is None and filename is None:\n        return None\n\n    if filename is None:\n        filename = \"{}.png\".format(icon_name)\n\n    path = os.path.join(\n        os.path.dirname(os.path.abspath(__file__)),\n        \"images\",\n        filename\n    )\n    if os.path.exists(path):\n        return path\n    return None\n\n\ndef get_image(icon_name=None, filename=None):\n    \"\"\"Load image from './images' as QImage.\"\"\"\n    path = get_icon_path(icon_name, filename)\n    if path:\n        return QtGui.QImage(path)\n    return None\n\n\ndef get_pixmap(icon_name=None, filename=None):\n    \"\"\"Load image from './images' as QPixmap.\"\"\"\n    path = get_icon_path(icon_name, filename)\n    if path:\n        return QtGui.QPixmap(path)\n    return None\n\n\ndef get_icon(icon_name=None, filename=None):\n    \"\"\"Load image from './images' as QICon.\"\"\"\n    pix = get_pixmap(icon_name, filename)\n    if pix:\n        return QtGui.QIcon(pix)\n    return None\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/list_view_widgets.py",
    "content": "\"\"\"Simple easy instance view grouping instances into collapsible groups.\n\nView has multiselection ability. Groups are defined by `creator_label`\nattribute on instance (Group defined by creator).\n\nEach item can be enabled/disabled with their checkbox, whole group\ncan be enabled/disabled with checkbox on group or\nselection can be enabled disabled using checkbox or keyboard key presses:\n- Space - change state of selection to opposite\n- Enter - enable selection\n- Backspace - disable selection\n\n```\n|- Context\n|- <Group 1> [x]\n|  |- <Instance 1> [x]\n|  |- <Instance 2> [x]\n|  ...\n|- <Group 2> [ ]\n|  |- <Instance 3> [ ]\n|  ...\n...\n```\n\"\"\"\nimport collections\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.style import get_objected_colors\nfrom openpype.widgets.nice_checkbox import NiceCheckbox\nfrom openpype.tools.utils.lib import html_escape, checkstate_int_to_enum\nfrom .widgets import AbstractInstanceView\nfrom ..constants import (\n    INSTANCE_ID_ROLE,\n    SORT_VALUE_ROLE,\n    IS_GROUP_ROLE,\n    CONTEXT_ID,\n    CONTEXT_LABEL,\n    GROUP_ROLE,\n    CONVERTER_IDENTIFIER_ROLE,\n    CONVERTOR_ITEM_GROUP,\n)\n\n\nclass ListItemDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Generic delegate for instance group.\n\n    All indexes having `IS_GROUP_ROLE` data set to True will use\n    `group_item_paint` method to draw it's content otherwise default styled\n    item delegate paint method is used.\n\n    Goal is to draw group items with different colors for normal, hover and\n    pressed state.\n    \"\"\"\n    radius_ratio = 0.3\n\n    def __init__(self, parent):\n        super(ListItemDelegate, self).__init__(parent)\n\n        group_color_info = get_objected_colors(\"publisher\", \"list-view-group\")\n\n        self._group_colors = {\n            key: value.get_qcolor()\n            for key, value in group_color_info.items()\n        }\n\n    def paint(self, painter, option, index):\n        if index.data(IS_GROUP_ROLE):\n            self.group_item_paint(painter, option, index)\n        else:\n            super(ListItemDelegate, self).paint(painter, option, index)\n\n    def group_item_paint(self, painter, option, index):\n        \"\"\"Paint group item.\"\"\"\n        self.initStyleOption(option, index)\n\n        bg_rect = QtCore.QRectF(\n            option.rect.left(), option.rect.top() + 1,\n            option.rect.width(), option.rect.height() - 2\n        )\n        ratio = bg_rect.height() * self.radius_ratio\n        bg_path = QtGui.QPainterPath()\n        bg_path.addRoundedRect(\n            QtCore.QRectF(bg_rect), ratio, ratio\n        )\n\n        painter.save()\n        painter.setRenderHints(\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n            | QtGui.QPainter.TextAntialiasing\n        )\n\n        # Draw backgrounds\n        painter.fillPath(bg_path, self._group_colors[\"bg\"])\n        selected = option.state & QtWidgets.QStyle.State_Selected\n        hovered = option.state & QtWidgets.QStyle.State_MouseOver\n        if selected and hovered:\n            painter.fillPath(bg_path, self._group_colors[\"bg-selected-hover\"])\n\n        elif hovered:\n            painter.fillPath(bg_path, self._group_colors[\"bg-hover\"])\n\n        painter.restore()\n\n\nclass InstanceListItemWidget(QtWidgets.QWidget):\n    \"\"\"Widget with instance info drawn over delegate paint.\n\n    This is required to be able use custom checkbox on custom place.\n    \"\"\"\n    active_changed = QtCore.Signal(str, bool)\n\n    def __init__(self, instance, parent):\n        super(InstanceListItemWidget, self).__init__(parent)\n\n        self.instance = instance\n\n        instance_label = instance.label\n        if instance_label is None:\n            # Do not cause UI crash if label is 'None'\n            instance_label = \"No label\"\n\n        instance_label = html_escape(instance_label)\n\n        subset_name_label = QtWidgets.QLabel(instance_label, self)\n        subset_name_label.setObjectName(\"ListViewSubsetName\")\n\n        active_checkbox = NiceCheckbox(parent=self)\n        active_checkbox.setChecked(instance[\"active\"])\n\n        layout = QtWidgets.QHBoxLayout(self)\n        content_margins = layout.contentsMargins()\n        layout.setContentsMargins(content_margins.left() + 2, 0, 2, 0)\n        layout.addWidget(subset_name_label)\n        layout.addStretch(1)\n        layout.addWidget(active_checkbox)\n\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        subset_name_label.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        active_checkbox.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        active_checkbox.stateChanged.connect(self._on_active_change)\n\n        self._instance_label_widget = subset_name_label\n        self._active_checkbox = active_checkbox\n\n        self._has_valid_context = None\n\n        self._set_valid_property(instance.has_valid_context)\n\n    def _set_valid_property(self, valid):\n        if self._has_valid_context == valid:\n            return\n        self._has_valid_context = valid\n        state = \"\"\n        if not valid:\n            state = \"invalid\"\n        self._instance_label_widget.setProperty(\"state\", state)\n        self._instance_label_widget.style().polish(self._instance_label_widget)\n\n    def is_active(self):\n        \"\"\"Instance is activated.\"\"\"\n        return self.instance[\"active\"]\n\n    def set_active(self, new_value):\n        \"\"\"Change active state of instance and checkbox.\"\"\"\n        checkbox_value = self._active_checkbox.isChecked()\n        instance_value = self.instance[\"active\"]\n        if new_value is None:\n            new_value = not instance_value\n\n        # First change instance value and them change checkbox\n        # - prevent to trigger `active_changed` signal\n        if instance_value != new_value:\n            self.instance[\"active\"] = new_value\n\n        if checkbox_value != new_value:\n            self._active_checkbox.setChecked(new_value)\n\n    def update_instance(self, instance):\n        \"\"\"Update instance object.\"\"\"\n        self.instance = instance\n        self.update_instance_values()\n\n    def update_instance_values(self):\n        \"\"\"Update instance data propagated to widgets.\"\"\"\n        # Check subset name\n        label = self.instance.label\n        if label != self._instance_label_widget.text():\n            self._instance_label_widget.setText(html_escape(label))\n        # Check active state\n        self.set_active(self.instance[\"active\"])\n        # Check valid states\n        self._set_valid_property(self.instance.has_valid_context)\n\n    def _on_active_change(self):\n        new_value = self._active_checkbox.isChecked()\n        old_value = self.instance[\"active\"]\n        if new_value == old_value:\n            return\n\n        self.instance[\"active\"] = new_value\n        self.active_changed.emit(self.instance.id, new_value)\n\n    def set_active_toggle_enabled(self, enabled):\n        self._active_checkbox.setEnabled(enabled)\n\n\nclass ListContextWidget(QtWidgets.QFrame):\n    \"\"\"Context (or global attributes) widget.\"\"\"\n    def __init__(self, parent):\n        super(ListContextWidget, self).__init__(parent)\n\n        label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(5, 0, 2, 0)\n        layout.addWidget(\n            label_widget, 1, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter\n        )\n\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        self.label_widget = label_widget\n\n\nclass InstanceListGroupWidget(QtWidgets.QFrame):\n    \"\"\"Widget representing group of instances.\n\n    Has collapse/expand indicator, label of group and checkbox modifying all of\n    it's children.\n    \"\"\"\n    expand_changed = QtCore.Signal(str, bool)\n    toggle_requested = QtCore.Signal(str, int)\n\n    def __init__(self, group_name, parent):\n        super(InstanceListGroupWidget, self).__init__(parent)\n        self.setObjectName(\"InstanceListGroupWidget\")\n\n        self.group_name = group_name\n        self._expanded = False\n\n        expand_btn = QtWidgets.QToolButton(self)\n        expand_btn.setObjectName(\"ArrowBtn\")\n        expand_btn.setArrowType(QtCore.Qt.RightArrow)\n        expand_btn.setMaximumWidth(14)\n\n        name_label = QtWidgets.QLabel(group_name, self)\n\n        toggle_checkbox = NiceCheckbox(parent=self)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(5, 0, 2, 0)\n        layout.addWidget(expand_btn)\n        layout.addWidget(\n            name_label, 1, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter\n        )\n        layout.addWidget(toggle_checkbox, 0)\n\n        name_label.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        expand_btn.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        expand_btn.clicked.connect(self._on_expand_clicked)\n        toggle_checkbox.stateChanged.connect(self._on_checkbox_change)\n\n        self._ignore_state_change = False\n\n        self._expected_checkstate = None\n\n        self.name_label = name_label\n        self.expand_btn = expand_btn\n        self.toggle_checkbox = toggle_checkbox\n\n    def set_checkstate(self, state):\n        \"\"\"Change checkstate of \"active\" checkbox.\n\n        Args:\n            state(QtCore.Qt.CheckState): Checkstate of checkbox. Have 3\n                variants Unchecked, Checked and PartiallyChecked.\n        \"\"\"\n\n        if self.checkstate() == state:\n            return\n        self._ignore_state_change = True\n        self.toggle_checkbox.setCheckState(state)\n        self._ignore_state_change = False\n\n    def checkstate(self):\n        \"\"\"Current checkstate of \"active\" checkbox.\"\"\"\n\n        return self.toggle_checkbox.checkState()\n\n    def _on_checkbox_change(self, state):\n        if not self._ignore_state_change:\n            self.toggle_requested.emit(self.group_name, state)\n\n    def _on_expand_clicked(self):\n        self.expand_changed.emit(self.group_name, not self._expanded)\n\n    def set_expanded(self, expanded):\n        \"\"\"Change icon of collapse/expand identifier.\"\"\"\n        if self._expanded == expanded:\n            return\n\n        self._expanded = expanded\n        if expanded:\n            self.expand_btn.setArrowType(QtCore.Qt.DownArrow)\n        else:\n            self.expand_btn.setArrowType(QtCore.Qt.RightArrow)\n\n    def set_active_toggle_enabled(self, enabled):\n        self.toggle_checkbox.setEnabled(enabled)\n\n\nclass InstanceTreeView(QtWidgets.QTreeView):\n    \"\"\"View showing instances and their groups.\"\"\"\n    toggle_requested = QtCore.Signal(int)\n\n    def __init__(self, *args, **kwargs):\n        super(InstanceTreeView, self).__init__(*args, **kwargs)\n\n        self.setObjectName(\"InstanceListView\")\n        self.setHeaderHidden(True)\n        self.setIndentation(0)\n        self.setExpandsOnDoubleClick(False)\n        self.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection\n        )\n        self.viewport().setMouseTracking(True)\n        self._pressed_group_index = None\n\n    def _expand_item(self, index, expand=None):\n        is_expanded = self.isExpanded(index)\n        if expand is None:\n            expand = not is_expanded\n\n        if expand != is_expanded:\n            if expand:\n                self.expand(index)\n            else:\n                self.collapse(index)\n\n    def get_selected_instance_ids(self):\n        \"\"\"Ids of selected instances.\"\"\"\n        instance_ids = set()\n        for index in self.selectionModel().selectedIndexes():\n            if index.data(CONVERTER_IDENTIFIER_ROLE) is not None:\n                continue\n\n            instance_id = index.data(INSTANCE_ID_ROLE)\n            if instance_id is not None:\n                instance_ids.add(instance_id)\n        return instance_ids\n\n    def event(self, event):\n        if not event.type() == QtCore.QEvent.KeyPress:\n            pass\n\n        elif event.key() == QtCore.Qt.Key_Space:\n            self.toggle_requested.emit(-1)\n            return True\n\n        elif event.key() == QtCore.Qt.Key_Backspace:\n            self.toggle_requested.emit(0)\n            return True\n\n        elif event.key() == QtCore.Qt.Key_Return:\n            self.toggle_requested.emit(1)\n            return True\n\n        return super(InstanceTreeView, self).event(event)\n\n    def _mouse_press(self, event):\n        \"\"\"Store index of pressed group.\n\n        This is to be able change state of group and process mouse\n        \"double click\" as 2x \"single click\".\n        \"\"\"\n        if event.button() != QtCore.Qt.LeftButton:\n            return\n\n        pressed_group_index = None\n        pos_index = self.indexAt(event.pos())\n        if pos_index.data(IS_GROUP_ROLE):\n            pressed_group_index = pos_index\n\n        self._pressed_group_index = pressed_group_index\n\n    def mousePressEvent(self, event):\n        self._mouse_press(event)\n        super(InstanceTreeView, self).mousePressEvent(event)\n\n    def mouseDoubleClickEvent(self, event):\n        self._mouse_press(event)\n        super(InstanceTreeView, self).mouseDoubleClickEvent(event)\n\n    def _mouse_release(self, event, pressed_index):\n        if event.button() != QtCore.Qt.LeftButton:\n            return False\n\n        pos_index = self.indexAt(event.pos())\n        if not pos_index.data(IS_GROUP_ROLE) or pressed_index != pos_index:\n            return False\n\n        if self.state() == QtWidgets.QTreeView.State.DragSelectingState:\n            indexes = self.selectionModel().selectedIndexes()\n            if len(indexes) != 1 or indexes[0] != pos_index:\n                return False\n\n        self._expand_item(pos_index)\n        return True\n\n    def mouseReleaseEvent(self, event):\n        pressed_index = self._pressed_group_index\n        self._pressed_group_index = None\n        result = self._mouse_release(event, pressed_index)\n        if not result:\n            super(InstanceTreeView, self).mouseReleaseEvent(event)\n\n\nclass InstanceListView(AbstractInstanceView):\n    \"\"\"Widget providing abstract methods of AbstractInstanceView for list view.\n\n    This is public access to and from list view.\n    \"\"\"\n    def __init__(self, controller, parent):\n        super(InstanceListView, self).__init__(parent)\n\n        self._controller = controller\n\n        instance_view = InstanceTreeView(self)\n        instance_delegate = ListItemDelegate(instance_view)\n        instance_view.setItemDelegate(instance_delegate)\n        instance_model = QtGui.QStandardItemModel()\n\n        proxy_model = QtCore.QSortFilterProxyModel()\n        proxy_model.setSourceModel(instance_model)\n        proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)\n        proxy_model.setSortRole(SORT_VALUE_ROLE)\n        proxy_model.setFilterKeyColumn(0)\n        proxy_model.setDynamicSortFilter(True)\n\n        instance_view.setModel(proxy_model)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(instance_view)\n\n        instance_view.selectionModel().selectionChanged.connect(\n            self._on_selection_change\n        )\n        instance_view.collapsed.connect(self._on_collapse)\n        instance_view.expanded.connect(self._on_expand)\n        instance_view.toggle_requested.connect(self._on_toggle_request)\n\n        self._group_items = {}\n        self._group_widgets = {}\n        self._widgets_by_id = {}\n        # Group by instance id for handling of active state\n        self._group_by_instance_id = {}\n        self._context_item = None\n        self._context_widget = None\n\n        self._convertor_group_item = None\n        self._convertor_group_widget = None\n        self._convertor_items_by_id = {}\n\n        self._instance_view = instance_view\n        self._instance_delegate = instance_delegate\n        self._instance_model = instance_model\n        self._proxy_model = proxy_model\n\n        self._active_toggle_enabled = True\n\n    def _on_expand(self, index):\n        self._update_widget_expand_state(index, True)\n\n    def _on_collapse(self, index):\n        self._update_widget_expand_state(index, False)\n\n    def _update_widget_expand_state(self, index, expanded):\n        group_name = index.data(GROUP_ROLE)\n        if group_name == CONVERTOR_ITEM_GROUP:\n            group_widget = self._convertor_group_widget\n        else:\n            group_widget = self._group_widgets.get(group_name)\n\n        if group_widget:\n            group_widget.set_expanded(expanded)\n\n    def _on_toggle_request(self, toggle):\n        if not self._active_toggle_enabled:\n            return\n\n        selected_instance_ids = self._instance_view.get_selected_instance_ids()\n        if toggle == -1:\n            active = None\n        elif toggle == 1:\n            active = True\n        else:\n            active = False\n\n        group_names = set()\n        for instance_id in selected_instance_ids:\n            widget = self._widgets_by_id.get(instance_id)\n            if widget is None:\n                continue\n\n            widget.set_active(active)\n            group_name = self._group_by_instance_id.get(instance_id)\n            if group_name is not None:\n                group_names.add(group_name)\n\n        for group_name in group_names:\n            self._update_group_checkstate(group_name)\n\n    def _update_group_checkstate(self, group_name):\n        \"\"\"Update checkstate of one group.\"\"\"\n        widget = self._group_widgets.get(group_name)\n        if widget is None:\n            return\n\n        activity = None\n        for instance_id, _group_name in self._group_by_instance_id.items():\n            if _group_name != group_name:\n                continue\n\n            instance_widget = self._widgets_by_id.get(instance_id)\n            if not instance_widget:\n                continue\n\n            if activity is None:\n                activity = int(instance_widget.is_active())\n\n            elif activity != instance_widget.is_active():\n                activity = -1\n                break\n\n        if activity is None:\n            return\n\n        state = QtCore.Qt.PartiallyChecked\n        if activity == 0:\n            state = QtCore.Qt.Unchecked\n        elif activity == 1:\n            state = QtCore.Qt.Checked\n        widget.set_checkstate(state)\n\n    def refresh(self):\n        \"\"\"Refresh instances in the view.\"\"\"\n        # Sort view at the end of refresh\n        # - is turned off until any change in view happens\n        sort_at_the_end = False\n        # Create or use already existing context item\n        # - context widget does not change so we don't have to update anything\n        if self._make_sure_context_item_exists():\n            sort_at_the_end = True\n\n        self._update_convertor_items_group()\n\n        # Prepare instances by their groups\n        instances_by_group_name = collections.defaultdict(list)\n        group_names = set()\n        for instance in self._controller.instances.values():\n            group_label = instance.group_label\n            group_names.add(group_label)\n            instances_by_group_name[group_label].append(instance)\n\n        # Create new groups based on prepared `instances_by_group_name`\n        if self._make_sure_groups_exists(group_names):\n            sort_at_the_end = True\n\n        # Remove groups that are not available anymore\n        self._remove_groups_except(group_names)\n\n        # Store which groups should be expanded at the end\n        expand_groups = set()\n        # Process changes in each group item\n        # - create new instance, update existing and remove not existing\n        for group_name, group_item in self._group_items.items():\n            # Instance items to remove\n            # - will contain all existing instance ids at the start\n            # - instance ids may be removed when existing instances are checked\n            to_remove = set()\n            # Mapping of existing instances under group item\n            existing_mapping = {}\n\n            # Get group index to be able get children indexes\n            group_index = self._instance_model.index(\n                group_item.row(), group_item.column()\n            )\n\n            # Iterate over children indexes of group item\n            for idx in range(group_item.rowCount()):\n                index = self._instance_model.index(idx, 0, group_index)\n                instance_id = index.data(INSTANCE_ID_ROLE)\n                # Add all instance into `to_remove` set\n                to_remove.add(instance_id)\n                existing_mapping[instance_id] = idx\n\n            # Collect all new instances that are not existing under group\n            # New items\n            new_items = []\n            # Tuples of new instance and instance itself\n            new_items_with_instance = []\n            # Group activity (should be {-1;0;1} at the end)\n            # - 0 when all instances are disabled\n            # - 1 when all instances are enabled\n            # - -1 when it's mixed\n            activity = None\n            for instance in instances_by_group_name[group_name]:\n                instance_id = instance.id\n                # Handle group activity\n                if activity is None:\n                    activity = int(instance[\"active\"])\n                elif activity == -1:\n                    pass\n                elif activity != instance[\"active\"]:\n                    activity = -1\n\n                self._group_by_instance_id[instance_id] = group_name\n                # Remove instance id from `to_remove` if already exists and\n                #   trigger update of widget\n                if instance_id in to_remove:\n                    to_remove.remove(instance_id)\n                    widget = self._widgets_by_id[instance_id]\n                    widget.update_instance(instance)\n                    continue\n\n                # Create new item and store it as new\n                item = QtGui.QStandardItem()\n                item.setData(instance[\"subset\"], SORT_VALUE_ROLE)\n                item.setData(instance[\"subset\"], GROUP_ROLE)\n                item.setData(instance_id, INSTANCE_ID_ROLE)\n                new_items.append(item)\n                new_items_with_instance.append((item, instance))\n\n            # Set checkstate of group checkbox\n            state = QtCore.Qt.PartiallyChecked\n            if activity == 0:\n                state = QtCore.Qt.Unchecked\n            elif activity == 1:\n                state = QtCore.Qt.Checked\n\n            widget = self._group_widgets[group_name]\n            widget.set_checkstate(state)\n\n            # Remove items that were not found\n            idx_to_remove = []\n            for instance_id in to_remove:\n                idx_to_remove.append(existing_mapping[instance_id])\n\n            # Remove them in reverse order to prevent row index changes\n            for idx in reversed(sorted(idx_to_remove)):\n                group_item.removeRows(idx, 1)\n\n            # Cleanup instance related widgets\n            for instance_id in to_remove:\n                self._group_by_instance_id.pop(instance_id)\n                widget = self._widgets_by_id.pop(instance_id)\n                widget.deleteLater()\n\n            # Process new instance items and add them to model and create\n            #   their widgets\n            if new_items:\n                # Trigger sort at the end when new instances are available\n                sort_at_the_end = True\n\n                # Add items under group item\n                group_item.appendRows(new_items)\n\n                for item, instance in new_items_with_instance:\n                    if not instance.has_valid_context:\n                        expand_groups.add(group_name)\n                    item_index = self._instance_model.index(\n                        item.row(),\n                        item.column(),\n                        group_index\n                    )\n                    proxy_index = self._proxy_model.mapFromSource(item_index)\n                    widget = InstanceListItemWidget(\n                        instance, self._instance_view\n                    )\n                    widget.set_active_toggle_enabled(\n                        self._active_toggle_enabled\n                    )\n                    widget.active_changed.connect(self._on_active_changed)\n                    self._instance_view.setIndexWidget(proxy_index, widget)\n                    self._widgets_by_id[instance.id] = widget\n\n            # Trigger sort at the end of refresh\n            if sort_at_the_end:\n                self._proxy_model.sort(0)\n\n        # Expand groups marked for expanding\n        for group_name in expand_groups:\n            group_item = self._group_items[group_name]\n            proxy_index = self._proxy_model.mapFromSource(group_item.index())\n\n            self._instance_view.expand(proxy_index)\n\n    def _make_sure_context_item_exists(self):\n        if self._context_item is not None:\n            return False\n\n        root_item = self._instance_model.invisibleRootItem()\n        context_item = QtGui.QStandardItem()\n        context_item.setData(0, SORT_VALUE_ROLE)\n        context_item.setData(CONTEXT_ID, INSTANCE_ID_ROLE)\n\n        root_item.appendRow(context_item)\n\n        index = self._instance_model.index(\n            context_item.row(), context_item.column()\n        )\n        proxy_index = self._proxy_model.mapFromSource(index)\n        widget = ListContextWidget(self._instance_view)\n        self._instance_view.setIndexWidget(proxy_index, widget)\n\n        self._context_widget = widget\n        self._context_item = context_item\n        return True\n\n    def _update_convertor_items_group(self):\n        created_new_items = False\n        convertor_items_by_id = self._controller.convertor_items\n        group_item = self._convertor_group_item\n        if not convertor_items_by_id and group_item is None:\n            return created_new_items\n\n        root_item = self._instance_model.invisibleRootItem()\n        if not convertor_items_by_id:\n            root_item.removeRow(group_item.row())\n            self._convertor_group_widget.deleteLater()\n            self._convertor_group_widget = None\n            self._convertor_items_by_id = {}\n            return created_new_items\n\n        if group_item is None:\n            created_new_items = True\n            group_item = QtGui.QStandardItem()\n            group_item.setData(CONVERTOR_ITEM_GROUP, GROUP_ROLE)\n            group_item.setData(1, SORT_VALUE_ROLE)\n            group_item.setData(True, IS_GROUP_ROLE)\n            group_item.setFlags(QtCore.Qt.ItemIsEnabled)\n\n            root_item.appendRow(group_item)\n\n            index = self._instance_model.index(\n                group_item.row(), group_item.column()\n            )\n            proxy_index = self._proxy_model.mapFromSource(index)\n            widget = InstanceListGroupWidget(\n                CONVERTOR_ITEM_GROUP, self._instance_view\n            )\n            widget.toggle_checkbox.setVisible(False)\n            widget.expand_changed.connect(\n                self._on_convertor_group_expand_request\n            )\n            self._instance_view.setIndexWidget(proxy_index, widget)\n\n            self._convertor_group_item = group_item\n            self._convertor_group_widget = widget\n\n        for row in reversed(range(group_item.rowCount())):\n            child_item = group_item.child(row)\n            child_identifier = child_item.data(CONVERTER_IDENTIFIER_ROLE)\n            if child_identifier not in convertor_items_by_id:\n                self._convertor_items_by_id.pop(child_identifier, None)\n                group_item.removeRows(row, 1)\n\n        new_items = []\n        for identifier, convertor_item in convertor_items_by_id.items():\n            item = self._convertor_items_by_id.get(identifier)\n            if item is None:\n                created_new_items = True\n                item = QtGui.QStandardItem(convertor_item.label)\n                new_items.append(item)\n            item.setData(convertor_item.id, INSTANCE_ID_ROLE)\n            item.setData(convertor_item.label, SORT_VALUE_ROLE)\n            item.setData(CONVERTOR_ITEM_GROUP, GROUP_ROLE)\n            item.setData(\n                convertor_item.identifier, CONVERTER_IDENTIFIER_ROLE\n            )\n            self._convertor_items_by_id[identifier] = item\n\n        if new_items:\n            group_item.appendRows(new_items)\n\n        return created_new_items\n\n    def _make_sure_groups_exists(self, group_names):\n        new_group_items = []\n        for group_name in group_names:\n            if group_name in self._group_items:\n                continue\n\n            group_item = QtGui.QStandardItem()\n            group_item.setData(group_name, GROUP_ROLE)\n            group_item.setData(group_name, SORT_VALUE_ROLE)\n            group_item.setData(True, IS_GROUP_ROLE)\n            group_item.setFlags(QtCore.Qt.ItemIsEnabled)\n            self._group_items[group_name] = group_item\n            new_group_items.append(group_item)\n\n        # Add new group items to root item if there are any\n        if not new_group_items:\n            return False\n\n        # Access to root item of main model\n        root_item = self._instance_model.invisibleRootItem()\n        root_item.appendRows(new_group_items)\n\n        # Create widget for each new group item and store it for future usage\n        for group_item in new_group_items:\n            index = self._instance_model.index(\n                group_item.row(), group_item.column()\n            )\n            proxy_index = self._proxy_model.mapFromSource(index)\n            group_name = group_item.data(GROUP_ROLE)\n            widget = InstanceListGroupWidget(group_name, self._instance_view)\n            widget.set_active_toggle_enabled(\n                self._active_toggle_enabled\n            )\n            widget.expand_changed.connect(self._on_group_expand_request)\n            widget.toggle_requested.connect(self._on_group_toggle_request)\n            self._group_widgets[group_name] = widget\n            self._instance_view.setIndexWidget(proxy_index, widget)\n\n        return True\n\n    def _remove_groups_except(self, group_names):\n        # Remove groups that are not available anymore\n        root_item = self._instance_model.invisibleRootItem()\n        for group_name in tuple(self._group_items.keys()):\n            if group_name in group_names:\n                continue\n\n            group_item = self._group_items.pop(group_name)\n            root_item.removeRow(group_item.row())\n            widget = self._group_widgets.pop(group_name)\n            widget.deleteLater()\n\n    def refresh_instance_states(self):\n        \"\"\"Trigger update of all instances.\"\"\"\n        for widget in self._widgets_by_id.values():\n            widget.update_instance_values()\n\n    def _on_active_changed(self, changed_instance_id, new_value):\n        selected_instance_ids, _, _ = self.get_selected_items()\n\n        selected_ids = set()\n        found = False\n        for instance_id in selected_instance_ids:\n            selected_ids.add(instance_id)\n            if not found and instance_id == changed_instance_id:\n                found = True\n\n        if not found:\n            selected_ids = set()\n            selected_ids.add(changed_instance_id)\n\n        self._change_active_instances(selected_ids, new_value)\n        group_names = set()\n        for instance_id in selected_ids:\n            group_name = self._group_by_instance_id.get(instance_id)\n            if group_name is not None:\n                group_names.add(group_name)\n\n        for group_name in group_names:\n            self._update_group_checkstate(group_name)\n\n    def _change_active_instances(self, instance_ids, new_value):\n        if not instance_ids:\n            return\n\n        changed_ids = set()\n        for instance_id in instance_ids:\n            widget = self._widgets_by_id.get(instance_id)\n            if widget:\n                changed_ids.add(instance_id)\n                widget.set_active(new_value)\n\n        if changed_ids:\n            self.active_changed.emit()\n\n    def _on_selection_change(self, *_args):\n        self.selection_changed.emit()\n\n    def _on_group_expand_request(self, group_name, expanded):\n        group_item = self._group_items.get(group_name)\n        if not group_item:\n            return\n\n        group_index = self._instance_model.index(\n            group_item.row(), group_item.column()\n        )\n        proxy_index = self._proxy_model.mapFromSource(group_index)\n        self._instance_view.setExpanded(proxy_index, expanded)\n\n    def _on_convertor_group_expand_request(self, _, expanded):\n        group_item = self._convertor_group_item\n        if not group_item:\n            return\n        group_index = self._instance_model.index(\n            group_item.row(), group_item.column()\n        )\n        proxy_index = self._proxy_model.mapFromSource(group_index)\n        self._instance_view.setExpanded(proxy_index, expanded)\n\n    def _on_group_toggle_request(self, group_name, state):\n        state = checkstate_int_to_enum(state)\n        if state == QtCore.Qt.PartiallyChecked:\n            return\n\n        if state == QtCore.Qt.Checked:\n            active = True\n        else:\n            active = False\n\n        group_item = self._group_items.get(group_name)\n        if not group_item:\n            return\n\n        instance_ids = set()\n        for row in range(group_item.rowCount()):\n            item = group_item.child(row)\n            instance_id = item.data(INSTANCE_ID_ROLE)\n            if instance_id is not None:\n                instance_ids.add(instance_id)\n\n        self._change_active_instances(instance_ids, active)\n\n        proxy_index = self._proxy_model.mapFromSource(group_item.index())\n        if not self._instance_view.isExpanded(proxy_index):\n            self._instance_view.expand(proxy_index)\n\n    def has_items(self):\n        if self._convertor_group_widget is not None:\n            return True\n        if self._group_items:\n            return True\n        return False\n\n    def get_selected_items(self):\n        \"\"\"Get selected instance ids and context selection.\n\n        Returns:\n            tuple<list, bool>: Selected instance ids and boolean if context\n                is selected.\n        \"\"\"\n\n        instance_ids = []\n        convertor_identifiers = []\n        context_selected = False\n\n        for index in self._instance_view.selectionModel().selectedIndexes():\n            convertor_identifier = index.data(CONVERTER_IDENTIFIER_ROLE)\n            if convertor_identifier is not None:\n                convertor_identifiers.append(convertor_identifier)\n                continue\n\n            instance_id = index.data(INSTANCE_ID_ROLE)\n            if not context_selected and instance_id == CONTEXT_ID:\n                context_selected = True\n\n            elif instance_id is not None:\n                instance_ids.append(instance_id)\n\n        return instance_ids, context_selected, convertor_identifiers\n\n    def set_selected_items(\n        self, instance_ids, context_selected, convertor_identifiers\n    ):\n        s_instance_ids = set(instance_ids)\n        s_convertor_identifiers = set(convertor_identifiers)\n        cur_ids, cur_context, cur_convertor_identifiers = (\n            self.get_selected_items()\n        )\n        if (\n            set(cur_ids) == s_instance_ids\n            and cur_context == context_selected\n            and set(cur_convertor_identifiers) == s_convertor_identifiers\n        ):\n            return\n\n        view = self._instance_view\n        src_model = self._instance_model\n        proxy_model = self._proxy_model\n\n        select_indexes = []\n\n        select_queue = collections.deque()\n        select_queue.append(\n            (src_model.invisibleRootItem(), [])\n        )\n        while select_queue:\n            queue_item = select_queue.popleft()\n            item, parent_items = queue_item\n\n            if item.hasChildren():\n                new_parent_items = list(parent_items)\n                new_parent_items.append(item)\n                for row in range(item.rowCount()):\n                    select_queue.append(\n                        (item.child(row), list(new_parent_items))\n                    )\n\n            convertor_identifier = item.data(CONVERTER_IDENTIFIER_ROLE)\n\n            select = False\n            expand_parent = True\n            if convertor_identifier is not None:\n                if convertor_identifier in s_convertor_identifiers:\n                    select = True\n            else:\n                instance_id = item.data(INSTANCE_ID_ROLE)\n                if instance_id == CONTEXT_ID:\n                    if context_selected:\n                        select = True\n                        expand_parent = False\n\n                elif instance_id in s_instance_ids:\n                    select = True\n\n            if not select:\n                continue\n\n            select_indexes.append(item.index())\n            if not expand_parent:\n                continue\n\n            for parent_item in parent_items:\n                index = parent_item.index()\n                proxy_index = proxy_model.mapFromSource(index)\n                if not view.isExpanded(proxy_index):\n                    view.expand(proxy_index)\n\n        selection_model = view.selectionModel()\n        if not select_indexes:\n            selection_model.clear()\n            return\n\n        if len(select_indexes) == 1:\n            proxy_index = proxy_model.mapFromSource(select_indexes[0])\n            selection_model.setCurrentIndex(\n                proxy_index,\n                QtCore.QItemSelectionModel.ClearAndSelect\n                | QtCore.QItemSelectionModel.Rows\n            )\n            return\n\n        first_index = proxy_model.mapFromSource(select_indexes.pop(0))\n        last_index = proxy_model.mapFromSource(select_indexes.pop(-1))\n\n        selection_model.setCurrentIndex(\n            first_index,\n            QtCore.QItemSelectionModel.ClearAndSelect\n            | QtCore.QItemSelectionModel.Rows\n        )\n\n        for index in select_indexes:\n            proxy_index = proxy_model.mapFromSource(index)\n            selection_model.select(\n                proxy_index,\n                QtCore.QItemSelectionModel.Select\n                | QtCore.QItemSelectionModel.Rows\n            )\n\n        selection_model.setCurrentIndex(\n            last_index,\n            QtCore.QItemSelectionModel.Select\n            | QtCore.QItemSelectionModel.Rows\n        )\n\n    def set_active_toggle_enabled(self, enabled):\n        if self._active_toggle_enabled is enabled:\n            return\n\n        self._active_toggle_enabled = enabled\n        for widget in self._widgets_by_id.values():\n            if isinstance(widget, InstanceListItemWidget):\n                widget.set_active_toggle_enabled(enabled)\n\n        for widget in self._group_widgets.values():\n            if isinstance(widget, InstanceListGroupWidget):\n                widget.set_active_toggle_enabled(enabled)\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/overview_widget.py",
    "content": "from qtpy import QtWidgets, QtCore\n\nfrom openpype import AYON_SERVER_ENABLED\n\nfrom .border_label_widget import BorderedLabelWidget\n\nfrom .card_view_widgets import InstanceCardView\nfrom .list_view_widgets import InstanceListView\nfrom .widgets import (\n    SubsetAttributesWidget,\n    CreateInstanceBtn,\n    RemoveInstanceBtn,\n    ChangeViewBtn,\n)\nfrom .create_widget import CreateWidget\n\n\nclass OverviewWidget(QtWidgets.QFrame):\n    active_changed = QtCore.Signal()\n    instance_context_changed = QtCore.Signal()\n    create_requested = QtCore.Signal()\n    convert_requested = QtCore.Signal()\n\n    anim_end_value = 200\n    anim_duration = 200\n\n    def __init__(self, controller, parent):\n        super(OverviewWidget, self).__init__(parent)\n\n        self._refreshing_instances = False\n        self._controller = controller\n\n        subset_content_widget = QtWidgets.QWidget(self)\n\n        create_widget = CreateWidget(controller, subset_content_widget)\n\n        # --- Created Subsets/Instances ---\n        # Common widget for creation and overview\n        subset_views_widget = BorderedLabelWidget(\n            \"{} to publish\".format(\n                \"Products\" if AYON_SERVER_ENABLED else \"Subsets\"\n            ),\n            subset_content_widget\n        )\n\n        subset_view_cards = InstanceCardView(controller, subset_views_widget)\n        subset_list_view = InstanceListView(controller, subset_views_widget)\n\n        subset_views_layout = QtWidgets.QStackedLayout()\n        subset_views_layout.addWidget(subset_view_cards)\n        subset_views_layout.addWidget(subset_list_view)\n        subset_views_layout.setCurrentWidget(subset_view_cards)\n\n        # Buttons at the bottom of subset view\n        create_btn = CreateInstanceBtn(subset_views_widget)\n        delete_btn = RemoveInstanceBtn(subset_views_widget)\n        change_view_btn = ChangeViewBtn(subset_views_widget)\n\n        # --- Overview ---\n        # Subset details widget\n        subset_attributes_wrap = BorderedLabelWidget(\n            \"Publish options\", subset_content_widget\n        )\n        subset_attributes_widget = SubsetAttributesWidget(\n            controller, subset_attributes_wrap\n        )\n        subset_attributes_wrap.set_center_widget(subset_attributes_widget)\n\n        # Layout of buttons at the bottom of subset view\n        subset_view_btns_layout = QtWidgets.QHBoxLayout()\n        subset_view_btns_layout.setContentsMargins(0, 5, 0, 0)\n        subset_view_btns_layout.addWidget(create_btn)\n        subset_view_btns_layout.addSpacing(5)\n        subset_view_btns_layout.addWidget(delete_btn)\n        subset_view_btns_layout.addStretch(1)\n        subset_view_btns_layout.addWidget(change_view_btn)\n\n        # Layout of view and buttons\n        # - widget 'subset_view_widget' is necessary\n        # - only layout won't be resized automatically to minimum size hint\n        #   on child resize request!\n        subset_view_widget = QtWidgets.QWidget(subset_views_widget)\n        subset_view_layout = QtWidgets.QVBoxLayout(subset_view_widget)\n        subset_view_layout.setContentsMargins(0, 0, 0, 0)\n        subset_view_layout.addLayout(subset_views_layout, 1)\n        subset_view_layout.addLayout(subset_view_btns_layout, 0)\n\n        subset_views_widget.set_center_widget(subset_view_widget)\n\n        # Whole subset layout with attributes and details\n        subset_content_layout = QtWidgets.QHBoxLayout(subset_content_widget)\n        subset_content_layout.setContentsMargins(0, 0, 0, 0)\n        subset_content_layout.addWidget(create_widget, 7)\n        subset_content_layout.addWidget(subset_views_widget, 3)\n        subset_content_layout.addWidget(subset_attributes_wrap, 7)\n\n        # Subset frame layout\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(subset_content_widget, 1)\n\n        change_anim = QtCore.QVariantAnimation()\n        change_anim.setStartValue(float(0))\n        change_anim.setEndValue(float(self.anim_end_value))\n        change_anim.setDuration(self.anim_duration)\n        change_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad)\n\n        # --- Calbacks for instances/subsets view ---\n        create_btn.clicked.connect(self._on_create_clicked)\n        delete_btn.clicked.connect(self._on_delete_clicked)\n        change_view_btn.clicked.connect(self._on_change_view_clicked)\n\n        change_anim.valueChanged.connect(self._on_change_anim)\n        change_anim.finished.connect(self._on_change_anim_finished)\n\n        # Selection changed\n        subset_list_view.selection_changed.connect(\n            self._on_subset_change\n        )\n        subset_view_cards.selection_changed.connect(\n            self._on_subset_change\n        )\n        # Active instances changed\n        subset_list_view.active_changed.connect(\n            self._on_active_changed\n        )\n        subset_view_cards.active_changed.connect(\n            self._on_active_changed\n        )\n        # Instance context has changed\n        subset_attributes_widget.instance_context_changed.connect(\n            self._on_instance_context_change\n        )\n        subset_attributes_widget.convert_requested.connect(\n            self._on_convert_requested\n        )\n\n        # --- Controller callbacks ---\n        controller.event_system.add_callback(\n            \"publish.process.started\", self._on_publish_start\n        )\n        controller.event_system.add_callback(\n            \"controller.reset.started\", self._on_controller_reset_start\n        )\n        controller.event_system.add_callback(\n            \"publish.reset.finished\", self._on_publish_reset\n        )\n        controller.event_system.add_callback(\n            \"instances.refresh.finished\", self._on_instances_refresh\n        )\n\n        self._subset_content_widget = subset_content_widget\n        self._subset_content_layout = subset_content_layout\n\n        self._subset_view_cards = subset_view_cards\n        self._subset_list_view = subset_list_view\n        self._subset_views_layout = subset_views_layout\n\n        self._create_btn = create_btn\n        self._delete_btn = delete_btn\n\n        self._subset_attributes_widget = subset_attributes_widget\n        self._create_widget = create_widget\n        self._subset_views_widget = subset_views_widget\n        self._subset_attributes_wrap = subset_attributes_wrap\n\n        self._change_anim = change_anim\n\n        # Start in create mode\n        self._current_state = \"create\"\n        subset_attributes_wrap.setVisible(False)\n\n    def make_sure_animation_is_finished(self):\n        if self._change_anim.state() == QtCore.QAbstractAnimation.Running:\n            self._change_anim.stop()\n        self._on_change_anim_finished()\n\n    def set_state(self, new_state, animate):\n        if new_state == self._current_state:\n            return\n\n        self._current_state = new_state\n\n        if not animate:\n            self.make_sure_animation_is_finished()\n            return\n\n        if new_state == \"create\":\n            direction = QtCore.QAbstractAnimation.Backward\n        else:\n            direction = QtCore.QAbstractAnimation.Forward\n        self._change_anim.setDirection(direction)\n\n        if (\n            self._change_anim.state() != QtCore.QAbstractAnimation.Running\n        ):\n            self._start_animation()\n\n    def _start_animation(self):\n        views_geo = self._subset_views_widget.geometry()\n        layout_spacing = self._subset_content_layout.spacing()\n        if self._create_widget.isVisible():\n            create_geo = self._create_widget.geometry()\n            subset_geo = QtCore.QRect(create_geo)\n            subset_geo.moveTop(views_geo.top())\n            subset_geo.moveLeft(views_geo.right() + layout_spacing)\n            self._subset_attributes_wrap.setVisible(True)\n\n        elif self._subset_attributes_wrap.isVisible():\n            subset_geo = self._subset_attributes_wrap.geometry()\n            create_geo = QtCore.QRect(subset_geo)\n            create_geo.moveTop(views_geo.top())\n            create_geo.moveRight(views_geo.left() - (layout_spacing + 1))\n            self._create_widget.setVisible(True)\n        else:\n            self._change_anim.start()\n            return\n\n        while self._subset_content_layout.count():\n            self._subset_content_layout.takeAt(0)\n        self._subset_views_widget.setGeometry(views_geo)\n        self._create_widget.setGeometry(create_geo)\n        self._subset_attributes_wrap.setGeometry(subset_geo)\n\n        self._change_anim.start()\n\n    def get_subset_views_geo(self):\n        parent = self._subset_views_widget.parent()\n        global_pos = parent.mapToGlobal(self._subset_views_widget.pos())\n        return QtCore.QRect(\n            global_pos.x(),\n            global_pos.y(),\n            self._subset_views_widget.width(),\n            self._subset_views_widget.height()\n        )\n\n    def has_items(self):\n        view = self._subset_views_layout.currentWidget()\n        return view.has_items()\n\n    def _on_create_clicked(self):\n        \"\"\"Pass signal to parent widget which should care about changing state.\n\n        We don't change anything here until the parent will care about it.\n        \"\"\"\n\n        self.create_requested.emit()\n\n    def _on_delete_clicked(self):\n        instance_ids, _, _ = self.get_selected_items()\n\n        # Ask user if he really wants to remove instances\n        dialog = QtWidgets.QMessageBox(self)\n        dialog.setIcon(QtWidgets.QMessageBox.Question)\n        dialog.setWindowTitle(\"Are you sure?\")\n        if len(instance_ids) > 1:\n            msg = (\n                \"Do you really want to remove {} instances?\"\n            ).format(len(instance_ids))\n        else:\n            msg = (\n                \"Do you really want to remove the instance?\"\n            )\n        dialog.setText(msg)\n        dialog.setStandardButtons(\n            QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel\n        )\n        dialog.setDefaultButton(QtWidgets.QMessageBox.Ok)\n        dialog.setEscapeButton(QtWidgets.QMessageBox.Cancel)\n        dialog.exec_()\n        # Skip if OK was not clicked\n        if dialog.result() == QtWidgets.QMessageBox.Ok:\n            instance_ids = set(instance_ids)\n            self._controller.remove_instances(instance_ids)\n\n    def _on_change_view_clicked(self):\n        self._change_view_type()\n\n    def _on_subset_change(self, *_args):\n        # Ignore changes if in middle of refreshing\n        if self._refreshing_instances:\n            return\n\n        instance_ids, context_selected, convertor_identifiers = (\n            self.get_selected_items()\n        )\n\n        # Disable delete button if nothing is selected\n        self._delete_btn.setEnabled(len(instance_ids) > 0)\n\n        instances_by_id = self._controller.instances\n        instances = [\n            instances_by_id[instance_id]\n            for instance_id in instance_ids\n        ]\n        self._subset_attributes_widget.set_current_instances(\n            instances, context_selected, convertor_identifiers\n        )\n\n    def _on_active_changed(self):\n        if self._refreshing_instances:\n            return\n        self.active_changed.emit()\n\n    def _on_change_anim(self, value):\n        self._create_widget.setVisible(True)\n        self._subset_attributes_wrap.setVisible(True)\n        layout_spacing = self._subset_content_layout.spacing()\n\n        content_width = (\n            self._subset_content_widget.width() - (layout_spacing * 2)\n        )\n        content_height = self._subset_content_widget.height()\n        views_width = max(\n            int(content_width * 0.3),\n            self._subset_views_widget.minimumWidth()\n        )\n        width = content_width - views_width\n        # Visible widths of other widgets\n        subset_attrs_width = int((float(width) / self.anim_end_value) * value)\n        create_width = width - subset_attrs_width\n\n        views_geo = QtCore.QRect(\n            create_width + layout_spacing, 0,\n            views_width, content_height\n        )\n        create_geo = QtCore.QRect(0, 0, width, content_height)\n        subset_attrs_geo = QtCore.QRect(create_geo)\n        create_geo.moveRight(views_geo.left() - (layout_spacing + 1))\n        subset_attrs_geo.moveLeft(views_geo.right() + layout_spacing)\n\n        self._subset_views_widget.setGeometry(views_geo)\n        self._create_widget.setGeometry(create_geo)\n        self._subset_attributes_wrap.setGeometry(subset_attrs_geo)\n\n    def _on_change_anim_finished(self):\n        self._change_visibility_for_state()\n        self._subset_content_layout.addWidget(self._create_widget, 7)\n        self._subset_content_layout.addWidget(self._subset_views_widget, 3)\n        self._subset_content_layout.addWidget(self._subset_attributes_wrap, 7)\n\n    def _change_visibility_for_state(self):\n        self._create_widget.setVisible(\n            self._current_state == \"create\"\n        )\n        self._subset_attributes_wrap.setVisible(\n            self._current_state == \"publish\"\n        )\n\n    def _on_instance_context_change(self):\n        current_idx = self._subset_views_layout.currentIndex()\n        for idx in range(self._subset_views_layout.count()):\n            if idx == current_idx:\n                continue\n            widget = self._subset_views_layout.widget(idx)\n            if widget.refreshed:\n                widget.set_refreshed(False)\n\n        current_widget = self._subset_views_layout.widget(current_idx)\n        current_widget.refresh_instance_states()\n\n        self.instance_context_changed.emit()\n\n    def _on_convert_requested(self):\n        self.convert_requested.emit()\n\n    def get_selected_items(self):\n        \"\"\"Selected items in current view widget.\n\n        Returns:\n            tuple[list[str], bool, list[str]]: Selected items. List of\n                instance ids, context is selected, list of selected legacy\n                convertor plugins.\n        \"\"\"\n\n        view = self._subset_views_layout.currentWidget()\n        return view.get_selected_items()\n\n    def get_selected_legacy_convertors(self):\n        \"\"\"Selected legacy convertor identifiers.\n\n        Returns:\n            list[str]: Selected legacy convertor identifiers.\n                Example: ['io.openpype.creators.houdini.legacy']\n        \"\"\"\n\n        _, _, convertor_identifiers = self.get_selected_items()\n        return convertor_identifiers\n\n    def _change_view_type(self):\n        idx = self._subset_views_layout.currentIndex()\n        new_idx = (idx + 1) % self._subset_views_layout.count()\n\n        old_view = self._subset_views_layout.currentWidget()\n        new_view = self._subset_views_layout.widget(new_idx)\n\n        if not new_view.refreshed:\n            new_view.refresh()\n            new_view.set_refreshed(True)\n        else:\n            new_view.refresh_instance_states()\n\n        instance_ids, context_selected, convertor_identifiers = (\n            old_view.get_selected_items()\n        )\n        new_view.set_selected_items(\n            instance_ids, context_selected, convertor_identifiers\n        )\n\n        self._subset_views_layout.setCurrentIndex(new_idx)\n\n        self._on_subset_change()\n\n    def _refresh_instances(self):\n        if self._refreshing_instances:\n            return\n\n        self._refreshing_instances = True\n\n        for idx in range(self._subset_views_layout.count()):\n            widget = self._subset_views_layout.widget(idx)\n            widget.set_refreshed(False)\n\n        view = self._subset_views_layout.currentWidget()\n        view.refresh()\n        view.set_refreshed(True)\n\n        self._refreshing_instances = False\n\n        # Force to change instance and refresh details\n        self._on_subset_change()\n\n    def _on_publish_start(self):\n        \"\"\"Publish started.\"\"\"\n\n        self._create_btn.setEnabled(False)\n        self._subset_attributes_wrap.setEnabled(False)\n        for idx in range(self._subset_views_layout.count()):\n            widget = self._subset_views_layout.widget(idx)\n            widget.set_active_toggle_enabled(False)\n\n    def _on_controller_reset_start(self):\n        \"\"\"Controller reset started.\"\"\"\n\n        for idx in range(self._subset_views_layout.count()):\n            widget = self._subset_views_layout.widget(idx)\n            widget.set_active_toggle_enabled(True)\n\n    def _on_publish_reset(self):\n        \"\"\"Context in controller has been reseted.\"\"\"\n\n        self._create_btn.setEnabled(True)\n        self._subset_attributes_wrap.setEnabled(True)\n        self._subset_content_widget.setEnabled(self._controller.host_is_valid)\n\n    def _on_instances_refresh(self):\n        \"\"\"Controller refreshed instances.\"\"\"\n\n        self._refresh_instances()\n\n        # Give a change to process Resize Request\n        QtWidgets.QApplication.processEvents()\n        # Trigger update geometry of\n        widget = self._subset_views_layout.currentWidget()\n        widget.updateGeometry()\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/precreate_widget.py",
    "content": "from qtpy import QtWidgets, QtCore\n\nfrom openpype.tools.attribute_defs import create_widget_for_attr_def\n\nfrom ..constants import INPUTS_LAYOUT_HSPACING, INPUTS_LAYOUT_VSPACING\n\n\nclass PreCreateWidget(QtWidgets.QWidget):\n    def __init__(self, parent):\n        super(PreCreateWidget, self).__init__(parent)\n\n        # Precreate attribute defininitions of Creator\n        scroll_area = QtWidgets.QScrollArea(self)\n        contet_widget = QtWidgets.QWidget(scroll_area)\n        scroll_area.setWidget(contet_widget)\n        scroll_area.setWidgetResizable(True)\n\n        attributes_widget = AttributesWidget(contet_widget)\n        contet_layout = QtWidgets.QVBoxLayout(contet_widget)\n        contet_layout.setContentsMargins(0, 0, 0, 0)\n        contet_layout.addWidget(attributes_widget, 0)\n        contet_layout.addStretch(1)\n\n        # Widget showed when there are no attribute definitions from creator\n        empty_widget = QtWidgets.QWidget(self)\n        empty_widget.setVisible(False)\n\n        # Label showed when creator is not selected\n        no_creator_label = QtWidgets.QLabel(\n            \"Creator is not selected\",\n            empty_widget\n        )\n        no_creator_label.setWordWrap(True)\n\n        # Creator does not have precreate attributes\n        empty_label = QtWidgets.QLabel(\n            \"This creator has no configurable options\",\n            empty_widget\n        )\n        empty_label.setWordWrap(True)\n        empty_label.setVisible(False)\n\n        empty_layout = QtWidgets.QVBoxLayout(empty_widget)\n        empty_layout.setContentsMargins(0, 0, 0, 0)\n        empty_layout.addWidget(empty_label, 0, QtCore.Qt.AlignCenter)\n        empty_layout.addWidget(no_creator_label, 0, QtCore.Qt.AlignCenter)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(scroll_area, 1)\n        main_layout.addWidget(empty_widget, 1)\n\n        self._scroll_area = scroll_area\n        self._empty_widget = empty_widget\n\n        self._empty_label = empty_label\n        self._no_creator_label = no_creator_label\n        self._attributes_widget = attributes_widget\n\n    def current_value(self):\n        return self._attributes_widget.current_value()\n\n    def set_creator_item(self, creator_item):\n        attr_defs = []\n        creator_selected = False\n        if creator_item is not None:\n            creator_selected = True\n            attr_defs = creator_item.pre_create_attributes_defs\n\n        self._attributes_widget.set_attr_defs(attr_defs)\n\n        attr_defs_available = len(attr_defs) > 0\n        self._scroll_area.setVisible(attr_defs_available)\n        self._empty_widget.setVisible(not attr_defs_available)\n\n        self._empty_label.setVisible(creator_selected)\n        self._no_creator_label.setVisible(not creator_selected)\n\n\nclass AttributesWidget(QtWidgets.QWidget):\n    def __init__(self, parent=None):\n        super(AttributesWidget, self).__init__(parent)\n\n        layout = QtWidgets.QGridLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING)\n        layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING)\n\n        self._layout = layout\n\n        self._widgets = []\n\n    def current_value(self):\n        output = {}\n        for widget in self._widgets:\n            attr_def = widget.attr_def\n            if attr_def.is_value_def:\n                output[attr_def.key] = widget.current_value()\n        return output\n\n    def clear_attr_defs(self):\n        while self._layout.count():\n            item = self._layout.takeAt(0)\n            widget = item.widget()\n            if widget:\n                widget.setVisible(False)\n                widget.deleteLater()\n\n        self._widgets = []\n\n    def set_attr_defs(self, attr_defs):\n        self.clear_attr_defs()\n\n        row = 0\n        for attr_def in attr_defs:\n            widget = create_widget_for_attr_def(attr_def, self)\n\n            expand_cols = 2\n            if attr_def.is_value_def and attr_def.is_label_horizontal:\n                expand_cols = 1\n\n            col_num = 2 - expand_cols\n\n            if attr_def.is_value_def and attr_def.label:\n                label_widget = QtWidgets.QLabel(attr_def.label, self)\n                tooltip = attr_def.tooltip\n                if tooltip:\n                    label_widget.setToolTip(tooltip)\n                if attr_def.is_label_horizontal:\n                    label_widget.setAlignment(\n                        QtCore.Qt.AlignRight\n                        | QtCore.Qt.AlignVCenter\n                    )\n                self._layout.addWidget(\n                    label_widget, row, 0, 1, expand_cols\n                )\n                if not attr_def.is_label_horizontal:\n                    row += 1\n\n            self._layout.addWidget(\n                widget, row, col_num, 1, expand_cols\n            )\n            self._widgets.append(widget)\n\n            row += 1\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/publish_frame.py",
    "content": "import os\nimport json\nimport time\n\nfrom qtpy import QtWidgets, QtCore\n\nfrom .widgets import (\n    StopBtn,\n    ResetBtn,\n    ValidateBtn,\n    PublishBtn,\n    PublishReportBtn,\n)\n\n\nclass PublishFrame(QtWidgets.QWidget):\n    \"\"\"Frame showed during publishing.\n\n    Shows all information related to publishing. Contains validation error\n    widget which is showed if only validation error happens during validation.\n\n    Processing layer is default layer. Validation error layer is shown if only\n    validation exception is raised during publishing. Report layer is available\n    only when publishing process is stopped and must be manually triggered to\n    change into that layer.\n\n    +------------------------------------------------------------------------+\n    |                             < Main label >                             |\n    |                             < Label top >                              |\n    |        (####                10%  <Progress bar>                )       |\n    | <Instance label>                                        <Plugin label> |\n    | <Report>                              <Reset><Stop><Validate><Publish> |\n    +------------------------------------------------------------------------+\n    \"\"\"\n\n    details_page_requested = QtCore.Signal()\n\n    def __init__(self, controller, borders, parent):\n        super(PublishFrame, self).__init__(parent)\n\n        # Bottom part of widget where process and callback buttons are showed\n        # - QFrame used to be able set background using stylesheets easily\n        #   and not override all children widgets style\n        content_frame = QtWidgets.QFrame(self)\n        content_frame.setObjectName(\"PublishInfoFrame\")\n\n        top_content_widget = QtWidgets.QWidget(content_frame)\n\n        # Center widget displaying current state (without any specific info)\n        main_label = QtWidgets.QLabel(top_content_widget)\n        main_label.setObjectName(\"PublishInfoMainLabel\")\n        main_label.setAlignment(QtCore.Qt.AlignCenter)\n\n        # Supporting labels for main label\n        # Top label is displayed just under main label\n        message_label_top = QtWidgets.QLabel(top_content_widget)\n        message_label_top.setAlignment(QtCore.Qt.AlignCenter)\n\n        # Label showing currently processed instance\n        progress_widget = QtWidgets.QWidget(top_content_widget)\n        instance_plugin_widget = QtWidgets.QWidget(progress_widget)\n        instance_label = QtWidgets.QLabel(\n            \"<Instance name>\", instance_plugin_widget\n        )\n        instance_label.setAlignment(\n            QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter\n        )\n        # Label showing currently processed plugin\n        plugin_label = QtWidgets.QLabel(\n            \"<Plugin name>\", instance_plugin_widget\n        )\n        plugin_label.setAlignment(\n            QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter\n        )\n        instance_plugin_layout = QtWidgets.QHBoxLayout(instance_plugin_widget)\n        instance_plugin_layout.setContentsMargins(0, 0, 0, 0)\n        instance_plugin_layout.addWidget(instance_label, 1)\n        instance_plugin_layout.addWidget(plugin_label, 1)\n\n        # Progress bar showing progress of publishing\n        progress_bar = QtWidgets.QProgressBar(progress_widget)\n        progress_bar.setObjectName(\"PublishProgressBar\")\n\n        progress_layout = QtWidgets.QVBoxLayout(progress_widget)\n        progress_layout.setSpacing(5)\n        progress_layout.setContentsMargins(0, 0, 0, 0)\n        progress_layout.addWidget(instance_plugin_widget, 0)\n        progress_layout.addWidget(progress_bar, 0)\n\n        top_content_layout = QtWidgets.QVBoxLayout(top_content_widget)\n        top_content_layout.setContentsMargins(0, 0, 0, 0)\n        top_content_layout.setSpacing(5)\n        top_content_layout.setAlignment(QtCore.Qt.AlignCenter)\n        top_content_layout.addWidget(main_label)\n        # TODO stretches should be probably replaced by spacing...\n        # - stretch in floating frame doesn't make sense\n        top_content_layout.addWidget(message_label_top)\n        top_content_layout.addWidget(progress_widget)\n\n        # Publishing buttons to stop, reset or trigger publishing\n        footer_widget = QtWidgets.QWidget(content_frame)\n\n        report_btn = PublishReportBtn(footer_widget)\n\n        shrunk_main_label = QtWidgets.QLabel(footer_widget)\n        shrunk_main_label.setObjectName(\"PublishInfoMainLabel\")\n        shrunk_main_label.setAlignment(\n            QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft\n        )\n\n        reset_btn = ResetBtn(footer_widget)\n        stop_btn = StopBtn(footer_widget)\n        validate_btn = ValidateBtn(footer_widget)\n        publish_btn = PublishBtn(footer_widget)\n\n        report_btn.add_action(\"Go to details\", \"go_to_report\")\n        report_btn.add_action(\"Copy report\", \"copy_report\")\n        report_btn.add_action(\"Export report\", \"export_report\")\n\n        # Footer on info frame layout\n        footer_layout = QtWidgets.QHBoxLayout(footer_widget)\n        footer_layout.setContentsMargins(0, 0, 0, 0)\n        footer_layout.addWidget(report_btn, 0)\n        footer_layout.addWidget(shrunk_main_label, 1)\n        footer_layout.addWidget(reset_btn, 0)\n        footer_layout.addWidget(stop_btn, 0)\n        footer_layout.addWidget(validate_btn, 0)\n        footer_layout.addWidget(publish_btn, 0)\n\n        # Info frame content\n        content_layout = QtWidgets.QVBoxLayout(content_frame)\n        content_layout.setSpacing(5)\n\n        content_layout.addWidget(top_content_widget)\n        content_layout.addWidget(footer_widget)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(borders, 0, borders, borders)\n        main_layout.addWidget(content_frame)\n\n        shrunk_anim = QtCore.QVariantAnimation()\n        shrunk_anim.setDuration(140)\n        shrunk_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad)\n\n        # Force translucent background for widgets\n        for widget in (\n            self,\n            top_content_widget,\n            footer_widget,\n            progress_widget,\n            instance_plugin_widget,\n        ):\n            widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        report_btn.triggered.connect(self._on_report_triggered)\n        reset_btn.clicked.connect(self._on_reset_clicked)\n        stop_btn.clicked.connect(self._on_stop_clicked)\n        validate_btn.clicked.connect(self._on_validate_clicked)\n        publish_btn.clicked.connect(self._on_publish_clicked)\n\n        shrunk_anim.valueChanged.connect(self._on_shrunk_anim)\n        shrunk_anim.finished.connect(self._on_shrunk_anim_finish)\n\n        controller.event_system.add_callback(\n            \"publish.reset.finished\", self._on_publish_reset\n        )\n        controller.event_system.add_callback(\n            \"publish.process.started\", self._on_publish_start\n        )\n        controller.event_system.add_callback(\n            \"publish.has_validated.changed\", self._on_publish_validated_change\n        )\n        controller.event_system.add_callback(\n            \"publish.process.stopped\", self._on_publish_stop\n        )\n\n        controller.event_system.add_callback(\n            \"publish.process.instance.changed\", self._on_instance_change\n        )\n        controller.event_system.add_callback(\n            \"publish.process.plugin.changed\", self._on_plugin_change\n        )\n\n        self._shrunk_anim = shrunk_anim\n\n        self._controller = controller\n\n        self._content_frame = content_frame\n        self._content_layout = content_layout\n        self._top_content_layout = top_content_layout\n        self._top_content_widget = top_content_widget\n\n        self._main_label = main_label\n        self._message_label_top = message_label_top\n\n        self._instance_label = instance_label\n        self._plugin_label = plugin_label\n\n        self._progress_bar = progress_bar\n        self._progress_widget = progress_widget\n\n        self._shrunk_main_label = shrunk_main_label\n        self._reset_btn = reset_btn\n        self._stop_btn = stop_btn\n        self._validate_btn = validate_btn\n        self._publish_btn = publish_btn\n\n        self._shrunken = False\n        self._top_widget_max_height = None\n        self._top_widget_size_policy = top_content_widget.sizePolicy()\n        self._last_instance_label = None\n        self._last_plugin_label = None\n\n    def mouseReleaseEvent(self, event):\n        super(PublishFrame, self).mouseReleaseEvent(event)\n        self._change_shrunk_state()\n\n    def _change_shrunk_state(self):\n        self.set_shrunk_state(not self._shrunken)\n\n    def set_shrunk_state(self, shrunk):\n        if shrunk is self._shrunken:\n            return\n\n        if self._top_widget_max_height is None:\n            self._top_widget_max_height = (\n                self._top_content_widget.maximumHeight()\n            )\n\n        self._shrunken = shrunk\n\n        anim_is_running = (\n            self._shrunk_anim.state() == QtCore.QAbstractAnimation.Running\n        )\n        if not self.isVisible():\n            if anim_is_running:\n                self._shrunk_anim.stop()\n            self._on_shrunk_anim_finish()\n            return\n\n        start = 0\n        end = 0\n        if shrunk:\n            start = self._top_content_widget.height()\n        else:\n            if anim_is_running:\n                start = self._shrunk_anim.currentValue()\n            hint = self._top_content_widget.minimumSizeHint()\n            end = hint.height()\n\n        self._shrunk_anim.setStartValue(float(start))\n        self._shrunk_anim.setEndValue(float(end))\n        if not anim_is_running:\n            self._shrunk_anim.start()\n\n    def _on_shrunk_anim(self, value):\n        diff = self._top_content_widget.height() - int(value)\n        if not self._top_content_widget.isVisible():\n            diff -= self._content_layout.spacing()\n\n        window_pos = self.pos()\n        window_pos_y = window_pos.y() + diff\n        window_height = self.height() - diff\n\n        self._top_content_widget.setMinimumHeight(value)\n        self._top_content_widget.setMaximumHeight(value)\n        self._top_content_widget.setVisible(True)\n\n        self.resize(self.width(), window_height)\n        self.move(window_pos.x(), window_pos_y)\n\n    def _on_shrunk_anim_finish(self):\n        self._top_content_widget.setVisible(not self._shrunken)\n        self._top_content_widget.setMinimumHeight(0)\n        self._top_content_widget.setMaximumHeight(\n            self._top_widget_max_height\n        )\n        self._top_content_widget.setSizePolicy(self._top_widget_size_policy)\n\n        if self._shrunken:\n            self._shrunk_main_label.setText(self._main_label.text())\n        else:\n            self._shrunk_main_label.setText(\"\")\n\n        if self._shrunken:\n            content_frame_hint = self._content_frame.sizeHint()\n\n            layout = self.layout()\n            margins = layout.contentsMargins()\n            window_height = (\n                content_frame_hint.height()\n                + margins.bottom()\n                + margins.top()\n            )\n            diff = self.height() - window_height\n            window_pos = self.pos()\n            window_pos_y = window_pos.y() + diff\n            self.resize(self.width(), window_height)\n            self.move(window_pos.x(), window_pos_y)\n\n    def _set_main_label(self, message):\n        self._main_label.setText(message)\n        if self._shrunken:\n            self._shrunk_main_label.setText(message)\n\n    def _on_publish_reset(self):\n        self._last_instance_label = None\n        self._last_plugin_label = None\n\n        self._set_success_property()\n        self._set_progress_visibility(True)\n\n        self._main_label.setText(\"\")\n        self._message_label_top.setText(\"\")\n\n        self._reset_btn.setEnabled(True)\n        self._stop_btn.setEnabled(False)\n        self._validate_btn.setEnabled(True)\n        self._publish_btn.setEnabled(True)\n\n        self._progress_bar.setValue(self._controller.publish_progress)\n        self._progress_bar.setMaximum(self._controller.publish_max_progress)\n\n    def _on_publish_start(self):\n        if self._last_plugin_label:\n            self._plugin_label.setText(self._last_plugin_label)\n\n        if self._last_instance_label:\n            self._instance_label.setText(self._last_instance_label)\n\n        self._set_success_property(3)\n        self._set_progress_visibility(True)\n        self._set_main_label(\"Publishing...\")\n        self._message_label_top.setText(\"\")\n\n        self._reset_btn.setEnabled(False)\n        self._stop_btn.setEnabled(True)\n        self._validate_btn.setEnabled(False)\n        self._publish_btn.setEnabled(False)\n\n        self.set_shrunk_state(False)\n\n    def _on_publish_validated_change(self, event):\n        if event[\"value\"]:\n            self._validate_btn.setEnabled(False)\n\n    def _on_instance_change(self, event):\n        \"\"\"Change instance label when instance is going to be processed.\"\"\"\n\n        self._last_instance_label = event[\"instance_label\"]\n        self._instance_label.setText(event[\"instance_label\"])\n        QtWidgets.QApplication.processEvents()\n\n    def _on_plugin_change(self, event):\n        \"\"\"Change plugin label when instance is going to be processed.\"\"\"\n\n        self._last_plugin_label = event[\"plugin_label\"]\n        self._progress_bar.setValue(self._controller.publish_progress)\n        self._plugin_label.setText(event[\"plugin_label\"])\n        QtWidgets.QApplication.processEvents()\n\n    def _on_publish_stop(self):\n        self._progress_bar.setValue(self._controller.publish_progress)\n\n        self._reset_btn.setEnabled(True)\n        self._stop_btn.setEnabled(False)\n\n        self._instance_label.setText(\"\")\n        self._plugin_label.setText(\"\")\n\n        validate_enabled = not self._controller.publish_has_crashed\n        publish_enabled = not self._controller.publish_has_crashed\n        if validate_enabled:\n            validate_enabled = not self._controller.publish_has_validated\n        if publish_enabled:\n            if (\n                self._controller.publish_has_validated\n                and self._controller.publish_has_validation_errors\n            ):\n                publish_enabled = False\n\n            else:\n                publish_enabled = not self._controller.publish_has_finished\n\n        self._validate_btn.setEnabled(validate_enabled)\n        self._publish_btn.setEnabled(publish_enabled)\n\n        if self._controller.publish_has_crashed:\n            self._set_error_msg()\n\n        elif self._controller.publish_has_validation_errors:\n            self._set_progress_visibility(False)\n            self._set_validation_errors()\n\n        elif self._controller.publish_has_finished:\n            self._set_finished()\n\n        else:\n            self._set_stopped()\n\n    def _set_stopped(self):\n        main_label = \"Publish paused\"\n        if self._controller.publish_has_validated:\n            main_label += \" - Validation passed\"\n\n        self._set_main_label(main_label)\n        self._message_label_top.setText(\n            \"Hit publish (play button) to continue.\"\n        )\n\n        self._set_success_property(4)\n\n    def _set_error_msg(self):\n        \"\"\"Show error message to artist on publish crash.\"\"\"\n\n        self._set_main_label(\"Error happened\")\n\n        self._message_label_top.setText(self._controller.publish_error_msg)\n\n        self._set_success_property(1)\n\n    def _set_validation_errors(self):\n        self._set_main_label(\"Your publish didn't pass studio validations\")\n        self._message_label_top.setText(\"Check results above please\")\n        self._set_success_property(2)\n\n    def _set_finished(self):\n        self._set_main_label(\"Finished\")\n        self._message_label_top.setText(\"\")\n        self._set_success_property(0)\n\n    def _set_progress_visibility(self, visible):\n        window_height = self.height()\n        self._progress_widget.setVisible(visible)\n        # Ignore rescaling and move of widget if is shrunken of progress bar\n        #   should be visible\n        if self._shrunken or visible:\n            return\n\n        height = self._progress_widget.height()\n        diff = height + self._top_content_layout.spacing()\n\n        window_pos = self.pos()\n        window_pos_y = self.pos().y() + diff\n        window_height -= diff\n\n        self.resize(self.width(), window_height)\n        self.move(window_pos.x(), window_pos_y)\n\n    def _set_success_property(self, state=None):\n        \"\"\"Apply styles by state.\n\n        State enum:\n        - None - Default state after restart\n        - 0 - Success finish\n        - 1 - Error happened\n        - 2 - Validation error\n        - 3 - In progress\n        - 4 - Stopped/Paused\n        \"\"\"\n\n        if state is None:\n            state = \"\"\n        else:\n            state = str(state)\n\n        for widget in (self._progress_bar, self._content_frame):\n            if widget.property(\"state\") != state:\n                widget.setProperty(\"state\", state)\n                widget.style().polish(widget)\n\n    def _on_report_triggered(self, identifier):\n        if identifier == \"export_report\":\n            self._controller.event_system.emit(\n                \"export_report.request\", {}, \"publish_frame\")\n\n        elif identifier == \"copy_report\":\n            self._controller.event_system.emit(\n                \"copy_report.request\", {}, \"publish_frame\")\n\n        elif identifier == \"go_to_report\":\n            self.details_page_requested.emit()\n\n    def _on_reset_clicked(self):\n        self._controller.reset()\n\n    def _on_stop_clicked(self):\n        self._controller.stop_publish()\n\n    def _on_validate_clicked(self):\n        self._controller.validate()\n\n    def _on_publish_clicked(self):\n        self._controller.publish()\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/report_page.py",
    "content": "# -*- coding: utf-8 -*-\nimport collections\nimport logging\n\ntry:\n    import commonmark\nexcept Exception:\n    commonmark = None\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.style import get_objected_colors\nfrom openpype.tools.utils import (\n    BaseClickableFrame,\n    ClickableFrame,\n    ExpandingTextEdit,\n    FlowLayout,\n    ClassicExpandBtn,\n    paint_image_with_color,\n    SeparatorWidget,\n)\nfrom .widgets import IconValuePixmapLabel\nfrom .icons import (\n    get_pixmap,\n    get_image,\n)\nfrom ..constants import (\n    INSTANCE_ID_ROLE,\n    CONTEXT_ID,\n    CONTEXT_LABEL,\n)\n\nLOG_DEBUG_VISIBLE = 1 << 0\nLOG_INFO_VISIBLE = 1 << 1\nLOG_WARNING_VISIBLE = 1 << 2\nLOG_ERROR_VISIBLE = 1 << 3\nLOG_CRITICAL_VISIBLE = 1 << 4\nERROR_VISIBLE = 1 << 5\nINFO_VISIBLE = 1 << 6\n\n\nclass VerticalScrollArea(QtWidgets.QScrollArea):\n    \"\"\"Scroll area for validation error titles.\n\n    The biggest difference is that the scroll area has scroll bar on left side\n    and resize of content will also resize scrollarea itself.\n\n    Resize if deferred by 100ms because at the moment of resize are not yet\n    propagated sizes and visibility of scroll bars.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(VerticalScrollArea, self).__init__(*args, **kwargs)\n\n        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)\n        self.setLayoutDirection(QtCore.Qt.RightToLeft)\n\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        # Background of scrollbar will be transparent\n        scrollbar_bg = self.verticalScrollBar().parent()\n        if scrollbar_bg:\n            scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        self.setViewportMargins(0, 0, 0, 0)\n\n        self.verticalScrollBar().installEventFilter(self)\n\n        # Timer with 100ms offset after changing size\n        size_changed_timer = QtCore.QTimer()\n        size_changed_timer.setInterval(100)\n        size_changed_timer.setSingleShot(True)\n\n        size_changed_timer.timeout.connect(self._on_timer_timeout)\n        self._size_changed_timer = size_changed_timer\n\n    def setVerticalScrollBar(self, widget):\n        old_widget = self.verticalScrollBar()\n        if old_widget:\n            old_widget.removeEventFilter(self)\n\n        super(VerticalScrollArea, self).setVerticalScrollBar(widget)\n        if widget:\n            widget.installEventFilter(self)\n\n    def setWidget(self, widget):\n        old_widget = self.widget()\n        if old_widget:\n            old_widget.removeEventFilter(self)\n\n        super(VerticalScrollArea, self).setWidget(widget)\n        if widget:\n            widget.installEventFilter(self)\n\n    def _on_timer_timeout(self):\n        width = self.widget().width()\n        if self.verticalScrollBar().isVisible():\n            width += self.verticalScrollBar().width()\n        self.setMinimumWidth(width)\n\n    def eventFilter(self, obj, event):\n        if (\n            event.type() == QtCore.QEvent.Resize\n            and (obj is self.widget() or obj is self.verticalScrollBar())\n        ):\n            self._size_changed_timer.start()\n        return super(VerticalScrollArea, self).eventFilter(obj, event)\n\n\n# --- Publish actions widget ---\nclass ActionButton(BaseClickableFrame):\n    \"\"\"Plugin's action callback button.\n\n    Action may have label or icon or both.\n\n    Args:\n        plugin_action_item (PublishPluginActionItem): Action item that can be\n            triggered by its id.\n    \"\"\"\n\n    action_clicked = QtCore.Signal(str, str)\n\n    def __init__(self, plugin_action_item, parent):\n        super(ActionButton, self).__init__(parent)\n\n        self.setObjectName(\"ValidationActionButton\")\n\n        self.plugin_action_item = plugin_action_item\n\n        action_label = plugin_action_item.label\n        action_icon = plugin_action_item.icon\n        label_widget = QtWidgets.QLabel(action_label, self)\n        icon_label = None\n        if action_icon:\n            icon_label = IconValuePixmapLabel(action_icon, self)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(5, 0, 5, 0)\n        layout.addWidget(label_widget, 1)\n        if icon_label:\n            layout.addWidget(icon_label, 0)\n\n        self.setSizePolicy(\n            QtWidgets.QSizePolicy.Minimum,\n            self.sizePolicy().verticalPolicy()\n        )\n\n    def _mouse_release_callback(self):\n        self.action_clicked.emit(\n            self.plugin_action_item.plugin_id,\n            self.plugin_action_item.action_id\n        )\n\n\nclass ValidateActionsWidget(QtWidgets.QFrame):\n    \"\"\"Wrapper widget for plugin actions.\n\n    Change actions based on selected validation error.\n    \"\"\"\n\n    def __init__(self, controller, parent):\n        super(ValidateActionsWidget, self).__init__(parent)\n\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        content_widget = QtWidgets.QWidget(self)\n        content_layout = FlowLayout(content_widget)\n        content_layout.setContentsMargins(0, 0, 0, 0)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(content_widget)\n\n        self._controller = controller\n        self._content_widget = content_widget\n        self._content_layout = content_layout\n\n        self._actions_mapping = {}\n\n        self._visible_mode = True\n\n    def _update_visibility(self):\n        self.setVisible(\n            self._visible_mode\n            and self._content_layout.count() > 0\n        )\n\n    def set_visible_mode(self, visible):\n        if self._visible_mode is visible:\n            return\n        self._visible_mode = visible\n        self._update_visibility()\n\n    def _clear(self):\n        \"\"\"Remove actions from widget.\"\"\"\n        while self._content_layout.count():\n            item = self._content_layout.takeAt(0)\n            widget = item.widget()\n            if widget:\n                widget.setVisible(False)\n                widget.deleteLater()\n        self._actions_mapping = {}\n\n    def set_error_info(self, error_info):\n        \"\"\"Set selected plugin and show it's actions.\n\n        Clears current actions from widget and recreate them from the plugin.\n\n        Args:\n            Dict[str, Any]: Object holding error items, title and possible\n                actions to run.\n        \"\"\"\n\n        self._clear()\n\n        if not error_info:\n            self.setVisible(False)\n            return\n\n        plugin_action_items = error_info[\"plugin_action_items\"]\n        for plugin_action_item in plugin_action_items:\n            if not plugin_action_item.active:\n                continue\n\n            if plugin_action_item.on_filter not in (\"failed\", \"all\"):\n                continue\n\n            action_id = plugin_action_item.action_id\n            self._actions_mapping[action_id] = plugin_action_item\n\n            action_btn = ActionButton(plugin_action_item, self._content_widget)\n            action_btn.action_clicked.connect(self._on_action_click)\n            self._content_layout.addWidget(action_btn)\n\n        self._update_visibility()\n\n    def _on_action_click(self, plugin_id, action_id):\n        self._controller.run_action(plugin_id, action_id)\n\n\n# --- Validation error titles ---\nclass ValidationErrorInstanceList(QtWidgets.QListView):\n    \"\"\"List of publish instances that caused a validation error.\n\n    Instances are collected per plugin's validation error title.\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        super(ValidationErrorInstanceList, self).__init__(*args, **kwargs)\n\n        self.setObjectName(\"ValidationErrorInstanceList\")\n\n        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)\n\n    def minimumSizeHint(self):\n        return self.sizeHint()\n\n    def sizeHint(self):\n        result = super(ValidationErrorInstanceList, self).sizeHint()\n        row_count = self.model().rowCount()\n        height = 0\n        if row_count > 0:\n            height = self.sizeHintForRow(0) * row_count\n        result.setHeight(height)\n        return result\n\n\nclass ValidationErrorTitleWidget(QtWidgets.QWidget):\n    \"\"\"Title of validation error.\n\n    Widget is used as radio button so requires clickable functionality and\n    changing style on selection/deselection.\n\n    Has toggle button to show/hide instances on which validation error happened\n    if there is a list (Valdation error may happen on context).\n    \"\"\"\n\n    selected = QtCore.Signal(str)\n    instance_changed = QtCore.Signal(str)\n\n    def __init__(self, title_id, error_info, parent):\n        super(ValidationErrorTitleWidget, self).__init__(parent)\n\n        self._title_id = title_id\n        self._error_info = error_info\n        self._selected = False\n\n        title_frame = ClickableFrame(self)\n        title_frame.setObjectName(\"ValidationErrorTitleFrame\")\n\n        toggle_instance_btn = QtWidgets.QToolButton(title_frame)\n        toggle_instance_btn.setObjectName(\"ArrowBtn\")\n        toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)\n        toggle_instance_btn.setMaximumWidth(14)\n\n        label_widget = QtWidgets.QLabel(error_info[\"title\"], title_frame)\n\n        title_frame_layout = QtWidgets.QHBoxLayout(title_frame)\n        title_frame_layout.addWidget(label_widget, 1)\n        title_frame_layout.addWidget(toggle_instance_btn, 0)\n\n        instances_model = QtGui.QStandardItemModel()\n\n        instance_ids = []\n\n        items = []\n        context_validation = False\n        for error_item in error_info[\"error_items\"]:\n            context_validation = error_item.context_validation\n            if context_validation:\n                toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow)\n                instance_ids.append(CONTEXT_ID)\n                # Add fake item to have minimum size hint of view widget\n                items.append(QtGui.QStandardItem(CONTEXT_LABEL))\n                continue\n\n            label = error_item.instance_label\n            item = QtGui.QStandardItem(label)\n            item.setFlags(\n                QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable\n            )\n            item.setData(label, QtCore.Qt.ToolTipRole)\n            item.setData(error_item.instance_id, INSTANCE_ID_ROLE)\n            items.append(item)\n            instance_ids.append(error_item.instance_id)\n\n        if items:\n            root_item = instances_model.invisibleRootItem()\n            root_item.appendRows(items)\n\n        instances_view = ValidationErrorInstanceList(self)\n        instances_view.setModel(instances_model)\n\n        self.setLayoutDirection(QtCore.Qt.LeftToRight)\n\n        view_widget = QtWidgets.QWidget(self)\n        view_layout = QtWidgets.QHBoxLayout(view_widget)\n        view_layout.setContentsMargins(0, 0, 0, 0)\n        view_layout.setSpacing(0)\n        view_layout.addSpacing(14)\n        view_layout.addWidget(instances_view, 0)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setSpacing(0)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(title_frame, 0)\n        layout.addWidget(view_widget, 0)\n        view_widget.setVisible(False)\n\n        if not context_validation:\n            toggle_instance_btn.clicked.connect(self._on_toggle_btn_click)\n\n        title_frame.clicked.connect(self._mouse_release_callback)\n        instances_view.selectionModel().selectionChanged.connect(\n            self._on_selection_change\n        )\n\n        self._title_frame = title_frame\n\n        self._toggle_instance_btn = toggle_instance_btn\n\n        self._view_widget = view_widget\n\n        self._instances_model = instances_model\n        self._instances_view = instances_view\n\n        self._context_validation = context_validation\n\n        self._instance_ids = instance_ids\n        self._expanded = False\n\n    def sizeHint(self):\n        result = super(ValidationErrorTitleWidget, self).sizeHint()\n        expected_width = max(\n            self._view_widget.minimumSizeHint().width(),\n            self._view_widget.sizeHint().width()\n        )\n\n        if expected_width < 200:\n            expected_width = 200\n\n        if result.width() < expected_width:\n            result.setWidth(expected_width)\n\n        return result\n\n    def minimumSizeHint(self):\n        return self.sizeHint()\n\n    def _mouse_release_callback(self):\n        \"\"\"Mark this widget as selected on click.\"\"\"\n\n        self.set_selected(True)\n\n    @property\n    def is_selected(self):\n        \"\"\"Is widget marked a selected.\n\n        Returns:\n            bool: Item is selected or not.\n        \"\"\"\n\n        return self._selected\n\n    @property\n    def id(self):\n        return self._title_id\n\n    def _change_style_property(self, selected):\n        \"\"\"Change style of widget based on selection.\"\"\"\n\n        value = \"1\" if selected else \"\"\n        self._title_frame.setProperty(\"selected\", value)\n        self._title_frame.style().polish(self._title_frame)\n\n    def set_selected(self, selected=None):\n        \"\"\"Change selected state of widget.\"\"\"\n\n        if selected is None:\n            selected = not self._selected\n\n        # Clear instance view selection on deselect\n        if not selected:\n            self._instances_view.clearSelection()\n\n        # Skip if has same value\n        if selected == self._selected:\n            return\n\n        self._selected = selected\n        self._change_style_property(selected)\n        if selected:\n            self.selected.emit(self._title_id)\n            self._set_expanded(True)\n\n    def _on_toggle_btn_click(self):\n        \"\"\"Show/hide instances list.\"\"\"\n\n        self._set_expanded()\n\n    def _set_expanded(self, expanded=None):\n        if expanded is None:\n            expanded = not self._expanded\n\n        elif expanded is self._expanded:\n            return\n\n        if expanded and self._context_validation:\n            return\n\n        self._expanded = expanded\n        self._view_widget.setVisible(expanded)\n        if expanded:\n            self._toggle_instance_btn.setArrowType(QtCore.Qt.DownArrow)\n        else:\n            self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)\n\n    def _on_selection_change(self):\n        self.instance_changed.emit(self._title_id)\n\n    def get_selected_instances(self):\n        if self._context_validation:\n            return [CONTEXT_ID]\n        sel_model = self._instances_view.selectionModel()\n        return [\n            index.data(INSTANCE_ID_ROLE)\n            for index in sel_model.selectedIndexes()\n            if index.isValid()\n        ]\n\n    def get_available_instances(self):\n        return list(self._instance_ids)\n\n\nclass ValidationArtistMessage(QtWidgets.QWidget):\n    def __init__(self, message, parent):\n        super(ValidationArtistMessage, self).__init__(parent)\n\n        artist_msg_label = QtWidgets.QLabel(message, self)\n        artist_msg_label.setAlignment(QtCore.Qt.AlignCenter)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(\n            artist_msg_label, 1, QtCore.Qt.AlignCenter\n        )\n\n\nclass ValidationErrorsView(QtWidgets.QWidget):\n    selection_changed = QtCore.Signal()\n\n    def __init__(self, parent):\n        super(ValidationErrorsView, self).__init__(parent)\n\n        errors_scroll = VerticalScrollArea(self)\n        errors_scroll.setWidgetResizable(True)\n\n        errors_widget = QtWidgets.QWidget(errors_scroll)\n        errors_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        errors_scroll.setWidget(errors_widget)\n\n        errors_layout = QtWidgets.QVBoxLayout(errors_widget)\n        errors_layout.setContentsMargins(0, 0, 0, 0)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(errors_scroll, 1)\n\n        self._errors_widget = errors_widget\n        self._errors_layout = errors_layout\n        self._title_widgets = {}\n        self._previous_select = None\n\n    def _clear(self):\n        \"\"\"Delete all dynamic widgets and hide all wrappers.\"\"\"\n\n        self._title_widgets = {}\n        self._previous_select = None\n        while self._errors_layout.count():\n            item = self._errors_layout.takeAt(0)\n            widget = item.widget()\n            if widget:\n                widget.deleteLater()\n\n    def set_errors(self, grouped_error_items):\n        \"\"\"Set errors into context and created titles.\n\n        Args:\n            validation_error_report (PublishValidationErrorsReport): Report\n                with information about validation errors and publish plugin\n                actions.\n        \"\"\"\n\n        self._clear()\n\n        first_id = None\n        for title_item in grouped_error_items:\n            title_id = title_item[\"id\"]\n            if first_id is None:\n                first_id = title_id\n            widget = ValidationErrorTitleWidget(title_id, title_item, self)\n            widget.selected.connect(self._on_select)\n            widget.instance_changed.connect(self._on_instance_change)\n            self._errors_layout.addWidget(widget)\n            self._title_widgets[title_id] = widget\n\n        self._errors_layout.addStretch(1)\n\n        if first_id:\n            self._title_widgets[first_id].set_selected(True)\n        else:\n            self.selection_changed.emit()\n\n        self.updateGeometry()\n\n    def _on_select(self, title_id):\n        if self._previous_select:\n            if self._previous_select.id == title_id:\n                return\n            self._previous_select.set_selected(False)\n\n        self._previous_select = self._title_widgets[title_id]\n        self.selection_changed.emit()\n\n    def _on_instance_change(self, title_id):\n        if self._previous_select and self._previous_select.id != title_id:\n            self._title_widgets[title_id].set_selected(True)\n        else:\n            self.selection_changed.emit()\n\n    def get_selected_items(self):\n        if not self._previous_select:\n            return None, []\n\n        title_id = self._previous_select.id\n        instance_ids = self._previous_select.get_selected_instances()\n        if not instance_ids:\n            instance_ids = self._previous_select.get_available_instances()\n        return title_id, instance_ids\n\n\n# ----- Publish instance report -----\nclass _InstanceItem:\n    \"\"\"Publish instance item for report UI.\n\n    Contains only data related to an instance in publishing. Has implemented\n    sorting methods and prepares information, e.g. if contains error or\n    warnings.\n    \"\"\"\n\n    _attrs = (\n        \"creator_identifier\",\n        \"family\",\n        \"label\",\n        \"name\",\n    )\n\n    def __init__(\n        self,\n        instance_id,\n        creator_identifier,\n        family,\n        name,\n        label,\n        exists,\n        logs,\n        errored,\n        warned\n    ):\n        self.id = instance_id\n        self.creator_identifier = creator_identifier\n        self.family = family\n        self.name = name\n        self.label = label\n        self.exists = exists\n        self.logs = logs\n        self.errored = errored\n        self.warned = warned\n\n    def __eq__(self, other):\n        for attr in self._attrs:\n            if getattr(self, attr) != getattr(other, attr):\n                return False\n        return True\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    def __gt__(self, other):\n        for attr in self._attrs:\n            self_value = getattr(self, attr)\n            other_value = getattr(other, attr)\n            if self_value == other_value:\n                continue\n            values = [self_value, other_value]\n            values.sort()\n            return values[0] == other_value\n        return None\n\n    def __lt__(self, other):\n        for attr in self._attrs:\n            self_value = getattr(self, attr)\n            other_value = getattr(other, attr)\n            if self_value == other_value:\n                continue\n            if self_value is None:\n                return False\n            if other_value is None:\n                return True\n            values = [self_value, other_value]\n            values.sort()\n            return values[0] == self_value\n        return None\n\n    def __ge__(self, other):\n        if self == other:\n            return True\n        return self.__gt__(other)\n\n    def __le__(self, other):\n        if self == other:\n            return True\n        return self.__lt__(other)\n\n    @classmethod\n    def from_report(cls, instance_id, instance_data, logs):\n        errored, warned = cls.extract_basic_log_info(logs)\n\n        return cls(\n            instance_id,\n            instance_data[\"creator_identifier\"],\n            instance_data[\"family\"],\n            instance_data[\"name\"],\n            instance_data[\"label\"],\n            instance_data[\"exists\"],\n            logs,\n            errored,\n            warned,\n        )\n\n    @classmethod\n    def create_context_item(cls, context_label, logs):\n        errored, warned = cls.extract_basic_log_info(logs)\n        return cls(\n            CONTEXT_ID,\n            None,\n            \"\",\n            CONTEXT_LABEL,\n            context_label,\n            True,\n            logs,\n            errored,\n            warned\n        )\n\n    @staticmethod\n    def extract_basic_log_info(logs):\n        warned = False\n        errored = False\n        for log in logs:\n            if log[\"type\"] == \"error\":\n                errored = True\n            elif log[\"type\"] == \"record\":\n                level_no = log[\"levelno\"]\n                if level_no and level_no >= logging.WARNING:\n                    warned = True\n\n            if warned and errored:\n                break\n        return errored, warned\n\n\nclass FamilyGroupLabel(QtWidgets.QWidget):\n    def __init__(self, family, parent):\n        super(FamilyGroupLabel, self).__init__(parent)\n\n        self.setLayoutDirection(QtCore.Qt.LeftToRight)\n\n        label_widget = QtWidgets.QLabel(family, self)\n\n        line_widget = QtWidgets.QWidget(self)\n        line_widget.setObjectName(\"Separator\")\n        line_widget.setMinimumHeight(2)\n        line_widget.setMaximumHeight(2)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setAlignment(QtCore.Qt.AlignVCenter)\n        main_layout.setSpacing(10)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(label_widget, 0)\n        main_layout.addWidget(line_widget, 1)\n\n\nclass PublishInstanceCardWidget(BaseClickableFrame):\n    selection_requested = QtCore.Signal(str)\n\n    _warning_pix = None\n    _error_pix = None\n    _success_pix = None\n    _in_progress_pix = None\n\n    def __init__(self, instance, icon, publish_finished, parent):\n        super(PublishInstanceCardWidget, self).__init__(parent)\n\n        self.setObjectName(\"CardViewWidget\")\n\n        icon_widget = IconValuePixmapLabel(icon, self)\n        icon_widget.setObjectName(\"FamilyIconLabel\")\n\n        label_widget = QtWidgets.QLabel(instance.label, self)\n\n        if instance.errored:\n            state_pix = self.get_error_pix()\n        elif instance.warned:\n            state_pix = self.get_warning_pix()\n        elif publish_finished:\n            state_pix = self.get_success_pix()\n        else:\n            state_pix = self.get_in_progress_pix()\n\n        state_label = IconValuePixmapLabel(state_pix, self)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(10, 7, 10, 7)\n        layout.addWidget(icon_widget, 0)\n        layout.addWidget(label_widget, 1)\n        layout.addWidget(state_label, 0)\n\n        # Change direction -> parent is scroll area where scrolls are on\n        #   left side\n        self.setLayoutDirection(QtCore.Qt.LeftToRight)\n\n        self._id = instance.id\n\n        self._selected = False\n\n        self._update_style_state()\n\n    @classmethod\n    def _prepare_pixes(cls):\n        publisher_colors = get_objected_colors(\"publisher\")\n        cls._warning_pix = paint_image_with_color(\n            get_image(\"warning\"),\n            publisher_colors[\"warning\"].get_qcolor()\n        )\n        cls._error_pix = paint_image_with_color(\n            get_image(\"error\"),\n            publisher_colors[\"error\"].get_qcolor()\n        )\n        cls._success_pix = paint_image_with_color(\n            get_image(\"success\"),\n            publisher_colors[\"success\"].get_qcolor()\n        )\n        cls._in_progress_pix = paint_image_with_color(\n            get_image(\"success\"),\n            publisher_colors[\"progress\"].get_qcolor()\n        )\n\n    @classmethod\n    def get_warning_pix(cls):\n        if cls._warning_pix is None:\n            cls._prepare_pixes()\n        return cls._warning_pix\n\n    @classmethod\n    def get_error_pix(cls):\n        if cls._error_pix is None:\n            cls._prepare_pixes()\n        return cls._error_pix\n\n    @classmethod\n    def get_success_pix(cls):\n        if cls._success_pix is None:\n            cls._prepare_pixes()\n        return cls._success_pix\n\n    @classmethod\n    def get_in_progress_pix(cls):\n        if cls._in_progress_pix is None:\n            cls._prepare_pixes()\n        return cls._in_progress_pix\n\n    @property\n    def id(self):\n        \"\"\"Id of card.\n\n        Returns:\n            str: Id of item.\n        \"\"\"\n\n        return self._id\n\n    @property\n    def is_selected(self):\n        \"\"\"Is card selected.\n\n        Returns:\n            bool: Item widget is marked as selected.\n        \"\"\"\n\n        return self._selected\n\n    def set_selected(self, selected):\n        \"\"\"Set card as selected.\n\n        Args:\n            selected (bool): Item should be marked as selected.\n        \"\"\"\n\n        if selected == self._selected:\n            return\n        self._selected = selected\n        self._update_style_state()\n\n    def _update_style_state(self):\n        state = \"\"\n        if self._selected:\n            state = \"selected\"\n\n        self.setProperty(\"state\", state)\n        self.style().polish(self)\n\n    def _mouse_release_callback(self):\n        \"\"\"Trigger selected signal.\"\"\"\n\n        self.selection_requested.emit(self.id)\n\n\nclass PublishInstancesViewWidget(QtWidgets.QWidget):\n    # Sane minimum width of instance cards - size calulated using font metrics\n    _min_width_measure_string = 24 * \"O\"\n    selection_changed = QtCore.Signal()\n\n    def __init__(self, controller, parent):\n        super(PublishInstancesViewWidget, self).__init__(parent)\n\n        scroll_area = VerticalScrollArea(self)\n        scroll_area.setWidgetResizable(True)\n        scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n        scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)\n        scrollbar_bg = scroll_area.verticalScrollBar().parent()\n        if scrollbar_bg:\n            scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        scroll_area.setViewportMargins(0, 0, 0, 0)\n\n        instance_view = QtWidgets.QWidget(scroll_area)\n\n        scroll_area.setWidget(instance_view)\n\n        instance_layout = QtWidgets.QVBoxLayout(instance_view)\n        instance_layout.setContentsMargins(0, 0, 0, 0)\n        instance_layout.addStretch(1)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(scroll_area, 1)\n\n        self._controller = controller\n        self._scroll_area = scroll_area\n        self._instance_view = instance_view\n        self._instance_layout = instance_layout\n\n        self._context_widget = None\n\n        self._widgets_by_instance_id = {}\n        self._group_widgets = []\n        self._ordered_widgets = []\n\n        self._explicitly_selected_instance_ids = []\n\n        self.setSizePolicy(\n            QtWidgets.QSizePolicy.Minimum,\n            self.sizePolicy().verticalPolicy()\n        )\n\n    def sizeHint(self):\n        \"\"\"Modify sizeHint based on visibility of scroll bars.\"\"\"\n        # Calculate width hint by content widget and vertical scroll bar\n        scroll_bar = self._scroll_area.verticalScrollBar()\n        view_size = self._instance_view.sizeHint().width()\n        fm = self._instance_view.fontMetrics()\n        width = (\n            max(view_size, fm.width(self._min_width_measure_string))\n            + scroll_bar.sizeHint().width()\n        )\n\n        result = super(PublishInstancesViewWidget, self).sizeHint()\n        result.setWidth(width)\n        return result\n\n    def _get_selected_widgets(self):\n        return [\n            widget\n            for widget in self._ordered_widgets\n            if widget.is_selected\n        ]\n\n    def get_selected_instance_ids(self):\n        return [\n            widget.id\n            for widget in self._get_selected_widgets()\n        ]\n\n    def clear(self):\n        \"\"\"Remove actions from widget.\"\"\"\n        while self._instance_layout.count():\n            item = self._instance_layout.takeAt(0)\n            widget = item.widget()\n            if widget:\n                widget.setVisible(False)\n                widget.deleteLater()\n        self._ordered_widgets = []\n        self._group_widgets = []\n        self._widgets_by_instance_id = {}\n\n    def update_instances(self, instance_items):\n        self.clear()\n        identifiers = {\n            instance_item.creator_identifier\n            for instance_item in instance_items\n        }\n        identifier_icons = {\n            identifier: self._controller.get_creator_icon(identifier)\n            for identifier in identifiers\n        }\n\n        widgets = []\n        group_widgets = []\n\n        publish_finished = (\n            self._controller.publish_has_crashed\n            or self._controller.publish_has_validation_errors\n            or self._controller.publish_has_finished\n        )\n        instances_by_family = collections.defaultdict(list)\n        for instance_item in instance_items:\n            if not instance_item.exists:\n                continue\n            instances_by_family[instance_item.family].append(instance_item)\n\n        sorted_by_family = sorted(\n            instances_by_family.items(), key=lambda i: i[0]\n        )\n        for family, instance_items in sorted_by_family:\n            # Only instance without family is context\n            if family:\n                group_widget = FamilyGroupLabel(family, self._instance_view)\n                self._instance_layout.addWidget(group_widget, 0)\n                group_widgets.append(group_widget)\n\n            sorted_items = sorted(instance_items, key=lambda i: i.label)\n            for instance_item in sorted_items:\n                icon = identifier_icons[instance_item.creator_identifier]\n\n                widget = PublishInstanceCardWidget(\n                    instance_item, icon, publish_finished, self._instance_view\n                )\n                widget.selection_requested.connect(self._on_selection_request)\n                self._instance_layout.addWidget(widget, 0)\n\n                widgets.append(widget)\n                self._widgets_by_instance_id[widget.id] = widget\n        self._instance_layout.addStretch(1)\n        self._ordered_widgets = widgets\n        self._group_widgets = group_widgets\n\n    def _on_selection_request(self, instance_id):\n        instance_widget = self._widgets_by_instance_id[instance_id]\n        selected_widgets = self._get_selected_widgets()\n        if instance_widget in selected_widgets:\n            instance_widget.set_selected(False)\n        else:\n            instance_widget.set_selected(True)\n            for widget in selected_widgets:\n                widget.set_selected(False)\n        self.selection_changed.emit()\n\n\nclass LogIconFrame(QtWidgets.QFrame):\n    \"\"\"Draw log item icon next to message.\n\n    Todos:\n        Paint event could be slow, maybe we could cache the image into pixmaps\n            so each item does not have to redraw it again.\n    \"\"\"\n\n    info_color = QtGui.QColor(\"#ffffff\")\n    error_color = QtGui.QColor(\"#ff4a4a\")\n    level_to_color = dict((\n        (10, QtGui.QColor(\"#ff66e8\")),\n        (20, QtGui.QColor(\"#66abff\")),\n        (30, QtGui.QColor(\"#ffba66\")),\n        (40, QtGui.QColor(\"#ff4d58\")),\n        (50, QtGui.QColor(\"#ff4f75\")),\n    ))\n    _error_pix = None\n    _validation_error_pix = None\n\n    def __init__(self, parent, log_type, log_level, is_validation_error):\n        super(LogIconFrame, self).__init__(parent)\n\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        self._is_record = log_type == \"record\"\n        self._is_error = log_type == \"error\"\n        self._is_validation_error = bool(is_validation_error)\n        self._log_color = self.level_to_color.get(log_level)\n\n    @classmethod\n    def get_validation_error_icon(cls):\n        if cls._validation_error_pix is None:\n            cls._validation_error_pix = get_pixmap(\"warning\")\n        return cls._validation_error_pix\n\n    @classmethod\n    def get_error_icon(cls):\n        if cls._error_pix is None:\n            cls._error_pix = get_pixmap(\"error\")\n        return cls._error_pix\n\n    def minimumSizeHint(self):\n        fm = self.fontMetrics()\n        size = fm.height()\n        return QtCore.QSize(size, size)\n\n    def paintEvent(self, event):\n        painter = QtGui.QPainter(self)\n        painter.setRenderHints(\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n        )\n        painter.setPen(QtCore.Qt.NoPen)\n        rect = self.rect()\n        new_size = min(rect.width(), rect.height())\n        new_rect = QtCore.QRect(1, 1, new_size - 2, new_size - 2)\n        if self._is_error:\n            if self._is_validation_error:\n                error_icon = self.get_validation_error_icon()\n            else:\n                error_icon = self.get_error_icon()\n            scaled_error_icon = error_icon.scaled(\n                new_rect.size(),\n                QtCore.Qt.KeepAspectRatio,\n                QtCore.Qt.SmoothTransformation\n            )\n            painter.drawPixmap(new_rect, scaled_error_icon)\n\n        else:\n            if self._is_record:\n                color = self._log_color\n            else:\n                color = QtGui.QColor(255, 255, 255)\n            painter.setBrush(color)\n            painter.drawEllipse(new_rect)\n        painter.end()\n\n\nclass LogItemWidget(QtWidgets.QWidget):\n    log_level_to_flag = {\n        10: LOG_DEBUG_VISIBLE,\n        20: LOG_INFO_VISIBLE,\n        30: LOG_WARNING_VISIBLE,\n        40: LOG_ERROR_VISIBLE,\n        50: LOG_CRITICAL_VISIBLE,\n    }\n\n    def __init__(self, log, parent):\n        super(LogItemWidget, self).__init__(parent)\n\n        type_flag, level_n = self._get_log_info(log)\n        icon_label = LogIconFrame(\n            self, log[\"type\"], level_n, log.get(\"is_validation_error\"))\n        message_label = QtWidgets.QLabel(log[\"msg\"].rstrip(), self)\n        message_label.setObjectName(\"PublishLogMessage\")\n        message_label.setTextInteractionFlags(\n            QtCore.Qt.TextBrowserInteraction)\n        message_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))\n        message_label.setWordWrap(True)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(8)\n        main_layout.addWidget(icon_label, 0)\n        main_layout.addWidget(message_label, 1)\n\n        self._type_flag = type_flag\n        self._plugin_id = log[\"plugin_id\"]\n        self._log_type_filtered = False\n        self._plugin_filtered = False\n\n    @property\n    def type_flag(self):\n        return self._type_flag\n\n    @property\n    def plugin_id(self):\n        return self._plugin_id\n\n    def _get_log_info(self, log):\n        log_type = log[\"type\"]\n        if log_type == \"error\":\n            return ERROR_VISIBLE, None\n\n        if log_type != \"record\":\n            return INFO_VISIBLE, None\n\n        level_n = log[\"levelno\"]\n        if level_n < 10:\n            level_n = 10\n        elif level_n % 10 != 0:\n            level_n -= (level_n % 10) + 10\n\n        flag = self.log_level_to_flag.get(level_n, LOG_CRITICAL_VISIBLE)\n        return flag, level_n\n\n    def _update_visibility(self):\n        self.setVisible(\n            not self._log_type_filtered\n            and not self._plugin_filtered\n        )\n\n    def set_log_type_filtered(self, filtered):\n        if filtered is self._log_type_filtered:\n            return\n        self._log_type_filtered = filtered\n        self._update_visibility()\n\n    def set_plugin_filtered(self, filtered):\n        if filtered is self._plugin_filtered:\n            return\n        self._plugin_filtered = filtered\n        self._update_visibility()\n\n\nclass LogsWithIconsView(QtWidgets.QWidget):\n    \"\"\"Show logs in a grid with 2 columns.\n\n    First column is for icon second is for message.\n\n    Todos:\n        Add filtering by type (exception, debug, info, etc.).\n    \"\"\"\n\n    def __init__(self, logs, parent):\n        super(LogsWithIconsView, self).__init__(parent)\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        logs_layout = QtWidgets.QVBoxLayout(self)\n        logs_layout.setContentsMargins(0, 0, 0, 0)\n        logs_layout.setSpacing(4)\n\n        widgets_by_flag = collections.defaultdict(list)\n        widgets_by_plugins_id = collections.defaultdict(list)\n\n        for log in logs:\n            widget = LogItemWidget(log, self)\n            widgets_by_flag[widget.type_flag].append(widget)\n            widgets_by_plugins_id[widget.plugin_id].append(widget)\n            logs_layout.addWidget(widget, 0)\n\n        self._widgets_by_flag = widgets_by_flag\n        self._widgets_by_plugins_id = widgets_by_plugins_id\n\n        self._visibility_by_flags = {\n            LOG_DEBUG_VISIBLE: True,\n            LOG_INFO_VISIBLE: True,\n            LOG_WARNING_VISIBLE: True,\n            LOG_ERROR_VISIBLE: True,\n            LOG_CRITICAL_VISIBLE: True,\n            ERROR_VISIBLE: True,\n            INFO_VISIBLE: True,\n        }\n        self._flags_filter = sum(self._visibility_by_flags.keys())\n        self._plugin_ids_filter = None\n\n    def _update_flags_filtering(self):\n        for flag in (\n            LOG_DEBUG_VISIBLE,\n            LOG_INFO_VISIBLE,\n            LOG_WARNING_VISIBLE,\n            LOG_ERROR_VISIBLE,\n            LOG_CRITICAL_VISIBLE,\n            ERROR_VISIBLE,\n            INFO_VISIBLE,\n        ):\n            visible = (self._flags_filter & flag) != 0\n            if visible is not self._visibility_by_flags[flag]:\n                self._visibility_by_flags[flag] = visible\n                for widget in self._widgets_by_flag[flag]:\n                    widget.set_log_type_filtered(not visible)\n\n    def _update_plugin_filtering(self):\n        if self._plugin_ids_filter is None:\n            for widgets in self._widgets_by_plugins_id.values():\n                for widget in widgets:\n                    widget.set_plugin_filtered(False)\n\n        else:\n            for plugin_id, widgets in self._widgets_by_plugins_id.items():\n                filtered = plugin_id not in self._plugin_ids_filter\n                for widget in widgets:\n                    widget.set_plugin_filtered(filtered)\n\n    def set_log_filters(self, visibility_filter, plugin_ids):\n        if self._flags_filter != visibility_filter:\n            self._flags_filter = visibility_filter\n            self._update_flags_filtering()\n\n        if self._plugin_ids_filter != plugin_ids:\n            if plugin_ids is not None:\n                plugin_ids = set(plugin_ids)\n            self._plugin_ids_filter = plugin_ids\n            self._update_plugin_filtering()\n\n\nclass InstanceLogsWidget(QtWidgets.QWidget):\n    \"\"\"Widget showing logs of one publish instance.\n\n    Args:\n        instance (_InstanceItem): Item of instance used as data source.\n        parent (QtWidgets.QWidget): Parent widget.\n    \"\"\"\n\n    def __init__(self, instance, parent):\n        super(InstanceLogsWidget, self).__init__(parent)\n\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        label_widget = QtWidgets.QLabel(instance.label, self)\n        label_widget.setObjectName(\"PublishInstanceLogsLabel\")\n        logs_grid = LogsWithIconsView(instance.logs, self)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(label_widget, 0)\n        layout.addWidget(logs_grid, 0)\n\n        self._logs_grid = logs_grid\n\n    def set_log_filters(self, visibility_filter, plugin_ids):\n        \"\"\"Change logs filter.\n\n        Args:\n            visibility_filter (int): Number contained of flags for each log\n                type and level.\n            plugin_ids (Iterable[str]): Plugin ids to which are logs filtered.\n        \"\"\"\n\n        self._logs_grid.set_log_filters(visibility_filter, plugin_ids)\n\n\nclass InstancesLogsView(QtWidgets.QFrame):\n    \"\"\"Publish instances logs view widget.\"\"\"\n\n    def __init__(self, parent):\n        super(InstancesLogsView, self).__init__(parent)\n        self.setObjectName(\"InstancesLogsView\")\n\n        scroll_area = QtWidgets.QScrollArea(self)\n        scroll_area.setWidgetResizable(True)\n        scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n        scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)\n        scroll_area.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        scrollbar_bg = scroll_area.verticalScrollBar().parent()\n        if scrollbar_bg:\n            scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        content_wrap_widget = QtWidgets.QWidget(scroll_area)\n        content_wrap_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        content_widget = QtWidgets.QWidget(content_wrap_widget)\n        content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.setSpacing(15)\n\n        scroll_area.setWidget(content_wrap_widget)\n\n        content_wrap_layout = QtWidgets.QVBoxLayout(content_wrap_widget)\n        content_wrap_layout.setContentsMargins(0, 0, 0, 0)\n        content_wrap_layout.addWidget(content_widget, 0)\n        content_wrap_layout.addStretch(1)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(scroll_area, 1)\n\n        self._visible_filters = (\n            LOG_INFO_VISIBLE\n            | LOG_WARNING_VISIBLE\n            | LOG_ERROR_VISIBLE\n            | LOG_CRITICAL_VISIBLE\n            | ERROR_VISIBLE\n            | INFO_VISIBLE\n        )\n\n        self._content_widget = content_widget\n        self._content_layout = content_layout\n\n        self._instances_order = []\n        self._instances_by_id = {}\n        self._views_by_instance_id = {}\n        self._is_showed = False\n        self._clear_needed = False\n        self._update_needed = False\n        self._instance_ids_filter = []\n        self._plugin_ids_filter = None\n\n    def showEvent(self, event):\n        super(InstancesLogsView, self).showEvent(event)\n        self._is_showed = True\n        self._update_instances()\n\n    def hideEvent(self, event):\n        super(InstancesLogsView, self).hideEvent(event)\n        self._is_showed = False\n\n    def closeEvent(self, event):\n        super(InstancesLogsView, self).closeEvent(event)\n        self._is_showed = False\n\n    def _update_instances(self):\n        if not self._is_showed:\n            return\n\n        if self._clear_needed:\n            self._clear_widgets()\n            self._clear_needed = False\n\n        if not self._update_needed:\n            return\n        self._update_needed = False\n\n        instance_ids = self._instance_ids_filter\n        to_hide = set()\n        if not instance_ids:\n            instance_ids = self._instances_by_id\n        else:\n            to_hide = set(self._instances_by_id) - set(instance_ids)\n\n        for instance_id in instance_ids:\n            widget = self._views_by_instance_id.get(instance_id)\n            if widget is None:\n                instance = self._instances_by_id[instance_id]\n                widget = InstanceLogsWidget(instance, self._content_widget)\n                self._views_by_instance_id[instance_id] = widget\n                self._content_layout.addWidget(widget, 0)\n\n            widget.setVisible(True)\n            widget.set_log_filters(\n                self._visible_filters, self._plugin_ids_filter\n            )\n\n        for instance_id in to_hide:\n            widget = self._views_by_instance_id.get(instance_id)\n            if widget is not None:\n                widget.setVisible(False)\n\n    def _clear_widgets(self):\n        \"\"\"Remove all widgets from layout and from cache.\"\"\"\n\n        while self._content_layout.count():\n            item = self._content_layout.takeAt(0)\n            widget = item.widget()\n            if widget:\n                widget.setVisible(False)\n                widget.deleteLater()\n        self._views_by_instance_id = {}\n\n    def update_instances(self, instances):\n        \"\"\"Update publish instance from report.\n\n        Args:\n            instances (list[_InstanceItem]): Instance data from report.\n        \"\"\"\n\n        self._instances_order = [\n            instance.id for instance in instances\n        ]\n        self._instances_by_id = {\n            instance.id: instance\n            for instance in instances\n        }\n        self._instance_ids_filter = []\n        self._plugin_ids_filter = None\n        self._clear_needed = True\n        self._update_needed = True\n        self._update_instances()\n\n    def set_instances_filter(self, instance_ids=None):\n        \"\"\"Set instance filter.\n\n        Args:\n            instance_ids (Optional[list[str]]): List of instances to keep\n                visible. Pass empty list to hide all items.\n        \"\"\"\n\n        self._instance_ids_filter = instance_ids\n        self._update_needed = True\n        self._update_instances()\n\n    def set_plugins_filter(self, plugin_ids=None):\n        if self._plugin_ids_filter == plugin_ids:\n            return\n        self._plugin_ids_filter = plugin_ids\n        self._update_needed = True\n        self._update_instances()\n\n\nclass CrashWidget(QtWidgets.QWidget):\n    \"\"\"Widget shown when publishing crashes.\n\n    Contains only minimal information for artist with easy access to report\n    actions.\n    \"\"\"\n\n    def __init__(self, controller, parent):\n        super(CrashWidget, self).__init__(parent)\n\n        main_label = QtWidgets.QLabel(\"This is not your fault\", self)\n        main_label.setAlignment(QtCore.Qt.AlignCenter)\n        main_label.setObjectName(\"PublishCrashMainLabel\")\n\n        report_label = QtWidgets.QLabel(\n            (\n                \"Please report the error to your pipeline support\"\n                \" using one of the options below.\"\n            ),\n            self\n        )\n        report_label.setAlignment(QtCore.Qt.AlignCenter)\n        report_label.setWordWrap(True)\n        report_label.setObjectName(\"PublishCrashReportLabel\")\n\n        btns_widget = QtWidgets.QWidget(self)\n        copy_clipboard_btn = QtWidgets.QPushButton(\n            \"Copy to clipboard\", btns_widget)\n        save_to_disk_btn = QtWidgets.QPushButton(\n            \"Save to disk\", btns_widget)\n\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(copy_clipboard_btn, 0)\n        btns_layout.addSpacing(20)\n        btns_layout.addWidget(save_to_disk_btn, 0)\n        btns_layout.addStretch(1)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addStretch(1)\n        layout.addWidget(main_label, 0)\n        layout.addSpacing(20)\n        layout.addWidget(report_label, 0)\n        layout.addSpacing(20)\n        layout.addWidget(btns_widget, 0)\n        layout.addStretch(2)\n\n        copy_clipboard_btn.clicked.connect(self._on_copy_to_clipboard)\n        save_to_disk_btn.clicked.connect(self._on_save_to_disk_click)\n\n        self._controller = controller\n\n    def _on_copy_to_clipboard(self):\n        self._controller.event_system.emit(\n            \"copy_report.request\", {}, \"report_page\")\n\n    def _on_save_to_disk_click(self):\n        self._controller.event_system.emit(\n            \"export_report.request\", {}, \"report_page\")\n\n\nclass ErrorDetailsWidget(QtWidgets.QWidget):\n    def __init__(self, parent):\n        super(ErrorDetailsWidget, self).__init__(parent)\n\n        inputs_widget = QtWidgets.QWidget(self)\n        # Error 'Description' input\n        error_description_input = ExpandingTextEdit(inputs_widget)\n        error_description_input.setObjectName(\"InfoText\")\n        error_description_input.setTextInteractionFlags(\n            QtCore.Qt.TextBrowserInteraction\n        )\n\n        # Error 'Details' widget -> Collapsible\n        error_details_widget = QtWidgets.QWidget(inputs_widget)\n\n        error_details_top = ClickableFrame(error_details_widget)\n\n        error_details_expand_btn = ClassicExpandBtn(error_details_top)\n        error_details_expand_label = QtWidgets.QLabel(\n            \"Details\", error_details_top)\n\n        line_widget = SeparatorWidget(1, parent=error_details_top)\n\n        error_details_top_l = QtWidgets.QHBoxLayout(error_details_top)\n        error_details_top_l.setContentsMargins(0, 0, 10, 0)\n        error_details_top_l.addWidget(error_details_expand_btn, 0)\n        error_details_top_l.addWidget(error_details_expand_label, 0)\n        error_details_top_l.addWidget(line_widget, 1)\n\n        error_details_input = ExpandingTextEdit(error_details_widget)\n        error_details_input.setObjectName(\"InfoText\")\n        error_details_input.setTextInteractionFlags(\n            QtCore.Qt.TextBrowserInteraction\n        )\n        error_details_input.setVisible(not error_details_expand_btn.collapsed)\n\n        error_details_layout = QtWidgets.QVBoxLayout(error_details_widget)\n        error_details_layout.setContentsMargins(0, 0, 0, 0)\n        error_details_layout.addWidget(error_details_top, 0)\n        error_details_layout.addWidget(error_details_input, 0)\n        error_details_layout.addStretch(1)\n\n        # Description and Details layout\n        inputs_layout = QtWidgets.QVBoxLayout(inputs_widget)\n        inputs_layout.setContentsMargins(0, 0, 0, 0)\n        inputs_layout.setSpacing(10)\n        inputs_layout.addWidget(error_description_input, 0)\n        inputs_layout.addWidget(error_details_widget, 1)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(inputs_widget, 1)\n\n        error_details_top.clicked.connect(self._on_detail_toggle)\n\n        self._error_details_widget = error_details_widget\n        self._error_description_input = error_description_input\n        self._error_details_expand_btn = error_details_expand_btn\n        self._error_details_input = error_details_input\n\n    def _on_detail_toggle(self):\n        self._error_details_expand_btn.set_collapsed()\n        self._error_details_input.setVisible(\n            not self._error_details_expand_btn.collapsed)\n\n    def set_error_item(self, error_item):\n        detail = \"\"\n        description = \"\"\n        if error_item:\n            description = error_item.description or description\n            detail = error_item.detail or detail\n\n        if commonmark:\n            self._error_description_input.setHtml(\n                commonmark.commonmark(description)\n            )\n            self._error_details_input.setHtml(\n                commonmark.commonmark(detail)\n            )\n\n        elif hasattr(self._error_details_input, \"setMarkdown\"):\n            self._error_description_input.setMarkdown(description)\n            self._error_details_input.setMarkdown(detail)\n\n        else:\n            self._error_description_input.setText(description)\n            self._error_details_input.setText(detail)\n\n        self._error_details_widget.setVisible(bool(detail))\n\n\nclass ReportsWidget(QtWidgets.QWidget):\n    \"\"\"\n        # Crash layout\n        ┌──────┬─────────┬─────────┐\n        │Views │ Logs    │ Details │\n        │      │         │         │\n        │      │         │         │\n        └──────┴─────────┴─────────┘\n        # Success layout\n        ┌──────┬───────────────────┐\n        │View  │ Logs              │\n        │      │                   │\n        │      │                   │\n        └──────┴───────────────────┘\n        # Validation errors layout\n        ┌──────┬─────────┬─────────┐\n        │Views │ Actions │         │\n        │      ├─────────┤ Details │\n        │      │ Logs    │         │\n        │      │         │         │\n        └──────┴─────────┴─────────┘\n    \"\"\"\n\n    def __init__(self, controller, parent):\n        super(ReportsWidget, self).__init__(parent)\n\n        # Instances view\n        views_widget = QtWidgets.QWidget(self)\n\n        instances_view = PublishInstancesViewWidget(controller, views_widget)\n\n        validation_error_view = ValidationErrorsView(views_widget)\n\n        views_layout = QtWidgets.QStackedLayout(views_widget)\n        views_layout.setContentsMargins(0, 0, 0, 0)\n        views_layout.addWidget(instances_view)\n        views_layout.addWidget(validation_error_view)\n\n        views_layout.setCurrentWidget(instances_view)\n\n        # Error description with actions and optional detail\n        details_widget = QtWidgets.QFrame(self)\n        details_widget.setObjectName(\"PublishInstancesDetails\")\n\n        # Actions widget\n        actions_widget = ValidateActionsWidget(controller, details_widget)\n\n        pages_widget = QtWidgets.QWidget(details_widget)\n\n        # Logs view\n        logs_view = InstancesLogsView(pages_widget)\n\n        # Validation details\n        # Description and details inputs are in scroll\n        # - single scroll for both inputs, they are forced to not use theirs\n        detail_inputs_spacer = QtWidgets.QWidget(pages_widget)\n        detail_inputs_spacer.setMinimumWidth(30)\n        detail_inputs_spacer.setMaximumWidth(30)\n\n        detail_input_scroll = QtWidgets.QScrollArea(pages_widget)\n\n        detail_inputs_widget = ErrorDetailsWidget(detail_input_scroll)\n        detail_inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        detail_input_scroll.setWidget(detail_inputs_widget)\n        detail_input_scroll.setWidgetResizable(True)\n        detail_input_scroll.setViewportMargins(0, 0, 0, 0)\n\n        # Crash information\n        crash_widget = CrashWidget(controller, details_widget)\n\n        # Layout pages\n        pages_layout = QtWidgets.QHBoxLayout(pages_widget)\n        pages_layout.setContentsMargins(0, 0, 0, 0)\n        pages_layout.addWidget(logs_view, 1)\n        pages_layout.addWidget(detail_inputs_spacer, 0)\n        pages_layout.addWidget(detail_input_scroll, 1)\n        pages_layout.addWidget(crash_widget, 1)\n\n        details_layout = QtWidgets.QVBoxLayout(details_widget)\n        margins = details_layout.contentsMargins()\n        margins.setTop(margins.top() * 2)\n        margins.setBottom(margins.bottom() * 2)\n        details_layout.setContentsMargins(margins)\n        details_layout.setSpacing(margins.top())\n        details_layout.addWidget(actions_widget, 0)\n        details_layout.addWidget(pages_widget, 1)\n\n        content_layout = QtWidgets.QHBoxLayout(self)\n        content_layout.setContentsMargins(0, 0, 0, 0)\n        content_layout.addWidget(views_widget, 0)\n        content_layout.addWidget(details_widget, 1)\n\n        instances_view.selection_changed.connect(self._on_instance_selection)\n        validation_error_view.selection_changed.connect(\n            self._on_error_selection)\n\n        self._views_layout = views_layout\n        self._instances_view = instances_view\n        self._validation_error_view = validation_error_view\n\n        self._actions_widget = actions_widget\n        self._detail_inputs_widget = detail_inputs_widget\n        self._logs_view = logs_view\n        self._detail_inputs_spacer = detail_inputs_spacer\n        self._detail_input_scroll = detail_input_scroll\n        self._crash_widget = crash_widget\n\n        self._controller = controller\n\n        self._validation_errors_by_id = {}\n\n    def _get_instance_items(self):\n        report = self._controller.get_publish_report()\n        context_label = report[\"context\"][\"label\"] or CONTEXT_LABEL\n        instances_by_id = report[\"instances\"]\n        plugins_info = report[\"plugins_data\"]\n        logs_by_instance_id = collections.defaultdict(list)\n        for plugin_info in plugins_info:\n            plugin_id = plugin_info[\"id\"]\n            for instance_info in plugin_info[\"instances_data\"]:\n                instance_id = instance_info[\"id\"] or CONTEXT_ID\n                for log in instance_info[\"logs\"]:\n                    log[\"plugin_id\"] = plugin_id\n                logs_by_instance_id[instance_id].extend(instance_info[\"logs\"])\n\n        context_item = _InstanceItem.create_context_item(\n            context_label, logs_by_instance_id[CONTEXT_ID])\n        instance_items = [\n            _InstanceItem.from_report(\n                instance_id, instance, logs_by_instance_id[instance_id]\n            )\n            for instance_id, instance in instances_by_id.items()\n            if instance[\"exists\"]\n        ]\n        instance_items.sort()\n        instance_items.insert(0, context_item)\n        return instance_items\n\n    def update_data(self):\n        view = self._instances_view\n        validation_error_mode = False\n        if (\n            not self._controller.publish_has_crashed\n            and self._controller.publish_has_validation_errors\n        ):\n            view = self._validation_error_view\n            validation_error_mode = True\n\n        self._actions_widget.set_visible_mode(validation_error_mode)\n        self._detail_inputs_spacer.setVisible(validation_error_mode)\n        self._detail_input_scroll.setVisible(validation_error_mode)\n        self._views_layout.setCurrentWidget(view)\n\n        self._crash_widget.setVisible(self._controller.publish_has_crashed)\n        self._logs_view.setVisible(not self._controller.publish_has_crashed)\n\n        # Instance view & logs update\n        instance_items = self._get_instance_items()\n        self._instances_view.update_instances(instance_items)\n        self._logs_view.update_instances(instance_items)\n\n        # Validation errors\n        validation_errors = self._controller.get_validation_errors()\n        grouped_error_items = validation_errors.group_items_by_title()\n\n        validation_errors_by_id = {\n            title_item[\"id\"]: title_item\n            for title_item in grouped_error_items\n        }\n\n        self._validation_errors_by_id = validation_errors_by_id\n        self._validation_error_view.set_errors(grouped_error_items)\n\n    def _on_instance_selection(self):\n        instance_ids = self._instances_view.get_selected_instance_ids()\n        self._logs_view.set_instances_filter(instance_ids)\n\n    def _on_error_selection(self):\n        title_id, instance_ids = (\n            self._validation_error_view.get_selected_items())\n        error_info = self._validation_errors_by_id.get(title_id)\n        if error_info is None:\n            self._actions_widget.set_error_info(None)\n            self._detail_inputs_widget.set_error_item(None)\n            return\n\n        self._logs_view.set_instances_filter(instance_ids)\n        self._logs_view.set_plugins_filter([error_info[\"plugin_id\"]])\n\n        match_error_item = None\n        for error_item in error_info[\"error_items\"]:\n            instance_id = error_item.instance_id or CONTEXT_ID\n            if instance_id in instance_ids:\n                match_error_item = error_item\n                break\n\n        self._actions_widget.set_error_info(error_info)\n        self._detail_inputs_widget.set_error_item(match_error_item)\n\n\nclass ReportPageWidget(QtWidgets.QFrame):\n    \"\"\"Widgets showing report for artis.\n\n    There are 5 possible states:\n    1. Publishing did not start yet.         > Only label.\n    2. Publishing is paused.                ┐\n    3. Publishing successfully finished.    │> Instances with logs.\n    4. Publishing crashed.                  ┘\n    5. Crashed because of validation error.  > Errors with logs.\n\n    This widget is shown if validation errors happened during validation part.\n\n    Shows validation error titles with instances on which they happened\n    and validation error detail with possible actions (repair).\n    \"\"\"\n\n    def __init__(self, controller, parent):\n        super(ReportPageWidget, self).__init__(parent)\n\n        header_label = QtWidgets.QLabel(self)\n        header_label.setAlignment(QtCore.Qt.AlignCenter)\n        header_label.setObjectName(\"PublishReportHeader\")\n\n        publish_instances_widget = ReportsWidget(controller, self)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(header_label, 0)\n        layout.addWidget(publish_instances_widget, 0)\n\n        controller.event_system.add_callback(\n            \"publish.process.started\", self._on_publish_start\n        )\n        controller.event_system.add_callback(\n            \"publish.reset.finished\", self._on_publish_reset\n        )\n        controller.event_system.add_callback(\n            \"publish.process.stopped\", self._on_publish_stop\n        )\n\n        self._header_label = header_label\n        self._publish_instances_widget = publish_instances_widget\n\n        self._controller = controller\n\n    def _update_label(self):\n        if not self._controller.publish_has_started:\n            # This probably never happen when this widget is visible\n            header_label = \"Nothing to report until you run publish\"\n        elif self._controller.publish_has_crashed:\n            header_label = \"Publish error report\"\n        elif self._controller.publish_has_validation_errors:\n            header_label = \"Publish validation report\"\n        elif self._controller.publish_has_finished:\n            header_label = \"Publish success report\"\n        else:\n            header_label = \"Publish report\"\n        self._header_label.setText(header_label)\n\n    def _update_state(self):\n        self._update_label()\n        publish_started = self._controller.publish_has_started\n        self._publish_instances_widget.setVisible(publish_started)\n        if publish_started:\n            self._publish_instances_widget.update_data()\n\n        self.updateGeometry()\n\n    def _on_publish_start(self):\n        self._update_state()\n\n    def _on_publish_reset(self):\n        self._update_state()\n\n    def _on_publish_stop(self):\n        self._update_state()\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/screenshot_widget.py",
    "content": "import os\nimport tempfile\n\nfrom qtpy import QtCore, QtGui, QtWidgets\n\n\nclass ScreenMarquee(QtWidgets.QDialog):\n    \"\"\"Dialog to interactively define screen area.\n\n    This allows to select a screen area through a marquee selection.\n\n    You can use any of its classmethods for easily saving an image,\n    capturing to QClipboard or returning a QPixmap, respectively\n    `capture_to_file`, `capture_to_clipboard` and `capture_to_pixmap`.\n    \"\"\"\n\n    def __init__(self, parent=None):\n        super(ScreenMarquee, self).__init__(parent=parent)\n\n        self.setWindowFlags(\n            QtCore.Qt.Window\n            | QtCore.Qt.FramelessWindowHint\n            | QtCore.Qt.WindowStaysOnTopHint\n            | QtCore.Qt.CustomizeWindowHint\n        )\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        self.setCursor(QtCore.Qt.CrossCursor)\n        self.setMouseTracking(True)\n\n        app = QtWidgets.QApplication.instance()\n        if hasattr(app, \"screenAdded\"):\n            app.screenAdded.connect(self._on_screen_added)\n            app.screenRemoved.connect(self._fit_screen_geometry)\n        elif hasattr(app, \"desktop\"):\n            desktop = app.desktop()\n            desktop.screenCountChanged.connect(self._fit_screen_geometry)\n\n        for screen in QtWidgets.QApplication.screens():\n            screen.geometryChanged.connect(self._fit_screen_geometry)\n\n        self._opacity = 50\n        self._click_pos = None\n        self._capture_rect = None\n\n    def get_captured_pixmap(self):\n        if self._capture_rect is None:\n            return QtGui.QPixmap()\n\n        return self.get_desktop_pixmap(self._capture_rect)\n\n    def paintEvent(self, event):\n        \"\"\"Paint event\"\"\"\n\n        # Convert click and current mouse positions to local space.\n        mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())\n        click_pos = None\n        if self._click_pos is not None:\n            click_pos = self.mapFromGlobal(self._click_pos)\n\n        painter = QtGui.QPainter(self)\n        painter.setRenderHints(\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n        )\n\n        # Draw background. Aside from aesthetics, this makes the full\n        # tool region accept mouse events.\n        painter.setBrush(QtGui.QColor(0, 0, 0, self._opacity))\n        painter.setPen(QtCore.Qt.NoPen)\n        rect = event.rect()\n        fill_path = QtGui.QPainterPath()\n        fill_path.addRect(rect)\n\n        # Clear the capture area\n        if click_pos is not None:\n            sub_path = QtGui.QPainterPath()\n            capture_rect = QtCore.QRect(click_pos, mouse_pos)\n            sub_path.addRect(capture_rect)\n            fill_path = fill_path.subtracted(sub_path)\n\n        painter.drawPath(fill_path)\n\n        pen_color = QtGui.QColor(255, 255, 255, self._opacity)\n        pen = QtGui.QPen(pen_color, 1, QtCore.Qt.DotLine)\n        painter.setPen(pen)\n\n        # Draw cropping markers at click position\n        if click_pos is not None:\n            painter.drawLine(\n                rect.left(), click_pos.y(),\n                rect.right(), click_pos.y()\n            )\n            painter.drawLine(\n                click_pos.x(), rect.top(),\n                click_pos.x(), rect.bottom()\n            )\n\n        # Draw cropping markers at current mouse position\n        painter.drawLine(\n            rect.left(), mouse_pos.y(),\n            rect.right(), mouse_pos.y()\n        )\n        painter.drawLine(\n            mouse_pos.x(), rect.top(),\n            mouse_pos.x(), rect.bottom()\n        )\n        painter.end()\n\n    def mousePressEvent(self, event):\n        \"\"\"Mouse click event\"\"\"\n\n        if event.button() == QtCore.Qt.LeftButton:\n            # Begin click drag operation\n            self._click_pos = event.globalPos()\n\n    def mouseReleaseEvent(self, event):\n        \"\"\"Mouse release event\"\"\"\n        if (\n            self._click_pos is not None\n            and event.button() == QtCore.Qt.LeftButton\n        ):\n            # End click drag operation and commit the current capture rect\n            self._capture_rect = QtCore.QRect(\n                self._click_pos, event.globalPos()\n            ).normalized()\n            self._click_pos = None\n        self.close()\n\n    def mouseMoveEvent(self, event):\n        \"\"\"Mouse move event\"\"\"\n        self.repaint()\n\n    def keyPressEvent(self, event):\n        \"\"\"Mouse press event\"\"\"\n        if event.key() == QtCore.Qt.Key_Escape:\n            self._click_pos = None\n            self._capture_rect = None\n            event.accept()\n            self.close()\n            return\n        return super(ScreenMarquee, self).keyPressEvent(event)\n\n    def showEvent(self, event):\n        self._fit_screen_geometry()\n\n    def _fit_screen_geometry(self):\n        # Compute the union of all screen geometries, and resize to fit.\n        workspace_rect = QtCore.QRect()\n        for screen in QtWidgets.QApplication.screens():\n            workspace_rect = workspace_rect.united(screen.geometry())\n        self.setGeometry(workspace_rect)\n\n    def _on_screen_added(self):\n        for screen in QtGui.QGuiApplication.screens():\n            screen.geometryChanged.connect(self._fit_screen_geometry)\n\n    @classmethod\n    def get_desktop_pixmap(cls, rect):\n        \"\"\"Performs a screen capture on the specified rectangle.\n\n        Args:\n            rect (QtCore.QRect): The rectangle to capture.\n\n        Returns:\n            QtGui.QPixmap: Captured pixmap image\n        \"\"\"\n\n        if rect.width() < 1 or rect.height() < 1:\n            return QtGui.QPixmap()\n\n        screen_pixes = []\n        for screen in QtWidgets.QApplication.screens():\n            screen_geo = screen.geometry()\n            if not screen_geo.intersects(rect):\n                continue\n\n            screen_pix_rect = screen_geo.intersected(rect)\n            screen_pix = screen.grabWindow(\n                0,\n                screen_pix_rect.x() - screen_geo.x(),\n                screen_pix_rect.y() - screen_geo.y(),\n                screen_pix_rect.width(), screen_pix_rect.height()\n            )\n            paste_point = QtCore.QPoint(\n                screen_pix_rect.x() - rect.x(),\n                screen_pix_rect.y() - rect.y()\n            )\n            screen_pixes.append((screen_pix, paste_point))\n\n        output_pix = QtGui.QPixmap(rect.width(), rect.height())\n        output_pix.fill(QtCore.Qt.transparent)\n        pix_painter = QtGui.QPainter()\n        pix_painter.begin(output_pix)\n        for item in screen_pixes:\n            (screen_pix, offset) = item\n            pix_painter.drawPixmap(offset, screen_pix)\n\n        pix_painter.end()\n\n        return output_pix\n\n    @classmethod\n    def capture_to_pixmap(cls):\n        \"\"\"Take screenshot with marquee into pixmap.\n\n        Note:\n            The pixmap can be invalid (use 'isNull' to check).\n\n        Returns:\n            QtGui.QPixmap: Captured pixmap image.\n        \"\"\"\n\n        tool = cls()\n        # Activate so Escape event is not ignored.\n        tool.setWindowState(QtCore.Qt.WindowActive)\n        # Exec dialog and return captured pixmap.\n        tool.exec_()\n        return tool.get_captured_pixmap()\n\n    @classmethod\n    def capture_to_file(cls, filepath=None):\n        \"\"\"Take screenshot with marquee into file.\n\n        Args:\n            filepath (Optional[str]): Path where screenshot will be saved.\n\n        Returns:\n            Union[str, None]: Path to the saved screenshot, or None if user\n                cancelled the operation.\n        \"\"\"\n\n        pixmap = cls.capture_to_pixmap()\n        if pixmap.isNull():\n            return None\n\n        if filepath is None:\n            with tempfile.NamedTemporaryFile(\n                prefix=\"screenshot_\", suffix=\".png\", delete=False\n            ) as tmpfile:\n                filepath = tmpfile.name\n\n        else:\n            output_dir = os.path.dirname(filepath)\n            if not os.path.exists(output_dir):\n                os.makedirs(output_dir)\n\n        pixmap.save(filepath)\n        return filepath\n\n    @classmethod\n    def capture_to_clipboard(cls):\n        \"\"\"Take screenshot with marquee into clipboard.\n\n        Notes:\n            Screenshot is not in clipboard if user cancelled the operation.\n\n        Returns:\n            bool: Screenshot was added to clipboard.\n        \"\"\"\n\n        clipboard = QtWidgets.QApplication.clipboard()\n        pixmap = cls.capture_to_pixmap()\n        if pixmap.isNull():\n            return False\n        image = pixmap.toImage()\n        clipboard.setImage(image, QtGui.QClipboard.Clipboard)\n        return True\n\n\ndef capture_to_pixmap():\n    \"\"\"Take screenshot with marquee into pixmap.\n\n    Note:\n        The pixmap can be invalid (use 'isNull' to check).\n\n    Returns:\n        QtGui.QPixmap: Captured pixmap image.\n    \"\"\"\n\n    return ScreenMarquee.capture_to_pixmap()\n\n\ndef capture_to_file(filepath=None):\n    \"\"\"Take screenshot with marquee into file.\n\n    Args:\n        filepath (Optional[str]): Path where screenshot will be saved.\n\n    Returns:\n        Union[str, None]: Path to the saved screenshot, or None if user\n            cancelled the operation.\n    \"\"\"\n\n    return ScreenMarquee.capture_to_file(filepath)\n\n\ndef capture_to_clipboard():\n    \"\"\"Take screenshot with marquee into clipboard.\n\n    Notes:\n        Screenshot is not in clipboard if user cancelled the operation.\n\n    Returns:\n        bool: Screenshot was added to clipboard.\n    \"\"\"\n\n    return ScreenMarquee.capture_to_clipboard()\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/tabs_widget.py",
    "content": "from qtpy import QtWidgets, QtCore\nfrom openpype.tools.utils import set_style_property\n\n\nclass PublisherTabBtn(QtWidgets.QPushButton):\n    tab_clicked = QtCore.Signal(str)\n\n    def __init__(self, identifier, label, parent):\n        super(PublisherTabBtn, self).__init__(label, parent)\n        self._identifier = identifier\n        self._active = False\n\n        self.clicked.connect(self._on_click)\n\n    def _on_click(self):\n        self.tab_clicked.emit(self.identifier)\n\n    @property\n    def identifier(self):\n        return self._identifier\n\n    def activate(self):\n        if self._active:\n            return\n        self._active = True\n        set_style_property(self, \"active\", \"1\")\n\n    def deactivate(self):\n        if not self._active:\n            return\n        self._active = False\n        set_style_property(self, \"active\", \"\")\n\n\nclass PublisherTabsWidget(QtWidgets.QFrame):\n    tab_changed = QtCore.Signal(str, str)\n\n    def __init__(self, parent=None):\n        super(PublisherTabsWidget, self).__init__(parent)\n\n        btns_widget = QtWidgets.QWidget(self)\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n        btns_layout.setSpacing(0)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(btns_widget, 0)\n        layout.addStretch(1)\n\n        self._btns_layout = btns_layout\n\n        self._current_identifier = None\n        self._buttons_by_identifier = {}\n\n    def is_current_tab(self, identifier):\n        if isinstance(identifier, int):\n            identifier = self.get_tab_by_index(identifier)\n\n        if isinstance(identifier, PublisherTabBtn):\n            identifier = identifier.identifier\n        return self._current_identifier == identifier\n\n    def add_tab(self, label, identifier):\n        button = PublisherTabBtn(identifier, label, self)\n        button.tab_clicked.connect(self._on_tab_click)\n        self._btns_layout.addWidget(button, 0)\n        self._buttons_by_identifier[identifier] = button\n\n        if self._current_identifier is None:\n            self.set_current_tab(identifier)\n        return button\n\n    def get_tab_by_index(self, index):\n        if 0 >= index < self._btns_layout.count():\n            item = self._btns_layout.itemAt(index)\n            return item.widget()\n        return None\n\n    def set_current_tab(self, identifier):\n        if isinstance(identifier, int):\n            identifier = self.get_tab_by_index(identifier)\n\n        if isinstance(identifier, PublisherTabBtn):\n            identifier = identifier.identifier\n\n        if identifier == self._current_identifier:\n            return\n\n        new_btn = self._buttons_by_identifier.get(identifier)\n        if new_btn is None:\n            return\n\n        old_identifier = self._current_identifier\n        old_btn = self._buttons_by_identifier.get(old_identifier)\n        self._current_identifier = identifier\n\n        if old_btn is not None:\n            old_btn.deactivate()\n        new_btn.activate()\n        self.tab_changed.emit(old_identifier, identifier)\n\n    def current_tab(self):\n        return self._current_identifier\n\n    def _on_tab_click(self, identifier):\n        self.set_current_tab(identifier)\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/tasks_widget.py",
    "content": "from qtpy import QtCore, QtGui\n\nfrom openpype.tools.utils.tasks_widget import TasksWidget, TASK_NAME_ROLE\nfrom openpype.tools.utils.lib import get_default_task_icon\n\n\nclass TasksModel(QtGui.QStandardItemModel):\n    \"\"\"Tasks model.\n\n    Task model must have set context of asset documents.\n\n    Items in model are based on 0-infinite asset documents. Always contain\n    an interserction of context asset tasks. When no assets are in context\n    them model is empty if 2 or more are in context assets that don't have\n    tasks with same names then model is empty too.\n\n    Args:\n        controller (PublisherController): Controller which handles creation and\n            publishing.\n    \"\"\"\n    def __init__(self, controller, allow_empty_task=False):\n        super(TasksModel, self).__init__()\n\n        self._allow_empty_task = allow_empty_task\n        self._controller = controller\n        self._items_by_name = {}\n        self._asset_names = []\n        self._task_names_by_asset_name = {}\n\n    def set_asset_names(self, asset_names):\n        \"\"\"Set assets context.\"\"\"\n        self._asset_names = asset_names\n        self.reset()\n\n    @staticmethod\n    def get_intersection_of_tasks(task_names_by_asset_name):\n        \"\"\"Calculate intersection of task names from passed data.\n\n        Example:\n        ```\n        # Passed `task_names_by_asset_name`\n        {\n            \"asset_1\": [\"compositing\", \"animation\"],\n            \"asset_2\": [\"compositing\", \"editorial\"]\n        }\n        ```\n        Result:\n        ```\n        # Set\n        {\"compositing\"}\n        ```\n\n        Args:\n            task_names_by_asset_name (dict): Task names in iterable by parent.\n        \"\"\"\n        tasks = None\n        for task_names in task_names_by_asset_name.values():\n            if tasks is None:\n                tasks = set(task_names)\n            else:\n                tasks &= set(task_names)\n\n            if not tasks:\n                break\n        return tasks or set()\n\n    def is_task_name_valid(self, asset_name, task_name):\n        \"\"\"Is task name available for asset.\n\n        Args:\n            asset_name (str): Name of asset where should look for task.\n            task_name (str): Name of task which should be available in asset's\n                tasks.\n        \"\"\"\n        if asset_name not in self._task_names_by_asset_name:\n            return False\n\n        if self._allow_empty_task and not task_name:\n            return True\n\n        task_names = self._task_names_by_asset_name[asset_name]\n        if task_name in task_names:\n            return True\n        return False\n\n    def reset(self):\n        \"\"\"Update model by current context.\"\"\"\n        if not self._asset_names:\n            self._items_by_name = {}\n            self._task_names_by_asset_name = {}\n            self.clear()\n            return\n\n        task_names_by_asset_name = (\n            self._controller.get_task_names_by_asset_names(self._asset_names)\n        )\n\n        self._task_names_by_asset_name = task_names_by_asset_name\n\n        new_task_names = self.get_intersection_of_tasks(\n            task_names_by_asset_name\n        )\n        if self._allow_empty_task:\n            new_task_names.add(\"\")\n        old_task_names = set(self._items_by_name.keys())\n        if new_task_names == old_task_names:\n            return\n\n        root_item = self.invisibleRootItem()\n        for task_name in old_task_names:\n            if task_name not in new_task_names:\n                item = self._items_by_name.pop(task_name)\n                root_item.removeRow(item.row())\n\n        new_items = []\n        for task_name in new_task_names:\n            if task_name in self._items_by_name:\n                continue\n\n            item = QtGui.QStandardItem(task_name)\n            item.setData(task_name, TASK_NAME_ROLE)\n            if task_name:\n                item.setData(get_default_task_icon(), QtCore.Qt.DecorationRole)\n            self._items_by_name[task_name] = item\n            new_items.append(item)\n\n        if new_items:\n            root_item.appendRows(new_items)\n\n    def headerData(self, section, orientation, role=None):\n        if role is None:\n            role = QtCore.Qt.EditRole\n        # Show nice labels in the header\n        if section == 0:\n            if (\n                role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole)\n                and orientation == QtCore.Qt.Horizontal\n            ):\n                return \"Tasks\"\n\n        return super(TasksModel, self).headerData(section, orientation, role)\n\n\nclass CreateWidgetTasksWidget(TasksWidget):\n    def __init__(self, controller, parent):\n        self._controller = controller\n        super(CreateWidgetTasksWidget, self).__init__(None, parent)\n\n        self._enabled = None\n\n    def _create_source_model(self):\n        return TasksModel(self._controller)\n\n    def set_asset_name(self, asset_name):\n        current = self.get_selected_task_name()\n        if current:\n            self._last_selected_task_name = current\n\n        self._tasks_model.set_asset_names([asset_name])\n        if self._last_selected_task_name and self._enabled:\n            self.select_task_name(self._last_selected_task_name)\n\n        # Force a task changed emit.\n        self.task_changed.emit()\n\n    def select_task_name(self, task_name):\n        super(CreateWidgetTasksWidget, self).select_task_name(task_name)\n        if not self._enabled:\n            current = self.get_selected_task_name()\n            if current:\n                self._last_selected_task_name = current\n            self._clear_selection()\n\n    def set_enabled(self, enabled):\n        self._enabled = enabled\n        if not enabled:\n            last_selected_task_name = self.get_selected_task_name()\n            if last_selected_task_name:\n                self._last_selected_task_name = last_selected_task_name\n            self._clear_selection()\n\n        elif self._last_selected_task_name is not None:\n            self.select_task_name(self._last_selected_task_name)\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/thumbnail_widget.py",
    "content": "import os\nimport uuid\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.style import get_objected_colors\nfrom openpype.lib import (\n    run_subprocess,\n    is_oiio_supported,\n    get_oiio_tool_args,\n    get_ffmpeg_tool_args,\n)\nfrom openpype.lib.transcoding import (\n    IMAGE_EXTENSIONS,\n    VIDEO_EXTENSIONS,\n)\n\nfrom openpype.tools.utils import (\n    paint_image_with_color,\n    PixmapButton,\n)\nfrom openpype.tools.publisher.control import CardMessageTypes\n\nfrom .icons import get_image\nfrom .screenshot_widget import capture_to_file\n\n\nclass ThumbnailPainterWidget(QtWidgets.QWidget):\n    width_ratio = 3.0\n    height_ratio = 2.0\n    border_width = 1\n    max_thumbnails = 3\n    offset_sep = 4\n    checker_boxes_count = 20\n\n    def __init__(self, parent):\n        super(ThumbnailPainterWidget, self).__init__(parent)\n\n        border_color = get_objected_colors(\"bg-buttons\").get_qcolor()\n        thumbnail_bg_color = get_objected_colors(\"bg-view\").get_qcolor()\n        overlay_color = get_objected_colors(\"font\").get_qcolor()\n\n        default_image = get_image(\"thumbnail\")\n        default_pix = paint_image_with_color(default_image, border_color)\n\n        self.border_color = border_color\n        self.thumbnail_bg_color = thumbnail_bg_color\n        self.overlay_color = overlay_color\n        self._default_pix = default_pix\n\n        self._cached_pix = None\n        self._current_pixes = None\n        self._has_pixes = False\n\n    @property\n    def has_pixes(self):\n        return self._has_pixes\n\n    def clear_cache(self):\n        self._cached_pix = None\n        self.repaint()\n\n    def set_current_thumbnails(self, thumbnail_paths=None):\n        pixes = []\n        if thumbnail_paths:\n            for thumbnail_path in thumbnail_paths:\n                pixes.append(QtGui.QPixmap(thumbnail_path))\n\n        self._current_pixes = pixes or None\n        self._has_pixes = self._current_pixes is not None\n        self.clear_cache()\n\n    def paintEvent(self, event):\n        if self._cached_pix is None:\n            self._cache_pix()\n\n        painter = QtGui.QPainter()\n        painter.begin(self)\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n        painter.drawPixmap(0, 0, self._cached_pix)\n        painter.end()\n\n    def _paint_checker(self, width, height):\n        checker_size = int(float(width) / self.checker_boxes_count)\n        if checker_size < 1:\n            checker_size = 1\n\n        checker_pix = QtGui.QPixmap(checker_size * 2, checker_size * 2)\n        checker_pix.fill(QtCore.Qt.transparent)\n        checker_painter = QtGui.QPainter()\n        checker_painter.begin(checker_pix)\n        checker_painter.setPen(QtCore.Qt.NoPen)\n        checker_painter.setBrush(QtGui.QColor(89, 89, 89))\n        checker_painter.drawRect(\n            0, 0, checker_pix.width(), checker_pix.height()\n        )\n        checker_painter.setBrush(QtGui.QColor(188, 187, 187))\n        checker_painter.drawRect(\n            0, 0, checker_size, checker_size\n        )\n        checker_painter.drawRect(\n            checker_size, checker_size, checker_size, checker_size\n        )\n        checker_painter.end()\n        return checker_pix\n\n    def _paint_default_pix(self, pix_width, pix_height):\n        full_border_width = 2 * self.border_width\n        width = pix_width - full_border_width\n        height = pix_height - full_border_width\n        if width > 100:\n            width = int(width * 0.6)\n            height = int(height * 0.6)\n\n        scaled_pix = self._default_pix.scaled(\n            width,\n            height,\n            QtCore.Qt.KeepAspectRatio,\n            QtCore.Qt.SmoothTransformation\n        )\n        pos_x = int(\n            (pix_width - scaled_pix.width()) / 2\n        )\n        pos_y = int(\n            (pix_height - scaled_pix.height()) / 2\n        )\n        new_pix = QtGui.QPixmap(pix_width, pix_height)\n        new_pix.fill(QtCore.Qt.transparent)\n        pix_painter = QtGui.QPainter()\n        pix_painter.begin(new_pix)\n        render_hints = (\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n        )\n        if hasattr(QtGui.QPainter, \"HighQualityAntialiasing\"):\n            render_hints |= QtGui.QPainter.HighQualityAntialiasing\n\n        pix_painter.setRenderHints(render_hints)\n        pix_painter.drawPixmap(pos_x, pos_y, scaled_pix)\n        pix_painter.end()\n        return new_pix\n\n    def _draw_thumbnails(self, thumbnails, pix_width, pix_height):\n        full_border_width = 2 * self.border_width\n\n        checker_pix = self._paint_checker(pix_width, pix_height)\n\n        backgrounded_images = []\n        for src_pix in thumbnails:\n            scaled_pix = src_pix.scaled(\n                pix_width - full_border_width,\n                pix_height - full_border_width,\n                QtCore.Qt.KeepAspectRatio,\n                QtCore.Qt.SmoothTransformation\n            )\n            pos_x = int(\n                (pix_width - scaled_pix.width()) / 2\n            )\n            pos_y = int(\n                (pix_height - scaled_pix.height()) / 2\n            )\n\n            new_pix = QtGui.QPixmap(pix_width, pix_height)\n            new_pix.fill(QtCore.Qt.transparent)\n            pix_painter = QtGui.QPainter()\n            pix_painter.begin(new_pix)\n            render_hints = (\n                QtGui.QPainter.Antialiasing\n                | QtGui.QPainter.SmoothPixmapTransform\n            )\n            if hasattr(QtGui.QPainter, \"HighQualityAntialiasing\"):\n                render_hints |= QtGui.QPainter.HighQualityAntialiasing\n            pix_painter.setRenderHints(render_hints)\n\n            tiled_rect = QtCore.QRectF(\n                pos_x, pos_y, scaled_pix.width(), scaled_pix.height()\n            )\n            pix_painter.drawTiledPixmap(\n                tiled_rect,\n                checker_pix,\n                QtCore.QPointF(0.0, 0.0)\n            )\n            pix_painter.drawPixmap(pos_x, pos_y, scaled_pix)\n            pix_painter.end()\n            backgrounded_images.append(new_pix)\n        return backgrounded_images\n\n    def _paint_dash_line(self, painter, rect):\n        pen = QtGui.QPen()\n        pen.setWidth(1)\n        pen.setBrush(QtCore.Qt.darkGray)\n        pen.setStyle(QtCore.Qt.DashLine)\n\n        new_rect = rect.adjusted(1, 1, -1, -1)\n        painter.setPen(pen)\n        painter.setBrush(QtCore.Qt.transparent)\n        # painter.drawRect(rect)\n        painter.drawRect(new_rect)\n\n    def _cache_pix(self):\n        rect = self.rect()\n        rect_width = rect.width()\n        rect_height = rect.height()\n\n        pix_x_offset = 0\n        pix_y_offset = 0\n        expected_height = int(\n            (rect_width / self.width_ratio) * self.height_ratio\n        )\n        if expected_height > rect_height:\n            expected_height = rect_height\n            expected_width = int(\n                (rect_height / self.height_ratio) * self.width_ratio\n            )\n            pix_x_offset = (rect_width - expected_width) / 2\n        else:\n            expected_width = rect_width\n            pix_y_offset = (rect_height - expected_height) / 2\n\n        if self._current_pixes is None:\n            used_default_pix = True\n            pixes_to_draw = None\n            pixes_len = 1\n        else:\n            used_default_pix = False\n            pixes_to_draw = self._current_pixes\n            if len(pixes_to_draw) > self.max_thumbnails:\n                pixes_to_draw = pixes_to_draw[:-self.max_thumbnails]\n            pixes_len = len(pixes_to_draw)\n\n        width_offset, height_offset = self._get_pix_offset_size(\n            expected_width, expected_height, pixes_len\n        )\n        pix_width = expected_width - width_offset\n        pix_height = expected_height - height_offset\n\n        if used_default_pix:\n            thumbnail_images = [self._paint_default_pix(pix_width, pix_height)]\n        else:\n            thumbnail_images = self._draw_thumbnails(\n                pixes_to_draw, pix_width, pix_height\n            )\n\n        if pixes_len == 1:\n            width_offset_part = 0\n            height_offset_part = 0\n        else:\n            width_offset_part = int(float(width_offset) / (pixes_len - 1))\n            height_offset_part = int(float(height_offset) / (pixes_len - 1))\n        full_width_offset = width_offset + pix_x_offset\n\n        final_pix = QtGui.QPixmap(rect_width, rect_height)\n        final_pix.fill(QtCore.Qt.transparent)\n\n        bg_pen = QtGui.QPen()\n        bg_pen.setWidth(self.border_width)\n        bg_pen.setColor(self.border_color)\n\n        final_painter = QtGui.QPainter()\n        final_painter.begin(final_pix)\n        render_hints = (\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n        )\n        if hasattr(QtGui.QPainter, \"HighQualityAntialiasing\"):\n            render_hints |= QtGui.QPainter.HighQualityAntialiasing\n\n        final_painter.setRenderHints(render_hints)\n\n        final_painter.setBrush(QtGui.QBrush(self.thumbnail_bg_color))\n        final_painter.setPen(bg_pen)\n        final_painter.drawRect(rect)\n\n        for idx, pix in enumerate(thumbnail_images):\n            x_offset = full_width_offset - (width_offset_part * idx)\n            y_offset = (height_offset_part * idx) + pix_y_offset\n            final_painter.drawPixmap(x_offset, y_offset, pix)\n\n        # Draw drop enabled dashes\n        if used_default_pix:\n            self._paint_dash_line(final_painter, rect)\n\n        final_painter.end()\n\n        self._cached_pix = final_pix\n\n    def _get_pix_offset_size(self, width, height, image_count):\n        if image_count == 1:\n            return 0, 0\n\n        part_width = width / self.offset_sep\n        part_height = height / self.offset_sep\n        return part_width, part_height\n\n\nclass ThumbnailWidget(QtWidgets.QWidget):\n    \"\"\"Instance thumbnail widget.\"\"\"\n\n    thumbnail_created = QtCore.Signal(str)\n    thumbnail_cleared = QtCore.Signal()\n\n    def __init__(self, controller, parent):\n        # Missing implementation for thumbnail\n        # - widget kept to make a visial offset of global attr widget offset\n        super(ThumbnailWidget, self).__init__(parent)\n        self.setAcceptDrops(True)\n\n        thumbnail_painter = ThumbnailPainterWidget(self)\n\n        icon_color = get_objected_colors(\"bg-view-selection\").get_qcolor()\n        icon_color.setAlpha(255)\n\n        buttons_widget = QtWidgets.QWidget(self)\n        buttons_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        clear_image = get_image(\"clear_thumbnail\")\n        clear_pix = paint_image_with_color(clear_image, icon_color)\n        clear_button = PixmapButton(clear_pix, buttons_widget)\n        clear_button.setObjectName(\"ThumbnailPixmapHoverButton\")\n        clear_button.setToolTip(\"Clear thumbnail\")\n\n        take_screenshot_image = get_image(\"take_screenshot\")\n        take_screenshot_pix = paint_image_with_color(\n            take_screenshot_image, icon_color)\n        take_screenshot_btn = PixmapButton(\n            take_screenshot_pix, buttons_widget)\n        take_screenshot_btn.setObjectName(\"ThumbnailPixmapHoverButton\")\n        take_screenshot_btn.setToolTip(\"Take screenshot\")\n\n        paste_image = get_image(\"paste\")\n        paste_pix = paint_image_with_color(paste_image, icon_color)\n        paste_btn = PixmapButton(paste_pix, buttons_widget)\n        paste_btn.setObjectName(\"ThumbnailPixmapHoverButton\")\n        paste_btn.setToolTip(\"Paste from clipboard\")\n\n        browse_image = get_image(\"browse\")\n        browse_pix = paint_image_with_color(browse_image, icon_color)\n        browse_btn = PixmapButton(browse_pix, buttons_widget)\n        browse_btn.setObjectName(\"ThumbnailPixmapHoverButton\")\n        browse_btn.setToolTip(\"Browse...\")\n\n        buttons_layout = QtWidgets.QHBoxLayout(buttons_widget)\n        buttons_layout.setContentsMargins(0, 0, 0, 0)\n        buttons_layout.addWidget(take_screenshot_btn, 0)\n        buttons_layout.addWidget(paste_btn, 0)\n        buttons_layout.addWidget(browse_btn, 0)\n        buttons_layout.addWidget(clear_button, 0)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(thumbnail_painter)\n\n        clear_button.clicked.connect(self._on_clear_clicked)\n        take_screenshot_btn.clicked.connect(self._on_take_screenshot)\n        paste_btn.clicked.connect(self._on_paste_from_clipboard)\n        browse_btn.clicked.connect(self._on_browse_clicked)\n\n        self._controller = controller\n        self._output_dir = controller.get_thumbnail_temp_dir_path()\n\n        self._review_extensions = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS)\n\n        self._height = None\n        self._width = None\n        self._adapted_to_size = True\n        self._last_width = None\n        self._last_height = None\n        self._hide_on_finish = False\n\n        self._buttons_widget = buttons_widget\n        self._thumbnail_painter = thumbnail_painter\n        self._clear_button = clear_button\n        self._take_screenshot_btn = take_screenshot_btn\n        self._paste_btn = paste_btn\n        self._browse_btn = browse_btn\n\n        clear_button.setEnabled(False)\n\n    @property\n    def width_ratio(self):\n        return self._thumbnail_painter.width_ratio\n\n    @property\n    def height_ratio(self):\n        return self._thumbnail_painter.height_ratio\n\n    def _get_filepath_from_event(self, event):\n        mime_data = event.mimeData()\n        if not mime_data.hasUrls():\n            return None\n\n        filepaths = []\n        for url in mime_data.urls():\n            filepath = url.toLocalFile()\n            if os.path.exists(filepath):\n                filepaths.append(filepath)\n\n        if len(filepaths) == 1:\n            filepath = filepaths[0]\n            ext = os.path.splitext(filepath)[-1]\n            if ext in self._review_extensions:\n                return filepath\n        return None\n\n    def dragEnterEvent(self, event):\n        filepath = self._get_filepath_from_event(event)\n        if filepath:\n            event.setDropAction(QtCore.Qt.CopyAction)\n            event.accept()\n\n    def dragLeaveEvent(self, event):\n        event.accept()\n\n    def dropEvent(self, event):\n        filepath = self._get_filepath_from_event(event)\n        if not filepath:\n            return\n\n        output = export_thumbnail(filepath, self._output_dir)\n        if output:\n            self.thumbnail_created.emit(output)\n        else:\n            self._controller.emit_card_message(\n                \"Couldn't convert the source for thumbnail\",\n                CardMessageTypes.error\n            )\n\n    def set_adapted_to_hint(self, enabled):\n        self._adapted_to_size = enabled\n        if self._width is not None:\n            self.setMinimumHeight(0)\n            self._width = None\n\n        if self._height is not None:\n            self.setMinimumWidth(0)\n            self._height = None\n\n    def set_width(self, width):\n        if self._width == width:\n            return\n\n        self._adapted_to_size = False\n        self._width = width\n        self.setMinimumHeight(int(\n            (width / self.width_ratio) * self.height_ratio\n        ))\n        if self._height is not None:\n            self.setMinimumWidth(0)\n            self._height = None\n        self._thumbnail_painter.clear_cache()\n\n    def set_height(self, height):\n        if self._height == height:\n            return\n\n        self._height = height\n        self._adapted_to_size = False\n        self.setMinimumWidth(int(\n            (height / self.height_ratio) * self.width_ratio\n        ))\n        if self._width is not None:\n            self.setMinimumHeight(0)\n            self._width = None\n\n        self._thumbnail_painter.clear_cache()\n\n    def _set_current_thumbails(self, thumbnail_paths):\n        self._thumbnail_painter.set_current_thumbnails(thumbnail_paths)\n        self._update_buttons_position()\n\n    def set_current_thumbnails(self, thumbnail_paths=None):\n        self._thumbnail_painter.set_current_thumbnails(thumbnail_paths)\n        self._update_buttons_position()\n        self._clear_button.setEnabled(self._thumbnail_painter.has_pixes)\n\n    def _on_clear_clicked(self):\n        self.set_current_thumbnails()\n        self.thumbnail_cleared.emit()\n        self._clear_button.setEnabled(False)\n\n    def _on_take_screenshot(self):\n        window = self.window()\n        state = window.windowState()\n        window.setWindowState(QtCore.Qt.WindowMinimized)\n        output_path = os.path.join(\n            self._output_dir, uuid.uuid4().hex + \".png\")\n        if capture_to_file(output_path):\n            self.thumbnail_created.emit(output_path)\n        # restore original window state\n        window.setWindowState(state)\n\n    def _on_paste_from_clipboard(self):\n        \"\"\"Set thumbnail from a pixmap image in the system clipboard\"\"\"\n\n        clipboard = QtWidgets.QApplication.clipboard()\n        pixmap = clipboard.pixmap()\n        if pixmap.isNull():\n            return\n\n        # Save as temporary file\n        output_path = os.path.join(\n            self._output_dir, uuid.uuid4().hex + \".png\")\n\n        output_dir = os.path.dirname(output_path)\n        if not os.path.exists(output_dir):\n            os.makedirs(output_dir)\n\n        if pixmap.save(output_path):\n            self.thumbnail_created.emit(output_path)\n\n    def _on_browse_clicked(self):\n        ext_filter = \"Source (*{0})\".format(\n            \" *\".join(self._review_extensions)\n        )\n        filepath, _ = QtWidgets.QFileDialog.getOpenFileName(\n            self, \"Choose thumbnail\", os.path.expanduser(\"~\"), ext_filter\n        )\n        if not filepath:\n            return\n        valid_path = False\n        ext = os.path.splitext(filepath)[-1].lower()\n        if ext in self._review_extensions:\n            valid_path = True\n\n        output = None\n        if valid_path:\n            output = export_thumbnail(filepath, self._output_dir)\n\n        if output:\n            self.thumbnail_created.emit(output)\n        else:\n            self._controller.emit_card_message(\n                \"Couldn't convert the source for thumbnail\",\n                CardMessageTypes.error\n            )\n\n    def _adapt_to_size(self):\n        if not self._adapted_to_size:\n            return\n\n        width = self.width()\n        height = self.height()\n        if width == self._last_width and height == self._last_height:\n            return\n\n        self._last_width = width\n        self._last_height = height\n        self._thumbnail_painter.clear_cache()\n\n    def _update_buttons_position(self):\n        size = self.size()\n        my_width = size.width()\n        my_height = size.height()\n        buttons_sh = self._buttons_widget.sizeHint()\n        buttons_height = buttons_sh.height()\n        buttons_width = buttons_sh.width()\n        pos_x = my_width - (buttons_width + 3)\n        pos_y = my_height - (buttons_height + 3)\n        if pos_x < 0:\n            pos_x = 0\n            buttons_width = my_width\n        if pos_y < 0:\n            pos_y = 0\n            buttons_height = my_height\n        self._buttons_widget.setGeometry(\n            pos_x,\n            pos_y,\n            buttons_width,\n            buttons_height\n        )\n\n    def resizeEvent(self, event):\n        super(ThumbnailWidget, self).resizeEvent(event)\n        self._adapt_to_size()\n        self._update_buttons_position()\n\n    def showEvent(self, event):\n        super(ThumbnailWidget, self).showEvent(event)\n        self._adapt_to_size()\n        self._update_buttons_position()\n\n\ndef _run_silent_subprocess(args):\n    with open(os.devnull, \"w\") as devnull:\n        run_subprocess(args, stdout=devnull, stderr=devnull)\n\n\ndef _convert_thumbnail_oiio(src_path, dst_path):\n    if not is_oiio_supported():\n        return None\n\n    oiio_cmd = get_oiio_tool_args(\n        \"oiiotool\",\n        \"-i\", src_path,\n        \"--subimage\", \"0\",\n        \"-o\", dst_path\n    )\n    try:\n        _run_silent_subprocess(oiio_cmd)\n    except Exception:\n        return None\n    return dst_path\n\n\ndef _convert_thumbnail_ffmpeg(src_path, dst_path):\n    ffmpeg_cmd = get_ffmpeg_tool_args(\n        \"ffmpeg\",\n        \"-y\",\n        \"-i\", src_path,\n        dst_path\n    )\n    try:\n        _run_silent_subprocess(ffmpeg_cmd)\n    except Exception:\n        return None\n    return dst_path\n\n\ndef export_thumbnail(src_path, root_dir):\n    if not os.path.exists(root_dir):\n        os.makedirs(root_dir)\n\n    ext = os.path.splitext(src_path)[-1]\n    if ext not in (\".jpeg\", \".jpg\", \".png\"):\n        ext = \".jpeg\"\n    filename = str(uuid.uuid4()) + ext\n    dst_path = os.path.join(root_dir, filename)\n\n    output_path = _convert_thumbnail_oiio(src_path, dst_path)\n    if not output_path:\n        output_path = _convert_thumbnail_ffmpeg(src_path, dst_path)\n    return output_path\n"
  },
  {
    "path": "openpype/tools/publisher/widgets/widgets.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nimport re\nimport copy\nimport functools\nimport uuid\nimport shutil\nimport collections\nfrom qtpy import QtWidgets, QtCore, QtGui\nimport qtawesome\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.lib.attribute_definitions import UnknownDef\nfrom openpype.tools.attribute_defs import create_widget_for_attr_def\nfrom openpype.tools import resources\nfrom openpype.tools.flickcharm import FlickCharm\nfrom openpype.tools.utils import (\n    PlaceholderLineEdit,\n    IconButton,\n    PixmapLabel,\n    BaseClickableFrame,\n    set_style_property,\n)\nfrom openpype.style import get_objected_colors\nfrom openpype.pipeline.create import (\n    SUBSET_NAME_ALLOWED_SYMBOLS,\n    TaskNotSetError,\n)\nfrom .thumbnail_widget import ThumbnailWidget\nfrom .assets_widget import AssetsDialog\nfrom .tasks_widget import TasksModel\nfrom .icons import (\n    get_pixmap,\n    get_icon_path\n)\n\nfrom ..constants import (\n    VARIANT_TOOLTIP,\n    ResetKeySequence,\n    INPUTS_LAYOUT_HSPACING,\n    INPUTS_LAYOUT_VSPACING,\n)\n\nFA_PREFIXES = [\"\", \"fa.\", \"fa5.\", \"fa5b.\", \"fa5s.\", \"ei.\", \"mdi.\"]\n\n\ndef parse_icon_def(\n    icon_def, default_width=None, default_height=None, color=None\n):\n    if not icon_def:\n        return None\n\n    if isinstance(icon_def, QtGui.QPixmap):\n        return icon_def\n\n    color = color or \"white\"\n    default_width = default_width or 512\n    default_height = default_height or 512\n\n    if isinstance(icon_def, QtGui.QIcon):\n        return icon_def.pixmap(default_width, default_height)\n\n    try:\n        if os.path.exists(icon_def):\n            return QtGui.QPixmap(icon_def)\n    except Exception:\n        # TODO logging\n        pass\n\n    for prefix in FA_PREFIXES:\n        try:\n            icon_name = \"{}{}\".format(prefix, icon_def)\n            icon = qtawesome.icon(icon_name, color=color)\n            return icon.pixmap(default_width, default_height)\n        except Exception:\n            # TODO logging\n            continue\n\n\nclass PublishPixmapLabel(PixmapLabel):\n    def _get_pix_size(self):\n        size = self.fontMetrics().height()\n        size += size % 2\n        return size, size\n\n\nclass IconValuePixmapLabel(PublishPixmapLabel):\n    \"\"\"Label resizing to width and height of font.\n\n    Handle icon parsing from creators/instances. Using of QAwesome module\n    of path to images.\n    \"\"\"\n    default_size = 200\n\n    def __init__(self, icon_def, parent):\n        source_pixmap = self._parse_icon_def(icon_def)\n\n        super(IconValuePixmapLabel, self).__init__(source_pixmap, parent)\n\n    def set_icon_def(self, icon_def):\n        \"\"\"Set icon by it's definition name.\n\n        Args:\n            icon_def (str): Name of FontAwesome icon or path to image.\n        \"\"\"\n        source_pixmap = self._parse_icon_def(icon_def)\n        self.set_source_pixmap(source_pixmap)\n\n    def _default_pixmap(self):\n        pix = QtGui.QPixmap(1, 1)\n        pix.fill(QtCore.Qt.transparent)\n        return pix\n\n    def _parse_icon_def(self, icon_def):\n        icon = parse_icon_def(icon_def, self.default_size, self.default_size)\n        if icon:\n            return icon\n        return self._default_pixmap()\n\n\nclass ContextWarningLabel(PublishPixmapLabel):\n    \"\"\"Pixmap label with warning icon.\"\"\"\n    def __init__(self, parent):\n        pix = get_pixmap(\"warning\")\n\n        super(ContextWarningLabel, self).__init__(pix, parent)\n\n        self.setToolTip(\n            \"Contain invalid context. Please check details.\"\n        )\n        self.setObjectName(\"FamilyIconLabel\")\n\n\nclass PublishIconBtn(IconButton):\n    \"\"\"Button using alpha of source image to redraw with different color.\n\n    Main class for buttons showed in publisher.\n\n    TODO:\n    Add different states:\n    - normal           : before publishing\n    - publishing       : publishing is running\n    - validation error : validation error happened\n    - error            : other error happened\n    - success          : publishing finished\n    \"\"\"\n\n    def __init__(self, pixmap_path, *args, **kwargs):\n        super(PublishIconBtn, self).__init__(*args, **kwargs)\n\n        colors = get_objected_colors()\n        icon = self.generate_icon(\n            pixmap_path,\n            enabled_color=colors[\"font\"].get_qcolor(),\n            disabled_color=colors[\"font-disabled\"].get_qcolor())\n        self.setIcon(icon)\n\n    def generate_icon(self, pixmap_path, enabled_color, disabled_color):\n        icon = QtGui.QIcon()\n        image = QtGui.QImage(pixmap_path)\n        enabled_pixmap = self.paint_image_with_color(image, enabled_color)\n        icon.addPixmap(enabled_pixmap, QtGui.QIcon.Normal)\n        disabled_pixmap = self.paint_image_with_color(image, disabled_color)\n        icon.addPixmap(disabled_pixmap, QtGui.QIcon.Disabled)\n        return icon\n\n    @staticmethod\n    def paint_image_with_color(image, color):\n        \"\"\"Redraw image with single color using it's alpha.\n\n        It is expected that input image is singlecolor image with alpha.\n\n        Args:\n            image (QImage): Loaded image with alpha.\n            color (QColor): Color that will be used to paint image.\n        \"\"\"\n        width = image.width()\n        height = image.height()\n        partition = 8\n        part_w = int(width / partition)\n        part_h = int(height / partition)\n        part_w -= part_w % 2\n        part_h -= part_h % 2\n        scaled_image = image.scaled(\n            width - (2 * part_w),\n            height - (2 * part_h),\n            QtCore.Qt.IgnoreAspectRatio,\n            QtCore.Qt.SmoothTransformation\n        )\n        alpha_mask = scaled_image.createAlphaMask()\n        alpha_region = QtGui.QRegion(QtGui.QBitmap.fromImage(alpha_mask))\n        alpha_region.translate(part_w, part_h)\n\n        pixmap = QtGui.QPixmap(width, height)\n        pixmap.fill(QtCore.Qt.transparent)\n\n        painter = QtGui.QPainter(pixmap)\n        painter.setClipRegion(alpha_region)\n        painter.setPen(QtCore.Qt.NoPen)\n        painter.setBrush(color)\n        painter.drawRect(QtCore.QRect(0, 0, width, height))\n        painter.end()\n\n        return pixmap\n\n\nclass CreateBtn(PublishIconBtn):\n    \"\"\"Create instance button.\"\"\"\n\n    def __init__(self, parent=None):\n        icon_path = get_icon_path(\"create\")\n        super(CreateBtn, self).__init__(icon_path, \"Create\", parent)\n        self.setToolTip(\"Create new {}/s\".format(\n            \"product\" if AYON_SERVER_ENABLED else \"subset\"\n        ))\n        self.setLayoutDirection(QtCore.Qt.RightToLeft)\n\n\nclass SaveBtn(PublishIconBtn):\n    \"\"\"Save context and instances information.\"\"\"\n    def __init__(self, parent=None):\n        icon_path = get_icon_path(\"save\")\n        super(SaveBtn, self).__init__(icon_path, parent)\n        self.setToolTip(\n            \"Save changes ({})\".format(\n                QtGui.QKeySequence(QtGui.QKeySequence.Save).toString()\n            )\n        )\n\n\nclass ResetBtn(PublishIconBtn):\n    \"\"\"Publish reset button.\"\"\"\n    def __init__(self, parent=None):\n        icon_path = get_icon_path(\"refresh\")\n        super(ResetBtn, self).__init__(icon_path, parent)\n        self.setToolTip(\n            \"Reset & discard changes ({})\".format(ResetKeySequence.toString())\n        )\n\n\nclass StopBtn(PublishIconBtn):\n    \"\"\"Publish stop button.\"\"\"\n    def __init__(self, parent):\n        icon_path = get_icon_path(\"stop\")\n        super(StopBtn, self).__init__(icon_path, parent)\n        self.setToolTip(\"Stop/Pause publishing\")\n\n\nclass ValidateBtn(PublishIconBtn):\n    \"\"\"Publish validate button.\"\"\"\n    def __init__(self, parent=None):\n        icon_path = get_icon_path(\"validate\")\n        super(ValidateBtn, self).__init__(icon_path, parent)\n        self.setToolTip(\"Validate\")\n\n\nclass PublishBtn(PublishIconBtn):\n    \"\"\"Publish start publish button.\"\"\"\n    def __init__(self, parent=None):\n        icon_path = get_icon_path(\"play\")\n        super(PublishBtn, self).__init__(icon_path, \"Publish\", parent)\n        self.setToolTip(\"Publish\")\n\n\nclass CreateInstanceBtn(PublishIconBtn):\n    \"\"\"Create add button.\"\"\"\n    def __init__(self, parent=None):\n        icon_path = get_icon_path(\"add\")\n        super(CreateInstanceBtn, self).__init__(icon_path, parent)\n        self.setToolTip(\"Create new instance\")\n\n\nclass PublishReportBtn(PublishIconBtn):\n    \"\"\"Publish report button.\"\"\"\n\n    triggered = QtCore.Signal(str)\n\n    def __init__(self, parent=None):\n        icon_path = get_icon_path(\"view_report\")\n        super(PublishReportBtn, self).__init__(icon_path, parent)\n        self.setToolTip(\"Copy report\")\n        self._actions = []\n\n    def add_action(self, label, identifier):\n        self._actions.append(\n            (label, identifier)\n        )\n\n    def _on_action_trigger(self, identifier):\n        self.triggered.emit(identifier)\n\n    def mouseReleaseEvent(self, event):\n        super(PublishReportBtn, self).mouseReleaseEvent(event)\n        menu = QtWidgets.QMenu(self)\n        actions = []\n        for item in self._actions:\n            label, identifier = item\n            action = QtWidgets.QAction(label, menu)\n            action.triggered.connect(\n                functools.partial(self._on_action_trigger, identifier)\n            )\n            actions.append(action)\n        menu.addActions(actions)\n        menu.exec_(event.globalPos())\n\n\nclass RemoveInstanceBtn(PublishIconBtn):\n    \"\"\"Create remove button.\"\"\"\n    def __init__(self, parent=None):\n        icon_path = resources.get_icon_path(\"delete\")\n        super(RemoveInstanceBtn, self).__init__(icon_path, parent)\n        self.setToolTip(\"Remove selected instances\")\n\n\nclass ChangeViewBtn(PublishIconBtn):\n    \"\"\"Create toggle view button.\"\"\"\n    def __init__(self, parent=None):\n        icon_path = get_icon_path(\"change_view\")\n        super(ChangeViewBtn, self).__init__(icon_path, parent)\n        self.setToolTip(\"Swap between views\")\n\n\nclass AbstractInstanceView(QtWidgets.QWidget):\n    \"\"\"Abstract class for instance view in creation part.\"\"\"\n    selection_changed = QtCore.Signal()\n    active_changed = QtCore.Signal()\n    # Refreshed attribute is not changed by view itself\n    # - widget which triggers `refresh` is changing the state\n    # TODO store that information in widget which cares about refreshing\n    refreshed = False\n\n    def set_refreshed(self, refreshed):\n        \"\"\"View is refreshed with last instances.\n\n        Views are not updated all the time. Only if are visible.\n        \"\"\"\n        self.refreshed = refreshed\n\n    def refresh(self):\n        \"\"\"Refresh instances in the view from current `CreatedContext`.\"\"\"\n        raise NotImplementedError((\n            \"{} Method 'refresh' is not implemented.\"\n        ).format(self.__class__.__name__))\n\n    def has_items(self):\n        \"\"\"View has at least one item.\n\n        This is more a question for controller but is called from widget\n        which should probably should not use controller.\n\n        Returns:\n            bool: There is at least one instance or conversion item.\n        \"\"\"\n\n        raise NotImplementedError((\n            \"{} Method 'has_items' is not implemented.\"\n        ).format(self.__class__.__name__))\n\n    def get_selected_items(self):\n        \"\"\"Selected instances required for callbacks.\n\n        Example: When delete button is clicked to know what should be deleted.\n        \"\"\"\n\n        raise NotImplementedError((\n            \"{} Method 'get_selected_items' is not implemented.\"\n        ).format(self.__class__.__name__))\n\n    def set_selected_items(self, instance_ids, context_selected):\n        \"\"\"Change selection for instances and context.\n\n        Used to applying selection from one view to other.\n\n        Args:\n            instance_ids (List[str]): Selected instance ids.\n            context_selected (bool): Context is selected.\n        \"\"\"\n\n        raise NotImplementedError((\n            \"{} Method 'set_selected_items' is not implemented.\"\n        ).format(self.__class__.__name__))\n\n    def set_active_toggle_enabled(self, enabled):\n        \"\"\"Instances are disabled for changing enabled state.\n\n        Active state should stay the same until is \"unset\".\n\n        Args:\n            enabled (bool): Instance state can be changed.\n        \"\"\"\n\n        raise NotImplementedError((\n            \"{} Method 'set_active_toggle_enabled' is not implemented.\"\n        ).format(self.__class__.__name__))\n\n\nclass ClickableLineEdit(QtWidgets.QLineEdit):\n    \"\"\"QLineEdit capturing left mouse click.\n\n    Triggers `clicked` signal on mouse click.\n    \"\"\"\n    clicked = QtCore.Signal()\n\n    def __init__(self, *args, **kwargs):\n        super(ClickableLineEdit, self).__init__(*args, **kwargs)\n        self.setReadOnly(True)\n        self._mouse_pressed = False\n\n    def mousePressEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self._mouse_pressed = True\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        if self._mouse_pressed:\n            self._mouse_pressed = False\n            if self.rect().contains(event.pos()):\n                self.clicked.emit()\n        event.accept()\n\n    def mouseDoubleClickEvent(self, event):\n        event.accept()\n\n\nclass AssetsField(BaseClickableFrame):\n    \"\"\"Field where asset name of selected instance/s is showed.\n\n    Click on the field will trigger `AssetsDialog`.\n    \"\"\"\n    value_changed = QtCore.Signal()\n\n    def __init__(self, controller, parent):\n        super(AssetsField, self).__init__(parent)\n        self.setObjectName(\"AssetNameInputWidget\")\n\n        # Don't use 'self' for parent!\n        # - this widget has specific styles\n        dialog = AssetsDialog(controller, parent)\n\n        name_input = ClickableLineEdit(self)\n        name_input.setObjectName(\"AssetNameInput\")\n\n        icon_name = \"fa.window-maximize\"\n        icon = qtawesome.icon(icon_name, color=\"white\")\n        icon_btn = QtWidgets.QPushButton(self)\n        icon_btn.setIcon(icon)\n        icon_btn.setObjectName(\"AssetNameInputButton\")\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(0)\n        layout.addWidget(name_input, 1)\n        layout.addWidget(icon_btn, 0)\n\n        # Make sure all widgets are vertically extended to highest widget\n        for widget in (\n            name_input,\n            icon_btn\n        ):\n            size_policy = widget.sizePolicy()\n            size_policy.setVerticalPolicy(\n                QtWidgets.QSizePolicy.MinimumExpanding)\n            widget.setSizePolicy(size_policy)\n        name_input.clicked.connect(self._mouse_release_callback)\n        icon_btn.clicked.connect(self._mouse_release_callback)\n        dialog.finished.connect(self._on_dialog_finish)\n\n        self._dialog = dialog\n        self._name_input = name_input\n        self._icon_btn = icon_btn\n\n        self._origin_value = []\n        self._origin_selection = []\n        self._selected_items = []\n        self._has_value_changed = False\n        self._is_valid = True\n        self._multiselection_text = None\n\n    def _on_dialog_finish(self, result):\n        if not result:\n            return\n\n        asset_name = self._dialog.get_selected_asset()\n        if asset_name is None:\n            return\n\n        self._selected_items = [asset_name]\n        self._has_value_changed = (\n            self._origin_value != self._selected_items\n        )\n        self.set_text(asset_name)\n        self._set_is_valid(True)\n\n        self.value_changed.emit()\n\n    def _mouse_release_callback(self):\n        self._dialog.set_selected_assets(self._selected_items)\n        self._dialog.open()\n\n    def set_multiselection_text(self, text):\n        \"\"\"Change text for multiselection of different assets.\n\n        When there are selected multiple instances at once and they don't have\n        same asset in context.\n        \"\"\"\n        self._multiselection_text = text\n\n    def _set_is_valid(self, valid):\n        if valid == self._is_valid:\n            return\n        self._is_valid = valid\n        state = \"\"\n        if not valid:\n            state = \"invalid\"\n        self._set_state_property(state)\n\n    def _set_state_property(self, state):\n        set_style_property(self, \"state\", state)\n        set_style_property(self._name_input, \"state\", state)\n        set_style_property(self._icon_btn, \"state\", state)\n\n    def is_valid(self):\n        \"\"\"Is asset valid.\"\"\"\n        return self._is_valid\n\n    def has_value_changed(self):\n        \"\"\"Value of asset has changed.\"\"\"\n        return self._has_value_changed\n\n    def get_selected_items(self):\n        \"\"\"Selected asset names.\"\"\"\n        return list(self._selected_items)\n\n    def set_text(self, text):\n        \"\"\"Set text in text field.\n\n        Does not change selected items (assets).\n        \"\"\"\n        self._name_input.setText(text)\n        self._name_input.end(False)\n\n    def set_selected_items(self, asset_names=None):\n        \"\"\"Set asset names for selection of instances.\n\n        Passed asset names are validated and if there are 2 or more different\n        asset names then multiselection text is shown.\n\n        Args:\n            asset_names (list, tuple, set, NoneType): List of asset names.\n        \"\"\"\n        if asset_names is None:\n            asset_names = []\n\n        self._has_value_changed = False\n        self._origin_value = list(asset_names)\n        self._selected_items = list(asset_names)\n        is_valid = True\n        if not asset_names:\n            self.set_text(\"\")\n\n        elif len(asset_names) == 1:\n            asset_name = tuple(asset_names)[0]\n            is_valid = self._dialog.name_is_valid(asset_name)\n            self.set_text(asset_name)\n        else:\n            for asset_name in asset_names:\n                is_valid = self._dialog.name_is_valid(asset_name)\n                if not is_valid:\n                    break\n\n            multiselection_text = self._multiselection_text\n            if multiselection_text is None:\n                multiselection_text = \"|\".join(asset_names)\n            self.set_text(multiselection_text)\n\n        self._set_is_valid(is_valid)\n\n    def reset_to_origin(self):\n        \"\"\"Change to asset names set with last `set_selected_items` call.\"\"\"\n        self.set_selected_items(self._origin_value)\n\n    def confirm_value(self):\n        self._origin_value = copy.deepcopy(self._selected_items)\n        self._has_value_changed = False\n\n\nclass TasksComboboxProxy(QtCore.QSortFilterProxyModel):\n    def __init__(self, *args, **kwargs):\n        super(TasksComboboxProxy, self).__init__(*args, **kwargs)\n        self._filter_empty = False\n\n    def set_filter_empty(self, filter_empty):\n        if self._filter_empty is filter_empty:\n            return\n        self._filter_empty = filter_empty\n        self.invalidate()\n\n    def filterAcceptsRow(self, source_row, parent_index):\n        if self._filter_empty:\n            model = self.sourceModel()\n            source_index = model.index(\n                source_row, self.filterKeyColumn(), parent_index\n            )\n            if not source_index.data(QtCore.Qt.DisplayRole):\n                return False\n        return True\n\n\nclass TasksCombobox(QtWidgets.QComboBox):\n    \"\"\"Combobox to show tasks for selected instances.\n\n    Combobox gives ability to select only from intersection of task names for\n    asset names in selected instances.\n\n    If asset names in selected instances does not have same tasks then combobox\n    will be empty.\n    \"\"\"\n    value_changed = QtCore.Signal()\n\n    def __init__(self, controller, parent):\n        super(TasksCombobox, self).__init__(parent)\n        self.setObjectName(\"TasksCombobox\")\n\n        # Set empty delegate to propagate stylesheet to a combobox\n        delegate = QtWidgets.QStyledItemDelegate()\n        self.setItemDelegate(delegate)\n\n        model = TasksModel(controller, True)\n        proxy_model = TasksComboboxProxy()\n        proxy_model.setSourceModel(model)\n        self.setModel(proxy_model)\n\n        self.currentIndexChanged.connect(self._on_index_change)\n\n        self._delegate = delegate\n        self._model = model\n        self._proxy_model = proxy_model\n        self._origin_value = []\n        self._origin_selection = []\n        self._selected_items = []\n        self._has_value_changed = False\n        self._ignore_index_change = False\n        self._multiselection_text = None\n        self._is_valid = True\n\n        self._text = None\n\n        # Make sure combobox is extended horizontally\n        size_policy = self.sizePolicy()\n        size_policy.setHorizontalPolicy(\n            QtWidgets.QSizePolicy.MinimumExpanding)\n        self.setSizePolicy(size_policy)\n\n    def set_invalid_empty_task(self, invalid=True):\n        self._proxy_model.set_filter_empty(invalid)\n        if invalid:\n            self._set_is_valid(False)\n            self.set_text(\n                \"< One or more {} require Task selected >\".format(\n                    \"products\" if AYON_SERVER_ENABLED else \"subsets\"\n                )\n            )\n        else:\n            self.set_text(None)\n\n    def set_multiselection_text(self, text):\n        \"\"\"Change text shown when multiple different tasks are in context.\"\"\"\n        self._multiselection_text = text\n\n    def _on_index_change(self):\n        if self._ignore_index_change:\n            return\n\n        self.set_text(None)\n        text = self.currentText()\n        idx = self.findText(text)\n        if idx < 0:\n            return\n\n        self._set_is_valid(True)\n        self._selected_items = [text]\n        self._has_value_changed = (\n            self._origin_selection != self._selected_items\n        )\n\n        self.value_changed.emit()\n\n    def set_text(self, text):\n        \"\"\"Set context shown in combobox without changing selected items.\"\"\"\n        if text == self._text:\n            return\n\n        self._text = text\n        self.repaint()\n\n    def paintEvent(self, event):\n        \"\"\"Paint custom text without using QLineEdit.\n\n        The easiest way how to draw custom text in combobox and keep combobox\n        properties and event handling.\n        \"\"\"\n        painter = QtGui.QPainter(self)\n        painter.setPen(self.palette().color(QtGui.QPalette.Text))\n        opt = QtWidgets.QStyleOptionComboBox()\n        self.initStyleOption(opt)\n        if self._text is not None:\n            opt.currentText = self._text\n\n        style = self.style()\n        style.drawComplexControl(\n            QtWidgets.QStyle.CC_ComboBox, opt, painter, self\n        )\n        style.drawControl(\n            QtWidgets.QStyle.CE_ComboBoxLabel, opt, painter, self\n        )\n        painter.end()\n\n    def is_valid(self):\n        \"\"\"Are all selected items valid.\"\"\"\n        return self._is_valid\n\n    def has_value_changed(self):\n        \"\"\"Did selection of task changed.\"\"\"\n        return self._has_value_changed\n\n    def _set_is_valid(self, valid):\n        if valid == self._is_valid:\n            return\n        self._is_valid = valid\n        state = \"\"\n        if not valid:\n            state = \"invalid\"\n        self._set_state_property(state)\n\n    def _set_state_property(self, state):\n        current_value = self.property(\"state\")\n        if current_value != state:\n            self.setProperty(\"state\", state)\n            self.style().polish(self)\n\n    def get_selected_items(self):\n        \"\"\"Get selected tasks.\n\n        If value has changed then will return list with single item.\n\n        Returns:\n            list: Selected tasks.\n        \"\"\"\n        return list(self._selected_items)\n\n    def set_asset_names(self, asset_names):\n        \"\"\"Set asset names for which should show tasks.\"\"\"\n        self._ignore_index_change = True\n\n        self._model.set_asset_names(asset_names)\n        self._proxy_model.set_filter_empty(False)\n        self._proxy_model.sort(0)\n\n        self._ignore_index_change = False\n\n        # It is a bug if not exactly one asset got here\n        if len(asset_names) != 1:\n            self.set_selected_item(\"\")\n            self._set_is_valid(False)\n            return\n\n        asset_name = tuple(asset_names)[0]\n\n        is_valid = False\n        if self._selected_items:\n            is_valid = True\n\n        valid_task_names = []\n        for task_name in self._selected_items:\n            _is_valid = self._model.is_task_name_valid(asset_name, task_name)\n            if _is_valid:\n                valid_task_names.append(task_name)\n            else:\n                is_valid = _is_valid\n\n        self._selected_items = valid_task_names\n        if len(self._selected_items) == 0:\n            self.set_selected_item(\"\")\n\n        elif len(self._selected_items) == 1:\n            self.set_selected_item(self._selected_items[0])\n\n        else:\n            multiselection_text = self._multiselection_text\n            if multiselection_text is None:\n                multiselection_text = \"|\".join(self._selected_items)\n            self.set_selected_item(multiselection_text)\n\n        self._set_is_valid(is_valid)\n\n    def confirm_value(self, asset_names):\n        new_task_name = self._selected_items[0]\n        self._origin_value = [\n            (asset_name, new_task_name)\n            for asset_name in asset_names\n        ]\n        self._origin_selection = copy.deepcopy(self._selected_items)\n        self._has_value_changed = False\n\n    def set_selected_items(self, asset_task_combinations=None):\n        \"\"\"Set items for selected instances.\n\n        Args:\n            asset_task_combinations (list): List of tuples. Each item in\n                the list contain asset name and task name.\n        \"\"\"\n        self._proxy_model.set_filter_empty(False)\n        self._proxy_model.sort(0)\n\n        if asset_task_combinations is None:\n            asset_task_combinations = []\n\n        task_names = set()\n        task_names_by_asset_name = collections.defaultdict(set)\n        for asset_name, task_name in asset_task_combinations:\n            task_names.add(task_name)\n            task_names_by_asset_name[asset_name].add(task_name)\n        asset_names = set(task_names_by_asset_name.keys())\n\n        self._ignore_index_change = True\n\n        self._model.set_asset_names(asset_names)\n\n        self._has_value_changed = False\n\n        self._origin_value = copy.deepcopy(asset_task_combinations)\n\n        self._origin_selection = list(task_names)\n        self._selected_items = list(task_names)\n        # Reset current index\n        self.setCurrentIndex(-1)\n        is_valid = True\n        if not task_names:\n            self.set_selected_item(\"\")\n\n        elif len(task_names) == 1:\n            task_name = tuple(task_names)[0]\n            idx = self.findText(task_name)\n            is_valid = not idx < 0\n            if not is_valid and len(asset_names) > 1:\n                is_valid = self._validate_task_names_by_asset_names(\n                    task_names_by_asset_name\n                )\n            self.set_selected_item(task_name)\n\n        else:\n            for task_name in task_names:\n                idx = self.findText(task_name)\n                is_valid = not idx < 0\n                if not is_valid:\n                    break\n\n            if not is_valid and len(asset_names) > 1:\n                is_valid = self._validate_task_names_by_asset_names(\n                    task_names_by_asset_name\n                )\n            multiselection_text = self._multiselection_text\n            if multiselection_text is None:\n                multiselection_text = \"|\".join(task_names)\n            self.set_selected_item(multiselection_text)\n\n        self._set_is_valid(is_valid)\n\n        self._ignore_index_change = False\n\n        self.value_changed.emit()\n\n    def _validate_task_names_by_asset_names(self, task_names_by_asset_name):\n        for asset_name, task_names in task_names_by_asset_name.items():\n            for task_name in task_names:\n                if not self._model.is_task_name_valid(asset_name, task_name):\n                    return False\n        return True\n\n    def set_selected_item(self, item_name):\n        \"\"\"Set task which is set on selected instance.\n\n        Args:\n            item_name(str): Task name which should be selected.\n        \"\"\"\n        idx = self.findText(item_name)\n        # Set current index (must be set to -1 if is invalid)\n        self.setCurrentIndex(idx)\n        self.set_text(item_name)\n\n    def reset_to_origin(self):\n        \"\"\"Change to task names set with last `set_selected_items` call.\"\"\"\n        self.set_selected_items(self._origin_value)\n\n\nclass VariantInputWidget(PlaceholderLineEdit):\n    \"\"\"Input widget for variant.\"\"\"\n    value_changed = QtCore.Signal()\n\n    def __init__(self, parent):\n        super(VariantInputWidget, self).__init__(parent)\n\n        self.setObjectName(\"VariantInput\")\n        self.setToolTip(VARIANT_TOOLTIP)\n\n        name_pattern = \"^[{}]*$\".format(SUBSET_NAME_ALLOWED_SYMBOLS)\n        self._name_pattern = name_pattern\n        self._compiled_name_pattern = re.compile(name_pattern)\n\n        self._origin_value = []\n        self._current_value = []\n\n        self._ignore_value_change = False\n        self._has_value_changed = False\n        self._multiselection_text = None\n\n        self._is_valid = True\n\n        self.textChanged.connect(self._on_text_change)\n\n    def is_valid(self):\n        \"\"\"Is variant text valid.\"\"\"\n        return self._is_valid\n\n    def has_value_changed(self):\n        \"\"\"Value of variant has changed.\"\"\"\n        return self._has_value_changed\n\n    def _set_state_property(self, state):\n        current_value = self.property(\"state\")\n        if current_value != state:\n            self.setProperty(\"state\", state)\n            self.style().polish(self)\n\n    def set_multiselection_text(self, text):\n        \"\"\"Change text of multiselection.\"\"\"\n        self._multiselection_text = text\n\n    def confirm_value(self):\n        self._origin_value = copy.deepcopy(self._current_value)\n        self._has_value_changed = False\n\n    def _set_is_valid(self, valid):\n        if valid == self._is_valid:\n            return\n        self._is_valid = valid\n        state = \"\"\n        if not valid:\n            state = \"invalid\"\n        self._set_state_property(state)\n\n    def _on_text_change(self):\n        if self._ignore_value_change:\n            return\n\n        is_valid = bool(self._compiled_name_pattern.match(self.text()))\n        self._set_is_valid(is_valid)\n\n        self._current_value = [self.text()]\n        self._has_value_changed = self._current_value != self._origin_value\n\n        self.value_changed.emit()\n\n    def reset_to_origin(self):\n        \"\"\"Set origin value of selected instances.\"\"\"\n        self.set_value(self._origin_value)\n\n    def get_value(self):\n        \"\"\"Get current value.\n\n        Origin value returned if didn't change.\n        \"\"\"\n        return copy.deepcopy(self._current_value)\n\n    def set_value(self, variants=None):\n        \"\"\"Set value of currently selected instances.\"\"\"\n        if variants is None:\n            variants = []\n\n        self._ignore_value_change = True\n\n        self._has_value_changed = False\n\n        self._origin_value = list(variants)\n        self._current_value = list(variants)\n\n        self.setPlaceholderText(\"\")\n        if not variants:\n            self.setText(\"\")\n\n        elif len(variants) == 1:\n            self.setText(self._current_value[0])\n\n        else:\n            multiselection_text = self._multiselection_text\n            if multiselection_text is None:\n                multiselection_text = \"|\".join(variants)\n            self.setText(\"\")\n            self.setPlaceholderText(multiselection_text)\n\n        self._ignore_value_change = False\n\n\nclass MultipleItemWidget(QtWidgets.QWidget):\n    \"\"\"Widget for immutable text which can have more than one value.\n\n    Content may be bigger than widget's size and does not have scroll but has\n    flick widget on top (is possible to move around with clicked mouse).\n    \"\"\"\n\n    def __init__(self, parent):\n        super(MultipleItemWidget, self).__init__(parent)\n\n        model = QtGui.QStandardItemModel()\n\n        view = QtWidgets.QListView(self)\n        view.setObjectName(\"MultipleItemView\")\n        view.setLayoutMode(QtWidgets.QListView.Batched)\n        view.setViewMode(QtWidgets.QListView.IconMode)\n        view.setResizeMode(QtWidgets.QListView.Adjust)\n        view.setWrapping(False)\n        view.setSpacing(2)\n        view.setModel(model)\n        view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n        view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n\n        flick = FlickCharm(parent=view)\n        flick.activateOn(view)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(view)\n\n        model.rowsInserted.connect(self._on_insert)\n\n        self._view = view\n        self._model = model\n\n        self._value = []\n\n    def _on_insert(self):\n        self._update_size()\n\n    def _update_size(self):\n        model = self._view.model()\n        if model.rowCount() == 0:\n            return\n        height = self._view.sizeHintForRow(0)\n        self.setMaximumHeight(height + (2 * self._view.spacing()))\n\n    def showEvent(self, event):\n        super(MultipleItemWidget, self).showEvent(event)\n        tmp_item = None\n        if not self._value:\n            # Add temp item to be able calculate maximum height of widget\n            tmp_item = QtGui.QStandardItem(\"tmp\")\n            self._model.appendRow(tmp_item)\n            self._update_size()\n\n        if tmp_item is not None:\n            self._model.clear()\n\n    def resizeEvent(self, event):\n        super(MultipleItemWidget, self).resizeEvent(event)\n        self._update_size()\n\n    def set_value(self, value=None):\n        \"\"\"Set value/s of currently selected instance.\"\"\"\n        if value is None:\n            value = []\n        self._value = value\n\n        self._model.clear()\n        for item_text in value:\n            item = QtGui.QStandardItem(item_text)\n            item.setEditable(False)\n            item.setSelectable(False)\n            self._model.appendRow(item)\n\n\nclass GlobalAttrsWidget(QtWidgets.QWidget):\n    \"\"\"Global attributes mainly to define context and subset name of instances.\n\n    Subset name is or may be affected on context. Gives abiity to modify\n    context and subset name of instance. This change is not autopromoted but\n    must be submitted.\n\n    Warning: Until artist hit `Submit` changes must not be propagated to\n    instance data.\n\n    Global attributes contain these widgets:\n    Variant:      [  text input  ]\n    Asset:        [ asset dialog ]\n    Task:         [   combobox   ]\n    Family:       [   immutable  ]\n    Subset name:  [   immutable  ]\n                     [Submit] [Cancel]\n    \"\"\"\n    instance_context_changed = QtCore.Signal()\n\n    multiselection_text = \"< Multiselection >\"\n    unknown_value = \"N/A\"\n\n    def __init__(self, controller, parent):\n        super(GlobalAttrsWidget, self).__init__(parent)\n\n        self._controller = controller\n        self._current_instances = []\n\n        variant_input = VariantInputWidget(self)\n        asset_value_widget = AssetsField(controller, self)\n        task_value_widget = TasksCombobox(controller, self)\n        family_value_widget = MultipleItemWidget(self)\n        subset_value_widget = MultipleItemWidget(self)\n\n        variant_input.set_multiselection_text(self.multiselection_text)\n        asset_value_widget.set_multiselection_text(self.multiselection_text)\n        task_value_widget.set_multiselection_text(self.multiselection_text)\n\n        variant_input.set_value()\n        asset_value_widget.set_selected_items()\n        task_value_widget.set_selected_items()\n        family_value_widget.set_value()\n        subset_value_widget.set_value()\n\n        submit_btn = QtWidgets.QPushButton(\"Confirm\", self)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", self)\n        submit_btn.setEnabled(False)\n        cancel_btn.setEnabled(False)\n\n        btns_layout = QtWidgets.QHBoxLayout()\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n        btns_layout.addStretch(1)\n        btns_layout.setSpacing(5)\n        btns_layout.addWidget(submit_btn)\n        btns_layout.addWidget(cancel_btn)\n\n        main_layout = QtWidgets.QFormLayout(self)\n        main_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING)\n        main_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING)\n        main_layout.addRow(\"Variant\", variant_input)\n        main_layout.addRow(\n            \"Folder\" if AYON_SERVER_ENABLED else \"Asset\",\n            asset_value_widget)\n        main_layout.addRow(\"Task\", task_value_widget)\n        main_layout.addRow(\n            \"Product type\" if AYON_SERVER_ENABLED else \"Family\",\n            family_value_widget)\n        main_layout.addRow(\n            \"Product name\" if AYON_SERVER_ENABLED else \"Subset\",\n            subset_value_widget)\n        main_layout.addRow(btns_layout)\n\n        variant_input.value_changed.connect(self._on_variant_change)\n        asset_value_widget.value_changed.connect(self._on_asset_change)\n        task_value_widget.value_changed.connect(self._on_task_change)\n        submit_btn.clicked.connect(self._on_submit)\n        cancel_btn.clicked.connect(self._on_cancel)\n\n        self.variant_input = variant_input\n        self.asset_value_widget = asset_value_widget\n        self.task_value_widget = task_value_widget\n        self.family_value_widget = family_value_widget\n        self.subset_value_widget = subset_value_widget\n        self.submit_btn = submit_btn\n        self.cancel_btn = cancel_btn\n\n    def _on_submit(self):\n        \"\"\"Commit changes for selected instances.\"\"\"\n\n        variant_value = None\n        asset_name = None\n        task_name = None\n        if self.variant_input.has_value_changed():\n            variant_value = self.variant_input.get_value()[0]\n\n        if self.asset_value_widget.has_value_changed():\n            asset_name = self.asset_value_widget.get_selected_items()[0]\n\n        if self.task_value_widget.has_value_changed():\n            task_name = self.task_value_widget.get_selected_items()[0]\n\n        subset_names = set()\n        invalid_tasks = False\n        asset_names = []\n        for instance in self._current_instances:\n            new_variant_value = instance.get(\"variant\")\n            if AYON_SERVER_ENABLED:\n                new_asset_name = instance.get(\"folderPath\")\n            else:\n                new_asset_name = instance.get(\"asset\")\n            new_task_name = instance.get(\"task\")\n            if variant_value is not None:\n                new_variant_value = variant_value\n\n            if asset_name is not None:\n                new_asset_name = asset_name\n\n            if task_name is not None:\n                new_task_name = task_name\n\n            asset_names.append(new_asset_name)\n            try:\n                new_subset_name = self._controller.get_subset_name(\n                    instance.creator_identifier,\n                    new_variant_value,\n                    new_task_name,\n                    new_asset_name,\n                    instance.id,\n                )\n\n            except TaskNotSetError:\n                invalid_tasks = True\n                instance.set_task_invalid(True)\n                subset_names.add(instance[\"subset\"])\n                continue\n\n            subset_names.add(new_subset_name)\n            if variant_value is not None:\n                instance[\"variant\"] = variant_value\n\n            if asset_name is not None:\n                if AYON_SERVER_ENABLED:\n                    instance[\"folderPath\"] = asset_name\n                else:\n                    instance[\"asset\"] = asset_name\n\n                instance.set_asset_invalid(False)\n\n            if task_name is not None:\n                instance[\"task\"] = task_name or None\n                instance.set_task_invalid(False)\n\n            instance[\"subset\"] = new_subset_name\n\n        if invalid_tasks:\n            self.task_value_widget.set_invalid_empty_task()\n\n        self.subset_value_widget.set_value(subset_names)\n\n        self._set_btns_enabled(False)\n        self._set_btns_visible(invalid_tasks)\n\n        if variant_value is not None:\n            self.variant_input.confirm_value()\n\n        if asset_name is not None:\n            self.asset_value_widget.confirm_value()\n\n        if task_name is not None:\n            self.task_value_widget.confirm_value(asset_names)\n\n        self.instance_context_changed.emit()\n\n    def _on_cancel(self):\n        \"\"\"Cancel changes and set back to their irigin value.\"\"\"\n\n        self.variant_input.reset_to_origin()\n        self.asset_value_widget.reset_to_origin()\n        self.task_value_widget.reset_to_origin()\n        self._set_btns_enabled(False)\n\n    def _on_value_change(self):\n        any_invalid = (\n            not self.variant_input.is_valid()\n            or not self.asset_value_widget.is_valid()\n            or not self.task_value_widget.is_valid()\n        )\n        any_changed = (\n            self.variant_input.has_value_changed()\n            or self.asset_value_widget.has_value_changed()\n            or self.task_value_widget.has_value_changed()\n        )\n        self._set_btns_visible(any_changed or any_invalid)\n        self.cancel_btn.setEnabled(any_changed)\n        self.submit_btn.setEnabled(not any_invalid)\n\n    def _on_variant_change(self):\n        self._on_value_change()\n\n    def _on_asset_change(self):\n        asset_names = self.asset_value_widget.get_selected_items()\n        self.task_value_widget.set_asset_names(asset_names)\n        self._on_value_change()\n\n    def _on_task_change(self):\n        self._on_value_change()\n\n    def _set_btns_visible(self, visible):\n        self.cancel_btn.setVisible(visible)\n        self.submit_btn.setVisible(visible)\n\n    def _set_btns_enabled(self, enabled):\n        self.cancel_btn.setEnabled(enabled)\n        self.submit_btn.setEnabled(enabled)\n\n    def set_current_instances(self, instances):\n        \"\"\"Set currently selected instances.\n\n        Args:\n            instances(List[CreatedInstance]): List of selected instances.\n                Empty instances tells that nothing or context is selected.\n        \"\"\"\n        self._set_btns_visible(False)\n\n        self._current_instances = instances\n\n        asset_names = set()\n        variants = set()\n        families = set()\n        subset_names = set()\n\n        editable = True\n        if len(instances) == 0:\n            editable = False\n\n        asset_task_combinations = []\n        for instance in instances:\n            # NOTE I'm not sure how this can even happen?\n            if instance.creator_identifier is None:\n                editable = False\n\n            variants.add(instance.get(\"variant\") or self.unknown_value)\n            families.add(instance.get(\"family\") or self.unknown_value)\n            if AYON_SERVER_ENABLED:\n                asset_name = instance.get(\"folderPath\") or self.unknown_value\n            else:\n                asset_name = instance.get(\"asset\") or self.unknown_value\n            task_name = instance.get(\"task\") or \"\"\n            asset_names.add(asset_name)\n            asset_task_combinations.append((asset_name, task_name))\n            subset_names.add(instance.get(\"subset\") or self.unknown_value)\n\n        self.variant_input.set_value(variants)\n\n        # Set context of asset widget\n        self.asset_value_widget.set_selected_items(asset_names)\n        # Set context of task widget\n        self.task_value_widget.set_selected_items(asset_task_combinations)\n        self.family_value_widget.set_value(families)\n        self.subset_value_widget.set_value(subset_names)\n\n        self.variant_input.setEnabled(editable)\n        self.asset_value_widget.setEnabled(editable)\n        self.task_value_widget.setEnabled(editable)\n\n\nclass CreatorAttrsWidget(QtWidgets.QWidget):\n    \"\"\"Widget showing creator specific attributes for selected instances.\n\n    Attributes are defined on creator so are dynamic. Their look and type is\n    based on attribute definitions that are defined in\n    `~/openpype/pipeline/lib/attribute_definitions.py` and their widget\n    representation in `~/openpype/tools/attribute_defs/*`.\n\n    Widgets are disabled if context of instance is not valid.\n\n    Definitions are shown for all instance no matter if they are created with\n    different creators. If creator have same (similar) definitions their\n    widgets are merged into one (different label does not count).\n    \"\"\"\n\n    def __init__(self, controller, parent):\n        super(CreatorAttrsWidget, self).__init__(parent)\n\n        scroll_area = QtWidgets.QScrollArea(self)\n        scroll_area.setWidgetResizable(True)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n        main_layout.addWidget(scroll_area, 1)\n\n        self._main_layout = main_layout\n\n        self._controller = controller\n        self._scroll_area = scroll_area\n\n        self._attr_def_id_to_instances = {}\n        self._attr_def_id_to_attr_def = {}\n\n        # To store content of scroll area to prevent garbage collection\n        self._content_widget = None\n\n    def set_instances_valid(self, valid):\n        \"\"\"Change valid state of current instances.\"\"\"\n\n        if (\n            self._content_widget is not None\n            and self._content_widget.isEnabled() != valid\n        ):\n            self._content_widget.setEnabled(valid)\n\n    def set_current_instances(self, instances):\n        \"\"\"Set current instances for which are attribute definitions shown.\"\"\"\n\n        prev_content_widget = self._scroll_area.widget()\n        if prev_content_widget:\n            self._scroll_area.takeWidget()\n            prev_content_widget.hide()\n            prev_content_widget.deleteLater()\n\n        self._content_widget = None\n        self._attr_def_id_to_instances = {}\n        self._attr_def_id_to_attr_def = {}\n\n        result = self._controller.get_creator_attribute_definitions(\n            instances\n        )\n\n        content_widget = QtWidgets.QWidget(self._scroll_area)\n        content_layout = QtWidgets.QGridLayout(content_widget)\n        content_layout.setColumnStretch(0, 0)\n        content_layout.setColumnStretch(1, 1)\n        content_layout.setAlignment(QtCore.Qt.AlignTop)\n        content_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING)\n        content_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING)\n\n        row = 0\n        for attr_def, attr_instances, values in result:\n            widget = create_widget_for_attr_def(attr_def, content_widget)\n            if attr_def.is_value_def:\n                if len(values) == 1:\n                    value = values[0]\n                    if value is not None:\n                        widget.set_value(values[0])\n                else:\n                    widget.set_value(values, True)\n\n            widget.value_changed.connect(self._input_value_changed)\n            self._attr_def_id_to_instances[attr_def.id] = attr_instances\n            self._attr_def_id_to_attr_def[attr_def.id] = attr_def\n\n            if attr_def.hidden:\n                continue\n\n            expand_cols = 2\n            if attr_def.is_value_def and attr_def.is_label_horizontal:\n                expand_cols = 1\n\n            col_num = 2 - expand_cols\n\n            label = None\n            if attr_def.is_value_def:\n                label = attr_def.label or attr_def.key\n            if label:\n                label_widget = QtWidgets.QLabel(label, self)\n                tooltip = attr_def.tooltip\n                if tooltip:\n                    label_widget.setToolTip(tooltip)\n                if attr_def.is_label_horizontal:\n                    label_widget.setAlignment(\n                        QtCore.Qt.AlignRight\n                        | QtCore.Qt.AlignVCenter\n                    )\n                content_layout.addWidget(\n                    label_widget, row, 0, 1, expand_cols\n                )\n                if not attr_def.is_label_horizontal:\n                    row += 1\n\n            content_layout.addWidget(\n                widget, row, col_num, 1, expand_cols\n            )\n            row += 1\n\n        self._scroll_area.setWidget(content_widget)\n        self._content_widget = content_widget\n\n    def _input_value_changed(self, value, attr_id):\n        instances = self._attr_def_id_to_instances.get(attr_id)\n        attr_def = self._attr_def_id_to_attr_def.get(attr_id)\n        if not instances or not attr_def:\n            return\n\n        for instance in instances:\n            creator_attributes = instance[\"creator_attributes\"]\n            if attr_def.key in creator_attributes:\n                creator_attributes[attr_def.key] = value\n\n\nclass PublishPluginAttrsWidget(QtWidgets.QWidget):\n    \"\"\"Widget showing publsish plugin attributes for selected instances.\n\n    Attributes are defined on publish plugins. Publish plugin may define\n    attribute definitions but must inherit `OpenPypePyblishPluginMixin`\n    (~/openpype/pipeline/publish). At the moment requires to implement\n    `get_attribute_defs` and `convert_attribute_values` class methods.\n\n    Look and type of attributes is based on attribute definitions that are\n    defined in `~/openpype/pipeline/lib/attribute_definitions.py` and their\n    widget representation in `~/openpype/tools/attribute_defs/*`.\n\n    Widgets are disabled if context of instance is not valid.\n\n    Definitions are shown for all instance no matter if they have different\n    families. Similar definitions are merged into one (different label\n    does not count).\n    \"\"\"\n\n    def __init__(self, controller, parent):\n        super(PublishPluginAttrsWidget, self).__init__(parent)\n\n        scroll_area = QtWidgets.QScrollArea(self)\n        scroll_area.setWidgetResizable(True)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n        main_layout.addWidget(scroll_area, 1)\n\n        self._main_layout = main_layout\n\n        self._controller = controller\n        self._scroll_area = scroll_area\n\n        self._attr_def_id_to_instances = {}\n        self._attr_def_id_to_attr_def = {}\n        self._attr_def_id_to_plugin_name = {}\n\n        # Store content of scroll area to prevent garbage collection\n        self._content_widget = None\n\n    def set_instances_valid(self, valid):\n        \"\"\"Change valid state of current instances.\"\"\"\n        if (\n            self._content_widget is not None\n            and self._content_widget.isEnabled() != valid\n        ):\n            self._content_widget.setEnabled(valid)\n\n    def set_current_instances(self, instances, context_selected):\n        \"\"\"Set current instances for which are attribute definitions shown.\"\"\"\n\n        prev_content_widget = self._scroll_area.widget()\n        if prev_content_widget:\n            self._scroll_area.takeWidget()\n            prev_content_widget.hide()\n            prev_content_widget.deleteLater()\n\n        self._content_widget = None\n\n        self._attr_def_id_to_instances = {}\n        self._attr_def_id_to_attr_def = {}\n        self._attr_def_id_to_plugin_name = {}\n\n        result = self._controller.get_publish_attribute_definitions(\n            instances, context_selected\n        )\n\n        content_widget = QtWidgets.QWidget(self._scroll_area)\n        attr_def_widget = QtWidgets.QWidget(content_widget)\n        attr_def_layout = QtWidgets.QGridLayout(attr_def_widget)\n        attr_def_layout.setColumnStretch(0, 0)\n        attr_def_layout.setColumnStretch(1, 1)\n        attr_def_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING)\n        attr_def_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING)\n\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.addWidget(attr_def_widget, 0)\n        content_layout.addStretch(1)\n\n        row = 0\n        for plugin_name, attr_defs, all_plugin_values in result:\n            plugin_values = all_plugin_values[plugin_name]\n\n            for attr_def in attr_defs:\n                widget = create_widget_for_attr_def(\n                    attr_def, content_widget\n                )\n                hidden_widget = attr_def.hidden\n                # Hide unknown values of publish plugins\n                # - The keys in most of cases does not represent what would\n                #   label represent\n                if isinstance(attr_def, UnknownDef):\n                    widget.setVisible(False)\n                    hidden_widget = True\n\n                if not hidden_widget:\n                    expand_cols = 2\n                    if attr_def.is_value_def and attr_def.is_label_horizontal:\n                        expand_cols = 1\n\n                    col_num = 2 - expand_cols\n                    label = None\n                    if attr_def.is_value_def:\n                        label = attr_def.label or attr_def.key\n                    if label:\n                        label_widget = QtWidgets.QLabel(label, content_widget)\n                        tooltip = attr_def.tooltip\n                        if tooltip:\n                            label_widget.setToolTip(tooltip)\n                        if attr_def.is_label_horizontal:\n                            label_widget.setAlignment(\n                                QtCore.Qt.AlignRight\n                                | QtCore.Qt.AlignVCenter\n                            )\n                        attr_def_layout.addWidget(\n                            label_widget, row, 0, 1, expand_cols\n                        )\n                        if not attr_def.is_label_horizontal:\n                            row += 1\n                    attr_def_layout.addWidget(\n                        widget, row, col_num, 1, expand_cols\n                    )\n                    row += 1\n\n                if not attr_def.is_value_def:\n                    continue\n\n                widget.value_changed.connect(self._input_value_changed)\n\n                attr_values = plugin_values[attr_def.key]\n                multivalue = len(attr_values) > 1\n                values = []\n                instances = []\n                for instance, value in attr_values:\n                    values.append(value)\n                    instances.append(instance)\n\n                self._attr_def_id_to_attr_def[attr_def.id] = attr_def\n                self._attr_def_id_to_instances[attr_def.id] = instances\n                self._attr_def_id_to_plugin_name[attr_def.id] = plugin_name\n\n                if multivalue:\n                    widget.set_value(values, multivalue)\n                else:\n                    widget.set_value(values[0])\n\n        self._scroll_area.setWidget(content_widget)\n        self._content_widget = content_widget\n\n    def _input_value_changed(self, value, attr_id):\n        instances = self._attr_def_id_to_instances.get(attr_id)\n        attr_def = self._attr_def_id_to_attr_def.get(attr_id)\n        plugin_name = self._attr_def_id_to_plugin_name.get(attr_id)\n        if not instances or not attr_def or not plugin_name:\n            return\n\n        for instance in instances:\n            plugin_val = instance.publish_attributes[plugin_name]\n            plugin_val[attr_def.key] = value\n\n\nclass SubsetAttributesWidget(QtWidgets.QWidget):\n    \"\"\"Wrapper widget where attributes of instance/s are modified.\n    ┌─────────────────┬─────────────┐\n    │   Global        │             │\n    │   attributes    │  Thumbnail  │  TOP\n    │                 │             │\n    ├─────────────┬───┴─────────────┤\n    │  Creator    │   Publish       │\n    │  attributes │   plugin        │  BOTTOM\n    │             │   attributes    │\n    └───────────────────────────────┘\n    \"\"\"\n    instance_context_changed = QtCore.Signal()\n    convert_requested = QtCore.Signal()\n\n    def __init__(self, controller, parent):\n        super(SubsetAttributesWidget, self).__init__(parent)\n\n        # TOP PART\n        top_widget = QtWidgets.QWidget(self)\n\n        # Global attributes\n        global_attrs_widget = GlobalAttrsWidget(controller, top_widget)\n        thumbnail_widget = ThumbnailWidget(controller, top_widget)\n\n        top_layout = QtWidgets.QHBoxLayout(top_widget)\n        top_layout.setContentsMargins(0, 0, 0, 0)\n        top_layout.addWidget(global_attrs_widget, 7)\n        top_layout.addWidget(thumbnail_widget, 3)\n\n        # BOTTOM PART\n        bottom_widget = QtWidgets.QWidget(self)\n\n        # Wrap Creator attributes to widget to be able add convert button\n        creator_widget = QtWidgets.QWidget(bottom_widget)\n\n        # Convert button widget (with layout to handle stretch)\n        convert_widget = QtWidgets.QWidget(creator_widget)\n        convert_label = QtWidgets.QLabel(creator_widget)\n        # Set the label text with 'setText' to apply html\n        convert_label.setText(\n            (\n                \"Found old publishable subsets\"\n                \" incompatible with new publisher.\"\n                \"<br/><br/>Press the <b>update subsets</b> button\"\n                \" to automatically update them\"\n                \" to be able to publish again.\"\n            )\n        )\n        convert_label.setWordWrap(True)\n        convert_label.setAlignment(QtCore.Qt.AlignCenter)\n\n        convert_btn = QtWidgets.QPushButton(\n            \"Update subsets\", convert_widget\n        )\n        convert_separator = QtWidgets.QFrame(convert_widget)\n        convert_separator.setObjectName(\"Separator\")\n        convert_separator.setMinimumHeight(1)\n        convert_separator.setMaximumHeight(1)\n\n        convert_layout = QtWidgets.QGridLayout(convert_widget)\n        convert_layout.setContentsMargins(5, 0, 5, 0)\n        convert_layout.setVerticalSpacing(10)\n        convert_layout.addWidget(convert_label, 0, 0, 1, 3)\n        convert_layout.addWidget(convert_btn, 1, 1)\n        convert_layout.addWidget(convert_separator, 2, 0, 1, 3)\n        convert_layout.setColumnStretch(0, 1)\n        convert_layout.setColumnStretch(1, 0)\n        convert_layout.setColumnStretch(2, 1)\n\n        # Creator attributes widget\n        creator_attrs_widget = CreatorAttrsWidget(\n            controller, creator_widget\n        )\n        creator_layout = QtWidgets.QVBoxLayout(creator_widget)\n        creator_layout.setContentsMargins(0, 0, 0, 0)\n        creator_layout.addWidget(convert_widget, 0)\n        creator_layout.addWidget(creator_attrs_widget, 1)\n\n        publish_attrs_widget = PublishPluginAttrsWidget(\n            controller, bottom_widget\n        )\n\n        bottom_separator = QtWidgets.QWidget(bottom_widget)\n        bottom_separator.setObjectName(\"Separator\")\n        bottom_separator.setMinimumWidth(1)\n\n        bottom_layout = QtWidgets.QHBoxLayout(bottom_widget)\n        bottom_layout.setContentsMargins(0, 0, 0, 0)\n        bottom_layout.addWidget(creator_widget, 1)\n        bottom_layout.addWidget(bottom_separator, 0)\n        bottom_layout.addWidget(publish_attrs_widget, 1)\n\n        top_bottom = QtWidgets.QWidget(self)\n        top_bottom.setObjectName(\"Separator\")\n        top_bottom.setMinimumHeight(1)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(top_widget, 0)\n        layout.addWidget(top_bottom, 0)\n        layout.addWidget(bottom_widget, 1)\n\n        self._convertor_identifiers = None\n        self._current_instances = None\n        self._context_selected = False\n        self._all_instances_valid = True\n\n        global_attrs_widget.instance_context_changed.connect(\n            self._on_instance_context_changed\n        )\n        convert_btn.clicked.connect(self._on_convert_click)\n        thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)\n        thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear)\n\n        controller.event_system.add_callback(\n            \"instance.thumbnail.changed\", self._on_thumbnail_changed\n        )\n\n        self._controller = controller\n\n        self._convert_widget = convert_widget\n\n        self.global_attrs_widget = global_attrs_widget\n\n        self.creator_attrs_widget = creator_attrs_widget\n        self.publish_attrs_widget = publish_attrs_widget\n        self._thumbnail_widget = thumbnail_widget\n\n        self.top_bottom = top_bottom\n        self.bottom_separator = bottom_separator\n\n    def _on_instance_context_changed(self):\n        all_valid = True\n        for instance in self._current_instances:\n            if not instance.has_valid_context:\n                all_valid = False\n                break\n\n        self._all_instances_valid = all_valid\n        self.creator_attrs_widget.set_instances_valid(all_valid)\n        self.publish_attrs_widget.set_instances_valid(all_valid)\n\n        self.instance_context_changed.emit()\n\n    def _on_convert_click(self):\n        self.convert_requested.emit()\n\n    def set_current_instances(\n        self, instances, context_selected, convertor_identifiers\n    ):\n        \"\"\"Change currently selected items.\n\n        Args:\n            instances(List[CreatedInstance]): List of currently selected\n                instances.\n            context_selected(bool): Is context selected.\n            convertor_identifiers(List[str]): Identifiers of convert items.\n        \"\"\"\n\n        all_valid = True\n        for instance in instances:\n            if not instance.has_valid_context:\n                all_valid = False\n                break\n\n        s_convertor_identifiers = set(convertor_identifiers)\n        self._convertor_identifiers = s_convertor_identifiers\n        self._current_instances = instances\n        self._context_selected = context_selected\n        self._all_instances_valid = all_valid\n\n        self._convert_widget.setVisible(len(s_convertor_identifiers) > 0)\n        self.global_attrs_widget.set_current_instances(instances)\n        self.creator_attrs_widget.set_current_instances(instances)\n        self.publish_attrs_widget.set_current_instances(\n            instances, context_selected\n        )\n        self.creator_attrs_widget.set_instances_valid(all_valid)\n        self.publish_attrs_widget.set_instances_valid(all_valid)\n\n        self._update_thumbnails()\n\n    def _on_thumbnail_create(self, path):\n        instance_ids = [\n            instance.id\n            for instance in self._current_instances\n        ]\n        if self._context_selected:\n            instance_ids.append(None)\n\n        if not instance_ids:\n            return\n\n        mapping = {}\n        if len(instance_ids) == 1:\n            mapping[instance_ids[0]] = path\n\n        else:\n            for instance_id in instance_ids:\n                root = os.path.dirname(path)\n                ext = os.path.splitext(path)[-1]\n                dst_path = os.path.join(root, str(uuid.uuid4()) + ext)\n                shutil.copy(path, dst_path)\n                mapping[instance_id] = dst_path\n\n        self._controller.set_thumbnail_paths_for_instances(mapping)\n\n    def _on_thumbnail_clear(self):\n        instance_ids = [\n            instance.id\n            for instance in self._current_instances\n        ]\n        if self._context_selected:\n            instance_ids.append(None)\n\n        if not instance_ids:\n            return\n\n        mapping = {\n            instance_id: None\n            for instance_id in instance_ids\n        }\n        self._controller.set_thumbnail_paths_for_instances(mapping)\n\n    def _on_thumbnail_changed(self, event):\n        self._update_thumbnails()\n\n    def _update_thumbnails(self):\n        instance_ids = [\n            instance.id\n            for instance in self._current_instances\n        ]\n        if self._context_selected:\n            instance_ids.append(None)\n\n        if not instance_ids:\n            self._thumbnail_widget.setVisible(False)\n            self._thumbnail_widget.set_current_thumbnails(None)\n            return\n\n        mapping = self._controller.get_thumbnail_paths_for_instances(\n            instance_ids\n        )\n        thumbnail_paths = []\n        for instance_id in instance_ids:\n            path = mapping[instance_id]\n            if path:\n                thumbnail_paths.append(path)\n\n        self._thumbnail_widget.setVisible(True)\n        self._thumbnail_widget.set_current_thumbnails(thumbnail_paths)\n\n\nclass CreateNextPageOverlay(QtWidgets.QWidget):\n    clicked = QtCore.Signal()\n\n    def __init__(self, parent):\n        super(CreateNextPageOverlay, self).__init__(parent)\n        self.setCursor(QtCore.Qt.PointingHandCursor)\n        self._arrow_color = (\n            get_objected_colors(\"font\").get_qcolor()\n        )\n        self._bg_color = (\n            get_objected_colors(\"bg-buttons\").get_qcolor()\n        )\n\n        change_anim = QtCore.QVariantAnimation()\n        change_anim.setStartValue(0.0)\n        change_anim.setEndValue(1.0)\n        change_anim.setDuration(200)\n        change_anim.setEasingCurve(QtCore.QEasingCurve.OutCubic)\n\n        change_anim.valueChanged.connect(self._on_anim)\n\n        self._change_anim = change_anim\n        self._is_visible = None\n        self._anim_value = 0.0\n        self._increasing = False\n        self._under_mouse = None\n        self._handle_show_on_own = True\n        self._mouse_pressed = False\n        self.set_visible(True)\n\n    def set_increasing(self, increasing):\n        if self._increasing is increasing:\n            return\n        self._increasing = increasing\n        if increasing:\n            self._change_anim.setDirection(QtCore.QAbstractAnimation.Forward)\n        else:\n            self._change_anim.setDirection(QtCore.QAbstractAnimation.Backward)\n\n        if self._change_anim.state() != QtCore.QAbstractAnimation.Running:\n            self._change_anim.start()\n\n    def set_visible(self, visible):\n        if self._is_visible is visible:\n            return\n\n        self._is_visible = visible\n        if not visible:\n            self.set_increasing(False)\n            if not self._is_anim_finished():\n                return\n\n        self.setVisible(visible)\n        self._check_anim_timer()\n\n    def _is_anim_finished(self):\n        if self._increasing:\n            return self._anim_value == 1.0\n        return self._anim_value == 0.0\n\n    def _on_anim(self, value):\n        self._check_anim_timer()\n\n        self._anim_value = value\n\n        self.update()\n\n        if not self._is_anim_finished():\n            return\n\n        if not self._is_visible:\n            self.setVisible(False)\n\n    def set_under_mouse(self, under_mouse):\n        if self._under_mouse is under_mouse:\n            return\n\n        self._under_mouse = under_mouse\n        self.set_increasing(under_mouse)\n\n    def _is_under_mouse(self):\n        mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())\n        under_mouse = self.rect().contains(mouse_pos)\n        return under_mouse\n\n    def _check_anim_timer(self):\n        if not self.isVisible():\n            return\n\n        self.set_increasing(self._under_mouse)\n\n    def mousePressEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self._mouse_pressed = True\n        super(CreateNextPageOverlay, self).mousePressEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        if self._mouse_pressed:\n            self._mouse_pressed = False\n            if self.rect().contains(event.pos()):\n                self.clicked.emit()\n\n        super(CreateNextPageOverlay, self).mouseReleaseEvent(event)\n\n    def paintEvent(self, event):\n        painter = QtGui.QPainter()\n        painter.begin(self)\n        if self._anim_value == 0.0:\n            painter.end()\n            return\n\n        painter.setClipRect(event.rect())\n        painter.setRenderHints(\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n        )\n\n        painter.setPen(QtCore.Qt.NoPen)\n\n        rect = QtCore.QRect(self.rect())\n        rect_width = rect.width()\n        rect_height = rect.height()\n        radius = rect_width * 0.2\n\n        x_offset = 0\n        y_offset = 0\n        if self._anim_value != 1.0:\n            x_offset += rect_width - (rect_width * self._anim_value)\n\n        arrow_height = rect_height * 0.4\n        arrow_half_height = arrow_height * 0.5\n        arrow_x_start = x_offset + ((rect_width - arrow_half_height) * 0.5)\n        arrow_x_end = arrow_x_start + arrow_half_height\n        center_y = rect.center().y()\n\n        painter.setBrush(self._bg_color)\n        painter.drawRoundedRect(\n            x_offset, y_offset,\n            rect_width + radius, rect_height,\n            radius, radius\n        )\n\n        src_arrow_path = QtGui.QPainterPath()\n        src_arrow_path.moveTo(arrow_x_start, center_y - arrow_half_height)\n        src_arrow_path.lineTo(arrow_x_end, center_y)\n        src_arrow_path.lineTo(arrow_x_start, center_y + arrow_half_height)\n\n        arrow_stroker = QtGui.QPainterPathStroker()\n        arrow_stroker.setWidth(min(4, arrow_half_height * 0.2))\n        arrow_path = arrow_stroker.createStroke(src_arrow_path)\n\n        painter.fillPath(arrow_path, self._arrow_color)\n\n        painter.end()\n"
  },
  {
    "path": "openpype/tools/publisher/window.py",
    "content": "import os\nimport json\nimport time\nimport collections\nimport copy\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import (\n    resources,\n    style\n)\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.tools.utils import (\n    ErrorMessageBox,\n    PlaceholderLineEdit,\n    MessageOverlayObject,\n    PixmapLabel,\n)\nfrom openpype.tools.utils.lib import center_window\n\nfrom .constants import ResetKeySequence\nfrom .publish_report_viewer import PublishReportViewerWidget\nfrom .control import CardMessageTypes\nfrom .control_qt import QtPublisherController\nfrom .widgets import (\n    OverviewWidget,\n    ReportPageWidget,\n    PublishFrame,\n\n    PublisherTabsWidget,\n\n    SaveBtn,\n    ResetBtn,\n    StopBtn,\n    ValidateBtn,\n    PublishBtn,\n\n    HelpButton,\n    HelpDialog,\n\n    CreateNextPageOverlay,\n)\n\n\nclass PublisherWindow(QtWidgets.QDialog):\n    \"\"\"Main window of publisher.\"\"\"\n    default_width = 1300\n    default_height = 800\n    footer_border = 8\n    publish_footer_spacer = 2\n\n    def __init__(self, parent=None, controller=None, reset_on_show=None):\n        super(PublisherWindow, self).__init__(parent)\n\n        self.setObjectName(\"PublishWindow\")\n\n        self.setWindowTitle(\"{} publisher\".format(\n            \"AYON\" if AYON_SERVER_ENABLED else \"OpenPype\"\n        ))\n\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n\n        if reset_on_show is None:\n            reset_on_show = True\n\n        self.setWindowFlags(\n            QtCore.Qt.Window\n            | QtCore.Qt.WindowTitleHint\n            | QtCore.Qt.WindowMaximizeButtonHint\n            | QtCore.Qt.WindowMinimizeButtonHint\n            | QtCore.Qt.WindowCloseButtonHint\n        )\n\n        if controller is None:\n            controller = QtPublisherController()\n\n        help_dialog = HelpDialog(controller, self)\n\n        overlay_object = MessageOverlayObject(self)\n\n        # Header\n        header_widget = QtWidgets.QWidget(self)\n\n        icon_pixmap = QtGui.QPixmap(resources.get_openpype_icon_filepath())\n        icon_label = PixmapLabel(icon_pixmap, header_widget)\n        icon_label.setObjectName(\"PublishContextLabel\")\n\n        context_label = QtWidgets.QLabel(header_widget)\n        context_label.setObjectName(\"PublishContextLabel\")\n\n        header_extra_widget = QtWidgets.QWidget(header_widget)\n\n        help_btn = HelpButton(header_widget)\n\n        header_layout = QtWidgets.QHBoxLayout(header_widget)\n        header_layout.setContentsMargins(15, 15, 0, 15)\n        header_layout.setSpacing(15)\n        header_layout.addWidget(icon_label, 0)\n        header_layout.addWidget(context_label, 0)\n        header_layout.addStretch(1)\n        header_layout.addWidget(header_extra_widget, 0)\n        header_layout.addWidget(help_btn, 0)\n\n        # Tabs widget under header\n        tabs_widget = PublisherTabsWidget(self)\n        create_tab = tabs_widget.add_tab(\"Create\", \"create\")\n        tabs_widget.add_tab(\"Publish\", \"publish\")\n        tabs_widget.add_tab(\"Report\", \"report\")\n        tabs_widget.add_tab(\"Details\", \"details\")\n\n        # Widget where is stacked publish overlay and widgets that should be\n        #   covered by it\n        under_publish_stack = QtWidgets.QWidget(self)\n        # Added wrap widget where all widgets under overlay are added\n        # - this is because footer is also under overlay and the top part\n        #       is faked with floating frame\n        under_publish_widget = QtWidgets.QWidget(under_publish_stack)\n\n        # Footer\n        footer_widget = QtWidgets.QWidget(under_publish_widget)\n        footer_bottom_widget = QtWidgets.QWidget(footer_widget)\n\n        comment_input = PlaceholderLineEdit(footer_widget)\n        comment_input.setObjectName(\"PublishCommentInput\")\n        comment_input.setPlaceholderText(\n            \"Attach a comment to your publish\"\n        )\n\n        save_btn = SaveBtn(footer_widget)\n        reset_btn = ResetBtn(footer_widget)\n        stop_btn = StopBtn(footer_widget)\n        validate_btn = ValidateBtn(footer_widget)\n        publish_btn = PublishBtn(footer_widget)\n\n        footer_bottom_layout = QtWidgets.QHBoxLayout(footer_bottom_widget)\n        footer_bottom_layout.setContentsMargins(0, 0, 0, 0)\n        footer_bottom_layout.addStretch(1)\n        footer_bottom_layout.addWidget(save_btn, 0)\n        footer_bottom_layout.addWidget(reset_btn, 0)\n        footer_bottom_layout.addWidget(stop_btn, 0)\n        footer_bottom_layout.addWidget(validate_btn, 0)\n        footer_bottom_layout.addWidget(publish_btn, 0)\n\n        # Spacer helps keep distance of Publish Frame when comment input\n        #   is hidden - so when is shrunken it is not overlaying pages\n        footer_spacer = QtWidgets.QWidget(footer_widget)\n        footer_spacer.setMinimumHeight(self.publish_footer_spacer)\n        footer_spacer.setMaximumHeight(self.publish_footer_spacer)\n        footer_spacer.setVisible(False)\n\n        footer_layout = QtWidgets.QVBoxLayout(footer_widget)\n        footer_margins = footer_layout.contentsMargins()\n\n        footer_layout.setContentsMargins(\n            footer_margins.left() + self.footer_border,\n            footer_margins.top(),\n            footer_margins.right() + self.footer_border,\n            footer_margins.bottom() + self.footer_border\n        )\n\n        footer_layout.addWidget(comment_input, 0)\n        footer_layout.addWidget(footer_spacer, 0)\n        footer_layout.addWidget(footer_bottom_widget, 0)\n\n        # Content\n        # - wrap stacked widget under one more widget to be able to propagate\n        #   margins (QStackedLayout can't have margins)\n        content_widget = QtWidgets.QWidget(under_publish_widget)\n\n        content_stacked_widget = QtWidgets.QWidget(content_widget)\n\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        marings = content_layout.contentsMargins()\n        marings.setLeft(marings.left() * 2)\n        marings.setRight(marings.right() * 2)\n        marings.setTop(marings.top() * 2)\n        marings.setBottom(0)\n        content_layout.setContentsMargins(marings)\n        content_layout.addWidget(content_stacked_widget, 1)\n\n        # Overview - create and attributes part\n        overview_widget = OverviewWidget(\n            controller, content_stacked_widget\n        )\n\n        report_widget = ReportPageWidget(controller, content_stacked_widget)\n\n        # Details - Publish details\n        publish_details_widget = PublishReportViewerWidget(\n            content_stacked_widget\n        )\n\n        content_stacked_layout = QtWidgets.QStackedLayout(\n            content_stacked_widget\n        )\n        content_stacked_layout.setContentsMargins(0, 0, 0, 0)\n        content_stacked_layout.setStackingMode(\n            QtWidgets.QStackedLayout.StackAll\n        )\n        content_stacked_layout.addWidget(overview_widget)\n        content_stacked_layout.addWidget(report_widget)\n        content_stacked_layout.addWidget(publish_details_widget)\n        content_stacked_layout.setCurrentWidget(overview_widget)\n\n        under_publish_layout = QtWidgets.QVBoxLayout(under_publish_widget)\n        under_publish_layout.setContentsMargins(0, 0, 0, 0)\n        under_publish_layout.setSpacing(0)\n        under_publish_layout.addWidget(content_widget, 1)\n        under_publish_layout.addWidget(footer_widget, 0)\n\n        # Overlay which covers inputs during publishing\n        publish_overlay = QtWidgets.QFrame(under_publish_stack)\n        publish_overlay.setObjectName(\"OverlayFrame\")\n\n        under_publish_stack_layout = QtWidgets.QStackedLayout(\n            under_publish_stack\n        )\n        under_publish_stack_layout.setContentsMargins(0, 0, 0, 0)\n        under_publish_stack_layout.setStackingMode(\n            QtWidgets.QStackedLayout.StackAll\n        )\n        under_publish_stack_layout.addWidget(under_publish_widget)\n        under_publish_stack_layout.addWidget(publish_overlay)\n        under_publish_stack_layout.setCurrentWidget(under_publish_widget)\n\n        # Add main frame to this window\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n        main_layout.addWidget(header_widget, 0)\n        main_layout.addWidget(tabs_widget, 0)\n        main_layout.addWidget(under_publish_stack, 1)\n\n        # Floating publish frame\n        publish_frame = PublishFrame(controller, self.footer_border, self)\n\n        create_overlay_button = CreateNextPageOverlay(self)\n\n        show_timer = QtCore.QTimer()\n        show_timer.setInterval(1)\n        show_timer.timeout.connect(self._on_show_timer)\n\n        errors_dialog_message_timer = QtCore.QTimer()\n        errors_dialog_message_timer.setInterval(100)\n        errors_dialog_message_timer.timeout.connect(\n            self._on_errors_message_timeout\n        )\n\n        help_btn.clicked.connect(self._on_help_click)\n        tabs_widget.tab_changed.connect(self._on_tab_change)\n        overview_widget.active_changed.connect(\n            self._on_context_or_active_change\n        )\n        overview_widget.instance_context_changed.connect(\n            self._on_context_or_active_change\n        )\n        overview_widget.create_requested.connect(\n            self._on_create_request\n        )\n        overview_widget.convert_requested.connect(\n            self._on_convert_requested\n        )\n\n        save_btn.clicked.connect(self._on_save_clicked)\n        reset_btn.clicked.connect(self._on_reset_clicked)\n        stop_btn.clicked.connect(self._on_stop_clicked)\n        validate_btn.clicked.connect(self._on_validate_clicked)\n        publish_btn.clicked.connect(self._on_publish_clicked)\n\n        publish_frame.details_page_requested.connect(self._go_to_details_tab)\n        create_overlay_button.clicked.connect(\n            self._on_create_overlay_button_click\n        )\n\n        controller.event_system.add_callback(\n            \"instances.refresh.finished\", self._on_instances_refresh\n        )\n        controller.event_system.add_callback(\n            \"publish.reset.finished\", self._on_publish_reset\n        )\n        controller.event_system.add_callback(\n            \"controller.reset.finished\", self._on_controller_reset\n        )\n        controller.event_system.add_callback(\n            \"publish.process.started\", self._on_publish_start\n        )\n        controller.event_system.add_callback(\n            \"publish.has_validated.changed\", self._on_publish_validated_change\n        )\n        controller.event_system.add_callback(\n            \"publish.finished.changed\", self._on_publish_finished_change\n        )\n        controller.event_system.add_callback(\n            \"publish.process.stopped\", self._on_publish_stop\n        )\n        controller.event_system.add_callback(\n            \"show.card.message\", self._on_overlay_message\n        )\n        controller.event_system.add_callback(\n            \"instances.collection.failed\", self._on_creator_error\n        )\n        controller.event_system.add_callback(\n            \"instances.save.failed\", self._on_creator_error\n        )\n        controller.event_system.add_callback(\n            \"instances.remove.failed\", self._on_creator_error\n        )\n        controller.event_system.add_callback(\n            \"instances.create.failed\", self._on_creator_error\n        )\n        controller.event_system.add_callback(\n            \"convertors.convert.failed\", self._on_convertor_error\n        )\n        controller.event_system.add_callback(\n            \"convertors.find.failed\", self._on_convertor_error\n        )\n        controller.event_system.add_callback(\n            \"publish.action.failed\", self._on_action_error\n        )\n        controller.event_system.add_callback(\n            \"export_report.request\", self._export_report\n        )\n        controller.event_system.add_callback(\n            \"copy_report.request\", self._copy_report\n        )\n\n        # Store extra header widget for TrayPublisher\n        # - can be used to add additional widgets to header between context\n        #   label and help button\n        self._help_dialog = help_dialog\n        self._help_btn = help_btn\n\n        self._header_extra_widget = header_extra_widget\n\n        self._tabs_widget = tabs_widget\n        self._create_tab = create_tab\n\n        self._under_publish_stack_layout = under_publish_stack_layout\n\n        self._under_publish_widget = under_publish_widget\n        self._publish_overlay = publish_overlay\n        self._publish_frame = publish_frame\n\n        self._content_widget = content_widget\n        self._content_stacked_layout = content_stacked_layout\n\n        self._overview_widget = overview_widget\n        self._report_widget = report_widget\n        self._publish_details_widget = publish_details_widget\n\n        self._context_label = context_label\n\n        self._comment_input = comment_input\n        self._footer_spacer = footer_spacer\n\n        self._save_btn = save_btn\n        self._reset_btn = reset_btn\n        self._stop_btn = stop_btn\n        self._validate_btn = validate_btn\n        self._publish_btn = publish_btn\n\n        self._overlay_object = overlay_object\n\n        self._controller = controller\n\n        self._first_show = True\n        self._first_reset = True\n        # This is a little bit confusing but 'reset_on_first_show' is too long\n        #   for init\n        self._reset_on_first_show = reset_on_show\n        self._reset_on_show = True\n        self._publish_frame_visible = None\n        self._tab_on_reset = None\n\n        self._error_messages_to_show = collections.deque()\n        self._errors_dialog_message_timer = errors_dialog_message_timer\n\n        self._set_publish_visibility(False)\n\n        self._create_overlay_button = create_overlay_button\n        self._app_event_listener_installed = False\n\n        self._show_timer = show_timer\n        self._show_counter = 0\n        self._window_is_visible = False\n\n    @property\n    def controller(self):\n        return self._controller\n\n    def show_and_publish(self, comment=None):\n        \"\"\"Show the window and start publishing.\n\n        The method will reset controller and start the publishing afterwards.\n\n        Todos:\n            Move validations from '_on_publish_clicked' and change of\n                'comment' value in controller to controller so it can be\n                simplified.\n\n        Args:\n            comment (Optional[str]): Comment to be set to publish.\n                If is set to 'None' a comment is not changed at all.\n        \"\"\"\n\n        self._reset_on_show = False\n        self._reset_on_first_show = False\n\n        if comment is not None:\n            self.set_comment(comment)\n        self.make_sure_is_visible()\n        # Reset controller\n        self._controller.reset()\n        # Fake publish click to trigger save validation and propagate\n        #   comment to controller\n        self._on_publish_clicked()\n\n    def set_comment(self, comment):\n        \"\"\"Change comment text.\n\n        Todos:\n            Be able to set the comment via controller.\n\n        Args:\n            comment (str): Comment text.\n        \"\"\"\n\n        self._comment_input.setText(comment)\n\n    def make_sure_is_visible(self):\n        if self._window_is_visible:\n            self.setWindowState(QtCore.Qt.WindowActive)\n\n        else:\n            self.show()\n\n    def showEvent(self, event):\n        self._window_is_visible = True\n        super(PublisherWindow, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self._on_first_show()\n\n        self._show_timer.start()\n\n    def resizeEvent(self, event):\n        super(PublisherWindow, self).resizeEvent(event)\n        self._update_publish_frame_rect()\n        self._update_create_overlay_size()\n\n    def closeEvent(self, event):\n        self._window_is_visible = False\n        self._uninstall_app_event_listener()\n        # TODO capture changes and ask user if wants to save changes on close\n        if not self._controller.host_context_has_changed:\n            self._save_changes(False)\n        self._comment_input.setText(\"\")  # clear comment\n        self._reset_on_show = True\n        self._controller.clear_thumbnail_temp_dir_path()\n        # Trigger custom event that should be captured only in UI\n        #   - backend (controller) must not be dependent on this event topic!!!\n        self._controller.event_system.emit(\"main.window.closed\", {}, \"window\")\n        super(PublisherWindow, self).closeEvent(event)\n\n    def leaveEvent(self, event):\n        super(PublisherWindow, self).leaveEvent(event)\n        self._update_create_overlay_visibility()\n\n    def eventFilter(self, obj, event):\n        if event.type() == QtCore.QEvent.MouseMove:\n            self._update_create_overlay_visibility(event.globalPos())\n        return super(PublisherWindow, self).eventFilter(obj, event)\n\n    def _install_app_event_listener(self):\n        if self._app_event_listener_installed:\n            return\n        self._app_event_listener_installed = True\n        app = QtWidgets.QApplication.instance()\n        app.installEventFilter(self)\n\n    def _uninstall_app_event_listener(self):\n        if not self._app_event_listener_installed:\n            return\n        self._app_event_listener_installed = False\n        app = QtWidgets.QApplication.instance()\n        app.removeEventFilter(self)\n\n    def keyPressEvent(self, event):\n        if event.key() in {\n            # Ignore escape button to close window\n            QtCore.Qt.Key_Escape,\n            # Ignore enter keyboard event which by default triggers\n            #   first available button in QDialog\n            QtCore.Qt.Key_Enter,\n            QtCore.Qt.Key_Return,\n        }:\n            event.accept()\n            return\n\n        save_match = event.matches(QtGui.QKeySequence.Save)\n        # PySide2 and PySide6 support\n        if not isinstance(save_match, bool):\n            save_match = save_match == QtGui.QKeySequence.ExactMatch\n\n        if save_match:\n            if not self._controller.publish_has_started:\n                self._save_changes(True)\n            event.accept()\n            return\n\n        # PySide6 Support\n        if hasattr(event, \"keyCombination\"):\n            reset_match_result = ResetKeySequence.matches(\n                QtGui.QKeySequence(event.keyCombination())\n            )\n        else:\n            reset_match_result = ResetKeySequence.matches(\n                QtGui.QKeySequence(event.modifiers() | event.key())\n            )\n\n        if reset_match_result == QtGui.QKeySequence.ExactMatch:\n            if not self.controller.publish_is_running:\n                self.reset()\n            event.accept()\n            return\n\n        super(PublisherWindow, self).keyPressEvent(event)\n\n    def _on_overlay_message(self, event):\n        self._overlay_object.add_message(\n            event[\"message\"],\n            event.get(\"message_type\")\n        )\n\n    def _on_first_show(self):\n        self.resize(self.default_width, self.default_height)\n        self.setStyleSheet(style.load_stylesheet())\n        center_window(self)\n        self._reset_on_show = self._reset_on_first_show\n\n    def _on_show_timer(self):\n        # Add 1 to counter until hits 2\n        if self._show_counter < 3:\n            self._show_counter += 1\n            return\n\n        # Stop the timer\n        self._show_timer.stop()\n        # Reset counter when done for next show event\n        self._show_counter = 0\n\n        self._update_create_overlay_size()\n        self._update_create_overlay_visibility()\n        if self._is_on_create_tab():\n            self._install_app_event_listener()\n\n        # Reset if requested\n        if self._reset_on_show:\n            self._reset_on_show = False\n            self.reset()\n\n    def _checks_before_save(self, explicit_save):\n        \"\"\"Save of changes may trigger some issues.\n\n        Check if context did change and ask user if he is really sure the\n        save should happen. A dialog can be shown during this method.\n\n        Args:\n            explicit_save (bool): Method was called when user explicitly asked\n                for save. Value affects shown message.\n\n        Returns:\n            bool: Save can happen.\n        \"\"\"\n\n        if not self._controller.host_context_has_changed:\n            return True\n\n        title = \"Host context changed\"\n        if explicit_save:\n            message = (\n                \"Context has changed since Publisher window was refreshed last\"\n                \" time.\\n\\nAre you sure you want to save changes?\"\n            )\n        else:\n            message = (\n                \"Your action requires save of changes but context has changed\"\n                \" since Publisher window was refreshed last time.\\n\\nAre you\"\n                \" sure you want to continue and save changes?\"\n            )\n\n        result = QtWidgets.QMessageBox.question(\n            self,\n            title,\n            message,\n            QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Cancel\n        )\n        return result == QtWidgets.QMessageBox.Save\n\n    def _save_changes(self, explicit_save):\n        \"\"\"Save changes of Creation part.\n\n        All possible triggers of save changes were moved to main window (here),\n        so it can handle possible issues with save at one place. Do checks,\n        so user don't accidentally save changes to different file or using\n        different context.\n        Moving responsibility to this place gives option to show the dialog and\n        wait for user's response without breaking action he wanted to do.\n\n        Args:\n            explicit_save (bool): Method was called when user explicitly asked\n                for save. Value affects shown message.\n\n        Returns:\n            bool: Save happened successfully.\n        \"\"\"\n\n        if not self._checks_before_save(explicit_save):\n            return False\n        return self._controller.save_changes()\n\n    def reset(self):\n        self._controller.reset()\n\n    def set_context_label(self, label):\n        self._context_label.setText(label)\n\n    def set_tab_on_reset(self, tab):\n        \"\"\"Define tab that will be selected on window show.\n\n        This is single use method, when publisher window is showed the value is\n            unset and not used on next show.\n\n        Args:\n            tab (Union[int, Literal[create, publish, details, report]]: Index\n                or name of tab which will be selected on show (after reset).\n        \"\"\"\n\n        self._tab_on_reset = tab\n\n    def _update_publish_details_widget(self, force=False):\n        if not force and not self._is_on_details_tab():\n            return\n\n        report_data = self.controller.get_publish_report()\n        self._publish_details_widget.set_report_data(report_data)\n\n    def _on_help_click(self):\n        if self._help_dialog.isVisible():\n            return\n\n        self._help_dialog.show()\n\n        window = self.window()\n        if hasattr(QtWidgets.QApplication, \"desktop\"):\n            desktop = QtWidgets.QApplication.desktop()\n            screen_idx = desktop.screenNumber(window)\n            screen_geo = desktop.screenGeometry(screen_idx)\n        else:\n            screen = window.screen()\n            screen_geo = screen.geometry()\n\n        window_geo = window.geometry()\n        dialog_x = window_geo.x() + window_geo.width()\n        dialog_right = (dialog_x + self._help_dialog.width()) - 1\n        diff = dialog_right - screen_geo.right()\n        if diff > 0:\n            dialog_x -= diff\n\n        self._help_dialog.setGeometry(\n            dialog_x, window_geo.y(),\n            self._help_dialog.width(), self._help_dialog.height()\n        )\n\n    def _on_create_overlay_button_click(self):\n        self._create_overlay_button.set_under_mouse(False)\n        self._go_to_publish_tab()\n\n    def _on_tab_change(self, old_tab, new_tab):\n        if old_tab == \"details\":\n            self._publish_details_widget.close_details_popup()\n\n        if new_tab == \"details\":\n            self._content_stacked_layout.setCurrentWidget(\n                self._publish_details_widget\n            )\n            self._update_publish_details_widget()\n\n        elif new_tab == \"report\":\n            self._content_stacked_layout.setCurrentWidget(\n                self._report_widget\n            )\n\n        old_on_overview = old_tab in (\"create\", \"publish\")\n        if new_tab in (\"create\", \"publish\"):\n            self._content_stacked_layout.setCurrentWidget(\n                self._overview_widget\n            )\n            # Overview state is animated only when switching between\n            #   'create' and 'publish' tab\n            self._overview_widget.set_state(new_tab, old_on_overview)\n\n        elif old_on_overview:\n            # Make sure animation finished if previous tab was 'create'\n            #   or 'publish'. That is just for safety to avoid stuck animation\n            #   when user clicks too fast.\n            self._overview_widget.make_sure_animation_is_finished()\n\n        is_create = new_tab == \"create\"\n        if is_create:\n            self._install_app_event_listener()\n        else:\n            self._uninstall_app_event_listener()\n        self._create_overlay_button.set_visible(is_create)\n\n    def _on_context_or_active_change(self):\n        self._validate_create_instances()\n\n    def _on_create_request(self):\n        self._go_to_create_tab()\n\n    def _on_convert_requested(self):\n        if not self._save_changes(False):\n            return\n        convertor_identifiers = (\n            self._overview_widget.get_selected_legacy_convertors()\n        )\n        self._controller.trigger_convertor_items(convertor_identifiers)\n\n    def _set_current_tab(self, identifier):\n        self._tabs_widget.set_current_tab(identifier)\n\n    def set_current_tab(self, tab):\n        if tab == \"create\":\n            self._go_to_create_tab()\n        elif tab == \"publish\":\n            self._go_to_publish_tab()\n        elif tab == \"report\":\n            self._go_to_report_tab()\n        elif tab == \"details\":\n            self._go_to_details_tab()\n\n        if not self._window_is_visible:\n            self.set_tab_on_reset(tab)\n\n    def _is_current_tab(self, identifier):\n        return self._tabs_widget.is_current_tab(identifier)\n\n    def _go_to_create_tab(self):\n        if self._create_tab.isEnabled():\n            self._set_current_tab(\"create\")\n            return\n\n        self._overlay_object.add_message(\n            \"Can't switch to Create tab because publishing is paused.\",\n            message_type=\"info\"\n        )\n\n    def _go_to_publish_tab(self):\n        self._set_current_tab(\"publish\")\n\n    def _go_to_report_tab(self):\n        self._set_current_tab(\"report\")\n\n    def _go_to_details_tab(self):\n        self._set_current_tab(\"details\")\n\n    def _is_on_create_tab(self):\n        return self._is_current_tab(\"create\")\n\n    def _is_on_publish_tab(self):\n        return self._is_current_tab(\"publish\")\n\n    def _is_on_report_tab(self):\n        return self._is_current_tab(\"report\")\n\n    def _is_on_details_tab(self):\n        return self._is_current_tab(\"details\")\n\n    def _set_publish_overlay_visibility(self, visible):\n        if visible:\n            widget = self._publish_overlay\n        else:\n            widget = self._under_publish_widget\n        self._under_publish_stack_layout.setCurrentWidget(widget)\n\n    def _set_publish_visibility(self, visible):\n        if visible is self._publish_frame_visible:\n            return\n        self._publish_frame_visible = visible\n        self._publish_frame.setVisible(visible)\n        self._update_publish_frame_rect()\n\n    def _on_save_clicked(self):\n        self._save_changes(True)\n\n    def _on_reset_clicked(self):\n        self.reset()\n\n    def _on_stop_clicked(self):\n        self._controller.stop_publish()\n\n    def _set_publish_comment(self):\n        self._controller.set_comment(self._comment_input.text())\n\n    def _on_validate_clicked(self):\n        if self._save_changes(False):\n            self._set_publish_comment()\n            self._controller.validate()\n\n    def _on_publish_clicked(self):\n        if self._save_changes(False):\n            self._set_publish_comment()\n            self._controller.publish()\n\n    def _set_footer_enabled(self, enabled):\n        self._save_btn.setEnabled(True)\n        self._reset_btn.setEnabled(True)\n        if enabled:\n            self._stop_btn.setEnabled(False)\n            self._validate_btn.setEnabled(True)\n            self._publish_btn.setEnabled(True)\n        else:\n            self._stop_btn.setEnabled(enabled)\n            self._validate_btn.setEnabled(enabled)\n            self._publish_btn.setEnabled(enabled)\n\n    def _on_publish_reset(self):\n        self._create_tab.setEnabled(True)\n        self._set_comment_input_visiblity(True)\n        self._set_publish_overlay_visibility(False)\n        self._set_publish_visibility(False)\n        self._set_footer_enabled(False)\n        self._update_publish_details_widget()\n\n    def _on_controller_reset(self):\n        self._first_reset, first_reset = False, self._first_reset\n        if self._tab_on_reset is not None:\n            self._tab_on_reset, new_tab = None, self._tab_on_reset\n            self._set_current_tab(new_tab)\n            return\n\n        # On first reset change tab based on available items\n        # - if there is at least one instance the tab is changed to 'publish'\n        #   otherwise 'create' is used\n        # - this happens only on first show\n        if first_reset:\n            self._go_to_create_tab()\n\n        elif self._is_on_report_tab():\n            # Go to 'Publish' tab if is on 'Details' tab\n            #   - this can happen when publishing started and was reset\n            #       at that moment it doesn't make sense to stay at publish\n            #       specific tabs.\n            self._go_to_publish_tab()\n\n    def _on_publish_start(self):\n        self._create_tab.setEnabled(False)\n\n        self._reset_btn.setEnabled(False)\n        self._stop_btn.setEnabled(True)\n        self._validate_btn.setEnabled(False)\n        self._publish_btn.setEnabled(False)\n\n        self._set_comment_input_visiblity(False)\n        self._set_publish_visibility(True)\n        self._set_publish_overlay_visibility(True)\n\n        self._publish_details_widget.close_details_popup()\n\n        if self._is_on_create_tab():\n            self._go_to_publish_tab()\n\n    def _on_publish_validated_change(self, event):\n        if event[\"value\"]:\n            self._validate_btn.setEnabled(False)\n\n    def _on_publish_finished_change(self, event):\n        if event[\"value\"]:\n            # Successful publish, remove comment from UI\n            self._comment_input.setText(\"\")\n\n    def _on_publish_stop(self):\n        self._set_publish_overlay_visibility(False)\n        self._reset_btn.setEnabled(True)\n        self._stop_btn.setEnabled(False)\n        publish_has_crashed = self._controller.publish_has_crashed\n        validate_enabled = not publish_has_crashed\n        publish_enabled = not publish_has_crashed\n        if self._is_on_publish_tab():\n            self._go_to_report_tab()\n\n        if validate_enabled:\n            validate_enabled = not self._controller.publish_has_validated\n        if publish_enabled:\n            if (\n                self._controller.publish_has_validated\n                and self._controller.publish_has_validation_errors\n            ):\n                publish_enabled = False\n\n            else:\n                publish_enabled = not self._controller.publish_has_finished\n\n        self._validate_btn.setEnabled(validate_enabled)\n        self._publish_btn.setEnabled(publish_enabled)\n\n        if not publish_enabled:\n            self._publish_frame.set_shrunk_state(True)\n\n        self._update_publish_details_widget()\n\n    def _validate_create_instances(self):\n        if not self._controller.host_is_valid:\n            self._set_footer_enabled(True)\n            return\n\n        all_valid = None\n        for instance in self._controller.instances.values():\n            if not instance[\"active\"]:\n                continue\n\n            if not instance.has_valid_context:\n                all_valid = False\n                break\n\n            if all_valid is None:\n                all_valid = True\n\n        self._set_footer_enabled(bool(all_valid))\n\n    def _on_instances_refresh(self):\n        self._validate_create_instances()\n\n        context_title = self.controller.get_context_title()\n        self.set_context_label(context_title)\n        self._update_publish_details_widget()\n\n    def _set_comment_input_visiblity(self, visible):\n        self._comment_input.setVisible(visible)\n        self._footer_spacer.setVisible(not visible)\n\n    def _update_publish_frame_rect(self):\n        if not self._publish_frame_visible:\n            return\n\n        window_size = self.size()\n        size_hint = self._publish_frame.minimumSizeHint()\n\n        width = window_size.width()\n        height = size_hint.height()\n\n        self._publish_frame.resize(width, height)\n\n        self._publish_frame.move(\n            0, window_size.height() - height\n        )\n\n    def add_error_message_dialog(self, title, failed_info, message_start=None):\n        self._error_messages_to_show.append(\n            (title, failed_info, message_start)\n        )\n        self._errors_dialog_message_timer.start()\n\n    def _on_errors_message_timeout(self):\n        if not self._error_messages_to_show:\n            self._errors_dialog_message_timer.stop()\n            return\n\n        item = self._error_messages_to_show.popleft()\n        title, failed_info, message_start = item\n        dialog = ErrorsMessageBox(\n            title, failed_info, message_start, self\n        )\n        dialog.exec_()\n        dialog.deleteLater()\n\n    def _on_creator_error(self, event):\n        new_failed_info = []\n        for item in event[\"failed_info\"]:\n            new_item = copy.deepcopy(item)\n            new_item[\"label\"] = new_item.pop(\"creator_label\")\n            new_item[\"identifier\"] = new_item.pop(\"creator_identifier\")\n            new_failed_info.append(new_item)\n        self.add_error_message_dialog(event[\"title\"], new_failed_info, \"Creator:\")\n\n    def _on_convertor_error(self, event):\n        new_failed_info = []\n        for item in event[\"failed_info\"]:\n            new_item = copy.deepcopy(item)\n            new_item[\"identifier\"] = new_item.pop(\"convertor_identifier\")\n            new_failed_info.append(new_item)\n        self.add_error_message_dialog(\n            event[\"title\"], new_failed_info, \"Convertor:\"\n        )\n\n    def _on_action_error(self, event):\n        self.add_error_message_dialog(\n            event[\"title\"],\n            [{\n                \"message\": event[\"message\"],\n                \"traceback\": event[\"traceback\"],\n                \"label\": event[\"label\"],\n                \"identifier\": event[\"identifier\"]\n            }],\n            \"Action:\"\n        )\n\n    def _update_create_overlay_size(self):\n        metrics = self._create_overlay_button.fontMetrics()\n        height = int(metrics.height())\n        width = int(height * 0.7)\n        end_pos_x = self.width()\n        start_pos_x = end_pos_x - width\n\n        center = self._content_widget.parent().mapTo(\n            self,\n            self._content_widget.rect().center()\n        )\n        pos_y = center.y() - (height * 0.5)\n\n        self._create_overlay_button.setGeometry(\n            start_pos_x, pos_y,\n            width, height\n        )\n\n    def _update_create_overlay_visibility(self, global_pos=None):\n        if global_pos is None:\n            global_pos = QtGui.QCursor.pos()\n\n        under_mouse = False\n        my_pos = self.mapFromGlobal(global_pos)\n        if self.rect().contains(my_pos):\n            widget_geo = self._overview_widget.get_subset_views_geo()\n            widget_x = widget_geo.left() + (widget_geo.width() * 0.5)\n            under_mouse = widget_x < global_pos.x()\n        self._create_overlay_button.set_under_mouse(under_mouse)\n\n    def _copy_report(self):\n        logs = self._controller.get_publish_report()\n        logs_string = json.dumps(logs, indent=4)\n\n        mime_data = QtCore.QMimeData()\n        mime_data.setText(logs_string)\n        QtWidgets.QApplication.instance().clipboard().setMimeData(\n            mime_data\n        )\n        self._controller.emit_card_message(\n            \"Report added to clipboard\",\n            CardMessageTypes.info)\n\n    def _export_report(self):\n        default_filename = \"publish-report-{}\".format(\n            time.strftime(\"%y%m%d-%H-%M\")\n        )\n        default_filepath = os.path.join(\n            os.path.expanduser(\"~\"),\n            default_filename\n        )\n        new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName(\n            self, \"Save report\", default_filepath, \".json\"\n        )\n        if not ext or not new_filepath:\n            return\n\n        logs = self._controller.get_publish_report()\n        full_path = new_filepath + ext\n        dir_path = os.path.dirname(full_path)\n        if not os.path.exists(dir_path):\n            os.makedirs(dir_path)\n\n        with open(full_path, \"w\") as file_stream:\n            json.dump(logs, file_stream)\n\n        self._controller.emit_card_message(\n            \"Report saved\",\n            CardMessageTypes.info)\n\n\nclass ErrorsMessageBox(ErrorMessageBox):\n    def __init__(self, error_title, failed_info, message_start, parent):\n        self._failed_info = failed_info\n        self._message_start = message_start\n        self._info_with_id = [\n            # Id must be string when used in tab widget\n            {\"id\": str(idx), \"info\": info}\n            for idx, info in enumerate(failed_info)\n        ]\n        self._widgets_by_id = {}\n        self._tabs_widget = None\n        self._stack_layout = None\n\n        super(ErrorsMessageBox, self).__init__(error_title, parent)\n\n        layout = self.layout()\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(0)\n\n        footer_layout = self._footer_widget.layout()\n        footer_layout.setContentsMargins(5, 5, 5, 5)\n\n    def _create_top_widget(self, parent_widget):\n        return None\n\n    def _get_report_data(self):\n        output = []\n        for info in self._failed_info:\n            item_label = info.get(\"label\")\n            item_identifier = info[\"identifier\"]\n            if item_label:\n                report_message = \"{} ({})\".format(\n                    item_label, item_identifier)\n            else:\n                report_message = \"{}\".format(item_identifier)\n\n            if self._message_start:\n                report_message = \"{} {}\".format(\n                    self._message_start, report_message\n                )\n\n            report_message += \"\\n\\nError: {}\".format(info[\"message\"])\n            formatted_traceback = info.get(\"traceback\")\n            if formatted_traceback:\n                report_message += \"\\n\\n{}\".format(formatted_traceback)\n            output.append(report_message)\n        return output\n\n    def _create_content(self, content_layout):\n        tabs_widget = PublisherTabsWidget(self)\n\n        stack_widget = QtWidgets.QFrame(self._content_widget)\n        stack_layout = QtWidgets.QStackedLayout(stack_widget)\n\n        first = True\n        for item in self._info_with_id:\n            item_id = item[\"id\"]\n            info = item[\"info\"]\n            message = info[\"message\"]\n            formatted_traceback = info.get(\"traceback\")\n            item_label = info.get(\"label\")\n            if not item_label:\n                item_label = info[\"identifier\"]\n\n            msg_widget = QtWidgets.QWidget(stack_widget)\n            msg_layout = QtWidgets.QVBoxLayout(msg_widget)\n\n            exc_msg_template = \"<span style='font-weight:bold'>{}</span>\"\n            message_label_widget = QtWidgets.QLabel(msg_widget)\n            message_label_widget.setText(\n                exc_msg_template.format(self.convert_text_for_html(message))\n            )\n            msg_layout.addWidget(message_label_widget, 0)\n\n            if formatted_traceback:\n                line_widget = self._create_line(msg_widget)\n                tb_widget = self._create_traceback_widget(formatted_traceback)\n                msg_layout.addWidget(line_widget, 0)\n                msg_layout.addWidget(tb_widget, 0)\n\n            msg_layout.addStretch(1)\n\n            tabs_widget.add_tab(item_label, item_id)\n            stack_layout.addWidget(msg_widget)\n            if first:\n                first = False\n                stack_layout.setCurrentWidget(msg_widget)\n\n            self._widgets_by_id[item_id] = msg_widget\n\n        content_layout.addWidget(tabs_widget, 0)\n        content_layout.addWidget(stack_widget, 1)\n\n        tabs_widget.tab_changed.connect(self._on_tab_change)\n\n        self._tabs_widget = tabs_widget\n        self._stack_layout = stack_layout\n\n    def _on_tab_change(self, old_identifier, identifier):\n        widget = self._widgets_by_id[identifier]\n        self._stack_layout.setCurrentWidget(widget)\n"
  },
  {
    "path": "openpype/tools/push_to_project/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/tools/push_to_project/app.py",
    "content": "import click\n\nfrom openpype.tools.utils import get_openpype_qt_app\nfrom openpype.tools.push_to_project.window import PushToContextSelectWindow\n\n\ndef show(project, version, library_filter, context_only):\n    window = PushToContextSelectWindow(\n        library_filter=library_filter, context_only=context_only\n    )\n    window.show()\n    window.controller.set_source(project, version)\n\n    if __name__ == \"__main__\":\n        app = get_openpype_qt_app()\n        app.exec_()\n    else:\n        window.exec_()\n\n    return window.context\n\n\n@click.command()\n@click.option(\"--project\", help=\"Source project name\")\n@click.option(\"--version\", help=\"Source version id\")\ndef main(project, version):\n    \"\"\"Run PushToProject tool to integrate version in different project.\n\n    Args:\n        project (str): Source project name.\n        version (str): Version id.\n        version (bool): Filter to library projects only.\n    \"\"\"\n    show(project, version, True, False)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "openpype/tools/push_to_project/control_context.py",
    "content": "import re\nimport collections\nimport threading\n\nfrom openpype.client import (\n    get_projects,\n    get_assets,\n    get_asset_by_id,\n    get_subset_by_id,\n    get_version_by_id,\n    get_representations,\n)\nfrom openpype.settings import get_project_settings\nfrom openpype.lib import prepare_template_data\nfrom openpype.lib.events import EventSystem\nfrom openpype.pipeline.create import (\n    SUBSET_NAME_ALLOWED_SYMBOLS,\n    get_subset_name_template,\n)\n\nfrom .control_integrate import (\n    ProjectPushItem,\n    ProjectPushItemProcess,\n    ProjectPushItemStatus,\n)\n\n\nclass AssetItem:\n    def __init__(\n        self,\n        entity_id,\n        name,\n        icon_name,\n        icon_color,\n        parent_id,\n        has_children\n    ):\n        self.id = entity_id\n        self.name = name\n        self.icon_name = icon_name\n        self.icon_color = icon_color\n        self.parent_id = parent_id\n        self.has_children = has_children\n\n    @classmethod\n    def from_doc(cls, asset_doc, has_children=True):\n        parent_id = asset_doc[\"data\"].get(\"visualParent\")\n        if parent_id is not None:\n            parent_id = str(parent_id)\n        return cls(\n            str(asset_doc[\"_id\"]),\n            asset_doc[\"name\"],\n            asset_doc[\"data\"].get(\"icon\"),\n            asset_doc[\"data\"].get(\"color\"),\n            parent_id,\n            has_children\n        )\n\n\nclass TaskItem:\n    def __init__(self, asset_id, name, task_type, short_name):\n        self.asset_id = asset_id\n        self.name = name\n        self.task_type = task_type\n        self.short_name = short_name\n\n    @classmethod\n    def from_asset_doc(cls, asset_doc, project_doc):\n        asset_tasks = asset_doc[\"data\"].get(\"tasks\") or {}\n        project_task_types = project_doc[\"config\"][\"tasks\"]\n        output = []\n        for task_name, task_info in asset_tasks.items():\n            task_type = task_info.get(\"type\")\n            task_type_info = project_task_types.get(task_type) or {}\n            output.append(cls(\n                asset_doc[\"_id\"],\n                task_name,\n                task_type,\n                task_type_info.get(\"short_name\")\n            ))\n        return output\n\n\nclass EntitiesModel:\n    def __init__(self, event_system, library_filter=True):\n        self._event_system = event_system\n        self._project_names = None\n        self._project_docs_by_name = {}\n        self._assets_by_project = {}\n        self._tasks_by_asset_id = collections.defaultdict(dict)\n        self.library_filter = library_filter\n\n    def has_cached_projects(self):\n        return self._project_names is None\n\n    def has_cached_assets(self, project_name):\n        if not project_name:\n            return True\n        return project_name in self._assets_by_project\n\n    def has_cached_tasks(self, project_name):\n        return self.has_cached_assets(project_name)\n\n    def get_projects(self):\n        if self._project_names is None:\n            self.refresh_projects()\n        return list(self._project_names)\n\n    def get_assets(self, project_name):\n        if project_name not in self._assets_by_project:\n            self.refresh_assets(project_name)\n        return dict(self._assets_by_project[project_name])\n\n    def get_asset_by_id(self, project_name, asset_id):\n        return self._assets_by_project[project_name].get(asset_id)\n\n    def get_tasks(self, project_name, asset_id):\n        if not project_name or not asset_id:\n            return []\n\n        if project_name not in self._tasks_by_asset_id:\n            self.refresh_assets(project_name)\n\n        all_task_items = self._tasks_by_asset_id[project_name]\n        asset_task_items = all_task_items.get(asset_id)\n        if not asset_task_items:\n            return []\n        return list(asset_task_items)\n\n    def refresh_projects(self, force=False):\n        self._event_system.emit(\n            \"projects.refresh.started\", {}, \"entities.model\"\n        )\n        if force or self._project_names is None:\n            project_names = []\n            project_docs_by_name = {}\n            for project_doc in get_projects():\n                library_project = project_doc[\"data\"].get(\"library_project\")\n                if not library_project and self.library_filter:\n                    continue\n                project_name = project_doc[\"name\"]\n                project_names.append(project_name)\n                project_docs_by_name[project_name] = project_doc\n            self._project_names = project_names\n            self._project_docs_by_name = project_docs_by_name\n        self._event_system.emit(\n            \"projects.refresh.finished\", {}, \"entities.model\"\n        )\n\n    def _refresh_assets(self, project_name):\n        asset_items_by_id = {}\n        task_items_by_asset_id = {}\n        self._assets_by_project[project_name] = asset_items_by_id\n        self._tasks_by_asset_id[project_name] = task_items_by_asset_id\n        if not project_name:\n            return\n\n        project_doc = self._project_docs_by_name[project_name]\n        asset_docs_by_parent_id = collections.defaultdict(list)\n        for asset_doc in get_assets(project_name):\n            parent_id = asset_doc[\"data\"].get(\"visualParent\")\n            asset_docs_by_parent_id[parent_id].append(asset_doc)\n\n        hierarchy_queue = collections.deque()\n        for asset_doc in asset_docs_by_parent_id[None]:\n            hierarchy_queue.append(asset_doc)\n\n        while hierarchy_queue:\n            asset_doc = hierarchy_queue.popleft()\n            children = asset_docs_by_parent_id[asset_doc[\"_id\"]]\n            asset_item = AssetItem.from_doc(asset_doc, len(children) > 0)\n            asset_items_by_id[asset_item.id] = asset_item\n            task_items_by_asset_id[asset_item.id] = (\n                TaskItem.from_asset_doc(asset_doc, project_doc)\n            )\n            for child in children:\n                hierarchy_queue.append(child)\n\n    def refresh_assets(self, project_name, force=False):\n        self._event_system.emit(\n            \"assets.refresh.started\",\n            {\"project_name\": project_name},\n            \"entities.model\"\n        )\n\n        if force or project_name not in self._assets_by_project:\n            self._refresh_assets(project_name)\n\n        self._event_system.emit(\n            \"assets.refresh.finished\",\n            {\"project_name\": project_name},\n            \"entities.model\"\n        )\n\n\nclass SelectionModel:\n    def __init__(self, event_system):\n        self._event_system = event_system\n\n        self.project_name = None\n        self.asset_id = None\n        self.task_name = None\n\n    def select_project(self, project_name):\n        if self.project_name == project_name:\n            return\n\n        self.project_name = project_name\n        self._event_system.emit(\n            \"project.changed\",\n            {\"project_name\": project_name},\n            \"selection.model\"\n        )\n\n    def select_asset(self, asset_id):\n        if self.asset_id == asset_id:\n            return\n        self.asset_id = asset_id\n        self._event_system.emit(\n            \"asset.changed\",\n            {\n                \"project_name\": self.project_name,\n                \"asset_id\": asset_id\n            },\n            \"selection.model\"\n        )\n\n    def select_task(self, task_name):\n        if self.task_name == task_name:\n            return\n        self.task_name = task_name\n        self._event_system.emit(\n            \"task.changed\",\n            {\n                \"project_name\": self.project_name,\n                \"asset_id\": self.asset_id,\n                \"task_name\": task_name\n            },\n            \"selection.model\"\n        )\n\n\nclass UserPublishValues:\n    \"\"\"Helper object to validate values required for push to different project.\n\n    Args:\n        event_system (EventSystem): Event system to catch and emit events.\n        new_asset_name (str): Name of new asset name.\n        variant (str): Variant for new subset name in new project.\n    \"\"\"\n\n    asset_name_regex = re.compile(\"^[a-zA-Z0-9_.]+$\")\n    variant_regex = re.compile(\"^[{}]+$\".format(SUBSET_NAME_ALLOWED_SYMBOLS))\n\n    def __init__(self, event_system):\n        self._event_system = event_system\n        self._new_asset_name = None\n        self._variant = None\n        self._comment = None\n        self._is_variant_valid = False\n        self._is_new_asset_name_valid = False\n\n        self.set_new_asset(\"\")\n        self.set_variant(\"\")\n        self.set_comment(\"\")\n\n    @property\n    def new_asset_name(self):\n        return self._new_asset_name\n\n    @property\n    def variant(self):\n        return self._variant\n\n    @property\n    def comment(self):\n        return self._comment\n\n    @property\n    def is_variant_valid(self):\n        return self._is_variant_valid\n\n    @property\n    def is_new_asset_name_valid(self):\n        return self._is_new_asset_name_valid\n\n    @property\n    def is_valid(self):\n        return self.is_variant_valid and self.is_new_asset_name_valid\n\n    def set_variant(self, variant):\n        if variant == self._variant:\n            return\n\n        old_variant = self._variant\n        old_is_valid = self._is_variant_valid\n\n        self._variant = variant\n        is_valid = False\n        if variant:\n            is_valid = self.variant_regex.match(variant) is not None\n        self._is_variant_valid = is_valid\n\n        changes = {\n            key: {\"new\": new, \"old\": old}\n            for key, old, new in (\n                (\"variant\", old_variant, variant),\n                (\"is_valid\", old_is_valid, is_valid)\n            )\n        }\n\n        self._event_system.emit(\n            \"variant.changed\",\n            {\n                \"variant\": variant,\n                \"is_valid\": self._is_variant_valid,\n                \"changes\": changes\n            },\n            \"user_values\"\n        )\n\n    def set_new_asset(self, asset_name):\n        if self._new_asset_name == asset_name:\n            return\n        old_asset_name = self._new_asset_name\n        old_is_valid = self._is_new_asset_name_valid\n        self._new_asset_name = asset_name\n        is_valid = True\n        if asset_name:\n            is_valid = (\n                self.asset_name_regex.match(asset_name) is not None\n            )\n        self._is_new_asset_name_valid = is_valid\n        changes = {\n            key: {\"new\": new, \"old\": old}\n            for key, old, new in (\n                (\"new_asset_name\", old_asset_name, asset_name),\n                (\"is_valid\", old_is_valid, is_valid)\n            )\n        }\n\n        self._event_system.emit(\n            \"new_asset_name.changed\",\n            {\n                \"new_asset_name\": self._new_asset_name,\n                \"is_valid\": self._is_new_asset_name_valid,\n                \"changes\": changes\n            },\n            \"user_values\"\n        )\n\n    def set_comment(self, comment):\n        if comment == self._comment:\n            return\n        old_comment = self._comment\n        self._comment = comment\n        self._event_system.emit(\n            \"comment.changed\",\n            {\n                \"comment\": comment,\n                \"changes\": {\n                    \"comment\": {\"new\": comment, \"old\": old_comment}\n                }\n            },\n            \"user_values\"\n        )\n\n\nclass PushToContextController:\n    def __init__(\n        self, project_name=None, version_id=None, library_filter=True\n    ):\n        self._src_project_name = None\n        self._src_version_id = None\n        self._src_asset_doc = None\n        self._src_subset_doc = None\n        self._src_version_doc = None\n\n        event_system = EventSystem()\n        entities_model = EntitiesModel(\n            event_system, library_filter=library_filter\n        )\n        selection_model = SelectionModel(event_system)\n        user_values = UserPublishValues(event_system)\n\n        self._event_system = event_system\n        self._entities_model = entities_model\n        self._selection_model = selection_model\n        self._user_values = user_values\n\n        event_system.add_callback(\"project.changed\", self._on_project_change)\n        event_system.add_callback(\"asset.changed\", self._invalidate)\n        event_system.add_callback(\"variant.changed\", self._invalidate)\n        event_system.add_callback(\"new_asset_name.changed\", self._invalidate)\n\n        self._submission_enabled = False\n        self._process_thread = None\n        self._process_item = None\n\n        self.set_source(project_name, version_id)\n\n    def _get_task_info_from_repre_docs(self, asset_doc, repre_docs):\n        asset_tasks = asset_doc[\"data\"].get(\"tasks\") or {}\n        found_comb = []\n        for repre_doc in repre_docs:\n            context = repre_doc[\"context\"]\n            task_info = context.get(\"task\")\n            if task_info is None:\n                continue\n\n            task_name = None\n            task_type = None\n            if isinstance(task_info, str):\n                task_name = task_info\n                asset_task_info = asset_tasks.get(task_info) or {}\n                task_type = asset_task_info.get(\"type\")\n\n            elif isinstance(task_info, dict):\n                task_name = task_info.get(\"name\")\n                task_type = task_info.get(\"type\")\n\n            if task_name and task_type:\n                return task_name, task_type\n\n            if task_name:\n                found_comb.append((task_name, task_type))\n\n        for task_name, task_type in found_comb:\n            return task_name, task_type\n        return None, None\n\n    def _get_src_variant(self):\n        project_name = self._src_project_name\n        version_doc = self._src_version_doc\n        asset_doc = self._src_asset_doc\n        repre_docs = get_representations(\n            project_name, version_ids=[version_doc[\"_id\"]]\n        )\n        task_name, task_type = self._get_task_info_from_repre_docs(\n            asset_doc, repre_docs\n        )\n\n        project_settings = get_project_settings(project_name)\n        subset_doc = self.src_subset_doc\n        family = subset_doc[\"data\"].get(\"family\")\n        if not family:\n            family = subset_doc[\"data\"][\"families\"][0]\n        template = get_subset_name_template(\n            self._src_project_name,\n            family,\n            task_name,\n            task_type,\n            None,\n            project_settings=project_settings\n        )\n        template_low = template.lower()\n        variant_placeholder = \"{variant}\"\n        if (\n            variant_placeholder not in template_low\n            or (not task_name and \"{task\" in template_low)\n        ):\n            return \"\"\n\n        idx = template_low.index(variant_placeholder)\n        template_s = template[:idx]\n        template_e = template[idx + len(variant_placeholder):]\n        fill_data = prepare_template_data({\n            \"family\": family,\n            \"task\": task_name\n        })\n        try:\n            subset_s = template_s.format(**fill_data)\n            subset_e = template_e.format(**fill_data)\n        except Exception as exc:\n            print(\"Failed format\", exc)\n            return \"\"\n\n        subset_name = self.src_subset_doc[\"name\"]\n        if (\n            (subset_s and not subset_name.startswith(subset_s))\n            or (subset_e and not subset_name.endswith(subset_e))\n        ):\n            return \"\"\n\n        if subset_s:\n            subset_name = subset_name[len(subset_s):]\n        if subset_e:\n            subset_name = subset_name[:len(subset_e)]\n        return subset_name\n\n    def set_source(self, project_name, version_id):\n        if (\n            project_name == self._src_project_name\n            and version_id == self._src_version_id\n        ):\n            return\n\n        self._src_project_name = project_name\n        self._src_version_id = version_id\n        asset_doc = None\n        subset_doc = None\n        version_doc = None\n        if project_name and version_id:\n            version_doc = get_version_by_id(project_name, version_id)\n\n        if version_doc:\n            subset_doc = get_subset_by_id(project_name, version_doc[\"parent\"])\n\n        if subset_doc:\n            asset_doc = get_asset_by_id(project_name, subset_doc[\"parent\"])\n\n        self._src_asset_doc = asset_doc\n        self._src_subset_doc = subset_doc\n        self._src_version_doc = version_doc\n        if asset_doc:\n            self.user_values.set_new_asset(asset_doc[\"name\"])\n            variant = self._get_src_variant()\n            if variant:\n                self.user_values.set_variant(variant)\n\n            comment = version_doc[\"data\"].get(\"comment\")\n            if comment:\n                self.user_values.set_comment(comment)\n\n        self._event_system.emit(\n            \"source.changed\", {\n                \"project_name\": project_name,\n                \"version_id\": version_id\n            },\n            \"controller\"\n        )\n\n    @property\n    def src_project_name(self):\n        return self._src_project_name\n\n    @property\n    def src_version_id(self):\n        return self._src_version_id\n\n    @property\n    def src_label(self):\n        if not self._src_project_name or not self._src_version_id:\n            return \"Source is not defined\"\n\n        asset_doc = self.src_asset_doc\n        if not asset_doc:\n            return \"Source is invalid\"\n\n        asset_path_parts = list(asset_doc[\"data\"][\"parents\"])\n        asset_path_parts.append(asset_doc[\"name\"])\n        asset_path = \"/\".join(asset_path_parts)\n        subset_doc = self.src_subset_doc\n        version_doc = self.src_version_doc\n        return \"Source: {}/{}/{}/v{:0>3}\".format(\n            self._src_project_name,\n            asset_path,\n            subset_doc[\"name\"],\n            version_doc[\"name\"]\n        )\n\n    @property\n    def src_version_doc(self):\n        return self._src_version_doc\n\n    @property\n    def src_subset_doc(self):\n        return self._src_subset_doc\n\n    @property\n    def src_asset_doc(self):\n        return self._src_asset_doc\n\n    @property\n    def event_system(self):\n        return self._event_system\n\n    @property\n    def model(self):\n        return self._entities_model\n\n    @property\n    def selection_model(self):\n        return self._selection_model\n\n    @property\n    def user_values(self):\n        return self._user_values\n\n    @property\n    def submission_enabled(self):\n        return self._submission_enabled\n\n    def _on_project_change(self, event):\n        project_name = event[\"project_name\"]\n        self.model.refresh_assets(project_name)\n        self._invalidate()\n\n    def _invalidate(self):\n        submission_enabled = self._check_submit_validations()\n        if submission_enabled == self._submission_enabled:\n            return\n        self._submission_enabled = submission_enabled\n        self._event_system.emit(\n            \"submission.enabled.changed\",\n            {\"enabled\": submission_enabled},\n            \"controller\"\n        )\n\n    def _check_submit_validations(self):\n        if not self._user_values.is_valid:\n            return False\n\n        if not self.selection_model.project_name:\n            return False\n\n        if (\n            not self._user_values.new_asset_name\n            and not self.selection_model.asset_id\n        ):\n            return False\n\n        return True\n\n    def get_selected_asset_name(self):\n        project_name = self._selection_model.project_name\n        asset_id = self._selection_model.asset_id\n        if not project_name or not asset_id:\n            return None\n        asset_item = self._entities_model.get_asset_by_id(\n            project_name, asset_id\n        )\n        if asset_item:\n            return asset_item.name\n        return None\n\n    def submit(self, wait=True, context_only=False):\n        if not self.submission_enabled:\n            return\n\n        if self._process_thread is not None:\n            return\n\n        if context_only:\n            return\n\n        item = ProjectPushItem(\n            self.src_project_name,\n            self.src_version_id,\n            self.selection_model.project_name,\n            self.selection_model.asset_id,\n            self.selection_model.task_name,\n            self.user_values.variant,\n            comment=self.user_values.comment,\n            new_asset_name=self.user_values.new_asset_name,\n            dst_version=1\n        )\n\n        status_item = ProjectPushItemStatus(event_system=self._event_system)\n        process_item = ProjectPushItemProcess(item, status_item)\n        self._process_item = process_item\n        self._event_system.emit(\"submit.started\", {}, \"controller\")\n        if wait:\n            self._submit_callback()\n            self._process_item = None\n            return process_item\n\n        thread = threading.Thread(target=self._submit_callback)\n        self._process_thread = thread\n        thread.start()\n        return process_item\n\n    def wait_for_process_thread(self):\n        if self._process_thread is None:\n            return\n        self._process_thread.join()\n        self._process_thread = None\n\n    def _submit_callback(self):\n        process_item = self._process_item\n        if process_item is None:\n            return\n        process_item.process()\n        self._event_system.emit(\"submit.finished\", {}, \"controller\")\n        if process_item is self._process_item:\n            self._process_item = None\n"
  },
  {
    "path": "openpype/tools/push_to_project/control_integrate.py",
    "content": "import os\nimport re\nimport copy\nimport socket\nimport itertools\nimport datetime\nimport sys\nimport traceback\n\nfrom bson.objectid import ObjectId\n\nfrom openpype.client import (\n    get_project,\n    get_assets,\n    get_asset_by_id,\n    get_subset_by_id,\n    get_subset_by_name,\n    get_version_by_id,\n    get_last_version_by_subset_id,\n    get_version_by_name,\n    get_representations,\n)\nfrom openpype.client.operations import (\n    OperationsSession,\n    new_asset_document,\n    new_subset_document,\n    new_version_doc,\n    new_representation_doc,\n    prepare_version_update_data,\n    prepare_representation_update_data,\n)\nfrom openpype.modules import ModulesManager\nfrom openpype.lib import (\n    StringTemplate,\n    get_openpype_username,\n    get_formatted_current_time,\n    source_hash,\n)\n\nfrom openpype.lib.file_transaction import FileTransaction\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import Anatomy\nfrom openpype.pipeline.version_start import get_versioning_start\nfrom openpype.pipeline.template_data import get_template_data\nfrom openpype.pipeline.publish import get_publish_template_name\nfrom openpype.pipeline.create import get_subset_name\n\nUNKNOWN = object()\n\n\nclass PushToProjectError(Exception):\n    pass\n\n\nclass FileItem(object):\n    def __init__(self, path):\n        self.path = path\n\n    @property\n    def is_valid_file(self):\n        return os.path.exists(self.path) and os.path.isfile(self.path)\n\n\nclass SourceFile(FileItem):\n    def __init__(self, path, frame=None, udim=None):\n        super(SourceFile, self).__init__(path)\n        self.frame = frame\n        self.udim = udim\n\n    def __repr__(self):\n        subparts = [self.__class__.__name__]\n        if self.frame is not None:\n            subparts.append(\"frame: {}\".format(self.frame))\n        if self.udim is not None:\n            subparts.append(\"UDIM: {}\".format(self.udim))\n\n        return \"<{}> '{}'\".format(\" - \".join(subparts), self.path)\n\n\nclass ResourceFile(FileItem):\n    def __init__(self, path, relative_path):\n        super(ResourceFile, self).__init__(path)\n        self.relative_path = relative_path\n\n    def __repr__(self):\n        return \"<{}> '{}'\".format(self.__class__.__name__, self.relative_path)\n\n    @property\n    def is_valid_file(self):\n        if not self.relative_path:\n            return False\n        return super(ResourceFile, self).is_valid_file\n\n\nclass ProjectPushItem:\n    def __init__(\n        self,\n        src_project_name,\n        src_version_id,\n        dst_project_name,\n        dst_asset_id,\n        dst_task_name,\n        variant,\n        comment=None,\n        new_asset_name=None,\n        dst_version=None\n    ):\n        self.src_project_name = src_project_name\n        self.src_version_id = src_version_id\n        self.dst_project_name = dst_project_name\n        self.dst_asset_id = dst_asset_id\n        self.dst_task_name = dst_task_name\n        self.dst_version = dst_version\n        self.variant = variant\n        self.new_asset_name = new_asset_name\n        self.comment = comment or \"\"\n        self._id = \"|\".join([\n            src_project_name,\n            src_version_id,\n            dst_project_name,\n            str(dst_asset_id),\n            str(new_asset_name),\n            str(dst_task_name),\n            str(dst_version)\n        ])\n\n    @property\n    def id(self):\n        return self._id\n\n    def __repr__(self):\n        return \"<{} - {}>\".format(self.__class__.__name__, self.id)\n\n\nclass StatusMessage:\n    def __init__(self, message, level):\n        self.message = message\n        self.level = level\n\n    def __str__(self):\n        return \"{}: {}\".format(self.level.upper(), self.message)\n\n    def __repr__(self):\n        return \"<{} - {}> {}\".format(\n            self.__class__.__name__, self.level.upper, self.message\n        )\n\n\nclass ProjectPushItemStatus:\n    def __init__(\n        self,\n        failed=False,\n        finished=False,\n        fail_reason=None,\n        formatted_traceback=None,\n        messages=None,\n        event_system=None\n    ):\n        if messages is None:\n            messages = []\n        self._failed = failed\n        self._finished = finished\n        self._fail_reason = fail_reason\n        self._traceback = formatted_traceback\n        self._messages = messages\n        self._event_system = event_system\n\n    def emit_event(self, topic, data=None):\n        if self._event_system is None:\n            return\n\n        self._event_system.emit(topic, data or {}, \"push.status\")\n\n    def get_finished(self):\n        \"\"\"Processing of push to project finished.\n\n        Returns:\n            bool: Finished.\n        \"\"\"\n\n        return self._finished\n\n    def set_finished(self, finished=True):\n        \"\"\"Mark status as finished.\n\n        Args:\n            finished (bool): Processing finished (failed or not).\n        \"\"\"\n\n        if finished != self._finished:\n            self._finished = finished\n            self.emit_event(\"push.finished.changed\", {\"finished\": finished})\n\n    finished = property(get_finished, set_finished)\n\n    def set_failed(self, fail_reason, exc_info=None):\n        \"\"\"Set status as failed.\n\n        Attribute 'fail_reason' can change automatically based on passed value.\n        Reason is unset if 'failed' is 'False' and is set do default reason if\n        is set to 'True' and reason is not set.\n\n        Args:\n            failed (bool): Push to project failed.\n            fail_reason (str): Reason why failed.\n        \"\"\"\n\n        failed = True\n        if not fail_reason and not exc_info:\n            failed = False\n\n        full_traceback = None\n        if exc_info is not None:\n            full_traceback = \"\".join(traceback.format_exception(*exc_info))\n            if not fail_reason:\n                fail_reason = \"Failed without specified reason\"\n\n        if (\n            self._failed == failed\n            and self._traceback == full_traceback\n            and self._fail_reason == fail_reason\n        ):\n            return\n\n        self._failed = failed\n        self._fail_reason = fail_reason or None\n        self._traceback = full_traceback\n\n        self.emit_event(\n            \"push.failed.changed\",\n            {\n                \"failed\": failed,\n                \"reason\": fail_reason,\n                \"traceback\": full_traceback\n            }\n        )\n\n    @property\n    def failed(self):\n        \"\"\"Processing failed.\n\n        Returns:\n            bool: Processing failed.\n        \"\"\"\n\n        return self._failed\n\n    @property\n    def fail_reason(self):\n        \"\"\"Reason why push to process failed.\n\n        Returns:\n            Union[str, None]: Reason why push failed or None.\n        \"\"\"\n\n        return self._fail_reason\n\n    @property\n    def traceback(self):\n        \"\"\"Traceback of failed process.\n\n        Traceback is available only if unhandled exception happened.\n\n        Returns:\n            Union[str, None]: Formatted traceback.\n        \"\"\"\n\n        return self._traceback\n\n    # Loggin helpers\n    # TODO better logging\n    def add_message(self, message, level):\n        message_obj = StatusMessage(message, level)\n        self._messages.append(message_obj)\n        self.emit_event(\n            \"push.message.added\",\n            {\"message\": message, \"level\": level}\n        )\n        print(message_obj)\n        return message_obj\n\n    def debug(self, message):\n        return self.add_message(message, \"debug\")\n\n    def info(self, message):\n        return self.add_message(message, \"info\")\n\n    def warning(self, message):\n        return self.add_message(message, \"warning\")\n\n    def error(self, message):\n        return self.add_message(message, \"error\")\n\n    def critical(self, message):\n        return self.add_message(message, \"critical\")\n\n\nclass ProjectPushRepreItem:\n    \"\"\"Representation item.\n\n    Representation item based on representation document and project roots.\n\n    Representation document may have reference to:\n    - source files: Files defined with publish template\n    - resource files: Files that should be in publish directory\n        but filenames are not template based.\n\n    Args:\n        repre_doc (Dict[str, Ant]): Representation document.\n        roots (Dict[str, str]): Project roots (based on project anatomy).\n    \"\"\"\n\n    def __init__(self, repre_doc, roots):\n        self._repre_doc = repre_doc\n        self._roots = roots\n        self._src_files = None\n        self._resource_files = None\n        self._frame = UNKNOWN\n\n    @property\n    def repre_doc(self):\n        return self._repre_doc\n\n    @property\n    def src_files(self):\n        if self._src_files is None:\n            self.get_source_files()\n        return self._src_files\n\n    @property\n    def resource_files(self):\n        if self._resource_files is None:\n            self.get_source_files()\n        return self._resource_files\n\n    @staticmethod\n    def _clean_path(path):\n        new_value = path.replace(\"\\\\\", \"/\")\n        while \"//\" in new_value:\n            new_value = new_value.replace(\"//\", \"/\")\n        return new_value\n\n    @staticmethod\n    def _get_relative_path(path, src_dirpath):\n        dirpath, basename = os.path.split(path)\n        if not dirpath.lower().startswith(src_dirpath.lower()):\n            return None\n\n        relative_dir = dirpath[len(src_dirpath):].lstrip(\"/\")\n        if relative_dir:\n            relative_path = \"/\".join([relative_dir, basename])\n        else:\n            relative_path = basename\n        return relative_path\n\n    @property\n    def frame(self):\n        \"\"\"First frame of representation files.\n\n        This value will be in representation document context if is sequence.\n\n        Returns:\n            Union[int, None]: First frame in representation files based on\n                source files or None if frame is not part of filename.\n        \"\"\"\n\n        if self._frame is UNKNOWN:\n            frame = None\n            for src_file in self.src_files:\n                src_frame = src_file.frame\n                if (\n                    src_frame is not None\n                    and (frame is None or src_frame < frame)\n                ):\n                    frame = src_frame\n            self._frame = frame\n        return self._frame\n\n    @staticmethod\n    def validate_source_files(src_files, resource_files):\n        if not src_files:\n            raise AssertionError((\n                \"Couldn't figure out source files from representation.\"\n                \" Found resource files {}\"\n            ).format(\", \".join(str(i) for i in resource_files)))\n\n        invalid_items = [\n            item\n            for item in itertools.chain(src_files, resource_files)\n            if not item.is_valid_file\n        ]\n        if invalid_items:\n            raise AssertionError((\n                \"Source files that were not found on disk: {}\"\n            ).format(\", \".join(str(i) for i in invalid_items)))\n\n    def get_source_files(self):\n        if self._src_files is not None:\n            return self._src_files, self._resource_files\n\n        repre_context = self._repre_doc[\"context\"]\n        if \"frame\" in repre_context or \"udim\" in repre_context:\n            src_files, resource_files = self._get_source_files_with_frames()\n        else:\n            src_files, resource_files = self._get_source_files()\n\n        self.validate_source_files(src_files, resource_files)\n\n        self._src_files = src_files\n        self._resource_files = resource_files\n        return self._src_files, self._resource_files\n\n    def _get_source_files_with_frames(self):\n        frame_placeholder = \"__frame__\"\n        udim_placeholder = \"__udim__\"\n        src_files = []\n        resource_files = []\n        template = self._repre_doc[\"data\"][\"template\"]\n        # Remove padding from 'udim' and 'frame' formatting keys\n        # - \"{frame:0>4}\" -> \"{frame}\"\n        for key in (\"udim\", \"frame\"):\n            sub_part = \"{\" + key + \"[^}]*}\"\n            replacement = \"{{{}}}\".format(key)\n            template = re.sub(sub_part, replacement, template)\n\n        repre_context = self._repre_doc[\"context\"]\n        fill_repre_context = copy.deepcopy(repre_context)\n        if \"frame\" in fill_repre_context:\n            fill_repre_context[\"frame\"] = frame_placeholder\n\n        if \"udim\" in fill_repre_context:\n            fill_repre_context[\"udim\"] = udim_placeholder\n\n        fill_roots = fill_repre_context[\"root\"]\n        for root_name in tuple(fill_roots.keys()):\n            fill_roots[root_name] = \"{{root[{}]}}\".format(root_name)\n        repre_path = StringTemplate.format_template(\n            template, fill_repre_context)\n        repre_path = self._clean_path(repre_path)\n        src_dirpath, src_basename = os.path.split(repre_path)\n        src_basename = (\n            re.escape(src_basename)\n            .replace(frame_placeholder, \"(?P<frame>[0-9]+)\")\n            .replace(udim_placeholder, \"(?P<udim>[0-9]+)\")\n        )\n        src_basename_regex = re.compile(\"^{}$\".format(src_basename))\n        for file_info in self._repre_doc[\"files\"]:\n            filepath_template = self._clean_path(file_info[\"path\"])\n            filepath = self._clean_path(\n                filepath_template.format(root=self._roots)\n            )\n            dirpath, basename = os.path.split(filepath_template)\n            if (\n                dirpath.lower() != src_dirpath.lower()\n                or not src_basename_regex.match(basename)\n            ):\n                relative_path = self._get_relative_path(filepath, src_dirpath)\n                resource_files.append(ResourceFile(filepath, relative_path))\n                continue\n\n            filepath = os.path.join(src_dirpath, basename)\n            frame = None\n            udim = None\n            for item in src_basename_regex.finditer(basename):\n                group_name = item.lastgroup\n                value = item.group(group_name)\n                if group_name == \"frame\":\n                    frame = int(value)\n                elif group_name == \"udim\":\n                    udim = value\n\n            src_files.append(SourceFile(filepath, frame, udim))\n\n        return src_files, resource_files\n\n    def _get_source_files(self):\n        src_files = []\n        resource_files = []\n        template = self._repre_doc[\"data\"][\"template\"]\n        repre_context = self._repre_doc[\"context\"]\n        fill_repre_context = copy.deepcopy(repre_context)\n        fill_roots = fill_repre_context[\"root\"]\n        for root_name in tuple(fill_roots.keys()):\n            fill_roots[root_name] = \"{{root[{}]}}\".format(root_name)\n        repre_path = StringTemplate.format_template(template,\n                                                    fill_repre_context)\n        repre_path = self._clean_path(repre_path)\n        src_dirpath = os.path.dirname(repre_path)\n        for file_info in self._repre_doc[\"files\"]:\n            filepath_template = self._clean_path(file_info[\"path\"])\n            filepath = self._clean_path(\n                filepath_template.format(root=self._roots))\n\n            if filepath_template.lower() == repre_path.lower():\n                src_files.append(\n                    SourceFile(repre_path.format(root=self._roots))\n                )\n            else:\n                relative_path = self._get_relative_path(\n                    filepath_template, src_dirpath\n                )\n                resource_files.append(\n                    ResourceFile(filepath, relative_path)\n                )\n        return src_files, resource_files\n\n\nclass ProjectPushItemProcess:\n    \"\"\"\n    Args:\n        item (ProjectPushItem): Item which is being processed.\n        item_status (ProjectPushItemStatus): Object to store status.\n    \"\"\"\n\n    # TODO where to get host?!!!\n    host_name = \"republisher\"\n\n    def __init__(self, item, item_status=None):\n        self._item = item\n\n        self._src_project_doc = None\n        self._src_asset_doc = None\n        self._src_subset_doc = None\n        self._src_version_doc = None\n        self._src_repre_items = None\n        self._src_anatomy = None\n\n        self._project_doc = None\n        self._anatomy = None\n        self._asset_doc = None\n        self._created_asset_doc = None\n        self._task_info = None\n        self._subset_doc = None\n        self._version_doc = None\n\n        self._family = None\n        self._subset_name = None\n\n        self._project_settings = None\n        self._template_name = None\n\n        if item_status is None:\n            item_status = ProjectPushItemStatus()\n        self._status = item_status\n        self._operations = OperationsSession()\n        self._file_transaction = FileTransaction()\n\n    @property\n    def status(self):\n        return self._status\n\n    @property\n    def src_project_doc(self):\n        return self._src_project_doc\n\n    @property\n    def src_anatomy(self):\n        return self._src_anatomy\n\n    @property\n    def src_asset_doc(self):\n        return self._src_asset_doc\n\n    @property\n    def src_subset_doc(self):\n        return self._src_subset_doc\n\n    @property\n    def src_version_doc(self):\n        return self._src_version_doc\n\n    @property\n    def src_repre_items(self):\n        return self._src_repre_items\n\n    @property\n    def project_doc(self):\n        return self._project_doc\n\n    @property\n    def anatomy(self):\n        return self._anatomy\n\n    @property\n    def project_settings(self):\n        return self._project_settings\n\n    @property\n    def asset_doc(self):\n        return self._asset_doc\n\n    @property\n    def task_info(self):\n        return self._task_info\n\n    @property\n    def subset_doc(self):\n        return self._subset_doc\n\n    @property\n    def version_doc(self):\n        return self._version_doc\n\n    @property\n    def variant(self):\n        return self._item.variant\n\n    @property\n    def family(self):\n        return self._family\n\n    @property\n    def subset_name(self):\n        return self._subset_name\n\n    @property\n    def template_name(self):\n        return self._template_name\n\n    def fill_source_variables(self):\n        src_project_name = self._item.src_project_name\n        src_version_id = self._item.src_version_id\n\n        project_doc = get_project(src_project_name)\n        if not project_doc:\n            self._status.set_failed(\n                f\"Source project \\\"{src_project_name}\\\" was not found\"\n            )\n            raise PushToProjectError(self._status.fail_reason)\n\n        self._status.debug(f\"Project '{src_project_name}' found\")\n\n        version_doc = get_version_by_id(src_project_name, src_version_id)\n        if not version_doc:\n            self._status.set_failed((\n                f\"Source version with id \\\"{src_version_id}\\\"\"\n                f\" was not found in project \\\"{src_project_name}\\\"\"\n            ))\n            raise PushToProjectError(self._status.fail_reason)\n\n        subset_id = version_doc[\"parent\"]\n        subset_doc = get_subset_by_id(src_project_name, subset_id)\n        if not subset_doc:\n            self._status.set_failed((\n                f\"Could find subset with id \\\"{subset_id}\\\"\"\n                f\" in project \\\"{src_project_name}\\\"\"\n            ))\n            raise PushToProjectError(self._status.fail_reason)\n\n        asset_id = subset_doc[\"parent\"]\n        asset_doc = get_asset_by_id(src_project_name, asset_id)\n        if not asset_doc:\n            self._status.set_failed((\n                f\"Could find asset with id \\\"{asset_id}\\\"\"\n                f\" in project \\\"{src_project_name}\\\"\"\n            ))\n            raise PushToProjectError(self._status.fail_reason)\n\n        anatomy = Anatomy(src_project_name)\n\n        repre_docs = get_representations(\n            src_project_name,\n            version_ids=[src_version_id]\n        )\n        repre_items = [\n            ProjectPushRepreItem(repre_doc, anatomy.roots)\n            for repre_doc in repre_docs\n        ]\n        self._status.debug((\n            f\"Found {len(repre_items)} representations on\"\n            f\" version {src_version_id} in project '{src_project_name}'\"\n        ))\n        if not repre_items:\n            self._status.set_failed(\n                \"Source version does not have representations\"\n                f\" (Version id: {src_version_id})\"\n            )\n            raise PushToProjectError(self._status.fail_reason)\n\n        self._src_anatomy = anatomy\n        self._src_project_doc = project_doc\n        self._src_asset_doc = asset_doc\n        self._src_subset_doc = subset_doc\n        self._src_version_doc = version_doc\n        self._src_repre_items = repre_items\n\n    def fill_destination_project(self):\n        # --- Destination entities ---\n        dst_project_name = self._item.dst_project_name\n        # Validate project existence\n        dst_project_doc = get_project(dst_project_name)\n        if not dst_project_doc:\n            self._status.set_failed(\n                f\"Destination project '{dst_project_name}' was not found\"\n            )\n            raise PushToProjectError(self._status.fail_reason)\n\n        self._status.debug(\n            f\"Destination project '{dst_project_name}' found\"\n        )\n        self._project_doc = dst_project_doc\n        self._anatomy = Anatomy(dst_project_name)\n        self._project_settings = get_project_settings(\n            self._item.dst_project_name\n        )\n\n    def _create_asset(\n        self,\n        src_asset_doc,\n        project_doc,\n        parent_asset_doc,\n        asset_name\n    ):\n        parent_id = None\n        parents = []\n        tools = []\n        if parent_asset_doc:\n            parent_id = parent_asset_doc[\"_id\"]\n            parents = list(parent_asset_doc[\"data\"][\"parents\"])\n            parents.append(parent_asset_doc[\"name\"])\n            _tools = parent_asset_doc[\"data\"].get(\"tools_env\")\n            if _tools:\n                tools = list(_tools)\n\n        asset_name_low = asset_name.lower()\n        other_asset_docs = get_assets(\n            project_doc[\"name\"], fields=[\"_id\", \"name\", \"data.visualParent\"]\n        )\n        for other_asset_doc in other_asset_docs:\n            other_name = other_asset_doc[\"name\"]\n            other_parent_id = other_asset_doc[\"data\"].get(\"visualParent\")\n            if other_name.lower() != asset_name_low:\n                continue\n\n            if other_parent_id != parent_id:\n                self._status.set_failed((\n                    f\"Asset with name \\\"{other_name}\\\" already\"\n                    \" exists in different hierarchy.\"\n                ))\n                raise PushToProjectError(self._status.fail_reason)\n\n            self._status.debug((\n                f\"Found already existing asset with name \\\"{other_name}\\\"\"\n                f\" which match requested name \\\"{asset_name}\\\"\"\n            ))\n            return get_asset_by_id(project_doc[\"name\"], other_asset_doc[\"_id\"])\n\n        data_keys = (\n            \"clipIn\",\n            \"clipOut\",\n            \"frameStart\",\n            \"frameEnd\",\n            \"handleStart\",\n            \"handleEnd\",\n            \"resolutionWidth\",\n            \"resolutionHeight\",\n            \"fps\",\n            \"pixelAspect\",\n        )\n        asset_data = {\n            \"visualParent\": parent_id,\n            \"parents\": parents,\n            \"tasks\": {},\n            \"tools_env\": tools\n        }\n        src_asset_data = src_asset_doc[\"data\"]\n        for key in data_keys:\n            if key in src_asset_data:\n                asset_data[key] = src_asset_data[key]\n\n        asset_doc = new_asset_document(\n            asset_name,\n            project_doc[\"_id\"],\n            parent_id,\n            parents,\n            data=asset_data\n        )\n        self._operations.create_entity(\n            project_doc[\"name\"],\n            asset_doc[\"type\"],\n            asset_doc\n        )\n        self._status.info(\n            f\"Creating new asset with name \\\"{asset_name}\\\"\"\n        )\n        self._created_asset_doc = asset_doc\n        return asset_doc\n\n    def fill_or_create_destination_asset(self):\n        dst_project_name = self._item.dst_project_name\n        dst_asset_id = self._item.dst_asset_id\n        dst_task_name = self._item.dst_task_name\n        new_asset_name = self._item.new_asset_name\n        if not dst_asset_id and not new_asset_name:\n            self._status.set_failed(\n                \"Push item does not have defined destination asset\"\n            )\n            raise PushToProjectError(self._status.fail_reason)\n\n        # Get asset document\n        parent_asset_doc = None\n        if dst_asset_id:\n            parent_asset_doc = get_asset_by_id(\n                self._item.dst_project_name, self._item.dst_asset_id\n            )\n            if not parent_asset_doc:\n                self._status.set_failed(\n                    f\"Could find asset with id \\\"{dst_asset_id}\\\"\"\n                    f\" in project \\\"{dst_project_name}\\\"\"\n                )\n                raise PushToProjectError(self._status.fail_reason)\n\n        if not new_asset_name:\n            asset_doc = parent_asset_doc\n        else:\n            asset_doc = self._create_asset(\n                self.src_asset_doc,\n                self.project_doc,\n                parent_asset_doc,\n                new_asset_name\n            )\n        self._asset_doc = asset_doc\n        if not dst_task_name:\n            self._task_info = {}\n            return\n\n        asset_path_parts = list(asset_doc[\"data\"][\"parents\"])\n        asset_path_parts.append(asset_doc[\"name\"])\n        asset_path = \"/\".join(asset_path_parts)\n        asset_tasks = asset_doc.get(\"data\", {}).get(\"tasks\") or {}\n        task_info = asset_tasks.get(dst_task_name)\n        if not task_info:\n            self._status.set_failed(\n                f\"Could find task with name \\\"{dst_task_name}\\\"\"\n                f\" on asset \\\"{asset_path}\\\"\"\n                f\" in project \\\"{dst_project_name}\\\"\"\n            )\n            raise PushToProjectError(self._status.fail_reason)\n\n        # Create copy of task info to avoid changing data in asset document\n        task_info = copy.deepcopy(task_info)\n        task_info[\"name\"] = dst_task_name\n        # Fill rest of task information based on task type\n        task_type = task_info[\"type\"]\n        task_type_info = self.project_doc[\"config\"][\"tasks\"].get(task_type, {})\n        task_info.update(task_type_info)\n        self._task_info = task_info\n\n    def determine_family(self):\n        subset_doc = self.src_subset_doc\n        family = subset_doc[\"data\"].get(\"family\")\n        families = subset_doc[\"data\"].get(\"families\")\n        if not family and families:\n            family = families[0]\n\n        if not family:\n            self._status.set_failed(\n                \"Couldn't figure out family from source subset\"\n            )\n            raise PushToProjectError(self._status.fail_reason)\n\n        self._status.debug(\n            f\"Publishing family is '{family}' (Based on source subset)\"\n        )\n        self._family = family\n\n    def determine_publish_template_name(self):\n        template_name = get_publish_template_name(\n            self._item.dst_project_name,\n            self.host_name,\n            self.family,\n            self.task_info.get(\"name\"),\n            self.task_info.get(\"type\"),\n            project_settings=self.project_settings\n        )\n        self._status.debug(\n            f\"Using template '{template_name}' for integration\"\n        )\n        self._template_name = template_name\n\n    def determine_subset_name(self):\n        family = self.family\n        asset_doc = self.asset_doc\n        task_info = self.task_info\n        subset_name = get_subset_name(\n            family,\n            self.variant,\n            task_info.get(\"name\"),\n            asset_doc,\n            project_name=self._item.dst_project_name,\n            host_name=self.host_name,\n            project_settings=self.project_settings\n        )\n        self._status.info(\n            f\"Push will be integrating to subset with name '{subset_name}'\"\n        )\n        self._subset_name = subset_name\n\n    def make_sure_subset_exists(self):\n        project_name = self._item.dst_project_name\n        asset_id = self.asset_doc[\"_id\"]\n        subset_name = self.subset_name\n        family = self.family\n        subset_doc = get_subset_by_name(project_name, subset_name, asset_id)\n        if subset_doc:\n            self._subset_doc = subset_doc\n            return subset_doc\n\n        data = {\n            \"families\": [family]\n        }\n        subset_doc = new_subset_document(\n            subset_name, family, asset_id, data\n        )\n        self._operations.create_entity(project_name, \"subset\", subset_doc)\n        self._subset_doc = subset_doc\n\n    def make_sure_version_exists(self):\n        \"\"\"Make sure version document exits in database.\"\"\"\n\n        project_name = self._item.dst_project_name\n        version = self._item.dst_version\n        src_version_doc = self.src_version_doc\n        subset_doc = self.subset_doc\n        subset_id = subset_doc[\"_id\"]\n        src_data = src_version_doc[\"data\"]\n        families = subset_doc[\"data\"].get(\"families\")\n        if not families:\n            families = [subset_doc[\"data\"][\"family\"]]\n\n        version_data = {\n            \"families\": list(families),\n            \"fps\": src_data.get(\"fps\"),\n            \"source\": src_data.get(\"source\"),\n            \"machine\": socket.gethostname(),\n            \"comment\": self._item.comment or \"\",\n            \"author\": get_openpype_username(),\n            \"time\": get_formatted_current_time(),\n        }\n        if version is None:\n            last_version_doc = get_last_version_by_subset_id(\n                project_name, subset_id\n            )\n            if last_version_doc:\n                version = int(last_version_doc[\"name\"]) + 1\n            else:\n                version = get_versioning_start(\n                    project_name,\n                    self.host_name,\n                    task_name=self.task_info[\"name\"],\n                    task_type=self.task_info[\"type\"],\n                    family=families[0],\n                    subset=subset_doc[\"name\"]\n                )\n\n        existing_version_doc = get_version_by_name(\n            project_name, version, subset_id\n        )\n        # Update existing version\n        if existing_version_doc:\n            version_doc = new_version_doc(\n                version, subset_id, version_data, existing_version_doc[\"_id\"]\n            )\n            update_data = prepare_version_update_data(\n                existing_version_doc, version_doc\n            )\n            if update_data:\n                self._operations.update_entity(\n                    project_name,\n                    \"version\",\n                    existing_version_doc[\"_id\"],\n                    update_data\n                )\n            self._version_doc = version_doc\n\n            return\n\n        version_doc = new_version_doc(\n            version, subset_id, version_data\n        )\n        self._operations.create_entity(project_name, \"version\", version_doc)\n\n        self._version_doc = version_doc\n\n    def integrate_representations(self):\n        try:\n            self._integrate_representations()\n        except Exception:\n            self._operations.clear()\n            self._file_transaction.rollback()\n            raise\n\n    def _integrate_representations(self):\n        version_doc = self.version_doc\n        version_id = version_doc[\"_id\"]\n        existing_repres = get_representations(\n            self._item.dst_project_name,\n            version_ids=[version_id]\n        )\n        existing_repres_by_low_name = {\n            repre_doc[\"name\"].lower(): repre_doc\n            for repre_doc in existing_repres\n        }\n        template_name = self.template_name\n        anatomy = self.anatomy\n        formatting_data = get_template_data(\n            self.project_doc,\n            self.asset_doc,\n            self.task_info.get(\"name\"),\n            self.host_name\n        )\n        formatting_data.update({\n            \"subset\": self.subset_name,\n            \"family\": self.family,\n            \"version\": version_doc[\"name\"]\n        })\n\n        path_template = anatomy.templates[template_name][\"path\"].replace(\n            \"\\\\\", \"/\"\n        )\n        file_template = StringTemplate(\n            anatomy.templates[template_name][\"file\"]\n        )\n        self._status.info(\"Preparing files to transfer\")\n        processed_repre_items = self._prepare_file_transactions(\n            anatomy, template_name, formatting_data, file_template\n        )\n        self._file_transaction.process()\n        self._status.info(\"Preparing database changes\")\n        self._prepare_database_operations(\n            version_id,\n            processed_repre_items,\n            path_template,\n            existing_repres_by_low_name\n        )\n        self._status.info(\"Finalization\")\n        self._operations.commit()\n        self._file_transaction.finalize()\n\n    def _prepare_file_transactions(\n        self, anatomy, template_name, formatting_data, file_template\n    ):\n        processed_repre_items = []\n        for repre_item in self.src_repre_items:\n            repre_doc = repre_item.repre_doc\n            repre_name = repre_doc[\"name\"]\n            repre_format_data = copy.deepcopy(formatting_data)\n            repre_format_data[\"representation\"] = repre_name\n            for src_file in repre_item.src_files:\n                ext = os.path.splitext(src_file.path)[-1]\n                repre_format_data[\"ext\"] = ext[1:]\n                break\n\n            # Re-use 'output' from source representation\n            repre_output_name = repre_doc[\"context\"].get(\"output\")\n            if repre_output_name is not None:\n                repre_format_data[\"output\"] = repre_output_name\n\n            template_obj = anatomy.templates_obj[template_name][\"folder\"]\n            folder_path = template_obj.format_strict(formatting_data)\n            repre_context = folder_path.used_values\n            folder_path_rootless = folder_path.rootless\n            repre_filepaths = []\n            published_path = None\n            for src_file in repre_item.src_files:\n                file_data = copy.deepcopy(repre_format_data)\n                frame = src_file.frame\n                if frame is not None:\n                    file_data[\"frame\"] = frame\n\n                udim = src_file.udim\n                if udim is not None:\n                    file_data[\"udim\"] = udim\n\n                filename = file_template.format_strict(file_data)\n                dst_filepath = os.path.normpath(\n                    os.path.join(folder_path, filename)\n                )\n                dst_rootless_path = os.path.normpath(\n                    os.path.join(folder_path_rootless, filename)\n                )\n                if published_path is None or frame == repre_item.frame:\n                    published_path = dst_filepath\n                    repre_context.update(filename.used_values)\n\n                repre_filepaths.append((dst_filepath, dst_rootless_path))\n                self._file_transaction.add(src_file.path, dst_filepath)\n\n            for resource_file in repre_item.resource_files:\n                dst_filepath = os.path.normpath(\n                    os.path.join(folder_path, resource_file.relative_path)\n                )\n                dst_rootless_path = os.path.normpath(\n                    os.path.join(\n                        folder_path_rootless, resource_file.relative_path\n                    )\n                )\n                repre_filepaths.append((dst_filepath, dst_rootless_path))\n                self._file_transaction.add(resource_file.path, dst_filepath)\n            processed_repre_items.append(\n                (repre_item, repre_filepaths, repre_context, published_path)\n            )\n        return processed_repre_items\n\n    def _prepare_database_operations(\n        self,\n        version_id,\n        processed_repre_items,\n        path_template,\n        existing_repres_by_low_name\n    ):\n        modules_manager = ModulesManager()\n        sync_server_module = modules_manager.get(\"sync_server\")\n        if sync_server_module is None or not sync_server_module.enabled:\n            sites = [{\n                \"name\": \"studio\",\n                \"created_dt\": datetime.datetime.now()\n            }]\n        else:\n            sites = sync_server_module.compute_resource_sync_sites(\n                project_name=self._item.dst_project_name\n            )\n\n        added_repre_names = set()\n        for item in processed_repre_items:\n            (repre_item, repre_filepaths, repre_context, published_path) = item\n            repre_name = repre_item.repre_doc[\"name\"]\n            added_repre_names.add(repre_name.lower())\n            new_repre_data = {\n                \"path\": published_path,\n                \"template\": path_template\n            }\n            new_repre_files = []\n            for (path, rootless_path) in repre_filepaths:\n                new_repre_files.append({\n                    \"_id\": ObjectId(),\n                    \"path\": rootless_path,\n                    \"size\": os.path.getsize(path),\n                    \"hash\": source_hash(path),\n                    \"sites\": sites\n                })\n\n            existing_repre = existing_repres_by_low_name.get(\n                repre_name.lower()\n            )\n            entity_id = None\n            if existing_repre:\n                entity_id = existing_repre[\"_id\"]\n            new_repre_doc = new_representation_doc(\n                repre_name,\n                version_id,\n                repre_context,\n                data=new_repre_data,\n                entity_id=entity_id\n            )\n            new_repre_doc[\"files\"] = new_repre_files\n            if not existing_repre:\n                self._operations.create_entity(\n                    self._item.dst_project_name,\n                    new_repre_doc[\"type\"],\n                    new_repre_doc\n                )\n            else:\n                update_data = prepare_representation_update_data(\n                    existing_repre, new_repre_doc\n                )\n                if update_data:\n                    self._operations.update_entity(\n                        self._item.dst_project_name,\n                        new_repre_doc[\"type\"],\n                        new_repre_doc[\"_id\"],\n                        update_data\n                    )\n\n        existing_repre_names = set(existing_repres_by_low_name.keys())\n        for repre_name in (existing_repre_names - added_repre_names):\n            repre_doc = existing_repres_by_low_name[repre_name]\n            self._operations.update_entity(\n                self._item.dst_project_name,\n                repre_doc[\"type\"],\n                repre_doc[\"_id\"],\n                {\"type\": \"archived_representation\"}\n            )\n\n    def process(self):\n        try:\n            self._status.info(\"Process started\")\n            self.fill_source_variables()\n            self._status.info(\"Source entities were found\")\n            self.fill_destination_project()\n            self._status.info(\"Destination project was found\")\n            self.fill_or_create_destination_asset()\n            self._status.info(\"Destination asset was determined\")\n            self.determine_family()\n            self.determine_publish_template_name()\n            self.determine_subset_name()\n            self.make_sure_subset_exists()\n            self.make_sure_version_exists()\n            self._status.info(\"Prerequirements were prepared\")\n            self.integrate_representations()\n            self._status.info(\"Integration finished\")\n\n        except PushToProjectError as exc:\n            if not self._status.failed:\n                self._status.set_failed(str(exc))\n\n        except Exception as exc:\n            _exc, _value, _tb = sys.exc_info()\n            self._status.set_failed(\n                \"Unhandled error happened: {}\".format(str(exc)),\n                (_exc, _value, _tb)\n            )\n\n        finally:\n            self._status.set_finished()\n"
  },
  {
    "path": "openpype/tools/push_to_project/window.py",
    "content": "import collections\n\nfrom qtpy import QtWidgets, QtGui, QtCore\n\nfrom openpype.style import load_stylesheet, get_app_icon_path\nfrom openpype.tools.utils import (\n    PlaceholderLineEdit,\n    SeparatorWidget,\n    get_asset_icon_by_name,\n    set_style_property,\n)\nfrom openpype.tools.utils.views import DeselectableTreeView\n\nfrom .control_context import PushToContextController\n\nPROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1\nASSET_NAME_ROLE = QtCore.Qt.UserRole + 2\nASSET_ID_ROLE = QtCore.Qt.UserRole + 3\nTASK_NAME_ROLE = QtCore.Qt.UserRole + 4\nTASK_TYPE_ROLE = QtCore.Qt.UserRole + 5\n\n\nclass ProjectsModel(QtGui.QStandardItemModel):\n    empty_text = \"< Empty >\"\n    refreshing_text = \"< Refreshing >\"\n    select_project_text = \"< Select Project >\"\n\n    refreshed = QtCore.Signal()\n\n    def __init__(self, controller):\n        super(ProjectsModel, self).__init__()\n        self._controller = controller\n\n        self.event_system.add_callback(\n            \"projects.refresh.finished\", self._on_refresh_finish\n        )\n\n        placeholder_item = QtGui.QStandardItem(self.empty_text)\n\n        root_item = self.invisibleRootItem()\n        root_item.appendRows([placeholder_item])\n        items = {None: placeholder_item}\n\n        self._placeholder_item = placeholder_item\n        self._items = items\n\n    @property\n    def event_system(self):\n        return self._controller.event_system\n\n    def _on_refresh_finish(self):\n        root_item = self.invisibleRootItem()\n        project_names = self._controller.model.get_projects()\n\n        if not project_names:\n            placeholder_text = self.empty_text\n        else:\n            placeholder_text = self.select_project_text\n        self._placeholder_item.setData(placeholder_text, QtCore.Qt.DisplayRole)\n\n        new_items = []\n        if None not in self._items:\n            new_items.append(self._placeholder_item)\n\n        current_project_names = set(self._items.keys())\n        for project_name in current_project_names - set(project_names):\n            if project_name is None:\n                continue\n            item = self._items.pop(project_name)\n            root_item.takeRow(item.row())\n\n        for project_name in project_names:\n            if project_name in self._items:\n                continue\n            item = QtGui.QStandardItem(project_name)\n            item.setData(project_name, PROJECT_NAME_ROLE)\n            new_items.append(item)\n\n        if new_items:\n            root_item.appendRows(new_items)\n        self.refreshed.emit()\n\n\nclass ProjectProxyModel(QtCore.QSortFilterProxyModel):\n    def __init__(self):\n        super(ProjectProxyModel, self).__init__()\n        self._filter_empty_projects = False\n\n    def set_filter_empty_project(self, filter_empty_projects):\n        if filter_empty_projects == self._filter_empty_projects:\n            return\n        self._filter_empty_projects = filter_empty_projects\n        self.invalidate()\n\n    def filterAcceptsRow(self, row, parent):\n        if not self._filter_empty_projects:\n            return True\n        model = self.sourceModel()\n        source_index = model.index(row, self.filterKeyColumn(), parent)\n        if model.data(source_index, PROJECT_NAME_ROLE) is None:\n            return False\n        return True\n\n\nclass AssetsModel(QtGui.QStandardItemModel):\n    items_changed = QtCore.Signal()\n    empty_text = \"< Empty >\"\n\n    def __init__(self, controller):\n        super(AssetsModel, self).__init__()\n        self._controller = controller\n\n        placeholder_item = QtGui.QStandardItem(self.empty_text)\n        placeholder_item.setFlags(QtCore.Qt.ItemIsEnabled)\n\n        root_item = self.invisibleRootItem()\n        root_item.appendRows([placeholder_item])\n\n        self.event_system.add_callback(\n            \"project.changed\", self._on_project_change\n        )\n        self.event_system.add_callback(\n            \"assets.refresh.started\", self._on_refresh_start\n        )\n        self.event_system.add_callback(\n            \"assets.refresh.finished\", self._on_refresh_finish\n        )\n\n        self._items = {None: placeholder_item}\n\n        self._placeholder_item = placeholder_item\n        self._last_project = None\n\n    @property\n    def event_system(self):\n        return self._controller.event_system\n\n    def _clear(self):\n        placeholder_in = False\n        root_item = self.invisibleRootItem()\n        for row in reversed(range(root_item.rowCount())):\n            item = root_item.child(row)\n            asset_id = item.data(ASSET_ID_ROLE)\n            if asset_id is None:\n                placeholder_in = True\n                continue\n            root_item.removeRow(item.row())\n\n        for key in tuple(self._items.keys()):\n            if key is not None:\n                self._items.pop(key)\n\n        if not placeholder_in:\n            root_item.appendRows([self._placeholder_item])\n        self._items[None] = self._placeholder_item\n\n    def _on_project_change(self, event):\n        project_name = event[\"project_name\"]\n        if project_name == self._last_project:\n            return\n\n        self._last_project = project_name\n        self._clear()\n        self.items_changed.emit()\n\n    def _on_refresh_start(self, event):\n        pass\n\n    def _on_refresh_finish(self, event):\n        event_project_name = event[\"project_name\"]\n        project_name = self._controller.selection_model.project_name\n        if event_project_name != project_name:\n            return\n\n        self._last_project = event[\"project_name\"]\n        if project_name is None:\n            if None not in self._items:\n                self._clear()\n                self.items_changed.emit()\n            return\n\n        asset_items_by_id = self._controller.model.get_assets(project_name)\n        if not asset_items_by_id:\n            self._clear()\n            self.items_changed.emit()\n            return\n\n        assets_by_parent_id = collections.defaultdict(list)\n        for asset_item in asset_items_by_id.values():\n            assets_by_parent_id[asset_item.parent_id].append(asset_item)\n\n        root_item = self.invisibleRootItem()\n        if None in self._items:\n            self._items.pop(None)\n            root_item.takeRow(self._placeholder_item.row())\n\n        items_to_remove = set(self._items) - set(asset_items_by_id.keys())\n        hierarchy_queue = collections.deque()\n        hierarchy_queue.append((None, root_item))\n        while hierarchy_queue:\n            parent_id, parent_item = hierarchy_queue.popleft()\n            new_items = []\n            for asset_item in assets_by_parent_id[parent_id]:\n                item = self._items.get(asset_item.id)\n                if item is None:\n                    item = QtGui.QStandardItem()\n                    item.setFlags(\n                        QtCore.Qt.ItemIsSelectable\n                        | QtCore.Qt.ItemIsEnabled\n                    )\n                    new_items.append(item)\n                    self._items[asset_item.id] = item\n\n                elif item.parent() is not parent_item:\n                    new_items.append(item)\n\n                icon = get_asset_icon_by_name(\n                    asset_item.icon_name, asset_item.icon_color\n                )\n                item.setData(asset_item.name, QtCore.Qt.DisplayRole)\n                item.setData(icon, QtCore.Qt.DecorationRole)\n                item.setData(asset_item.id, ASSET_ID_ROLE)\n\n                hierarchy_queue.append((asset_item.id, item))\n\n            if new_items:\n                parent_item.appendRows(new_items)\n\n        for item_id in items_to_remove:\n            item = self._items.pop(item_id, None)\n            if item is None:\n                continue\n            row = item.row()\n            if row < 0:\n                continue\n            parent = item.parent()\n            if parent is None:\n                parent = root_item\n            parent.takeRow(row)\n\n        self.items_changed.emit()\n\n\nclass TasksModel(QtGui.QStandardItemModel):\n    items_changed = QtCore.Signal()\n    empty_text = \"< Empty >\"\n\n    def __init__(self, controller):\n        super(TasksModel, self).__init__()\n        self._controller = controller\n\n        placeholder_item = QtGui.QStandardItem(self.empty_text)\n        placeholder_item.setFlags(QtCore.Qt.ItemIsEnabled)\n\n        root_item = self.invisibleRootItem()\n        root_item.appendRows([placeholder_item])\n\n        self.event_system.add_callback(\n            \"project.changed\", self._on_project_change\n        )\n        self.event_system.add_callback(\n            \"assets.refresh.finished\", self._on_asset_refresh_finish\n        )\n        self.event_system.add_callback(\n            \"asset.changed\", self._on_asset_change\n        )\n\n        self._items = {None: placeholder_item}\n\n        self._placeholder_item = placeholder_item\n        self._last_project = None\n\n    @property\n    def event_system(self):\n        return self._controller.event_system\n\n    def _clear(self):\n        placeholder_in = False\n        root_item = self.invisibleRootItem()\n        for row in reversed(range(root_item.rowCount())):\n            item = root_item.child(row)\n            task_name = item.data(TASK_NAME_ROLE)\n            if task_name is None:\n                placeholder_in = True\n                continue\n            root_item.removeRow(item.row())\n\n        for key in tuple(self._items.keys()):\n            if key is not None:\n                self._items.pop(key)\n\n        if not placeholder_in:\n            root_item.appendRows([self._placeholder_item])\n        self._items[None] = self._placeholder_item\n\n    def _on_project_change(self, event):\n        project_name = event[\"project_name\"]\n        if project_name == self._last_project:\n            return\n\n        self._last_project = project_name\n        self._clear()\n        self.items_changed.emit()\n\n    def _on_asset_refresh_finish(self, event):\n        self._refresh(event[\"project_name\"])\n\n    def _on_asset_change(self, event):\n        self._refresh(event[\"project_name\"])\n\n    def _refresh(self, new_project_name):\n        project_name = self._controller.selection_model.project_name\n        if new_project_name != project_name:\n            return\n\n        self._last_project = project_name\n        if project_name is None:\n            if None not in self._items:\n                self._clear()\n                self.items_changed.emit()\n            return\n\n        asset_id = self._controller.selection_model.asset_id\n        task_items = self._controller.model.get_tasks(\n            project_name, asset_id\n        )\n        if not task_items:\n            self._clear()\n            self.items_changed.emit()\n            return\n\n        root_item = self.invisibleRootItem()\n        if None in self._items:\n            self._items.pop(None)\n            root_item.takeRow(self._placeholder_item.row())\n\n        new_items = []\n        task_names = set()\n        for task_item in task_items:\n            task_name = task_item.name\n            item = self._items.get(task_name)\n            if item is None:\n                item = QtGui.QStandardItem()\n                item.setFlags(\n                    QtCore.Qt.ItemIsSelectable\n                    | QtCore.Qt.ItemIsEnabled\n                )\n                new_items.append(item)\n                self._items[task_name] = item\n\n            item.setData(task_name, QtCore.Qt.DisplayRole)\n            item.setData(task_name, TASK_NAME_ROLE)\n            item.setData(task_item.task_type, TASK_TYPE_ROLE)\n\n        if new_items:\n            root_item.appendRows(new_items)\n\n        items_to_remove = set(self._items) - task_names\n        for item_id in items_to_remove:\n            item = self._items.pop(item_id, None)\n            if item is None:\n                continue\n            parent = item.parent()\n            if parent is not None:\n                parent.removeRow(item.row())\n\n        self.items_changed.emit()\n\n\nclass PushToContextSelectWindow(QtWidgets.QDialog):\n    def __init__(\n        self, controller=None, library_filter=True, context_only=False\n    ):\n        super(PushToContextSelectWindow, self).__init__()\n        if controller is None:\n            controller = PushToContextController(library_filter=library_filter)\n        self._controller = controller\n        self.context_only = context_only\n        self.context = None\n\n        self.setWindowTitle(\"Push to project (select context)\")\n        self.setWindowIcon(QtGui.QIcon(get_app_icon_path()))\n\n        main_context_widget = QtWidgets.QWidget(self)\n\n        header_widget = QtWidgets.QWidget(main_context_widget)\n\n        header_label = QtWidgets.QLabel(controller.src_label, header_widget)\n\n        header_layout = QtWidgets.QHBoxLayout(header_widget)\n        header_layout.setContentsMargins(0, 0, 0, 0)\n        header_layout.addWidget(header_label)\n\n        main_splitter = QtWidgets.QSplitter(\n            QtCore.Qt.Horizontal, main_context_widget\n        )\n\n        context_widget = QtWidgets.QWidget(main_splitter)\n\n        project_combobox = QtWidgets.QComboBox(context_widget)\n        project_model = ProjectsModel(controller)\n        project_proxy = ProjectProxyModel()\n        project_proxy.setSourceModel(project_model)\n        project_proxy.setDynamicSortFilter(True)\n        project_delegate = QtWidgets.QStyledItemDelegate()\n        project_combobox.setItemDelegate(project_delegate)\n        project_combobox.setModel(project_proxy)\n\n        asset_task_splitter = QtWidgets.QSplitter(\n            QtCore.Qt.Vertical, context_widget\n        )\n\n        asset_view = DeselectableTreeView(asset_task_splitter)\n        asset_view.setHeaderHidden(True)\n        asset_model = AssetsModel(controller)\n        asset_proxy = QtCore.QSortFilterProxyModel()\n        asset_proxy.setSourceModel(asset_model)\n        asset_proxy.setDynamicSortFilter(True)\n        asset_view.setModel(asset_proxy)\n\n        task_view = QtWidgets.QListView(asset_task_splitter)\n        task_proxy = QtCore.QSortFilterProxyModel()\n        task_model = TasksModel(controller)\n        task_proxy.setSourceModel(task_model)\n        task_proxy.setDynamicSortFilter(True)\n        task_view.setModel(task_proxy)\n\n        asset_task_splitter.addWidget(asset_view)\n        asset_task_splitter.addWidget(task_view)\n\n        context_layout = QtWidgets.QVBoxLayout(context_widget)\n        context_layout.setContentsMargins(0, 0, 0, 0)\n        context_layout.addWidget(project_combobox, 0)\n        context_layout.addWidget(asset_task_splitter, 1)\n\n        # --- Inputs widget ---\n        inputs_widget = QtWidgets.QWidget(main_splitter)\n\n        asset_name_input = PlaceholderLineEdit(inputs_widget)\n        asset_name_input.setPlaceholderText(\"< Name of new asset >\")\n        asset_name_input.setObjectName(\"ValidatedLineEdit\")\n\n        variant_input = PlaceholderLineEdit(inputs_widget)\n        variant_input.setPlaceholderText(\"< Variant >\")\n        variant_input.setObjectName(\"ValidatedLineEdit\")\n\n        comment_input = PlaceholderLineEdit(inputs_widget)\n        comment_input.setPlaceholderText(\"< Publish comment >\")\n\n        inputs_layout = QtWidgets.QFormLayout(inputs_widget)\n        inputs_layout.setContentsMargins(0, 0, 0, 0)\n        inputs_layout.addRow(\"New asset name\", asset_name_input)\n        inputs_layout.addRow(\"Variant\", variant_input)\n        inputs_layout.addRow(\"Comment\", comment_input)\n\n        main_splitter.addWidget(context_widget)\n        main_splitter.addWidget(inputs_widget)\n\n        # --- Buttons widget ---\n        btns_widget = QtWidgets.QWidget(self)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", btns_widget)\n        push_btn = QtWidgets.QPushButton(\"Push\", btns_widget)\n\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(cancel_btn, 0)\n        btns_layout.addWidget(push_btn, 0)\n\n        sep_1 = SeparatorWidget(parent=main_context_widget)\n        sep_2 = SeparatorWidget(parent=main_context_widget)\n        main_context_layout = QtWidgets.QVBoxLayout(main_context_widget)\n        main_context_layout.addWidget(header_widget, 0)\n        main_context_layout.addWidget(sep_1, 0)\n        main_context_layout.addWidget(main_splitter, 1)\n        main_context_layout.addWidget(sep_2, 0)\n        main_context_layout.addWidget(btns_widget, 0)\n\n        # NOTE This was added in hurry\n        # - should be reorganized and changed styles\n        overlay_widget = QtWidgets.QFrame(self)\n        overlay_widget.setObjectName(\"OverlayFrame\")\n\n        overlay_label = QtWidgets.QLabel(overlay_widget)\n        overlay_label.setAlignment(QtCore.Qt.AlignCenter)\n\n        overlay_btns_widget = QtWidgets.QWidget(overlay_widget)\n        overlay_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        # Add try again button (requires changes in controller)\n        overlay_try_btn = QtWidgets.QPushButton(\n            \"Try again\", overlay_btns_widget\n        )\n        overlay_close_btn = QtWidgets.QPushButton(\n            \"Close\", overlay_btns_widget\n        )\n\n        overlay_btns_layout = QtWidgets.QHBoxLayout(overlay_btns_widget)\n        overlay_btns_layout.addStretch(1)\n        overlay_btns_layout.addWidget(overlay_try_btn, 0)\n        overlay_btns_layout.addWidget(overlay_close_btn, 0)\n        overlay_btns_layout.addStretch(1)\n\n        overlay_layout = QtWidgets.QVBoxLayout(overlay_widget)\n        overlay_layout.addWidget(overlay_label, 0)\n        overlay_layout.addWidget(overlay_btns_widget, 0)\n        overlay_layout.setAlignment(QtCore.Qt.AlignCenter)\n\n        main_layout = QtWidgets.QStackedLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(main_context_widget)\n        main_layout.addWidget(overlay_widget)\n        main_layout.setStackingMode(QtWidgets.QStackedLayout.StackAll)\n        main_layout.setCurrentWidget(main_context_widget)\n\n        show_timer = QtCore.QTimer()\n        show_timer.setInterval(1)\n\n        main_thread_timer = QtCore.QTimer()\n        main_thread_timer.setInterval(10)\n\n        user_input_changed_timer = QtCore.QTimer()\n        user_input_changed_timer.setInterval(200)\n        user_input_changed_timer.setSingleShot(True)\n\n        main_thread_timer.timeout.connect(self._on_main_thread_timer)\n        show_timer.timeout.connect(self._on_show_timer)\n        user_input_changed_timer.timeout.connect(self._on_user_input_timer)\n        asset_name_input.textChanged.connect(self._on_new_asset_change)\n        variant_input.textChanged.connect(self._on_variant_change)\n        comment_input.textChanged.connect(self._on_comment_change)\n        project_model.refreshed.connect(self._on_projects_refresh)\n        project_combobox.currentIndexChanged.connect(self._on_project_change)\n        asset_view.selectionModel().selectionChanged.connect(\n            self._on_asset_change\n        )\n        asset_model.items_changed.connect(self._on_asset_model_change)\n        task_view.selectionModel().selectionChanged.connect(\n            self._on_task_change\n        )\n        task_model.items_changed.connect(self._on_task_model_change)\n        push_btn.clicked.connect(self._on_select_click)\n        cancel_btn.clicked.connect(self._on_close_click)\n        overlay_close_btn.clicked.connect(self._on_close_click)\n        overlay_try_btn.clicked.connect(self._on_try_again_click)\n\n        controller.event_system.add_callback(\n            \"new_asset_name.changed\", self._on_controller_new_asset_change\n        )\n        controller.event_system.add_callback(\n            \"variant.changed\", self._on_controller_variant_change\n        )\n        controller.event_system.add_callback(\n            \"comment.changed\", self._on_controller_comment_change\n        )\n        controller.event_system.add_callback(\n            \"submission.enabled.changed\", self._on_submission_change\n        )\n        controller.event_system.add_callback(\n            \"source.changed\", self._on_controller_source_change\n        )\n        controller.event_system.add_callback(\n            \"submit.started\", self._on_controller_submit_start\n        )\n        controller.event_system.add_callback(\n            \"submit.finished\", self._on_controller_submit_end\n        )\n        controller.event_system.add_callback(\n            \"push.message.added\", self._on_push_message\n        )\n\n        self._main_layout = main_layout\n\n        self._main_context_widget = main_context_widget\n\n        self._header_label = header_label\n        self._main_splitter = main_splitter\n\n        self._project_combobox = project_combobox\n        self._project_model = project_model\n        self._project_proxy = project_proxy\n        self._project_delegate = project_delegate\n\n        self._asset_view = asset_view\n        self._asset_model = asset_model\n        self._asset_proxy_model = asset_proxy\n\n        self._task_view = task_view\n        self._task_proxy_model = task_proxy\n\n        self._variant_input = variant_input\n        self._asset_name_input = asset_name_input\n        self._comment_input = comment_input\n\n        self._push_btn = push_btn\n\n        self._overlay_widget = overlay_widget\n        self._overlay_close_btn = overlay_close_btn\n        self._overlay_try_btn = overlay_try_btn\n        self._overlay_label = overlay_label\n\n        self._user_input_changed_timer = user_input_changed_timer\n        # Store current value on input text change\n        #   The value is unset when is passed to controller\n        # The goal is to have controll over changes happened during user change\n        #   in UI and controller auto-changes\n        self._variant_input_text = None\n        self._new_asset_name_input_text = None\n        self._comment_input_text = None\n        self._show_timer = show_timer\n        self._show_counter = 2\n        self._first_show = True\n\n        self._main_thread_timer = main_thread_timer\n        self._main_thread_timer_can_stop = True\n        self._last_submit_message = None\n        self._process_item = None\n\n        push_btn.setEnabled(False)\n        overlay_close_btn.setVisible(False)\n        overlay_try_btn.setVisible(False)\n\n        if controller.user_values.new_asset_name:\n            asset_name_input.setText(controller.user_values.new_asset_name)\n        if controller.user_values.variant:\n            variant_input.setText(controller.user_values.variant)\n        self._invalidate_variant()\n        self._invalidate_new_asset_name()\n\n    @property\n    def controller(self):\n        return self._controller\n\n    def showEvent(self, event):\n        super(PushToContextSelectWindow, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self.setStyleSheet(load_stylesheet())\n            self._invalidate_variant()\n            self._show_timer.start()\n\n    def _on_show_timer(self):\n        if self._show_counter == 0:\n            self._show_timer.stop()\n            return\n\n        self._show_counter -= 1\n        if self._show_counter == 1:\n            width = 740\n            height = 640\n            inputs_width = 360\n            self.resize(width, height)\n            self._main_splitter.setSizes([width - inputs_width, inputs_width])\n\n        if self._show_counter > 0:\n            return\n\n        self._controller.model.refresh_projects()\n\n    def _on_new_asset_change(self, text):\n        self._new_asset_name_input_text = text\n        self._user_input_changed_timer.start()\n\n    def _on_variant_change(self, text):\n        self._variant_input_text = text\n        self._user_input_changed_timer.start()\n\n    def _on_comment_change(self, text):\n        self._comment_input_text = text\n        self._user_input_changed_timer.start()\n\n    def _on_user_input_timer(self):\n        asset_name = self._new_asset_name_input_text\n        if asset_name is not None:\n            self._new_asset_name_input_text = None\n            self._controller.user_values.set_new_asset(asset_name)\n\n        variant = self._variant_input_text\n        if variant is not None:\n            self._variant_input_text = None\n            self._controller.user_values.set_variant(variant)\n\n        comment = self._comment_input_text\n        if comment is not None:\n            self._comment_input_text = None\n            self._controller.user_values.set_comment(comment)\n\n    def _on_controller_new_asset_change(self, event):\n        asset_name = event[\"changes\"][\"new_asset_name\"][\"new\"]\n        if (\n            self._new_asset_name_input_text is None\n            and asset_name != self._asset_name_input.text()\n        ):\n            self._asset_name_input.setText(asset_name)\n\n        self._invalidate_new_asset_name()\n\n    def _on_controller_variant_change(self, event):\n        is_valid_changes = event[\"changes\"][\"is_valid\"]\n        variant = event[\"changes\"][\"variant\"][\"new\"]\n        if (\n            self._variant_input_text is None\n            and variant != self._variant_input.text()\n        ):\n            self._variant_input.setText(variant)\n\n        if is_valid_changes[\"old\"] != is_valid_changes[\"new\"]:\n            self._invalidate_variant()\n\n    def _on_controller_comment_change(self, event):\n        comment = event[\"comment\"]\n        if (\n            self._comment_input_text is None\n            and comment != self._comment_input.text()\n        ):\n            self._comment_input.setText(comment)\n\n    def _on_controller_source_change(self):\n        self._header_label.setText(self._controller.src_label)\n\n    def _invalidate_new_asset_name(self):\n        asset_name = self._controller.user_values.new_asset_name\n        self._task_view.setVisible(not asset_name)\n\n        valid = None\n        if asset_name:\n            valid = self._controller.user_values.is_new_asset_name_valid\n\n        state = \"\"\n        if valid is True:\n            state = \"valid\"\n        elif valid is False:\n            state = \"invalid\"\n        set_style_property(self._asset_name_input, \"state\", state)\n\n    def _invalidate_variant(self):\n        valid = self._controller.user_values.is_variant_valid\n        state = \"invalid\"\n        if valid is True:\n            state = \"valid\"\n        set_style_property(self._variant_input, \"state\", state)\n\n    def _on_projects_refresh(self):\n        self._project_proxy.sort(0, QtCore.Qt.AscendingOrder)\n\n    def _on_project_change(self):\n        idx = self._project_combobox.currentIndex()\n        if idx < 0:\n            self._project_proxy.set_filter_empty_project(False)\n            return\n\n        project_name = self._project_combobox.itemData(idx, PROJECT_NAME_ROLE)\n        self._project_proxy.set_filter_empty_project(project_name is not None)\n        self._controller.selection_model.select_project(project_name)\n\n    def _on_asset_change(self):\n        indexes = self._asset_view.selectedIndexes()\n        index = next(iter(indexes), None)\n        asset_id = None\n        if index is not None:\n            model = self._asset_view.model()\n            asset_id = model.data(index, ASSET_ID_ROLE)\n        self._controller.selection_model.select_asset(asset_id)\n\n    def _on_asset_model_change(self):\n        self._asset_proxy_model.sort(0, QtCore.Qt.AscendingOrder)\n\n    def _on_task_model_change(self):\n        self._task_proxy_model.sort(0, QtCore.Qt.AscendingOrder)\n\n    def _on_task_change(self):\n        indexes = self._task_view.selectedIndexes()\n        index = next(iter(indexes), None)\n        task_name = None\n        if index is not None:\n            model = self._task_view.model()\n            task_name = model.data(index, TASK_NAME_ROLE)\n        self._controller.selection_model.select_task(task_name)\n\n    def _on_submission_change(self, event):\n        self._push_btn.setEnabled(event[\"enabled\"])\n\n    def _on_close_click(self):\n        self.close()\n\n    def _on_select_click(self):\n        result = self._controller.submit(\n            wait=True, context_only=self.context_only\n        )\n\n        if self.context_only:\n            self.context = {\n                \"project_name\": self._controller.selection_model.project_name,\n                \"asset_id\": self._controller.selection_model.asset_id,\n                \"task_name\": self._controller.selection_model.task_name,\n                \"variant\": self._controller.user_values.variant,\n                \"comment\": self._controller.user_values.comment,\n                \"asset_name\": self._controller.user_values.new_asset_name\n            }\n            self.close()\n\n        self._process_item = result\n\n    def _on_try_again_click(self):\n        self._process_item = None\n        self._last_submit_message = None\n\n        self._overlay_close_btn.setVisible(False)\n        self._overlay_try_btn.setVisible(False)\n        self._main_layout.setCurrentWidget(self._main_context_widget)\n\n    def _on_main_thread_timer(self):\n        if self._last_submit_message:\n            self._overlay_label.setText(self._last_submit_message)\n            self._last_submit_message = None\n\n        process_status = self._process_item.status\n        push_failed = process_status.failed\n        fail_traceback = process_status.traceback\n        if self._main_thread_timer_can_stop:\n            self._main_thread_timer.stop()\n            self._overlay_close_btn.setVisible(True)\n            if push_failed and not fail_traceback:\n                self._overlay_try_btn.setVisible(True)\n\n        if push_failed:\n            message = \"Push Failed:\\n{}\".format(process_status.fail_reason)\n            if fail_traceback:\n                message += \"\\n{}\".format(fail_traceback)\n            self._overlay_label.setText(message)\n            set_style_property(self._overlay_close_btn, \"state\", \"error\")\n\n        if self._main_thread_timer_can_stop:\n            # Join thread in controller\n            self._controller.wait_for_process_thread()\n            # Reset process item to None\n            self._process_item = None\n\n    def _on_controller_submit_start(self):\n        self._main_thread_timer_can_stop = False\n        self._main_thread_timer.start()\n        self._main_layout.setCurrentWidget(self._overlay_widget)\n        self._overlay_label.setText(\"Submittion started\")\n\n    def _on_controller_submit_end(self):\n        self._main_thread_timer_can_stop = True\n\n    def _on_push_message(self, event):\n        self._last_submit_message = event[\"message\"]\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/__init__.py",
    "content": "from .version import version, version_info, __version__\n\n# This must be run prior to importing the application, due to the\n# application requiring a discovered copy of Qt bindings.\n\nfrom .app import show\n\n__all__ = [\n    'show',\n    'version',\n    'version_info',\n    '__version__'\n]\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/__main__.py",
    "content": "from .app import show\n\n\nif __name__ == '__main__':\n    import argparse\n\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--debug\", action=\"store_true\")\n\n    args = parser.parse_args()\n\n    if args.debug:\n        from . import mock\n        import pyblish.api\n\n        for Plugin in mock.plugins:\n            pyblish.api.register_plugin(Plugin)\n\n    show()\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/app.css",
    "content": "/* Global CSS */\n\n* {\n\toutline: none;\n\tcolor: #ddd;\n  \tfont-family: \"Open Sans\";\n\tfont-style: normal;\n}\n\n/* General CSS */\n\nQWidget {\n\tbackground: #555;\n\tbackground-position: center center;\n\tbackground-repeat: no-repeat;\n\tfont-size: 12px;\n}\n\nQMenu {\n    background-color: #555; /* sets background of the menu */\n    border: 1px solid #222;\n}\n\nQMenu::item {\n    /* sets background of menu item. set this to something non-transparent\n        if you want menu color and menu item color to be different */\n    background-color: transparent;\n    padding: 5px;\n    padding-left: 30px;\n}\n\nQMenu::item:selected { /* when user selects item using mouse or keyboard */\n    background-color: #666;\n}\n\nQDialog {\n\tmin-width: 300;\n\tbackground: \"#555\";\n}\n\nQListView {\n\tborder: 0px;\n\tbackground: \"transparent\"\n}\n\nQTreeView {\n\tborder: 0px;\n\tbackground: \"transparent\"\n}\n\nQPushButton {\n\twidth: 27px;\n\theight: 27px;\n\tbackground: #555;\n\tborder: 1px solid #aaa;\n\tborder-radius: 4px;\n\tfont-family: \"FontAwesome\";\n\tfont-size: 11pt;\n\tcolor: white;\n\tpadding: 0px;\n}\n\nQPushButton:pressed {\n\tbackground: \"#777\";\n}\n\nQPushButton:hover {\n\tcolor: white;\n\tbackground: \"#666\";\n}\n\nQPushButton:disabled {\n\tcolor: rgba(255, 255, 255, 50);\n}\n\nQTextEdit, QLineEdit {\n\tbackground: #555;\n\tborder: 1px solid #333;\n\tfont-size: 9pt;\n\tcolor: #fff;\n}\n\nQCheckBox {\n\tmin-width: 17px;\n\tmax-width: 17px;\n\tborder: 1px solid #222;\n\tbackground: transparent;\n}\n\nQCheckBox::indicator {\n\twidth: 15px;\n\theight: 15px;\n\t/*background: #444;*/\n\tbackground: transparent;\n\tborder: 1px solid #555;\n}\n\nQCheckBox::indicator:checked {\n\tbackground: #222;\n}\n\nQComboBox {\n\tbackground: #444;\n\tcolor: #EEE;\n\tfont-size: 8pt;\n\tborder: 1px solid #333;\n\tpadding: 0px;\n}\n\nQComboBox[combolist=\"true\"]::drop-down {\n\tbackground: transparent;\n}\n\nQComboBox[combolist=\"true\"]::down-arrow {\n\tmax-width: 0px;\n\twidth: 1px;\n}\n\nQComboBox[combolist=\"true\"] QAbstractItemView {\n\tbackground: #555;\n}\n\nQScrollBar:vertical {\n \tborder: none;\n \tbackground: transparent;\n \twidth: 6px;\n \tmargin: 0;\n}\n\nQScrollBar::handle:vertical {\n \tbackground: #333;\n \tborder-radius: 3px;\n \tmin-height: 20px;\n}\n\nQScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {\n \theight: 0px;\n}\n\nQScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {\n \tborder: 1px solid #444;\n \twidth: 3px;\n \theight: 3px;\n \tbackground: white;\n}\n\nQScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {\n \tbackground: none;\n}\n\nQToolTip {\n\tcolor: #eee;\n\tbackground-color: #555;\n\tborder: none;\n\tpadding: 5px;\n}\n\nQLabel {\n\tborder-radius: 0px;\n}\n\nQToolButton {\n\tbackground-color: transparent;\n\tmargin: 0px;\n\tpadding: 0px;\n\tborder-radius: 0px;\n\tborder: none;\n}\n\n/* Specific CSS */\n#PerspectiveToggleBtn {\n\tborder-bottom: 3px solid lightblue;\n\tborder-top: 0px;\n\tborder-radius: 0px;\n\tborder-right: 1px solid #232323;\n\tborder-left: 0px;\n\tfont-size: 26pt;\n\tfont-family: \"FontAwesome\";\n}\n\n#Terminal QComboBox::drop-down {\n\twidth: 60px;\n}\n\n#Header {\n\tbackground: #555;\n\tborder: 1px solid #444;\n\tpadding: 0px;\n\tmargin: 0px;\n}\n\n#Header QRadioButton {\n\tborder: 3px solid \"transparent\";\n\tborder-right: 1px solid #333;\n\tleft: 2px;\n}\n\n#Header QRadioButton::indicator {\n\twidth: 65px;\n\theight: 40px;\n\tbackground-repeat: no-repeat;\n\tbackground-position: center center;\n\timage: none;\n}\n\n#Header QRadioButton:hover {\n\tbackground-color: rgba(255, 255, 255, 10);\n}\n\n#Header QRadioButton:checked {\n\tbackground-color: rgba(255, 255, 255, 20);\n\tborder-bottom: 3px solid \"lightblue\";\n}\n\n#Body {\n\tpadding: 0px;\n\tborder: 1px solid #333;\n\tbackground: #444;\n}\n\n#Body QWidget {\n\tbackground: #444;\n}\n\n#Header #TerminalTab {\n\tbackground-image: url(\"img/tab-terminal.png\");\n}\n\n#Header #OverviewTab {\n\tbackground-image: url(\"img/tab-overview.png\");\n}\n\n#ButtonWithMenu {\n\tbackground: #555;\n\tborder: 1px solid #fff;\n\tborder-radius: 4px;\n\tfont-family: \"FontAwesome\";\n\tfont-size: 11pt;\n\tcolor: white;\n}\n\n#ButtonWithMenu:pressed {\n\tbackground: #777;\n}\n\n#ButtonWithMenu:hover {\n\tcolor: white;\n\tbackground: #666;\n}\n#ButtonWithMenu:disabled {\n\tbackground: #666;\n    color: #999;\n\tborder: 1px solid #999;\n}\n\n#FooterSpacer, #FooterInfo, #HeaderSpacer {\n\tbackground: transparent;\n}\n\n#Footer {\n\tbackground: #555;\n\tmin-height: 43px;\n}\n\n#Footer[success=\"1\"] {\n\tbackground: #458056\n}\n\n#Footer[success=\"0\"] {\n\tbackground-color: #AA5050\n}\n\n#Footer QPushButton {\n\tbackground: #555;\n\tborder: 1px solid #aaa;\n\tborder-radius: 4px;\n\tfont-family: \"FontAwesome\";\n\tfont-size: 11pt;\n\tcolor: white;\n\tpadding: 0px;\n}\n\n#Footer QPushButton:pressed:hover {\n\tcolor: #3784c5;\n\tbackground: #444;\n}\n\n#Footer QPushButton:hover {\n\tbackground: #505050;\n\tborder: 2px solid #3784c5;\n}\n\n#Footer QPushButton:disabled {\n\tborder: 1px solid #888;\n\tbackground: #666;\n\tcolor: #999;\n}\n\n#ClosingPlaceholder {\n\tbackground: rgba(0, 0, 0, 50);\n}\n\n#CommentIntentWidget {\n\tbackground: transparent;\n}\n\n#CommentBox, #CommentPlaceholder {\n\tfont-family: \"Open Sans\";\n\tfont-size: 8pt;\n\tpadding: 5px;\n\tbackground: #444;\n}\n\n#CommentBox {\n\tselection-background-color: #222;\n}\n\n#CommentBox:disabled, #CommentPlaceholder:disabled, #IntentBox:disabled {\n\tbackground: #555;\n}\n\n#CommentPlaceholder {\n\tcolor: #888\n}\n\n#IntentBox {\n    background: #444;\n\tfont-size: 8pt;\n\tpadding: 5px;\n    min-width: 75px;\n\tcolor: #EEE;\n}\n\n#IntentBox::drop-down:button {\n\tborder: 0px;\n\tbackground: transparent;\n}\n\n#IntentBox::down-arrow {\n\timage: url(\"/img/down_arrow.png\");\n}\n\n#IntentBox::down-arrow:disabled {\n\timage: url();\n}\n\n#TerminalView {\n\tbackground-color: transparent;\n}\n\n#TerminalView:item {\n\tbackground-color: transparent;\n}\n\n#TerminalView:hover {\n\tbackground-color: transparent;\n}\n\n#TerminalView:selected {\n\tbackground-color: transparent;\n}\n\n#TerminalView:item:hover {\n\tcolor: #ffffff;\n}\n\n#TerminalView:item:selected {\n\tcolor: #eeeeee;\n}\n\n#TerminalView QTextEdit {\n\tpadding:3px;\n\tcolor: #aaa;\n\tborder-radius: 7px;\n\tborder-color: #222;\n\tborder-style: solid;\n\tborder-width: 2px;\n\tbackground-color: #333;\n}\n\n#TerminalView QTextEdit:hover {\n\tbackground-color: #353535;\n}\n\n#TerminalView QTextEdit:selected {\n\tbackground-color: #303030;\n}\n\n#ExpandableWidgetContent {\n\tborder: none;\n\tbackground-color: #232323;\n\tcolor:#eeeeee;\n}\n\n#EllidableLabel {\n\tfont-size: 16pt;\n\tfont-weight: normal;\n}\n\n#PerspectiveScrollContent {\n\tborder: 1px solid #333;\n\tborder-radius: 0px;\n}\n\n#PerspectiveWidgetContent{\n\tpadding: 0px;\n}\n\n#PerspectiveLabel {\n\tbackground-color: transparent;\n\tborder: none;\n}\n\n#PerspectiveIndicator {\n\tfont-size: 16pt;\n\tfont-weight: normal;\n\tpadding: 5px;\n\tbackground-color: #ffffff;\n\tcolor: #333333;\n}\n\n#PerspectiveIndicator[state=\"warning\"] {\n\tbackground-color: #ff9900;\n\tcolor: #ffffff;\n}\n\n#PerspectiveIndicator[state=\"active\"] {\n\tbackground-color: #99CEEE;\n\tcolor: #ffffff;\n}\n\n#PerspectiveIndicator[state=\"error\"] {\n\tbackground-color: #cc4a4a;\n\tcolor: #ffffff;\n}\n\n#PerspectiveIndicator[state=\"ok\"] {\n\tbackground-color: #69a567;\n\tcolor: #ffffff;\n}\n\n#ExpandableHeader {\n\tbackground-color: transparent;\n\tmargin: 0px;\n\tpadding: 0px;\n\tborder-radius: 0px;\n\tborder: none;\n}\n\n#ExpandableHeader QWidget {\n\tcolor: #ddd;\n}\n\n#ExpandableHeader QWidget:hover {\n\tcolor: #fff;\n}\n\n#TerminalFilterWidget QPushButton {\n\t/* font: %(font_size_pt)spt; */\n\tfont-family: \"FontAwesome\";\n\ttext-align: center;\n\tbackground-color: transparent;\n\tborder-width: 1px;\n\tborder-color: #777777;\n\tborder-style: none;\n\tpadding: 0px;\n\tborder-radius: 8px;\n}\n#TerminalFilterWidget QPushButton:hover {\n\tbackground: #5f5f5f;\n\tborder-style: none;\n}\n#TerminalFilterWidget QPushButton:pressed {\n\tbackground: #606060;\n\tborder-style: none;\n}\n#TerminalFilterWidget QPushButton:pressed:hover {\n\tbackground: #626262;\n\tborder-style: none;\n}\n\n#TerminalFilerBtn[type=\"info\"]:checked {color: rgb(255, 255, 255);}\n#TerminalFilerBtn[type=\"info\"]:hover:pressed {color: rgba(255, 255, 255, 163);}\n#TerminalFilerBtn[type=\"info\"] {color: rgba(255, 255, 255, 63);}\n\n#TerminalFilerBtn[type=\"error\"]:checked {color: rgb(255, 74, 74);}\n#TerminalFilerBtn[type=\"error\"]:hover:pressed {color: rgba(255, 74, 74, 163);}\n#TerminalFilerBtn[type=\"error\"] {color: rgba(255, 74, 74, 63);}\n\n#TerminalFilerBtn[type=\"log_debug\"]:checked {color: rgb(255, 102, 232);}\n#TerminalFilerBtn[type=\"log_debug\"] {color: rgba(255, 102, 232, 63);}\n#TerminalFilerBtn[type=\"log_debug\"]:hover:pressed {\n\tcolor: rgba(255, 102, 232, 163);\n}\n\n#TerminalFilerBtn[type=\"log_info\"]:checked {color: rgb(102, 171, 255);}\n#TerminalFilerBtn[type=\"log_info\"] {color: rgba(102, 171, 255, 63);}\n#TerminalFilerBtn[type=\"log_info\"]:hover:pressed {\n\tcolor: rgba(102, 171, 255, 163);\n}\n\n#TerminalFilerBtn[type=\"log_warning\"]:checked {color: rgb(255, 186, 102);}\n#TerminalFilerBtn[type=\"log_warning\"] {color: rgba(255, 186, 102, 63);}\n#TerminalFilerBtn[type=\"log_warning\"]:hover:pressed {\n\tcolor: rgba(255, 186, 102, 163);\n}\n\n#TerminalFilerBtn[type=\"log_error\"]:checked {color: rgb(255, 77, 88);}\n#TerminalFilerBtn[type=\"log_error\"] {color: rgba(255, 77, 88, 63);}\n#TerminalFilerBtn[type=\"log_error\"]:hover:pressed {\n\tcolor: rgba(255, 77, 88, 163);\n}\n\n#TerminalFilerBtn[type=\"log_critical\"]:checked {color: rgb(255, 79, 117);}\n#TerminalFilerBtn[type=\"log_critical\"] {color: rgba(255, 79, 117, 63);}\n#TerminalFilerBtn[type=\"log_critical\"]:hover:pressed {\n\tcolor: rgba(255, 79, 117, 163);\n}\n\n#SuspendLogsBtn {\n\tbackground: #444;\n\tborder: none;\n\tborder-top-right-radius: 7px;\n\tborder-bottom-right-radius: 7px;\n\tborder-top-left-radius: 0px;\n\tborder-bottom-left-radius: 0px;\n\tfont-family: \"FontAwesome\";\n\tfont-size: 11pt;\n\tcolor: white;\n\tpadding: 0px;\n}\n\n#SuspendLogsBtn:hover {\n\tbackground: #333;\n}\n\n#SuspendLogsBtn:disabled {\n\tbackground: #4c4c4c;\n}\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/app.py",
    "content": "from __future__ import print_function\n\nimport os\nimport sys\nimport ctypes\nimport platform\nimport contextlib\n\nfrom qtpy import QtCore, QtGui, QtWidgets\n\nfrom . import control, settings, util, window\n\nself = sys.modules[__name__]\n\n# Maintain reference to currently opened window\nself._window = None\n\n\n@contextlib.contextmanager\ndef application():\n    app = QtWidgets.QApplication.instance()\n\n    if not app:\n        print(\"Starting new QApplication..\")\n        app = QtWidgets.QApplication(sys.argv)\n        yield app\n        app.exec_()\n    else:\n        print(\"Using existing QApplication..\")\n        yield app\n        if os.environ.get(\"PYBLISH_GUI_ALWAYS_EXEC\"):\n            app.exec_()\n\n\ndef install_translator(app):\n    translator = QtCore.QTranslator(app)\n    translator.load(QtCore.QLocale.system(), \"i18n/\",\n                    directory=util.root)\n    app.installTranslator(translator)\n    print(\"Installed translator\")\n\n\ndef install_fonts():\n    database = QtGui.QFontDatabase()\n    fonts = [\n        \"opensans/OpenSans-Bold.ttf\",\n        \"opensans/OpenSans-BoldItalic.ttf\",\n        \"opensans/OpenSans-ExtraBold.ttf\",\n        \"opensans/OpenSans-ExtraBoldItalic.ttf\",\n        \"opensans/OpenSans-Italic.ttf\",\n        \"opensans/OpenSans-Light.ttf\",\n        \"opensans/OpenSans-LightItalic.ttf\",\n        \"opensans/OpenSans-Regular.ttf\",\n        \"opensans/OpenSans-Semibold.ttf\",\n        \"opensans/OpenSans-SemiboldItalic.ttf\",\n        \"fontawesome/fontawesome-webfont.ttf\"\n    ]\n\n    for font in fonts:\n        path = util.get_asset(\"font\", font)\n\n        # TODO(marcus): Check if they are already installed first.\n        # In hosts, this will be called each time the GUI is shown,\n        # potentially installing a font each time.\n        if database.addApplicationFont(path) < 0:\n            print(\"Could not install %s\" % path)\n        else:\n            print(\"Installed %s\" % font)\n\n\ndef on_destroyed():\n    \"\"\"Remove internal reference to window on window destroyed\"\"\"\n    self._window = None\n\n\ndef show(parent=None):\n    with open(util.get_asset(\"app.css\")) as f:\n        css = f.read()\n\n        # Make relative paths absolute\n        root = util.get_asset(\"\").replace(\"\\\\\", \"/\")\n        css = css.replace(\"url(\\\"\", \"url(\\\"%s\" % root)\n\n    with application() as app:\n\n        if platform.system().lower() == \"windows\":\n            ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(\n                u\"pyblish_pype\"\n            )\n\n        install_fonts()\n        install_translator(app)\n\n        if self._window is None:\n            ctrl = control.Controller()\n            self._window = window.Window(ctrl, parent)\n            self._window.destroyed.connect(on_destroyed)\n\n        self._window.show()\n        self._window.activateWindow()\n        self._window.setWindowTitle(settings.WindowTitle)\n\n        font = QtGui.QFont(\"Open Sans\", 8, QtGui.QFont.Normal)\n        self._window.setFont(font)\n        self._window.setStyleSheet(css)\n\n        self._window.reset()\n        self._window.resize(*settings.WindowSize)\n\n        return self._window\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/awesome.py",
    "content": "\ntags = {\n    \"500px\": u\"\\uf26e\",\n    \"adjust\": u\"\\uf042\",\n    \"adn\": u\"\\uf170\",\n    \"align-center\": u\"\\uf037\",\n    \"align-justify\": u\"\\uf039\",\n    \"align-left\": u\"\\uf036\",\n    \"align-right\": u\"\\uf038\",\n    \"amazon\": u\"\\uf270\",\n    \"ambulance\": u\"\\uf0f9\",\n    \"american-sign-language-interpreting\": u\"\\uf2a3\",\n    \"anchor\": u\"\\uf13d\",\n    \"android\": u\"\\uf17b\",\n    \"angellist\": u\"\\uf209\",\n    \"angle-double-down\": u\"\\uf103\",\n    \"angle-double-left\": u\"\\uf100\",\n    \"angle-double-right\": u\"\\uf101\",\n    \"angle-double-up\": u\"\\uf102\",\n    \"angle-down\": u\"\\uf107\",\n    \"angle-left\": u\"\\uf104\",\n    \"angle-right\": u\"\\uf105\",\n    \"angle-up\": u\"\\uf106\",\n    \"apple\": u\"\\uf179\",\n    \"archive\": u\"\\uf187\",\n    \"area-chart\": u\"\\uf1fe\",\n    \"arrow-circle-down\": u\"\\uf0ab\",\n    \"arrow-circle-left\": u\"\\uf0a8\",\n    \"arrow-circle-o-down\": u\"\\uf01a\",\n    \"arrow-circle-o-left\": u\"\\uf190\",\n    \"arrow-circle-o-right\": u\"\\uf18e\",\n    \"arrow-circle-o-up\": u\"\\uf01b\",\n    \"arrow-circle-right\": u\"\\uf0a9\",\n    \"arrow-circle-up\": u\"\\uf0aa\",\n    \"arrow-down\": u\"\\uf063\",\n    \"arrow-left\": u\"\\uf060\",\n    \"arrow-right\": u\"\\uf061\",\n    \"arrow-up\": u\"\\uf062\",\n    \"arrows\": u\"\\uf047\",\n    \"arrows-alt\": u\"\\uf0b2\",\n    \"arrows-h\": u\"\\uf07e\",\n    \"arrows-v\": u\"\\uf07d\",\n    \"asl-interpreting (alias)\": u\"\\uf2a3\",\n    \"assistive-listening-systems\": u\"\\uf2a2\",\n    \"asterisk\": u\"\\uf069\",\n    \"at\": u\"\\uf1fa\",\n    \"audio-description\": u\"\\uf29e\",\n    \"automobile (alias)\": u\"\\uf1b9\",\n    \"backward\": u\"\\uf04a\",\n    \"balance-scale\": u\"\\uf24e\",\n    \"ban\": u\"\\uf05e\",\n    \"bank (alias)\": u\"\\uf19c\",\n    \"bar-chart\": u\"\\uf080\",\n    \"bar-chart-o (alias)\": u\"\\uf080\",\n    \"barcode\": u\"\\uf02a\",\n    \"bars\": u\"\\uf0c9\",\n    \"battery-0 (alias)\": u\"\\uf244\",\n    \"battery-1 (alias)\": u\"\\uf243\",\n    \"battery-2 (alias)\": u\"\\uf242\",\n    \"battery-3 (alias)\": u\"\\uf241\",\n    \"battery-4 (alias)\": u\"\\uf240\",\n    \"battery-empty\": u\"\\uf244\",\n    \"battery-full\": u\"\\uf240\",\n    \"battery-half\": u\"\\uf242\",\n    \"battery-quarter\": u\"\\uf243\",\n    \"battery-three-quarters\": u\"\\uf241\",\n    \"bed\": u\"\\uf236\",\n    \"beer\": u\"\\uf0fc\",\n    \"behance\": u\"\\uf1b4\",\n    \"behance-square\": u\"\\uf1b5\",\n    \"bell\": u\"\\uf0f3\",\n    \"bell-o\": u\"\\uf0a2\",\n    \"bell-slash\": u\"\\uf1f6\",\n    \"bell-slash-o\": u\"\\uf1f7\",\n    \"bicycle\": u\"\\uf206\",\n    \"binoculars\": u\"\\uf1e5\",\n    \"birthday-cake\": u\"\\uf1fd\",\n    \"bitbucket\": u\"\\uf171\",\n    \"bitbucket-square\": u\"\\uf172\",\n    \"bitcoin (alias)\": u\"\\uf15a\",\n    \"black-tie\": u\"\\uf27e\",\n    \"blind\": u\"\\uf29d\",\n    \"bluetooth\": u\"\\uf293\",\n    \"bluetooth-b\": u\"\\uf294\",\n    \"bold\": u\"\\uf032\",\n    \"bolt\": u\"\\uf0e7\",\n    \"bomb\": u\"\\uf1e2\",\n    \"book\": u\"\\uf02d\",\n    \"bookmark\": u\"\\uf02e\",\n    \"bookmark-o\": u\"\\uf097\",\n    \"braille\": u\"\\uf2a1\",\n    \"briefcase\": u\"\\uf0b1\",\n    \"btc\": u\"\\uf15a\",\n    \"bug\": u\"\\uf188\",\n    \"building\": u\"\\uf1ad\",\n    \"building-o\": u\"\\uf0f7\",\n    \"bullhorn\": u\"\\uf0a1\",\n    \"bullseye\": u\"\\uf140\",\n    \"bus\": u\"\\uf207\",\n    \"buysellads\": u\"\\uf20d\",\n    \"cab (alias)\": u\"\\uf1ba\",\n    \"calculator\": u\"\\uf1ec\",\n    \"calendar\": u\"\\uf073\",\n    \"calendar-check-o\": u\"\\uf274\",\n    \"calendar-minus-o\": u\"\\uf272\",\n    \"calendar-o\": u\"\\uf133\",\n    \"calendar-plus-o\": u\"\\uf271\",\n    \"calendar-times-o\": u\"\\uf273\",\n    \"camera\": u\"\\uf030\",\n    \"camera-retro\": u\"\\uf083\",\n    \"car\": u\"\\uf1b9\",\n    \"caret-down\": u\"\\uf0d7\",\n    \"caret-left\": u\"\\uf0d9\",\n    \"caret-right\": u\"\\uf0da\",\n    \"caret-square-o-down\": u\"\\uf150\",\n    \"caret-square-o-left\": u\"\\uf191\",\n    \"caret-square-o-right\": u\"\\uf152\",\n    \"caret-square-o-up\": u\"\\uf151\",\n    \"caret-up\": u\"\\uf0d8\",\n    \"cart-arrow-down\": u\"\\uf218\",\n    \"cart-plus\": u\"\\uf217\",\n    \"cc\": u\"\\uf20a\",\n    \"cc-amex\": u\"\\uf1f3\",\n    \"cc-diners-club\": u\"\\uf24c\",\n    \"cc-discover\": u\"\\uf1f2\",\n    \"cc-jcb\": u\"\\uf24b\",\n    \"cc-mastercard\": u\"\\uf1f1\",\n    \"cc-paypal\": u\"\\uf1f4\",\n    \"cc-stripe\": u\"\\uf1f5\",\n    \"cc-visa\": u\"\\uf1f0\",\n    \"certificate\": u\"\\uf0a3\",\n    \"chain (alias)\": u\"\\uf0c1\",\n    \"chain-broken\": u\"\\uf127\",\n    \"check\": u\"\\uf00c\",\n    \"check-circle\": u\"\\uf058\",\n    \"check-circle-o\": u\"\\uf05d\",\n    \"check-square\": u\"\\uf14a\",\n    \"check-square-o\": u\"\\uf046\",\n    \"chevron-circle-down\": u\"\\uf13a\",\n    \"chevron-circle-left\": u\"\\uf137\",\n    \"chevron-circle-right\": u\"\\uf138\",\n    \"chevron-circle-up\": u\"\\uf139\",\n    \"chevron-down\": u\"\\uf078\",\n    \"chevron-left\": u\"\\uf053\",\n    \"chevron-right\": u\"\\uf054\",\n    \"chevron-up\": u\"\\uf077\",\n    \"child\": u\"\\uf1ae\",\n    \"chrome\": u\"\\uf268\",\n    \"circle\": u\"\\uf111\",\n    \"circle-o\": u\"\\uf10c\",\n    \"circle-o-notch\": u\"\\uf1ce\",\n    \"circle-thin\": u\"\\uf1db\",\n    \"clipboard\": u\"\\uf0ea\",\n    \"clock-o\": u\"\\uf017\",\n    \"clone\": u\"\\uf24d\",\n    \"close (alias)\": u\"\\uf00d\",\n    \"cloud\": u\"\\uf0c2\",\n    \"cloud-download\": u\"\\uf0ed\",\n    \"cloud-upload\": u\"\\uf0ee\",\n    \"cny (alias)\": u\"\\uf157\",\n    \"code\": u\"\\uf121\",\n    \"code-fork\": u\"\\uf126\",\n    \"codepen\": u\"\\uf1cb\",\n    \"codiepie\": u\"\\uf284\",\n    \"coffee\": u\"\\uf0f4\",\n    \"cog\": u\"\\uf013\",\n    \"cogs\": u\"\\uf085\",\n    \"columns\": u\"\\uf0db\",\n    \"comment\": u\"\\uf075\",\n    \"comment-o\": u\"\\uf0e5\",\n    \"commenting\": u\"\\uf27a\",\n    \"commenting-o\": u\"\\uf27b\",\n    \"comments\": u\"\\uf086\",\n    \"comments-o\": u\"\\uf0e6\",\n    \"compass\": u\"\\uf14e\",\n    \"compress\": u\"\\uf066\",\n    \"connectdevelop\": u\"\\uf20e\",\n    \"contao\": u\"\\uf26d\",\n    \"copy (alias)\": u\"\\uf0c5\",\n    \"copyright\": u\"\\uf1f9\",\n    \"creative-commons\": u\"\\uf25e\",\n    \"credit-card\": u\"\\uf09d\",\n    \"credit-card-alt\": u\"\\uf283\",\n    \"crop\": u\"\\uf125\",\n    \"crosshairs\": u\"\\uf05b\",\n    \"css3\": u\"\\uf13c\",\n    \"cube\": u\"\\uf1b2\",\n    \"cubes\": u\"\\uf1b3\",\n    \"cut (alias)\": u\"\\uf0c4\",\n    \"cutlery\": u\"\\uf0f5\",\n    \"dashboard (alias)\": u\"\\uf0e4\",\n    \"dashcube\": u\"\\uf210\",\n    \"database\": u\"\\uf1c0\",\n    \"deaf\": u\"\\uf2a4\",\n    \"deafness (alias)\": u\"\\uf2a4\",\n    \"dedent (alias)\": u\"\\uf03b\",\n    \"delicious\": u\"\\uf1a5\",\n    \"desktop\": u\"\\uf108\",\n    \"deviantart\": u\"\\uf1bd\",\n    \"diamond\": u\"\\uf219\",\n    \"digg\": u\"\\uf1a6\",\n    \"dollar (alias)\": u\"\\uf155\",\n    \"dot-circle-o\": u\"\\uf192\",\n    \"download\": u\"\\uf019\",\n    \"dribbble\": u\"\\uf17d\",\n    \"dropbox\": u\"\\uf16b\",\n    \"drupal\": u\"\\uf1a9\",\n    \"edge\": u\"\\uf282\",\n    \"edit (alias)\": u\"\\uf044\",\n    \"eject\": u\"\\uf052\",\n    \"ellipsis-h\": u\"\\uf141\",\n    \"ellipsis-v\": u\"\\uf142\",\n    \"empire\": u\"\\uf1d1\",\n    \"envelope\": u\"\\uf0e0\",\n    \"envelope-o\": u\"\\uf003\",\n    \"envelope-square\": u\"\\uf199\",\n    \"envira\": u\"\\uf299\",\n    \"eraser\": u\"\\uf12d\",\n    \"eur\": u\"\\uf153\",\n    \"euro (alias)\": u\"\\uf153\",\n    \"exchange\": u\"\\uf0ec\",\n    \"exclamation\": u\"\\uf12a\",\n    \"exclamation-circle\": u\"\\uf06a\",\n    \"exclamation-triangle\": u\"\\uf071\",\n    \"expand\": u\"\\uf065\",\n    \"expeditedssl\": u\"\\uf23e\",\n    \"external-link\": u\"\\uf08e\",\n    \"external-link-square\": u\"\\uf14c\",\n    \"eye\": u\"\\uf06e\",\n    \"eye-slash\": u\"\\uf070\",\n    \"eyedropper\": u\"\\uf1fb\",\n    \"fa (alias)\": u\"\\uf2b4\",\n    \"facebook\": u\"\\uf09a\",\n    \"facebook-f (alias)\": u\"\\uf09a\",\n    \"facebook-official\": u\"\\uf230\",\n    \"facebook-square\": u\"\\uf082\",\n    \"fast-backward\": u\"\\uf049\",\n    \"fast-forward\": u\"\\uf050\",\n    \"fax\": u\"\\uf1ac\",\n    \"feed (alias)\": u\"\\uf09e\",\n    \"female\": u\"\\uf182\",\n    \"fighter-jet\": u\"\\uf0fb\",\n    \"file\": u\"\\uf15b\",\n    \"file-archive-o\": u\"\\uf1c6\",\n    \"file-audio-o\": u\"\\uf1c7\",\n    \"file-code-o\": u\"\\uf1c9\",\n    \"file-excel-o\": u\"\\uf1c3\",\n    \"file-image-o\": u\"\\uf1c5\",\n    \"file-movie-o (alias)\": u\"\\uf1c8\",\n    \"file-o\": u\"\\uf016\",\n    \"file-pdf-o\": u\"\\uf1c1\",\n    \"file-photo-o (alias)\": u\"\\uf1c5\",\n    \"file-picture-o (alias)\": u\"\\uf1c5\",\n    \"file-powerpoint-o\": u\"\\uf1c4\",\n    \"file-sound-o (alias)\": u\"\\uf1c7\",\n    \"file-text\": u\"\\uf15c\",\n    \"file-text-o\": u\"\\uf0f6\",\n    \"file-video-o\": u\"\\uf1c8\",\n    \"file-word-o\": u\"\\uf1c2\",\n    \"file-zip-o (alias)\": u\"\\uf1c6\",\n    \"files-o\": u\"\\uf0c5\",\n    \"film\": u\"\\uf008\",\n    \"filter\": u\"\\uf0b0\",\n    \"fire\": u\"\\uf06d\",\n    \"fire-extinguisher\": u\"\\uf134\",\n    \"firefox\": u\"\\uf269\",\n    \"first-order\": u\"\\uf2b0\",\n    \"flag\": u\"\\uf024\",\n    \"flag-checkered\": u\"\\uf11e\",\n    \"flag-o\": u\"\\uf11d\",\n    \"flash (alias)\": u\"\\uf0e7\",\n    \"flask\": u\"\\uf0c3\",\n    \"flickr\": u\"\\uf16e\",\n    \"floppy-o\": u\"\\uf0c7\",\n    \"folder\": u\"\\uf07b\",\n    \"folder-o\": u\"\\uf114\",\n    \"folder-open\": u\"\\uf07c\",\n    \"folder-open-o\": u\"\\uf115\",\n    \"font\": u\"\\uf031\",\n    \"font-awesome\": u\"\\uf2b4\",\n    \"fonticons\": u\"\\uf280\",\n    \"fort-awesome\": u\"\\uf286\",\n    \"forumbee\": u\"\\uf211\",\n    \"forward\": u\"\\uf04e\",\n    \"foursquare\": u\"\\uf180\",\n    \"frown-o\": u\"\\uf119\",\n    \"futbol-o\": u\"\\uf1e3\",\n    \"gamepad\": u\"\\uf11b\",\n    \"gavel\": u\"\\uf0e3\",\n    \"gbp\": u\"\\uf154\",\n    \"ge (alias)\": u\"\\uf1d1\",\n    \"gear (alias)\": u\"\\uf013\",\n    \"gears (alias)\": u\"\\uf085\",\n    \"genderless\": u\"\\uf22d\",\n    \"get-pocket\": u\"\\uf265\",\n    \"gg\": u\"\\uf260\",\n    \"gg-circle\": u\"\\uf261\",\n    \"gift\": u\"\\uf06b\",\n    \"git\": u\"\\uf1d3\",\n    \"git-square\": u\"\\uf1d2\",\n    \"github\": u\"\\uf09b\",\n    \"github-alt\": u\"\\uf113\",\n    \"github-square\": u\"\\uf092\",\n    \"gitlab\": u\"\\uf296\",\n    \"gittip (alias)\": u\"\\uf184\",\n    \"glass\": u\"\\uf000\",\n    \"glide\": u\"\\uf2a5\",\n    \"glide-g\": u\"\\uf2a6\",\n    \"globe\": u\"\\uf0ac\",\n    \"google\": u\"\\uf1a0\",\n    \"google-plus\": u\"\\uf0d5\",\n    \"google-plus-circle (alias)\": u\"\\uf2b3\",\n    \"google-plus-official\": u\"\\uf2b3\",\n    \"google-plus-square\": u\"\\uf0d4\",\n    \"google-wallet\": u\"\\uf1ee\",\n    \"graduation-cap\": u\"\\uf19d\",\n    \"gratipay\": u\"\\uf184\",\n    \"group (alias)\": u\"\\uf0c0\",\n    \"h-square\": u\"\\uf0fd\",\n    \"hacker-news\": u\"\\uf1d4\",\n    \"hand-grab-o (alias)\": u\"\\uf255\",\n    \"hand-lizard-o\": u\"\\uf258\",\n    \"hand-o-down\": u\"\\uf0a7\",\n    \"hand-o-left\": u\"\\uf0a5\",\n    \"hand-o-right\": u\"\\uf0a4\",\n    \"hand-o-up\": u\"\\uf0a6\",\n    \"hand-paper-o\": u\"\\uf256\",\n    \"hand-peace-o\": u\"\\uf25b\",\n    \"hand-pointer-o\": u\"\\uf25a\",\n    \"hand-rock-o\": u\"\\uf255\",\n    \"hand-scissors-o\": u\"\\uf257\",\n    \"hand-spock-o\": u\"\\uf259\",\n    \"hand-stop-o (alias)\": u\"\\uf256\",\n    \"hard-of-hearing (alias)\": u\"\\uf2a4\",\n    \"hashtag\": u\"\\uf292\",\n    \"hdd-o\": u\"\\uf0a0\",\n    \"header\": u\"\\uf1dc\",\n    \"headphones\": u\"\\uf025\",\n    \"heart\": u\"\\uf004\",\n    \"heart-o\": u\"\\uf08a\",\n    \"heartbeat\": u\"\\uf21e\",\n    \"history\": u\"\\uf1da\",\n    \"home\": u\"\\uf015\",\n    \"hospital-o\": u\"\\uf0f8\",\n    \"hotel (alias)\": u\"\\uf236\",\n    \"hourglass\": u\"\\uf254\",\n    \"hourglass-1 (alias)\": u\"\\uf251\",\n    \"hourglass-2 (alias)\": u\"\\uf252\",\n    \"hourglass-3 (alias)\": u\"\\uf253\",\n    \"hourglass-end\": u\"\\uf253\",\n    \"hourglass-half\": u\"\\uf252\",\n    \"hourglass-o\": u\"\\uf250\",\n    \"hourglass-start\": u\"\\uf251\",\n    \"houzz\": u\"\\uf27c\",\n    \"html5\": u\"\\uf13b\",\n    \"i-cursor\": u\"\\uf246\",\n    \"ils\": u\"\\uf20b\",\n    \"image (alias)\": u\"\\uf03e\",\n    \"inbox\": u\"\\uf01c\",\n    \"indent\": u\"\\uf03c\",\n    \"industry\": u\"\\uf275\",\n    \"info\": u\"\\uf129\",\n    \"info-circle\": u\"\\uf05a\",\n    \"inr\": u\"\\uf156\",\n    \"instagram\": u\"\\uf16d\",\n    \"institution (alias)\": u\"\\uf19c\",\n    \"internet-explorer\": u\"\\uf26b\",\n    \"intersex (alias)\": u\"\\uf224\",\n    \"ioxhost\": u\"\\uf208\",\n    \"italic\": u\"\\uf033\",\n    \"joomla\": u\"\\uf1aa\",\n    \"jpy\": u\"\\uf157\",\n    \"jsfiddle\": u\"\\uf1cc\",\n    \"key\": u\"\\uf084\",\n    \"keyboard-o\": u\"\\uf11c\",\n    \"krw\": u\"\\uf159\",\n    \"language\": u\"\\uf1ab\",\n    \"laptop\": u\"\\uf109\",\n    \"lastfm\": u\"\\uf202\",\n    \"lastfm-square\": u\"\\uf203\",\n    \"leaf\": u\"\\uf06c\",\n    \"leanpub\": u\"\\uf212\",\n    \"legal (alias)\": u\"\\uf0e3\",\n    \"lemon-o\": u\"\\uf094\",\n    \"level-down\": u\"\\uf149\",\n    \"level-up\": u\"\\uf148\",\n    \"life-bouy (alias)\": u\"\\uf1cd\",\n    \"life-buoy (alias)\": u\"\\uf1cd\",\n    \"life-ring\": u\"\\uf1cd\",\n    \"life-saver (alias)\": u\"\\uf1cd\",\n    \"lightbulb-o\": u\"\\uf0eb\",\n    \"line-chart\": u\"\\uf201\",\n    \"link\": u\"\\uf0c1\",\n    \"linkedin\": u\"\\uf0e1\",\n    \"linkedin-square\": u\"\\uf08c\",\n    \"linux\": u\"\\uf17c\",\n    \"list\": u\"\\uf03a\",\n    \"list-alt\": u\"\\uf022\",\n    \"list-ol\": u\"\\uf0cb\",\n    \"list-ul\": u\"\\uf0ca\",\n    \"location-arrow\": u\"\\uf124\",\n    \"lock\": u\"\\uf023\",\n    \"long-arrow-down\": u\"\\uf175\",\n    \"long-arrow-left\": u\"\\uf177\",\n    \"long-arrow-right\": u\"\\uf178\",\n    \"long-arrow-up\": u\"\\uf176\",\n    \"low-vision\": u\"\\uf2a8\",\n    \"magic\": u\"\\uf0d0\",\n    \"magnet\": u\"\\uf076\",\n    \"mail-forward (alias)\": u\"\\uf064\",\n    \"mail-reply (alias)\": u\"\\uf112\",\n    \"mail-reply-all (alias)\": u\"\\uf122\",\n    \"male\": u\"\\uf183\",\n    \"map\": u\"\\uf279\",\n    \"map-marker\": u\"\\uf041\",\n    \"map-o\": u\"\\uf278\",\n    \"map-pin\": u\"\\uf276\",\n    \"map-signs\": u\"\\uf277\",\n    \"mars\": u\"\\uf222\",\n    \"mars-double\": u\"\\uf227\",\n    \"mars-stroke\": u\"\\uf229\",\n    \"mars-stroke-h\": u\"\\uf22b\",\n    \"mars-stroke-v\": u\"\\uf22a\",\n    \"maxcdn\": u\"\\uf136\",\n    \"meanpath\": u\"\\uf20c\",\n    \"medium\": u\"\\uf23a\",\n    \"medkit\": u\"\\uf0fa\",\n    \"meh-o\": u\"\\uf11a\",\n    \"mercury\": u\"\\uf223\",\n    \"microphone\": u\"\\uf130\",\n    \"microphone-slash\": u\"\\uf131\",\n    \"minus\": u\"\\uf068\",\n    \"minus-circle\": u\"\\uf056\",\n    \"minus-square\": u\"\\uf146\",\n    \"minus-square-o\": u\"\\uf147\",\n    \"mixcloud\": u\"\\uf289\",\n    \"mobile\": u\"\\uf10b\",\n    \"mobile-phone (alias)\": u\"\\uf10b\",\n    \"modx\": u\"\\uf285\",\n    \"money\": u\"\\uf0d6\",\n    \"moon-o\": u\"\\uf186\",\n    \"mortar-board (alias)\": u\"\\uf19d\",\n    \"motorcycle\": u\"\\uf21c\",\n    \"mouse-pointer\": u\"\\uf245\",\n    \"music\": u\"\\uf001\",\n    \"navicon (alias)\": u\"\\uf0c9\",\n    \"neuter\": u\"\\uf22c\",\n    \"newspaper-o\": u\"\\uf1ea\",\n    \"object-group\": u\"\\uf247\",\n    \"object-ungroup\": u\"\\uf248\",\n    \"odnoklassniki\": u\"\\uf263\",\n    \"odnoklassniki-square\": u\"\\uf264\",\n    \"opencart\": u\"\\uf23d\",\n    \"openid\": u\"\\uf19b\",\n    \"opera\": u\"\\uf26a\",\n    \"optin-monster\": u\"\\uf23c\",\n    \"outdent\": u\"\\uf03b\",\n    \"pagelines\": u\"\\uf18c\",\n    \"paint-brush\": u\"\\uf1fc\",\n    \"paper-plane\": u\"\\uf1d8\",\n    \"paper-plane-o\": u\"\\uf1d9\",\n    \"paperclip\": u\"\\uf0c6\",\n    \"paragraph\": u\"\\uf1dd\",\n    \"paste (alias)\": u\"\\uf0ea\",\n    \"pause\": u\"\\uf04c\",\n    \"pause-circle\": u\"\\uf28b\",\n    \"pause-circle-o\": u\"\\uf28c\",\n    \"paw\": u\"\\uf1b0\",\n    \"paypal\": u\"\\uf1ed\",\n    \"pencil\": u\"\\uf040\",\n    \"pencil-square\": u\"\\uf14b\",\n    \"pencil-square-o\": u\"\\uf044\",\n    \"percent\": u\"\\uf295\",\n    \"phone\": u\"\\uf095\",\n    \"phone-square\": u\"\\uf098\",\n    \"photo (alias)\": u\"\\uf03e\",\n    \"picture-o\": u\"\\uf03e\",\n    \"pie-chart\": u\"\\uf200\",\n    \"pied-piper\": u\"\\uf2ae\",\n    \"pied-piper-alt\": u\"\\uf1a8\",\n    \"pied-piper-pp\": u\"\\uf1a7\",\n    \"pinterest\": u\"\\uf0d2\",\n    \"pinterest-p\": u\"\\uf231\",\n    \"pinterest-square\": u\"\\uf0d3\",\n    \"plane\": u\"\\uf072\",\n    \"play\": u\"\\uf04b\",\n    \"play-circle\": u\"\\uf144\",\n    \"play-circle-o\": u\"\\uf01d\",\n    \"plug\": u\"\\uf1e6\",\n    \"plus\": u\"\\uf067\",\n    \"plus-circle\": u\"\\uf055\",\n    \"plus-square\": u\"\\uf0fe\",\n    \"plus-square-o\": u\"\\uf196\",\n    \"power-off\": u\"\\uf011\",\n    \"print\": u\"\\uf02f\",\n    \"product-hunt\": u\"\\uf288\",\n    \"puzzle-piece\": u\"\\uf12e\",\n    \"qq\": u\"\\uf1d6\",\n    \"qrcode\": u\"\\uf029\",\n    \"question\": u\"\\uf128\",\n    \"question-circle\": u\"\\uf059\",\n    \"question-circle-o\": u\"\\uf29c\",\n    \"quote-left\": u\"\\uf10d\",\n    \"quote-right\": u\"\\uf10e\",\n    \"ra (alias)\": u\"\\uf1d0\",\n    \"random\": u\"\\uf074\",\n    \"rebel\": u\"\\uf1d0\",\n    \"recycle\": u\"\\uf1b8\",\n    \"reddit\": u\"\\uf1a1\",\n    \"reddit-alien\": u\"\\uf281\",\n    \"reddit-square\": u\"\\uf1a2\",\n    \"refresh\": u\"\\uf021\",\n    \"registered\": u\"\\uf25d\",\n    \"remove (alias)\": u\"\\uf00d\",\n    \"renren\": u\"\\uf18b\",\n    \"reorder (alias)\": u\"\\uf0c9\",\n    \"repeat\": u\"\\uf01e\",\n    \"reply\": u\"\\uf112\",\n    \"reply-all\": u\"\\uf122\",\n    \"resistance (alias)\": u\"\\uf1d0\",\n    \"retweet\": u\"\\uf079\",\n    \"rmb (alias)\": u\"\\uf157\",\n    \"road\": u\"\\uf018\",\n    \"rocket\": u\"\\uf135\",\n    \"rotate-left (alias)\": u\"\\uf0e2\",\n    \"rotate-right (alias)\": u\"\\uf01e\",\n    \"rouble (alias)\": u\"\\uf158\",\n    \"rss\": u\"\\uf09e\",\n    \"rss-square\": u\"\\uf143\",\n    \"rub\": u\"\\uf158\",\n    \"ruble (alias)\": u\"\\uf158\",\n    \"rupee (alias)\": u\"\\uf156\",\n    \"safari\": u\"\\uf267\",\n    \"save (alias)\": u\"\\uf0c7\",\n    \"scissors\": u\"\\uf0c4\",\n    \"scribd\": u\"\\uf28a\",\n    \"search\": u\"\\uf002\",\n    \"search-minus\": u\"\\uf010\",\n    \"search-plus\": u\"\\uf00e\",\n    \"sellsy\": u\"\\uf213\",\n    \"send (alias)\": u\"\\uf1d8\",\n    \"send-o (alias)\": u\"\\uf1d9\",\n    \"server\": u\"\\uf233\",\n    \"share\": u\"\\uf064\",\n    \"share-alt\": u\"\\uf1e0\",\n    \"share-alt-square\": u\"\\uf1e1\",\n    \"share-square\": u\"\\uf14d\",\n    \"share-square-o\": u\"\\uf045\",\n    \"shekel (alias)\": u\"\\uf20b\",\n    \"sheqel (alias)\": u\"\\uf20b\",\n    \"shield\": u\"\\uf132\",\n    \"ship\": u\"\\uf21a\",\n    \"shirtsinbulk\": u\"\\uf214\",\n    \"shopping-bag\": u\"\\uf290\",\n    \"shopping-basket\": u\"\\uf291\",\n    \"shopping-cart\": u\"\\uf07a\",\n    \"sign-in\": u\"\\uf090\",\n    \"sign-language\": u\"\\uf2a7\",\n    \"sign-out\": u\"\\uf08b\",\n    \"signal\": u\"\\uf012\",\n    \"signing (alias)\": u\"\\uf2a7\",\n    \"simplybuilt\": u\"\\uf215\",\n    \"sitemap\": u\"\\uf0e8\",\n    \"skyatlas\": u\"\\uf216\",\n    \"skype\": u\"\\uf17e\",\n    \"slack\": u\"\\uf198\",\n    \"sliders\": u\"\\uf1de\",\n    \"slideshare\": u\"\\uf1e7\",\n    \"smile-o\": u\"\\uf118\",\n    \"snapchat\": u\"\\uf2ab\",\n    \"snapchat-ghost\": u\"\\uf2ac\",\n    \"snapchat-square\": u\"\\uf2ad\",\n    \"soccer-ball-o (alias)\": u\"\\uf1e3\",\n    \"sort\": u\"\\uf0dc\",\n    \"sort-alpha-asc\": u\"\\uf15d\",\n    \"sort-alpha-desc\": u\"\\uf15e\",\n    \"sort-amount-asc\": u\"\\uf160\",\n    \"sort-amount-desc\": u\"\\uf161\",\n    \"sort-asc\": u\"\\uf0de\",\n    \"sort-desc\": u\"\\uf0dd\",\n    \"sort-down (alias)\": u\"\\uf0dd\",\n    \"sort-numeric-asc\": u\"\\uf162\",\n    \"sort-numeric-desc\": u\"\\uf163\",\n    \"sort-up (alias)\": u\"\\uf0de\",\n    \"soundcloud\": u\"\\uf1be\",\n    \"space-shuttle\": u\"\\uf197\",\n    \"spinner\": u\"\\uf110\",\n    \"spoon\": u\"\\uf1b1\",\n    \"spotify\": u\"\\uf1bc\",\n    \"square\": u\"\\uf0c8\",\n    \"square-o\": u\"\\uf096\",\n    \"stack-exchange\": u\"\\uf18d\",\n    \"stack-overflow\": u\"\\uf16c\",\n    \"star\": u\"\\uf005\",\n    \"star-half\": u\"\\uf089\",\n    \"star-half-empty (alias)\": u\"\\uf123\",\n    \"star-half-full (alias)\": u\"\\uf123\",\n    \"star-half-o\": u\"\\uf123\",\n    \"star-o\": u\"\\uf006\",\n    \"steam\": u\"\\uf1b6\",\n    \"steam-square\": u\"\\uf1b7\",\n    \"step-backward\": u\"\\uf048\",\n    \"step-forward\": u\"\\uf051\",\n    \"stethoscope\": u\"\\uf0f1\",\n    \"sticky-note\": u\"\\uf249\",\n    \"sticky-note-o\": u\"\\uf24a\",\n    \"stop\": u\"\\uf04d\",\n    \"stop-circle\": u\"\\uf28d\",\n    \"stop-circle-o\": u\"\\uf28e\",\n    \"street-view\": u\"\\uf21d\",\n    \"strikethrough\": u\"\\uf0cc\",\n    \"stumbleupon\": u\"\\uf1a4\",\n    \"stumbleupon-circle\": u\"\\uf1a3\",\n    \"subscript\": u\"\\uf12c\",\n    \"subway\": u\"\\uf239\",\n    \"suitcase\": u\"\\uf0f2\",\n    \"sun-o\": u\"\\uf185\",\n    \"superscript\": u\"\\uf12b\",\n    \"support (alias)\": u\"\\uf1cd\",\n    \"table\": u\"\\uf0ce\",\n    \"tablet\": u\"\\uf10a\",\n    \"tachometer\": u\"\\uf0e4\",\n    \"tag\": u\"\\uf02b\",\n    \"tags\": u\"\\uf02c\",\n    \"tasks\": u\"\\uf0ae\",\n    \"taxi\": u\"\\uf1ba\",\n    \"television\": u\"\\uf26c\",\n    \"tencent-weibo\": u\"\\uf1d5\",\n    \"terminal\": u\"\\uf120\",\n    \"text-height\": u\"\\uf034\",\n    \"text-width\": u\"\\uf035\",\n    \"th\": u\"\\uf00a\",\n    \"th-large\": u\"\\uf009\",\n    \"th-list\": u\"\\uf00b\",\n    \"themeisle\": u\"\\uf2b2\",\n    \"thumb-tack\": u\"\\uf08d\",\n    \"thumbs-down\": u\"\\uf165\",\n    \"thumbs-o-down\": u\"\\uf088\",\n    \"thumbs-o-up\": u\"\\uf087\",\n    \"thumbs-up\": u\"\\uf164\",\n    \"ticket\": u\"\\uf145\",\n    \"times\": u\"\\uf00d\",\n    \"times-circle\": u\"\\uf057\",\n    \"times-circle-o\": u\"\\uf05c\",\n    \"tint\": u\"\\uf043\",\n    \"toggle-down (alias)\": u\"\\uf150\",\n    \"toggle-left (alias)\": u\"\\uf191\",\n    \"toggle-off\": u\"\\uf204\",\n    \"toggle-on\": u\"\\uf205\",\n    \"toggle-right (alias)\": u\"\\uf152\",\n    \"toggle-up (alias)\": u\"\\uf151\",\n    \"trademark\": u\"\\uf25c\",\n    \"train\": u\"\\uf238\",\n    \"transgender\": u\"\\uf224\",\n    \"transgender-alt\": u\"\\uf225\",\n    \"trash\": u\"\\uf1f8\",\n    \"trash-o\": u\"\\uf014\",\n    \"tree\": u\"\\uf1bb\",\n    \"trello\": u\"\\uf181\",\n    \"tripadvisor\": u\"\\uf262\",\n    \"trophy\": u\"\\uf091\",\n    \"truck\": u\"\\uf0d1\",\n    \"try\": u\"\\uf195\",\n    \"tty\": u\"\\uf1e4\",\n    \"tumblr\": u\"\\uf173\",\n    \"tumblr-square\": u\"\\uf174\",\n    \"turkish-lira (alias)\": u\"\\uf195\",\n    \"tv (alias)\": u\"\\uf26c\",\n    \"twitch\": u\"\\uf1e8\",\n    \"twitter\": u\"\\uf099\",\n    \"twitter-square\": u\"\\uf081\",\n    \"umbrella\": u\"\\uf0e9\",\n    \"underline\": u\"\\uf0cd\",\n    \"undo\": u\"\\uf0e2\",\n    \"universal-access\": u\"\\uf29a\",\n    \"university\": u\"\\uf19c\",\n    \"unlink (alias)\": u\"\\uf127\",\n    \"unlock\": u\"\\uf09c\",\n    \"unlock-alt\": u\"\\uf13e\",\n    \"unsorted (alias)\": u\"\\uf0dc\",\n    \"upload\": u\"\\uf093\",\n    \"usb\": u\"\\uf287\",\n    \"usd\": u\"\\uf155\",\n    \"user\": u\"\\uf007\",\n    \"user-md\": u\"\\uf0f0\",\n    \"user-plus\": u\"\\uf234\",\n    \"user-secret\": u\"\\uf21b\",\n    \"user-times\": u\"\\uf235\",\n    \"users\": u\"\\uf0c0\",\n    \"venus\": u\"\\uf221\",\n    \"venus-double\": u\"\\uf226\",\n    \"venus-mars\": u\"\\uf228\",\n    \"viacoin\": u\"\\uf237\",\n    \"viadeo\": u\"\\uf2a9\",\n    \"viadeo-square\": u\"\\uf2aa\",\n    \"video-camera\": u\"\\uf03d\",\n    \"vimeo\": u\"\\uf27d\",\n    \"vimeo-square\": u\"\\uf194\",\n    \"vine\": u\"\\uf1ca\",\n    \"vk\": u\"\\uf189\",\n    \"volume-control-phone\": u\"\\uf2a0\",\n    \"volume-down\": u\"\\uf027\",\n    \"volume-off\": u\"\\uf026\",\n    \"volume-up\": u\"\\uf028\",\n    \"warning (alias)\": u\"\\uf071\",\n    \"wechat (alias)\": u\"\\uf1d7\",\n    \"weibo\": u\"\\uf18a\",\n    \"weixin\": u\"\\uf1d7\",\n    \"whatsapp\": u\"\\uf232\",\n    \"wheelchair\": u\"\\uf193\",\n    \"wheelchair-alt\": u\"\\uf29b\",\n    \"wifi\": u\"\\uf1eb\",\n    \"wikipedia-w\": u\"\\uf266\",\n    \"windows\": u\"\\uf17a\",\n    \"won (alias)\": u\"\\uf159\",\n    \"wordpress\": u\"\\uf19a\",\n    \"wpbeginner\": u\"\\uf297\",\n    \"wpforms\": u\"\\uf298\",\n    \"wrench\": u\"\\uf0ad\",\n    \"xing\": u\"\\uf168\",\n    \"xing-square\": u\"\\uf169\",\n    \"y-combinator\": u\"\\uf23b\",\n    \"y-combinator-square (alias)\": u\"\\uf1d4\",\n    \"yahoo\": u\"\\uf19e\",\n    \"yc (alias)\": u\"\\uf23b\",\n    \"yc-square (alias)\": u\"\\uf1d4\",\n    \"yelp\": u\"\\uf1e9\",\n    \"yen (alias)\": u\"\\uf157\",\n    \"yoast\": u\"\\uf2b1\",\n    \"youtube\": u\"\\uf167\",\n    \"youtube-play\": u\"\\uf16a\",\n    \"youtube-square\": u\"\\uf166\"\n}\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/constants.py",
    "content": "from qtpy import QtCore\n\nEXPANDER_WIDTH = 20\n\n\ndef flags(*args, **kwargs):\n    type_name = kwargs.pop(\"type_name\", \"Flags\")\n    with_base = kwargs.pop(\"with_base\", False)\n    enums = {}\n    for idx, attr_name in enumerate(args):\n        if with_base:\n            if idx == 0:\n                enums[attr_name] = 0\n                continue\n            idx -= 1\n        enums[attr_name] = 2**idx\n\n    for attr_name, value in kwargs.items():\n        enums[attr_name] = value\n    return type(type_name, (), enums)\n\n\ndef roles(*args, **kwargs):\n    type_name = kwargs.pop(\"type_name\", \"Roles\")\n    enums = {}\n    for attr_name, value in kwargs.items():\n        enums[attr_name] = value\n\n    offset = 0\n    for idx, attr_name in enumerate(args):\n        _idx = idx + QtCore.Qt.UserRole + offset\n        while _idx in enums.values():\n            offset += 1\n            _idx = idx + offset\n\n        enums[attr_name] = _idx\n\n    return type(type_name, (), enums)\n\n\nRoles = roles(\n    \"ObjectIdRole\",\n    \"ObjectUIdRole\",\n    \"TypeRole\",\n    \"PublishFlagsRole\",\n    \"LogRecordsRole\",\n\n    \"IsOptionalRole\",\n    \"IsEnabledRole\",\n\n    \"FamiliesRole\",\n\n    \"DocstringRole\",\n    \"PathModuleRole\",\n    \"PluginActionsVisibleRole\",\n    \"PluginValidActionsRole\",\n    \"PluginActionProgressRole\",\n\n    \"TerminalItemTypeRole\",\n\n    \"IntentItemValue\",\n\n    type_name=\"ModelRoles\"\n)\n\nInstanceStates = flags(\n    \"ContextType\",\n    \"InProgress\",\n    \"HasWarning\",\n    \"HasError\",\n    \"HasFinished\",\n    type_name=\"InstanceState\"\n)\n\nPluginStates = flags(\n    \"IsCompatible\",\n    \"InProgress\",\n    \"WasProcessed\",\n    \"WasSkipped\",\n    \"HasWarning\",\n    \"HasError\",\n    type_name=\"PluginState\"\n)\n\nGroupStates = flags(\n    \"HasWarning\",\n    \"HasError\",\n    \"HasFinished\",\n    type_name=\"GroupStates\"\n)\n\nPluginActionStates = flags(\n    \"InProgress\",\n    \"HasFailed\",\n    \"HasFinished\",\n    type_name=\"PluginActionStates\"\n)\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/control.py",
    "content": "\"\"\"The Controller in a Model/View/Controller-based application\nThe graphical components of Pyblish Lite use this object to perform\npublishing. It communicates via the Qt Signals/Slots mechanism\nand has no direct connection to any graphics. This is important,\nbecause this is how unittests are able to run without requiring\nan active window manager; such as via Travis-CI.\n\"\"\"\nimport os\nimport sys\nimport inspect\nimport logging\nimport collections\n\nfrom qtpy import QtCore\n\nimport pyblish.api\nimport pyblish.util\nimport pyblish.logic\nimport pyblish.lib\nimport pyblish.version\n\nfrom . import util\nfrom .constants import InstanceStates\n\nfrom openpype.settings import get_current_project_settings\n\n\nclass IterationBreak(Exception):\n    pass\n\n\nclass MainThreadItem:\n    \"\"\"Callback with args and kwargs.\"\"\"\n    def __init__(self, callback, *args, **kwargs):\n        self.callback = callback\n        self.args = args\n        self.kwargs = kwargs\n\n    def process(self):\n        self.callback(*self.args, **self.kwargs)\n\n\nclass MainThreadProcess(QtCore.QObject):\n    \"\"\"Qt based main thread process executor.\n\n    Has timer which controls each 50ms if there is new item to process.\n\n    This approach gives ability to update UI meanwhile plugin is in progress.\n    \"\"\"\n    # How many times let pass QtApplication to process events\n    # - use 2 as resize event can trigger repaint event but not process in\n    #   same loop\n    count_timeout = 2\n\n    def __init__(self):\n        super(MainThreadProcess, self).__init__()\n        self._items_to_process = collections.deque()\n\n        timer = QtCore.QTimer()\n        timer.setInterval(0)\n\n        timer.timeout.connect(self._execute)\n\n        self._timer = timer\n        self._switch_counter = self.count_timeout\n\n    def process(self, func, *args, **kwargs):\n        item = MainThreadItem(func, *args, **kwargs)\n        self.add_item(item)\n\n    def add_item(self, item):\n        self._items_to_process.append(item)\n\n    def _execute(self):\n        if not self._items_to_process:\n            return\n\n        if self._switch_counter > 0:\n            self._switch_counter -= 1\n            return\n\n        self._switch_counter = self.count_timeout\n\n        item = self._items_to_process.popleft()\n        item.process()\n\n    def start(self):\n        if not self._timer.isActive():\n            self._timer.start()\n\n    def stop(self):\n        if self._timer.isActive():\n            self._timer.stop()\n\n    def clear(self):\n        if self._timer.isActive():\n            self._timer.stop()\n        self._items_to_process = collections.deque()\n\n    def stop_if_empty(self):\n        if self._timer.isActive():\n            item = MainThreadItem(self._stop_if_empty)\n            self.add_item(item)\n\n    def _stop_if_empty(self):\n        if not self._items_to_process:\n            self.stop()\n\n\nclass Controller(QtCore.QObject):\n    log = logging.getLogger(\"PyblishController\")\n    # Emitted when the GUI is about to start processing;\n    # e.g. resetting, validating or publishing.\n    about_to_process = QtCore.Signal(object, object)\n\n    # ??? Emitted for each process\n    was_processed = QtCore.Signal(dict)\n\n    # Emitted when reset\n    # - all data are reset (plugins, processing, pari yielder, etc.)\n    was_reset = QtCore.Signal()\n\n    # Emitted when previous group changed\n    passed_group = QtCore.Signal(object)\n\n    # Emitted when want to change state of instances\n    switch_toggleability = QtCore.Signal(bool)\n\n    # On action finished\n    was_acted = QtCore.Signal(dict)\n\n    # Emitted when processing has stopped\n    was_stopped = QtCore.Signal()\n\n    # Emitted when processing has finished\n    was_finished = QtCore.Signal()\n\n    # Emitted when plugin was skipped\n    was_skipped = QtCore.Signal(object)\n\n    # store OrderGroups - now it is a singleton\n    order_groups = util.OrderGroups\n\n    # When instance is toggled\n    instance_toggled = QtCore.Signal(object, object, object)\n\n    def __init__(self, parent=None):\n        super(Controller, self).__init__(parent)\n        self.context = None\n        self.plugins = {}\n        self.optional_default = {}\n        self.instance_toggled.connect(self._on_instance_toggled)\n        self._main_thread_processor = MainThreadProcess()\n\n        self._current_state = \"\"\n\n    def reset_variables(self):\n        self.log.debug(\"Resetting pyblish context variables\")\n\n        # Data internal to the GUI itself\n        self.is_running = False\n        self.stopped = False\n        self.errored = False\n        self._current_state = \"\"\n\n        # Active producer of pairs\n        self.pair_generator = None\n        # Active pair\n        self.current_pair = None\n\n        # Orders which changes GUI\n        # - passing collectors order disables plugin/instance toggle\n        self.collect_state = 0\n\n        # - passing validators order disables validate button and gives ability\n        #   to know when to stop on validate button press\n        self.validators_order = None\n        self.validated = False\n\n        # Get collectors and validators order\n        plugin_groups_keys = list(self.order_groups.groups.keys())\n        self.validators_order = self.order_groups.validation_order\n        next_group_order = None\n        if len(plugin_groups_keys) > 1:\n            next_group_order = plugin_groups_keys[1]\n\n        # This is used to track whether or not to continue\n        # processing when, for example, validation has failed.\n        self.processing = {\n            \"stop_on_validation\": False,\n            # Used?\n            \"last_plugin_order\": None,\n            \"current_group_order\": plugin_groups_keys[0],\n            \"next_group_order\": next_group_order,\n            \"nextOrder\": None,\n            \"ordersWithError\": set()\n        }\n        self._set_state_by_order()\n        self.log.debug(\"Reset of pyblish context variables done\")\n\n    @property\n    def current_state(self):\n        return self._current_state\n\n    def presets_by_hosts(self):\n        # Get global filters as base\n        presets = get_current_project_settings()\n        if not presets:\n            return {}\n\n        result = presets.get(\"global\", {}).get(\"filters\", {})\n        hosts = pyblish.api.registered_hosts()\n        for host in hosts:\n            host_presets = presets.get(host, {}).get(\"filters\")\n            if not host_presets:\n                continue\n\n            for key, value in host_presets.items():\n                if value is None:\n                    if key in result:\n                        result.pop(key)\n                    continue\n\n                result[key] = value\n\n        return result\n\n    def reset_context(self):\n        self.log.debug(\"Resetting pyblish context object\")\n\n        comment = None\n        if (\n            self.context is not None and\n            self.context.data.get(\"comment\") and\n            # We only preserve the user typed comment if we are *not*\n            # resetting from a successful publish without errors\n            self._current_state != \"Published\"\n        ):\n            comment = self.context.data[\"comment\"]\n\n        self.context = pyblish.api.Context()\n\n        self.context._publish_states = InstanceStates.ContextType\n        self.context.optional = False\n\n        self.context.data[\"publish\"] = True\n        self.context.data[\"name\"] = \"context\"\n\n        self.context.data[\"host\"] = reversed(pyblish.api.registered_hosts())\n        self.context.data[\"port\"] = int(\n            os.environ.get(\"PYBLISH_CLIENT_PORT\", -1)\n        )\n        self.context.data[\"connectTime\"] = pyblish.lib.time(),\n        self.context.data[\"pyblishVersion\"] = pyblish.version,\n        self.context.data[\"pythonVersion\"] = sys.version\n\n        self.context.data[\"icon\"] = \"book\"\n\n        self.context.families = (\"__context__\",)\n\n        if comment:\n            # Preserve comment on reset if user previously had a comment\n            self.context.data[\"comment\"] = comment\n\n        self.log.debug(\"Reset of pyblish context object done\")\n\n    def reset(self):\n        \"\"\"Discover plug-ins and run collection.\"\"\"\n        self._main_thread_processor.clear()\n        self._main_thread_processor.process(self._reset)\n        self._main_thread_processor.start()\n\n    def _reset(self):\n        self.reset_context()\n        self.reset_variables()\n\n        self.possible_presets = self.presets_by_hosts()\n\n        # Load plugins and set pair generator\n        self.load_plugins()\n        self.pair_generator = self._pair_yielder(self.plugins)\n\n        self.was_reset.emit()\n\n        # Process collectors load rest of plugins with collected instances\n        self.collect()\n\n    def load_plugins(self):\n        self.test = pyblish.logic.registered_test()\n        self.optional_default = {}\n\n        plugins = pyblish.api.discover()\n\n        targets = set(pyblish.logic.registered_targets())\n        targets.add(\"default\")\n        targets = list(targets)\n        plugins_by_targets = pyblish.logic.plugins_by_targets(plugins, targets)\n\n        _plugins = []\n        for plugin in plugins_by_targets:\n            # Skip plugin if is not optional and not active\n            if (\n                not getattr(plugin, \"optional\", False)\n                and not getattr(plugin, \"active\", True)\n            ):\n                continue\n            _plugins.append(plugin)\n        self.plugins = _plugins\n\n    def on_published(self):\n        if self.is_running:\n            self.is_running = False\n        self._current_state = (\n            \"Published\" if not self.errored else \"Published, with errors\"\n        )\n        self.was_finished.emit()\n        self._main_thread_processor.stop()\n\n    def stop(self):\n        self.log.debug(\"Stopping\")\n        self.stopped = True\n\n    def act(self, plugin, action):\n        self.is_running = True\n        item = MainThreadItem(self._process_action, plugin, action)\n        self._main_thread_processor.add_item(item)\n        self._main_thread_processor.start()\n        self._main_thread_processor.stop_if_empty()\n\n    def _process_action(self, plugin, action):\n        result = pyblish.plugin.process(\n            plugin, self.context, None, action.id\n        )\n        self.is_running = False\n        self.was_acted.emit(result)\n\n    def emit_(self, signal, kwargs):\n        pyblish.api.emit(signal, **kwargs)\n\n    def _process(self, plugin, instance=None):\n        \"\"\"Produce `result` from `plugin` and `instance`\n        :func:`process` shares state with :func:`_iterator` such that\n        an instance/plugin pair can be fetched and processed in isolation.\n        Arguments:\n            plugin (pyblish.api.Plugin): Produce result using plug-in\n            instance (optional, pyblish.api.Instance): Process this instance,\n                if no instance is provided, context is processed.\n        \"\"\"\n\n        self.processing[\"nextOrder\"] = plugin.order\n\n        try:\n            result = pyblish.plugin.process(plugin, self.context, instance)\n            # Make note of the order at which the\n            # potential error error occurred.\n            if result[\"error\"] is not None:\n                self.processing[\"ordersWithError\"].add(plugin.order)\n\n        except Exception as exc:\n            raise Exception(\"Unknown error({}): {}\".format(\n                plugin.__name__, str(exc)\n            ))\n\n        return result\n\n    def _pair_yielder(self, plugins):\n        for plugin in plugins:\n            if (\n                self.processing[\"current_group_order\"] is not None\n                and plugin.order > self.processing[\"current_group_order\"]\n            ):\n                current_group_order = self.processing[\"current_group_order\"]\n\n                new_next_group_order = None\n                new_current_group_order = self.processing[\"next_group_order\"]\n                if new_current_group_order is not None:\n                    current_next_order_found = False\n                    for order in self.order_groups.groups.keys():\n                        if current_next_order_found:\n                            new_next_group_order = order\n                            break\n\n                        if order == new_current_group_order:\n                            current_next_order_found = True\n\n                self.processing[\"next_group_order\"] = new_next_group_order\n                self.processing[\"current_group_order\"] = (\n                    new_current_group_order\n                )\n\n                # Force update to the current state\n                self._set_state_by_order()\n\n                if self.collect_state == 0:\n                    self.collect_state = 1\n                    self._current_state = (\n                        \"Ready\" if not self.errored else\n                        \"Collected, with errors\"\n                    )\n                    self.switch_toggleability.emit(True)\n                    self.passed_group.emit(current_group_order)\n                    yield IterationBreak(\"Collected\")\n\n                else:\n                    self.passed_group.emit(current_group_order)\n                    if self.errored:\n                        self._current_state = (\n                            \"Stopped, due to errors\" if not\n                            self.processing[\"stop_on_validation\"] else\n                            \"Validated, with errors\"\n                        )\n                        yield IterationBreak(\"Last group errored\")\n\n            if self.collect_state == 1:\n                self.collect_state = 2\n                self.switch_toggleability.emit(False)\n\n            if not self.validated and plugin.order > self.validators_order:\n                self.validated = True\n                if self.processing[\"stop_on_validation\"]:\n                    self._current_state = (\n                        \"Validated\" if not self.errored else\n                        \"Validated, with errors\"\n                    )\n                    yield IterationBreak(\"Validated\")\n\n            # Stop if was stopped\n            if self.stopped:\n                self.stopped = False\n                self._current_state = \"Paused\"\n                yield IterationBreak(\"Stopped\")\n\n            # check test if will stop\n            self.processing[\"nextOrder\"] = plugin.order\n            message = self.test(**self.processing)\n            if message:\n                self._current_state = \"Paused\"\n                yield IterationBreak(\"Stopped due to \\\"{}\\\"\".format(message))\n\n            self.processing[\"last_plugin_order\"] = plugin.order\n            if not plugin.active:\n                pyblish.logic.log.debug(\"%s was inactive, skipping..\" % plugin)\n                self.was_skipped.emit(plugin)\n                continue\n\n            in_collect_stage = self.collect_state == 0\n            if plugin.__instanceEnabled__:\n                instances = pyblish.logic.instances_by_plugin(\n                    self.context, plugin\n                )\n                if not instances:\n                    self.was_skipped.emit(plugin)\n                    continue\n\n                for instance in instances:\n                    if (\n                        not in_collect_stage\n                        and instance.data.get(\"publish\") is False\n                    ):\n                        pyblish.logic.log.debug(\n                            \"%s was inactive, skipping..\" % instance\n                        )\n                        continue\n                    # Stop if was stopped\n                    if self.stopped:\n                        self.stopped = False\n                        self._current_state = \"Paused\"\n                        yield IterationBreak(\"Stopped\")\n\n                    yield (plugin, instance)\n            else:\n                families = util.collect_families_from_instances(\n                    self.context, only_active=not in_collect_stage\n                )\n                plugins = pyblish.logic.plugins_by_families(\n                    [plugin], families\n                )\n                if not plugins:\n                    self.was_skipped.emit(plugin)\n                    continue\n                yield (plugin, None)\n\n        self.passed_group.emit(self.processing[\"next_group_order\"])\n\n    def iterate_and_process(self, on_finished=None):\n        \"\"\" Iterating inserted plugins with current context.\n        Collectors do not contain instances, they are None when collecting!\n        This process don't stop on one\n        \"\"\"\n        self._main_thread_processor.start()\n\n        def on_next():\n            self.log.debug(\"Looking for next pair to process\")\n            try:\n                self.current_pair = next(self.pair_generator)\n                if isinstance(self.current_pair, IterationBreak):\n                    raise self.current_pair\n\n            except IterationBreak:\n                self.log.debug(\"Iteration break was raised\")\n                self.is_running = False\n                self.was_stopped.emit()\n                self._main_thread_processor.stop()\n                return\n\n            except StopIteration:\n                self.log.debug(\"Iteration stop was raised\")\n                self.is_running = False\n                # All pairs were processed successfully!\n                if on_finished is not None:\n                    self._main_thread_processor.add_item(\n                        MainThreadItem(on_finished)\n                    )\n                self._main_thread_processor.stop_if_empty()\n                return\n\n            except Exception as exc:\n                self.log.warning(\n                    \"Unexpected exception during `on_next` happened\",\n                    exc_info=True\n                )\n                exc_msg = str(exc)\n                self._main_thread_processor.add_item(\n                    MainThreadItem(on_unexpected_error, error=exc_msg)\n                )\n                return\n\n            self.about_to_process.emit(*self.current_pair)\n            self._main_thread_processor.add_item(\n                MainThreadItem(on_process)\n            )\n\n        def on_process():\n            try:\n                self.log.debug(\n                    \"Processing pair: {}\".format(str(self.current_pair))\n                )\n                result = self._process(*self.current_pair)\n                if result[\"error\"] is not None:\n                    self.log.debug(\"Error happened\")\n                    self.errored = True\n\n                self.log.debug(\"Pair processed\")\n                self.was_processed.emit(result)\n\n            except Exception as exc:\n                self.log.warning(\n                    \"Unexpected exception during `on_process` happened\",\n                    exc_info=True\n                )\n                exc_msg = str(exc)\n                self._main_thread_processor.add_item(\n                    MainThreadItem(on_unexpected_error, error=exc_msg)\n                )\n                return\n\n            self._main_thread_processor.add_item(\n                MainThreadItem(on_next)\n            )\n\n        def on_unexpected_error(error):\n            # TODO this should be handled much differently\n            # TODO emit crash signal to show message box with traceback?\n            self.is_running = False\n            self.was_stopped.emit()\n            util.u_print(u\"An unexpected error occurred:\\n %s\" % error)\n            if on_finished is not None:\n                self._main_thread_processor.add_item(\n                    MainThreadItem(on_finished)\n                )\n            self._main_thread_processor.stop_if_empty()\n\n        self.is_running = True\n        self._main_thread_processor.add_item(\n            MainThreadItem(on_next)\n        )\n\n    def _set_state_by_order(self):\n        order = self.processing[\"current_group_order\"]\n        self._current_state = self.order_groups.groups[order][\"state\"]\n\n    def collect(self):\n        \"\"\" Iterate and process Collect plugins\n        - load_plugins method is launched again when finished\n        \"\"\"\n        self._set_state_by_order()\n        self._main_thread_processor.process(self._start_collect)\n        self._main_thread_processor.start()\n\n    def validate(self):\n        \"\"\" Process plugins to validations_order value.\"\"\"\n        self._set_state_by_order()\n        self._main_thread_processor.process(self._start_validate)\n        self._main_thread_processor.start()\n\n    def publish(self):\n        \"\"\" Iterate and process all remaining plugins.\"\"\"\n        self._set_state_by_order()\n        self._main_thread_processor.process(self._start_publish)\n        self._main_thread_processor.start()\n\n    def _start_collect(self):\n        self.iterate_and_process()\n\n    def _start_validate(self):\n        self.processing[\"stop_on_validation\"] = True\n        self.iterate_and_process()\n\n    def _start_publish(self):\n        self.processing[\"stop_on_validation\"] = False\n        self.iterate_and_process(self.on_published)\n\n    def cleanup(self):\n        \"\"\"Forcefully delete objects from memory\n        In an ideal world, this shouldn't be necessary. Garbage\n        collection guarantees that anything without reference\n        is automatically removed.\n        However, because this application is designed to be run\n        multiple times from the same interpreter process, extra\n        case must be taken to ensure there are no memory leaks.\n        Explicitly deleting objects shines a light on where objects\n        may still be referenced in the form of an error. No errors\n        means this was unnecessary, but that's ok.\n        \"\"\"\n\n        for instance in self.context:\n            del(instance)\n\n        for plugin in self.plugins:\n            del(plugin)\n\n    def _on_instance_toggled(self, instance, old_value, new_value):\n        callbacks = pyblish.api.registered_callbacks().get(\"instanceToggled\")\n        if not callbacks:\n            return\n\n        for callback in callbacks:\n            try:\n                callback(instance, old_value, new_value)\n            except Exception:\n                self.log.warning(\n                    \"Callback for `instanceToggled` crashed. {}\".format(\n                        os.path.abspath(inspect.getfile(callback))\n                    ),\n                    exc_info=True\n                )\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/delegate.py",
    "content": "import platform\n\nfrom qtpy import QtWidgets, QtGui, QtCore\n\nfrom . import model\nfrom .awesome import tags as awesome\nfrom .constants import (\n    PluginStates, InstanceStates, PluginActionStates, Roles, EXPANDER_WIDTH\n)\n\ncolors = {\n    \"error\": QtGui.QColor(\"#ff4a4a\"),\n    \"warning\": QtGui.QColor(\"#ff9900\"),\n    \"ok\": QtGui.QColor(\"#77AE24\"),\n    \"active\": QtGui.QColor(\"#99CEEE\"),\n    \"idle\": QtCore.Qt.white,\n    \"inactive\": QtGui.QColor(\"#888\"),\n    \"hover\": QtGui.QColor(255, 255, 255, 10),\n    \"selected\": QtGui.QColor(255, 255, 255, 20),\n    \"outline\": QtGui.QColor(\"#333\"),\n    \"group\": QtGui.QColor(\"#333\"),\n    \"group-hover\": QtGui.QColor(\"#3c3c3c\"),\n    \"group-selected-hover\": QtGui.QColor(\"#555555\"),\n    \"expander-bg\": QtGui.QColor(\"#222\"),\n    \"expander-hover\": QtGui.QColor(\"#2d6c9f\"),\n    \"expander-selected-hover\": QtGui.QColor(\"#3784c5\")\n}\n\nscale_factors = {\"darwin\": 1.5}\nscale_factor = scale_factors.get(platform.system().lower(), 1.0)\nfonts = {\n    \"h3\": QtGui.QFont(\"Open Sans\", 10 * scale_factor, QtGui.QFont.Normal),\n    \"h4\": QtGui.QFont(\"Open Sans\", 8 * scale_factor, QtGui.QFont.Normal),\n    \"h5\": QtGui.QFont(\"Open Sans\", 8 * scale_factor, QtGui.QFont.DemiBold),\n    \"awesome6\": QtGui.QFont(\"FontAwesome\", 6 * scale_factor),\n    \"awesome10\": QtGui.QFont(\"FontAwesome\", 10 * scale_factor),\n    \"smallAwesome\": QtGui.QFont(\"FontAwesome\", 8 * scale_factor),\n    \"largeAwesome\": QtGui.QFont(\"FontAwesome\", 16 * scale_factor),\n}\nfont_metrics = {\n    \"awesome6\": QtGui.QFontMetrics(fonts[\"awesome6\"]),\n    \"h4\": QtGui.QFontMetrics(fonts[\"h4\"]),\n    \"h5\": QtGui.QFontMetrics(fonts[\"h5\"])\n}\nicons = {\n    \"action\": awesome[\"adn\"],\n    \"angle-right\": awesome[\"angle-right\"],\n    \"angle-left\": awesome[\"angle-left\"],\n    \"plus-sign\": awesome['plus'],\n    \"minus-sign\": awesome['minus']\n}\n\n\nclass PluginItemDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Generic delegate for model items\"\"\"\n\n    def paint(self, painter, option, index):\n        \"\"\"Paint checkbox and text.\n         _\n        |_|  My label    >\n        \"\"\"\n\n        body_rect = QtCore.QRectF(option.rect)\n\n        check_rect = QtCore.QRectF(body_rect)\n        check_rect.setWidth(check_rect.height())\n        check_offset = (check_rect.height() / 4) + 1\n        check_rect.adjust(\n            check_offset, check_offset, -check_offset, -check_offset\n        )\n\n        check_color = colors[\"idle\"]\n\n        perspective_icon = icons[\"angle-right\"]\n        perspective_rect = QtCore.QRectF(body_rect)\n        perspective_rect.setWidth(perspective_rect.height())\n        perspective_rect.adjust(0, 3, 0, 0)\n        perspective_rect.translate(\n            body_rect.width() - (perspective_rect.width() / 2 + 2),\n            0\n        )\n\n        publish_states = index.data(Roles.PublishFlagsRole)\n        if publish_states & PluginStates.InProgress:\n            check_color = colors[\"active\"]\n\n        elif publish_states & PluginStates.HasError:\n            check_color = colors[\"error\"]\n\n        elif publish_states & PluginStates.HasWarning:\n            check_color = colors[\"warning\"]\n\n        elif publish_states & PluginStates.WasProcessed:\n            check_color = colors[\"ok\"]\n\n        elif not index.data(Roles.IsEnabledRole):\n            check_color = colors[\"inactive\"]\n\n        offset = (body_rect.height() - font_metrics[\"h4\"].height()) / 2\n        label_rect = QtCore.QRectF(body_rect.adjusted(\n            check_rect.width() + 12, offset - 1, 0, 0\n        ))\n\n        assert label_rect.width() > 0\n\n        label = index.data(QtCore.Qt.DisplayRole)\n        label = font_metrics[\"h4\"].elidedText(\n            label,\n            QtCore.Qt.ElideRight,\n            label_rect.width() - 20\n        )\n\n        font_color = colors[\"idle\"]\n        if not index.data(QtCore.Qt.CheckStateRole):\n            font_color = colors[\"inactive\"]\n\n        # Maintain reference to state, so we can restore it once we're done\n        painter.save()\n\n        # Draw perspective icon\n        painter.setFont(fonts[\"awesome10\"])\n        painter.setPen(QtGui.QPen(font_color))\n        painter.drawText(perspective_rect, perspective_icon)\n\n        # Draw label\n        painter.setFont(fonts[\"h4\"])\n        painter.setPen(QtGui.QPen(font_color))\n        painter.drawText(label_rect, label)\n\n        # Draw action icon\n        if index.data(Roles.PluginActionsVisibleRole):\n            painter.save()\n            action_state = index.data(Roles.PluginActionProgressRole)\n            if action_state & PluginActionStates.HasFailed:\n                color = colors[\"error\"]\n            elif action_state & PluginActionStates.HasFinished:\n                color = colors[\"ok\"]\n            elif action_state & PluginActionStates.InProgress:\n                color = colors[\"active\"]\n            else:\n                color = colors[\"idle\"]\n\n            painter.setFont(fonts[\"smallAwesome\"])\n            painter.setPen(QtGui.QPen(color))\n\n            icon_rect = QtCore.QRectF(\n                option.rect.adjusted(\n                    label_rect.width() - perspective_rect.width() / 2,\n                    label_rect.height() / 3, 0, 0\n                )\n            )\n            painter.drawText(icon_rect, icons[\"action\"])\n\n            painter.restore()\n\n        # Draw checkbox\n        pen = QtGui.QPen(check_color, 1)\n        painter.setPen(pen)\n\n        if index.data(Roles.IsOptionalRole):\n            painter.drawRect(check_rect)\n\n            if index.data(QtCore.Qt.CheckStateRole):\n                optional_check_rect = QtCore.QRectF(check_rect)\n                optional_check_rect.adjust(2, 2, -1, -1)\n                painter.fillRect(optional_check_rect, check_color)\n\n        else:\n            painter.fillRect(check_rect, check_color)\n\n        if option.state & QtWidgets.QStyle.State_MouseOver:\n            painter.fillRect(body_rect, colors[\"hover\"])\n\n        if option.state & QtWidgets.QStyle.State_Selected:\n            painter.fillRect(body_rect, colors[\"selected\"])\n\n        # Ok, we're done, tidy up.\n        painter.restore()\n\n    def sizeHint(self, option, index):\n        return QtCore.QSize(option.rect.width(), 20)\n\n\nclass InstanceItemDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Generic delegate for model items\"\"\"\n\n    def paint(self, painter, option, index):\n        \"\"\"Paint checkbox and text.\n         _\n        |_|  My label    >\n        \"\"\"\n\n        body_rect = QtCore.QRectF(option.rect)\n\n        check_rect = QtCore.QRectF(body_rect)\n        check_rect.setWidth(check_rect.height())\n        offset = (check_rect.height() / 4) + 1\n        check_rect.adjust(offset, offset, -(offset), -(offset))\n\n        check_color = colors[\"idle\"]\n\n        perspective_icon = icons[\"angle-right\"]\n        perspective_rect = QtCore.QRectF(body_rect)\n        perspective_rect.setWidth(perspective_rect.height())\n        perspective_rect.adjust(0, 3, 0, 0)\n        perspective_rect.translate(\n            body_rect.width() - (perspective_rect.width() / 2 + 2),\n            0\n        )\n\n        publish_states = index.data(Roles.PublishFlagsRole)\n        if publish_states & InstanceStates.InProgress:\n            check_color = colors[\"active\"]\n\n        elif publish_states & InstanceStates.HasError:\n            check_color = colors[\"error\"]\n\n        elif publish_states & InstanceStates.HasWarning:\n            check_color = colors[\"warning\"]\n\n        elif publish_states & InstanceStates.HasFinished:\n            check_color = colors[\"ok\"]\n\n        elif not index.data(Roles.IsEnabledRole):\n            check_color = colors[\"inactive\"]\n\n        offset = (body_rect.height() - font_metrics[\"h4\"].height()) / 2\n        label_rect = QtCore.QRectF(body_rect.adjusted(\n            check_rect.width() + 12, offset - 1, 0, 0\n        ))\n\n        assert label_rect.width() > 0\n\n        label = index.data(QtCore.Qt.DisplayRole)\n        label = font_metrics[\"h4\"].elidedText(\n            label,\n            QtCore.Qt.ElideRight,\n            label_rect.width() - 20\n        )\n\n        font_color = colors[\"idle\"]\n        if not index.data(QtCore.Qt.CheckStateRole):\n            font_color = colors[\"inactive\"]\n\n        # Maintain reference to state, so we can restore it once we're done\n        painter.save()\n\n        # Draw perspective icon\n        painter.setFont(fonts[\"awesome10\"])\n        painter.setPen(QtGui.QPen(font_color))\n        painter.drawText(perspective_rect, perspective_icon)\n\n        # Draw label\n        painter.setFont(fonts[\"h4\"])\n        painter.setPen(QtGui.QPen(font_color))\n        painter.drawText(label_rect, label)\n\n        # Draw checkbox\n        pen = QtGui.QPen(check_color, 1)\n        painter.setPen(pen)\n\n        if index.data(Roles.IsOptionalRole):\n            painter.drawRect(check_rect)\n\n            if index.data(QtCore.Qt.CheckStateRole):\n                optional_check_rect = QtCore.QRectF(check_rect)\n                optional_check_rect.adjust(2, 2, -1, -1)\n                painter.fillRect(optional_check_rect, check_color)\n\n        else:\n            painter.fillRect(check_rect, check_color)\n\n        if option.state & QtWidgets.QStyle.State_MouseOver:\n            painter.fillRect(body_rect, colors[\"hover\"])\n\n        if option.state & QtWidgets.QStyle.State_Selected:\n            painter.fillRect(body_rect, colors[\"selected\"])\n\n        # Ok, we're done, tidy up.\n        painter.restore()\n\n    def sizeHint(self, option, index):\n        return QtCore.QSize(option.rect.width(), 20)\n\n\nclass InstanceDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Generic delegate for instance header\"\"\"\n\n    radius = 8.0\n\n    def __init__(self, parent):\n        super(InstanceDelegate, self).__init__(parent)\n        self.item_delegate = InstanceItemDelegate(parent)\n\n    def paint(self, painter, option, index):\n        if index.data(Roles.TypeRole) in (\n            model.InstanceType, model.PluginType\n        ):\n            self.item_delegate.paint(painter, option, index)\n            return\n\n        self.group_item_paint(painter, option, index)\n\n    def group_item_paint(self, painter, option, index):\n        \"\"\"Paint text\n         _\n        My label\n        \"\"\"\n        body_rect = QtCore.QRectF(option.rect)\n        bg_rect = QtCore.QRectF(\n            body_rect.left(), body_rect.top() + 1,\n            body_rect.width() - 5, body_rect.height() - 2\n        )\n\n        expander_rect = QtCore.QRectF(bg_rect)\n        expander_rect.setWidth(EXPANDER_WIDTH)\n\n        remainder_rect = QtCore.QRectF(\n            expander_rect.x() + expander_rect.width(),\n            expander_rect.y(),\n            bg_rect.width() - expander_rect.width(),\n            expander_rect.height()\n        )\n\n        width = float(expander_rect.width())\n        height = float(expander_rect.height())\n\n        x_pos = expander_rect.x()\n        y_pos = expander_rect.y()\n\n        x_radius = min(self.radius, width / 2)\n        y_radius = min(self.radius, height / 2)\n        x_radius2 = x_radius * 2\n        y_radius2 = y_radius * 2\n\n        expander_path = QtGui.QPainterPath()\n        expander_path.moveTo(x_pos, y_pos + y_radius)\n        expander_path.arcTo(\n            x_pos, y_pos,\n            x_radius2, y_radius2,\n            180.0, -90.0\n        )\n        expander_path.lineTo(x_pos + width, y_pos)\n        expander_path.lineTo(x_pos + width, y_pos + height)\n        expander_path.lineTo(x_pos + x_radius, y_pos + height)\n        expander_path.arcTo(\n            x_pos, y_pos + height - y_radius2,\n            x_radius2, y_radius2,\n            270.0, -90.0\n        )\n        expander_path.closeSubpath()\n\n        width = float(remainder_rect.width())\n        height = float(remainder_rect.height())\n        x_pos = remainder_rect.x()\n        y_pos = remainder_rect.y()\n\n        x_radius = min(self.radius, width / 2)\n        y_radius = min(self.radius, height / 2)\n        x_radius2 = x_radius * 2\n        y_radius2 = y_radius * 2\n\n        remainder_path = QtGui.QPainterPath()\n        remainder_path.moveTo(x_pos + width, y_pos + height - y_radius)\n        remainder_path.arcTo(\n            x_pos + width - x_radius2, y_pos + height - y_radius2,\n            x_radius2, y_radius2,\n            0.0, -90.0\n        )\n        remainder_path.lineTo(x_pos, y_pos + height)\n        remainder_path.lineTo(x_pos, y_pos)\n        remainder_path.lineTo(x_pos + width - x_radius, y_pos)\n        remainder_path.arcTo(\n            x_pos + width - x_radius2, y_pos,\n            x_radius2, y_radius2,\n            90.0, -90.0\n        )\n        remainder_path.closeSubpath()\n\n        painter.fillPath(expander_path, colors[\"expander-bg\"])\n        painter.fillPath(remainder_path, colors[\"group\"])\n\n        mouse_pos = option.widget.mapFromGlobal(QtGui.QCursor.pos())\n        selected = option.state & QtWidgets.QStyle.State_Selected\n        hovered = option.state & QtWidgets.QStyle.State_MouseOver\n\n        if selected and hovered:\n            if expander_rect.contains(mouse_pos):\n                painter.fillPath(\n                    expander_path, colors[\"expander-selected-hover\"]\n                )\n            else:\n                painter.fillPath(\n                    remainder_path, colors[\"group-selected-hover\"]\n                )\n\n        elif hovered:\n            if expander_rect.contains(mouse_pos):\n                painter.fillPath(expander_path, colors[\"expander-hover\"])\n            else:\n                painter.fillPath(remainder_path, colors[\"group-hover\"])\n\n        text_height = font_metrics[\"awesome6\"].height()\n        adjust_value = (expander_rect.height() - text_height) / 2\n        expander_rect.adjust(\n            adjust_value + 1.5, adjust_value - 0.5,\n            -adjust_value + 1.5, -adjust_value - 0.5\n        )\n\n        offset = (remainder_rect.height() - font_metrics[\"h5\"].height()) / 2\n        label_rect = QtCore.QRectF(remainder_rect.adjusted(\n            5, offset - 1, 0, 0\n        ))\n\n        expander_icon = icons[\"plus-sign\"]\n\n        expanded = self.parent().isExpanded(index)\n        if expanded:\n            expander_icon = icons[\"minus-sign\"]\n        label = index.data(QtCore.Qt.DisplayRole)\n        label = font_metrics[\"h5\"].elidedText(\n            label, QtCore.Qt.ElideRight, label_rect.width()\n        )\n\n        # Maintain reference to state, so we can restore it once we're done\n        painter.save()\n\n        painter.setFont(fonts[\"awesome6\"])\n        painter.setPen(QtGui.QPen(colors[\"idle\"]))\n        painter.drawText(expander_rect, QtCore.Qt.AlignCenter, expander_icon)\n\n        # Draw label\n        painter.setFont(fonts[\"h5\"])\n        painter.drawText(label_rect, label)\n\n        # Ok, we're done, tidy up.\n        painter.restore()\n\n    def sizeHint(self, option, index):\n        return QtCore.QSize(option.rect.width(), 20)\n\n\nclass PluginDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"Generic delegate for plugin header\"\"\"\n\n    def __init__(self, parent):\n        super(PluginDelegate, self).__init__(parent)\n        self.item_delegate = PluginItemDelegate(parent)\n\n    def paint(self, painter, option, index):\n        if index.data(Roles.TypeRole) in (\n            model.InstanceType, model.PluginType\n        ):\n            self.item_delegate.paint(painter, option, index)\n            return\n\n        self.group_item_paint(painter, option, index)\n\n    def group_item_paint(self, painter, option, index):\n        \"\"\"Paint text\n         _\n        My label\n        \"\"\"\n        body_rect = QtCore.QRectF(option.rect)\n        bg_rect = QtCore.QRectF(\n            body_rect.left(), body_rect.top() + 1,\n            body_rect.width() - 5, body_rect.height() - 2\n        )\n        radius = 8.0\n        bg_path = QtGui.QPainterPath()\n        bg_path.addRoundedRect(bg_rect, radius, radius)\n        hovered = option.state & QtWidgets.QStyle.State_MouseOver\n        selected = option.state & QtWidgets.QStyle.State_Selected\n        if hovered and selected:\n            painter.fillPath(bg_path, colors[\"group-selected-hover\"])\n        elif hovered:\n            painter.fillPath(bg_path, colors[\"group-hover\"])\n        else:\n            painter.fillPath(bg_path, colors[\"group\"])\n\n        expander_rect = QtCore.QRectF(bg_rect)\n        expander_rect.setWidth(expander_rect.height())\n        text_height = font_metrics[\"awesome6\"].height()\n        adjust_value = (expander_rect.height() - text_height) / 2\n        expander_rect.adjust(\n            adjust_value + 1.5, adjust_value - 0.5,\n            -adjust_value + 1.5, -adjust_value - 0.5\n        )\n\n        offset = (bg_rect.height() - font_metrics[\"h5\"].height()) / 2\n        label_rect = QtCore.QRectF(bg_rect.adjusted(\n            expander_rect.width() + 12, offset - 1, 0, 0\n        ))\n\n        assert label_rect.width() > 0\n\n        expander_icon = icons[\"plus-sign\"]\n\n        expanded = self.parent().isExpanded(index)\n        if expanded:\n            expander_icon = icons[\"minus-sign\"]\n        label = index.data(QtCore.Qt.DisplayRole)\n        label = font_metrics[\"h5\"].elidedText(\n            label, QtCore.Qt.ElideRight, label_rect.width()\n        )\n\n        # Maintain reference to state, so we can restore it once we're done\n        painter.save()\n\n        painter.setFont(fonts[\"awesome6\"])\n        painter.setPen(QtGui.QPen(colors[\"idle\"]))\n        painter.drawText(expander_rect, QtCore.Qt.AlignCenter, expander_icon)\n\n        # Draw label\n        painter.setFont(fonts[\"h5\"])\n        painter.drawText(label_rect, label)\n\n        # Ok, we're done, tidy up.\n        painter.restore()\n\n    def sizeHint(self, option, index):\n        return QtCore.QSize(option.rect.width(), 20)\n\n\nclass TerminalItem(QtWidgets.QStyledItemDelegate):\n    \"\"\"Delegate used exclusively for the Terminal\"\"\"\n\n    def paint(self, painter, option, index):\n        super(TerminalItem, self).paint(painter, option, index)\n        item_type = index.data(Roles.TypeRole)\n        if item_type == model.TerminalDetailType:\n            return\n\n        hover = QtGui.QPainterPath()\n        hover.addRect(QtCore.QRectF(option.rect).adjusted(0, 0, -1, -1))\n        if option.state & QtWidgets.QStyle.State_Selected:\n            painter.fillPath(hover, colors[\"selected\"])\n\n        if option.state & QtWidgets.QStyle.State_MouseOver:\n            painter.fillPath(hover, colors[\"hover\"])\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/font/opensans/LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/i18n/pyblish_lite.pro",
    "content": "SOURCES = ../window.py\nTRANSLATIONS = zh_CN.ts"
  },
  {
    "path": "openpype/tools/pyblish_pype/i18n/zh_CN.ts",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE TS><TS version=\"1.1\" language=\"zh_CN\">\n<context>\n    <name>Window</name>\n    <message>\n        <location filename=\"../window.py\" line=\"763\"/>\n        <source>Finishing up reset..</source>\n        <translation>完成重置..</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"228\"/>\n        <source>Comment..</source>\n        <translation>备注..</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"722\"/>\n        <source>Processing</source>\n        <translation>处理</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"877\"/>\n        <source>Stopped due to error(s), see Terminal.</source>\n        <translation>因错误终止, 请查看终端。</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"879\"/>\n        <source>Finished successfully!</source>\n        <translation>成功完成!</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"889\"/>\n        <source>About to reset..</source>\n        <translation>即将重置..</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"911\"/>\n        <source>Preparing validate..</source>\n        <translation>准备校验..</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"919\"/>\n        <source>Preparing publish..</source>\n        <translation>准备发布..</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"928\"/>\n        <source>Preparing</source>\n        <translation>准备</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"951\"/>\n        <source>Action prepared.</source>\n        <translation>动作已就绪。</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"969\"/>\n        <source>Cleaning up models..</source>\n        <translation>清理数据模型..</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"974\"/>\n        <source>Cleaning up terminal..</source>\n        <translation>清理终端..</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"978\"/>\n        <source>Cleaning up controller..</source>\n        <translation>清理控制器..</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"981\"/>\n        <source>All clean!</source>\n        <translation>清理完成!</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"982\"/>\n        <source>Good bye</source>\n        <translation>再见</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"993\"/>\n        <source>..as soon as processing is finished..</source>\n        <translation>..处理即将完成..</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"1008\"/>\n        <source>Stopping..</source>\n        <translation>正在停止..</translation>\n    </message>\n    <message>\n        <location filename=\"../window.py\" line=\"985\"/>\n        <source>Closing..</source>\n        <translation>正在关闭..</translation>\n    </message>\n</context>\n</TS>\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/mock.py",
    "content": "import os\nimport time\nimport subprocess\n\nimport pyblish.api\n\n\nclass MyAction(pyblish.api.Action):\n    label = \"My Action\"\n    on = \"processed\"\n\n    def process(self, context, plugin):\n        self.log.info(\"Running!\")\n\n\nclass MyOtherAction(pyblish.api.Action):\n    label = \"My Other Action\"\n\n    def process(self, context, plugin):\n        self.log.info(\"Running!\")\n\n\nclass CollectComment(pyblish.api.ContextPlugin):\n    \"\"\"This collector has a very long comment.\n\n    The idea is that this comment should either be elided, or word-\n    wrapped in the corresponding view.\n\n    \"\"\"\n\n    order = pyblish.api.CollectorOrder\n\n    def process(self, context):\n        context.data[\"comment\"] = \"\"\n\n\nclass MyCollector(pyblish.api.ContextPlugin):\n    label = \"My Collector\"\n    order = pyblish.api.CollectorOrder\n\n    def process(self, context):\n        context.create_instance(\"MyInstance 1\", families=[\"myFamily\"])\n        context.create_instance(\"MyInstance 2\", families=[\"myFamily 2\"])\n        context.create_instance(\n            \"MyInstance 3\",\n            families=[\"myFamily 2\"],\n            publish=False\n        )\n\n\nclass MyValidator(pyblish.api.InstancePlugin):\n    order = pyblish.api.ValidatorOrder\n    active = False\n    label = \"My Validator\"\n    actions = [MyAction,\n               MyOtherAction]\n\n    def process(self, instance):\n        self.log.info(\"Validating: %s\" % instance)\n\n\nclass MyExtractor(pyblish.api.InstancePlugin):\n    order = pyblish.api.ExtractorOrder\n    families = [\"myFamily\"]\n    label = \"My Extractor\"\n\n    def process(self, instance):\n        self.log.info(\"Extracting: %s\" % instance)\n\n\nclass CollectRenamed(pyblish.api.Collector):\n    def process(self, context):\n        i = context.create_instance(\"MyInstanceXYZ\", family=\"MyFamily\")\n        i.set_data(\"name\", \"My instance\")\n\n\nclass CollectNegatron(pyblish.api.Collector):\n    \"\"\"Negative collector adds Negatron\"\"\"\n\n    order = pyblish.api.Collector.order - 0.49\n\n    def process_context(self, context):\n        self.log.info(\"Collecting Negatron\")\n        context.create_instance(\"Negatron\", family=\"MyFamily\")\n\n\nclass CollectPositron(pyblish.api.Collector):\n    \"\"\"Positive collector adds Positron\"\"\"\n\n    order = pyblish.api.Collector.order + 0.49\n\n    def process_context(self, context):\n        self.log.info(\"Collecting Positron\")\n        context.create_instance(\"Positron\", family=\"MyFamily\")\n\n\nclass SelectInstances(pyblish.api.Selector):\n    \"\"\"Select debugging instances\n\n    These instances are part of the evil plan to destroy the world.\n    Be weary, be vigilant, be sexy.\n\n    \"\"\"\n\n    def process_context(self, context):\n        self.log.info(\"Selecting instances..\")\n\n        for instance in instances[:-1]:\n            name, data = instance[\"name\"], instance[\"data\"]\n            self.log.info(\"Selecting: %s\" % name)\n            instance = context.create_instance(name)\n\n            for key, value in data.items():\n                instance.set_data(key, value)\n\n\nclass SelectDiInstances(pyblish.api.Selector):\n    \"\"\"Select DI instances\"\"\"\n\n    name = \"Select Dependency Instances\"\n\n    def process(self, context):\n        name, data = instances[-1][\"name\"], instances[-1][\"data\"]\n        self.log.info(\"Selecting: %s\" % name)\n        instance = context.create_instance(name)\n\n        for key, value in data.items():\n            instance.set_data(key, value)\n\n\nclass SelectInstancesFailure(pyblish.api.Selector):\n    \"\"\"Select some instances, but fail before adding anything to the context.\n\n    That's right. I'm programmed to fail. Try me.\n\n    \"\"\"\n\n    __fail__ = True\n\n    def process_context(self, context):\n        self.log.warning(\"I'm about to fail\")\n        raise AssertionError(\"I was programmed to fail\")\n\n\nclass SelectInstances2(pyblish.api.Selector):\n    def process(self, context):\n        self.log.warning(\"I'm good\")\n\n\nclass ValidateNamespace(pyblish.api.Validator):\n    \"\"\"Namespaces must be orange\n\n    In case a namespace is not orange, report immediately to\n    your officer in charge, ask for a refund, do a backflip.\n\n    This has been an example of:\n\n    - A long doc-string\n    - With a list\n    - And plenty of newlines and tabs.\n\n    \"\"\"\n\n    families = [\"B\"]\n\n    def process(self, instance):\n        self.log.info(\"Validating the namespace of %s\" % instance.data(\"name\"))\n        self.log.info(\"\"\"And here's another message, quite long, in fact it's\ntoo long to be displayed in a single row of text.\nBut that's how we roll down here. It's got \\nnew lines\\nas well.\n\n- And lists\n- And more lists\n\n        \"\"\")\n\n\nclass ValidateContext(pyblish.api.Validator):\n    families = [\"A\", \"B\"]\n\n    def process_context(self, context):\n        self.log.info(\"Processing context..\")\n\n\nclass ValidateContextFailure(pyblish.api.Validator):\n    optional = True\n    families = [\"C\"]\n    __fail__ = True\n\n    def process_context(self, context):\n        self.log.info(\"About to fail..\")\n        raise AssertionError(\"\"\"I was programmed to fail\n\nThe reason I failed was because the sun was not aligned with the tides,\nand the moon is gray; not yellow. Try again when the moon is yellow.\"\"\")\n\n\nclass Validator1(pyblish.api.Validator):\n    \"\"\"Test of the order attribute\"\"\"\n    order = pyblish.api.Validator.order + 0.1\n    families = [\"A\"]\n\n    def process_instance(self, instance):\n        pass\n\n\nclass Validator2(pyblish.api.Validator):\n    order = pyblish.api.Validator.order + 0.2\n    families = [\"B\"]\n\n    def process_instance(self, instance):\n        pass\n\n\nclass Validator3(pyblish.api.Validator):\n    order = pyblish.api.Validator.order + 0.3\n    families = [\"B\"]\n\n    def process_instance(self, instance):\n        pass\n\n\nclass ValidateFailureMock(pyblish.api.Validator):\n    \"\"\"Plug-in that always fails\"\"\"\n    optional = True\n    order = pyblish.api.Validator.order + 0.1\n    families = [\"C\"]\n    __fail__ = True\n\n    def process_instance(self, instance):\n        self.log.debug(\"e = mc^2\")\n        self.log.info(\"About to fail..\")\n        self.log.warning(\"Failing.. soooon..\")\n        self.log.critical(\"Ok, you're done.\")\n        raise AssertionError(\"\"\"ValidateFailureMock was destined to fail..\n\nHere's some extended information about what went wrong.\n\nIt has quite the long string associated with it, including\na few newlines and a list.\n\n- Item 1\n- Item 2\n\n\"\"\")\n\n\nclass ValidateIsIncompatible(pyblish.api.Validator):\n    \"\"\"This plug-in should never appear..\"\"\"\n    requires = False  # This is invalid\n\n\nclass ValidateWithRepair(pyblish.api.Validator):\n    \"\"\"A validator with repair functionality\"\"\"\n    optional = True\n    families = [\"C\"]\n    __fail__ = True\n\n    def process_instance(self, instance):\n        raise AssertionError(\n            \"%s is invalid, try repairing it!\" % instance.name\n        )\n\n    def repair_instance(self, instance):\n        self.log.info(\"Attempting to repair..\")\n        self.log.info(\"Success!\")\n\n\nclass ValidateWithRepairFailure(pyblish.api.Validator):\n    \"\"\"A validator with repair functionality that fails\"\"\"\n    optional = True\n    families = [\"C\"]\n    __fail__ = True\n\n    def process_instance(self, instance):\n        raise AssertionError(\n            \"%s is invalid, try repairing it!\" % instance.name\n        )\n\n    def repair_instance(self, instance):\n        self.log.info(\"Attempting to repair..\")\n        raise AssertionError(\"Could not repair due to X\")\n\n\nclass ValidateWithVeryVeryVeryLongLongNaaaaame(pyblish.api.Validator):\n    \"\"\"A validator with repair functionality that fails\"\"\"\n    families = [\"A\"]\n\n\nclass ValidateWithRepairContext(pyblish.api.Validator):\n    \"\"\"A validator with repair functionality that fails\"\"\"\n    optional = True\n    families = [\"C\"]\n    __fail__ = True\n\n    def process_context(self, context):\n        raise AssertionError(\"Could not validate context, try repairing it\")\n\n    def repair_context(self, context):\n        self.log.info(\"Attempting to repair..\")\n        raise AssertionError(\"Could not repair\")\n\n\nclass ExtractAsMa(pyblish.api.Extractor):\n    \"\"\"Extract contents of each instance into .ma\n\n    Serialise scene using Maya's own facilities and then put\n    it on the hard-disk. Once complete, this plug-in relies\n    on a Conformer to put it in it's final location, as this\n    extractor merely positions it in the users local temp-\n    directory.\n\n    \"\"\"\n\n    optional = True\n    __expected__ = {\n        \"logCount\": \">=4\"\n    }\n\n    def process_instance(self, instance):\n        self.log.info(\"About to extract scene to .ma..\")\n        self.log.info(\"Extraction went well, now verifying the data..\")\n\n        if instance.name == \"Richard05\":\n            self.log.warning(\"You're almost running out of disk space!\")\n\n        self.log.info(\"About to finish up\")\n        self.log.info(\"Finished successfully\")\n\n\nclass ConformAsset(pyblish.api.Conformer):\n    \"\"\"Conform the world\n\n    Step 1: Conform all humans and Step 2: Conform all non-humans.\n    Once conforming has completed, rinse and repeat.\n\n    \"\"\"\n\n    optional = True\n\n    def process_instance(self, instance):\n        self.log.info(\"About to conform all humans..\")\n\n        if instance.name == \"Richard05\":\n            self.log.warning(\"Richard05 is a conformist!\")\n\n        self.log.info(\"About to conform all non-humans..\")\n        self.log.info(\"Conformed Successfully\")\n\n\nclass ValidateInstancesDI(pyblish.api.Validator):\n    \"\"\"Validate using the DI interface\"\"\"\n    families = [\"diFamily\"]\n\n    def process(self, instance):\n        self.log.info(\"Validating %s..\" % instance.data(\"name\"))\n\n\nclass ValidateDIWithRepair(pyblish.api.Validator):\n    families = [\"diFamily\"]\n    optional = True\n    __fail__ = True\n\n    def process(self, instance):\n        raise AssertionError(\"I was programmed to fail, for repair\")\n\n    def repair(self, instance):\n        self.log.info(\"Repairing %s\" % instance.data(\"name\"))\n\n\nclass ExtractInstancesDI(pyblish.api.Extractor):\n    \"\"\"Extract using the DI interface\"\"\"\n    families = [\"diFamily\"]\n\n    def process(self, instance):\n        self.log.info(\"Extracting %s..\" % instance.data(\"name\"))\n\n\nclass ValidateWithLabel(pyblish.api.Validator):\n    \"\"\"Validate using the DI interface\"\"\"\n    label = \"Validate with Label\"\n\n\nclass ValidateWithLongLabel(pyblish.api.Validator):\n    \"\"\"Validate using the DI interface\"\"\"\n    label = \"Validate with Loooooooooooooooooooooong Label\"\n\n\nclass SimplePlugin1(pyblish.api.Plugin):\n    \"\"\"Validate using the simple-plugin interface\"\"\"\n\n    def process(self):\n        self.log.info(\"I'm a simple plug-in, only processed once\")\n\n\nclass SimplePlugin2(pyblish.api.Plugin):\n    \"\"\"Validate using the simple-plugin interface\n\n    It doesn't have an order, and will likely end up *before* all\n    other plug-ins. (due to how sorted([1, 2, 3, None]) works)\n\n    \"\"\"\n\n    def process(self, context):\n        self.log.info(\"Processing the context, simply: %s\" % context)\n\n\nclass SimplePlugin3(pyblish.api.Plugin):\n    \"\"\"Simply process every instance\"\"\"\n\n    def process(self, instance):\n        self.log.info(\"Processing the instance, simply: %s\" % instance)\n\n\nclass ContextAction(pyblish.api.Action):\n    label = \"Context action\"\n\n    def process(self, context):\n        self.log.info(\"I have access to the context\")\n        self.log.info(\"Context.instances: %s\" % str(list(context)))\n\n\nclass FailingAction(pyblish.api.Action):\n    label = \"Failing action\"\n\n    def process(self):\n        self.log.info(\"About to fail..\")\n        raise Exception(\"I failed\")\n\n\nclass LongRunningAction(pyblish.api.Action):\n    label = \"Long-running action\"\n\n    def process(self):\n        self.log.info(\"Sleeping for 2 seconds..\")\n        time.sleep(2)\n        self.log.info(\"Ah, that's better\")\n\n\nclass IconAction(pyblish.api.Action):\n    label = \"Icon action\"\n    icon = \"crop\"\n\n    def process(self):\n        self.log.info(\"I have an icon\")\n\n\nclass PluginAction(pyblish.api.Action):\n    label = \"Plugin action\"\n\n    def process(self, plugin):\n        self.log.info(\"I have access to my parent plug-in\")\n        self.log.info(\"Which is %s\" % plugin.id)\n\n\nclass LaunchExplorerAction(pyblish.api.Action):\n    label = \"Open in Explorer\"\n    icon = \"folder-open\"\n\n    def process(self, context):\n        cwd = os.getcwd()\n        self.log.info(\"Opening %s in Explorer\" % cwd)\n        result = subprocess.call(\"start .\", cwd=cwd, shell=True)\n        self.log.debug(result)\n\n\nclass ProcessedAction(pyblish.api.Action):\n    label = \"Success action\"\n    icon = \"check\"\n    on = \"processed\"\n\n    def process(self):\n        self.log.info(\"I am only available on a successful plug-in\")\n\n\nclass FailedAction(pyblish.api.Action):\n    label = \"Failure action\"\n    icon = \"close\"\n    on = \"failed\"\n\n\nclass SucceededAction(pyblish.api.Action):\n    label = \"Success action\"\n    icon = \"check\"\n    on = \"succeeded\"\n\n    def process(self):\n        self.log.info(\"I am only available on a successful plug-in\")\n\n\nclass LongLabelAction(pyblish.api.Action):\n    label = \"An incredibly, incredicly looooon label. Very long.\"\n    icon = \"close\"\n\n\nclass BadEventAction(pyblish.api.Action):\n    label = \"Bad event action\"\n    on = \"not exist\"\n\n\nclass InactiveAction(pyblish.api.Action):\n    active = False\n\n\nclass PluginWithActions(pyblish.api.Validator):\n    optional = True\n    actions = [\n        pyblish.api.Category(\"General\"),\n        ContextAction,\n        FailingAction,\n        LongRunningAction,\n        IconAction,\n        PluginAction,\n        pyblish.api.Category(\"Empty\"),\n        pyblish.api.Category(\"OS\"),\n        LaunchExplorerAction,\n        pyblish.api.Separator,\n        FailedAction,\n        SucceededAction,\n        pyblish.api.Category(\"Debug\"),\n        BadEventAction,\n        InactiveAction,\n        LongLabelAction,\n        pyblish.api.Category(\"Empty\"),\n    ]\n\n    def process(self):\n        self.log.info(\"Ran PluginWithActions\")\n\n\nclass FailingPluginWithActions(pyblish.api.Validator):\n    optional = True\n    actions = [\n        FailedAction,\n        SucceededAction,\n    ]\n\n    def process(self):\n        raise Exception(\"I was programmed to fail\")\n\n\nclass ValidateDefaultOff(pyblish.api.Validator):\n    families = [\"A\", \"B\"]\n    active = False\n    optional = True\n\n    def process(self, instance):\n        self.log.info(\"Processing instance..\")\n\n\nclass ValidateWithHyperlinks(pyblish.api.Validator):\n    \"\"\"To learn about Pyblish\n\n    <a href=\"http://pyblish.com\">click here</a> (http://pyblish.com)\n\n    \"\"\"\n\n    families = [\"A\", \"B\"]\n\n    def process(self, instance):\n        self.log.info(\"Processing instance..\")\n\n        msg = \"To learn about Pyblish, <a href='http://pyblish.com'>\"\n        msg += \"click here</a> (http://pyblish.com)\"\n\n        self.log.info(msg)\n\n\nclass LongRunningCollector(pyblish.api.Collector):\n    \"\"\"I will take at least 2 seconds...\"\"\"\n    def process(self, context):\n        self.log.info(\"Sleeping for 2 seconds..\")\n        time.sleep(2)\n        self.log.info(\"Good morning\")\n\n\nclass LongRunningValidator(pyblish.api.Validator):\n    \"\"\"I will take at least 2 seconds...\"\"\"\n    def process(self, context):\n        self.log.info(\"Sleeping for 2 seconds..\")\n        time.sleep(2)\n        self.log.info(\"Good morning\")\n\n\nclass RearrangingPlugin(pyblish.api.ContextPlugin):\n    \"\"\"Sort plug-ins by family, and then reverse it\"\"\"\n    order = pyblish.api.CollectorOrder + 0.2\n\n    def process(self, context):\n        self.log.info(\"Reversing instances in the context..\")\n        context[:] = sorted(\n            context,\n            key=lambda i: i.data[\"family\"],\n            reverse=True\n        )\n        self.log.info(\"Reversed!\")\n\n\nclass InactiveInstanceCollectorPlugin(pyblish.api.InstancePlugin):\n    \"\"\"Special case of an InstancePlugin running as a Collector\"\"\"\n    order = pyblish.api.CollectorOrder + 0.1\n    active = False\n\n    def process(self, instance):\n        raise TypeError(\"I shouldn't have run in the first place\")\n\n\nclass CollectWithIcon(pyblish.api.ContextPlugin):\n    order = pyblish.api.CollectorOrder\n\n    def process(self, context):\n        instance = context.create_instance(\"With Icon\")\n        instance.data[\"icon\"] = \"play\"\n\n\ninstances = [\n    {\n        \"name\": \"Peter01\",\n        \"data\": {\n            \"family\": \"A\",\n            \"publish\": False\n        }\n    },\n    {\n        \"name\": \"Richard05\",\n        \"data\": {\n            \"family\": \"A\",\n        }\n    },\n    {\n        \"name\": \"Steven11\",\n        \"data\": {\n            \"family\": \"B\",\n        }\n    },\n    {\n        \"name\": \"Piraya12\",\n        \"data\": {\n            \"family\": \"B\",\n        }\n    },\n    {\n        \"name\": \"Marcus\",\n        \"data\": {\n            \"family\": \"C\",\n        }\n    },\n    {\n        \"name\": \"Extra1\",\n        \"data\": {\n            \"family\": \"C\",\n        }\n    },\n    {\n        \"name\": \"DependencyInstance\",\n        \"data\": {\n            \"family\": \"diFamily\"\n        }\n    },\n    {\n        \"name\": \"NoFamily\",\n        \"data\": {}\n    },\n    {\n        \"name\": \"Failure 1\",\n        \"data\": {\n            \"family\": \"failure\",\n            \"fail\": False\n        }\n    },\n    {\n        \"name\": \"Failure 2\",\n        \"data\": {\n            \"family\": \"failure\",\n            \"fail\": True\n        }\n    }\n]\n\nplugins = [\n    MyCollector,\n    MyValidator,\n    MyExtractor,\n\n    CollectRenamed,\n    CollectNegatron,\n    CollectPositron,\n    SelectInstances,\n    SelectInstances2,\n    SelectDiInstances,\n    SelectInstancesFailure,\n    ValidateFailureMock,\n    ValidateNamespace,\n    # ValidateIsIncompatible,\n    ValidateWithVeryVeryVeryLongLongNaaaaame,\n    ValidateContext,\n    ValidateContextFailure,\n    Validator1,\n    Validator2,\n    Validator3,\n    ValidateWithRepair,\n    ValidateWithRepairFailure,\n    ValidateWithRepairContext,\n    ValidateWithLabel,\n    ValidateWithLongLabel,\n    ValidateDefaultOff,\n    ValidateWithHyperlinks,\n    ExtractAsMa,\n    ConformAsset,\n\n    SimplePlugin1,\n    SimplePlugin2,\n    SimplePlugin3,\n\n    ValidateInstancesDI,\n    ExtractInstancesDI,\n    ValidateDIWithRepair,\n\n    PluginWithActions,\n    FailingPluginWithActions,\n\n    # LongRunningCollector,\n    # LongRunningValidator,\n\n    RearrangingPlugin,\n    InactiveInstanceCollectorPlugin,\n\n    CollectComment,\n    CollectWithIcon,\n]\n\npyblish.api.sort_plugins(plugins)\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/model.py",
    "content": "\"\"\"Qt models\n\nDescription:\n    The model contains the original objects from Pyblish, such as\n    pyblish.api.Instance and pyblish.api.Plugin. The model then\n    provides an interface for reading and writing to those.\n\nGUI data:\n    Aside from original data, such as pyblish.api.Plugin.optional,\n    the GUI also hosts data internal to itself, such as whether or\n    not an item has processed such that it may be colored appropriately\n    in the view. This data is prefixed with two underscores (__).\n\n    E.g.\n\n    _has_processed\n\n    This is so that the the GUI-only data doesn't accidentally overwrite\n    or cause confusion with existing data in plug-ins and instances.\n\nRoles:\n    Data is accessed via standard Qt \"roles\". You can think of a role\n    as the key of a dictionary, except they can only be integers.\n\n\"\"\"\nfrom __future__ import unicode_literals\n\nimport pyblish\n\nfrom . import settings, util\nfrom .awesome import tags as awesome\nfrom qtpy import QtCore, QtGui\nimport qtawesome\nfrom six import text_type\nfrom .constants import PluginStates, InstanceStates, GroupStates, Roles\n\nfrom openpype.settings import get_system_settings\n\n\n# ItemTypes\nUserType = QtGui.QStandardItem.UserType\nif hasattr(UserType, \"value\"):\n    UserType = UserType.value\nInstanceType = UserType\nPluginType = UserType + 1\nGroupType = UserType + 2\nTerminalLabelType = UserType + 3\nTerminalDetailType = UserType + 4\n\n\nclass QAwesomeTextIconFactory:\n    icons = {}\n\n    @classmethod\n    def icon(cls, icon_name):\n        if icon_name not in cls.icons:\n            cls.icons[icon_name] = awesome.get(icon_name)\n        return cls.icons[icon_name]\n\n\nclass QAwesomeIconFactory:\n    icons = {}\n\n    @classmethod\n    def icon(cls, icon_name, icon_color):\n        if icon_name not in cls.icons:\n            cls.icons[icon_name] = {}\n\n        if icon_color not in cls.icons[icon_name]:\n            cls.icons[icon_name][icon_color] = qtawesome.icon(\n                icon_name,\n                color=icon_color\n            )\n        return cls.icons[icon_name][icon_color]\n\n\nclass IntentModel(QtGui.QStandardItemModel):\n    \"\"\"Model for QComboBox with intents.\n\n    It is expected that one inserted item is dictionary.\n    Key represents #Label and Value represent #Value.\n\n    Example:\n    {\n        \"Testing\": \"test\",\n        \"Publishing\": \"publish\"\n    }\n\n    First and default value is {\"< Not Set >\": None}\n    \"\"\"\n\n    default_empty_label = \"< Not set >\"\n\n    def __init__(self, parent=None):\n        super(IntentModel, self).__init__(parent)\n        self._item_count = 0\n        self.default_index = 0\n\n    @property\n    def has_items(self):\n        return self._item_count > 0\n\n    def reset(self):\n        self.clear()\n        self._item_count = 0\n        self.default_index = 0\n\n        intent_settings = (\n            get_system_settings()\n            .get(\"modules\", {})\n            .get(\"ftrack\", {})\n            .get(\"intent\", {})\n        )\n\n        items = intent_settings.get(\"items\", {})\n        if not items:\n            return\n\n        allow_empty_intent = intent_settings.get(\"allow_empty_intent\", True)\n        empty_intent_label = (\n            intent_settings.get(\"empty_intent_label\")\n            or self.default_empty_label\n        )\n        listed_items = list(items.items())\n        if allow_empty_intent:\n            listed_items.insert(0, (\"\", empty_intent_label))\n\n        default = intent_settings.get(\"default\")\n\n        for idx, item in enumerate(listed_items):\n            item_value = item[0]\n            if item_value == default:\n                self.default_index = idx\n                break\n\n        self._add_items(listed_items)\n\n    def _add_items(self, items):\n        for item in items:\n            value, label = item\n            new_item = QtGui.QStandardItem()\n            new_item.setData(label, QtCore.Qt.DisplayRole)\n            new_item.setData(value, Roles.IntentItemValue)\n\n            self.setItem(self._item_count, new_item)\n            self._item_count += 1\n\n\nclass PluginItem(QtGui.QStandardItem):\n    \"\"\"Plugin item implementation.\"\"\"\n\n    def __init__(self, plugin):\n        super(PluginItem, self).__init__()\n\n        item_text = plugin.__name__\n        if settings.UseLabel:\n            if hasattr(plugin, \"label\") and plugin.label:\n                item_text = plugin.label\n\n        self.plugin = plugin\n\n        self.setData(item_text, QtCore.Qt.DisplayRole)\n        self.setData(False, Roles.IsEnabledRole)\n        self.setData(0, Roles.PublishFlagsRole)\n        self.setData(0, Roles.PluginActionProgressRole)\n        icon_name = \"\"\n        if hasattr(plugin, \"icon\") and plugin.icon:\n            icon_name = plugin.icon\n        icon = QAwesomeTextIconFactory.icon(icon_name)\n        self.setData(icon, QtCore.Qt.DecorationRole)\n\n        actions = []\n        if hasattr(plugin, \"actions\") and plugin.actions:\n            actions = list(plugin.actions)\n        plugin.actions = actions\n\n        is_checked = True\n        is_optional = getattr(plugin, \"optional\", False)\n        if is_optional:\n            is_checked = getattr(plugin, \"active\", True)\n\n        plugin.active = is_checked\n        plugin.optional = is_optional\n\n        self.setData(\n            \"{}.{}\".format(plugin.__module__, plugin.__name__),\n            Roles.ObjectUIdRole\n        )\n\n        self.setFlags(\n            QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled\n        )\n\n    def type(self):\n        return PluginType\n\n    def data(self, role=QtCore.Qt.DisplayRole):\n        if role == Roles.IsOptionalRole:\n            return self.plugin.optional\n\n        if role == Roles.ObjectIdRole:\n            return self.plugin.id\n\n        if role == Roles.TypeRole:\n            return self.type()\n\n        if role == QtCore.Qt.CheckStateRole:\n            return self.plugin.active\n\n        if role == Roles.PathModuleRole:\n            return self.plugin.__module__\n\n        if role == Roles.FamiliesRole:\n            return self.plugin.families\n\n        if role == Roles.DocstringRole:\n            return self.plugin.__doc__\n\n        if role == Roles.PluginActionsVisibleRole:\n            return self._data_actions_visible()\n\n        if role == Roles.PluginValidActionsRole:\n            return self._data_valid_actions()\n\n        return super(PluginItem, self).data(role)\n\n    def _data_actions_visible(self):\n        # Can only run actions on active plug-ins.\n        if not self.plugin.active or not self.plugin.actions:\n            return False\n\n        publish_states = self.data(Roles.PublishFlagsRole)\n        if (\n            not publish_states & PluginStates.IsCompatible\n            or publish_states & PluginStates.WasSkipped\n        ):\n            return False\n\n        # Context specific actions\n        for action in self.plugin.actions:\n            if action.on == \"failed\":\n                if publish_states & PluginStates.HasError:\n                    return True\n\n            elif action.on == \"succeeded\":\n                if (\n                    publish_states & PluginStates.WasProcessed\n                    and not publish_states & PluginStates.HasError\n                ):\n                    return True\n\n            elif action.on == \"processed\":\n                if publish_states & PluginStates.WasProcessed:\n                    return True\n\n            elif action.on == \"notProcessed\":\n                if not publish_states & PluginStates.WasProcessed:\n                    return True\n        return False\n\n    def _data_valid_actions(self):\n        valid_actions = []\n\n        # Can only run actions on active plug-ins.\n        if not self.plugin.active or not self.plugin.actions:\n            return valid_actions\n\n        if not self.plugin.active or not self.plugin.actions:\n            return False\n\n        publish_states = self.data(Roles.PublishFlagsRole)\n        if (\n            not publish_states & PluginStates.IsCompatible\n            or publish_states & PluginStates.WasSkipped\n        ):\n            return False\n\n        # Context specific actions\n        for action in self.plugin.actions:\n            valid = False\n            if action.on == \"failed\":\n                if publish_states & PluginStates.HasError:\n                    valid = True\n\n            elif action.on == \"succeeded\":\n                if (\n                    publish_states & PluginStates.WasProcessed\n                    and not publish_states & PluginStates.HasError\n                ):\n                    valid = True\n\n            elif action.on == \"processed\":\n                if publish_states & PluginStates.WasProcessed:\n                    valid = True\n\n            elif action.on == \"notProcessed\":\n                if not publish_states & PluginStates.WasProcessed:\n                    valid = True\n\n            if valid:\n                valid_actions.append(action)\n\n        if not valid_actions:\n            return valid_actions\n\n        actions_len = len(valid_actions)\n        # Discard empty groups\n        indexex_to_remove = []\n        for idx, action in enumerate(valid_actions):\n            if action.__type__ != \"category\":\n                continue\n\n            next_id = idx + 1\n            if next_id >= actions_len:\n                indexex_to_remove.append(idx)\n                continue\n\n            next = valid_actions[next_id]\n            if next.__type__ != \"action\":\n                indexex_to_remove.append(idx)\n\n        for idx in reversed(indexex_to_remove):\n            valid_actions.pop(idx)\n\n        return valid_actions\n\n    def setData(self, value, role=None):\n        if role is None:\n            role = QtCore.Qt.UserRole + 1\n\n        if role == QtCore.Qt.CheckStateRole:\n            if not self.data(Roles.IsEnabledRole):\n                return False\n            self.plugin.active = value\n            self.emitDataChanged()\n            return\n\n        elif role == Roles.PluginActionProgressRole:\n            if isinstance(value, list):\n                _value = self.data(Roles.PluginActionProgressRole)\n                for flag in value:\n                    _value |= flag\n                value = _value\n\n            elif isinstance(value, dict):\n                _value = self.data(Roles.PluginActionProgressRole)\n                for flag, _bool in value.items():\n                    if _bool is True:\n                        _value |= flag\n                    elif _value & flag:\n                        _value ^= flag\n                value = _value\n\n        elif role == Roles.PublishFlagsRole:\n            if isinstance(value, list):\n                _value = self.data(Roles.PublishFlagsRole)\n                for flag in value:\n                    _value |= flag\n                value = _value\n\n            elif isinstance(value, dict):\n                _value = self.data(Roles.PublishFlagsRole)\n                for flag, _bool in value.items():\n                    if _bool is True:\n                        _value |= flag\n                    elif _value & flag:\n                        _value ^= flag\n                value = _value\n\n            if value & PluginStates.HasWarning:\n                if self.parent():\n                    self.parent().setData(\n                        {GroupStates.HasWarning: True},\n                        Roles.PublishFlagsRole\n                    )\n            if value & PluginStates.HasError:\n                if self.parent():\n                    self.parent().setData(\n                        {GroupStates.HasError: True},\n                        Roles.PublishFlagsRole\n                    )\n\n        return super(PluginItem, self).setData(value, role)\n\n\nclass GroupItem(QtGui.QStandardItem):\n    def __init__(self, *args, **kwargs):\n        self.order = kwargs.pop(\"order\", None)\n        self.publish_states = 0\n        super(GroupItem, self).__init__(*args, **kwargs)\n\n    def flags(self):\n        return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled\n\n    def data(self, role=QtCore.Qt.DisplayRole):\n        if role == Roles.PublishFlagsRole:\n            return self.publish_states\n\n        if role == Roles.TypeRole:\n            return self.type()\n\n        return super(GroupItem, self).data(role)\n\n    def setData(self, value, role=(QtCore.Qt.UserRole + 1)):\n        if role == Roles.PublishFlagsRole:\n            if isinstance(value, list):\n                _value = self.data(Roles.PublishFlagsRole)\n                for flag in value:\n                    _value |= flag\n                value = _value\n\n            elif isinstance(value, dict):\n                _value = self.data(Roles.PublishFlagsRole)\n                for flag, _bool in value.items():\n                    if _bool is True:\n                        _value |= flag\n                    elif _value & flag:\n                        _value ^= flag\n                value = _value\n            self.publish_states = value\n            self.emitDataChanged()\n            return True\n\n        return super(GroupItem, self).setData(value, role)\n\n    def type(self):\n        return GroupType\n\n\nclass PluginModel(QtGui.QStandardItemModel):\n    def __init__(self, controller, *args, **kwargs):\n        super(PluginModel, self).__init__(*args, **kwargs)\n\n        self.controller = controller\n        self.checkstates = {}\n        self.group_items = {}\n        self.plugin_items = {}\n\n    def reset(self):\n        self.group_items = {}\n        self.plugin_items = {}\n        self.clear()\n\n    def append(self, plugin):\n        plugin_groups = self.controller.order_groups.groups\n        label = None\n        order = None\n        for _order, item in reversed(plugin_groups.items()):\n            if _order is None or plugin.order < _order:\n                label = item[\"label\"]\n                order = _order\n            else:\n                break\n\n        if label is None:\n            label = \"Other\"\n\n        group_item = self.group_items.get(label)\n        if not group_item:\n            group_item = GroupItem(label, order=order)\n            self.appendRow(group_item)\n            self.group_items[label] = group_item\n\n        new_item = PluginItem(plugin)\n        group_item.appendRow(new_item)\n\n        self.plugin_items[plugin._id] = new_item\n\n    def store_checkstates(self):\n        self.checkstates.clear()\n\n        for plugin_item in self.plugin_items.values():\n            if not plugin_item.plugin.optional:\n                continue\n\n            uid = plugin_item.data(Roles.ObjectUIdRole)\n            self.checkstates[uid] = plugin_item.data(QtCore.Qt.CheckStateRole)\n\n    def restore_checkstates(self):\n        for plugin_item in self.plugin_items.values():\n            if not plugin_item.plugin.optional:\n                continue\n\n            uid = plugin_item.data(Roles.ObjectUIdRole)\n            state = self.checkstates.get(uid)\n            if state is not None:\n                plugin_item.setData(state, QtCore.Qt.CheckStateRole)\n\n    def update_with_result(self, result):\n        plugin = result[\"plugin\"]\n        item = self.plugin_items[plugin.id]\n\n        new_flag_states = {\n            PluginStates.InProgress: False,\n            PluginStates.WasProcessed: True\n        }\n\n        publish_states = item.data(Roles.PublishFlagsRole)\n\n        has_warning = publish_states & PluginStates.HasWarning\n        new_records = result.get(\"records\") or []\n        if not has_warning:\n            for record in new_records:\n                level_no = record.get(\"levelno\")\n                if level_no and level_no >= 30:\n                    new_flag_states[PluginStates.HasWarning] = True\n                    break\n\n        if (\n            not publish_states & PluginStates.HasError\n            and not result[\"success\"]\n        ):\n            new_flag_states[PluginStates.HasError] = True\n\n        if not publish_states & PluginStates.IsCompatible:\n            new_flag_states[PluginStates.IsCompatible] = True\n\n        item.setData(new_flag_states, Roles.PublishFlagsRole)\n\n        records = item.data(Roles.LogRecordsRole) or []\n        records.extend(new_records)\n\n        item.setData(records, Roles.LogRecordsRole)\n\n        return item\n\n    def update_compatibility(self):\n        context = self.controller.context\n\n        families = util.collect_families_from_instances(context, True)\n        for plugin_item in self.plugin_items.values():\n            publish_states = plugin_item.data(Roles.PublishFlagsRole)\n            if (\n                publish_states & PluginStates.WasProcessed\n                or publish_states & PluginStates.WasSkipped\n            ):\n                continue\n\n            is_compatible = False\n            # A plugin should always show if it has processed.\n            if plugin_item.plugin.__instanceEnabled__:\n                compatible_instances = pyblish.logic.instances_by_plugin(\n                    context, plugin_item.plugin\n                )\n                for instance in context:\n                    if not instance.data.get(\"publish\"):\n                        continue\n\n                    if instance in compatible_instances:\n                        is_compatible = True\n                        break\n            else:\n                plugins = pyblish.logic.plugins_by_families(\n                    [plugin_item.plugin], families\n                )\n                if plugins:\n                    is_compatible = True\n\n            current_is_compatible = publish_states & PluginStates.IsCompatible\n            if (\n                (is_compatible and not current_is_compatible)\n                or (not is_compatible and current_is_compatible)\n            ):\n                new_flag = {\n                    PluginStates.IsCompatible: is_compatible\n                }\n                plugin_item.setData(new_flag, Roles.PublishFlagsRole)\n\n\nclass PluginFilterProxy(QtCore.QSortFilterProxyModel):\n    def filterAcceptsRow(self, source_row, source_parent):\n        index = self.sourceModel().index(source_row, 0, source_parent)\n        item_type = index.data(Roles.TypeRole)\n        if item_type != PluginType:\n            return True\n\n        publish_states = index.data(Roles.PublishFlagsRole)\n        if (\n            publish_states & PluginStates.WasSkipped\n            or not publish_states & PluginStates.IsCompatible\n        ):\n            return False\n        return True\n\n\nclass InstanceItem(QtGui.QStandardItem):\n    \"\"\"Instance item implementation.\"\"\"\n\n    def __init__(self, instance):\n        super(InstanceItem, self).__init__()\n\n        self.instance = instance\n        self.is_context = False\n        publish_states = getattr(instance, \"_publish_states\", 0)\n        if publish_states & InstanceStates.ContextType:\n            self.is_context = True\n\n        instance._publish_states = publish_states\n        instance._logs = []\n        instance.optional = getattr(instance, \"optional\", True)\n        instance.data[\"publish\"] = instance.data.get(\"publish\", True)\n\n        family = self.data(Roles.FamiliesRole)[0]\n        self.setData(\n            \"{}.{}\".format(family, self.instance.data[\"name\"]),\n            Roles.ObjectUIdRole\n        )\n\n    def flags(self):\n        return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled\n\n    def type(self):\n        return InstanceType\n\n    def data(self, role=QtCore.Qt.DisplayRole):\n        if role == QtCore.Qt.DisplayRole:\n            label = None\n            if settings.UseLabel:\n                label = self.instance.data.get(\"label\")\n\n            if not label:\n                if self.is_context:\n                    label = \"Context\"\n                else:\n                    label = self.instance.data[\"name\"]\n            return label\n\n        if role == QtCore.Qt.DecorationRole:\n            icon_name = self.instance.data.get(\"icon\") or \"file\"\n            return QAwesomeTextIconFactory.icon(icon_name)\n\n        if role == Roles.TypeRole:\n            return self.type()\n\n        if role == Roles.ObjectIdRole:\n            return self.instance.id\n\n        if role == Roles.FamiliesRole:\n            if self.is_context:\n                return [\"Context\"]\n\n            families = []\n            family = self.instance.data.get(\"family\")\n            if family:\n                families.append(family)\n\n            _families = self.instance.data.get(\"families\") or []\n            for _family in _families:\n                if _family not in families:\n                    families.append(_family)\n\n            return families\n\n        if role == Roles.IsOptionalRole:\n            return self.instance.optional\n\n        if role == QtCore.Qt.CheckStateRole:\n            return self.instance.data[\"publish\"]\n\n        if role == Roles.PublishFlagsRole:\n            return self.instance._publish_states\n\n        if role == Roles.LogRecordsRole:\n            return self.instance._logs\n\n        return super(InstanceItem, self).data(role)\n\n    def setData(self, value, role=(QtCore.Qt.UserRole + 1)):\n        if role == QtCore.Qt.CheckStateRole:\n            if not self.data(Roles.IsEnabledRole):\n                return\n            self.instance.data[\"publish\"] = value\n            self.emitDataChanged()\n            return\n\n        if role == Roles.IsEnabledRole:\n            if not self.instance.optional:\n                return\n\n        if role == Roles.PublishFlagsRole:\n            if isinstance(value, list):\n                _value = self.instance._publish_states\n                for flag in value:\n                    _value |= flag\n                value = _value\n\n            elif isinstance(value, dict):\n                _value = self.instance._publish_states\n                for flag, _bool in value.items():\n                    if _bool is True:\n                        _value |= flag\n                    elif _value & flag:\n                        _value ^= flag\n                value = _value\n\n            if value & InstanceStates.HasWarning:\n                if self.parent():\n                    self.parent().setData(\n                        {GroupStates.HasWarning: True},\n                        Roles.PublishFlagsRole\n                    )\n            if value & InstanceStates.HasError:\n                if self.parent():\n                    self.parent().setData(\n                        {GroupStates.HasError: True},\n                        Roles.PublishFlagsRole\n                    )\n\n            self.instance._publish_states = value\n            self.emitDataChanged()\n            return\n\n        if role == Roles.LogRecordsRole:\n            self.instance._logs = value\n            self.emitDataChanged()\n            return\n\n        return super(InstanceItem, self).setData(value, role)\n\n\nclass InstanceModel(QtGui.QStandardItemModel):\n\n    group_created = QtCore.Signal(QtCore.QModelIndex)\n\n    def __init__(self, controller, *args, **kwargs):\n        super(InstanceModel, self).__init__(*args, **kwargs)\n\n        self.controller = controller\n        self.checkstates = {}\n        self.group_items = {}\n        self.instance_items = {}\n\n    def reset(self):\n        self.group_items = {}\n        self.instance_items = {}\n        self.clear()\n\n    def append(self, instance):\n        new_item = InstanceItem(instance)\n        if new_item.is_context:\n            self.appendRow(new_item)\n        else:\n            families = new_item.data(Roles.FamiliesRole)\n            group_item = self.group_items.get(families[0])\n            if not group_item:\n                group_item = GroupItem(families[0])\n                self.appendRow(group_item)\n                self.group_items[families[0]] = group_item\n                self.group_created.emit(group_item.index())\n\n            group_item.appendRow(new_item)\n        instance_id = instance.id\n        self.instance_items[instance_id] = new_item\n\n    def remove(self, instance_id):\n        instance_item = self.instance_items.pop(instance_id)\n        parent_item = instance_item.parent()\n        parent_item.removeRow(instance_item.row())\n        if parent_item.rowCount():\n            return\n\n        self.group_items.pop(parent_item.data(QtCore.Qt.DisplayRole))\n        self.removeRow(parent_item.row())\n\n    def store_checkstates(self):\n        self.checkstates.clear()\n\n        for instance_item in self.instance_items.values():\n            if not instance_item.instance.optional:\n                continue\n\n            uid = instance_item.data(Roles.ObjectUIdRole)\n            self.checkstates[uid] = instance_item.data(\n                QtCore.Qt.CheckStateRole\n            )\n\n    def restore_checkstates(self):\n        for instance_item in self.instance_items.values():\n            if not instance_item.instance.optional:\n                continue\n\n            uid = instance_item.data(Roles.ObjectUIdRole)\n            state = self.checkstates.get(uid)\n            if state is not None:\n                instance_item.setData(state, QtCore.Qt.CheckStateRole)\n\n    def update_with_result(self, result):\n        instance = result[\"instance\"]\n        if instance is None:\n            instance_id = self.controller.context.id\n        else:\n            instance_id = instance.id\n\n        item = self.instance_items.get(instance_id)\n        if not item:\n            return\n\n        new_flag_states = {\n            InstanceStates.InProgress: False\n        }\n\n        publish_states = item.data(Roles.PublishFlagsRole)\n        has_warning = publish_states & InstanceStates.HasWarning\n        new_records = result.get(\"records\") or []\n        if not has_warning:\n            for record in new_records:\n                level_no = record.get(\"levelno\")\n                if level_no and level_no >= 30:\n                    new_flag_states[InstanceStates.HasWarning] = True\n                    break\n\n        if (\n            not publish_states & InstanceStates.HasError\n            and not result[\"success\"]\n        ):\n            new_flag_states[InstanceStates.HasError] = True\n\n        item.setData(new_flag_states, Roles.PublishFlagsRole)\n\n        records = item.data(Roles.LogRecordsRole) or []\n        records.extend(new_records)\n\n        item.setData(records, Roles.LogRecordsRole)\n\n        return item\n\n    def update_compatibility(self, context, instances):\n        families = util.collect_families_from_instances(context, True)\n        for plugin_item in self.plugin_items.values():\n            publish_states = plugin_item.data(Roles.PublishFlagsRole)\n            if (\n                publish_states & PluginStates.WasProcessed\n                or publish_states & PluginStates.WasSkipped\n            ):\n                continue\n\n            is_compatible = False\n            # A plugin should always show if it has processed.\n            if plugin_item.plugin.__instanceEnabled__:\n                compatibleInstances = pyblish.logic.instances_by_plugin(\n                    context, plugin_item.plugin\n                )\n                for instance in instances:\n                    if not instance.data.get(\"publish\"):\n                        continue\n\n                    if instance in compatibleInstances:\n                        is_compatible = True\n                        break\n            else:\n                plugins = pyblish.logic.plugins_by_families(\n                    [plugin_item.plugin], families\n                )\n                if plugins:\n                    is_compatible = True\n\n            current_is_compatible = publish_states & PluginStates.IsCompatible\n            if (\n                (is_compatible and not current_is_compatible)\n                or (not is_compatible and current_is_compatible)\n            ):\n                plugin_item.setData(\n                    {PluginStates.IsCompatible: is_compatible},\n                    Roles.PublishFlagsRole\n                )\n\n\nclass InstanceSortProxy(QtCore.QSortFilterProxyModel):\n    def __init__(self, *args, **kwargs):\n        super(InstanceSortProxy, self).__init__(*args, **kwargs)\n        # Do not care about lower/upper case\n        self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n    def lessThan(self, x_index, y_index):\n        x_type = x_index.data(Roles.TypeRole)\n        y_type = y_index.data(Roles.TypeRole)\n        if x_type != y_type:\n            if x_type == GroupType:\n                return False\n            return True\n        return super(InstanceSortProxy, self).lessThan(x_index, y_index)\n\n\nclass TerminalDetailItem(QtGui.QStandardItem):\n    key_label_record_map = (\n        (\"instance\", \"Instance\"),\n        (\"msg\", \"Message\"),\n        (\"name\", \"Plugin\"),\n        (\"pathname\", \"Path\"),\n        (\"lineno\", \"Line\"),\n        (\"traceback\", \"Traceback\"),\n        (\"levelname\", \"Level\"),\n        (\"threadName\", \"Thread\"),\n        (\"msecs\", \"Millis\")\n    )\n\n    def __init__(self, record_item):\n        self.record_item = record_item\n        self.msg = None\n        msg = record_item.get(\"msg\")\n        if msg is None:\n            msg = record_item[\"label\"].split(\"\\n\")[0]\n\n        super(TerminalDetailItem, self).__init__(msg)\n\n    def data(self, role=QtCore.Qt.DisplayRole):\n        if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):\n            if self.msg is None:\n                self.msg = self.compute_detail_text(self.record_item)\n            return self.msg\n        return super(TerminalDetailItem, self).data(role)\n\n    def compute_detail_text(self, item_data):\n        if item_data[\"type\"] == \"info\":\n            return item_data[\"label\"]\n\n        html_text = \"\"\n        for key, title in self.key_label_record_map:\n            if key not in item_data:\n                continue\n            value = item_data[key]\n            text = (\n                str(value)\n                .replace(\"<\", \"&#60;\")\n                .replace(\">\", \"&#62;\")\n                .replace('\\n', '<br/>')\n                .replace(' ', '&nbsp;')\n            )\n\n            title_tag = (\n                '<span style=\\\" font-size:8pt; font-weight:600;'\n                # ' background-color:#bbb; color:#333;\\\" >{}:</span> '\n                ' color:#fff;\\\" >{}:</span> '\n            ).format(title)\n\n            html_text += (\n                '<tr><td width=\"100%\" align=left>{}</td></tr>'\n                '<tr><td width=\"100%\">{}</td></tr>'\n            ).format(title_tag, text)\n\n        html_text = '<table width=\"100%\" cellspacing=\"3\">{}</table>'.format(\n            html_text\n        )\n        return html_text\n\n\nclass TerminalModel(QtGui.QStandardItemModel):\n    item_icon_name = {\n        \"info\": \"fa.info\",\n        \"record\": \"fa.circle\",\n        \"error\": \"fa.exclamation-triangle\",\n    }\n\n    item_icon_colors = {\n        \"info\": \"#ffffff\",\n        \"error\": \"#ff4a4a\",\n        \"log_debug\": \"#ff66e8\",\n        \"log_info\": \"#66abff\",\n        \"log_warning\": \"#ffba66\",\n        \"log_error\": \"#ff4d58\",\n        \"log_critical\": \"#ff4f75\",\n        None: \"#333333\"\n    }\n\n    level_to_record = (\n        (10, \"log_debug\"),\n        (20, \"log_info\"),\n        (30, \"log_warning\"),\n        (40, \"log_error\"),\n        (50, \"log_critical\")\n\n    )\n\n    def __init__(self, *args, **kwargs):\n        super(TerminalModel, self).__init__(*args, **kwargs)\n        self.reset()\n\n    def reset(self):\n        self.clear()\n\n    def prepare_records(self, result, suspend_logs):\n        prepared_records = []\n        instance_name = None\n        instance = result[\"instance\"]\n        if instance is not None:\n            instance_name = instance.data[\"name\"]\n\n        if not suspend_logs:\n            for record in result.get(\"records\") or []:\n                if isinstance(record, dict):\n                    record_item = record\n                else:\n                    record_item = {\n                        \"label\": text_type(record.msg),\n                        \"type\": \"record\",\n                        \"levelno\": record.levelno,\n                        \"threadName\": record.threadName,\n                        \"name\": record.name,\n                        \"filename\": record.filename,\n                        \"pathname\": record.pathname,\n                        \"lineno\": record.lineno,\n                        \"msg\": text_type(record.msg),\n                        \"msecs\": record.msecs,\n                        \"levelname\": record.levelname\n                    }\n\n                if instance_name is not None:\n                    record_item[\"instance\"] = instance_name\n\n                prepared_records.append(record_item)\n\n        error = result.get(\"error\")\n        if error:\n            fname, line_no, func, exc = error.traceback\n            error_item = {\n                \"label\": str(error),\n                \"type\": \"error\",\n                \"filename\": str(fname),\n                \"lineno\": str(line_no),\n                \"func\": str(func),\n                \"traceback\": error.formatted_traceback,\n            }\n\n            if instance_name is not None:\n                error_item[\"instance\"] = instance_name\n\n            prepared_records.append(error_item)\n\n        return prepared_records\n\n    def append(self, record_items):\n        all_record_items = []\n        for record_item in record_items:\n            record_type = record_item[\"type\"]\n            # Add error message to detail\n            if record_type == \"error\":\n                record_item[\"msg\"] = record_item[\"label\"]\n            terminal_item_type = None\n            if record_type == \"record\":\n                for level, _type in self.level_to_record:\n                    if level > record_item[\"levelno\"]:\n                        break\n                    terminal_item_type = _type\n\n            else:\n                terminal_item_type = record_type\n\n            icon_color = self.item_icon_colors.get(terminal_item_type)\n            icon_name = self.item_icon_name.get(record_type)\n\n            top_item_icon = None\n            if icon_color and icon_name:\n                top_item_icon = QAwesomeIconFactory.icon(icon_name, icon_color)\n\n            label = record_item[\"label\"].split(\"\\n\")[0]\n\n            top_item = QtGui.QStandardItem()\n            all_record_items.append(top_item)\n\n            detail_item = TerminalDetailItem(record_item)\n            top_item.appendRow(detail_item)\n\n            top_item.setData(TerminalLabelType, Roles.TypeRole)\n            top_item.setData(terminal_item_type, Roles.TerminalItemTypeRole)\n            top_item.setData(label, QtCore.Qt.DisplayRole)\n            top_item.setFlags(\n                QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled\n            )\n\n            if top_item_icon:\n                top_item.setData(top_item_icon, QtCore.Qt.DecorationRole)\n\n            detail_item.setData(TerminalDetailType, Roles.TypeRole)\n\n        self.invisibleRootItem().appendRows(all_record_items)\n\n    def update_with_result(self, result):\n        self.append(result[\"records\"])\n\n\nclass TerminalProxy(QtCore.QSortFilterProxyModel):\n    filter_buttons_checks = {\n        \"info\": settings.TerminalFilters.get(\"info\", True),\n        \"log_debug\": settings.TerminalFilters.get(\"log_debug\", True),\n        \"log_info\": settings.TerminalFilters.get(\"log_info\", True),\n        \"log_warning\": settings.TerminalFilters.get(\"log_warning\", True),\n        \"log_error\": settings.TerminalFilters.get(\"log_error\", True),\n        \"log_critical\": settings.TerminalFilters.get(\"log_critical\", True),\n        \"error\": settings.TerminalFilters.get(\"error\", True)\n    }\n\n    instances = []\n\n    def __init__(self, view, *args, **kwargs):\n        super(self.__class__, self).__init__(*args, **kwargs)\n        self.__class__.instances.append(self)\n        # Store parent because by own `QSortFilterProxyModel` has `parent`\n        # method not returning parent QObject in PySide and PyQt4\n        self.view = view\n\n    @classmethod\n    def change_filter(cls, name, value):\n        cls.filter_buttons_checks[name] = value\n\n        for instance in cls.instances:\n            try:\n                instance.invalidate()\n                if instance.view:\n                    instance.view.updateGeometry()\n\n            except RuntimeError:\n                # C++ Object was deleted\n                cls.instances.remove(instance)\n\n    def filterAcceptsRow(self, source_row, source_parent):\n        index = self.sourceModel().index(source_row, 0, source_parent)\n        item_type = index.data(Roles.TypeRole)\n        if not item_type == TerminalLabelType:\n            return True\n        terminal_item_type = index.data(Roles.TerminalItemTypeRole)\n        return self.__class__.filter_buttons_checks.get(\n            terminal_item_type, True\n        )\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/settings.py",
    "content": "from .util import env_variable_to_bool\n\n# Customize the window of the pyblish-lite window.\nWindowTitle = \"Pyblish\"\n\n# Customize whether to show label names for plugins.\nUseLabel = True\n\n# Customize which tab to start on. Possible choices are: \"artist\", \"overview\"\n# and \"terminal\".\nInitialTab = \"overview\"\n\n# Customize the window size.\nWindowSize = (430, 600)\n\nTerminalFilters = {\n    \"info\": True,\n    \"log_debug\": True,\n    \"log_info\": True,\n    \"log_warning\": True,\n    \"log_error\": True,\n    \"log_critical\": True,\n    \"traceback\": True,\n}\n\n# Allow animations in GUI\nAnimated = env_variable_to_bool(\"OPENPYPE_PYBLISH_ANIMATED\", True)\n\n# Print UI info message to console\nPrintInfo = env_variable_to_bool(\"OPENPYPE_PYBLISH_PRINT_INFO\", True)\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/util.py",
    "content": "from __future__ import (\n    absolute_import,\n    division,\n    print_function,\n    unicode_literals\n)\n\nimport os\nimport sys\nimport numbers\nimport copy\nimport collections\n\nfrom qtpy import QtCore\nfrom six import text_type\nimport pyblish.api\n\nroot = os.path.dirname(__file__)\n\n\ndef get_asset(*path):\n    \"\"\"Return path to asset, relative the install directory\n\n    Usage:\n        >>> path = get_asset(\"dir\", \"to\", \"asset.png\")\n        >>> path == os.path.join(root, \"dir\", \"to\", \"asset.png\")\n        True\n\n    Arguments:\n        path (str): One or more paths, to be concatenated\n\n    \"\"\"\n\n    return os.path.join(root, *path)\n\n\ndef defer(delay, func):\n    \"\"\"Append artificial delay to `func`\n\n    This aids in keeping the GUI responsive, but complicates logic\n    when producing tests. To combat this, the environment variable ensures\n    that every operation is synchonous.\n\n    Arguments:\n        delay (float): Delay multiplier; default 1, 0 means no delay\n        func (callable): Any callable\n\n    \"\"\"\n\n    delay *= float(os.getenv(\"PYBLISH_DELAY\", 1))\n    if delay > 0:\n        return QtCore.QTimer.singleShot(delay, func)\n    else:\n        return func()\n\n\ndef u_print(msg, **kwargs):\n    \"\"\"`print` with encoded unicode.\n\n    `print` unicode may cause UnicodeEncodeError\n    or non-readable result when `PYTHONIOENCODING` is not set.\n    this will fix it.\n\n    Arguments:\n        msg (unicode): Message to print.\n        **kwargs: Keyword argument for `print` function.\n    \"\"\"\n\n    if isinstance(msg, text_type):\n        encoding = None\n        try:\n            encoding = os.getenv('PYTHONIOENCODING', sys.stdout.encoding)\n        except AttributeError:\n            # `sys.stdout.encoding` may not exists.\n            pass\n        msg = msg.encode(encoding or 'utf-8', 'replace')\n    print(msg, **kwargs)\n\n\ndef collect_families_from_instances(instances, only_active=False):\n    all_families = set()\n    for instance in instances:\n        if only_active:\n            if instance.data.get(\"publish\") is False:\n                continue\n        family = instance.data.get(\"family\")\n        if family:\n            all_families.add(family)\n\n        families = instance.data.get(\"families\") or tuple()\n        for family in families:\n            all_families.add(family)\n\n    return list(all_families)\n\n\nclass OrderGroups:\n    validation_order = pyblish.api.ValidatorOrder + 0.5\n    groups = collections.OrderedDict((\n        (\n            pyblish.api.CollectorOrder + 0.5,\n            {\n                \"label\": \"Collect\",\n                \"state\": \"Collecting..\"\n            }\n        ),\n        (\n            pyblish.api.ValidatorOrder + 0.5,\n            {\n                \"label\": \"Validate\",\n                \"state\": \"Validating..\"\n            }\n        ),\n        (\n            pyblish.api.ExtractorOrder + 0.5,\n            {\n                \"label\": \"Extract\",\n                \"state\": \"Extracting..\"\n            }\n        ),\n        (\n            pyblish.api.IntegratorOrder + 0.5,\n            {\n                \"label\": \"Integrate\",\n                \"state\": \"Integrating..\"\n            }\n        ),\n        (\n            None,\n            {\n                \"label\": \"Other\",\n                \"state\": \"Finishing..\"\n            }\n        )\n    ))\n\n\ndef env_variable_to_bool(env_key, default=False):\n    \"\"\"Boolean based on environment variable value.\"\"\"\n    # TODO: move to pype lib\n    value = os.environ.get(env_key)\n    if value is not None:\n        value = value.lower()\n        if value in (\"true\", \"1\", \"yes\", \"on\"):\n            return True\n        elif value in (\"false\", \"0\", \"no\", \"off\"):\n            return False\n    return default\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/vendor/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/tools/pyblish_pype/vendor/qtawesome/__init__.py",
    "content": "\"\"\"\nqtawesome - use font-awesome in PyQt / PySide applications\n\nThis is a port to Python of the C++ QtAwesome library by Rick Blommers\n\"\"\"\nfrom .iconic_font import IconicFont, set_global_defaults\nfrom .animation import Pulse, Spin\nfrom ._version import version_info, __version__\n\n_resource = {'iconic': None, }\n\n\ndef _instance():\n    if _resource['iconic'] is None:\n        _resource['iconic'] = IconicFont(('fa', 'fontawesome-webfont.ttf', 'fontawesome-webfont-charmap.json'),\n                                         ('ei', 'elusiveicons-webfont.ttf', 'elusiveicons-webfont-charmap.json'))\n    return _resource['iconic']\n\n\ndef icon(*args, **kwargs):\n    return _instance().icon(*args, **kwargs)\n\n\ndef load_font(*args, **kwargs):\n    return _instance().load_font(*args, **kwargs)\n\n\ndef charmap(prefixed_name):\n    prefix, name = prefixed_name.split('.')\n    return _instance().charmap[prefix][name]\n\n\ndef font(*args, **kwargs):\n    return _instance().font(*args, **kwargs)\n\n\ndef set_defaults(**kwargs):\n    return set_global_defaults(**kwargs)\n\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/vendor/qtawesome/_version.py",
    "content": "version_info = (0, 3, 0, 'dev')\n__version__ = '.'.join(map(str, version_info))\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/vendor/qtawesome/animation.py",
    "content": "from qtpy import QtCore\n\n\nclass Spin:\n\n    def __init__(self, parent_widget, interval=10, step=1):\n        self.parent_widget = parent_widget\n        self.interval, self.step = interval, step\n        self.info = {}\n\n    def _update(self, parent_widget):\n        if self.parent_widget in self.info:\n            timer, angle, step = self.info[self.parent_widget]\n\n            if angle >= 360:\n                angle = 0\n\n            angle += step\n            self.info[parent_widget] = timer, angle, step\n            parent_widget.update()\n\n    def setup(self, icon_painter, painter, rect):\n\n        if self.parent_widget not in self.info:\n            timer = QtCore.QTimer()\n            timer.timeout.connect(lambda: self._update(self.parent_widget))\n            self.info[self.parent_widget] = [timer, 0, self.step]\n            timer.start(self.interval)\n        else:\n            timer, angle, self.step = self.info[self.parent_widget]\n            x_center = rect.width() * 0.5\n            y_center = rect.height() * 0.5\n            painter.translate(x_center, y_center)\n            painter.rotate(angle)\n            painter.translate(-x_center, -y_center)\n\n\nclass Pulse(Spin):\n\n    def __init__(self, parent_widget):\n        Spin.__init__(self, parent_widget, interval=300, step=45)\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont-charmap.json",
    "content": "{\n    \"address-book\": \"0xf102\",\n    \"address-book-alt\": \"0xf101\",\n    \"adjust\": \"0xf104\",\n    \"adjust-alt\": \"0xf103\",\n    \"adult\": \"0xf105\",\n    \"align-center\": \"0xf106\",\n    \"align-justify\": \"0xf107\",\n    \"align-left\": \"0xf108\",\n    \"align-right\": \"0xf109\",\n    \"arrow-down\": \"0xf10a\",\n    \"arrow-left\": \"0xf10b\",\n    \"arrow-right\": \"0xf10c\",\n    \"arrow-up\": \"0xf10d\",\n    \"asl\": \"0xf10e\",\n    \"asterisk\": \"0xf10f\",\n    \"backward\": \"0xf110\",\n    \"ban-circle\": \"0xf111\",\n    \"barcode\": \"0xf112\",\n    \"behance\": \"0xf113\",\n    \"bell\": \"0xf114\",\n    \"blind\": \"0xf115\",\n    \"blogger\": \"0xf116\",\n    \"bold\": \"0xf117\",\n    \"book\": \"0xf118\",\n    \"bookmark\": \"0xf11a\",\n    \"bookmark-empty\": \"0xf119\",\n    \"braille\": \"0xf11b\",\n    \"briefcase\": \"0xf11c\",\n    \"broom\": \"0xf11d\",\n    \"brush\": \"0xf11e\",\n    \"bulb\": \"0xf11f\",\n    \"bullhorn\": \"0xf120\",\n    \"calendar\": \"0xf122\",\n    \"calendar-sign\": \"0xf121\",\n    \"camera\": \"0xf123\",\n    \"car\": \"0xf124\",\n    \"caret-down\": \"0xf125\",\n    \"caret-left\": \"0xf126\",\n    \"caret-right\": \"0xf127\",\n    \"caret-up\": \"0xf128\",\n    \"cc\": \"0xf129\",\n    \"certificate\": \"0xf12a\",\n    \"check\": \"0xf12c\",\n    \"check-empty\": \"0xf12b\",\n    \"chevron-down\": \"0xf12d\",\n    \"chevron-left\": \"0xf12e\",\n    \"chevron-right\": \"0xf12f\",\n    \"chevron-up\": \"0xf130\",\n    \"child\": \"0xf131\",\n    \"circle-arrow-down\": \"0xf132\",\n    \"circle-arrow-left\": \"0xf133\",\n    \"circle-arrow-right\": \"0xf134\",\n    \"circle-arrow-up\": \"0xf135\",\n    \"cloud\": \"0xf137\",\n    \"cloud-alt\": \"0xf136\",\n    \"cog\": \"0xf139\",\n    \"cog-alt\": \"0xf138\",\n    \"cogs\": \"0xf13a\",\n    \"comment\": \"0xf13c\",\n    \"comment-alt\": \"0xf13b\",\n    \"compass\": \"0xf13e\",\n    \"compass-alt\": \"0xf13d\",\n    \"credit-card\": \"0xf13f\",\n    \"css\": \"0xf140\",\n    \"dashboard\": \"0xf141\",\n    \"delicious\": \"0xf142\",\n    \"deviantart\": \"0xf143\",\n    \"digg\": \"0xf144\",\n    \"download\": \"0xf146\",\n    \"download-alt\": \"0xf145\",\n    \"dribbble\": \"0xf147\",\n    \"edit\": \"0xf148\",\n    \"eject\": \"0xf149\",\n    \"envelope\": \"0xf14b\",\n    \"envelope-alt\": \"0xf14a\",\n    \"error\": \"0xf14d\",\n    \"error-alt\": \"0xf14c\",\n    \"eur\": \"0xf14e\",\n    \"exclamation-sign\": \"0xf14f\",\n    \"eye-close\": \"0xf150\",\n    \"eye-open\": \"0xf151\",\n    \"facebook\": \"0xf152\",\n    \"facetime-video\": \"0xf153\",\n    \"fast-backward\": \"0xf154\",\n    \"fast-forward\": \"0xf155\",\n    \"female\": \"0xf156\",\n    \"file\": \"0xf15c\",\n    \"file-alt\": \"0xf157\",\n    \"file-edit\": \"0xf159\",\n    \"file-edit-alt\": \"0xf158\",\n    \"file-new\": \"0xf15b\",\n    \"file-new-alt\": \"0xf15a\",\n    \"film\": \"0xf15d\",\n    \"filter\": \"0xf15e\",\n    \"fire\": \"0xf15f\",\n    \"flag\": \"0xf161\",\n    \"flag-alt\": \"0xf160\",\n    \"flickr\": \"0xf162\",\n    \"folder\": \"0xf166\",\n    \"folder-close\": \"0xf163\",\n    \"folder-open\": \"0xf164\",\n    \"folder-sign\": \"0xf165\",\n    \"font\": \"0xf167\",\n    \"fontsize\": \"0xf168\",\n    \"fork\": \"0xf169\",\n    \"forward\": \"0xf16b\",\n    \"forward-alt\": \"0xf16a\",\n    \"foursquare\": \"0xf16c\",\n    \"friendfeed\": \"0xf16e\",\n    \"friendfeed-rect\": \"0xf16d\",\n    \"fullscreen\": \"0xf16f\",\n    \"gbp\": \"0xf170\",\n    \"gift\": \"0xf171\",\n    \"github\": \"0xf173\",\n    \"github-text\": \"0xf172\",\n    \"glass\": \"0xf174\",\n    \"glasses\": \"0xf175\",\n    \"globe\": \"0xf177\",\n    \"globe-alt\": \"0xf176\",\n    \"googleplus\": \"0xf178\",\n    \"graph\": \"0xf17a\",\n    \"graph-alt\": \"0xf179\",\n    \"group\": \"0xf17c\",\n    \"group-alt\": \"0xf17b\",\n    \"guidedog\": \"0xf17d\",\n    \"hand-down\": \"0xf17e\",\n    \"hand-left\": \"0xf17f\",\n    \"hand-right\": \"0xf180\",\n    \"hand-up\": \"0xf181\",\n    \"hdd\": \"0xf182\",\n    \"headphones\": \"0xf183\",\n    \"hearing-impaired\": \"0xf184\",\n    \"heart\": \"0xf187\",\n    \"heart-alt\": \"0xf185\",\n    \"heart-empty\": \"0xf186\",\n    \"home\": \"0xf189\",\n    \"home-alt\": \"0xf188\",\n    \"hourglass\": \"0xf18a\",\n    \"idea\": \"0xf18c\",\n    \"idea-alt\": \"0xf18b\",\n    \"inbox\": \"0xf18f\",\n    \"inbox-alt\": \"0xf18d\",\n    \"inbox-box\": \"0xf18e\",\n    \"indent-left\": \"0xf190\",\n    \"indent-right\": \"0xf191\",\n    \"info-circle\": \"0xf192\",\n    \"instagram\": \"0xf193\",\n    \"iphone-home\": \"0xf194\",\n    \"italic\": \"0xf195\",\n    \"key\": \"0xf196\",\n    \"laptop\": \"0xf198\",\n    \"laptop-alt\": \"0xf197\",\n    \"lastfm\": \"0xf199\",\n    \"leaf\": \"0xf19a\",\n    \"lines\": \"0xf19b\",\n    \"link\": \"0xf19c\",\n    \"linkedin\": \"0xf19d\",\n    \"list\": \"0xf19f\",\n    \"list-alt\": \"0xf19e\",\n    \"livejournal\": \"0xf1a0\",\n    \"lock\": \"0xf1a2\",\n    \"lock-alt\": \"0xf1a1\",\n    \"magic\": \"0xf1a3\",\n    \"magnet\": \"0xf1a4\",\n    \"male\": \"0xf1a5\",\n    \"map-marker\": \"0xf1a7\",\n    \"map-marker-alt\": \"0xf1a6\",\n    \"mic\": \"0xf1a9\",\n    \"mic-alt\": \"0xf1a8\",\n    \"minus\": \"0xf1ab\",\n    \"minus-sign\": \"0xf1aa\",\n    \"move\": \"0xf1ac\",\n    \"music\": \"0xf1ad\",\n    \"myspace\": \"0xf1ae\",\n    \"network\": \"0xf1af\",\n    \"off\": \"0xf1b0\",\n    \"ok\": \"0xf1b3\",\n    \"ok-circle\": \"0xf1b1\",\n    \"ok-sign\": \"0xf1b2\",\n    \"opensource\": \"0xf1b4\",\n    \"paper-clip\": \"0xf1b6\",\n    \"paper-clip-alt\": \"0xf1b5\",\n    \"path\": \"0xf1b7\",\n    \"pause\": \"0xf1b9\",\n    \"pause-alt\": \"0xf1b8\",\n    \"pencil\": \"0xf1bb\",\n    \"pencil-alt\": \"0xf1ba\",\n    \"person\": \"0xf1bc\",\n    \"phone\": \"0xf1be\",\n    \"phone-alt\": \"0xf1bd\",\n    \"photo\": \"0xf1c0\",\n    \"photo-alt\": \"0xf1bf\",\n    \"picasa\": \"0xf1c1\",\n    \"picture\": \"0xf1c2\",\n    \"pinterest\": \"0xf1c3\",\n    \"plane\": \"0xf1c4\",\n    \"play\": \"0xf1c7\",\n    \"play-alt\": \"0xf1c5\",\n    \"play-circle\": \"0xf1c6\",\n    \"plurk\": \"0xf1c9\",\n    \"plurk-alt\": \"0xf1c8\",\n    \"plus\": \"0xf1cb\",\n    \"plus-sign\": \"0xf1ca\",\n    \"podcast\": \"0xf1cc\",\n    \"print\": \"0xf1cd\",\n    \"puzzle\": \"0xf1ce\",\n    \"qrcode\": \"0xf1cf\",\n    \"question\": \"0xf1d1\",\n    \"question-sign\": \"0xf1d0\",\n    \"quote-alt\": \"0xf1d2\",\n    \"quote-right\": \"0xf1d4\",\n    \"quote-right-alt\": \"0xf1d3\",\n    \"quotes\": \"0xf1d5\",\n    \"random\": \"0xf1d6\",\n    \"record\": \"0xf1d7\",\n    \"reddit\": \"0xf1d8\",\n    \"redux\": \"0xf1d9\",\n    \"refresh\": \"0xf1da\",\n    \"remove\": \"0xf1dd\",\n    \"remove-circle\": \"0xf1db\",\n    \"remove-sign\": \"0xf1dc\",\n    \"repeat\": \"0xf1df\",\n    \"repeat-alt\": \"0xf1de\",\n    \"resize-full\": \"0xf1e0\",\n    \"resize-horizontal\": \"0xf1e1\",\n    \"resize-small\": \"0xf1e2\",\n    \"resize-vertical\": \"0xf1e3\",\n    \"return-key\": \"0xf1e4\",\n    \"retweet\": \"0xf1e5\",\n    \"reverse-alt\": \"0xf1e6\",\n    \"road\": \"0xf1e7\",\n    \"rss\": \"0xf1e8\",\n    \"scissors\": \"0xf1e9\",\n    \"screen\": \"0xf1eb\",\n    \"screen-alt\": \"0xf1ea\",\n    \"screenshot\": \"0xf1ec\",\n    \"search\": \"0xf1ee\",\n    \"search-alt\": \"0xf1ed\",\n    \"share\": \"0xf1f0\",\n    \"share-alt\": \"0xf1ef\",\n    \"shopping-cart\": \"0xf1f2\",\n    \"shopping-cart-sign\": \"0xf1f1\",\n    \"signal\": \"0xf1f3\",\n    \"skype\": \"0xf1f4\",\n    \"slideshare\": \"0xf1f5\",\n    \"smiley\": \"0xf1f7\",\n    \"smiley-alt\": \"0xf1f6\",\n    \"soundcloud\": \"0xf1f8\",\n    \"speaker\": \"0xf1f9\",\n    \"spotify\": \"0xf1fa\",\n    \"stackoverflow\": \"0xf1fb\",\n    \"star\": \"0xf1fe\",\n    \"star-alt\": \"0xf1fc\",\n    \"star-empty\": \"0xf1fd\",\n    \"step-backward\": \"0xf1ff\",\n    \"step-forward\": \"0xf200\",\n    \"stop\": \"0xf202\",\n    \"stop-alt\": \"0xf201\",\n    \"stumbleupon\": \"0xf203\",\n    \"tag\": \"0xf204\",\n    \"tags\": \"0xf205\",\n    \"tasks\": \"0xf206\",\n    \"text-height\": \"0xf207\",\n    \"text-width\": \"0xf208\",\n    \"th\": \"0xf20b\",\n    \"th-large\": \"0xf209\",\n    \"th-list\": \"0xf20a\",\n    \"thumbs-down\": \"0xf20c\",\n    \"thumbs-up\": \"0xf20d\",\n    \"time\": \"0xf20f\",\n    \"time-alt\": \"0xf20e\",\n    \"tint\": \"0xf210\",\n    \"torso\": \"0xf211\",\n    \"trash\": \"0xf213\",\n    \"trash-alt\": \"0xf212\",\n    \"tumblr\": \"0xf214\",\n    \"twitter\": \"0xf215\",\n    \"universal-access\": \"0xf216\",\n    \"unlock\": \"0xf218\",\n    \"unlock-alt\": \"0xf217\",\n    \"upload\": \"0xf219\",\n    \"usd\": \"0xf21a\",\n    \"user\": \"0xf21b\",\n    \"viadeo\": \"0xf21c\",\n    \"video\": \"0xf21f\",\n    \"video-alt\": \"0xf21d\",\n    \"video-chat\": \"0xf21e\",\n    \"view-mode\": \"0xf220\",\n    \"vimeo\": \"0xf221\",\n    \"vkontakte\": \"0xf222\",\n    \"volume-down\": \"0xf223\",\n    \"volume-off\": \"0xf224\",\n    \"volume-up\": \"0xf225\",\n    \"w3c\": \"0xf226\",\n    \"warning-sign\": \"0xf227\",\n    \"website\": \"0xf229\",\n    \"website-alt\": \"0xf228\",\n    \"wheelchair\": \"0xf22a\",\n    \"wordpress\": \"0xf22b\",\n    \"wrench\": \"0xf22d\",\n    \"wrench-alt\": \"0xf22c\",\n    \"youtube\": \"0xf22e\",\n    \"zoom-in\": \"0xf22f\",\n    \"zoom-out\": \"0xf230\"\n}\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont-charmap.json",
    "content": "{\n    \"500px\": \"f26e\", \n    \"adjust\": \"f042\", \n    \"adn\": \"f170\", \n    \"align-center\": \"f037\", \n    \"align-justify\": \"f039\", \n    \"align-left\": \"f036\", \n    \"align-right\": \"f038\", \n    \"amazon\": \"f270\", \n    \"ambulance\": \"f0f9\", \n    \"anchor\": \"f13d\", \n    \"android\": \"f17b\", \n    \"angellist\": \"f209\", \n    \"angle-double-down\": \"f103\", \n    \"angle-double-left\": \"f100\", \n    \"angle-double-right\": \"f101\", \n    \"angle-double-up\": \"f102\", \n    \"angle-down\": \"f107\", \n    \"angle-left\": \"f104\", \n    \"angle-right\": \"f105\", \n    \"angle-up\": \"f106\", \n    \"apple\": \"f179\", \n    \"archive\": \"f187\", \n    \"area-chart\": \"f1fe\", \n    \"arrow-circle-down\": \"f0ab\", \n    \"arrow-circle-left\": \"f0a8\", \n    \"arrow-circle-o-down\": \"f01a\", \n    \"arrow-circle-o-left\": \"f190\", \n    \"arrow-circle-o-right\": \"f18e\", \n    \"arrow-circle-o-up\": \"f01b\", \n    \"arrow-circle-right\": \"f0a9\", \n    \"arrow-circle-up\": \"f0aa\", \n    \"arrow-down\": \"f063\", \n    \"arrow-left\": \"f060\", \n    \"arrow-right\": \"f061\", \n    \"arrow-up\": \"f062\", \n    \"arrows\": \"f047\", \n    \"arrows-alt\": \"f0b2\", \n    \"arrows-h\": \"f07e\", \n    \"arrows-v\": \"f07d\", \n    \"asterisk\": \"f069\", \n    \"at\": \"f1fa\", \n    \"automobile\": \"f1b9\", \n    \"backward\": \"f04a\", \n    \"balance-scale\": \"f24e\", \n    \"ban\": \"f05e\", \n    \"bank\": \"f19c\", \n    \"bar-chart\": \"f080\", \n    \"bar-chart-o\": \"f080\", \n    \"barcode\": \"f02a\", \n    \"bars\": \"f0c9\", \n    \"battery-0\": \"f244\", \n    \"battery-1\": \"f243\", \n    \"battery-2\": \"f242\", \n    \"battery-3\": \"f241\", \n    \"battery-4\": \"f240\", \n    \"battery-empty\": \"f244\", \n    \"battery-full\": \"f240\", \n    \"battery-half\": \"f242\", \n    \"battery-quarter\": \"f243\", \n    \"battery-three-quarters\": \"f241\", \n    \"bed\": \"f236\", \n    \"beer\": \"f0fc\", \n    \"behance\": \"f1b4\", \n    \"behance-square\": \"f1b5\", \n    \"bell\": \"f0f3\", \n    \"bell-o\": \"f0a2\", \n    \"bell-slash\": \"f1f6\", \n    \"bell-slash-o\": \"f1f7\", \n    \"bicycle\": \"f206\", \n    \"binoculars\": \"f1e5\", \n    \"birthday-cake\": \"f1fd\", \n    \"bitbucket\": \"f171\", \n    \"bitbucket-square\": \"f172\", \n    \"bitcoin\": \"f15a\", \n    \"black-tie\": \"f27e\", \n    \"bluetooth\": \"f293\", \n    \"bluetooth-b\": \"f294\", \n    \"bold\": \"f032\", \n    \"bolt\": \"f0e7\", \n    \"bomb\": \"f1e2\", \n    \"book\": \"f02d\", \n    \"bookmark\": \"f02e\", \n    \"bookmark-o\": \"f097\", \n    \"briefcase\": \"f0b1\", \n    \"btc\": \"f15a\", \n    \"bug\": \"f188\", \n    \"building\": \"f1ad\", \n    \"building-o\": \"f0f7\", \n    \"bullhorn\": \"f0a1\", \n    \"bullseye\": \"f140\", \n    \"bus\": \"f207\", \n    \"buysellads\": \"f20d\", \n    \"cab\": \"f1ba\", \n    \"calculator\": \"f1ec\", \n    \"calendar\": \"f073\", \n    \"calendar-check-o\": \"f274\", \n    \"calendar-minus-o\": \"f272\", \n    \"calendar-o\": \"f133\", \n    \"calendar-plus-o\": \"f271\", \n    \"calendar-times-o\": \"f273\", \n    \"camera\": \"f030\", \n    \"camera-retro\": \"f083\", \n    \"car\": \"f1b9\", \n    \"caret-down\": \"f0d7\", \n    \"caret-left\": \"f0d9\", \n    \"caret-right\": \"f0da\", \n    \"caret-square-o-down\": \"f150\", \n    \"caret-square-o-left\": \"f191\", \n    \"caret-square-o-right\": \"f152\", \n    \"caret-square-o-up\": \"f151\", \n    \"caret-up\": \"f0d8\", \n    \"cart-arrow-down\": \"f218\", \n    \"cart-plus\": \"f217\", \n    \"cc\": \"f20a\", \n    \"cc-amex\": \"f1f3\", \n    \"cc-diners-club\": \"f24c\", \n    \"cc-discover\": \"f1f2\", \n    \"cc-jcb\": \"f24b\", \n    \"cc-mastercard\": \"f1f1\", \n    \"cc-paypal\": \"f1f4\", \n    \"cc-stripe\": \"f1f5\", \n    \"cc-visa\": \"f1f0\", \n    \"certificate\": \"f0a3\", \n    \"chain\": \"f0c1\", \n    \"chain-broken\": \"f127\", \n    \"check\": \"f00c\", \n    \"check-circle\": \"f058\", \n    \"check-circle-o\": \"f05d\", \n    \"check-square\": \"f14a\", \n    \"check-square-o\": \"f046\", \n    \"chevron-circle-down\": \"f13a\", \n    \"chevron-circle-left\": \"f137\", \n    \"chevron-circle-right\": \"f138\", \n    \"chevron-circle-up\": \"f139\", \n    \"chevron-down\": \"f078\", \n    \"chevron-left\": \"f053\", \n    \"chevron-right\": \"f054\", \n    \"chevron-up\": \"f077\", \n    \"child\": \"f1ae\", \n    \"chrome\": \"f268\", \n    \"circle\": \"f111\", \n    \"circle-o\": \"f10c\", \n    \"circle-o-notch\": \"f1ce\", \n    \"circle-thin\": \"f1db\", \n    \"clipboard\": \"f0ea\", \n    \"clock-o\": \"f017\", \n    \"clone\": \"f24d\", \n    \"close\": \"f00d\", \n    \"cloud\": \"f0c2\", \n    \"cloud-download\": \"f0ed\", \n    \"cloud-upload\": \"f0ee\", \n    \"cny\": \"f157\", \n    \"code\": \"f121\", \n    \"code-fork\": \"f126\", \n    \"codepen\": \"f1cb\", \n    \"codiepie\": \"f284\", \n    \"coffee\": \"f0f4\", \n    \"cog\": \"f013\", \n    \"cogs\": \"f085\", \n    \"columns\": \"f0db\", \n    \"comment\": \"f075\", \n    \"comment-o\": \"f0e5\", \n    \"commenting\": \"f27a\", \n    \"commenting-o\": \"f27b\", \n    \"comments\": \"f086\", \n    \"comments-o\": \"f0e6\", \n    \"compass\": \"f14e\", \n    \"compress\": \"f066\", \n    \"connectdevelop\": \"f20e\", \n    \"contao\": \"f26d\", \n    \"copy\": \"f0c5\", \n    \"copyright\": \"f1f9\", \n    \"creative-commons\": \"f25e\", \n    \"credit-card\": \"f09d\", \n    \"credit-card-alt\": \"f283\", \n    \"crop\": \"f125\", \n    \"crosshairs\": \"f05b\", \n    \"css3\": \"f13c\", \n    \"cube\": \"f1b2\", \n    \"cubes\": \"f1b3\", \n    \"cut\": \"f0c4\", \n    \"cutlery\": \"f0f5\", \n    \"dashboard\": \"f0e4\", \n    \"dashcube\": \"f210\", \n    \"database\": \"f1c0\", \n    \"dedent\": \"f03b\", \n    \"delicious\": \"f1a5\", \n    \"desktop\": \"f108\", \n    \"deviantart\": \"f1bd\", \n    \"diamond\": \"f219\", \n    \"digg\": \"f1a6\", \n    \"dollar\": \"f155\", \n    \"dot-circle-o\": \"f192\", \n    \"download\": \"f019\", \n    \"dribbble\": \"f17d\", \n    \"dropbox\": \"f16b\", \n    \"drupal\": \"f1a9\", \n    \"edge\": \"f282\", \n    \"edit\": \"f044\", \n    \"eject\": \"f052\", \n    \"ellipsis-h\": \"f141\", \n    \"ellipsis-v\": \"f142\", \n    \"empire\": \"f1d1\", \n    \"envelope\": \"f0e0\", \n    \"envelope-o\": \"f003\", \n    \"envelope-square\": \"f199\", \n    \"eraser\": \"f12d\", \n    \"eur\": \"f153\", \n    \"euro\": \"f153\", \n    \"exchange\": \"f0ec\", \n    \"exclamation\": \"f12a\", \n    \"exclamation-circle\": \"f06a\", \n    \"exclamation-triangle\": \"f071\", \n    \"expand\": \"f065\", \n    \"expeditedssl\": \"f23e\", \n    \"external-link\": \"f08e\", \n    \"external-link-square\": \"f14c\", \n    \"eye\": \"f06e\", \n    \"eye-slash\": \"f070\", \n    \"eyedropper\": \"f1fb\", \n    \"facebook\": \"f09a\", \n    \"facebook-f\": \"f09a\", \n    \"facebook-official\": \"f230\", \n    \"facebook-square\": \"f082\", \n    \"fast-backward\": \"f049\", \n    \"fast-forward\": \"f050\", \n    \"fax\": \"f1ac\", \n    \"feed\": \"f09e\", \n    \"female\": \"f182\", \n    \"fighter-jet\": \"f0fb\", \n    \"file\": \"f15b\", \n    \"file-archive-o\": \"f1c6\", \n    \"file-audio-o\": \"f1c7\", \n    \"file-code-o\": \"f1c9\", \n    \"file-excel-o\": \"f1c3\", \n    \"file-image-o\": \"f1c5\", \n    \"file-movie-o\": \"f1c8\", \n    \"file-o\": \"f016\", \n    \"file-pdf-o\": \"f1c1\", \n    \"file-photo-o\": \"f1c5\", \n    \"file-picture-o\": \"f1c5\", \n    \"file-powerpoint-o\": \"f1c4\", \n    \"file-sound-o\": \"f1c7\", \n    \"file-text\": \"f15c\", \n    \"file-text-o\": \"f0f6\", \n    \"file-video-o\": \"f1c8\", \n    \"file-word-o\": \"f1c2\", \n    \"file-zip-o\": \"f1c6\", \n    \"files-o\": \"f0c5\", \n    \"film\": \"f008\", \n    \"filter\": \"f0b0\", \n    \"fire\": \"f06d\", \n    \"fire-extinguisher\": \"f134\", \n    \"firefox\": \"f269\", \n    \"flag\": \"f024\", \n    \"flag-checkered\": \"f11e\", \n    \"flag-o\": \"f11d\", \n    \"flash\": \"f0e7\", \n    \"flask\": \"f0c3\", \n    \"flickr\": \"f16e\", \n    \"floppy-o\": \"f0c7\", \n    \"folder\": \"f07b\", \n    \"folder-o\": \"f114\", \n    \"folder-open\": \"f07c\", \n    \"folder-open-o\": \"f115\", \n    \"font\": \"f031\", \n    \"fonticons\": \"f280\", \n    \"fort-awesome\": \"f286\", \n    \"forumbee\": \"f211\", \n    \"forward\": \"f04e\", \n    \"foursquare\": \"f180\", \n    \"frown-o\": \"f119\", \n    \"futbol-o\": \"f1e3\", \n    \"gamepad\": \"f11b\", \n    \"gavel\": \"f0e3\", \n    \"gbp\": \"f154\", \n    \"ge\": \"f1d1\", \n    \"gear\": \"f013\", \n    \"gears\": \"f085\", \n    \"genderless\": \"f22d\", \n    \"get-pocket\": \"f265\", \n    \"gg\": \"f260\", \n    \"gg-circle\": \"f261\", \n    \"gift\": \"f06b\", \n    \"git\": \"f1d3\", \n    \"git-square\": \"f1d2\", \n    \"github\": \"f09b\", \n    \"github-alt\": \"f113\", \n    \"github-square\": \"f092\", \n    \"gittip\": \"f184\", \n    \"glass\": \"f000\", \n    \"globe\": \"f0ac\", \n    \"google\": \"f1a0\", \n    \"google-plus\": \"f0d5\", \n    \"google-plus-square\": \"f0d4\", \n    \"google-wallet\": \"f1ee\", \n    \"graduation-cap\": \"f19d\", \n    \"gratipay\": \"f184\", \n    \"group\": \"f0c0\", \n    \"h-square\": \"f0fd\", \n    \"hacker-news\": \"f1d4\", \n    \"hand-grab-o\": \"f255\", \n    \"hand-lizard-o\": \"f258\", \n    \"hand-o-down\": \"f0a7\", \n    \"hand-o-left\": \"f0a5\", \n    \"hand-o-right\": \"f0a4\", \n    \"hand-o-up\": \"f0a6\", \n    \"hand-paper-o\": \"f256\", \n    \"hand-peace-o\": \"f25b\", \n    \"hand-pointer-o\": \"f25a\", \n    \"hand-rock-o\": \"f255\", \n    \"hand-scissors-o\": \"f257\", \n    \"hand-spock-o\": \"f259\", \n    \"hand-stop-o\": \"f256\", \n    \"hashtag\": \"f292\", \n    \"hdd-o\": \"f0a0\", \n    \"header\": \"f1dc\", \n    \"headphones\": \"f025\", \n    \"heart\": \"f004\", \n    \"heart-o\": \"f08a\", \n    \"heartbeat\": \"f21e\", \n    \"history\": \"f1da\", \n    \"home\": \"f015\", \n    \"hospital-o\": \"f0f8\", \n    \"hotel\": \"f236\", \n    \"hourglass\": \"f254\", \n    \"hourglass-1\": \"f251\", \n    \"hourglass-2\": \"f252\", \n    \"hourglass-3\": \"f253\", \n    \"hourglass-end\": \"f253\", \n    \"hourglass-half\": \"f252\", \n    \"hourglass-o\": \"f250\", \n    \"hourglass-start\": \"f251\", \n    \"houzz\": \"f27c\", \n    \"html5\": \"f13b\", \n    \"i-cursor\": \"f246\", \n    \"ils\": \"f20b\", \n    \"image\": \"f03e\", \n    \"inbox\": \"f01c\", \n    \"indent\": \"f03c\", \n    \"industry\": \"f275\", \n    \"info\": \"f129\", \n    \"info-circle\": \"f05a\", \n    \"inr\": \"f156\", \n    \"instagram\": \"f16d\", \n    \"institution\": \"f19c\", \n    \"internet-explorer\": \"f26b\", \n    \"intersex\": \"f224\", \n    \"ioxhost\": \"f208\", \n    \"italic\": \"f033\", \n    \"joomla\": \"f1aa\", \n    \"jpy\": \"f157\", \n    \"jsfiddle\": \"f1cc\", \n    \"key\": \"f084\", \n    \"keyboard-o\": \"f11c\", \n    \"krw\": \"f159\", \n    \"language\": \"f1ab\", \n    \"laptop\": \"f109\", \n    \"lastfm\": \"f202\", \n    \"lastfm-square\": \"f203\", \n    \"leaf\": \"f06c\", \n    \"leanpub\": \"f212\", \n    \"legal\": \"f0e3\", \n    \"lemon-o\": \"f094\", \n    \"level-down\": \"f149\", \n    \"level-up\": \"f148\", \n    \"life-bouy\": \"f1cd\", \n    \"life-buoy\": \"f1cd\", \n    \"life-ring\": \"f1cd\", \n    \"life-saver\": \"f1cd\", \n    \"lightbulb-o\": \"f0eb\", \n    \"line-chart\": \"f201\", \n    \"link\": \"f0c1\", \n    \"linkedin\": \"f0e1\", \n    \"linkedin-square\": \"f08c\", \n    \"linux\": \"f17c\", \n    \"list\": \"f03a\", \n    \"list-alt\": \"f022\", \n    \"list-ol\": \"f0cb\", \n    \"list-ul\": \"f0ca\", \n    \"location-arrow\": \"f124\", \n    \"lock\": \"f023\", \n    \"long-arrow-down\": \"f175\", \n    \"long-arrow-left\": \"f177\", \n    \"long-arrow-right\": \"f178\", \n    \"long-arrow-up\": \"f176\", \n    \"magic\": \"f0d0\", \n    \"magnet\": \"f076\", \n    \"mail-forward\": \"f064\", \n    \"mail-reply\": \"f112\", \n    \"mail-reply-all\": \"f122\", \n    \"male\": \"f183\", \n    \"map\": \"f279\", \n    \"map-marker\": \"f041\", \n    \"map-o\": \"f278\", \n    \"map-pin\": \"f276\", \n    \"map-signs\": \"f277\", \n    \"mars\": \"f222\", \n    \"mars-double\": \"f227\", \n    \"mars-stroke\": \"f229\", \n    \"mars-stroke-h\": \"f22b\", \n    \"mars-stroke-v\": \"f22a\", \n    \"maxcdn\": \"f136\", \n    \"meanpath\": \"f20c\", \n    \"medium\": \"f23a\", \n    \"medkit\": \"f0fa\", \n    \"meh-o\": \"f11a\", \n    \"mercury\": \"f223\", \n    \"microphone\": \"f130\", \n    \"microphone-slash\": \"f131\", \n    \"minus\": \"f068\", \n    \"minus-circle\": \"f056\", \n    \"minus-square\": \"f146\", \n    \"minus-square-o\": \"f147\", \n    \"mixcloud\": \"f289\", \n    \"mobile\": \"f10b\", \n    \"mobile-phone\": \"f10b\", \n    \"modx\": \"f285\", \n    \"money\": \"f0d6\", \n    \"moon-o\": \"f186\", \n    \"mortar-board\": \"f19d\", \n    \"motorcycle\": \"f21c\", \n    \"mouse-pointer\": \"f245\", \n    \"music\": \"f001\", \n    \"navicon\": \"f0c9\", \n    \"neuter\": \"f22c\", \n    \"newspaper-o\": \"f1ea\", \n    \"object-group\": \"f247\", \n    \"object-ungroup\": \"f248\", \n    \"odnoklassniki\": \"f263\", \n    \"odnoklassniki-square\": \"f264\", \n    \"opencart\": \"f23d\", \n    \"openid\": \"f19b\", \n    \"opera\": \"f26a\", \n    \"optin-monster\": \"f23c\", \n    \"outdent\": \"f03b\", \n    \"pagelines\": \"f18c\", \n    \"paint-brush\": \"f1fc\", \n    \"paper-plane\": \"f1d8\", \n    \"paper-plane-o\": \"f1d9\", \n    \"paperclip\": \"f0c6\", \n    \"paragraph\": \"f1dd\", \n    \"paste\": \"f0ea\", \n    \"pause\": \"f04c\", \n    \"pause-circle\": \"f28b\", \n    \"pause-circle-o\": \"f28c\", \n    \"paw\": \"f1b0\", \n    \"paypal\": \"f1ed\", \n    \"pencil\": \"f040\", \n    \"pencil-square\": \"f14b\", \n    \"pencil-square-o\": \"f044\", \n    \"percent\": \"f295\", \n    \"phone\": \"f095\", \n    \"phone-square\": \"f098\", \n    \"photo\": \"f03e\", \n    \"picture-o\": \"f03e\", \n    \"pie-chart\": \"f200\", \n    \"pied-piper\": \"f1a7\", \n    \"pied-piper-alt\": \"f1a8\", \n    \"pinterest\": \"f0d2\", \n    \"pinterest-p\": \"f231\", \n    \"pinterest-square\": \"f0d3\", \n    \"plane\": \"f072\", \n    \"play\": \"f04b\", \n    \"play-circle\": \"f144\", \n    \"play-circle-o\": \"f01d\", \n    \"plug\": \"f1e6\", \n    \"plus\": \"f067\", \n    \"plus-circle\": \"f055\", \n    \"plus-square\": \"f0fe\", \n    \"plus-square-o\": \"f196\", \n    \"power-off\": \"f011\", \n    \"print\": \"f02f\", \n    \"product-hunt\": \"f288\", \n    \"puzzle-piece\": \"f12e\", \n    \"qq\": \"f1d6\", \n    \"qrcode\": \"f029\", \n    \"question\": \"f128\", \n    \"question-circle\": \"f059\", \n    \"quote-left\": \"f10d\", \n    \"quote-right\": \"f10e\", \n    \"ra\": \"f1d0\", \n    \"random\": \"f074\", \n    \"rebel\": \"f1d0\", \n    \"recycle\": \"f1b8\", \n    \"reddit\": \"f1a1\", \n    \"reddit-alien\": \"f281\", \n    \"reddit-square\": \"f1a2\", \n    \"refresh\": \"f021\", \n    \"registered\": \"f25d\", \n    \"remove\": \"f00d\", \n    \"renren\": \"f18b\", \n    \"reorder\": \"f0c9\", \n    \"repeat\": \"f01e\", \n    \"reply\": \"f112\", \n    \"reply-all\": \"f122\", \n    \"retweet\": \"f079\", \n    \"rmb\": \"f157\", \n    \"road\": \"f018\", \n    \"rocket\": \"f135\", \n    \"rotate-left\": \"f0e2\", \n    \"rotate-right\": \"f01e\", \n    \"rouble\": \"f158\", \n    \"rss\": \"f09e\", \n    \"rss-square\": \"f143\", \n    \"rub\": \"f158\", \n    \"ruble\": \"f158\", \n    \"rupee\": \"f156\", \n    \"safari\": \"f267\", \n    \"save\": \"f0c7\", \n    \"scissors\": \"f0c4\", \n    \"scribd\": \"f28a\", \n    \"search\": \"f002\", \n    \"search-minus\": \"f010\", \n    \"search-plus\": \"f00e\", \n    \"sellsy\": \"f213\", \n    \"send\": \"f1d8\", \n    \"send-o\": \"f1d9\", \n    \"server\": \"f233\", \n    \"share\": \"f064\", \n    \"share-alt\": \"f1e0\", \n    \"share-alt-square\": \"f1e1\", \n    \"share-square\": \"f14d\", \n    \"share-square-o\": \"f045\", \n    \"shekel\": \"f20b\", \n    \"sheqel\": \"f20b\", \n    \"shield\": \"f132\", \n    \"ship\": \"f21a\", \n    \"shirtsinbulk\": \"f214\", \n    \"shopping-bag\": \"f290\", \n    \"shopping-basket\": \"f291\", \n    \"shopping-cart\": \"f07a\", \n    \"sign-in\": \"f090\", \n    \"sign-out\": \"f08b\", \n    \"signal\": \"f012\", \n    \"simplybuilt\": \"f215\", \n    \"sitemap\": \"f0e8\", \n    \"skyatlas\": \"f216\", \n    \"skype\": \"f17e\", \n    \"slack\": \"f198\", \n    \"sliders\": \"f1de\", \n    \"slideshare\": \"f1e7\", \n    \"smile-o\": \"f118\", \n    \"soccer-ball-o\": \"f1e3\", \n    \"sort\": \"f0dc\", \n    \"sort-alpha-asc\": \"f15d\", \n    \"sort-alpha-desc\": \"f15e\", \n    \"sort-amount-asc\": \"f160\", \n    \"sort-amount-desc\": \"f161\", \n    \"sort-asc\": \"f0de\", \n    \"sort-desc\": \"f0dd\", \n    \"sort-down\": \"f0dd\", \n    \"sort-numeric-asc\": \"f162\", \n    \"sort-numeric-desc\": \"f163\", \n    \"sort-up\": \"f0de\", \n    \"soundcloud\": \"f1be\", \n    \"space-shuttle\": \"f197\", \n    \"spinner\": \"f110\", \n    \"spoon\": \"f1b1\", \n    \"spotify\": \"f1bc\", \n    \"square\": \"f0c8\", \n    \"square-o\": \"f096\", \n    \"stack-exchange\": \"f18d\", \n    \"stack-overflow\": \"f16c\", \n    \"star\": \"f005\", \n    \"star-half\": \"f089\", \n    \"star-half-empty\": \"f123\", \n    \"star-half-full\": \"f123\", \n    \"star-half-o\": \"f123\", \n    \"star-o\": \"f006\", \n    \"steam\": \"f1b6\", \n    \"steam-square\": \"f1b7\", \n    \"step-backward\": \"f048\", \n    \"step-forward\": \"f051\", \n    \"stethoscope\": \"f0f1\", \n    \"sticky-note\": \"f249\", \n    \"sticky-note-o\": \"f24a\", \n    \"stop\": \"f04d\", \n    \"stop-circle\": \"f28d\", \n    \"stop-circle-o\": \"f28e\", \n    \"street-view\": \"f21d\", \n    \"strikethrough\": \"f0cc\", \n    \"stumbleupon\": \"f1a4\", \n    \"stumbleupon-circle\": \"f1a3\", \n    \"subscript\": \"f12c\", \n    \"subway\": \"f239\", \n    \"suitcase\": \"f0f2\", \n    \"sun-o\": \"f185\", \n    \"superscript\": \"f12b\", \n    \"support\": \"f1cd\", \n    \"table\": \"f0ce\", \n    \"tablet\": \"f10a\", \n    \"tachometer\": \"f0e4\", \n    \"tag\": \"f02b\", \n    \"tags\": \"f02c\", \n    \"tasks\": \"f0ae\", \n    \"taxi\": \"f1ba\", \n    \"television\": \"f26c\", \n    \"tencent-weibo\": \"f1d5\", \n    \"terminal\": \"f120\", \n    \"text-height\": \"f034\", \n    \"text-width\": \"f035\", \n    \"th\": \"f00a\", \n    \"th-large\": \"f009\", \n    \"th-list\": \"f00b\", \n    \"thumb-tack\": \"f08d\", \n    \"thumbs-down\": \"f165\", \n    \"thumbs-o-down\": \"f088\", \n    \"thumbs-o-up\": \"f087\", \n    \"thumbs-up\": \"f164\", \n    \"ticket\": \"f145\", \n    \"times\": \"f00d\", \n    \"times-circle\": \"f057\", \n    \"times-circle-o\": \"f05c\", \n    \"tint\": \"f043\", \n    \"toggle-down\": \"f150\", \n    \"toggle-left\": \"f191\", \n    \"toggle-off\": \"f204\", \n    \"toggle-on\": \"f205\", \n    \"toggle-right\": \"f152\", \n    \"toggle-up\": \"f151\", \n    \"trademark\": \"f25c\", \n    \"train\": \"f238\", \n    \"transgender\": \"f224\", \n    \"transgender-alt\": \"f225\", \n    \"trash\": \"f1f8\", \n    \"trash-o\": \"f014\", \n    \"tree\": \"f1bb\", \n    \"trello\": \"f181\", \n    \"tripadvisor\": \"f262\", \n    \"trophy\": \"f091\", \n    \"truck\": \"f0d1\", \n    \"try\": \"f195\", \n    \"tty\": \"f1e4\", \n    \"tumblr\": \"f173\", \n    \"tumblr-square\": \"f174\", \n    \"turkish-lira\": \"f195\", \n    \"tv\": \"f26c\", \n    \"twitch\": \"f1e8\", \n    \"twitter\": \"f099\", \n    \"twitter-square\": \"f081\", \n    \"umbrella\": \"f0e9\", \n    \"underline\": \"f0cd\", \n    \"undo\": \"f0e2\", \n    \"university\": \"f19c\", \n    \"unlink\": \"f127\", \n    \"unlock\": \"f09c\", \n    \"unlock-alt\": \"f13e\", \n    \"unsorted\": \"f0dc\", \n    \"upload\": \"f093\", \n    \"usb\": \"f287\", \n    \"usd\": \"f155\", \n    \"user\": \"f007\", \n    \"user-md\": \"f0f0\", \n    \"user-plus\": \"f234\", \n    \"user-secret\": \"f21b\", \n    \"user-times\": \"f235\", \n    \"users\": \"f0c0\", \n    \"venus\": \"f221\", \n    \"venus-double\": \"f226\", \n    \"venus-mars\": \"f228\", \n    \"viacoin\": \"f237\", \n    \"video-camera\": \"f03d\", \n    \"vimeo\": \"f27d\", \n    \"vimeo-square\": \"f194\", \n    \"vine\": \"f1ca\", \n    \"vk\": \"f189\", \n    \"volume-down\": \"f027\", \n    \"volume-off\": \"f026\", \n    \"volume-up\": \"f028\", \n    \"warning\": \"f071\", \n    \"wechat\": \"f1d7\", \n    \"weibo\": \"f18a\", \n    \"weixin\": \"f1d7\", \n    \"whatsapp\": \"f232\", \n    \"wheelchair\": \"f193\", \n    \"wifi\": \"f1eb\", \n    \"wikipedia-w\": \"f266\", \n    \"windows\": \"f17a\", \n    \"won\": \"f159\", \n    \"wordpress\": \"f19a\", \n    \"wrench\": \"f0ad\", \n    \"xing\": \"f168\", \n    \"xing-square\": \"f169\", \n    \"y-combinator\": \"f23b\", \n    \"y-combinator-square\": \"f1d4\", \n    \"yahoo\": \"f19e\", \n    \"yc\": \"f23b\", \n    \"yc-square\": \"f1d4\", \n    \"yelp\": \"f1e9\", \n    \"yen\": \"f157\", \n    \"youtube\": \"f167\", \n    \"youtube-play\": \"f16a\", \n    \"youtube-square\": \"f166\"\n}"
  },
  {
    "path": "openpype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py",
    "content": "\"\"\"Classes handling iconic fonts\"\"\"\n\nfrom __future__ import print_function\n\nimport json\nimport os\n\nimport six\nfrom qtpy import QtCore, QtGui\n\n\n_default_options = {\n    'color': QtGui.QColor(50, 50, 50),\n    'color_disabled': QtGui.QColor(150, 150, 150),\n    'opacity': 1.0,\n    'scale_factor': 1.0,\n}\n\n\ndef set_global_defaults(**kwargs):\n    \"\"\"Set global defaults for all icons\"\"\"\n    valid_options = ['active', 'animation', 'color', 'color_active',\n                     'color_disabled', 'color_selected', 'disabled', 'offset',\n                     'scale_factor', 'selected']\n    for kw in kwargs:\n        if kw in valid_options:\n            _default_options[kw] = kwargs[kw]\n        else:\n            error = \"Invalid option '{0}'\".format(kw)\n            raise KeyError(error)\n\n\nclass CharIconPainter:\n\n    \"\"\"Char icon painter\"\"\"\n\n    def paint(self, iconic, painter, rect, mode, state, options):\n        \"\"\"Main paint method\"\"\"\n        for opt in options:\n            self._paint_icon(iconic, painter, rect, mode, state, opt)\n\n    def _paint_icon(self, iconic, painter, rect, mode, state, options):\n        \"\"\"Paint a single icon\"\"\"\n        painter.save()\n        color, char = options['color'], options['char']\n\n        if mode == QtGui.QIcon.Disabled:\n            color = options.get('color_disabled', color)\n            char = options.get('disabled', char)\n        elif mode == QtGui.QIcon.Active:\n            color = options.get('color_active', color)\n            char = options.get('active', char)\n        elif mode == QtGui.QIcon.Selected:\n            color = options.get('color_selected', color)\n            char = options.get('selected', char)\n\n        painter.setPen(QtGui.QColor(color))\n        # A 16 pixel-high icon yields a font size of 14, which is pixel perfect\n        # for font-awesome. 16 * 0.875 = 14\n        # The reason for not using full-sized glyphs is the negative bearing of\n        # fonts.\n        draw_size = 0.875 * round(rect.height() * options['scale_factor'])\n        prefix = options['prefix']\n\n        # Animation setup hook\n        animation = options.get('animation')\n        if animation is not None:\n            animation.setup(self, painter, rect)\n\n        painter.setFont(iconic.font(prefix, draw_size))\n        if 'offset' in options:\n            rect = QtCore.QRect(rect)\n            rect.translate(options['offset'][0] * rect.width(),\n                           options['offset'][1] * rect.height())\n\n        painter.setOpacity(options.get('opacity', 1.0))\n\n        painter.drawText(rect,\n                         QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter,\n                         char)\n        painter.restore()\n\n\nclass CharIconEngine(QtGui.QIconEngine):\n\n    \"\"\"Specialization of QtGui.QIconEngine used to draw font-based icons\"\"\"\n\n    def __init__(self, iconic, painter, options):\n        super(CharIconEngine, self).__init__()\n        self.iconic = iconic\n        self.painter = painter\n        self.options = options\n\n    def paint(self, painter, rect, mode, state):\n        self.painter.paint(\n            self.iconic, painter, rect, mode, state, self.options)\n\n    def pixmap(self, size, mode, state):\n        pm = QtGui.QPixmap(size)\n        pm.fill(QtCore.Qt.transparent)\n        self.paint(QtGui.QPainter(pm),\n                   QtCore.QRect(QtCore.QPoint(0, 0), size),\n                   mode,\n                   state)\n        return pm\n\n\nclass IconicFont(QtCore.QObject):\n\n    \"\"\"Main class for managing iconic fonts\"\"\"\n\n    def __init__(self, *args):\n        \"\"\"Constructor\n\n        :param *args: tuples\n            Each positional argument is a tuple of 3 or 4 values\n            - The prefix string to be used when accessing a given font set\n            - The ttf font filename\n            - The json charmap filename\n            - Optionally, the directory containing these files. When not\n              provided, the files will be looked up in ./fonts/\n        \"\"\"\n        super(IconicFont, self).__init__()\n        self.painter = CharIconPainter()\n        self.painters = {}\n        self.fontname = {}\n        self.charmap = {}\n        for fargs in args:\n            self.load_font(*fargs)\n\n    def load_font(self,\n                  prefix,\n                  ttf_filename,\n                  charmap_filename,\n                  directory=None):\n        \"\"\"Loads a font file and the associated charmap\n\n        If `directory` is None, the files will be looked up in ./fonts/\n\n        Arguments\n        ---------\n        prefix: str\n            prefix string to be used when accessing a given font set\n        ttf_filename: str\n            ttf font filename\n        charmap_filename: str\n            charmap filename\n        directory: str or None, optional\n            directory for font and charmap files\n        \"\"\"\n\n        def hook(obj):\n            result = {}\n            for key in obj:\n                result[key] = six.unichr(int(obj[key], 16))\n            return result\n\n        if directory is None:\n            directory = os.path.join(\n                os.path.dirname(os.path.realpath(__file__)), 'fonts')\n\n        with open(os.path.join(directory, charmap_filename), 'r') as codes:\n            self.charmap[prefix] = json.load(codes, object_hook=hook)\n\n        id_ = QtGui.QFontDatabase.addApplicationFont(\n            os.path.join(directory, ttf_filename))\n\n        loadedFontFamilies = QtGui.QFontDatabase.applicationFontFamilies(id_)\n\n        if(loadedFontFamilies):\n            self.fontname[prefix] = loadedFontFamilies[0]\n        else:\n            print('Font is empty')\n\n    def icon(self, *names, **kwargs):\n        \"\"\"Returns a QtGui.QIcon object corresponding to the provided icon name\n        (including prefix)\n\n        Arguments\n        ---------\n        names: list of str\n            icon name, of the form PREFIX.NAME\n\n        options: dict\n            options to be passed to the icon painter\n        \"\"\"\n        options_list = kwargs.pop('options', [{}] * len(names))\n        general_options = kwargs\n\n        if len(options_list) != len(names):\n            error = '\"options\" must be a list of size {0}'.format(len(names))\n            raise Exception(error)\n\n        parsed_options = []\n        for i in range(len(options_list)):\n            specific_options = options_list[i]\n            parsed_options.append(self._parse_options(specific_options,\n                                                      general_options,\n                                                      names[i]))\n\n        # Process high level API\n        api_options = parsed_options\n\n        return self._icon_by_painter(self.painter, api_options)\n\n    def _parse_options(self, specific_options, general_options, name):\n        \"\"\" \"\"\"\n        options = dict(_default_options, **general_options)\n        options.update(specific_options)\n\n        # Handle icons for states\n        icon_kw = ['disabled', 'active', 'selected', 'char']\n        names = [options.get(kw, name) for kw in icon_kw]\n        prefix, chars = self._get_prefix_chars(names)\n        options.update(dict(zip(*(icon_kw, chars))))\n        options.update({'prefix': prefix})\n\n        # Handle colors for states\n        color_kw = ['color_active', 'color_selected']\n        colors = [options.get(kw, options['color']) for kw in color_kw]\n        options.update(dict(zip(*(color_kw, colors))))\n\n        return options\n\n    def _get_prefix_chars(self, names):\n        \"\"\" \"\"\"\n        chars = []\n        for name in names:\n            if '.' in name:\n                prefix, n = name.split('.')\n                if prefix in self.charmap:\n                    if n in self.charmap[prefix]:\n                        chars.append(self.charmap[prefix][n])\n                    else:\n                        error = 'Invalid icon name \"{0}\" in font \"{1}\"'.format(\n                            n, prefix)\n                        raise Exception(error)\n                else:\n                    error = 'Invalid font prefix \"{0}\"'.format(prefix)\n                    raise Exception(error)\n            else:\n                raise Exception('Invalid icon name')\n\n        return prefix, chars\n\n    def font(self, prefix, size):\n        \"\"\"Returns QtGui.QFont corresponding to the given prefix and size\n\n        Arguments\n        ---------\n        prefix: str\n            prefix string of the loaded font\n        size: int\n            size for the font\n        \"\"\"\n        font = QtGui.QFont(self.fontname[prefix])\n        font.setPixelSize(size)\n        return font\n\n    def set_custom_icon(self, name, painter):\n        \"\"\"Associates a user-provided CharIconPainter to an icon name\n        The custom icon can later be addressed by calling\n        icon('custom.NAME') where NAME is the provided name for that icon.\n\n        Arguments\n        ---------\n        name: str\n            name of the custom icon\n        painter: CharIconPainter\n            The icon painter, implementing\n            `paint(self, iconic, painter, rect, mode, state, options)`\n        \"\"\"\n        self.painters[name] = painter\n\n    def _custom_icon(self, name, **kwargs):\n        \"\"\"Returns the custom icon corresponding to the given name\"\"\"\n        options = dict(_default_options, **kwargs)\n        if name in self.painters:\n            painter = self.painters[name]\n            return self._icon_by_painter(painter, options)\n        else:\n            return QtGui.QIcon()\n\n    def _icon_by_painter(self, painter, options):\n        \"\"\"Returns the icon corresponding to the given painter\"\"\"\n        engine = CharIconEngine(self, painter, options)\n        return QtGui.QIcon(engine)\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/version.py",
    "content": "\nVERSION_MAJOR = 2\nVERSION_MINOR = 9\nVERSION_PATCH = 0\n\n\nversion_info = (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)\nversion = '%i.%i.%i' % version_info\n__version__ = version\n\n__all__ = ['version', 'version_info', '__version__']\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/view.py",
    "content": "from qtpy import QtCore, QtWidgets\nfrom . import model\nfrom .constants import Roles, EXPANDER_WIDTH\n# Imported when used\nwidgets = None\n\n\ndef _import_widgets():\n    global widgets\n    if widgets is None:\n        from . import widgets\n\n\nclass OverviewView(QtWidgets.QTreeView):\n    # An item is requesting to be toggled, with optional forced-state\n    toggled = QtCore.Signal(QtCore.QModelIndex, object)\n    show_perspective = QtCore.Signal(QtCore.QModelIndex)\n\n    def __init__(self, parent=None):\n        super(OverviewView, self).__init__(parent)\n\n        self.horizontalScrollBar().hide()\n        self.viewport().setAttribute(QtCore.Qt.WA_Hover, True)\n        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)\n        self.setItemsExpandable(True)\n        self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)\n        self.setHeaderHidden(True)\n        self.setRootIsDecorated(False)\n        self.setIndentation(0)\n\n    def event(self, event):\n        if not event.type() == QtCore.QEvent.KeyPress:\n            return super(OverviewView, self).event(event)\n\n        elif event.key() == QtCore.Qt.Key_Space:\n            for index in self.selectionModel().selectedIndexes():\n                self.toggled.emit(index, None)\n\n            return True\n\n        elif event.key() == QtCore.Qt.Key_Backspace:\n            for index in self.selectionModel().selectedIndexes():\n                self.toggled.emit(index, False)\n\n            return True\n\n        elif event.key() == QtCore.Qt.Key_Return:\n            for index in self.selectionModel().selectedIndexes():\n                self.toggled.emit(index, True)\n\n            return True\n\n        return super(OverviewView, self).event(event)\n\n    def focusOutEvent(self, event):\n        self.selectionModel().clear()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() in (QtCore.Qt.LeftButton, QtCore.Qt.RightButton):\n            # Deselect all group labels\n            indexes = self.selectionModel().selectedIndexes()\n            for index in indexes:\n                if index.data(Roles.TypeRole) == model.GroupType:\n                    self.selectionModel().select(\n                        index, QtCore.QItemSelectionModel.Deselect\n                    )\n\n        return super(OverviewView, self).mouseReleaseEvent(event)\n\n\nclass PluginView(OverviewView):\n    def __init__(self, *args, **kwargs):\n        super(PluginView, self).__init__(*args, **kwargs)\n        self.clicked.connect(self.item_expand)\n\n    def item_expand(self, index):\n        if index.data(Roles.TypeRole) == model.GroupType:\n            if self.isExpanded(index):\n                self.collapse(index)\n            else:\n                self.expand(index)\n\n    def mouseReleaseEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            indexes = self.selectionModel().selectedIndexes()\n            if len(indexes) == 1:\n                index = indexes[0]\n                pos_index = self.indexAt(event.pos())\n                # If instance or Plugin and is selected\n                if (\n                    index == pos_index\n                    and index.data(Roles.TypeRole) == model.PluginType\n                ):\n                    if event.pos().x() < 20:\n                        self.toggled.emit(index, None)\n                    elif event.pos().x() > self.width() - 20:\n                        self.show_perspective.emit(index)\n\n        return super(PluginView, self).mouseReleaseEvent(event)\n\n\nclass InstanceView(OverviewView):\n    def __init__(self, *args, **kwargs):\n        super(InstanceView, self).__init__(*args, **kwargs)\n        self.setSortingEnabled(True)\n        self.sortByColumn(0, QtCore.Qt.AscendingOrder)\n        self.viewport().setMouseTracking(True)\n        self._pressed_group_index = None\n        self._pressed_expander = None\n\n    def mouseMoveEvent(self, event):\n        index = self.indexAt(event.pos())\n        if index.data(Roles.TypeRole) == model.GroupType:\n            self.update(index)\n        super(InstanceView, self).mouseMoveEvent(event)\n\n    def item_expand(self, index, expand=None):\n        if expand is None:\n            expand = not self.isExpanded(index)\n\n        if expand:\n            self.expand(index)\n        else:\n            self.collapse(index)\n\n    def group_toggle(self, index):\n        if not index.isValid():\n            return\n        model = index.model()\n\n        chilren_indexes_checked = []\n        chilren_indexes_unchecked = []\n        for idx in range(model.rowCount(index)):\n            child_index = model.index(idx, 0, index)\n            if not child_index.data(Roles.IsEnabledRole):\n                continue\n\n            if child_index.data(QtCore.Qt.CheckStateRole):\n                chilren_indexes_checked.append(child_index)\n            else:\n                chilren_indexes_unchecked.append(child_index)\n\n        if chilren_indexes_checked:\n            to_change_indexes = chilren_indexes_checked\n            new_state = False\n        else:\n            to_change_indexes = chilren_indexes_unchecked\n            new_state = True\n\n        for index in to_change_indexes:\n            model.setData(index, new_state, QtCore.Qt.CheckStateRole)\n            self.toggled.emit(index, new_state)\n\n    def _mouse_press(self, event):\n        if event.button() != QtCore.Qt.LeftButton:\n            return\n\n        self._pressed_group_index = None\n        self._pressed_expander = None\n\n        pos_index = self.indexAt(event.pos())\n        if not pos_index.isValid():\n            return\n\n        if pos_index.data(Roles.TypeRole) != model.InstanceType:\n            self._pressed_group_index = pos_index\n            if event.pos().x() < 20:\n                self._pressed_expander = True\n            else:\n                self._pressed_expander = False\n\n        elif event.pos().x() < 20:\n            indexes = self.selectionModel().selectedIndexes()\n            any_checked = False\n            if len(indexes) <= 1:\n                return\n\n            if pos_index in indexes:\n                for index in indexes:\n                    if index.data(QtCore.Qt.CheckStateRole):\n                        any_checked = True\n                        break\n\n                for index in indexes:\n                    self.toggled.emit(index, not any_checked)\n                return True\n            self.toggled.emit(pos_index, not any_checked)\n\n    def mousePressEvent(self, event):\n        if self._mouse_press(event):\n            return\n        return super(InstanceView, self).mousePressEvent(event)\n\n    def _mouse_release(self, event, pressed_expander, pressed_index):\n        if event.button() != QtCore.Qt.LeftButton:\n            return\n\n        pos_index = self.indexAt(event.pos())\n        if not pos_index.isValid():\n            return\n\n        if pos_index.data(Roles.TypeRole) == model.InstanceType:\n            indexes = self.selectionModel().selectedIndexes()\n            if len(indexes) == 1 and indexes[0] == pos_index:\n                if event.pos().x() < 20:\n                    self.toggled.emit(indexes[0], None)\n                elif event.pos().x() > self.width() - 20:\n                    self.show_perspective.emit(indexes[0])\n                return True\n            return\n\n        if pressed_index != pos_index:\n            return\n\n        if self.state() == QtWidgets.QTreeView.State.DragSelectingState:\n            indexes = self.selectionModel().selectedIndexes()\n            if len(indexes) != 1 or indexes[0] != pos_index:\n                return\n\n        if event.pos().x() < EXPANDER_WIDTH:\n            if pressed_expander is True:\n                self.item_expand(pos_index)\n                return True\n        else:\n            if pressed_expander is False:\n                self.group_toggle(pos_index)\n                self.item_expand(pos_index, True)\n                return True\n\n    def mouseReleaseEvent(self, event):\n        pressed_index = self._pressed_group_index\n        pressed_expander = self._pressed_expander is True\n        self._pressed_group_index = None\n        self._pressed_expander = None\n        result = self._mouse_release(event, pressed_expander, pressed_index)\n        if result:\n            return\n        return super(InstanceView, self).mouseReleaseEvent(event)\n\n\nclass TerminalView(QtWidgets.QTreeView):\n    # An item is requesting to be toggled, with optional forced-state\n    def __init__(self, parent=None):\n        super(TerminalView, self).__init__(parent)\n        self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)\n        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)\n        self.setAutoScroll(False)\n        self.setHeaderHidden(True)\n        self.setIndentation(0)\n        self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)\n        self.verticalScrollBar().setSingleStep(10)\n        self.setRootIsDecorated(False)\n\n        self.clicked.connect(self.item_expand)\n\n        _import_widgets()\n\n    def event(self, event):\n        if not event.type() == QtCore.QEvent.KeyPress:\n            return super(TerminalView, self).event(event)\n\n        elif event.key() == QtCore.Qt.Key_Space:\n            for index in self.selectionModel().selectedIndexes():\n                if self.isExpanded(index):\n                    self.collapse(index)\n                else:\n                    self.expand(index)\n\n        elif event.key() == QtCore.Qt.Key_Backspace:\n            for index in self.selectionModel().selectedIndexes():\n                self.collapse(index)\n\n        elif event.key() == QtCore.Qt.Key_Return:\n            for index in self.selectionModel().selectedIndexes():\n                self.expand(index)\n\n        return super(TerminalView, self).event(event)\n\n    def focusOutEvent(self, event):\n        self.selectionModel().clear()\n\n    def item_expand(self, index):\n        if index.data(Roles.TypeRole) == model.TerminalLabelType:\n            if self.isExpanded(index):\n                self.collapse(index)\n            else:\n                self.expand(index)\n                self.model().layoutChanged.emit()\n            self.updateGeometry()\n\n    def rowsInserted(self, parent, start, end):\n        \"\"\"Automatically scroll to bottom on each new item added.\"\"\"\n        super(TerminalView, self).rowsInserted(parent, start, end)\n        self.updateGeometry()\n        self.scrollToBottom()\n\n    def expand(self, index):\n        \"\"\"Wrapper to set widget for expanded index.\"\"\"\n        model = index.model()\n        row_count = model.rowCount(index)\n        is_new = False\n        for child_idx in range(row_count):\n            child_index = model.index(child_idx, index.column(), index)\n            widget = self.indexWidget(child_index)\n            if widget is None:\n                is_new = True\n                msg = child_index.data(QtCore.Qt.DisplayRole)\n                widget = widgets.TerminalDetail(msg)\n                self.setIndexWidget(child_index, widget)\n        super(TerminalView, self).expand(index)\n        if is_new:\n            self.updateGeometries()\n\n    def resizeEvent(self, event):\n        super(self.__class__, self).resizeEvent(event)\n        self.model().layoutChanged.emit()\n\n    def sizeHint(self):\n        size = super(TerminalView, self).sizeHint()\n        height = (\n            self.contentsMargins().top()\n            + self.contentsMargins().bottom()\n        )\n        for idx_i in range(self.model().rowCount()):\n            index = self.model().index(idx_i, 0)\n            height += self.rowHeight(index)\n            if self.isExpanded(index):\n                for idx_j in range(index.model().rowCount(index)):\n                    child_index = index.child(idx_j, 0)\n                    height += self.rowHeight(child_index)\n\n        size.setHeight(height)\n        return size\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/widgets.py",
    "content": "import sys\nfrom qtpy import QtCore, QtWidgets, QtGui\nfrom . import model, delegate, view, awesome\nfrom .constants import PluginStates, InstanceStates, Roles\n\n\nclass EllidableLabel(QtWidgets.QLabel):\n    def __init__(self, *args, **kwargs):\n        super(EllidableLabel, self).__init__(*args, **kwargs)\n        self.setObjectName(\"EllidableLabel\")\n\n    def paintEvent(self, event):\n        painter = QtGui.QPainter(self)\n\n        metrics = QtGui.QFontMetrics(self.font())\n        elided = metrics.elidedText(\n            self.text(), QtCore.Qt.ElideRight, self.width()\n        )\n        painter.drawText(self.rect(), self.alignment(), elided)\n\n\nclass PerspectiveLabel(QtWidgets.QTextEdit):\n    def __init__(self, parent=None):\n        super(PerspectiveLabel, self).__init__(parent)\n        self.setObjectName(\"PerspectiveLabel\")\n\n        size_policy = self.sizePolicy()\n        size_policy.setHeightForWidth(True)\n        size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Preferred)\n        self.setSizePolicy(size_policy)\n\n        self.textChanged.connect(self.on_text_changed)\n\n    def on_text_changed(self, *args, **kwargs):\n        self.updateGeometry()\n\n    def hasHeightForWidth(self):\n        return True\n\n    def heightForWidth(self, width):\n        margins = self.contentsMargins()\n\n        document_width = 0\n        if width >= margins.left() + margins.right():\n            document_width = width - margins.left() - margins.right()\n\n        document = self.document().clone()\n        document.setTextWidth(document_width)\n\n        return margins.top() + document.size().height() + margins.bottom()\n\n    def sizeHint(self):\n        width = super(PerspectiveLabel, self).sizeHint().width()\n        return QtCore.QSize(width, self.heightForWidth(width))\n\n\nclass PerspectiveWidget(QtWidgets.QWidget):\n    l_doc = \"Documentation\"\n    l_rec = \"Records\"\n    l_path = \"Path\"\n\n    def __init__(self, parent):\n        super(PerspectiveWidget, self).__init__(parent)\n\n        self.parent_widget = parent\n        main_layout = QtWidgets.QVBoxLayout(self)\n\n        header_widget = QtWidgets.QWidget()\n        toggle_button = QtWidgets.QPushButton(parent=header_widget)\n        toggle_button.setObjectName(\"PerspectiveToggleBtn\")\n        toggle_button.setText(delegate.icons[\"angle-left\"])\n        toggle_button.setMinimumHeight(50)\n        toggle_button.setFixedWidth(40)\n\n        indicator = QtWidgets.QLabel(\"\", parent=header_widget)\n        indicator.setFixedWidth(30)\n        indicator.setAlignment(QtCore.Qt.AlignCenter)\n        indicator.setObjectName(\"PerspectiveIndicator\")\n\n        name = EllidableLabel('*Name of inspected', parent=header_widget)\n\n        header_layout = QtWidgets.QHBoxLayout(header_widget)\n        header_layout.setAlignment(QtCore.Qt.AlignLeft)\n        header_layout.addWidget(toggle_button)\n        header_layout.addWidget(indicator)\n        header_layout.addWidget(name)\n        header_layout.setContentsMargins(0, 0, 0, 0)\n        header_layout.setSpacing(10)\n        header_widget.setLayout(header_layout)\n\n        main_layout.setAlignment(QtCore.Qt.AlignTop)\n        main_layout.addWidget(header_widget)\n\n        scroll_widget = QtWidgets.QScrollArea(self)\n        scroll_widget.setObjectName(\"PerspectiveScrollContent\")\n\n        contents_widget = QtWidgets.QWidget(scroll_widget)\n        contents_widget.setObjectName(\"PerspectiveWidgetContent\")\n\n        layout = QtWidgets.QVBoxLayout()\n        layout.setAlignment(QtCore.Qt.AlignTop)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        documentation = ExpandableWidget(self, self.l_doc)\n        doc_label = PerspectiveLabel()\n        documentation.set_content(doc_label)\n        layout.addWidget(documentation)\n\n        path = ExpandableWidget(self, self.l_path)\n        path_label = PerspectiveLabel()\n        path.set_content(path_label)\n        layout.addWidget(path)\n\n        records = ExpandableWidget(self, self.l_rec)\n        layout.addWidget(records)\n\n        contents_widget.setLayout(layout)\n\n        terminal_view = view.TerminalView()\n        terminal_view.setObjectName(\"TerminalView\")\n        terminal_model = model.TerminalModel()\n        terminal_proxy = model.TerminalProxy(terminal_view)\n        terminal_proxy.setSourceModel(terminal_model)\n\n        terminal_view.setModel(terminal_proxy)\n        terminal_delegate = delegate.TerminalItem()\n        terminal_view.setItemDelegate(terminal_delegate)\n        records.set_content(terminal_view)\n\n        scroll_widget.setWidgetResizable(True)\n        scroll_widget.setWidget(contents_widget)\n\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n        main_layout.addWidget(scroll_widget)\n        self.setLayout(main_layout)\n\n        self.terminal_view = terminal_view\n        self.terminal_model = terminal_model\n        self.terminal_proxy = terminal_proxy\n\n        self.indicator = indicator\n        self.scroll_widget = scroll_widget\n        self.contents_widget = contents_widget\n        self.toggle_button = toggle_button\n        self.name_widget = name\n        self.documentation = documentation\n        self.path = path\n        self.records = records\n\n        self.toggle_button.clicked.connect(self.toggle_me)\n\n        self.last_type = None\n        self.last_item_id = None\n        self.last_id = None\n\n    def trim(self, docstring):\n        if not docstring:\n            return \"\"\n        # Convert tabs to spaces (following the normal Python rules)\n        # and split into a list of lines:\n        lines = docstring.expandtabs().splitlines()\n        # Determine minimum indentation (first line doesn't count):\n        try:\n            indent = sys.maxint\n            max = sys.maxint\n        except Exception:\n            indent = sys.maxsize\n            max = sys.maxsize\n\n        for line in lines[1:]:\n            stripped = line.lstrip()\n            if stripped:\n                indent = min(indent, len(line) - len(stripped))\n        # Remove indentation (first line is special):\n        trimmed = [lines[0].strip()]\n        if indent < max:\n            for line in lines[1:]:\n                trimmed.append(line[indent:].rstrip())\n        # Strip off trailing and leading blank lines:\n        while trimmed and not trimmed[-1]:\n            trimmed.pop()\n        while trimmed and not trimmed[0]:\n            trimmed.pop(0)\n        # Return a single string:\n        return \"\\n\".join(trimmed)\n\n    def set_indicator_state(self, state):\n        self.indicator.setProperty(\"state\", state)\n        self.indicator.style().polish(self.indicator)\n\n    def reset(self):\n        self.last_id = None\n        self.set_records(list())\n        self.set_indicator_state(None)\n\n    def update_context(self, plugin_item, instance_item):\n        if not self.last_item_id or not self.last_type:\n            return\n\n        if self.last_type == model.PluginType:\n            if not self.last_id:\n                _item_id = plugin_item.data(Roles.ObjectUIdRole)\n                if _item_id != self.last_item_id:\n                    return\n                self.last_id = plugin_item.plugin.id\n\n            elif self.last_id != plugin_item.plugin.id:\n                return\n\n            self.set_context(plugin_item.index())\n            return\n\n        if self.last_type == model.InstanceType:\n            if not self.last_id:\n                _item_id = instance_item.data(Roles.ObjectUIdRole)\n                if _item_id != self.last_item_id:\n                    return\n                self.last_id = instance_item.instance.id\n\n            elif self.last_id != instance_item.instance.id:\n                return\n\n            self.set_context(instance_item.index())\n            return\n\n    def set_context(self, index):\n        if not index or not index.isValid():\n            index_type = None\n        else:\n            index_type = index.data(Roles.TypeRole)\n\n        if index_type == model.InstanceType:\n            item_id = index.data(Roles.ObjectIdRole)\n            publish_states = index.data(Roles.PublishFlagsRole)\n            if publish_states & InstanceStates.ContextType:\n                type_indicator = \"C\"\n            else:\n                type_indicator = \"I\"\n\n            if publish_states & InstanceStates.InProgress:\n                self.set_indicator_state(\"active\")\n\n            elif publish_states & InstanceStates.HasError:\n                self.set_indicator_state(\"error\")\n\n            elif publish_states & InstanceStates.HasWarning:\n                self.set_indicator_state(\"warning\")\n\n            elif publish_states & InstanceStates.HasFinished:\n                self.set_indicator_state(\"ok\")\n            else:\n                self.set_indicator_state(None)\n\n            self.documentation.setVisible(False)\n            self.path.setVisible(False)\n\n        elif index_type == model.PluginType:\n            item_id = index.data(Roles.ObjectIdRole)\n            type_indicator = \"P\"\n\n            doc = index.data(Roles.DocstringRole)\n            doc_str = \"\"\n            if doc:\n                doc_str = self.trim(doc)\n\n            publish_states = index.data(Roles.PublishFlagsRole)\n            if publish_states & PluginStates.InProgress:\n                self.set_indicator_state(\"active\")\n\n            elif publish_states & PluginStates.HasError:\n                self.set_indicator_state(\"error\")\n\n            elif publish_states & PluginStates.HasWarning:\n                self.set_indicator_state(\"warning\")\n\n            elif publish_states & PluginStates.WasProcessed:\n                self.set_indicator_state(\"ok\")\n\n            else:\n                self.set_indicator_state(None)\n\n            self.documentation.toggle_content(bool(doc_str))\n            self.documentation.content.setText(doc_str)\n\n            path = index.data(Roles.PathModuleRole) or \"\"\n            self.path.toggle_content(path.strip() != \"\")\n            self.path.content.setText(path)\n\n            self.documentation.setVisible(True)\n            self.path.setVisible(True)\n\n        else:\n            self.last_type = None\n            self.last_id = None\n            self.indicator.setText(\"?\")\n            self.set_indicator_state(None)\n            self.documentation.setVisible(False)\n            self.path.setVisible(False)\n            self.records.setVisible(False)\n            return\n\n        self.last_type = index_type\n        self.last_id = item_id\n        self.last_item_id = index.data(Roles.ObjectUIdRole)\n\n        self.indicator.setText(type_indicator)\n\n        label = index.data(QtCore.Qt.DisplayRole)\n        self.name_widget.setText(label)\n        self.records.setVisible(True)\n\n        records = index.data(Roles.LogRecordsRole) or []\n        self.set_records(records)\n\n    def set_records(self, records):\n        len_records = 0\n        if records:\n            len_records += len(records)\n\n        data = {\"records\": records}\n        self.terminal_model.reset()\n        self.terminal_model.update_with_result(data)\n\n        self.records.button_toggle_text.setText(\n            \"{} ({})\".format(self.l_rec, len_records)\n        )\n        self.records.toggle_content(len_records > 0)\n\n    def toggle_me(self):\n        self.parent_widget.parent().toggle_perspective_widget()\n\n\nclass ClickableWidget(QtWidgets.QLabel):\n    clicked = QtCore.Signal()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self.clicked.emit()\n        super(ClickableWidget, self).mouseReleaseEvent(event)\n\n\nclass ExpandableWidget(QtWidgets.QWidget):\n\n    content = None\n\n    def __init__(self, parent, title):\n        super(ExpandableWidget, self).__init__(parent)\n\n        top_part = ClickableWidget(parent=self)\n        top_part.setObjectName(\"ExpandableHeader\")\n\n        button_size = QtCore.QSize(5, 5)\n        button_toggle = QtWidgets.QToolButton(parent=top_part)\n        button_toggle.setIconSize(button_size)\n        button_toggle.setArrowType(QtCore.Qt.RightArrow)\n        button_toggle.setCheckable(True)\n        button_toggle.setChecked(False)\n\n        button_toggle_text = QtWidgets.QLabel(title, parent=top_part)\n\n        layout = QtWidgets.QHBoxLayout(top_part)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(5)\n        layout.addWidget(button_toggle)\n        layout.addWidget(button_toggle_text)\n        top_part.setLayout(layout)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(9, 9, 9, 0)\n\n        content = QtWidgets.QFrame(self)\n        content.setObjectName(\"ExpandableWidgetContent\")\n        content.setVisible(False)\n\n        content_layout = QtWidgets.QVBoxLayout(content)\n\n        main_layout.addWidget(top_part)\n        main_layout.addWidget(content)\n        self.setLayout(main_layout)\n\n        self.setAttribute(QtCore.Qt.WA_StyledBackground)\n\n        self.top_part = top_part\n        self.button_toggle = button_toggle\n        self.button_toggle_text = button_toggle_text\n\n        self.content_widget = content\n        self.content_layout = content_layout\n\n        self.top_part.clicked.connect(self.top_part_clicked)\n        self.button_toggle.clicked.connect(self.toggle_content)\n\n    def top_part_clicked(self):\n        self.toggle_content(not self.button_toggle.isChecked())\n\n    def toggle_content(self, *args):\n        if len(args) > 0:\n            checked = args[0]\n        else:\n            checked = self.button_toggle.isChecked()\n        arrow_type = QtCore.Qt.RightArrow\n        if checked:\n            arrow_type = QtCore.Qt.DownArrow\n        self.button_toggle.setChecked(checked)\n        self.button_toggle.setArrowType(arrow_type)\n        self.content_widget.setVisible(checked)\n\n    def resizeEvent(self, event):\n        super(ExpandableWidget, self).resizeEvent(event)\n        self.content.updateGeometry()\n\n    def set_content(self, in_widget):\n        if self.content:\n            self.content.hide()\n            self.content_layout.removeWidget(self.content)\n        self.content_layout.addWidget(in_widget)\n        self.content = in_widget\n\n\nclass ButtonWithMenu(QtWidgets.QWidget):\n    def __init__(self, button_title, parent=None):\n        super(ButtonWithMenu, self).__init__(parent=parent)\n        self.setSizePolicy(QtWidgets.QSizePolicy(\n            QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum\n        ))\n\n        self.layout = QtWidgets.QHBoxLayout(self)\n        self.layout.setContentsMargins(0, 0, 0, 0)\n        self.layout.setSpacing(0)\n\n        self.menu = QtWidgets.QMenu()\n        # TODO move to stylesheets\n        self.menu.setStyleSheet(\"\"\"\n            *{color: #fff; background-color: #555; border: 1px solid #222;}\n            ::item {background-color: transparent;padding: 5px;\n            padding-left: 10px;padding-right: 10px;}\n            ::item:selected {background-color: #666;}\n        \"\"\")\n\n        self.button = QtWidgets.QPushButton(button_title)\n        self.button.setObjectName(\"ButtonWithMenu\")\n\n        self.layout.addWidget(self.button)\n\n        self.button.clicked.connect(self.btn_clicked)\n\n    def btn_clicked(self):\n        self.menu.popup(self.button.mapToGlobal(\n            QtCore.QPoint(0, self.button.height())\n        ))\n\n    def addItem(self, text, callback):\n        self.menu.addAction(text, callback)\n        self.button.setToolTip(\"Select to apply predefined presets\")\n\n    def clearMenu(self):\n        self.menu.clear()\n        self.button.setToolTip(\"Presets not found\")\n\n\nclass CommentBox(QtWidgets.QLineEdit):\n\n    def __init__(self, placeholder_text, parent=None):\n        super(CommentBox, self).__init__(parent=parent)\n        self.placeholder = QtWidgets.QLabel(placeholder_text, self)\n        self.placeholder.move(2, 2)\n\n    def focusInEvent(self, event):\n        self.placeholder.setVisible(False)\n        return super(CommentBox, self).focusInEvent(event)\n\n    def focusOutEvent(self, event):\n        current_text = self.text()\n        current_text = current_text.strip(\" \")\n        self.setText(current_text)\n        if not self.text():\n            self.placeholder.setVisible(True)\n        return super(CommentBox, self).focusOutEvent(event)\n\n\nclass TerminalDetail(QtWidgets.QTextEdit):\n    def __init__(self, text, *args, **kwargs):\n        super(TerminalDetail, self).__init__(*args, **kwargs)\n\n        self.setReadOnly(True)\n        self.setHtml(text)\n        self.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)\n        self.setWordWrapMode(\n            QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere\n        )\n\n    def sizeHint(self):\n        content_margins = (\n            self.contentsMargins().top()\n            + self.contentsMargins().bottom()\n        )\n        size = self.document().documentLayout().documentSize().toSize()\n        size.setHeight(size.height() + content_margins)\n        return size\n\n\nclass FilterButton(QtWidgets.QPushButton):\n    def __init__(self, name, *args, **kwargs):\n        self.filter_name = name\n\n        super(FilterButton, self).__init__(*args, **kwargs)\n\n        self.toggled.connect(self.on_toggle)\n\n        self.setProperty(\"type\", name)\n        self.setObjectName(\"TerminalFilerBtn\")\n        self.setCheckable(True)\n        self.setChecked(\n            model.TerminalProxy.filter_buttons_checks[name]\n        )\n\n    def on_toggle(self, toggle_state):\n        model.TerminalProxy.change_filter(self.filter_name, toggle_state)\n\n\nclass TerminalFilterWidget(QtWidgets.QWidget):\n    # timer.timeout.connect(lambda: self._update(self.parent_widget))\n    def __init__(self, *args, **kwargs):\n        super(TerminalFilterWidget, self).__init__(*args, **kwargs)\n        self.setObjectName(\"TerminalFilterWidget\")\n        self.filter_changed = QtCore.Signal()\n\n        info_icon = awesome.tags[\"info\"]\n        log_icon = awesome.tags[\"circle\"]\n        error_icon = awesome.tags[\"exclamation-triangle\"]\n\n        filter_buttons = (\n            FilterButton(\"info\", info_icon, self),\n            FilterButton(\"log_debug\", log_icon, self),\n            FilterButton(\"log_info\", log_icon, self),\n            FilterButton(\"log_warning\", log_icon, self),\n            FilterButton(\"log_error\", log_icon, self),\n            FilterButton(\"log_critical\", log_icon, self),\n            FilterButton(\"error\", error_icon, self)\n        )\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        # Add spacers\n        spacer = QtWidgets.QWidget()\n        spacer.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        layout.addWidget(spacer, 1)\n\n        for btn in filter_buttons:\n            layout.addWidget(btn)\n\n        self.setLayout(layout)\n\n        self.filter_buttons = filter_buttons\n"
  },
  {
    "path": "openpype/tools/pyblish_pype/window.py",
    "content": "\"\"\"Main Window\n\nStates:\n    These are all possible states and their transitions.\n\n\n      reset\n        '\n        '\n        '\n     ___v__\n    |      |       reset\n    | Idle |--------------------.\n    |      |<-------------------'\n    |      |\n    |      |                   _____________\n    |      |     validate     |             |    reset     # TODO\n    |      |----------------->| In-progress |-----------.\n    |      |                  |_____________|           '\n    |      |<-------------------------------------------'\n    |      |\n    |      |                   _____________\n    |      |      publish     |             |\n    |      |----------------->| In-progress |---.\n    |      |                  |_____________|   '\n    |      |<-----------------------------------'\n    |______|\n\n\nTodo:\n    There are notes spread throughout this project with the syntax:\n\n    - TODO(username)\n\n    The `username` is a quick and dirty indicator of who made the note\n    and is by no means exclusive to that person in terms of seeing it\n    done. Feel free to do, or make your own TODO's as you code. Just\n    make sure the description is sufficient for anyone reading it for\n    the first time to understand how to actually to it!\n\n\"\"\"\nimport sys\nfrom functools import partial\n\nfrom . import delegate, model, settings, util, view, widgets\nfrom .awesome import tags as awesome\n\nfrom qtpy import QtCore, QtGui, QtWidgets\nfrom .constants import (\n    PluginStates, PluginActionStates, InstanceStates, GroupStates, Roles\n)\nif sys.version_info[0] == 3:\n    from queue import Queue\nelse:\n    from Queue import Queue\n\n\nclass Window(QtWidgets.QDialog):\n    def __init__(self, controller, parent=None):\n        super(Window, self).__init__(parent=parent)\n\n        self._suspend_logs = False\n\n        # Use plastique style for specific ocations\n        # TODO set style name via environment variable\n        low_keys = {\n            key.lower(): key\n            for key in QtWidgets.QStyleFactory.keys()\n        }\n        if \"plastique\" in low_keys:\n            self.setStyle(\n                QtWidgets.QStyleFactory.create(low_keys[\"plastique\"])\n            )\n\n        icon = QtGui.QIcon(util.get_asset(\"img\", \"logo-extrasmall.png\"))\n        if parent is None:\n            on_top_flag = QtCore.Qt.WindowStaysOnTopHint\n        else:\n            on_top_flag = QtCore.Qt.Dialog\n\n        self.setWindowFlags(\n            self.windowFlags()\n            | QtCore.Qt.WindowTitleHint\n            | QtCore.Qt.WindowMaximizeButtonHint\n            | QtCore.Qt.WindowMinimizeButtonHint\n            | QtCore.Qt.WindowCloseButtonHint\n            | on_top_flag\n        )\n        self.setWindowIcon(icon)\n        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)\n\n        self.controller = controller\n\n        main_widget = QtWidgets.QWidget(self)\n\n        # General layout\n        header_widget = QtWidgets.QWidget(parent=main_widget)\n\n        header_tab_widget = QtWidgets.QWidget(header_widget)\n        header_tab_overview = QtWidgets.QRadioButton(header_tab_widget)\n        header_tab_terminal = QtWidgets.QRadioButton(header_tab_widget)\n        header_spacer = QtWidgets.QWidget(header_tab_widget)\n\n        button_suspend_logs_widget = QtWidgets.QWidget()\n        button_suspend_logs_widget_layout = QtWidgets.QHBoxLayout(\n            button_suspend_logs_widget\n        )\n        button_suspend_logs_widget_layout.setContentsMargins(0, 10, 0, 10)\n        button_suspend_logs = QtWidgets.QPushButton(header_widget)\n        button_suspend_logs.setFixedWidth(7)\n        button_suspend_logs.setSizePolicy(\n            QtWidgets.QSizePolicy.Preferred,\n            QtWidgets.QSizePolicy.Expanding\n        )\n        button_suspend_logs_widget_layout.addWidget(button_suspend_logs)\n        header_aditional_btns = QtWidgets.QWidget(header_tab_widget)\n\n        aditional_btns_layout = QtWidgets.QHBoxLayout(header_aditional_btns)\n\n        presets_button = widgets.ButtonWithMenu(awesome[\"filter\"])\n        presets_button.setEnabled(False)\n        aditional_btns_layout.addWidget(presets_button)\n\n        layout_tab = QtWidgets.QHBoxLayout(header_tab_widget)\n        layout_tab.setContentsMargins(0, 0, 0, 0)\n        layout_tab.setSpacing(0)\n        layout_tab.addWidget(header_tab_overview, 0)\n        layout_tab.addWidget(header_tab_terminal, 0)\n        layout_tab.addWidget(button_suspend_logs_widget, 0)\n\n        # Compress items to the left\n        layout_tab.addWidget(header_spacer, 1)\n        layout_tab.addWidget(header_aditional_btns, 0)\n\n        layout = QtWidgets.QHBoxLayout(header_widget)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(0)\n        layout.addWidget(header_tab_widget)\n\n        header_widget.setLayout(layout)\n\n        # Overview Page\n        # TODO add parent\n        overview_page = QtWidgets.QWidget()\n\n        overview_instance_view = view.InstanceView(parent=overview_page)\n        overview_instance_view.setAnimated(settings.Animated)\n        overview_instance_delegate = delegate.InstanceDelegate(\n            parent=overview_instance_view\n        )\n        instance_model = model.InstanceModel(controller)\n        instance_sort_proxy = model.InstanceSortProxy()\n        instance_sort_proxy.setSourceModel(instance_model)\n\n        overview_instance_view.setItemDelegate(overview_instance_delegate)\n        overview_instance_view.setModel(instance_sort_proxy)\n\n        overview_plugin_view = view.PluginView(parent=overview_page)\n        overview_plugin_view.setAnimated(settings.Animated)\n        overview_plugin_delegate = delegate.PluginDelegate(\n            parent=overview_plugin_view\n        )\n        overview_plugin_view.setItemDelegate(overview_plugin_delegate)\n        plugin_model = model.PluginModel(controller)\n        plugin_proxy = model.PluginFilterProxy()\n        plugin_proxy.setSourceModel(plugin_model)\n        overview_plugin_view.setModel(plugin_proxy)\n\n        layout = QtWidgets.QHBoxLayout(overview_page)\n        layout.addWidget(overview_instance_view, 1)\n        layout.addWidget(overview_plugin_view, 1)\n        layout.setContentsMargins(5, 5, 5, 5)\n        layout.setSpacing(0)\n        overview_page.setLayout(layout)\n\n        # Terminal\n        terminal_container = QtWidgets.QWidget()\n\n        terminal_view = view.TerminalView()\n        terminal_model = model.TerminalModel()\n        terminal_proxy = model.TerminalProxy(terminal_view)\n        terminal_proxy.setSourceModel(terminal_model)\n\n        terminal_view.setModel(terminal_proxy)\n        terminal_delegate = delegate.TerminalItem()\n        terminal_view.setItemDelegate(terminal_delegate)\n\n        layout = QtWidgets.QVBoxLayout(terminal_container)\n        layout.addWidget(terminal_view)\n        layout.setContentsMargins(5, 5, 5, 5)\n        layout.setSpacing(0)\n\n        terminal_container.setLayout(layout)\n\n        terminal_page = QtWidgets.QWidget()\n        layout = QtWidgets.QVBoxLayout(terminal_page)\n        layout.addWidget(terminal_container)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(0)\n\n        # Add some room between window borders and contents\n        body_widget = QtWidgets.QWidget(main_widget)\n        layout = QtWidgets.QHBoxLayout(body_widget)\n        layout.setContentsMargins(5, 5, 5, 1)\n        layout.addWidget(overview_page)\n        layout.addWidget(terminal_page)\n\n        # Comment Box\n        comment_box = widgets.CommentBox(\"Comment...\", self)\n\n        intent_box = QtWidgets.QComboBox()\n\n        intent_model = model.IntentModel()\n        intent_box.setModel(intent_model)\n\n        comment_intent_widget = QtWidgets.QWidget()\n        comment_intent_layout = QtWidgets.QHBoxLayout(comment_intent_widget)\n        comment_intent_layout.setContentsMargins(0, 0, 0, 0)\n        comment_intent_layout.setSpacing(5)\n        comment_intent_layout.addWidget(comment_box)\n        comment_intent_layout.addWidget(intent_box)\n\n        # Terminal filtering\n        terminal_filters_widget = widgets.TerminalFilterWidget()\n\n        # Footer\n        footer_widget = QtWidgets.QWidget(main_widget)\n\n        footer_info = QtWidgets.QLabel(footer_widget)\n        footer_spacer = QtWidgets.QWidget(footer_widget)\n\n        footer_button_stop = QtWidgets.QPushButton(\n            awesome[\"stop\"], footer_widget\n        )\n        footer_button_stop.setToolTip(\"Stop publishing\")\n        footer_button_reset = QtWidgets.QPushButton(\n            awesome[\"refresh\"], footer_widget\n        )\n        footer_button_reset.setToolTip(\"Restart publishing\")\n        footer_button_validate = QtWidgets.QPushButton(\n            awesome[\"flask\"], footer_widget\n        )\n        footer_button_validate.setToolTip(\"Run validations\")\n        footer_button_play = QtWidgets.QPushButton(\n            awesome[\"play\"], footer_widget\n        )\n        footer_button_play.setToolTip(\"Publish\")\n        layout = QtWidgets.QHBoxLayout()\n        layout.setContentsMargins(5, 5, 5, 5)\n        layout.addWidget(footer_info, 0)\n        layout.addWidget(footer_spacer, 1)\n\n        layout.addWidget(footer_button_stop, 0)\n        layout.addWidget(footer_button_reset, 0)\n        layout.addWidget(footer_button_validate, 0)\n        layout.addWidget(footer_button_play, 0)\n\n        footer_layout = QtWidgets.QVBoxLayout(footer_widget)\n        footer_layout.addWidget(terminal_filters_widget)\n        footer_layout.addWidget(comment_intent_widget)\n        footer_layout.addLayout(layout)\n\n        footer_widget.setProperty(\"success\", -1)\n\n        # Placeholder for when GUI is closing\n        # TODO(marcus): Fade to black and the the user about what's happening\n        closing_placeholder = QtWidgets.QWidget(main_widget)\n        closing_placeholder.setSizePolicy(\n            QtWidgets.QSizePolicy.Expanding,\n            QtWidgets.QSizePolicy.Expanding\n        )\n        closing_placeholder.hide()\n\n        perspective_widget = widgets.PerspectiveWidget(main_widget)\n        perspective_widget.hide()\n\n        pages_widget = QtWidgets.QWidget(main_widget)\n        layout = QtWidgets.QVBoxLayout(pages_widget)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(0)\n        layout.addWidget(header_widget, 0)\n        layout.addWidget(body_widget, 1)\n\n        # Main layout\n        layout = QtWidgets.QVBoxLayout(main_widget)\n        layout.addWidget(pages_widget, 3)\n        layout.addWidget(perspective_widget, 3)\n        layout.addWidget(closing_placeholder, 1)\n        layout.addWidget(footer_widget, 0)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(0)\n        main_widget.setLayout(layout)\n\n        self.main_layout = QtWidgets.QVBoxLayout(self)\n        self.main_layout.setContentsMargins(0, 0, 0, 0)\n        self.main_layout.setSpacing(0)\n        self.main_layout.addWidget(main_widget)\n\n        \"\"\"Setup\n\n        Widgets are referred to in CSS via their object-name. We\n        use the same mechanism internally to refer to objects; so rather\n        than storing widgets as self.my_widget, it is referred to as:\n\n        >>> my_widget = self.findChild(QtWidgets.QWidget, \"MyWidget\")\n\n        This way there is only ever a single method of referring to any widget.\n        \"\"\"\n\n        names = {\n            # Main\n            \"Header\": header_widget,\n            \"Body\": body_widget,\n            \"Footer\": footer_widget,\n\n            # Pages\n            \"Overview\": overview_page,\n            \"Terminal\": terminal_page,\n\n            # Tabs\n            \"OverviewTab\": header_tab_overview,\n            \"TerminalTab\": header_tab_terminal,\n\n            # Views\n            \"TerminalView\": terminal_view,\n\n            # Buttons\n            \"SuspendLogsBtn\": button_suspend_logs,\n            \"Stop\": footer_button_stop,\n            \"Reset\": footer_button_reset,\n            \"Validate\": footer_button_validate,\n            \"Play\": footer_button_play,\n\n            # Misc\n            \"HeaderSpacer\": header_spacer,\n            \"FooterSpacer\": footer_spacer,\n            \"FooterInfo\": footer_info,\n            \"CommentIntentWidget\": comment_intent_widget,\n            \"CommentBox\": comment_box,\n            \"CommentPlaceholder\": comment_box.placeholder,\n            \"ClosingPlaceholder\": closing_placeholder,\n            \"IntentBox\": intent_box\n        }\n\n        for name, _widget in names.items():\n            _widget.setObjectName(name)\n\n        # Enable CSS on plain QWidget objects\n        for _widget in (\n            pages_widget,\n            header_widget,\n            body_widget,\n            comment_box,\n            overview_page,\n            terminal_page,\n            footer_widget,\n            button_suspend_logs,\n            footer_button_stop,\n            footer_button_reset,\n            footer_button_validate,\n            footer_button_play,\n            footer_spacer,\n            closing_placeholder\n        ):\n            _widget.setAttribute(QtCore.Qt.WA_StyledBackground)\n\n        # Signals\n        header_tab_overview.toggled.connect(\n            lambda: self.on_tab_changed(\"overview\")\n        )\n        header_tab_terminal.toggled.connect(\n            lambda: self.on_tab_changed(\"terminal\")\n        )\n\n        overview_instance_view.show_perspective.connect(\n            self.toggle_perspective_widget\n        )\n        overview_plugin_view.show_perspective.connect(\n            self.toggle_perspective_widget\n        )\n\n        controller.switch_toggleability.connect(self.change_toggleability)\n\n        controller.was_reset.connect(self.on_was_reset)\n        # This is called synchronously on each process\n        controller.was_processed.connect(self.on_was_processed)\n        controller.passed_group.connect(self.on_passed_group)\n        controller.was_stopped.connect(self.on_was_stopped)\n        controller.was_finished.connect(self.on_was_finished)\n\n        controller.was_skipped.connect(self.on_was_skipped)\n        controller.was_acted.connect(self.on_was_acted)\n\n        # NOTE: Listeners to this signal are run in the main thread\n        controller.about_to_process.connect(\n            self.on_about_to_process,\n            QtCore.Qt.DirectConnection\n        )\n\n        overview_instance_view.toggled.connect(self.on_instance_toggle)\n        overview_plugin_view.toggled.connect(self.on_plugin_toggle)\n\n        button_suspend_logs.clicked.connect(self.on_suspend_clicked)\n        footer_button_stop.clicked.connect(self.on_stop_clicked)\n        footer_button_reset.clicked.connect(self.on_reset_clicked)\n        footer_button_validate.clicked.connect(self.on_validate_clicked)\n        footer_button_play.clicked.connect(self.on_play_clicked)\n\n        comment_box.textChanged.connect(self.on_comment_entered)\n        comment_box.returnPressed.connect(self.on_play_clicked)\n        overview_plugin_view.customContextMenuRequested.connect(\n            self.on_plugin_action_menu_requested\n        )\n\n        instance_model.group_created.connect(self.on_instance_group_created)\n\n        self.main_widget = main_widget\n\n        self.pages_widget = pages_widget\n        self.header_widget = header_widget\n        self.body_widget = body_widget\n\n        self.terminal_filters_widget = terminal_filters_widget\n\n        self.footer_widget = footer_widget\n        self.button_suspend_logs = button_suspend_logs\n        self.footer_button_stop = footer_button_stop\n        self.footer_button_reset = footer_button_reset\n        self.footer_button_validate = footer_button_validate\n        self.footer_button_play = footer_button_play\n\n        self.footer_info = footer_info\n\n        self.overview_instance_view = overview_instance_view\n        self.overview_plugin_view = overview_plugin_view\n        self.plugin_model = plugin_model\n        self.plugin_proxy = plugin_proxy\n        self.instance_model = instance_model\n        self.instance_sort_proxy = instance_sort_proxy\n\n        self.presets_button = presets_button\n\n        self.terminal_model = terminal_model\n        self.terminal_proxy = terminal_proxy\n        self.terminal_view = terminal_view\n\n        self.comment_main_widget = comment_intent_widget\n        self.comment_box = comment_box\n        self.intent_box = intent_box\n        self.intent_model = intent_model\n\n        self.perspective_widget = perspective_widget\n\n        self.tabs = {\n            \"overview\": header_tab_overview,\n            \"terminal\": header_tab_terminal\n        }\n        self.pages = (\n            (\"overview\", overview_page),\n            (\"terminal\", terminal_page)\n        )\n\n        current_page = settings.InitialTab or \"overview\"\n        self.comment_main_widget.setVisible(\n            not current_page == \"terminal\"\n        )\n        self.terminal_filters_widget.setVisible(\n            current_page == \"terminal\"\n        )\n\n        self._current_page = current_page\n        self._hidden_for_plugin_process = False\n\n        self.tabs[current_page].setChecked(True)\n\n        self.apply_log_suspend_value(\n            util.env_variable_to_bool(\"PYBLISH_SUSPEND_LOGS\")\n        )\n\n    # -------------------------------------------------------------------------\n    #\n    # Event handlers\n    #\n    # -------------------------------------------------------------------------\n    def set_presets(self, key):\n        plugin_settings = self.controller.possible_presets.get(key)\n        if not plugin_settings:\n            return\n\n        for plugin_item in self.plugin_model.plugin_items.values():\n            if not plugin_item.plugin.optional:\n                continue\n\n            value = plugin_settings.get(\n                plugin_item.plugin.__name__,\n                # if plugin is not in presets then set default value\n                self.controller.optional_default.get(\n                    plugin_item.plugin.__name__\n                )\n            )\n            if value is None:\n                continue\n\n            plugin_item.setData(value, QtCore.Qt.CheckStateRole)\n\n    def toggle_perspective_widget(self, index=None):\n        show = False\n        if index:\n            show = True\n            self.perspective_widget.set_context(index)\n\n        self.pages_widget.setVisible(not show)\n        self.perspective_widget.setVisible(show)\n        self.footer_items_visibility()\n\n    def change_toggleability(self, enable_value):\n        for plugin_item in self.plugin_model.plugin_items.values():\n            plugin_item.setData(enable_value, Roles.IsEnabledRole)\n\n        for instance_item in (\n            self.instance_model.instance_items.values()\n        ):\n            instance_item.setData(enable_value, Roles.IsEnabledRole)\n\n    def _add_intent_to_context(self):\n        context_value = None\n        if (\n            self.intent_model.has_items\n            and \"intent\" not in self.controller.context.data\n        ):\n            idx = self.intent_model.index(self.intent_box.currentIndex(), 0)\n            intent_value = self.intent_model.data(idx, Roles.IntentItemValue)\n            intent_label = self.intent_model.data(idx, QtCore.Qt.DisplayRole)\n            if intent_value:\n                context_value = {\n                    \"value\": intent_value,\n                    \"label\": intent_label\n                }\n\n        # Unset intent if is set to empty value\n        if context_value is None:\n            self.controller.context.data.pop(\"intent\", None)\n        else:\n            self.controller.context.data[\"intent\"] = context_value\n\n    def on_instance_toggle(self, index, state=None):\n        \"\"\"An item is requesting to be toggled\"\"\"\n        if not index.data(Roles.IsOptionalRole):\n            return self.info(\"This item is mandatory\")\n\n        if self.controller.collect_state != 1:\n            return self.info(\"Cannot toggle\")\n\n        current_state = index.data(QtCore.Qt.CheckStateRole)\n        if state is None:\n            state = not current_state\n\n        instance_id = index.data(Roles.ObjectIdRole)\n        instance_item = self.instance_model.instance_items[instance_id]\n        instance_item.setData(state, QtCore.Qt.CheckStateRole)\n\n        self.controller.instance_toggled.emit(\n            instance_item.instance, current_state, state\n        )\n\n        self.update_compatibility()\n\n    def on_instance_group_created(self, index):\n        _index = self.instance_sort_proxy.mapFromSource(index)\n        self.overview_instance_view.expand(_index)\n\n    def on_plugin_toggle(self, index, state=None):\n        \"\"\"An item is requesting to be toggled\"\"\"\n        if not index.data(Roles.IsOptionalRole):\n            return self.info(\"This item is mandatory\")\n\n        if self.controller.collect_state != 1:\n            return self.info(\"Cannot toggle\")\n\n        if state is None:\n            state = not index.data(QtCore.Qt.CheckStateRole)\n\n        plugin_id = index.data(Roles.ObjectIdRole)\n        plugin_item = self.plugin_model.plugin_items[plugin_id]\n        plugin_item.setData(state, QtCore.Qt.CheckStateRole)\n\n        self.update_compatibility()\n\n    def on_tab_changed(self, target):\n        previous_page = None\n        target_page = None\n        direction = None\n        for name, page in self.pages:\n            if name == target:\n                target_page = page\n                if direction is None:\n                    direction = -1\n            elif name == self._current_page:\n                previous_page = page\n                if direction is None:\n                    direction = 1\n            else:\n                page.setVisible(False)\n\n        self._current_page = target\n        self.slide_page(previous_page, target_page, direction)\n\n    def slide_page(self, previous_page, target_page, direction):\n        if previous_page is None:\n            for name, page in self.pages:\n                for _name, _page in self.pages:\n                    if name != _name:\n                        _page.hide()\n                page.show()\n                page.hide()\n\n        if (\n            previous_page == target_page\n            or previous_page is None\n        ):\n            if not target_page.isVisible():\n                target_page.show()\n            return\n\n        if not settings.Animated:\n            previous_page.setVisible(False)\n            target_page.setVisible(True)\n            return\n\n        width = previous_page.frameGeometry().width()\n        offset = QtCore.QPoint(direction * width, 0)\n\n        previous_rect = (\n            previous_page.frameGeometry().x(),\n            previous_page.frameGeometry().y(),\n            width,\n            previous_page.frameGeometry().height()\n        )\n        curr_pos = previous_page.pos()\n\n        previous_page.hide()\n        target_page.show()\n        target_page.update()\n        target_rect = (\n            target_page.frameGeometry().x(),\n            target_page.frameGeometry().y(),\n            target_page.frameGeometry().width(),\n            target_page.frameGeometry().height()\n        )\n        previous_page.show()\n\n        target_page.raise_()\n        previous_page.setGeometry(*previous_rect)\n        target_page.setGeometry(*target_rect)\n\n        target_page.move(curr_pos + offset)\n\n        duration = 250\n\n        anim_old = QtCore.QPropertyAnimation(\n            previous_page, b\"pos\", self\n        )\n        anim_old.setDuration(duration)\n        anim_old.setStartValue(curr_pos)\n        anim_old.setEndValue(curr_pos - offset)\n        anim_old.setEasingCurve(QtCore.QEasingCurve.OutQuad)\n\n        anim_new = QtCore.QPropertyAnimation(\n            target_page, b\"pos\", self\n        )\n        anim_new.setDuration(duration)\n        anim_new.setStartValue(curr_pos + offset)\n        anim_new.setEndValue(curr_pos)\n        anim_new.setEasingCurve(QtCore.QEasingCurve.OutQuad)\n\n        anim_group = QtCore.QParallelAnimationGroup(self)\n        anim_group.addAnimation(anim_old)\n        anim_group.addAnimation(anim_new)\n\n        def slide_finished():\n            previous_page.hide()\n            self.footer_items_visibility()\n\n        anim_group.finished.connect(slide_finished)\n        anim_group.start()\n\n    def footer_items_visibility(\n        self,\n        comment_visible=None,\n        terminal_filters_visibile=None\n    ):\n        target = self._current_page\n        comment_visibility = (\n            not self.perspective_widget.isVisible()\n            and not target == \"terminal\"\n            and self.comment_box.isEnabled()\n        )\n        terminal_filters_visibility = (\n            target == \"terminal\"\n            or self.perspective_widget.isVisible()\n        )\n\n        if comment_visible is not None and comment_visibility:\n            comment_visibility = comment_visible\n\n        if (\n            terminal_filters_visibile is not None\n            and terminal_filters_visibility\n        ):\n            terminal_filters_visibility = terminal_filters_visibile\n\n        duration = 150\n\n        hiding_widgets = []\n        showing_widgets = []\n        if (comment_visibility != (\n            self.comment_main_widget.isVisible()\n        )):\n            if self.comment_main_widget.isVisible():\n                hiding_widgets.append(self.comment_main_widget)\n            else:\n                showing_widgets.append(self.comment_main_widget)\n\n        if (terminal_filters_visibility != (\n            self.terminal_filters_widget.isVisible()\n        )):\n            if self.terminal_filters_widget.isVisible():\n                hiding_widgets.append(self.terminal_filters_widget)\n            else:\n                showing_widgets.append(self.terminal_filters_widget)\n\n        if not hiding_widgets and not showing_widgets:\n            return\n\n        hiding_widgets_queue = Queue()\n        showing_widgets_queue = Queue()\n        widgets_by_pos_y = {}\n        for widget in hiding_widgets:\n            key = widget.mapToGlobal(widget.rect().topLeft()).x()\n            widgets_by_pos_y[key] = widget\n\n        for key in sorted(widgets_by_pos_y.keys()):\n            widget = widgets_by_pos_y[key]\n            hiding_widgets_queue.put((widget, ))\n\n        for widget in hiding_widgets:\n            widget.hide()\n\n        for widget in showing_widgets:\n            widget.show()\n\n        self.footer_widget.updateGeometry()\n        widgets_by_pos_y = {}\n        for widget in showing_widgets:\n            key = widget.mapToGlobal(widget.rect().topLeft()).x()\n            widgets_by_pos_y[key] = widget\n\n        for key in reversed(sorted(widgets_by_pos_y.keys())):\n            widget = widgets_by_pos_y[key]\n            showing_widgets_queue.put(widget)\n\n        for widget in showing_widgets:\n            widget.hide()\n\n        for widget in hiding_widgets:\n            widget.show()\n\n        def process_showing():\n            if showing_widgets_queue.empty():\n                return\n\n            widget = showing_widgets_queue.get()\n            widget.show()\n\n            widget_rect = widget.frameGeometry()\n            second_rect = QtCore.QRect(widget_rect)\n            second_rect.setTopLeft(second_rect.bottomLeft())\n\n            animation = QtCore.QPropertyAnimation(\n                widget, b\"geometry\", self\n            )\n            animation.setDuration(duration)\n            animation.setStartValue(second_rect)\n            animation.setEndValue(widget_rect)\n            animation.setEasingCurve(QtCore.QEasingCurve.OutQuad)\n\n            animation.finished.connect(process_showing)\n            animation.start()\n\n        def process_hiding():\n            if hiding_widgets_queue.empty():\n                return process_showing()\n\n            item = hiding_widgets_queue.get()\n            if isinstance(item, tuple):\n                widget = item[0]\n                hiding_widgets_queue.put(widget)\n                widget_rect = widget.frameGeometry()\n                second_rect = QtCore.QRect(widget_rect)\n                second_rect.setTopLeft(second_rect.bottomLeft())\n\n                anim = QtCore.QPropertyAnimation(\n                    widget, b\"geometry\", self\n                )\n                anim.setDuration(duration)\n                anim.setStartValue(widget_rect)\n                anim.setEndValue(second_rect)\n                anim.setEasingCurve(QtCore.QEasingCurve.OutQuad)\n\n                anim.finished.connect(process_hiding)\n                anim.start()\n            else:\n                item.hide()\n                return process_hiding()\n\n        process_hiding()\n\n    def on_validate_clicked(self):\n        self.comment_box.setEnabled(False)\n        self.footer_items_visibility()\n        self.intent_box.setEnabled(False)\n\n        self._add_intent_to_context()\n\n        self.validate()\n\n    def on_play_clicked(self):\n        self.comment_box.setEnabled(False)\n        self.footer_items_visibility()\n        self.intent_box.setEnabled(False)\n\n        self._add_intent_to_context()\n\n        self.publish()\n\n    def on_reset_clicked(self):\n        self.reset()\n\n    def on_stop_clicked(self):\n        self.info(\"Stopping..\")\n        self.controller.stop()\n\n        # TODO checks\n        self.footer_button_reset.setEnabled(True)\n        self.footer_button_play.setEnabled(False)\n        self.footer_button_stop.setEnabled(False)\n\n    def on_suspend_clicked(self, value=None):\n        self.apply_log_suspend_value(not self._suspend_logs)\n\n    def apply_log_suspend_value(self, value):\n        self._suspend_logs = value\n        if self._current_page == \"terminal\":\n            self.tabs[\"overview\"].setChecked(True)\n\n        self.tabs[\"terminal\"].setVisible(not self._suspend_logs)\n\n    def on_comment_entered(self):\n        \"\"\"The user has typed a comment.\"\"\"\n        self.controller.context.data[\"comment\"] = self.comment_box.text()\n\n    def on_about_to_process(self, plugin, instance):\n        \"\"\"Reflect currently running pair in GUI\"\"\"\n        if instance is None:\n            instance_id = self.controller.context.id\n        else:\n            instance_id = instance.id\n\n        instance_item = (\n            self.instance_model.instance_items[instance_id]\n        )\n        instance_item.setData(\n            {InstanceStates.InProgress: True},\n            Roles.PublishFlagsRole\n        )\n\n        plugin_item = self.plugin_model.plugin_items[plugin._id]\n        plugin_item.setData(\n            {PluginStates.InProgress: True},\n            Roles.PublishFlagsRole\n        )\n\n        self.info(\"{} {}\".format(\n            self.tr(\"Processing\"), plugin_item.data(QtCore.Qt.DisplayRole)\n        ))\n\n        visibility = True\n        if hasattr(plugin, \"hide_ui_on_process\") and plugin.hide_ui_on_process:\n            visibility = False\n        self._hidden_for_plugin_process = not visibility\n\n        self._ensure_visible(visibility)\n\n    def _ensure_visible(self, visible):\n        if self.isVisible() == visible:\n            return\n\n        if not visible:\n            self.setVisible(visible)\n        else:\n            self.show()\n            self.raise_()\n            self.activateWindow()\n            self.showNormal()\n\n    def on_plugin_action_menu_requested(self, pos):\n        \"\"\"The user right-clicked on a plug-in\n         __________\n        |          |\n        | Action 1 |\n        | Action 2 |\n        | Action 3 |\n        |          |\n        |__________|\n\n        \"\"\"\n\n        index = self.overview_plugin_view.indexAt(pos)\n        actions = index.data(Roles.PluginValidActionsRole)\n\n        if not actions:\n            return\n\n        menu = QtWidgets.QMenu(self)\n        plugin_id = index.data(Roles.ObjectIdRole)\n        plugin_item = self.plugin_model.plugin_items[plugin_id]\n        print(\"plugin is: %s\" % plugin_item.plugin)\n\n        for action in actions:\n            qaction = QtWidgets.QAction(action.label or action.__name__, self)\n            qaction.triggered.connect(partial(self.act, plugin_item, action))\n            menu.addAction(qaction)\n\n        menu.popup(self.overview_plugin_view.viewport().mapToGlobal(pos))\n\n    def update_compatibility(self):\n        self.plugin_model.update_compatibility()\n        self.plugin_proxy.invalidateFilter()\n\n    def on_was_reset(self):\n        # Append context object to instances model\n        self.instance_model.append(self.controller.context)\n\n        for plugin in self.controller.plugins:\n            self.plugin_model.append(plugin)\n\n        self.overview_instance_view.expandAll()\n        self.overview_plugin_view.expandAll()\n\n        self.presets_button.clearMenu()\n        if self.controller.possible_presets:\n            self.presets_button.setEnabled(True)\n            for key in self.controller.possible_presets:\n                self.presets_button.addItem(\n                    key, partial(self.set_presets, key)\n                )\n\n        self.instance_model.restore_checkstates()\n        self.plugin_model.restore_checkstates()\n\n        self.perspective_widget.reset()\n\n        # Append placeholder comment from Context\n        # This allows users to inject a comment from elsewhere,\n        # or to perhaps provide a placeholder comment/template\n        # for artists to fill in.\n        comment = self.controller.context.data.get(\"comment\")\n        self.comment_box.setText(comment or None)\n        self.comment_box.setEnabled(True)\n        self.footer_items_visibility()\n\n        self.intent_box.setEnabled(True)\n\n        # Refresh tab\n        self.on_tab_changed(self._current_page)\n        self.update_compatibility()\n\n        self.button_suspend_logs.setEnabled(False)\n\n        self.footer_button_validate.setEnabled(False)\n        self.footer_button_reset.setEnabled(False)\n        self.footer_button_stop.setEnabled(True)\n        self.footer_button_play.setEnabled(False)\n\n        self._update_state()\n\n    def on_passed_group(self, order):\n        for group_item in self.instance_model.group_items.values():\n            group_index = self.instance_sort_proxy.mapFromSource(\n                group_item.index()\n            )\n            if self.overview_instance_view.isExpanded(group_index):\n                continue\n\n            if group_item.publish_states & GroupStates.HasError:\n                self.overview_instance_view.expand(group_index)\n\n        for group_item in self.plugin_model.group_items.values():\n            # TODO check only plugins from the group\n            if group_item.publish_states & GroupStates.HasFinished:\n                continue\n\n            if order != group_item.order:\n                continue\n\n            group_index = self.plugin_proxy.mapFromSource(group_item.index())\n            if group_item.publish_states & GroupStates.HasError:\n                self.overview_plugin_view.expand(group_index)\n                continue\n\n            group_item.setData(\n                {GroupStates.HasFinished: True},\n                Roles.PublishFlagsRole\n            )\n            self.overview_plugin_view.setAnimated(False)\n            self.overview_plugin_view.collapse(group_index)\n\n        self._update_state()\n\n    def on_was_stopped(self):\n        self.overview_plugin_view.setAnimated(settings.Animated)\n        errored = self.controller.errored\n        if self.controller.collect_state == 0:\n            self.footer_button_play.setEnabled(False)\n            self.footer_button_validate.setEnabled(False)\n        else:\n            self.footer_button_play.setEnabled(not errored)\n            self.footer_button_validate.setEnabled(\n                not errored and not self.controller.validated\n            )\n        self.footer_button_play.setFocus()\n\n        self.footer_button_reset.setEnabled(True)\n        self.footer_button_stop.setEnabled(False)\n        if errored:\n            self.footer_widget.setProperty(\"success\", 0)\n            self.footer_widget.style().polish(self.footer_widget)\n\n        suspend_log_bool = (\n            self.controller.collect_state == 1\n            and not self.controller.stopped\n        )\n        self.button_suspend_logs.setEnabled(suspend_log_bool)\n\n        self._update_state()\n\n        if self._hidden_for_plugin_process:\n            self._hidden_for_plugin_process = False\n            self._ensure_visible(True)\n\n    def on_was_skipped(self, plugin):\n        plugin_item = self.plugin_model.plugin_items[plugin.id]\n        plugin_item.setData(\n            {PluginStates.WasSkipped: True},\n            Roles.PublishFlagsRole\n        )\n\n    def on_was_finished(self):\n        self.overview_plugin_view.setAnimated(settings.Animated)\n        self.footer_button_play.setEnabled(False)\n        self.footer_button_validate.setEnabled(False)\n        self.footer_button_reset.setEnabled(True)\n        self.footer_button_stop.setEnabled(False)\n\n        if self.controller.errored:\n            success_val = 0\n            self.info(self.tr(\"Stopped due to error(s), see Terminal.\"))\n            self.comment_box.setEnabled(False)\n            self.intent_box.setEnabled(False)\n\n        else:\n            success_val = 1\n            self.info(self.tr(\"Finished successfully!\"))\n\n        self.footer_widget.setProperty(\"success\", success_val)\n        self.footer_widget.style().polish(self.footer_widget)\n\n        for instance_item in (\n            self.instance_model.instance_items.values()\n        ):\n            instance_item.setData(\n                {InstanceStates.HasFinished: True},\n                Roles.PublishFlagsRole\n            )\n\n        for group_item in self.instance_model.group_items.values():\n            group_item.setData(\n                {GroupStates.HasFinished: True},\n                Roles.PublishFlagsRole\n            )\n\n        self.update_compatibility()\n        self._update_state()\n\n    def on_was_processed(self, result):\n        existing_ids = set(self.instance_model.instance_items.keys())\n        existing_ids.remove(self.controller.context.id)\n        for instance in self.controller.context:\n            if instance.id not in existing_ids:\n                self.instance_model.append(instance)\n            else:\n                existing_ids.remove(instance.id)\n\n        for instance_id in existing_ids:\n            self.instance_model.remove(instance_id)\n\n        result[\"records\"] = self.terminal_model.prepare_records(\n            result,\n            self._suspend_logs\n        )\n\n        plugin_item = self.plugin_model.update_with_result(result)\n        instance_item = self.instance_model.update_with_result(result)\n\n        self.terminal_model.update_with_result(result)\n\n        self.update_compatibility()\n\n        if self.perspective_widget.isVisible():\n            self.perspective_widget.update_context(\n                plugin_item, instance_item\n            )\n\n        if self._hidden_for_plugin_process:\n            self._hidden_for_plugin_process = False\n            self._ensure_visible(True)\n\n    # -------------------------------------------------------------------------\n    #\n    # Functions\n    #\n    # -------------------------------------------------------------------------\n\n    def reset(self):\n        \"\"\"Prepare GUI for reset\"\"\"\n        self.info(self.tr(\"About to reset..\"))\n\n        self.presets_button.setEnabled(False)\n        self.footer_widget.setProperty(\"success\", -1)\n        self.footer_widget.style().polish(self.footer_widget)\n\n        self.instance_model.store_checkstates()\n        self.plugin_model.store_checkstates()\n\n        # Reset current ids to secure no previous instances get mixed in.\n        self.instance_model.reset()\n        self.plugin_model.reset()\n        self.intent_model.reset()\n        self.terminal_model.reset()\n\n        self.footer_button_stop.setEnabled(False)\n        self.footer_button_reset.setEnabled(False)\n        self.footer_button_validate.setEnabled(False)\n        self.footer_button_play.setEnabled(False)\n\n        self.intent_box.setVisible(self.intent_model.has_items)\n        if self.intent_model.has_items:\n            self.intent_box.setCurrentIndex(self.intent_model.default_index)\n\n        self.comment_box.placeholder.setVisible(False)\n        # Launch controller reset\n        self.controller.reset()\n        if not self.comment_box.text():\n            self.comment_box.placeholder.setVisible(True)\n\n    def validate(self):\n        self.info(self.tr(\"Preparing validate..\"))\n        self.footer_button_stop.setEnabled(True)\n        self.footer_button_reset.setEnabled(False)\n        self.footer_button_validate.setEnabled(False)\n        self.footer_button_play.setEnabled(False)\n\n        self.button_suspend_logs.setEnabled(False)\n\n        self.controller.validate()\n\n        self._update_state()\n\n    def publish(self):\n        self.info(self.tr(\"Preparing publish..\"))\n        self.footer_button_stop.setEnabled(True)\n        self.footer_button_reset.setEnabled(False)\n        self.footer_button_validate.setEnabled(False)\n        self.footer_button_play.setEnabled(False)\n\n        self.button_suspend_logs.setEnabled(False)\n\n        self.controller.publish()\n\n        self._update_state()\n\n    def act(self, plugin_item, action):\n        self.info(\"%s %s..\" % (self.tr(\"Preparing\"), action))\n\n        self.footer_button_stop.setEnabled(True)\n        self.footer_button_reset.setEnabled(False)\n        self.footer_button_validate.setEnabled(False)\n        self.footer_button_play.setEnabled(False)\n\n        # Cause view to update, but it won't visually\n        # happen until Qt is given time to idle..\n        plugin_item.setData(\n            PluginActionStates.InProgress, Roles.PluginActionProgressRole\n        )\n\n        # Give Qt time to draw\n        self.controller.act(plugin_item.plugin, action)\n\n        self.info(self.tr(\"Action prepared.\"))\n\n    def on_was_acted(self, result):\n        self.footer_button_reset.setEnabled(True)\n        self.footer_button_stop.setEnabled(False)\n\n        # Update action with result\n        plugin_item = self.plugin_model.plugin_items[result[\"plugin\"].id]\n        action_state = plugin_item.data(Roles.PluginActionProgressRole)\n        action_state |= PluginActionStates.HasFinished\n        result[\"records\"] = self.terminal_model.prepare_records(\n            result,\n            self._suspend_logs\n        )\n\n        if result.get(\"error\"):\n            action_state |= PluginActionStates.HasFailed\n\n        plugin_item.setData(action_state, Roles.PluginActionProgressRole)\n\n        self.terminal_model.update_with_result(result)\n        plugin_item = self.plugin_model.update_with_result(result)\n        instance_item = self.instance_model.update_with_result(result)\n\n        if self.perspective_widget.isVisible():\n            self.perspective_widget.update_context(\n                plugin_item, instance_item\n            )\n\n    def closeEvent(self, event):\n        \"\"\"Perform post-flight checks before closing\n\n        Make sure processing of any kind is wrapped up before closing\n\n        \"\"\"\n\n        self.info(self.tr(\"Closing..\"))\n\n        if self.controller.is_running:\n            self.info(self.tr(\"..as soon as processing is finished..\"))\n            self.controller.stop()\n\n            self.info(self.tr(\"Cleaning up controller..\"))\n            self.controller.cleanup()\n\n            self.overview_instance_view.setModel(None)\n            self.overview_plugin_view.setModel(None)\n            self.terminal_view.setModel(None)\n\n        event.accept()\n\n    def reject(self):\n        \"\"\"Handle ESC key\"\"\"\n\n        if self.controller.is_running:\n            self.info(self.tr(\"Stopping..\"))\n            self.controller.stop()\n\n    # -------------------------------------------------------------------------\n    #\n    # Feedback\n    #\n    # -------------------------------------------------------------------------\n\n    def _update_state(self):\n        self.footer_info.setText(self.controller.current_state)\n\n    def info(self, message):\n        \"\"\"Print user-facing information\n\n        Arguments:\n            message (str): Text message for the user\n\n        \"\"\"\n        # Include message in terminal\n        self.terminal_model.append([{\n            \"label\": message,\n            \"type\": \"info\"\n        }])\n\n        if settings.PrintInfo:\n            # Print message to console\n            util.u_print(message)\n\n    def warning(self, message):\n        \"\"\"Block processing and print warning until user hits \"Continue\"\n\n        Arguments:\n            message (str): Message to display\n\n        \"\"\"\n\n        # TODO(marcus): Implement this.\n        self.info(message)\n\n    def heads_up(self, title, message, command=None):\n        \"\"\"Provide a front-and-center message with optional command\n\n        Arguments:\n            title (str): Bold and short message\n            message (str): Extended message\n            command (optional, callable): Function is provided as a button\n\n        \"\"\"\n\n        # TODO(marcus): Implement this.\n        self.info(message)\n"
  },
  {
    "path": "openpype/tools/repack_version.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Script to rehash and repack current version.\"\"\"\n\nimport enlighten\nimport blessed\nfrom pathlib import Path\nimport platform\nfrom zipfile import ZipFile\nfrom typing import List\nimport hashlib\nimport sys\nfrom igniter.bootstrap_repos import OpenPypeVersion\n\n\nclass VersionRepacker:\n\n    def __init__(self, directory: str):\n        self._term = blessed.Terminal()\n        self._manager = enlighten.get_manager()\n        self._last_increment = 0\n        self.version_path = Path(directory)\n        self.zip_path = self.version_path.parent\n        _version = {}\n        with open(self.version_path / \"openpype\" / \"version.py\") as fp:\n            exec(fp.read(), _version)\n        self._version_py = _version[\"__version__\"]\n        del _version\n\n    def _print(self, msg: str, message_type: int = 0) -> None:\n        \"\"\"Print message to console.\n\n        Args:\n            msg (str): message to print\n            message_type (int): type of message (0 info, 1 error, 2 note)\n\n        \"\"\"\n        if message_type == 0:\n            header = self._term.aquamarine3(\">>> \")\n        elif message_type == 1:\n            header = self._term.orangered2(\"!!! \")\n        elif message_type == 2:\n            header = self._term.tan1(\"... \")\n        else:\n            header = self._term.darkolivegreen3(\"--- \")\n\n        print(\"{}{}\".format(header, msg))\n\n    @staticmethod\n    def sha256sum(filename):\n        \"\"\"Calculate sha256 for content of the file.\n\n        Args:\n             filename (str): Path to file.\n\n        Returns:\n            str: hex encoded sha256\n\n        \"\"\"\n        h = hashlib.sha256()\n        b = bytearray(128 * 1024)\n        mv = memoryview(b)\n        with open(filename, 'rb', buffering=0) as f:\n            for n in iter(lambda: f.readinto(mv), 0):\n                h.update(mv[:n])\n        return h.hexdigest()\n\n    @staticmethod\n    def _filter_dir(path: Path, path_filter: List) -> List[Path]:\n        \"\"\"Recursively crawl over path and filter.\"\"\"\n        result = []\n        for item in path.iterdir():\n            if item.name in path_filter:\n                continue\n            if item.name.startswith('.'):\n                continue\n            if item.is_dir():\n                result.extend(VersionRepacker._filter_dir(item, path_filter))\n            else:\n                result.append(item)\n        return result\n\n    def process(self):\n        if (self.version_path / \"pyproject.toml\").exists():\n            self._print(\n                (\"This cannot run on OpenPype sources. \"\n                 \"Please run it on extracted version.\"), 1)\n            return\n        self._print(f\"Rehashing and zipping {self.version_path}\")\n        version = OpenPypeVersion.version_in_str(self.version_path.name)\n        if not version:\n            self._print(\"Cannot get version from directory\", 1)\n            return\n\n        self._print(f\"Detected version is {version}\")\n        # replace version in version.py\n        self._replace_version(version, self.version_path)\n        self._print(\"Recalculating checksums ...\", 2)\n\n        checksums = []\n\n        file_list = VersionRepacker._filter_dir(self.version_path, [])\n        progress_bar = enlighten.Counter(\n            total=len(file_list), desc=\"Calculating checksums\",\n            nits=\"%\", color=\"green\")\n        for file in file_list:\n            checksums.append((\n                VersionRepacker.sha256sum(file.as_posix()),\n                file.resolve().relative_to(self.version_path),\n                file\n            ))\n            progress_bar.update()\n        progress_bar.close()\n\n        progress_bar = enlighten.Counter(\n            total=len(checksums), desc=\"Zipping directory\",\n            nits=\"%\", color=(56, 211, 159))\n\n        zip_filename = self.zip_path / f\"openpype-v{version}.zip\"\n        with ZipFile(zip_filename, \"w\") as zip_file:\n\n            for item in checksums:\n                if item[1].as_posix() == \"checksums\":\n                    progress_bar.update()\n                    continue\n                zip_file.write(item[2], item[1])\n                progress_bar.update()\n\n            checksums_str = \"\"\n            for c in checksums:\n                file_str = c[1]\n                if platform.system().lower() == \"windows\":\n                    file_str = c[1].as_posix().replace(\"\\\\\", \"/\")\n                checksums_str += \"{}:{}\\n\".format(c[0], file_str)\n            zip_file.writestr(\"checksums\", checksums_str)\n            # test if zip is ok\n            zip_file.testzip()\n        self._print(f\"All done, you can find new zip here: {zip_filename}\")\n\n    @staticmethod\n    def _replace_version(version: OpenPypeVersion, path: Path):\n        \"\"\"Replace version in version.py.\n\n        Args:\n            version (OpenPypeVersion): OpenPype version to set\n            path (Path): Path to unzipped version.\n\n        \"\"\"\n        with open(path / \"openpype\" / \"version.py\", \"r\") as op_version_file:\n            replacement = \"\"\n\n            for line in op_version_file:\n                stripped_line = line.strip()\n                if stripped_line.strip().startswith(\"__version__ =\"):\n                    line = f'__version__ = \"{version}\"\\n'\n                replacement += line\n\n        with open(path / \"openpype\" / \"version.py\", \"w\") as op_version_file:\n            op_version_file.write(replacement)\n\n\nif __name__ == '__main__':\n    print(sys.argv[1])\n    version_packer = VersionRepacker(sys.argv[1])\n    version_packer.process()\n"
  },
  {
    "path": "openpype/tools/resources/__init__.py",
    "content": "import os\n\nfrom qtpy import QtGui\n\n\ndef get_icon_path(icon_name=None, filename=None):\n    \"\"\"Path to image in './images' folder.\"\"\"\n    if icon_name is None and filename is None:\n        return None\n\n    if filename is None:\n        filename = \"{}.png\".format(icon_name)\n\n    path = os.path.join(\n        os.path.dirname(os.path.abspath(__file__)),\n        \"images\",\n        filename\n    )\n    if os.path.exists(path):\n        return path\n    return None\n\n\ndef get_image(icon_name=None, filename=None):\n    \"\"\"Load image from './images' as QImage.\"\"\"\n    path = get_icon_path(icon_name, filename)\n    if path:\n        return QtGui.QImage(path)\n    return None\n\n\ndef get_pixmap(icon_name=None, filename=None):\n    \"\"\"Load image from './images' as QPixmap.\"\"\"\n    path = get_icon_path(icon_name, filename)\n    if path:\n        return QtGui.QPixmap(path)\n    return None\n\n\ndef get_icon(icon_name=None, filename=None):\n    \"\"\"Load image from './images' as QICon.\"\"\"\n    pix = get_pixmap(icon_name, filename)\n    if pix:\n        return QtGui.QIcon(pix)\n    return None\n"
  },
  {
    "path": "openpype/tools/sceneinventory/__init__.py",
    "content": "from .window import (\n    show,\n    SceneInventoryWindow\n)\n\n__all__ = (\n    \"show\",\n    \"SceneInventoryWindow\"\n)\n"
  },
  {
    "path": "openpype/tools/sceneinventory/lib.py",
    "content": "def walk_hierarchy(node):\n    \"\"\"Recursively yield group node.\"\"\"\n    for child in node.children():\n        if child.get(\"isGroupNode\"):\n            yield child\n\n        for _child in walk_hierarchy(child):\n            yield _child\n"
  },
  {
    "path": "openpype/tools/sceneinventory/model.py",
    "content": "import re\nimport logging\n\nfrom collections import defaultdict\n\nfrom qtpy import QtCore, QtGui\nimport qtawesome\n\nfrom openpype.host import ILoadHost\nfrom openpype.client import (\n    get_asset_by_id,\n    get_subset_by_id,\n    get_version_by_id,\n    get_last_version_by_subset_id,\n    get_representation_by_id,\n)\nfrom openpype.pipeline import (\n    get_current_project_name,\n    schema,\n    HeroVersionType,\n    registered_host,\n)\nfrom openpype.style import get_default_entity_icon_color\nfrom openpype.tools.utils.models import TreeModel, Item\nfrom openpype.modules import ModulesManager\n\nfrom .lib import walk_hierarchy\n\n\nclass InventoryModel(TreeModel):\n    \"\"\"The model for the inventory\"\"\"\n\n    Columns = [\"Name\", \"version\", \"count\", \"family\",\n               \"group\", \"loader\", \"objectName\"]\n\n    OUTDATED_COLOR = QtGui.QColor(235, 30, 30)\n    CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30)\n    GRAYOUT_COLOR = QtGui.QColor(160, 160, 160)\n\n    UniqueRole = QtCore.Qt.UserRole + 2     # unique label role\n\n    def __init__(self, family_config_cache, parent=None):\n        super(InventoryModel, self).__init__(parent)\n        self.log = logging.getLogger(self.__class__.__name__)\n\n        self.family_config_cache = family_config_cache\n\n        self._hierarchy_view = False\n\n        self._default_icon_color = get_default_entity_icon_color()\n\n        manager = ModulesManager()\n        sync_server = manager.modules_by_name.get(\"sync_server\")\n        self.sync_enabled = (\n            sync_server is not None and sync_server.enabled\n        )\n        self._site_icons = {}\n        self.active_site = self.remote_site = None\n        self.active_provider = self.remote_provider = None\n\n        if not self.sync_enabled:\n            return\n\n        project_name = get_current_project_name()\n        active_site = sync_server.get_active_site(project_name)\n        remote_site = sync_server.get_remote_site(project_name)\n\n        active_provider = \"studio\"\n        remote_provider = \"studio\"\n        if active_site != \"studio\":\n            # sanitized for icon\n            active_provider = sync_server.get_provider_for_site(\n                project_name, active_site\n            )\n\n        if remote_site != \"studio\":\n            remote_provider = sync_server.get_provider_for_site(\n                project_name, remote_site\n            )\n\n        self.sync_server = sync_server\n        self.active_site = active_site\n        self.active_provider = active_provider\n        self.remote_site = remote_site\n        self.remote_provider = remote_provider\n        self._site_icons = {\n            provider: QtGui.QIcon(icon_path)\n            for provider, icon_path in sync_server.get_site_icons().items()\n        }\n        if \"active_site\" not in self.Columns:\n            self.Columns.append(\"active_site\")\n        if \"remote_site\" not in self.Columns:\n            self.Columns.append(\"remote_site\")\n\n    def outdated(self, item):\n        value = item.get(\"version\")\n        if isinstance(value, HeroVersionType):\n            return False\n\n        if item.get(\"version\") == item.get(\"highest_version\"):\n            return False\n        return True\n\n    def data(self, index, role):\n        if not index.isValid():\n            return\n\n        item = index.internalPointer()\n\n        if role == QtCore.Qt.FontRole:\n            # Make top-level entries bold\n            if item.get(\"isGroupNode\") or item.get(\"isNotSet\"):  # group-item\n                font = QtGui.QFont()\n                font.setBold(True)\n                return font\n\n        if role == QtCore.Qt.ForegroundRole:\n            # Set the text color to the OUTDATED_COLOR when the\n            # collected version is not the same as the highest version\n            key = self.Columns[index.column()]\n            if key == \"version\":  # version\n                if item.get(\"isGroupNode\"):  # group-item\n                    if self.outdated(item):\n                        return self.OUTDATED_COLOR\n\n                    if self._hierarchy_view:\n                        # If current group is not outdated, check if any\n                        # outdated children.\n                        for _node in walk_hierarchy(item):\n                            if self.outdated(_node):\n                                return self.CHILD_OUTDATED_COLOR\n                else:\n\n                    if self._hierarchy_view:\n                        # Although this is not a group item, we still need\n                        # to distinguish which one contain outdated child.\n                        for _node in walk_hierarchy(item):\n                            if self.outdated(_node):\n                                return self.CHILD_OUTDATED_COLOR.darker(150)\n\n                    return self.GRAYOUT_COLOR\n\n            if key == \"Name\" and not item.get(\"isGroupNode\"):\n                return self.GRAYOUT_COLOR\n\n        # Add icons\n        if role == QtCore.Qt.DecorationRole:\n            if index.column() == 0:\n                # Override color\n                color = item.get(\"color\", self._default_icon_color)\n                if item.get(\"isGroupNode\"):  # group-item\n                    return qtawesome.icon(\"fa.folder\", color=color)\n                if item.get(\"isNotSet\"):\n                    return qtawesome.icon(\"fa.exclamation-circle\", color=color)\n\n                return qtawesome.icon(\"fa.file-o\", color=color)\n\n            if index.column() == 3:\n                # Family icon\n                return item.get(\"familyIcon\", None)\n\n            column_name = self.Columns[index.column()]\n\n            if column_name == \"group\" and item.get(\"group\"):\n                return qtawesome.icon(\"fa.object-group\",\n                                      color=get_default_entity_icon_color())\n\n            if item.get(\"isGroupNode\"):\n                if column_name == \"active_site\":\n                    provider = item.get(\"active_site_provider\")\n                    return self._site_icons.get(provider)\n\n                if column_name == \"remote_site\":\n                    provider = item.get(\"remote_site_provider\")\n                    return self._site_icons.get(provider)\n\n        if role == QtCore.Qt.DisplayRole and item.get(\"isGroupNode\"):\n            column_name = self.Columns[index.column()]\n            progress = None\n            if column_name == 'active_site':\n                progress = item.get(\"active_site_progress\", 0)\n            elif column_name == 'remote_site':\n                progress = item.get(\"remote_site_progress\", 0)\n            if progress is not None:\n                return \"{}%\".format(max(progress, 0) * 100)\n\n        if role == self.UniqueRole:\n            return item[\"representation\"] + item.get(\"objectName\", \"<none>\")\n\n        return super(InventoryModel, self).data(index, role)\n\n    def set_hierarchy_view(self, state):\n        \"\"\"Set whether to display subsets in hierarchy view.\"\"\"\n        state = bool(state)\n\n        if state != self._hierarchy_view:\n            self._hierarchy_view = state\n\n    def refresh(self, selected=None, items=None):\n        \"\"\"Refresh the model\"\"\"\n\n        host = registered_host()\n        # for debugging or testing, injecting items from outside\n        if items is None:\n            if isinstance(host, ILoadHost):\n                items = host.get_containers()\n            elif hasattr(host, \"ls\"):\n                items = host.ls()\n            else:\n                items = []\n\n        self.clear()\n        if not selected or not self._hierarchy_view:\n            self.add_items(items)\n            return\n\n        if (\n            not hasattr(host, \"pipeline\")\n            or not hasattr(host.pipeline, \"update_hierarchy\")\n        ):\n            # If host doesn't support hierarchical containers, then\n            # cherry-pick only.\n            self.add_items((\n                item\n                for item in items\n                if item[\"objectName\"] in selected\n            ))\n            return\n\n        # TODO find out what this part does. Function 'update_hierarchy' is\n        #   available only in 'blender' at this moment.\n\n        # Update hierarchy info for all containers\n        items_by_name = {\n            item[\"objectName\"]: item\n            for item in host.pipeline.update_hierarchy(items)\n        }\n\n        selected_items = set()\n\n        def walk_children(names):\n            \"\"\"Select containers and extend to chlid containers\"\"\"\n            for name in [n for n in names if n not in selected_items]:\n                selected_items.add(name)\n                item = items_by_name[name]\n                yield item\n\n                for child in walk_children(item[\"children\"]):\n                    yield child\n\n        items = list(walk_children(selected))  # Cherry-picked and extended\n\n        # Cut unselected upstream containers\n        for item in items:\n            if not item.get(\"parent\") in selected_items:\n                # Parent not in selection, this is root item.\n                item[\"parent\"] = None\n\n        parents = [self._root_item]\n\n        # The length of `items` array is the maximum depth that a\n        # hierarchy could be.\n        # Take this as an easiest way to prevent looping forever.\n        maximum_loop = len(items)\n        count = 0\n        while items:\n            if count > maximum_loop:\n                self.log.warning(\"Maximum loop count reached, possible \"\n                                 \"missing parent node.\")\n                break\n\n            _parents = list()\n            for parent in parents:\n                _unparented = list()\n\n                def _children():\n                    \"\"\"Child item provider\"\"\"\n                    for item in items:\n                        if item.get(\"parent\") == parent.get(\"objectName\"):\n                            # (NOTE)\n                            # Since `self._root_node` has no \"objectName\"\n                            # entry, it will be paired with root item if\n                            # the value of key \"parent\" is None, or not\n                            # having the key.\n                            yield item\n                        else:\n                            # Not current parent's child, try next\n                            _unparented.append(item)\n\n                self.add_items(_children(), parent)\n\n                items[:] = _unparented\n\n                # Parents of next level\n                for group_node in parent.children():\n                    _parents += group_node.children()\n\n            parents[:] = _parents\n            count += 1\n\n    def add_items(self, items, parent=None):\n        \"\"\"Add the items to the model.\n\n        The items should be formatted similar to `api.ls()` returns, an item\n        is then represented as:\n            {\"filename_v001.ma\": [full/filename/of/loaded/filename_v001.ma,\n                                  full/filename/of/loaded/filename_v001.ma],\n             \"nodetype\" : \"reference\",\n             \"node\": \"referenceNode1\"}\n\n        Note: When performing an additional call to `add_items` it will *not*\n            group the new items with previously existing item groups of the\n            same type.\n\n        Args:\n            items (generator): the items to be processed as returned by `ls()`\n            parent (Item, optional): Set this item as parent for the added\n              items when provided. Defaults to the root of the model.\n\n        Returns:\n            node.Item: root node which has children added based on the data\n        \"\"\"\n\n        # NOTE: @iLLiCiTiT this need refactor\n        project_name = get_current_project_name()\n\n        self.beginResetModel()\n\n        # Group by representation\n        grouped = defaultdict(lambda: {\"items\": list()})\n        for item in items:\n            grouped[item[\"representation\"]][\"items\"].append(item)\n\n        # Add to model\n        not_found = defaultdict(list)\n        not_found_ids = []\n        for repre_id, group_dict in sorted(grouped.items()):\n            group_items = group_dict[\"items\"]\n            # Get parenthood per group\n            representation = get_representation_by_id(\n                project_name, repre_id\n            )\n            if not representation:\n                not_found[\"representation\"].extend(group_items)\n                not_found_ids.append(repre_id)\n                continue\n\n            version = get_version_by_id(\n                project_name, representation[\"parent\"]\n            )\n            if not version:\n                not_found[\"version\"].extend(group_items)\n                not_found_ids.append(repre_id)\n                continue\n\n            elif version[\"type\"] == \"hero_version\":\n                _version = get_version_by_id(\n                    project_name, version[\"version_id\"]\n                )\n                version[\"name\"] = HeroVersionType(_version[\"name\"])\n                version[\"data\"] = _version[\"data\"]\n\n            subset = get_subset_by_id(project_name, version[\"parent\"])\n            if not subset:\n                not_found[\"subset\"].extend(group_items)\n                not_found_ids.append(repre_id)\n                continue\n\n            asset = get_asset_by_id(project_name, subset[\"parent\"])\n            if not asset:\n                not_found[\"asset\"].extend(group_items)\n                not_found_ids.append(repre_id)\n                continue\n\n            grouped[repre_id].update({\n                \"representation\": representation,\n                \"version\": version,\n                \"subset\": subset,\n                \"asset\": asset\n            })\n\n        for id in not_found_ids:\n            grouped.pop(id)\n\n        for where, group_items in not_found.items():\n            # create the group header\n            group_node = Item()\n            name = \"< NOT FOUND - {} >\".format(where)\n            group_node[\"Name\"] = name\n            group_node[\"representation\"] = name\n            group_node[\"count\"] = len(group_items)\n            group_node[\"isGroupNode\"] = False\n            group_node[\"isNotSet\"] = True\n\n            self.add_child(group_node, parent=parent)\n\n            for item in group_items:\n                item_node = Item()\n                item_node.update(item)\n                item_node[\"Name\"] = item.get(\"objectName\", \"NO NAME\")\n                item_node[\"isNotFound\"] = True\n                self.add_child(item_node, parent=group_node)\n\n        for repre_id, group_dict in sorted(grouped.items()):\n            group_items = group_dict[\"items\"]\n            representation = grouped[repre_id][\"representation\"]\n            version = grouped[repre_id][\"version\"]\n            subset = grouped[repre_id][\"subset\"]\n            asset = grouped[repre_id][\"asset\"]\n\n            # Get the primary family\n            no_family = \"\"\n            maj_version, _ = schema.get_schema_version(subset[\"schema\"])\n            if maj_version < 3:\n                prim_family = version[\"data\"].get(\"family\")\n                if not prim_family:\n                    families = version[\"data\"].get(\"families\")\n                    prim_family = families[0] if families else no_family\n            else:\n                families = subset[\"data\"].get(\"families\") or []\n                prim_family = families[0] if families else no_family\n\n            # Get the label and icon for the family if in configuration\n            family_config = self.family_config_cache.family_config(prim_family)\n            family = family_config.get(\"label\", prim_family)\n            family_icon = family_config.get(\"icon\", None)\n\n            # Store the highest available version so the model can know\n            # whether current version is currently up-to-date.\n            highest_version = get_last_version_by_subset_id(\n                project_name, version[\"parent\"]\n            )\n\n            # create the group header\n            group_node = Item()\n            group_node[\"Name\"] = \"%s_%s: (%s)\" % (asset[\"name\"],\n                                                  subset[\"name\"],\n                                                  representation[\"name\"])\n            group_node[\"representation\"] = repre_id\n            group_node[\"version\"] = version[\"name\"]\n            group_node[\"highest_version\"] = highest_version[\"name\"]\n            group_node[\"family\"] = family\n            group_node[\"familyIcon\"] = family_icon\n            group_node[\"count\"] = len(group_items)\n            group_node[\"isGroupNode\"] = True\n            group_node[\"group\"] = subset[\"data\"].get(\"subsetGroup\")\n\n            if self.sync_enabled:\n                progress = self.sync_server.get_progress_for_repre(\n                    representation, self.active_site, self.remote_site\n                )\n                group_node[\"active_site\"] = self.active_site\n                group_node[\"active_site_provider\"] = self.active_provider\n                group_node[\"remote_site\"] = self.remote_site\n                group_node[\"remote_site_provider\"] = self.remote_provider\n                group_node[\"active_site_progress\"] = progress[self.active_site]\n                group_node[\"remote_site_progress\"] = progress[self.remote_site]\n\n            self.add_child(group_node, parent=parent)\n\n            for item in group_items:\n                item_node = Item()\n                item_node.update(item)\n\n                # store the current version on the item\n                item_node[\"version\"] = version[\"name\"]\n\n                # Remapping namespace to item name.\n                # Noted that the name key is capital \"N\", by doing this, we\n                # can view namespace in GUI without changing container data.\n                item_node[\"Name\"] = item[\"namespace\"]\n\n                self.add_child(item_node, parent=group_node)\n\n        self.endResetModel()\n\n        return self._root_item\n\n\nclass FilterProxyModel(QtCore.QSortFilterProxyModel):\n    \"\"\"Filter model to where key column's value is in the filtered tags\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(FilterProxyModel, self).__init__(*args, **kwargs)\n        self._filter_outdated = False\n        self._hierarchy_view = False\n\n    def filterAcceptsRow(self, row, parent):\n        model = self.sourceModel()\n        source_index = model.index(row, self.filterKeyColumn(), parent)\n\n        # Always allow bottom entries (individual containers), since their\n        # parent group hidden if it wouldn't have been validated.\n        rows = model.rowCount(source_index)\n        if not rows:\n            return True\n\n        # Filter by regex\n        if hasattr(self, \"filterRegExp\"):\n            regex = self.filterRegExp()\n        else:\n            regex = self.filterRegularExpression()\n        pattern = regex.pattern()\n        if pattern:\n            pattern = re.escape(pattern)\n\n            if not self._matches(row, parent, pattern):\n                return False\n\n        if self._filter_outdated:\n            # When filtering to outdated we filter the up to date entries\n            # thus we \"allow\" them when they are outdated\n            if not self._is_outdated(row, parent):\n                return False\n\n        return True\n\n    def set_filter_outdated(self, state):\n        \"\"\"Set whether to show the outdated entries only.\"\"\"\n        state = bool(state)\n\n        if state != self._filter_outdated:\n            self._filter_outdated = bool(state)\n            self.invalidateFilter()\n\n    def set_hierarchy_view(self, state):\n        state = bool(state)\n\n        if state != self._hierarchy_view:\n            self._hierarchy_view = state\n\n    def _is_outdated(self, row, parent):\n        \"\"\"Return whether row is outdated.\n\n        A row is considered outdated if it has \"version\" and \"highest_version\"\n        data and in the internal data structure, and they are not of an\n        equal value.\n\n        \"\"\"\n        def outdated(node):\n            version = node.get(\"version\", None)\n            highest = node.get(\"highest_version\", None)\n\n            # Always allow indices that have no version data at all\n            if version is None and highest is None:\n                return True\n\n            # If either a version or highest is present but not the other\n            # consider the item invalid.\n            if not self._hierarchy_view:\n                # Skip this check if in hierarchy view, or the child item\n                # node will be hidden even it's actually outdated.\n                if version is None or highest is None:\n                    return False\n            return version != highest\n\n        index = self.sourceModel().index(row, self.filterKeyColumn(), parent)\n\n        # The scene contents are grouped by \"representation\", e.g. the same\n        # \"representation\" loaded twice is grouped under the same header.\n        # Since the version check filters these parent groups we skip that\n        # check for the individual children.\n        has_parent = index.parent().isValid()\n        if has_parent and not self._hierarchy_view:\n            return True\n\n        # Filter to those that have the different version numbers\n        node = index.internalPointer()\n        if outdated(node):\n            return True\n\n        if self._hierarchy_view:\n            for _node in walk_hierarchy(node):\n                if outdated(_node):\n                    return True\n\n        return False\n\n    def _matches(self, row, parent, pattern):\n        \"\"\"Return whether row matches regex pattern.\n\n        Args:\n            row (int): row number in model\n            parent (QtCore.QModelIndex): parent index\n            pattern (regex.pattern): pattern to check for in key\n\n        Returns:\n            bool\n\n        \"\"\"\n        model = self.sourceModel()\n        column = self.filterKeyColumn()\n        role = self.filterRole()\n\n        def matches(row, parent, pattern):\n            index = model.index(row, column, parent)\n            key = model.data(index, role)\n            if re.search(pattern, key, re.IGNORECASE):\n                return True\n\n        if matches(row, parent, pattern):\n            return True\n\n        # Also allow if any of the children matches\n        source_index = model.index(row, column, parent)\n        rows = model.rowCount(source_index)\n\n        if any(\n            matches(idx, source_index, pattern)\n            for idx in range(rows)\n        ):\n            return True\n\n        if not self._hierarchy_view:\n            return False\n\n        for idx in range(rows):\n            child_index = model.index(idx, column, source_index)\n            child_rows = model.rowCount(child_index)\n            return any(\n                self._matches(child_idx, child_index, pattern)\n                for child_idx in range(child_rows)\n            )\n\n        return True\n"
  },
  {
    "path": "openpype/tools/sceneinventory/switch_dialog.py",
    "content": "import collections\nimport logging\nfrom qtpy import QtWidgets, QtCore\nimport qtawesome\n\nfrom openpype.client import (\n    get_asset_by_name,\n    get_assets,\n    get_subset_by_name,\n    get_subsets,\n    get_versions,\n    get_hero_versions,\n    get_last_versions,\n    get_representations,\n)\nfrom openpype.pipeline import legacy_io\nfrom openpype.pipeline.load import (\n    discover_loader_plugins,\n    switch_container,\n    get_repres_contexts,\n    loaders_from_repre_context,\n    LoaderSwitchNotImplementedError,\n    IncompatibleLoaderError,\n    LoaderNotFoundError\n)\n\nfrom .widgets import (\n    ButtonWithMenu,\n    SearchComboBox\n)\n\nlog = logging.getLogger(\"SwitchAssetDialog\")\n\n\nclass ValidationState:\n    def __init__(self):\n        self.asset_ok = True\n        self.subset_ok = True\n        self.repre_ok = True\n\n    @property\n    def all_ok(self):\n        return (\n            self.asset_ok\n            and self.subset_ok\n            and self.repre_ok\n        )\n\n\nclass SwitchAssetDialog(QtWidgets.QDialog):\n    \"\"\"Widget to support asset switching\"\"\"\n\n    MIN_WIDTH = 550\n\n    switched = QtCore.Signal()\n\n    def __init__(self, parent=None, items=None):\n        super(SwitchAssetDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"Switch selected items ...\")\n\n        # Force and keep focus dialog\n        self.setModal(True)\n\n        assets_combox = SearchComboBox(self)\n        subsets_combox = SearchComboBox(self)\n        repres_combobox = SearchComboBox(self)\n\n        assets_combox.set_placeholder(\"<asset>\")\n        subsets_combox.set_placeholder(\"<subset>\")\n        repres_combobox.set_placeholder(\"<representation>\")\n\n        asset_label = QtWidgets.QLabel(self)\n        subset_label = QtWidgets.QLabel(self)\n        repre_label = QtWidgets.QLabel(self)\n\n        current_asset_btn = QtWidgets.QPushButton(\"Use current asset\")\n\n        accept_icon = qtawesome.icon(\"fa.check\", color=\"white\")\n        accept_btn = ButtonWithMenu(self)\n        accept_btn.setIcon(accept_icon)\n\n        main_layout = QtWidgets.QGridLayout(self)\n        # Asset column\n        main_layout.addWidget(current_asset_btn, 0, 0)\n        main_layout.addWidget(assets_combox, 1, 0)\n        main_layout.addWidget(asset_label, 2, 0)\n        # Subset column\n        main_layout.addWidget(subsets_combox, 1, 1)\n        main_layout.addWidget(subset_label, 2, 1)\n        # Representation column\n        main_layout.addWidget(repres_combobox, 1, 2)\n        main_layout.addWidget(repre_label, 2, 2)\n        # Btn column\n        main_layout.addWidget(accept_btn, 1, 3)\n        main_layout.setColumnStretch(0, 1)\n        main_layout.setColumnStretch(1, 1)\n        main_layout.setColumnStretch(2, 1)\n        main_layout.setColumnStretch(3, 0)\n\n        assets_combox.currentIndexChanged.connect(\n            self._combobox_value_changed\n        )\n        subsets_combox.currentIndexChanged.connect(\n            self._combobox_value_changed\n        )\n        repres_combobox.currentIndexChanged.connect(\n            self._combobox_value_changed\n        )\n        accept_btn.clicked.connect(self._on_accept)\n        current_asset_btn.clicked.connect(self._on_current_asset)\n\n        self._current_asset_btn = current_asset_btn\n\n        self._assets_box = assets_combox\n        self._subsets_box = subsets_combox\n        self._representations_box = repres_combobox\n\n        self._asset_label = asset_label\n        self._subset_label = subset_label\n        self._repre_label = repre_label\n\n        self._accept_btn = accept_btn\n\n        self.setMinimumWidth(self.MIN_WIDTH)\n\n        # Set default focus to accept button so you don't directly type in\n        # first asset field, this also allows to see the placeholder value.\n        accept_btn.setFocus()\n\n        self.content_loaders = set()\n        self.content_assets = {}\n        self.content_subsets = {}\n        self.content_versions = {}\n        self.content_repres = {}\n\n        self.hero_version_ids = set()\n\n        self.missing_assets = []\n        self.missing_versions = []\n        self.missing_subsets = []\n        self.missing_repres = []\n        self.missing_docs = False\n\n        self.archived_assets = []\n        self.archived_subsets = []\n        self.archived_repres = []\n\n        self._init_asset_name = None\n        self._init_subset_name = None\n        self._init_repre_name = None\n\n        self._fill_check = False\n\n        self._items = items\n        self._prepare_content_data()\n        self.refresh(True)\n\n    def active_project(self):\n        return legacy_io.active_project()\n\n    def _prepare_content_data(self):\n        repre_ids = set()\n        content_loaders = set()\n        for item in self._items:\n            repre_ids.add(str(item[\"representation\"]))\n            content_loaders.add(item[\"loader\"])\n\n        project_name = self.active_project()\n        repres = list(get_representations(\n            project_name,\n            representation_ids=repre_ids,\n            archived=True\n        ))\n        repres_by_id = {str(repre[\"_id\"]): repre for repre in repres}\n\n        # stash context values, works only for single representation\n        if len(repres) == 1:\n            self._init_asset_name = repres[0][\"context\"][\"asset\"]\n            self._init_subset_name = repres[0][\"context\"][\"subset\"]\n            self._init_repre_name = repres[0][\"context\"][\"representation\"]\n\n        content_repres = {}\n        archived_repres = []\n        missing_repres = []\n        version_ids = set()\n        for repre_id in repre_ids:\n            if repre_id not in repres_by_id:\n                missing_repres.append(repre_id)\n            elif repres_by_id[repre_id][\"type\"] == \"archived_representation\":\n                repre = repres_by_id[repre_id]\n                archived_repres.append(repre)\n                version_ids.add(repre[\"parent\"])\n            else:\n                repre = repres_by_id[repre_id]\n                content_repres[repre_id] = repres_by_id[repre_id]\n                version_ids.add(repre[\"parent\"])\n\n        versions = get_versions(\n            project_name,\n            version_ids=version_ids,\n            hero=True\n        )\n        content_versions = {}\n        hero_version_ids = set()\n        for version in versions:\n            content_versions[version[\"_id\"]] = version\n            if version[\"type\"] == \"hero_version\":\n                hero_version_ids.add(version[\"_id\"])\n\n        missing_versions = []\n        subset_ids = []\n        for version_id in version_ids:\n            if version_id not in content_versions:\n                missing_versions.append(version_id)\n            else:\n                subset_ids.append(content_versions[version_id][\"parent\"])\n\n        subsets = get_subsets(\n            project_name, subset_ids=subset_ids, archived=True\n        )\n        subsets_by_id = {sub[\"_id\"]: sub for sub in subsets}\n\n        asset_ids = []\n        archived_subsets = []\n        missing_subsets = []\n        content_subsets = {}\n        for subset_id in subset_ids:\n            if subset_id not in subsets_by_id:\n                missing_subsets.append(subset_id)\n            elif subsets_by_id[subset_id][\"type\"] == \"archived_subset\":\n                subset = subsets_by_id[subset_id]\n                asset_ids.append(subset[\"parent\"])\n                archived_subsets.append(subset)\n            else:\n                subset = subsets_by_id[subset_id]\n                asset_ids.append(subset[\"parent\"])\n                content_subsets[subset_id] = subset\n\n        assets = get_assets(project_name, asset_ids=asset_ids, archived=True)\n        assets_by_id = {asset[\"_id\"]: asset for asset in assets}\n\n        missing_assets = []\n        archived_assets = []\n        content_assets = {}\n        for asset_id in asset_ids:\n            if asset_id not in assets_by_id:\n                missing_assets.append(asset_id)\n            elif assets_by_id[asset_id][\"type\"] == \"archived_asset\":\n                archived_assets.append(assets_by_id[asset_id])\n            else:\n                content_assets[asset_id] = assets_by_id[asset_id]\n\n        self.content_loaders = content_loaders\n        self.content_assets = content_assets\n        self.content_subsets = content_subsets\n        self.content_versions = content_versions\n        self.content_repres = content_repres\n\n        self.hero_version_ids = hero_version_ids\n\n        self.missing_assets = missing_assets\n        self.missing_versions = missing_versions\n        self.missing_subsets = missing_subsets\n        self.missing_repres = missing_repres\n        self.missing_docs = (\n            bool(missing_assets)\n            or bool(missing_versions)\n            or bool(missing_subsets)\n            or bool(missing_repres)\n        )\n\n        self.archived_assets = archived_assets\n        self.archived_subsets = archived_subsets\n        self.archived_repres = archived_repres\n\n    def _combobox_value_changed(self, *args, **kwargs):\n        self.refresh()\n\n    def refresh(self, init_refresh=False):\n        \"\"\"Build the need comboboxes with content\"\"\"\n        if not self._fill_check and not init_refresh:\n            return\n\n        self._fill_check = False\n\n        if init_refresh:\n            asset_values = self._get_asset_box_values()\n            self._fill_combobox(asset_values, \"asset\")\n\n        validation_state = ValidationState()\n\n        # Set other comboboxes to empty if any document is missing or any asset\n        # of loaded representations is archived.\n        self._is_asset_ok(validation_state)\n        if validation_state.asset_ok:\n            subset_values = self._get_subset_box_values()\n            self._fill_combobox(subset_values, \"subset\")\n            self._is_subset_ok(validation_state)\n\n        if validation_state.asset_ok and validation_state.subset_ok:\n            repre_values = sorted(self._representations_box_values())\n            self._fill_combobox(repre_values, \"repre\")\n            self._is_repre_ok(validation_state)\n\n        # Fill comboboxes with values\n        self.set_labels()\n\n        self.apply_validations(validation_state)\n\n        self._build_loaders_menu()\n\n        if init_refresh:  # pre select context if possible\n            self._assets_box.set_valid_value(self._init_asset_name)\n            self._subsets_box.set_valid_value(self._init_subset_name)\n            self._representations_box.set_valid_value(self._init_repre_name)\n\n        self._fill_check = True\n\n    def _build_loaders_menu(self):\n        repre_ids = self._get_current_output_repre_ids()\n        loaders = self._get_loaders(repre_ids)\n        # Get and destroy the action group\n        self._accept_btn.clear_actions()\n\n        if not loaders:\n            return\n\n        # Build new action group\n        group = QtWidgets.QActionGroup(self._accept_btn)\n\n        for loader in loaders:\n            # Label\n            label = getattr(loader, \"label\", None)\n            if label is None:\n                label = loader.__name__\n\n            action = group.addAction(label)\n            # action = QtWidgets.QAction(label)\n            action.setData(loader)\n\n            # Support font-awesome icons using the `.icon` and `.color`\n            # attributes on plug-ins.\n            icon = getattr(loader, \"icon\", None)\n            if icon is not None:\n                try:\n                    key = \"fa.{0}\".format(icon)\n                    color = getattr(loader, \"color\", \"white\")\n                    action.setIcon(qtawesome.icon(key, color=color))\n\n                except Exception as exc:\n                    print(\"Unable to set icon for loader {}: {}\".format(\n                        loader, str(exc)\n                    ))\n\n            self._accept_btn.add_action(action)\n\n        group.triggered.connect(self._on_action_clicked)\n\n    def _on_action_clicked(self, action):\n        loader_plugin = action.data()\n        self._trigger_switch(loader_plugin)\n\n    def _get_loaders(self, repre_ids):\n        repre_contexts = None\n        if repre_ids:\n            repre_contexts = get_repres_contexts(repre_ids)\n\n        if not repre_contexts:\n            return list()\n\n        available_loaders = []\n        for loader_plugin in discover_loader_plugins():\n            # Skip loaders without switch method\n            if not hasattr(loader_plugin, \"switch\"):\n                continue\n\n            # Skip utility loaders\n            if (\n                hasattr(loader_plugin, \"is_utility\")\n                and loader_plugin.is_utility\n            ):\n                continue\n            available_loaders.append(loader_plugin)\n\n        loaders = None\n        for repre_context in repre_contexts.values():\n            _loaders = set(loaders_from_repre_context(\n                available_loaders, repre_context\n            ))\n            if loaders is None:\n                loaders = _loaders\n            else:\n                loaders = _loaders.intersection(loaders)\n\n            if not loaders:\n                break\n\n        if loaders is None:\n            loaders = []\n        else:\n            loaders = list(loaders)\n\n        return loaders\n\n    def _fill_combobox(self, values, combobox_type):\n        if combobox_type == \"asset\":\n            combobox_widget = self._assets_box\n        elif combobox_type == \"subset\":\n            combobox_widget = self._subsets_box\n        elif combobox_type == \"repre\":\n            combobox_widget = self._representations_box\n        else:\n            return\n        selected_value = combobox_widget.get_valid_value()\n\n        # Fill combobox\n        if values is not None:\n            combobox_widget.populate(list(sorted(values)))\n            if selected_value and selected_value in values:\n                index = None\n                for idx in range(combobox_widget.count()):\n                    if selected_value == str(combobox_widget.itemText(idx)):\n                        index = idx\n                        break\n                if index is not None:\n                    combobox_widget.setCurrentIndex(index)\n\n    def set_labels(self):\n        asset_label = self._assets_box.get_valid_value()\n        subset_label = self._subsets_box.get_valid_value()\n        repre_label = self._representations_box.get_valid_value()\n\n        default = \"*No changes\"\n        self._asset_label.setText(asset_label or default)\n        self._subset_label.setText(subset_label or default)\n        self._repre_label.setText(repre_label or default)\n\n    def apply_validations(self, validation_state):\n        error_msg = \"*Please select\"\n        error_sheet = \"border: 1px solid red;\"\n\n        asset_sheet = None\n        subset_sheet = None\n        repre_sheet = None\n        accept_state = \"\"\n        if validation_state.asset_ok is False:\n            asset_sheet = error_sheet\n            self._asset_label.setText(error_msg)\n        elif validation_state.subset_ok is False:\n            subset_sheet = error_sheet\n            self._subset_label.setText(error_msg)\n        elif validation_state.repre_ok is False:\n            repre_sheet = error_sheet\n            self._repre_label.setText(error_msg)\n\n        if validation_state.all_ok:\n            accept_state = \"1\"\n\n        self._assets_box.setStyleSheet(asset_sheet or \"\")\n        self._subsets_box.setStyleSheet(subset_sheet or \"\")\n        self._representations_box.setStyleSheet(repre_sheet or \"\")\n\n        self._accept_btn.setEnabled(validation_state.all_ok)\n        self._set_style_property(self._accept_btn, \"state\", accept_state)\n\n    def _set_style_property(self, widget, name, value):\n        cur_value = widget.property(name)\n        if cur_value == value:\n            return\n        widget.setProperty(name, value)\n        widget.style().polish(widget)\n\n    def _get_current_output_repre_ids(self):\n        # NOTE hero versions are not used because it is expected that\n        # hero version has same representations as latests\n        selected_asset = self._assets_box.currentText()\n        selected_subset = self._subsets_box.currentText()\n        selected_repre = self._representations_box.currentText()\n\n        # Nothing is selected\n        # [ ] [ ] [ ]\n        if not selected_asset and not selected_subset and not selected_repre:\n            return list(self.content_repres.keys())\n\n        # Prepare asset document if asset is selected\n        asset_doc = None\n        if selected_asset:\n            asset_doc = get_asset_by_name(\n                self.active_project(),\n                selected_asset,\n                fields=[\"_id\"]\n            )\n            if not asset_doc:\n                return []\n\n        # Everything is selected\n        # [x] [x] [x]\n        if selected_asset and selected_subset and selected_repre:\n            return self._get_current_output_repre_ids_xxx(\n                asset_doc, selected_subset, selected_repre\n            )\n\n        # [x] [x] [ ]\n        # If asset and subset is selected\n        if selected_asset and selected_subset:\n            return self._get_current_output_repre_ids_xxo(\n                asset_doc, selected_subset\n            )\n\n        # [x] [ ] [x]\n        # If asset and repre is selected\n        if selected_asset and selected_repre:\n            return self._get_current_output_repre_ids_xox(\n                asset_doc, selected_repre\n            )\n\n        # [x] [ ] [ ]\n        # If asset and subset is selected\n        if selected_asset:\n            return self._get_current_output_repre_ids_xoo(asset_doc)\n\n        # [ ] [x] [x]\n        if selected_subset and selected_repre:\n            return self._get_current_output_repre_ids_oxx(\n                selected_subset, selected_repre\n            )\n\n        # [ ] [x] [ ]\n        if selected_subset:\n            return self._get_current_output_repre_ids_oxo(\n                selected_subset\n            )\n\n        # [ ] [ ] [x]\n        return self._get_current_output_repre_ids_oox(selected_repre)\n\n    def _get_current_output_repre_ids_xxx(\n        self, asset_doc, selected_subset, selected_repre\n    ):\n        project_name = self.active_project()\n        subset_doc = get_subset_by_name(\n            project_name,\n            selected_subset,\n            asset_doc[\"_id\"],\n            fields=[\"_id\"]\n        )\n\n        subset_id = subset_doc[\"_id\"]\n        last_versions_by_subset_id = self.find_last_versions([subset_id])\n        version_doc = last_versions_by_subset_id.get(subset_id)\n        if not version_doc:\n            return []\n\n        repre_docs = get_representations(\n            project_name,\n            version_ids=[version_doc[\"_id\"]],\n            representation_names=[selected_repre],\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_current_output_repre_ids_xxo(self, asset_doc, selected_subset):\n        project_name = self.active_project()\n        subset_doc = get_subset_by_name(\n            project_name,\n            selected_subset,\n            asset_doc[\"_id\"],\n            fields=[\"_id\"]\n        )\n        if not subset_doc:\n            return []\n\n        repre_names = set()\n        for repre_doc in self.content_repres.values():\n            repre_names.add(repre_doc[\"name\"])\n\n        # TODO where to take version ids?\n        version_ids = []\n        repre_docs = get_representations(\n            project_name,\n            representation_names=repre_names,\n            version_ids=version_ids,\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_current_output_repre_ids_xox(self, asset_doc, selected_repre):\n        subset_names = set()\n        for subset_doc in self.content_subsets.values():\n            subset_names.add(subset_doc[\"name\"])\n\n        project_name = self.active_project()\n        subset_docs = get_subsets(\n            project_name,\n            asset_ids=[asset_doc[\"_id\"]],\n            subset_names=subset_names,\n            fields=[\"_id\", \"name\"]\n        )\n        subset_name_by_id = {\n            subset_doc[\"_id\"]: subset_doc[\"name\"]\n            for subset_doc in subset_docs\n        }\n        subset_ids = list(subset_name_by_id.keys())\n        last_versions_by_subset_id = self.find_last_versions(subset_ids)\n        last_version_id_by_subset_name = {}\n        for subset_id, last_version in last_versions_by_subset_id.items():\n            subset_name = subset_name_by_id[subset_id]\n            last_version_id_by_subset_name[subset_name] = (\n                last_version[\"_id\"]\n            )\n\n        repre_docs = get_representations(\n            project_name,\n            version_ids=last_version_id_by_subset_name.values(),\n            representation_names=[selected_repre],\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_current_output_repre_ids_xoo(self, asset_doc):\n        project_name = self.active_project()\n        repres_by_subset_name = collections.defaultdict(set)\n        for repre_doc in self.content_repres.values():\n            repre_name = repre_doc[\"name\"]\n            version_doc = self.content_versions[repre_doc[\"parent\"]]\n            subset_doc = self.content_subsets[version_doc[\"parent\"]]\n            subset_name = subset_doc[\"name\"]\n            repres_by_subset_name[subset_name].add(repre_name)\n\n        subset_docs = list(get_subsets(\n            project_name,\n            asset_ids=[asset_doc[\"_id\"]],\n            subset_names=repres_by_subset_name.keys(),\n            fields=[\"_id\", \"name\"]\n        ))\n        subset_name_by_id = {\n            subset_doc[\"_id\"]: subset_doc[\"name\"]\n            for subset_doc in subset_docs\n        }\n        subset_ids = list(subset_name_by_id.keys())\n        last_versions_by_subset_id = self.find_last_versions(subset_ids)\n        last_version_id_by_subset_name = {}\n        for subset_id, last_version in last_versions_by_subset_id.items():\n            subset_name = subset_name_by_id[subset_id]\n            last_version_id_by_subset_name[subset_name] = (\n                last_version[\"_id\"]\n            )\n\n        repre_names_by_version_id = {}\n        for subset_name, repre_names in repres_by_subset_name.items():\n            version_id = last_version_id_by_subset_name.get(subset_name)\n            # This should not happen but why to crash?\n            if version_id is not None:\n                repre_names_by_version_id[version_id] = list(repre_names)\n\n        repre_docs = get_representations(\n            project_name,\n            names_by_version_ids=repre_names_by_version_id,\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_current_output_repre_ids_oxx(\n        self, selected_subset, selected_repre\n    ):\n        project_name = self.active_project()\n        subset_docs = get_subsets(\n            project_name,\n            asset_ids=self.content_assets.keys(),\n            subset_names=[selected_subset],\n            fields=[\"_id\"]\n        )\n        subset_ids = [subset_doc[\"_id\"] for subset_doc in subset_docs]\n        last_versions_by_subset_id = self.find_last_versions(subset_ids)\n        last_version_ids = [\n            last_version[\"_id\"]\n            for last_version in last_versions_by_subset_id.values()\n        ]\n        repre_docs = get_representations(\n            project_name,\n            version_ids=last_version_ids,\n            representation_names=[selected_repre],\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_current_output_repre_ids_oxo(self, selected_subset):\n        project_name = self.active_project()\n        subset_docs = get_subsets(\n            project_name,\n            asset_ids=self.content_assets.keys(),\n            subset_names=[selected_subset],\n            fields=[\"_id\", \"parent\"]\n        )\n        subset_docs_by_id = {\n            subset_doc[\"_id\"]: subset_doc\n            for subset_doc in subset_docs\n        }\n        if not subset_docs:\n            return list()\n\n        last_versions_by_subset_id = self.find_last_versions(\n            subset_docs_by_id.keys()\n        )\n\n        subset_id_by_version_id = {}\n        for subset_id, last_version in last_versions_by_subset_id.items():\n            version_id = last_version[\"_id\"]\n            subset_id_by_version_id[version_id] = subset_id\n\n        if not subset_id_by_version_id:\n            return list()\n\n        repre_names_by_asset_id = collections.defaultdict(set)\n        for repre_doc in self.content_repres.values():\n            version_doc = self.content_versions[repre_doc[\"parent\"]]\n            subset_doc = self.content_subsets[version_doc[\"parent\"]]\n            asset_doc = self.content_assets[subset_doc[\"parent\"]]\n            repre_name = repre_doc[\"name\"]\n            asset_id = asset_doc[\"_id\"]\n            repre_names_by_asset_id[asset_id].add(repre_name)\n\n        repre_names_by_version_id = {}\n        for last_version_id, subset_id in subset_id_by_version_id.items():\n            subset_doc = subset_docs_by_id[subset_id]\n            asset_id = subset_doc[\"parent\"]\n            repre_names = repre_names_by_asset_id.get(asset_id)\n            if not repre_names:\n                continue\n            repre_names_by_version_id[last_version_id] = repre_names\n\n        repre_docs = get_representations(\n            project_name,\n            names_by_version_ids=repre_names_by_version_id,\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_current_output_repre_ids_oox(self, selected_repre):\n        project_name = self.active_project()\n        repre_docs = get_representations(\n            project_name,\n            representation_names=[selected_repre],\n            version_ids=self.content_versions.keys(),\n            fields=[\"_id\"]\n        )\n        return [repre_doc[\"_id\"] for repre_doc in repre_docs]\n\n    def _get_asset_box_values(self):\n        project_name = self.active_project()\n        asset_docs = get_assets(project_name, fields=[\"_id\", \"name\"])\n        asset_names_by_id = {\n            asset_doc[\"_id\"]: asset_doc[\"name\"]\n            for asset_doc in asset_docs\n        }\n        subsets = get_subsets(\n            project_name,\n            asset_ids=asset_names_by_id.keys(),\n            fields=[\"parent\"]\n        )\n        filtered_assets = []\n        for subset in subsets:\n            asset_name = asset_names_by_id[subset[\"parent\"]]\n            if asset_name not in filtered_assets:\n                filtered_assets.append(asset_name)\n        return sorted(filtered_assets)\n\n    def _get_subset_box_values(self):\n        project_name = self.active_project()\n        selected_asset = self._assets_box.get_valid_value()\n        if selected_asset:\n            asset_doc = get_asset_by_name(\n                project_name, selected_asset, fields=[\"_id\"]\n            )\n            asset_ids = [asset_doc[\"_id\"]]\n        else:\n            asset_ids = list(self.content_assets.keys())\n\n        subsets = get_subsets(\n            project_name,\n            asset_ids=asset_ids,\n            fields=[\"parent\", \"name\"]\n        )\n\n        subset_names_by_parent_id = collections.defaultdict(set)\n        for subset in subsets:\n            subset_names_by_parent_id[subset[\"parent\"]].add(subset[\"name\"])\n\n        possible_subsets = None\n        for subset_names in subset_names_by_parent_id.values():\n            if possible_subsets is None:\n                possible_subsets = subset_names\n            else:\n                possible_subsets = (possible_subsets & subset_names)\n\n            if not possible_subsets:\n                break\n\n        return list(possible_subsets or list())\n\n    def _representations_box_values(self):\n        # NOTE hero versions are not used because it is expected that\n        # hero version has same representations as latests\n        project_name = self.active_project()\n        selected_asset = self._assets_box.currentText()\n        selected_subset = self._subsets_box.currentText()\n\n        # If nothing is selected\n        # [ ] [ ] [?]\n        if not selected_asset and not selected_subset:\n            # Find all representations of selection's subsets\n            possible_repres = get_representations(\n                project_name,\n                version_ids=self.content_versions.keys(),\n                fields=[\"parent\", \"name\"]\n            )\n\n            possible_repres_by_parent = collections.defaultdict(set)\n            for repre in possible_repres:\n                possible_repres_by_parent[repre[\"parent\"]].add(repre[\"name\"])\n\n            output_repres = None\n            for repre_names in possible_repres_by_parent.values():\n                if output_repres is None:\n                    output_repres = repre_names\n                else:\n                    output_repres = (output_repres & repre_names)\n\n                if not output_repres:\n                    break\n\n            return list(output_repres or list())\n\n        # [x] [x] [?]\n        if selected_asset and selected_subset:\n            asset_doc = get_asset_by_name(\n                project_name, selected_asset, fields=[\"_id\"]\n            )\n            subset_doc = get_subset_by_name(\n                project_name,\n                selected_subset,\n                asset_doc[\"_id\"],\n                fields=[\"_id\"]\n            )\n\n            subset_id = subset_doc[\"_id\"]\n            last_versions_by_subset_id = self.find_last_versions([subset_id])\n            version_doc = last_versions_by_subset_id.get(subset_id)\n            repre_docs = get_representations(\n                project_name,\n                version_ids=[version_doc[\"_id\"]],\n                fields=[\"name\"]\n            )\n            return [\n                repre_doc[\"name\"]\n                for repre_doc in repre_docs\n            ]\n\n        # [x] [ ] [?]\n        # If asset only is selected\n        if selected_asset:\n            asset_doc = get_asset_by_name(\n                project_name, selected_asset, fields=[\"_id\"]\n            )\n            if not asset_doc:\n                return list()\n\n            # Filter subsets by subset names from content\n            subset_names = set()\n            for subset_doc in self.content_subsets.values():\n                subset_names.add(subset_doc[\"name\"])\n\n            subset_docs = get_subsets(\n                project_name,\n                asset_ids=[asset_doc[\"_id\"]],\n                subset_names=subset_names,\n                fields=[\"_id\"]\n            )\n            subset_ids = [\n                subset_doc[\"_id\"]\n                for subset_doc in subset_docs\n            ]\n            if not subset_ids:\n                return list()\n\n            last_versions_by_subset_id = self.find_last_versions(subset_ids)\n            subset_id_by_version_id = {}\n            for subset_id, last_version in last_versions_by_subset_id.items():\n                version_id = last_version[\"_id\"]\n                subset_id_by_version_id[version_id] = subset_id\n\n            if not subset_id_by_version_id:\n                return list()\n\n            repre_docs = list(get_representations(\n                project_name,\n                version_ids=subset_id_by_version_id.keys(),\n                fields=[\"name\", \"parent\"]\n            ))\n            if not repre_docs:\n                return list()\n\n            repre_names_by_parent = collections.defaultdict(set)\n            for repre_doc in repre_docs:\n                repre_names_by_parent[repre_doc[\"parent\"]].add(\n                    repre_doc[\"name\"]\n                )\n\n            available_repres = None\n            for repre_names in repre_names_by_parent.values():\n                if available_repres is None:\n                    available_repres = repre_names\n                    continue\n\n                available_repres = available_repres.intersection(repre_names)\n\n            return list(available_repres)\n\n        # [ ] [x] [?]\n        subset_docs = list(get_subsets(\n            project_name,\n            asset_ids=self.content_assets.keys(),\n            subset_names=[selected_subset],\n            fields=[\"_id\", \"parent\"]\n        ))\n        if not subset_docs:\n            return list()\n\n        subset_docs_by_id = {\n            subset_doc[\"_id\"]: subset_doc\n            for subset_doc in subset_docs\n        }\n        last_versions_by_subset_id = self.find_last_versions(\n            subset_docs_by_id.keys()\n        )\n\n        subset_id_by_version_id = {}\n        for subset_id, last_version in last_versions_by_subset_id.items():\n            version_id = last_version[\"_id\"]\n            subset_id_by_version_id[version_id] = subset_id\n\n        if not subset_id_by_version_id:\n            return list()\n\n        repre_docs = list(\n            get_representations(\n                project_name,\n                version_ids=subset_id_by_version_id.keys(),\n                fields=[\"name\", \"parent\"]\n            )\n        )\n        if not repre_docs:\n            return list()\n\n        repre_names_by_asset_id = {}\n        for repre_doc in repre_docs:\n            subset_id = subset_id_by_version_id[repre_doc[\"parent\"]]\n            asset_id = subset_docs_by_id[subset_id][\"parent\"]\n            if asset_id not in repre_names_by_asset_id:\n                repre_names_by_asset_id[asset_id] = set()\n            repre_names_by_asset_id[asset_id].add(repre_doc[\"name\"])\n\n        available_repres = None\n        for repre_names in repre_names_by_asset_id.values():\n            if available_repres is None:\n                available_repres = repre_names\n                continue\n\n            available_repres = available_repres.intersection(repre_names)\n\n        return list(available_repres)\n\n    def _is_asset_ok(self, validation_state):\n        selected_asset = self._assets_box.get_valid_value()\n        if (\n            selected_asset is None\n            and (self.missing_docs or self.archived_assets)\n        ):\n            validation_state.asset_ok = False\n\n    def _is_subset_ok(self, validation_state):\n        selected_asset = self._assets_box.get_valid_value()\n        selected_subset = self._subsets_box.get_valid_value()\n\n        # [?] [x] [?]\n        # If subset is selected then must be ok\n        if selected_subset is not None:\n            return\n\n        # [ ] [ ] [?]\n        if selected_asset is None:\n            # If there were archived subsets and asset is not selected\n            if self.archived_subsets:\n                validation_state.subset_ok = False\n            return\n\n        # [x] [ ] [?]\n        project_name = self.active_project()\n        asset_doc = get_asset_by_name(\n            project_name, selected_asset, fields=[\"_id\"]\n        )\n        subset_docs = get_subsets(\n            project_name, asset_ids=[asset_doc[\"_id\"]], fields=[\"name\"]\n        )\n\n        subset_names = set(\n            subset_doc[\"name\"]\n            for subset_doc in subset_docs\n        )\n\n        for subset_doc in self.content_subsets.values():\n            if subset_doc[\"name\"] not in subset_names:\n                validation_state.subset_ok = False\n                break\n\n    def find_last_versions(self, subset_ids):\n        project_name = self.active_project()\n        return get_last_versions(\n            project_name,\n            subset_ids=subset_ids,\n            fields=[\"_id\", \"parent\", \"type\"]\n        )\n\n    def _is_repre_ok(self, validation_state):\n        selected_asset = self._assets_box.get_valid_value()\n        selected_subset = self._subsets_box.get_valid_value()\n        selected_repre = self._representations_box.get_valid_value()\n\n        # [?] [?] [x]\n        # If subset is selected then must be ok\n        if selected_repre is not None:\n            return\n\n        # [ ] [ ] [ ]\n        if selected_asset is None and selected_subset is None:\n            if (\n                self.archived_repres\n                or self.missing_versions\n                or self.missing_repres\n            ):\n                validation_state.repre_ok = False\n            return\n\n        # [x] [x] [ ]\n        project_name = self.active_project()\n        if selected_asset is not None and selected_subset is not None:\n            asset_doc = get_asset_by_name(\n                project_name, selected_asset, fields=[\"_id\"]\n            )\n            subset_doc = get_subset_by_name(\n                project_name,\n                selected_subset,\n                asset_doc[\"_id\"],\n                fields=[\"_id\"]\n            )\n            subset_id = subset_doc[\"_id\"]\n            last_versions_by_subset_id = self.find_last_versions([subset_id])\n            last_version = last_versions_by_subset_id.get(subset_id)\n            if not last_version:\n                validation_state.repre_ok = False\n                return\n\n            repre_docs = get_representations(\n                project_name,\n                version_ids=[last_version[\"_id\"]],\n                fields=[\"name\"]\n            )\n\n            repre_names = set(\n                repre_doc[\"name\"]\n                for repre_doc in repre_docs\n            )\n            for repre_doc in self.content_repres.values():\n                if repre_doc[\"name\"] not in repre_names:\n                    validation_state.repre_ok = False\n                    break\n            return\n\n        # [x] [ ] [ ]\n        if selected_asset is not None:\n            asset_doc = get_asset_by_name(\n                project_name, selected_asset, fields=[\"_id\"]\n            )\n            subset_docs = list(get_subsets(\n                project_name,\n                asset_ids=[asset_doc[\"_id\"]],\n                fields=[\"_id\", \"name\"]\n            ))\n\n            subset_name_by_id = {}\n            subset_ids = set()\n            for subset_doc in subset_docs:\n                subset_id = subset_doc[\"_id\"]\n                subset_ids.add(subset_id)\n                subset_name_by_id[subset_id] = subset_doc[\"name\"]\n\n            last_versions_by_subset_id = self.find_last_versions(subset_ids)\n\n            subset_id_by_version_id = {}\n            for subset_id, last_version in last_versions_by_subset_id.items():\n                version_id = last_version[\"_id\"]\n                subset_id_by_version_id[version_id] = subset_id\n\n            repre_docs = get_representations(\n                project_name,\n                version_ids=subset_id_by_version_id.keys(),\n                fields=[\"name\", \"parent\"]\n            )\n            repres_by_subset_name = {}\n            for repre_doc in repre_docs:\n                subset_id = subset_id_by_version_id[repre_doc[\"parent\"]]\n                subset_name = subset_name_by_id[subset_id]\n                if subset_name not in repres_by_subset_name:\n                    repres_by_subset_name[subset_name] = set()\n                repres_by_subset_name[subset_name].add(repre_doc[\"name\"])\n\n            for repre_doc in self.content_repres.values():\n                version_doc = self.content_versions[repre_doc[\"parent\"]]\n                subset_doc = self.content_subsets[version_doc[\"parent\"]]\n                repre_names = (\n                    repres_by_subset_name.get(subset_doc[\"name\"]) or []\n                )\n                if repre_doc[\"name\"] not in repre_names:\n                    validation_state.repre_ok = False\n                    break\n            return\n\n        # [ ] [x] [ ]\n        # Subset documents\n        subset_docs = get_subsets(\n            project_name,\n            asset_ids=self.content_assets.keys(),\n            subset_names=[selected_subset],\n            fields=[\"_id\", \"name\", \"parent\"]\n        )\n        subset_docs_by_id = {}\n        for subset_doc in subset_docs:\n            subset_docs_by_id[subset_doc[\"_id\"]] = subset_doc\n\n        last_versions_by_subset_id = self.find_last_versions(\n            subset_docs_by_id.keys()\n        )\n        subset_id_by_version_id = {}\n        for subset_id, last_version in last_versions_by_subset_id.items():\n            version_id = last_version[\"_id\"]\n            subset_id_by_version_id[version_id] = subset_id\n\n        repre_docs = get_representations(\n            project_name,\n            version_ids=subset_id_by_version_id.keys(),\n            fields=[\"name\", \"parent\"]\n        )\n        repres_by_asset_id = {}\n        for repre_doc in repre_docs:\n            subset_id = subset_id_by_version_id[repre_doc[\"parent\"]]\n            asset_id = subset_docs_by_id[subset_id][\"parent\"]\n            if asset_id not in repres_by_asset_id:\n                repres_by_asset_id[asset_id] = set()\n            repres_by_asset_id[asset_id].add(repre_doc[\"name\"])\n\n        for repre_doc in self.content_repres.values():\n            version_doc = self.content_versions[repre_doc[\"parent\"]]\n            subset_doc = self.content_subsets[version_doc[\"parent\"]]\n            asset_id = subset_doc[\"parent\"]\n            repre_names = (\n                repres_by_asset_id.get(asset_id) or []\n            )\n            if repre_doc[\"name\"] not in repre_names:\n                validation_state.repre_ok = False\n                break\n\n    def _on_current_asset(self):\n        # Set initial asset as current.\n        asset_name = legacy_io.Session[\"AVALON_ASSET\"]\n        index = self._assets_box.findText(\n            asset_name, QtCore.Qt.MatchFixedString\n        )\n        if index >= 0:\n            print(\"Setting asset to {}\".format(asset_name))\n            self._assets_box.setCurrentIndex(index)\n\n    def _on_accept(self):\n        self._trigger_switch()\n\n    def _trigger_switch(self, loader=None):\n        # Use None when not a valid value or when placeholder value\n        selected_asset = self._assets_box.get_valid_value()\n        selected_subset = self._subsets_box.get_valid_value()\n        selected_representation = self._representations_box.get_valid_value()\n\n        project_name = self.active_project()\n        if selected_asset:\n            asset_doc = get_asset_by_name(project_name, selected_asset)\n            asset_docs_by_id = {asset_doc[\"_id\"]: asset_doc}\n        else:\n            asset_docs_by_id = self.content_assets\n\n        asset_docs_by_name = {\n            asset_doc[\"name\"]: asset_doc\n            for asset_doc in asset_docs_by_id.values()\n        }\n\n        subset_names = None\n        if selected_subset:\n            subset_names = [selected_subset]\n\n        subset_docs = list(get_subsets(\n            project_name,\n            subset_names=subset_names,\n            asset_ids=asset_docs_by_id.keys()\n        ))\n        subset_ids = []\n        subset_docs_by_parent_and_name = collections.defaultdict(dict)\n        for subset in subset_docs:\n            subset_ids.append(subset[\"_id\"])\n            parent_id = subset[\"parent\"]\n            name = subset[\"name\"]\n            subset_docs_by_parent_and_name[parent_id][name] = subset\n\n        # versions\n        _version_docs = get_versions(project_name, subset_ids=subset_ids)\n        version_docs = list(reversed(\n            sorted(_version_docs, key=lambda item: item[\"name\"])\n        ))\n\n        hero_version_docs = list(get_hero_versions(\n            project_name, subset_ids=subset_ids\n        ))\n\n        version_ids = list()\n\n        version_docs_by_parent_id_and_name = collections.defaultdict(dict)\n        for version_doc in version_docs:\n            parent_id = version_doc[\"parent\"]\n            version_ids.append(version_doc[\"_id\"])\n            name = version_doc[\"name\"]\n            version_docs_by_parent_id_and_name[parent_id][name] = version_doc\n\n        hero_version_docs_by_parent_id = {}\n        for hero_version_doc in hero_version_docs:\n            version_ids.append(hero_version_doc[\"_id\"])\n            parent_id = hero_version_doc[\"parent\"]\n            hero_version_docs_by_parent_id[parent_id] = hero_version_doc\n\n        repre_docs = get_representations(project_name, version_ids=version_ids)\n        repre_docs_by_parent_id_by_name = collections.defaultdict(dict)\n        for repre_doc in repre_docs:\n            parent_id = repre_doc[\"parent\"]\n            name = repre_doc[\"name\"]\n            repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc\n\n        for container in self._items:\n            container_repre_id = container[\"representation\"]\n            container_repre = self.content_repres[container_repre_id]\n            container_repre_name = container_repre[\"name\"]\n\n            container_version_id = container_repre[\"parent\"]\n            container_version = self.content_versions[container_version_id]\n\n            container_subset_id = container_version[\"parent\"]\n            container_subset = self.content_subsets[container_subset_id]\n            container_subset_name = container_subset[\"name\"]\n\n            container_asset_id = container_subset[\"parent\"]\n            container_asset = self.content_assets[container_asset_id]\n            container_asset_name = container_asset[\"name\"]\n\n            if selected_asset:\n                asset_doc = asset_docs_by_name[selected_asset]\n            else:\n                asset_doc = asset_docs_by_name[container_asset_name]\n\n            subsets_by_name = subset_docs_by_parent_and_name[asset_doc[\"_id\"]]\n            if selected_subset:\n                subset_doc = subsets_by_name[selected_subset]\n            else:\n                subset_doc = subsets_by_name[container_subset_name]\n\n            repre_doc = None\n            subset_id = subset_doc[\"_id\"]\n            if container_version[\"type\"] == \"hero_version\":\n                hero_version = hero_version_docs_by_parent_id.get(\n                    subset_id\n                )\n                if hero_version:\n                    _repres = repre_docs_by_parent_id_by_name.get(\n                        hero_version[\"_id\"]\n                    )\n                    if selected_representation:\n                        repre_doc = _repres.get(selected_representation)\n                    else:\n                        repre_doc = _repres.get(container_repre_name)\n\n            if not repre_doc:\n                version_docs_by_name = version_docs_by_parent_id_and_name[\n                    subset_id\n                ]\n\n                # If asset or subset are selected for switching, we use latest\n                # version else we try to keep the current container version.\n                version_name = None\n                if (\n                    selected_asset in (None, container_asset_name)\n                    and selected_subset in (None, container_subset_name)\n                ):\n                    version_name = container_version.get(\"name\")\n\n                version_doc = None\n                if version_name is not None:\n                    version_doc = version_docs_by_name.get(version_name)\n\n                if version_doc is None:\n                    version_name = max(version_docs_by_name)\n                    version_doc = version_docs_by_name[version_name]\n\n                version_id = version_doc[\"_id\"]\n                repres_docs_by_name = repre_docs_by_parent_id_by_name[\n                    version_id\n                ]\n\n                if selected_representation:\n                    repres_name = selected_representation\n                else:\n                    repres_name = container_repre_name\n\n                repre_doc = repres_docs_by_name[repres_name]\n\n            error = None\n            try:\n                switch_container(container, repre_doc, loader)\n            except (\n                LoaderSwitchNotImplementedError,\n                IncompatibleLoaderError,\n                LoaderNotFoundError,\n            ) as exc:\n                error = str(exc)\n            except Exception:\n                error = (\n                    \"Switch asset failed. \"\n                    \"Search console log for more details.\"\n                )\n            if error is not None:\n                log.warning((\n                    \"Couldn't switch asset.\"\n                    \"See traceback for more information.\"\n                ), exc_info=True)\n                dialog = QtWidgets.QMessageBox(self)\n                dialog.setWindowTitle(\"Switch asset failed\")\n                dialog.setText(error)\n                dialog.exec_()\n\n        self.switched.emit()\n\n        self.close()\n"
  },
  {
    "path": "openpype/tools/sceneinventory/view.py",
    "content": "import collections\nimport logging\nimport itertools\nfrom functools import partial\n\nfrom qtpy import QtWidgets, QtCore\nimport qtawesome\n\nfrom openpype.client import (\n    get_version_by_id,\n    get_versions,\n    get_hero_versions,\n    get_representation_by_id,\n    get_representations,\n)\nfrom openpype import style\nfrom openpype.pipeline import (\n    legacy_io,\n    HeroVersionType,\n    update_container,\n    remove_container,\n    discover_inventory_actions,\n)\nfrom openpype.modules import ModulesManager\nfrom openpype.tools.utils.lib import (\n    iter_model_rows,\n    format_version\n)\n\nfrom .switch_dialog import SwitchAssetDialog\nfrom .model import InventoryModel\n\n\nDEFAULT_COLOR = \"#fb9c15\"\n\nlog = logging.getLogger(\"SceneInventory\")\n\n\nclass SceneInventoryView(QtWidgets.QTreeView):\n    data_changed = QtCore.Signal()\n    hierarchy_view_changed = QtCore.Signal(bool)\n\n    def __init__(self, parent=None):\n        super(SceneInventoryView, self).__init__(parent=parent)\n\n        # view settings\n        self.setIndentation(12)\n        self.setAlternatingRowColors(True)\n        self.setSortingEnabled(True)\n        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)\n        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        self.customContextMenuRequested.connect(self._show_right_mouse_menu)\n        self._hierarchy_view = False\n        self._selected = None\n\n        manager = ModulesManager()\n        sync_server = manager.modules_by_name.get(\"sync_server\")\n        sync_enabled = sync_server is not None and sync_server.enabled\n\n        self.sync_server = sync_server\n        self.sync_enabled = sync_enabled\n\n    def _set_hierarchy_view(self, enabled):\n        if enabled == self._hierarchy_view:\n            return\n        self._hierarchy_view = enabled\n        self.hierarchy_view_changed.emit(enabled)\n\n    def _enter_hierarchy(self, items):\n        self._selected = set(i[\"objectName\"] for i in items)\n        self._set_hierarchy_view(True)\n        self.data_changed.emit()\n        self.expandToDepth(1)\n        self.setStyleSheet(\"\"\"\n        QTreeView {\n             border-color: #fb9c15;\n        }\n        \"\"\")\n\n    def _leave_hierarchy(self):\n        self._set_hierarchy_view(False)\n        self.data_changed.emit()\n        self.setStyleSheet(\"QTreeView {}\")\n\n    def _build_item_menu_for_selection(self, items, menu):\n\n        # Exclude items that are \"NOT FOUND\" since setting versions, updating\n        # and removal won't work for those items.\n        items = [item for item in items if not item.get(\"isNotFound\")]\n\n        if not items:\n            return\n\n        # An item might not have a representation, for example when an item\n        # is listed as \"NOT FOUND\"\n        repre_ids = {\n            item[\"representation\"]\n            for item in items\n        }\n\n        project_name = legacy_io.active_project()\n        repre_docs = get_representations(\n            project_name, representation_ids=repre_ids, fields=[\"parent\"]\n        )\n\n        version_ids = {\n            repre_doc[\"parent\"]\n            for repre_doc in repre_docs\n        }\n\n        loaded_versions = get_versions(\n            project_name, version_ids=version_ids, hero=True\n        )\n\n        loaded_hero_versions = []\n        versions_by_parent_id = collections.defaultdict(list)\n        subset_ids = set()\n        for version in loaded_versions:\n            if version[\"type\"] == \"hero_version\":\n                loaded_hero_versions.append(version)\n            else:\n                parent_id = version[\"parent\"]\n                versions_by_parent_id[parent_id].append(version)\n                subset_ids.add(parent_id)\n\n        all_versions = get_versions(\n            project_name, subset_ids=subset_ids, hero=True\n        )\n        hero_versions = []\n        versions = []\n        for version in all_versions:\n            if version[\"type\"] == \"hero_version\":\n                hero_versions.append(version)\n            else:\n                versions.append(version)\n\n        has_loaded_hero_versions = len(loaded_hero_versions) > 0\n        has_available_hero_version = len(hero_versions) > 0\n        has_outdated = False\n\n        for version in versions:\n            parent_id = version[\"parent\"]\n            current_versions = versions_by_parent_id[parent_id]\n            for current_version in current_versions:\n                if current_version[\"name\"] < version[\"name\"]:\n                    has_outdated = True\n                    break\n\n            if has_outdated:\n                break\n\n        switch_to_versioned = None\n        if has_loaded_hero_versions:\n            def _on_switch_to_versioned(items):\n                repre_ids = {\n                    item[\"representation\"]\n                    for item in items\n                }\n\n                repre_docs = get_representations(\n                    project_name,\n                    representation_ids=repre_ids,\n                    fields=[\"parent\"]\n                )\n\n                version_ids = set()\n                version_id_by_repre_id = {}\n                for repre_doc in repre_docs:\n                    version_id = repre_doc[\"parent\"]\n                    repre_id = str(repre_doc[\"_id\"])\n                    version_id_by_repre_id[repre_id] = version_id\n                    version_ids.add(version_id)\n\n                hero_versions = get_hero_versions(\n                    project_name,\n                    version_ids=version_ids,\n                    fields=[\"version_id\"]\n                )\n\n                hero_src_version_ids = set()\n                for hero_version in hero_versions:\n                    version_id = hero_version[\"version_id\"]\n                    hero_src_version_ids.add(version_id)\n                    hero_version_id = hero_version[\"_id\"]\n                    for _repre_id, current_version_id in (\n                        version_id_by_repre_id.items()\n                    ):\n                        if current_version_id == hero_version_id:\n                            version_id_by_repre_id[_repre_id] = version_id\n\n                version_docs = get_versions(\n                    project_name,\n                    version_ids=hero_src_version_ids,\n                    fields=[\"name\"]\n                )\n                version_name_by_id = {}\n                for version_doc in version_docs:\n                    version_name_by_id[version_doc[\"_id\"]] = \\\n                        version_doc[\"name\"]\n\n                # Specify version per item to update to\n                update_items = []\n                update_versions = []\n                for item in items:\n                    repre_id = item[\"representation\"]\n                    version_id = version_id_by_repre_id.get(repre_id)\n                    version_name = version_name_by_id.get(version_id)\n                    if version_name is not None:\n                        update_items.append(item)\n                        update_versions.append(version_name)\n                self._update_containers(update_items, update_versions)\n\n            update_icon = qtawesome.icon(\n                \"fa.asterisk\",\n                color=DEFAULT_COLOR\n            )\n            switch_to_versioned = QtWidgets.QAction(\n                update_icon,\n                \"Switch to versioned\",\n                menu\n            )\n            switch_to_versioned.triggered.connect(\n                lambda: _on_switch_to_versioned(items)\n            )\n\n        update_to_latest_action = None\n        if has_outdated or has_loaded_hero_versions:\n            update_icon = qtawesome.icon(\n                \"fa.angle-double-up\",\n                color=DEFAULT_COLOR\n            )\n            update_to_latest_action = QtWidgets.QAction(\n                update_icon,\n                \"Update to latest\",\n                menu\n            )\n            update_to_latest_action.triggered.connect(\n                lambda: self._update_containers(items, version=-1)\n            )\n\n        change_to_hero = None\n        if has_available_hero_version:\n            # TODO change icon\n            change_icon = qtawesome.icon(\n                \"fa.asterisk\",\n                color=\"#00b359\"\n            )\n            change_to_hero = QtWidgets.QAction(\n                change_icon,\n                \"Change to hero\",\n                menu\n            )\n            change_to_hero.triggered.connect(\n                lambda: self._update_containers(items,\n                                                version=HeroVersionType(-1))\n            )\n\n        # set version\n        set_version_icon = qtawesome.icon(\"fa.hashtag\", color=DEFAULT_COLOR)\n        set_version_action = QtWidgets.QAction(\n            set_version_icon,\n            \"Set version\",\n            menu\n        )\n        set_version_action.triggered.connect(\n            lambda: self._show_version_dialog(items))\n\n        # switch asset\n        switch_asset_icon = qtawesome.icon(\"fa.sitemap\", color=DEFAULT_COLOR)\n        switch_asset_action = QtWidgets.QAction(\n            switch_asset_icon,\n            \"Switch Asset\",\n            menu\n        )\n        switch_asset_action.triggered.connect(\n            lambda: self._show_switch_dialog(items))\n\n        # remove\n        remove_icon = qtawesome.icon(\"fa.remove\", color=DEFAULT_COLOR)\n        remove_action = QtWidgets.QAction(remove_icon, \"Remove items\", menu)\n        remove_action.triggered.connect(\n            lambda: self._show_remove_warning_dialog(items))\n\n        # add the actions\n        if switch_to_versioned:\n            menu.addAction(switch_to_versioned)\n\n        if update_to_latest_action:\n            menu.addAction(update_to_latest_action)\n\n        if change_to_hero:\n            menu.addAction(change_to_hero)\n\n        menu.addAction(set_version_action)\n        menu.addAction(switch_asset_action)\n\n        menu.addSeparator()\n\n        menu.addAction(remove_action)\n\n        self._handle_sync_server(menu, repre_ids)\n\n    def _handle_sync_server(self, menu, repre_ids):\n        \"\"\"\n            Adds actions for download/upload when SyncServer is enabled\n\n            Args:\n                menu (OptionMenu)\n                repre_ids (list) of object_ids\n            Returns:\n                (OptionMenu)\n        \"\"\"\n        if not self.sync_enabled:\n            return\n\n        menu.addSeparator()\n\n        download_icon = qtawesome.icon(\"fa.download\", color=DEFAULT_COLOR)\n        download_active_action = QtWidgets.QAction(\n            download_icon,\n            \"Download\",\n            menu\n        )\n        download_active_action.triggered.connect(\n            lambda: self._add_sites(repre_ids, 'active_site'))\n\n        upload_icon = qtawesome.icon(\"fa.upload\", color=DEFAULT_COLOR)\n        upload_remote_action = QtWidgets.QAction(\n            upload_icon,\n            \"Upload\",\n            menu\n        )\n        upload_remote_action.triggered.connect(\n            lambda: self._add_sites(repre_ids, 'remote_site'))\n\n        menu.addAction(download_active_action)\n        menu.addAction(upload_remote_action)\n\n    def _add_sites(self, repre_ids, side):\n        \"\"\"\n            (Re)sync all 'repre_ids' to specific site.\n\n            It checks if opposite site has fully available content to limit\n            accidents. (ReSync active when no remote >> losing active content)\n\n            Args:\n                repre_ids (list)\n                side (str): 'active_site'|'remote_site'\n        \"\"\"\n        project_name = legacy_io.Session[\"AVALON_PROJECT\"]\n        active_site = self.sync_server.get_active_site(project_name)\n        remote_site = self.sync_server.get_remote_site(project_name)\n\n        repre_docs = get_representations(\n            project_name, representation_ids=repre_ids\n        )\n        repre_docs_by_id = {\n            repre_doc[\"_id\"]: repre_doc\n            for repre_doc in repre_docs\n        }\n        for repre_id in repre_ids:\n            repre_doc = repre_docs_by_id.get(repre_id)\n            if not repre_doc:\n                continue\n\n            progress = self.sync_server.get_progress_for_repre(\n                repre_doc,\n                active_site,\n                remote_site\n            )\n            if side == \"active_site\":\n                # check opposite from added site, must be 1 or unable to sync\n                check_progress = progress[remote_site]\n                site = active_site\n            else:\n                check_progress = progress[active_site]\n                site = remote_site\n\n            if check_progress == 1:\n                self.sync_server.add_site(\n                    project_name, repre_id, site, force=True\n                )\n\n        self.data_changed.emit()\n\n    def _build_item_menu(self, items=None):\n        \"\"\"Create menu for the selected items\"\"\"\n\n        if not items:\n            items = []\n\n        menu = QtWidgets.QMenu(self)\n\n        # add the actions\n        self._build_item_menu_for_selection(items, menu)\n\n        # These two actions should be able to work without selection\n        # expand all items\n        expandall_action = QtWidgets.QAction(menu, text=\"Expand all items\")\n        expandall_action.triggered.connect(self.expandAll)\n\n        # collapse all items\n        collapse_action = QtWidgets.QAction(menu, text=\"Collapse all items\")\n        collapse_action.triggered.connect(self.collapseAll)\n\n        menu.addAction(expandall_action)\n        menu.addAction(collapse_action)\n\n        custom_actions = self._get_custom_actions(containers=items)\n        if custom_actions:\n            submenu = QtWidgets.QMenu(\"Actions\", self)\n            for action in custom_actions:\n                color = action.color or DEFAULT_COLOR\n                icon = qtawesome.icon(\"fa.%s\" % action.icon, color=color)\n                action_item = QtWidgets.QAction(icon, action.label, submenu)\n                action_item.triggered.connect(\n                    partial(self._process_custom_action, action, items))\n\n                submenu.addAction(action_item)\n\n            menu.addMenu(submenu)\n\n        # go back to flat view\n        if self._hierarchy_view:\n            back_to_flat_icon = qtawesome.icon(\"fa.list\", color=DEFAULT_COLOR)\n            back_to_flat_action = QtWidgets.QAction(\n                back_to_flat_icon,\n                \"Back to Full-View\",\n                menu\n            )\n            back_to_flat_action.triggered.connect(self._leave_hierarchy)\n\n        # send items to hierarchy view\n        enter_hierarchy_icon = qtawesome.icon(\"fa.indent\", color=\"#d8d8d8\")\n        enter_hierarchy_action = QtWidgets.QAction(\n            enter_hierarchy_icon,\n            \"Cherry-Pick (Hierarchy)\",\n            menu\n        )\n        enter_hierarchy_action.triggered.connect(\n            lambda: self._enter_hierarchy(items))\n\n        if items:\n            menu.addAction(enter_hierarchy_action)\n\n        if self._hierarchy_view:\n            menu.addAction(back_to_flat_action)\n\n        return menu\n\n    def _get_custom_actions(self, containers):\n        \"\"\"Get the registered Inventory Actions\n\n        Args:\n            containers(list): collection of containers\n\n        Returns:\n            list: collection of filter and initialized actions\n        \"\"\"\n\n        def sorter(Plugin):\n            \"\"\"Sort based on order attribute of the plugin\"\"\"\n            return Plugin.order\n\n        # Fedd an empty dict if no selection, this will ensure the compat\n        # lookup always work, so plugin can interact with Scene Inventory\n        # reversely.\n        containers = containers or [dict()]\n\n        # Check which action will be available in the menu\n        Plugins = discover_inventory_actions()\n        compatible = [p() for p in Plugins if\n                      any(p.is_compatible(c) for c in containers)]\n\n        return sorted(compatible, key=sorter)\n\n    def _process_custom_action(self, action, containers):\n        \"\"\"Run action and if results are returned positive update the view\n\n        If the result is list or dict, will select view items by the result.\n\n        Args:\n            action (InventoryAction): Inventory Action instance\n            containers (list): Data of currently selected items\n\n        Returns:\n            None\n        \"\"\"\n\n        result = action.process(containers)\n        if result:\n            self.data_changed.emit()\n\n            if isinstance(result, (list, set)):\n                self._select_items_by_action(result)\n\n            if isinstance(result, dict):\n                self._select_items_by_action(\n                    result[\"objectNames\"], result[\"options\"]\n                )\n\n    def _select_items_by_action(self, object_names, options=None):\n        \"\"\"Select view items by the result of action\n\n        Args:\n            object_names (list or set): A list/set of container object name\n            options (dict): GUI operation options.\n\n        Returns:\n            None\n\n        \"\"\"\n        options = options or dict()\n\n        if options.get(\"clear\", True):\n            self.clearSelection()\n\n        object_names = set(object_names)\n        if (\n            self._hierarchy_view\n            and not self._selected.issuperset(object_names)\n        ):\n            # If any container not in current cherry-picked view, update\n            # view before selecting them.\n            self._selected.update(object_names)\n            self.data_changed.emit()\n\n        model = self.model()\n        selection_model = self.selectionModel()\n\n        select_mode = {\n            \"select\": QtCore.QItemSelectionModel.Select,\n            \"deselect\": QtCore.QItemSelectionModel.Deselect,\n            \"toggle\": QtCore.QItemSelectionModel.Toggle,\n        }[options.get(\"mode\", \"select\")]\n\n        for index in iter_model_rows(model, 0):\n            item = index.data(InventoryModel.ItemRole)\n            if item.get(\"isGroupNode\"):\n                continue\n\n            name = item.get(\"objectName\")\n            if name in object_names:\n                self.scrollTo(index)  # Ensure item is visible\n                flags = select_mode | QtCore.QItemSelectionModel.Rows\n                selection_model.select(index, flags)\n\n                object_names.remove(name)\n\n            if len(object_names) == 0:\n                break\n\n    def _show_right_mouse_menu(self, pos):\n        \"\"\"Display the menu when at the position of the item clicked\"\"\"\n\n        globalpos = self.viewport().mapToGlobal(pos)\n\n        if not self.selectionModel().hasSelection():\n            print(\"No selection\")\n            # Build menu without selection, feed an empty list\n            menu = self._build_item_menu()\n            menu.exec_(globalpos)\n            return\n\n        active = self.currentIndex()  # index under mouse\n        active = active.sibling(active.row(), 0)  # get first column\n\n        # move index under mouse\n        indices = self.get_indices()\n        if active in indices:\n            indices.remove(active)\n\n        indices.append(active)\n\n        # Extend to the sub-items\n        all_indices = self._extend_to_children(indices)\n        items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices\n                 if i.parent().isValid()]\n\n        if self._hierarchy_view:\n            # Ensure no group item\n            items = [n for n in items if not n.get(\"isGroupNode\")]\n\n        menu = self._build_item_menu(items)\n        menu.exec_(globalpos)\n\n    def get_indices(self):\n        \"\"\"Get the selected rows\"\"\"\n        selection_model = self.selectionModel()\n        return selection_model.selectedRows()\n\n    def _extend_to_children(self, indices):\n        \"\"\"Extend the indices to the children indices.\n\n        Top-level indices are extended to its children indices. Sub-items\n        are kept as is.\n\n        Args:\n            indices (list): The indices to extend.\n\n        Returns:\n            list: The children indices\n\n        \"\"\"\n        def get_children(i):\n            model = i.model()\n            rows = model.rowCount(parent=i)\n            for row in range(rows):\n                child = model.index(row, 0, parent=i)\n                yield child\n\n        subitems = set()\n        for i in indices:\n            valid_parent = i.parent().isValid()\n            if valid_parent and i not in subitems:\n                subitems.add(i)\n\n                if self._hierarchy_view:\n                    # Assume this is a group item\n                    for child in get_children(i):\n                        subitems.add(child)\n            else:\n                # is top level item\n                for child in get_children(i):\n                    subitems.add(child)\n\n        return list(subitems)\n\n    def _show_version_dialog(self, items):\n        \"\"\"Create a dialog with the available versions for the selected file\n\n        Args:\n            items (list): list of items to run the \"set_version\" for\n\n        Returns:\n            None\n        \"\"\"\n\n        active = items[-1]\n\n        project_name = legacy_io.active_project()\n        # Get available versions for active representation\n        repre_doc = get_representation_by_id(\n            project_name,\n            active[\"representation\"],\n            fields=[\"parent\"]\n        )\n\n        repre_version_doc = get_version_by_id(\n            project_name,\n            repre_doc[\"parent\"],\n            fields=[\"parent\"]\n        )\n\n        version_docs = list(get_versions(\n            project_name,\n            subset_ids=[repre_version_doc[\"parent\"]],\n            hero=True\n        ))\n        hero_version = None\n        standard_versions = []\n        for version_doc in version_docs:\n            if version_doc[\"type\"] == \"hero_version\":\n                hero_version = version_doc\n            else:\n                standard_versions.append(version_doc)\n        versions = list(reversed(\n            sorted(standard_versions, key=lambda item: item[\"name\"])\n        ))\n        if hero_version:\n            _version_id = hero_version[\"version_id\"]\n            for _version in versions:\n                if _version[\"_id\"] != _version_id:\n                    continue\n\n                hero_version[\"name\"] = HeroVersionType(\n                    _version[\"name\"]\n                )\n                hero_version[\"data\"] = _version[\"data\"]\n                break\n\n        # Get index among the listed versions\n        current_item = None\n        current_version = active[\"version\"]\n        if isinstance(current_version, HeroVersionType):\n            current_item = hero_version\n        else:\n            for version in versions:\n                if version[\"name\"] == current_version:\n                    current_item = version\n                    break\n\n        all_versions = []\n        if hero_version:\n            all_versions.append(hero_version)\n        all_versions.extend(versions)\n\n        if current_item:\n            index = all_versions.index(current_item)\n        else:\n            index = 0\n\n        versions_by_label = dict()\n        labels = []\n        for version in all_versions:\n            is_hero = version[\"type\"] == \"hero_version\"\n            label = format_version(version[\"name\"], is_hero)\n            labels.append(label)\n            versions_by_label[label] = version[\"name\"]\n\n        label, state = QtWidgets.QInputDialog.getItem(\n            self,\n            \"Set version..\",\n            \"Set version number to\",\n            labels,\n            current=index,\n            editable=False\n        )\n        if not state:\n            return\n\n        if label:\n            version = versions_by_label[label]\n            self._update_containers(items, version)\n\n    def _show_switch_dialog(self, items):\n        \"\"\"Display Switch dialog\"\"\"\n        dialog = SwitchAssetDialog(self, items)\n        dialog.switched.connect(self.data_changed.emit)\n        dialog.show()\n\n    def _show_remove_warning_dialog(self, items):\n        \"\"\"Prompt a dialog to inform the user the action will remove items\"\"\"\n\n        accept = QtWidgets.QMessageBox.Ok\n        buttons = accept | QtWidgets.QMessageBox.Cancel\n\n        state = QtWidgets.QMessageBox.question(\n            self,\n            \"Are you sure?\",\n            \"Are you sure you want to remove {} item(s)\".format(len(items)),\n            buttons=buttons,\n            defaultButton=accept\n        )\n\n        if state != accept:\n            return\n\n        for item in items:\n            remove_container(item)\n        self.data_changed.emit()\n\n    def _show_version_error_dialog(self, version, items):\n        \"\"\"Shows QMessageBox when version switch doesn't work\n\n            Args:\n                version: str or int or None\n        \"\"\"\n        if version == -1:\n            version_str = \"latest\"\n        elif isinstance(version, HeroVersionType):\n            version_str = \"hero\"\n        elif isinstance(version, int):\n            version_str = \"v{:03d}\".format(version)\n        else:\n            version_str = version\n\n        dialog = QtWidgets.QMessageBox(self)\n        dialog.setIcon(QtWidgets.QMessageBox.Warning)\n        dialog.setStyleSheet(style.load_stylesheet())\n        dialog.setWindowTitle(\"Update failed\")\n\n        switch_btn = dialog.addButton(\n            \"Switch Asset\",\n            QtWidgets.QMessageBox.ActionRole\n        )\n        switch_btn.clicked.connect(lambda: self._show_switch_dialog(items))\n\n        dialog.addButton(QtWidgets.QMessageBox.Cancel)\n\n        msg = (\n            \"Version update to '{}' failed as representation doesn't exist.\"\n            \"\\n\\nPlease update to version with a valid representation\"\n            \" OR \\n use 'Switch Asset' button to change asset.\"\n        ).format(version_str)\n        dialog.setText(msg)\n        dialog.exec_()\n\n    def update_all(self):\n        \"\"\"Update all items that are currently 'outdated' in the view\"\"\"\n        # Get the source model through the proxy model\n        model = self.model().sourceModel()\n\n        # Get all items from outdated groups\n        outdated_items = []\n        for index in iter_model_rows(model,\n                                     column=0,\n                                     include_root=False):\n            item = index.data(model.ItemRole)\n\n            if not item.get(\"isGroupNode\"):\n                continue\n\n            # Only the group nodes contain the \"highest_version\" data and as\n            # such we find only the groups and take its children.\n            if not model.outdated(item):\n                continue\n\n            # Collect all children which we want to update\n            children = item.children()\n            outdated_items.extend(children)\n\n        if not outdated_items:\n            log.info(\"Nothing to update.\")\n            return\n\n        # Trigger update to latest\n        self._update_containers(outdated_items, version=-1)\n\n    def _update_containers(self, items, version):\n        \"\"\"Helper to update items to given version (or version per item)\n\n        If at least one item is specified this will always try to refresh\n        the inventory even if errors occurred on any of the items.\n\n        Arguments:\n            items (list): Items to update\n            version (int or list): Version to set to.\n                This can be a list specifying a version for each item.\n                Like `update_container` version -1 sets the latest version\n                and HeroTypeVersion instances set the hero version.\n\n        \"\"\"\n\n        if isinstance(version, (list, tuple)):\n            # We allow a unique version to be specified per item. In that case\n            # the length must match with the items\n            assert len(items) == len(version), (\n                \"Number of items mismatches number of versions: \"\n                \"{} items - {} versions\".format(len(items), len(version))\n            )\n            versions = version\n        else:\n            # Repeat the same version infinitely\n            versions = itertools.repeat(version)\n\n        # Trigger update to latest\n        try:\n            for item, item_version in zip(items, versions):\n                try:\n                    update_container(item, item_version)\n                except AssertionError:\n                    self._show_version_error_dialog(item_version, [item])\n                    log.warning(\"Update failed\", exc_info=True)\n        finally:\n            # Always update the scene inventory view, even if errors occurred\n            self.data_changed.emit()\n"
  },
  {
    "path": "openpype/tools/sceneinventory/widgets.py",
    "content": "from qtpy import QtWidgets, QtCore\nfrom openpype import style\n\n\nclass ButtonWithMenu(QtWidgets.QToolButton):\n    def __init__(self, parent=None):\n        super(ButtonWithMenu, self).__init__(parent)\n\n        self.setObjectName(\"ButtonWithMenu\")\n\n        self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)\n        menu = QtWidgets.QMenu(self)\n\n        self.setMenu(menu)\n\n        self._menu = menu\n        self._actions = []\n\n    def menu(self):\n        return self._menu\n\n    def clear_actions(self):\n        if self._menu is not None:\n            self._menu.clear()\n        self._actions = []\n\n    def add_action(self, action):\n        self._actions.append(action)\n        self._menu.addAction(action)\n\n    def _on_action_trigger(self):\n        action = self.sender()\n        if action not in self._actions:\n            return\n        action.trigger()\n\n\nclass SearchComboBox(QtWidgets.QComboBox):\n    \"\"\"Searchable ComboBox with empty placeholder value as first value\"\"\"\n\n    def __init__(self, parent):\n        super(SearchComboBox, self).__init__(parent)\n\n        self.setEditable(True)\n        self.setInsertPolicy(QtWidgets.QComboBox.NoInsert)\n\n        combobox_delegate = QtWidgets.QStyledItemDelegate(self)\n        self.setItemDelegate(combobox_delegate)\n\n        completer = self.completer()\n        completer.setCompletionMode(\n            QtWidgets.QCompleter.PopupCompletion\n        )\n        completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        completer_view = completer.popup()\n        completer_view.setObjectName(\"CompleterView\")\n        completer_delegate = QtWidgets.QStyledItemDelegate(completer_view)\n        completer_view.setItemDelegate(completer_delegate)\n        completer_view.setStyleSheet(style.load_stylesheet())\n\n        self._combobox_delegate = combobox_delegate\n\n        self._completer_delegate = completer_delegate\n        self._completer = completer\n\n    def set_placeholder(self, placeholder):\n        self.lineEdit().setPlaceholderText(placeholder)\n\n    def populate(self, items):\n        self.clear()\n        self.addItems([\"\"])     # ensure first item is placeholder\n        self.addItems(items)\n\n    def get_valid_value(self):\n        \"\"\"Return the current text if it's a valid value else None\n\n        Note: The empty placeholder value is valid and returns as \"\"\n\n        \"\"\"\n\n        text = self.currentText()\n        lookup = set(self.itemText(i) for i in range(self.count()))\n        if text not in lookup:\n            return None\n\n        return text or None\n\n    def set_valid_value(self, value):\n        \"\"\"Try to locate 'value' and pre-select it in dropdown.\"\"\"\n        index = self.findText(value)\n        if index > -1:\n            self.setCurrentIndex(index)\n"
  },
  {
    "path": "openpype/tools/sceneinventory/window.py",
    "content": "import os\nimport sys\n\nfrom qtpy import QtWidgets, QtCore\nimport qtawesome\n\nfrom openpype import style\nfrom openpype.client import get_projects\nfrom openpype.pipeline import legacy_io\nfrom openpype.tools.utils.delegates import VersionDelegate\nfrom openpype.tools.utils.lib import (\n    qt_app_context,\n    preserve_expanded_rows,\n    preserve_selection,\n    FamilyConfigCache\n)\n\nfrom .model import (\n    InventoryModel,\n    FilterProxyModel\n)\nfrom .view import SceneInventoryView\n\n\nmodule = sys.modules[__name__]\nmodule.window = None\n\n\nclass SceneInventoryWindow(QtWidgets.QDialog):\n    \"\"\"Scene Inventory window\"\"\"\n\n    def __init__(self, parent=None):\n        super(SceneInventoryWindow, self).__init__(parent)\n\n        if not parent:\n            self.setWindowFlags(\n                self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint\n            )\n\n        project_name = os.getenv(\"AVALON_PROJECT\") or \"<Project not set>\"\n        self.setWindowTitle(\"Scene Inventory 1.0 - {}\".format(project_name))\n        self.setObjectName(\"SceneInventory\")\n\n        self.resize(1100, 480)\n\n        # region control\n        filter_label = QtWidgets.QLabel(\"Search\", self)\n        text_filter = QtWidgets.QLineEdit(self)\n\n        outdated_only_checkbox = QtWidgets.QCheckBox(\n            \"Filter to outdated\", self\n        )\n        outdated_only_checkbox.setToolTip(\"Show outdated files only\")\n        outdated_only_checkbox.setChecked(False)\n\n        icon = qtawesome.icon(\"fa.arrow-up\", color=\"white\")\n        update_all_button = QtWidgets.QPushButton(self)\n        update_all_button.setToolTip(\"Update all outdated to latest version\")\n        update_all_button.setIcon(icon)\n\n        icon = qtawesome.icon(\"fa.refresh\", color=\"white\")\n        refresh_button = QtWidgets.QPushButton(self)\n        refresh_button.setToolTip(\"Refresh\")\n        refresh_button.setIcon(icon)\n\n        control_layout = QtWidgets.QHBoxLayout()\n        control_layout.addWidget(filter_label)\n        control_layout.addWidget(text_filter)\n        control_layout.addWidget(outdated_only_checkbox)\n        control_layout.addWidget(update_all_button)\n        control_layout.addWidget(refresh_button)\n\n        # endregion control\n        family_config_cache = FamilyConfigCache(legacy_io)\n\n        model = InventoryModel(family_config_cache)\n        proxy = FilterProxyModel()\n        proxy.setSourceModel(model)\n        proxy.setDynamicSortFilter(True)\n        proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        view = SceneInventoryView(self)\n        view.setModel(proxy)\n\n        # set some nice default widths for the view\n        view.setColumnWidth(0, 250)  # name\n        view.setColumnWidth(1, 55)   # version\n        view.setColumnWidth(2, 55)   # count\n        view.setColumnWidth(3, 150)  # family\n        view.setColumnWidth(4, 120)  # group\n        view.setColumnWidth(5, 150)  # loader\n\n        # apply delegates\n        version_delegate = VersionDelegate(legacy_io, self)\n        column = model.Columns.index(\"version\")\n        view.setItemDelegateForColumn(column, version_delegate)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addLayout(control_layout)\n        layout.addWidget(view)\n\n        # signals\n        text_filter.textChanged.connect(self._on_text_filter_change)\n        outdated_only_checkbox.stateChanged.connect(\n            self._on_outdated_state_change\n        )\n        view.hierarchy_view_changed.connect(\n            self._on_hierarchy_view_change\n        )\n        view.data_changed.connect(self._on_refresh_request)\n        refresh_button.clicked.connect(self._on_refresh_request)\n        update_all_button.clicked.connect(self._on_update_all)\n\n        self._update_all_button = update_all_button\n        self._outdated_only_checkbox = outdated_only_checkbox\n        self._view = view\n        self._model = model\n        self._proxy = proxy\n        self._version_delegate = version_delegate\n        self._family_config_cache = family_config_cache\n\n        self._first_show = True\n\n        family_config_cache.refresh()\n\n    def showEvent(self, event):\n        super(SceneInventoryWindow, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self.setStyleSheet(style.load_stylesheet())\n\n    def keyPressEvent(self, event):\n        \"\"\"Custom keyPressEvent.\n\n        Override keyPressEvent to do nothing so that Maya's panels won't\n        take focus when pressing \"SHIFT\" whilst mouse is over viewport or\n        outliner. This way users don't accidentally perform Maya commands\n        whilst trying to name an instance.\n\n        \"\"\"\n\n    def _on_refresh_request(self):\n        \"\"\"Signal callback to trigger 'refresh' without any arguments.\"\"\"\n\n        self.refresh()\n\n    def refresh(self, items=None):\n        with preserve_expanded_rows(\n            tree_view=self._view,\n            role=self._model.UniqueRole\n        ):\n            with preserve_selection(\n                tree_view=self._view,\n                role=self._model.UniqueRole,\n                current_index=False\n            ):\n                kwargs = {\"items\": items}\n                # TODO do not touch view's inner attribute\n                if self._view._hierarchy_view:\n                    kwargs[\"selected\"] = self._view._selected\n                self._model.refresh(**kwargs)\n\n    def _on_hierarchy_view_change(self, enabled):\n        self._proxy.set_hierarchy_view(enabled)\n        self._model.set_hierarchy_view(enabled)\n\n    def _on_text_filter_change(self, text_filter):\n        if hasattr(self._proxy, \"setFilterRegExp\"):\n            self._proxy.setFilterRegExp(text_filter)\n        else:\n            self._proxy.setFilterRegularExpression(text_filter)\n\n    def _on_outdated_state_change(self):\n        self._proxy.set_filter_outdated(\n            self._outdated_only_checkbox.isChecked()\n        )\n\n    def _on_update_all(self):\n        self._view.update_all()\n\n\ndef show(root=None, debug=False, parent=None, items=None):\n    \"\"\"Display Scene Inventory GUI\n\n    Arguments:\n        debug (bool, optional): Run in debug-mode,\n            defaults to False\n        parent (QtCore.QObject, optional): When provided parent the interface\n            to this QObject.\n        items (list) of dictionaries - for injection of items for standalone\n                testing\n\n    \"\"\"\n\n    try:\n        module.window.close()\n        del module.window\n    except (RuntimeError, AttributeError):\n        pass\n\n    if debug is True:\n        legacy_io.install()\n\n        if not os.environ.get(\"AVALON_PROJECT\"):\n            any_project = next(\n                project for project in get_projects()\n            )\n\n            project_name = any_project[\"name\"]\n        else:\n            project_name = os.environ.get(\"AVALON_PROJECT\")\n        legacy_io.Session[\"AVALON_PROJECT\"] = project_name\n\n    with qt_app_context():\n        window = SceneInventoryWindow(parent)\n        window.show()\n        window.refresh(items=items)\n\n        module.window = window\n\n        # Pull window to the front.\n        module.window.raise_()\n        module.window.activateWindow()\n"
  },
  {
    "path": "openpype/tools/settings/__init__.py",
    "content": "import sys\nfrom qtpy import QtGui\n\nfrom openpype import style\nfrom openpype.tools.utils import get_openpype_qt_app\nfrom .lib import (\n    BTN_FIXED_SIZE,\n    CHILD_OFFSET\n)\nfrom .local_settings import LocalSettingsWindow\nfrom .settings import (\n    MainWidget,\n    ProjectListWidget\n)\n\n\ndef main(user_role=None):\n    if user_role is None:\n        user_role = \"manager\"\n\n    user_role_low = user_role.lower()\n    allowed_roles = (\"developer\", \"manager\")\n    if user_role_low not in allowed_roles:\n        raise ValueError(\"Invalid user role \\\"{}\\\". Expected {}\".format(\n            user_role, \", \".join(allowed_roles)\n        ))\n\n    app = get_openpype_qt_app()\n    app.setWindowIcon(QtGui.QIcon(style.app_icon_path()))\n\n    widget = MainWidget(user_role)\n    widget.show()\n\n    sys.exit(app.exec_())\n\n\n__all__ = (\n    \"BTN_FIXED_SIZE\",\n    \"CHILD_OFFSET\",\n\n    \"MainWidget\",\n    \"ProjectListWidget\",\n    \"LocalSettingsWindow\",\n    \"main\"\n)\n"
  },
  {
    "path": "openpype/tools/settings/__main__.py",
    "content": "try:\n    from . import main\nexcept ImportError:\n    from settings import main\n\n\nmain()\n"
  },
  {
    "path": "openpype/tools/settings/lib.py",
    "content": "CHILD_OFFSET = 15\nBTN_FIXED_SIZE = 20\n"
  },
  {
    "path": "openpype/tools/settings/local_settings/__init__.py",
    "content": "from .window import LocalSettingsWindow\n\n\n__all__ = (\n    \"LocalSettingsWindow\",\n)\n"
  },
  {
    "path": "openpype/tools/settings/local_settings/apps_widget.py",
    "content": "import platform\nfrom qtpy import QtWidgets\nfrom .widgets import (\n    Separator,\n    ExpandingWidget\n)\nfrom openpype.tools.settings import CHILD_OFFSET\nfrom openpype.tools.utils import PlaceholderLineEdit\n\n\nclass AppVariantWidget(QtWidgets.QWidget):\n    exec_placeholder = \"< Specific path for this machine >\"\n\n    def __init__(\n        self, group_label, variant_name, variant_label, variant_entity, parent\n    ):\n        super(AppVariantWidget, self).__init__(parent)\n\n        self.executable_input_widget = None\n\n        if not variant_label:\n            variant_label = variant_name\n\n        label = \" \".join([group_label, variant_label])\n\n        expading_widget = ExpandingWidget(label, self)\n        content_widget = QtWidgets.QWidget(expading_widget)\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0)\n\n        expading_widget.set_content_widget(content_widget)\n\n        # Add expanding widget to main layout\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(expading_widget)\n\n        # TODO For celaction - not sure what is \"Celaction publish\" for\n        if not variant_entity[\"executables\"].multiplatform:\n            warn_label = QtWidgets.QLabel(\n                \"Application without multiplatform paths\"\n            )\n            content_layout.addWidget(warn_label)\n            return\n\n        executable_input_widget = PlaceholderLineEdit(content_widget)\n        executable_input_widget.setPlaceholderText(self.exec_placeholder)\n        content_layout.addWidget(executable_input_widget)\n\n        self.executable_input_widget = executable_input_widget\n\n        studio_executables = (\n            variant_entity[\"executables\"][platform.system().lower()]\n        )\n        if len(studio_executables) < 1:\n            return\n\n        content_layout.addWidget(Separator(parent=self))\n        content_layout.addWidget(\n            QtWidgets.QLabel(\"Studio paths:\", self)\n        )\n\n        for item in studio_executables:\n            path_widget = QtWidgets.QLineEdit(content_widget)\n            path_widget.setObjectName(\"LikeDisabledInput\")\n            path_widget.setText(item.value)\n            path_widget.setReadOnly(True)\n            content_layout.addWidget(path_widget)\n\n    def update_local_settings(self, value):\n        if not self.executable_input_widget:\n            return\n\n        if not value:\n            value = {}\n        elif not isinstance(value, dict):\n            print(\"Got invalid value type {}. Expected {}\".format(\n                type(value), dict\n            ))\n            value = {}\n\n        executable_path = value.get(\"executable\")\n        if not executable_path:\n            executable_path = \"\"\n        elif isinstance(executable_path, list):\n            print(\"Got list in executable path so using first item as value\")\n            executable_path = executable_path[0]\n\n        if not isinstance(executable_path, str):\n            executable_path = \"\"\n            print((\n                \"Got invalid value type of app executable {}. Expected {}\"\n            ).format(type(value), str))\n\n        self.executable_input_widget.setText(executable_path)\n\n    def settings_value(self):\n        if not self.executable_input_widget:\n            return None\n        value = self.executable_input_widget.text()\n        if not value:\n            return None\n        return {\"executable\": value}\n\n\nclass AppGroupWidget(QtWidgets.QWidget):\n    def __init__(self, group_entity, group_label, parent, dynamic=False):\n        super(AppGroupWidget, self).__init__(parent)\n\n        variants_entity = group_entity[\"variants\"]\n        valid_variants = {}\n        for key, entity in variants_entity.items():\n            if \"enabled\" not in entity or entity[\"enabled\"].value:\n                valid_variants[key] = entity\n\n        expading_widget = ExpandingWidget(group_label, self)\n        content_widget = QtWidgets.QWidget(expading_widget)\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0)\n\n        widgets_by_variant_name = {}\n        for variant_name, variant_entity in valid_variants.items():\n            if \"executables\" not in variant_entity:\n                continue\n\n            variant_label = variant_entity.label\n            if dynamic and hasattr(variants_entity, \"get_key_label\"):\n                variant_label = variants_entity.get_key_label(variant_name)\n\n            variant_widget = AppVariantWidget(\n                group_label,\n                variant_name,\n                variant_label,\n                variant_entity,\n                content_widget\n            )\n            widgets_by_variant_name[variant_name] = variant_widget\n            content_layout.addWidget(variant_widget)\n\n        expading_widget.set_content_widget(content_widget)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(expading_widget)\n\n        self.widgets_by_variant_name = widgets_by_variant_name\n\n    def update_local_settings(self, value):\n        if not value:\n            value = {}\n\n        for variant_name, widget in self.widgets_by_variant_name.items():\n            widget.update_local_settings(value.get(variant_name))\n\n    def settings_value(self):\n        output = {}\n        for variant_name, widget in self.widgets_by_variant_name.items():\n            value = widget.settings_value()\n            if value:\n                output[variant_name] = value\n\n        if not output:\n            return None\n        return output\n\n\nclass LocalApplicationsWidgets(QtWidgets.QWidget):\n    def __init__(self, system_settings_entity, parent):\n        super(LocalApplicationsWidgets, self).__init__(parent)\n\n        self.widgets_by_group_name = {}\n        self.system_settings_entity = system_settings_entity\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        self.content_layout = layout\n\n    def _filter_group_entity(self, entity):\n        if not entity[\"enabled\"].value:\n            return False\n\n        # Check if has enabled any variant\n        for variant_entity in entity[\"variants\"].values():\n            if (\n                \"enabled\" not in variant_entity\n                or variant_entity[\"enabled\"].value\n            ):\n                return True\n\n        return False\n\n    def _reset_app_widgets(self):\n        while self.content_layout.count() > 0:\n            item = self.content_layout.itemAt(0)\n            widget = item.widget()\n            if widget is not None:\n                widget.setVisible(False)\n            self.content_layout.removeItem(item)\n        self.widgets_by_group_name.clear()\n\n        app_items = {}\n        additional_apps = set()\n        additional_apps_entity = None\n        for key, entity in self.system_settings_entity[\"applications\"].items():\n            if key != \"additional_apps\":\n                app_items[key] = entity\n                continue\n\n            additional_apps_entity = entity\n            for _key, _entity in entity.items():\n                app_items[_key] = _entity\n                additional_apps.add(_key)\n\n        for key, entity in app_items.items():\n            if not self._filter_group_entity(entity):\n                continue\n\n            dynamic = key in additional_apps\n            group_label = None\n            if dynamic and hasattr(additional_apps_entity, \"get_key_label\"):\n                group_label = additional_apps_entity.get_key_label(key)\n\n            if not group_label:\n                group_label = entity.label\n                if not group_label:\n                    group_label = key\n\n            # Create App group specific widget and store it by the key\n            group_widget = AppGroupWidget(entity, group_label, self, dynamic)\n            if group_widget.widgets_by_variant_name:\n                self.widgets_by_group_name[key] = group_widget\n                self.content_layout.addWidget(group_widget)\n            else:\n                group_widget.setVisible(False)\n                group_widget.deleteLater()\n\n    def update_local_settings(self, value):\n        if not value:\n            value = {}\n\n        self._reset_app_widgets()\n\n        for group_name, widget in self.widgets_by_group_name.items():\n            widget.update_local_settings(value.get(group_name))\n\n    def settings_value(self):\n        output = {}\n        for group_name, widget in self.widgets_by_group_name.items():\n            value = widget.settings_value()\n            if value:\n                output[group_name] = value\n        if not output:\n            return None\n        return output\n"
  },
  {
    "path": "openpype/tools/settings/local_settings/constants.py",
    "content": "# Action labels\nLABEL_REMOVE_DEFAULT = \"Remove from default\"\nLABEL_ADD_DEFAULT = \"Add to default\"\nLABEL_REMOVE_PROJECT = \"Remove from project\"\nLABEL_ADD_PROJECT = \"Add to project\"\nLABEL_DISCARD_CHANGES = \"Discard changes\"\n\n# Local setting constants\n# TODO move to settings constants\nLOCAL_GENERAL_KEY = \"general\"\nLOCAL_PROJECTS_KEY = \"projects\"\nLOCAL_ENV_KEY = \"environments\"\nLOCAL_APPS_KEY = \"applications\"\n\n# Roots key constant\nLOCAL_ROOTS_KEY = \"roots\"\n\n\n__all__ = (\n    \"LABEL_REMOVE_DEFAULT\",\n    \"LABEL_ADD_DEFAULT\",\n    \"LABEL_REMOVE_PROJECT\",\n    \"LABEL_ADD_PROJECT\",\n    \"LABEL_DISCARD_CHANGES\",\n\n    \"LOCAL_GENERAL_KEY\",\n    \"LOCAL_PROJECTS_KEY\",\n    \"LOCAL_APPS_KEY\",\n\n    \"LOCAL_ROOTS_KEY\"\n)\n"
  },
  {
    "path": "openpype/tools/settings/local_settings/environments_widget.py",
    "content": "from qtpy import QtWidgets\n\nfrom openpype.tools.utils import PlaceholderLineEdit\n\n\nclass LocalEnvironmentsWidgets(QtWidgets.QWidget):\n    def __init__(self, system_settings_entity, parent):\n        super(LocalEnvironmentsWidgets, self).__init__(parent)\n\n        self._widgets_by_env_key = {}\n        self.system_settings_entity = system_settings_entity\n\n        content_widget = QtWidgets.QWidget(self)\n        content_layout = QtWidgets.QGridLayout(content_widget)\n        content_layout.setContentsMargins(0, 0, 0, 0)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        self._layout = layout\n        self._content_layout = content_layout\n        self._content_widget = content_widget\n\n    def _clear_layout(self, layout):\n        while layout.count() > 0:\n            item = layout.itemAt(0)\n            widget = item.widget()\n            layout.removeItem(item)\n            if widget is not None:\n                widget.setVisible(False)\n                widget.deleteLater()\n\n    def _reset_env_widgets(self):\n        self._clear_layout(self._content_layout)\n        self._clear_layout(self._layout)\n\n        content_widget = QtWidgets.QWidget(self)\n        content_layout = QtWidgets.QGridLayout(content_widget)\n        content_layout.setContentsMargins(0, 0, 0, 0)\n        white_list_entity = (\n            self.system_settings_entity[\"general\"][\"local_env_white_list\"]\n        )\n        row = -1\n        for row, item in enumerate(white_list_entity):\n            key = item.value\n            label_widget = QtWidgets.QLabel(key, self)\n            input_widget = PlaceholderLineEdit(self)\n            input_widget.setPlaceholderText(\"< Keep studio value >\")\n\n            content_layout.addWidget(label_widget, row, 0)\n            content_layout.addWidget(input_widget, row, 1)\n\n            self._widgets_by_env_key[key] = input_widget\n\n        if row < 0:\n            label_widget = QtWidgets.QLabel(\n                (\n                    \"Your studio does not allow to change\"\n                    \" Environment variables locally.\"\n                ),\n                self\n            )\n            content_layout.addWidget(label_widget, 0, 0)\n            content_layout.setColumnStretch(0, 1)\n\n        else:\n            content_layout.setColumnStretch(0, 0)\n            content_layout.setColumnStretch(1, 1)\n\n        self._layout.addWidget(content_widget, 1)\n\n        self._content_layout = content_layout\n        self._content_widget = content_widget\n\n    def update_local_settings(self, value):\n        if not value:\n            value = {}\n\n        self._reset_env_widgets()\n\n        for env_key, widget in self._widgets_by_env_key.items():\n            env_value = value.get(env_key) or \"\"\n            widget.setText(env_value)\n\n    def settings_value(self):\n        output = {}\n        for env_key, widget in self._widgets_by_env_key.items():\n            value = widget.text()\n            if value:\n                output[env_key] = value\n        if not output:\n            return None\n        return output\n"
  },
  {
    "path": "openpype/tools/settings/local_settings/experimental_widget.py",
    "content": "from qtpy import QtWidgets\nfrom openpype.tools.experimental_tools import (\n    ExperimentalTools,\n    LOCAL_EXPERIMENTAL_KEY\n)\n\n\n__all__ = (\n    \"LocalExperimentalToolsWidgets\",\n    \"LOCAL_EXPERIMENTAL_KEY\"\n)\n\n\nclass LocalExperimentalToolsWidgets(QtWidgets.QWidget):\n    def __init__(self, parent):\n        super(LocalExperimentalToolsWidgets, self).__init__(parent)\n\n        self._loading_local_settings = False\n\n        layout = QtWidgets.QFormLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        # Label that says there are no experimental tools available\n        empty_label = QtWidgets.QLabel(self)\n        empty_label.setText(\n            \"There are no experimental tools available...\"\n        )\n\n        layout.addRow(empty_label)\n\n        experimental_defs = ExperimentalTools(refresh=False)\n        checkboxes_by_identifier = {}\n        for tool in experimental_defs.tools:\n            checkbox = QtWidgets.QCheckBox(self)\n            label_widget = QtWidgets.QLabel(tool.label, self)\n            checkbox.setToolTip(tool.tooltip)\n            label_widget.setToolTip(tool.tooltip)\n            layout.addRow(label_widget, checkbox)\n\n            checkboxes_by_identifier[tool.identifier] = checkbox\n\n        empty_label.setVisible(len(checkboxes_by_identifier) == 0)\n\n        self._empty_label = empty_label\n        self._checkboxes_by_identifier = checkboxes_by_identifier\n        self._experimental_defs = experimental_defs\n\n    def update_local_settings(self, value):\n        self._loading_local_settings = True\n        value = value or {}\n\n        for identifier, checkbox in self._checkboxes_by_identifier.items():\n            checked = value.get(identifier, False)\n            checkbox.setChecked(checked)\n\n        self._loading_local_settings = False\n\n    def settings_value(self):\n        # Add changed\n        # If these have changed then\n        output = {}\n        for identifier, checkbox in self._checkboxes_by_identifier.items():\n            if checkbox.isChecked():\n                output[identifier] = True\n        return output\n"
  },
  {
    "path": "openpype/tools/settings/local_settings/general_widget.py",
    "content": "import getpass\n\nfrom qtpy import QtWidgets, QtCore\nfrom openpype.lib import is_admin_password_required\nfrom openpype.widgets import PasswordDialog\nfrom openpype.tools.utils import PlaceholderLineEdit\n\n\nclass LocalGeneralWidgets(QtWidgets.QWidget):\n    def __init__(self, parent):\n        super(LocalGeneralWidgets, self).__init__(parent)\n\n        self._loading_local_settings = False\n\n        username_input = PlaceholderLineEdit(self)\n        username_input.setPlaceholderText(getpass.getuser())\n\n        is_admin_input = QtWidgets.QCheckBox(self)\n\n        layout = QtWidgets.QFormLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        layout.addRow(\"OpenPype Username\", username_input)\n        layout.addRow(\"Admin permissions\", is_admin_input)\n\n        is_admin_input.stateChanged.connect(self._on_admin_check_change)\n\n        self.username_input = username_input\n        self.is_admin_input = is_admin_input\n\n    def update_local_settings(self, value):\n        self._loading_local_settings = True\n\n        username = \"\"\n        is_admin = False\n        if value:\n            username = value.get(\"username\", username)\n            is_admin = value.get(\"is_admin\", is_admin)\n\n        self.username_input.setText(username)\n\n        if self.is_admin_input.isChecked() != is_admin:\n            # Use state as `stateChanged` is connected to callbacks\n            if is_admin:\n                state = QtCore.Qt.Checked\n            else:\n                state = QtCore.Qt.Unchecked\n            self.is_admin_input.setCheckState(state)\n\n        self._loading_local_settings = False\n\n    def _on_admin_check_change(self):\n        if self._loading_local_settings:\n            return\n\n        if not self.is_admin_input.isChecked():\n            return\n\n        if not is_admin_password_required():\n            return\n\n        dialog = PasswordDialog(self, False)\n        dialog.setModal(True)\n        dialog.exec_()\n        result = dialog.result()\n        if self.is_admin_input.isChecked() != result:\n            # Use state as `stateChanged` is connected to callbacks\n            if result:\n                state = QtCore.Qt.Checked\n            else:\n                state = QtCore.Qt.Unchecked\n            self.is_admin_input.setCheckState(state)\n\n    def settings_value(self):\n        # Add changed\n        # If these have changed then\n        output = {}\n        username = self.username_input.text()\n        if username:\n            output[\"username\"] = username\n\n        is_admin = self.is_admin_input.isChecked()\n        if is_admin:\n            output[\"is_admin\"] = is_admin\n        return output\n"
  },
  {
    "path": "openpype/tools/settings/local_settings/mongo_widget.py",
    "content": "import os\nimport sys\nimport traceback\n\nfrom qtpy import QtWidgets\nfrom pymongo.errors import ServerSelectionTimeoutError\n\nfrom openpype.lib import change_openpype_mongo_url\nfrom openpype.tools.utils import PlaceholderLineEdit\n\n\nclass OpenPypeMongoWidget(QtWidgets.QWidget):\n    def __init__(self, parent):\n        super(OpenPypeMongoWidget, self).__init__(parent)\n\n        # Warning label\n        warning_label = QtWidgets.QLabel((\n            \"WARNING: Requires restart. Change of OpenPype Mongo requires to\"\n            \" restart of all running Pype processes and process using Pype\"\n            \" (Including this).\"\n            \"\\n- all changes in different categories won't be saved.\"\n        ), self)\n        warning_label.setStyleSheet(\"font-weight: bold;\")\n\n        # Label\n        mongo_url_label = QtWidgets.QLabel(\"OpenPype Mongo URL\", self)\n\n        # Input\n        mongo_url_input = PlaceholderLineEdit(self)\n        mongo_url_input.setPlaceholderText(\"< OpenPype Mongo URL >\")\n        mongo_url_input.setText(os.environ[\"OPENPYPE_MONGO\"])\n\n        # Confirm button\n        mongo_url_change_btn = QtWidgets.QPushButton(\"Confirm Change\", self)\n\n        layout = QtWidgets.QGridLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(warning_label, 0, 0, 1, 3)\n        layout.addWidget(mongo_url_label, 1, 0)\n        layout.addWidget(mongo_url_input, 1, 1)\n        layout.addWidget(mongo_url_change_btn, 1, 2)\n\n        mongo_url_change_btn.clicked.connect(self._on_confirm_click)\n\n        self.mongo_url_input = mongo_url_input\n\n    def _on_confirm_click(self):\n        value = self.mongo_url_input.text()\n\n        dialog = QtWidgets.QMessageBox(self)\n\n        title = \"OpenPype mongo changed\"\n        message = (\n            \"OpenPype mongo url was successfully changed.\"\n            \" Restart OpenPype application please.\"\n        )\n        details = None\n\n        try:\n            change_openpype_mongo_url(value)\n        except Exception as exc:\n            if isinstance(exc, ServerSelectionTimeoutError):\n                error_message = (\n                    \"Connection timeout passed.\"\n                    \" Probably can't connect to the Mongo server.\"\n                )\n            else:\n                error_message = str(exc)\n\n            title = \"OpenPype mongo change failed\"\n            # TODO catch exception message more gracefully\n            message = (\n                \"OpenPype mongo change was not successful.\"\n                \" Full traceback can be found in details section.\\n\\n\"\n                \"Error message:\\n{}\"\n            ).format(error_message)\n            details = \"\\n\".join(traceback.format_exception(*sys.exc_info()))\n        dialog.setWindowTitle(title)\n        dialog.setText(message)\n        if details:\n            dialog.setDetailedText(details)\n        dialog.exec_()\n"
  },
  {
    "path": "openpype/tools/settings/local_settings/projects_widget.py",
    "content": "import platform\nimport copy\nfrom qtpy import QtWidgets, QtCore, QtGui\nfrom openpype.tools.settings.settings import ProjectListWidget\nfrom openpype.tools.utils import PlaceholderLineEdit\nfrom openpype.settings.constants import (\n    PROJECT_ANATOMY_KEY,\n    DEFAULT_PROJECT_KEY\n)\nfrom .widgets import ProxyLabelWidget\nfrom .constants import (\n    LABEL_REMOVE_DEFAULT,\n    LABEL_ADD_DEFAULT,\n    LABEL_REMOVE_PROJECT,\n    LABEL_ADD_PROJECT,\n    LABEL_DISCARD_CHANGES,\n    LOCAL_ROOTS_KEY\n)\n\nNOT_SET = type(\"NOT_SET\", (), {})()\n\n\nclass _ProjectListWidget(ProjectListWidget):\n    def on_item_clicked(self, new_index):\n        new_project_name = new_index.data(QtCore.Qt.DisplayRole)\n        if new_project_name is None:\n            return\n\n        if self.current_project == new_project_name:\n            return\n\n        self.select_project(new_project_name)\n        self.current_project = new_project_name\n        self.project_changed.emit()\n\n\nclass DynamicInputItem(QtCore.QObject):\n    value_changed = QtCore.Signal(str, str)\n\n    def __init__(\n        self,\n        input_def,\n        site_name,\n        value_item,\n        label_widget,\n        parent\n    ):\n        super(DynamicInputItem, self).__init__()\n        input_widget = PlaceholderLineEdit(parent)\n\n        settings_value = input_def.get(\"value\")\n        placeholder = input_def.get(\"placeholder\")\n\n        value_placeholder_template = \"< {} >\"\n        if (\n            not placeholder\n            and value_item.project_name != DEFAULT_PROJECT_KEY\n            and value_item.default_value\n        ):\n            placeholder = value_placeholder_template.format(\n                value_item.default_value\n            )\n\n        if not placeholder and settings_value:\n            placeholder = value_placeholder_template.format(settings_value)\n\n        if placeholder:\n            input_widget.setPlaceholderText(placeholder)\n\n        if value_item.value:\n            input_widget.setText(value_item.value)\n\n        input_widget.textChanged.connect(self._on_str_change)\n\n        self.value_item = value_item\n        self.site_name = site_name\n        self.key = input_def[\"key\"]\n\n        self.settings_value = settings_value\n\n        self.current_value = input_widget.text()\n\n        self.input_widget = input_widget\n        self.label_widget = label_widget\n\n        self.parent_widget = parent\n\n        label_widget.set_mouse_release_callback(self._mouse_release_callback)\n        self._update_style()\n\n    @property\n    def origin_value(self):\n        return self.value_item.orig_value\n\n    @property\n    def project_name(self):\n        return self.value_item.project_name\n\n    def _on_str_change(self, value):\n        if self.current_value == value:\n            return\n\n        self.current_value = value\n        self.value_changed.emit(self.site_name, self.key)\n        self._update_style()\n\n    def is_modified(self):\n        return self.origin_value != self.input_widget.text()\n\n    def _mouse_release_callback(self, event):\n        if event.button() != QtCore.Qt.RightButton:\n            return\n        self._show_actions()\n        event.accept()\n\n    def _get_style_state(self):\n        if self.project_name is None:\n            return \"\"\n\n        if self.is_modified():\n            return \"modified\"\n\n        current_value = self.input_widget.text()\n        if self.project_name == DEFAULT_PROJECT_KEY:\n            if current_value:\n                return \"studio\"\n        else:\n            if current_value:\n                return \"overridden\"\n\n            if self.value_item.default_value:\n                return \"studio\"\n        return \"\"\n\n    def _update_style(self):\n        state = self._get_style_state()\n\n        self.input_widget.setProperty(\"input-state\", state)\n        self.input_widget.style().polish(self.input_widget)\n\n        self.label_widget.set_label_property(\"state\", state)\n\n    def _remove_from_local(self):\n        self.input_widget.setText(\"\")\n\n    def _add_to_local(self):\n        value = self.value_item.default_value\n        if self.project_name == DEFAULT_PROJECT_KEY or not value:\n            value = self.settings_value\n\n        self.input_widget.setText(value)\n\n    def discard_changes(self):\n        self.input_widget.setText(self.origin_value)\n\n    def _show_actions(self):\n        if self.project_name is None:\n            return\n\n        menu = QtWidgets.QMenu(self.parent_widget)\n        actions_mapping = {}\n\n        if self.project_name == DEFAULT_PROJECT_KEY:\n            remove_label = LABEL_REMOVE_DEFAULT\n            add_label = LABEL_ADD_DEFAULT\n        else:\n            remove_label = LABEL_REMOVE_PROJECT\n            add_label = LABEL_ADD_PROJECT\n\n        if self.input_widget.text():\n            action = QtWidgets.QAction(remove_label)\n            callback = self._remove_from_local\n        else:\n            action = QtWidgets.QAction(add_label)\n            callback = self._add_to_local\n\n        actions_mapping[action] = callback\n        menu.addAction(action)\n\n        if self.is_modified():\n            discard_changes_action = QtWidgets.QAction(LABEL_DISCARD_CHANGES)\n            actions_mapping[discard_changes_action] = self.discard_changes\n            menu.addAction(discard_changes_action)\n\n        result = menu.exec_(QtGui.QCursor.pos())\n        if result:\n            to_run = actions_mapping[result]\n            if to_run:\n                to_run()\n\n\nclass SiteValueItem:\n    def __init__(\n        self,\n        project_name,\n        value,\n        default_value,\n        orig_value,\n        orig_default_value\n    ):\n        self.project_name = project_name\n        self.value = value or \"\"\n        self.default_value = default_value or \"\"\n        self.orig_value = orig_value or \"\"\n        self.orig_default_value = orig_default_value or \"\"\n\n    def __repr__(self):\n        return \"\\n\".join((\n            \"Project: {}\".format(self.project_name),\n            \"Value: {}\".format(self.value),\n            \"Default value: {}\".format(self.default_value),\n            \"Orig value: {}\".format(self.orig_value),\n            \"Orig default value: {}\".format(self.orig_default_value),\n        ))\n\n\nclass SitesWidget(QtWidgets.QWidget):\n    def __init__(self, modules_manager, project_settings, parent):\n        super(SitesWidget, self).__init__(parent)\n\n        self.modules_manager = modules_manager\n        self.project_settings = project_settings\n        self.input_objects = {}\n        self.local_project_settings = None\n        self.local_project_settings_orig = None\n        self._project_name = None\n\n        comboboxes_widget = QtWidgets.QWidget(self)\n\n        active_site_widget = AciveSiteCombo(\n            modules_manager, project_settings, comboboxes_widget\n        )\n        remote_site_widget = RemoteSiteCombo(\n            modules_manager, project_settings, comboboxes_widget\n        )\n\n        comboboxes_layout = QtWidgets.QHBoxLayout(comboboxes_widget)\n        comboboxes_layout.setContentsMargins(0, 0, 0, 0)\n        comboboxes_layout.addWidget(active_site_widget, 0)\n        comboboxes_layout.addWidget(remote_site_widget, 0)\n        comboboxes_layout.addStretch(1)\n\n        content_widget = QtWidgets.QWidget(self)\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.setContentsMargins(0, 0, 0, 0)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(comboboxes_widget, 0)\n        main_layout.addWidget(content_widget, 1)\n\n        active_site_widget.value_changed.connect(self.refresh)\n        remote_site_widget.value_changed.connect(self.refresh)\n\n        self.active_site_widget = active_site_widget\n        self.remote_site_widget = remote_site_widget\n\n        self.content_widget = content_widget\n        self.content_layout = content_layout\n\n    def _clear_widgets(self):\n        while self.content_layout.count():\n            item = self.content_layout.itemAt(0)\n            widget = item.widget()\n            if widget is not None:\n                widget.setVisible(False)\n            self.content_layout.removeItem(item)\n        self.input_objects = {}\n\n    def _get_sites_inputs(self):\n        output = []\n        if self._project_name is None:\n            return output\n\n        sync_server_module = self.modules_manager.modules_by_name.get(\n            \"sync_server\")\n        if sync_server_module is None or not sync_server_module.enabled:\n            return output\n\n        site_configs = sync_server_module.get_all_site_configs(\n            self._project_name, local_editable_only=True)\n\n        site_names = [self.active_site_widget.current_text(),\n                      self.remote_site_widget.current_text()]\n        for site_name in site_names:\n            if not site_name:\n                continue\n\n            site_inputs = []\n            site_config = site_configs.get(site_name, {})\n            for root_name, path_entity in site_config.get(\"root\", {}).items():\n                if not path_entity:\n                    continue\n                platform_value = path_entity[platform.system().lower()]\n                site_inputs.append({\n                    \"label\": root_name,\n                    \"key\": root_name,\n                    \"value\": platform_value\n                })\n\n            output.append(\n                (site_name, site_inputs)\n            )\n        return output\n\n    @staticmethod\n    def _extract_value_from_data(data, project_name, site_name, key):\n        _s_value = data\n        for _key in (project_name, site_name, key):\n            if _key not in _s_value:\n                return None\n            _s_value = _s_value[_key]\n        return _s_value\n\n    def _prepare_value_item(self, site_name, key):\n        value = self._extract_value_from_data(\n            self.local_project_settings,\n            self._project_name,\n            site_name,\n            key\n        )\n        orig_value = self._extract_value_from_data(\n            self.local_project_settings_orig,\n            self._project_name,\n            site_name,\n            key\n        )\n        orig_default_value = None\n        default_value = None\n        if self._project_name != DEFAULT_PROJECT_KEY:\n            default_value = self._extract_value_from_data(\n                self.local_project_settings,\n                DEFAULT_PROJECT_KEY,\n                site_name,\n                key\n            )\n            orig_default_value = self._extract_value_from_data(\n                self.local_project_settings_orig,\n                DEFAULT_PROJECT_KEY,\n                site_name,\n                key\n            )\n\n        return SiteValueItem(\n            self._project_name,\n            value,\n            default_value,\n            orig_value,\n            orig_default_value\n        )\n\n    def refresh(self):\n        self._clear_widgets()\n\n        # Site label\n        for site_name, site_inputs in self._get_sites_inputs():\n            site_widget = QtWidgets.QWidget(self.content_widget)\n            site_layout = QtWidgets.QVBoxLayout(site_widget)\n\n            site_label = QtWidgets.QLabel(site_name, site_widget)\n\n            inputs_widget = QtWidgets.QWidget(site_widget)\n            inputs_layout = QtWidgets.QGridLayout(inputs_widget)\n\n            site_input_objects = {}\n            for idx, input_def in enumerate(site_inputs):\n                key = input_def[\"key\"]\n                label = input_def.get(\"label\") or key\n                label_widget = ProxyLabelWidget(label, None, inputs_widget)\n\n                value_item = self._prepare_value_item(site_name, key)\n\n                input_obj = DynamicInputItem(\n                    input_def,\n                    site_name,\n                    value_item,\n                    label_widget,\n                    inputs_widget\n                )\n                input_obj.value_changed.connect(self._on_input_value_change)\n                site_input_objects[key] = input_obj\n                inputs_layout.addWidget(label_widget, idx, 0)\n                inputs_layout.addWidget(input_obj.input_widget, idx, 1)\n\n            site_layout.addWidget(site_label)\n            site_layout.addWidget(inputs_widget)\n\n            self.content_layout.addWidget(site_widget)\n            self.input_objects[site_name] = site_input_objects\n\n        # Add spacer so other widgets are squeezed to top\n        self.content_layout.addStretch(1)\n\n    def _on_input_value_change(self, site_name, key):\n        if (\n            site_name not in self.input_objects\n            or key not in self.input_objects[site_name]\n        ):\n            return\n\n        input_obj = self.input_objects[site_name][key]\n        value = input_obj.current_value\n\n        if not value:\n            if self._project_name not in self.local_project_settings:\n                return\n\n            project_values = self.local_project_settings[self._project_name]\n            if site_name not in project_values:\n                return\n\n            project_values[site_name][key] = None\n\n        else:\n            if self._project_name not in self.local_project_settings:\n                self.local_project_settings[self._project_name] = {}\n\n            project_values = self.local_project_settings[self._project_name]\n            if site_name not in project_values:\n                project_values[site_name] = {}\n\n            project_values[site_name][key] = value\n\n    def update_local_settings(self, local_project_settings):\n        self.local_project_settings = local_project_settings\n        self.local_project_settings_orig = copy.deepcopy(\n            dict(local_project_settings)\n        )\n        self.active_site_widget.update_local_settings(local_project_settings)\n        self.remote_site_widget.update_local_settings(local_project_settings)\n\n    def change_project(self, project_name):\n        self._project_name = None\n        self.refresh()\n\n        self.active_site_widget.change_project(project_name)\n        self.remote_site_widget.change_project(project_name)\n\n        self._project_name = project_name\n        self.refresh()\n\n\nclass _SiteCombobox(QtWidgets.QWidget):\n    input_label = None\n    value_changed = QtCore.Signal()\n\n    def __init__(self, modules_manager, project_settings, parent):\n        super(_SiteCombobox, self).__init__(parent)\n        self.project_settings = project_settings\n\n        self.modules_manager = modules_manager\n\n        self.local_project_settings = None\n        self.local_project_settings_orig = None\n        self.project_name = None\n\n        self.default_override_value = None\n        self.project_override_value = None\n\n        label_widget = ProxyLabelWidget(\n            self.input_label,\n            self._mouse_release_callback,\n            self\n        )\n        combobox_input = QtWidgets.QComboBox(self)\n        combobox_delegate = QtWidgets.QStyledItemDelegate()\n        combobox_input.setItemDelegate(combobox_delegate)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.addWidget(label_widget)\n        main_layout.addWidget(combobox_input)\n\n        combobox_input.currentIndexChanged.connect(self._on_index_change)\n        self.label_widget = label_widget\n        self.combobox_input = combobox_input\n        self._combobox_delegate = combobox_delegate\n\n    def _set_current_text(self, text):\n        index = None\n        if text:\n            idx = self.combobox_input.findText(text)\n            if idx >= 0:\n                index = idx\n\n        if index is not None:\n            self.combobox_input.setCurrentIndex(index)\n            return True\n        return False\n\n    def is_modified(self, current_value=NOT_SET, orig_value=NOT_SET):\n        if current_value is NOT_SET:\n            current_value = self._get_local_settings_item(self.project_name)\n        if orig_value is NOT_SET:\n            orig_value = self._get_local_settings_item(\n                self.project_name, self.local_project_settings_orig\n            )\n        if current_value and orig_value:\n            modified = current_value != orig_value\n        elif not current_value and not orig_value:\n            modified = False\n        else:\n            modified = True\n        return modified\n\n    def _get_style_state(self):\n        if self.project_name is None:\n            return \"\"\n\n        current_value = self._get_local_settings_item(self.project_name)\n        orig_value = self._get_local_settings_item(\n            self.project_name, self.local_project_settings_orig\n        )\n\n        if self.is_modified(current_value, orig_value):\n            return \"modified\"\n\n        if self.project_name == DEFAULT_PROJECT_KEY:\n            if current_value:\n                return \"studio\"\n        else:\n            if current_value:\n                return \"overridden\"\n\n            studio_value = self._get_local_settings_item(DEFAULT_PROJECT_KEY)\n            if studio_value:\n                return \"studio\"\n        return \"\"\n\n    def _update_style(self):\n        state = self._get_style_state()\n\n        self.combobox_input.setProperty(\"input-state\", state)\n        self.combobox_input.style().polish(self.combobox_input)\n\n        self.label_widget.set_label_property(\"state\", state)\n\n    def _mouse_release_callback(self, event):\n        if event.button() != QtCore.Qt.RightButton:\n            return\n        self._show_actions()\n\n    def _remove_from_local(self):\n        settings_value = self._get_value_from_project_settings()\n        combobox_value = None\n        if self.project_name == DEFAULT_PROJECT_KEY:\n            combobox_value = self._get_local_settings_item(DEFAULT_PROJECT_KEY)\n            if combobox_value:\n                idx = self.combobox_input.findText(combobox_value)\n                if idx < 0:\n                    combobox_value = None\n\n        if not combobox_value:\n            combobox_value = settings_value\n\n        if combobox_value:\n            _project_name = self.project_name\n            self.project_name = None\n            self._set_current_text(combobox_value)\n            self.project_name = _project_name\n\n        self._set_local_settings_value(\"\")\n        self._update_style()\n\n    def _add_to_local(self):\n        self._set_local_settings_value(self.current_text())\n        self._update_style()\n\n    def discard_changes(self):\n        orig_value = self._get_local_settings_item(\n            self.project_name, self.local_project_settings_orig\n        )\n        self._set_current_text(orig_value)\n\n    def _show_actions(self):\n        if self.project_name is None:\n            return\n\n        menu = QtWidgets.QMenu(self)\n        actions_mapping = {}\n\n        if self.project_name == DEFAULT_PROJECT_KEY:\n            remove_label = LABEL_REMOVE_DEFAULT\n            add_label = LABEL_ADD_DEFAULT\n        else:\n            remove_label = LABEL_REMOVE_PROJECT\n            add_label = LABEL_ADD_PROJECT\n\n        has_value = self._get_local_settings_item(self.project_name)\n        if has_value:\n            action = QtWidgets.QAction(remove_label)\n            callback = self._remove_from_local\n        else:\n            action = QtWidgets.QAction(add_label)\n            callback = self._add_to_local\n\n        actions_mapping[action] = callback\n        menu.addAction(action)\n\n        if self.is_modified():\n            discard_changes_action = QtWidgets.QAction(LABEL_DISCARD_CHANGES)\n            actions_mapping[discard_changes_action] = self.discard_changes\n            menu.addAction(discard_changes_action)\n\n        result = menu.exec_(QtGui.QCursor.pos())\n        if result:\n            to_run = actions_mapping[result]\n            if to_run:\n                to_run()\n\n    def update_local_settings(self, local_project_settings):\n        self.local_project_settings = local_project_settings\n        self.local_project_settings_orig = copy.deepcopy(\n            dict(local_project_settings)\n        )\n\n    def current_text(self):\n        return self.combobox_input.currentText()\n\n    def change_project(self, project_name):\n        self.default_override_value = None\n        self.project_override_value = None\n\n        self.project_name = None\n        self.combobox_input.clear()\n        if project_name is None:\n            self._update_style()\n            return\n\n        is_default_project = bool(project_name == DEFAULT_PROJECT_KEY)\n        site_items = self._get_project_sites()\n        self.combobox_input.addItems(site_items)\n\n        default_item = self._get_local_settings_item(DEFAULT_PROJECT_KEY)\n        if is_default_project:\n            project_item = None\n        else:\n            project_item = self._get_local_settings_item(project_name)\n\n        index = None\n        if project_item:\n            idx = self.combobox_input.findText(project_item)\n            if idx >= 0:\n                self.project_override_value = project_item\n                index = idx\n\n        if default_item:\n            idx = self.combobox_input.findText(default_item)\n            if idx >= 0:\n                self.default_override_value = default_item\n                if index is None:\n                    index = idx\n\n        if index is None:\n            settings_value = self._get_value_from_project_settings()\n            idx = self.combobox_input.findText(settings_value)\n            if idx >= 0:\n                index = idx\n\n        if index is not None:\n            self.combobox_input.setCurrentIndex(index)\n\n        self.project_name = project_name\n        self._update_style()\n\n    def _on_index_change(self):\n        if self.project_name is None:\n            return\n\n        self._set_local_settings_value(self.current_text())\n        self._update_style()\n        self.value_changed.emit()\n\n    def _set_local_settings_value(self, value):\n        raise NotImplementedError(\n            \"{} `_set_local_settings_value` not implemented\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def _get_project_sites(self):\n        raise NotImplementedError(\n            \"{} `_get_project_sites` not implemented\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def _get_local_settings_item(self, project_name=None, data=None):\n        raise NotImplementedError(\n            \"{}`_get_local_settings_item` not implemented\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def _get_value_from_project_settings(self):\n        raise NotImplementedError(\n            \"{}`_get_value_from_project_settings` not implemented\".format(\n                self.__class__.__name__\n            )\n        )\n\n\nclass AciveSiteCombo(_SiteCombobox):\n    input_label = \"Active site\"\n\n    def _get_project_sites(self):\n        sync_server_module = (\n            self.modules_manager.modules_by_name[\"sync_server\"]\n        )\n        if self.project_name is None:\n            return sync_server_module.get_active_sites_from_settings(\n                self.project_settings[\"project_settings\"].value\n            )\n        return sync_server_module.get_active_sites(self.project_name)\n\n    def _get_local_settings_item(self, project_name=None, data=None):\n        if project_name is None:\n            project_name = self.project_name\n\n        if data is None:\n            data = self.local_project_settings\n        project_values = data.get(project_name)\n        value = None\n        if project_values:\n            value = project_values.get(\"active_site\")\n        return value\n\n    def _get_value_from_project_settings(self):\n        global_entity = self.project_settings[\"project_settings\"][\"global\"]\n        return global_entity[\"sync_server\"][\"config\"][\"active_site\"].value\n\n    def _set_local_settings_value(self, value):\n        if self.project_name not in self.local_project_settings:\n            self.local_project_settings[self.project_name] = {}\n        self.local_project_settings[self.project_name][\"active_site\"] = value\n\n\nclass RemoteSiteCombo(_SiteCombobox):\n    input_label = \"Remote site\"\n\n    def change_project(self, *args, **kwargs):\n        super(RemoteSiteCombo, self).change_project(*args, **kwargs)\n\n        self.setVisible(self.combobox_input.count() > 0)\n        if not self.isVisible():\n            self._set_local_settings_value(\"\")\n\n    def _get_project_sites(self):\n        sync_server_module = (\n            self.modules_manager.modules_by_name[\"sync_server\"]\n        )\n        if self.project_name is None:\n            return sync_server_module.get_remote_sites_from_settings(\n                self.project_settings[\"project_settings\"].value\n            )\n        return sync_server_module.get_remote_sites(self.project_name)\n\n    def _get_local_settings_item(self, project_name=None, data=None):\n        if project_name is None:\n            project_name = self.project_name\n        if data is None:\n            data = self.local_project_settings\n        project_values = data.get(project_name)\n        value = None\n        if project_values:\n            value = project_values.get(\"remote_site\")\n        return value\n\n    def _get_value_from_project_settings(self):\n        global_entity = self.project_settings[\"project_settings\"][\"global\"]\n        return global_entity[\"sync_server\"][\"config\"][\"remote_site\"].value\n\n    def _set_local_settings_value(self, value):\n        if self.project_name not in self.local_project_settings:\n            self.local_project_settings[self.project_name] = {}\n        self.local_project_settings[self.project_name][\"remote_site\"] = value\n\n\nclass RootSiteWidget(QtWidgets.QWidget):\n    def __init__(self, modules_manager, project_settings, parent):\n        self._parent_widget = parent\n        super(RootSiteWidget, self).__init__(parent)\n\n        self.modules_manager = modules_manager\n        self.project_settings = project_settings\n        self._project_name = None\n\n        sites_widget = SitesWidget(modules_manager, project_settings, self)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(sites_widget)\n        main_layout.addStretch(1)\n\n        self.sites_widget = sites_widget\n\n    def update_local_settings(self, local_project_settings):\n        self.local_project_settings = local_project_settings\n        self.sites_widget.update_local_settings(local_project_settings)\n        project_name = self._project_name\n        if project_name is None:\n            project_name = DEFAULT_PROJECT_KEY\n\n        self.change_project(project_name)\n\n    def change_project(self, project_name):\n        self._project_name = project_name\n\n        # Change project name in roots widget\n        self.sites_widget.change_project(project_name)\n\n\nclass ProjectValue(dict):\n    pass\n\n\nclass ProjectSettingsWidget(QtWidgets.QWidget):\n    def __init__(self, modules_manager, project_settings, parent):\n        super(ProjectSettingsWidget, self).__init__(parent)\n\n        self.local_project_settings = {}\n\n        self.modules_manager = modules_manager\n\n        projects_widget = _ProjectListWidget(self, only_active=True)\n        roos_site_widget = RootSiteWidget(\n            modules_manager, project_settings, self\n        )\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(projects_widget, 0)\n        main_layout.addWidget(roos_site_widget, 1)\n\n        projects_widget.project_changed.connect(self._on_project_change)\n\n        self.project_settings = project_settings\n\n        self.projects_widget = projects_widget\n        self.roos_site_widget = roos_site_widget\n\n    def project_name(self):\n        return self.projects_widget.project_name()\n\n    def _on_project_change(self):\n        project_name = self.project_name()\n        self.project_settings.change_project(project_name)\n        if project_name is None:\n            project_name = DEFAULT_PROJECT_KEY\n        self.roos_site_widget.change_project(project_name)\n\n    def update_local_settings(self, value):\n        if not value:\n            value = {}\n        self.local_project_settings = ProjectValue(value)\n\n        self.roos_site_widget.update_local_settings(\n            self.local_project_settings\n        )\n\n        self.projects_widget.refresh()\n\n    def _clear_value(self, value):\n        if not value:\n            return None\n\n        if not isinstance(value, dict):\n            return value\n\n        output = {}\n        for _key, _value in value.items():\n            _modified_value = self._clear_value(_value)\n            if _modified_value:\n                output[_key] = _modified_value\n        return output\n\n    def settings_value(self):\n        output = self._clear_value(self.local_project_settings)\n        if not output:\n            return None\n        return output\n"
  },
  {
    "path": "openpype/tools/settings/local_settings/widgets.py",
    "content": "from qtpy import QtWidgets, QtCore\nfrom openpype.tools.settings.settings.widgets import (\n    ExpandingWidget\n)\n\n\nclass Separator(QtWidgets.QFrame):\n    def __init__(self, height=None, parent=None):\n        super(Separator, self).__init__(parent)\n        if height is None:\n            height = 2\n\n        splitter_item = QtWidgets.QWidget(self)\n        splitter_item.setStyleSheet(\"background-color: #21252B;\")\n        splitter_item.setMinimumHeight(height)\n        splitter_item.setMaximumHeight(height)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(5, 5, 5, 5)\n        layout.addWidget(splitter_item)\n\n\nclass ProxyLabelWidget(QtWidgets.QWidget):\n    def __init__(self, label, mouse_release_callback=None, parent=None):\n        super(ProxyLabelWidget, self).__init__(parent)\n\n        self.mouse_release_callback = mouse_release_callback\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(0)\n\n        label_widget = QtWidgets.QLabel(label, self)\n        layout.addWidget(label_widget)\n\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        self.label_widget = label_widget\n\n    def set_mouse_release_callback(self, callback):\n        self.mouse_release_callback = callback\n\n    def setText(self, text):\n        self.label_widget.setText(text)\n\n    def set_label_property(self, *args, **kwargs):\n        self.label_widget.setProperty(*args, **kwargs)\n        self.label_widget.style().polish(self.label_widget)\n\n    def mouseReleaseEvent(self, event):\n        if self.mouse_release_callback:\n            return self.mouse_release_callback(event)\n        return super(ProxyLabelWidget, self).mouseReleaseEvent(event)\n\n\n__all__ = (\n    \"ExpandingWidget\",\n    \"Separator\",\n)\n"
  },
  {
    "path": "openpype/tools/settings/local_settings/window.py",
    "content": "from qtpy import QtWidgets, QtGui\n\nfrom openpype import style\n\nfrom openpype.settings import (\n    SystemSettings,\n    ProjectSettings\n)\nfrom openpype.settings.lib import (\n    get_local_settings,\n    save_local_settings\n)\nfrom openpype.lib import Logger\nfrom openpype.tools.settings import CHILD_OFFSET\nfrom openpype.tools.utils import MessageOverlayObject\nfrom openpype.modules import ModulesManager\n\nfrom .widgets import (\n    ExpandingWidget\n)\nfrom .mongo_widget import OpenPypeMongoWidget\nfrom .general_widget import LocalGeneralWidgets\nfrom .experimental_widget import (\n    LocalExperimentalToolsWidgets,\n    LOCAL_EXPERIMENTAL_KEY\n)\nfrom .apps_widget import LocalApplicationsWidgets\nfrom .environments_widget import LocalEnvironmentsWidgets\nfrom .projects_widget import ProjectSettingsWidget\n\nfrom .constants import (\n    LOCAL_GENERAL_KEY,\n    LOCAL_PROJECTS_KEY,\n    LOCAL_ENV_KEY,\n    LOCAL_APPS_KEY\n)\n\nlog = Logger.get_logger(__name__)\n\n\nclass LocalSettingsWidget(QtWidgets.QWidget):\n    def __init__(self, parent=None):\n        super(LocalSettingsWidget, self).__init__(parent)\n\n        self.system_settings = SystemSettings()\n        self.project_settings = ProjectSettings()\n        self.modules_manager = ModulesManager()\n\n        self.main_layout = QtWidgets.QVBoxLayout(self)\n\n        self.pype_mongo_widget = None\n        self.general_widget = None\n        self.experimental_widget = None\n        self.envs_widget = None\n        self.apps_widget = None\n        self.projects_widget = None\n\n        self._create_mongo_url_ui()\n        self._create_general_ui()\n        self._create_experimental_ui()\n        self._create_environments_ui()\n        self._create_app_ui()\n        self._create_project_ui()\n\n        self.main_layout.addStretch(1)\n\n    def _create_mongo_url_ui(self):\n        pype_mongo_expand_widget = ExpandingWidget(\"OpenPype Mongo URL\", self)\n        pype_mongo_content = QtWidgets.QWidget(self)\n        pype_mongo_layout = QtWidgets.QVBoxLayout(pype_mongo_content)\n        pype_mongo_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0)\n        pype_mongo_expand_widget.set_content_widget(pype_mongo_content)\n\n        pype_mongo_widget = OpenPypeMongoWidget(self)\n        pype_mongo_layout.addWidget(pype_mongo_widget)\n\n        self.main_layout.addWidget(pype_mongo_expand_widget)\n\n        self.pype_mongo_widget = pype_mongo_widget\n\n    def _create_general_ui(self):\n        # General\n        general_expand_widget = ExpandingWidget(\"General\", self)\n\n        general_content = QtWidgets.QWidget(self)\n        general_layout = QtWidgets.QVBoxLayout(general_content)\n        general_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0)\n        general_expand_widget.set_content_widget(general_content)\n\n        general_widget = LocalGeneralWidgets(general_content)\n        general_layout.addWidget(general_widget)\n\n        self.main_layout.addWidget(general_expand_widget)\n\n        self.general_widget = general_widget\n\n    def _create_experimental_ui(self):\n        # General\n        experimental_expand_widget = ExpandingWidget(\n            \"Experimental tools\", self\n        )\n\n        experimental_content = QtWidgets.QWidget(self)\n        experimental_layout = QtWidgets.QVBoxLayout(experimental_content)\n        experimental_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0)\n        experimental_expand_widget.set_content_widget(experimental_content)\n\n        experimental_widget = LocalExperimentalToolsWidgets(\n            experimental_content\n        )\n        experimental_layout.addWidget(experimental_widget)\n\n        self.main_layout.addWidget(experimental_expand_widget)\n\n        self.experimental_widget = experimental_widget\n\n    def _create_environments_ui(self):\n        envs_expand_widget = ExpandingWidget(\"Environments\", self)\n        envs_content = QtWidgets.QWidget(self)\n        envs_layout = QtWidgets.QVBoxLayout(envs_content)\n        envs_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0)\n        envs_expand_widget.set_content_widget(envs_content)\n\n        envs_widget = LocalEnvironmentsWidgets(\n            self.system_settings, envs_content\n        )\n        envs_layout.addWidget(envs_widget)\n\n        self.main_layout.addWidget(envs_expand_widget)\n\n        self.envs_widget = envs_widget\n\n    def _create_app_ui(self):\n        # Applications\n        app_expand_widget = ExpandingWidget(\"Applications\", self)\n\n        app_content = QtWidgets.QWidget(self)\n        app_layout = QtWidgets.QVBoxLayout(app_content)\n        app_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0)\n        app_expand_widget.set_content_widget(app_content)\n\n        app_widget = LocalApplicationsWidgets(\n            self.system_settings, app_content\n        )\n        app_layout.addWidget(app_widget)\n\n        self.main_layout.addWidget(app_expand_widget)\n\n        self.app_widget = app_widget\n\n    def _create_project_ui(self):\n        project_expand_widget = ExpandingWidget(\"Project settings\", self)\n        project_content = QtWidgets.QWidget(self)\n        project_layout = QtWidgets.QVBoxLayout(project_content)\n        project_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0)\n        project_expand_widget.set_content_widget(project_content)\n\n        projects_widget = ProjectSettingsWidget(\n            self.modules_manager, self.project_settings, self\n        )\n        project_layout.addWidget(projects_widget)\n\n        self.main_layout.addWidget(project_expand_widget)\n\n        self.projects_widget = projects_widget\n\n    def update_local_settings(self, value):\n        if not value:\n            value = {}\n\n        self.system_settings.reset()\n        self.project_settings.reset()\n\n        self.general_widget.update_local_settings(\n            value.get(LOCAL_GENERAL_KEY)\n        )\n        self.envs_widget.update_local_settings(\n            value.get(LOCAL_ENV_KEY)\n        )\n        self.app_widget.update_local_settings(\n            value.get(LOCAL_APPS_KEY)\n        )\n        self.projects_widget.update_local_settings(\n            value.get(LOCAL_PROJECTS_KEY)\n        )\n        self.experimental_widget.update_local_settings(\n            value.get(LOCAL_EXPERIMENTAL_KEY)\n        )\n\n    def settings_value(self):\n        output = {}\n        general_value = self.general_widget.settings_value()\n        if general_value:\n            output[LOCAL_GENERAL_KEY] = general_value\n\n        envs_value = self.envs_widget.settings_value()\n        if envs_value:\n            output[LOCAL_ENV_KEY] = envs_value\n\n        app_value = self.app_widget.settings_value()\n        if app_value:\n            output[LOCAL_APPS_KEY] = app_value\n\n        projects_value = self.projects_widget.settings_value()\n        if projects_value:\n            output[LOCAL_PROJECTS_KEY] = projects_value\n\n        experimental_value = self.experimental_widget.settings_value()\n        if experimental_value:\n            output[LOCAL_EXPERIMENTAL_KEY] = experimental_value\n        return output\n\n\nclass LocalSettingsWindow(QtWidgets.QWidget):\n    def __init__(self, parent=None):\n        super(LocalSettingsWindow, self).__init__(parent)\n\n        self._reset_on_show = True\n\n        self.resize(1000, 600)\n\n        self.setWindowTitle(\"OpenPype Local settings\")\n\n        overlay_object = MessageOverlayObject(self)\n\n        stylesheet = style.load_stylesheet()\n        self.setStyleSheet(stylesheet)\n        self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))\n\n        scroll_widget = QtWidgets.QScrollArea(self)\n        scroll_widget.setObjectName(\"GroupWidget\")\n        scroll_widget.setWidgetResizable(True)\n\n        footer = QtWidgets.QWidget(self)\n\n        save_btn = QtWidgets.QPushButton(\"Save\", footer)\n        reset_btn = QtWidgets.QPushButton(\"Reset\", footer)\n\n        footer_layout = QtWidgets.QHBoxLayout(footer)\n        footer_layout.addWidget(reset_btn, 0)\n        footer_layout.addStretch(1)\n        footer_layout.addWidget(save_btn, 0)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(scroll_widget, 1)\n        main_layout.addWidget(footer, 0)\n\n        save_btn.clicked.connect(self._on_save_clicked)\n        reset_btn.clicked.connect(self._on_reset_clicked)\n\n        self._overlay_object = overlay_object\n        # Do not create local settings widget in init phase as it's using\n        #   settings objects that must be OK to be able create this widget\n        #   - we want to show dialog if anything goes wrong\n        #   - without resetting nothing is shown\n        self._settings_widget = None\n        self._scroll_widget = scroll_widget\n        self.reset_btn = reset_btn\n        self.save_btn = save_btn\n\n    def showEvent(self, event):\n        super(LocalSettingsWindow, self).showEvent(event)\n        if self._reset_on_show:\n            self.reset()\n\n    def reset(self):\n        if self._reset_on_show:\n            self._reset_on_show = False\n\n        error_msg = None\n        try:\n            # Create settings widget if is not created yet\n            if self._settings_widget is None:\n                self._settings_widget = LocalSettingsWidget(\n                    self._scroll_widget\n                )\n                self._scroll_widget.setWidget(self._settings_widget)\n\n            value = get_local_settings()\n            self._settings_widget.update_local_settings(value)\n\n        except Exception as exc:\n            log.warning(\n                \"Failed to create local settings window\", exc_info=True\n            )\n            error_msg = str(exc)\n\n        crashed = error_msg is not None\n        # Enable/Disable save button if crashed or not\n        self.save_btn.setEnabled(not crashed)\n        # Show/Hide settings widget if crashed or not\n        if self._settings_widget:\n            self._settings_widget.setVisible(not crashed)\n\n        if not crashed:\n            return\n\n        # Show message with error\n        title = \"Something went wrong\"\n        msg = (\n            \"Bug: Loading of settings failed.\"\n            \" Please contact your project manager or OpenPype team.\"\n            \"\\n\\nError message:\\n{}\"\n        ).format(error_msg)\n\n        dialog = QtWidgets.QMessageBox(\n            QtWidgets.QMessageBox.Critical,\n            title,\n            msg,\n            QtWidgets.QMessageBox.Ok,\n            self\n        )\n        dialog.exec_()\n\n    def _on_reset_clicked(self):\n        self.reset()\n        self._overlay_object.add_message(\"Refreshed...\")\n\n    def _on_save_clicked(self):\n        value = self._settings_widget.settings_value()\n        save_local_settings(value)\n        self._overlay_object.add_message(\"Saved...\", message_type=\"success\")\n        self.reset()\n"
  },
  {
    "path": "openpype/tools/settings/resources/__init__.py",
    "content": "import os\n\n\nRESOURCES_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\ndef get_resource(*args):\n    return os.path.normpath(os.path.join(RESOURCES_DIR, *args))\n"
  },
  {
    "path": "openpype/tools/settings/settings/README.md",
    "content": "# Creating GUI schemas\n\n## Basic rules\n- configurations does not define GUI, but GUI defines configurations!\n- output is always json (yaml is not needed for anatomy templates anymore)\n- GUI schema has multiple input types, all inputs are represented by a dictionary\n- each input may have \"input modifiers\" (keys in dictionary) that are required or optional\n    - only required modifier for all input items is key `\"type\"` which says what type of item it is\n- there are special keys across all inputs\n    - `\"is_file\"` - this key is for storing openpype defaults in `openpype` repo\n        - reasons of existence: developing new schemas does not require to create defaults manually\n        - key is validated, must be once in hierarchy else it won't be possible to store openpype defaults\n    - `\"is_group\"` - define that all values under key in hierarchy will be overridden if any value is modified, this information is also stored to overrides\n        - this keys is not allowed for all inputs as they may have not reason for that\n        - key is validated, can be only once in hierarchy but is not required\n- currently there are `system configurations` and `project configurations`\n\n## Inner schema\n- GUI schemas are huge json files, to be able to split whole configuration into multiple schema there's type `schema`\n- system configuration schemas are stored in `~/tools/settings/settings/gui_schemas/system_schema/` and project configurations in `~/tools/settings/settings/gui_schemas/projects_schema/`\n- each schema name is filename of json file except extension (without \".json\")\n- if content is dictionary content will be used as `schema` else will be used as `schema_template`\n\n### schema\n- can have only key `\"children\"` which is list of strings, each string should represent another schema (order matters) string represebts name of the schema\n- will just paste schemas from other schema file in order of \"children\" list\n\n```\n{\n    \"type\": \"schema\",\n    \"name\": \"my_schema_name\"\n}\n```\n\n### schema_template\n- allows to define schema \"templates\" to not duplicate same content multiple times\n```javascript\n// EXAMPLE json file content (filename: example_template.json)\n[\n    {\n        \"__default_values__\": {\n            \"multipath_executables\": true\n        }\n    }, {\n        \"type\": \"raw-json\",\n        \"label\": \"{host_label} Environments\",\n        \"key\": \"{host_name}_environments\"\n    }, {\n        \"type\": \"path-widget\",\n        \"key\": \"{host_name}_executables\",\n        \"label\": \"{host_label} - Full paths to executables\",\n        \"multiplatform\": \"{multipath_executables}\",\n        \"multipath\": true\n    }\n]\n```\n```javascript\n// EXAMPLE usage of the template in schema\n{\n    \"type\": \"dict\",\n    \"key\": \"schema_template_examples\",\n    \"label\": \"Schema template examples\",\n    \"children\": [\n        {\n            \"type\": \"schema_template\",\n            // filename of template (example_template.json)\n            \"name\": \"example_template\",\n            \"template_data\": {\n                \"host_label\": \"Maya 2019\",\n                \"host_name\": \"maya_2019\",\n                \"multipath_executables\": false\n            }\n        }, {\n            \"type\": \"schema_template\",\n            \"name\": \"example_template\",\n            \"template_data\": {\n                \"host_label\": \"Maya 2020\",\n                \"host_name\": \"maya_2020\"\n            }\n        }\n    ]\n}\n```\n- item in schema mush contain `\"type\"` and `\"name\"` keys but it is also expected that `\"template_data\"` will be entered too\n- all items in the list, except `__default_values__`, will replace `schema_template` item in schema\n- template may contain another template or schema\n- it is expected that schema template will have unfilled fields as in example\n    - unfilled fields are allowed only in values of schema dictionary\n```javascript\n{\n    ...\n    // Allowed\n    \"key\": \"{to_fill}\"\n    ...\n    // Not allowed\n    \"{to_fill}\": \"value\"\n    ...\n}\n```\n- Unfilled fields can be also used for non string values, in that case value must contain only one key and value for fill must contain right type.\n```javascript\n{\n    ...\n    // Allowed\n    \"multiplatform\": \"{executable_multiplatform}\"\n    ...\n    // Not allowed\n    \"multiplatform\": \"{executable_multiplatform}_enhanced_string\"\n    ...\n}\n```\n- It is possible to define default values for unfilled fields to do so one of items in list must be dictionary with key `\"__default_values__\"` and value as dictionary with default key: values (as in example above).\n\n\n## Basic Dictionary inputs\n- these inputs wraps another inputs into {key: value} relation\n\n## dict\n- this is another dictionary input wrapping more inputs but visually makes them different\n- item may be used as widget (in `list` or `dict-modifiable`)\n    - in that case the only key modifier is `children` which is list of it's keys\n    - USAGE: e.g. List of dictionaries where each dictionary have same structure.\n- item may be with or without `\"label\"` if is not used as widget\n    - required keys are `\"key\"` under which will be stored\n    - without label it is just wrap item holding `\"key\"`\n        - can't have `\"is_group\"` key set to True as it breaks visual override showing\n    - if `\"label\"` is entetered there which will be shown in GUI\n        - item with label can be collapsible\n            - that can be set with key `\"collapsible\"` as `True`/`False` (Default: `True`)\n                - with key `\"collapsed\"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`)\n        - it is possible to add darker background with `\"highlight_content\"` (Default: `False`)\n            - darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color\n    - output is dictionary `{the \"key\": children values}`\n```\n# Example\n{\n    \"key\": \"applications\",\n    \"type\": \"dict\",\n    \"label\": \"Applications\",\n    \"collapsible\": true,\n    \"highlight_content\": true,\n    \"is_group\": true,\n    \"is_file\": true,\n    \"children\": [\n        ...ITEMS...\n    ]\n}\n\n# Without label\n{\n    \"type\": \"dict\",\n    \"key\": \"global\",\n    \"children\": [\n        ...ITEMS...\n    ]\n}\n\n# When used as widget\n{\n    \"type\": \"list\",\n    \"key\": \"profiles\",\n    \"label\": \"Profiles\",\n    \"object_type\": {\n        \"type\": \"dict\",\n        \"children\": [\n            {\n                \"key\": \"families\",\n                \"label\": \"Families\",\n                \"type\": \"list\",\n                \"object_type\": \"text\"\n            }, {\n                \"key\": \"hosts\",\n                \"label\": \"Hosts\",\n                \"type\": \"list\",\n                \"object_type\": \"text\"\n            }\n            ...\n        ]\n    }\n}\n```\n\n## Inputs for setting any kind of value (`Pure` inputs)\n- all these input must have defined `\"key\"` under which will be stored and `\"label\"` which will be shown next to input\n    - unless they are used in different types of inputs (later) \"as widgets\" in that case `\"key\"` and `\"label\"` are not required as there is not place where to set them\n\n### boolean\n- simple checkbox, nothing more to set\n```\n{\n    \"type\": \"boolean\",\n    \"key\": \"my_boolean_key\",\n    \"label\": \"Do you want to use OpenPype?\"\n}\n```\n\n### number\n- number input, can be used for both integer and float\n    - key `\"decimal\"` defines how many decimal places will be used, 0 is for integer input (Default: `0`)\n    - key `\"minimum\"` as minimum allowed number to enter (Default: `-99999`)\n    - key `\"maximum\"` as maximum allowed number to enter (Default: `99999`)\n```\n{\n    \"type\": \"number\",\n    \"key\": \"fps\",\n    \"label\": \"Frame rate (FPS)\"\n    \"decimal\": 2,\n    \"minimum\": 1,\n    \"maximum\": 300000\n}\n```\n\n### text\n- simple text input\n    - key `\"multiline\"` allows to enter multiple lines of text (Default: `False`)\n    - key `\"placeholder\"` allows to show text inside input when is empty (Default: `None`)\n\n```\n{\n    \"type\": \"text\",\n    \"key\": \"deadline_pool\",\n    \"label\": \"Deadline pool\"\n}\n```\n\n### path-input\n- enhanced text input\n    - does not allow to enter backslash, is auto-converted to forward slash\n    - may be added another validations, like do not allow end path with slash\n- this input is implemented to add additional features to text input\n- this is meant to be used in proxy input `path-widget`\n    - DO NOT USE this input in schema please\n\n### raw-json\n- a little bit enhanced text input for raw json\n- has validations of json format\n    - empty value is invalid value, always must be at least `{}` of `[]`\n\n```\n{\n    \"type\": \"raw-json\",\n    \"key\": \"profiles\",\n    \"label\": \"Extract Review profiles\"\n}\n```\n\n### enum\n- returns value of single on multiple items from predefined values\n- multiselection can be allowed with setting key `\"multiselection\"` to `True` (Default: `False`)\n- values are defined under value of key `\"enum_items\"` as list\n    - each item in list is simple dictionary where value is label and key is value which will be stored\n    - should be possible to enter single dictionary if order of items doesn't matter\n\n```\n{\n    \"key\": \"tags\",\n    \"label\": \"Tags\",\n    \"type\": \"enum\",\n    \"multiselection\": true,\n    \"enum_items\": [\n        {\"burnin\": \"Add burnins\"},\n        {\"ftrackreview\": \"Add to Ftrack\"},\n        {\"delete\": \"Delete output\"},\n        {\"slate-frame\": \"Add slate frame\"},\n        {\"no-hnadles\": \"Skip handle frames\"}\n    ]\n}\n```\n\n## Inputs for setting value using Pure inputs\n- these inputs also have required `\"key\"`\n- attribute `\"label\"` is required in few conditions\n    - when item is marked `as_group` or when `use_label_wrap`\n- they use Pure inputs \"as widgets\"\n\n### list\n- output is list\n- items can be added and removed\n- items in list must be the same type\n- to wrap item in collapsible widget with label on top set `use_label_wrap` to `True`\n    - when this is used `collapsible` and `collapsed` can be set (same as `dict` item does)\n- type of items is defined with key `\"object_type\"`\n- there are 2 possible ways how to set the type:\n    1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `\"type\"` (example below)\n    2.) item type name as string without modifiers (e.g. `text`)\n\n1.) with item modifiers\n```\n{\n    \"type\": \"list\",\n    \"key\": \"exclude_ports\",\n    \"label\": \"Exclude ports\",\n    \"object_type\": {\n        \"type\": \"number\", # number item type\n        \"minimum\": 1, # minimum modifier\n        \"maximum\": 65535 # maximum modifier\n    }\n}\n```\n\n2.) without modifiers\n```\n{\n    \"type\": \"list\",\n    \"key\": \"exclude_ports\",\n    \"label\": \"Exclude ports\",\n    \"object_type\": \"text\"\n}\n```\n\n### dict-modifiable\n- one of dictionary inputs, this is only used as value input\n- items in this input can be removed and added same way as in `list` input\n- value items in dictionary must be the same type\n- type of items is defined with key `\"object_type\"`\n- required keys may be defined under `\"required_keys\"`\n    - required keys must be defined as a list (e.g. `[\"key_1\"]`) and are moved to the top\n    - these keys can't be removed or edited (it is possible to edit label if item is collapsible)\n- there are 2 possible ways how to set the type:\n    1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `\"type\"` (example below)\n    2.) item type name as string without modifiers (e.g. `text`)\n- this input can be collapsible\n    - that can be set with key `\"collapsible\"` as `True`/`False` (Default: `True`)\n        - with key `\"collapsed\"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`)\n\n1.) with item modifiers\n```\n{\n    \"type\": \"dict-modifiable\",\n    \"object_type\": {\n        \"type\": \"number\",\n        \"minimum\": 0,\n        \"maximum\": 300\n    },\n    \"is_group\": true,\n    \"key\": \"templates_mapping\",\n    \"label\": \"Deadline - Templates mapping\",\n    \"is_file\": true\n}\n```\n\n2.) without modifiers\n```\n{\n    \"type\": \"dict-modifiable\",\n    \"object_type\": \"text\",\n    \"is_group\": true,\n    \"key\": \"templates_mapping\",\n    \"label\": \"Deadline - Templates mapping\",\n    \"is_file\": true\n}\n```\n\n### path-widget\n- input for paths, use `path-input` internally\n- has 2 input modifiers `\"multiplatform\"` and `\"multipath\"`\n    - `\"multiplatform\"` - adds `\"windows\"`, `\"linux\"` and `\"darwin\"` path inputs result is dictionary\n    - `\"multipath\"` - it is possible to enter multiple paths\n    - if both are enabled result is dictionary with lists\n\n```\n{\n    \"type\": \"path-widget\",\n    \"key\": \"ffmpeg_path\",\n    \"label\": \"FFmpeg path\",\n    \"multiplatform\": true,\n    \"multipath\": true\n}\n```\n\n### list-strict\n- input for strict number of items in list\n- each child item can be different type with different possible modifiers\n- it is possible to display them in horizontal or vertical layout\n    - key `\"horizontal\"` as `True`/`False` (Default: `True`)\n- each child may have defined `\"label\"` which is shown next to input\n    - label does not reflect modifications or overrides (TODO)\n- children item are defined under key `\"object_types\"` which is list of dictionaries\n    - key `\"children\"` is not used because is used for hierarchy validations in schema\n- USAGE: For colors, transformations, etc. Custom number and different modifiers\n  give ability to define if color is HUE or RGB, 0-255, 0-1, 0-100 etc.\n\n```\n{\n    \"type\": \"list-strict\",\n    \"key\": \"color\",\n    \"label\": \"Color\",\n    \"object_types\": [\n        {\n            \"label\": \"Red\",\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"maximum\": 255,\n            \"decimal\": 0\n        }, {\n            \"label\": \"Green\",\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"maximum\": 255,\n            \"decimal\": 0\n        }, {\n            \"label\": \"Blue\",\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"maximum\": 255,\n            \"decimal\": 0\n        }, {\n            \"label\": \"Alpha\",\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"maximum\": 1,\n            \"decimal\": 6\n        }\n    ]\n}\n```\n\n\n## Noninteractive widgets\n- have nothing to do with data\n\n### label\n- add label with note or explanations\n- it is possible to use html tags inside the label\n\n```\n{\n    \"type\": \"label\",\n    \"label\": \"<span style=\\\"color:#FF0000\\\";>RED LABEL:</span> Normal label\"\n}\n```\n\n### splitter\n- visual splitter of items (more divider than splitter)\n\n```\n{\n    \"type\": \"splitter\"\n}\n```\n\n## Proxy wrappers\n- should wraps multiple inputs only visually\n- these does not have `\"key\"` key and do not allow to have `\"is_file\"` or `\"is_group\"` modifiers enabled\n- can't be used as widget (first item in e.g. `list`, `dict-modifiable`, etc.)\n\n### form\n- wraps inputs into form look layout\n- should be used only for Pure inputs\n\n```\n{\n    \"type\": \"dict-form\",\n    \"children\": [\n        {\n            \"type\": \"text\",\n            \"key\": \"deadline_department\",\n            \"label\": \"Deadline apartment\"\n        }, {\n            \"type\": \"number\",\n            \"key\": \"deadline_priority\",\n            \"label\": \"Deadline priority\"\n        }, {\n           ...\n        }\n    ]\n}\n```\n\n\n### collapsible-wrap\n- wraps inputs into collapsible widget\n    - looks like `dict` but does not hold `\"key\"`\n- should be used only for Pure inputs\n\n```\n{\n    \"type\": \"collapsible-wrap\",\n    \"label\": \"Collapsible example\"\n    \"children\": [\n        {\n            \"type\": \"text\",\n            \"key\": \"_example_input_collapsible\",\n            \"label\": \"Example input in collapsible wrapper\"\n        }, {\n           ...\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/tools/settings/settings/__init__.py",
    "content": "from .window import MainWidget\nfrom .widgets import ProjectListWidget\n\n\n__all__ = (\n    \"MainWidget\",\n    \"ProjectListWidget\"\n)\n"
  },
  {
    "path": "openpype/tools/settings/settings/base.py",
    "content": "import os\nimport sys\nimport json\nimport traceback\nimport functools\nimport datetime\n\nfrom qtpy import QtWidgets, QtGui, QtCore\n\nfrom openpype.settings.entities import ProjectSettings\nfrom openpype.tools.settings import CHILD_OFFSET\n\nfrom .widgets import ExpandingWidget\nfrom .lib import create_deffered_value_change_timer\nfrom .constants import (\n    DEFAULT_PROJECT_LABEL,\n    SETTINGS_PATH_KEY,\n    ROOT_KEY,\n    VALUE_KEY,\n    SAVE_TIME_KEY,\n    PROJECT_NAME_KEY,\n)\n\n_MENU_SEPARATOR_REQ = object()\n\n\nclass ExtractHelper:\n    _last_save_dir = os.path.expanduser(\"~\")\n\n    @classmethod\n    def get_last_save_dir(cls):\n        return cls._last_save_dir\n\n    @classmethod\n    def set_last_save_dir(cls, save_dir):\n        cls._last_save_dir = save_dir\n\n    @classmethod\n    def ask_for_save_filepath(cls, parent):\n        dialog = QtWidgets.QFileDialog(\n            parent,\n            \"Save settings values\",\n            cls.get_last_save_dir(),\n            \"Values (*.json)\"\n        )\n        # dialog.setOption(dialog.DontUseNativeDialog)\n        dialog.setAcceptMode(dialog.AcceptSave)\n        if dialog.exec() != dialog.Accepted:\n            return\n\n        selected_urls = dialog.selectedUrls()\n        if not selected_urls:\n            return\n\n        filepath = selected_urls[0].toLocalFile()\n        if not filepath:\n            return\n\n        if not filepath.lower().endswith(\".json\"):\n            filepath += \".json\"\n        return filepath\n\n    @classmethod\n    def extract_settings_to_json(cls, filepath, settings_data, project_name):\n        now = datetime.datetime.now()\n        settings_data[SAVE_TIME_KEY] = now.strftime(\"%Y-%m-%d %H:%M:%S\")\n        if project_name != 0:\n            settings_data[PROJECT_NAME_KEY] = project_name\n\n        with open(filepath, \"w\") as stream:\n            json.dump(settings_data, stream, indent=4)\n\n        new_dir = os.path.dirname(filepath)\n        cls.set_last_save_dir(new_dir)\n\n\nclass BaseWidget(QtWidgets.QWidget):\n    allow_actions = True\n\n    def __init__(self, category_widget, entity, entity_widget):\n        self.category_widget = category_widget\n        self.entity = entity\n        self.entity_widget = entity_widget\n\n        self.ignore_input_changes = entity_widget.ignore_input_changes\n\n        self._is_invalid = False\n        self._style_state = None\n\n        super(BaseWidget, self).__init__(entity_widget.content_widget)\n        if not self.entity.gui_type:\n            self.entity.on_change_callbacks.append(self._on_entity_change)\n\n        if self.entity.tooltip:\n            self.setToolTip(self.entity.tooltip)\n\n        self.label_widget = None\n        self.create_ui()\n\n    @staticmethod\n    def set_style_property(obj, property_name, property_value):\n        \"\"\"Change QWidget property and polish it's style.\"\"\"\n        if obj.property(property_name) == property_value:\n            return\n\n        obj.setProperty(property_name, property_value)\n        obj.style().polish(obj)\n\n    def scroll_to(self, widget):\n        self.category_widget.scroll_to(widget)\n\n    def set_path(self, path):\n        self.category_widget.set_path(path)\n\n    def set_focus(self, scroll_to=False):\n        \"\"\"Set focus of a widget.\n\n        Args:\n            scroll_to(bool): Also scroll to widget in category widget.\n        \"\"\"\n        if scroll_to:\n            self.scroll_to(self)\n        self.setFocus()\n\n    def make_sure_is_visible(self, path, scroll_to):\n        \"\"\"Make a widget of entity visible by it's path.\n\n        Args:\n            path(str): Path to entity.\n            scroll_to(bool): Should be scrolled to entity.\n\n        Returns:\n            bool: Entity with path was found.\n        \"\"\"\n        raise NotImplementedError(\n            \"{} not implemented `make_sure_is_visible`\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def trigger_hierarchical_style_update(self):\n        self.category_widget.hierarchical_style_update()\n\n    def create_ui_for_entity(self, *args, **kwargs):\n        return self.category_widget.create_ui_for_entity(*args, **kwargs)\n\n    @property\n    def is_invalid(self):\n        return self._is_invalid\n\n    @staticmethod\n    def get_style_state(\n        is_invalid, is_modified, has_project_override, has_studio_override\n    ):\n        \"\"\"Return stylesheet state by intered booleans.\"\"\"\n        if is_invalid:\n            return \"invalid\"\n        if is_modified:\n            return \"modified\"\n        if has_project_override:\n            return \"overridden\"\n        if has_studio_override:\n            return \"studio\"\n        return \"\"\n\n    def hierarchical_style_update(self):\n        raise NotImplementedError(\n            \"{} not implemented `hierarchical_style_update`\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def get_invalid(self):\n        raise NotImplementedError(\n            \"{} not implemented `get_invalid`\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def _on_entity_change(self):\n        \"\"\"Not yet used.\"\"\"\n        print(\"{}: Warning missing `_on_entity_change` implementation\".format(\n            self.__class__.__name__\n        ))\n\n    def _discard_changes_action(self, menu, actions_mapping):\n        # TODO use better condition as unsaved changes may be caused due to\n        #   changes in schema.\n        if not self.entity.can_trigger_discard_changes:\n            return\n\n        def discard_changes():\n            with self.category_widget.working_state_context():\n                self.ignore_input_changes.set_ignore(True)\n                self.entity.discard_changes()\n                self.ignore_input_changes.set_ignore(False)\n\n        action = QtWidgets.QAction(\"Discard changes\")\n        actions_mapping[action] = discard_changes\n        menu.addAction(action)\n\n    def _add_to_studio_default(self, menu, actions_mapping):\n        \"\"\"Set values as studio overrides.\"\"\"\n        # Skip if not in studio overrides\n        if not self.entity.can_trigger_add_to_studio_default:\n            return\n\n        def add_to_studio_default():\n            with self.category_widget.working_state_context():\n                self.entity.add_to_studio_default()\n        action = QtWidgets.QAction(\"Add to studio default\")\n        actions_mapping[action] = add_to_studio_default\n        menu.addAction(action)\n\n    def _remove_from_studio_default_action(self, menu, actions_mapping):\n        if not self.entity.can_trigger_remove_from_studio_default:\n            return\n\n        def remove_from_studio_default():\n            with self.category_widget.working_state_context():\n                self.ignore_input_changes.set_ignore(True)\n                self.entity.remove_from_studio_default()\n                self.ignore_input_changes.set_ignore(False)\n        action = QtWidgets.QAction(\"Remove from studio default\")\n        actions_mapping[action] = remove_from_studio_default\n        menu.addAction(action)\n\n    def _add_to_project_override_action(self, menu, actions_mapping):\n        if not self.entity.can_trigger_add_to_project_override:\n            return\n\n        def add_to_project_override():\n            with self.category_widget.working_state_context():\n                self.entity.add_to_project_override\n\n        action = QtWidgets.QAction(\"Add to project override\")\n        actions_mapping[action] = add_to_project_override\n        menu.addAction(action)\n\n    def _remove_from_project_override_action(self, menu, actions_mapping):\n        if not self.entity.can_trigger_remove_from_project_override:\n            return\n\n        def remove_from_project_override():\n            with self.category_widget.working_state_context():\n                self.ignore_input_changes.set_ignore(True)\n                self.entity.remove_from_project_override()\n                self.ignore_input_changes.set_ignore(False)\n\n        action = QtWidgets.QAction(\"Remove from project override\")\n        actions_mapping[action] = remove_from_project_override\n        menu.addAction(action)\n\n    def _get_mime_data_from_entity(self):\n        if self.entity.is_dynamic_item or self.entity.is_in_dynamic_item:\n            entity_path = None\n        else:\n            entity_path = \"/\".join(\n                [self.entity.root_key, self.entity.path]\n            )\n\n        value = self.entity.value\n\n        # Copy for settings tool\n        return {\n            VALUE_KEY: value,\n            ROOT_KEY: self.entity.root_key,\n            SETTINGS_PATH_KEY: entity_path\n        }\n\n    def _copy_value_actions(self, menu):\n        def copy_value():\n            mime_data = QtCore.QMimeData()\n\n            # Copy for settings tool\n            settings_data = self._get_mime_data_from_entity()\n            settings_encoded_data = QtCore.QByteArray()\n            settings_stream = QtCore.QDataStream(\n                settings_encoded_data, QtCore.QIODevice.WriteOnly\n            )\n            settings_stream.writeQString(json.dumps(settings_data))\n            mime_data.setData(\n                \"application/copy_settings_value\", settings_encoded_data\n            )\n\n            # Copy as json\n            value = settings_data[VALUE_KEY]\n            json_encoded_data = None\n            if isinstance(value, (dict, list)):\n                json_encoded_data = QtCore.QByteArray()\n                json_stream = QtCore.QDataStream(\n                    json_encoded_data, QtCore.QIODevice.WriteOnly\n                )\n                json_stream.writeQString(json.dumps(value))\n\n                mime_data.setData(\"application/json\", json_encoded_data)\n\n            # Copy as text\n            if json_encoded_data is None:\n                # Store value as string\n                mime_data.setText(str(value))\n            else:\n                # Store data as json string\n                mime_data.setText(json.dumps(value, indent=4))\n\n            QtWidgets.QApplication.clipboard().setMimeData(mime_data)\n\n        action = QtWidgets.QAction(\"Copy\", menu)\n        return [(action, copy_value)]\n\n    def _extract_to_file(self):\n        filepath = ExtractHelper.ask_for_save_filepath(self)\n        if not filepath:\n            return\n\n        settings_data = self._get_mime_data_from_entity()\n        project_name = 0\n        if hasattr(self.category_widget, \"project_name\"):\n            project_name = self.category_widget.project_name\n\n        ExtractHelper.extract_settings_to_json(\n            filepath, settings_data, project_name\n        )\n\n    def _extract_value_to_file_actions(self, menu):\n        extract_action = QtWidgets.QAction(\"Extract to file\", menu)\n        return [\n            _MENU_SEPARATOR_REQ,\n            (extract_action, self._extract_to_file)\n        ]\n\n    def _parse_source_data_for_paste(self, data):\n        settings_path = None\n        root_key = None\n        if isinstance(data, dict):\n            data.pop(SAVE_TIME_KEY, None)\n            data.pop(PROJECT_NAME_KEY, None)\n            settings_path = data.pop(SETTINGS_PATH_KEY, settings_path)\n            root_key = data.pop(ROOT_KEY, root_key)\n            data = data.pop(VALUE_KEY, data)\n\n        return {\n            VALUE_KEY: data,\n            SETTINGS_PATH_KEY: settings_path,\n            ROOT_KEY: root_key\n        }\n\n    def _get_value_from_clipboard(self):\n        clipboard = QtWidgets.QApplication.clipboard()\n        mime_data = clipboard.mimeData()\n        app_value = mime_data.data(\"application/copy_settings_value\")\n        if app_value:\n            settings_stream = QtCore.QDataStream(\n                app_value, QtCore.QIODevice.ReadOnly\n            )\n            mime_data_value_str = settings_stream.readQString()\n            return json.loads(mime_data_value_str)\n\n        if mime_data.hasUrls():\n            for url in mime_data.urls():\n                local_file = url.toLocalFile()\n                try:\n                    with open(local_file, \"r\") as stream:\n                        value = json.load(stream)\n                except Exception:\n                    continue\n                if value:\n                    return self._parse_source_data_for_paste(value)\n\n        if mime_data.hasText():\n            text = mime_data.text()\n            try:\n                value = json.loads(text)\n            except Exception:\n                try:\n                    value = self.entity.convert_to_valid_type(text)\n                except Exception:\n                    return None\n            return self._parse_source_data_for_paste(value)\n\n    def _paste_value_actions(self, menu):\n        output = []\n        # Allow paste of value only if were copied from this UI\n        mime_data_value = self._get_value_from_clipboard()\n        # Skip if there is nothing to do\n        if not mime_data_value:\n            return output\n\n        value = mime_data_value[VALUE_KEY]\n        path = mime_data_value[SETTINGS_PATH_KEY]\n        root_key = mime_data_value[ROOT_KEY]\n\n        # Try to find matching entity to be able paste values to same spot\n        # - entity can't by dynamic or in dynamic item\n        # - must be in same root entity as source copy\n        #       Can't copy system settings <-> project settings\n        matching_entity = None\n        if path and root_key == self.entity.root_key:\n            try:\n                matching_entity = self.entity.get_entity_from_path(path)\n            except Exception:\n                pass\n\n        def _set_entity_value(_entity, _value):\n            try:\n                _entity.set(_value)\n            except Exception:\n                dialog = QtWidgets.QMessageBox(self)\n                dialog.setWindowTitle(\"Value does not match settings schema\")\n                dialog.setIcon(QtWidgets.QMessageBox.Warning)\n                dialog.setText((\n                    \"Pasted value does not seem to match schema of destination\"\n                    \" settings entity.\"\n                ))\n                dialog.exec_()\n\n        # Simple paste value method\n        def paste_value():\n            with self.category_widget.working_state_context():\n                _set_entity_value(self.entity, value)\n\n        action = QtWidgets.QAction(\"Paste\", menu)\n        output.append((action, paste_value))\n\n        # Paste value to matching entity\n        def paste_value_to_path():\n            with self.category_widget.working_state_context():\n                _set_entity_value(matching_entity, value)\n\n        if matching_entity is not None:\n            action = QtWidgets.QAction(\"Paste to same place\", menu)\n            output.append((action, paste_value_to_path))\n\n        return output\n\n    def _apply_values_from_project_action(self, menu, actions_mapping):\n        for attr_name in (\"project_name\", \"get_project_names\"):\n            if not hasattr(self.category_widget, attr_name):\n                return\n\n        if self.entity.is_dynamic_item or self.entity.is_in_dynamic_item:\n            return\n\n        current_project_name = self.category_widget.project_name\n        project_names = []\n        for project_name in self.category_widget.get_project_names():\n            if project_name != current_project_name:\n                project_names.append(project_name)\n\n        if not project_names:\n            return\n\n        submenu = QtWidgets.QMenu(\"Apply values from\", menu)\n\n        for project_name in project_names:\n            if project_name is None:\n                project_name = DEFAULT_PROJECT_LABEL\n\n            action = QtWidgets.QAction(project_name)\n            submenu.addAction(action)\n            actions_mapping[action] = functools.partial(\n                self._apply_values_from_project,\n                project_name\n            )\n        menu.addMenu(submenu)\n\n    def _apply_values_from_project(self, project_name):\n        with self.category_widget.working_state_context():\n            try:\n                path_keys = [\n                    item\n                    for item in self.entity.path.split(\"/\")\n                    if item\n                ]\n                entity = ProjectSettings(project_name)\n                for key in path_keys:\n                    entity = entity[key]\n                self.entity.set(entity.value)\n\n            except Exception:\n                if project_name is None:\n                    project_name = DEFAULT_PROJECT_LABEL\n\n                # TODO better message\n                title = \"Applying values failed\"\n                msg = \"Applying values from project \\\"{}\\\" failed.\".format(\n                    project_name\n                )\n                detail_msg = \"\".join(\n                    traceback.format_exception(*sys.exc_info())\n                )\n                dialog = QtWidgets.QMessageBox(self)\n                dialog.setWindowTitle(title)\n                dialog.setIcon(QtWidgets.QMessageBox.Warning)\n                dialog.setText(msg)\n                dialog.setDetailedText(detail_msg)\n                dialog.exec_()\n\n    def show_actions_menu(self, event=None):\n        if event and event.button() != QtCore.Qt.RightButton:\n            return\n\n        if not self.allow_actions:\n            if event:\n                return self.mouseReleaseEvent(event)\n            return\n\n        menu = QtWidgets.QMenu(self)\n\n        actions_mapping = {}\n\n        self._discard_changes_action(menu, actions_mapping)\n        self._add_to_studio_default(menu, actions_mapping)\n        self._remove_from_studio_default_action(menu, actions_mapping)\n        self._add_to_project_override_action(menu, actions_mapping)\n        self._remove_from_project_override_action(menu, actions_mapping)\n        self._apply_values_from_project_action(menu, actions_mapping)\n\n        ui_actions = []\n        ui_actions.extend(self._copy_value_actions(menu))\n        ui_actions.extend(self._paste_value_actions(menu))\n        if ui_actions:\n            ui_actions.insert(0, _MENU_SEPARATOR_REQ)\n\n        ui_actions.extend(self._extract_value_to_file_actions(menu))\n\n        for item in ui_actions:\n            if item is _MENU_SEPARATOR_REQ:\n                if len(menu.actions()) > 0:\n                    menu.addSeparator()\n                continue\n\n            action, callback = item\n            menu.addAction(action)\n            actions_mapping[action] = callback\n\n        if not actions_mapping:\n            action = QtWidgets.QAction(\"< No action >\")\n            actions_mapping[action] = None\n            menu.addAction(action)\n\n        result = menu.exec_(QtGui.QCursor.pos())\n        if result:\n            to_run = actions_mapping[result]\n            if to_run:\n                to_run()\n\n    def focused_in(self):\n        if self.entity is not None:\n            self.set_path(self.entity.path)\n\n    def mouseReleaseEvent(self, event):\n        if self.allow_actions and event.button() == QtCore.Qt.RightButton:\n            return self.show_actions_menu()\n\n        focused_in = False\n        if event.button() == QtCore.Qt.LeftButton:\n            focused_in = True\n            self.focused_in()\n\n        result = super(BaseWidget, self).mouseReleaseEvent(event)\n        if focused_in and not event.isAccepted():\n            event.accept()\n        return result\n\n\nclass InputWidget(BaseWidget):\n    def __init__(self, *args, **kwargs):\n        super(InputWidget, self).__init__(*args, **kwargs)\n\n        # Input widgets have always timer available (but may not be used).\n        self._value_change_timer = create_deffered_value_change_timer(\n            self._on_value_change_timer\n        )\n\n    def start_value_timer(self):\n        self._value_change_timer.start()\n\n    def _on_value_change_timer(self):\n        pass\n\n    def create_ui(self):\n        if self.entity.use_label_wrap:\n            label = None\n            self._create_label_wrap_ui()\n        else:\n            label = self.entity.label\n            self.label_widget = None\n            self.body_widget = None\n            self.content_widget = self\n            self.content_layout = self._create_layout(self)\n\n        self._add_inputs_to_layout()\n\n        self.entity_widget.add_widget_to_layout(self, label)\n\n    def _create_label_wrap_ui(self):\n        content_widget = QtWidgets.QWidget(self)\n        content_widget.setObjectName(\"ContentWidget\")\n\n        content_widget.setProperty(\"content_state\", \"\")\n        content_layout_margins = (CHILD_OFFSET, 5, 0, 0)\n\n        body_widget = ExpandingWidget(self.entity.label, self)\n        label_widget = body_widget.label_widget\n        body_widget.set_content_widget(content_widget)\n\n        content_layout = self._create_layout(content_widget)\n        content_layout.setContentsMargins(*content_layout_margins)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n        main_layout.addWidget(body_widget)\n\n        self.label_widget = label_widget\n        self.body_widget = body_widget\n        self.content_widget = content_widget\n        self.content_layout = content_layout\n\n    def _create_layout(self, parent_widget):\n        layout = QtWidgets.QHBoxLayout(parent_widget)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(5)\n        return layout\n\n    def _add_inputs_to_layout(self):\n        raise NotImplementedError(\n            \"Method `_add_inputs_to_layout` not implemented {}\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def make_sure_is_visible(self, path, scroll_to):\n        if path:\n            entity_path = self.entity.path\n            if entity_path == path:\n                self.set_focus(scroll_to)\n                return True\n        return False\n\n    def update_style(self):\n        has_unsaved_changes = self.entity.has_unsaved_changes\n        if not has_unsaved_changes and self.entity.group_item:\n            has_unsaved_changes = self.entity.group_item.has_unsaved_changes\n        style_state = self.get_style_state(\n            self.is_invalid,\n            has_unsaved_changes,\n            self.entity.has_project_override,\n            self.entity.has_studio_override\n        )\n        if self._style_state == style_state:\n            return\n\n        self._style_state = style_state\n\n        self.input_field.setProperty(\"input-state\", style_state)\n        self.input_field.style().polish(self.input_field)\n        if self.label_widget:\n            self.label_widget.setProperty(\"state\", style_state)\n            self.label_widget.style().polish(self.label_widget)\n\n        if self.body_widget:\n            if style_state:\n                child_style_state = \"child-{}\".format(style_state)\n            else:\n                child_style_state = \"\"\n\n            self.body_widget.side_line_widget.setProperty(\n                \"state\", child_style_state\n            )\n            self.body_widget.side_line_widget.style().polish(\n                self.body_widget.side_line_widget\n            )\n\n    @property\n    def child_invalid(self):\n        return self.is_invalid\n\n    def hierarchical_style_update(self):\n        self.update_style()\n\n    def get_invalid(self):\n        invalid = []\n        if self.is_invalid:\n            invalid.append(self)\n        return invalid\n\n\nclass GUIWidget(BaseWidget):\n    allow_actions = False\n    separator_height = 2\n    child_invalid = False\n\n    def create_ui(self):\n        entity_type = self.entity[\"type\"]\n        if entity_type == \"label\":\n            self._create_label_ui()\n        elif entity_type in (\"separator\", \"splitter\"):\n            self._create_separator_ui()\n        else:\n            raise KeyError(\"Unknown GUI type {}\".format(entity_type))\n\n        self.entity_widget.add_widget_to_layout(self)\n\n    def _create_label_ui(self):\n        label = self.entity[\"label\"]\n        word_wrap = self.entity.schema_data.get(\"word_wrap\", False)\n        label_widget = QtWidgets.QLabel(label, self)\n        label_widget.setWordWrap(word_wrap)\n        label_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)\n        label_widget.setObjectName(\"SettingsLabel\")\n        label_widget.linkActivated.connect(self._on_link_activate)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 5, 0, 5)\n        layout.addWidget(label_widget)\n\n    def _create_separator_ui(self):\n        splitter_item = QtWidgets.QWidget(self)\n        splitter_item.setObjectName(\"Separator\")\n        splitter_item.setMinimumHeight(self.separator_height)\n        splitter_item.setMaximumHeight(self.separator_height)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(5, 5, 5, 5)\n        layout.addWidget(splitter_item)\n\n    def _on_link_activate(self, url):\n        if not url.startswith(\"settings://\"):\n            QtGui.QDesktopServices.openUrl(url)\n            return\n\n        path = url.replace(\"settings://\", \"\")\n        self.category_widget.go_to_fullpath(path)\n\n    def set_entity_value(self):\n        pass\n\n    def hierarchical_style_update(self):\n        pass\n\n    def make_sure_is_visible(self, *args, **kwargs):\n        return False\n\n    def focused_in(self):\n        pass\n\n    def set_path(self, *args, **kwargs):\n        pass\n\n    def get_invalid(self):\n        return []\n\n\nclass MockUpWidget(BaseWidget):\n    allow_actions = False\n    child_invalid = False\n\n    def create_ui(self):\n        label = \"Mockup widget for entity {}\".format(self.entity.path)\n        label_widget = QtWidgets.QLabel(label, self)\n        label_widget.setObjectName(\"SettingsLabel\")\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 5, 0, 5)\n        layout.addWidget(label_widget)\n        self.entity_widget.add_widget_to_layout(self)\n\n    def set_entity_value(self):\n        return\n\n    def hierarchical_style_update(self):\n        pass\n\n    def get_invalid(self):\n        return []\n"
  },
  {
    "path": "openpype/tools/settings/settings/breadcrumbs_widget.py",
    "content": "from qtpy import QtWidgets, QtGui, QtCore\n\nPREFIX_ROLE = QtCore.Qt.UserRole + 1\nLAST_SEGMENT_ROLE = QtCore.Qt.UserRole + 2\n\n\nclass BreadcrumbItem(QtGui.QStandardItem):\n    def __init__(self, *args, **kwargs):\n        self._display_value = None\n        self._edit_value = None\n        super(BreadcrumbItem, self).__init__(*args, **kwargs)\n\n    def data(self, role=None):\n        if role == QtCore.Qt.DisplayRole:\n            return self._display_value\n\n        if role == QtCore.Qt.EditRole:\n            return self._edit_value\n\n        if role is None:\n            args = tuple()\n        else:\n            args = (role, )\n        return super(BreadcrumbItem, self).data(*args)\n\n    def setData(self, value, role):\n        if role == QtCore.Qt.DisplayRole:\n            self._display_value = value\n            return True\n\n        if role == QtCore.Qt.EditRole:\n            self._edit_value = value\n            return True\n\n        if role is None:\n            args = (value, )\n        else:\n            args = (value, role)\n        return super(BreadcrumbItem, self).setData(*args)\n\n\nclass BreadcrumbsModel(QtGui.QStandardItemModel):\n    def __init__(self):\n        super(BreadcrumbsModel, self).__init__()\n        self.current_path = \"\"\n\n        self.reset()\n\n    def reset(self):\n        return\n\n\nclass SettingsBreadcrumbs(BreadcrumbsModel):\n    def __init__(self):\n        self.entity = None\n\n        self.entities_by_path = {}\n        self.dynamic_paths = set()\n\n        super(SettingsBreadcrumbs, self).__init__()\n\n    def set_entity(self, entity):\n        self.entities_by_path = {}\n        self.dynamic_paths = set()\n        self.entity = entity\n        self.reset()\n\n    def has_children(self, path):\n        for key in self.entities_by_path.keys():\n            if key.startswith(path):\n                return True\n        return False\n\n    def get_valid_path(self, path):\n        if not path:\n            return \"\"\n\n        path_items = path.split(\"/\")\n        new_path_items = []\n        entity = self.entity\n        for item in path_items:\n            if not entity.has_child_with_key(item):\n                break\n\n            new_path_items.append(item)\n            entity = entity[item]\n\n        return \"/\".join(new_path_items)\n\n    def is_valid_path(self, path):\n        if not path:\n            return True\n\n        path_items = path.split(\"/\")\n\n        entity = self.entity\n        for item in path_items:\n            if not entity.has_child_with_key(item):\n                return False\n\n            entity = entity[item]\n\n        return True\n\n\nclass SystemSettingsBreadcrumbs(SettingsBreadcrumbs):\n    def reset(self):\n        root_item = self.invisibleRootItem()\n        rows = root_item.rowCount()\n        if rows > 0:\n            root_item.removeRows(0, rows)\n\n        if self.entity is None:\n            return\n\n        entities_by_path = self.entity.collect_static_entities_by_path()\n        self.entities_by_path = entities_by_path\n        items = []\n        for path in entities_by_path.keys():\n            if not path:\n                continue\n            path_items = path.split(\"/\")\n            value = path\n            label = path_items.pop(-1)\n            prefix = \"/\".join(path_items)\n            if prefix:\n                prefix += \"/\"\n\n            item = QtGui.QStandardItem(value)\n            item.setData(label, LAST_SEGMENT_ROLE)\n            item.setData(prefix, PREFIX_ROLE)\n\n            items.append(item)\n\n        root_item.appendRows(items)\n\n\nclass ProjectSettingsBreadcrumbs(SettingsBreadcrumbs):\n    def reset(self):\n        root_item = self.invisibleRootItem()\n        rows = root_item.rowCount()\n        if rows > 0:\n            root_item.removeRows(0, rows)\n\n        if self.entity is None:\n            return\n\n        entities_by_path = self.entity.collect_static_entities_by_path()\n        self.entities_by_path = entities_by_path\n        items = []\n        for path in entities_by_path.keys():\n            if not path:\n                continue\n            path_items = path.split(\"/\")\n            value = path\n            label = path_items.pop(-1)\n            prefix = \"/\".join(path_items)\n            if prefix:\n                prefix += \"/\"\n\n            item = QtGui.QStandardItem(value)\n            item.setData(label, LAST_SEGMENT_ROLE)\n            item.setData(prefix, PREFIX_ROLE)\n\n            items.append(item)\n\n        root_item.appendRows(items)\n\n\nclass BreadcrumbsProxy(QtCore.QSortFilterProxyModel):\n    def __init__(self, *args, **kwargs):\n        super(BreadcrumbsProxy, self).__init__(*args, **kwargs)\n\n        self._current_path = \"\"\n\n    def set_path_prefix(self, prefix):\n        path = prefix\n        if not prefix.endswith(\"/\"):\n            path_items = path.split(\"/\")\n            if len(path_items) == 1:\n                path = \"\"\n            else:\n                path_items.pop(-1)\n                path = \"/\".join(path_items) + \"/\"\n\n        if path == self._current_path:\n            return\n\n        self._current_path = prefix\n\n        self.invalidateFilter()\n\n    def filterAcceptsRow(self, row, parent):\n        index = self.sourceModel().index(row, 0, parent)\n        prefix_path = index.data(PREFIX_ROLE)\n        return prefix_path == self._current_path\n\n\nclass BreadcrumbsHintMenu(QtWidgets.QMenu):\n    def __init__(self, model, path_prefix, parent):\n        super(BreadcrumbsHintMenu, self).__init__(parent)\n\n        self._path_prefix = path_prefix\n        self._model = model\n\n    def showEvent(self, event):\n        self.clear()\n\n        self._model.set_path_prefix(self._path_prefix)\n\n        row_count = self._model.rowCount()\n        if row_count == 0:\n            action = self.addAction(\"* Nothing\")\n            action.setData(\".\")\n        else:\n            for row in range(self._model.rowCount()):\n                index = self._model.index(row, 0)\n                label = index.data(LAST_SEGMENT_ROLE)\n                value = index.data(QtCore.Qt.EditRole)\n                action = self.addAction(label)\n                action.setData(value)\n\n        super(BreadcrumbsHintMenu, self).showEvent(event)\n\n\nclass ClickableWidget(QtWidgets.QWidget):\n    clicked = QtCore.Signal()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self.clicked.emit()\n        super(ClickableWidget, self).mouseReleaseEvent(event)\n\n\nclass BreadcrumbsPathInput(QtWidgets.QLineEdit):\n    cancelled = QtCore.Signal()\n    confirmed = QtCore.Signal()\n\n    def __init__(self, model, proxy_model, parent):\n        super(BreadcrumbsPathInput, self).__init__(parent)\n\n        self.setObjectName(\"BreadcrumbsPathInput\")\n\n        self.setFrame(False)\n\n        completer = QtWidgets.QCompleter(self)\n        completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)\n        completer.setModel(proxy_model)\n\n        popup = completer.popup()\n        popup.setUniformItemSizes(True)\n        popup.setLayoutMode(QtWidgets.QListView.Batched)\n\n        self.setCompleter(completer)\n\n        completer.activated.connect(self._on_completer_activated)\n        self.textEdited.connect(self._on_text_change)\n\n        self._completer = completer\n        self._model = model\n        self._proxy_model = proxy_model\n\n        self._context_menu_visible = False\n\n    def set_model(self, model):\n        self._model = model\n\n    def event(self, event):\n        if (\n            event.type() == QtCore.QEvent.KeyPress\n            and event.key() == QtCore.Qt.Key_Tab\n        ):\n            if self._model:\n                find_value = self.text() + \"/\"\n                if self._model.has_children(find_value):\n                    self.insert(\"/\")\n                else:\n                    self._completer.popup().hide()\n                event.accept()\n                return True\n\n        return super(BreadcrumbsPathInput, self).event(event)\n\n    def keyPressEvent(self, event):\n        if event.key() == QtCore.Qt.Key_Escape:\n            self.cancelled.emit()\n            return\n\n        if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):\n            self.confirmed.emit()\n            return\n\n        super(BreadcrumbsPathInput, self).keyPressEvent(event)\n\n    def focusOutEvent(self, event):\n        if not self._context_menu_visible:\n            self.cancelled.emit()\n\n        self._context_menu_visible = False\n        super(BreadcrumbsPathInput, self).focusOutEvent(event)\n\n    def contextMenuEvent(self, event):\n        self._context_menu_visible = True\n        super(BreadcrumbsPathInput, self).contextMenuEvent(event)\n\n    def _on_completer_activated(self, path):\n        self.confirmed.emit()\n\n    def _on_text_change(self, path):\n        self._proxy_model.set_path_prefix(path)\n\n\nclass BreadcrumbsButton(QtWidgets.QToolButton):\n    path_selected = QtCore.Signal(str)\n\n    def __init__(self, path, model, parent):\n        super(BreadcrumbsButton, self).__init__(parent)\n\n        self.setObjectName(\"BreadcrumbsButton\")\n\n        path_prefix = path\n        if path:\n            path_prefix += \"/\"\n\n        self.setAutoRaise(True)\n        self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)\n\n        self.setMouseTracking(True)\n\n        if path:\n            self.setText(path.split(\"/\")[-1])\n        else:\n            self.setProperty(\"empty\", \"1\")\n\n        menu = BreadcrumbsHintMenu(model, path_prefix, self)\n\n        self.setMenu(menu)\n\n        # fixed size breadcrumbs\n        self.setMinimumSize(self.minimumSizeHint())\n        size_policy = self.sizePolicy()\n        size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum)\n        self.setSizePolicy(size_policy)\n\n        menu.triggered.connect(self._on_menu_click)\n        # Don't allow to go to root with mouse click\n        if path:\n            self.clicked.connect(self._on_click)\n\n        self._path = path\n        self._path_prefix = path_prefix\n        self._model = model\n        self._menu = menu\n\n    def _on_click(self):\n        self.path_selected.emit(self._path)\n\n    def _on_menu_click(self, action):\n        item = action.data()\n        self.path_selected.emit(item)\n\n\nclass BreadcrumbsAddressBar(QtWidgets.QFrame):\n    \"Windows Explorer-like address bar\"\n    path_changed = QtCore.Signal(str)\n    path_edited = QtCore.Signal(str)\n\n    def __init__(self, parent=None):\n        super(BreadcrumbsAddressBar, self).__init__(parent)\n\n        self.setAutoFillBackground(True)\n        self.setFrameShape(QtWidgets.QFrame.StyledPanel)\n\n        # Edit presented path textually\n        proxy_model = BreadcrumbsProxy()\n        path_input = BreadcrumbsPathInput(None, proxy_model, self)\n        path_input.setVisible(False)\n\n        path_input.cancelled.connect(self._on_input_cancel)\n        path_input.confirmed.connect(self._on_input_confirm)\n\n        # Container for `crumbs_panel`\n        crumbs_container = QtWidgets.QWidget(self)\n\n        # Container for breadcrumbs\n        crumbs_panel = QtWidgets.QWidget(crumbs_container)\n        crumbs_panel.setObjectName(\"BreadcrumbsPanel\")\n\n        crumbs_layout = QtWidgets.QHBoxLayout()\n        crumbs_layout.setContentsMargins(0, 0, 0, 0)\n        crumbs_layout.setSpacing(0)\n\n        crumbs_cont_layout = QtWidgets.QHBoxLayout(crumbs_container)\n        crumbs_cont_layout.setContentsMargins(0, 0, 0, 0)\n        crumbs_cont_layout.setSpacing(0)\n        crumbs_cont_layout.addWidget(crumbs_panel)\n\n        # Clicking on empty space to the right puts the bar into edit mode\n        switch_space = ClickableWidget(self)\n\n        crumb_panel_layout = QtWidgets.QHBoxLayout(crumbs_panel)\n        crumb_panel_layout.setContentsMargins(0, 0, 0, 0)\n        crumb_panel_layout.setSpacing(0)\n        crumb_panel_layout.addLayout(crumbs_layout, 0)\n        crumb_panel_layout.addWidget(switch_space, 1)\n\n        switch_space.clicked.connect(self.switch_space_mouse_up)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(0)\n        layout.addWidget(path_input)\n        layout.addWidget(crumbs_container)\n\n        self.setMaximumHeight(path_input.height())\n\n        self.crumbs_layout = crumbs_layout\n        self.crumbs_panel = crumbs_panel\n        self.switch_space = switch_space\n        self.path_input = path_input\n        self.crumbs_container = crumbs_container\n\n        self._model = None\n        self._proxy_model = proxy_model\n\n        self._current_path = None\n\n    def set_model(self, model):\n        self._model = model\n        self.path_input.set_model(model)\n        self._proxy_model.setSourceModel(model)\n\n    def _on_input_confirm(self):\n        self.change_path(self.path_input.text())\n\n    def _on_input_cancel(self):\n        self._cancel_edit()\n\n    def _clear_crumbs(self):\n        while self.crumbs_layout.count():\n            widget = self.crumbs_layout.takeAt(0).widget()\n            if widget:\n                widget.deleteLater()\n\n    def _insert_crumb(self, path):\n        btn = BreadcrumbsButton(path, self._proxy_model, self.crumbs_panel)\n\n        self.crumbs_layout.insertWidget(0, btn)\n\n        btn.path_selected.connect(self._on_crumb_clicked)\n\n    def _on_crumb_clicked(self, path):\n        \"Breadcrumb was clicked\"\n        self.change_path(path)\n\n    def change_path(self, path):\n        path = self._model.get_valid_path(path)\n        if self._model and not self._model.is_valid_path(path):\n            self._show_address_field()\n        else:\n            self.set_path(path)\n            self.path_edited.emit(path)\n\n    def set_path(self, path):\n        if path is None or path == \".\":\n            path = self._current_path\n\n        # exit edit mode\n        self._cancel_edit()\n\n        self._clear_crumbs()\n        self._current_path = path\n        self.path_input.setText(path)\n        path_items = [\n            item\n            for item in path.split(\"/\")\n            if item\n        ]\n        while path_items:\n            item = \"/\".join(path_items)\n            self._insert_crumb(item)\n            path_items.pop(-1)\n        self._insert_crumb(\"\")\n\n        self.path_changed.emit(self._current_path)\n\n    def _cancel_edit(self):\n        \"Set edit line text back to current path and switch to view mode\"\n        # revert path\n        self.path_input.setText(self.path())\n        # switch back to breadcrumbs view\n        self._show_address_field(False)\n\n    def path(self):\n        \"Get path displayed in this BreadcrumbsAddressBar\"\n        return self._current_path\n\n    def switch_space_mouse_up(self):\n        \"EVENT: switch_space mouse clicked\"\n        self._show_address_field(True)\n\n    def _show_address_field(self, show=True):\n        \"Show text address field\"\n        self.crumbs_container.setVisible(not show)\n        self.path_input.setVisible(show)\n        if show:\n            self.path_input.setFocus()\n            self.path_input.selectAll()\n\n    def minimumSizeHint(self):\n        result = super(BreadcrumbsAddressBar, self).minimumSizeHint()\n        result.setHeight(self.path_input.minimumSizeHint().height())\n        return result\n"
  },
  {
    "path": "openpype/tools/settings/settings/categories.py",
    "content": "import sys\nimport traceback\nimport contextlib\nfrom enum import Enum\nfrom qtpy import QtWidgets, QtCore\nimport qtawesome\n\nfrom openpype.lib import get_openpype_version\nfrom openpype.tools.utils import set_style_property\nfrom openpype.settings.entities import (\n    SystemSettings,\n    ProjectSettings,\n\n    GUIEntity,\n    DictImmutableKeysEntity,\n    DictMutableKeysEntity,\n    DictConditionalEntity,\n    ListEntity,\n    PathEntity,\n    ListStrictEntity,\n\n    NumberEntity,\n    BoolEntity,\n    BaseEnumEntity,\n    TextEntity,\n    PathInput,\n    RawJsonEntity,\n    ColorEntity,\n\n    DefaultsNotDefined,\n    StudioDefaultsNotDefined,\n    SchemaError\n)\nfrom openpype.settings.entities.op_version_entity import (\n    OpenPypeVersionInput\n)\n\nfrom openpype.settings import SaveWarningExc\nfrom openpype.settings.lib import (\n    get_system_last_saved_info,\n    get_project_last_saved_info,\n)\nfrom .dialogs import SettingsLastSavedChanged, SettingsControlTaken\nfrom .widgets import (\n    ProjectListWidget,\n    VersionAction\n)\nfrom .breadcrumbs_widget import (\n    BreadcrumbsAddressBar,\n    SystemSettingsBreadcrumbs,\n    ProjectSettingsBreadcrumbs\n)\nfrom .constants import (\n    SETTINGS_PATH_KEY,\n    ROOT_KEY,\n    VALUE_KEY,\n)\nfrom .base import (\n    ExtractHelper,\n    GUIWidget,\n)\nfrom .list_item_widget import ListWidget\nfrom .list_strict_widget import ListStrictWidget\nfrom .dict_mutable_widget import DictMutableKeysWidget\nfrom .dict_conditional import DictConditionalWidget\nfrom .item_widgets import (\n    BoolWidget,\n    DictImmutableKeysWidget,\n    TextWidget,\n    OpenPypeVersionText,\n    NumberWidget,\n    RawJsonWidget,\n    EnumeratorWidget,\n    PathWidget,\n    PathInputWidget\n)\nfrom .color_widget import ColorWidget\n\n\nclass CategoryState(Enum):\n    Idle = object()\n    Working = object()\n\n\nclass IgnoreInputChangesObj:\n    def __init__(self, top_widget):\n        self._ignore_changes = False\n        self.top_widget = top_widget\n\n    def __bool__(self):\n        return self._ignore_changes\n\n    def set_ignore(self, ignore_changes=True):\n        if self._ignore_changes == ignore_changes:\n            return\n        self._ignore_changes = ignore_changes\n        if not ignore_changes:\n            self.top_widget.hierarchical_style_update()\n\n\nclass SettingsCategoryWidget(QtWidgets.QWidget):\n    state_changed = QtCore.Signal()\n    saved = QtCore.Signal(QtWidgets.QWidget)\n    restart_required_trigger = QtCore.Signal()\n    reset_started = QtCore.Signal()\n    reset_finished = QtCore.Signal()\n    full_path_requested = QtCore.Signal(str, str)\n\n    require_restart_label_text = (\n        \"Your changes require restart of\"\n        \" all running OpenPype processes to take affect.\"\n    )\n    outdated_version_label_text = (\n        \"Your settings are loaded from an older version.\"\n    )\n    source_version_tooltip = \"Using settings of current OpenPype version\"\n    source_version_tooltip_outdated = (\n        \"Please check that all settings are still correct (blue colour\\n\"\n        \"indicates potential changes in the new version) and save your\\n\"\n        \"settings to update them to you current running OpenPype version.\"\n    )\n\n    def __init__(self, controller, parent=None):\n        super(SettingsCategoryWidget, self).__init__(parent)\n\n        self._controller = controller\n        controller.event_system.add_callback(\n            \"edit.mode.changed\",\n            self._edit_mode_changed\n        )\n\n        self.entity = None\n        self._edit_mode = None\n        self._last_saved_info = None\n        self._reset_crashed = False\n\n        self._state = CategoryState.Idle\n\n        self._hide_studio_overrides = False\n        self._updating_root = False\n        self._use_version = None\n        self._current_version = get_openpype_version()\n\n        self.ignore_input_changes = IgnoreInputChangesObj(self)\n\n        self.keys = []\n        self.input_fields = []\n\n        self.initialize_attributes()\n\n        self.create_ui()\n\n    @staticmethod\n    def create_ui_for_entity(category_widget, entity, entity_widget):\n        args = (category_widget, entity, entity_widget)\n        if isinstance(entity, GUIEntity):\n            return GUIWidget(*args)\n\n        elif isinstance(entity, DictConditionalEntity):\n            return DictConditionalWidget(*args)\n\n        elif isinstance(entity, DictImmutableKeysEntity):\n            return DictImmutableKeysWidget(*args)\n\n        elif isinstance(entity, BoolEntity):\n            return BoolWidget(*args)\n\n        elif isinstance(entity, OpenPypeVersionInput):\n            return OpenPypeVersionText(*args)\n\n        elif isinstance(entity, TextEntity):\n            return TextWidget(*args)\n\n        elif isinstance(entity, NumberEntity):\n            return NumberWidget(*args)\n\n        elif isinstance(entity, RawJsonEntity):\n            return RawJsonWidget(*args)\n\n        elif isinstance(entity, ColorEntity):\n            return ColorWidget(*args)\n\n        elif isinstance(entity, BaseEnumEntity):\n            return EnumeratorWidget(*args)\n\n        elif isinstance(entity, PathEntity):\n            return PathWidget(*args)\n\n        elif isinstance(entity, PathInput):\n            return PathInputWidget(*args)\n\n        elif isinstance(entity, ListEntity):\n            return ListWidget(*args)\n\n        elif isinstance(entity, DictMutableKeysEntity):\n            return DictMutableKeysWidget(*args)\n\n        elif isinstance(entity, ListStrictEntity):\n            return ListStrictWidget(*args)\n\n        label = \"<{}>: {} ({})\".format(\n            entity.__class__.__name__, entity.path, entity.value\n        )\n        raise TypeError(\"Unknown type: {}\".format(label))\n\n    def _edit_mode_changed(self, event):\n        self.set_edit_mode(event[\"edit_mode\"])\n\n    def set_edit_mode(self, enabled):\n        if enabled is self._edit_mode:\n            return\n\n        was_false = self._edit_mode is False\n        self._edit_mode = enabled\n\n        self.save_btn.setEnabled(enabled and not self._reset_crashed)\n        if enabled:\n            tooltip = (\n                \"Someone else has opened settings UI.\"\n                \"\\nTry hit refresh to check if settings are already available.\"\n            )\n        else:\n            tooltip = \"Save settings\"\n\n        self.save_btn.setToolTip(tooltip)\n\n        # Reset when last saved information has changed\n        if was_false and not self._check_last_saved_info():\n            self.reset()\n\n    @property\n    def state(self):\n        return self._state\n\n    @state.setter\n    def state(self, value):\n        self.set_state(value)\n\n    def set_state(self, state):\n        if self._state == state:\n            return\n\n        self._state = state\n        self.state_changed.emit()\n\n        # Process events so emitted signal is processed\n        app = QtWidgets.QApplication.instance()\n        if app:\n            app.processEvents()\n\n    def initialize_attributes(self):\n        return\n\n    @property\n    def is_modifying_defaults(self):\n        if self.modify_defaults_checkbox is None:\n            return False\n        return self.modify_defaults_checkbox.isChecked()\n\n    def create_ui(self):\n        self.modify_defaults_checkbox = None\n\n        conf_wrapper_widget = QtWidgets.QSplitter(self)\n        configurations_widget = QtWidgets.QWidget(conf_wrapper_widget)\n\n        # Breadcrumbs/Path widget\n        breadcrumbs_widget = QtWidgets.QWidget(self)\n        breadcrumbs_label = QtWidgets.QLabel(\"Path:\", breadcrumbs_widget)\n        breadcrumbs_bar = BreadcrumbsAddressBar(breadcrumbs_widget)\n\n        refresh_icon = qtawesome.icon(\"fa.refresh\", color=\"white\")\n        refresh_btn = QtWidgets.QPushButton(breadcrumbs_widget)\n        refresh_btn.setIcon(refresh_icon)\n\n        breadcrumbs_layout = QtWidgets.QHBoxLayout(breadcrumbs_widget)\n        breadcrumbs_layout.setContentsMargins(5, 5, 5, 5)\n        breadcrumbs_layout.setSpacing(5)\n        breadcrumbs_layout.addWidget(breadcrumbs_label, 0)\n        breadcrumbs_layout.addWidget(breadcrumbs_bar, 1)\n        breadcrumbs_layout.addWidget(refresh_btn, 0)\n\n        # Widgets representing settings entities\n        scroll_widget = QtWidgets.QScrollArea(configurations_widget)\n        content_widget = QtWidgets.QWidget(scroll_widget)\n        scroll_widget.setWidgetResizable(True)\n        scroll_widget.setWidget(content_widget)\n\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.setContentsMargins(3, 3, 3, 3)\n        content_layout.setSpacing(5)\n        content_layout.setAlignment(QtCore.Qt.AlignTop)\n\n        # Footer widget\n        footer_widget = QtWidgets.QWidget(self)\n        footer_widget.setObjectName(\"SettingsFooter\")\n\n        # Info labels\n        # TODO dynamic labels\n        labels_alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter\n        empty_label = QtWidgets.QLabel(footer_widget)\n\n        outdated_version_label = QtWidgets.QLabel(\n            self.outdated_version_label_text, footer_widget\n        )\n        outdated_version_label.setToolTip(self.source_version_tooltip_outdated)\n        outdated_version_label.setAlignment(labels_alignment)\n        outdated_version_label.setVisible(False)\n        outdated_version_label.setObjectName(\"SettingsOutdatedSourceVersion\")\n\n        require_restart_label = QtWidgets.QLabel(\n            self.require_restart_label_text, footer_widget\n        )\n        require_restart_label.setAlignment(labels_alignment)\n        require_restart_label.setVisible(False)\n\n        # Label showing source version of loaded settings\n        source_version_label = QtWidgets.QLabel(\"\", footer_widget)\n        source_version_label.setObjectName(\"SourceVersionLabel\")\n        set_style_property(source_version_label, \"state\", \"\")\n        source_version_label.setToolTip(self.source_version_tooltip)\n\n        save_btn = QtWidgets.QPushButton(\"Save\", footer_widget)\n\n        footer_layout = QtWidgets.QHBoxLayout(footer_widget)\n        footer_layout.setContentsMargins(5, 5, 5, 5)\n        if self._controller.user_role == \"developer\":\n            self._add_developer_ui(footer_layout, footer_widget)\n\n        footer_layout.addWidget(empty_label, 1)\n        footer_layout.addWidget(outdated_version_label, 1)\n        footer_layout.addWidget(require_restart_label, 1)\n        footer_layout.addWidget(source_version_label, 0)\n        footer_layout.addWidget(save_btn, 0)\n\n        configurations_layout = QtWidgets.QVBoxLayout(configurations_widget)\n        configurations_layout.setContentsMargins(0, 0, 0, 0)\n        configurations_layout.setSpacing(0)\n\n        configurations_layout.addWidget(scroll_widget, 1)\n\n        conf_wrapper_widget.addWidget(configurations_widget)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n        main_layout.addWidget(breadcrumbs_widget, 0)\n        main_layout.addWidget(conf_wrapper_widget, 1)\n        main_layout.addWidget(footer_widget, 0)\n\n        save_btn.clicked.connect(self._save)\n        refresh_btn.clicked.connect(self._on_refresh)\n        breadcrumbs_bar.path_edited.connect(self._on_path_edit)\n\n        self._require_restart_label = require_restart_label\n        self._outdated_version_label = outdated_version_label\n        self._empty_label = empty_label\n\n        self._is_loaded_version_outdated = False\n\n        self.save_btn = save_btn\n        self._source_version_label = source_version_label\n\n        self.scroll_widget = scroll_widget\n        self.content_layout = content_layout\n        self.content_widget = content_widget\n        self.breadcrumbs_bar = breadcrumbs_bar\n\n        self.breadcrumbs_model = None\n        self.refresh_btn = refresh_btn\n\n        self.conf_wrapper_widget = conf_wrapper_widget\n        self.main_layout = main_layout\n\n        self.ui_tweaks()\n\n    def ui_tweaks(self):\n        return\n\n    def _on_path_edit(self, path):\n        for input_field in self.input_fields:\n            if input_field.make_sure_is_visible(path, True):\n                break\n\n    def scroll_to(self, widget):\n        if widget:\n            # Process events which happened before ensurence\n            # - that is because some widgets could be not visible before\n            #   this method was called and have incorrect size\n            QtWidgets.QApplication.processEvents()\n            # Scroll to widget\n            self.scroll_widget.ensureWidgetVisible(widget)\n\n    def go_to_fullpath(self, full_path):\n        \"\"\"Full path of settings entity which can lead to different category.\n\n        Args:\n            full_path (str): Full path to settings entity. It is expected that\n                path starts with category name (\"system_setting\" etc.).\n        \"\"\"\n        if not full_path:\n            return\n        items = full_path.split(\"/\")\n        category = items[0]\n        path = \"\"\n        if len(items) > 1:\n            path = \"/\".join(items[1:])\n        self.full_path_requested.emit(category, path)\n\n    def contain_category_key(self, category):\n        \"\"\"Parent widget ask if category of full path lead to this widget.\n\n        Args:\n            category (str): The category name.\n\n        Returns:\n            bool: Passed category lead to this widget.\n        \"\"\"\n        return False\n\n    def set_category_path(self, category, path):\n        \"\"\"Change path of widget based on category full path.\"\"\"\n        pass\n\n    def change_path(self, path):\n        \"\"\"Change path and go to widget.\"\"\"\n        self.breadcrumbs_bar.change_path(path)\n\n    def set_path(self, path):\n        \"\"\"Called from clicked widget.\"\"\"\n        self.breadcrumbs_bar.set_path(path)\n\n    def _add_developer_ui(self, footer_layout, footer_widget):\n        modify_defaults_checkbox = QtWidgets.QCheckBox(footer_widget)\n        modify_defaults_checkbox.setChecked(self._hide_studio_overrides)\n        label_widget = QtWidgets.QLabel(\n            \"Modify defaults\", footer_widget\n        )\n\n        footer_layout.addWidget(label_widget, 0)\n        footer_layout.addWidget(modify_defaults_checkbox, 0)\n\n        modify_defaults_checkbox.stateChanged.connect(\n            self._on_modify_defaults\n        )\n        self.modify_defaults_checkbox = modify_defaults_checkbox\n\n    def get_invalid(self):\n        invalid = []\n        for input_field in self.input_fields:\n            invalid.extend(input_field.get_invalid())\n        return invalid\n\n    def hierarchical_style_update(self):\n        for input_field in self.input_fields:\n            input_field.hierarchical_style_update()\n\n    def _on_entity_change(self):\n        self.hierarchical_style_update()\n\n    def add_widget_to_layout(self, widget, label_widget=None):\n        if label_widget:\n            raise NotImplementedError(\n                \"`add_widget_to_layout` on Category item can't accept labels\"\n            )\n        self.content_layout.addWidget(widget, 0)\n\n    @contextlib.contextmanager\n    def working_state_context(self):\n        self.set_state(CategoryState.Working)\n        yield\n        self.set_state(CategoryState.Idle)\n\n    def save(self):\n        if not self._edit_mode:\n            return\n\n        if not self.items_are_valid():\n            return\n\n        try:\n            self.entity.save()\n            self._use_version = None\n\n            # NOTE There are relations to previous entities and C++ callbacks\n            #   so it is easier to just use new entity and recreate UI but\n            #   would be nice to change this and add cleanup part so this is\n            #   not required.\n            self.reset()\n\n        except SaveWarningExc as exc:\n            warnings = [\n                \"<b>Settings were saved but few issues happened.</b>\"\n            ]\n            for item in exc.warnings:\n                warnings.append(item.replace(\"\\n\", \"<br>\"))\n\n            msg = \"<br><br>\".join(warnings)\n\n            dialog = QtWidgets.QMessageBox(self)\n            dialog.setWindowTitle(\"Save warnings\")\n            dialog.setText(msg)\n            dialog.setIcon(QtWidgets.QMessageBox.Warning)\n            dialog.exec_()\n\n            self.reset()\n\n        except Exception as exc:\n            formatted_traceback = traceback.format_exception(*sys.exc_info())\n            dialog = QtWidgets.QMessageBox(self)\n            dialog.setWindowTitle(\"Unexpected error\")\n            msg = \"Unexpected error happened!\\n\\nError: {}\".format(str(exc))\n            dialog.setText(msg)\n            dialog.setDetailedText(\"\\n\".join(formatted_traceback))\n            dialog.setIcon(QtWidgets.QMessageBox.Critical)\n\n            line_widths = set()\n            metricts = dialog.fontMetrics()\n            for line in formatted_traceback:\n                line_widths.add(metricts.width(line))\n            max_width = max(line_widths)\n\n            spacer = QtWidgets.QSpacerItem(\n                max_width, 0,\n                QtWidgets.QSizePolicy.Minimum,\n                QtWidgets.QSizePolicy.Expanding\n            )\n            layout = dialog.layout()\n            layout.addItem(\n                spacer, layout.rowCount(), 0, 1, layout.columnCount()\n            )\n            dialog.exec_()\n\n    def _create_root_entity(self):\n        raise NotImplementedError(\n            \"`create_root_entity` method not implemented\"\n        )\n\n    def _on_reset_start(self):\n        return\n\n    def _on_require_restart_change(self):\n        self._update_labels_visibility()\n\n    def reset(self):\n        self.reset_started.emit()\n        self.set_state(CategoryState.Working)\n\n        self._on_reset_start()\n\n        self.input_fields = []\n\n        while self.content_layout.count() != 0:\n            widget = self.content_layout.itemAt(0).widget()\n            if widget is not None:\n                widget.setVisible(False)\n\n            self.content_layout.removeWidget(widget)\n            widget.deleteLater()\n\n        dialog = None\n        self._updating_root = True\n        source_version = \"\"\n        try:\n            self._create_root_entity()\n\n            self.entity.add_require_restart_change_callback(\n                self._on_require_restart_change\n            )\n\n            self.add_children_gui()\n\n            self.ignore_input_changes.set_ignore(True)\n\n            for input_field in self.input_fields:\n                input_field.set_entity_value()\n\n            self.ignore_input_changes.set_ignore(False)\n            source_version = self.entity.source_version\n\n        except DefaultsNotDefined:\n            dialog = QtWidgets.QMessageBox(self)\n            dialog.setWindowTitle(\"Missing default values\")\n            dialog.setText((\n                \"Default values are not set and you\"\n                \" don't have permissions to modify them.\"\n                \" Please contact OpenPype team.\"\n            ))\n            dialog.setIcon(QtWidgets.QMessageBox.Critical)\n\n        except SchemaError as exc:\n            dialog = QtWidgets.QMessageBox(self)\n            dialog.setWindowTitle(\"Schema error\")\n            msg = \"Implementation bug!\\n\\nError: {}\".format(str(exc))\n            dialog.setText(msg)\n            dialog.setIcon(QtWidgets.QMessageBox.Warning)\n\n        except Exception as exc:\n            formatted_traceback = traceback.format_exception(*sys.exc_info())\n            dialog = QtWidgets.QMessageBox(self)\n            dialog.setWindowTitle(\"Unexpected error\")\n            msg = \"Unexpected error happened!\\n\\nError: {}\".format(str(exc))\n            dialog.setText(msg)\n            dialog.setDetailedText(\"\\n\".join(formatted_traceback))\n            dialog.setIcon(QtWidgets.QMessageBox.Critical)\n\n            line_widths = set()\n            metricts = dialog.fontMetrics()\n            for line in formatted_traceback:\n                line_widths.add(metricts.width(line))\n            max_width = max(line_widths)\n\n            spacer = QtWidgets.QSpacerItem(\n                max_width, 0,\n                QtWidgets.QSizePolicy.Minimum,\n                QtWidgets.QSizePolicy.Expanding\n            )\n            layout = dialog.layout()\n            layout.addItem(\n                spacer, layout.rowCount(), 0, 1, layout.columnCount()\n            )\n\n        self._updating_root = False\n\n        # Update source version label\n        state_value = \"\"\n        tooltip = \"\"\n        outdated = False\n        if source_version:\n            if source_version != self._current_version:\n                state_value = \"different\"\n                tooltip = self.source_version_tooltip_outdated\n                outdated = True\n            else:\n                state_value = \"same\"\n                tooltip = self.source_version_tooltip\n\n        self._is_loaded_version_outdated = outdated\n        self._source_version_label.setText(source_version)\n        self._source_version_label.setToolTip(tooltip)\n        set_style_property(self._source_version_label, \"state\", state_value)\n        self._update_labels_visibility()\n\n        self.set_state(CategoryState.Idle)\n\n        if dialog:\n            dialog.exec_()\n            self._on_reset_crash()\n        else:\n            self._on_reset_success()\n        self.reset_finished.emit()\n\n    def _on_source_version_change(self, version):\n        if self._updating_root:\n            return\n\n        if version == self._current_version:\n            version = None\n\n        self._use_version = version\n        QtCore.QTimer.singleShot(20, self.reset)\n\n    def add_context_actions(self, menu):\n        if not self.entity or self.is_modifying_defaults:\n            return\n\n        versions = self.entity.get_available_studio_versions(sorted=True)\n        if not versions:\n            return\n\n        submenu = QtWidgets.QMenu(\"Use settings from version\", menu)\n        for version in reversed(versions):\n            action = VersionAction(version, submenu)\n            action.version_triggered.connect(\n                self._on_context_version_trigger\n            )\n            submenu.addAction(action)\n\n        menu.addMenu(submenu)\n\n        extract_action = QtWidgets.QAction(\"Extract to file\", menu)\n        extract_action.triggered.connect(self._on_extract_to_file)\n\n        menu.addAction(extract_action)\n\n    def _on_context_version_trigger(self, version):\n        self._on_source_version_change(version)\n\n    def _on_extract_to_file(self):\n        filepath = ExtractHelper.ask_for_save_filepath(self)\n        if not filepath:\n            return\n\n        settings_data = {\n            SETTINGS_PATH_KEY: self.entity.root_key,\n            ROOT_KEY: self.entity.root_key,\n            VALUE_KEY: self.entity.value\n        }\n        project_name = 0\n        if hasattr(self, \"project_name\"):\n            project_name = self.project_name\n\n        ExtractHelper.extract_settings_to_json(\n            filepath, settings_data, project_name\n        )\n\n    def _on_reset_crash(self):\n        self._reset_crashed = True\n        self.save_btn.setEnabled(False)\n\n        if self.breadcrumbs_model is not None:\n            self.breadcrumbs_model.set_entity(None)\n\n    def _on_reset_success(self):\n        self._reset_crashed = False\n        if not self.save_btn.isEnabled():\n            self.save_btn.setEnabled(self._edit_mode)\n\n        if self.breadcrumbs_model is not None:\n            path = self.breadcrumbs_bar.path()\n            self.breadcrumbs_bar.set_path(\"\")\n            self.breadcrumbs_model.set_entity(self.entity)\n            self.breadcrumbs_bar.change_path(path)\n\n    def add_children_gui(self):\n        for child_obj in self.entity.children:\n            item = self.create_ui_for_entity(self, child_obj, self)\n            self.input_fields.append(item)\n\n        # Add spacer to stretch children guis\n        self.content_layout.addWidget(\n            QtWidgets.QWidget(self.content_widget), 1\n        )\n\n    def items_are_valid(self):\n        invalid_items = self.get_invalid()\n        if not invalid_items:\n            return True\n\n        msg_box = QtWidgets.QMessageBox(\n            QtWidgets.QMessageBox.Warning,\n            \"Invalid input\",\n            (\n                \"There is invalid value in one of inputs.\"\n                \" Please lead red color and fix them.\"\n            ),\n            parent=self\n        )\n        msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok)\n        msg_box.exec_()\n\n        first_invalid_item = invalid_items[0]\n        self.scroll_widget.ensureWidgetVisible(first_invalid_item)\n        if first_invalid_item.isVisible():\n            first_invalid_item.setFocus()\n        return False\n\n    def on_saved(self, saved_tab_widget):\n        \"\"\"Callback on any tab widget save.\"\"\"\n        return\n\n    def _check_last_saved_info(self):\n        raise NotImplementedError((\n            \"{} does not have implemented '_check_last_saved_info'\"\n        ).format(self.__class__.__name__))\n\n    def _save(self):\n        self._controller.update_last_opened_info()\n        if not self._controller.opened_info:\n            dialog = SettingsControlTaken(self._last_saved_info, self)\n            dialog.exec_()\n            return\n\n        if not self._check_last_saved_info():\n            dialog = SettingsLastSavedChanged(self._last_saved_info, self)\n            dialog.exec_()\n            if dialog.result() == 0:\n                return\n\n        # Don't trigger restart if defaults are modified\n        if self.is_modifying_defaults:\n            require_restart = False\n        else:\n            require_restart = self.entity.require_restart\n\n        self.set_state(CategoryState.Working)\n\n        if self.items_are_valid():\n            self.save()\n\n        self.set_state(CategoryState.Idle)\n\n        self.saved.emit(self)\n\n        if require_restart:\n            self.restart_required_trigger.emit()\n\n    def _update_labels_visibility(self):\n        visible_label = None\n        labels = {\n            self._empty_label,\n            self._outdated_version_label,\n            self._require_restart_label,\n        }\n        if self.is_modifying_defaults or self.entity is None:\n            require_restart = False\n        else:\n            require_restart = self.entity.require_restart\n\n        if require_restart:\n            visible_label = self._require_restart_label\n        elif self._is_loaded_version_outdated:\n            visible_label = self._outdated_version_label\n        else:\n            visible_label = self._empty_label\n\n        if visible_label.isVisible():\n            return\n\n        for label in labels:\n            if label is visible_label:\n                visible_label.setVisible(True)\n            else:\n                label.setVisible(False)\n\n    def _on_refresh(self):\n        self.reset()\n\n    def _on_hide_studio_overrides(self, state):\n        self._hide_studio_overrides = (state == QtCore.Qt.Checked)\n\n\nclass SystemWidget(SettingsCategoryWidget):\n    def __init__(self, *args, **kwargs):\n        self._actions = []\n        super(SystemWidget, self).__init__(*args, **kwargs)\n\n    def _check_last_saved_info(self):\n        if self.is_modifying_defaults:\n            return True\n\n        last_saved_info = get_system_last_saved_info()\n        return self._last_saved_info == last_saved_info\n\n    def contain_category_key(self, category):\n        if category == \"system_settings\":\n            return True\n        return False\n\n    def set_category_path(self, category, path):\n        self.breadcrumbs_bar.change_path(path)\n\n    def _create_root_entity(self):\n        entity = SystemSettings(\n            set_studio_state=False, source_version=self._use_version\n        )\n        entity.on_change_callbacks.append(self._on_entity_change)\n        self.entity = entity\n        last_saved_info = None\n        if not self.is_modifying_defaults:\n            last_saved_info = get_system_last_saved_info()\n        self._last_saved_info = last_saved_info\n        try:\n            if self.is_modifying_defaults:\n                entity.set_defaults_state()\n            else:\n                entity.set_studio_state()\n\n            if self.modify_defaults_checkbox:\n                self.modify_defaults_checkbox.setEnabled(True)\n        except DefaultsNotDefined:\n            if not self.modify_defaults_checkbox:\n                raise\n\n            entity.set_defaults_state()\n            self.modify_defaults_checkbox.setChecked(True)\n            self.modify_defaults_checkbox.setEnabled(False)\n\n    def ui_tweaks(self):\n        self.breadcrumbs_model = SystemSettingsBreadcrumbs()\n        self.breadcrumbs_bar.set_model(self.breadcrumbs_model)\n\n    def _on_modify_defaults(self):\n        if self.is_modifying_defaults:\n            if not self.entity.is_in_defaults_state():\n                self.reset()\n        else:\n            if not self.entity.is_in_studio_state():\n                self.reset()\n\n\nclass ProjectWidget(SettingsCategoryWidget):\n    def __init__(self, *args, **kwargs):\n        super(ProjectWidget, self).__init__(*args, **kwargs)\n\n    def set_edit_mode(self, enabled):\n        super(ProjectWidget, self).set_edit_mode(enabled)\n        self.project_list_widget.set_edit_mode(enabled)\n\n    def _check_last_saved_info(self):\n        if self.is_modifying_defaults:\n            return True\n\n        last_saved_info = get_project_last_saved_info(self.project_name)\n        return self._last_saved_info == last_saved_info\n\n    def contain_category_key(self, category):\n        if category in (\"project_settings\", \"project_anatomy\"):\n            return True\n        return False\n\n    def set_category_path(self, category, path):\n        if path:\n            path_items = path.split(\"/\")\n            if path_items[0] not in (\"project_settings\", \"project_anatomy\"):\n                path = \"/\".join([category, path])\n        else:\n            path = category\n\n        self.breadcrumbs_bar.change_path(path)\n\n    def initialize_attributes(self):\n        self.project_name = None\n\n    def ui_tweaks(self):\n        self.breadcrumbs_model = ProjectSettingsBreadcrumbs()\n        self.breadcrumbs_bar.set_model(self.breadcrumbs_model)\n\n        project_list_widget = ProjectListWidget(self)\n\n        self.conf_wrapper_widget.insertWidget(0, project_list_widget)\n        self.conf_wrapper_widget.setStretchFactor(0, 0)\n        self.conf_wrapper_widget.setStretchFactor(1, 1)\n\n        project_list_widget.project_changed.connect(self._on_project_change)\n        project_list_widget.version_change_requested.connect(\n            self._on_source_version_change\n        )\n        project_list_widget.extract_to_file_requested.connect(\n            self._on_extract_to_file\n        )\n\n        self.project_list_widget = project_list_widget\n\n    def get_project_names(self):\n        if self.is_modifying_defaults:\n            return []\n        return self.project_list_widget.get_project_names()\n\n    def on_saved(self, saved_tab_widget):\n        \"\"\"Callback on any tab widget save.\n\n        Check if AVALON_MONGO is still same.\n        \"\"\"\n        if self is saved_tab_widget:\n            return\n\n    def _on_context_version_trigger(self, version):\n        self.project_list_widget.select_project(None)\n        super(ProjectWidget, self)._on_context_version_trigger(version)\n\n    def _on_reset_start(self):\n        self.project_list_widget.refresh()\n\n    def _on_reset_crash(self):\n        self._set_enabled_project_list(False)\n        super(ProjectWidget, self)._on_reset_crash()\n\n    def _on_reset_success(self):\n        self._set_enabled_project_list(True)\n        super(ProjectWidget, self)._on_reset_success()\n\n    def _set_enabled_project_list(self, enabled):\n        if enabled and self.is_modifying_defaults:\n            enabled = False\n        if self.project_list_widget.isEnabled() != enabled:\n            self.project_list_widget.setEnabled(enabled)\n\n    def _create_root_entity(self):\n        entity = ProjectSettings(\n            change_state=False, source_version=self._use_version\n        )\n        entity.on_change_callbacks.append(self._on_entity_change)\n        self.project_list_widget.set_entity(entity)\n        self.entity = entity\n\n        last_saved_info = None\n        if not self.is_modifying_defaults:\n            last_saved_info = get_project_last_saved_info(self.project_name)\n        self._last_saved_info = last_saved_info\n        try:\n            if self.is_modifying_defaults:\n                self.entity.set_defaults_state()\n\n            elif self.project_name is None:\n                self.entity.set_studio_state()\n\n            else:\n                self.entity.change_project(\n                    self.project_name, self._use_version\n                )\n\n            if self.modify_defaults_checkbox:\n                self.modify_defaults_checkbox.setEnabled(True)\n\n            self._set_enabled_project_list(True)\n\n        except DefaultsNotDefined:\n            if not self.modify_defaults_checkbox:\n                raise\n\n            self.entity.set_defaults_state()\n            self.modify_defaults_checkbox.setChecked(True)\n            self.modify_defaults_checkbox.setEnabled(False)\n            self._set_enabled_project_list(False)\n\n        except StudioDefaultsNotDefined:\n            self.select_default_project()\n\n    def _on_project_change(self):\n        project_name = self.project_list_widget.project_name()\n        if project_name == self.project_name:\n            return\n\n        self.project_name = project_name\n\n        self.set_state(CategoryState.Working)\n\n        self.reset()\n\n        self.set_state(CategoryState.Idle)\n\n    def _on_modify_defaults(self):\n        if self.is_modifying_defaults:\n            self._set_enabled_project_list(False)\n            if not self.entity.is_in_defaults_state():\n                self.reset()\n        else:\n            self._set_enabled_project_list(True)\n            if not self.entity.is_in_studio_state():\n                self.reset()\n"
  },
  {
    "path": "openpype/tools/settings/settings/color_widget.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\nfrom .item_widgets import InputWidget\n\nfrom openpype.widgets.color_widgets import (\n    ColorPickerWidget,\n    draw_checkerboard_tile\n)\n\n\nclass ColorWidget(InputWidget):\n    def _add_inputs_to_layout(self):\n        self.input_field = ColorViewer(self.content_widget)\n\n        self.setFocusProxy(self.input_field)\n\n        self.content_layout.addWidget(self.input_field, 1)\n\n        self.input_field.clicked.connect(self._on_click)\n\n        self._dialog = None\n\n    def _on_click(self):\n        if self._dialog:\n            self._dialog.open()\n            return\n\n        dialog = ColorDialog(\n            self.input_field.color(), self.entity.use_alpha, self\n        )\n        self._dialog = dialog\n\n        dialog.open()\n        dialog.finished.connect(self._on_dialog_finish)\n\n    def _on_dialog_finish(self, *_args):\n        if not self._dialog:\n            return\n\n        color = self._dialog.result()\n        if color is not None:\n            self.input_field.set_color(color)\n            self._on_value_change()\n\n        self._dialog.deleteLater()\n        self._dialog = None\n\n    def _on_entity_change(self):\n        if self.entity.value != self.input_value():\n            self.set_entity_value()\n\n    def set_entity_value(self):\n        self.input_field.set_color(*self.entity.value)\n\n    def input_value(self):\n        color = self.input_field.color()\n        return [color.red(), color.green(), color.blue(), color.alpha()]\n\n    def _on_value_change(self):\n        if self.ignore_input_changes:\n            return\n\n        self.entity.set(self.input_value())\n\n\nclass ColorViewer(QtWidgets.QWidget):\n    clicked = QtCore.Signal()\n\n    def __init__(self, parent=None):\n        super(ColorViewer, self).__init__(parent)\n\n        self.setMinimumSize(10, 10)\n\n        self.actual_pen = QtGui.QPen()\n        self.actual_color = QtGui.QColor()\n        self._checkerboard = None\n\n    def mouseReleaseEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self.clicked.emit()\n        super(ColorViewer, self).mouseReleaseEvent(event)\n\n    def checkerboard(self):\n        if not self._checkerboard:\n            self._checkerboard = draw_checkerboard_tile(self.height() / 4)\n        return self._checkerboard\n\n    def color(self):\n        return self.actual_color\n\n    def set_color(self, *args):\n        # Create copy of entered color\n        self.actual_color = QtGui.QColor(*args)\n        # Repaint\n        self.update()\n\n    def set_alpha(self, alpha):\n        # Change alpha of current color\n        self.actual_color.setAlpha(alpha)\n        # Repaint\n        self.update()\n\n    def paintEvent(self, event):\n        rect = event.rect()\n\n        painter = QtGui.QPainter(self)\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n\n        radius = rect.height() / 2\n        rounded_rect = QtGui.QPainterPath()\n        rounded_rect.addRoundedRect(QtCore.QRectF(rect), radius, radius)\n        painter.setClipPath(rounded_rect)\n\n        pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 67))\n        pen.setWidth(1)\n        painter.setPen(pen)\n        painter.drawTiledPixmap(rect, self.checkerboard())\n        painter.fillRect(rect, self.actual_color)\n        painter.drawPath(rounded_rect)\n\n        painter.end()\n\n\nclass ColorDialog(QtWidgets.QDialog):\n    def __init__(self, color=None, use_alpha=True, parent=None):\n        super(ColorDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"Color picker dialog\")\n\n        picker_widget = ColorPickerWidget(color, use_alpha, self)\n\n        footer_widget = QtWidgets.QWidget(self)\n\n        ok_btn = QtWidgets.QPushButton(\"Ok\", footer_widget)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", footer_widget)\n\n        footer_layout = QtWidgets.QHBoxLayout(footer_widget)\n        footer_layout.setContentsMargins(0, 0, 0, 0)\n        footer_layout.addStretch(1)\n        footer_layout.addWidget(ok_btn)\n        footer_layout.addWidget(cancel_btn)\n\n        layout = QtWidgets.QVBoxLayout(self)\n\n        layout.addWidget(picker_widget, 1)\n        layout.addWidget(footer_widget, 0)\n\n        ok_btn.clicked.connect(self.on_ok_clicked)\n        cancel_btn.clicked.connect(self.on_cancel_clicked)\n\n        self.picker_widget = picker_widget\n        self.ok_btn = ok_btn\n        self.cancel_btn = cancel_btn\n\n        self._result = None\n\n    def showEvent(self, event):\n        super(ColorDialog, self).showEvent(event)\n\n        btns_width = max(self.ok_btn.width(), self.cancel_btn.width())\n        self.ok_btn.setFixedWidth(btns_width)\n        self.cancel_btn.setFixedWidth(btns_width)\n\n    def on_ok_clicked(self):\n        self._result = self.picker_widget.color()\n        self.close()\n\n    def on_cancel_clicked(self):\n        self._result = None\n        self.close()\n\n    def result(self):\n        return self._result\n"
  },
  {
    "path": "openpype/tools/settings/settings/constants.py",
    "content": "from qtpy import QtCore\n\n\nDEFAULT_PROJECT_LABEL = \"< Default >\"\nPROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1\nPROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2\nPROJECT_IS_SELECTED_ROLE = QtCore.Qt.UserRole + 3\nPROJECT_VERSION_ROLE = QtCore.Qt.UserRole + 4\n\n# Save/Extract keys\nSETTINGS_PATH_KEY = \"__settings_path__\"\nROOT_KEY = \"__root_key__\"\nVALUE_KEY = \"__value__\"\nSAVE_TIME_KEY = \"__extracted__\"\nPROJECT_NAME_KEY = \"__project_name__\"\n\n__all__ = (\n    \"DEFAULT_PROJECT_LABEL\",\n\n    \"PROJECT_NAME_ROLE\",\n    \"PROJECT_IS_ACTIVE_ROLE\",\n    \"PROJECT_IS_SELECTED_ROLE\",\n    \"PROJECT_VERSION_ROLE\",\n\n    \"SETTINGS_PATH_KEY\",\n    \"ROOT_KEY\",\n    \"VALUE_KEY\",\n    \"SAVE_TIME_KEY\",\n    \"PROJECT_NAME_KEY\",\n)\n"
  },
  {
    "path": "openpype/tools/settings/settings/dialogs.py",
    "content": "from qtpy import QtWidgets, QtCore\n\nfrom openpype.tools.utils.delegates import pretty_date\n\n\nclass BaseInfoDialog(QtWidgets.QDialog):\n    width = 600\n    height = 400\n\n    def __init__(self, message, title, info_obj, parent=None):\n        super(BaseInfoDialog, self).__init__(parent)\n        self._result = 0\n        self._info_obj = info_obj\n\n        self.setWindowTitle(title)\n\n        message_label = QtWidgets.QLabel(message, self)\n        message_label.setWordWrap(True)\n\n        separator_widget_1 = QtWidgets.QFrame(self)\n        separator_widget_2 = QtWidgets.QFrame(self)\n        for separator_widget in (\n            separator_widget_1,\n            separator_widget_2\n        ):\n            separator_widget.setObjectName(\"Separator\")\n            separator_widget.setMinimumHeight(1)\n            separator_widget.setMaximumHeight(1)\n\n        other_information = QtWidgets.QWidget(self)\n        other_information_layout = QtWidgets.QFormLayout(other_information)\n        other_information_layout.setContentsMargins(0, 0, 0, 0)\n        for label, value in (\n            (\"Username\", info_obj.username),\n            (\"Host name\", info_obj.hostname),\n            (\"Host IP\", info_obj.hostip),\n            (\"System name\", info_obj.system_name),\n            (\"Local ID\", info_obj.local_id),\n        ):\n            other_information_layout.addRow(\n                label,\n                QtWidgets.QLabel(value or \"N/A\", other_information)\n            )\n\n        timestamp_label = QtWidgets.QLabel(\n            pretty_date(info_obj.timestamp_obj), other_information\n        )\n        other_information_layout.addRow(\"Time\", timestamp_label)\n\n        footer_widget = QtWidgets.QWidget(self)\n        buttons_widget = QtWidgets.QWidget(footer_widget)\n\n        buttons_layout = QtWidgets.QHBoxLayout(buttons_widget)\n        buttons_layout.setContentsMargins(0, 0, 0, 0)\n        buttons = self.get_buttons(buttons_widget)\n        for button in buttons:\n            buttons_layout.addWidget(button, 1)\n\n        footer_layout = QtWidgets.QHBoxLayout(footer_widget)\n        footer_layout.setContentsMargins(0, 0, 0, 0)\n        footer_layout.addStretch(1)\n        footer_layout.addWidget(buttons_widget, 0)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(message_label, 0)\n        layout.addWidget(separator_widget_1, 0)\n        layout.addStretch(1)\n        layout.addWidget(other_information, 0, QtCore.Qt.AlignHCenter)\n        layout.addStretch(1)\n        layout.addWidget(separator_widget_2, 0)\n        layout.addWidget(footer_widget, 0)\n\n        timestamp_timer = QtCore.QTimer()\n        timestamp_timer.setInterval(1000)\n        timestamp_timer.timeout.connect(self._on_timestamp_timer)\n\n        self._timestamp_label = timestamp_label\n        self._timestamp_timer = timestamp_timer\n\n    def showEvent(self, event):\n        super(BaseInfoDialog, self).showEvent(event)\n        self._timestamp_timer.start()\n        self.resize(self.width, self.height)\n\n    def closeEvent(self, event):\n        self._timestamp_timer.stop()\n        super(BaseInfoDialog, self).closeEvent(event)\n\n    def _on_timestamp_timer(self):\n        self._timestamp_label.setText(\n            pretty_date(self._info_obj.timestamp_obj)\n        )\n\n    def result(self):\n        return self._result\n\n    def get_buttons(self, parent):\n        return []\n\n\nclass SettingsUIOpenedElsewhere(BaseInfoDialog):\n    def __init__(self, info_obj, parent=None):\n        title = \"Someone else has opened Settings UI\"\n        message = (\n            \"Someone else has opened Settings UI which could cause data loss.\"\n            \" Please contact the person on the other side.\"\n            \"<br/><br/>You can continue in <b>view-only mode</b>.\"\n            \" All changes in view mode will be lost.\"\n            \"<br/><br/>You can <b>take control</b> which will cause that\"\n            \" all changes of settings on the other side will be lost.<br/>\"\n        )\n        super(SettingsUIOpenedElsewhere, self).__init__(\n            message, title, info_obj, parent\n        )\n\n    def _on_take_control(self):\n        self._result = 1\n        self.close()\n\n    def _on_view_mode(self):\n        self._result = 0\n        self.close()\n\n    def get_buttons(self, parent):\n        take_control_btn = QtWidgets.QPushButton(\n            \"Take control\", parent\n        )\n        view_mode_btn = QtWidgets.QPushButton(\n            \"View only\", parent\n        )\n\n        take_control_btn.clicked.connect(self._on_take_control)\n        view_mode_btn.clicked.connect(self._on_view_mode)\n\n        return [\n            take_control_btn,\n            view_mode_btn\n        ]\n\n\nclass SettingsLastSavedChanged(BaseInfoDialog):\n    width = 500\n    height = 300\n\n    def __init__(self, info_obj, parent=None):\n        title = \"Settings has changed\"\n        message = (\n            \"Settings has changed while you had opened this settings session.\"\n            \"<br/><br/>It is <b>recommended to refresh settings</b>\"\n            \" and re-apply changes in the new session.\"\n        )\n        super(SettingsLastSavedChanged, self).__init__(\n            message, title, info_obj, parent\n        )\n\n    def _on_save(self):\n        self._result = 1\n        self.close()\n\n    def _on_close(self):\n        self._result = 0\n        self.close()\n\n    def get_buttons(self, parent):\n        close_btn = QtWidgets.QPushButton(\n            \"Close\", parent\n        )\n        save_btn = QtWidgets.QPushButton(\n            \"Save anyway\", parent\n        )\n\n        close_btn.clicked.connect(self._on_close)\n        save_btn.clicked.connect(self._on_save)\n\n        return [\n            close_btn,\n            save_btn\n        ]\n\n\nclass SettingsControlTaken(BaseInfoDialog):\n    width = 500\n    height = 300\n\n    def __init__(self, info_obj, parent=None):\n        title = \"Settings control taken\"\n        message = (\n            \"Someone took control over your settings.\"\n            \"<br/><br/>It is not possible to save changes of currently\"\n            \" opened session. Copy changes you want to keep and hit refresh.\"\n        )\n        super(SettingsControlTaken, self).__init__(\n            message, title, info_obj, parent\n        )\n\n    def _on_confirm(self):\n        self.close()\n\n    def get_buttons(self, parent):\n        confirm_btn = QtWidgets.QPushButton(\"Understand\", parent)\n        confirm_btn.clicked.connect(self._on_confirm)\n        return [confirm_btn]\n"
  },
  {
    "path": "openpype/tools/settings/settings/dict_conditional.py",
    "content": "from qtpy import QtWidgets\n\nfrom .widgets import (\n    ExpandingWidget,\n    GridLabelWidget\n)\nfrom .wrapper_widgets import (\n    WrapperWidget,\n    CollapsibleWrapper,\n    FormWrapper\n)\nfrom .base import BaseWidget\nfrom openpype.tools.settings import CHILD_OFFSET\n\n\nclass DictConditionalWidget(BaseWidget):\n    def create_ui(self):\n        self.input_fields = []\n\n        self._content_by_enum_value = {}\n        self._last_enum_value = None\n\n        self.label_widget = None\n        self.body_widget = None\n        self.content_widget = None\n        self.content_layout = None\n        self.enum_layout = None\n\n        label = None\n        if self.entity.is_dynamic_item:\n            self._ui_as_dynamic_item()\n\n        elif self.entity.use_label_wrap:\n            self._ui_label_wrap()\n\n        else:\n            self._ui_item_base()\n            label = self.entity.label\n\n        self._parent_widget_by_entity_id = {}\n        self._enum_key_by_wrapper_id = {}\n        self._added_wrapper_ids = set()\n\n        enum_layout = QtWidgets.QGridLayout()\n        enum_layout.setContentsMargins(0, 0, 0, 0)\n        enum_layout.setColumnStretch(0, 0)\n        enum_layout.setColumnStretch(1, 1)\n\n        all_children_layout = QtWidgets.QVBoxLayout()\n        all_children_layout.setContentsMargins(0, 0, 0, 0)\n\n        if self.entity.enum_is_horizontal:\n            if self.entity.enum_on_right:\n                self.content_layout.addLayout(all_children_layout, 0, 0)\n                self.content_layout.addLayout(enum_layout, 0, 1)\n                # Stretch combobox to minimum and expand value\n                self.content_layout.setColumnStretch(0, 1)\n                self.content_layout.setColumnStretch(1, 0)\n            else:\n                self.content_layout.addLayout(enum_layout, 0, 0)\n                self.content_layout.addLayout(all_children_layout, 0, 1)\n                # Stretch combobox to minimum and expand value\n                self.content_layout.setColumnStretch(0, 0)\n                self.content_layout.setColumnStretch(1, 1)\n\n        else:\n            # Expand content\n            self.content_layout.setColumnStretch(0, 1)\n            self.content_layout.addLayout(enum_layout, 0, 0)\n            self.content_layout.addLayout(all_children_layout, 1, 0)\n\n        self.enum_layout = enum_layout\n        self.all_children_layout = all_children_layout\n\n        # Add enum entity to layout mapping\n        enum_entity = self.entity.enum_entity\n        self._parent_widget_by_entity_id[enum_entity.id] = self.content_widget\n\n        # Add rest of entities to wrapper mappings\n        for enum_key, children in self.entity.gui_layout.items():\n            parent_widget_by_entity_id = {}\n\n            content_widget = QtWidgets.QWidget(self.content_widget)\n            content_layout = QtWidgets.QGridLayout(content_widget)\n            content_layout.setColumnStretch(0, 0)\n            content_layout.setColumnStretch(1, 1)\n            content_layout.setContentsMargins(0, 0, 0, 0)\n            content_layout.setSpacing(5)\n\n            all_children_layout.addWidget(content_widget)\n\n            self._content_by_enum_value[enum_key] = {\n                \"widget\": content_widget,\n                \"layout\": content_layout\n            }\n\n            self._prepare_entity_layouts(\n                children,\n                content_widget,\n                parent_widget_by_entity_id\n            )\n            for item_id in parent_widget_by_entity_id.keys():\n                self._enum_key_by_wrapper_id[item_id] = enum_key\n            self._parent_widget_by_entity_id.update(parent_widget_by_entity_id)\n\n        enum_input_field = self.create_ui_for_entity(\n            self.category_widget, self.entity.enum_entity, self\n        )\n        self.enum_input_field = enum_input_field\n        self.input_fields.append(enum_input_field)\n\n        for item_key, children in self.entity.children.items():\n            content_widget = self._content_by_enum_value[item_key][\"widget\"]\n            for child_obj in children:\n                self.input_fields.append(\n                    self.create_ui_for_entity(\n                        self.category_widget, child_obj, self\n                    )\n                )\n\n        if self.entity.use_label_wrap and self.content_layout.count() == 0:\n            self.body_widget.hide_toolbox(True)\n\n        self.entity_widget.add_widget_to_layout(self, label)\n\n    def _prepare_entity_layouts(\n        self, gui_layout, widget, parent_widget_by_entity_id\n    ):\n        for child in gui_layout:\n            if not isinstance(child, dict):\n                parent_widget_by_entity_id[child.id] = widget\n                continue\n\n            if child[\"type\"] == \"collapsible-wrap\":\n                wrapper = CollapsibleWrapper(child, widget)\n\n            elif child[\"type\"] == \"form\":\n                wrapper = FormWrapper(child, widget)\n\n            else:\n                raise KeyError(\n                    \"Unknown Wrapper type \\\"{}\\\"\".format(child[\"type\"])\n                )\n\n            parent_widget_by_entity_id[wrapper.id] = widget\n\n            self._prepare_entity_layouts(\n                child[\"children\"], wrapper, parent_widget_by_entity_id\n            )\n\n    def _ui_item_base(self):\n        self.setObjectName(\"DictInvisible\")\n\n        self.content_widget = self\n        self.content_layout = QtWidgets.QGridLayout(self)\n        self.content_layout.setContentsMargins(0, 0, 0, 0)\n        self.content_layout.setSpacing(5)\n\n    def _ui_as_dynamic_item(self):\n        content_widget = QtWidgets.QWidget(self)\n        content_widget.setObjectName(\"DictAsWidgetBody\")\n\n        show_borders = str(int(self.entity.show_borders))\n        content_widget.setProperty(\"show_borders\", show_borders)\n\n        label_widget = QtWidgets.QLabel(self.entity.label)\n        label_widget.setObjectName(\"SettingsLabel\")\n\n        content_layout = QtWidgets.QGridLayout(content_widget)\n        content_layout.setContentsMargins(5, 5, 5, 5)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(5)\n        main_layout.addWidget(content_widget)\n\n        self.label_widget = label_widget\n        self.content_widget = content_widget\n        self.content_layout = content_layout\n\n    def _ui_label_wrap(self):\n        content_widget = QtWidgets.QWidget(self)\n        content_widget.setObjectName(\"ContentWidget\")\n\n        if self.entity.highlight_content:\n            content_state = \"highlighted\"\n            bottom_margin = 5\n        else:\n            content_state = \"\"\n            bottom_margin = 0\n        content_widget.setProperty(\"content_state\", content_state)\n        content_layout_margins = (CHILD_OFFSET, 5, 0, bottom_margin)\n\n        body_widget = ExpandingWidget(self.entity.label, self)\n        label_widget = body_widget.label_widget\n        body_widget.set_content_widget(content_widget)\n\n        content_layout = QtWidgets.QGridLayout(content_widget)\n        content_layout.setContentsMargins(*content_layout_margins)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n        main_layout.addWidget(body_widget)\n\n        self.label_widget = label_widget\n        self.body_widget = body_widget\n        self.content_widget = content_widget\n        self.content_layout = content_layout\n\n        if self.entity.collapsible:\n            if not self.entity.collapsed:\n                body_widget.toggle_content()\n        else:\n            body_widget.hide_toolbox(hide_content=False)\n\n    def make_sure_is_visible(self, path, scroll_to):\n        if not path:\n            return False\n\n        entity_path = self.entity.path\n        if entity_path == path:\n            self.set_focus(scroll_to)\n            return True\n\n        if not path.startswith(entity_path):\n            return False\n\n        if self.body_widget and not self.body_widget.is_expanded():\n            self.body_widget.toggle_content(True)\n\n        for input_field in self.input_fields:\n            if input_field.make_sure_is_visible(path, scroll_to):\n                return True\n        return False\n\n    def add_widget_to_layout(self, widget, label=None):\n        if not widget.entity:\n            map_id = widget.id\n        else:\n            map_id = widget.entity.id\n\n        is_enum_item = map_id == self.entity.enum_entity.id\n        if is_enum_item:\n            content_widget = self.content_widget\n            content_layout = self.enum_layout\n\n            if not label:\n                content_layout.addWidget(widget, 0, 0, 1, 2)\n                return\n\n            label_widget = GridLabelWidget(label, widget)\n            label_widget.input_field = widget\n            widget.label_widget = label_widget\n            content_layout.addWidget(label_widget, 0, 0, 1, 1)\n            content_layout.addWidget(widget, 0, 1, 1, 1)\n            return\n\n        enum_value = self._enum_key_by_wrapper_id[map_id]\n        content_widget = self._content_by_enum_value[enum_value][\"widget\"]\n        content_layout = self._content_by_enum_value[enum_value][\"layout\"]\n\n        wrapper = self._parent_widget_by_entity_id[map_id]\n        if wrapper is not content_widget:\n            wrapper.add_widget_to_layout(widget, label)\n            if wrapper.id not in self._added_wrapper_ids:\n                self.add_widget_to_layout(wrapper)\n                self._added_wrapper_ids.add(wrapper.id)\n            return\n\n        row = content_layout.rowCount()\n        if not label or isinstance(widget, WrapperWidget):\n            content_layout.addWidget(widget, row, 0, 1, 2)\n        else:\n            label_widget = GridLabelWidget(label, widget)\n            label_widget.input_field = widget\n            widget.label_widget = label_widget\n            content_layout.addWidget(label_widget, row, 0, 1, 1)\n            content_layout.addWidget(widget, row, 1, 1, 1)\n\n    def set_entity_value(self):\n        for input_field in self.input_fields:\n            input_field.set_entity_value()\n\n        self._on_entity_change()\n\n    def hierarchical_style_update(self):\n        self.update_style()\n        for input_field in self.input_fields:\n            input_field.hierarchical_style_update()\n\n    def update_style(self):\n        if not self.body_widget and not self.label_widget:\n            return\n\n        if self.entity.group_item:\n            group_item = self.entity.group_item\n            has_unsaved_changes = group_item.has_unsaved_changes\n            has_project_override = group_item.has_project_override\n            has_studio_override = group_item.has_studio_override\n        else:\n            has_unsaved_changes = self.entity.has_unsaved_changes\n            has_project_override = self.entity.has_project_override\n            has_studio_override = self.entity.has_studio_override\n\n        style_state = self.get_style_state(\n            self.is_invalid,\n            has_unsaved_changes,\n            has_project_override,\n            has_studio_override\n        )\n        if self._style_state == style_state:\n            return\n\n        self._style_state = style_state\n\n        if self.body_widget:\n            if style_state:\n                child_style_state = \"child-{}\".format(style_state)\n            else:\n                child_style_state = \"\"\n\n            self.body_widget.side_line_widget.setProperty(\n                \"state\", child_style_state\n            )\n            self.body_widget.side_line_widget.style().polish(\n                self.body_widget.side_line_widget\n            )\n\n        # There is nothing to care if there is no label\n        if not self.label_widget:\n            return\n\n        # Don't change label if is not group or under group item\n        if not self.entity.is_group and not self.entity.group_item:\n            return\n\n        self.label_widget.setProperty(\"state\", style_state)\n        self.label_widget.style().polish(self.label_widget)\n\n    def _on_entity_change(self):\n        enum_value = self.enum_input_field.entity.value\n        if enum_value == self._last_enum_value:\n            return\n\n        self._last_enum_value = enum_value\n        for item_key, content in self._content_by_enum_value.items():\n            widget = content[\"widget\"]\n            widget.setVisible(item_key == enum_value)\n\n    @property\n    def is_invalid(self):\n        return self._is_invalid or self._child_invalid\n\n    @property\n    def _child_invalid(self):\n        for input_field in self.input_fields:\n            if input_field.is_invalid:\n                return True\n        return False\n\n    def get_invalid(self):\n        invalid = []\n        for input_field in self.input_fields:\n            invalid.extend(input_field.get_invalid())\n        return invalid\n"
  },
  {
    "path": "openpype/tools/settings/settings/dict_mutable_widget.py",
    "content": "from uuid import uuid4\n\nfrom qtpy import QtWidgets, QtCore\n\nfrom .base import BaseWidget\nfrom .lib import (\n    create_deffered_value_change_timer,\n    create_add_btn,\n    create_remove_btn,\n    create_confirm_btn\n)\nfrom .widgets import (\n    ExpandingWidget,\n    IconButton\n)\nfrom openpype.tools.settings import (\n    BTN_FIXED_SIZE,\n    CHILD_OFFSET\n)\nfrom openpype.settings.constants import KEY_REGEX\n\n\nKEY_INPUT_TOOLTIP = (\n    \"Keys can't be duplicated and may contain alphabetical character (a-Z)\"\n    \"\\nnumerical characters (0-9) dash (\\\"-\\\") or underscore (\\\"_\\\").\"\n)\n\n\nclass ModifiableDictEmptyItem(QtWidgets.QWidget):\n    def __init__(self, entity_widget, store_as_list, parent):\n        super(ModifiableDictEmptyItem, self).__init__(parent)\n        self.entity_widget = entity_widget\n        self.collapsible_key = entity_widget.entity.collapsible_key\n        self.ignore_input_changes = entity_widget.ignore_input_changes\n\n        self.store_as_list = store_as_list\n        self.is_duplicated = False\n        self.key_is_valid = store_as_list\n\n        self.confirm_btn = None\n\n        if self.collapsible_key:\n            self.create_collapsible_ui()\n        else:\n            self.create_addible_ui()\n\n    def add_new_item(self, key=None, label=None):\n        input_field = self.entity_widget.add_new_key(key, label)\n        if self.collapsible_key:\n            self.key_input.setFocus()\n        else:\n            input_field.key_input.setFocus()\n        return input_field\n\n    def _on_add_clicked(self):\n        self.add_new_item()\n\n    def create_addible_ui(self):\n        add_btn = create_add_btn(self)\n        remove_btn = create_remove_btn(self)\n\n        remove_btn.setEnabled(False)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(3)\n        layout.addWidget(add_btn, 0)\n        layout.addWidget(remove_btn, 0)\n        layout.addStretch(1)\n\n        add_btn.clicked.connect(self._on_add_clicked)\n\n        self.add_btn = add_btn\n        self.remove_btn = remove_btn\n\n    def _on_focus_lose(self):\n        if self.key_input.hasFocus() or self.key_label_input.hasFocus():\n            return\n        self._on_enter_press()\n\n    def _on_enter_press(self):\n        if not self.collapsible_key:\n            return\n\n        if self.is_duplicated:\n            return\n\n        if not self.key_is_valid:\n            return\n\n        key = self.key_input.text()\n        if key:\n            label = self.key_label_input.text()\n            self.key_input.clear()\n            self.key_label_input.clear()\n            self.add_new_item(key, label)\n\n    def _on_key_change(self):\n        key = self.key_input.text()\n        if not self.store_as_list:\n            self.key_is_valid = KEY_REGEX.match(key)\n\n        if self.ignore_input_changes:\n            return\n\n        self.is_duplicated = self.entity_widget.is_key_duplicated(key)\n        key_input_state = \"\"\n        # Collapsible key and empty key are not invalid\n        key_value = self.key_input.text()\n        if self.confirm_btn is not None:\n            conf_disabled = (\n                key_value == \"\"\n                or not self.key_is_valid\n                or self.is_duplicated\n            )\n            self.confirm_btn.setEnabled(not conf_disabled)\n\n        if self.collapsible_key and key_value == \"\":\n            pass\n        elif self.is_duplicated or not self.key_is_valid:\n            key_input_state = \"invalid\"\n        elif key != \"\":\n            key_input_state = \"modified\"\n\n        self.key_input.setProperty(\"state\", key_input_state)\n        self.key_input.style().polish(self.key_input)\n\n    def create_collapsible_ui(self):\n        key_input = QtWidgets.QLineEdit(self)\n        key_input.setObjectName(\"DictKey\")\n        key_input.setToolTip(KEY_INPUT_TOOLTIP)\n\n        key_label_input = QtWidgets.QLineEdit(self)\n\n        def key_input_focused_out(event):\n            QtWidgets.QLineEdit.focusOutEvent(key_input, event)\n            self._on_focus_lose()\n\n        def key_label_input_focused_out(event):\n            QtWidgets.QLineEdit.focusOutEvent(key_label_input, event)\n            self._on_focus_lose()\n\n        key_input.focusOutEvent = key_input_focused_out\n        key_label_input.focusOutEvent = key_label_input_focused_out\n\n        key_input_label_widget = QtWidgets.QLabel(\"Key:\", self)\n        key_label_input_label_widget = QtWidgets.QLabel(\"Label:\", self)\n\n        confirm_btn = create_confirm_btn(self)\n        confirm_btn.setEnabled(False)\n\n        wrapper_widget = ExpandingWidget(\"\", self)\n        wrapper_widget.add_widget_after_label(key_input_label_widget)\n        wrapper_widget.add_widget_after_label(key_input)\n        wrapper_widget.add_widget_after_label(key_label_input_label_widget)\n        wrapper_widget.add_widget_after_label(key_label_input)\n        wrapper_widget.add_widget_after_label(confirm_btn)\n        wrapper_widget.hide_toolbox()\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(3)\n        layout.addWidget(wrapper_widget)\n\n        key_input.textChanged.connect(self._on_key_change)\n        key_input.returnPressed.connect(self._on_enter_press)\n        key_label_input.returnPressed.connect(self._on_enter_press)\n\n        confirm_btn.clicked.connect(self._on_enter_press)\n\n        self.key_input = key_input\n        self.key_label_input = key_label_input\n        self.wrapper_widget = wrapper_widget\n        self.confirm_btn = confirm_btn\n\n\nclass ModifiableDictItem(QtWidgets.QWidget):\n    def __init__(self, collapsible_key, store_as_list, entity, entity_widget):\n        super(ModifiableDictItem, self).__init__(entity_widget.content_widget)\n\n        self.store_as_list = store_as_list\n\n        self.collapsible_key = collapsible_key\n        self.entity = entity\n        self.entity_widget = entity_widget\n\n        self.ignore_input_changes = entity_widget.ignore_input_changes\n\n        self.is_key_duplicated = False\n        self.key_is_valid = store_as_list\n        self.is_required = False\n\n        self.origin_key = None\n        self.origin_key_label = None\n\n        self.temp_key = \"\"\n        self.uuid_key = None\n\n        self._style_state = \"\"\n\n        self.wrapper_widget = None\n\n        self.key_label_input = None\n\n        self.confirm_btn = None\n\n        self._key_change_timer = create_deffered_value_change_timer(\n            self._on_timeout\n        )\n\n        if collapsible_key:\n            self.create_collapsible_ui()\n        else:\n            self.create_addible_ui()\n\n        self.key_input.setToolTip(KEY_INPUT_TOOLTIP)\n        self.update_style()\n\n    @property\n    def category_widget(self):\n        return self.entity_widget.category_widget\n\n    def create_ui_for_entity(self, *args, **kwargs):\n        return self.entity_widget.create_ui_for_entity(*args, **kwargs)\n\n    def create_addible_ui(self):\n        key_input = QtWidgets.QLineEdit(self)\n        key_input.setObjectName(\"DictKey\")\n\n        add_btn = create_add_btn(self)\n        remove_btn = create_remove_btn(self)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(3)\n        layout.addWidget(add_btn, 0)\n        layout.addWidget(remove_btn, 0)\n        layout.addWidget(key_input, 0)\n\n        key_input.textChanged.connect(self._on_key_change)\n        key_input.returnPressed.connect(self._on_enter_press)\n\n        add_btn.clicked.connect(self.on_add_clicked)\n        remove_btn.clicked.connect(self.on_remove_clicked)\n\n        self.key_input = key_input\n        self.add_btn = add_btn\n        self.remove_btn = remove_btn\n\n        self.content_widget = self\n        self.content_layout = layout\n\n        self.input_field = self.create_ui_for_entity(\n            self.category_widget, self.entity, self\n        )\n\n    def add_widget_to_layout(self, widget, label=None):\n        self.content_layout.addWidget(widget, 1)\n        self.setFocusProxy(widget)\n\n    def create_collapsible_ui(self):\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(3)\n\n        key_input = QtWidgets.QLineEdit(self)\n        key_input.setObjectName(\"DictKey\")\n\n        key_label_input = QtWidgets.QLineEdit(self)\n\n        wrapper_widget = ExpandingWidget(\"\", self)\n        layout.addWidget(wrapper_widget)\n\n        content_widget = QtWidgets.QWidget(wrapper_widget)\n        content_widget.setObjectName(\"ContentWidget\")\n        content_layout = QtWidgets.QHBoxLayout(content_widget)\n        content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0)\n        content_layout.setSpacing(5)\n\n        wrapper_widget.set_content_widget(content_widget)\n\n        def key_input_focused_out(event):\n            QtWidgets.QLineEdit.focusOutEvent(key_input, event)\n            self._on_focus_lose()\n\n        def key_label_input_focused_out(event):\n            QtWidgets.QLineEdit.focusOutEvent(key_label_input, event)\n            self._on_focus_lose()\n\n        key_input.focusOutEvent = key_input_focused_out\n        key_label_input.focusOutEvent = key_label_input_focused_out\n\n        edit_btn = IconButton(\n            \"fa.edit\", QtCore.Qt.lightGray, QtCore.Qt.white\n        )\n        edit_btn.setFocusPolicy(QtCore.Qt.ClickFocus)\n        edit_btn.setObjectName(\"SettingsToolIconBtn\")\n        edit_btn.setFixedHeight(BTN_FIXED_SIZE)\n\n        confirm_btn = create_confirm_btn(self)\n        confirm_btn.setVisible(False)\n\n        remove_btn = create_remove_btn(self)\n\n        key_input_label_widget = QtWidgets.QLabel(\"Key:\")\n        key_label_input_label_widget = QtWidgets.QLabel(\"Label:\")\n        wrapper_widget.add_widget_before_label(edit_btn)\n        wrapper_widget.add_widget_after_label(key_input_label_widget)\n        wrapper_widget.add_widget_after_label(key_input)\n        wrapper_widget.add_widget_after_label(key_label_input_label_widget)\n        wrapper_widget.add_widget_after_label(key_label_input)\n        wrapper_widget.add_widget_after_label(confirm_btn)\n        wrapper_widget.add_widget_after_label(remove_btn)\n\n        key_input.textChanged.connect(self._on_key_change)\n        key_input.returnPressed.connect(self._on_enter_press)\n\n        key_label_input.textChanged.connect(self._on_key_label_change)\n        key_label_input.returnPressed.connect(self._on_enter_press)\n\n        edit_btn.clicked.connect(self.on_edit_pressed)\n        confirm_btn.clicked.connect(self._on_enter_press)\n        remove_btn.clicked.connect(self.on_remove_clicked)\n\n        # Hide edit inputs\n        key_input.setVisible(False)\n        key_input_label_widget.setVisible(False)\n        key_label_input.setVisible(False)\n        key_label_input_label_widget.setVisible(False)\n        remove_btn.setVisible(False)\n\n        self.key_input = key_input\n        self.key_input_label_widget = key_input_label_widget\n        self.key_label_input = key_label_input\n        self.key_label_input_label_widget = key_label_input_label_widget\n        self.wrapper_widget = wrapper_widget\n        self.edit_btn = edit_btn\n        self.confirm_btn = confirm_btn\n        self.remove_btn = remove_btn\n\n        self.content_widget = content_widget\n        self.content_layout = content_layout\n\n        self.input_field = self.create_ui_for_entity(\n            self.category_widget, self.entity, self\n        )\n\n    def make_sure_is_visible(self, *args, **kwargs):\n        return self.input_field.make_sure_is_visible(*args, **kwargs)\n\n    def get_style_state(self):\n        if self.is_invalid:\n            return \"invalid\"\n        if self.entity.has_unsaved_changes:\n            return \"modified\"\n        if self.entity.has_project_override:\n            return \"overridden\"\n        if self.entity.has_studio_override:\n            return \"studio\"\n        return \"\"\n\n    def key_value(self):\n        return self.key_input.text()\n\n    def set_key(self, key):\n        if self.uuid_key is not None and key == self.uuid_key:\n            self.key_input.setText(\"\")\n        else:\n            self.key_input.setText(key)\n\n    def set_entity_value(self):\n        self.input_field.set_entity_value()\n\n    def set_is_key_duplicated(self, is_key_duplicated):\n        if is_key_duplicated == self.is_key_duplicated:\n            return\n\n        self.is_key_duplicated = is_key_duplicated\n        if self.collapsible_key:\n            if is_key_duplicated:\n                self.set_edit_mode(True)\n            else:\n                self._on_focus_lose()\n\n        if not self.is_key_duplicated and self.key_is_valid:\n            self.entity_widget.change_key(self.key_value(), self)\n\n    def set_key_label(self, key, label):\n        self.set_key(key)\n        self.set_label(label)\n        self.set_edit_mode(False)\n\n    def set_label(self, label):\n        if self.key_label_input and label is not None:\n            self.key_label_input.setText(label)\n        self.update_key_label()\n\n    def set_as_required(self, key):\n        self.key_input.setText(key)\n        self.key_input.setEnabled(False)\n        self.is_required = True\n\n        if self.collapsible_key:\n            self.remove_btn.setVisible(False)\n        else:\n            self.remove_btn.setEnabled(False)\n            self.add_btn.setEnabled(False)\n\n    def set_as_last_required(self):\n        if not self.collapsible_key:\n            self.add_btn.setEnabled(True)\n\n    def _on_focus_lose(self):\n        if (\n            self.edit_btn.hasFocus()\n            or self.key_input.hasFocus()\n            or self.key_label_input.hasFocus()\n            or self.remove_btn.hasFocus()\n        ):\n            return\n        self._on_enter_press()\n\n    def _on_enter_press(self):\n        if self.collapsible_key:\n            self.set_edit_mode(False)\n\n    def _on_key_label_change(self):\n        if self.ignore_input_changes:\n            return\n\n        label = self.key_label_value()\n        self.entity_widget.change_label(label, self)\n        self.update_key_label()\n\n    def _on_key_change(self):\n        key = self.key_value()\n        if not self.store_as_list:\n            self.key_is_valid = KEY_REGEX.match(key)\n\n        if self.ignore_input_changes:\n            return\n\n        self._key_change_timer.start()\n\n    def _on_timeout(self):\n        key = self.key_value()\n        is_key_duplicated = self.entity_widget.validate_key_duplication(\n            self.temp_key, key, self\n        )\n        self.temp_key = key\n        if self.confirm_btn is not None:\n            conf_disabled = (\n                key == \"\"\n                or not self.key_is_valid\n                or is_key_duplicated\n            )\n            self.confirm_btn.setEnabled(not conf_disabled)\n\n        if is_key_duplicated or not self.key_is_valid:\n            return\n\n        if key:\n            self.update_key_label()\n\n        self.entity_widget.change_key(key, self)\n        self.update_style()\n\n    def update_key_label(self):\n        if not self.collapsible_key:\n            return\n        key_value = self.key_input.text()\n        key_label_value = self.key_label_input.text()\n        if key_label_value:\n            label = \"{} ({})\".format(key_value, key_label_value)\n        else:\n            label = key_value\n        self.wrapper_widget.label_widget.setText(label)\n\n    def on_add_clicked(self):\n        widget = self.entity_widget.add_new_key(None, None)\n        widget.key_input.setFocus()\n\n    def on_edit_pressed(self):\n        if not self.key_input.isVisible():\n            self.set_edit_mode()\n        else:\n            self.key_input.setFocus()\n\n    def set_edit_mode(self, enabled=True):\n        if self.is_invalid and not enabled:\n            return\n        self.wrapper_widget.label_widget.setVisible(not enabled)\n        self.key_label_input_label_widget.setVisible(enabled)\n        self.key_input.setVisible(enabled)\n        self.key_input_label_widget.setVisible(enabled)\n        self.key_label_input.setVisible(enabled)\n        self.confirm_btn.setVisible(enabled)\n        if not self.is_required:\n            self.remove_btn.setVisible(enabled)\n        if enabled:\n            if self.key_input.isEnabled():\n                self.key_input.setFocus()\n            else:\n                self.key_label_input.setFocus()\n\n    def on_remove_clicked(self):\n        self.entity_widget.remove_key(self)\n\n    def is_key_modified(self):\n        return self.key_value() != self.origin_key\n\n    def is_key_label_modified(self):\n        return self.key_label_value() != self.origin_key_label\n\n    def trigger_hierarchical_style_update(self):\n        self.entity_widget.trigger_hierarchical_style_update()\n\n    def hierarchical_style_update(self):\n        self.input_field.hierarchical_style_update()\n        self.update_style()\n\n    @property\n    def is_invalid(self):\n        return (\n            self.is_key_duplicated\n            or self.key_value() == \"\"\n            or self.child_invalid\n            or not self.key_is_valid\n        )\n\n    @property\n    def child_invalid(self):\n        return self.input_field.is_invalid\n\n    def get_invalid(self):\n        invalid = []\n        if self.is_key_duplicated or self.key_value() == \"\":\n            invalid.append(self.key_input)\n        invalid.extend(self.input_field.get_invalid())\n        return invalid\n\n    def update_style(self):\n        key_input_state = \"\"\n        if (\n            self.is_key_duplicated\n            or self.key_value() == \"\"\n            or not self.key_is_valid\n        ):\n            key_input_state = \"invalid\"\n        elif self.is_key_modified():\n            key_input_state = \"modified\"\n\n        self.key_input.setProperty(\"state\", key_input_state)\n        self.key_input.style().polish(self.key_input)\n\n        if not self.wrapper_widget:\n            return\n\n        state = self.get_style_state()\n\n        if self._style_state == state:\n            return\n\n        self._style_state = state\n\n        if self.wrapper_widget.label_widget:\n            self.wrapper_widget.label_widget.setProperty(\"state\", state)\n            self.wrapper_widget.label_widget.style().polish(\n                self.wrapper_widget.label_widget\n            )\n\n        if state:\n            child_state = \"child-{}\".format(state)\n        else:\n            child_state = \"\"\n\n        self.wrapper_widget.side_line_widget.setProperty(\"state\", child_state)\n        self.wrapper_widget.side_line_widget.style().polish(\n            self.wrapper_widget.side_line_widget\n        )\n\n    def row(self):\n        return self.entity_widget.input_fields.index(self)\n\n    def key_label_value(self):\n        if self.key_label_input:\n            return self.key_label_input.text()\n        return None\n\n    def mouseReleaseEvent(self, event):\n        return QtWidgets.QWidget.mouseReleaseEvent(self, event)\n\n\nclass DictMutableKeysWidget(BaseWidget):\n    def create_ui(self):\n        self.input_fields = []\n        self.required_inputs_by_key = {}\n\n        if self.entity.highlight_content:\n            content_state = \"highlighted\"\n            bottom_margin = 5\n        else:\n            content_state = \"\"\n            bottom_margin = 0\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n\n        label = self.entity.label\n        body_widget = None\n        content_left_margin = 0\n        if label:\n            content_left_margin = CHILD_OFFSET\n            body_widget = ExpandingWidget(label, self)\n            main_layout.addWidget(body_widget)\n            label = None\n            self.label_widget = body_widget.label_widget\n\n        if body_widget is None:\n            content_parent_widget = self\n        else:\n            content_parent_widget = body_widget\n\n        content_widget = QtWidgets.QWidget(content_parent_widget)\n        content_widget.setObjectName(\"ContentWidget\")\n        content_widget.setProperty(\"content_state\", content_state)\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.setContentsMargins(\n            content_left_margin, 5, 0, bottom_margin\n        )\n\n        if body_widget is None:\n            main_layout.addWidget(content_widget)\n        else:\n            body_widget.set_content_widget(content_widget)\n\n        self.body_widget = body_widget\n        self.content_widget = content_widget\n        self.content_layout = content_layout\n\n        if body_widget:\n            if not self.entity.collapsible:\n                body_widget.hide_toolbox(hide_content=False)\n            elif not self.entity.collapsed:\n                body_widget.toggle_content()\n\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        self.add_required_keys()\n\n        self.empty_row = ModifiableDictEmptyItem(\n            self, self.entity.store_as_list, self.content_widget\n        )\n        self.content_layout.addWidget(self.empty_row)\n\n        self.entity_widget.add_widget_to_layout(self, label)\n\n    @property\n    def is_invalid(self):\n        return self._is_invalid or self.child_invalid\n\n    @property\n    def child_invalid(self):\n        for input_field in self.input_fields:\n            if input_field.is_invalid:\n                return True\n        return False\n\n    def get_invalid(self):\n        invalid = []\n        for input_field in self.input_fields:\n            invalid.extend(input_field.get_invalid())\n        return invalid\n\n    def add_required_keys(self):\n        # TODO implement\n        pass\n\n    def add_new_key(self, key, label=None):\n        uuid_key = None\n        entity_key = key\n        if not key:\n            uuid_key = str(uuid4())\n            entity_key = uuid_key\n\n        # Creating of new item will trigger entity change so input field is\n        #   created at this moment\n        child_entity = self.entity.add_key(entity_key)\n\n        # Find created item\n        input_field = None\n        for _input_field in self.input_fields:\n            if _input_field.entity is child_entity:\n                input_field = _input_field\n                break\n\n        # Backup solution (for testing)\n        if input_field is None:\n            input_field = self.add_widget_for_child(child_entity)\n\n        if key:\n            # Happens when created from collapsible key items where key\n            #   must be entered to be able create new item.\n            input_field.set_key_label(key, label)\n\n        elif uuid_key:\n            # Happens when clicked on add button from non-collapsible item.\n            input_field.uuid_key = uuid_key\n            # Change key to empty string\n            input_field.set_key(\"\")\n\n        input_field.set_entity_value()\n\n        self.on_shuffle()\n\n        return input_field\n\n    def remove_key(self, widget):\n        key = self.entity.get_child_key(widget.entity)\n        self.entity.pop(key)\n\n    def change_key(self, new_key, widget):\n        if not new_key or widget.is_key_duplicated:\n            return\n\n        child_obj = self.entity.get(new_key)\n        # Skip if same key is already stored under the key\n        if child_obj is widget.entity:\n            return\n\n        # Just change the key if not exist the same object\n        if not child_obj:\n            self.entity.change_child_key(widget.entity, new_key)\n            return\n\n        same_key_widget = None\n        for input_field in self.input_fields:\n            if input_field.entity is child_obj:\n                same_key_widget = input_field\n                break\n\n        if not same_key_widget:\n            # Would mean that child entity does not have input field in\n            # this widget!\n            raise KeyError(\"BUG: didn't find same key widget!\")\n\n        if same_key_widget.is_key_duplicated:\n            return\n\n        sk_new_key = same_key_widget.key_value()\n        sk_old_key = self.entity.get_child_key(same_key_widget.entity)\n        if sk_old_key != new_key:\n            self.change_key(sk_new_key, same_key_widget)\n            self.entity.change_child_key(widget.entity, new_key)\n        else:\n            # Swap entities if keys of each other are matching\n            old_key = self.entity.get_child_key(widget.entity)\n            (\n                self.entity.children_by_key[new_key],\n                self.entity.children_by_key[sk_new_key]\n            ) = (\n                self.entity.children_by_key[old_key],\n                self.entity.children_by_key[sk_old_key]\n            )\n\n    def change_label(self, label, input_field):\n        entity = input_field.entity\n        _label = self.entity.get_child_label(entity)\n        if _label == label:\n            return\n        self.entity.set_child_label(entity, label)\n\n    def add_widget_for_child(self, child_entity):\n        input_field = ModifiableDictItem(\n            self.entity.collapsible_key, self.entity.store_as_list,\n            child_entity, self\n        )\n        self.input_fields.append(input_field)\n\n        new_widget_index = self.content_layout.count() - 1\n\n        self.content_layout.insertWidget(new_widget_index, input_field)\n\n        return input_field\n\n    def remove_row(self, widget):\n        if widget.is_key_duplicated:\n            new_key = widget.uuid_key\n            if new_key is None:\n                new_key = str(uuid4())\n            self.validate_key_duplication(widget.temp_key, new_key, widget)\n        self.input_fields.remove(widget)\n        self.content_layout.removeWidget(widget)\n        widget.deleteLater()\n        self.on_shuffle()\n\n    def is_key_duplicated(self, key):\n        \"\"\"Method meant only for empty item to check duplicated keys.\"\"\"\n        for input_field in self.input_fields:\n            item_key = input_field.key_value()\n            if item_key == key:\n                return True\n        return False\n\n    def validate_key_duplication(self, old_key, new_key, widget):\n        old_key_items = []\n        duplicated_items = []\n        for input_field in self.input_fields:\n            if input_field is widget:\n                continue\n\n            item_key = input_field.key_value()\n            if item_key == new_key:\n                duplicated_items.append(input_field)\n            elif item_key == old_key:\n                old_key_items.append(input_field)\n\n        if duplicated_items:\n            for input_field in duplicated_items:\n                input_field.set_is_key_duplicated(True)\n            widget.set_is_key_duplicated(True)\n        else:\n            widget.set_is_key_duplicated(False)\n\n        if len(old_key_items) == 1:\n            for input_field in old_key_items:\n                input_field.set_is_key_duplicated(False)\n                input_field.set_key(old_key)\n                input_field.update_key_label()\n        self.trigger_hierarchical_style_update()\n        return bool(duplicated_items)\n\n    def on_shuffle(self):\n        if not self.entity.collapsible_key:\n            self.empty_row.setVisible(len(self.input_fields) == 0)\n        self.update_style()\n\n    def _on_entity_change(self):\n        changed = False\n        to_remove = []\n        for input_field in self.input_fields:\n            found = False\n            for child_entity in self.entity.values():\n                if child_entity is input_field.entity:\n                    found = True\n                    break\n\n            if not found:\n                to_remove.append(input_field)\n\n        for input_field in to_remove:\n            changed = True\n            self.remove_row(input_field)\n\n        for key, child_entity in self.entity.items():\n            found = False\n            for input_field in self.input_fields:\n                if input_field.entity is child_entity:\n                    found = True\n                    break\n\n            if not found:\n                changed = True\n\n                _input_field = self.add_widget_for_child(child_entity)\n                _input_field.origin_key = key\n                _input_field.set_key(key)\n                if self.entity.collapsible_key:\n                    label = self.entity.get_child_label(child_entity)\n                    _input_field.origin_key_label = label\n                    _input_field.set_label(label)\n                _input_field.set_entity_value()\n\n            else:\n                if (\n                    not input_field.is_key_duplicated\n                    and input_field.key_value() != key\n                ):\n                    changed = True\n                    input_field.set_key(key)\n\n                if self.entity.collapsible_key:\n                    label = self.entity.get_child_label(child_entity)\n                    if input_field.key_label_value() != label:\n                        input_field.set_label(label)\n\n        if changed:\n            self.on_shuffle()\n\n    def make_sure_is_visible(self, path, scroll_to):\n        if not path:\n            return False\n\n        entity_path = self.entity.path\n        if entity_path == path:\n            self.set_focus(scroll_to)\n            return True\n\n        if not path.startswith(entity_path):\n            return False\n\n        if self.body_widget and not self.body_widget.is_expanded():\n            self.body_widget.toggle_content(True)\n\n        for input_field in self.input_fields:\n            if input_field.make_sure_is_visible(path, scroll_to):\n                return True\n        return False\n\n    def set_entity_value(self):\n        while self.input_fields:\n            self.remove_row(self.input_fields[0])\n\n        keys_order = list(self.entity.required_keys)\n        last_required = None\n        if keys_order:\n            last_required = keys_order[-1]\n        for key in self.entity.keys():\n            if key not in keys_order:\n                keys_order.append(key)\n\n        for key in keys_order:\n            child_entity = self.entity[key]\n            input_field = self.add_widget_for_child(child_entity)\n            input_field.origin_key = key\n            if key in self.entity.required_keys:\n                input_field.set_as_required(key)\n                if key == last_required:\n                    input_field.set_as_last_required()\n            else:\n                input_field.set_key(key)\n            if self.entity.collapsible_key:\n                label = self.entity.get_child_label(child_entity)\n                input_field.origin_key_label = label\n                input_field.set_label(label)\n\n        for input_field in self.input_fields:\n            input_field.set_entity_value()\n        self.on_shuffle()\n\n    def hierarchical_style_update(self):\n        for input_field in self.input_fields:\n            input_field.hierarchical_style_update()\n        self.update_style()\n\n    def update_style(self):\n        _style_state = self.get_style_state(\n            self.is_invalid,\n            self.entity.has_unsaved_changes,\n            self.entity.has_project_override,\n            self.entity.has_studio_override\n        )\n\n        if self._style_state == _style_state:\n            return\n\n        self._style_state = _style_state\n\n        if self.label_widget:\n            self.label_widget.setProperty(\"state\", _style_state)\n            self.label_widget.style().polish(self.label_widget)\n\n        if not self.body_widget:\n            return\n\n        if _style_state:\n            child_state = \"child-{}\".format(_style_state)\n        else:\n            child_state = \"\"\n\n        self.body_widget.side_line_widget.setProperty(\"state\", child_state)\n        self.body_widget.side_line_widget.style().polish(\n            self.body_widget.side_line_widget\n        )\n"
  },
  {
    "path": "openpype/tools/settings/settings/images/__init__.py",
    "content": "import os\nfrom qtpy import QtGui\n\n\ndef get_image_path(image_filename):\n    return os.path.join(\n        os.path.dirname(os.path.abspath(__file__)),\n        image_filename\n    )\n\n\ndef get_image(image_filename):\n    image_path = get_image_path(image_filename)\n    return QtGui.QImage(image_path)\n\n\ndef get_pixmap(image_filename):\n    image_path = get_image_path(image_filename)\n    return QtGui.QPixmap(image_path)\n"
  },
  {
    "path": "openpype/tools/settings/settings/item_widgets.py",
    "content": "import json\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.widgets.sliders import NiceSlider\nfrom openpype.tools.settings import CHILD_OFFSET\nfrom openpype.tools.utils import MultiSelectionComboBox\nfrom openpype.settings.entities.exceptions import BaseInvalidValue\n\nfrom .widgets import (\n    ExpandingWidget,\n    NumberSpinBox,\n    GridLabelWidget,\n    SettingsComboBox,\n    SettingsPlainTextEdit,\n    SettingsNiceCheckbox,\n    SettingsLineEdit\n)\nfrom .wrapper_widgets import (\n    WrapperWidget,\n    CollapsibleWrapper,\n    FormWrapper\n)\nfrom .base import (\n    BaseWidget,\n    InputWidget\n)\n\n\nclass DictImmutableKeysWidget(BaseWidget):\n    def create_ui(self):\n        self.input_fields = []\n        self.checkbox_child = None\n\n        self.label_widget = None\n        self.body_widget = None\n        self.content_widget = None\n        self.content_layout = None\n\n        label = None\n        if self.entity.is_dynamic_item:\n            self._ui_as_dynamic_item()\n\n        elif self.entity.use_label_wrap:\n            self._ui_label_wrap()\n            self.checkbox_child = self.entity.non_gui_children.get(\n                self.entity.checkbox_key\n            )\n\n        else:\n            self._ui_item_base()\n            label = self.entity.label\n\n        # Set stretch of second column to 1\n        if isinstance(self.content_layout, QtWidgets.QGridLayout):\n            self.content_layout.setColumnStretch(1, 1)\n\n        self._direct_children_widgets = []\n        self._parent_widget_by_entity_id = {}\n        self._added_wrapper_ids = set()\n        self._prepare_entity_layouts(\n            self.entity.gui_layout, self.content_widget\n        )\n\n        for child_obj in self.entity.children:\n            self.input_fields.append(\n                self.create_ui_for_entity(\n                    self.category_widget, child_obj, self\n                )\n            )\n\n        if self.entity.use_label_wrap and self.content_layout.count() == 0:\n            self.body_widget.hide_toolbox(True)\n\n        self.entity_widget.add_widget_to_layout(self, label)\n\n    def _prepare_entity_layouts(self, children, widget):\n        for child in children:\n            if not isinstance(child, dict):\n                if child is not self.checkbox_child:\n                    self._parent_widget_by_entity_id[child.id] = widget\n                continue\n\n            if child[\"type\"] == \"collapsible-wrap\":\n                wrapper = CollapsibleWrapper(child, widget)\n\n            elif child[\"type\"] == \"form\":\n                wrapper = FormWrapper(child, widget)\n\n            else:\n                raise KeyError(\n                    \"Unknown Wrapper type \\\"{}\\\"\".format(child[\"type\"])\n                )\n\n            self._parent_widget_by_entity_id[wrapper.id] = widget\n\n            self._prepare_entity_layouts(child[\"children\"], wrapper)\n\n    def set_focus(self, scroll_to=False):\n        \"\"\"Set focus of a widget.\n\n        Args:\n            scroll_to(bool): Also scroll to widget in category widget.\n        \"\"\"\n        if self.body_widget:\n            if scroll_to:\n                self.scroll_to(self.body_widget.top_part)\n            self.body_widget.top_part.setFocus()\n\n        else:\n            if scroll_to:\n                if not self.input_fields:\n                    self.scroll_to(self)\n                else:\n                    self.scroll_to(self.input_fields[0])\n            self.setFocus()\n\n    def _ui_item_base(self):\n        self.setObjectName(\"DictInvisible\")\n\n        self.content_widget = self\n        self.content_layout = QtWidgets.QGridLayout(self)\n        self.content_layout.setContentsMargins(0, 0, 0, 0)\n        self.content_layout.setSpacing(5)\n\n    def _ui_as_dynamic_item(self):\n        content_widget = QtWidgets.QWidget(self)\n        content_widget.setObjectName(\"DictAsWidgetBody\")\n\n        show_borders = str(int(self.entity.show_borders))\n        content_widget.setProperty(\"show_borders\", show_borders)\n\n        label_widget = QtWidgets.QLabel(self.entity.label)\n        label_widget.setObjectName(\"SettingsLabel\")\n\n        content_layout = QtWidgets.QGridLayout(content_widget)\n        content_layout.setContentsMargins(5, 5, 5, 5)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(5)\n        main_layout.addWidget(content_widget)\n\n        self.label_widget = label_widget\n        self.content_widget = content_widget\n        self.content_layout = content_layout\n\n    def _ui_label_wrap(self):\n        content_widget = QtWidgets.QWidget(self)\n        content_widget.setObjectName(\"ContentWidget\")\n\n        if self.entity.highlight_content:\n            content_state = \"highlighted\"\n            bottom_margin = 5\n        else:\n            content_state = \"\"\n            bottom_margin = 0\n        content_widget.setProperty(\"content_state\", content_state)\n        content_layout_margins = (CHILD_OFFSET, 5, 0, bottom_margin)\n\n        body_widget = ExpandingWidget(self.entity.label, self)\n        label_widget = body_widget.label_widget\n        body_widget.set_content_widget(content_widget)\n\n        content_layout = QtWidgets.QGridLayout(content_widget)\n        content_layout.setContentsMargins(*content_layout_margins)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n        main_layout.addWidget(body_widget)\n\n        self.label_widget = label_widget\n        self.body_widget = body_widget\n        self.content_widget = content_widget\n        self.content_layout = content_layout\n\n        if len(self.input_fields) == 1 and self.checkbox_child:\n            body_widget.hide_toolbox(hide_content=True)\n\n        elif self.entity.collapsible:\n            if not self.entity.collapsed:\n                body_widget.toggle_content()\n        else:\n            body_widget.hide_toolbox(hide_content=False)\n\n    def make_sure_is_visible(self, path, scroll_to):\n        if not path:\n            return False\n\n        entity_path = self.entity.path\n        if entity_path == path:\n            self.set_focus(scroll_to)\n            return True\n\n        if not path.startswith(entity_path):\n            return False\n\n        is_checkbox_child = False\n        changed = False\n        for direct_child in self._direct_children_widgets:\n            if direct_child.make_sure_is_visible(path, scroll_to):\n                changed = True\n                if direct_child.entity is self.checkbox_child:\n                    is_checkbox_child = True\n                break\n\n        # Change scroll to this widget\n        if is_checkbox_child:\n            self.scroll_to(self)\n\n        elif self.body_widget and not self.body_widget.is_expanded():\n            # Expand widget if is callapsible\n            self.body_widget.toggle_content(True)\n\n        return changed\n\n    def add_widget_to_layout(self, widget, label=None):\n        if self.checkbox_child and widget.entity is self.checkbox_child:\n            self.body_widget.add_widget_before_label(widget)\n            self._direct_children_widgets.append(widget)\n            return\n\n        if not widget.entity:\n            map_id = widget.id\n        else:\n            map_id = widget.entity.id\n\n        wrapper = self._parent_widget_by_entity_id[map_id]\n        if wrapper is not self.content_widget:\n            wrapper.add_widget_to_layout(widget, label)\n            if wrapper.id not in self._added_wrapper_ids:\n                self.add_widget_to_layout(wrapper)\n                self._added_wrapper_ids.add(wrapper.id)\n            return\n\n        self._direct_children_widgets.append(widget)\n\n        row = self.content_layout.rowCount()\n        if not label or isinstance(widget, WrapperWidget):\n            self.content_layout.addWidget(widget, row, 0, 1, 2)\n        else:\n            label_widget = GridLabelWidget(label, widget)\n            label_widget.input_field = widget\n            widget.label_widget = label_widget\n            self.content_layout.addWidget(label_widget, row, 0, 1, 1)\n            self.content_layout.addWidget(widget, row, 1, 1, 1)\n\n    def set_entity_value(self):\n        for input_field in self.input_fields:\n            input_field.set_entity_value()\n\n    def hierarchical_style_update(self):\n        self.update_style()\n        for input_field in self.input_fields:\n            input_field.hierarchical_style_update()\n\n    def update_style(self):\n        if not self.body_widget and not self.label_widget:\n            return\n\n        if self.entity.group_item:\n            group_item = self.entity.group_item\n            has_unsaved_changes = group_item.has_unsaved_changes\n            has_project_override = group_item.has_project_override\n            has_studio_override = group_item.has_studio_override\n        else:\n            has_unsaved_changes = self.entity.has_unsaved_changes\n            has_project_override = self.entity.has_project_override\n            has_studio_override = self.entity.has_studio_override\n\n        style_state = self.get_style_state(\n            self.is_invalid,\n            has_unsaved_changes,\n            has_project_override,\n            has_studio_override\n        )\n        if self._style_state == style_state:\n            return\n\n        self._style_state = style_state\n\n        if self.body_widget:\n            if style_state:\n                child_style_state = \"child-{}\".format(style_state)\n            else:\n                child_style_state = \"\"\n\n            self.body_widget.side_line_widget.setProperty(\n                \"state\", child_style_state\n            )\n            self.body_widget.side_line_widget.style().polish(\n                self.body_widget.side_line_widget\n            )\n\n        # There is nothing to care if there is no label\n        if not self.label_widget:\n            return\n\n        # Don't change label if is not group or under group item\n        if not self.entity.is_group and not self.entity.group_item:\n            return\n\n        self.label_widget.setProperty(\"state\", style_state)\n        self.label_widget.style().polish(self.label_widget)\n\n    def _on_entity_change(self):\n        pass\n\n    @property\n    def is_invalid(self):\n        return self._is_invalid or self._child_invalid\n\n    @property\n    def _child_invalid(self):\n        for input_field in self.input_fields:\n            if input_field.is_invalid:\n                return True\n        return False\n\n    def get_invalid(self):\n        invalid = []\n        for input_field in self.input_fields:\n            invalid.extend(input_field.get_invalid())\n        return invalid\n\n\nclass BoolWidget(InputWidget):\n    def _add_inputs_to_layout(self):\n        self.input_field = SettingsNiceCheckbox(parent=self.content_widget)\n\n        self.content_layout.addWidget(self.input_field, 0)\n        self.content_layout.addStretch(1)\n\n        self.setFocusProxy(self.input_field)\n\n        self.input_field.focused_in.connect(self._on_input_focus)\n        self.input_field.stateChanged.connect(self._on_value_change)\n\n    def _on_input_focus(self):\n        self.focused_in()\n\n    def _on_entity_change(self):\n        if self.entity.value != self.input_field.isChecked():\n            self.set_entity_value()\n\n    def set_entity_value(self):\n        self.input_field.setChecked(self.entity.value)\n\n    def _on_value_change(self):\n        if self.ignore_input_changes:\n            return\n        self.start_value_timer()\n\n    def _on_value_change_timer(self):\n        self.entity.set(self.input_field.isChecked())\n\n\nclass TextWidget(InputWidget):\n    def _add_inputs_to_layout(self):\n        multiline = self.entity.multiline\n        if multiline:\n            input_field = SettingsPlainTextEdit(self.content_widget)\n            if self.entity.minimum_lines_count:\n                input_field.set_minimum_lines(self.entity.minimum_lines_count)\n        else:\n            input_field = SettingsLineEdit(self.content_widget)\n        placeholder_text = self.entity.placeholder_text\n        if placeholder_text:\n            input_field.setPlaceholderText(placeholder_text)\n\n        self.input_field = input_field\n        self.setFocusProxy(self.input_field)\n\n        layout_kwargs = {}\n        if multiline:\n            layout_kwargs[\"alignment\"] = QtCore.Qt.AlignTop\n\n        self.content_layout.addWidget(self.input_field, 1, **layout_kwargs)\n\n        self.input_field.focused_in.connect(self._on_input_focus)\n        self.input_field.textChanged.connect(self._on_value_change)\n\n        self._refresh_completer()\n\n    def _refresh_completer(self):\n        # Multiline entity can't have completer\n        #   - there is not space for this UI component\n        if self.entity.multiline:\n            return\n\n        self.input_field.update_completer_values(self.entity.value_hints)\n\n    def _on_input_focus(self):\n        self.focused_in()\n\n    def _on_entity_change(self):\n        if self.entity.value != self.input_value():\n            self.set_entity_value()\n\n    def set_entity_value(self):\n        if self.entity.multiline:\n            self.input_field.setPlainText(self.entity.value)\n        else:\n            self.input_field.setText(self.entity.value)\n\n    def input_value(self):\n        if self.entity.multiline:\n            return self.input_field.toPlainText()\n        else:\n            return self.input_field.text()\n\n    def _on_value_change(self):\n        if self.ignore_input_changes:\n            return\n        self.start_value_timer()\n\n    def _on_value_change_timer(self):\n        self.entity.set(self.input_value())\n\n\nclass OpenPypeVersionText(TextWidget):\n    def __init__(self, *args, **kwargs):\n        self._info_widget = None\n        super(OpenPypeVersionText, self).__init__(*args, **kwargs)\n\n    def create_ui(self):\n        super(OpenPypeVersionText, self).create_ui()\n        info_widget = QtWidgets.QLabel(self)\n        info_widget.setObjectName(\"OpenPypeVersionLabel\")\n        self.content_layout.addWidget(info_widget, 1)\n\n        self._info_widget = info_widget\n\n    def _update_info_widget(self):\n        value = self.input_value()\n\n        message = \"\"\n        tooltip = \"\"\n        state = None\n        if self._is_invalid:\n            message = \"Invalid OpenPype version format\"\n\n        elif value == \"\":\n            message = \"Use latest available version\"\n            tooltip = (\n                \"Latest version from OpenPype zip repository will be used\"\n            )\n\n        elif value in self.entity.value_hints:\n            state = \"success\"\n            message = \"Version {} will be used\".format(value)\n\n        else:\n            state = \"warning\"\n            message = (\n                \"Version {} not found in listed versions\".format(value)\n            )\n            if self.entity.value_hints:\n                tooltip = \"Listed versions: {}\".format(\", \".join(\n                    ['\"{}\"'.format(hint) for hint in self.entity.value_hints]\n                ))\n            else:\n                tooltip = \"No versions were listed\"\n\n        self._info_widget.setText(message)\n        self._info_widget.setToolTip(tooltip)\n        self.set_style_property(self._info_widget, \"state\", state)\n\n    def set_entity_value(self):\n        super(OpenPypeVersionText, self).set_entity_value()\n        self._invalidate()\n        self._update_info_widget()\n\n    def _on_value_change_timer(self):\n        value = self.input_value()\n        self._invalidate()\n        if not self.is_invalid:\n            self.entity.set(value)\n            self.update_style()\n        else:\n            # Manually trigger hierarchical style update\n            self.ignore_input_changes.set_ignore(True)\n            self.ignore_input_changes.set_ignore(False)\n\n        self._update_info_widget()\n\n    def _invalidate(self):\n        value = self.input_value()\n        try:\n            self.entity.convert_to_valid_type(value)\n            is_invalid = False\n        except BaseInvalidValue:\n            is_invalid = True\n        self._is_invalid = is_invalid\n\n    def _on_entity_change(self):\n        super(OpenPypeVersionText, self)._on_entity_change()\n        self._refresh_completer()\n\n\nclass NumberWidget(InputWidget):\n    _slider_widget = None\n\n    def _add_inputs_to_layout(self):\n        kwargs = {\n            \"minimum\": self.entity.minimum,\n            \"maximum\": self.entity.maximum,\n            \"decimal\": self.entity.decimal,\n            \"steps\": self.entity.steps\n        }\n        self.input_field = NumberSpinBox(self.content_widget, **kwargs)\n        input_field_stretch = 1\n\n        slider_multiplier = 1\n        if self.entity.show_slider:\n            # Slider can't handle float numbers so all decimals are converted\n            #   to integer range.\n            slider_multiplier = 10 ** self.entity.decimal\n            slider_widget = NiceSlider(QtCore.Qt.Horizontal, self)\n            slider_widget.setRange(\n                int(self.entity.minimum * slider_multiplier),\n                int(self.entity.maximum * slider_multiplier)\n            )\n            if self.entity.steps is not None:\n                slider_widget.setSingleStep(\n                    self.entity.steps * slider_multiplier\n                )\n\n            self.content_layout.addWidget(slider_widget, 1)\n\n            slider_widget.valueChanged.connect(self._on_slider_change)\n\n            self._slider_widget = slider_widget\n\n            input_field_stretch = 0\n\n        self._slider_multiplier = slider_multiplier\n\n        self.setFocusProxy(self.input_field)\n\n        self.content_layout.addWidget(self.input_field, input_field_stretch)\n\n        self.input_field.valueChanged.connect(self._on_value_change)\n        self.input_field.focused_in.connect(self._on_input_focus)\n\n        self._ignore_slider_change = False\n        self._ignore_input_change = False\n\n    def _on_input_focus(self):\n        self.focused_in()\n\n    def _on_entity_change(self):\n        if self.entity.value != self.input_field.value():\n            self.set_entity_value()\n\n    def set_entity_value(self):\n        self.input_field.setValue(self.entity.value)\n\n    def _on_slider_change(self, new_value):\n        if self._ignore_slider_change:\n            return\n\n        self._ignore_input_change = True\n        self.input_field.setValue(new_value / self._slider_multiplier)\n        self._ignore_input_change = False\n\n    def _on_value_change(self):\n        if self.ignore_input_changes:\n            return\n\n        self.start_value_timer()\n\n    def _on_value_change_timer(self):\n        value = self.input_field.value()\n        if self._slider_widget is not None and not self._ignore_input_change:\n            self._ignore_slider_change = True\n            self._slider_widget.setValue(value * self._slider_multiplier)\n            self._ignore_slider_change = False\n\n        self.entity.set(value)\n\n\nclass RawJsonInput(SettingsPlainTextEdit):\n    tab_length = 4\n\n    def __init__(self, valid_type, *args, **kwargs):\n        super(RawJsonInput, self).__init__(*args, **kwargs)\n        self.setObjectName(\"RawJsonInput\")\n        self.setTabStopDistance(\n            QtGui.QFontMetricsF(\n                self.font()\n            ).horizontalAdvance(\" \") * self.tab_length\n        )\n        self.valid_type = valid_type\n\n    def sizeHint(self):\n        document = self.document()\n        layout = document.documentLayout()\n\n        height = document.documentMargin() + 2 * self.frameWidth() + 1\n        block = document.begin()\n        while block != document.end():\n            height += layout.blockBoundingRect(block).height()\n            block = block.next()\n\n        hint = super(RawJsonInput, self).sizeHint()\n        hint.setHeight(height)\n\n        return hint\n\n    def set_value(self, value):\n        if not isinstance(value, str):\n            try:\n                value = json.dumps(value, indent=4)\n            except Exception:\n                value = \"\"\n        self.setPlainText(value)\n\n    def json_value(self):\n        return json.loads(self.toPlainText())\n\n    def has_invalid_value(self):\n        try:\n            value = self.json_value()\n            return not isinstance(value, self.valid_type)\n        except Exception:\n            return True\n\n    def resizeEvent(self, event):\n        self.updateGeometry()\n        super(RawJsonInput, self).resizeEvent(event)\n\n\nclass RawJsonWidget(InputWidget):\n    def _add_inputs_to_layout(self):\n        if self.entity.is_list:\n            valid_type = list\n        else:\n            valid_type = dict\n        self.input_field = RawJsonInput(valid_type, self.content_widget)\n        self.input_field.setSizePolicy(\n            QtWidgets.QSizePolicy.Minimum,\n            QtWidgets.QSizePolicy.MinimumExpanding\n        )\n        self.setFocusProxy(self.input_field)\n\n        self.content_layout.addWidget(\n            self.input_field, 1, alignment=QtCore.Qt.AlignTop\n        )\n\n        self.input_field.focused_in.connect(self._on_input_focus)\n        self.input_field.textChanged.connect(self._on_value_change)\n\n    def _on_input_focus(self):\n        self.focused_in()\n\n    def set_entity_value(self):\n        self.input_field.set_value(self.entity.value)\n        self._is_invalid = self.input_field.has_invalid_value()\n\n    def _on_entity_change(self):\n        if self.is_invalid:\n            self.set_entity_value()\n        else:\n            if self.entity.value != self.input_field.json_value():\n                self.set_entity_value()\n\n    def _on_value_change(self):\n        if self.ignore_input_changes:\n            return\n        self.start_value_timer()\n\n    def _on_value_change_timer(self):\n        self._is_invalid = self.input_field.has_invalid_value()\n        if not self.is_invalid:\n            self.entity.set(self.input_field.json_value())\n            self.update_style()\n        else:\n            # Manually trigger hierarchical style update\n            self.ignore_input_changes.set_ignore(True)\n            self.ignore_input_changes.set_ignore(False)\n\n\nclass EnumeratorWidget(InputWidget):\n    def _add_inputs_to_layout(self):\n        if self.entity.multiselection:\n            self.input_field = MultiSelectionComboBox(\n                placeholder=self.entity.placeholder, parent=self.content_widget\n            )\n\n        else:\n            self.input_field = SettingsComboBox(self.content_widget)\n\n        for enum_item in self.entity.enum_items:\n            for value, label in enum_item.items():\n                self.input_field.addItem(label, value)\n\n        self.content_layout.addWidget(self.input_field, 0)\n\n        self.setFocusProxy(self.input_field)\n\n        self.input_field.focused_in.connect(self._on_input_focus)\n        self.input_field.value_changed.connect(self._on_value_change)\n\n    def _on_input_focus(self):\n        self.focused_in()\n\n    def _on_entity_change(self):\n        if self.entity.value != self.input_field.value():\n            self.set_entity_value()\n\n    def set_entity_value(self):\n        self.input_field.set_value(self.entity.value)\n\n    def _on_value_change(self):\n        if self.ignore_input_changes:\n            return\n        self.entity.set(self.input_field.value())\n\n\nclass PathWidget(BaseWidget):\n    def create_ui(self):\n        self._child_style_state = \"\"\n\n        if self.entity.use_label_wrap:\n            entity_label = None\n            self._create_label_wrapper()\n        else:\n            entity_label = self.entity.label\n            self.content_widget = self\n            self.content_layout = QtWidgets.QGridLayout(self)\n            self.content_layout.setContentsMargins(0, 0, 0, 0)\n            self.content_layout.setSpacing(5)\n            # Add stretch to second column\n            self.content_layout.setColumnStretch(1, 1)\n            self.body_widget = None\n\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        self.input_field = self.create_ui_for_entity(\n            self.category_widget, self.entity.child_obj, self\n        )\n        self.entity_widget.add_widget_to_layout(self, entity_label)\n\n    def _create_label_wrapper(self):\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n\n        body_widget = ExpandingWidget(self.entity.label, self)\n        main_layout.addWidget(body_widget)\n        self.label_widget = body_widget.label_widget\n\n        self.body_widget = body_widget\n\n        content_widget = QtWidgets.QWidget(body_widget)\n        content_widget.setObjectName(\"ContentWidget\")\n        content_widget.setProperty(\"content_state\", \"\")\n        content_layout = QtWidgets.QGridLayout(content_widget)\n        content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 5)\n\n        body_widget.set_content_widget(content_widget)\n\n        self.body_widget = body_widget\n        self.content_widget = content_widget\n        self.content_layout = content_layout\n\n        if not self.entity.collapsible:\n            body_widget.hide_toolbox(hide_content=False)\n\n        elif self.entity.collapsed:\n            body_widget.toggle_content()\n\n    def add_widget_to_layout(self, widget, label=None):\n        row = self.content_layout.rowCount()\n        if label:\n            label_widget = GridLabelWidget(label, widget)\n            label_widget.input_field = widget\n            widget.label_widget = label_widget\n            self.content_layout.addWidget(label_widget, row, 0, 1, 1)\n            self.content_layout.addWidget(widget, row, 1, 1, 1)\n        else:\n            self.content_layout.addWidget(widget, row, 0, 1, 2)\n\n    def set_entity_value(self):\n        self.input_field.set_entity_value()\n\n    def make_sure_is_visible(self, *args, **kwargs):\n        return self.input_field.make_sure_is_visible(*args, **kwargs)\n\n    def hierarchical_style_update(self):\n        self.update_style()\n        self.input_field.hierarchical_style_update()\n\n    def _on_entity_change(self):\n        # No need to do anything. Styles will be updated from top hierarchy.\n        pass\n\n    def update_style(self):\n        if not self.body_widget and not self.label_widget:\n            return\n\n        if self.entity.group_item:\n            group_item = self.entity.group_item\n            has_unsaved_changes = group_item.has_unsaved_changes\n            has_project_override = group_item.has_project_override\n            has_studio_override = group_item.has_studio_override\n        else:\n            has_unsaved_changes = self.entity.has_unsaved_changes\n            has_project_override = self.entity.has_project_override\n            has_studio_override = self.entity.has_studio_override\n\n        child_invalid = self.is_invalid\n\n        if self.body_widget:\n            child_style_state = self.get_style_state(\n                child_invalid,\n                has_unsaved_changes,\n                has_project_override,\n                has_studio_override\n            )\n            if child_style_state:\n                child_style_state = \"child-{}\".format(child_style_state)\n\n            if child_style_state != self._child_style_state:\n                self.body_widget.side_line_widget.setProperty(\n                    \"state\", child_style_state\n                )\n                self.body_widget.side_line_widget.style().polish(\n                    self.body_widget.side_line_widget\n                )\n                self._child_style_state = child_style_state\n\n        if self.label_widget:\n            style_state = self.get_style_state(\n                child_invalid,\n                has_unsaved_changes,\n                has_project_override,\n                has_studio_override\n            )\n            if self._style_state != style_state:\n                self.label_widget.setProperty(\"state\", style_state)\n                self.label_widget.style().polish(self.label_widget)\n\n                self._style_state = style_state\n\n    @property\n    def is_invalid(self):\n        return self._is_invalid or self.child_invalid\n\n    @property\n    def child_invalid(self):\n        return self.input_field.is_invalid\n\n    def get_invalid(self):\n        return self.input_field.get_invalid()\n\n\nclass PathInputWidget(InputWidget):\n    def _add_inputs_to_layout(self):\n        self.input_field = SettingsLineEdit(self.content_widget)\n        placeholder = self.entity.placeholder_text\n        if placeholder:\n            self.input_field.setPlaceholderText(placeholder)\n\n        self.setFocusProxy(self.input_field)\n        self.content_layout.addWidget(self.input_field)\n\n        self.input_field.textChanged.connect(self._on_value_change)\n        self.input_field.focused_in.connect(self._on_input_focus)\n\n    def _on_input_focus(self):\n        self.focused_in()\n\n    def _on_entity_change(self):\n        if self.entity.value != self.input_value():\n            self.set_entity_value()\n\n    def set_entity_value(self):\n        self.input_field.setText(self.entity.value)\n\n    def input_value(self):\n        return self.input_field.text()\n\n    def _on_value_change(self):\n        if self.ignore_input_changes:\n            return\n        self.start_value_timer()\n\n    def _on_value_change_timer(self):\n        self.entity.set(self.input_value())\n"
  },
  {
    "path": "openpype/tools/settings/settings/lib.py",
    "content": "from qtpy import QtCore\n\nfrom .widgets import SettingsToolBtn\n\n# Offset of value change trigger in ms\nVALUE_CHANGE_OFFSET_MS = 300\n\n\ndef create_deffered_value_change_timer(callback):\n    \"\"\"Defer value change callback.\n\n    UI won't trigger all callbacks on each value change but after predefined\n    time. Timer is reset on each start so callback is triggered after user\n    finish editing.\n    \"\"\"\n    timer = QtCore.QTimer()\n    timer.setSingleShot(True)\n    timer.setInterval(VALUE_CHANGE_OFFSET_MS)\n    timer.timeout.connect(callback)\n    return timer\n\n\ndef create_add_btn(parent):\n    add_btn = SettingsToolBtn(\"add\", parent)\n    add_btn.setFocusPolicy(QtCore.Qt.ClickFocus)\n    return add_btn\n\n\ndef create_remove_btn(parent):\n    remove_btn = SettingsToolBtn(\"remove\", parent)\n    remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus)\n    return remove_btn\n\n\ndef create_up_btn(parent):\n    remove_btn = SettingsToolBtn(\"up\", parent)\n    remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus)\n    return remove_btn\n\n\ndef create_down_btn(parent):\n    add_btn = SettingsToolBtn(\"down\", parent)\n    add_btn.setFocusPolicy(QtCore.Qt.ClickFocus)\n    return add_btn\n\n\ndef create_confirm_btn(parent):\n    remove_btn = SettingsToolBtn(\"confirm\", parent)\n    remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus)\n    return remove_btn\n"
  },
  {
    "path": "openpype/tools/settings/settings/list_item_widget.py",
    "content": "from qtpy import QtWidgets, QtCore\n\nfrom openpype.tools.settings import (\n    CHILD_OFFSET\n)\n\nfrom .base import InputWidget\nfrom .widgets import ExpandingWidget\nfrom .lib import (\n    create_add_btn,\n    create_remove_btn,\n    create_up_btn,\n    create_down_btn\n)\n\n\nclass EmptyListItem(QtWidgets.QWidget):\n    def __init__(self, entity_widget, parent):\n        super(EmptyListItem, self).__init__(parent)\n\n        self.entity_widget = entity_widget\n\n        add_btn = create_add_btn(self)\n        remove_btn = create_remove_btn(self)\n\n        remove_btn.setEnabled(False)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(3)\n        layout.addWidget(add_btn, 0)\n        layout.addWidget(remove_btn, 0)\n        layout.addStretch(1)\n\n        add_btn.clicked.connect(self._on_add_clicked)\n\n        self.add_btn = add_btn\n        self.remove_btn = remove_btn\n\n    def _on_add_clicked(self):\n        self.entity_widget.add_new_item()\n\n\nclass ListItem(QtWidgets.QWidget):\n    def __init__(self, entity, entity_widget):\n        super(ListItem, self).__init__(entity_widget.content_widget)\n        self.entity_widget = entity_widget\n        self.entity = entity\n\n        self.ignore_input_changes = entity_widget.ignore_input_changes\n\n        add_btn = create_add_btn(self)\n        remove_btn = create_remove_btn(self)\n        up_btn = create_up_btn(self)\n        down_btn = create_down_btn(self)\n\n        add_btn.clicked.connect(self._on_add_clicked)\n        remove_btn.clicked.connect(self._on_remove_clicked)\n        up_btn.clicked.connect(self._on_up_clicked)\n        down_btn.clicked.connect(self._on_down_clicked)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(3)\n\n        layout.addWidget(add_btn, 0)\n        layout.addWidget(remove_btn, 0)\n\n        self.content_widget = self\n        self.content_layout = layout\n\n        self.input_field = self.create_ui_for_entity(\n            self.category_widget, self.entity, self\n        )\n\n        layout.addWidget(up_btn, 0)\n        layout.addWidget(down_btn, 0)\n\n        self.add_btn = add_btn\n        self.remove_btn = remove_btn\n        self.up_btn = up_btn\n        self.down_btn = down_btn\n\n        self._row = -1\n        self._is_last = False\n\n    @property\n    def category_widget(self):\n        return self.entity_widget.category_widget\n\n    def create_ui_for_entity(self, *args, **kwargs):\n        return self.entity_widget.create_ui_for_entity(\n            *args, **kwargs\n        )\n\n    def make_sure_is_visible(self, *args, **kwargs):\n        return self.input_field.make_sure_is_visible(*args, **kwargs)\n\n    @property\n    def is_invalid(self):\n        return self.input_field.is_invalid\n\n    def get_invalid(self):\n        return self.input_field.get_invalid()\n\n    def add_widget_to_layout(self, widget, label=None):\n        self.content_layout.addWidget(widget, 1)\n\n    def set_row(self, row, is_last):\n        if row == self._row and is_last == self._is_last:\n            return\n\n        trigger_order_changed = (\n            row != self._row\n            or is_last != self._is_last\n        )\n        self._row = row\n        self._is_last = is_last\n\n        if trigger_order_changed:\n            self.order_changed()\n\n    @property\n    def row(self):\n        return self._row\n\n    def parent_rows_count(self):\n        return len(self.entity_widget.input_fields)\n\n    def _on_add_clicked(self):\n        self.entity_widget.add_new_item(row=self.row + 1)\n\n    def _on_remove_clicked(self):\n        self.entity_widget.remove_row(self)\n\n    def _on_up_clicked(self):\n        self.entity_widget.swap_rows(self.row - 1, self.row)\n\n    def _on_down_clicked(self):\n        self.entity_widget.swap_rows(self.row, self.row + 1)\n\n    def order_changed(self):\n        parent_row_count = self.parent_rows_count()\n        if parent_row_count == 1:\n            self.up_btn.setVisible(False)\n            self.down_btn.setVisible(False)\n            return\n\n        if not self.up_btn.isVisible():\n            self.up_btn.setVisible(True)\n            self.down_btn.setVisible(True)\n\n        if self.row == 0:\n            self.up_btn.setEnabled(False)\n            self.down_btn.setEnabled(True)\n\n        elif self.row == parent_row_count - 1:\n            self.up_btn.setEnabled(True)\n            self.down_btn.setEnabled(False)\n\n        else:\n            self.up_btn.setEnabled(True)\n            self.down_btn.setEnabled(True)\n\n    def hierarchical_style_update(self):\n        self.input_field.hierarchical_style_update()\n\n    def trigger_hierarchical_style_update(self):\n        self.entity_widget.trigger_hierarchical_style_update()\n\n\nclass ListWidget(InputWidget):\n    def create_ui(self):\n        self._child_style_state = \"\"\n        self.input_fields = []\n        self._input_fields_by_entity_id = {}\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n\n        body_widget = None\n        entity_label = self.entity.label\n        if self.entity.use_label_wrap:\n            body_widget = ExpandingWidget(entity_label, self)\n            entity_label = None\n            main_layout.addWidget(body_widget)\n            self.label_widget = body_widget.label_widget\n\n        self.body_widget = body_widget\n\n        if body_widget is None:\n            content_parent_widget = self\n        else:\n            content_parent_widget = body_widget\n\n        content_state = \"\"\n\n        content_widget = QtWidgets.QWidget(content_parent_widget)\n        content_widget.setObjectName(\"ContentWidget\")\n        content_widget.setProperty(\"content_state\", content_state)\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 5)\n\n        if body_widget is None:\n            main_layout.addWidget(content_widget)\n        else:\n            body_widget.set_content_widget(content_widget)\n\n        self.body_widget = body_widget\n        self.content_widget = content_widget\n        self.content_layout = content_layout\n\n        if body_widget:\n            if not self.entity.collapsible:\n                body_widget.hide_toolbox(hide_content=False)\n\n            elif self.entity.collapsed:\n                body_widget.toggle_content()\n\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        self.empty_row = EmptyListItem(self, self.content_widget)\n        self.content_layout.addWidget(self.empty_row)\n\n        self.entity_widget.add_widget_to_layout(self, entity_label)\n\n    def set_entity_value(self):\n        self.remove_all_rows()\n\n        for entity in self.entity.children:\n            self.add_row(entity)\n\n        self.empty_row.setVisible(self.count() == 0)\n\n    def get_invalid(self):\n        invalid = []\n        if self.is_invalid:\n            invalid.append(self)\n\n        for input_field in self.input_fields:\n            invalid.extend(input_field.get_invalid())\n        return invalid\n\n    def make_sure_is_visible(self, path, scroll_to):\n        if not path:\n            return False\n\n        entity_path = self.entity.path\n        if entity_path == path:\n            self.set_focus(scroll_to)\n            return True\n\n        if not path.startswith(entity_path):\n            return False\n\n        if self.body_widget and not self.body_widget.is_expanded():\n            self.body_widget.toggle_content(True)\n\n        for input_field in self.input_fields:\n            if input_field.make_sure_is_visible(path, scroll_to):\n                return True\n        return False\n\n    def _on_entity_change(self):\n        # TODO do less inefficient\n        childen_order = []\n        new_children = []\n        for idx, child_entity in enumerate(self.entity):\n            input_field = self._input_fields_by_entity_id.get(child_entity.id)\n            if input_field is not None:\n                childen_order.append(input_field)\n            else:\n                new_children.append((idx, child_entity))\n\n        order_changed = False\n        for idx, input_field in enumerate(childen_order):\n            current_field = self.input_fields[idx]\n            if current_field is input_field:\n                continue\n            order_changed = True\n            old_idx = self.input_fields.index(input_field)\n            self.input_fields[old_idx], self.input_fields[idx] = (\n                current_field, input_field\n            )\n            self.content_layout.insertWidget(idx + 1, input_field)\n\n        kept_len = len(childen_order)\n        fields_len = len(self.input_fields)\n        if fields_len > kept_len:\n            order_changed = True\n            for row in reversed(range(kept_len, fields_len)):\n                self.remove_row(row=row)\n\n        for idx, child_entity in new_children:\n            order_changed = False\n            self.add_row(child_entity, idx)\n\n        if not order_changed:\n            return\n\n        self._on_order_change()\n\n        input_field_len = self.count()\n        self.empty_row.setVisible(input_field_len == 0)\n\n    def _on_order_change(self):\n        last_idx = self.count() - 1\n        previous_input = None\n        for idx, input_field in enumerate(self.input_fields):\n            input_field.set_row(idx, idx == last_idx)\n            next_input = input_field.input_field.focusProxy()\n            if previous_input is not None:\n                self.setTabOrder(previous_input, next_input)\n            else:\n                self.setTabOrder(self, next_input)\n            previous_input = next_input\n\n        if previous_input is not None:\n            self.setTabOrder(previous_input, self)\n\n    def count(self):\n        return len(self.input_fields)\n\n    def swap_rows(self, row_1, row_2):\n        if row_1 == row_2:\n            return\n\n        self.entity.swap_indexes(row_1, row_2)\n\n    def add_new_item(self, row=None):\n        new_entity = self.entity.add_new_item(row)\n        input_field = self._input_fields_by_entity_id.get(new_entity.id)\n        if input_field is not None:\n            input_field.input_field.setFocus()\n        return new_entity\n\n    def add_row(self, child_entity, row=None):\n        # Create new item\n        item_widget = ListItem(child_entity, self)\n        self._input_fields_by_entity_id[child_entity.id] = item_widget\n\n        if row is None:\n            self.content_layout.addWidget(item_widget)\n            self.input_fields.append(item_widget)\n        else:\n            self.content_layout.insertWidget(row + 1, item_widget)\n            self.input_fields.insert(row, item_widget)\n\n        # Change to entity value after item is added to `input_fields`\n        # - may cause recursion error as setting a value may cause input field\n        #   change which will trigger this validation if entity is already\n        #   added as widget here which won't because is not in input_fields\n        item_widget.input_field.set_entity_value()\n\n        self._on_order_change()\n\n        input_field_len = self.count()\n        self.empty_row.setVisible(input_field_len == 0)\n\n        self.updateGeometry()\n\n    def remove_all_rows(self):\n        self._input_fields_by_entity_id = {}\n        while self.input_fields:\n            item_widget = self.input_fields.pop(0)\n            self.content_layout.removeWidget(item_widget)\n            item_widget.setParent(None)\n            item_widget.deleteLater()\n\n        self.empty_row.setVisible(True)\n\n        self.updateGeometry()\n\n    def remove_row(self, item_widget=None, row=None):\n        if item_widget is None:\n            item_widget = self.input_fields[row]\n        elif row is None:\n            row = self.input_fields.index(item_widget)\n\n        self.content_layout.removeWidget(item_widget)\n        self.input_fields.pop(row)\n        self._input_fields_by_entity_id.pop(item_widget.entity.id)\n        item_widget.setParent(None)\n        item_widget.deleteLater()\n\n        if item_widget.entity in self.entity:\n            self.entity.remove(item_widget.entity)\n\n        rows = self.count()\n        any_item = rows == 0\n        if any_item:\n            start_row = 0\n            if row > 0:\n                start_row = row - 1\n\n            last_row = rows - 1\n            _enum = enumerate(self.input_fields[start_row:rows])\n            for idx, _item_widget in _enum:\n                _item_widget.set_row(idx, idx == last_row)\n\n        self.empty_row.setVisible(any_item)\n\n        self.updateGeometry()\n\n    @property\n    def is_invalid(self):\n        return self._is_invalid or self.child_invalid\n\n    @property\n    def child_invalid(self):\n        for input_field in self.input_fields:\n            if input_field.is_invalid:\n                return True\n        return False\n\n    def update_style(self):\n        if not self.body_widget and not self.label_widget:\n            return\n\n        if self.entity.group_item:\n            group_item = self.entity.group_item\n            has_unsaved_changes = group_item.has_unsaved_changes\n            has_project_override = group_item.has_project_override\n            has_studio_override = group_item.has_studio_override\n        else:\n            has_unsaved_changes = self.entity.has_unsaved_changes\n            has_project_override = self.entity.has_project_override\n            has_studio_override = self.entity.has_studio_override\n\n        child_invalid = self.is_invalid\n\n        if self.body_widget:\n            child_style_state = self.get_style_state(\n                child_invalid,\n                has_unsaved_changes,\n                has_project_override,\n                has_studio_override\n            )\n            if child_style_state:\n                child_style_state = \"child-{}\".format(child_style_state)\n\n            if child_style_state != self._child_style_state:\n                self.body_widget.side_line_widget.setProperty(\n                    \"state\", child_style_state\n                )\n                self.body_widget.side_line_widget.style().polish(\n                    self.body_widget.side_line_widget\n                )\n                self._child_style_state = child_style_state\n\n        if self.label_widget:\n            style_state = self.get_style_state(\n                child_invalid,\n                has_unsaved_changes,\n                has_project_override,\n                has_studio_override\n            )\n            if self._style_state != style_state:\n                self.label_widget.setProperty(\"state\", style_state)\n                self.label_widget.style().polish(self.label_widget)\n\n                self._style_state = style_state\n\n    def hierarchical_style_update(self):\n        self.update_style()\n        for input_field in self.input_fields:\n            input_field.hierarchical_style_update()\n"
  },
  {
    "path": "openpype/tools/settings/settings/list_strict_widget.py",
    "content": "from qtpy import QtWidgets, QtCore\n\nfrom .widgets import (\n    GridLabelWidget,\n    SpacerWidget\n)\nfrom .base import BaseWidget\n\n\nclass ListStrictWidget(BaseWidget):\n    def create_ui(self):\n        self.setObjectName(\"ListStrictWidget\")\n\n        self._child_style_state = \"\"\n        self.input_fields = []\n\n        content_layout = QtWidgets.QGridLayout(self)\n        content_layout.setContentsMargins(0, 0, 0, 0)\n        content_layout.setSpacing(3)\n\n        self.content_layout = content_layout\n        self.content_widget = self\n\n        any_children_has_label = False\n        for child_obj in self.entity.children:\n            if child_obj.label:\n                any_children_has_label = True\n                break\n\n        self._any_children_has_label = any_children_has_label\n        # Change column stretch factor for vertical alignment\n        if not self.entity.is_horizontal:\n            col_index = 2 if any_children_has_label else 1\n            content_layout.setColumnStretch(col_index, 1)\n\n        for child_obj in self.entity.children:\n            self.input_fields.append(\n                self.create_ui_for_entity(\n                    self.category_widget, child_obj, self\n                )\n            )\n\n        if self.entity.is_horizontal:\n            col = self.content_layout.columnCount()\n            spacer = SpacerWidget(self)\n            self.content_layout.addWidget(spacer, 0, col, 2, 1)\n            self.content_layout.setColumnStretch(col, 1)\n\n        self.entity_widget.add_widget_to_layout(self, self.entity.label)\n\n    @property\n    def is_invalid(self):\n        return self._is_invalid or self._child_invalid\n\n    @property\n    def _child_invalid(self):\n        for input_field in self.input_fields:\n            if input_field.is_invalid:\n                return True\n        return False\n\n    def get_invalid(self):\n        invalid = []\n        for input_field in self.input_fields:\n            invalid.extend(input_field.get_invalid())\n        return invalid\n\n    def make_sure_is_visible(self, path, scroll_to):\n        if not path:\n            return False\n\n        entity_path = self.entity.path\n        if entity_path == path:\n            self.set_focus(scroll_to)\n            return True\n\n        if path.startswith(entity_path):\n            for input_field in self.input_fields:\n                if input_field.make_sure_is_visible(path, scroll_to):\n                    return True\n        return False\n\n    def add_widget_to_layout(self, widget, label=None):\n        # Horizontally added children\n        if self.entity.is_horizontal:\n            self._add_child_horizontally(widget, label)\n        else:\n            self._add_child_vertically(widget, label)\n\n        self.updateGeometry()\n\n    def _add_child_horizontally(self, widget, label):\n        col = self.content_layout.columnCount()\n        # Expand to whole grid if all children are without label\n        if not self._any_children_has_label:\n            self.content_layout.addWidget(widget, 0, col, 1, 2)\n        else:\n            if label:\n                label_widget = GridLabelWidget(label, widget)\n                label_widget.input_field = widget\n                widget.label_widget = label_widget\n                self.content_layout.addWidget(label_widget, 0, col, 1, 1)\n                col += 1\n            self.content_layout.addWidget(widget, 0, col, 1, 1)\n\n    def _add_child_vertically(self, widget, label):\n        row = self.content_layout.rowCount()\n        if not self._any_children_has_label:\n            self.content_layout.addWidget(widget, row, 0, 1, 1)\n\n            spacer_widget = SpacerWidget(self)\n            self.content_layout.addWidget(spacer_widget, row, 1, 1, 1)\n\n        else:\n            if label:\n                label_widget = GridLabelWidget(label, widget)\n                label_widget.input_field = widget\n                widget.label_widget = label_widget\n                self.content_layout.addWidget(\n                    label_widget, row, 0, 1, 1,\n                    alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop\n                )\n            self.content_layout.addWidget(widget, row, 1, 1, 1)\n\n            spacer_widget = SpacerWidget(self)\n            self.content_layout.addWidget(spacer_widget, row, 2, 1, 1)\n\n    def hierarchical_style_update(self):\n        self.update_style()\n        for input_field in self.input_fields:\n            input_field.hierarchical_style_update()\n\n    def set_entity_value(self):\n        for input_field in self.input_fields:\n            input_field.set_entity_value()\n\n    def _on_entity_change(self):\n        pass\n\n    def update_style(self):\n        if not self.label_widget:\n            return\n\n        style_state = self.get_style_state(\n            self.is_invalid,\n            self.entity.has_unsaved_changes,\n            self.entity.has_project_override,\n            self.entity.has_studio_override\n        )\n\n        if self._style_state == style_state:\n            return\n\n        self.label_widget.setProperty(\"state\", style_state)\n        self.label_widget.style().polish(self.label_widget)\n\n        self._style_state = style_state\n"
  },
  {
    "path": "openpype/tools/settings/settings/search_dialog.py",
    "content": "import re\nimport collections\n\nfrom qtpy import QtCore, QtWidgets, QtGui\n\nENTITY_LABEL_ROLE = QtCore.Qt.UserRole + 1\nENTITY_PATH_ROLE = QtCore.Qt.UserRole + 2\n\n\ndef get_entity_children(entity):\n    # TODO find better way how to go through all children\n    if hasattr(entity, \"values\"):\n        return entity.values()\n    return []\n\n\nclass RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):\n    \"\"\"Filters recursively to regex in all columns\"\"\"\n\n    def __init__(self):\n        super(RecursiveSortFilterProxyModel, self).__init__()\n\n        # Note: Recursive filtering was introduced in Qt 5.10.\n        self.setRecursiveFilteringEnabled(True)\n\n    def filterAcceptsRow(self, row, parent):\n        if not parent.isValid():\n            return False\n\n        if hasattr(self, \"filterRegExp\"):\n            regex = self.filterRegExp()\n        else:\n            regex = self.filterRegularExpression()\n\n        pattern = regex.pattern()\n        if pattern and regex.isValid():\n            pattern = regex.pattern()\n            compiled_regex = re.compile(pattern, re.IGNORECASE)\n            source_model = self.sourceModel()\n\n            # Check current index itself in all columns\n            source_index = source_model.index(row, 0, parent)\n            if source_index.isValid():\n                for role in (ENTITY_PATH_ROLE, ENTITY_LABEL_ROLE):\n                    value = source_model.data(source_index, role)\n                    if value and compiled_regex.search(value):\n                        return True\n            return False\n\n        return super(\n            RecursiveSortFilterProxyModel, self\n        ).filterAcceptsRow(row, parent)\n\n\nclass SearchEntitiesDialog(QtWidgets.QDialog):\n    path_clicked = QtCore.Signal(str)\n\n    def __init__(self, parent):\n        super(SearchEntitiesDialog, self).__init__(parent=parent)\n\n        self.setWindowTitle(\"Search Settings\")\n\n        filter_edit = QtWidgets.QLineEdit(self)\n        filter_edit.setPlaceholderText(\"Search...\")\n\n        model = EntityTreeModel()\n        proxy = RecursiveSortFilterProxyModel()\n        proxy.setSourceModel(model)\n        proxy.setDynamicSortFilter(True)\n\n        view = QtWidgets.QTreeView(self)\n        view.setAllColumnsShowFocus(True)\n        view.setSortingEnabled(True)\n        view.setModel(proxy)\n        model.setColumnCount(3)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(filter_edit)\n        layout.addWidget(view)\n\n        filter_changed_timer = QtCore.QTimer()\n        filter_changed_timer.setInterval(200)\n        filter_changed_timer.setSingleShot(True)\n\n        view.selectionModel().selectionChanged.connect(\n            self._on_selection_change\n        )\n        filter_changed_timer.timeout.connect(self._on_filter_timer)\n        filter_edit.textChanged.connect(self._on_filter_changed)\n\n        self._filter_edit = filter_edit\n        self._model = model\n        self._proxy = proxy\n        self._view = view\n        self._filter_changed_timer = filter_changed_timer\n\n        self._first_show = True\n\n    def set_root_entity(self, entity):\n        self._model.set_root_entity(entity)\n        self._view.resizeColumnToContents(0)\n\n    def showEvent(self, event):\n        super(SearchEntitiesDialog, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self.resize(700, 500)\n\n    def _on_filter_changed(self, txt):\n        self._filter_changed_timer.start()\n\n    def _on_filter_timer(self):\n        text = self._filter_edit.text()\n        if hasattr(self._proxy, \"setFilterRegExp\"):\n            self._proxy.setFilterRegExp(text)\n        else:\n            self._proxy.setFilterRegularExpression(text)\n\n        # WARNING This expanding and resizing is relatively slow.\n        self._view.expandAll()\n        self._view.resizeColumnToContents(0)\n\n    def _on_selection_change(self):\n        current = self._view.currentIndex()\n        path = current.data(ENTITY_PATH_ROLE)\n        self.path_clicked.emit(path)\n\n\nclass EntityTreeModel(QtGui.QStandardItemModel):\n    def __init__(self, *args, **kwargs):\n        super(EntityTreeModel, self).__init__(*args, **kwargs)\n        self.setColumnCount(3)\n\n    def data(self, index, role=None):\n        if role is None:\n            role = QtCore.Qt.DisplayRole\n\n        col = index.column()\n        if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):\n            if col == 0:\n                pass\n            elif col == 1:\n                role = ENTITY_LABEL_ROLE\n            elif col == 2:\n                role = ENTITY_PATH_ROLE\n\n        if col > 0:\n            index = self.index(index.row(), 0, index.parent())\n        return super(EntityTreeModel, self).data(index, role)\n\n    def flags(self, index):\n        if index.column() > 0:\n            index = self.index(index.row(), 0, index.parent())\n        return super(EntityTreeModel, self).flags(index)\n\n    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):\n        if role == QtCore.Qt.DisplayRole:\n            if section == 0:\n                return \"Key\"\n            elif section == 1:\n                return \"Label\"\n            elif section == 2:\n                return \"Path\"\n            return \"\"\n        return super(EntityTreeModel, self).headerData(\n            section, orientation, role\n        )\n\n    def set_root_entity(self, root_entity):\n        parent = self.invisibleRootItem()\n        parent.removeRows(0, parent.rowCount())\n        if not root_entity:\n            return\n\n        # We don't want to see the root entity so we directly add its children\n        fill_queue = collections.deque()\n        fill_queue.append((root_entity, parent))\n        cols = self.columnCount()\n        while fill_queue:\n            parent_entity, parent_item = fill_queue.popleft()\n            child_items = []\n            for child in get_entity_children(parent_entity):\n                label = child.label\n                path = child.path\n                key = path.split(\"/\")[-1]\n                item = QtGui.QStandardItem(key)\n                item.setEditable(False)\n                item.setData(label, ENTITY_LABEL_ROLE)\n                item.setData(path, ENTITY_PATH_ROLE)\n                item.setColumnCount(cols)\n                child_items.append(item)\n                fill_queue.append((child, item))\n\n            if child_items:\n                parent_item.appendRows(child_items)\n"
  },
  {
    "path": "openpype/tools/settings/settings/tests.py",
    "content": "from qtpy import QtWidgets, QtCore\n\n\ndef indented_print(data, indent=0):\n    spaces = \" \" * (indent * 4)\n    if not isinstance(data, dict):\n        print(\"{}{}\".format(spaces, data))\n        return\n\n    for key, value in data.items():\n        print(\"{}{}\".format(spaces, key))\n        indented_print(value, indent + 1)\n\n\nclass SelectableMenu(QtWidgets.QMenu):\n\n    selection_changed = QtCore.Signal()\n\n    def mouseReleaseEvent(self, event):\n        action = self.activeAction()\n        if action and action.isEnabled():\n            action.trigger()\n            self.selection_changed.emit()\n        else:\n            super(SelectableMenu, self).mouseReleaseEvent(event)\n\n    def event(self, event):\n        result = super(SelectableMenu, self).event(event)\n        if event.type() == QtCore.QEvent.Show:\n            parent = self.parent()\n\n            move_point = parent.mapToGlobal(QtCore.QPoint(0, parent.height()))\n            check_point = (\n                move_point\n                + QtCore.QPoint(self.width(), self.height())\n            )\n            visibility_check = (\n                QtWidgets.QApplication.desktop().rect().contains(check_point)\n            )\n            if not visibility_check:\n                move_point -= QtCore.QPoint(0, parent.height() + self.height())\n            self.move(move_point)\n\n            self.updateGeometry()\n            self.repaint()\n\n        return result\n\n\nclass AddibleComboBox(QtWidgets.QComboBox):\n    \"\"\"Searchable ComboBox with empty placeholder value as first value\"\"\"\n\n    def __init__(self, placeholder=\"\", parent=None):\n        super(AddibleComboBox, self).__init__(parent)\n\n        self.setEditable(True)\n        # self.setInsertPolicy(QtWidgets.QComboBox.NoInsert)\n\n        self.lineEdit().setPlaceholderText(placeholder)\n        # self.lineEdit().returnPressed.connect(self.on_return_pressed)\n\n        # Apply completer settings\n        completer = self.completer()\n        completer.setCompletionMode(completer.PopupCompletion)\n        completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n    # def on_return_pressed(self):\n    #     text = self.lineEdit().text().strip()\n    #     if not text:\n    #         return\n    #\n    #     index = self.findText(text)\n    #     if index < 0:\n    #         self.addItems([text])\n    #         index = self.findText(text)\n\n    def populate(self, items):\n        self.clear()\n        # self.addItems([\"\"])     # ensure first item is placeholder\n        self.addItems(items)\n\n    def get_valid_value(self):\n        \"\"\"Return the current text if it's a valid value else None\n\n        Note: The empty placeholder value is valid and returns as \"\"\n\n        \"\"\"\n\n        text = self.currentText()\n        lookup = set(self.itemText(i) for i in range(self.count()))\n        if text not in lookup:\n            return None\n\n        return text or None\n\n\nclass MultiselectEnum(QtWidgets.QWidget):\n\n    selection_changed = QtCore.Signal()\n\n    def __init__(self, title, parent=None):\n        super(MultiselectEnum, self).__init__(parent)\n        toolbutton = QtWidgets.QToolButton(self)\n        toolbutton.setText(title)\n\n        toolmenu = SelectableMenu(toolbutton)\n\n        toolbutton.setMenu(toolmenu)\n        toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)\n\n        layout = QtWidgets.QHBoxLayout()\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(toolbutton)\n\n        self.setLayout(layout)\n\n        toolmenu.selection_changed.connect(self.selection_changed)\n\n        self.toolbutton = toolbutton\n        self.toolmenu = toolmenu\n        self.main_layout = layout\n\n    def populate(self, items):\n        self.toolmenu.clear()\n        self.addItems(items)\n\n    def addItems(self, items):\n        for item in items:\n            action = self.toolmenu.addAction(item)\n            action.setCheckable(True)\n            action.setChecked(True)\n            self.toolmenu.addAction(action)\n\n    def items(self):\n        for action in self.toolmenu.actions():\n            yield action\n"
  },
  {
    "path": "openpype/tools/settings/settings/widgets.py",
    "content": "import copy\nimport uuid\nfrom qtpy import QtWidgets, QtCore, QtGui\nimport qtawesome\n\nfrom openpype.client import get_projects\nfrom openpype.style import get_objected_colors\nfrom openpype.tools.utils.widgets import ImageButton\nfrom openpype.tools.utils.lib import paint_image_with_color\n\nfrom openpype.widgets.nice_checkbox import NiceCheckbox\nfrom openpype.tools.utils import (\n    PlaceholderLineEdit,\n    DynamicQThread\n)\nfrom openpype.settings.lib import find_closest_version_for_projects\nfrom openpype.lib import get_openpype_version\nfrom .images import (\n    get_pixmap,\n    get_image\n)\nfrom .constants import (\n    DEFAULT_PROJECT_LABEL,\n    PROJECT_NAME_ROLE,\n    PROJECT_VERSION_ROLE,\n    PROJECT_IS_ACTIVE_ROLE,\n    PROJECT_IS_SELECTED_ROLE\n)\n\n\nclass SettingsTabWidget(QtWidgets.QTabWidget):\n    context_menu_requested = QtCore.Signal(int)\n\n    def __init__(self, *args, **kwargs):\n        super(SettingsTabWidget, self).__init__(*args, **kwargs)\n        self._right_click_tab_idx = None\n\n    def mousePressEvent(self, event):\n        super(SettingsTabWidget, self).mousePressEvent(event)\n        if event.button() == QtCore.Qt.RightButton:\n            tab_bar = self.tabBar()\n            pos = tab_bar.mapFromGlobal(event.globalPos())\n            tab_idx = tab_bar.tabAt(pos)\n            if tab_idx < 0:\n                tab_idx = None\n            self._right_click_tab_idx = tab_idx\n\n    def mouseReleaseEvent(self, event):\n        super(SettingsTabWidget, self).mouseReleaseEvent(event)\n        if event.button() == QtCore.Qt.RightButton:\n            tab_bar = self.tabBar()\n            pos = tab_bar.mapFromGlobal(event.globalPos())\n            tab_idx = tab_bar.tabAt(pos)\n            if tab_idx == self._right_click_tab_idx:\n                self.context_menu_requested.emit(tab_idx)\n            self._right_click_tab = None\n\n\nclass CompleterFilter(QtCore.QSortFilterProxyModel):\n    def __init__(self, *args, **kwargs):\n        super(CompleterFilter, self).__init__(*args, **kwargs)\n\n        self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        self._text_filter = \"\"\n\n    def set_text_filter(self, text):\n        if self._text_filter == text:\n            return\n        self._text_filter = text\n        self.invalidateFilter()\n\n    def filterAcceptsRow(self, row, parent_index):\n        if not self._text_filter:\n            return True\n        model = self.sourceModel()\n        index = model.index(row, self.filterKeyColumn(), parent_index)\n        value = index.data(QtCore.Qt.DisplayRole)\n        if self._text_filter in value:\n            if self._text_filter == value:\n                return False\n            return True\n        return False\n\n\nclass CompleterView(QtWidgets.QListView):\n    row_activated = QtCore.Signal(str)\n\n    def __init__(self, parent):\n        super(CompleterView, self).__init__(parent)\n\n        self.setWindowFlags(\n            QtCore.Qt.FramelessWindowHint\n            | QtCore.Qt.Tool\n        )\n\n        # Open the widget unactivated\n        self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating)\n        self.setAttribute(QtCore.Qt.WA_NoMouseReplay)\n        delegate = QtWidgets.QStyledItemDelegate()\n        self.setItemDelegate(delegate)\n\n        model = QtGui.QStandardItemModel()\n        filter_model = CompleterFilter()\n        filter_model.setSourceModel(model)\n        self.setModel(filter_model)\n\n        # self.installEventFilter(parent)\n\n        self.clicked.connect(self._on_activated)\n\n        self._last_loaded_values = None\n        self._model = model\n        self._filter_model = filter_model\n        self._delegate = delegate\n\n    def _on_activated(self, index):\n        if index.isValid():\n            value = index.data(QtCore.Qt.DisplayRole)\n            self.row_activated.emit(value)\n\n    def set_text_filter(self, text):\n        self._filter_model.set_text_filter(text)\n        self._update_geo()\n\n    def sizeHint(self):\n        result = super(CompleterView, self).sizeHint()\n        if self._filter_model.rowCount() == 0:\n            result.setHeight(0)\n\n        return result\n\n    def showEvent(self, event):\n        super(CompleterView, self).showEvent(event)\n        self._update_geo()\n\n    def _update_geo(self):\n        size_hint = self.sizeHint()\n        self.resize(size_hint.width(), size_hint.height())\n\n    def update_values(self, values):\n        if not values:\n            values = []\n\n        if self._last_loaded_values == values:\n            return\n        self._last_loaded_values = copy.deepcopy(values)\n\n        root_item = self._model.invisibleRootItem()\n        existing_values = {}\n        for row in reversed(range(root_item.rowCount())):\n            child = root_item.child(row)\n            value = child.data(QtCore.Qt.DisplayRole)\n            if value not in values:\n                root_item.removeRows(child.row())\n            else:\n                existing_values[value] = child\n\n        for row, value in enumerate(values):\n            if value in existing_values:\n                item = existing_values[value]\n                if item.row() == row:\n                    continue\n            else:\n                item = QtGui.QStandardItem(value)\n                item.setEditable(False)\n\n            root_item.setChild(row, item)\n\n        self._update_geo()\n\n    def _get_selected_row(self):\n        indexes = self.selectionModel().selectedIndexes()\n        if not indexes:\n            return -1\n        return indexes[0].row()\n\n    def _select_row(self, row):\n        index = self._filter_model.index(row, 0)\n        self.setCurrentIndex(index)\n\n    def move_up(self):\n        rows = self._filter_model.rowCount()\n        if rows == 0:\n            return\n\n        selected_row = self._get_selected_row()\n        if selected_row < 0:\n            new_row = rows - 1\n        else:\n            new_row = selected_row - 1\n            if new_row < 0:\n                new_row = rows - 1\n\n        if new_row != selected_row:\n            self._select_row(new_row)\n\n    def move_down(self):\n        rows = self._filter_model.rowCount()\n        if rows == 0:\n            return\n\n        selected_row = self._get_selected_row()\n        if selected_row < 0:\n            new_row = 0\n        else:\n            new_row = selected_row + 1\n            if new_row >= rows:\n                new_row = 0\n\n        if new_row != selected_row:\n            self._select_row(new_row)\n\n    def enter_pressed(self):\n        selected_row = self._get_selected_row()\n        if selected_row < 0:\n            return\n        index = self._filter_model.index(selected_row, 0)\n        self._on_activated(index)\n\n\nclass SettingsLineEdit(PlaceholderLineEdit):\n    focused_in = QtCore.Signal()\n\n    def __init__(self, *args, **kwargs):\n        super(SettingsLineEdit, self).__init__(*args, **kwargs)\n\n        # Timer which will get started on focus in and stopped on focus out\n        # - callback checks if line edit or completer have focus\n        #   and hide completer if not\n        focus_timer = QtCore.QTimer()\n        focus_timer.setInterval(50)\n\n        focus_timer.timeout.connect(self._on_focus_timer)\n        self.textChanged.connect(self._on_text_change)\n\n        self._completer = None\n        self._focus_timer = focus_timer\n\n    def _on_text_change(self, text):\n        if self._completer is not None:\n            self._completer.set_text_filter(text)\n\n    def _completer_should_be_visible(self):\n        return (\n            self.isVisible()\n            and (self.hasFocus() or self._completer.hasFocus())\n        )\n\n    def _show_completer(self):\n        if self._completer_should_be_visible():\n            self._focus_timer.start()\n            self._completer.show()\n            self._update_completer()\n\n    def _update_completer(self):\n        if self._completer is None or not self._completer.isVisible():\n            return\n        point = self.frameGeometry().bottomLeft()\n        new_point = self.mapToGlobal(point)\n        self._completer.move(new_point)\n\n    def _on_focus_timer(self):\n        if not self._completer_should_be_visible():\n            self._completer.hide()\n            self._focus_timer.stop()\n\n    def focusInEvent(self, event):\n        super(SettingsLineEdit, self).focusInEvent(event)\n        self.focused_in.emit()\n\n        if self._completer is not None:\n            self._show_completer()\n\n    def paintEvent(self, event):\n        super(SettingsLineEdit, self).paintEvent(event)\n        self._update_completer()\n\n    def update_completer_values(self, values):\n        if not values and self._completer is None:\n            return\n\n        self._create_completer()\n\n        self._completer.update_values(values)\n\n    def _create_completer(self):\n        if self._completer is None:\n            self._completer = CompleterView(self)\n            self._completer.row_activated.connect(self._completer_activated)\n\n    def _completer_activated(self, text):\n        self.setText(text)\n\n    def keyPressEvent(self, event):\n        if self._completer is None:\n            super(SettingsLineEdit, self).keyPressEvent(event)\n            return\n\n        key = event.key()\n        if key == QtCore.Qt.Key_Up:\n            self._completer.move_up()\n        elif key == QtCore.Qt.Key_Down:\n            self._completer.move_down()\n        elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):\n            self._completer.enter_pressed()\n        else:\n            super(SettingsLineEdit, self).keyPressEvent(event)\n\n\nclass SettingsPlainTextEdit(QtWidgets.QPlainTextEdit):\n    focused_in = QtCore.Signal()\n    _min_lines = 0\n\n    def focusInEvent(self, event):\n        super(SettingsPlainTextEdit, self).focusInEvent(event)\n        self.focused_in.emit()\n\n    def set_minimum_lines(self, lines):\n        self._min_lines = lines\n        self.update()\n\n    def minimumSizeHint(self):\n        result = super(SettingsPlainTextEdit, self).minimumSizeHint()\n        if self._min_lines < 1:\n            return result\n        document = self.document()\n        margins = self.contentsMargins()\n        d_margin = (\n            ((document.documentMargin() + self.frameWidth()) * 2)\n            + margins.top() + margins.bottom()\n        )\n        font = document.defaultFont()\n        font_metrics = QtGui.QFontMetrics(font)\n        result.setHeight(\n            d_margin + (font_metrics.lineSpacing() * self._min_lines))\n        return result\n\n\nclass SettingsToolBtn(ImageButton):\n    _mask_pixmap = None\n    _cached_icons = {}\n\n    def __init__(self, btn_type, parent):\n        super(SettingsToolBtn, self).__init__(parent)\n\n        icon, hover_icon = self._get_icon_type(btn_type)\n\n        self.setIcon(icon)\n\n        self._icon = icon\n        self._hover_icon = hover_icon\n\n    @classmethod\n    def _get_icon_type(cls, btn_type):\n        if btn_type not in cls._cached_icons:\n            settings_colors = get_objected_colors(\"settings\")\n            normal_color = settings_colors[\"image-btn\"].get_qcolor()\n            hover_color = settings_colors[\"image-btn-hover\"].get_qcolor()\n            disabled_color = settings_colors[\"image-btn-disabled\"].get_qcolor()\n\n            image = get_image(\"{}.png\".format(btn_type))\n\n            pixmap = paint_image_with_color(image, normal_color)\n            hover_pixmap = paint_image_with_color(image, hover_color)\n            disabled_pixmap = paint_image_with_color(image, disabled_color)\n\n            icon = QtGui.QIcon(pixmap)\n            hover_icon = QtGui.QIcon(hover_pixmap)\n            icon.addPixmap(\n                disabled_pixmap, QtGui.QIcon.Disabled, QtGui.QIcon.On\n            )\n            icon.addPixmap(\n                disabled_pixmap, QtGui.QIcon.Disabled, QtGui.QIcon.Off\n            )\n            hover_icon.addPixmap(\n                disabled_pixmap, QtGui.QIcon.Disabled, QtGui.QIcon.On\n            )\n            hover_icon.addPixmap(\n                disabled_pixmap, QtGui.QIcon.Disabled, QtGui.QIcon.Off\n            )\n            cls._cached_icons[btn_type] = icon, hover_icon\n        return cls._cached_icons[btn_type]\n\n    def enterEvent(self, event):\n        self.setIcon(self._hover_icon)\n        super(SettingsToolBtn, self).enterEvent(event)\n\n    def leaveEvent(self, event):\n        self.setIcon(self._icon)\n        super(SettingsToolBtn, self).leaveEvent(event)\n\n    @classmethod\n    def _get_mask_pixmap(cls):\n        if cls._mask_pixmap is None:\n            mask_pixmap = get_pixmap(\"mask.png\")\n            cls._mask_pixmap = mask_pixmap\n        return cls._mask_pixmap\n\n    def _change_size(self):\n        super(SettingsToolBtn, self)._change_size()\n        size = self.iconSize()\n        scaled = self._get_mask_pixmap().scaled(\n            size.width(),\n            size.height(),\n            QtCore.Qt.IgnoreAspectRatio,\n            QtCore.Qt.SmoothTransformation\n        )\n        self.setMask(scaled.mask())\n\n\nclass ShadowWidget(QtWidgets.QWidget):\n    def __init__(self, message, parent):\n        super(ShadowWidget, self).__init__(parent)\n        self.setObjectName(\"ShadowWidget\")\n\n        self.parent_widget = parent\n        self.message = message\n\n        def wrapper(func):\n            def wrapped(*args, **kwarg):\n                result = func(*args, **kwarg)\n                self._update_geometry()\n                return result\n            return wrapped\n\n        parent.resizeEvent = wrapper(parent.resizeEvent)\n        parent.moveEvent = wrapper(parent.moveEvent)\n        parent.showEvent = wrapper(parent.showEvent)\n\n    def set_message(self, message):\n        self.message = message\n        if self.isVisible():\n            self.repaint()\n\n    def _update_geometry(self):\n        self.setGeometry(self.parent_widget.rect())\n\n    def paintEvent(self, event):\n        painter = QtGui.QPainter(self)\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n        painter.fillRect(\n            event.rect(), QtGui.QBrush(QtGui.QColor(0, 0, 0, 127))\n        )\n        if self.message:\n            painter.drawText(\n                event.rect(),\n                QtCore.Qt.AlignCenter | QtCore.Qt.AlignCenter,\n                self.message\n            )\n        painter.end()\n\n\nclass IconButton(QtWidgets.QPushButton):\n    def __init__(self, icon_name, color, hover_color, *args, **kwargs):\n        super(IconButton, self).__init__(*args, **kwargs)\n\n        self.icon = qtawesome.icon(icon_name, color=color)\n        self.hover_icon = qtawesome.icon(icon_name, color=hover_color)\n\n        self.setIcon(self.icon)\n\n    def enterEvent(self, event):\n        self.setIcon(self.hover_icon)\n        super(IconButton, self).enterEvent(event)\n\n    def leaveEvent(self, event):\n        self.setIcon(self.icon)\n        super(IconButton, self).leaveEvent(event)\n\n\nclass NumberSpinBox(QtWidgets.QDoubleSpinBox):\n    focused_in = QtCore.Signal()\n\n    def __init__(self, *args, **kwargs):\n        min_value = kwargs.pop(\"minimum\", -99999)\n        max_value = kwargs.pop(\"maximum\", 99999)\n        decimals = kwargs.pop(\"decimal\", 0)\n        steps = kwargs.pop(\"steps\", None)\n\n        super(NumberSpinBox, self).__init__(*args, **kwargs)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n        self.setDecimals(decimals)\n        self.setMinimum(min_value)\n        self.setMaximum(max_value)\n        if steps is not None:\n            self.setSingleStep(steps)\n\n    def focusInEvent(self, event):\n        super(NumberSpinBox, self).focusInEvent(event)\n        self.focused_in.emit()\n\n    def wheelEvent(self, event):\n        if self.hasFocus():\n            super(NumberSpinBox, self).wheelEvent(event)\n        else:\n            event.ignore()\n\n    def value(self):\n        output = super(NumberSpinBox, self).value()\n        if self.decimals() == 0:\n            output = int(output)\n        return output\n\n\nclass SettingsComboBox(QtWidgets.QComboBox):\n    value_changed = QtCore.Signal()\n    focused_in = QtCore.Signal()\n\n    def __init__(self, *args, **kwargs):\n        super(SettingsComboBox, self).__init__(*args, **kwargs)\n\n        delegate = QtWidgets.QStyledItemDelegate()\n        self.setItemDelegate(delegate)\n\n        self.currentIndexChanged.connect(self._on_change)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        self._delegate = delegate\n\n    def wheelEvent(self, event):\n        if self.hasFocus():\n            return super(SettingsComboBox, self).wheelEvent(event)\n\n    def focusInEvent(self, event):\n        self.focused_in.emit()\n        return super(SettingsComboBox, self).focusInEvent(event)\n\n    def _on_change(self, *args, **kwargs):\n        self.value_changed.emit()\n\n    def set_value(self, value):\n        for idx in range(self.count()):\n            _value = self.itemData(idx, role=QtCore.Qt.UserRole)\n            if _value == value:\n                self.setCurrentIndex(idx)\n                break\n\n    def value(self):\n        return self.itemData(self.currentIndex(), role=QtCore.Qt.UserRole)\n\n\nclass ClickableWidget(QtWidgets.QWidget):\n    clicked = QtCore.Signal()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self.clicked.emit()\n        super(ClickableWidget, self).mouseReleaseEvent(event)\n\n\nclass ExpandingWidget(QtWidgets.QWidget):\n    def __init__(self, label, parent):\n        super(ExpandingWidget, self).__init__(parent)\n\n        self.content_widget = None\n        self.toolbox_hidden = False\n\n        top_part = ClickableWidget(parent=self)\n\n        side_line_widget = QtWidgets.QWidget(top_part)\n        side_line_widget.setObjectName(\"SideLineWidget\")\n\n        button_size = QtCore.QSize(5, 5)\n        button_toggle = QtWidgets.QToolButton(parent=side_line_widget)\n        button_toggle.setObjectName(\"ExpandToggleBtn\")\n        button_toggle.setIconSize(button_size)\n        button_toggle.setArrowType(QtCore.Qt.RightArrow)\n        button_toggle.setCheckable(True)\n        button_toggle.setChecked(False)\n\n        label_widget = QtWidgets.QLabel(label, parent=side_line_widget)\n        label_widget.setObjectName(\"ExpandLabel\")\n\n        before_label_widget = QtWidgets.QWidget(side_line_widget)\n        before_label_layout = QtWidgets.QHBoxLayout(before_label_widget)\n        before_label_layout.setContentsMargins(0, 0, 0, 0)\n\n        after_label_widget = QtWidgets.QWidget(side_line_widget)\n        after_label_layout = QtWidgets.QHBoxLayout(after_label_widget)\n        after_label_layout.setContentsMargins(0, 0, 0, 0)\n\n        side_line_layout = QtWidgets.QHBoxLayout(side_line_widget)\n        side_line_layout.setContentsMargins(5, 10, 0, 10)\n        side_line_layout.addWidget(button_toggle)\n        side_line_layout.addWidget(before_label_widget)\n        side_line_layout.addWidget(label_widget)\n        side_line_layout.addWidget(after_label_widget)\n        side_line_layout.addStretch(1)\n\n        top_part_layout = QtWidgets.QHBoxLayout(top_part)\n        top_part_layout.setContentsMargins(0, 0, 0, 0)\n        top_part_layout.addWidget(side_line_widget)\n\n        before_label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        after_label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        self.top_part_ending = None\n        self.after_label_layout = after_label_layout\n        self.before_label_layout = before_label_layout\n\n        self.side_line_widget = side_line_widget\n        self.side_line_layout = side_line_layout\n        self.button_toggle = button_toggle\n        self.label_widget = label_widget\n\n        top_part.clicked.connect(self._top_part_clicked)\n        self.button_toggle.clicked.connect(self._btn_clicked)\n\n        self.main_layout = QtWidgets.QVBoxLayout(self)\n        self.main_layout.setContentsMargins(0, 0, 0, 0)\n        self.main_layout.setSpacing(0)\n        self.main_layout.addWidget(top_part)\n\n        self.top_part = top_part\n\n    def hide_toolbox(self, hide_content=False):\n        self.button_toggle.setArrowType(QtCore.Qt.NoArrow)\n        self.toolbox_hidden = True\n        if self.content_widget:\n            self.content_widget.setVisible(not hide_content)\n        self.parent().updateGeometry()\n\n    def show_toolbox(self):\n        self.toolbox_hidden = False\n        self.toggle_content(self.button_toggle.isChecked())\n\n        self.parent().updateGeometry()\n\n    def set_content_widget(self, content_widget):\n        content_widget.setVisible(False)\n        self.main_layout.addWidget(content_widget)\n        self.content_widget = content_widget\n\n    def is_expanded(self):\n        return self.button_toggle.isChecked()\n\n    def _btn_clicked(self):\n        self.toggle_content(self.button_toggle.isChecked())\n\n    def _top_part_clicked(self):\n        self.toggle_content()\n\n    def toggle_content(self, *args):\n        if self.toolbox_hidden:\n            return\n\n        if len(args) > 0:\n            checked = args[0]\n        else:\n            checked = not self.button_toggle.isChecked()\n        arrow_type = QtCore.Qt.RightArrow\n        if checked:\n            arrow_type = QtCore.Qt.DownArrow\n        self.button_toggle.setChecked(checked)\n        self.button_toggle.setArrowType(arrow_type)\n        if self.content_widget:\n            self.content_widget.setVisible(checked)\n        self.parent().updateGeometry()\n\n    def add_widget_after_label(self, widget):\n        self.after_label_layout.addWidget(widget)\n\n    def add_widget_before_label(self, widget):\n        self.before_label_layout.addWidget(widget)\n\n    def resizeEvent(self, event):\n        super(ExpandingWidget, self).resizeEvent(event)\n        if self.content_widget:\n            self.content_widget.updateGeometry()\n\n\nclass UnsavedChangesDialog(QtWidgets.QDialog):\n    message = \"You have unsaved changes. What do you want to do with them?\"\n\n    def __init__(self, parent=None):\n        super(UnsavedChangesDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"Unsaved changes\")\n\n        message_label = QtWidgets.QLabel(self.message)\n\n        btns_widget = QtWidgets.QWidget(self)\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n\n        btn_ok = QtWidgets.QPushButton(\"Save\")\n        btn_ok.clicked.connect(self.on_ok_pressed)\n        btn_discard = QtWidgets.QPushButton(\"Discard\")\n        btn_discard.clicked.connect(self.on_discard_pressed)\n        btn_cancel = QtWidgets.QPushButton(\"Cancel\")\n        btn_cancel.clicked.connect(self.on_cancel_pressed)\n\n        btns_layout.addWidget(btn_ok)\n        btns_layout.addWidget(btn_discard)\n        btns_layout.addWidget(btn_cancel)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(message_label)\n        layout.addWidget(btns_widget)\n\n    def on_cancel_pressed(self):\n        self.done(0)\n\n    def on_ok_pressed(self):\n        self.done(1)\n\n    def on_discard_pressed(self):\n        self.done(2)\n\n\nclass RestartDialog(QtWidgets.QDialog):\n    message = (\n        \"Your changes require restart of process to take effect.\"\n        \" Do you want to restart now?\"\n    )\n\n    def __init__(self, parent=None):\n        super(RestartDialog, self).__init__(parent)\n        message_label = QtWidgets.QLabel(self.message)\n\n        btns_widget = QtWidgets.QWidget(self)\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n\n        btn_restart = QtWidgets.QPushButton(\"Restart\")\n        btn_restart.clicked.connect(self.on_restart_pressed)\n        btn_cancel = QtWidgets.QPushButton(\"Cancel\")\n        btn_cancel.clicked.connect(self.on_cancel_pressed)\n\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(btn_restart)\n        btns_layout.addWidget(btn_cancel)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(message_label)\n        layout.addWidget(btns_widget)\n\n        self.btn_cancel = btn_cancel\n        self.btn_restart = btn_restart\n\n    def showEvent(self, event):\n        super(RestartDialog, self).showEvent(event)\n        btns_width = max(self.btn_cancel.width(), self.btn_restart.width())\n        self.btn_cancel.setFixedWidth(btns_width)\n        self.btn_restart.setFixedWidth(btns_width)\n\n    def on_cancel_pressed(self):\n        self.done(0)\n\n    def on_restart_pressed(self):\n        self.done(1)\n\n\nclass SpacerWidget(QtWidgets.QWidget):\n    def __init__(self, parent=None):\n        super(SpacerWidget, self).__init__(parent)\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n\nclass GridLabelWidget(QtWidgets.QWidget):\n    def __init__(self, label, parent=None):\n        super(GridLabelWidget, self).__init__(parent)\n\n        self.input_field = None\n\n        self.properties = {}\n\n        label_widget = QtWidgets.QLabel(label, self)\n        label_widget.setObjectName(\"SettingsLabel\")\n\n        label_proxy_layout = QtWidgets.QHBoxLayout()\n        label_proxy_layout.setContentsMargins(0, 0, 0, 0)\n        label_proxy_layout.setSpacing(0)\n\n        label_proxy_layout.addWidget(label_widget, 0, QtCore.Qt.AlignRight)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 2, 0, 0)\n        layout.setSpacing(0)\n\n        layout.addLayout(label_proxy_layout, 0)\n        layout.addStretch(1)\n\n        label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        self.label_widget = label_widget\n\n    def setProperty(self, name, value):\n        cur_value = self.properties.get(name)\n        if cur_value == value:\n            return\n\n        self.label_widget.setProperty(name, value)\n        self.label_widget.style().polish(self.label_widget)\n\n    def mouseReleaseEvent(self, event):\n        if self.input_field:\n            if event and event.button() == QtCore.Qt.LeftButton:\n                self.input_field.focused_in()\n            return self.input_field.show_actions_menu(event)\n        return super(GridLabelWidget, self).mouseReleaseEvent(event)\n\n\nclass SettingsNiceCheckbox(NiceCheckbox):\n    focused_in = QtCore.Signal()\n\n    def mousePressEvent(self, event):\n        self.focused_in.emit()\n        super(SettingsNiceCheckbox, self).mousePressEvent(event)\n\n\nclass ProjectModel(QtGui.QStandardItemModel):\n    _update_versions = QtCore.Signal()\n\n    def __init__(self, only_active, *args, **kwargs):\n        super(ProjectModel, self).__init__(*args, **kwargs)\n\n        self.setColumnCount(2)\n\n        self._only_active = only_active\n        self._default_item = None\n        self._items_by_name = {}\n        self._versions_by_project = {}\n\n        font_color = get_objected_colors(\"font\").get_qcolor()\n        font_color.setAlpha(67)\n        self._version_font_color = font_color\n        self._current_version = get_openpype_version()\n\n        self._version_refresh_threads = []\n        self._version_refresh_id = None\n\n        self._update_versions.connect(self._on_update_versions_signal)\n\n    def _on_update_versions_signal(self):\n        for project_name, version in self._versions_by_project.items():\n            if project_name is None:\n                item = self._default_item\n            else:\n                item = self._items_by_name.get(project_name)\n\n            if item and version != self._current_version:\n                item.setData(version, PROJECT_VERSION_ROLE)\n\n    def _fetch_settings_versions(self):\n        \"\"\"Used versions per project are loaded in thread to not stuck UI.\"\"\"\n        version_refresh_id = self._version_refresh_id\n        all_project_names = list(self._items_by_name.keys())\n        all_project_names.append(None)\n        closest_by_project_name = find_closest_version_for_projects(\n            all_project_names\n        )\n        if self._version_refresh_id == version_refresh_id:\n            self._versions_by_project = closest_by_project_name\n            self._update_versions.emit()\n\n    def flags(self, index):\n        if index.column() == 1:\n            index = self.index(index.row(), 0, index.parent())\n        return super(ProjectModel, self).flags(index)\n\n    def refresh(self):\n        # Change id of versions refresh\n        self._version_refresh_id = uuid.uuid4()\n\n        new_items = []\n        if self._default_item is None:\n            item = QtGui.QStandardItem(DEFAULT_PROJECT_LABEL)\n            item.setData(None, PROJECT_NAME_ROLE)\n            item.setData(True, PROJECT_IS_ACTIVE_ROLE)\n            item.setData(False, PROJECT_IS_SELECTED_ROLE)\n            new_items.append(item)\n            self._default_item = item\n\n        self._default_item.setData(\"\", PROJECT_VERSION_ROLE)\n        project_names = set()\n        for project_doc in get_projects(\n            inactive=not self._only_active,\n            fields=[\"name\", \"data.active\"]\n        ):\n            project_name = project_doc[\"name\"]\n            project_names.add(project_name)\n            if project_name in self._items_by_name:\n                item = self._items_by_name[project_name]\n            else:\n                item = QtGui.QStandardItem(project_name)\n\n                self._items_by_name[project_name] = item\n                new_items.append(item)\n\n            is_active = project_doc.get(\"data\", {}).get(\"active\", True)\n            item.setData(project_name, PROJECT_NAME_ROLE)\n            item.setData(is_active, PROJECT_IS_ACTIVE_ROLE)\n            item.setData(\"\", PROJECT_VERSION_ROLE)\n            item.setData(False, PROJECT_IS_SELECTED_ROLE)\n\n            if not is_active:\n                font = item.font()\n                font.setItalic(True)\n                item.setFont(font)\n\n        root_item = self.invisibleRootItem()\n        for project_name in tuple(self._items_by_name.keys()):\n            if project_name not in project_names:\n                item = self._items_by_name.pop(project_name)\n                root_item.removeRow(item.row())\n\n        if new_items:\n            root_item.appendRows(new_items)\n\n        # Fetch versions per project in thread\n        thread = DynamicQThread(self._fetch_settings_versions)\n        self._version_refresh_threads.append(thread)\n        thread.start()\n\n        # Cleanup done threads\n        for thread in tuple(self._version_refresh_threads):\n            if thread.isFinished():\n                self._version_refresh_threads.remove(thread)\n\n    def data(self, index, role=QtCore.Qt.DisplayRole):\n        if index.column() == 1:\n            if role == QtCore.Qt.TextAlignmentRole:\n                return QtCore.Qt.AlignRight\n            if role == QtCore.Qt.ForegroundRole:\n                return self._version_font_color\n            index = self.index(index.row(), 0, index.parent())\n            if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):\n                role = PROJECT_VERSION_ROLE\n\n        return super(ProjectModel, self).data(index, role)\n\n    def setData(self, index, value, role=QtCore.Qt.EditRole):\n        if index.column() == 1:\n            index = self.index(index.row(), 0, index.parent())\n            if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):\n                role = PROJECT_VERSION_ROLE\n        return super(ProjectModel, self).setData(index, value, role)\n\n    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):\n        if role == QtCore.Qt.DisplayRole:\n            if section == 0:\n                return \"Project name\"\n\n            elif section == 1:\n                return \"Used version\"\n            return \"\"\n        return super(ProjectModel, self).headerData(\n            section, orientation, role\n        )\n\n\nclass VersionAction(QtWidgets.QAction):\n    version_triggered = QtCore.Signal(str)\n\n    def __init__(self, version, *args, **kwargs):\n        super(VersionAction, self).__init__(version, *args, **kwargs)\n        self._version = version\n        self.triggered.connect(self._on_trigger)\n\n    def _on_trigger(self):\n        self.version_triggered.emit(self._version)\n\n\nclass ProjectView(QtWidgets.QTreeView):\n    left_mouse_released_at = QtCore.Signal(QtCore.QModelIndex)\n    right_mouse_released_at = QtCore.Signal(QtCore.QModelIndex)\n\n    def __init__(self, *args, **kwargs):\n        super(ProjectView, self).__init__(*args, **kwargs)\n        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        self.setIndentation(0)\n\n        # Do not allow editing\n        self.setEditTriggers(\n            QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers\n        )\n        # Do not automatically handle selection\n        self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)\n        self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)\n\n    def mouseReleaseEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            index = self.indexAt(event.pos())\n            self.left_mouse_released_at.emit(index)\n\n        elif event.button() == QtCore.Qt.RightButton:\n            index = self.indexAt(event.pos())\n            self.right_mouse_released_at.emit(index)\n\n        super(ProjectView, self).mouseReleaseEvent(event)\n\n\nclass ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):\n    def __init__(self, *args, **kwargs):\n        super(ProjectSortFilterProxy, self).__init__(*args, **kwargs)\n        self._enable_filter = True\n\n    def lessThan(self, left_index, right_index):\n        if left_index.data(PROJECT_NAME_ROLE) is None:\n            return True\n\n        if right_index.data(PROJECT_NAME_ROLE) is None:\n            return False\n\n        left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE)\n        right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE)\n        if right_is_active == left_is_active:\n            return super(ProjectSortFilterProxy, self).lessThan(\n                left_index, right_index\n            )\n\n        if left_is_active:\n            return True\n        return False\n\n    def filterAcceptsRow(self, source_row, source_parent):\n        if not self._enable_filter:\n            return True\n\n        index = self.sourceModel().index(source_row, 0, source_parent)\n        is_active = bool(index.data(self.filterRole()))\n        is_selected = bool(index.data(PROJECT_IS_SELECTED_ROLE))\n\n        return is_active or is_selected\n\n    def is_filter_enabled(self):\n        return self._enable_filter\n\n    def set_filter_enabled(self, value):\n        self._enable_filter = value\n        self.invalidateFilter()\n\n\nclass ProjectListWidget(QtWidgets.QWidget):\n    project_changed = QtCore.Signal()\n    version_change_requested = QtCore.Signal(str)\n    extract_to_file_requested = QtCore.Signal()\n\n    def __init__(self, parent, only_active=False):\n        self._parent = parent\n\n        self._entity = None\n        self.current_project = None\n        self._edit_mode = True\n\n        super(ProjectListWidget, self).__init__(parent)\n        self.setObjectName(\"ProjectListWidget\")\n\n        content_frame = QtWidgets.QFrame(self)\n        content_frame.setObjectName(\"ProjectListContentWidget\")\n\n        project_list = ProjectView(content_frame)\n        project_model = ProjectModel(only_active)\n        project_proxy = ProjectSortFilterProxy()\n\n        project_proxy.setFilterRole(PROJECT_IS_ACTIVE_ROLE)\n        project_proxy.setSourceModel(project_model)\n        project_list.setModel(project_proxy)\n\n        content_layout = QtWidgets.QVBoxLayout(content_frame)\n        content_layout.setContentsMargins(0, 0, 0, 0)\n        content_layout.setSpacing(0)\n        content_layout.addWidget(project_list, 1)\n\n        inactive_chk = None\n        if not only_active:\n            checkbox_wrapper = QtWidgets.QWidget(content_frame)\n            checkbox_wrapper.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n            inactive_chk = QtWidgets.QCheckBox(\n                \"Show Inactive Projects\", checkbox_wrapper\n            )\n            inactive_chk.setChecked(not project_proxy.is_filter_enabled())\n\n            wrapper_layout = QtWidgets.QHBoxLayout(checkbox_wrapper)\n            wrapper_layout.addWidget(inactive_chk, 1)\n\n            content_layout.addWidget(checkbox_wrapper, 0)\n\n            inactive_chk.stateChanged.connect(self.on_inactive_vis_changed)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        # Margins '3' are matching to configurables widget scroll area on right\n        layout.setContentsMargins(5, 3, 3, 3)\n        layout.addWidget(content_frame, 1)\n\n        project_list.left_mouse_released_at.connect(self.on_item_clicked)\n        project_list.right_mouse_released_at.connect(\n            self._on_item_right_clicked\n        )\n\n        self.project_list = project_list\n        self.project_proxy = project_proxy\n        self.project_model = project_model\n        self.inactive_chk = inactive_chk\n\n    def set_edit_mode(self, enabled):\n        if self._edit_mode is not enabled:\n            self._edit_mode = enabled\n\n    def set_entity(self, entity):\n        self._entity = entity\n\n    def _on_item_right_clicked(self, index):\n        if not index.isValid():\n            return\n        project_name = index.data(PROJECT_NAME_ROLE)\n        if project_name is None:\n            project_name = DEFAULT_PROJECT_LABEL\n\n        if self.current_project != project_name:\n            self.on_item_clicked(index)\n\n        if self.current_project != project_name:\n            return\n\n        if not self._entity:\n            return\n\n        versions = self._entity.get_available_source_versions(sorted=True)\n        if not versions:\n            return\n\n        menu = QtWidgets.QMenu(self)\n        submenu = QtWidgets.QMenu(\"Use settings from version\", menu)\n        for version in reversed(versions):\n            action = VersionAction(version, submenu)\n            action.version_triggered.connect(\n                self.version_change_requested\n            )\n            submenu.addAction(action)\n\n        extract_action = QtWidgets.QAction(\"Extract to file\", menu)\n        extract_action.triggered.connect(self.extract_to_file_requested)\n\n        menu.addMenu(submenu)\n        menu.addAction(extract_action)\n        menu.exec_(QtGui.QCursor.pos())\n\n    def on_item_clicked(self, new_index):\n        if not new_index.isValid():\n            return\n        new_project_name = new_index.data(PROJECT_NAME_ROLE)\n        if new_project_name is None:\n            new_project_name = DEFAULT_PROJECT_LABEL\n\n        if self.current_project == new_project_name:\n            return\n\n        save_changes = False\n        change_project = False\n        if not self._edit_mode or self.validate_context_change():\n            change_project = True\n\n        else:\n            dialog = UnsavedChangesDialog(self)\n            result = dialog.exec_()\n            if result == 1:\n                save_changes = True\n                change_project = True\n\n            elif result == 2:\n                change_project = True\n\n        if save_changes:\n            self._parent._save()\n\n        if change_project:\n            self.select_project(new_project_name)\n            self.current_project = new_project_name\n            self.project_changed.emit()\n        else:\n            self.select_project(self.current_project)\n\n    def on_inactive_vis_changed(self):\n        if self.inactive_chk is None:\n            # should not happen.\n            return\n\n        enable_filter = not self.inactive_chk.isChecked()\n        self.project_proxy.set_filter_enabled(enable_filter)\n\n    def validate_context_change(self):\n        return not self._parent.entity.has_unsaved_changes\n\n    def project_name(self):\n        if self.current_project == DEFAULT_PROJECT_LABEL:\n            return None\n        return self.current_project\n\n    def select_default_project(self):\n        self.select_project(DEFAULT_PROJECT_LABEL)\n\n    def select_project(self, project_name):\n        model = self.project_model\n        proxy = self.project_proxy\n\n        found_items = model.findItems(project_name)\n        if not found_items:\n            found_items = model.findItems(DEFAULT_PROJECT_LABEL)\n\n        index = model.indexFromItem(found_items[0])\n        model.setData(index, True, PROJECT_IS_SELECTED_ROLE)\n\n        src_indexes = []\n        col_count = model.columnCount()\n        if col_count > 1:\n            for col in range(col_count):\n                src_indexes.append(\n                    model.index(index.row(), col, index.parent())\n                )\n        dst_indexes = []\n        for index in src_indexes:\n            dst_indexes.append(proxy.mapFromSource(index))\n\n        selection_model = self.project_list.selectionModel()\n        selection_model.clear()\n\n        first = True\n        for index in dst_indexes:\n            if first:\n                selection_model.setCurrentIndex(\n                    index,\n                    QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent\n                )\n                first = False\n                continue\n            selection_model.select(index, QtCore.QItemSelectionModel.Select)\n\n    def get_project_names(self):\n        output = []\n        for row in range(self.project_proxy.rowCount()):\n            index = self.project_proxy.index(row, 0)\n            output.append(index.data(PROJECT_NAME_ROLE))\n        return output\n\n    def refresh(self):\n        selected_project = None\n        for index in self.project_list.selectedIndexes():\n            selected_project = index.data(PROJECT_NAME_ROLE)\n            break\n\n        self.project_model.refresh()\n\n        self.project_proxy.sort(0)\n\n        self.select_project(selected_project)\n\n        self.current_project = self.project_list.currentIndex().data(\n            PROJECT_NAME_ROLE\n        )\n        self.project_list.resizeColumnToContents(0)\n"
  },
  {
    "path": "openpype/tools/settings/settings/window.py",
    "content": "from qtpy import QtWidgets, QtGui, QtCore\n\nfrom openpype import style\n\nfrom openpype.lib import is_admin_password_required\nfrom openpype.lib.events import EventSystem\nfrom openpype.widgets import PasswordDialog\n\nfrom openpype.settings.lib import (\n    get_last_opened_info,\n    opened_settings_ui,\n    closed_settings_ui,\n)\n\nfrom .dialogs import SettingsUIOpenedElsewhere\nfrom .categories import (\n    CategoryState,\n    SystemWidget,\n    ProjectWidget\n)\nfrom .widgets import (\n    ShadowWidget,\n    RestartDialog,\n    SettingsTabWidget\n)\nfrom .search_dialog import SearchEntitiesDialog\n\n\nclass SettingsController:\n    \"\"\"Controller for settings tools.\n\n    Added when tool was finished for checks of last opened in settings\n    categories and being able communicated with main widget logic.\n    \"\"\"\n\n    def __init__(self, user_role):\n        self._user_role = user_role\n        self._event_system = EventSystem()\n\n        self._opened_info = None\n        self._last_opened_info = None\n        self._edit_mode = None\n\n    @property\n    def user_role(self):\n        return self._user_role\n\n    @property\n    def event_system(self):\n        return self._event_system\n\n    @property\n    def opened_info(self):\n        return self._opened_info\n\n    @property\n    def last_opened_info(self):\n        return self._last_opened_info\n\n    @property\n    def edit_mode(self):\n        return self._edit_mode\n\n    def ui_closed(self):\n        if self._opened_info is not None:\n            closed_settings_ui(self._opened_info)\n\n        self._opened_info = None\n        self._edit_mode = None\n\n    def set_edit_mode(self, enabled):\n        if self._edit_mode is enabled:\n            return\n\n        opened_info = None\n        if enabled:\n            opened_info = opened_settings_ui()\n            self._last_opened_info = opened_info\n\n        self._opened_info = opened_info\n        self._edit_mode = enabled\n\n        self.event_system.emit(\n            \"edit.mode.changed\",\n            {\"edit_mode\": enabled},\n            \"controller\"\n        )\n\n    def update_last_opened_info(self):\n        last_opened_info = get_last_opened_info()\n        enabled = False\n        if (\n            last_opened_info is None\n            or self._opened_info == last_opened_info\n        ):\n            enabled = True\n\n        self._last_opened_info = last_opened_info\n\n        self.set_edit_mode(enabled)\n\n\nclass MainWidget(QtWidgets.QWidget):\n    trigger_restart = QtCore.Signal()\n\n    widget_width = 1000\n    widget_height = 600\n    window_title = \"OpenPype Settings\"\n\n    def __init__(self, user_role, parent=None, reset_on_show=True):\n        super(MainWidget, self).__init__(parent)\n\n        controller = SettingsController(user_role)\n\n        # Object referencing to this machine and time when UI was opened\n        # - is used on close event\n        self._main_reset = False\n        self._controller = controller\n\n        self._user_passed = False\n        self._reset_on_show = reset_on_show\n\n        self._password_dialog = None\n\n        self.setObjectName(\"SettingsMainWidget\")\n        self.setWindowTitle(self.window_title)\n\n        self.resize(self.widget_width, self.widget_height)\n\n        stylesheet = style.load_stylesheet()\n        self.setStyleSheet(stylesheet)\n        self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))\n\n        header_tab_widget = SettingsTabWidget(parent=self)\n\n        studio_widget = SystemWidget(controller, header_tab_widget)\n        project_widget = ProjectWidget(controller, header_tab_widget)\n\n        tab_widgets = [\n            studio_widget,\n            project_widget\n        ]\n\n        header_tab_widget.addTab(studio_widget, \"System\")\n        header_tab_widget.addTab(project_widget, \"Project\")\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(5, 5, 5, 5)\n        layout.setSpacing(0)\n        layout.addWidget(header_tab_widget)\n\n        self.setLayout(layout)\n\n        search_dialog = SearchEntitiesDialog(self)\n\n        self._shadow_widget = ShadowWidget(\"Working...\", self)\n        self._shadow_widget.setVisible(False)\n\n        controller.event_system.add_callback(\n            \"edit.mode.changed\",\n            self._edit_mode_changed\n        )\n\n        header_tab_widget.currentChanged.connect(self._on_tab_changed)\n        search_dialog.path_clicked.connect(self._on_search_path_clicked)\n\n        for tab_widget in tab_widgets:\n            tab_widget.saved.connect(self._on_tab_save)\n            tab_widget.state_changed.connect(self._on_state_change)\n            tab_widget.restart_required_trigger.connect(\n                self._on_restart_required\n            )\n            tab_widget.reset_started.connect(self._on_reset_started)\n            tab_widget.reset_finished.connect(self._on_reset_finished)\n            tab_widget.full_path_requested.connect(self._on_full_path_request)\n\n        header_tab_widget.context_menu_requested.connect(\n            self._on_context_menu_request\n        )\n\n        self._header_tab_widget = header_tab_widget\n        self.tab_widgets = tab_widgets\n        self._search_dialog = search_dialog\n\n    def _on_tab_save(self, source_widget):\n        for tab_widget in self.tab_widgets:\n            tab_widget.on_saved(source_widget)\n\n    def _on_state_change(self):\n        any_working = False\n        for widget in self.tab_widgets:\n            if widget.state is CategoryState.Working:\n                any_working = True\n                break\n\n        if (\n            (any_working and self._shadow_widget.isVisible())\n            or (not any_working and not self._shadow_widget.isVisible())\n        ):\n            return\n\n        self._shadow_widget.setVisible(any_working)\n\n        # Process events to apply shadow widget visibility\n        app = QtWidgets.QApplication.instance()\n        if app:\n            app.processEvents()\n\n    def _on_full_path_request(self, category, path):\n        for tab_widget in self.tab_widgets:\n            if tab_widget.contain_category_key(category):\n                idx = self._header_tab_widget.indexOf(tab_widget)\n                self._header_tab_widget.setCurrentIndex(idx)\n                tab_widget.set_category_path(category, path)\n                break\n\n    def _on_context_menu_request(self, tab_idx):\n        widget = self._header_tab_widget.widget(tab_idx)\n        if not widget:\n            return\n\n        menu = QtWidgets.QMenu(self)\n        widget.add_context_actions(menu)\n        if menu.actions():\n            result = menu.exec_(QtGui.QCursor.pos())\n            if result is not None:\n                self._header_tab_widget.setCurrentIndex(tab_idx)\n\n    def showEvent(self, event):\n        super(MainWidget, self).showEvent(event)\n\n        if self._reset_on_show:\n            self._reset_on_show = False\n            # Trigger reset with 100ms delay\n            QtCore.QTimer.singleShot(100, self.reset)\n\n    def closeEvent(self, event):\n        self._controller.ui_closed()\n\n        super(MainWidget, self).closeEvent(event)\n\n    def _check_on_reset(self):\n        self._controller.update_last_opened_info()\n        if self._controller.edit_mode:\n            return\n\n        # if self._edit_mode is False:\n        #     return\n\n        dialog = SettingsUIOpenedElsewhere(\n            self._controller.last_opened_info, self\n        )\n        dialog.exec_()\n        self._controller.set_edit_mode(dialog.result() == 1)\n\n    def _show_password_dialog(self):\n        if self._password_dialog:\n            self._password_dialog.open()\n\n    def _on_password_dialog_close(self, password_passed):\n        # Store result for future settings reset\n        self._user_passed = password_passed\n        # Remove reference to password dialog\n        self._password_dialog = None\n        if password_passed:\n            self.reset()\n            if not self.isVisible():\n                self.show()\n        else:\n            self.close()\n\n    def reset(self):\n        if self._password_dialog:\n            return\n\n        if not self._user_passed:\n            self._user_passed = not is_admin_password_required()\n\n        self._on_state_change()\n\n        if not self._user_passed:\n            # Avoid doubled dialog\n            dialog = PasswordDialog(self)\n            dialog.setModal(True)\n            dialog.finished.connect(self._on_password_dialog_close)\n\n            self._password_dialog = dialog\n\n            QtCore.QTimer.singleShot(100, self._show_password_dialog)\n\n            return\n\n        if self._reset_on_show:\n            self._reset_on_show = False\n\n        self._main_reset = True\n        for tab_widget in self.tab_widgets:\n            tab_widget.reset()\n        self._main_reset = False\n        self._check_on_reset()\n\n    def _update_search_dialog(self, clear=False):\n        if self._search_dialog.isVisible():\n            entity = None\n            if not clear:\n                widget = self._header_tab_widget.currentWidget()\n                entity = widget.entity\n            self._search_dialog.set_root_entity(entity)\n\n    def _edit_mode_changed(self, event):\n        title = self.window_title\n        if not event[\"edit_mode\"]:\n            title += \" [View only]\"\n        self.setWindowTitle(title)\n\n    def _on_tab_changed(self):\n        self._update_search_dialog()\n\n    def _on_search_path_clicked(self, path):\n        widget = self._header_tab_widget.currentWidget()\n        widget.change_path(path)\n\n    def _on_restart_required(self):\n        # Don't show dialog if there are not registered slots for\n        #   `trigger_restart` signal.\n        # - For example when settings are running as standalone tool\n        # - PySide2 and PyQt5 compatible way how to find out\n        method_index = self.metaObject().indexOfMethod(\"trigger_restart()\")\n        method = self.metaObject().method(method_index)\n        if not self.isSignalConnected(method):\n            return\n\n        dialog = RestartDialog(self)\n        result = dialog.exec_()\n        if result == 1:\n            self.trigger_restart.emit()\n\n    def _on_reset_started(self):\n        widget = self.sender()\n        current_widget = self._header_tab_widget.currentWidget()\n        if current_widget is widget:\n            self._update_search_dialog(True)\n\n    def _on_reset_finished(self):\n        widget = self.sender()\n        current_widget = self._header_tab_widget.currentWidget()\n        if current_widget is widget:\n            self._update_search_dialog()\n\n        if not self._main_reset:\n            self._check_on_reset()\n\n    def keyPressEvent(self, event):\n        if event.matches(QtGui.QKeySequence.Find):\n            # todo: search in all widgets (or in active)?\n            widget = self._header_tab_widget.currentWidget()\n            self._search_dialog.show()\n            self._search_dialog.set_root_entity(widget.entity)\n            event.accept()\n            return\n\n        return super(MainWidget, self).keyPressEvent(event)\n"
  },
  {
    "path": "openpype/tools/settings/settings/wrapper_widgets.py",
    "content": "from uuid import uuid4\nfrom qtpy import QtWidgets\n\nfrom .widgets import (\n    ExpandingWidget,\n    GridLabelWidget\n)\nfrom openpype.tools.settings import CHILD_OFFSET\n\n\nclass WrapperWidget(QtWidgets.QWidget):\n    def __init__(self, schema_data, parent=None):\n        super(WrapperWidget, self).__init__(parent)\n\n        self.entity = None\n        self.id = uuid4()\n        self.schema_data = schema_data\n        self.input_fields = []\n\n        self.create_ui()\n\n    def make_sure_is_visible(self, *args, **kwargs):\n        changed = False\n        for input_field in self.input_fields:\n            if input_field.make_sure_is_visible(*args, **kwargs):\n                changed = True\n                break\n        return changed\n\n    def create_ui(self):\n        raise NotImplementedError(\n            \"{} does not have implemented `create_ui`.\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def add_widget_to_layout(self, widget, label=None):\n        raise NotImplementedError(\n            \"{} does not have implemented `add_widget_to_layout`.\".format(\n                self.__class__.__name__\n            )\n        )\n\n\nclass FormWrapper(WrapperWidget):\n    def create_ui(self):\n        self.content_layout = QtWidgets.QFormLayout(self)\n        self.content_layout.setContentsMargins(0, 0, 0, 0)\n\n    def add_widget_to_layout(self, widget, label=None):\n        if isinstance(widget, WrapperWidget):\n            raise TypeError(\n                \"FormWrapper can't have other wrappers as children.\"\n            )\n\n        self.input_fields.append(widget)\n\n        label_widget = GridLabelWidget(label, widget)\n        label_widget.input_field = widget\n        widget.label_widget = label_widget\n\n        self.content_layout.addRow(label_widget, widget)\n\n\nclass CollapsibleWrapper(WrapperWidget):\n    def create_ui(self):\n        self.collapsible = self.schema_data.get(\"collapsible\", True)\n        self.collapsed = self.schema_data.get(\"collapsed\", True)\n\n        content_widget = QtWidgets.QWidget(self)\n        content_widget.setObjectName(\"ContentWidget\")\n        content_widget.setProperty(\"content_state\", \"\")\n\n        content_layout = QtWidgets.QGridLayout(content_widget)\n        content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0)\n\n        body_widget = ExpandingWidget(self.schema_data[\"label\"], self)\n        body_widget.set_content_widget(content_widget)\n\n        label_widget = body_widget.label_widget\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n        if not body_widget:\n            main_layout.addWidget(content_widget)\n        else:\n            main_layout.addWidget(body_widget)\n\n        self.label_widget = label_widget\n        self.body_widget = body_widget\n        self.content_layout = content_layout\n\n        if self.collapsible:\n            if not self.collapsed:\n                body_widget.toggle_content()\n        else:\n            body_widget.hide_toolbox(hide_content=False)\n\n    def make_sure_is_visible(self, *args, **kwargs):\n        result = super(CollapsibleWrapper, self).make_sure_is_visible(\n            *args, **kwargs\n        )\n        if result:\n            self.body_widget.toggle_content(True)\n        return result\n\n    def add_widget_to_layout(self, widget, label=None):\n        self.input_fields.append(widget)\n\n        row = self.content_layout.rowCount()\n\n        if not label or isinstance(widget, WrapperWidget):\n            self.content_layout.addWidget(widget, row, 0, 1, 2)\n\n        else:\n            label_widget = GridLabelWidget(label, widget)\n            label_widget.input_field = widget\n            widget.label_widget = label_widget\n            self.content_layout.addWidget(label_widget, row, 0, 1, 1)\n            self.content_layout.addWidget(widget, row, 1, 1, 1)\n"
  },
  {
    "path": "openpype/tools/standalonepublish/__init__.py",
    "content": "from .app import (\n    main,\n    Window\n)\n\n\n__all__ = (\n    \"main\",\n    \"Window\"\n)\n"
  },
  {
    "path": "openpype/tools/standalonepublish/app.py",
    "content": "import os\nimport sys\nimport ctypes\nimport signal\n\nfrom bson.objectid import ObjectId\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.client import get_asset_by_id\n\nfrom .widgets import (\n    AssetWidget, FamilyWidget, ComponentsWidget, ShadowWidget\n)\nfrom .widgets.constants import HOST_NAME\nfrom openpype import style\nfrom openpype import resources\nfrom openpype.pipeline import AvalonMongoDB\nfrom openpype.modules import ModulesManager\n\n\nclass Window(QtWidgets.QDialog):\n    \"\"\"Main window of Standalone publisher.\n\n    :param parent: Main widget that cares about all GUIs\n    :type parent: QtWidgets.QMainWindow\n    \"\"\"\n    _jobs = {}\n    valid_family = False\n    valid_components = False\n    initialized = False\n    WIDTH = 1100\n    HEIGHT = 500\n\n    def __init__(self, pyblish_paths, parent=None):\n        super(Window, self).__init__(parent=parent)\n        self._db = AvalonMongoDB()\n        self._db.install()\n\n        try:\n            settings = QtCore.QSettings(\"pypeclub\", \"StandalonePublisher\")\n        except Exception:\n            settings = None\n\n        self._settings = settings\n\n        self.pyblish_paths = pyblish_paths\n\n        self.setWindowTitle(\"Standalone Publish\")\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)\n\n        # Validators\n        self.valid_parent = False\n\n        # assets widget\n        widget_assets = AssetWidget(self._db, settings, self)\n\n        # family widget\n        widget_family = FamilyWidget(dbcon=self._db, parent=self)\n\n        # components widget\n        widget_components = ComponentsWidget(parent=self)\n\n        # Body\n        body = QtWidgets.QSplitter()\n        body.setContentsMargins(0, 0, 0, 0)\n        body.setSizePolicy(\n            QtWidgets.QSizePolicy.Expanding,\n            QtWidgets.QSizePolicy.Expanding\n        )\n        body.setOrientation(QtCore.Qt.Horizontal)\n        body.addWidget(widget_assets)\n        body.addWidget(widget_family)\n        body.addWidget(widget_components)\n        body.setStretchFactor(body.indexOf(widget_assets), 2)\n        body.setStretchFactor(body.indexOf(widget_family), 3)\n        body.setStretchFactor(body.indexOf(widget_components), 5)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(body)\n\n        self.resize(self.WIDTH, self.HEIGHT)\n\n        # signals\n        widget_assets.selection_changed.connect(self.on_asset_changed)\n        widget_assets.task_changed.connect(self._on_task_change)\n        widget_assets.project_changed.connect(self.on_project_change)\n        widget_family.stateChanged.connect(self.set_valid_family)\n\n        self.widget_assets = widget_assets\n        self.widget_family = widget_family\n        self.widget_components = widget_components\n\n        # on start\n        self.on_start()\n\n    @property\n    def db(self):\n        ''' Returns DB object for MongoDB I/O\n        '''\n        return self._db\n\n    def on_start(self):\n        ''' Things must be done when initialized.\n        '''\n        # Refresh asset input in Family widget\n        self.on_asset_changed()\n        self.widget_components.validation()\n        # Initializing shadow widget\n        self.shadow_widget = ShadowWidget(self)\n        self.shadow_widget.setVisible(False)\n\n    def resizeEvent(self, event=None):\n        ''' Helps resize shadow widget\n        '''\n        position_x = (\n            self.frameGeometry().width()\n            - self.shadow_widget.frameGeometry().width()\n        ) / 2\n        position_y = (\n            self.frameGeometry().height()\n            - self.shadow_widget.frameGeometry().height()\n        ) / 2\n        self.shadow_widget.move(position_x, position_y)\n        w = self.frameGeometry().width()\n        h = self.frameGeometry().height()\n        self.shadow_widget.resize(QtCore.QSize(w, h))\n        if event:\n            super().resizeEvent(event)\n\n    def on_project_change(self, project_name):\n        self.widget_family.refresh()\n\n    def on_asset_changed(self):\n        '''Callback on asset selection changed\n\n        Updates the task view.\n\n        '''\n        selected = [\n            asset_id for asset_id in self.widget_assets.get_selected_assets()\n            if isinstance(asset_id, ObjectId)\n        ]\n        if len(selected) == 1:\n            self.valid_parent = True\n            project_name = self.db.active_project()\n            asset = get_asset_by_id(\n                project_name, selected[0], fields=[\"name\"]\n            )\n            self.widget_family.change_asset(asset['name'])\n        else:\n            self.valid_parent = False\n            self.widget_family.change_asset(None)\n        self.widget_family.on_data_changed()\n\n    def _on_task_change(self):\n        self.widget_family.on_task_change()\n\n    def keyPressEvent(self, event):\n        ''' Handling Ctrl+V KeyPress event\n        Can handle:\n            - files/folders in clipboard (tested only on Windows OS)\n            - copied path of file/folder in clipboard ('c:/path/to/folder')\n        '''\n        if (\n            event.key() == QtCore.Qt.Key_V\n            and event.modifiers() == QtCore.Qt.ControlModifier\n        ):\n            clip = QtWidgets.QApplication.clipboard()\n            self.widget_components.process_mime_data(clip)\n        super().keyPressEvent(event)\n\n    def working_start(self, msg=None):\n        ''' Shows shadowed foreground with message\n        :param msg: Message that will be displayed\n        (set to `Please wait...` if `None` entered)\n        :type msg: str\n        '''\n        if msg is None:\n            msg = 'Please wait...'\n        self.shadow_widget.message = msg\n        self.shadow_widget.setVisible(True)\n        self.resizeEvent()\n        QtWidgets.QApplication.processEvents()\n\n    def working_stop(self):\n        ''' Hides shadowed foreground\n        '''\n        if self.shadow_widget.isVisible():\n            self.shadow_widget.setVisible(False)\n        # Refresh version\n        self.widget_family.on_version_refresh()\n\n    def set_valid_family(self, valid):\n        ''' Sets `valid_family` attribute for validation\n\n        .. note::\n            if set to `False` publishing is not possible\n        '''\n        self.valid_family = valid\n        # If widget_components not initialized yet\n        if hasattr(self, 'widget_components'):\n            self.widget_components.validation()\n\n    def collect_data(self):\n        ''' Collecting necessary data for pyblish from child widgets\n        '''\n        data = {}\n        data.update(self.widget_assets.collect_data())\n        data.update(self.widget_family.collect_data())\n        data.update(self.widget_components.collect_data())\n\n        return data\n\n\ndef main():\n    os.environ[\"AVALON_APP\"] = HOST_NAME\n\n    # Allow to change icon of running process in windows taskbar\n    if os.name == \"nt\":\n        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(\n            u\"standalonepublish\"\n        )\n\n    qt_app = QtWidgets.QApplication([])\n    # app.setQuitOnLastWindowClosed(False)\n    qt_app.setStyleSheet(style.load_stylesheet())\n    icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n    qt_app.setWindowIcon(icon)\n\n    def signal_handler(sig, frame):\n        print(\"You pressed Ctrl+C. Process ended.\")\n        qt_app.quit()\n\n    signal.signal(signal.SIGINT, signal_handler)\n    signal.signal(signal.SIGTERM, signal_handler)\n\n    modules_manager = ModulesManager()\n    module = modules_manager.modules_by_name[\"standalonepublisher\"]\n\n    window = Window(module.publish_paths)\n    window.show()\n\n    sys.exit(qt_app.exec_())\n"
  },
  {
    "path": "openpype/tools/standalonepublish/publish.py",
    "content": "import os\nimport sys\n\nimport pyblish.api\nfrom openpype.pipeline import install_openpype_plugins\nfrom openpype.tools.utils.host_tools import show_publish\n\n\ndef main(env):\n    # Registers pype's Global pyblish plugins\n    install_openpype_plugins()\n\n    # Register additional paths\n    addition_paths_str = env.get(\"PUBLISH_PATHS\") or \"\"\n    addition_paths = addition_paths_str.split(os.pathsep)\n    for path in addition_paths:\n        path = os.path.normpath(path)\n        if not os.path.exists(path):\n            continue\n        pyblish.api.register_plugin_path(path)\n\n    return show_publish()\n\n\nif __name__ == \"__main__\":\n    result = main(os.environ)\n    sys.exit(not bool(result))\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/__init__.py",
    "content": "from qtpy import QtCore\n\nHelpRole = QtCore.Qt.UserRole + 2\nFamilyRole = QtCore.Qt.UserRole + 3\nExistsRole = QtCore.Qt.UserRole + 4\nPluginRole = QtCore.Qt.UserRole + 5\nPluginKeyRole = QtCore.Qt.UserRole + 6\n\nfrom .model_node import Node\nfrom .model_tree import TreeModel\nfrom .model_asset import AssetModel, _iter_model_rows\nfrom .model_filter_proxy_exact_match import ExactMatchesFilterProxyModel\nfrom .model_filter_proxy_recursive_sort import RecursiveSortFilterProxyModel\nfrom .model_tasks_template import TasksTemplateModel\nfrom .model_tree_view_deselectable import DeselectableTreeView\n\nfrom .widget_asset import AssetWidget\n\nfrom .widget_family_desc import FamilyDescriptionWidget\nfrom .widget_family import FamilyWidget\n\nfrom .widget_drop_empty import DropEmpty\nfrom .widget_component_item import ComponentItem\nfrom .widget_components_list import ComponentsList\nfrom .widget_drop_frame import DropDataFrame\nfrom .widget_components import ComponentsWidget\n\nfrom .widget_shadow import ShadowWidget\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/constants.py",
    "content": "HOST_NAME = \"standalonepublisher\"\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/model_asset.py",
    "content": "import logging\nimport collections\n\nfrom qtpy import QtCore, QtGui\nimport qtawesome\n\nfrom openpype.client import get_assets\nfrom openpype.style import (\n    get_default_entity_icon_color,\n    get_deprecated_entity_font_color,\n)\n\nfrom . import TreeModel, Node\n\nlog = logging.getLogger(__name__)\n\n\ndef _iter_model_rows(model,\n                     column,\n                     include_root=False):\n    \"\"\"Iterate over all row indices in a model\"\"\"\n    indices = [QtCore.QModelIndex()]  # start iteration at root\n\n    for index in indices:\n\n        # Add children to the iterations\n        child_rows = model.rowCount(index)\n        for child_row in range(child_rows):\n            child_index = model.index(child_row, column, index)\n            indices.append(child_index)\n\n        if not include_root and not index.isValid():\n            continue\n\n        yield index\n\n\nclass AssetModel(TreeModel):\n    \"\"\"A model listing assets in the active project.\n\n    The assets are displayed in a treeview, they are visually parented by\n    a `visualParent` field in the database containing an `_id` to a parent\n    asset.\n\n    \"\"\"\n\n    COLUMNS = [\"label\"]\n    Name = 0\n    Deprecated = 2\n    ObjectId = 3\n\n    DocumentRole = QtCore.Qt.UserRole + 2\n    ObjectIdRole = QtCore.Qt.UserRole + 3\n\n    def __init__(self, dbcon, parent=None):\n        super(AssetModel, self).__init__(parent=parent)\n        self.dbcon = dbcon\n\n        self._default_asset_icon_color = QtGui.QColor(\n            get_default_entity_icon_color()\n        )\n        self._deprecated_asset_font_color = QtGui.QColor(\n            get_deprecated_entity_font_color()\n        )\n\n        self.refresh()\n\n    def _add_hierarchy(self, assets, parent=None):\n        \"\"\"Add the assets that are related to the parent as children items.\n\n        This method does *not* query the database. These instead are queried\n        in a single batch upfront as an optimization to reduce database\n        queries. Resulting in up to 10x speed increase.\n\n        Args:\n            assets (dict): All assets from current project.\n        \"\"\"\n        parent_id = parent[\"_id\"] if parent else None\n        current_assets = assets.get(parent_id, list())\n\n        for asset in current_assets:\n            # get label from data, otherwise use name\n            data = asset.get(\"data\", {})\n            label = data.get(\"label\") or asset[\"name\"]\n            tags = data.get(\"tags\", [])\n\n            # store for the asset for optimization\n            deprecated = \"deprecated\" in tags\n\n            node = Node({\n                \"_id\": asset[\"_id\"],\n                \"name\": asset[\"name\"],\n                \"label\": label,\n                \"type\": asset[\"type\"],\n                \"tags\": \", \".join(tags),\n                \"deprecated\": deprecated,\n                \"_document\": asset\n            })\n            self.add_child(node, parent=parent)\n\n            # Add asset's children recursively if it has children\n            if asset[\"_id\"] in assets:\n                self._add_hierarchy(assets, parent=node)\n\n    def refresh(self):\n        \"\"\"Refresh the data for the model.\"\"\"\n\n        project_name = self.dbcon.active_project()\n        self.clear()\n        if not project_name:\n            return\n\n        self.beginResetModel()\n\n        # Get all assets in current project sorted by name\n        asset_docs = get_assets(project_name)\n        db_assets = list(\n            sorted(asset_docs, key=lambda item: item[\"name\"])\n        )\n\n        # Group the assets by their visual parent's id\n        assets_by_parent = collections.defaultdict(list)\n        for asset in db_assets:\n            parent_id = asset.get(\"data\", {}).get(\"visualParent\")\n            assets_by_parent[parent_id].append(asset)\n\n        # Build the hierarchical tree items recursively\n        self._add_hierarchy(\n            assets_by_parent,\n            parent=None\n        )\n\n        self.endResetModel()\n\n    def flags(self, index):\n        return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable\n\n    def data(self, index, role):\n\n        if not index.isValid():\n            return\n\n        node = index.internalPointer()\n        if role == QtCore.Qt.DecorationRole:        # icon\n\n            column = index.column()\n            if column == self.Name:\n\n                # Allow a custom icon and custom icon color to be defined\n                data = node.get(\"_document\", {}).get(\"data\", {})\n                icon = data.get(\"icon\", None)\n                color = data.get(\"color\", self._default_asset_icon_color)\n\n                if icon is None:\n                    # Use default icons if no custom one is specified.\n                    # If it has children show a full folder, otherwise\n                    # show an open folder\n                    has_children = self.rowCount(index) > 0\n                    icon = \"folder\" if has_children else \"folder-o\"\n\n                # Make the color darker when the asset is deprecated\n                if node.get(\"deprecated\", False):\n                    color = QtGui.QColor(color).darker(250)\n\n                try:\n                    key = \"fa.{0}\".format(icon)  # font-awesome key\n                    icon = qtawesome.icon(key, color=color)\n                    return icon\n                except Exception as exception:\n                    # Log an error message instead of erroring out completely\n                    # when the icon couldn't be created (e.g. invalid name)\n                    log.error(exception)\n\n                return\n\n        if role == QtCore.Qt.ForegroundRole:        # font color\n            if \"deprecated\" in node.get(\"tags\", []):\n                return QtGui.QColor(self._deprecated_asset_font_color)\n\n        if role == self.ObjectIdRole:\n            return node.get(\"_id\", None)\n\n        if role == self.DocumentRole:\n            return node.get(\"_document\", None)\n\n        return super(AssetModel, self).data(index, role)\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py",
    "content": "from qtpy import QtCore\n\n\nclass ExactMatchesFilterProxyModel(QtCore.QSortFilterProxyModel):\n    \"\"\"Filter model to where key column's value is in the filtered tags\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(ExactMatchesFilterProxyModel, self).__init__(*args, **kwargs)\n        self._filters = set()\n\n    def setFilters(self, filters):\n        self._filters = set(filters)\n\n    def filterAcceptsRow(self, source_row, source_parent):\n\n        # No filter\n        if not self._filters:\n            return True\n\n        else:\n            model = self.sourceModel()\n            column = self.filterKeyColumn()\n            idx = model.index(source_row, column, source_parent)\n            data = model.data(idx, self.filterRole())\n            if data in self._filters:\n                return True\n            else:\n                return False\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py",
    "content": "import re\nfrom qtpy import QtCore\n\n\nclass RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):\n    \"\"\"Filters to the regex if any of the children matches allow parent\"\"\"\n    def filterAcceptsRow(self, row, parent):\n        if hasattr(self, \"filterRegExp\"):\n            regex = self.filterRegExp()\n        else:\n            regex = self.filterRegularExpression()\n        pattern = regex.pattern()\n        if pattern:\n            model = self.sourceModel()\n            source_index = model.index(row, self.filterKeyColumn(), parent)\n            if source_index.isValid():\n                # Check current index itself\n                key = model.data(source_index, self.filterRole())\n                if re.search(pattern, key, re.IGNORECASE):\n                    return True\n\n                # Check children\n                rows = model.rowCount(source_index)\n                for i in range(rows):\n                    if self.filterAcceptsRow(i, source_index):\n                        return True\n\n                # Otherwise filter it\n                return False\n\n        return super(RecursiveSortFilterProxyModel,\n                     self).filterAcceptsRow(row, parent)\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/model_node.py",
    "content": "import logging\n\n\nlog = logging.getLogger(__name__)\n\n\nclass Node(dict):\n    \"\"\"A node that can be represented in a tree view.\n\n    The node can store data just like a dictionary.\n\n    >>> data = {\"name\": \"John\", \"score\": 10}\n    >>> node = Node(data)\n    >>> assert node[\"name\"] == \"John\"\n\n    \"\"\"\n\n    def __init__(self, data=None):\n        super(Node, self).__init__()\n\n        self._children = list()\n        self._parent = None\n\n        if data is not None:\n            assert isinstance(data, dict)\n            self.update(data)\n\n    def childCount(self):\n        return len(self._children)\n\n    def child(self, row):\n\n        if row >= len(self._children):\n            log.warning(\"Invalid row as child: {0}\".format(row))\n            return\n\n        return self._children[row]\n\n    def children(self):\n        return self._children\n\n    def parent(self):\n        return self._parent\n\n    def row(self):\n        \"\"\"\n        Returns:\n             int: Index of this node under parent\"\"\"\n        if self._parent is not None:\n            siblings = self.parent().children()\n            return siblings.index(self)\n\n    def add_child(self, child):\n        \"\"\"Add a child to this node\"\"\"\n        child._parent = self\n        self._children.append(child)\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/model_tasks_template.py",
    "content": "from qtpy import QtCore\nimport qtawesome\n\nfrom openpype.style import get_default_entity_icon_color\n\nfrom . import Node, TreeModel\n\n\nclass TasksTemplateModel(TreeModel):\n    \"\"\"A model listing the tasks combined for a list of assets\"\"\"\n\n    COLUMNS = [\"Tasks\"]\n\n    def __init__(self, selectable=True):\n        super(TasksTemplateModel, self).__init__()\n        self.selectable = selectable\n        self.icon = qtawesome.icon(\n            'fa.calendar-check-o',\n            color=get_default_entity_icon_color()\n        )\n\n    def set_tasks(self, tasks):\n        \"\"\"Set assets to track by their database id\n\n        Arguments:\n            asset_ids (list): List of asset ids.\n\n        \"\"\"\n\n        self.clear()\n\n        # let cleared task view if no tasks are available\n        if len(tasks) == 0:\n            return\n\n        self.beginResetModel()\n\n        for task in tasks:\n            node = Node({\n                \"Tasks\": task,\n                \"icon\": self.icon\n            })\n            self.add_child(node)\n\n        self.endResetModel()\n\n    def flags(self, index):\n        if self.selectable is False:\n            return QtCore.Qt.ItemIsEnabled\n        else:\n            return (\n                QtCore.Qt.ItemIsEnabled |\n                QtCore.Qt.ItemIsSelectable\n            )\n\n    def data(self, index, role):\n\n        if not index.isValid():\n            return\n\n        # Add icon to the first column\n        if role == QtCore.Qt.DecorationRole:\n            if index.column() == 0:\n                return index.internalPointer()['icon']\n\n        return super(TasksTemplateModel, self).data(index, role)\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/model_tree.py",
    "content": "from qtpy import QtCore\nfrom . import Node\n\n\nclass TreeModel(QtCore.QAbstractItemModel):\n\n    COLUMNS = list()\n    ItemRole = QtCore.Qt.UserRole + 1\n\n    def __init__(self, parent=None):\n        super(TreeModel, self).__init__(parent)\n        self._root_node = Node()\n\n    def rowCount(self, parent):\n        if parent.isValid():\n            node = parent.internalPointer()\n        else:\n            node = self._root_node\n\n        return node.childCount()\n\n    def columnCount(self, parent):\n        return len(self.COLUMNS)\n\n    def data(self, index, role):\n\n        if not index.isValid():\n            return None\n\n        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:\n\n            node = index.internalPointer()\n            column = index.column()\n\n            key = self.COLUMNS[column]\n            return node.get(key, None)\n\n        if role == self.ItemRole:\n            return index.internalPointer()\n\n    def setData(self, index, value, role=QtCore.Qt.EditRole):\n        \"\"\"Change the data on the nodes.\n\n        Returns:\n            bool: Whether the edit was successful\n        \"\"\"\n\n        if index.isValid():\n            if role == QtCore.Qt.EditRole:\n\n                node = index.internalPointer()\n                column = index.column()\n                key = self.COLUMNS[column]\n                node[key] = value\n\n                # passing `list()` for PyQt5 (see PYSIDE-462)\n                self.dataChanged.emit(index, index, list())\n\n                # must return true if successful\n                return True\n\n        return False\n\n    def setColumns(self, keys):\n        assert isinstance(keys, (list, tuple))\n        self.COLUMNS = keys\n\n    def headerData(self, section, orientation, role):\n\n        if role == QtCore.Qt.DisplayRole:\n            if section < len(self.COLUMNS):\n                return self.COLUMNS[section]\n\n        super(TreeModel, self).headerData(section, orientation, role)\n\n    def flags(self, index):\n        return (\n            QtCore.Qt.ItemIsEnabled |\n            QtCore.Qt.ItemIsSelectable\n        )\n\n    def parent(self, index):\n\n        node = index.internalPointer()\n        parent_node = node.parent()\n\n        # If it has no parents we return invalid\n        if parent_node == self._root_node or not parent_node:\n            return QtCore.QModelIndex()\n\n        return self.createIndex(parent_node.row(), 0, parent_node)\n\n    def index(self, row, column, parent):\n        \"\"\"Return index for row/column under parent\"\"\"\n\n        if not parent.isValid():\n            parentNode = self._root_node\n        else:\n            parentNode = parent.internalPointer()\n\n        childItem = parentNode.child(row)\n        if childItem:\n            return self.createIndex(row, column, childItem)\n        else:\n            return QtCore.QModelIndex()\n\n    def add_child(self, node, parent=None):\n        if parent is None:\n            parent = self._root_node\n\n        parent.add_child(node)\n\n    def column_name(self, column):\n        \"\"\"Return column key by index\"\"\"\n\n        if column < len(self.COLUMNS):\n            return self.COLUMNS[column]\n\n    def clear(self):\n        self.beginResetModel()\n        self._root_node = Node()\n        self.endResetModel()\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/model_tree_view_deselectable.py",
    "content": "from qtpy import QtWidgets, QtCore\n\n\nclass DeselectableTreeView(QtWidgets.QTreeView):\n    \"\"\"A tree view that deselects on clicking on an empty area in the view\"\"\"\n\n    def mousePressEvent(self, event):\n\n        index = self.indexAt(event.pos())\n        if not index.isValid():\n            # clear the selection\n            self.clearSelection()\n            # clear the current index\n            self.setCurrentIndex(QtCore.QModelIndex())\n\n        QtWidgets.QTreeView.mousePressEvent(self, event)\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/resources/__init__.py",
    "content": "import os\n\n\nresource_path = os.path.dirname(__file__)\n\n\ndef get_resource(*args):\n    \"\"\" Serves to simple resources access\n\n    :param \\*args: should contain *subfolder* names and *filename* of\n                  resource from resources folder\n    :type \\*args: list\n    \"\"\"\n    return os.path.normpath(os.path.join(resource_path, *args))\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/widget_asset.py",
    "content": "import contextlib\nfrom qtpy import QtWidgets, QtCore\nimport qtawesome\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_projects,\n    get_project,\n    get_asset_by_id,\n)\nfrom openpype.tools.utils import PlaceholderLineEdit\n\nfrom openpype.style import get_default_tools_icon_color\n\nfrom . import RecursiveSortFilterProxyModel, AssetModel\nfrom . import TasksTemplateModel, DeselectableTreeView\nfrom . import _iter_model_rows\n\n@contextlib.contextmanager\ndef preserve_expanded_rows(tree_view,\n                           column=0,\n                           role=QtCore.Qt.DisplayRole):\n    \"\"\"Preserves expanded row in QTreeView by column's data role.\n\n    This function is created to maintain the expand vs collapse status of\n    the model items. When refresh is triggered the items which are expanded\n    will stay expanded and vice versa.\n\n    Arguments:\n        tree_view (QWidgets.QTreeView): the tree view which is\n            nested in the application\n        column (int): the column to retrieve the data from\n        role (int): the role which dictates what will be returned\n\n    Returns:\n        None\n\n    \"\"\"\n\n    model = tree_view.model()\n\n    expanded = set()\n\n    for index in _iter_model_rows(model,\n                                  column=column,\n                                  include_root=False):\n        if tree_view.isExpanded(index):\n            value = index.data(role)\n            expanded.add(value)\n\n    try:\n        yield\n    finally:\n        if not expanded:\n            return\n\n        for index in _iter_model_rows(model,\n                                      column=column,\n                                      include_root=False):\n            value = index.data(role)\n            state = value in expanded\n            if state:\n                tree_view.expand(index)\n            else:\n                tree_view.collapse(index)\n\n\n@contextlib.contextmanager\ndef preserve_selection(tree_view,\n                       column=0,\n                       role=QtCore.Qt.DisplayRole,\n                       current_index=True):\n    \"\"\"Preserves row selection in QTreeView by column's data role.\n\n    This function is created to maintain the selection status of\n    the model items. When refresh is triggered the items which are expanded\n    will stay expanded and vice versa.\n\n        tree_view (QWidgets.QTreeView): the tree view nested in the application\n        column (int): the column to retrieve the data from\n        role (int): the role which dictates what will be returned\n\n    Returns:\n        None\n\n    \"\"\"\n\n    model = tree_view.model()\n    selection_model = tree_view.selectionModel()\n    flags = (\n        QtCore.QItemSelectionModel.Select\n        | QtCore.QItemSelectionModel.Rows\n    )\n\n    if current_index:\n        current_index_value = tree_view.currentIndex().data(role)\n    else:\n        current_index_value = None\n\n    selected_rows = selection_model.selectedRows()\n    if not selected_rows:\n        yield\n        return\n\n    selected = set(row.data(role) for row in selected_rows)\n    try:\n        yield\n    finally:\n        if not selected:\n            return\n\n        # Go through all indices, select the ones with similar data\n        for index in _iter_model_rows(model,\n                                      column=column,\n                                      include_root=False):\n\n            value = index.data(role)\n            state = value in selected\n            if state:\n                tree_view.scrollTo(index)  # Ensure item is visible\n                selection_model.select(index, flags)\n\n            if current_index_value and value == current_index_value:\n                tree_view.setCurrentIndex(index)\n\n\nclass AssetWidget(QtWidgets.QWidget):\n    \"\"\"A Widget to display a tree of assets with filter\n\n    To list the assets of the active project:\n        >>> # widget = AssetWidget()\n        >>> # widget.refresh()\n        >>> # widget.show()\n\n    \"\"\"\n\n    project_changed = QtCore.Signal(str)\n    assets_refreshed = QtCore.Signal()   # on model refresh\n    selection_changed = QtCore.Signal()  # on view selection change\n    current_changed = QtCore.Signal()    # on view current index change\n    task_changed = QtCore.Signal()\n\n    def __init__(self, dbcon, settings, parent=None):\n        super(AssetWidget, self).__init__(parent=parent)\n        self.setContentsMargins(0, 0, 0, 0)\n\n        self.dbcon = dbcon\n        self._settings = settings\n\n        layout = QtWidgets.QVBoxLayout()\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(4)\n\n        # Project\n        self.combo_projects = QtWidgets.QComboBox()\n        # Change delegate so stylysheets are applied\n        project_delegate = QtWidgets.QStyledItemDelegate(self.combo_projects)\n        self.combo_projects.setItemDelegate(project_delegate)\n\n        self._set_projects()\n        self.combo_projects.currentTextChanged.connect(self.on_project_change)\n        # Tree View\n        model = AssetModel(dbcon=self.dbcon, parent=self)\n        proxy = RecursiveSortFilterProxyModel()\n        proxy.setSourceModel(model)\n        proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        view = DeselectableTreeView()\n        view.setIndentation(15)\n        view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        view.setHeaderHidden(True)\n        view.setModel(proxy)\n\n        # Header\n        header = QtWidgets.QHBoxLayout()\n\n        icon = qtawesome.icon(\n            \"fa.refresh\", color=get_default_tools_icon_color()\n        )\n        refresh = QtWidgets.QPushButton(icon, \"\")\n        refresh.setToolTip(\"Refresh items\")\n\n        filter = PlaceholderLineEdit()\n        filter.textChanged.connect(proxy.setFilterFixedString)\n        filter.setPlaceholderText(\"Filter {}..\".format(\n            \"folders\" if AYON_SERVER_ENABLED else \"assets\"))\n\n        header.addWidget(filter)\n        header.addWidget(refresh)\n\n        # Layout\n        layout.addWidget(self.combo_projects)\n        layout.addLayout(header)\n        layout.addWidget(view)\n\n        # tasks\n        task_view = DeselectableTreeView()\n        task_view.setIndentation(0)\n        task_view.setHeaderHidden(True)\n        task_view.setVisible(False)\n\n        task_model = TasksTemplateModel()\n        task_view.setModel(task_model)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(4)\n        main_layout.addLayout(layout, 80)\n        main_layout.addWidget(task_view, 20)\n\n        # Signals/Slots\n        selection = view.selectionModel()\n        selection.selectionChanged.connect(self.selection_changed)\n        selection.currentChanged.connect(self.current_changed)\n        task_view.selectionModel().selectionChanged.connect(\n            self._on_task_change\n        )\n        refresh.clicked.connect(self.refresh)\n\n        self.selection_changed.connect(self._refresh_tasks)\n\n        self.project_delegate = project_delegate\n        self.task_view = task_view\n        self.task_model = task_model\n        self.refreshButton = refresh\n        self.model = model\n        self.proxy = proxy\n        self.view = view\n\n    def collect_data(self):\n        project_name = self.dbcon.active_project()\n        project = get_project(project_name, fields=[\"name\"])\n        asset = self.get_active_asset()\n\n        try:\n            index = self.task_view.selectedIndexes()[0]\n            task = self.task_model.itemData(index)[0]\n        except Exception:\n            task = None\n        data = {\n            'project': project['name'],\n            'asset': asset['name'],\n            'parents': self.get_parents(asset),\n            'task': task\n        }\n\n        return data\n\n    def get_parents(self, entity):\n        ent_parents = entity.get(\"data\", {}).get(\"parents\")\n        if ent_parents is not None and isinstance(ent_parents, list):\n            return ent_parents\n\n        output = []\n        parent_asset_id = entity.get('data', {}).get('visualParent', None)\n        if parent_asset_id is None:\n            return output\n\n        project_name = self.dbcon.active_project()\n        parent = get_asset_by_id(\n            project_name,\n            parent_asset_id,\n            fields=[\"name\", \"data.visualParent\"]\n        )\n        output.append(parent['name'])\n        output.extend(self.get_parents(parent))\n        return output\n\n    def _get_last_projects(self):\n        if not self._settings:\n            return []\n\n        project_names = []\n        for project_name in self._settings.value(\"projects\", \"\").split(\"|\"):\n            if project_name:\n                project_names.append(project_name)\n        return project_names\n\n    def _add_last_project(self, project_name):\n        if not self._settings:\n            return\n\n        last_projects = []\n        for _project_name in self._settings.value(\"projects\", \"\").split(\"|\"):\n            if _project_name:\n                last_projects.append(_project_name)\n\n        if project_name in last_projects:\n            last_projects.remove(project_name)\n\n        last_projects.insert(0, project_name)\n        while len(last_projects) > 5:\n            last_projects.pop(-1)\n\n        self._settings.setValue(\"projects\", \"|\".join(last_projects))\n\n    def _set_projects(self):\n        project_names = list()\n\n        for doc in get_projects(fields=[\"name\"]):\n            project_name = doc.get(\"name\")\n            if project_name:\n                project_names.append(project_name)\n\n        self.combo_projects.clear()\n\n        if not project_names:\n            return\n\n        sorted_project_names = list(sorted(project_names))\n        self.combo_projects.addItems(list(sorted(sorted_project_names)))\n\n        last_project = sorted_project_names[0]\n        for project_name in self._get_last_projects():\n            if project_name in sorted_project_names:\n                last_project = project_name\n                break\n\n        index = sorted_project_names.index(last_project)\n        self.combo_projects.setCurrentIndex(index)\n\n        self.dbcon.Session[\"AVALON_PROJECT\"] = last_project\n\n    def on_project_change(self):\n        projects = list()\n\n        for project in get_projects(fields=[\"name\"]):\n            projects.append(project['name'])\n        project_name = self.combo_projects.currentText()\n        if project_name in projects:\n            self.dbcon.Session[\"AVALON_PROJECT\"] = project_name\n            self._add_last_project(project_name)\n\n        self.project_changed.emit(project_name)\n\n        self.refresh()\n\n    def _refresh_model(self):\n        with preserve_expanded_rows(\n            self.view, column=0, role=self.model.ObjectIdRole\n        ):\n            with preserve_selection(\n                self.view, column=0, role=self.model.ObjectIdRole\n            ):\n                self.model.refresh()\n\n        self.assets_refreshed.emit()\n\n    def refresh(self):\n        self._refresh_model()\n\n    def _on_task_change(self):\n        try:\n            index = self.task_view.selectedIndexes()[0]\n            task_name = self.task_model.itemData(index)[0]\n        except Exception:\n            task_name = None\n\n        self.dbcon.Session[\"AVALON_TASK\"] = task_name\n        self.task_changed.emit()\n\n    def _refresh_tasks(self):\n        self.dbcon.Session[\"AVALON_TASK\"] = None\n        tasks = []\n        selected = self.get_selected_assets()\n        if len(selected) == 1:\n            project_name = self.dbcon.active_project()\n            asset = get_asset_by_id(\n                project_name, selected[0], fields=[\"data.tasks\"]\n            )\n            if asset:\n                tasks = asset.get('data', {}).get('tasks', [])\n        self.task_model.set_tasks(tasks)\n        self.task_view.setVisible(len(tasks) > 0)\n        self.task_changed.emit()\n\n    def get_active_asset(self):\n        \"\"\"Return the asset id the current asset.\"\"\"\n        current = self.view.currentIndex()\n        return current.data(self.model.ItemRole)\n\n    def get_active_index(self):\n        return self.view.currentIndex()\n\n    def get_selected_assets(self):\n        \"\"\"Return the assets' ids that are selected.\"\"\"\n        selection = self.view.selectionModel()\n        rows = selection.selectedRows()\n        return [row.data(self.model.ObjectIdRole) for row in rows]\n\n    def select_assets(self, assets, expand=True, key=\"name\"):\n        \"\"\"Select assets by name.\n\n        Args:\n            assets (list): List of asset names\n            expand (bool): Whether to also expand to the asset in the view\n\n        Returns:\n            None\n\n        \"\"\"\n        # TODO: Instead of individual selection optimize for many assets\n\n        if not isinstance(assets, (tuple, list)):\n            assets = [assets]\n        assert isinstance(\n            assets, (tuple, list)\n        ), \"Assets must be list or tuple\"\n\n        # convert to list - tuple can't be modified\n        assets = list(assets)\n\n        # Clear selection\n        selection_model = self.view.selectionModel()\n        selection_model.clearSelection()\n\n        # Select\n        mode = (\n            QtCore.QItemSelectionModel.Select\n            | QtCore.QItemSelectionModel.Rows\n        )\n        for index in _iter_model_rows(\n            self.proxy, column=0, include_root=False\n        ):\n            # stop iteration if there are no assets to process\n            if not assets:\n                break\n\n            value = index.data(self.model.ItemRole).get(key)\n            if value not in assets:\n                continue\n\n            # Remove processed asset\n            assets.pop(assets.index(value))\n\n            selection_model.select(index, mode)\n\n            if expand:\n                # Expand parent index\n                self.view.expand(self.proxy.parent(index))\n\n            # Set the currently active index\n            self.view.setCurrentIndex(index)\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/widget_component_item.py",
    "content": "import os\nfrom qtpy import QtCore, QtGui, QtWidgets\nfrom .resources import get_resource\n\n\nclass ComponentItem(QtWidgets.QFrame):\n\n    signal_remove = QtCore.Signal(object)\n    signal_thumbnail = QtCore.Signal(object)\n    signal_preview = QtCore.Signal(object)\n    signal_repre_change = QtCore.Signal(object, object)\n\n    preview_text = \"PREVIEW\"\n    thumbnail_text = \"THUMBNAIL\"\n\n    def __init__(self, parent, main_parent):\n        super().__init__()\n\n        self.setObjectName(\"ComponentItem\")\n\n        self.has_valid_repre = True\n        self.actions = []\n        self.resize(290, 70)\n        self.setMinimumSize(QtCore.QSize(0, 70))\n        self.parent_list = parent\n        self.parent_widget = main_parent\n        # Font\n        font = QtGui.QFont()\n        font.setFamily(\"DejaVu Sans Condensed\")\n        font.setPointSize(9)\n        font.setBold(True)\n        font.setWeight(50)\n        font.setKerning(True)\n\n        # Main widgets\n        frame = QtWidgets.QFrame(self)\n        frame.setObjectName(\"ComponentFrame\")\n        frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        frame.setFrameShadow(QtWidgets.QFrame.Raised)\n\n        layout_main = QtWidgets.QHBoxLayout(frame)\n        layout_main.setSpacing(2)\n        layout_main.setContentsMargins(2, 2, 2, 2)\n\n        # Image + Info\n        frame_image_info = QtWidgets.QFrame(frame)\n\n        # Layout image info\n        layout = QtWidgets.QVBoxLayout(frame_image_info)\n        layout.setSpacing(2)\n        layout.setContentsMargins(2, 2, 2, 2)\n\n        self.icon = QtWidgets.QLabel(frame)\n        self.icon.setMinimumSize(QtCore.QSize(22, 22))\n        self.icon.setMaximumSize(QtCore.QSize(22, 22))\n        self.icon.setText(\"\")\n        self.icon.setScaledContents(True)\n\n        self.btn_action_menu = PngButton(\n            name=\"menu\", size=QtCore.QSize(22, 22)\n        )\n\n        self.action_menu = QtWidgets.QMenu(self.btn_action_menu)\n\n        expanding_sizePolicy = QtWidgets.QSizePolicy(\n            QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding\n        )\n        expanding_sizePolicy.setHorizontalStretch(0)\n        expanding_sizePolicy.setVerticalStretch(0)\n\n        layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter)\n        layout.addWidget(self.btn_action_menu, alignment=QtCore.Qt.AlignCenter)\n\n        layout_main.addWidget(frame_image_info)\n\n        # Name + representation\n        self.name = QtWidgets.QLabel(frame)\n        self.file_info = QtWidgets.QLabel(frame)\n        self.ext = QtWidgets.QLabel(frame)\n\n        self.name.setFont(font)\n        self.file_info.setFont(font)\n        self.ext.setFont(font)\n\n        self.file_info.setStyleSheet('padding-left:3px;')\n\n        expanding_sizePolicy.setHeightForWidth(\n            self.name.sizePolicy().hasHeightForWidth()\n        )\n\n        frame_name_repre = QtWidgets.QFrame(frame)\n\n        self.file_info.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)\n        self.ext.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)\n        self.name.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)\n\n        layout = QtWidgets.QHBoxLayout(frame_name_repre)\n        layout.setSpacing(0)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(self.name, alignment=QtCore.Qt.AlignLeft)\n        layout.addWidget(self.file_info, alignment=QtCore.Qt.AlignLeft)\n        layout.addWidget(self.ext, alignment=QtCore.Qt.AlignRight)\n\n        frame_name_repre.setSizePolicy(\n            QtWidgets.QSizePolicy.MinimumExpanding,\n            QtWidgets.QSizePolicy.MinimumExpanding\n        )\n\n        # Repre + icons\n        frame_repre_icons = QtWidgets.QFrame(frame)\n\n        frame_repre = QtWidgets.QFrame(frame_repre_icons)\n\n        label_repre = QtWidgets.QLabel()\n        label_repre.setText('Representation:')\n\n        self.input_repre = QtWidgets.QLineEdit()\n        self.input_repre.setMaximumWidth(50)\n\n        layout = QtWidgets.QHBoxLayout(frame_repre)\n        layout.setSpacing(0)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft)\n        layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft)\n\n        frame_icons = QtWidgets.QFrame(frame_repre_icons)\n\n        self.preview = LightingButton(self.preview_text)\n        self.thumbnail = LightingButton(self.thumbnail_text)\n\n        layout = QtWidgets.QHBoxLayout(frame_icons)\n        layout.setSpacing(6)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(self.thumbnail)\n        layout.addWidget(self.preview)\n\n        layout = QtWidgets.QHBoxLayout(frame_repre_icons)\n        layout.setSpacing(0)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        layout.addWidget(frame_repre, alignment=QtCore.Qt.AlignLeft)\n        layout.addWidget(frame_icons, alignment=QtCore.Qt.AlignRight)\n\n        frame_middle = QtWidgets.QFrame(frame)\n\n        layout = QtWidgets.QVBoxLayout(frame_middle)\n        layout.setSpacing(0)\n        layout.setContentsMargins(4, 0, 4, 0)\n        layout.addWidget(frame_name_repre)\n        layout.addWidget(frame_repre_icons)\n\n        layout.setStretchFactor(frame_name_repre, 1)\n        layout.setStretchFactor(frame_repre_icons, 1)\n\n        layout_main.addWidget(frame_middle)\n\n        self.remove = PngButton(name=\"trash\", size=QtCore.QSize(22, 22))\n        layout_main.addWidget(self.remove)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setSpacing(0)\n        layout.setContentsMargins(2, 2, 2, 2)\n        layout.addWidget(frame)\n\n        self.preview.setToolTip('Mark component as Preview')\n        self.thumbnail.setToolTip('Component will be selected as thumbnail')\n\n        # self.frame.setStyleSheet(\"border: 1px solid black;\")\n\n    def set_context(self, data):\n        self.btn_action_menu.setVisible(False)\n        self.in_data = data\n        self.remove.clicked.connect(self._remove)\n        self.thumbnail.clicked.connect(self._thumbnail_clicked)\n        self.preview.clicked.connect(self._preview_clicked)\n        self.input_repre.textChanged.connect(self._handle_duplicate_repre)\n        name = data['name']\n        representation = data['representation']\n        ext = data['ext']\n        file_info = data['file_info']\n        thumb = data['thumb']\n        prev = data['prev']\n        icon = data['icon']\n\n        resource = None\n        if icon is not None:\n            resource = get_resource('{}.png'.format(icon))\n\n        if resource is None or not os.path.isfile(resource):\n            if data['is_sequence']:\n                resource = get_resource('files.png')\n            else:\n                resource = get_resource('file.png')\n\n        pixmap = QtGui.QPixmap(resource)\n        self.icon.setPixmap(pixmap)\n\n        self.name.setText(name)\n        self.input_repre.setText(representation)\n        self.ext.setText('( {} )'.format(ext))\n        if file_info is None:\n            self.file_info.setVisible(False)\n        else:\n            self.file_info.setText('[{}]'.format(file_info))\n\n        self.thumbnail.setVisible(thumb)\n        self.preview.setVisible(prev)\n\n    def add_action(self, action_name):\n        if action_name.lower() == 'split':\n            for action in self.actions:\n                if action.text() == 'Split to frames':\n                    return\n            new_action = QtWidgets.QAction('Split to frames', self)\n            new_action.triggered.connect(self.split_sequence)\n        elif action_name.lower() == 'merge':\n            for action in self.actions:\n                if action.text() == 'Merge components':\n                    return\n            new_action = QtWidgets.QAction('Merge components', self)\n            new_action.triggered.connect(self.merge_sequence)\n        else:\n            print('unknown action')\n            return\n        self.action_menu.addAction(new_action)\n        self.actions.append(new_action)\n        if not self.btn_action_menu.isVisible():\n            self.btn_action_menu.setVisible(True)\n            self.btn_action_menu.clicked.connect(self.show_actions)\n\n    def set_repre_name_valid(self, valid):\n        self.has_valid_repre = valid\n        if valid:\n            self.input_repre.setStyleSheet(\"\")\n        else:\n            self.input_repre.setStyleSheet(\"border: 1px solid red;\")\n\n    def split_sequence(self):\n        self.parent_widget.split_items(self)\n\n    def merge_sequence(self):\n        self.parent_widget.merge_items(self)\n\n    def show_actions(self):\n        position = QtGui.QCursor().pos()\n        self.action_menu.popup(position)\n\n    def _remove(self):\n        self.signal_remove.emit(self)\n\n    def _thumbnail_clicked(self):\n        self.signal_thumbnail.emit(self)\n\n    def _preview_clicked(self):\n        self.signal_preview.emit(self)\n\n    def _handle_duplicate_repre(self, repre_name):\n        self.signal_repre_change.emit(self, repre_name)\n\n    def is_thumbnail(self):\n        return self.thumbnail.isChecked()\n\n    def change_thumbnail(self, hover=True):\n        self.thumbnail.setChecked(hover)\n\n    def is_preview(self):\n        return self.preview.isChecked()\n\n    def change_preview(self, hover=True):\n        self.preview.setChecked(hover)\n\n    def collect_data(self):\n        in_files = self.in_data['files']\n        staging_dir = os.path.dirname(in_files[0])\n\n        files = [os.path.basename(file) for file in in_files]\n        if len(files) == 1:\n            files = files[0]\n\n        data = {\n            'ext': self.in_data['ext'],\n            'label': self.name.text(),\n            'name': self.input_repre.text(),\n            'stagingDir': staging_dir,\n            'files': files,\n            'thumbnail': self.is_thumbnail(),\n            'preview': self.is_preview()\n        }\n\n        if (\"frameStart\" in self.in_data and \"frameEnd\" in self.in_data):\n            data[\"frameStart\"] = self.in_data[\"frameStart\"]\n            data[\"frameEnd\"] = self.in_data[\"frameEnd\"]\n\n        if 'fps' in self.in_data:\n            data['fps'] = self.in_data['fps']\n\n        return data\n\n\nclass LightingButton(QtWidgets.QPushButton):\n    lightingbtnstyle = \"\"\"\n    QPushButton {\n        font: %(font_size_pt)spt;\n        text-align: center;\n        color: #777777;\n        background-color: transparent;\n        border-width: 1px;\n        border-color: #777777;\n        border-style: solid;\n        padding-top: 0px;\n        padding-bottom: 0px;\n        padding-left: 3px;\n        padding-right: 3px;\n        border-radius: 3px;\n    }\n\n    QPushButton:hover {\n        border-color: #cccccc;\n        color: #cccccc;\n    }\n\n    QPushButton:pressed {\n        border-color: #ffffff;\n        color: #ffffff;\n    }\n\n    QPushButton:disabled {\n        border-color: #3A3939;\n        color: #3A3939;\n    }\n\n    QPushButton:checked {\n        border-color: #4BB543;\n        color: #4BB543;\n    }\n\n    QPushButton:checked:hover {\n        border-color: #4Bd543;\n        color: #4Bd543;\n    }\n\n    QPushButton:checked:pressed {\n        border-color: #4BF543;\n        color: #4BF543;\n    }\n    \"\"\"\n\n    def __init__(self, text, font_size_pt=8, *args, **kwargs):\n        super(LightingButton, self).__init__(text, *args, **kwargs)\n        self.setStyleSheet(self.lightingbtnstyle % {\n            \"font_size_pt\": font_size_pt\n        })\n        self.setCheckable(True)\n\n\nclass PngFactory:\n    png_names = None\n\n    @classmethod\n    def init(cls):\n        cls.png_names = {\n            \"trash\": {\n                \"normal\": QtGui.QIcon(get_resource(\"trash.png\")),\n                \"hover\": QtGui.QIcon(get_resource(\"trash_hover.png\")),\n                \"pressed\": QtGui.QIcon(get_resource(\"trash_pressed.png\")),\n                \"pressed_hover\": QtGui.QIcon(\n                    get_resource(\"trash_pressed_hover.png\")\n                ),\n                \"disabled\": QtGui.QIcon(get_resource(\"trash_disabled.png\"))\n            },\n\n            \"menu\": {\n                \"normal\": QtGui.QIcon(get_resource(\"menu.png\")),\n                \"hover\": QtGui.QIcon(get_resource(\"menu_hover.png\")),\n                \"pressed\": QtGui.QIcon(get_resource(\"menu_pressed.png\")),\n                \"pressed_hover\": QtGui.QIcon(\n                    get_resource(\"menu_pressed_hover.png\")\n                ),\n                \"disabled\": QtGui.QIcon(get_resource(\"menu_disabled.png\"))\n            }\n        }\n\n    @classmethod\n    def get_png(cls, name):\n        if cls.png_names is None:\n            cls.init()\n        return cls.png_names.get(name)\n\n\nclass PngButton(QtWidgets.QPushButton):\n    png_button_style = \"\"\"\n    QPushButton {\n        border: none;\n        background-color: transparent;\n        padding-top: 0px;\n        padding-bottom: 0px;\n        padding-left: 0px;\n        padding-right: 0px;\n    }\n    QPushButton:hover {}\n    QPushButton:pressed {}\n    QPushButton:disabled {}\n    QPushButton:checked {}\n    QPushButton:checked:hover {}\n    QPushButton:checked:pressed {}\n    \"\"\"\n\n    def __init__(\n        self, name=None, path=None, hover_path=None, pressed_path=None,\n        hover_pressed_path=None, disabled_path=None,\n        size=None, *args, **kwargs\n    ):\n        self._hovered = False\n        self._pressed = False\n        super(PngButton, self).__init__(*args, **kwargs)\n        self.setStyleSheet(self.png_button_style)\n\n        png_dict = {}\n        if name:\n            png_dict = PngFactory.get_png(name) or {}\n            if not png_dict:\n                print((\n                    \"WARNING: There is not set icon with name \\\"{}\\\"\"\n                    \"in PngFactory!\"\n                ).format(name))\n\n        ico_normal = png_dict.get(\"normal\")\n        ico_hover = png_dict.get(\"hover\")\n        ico_pressed = png_dict.get(\"pressed\")\n        ico_hover_pressed = png_dict.get(\"pressed_hover\")\n        ico_disabled = png_dict.get(\"disabled\")\n\n        if path:\n            ico_normal = QtGui.QIcon(path)\n        if hover_path:\n            ico_hover = QtGui.QIcon(hover_path)\n\n        if pressed_path:\n            ico_pressed = QtGui.QIcon(hover_path)\n\n        if hover_pressed_path:\n            ico_hover_pressed = QtGui.QIcon(hover_pressed_path)\n\n        if disabled_path:\n            ico_disabled = QtGui.QIcon(disabled_path)\n\n        self.setIcon(ico_normal)\n        if size:\n            self.setIconSize(size)\n            self.setMaximumSize(size)\n\n        self.ico_normal = ico_normal\n        self.ico_hover = ico_hover\n        self.ico_pressed = ico_pressed\n        self.ico_hover_pressed = ico_hover_pressed\n        self.ico_disabled = ico_disabled\n\n    def setDisabled(self, in_bool):\n        super(PngButton, self).setDisabled(in_bool)\n        icon = self.ico_normal\n        if in_bool and self.ico_disabled:\n            icon = self.ico_disabled\n        self.setIcon(icon)\n\n    def enterEvent(self, event):\n        self._hovered = True\n        if not self.isEnabled():\n            return\n        icon = self.ico_normal\n        if self.ico_hover:\n            icon = self.ico_hover\n\n        if self._pressed and self.ico_hover_pressed:\n            icon = self.ico_hover_pressed\n\n        if self.icon() != icon:\n            self.setIcon(icon)\n\n    def mouseMoveEvent(self, event):\n        super(PngButton, self).mouseMoveEvent(event)\n        if self._pressed:\n            mouse_pos = event.pos()\n            hovering = self.rect().contains(mouse_pos)\n            if hovering and not self._hovered:\n                self.enterEvent(event)\n            elif not hovering and self._hovered:\n                self.leaveEvent(event)\n\n    def leaveEvent(self, event):\n        self._hovered = False\n        if not self.isEnabled():\n            return\n        icon = self.ico_normal\n        if self._pressed and self.ico_pressed:\n            icon = self.ico_pressed\n\n        if self.icon() != icon:\n            self.setIcon(icon)\n\n    def mousePressEvent(self, event):\n        self._pressed = True\n        if not self.isEnabled():\n            return\n        icon = self.ico_hover\n        if self.ico_pressed:\n            icon = self.ico_pressed\n\n        if self.ico_hover_pressed:\n            mouse_pos = event.pos()\n            if self.rect().contains(mouse_pos):\n                icon = self.ico_hover_pressed\n\n        if icon is None:\n            icon = self.ico_normal\n\n        if self.icon() != icon:\n            self.setIcon(icon)\n\n    def mouseReleaseEvent(self, event):\n        if not self.isEnabled():\n            return\n        if self._pressed:\n            self._pressed = False\n            mouse_pos = event.pos()\n            if self.rect().contains(mouse_pos):\n                self.clicked.emit()\n\n        icon = self.ico_normal\n        if self._hovered and self.ico_hover:\n            icon = self.ico_hover\n\n        if self.icon() != icon:\n            self.setIcon(icon)\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/widget_components.py",
    "content": "import os\nimport json\nimport tempfile\nimport random\nimport string\n\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.pipeline import legacy_io\nfrom openpype.lib import (\n    execute,\n    Logger,\n    get_openpype_execute_args,\n    apply_project_environments_value\n)\n\nfrom . import DropDataFrame\nfrom .constants import HOST_NAME\n\nlog = Logger.get_logger(\"standalonepublisher\")\n\n\nclass ComponentsWidget(QtWidgets.QWidget):\n    def __init__(self, parent):\n        super().__init__()\n        self.initialized = False\n        self.valid_components = False\n        self.valid_family = False\n        self.valid_repre_names = False\n\n        body = QtWidgets.QWidget()\n        self.parent_widget = parent\n        self.drop_frame = DropDataFrame(self)\n\n        buttons = QtWidgets.QWidget()\n\n        layout = QtWidgets.QHBoxLayout(buttons)\n\n        self.btn_browse = QtWidgets.QPushButton('Browse')\n        self.btn_browse.setToolTip('Browse for file(s).')\n        self.btn_browse.setFocusPolicy(QtCore.Qt.NoFocus)\n\n        self.btn_publish = QtWidgets.QPushButton('Publish')\n        self.btn_publish.setToolTip('Publishes data.')\n        self.btn_publish.setFocusPolicy(QtCore.Qt.NoFocus)\n\n        layout.addWidget(self.btn_browse, alignment=QtCore.Qt.AlignLeft)\n        layout.addWidget(self.btn_publish, alignment=QtCore.Qt.AlignRight)\n\n        layout = QtWidgets.QVBoxLayout(body)\n        layout.setSpacing(0)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(self.drop_frame)\n        layout.addWidget(buttons)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setSpacing(0)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(body)\n\n        self.btn_browse.clicked.connect(self._browse)\n        self.btn_publish.clicked.connect(self._publish)\n        self.initialized = True\n\n    def validation(self):\n        if self.initialized is False:\n            return\n        valid = (\n            self.parent_widget.valid_family and\n            self.valid_components and\n            self.valid_repre_names\n        )\n        self.btn_publish.setEnabled(valid)\n\n    def set_valid_components(self, valid):\n        self.valid_components = valid\n        self.validation()\n\n    def set_valid_repre_names(self, valid):\n        self.valid_repre_names = valid\n        self.validation()\n\n    def process_mime_data(self, mime_data):\n        self.drop_frame.process_ent_mime(mime_data)\n\n    def collect_data(self):\n        return self.drop_frame.collect_data()\n\n    def _browse(self):\n        options = [\n            QtWidgets.QFileDialog.DontResolveSymlinks,\n            QtWidgets.QFileDialog.DontUseNativeDialog\n        ]\n        folders = False\n        if folders:\n            # browse folders specifics\n            caption = \"Browse folders to publish image sequences\"\n            file_mode = QtWidgets.QFileDialog.Directory\n            options.append(QtWidgets.QFileDialog.ShowDirsOnly)\n        else:\n            # browse files specifics\n            caption = \"Browse files to publish\"\n            file_mode = QtWidgets.QFileDialog.ExistingFiles\n\n        # create the dialog\n        file_dialog = QtWidgets.QFileDialog(parent=self, caption=caption)\n        file_dialog.setLabelText(QtWidgets.QFileDialog.Accept, \"Select\")\n        file_dialog.setLabelText(QtWidgets.QFileDialog.Reject, \"Cancel\")\n        file_dialog.setFileMode(file_mode)\n\n        # set the appropriate options\n        for option in options:\n            file_dialog.setOption(option)\n\n        # browse!\n        if not file_dialog.exec_():\n            return\n\n        # process the browsed files/folders for publishing\n        paths = file_dialog.selectedFiles()\n        self.drop_frame._process_paths(paths)\n\n    def working_start(self, msg=None):\n        if hasattr(self, 'parent_widget'):\n            self.parent_widget.working_start(msg)\n\n    def working_stop(self):\n        if hasattr(self, 'parent_widget'):\n            self.parent_widget.working_stop()\n\n    def _publish(self):\n        log.info(self.parent_widget.pyblish_paths)\n        self.working_start('Pyblish is running')\n        try:\n            data = self.parent_widget.collect_data()\n            set_context(\n                data['project'],\n                data['asset'],\n                data['task']\n            )\n            result = cli_publish(data, self.parent_widget.pyblish_paths)\n            # Clear widgets from components list if publishing was successful\n            if result:\n                self.drop_frame.components_list.clear_widgets()\n                self.drop_frame._refresh_view()\n        finally:\n            self.working_stop()\n\n\ndef set_context(project, asset, task):\n    ''' Sets context for pyblish (must be done before pyblish is launched)\n    :param project: Name of `Project` where instance should be published\n    :type project: str\n    :param asset: Name of `Asset` where instance should be published\n    :type asset: str\n    '''\n    os.environ[\"AVALON_PROJECT\"] = project\n    legacy_io.Session[\"AVALON_PROJECT\"] = project\n    os.environ[\"AVALON_ASSET\"] = asset\n    legacy_io.Session[\"AVALON_ASSET\"] = asset\n    if not task:\n        task = ''\n    os.environ[\"AVALON_TASK\"] = task\n    legacy_io.Session[\"AVALON_TASK\"] = task\n\n    legacy_io.Session[\"current_dir\"] = os.path.normpath(os.getcwd())\n\n    os.environ[\"AVALON_APP\"] = HOST_NAME\n    legacy_io.Session[\"AVALON_APP\"] = HOST_NAME\n\n\ndef cli_publish(data, publish_paths, gui=True):\n    PUBLISH_SCRIPT_PATH = os.path.join(\n        os.path.dirname(os.path.dirname(__file__)),\n        \"publish.py\"\n    )\n    legacy_io.install()\n\n    # Create hash name folder in temp\n    chars = \"\".join([random.choice(string.ascii_letters) for i in range(15)])\n    staging_dir = tempfile.mkdtemp(chars)\n\n    # create also json and fill with data\n    json_data_path = staging_dir + os.path.basename(staging_dir) + '.json'\n    with open(json_data_path, 'w') as outfile:\n        json.dump(data, outfile)\n\n    envcopy = os.environ.copy()\n    envcopy[\"PYBLISH_HOSTS\"] = \"standalonepublisher\"\n    envcopy[\"SAPUBLISH_INPATH\"] = json_data_path\n    envcopy[\"PYBLISHGUI\"] = \"pyblish_pype\"\n    envcopy[\"PUBLISH_PATHS\"] = os.pathsep.join(publish_paths)\n    if data.get(\"family\", \"\").lower() == \"editorial\":\n        envcopy[\"PYBLISH_SUSPEND_LOGS\"] = \"1\"\n\n    project_name = os.environ[\"AVALON_PROJECT\"]\n    env_copy = apply_project_environments_value(project_name, envcopy)\n\n    args = get_openpype_execute_args(\"run\", PUBLISH_SCRIPT_PATH)\n    result = execute(args, env=envcopy)\n\n    result = {}\n    if os.path.exists(json_data_path):\n        with open(json_data_path, \"r\") as f:\n            result = json.load(f)\n        os.remove(json_data_path)\n\n    log.info(f\"Publish result: {result}\")\n\n    legacy_io.uninstall()\n\n    return False\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/widget_components_list.py",
    "content": "from qtpy import QtWidgets\n\n\nclass ComponentsList(QtWidgets.QTableWidget):\n    def __init__(self, parent=None):\n        super().__init__(parent=parent)\n\n        self.setObjectName(\"ComponentList\")\n\n        self._main_column = 0\n\n        self.setColumnCount(1)\n        self.setSelectionBehavior(\n            QtWidgets.QAbstractItemView.SelectRows\n        )\n        self.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection\n        )\n        self.setVerticalScrollMode(\n            QtWidgets.QAbstractItemView.ScrollPerPixel\n        )\n        self.verticalHeader().hide()\n\n        try:\n            self.verticalHeader().setResizeMode(\n                QtWidgets.QHeaderView.ResizeToContents\n            )\n        except Exception:\n            self.verticalHeader().setSectionResizeMode(\n                QtWidgets.QHeaderView.ResizeToContents\n            )\n\n        self.horizontalHeader().setStretchLastSection(True)\n        self.horizontalHeader().hide()\n\n    def count(self):\n        return self.rowCount()\n\n    def add_widget(self, widget, row=None):\n        if row is None:\n            row = self.count()\n\n        self.insertRow(row)\n        self.setCellWidget(row, self._main_column, widget)\n\n        self.resizeRowToContents(row)\n\n        return row\n\n    def remove_widget(self, row):\n        self.removeRow(row)\n\n    def move_widget(self, widget, newRow):\n        oldRow = self.indexOfWidget(widget)\n        if oldRow:\n            self.insertRow(newRow)\n            # Collect the oldRow after insert to make sure we move the correct\n            # widget.\n            oldRow = self.indexOfWidget(widget)\n\n            self.setCellWidget(newRow, self._main_column, widget)\n            self.resizeRowToContents(oldRow)\n\n            # Remove the old row\n            self.removeRow(oldRow)\n\n    def clear_widgets(self):\n        '''Remove all widgets.'''\n        self.clear()\n        self.setRowCount(0)\n\n    def widget_index(self, widget):\n        index = None\n        for row in range(self.count()):\n            candidateWidget = self.widget_at(row)\n            if candidateWidget == widget:\n                index = row\n                break\n\n        return index\n\n    def widgets(self):\n        widgets = []\n        for row in range(self.count()):\n            widget = self.widget_at(row)\n            widgets.append(widget)\n\n        return widgets\n\n    def widget_at(self, row):\n        return self.cellWidget(row, self._main_column)\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/widget_drop_empty.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\n\nclass DropEmpty(QtWidgets.QWidget):\n\n    def __init__(self, parent):\n        '''Initialise DataDropZone widget.'''\n        super().__init__(parent)\n\n        layout = QtWidgets.QVBoxLayout(self)\n\n        BottomCenterAlignment = QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter\n        TopCenterAlignment = QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter\n\n        font = QtGui.QFont()\n        font.setFamily(\"DejaVu Sans Condensed\")\n        font.setPointSize(26)\n        font.setBold(True)\n        font.setWeight(50)\n        font.setKerning(True)\n\n        self._label = QtWidgets.QLabel('Drag & Drop')\n        self._label.setFont(font)\n        self._label.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        font.setPointSize(12)\n        self._sub_label = QtWidgets.QLabel('(drop files here)')\n        self._sub_label.setFont(font)\n        self._sub_label.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        layout.addWidget(self._label, alignment=BottomCenterAlignment)\n        layout.addWidget(self._sub_label, alignment=TopCenterAlignment)\n\n    def paintEvent(self, event):\n        super().paintEvent(event)\n        painter = QtGui.QPainter(self)\n        pen = QtGui.QPen()\n        pen.setWidth(1)\n        pen.setBrush(QtCore.Qt.darkGray)\n        pen.setStyle(QtCore.Qt.DashLine)\n        painter.setPen(pen)\n        painter.drawRect(\n            10,\n            10,\n            self.rect().width() - 15,\n            self.rect().height() - 15\n        )\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/widget_drop_frame.py",
    "content": "import os\nimport re\nimport json\nimport clique\nimport subprocess\nimport openpype.lib\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.lib import get_ffprobe_data\nfrom . import DropEmpty, ComponentsList, ComponentItem\n\n\nclass DropDataFrame(QtWidgets.QFrame):\n    image_extensions = [\n        \".ani\", \".anim\", \".apng\", \".art\", \".bmp\", \".bpg\", \".bsave\", \".cal\",\n        \".cin\", \".cpc\", \".cpt\", \".dds\", \".dpx\", \".ecw\", \".exr\", \".fits\",\n        \".flic\", \".flif\", \".fpx\", \".gif\", \".hdri\", \".hevc\", \".icer\",\n        \".icns\", \".ico\", \".cur\", \".ics\", \".ilbm\", \".jbig\", \".jbig2\",\n        \".jng\", \".jpeg\", \".jpeg-ls\", \".jpeg\", \".2000\", \".jpg\", \".xr\",\n        \".jpeg\", \".xt\", \".jpeg-hdr\", \".kra\", \".mng\", \".miff\", \".nrrd\",\n        \".ora\", \".pam\", \".pbm\", \".pgm\", \".ppm\", \".pnm\", \".pcx\", \".pgf\",\n        \".pictor\", \".png\", \".psb\", \".psp\", \".qtvr\", \".ras\",\n        \".rgbe\", \".logluv\", \".tiff\", \".sgi\", \".tga\", \".tiff\", \".tiff/ep\",\n        \".tiff/it\", \".ufo\", \".ufp\", \".wbmp\", \".webp\", \".xbm\", \".xcf\",\n        \".xpm\", \".xwd\"\n    ]\n    video_extensions = [\n        \".3g2\", \".3gp\", \".amv\", \".asf\", \".avi\", \".drc\", \".f4a\", \".f4b\",\n        \".f4p\", \".f4v\", \".flv\", \".gif\", \".gifv\", \".m2v\", \".m4p\", \".m4v\",\n        \".mkv\", \".mng\", \".mov\", \".mp2\", \".mp4\", \".mpe\", \".mpeg\", \".mpg\",\n        \".mpv\", \".mxf\", \".nsv\", \".ogg\", \".ogv\", \".qt\", \".rm\", \".rmvb\",\n        \".roq\", \".svi\", \".vob\", \".webm\", \".wmv\", \".yuv\"\n    ]\n    extensions = {\n        \"nuke\": [\".nk\"],\n        \"maya\": [\".ma\", \".mb\"],\n        \"houdini\": [\".hip\"],\n        \"image_file\": image_extensions,\n        \"video_file\": video_extensions\n    }\n\n    sequence_types = [\n        \".bgeo\", \".vdb\", \".bgeosc\", \".bgeogz\"\n    ]\n\n    def __init__(self, parent):\n        super().__init__()\n        self.parent_widget = parent\n\n        self.setAcceptDrops(True)\n        layout = QtWidgets.QVBoxLayout(self)\n        self.components_list = ComponentsList(self)\n        layout.addWidget(self.components_list)\n\n        self.drop_widget = DropEmpty(self)\n\n        sizePolicy = QtWidgets.QSizePolicy(\n            QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(\n            self.drop_widget.sizePolicy().hasHeightForWidth()\n        )\n        self.drop_widget.setSizePolicy(sizePolicy)\n\n        layout.addWidget(self.drop_widget)\n\n        self._refresh_view()\n\n    def dragEnterEvent(self, event):\n        event.setDropAction(QtCore.Qt.CopyAction)\n        event.accept()\n\n    def dragLeaveEvent(self, event):\n        event.accept()\n\n    def dropEvent(self, event):\n        self.process_ent_mime(event)\n        event.accept()\n\n    def process_ent_mime(self, ent):\n        paths = []\n        if ent.mimeData().hasUrls():\n            paths = self._processMimeData(ent.mimeData())\n        else:\n            # If path is in clipboard as string\n            try:\n                path = os.path.normpath(ent.text())\n                if os.path.exists(path):\n                    paths.append(path)\n                else:\n                    print('Dropped invalid file/folder')\n            except Exception:\n                pass\n        if paths:\n            self._process_paths(paths)\n\n    def _processMimeData(self, mimeData):\n        paths = []\n\n        for path in mimeData.urls():\n            local_path = path.toLocalFile()\n            if os.path.isfile(local_path) or os.path.isdir(local_path):\n                paths.append(local_path)\n            else:\n                print('Invalid input: \"{}\"'.format(local_path))\n        return paths\n\n    def _add_item(self, data, actions=[]):\n        # Assign to self so garbage collector won't remove the component\n        # during initialization\n        new_component = ComponentItem(self.components_list, self)\n        new_component.set_context(data)\n        self.components_list.add_widget(new_component)\n\n        new_component.signal_remove.connect(self._remove_item)\n        new_component.signal_preview.connect(self._set_preview)\n        new_component.signal_thumbnail.connect(\n            self._set_thumbnail\n        )\n        new_component.signal_repre_change.connect(self.repre_name_changed)\n        for action in actions:\n            new_component.add_action(action)\n\n        if len(self.components_list.widgets()) == 1:\n            self.parent_widget.set_valid_repre_names(True)\n        self._refresh_view()\n\n    def _set_thumbnail(self, in_item):\n        current_state = in_item.is_thumbnail()\n        in_item.change_thumbnail(not current_state)\n\n        checked_item = None\n        for item in self.components_list.widgets():\n            if item.is_thumbnail():\n                checked_item = item\n                break\n        if checked_item is not None and checked_item != in_item:\n            checked_item.change_thumbnail(False)\n\n        in_item.change_thumbnail(current_state)\n\n    def _set_preview(self, in_item):\n        current_state = in_item.is_preview()\n        in_item.change_preview(not current_state)\n\n        checked_item = None\n        for item in self.components_list.widgets():\n            if item.is_preview():\n                checked_item = item\n                break\n        if checked_item is not None and checked_item != in_item:\n            checked_item.change_preview(False)\n\n        in_item.change_preview(current_state)\n\n    def _remove_item(self, in_item):\n        valid_repre = in_item.has_valid_repre is True\n\n        self.components_list.remove_widget(\n            self.components_list.widget_index(in_item)\n        )\n        self._refresh_view()\n        if valid_repre:\n            return\n        for item in self.components_list.widgets():\n            if item.has_valid_repre:\n                continue\n            self.repre_name_changed(item, item.input_repre.text())\n\n    def _refresh_view(self):\n        _bool = len(self.components_list.widgets()) == 0\n        self.components_list.setVisible(not _bool)\n        self.drop_widget.setVisible(_bool)\n\n        self.parent_widget.set_valid_components(not _bool)\n\n    def _process_paths(self, in_paths):\n        self.parent_widget.working_start()\n        paths = self._get_all_paths(in_paths)\n        collectionable_paths = []\n        non_collectionable_paths = []\n        for path in paths:\n            ext = os.path.splitext(path)[1]\n            if ext in self.image_extensions or ext in self.sequence_types:\n                collectionable_paths.append(path)\n            else:\n                non_collectionable_paths.append(path)\n\n        collections, remainders = clique.assemble(collectionable_paths)\n        non_collectionable_paths.extend(remainders)\n        for collection in collections:\n            self._process_collection(collection)\n\n        for remainder in non_collectionable_paths:\n            self._process_remainder(remainder)\n        self.parent_widget.working_stop()\n\n    def _get_all_paths(self, paths):\n        output_paths = []\n        for path in paths:\n            path = os.path.normpath(path)\n            if os.path.isfile(path):\n                output_paths.append(path)\n            elif os.path.isdir(path):\n                s_paths = []\n                for s_item in os.listdir(path):\n                    s_path = os.path.sep.join([path, s_item])\n                    s_paths.append(s_path)\n                output_paths.extend(self._get_all_paths(s_paths))\n            else:\n                print('Invalid path: \"{}\"'.format(path))\n        return output_paths\n\n    def _process_collection(self, collection):\n        file_base = os.path.basename(collection.head)\n        folder_path = os.path.dirname(collection.head)\n        if file_base[-1] in ['.', '_']:\n            file_base = file_base[:-1]\n        file_ext = os.path.splitext(\n            collection.format('{head}{padding}{tail}'))[1]\n        repr_name = file_ext.replace('.', '')\n        range = collection.format('{ranges}')\n\n        # TODO: ranges must not be with missing frames!!!\n        # - this is goal implementation:\n        # startFrame, endFrame = range.split('-')\n        rngs = range.split(',')\n        startFrame = rngs[0].split('-')[0]\n        endFrame = rngs[-1].split('-')[-1]\n\n        actions = []\n\n        data = {\n            'files': [file for file in collection],\n            'name': file_base,\n            'ext': file_ext,\n            'file_info': range,\n            \"frameStart\": startFrame,\n            \"frameEnd\": endFrame,\n            'representation': repr_name,\n            'folder_path': folder_path,\n            'is_sequence': True,\n            'actions': actions\n        }\n\n        self._process_data(data)\n\n    def _process_remainder(self, remainder):\n        filename = os.path.basename(remainder)\n        folder_path = os.path.dirname(remainder)\n        file_base, file_ext = os.path.splitext(filename)\n        repr_name = file_ext.replace('.', '')\n        file_info = None\n\n        files = []\n        files.append(remainder)\n\n        actions = []\n\n        data = {\n            'files': files,\n            'name': file_base,\n            'ext': file_ext,\n            'representation': repr_name,\n            'folder_path': folder_path,\n            'is_sequence': False,\n            'actions': actions\n        }\n\n        self._process_data(data)\n\n    def load_data_with_probe(self, filepath):\n        ffprobe_data = get_ffprobe_data(filepath)\n        return ffprobe_data[\"streams\"][0]\n\n    def get_file_data(self, data):\n        filepath = data['files'][0]\n        ext = data['ext'].lower()\n        output = {\"fps\": None}\n\n        file_info = None\n        if 'file_info' in data:\n            file_info = data['file_info']\n\n        if ext in self.image_extensions or ext in self.video_extensions:\n            probe_data = self.load_data_with_probe(filepath)\n            if 'fps' not in data:\n                # default value\n                fps = 25\n                fps_string = probe_data.get('r_frame_rate')\n                if fps_string:\n                    fps = int(fps_string.split('/')[0])\n\n                output['fps'] = fps\n\n            if \"frameStart\" not in data or \"frameEnd\" not in data:\n                startFrame = endFrame = 1\n                endFrame_string = probe_data.get('nb_frames')\n\n                if endFrame_string:\n                    endFrame = int(endFrame_string)\n\n                output[\"frameStart\"] = startFrame\n                output[\"frameEnd\"] = endFrame\n\n            if (ext == '.mov') and (not file_info):\n                file_info = probe_data.get('codec_name')\n\n        output['file_info'] = file_info\n\n        return output\n\n    def _process_data(self, data):\n        ext = data['ext']\n        # load file data info\n        file_data = self.get_file_data(data)\n        for key, value in file_data.items():\n            data[key] = value\n\n        icon = 'default'\n        for ico, exts in self.extensions.items():\n            if ext in exts:\n                icon = ico\n                break\n\n        new_is_seq = data['is_sequence']\n        # Add 's' to icon_name if is sequence (image -> images)\n        if new_is_seq:\n            icon += 's'\n        data['icon'] = icon\n        data['thumb'] = (\n            ext in self.image_extensions\n            or ext in self.video_extensions\n        )\n        data['prev'] = (\n            ext in self.video_extensions\n            or (new_is_seq and ext in self.image_extensions)\n        )\n\n        actions = []\n\n        found = False\n        if data[\"ext\"] in self.image_extensions:\n            for item in self.components_list.widgets():\n                if data['ext'] != item.in_data['ext']:\n                    continue\n                if data['folder_path'] != item.in_data['folder_path']:\n                    continue\n\n                ex_is_seq = item.in_data['is_sequence']\n\n                # If both are single files\n                if not new_is_seq and not ex_is_seq:\n                    if data['name'] == item.in_data['name']:\n                        found = True\n                        break\n                    paths = list(data['files'])\n                    paths.extend(item.in_data['files'])\n                    c, r = clique.assemble(paths)\n                    if len(c) == 0:\n                        continue\n                    a_name = 'merge'\n                    item.add_action(a_name)\n                    if a_name not in actions:\n                        actions.append(a_name)\n\n                # If new is sequence and ex is single file\n                elif new_is_seq and not ex_is_seq:\n                    if data['name'] not in item.in_data['name']:\n                        continue\n                    ex_file = item.in_data['files'][0]\n\n                    a_name = 'merge'\n                    item.add_action(a_name)\n                    if a_name not in actions:\n                        actions.append(a_name)\n                    continue\n\n                # If new is single file existing is sequence\n                elif not new_is_seq and ex_is_seq:\n                    if item.in_data['name'] not in data['name']:\n                        continue\n                    a_name = 'merge'\n                    item.add_action(a_name)\n                    if a_name not in actions:\n                        actions.append(a_name)\n\n                # If both are sequence\n                else:\n                    if data['name'] != item.in_data['name']:\n                        continue\n                    if data['files'] == list(item.in_data['files']):\n                        found = True\n                        break\n                    a_name = 'merge'\n                    item.add_action(a_name)\n                    if a_name not in actions:\n                        actions.append(a_name)\n\n        if new_is_seq:\n            actions.append('split')\n\n        if found is False:\n            new_repre = self.handle_new_repre_name(data['representation'])\n            data['representation'] = new_repre\n            self._add_item(data, actions)\n\n    def handle_new_repre_name(self, repre_name):\n        renamed = False\n        for item in self.components_list.widgets():\n            if repre_name == item.input_repre.text():\n                check_regex = '_\\w+$'\n                result = re.findall(check_regex, repre_name)\n                next_num = 2\n                if len(result) == 1:\n                    repre_name = repre_name.replace(result[0], '')\n                    next_num = int(result[0].replace('_', ''))\n                    next_num += 1\n                repre_name = '{}_{}'.format(repre_name, next_num)\n                renamed = True\n                break\n        if renamed:\n            return self.handle_new_repre_name(repre_name)\n        return repre_name\n\n    def repre_name_changed(self, in_item, repre_name):\n        is_valid = True\n        if repre_name.strip() == '':\n            in_item.set_repre_name_valid(False)\n            is_valid = False\n        else:\n            for item in self.components_list.widgets():\n                if item == in_item:\n                    continue\n                if item.input_repre.text() == repre_name:\n                    item.set_repre_name_valid(False)\n                    in_item.set_repre_name_valid(False)\n                    is_valid = False\n        global_valid = is_valid\n        if is_valid:\n            in_item.set_repre_name_valid(True)\n            for item in self.components_list.widgets():\n                if item.has_valid_repre:\n                    continue\n                self.repre_name_changed(item, item.input_repre.text())\n            for item in self.components_list.widgets():\n                if not item.has_valid_repre:\n                    global_valid = False\n                    break\n        self.parent_widget.set_valid_repre_names(global_valid)\n\n    def merge_items(self, in_item):\n        self.parent_widget.working_start()\n        items = []\n        in_paths = in_item.in_data['files']\n        paths = in_paths\n        for item in self.components_list.widgets():\n            if item.in_data['files'] == in_paths:\n                items.append(item)\n                continue\n            copy_paths = paths.copy()\n            copy_paths.extend(item.in_data['files'])\n            collections, remainders = clique.assemble(copy_paths)\n            if len(collections) == 1 and len(remainders) == 0:\n                paths.extend(item.in_data['files'])\n                items.append(item)\n        for item in items:\n            self._remove_item(item)\n        self._process_paths(paths)\n        self.parent_widget.working_stop()\n\n    def split_items(self, item):\n        self.parent_widget.working_start()\n        paths = item.in_data['files']\n        self._remove_item(item)\n        for path in paths:\n            self._process_remainder(path)\n        self.parent_widget.working_stop()\n\n    def collect_data(self):\n        data = {'representations' : []}\n        for item in self.components_list.widgets():\n            data['representations'].append(item.collect_data())\n        return data\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/widget_family.py",
    "content": "import re\n\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.client import (\n    get_asset_by_name,\n    get_subset_by_name,\n    get_subsets,\n    get_last_version_by_subset_id,\n)\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import LegacyCreator\nfrom openpype.pipeline.version_start import get_versioning_start\nfrom openpype.pipeline.create import (\n    SUBSET_NAME_ALLOWED_SYMBOLS,\n    TaskNotSetError,\n)\n\nfrom . import HelpRole, FamilyRole, ExistsRole, PluginRole, PluginKeyRole\nfrom . import FamilyDescriptionWidget\n\n\nclass FamilyWidget(QtWidgets.QWidget):\n\n    stateChanged = QtCore.Signal(bool)\n    data = dict()\n    _jobs = dict()\n    Separator = \"---separator---\"\n    NOT_SELECTED = '< Nothing is selected >'\n\n    def __init__(self, dbcon, parent=None):\n        super(FamilyWidget, self).__init__(parent=parent)\n        # Store internal states in here\n        self.state = {\"valid\": False}\n        self.dbcon = dbcon\n        self.asset_name = self.NOT_SELECTED\n\n        body = QtWidgets.QWidget()\n        lists = QtWidgets.QWidget()\n\n        container = QtWidgets.QWidget()\n\n        list_families = QtWidgets.QListWidget()\n\n        input_subset = QtWidgets.QLineEdit()\n        input_result = QtWidgets.QLineEdit()\n        input_result.setEnabled(False)\n\n        # region Menu for default subset names\n        btn_subset = QtWidgets.QPushButton()\n        btn_subset.setFixedWidth(18)\n        menu_subset = QtWidgets.QMenu(btn_subset)\n        btn_subset.setMenu(menu_subset)\n\n        # endregion\n        name_layout = QtWidgets.QHBoxLayout()\n        name_layout.addWidget(input_subset, 1)\n        name_layout.addWidget(btn_subset, 0)\n        name_layout.setContentsMargins(0, 0, 0, 0)\n\n        # version\n        version_spinbox = QtWidgets.QSpinBox()\n        version_spinbox.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n        version_spinbox.setMinimum(1)\n        version_spinbox.setMaximum(9999)\n        version_spinbox.setEnabled(False)\n\n        version_checkbox = QtWidgets.QCheckBox(\"Next Available Version\")\n        version_checkbox.setCheckState(QtCore.Qt.CheckState(2))\n\n        version_layout = QtWidgets.QHBoxLayout()\n        version_layout.addWidget(version_spinbox)\n        version_layout.addWidget(version_checkbox)\n\n        layout = QtWidgets.QVBoxLayout(container)\n\n        header = FamilyDescriptionWidget(parent=self)\n        layout.addWidget(header)\n\n        layout.addWidget(QtWidgets.QLabel(\"Family\"))\n        layout.addWidget(list_families)\n        layout.addWidget(QtWidgets.QLabel(\"Subset\"))\n        layout.addLayout(name_layout)\n        layout.addWidget(input_result)\n        layout.addWidget(QtWidgets.QLabel(\"Version\"))\n        layout.addLayout(version_layout)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        options = QtWidgets.QWidget()\n\n        layout = QtWidgets.QGridLayout(options)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        layout = QtWidgets.QHBoxLayout(lists)\n        layout.addWidget(container)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        layout = QtWidgets.QVBoxLayout(body)\n\n        layout.addWidget(lists)\n        layout.addWidget(options, 0, QtCore.Qt.AlignLeft)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(body)\n\n        input_subset.textChanged.connect(self.on_data_changed)\n        list_families.currentItemChanged.connect(self.on_selection_changed)\n        list_families.currentItemChanged.connect(header.set_item)\n        version_checkbox.stateChanged.connect(self.on_version_refresh)\n\n        self.stateChanged.connect(self._on_state_changed)\n\n        self.input_subset = input_subset\n        self.menu_subset = menu_subset\n        self.btn_subset = btn_subset\n        self.list_families = list_families\n        self.input_result = input_result\n        self.version_checkbox = version_checkbox\n        self.version_spinbox = version_spinbox\n\n        self.refresh()\n\n    def collect_data(self):\n        plugin = self.list_families.currentItem().data(PluginRole)\n        key = self.list_families.currentItem().data(PluginKeyRole)\n        family = plugin.family.rsplit(\".\", 1)[-1]\n        data = {\n            'family_preset_key': key,\n            'family': family,\n            'subset': self.input_result.text(),\n            'version': self.version_spinbox.value(),\n            'use_next_available_version': self.version_checkbox.isChecked(),\n        }\n        return data\n\n    def on_task_change(self):\n        self.on_data_changed()\n\n    def change_asset(self, name):\n        if name is None:\n            name = self.NOT_SELECTED\n        self.asset_name = name\n        self.on_data_changed()\n\n    def _on_state_changed(self, state):\n        self.state['valid'] = state\n\n    def _build_menu(self, default_names):\n        \"\"\"Create optional predefined subset names\n\n        Args:\n            default_names(list): all predefined names\n\n        Returns:\n             None\n        \"\"\"\n\n        # Get and destroy the action group\n        group = self.btn_subset.findChild(QtWidgets.QActionGroup)\n        if group:\n            group.deleteLater()\n\n        state = any(default_names)\n        self.btn_subset.setEnabled(state)\n        if state is False:\n            return\n\n        # Build new action group\n        group = QtWidgets.QActionGroup(self.btn_subset)\n        for name in default_names:\n            if name == self.Separator:\n                self.menu_subset.addSeparator()\n                continue\n            action = group.addAction(name)\n            self.menu_subset.addAction(action)\n\n        group.triggered.connect(self._on_action_clicked)\n\n    def _on_action_clicked(self, action):\n        self.input_subset.setText(action.text())\n\n    def _on_data_changed(self):\n        asset_name = self.asset_name\n        user_input_text = self.input_subset.text()\n        item = self.list_families.currentItem()\n\n        if item is None:\n            return\n\n        # Early exit if no asset name\n        if (\n            asset_name == self.NOT_SELECTED\n            or not asset_name.strip()\n        ):\n            self._build_menu([])\n            item.setData(ExistsRole, False)\n            print(\"Asset name is required ..\")\n            self.stateChanged.emit(False)\n            return\n\n        # Get the asset from the database which match with the name\n        project_name = self.dbcon.active_project()\n        asset_doc = get_asset_by_name(\n            project_name, asset_name, fields=[\"_id\"]\n        )\n\n        # Get plugin\n        plugin = item.data(PluginRole)\n\n        if asset_doc and plugin:\n            asset_id = asset_doc[\"_id\"]\n            task_name = self.dbcon.Session[\"AVALON_TASK\"]\n\n            # Calculate subset name with Creator plugin\n            try:\n                subset_name = plugin.get_subset_name(\n                    user_input_text, task_name, asset_id, project_name\n                )\n                # Force replacement of prohibited symbols\n                # QUESTION should Creator care about this and here should be\n                #   only validated with schema regex?\n                subset_name = re.sub(\n                    \"[^{}]+\".format(SUBSET_NAME_ALLOWED_SYMBOLS),\n                    \"\",\n                    subset_name\n                )\n                self.input_result.setText(subset_name)\n\n            except TaskNotSetError:\n                subset_name = \"\"\n                self.input_result.setText(\"Select task please\")\n\n            # Get all subsets of the current asset\n            subset_docs = get_subsets(\n                project_name, asset_ids=[asset_id], fields=[\"name\"]\n            )\n\n            existing_subset_names = {\n                subset_doc[\"name\"]\n                for subset_doc in subset_docs\n            }\n\n            # Defaults to dropdown\n            defaults = []\n            # Check if Creator plugin has set defaults\n            if (\n                plugin.defaults\n                and isinstance(plugin.defaults, (list, tuple, set))\n            ):\n                defaults = list(plugin.defaults)\n\n            # Replace\n            compare_regex = re.compile(re.sub(\n                user_input_text, \"(.+)\", subset_name, flags=re.IGNORECASE\n            ))\n            subset_hints = set()\n            if user_input_text:\n                for _name in existing_subset_names:\n                    _result = compare_regex.search(_name)\n                    if _result:\n                        subset_hints |= set(_result.groups())\n\n            subset_hints = subset_hints - set(defaults)\n            if subset_hints:\n                if defaults:\n                    defaults.append(self.Separator)\n                defaults.extend(subset_hints)\n            self._build_menu(defaults)\n\n            item.setData(ExistsRole, True)\n\n        else:\n            subset_name = user_input_text\n            self._build_menu([])\n            item.setData(ExistsRole, False)\n\n            if not plugin:\n                print(\"No registered families ..\")\n            else:\n                print(\"Asset '%s' not found ..\" % asset_name)\n\n        self.on_version_refresh()\n\n        # Update the valid state\n        valid = (\n            asset_name != self.NOT_SELECTED and\n            subset_name.strip() != \"\" and\n            item.data(QtCore.Qt.ItemIsEnabled) and\n            item.data(ExistsRole)\n        )\n        self.stateChanged.emit(valid)\n\n    def on_version_refresh(self):\n        auto_version = self.version_checkbox.isChecked()\n        self.version_spinbox.setEnabled(not auto_version)\n        if not auto_version:\n            return\n\n        project_name = self.dbcon.active_project()\n        asset_name = self.asset_name\n        subset_name = self.input_result.text()\n        plugin = self.list_families.currentItem().data(PluginRole)\n        family = plugin.family.rsplit(\".\", 1)[-1]\n        version = get_versioning_start(\n            project_name,\n            \"standalonepublisher\",\n            task_name=self.dbcon.Session[\"AVALON_TASK\"],\n            family=family,\n            subset=subset_name\n        )\n\n        asset_doc = None\n        subset_doc = None\n        if (\n            asset_name != self.NOT_SELECTED and\n            subset_name.strip() != ''\n        ):\n            asset_doc = get_asset_by_name(\n                project_name, asset_name, fields=[\"_id\"]\n            )\n\n        if asset_doc:\n            subset_doc = get_subset_by_name(\n                project_name,\n                subset_name,\n                asset_doc['_id'],\n                fields=[\"_id\"]\n            )\n\n        if subset_doc:\n            last_version = get_last_version_by_subset_id(\n                project_name,\n                subset_doc[\"_id\"],\n                fields=[\"name\"]\n            )\n            if last_version:\n                version = last_version[\"name\"] + 1\n\n        self.version_spinbox.setValue(version)\n\n    def on_data_changed(self, *args):\n\n        # Set invalid state until it's reconfirmed to be valid by the\n        # scheduled callback so any form of creation is held back until\n        # valid again\n        self.stateChanged.emit(False)\n        self.schedule(self._on_data_changed, 500, channel=\"gui\")\n\n    def on_selection_changed(self, *args):\n        item = self.list_families.currentItem()\n        if not item:\n            return\n        plugin = item.data(PluginRole)\n        if plugin is None:\n            return\n\n        if plugin.defaults and isinstance(plugin.defaults, list):\n            default = plugin.defaults[0]\n        else:\n            default = \"Default\"\n\n        self.input_subset.setText(default)\n\n        self.on_data_changed()\n\n    def keyPressEvent(self, event):\n        \"\"\"Custom keyPressEvent.\n\n        Override keyPressEvent to do nothing so that Maya's panels won't\n        take focus when pressing \"SHIFT\" whilst mouse is over viewport or\n        outliner. This way users don't accidentally perform Maya commands\n        whilst trying to name an instance.\n\n        \"\"\"\n\n    def refresh(self):\n        self.list_families.clear()\n\n        has_families = False\n        project_name = self.dbcon.Session.get(\"AVALON_PROJECT\")\n        if not project_name:\n            return\n\n        settings = get_project_settings(project_name)\n        sp_settings = settings.get('standalonepublisher', {})\n\n        for key, creator_data in sp_settings.get(\"create\", {}).items():\n            creator = type(key, (LegacyCreator, ), creator_data)\n\n            label = creator.label or creator.family\n            item = QtWidgets.QListWidgetItem(label)\n            item.setData(QtCore.Qt.ItemIsEnabled, True)\n            item.setData(HelpRole, creator.help or \"\")\n            item.setData(FamilyRole, creator.family)\n            item.setData(PluginRole, creator)\n            item.setData(PluginKeyRole, key)\n            item.setData(ExistsRole, False)\n            self.list_families.addItem(item)\n\n            has_families = True\n\n        if not has_families:\n            item = QtWidgets.QListWidgetItem(\"No registered families\")\n            item.setData(QtCore.Qt.ItemIsEnabled, False)\n            self.list_families.addItem(item)\n\n        self.list_families.setCurrentItem(self.list_families.item(0))\n\n    def schedule(self, func, time, channel=\"default\"):\n        try:\n            self._jobs[channel].stop()\n        except (AttributeError, KeyError):\n            pass\n\n        timer = QtCore.QTimer()\n        timer.setSingleShot(True)\n        timer.timeout.connect(func)\n        timer.start(time)\n\n        self._jobs[channel] = timer\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/widget_family_desc.py",
    "content": "import six\nfrom qtpy import QtWidgets, QtCore, QtGui\nimport qtawesome\nfrom . import FamilyRole, PluginRole\n\n\nclass FamilyDescriptionWidget(QtWidgets.QWidget):\n    \"\"\"A family description widget.\n\n    Shows a family icon, family name and a help description.\n    Used in creator header.\n\n     _________________\n    |  ____           |\n    | |icon| FAMILY   |\n    | |____| help     |\n    |_________________|\n\n    \"\"\"\n\n    SIZE = 35\n\n    def __init__(self, parent=None):\n        super(FamilyDescriptionWidget, self).__init__(parent=parent)\n\n        # Header font\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setPointSize(14)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        icon = QtWidgets.QLabel()\n        icon.setSizePolicy(QtWidgets.QSizePolicy.Maximum,\n                           QtWidgets.QSizePolicy.Maximum)\n\n        # Add 4 pixel padding to avoid icon being cut off\n        icon.setFixedWidth(self.SIZE + 4)\n        icon.setFixedHeight(self.SIZE + 4)\n        icon.setStyleSheet(\"\"\"\n        QLabel {\n            padding-right: 5px;\n        }\n        \"\"\")\n\n        label_layout = QtWidgets.QVBoxLayout()\n        label_layout.setSpacing(0)\n\n        family = QtWidgets.QLabel(\"family\")\n        family.setFont(font)\n        family.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft)\n\n        help = QtWidgets.QLabel(\"help\")\n        help.setWordWrap(True)\n        help.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)\n\n        label_layout.addWidget(family)\n        label_layout.addWidget(help)\n\n        layout.addWidget(icon)\n        layout.addLayout(label_layout)\n\n        self.help = help\n        self.family = family\n        self.icon = icon\n\n    def set_item(self, item):\n        \"\"\"Update elements to display information of a family item.\n\n        Args:\n            family (dict): A family item as registered with name, help and icon\n\n        Returns:\n            None\n\n        \"\"\"\n        if not item:\n            return\n\n        # Support a font-awesome icon\n        plugin = item.data(PluginRole)\n        icon = getattr(plugin, \"icon\", \"info-circle\")\n        assert isinstance(icon, six.string_types)\n        icon = qtawesome.icon(\"fa.{}\".format(icon), color=\"white\")\n        pixmap = icon.pixmap(self.SIZE, self.SIZE)\n        pixmap = pixmap.scaled(self.SIZE, self.SIZE)\n\n        # Parse a clean line from the Creator's docstring\n        docstring = plugin.help or \"\"\n\n        help = docstring.splitlines()[0] if docstring else \"\"\n\n        self.icon.setPixmap(pixmap)\n        self.family.setText(item.data(FamilyRole))\n        self.help.setText(help)\n"
  },
  {
    "path": "openpype/tools/standalonepublish/widgets/widget_shadow.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\n\nclass ShadowWidget(QtWidgets.QWidget):\n    def __init__(self, parent):\n        self.parent_widget = parent\n        super().__init__(parent)\n        w = self.parent_widget.frameGeometry().width()\n        h = self.parent_widget.frameGeometry().height()\n        self.resize(QtCore.QSize(w, h))\n        palette = QtGui.QPalette(self.palette())\n        palette.setColor(palette.Background, QtCore.Qt.transparent)\n        self.setPalette(palette)\n        self.message = ''\n\n        font = QtGui.QFont()\n        font.setFamily(\"DejaVu Sans Condensed\")\n        font.setPointSize(40)\n        font.setBold(True)\n        font.setWeight(50)\n        font.setKerning(True)\n        self.font = font\n\n    def paintEvent(self, event):\n        painter = QtGui.QPainter()\n        painter.begin(self)\n        painter.setFont(self.font)\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n        painter.fillRect(\n            event.rect(), QtGui.QBrush(QtGui.QColor(0, 0, 0, 127))\n        )\n        painter.drawText(\n            QtCore.QRectF(\n                0.0,\n                0.0,\n                self.parent_widget.frameGeometry().width(),\n                self.parent_widget.frameGeometry().height()\n            ),\n            QtCore.Qt.AlignCenter | QtCore.Qt.AlignCenter,\n            self.message\n        )\n        painter.end()\n"
  },
  {
    "path": "openpype/tools/stdout_broker/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/tools/stdout_broker/app.py",
    "content": "import os\nimport sys\nimport threading\nimport collections\nimport websocket\nimport json\nfrom datetime import datetime\n\nfrom openpype.lib import Logger\nfrom openpype_modules.webserver.host_console_listener import MsgAction\n\nlog = Logger.get_logger(__name__)\n\n\nclass StdOutBroker:\n    \"\"\"\n    Application showing console in Services tray for non python hosts\n    instead of cmd window.\n    \"\"\"\n    MAX_LINES = 10000\n    TIMER_TIMEOUT = 0.200\n\n    def __init__(self, host_name):\n        self.host_name = host_name\n        self.webserver_client = None\n\n        self.original_stdout_write = None\n        self.original_stderr_write = None\n        self.log_queue = collections.deque()\n\n        date_str = datetime.now().strftime(\"%d%m%Y%H%M%S\")\n        self.host_id = \"{}_{}\".format(self.host_name, date_str)\n\n        self._std_available = False\n        self._is_running = False\n        self._catch_std_outputs()\n\n        self._timer = None\n\n    @property\n    def send_to_tray(self):\n        \"\"\"Checks if connected to tray and have access to logs.\"\"\"\n        return self.webserver_client and self._std_available\n\n    def start(self):\n        \"\"\"Start app, create and start timer\"\"\"\n        if not self._std_available or self._is_running:\n            return\n        self._is_running = True\n        self._create_timer()\n        self._connect_to_tray()\n\n    def stop(self):\n        \"\"\"Disconnect from Tray, process last logs\"\"\"\n        if not self._is_running:\n            return\n        self._is_running = False\n        self._process_queue()\n        self._disconnect_from_tray()\n\n    def host_connected(self):\n        \"\"\"Send to Tray console that host is ready - icon change. \"\"\"\n        log.info(\"Host {} connected\".format(self.host_id))\n\n        payload = {\n            \"host\": self.host_id,\n            \"action\": MsgAction.INITIALIZED,\n            \"text\": \"Integration with {}\".format(\n                str.capitalize(self.host_name))\n        }\n        self._send(payload)\n\n    def _create_timer(self):\n        timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback)\n        timer.start()\n        self._timer = timer\n\n    def _timer_callback(self):\n        if not self._is_running:\n            return\n        self._process_queue()\n        self._create_timer()\n\n    def _connect_to_tray(self):\n        \"\"\"Connect to Tray webserver to pass console output. \"\"\"\n        if not self._std_available:  # not content to log\n            return\n        ws = websocket.WebSocket()\n        webserver_url = os.environ.get(\"OPENPYPE_WEBSERVER_URL\")\n\n        if not webserver_url:\n            print(\"Unknown webserver url, cannot connect to pass log\")\n            return\n\n        webserver_url = webserver_url.replace(\"http\", \"ws\")\n        ws.connect(\"{}/ws/host_listener\".format(webserver_url))\n        self.webserver_client = ws\n\n        payload = {\n            \"host\": self.host_id,\n            \"action\": MsgAction.CONNECTING,\n            \"text\": \"Integration with {}\".format(\n                str.capitalize(self.host_name))\n        }\n        self._send(payload)\n\n    def _disconnect_from_tray(self):\n        \"\"\"Send to Tray that host is closing - remove from Services. \"\"\"\n        print(\"Host {} closing\".format(self.host_name))\n        if not self.webserver_client:\n            return\n\n        payload = {\n            \"host\": self.host_id,\n            \"action\": MsgAction.CLOSE,\n            \"text\": \"Integration with {}\".format(\n                str.capitalize(self.host_name))\n        }\n\n        self._send(payload)\n        self.webserver_client.close()\n\n    def _catch_std_outputs(self):\n        \"\"\"Redirects standard out and error to own functions\"\"\"\n        if sys.stdout:\n            self.original_stdout_write = sys.stdout.write\n            sys.stdout.write = self._my_stdout_write\n            self._std_available = True\n\n        if sys.stderr:\n            self.original_stderr_write = sys.stderr.write\n            sys.stderr.write = self._my_stderr_write\n            self._std_available = True\n\n    def _my_stdout_write(self, text):\n        \"\"\"Appends outputted text to queue, keep writing to original stdout\"\"\"\n        if self.original_stdout_write is not None:\n            self.original_stdout_write(text)\n        if self.send_to_tray:\n            self.log_queue.append(text)\n\n    def _my_stderr_write(self, text):\n        \"\"\"Appends outputted text to queue, keep writing to original stderr\"\"\"\n        if self.original_stderr_write is not None:\n            self.original_stderr_write(text)\n        if self.send_to_tray:\n            self.log_queue.append(text)\n\n    def _process_queue(self):\n        \"\"\"Sends lines and purges queue\"\"\"\n        if not self.send_to_tray:\n            return\n\n        lines = tuple(self.log_queue)\n        self.log_queue.clear()\n        if lines:\n            payload = {\n                \"host\": self.host_id,\n                \"action\": MsgAction.ADD,\n                \"text\": \"\\n\".join(lines)\n            }\n\n            self._send(payload)\n\n    def _send(self, payload):\n        \"\"\"Worker method to send to existing websocket connection.\"\"\"\n        if not self.send_to_tray:\n            return\n\n        try:\n            self.webserver_client.send(json.dumps(payload))\n        except ConnectionResetError:  # Tray closed\n            self._connect_to_tray()\n"
  },
  {
    "path": "openpype/tools/stdout_broker/window.py",
    "content": "import re\nimport collections\n\nfrom qtpy import QtWidgets\n\nfrom openpype import style\n\n\nclass ConsoleDialog(QtWidgets.QDialog):\n    \"\"\"Qt dialog to show stdout instead of unwieldy cmd window\"\"\"\n    WIDTH = 720\n    HEIGHT = 450\n    MAX_LINES = 10000\n\n    sdict = {\n        r\">>> \":\n            '<span style=\"font-weight: bold;color:#EE5C42\"> >>> </span>',\n        r\"!!!(?!\\sCRI|\\sERR)\":\n            '<span style=\"font-weight: bold;color:red\"> !!! </span>',\n        r\"\\-\\-\\- \":\n            '<span style=\"font-weight: bold;color:cyan\"> --- </span>',\n        r\"\\*\\*\\*(?!\\sWRN)\":\n            '<span style=\"font-weight: bold;color:#FFD700\"> *** </span>',\n        r\"\\*\\*\\* WRN\":\n            '<span style=\"font-weight: bold;color:#FFD700\"> *** WRN</span>',\n        r\"  \\- \":\n            '<span style=\"font-weight: bold;color:#FFD700\">  - </span>',\n        r\"\\[ \":\n            '<span style=\"font-weight: bold;color:#66CDAA\">[</span>',\n        r\"\\]\":\n            '<span style=\"font-weight: bold;color:#66CDAA\">]</span>',\n        r\"{\":\n            '<span style=\"color:#66CDAA\">{',\n        r\"}\":\n            r\"}</span>\",\n        r\"\\(\":\n            '<span style=\"color:#66CDAA\">(',\n        r\"\\)\":\n            r\")</span>\",\n        r\"^\\.\\.\\. \":\n            '<span style=\"font-weight: bold;color:#EE5C42\"> ... </span>',\n        r\"!!! ERR: \":\n            '<span style=\"font-weight: bold;color:#EE5C42\"> !!! ERR: </span>',\n        r\"!!! CRI: \":\n            '<span style=\"font-weight: bold;color:red\"> !!! CRI: </span>',\n        r\"(?i)failed\":\n            '<span style=\"font-weight: bold;color:#EE5C42\"> FAILED </span>',\n        r\"(?i)error\":\n            '<span style=\"font-weight: bold;color:#EE5C42\"> ERROR </span>'\n    }\n\n    def __init__(self, text, parent=None):\n        super(ConsoleDialog, self).__init__(parent)\n        layout = QtWidgets.QHBoxLayout(parent)\n\n        plain_text = QtWidgets.QPlainTextEdit(self)\n        plain_text.setReadOnly(True)\n        plain_text.resize(self.WIDTH, self.HEIGHT)\n        plain_text.maximumBlockCount = self.MAX_LINES\n\n        while text:\n            plain_text.appendPlainText(text.popleft().strip())\n\n        layout.addWidget(plain_text)\n\n        self.setWindowTitle(\"Console output\")\n\n        self.plain_text = plain_text\n\n        self.setStyleSheet(style.load_stylesheet())\n\n        self.resize(self.WIDTH, self.HEIGHT)\n\n    def append_text(self, new_text):\n        if isinstance(new_text, str):\n            new_text = collections.deque(new_text.split(\"\\n\"))\n        while new_text:\n            text = new_text.popleft()\n            if text:\n                self.plain_text.appendHtml(self.color(text))\n\n    def _multiple_replace(self, text, adict):\n        \"\"\"Replace multiple tokens defined in dict.\n\n        Find and replace all occurrences of strings defined in dict is\n        supplied string.\n\n        Args:\n            text (str): string to be searched\n            adict (dict): dictionary with `{'search': 'replace'}`\n\n        Returns:\n            str: string with replaced tokens\n\n        \"\"\"\n        for r, v in adict.items():\n            text = re.sub(r, v, text)\n\n        return text\n\n    def color(self, message):\n        \"\"\"Color message with html tags. \"\"\"\n        message = self._multiple_replace(message, self.sdict)\n\n        return message\n"
  },
  {
    "path": "openpype/tools/subsetmanager/README.md",
    "content": "Subset manager\n--------------\n\nSimple UI showing list of created subset that will be published via Pyblish.\nUseful for applications (Photoshop, AfterEffects, TVPaint, Harmony) which are\nstoring metadata about instance hidden from user.\n\nThis UI allows listing all created subset and removal of them if needed (\nin case use doesn't want to publish anymore, its using workfile as a starting \nfile for different task and instances should be completely different etc.\n)\n\nHost is expected to implemented:\n- `list_instances` - returning list of dictionaries (instances), must contain\n    unique uuid field\n    example: \n    ```[{\"uuid\":\"15\",\"active\":true,\"subset\":\"imageBG\",\"family\":\"image\",\"id\":\"pyblish.avalon.instance\",\"asset\":\"Town\"}]```\n- `remove_instance(instance)` - removes instance from file's metadata\n    instance is a dictionary, with uuid field "
  },
  {
    "path": "openpype/tools/subsetmanager/__init__.py",
    "content": "from .window import (\n    show,\n    SubsetManagerWindow\n)\n\n__all__ = (\n    \"show\",\n    \"SubsetManagerWindow\"\n)\n"
  },
  {
    "path": "openpype/tools/subsetmanager/model.py",
    "content": "import uuid\n\nfrom qtpy import QtCore, QtGui\n\nfrom openpype.pipeline import registered_host\n\nITEM_ID_ROLE = QtCore.Qt.UserRole + 1\n\n\nclass InstanceModel(QtGui.QStandardItemModel):\n    def __init__(self, *args, **kwargs):\n        super(InstanceModel, self).__init__(*args, **kwargs)\n        self._instances_by_item_id = {}\n\n    def get_instance_by_id(self, item_id):\n        return self._instances_by_item_id.get(item_id)\n\n    def refresh(self):\n        self.clear()\n\n        self._instances_by_item_id = {}\n\n        instances = None\n        host = registered_host()\n        list_instances = getattr(host, \"list_instances\", None)\n        if list_instances:\n            instances = list_instances()\n\n        if not instances:\n            return\n\n        items = []\n        for instance_data in instances:\n            item_id = str(uuid.uuid4())\n            label = instance_data.get(\"label\") or instance_data[\"subset\"]\n            item = QtGui.QStandardItem(label)\n            item.setEnabled(True)\n            item.setEditable(False)\n            item.setData(item_id, ITEM_ID_ROLE)\n            items.append(item)\n            self._instances_by_item_id[item_id] = instance_data\n\n        if items:\n            self.invisibleRootItem().appendRows(items)\n\n    def headerData(self, section, orientation, role):\n        if role == QtCore.Qt.DisplayRole and section == 0:\n            return \"Instance\"\n\n        return super(InstanceModel, self).headerData(\n            section, orientation, role\n        )\n"
  },
  {
    "path": "openpype/tools/subsetmanager/widgets.py",
    "content": "import json\nfrom qtpy import QtWidgets, QtCore\n\n\nclass InstanceDetail(QtWidgets.QWidget):\n    save_triggered = QtCore.Signal()\n\n    def __init__(self, parent=None):\n        super(InstanceDetail, self).__init__(parent)\n\n        details_widget = QtWidgets.QPlainTextEdit(self)\n        details_widget.setObjectName(\"SubsetManagerDetailsText\")\n\n        save_btn = QtWidgets.QPushButton(\"Save\", self)\n\n        self._block_changes = False\n        self._editable = False\n        self._item_id = None\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(details_widget, 1)\n        layout.addWidget(save_btn, 0, QtCore.Qt.AlignRight)\n\n        save_btn.clicked.connect(self._on_save_clicked)\n        details_widget.textChanged.connect(self._on_text_change)\n\n        self._details_widget = details_widget\n        self._save_btn = save_btn\n\n        self.set_editable(False)\n\n    def _on_save_clicked(self):\n        if self.is_valid():\n            self.save_triggered.emit()\n\n    def set_editable(self, enabled=True):\n        self._editable = enabled\n        self.update_state()\n\n    def update_state(self, valid=None):\n        editable = self._editable\n        if not self._item_id:\n            editable = False\n\n        self._save_btn.setVisible(editable)\n        self._details_widget.setReadOnly(not editable)\n        if valid is None:\n            valid = self.is_valid()\n\n        self._save_btn.setEnabled(valid)\n        self._set_invalid_detail(valid)\n\n    def _set_invalid_detail(self, valid):\n        state = \"\"\n        if not valid:\n            state = \"invalid\"\n\n        current_state = self._details_widget.property(\"state\")\n        if current_state != state:\n            self._details_widget.setProperty(\"state\", state)\n            self._details_widget.style().polish(self._details_widget)\n\n    def set_details(self, container, item_id):\n        self._item_id = item_id\n\n        text = \"Nothing selected\"\n        if item_id:\n            try:\n                text = json.dumps(container, indent=4)\n            except Exception:\n                text = str(container)\n\n        self._block_changes = True\n        self._details_widget.setPlainText(text)\n        self._block_changes = False\n\n        self.update_state()\n\n    def instance_data_from_text(self):\n        try:\n            jsoned = json.loads(self._details_widget.toPlainText())\n        except Exception:\n            jsoned = None\n        return jsoned\n\n    def item_id(self):\n        return self._item_id\n\n    def is_valid(self):\n        if not self._item_id:\n            return True\n\n        value = self._details_widget.toPlainText()\n        valid = False\n        try:\n            jsoned = json.loads(value)\n            if jsoned and isinstance(jsoned, dict):\n                valid = True\n\n        except Exception:\n            pass\n        return valid\n\n    def _on_text_change(self):\n        if self._block_changes or not self._item_id:\n            return\n\n        valid = self.is_valid()\n        self.update_state(valid)\n"
  },
  {
    "path": "openpype/tools/subsetmanager/window.py",
    "content": "import os\nimport sys\n\nfrom qtpy import QtWidgets, QtCore\nimport qtawesome\n\nfrom openpype import style\nfrom openpype.pipeline import registered_host\nfrom openpype.tools.utils import PlaceholderLineEdit\nfrom openpype.tools.utils.lib import (\n    iter_model_rows,\n    qt_app_context\n)\nfrom openpype.tools.utils.models import RecursiveSortFilterProxyModel\nfrom .model import (\n    InstanceModel,\n    ITEM_ID_ROLE\n)\nfrom .widgets import InstanceDetail\n\n\nmodule = sys.modules[__name__]\nmodule.window = None\n\n\nclass SubsetManagerWindow(QtWidgets.QDialog):\n    def __init__(self, parent=None):\n        super(SubsetManagerWindow, self).__init__(parent=parent)\n        self.setWindowTitle(\"Subset Manager 0.1\")\n        self.setObjectName(\"SubsetManager\")\n        if not parent:\n            self.setWindowFlags(\n                self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint\n            )\n\n        self.resize(780, 430)\n\n        # Trigger refresh on first called show\n        self._first_show = True\n\n        left_side_widget = QtWidgets.QWidget(self)\n\n        # Header part\n        header_widget = QtWidgets.QWidget(left_side_widget)\n\n        # Filter input\n        filter_input = PlaceholderLineEdit(header_widget)\n        filter_input.setPlaceholderText(\"Filter subsets..\")\n\n        # Refresh button\n        icon = qtawesome.icon(\"fa.refresh\", color=\"white\")\n        refresh_btn = QtWidgets.QPushButton(header_widget)\n        refresh_btn.setIcon(icon)\n\n        header_layout = QtWidgets.QHBoxLayout(header_widget)\n        header_layout.setContentsMargins(0, 0, 0, 0)\n        header_layout.addWidget(filter_input)\n        header_layout.addWidget(refresh_btn)\n\n        # Instances view\n        view = QtWidgets.QTreeView(left_side_widget)\n        view.setIndentation(0)\n        view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n\n        model = InstanceModel(view)\n        proxy = RecursiveSortFilterProxyModel()\n        proxy.setSourceModel(model)\n        proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        view.setModel(proxy)\n\n        left_side_layout = QtWidgets.QVBoxLayout(left_side_widget)\n        left_side_layout.setContentsMargins(0, 0, 0, 0)\n        left_side_layout.addWidget(header_widget)\n        left_side_layout.addWidget(view)\n\n        details_widget = InstanceDetail(self)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.addWidget(left_side_widget, 0)\n        layout.addWidget(details_widget, 1)\n\n        filter_input.textChanged.connect(proxy.setFilterFixedString)\n        refresh_btn.clicked.connect(self._on_refresh_clicked)\n        view.clicked.connect(self._on_activated)\n        view.customContextMenuRequested.connect(self.on_context_menu)\n        details_widget.save_triggered.connect(self._on_save)\n\n        self._model = model\n        self._proxy = proxy\n        self._view = view\n        self._details_widget = details_widget\n        self._refresh_btn = refresh_btn\n\n    def _on_refresh_clicked(self):\n        self.refresh()\n\n    def _on_activated(self, index):\n        container = None\n        item_id = None\n        if index.isValid():\n            item_id = index.data(ITEM_ID_ROLE)\n            container = self._model.get_instance_by_id(item_id)\n\n        self._details_widget.set_details(container, item_id)\n\n    def _on_save(self):\n        host = registered_host()\n        if not hasattr(host, \"save_instances\"):\n            print(\"BUG: Host does not have \\\"save_instances\\\" method\")\n            return\n\n        current_index = self._view.selectionModel().currentIndex()\n        if not current_index.isValid():\n            return\n\n        item_id = current_index.data(ITEM_ID_ROLE)\n        if item_id != self._details_widget.item_id():\n            return\n\n        item_data = self._details_widget.instance_data_from_text()\n        new_instances = []\n        for index in iter_model_rows(self._model, 0):\n            _item_id = index.data(ITEM_ID_ROLE)\n            if _item_id == item_id:\n                instance_data = item_data\n            else:\n                instance_data = self._model.get_instance_by_id(item_id)\n            new_instances.append(instance_data)\n\n        host.save_instances(new_instances)\n\n    def on_context_menu(self, point):\n        point_index = self._view.indexAt(point)\n        item_id = point_index.data(ITEM_ID_ROLE)\n        instance_data = self._model.get_instance_by_id(item_id)\n        if instance_data is None:\n            return\n\n        # Prepare menu\n        menu = QtWidgets.QMenu(self)\n        actions = []\n        host = registered_host()\n        if hasattr(host, \"remove_instance\"):\n            action = QtWidgets.QAction(\"Remove instance\", menu)\n            action.setData(host.remove_instance)\n            actions.append(action)\n\n        if hasattr(host, \"select_instance\"):\n            action = QtWidgets.QAction(\"Select instance\", menu)\n            action.setData(host.select_instance)\n            actions.append(action)\n\n        if not actions:\n            actions.append(QtWidgets.QAction(\"* Nothing to do\", menu))\n\n        for action in actions:\n            menu.addAction(action)\n\n        # Show menu under mouse\n        global_point = self._view.mapToGlobal(point)\n        action = menu.exec_(global_point)\n        if not action or not action.data():\n            return\n\n        # Process action\n        # TODO catch exceptions\n        function = action.data()\n        function(instance_data)\n\n        # Reset modified data\n        self.refresh()\n\n    def refresh(self):\n        self._details_widget.set_details(None, None)\n        self._model.refresh()\n\n        host = registered_host()\n        dev_mode = os.environ.get(\"AVALON_DEVELOP_MODE\") or \"\"\n        editable = False\n        if dev_mode.lower() in (\"1\", \"yes\", \"true\", \"on\"):\n            editable = hasattr(host, \"save_instances\")\n        self._details_widget.set_editable(editable)\n\n    def showEvent(self, *args, **kwargs):\n        super(SubsetManagerWindow, self).showEvent(*args, **kwargs)\n        if self._first_show:\n            self._first_show = False\n            self.setStyleSheet(style.load_stylesheet())\n            self.refresh()\n\n\ndef show(root=None, debug=False, parent=None):\n    \"\"\"Display Scene Inventory GUI\n\n    Arguments:\n        debug (bool, optional): Run in debug-mode,\n            defaults to False\n        parent (QtCore.QObject, optional): When provided parent the interface\n            to this QObject.\n\n    \"\"\"\n\n    try:\n        module.window.close()\n        del module.window\n    except (RuntimeError, AttributeError):\n        pass\n\n    with qt_app_context():\n        window = SubsetManagerWindow(parent)\n        window.show()\n\n        module.window = window\n\n        # Pull window to the front.\n        module.window.raise_()\n        module.window.activateWindow()\n"
  },
  {
    "path": "openpype/tools/texture_copy/app.py",
    "content": "import os\nimport re\nimport click\n\nimport speedcopy\n\nfrom openpype.client import get_project, get_asset_by_name\nfrom openpype.lib import Terminal\nfrom openpype.pipeline import legacy_io, Anatomy\n\n\nt = Terminal()\n\ntexture_extensions = ['.tif', '.tiff', '.jpg', '.jpeg', '.tx', '.png', '.tga',\n                      '.psd', '.dpx', '.hdr', '.hdri', '.exr', '.sxr', '.psb']\n\n\nclass TextureCopy:\n\n    def __init__(self):\n        if not legacy_io.Session:\n            legacy_io.install()\n\n    def _get_textures(self, path):\n        textures = []\n        for dir, subdir, files in os.walk(path):\n            textures.extend(\n                os.path.join(dir, x) for x in files\n                if os.path.splitext(x)[1].lower() in texture_extensions)\n        return textures\n\n    def _get_destination_path(self, asset, project):\n        project_name = project[\"name\"]\n        hierarchy = \"\"\n        parents = asset['data']['parents']\n        if parents and len(parents) > 0:\n            hierarchy = os.path.join(*parents)\n\n        template_data = {\n            \"project\": {\n                \"name\": project_name,\n                \"code\": project['data']['code']\n            },\n            \"asset\": asset['name'],\n            \"family\": 'texture',\n            \"subset\": 'Main',\n            \"hierarchy\": hierarchy\n        }\n        anatomy = Anatomy(project_name)\n        template_obj = anatomy.templates_obj[\"texture\"][\"path\"]\n        return template_obj.format_strict(template_data)\n\n    def _get_version(self, path):\n        versions = [0]\n        dirs = [f.path for f in os.scandir(path) if f.is_dir()]\n        for d in dirs:\n            ver = re.search(r'^v(\\d+)$',\n                            os.path.basename(d),\n                            flags=re.IGNORECASE)\n            if ver is not None:\n                versions.append(int(ver.group(1)))\n\n        return max(versions) + 1\n\n    def _copy_textures(self, textures, destination):\n        for tex in textures:\n            dst = os.path.join(destination,\n                               os.path.basename(tex))\n            t.echo(\"  - Copy {} -> {}\".format(tex, dst))\n            try:\n                speedcopy.copyfile(tex, dst)\n            except Exception as e:\n                t.echo(\"!!! Copying failed\")\n                t.echo(\"!!! {}\".format(e))\n                exit(1)\n\n    def process(self, asset_name, project_name, path):\n        \"\"\"\n        Process all textures found in path and copy them to asset under\n        project.\n        \"\"\"\n\n        t.echo(\">>> Looking for textures ...\")\n        textures = self._get_textures(path)\n        if len(textures) < 1:\n            t.echo(\"!!! no textures found.\")\n            exit(1)\n        else:\n            t.echo(\">>> Found {} textures ...\".format(len(textures)))\n\n        project = get_project(project_name)\n        if not project:\n            t.echo(\"!!! Project name [ {} ] not found.\".format(project_name))\n            exit(1)\n\n        asset = get_asset_by_name(project_name, asset_name)\n        if not asset:\n            t.echo(\"!!! Asset [ {} ] not found in project\".format(asset_name))\n            exit(1)\n        t.echo((\">>> Project [ {} ] and \"\n                \"asset [ {} ] seems to be OK ...\").format(project['name'],\n                                                          asset['name']))\n\n        dst_path = self._get_destination_path(asset, project)\n        t.echo(\"--- Using [ {} ] as destination path\".format(dst_path))\n        if not os.path.exists(dst_path):\n            try:\n                os.makedirs(dst_path)\n            except IOError as e:\n                t.echo(\"!!! Unable to create destination directory\")\n                t.echo(\"!!! {}\".format(e))\n                exit(1)\n        version = '%02d' % self._get_version(dst_path)\n        t.echo(\"--- Using version [ {} ]\".format(version))\n        dst_path = os.path.join(dst_path, \"v{}\".format(version))\n        t.echo(\"--- Final destination path [ {} ]\".format(dst_path))\n        try:\n            os.makedirs(dst_path)\n        except FileExistsError:\n            t.echo(\"!!! Somethings wrong, version directory already exists\")\n            exit(1)\n        except IOError as e:\n            t.echo(\"!!! Cannot create version directory\")\n            t.echo(\"!!! {}\".format(e))\n            exit(1)\n\n        t.echo(\">>> copying textures  ...\")\n        self._copy_textures(textures, dst_path)\n        t.echo(\">>> done.\")\n        t.echo(\"<<< terminating ...\")\n\n\n@click.command()\n@click.option('--asset', required=True)\n@click.option('--project', required=True)\n@click.option('--path', required=True)\ndef texture_copy(asset, project, path):\n    t.echo(\"*** Running Texture tool ***\")\n    t.echo(\">>> Initializing avalon session ...\")\n    os.environ[\"AVALON_PROJECT\"] = project\n    os.environ[\"AVALON_ASSET\"] = asset\n    TextureCopy().process(asset, project, path)\n\n\nif __name__ == '__main__':\n    texture_copy()\n"
  },
  {
    "path": "openpype/tools/tray/__init__.py",
    "content": "from .pype_tray import main\n\n__all__ = (\n    \"main\",\n)\n"
  },
  {
    "path": "openpype/tools/tray/__main__.py",
    "content": "try:\n    from . import pype_tray\nexcept ImportError:\n    import pype_tray\n\n\npype_tray.main()\n"
  },
  {
    "path": "openpype/tools/tray/pype_info_widget.py",
    "content": "import os\nimport json\nimport collections\n\nimport ayon_api\nfrom qtpy import QtCore, QtGui, QtWidgets\n\nfrom openpype import style\nimport openpype.version\nfrom openpype import resources\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.settings.lib import get_local_settings\nfrom openpype.lib import get_openpype_execute_args\nfrom openpype.lib.pype_info import (\n    get_all_current_info,\n    get_openpype_info,\n    get_workstation_info,\n    extract_pype_info_to_file\n)\n\nIS_MAIN_ROLE = QtCore.Qt.UserRole\n\n\nclass EnvironmentValueDelegate(QtWidgets.QStyledItemDelegate):\n    def createEditor(self, parent, option, index):\n        edit_widget = QtWidgets.QLineEdit(parent)\n        edit_widget.setReadOnly(True)\n        return edit_widget\n\n\nclass EnvironmentsView(QtWidgets.QTreeView):\n    def __init__(self, parent=None):\n        super(EnvironmentsView, self).__init__(parent)\n\n        self._scroll_enabled = False\n\n        model = QtGui.QStandardItemModel()\n\n        env = os.environ.copy()\n        keys = []\n        values = []\n        for key in sorted(env.keys()):\n            key_item = QtGui.QStandardItem(key)\n            key_item.setFlags(\n                QtCore.Qt.ItemIsSelectable\n                | QtCore.Qt.ItemIsEnabled\n            )\n            key_item.setData(True, IS_MAIN_ROLE)\n            keys.append(key_item)\n\n            value = env[key]\n            value_item = QtGui.QStandardItem(value)\n            value_item.setData(True, IS_MAIN_ROLE)\n            values.append(value_item)\n\n            value_parts = [\n                part\n                for part in value.split(os.pathsep) if part\n            ]\n            if len(value_parts) < 2:\n                continue\n\n            sub_parts = []\n            for part_value in value_parts:\n                part_item = QtGui.QStandardItem(part_value)\n                part_item.setData(False, IS_MAIN_ROLE)\n                sub_parts.append(part_item)\n            key_item.appendRows(sub_parts)\n\n        model.appendColumn(keys)\n        model.appendColumn(values)\n        model.setHorizontalHeaderLabels([\"Key\", \"Value\"])\n\n        self.setModel(model)\n        # self.setIndentation(0)\n        delegate = EnvironmentValueDelegate(self)\n        self.setItemDelegate(delegate)\n        self.header().setSectionResizeMode(\n            0, QtWidgets.QHeaderView.ResizeToContents\n        )\n        self.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection)\n\n    def get_selection_as_dict(self):\n        indexes = self.selectionModel().selectedIndexes()\n\n        main_mapping = collections.defaultdict(dict)\n        for index in indexes:\n            is_main = index.data(IS_MAIN_ROLE)\n            if not is_main:\n                continue\n            row = index.row()\n            value = index.data(QtCore.Qt.DisplayRole)\n            if index.column() == 0:\n                key = \"key\"\n            else:\n                key = \"value\"\n            main_mapping[row][key] = value\n\n        result = {}\n        for item in main_mapping.values():\n            result[item[\"key\"]] = item[\"value\"]\n        return result\n\n    def keyPressEvent(self, event):\n        if (\n            event.type() == QtGui.QKeyEvent.KeyPress\n            and event.matches(QtGui.QKeySequence.Copy)\n        ):\n            selected_data = self.get_selection_as_dict()\n            selected_str = json.dumps(selected_data, indent=4)\n\n            mime_data = QtCore.QMimeData()\n            mime_data.setText(selected_str)\n            QtWidgets.QApplication.instance().clipboard().setMimeData(\n                mime_data\n            )\n            event.accept()\n        else:\n            return super(EnvironmentsView, self).keyPressEvent(event)\n\n    def set_scroll_enabled(self, value):\n        self._scroll_enabled = value\n\n    def wheelEvent(self, event):\n        if not self._scroll_enabled:\n            event.ignore()\n            return\n        return super(EnvironmentsView, self).wheelEvent(event)\n\n\nclass ClickableWidget(QtWidgets.QWidget):\n    clicked = QtCore.Signal()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self.clicked.emit()\n        super(ClickableWidget, self).mouseReleaseEvent(event)\n\n\nclass CollapsibleWidget(QtWidgets.QWidget):\n    def __init__(self, label, parent):\n        super(CollapsibleWidget, self).__init__(parent)\n\n        self.content_widget = None\n\n        top_part = ClickableWidget(parent=self)\n\n        button_size = QtCore.QSize(5, 5)\n        button_toggle = QtWidgets.QToolButton(parent=top_part)\n        button_toggle.setIconSize(button_size)\n        button_toggle.setArrowType(QtCore.Qt.RightArrow)\n        button_toggle.setCheckable(True)\n        button_toggle.setChecked(False)\n\n        label_widget = QtWidgets.QLabel(label, parent=top_part)\n\n        top_part_layout = QtWidgets.QHBoxLayout(top_part)\n        top_part_layout.setContentsMargins(0, 0, 0, 5)\n        top_part_layout.addWidget(button_toggle)\n        top_part_layout.addWidget(label_widget)\n        top_part_layout.addStretch(1)\n\n        label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        self.button_toggle = button_toggle\n        self.label_widget = label_widget\n\n        top_part.clicked.connect(self._top_part_clicked)\n        self.button_toggle.clicked.connect(self._btn_clicked)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n        main_layout.setAlignment(QtCore.Qt.AlignTop)\n        main_layout.addWidget(top_part)\n\n        self.main_layout = main_layout\n\n    def set_content_widget(self, content_widget):\n        content_widget.setVisible(self.button_toggle.isChecked())\n        self.main_layout.addWidget(content_widget)\n        self.content_widget = content_widget\n\n    def _btn_clicked(self):\n        self.toggle_content(self.button_toggle.isChecked())\n\n    def _top_part_clicked(self):\n        self.toggle_content()\n\n    def toggle_content(self, *args):\n        if len(args) > 0:\n            checked = args[0]\n        else:\n            checked = not self.button_toggle.isChecked()\n        arrow_type = QtCore.Qt.RightArrow\n        if checked:\n            arrow_type = QtCore.Qt.DownArrow\n        self.button_toggle.setChecked(checked)\n        self.button_toggle.setArrowType(arrow_type)\n        if self.content_widget:\n            self.content_widget.setVisible(checked)\n        self.parent().updateGeometry()\n\n    def resizeEvent(self, event):\n        super(CollapsibleWidget, self).resizeEvent(event)\n        if self.content_widget:\n            self.content_widget.updateGeometry()\n\n\nclass PypeInfoWidget(QtWidgets.QWidget):\n    _resized = QtCore.Signal()\n\n    def __init__(self, parent=None):\n        super(PypeInfoWidget, self).__init__(parent)\n\n        self._scroll_at_bottom = False\n\n        self.setStyleSheet(style.load_stylesheet())\n\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n        self.setWindowTitle(\n            \"{} info\".format(\"AYON\" if AYON_SERVER_ENABLED else \"OpenPype\")\n        )\n\n        scroll_area = QtWidgets.QScrollArea(self)\n        info_widget = PypeInfoSubWidget(scroll_area)\n\n        scroll_area.setWidget(info_widget)\n        scroll_area.setWidgetResizable(True)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(scroll_area, 1)\n        main_layout.addWidget(self._create_btns_section(), 0)\n\n        scroll_area.verticalScrollBar().valueChanged.connect(\n            self._on_area_scroll\n        )\n        self._resized.connect(self._on_resize)\n        self.resize(740, 540)\n\n        self.scroll_area = scroll_area\n        self.info_widget = info_widget\n\n    def _on_area_scroll(self, value):\n        vertical_bar = self.scroll_area.verticalScrollBar()\n        self._scroll_at_bottom = vertical_bar.maximum() == vertical_bar.value()\n        self.info_widget.set_scroll_enabled(self._scroll_at_bottom)\n\n    def _on_resize(self):\n        if not self._scroll_at_bottom:\n            return\n        vertical_bar = self.scroll_area.verticalScrollBar()\n        vertical_bar.setValue(vertical_bar.maximum())\n\n    def resizeEvent(self, event):\n        super(PypeInfoWidget, self).resizeEvent(event)\n        self._resized.emit()\n        self.info_widget.set_content_height(\n            self.scroll_area.height()\n        )\n\n    def showEvent(self, event):\n        super(PypeInfoWidget, self).showEvent(event)\n        self.info_widget.set_content_height(\n            self.scroll_area.height()\n        )\n\n    def _create_btns_section(self):\n        btns_widget = QtWidgets.QWidget(self)\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n\n        copy_to_clipboard_btn = QtWidgets.QPushButton(\n            \"Copy to clipboard\", btns_widget\n        )\n        export_to_file_btn = QtWidgets.QPushButton(\n            \"Export\", btns_widget\n        )\n        btns_layout.addWidget(QtWidgets.QWidget(btns_widget), 1)\n        btns_layout.addWidget(copy_to_clipboard_btn)\n        btns_layout.addWidget(export_to_file_btn)\n\n        copy_to_clipboard_btn.clicked.connect(self._on_copy_to_clipboard)\n        export_to_file_btn.clicked.connect(self._on_export_to_file)\n\n        return btns_widget\n\n    def _on_export_to_file(self):\n        dst_dir_path = QtWidgets.QFileDialog.getExistingDirectory(\n            self,\n            \"Choose directory\",\n            os.path.expanduser(\"~\"),\n            QtWidgets.QFileDialog.ShowDirsOnly\n        )\n        if not dst_dir_path or not os.path.exists(dst_dir_path):\n            return\n\n        filepath = extract_pype_info_to_file(dst_dir_path)\n        title = \"Extraction done\"\n        message = \"Extraction is done. Destination filepath is \\\"{}\\\"\".format(\n            filepath.replace(\"\\\\\", \"/\")\n        )\n        dialog = QtWidgets.QMessageBox(self)\n        dialog.setIcon(QtWidgets.QMessageBox.NoIcon)\n        dialog.setWindowTitle(title)\n        dialog.setText(message)\n        dialog.exec_()\n\n    def _on_copy_to_clipboard(self):\n        all_data = get_all_current_info()\n        all_data_str = json.dumps(all_data, indent=4)\n\n        mime_data = QtCore.QMimeData()\n        mime_data.setText(all_data_str)\n        QtWidgets.QApplication.instance().clipboard().setMimeData(\n            mime_data\n        )\n\n\nclass PypeInfoSubWidget(QtWidgets.QWidget):\n    not_applicable = \"N/A\"\n\n    def __init__(self, parent=None):\n        super(PypeInfoSubWidget, self).__init__(parent)\n\n        self.env_view = None\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setAlignment(QtCore.Qt.AlignTop)\n        main_layout.addWidget(self._create_openpype_info_widget(), 0)\n        main_layout.addWidget(self._create_separator(), 0)\n        main_layout.addWidget(self._create_workstation_widget(), 0)\n        if not AYON_SERVER_ENABLED:\n            main_layout.addWidget(self._create_separator(), 0)\n            main_layout.addWidget(self._create_local_settings_widget(), 0)\n        main_layout.addWidget(self._create_separator(), 0)\n        main_layout.addWidget(self._create_environ_widget(), 1)\n\n    def set_content_height(self, height):\n        if self.env_view:\n            self.env_view.setMinimumHeight(height)\n\n    def set_scroll_enabled(self, value):\n        if self.env_view:\n            self.env_view.set_scroll_enabled(value)\n\n    def _create_separator(self):\n        separator_widget = QtWidgets.QWidget(self)\n        separator_widget.setObjectName(\"Separator\")\n        separator_widget.setMinimumHeight(2)\n        separator_widget.setMaximumHeight(2)\n        return separator_widget\n\n    def _create_workstation_widget(self):\n        key_label_mapping = {\n            \"system_name\": \"System:\",\n            \"local_id\": \"Local ID:\",\n            \"username\": \"Username:\",\n            \"hostname\": \"Hostname:\",\n            \"hostip\": \"Host IP:\"\n        }\n        keys_order = [\n            \"system_name\",\n            \"local_id\",\n            \"username\",\n            \"hostname\",\n            \"hostip\"\n        ]\n        workstation_info = get_workstation_info()\n        for key in workstation_info.keys():\n            if key not in keys_order:\n                keys_order.append(key)\n\n        wokstation_info_widget = CollapsibleWidget(\"Workstation info\", self)\n\n        info_widget = QtWidgets.QWidget(self)\n        info_layout = QtWidgets.QGridLayout(info_widget)\n        # Add spacer to 3rd column\n        info_layout.addWidget(QtWidgets.QWidget(info_widget), 0, 2)\n        info_layout.setColumnStretch(2, 1)\n\n        for key in keys_order:\n            if key not in workstation_info:\n                continue\n\n            label = key_label_mapping.get(key, key)\n            value = workstation_info[key]\n            row = info_layout.rowCount()\n            info_layout.addWidget(\n                QtWidgets.QLabel(label), row, 0, 1, 1\n            )\n            value_label = QtWidgets.QLabel(value)\n            value_label.setTextInteractionFlags(\n                QtCore.Qt.TextSelectableByMouse\n            )\n            info_layout.addWidget(\n                value_label, row, 1, 1, 1\n            )\n\n        wokstation_info_widget.set_content_widget(info_widget)\n        wokstation_info_widget.toggle_content()\n\n        return wokstation_info_widget\n\n    def _create_local_settings_widget(self):\n        local_settings = get_local_settings()\n\n        local_settings_widget = CollapsibleWidget(\"Local settings\", self)\n\n        settings_input = QtWidgets.QPlainTextEdit(local_settings_widget)\n        settings_input.setReadOnly(True)\n        settings_input.setPlainText(json.dumps(local_settings, indent=4))\n\n        local_settings_widget.set_content_widget(settings_input)\n\n        return local_settings_widget\n\n    def _create_environ_widget(self):\n        env_widget = CollapsibleWidget(\"Environments\", self)\n\n        env_view = EnvironmentsView(env_widget)\n        env_view.setMinimumHeight(300)\n        env_widget.set_content_widget(env_view)\n\n        self.env_view = env_view\n\n        return env_widget\n\n    def _create_openpype_info_widget(self):\n        \"\"\"Create widget with information about OpenPype application.\"\"\"\n\n        if AYON_SERVER_ENABLED:\n            executable_args = get_openpype_execute_args()\n            username = \"N/A\"\n            user_info = ayon_api.get_user()\n            if user_info:\n                username = user_info.get(\"name\") or username\n                full_name = user_info.get(\"attrib\", {}).get(\"fullName\")\n                if full_name:\n                    username = \"{} ({})\".format(full_name, username)\n            info_values = {\n                \"executable\": executable_args[-1],\n                \"server_url\": os.environ[\"AYON_SERVER_URL\"],\n                \"bundle_name\": os.environ[\"AYON_BUNDLE_NAME\"],\n                \"username\": username\n            }\n            key_label_mapping = {\n                \"executable\": \"AYON Executable:\",\n                \"server_url\": \"AYON Server:\",\n                \"bundle_name\": \"AYON Bundle:\",\n                \"username\": \"AYON Username:\"\n            }\n            # Prepare keys order\n            keys_order = [\n                \"server_url\",\n                \"bundle_name\",\n                \"username\",\n                \"executable\",\n            ]\n\n        else:\n            # Get pype info data\n            info_values = get_openpype_info()\n            # Modify version key/values\n            version_value = \"{} ({})\".format(\n                info_values.pop(\"version\", self.not_applicable),\n                info_values.pop(\"version_type\", self.not_applicable)\n            )\n            info_values[\"version_value\"] = version_value\n            # Prepare label mapping\n            key_label_mapping = {\n                \"version_value\": \"Running version:\",\n                \"build_verison\": \"Build version:\",\n                \"executable\": \"OpenPype executable:\",\n                \"pype_root\": \"OpenPype location:\",\n                \"mongo_url\": \"OpenPype Mongo URL:\"\n            }\n            # Prepare keys order\n            keys_order = [\n                \"version_value\",\n                \"build_verison\",\n                \"executable\",\n                \"pype_root\",\n                \"mongo_url\"\n            ]\n\n        for key in info_values.keys():\n            if key not in keys_order:\n                keys_order.append(key)\n\n        # Create widgets\n        info_widget = QtWidgets.QWidget(self)\n        info_layout = QtWidgets.QGridLayout(info_widget)\n        # Add spacer to 3rd column\n        info_layout.addWidget(QtWidgets.QWidget(info_widget), 0, 2)\n        info_layout.setColumnStretch(2, 1)\n\n        title_label = QtWidgets.QLabel(info_widget)\n        title_label.setText(\"Application information\")\n        title_label.setStyleSheet(\"font-weight: bold;\")\n        info_layout.addWidget(title_label, 0, 0, 1, 2)\n\n        for key in keys_order:\n            if key not in info_values:\n                continue\n            value = info_values[key]\n            label = key_label_mapping.get(key, key)\n            row = info_layout.rowCount()\n            info_layout.addWidget(\n                QtWidgets.QLabel(label), row, 0, 1, 1\n            )\n            value_label = QtWidgets.QLabel(value)\n            value_label.setTextInteractionFlags(\n                QtCore.Qt.TextSelectableByMouse\n            )\n            info_layout.addWidget(\n                value_label, row, 1, 1, 1\n            )\n        if AYON_SERVER_ENABLED:\n            row = info_layout.rowCount()\n            info_layout.addWidget(\n                QtWidgets.QLabel(\"OpenPype Addon:\"), row, 0, 1, 1\n            )\n            value_label = QtWidgets.QLabel(openpype.version.__version__)\n            value_label.setTextInteractionFlags(\n                QtCore.Qt.TextSelectableByMouse\n            )\n            info_layout.addWidget(\n                value_label, row, 1, 1, 1\n            )\n        return info_widget\n"
  },
  {
    "path": "openpype/tools/tray/pype_tray.py",
    "content": "import collections\nimport os\nimport sys\nimport atexit\n\nimport platform\n\nfrom qtpy import QtCore, QtGui, QtWidgets\n\nimport openpype.version\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype import resources, style\nfrom openpype.lib import (\n    Logger,\n    get_openpype_execute_args,\n    run_detached_process,\n)\nfrom openpype.lib.openpype_version import (\n    op_version_control_available,\n    get_expected_version,\n    get_installed_version,\n    is_current_version_studio_latest,\n    is_current_version_higher_than_expected,\n    is_running_from_build,\n    get_openpype_version,\n    is_running_staging,\n    is_staging_enabled,\n)\nfrom openpype.modules import TrayModulesManager\nfrom openpype.settings import (\n    get_system_settings,\n    SystemSettings,\n    ProjectSettings,\n    DefaultsNotDefined\n)\nfrom openpype.tools.utils import (\n    WrappedCallbackItem,\n    paint_image_with_color,\n    get_warning_pixmap,\n    get_openpype_qt_app,\n)\n\nfrom .pype_info_widget import PypeInfoWidget\n\n\n# TODO PixmapLabel should be moved to 'utils' in other future PR so should be\n#   imported from there\nclass PixmapLabel(QtWidgets.QLabel):\n    \"\"\"Label resizing image to height of font.\"\"\"\n    def __init__(self, pixmap, parent):\n        super(PixmapLabel, self).__init__(parent)\n        self._empty_pixmap = QtGui.QPixmap(0, 0)\n        self._source_pixmap = pixmap\n\n    def set_source_pixmap(self, pixmap):\n        \"\"\"Change source image.\"\"\"\n        self._source_pixmap = pixmap\n        self._set_resized_pix()\n\n    def _get_pix_size(self):\n        size = self.fontMetrics().height() * 3\n        return size, size\n\n    def _set_resized_pix(self):\n        if self._source_pixmap is None:\n            self.setPixmap(self._empty_pixmap)\n            return\n        width, height = self._get_pix_size()\n        self.setPixmap(\n            self._source_pixmap.scaled(\n                width,\n                height,\n                QtCore.Qt.KeepAspectRatio,\n                QtCore.Qt.SmoothTransformation\n            )\n        )\n\n    def resizeEvent(self, event):\n        self._set_resized_pix()\n        super(PixmapLabel, self).resizeEvent(event)\n\n\nclass VersionUpdateDialog(QtWidgets.QDialog):\n    restart_requested = QtCore.Signal()\n    ignore_requested = QtCore.Signal()\n\n    _min_width = 400\n    _min_height = 130\n\n    def __init__(self, parent=None):\n        super(VersionUpdateDialog, self).__init__(parent)\n\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n        self.setWindowFlags(\n            self.windowFlags()\n            | QtCore.Qt.WindowStaysOnTopHint\n        )\n\n        self.setMinimumWidth(self._min_width)\n        self.setMinimumHeight(self._min_height)\n\n        top_widget = QtWidgets.QWidget(self)\n\n        gift_pixmap = self._get_gift_pixmap()\n        gift_icon_label = PixmapLabel(gift_pixmap, top_widget)\n\n        label_widget = QtWidgets.QLabel(top_widget)\n        label_widget.setWordWrap(True)\n\n        top_layout = QtWidgets.QHBoxLayout(top_widget)\n        top_layout.setSpacing(10)\n        top_layout.addWidget(gift_icon_label, 0, QtCore.Qt.AlignCenter)\n        top_layout.addWidget(label_widget, 1)\n\n        ignore_btn = QtWidgets.QPushButton(self)\n        restart_btn = QtWidgets.QPushButton(self)\n        restart_btn.setObjectName(\"TrayRestartButton\")\n\n        btns_layout = QtWidgets.QHBoxLayout()\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(ignore_btn, 0)\n        btns_layout.addWidget(restart_btn, 0)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(top_widget, 0)\n        layout.addStretch(1)\n        layout.addLayout(btns_layout, 0)\n\n        ignore_btn.clicked.connect(self._on_ignore)\n        restart_btn.clicked.connect(self._on_reset)\n\n        self._label_widget = label_widget\n        self._gift_icon_label = gift_icon_label\n        self._ignore_btn = ignore_btn\n        self._restart_btn = restart_btn\n\n        self._restart_accepted = False\n        self._current_is_higher = False\n\n        self.setStyleSheet(style.load_stylesheet())\n\n    def _get_gift_pixmap(self):\n        image_path = os.path.join(\n            os.path.dirname(os.path.abspath(__file__)),\n            \"images\",\n            \"gifts.png\"\n        )\n        src_image = QtGui.QImage(image_path)\n        color_value = style.get_objected_colors(\"font\")\n\n        return paint_image_with_color(\n            src_image,\n            color_value.get_qcolor()\n        )\n\n    def showEvent(self, event):\n        super(VersionUpdateDialog, self).showEvent(event)\n        self._restart_accepted = False\n\n    def closeEvent(self, event):\n        super(VersionUpdateDialog, self).closeEvent(event)\n        if self._restart_accepted or self._current_is_higher:\n            return\n        # Trigger ignore requested only if restart was not clicked and current\n        #   version is lower\n        self.ignore_requested.emit()\n\n    def update_versions(\n        self, current_version, expected_version, current_is_higher\n    ):\n        if not current_is_higher:\n            title = \"OpenPype update is needed\"\n            label_message = (\n                \"Running OpenPype version is <b>{}</b>.\"\n                \" Your production has been updated to version <b>{}</b>.\"\n            ).format(str(current_version), str(expected_version))\n            ignore_label = \"Later\"\n            restart_label = \"Restart && Update\"\n        else:\n            title = \"OpenPype version is higher\"\n            label_message = (\n                \"Running OpenPype version is <b>{}</b>.\"\n                \" Your production uses version <b>{}</b>.\"\n            ).format(str(current_version), str(expected_version))\n            ignore_label = \"Ignore\"\n            restart_label = \"Restart && Change\"\n\n        self.setWindowTitle(title)\n\n        self._current_is_higher = current_is_higher\n\n        self._gift_icon_label.setVisible(not current_is_higher)\n\n        self._label_widget.setText(label_message)\n        self._ignore_btn.setText(ignore_label)\n        self._restart_btn.setText(restart_label)\n\n    def _on_ignore(self):\n        self.reject()\n\n    def _on_reset(self):\n        self._restart_accepted = True\n        self.restart_requested.emit()\n        self.accept()\n\n\nclass ProductionStagingDialog(QtWidgets.QDialog):\n    \"\"\"Tell user that he has enabled staging but is in production version.\n\n    This is showed only when staging is enabled with '--use-staging' and it's\n    version is the same as production's version.\n    \"\"\"\n\n    def __init__(self, parent=None):\n        super(ProductionStagingDialog, self).__init__(parent)\n\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n        self.setWindowTitle(\"Production and Staging versions are the same\")\n        self.setWindowFlags(\n            self.windowFlags()\n            | QtCore.Qt.WindowStaysOnTopHint\n        )\n\n        top_widget = QtWidgets.QWidget(self)\n\n        staging_pixmap = QtGui.QPixmap(\n            resources.get_openpype_staging_icon_filepath()\n        )\n        staging_icon_label = PixmapLabel(staging_pixmap, top_widget)\n        message = (\n            \"Because production and staging versions are the same\"\n            \" your changes and work will affect both.\"\n        )\n        content_label = QtWidgets.QLabel(message, self)\n        content_label.setWordWrap(True)\n\n        top_layout = QtWidgets.QHBoxLayout(top_widget)\n        top_layout.setContentsMargins(0, 0, 0, 0)\n        top_layout.setSpacing(10)\n        top_layout.addWidget(\n            staging_icon_label, 0,\n            QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter\n        )\n        top_layout.addWidget(content_label, 1)\n\n        footer_widget = QtWidgets.QWidget(self)\n        ok_btn = QtWidgets.QPushButton(\"I understand\", footer_widget)\n\n        footer_layout = QtWidgets.QHBoxLayout(footer_widget)\n        footer_layout.setContentsMargins(0, 0, 0, 0)\n        footer_layout.addStretch(1)\n        footer_layout.addWidget(ok_btn)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(top_widget, 0)\n        main_layout.addStretch(1)\n        main_layout.addWidget(footer_widget, 0)\n\n        self.setStyleSheet(style.load_stylesheet())\n        self.resize(400, 140)\n\n        ok_btn.clicked.connect(self._on_ok_clicked)\n\n    def _on_ok_clicked(self):\n        self.close()\n\n\nclass BuildVersionDialog(QtWidgets.QDialog):\n    \"\"\"Build/Installation version is too low for current OpenPype version.\n\n    This dialog tells to user that it's build OpenPype is too old.\n    \"\"\"\n    def __init__(self, parent=None):\n        super(BuildVersionDialog, self).__init__(parent)\n\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n        self.setWindowTitle(\"Outdated OpenPype installation\")\n        self.setWindowFlags(\n            self.windowFlags()\n            | QtCore.Qt.WindowStaysOnTopHint\n        )\n\n        top_widget = QtWidgets.QWidget(self)\n\n        warning_pixmap = get_warning_pixmap()\n        warning_icon_label = PixmapLabel(warning_pixmap, top_widget)\n\n        message = (\n            \"Your installation of OpenPype <b>does not match minimum\"\n            \" requirements</b>.<br/><br/>Please update OpenPype installation\"\n            \" to newer version.\"\n        )\n        content_label = QtWidgets.QLabel(message, self)\n\n        top_layout = QtWidgets.QHBoxLayout(top_widget)\n        top_layout.setContentsMargins(0, 0, 0, 0)\n        top_layout.addWidget(\n            warning_icon_label, 0,\n            QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter\n        )\n        top_layout.addWidget(content_label, 1)\n\n        footer_widget = QtWidgets.QWidget(self)\n        ok_btn = QtWidgets.QPushButton(\"I understand\", footer_widget)\n\n        footer_layout = QtWidgets.QHBoxLayout(footer_widget)\n        footer_layout.setContentsMargins(0, 0, 0, 0)\n        footer_layout.addStretch(1)\n        footer_layout.addWidget(ok_btn)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(top_widget, 0)\n        main_layout.addStretch(1)\n        main_layout.addWidget(footer_widget, 0)\n\n        self.setStyleSheet(style.load_stylesheet())\n\n        ok_btn.clicked.connect(self._on_ok_clicked)\n\n    def _on_ok_clicked(self):\n        self.close()\n\n\nclass TrayManager:\n    \"\"\"Cares about context of application.\n\n    Load submenus, actions, separators and modules into tray's context.\n    \"\"\"\n    def __init__(self, tray_widget, main_window):\n        self.tray_widget = tray_widget\n        self.main_window = main_window\n        self.pype_info_widget = None\n        self._restart_action = None\n\n        self.log = Logger.get_logger(self.__class__.__name__)\n\n        system_settings = get_system_settings()\n        self.module_settings = system_settings[\"modules\"]\n\n        version_check_interval = system_settings[\"general\"].get(\n            \"version_check_interval\"\n        )\n        if version_check_interval is None:\n            version_check_interval = 5\n        self._version_check_interval = version_check_interval * 60 * 1000\n\n        self.modules_manager = TrayModulesManager()\n\n        self.errors = []\n\n        self._version_check_timer = None\n        self._version_dialog = None\n\n        self.main_thread_timer = None\n        self._main_thread_callbacks = collections.deque()\n        self._execution_in_progress = None\n\n    @property\n    def doubleclick_callback(self):\n        \"\"\"Double-click callback for Tray icon.\"\"\"\n        callback_name = self.modules_manager.doubleclick_callback\n        return self.modules_manager.doubleclick_callbacks.get(callback_name)\n\n    def execute_doubleclick(self):\n        \"\"\"Execute double click callback in main thread.\"\"\"\n        callback = self.doubleclick_callback\n        if callback:\n            self.execute_in_main_thread(callback)\n\n    def _on_version_check_timer(self):\n        # Check if is running from build and stop future validations if yes\n        if not is_running_from_build() or not op_version_control_available():\n            self._version_check_timer.stop()\n            return\n\n        self.validate_openpype_version()\n\n    def validate_openpype_version(self):\n        using_requested = is_current_version_studio_latest()\n        # TODO Handle situations when version can't be detected\n        if using_requested is None:\n            using_requested = True\n\n        self._restart_action.setVisible(not using_requested)\n        if using_requested:\n            if (\n                self._version_dialog is not None\n                and self._version_dialog.isVisible()\n            ):\n                self._version_dialog.close()\n            return\n\n        installed_version = get_installed_version()\n        expected_version = get_expected_version()\n\n        # Request new build if is needed\n        if (\n            # Backwards compatibility\n            not hasattr(expected_version, \"is_compatible\")\n            or not expected_version.is_compatible(installed_version)\n        ):\n            if (\n                self._version_dialog is not None\n                and self._version_dialog.isVisible()\n            ):\n                self._version_dialog.close()\n\n            dialog = BuildVersionDialog()\n            dialog.exec_()\n            return\n\n        if self._version_dialog is None:\n            self._version_dialog = VersionUpdateDialog()\n            self._version_dialog.restart_requested.connect(\n                self._restart_and_install\n            )\n            self._version_dialog.ignore_requested.connect(\n                self._outdated_version_ignored\n            )\n\n        current_version = get_openpype_version()\n        current_is_higher = is_current_version_higher_than_expected()\n\n        self._version_dialog.update_versions(\n            current_version, expected_version, current_is_higher\n        )\n        self._version_dialog.show()\n        self._version_dialog.raise_()\n        self._version_dialog.activateWindow()\n\n    def _restart_and_install(self):\n        self.restart(use_expected_version=True)\n\n    def _outdated_version_ignored(self):\n        self.show_tray_message(\n            \"OpenPype version is outdated\",\n            (\n                \"Please update your OpenPype as soon as possible.\"\n                \" To update, restart OpenPype Tray application.\"\n            )\n        )\n\n    def execute_in_main_thread(self, callback, *args, **kwargs):\n        if isinstance(callback, WrappedCallbackItem):\n            item = callback\n        else:\n            item = WrappedCallbackItem(callback, *args, **kwargs)\n\n        self._main_thread_callbacks.append(item)\n\n        return item\n\n    def _main_thread_execution(self):\n        if self._execution_in_progress:\n            return\n        self._execution_in_progress = True\n        for _ in range(len(self._main_thread_callbacks)):\n            if self._main_thread_callbacks:\n                item = self._main_thread_callbacks.popleft()\n                item.execute()\n\n        self._execution_in_progress = False\n\n    def initialize_modules(self):\n        \"\"\"Add modules to tray.\"\"\"\n        from openpype.modules import (\n            ITrayAction,\n            ITrayService\n        )\n\n        self.modules_manager.initialize(self, self.tray_widget.menu)\n\n        admin_submenu = ITrayAction.admin_submenu(self.tray_widget.menu)\n        self.tray_widget.menu.addMenu(admin_submenu)\n\n        # Add services if they are\n        services_submenu = ITrayService.services_submenu(self.tray_widget.menu)\n        self.tray_widget.menu.addMenu(services_submenu)\n\n        # Add separator\n        self.tray_widget.menu.addSeparator()\n\n        self._add_version_item()\n\n        # Add Exit action to menu\n        exit_action = QtWidgets.QAction(\"Exit\", self.tray_widget)\n        exit_action.triggered.connect(self.tray_widget.exit)\n        self.tray_widget.menu.addAction(exit_action)\n\n        # Tell each module which modules were imported\n        self.modules_manager.start_modules()\n\n        # Print time report\n        self.modules_manager.print_report()\n\n        # create timer loop to check callback functions\n        main_thread_timer = QtCore.QTimer()\n        main_thread_timer.setInterval(300)\n        main_thread_timer.timeout.connect(self._main_thread_execution)\n        main_thread_timer.start()\n\n        self.main_thread_timer = main_thread_timer\n\n        version_check_timer = QtCore.QTimer()\n        if self._version_check_interval > 0:\n            version_check_timer.timeout.connect(self._on_version_check_timer)\n            version_check_timer.setInterval(self._version_check_interval)\n            version_check_timer.start()\n        self._version_check_timer = version_check_timer\n\n        # For storing missing settings dialog\n        self._settings_validation_dialog = None\n\n        self.execute_in_main_thread(self._startup_validations)\n\n    def _startup_validations(self):\n        \"\"\"Run possible startup validations.\"\"\"\n        # Trigger version validation on start\n        self._version_check_timer.timeout.emit()\n\n        self._validate_settings_defaults()\n\n        if not op_version_control_available():\n            dialog = BuildVersionDialog()\n            dialog.exec_()\n\n        elif is_staging_enabled() and not is_running_staging():\n            dialog = ProductionStagingDialog()\n            dialog.exec_()\n\n    def _validate_settings_defaults(self):\n        valid = True\n        try:\n            SystemSettings()\n            ProjectSettings()\n\n        except DefaultsNotDefined:\n            valid = False\n\n        if valid:\n            return\n\n        title = \"Settings miss default values\"\n        msg = (\n            \"Your OpenPype will not work as expected! \\n\"\n            \"Some default values in settings are missing. \\n\\n\"\n            \"Please contact OpenPype team.\"\n        )\n        msg_box = QtWidgets.QMessageBox(\n            QtWidgets.QMessageBox.Warning,\n            title,\n            msg,\n            QtWidgets.QMessageBox.Ok,\n            flags=QtCore.Qt.Dialog\n        )\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        msg_box.setWindowIcon(icon)\n        msg_box.setStyleSheet(style.load_stylesheet())\n        msg_box.buttonClicked.connect(self._post_validate_settings_defaults)\n\n        self._settings_validation_dialog = msg_box\n\n        msg_box.show()\n\n    def _post_validate_settings_defaults(self):\n        widget = self._settings_validation_dialog\n        self._settings_validation_dialog = None\n        widget.deleteLater()\n\n    def show_tray_message(self, title, message, icon=None, msecs=None):\n        \"\"\"Show tray message.\n\n        Args:\n            title (str): Title of message.\n            message (str): Content of message.\n            icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is\n                Information icon, may differ by Qt version.\n            msecs (int): Duration of message visibility in milliseconds.\n                Default is 10000 msecs, may differ by Qt version.\n        \"\"\"\n        args = [title, message]\n        kwargs = {}\n        if icon:\n            kwargs[\"icon\"] = icon\n        if msecs:\n            kwargs[\"msecs\"] = msecs\n\n        self.tray_widget.showMessage(*args, **kwargs)\n\n    def _add_version_item(self):\n        if AYON_SERVER_ENABLED:\n            login_action = QtWidgets.QAction(\"Login\", self.tray_widget)\n            login_action.triggered.connect(self._on_ayon_login)\n            self.tray_widget.menu.addAction(login_action)\n\n        subversion = os.environ.get(\"OPENPYPE_SUBVERSION\")\n        client_name = os.environ.get(\"OPENPYPE_CLIENT\")\n\n        if AYON_SERVER_ENABLED:\n            version_string = os.getenv(\"AYON_VERSION\", \"AYON Info\")\n        else:\n            version_string = openpype.version.__version__\n        if subversion:\n            version_string += \" ({})\".format(subversion)\n\n        if client_name:\n            version_string += \", {}\".format(client_name)\n\n        version_action = QtWidgets.QAction(version_string, self.tray_widget)\n        version_action.triggered.connect(self._on_version_action)\n\n        restart_action = QtWidgets.QAction(\n            \"Restart && Update\", self.tray_widget\n        )\n        restart_action.triggered.connect(self._on_restart_action)\n        restart_action.setVisible(False)\n\n        self.tray_widget.menu.addAction(version_action)\n        self.tray_widget.menu.addAction(restart_action)\n        self.tray_widget.menu.addSeparator()\n\n        self._restart_action = restart_action\n\n    def _on_ayon_login(self):\n        self.execute_in_main_thread(self._show_ayon_login)\n\n    def _show_ayon_login(self):\n        from ayon_common.connection.credentials import change_user_ui\n\n        result = change_user_ui()\n        if result.shutdown:\n            self.exit()\n\n        elif result.restart or result.token_changed:\n            # Remove environment variables from current connection\n            # - keep develop, staging, headless values\n            for key in {\n                \"AYON_SERVER_URL\",\n                \"AYON_API_KEY\",\n                \"AYON_BUNDLE_NAME\",\n            }:\n                os.environ.pop(key, None)\n            self.restart()\n\n    def _on_restart_action(self):\n        self.restart(use_expected_version=True)\n\n    def restart(self, use_expected_version=False, reset_version=False):\n        \"\"\"Restart Tray tool.\n\n        First creates new process with same argument and close current tray.\n\n        Args:\n            use_expected_version(bool): OpenPype version is set to expected\n                version.\n            reset_version(bool): OpenPype version is cleaned up so igniters\n                logic will decide which version will be used.\n        \"\"\"\n        args = get_openpype_execute_args()\n        envs = dict(os.environ.items())\n\n        # Create a copy of sys.argv\n        additional_args = list(sys.argv)\n        # Remove first argument from 'sys.argv'\n        # - when running from code the first argument is 'start.py'\n        # - when running from build the first argument is executable\n        additional_args.pop(0)\n\n        cleanup_additional_args = False\n        if use_expected_version:\n            cleanup_additional_args = True\n            expected_version = get_expected_version()\n            if expected_version is not None:\n                reset_version = False\n                envs[\"OPENPYPE_VERSION\"] = str(expected_version)\n            else:\n                # Trigger reset of version if expected version was not found\n                reset_version = True\n\n        # Pop OPENPYPE_VERSION\n        if reset_version:\n            cleanup_additional_args = True\n            envs.pop(\"OPENPYPE_VERSION\", None)\n\n        if cleanup_additional_args:\n            _additional_args = []\n            for arg in additional_args:\n                if arg == \"--use-staging\" or arg.startswith(\"--use-version\"):\n                    continue\n                _additional_args.append(arg)\n            additional_args = _additional_args\n\n        args.extend(additional_args)\n        run_detached_process(args, env=envs)\n        self.exit()\n\n    def exit(self):\n        self.tray_widget.exit()\n\n    def on_exit(self):\n        self.modules_manager.on_exit()\n\n    def _on_version_action(self):\n        if self.pype_info_widget is None:\n            self.pype_info_widget = PypeInfoWidget()\n\n        self.pype_info_widget.show()\n        self.pype_info_widget.raise_()\n        self.pype_info_widget.activateWindow()\n\n\nclass SystemTrayIcon(QtWidgets.QSystemTrayIcon):\n    \"\"\"Tray widget.\n\n    :param parent: Main widget that cares about all GUIs\n    :type parent: QtWidgets.QMainWindow\n    \"\"\"\n\n    doubleclick_time_ms = 100\n\n    def __init__(self, parent):\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n\n        super(SystemTrayIcon, self).__init__(icon, parent)\n\n        self._exited = False\n\n        # Store parent - QtWidgets.QMainWindow()\n        self.parent = parent\n\n        # Setup menu in Tray\n        self.menu = QtWidgets.QMenu()\n        self.menu.setStyleSheet(style.load_stylesheet())\n\n        # Set modules\n        self.tray_man = TrayManager(self, self.parent)\n\n        # Add menu to Context of SystemTrayIcon\n        self.setContextMenu(self.menu)\n\n        atexit.register(self.exit)\n\n        # Catch activate event for left click if not on MacOS\n        #   - MacOS has this ability by design and is harder to modify this\n        #       behavior\n        if platform.system().lower() == \"darwin\":\n            return\n\n        self.activated.connect(self.on_systray_activated)\n\n        click_timer = QtCore.QTimer()\n        click_timer.setInterval(self.doubleclick_time_ms)\n        click_timer.timeout.connect(self._click_timer_timeout)\n\n        self._click_timer = click_timer\n        self._doubleclick = False\n        self._click_pos = None\n\n        self._initializing_modules = False\n\n    @property\n    def initializing_modules(self):\n        return self._initializing_modules\n\n    def initialize_modules(self):\n        self._initializing_modules = True\n        self.tray_man.initialize_modules()\n        self._initializing_modules = False\n\n    def _click_timer_timeout(self):\n        self._click_timer.stop()\n        doubleclick = self._doubleclick\n        # Reset bool value\n        self._doubleclick = False\n        if doubleclick:\n            self.tray_man.execute_doubleclick()\n        else:\n            self._show_context_menu()\n\n    def _show_context_menu(self):\n        pos = self._click_pos\n        self._click_pos = None\n        if pos is None:\n            pos = QtGui.QCursor().pos()\n        self.contextMenu().popup(pos)\n\n    def on_systray_activated(self, reason):\n        # show contextMenu if left click\n        if reason == QtWidgets.QSystemTrayIcon.Trigger:\n            if self.tray_man.doubleclick_callback:\n                self._click_pos = QtGui.QCursor().pos()\n                self._click_timer.start()\n            else:\n                self._show_context_menu()\n\n        elif reason == QtWidgets.QSystemTrayIcon.DoubleClick:\n            self._doubleclick = True\n\n    def exit(self):\n        \"\"\" Exit whole application.\n\n        - Icon won't stay in tray after exit.\n        \"\"\"\n        if self._exited:\n            return\n        self._exited = True\n\n        self.hide()\n        self.tray_man.on_exit()\n        QtCore.QCoreApplication.exit()\n\n\nclass PypeTrayStarter(QtCore.QObject):\n    def __init__(self, app):\n        app.setQuitOnLastWindowClosed(False)\n        self._app = app\n        self._splash = None\n\n        main_window = QtWidgets.QMainWindow()\n        tray_widget = SystemTrayIcon(main_window)\n\n        start_timer = QtCore.QTimer()\n        start_timer.setInterval(100)\n        start_timer.start()\n\n        start_timer.timeout.connect(self._on_start_timer)\n\n        self._main_window = main_window\n        self._tray_widget = tray_widget\n        self._timer_counter = 0\n        self._start_timer = start_timer\n\n    def _on_start_timer(self):\n        if self._timer_counter == 0:\n            self._timer_counter += 1\n            splash = self._get_splash()\n            splash.show()\n            self._tray_widget.show()\n            # Make sure tray and splash are painted out\n            QtWidgets.QApplication.processEvents()\n\n        elif self._timer_counter == 1:\n            # Second processing of events to make sure splash is painted\n            QtWidgets.QApplication.processEvents()\n            self._timer_counter += 1\n            self._tray_widget.initialize_modules()\n\n        elif not self._tray_widget.initializing_modules:\n            splash = self._get_splash()\n            splash.hide()\n            self._start_timer.stop()\n\n    def _get_splash(self):\n        if self._splash is None:\n            self._splash = self._create_splash()\n        return self._splash\n\n    def _create_splash(self):\n        splash_pix = QtGui.QPixmap(resources.get_openpype_splash_filepath())\n        splash = QtWidgets.QSplashScreen(splash_pix)\n        splash.setMask(splash_pix.mask())\n        splash.setEnabled(False)\n        splash.setWindowFlags(\n            QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint\n        )\n        return splash\n\n\ndef main():\n    app = get_openpype_qt_app()\n\n    starter = PypeTrayStarter(app)\n\n    # TODO remove when pype.exe will have an icon\n    if os.name == \"nt\":\n        import ctypes\n        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(\n            u\"pype_tray\"\n        )\n\n    sys.exit(app.exec_())\n"
  },
  {
    "path": "openpype/tools/traypublisher/__init__.py",
    "content": "from .window import main\n\n\n__all__ = (\n    \"main\",\n)\n"
  },
  {
    "path": "openpype/tools/traypublisher/window.py",
    "content": "\"\"\"Tray publisher is extending publisher tool.\n\nAdds ability to select project using overlay widget with list of projects.\n\nTray publisher can be considered as host implementeation with creators and\npublishing plugins.\n\"\"\"\n\nimport platform\n\nfrom qtpy import QtWidgets, QtCore\nimport qtawesome\nimport appdirs\n\nfrom openpype.lib import JSONSettingRegistry\nfrom openpype.pipeline import install_host\nfrom openpype.hosts.traypublisher.api import TrayPublisherHost\nfrom openpype.tools.publisher.control_qt import QtPublisherController\nfrom openpype.tools.publisher.window import PublisherWindow\nfrom openpype.tools.utils import PlaceholderLineEdit, get_openpype_qt_app\nfrom openpype.tools.utils.constants import PROJECT_NAME_ROLE\nfrom openpype.tools.utils.models import (\n    ProjectModel,\n    ProjectSortFilterProxy\n)\n\n\nclass TrayPublisherController(QtPublisherController):\n    @property\n    def host(self):\n        return self._host\n\n    def reset_project_data_cache(self):\n        self._asset_docs_cache.reset()\n\n\nclass TrayPublisherRegistry(JSONSettingRegistry):\n    \"\"\"Class handling OpenPype general settings registry.\n\n    Attributes:\n        vendor (str): Name used for path construction.\n        product (str): Additional name used for path construction.\n\n    \"\"\"\n\n    def __init__(self):\n        self.vendor = \"pypeclub\"\n        self.product = \"openpype\"\n        name = \"tray_publisher\"\n        path = appdirs.user_data_dir(self.product, self.vendor)\n        super(TrayPublisherRegistry, self).__init__(name, path)\n\n\nclass StandaloneOverlayWidget(QtWidgets.QFrame):\n    project_selected = QtCore.Signal(str)\n\n    def __init__(self, publisher_window):\n        super(StandaloneOverlayWidget, self).__init__(publisher_window)\n        self.setObjectName(\"OverlayFrame\")\n\n        middle_frame = QtWidgets.QFrame(self)\n        middle_frame.setObjectName(\"ChooseProjectFrame\")\n\n        content_widget = QtWidgets.QWidget(middle_frame)\n\n        header_label = QtWidgets.QLabel(\"Choose project\", content_widget)\n        header_label.setObjectName(\"ChooseProjectLabel\")\n        # Create project models and view\n        projects_model = ProjectModel()\n        projects_proxy = ProjectSortFilterProxy()\n        projects_proxy.setSourceModel(projects_model)\n        projects_proxy.setFilterKeyColumn(0)\n\n        projects_view = QtWidgets.QListView(content_widget)\n        projects_view.setObjectName(\"ChooseProjectView\")\n        projects_view.setModel(projects_proxy)\n        projects_view.setEditTriggers(\n            QtWidgets.QAbstractItemView.NoEditTriggers\n        )\n\n        confirm_btn = QtWidgets.QPushButton(\"Confirm\", content_widget)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", content_widget)\n        cancel_btn.setVisible(False)\n        btns_layout = QtWidgets.QHBoxLayout()\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(cancel_btn, 0)\n        btns_layout.addWidget(confirm_btn, 0)\n\n        txt_filter = PlaceholderLineEdit(content_widget)\n        txt_filter.setPlaceholderText(\"Quick filter projects..\")\n        txt_filter.setClearButtonEnabled(True)\n        txt_filter.addAction(qtawesome.icon(\"fa.filter\", color=\"gray\"),\n                             QtWidgets.QLineEdit.LeadingPosition)\n\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.setContentsMargins(0, 0, 0, 0)\n        content_layout.setSpacing(20)\n        content_layout.addWidget(header_label, 0)\n        content_layout.addWidget(txt_filter, 0)\n        content_layout.addWidget(projects_view, 1)\n        content_layout.addLayout(btns_layout, 0)\n\n        middle_layout = QtWidgets.QHBoxLayout(middle_frame)\n        middle_layout.setContentsMargins(30, 30, 10, 10)\n        middle_layout.addWidget(content_widget)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.setContentsMargins(10, 10, 10, 10)\n        main_layout.addStretch(1)\n        main_layout.addWidget(middle_frame, 2)\n        main_layout.addStretch(1)\n\n        projects_view.doubleClicked.connect(self._on_double_click)\n        confirm_btn.clicked.connect(self._on_confirm_click)\n        cancel_btn.clicked.connect(self._on_cancel_click)\n        txt_filter.textChanged.connect(self._on_text_changed)\n\n        self._projects_view = projects_view\n        self._projects_model = projects_model\n        self._projects_proxy = projects_proxy\n        self._cancel_btn = cancel_btn\n        self._confirm_btn = confirm_btn\n        self._txt_filter = txt_filter\n\n        self._publisher_window = publisher_window\n        self._project_name = None\n\n    def showEvent(self, event):\n        self._projects_model.refresh()\n        # Sort projects after refresh\n        self._projects_proxy.sort(0)\n\n        setting_registry = TrayPublisherRegistry()\n        try:\n            project_name = setting_registry.get_item(\"project_name\")\n        except ValueError:\n            project_name = None\n\n        if project_name:\n            index = None\n            src_index = self._projects_model.find_project(project_name)\n            if src_index is not None:\n                index = self._projects_proxy.mapFromSource(src_index)\n\n            if index is not None:\n                selection_model = self._projects_view.selectionModel()\n                selection_model.select(\n                    index,\n                    QtCore.QItemSelectionModel.SelectCurrent\n                )\n                self._projects_view.setCurrentIndex(index)\n\n        self._cancel_btn.setVisible(self._project_name is not None)\n        super(StandaloneOverlayWidget, self).showEvent(event)\n\n    def _on_double_click(self):\n        self.set_selected_project()\n\n    def _on_confirm_click(self):\n        self.set_selected_project()\n\n    def _on_cancel_click(self):\n        self._set_project(self._project_name)\n\n    def _on_text_changed(self):\n        self._projects_proxy.setFilterRegularExpression(\n            self._txt_filter.text())\n\n    def set_selected_project(self):\n        index = self._projects_view.currentIndex()\n\n        project_name = index.data(PROJECT_NAME_ROLE)\n        if project_name:\n            self._set_project(project_name)\n\n    @property\n    def host(self):\n        return self._publisher_window.controller.host\n\n    def _set_project(self, project_name):\n        self._project_name = project_name\n        self.host.set_project_name(project_name)\n        self.setVisible(False)\n        self.project_selected.emit(project_name)\n\n        setting_registry = TrayPublisherRegistry()\n        setting_registry.set_item(\"project_name\", project_name)\n\n\nclass TrayPublishWindow(PublisherWindow):\n    def __init__(self, *args, **kwargs):\n        controller = TrayPublisherController()\n        super(TrayPublishWindow, self).__init__(\n            controller=controller, reset_on_show=False\n        )\n\n        flags = self.windowFlags()\n        # Disable always on top hint\n        if flags & QtCore.Qt.WindowStaysOnTopHint:\n            flags ^= QtCore.Qt.WindowStaysOnTopHint\n\n        self.setWindowFlags(flags)\n\n        overlay_widget = StandaloneOverlayWidget(self)\n\n        btns_widget = self._header_extra_widget\n\n        back_to_overlay_btn = QtWidgets.QPushButton(\n            \"Change project\", btns_widget\n        )\n        save_btn = QtWidgets.QPushButton(\"Save\", btns_widget)\n        # TODO implement save mechanism of tray publisher\n        save_btn.setVisible(False)\n\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n\n        btns_layout.addWidget(save_btn, 0)\n        btns_layout.addWidget(back_to_overlay_btn, 0)\n\n        overlay_widget.project_selected.connect(self._on_project_select)\n        back_to_overlay_btn.clicked.connect(self._on_back_to_overlay)\n        save_btn.clicked.connect(self._on_tray_publish_save)\n\n        self._back_to_overlay_btn = back_to_overlay_btn\n        self._overlay_widget = overlay_widget\n\n    def _set_publish_frame_visible(self, publish_frame_visible):\n        super(TrayPublishWindow, self)._set_publish_frame_visible(\n            publish_frame_visible\n        )\n        self._back_to_overlay_btn.setVisible(not publish_frame_visible)\n\n    def _on_back_to_overlay(self):\n        self._overlay_widget.setVisible(True)\n        self._resize_overlay()\n\n    def _resize_overlay(self):\n        self._overlay_widget.resize(\n            self.width(),\n            self.height()\n        )\n\n    def resizeEvent(self, event):\n        super(TrayPublishWindow, self).resizeEvent(event)\n        self._resize_overlay()\n\n    def _on_project_select(self, project_name):\n        # TODO register project specific plugin paths\n        self._controller.save_changes(False)\n        self._controller.reset_project_data_cache()\n\n        self.reset()\n        if not self._controller.instances:\n            self._go_to_create_tab()\n\n    def _on_tray_publish_save(self):\n        self._controller.save_changes()\n        print(\"NOT YET IMPLEMENTED\")\n\n\ndef main():\n    host = TrayPublisherHost()\n    install_host(host)\n\n    app_instance = get_openpype_qt_app()\n\n    if platform.system().lower() == \"windows\":\n        import ctypes\n        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(\n            u\"traypublisher\"\n        )\n\n    window = TrayPublishWindow()\n    window.show()\n    app_instance.exec_()\n"
  },
  {
    "path": "openpype/tools/utils/__init__.py",
    "content": "from .layouts import FlowLayout\nfrom .widgets import (\n    FocusSpinBox,\n    FocusDoubleSpinBox,\n    ComboBox,\n    CustomTextComboBox,\n    PlaceholderLineEdit,\n    ExpandingTextEdit,\n    BaseClickableFrame,\n    ClickableFrame,\n    ClickableLabel,\n    ExpandBtn,\n    ClassicExpandBtn,\n    PixmapLabel,\n    IconButton,\n    PixmapButton,\n    SeparatorWidget,\n    VerticalExpandButton,\n    SquareButton,\n    RefreshButton,\n    GoToCurrentButton,\n)\nfrom .views import (\n    DeselectableTreeView,\n    TreeView,\n)\nfrom .error_dialog import ErrorMessageBox\nfrom .lib import (\n    WrappedCallbackItem,\n    paint_image_with_color,\n    get_warning_pixmap,\n    set_style_property,\n    DynamicQThread,\n    qt_app_context,\n    get_qt_app,\n    get_openpype_qt_app,\n    get_asset_icon,\n    get_asset_icon_by_name,\n    get_asset_icon_name_from_doc,\n    get_asset_icon_color_from_doc,\n)\n\nfrom .models import (\n    RecursiveSortFilterProxyModel,\n)\nfrom .overlay_messages import (\n    MessageOverlayObject,\n)\nfrom .multiselection_combobox import MultiSelectionComboBox\nfrom .thumbnail_paint_widget import ThumbnailPainterWidget\n\n\n__all__ = (\n    \"FlowLayout\",\n\n    \"FocusSpinBox\",\n    \"FocusDoubleSpinBox\",\n    \"ComboBox\",\n    \"CustomTextComboBox\",\n    \"PlaceholderLineEdit\",\n    \"ExpandingTextEdit\",\n    \"BaseClickableFrame\",\n    \"ClickableFrame\",\n    \"ClickableLabel\",\n    \"ExpandBtn\",\n    \"ClassicExpandBtn\",\n    \"PixmapLabel\",\n    \"IconButton\",\n    \"PixmapButton\",\n    \"SeparatorWidget\",\n\n    \"VerticalExpandButton\",\n    \"SquareButton\",\n    \"RefreshButton\",\n    \"GoToCurrentButton\",\n\n    \"DeselectableTreeView\",\n    \"TreeView\",\n\n    \"ErrorMessageBox\",\n\n    \"WrappedCallbackItem\",\n    \"paint_image_with_color\",\n    \"get_warning_pixmap\",\n    \"set_style_property\",\n    \"DynamicQThread\",\n    \"qt_app_context\",\n    \"get_openpype_qt_app\",\n    \"get_asset_icon\",\n    \"get_asset_icon_by_name\",\n    \"get_asset_icon_name_from_doc\",\n    \"get_asset_icon_color_from_doc\",\n\n    \"RecursiveSortFilterProxyModel\",\n\n    \"MessageOverlayObject\",\n\n    \"MultiSelectionComboBox\",\n\n    \"ThumbnailPainterWidget\",\n)\n"
  },
  {
    "path": "openpype/tools/utils/assets_widget.py",
    "content": "import time\nimport collections\n\nimport qtpy\nfrom qtpy import QtWidgets, QtCore, QtGui\nimport qtawesome\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.client import (\n    get_project,\n    get_assets,\n)\nfrom openpype.style import (\n    get_objected_colors,\n    get_default_tools_icon_color,\n)\nfrom openpype.tools.flickcharm import FlickCharm\n\nfrom .views import (\n    TreeViewSpinner,\n    DeselectableTreeView\n)\nfrom .widgets import PlaceholderLineEdit\nfrom .models import RecursiveSortFilterProxyModel\nfrom .lib import (\n    DynamicQThread,\n    get_asset_icon\n)\n\nif qtpy.API == \"pyside\":\n    from PySide.QtGui import QStyleOptionViewItemV4\nelif qtpy.API == \"pyqt4\":\n    from PyQt4.QtGui import QStyleOptionViewItemV4\n\nASSET_ID_ROLE = QtCore.Qt.UserRole + 1\nASSET_NAME_ROLE = QtCore.Qt.UserRole + 2\nASSET_LABEL_ROLE = QtCore.Qt.UserRole + 3\nASSET_UNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 4\nASSET_PATH_ROLE = QtCore.Qt.UserRole + 5\n\n\nclass AssetsView(TreeViewSpinner, DeselectableTreeView):\n    \"\"\"Asset items view.\n\n    Adds abilities to deselect, show loading spinner and add flick charm\n    (scroll by mouse/touchpad click and move).\n    \"\"\"\n\n    def __init__(self, parent=None):\n        super(AssetsView, self).__init__(parent)\n        self.setIndentation(15)\n        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n        self.setHeaderHidden(True)\n\n        self._flick_charm_activated = False\n        self._flick_charm = FlickCharm(parent=self)\n        self._before_flick_scroll_mode = None\n\n    def activate_flick_charm(self):\n        if self._flick_charm_activated:\n            return\n        self._flick_charm_activated = True\n        self._before_flick_scroll_mode = self.verticalScrollMode()\n        self._flick_charm.activateOn(self)\n        self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)\n\n    def deactivate_flick_charm(self):\n        if not self._flick_charm_activated:\n            return\n        self._flick_charm_activated = False\n        self._flick_charm.deactivateFrom(self)\n        if self._before_flick_scroll_mode is not None:\n            self.setVerticalScrollMode(self._before_flick_scroll_mode)\n\n    def mousePressEvent(self, event):\n        index = self.indexAt(event.pos())\n        if not index.isValid():\n            modifiers = QtWidgets.QApplication.keyboardModifiers()\n            if modifiers == QtCore.Qt.ShiftModifier:\n                return\n            elif modifiers == QtCore.Qt.ControlModifier:\n                return\n\n        super(AssetsView, self).mousePressEvent(event)\n\n    def set_loading_state(self, loading, empty):\n        \"\"\"Change loading state.\n\n        TODO: Separate into 2 individual methods.\n\n        Args:\n            loading(bool): Is loading.\n            empty(bool): Is model empty.\n        \"\"\"\n        if self.is_loading != loading:\n            if loading:\n                self.spinner.repaintNeeded.connect(\n                    self.viewport().update\n                )\n            else:\n                self.spinner.repaintNeeded.disconnect()\n                self.viewport().update()\n\n        self.is_loading = loading\n        self.is_empty = empty\n\n\nclass UnderlinesAssetDelegate(QtWidgets.QItemDelegate):\n    \"\"\"Item delegate drawing bars under asset name.\n\n    This is used in loader and library loader tools. Multiselection of assets\n    may group subsets by name under colored groups. Selected color groups are\n    then propagated back to selected assets as underlines.\n    \"\"\"\n    bar_height = 3\n\n    def __init__(self, *args, **kwargs):\n        super(UnderlinesAssetDelegate, self).__init__(*args, **kwargs)\n        asset_view_colors = get_objected_colors(\"loader\", \"asset-view\")\n        self._selected_color = (\n            asset_view_colors[\"selected\"].get_qcolor()\n        )\n        self._hover_color = (\n            asset_view_colors[\"hover\"].get_qcolor()\n        )\n        self._selected_hover_color = (\n            asset_view_colors[\"selected-hover\"].get_qcolor()\n        )\n\n    def sizeHint(self, option, index):\n        \"\"\"Add bar height to size hint.\"\"\"\n        result = super(UnderlinesAssetDelegate, self).sizeHint(option, index)\n        height = result.height()\n        result.setHeight(height + self.bar_height)\n\n        return result\n\n    def paint(self, painter, option, index):\n        \"\"\"Replicate painting of an item and draw color bars if needed.\"\"\"\n        # Qt4 compat\n        if qtpy.API in (\"pyside\", \"pyqt4\"):\n            option = QStyleOptionViewItemV4(option)\n\n        painter.save()\n\n        item_rect = QtCore.QRect(option.rect)\n        item_rect.setHeight(option.rect.height() - self.bar_height)\n\n        subset_colors = index.data(ASSET_UNDERLINE_COLORS_ROLE) or []\n        subset_colors_width = 0\n        if subset_colors:\n            subset_colors_width = option.rect.width() / len(subset_colors)\n\n        subset_rects = []\n        counter = 0\n        for subset_c in subset_colors:\n            new_color = None\n            new_rect = None\n            if subset_c:\n                new_color = QtGui.QColor(*subset_c)\n\n                new_rect = QtCore.QRect(\n                    option.rect.left() + (counter * subset_colors_width),\n                    option.rect.top() + (\n                        option.rect.height() - self.bar_height\n                    ),\n                    subset_colors_width,\n                    self.bar_height\n                )\n            subset_rects.append((new_color, new_rect))\n            counter += 1\n\n        # Background\n        if option.state & QtWidgets.QStyle.State_Selected:\n            if len(subset_colors) == 0:\n                item_rect.setTop(item_rect.top() + (self.bar_height / 2))\n\n            if option.state & QtWidgets.QStyle.State_MouseOver:\n                bg_color = self._selected_hover_color\n            else:\n                bg_color = self._selected_color\n        else:\n            item_rect.setTop(item_rect.top() + (self.bar_height / 2))\n            if option.state & QtWidgets.QStyle.State_MouseOver:\n                bg_color = self._hover_color\n            else:\n                bg_color = QtGui.QColor()\n                bg_color.setAlpha(0)\n\n        # When not needed to do a rounded corners (easier and without\n        #   painter restore):\n        painter.fillRect(\n            option.rect,\n            QtGui.QBrush(bg_color)\n        )\n\n        if option.state & QtWidgets.QStyle.State_Selected:\n            for color, subset_rect in subset_rects:\n                if not color or not subset_rect:\n                    continue\n                painter.fillRect(subset_rect, QtGui.QBrush(color))\n\n        # Icon\n        icon_index = index.model().index(\n            index.row(), index.column(), index.parent()\n        )\n        # - Default icon_rect if not icon\n        icon_rect = QtCore.QRect(\n            item_rect.left(),\n            item_rect.top(),\n            # To make sure it's same size all the time\n            option.rect.height() - self.bar_height,\n            option.rect.height() - self.bar_height\n        )\n        icon = index.model().data(icon_index, QtCore.Qt.DecorationRole)\n\n        if icon:\n            mode = QtGui.QIcon.Normal\n            if not (option.state & QtWidgets.QStyle.State_Enabled):\n                mode = QtGui.QIcon.Disabled\n            elif option.state & QtWidgets.QStyle.State_Selected:\n                mode = QtGui.QIcon.Selected\n\n            if isinstance(icon, QtGui.QPixmap):\n                icon = QtGui.QIcon(icon)\n                option.decorationSize = icon.size() / icon.devicePixelRatio()\n\n            elif isinstance(icon, QtGui.QColor):\n                pixmap = QtGui.QPixmap(option.decorationSize)\n                pixmap.fill(icon)\n                icon = QtGui.QIcon(pixmap)\n\n            elif isinstance(icon, QtGui.QImage):\n                icon = QtGui.QIcon(QtGui.QPixmap.fromImage(icon))\n                option.decorationSize = icon.size() / icon.devicePixelRatio()\n\n            elif isinstance(icon, QtGui.QIcon):\n                state = QtGui.QIcon.Off\n                if option.state & QtWidgets.QStyle.State_Open:\n                    state = QtGui.QIcon.On\n                actual_size = option.icon.actualSize(\n                    option.decorationSize, mode, state\n                )\n                option.decorationSize = QtCore.QSize(\n                    min(option.decorationSize.width(), actual_size.width()),\n                    min(option.decorationSize.height(), actual_size.height())\n                )\n\n            state = QtGui.QIcon.Off\n            if option.state & QtWidgets.QStyle.State_Open:\n                state = QtGui.QIcon.On\n\n            icon.paint(\n                painter, icon_rect,\n                QtCore.Qt.AlignLeft, mode, state\n            )\n\n        # Text\n        text_rect = QtCore.QRect(\n            icon_rect.left() + icon_rect.width() + 2,\n            item_rect.top(),\n            item_rect.width(),\n            item_rect.height()\n        )\n\n        painter.drawText(\n            text_rect, QtCore.Qt.AlignVCenter,\n            index.data(QtCore.Qt.DisplayRole)\n        )\n\n        painter.restore()\n\n\nclass AssetModel(QtGui.QStandardItemModel):\n    \"\"\"A model listing assets in the active project.\n\n    The assets are displayed in a treeview, they are visually parented by\n    a `visualParent` field in the database containing an `_id` to a parent\n    asset.\n\n    Asset document may have defined label, icon or icon color.\n\n    Loading of data for model happens in thread which means that refresh\n    is not sequential. When refresh is triggered it is required to listen for\n    'refreshed' signal.\n\n    Args:\n        dbcon (AvalonMongoDB): Ready to use connection to mongo with.\n        parent (QObject): Parent Qt object.\n    \"\"\"\n\n    _doc_fetched = QtCore.Signal()\n    refreshed = QtCore.Signal(bool)\n\n    # Asset document projection\n    _asset_projection = {\n        \"name\": 1,\n        \"parent\": 1,\n        \"data.visualParent\": 1,\n        \"data.label\": 1,\n        \"data.icon\": 1,\n        \"data.color\": 1\n    }\n\n    def __init__(self, dbcon, parent=None):\n        super(AssetModel, self).__init__(parent=parent)\n        self.dbcon = dbcon\n\n        self._refreshing = False\n        self._doc_fetching_thread = None\n        self._doc_fetching_stop = False\n        self._doc_payload = []\n\n        self._doc_fetched.connect(self._on_docs_fetched)\n\n        self._item_ids_with_color = set()\n        self._items_by_asset_id = {}\n\n        self._last_project_name = None\n\n    @property\n    def refreshing(self):\n        return self._refreshing\n\n    def get_index_by_asset_id(self, asset_id):\n        item = self._items_by_asset_id.get(asset_id)\n        if item is not None:\n            return item.index()\n        return QtCore.QModelIndex()\n\n    def get_indexes_by_asset_ids(self, asset_ids):\n        return [\n            self.get_index_by_asset_id(asset_id)\n            for asset_id in asset_ids\n        ]\n\n    def get_index_by_asset_name(self, asset_name):\n        indexes = self.get_indexes_by_asset_names([asset_name])\n        for index in indexes:\n            if index.isValid():\n                return index\n        return indexes[0]\n\n    def get_indexes_by_asset_names(self, asset_names):\n        asset_ids_by_name = {\n            asset_name: None\n            for asset_name in asset_names\n        }\n\n        for asset_id, item in self._items_by_asset_id.items():\n            asset_name = item.data(ASSET_NAME_ROLE)\n            if asset_name in asset_ids_by_name:\n                asset_ids_by_name[asset_name] = asset_id\n\n        asset_ids = [\n            asset_ids_by_name[asset_name]\n            for asset_name in asset_names\n        ]\n\n        return self.get_indexes_by_asset_ids(asset_ids)\n\n    def refresh(self, force=False):\n        \"\"\"Refresh the data for the model.\n\n        Args:\n            force (bool): Stop currently running refresh start new refresh.\n        \"\"\"\n        # Skip fetch if there is already other thread fetching documents\n        if self._refreshing:\n            if not force:\n                return\n            self.stop_refresh()\n\n        project_name = self.dbcon.Session.get(\"AVALON_PROJECT\")\n        clear_model = False\n        if project_name != self._last_project_name:\n            clear_model = True\n            self._last_project_name = project_name\n\n        if clear_model:\n            self._clear_items()\n\n        # Fetch documents from mongo\n        # Restart payload\n        self._refreshing = True\n        self._doc_payload = []\n        self._doc_fetching_thread = DynamicQThread(self._threaded_fetch)\n        self._doc_fetching_thread.start()\n\n    def stop_refresh(self):\n        self._stop_fetch_thread()\n\n    def clear_underlines(self):\n        for asset_id in set(self._item_ids_with_color):\n            self._item_ids_with_color.remove(asset_id)\n            item = self._items_by_asset_id.get(asset_id)\n            if item is not None:\n                item.setData(None, ASSET_UNDERLINE_COLORS_ROLE)\n\n    def set_underline_colors(self, colors_by_asset_id):\n        self.clear_underlines()\n\n        for asset_id, colors in colors_by_asset_id.items():\n            item = self._items_by_asset_id.get(asset_id)\n            if item is None:\n                continue\n            item.setData(colors, ASSET_UNDERLINE_COLORS_ROLE)\n            self._item_ids_with_color.add(asset_id)\n\n    def _clear_items(self):\n        root_item = self.invisibleRootItem()\n        root_item.removeRows(0, root_item.rowCount())\n        self._items_by_asset_id = {}\n        self._item_ids_with_color = set()\n\n    def _on_docs_fetched(self):\n        # Make sure refreshing did not change\n        # - since this line is refreshing sequential and\n        #   triggering of new refresh will happen when this method is done\n        if not self._refreshing:\n            self._clear_items()\n            return\n\n        self._fill_assets(self._doc_payload)\n\n        self.refreshed.emit(bool(self._items_by_asset_id))\n\n        self._stop_fetch_thread()\n\n    def _fill_assets(self, asset_docs):\n        # Collect asset documents as needed\n        asset_ids = set()\n        asset_docs_by_id = {}\n        asset_ids_by_parents = collections.defaultdict(set)\n        for asset_doc in asset_docs:\n            asset_id = asset_doc[\"_id\"]\n            asset_data = asset_doc.get(\"data\") or {}\n            parent_id = asset_data.get(\"visualParent\")\n            asset_ids.add(asset_id)\n            asset_docs_by_id[asset_id] = asset_doc\n            asset_ids_by_parents[parent_id].add(asset_id)\n\n        # Prepare removed asset ids\n        removed_asset_ids = (\n            set(self._items_by_asset_id.keys()) - set(asset_docs_by_id.keys())\n        )\n\n        # Prepare queue for adding new items\n        asset_items_queue = collections.deque()\n\n        # Queue starts with root item and 'visualParent' None\n        root_item = self.invisibleRootItem()\n        asset_items_queue.append((None, root_item))\n\n        while asset_items_queue:\n            # Get item from queue\n            parent_id, parent_item = asset_items_queue.popleft()\n            # Skip if there are no children\n            children_ids = asset_ids_by_parents[parent_id]\n\n            # Go through current children of parent item\n            # - find out items that were deleted and skip creation of already\n            #   existing items\n            for row in reversed(range(parent_item.rowCount())):\n                child_item = parent_item.child(row, 0)\n                asset_id = child_item.data(ASSET_ID_ROLE)\n                # Remove item that is not available\n                if asset_id not in children_ids:\n                    if asset_id in removed_asset_ids:\n                        # Remove and destroy row\n                        parent_item.removeRow(row)\n                    else:\n                        # Just take the row from parent without destroying\n                        parent_item.takeRow(row)\n                    continue\n\n                # Remove asset id from `children_ids` set\n                #   - is used as set for creation of \"new items\"\n                children_ids.remove(asset_id)\n                # Add existing children to queue\n                asset_items_queue.append((asset_id, child_item))\n\n            new_items = []\n            for asset_id in children_ids:\n                # Look for item in cache (maybe parent changed)\n                item = self._items_by_asset_id.get(asset_id)\n                # Create new item if was not found\n                if item is None:\n                    item = QtGui.QStandardItem()\n                    item.setEditable(False)\n                    item.setData(asset_id, ASSET_ID_ROLE)\n                    self._items_by_asset_id[asset_id] = item\n                new_items.append(item)\n                # Add item to queue\n                asset_items_queue.append((asset_id, item))\n\n            if new_items:\n                parent_item.appendRows(new_items)\n\n        # Remove cache of removed items\n        for asset_id in removed_asset_ids:\n            self._items_by_asset_id.pop(asset_id)\n\n        # Refresh data\n        # - all items refresh all data except id\n        for asset_id, item in self._items_by_asset_id.items():\n            asset_doc = asset_docs_by_id[asset_id]\n\n            asset_name = asset_doc[\"name\"]\n            if item.data(ASSET_NAME_ROLE) != asset_name:\n                item.setData(asset_name, ASSET_NAME_ROLE)\n\n            asset_data = asset_doc.get(\"data\") or {}\n            asset_label = asset_data.get(\"label\") or asset_name\n            if item.data(ASSET_LABEL_ROLE) != asset_label:\n                item.setData(asset_label, QtCore.Qt.DisplayRole)\n                item.setData(asset_label, ASSET_LABEL_ROLE)\n\n            has_children = item.rowCount() > 0\n            icon = get_asset_icon(asset_doc, has_children)\n            item.setData(icon, QtCore.Qt.DecorationRole)\n\n    def _threaded_fetch(self):\n        asset_docs = self._fetch_asset_docs()\n        if not self._refreshing:\n            return\n\n        self._doc_payload = asset_docs\n\n        # Emit doc fetched only if was not stopped\n        self._doc_fetched.emit()\n\n    def _fetch_asset_docs(self):\n        project_name = self.dbcon.current_project()\n        if not project_name:\n            return []\n\n        project_doc = get_project(project_name, fields=[\"_id\"])\n        if not project_doc:\n            return []\n\n        # Get all assets sorted by name\n        return list(\n            get_assets(project_name, fields=self._asset_projection.keys())\n        )\n\n    def _stop_fetch_thread(self):\n        self._refreshing = False\n        if self._doc_fetching_thread is not None:\n            while self._doc_fetching_thread.isRunning():\n                time.sleep(0.01)\n            self._doc_fetching_thread = None\n\n\nclass AssetsWidget(QtWidgets.QWidget):\n    \"\"\"Base widget to display a tree of assets with filter.\n\n    Assets have only one column and are sorted by name.\n\n    Refreshing of assets happens in thread so calling 'refresh' method\n    is not sequential. To capture moment when refreshing is finished listen\n    to 'refreshed' signal.\n\n    To capture selection changes listen to 'selection_changed' signal. It won't\n    send any information about new selection as it may be different based on\n    inheritance changes.\n\n    Args:\n        dbcon (AvalonMongoDB): Connection to avalon mongo db.\n        parent (QWidget): Parent Qt widget.\n    \"\"\"\n\n    # on model refresh\n    refresh_triggered = QtCore.Signal()\n    refreshed = QtCore.Signal()\n    # on view selection change\n    selection_changed = QtCore.Signal()\n    # It was double clicked on view\n    double_clicked = QtCore.Signal()\n\n    def __init__(self, dbcon, parent=None):\n        super(AssetsWidget, self).__init__(parent=parent)\n\n        self.dbcon = dbcon\n\n        # Tree View\n        model = self._create_source_model()\n        proxy = self._create_proxy_model(model)\n\n        view = AssetsView(self)\n        view.setModel(proxy)\n\n        header_widget = QtWidgets.QWidget(self)\n\n        current_asset_icon = qtawesome.icon(\n            \"fa.arrow-down\", color=get_default_tools_icon_color()\n        )\n        current_asset_btn = QtWidgets.QPushButton(header_widget)\n        current_asset_btn.setIcon(current_asset_icon)\n        current_asset_btn.setToolTip(\"Go to Asset from current Session\")\n        # Hide by default\n        current_asset_btn.setVisible(False)\n\n        refresh_icon = qtawesome.icon(\n            \"fa.refresh\", color=get_default_tools_icon_color()\n        )\n        refresh_btn = QtWidgets.QPushButton(header_widget)\n        refresh_btn.setIcon(refresh_icon)\n        refresh_btn.setToolTip(\"Refresh items\")\n\n        filter_input = PlaceholderLineEdit(header_widget)\n        filter_input.setPlaceholderText(\"Filter {}..\".format(\n            \"folders\" if AYON_SERVER_ENABLED else \"assets\"))\n\n        # Header\n        header_layout = QtWidgets.QHBoxLayout(header_widget)\n        header_layout.setContentsMargins(0, 0, 0, 0)\n        header_layout.addWidget(filter_input)\n        header_layout.addWidget(current_asset_btn)\n        header_layout.addWidget(refresh_btn)\n\n        # Make header widgets expand vertically if there is a place\n        for widget in (\n            current_asset_btn,\n            refresh_btn,\n            filter_input,\n        ):\n            size_policy = widget.sizePolicy()\n            size_policy.setVerticalPolicy(\n                QtWidgets.QSizePolicy.MinimumExpanding)\n            widget.setSizePolicy(size_policy)\n\n        # Layout\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(header_widget, 0)\n        layout.addWidget(view, 1)\n\n        # Signals/Slots\n        filter_input.textChanged.connect(self._on_filter_text_change)\n\n        selection_model = view.selectionModel()\n        selection_model.selectionChanged.connect(self._on_selection_change)\n        refresh_btn.clicked.connect(self.refresh)\n        current_asset_btn.clicked.connect(self._on_current_asset_click)\n        view.doubleClicked.connect(self.double_clicked)\n\n        self._header_widget = header_widget\n        self._filter_input = filter_input\n        self._refresh_btn = refresh_btn\n        self._current_asset_btn = current_asset_btn\n        self._model = model\n        self._proxy = proxy\n        self._view = view\n        self._last_project_name = None\n\n        self._last_btns_height = None\n\n        self.model_selection = {}\n\n    @property\n    def header_widget(self):\n        return self._header_widget\n\n    def _create_source_model(self):\n        model = AssetModel(dbcon=self.dbcon, parent=self)\n        model.refreshed.connect(self._on_model_refresh)\n        return model\n\n    def _create_proxy_model(self, source_model):\n        proxy = RecursiveSortFilterProxyModel()\n        proxy.setSourceModel(source_model)\n        proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)\n        proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n        return proxy\n\n    @property\n    def refreshing(self):\n        return self._model.refreshing\n\n    def refresh(self):\n        self._refresh_model()\n\n    def stop_refresh(self):\n        self._model.stop_refresh()\n\n    def _get_current_session_asset(self):\n        return self.dbcon.Session.get(\"AVALON_ASSET\")\n\n    def _on_current_asset_click(self):\n        \"\"\"Trigger change of asset to current context asset.\n        This separation gives ability to override this method and use it\n        in differnt way.\n        \"\"\"\n\n        self.set_current_session_asset()\n\n    def set_current_session_asset(self):\n        asset_name = self._get_current_session_asset()\n        if asset_name:\n            self.select_asset_by_name(asset_name)\n\n    def set_refresh_btn_visibility(self, visible=None):\n        \"\"\"Hide set refresh button.\n        Some tools may have their global refresh button or do not support\n        refresh at all.\n        \"\"\"\n\n        if visible is None:\n            visible = not self._refresh_btn.isVisible()\n        self._refresh_btn.setVisible(visible)\n\n    def set_current_asset_btn_visibility(self, visible=None):\n        \"\"\"Hide set current asset button.\n\n        Not all tools support using of current context asset.\n        \"\"\"\n\n        if visible is None:\n            visible = not self._current_asset_btn.isVisible()\n        self._current_asset_btn.setVisible(visible)\n\n    def select_asset(self, asset_id):\n        index = self._model.get_index_by_asset_id(asset_id)\n        new_index = self._proxy.mapFromSource(index)\n        self._select_indexes([new_index])\n\n    def select_asset_by_name(self, asset_name):\n        index = self._model.get_index_by_asset_name(asset_name)\n        new_index = self._proxy.mapFromSource(index)\n        self._select_indexes([new_index])\n\n    def activate_flick_charm(self):\n        self._view.activate_flick_charm()\n\n    def deactivate_flick_charm(self):\n        self._view.deactivate_flick_charm()\n\n    def _on_selection_change(self):\n        self.selection_changed.emit()\n\n    def _on_filter_text_change(self, new_text):\n        self._proxy.setFilterFixedString(new_text)\n\n    def _on_model_refresh(self, has_item):\n        \"\"\"This method should be triggered on model refresh.\n\n        Default implementation register this callback in '_create_source_model'\n        so if you're modifying model keep in mind that this method should be\n        called when refresh is done.\n        \"\"\"\n\n        self._proxy.sort(0)\n        self._set_loading_state(loading=False, empty=not has_item)\n        self.refreshed.emit()\n\n    def _refresh_model(self):\n        # Store selection\n        self._set_loading_state(loading=True, empty=True)\n\n        # Trigger signal before refresh is called\n        self.refresh_triggered.emit()\n        # Refresh model\n        self._model.refresh()\n\n    def _set_loading_state(self, loading, empty):\n        self._view.set_loading_state(loading, empty)\n\n    def _clear_selection(self):\n        selection_model = self._view.selectionModel()\n        selection_model.clearSelection()\n\n    def _select_indexes(self, indexes):\n        valid_indexes = [\n            index\n            for index in indexes\n            if index.isValid()\n        ]\n        if not valid_indexes:\n            return\n\n        selection_model = self._view.selectionModel()\n        selection_model.clearSelection()\n\n        mode = (\n            QtCore.QItemSelectionModel.Select\n            | QtCore.QItemSelectionModel.Rows\n        )\n        for index in valid_indexes:\n            self._view.expand(self._proxy.parent(index))\n            selection_model.select(index, mode)\n        self._view.setCurrentIndex(valid_indexes[0])\n\n\nclass SingleSelectAssetsWidget(AssetsWidget):\n    \"\"\"Single selection asset widget.\n\n    Contain single selection specific api methods.\n    \"\"\"\n\n    def get_selected_asset_id(self):\n        \"\"\"Currently selected asset id.\"\"\"\n        selection_model = self._view.selectionModel()\n        indexes = selection_model.selectedRows()\n        for index in indexes:\n            return index.data(ASSET_ID_ROLE)\n        return None\n\n    def get_selected_asset_name(self):\n        \"\"\"Currently selected asset name.\"\"\"\n        selection_model = self._view.selectionModel()\n        indexes = selection_model.selectedRows()\n        for index in indexes:\n            return index.data(ASSET_NAME_ROLE)\n        return None\n\n\nclass MultiSelectAssetsWidget(AssetsWidget):\n    \"\"\"Multiselection asset widget.\n\n    Main purpose is for loader and library loader. If another tool would use\n    multiselection assets this widget should be split and loader's logic\n    separated.\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        super(MultiSelectAssetsWidget, self).__init__(*args, **kwargs)\n        self._view.setSelectionMode(\n            QtWidgets.QAbstractItemView.ExtendedSelection\n        )\n\n        delegate = UnderlinesAssetDelegate()\n        self._view.setItemDelegate(delegate)\n        self._delegate = delegate\n\n    def get_selected_asset_ids(self):\n        \"\"\"Currently selected asset ids.\"\"\"\n        selection_model = self._view.selectionModel()\n        indexes = selection_model.selectedRows()\n        return [\n            index.data(ASSET_ID_ROLE)\n            for index in indexes\n        ]\n\n    def get_selected_asset_names(self):\n        \"\"\"Currently selected asset names.\"\"\"\n        selection_model = self._view.selectionModel()\n        indexes = selection_model.selectedRows()\n        return [\n            index.data(ASSET_NAME_ROLE)\n            for index in indexes\n        ]\n\n    def select_assets(self, asset_ids):\n        \"\"\"Select assets by their ids.\n\n        Args:\n            asset_ids (list): List of asset ids.\n        \"\"\"\n        indexes = self._model.get_indexes_by_asset_ids(asset_ids)\n        new_indexes = [\n            self._proxy.mapFromSource(index)\n            for index in indexes\n        ]\n        self._select_indexes(new_indexes)\n\n    def select_assets_by_name(self, asset_names):\n        \"\"\"Select assets by their names.\n\n        Args:\n            asset_names (list): List of asset names.\n        \"\"\"\n        indexes = self._model.get_indexes_by_asset_names(asset_names)\n        new_indexes = [\n            self._proxy.mapFromSource(index)\n            for index in indexes\n        ]\n        self._select_indexes(new_indexes)\n\n    def clear_underlines(self):\n        \"\"\"Clear underlines in asset items.\"\"\"\n        self._model.clear_underlines()\n\n        self._view.updateGeometries()\n\n    def set_underline_colors(self, colors_by_asset_id):\n        \"\"\"Change underline colors for passed assets.\n\n        Args:\n            colors_by_asset_id (dict): Key is asset id and value is list\n                of underline colors.\n        \"\"\"\n        self._model.set_underline_colors(colors_by_asset_id)\n        # Trigger repaint\n        self._view.updateGeometries()\n"
  },
  {
    "path": "openpype/tools/utils/constants.py",
    "content": "from qtpy import QtCore\n\n\nUNCHECKED_INT = getattr(QtCore.Qt.Unchecked, \"value\", 0)\nPARTIALLY_CHECKED_INT = getattr(QtCore.Qt.PartiallyChecked, \"value\", 1)\nCHECKED_INT = getattr(QtCore.Qt.Checked, \"value\", 2)\n\n# Checkbox state\ntry:\n    ITEM_IS_USER_TRISTATE = QtCore.Qt.ItemIsUserTristate\nexcept AttributeError:\n    ITEM_IS_USER_TRISTATE = QtCore.Qt.ItemIsTristate\n\nDEFAULT_PROJECT_LABEL = \"< Default >\"\nPROJECT_NAME_ROLE = QtCore.Qt.UserRole + 101\nPROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 102\n\nLOCAL_PROVIDER_ROLE = QtCore.Qt.UserRole + 500  # provider of active site\nREMOTE_PROVIDER_ROLE = QtCore.Qt.UserRole + 501  # provider of remote site\nLOCAL_PROGRESS_ROLE = QtCore.Qt.UserRole + 502   # percentage downld on active\nREMOTE_PROGRESS_ROLE = QtCore.Qt.UserRole + 503  # percentage upload on remote\nLOCAL_AVAILABILITY_ROLE = QtCore.Qt.UserRole + 504  # ratio of presence active\nREMOTE_AVAILABILITY_ROLE = QtCore.Qt.UserRole + 505\nLOCAL_DATE_ROLE = QtCore.Qt.UserRole + 506  # created_dt on active site\nREMOTE_DATE_ROLE = QtCore.Qt.UserRole + 507\nLOCAL_FAILED_ROLE = QtCore.Qt.UserRole + 508\nREMOTE_FAILED_ROLE = QtCore.Qt.UserRole + 509\nHEADER_NAME_ROLE = QtCore.Qt.UserRole + 510\nEDIT_ICON_ROLE = QtCore.Qt.UserRole + 511\nSTATUS_ROLE = QtCore.Qt.UserRole + 512\nPATH_ROLE = QtCore.Qt.UserRole + 513\nLOCAL_SITE_NAME_ROLE = QtCore.Qt.UserRole + 514\nREMOTE_SITE_NAME_ROLE = QtCore.Qt.UserRole + 515\nERROR_ROLE = QtCore.Qt.UserRole + 516\nTRIES_ROLE = QtCore.Qt.UserRole + 517\n"
  },
  {
    "path": "openpype/tools/utils/delegates.py",
    "content": "import time\nfrom datetime import datetime\nimport logging\nimport numbers\n\nfrom qtpy import QtWidgets, QtGui, QtCore\n\nfrom openpype.client import (\n    get_versions,\n    get_hero_versions,\n)\nfrom openpype.pipeline import HeroVersionType\nfrom .models import TreeModel\nfrom . import lib\n\nlog = logging.getLogger(__name__)\n\n\nclass VersionDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"A delegate that display version integer formatted as version string.\"\"\"\n\n    version_changed = QtCore.Signal()\n    first_run = False\n    lock = False\n\n    def __init__(self, dbcon, *args, **kwargs):\n        self._dbcon = dbcon\n        super(VersionDelegate, self).__init__(*args, **kwargs)\n\n    def get_project_name(self):\n        return self._dbcon.active_project()\n\n    def displayText(self, value, locale):\n        if isinstance(value, HeroVersionType):\n            return lib.format_version(value, True)\n        if not isinstance(value, numbers.Integral):\n            # For cases where no version is resolved like NOT FOUND cases\n            # where a representation might not exist in current database\n            return\n\n        return lib.format_version(value)\n\n    def paint(self, painter, option, index):\n        fg_color = index.data(QtCore.Qt.ForegroundRole)\n        if fg_color:\n            if isinstance(fg_color, QtGui.QBrush):\n                fg_color = fg_color.color()\n            elif isinstance(fg_color, QtGui.QColor):\n                pass\n            else:\n                fg_color = None\n\n        if not fg_color:\n            return super(VersionDelegate, self).paint(painter, option, index)\n\n        if option.widget:\n            style = option.widget.style()\n        else:\n            style = QtWidgets.QApplication.style()\n\n        style.drawControl(\n            QtWidgets.QStyle.CE_ItemViewItem,\n            option,\n            painter,\n            option.widget\n        )\n\n        painter.save()\n\n        text = self.displayText(\n            index.data(QtCore.Qt.DisplayRole), option.locale\n        )\n        pen = painter.pen()\n        pen.setColor(fg_color)\n        painter.setPen(pen)\n\n        text_rect = style.subElementRect(\n            QtWidgets.QStyle.SE_ItemViewItemText,\n            option\n        )\n        text_margin = style.proxy().pixelMetric(\n            QtWidgets.QStyle.PM_FocusFrameHMargin, option, option.widget\n        ) + 1\n\n        painter.drawText(\n            text_rect.adjusted(text_margin, 0, - text_margin, 0),\n            option.displayAlignment,\n            text\n        )\n\n        painter.restore()\n\n    def createEditor(self, parent, option, index):\n        item = index.data(TreeModel.ItemRole)\n        if item.get(\"isGroup\") or item.get(\"isMerged\"):\n            return\n\n        editor = QtWidgets.QComboBox(parent)\n\n        def commit_data():\n            if not self.first_run:\n                self.commitData.emit(editor)  # Update model data\n                self.version_changed.emit()   # Display model data\n        editor.currentIndexChanged.connect(commit_data)\n\n        self.first_run = True\n        self.lock = False\n\n        return editor\n\n    def setEditorData(self, editor, index):\n        if self.lock:\n            # Only set editor data once per delegation\n            return\n\n        editor.clear()\n\n        # Current value of the index\n        item = index.data(TreeModel.ItemRole)\n        value = index.data(QtCore.Qt.DisplayRole)\n        if item[\"version_document\"][\"type\"] != \"hero_version\":\n            assert isinstance(value, numbers.Integral), (\n                \"Version is not integer\"\n            )\n\n        project_name = self.get_project_name()\n        # Add all available versions to the editor\n        parent_id = item[\"version_document\"][\"parent\"]\n        version_docs = [\n            version_doc\n            for version_doc in sorted(\n                get_versions(project_name, subset_ids=[parent_id]),\n                key=lambda item: item[\"name\"]\n            )\n            if version_doc[\"data\"].get(\"active\", True)\n        ]\n\n        hero_versions = list(\n            get_hero_versions(\n                project_name,\n                subset_ids=[parent_id],\n                fields=[\"name\", \"data.tags\", \"version_id\"]\n            )\n        )\n        hero_version_doc = None\n        if hero_versions:\n            hero_version_doc = hero_versions[0]\n\n        doc_for_hero_version = None\n\n        selected = None\n        items = []\n        for version_doc in version_docs:\n            version_tags = version_doc[\"data\"].get(\"tags\") or []\n            if \"deleted\" in version_tags:\n                continue\n\n            if (\n                hero_version_doc\n                and doc_for_hero_version is None\n                and hero_version_doc[\"version_id\"] == version_doc[\"_id\"]\n            ):\n                doc_for_hero_version = version_doc\n\n            label = lib.format_version(version_doc[\"name\"])\n            item = QtGui.QStandardItem(label)\n            item.setData(version_doc, QtCore.Qt.UserRole)\n            items.append(item)\n\n            if version_doc[\"name\"] == value:\n                selected = item\n\n        if hero_version_doc and doc_for_hero_version:\n            version_name = doc_for_hero_version[\"name\"]\n            label = lib.format_version(version_name, True)\n            if isinstance(value, HeroVersionType):\n                index = len(version_docs)\n            hero_version_doc[\"name\"] = HeroVersionType(version_name)\n\n            item = QtGui.QStandardItem(label)\n            item.setData(hero_version_doc, QtCore.Qt.UserRole)\n            items.append(item)\n\n        # Reverse items so latest versions be upper\n        items = list(reversed(items))\n        for item in items:\n            editor.model().appendRow(item)\n\n        index = 0\n        if selected:\n            index = selected.row()\n\n        # Will trigger index-change signal\n        editor.setCurrentIndex(index)\n        self.first_run = False\n        self.lock = True\n\n    def setModelData(self, editor, model, index):\n        \"\"\"Apply the integer version back in the model\"\"\"\n        version = editor.itemData(editor.currentIndex())\n        model.setData(index, version[\"name\"])\n\n\ndef pretty_date(t, now=None, strftime=\"%b %d %Y %H:%M\"):\n    \"\"\"Parse datetime to readable timestamp\n\n    Within first ten seconds:\n        - \"just now\",\n    Within first minute ago:\n        - \"%S seconds ago\"\n    Within one hour ago:\n        - \"%M minutes ago\".\n    Within one day ago:\n        - \"%H:%M hours ago\"\n    Else:\n        \"%Y-%m-%d %H:%M:%S\"\n\n    \"\"\"\n\n    assert isinstance(t, datetime)\n    if now is None:\n        now = datetime.now()\n    assert isinstance(now, datetime)\n    diff = now - t\n\n    second_diff = diff.seconds\n    day_diff = diff.days\n\n    # future (consider as just now)\n    if day_diff < 0:\n        return \"just now\"\n\n    # history\n    if day_diff == 0:\n        if second_diff < 10:\n            return \"just now\"\n        if second_diff < 60:\n            return str(second_diff) + \" seconds ago\"\n        if second_diff < 120:\n            return \"a minute ago\"\n        if second_diff < 3600:\n            return str(second_diff // 60) + \" minutes ago\"\n        if second_diff < 86400:\n            minutes = (second_diff % 3600) // 60\n            hours = second_diff // 3600\n            return \"{0}:{1:02d} hours ago\".format(hours, minutes)\n\n    return t.strftime(strftime)\n\n\ndef pretty_timestamp(t, now=None):\n    \"\"\"Parse timestamp to user readable format\n\n    >>> pretty_timestamp(\"20170614T151122Z\", now=\"20170614T151123Z\")\n    'just now'\n\n    >>> pretty_timestamp(\"20170614T151122Z\", now=\"20170614T171222Z\")\n    '2:01 hours ago'\n\n    Args:\n        t (str): The time string to parse.\n        now (str, optional)\n\n    Returns:\n        str: human readable \"recent\" date.\n\n    \"\"\"\n\n    if now is not None:\n        try:\n            now = time.strptime(now, \"%Y%m%dT%H%M%SZ\")\n            now = datetime.fromtimestamp(time.mktime(now))\n        except ValueError as e:\n            log.warning(\"Can't parse 'now' time format: {0} {1}\".format(t, e))\n            return None\n\n    if isinstance(t, float):\n        dt = datetime.fromtimestamp(t)\n    else:\n        # Parse the time format as if it is `str` result from\n        # `pyblish.lib.time()` which usually is stored in Avalon database.\n        try:\n            t = time.strptime(t, \"%Y%m%dT%H%M%SZ\")\n        except ValueError as e:\n            log.warning(\"Can't parse time format: {0} {1}\".format(t, e))\n            return None\n        dt = datetime.fromtimestamp(time.mktime(t))\n\n    # prettify\n    return pretty_date(dt, now=now)\n\n\nclass PrettyTimeDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"A delegate that displays a timestamp as a pretty date.\n\n    This displays dates like `pretty_date`.\n\n    \"\"\"\n\n    def displayText(self, value, locale):\n        if value is not None:\n            return pretty_timestamp(value)\n"
  },
  {
    "path": "openpype/tools/utils/error_dialog.py",
    "content": "from qtpy import QtWidgets, QtCore\n\nfrom .widgets import ClickableFrame, ExpandBtn, SeparatorWidget\n\n\ndef escape_text_for_html(text):\n    return (\n        text\n        .replace(\"<\", \"&#60;\")\n        .replace(\">\", \"&#62;\")\n        .replace(\"\\n\", \"<br>\")\n        .replace(\" \", \"&nbsp;\")\n    )\n\n\nclass TracebackWidget(QtWidgets.QWidget):\n    def __init__(self, tb_text, parent):\n        super(TracebackWidget, self).__init__(parent)\n\n        # Modify text to match html\n        # - add more replacements when needed\n        tb_text = escape_text_for_html(tb_text)\n        expand_btn = ExpandBtn(self)\n\n        clickable_frame = ClickableFrame(self)\n        clickable_layout = QtWidgets.QHBoxLayout(clickable_frame)\n        clickable_layout.setContentsMargins(0, 0, 0, 0)\n\n        expand_label = QtWidgets.QLabel(\"Details\", clickable_frame)\n        clickable_layout.addWidget(expand_label, 0)\n        clickable_layout.addStretch(1)\n\n        show_details_layout = QtWidgets.QHBoxLayout()\n        show_details_layout.addWidget(expand_btn, 0)\n        show_details_layout.addWidget(clickable_frame, 1)\n\n        text_widget = QtWidgets.QLabel(self)\n        text_widget.setText(tb_text)\n        text_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)\n        text_widget.setVisible(False)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addLayout(show_details_layout, 0)\n        layout.addWidget(text_widget, 1)\n\n        clickable_frame.clicked.connect(self._on_show_details_click)\n        expand_btn.clicked.connect(self._on_show_details_click)\n\n        self._expand_btn = expand_btn\n        self._text_widget = text_widget\n\n    def _on_show_details_click(self):\n        self._text_widget.setVisible(not self._text_widget.isVisible())\n        self._expand_btn.set_collapsed(not self._text_widget.isVisible())\n\n\nclass ErrorMessageBox(QtWidgets.QDialog):\n    _default_width = 660\n    _default_height = 350\n\n    def __init__(self, title, parent):\n        super(ErrorMessageBox, self).__init__(parent)\n        self.setWindowTitle(title)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        top_widget = self._create_top_widget(self)\n\n        content_scroll = QtWidgets.QScrollArea(self)\n        content_scroll.setWidgetResizable(True)\n\n        content_widget = QtWidgets.QWidget(content_scroll)\n        content_scroll.setWidget(content_widget)\n\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.setContentsMargins(0, 0, 0, 0)\n\n        # Store content widget before creation of content\n        self._content_widget = content_widget\n\n        self._create_content(content_layout)\n\n        content_layout.addStretch(1)\n\n        copy_report_btn = QtWidgets.QPushButton(\"Copy report\", self)\n        ok_btn = QtWidgets.QPushButton(\"OK\", self)\n\n        footer_widget = QtWidgets.QWidget(self)\n        footer_layout = QtWidgets.QHBoxLayout(footer_widget)\n        footer_layout.setContentsMargins(0, 0, 0, 0)\n        footer_layout.addWidget(copy_report_btn, 0)\n        footer_layout.addStretch(1)\n        footer_layout.addWidget(ok_btn, 0)\n\n        bottom_line = self._create_line()\n        main_layout = QtWidgets.QVBoxLayout(self)\n        if top_widget is not None:\n            main_layout.addWidget(top_widget, 0)\n        main_layout.addWidget(content_scroll, 1)\n        main_layout.addWidget(bottom_line, 0)\n        main_layout.addWidget(footer_widget, 0)\n\n        copy_report_btn.clicked.connect(self._on_copy_report)\n        ok_btn.clicked.connect(self._on_ok_clicked)\n\n        self.resize(self._default_width, self._default_height)\n\n        report_data = self._get_report_data()\n        if not report_data:\n            copy_report_btn.setVisible(False)\n\n        self._content_scroll = content_scroll\n        self._footer_widget = footer_widget\n        self._report_data = report_data\n\n    @staticmethod\n    def convert_text_for_html(text):\n        return escape_text_for_html(text)\n\n    def _create_top_widget(self, parent_widget):\n        label_widget = QtWidgets.QLabel(parent_widget)\n        label_widget.setText(\n            \"<span style='font-size:18pt;'>Something went wrong</span>\"\n        )\n        return label_widget\n\n    def _create_content(self, content_layout):\n        raise NotImplementedError(\n            \"Method '_fill_content_layout' is not implemented!\"\n        )\n\n    def _get_report_data(self):\n        return []\n\n    def _on_ok_clicked(self):\n        self.close()\n\n    def _on_copy_report(self):\n        sep = \"\\n{}\\n\".format(10 * \"*\")\n        report_text = sep.join(self._report_data)\n\n        mime_data = QtCore.QMimeData()\n        mime_data.setText(report_text)\n        QtWidgets.QApplication.instance().clipboard().setMimeData(\n            mime_data\n        )\n\n    def _create_line(self, parent=None):\n        if parent is None:\n            parent = self\n        return SeparatorWidget(2, parent=parent)\n\n    def _create_traceback_widget(self, traceback_text, parent=None):\n        if parent is None:\n            parent = self._content_widget\n        return TracebackWidget(traceback_text, parent)\n"
  },
  {
    "path": "openpype/tools/utils/host_tools.py",
    "content": "\"\"\"Single access point to all tools usable in hosts.\n\nIt is possible to create `HostToolsHelper` in host implementation or\nuse singleton approach with global functions (using helper anyway).\n\"\"\"\nimport os\n\nimport pyblish.api\n\nfrom openpype import AYON_SERVER_ENABLED\nfrom openpype.host import IWorkfileHost, ILoadHost\nfrom openpype.lib import Logger\nfrom openpype.pipeline import (\n    registered_host,\n    get_current_asset_name,\n)\n\nfrom .lib import qt_app_context\n\n\nclass HostToolsHelper:\n    \"\"\"Create and cache tool windows in memory.\n\n    Almost all methods expect parent widget but the parent is used only on\n    first tool creation.\n\n    Class may also contain tools that are available only for one or few hosts.\n    \"\"\"\n\n    def __init__(self, parent=None):\n        self._log = None\n        # Global parent for all tools (may and may not be set)\n        self._parent = parent\n\n        # Prepare attributes for all tools\n        self._workfiles_tool = None\n        self._loader_tool = None\n        self._creator_tool = None\n        self._publisher_tool = None\n        self._subset_manager_tool = None\n        self._scene_inventory_tool = None\n        self._library_loader_tool = None\n        self._experimental_tools_dialog = None\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    def _init_ayon_workfiles_tool(self, parent):\n        from openpype.tools.ayon_workfiles.widgets import WorkfilesToolWindow\n\n        workfiles_window = WorkfilesToolWindow(parent=parent)\n        self._workfiles_tool = workfiles_window\n\n    def _init_openpype_workfiles_tool(self, parent):\n        from openpype.tools.workfiles.app import Window\n\n        # Host validation\n        host = registered_host()\n        IWorkfileHost.validate_workfile_methods(host)\n\n        workfiles_window = Window(parent=parent)\n        self._workfiles_tool = workfiles_window\n\n    def get_workfiles_tool(self, parent):\n        \"\"\"Create, cache and return workfiles tool window.\"\"\"\n        if self._workfiles_tool is None:\n            if AYON_SERVER_ENABLED:\n                self._init_ayon_workfiles_tool(parent)\n            else:\n                self._init_openpype_workfiles_tool(parent)\n\n        return self._workfiles_tool\n\n    def show_workfiles(\n        self, parent=None, use_context=None, save=None, on_top=None\n    ):\n        \"\"\"Workfiles tool for changing context and saving workfiles.\"\"\"\n\n        with qt_app_context():\n            workfiles_tool = self.get_workfiles_tool(parent)\n            workfiles_tool.ensure_visible(use_context, save, on_top)\n\n    def get_loader_tool(self, parent):\n        \"\"\"Create, cache and return loader tool window.\"\"\"\n        if self._loader_tool is None:\n            host = registered_host()\n            ILoadHost.validate_load_methods(host)\n            if AYON_SERVER_ENABLED:\n                from openpype.tools.ayon_loader.ui import LoaderWindow\n                from openpype.tools.ayon_loader import LoaderController\n\n                controller = LoaderController(host=host)\n                loader_window = LoaderWindow(\n                    controller=controller,\n                    parent=parent or self._parent\n                )\n\n            else:\n                from openpype.tools.loader import LoaderWindow\n\n                loader_window = LoaderWindow(parent=parent or self._parent)\n            self._loader_tool = loader_window\n\n        return self._loader_tool\n\n    def show_loader(self, parent=None, use_context=None):\n        \"\"\"Loader tool for loading representations.\"\"\"\n        with qt_app_context():\n            loader_tool = self.get_loader_tool(parent)\n\n            loader_tool.show()\n            loader_tool.raise_()\n            loader_tool.activateWindow()\n            loader_tool.showNormal()\n\n            if use_context is None:\n                use_context = False\n\n            if not AYON_SERVER_ENABLED and use_context:\n                context = {\"asset\": get_current_asset_name()}\n                loader_tool.set_context(context, refresh=True)\n            else:\n                loader_tool.refresh()\n\n    def get_creator_tool(self, parent):\n        \"\"\"Create, cache and return creator tool window.\"\"\"\n        if self._creator_tool is None:\n            from openpype.tools.creator import CreatorWindow\n\n            creator_window = CreatorWindow(parent=parent or self._parent)\n            self._creator_tool = creator_window\n\n        return self._creator_tool\n\n    def show_creator(self, parent=None):\n        \"\"\"Show tool to create new instantes for publishing.\"\"\"\n        with qt_app_context():\n            creator_tool = self.get_creator_tool(parent)\n            creator_tool.refresh()\n            creator_tool.show()\n\n            # Pull window to the front.\n            creator_tool.raise_()\n            creator_tool.activateWindow()\n\n    def get_subset_manager_tool(self, parent):\n        \"\"\"Create, cache and return subset manager tool window.\"\"\"\n        if self._subset_manager_tool is None:\n            from openpype.tools.subsetmanager import SubsetManagerWindow\n\n            subset_manager_window = SubsetManagerWindow(\n                parent=parent or self._parent\n            )\n            self._subset_manager_tool = subset_manager_window\n\n        return self._subset_manager_tool\n\n    def show_subset_manager(self, parent=None):\n        \"\"\"Show tool display/remove existing created instances.\"\"\"\n        with qt_app_context():\n            subset_manager_tool = self.get_subset_manager_tool(parent)\n            subset_manager_tool.show()\n\n            # Pull window to the front.\n            subset_manager_tool.raise_()\n            subset_manager_tool.activateWindow()\n\n    def get_scene_inventory_tool(self, parent):\n        \"\"\"Create, cache and return scene inventory tool window.\"\"\"\n        if self._scene_inventory_tool is None:\n            host = registered_host()\n            ILoadHost.validate_load_methods(host)\n\n            if AYON_SERVER_ENABLED:\n                from openpype.tools.ayon_sceneinventory.window import (\n                    SceneInventoryWindow)\n\n                scene_inventory_window = SceneInventoryWindow(\n                    parent=parent or self._parent\n                )\n\n            else:\n                from openpype.tools.sceneinventory import SceneInventoryWindow\n\n                scene_inventory_window = SceneInventoryWindow(\n                    parent=parent or self._parent\n                )\n            self._scene_inventory_tool = scene_inventory_window\n\n        return self._scene_inventory_tool\n\n    def show_scene_inventory(self, parent=None):\n        \"\"\"Show tool maintain loaded containers.\"\"\"\n        with qt_app_context():\n            scene_inventory_tool = self.get_scene_inventory_tool(parent)\n            scene_inventory_tool.show()\n            scene_inventory_tool.refresh()\n\n            # Pull window to the front.\n            scene_inventory_tool.raise_()\n            scene_inventory_tool.activateWindow()\n            scene_inventory_tool.showNormal()\n\n    def get_library_loader_tool(self, parent):\n        \"\"\"Create, cache and return library loader tool window.\"\"\"\n        if AYON_SERVER_ENABLED:\n            return self.get_loader_tool(parent)\n\n        if self._library_loader_tool is None:\n            from openpype.tools.libraryloader import LibraryLoaderWindow\n\n            library_window = LibraryLoaderWindow(\n                parent=parent or self._parent\n            )\n            self._library_loader_tool = library_window\n\n        return self._library_loader_tool\n\n    def show_library_loader(self, parent=None):\n        \"\"\"Loader tool for loading representations from library project.\"\"\"\n        if AYON_SERVER_ENABLED:\n            return self.show_loader(parent)\n\n        with qt_app_context():\n            library_loader_tool = self.get_library_loader_tool(parent)\n            library_loader_tool.show()\n            library_loader_tool.raise_()\n            library_loader_tool.activateWindow()\n            library_loader_tool.showNormal()\n            library_loader_tool.refresh()\n\n    def show_publish(self, parent=None):\n        \"\"\"Try showing the most desirable publish GUI\n\n        This function cycles through the currently registered\n        graphical user interfaces, if any, and presents it to\n        the user.\n        \"\"\"\n\n        pyblish_show = self._discover_pyblish_gui()\n        return pyblish_show(parent)\n\n    def _discover_pyblish_gui(self):\n        \"\"\"Return the most desirable of the currently registered GUIs\"\"\"\n        # Prefer last registered\n        guis = list(reversed(pyblish.api.registered_guis()))\n        for gui in guis:\n            try:\n                gui = __import__(gui).show\n            except (ImportError, AttributeError):\n                continue\n            else:\n                return gui\n\n        raise ImportError(\"No Pyblish GUI found\")\n\n    def get_experimental_tools_dialog(self, parent=None):\n        \"\"\"Dialog of experimental tools.\n\n        For some hosts it is not easy to modify menu of tools. For\n        those cases was added experimental tools dialog which is Qt based\n        and can dynamically filled by experimental tools so\n        host need only single \"Experimental tools\" button to see them.\n\n        Dialog can be also empty with a message that there are not available\n        experimental tools.\n        \"\"\"\n        if self._experimental_tools_dialog is None:\n            from openpype.tools.experimental_tools import (\n                ExperimentalToolsDialog\n            )\n\n            self._experimental_tools_dialog = ExperimentalToolsDialog(parent)\n        return self._experimental_tools_dialog\n\n    def show_experimental_tools_dialog(self, parent=None):\n        \"\"\"Show dialog with experimental tools.\"\"\"\n        with qt_app_context():\n            dialog = self.get_experimental_tools_dialog(parent)\n\n            dialog.show()\n            dialog.raise_()\n            dialog.activateWindow()\n            dialog.showNormal()\n\n    def get_publisher_tool(self, parent=None, controller=None):\n        \"\"\"Create, cache and return publisher window.\"\"\"\n\n        if self._publisher_tool is None:\n            from openpype.tools.publisher.window import PublisherWindow\n\n            host = registered_host()\n            ILoadHost.validate_load_methods(host)\n\n            publisher_window = PublisherWindow(\n                controller=controller,\n                parent=parent or self._parent\n            )\n            self._publisher_tool = publisher_window\n\n        return self._publisher_tool\n\n    def show_publisher_tool(self, parent=None, controller=None, tab=None):\n        with qt_app_context():\n            window = self.get_publisher_tool(parent, controller)\n            if tab:\n                window.set_current_tab(tab)\n            window.make_sure_is_visible()\n\n    def get_tool_by_name(self, tool_name, parent=None, *args, **kwargs):\n        \"\"\"Show tool by it's name.\n\n        This is helper for\n        \"\"\"\n        if tool_name == \"workfiles\":\n            return self.get_workfiles_tool(parent, *args, **kwargs)\n\n        elif tool_name == \"loader\":\n            return self.get_loader_tool(parent, *args, **kwargs)\n\n        elif tool_name == \"libraryloader\":\n            return self.get_library_loader_tool(parent, *args, **kwargs)\n\n        elif tool_name == \"creator\":\n            return self.get_creator_tool(parent, *args, **kwargs)\n\n        elif tool_name == \"subsetmanager\":\n            return self.get_subset_manager_tool(parent, *args, **kwargs)\n\n        elif tool_name == \"sceneinventory\":\n            return self.get_scene_inventory_tool(parent, *args, **kwargs)\n\n        elif tool_name == \"publish\":\n            self.log.info(\"Can't return publish tool window.\")\n\n        # \"new\" publisher\n        elif tool_name == \"publisher\":\n            return self.get_publisher_tool(parent, *args, **kwargs)\n\n        elif tool_name == \"experimental_tools\":\n            return self.get_experimental_tools_dialog(parent, *args, **kwargs)\n\n        else:\n            self.log.warning(\n                \"Can't show unknown tool name: \\\"{}\\\"\".format(tool_name)\n            )\n\n    def show_tool_by_name(self, tool_name, parent=None, *args, **kwargs):\n        \"\"\"Show tool by it's name.\n\n        This is helper for\n        \"\"\"\n        if tool_name == \"workfiles\":\n            self.show_workfiles(parent, *args, **kwargs)\n\n        elif tool_name == \"loader\":\n            self.show_loader(parent, *args, **kwargs)\n\n        elif tool_name == \"libraryloader\":\n            self.show_library_loader(parent, *args, **kwargs)\n\n        elif tool_name == \"creator\":\n            self.show_creator(parent, *args, **kwargs)\n\n        elif tool_name == \"subsetmanager\":\n            self.show_subset_manager(parent, *args, **kwargs)\n\n        elif tool_name == \"sceneinventory\":\n            self.show_scene_inventory(parent, *args, **kwargs)\n\n        elif tool_name == \"publish\":\n            self.show_publish(parent, *args, **kwargs)\n\n        elif tool_name == \"publisher\":\n            self.show_publisher_tool(parent, *args, **kwargs)\n\n        elif tool_name == \"experimental_tools\":\n            self.show_experimental_tools_dialog(parent, *args, **kwargs)\n\n        else:\n            self.log.warning(\n                \"Can't show unknown tool name: \\\"{}\\\"\".format(tool_name)\n            )\n\n\nclass _SingletonPoint:\n    \"\"\"Singleton access to host tools.\n\n    Some hosts don't have ability to create 'HostToolsHelper' object anc can\n    only register function callbacks. For those cases is created this singleton\n    point where 'HostToolsHelper' is created \"in shared memory\".\n    \"\"\"\n    helper = None\n\n    @classmethod\n    def _create_helper(cls):\n        if cls.helper is None:\n            cls.helper = HostToolsHelper()\n\n    @classmethod\n    def show_tool_by_name(cls, tool_name, parent=None, *args, **kwargs):\n        cls._create_helper()\n        cls.helper.show_tool_by_name(tool_name, parent, *args, **kwargs)\n\n    @classmethod\n    def get_tool_by_name(cls, tool_name, parent=None, *args, **kwargs):\n        cls._create_helper()\n        return cls.helper.get_tool_by_name(tool_name, parent, *args, **kwargs)\n\n\n# Function callbacks using singleton access point\ndef get_tool_by_name(tool_name, parent=None, *args, **kwargs):\n    return _SingletonPoint.get_tool_by_name(tool_name, parent, *args, **kwargs)\n\n\ndef show_tool_by_name(tool_name, parent=None, *args, **kwargs):\n    _SingletonPoint.show_tool_by_name(tool_name, parent, *args, **kwargs)\n\n\ndef show_workfiles(*args, **kwargs):\n    _SingletonPoint.show_tool_by_name(\n        \"workfiles\", *args, **kwargs\n    )\n\n\ndef show_loader(parent=None, use_context=None):\n    _SingletonPoint.show_tool_by_name(\n        \"loader\", parent, use_context=use_context\n    )\n\n\ndef show_library_loader(parent=None):\n    _SingletonPoint.show_tool_by_name(\"libraryloader\", parent)\n\n\ndef show_creator(parent=None):\n    _SingletonPoint.show_tool_by_name(\"creator\", parent)\n\n\ndef show_subset_manager(parent=None):\n    _SingletonPoint.show_tool_by_name(\"subsetmanager\", parent)\n\n\ndef show_scene_inventory(parent=None):\n    _SingletonPoint.show_tool_by_name(\"sceneinventory\", parent)\n\n\ndef show_publish(parent=None):\n    _SingletonPoint.show_tool_by_name(\"publish\", parent)\n\n\ndef show_publisher(parent=None, **kwargs):\n    _SingletonPoint.show_tool_by_name(\"publisher\", parent, **kwargs)\n\n\ndef show_experimental_tools_dialog(parent=None):\n    _SingletonPoint.show_tool_by_name(\"experimental_tools\", parent)\n\n\ndef get_pyblish_icon():\n    pyblish_dir = os.path.abspath(os.path.dirname(pyblish.api.__file__))\n    icon_path = os.path.join(pyblish_dir, \"icons\", \"logo-32x32.svg\")\n    if os.path.exists(icon_path):\n        return icon_path\n    return None\n"
  },
  {
    "path": "openpype/tools/utils/images/__init__.py",
    "content": "import os\nfrom qtpy import QtGui\n\nIMAGES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))\n\n\ndef get_image_path(filename):\n    \"\"\"Get image path from './images'.\n\n    Returns:\n        Union[str, None]: Path to image file or None if not found.\n    \"\"\"\n\n    path = os.path.join(IMAGES_DIR, filename)\n    if os.path.exists(path):\n        return path\n    return None\n\n\ndef get_image(filename):\n    \"\"\"Load image from './images' as QImage.\n\n    Returns:\n        Union[QtGui.QImage, None]: QImage or None if not found.\n    \"\"\"\n\n    path = get_image_path(filename)\n    if path:\n        return QtGui.QImage(path)\n    return None\n\n\ndef get_pixmap(filename):\n    \"\"\"Load image from './images' as QPixmap.\n\n    Returns:\n        Union[QtGui.QPixmap, None]: QPixmap or None if not found.\n    \"\"\"\n\n    path = get_image_path(filename)\n    if path:\n        return QtGui.QPixmap(path)\n    return None\n\n\ndef get_icon(filename):\n    \"\"\"Load image from './images' as QIcon.\n\n    Returns:\n        Union[QtGui.QIcon, None]: QIcon or None if not found.\n    \"\"\"\n\n    pix = get_pixmap(filename)\n    if pix:\n        return QtGui.QIcon(pix)\n    return None\n"
  },
  {
    "path": "openpype/tools/utils/layouts.py",
    "content": "from qtpy import QtWidgets, QtCore\n\n\nclass FlowLayout(QtWidgets.QLayout):\n    \"\"\"Layout that organize widgets by minimum size into a flow layout.\n\n    Layout is putting widget from left to right and top to bottom. When widget\n    can't fit a row it is added to next line. Minimum size matches widget with\n    biggest 'sizeHint' width and height using calculated geometry.\n\n    Content margins are part of calculations. It is possible to define\n    horizontal and vertical spacing.\n\n    Layout does not support stretch and spacing items.\n\n    Todos:\n        Unified width concept -> use width of largest item so all of them are\n            same. This could allow to have minimum columns option too.\n    \"\"\"\n\n    def __init__(self, parent=None):\n        super(FlowLayout, self).__init__(parent)\n\n        # spaces between each item\n        self._horizontal_spacing = 5\n        self._vertical_spacing = 5\n\n        self._items = []\n\n    def __del__(self):\n        while self.count():\n            self.takeAt(0, False)\n\n    def isEmpty(self):\n        for item in self._items:\n            if not item.isEmpty():\n                return False\n        return True\n\n    def setSpacing(self, spacing):\n        self._horizontal_spacing = spacing\n        self._vertical_spacing = spacing\n        self.invalidate()\n\n    def setHorizontalSpacing(self, spacing):\n        self._horizontal_spacing = spacing\n        self.invalidate()\n\n    def setVerticalSpacing(self, spacing):\n        self._vertical_spacing = spacing\n        self.invalidate()\n\n    def addItem(self, item):\n        self._items.append(item)\n        self.invalidate()\n\n    def count(self):\n        return len(self._items)\n\n    def itemAt(self, index):\n        if 0 <= index < len(self._items):\n            return self._items[index]\n        return None\n\n    def takeAt(self, index, invalidate=True):\n        if 0 <= index < len(self._items):\n            item = self._items.pop(index)\n            if invalidate:\n                self.invalidate()\n            return item\n        return None\n\n    def expandingDirections(self):\n        return QtCore.Qt.Orientations(QtCore.Qt.Vertical)\n\n    def hasHeightForWidth(self):\n        return True\n\n    def heightForWidth(self, width):\n        return self._setup_geometry(QtCore.QRect(0, 0, width, 0), True)\n\n    def setGeometry(self, rect):\n        super(FlowLayout, self).setGeometry(rect)\n        self._setup_geometry(rect)\n\n    def sizeHint(self):\n        return self.minimumSize()\n\n    def minimumSize(self):\n        size = QtCore.QSize(0, 0)\n        for item in self._items:\n            widget = item.widget()\n            if widget is not None:\n                parent = widget.parent()\n                if not widget.isVisibleTo(parent):\n                    continue\n            size = size.expandedTo(item.minimumSize())\n\n        if size.width() < 1 or size.height() < 1:\n            return size\n        l_margin, t_margin, r_margin, b_margin = self.getContentsMargins()\n        size += QtCore.QSize(l_margin + r_margin, t_margin + b_margin)\n        return size\n\n    def _setup_geometry(self, rect, only_calculate=False):\n        h_spacing = self._horizontal_spacing\n        v_spacing = self._vertical_spacing\n        l_margin, t_margin, r_margin, b_margin = self.getContentsMargins()\n\n        left_x = rect.x() + l_margin\n        top_y = rect.y() + t_margin\n        pos_x = left_x\n        pos_y = top_y\n        row_height = 0\n        for item in self._items:\n            item_hint = item.sizeHint()\n            item_width = item_hint.width()\n            item_height = item_hint.height()\n            if item_width < 1 or item_height < 1:\n                continue\n\n            end_x = pos_x + item_width\n\n            wrap = (\n                row_height > 0\n                and (\n                    end_x > rect.right()\n                    or (end_x + r_margin) > rect.right()\n                )\n            )\n            if not wrap:\n                next_pos_x = end_x + h_spacing\n            else:\n                pos_x = left_x\n                pos_y += row_height + v_spacing\n                next_pos_x = pos_x + item_width + h_spacing\n                row_height = 0\n\n            if not only_calculate:\n                item.setGeometry(\n                    QtCore.QRect(pos_x, pos_y, item_width, item_height)\n                )\n\n            pos_x = next_pos_x\n            row_height = max(row_height, item_height)\n\n        height = (pos_y - top_y) + row_height\n        if height > 0:\n            height += b_margin\n        return height\n"
  },
  {
    "path": "openpype/tools/utils/lib.py",
    "content": "import os\nimport sys\nimport contextlib\nimport collections\nimport traceback\n\nfrom qtpy import QtWidgets, QtCore, QtGui\nimport qtawesome\n\nfrom openpype.client import (\n    get_project,\n    get_asset_by_name,\n)\nfrom openpype.style import (\n    get_default_entity_icon_color,\n    get_objected_colors,\n    get_app_icon_path,\n)\nfrom openpype.resources import get_image_path\nfrom openpype.lib import filter_profiles, Logger\nfrom openpype.settings import get_project_settings\nfrom openpype.pipeline import (\n    registered_host,\n    get_current_context,\n    get_current_host_name,\n)\n\nfrom .constants import CHECKED_INT, UNCHECKED_INT\n\nlog = Logger.get_logger(__name__)\n\n\ndef checkstate_int_to_enum(state):\n    if not isinstance(state, int):\n        return state\n    if state == CHECKED_INT:\n        return QtCore.Qt.Checked\n\n    if state == UNCHECKED_INT:\n        return QtCore.Qt.Unchecked\n    return QtCore.Qt.PartiallyChecked\n\n\ndef checkstate_enum_to_int(state):\n    if isinstance(state, int):\n        return state\n    if state == QtCore.Qt.Checked:\n        return 0\n    if state == QtCore.Qt.PartiallyChecked:\n        return 1\n    return 2\n\n\ndef center_window(window):\n    \"\"\"Move window to center of it's screen.\"\"\"\n\n    if hasattr(QtWidgets.QApplication, \"desktop\"):\n        desktop = QtWidgets.QApplication.desktop()\n        screen_idx = desktop.screenNumber(window)\n        screen_geo = desktop.screenGeometry(screen_idx)\n    else:\n        screen = window.screen()\n        screen_geo = screen.geometry()\n\n    geo = window.frameGeometry()\n    geo.moveCenter(screen_geo.center())\n    if geo.y() < screen_geo.y():\n        geo.setY(screen_geo.y())\n    window.move(geo.topLeft())\n\n\ndef html_escape(text):\n    \"\"\"Basic escape of html syntax symbols in text.\"\"\"\n\n    return (\n        text\n        .replace(\"&\", \"&amp;\")\n        .replace(\"<\", \"&lt;\")\n        .replace(\">\", \"&gt;\")\n        .replace('\"', \"&quot;\")\n        .replace(\"'\", \"&#x27;\")\n    )\n\n\ndef set_style_property(widget, property_name, property_value):\n    \"\"\"Set widget's property that may affect style.\n\n    If current property value is different then style of widget is polished.\n    \"\"\"\n    cur_value = widget.property(property_name)\n    if cur_value == property_value:\n        return\n    widget.setProperty(property_name, property_value)\n    style = widget.style()\n    style.polish(widget)\n\n\ndef paint_image_with_color(image, color):\n    \"\"\"Redraw image with single color using it's alpha.\n\n    It is expected that input image is singlecolor image with alpha.\n\n    Args:\n        image (QImage): Loaded image with alpha.\n        color (QColor): Color that will be used to paint image.\n    \"\"\"\n    width = image.width()\n    height = image.height()\n\n    alpha_mask = image.createAlphaMask()\n    alpha_region = QtGui.QRegion(QtGui.QBitmap.fromImage(alpha_mask))\n\n    pixmap = QtGui.QPixmap(width, height)\n    pixmap.fill(QtCore.Qt.transparent)\n\n    painter = QtGui.QPainter(pixmap)\n    render_hints = (\n        QtGui.QPainter.Antialiasing\n        | QtGui.QPainter.SmoothPixmapTransform\n    )\n    # Deprecated since 5.14\n    if hasattr(QtGui.QPainter, \"HighQualityAntialiasing\"):\n        render_hints |= QtGui.QPainter.HighQualityAntialiasing\n    painter.setRenderHints(render_hints)\n\n    painter.setClipRegion(alpha_region)\n    painter.setPen(QtCore.Qt.NoPen)\n    painter.setBrush(color)\n    painter.drawRect(QtCore.QRect(0, 0, width, height))\n    painter.end()\n\n    return pixmap\n\n\ndef format_version(value, hero_version=False):\n    \"\"\"Formats integer to displayable version name\"\"\"\n    label = \"v{0:03d}\".format(value)\n    if not hero_version:\n        return label\n    return \"[{}]\".format(label)\n\n\n@contextlib.contextmanager\ndef qt_app_context():\n    app = QtWidgets.QApplication.instance()\n\n    if not app:\n        print(\"Starting new QApplication..\")\n        app = QtWidgets.QApplication(sys.argv)\n        yield app\n        app.exec_()\n    else:\n        print(\"Using existing QApplication..\")\n        yield app\n\n\ndef get_qt_app():\n    \"\"\"Get Qt application.\n\n    The function initializes new Qt application if it is not already\n    initialized. It also sets some attributes to the application to\n    ensure that it will work properly on high DPI displays.\n\n    Returns:\n        QtWidgets.QApplication: Current Qt application.\n    \"\"\"\n\n    app = QtWidgets.QApplication.instance()\n    if app is None:\n        for attr_name in (\n            \"AA_EnableHighDpiScaling\",\n            \"AA_UseHighDpiPixmaps\",\n        ):\n            attr = getattr(QtCore.Qt, attr_name, None)\n            if attr is not None:\n                QtWidgets.QApplication.setAttribute(attr)\n\n        policy = os.getenv(\"QT_SCALE_FACTOR_ROUNDING_POLICY\")\n        if (\n            hasattr(\n                QtWidgets.QApplication, \"setHighDpiScaleFactorRoundingPolicy\"\n            )\n            and not policy\n        ):\n            QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy(\n                QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough\n            )\n\n        app = QtWidgets.QApplication(sys.argv)\n\n    return app\n\n\ndef get_openpype_qt_app():\n    \"\"\"Main Qt application initialized for OpenPype processed.\n\n    This function should be used only inside OpenPype process and never inside\n        other processes.\n    \"\"\"\n\n    app = get_qt_app()\n    app.setWindowIcon(QtGui.QIcon(get_app_icon_path()))\n    return app\n\n\nclass SharedObjects:\n    jobs = {}\n    icons = {}\n\n\ndef get_qta_icon_by_name_and_color(icon_name, icon_color):\n    if not icon_name or not icon_color:\n        return None\n\n    full_icon_name = \"{0}-{1}\".format(icon_name, icon_color)\n    if full_icon_name in SharedObjects.icons:\n        return SharedObjects.icons[full_icon_name]\n\n    variants = [icon_name]\n    qta_instance = qtawesome._instance()\n    for key in qta_instance.charmap.keys():\n        variants.append(\"{0}.{1}\".format(key, icon_name))\n\n    icon = None\n    used_variant = None\n    for variant in variants:\n        try:\n            icon = qtawesome.icon(variant, color=icon_color)\n            used_variant = variant\n            break\n        except Exception:\n            pass\n\n    if used_variant is None:\n        log.info(\"Didn't find icon \\\"{}\\\"\".format(icon_name))\n\n    elif used_variant != icon_name:\n        log.debug(\"Icon \\\"{}\\\" was not found \\\"{}\\\" is used instead\".format(\n            icon_name, used_variant\n        ))\n\n    SharedObjects.icons[full_icon_name] = icon\n    return icon\n\n\ndef get_project_icon(project_doc):\n    if project_doc:\n        icon_name = project_doc.get(\"data\", {}).get(\"icon\")\n        icon = get_qta_icon_by_name_and_color(icon_name, \"white\")\n        if icon:\n            return icon\n\n    return get_qta_icon_by_name_and_color(\n        \"fa.map\", get_default_entity_icon_color()\n    )\n\n\ndef get_asset_icon_name(asset_doc, has_children=True):\n    icon_name = get_asset_icon_name_from_doc(asset_doc)\n    if icon_name:\n        return icon_name\n    return get_default_asset_icon_name(has_children)\n\n\ndef get_asset_icon_color(asset_doc):\n    icon_color = get_asset_icon_color_from_doc(asset_doc)\n    if icon_color:\n        return icon_color\n    return get_default_entity_icon_color()\n\n\ndef get_default_asset_icon_name(has_children):\n    if has_children:\n        return \"fa.folder\"\n    return \"fa.folder-o\"\n\n\ndef get_asset_icon_name_from_doc(asset_doc):\n    if asset_doc:\n        return asset_doc[\"data\"].get(\"icon\")\n    return None\n\n\ndef get_asset_icon_color_from_doc(asset_doc):\n    if asset_doc:\n        return asset_doc[\"data\"].get(\"color\")\n    return None\n\n\ndef get_asset_icon_by_name(icon_name, icon_color, has_children=False):\n    if not icon_name:\n        icon_name = get_default_asset_icon_name(has_children)\n\n    if icon_color:\n        icon_color = QtGui.QColor(icon_color)\n    else:\n        icon_color = get_default_entity_icon_color()\n    icon = get_qta_icon_by_name_and_color(icon_name, icon_color)\n    if icon is not None:\n        return icon\n    return get_qta_icon_by_name_and_color(\n        get_default_asset_icon_name(has_children),\n        icon_color\n    )\n\n\ndef get_asset_icon(asset_doc, has_children=False):\n    icon_name = get_asset_icon_name(asset_doc, has_children)\n    icon_color = get_asset_icon_color(asset_doc)\n\n    return get_qta_icon_by_name_and_color(icon_name, icon_color)\n\n\ndef get_default_task_icon(color=None):\n    if color is None:\n        color = get_default_entity_icon_color()\n    return get_qta_icon_by_name_and_color(\"fa.male\", color)\n\n\ndef get_task_icon(project_doc, asset_doc, task_name):\n    \"\"\"Get icon for a task.\n\n    Icon should be defined by task type which is stored on project.\n    \"\"\"\n\n    color = get_default_entity_icon_color()\n\n    tasks_info = asset_doc.get(\"data\", {}).get(\"tasks\") or {}\n    task_info = tasks_info.get(task_name) or {}\n    task_icon = task_info.get(\"icon\")\n    if task_icon:\n        icon = get_qta_icon_by_name_and_color(task_icon, color)\n        if icon is not None:\n            return icon\n\n    task_type = task_info.get(\"type\")\n    task_types = project_doc[\"config\"][\"tasks\"]\n\n    task_type_info = task_types.get(task_type) or {}\n    task_type_icon = task_type_info.get(\"icon\")\n    if task_type_icon:\n        icon = get_qta_icon_by_name_and_color(task_icon, color)\n        if icon is not None:\n            return icon\n    return get_default_task_icon(color)\n\n\ndef schedule(func, time, channel=\"default\"):\n    \"\"\"Run `func` at a later `time` in a dedicated `channel`\n\n    Given an arbitrary function, call this function after a given\n    timeout. It will ensure that only one \"job\" is running within\n    the given channel at any one time and cancel any currently\n    running job if a new job is submitted before the timeout.\n\n    \"\"\"\n\n    try:\n        SharedObjects.jobs[channel].stop()\n    except (AttributeError, KeyError, RuntimeError):\n        pass\n\n    timer = QtCore.QTimer()\n    timer.setSingleShot(True)\n    timer.timeout.connect(func)\n    timer.start(time)\n\n    SharedObjects.jobs[channel] = timer\n\n\ndef iter_model_rows(model, column, include_root=False):\n    \"\"\"Iterate over all row indices in a model\"\"\"\n    indices = [QtCore.QModelIndex()]  # start iteration at root\n\n    for index in indices:\n        # Add children to the iterations\n        child_rows = model.rowCount(index)\n        for child_row in range(child_rows):\n            child_index = model.index(child_row, column, index)\n            indices.append(child_index)\n\n        if not include_root and not index.isValid():\n            continue\n\n        yield index\n\n\n@contextlib.contextmanager\ndef preserve_expanded_rows(tree_view, column=0, role=None):\n    \"\"\"Preserves expanded row in QTreeView by column's data role.\n\n    This function is created to maintain the expand vs collapse status of\n    the model items. When refresh is triggered the items which are expanded\n    will stay expanded and vice versa.\n\n    Arguments:\n        tree_view (QWidgets.QTreeView): the tree view which is\n            nested in the application\n        column (int): the column to retrieve the data from\n        role (int): the role which dictates what will be returned\n\n    Returns:\n        None\n\n    \"\"\"\n    if role is None:\n        role = QtCore.Qt.DisplayRole\n    model = tree_view.model()\n\n    expanded = set()\n\n    for index in iter_model_rows(model, column=column, include_root=False):\n        if tree_view.isExpanded(index):\n            value = index.data(role)\n            expanded.add(value)\n\n    try:\n        yield\n    finally:\n        if not expanded:\n            return\n\n        for index in iter_model_rows(model, column=column, include_root=False):\n            value = index.data(role)\n            state = value in expanded\n            if state:\n                tree_view.expand(index)\n            else:\n                tree_view.collapse(index)\n\n\n@contextlib.contextmanager\ndef preserve_selection(tree_view, column=0, role=None, current_index=True):\n    \"\"\"Preserves row selection in QTreeView by column's data role.\n\n    This function is created to maintain the selection status of\n    the model items. When refresh is triggered the items which are expanded\n    will stay expanded and vice versa.\n\n        tree_view (QWidgets.QTreeView): the tree view nested in the application\n        column (int): the column to retrieve the data from\n        role (int): the role which dictates what will be returned\n\n    Returns:\n        None\n\n    \"\"\"\n    if role is None:\n        role = QtCore.Qt.DisplayRole\n    model = tree_view.model()\n    selection_model = tree_view.selectionModel()\n    flags = (\n        QtCore.QItemSelectionModel.Select\n        | QtCore.QItemSelectionModel.Rows\n    )\n\n    if current_index:\n        current_index_value = tree_view.currentIndex().data(role)\n    else:\n        current_index_value = None\n\n    selected_rows = selection_model.selectedRows()\n    if not selected_rows:\n        yield\n        return\n\n    selected = set(row.data(role) for row in selected_rows)\n    try:\n        yield\n    finally:\n        if not selected:\n            return\n\n        # Go through all indices, select the ones with similar data\n        for index in iter_model_rows(model, column=column, include_root=False):\n            value = index.data(role)\n            state = value in selected\n            if state:\n                tree_view.scrollTo(index)  # Ensure item is visible\n                selection_model.select(index, flags)\n\n            if current_index_value and value == current_index_value:\n                selection_model.setCurrentIndex(\n                    index, selection_model.NoUpdate\n                )\n\n\nclass FamilyConfigCache:\n    default_color = \"#0091B2\"\n    _default_icon = None\n\n    def __init__(self, dbcon):\n        self.dbcon = dbcon\n        self.family_configs = {}\n        self._family_filters_set = False\n        self._family_filters_is_include = True\n        self._require_refresh = True\n\n    @classmethod\n    def default_icon(cls):\n        if cls._default_icon is None:\n            cls._default_icon = qtawesome.icon(\n                \"fa.folder\", color=cls.default_color\n            )\n        return cls._default_icon\n\n    def family_config(self, family_name):\n        \"\"\"Get value from config with fallback to default\"\"\"\n        if self._require_refresh:\n            self._refresh()\n\n        item = self.family_configs.get(family_name)\n        if not item:\n            item = {\n                \"icon\": self.default_icon()\n            }\n            if self._family_filters_set:\n                item[\"state\"] = not self._family_filters_is_include\n        return item\n\n    def refresh(self, force=False):\n        self._require_refresh = True\n\n        if force:\n            self._refresh()\n\n    def _refresh(self):\n        \"\"\"Get the family configurations from the database\n\n        The configuration must be stored on the project under `config`.\n        For example:\n\n        {\"config\": {\n            \"families\": [\n                {\"name\": \"avalon.camera\", label: \"Camera\", \"icon\": \"photo\"},\n                {\"name\": \"avalon.anim\", label: \"Animation\", \"icon\": \"male\"},\n            ]\n        }}\n\n        It is possible to override the default behavior and set specific\n        families checked. For example we only want the families imagesequence\n        and camera to be visible in the Loader.\n        \"\"\"\n        self._require_refresh = False\n        self._family_filters_set = False\n\n        self.family_configs.clear()\n        # Skip if we're not in host context\n        if not registered_host():\n            return\n\n        # Update the icons from the project configuration\n        context = get_current_context()\n        project_name = context[\"project_name\"]\n        asset_name = context[\"asset_name\"]\n        task_name = context[\"task_name\"]\n        host_name = get_current_host_name()\n        if not all((project_name, asset_name, task_name)):\n            return\n\n        matching_item = None\n        project_settings = get_project_settings(project_name)\n        profiles = (\n            project_settings\n            [\"global\"]\n            [\"tools\"]\n            [\"loader\"]\n            [\"family_filter_profiles\"]\n        )\n        if profiles:\n            # Make sure connection is installed\n            # - accessing attribute which does not have auto-install\n            asset_doc = get_asset_by_name(\n                project_name, asset_name, fields=[\"data.tasks\"]\n            ) or {}\n            tasks_info = asset_doc.get(\"data\", {}).get(\"tasks\") or {}\n            task_type = tasks_info.get(task_name, {}).get(\"type\")\n            profiles_filter = {\n                \"task_types\": task_type,\n                \"hosts\": host_name\n            }\n            matching_item = filter_profiles(profiles, profiles_filter)\n\n        families = []\n        is_include = True\n        if matching_item:\n            families = matching_item[\"filter_families\"]\n            is_include = matching_item[\"is_include\"]\n\n        if not families:\n            return\n\n        self._family_filters_set = True\n        self._family_filters_is_include = is_include\n\n        # Replace icons with a Qt icon we can use in the user interfaces\n        for family in families:\n            family_info = {\n                \"name\": family,\n                \"icon\": self.default_icon(),\n                \"state\": is_include\n            }\n\n            self.family_configs[family] = family_info\n\n\nclass GroupsConfig:\n    # Subset group item's default icon and order\n    _default_group_config = None\n\n    def __init__(self, dbcon):\n        self.dbcon = dbcon\n        self.groups = {}\n        self._default_group_color = get_default_entity_icon_color()\n\n    @classmethod\n    def default_group_config(cls):\n        if cls._default_group_config is None:\n            cls._default_group_config = {\n                \"icon\": qtawesome.icon(\n                    \"fa.object-group\",\n                    color=get_default_entity_icon_color()\n                ),\n                \"order\": 0\n            }\n        return cls._default_group_config\n\n    def refresh(self):\n        \"\"\"Get subset group configurations from the database\n\n        The 'group' configuration must be stored in the project `config` field.\n        See schema `config-1.0.json`\n\n        \"\"\"\n        # Clear cached groups\n        self.groups.clear()\n\n        group_configs = []\n        project_name = self.dbcon.Session.get(\"AVALON_PROJECT\")\n        if project_name:\n            # Get pre-defined group name and appearance from project config\n            project_doc = get_project(project_name, fields=[\"config.groups\"])\n\n            if project_doc:\n                group_configs = project_doc[\"config\"].get(\"groups\") or []\n            else:\n                print(\"Project not found! \\\"{}\\\"\".format(project_name))\n\n        # Build pre-defined group configs\n        for config in group_configs:\n            name = config[\"name\"]\n            icon = \"fa.\" + config.get(\"icon\", \"object-group\")\n            color = config.get(\"color\", self._default_group_color)\n            order = float(config.get(\"order\", 0))\n\n            self.groups[name] = {\n                \"icon\": qtawesome.icon(icon, color=color),\n                \"order\": order\n            }\n\n        return self.groups\n\n    def ordered_groups(self, group_names):\n        # default order zero included\n        _orders = set([0])\n        for config in self.groups.values():\n            _orders.add(config[\"order\"])\n\n        # Remap order to list index\n        orders = sorted(_orders)\n\n        _groups = list()\n        for name in group_names:\n            # Get group config\n            config = self.groups.get(name) or self.default_group_config()\n            # Base order\n            remapped_order = orders.index(config[\"order\"])\n\n            data = {\n                \"name\": name,\n                \"icon\": config[\"icon\"],\n                \"_order\": remapped_order,\n            }\n\n            _groups.append(data)\n\n        # Sort by tuple (base_order, name)\n        # If there are multiple groups in same order, will sorted by name.\n        ordered_groups = sorted(\n            _groups, key=lambda _group: (_group.pop(\"_order\"), _group[\"name\"])\n        )\n\n        total = len(ordered_groups)\n        order_temp = \"%0{}d\".format(len(str(total)))\n\n        # Update sorted order to config\n        for index, group_data in enumerate(ordered_groups):\n            order = index\n            inverse_order = total - index\n\n            # Format orders into fixed length string for groups sorting\n            group_data[\"order\"] = order_temp % order\n            group_data[\"inverseOrder\"] = order_temp % inverse_order\n\n        return ordered_groups\n\n    def active_groups(self, asset_ids, include_predefined=True):\n        \"\"\"Collect all active groups from each subset\"\"\"\n        # Collect groups from subsets\n        group_names = set(\n            self.dbcon.distinct(\n                \"data.subsetGroup\",\n                {\"type\": \"subset\", \"parent\": {\"$in\": asset_ids}}\n            )\n        )\n        if include_predefined:\n            # Ensure all predefined group configs will be included\n            group_names.update(self.groups.keys())\n\n        return self.ordered_groups(group_names)\n\n    def split_subsets_for_groups(self, subset_docs, grouping):\n        \"\"\"Collect all active groups from each subset\"\"\"\n        subset_docs_without_group = collections.defaultdict(list)\n        subset_docs_by_group = collections.defaultdict(dict)\n        for subset_doc in subset_docs:\n            subset_name = subset_doc[\"name\"]\n            if grouping:\n                group_name = subset_doc[\"data\"].get(\"subsetGroup\")\n                if group_name:\n                    if subset_name not in subset_docs_by_group[group_name]:\n                        subset_docs_by_group[group_name][subset_name] = []\n\n                    subset_docs_by_group[group_name][subset_name].append(\n                        subset_doc\n                    )\n                    continue\n\n            subset_docs_without_group[subset_name].append(subset_doc)\n\n        ordered_groups = self.ordered_groups(subset_docs_by_group.keys())\n\n        return ordered_groups, subset_docs_without_group, subset_docs_by_group\n\n\nclass DynamicQThread(QtCore.QThread):\n    \"\"\"QThread which can run any function with argument and kwargs.\n\n    Args:\n        func (function): Function which will be called.\n        args (tuple): Arguments which will be passed to function.\n        kwargs (tuple): Keyword arguments which will be passed to function.\n        parent (QObject): Parent of thread.\n    \"\"\"\n    def __init__(self, func, args=None, kwargs=None, parent=None):\n        super(DynamicQThread, self).__init__(parent)\n        if args is None:\n            args = tuple()\n        if kwargs is None:\n            kwargs = {}\n        self._func = func\n        self._args = args\n        self._kwargs = kwargs\n\n    def run(self):\n        \"\"\"Execute the function with arguments.\"\"\"\n        self._func(*self._args, **self._kwargs)\n\n\ndef create_qthread(func, *args, **kwargs):\n    class Thread(QtCore.QThread):\n        def run(self):\n            try:\n                func(*args, **kwargs)\n            except BaseException:\n                traceback.print_exception(*sys.exc_info())\n                raise\n    return Thread()\n\n\ndef get_repre_icons():\n    \"\"\"Returns a dict {'provider_name': QIcon}\"\"\"\n    icons = {}\n    try:\n        from openpype_modules import sync_server\n    except Exception:\n        # Backwards compatibility\n        try:\n            from openpype.modules import sync_server\n        except Exception:\n            return icons\n\n    resource_path = os.path.join(\n        os.path.dirname(sync_server.sync_server_module.__file__),\n        \"providers\", \"resources\"\n    )\n    if not os.path.exists(resource_path):\n        print(\"No icons for Site Sync found\")\n        return icons\n\n    for file_name in os.listdir(resource_path):\n        if file_name and not file_name.endswith(\"png\"):\n            continue\n\n        provider, _ = os.path.splitext(file_name)\n\n        pix_url = os.path.join(resource_path, file_name)\n        icons[provider] = QtGui.QIcon(pix_url)\n\n    return icons\n\n\ndef is_sync_loader(loader):\n    return is_remove_site_loader(loader) or is_add_site_loader(loader)\n\n\ndef is_remove_site_loader(loader):\n    return hasattr(loader, \"is_remove_site_loader\")\n\n\ndef is_add_site_loader(loader):\n    return hasattr(loader, \"is_add_site_loader\")\n\n\nclass WrappedCallbackItem:\n    \"\"\"Structure to store information about callback and args/kwargs for it.\n\n    Item can be used to execute callback in main thread which may be needed\n    for execution of Qt objects.\n\n    Item store callback (callable variable), arguments and keyword arguments\n    for the callback. Item hold information about it's process.\n    \"\"\"\n    not_set = object()\n    _log = None\n\n    def __init__(self, callback, *args, **kwargs):\n        self._done = False\n        self._exception = self.not_set\n        self._result = self.not_set\n        self._callback = callback\n        self._args = args\n        self._kwargs = kwargs\n\n    def __call__(self):\n        self.execute()\n\n    @property\n    def log(self):\n        cls = self.__class__\n        if cls._log is None:\n            cls._log = Logger.get_logger(cls.__name__)\n        return cls._log\n\n    @property\n    def done(self):\n        return self._done\n\n    @property\n    def exception(self):\n        return self._exception\n\n    @property\n    def result(self):\n        return self._result\n\n    def execute(self):\n        \"\"\"Execute callback and store its result.\n\n        Method must be called from main thread. Item is marked as `done`\n        when callback execution finished. Store output of callback of exception\n        information when callback raises one.\n        \"\"\"\n        if self.done:\n            self.log.warning(\"- item is already processed\")\n            return\n\n        try:\n            result = self._callback(*self._args, **self._kwargs)\n            self._result = result\n\n        except Exception as exc:\n            self._exception = exc\n\n        finally:\n            self._done = True\n\n\ndef get_warning_pixmap(color=None):\n    \"\"\"Warning icon as QPixmap.\n\n    Args:\n        color(QtGui.QColor): Color that will be used to paint warning icon.\n    \"\"\"\n    src_image_path = get_image_path(\"warning.png\")\n    src_image = QtGui.QImage(src_image_path)\n    if color is None:\n        color = get_objected_colors(\"delete-btn-bg\").get_qcolor()\n\n    return paint_image_with_color(src_image, color)\n"
  },
  {
    "path": "openpype/tools/utils/models.py",
    "content": "import re\nimport logging\n\nimport qtpy\nfrom qtpy import QtCore, QtGui\nfrom openpype.client import get_projects\nfrom .constants import (\n    PROJECT_IS_ACTIVE_ROLE,\n    PROJECT_NAME_ROLE,\n    DEFAULT_PROJECT_LABEL\n)\n\nlog = logging.getLogger(__name__)\n\n\nclass TreeModel(QtCore.QAbstractItemModel):\n\n    Columns = list()\n    ItemRole = QtCore.Qt.UserRole + 1\n    item_class = None\n\n    def __init__(self, parent=None):\n        super(TreeModel, self).__init__(parent)\n        self._root_item = self.ItemClass()\n\n    @property\n    def ItemClass(self):\n        if self.item_class is not None:\n            return self.item_class\n        return Item\n\n    def rowCount(self, parent=None):\n        if parent is None or not parent.isValid():\n            parent_item = self._root_item\n        else:\n            parent_item = parent.internalPointer()\n        return parent_item.childCount()\n\n    def columnCount(self, parent):\n        return len(self.Columns)\n\n    def data(self, index, role):\n        if not index.isValid():\n            return None\n\n        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:\n            item = index.internalPointer()\n            column = index.column()\n\n            key = self.Columns[column]\n            return item.get(key, None)\n\n        if role == self.ItemRole:\n            return index.internalPointer()\n\n    def setData(self, index, value, role=QtCore.Qt.EditRole):\n        \"\"\"Change the data on the items.\n\n        Returns:\n            bool: Whether the edit was successful\n        \"\"\"\n\n        if index.isValid():\n            if role == QtCore.Qt.EditRole:\n\n                item = index.internalPointer()\n                column = index.column()\n                key = self.Columns[column]\n                item[key] = value\n\n                # passing `list()` for PyQt5 (see PYSIDE-462)\n                if qtpy.API in (\"pyqt4\", \"pyside\"):\n                    self.dataChanged.emit(index, index)\n                else:\n                    self.dataChanged.emit(index, index, [role])\n\n                # must return true if successful\n                return True\n\n        return False\n\n    def setColumns(self, keys):\n        assert isinstance(keys, (list, tuple))\n        self.Columns = keys\n\n    def headerData(self, section, orientation, role):\n\n        if role == QtCore.Qt.DisplayRole:\n            if section < len(self.Columns):\n                return self.Columns[section]\n\n        super(TreeModel, self).headerData(section, orientation, role)\n\n    def flags(self, index):\n        flags = QtCore.Qt.ItemIsEnabled\n\n        item = index.internalPointer()\n        if item.get(\"enabled\", True):\n            flags |= QtCore.Qt.ItemIsSelectable\n\n        return flags\n\n    def parent(self, index):\n\n        item = index.internalPointer()\n        parent_item = item.parent()\n\n        # If it has no parents we return invalid\n        if parent_item == self._root_item or not parent_item:\n            return QtCore.QModelIndex()\n\n        return self.createIndex(parent_item.row(), 0, parent_item)\n\n    def index(self, row, column, parent=None):\n        \"\"\"Return index for row/column under parent\"\"\"\n\n        if parent is None or not parent.isValid():\n            parent_item = self._root_item\n        else:\n            parent_item = parent.internalPointer()\n\n        child_item = parent_item.child(row)\n        if child_item:\n            return self.createIndex(row, column, child_item)\n        else:\n            return QtCore.QModelIndex()\n\n    def add_child(self, item, parent=None):\n        if parent is None:\n            parent = self._root_item\n\n        parent.add_child(item)\n\n    def column_name(self, column):\n        \"\"\"Return column key by index\"\"\"\n\n        if column < len(self.Columns):\n            return self.Columns[column]\n\n    def clear(self):\n        self.beginResetModel()\n        self._root_item = self.ItemClass()\n        self.endResetModel()\n\n\nclass Item(dict):\n    \"\"\"An item that can be represented in a tree view using `TreeModel`.\n\n    The item can store data just like a regular dictionary.\n\n    >>> data = {\"name\": \"John\", \"score\": 10}\n    >>> item = Item(data)\n    >>> assert item[\"name\"] == \"John\"\n\n    \"\"\"\n\n    def __init__(self, data=None):\n        super(Item, self).__init__()\n\n        self._children = list()\n        self._parent = None\n\n        if data is not None:\n            assert isinstance(data, dict)\n            self.update(data)\n\n    def childCount(self):\n        return len(self._children)\n\n    def child(self, row):\n\n        if row >= len(self._children):\n            log.warning(\"Invalid row as child: {0}\".format(row))\n            return\n\n        return self._children[row]\n\n    def children(self):\n        return self._children\n\n    def parent(self):\n        return self._parent\n\n    def row(self):\n        \"\"\"\n        Returns:\n             int: Index of this item under parent\"\"\"\n        if self._parent is not None:\n            siblings = self.parent().children()\n            return siblings.index(self)\n        return -1\n\n    def add_child(self, child):\n        \"\"\"Add a child to this item\"\"\"\n        child._parent = self\n        self._children.append(child)\n\n\nclass RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):\n    \"\"\"Recursive proxy model.\n    Item is not filtered if any children match the filter.\n    Use case: Filtering by string - parent won't be filtered if does not match\n        the filter string but first checks if any children does.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(RecursiveSortFilterProxyModel, self).__init__(*args, **kwargs)\n        recursive_enabled = False\n        if hasattr(self, \"setRecursiveFilteringEnabled\"):\n            self.setRecursiveFilteringEnabled(True)\n            recursive_enabled = True\n        self._recursive_enabled = recursive_enabled\n\n    def filterAcceptsRow(self, row, parent_index):\n        if hasattr(self, \"filterRegExp\"):\n            regex = self.filterRegExp()\n        else:\n            regex = self.filterRegularExpression()\n\n        pattern = regex.pattern()\n        if pattern:\n            model = self.sourceModel()\n            source_index = model.index(\n                row, self.filterKeyColumn(), parent_index\n            )\n            if source_index.isValid():\n                pattern = regex.pattern()\n\n                # Check current index itself\n                value = model.data(source_index, self.filterRole())\n                matched = bool(re.search(pattern, value, re.IGNORECASE))\n                if matched or self._recursive_enabled:\n                    return matched\n\n                rows = model.rowCount(source_index)\n                for idx in range(rows):\n                    if self.filterAcceptsRow(idx, source_index):\n                        return True\n\n                # Otherwise filter it\n                return False\n\n        return super(RecursiveSortFilterProxyModel, self).filterAcceptsRow(\n            row, parent_index\n        )\n\n\nclass ProjectModel(QtGui.QStandardItemModel):\n    def __init__(\n        self, dbcon=None, only_active=True, add_default_project=False,\n        *args, **kwargs\n    ):\n        super(ProjectModel, self).__init__(*args, **kwargs)\n\n        self.dbcon = dbcon\n\n        self._only_active = only_active\n        self._add_default_project = add_default_project\n\n        self._default_item = None\n        self._items_by_name = {}\n        # Model was at least once refreshed\n        # - for `set_dbcon` method\n        self._refreshed = False\n\n    def set_default_project_available(self, available=True):\n        if available is None:\n            available = not self._add_default_project\n\n        if self._add_default_project == available:\n            return\n\n        self._add_default_project = available\n        if not available and self._default_item is not None:\n            root_item = self.invisibleRootItem()\n            root_item.removeRow(self._default_item.row())\n            self._default_item = None\n\n    def set_only_active(self, only_active=True):\n        if only_active is None:\n            only_active = not self._only_active\n\n        if self._only_active == only_active:\n            return\n\n        self._only_active = only_active\n\n        if self._refreshed:\n            self.refresh()\n\n    def set_dbcon(self, dbcon):\n        \"\"\"Change mongo connection.\"\"\"\n        self.dbcon = dbcon\n        # Trigger refresh if was already refreshed\n        if self._refreshed:\n            self.refresh()\n\n    def project_name_is_available(self, project_name):\n        \"\"\"Check availability of project name in current items.\"\"\"\n        return project_name in self._items_by_name\n\n    def refresh(self):\n        # Change '_refreshed' state\n        self._refreshed = True\n        new_items = []\n        # Add default item to model if should\n        if self._add_default_project and self._default_item is None:\n            item = QtGui.QStandardItem(DEFAULT_PROJECT_LABEL)\n            item.setData(None, PROJECT_NAME_ROLE)\n            item.setData(True, PROJECT_IS_ACTIVE_ROLE)\n            new_items.append(item)\n            self._default_item = item\n\n        project_names = set()\n        project_docs = get_projects(\n            inactive=not self._only_active,\n            fields=[\"name\", \"data.active\"]\n        )\n        for project_doc in project_docs:\n            project_name = project_doc[\"name\"]\n            project_names.add(project_name)\n            if project_name in self._items_by_name:\n                item = self._items_by_name[project_name]\n            else:\n                item = QtGui.QStandardItem(project_name)\n\n                self._items_by_name[project_name] = item\n                new_items.append(item)\n\n            is_active = project_doc.get(\"data\", {}).get(\"active\", True)\n            item.setData(project_name, PROJECT_NAME_ROLE)\n            item.setData(is_active, PROJECT_IS_ACTIVE_ROLE)\n\n            if not is_active:\n                font = item.font()\n                font.setItalic(True)\n                item.setFont(font)\n\n        root_item = self.invisibleRootItem()\n        for project_name in tuple(self._items_by_name.keys()):\n            if project_name not in project_names:\n                item = self._items_by_name.pop(project_name)\n                root_item.removeRow(item.row())\n\n        if new_items:\n            root_item.appendRows(new_items)\n\n    def find_project(self, project_name):\n        \"\"\"\n            Get index of 'project_name' value.\n\n            Args:\n                project_name (str):\n            Returns:\n                (QModelIndex)\n        \"\"\"\n        val = self._items_by_name.get(project_name)\n        if val:\n            return self.indexFromItem(val)\n\n\nclass ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):\n    def __init__(self, *args, **kwargs):\n        super(ProjectSortFilterProxy, self).__init__(*args, **kwargs)\n        self._filter_enabled = True\n        # Disable case sensitivity\n        self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n    def lessThan(self, left_index, right_index):\n        if left_index.data(PROJECT_NAME_ROLE) is None:\n            return True\n\n        if right_index.data(PROJECT_NAME_ROLE) is None:\n            return False\n\n        left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE)\n        right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE)\n        if right_is_active == left_is_active:\n            return super(ProjectSortFilterProxy, self).lessThan(\n                left_index, right_index\n            )\n\n        if left_is_active:\n            return True\n        return False\n\n    def filterAcceptsRow(self, source_row, source_parent):\n        index = self.sourceModel().index(source_row, 0, source_parent)\n        string_pattern = self.filterRegularExpression().pattern()\n        if self._filter_enabled:\n            result = self._custom_index_filter(index)\n            if result is not None:\n                project_name = index.data(PROJECT_NAME_ROLE)\n                if project_name is None:\n                    return result\n                return string_pattern.lower() in project_name.lower()\n\n        return super(ProjectSortFilterProxy, self).filterAcceptsRow(\n            source_row, source_parent\n        )\n\n    def _custom_index_filter(self, index):\n        is_active = bool(index.data(PROJECT_IS_ACTIVE_ROLE))\n\n        return is_active\n\n    def is_filter_enabled(self):\n        return self._filter_enabled\n\n    def set_filter_enabled(self, value):\n        self._filter_enabled = value\n        self.invalidateFilter()\n"
  },
  {
    "path": "openpype/tools/utils/multiselection_combobox.py",
    "content": "from qtpy import QtCore, QtGui, QtWidgets\n\nfrom .lib import (\n    checkstate_int_to_enum,\n    checkstate_enum_to_int,\n)\nfrom .constants import (\n    CHECKED_INT,\n    UNCHECKED_INT,\n    ITEM_IS_USER_TRISTATE,\n)\n\n\nclass ComboItemDelegate(QtWidgets.QStyledItemDelegate):\n    \"\"\"\n    Helper styled delegate (mostly based on existing private Qt's\n    delegate used by the QtWidgets.QComboBox). Used to style the popup like a\n    list view (e.g windows style).\n    \"\"\"\n\n    def paint(self, painter, option, index):\n        option = QtWidgets.QStyleOptionViewItem(option)\n        option.showDecorationSelected = True\n\n        # option.state &= (\n        #     ~QtWidgets.QStyle.State_HasFocus\n        #     & ~QtWidgets.QStyle.State_MouseOver\n        # )\n        super(ComboItemDelegate, self).paint(painter, option, index)\n\n\nclass MultiSelectionComboBox(QtWidgets.QComboBox):\n    value_changed = QtCore.Signal()\n    focused_in = QtCore.Signal()\n\n    ignored_keys = {\n        QtCore.Qt.Key_Up,\n        QtCore.Qt.Key_Down,\n        QtCore.Qt.Key_PageDown,\n        QtCore.Qt.Key_PageUp,\n        QtCore.Qt.Key_Home,\n        QtCore.Qt.Key_End,\n    }\n\n    top_bottom_padding = 2\n    left_right_padding = 3\n    left_offset = 4\n    top_bottom_margins = 2\n    item_spacing = 5\n\n    item_bg_color = QtGui.QColor(\"#31424e\")\n\n    def __init__(\n        self, parent=None, placeholder=\"\", separator=\", \", **kwargs\n    ):\n        super(MultiSelectionComboBox, self).__init__(parent=parent, **kwargs)\n        self.setObjectName(\"MultiSelectionComboBox\")\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        self._popup_is_shown = False\n        self._block_mouse_release_timer = QtCore.QTimer(self, singleShot=True)\n        self._initial_mouse_pos = None\n        self._separator = separator\n        self._placeholder_text = placeholder\n        delegate = ComboItemDelegate(self)\n        self.setItemDelegate(delegate)\n\n        self._lines = {}\n        self._item_height = None\n        self._custom_text = None\n        self._delegate = delegate\n\n    def get_placeholder_text(self):\n        return self._placeholder_text\n\n    def set_placeholder_text(self, text):\n        self._placeholder_text = text\n        self._update_size_hint()\n\n    def set_custom_text(self, text):\n        self._custom_text = text\n        self._update_size_hint()\n\n    def focusInEvent(self, event):\n        self.focused_in.emit()\n        return super(MultiSelectionComboBox, self).focusInEvent(event)\n\n    def mousePressEvent(self, event):\n        \"\"\"Reimplemented.\"\"\"\n        self._popup_is_shown = False\n        super(MultiSelectionComboBox, self).mousePressEvent(event)\n        if self._popup_is_shown:\n            self._initial_mouse_pos = self.mapToGlobal(event.pos())\n            self._block_mouse_release_timer.start(\n                QtWidgets.QApplication.doubleClickInterval()\n            )\n\n    def showPopup(self):\n        \"\"\"Reimplemented.\"\"\"\n        super(MultiSelectionComboBox, self).showPopup()\n        view = self.view()\n        view.installEventFilter(self)\n        view.viewport().installEventFilter(self)\n        self._popup_is_shown = True\n\n    def hidePopup(self):\n        \"\"\"Reimplemented.\"\"\"\n        self.view().removeEventFilter(self)\n        self.view().viewport().removeEventFilter(self)\n        self._popup_is_shown = False\n        self._initial_mouse_pos = None\n        super(MultiSelectionComboBox, self).hidePopup()\n        self.view().clearFocus()\n\n    def _event_popup_shown(self, obj, event):\n        if not self._popup_is_shown:\n            return\n\n        current_index = self.view().currentIndex()\n        model = self.model()\n\n        if event.type() == QtCore.QEvent.MouseMove:\n            if (\n                self.view().isVisible()\n                and self._initial_mouse_pos is not None\n                and self._block_mouse_release_timer.isActive()\n            ):\n                diff = obj.mapToGlobal(event.pos()) - self._initial_mouse_pos\n                if diff.manhattanLength() > 9:\n                    self._block_mouse_release_timer.stop()\n            return\n\n        index_flags = current_index.flags()\n        state = checkstate_int_to_enum(\n            current_index.data(QtCore.Qt.CheckStateRole)\n        )\n        new_state = None\n\n        if event.type() == QtCore.QEvent.MouseButtonRelease:\n            if (\n                self._block_mouse_release_timer.isActive()\n                or not current_index.isValid()\n                or not self.view().isVisible()\n                or not self.view().rect().contains(event.pos())\n                or not index_flags & QtCore.Qt.ItemIsSelectable\n                or not index_flags & QtCore.Qt.ItemIsEnabled\n                or not index_flags & QtCore.Qt.ItemIsUserCheckable\n            ):\n                return\n\n            if state == QtCore.Qt.Unchecked:\n                new_state = CHECKED_INT\n            else:\n                new_state = UNCHECKED_INT\n\n        elif event.type() == QtCore.QEvent.KeyPress:\n            # TODO: handle QtCore.Qt.Key_Enter, Key_Return?\n            if event.key() == QtCore.Qt.Key_Space:\n                if (\n                    index_flags & QtCore.Qt.ItemIsUserCheckable\n                    and index_flags & ITEM_IS_USER_TRISTATE\n                ):\n                    new_state = (checkstate_enum_to_int(state) + 1) % 3\n\n                elif index_flags & QtCore.Qt.ItemIsUserCheckable:\n                    # toggle the current items check state\n                    if state != QtCore.Qt.Checked:\n                        new_state = CHECKED_INT\n                    else:\n                        new_state = UNCHECKED_INT\n\n        if new_state is not None:\n            model.setData(current_index, new_state, QtCore.Qt.CheckStateRole)\n            self.view().update(current_index)\n            self._update_size_hint()\n            self.value_changed.emit()\n            return True\n\n    def eventFilter(self, obj, event):\n        \"\"\"Reimplemented.\"\"\"\n        result = self._event_popup_shown(obj, event)\n        if result is not None:\n            return result\n\n        return super(MultiSelectionComboBox, self).eventFilter(obj, event)\n\n    def addItem(self, *args, **kwargs):\n        idx = self.count()\n        super(MultiSelectionComboBox, self).addItem(*args, **kwargs)\n        self.model().item(idx).setCheckable(True)\n\n    def paintEvent(self, event):\n        \"\"\"Reimplemented.\"\"\"\n        painter = QtWidgets.QStylePainter(self)\n        option = QtWidgets.QStyleOptionComboBox()\n        self.initStyleOption(option)\n        painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, option)\n\n        items = self.checked_items_text()\n        # draw the icon and text\n        draw_text = True\n        combotext = None\n        if self._custom_text is not None:\n            combotext = self._custom_text\n        elif not items:\n            combotext = self._placeholder_text\n        else:\n            draw_text = False\n        if draw_text:\n            option.currentText = combotext\n            option.palette.setCurrentColorGroup(QtGui.QPalette.Disabled)\n            painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option)\n            return\n\n        font_metricts = self.fontMetrics()\n\n        if self._item_height is None:\n            self.updateGeometry()\n            self.update()\n            return\n\n        for line, items in self._lines.items():\n            top_y = (\n                option.rect.top()\n                + (line * self._item_height)\n                + self.top_bottom_margins\n            )\n            left_x = option.rect.left() + self.left_offset\n            for item in items:\n                label_rect = font_metricts.boundingRect(item)\n                label_height = label_rect.height()\n\n                label_rect.moveTop(top_y)\n                label_rect.moveLeft(left_x)\n                label_rect.setHeight(self._item_height)\n                label_rect.setWidth(\n                    label_rect.width() + self.left_right_padding\n                )\n\n                bg_rect = QtCore.QRectF(label_rect)\n                bg_rect.setWidth(\n                    label_rect.width() + self.left_right_padding\n                )\n                left_x = bg_rect.right() + self.item_spacing\n\n                label_rect.moveLeft(label_rect.x() + self.left_right_padding)\n\n                bg_rect.setHeight(label_height + (2 * self.top_bottom_padding))\n                bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins)\n\n                path = QtGui.QPainterPath()\n                path.addRoundedRect(bg_rect, 5, 5)\n\n                painter.fillPath(path, self.item_bg_color)\n\n                painter.drawText(\n                    label_rect,\n                    QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,\n                    item\n                )\n\n    def resizeEvent(self, *args, **kwargs):\n        super(MultiSelectionComboBox, self).resizeEvent(*args, **kwargs)\n        self._update_size_hint()\n\n    def _update_size_hint(self):\n        if self._custom_text is not None:\n            self.update()\n            return\n        self._lines = {}\n\n        items = self.checked_items_text()\n        if not items:\n            self.update()\n            self.repaint()\n            return\n\n        option = QtWidgets.QStyleOptionComboBox()\n        self.initStyleOption(option)\n        btn_rect = self.style().subControlRect(\n            QtWidgets.QStyle.CC_ComboBox,\n            option,\n            QtWidgets.QStyle.SC_ComboBoxArrow\n        )\n        total_width = option.rect.width() - btn_rect.width()\n\n        line = 0\n        self._lines = {line: []}\n\n        font_metricts = self.fontMetrics()\n        default_left_x = 0 + self.left_offset\n        left_x = int(default_left_x)\n        for item in items:\n            rect = font_metricts.boundingRect(item)\n            width = rect.width() + (2 * self.left_right_padding)\n            right_x = left_x + width\n            if right_x > total_width:\n                left_x = int(default_left_x)\n                if self._lines.get(line):\n                    line += 1\n                    self._lines[line] = [item]\n                    left_x += width\n                else:\n                    self._lines[line] = [item]\n                    line += 1\n            else:\n                if line in self._lines:\n                    self._lines[line].append(item)\n                else:\n                    self._lines[line] = [item]\n                left_x = left_x + width + self.item_spacing\n\n        self.update()\n        self.updateGeometry()\n\n    def sizeHint(self):\n        value = super(MultiSelectionComboBox, self).sizeHint()\n        lines = 1\n        if self._custom_text is None:\n            lines = len(self._lines)\n            if lines == 0:\n                lines = 1\n\n        if self._item_height is None:\n            self._item_height = (\n                self.fontMetrics().height()\n                + (2 * self.top_bottom_padding)\n                + (2 * self.top_bottom_margins)\n            )\n        value.setHeight(\n            (lines * self._item_height)\n            + (2 * self.top_bottom_margins)\n        )\n        return value\n\n    def setItemCheckState(self, index, state):\n        self.setItemData(index, state, QtCore.Qt.CheckStateRole)\n\n    def set_value(self, values):\n        for idx in range(self.count()):\n            value = self.itemData(idx, role=QtCore.Qt.UserRole)\n            if value in values:\n                check_state = CHECKED_INT\n            else:\n                check_state = UNCHECKED_INT\n            self.setItemData(idx, check_state, QtCore.Qt.CheckStateRole)\n        self._update_size_hint()\n\n    def value(self):\n        items = list()\n        for idx in range(self.count()):\n            state = checkstate_int_to_enum(\n                self.itemData(idx, role=QtCore.Qt.CheckStateRole)\n            )\n            if state == QtCore.Qt.Checked:\n                items.append(\n                    self.itemData(idx, role=QtCore.Qt.UserRole)\n                )\n        return items\n\n    def checked_items_text(self):\n        items = list()\n        for idx in range(self.count()):\n            state = checkstate_int_to_enum(\n                self.itemData(idx, role=QtCore.Qt.CheckStateRole)\n            )\n            if state == QtCore.Qt.Checked:\n                items.append(self.itemText(idx))\n        return items\n\n    def wheelEvent(self, event):\n        event.ignore()\n\n    def keyPressEvent(self, event):\n        if (\n            event.key() == QtCore.Qt.Key_Down\n            and event.modifiers() & QtCore.Qt.AltModifier\n        ):\n            return self.showPopup()\n\n        if event.key() in self.ignored_keys:\n            return event.ignore()\n\n        return super(MultiSelectionComboBox, self).keyPressEvent(event)\n"
  },
  {
    "path": "openpype/tools/utils/overlay_messages.py",
    "content": "import uuid\n\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.style import get_objected_colors\n\nfrom .lib import set_style_property\n\n\nclass CloseButton(QtWidgets.QFrame):\n    \"\"\"Close button drawed manually.\"\"\"\n\n    clicked = QtCore.Signal()\n\n    def __init__(self, parent):\n        super(CloseButton, self).__init__(parent)\n        close_btn_color = get_objected_colors(\"overlay-messages\", \"close-btn\")\n        self._color = close_btn_color.get_qcolor()\n        self._mouse_pressed = False\n        policy = QtWidgets.QSizePolicy(\n            QtWidgets.QSizePolicy.Fixed,\n            QtWidgets.QSizePolicy.Fixed\n        )\n        self.setSizePolicy(policy)\n\n    def sizeHint(self):\n        size = self.fontMetrics().height()\n        return QtCore.QSize(size, size)\n\n    def mousePressEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self._mouse_pressed = True\n        super(CloseButton, self).mousePressEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        if self._mouse_pressed:\n            self._mouse_pressed = False\n            if self.rect().contains(event.pos()):\n                self.clicked.emit()\n\n        super(CloseButton, self).mouseReleaseEvent(event)\n\n    def paintEvent(self, event):\n        rect = self.rect()\n        painter = QtGui.QPainter(self)\n        painter.setClipRect(event.rect())\n        pen = QtGui.QPen()\n        pen.setWidth(2)\n        pen.setColor(self._color)\n        pen.setStyle(QtCore.Qt.SolidLine)\n        pen.setCapStyle(QtCore.Qt.RoundCap)\n        painter.setPen(pen)\n        offset = int(rect.height() / 4)\n        top = rect.top() + offset\n        left = rect.left() + offset\n        right = rect.right() - offset\n        bottom = rect.bottom() - offset\n        painter.drawLine(\n            left, top,\n            right, bottom\n        )\n        painter.drawLine(\n            left, bottom,\n            right, top\n        )\n\n\nclass OverlayMessageWidget(QtWidgets.QFrame):\n    \"\"\"Message widget showed as overlay.\n\n    Message is hidden after timeout but can be overriden by mouse hover.\n    Mouse hover can add additional 2 seconds of widget's visibility.\n\n    Args:\n        message_id (str): Unique identifier of message widget for\n            'MessageOverlayObject'.\n        message (str): Text shown in message.\n        parent (QWidget): Parent widget where message is visible.\n        timeout (int): Timeout of message's visibility (default 5000).\n        message_type (str): Property which can be used in styles for specific\n            kid of message.\n    \"\"\"\n\n    close_requested = QtCore.Signal(str)\n    _default_timeout = 5000\n\n    def __init__(\n        self, message_id, message, parent, message_type=None, timeout=None\n    ):\n        super(OverlayMessageWidget, self).__init__(parent)\n        self.setObjectName(\"OverlayMessageWidget\")\n\n        if message_type:\n            set_style_property(self, \"type\", message_type)\n\n        if not timeout:\n            timeout = self._default_timeout\n        timeout_timer = QtCore.QTimer()\n        timeout_timer.setInterval(timeout)\n        timeout_timer.setSingleShot(True)\n\n        hover_timer = QtCore.QTimer()\n        hover_timer.setInterval(2000)\n        hover_timer.setSingleShot(True)\n\n        label_widget = QtWidgets.QLabel(message, self)\n        label_widget.setAlignment(QtCore.Qt.AlignCenter)\n        label_widget.setWordWrap(True)\n        close_btn = CloseButton(self)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(5, 5, 0, 5)\n        layout.addWidget(label_widget, 1)\n        layout.addWidget(close_btn, 0)\n\n        close_btn.clicked.connect(self._on_close_clicked)\n        timeout_timer.timeout.connect(self._on_timer_timeout)\n        hover_timer.timeout.connect(self._on_hover_timeout)\n\n        self._label_widget = label_widget\n        self._message_id = message_id\n        self._timeout_timer = timeout_timer\n        self._hover_timer = hover_timer\n\n    def update_message(self, message, message_type=None, timeout=None):\n        self._label_widget.setText(message)\n        if timeout:\n            self._timeout_timer.setInterval(timeout)\n\n        set_style_property(self, \"type\", message_type)\n\n        self._timeout_timer.start()\n\n    def size_hint_without_word_wrap(self):\n        \"\"\"Size hint in cases that word wrap of label is disabled.\"\"\"\n        self._label_widget.setWordWrap(False)\n        size_hint = self.sizeHint()\n        self._label_widget.setWordWrap(True)\n        return size_hint\n\n    def showEvent(self, event):\n        \"\"\"Start timeout on show.\"\"\"\n        super(OverlayMessageWidget, self).showEvent(event)\n        self._timeout_timer.start()\n\n    def _on_timer_timeout(self):\n        \"\"\"On message timeout.\"\"\"\n        # Skip closing if hover timer is active\n        if not self._hover_timer.isActive():\n            self._close_message()\n\n    def _on_hover_timeout(self):\n        \"\"\"Hover timer timed out.\"\"\"\n        # Check if is still under widget\n        if self.underMouse():\n            self._hover_timer.start()\n        else:\n            self._close_message()\n\n    def _on_close_clicked(self):\n        self._close_message()\n\n    def _close_message(self):\n        \"\"\"Emmit close request to 'MessageOverlayObject'.\"\"\"\n        self.close_requested.emit(self._message_id)\n\n    def enterEvent(self, event):\n        \"\"\"Start hover timer on hover.\"\"\"\n        super(OverlayMessageWidget, self).enterEvent(event)\n        self._hover_timer.start()\n\n    def leaveEvent(self, event):\n        \"\"\"Start hover timer on hover leave.\"\"\"\n        super(OverlayMessageWidget, self).leaveEvent(event)\n        self._hover_timer.start()\n\n\nclass MessageOverlayObject(QtCore.QObject):\n    \"\"\"Object that can be used to add overlay messages.\n\n    Args:\n        widget (QWidget):\n    \"\"\"\n\n    def __init__(self, widget, default_timeout=None):\n        super(MessageOverlayObject, self).__init__()\n\n        widget.installEventFilter(self)\n\n        # Timer which triggers recalculation of message positions\n        recalculate_timer = QtCore.QTimer()\n        recalculate_timer.setInterval(10)\n\n        recalculate_timer.timeout.connect(self._recalculate_positions)\n\n        self._widget = widget\n        self._recalculate_timer = recalculate_timer\n\n        self._messages_order = []\n        self._closing_messages = set()\n        self._messages = {}\n        self._spacing = 5\n        self._move_size = 4\n        self._move_size_remove = 8\n        self._default_timeout = default_timeout\n\n    def add_message(\n        self, message, message_type=None, timeout=None, message_id=None\n    ):\n        \"\"\"Add single message into overlay.\n\n        Args:\n            message (str): Message that will be shown.\n            timeout (int): Message timeout.\n            message_type (str): Message type can be used as property in\n                stylesheets.\n            message_id (str): UUID of already existing message to update\n                it's message and timeout. Is created with different id if is\n                not available anymore.\n\n        Returns:\n            str: UUID of message which can be used to update message.\n        \"\"\"\n        # Skip empty messages\n        if not message:\n            return\n\n        if timeout is None:\n            timeout = self._default_timeout\n\n        # Create unique id of message\n        widget = None\n        if message_id is not None:\n            widget = self._messages.get(message_id)\n\n        if message_id is None:\n            message_id = str(uuid.uuid4())\n\n        elif message_id in self._messages_order:\n            self._messages_order.remove(message_id)\n\n        if widget is not None:\n            # NOTE: Update of message won't change paint order which should be\n            #   ok in most of cases as it matters only when messages are\n            #   animated\n            widget.update_message(message, message_type, timeout)\n        else:\n            # Create message widget\n            widget = OverlayMessageWidget(\n                message_id, message, self._widget, message_type, timeout\n            )\n            widget.close_requested.connect(self._on_message_close_request)\n            widget.show()\n\n        # Move widget outside of window\n        pos = widget.pos()\n        pos.setY(pos.y() - widget.height())\n        widget.move(pos)\n        # Store message\n        self._messages[message_id] = widget\n        self._messages_order.append(message_id)\n        # Trigger recalculation timer\n        self._recalculate_timer.start()\n\n        return message_id\n\n    def _on_message_close_request(self, message_id):\n        \"\"\"Message widget requested removement.\"\"\"\n\n        widget = self._messages.get(message_id)\n        if widget is not None:\n            # Add message to closing messages and start recalculation\n            self._closing_messages.add(message_id)\n            self._recalculate_timer.start()\n\n    def _recalculate_positions(self):\n        \"\"\"Recalculate positions of widgets.\"\"\"\n\n        # Skip if there are no messages to process\n        if not self._messages_order:\n            self._recalculate_timer.stop()\n            return\n\n        # All message widgets are in expected positions\n        all_at_place = True\n        # Starting y position\n        pos_y = self._spacing\n        # Current widget width\n        widget_width = self._widget.width()\n        max_width = widget_width - (2 * self._spacing)\n        widget_half_width = widget_width / 2\n\n        # Store message ids that should be removed\n        message_ids_to_remove = set()\n        for message_id in reversed(self._messages_order):\n            widget = self._messages[message_id]\n            pos = widget.pos()\n            # Messages to remove are moved upwards\n            if message_id in self._closing_messages:\n                bottom = pos.y() + widget.height()\n                # Add message to remove if is not visible\n                if bottom < 0 or self._move_size_remove < 1:\n                    message_ids_to_remove.add(message_id)\n                    continue\n\n                # Calculate new y position of message\n                dst_pos_y = pos.y() - self._move_size_remove\n\n            else:\n                # Calculate y position of message\n                # - use y position of previous message widget and add\n                #   move size if is not in final destination yet\n                if widget.underMouse():\n                    dst_pos_y = pos.y()\n                elif pos.y() == pos_y or self._move_size < 1:\n                    dst_pos_y = pos_y\n                elif pos.y() < pos_y:\n                    dst_pos_y = min(pos_y, pos.y() + self._move_size)\n                else:\n                    dst_pos_y = max(pos_y, pos.y() - self._move_size)\n\n            # Store if widget is in place where should be\n            if all_at_place and dst_pos_y != pos_y:\n                all_at_place = False\n\n            # Calculate ideal width and height of message widget\n            height = widget.heightForWidth(max_width)\n            w_size_hint = widget.size_hint_without_word_wrap()\n            widget.resize(min(max_width, w_size_hint.width()), height)\n\n            # Center message widget\n            size = widget.size()\n            pos_x = widget_half_width - (size.width() / 2)\n            # Move widget to destination position\n            widget.move(pos_x, dst_pos_y)\n\n            # Add message widget height and spacing for next message widget\n            pos_y += size.height() + self._spacing\n\n        # Remove widgets to remove\n        for message_id in message_ids_to_remove:\n            self._messages_order.remove(message_id)\n            self._closing_messages.remove(message_id)\n            widget = self._messages.pop(message_id)\n            widget.hide()\n            widget.deleteLater()\n\n        # Stop recalculation timer if all widgets are where should be\n        if all_at_place:\n            self._recalculate_timer.stop()\n\n    def eventFilter(self, source, event):\n        # Trigger recalculation of timer on resize of widget\n        if source is self._widget and event.type() == QtCore.QEvent.Resize:\n            self._recalculate_timer.start()\n\n        return super(MessageOverlayObject, self).eventFilter(source, event)\n"
  },
  {
    "path": "openpype/tools/utils/tasks_widget.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\nimport qtawesome\n\nfrom openpype.client import (\n    get_project,\n    get_asset_by_id,\n)\nfrom openpype.style import get_disabled_entity_icon_color\nfrom openpype.tools.utils.lib import get_task_icon\n\nfrom .views import DeselectableTreeView\n\n\nTASK_NAME_ROLE = QtCore.Qt.UserRole + 1\nTASK_TYPE_ROLE = QtCore.Qt.UserRole + 2\nTASK_ORDER_ROLE = QtCore.Qt.UserRole + 3\nTASK_ASSIGNEE_ROLE = QtCore.Qt.UserRole + 4\n\n\nclass TasksModel(QtGui.QStandardItemModel):\n    \"\"\"A model listing the tasks combined for a list of assets\"\"\"\n\n    def __init__(self, dbcon, parent=None):\n        super(TasksModel, self).__init__(parent=parent)\n        self.dbcon = dbcon\n        self.setHeaderData(\n            0, QtCore.Qt.Horizontal, \"Tasks\", QtCore.Qt.DisplayRole\n        )\n\n        self._no_tasks_icon = qtawesome.icon(\n            \"fa.exclamation-circle\",\n            color=get_disabled_entity_icon_color()\n        )\n        self._cached_icons = {}\n        self._project_doc = {}\n\n        self._empty_tasks_item = None\n        self._last_asset_id = None\n        self._loaded_project_name = None\n\n    def _context_is_valid(self):\n        if self._get_current_project():\n            return True\n        return False\n\n    def refresh(self):\n        self._refresh_project_doc()\n        self.set_asset_id(self._last_asset_id)\n\n    def _refresh_project_doc(self):\n        # Get the project configured icons from database\n        project_doc = {}\n        if self._context_is_valid():\n            project_name = self.dbcon.active_project()\n            project_doc = get_project(project_name)\n\n        self._loaded_project_name = self._get_current_project()\n        self._project_doc = project_doc\n\n    def headerData(self, section, orientation, role=None):\n        if role is None:\n            role = QtCore.Qt.EditRole\n        # Show nice labels in the header\n        if section == 0:\n            if (\n                role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole)\n                and orientation == QtCore.Qt.Horizontal\n            ):\n                return \"Tasks\"\n\n        return super(TasksModel, self).headerData(section, orientation, role)\n\n    def _get_current_project(self):\n        return self.dbcon.Session.get(\"AVALON_PROJECT\")\n\n    def set_asset_id(self, asset_id):\n        asset_doc = None\n        if asset_id and self._context_is_valid():\n            project_name = self._get_current_project()\n            asset_doc = get_asset_by_id(\n                project_name, asset_id, fields=[\"data.tasks\"]\n            )\n        self._set_asset(asset_doc)\n\n    def _get_empty_task_item(self):\n        if self._empty_tasks_item is None:\n            item = QtGui.QStandardItem(\"No task\")\n            item.setData(self._no_tasks_icon, QtCore.Qt.DecorationRole)\n            item.setFlags(QtCore.Qt.NoItemFlags)\n            self._empty_tasks_item = item\n        return self._empty_tasks_item\n\n    def _set_asset(self, asset_doc):\n        \"\"\"Set assets to track by their database id\n\n        Arguments:\n            asset_doc (dict): Asset document from MongoDB.\n        \"\"\"\n        if self._loaded_project_name != self._get_current_project():\n            self._refresh_project_doc()\n\n        asset_tasks = {}\n        self._last_asset_id = None\n        if asset_doc:\n            asset_tasks = asset_doc.get(\"data\", {}).get(\"tasks\") or {}\n            self._last_asset_id = asset_doc[\"_id\"]\n\n        root_item = self.invisibleRootItem()\n        root_item.removeRows(0, root_item.rowCount())\n\n        items = []\n\n        for task_name, task_info in asset_tasks.items():\n            task_type = task_info.get(\"type\")\n            task_order = task_info.get(\"order\")\n            icon = get_task_icon(self._project_doc, asset_doc, task_name)\n\n            task_assignees = set()\n            assignees_data = task_info.get(\"assignees\") or []\n            for assignee in assignees_data:\n                username = assignee.get(\"username\")\n                if username:\n                    task_assignees.add(username)\n\n            label = \"{} ({})\".format(task_name, task_type or \"type N/A\")\n            item = QtGui.QStandardItem(label)\n            item.setData(task_name, TASK_NAME_ROLE)\n            item.setData(task_type, TASK_TYPE_ROLE)\n            item.setData(task_order, TASK_ORDER_ROLE)\n            item.setData(task_assignees, TASK_ASSIGNEE_ROLE)\n            item.setData(icon, QtCore.Qt.DecorationRole)\n            item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)\n            items.append(item)\n\n        if not items:\n            item = QtGui.QStandardItem(\"No task\")\n            item.setData(self._no_tasks_icon, QtCore.Qt.DecorationRole)\n            item.setFlags(QtCore.Qt.NoItemFlags)\n            items.append(item)\n\n        root_item.appendRows(items)\n\n\nclass TasksProxyModel(QtCore.QSortFilterProxyModel):\n    def lessThan(self, x_index, y_index):\n        x_order = x_index.data(TASK_ORDER_ROLE)\n        y_order = y_index.data(TASK_ORDER_ROLE)\n        if x_order is not None and y_order is not None:\n            if x_order < y_order:\n                return True\n            if x_order > y_order:\n                return False\n\n        elif x_order is None and y_order is not None:\n            return True\n\n        elif y_order is None and x_order is not None:\n            return False\n\n        x_name = x_index.data(QtCore.Qt.DisplayRole)\n        y_name = y_index.data(QtCore.Qt.DisplayRole)\n        if x_name == y_name:\n            return True\n\n        if x_name == tuple(sorted((x_name, y_name)))[0]:\n            return True\n        return False\n\n\nclass TasksWidget(QtWidgets.QWidget):\n    \"\"\"Widget showing active Tasks\"\"\"\n\n    task_changed = QtCore.Signal()\n\n    def __init__(self, dbcon, parent=None):\n        self._dbcon = dbcon\n\n        super(TasksWidget, self).__init__(parent)\n\n        tasks_view = DeselectableTreeView(self)\n        tasks_view.setIndentation(0)\n        tasks_view.setSortingEnabled(True)\n        tasks_view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\n\n        header_view = tasks_view.header()\n        header_view.setSortIndicator(0, QtCore.Qt.AscendingOrder)\n\n        tasks_model = self._create_source_model()\n        tasks_proxy = self._create_proxy_model(tasks_model)\n        tasks_view.setModel(tasks_proxy)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(tasks_view)\n\n        selection_model = tasks_view.selectionModel()\n        selection_model.selectionChanged.connect(self._on_task_change)\n\n        self._tasks_model = tasks_model\n        self._tasks_proxy = tasks_proxy\n        self._tasks_view = tasks_view\n\n        self._last_selected_task_name = None\n\n    def _create_source_model(self):\n        \"\"\"Create source model of tasks widget.\n\n        Model must have available 'refresh' method and 'set_asset_id' to change\n        context of asset.\n        \"\"\"\n        return TasksModel(self._dbcon)\n\n    def _create_proxy_model(self, source_model):\n        proxy = TasksProxyModel()\n        proxy.setSourceModel(source_model)\n        return proxy\n\n    def refresh(self):\n        self._tasks_model.refresh()\n\n    def set_asset_id(self, asset_id):\n        # Try and preserve the last selected task and reselect it\n        # after switching assets. If there's no currently selected\n        # asset keep whatever the \"last selected\" was prior to it.\n        current = self.get_selected_task_name()\n        if current:\n            self._last_selected_task_name = current\n\n        self._tasks_model.set_asset_id(asset_id)\n\n        if self._last_selected_task_name:\n            self.select_task_name(self._last_selected_task_name)\n\n        # Force a task changed emit.\n        self.task_changed.emit()\n\n    def _clear_selection(self):\n        selection_model = self._tasks_view.selectionModel()\n        selection_model.clearSelection()\n\n    def select_task_name(self, task_name):\n        \"\"\"Select a task by name.\n\n        If the task does not exist in the current model then selection is only\n        cleared.\n\n        Args:\n            task (str): Name of the task to select.\n\n        \"\"\"\n        task_view_model = self._tasks_view.model()\n        if not task_view_model:\n            return\n\n        # Clear selection\n        selection_model = self._tasks_view.selectionModel()\n        selection_model.clearSelection()\n\n        # Select the task\n        mode = (\n            QtCore.QItemSelectionModel.Select\n            | QtCore.QItemSelectionModel.Rows\n        )\n        for row in range(task_view_model.rowCount()):\n            index = task_view_model.index(row, 0)\n            name = index.data(TASK_NAME_ROLE)\n            if name == task_name:\n                selection_model.select(index, mode)\n\n                # Set the currently active index\n                self._tasks_view.setCurrentIndex(index)\n                break\n\n        last_selected_task_name = self.get_selected_task_name()\n        if last_selected_task_name:\n            self._last_selected_task_name = last_selected_task_name\n\n    def get_selected_task_name(self):\n        \"\"\"Return name of task at current index (selected)\n\n        Returns:\n            str: Name of the current task.\n\n        \"\"\"\n        index = self._tasks_view.currentIndex()\n        selection_model = self._tasks_view.selectionModel()\n        if index.isValid() and selection_model.isSelected(index):\n            return index.data(TASK_NAME_ROLE)\n        return None\n\n    def get_selected_task_type(self):\n        index = self._tasks_view.currentIndex()\n        selection_model = self._tasks_view.selectionModel()\n        if index.isValid() and selection_model.isSelected(index):\n            return index.data(TASK_TYPE_ROLE)\n        return None\n\n    def _on_task_change(self):\n        self.task_changed.emit()\n"
  },
  {
    "path": "openpype/tools/utils/thumbnail_paint_widget.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.style import get_objected_colors\n\nfrom .lib import paint_image_with_color\nfrom .images import get_image\n\n\nclass ThumbnailPainterWidget(QtWidgets.QWidget):\n    \"\"\"Widget for painting of thumbnails.\n\n    The widget use is to paint thumbnail or multiple thumbnails in a defined\n    area. Is not meant to show them in a grid but in overlay.\n\n    It is expected that there is a logic that will provide thumbnails to\n    paint and set them using 'set_current_thumbnails' or\n    'set_current_thumbnail_paths'.\n    \"\"\"\n\n    width_ratio = 3.0\n    height_ratio = 2.0\n    border_width = 1\n    max_thumbnails = 3\n    offset_sep = 4\n    checker_boxes_count = 20\n\n    def __init__(self, parent):\n        super(ThumbnailPainterWidget, self).__init__(parent)\n\n        border_color = get_objected_colors(\"bg-buttons\").get_qcolor()\n        thumbnail_bg_color = get_objected_colors(\"bg-view\").get_qcolor()\n\n        default_image = get_image(\"thumbnail.png\")\n        default_pix = paint_image_with_color(default_image, border_color)\n\n        self._border_color = border_color\n        self._thumbnail_bg_color = thumbnail_bg_color\n        self._default_pix = default_pix\n\n        self._cached_pix = None\n        self._current_pixes = None\n        self._has_pixes = False\n\n        self._bg_color = QtCore.Qt.transparent\n        self._use_checker = True\n        self._checker_color_1 = QtGui.QColor(89, 89, 89)\n        self._checker_color_2 = QtGui.QColor(188, 187, 187)\n\n    def set_background_color(self, color):\n        self._bg_color = color\n        self.repaint()\n\n    def set_use_checkboard(self, use_checker):\n        if self._use_checker is use_checker:\n            return\n        self._use_checker = use_checker\n        self.repaint()\n\n    def set_checker_colors(self, color_1, color_2):\n        self._checker_color_1 = color_1\n        self._checker_color_2 = color_2\n        self.repaint()\n\n    def set_border_color(self, color):\n        \"\"\"Change border color.\n\n        Args:\n            color (QtGui.QColor): Color to set.\n        \"\"\"\n\n        self._border_color = color\n        self._default_pix = None\n        self.clear_cache()\n\n    def set_thumbnail_bg_color(self, color):\n        \"\"\"Change background color.\n\n        Args:\n            color (QtGui.QColor): Color to set.\n        \"\"\"\n\n        self._thumbnail_bg_color = color\n        self.clear_cache()\n\n    @property\n    def has_pixes(self):\n        \"\"\"Has set thumbnails.\n\n        Returns:\n            bool: True if widget has thumbnails to paint.\n        \"\"\"\n\n        return self._has_pixes\n\n    def clear_cache(self):\n        \"\"\"Clear cache of resized thumbnails and repaint widget.\"\"\"\n\n        self._cached_pix = None\n        self.repaint()\n\n    def set_current_thumbnails(self, pixmaps=None):\n        \"\"\"Set current thumbnails.\n\n        Args:\n            pixmaps (Optional[List[QtGui.QPixmap]]): List of pixmaps.\n        \"\"\"\n\n        self._current_pixes = pixmaps or None\n        self._has_pixes = self._current_pixes is not None\n        self.clear_cache()\n\n    def set_current_thumbnail_paths(self, thumbnail_paths=None):\n        \"\"\"Set current thumbnails.\n\n        Set current thumbnails using paths to a files.\n\n        Args:\n            thumbnail_paths (Optional[List[str]]): List of paths to thumbnail\n                sources.\n        \"\"\"\n\n        pixes = []\n        if thumbnail_paths:\n            for thumbnail_path in thumbnail_paths:\n                pixes.append(QtGui.QPixmap(thumbnail_path))\n\n        self.set_current_thumbnails(pixes)\n\n    def paintEvent(self, event):\n        if self._cached_pix is None:\n            self._cache_pix()\n\n        painter = QtGui.QPainter()\n        painter.begin(self)\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n        painter.drawPixmap(0, 0, self._cached_pix)\n        painter.end()\n\n    def resizeEvent(self, event):\n        self._cached_pix = None\n        super(ThumbnailPainterWidget, self).resizeEvent(event)\n\n    def _get_default_pix(self):\n        if self._default_pix is None:\n            default_image = get_image(\"thumbnail\")\n            default_pix = paint_image_with_color(\n                default_image, self._border_color)\n            self._default_pix = default_pix\n        return self._default_pix\n\n    def _paint_tile(self, width, height):\n        if not self._use_checker:\n            tile_pix = QtGui.QPixmap(width, width)\n            tile_pix.fill(self._bg_color)\n            return tile_pix\n\n        checker_size = int(float(width) / self.checker_boxes_count)\n        if checker_size < 1:\n            checker_size = 1\n\n        checker_pix = QtGui.QPixmap(checker_size * 2, checker_size * 2)\n        checker_pix.fill(QtCore.Qt.transparent)\n        checker_painter = QtGui.QPainter()\n        checker_painter.begin(checker_pix)\n        checker_painter.setPen(QtCore.Qt.NoPen)\n        checker_painter.setBrush(self._checker_color_1)\n        checker_painter.drawRect(\n            0, 0, checker_pix.width(), checker_pix.height()\n        )\n        checker_painter.setBrush(self._checker_color_2)\n        checker_painter.drawRect(\n            0, 0, checker_size, checker_size\n        )\n        checker_painter.drawRect(\n            checker_size, checker_size, checker_size, checker_size\n        )\n        checker_painter.end()\n        return checker_pix\n\n    def _paint_default_pix(self, pix_width, pix_height):\n        full_border_width = 2 * self.border_width\n        width = pix_width - full_border_width\n        height = pix_height - full_border_width\n        if width > 100:\n            width = int(width * 0.6)\n            height = int(height * 0.6)\n\n        scaled_pix = self._get_default_pix().scaled(\n            width,\n            height,\n            QtCore.Qt.KeepAspectRatio,\n            QtCore.Qt.SmoothTransformation\n        )\n        pos_x = int(\n            (pix_width - scaled_pix.width()) / 2\n        )\n        pos_y = int(\n            (pix_height - scaled_pix.height()) / 2\n        )\n        new_pix = QtGui.QPixmap(pix_width, pix_height)\n        new_pix.fill(QtCore.Qt.transparent)\n        pix_painter = QtGui.QPainter()\n        pix_painter.begin(new_pix)\n        render_hints = (\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n        )\n        if hasattr(QtGui.QPainter, \"HighQualityAntialiasing\"):\n            render_hints |= QtGui.QPainter.HighQualityAntialiasing\n\n        pix_painter.setRenderHints(render_hints)\n        pix_painter.drawPixmap(pos_x, pos_y, scaled_pix)\n        pix_painter.end()\n        return new_pix\n\n    def _draw_thumbnails(self, thumbnails, pix_width, pix_height):\n        full_border_width = 2 * self.border_width\n\n        checker_pix = self._paint_tile(pix_width, pix_height)\n\n        backgrounded_images = []\n        for src_pix in thumbnails:\n            scaled_pix = src_pix.scaled(\n                pix_width - full_border_width,\n                pix_height - full_border_width,\n                QtCore.Qt.KeepAspectRatio,\n                QtCore.Qt.SmoothTransformation\n            )\n            pos_x = int(\n                (pix_width - scaled_pix.width()) / 2\n            )\n            pos_y = int(\n                (pix_height - scaled_pix.height()) / 2\n            )\n\n            new_pix = QtGui.QPixmap(pix_width, pix_height)\n            new_pix.fill(QtCore.Qt.transparent)\n            pix_painter = QtGui.QPainter()\n            pix_painter.begin(new_pix)\n            render_hints = (\n                QtGui.QPainter.Antialiasing\n                | QtGui.QPainter.SmoothPixmapTransform\n            )\n            if hasattr(QtGui.QPainter, \"HighQualityAntialiasing\"):\n                render_hints |= QtGui.QPainter.HighQualityAntialiasing\n            pix_painter.setRenderHints(render_hints)\n\n            tiled_rect = QtCore.QRectF(\n                pos_x, pos_y, scaled_pix.width(), scaled_pix.height()\n            )\n            pix_painter.drawTiledPixmap(\n                tiled_rect,\n                checker_pix,\n                QtCore.QPointF(0.0, 0.0)\n            )\n            pix_painter.drawPixmap(pos_x, pos_y, scaled_pix)\n            pix_painter.end()\n            backgrounded_images.append(new_pix)\n        return backgrounded_images\n\n    def _paint_dash_line(self, painter, rect):\n        pen = QtGui.QPen()\n        pen.setWidth(1)\n        pen.setBrush(QtCore.Qt.darkGray)\n        pen.setStyle(QtCore.Qt.DashLine)\n\n        new_rect = rect.adjusted(1, 1, -1, -1)\n        painter.setPen(pen)\n        painter.setBrush(QtCore.Qt.transparent)\n        # painter.drawRect(rect)\n        painter.drawRect(new_rect)\n\n    def _cache_pix(self):\n        rect = self.rect()\n        rect_width = rect.width()\n        rect_height = rect.height()\n\n        pix_x_offset = 0\n        pix_y_offset = 0\n        expected_height = int(\n            (rect_width / self.width_ratio) * self.height_ratio\n        )\n        if expected_height > rect_height:\n            expected_height = rect_height\n            expected_width = int(\n                (rect_height / self.height_ratio) * self.width_ratio\n            )\n            pix_x_offset = (rect_width - expected_width) / 2\n        else:\n            expected_width = rect_width\n            pix_y_offset = (rect_height - expected_height) / 2\n\n        if self._current_pixes is None:\n            used_default_pix = True\n            pixes_to_draw = None\n            pixes_len = 1\n        else:\n            used_default_pix = False\n            pixes_to_draw = self._current_pixes\n            if len(pixes_to_draw) > self.max_thumbnails:\n                pixes_to_draw = pixes_to_draw[:-self.max_thumbnails]\n            pixes_len = len(pixes_to_draw)\n\n        width_offset, height_offset = self._get_pix_offset_size(\n            expected_width, expected_height, pixes_len\n        )\n        pix_width = expected_width - width_offset\n        pix_height = expected_height - height_offset\n\n        if used_default_pix:\n            thumbnail_images = [self._paint_default_pix(pix_width, pix_height)]\n        else:\n            thumbnail_images = self._draw_thumbnails(\n                pixes_to_draw, pix_width, pix_height\n            )\n\n        if pixes_len == 1:\n            width_offset_part = 0\n            height_offset_part = 0\n        else:\n            width_offset_part = int(float(width_offset) / (pixes_len - 1))\n            height_offset_part = int(float(height_offset) / (pixes_len - 1))\n        full_width_offset = width_offset + pix_x_offset\n\n        final_pix = QtGui.QPixmap(rect_width, rect_height)\n        final_pix.fill(QtCore.Qt.transparent)\n\n        bg_pen = QtGui.QPen()\n        bg_pen.setWidth(self.border_width)\n        bg_pen.setColor(self._border_color)\n\n        final_painter = QtGui.QPainter()\n        final_painter.begin(final_pix)\n        render_hints = (\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n        )\n        if hasattr(QtGui.QPainter, \"HighQualityAntialiasing\"):\n            render_hints |= QtGui.QPainter.HighQualityAntialiasing\n\n        final_painter.setRenderHints(render_hints)\n\n        final_painter.setBrush(QtGui.QBrush(self._thumbnail_bg_color))\n        final_painter.setPen(bg_pen)\n        final_painter.drawRect(rect)\n\n        for idx, pix in enumerate(thumbnail_images):\n            x_offset = full_width_offset - (width_offset_part * idx)\n            y_offset = (height_offset_part * idx) + pix_y_offset\n            final_painter.drawPixmap(x_offset, y_offset, pix)\n\n        # Draw drop enabled dashes\n        if used_default_pix:\n            self._paint_dash_line(final_painter, rect)\n\n        final_painter.end()\n\n        self._cached_pix = final_pix\n\n    def _get_pix_offset_size(self, width, height, image_count):\n        if image_count == 1:\n            return 0, 0\n\n        part_width = width / self.offset_sep\n        part_height = height / self.offset_sep\n        return part_width, part_height\n"
  },
  {
    "path": "openpype/tools/utils/views.py",
    "content": "from openpype.resources import get_image_path\nfrom openpype.tools.flickcharm import FlickCharm\n\nfrom qtpy import QtWidgets, QtCore, QtGui, QtSvg\n\n\nclass DeselectableTreeView(QtWidgets.QTreeView):\n    \"\"\"A tree view that deselects on clicking on an empty area in the view\"\"\"\n\n    def mousePressEvent(self, event):\n\n        index = self.indexAt(event.pos())\n        if not index.isValid():\n            # clear the selection\n            self.clearSelection()\n            # clear the current index\n            self.setCurrentIndex(QtCore.QModelIndex())\n\n        QtWidgets.QTreeView.mousePressEvent(self, event)\n\n\nclass TreeViewSpinner(QtWidgets.QTreeView):\n    size = 160\n\n    def __init__(self, parent=None):\n        super(TreeViewSpinner, self).__init__(parent=parent)\n\n        loading_image_path = get_image_path(\"spinner-200.svg\")\n\n        self.spinner = QtSvg.QSvgRenderer(loading_image_path)\n\n        self.is_loading = False\n        self.is_empty = True\n\n    def paint_loading(self, event):\n        rect = event.rect()\n        rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight())\n        rect.moveTo(\n            rect.x() + rect.width() / 2 - self.size / 2,\n            rect.y() + rect.height() / 2 - self.size / 2\n        )\n        rect.setSize(QtCore.QSizeF(self.size, self.size))\n        painter = QtGui.QPainter(self.viewport())\n        self.spinner.render(painter, rect)\n\n    def paint_empty(self, event):\n        painter = QtGui.QPainter(self.viewport())\n        rect = event.rect()\n        rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight())\n        qtext_opt = QtGui.QTextOption(\n            QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter\n        )\n        painter.drawText(rect, \"No Data\", qtext_opt)\n\n    def paintEvent(self, event):\n        if self.is_loading:\n            self.paint_loading(event)\n        elif self.is_empty:\n            self.paint_empty(event)\n        else:\n            super(TreeViewSpinner, self).paintEvent(event)\n\n\nclass TreeView(QtWidgets.QTreeView):\n    \"\"\"Ultimate TreeView with flick charm and double click signals.\n\n    Tree view have deselectable mode, which allows to deselect items by\n    clicking on item area without any items.\n\n    Todos:\n        Add refresh animation.\n    \"\"\"\n\n    double_clicked = QtCore.Signal(QtGui.QMouseEvent)\n\n    def __init__(self, *args, **kwargs):\n        super(TreeView, self).__init__(*args, **kwargs)\n        self._deselectable = False\n\n        self._flick_charm_activated = False\n        self._flick_charm = FlickCharm(parent=self)\n        self._before_flick_scroll_mode = None\n\n    def is_deselectable(self):\n        return self._deselectable\n\n    def set_deselectable(self, deselectable):\n        self._deselectable = deselectable\n\n    deselectable = property(is_deselectable, set_deselectable)\n\n    def mousePressEvent(self, event):\n        if self._deselectable:\n            index = self.indexAt(event.pos())\n            if not index.isValid():\n                # clear the selection\n                self.clearSelection()\n                # clear the current index\n                self.setCurrentIndex(QtCore.QModelIndex())\n        super(TreeView, self).mousePressEvent(event)\n\n    def mouseDoubleClickEvent(self, event):\n        self.double_clicked.emit(event)\n\n        return super(TreeView, self).mouseDoubleClickEvent(event)\n\n    def activate_flick_charm(self):\n        if self._flick_charm_activated:\n            return\n        self._flick_charm_activated = True\n        self._before_flick_scroll_mode = self.verticalScrollMode()\n        self._flick_charm.activateOn(self)\n        self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)\n\n    def deactivate_flick_charm(self):\n        if not self._flick_charm_activated:\n            return\n        self._flick_charm_activated = False\n        self._flick_charm.deactivateFrom(self)\n        if self._before_flick_scroll_mode is not None:\n            self.setVerticalScrollMode(self._before_flick_scroll_mode)\n"
  },
  {
    "path": "openpype/tools/utils/widgets.py",
    "content": "import logging\n\nfrom qtpy import QtWidgets, QtCore, QtGui\nimport qargparse\nimport qtawesome\n\nfrom openpype.style import (\n    get_objected_colors,\n    get_style_image_path,\n    get_default_tools_icon_color,\n)\nfrom openpype.lib.attribute_definitions import AbstractAttrDef\n\nfrom .lib import get_qta_icon_by_name_and_color\n\nlog = logging.getLogger(__name__)\n\n\nclass FocusSpinBox(QtWidgets.QSpinBox):\n    \"\"\"QSpinBox which allow scroll wheel changes only in active state.\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(FocusSpinBox, self).__init__(*args, **kwargs)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n    def wheelEvent(self, event):\n        if not self.hasFocus():\n            event.ignore()\n        else:\n            super(FocusSpinBox, self).wheelEvent(event)\n\n\nclass FocusDoubleSpinBox(QtWidgets.QDoubleSpinBox):\n    \"\"\"QDoubleSpinBox which allow scroll wheel changes only in active state.\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(FocusDoubleSpinBox, self).__init__(*args, **kwargs)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n    def wheelEvent(self, event):\n        if not self.hasFocus():\n            event.ignore()\n        else:\n            super(FocusDoubleSpinBox, self).wheelEvent(event)\n\n\nclass ComboBox(QtWidgets.QComboBox):\n    \"\"\"Base of combobox with pre-implement changes used in tools.\n\n    Combobox is using styled delegate by default so stylesheets are propagated.\n\n    Items are not changed on scroll until the combobox is in focus.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(ComboBox, self).__init__(*args, **kwargs)\n        delegate = QtWidgets.QStyledItemDelegate()\n        self.setItemDelegate(delegate)\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        self._delegate = delegate\n\n    def wheelEvent(self, event):\n        if self.hasFocus():\n            return super(ComboBox, self).wheelEvent(event)\n\n\nclass CustomTextComboBox(ComboBox):\n    \"\"\"Combobox which can have different text showed.\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        self._custom_text = None\n        super(CustomTextComboBox, self).__init__(*args, **kwargs)\n\n    def set_custom_text(self, text=None):\n        if self._custom_text != text:\n            self._custom_text = text\n            self.repaint()\n\n    def paintEvent(self, event):\n        painter = QtWidgets.QStylePainter(self)\n        option = QtWidgets.QStyleOptionComboBox()\n        self.initStyleOption(option)\n        if self._custom_text is not None:\n            option.currentText = self._custom_text\n        painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, option)\n        painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option)\n\n\nclass PlaceholderLineEdit(QtWidgets.QLineEdit):\n    \"\"\"Set placeholder color of QLineEdit in Qt 5.12 and higher.\"\"\"\n    def __init__(self, *args, **kwargs):\n        super(PlaceholderLineEdit, self).__init__(*args, **kwargs)\n        # Change placeholder palette color\n        if hasattr(QtGui.QPalette, \"PlaceholderText\"):\n            filter_palette = self.palette()\n            color_obj = get_objected_colors(\"font\")\n            color = color_obj.get_qcolor()\n            color.setAlpha(67)\n            filter_palette.setColor(\n                QtGui.QPalette.PlaceholderText,\n                color\n            )\n            self.setPalette(filter_palette)\n\n\nclass ExpandingTextEdit(QtWidgets.QTextEdit):\n    \"\"\"QTextEdit which does not have sroll area but expands height.\"\"\"\n\n    def __init__(self, parent=None):\n        super(ExpandingTextEdit, self).__init__(parent)\n\n        size_policy = self.sizePolicy()\n        size_policy.setHeightForWidth(True)\n        size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Preferred)\n        self.setSizePolicy(size_policy)\n\n        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)\n\n        doc = self.document()\n        doc.contentsChanged.connect(self._on_doc_change)\n\n    def _on_doc_change(self):\n        self.updateGeometry()\n\n    def hasHeightForWidth(self):\n        return True\n\n    def heightForWidth(self, width):\n        margins = self.contentsMargins()\n\n        document_width = 0\n        if width >= margins.left() + margins.right():\n            document_width = width - margins.left() - margins.right()\n\n        document = self.document().clone()\n        document.setTextWidth(document_width)\n\n        return margins.top() + document.size().height() + margins.bottom()\n\n    def sizeHint(self):\n        width = super(ExpandingTextEdit, self).sizeHint().width()\n        return QtCore.QSize(width, self.heightForWidth(width))\n\n\nclass BaseClickableFrame(QtWidgets.QFrame):\n    \"\"\"Widget that catch left mouse click and can trigger a callback.\n\n    Callback is defined by overriding `_mouse_release_callback`.\n    \"\"\"\n    def __init__(self, parent):\n        super(BaseClickableFrame, self).__init__(parent)\n\n        self._mouse_pressed = False\n\n    def _mouse_release_callback(self):\n        pass\n\n    def mousePressEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self._mouse_pressed = True\n        super(BaseClickableFrame, self).mousePressEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        if self._mouse_pressed:\n            self._mouse_pressed = False\n            if self.rect().contains(event.pos()):\n                self._mouse_release_callback()\n\n        super(BaseClickableFrame, self).mouseReleaseEvent(event)\n\n\nclass ClickableFrame(BaseClickableFrame):\n    \"\"\"Extended clickable frame which triggers 'clicked' signal.\"\"\"\n    clicked = QtCore.Signal()\n\n    def _mouse_release_callback(self):\n        self.clicked.emit()\n\n\nclass ClickableLabel(QtWidgets.QLabel):\n    \"\"\"Label that catch left mouse click and can trigger 'clicked' signal.\"\"\"\n    clicked = QtCore.Signal()\n\n    def __init__(self, parent):\n        super(ClickableLabel, self).__init__(parent)\n\n        self._mouse_pressed = False\n\n    def mousePressEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self._mouse_pressed = True\n        super(ClickableLabel, self).mousePressEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        if self._mouse_pressed:\n            self._mouse_pressed = False\n            if self.rect().contains(event.pos()):\n                self.clicked.emit()\n\n        super(ClickableLabel, self).mouseReleaseEvent(event)\n\n\nclass ExpandBtnLabel(QtWidgets.QLabel):\n    \"\"\"Label showing expand icon meant for ExpandBtn.\"\"\"\n    state_changed = QtCore.Signal()\n\n\n    def __init__(self, parent):\n        super(ExpandBtnLabel, self).__init__(parent)\n        self._source_collapsed_pix = self._create_collapsed_pixmap()\n        self._source_expanded_pix = self._create_expanded_pixmap()\n\n        self._current_image = self._source_collapsed_pix\n        self._collapsed = True\n\n    def _create_collapsed_pixmap(self):\n        return QtGui.QPixmap(\n            get_style_image_path(\"branch_closed\")\n        )\n\n    def _create_expanded_pixmap(self):\n        return QtGui.QPixmap(\n            get_style_image_path(\"branch_open\")\n        )\n\n    @property\n    def collapsed(self):\n        return self._collapsed\n\n    def set_collapsed(self, collapsed=None):\n        if collapsed is None:\n            collapsed = not self._collapsed\n        if self._collapsed == collapsed:\n            return\n        self._collapsed = collapsed\n        if collapsed:\n            self._current_image = self._source_collapsed_pix\n        else:\n            self._current_image = self._source_expanded_pix\n        self._set_resized_pix()\n        self.state_changed.emit()\n\n    def resizeEvent(self, event):\n        self._set_resized_pix()\n        super(ExpandBtnLabel, self).resizeEvent(event)\n\n    def _set_resized_pix(self):\n        size = int(self.fontMetrics().height() / 2)\n        if size < 1:\n            size = 1\n        size += size % 2\n        self.setPixmap(\n            self._current_image.scaled(\n                size,\n                size,\n                QtCore.Qt.KeepAspectRatio,\n                QtCore.Qt.SmoothTransformation\n            )\n        )\n\n\nclass ExpandBtn(ClickableFrame):\n    state_changed = QtCore.Signal()\n\n    def __init__(self, parent=None):\n        super(ExpandBtn, self).__init__(parent)\n\n        pixmap_label = self._create_pix_widget(self)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(pixmap_label)\n\n        pixmap_label.state_changed.connect(self.state_changed)\n\n        self._pixmap_label = pixmap_label\n\n    def _create_pix_widget(self, parent=None):\n        if parent is None:\n            parent = self\n        return ExpandBtnLabel(parent)\n\n    @property\n    def collapsed(self):\n        return self._pixmap_label.collapsed\n\n    def set_collapsed(self, collapsed=None):\n        self._pixmap_label.set_collapsed(collapsed)\n\n\nclass ClassicExpandBtnLabel(ExpandBtnLabel):\n    def _create_collapsed_pixmap(self):\n        return QtGui.QPixmap(\n            get_style_image_path(\"right_arrow\")\n        )\n\n    def _create_expanded_pixmap(self):\n        return QtGui.QPixmap(\n            get_style_image_path(\"down_arrow\")\n        )\n\n\nclass ClassicExpandBtn(ExpandBtn):\n    \"\"\"Same as 'ExpandBtn' but with arrow images.\"\"\"\n\n    def _create_pix_widget(self, parent=None):\n        if parent is None:\n            parent = self\n        return ClassicExpandBtnLabel(parent)\n\n\nclass ImageButton(QtWidgets.QPushButton):\n    \"\"\"PushButton with icon and size of font.\n\n    Using font metrics height as icon size reference.\n\n    TODO:\n    - handle changes of screen (different resolution)\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(ImageButton, self).__init__(*args, **kwargs)\n        self.setObjectName(\"ImageButton\")\n\n    def _change_size(self):\n        font_height = self.fontMetrics().height()\n        self.setIconSize(QtCore.QSize(font_height, font_height))\n\n    def showEvent(self, event):\n        super(ImageButton, self).showEvent(event)\n\n        self._change_size()\n\n    def sizeHint(self):\n        return self.iconSize()\n\n\nclass IconButton(QtWidgets.QPushButton):\n    \"\"\"PushButton with icon and size of font.\n\n    Using font metrics height as icon size reference.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(IconButton, self).__init__(*args, **kwargs)\n        self.setObjectName(\"IconButton\")\n\n    def sizeHint(self):\n        result = super(IconButton, self).sizeHint()\n        icon_h = self.iconSize().height()\n        font_height = self.fontMetrics().height()\n        text_set = bool(self.text())\n        if not text_set and icon_h < font_height:\n            new_size = result.height() - icon_h + font_height\n            result.setHeight(new_size)\n            result.setWidth(new_size)\n\n        return result\n\n\nclass PixmapLabel(QtWidgets.QLabel):\n    \"\"\"Label resizing image to height of font.\"\"\"\n    def __init__(self, pixmap, parent):\n        super(PixmapLabel, self).__init__(parent)\n        self._empty_pixmap = QtGui.QPixmap(0, 0)\n        self._source_pixmap = pixmap\n\n        self._last_width = 0\n        self._last_height = 0\n\n    def set_source_pixmap(self, pixmap):\n        \"\"\"Change source image.\"\"\"\n        self._source_pixmap = pixmap\n        self._set_resized_pix()\n\n    def _get_pix_size(self):\n        size = self.fontMetrics().height()\n        size += size % 2\n        return size, size\n\n    def minimumSizeHint(self):\n        width, height = self._get_pix_size()\n        if width != self._last_width or height != self._last_height:\n            self._set_resized_pix()\n        return QtCore.QSize(width, height)\n\n    def _set_resized_pix(self):\n        if self._source_pixmap is None:\n            self.setPixmap(self._empty_pixmap)\n            return\n        width, height = self._get_pix_size()\n        self.setPixmap(\n            self._source_pixmap.scaled(\n                width,\n                height,\n                QtCore.Qt.KeepAspectRatio,\n                QtCore.Qt.SmoothTransformation\n            )\n        )\n        self._last_width = width\n        self._last_height = height\n\n    def resizeEvent(self, event):\n        self._set_resized_pix()\n        super(PixmapLabel, self).resizeEvent(event)\n\n\nclass PixmapButtonPainter(QtWidgets.QWidget):\n    def __init__(self, pixmap, parent):\n        super(PixmapButtonPainter, self).__init__(parent)\n\n        self._pixmap = pixmap\n        self._cached_pixmap = None\n        self._disabled = False\n\n    def resizeEvent(self, event):\n        super(PixmapButtonPainter, self).resizeEvent(event)\n        self._cached_pixmap = None\n        self.repaint()\n\n    def set_enabled(self, enabled):\n        if self._disabled != enabled:\n            return\n        self._disabled = not enabled\n        self.repaint()\n\n    def set_pixmap(self, pixmap):\n        self._pixmap = pixmap\n        self._cached_pixmap = None\n\n        self.repaint()\n\n    def _cache_pixmap(self):\n        size = self.size()\n        self._cached_pixmap = self._pixmap.scaled(\n            size.width(),\n            size.height(),\n            QtCore.Qt.KeepAspectRatio,\n            QtCore.Qt.SmoothTransformation\n        )\n\n    def paintEvent(self, event):\n        painter = QtGui.QPainter()\n        painter.begin(self)\n        if self._pixmap is None:\n            painter.end()\n            return\n\n        render_hints = (\n            QtGui.QPainter.Antialiasing\n            | QtGui.QPainter.SmoothPixmapTransform\n        )\n        if hasattr(QtGui.QPainter, \"HighQualityAntialiasing\"):\n            render_hints |= QtGui.QPainter.HighQualityAntialiasing\n\n        painter.setRenderHints(render_hints)\n        if self._cached_pixmap is None:\n            self._cache_pixmap()\n\n        if self._disabled:\n            painter.setOpacity(0.5)\n        painter.drawPixmap(0, 0, self._cached_pixmap)\n\n        painter.end()\n\n\nclass PixmapButton(ClickableFrame):\n    def __init__(self, pixmap=None, parent=None):\n        super(PixmapButton, self).__init__(parent)\n\n        button_painter = PixmapButtonPainter(pixmap, self)\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(2, 2, 2, 2)\n\n        self._button_painter = button_painter\n\n    def setContentsMargins(self, *args):\n        layout = self.layout()\n        layout.setContentsMargins(*args)\n        self._update_painter_geo()\n\n    def setEnabled(self, enabled):\n        self._button_painter.set_enabled(enabled)\n        super(PixmapButton, self).setEnabled(enabled)\n\n    def set_pixmap(self, pixmap):\n        self._button_painter.set_pixmap(pixmap)\n\n    def sizeHint(self):\n        font_height = self.fontMetrics().height()\n        return QtCore.QSize(font_height, font_height)\n\n    def resizeEvent(self, event):\n        super(PixmapButton, self).resizeEvent(event)\n        self._update_painter_geo()\n\n    def showEvent(self, event):\n        super(PixmapButton, self).showEvent(event)\n        self._update_painter_geo()\n\n    def _update_painter_geo(self):\n        size = self.size()\n        layout = self.layout()\n        left, top, right, bottom = layout.getContentsMargins()\n        self._button_painter.setGeometry(\n            left,\n            top,\n            size.width() - (left + right),\n            size.height() - (top + bottom)\n        )\n\n\nclass OptionalMenu(QtWidgets.QMenu):\n    \"\"\"A subclass of `QtWidgets.QMenu` to work with `OptionalAction`\n\n    This menu has reimplemented `mouseReleaseEvent`, `mouseMoveEvent` and\n    `leaveEvent` to provide better action highlighting and triggering for\n    actions that were instances of `QtWidgets.QWidgetAction`.\n\n    \"\"\"\n    def mouseReleaseEvent(self, event):\n        \"\"\"Emit option clicked signal if mouse released on it\"\"\"\n        active = self.actionAt(event.pos())\n        if active and active.use_option:\n            option = active.widget.option\n            if option.is_hovered(event.globalPos()):\n                option.clicked.emit()\n        super(OptionalMenu, self).mouseReleaseEvent(event)\n\n    def mouseMoveEvent(self, event):\n        \"\"\"Add highlight to active action\"\"\"\n        active = self.actionAt(event.pos())\n        for action in self.actions():\n            action.set_highlight(action is active, event.globalPos())\n        super(OptionalMenu, self).mouseMoveEvent(event)\n\n    def leaveEvent(self, event):\n        \"\"\"Remove highlight from all actions\"\"\"\n        for action in self.actions():\n            action.set_highlight(False)\n        super(OptionalMenu, self).leaveEvent(event)\n\n\nclass OptionalAction(QtWidgets.QWidgetAction):\n    \"\"\"Menu action with option box\n\n    A menu action like Maya's menu item with option box, implemented by\n    subclassing `QtWidgets.QWidgetAction`.\n\n    \"\"\"\n\n    def __init__(self, label, icon, use_option, parent):\n        super(OptionalAction, self).__init__(parent)\n        self.label = label\n        self.icon = icon\n        self.use_option = use_option\n        self.option_tip = \"\"\n        self.optioned = False\n        self.widget = None\n\n    def createWidget(self, parent):\n        widget = OptionalActionWidget(self.label, parent)\n        self.widget = widget\n\n        if self.icon:\n            widget.setIcon(self.icon)\n\n        if self.use_option:\n            widget.option.clicked.connect(self.on_option)\n            widget.option.setToolTip(self.option_tip)\n        else:\n            widget.option.setVisible(False)\n\n        return widget\n\n    def set_option_tip(self, options):\n        sep = \"\\n\\n\"\n        if not options or not isinstance(options[0], AbstractAttrDef):\n            mak = (lambda opt: opt[\"name\"] + \" :\\n    \" + opt[\"help\"])\n            self.option_tip = sep.join(mak(opt) for opt in options)\n            return\n\n        option_items = []\n        for option in options:\n            option_lines = []\n            if option.label:\n                option_lines.append(\n                    \"{} ({}) :\".format(option.label, option.key)\n                )\n            else:\n                option_lines.append(\"{} :\".format(option.key))\n\n            if option.tooltip:\n                option_lines.append(\"    - {}\".format(option.tooltip))\n            option_items.append(\"\\n\".join(option_lines))\n\n        self.option_tip = sep.join(option_items)\n\n    def on_option(self):\n        self.optioned = True\n\n    def set_highlight(self, state, global_pos=None):\n        option_state = False\n        if self.use_option:\n            option_state = self.widget.option.is_hovered(global_pos)\n        self.widget.set_hover_properties(state, option_state)\n\n\nclass OptionalActionWidget(QtWidgets.QWidget):\n    \"\"\"Main widget class for `OptionalAction`\"\"\"\n\n    def __init__(self, label, parent=None):\n        super(OptionalActionWidget, self).__init__(parent)\n\n        body_widget = QtWidgets.QWidget(self)\n        body_widget.setObjectName(\"OptionalActionBody\")\n\n        icon = QtWidgets.QLabel(body_widget)\n        label = QtWidgets.QLabel(label, body_widget)\n        # (NOTE) For removing ugly QLable shadow FX when highlighted in Nuke.\n        #   See https://stackoverflow.com/q/52838690/4145300\n        label.setStyle(QtWidgets.QStyleFactory.create(\"Plastique\"))\n        option = OptionBox(body_widget)\n        option.setObjectName(\"OptionalActionOption\")\n\n        icon.setFixedSize(24, 16)\n        option.setFixedSize(30, 30)\n\n        body_layout = QtWidgets.QHBoxLayout(body_widget)\n        body_layout.setContentsMargins(4, 0, 4, 0)\n        body_layout.setSpacing(2)\n        body_layout.addWidget(icon)\n        body_layout.addWidget(label)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(2, 1, 2, 1)\n        layout.setSpacing(0)\n        layout.addWidget(body_widget)\n        layout.addWidget(option)\n\n        body_widget.setMouseTracking(True)\n        label.setMouseTracking(True)\n        option.setMouseTracking(True)\n        self.setMouseTracking(True)\n        self.setFixedHeight(32)\n\n        self.icon = icon\n        self.label = label\n        self.option = option\n        self.body = body_widget\n\n    def set_hover_properties(self, hovered, option_hovered):\n        body_state = \"\"\n        option_state = \"\"\n        if hovered:\n            body_state = \"hover\"\n\n        if option_hovered:\n            option_state = \"hover\"\n\n        if self.body.property(\"state\") != body_state:\n            self.body.setProperty(\"state\", body_state)\n            self.body.style().polish(self.body)\n\n        if self.option.property(\"state\") != option_state:\n            self.option.setProperty(\"state\", option_state)\n            self.option.style().polish(self.option)\n\n    def setIcon(self, icon):\n        pixmap = icon.pixmap(16, 16)\n        self.icon.setPixmap(pixmap)\n\n\nclass OptionBox(QtWidgets.QLabel):\n    \"\"\"Option box widget class for `OptionalActionWidget`\"\"\"\n\n    clicked = QtCore.Signal()\n\n    def __init__(self, parent):\n        super(OptionBox, self).__init__(parent)\n\n        self.setAlignment(QtCore.Qt.AlignCenter)\n\n        icon = qtawesome.icon(\"fa.sticky-note-o\", color=\"#c6c6c6\")\n        pixmap = icon.pixmap(18, 18)\n        self.setPixmap(pixmap)\n\n    def is_hovered(self, global_pos):\n        if global_pos is None:\n            return False\n        pos = self.mapFromGlobal(global_pos)\n        return self.rect().contains(pos)\n\n\nclass OptionDialog(QtWidgets.QDialog):\n    \"\"\"Option dialog shown by option box\"\"\"\n\n    def __init__(self, parent=None):\n        super(OptionDialog, self).__init__(parent)\n        self.setModal(True)\n        self._options = dict()\n\n    def create(self, options):\n        parser = qargparse.QArgumentParser(arguments=options)\n\n        decision_widget = QtWidgets.QWidget(self)\n        accept_btn = QtWidgets.QPushButton(\"Accept\", decision_widget)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", decision_widget)\n\n        decision_layout = QtWidgets.QHBoxLayout(decision_widget)\n        decision_layout.addWidget(accept_btn)\n        decision_layout.addWidget(cancel_btn)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addWidget(parser)\n        layout.addWidget(decision_widget)\n\n        accept_btn.clicked.connect(self.accept)\n        cancel_btn.clicked.connect(self.reject)\n        parser.changed.connect(self.on_changed)\n\n    def on_changed(self, argument):\n        self._options[argument[\"name\"]] = argument.read()\n\n    def parse(self):\n        return self._options.copy()\n\n\nclass SeparatorWidget(QtWidgets.QFrame):\n    \"\"\"Prepared widget that can be used as separator with predefined color.\n\n    Args:\n        size (int): Size of separator (width or height).\n        orientation (Qt.Horizontal|Qt.Vertical): Orintation of widget.\n        parent (QtWidgets.QWidget): Parent widget.\n    \"\"\"\n\n    def __init__(self, size=2, orientation=QtCore.Qt.Horizontal, parent=None):\n        super(SeparatorWidget, self).__init__(parent)\n\n        self.setObjectName(\"Separator\")\n\n        maximum_width = self.maximumWidth()\n        maximum_height = self.maximumHeight()\n\n        self._size = None\n        self._orientation = orientation\n        self._maximum_width = maximum_width\n        self._maximum_height = maximum_height\n        self.set_size(size)\n\n    def set_size(self, size):\n        if size != self._size:\n            self._set_size(size)\n\n    def _set_size(self, size):\n        if self._orientation == QtCore.Qt.Vertical:\n            self.setMinimumWidth(size)\n            self.setMaximumWidth(size)\n        else:\n            self.setMinimumHeight(size)\n            self.setMaximumHeight(size)\n\n        self._size = size\n\n    def set_orientation(self, orientation):\n        if self._orientation == orientation:\n            return\n\n        # Reset min/max sizes in opossite direction\n        if self._orientation == QtCore.Qt.Vertical:\n            self.setMinimumHeight(0)\n            self.setMaximumHeight(self._maximum_height)\n        else:\n            self.setMinimumWidth(0)\n            self.setMaximumWidth(self._maximum_width)\n\n        self._orientation = orientation\n\n        self._set_size(self._size)\n\n\ndef get_refresh_icon():\n    return get_qta_icon_by_name_and_color(\n        \"fa.refresh\", get_default_tools_icon_color()\n    )\n\n\ndef get_go_to_current_icon():\n    return get_qta_icon_by_name_and_color(\n        \"fa.arrow-down\", get_default_tools_icon_color()\n    )\n\n\nclass VerticalExpandButton(QtWidgets.QPushButton):\n    \"\"\"Button which is expanding vertically.\n\n    By default, button is a little bit smaller than other widgets like\n        QLineEdit. This button is expanding vertically to match size of\n        other widgets, next to it.\n    \"\"\"\n\n    def __init__(self, parent=None):\n        super(VerticalExpandButton, self).__init__(parent)\n\n        sp = self.sizePolicy()\n        sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum)\n        self.setSizePolicy(sp)\n\n\nclass SquareButton(QtWidgets.QPushButton):\n    \"\"\"Make button square shape.\n\n    Change width to match height on resize.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(SquareButton, self).__init__(*args, **kwargs)\n\n        sp = self.sizePolicy()\n        sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum)\n        sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Minimum)\n        self.setSizePolicy(sp)\n        self._ideal_width = None\n\n    def showEvent(self, event):\n        super(SquareButton, self).showEvent(event)\n        self._ideal_width = self.height()\n        self.updateGeometry()\n\n    def resizeEvent(self, event):\n        super(SquareButton, self).resizeEvent(event)\n        self._ideal_width = self.height()\n        self.updateGeometry()\n\n    def sizeHint(self):\n        sh = super(SquareButton, self).sizeHint()\n        ideal_width = self._ideal_width\n        if ideal_width is None:\n            ideal_width = sh.height()\n        sh.setWidth(ideal_width)\n        return sh\n\n\nclass RefreshButton(VerticalExpandButton):\n    def __init__(self, parent=None):\n        super(RefreshButton, self).__init__(parent)\n        self.setIcon(get_refresh_icon())\n\n\nclass GoToCurrentButton(VerticalExpandButton):\n    def __init__(self, parent=None):\n        super(GoToCurrentButton, self).__init__(parent)\n        self.setIcon(get_go_to_current_icon())\n"
  },
  {
    "path": "openpype/tools/workfile_template_build/__init__.py",
    "content": "from .window import WorkfileBuildPlaceholderDialog\n\n__all__ = (\n    \"WorkfileBuildPlaceholderDialog\",\n)\n"
  },
  {
    "path": "openpype/tools/workfile_template_build/window.py",
    "content": "from qtpy import QtWidgets\n\nfrom openpype import style\nfrom openpype.lib import Logger\nfrom openpype.pipeline import legacy_io\nfrom openpype.tools.attribute_defs import AttributeDefinitionsWidget\n\n\nclass WorkfileBuildPlaceholderDialog(QtWidgets.QDialog):\n    def __init__(self, host, builder, parent=None):\n        super(WorkfileBuildPlaceholderDialog, self).__init__(parent)\n        self.setWindowTitle(\"Workfile Placeholder Manager\")\n\n        self._log = None\n\n        self._first_show = True\n        self._first_refreshed = False\n\n        self._builder = builder\n        self._host = host\n        # Mode can be 0 (create) or 1 (update)\n        # TODO write it a little bit better\n        self._mode = 0\n        self._update_item = None\n        self._last_selected_plugin = None\n\n        host_name = getattr(self._host, \"name\", None)\n        if not host_name:\n            host_name = legacy_io.Session.get(\"AVALON_APP\") or \"NA\"\n        self._host_name = host_name\n\n        plugins_combo = QtWidgets.QComboBox(self)\n\n        content_widget = QtWidgets.QWidget(self)\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        content_layout.setContentsMargins(0, 0, 0, 0)\n\n        btns_widget = QtWidgets.QWidget(self)\n        create_btn = QtWidgets.QPushButton(\"Create\", btns_widget)\n        save_btn = QtWidgets.QPushButton(\"Save\", btns_widget)\n        close_btn = QtWidgets.QPushButton(\"Close\", btns_widget)\n\n        create_btn.setVisible(False)\n        save_btn.setVisible(False)\n\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(create_btn, 0)\n        btns_layout.addWidget(save_btn, 0)\n        btns_layout.addWidget(close_btn, 0)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(plugins_combo, 0)\n        main_layout.addWidget(content_widget, 1)\n        main_layout.addWidget(btns_widget, 0)\n\n        create_btn.clicked.connect(self._on_create_click)\n        save_btn.clicked.connect(self._on_save_click)\n        close_btn.clicked.connect(self._on_close_click)\n        plugins_combo.currentIndexChanged.connect(self._on_plugin_change)\n\n        self._attr_defs_widget = None\n        self._plugins_combo = plugins_combo\n\n        self._content_widget = content_widget\n        self._content_layout = content_layout\n\n        self._create_btn = create_btn\n        self._save_btn = save_btn\n        self._close_btn = close_btn\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = Logger.get_logger(self.__class__.__name__)\n        return self._log\n\n    def _clear_content_widget(self):\n        while self._content_layout.count() > 0:\n            item = self._content_layout.takeAt(0)\n            widget = item.widget()\n            if widget:\n                widget.setVisible(False)\n                widget.deleteLater()\n\n    def _add_message_to_content(self, message):\n        msg_label = QtWidgets.QLabel(message, self._content_widget)\n        self._content_layout.addWidget(msg_label, 0)\n        self._content_layout.addStretch(1)\n\n    def refresh(self):\n        self._first_refreshed = True\n\n        self._clear_content_widget()\n\n        if not self._builder:\n            self._add_message_to_content((\n                \"Host \\\"{}\\\" does not have implemented logic\"\n                \" for template workfile build.\"\n            ).format(self._host_name))\n            self._update_ui_visibility()\n            return\n\n        placeholder_plugins = self._builder.placeholder_plugins\n\n        if self._mode == 1:\n            self._last_selected_plugin\n            plugin = self._builder.placeholder_plugins.get(\n                self._last_selected_plugin\n            )\n            self._create_option_widgets(\n                plugin, self._update_item.to_dict()\n            )\n            self._update_ui_visibility()\n            return\n\n        if not placeholder_plugins:\n            self._add_message_to_content((\n                \"Host \\\"{}\\\" does not have implemented plugins\"\n                \" for template workfile build.\"\n            ).format(self._host_name))\n            self._update_ui_visibility()\n            return\n\n        last_selected_plugin = self._last_selected_plugin\n        self._last_selected_plugin = None\n        self._plugins_combo.clear()\n        for identifier, plugin in placeholder_plugins.items():\n            label = plugin.label or identifier\n            self._plugins_combo.addItem(label, identifier)\n\n        index = self._plugins_combo.findData(last_selected_plugin)\n        if index < 0:\n            index = 0\n        self._plugins_combo.setCurrentIndex(index)\n        self._on_plugin_change()\n\n        self._update_ui_visibility()\n\n    def set_create_mode(self):\n        if self._mode == 0:\n            return\n\n        self._mode = 0\n        self._update_item = None\n        self.refresh()\n\n    def set_update_mode(self, update_item):\n        if self._mode == 1:\n            return\n\n        self._mode = 1\n        self._update_item = update_item\n        if update_item:\n            self._last_selected_plugin = update_item.plugin.identifier\n            self.refresh()\n            return\n\n        self._clear_content_widget()\n        self._add_message_to_content((\n            \"Nothing to update.\"\n            \" (You maybe don't have selected placeholder.)\"\n        ))\n        self._update_ui_visibility()\n\n    def _create_option_widgets(self, plugin, options=None):\n        self._clear_content_widget()\n        attr_defs = plugin.get_placeholder_options(options)\n        widget = AttributeDefinitionsWidget(attr_defs, self._content_widget)\n        self._content_layout.addWidget(widget, 0)\n        self._content_layout.addStretch(1)\n        self._attr_defs_widget = widget\n        self._last_selected_plugin = plugin.identifier\n\n    def _update_ui_visibility(self):\n        create_mode = self._mode == 0\n        self._plugins_combo.setVisible(create_mode)\n\n        if not self._builder:\n            self._save_btn.setVisible(False)\n            self._create_btn.setVisible(False)\n            return\n\n        save_enabled = not create_mode\n        if save_enabled:\n            save_enabled = self._update_item is not None\n        self._save_btn.setVisible(save_enabled)\n        self._create_btn.setVisible(create_mode)\n\n    def _on_plugin_change(self):\n        index = self._plugins_combo.currentIndex()\n        plugin_identifier = self._plugins_combo.itemData(index)\n        if plugin_identifier == self._last_selected_plugin:\n            return\n\n        plugin = self._builder.placeholder_plugins.get(plugin_identifier)\n        self._create_option_widgets(plugin)\n\n    def _on_save_click(self):\n        options = self._attr_defs_widget.current_value()\n        plugin = self._builder.placeholder_plugins.get(\n            self._last_selected_plugin\n        )\n        # TODO much better error handling\n        try:\n            plugin.update_placeholder(self._update_item, options)\n            self.accept()\n        except Exception:\n            self.log.warning(\"Something went wrong\", exc_info=True)\n            dialog = QtWidgets.QMessageBox(self)\n            dialog.setWindowTitle(\"Something went wrong\")\n            dialog.setText(\"Something went wrong\")\n            dialog.exec_()\n\n    def _on_create_click(self):\n        options = self._attr_defs_widget.current_value()\n        plugin = self._builder.placeholder_plugins.get(\n            self._last_selected_plugin\n        )\n        # TODO much better error handling\n        try:\n            plugin.create_placeholder(options)\n        except Exception:\n            self.log.warning(\"Something went wrong\", exc_info=True)\n            dialog = QtWidgets.QMessageBox(self)\n            dialog.setWindowTitle(\"Something went wrong\")\n            dialog.setText(\"Something went wrong\")\n            dialog.exec_()\n\n    def _on_close_click(self):\n        self.reject()\n\n    def showEvent(self, event):\n        super(WorkfileBuildPlaceholderDialog, self).showEvent(event)\n        if not self._first_refreshed:\n            self.refresh()\n\n        if self._first_show:\n            self._first_show = False\n            self.setStyleSheet(style.load_stylesheet())\n            self.resize(390, 450)\n"
  },
  {
    "path": "openpype/tools/workfiles/README.md",
    "content": "# Workfiles App\n\nThe Workfiles app facilitates easy saving, creation and launching of work files.\n\nThe current supported hosts are:\n\n- Maya\n- Houdini\n- Fusion\n\nThe app is available inside hosts via. the ```Avalon > Work Files``` menu.\n\n## Enabling Workfiles on launch\n\nBy default the Workfiles app will not launch on startup, so it has to be explicitly enabled in a config.\n\n```python\nworkfiles.show()\n```\n\n## Naming Files\n\nWorkfiles app enables user to easily save and create new work files.\n\nThe user is presented with a two parameters; ```version``` and ```comment```. The name of the work file is determined from a template.\n\n### ```Next Available Version```\n\nWill search for the next version number that is not in use.\n\n## Templates\n\nThe default template for work files is ```{task[name]}_v{version:0>4}<_{comment}>```. Launching Maya on an animation task and creating a version 1 will result in ```animation_v0001.ma```. Adding \"blocking\" to the optional comment input will result in ```animation_v0001_blocking.ma```.\n\nThis template can be customized per project with the ```workfile``` template.\n\nThere are other variables to customize the template with:\n\n```python\n{\n    \"project\": project,  # The project data from the database.\n    \"asset\": asset, # The asset data from the database.\n    \"task\": {\n        \"label\": label,  # Label of task chosen.\n        \"name\": name  # Sanitize version of the label.\n    },\n    \"user\": user,  # Name of the user on the machine.\n    \"version\": version,  # Chosen version of the user.\n    \"comment\": comment,  # Chosen comment of the user.\n}\n```\n\n### Optional template groups\n\nThe default template contains an optional template group ```<_{comment}>```. If any template group (```{comment}```) within angle bracket ```<>``` does not exist, the whole optional group is discarded.\n\n\n## Implementing a new host integration for Work Files\n\nFor the Work Files tool to work with a new host integration the host must\nimplement the following functions:\n\n- `file_extensions()`: The files the host should allow to open and show in the Work Files view.\n- `open_file(filepath)`: Open a file.\n- `save_file(filepath)`: Save the current file. This should return None if it failed to save, and return the path if it succeeded\n- `has_unsaved_changes()`: Return whether the current scene has unsaved changes.\n- `current_file()`: The path to the current file. None if not saved.\n- `work_root()`: The path to where the work files for this app should be saved.\n\nHere's an example code layout:\n\n```python\ndef file_extensions():\n    \"\"\"Return the filename extension formats that should be shown.\n\n    Note:\n        The first entry in the list will be used as the default file\n        format to save to when the current scene is not saved yet.\n\n    Returns:\n        list: A list of the file extensions supported by Work Files.\n\n    \"\"\"\n    return list()\n\n\ndef has_unsaved_changes():\n    \"\"\"Return whether current file has unsaved modifications.\"\"\"\n\n\ndef save_file(filepath):\n    \"\"\"Save to filepath.\n    \n    This should return None if it failed to save, and return the path if it \n    succeeded.\n    \"\"\"\n    pass\n\n\ndef open_file(filepath):\n    \"\"\"Open file\"\"\"\n    pass\n\n\ndef current_file():\n    \"\"\"Return path to currently open file or None if not saved.\n\n    Returns:\n        str or None: The full path to current file or None when not saved.\n\n    \"\"\"\n    pass\n\n\ndef work_root():\n    \"\"\"Return the default root for the Host to browse in for Work Files\n\n    Returns:\n        str: The path to look in.\n\n    \"\"\"\n    pass\n```\n\n#### Work Files Scenes root (AVALON_SCENEDIR)\n\nWhenever the host application has no built-in implementation that defines\nwhere scene files should be saved to then the Work Files API for that host\nshould fall back to the `AVALON_SCENEDIR` variable in `api.Session`.\n\nWhen `AVALON_SCENEDIR` is set the  directory is the relative folder inside the \n`AVALON_WORKDIR`. Otherwise, when it is not set or empty it should fall back\nto the Work Directory's root, `AVALON_WORKDIR` \n\n```python\nAVALON_WORKDIR=\"/path/to/work\"\nAVALON_SCENEDIR=\"scenes\"\n# Result: /path/to/work/scenes\n\nAVALON_WORKDIR=\"/path/to/work\"\nAVALON_SCENEDIR=None\n# Result: /path/to/work\n```"
  },
  {
    "path": "openpype/tools/workfiles/__init__.py",
    "content": "from .window import Window\nfrom .app import (\n    show,\n)\n\n__all__ = [\n    \"Window\",\n\n    \"show\",\n]\n"
  },
  {
    "path": "openpype/tools/workfiles/app.py",
    "content": "import sys\nimport logging\n\nfrom openpype.host import IWorkfileHost\nfrom openpype.pipeline import (\n    registered_host,\n    legacy_io,\n)\nfrom openpype.tools.utils import qt_app_context\nfrom .window import Window\n\nlog = logging.getLogger(__name__)\n\nmodule = sys.modules[__name__]\nmodule.window = None\n\n\ndef show(root=None, debug=False, parent=None, use_context=True, save=True):\n    \"\"\"Show Work Files GUI\"\"\"\n    # todo: remove `root` argument to show()\n\n    try:\n        module.window.close()\n        del(module.window)\n    except (AttributeError, RuntimeError):\n        pass\n\n    host = registered_host()\n    IWorkfileHost.validate_workfile_methods(host)\n\n    if debug:\n        legacy_io.Session[\"AVALON_ASSET\"] = \"Mock\"\n        legacy_io.Session[\"AVALON_TASK\"] = \"Testing\"\n\n    with qt_app_context():\n        window = Window(parent=parent)\n        window.refresh()\n\n        if use_context:\n            context = {\n                \"asset\": legacy_io.Session[\"AVALON_ASSET\"],\n                \"task\": legacy_io.Session[\"AVALON_TASK\"]\n            }\n            window.set_context(context)\n\n        window.set_save_enabled(save)\n\n        window.show()\n\n        module.window = window\n\n        # Pull window to the front.\n        module.window.raise_()\n        module.window.activateWindow()\n"
  },
  {
    "path": "openpype/tools/workfiles/files_widget.py",
    "content": "import os\nimport logging\nimport shutil\nimport copy\n\nimport qtpy\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.host import IWorkfileHost\nfrom openpype.client import get_asset_by_id\nfrom openpype.pipeline.workfile.lock_workfile import (\n    is_workfile_locked,\n    is_workfile_lock_enabled,\n    is_workfile_locked_for_current_process\n)\nfrom openpype.tools.utils import PlaceholderLineEdit\nfrom openpype.tools.utils.delegates import PrettyTimeDelegate\nfrom openpype.lib import emit_event\nfrom openpype.tools.workfiles.lock_dialog import WorkfileLockDialog\nfrom openpype.pipeline import (\n    registered_host,\n    legacy_io,\n    Anatomy,\n    get_current_project_name,\n)\nfrom openpype.pipeline.context_tools import (\n    compute_session_changes,\n    change_current_context\n)\nfrom openpype.pipeline.workfile import (\n    get_workfile_template_key,\n    create_workdir_extra_folders,\n)\n\nfrom .model import (\n    WorkAreaFilesModel,\n    PublishFilesModel,\n\n    FILEPATH_ROLE,\n    DATE_MODIFIED_ROLE,\n)\nfrom .save_as_dialog import SaveAsDialog\n\nlog = logging.getLogger(__name__)\n\n\nclass FilesView(QtWidgets.QTreeView):\n    doubleClickedLeft = QtCore.Signal()\n    doubleClickedRight = QtCore.Signal()\n\n    def mouseDoubleClickEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self.doubleClickedLeft.emit()\n\n        elif event.button() == QtCore.Qt.RightButton:\n            self.doubleClickedRight.emit()\n\n        return super(FilesView, self).mouseDoubleClickEvent(event)\n\n\nclass SelectContextOverlay(QtWidgets.QFrame):\n    def __init__(self, parent):\n        super(SelectContextOverlay, self).__init__(parent)\n\n        self.setObjectName(\"WorkfilesPublishedContextSelect\")\n        label_widget = QtWidgets.QLabel(\n            \"Please choose context on the left<br/>&lt\",\n            self\n        )\n        label_widget.setAlignment(QtCore.Qt.AlignCenter)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter)\n\n        label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n\n        parent.installEventFilter(self)\n\n    def eventFilter(self, obj, event):\n        if event.type() == QtCore.QEvent.Resize:\n            self.resize(obj.size())\n\n        return super(SelectContextOverlay, self).eventFilter(obj, event)\n\n\nclass FilesWidget(QtWidgets.QWidget):\n    \"\"\"A widget displaying files that allows to save and open files.\"\"\"\n    file_selected = QtCore.Signal(str)\n    file_opened = QtCore.Signal()\n    workfile_created = QtCore.Signal(str)\n    published_visible_changed = QtCore.Signal(bool)\n\n    def __init__(self, parent):\n        super(FilesWidget, self).__init__(parent)\n\n        # Setup\n        self._asset_id = None\n        self._asset_doc = None\n        self._task_name = None\n        self._task_type = None\n\n        # Pype's anatomy object for current project\n        project_name = get_current_project_name()\n        self.anatomy = Anatomy(project_name)\n        self.project_name = project_name\n        # Template key used to get work template from anatomy templates\n        self.template_key = \"work\"\n\n        # This is not root but workfile directory\n        self._workfiles_root = None\n        self._workdir_path = None\n        self.host = registered_host()\n        self.host_name = os.environ[\"AVALON_APP\"]\n\n        # Whether to automatically select the latest modified\n        # file on a refresh of the files model.\n        self.auto_select_latest_modified = True\n\n        # Avoid crash in Blender and store the message box\n        # (setting parent doesn't work as it hides the message box)\n        self._messagebox = None\n\n        # Filtering input\n        filter_widget = QtWidgets.QWidget(self)\n\n        published_checkbox = QtWidgets.QCheckBox(\"Published\", filter_widget)\n\n        filter_input = PlaceholderLineEdit(filter_widget)\n        filter_input.setPlaceholderText(\"Filter files..\")\n\n        filter_layout = QtWidgets.QHBoxLayout(filter_widget)\n        filter_layout.setContentsMargins(0, 0, 0, 0)\n        filter_layout.addWidget(filter_input, 1)\n        filter_layout.addWidget(published_checkbox, 0)\n\n        # Create the Files models\n        extensions = set(self._get_host_extensions())\n\n        views_widget = QtWidgets.QWidget(self)\n        # --- Workarea view ---\n        workarea_files_model = WorkAreaFilesModel(extensions)\n\n        # Create proxy model for files to be able sort and filter\n        workarea_proxy_model = QtCore.QSortFilterProxyModel()\n        workarea_proxy_model.setSourceModel(workarea_files_model)\n        workarea_proxy_model.setDynamicSortFilter(True)\n        workarea_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        # Set up the file list tree view\n        workarea_files_view = FilesView(views_widget)\n        workarea_files_view.setModel(workarea_proxy_model)\n        workarea_files_view.setSortingEnabled(True)\n        workarea_files_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n\n        # Date modified delegate\n        workarea_time_delegate = PrettyTimeDelegate()\n        workarea_files_view.setItemDelegateForColumn(1, workarea_time_delegate)\n        # smaller indentation\n        workarea_files_view.setIndentation(3)\n\n        # Default to a wider first filename column it is what we mostly care\n        # about and the date modified is relatively small anyway.\n        workarea_files_view.setColumnWidth(0, 330)\n\n        # --- Publish files view ---\n        publish_files_model = PublishFilesModel(\n            extensions, legacy_io, self.anatomy\n        )\n\n        publish_proxy_model = QtCore.QSortFilterProxyModel()\n        publish_proxy_model.setSourceModel(publish_files_model)\n        publish_proxy_model.setDynamicSortFilter(True)\n        publish_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)\n\n        publish_files_view = FilesView(views_widget)\n        publish_files_view.setModel(publish_proxy_model)\n\n        publish_files_view.setSortingEnabled(True)\n        publish_files_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)\n\n        # Date modified delegate\n        publish_time_delegate = PrettyTimeDelegate()\n        publish_files_view.setItemDelegateForColumn(1, publish_time_delegate)\n        # smaller indentation\n        publish_files_view.setIndentation(3)\n\n        # Default to a wider first filename column it is what we mostly care\n        # about and the date modified is relatively small anyway.\n        publish_files_view.setColumnWidth(0, 330)\n\n        publish_context_overlay = SelectContextOverlay(views_widget)\n        publish_context_overlay.setVisible(False)\n\n        views_layout = QtWidgets.QHBoxLayout(views_widget)\n        views_layout.setContentsMargins(0, 0, 0, 0)\n        views_layout.addWidget(workarea_files_view, 1)\n        views_layout.addWidget(publish_files_view, 1)\n\n        # Home Page\n        # Build buttons widget for files widget\n        btns_widget = QtWidgets.QWidget(self)\n\n        workarea_btns_widget = QtWidgets.QWidget(btns_widget)\n        btn_save = QtWidgets.QPushButton(\"Save As\", workarea_btns_widget)\n        btn_browse = QtWidgets.QPushButton(\"Browse\", workarea_btns_widget)\n        btn_open = QtWidgets.QPushButton(\"Open\", workarea_btns_widget)\n\n        workarea_btns_layout = QtWidgets.QHBoxLayout(workarea_btns_widget)\n        workarea_btns_layout.setContentsMargins(0, 0, 0, 0)\n        workarea_btns_layout.addWidget(btn_open, 1)\n        workarea_btns_layout.addWidget(btn_browse, 1)\n        workarea_btns_layout.addWidget(btn_save, 1)\n\n        publish_btns_widget = QtWidgets.QWidget(btns_widget)\n        btn_save_as_published = QtWidgets.QPushButton(\n            \"Copy && Open\", publish_btns_widget\n        )\n        btn_change_context = QtWidgets.QPushButton(\n            \"Choose different context\", publish_btns_widget\n        )\n        btn_select_context_published = QtWidgets.QPushButton(\n            \"Copy && Open\", publish_btns_widget\n        )\n        btn_cancel_published = QtWidgets.QPushButton(\n            \"Cancel\", publish_btns_widget\n        )\n\n        publish_btns_layout = QtWidgets.QHBoxLayout(publish_btns_widget)\n        publish_btns_layout.setContentsMargins(0, 0, 0, 0)\n        publish_btns_layout.addWidget(btn_save_as_published, 1)\n        publish_btns_layout.addWidget(btn_change_context, 1)\n        publish_btns_layout.addWidget(btn_select_context_published, 1)\n        publish_btns_layout.addWidget(btn_cancel_published, 1)\n\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n        btns_layout.addWidget(workarea_btns_widget, 1)\n        btns_layout.addWidget(publish_btns_widget, 1)\n\n        # Build files widgets for home page\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(filter_widget, 0)\n        main_layout.addWidget(views_widget, 1)\n        main_layout.addWidget(btns_widget, 0)\n\n        # Register signal callbacks\n        published_checkbox.stateChanged.connect(self._on_published_change)\n        filter_input.textChanged.connect(self._on_filter_text_change)\n\n        workarea_files_view.doubleClickedLeft.connect(\n            self._on_workarea_open_pressed\n        )\n        workarea_files_view.customContextMenuRequested.connect(\n            self._on_workarea_context_menu\n        )\n        workarea_files_view.selectionModel().selectionChanged.connect(\n            self.on_file_select\n        )\n\n        btn_open.pressed.connect(self._on_workarea_open_pressed)\n        btn_browse.pressed.connect(self.on_browse_pressed)\n        btn_save.pressed.connect(self._on_save_as_pressed)\n        btn_save_as_published.pressed.connect(\n            self._on_published_save_as_pressed\n        )\n        btn_change_context.pressed.connect(\n            self._on_publish_change_context_pressed\n        )\n        btn_select_context_published.pressed.connect(\n            self._on_publish_select_context_pressed\n        )\n        btn_cancel_published.pressed.connect(\n            self._on_publish_cancel_pressed\n        )\n\n        # Store attributes\n        self._published_checkbox = published_checkbox\n        self._filter_input = filter_input\n\n        self._workarea_time_delegate = workarea_time_delegate\n        self._workarea_files_view = workarea_files_view\n        self._workarea_files_model = workarea_files_model\n        self._workarea_proxy_model = workarea_proxy_model\n\n        self._publish_time_delegate = publish_time_delegate\n        self._publish_files_view = publish_files_view\n        self._publish_files_model = publish_files_model\n        self._publish_proxy_model = publish_proxy_model\n\n        self._publish_context_overlay = publish_context_overlay\n\n        self._workarea_btns_widget = workarea_btns_widget\n        self._publish_btns_widget = publish_btns_widget\n        self._btn_open = btn_open\n        self._btn_browse = btn_browse\n        self._btn_save = btn_save\n\n        self._btn_save_as_published = btn_save_as_published\n        self._btn_change_context = btn_change_context\n        self._btn_select_context_published = btn_select_context_published\n        self._btn_cancel_published = btn_cancel_published\n\n        # Create a proxy widget for files widget\n        self.setFocusProxy(btn_open)\n\n        # Hide publish files widgets\n        publish_files_view.setVisible(False)\n        publish_btns_widget.setVisible(False)\n        btn_select_context_published.setVisible(False)\n        btn_cancel_published.setVisible(False)\n\n        self._publish_context_select_mode = False\n\n    @property\n    def published_enabled(self):\n        return self._published_checkbox.isChecked()\n\n    def _on_published_change(self):\n        published_enabled = self.published_enabled\n\n        self._workarea_files_view.setVisible(not published_enabled)\n        self._workarea_btns_widget.setVisible(not published_enabled)\n\n        self._publish_files_view.setVisible(published_enabled)\n        self._publish_btns_widget.setVisible(published_enabled)\n\n        self._update_filtering()\n        self._update_asset_task()\n\n        self.published_visible_changed.emit(published_enabled)\n\n        self._select_last_modified_file()\n\n    def _on_filter_text_change(self):\n        self._update_filtering()\n\n    def _update_filtering(self):\n        text = self._filter_input.text()\n        if self.published_enabled:\n            self._publish_proxy_model.setFilterFixedString(text)\n        else:\n            self._workarea_proxy_model.setFilterFixedString(text)\n\n    def set_save_enabled(self, enabled):\n        self._btn_save.setEnabled(enabled)\n        if not enabled and self._published_checkbox.isChecked():\n            self._published_checkbox.setChecked(False)\n        self._published_checkbox.setVisible(enabled)\n\n    def set_asset_task(self, asset_id, task_name, task_type):\n        if asset_id != self._asset_id:\n            self._asset_doc = None\n        self._asset_id = asset_id\n        self._task_name = task_name\n        self._task_type = task_type\n        self._update_asset_task()\n\n    def _update_asset_task(self):\n        if self.published_enabled and not self._publish_context_select_mode:\n            self._publish_files_model.set_context(\n                self._asset_id, self._task_name\n            )\n            has_valid_items = self._publish_files_model.has_valid_items()\n            self._btn_save_as_published.setEnabled(has_valid_items)\n            self._btn_change_context.setEnabled(has_valid_items)\n\n        else:\n            # Define a custom session so we can query the work root\n            # for a \"Work area\" that is not our current Session.\n            # This way we can browse it even before we enter it.\n            if self._asset_id and self._task_name and self._task_type:\n                session = self._get_session()\n                self._workdir_path = session[\"AVALON_WORKDIR\"]\n                self._workfiles_root = self.host.work_root(session)\n                self._workarea_files_model.set_root(self._workfiles_root)\n\n            else:\n                self._workarea_files_model.set_root(None)\n\n            # Disable/Enable buttons based on available files in model\n            has_valid_items = self._workarea_files_model.has_valid_items()\n            self._btn_browse.setEnabled(True)\n            self._btn_open.setEnabled(has_valid_items)\n\n            if self._publish_context_select_mode:\n                self._btn_select_context_published.setEnabled(\n                    bool(self._asset_id) and bool(self._task_name)\n                )\n                return\n\n        # Manually trigger file selection\n        if not has_valid_items:\n            self.on_file_select()\n\n    def _get_asset_doc(self):\n        if self._asset_id is None:\n            return None\n\n        if self._asset_doc is None:\n            self._asset_doc = get_asset_by_id(\n                self.project_name, self._asset_id\n            )\n\n        return self._asset_doc\n\n    def _get_session(self):\n        \"\"\"Return a modified session for the current asset and task\"\"\"\n\n        session = legacy_io.Session.copy()\n        self.template_key = get_workfile_template_key(\n            self._task_type,\n            self.host_name,\n            project_name=self.project_name\n        )\n        changes = compute_session_changes(\n            session,\n            self._get_asset_doc(),\n            self._task_name,\n            template_key=self.template_key\n        )\n        session.update(changes)\n\n        return session\n\n    def _enter_session(self):\n        \"\"\"Enter the asset and task session currently selected\"\"\"\n\n        session = legacy_io.Session.copy()\n        changes = compute_session_changes(\n            session,\n            self._get_asset_doc(),\n            self._task_name,\n            template_key=self.template_key\n        )\n        if not changes:\n            # Return early if we're already in the right Session context\n            # to avoid any unwanted Task Changed callbacks to be triggered.\n            return\n\n        change_current_context(\n            self._get_asset_doc(),\n            self._task_name,\n            template_key=self.template_key\n        )\n\n    def _get_event_context_data(self):\n        asset_id = None\n        asset_name = None\n        asset_doc = self._get_asset_doc()\n        if asset_doc:\n            asset_id = asset_doc[\"_id\"]\n            asset_name = asset_doc[\"name\"]\n        return {\n            \"project_name\": self.project_name,\n            \"asset_id\": asset_id,\n            \"asset_name\": asset_name,\n            \"task_name\": self._task_name,\n            \"host_name\": self.host_name\n        }\n\n    def _is_workfile_locked(self, filepath):\n        if not is_workfile_lock_enabled(self.host_name, self.project_name):\n            return False\n        if not is_workfile_locked(filepath):\n            return False\n        return not is_workfile_locked_for_current_process(filepath)\n\n    def open_file(self, filepath):\n        host = self.host\n        if self._is_workfile_locked(filepath):\n            # add lockfile dialog\n            WorkfileLockDialog(filepath)\n\n        if isinstance(host, IWorkfileHost):\n            has_unsaved_changes = host.workfile_has_unsaved_changes()\n        else:\n            has_unsaved_changes = host.has_unsaved_changes()\n\n        if has_unsaved_changes:\n            result = self.save_changes_prompt()\n            if result is None:\n                # Cancel operation\n                return False\n\n            # Save first if has changes\n            if result:\n                if isinstance(host, IWorkfileHost):\n                    current_file = host.get_current_workfile()\n                else:\n                    current_file = host.current_file()\n                if not current_file:\n                    # If the user requested to save the current scene\n                    # we can't actually automatically do so if the current\n                    # file has not been saved with a name yet. So we'll have\n                    # to opt out.\n                    log.error(\"Can't save scene with no filename. Please \"\n                              \"first save your work file using 'Save As'.\")\n                    return\n\n                # Save current scene, continue to open file\n                if isinstance(host, IWorkfileHost):\n                    host.save_workfile(current_file)\n                else:\n                    host.save_file(current_file)\n\n        event_data_before = self._get_event_context_data()\n        event_data_before[\"filepath\"] = filepath\n        event_data_after = copy.deepcopy(event_data_before)\n        emit_event(\n            \"workfile.open.before\",\n            event_data_before,\n            source=\"workfiles.tool\"\n        )\n        self._enter_session()\n        if isinstance(host, IWorkfileHost):\n            host.open_workfile(filepath)\n        else:\n            host.open_file(filepath)\n        emit_event(\n            \"workfile.open.after\",\n            event_data_after,\n            source=\"workfiles.tool\"\n        )\n        self.file_opened.emit()\n\n    def save_changes_prompt(self):\n        self._messagebox = messagebox = QtWidgets.QMessageBox(parent=self)\n        messagebox.setWindowFlags(\n            messagebox.windowFlags() | QtCore.Qt.FramelessWindowHint\n        )\n        messagebox.setIcon(QtWidgets.QMessageBox.Warning)\n        messagebox.setWindowTitle(\"Unsaved Changes!\")\n        messagebox.setText(\n            \"There are unsaved changes to the current file.\"\n            \"\\nDo you want to save the changes?\"\n        )\n        messagebox.setStandardButtons(\n            QtWidgets.QMessageBox.Yes\n            | QtWidgets.QMessageBox.No\n            | QtWidgets.QMessageBox.Cancel\n        )\n\n        result = messagebox.exec_()\n        if result == QtWidgets.QMessageBox.Yes:\n            return True\n        if result == QtWidgets.QMessageBox.No:\n            return False\n        return None\n\n    def get_filename(self):\n        \"\"\"Show save dialog to define filename for save or duplicate\n\n        Returns:\n            str: The filename to create.\n\n        \"\"\"\n        session = self._get_session()\n\n        if self.published_enabled:\n            filepath = self._get_selected_filepath()\n            extensions = [os.path.splitext(filepath)[1]]\n        else:\n            extensions = self._get_host_extensions()\n\n        window = SaveAsDialog(\n            parent=self,\n            root=self._workfiles_root,\n            anatomy=self.anatomy,\n            template_key=self.template_key,\n            extensions=extensions,\n            session=session\n        )\n        window.exec_()\n\n        return window.get_result()\n\n    def on_duplicate_pressed(self):\n        work_file = self.get_filename()\n        if not work_file:\n            return\n\n        src = self._get_selected_filepath()\n        dst = os.path.join(self._workfiles_root, work_file)\n        shutil.copyfile(src, dst)\n\n        self.workfile_created.emit(dst)\n\n        self.refresh()\n\n    def _get_selected_filepath(self):\n        \"\"\"Return current filepath selected in view\"\"\"\n        if self.published_enabled:\n            source_view = self._publish_files_view\n        else:\n            source_view = self._workarea_files_view\n        selection = source_view.selectionModel()\n        index = selection.currentIndex()\n        if not index.isValid():\n            return\n\n        return index.data(FILEPATH_ROLE)\n\n    def _on_workarea_open_pressed(self):\n        path = self._get_selected_filepath()\n        if not path:\n            print(\"No file selected to open..\")\n            return\n\n        self.open_file(path)\n\n    def _get_host_extensions(self):\n        if isinstance(self.host, IWorkfileHost):\n            return self.host.get_workfile_extensions()\n        return self.host.file_extensions()\n\n    def on_browse_pressed(self):\n        ext_filter = \"Work File (*{0})\".format(\n            \" *\".join(self._get_host_extensions())\n        )\n        dir_key = \"directory\"\n        if qtpy.API in (\"pyside\", \"pyside2\", \"pyside6\"):\n            dir_key = \"dir\"\n\n        workfile_root = self._workfiles_root\n        # Find existing directory of workfile root\n        #   - Qt will use 'cwd' instead, if path does not exist, which may lead\n        #       to igniter directory\n        while workfile_root:\n            if os.path.exists(workfile_root):\n                break\n            workfile_root = os.path.dirname(workfile_root)\n\n        kwargs = {\n            \"caption\": \"Work Files\",\n            \"filter\": ext_filter,\n            dir_key: workfile_root\n        }\n\n        work_file = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]\n        if work_file:\n            self.open_file(work_file)\n\n    def _on_save_as_pressed(self):\n        self._save_as_with_dialog()\n\n    def _save_as_with_dialog(self):\n        work_filename = self.get_filename()\n        if not work_filename:\n            return None\n\n        src_path = self._get_selected_filepath()\n\n        # Trigger before save event\n        event_data_before = self._get_event_context_data()\n        event_data_before.update({\n            \"filename\": work_filename,\n            \"workdir_path\": self._workdir_path\n        })\n        emit_event(\n            \"workfile.save.before\",\n            event_data_before,\n            source=\"workfiles.tool\"\n        )\n\n        # Make sure workfiles root is updated\n        # - this triggers 'workio.work_root(...)' which may change value of\n        #   '_workfiles_root'\n        self.set_asset_task(\n            self._asset_id, self._task_name, self._task_type\n        )\n\n        # Create workfiles root folder\n        if not os.path.exists(self._workfiles_root):\n            log.debug(\"Initializing Work Directory: %s\", self._workfiles_root)\n            os.makedirs(self._workfiles_root)\n\n        # Prepare full path to workfile and save it\n        filepath = os.path.join(\n            os.path.normpath(self._workfiles_root), work_filename\n        )\n\n        # Update session if context has changed\n        self._enter_session()\n\n        if not self.published_enabled:\n            if isinstance(self.host, IWorkfileHost):\n                self.host.save_workfile(filepath)\n            else:\n                self.host.save_file(filepath)\n        else:\n            shutil.copyfile(src_path, filepath)\n            if isinstance(self.host, IWorkfileHost):\n                self.host.open_workfile(filepath)\n            else:\n                self.host.open_file(filepath)\n\n        # Create extra folders\n        create_workdir_extra_folders(\n            self._workdir_path,\n            self.host_name,\n            self._task_type,\n            self._task_name,\n            self.project_name\n        )\n        event_data_after = self._get_event_context_data()\n        event_data_after.update({\n            \"filename\": work_filename,\n            \"workdir_path\": self._workdir_path\n        })\n        # Trigger after save events\n        emit_event(\n            \"workfile.save.after\",\n            event_data_after,\n            source=\"workfiles.tool\"\n        )\n\n        self.workfile_created.emit(filepath)\n        # Refresh files model\n        if self.published_enabled:\n            self._published_checkbox.setChecked(False)\n        else:\n            self.refresh()\n        return filepath\n\n    def _on_published_save_as_pressed(self):\n        self._save_as_with_dialog()\n\n    def _set_publish_context_select_mode(self, enabled):\n        self._publish_context_select_mode = enabled\n\n        # Show buttons related to context selection\n        self._publish_context_overlay.setVisible(enabled)\n        self._btn_cancel_published.setVisible(enabled)\n        self._btn_select_context_published.setVisible(enabled)\n        # Change enabled state based on select context\n        self._btn_select_context_published.setEnabled(\n            bool(self._asset_id) and bool(self._task_name)\n        )\n\n        self._btn_save_as_published.setVisible(not enabled)\n        self._btn_change_context.setVisible(not enabled)\n\n        # Change views and disable workarea view if enabled\n        self._workarea_files_view.setEnabled(not enabled)\n        if self.published_enabled:\n            self._workarea_files_view.setVisible(enabled)\n            self._publish_files_view.setVisible(not enabled)\n        else:\n            self._workarea_files_view.setVisible(True)\n            self._publish_files_view.setVisible(False)\n\n        # Disable filter widgets\n        self._published_checkbox.setEnabled(not enabled)\n        self._filter_input.setEnabled(not enabled)\n\n    def _on_publish_change_context_pressed(self):\n        self._set_publish_context_select_mode(True)\n\n    def _on_publish_select_context_pressed(self):\n        result = self._save_as_with_dialog()\n        if result is not None:\n            self._set_publish_context_select_mode(False)\n            self._update_asset_task()\n\n    def _on_publish_cancel_pressed(self):\n        self._set_publish_context_select_mode(False)\n        self._update_asset_task()\n\n    def on_file_select(self):\n        self.file_selected.emit(self._get_selected_filepath())\n\n    def refresh(self):\n        \"\"\"Refresh listed files for current selection in the interface\"\"\"\n        if self.published_enabled:\n            self._publish_files_model.refresh()\n        else:\n            self._workarea_files_model.refresh()\n\n        if self.auto_select_latest_modified:\n            self._select_last_modified_file()\n\n    def _on_workarea_context_menu(self, point):\n        index = self._workarea_files_view.indexAt(point)\n        if not index.isValid():\n            return\n\n        if not index.flags() & QtCore.Qt.ItemIsEnabled:\n            return\n\n        menu = QtWidgets.QMenu(self)\n\n        # Duplicate\n        action = QtWidgets.QAction(\"Duplicate\", menu)\n        tip = \"Duplicate selected file.\"\n        action.setToolTip(tip)\n        action.setStatusTip(tip)\n        action.triggered.connect(self.on_duplicate_pressed)\n        menu.addAction(action)\n\n        # Show the context action menu\n        global_point = self._workarea_files_view.mapToGlobal(point)\n        action = menu.exec_(global_point)\n        if not action:\n            return\n\n    def _select_last_modified_file(self):\n        \"\"\"Utility function to select the file with latest date modified\"\"\"\n        if self.published_enabled:\n            source_view = self._publish_files_view\n        else:\n            source_view = self._workarea_files_view\n        model = source_view.model()\n\n        highest_index = None\n        highest = 0\n        for row in range(model.rowCount()):\n            index = model.index(row, 0, parent=QtCore.QModelIndex())\n            if not index.isValid():\n                continue\n\n            modified = index.data(DATE_MODIFIED_ROLE)\n            if modified is not None and modified > highest:\n                highest_index = index\n                highest = modified\n\n        if highest_index:\n            source_view.setCurrentIndex(highest_index)\n"
  },
  {
    "path": "openpype/tools/workfiles/lock_dialog.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\nfrom openpype.style import load_stylesheet, get_app_icon_path\n\nfrom openpype.pipeline.workfile.lock_workfile import get_workfile_lock_data\n\n\nclass WorkfileLockDialog(QtWidgets.QDialog):\n    def __init__(self, workfile_path, parent=None):\n        super(WorkfileLockDialog, self).__init__(parent)\n        self.setWindowTitle(\"Warning\")\n        icon = QtGui.QIcon(get_app_icon_path())\n        self.setWindowIcon(icon)\n\n        data = get_workfile_lock_data(workfile_path)\n\n        message = \"{} on {} machine is working on the same workfile.\".format(\n            data[\"username\"],\n            data[\"hostname\"]\n        )\n\n        msg_label = QtWidgets.QLabel(message, self)\n\n        btns_widget = QtWidgets.QWidget(self)\n\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", btns_widget)\n        ignore_btn = QtWidgets.QPushButton(\"Ignore lock\", btns_widget)\n\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.setContentsMargins(0, 0, 0, 0)\n        btns_layout.setSpacing(10)\n        btns_layout.addStretch(1)\n        btns_layout.addWidget(cancel_btn, 0)\n        btns_layout.addWidget(ignore_btn, 0)\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(15, 15, 15, 15)\n        main_layout.addWidget(msg_label, 1, QtCore.Qt.AlignCenter),\n        main_layout.addSpacing(10)\n        main_layout.addWidget(btns_widget, 0)\n\n        cancel_btn.clicked.connect(self.reject)\n        ignore_btn.clicked.connect(self.accept)\n\n    def showEvent(self, event):\n        super(WorkfileLockDialog, self).showEvent(event)\n\n        self.setStyleSheet(load_stylesheet())\n"
  },
  {
    "path": "openpype/tools/workfiles/model.py",
    "content": "import os\nimport logging\n\nfrom qtpy import QtCore, QtGui\nimport qtawesome\n\nfrom openpype.client import (\n    get_subsets,\n    get_versions,\n    get_representations,\n)\nfrom openpype.style import (\n    get_default_entity_icon_color,\n    get_disabled_entity_icon_color,\n)\nfrom openpype.pipeline import get_representation_path\n\nlog = logging.getLogger(__name__)\n\n\nFILEPATH_ROLE = QtCore.Qt.UserRole + 2\nDATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3\nITEM_ID_ROLE = QtCore.Qt.UserRole + 4\n\n\nclass WorkAreaFilesModel(QtGui.QStandardItemModel):\n    \"\"\"Model is looking into one folder for files with extension.\"\"\"\n\n    def __init__(self, extensions, *args, **kwargs):\n        super(WorkAreaFilesModel, self).__init__(*args, **kwargs)\n\n        self.setColumnCount(2)\n\n        self._root = None\n        self._file_extensions = extensions\n        self._invalid_path_item = None\n        self._empty_root_item = None\n        self._file_icon = qtawesome.icon(\n            \"fa.file-o\",\n            color=get_default_entity_icon_color()\n        )\n        self._invalid_item_visible = False\n        self._items_by_filename = {}\n\n    def _get_invalid_path_item(self):\n        if self._invalid_path_item is None:\n            message = \"Work Area does not exist. Use Save As to create it.\"\n            item = QtGui.QStandardItem(message)\n            icon = qtawesome.icon(\n                \"fa.times\",\n                color=get_disabled_entity_icon_color()\n            )\n            item.setData(icon, QtCore.Qt.DecorationRole)\n            item.setFlags(QtCore.Qt.NoItemFlags)\n            item.setColumnCount(self.columnCount())\n            self._invalid_path_item = item\n        return self._invalid_path_item\n\n    def _get_empty_root_item(self):\n        if self._empty_root_item is None:\n            message = \"Work Area is empty.\"\n            item = QtGui.QStandardItem(message)\n            icon = qtawesome.icon(\n                \"fa.times\",\n                color=get_disabled_entity_icon_color()\n            )\n            item.setData(icon, QtCore.Qt.DecorationRole)\n            item.setFlags(QtCore.Qt.NoItemFlags)\n            item.setColumnCount(self.columnCount())\n            self._empty_root_item = item\n        return self._empty_root_item\n\n    def set_root(self, root):\n        \"\"\"Change directory where to look for file.\"\"\"\n        self._root = root\n        if root and not os.path.exists(root):\n            log.debug(\"Work Area does not exist: {}\".format(root))\n        self.refresh()\n\n    def _clear(self):\n        root_item = self.invisibleRootItem()\n        rows = root_item.rowCount()\n        if rows > 0:\n            if self._invalid_item_visible:\n                for row in range(rows):\n                    root_item.takeRow(row)\n            else:\n                root_item.removeRows(0, rows)\n        self._items_by_filename = {}\n\n    def refresh(self):\n        \"\"\"Refresh and update model items.\"\"\"\n        root_item = self.invisibleRootItem()\n        # If path is not set or does not exist then add invalid path item\n        if not self._root or not os.path.exists(self._root):\n            self._clear()\n            # Add Work Area does not exist placeholder\n            item = self._get_invalid_path_item()\n            root_item.appendRow(item)\n            self._invalid_item_visible = True\n            return\n\n        # Clear items if previous refresh set '_invalid_item_visible' to True\n        # - Invalid items are not stored to '_items_by_filename' so they would\n        #   not be removed\n        if self._invalid_item_visible:\n            self._clear()\n\n        # Check for new items that should be added and items that should be\n        #   removed\n        new_items = []\n        items_to_remove = set(self._items_by_filename.keys())\n        for filename in os.listdir(self._root):\n            filepath = os.path.join(self._root, filename)\n            if os.path.isdir(filepath):\n                continue\n\n            ext = os.path.splitext(filename)[1]\n            if ext not in self._file_extensions:\n                continue\n\n            modified = os.path.getmtime(filepath)\n\n            # Use existing item or create new one\n            if filename in items_to_remove:\n                items_to_remove.remove(filename)\n                item = self._items_by_filename[filename]\n            else:\n                item = QtGui.QStandardItem(filename)\n                item.setColumnCount(self.columnCount())\n                item.setFlags(\n                    QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable\n                )\n                item.setData(self._file_icon, QtCore.Qt.DecorationRole)\n                new_items.append(item)\n                self._items_by_filename[filename] = item\n            # Update data that may be different\n            item.setData(filepath, FILEPATH_ROLE)\n            item.setData(modified, DATE_MODIFIED_ROLE)\n\n        # Add new items if there are any\n        if new_items:\n            root_item.appendRows(new_items)\n\n        # Remove items that are no longer available\n        for filename in items_to_remove:\n            item = self._items_by_filename.pop(filename)\n            root_item.removeRow(item.row())\n\n        # Add empty root item if there are not filenames that could be shown\n        if root_item.rowCount() > 0:\n            self._invalid_item_visible = False\n        else:\n            self._invalid_item_visible = True\n            item = self._get_empty_root_item()\n            root_item.appendRow(item)\n\n    def has_valid_items(self):\n        \"\"\"Directory has files that are listed in items.\"\"\"\n        return not self._invalid_item_visible\n\n    def flags(self, index):\n        # Use flags of first column for all columns\n        if index.column() != 0:\n            index = self.index(index.row(), 0, index.parent())\n        return super(WorkAreaFilesModel, self).flags(index)\n\n    def data(self, index, role=None):\n        if role is None:\n            role = QtCore.Qt.DisplayRole\n\n        # Handle roles for first column\n        if index.column() == 1:\n            if role == QtCore.Qt.DecorationRole:\n                return None\n\n            if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):\n                role = DATE_MODIFIED_ROLE\n            index = self.index(index.row(), 0, index.parent())\n\n        return super(WorkAreaFilesModel, self).data(index, role)\n\n    def headerData(self, section, orientation, role):\n        # Show nice labels in the header\n        if (\n            role == QtCore.Qt.DisplayRole\n            and orientation == QtCore.Qt.Horizontal\n        ):\n            if section == 0:\n                return \"Name\"\n            elif section == 1:\n                return \"Date modified\"\n\n        return super(WorkAreaFilesModel, self).headerData(\n            section, orientation, role\n        )\n\n\nclass PublishFilesModel(QtGui.QStandardItemModel):\n    \"\"\"Model filling files with published files calculated from representation.\n\n    This model looks for workfile family representations based on selected\n    asset and task.\n\n    Asset must set to be able look for representations that could be used.\n    Task is used to filter representations by task.\n    Model has few filter criteria for filling.\n    - First criteria is that version document must have \"workfile\" in\n        \"data.families\".\n    - Second cirteria is that representation must have extension same as\n        defined extensions\n    - If task is set then representation must have 'task[\"name\"]' with same\n        name.\n    \"\"\"\n\n    def __init__(self, extensions, dbcon, anatomy, *args, **kwargs):\n        super(PublishFilesModel, self).__init__(*args, **kwargs)\n\n        self.setColumnCount(2)\n\n        self._dbcon = dbcon\n        self._anatomy = anatomy\n\n        self._file_extensions = extensions\n\n        self._invalid_context_item = None\n        self._empty_root_item = None\n        self._file_icon = qtawesome.icon(\n            \"fa.file-o\",\n            color=get_default_entity_icon_color()\n        )\n        self._invalid_icon = qtawesome.icon(\n            \"fa.times\",\n            color=get_disabled_entity_icon_color()\n        )\n        self._invalid_item_visible = False\n\n        self._items_by_id = {}\n\n        self._asset_id = None\n        self._task_name = None\n\n    @property\n    def project_name(self):\n        return self._dbcon.Session[\"AVALON_PROJECT\"]\n\n    def _set_item_invalid(self, item):\n        item.setFlags(QtCore.Qt.NoItemFlags)\n        item.setData(self._invalid_icon, QtCore.Qt.DecorationRole)\n\n    def _set_item_valid(self, item):\n        item.setFlags(\n            QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable\n        )\n        item.setData(self._file_icon, QtCore.Qt.DecorationRole)\n\n    def _get_invalid_context_item(self):\n        if self._invalid_context_item is None:\n            item = QtGui.QStandardItem(\"Selected context is not valid.\")\n            item.setColumnCount(self.columnCount())\n            self._set_item_invalid(item)\n            self._invalid_context_item = item\n        return self._invalid_context_item\n\n    def _get_empty_root_item(self):\n        if self._empty_root_item is None:\n            item = QtGui.QStandardItem(\"Didn't find any published workfiles.\")\n            item.setColumnCount(self.columnCount())\n            self._set_item_invalid(item)\n            self._empty_root_item = item\n        return self._empty_root_item\n\n    def set_context(self, asset_id, task_name):\n        \"\"\"Change context to asset and task.\n\n        Args:\n            asset_id (ObjectId): Id of selected asset.\n            task_name (str): Name of selected task.\n        \"\"\"\n        self._asset_id = asset_id\n        self._task_name = task_name\n        self.refresh()\n\n    def _clear(self):\n        root_item = self.invisibleRootItem()\n        rows = root_item.rowCount()\n        if rows > 0:\n            if self._invalid_item_visible:\n                for row in range(rows):\n                    root_item.takeRow(row)\n            else:\n                root_item.removeRows(0, rows)\n        self._items_by_id = {}\n\n    def _get_workfie_representations(self):\n        output = []\n        # Get subset docs of asset\n        subset_docs = get_subsets(\n            self.project_name,\n            asset_ids=[self._asset_id],\n            fields=[\"_id\", \"name\"]\n        )\n\n        subset_ids = [subset_doc[\"_id\"] for subset_doc in subset_docs]\n        if not subset_ids:\n            return output\n\n        # Get version docs of subsets with their families\n        version_docs = get_versions(\n            self.project_name,\n            subset_ids=subset_ids,\n            fields=[\"_id\", \"parent\", \"data.families\"]\n        )\n\n        # Filter versions if they contain 'workfile' family\n        filtered_versions = []\n        for version_doc in version_docs:\n            data = version_doc.get(\"data\") or {}\n            families = data.get(\"families\") or []\n            if \"workfile\" in families:\n                filtered_versions.append(version_doc)\n\n        version_ids = [version_doc[\"_id\"] for version_doc in filtered_versions]\n        if not version_ids:\n            return output\n\n        # Query representations of filtered versions and add filter for\n        #   extension\n        extensions = [ext.replace(\".\", \"\") for ext in self._file_extensions]\n        repre_docs = get_representations(\n            self.project_name,\n            version_ids=version_ids,\n            context_filters={\"ext\": extensions}\n        )\n\n        # Filter queried representations by task name if task is set\n        filtered_repre_docs = []\n        for repre_doc in repre_docs:\n            if self._task_name is None:\n                filtered_repre_docs.append(repre_doc)\n                continue\n\n            task_info = repre_doc[\"context\"].get(\"task\")\n            if not task_info:\n                print(\"Not task info\")\n                continue\n\n            if isinstance(task_info, dict):\n                task_name = task_info.get(\"name\")\n            else:\n                task_name = task_info\n\n            if task_name == self._task_name:\n                filtered_repre_docs.append(repre_doc)\n\n        # Collect paths of representations\n        for repre_doc in filtered_repre_docs:\n            path = get_representation_path(\n                repre_doc, root=self._anatomy.roots\n            )\n            output.append((path, repre_doc[\"_id\"]))\n        return output\n\n    def refresh(self):\n        root_item = self.invisibleRootItem()\n        if not self._asset_id:\n            self._clear()\n            # Add Work Area does not exist placeholder\n            item = self._get_invalid_context_item()\n            root_item.appendRow(item)\n            self._invalid_item_visible = True\n            return\n\n        if self._invalid_item_visible:\n            self._clear()\n\n        new_items = []\n        items_to_remove = set(self._items_by_id.keys())\n        for item in self._get_workfie_representations():\n            filepath, repre_id = item\n            # TODO handle empty filepaths\n            if not filepath:\n                continue\n            filename = os.path.basename(filepath)\n\n            if repre_id in items_to_remove:\n                items_to_remove.remove(repre_id)\n                item = self._items_by_id[repre_id]\n            else:\n                item = QtGui.QStandardItem(filename)\n                item.setColumnCount(self.columnCount())\n                new_items.append(item)\n                self._items_by_id[repre_id] = item\n\n            if os.path.exists(filepath):\n                modified = os.path.getmtime(filepath)\n                tooltip = None\n                self._set_item_valid(item)\n            else:\n                modified = None\n                tooltip = \"File is not available from this machine\"\n                self._set_item_invalid(item)\n\n            item.setData(tooltip, QtCore.Qt.ToolTipRole)\n            item.setData(filepath, FILEPATH_ROLE)\n            item.setData(modified, DATE_MODIFIED_ROLE)\n            item.setData(repre_id, ITEM_ID_ROLE)\n\n        if new_items:\n            root_item.appendRows(new_items)\n\n        for filename in items_to_remove:\n            item = self._items_by_id.pop(filename)\n            root_item.removeRow(item.row())\n\n        if root_item.rowCount() > 0:\n            self._invalid_item_visible = False\n        else:\n            self._invalid_item_visible = True\n            item = self._get_empty_root_item()\n            root_item.appendRow(item)\n\n    def has_valid_items(self):\n        return not self._invalid_item_visible\n\n    def flags(self, index):\n        if index.column() != 0:\n            index = self.index(index.row(), 0, index.parent())\n        return super(PublishFilesModel, self).flags(index)\n\n    def data(self, index, role=None):\n        if role is None:\n            role = QtCore.Qt.DisplayRole\n\n        if index.column() == 1:\n            if role == QtCore.Qt.DecorationRole:\n                return None\n\n            if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):\n                role = DATE_MODIFIED_ROLE\n            index = self.index(index.row(), 0, index.parent())\n\n        return super(PublishFilesModel, self).data(index, role)\n\n    def headerData(self, section, orientation, role):\n        # Show nice labels in the header\n        if (\n            role == QtCore.Qt.DisplayRole\n            and orientation == QtCore.Qt.Horizontal\n        ):\n            if section == 0:\n                return \"Name\"\n            elif section == 1:\n                return \"Date modified\"\n\n        return super(PublishFilesModel, self).headerData(\n            section, orientation, role\n        )\n"
  },
  {
    "path": "openpype/tools/workfiles/save_as_dialog.py",
    "content": "import os\nimport re\nimport copy\nimport logging\n\nfrom qtpy import QtWidgets, QtCore\n\nfrom openpype.pipeline import (\n    registered_host,\n    legacy_io,\n)\nfrom openpype.pipeline.workfile import get_last_workfile_with_version\nfrom openpype.pipeline.template_data import get_template_data_with_names\nfrom openpype.tools.utils import PlaceholderLineEdit\nfrom openpype.pipeline import version_start, get_current_host_name\n\nlog = logging.getLogger(__name__)\n\n\ndef build_workfile_data(session):\n    \"\"\"Get the data required for workfile formatting from avalon `session`\"\"\"\n\n    # Set work file data for template formatting\n    project_name = session[\"AVALON_PROJECT\"]\n    asset_name = session[\"AVALON_ASSET\"]\n    task_name = session[\"AVALON_TASK\"]\n    host_name = session[\"AVALON_APP\"]\n\n    data = get_template_data_with_names(\n        project_name, asset_name, task_name, host_name\n    )\n    data.update({\n        \"version\": 1,\n        \"comment\": \"\",\n        \"ext\": None\n    })\n\n    return data\n\n\nclass CommentMatcher(object):\n    \"\"\"Use anatomy and work file data to parse comments from filenames\"\"\"\n    def __init__(self, anatomy, template_key, data):\n\n        self.fname_regex = None\n\n        template = anatomy.templates[template_key][\"file\"]\n        if \"{comment}\" not in template:\n            # Don't look for comment if template doesn't allow it\n            return\n\n        # Create a regex group for extensions\n        extensions = registered_host().file_extensions()\n        any_extension = \"(?:{})\".format(\n            \"|\".join(re.escape(ext.lstrip(\".\")) for ext in extensions)\n        )\n\n        # Use placeholders that will never be in the filename\n        temp_data = copy.deepcopy(data)\n        temp_data[\"comment\"] = \"<<comment>>\"\n        temp_data[\"version\"] = \"<<version>>\"\n        temp_data[\"ext\"] = \"<<ext>>\"\n\n        template_obj = anatomy.templates_obj[template_key][\"file\"]\n        fname_pattern = template_obj.format_strict(temp_data)\n        fname_pattern = re.escape(fname_pattern)\n\n        # Replace comment and version with something we can match with regex\n        replacements = {\n            \"<<comment>>\": \"(.+)\",\n            \"<<version>>\": \"[0-9]+\",\n            \"<<ext>>\": any_extension,\n        }\n        for src, dest in replacements.items():\n            fname_pattern = fname_pattern.replace(re.escape(src), dest)\n\n        # Match from beginning to end of string to be safe\n        fname_pattern = \"^{}$\".format(fname_pattern)\n\n        self.fname_regex = re.compile(fname_pattern)\n\n    def parse_comment(self, filepath):\n        \"\"\"Parse the {comment} part from a filename\"\"\"\n        if not self.fname_regex:\n            return\n\n        fname = os.path.basename(filepath)\n        match = self.fname_regex.match(fname)\n        if match:\n            return match.group(1)\n\n\nclass SubversionLineEdit(QtWidgets.QWidget):\n    \"\"\"QLineEdit with QPushButton for drop down selection of list of strings\"\"\"\n\n    text_changed = QtCore.Signal(str)\n\n    def __init__(self, *args, **kwargs):\n        super(SubversionLineEdit, self).__init__(*args, **kwargs)\n\n        input_field = PlaceholderLineEdit(self)\n        menu_btn = QtWidgets.QPushButton(self)\n        menu_btn.setFixedWidth(18)\n\n        menu = QtWidgets.QMenu(self)\n        menu_btn.setMenu(menu)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(3)\n\n        layout.addWidget(input_field, 1)\n        layout.addWidget(menu_btn, 0)\n\n        input_field.textChanged.connect(self.text_changed)\n\n        self.setFocusProxy(input_field)\n\n        self._input_field = input_field\n        self._menu_btn = menu_btn\n        self._menu = menu\n\n    def set_placeholder(self, placeholder):\n        self._input_field.setPlaceholderText(placeholder)\n\n    def set_text(self, text):\n        self._input_field.setText(text)\n\n    def set_values(self, values):\n        self._update(values)\n\n    def _on_button_clicked(self):\n        self._menu.exec_()\n\n    def _on_action_clicked(self, action):\n        self._input_field.setText(action.text())\n\n    def _update(self, values):\n        \"\"\"Create optional predefined subset names\n\n        Args:\n            default_names(list): all predefined names\n\n        Returns:\n             None\n        \"\"\"\n\n        menu = self._menu\n        button = self._menu_btn\n\n        state = any(values)\n        button.setEnabled(state)\n        if state is False:\n            return\n\n        # Include an empty string\n        values = [\"\"] + sorted(values)\n\n        # Get and destroy the action group\n        group = button.findChild(QtWidgets.QActionGroup)\n        if group:\n            group.deleteLater()\n\n        # Build new action group\n        group = QtWidgets.QActionGroup(button)\n        for name in values:\n            action = group.addAction(name)\n            menu.addAction(action)\n\n        group.triggered.connect(self._on_action_clicked)\n\n\nclass SaveAsDialog(QtWidgets.QDialog):\n    \"\"\"Name Window to define a unique filename inside a root folder\n\n    The filename will be based on the \"workfile\" template defined in the\n    project[\"config\"][\"template\"].\n\n    \"\"\"\n\n    def __init__(\n        self, parent, root, anatomy, template_key, extensions, session=None\n    ):\n        super(SaveAsDialog, self).__init__(parent=parent)\n        self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)\n\n        self.result = None\n        self.host = registered_host()\n        self.root = root\n        self.work_file = None\n        self._extensions = extensions\n\n        if not session:\n            # Fallback to active session\n            session = legacy_io.Session\n\n        self.data = build_workfile_data(session)\n\n        # Store project anatomy\n        self.anatomy = anatomy\n        self.template = anatomy.templates[template_key][\"file\"]\n        self.template_key = template_key\n\n        # Btns widget\n        btns_widget = QtWidgets.QWidget(self)\n\n        btn_ok = QtWidgets.QPushButton(\"Ok\", btns_widget)\n        btn_cancel = QtWidgets.QPushButton(\"Cancel\", btns_widget)\n\n        btns_layout = QtWidgets.QHBoxLayout(btns_widget)\n        btns_layout.addWidget(btn_ok)\n        btns_layout.addWidget(btn_cancel)\n\n        # Inputs widget\n        inputs_widget = QtWidgets.QWidget(self)\n\n        # Version widget\n        version_widget = QtWidgets.QWidget(inputs_widget)\n\n        # Version number input\n        version_input = QtWidgets.QSpinBox(version_widget)\n        version_input.setMinimum(\n            version_start.get_versioning_start(\n                self.data[\"project\"][\"name\"],\n                get_current_host_name(),\n                task_name=self.data[\"task\"][\"name\"],\n                task_type=self.data[\"task\"][\"type\"],\n                family=\"workfile\"\n            )\n        )\n        version_input.setMaximum(9999)\n\n        # Last version checkbox\n        last_version_check = QtWidgets.QCheckBox(\n            \"Next Available Version\", version_widget\n        )\n        last_version_check.setChecked(True)\n\n        version_layout = QtWidgets.QHBoxLayout(version_widget)\n        version_layout.setContentsMargins(0, 0, 0, 0)\n        version_layout.addWidget(version_input)\n        version_layout.addWidget(last_version_check)\n\n        # Preview widget\n        preview_label = QtWidgets.QLabel(\"Preview filename\", inputs_widget)\n\n        # Subversion input\n        subversion = SubversionLineEdit(inputs_widget)\n        subversion.set_placeholder(\"Will be part of filename.\")\n\n        # Extensions combobox\n        ext_combo = QtWidgets.QComboBox(inputs_widget)\n        # Add styled delegate to use stylesheets\n        ext_delegate = QtWidgets.QStyledItemDelegate()\n        ext_combo.setItemDelegate(ext_delegate)\n        ext_combo.addItems(self._extensions)\n\n        # Build inputs\n        inputs_layout = QtWidgets.QFormLayout(inputs_widget)\n        # Add version only if template contains version key\n        # - since the version can be padded with \"{version:0>4}\" we only search\n        #   for \"{version\".\n        if \"{version\" in self.template:\n            inputs_layout.addRow(\"Version:\", version_widget)\n        else:\n            version_widget.setVisible(False)\n\n        # Add subversion only if template contains `{comment}`\n        if \"{comment}\" in self.template:\n            inputs_layout.addRow(\"Subversion:\", subversion)\n\n            # Detect whether a {comment} is in the current filename - if so,\n            # preserve it by default and set it in the comment/subversion field\n            current_filepath = self.host.current_file()\n            if current_filepath:\n                # We match the current filename against the current session\n                # instead of the session where the user is saving to.\n                current_data = build_workfile_data(legacy_io.Session)\n                matcher = CommentMatcher(anatomy, template_key, current_data)\n                comment = matcher.parse_comment(current_filepath)\n                if comment:\n                    log.info(\"Detected subversion comment: {}\".format(comment))\n                    self.data[\"comment\"] = comment\n                    subversion.set_text(comment)\n\n            existing_comments = self.get_existing_comments()\n            subversion.set_values(existing_comments)\n\n        else:\n            subversion.setVisible(False)\n        inputs_layout.addRow(\"Extension:\", ext_combo)\n        inputs_layout.addRow(\"Preview:\", preview_label)\n\n        # Build layout\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.addWidget(inputs_widget)\n        main_layout.addWidget(btns_widget)\n\n        # Signal callback registration\n        version_input.valueChanged.connect(self.on_version_spinbox_changed)\n        last_version_check.stateChanged.connect(\n            self.on_version_checkbox_changed\n        )\n\n        subversion.text_changed.connect(self.on_comment_changed)\n        ext_combo.currentIndexChanged.connect(self.on_extension_changed)\n\n        btn_ok.pressed.connect(self.on_ok_pressed)\n        btn_cancel.pressed.connect(self.on_cancel_pressed)\n\n        # Allow \"Enter\" key to accept the save.\n        btn_ok.setDefault(True)\n\n        # Force default focus to comment, some hosts didn't automatically\n        # apply focus to this line edit (e.g. Houdini)\n        subversion.setFocus()\n\n        # Store widgets\n        self.btn_ok = btn_ok\n\n        self.version_widget = version_widget\n\n        self.version_input = version_input\n        self.last_version_check = last_version_check\n\n        self.preview_label = preview_label\n        self.subversion = subversion\n        self.ext_combo = ext_combo\n        self._ext_delegate = ext_delegate\n\n        self.refresh()\n\n    def get_existing_comments(self):\n        matcher = CommentMatcher(self.anatomy, self.template_key, self.data)\n        host_extensions = set(self._extensions)\n        comments = set()\n        if os.path.isdir(self.root):\n            for fname in os.listdir(self.root):\n                if not os.path.isfile(os.path.join(self.root, fname)):\n                    continue\n\n                ext = os.path.splitext(fname)[-1]\n                if ext not in host_extensions:\n                    continue\n\n                comment = matcher.parse_comment(fname)\n                if comment:\n                    comments.add(comment)\n\n        return list(comments)\n\n    def on_version_spinbox_changed(self, value):\n        self.data[\"version\"] = value\n        self.refresh()\n\n    def on_version_checkbox_changed(self, _value):\n        self.refresh()\n\n    def on_comment_changed(self, text):\n        self.data[\"comment\"] = text\n        self.refresh()\n\n    def on_extension_changed(self):\n        ext = self.ext_combo.currentText()\n        if ext == self.data[\"ext\"]:\n            return\n        self.data[\"ext\"] = ext\n        self.refresh()\n\n    def on_ok_pressed(self):\n        self.result = self.work_file\n        self.close()\n\n    def on_cancel_pressed(self):\n        self.close()\n\n    def get_result(self):\n        return self.result\n\n    def get_work_file(self):\n        data = copy.deepcopy(self.data)\n        if not data[\"comment\"]:\n            data.pop(\"comment\", None)\n\n        data[\"ext\"] = data[\"ext\"].lstrip(\".\")\n\n        template_obj = self.anatomy.templates_obj[self.template_key][\"file\"]\n        return template_obj.format_strict(data)\n\n    def refresh(self):\n        extensions = list(self._extensions)\n        extension = self.data[\"ext\"]\n        if extension is None:\n            # Define saving file extension\n            current_file = self.host.current_file()\n            if current_file:\n                # Match the extension of current file\n                _, extension = os.path.splitext(current_file)\n            else:\n                extension = extensions[0]\n\n        if extension != self.data[\"ext\"]:\n            self.data[\"ext\"] = extension\n            index = self.ext_combo.findText(\n                extension, QtCore.Qt.MatchFixedString\n            )\n            if index >= 0:\n                self.ext_combo.setCurrentIndex(index)\n\n        if not self.last_version_check.isChecked():\n            self.version_input.setEnabled(True)\n            self.data[\"version\"] = self.version_input.value()\n\n            work_file = self.get_work_file()\n\n        else:\n            self.version_input.setEnabled(False)\n\n            data = copy.deepcopy(self.data)\n            template = str(self.template)\n\n            if not data[\"comment\"]:\n                data.pop(\"comment\", None)\n\n            data[\"ext\"] = data[\"ext\"].lstrip(\".\")\n\n            version = get_last_workfile_with_version(\n                self.root, template, data, extensions\n            )[1]\n\n            if version is None:\n                version = version_start.get_versioning_start(\n                    data[\"project\"][\"name\"],\n                    get_current_host_name(),\n                    task_name=self.data[\"task\"][\"name\"],\n                    task_type=self.data[\"task\"][\"type\"],\n                    family=\"workfile\"\n                )\n            else:\n                version += 1\n\n            found_valid_version = False\n            # Check if next version is valid version and give a chance to try\n            # next 100 versions\n            for idx in range(100):\n                # Store version to data\n                self.data[\"version\"] = version\n\n                work_file = self.get_work_file()\n                # Safety check\n                path = os.path.join(self.root, work_file)\n                if not os.path.exists(path):\n                    found_valid_version = True\n                    break\n\n                # Try next version\n                version += 1\n                # Log warning\n                if idx == 0:\n                    log.warning((\n                        \"BUG: Function `get_last_workfile_with_version` \"\n                        \"didn't return last version.\"\n                    ))\n            # Raise exception if even 100 version fallback didn't help\n            if not found_valid_version:\n                raise AssertionError(\n                    \"This is a bug. Couldn't find valid version!\"\n                )\n\n        self.work_file = work_file\n\n        path_exists = os.path.exists(os.path.join(self.root, work_file))\n\n        self.btn_ok.setEnabled(not path_exists)\n\n        if path_exists:\n            self.preview_label.setText(\n                \"<font color='red'>Cannot create \\\"{0}\\\" because file exists!\"\n                \"</font>\".format(work_file)\n            )\n        else:\n            self.preview_label.setText(\n                \"<font color='green'>{0}</font>\".format(work_file)\n            )\n"
  },
  {
    "path": "openpype/tools/workfiles/window.py",
    "content": "import os\nimport datetime\nimport copy\nimport platform\nfrom qtpy import QtCore, QtWidgets, QtGui\n\nfrom openpype.client import (\n    get_asset_by_name,\n    get_workfile_info,\n)\nfrom openpype.client.operations import (\n    OperationsSession,\n    new_workfile_info_doc,\n    prepare_workfile_info_update_data,\n)\nfrom openpype import style\nfrom openpype import resources\nfrom openpype.pipeline import (\n    Anatomy,\n    get_current_project_name,\n    get_current_asset_name,\n    get_current_task_name,\n)\nfrom openpype.pipeline import legacy_io\nfrom openpype.tools.utils.assets_widget import SingleSelectAssetsWidget\nfrom openpype.tools.utils.tasks_widget import TasksWidget\n\nfrom .files_widget import FilesWidget\n\n\ndef file_size_to_string(file_size):\n    size = 0\n    size_ending_mapping = {\n        \"KB\": 1024 ** 1,\n        \"MB\": 1024 ** 2,\n        \"GB\": 1024 ** 3\n    }\n    ending = \"B\"\n    for _ending, _size in size_ending_mapping.items():\n        if file_size < _size:\n            break\n        size = file_size / _size\n        ending = _ending\n    return \"{:.2f} {}\".format(size, ending)\n\n\nclass SidePanelWidget(QtWidgets.QWidget):\n    save_clicked = QtCore.Signal()\n    published_workfile_message = (\n        \"<b>INFO</b>: Opened published workfiles will be stored in\"\n        \" temp directory on your machine. Current temp size: <b>{}</b>.\"\n    )\n\n    def __init__(self, parent=None):\n        super(SidePanelWidget, self).__init__(parent)\n\n        details_label = QtWidgets.QLabel(\"Details\", self)\n        details_input = QtWidgets.QPlainTextEdit(self)\n        details_input.setReadOnly(True)\n\n        artist_note_widget = QtWidgets.QWidget(self)\n        note_label = QtWidgets.QLabel(\"Artist note\", artist_note_widget)\n        note_input = QtWidgets.QPlainTextEdit(artist_note_widget)\n        btn_note_save = QtWidgets.QPushButton(\"Save note\", artist_note_widget)\n\n        artist_note_layout = QtWidgets.QVBoxLayout(artist_note_widget)\n        artist_note_layout.setContentsMargins(0, 0, 0, 0)\n        artist_note_layout.addWidget(note_label, 0)\n        artist_note_layout.addWidget(note_input, 1)\n        artist_note_layout.addWidget(\n            btn_note_save, 0, alignment=QtCore.Qt.AlignRight\n        )\n\n        main_layout = QtWidgets.QVBoxLayout(self)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(details_label, 0)\n        main_layout.addWidget(details_input, 1)\n        main_layout.addWidget(artist_note_widget, 1)\n\n        note_input.textChanged.connect(self._on_note_change)\n        btn_note_save.clicked.connect(self._on_save_click)\n\n        self._details_input = details_input\n        self._artist_note_widget = artist_note_widget\n        self._note_input = note_input\n        self._btn_note_save = btn_note_save\n\n        self._orig_note = \"\"\n        self._workfile_doc = None\n\n    def set_published_visible(self, published_visible):\n        self._artist_note_widget.setVisible(not published_visible)\n\n    def _on_note_change(self):\n        text = self._note_input.toPlainText()\n        self._btn_note_save.setEnabled(self._orig_note != text)\n\n    def _on_save_click(self):\n        self._orig_note = self._note_input.toPlainText()\n        self._on_note_change()\n        self.save_clicked.emit()\n\n    def get_user_name(self, file):\n        \"\"\"Get user name from file path\"\"\"\n        # Only run on Unix because pwd module is not available on Windows.\n        # NOTE: we tried adding \"win32security\" module but it was not working\n        # on all hosts so we decided to just support Linux until migration\n        # to Ayon\n        if platform.system().lower() == \"windows\":\n            return None\n        import pwd\n\n        filestat = os.stat(file)\n        return pwd.getpwuid(filestat.st_uid).pw_name\n\n    def set_context(self, asset_id, task_name, filepath, workfile_doc):\n        # Check if asset, task and file are selected\n        # NOTE workfile document is not requirement\n        enabled = bool(asset_id) and bool(task_name) and bool(filepath)\n\n        self._details_input.setEnabled(enabled)\n        self._note_input.setEnabled(enabled)\n        self._btn_note_save.setEnabled(enabled)\n\n        # Make sure workfile doc is overridden\n        self._workfile_doc = workfile_doc\n        # Disable inputs and remove texts if any required arguments are missing\n        if not enabled:\n            self._orig_note = \"\"\n            self._details_input.setPlainText(\"\")\n            self._note_input.setPlainText(\"\")\n            return\n\n        orig_note = \"\"\n        if workfile_doc:\n            orig_note = workfile_doc[\"data\"].get(\"note\") or orig_note\n\n        self._orig_note = orig_note\n        self._note_input.setPlainText(orig_note)\n        # Set as empty string\n        self._details_input.setPlainText(\"\")\n\n        filestat = os.stat(filepath)\n        size_value = file_size_to_string(filestat.st_size)\n\n        # Append html string\n        datetime_format = \"%b %d %Y %H:%M:%S\"\n        creation_time = datetime.datetime.fromtimestamp(filestat.st_ctime)\n        modification_time = datetime.datetime.fromtimestamp(filestat.st_mtime)\n        lines = (\n            \"<b>Size:</b>\",\n            size_value,\n            \"<b>Created:</b>\",\n            creation_time.strftime(datetime_format),\n            \"<b>Modified:</b>\",\n            modification_time.strftime(datetime_format),\n        )\n        username = self.get_user_name(filepath)\n        if username:\n            lines += (\n                \"<b>User:</b>\",\n                username,\n            )\n        self._details_input.appendHtml(\"<br>\".join(lines))\n\n    def get_workfile_data(self):\n        data = {\n            \"note\": self._note_input.toPlainText()\n        }\n        return self._workfile_doc, data\n\n\nclass Window(QtWidgets.QWidget):\n    \"\"\"Work Files Window\"\"\"\n    title = \"Work Files\"\n\n    def __init__(self, parent=None):\n        super(Window, self).__init__(parent=parent)\n        self.setWindowTitle(self.title)\n        icon = QtGui.QIcon(resources.get_openpype_icon_filepath())\n        self.setWindowIcon(icon)\n        self.setWindowFlags(self.windowFlags() | QtCore.Qt.Window)\n\n        # Create pages widget and set it as central widget\n        pages_widget = QtWidgets.QStackedWidget(self)\n\n        home_page_widget = QtWidgets.QWidget(pages_widget)\n        home_body_widget = QtWidgets.QWidget(home_page_widget)\n\n        assets_widget = SingleSelectAssetsWidget(\n            legacy_io, parent=home_body_widget\n        )\n        assets_widget.set_current_asset_btn_visibility(True)\n\n        tasks_widget = TasksWidget(legacy_io, home_body_widget)\n        files_widget = FilesWidget(home_body_widget)\n        side_panel = SidePanelWidget(home_body_widget)\n\n        pages_widget.addWidget(home_page_widget)\n\n        # Build home\n        home_page_layout = QtWidgets.QVBoxLayout(home_page_widget)\n        home_page_layout.addWidget(home_body_widget)\n\n        # Build home - body\n        body_layout = QtWidgets.QVBoxLayout(home_body_widget)\n        split_widget = QtWidgets.QSplitter(home_body_widget)\n        split_widget.addWidget(assets_widget)\n        split_widget.addWidget(tasks_widget)\n        split_widget.addWidget(files_widget)\n        split_widget.addWidget(side_panel)\n        split_widget.setSizes([255, 160, 455, 175])\n\n        body_layout.addWidget(split_widget)\n\n        # Add top margin for tasks to align it visually with files as\n        # the files widget has a filter field which tasks does not.\n        tasks_widget.setContentsMargins(0, 32, 0, 0)\n\n        main_layout = QtWidgets.QHBoxLayout(self)\n        main_layout.addWidget(pages_widget, 1)\n\n        # Set context after asset widget is refreshed\n        # - to do so it is necessary to wait until refresh is done\n        set_context_timer = QtCore.QTimer()\n        set_context_timer.setInterval(100)\n\n        # Connect signals\n        set_context_timer.timeout.connect(self._on_context_set_timeout)\n        assets_widget.selection_changed.connect(self._on_asset_changed)\n        tasks_widget.task_changed.connect(self._on_task_changed)\n        files_widget.file_selected.connect(self.on_file_select)\n        files_widget.workfile_created.connect(self.on_workfile_create)\n        files_widget.file_opened.connect(self._on_file_opened)\n        files_widget.published_visible_changed.connect(\n            self._on_published_change\n        )\n        side_panel.save_clicked.connect(self.on_side_panel_save)\n\n        self._set_context_timer = set_context_timer\n        self.home_page_widget = home_page_widget\n        self.pages_widget = pages_widget\n        self.home_body_widget = home_body_widget\n        self.split_widget = split_widget\n\n        self.assets_widget = assets_widget\n        self.tasks_widget = tasks_widget\n        self.files_widget = files_widget\n        self.side_panel = side_panel\n\n        # Force focus on the open button by default, required for Houdini.\n        files_widget.setFocus()\n\n        self.resize(1200, 600)\n\n        self._first_show = True\n        self._context_to_set = None\n\n    def ensure_visible(\n        self, use_context=None, save=None, on_top=None\n    ):\n        if save is None:\n            save = True\n\n        self.set_save_enabled(save)\n\n        if self.isVisible():\n            use_context = False\n        elif use_context is None:\n            use_context = True\n\n        if on_top is None and self._first_show:\n            on_top = self.parent() is None\n\n        window_flags = self.windowFlags()\n        new_window_flags = window_flags\n        if on_top is True:\n            new_window_flags = window_flags | QtCore.Qt.WindowStaysOnTopHint\n        elif on_top is False:\n            new_window_flags = window_flags & ~QtCore.Qt.WindowStaysOnTopHint\n\n        if new_window_flags != window_flags:\n            # Note this is not propagated after initialization of widget in\n            #   some Qt builds\n            self.setWindowFlags(new_window_flags)\n            self.show()\n\n        elif not self.isVisible():\n            self.show()\n\n        if use_context is None or use_context is True:\n            context = {\n                \"asset\": get_current_asset_name(),\n                \"task\": get_current_task_name()\n            }\n            self.set_context(context)\n\n        # Pull window to the front.\n        self.raise_()\n        self.activateWindow()\n\n    @property\n    def project_name(self):\n        return get_current_project_name()\n\n    def showEvent(self, event):\n        super(Window, self).showEvent(event)\n        if self._first_show:\n            self._first_show = False\n            self.refresh()\n            self.setStyleSheet(style.load_stylesheet())\n\n    def keyPressEvent(self, event):\n        \"\"\"Custom keyPressEvent.\n\n        Override keyPressEvent to do nothing so that Maya's panels won't\n        take focus when pressing \"SHIFT\" whilst mouse is over viewport or\n        outliner. This way users don't accidentally perform Maya commands\n        whilst trying to name an instance.\n\n        \"\"\"\n\n    def set_save_enabled(self, enabled):\n        self.files_widget.set_save_enabled(enabled)\n\n    def on_file_select(self, filepath):\n        asset_id = self.assets_widget.get_selected_asset_id()\n        task_name = self.tasks_widget.get_selected_task_name()\n\n        workfile_doc = None\n        if asset_id and task_name and filepath:\n            filename = os.path.split(filepath)[1]\n            project_name = self.project_name\n            workfile_doc = get_workfile_info(\n                project_name, asset_id, task_name, filename\n            )\n        self.side_panel.set_context(\n            asset_id, task_name, filepath, workfile_doc\n        )\n\n    def on_workfile_create(self, filepath):\n        self._create_workfile_doc(filepath)\n\n    def _on_file_opened(self):\n        self.close()\n\n    def _on_published_change(self, visible):\n        self.side_panel.set_published_visible(visible)\n\n    def on_side_panel_save(self):\n        workfile_doc, data = self.side_panel.get_workfile_data()\n        if not workfile_doc:\n            filepath = self.files_widget._get_selected_filepath()\n            workfile_doc = self._create_workfile_doc(filepath)\n\n        new_workfile_doc = copy.deepcopy(workfile_doc)\n        new_workfile_doc[\"data\"] = data\n        update_data = prepare_workfile_info_update_data(\n            workfile_doc, new_workfile_doc\n        )\n        if not update_data:\n            return\n\n        project_name = self.project_name\n\n        session = OperationsSession()\n        session.update_entity(\n            project_name, \"workfile\", workfile_doc[\"_id\"], update_data\n        )\n        session.commit()\n\n    def _get_current_workfile_doc(self, filepath=None):\n        if filepath is None:\n            filepath = self.files_widget._get_selected_filepath()\n        task_name = self.tasks_widget.get_selected_task_name()\n        asset_id = self.assets_widget.get_selected_asset_id()\n        if not task_name or not asset_id or not filepath:\n            return\n\n        filename = os.path.split(filepath)[1]\n        project_name = self.project_name\n        return get_workfile_info(\n            project_name, asset_id, task_name, filename\n        )\n\n    def _create_workfile_doc(self, filepath):\n        workfile_doc = self._get_current_workfile_doc(filepath)\n        if workfile_doc:\n            return workfile_doc\n\n        workdir, filename = os.path.split(filepath)\n\n        project_name = self.project_name\n        asset_id = self.assets_widget.get_selected_asset_id()\n        task_name = self.tasks_widget.get_selected_task_name()\n\n        anatomy = Anatomy(project_name)\n        success, rootless_dir = anatomy.find_root_template_from_path(workdir)\n        filepath = \"/\".join([\n            os.path.normpath(rootless_dir).replace(\"\\\\\", \"/\"),\n            filename\n        ])\n\n        workfile_doc = new_workfile_info_doc(\n            filename, asset_id, task_name, [filepath]\n        )\n\n        session = OperationsSession()\n        session.create_entity(project_name, \"workfile\", workfile_doc)\n        session.commit()\n        return workfile_doc\n\n    def refresh(self):\n        # Refresh asset widget\n        self.assets_widget.refresh()\n\n        self._on_task_changed()\n\n    def set_context(self, context):\n        self._context_to_set = context\n        self._set_context_timer.start()\n\n    def _on_context_set_timeout(self):\n        if self._context_to_set is None:\n            self._set_context_timer.stop()\n            return\n\n        if self.assets_widget.refreshing:\n            return\n\n        self._set_context_timer.stop()\n        self._context_to_set, context = None, self._context_to_set\n        if \"asset\" in context:\n            asset_doc = get_asset_by_name(\n                self.project_name, context[\"asset\"], fields=[\"_id\"]\n            )\n\n            asset_id = None\n            if asset_doc:\n                asset_id = asset_doc[\"_id\"]\n            # Select the asset\n            self.assets_widget.select_asset(asset_id)\n            self.tasks_widget.set_asset_id(asset_id)\n\n        if \"task\" in context:\n            self.tasks_widget.select_task_name(context[\"task\"])\n        self._on_task_changed()\n\n    def _on_asset_changed(self):\n        asset_id = self.assets_widget.get_selected_asset_id()\n        if asset_id:\n            self.tasks_widget.setEnabled(True)\n        else:\n            # Force disable the other widgets if no\n            # active selection\n            self.tasks_widget.setEnabled(False)\n            self.files_widget.setEnabled(False)\n\n        self.tasks_widget.set_asset_id(asset_id)\n\n    def _on_task_changed(self):\n        asset_id = self.assets_widget.get_selected_asset_id()\n        task_name = self.tasks_widget.get_selected_task_name()\n        task_type = self.tasks_widget.get_selected_task_type()\n\n        asset_is_valid = asset_id is not None\n        self.tasks_widget.setEnabled(asset_is_valid)\n\n        self.files_widget.setEnabled(bool(task_name) and asset_is_valid)\n        self.files_widget.set_asset_task(asset_id, task_name, task_type)\n        self.files_widget.refresh()\n"
  },
  {
    "path": "openpype/vendor/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/common/README.md",
    "content": "### Info\nHere are modules that can run under both Python 2 and Python 3 hosts."
  },
  {
    "path": "openpype/vendor/python/common/ayon_api/__init__.py",
    "content": "from .version import __version__\nfrom .utils import (\n    TransferProgress,\n    slugify_string,\n    create_dependency_package_basename,\n)\nfrom .server_api import (\n    ServerAPI,\n)\n\nfrom ._api import (\n    GlobalServerAPI,\n    ServiceContext,\n\n    init_service,\n    get_service_name,\n    get_service_addon_name,\n    get_service_addon_version,\n    get_service_addon_settings,\n\n    is_connection_created,\n    create_connection,\n    close_connection,\n    change_token,\n    set_environments,\n    get_server_api_connection,\n    get_site_id,\n    set_site_id,\n    get_client_version,\n    set_client_version,\n    get_default_settings_variant,\n    set_default_settings_variant,\n    get_sender,\n    set_sender,\n\n    get_base_url,\n    get_rest_url,\n\n    raw_get,\n    raw_post,\n    raw_put,\n    raw_patch,\n    raw_delete,\n\n    get,\n    post,\n    put,\n    patch,\n    delete,\n\n    get_timeout,\n    set_timeout,\n    get_max_retries,\n    set_max_retries,\n\n    get_event,\n    get_events,\n    dispatch_event,\n    update_event,\n    enroll_event_job,\n\n    download_file,\n    upload_file,\n\n    query_graphql,\n\n    get_addons_info,\n    get_addon_url,\n    download_addon_private_file,\n\n    get_installers,\n    create_installer,\n    update_installer,\n    delete_installer,\n    download_installer,\n    upload_installer,\n\n    get_dependency_packages,\n    create_dependency_package,\n    update_dependency_package,\n    delete_dependency_package,\n\n    download_dependency_package,\n    upload_dependency_package,\n\n    upload_addon_zip,\n\n    get_bundles,\n    create_bundle,\n    update_bundle,\n    delete_bundle,\n\n    get_info,\n    get_server_version,\n    get_server_version_tuple,\n    get_user,\n    get_users,\n\n    get_attributes_for_type,\n    get_attributes_fields_for_type,\n    get_default_fields_for_type,\n\n    get_project_anatomy_preset,\n    get_project_anatomy_presets,\n    get_project_roots_by_site,\n    get_project_roots_for_site,\n\n    get_addon_site_settings_schema,\n    get_addon_settings_schema,\n\n    get_addon_studio_settings,\n    get_addon_project_settings,\n    get_addon_settings,\n    get_bundle_settings,\n    get_addons_studio_settings,\n    get_addons_project_settings,\n    get_addons_settings,\n\n    get_secrets,\n    get_secret,\n    save_secret,\n    delete_secret,\n\n    get_project_names,\n    get_projects,\n    get_project,\n    create_project,\n    update_project,\n    delete_project,\n\n    get_folder_by_id,\n    get_folder_by_name,\n    get_folder_by_path,\n    get_folders,\n    get_folders_hierarchy,\n\n    get_tasks,\n    get_task_by_id,\n    get_task_by_name,\n\n    get_folder_ids_with_products,\n    get_product_by_id,\n    get_product_by_name,\n    get_products,\n    get_product_types,\n    get_project_product_types,\n    get_product_type_names,\n\n    get_version_by_id,\n    get_version_by_name,\n    version_is_latest,\n    get_versions,\n    get_hero_version_by_product_id,\n    get_hero_version_by_id,\n    get_hero_versions,\n    get_last_versions,\n    get_last_version_by_product_id,\n    get_last_version_by_product_name,\n    get_representation_by_id,\n    get_representation_by_name,\n    get_representations,\n    get_representations_parents,\n    get_representation_parents,\n    get_repre_ids_by_context_filters,\n\n    get_workfiles_info,\n    get_workfile_info,\n    get_workfile_info_by_id,\n\n    get_thumbnail_by_id,\n    get_thumbnail,\n    get_folder_thumbnail,\n    get_version_thumbnail,\n    get_workfile_thumbnail,\n    create_thumbnail,\n    update_thumbnail,\n\n    get_full_link_type_name,\n    get_link_types,\n    get_link_type,\n    create_link_type,\n    delete_link_type,\n    make_sure_link_type_exists,\n\n    create_link,\n    delete_link,\n    get_entities_links,\n    get_folder_links,\n    get_folders_links,\n    get_task_links,\n    get_tasks_links,\n    get_product_links,\n    get_products_links,\n    get_version_links,\n    get_versions_links,\n    get_representations_links,\n    get_representation_links,\n\n    send_batch_operations,\n)\n\n\n__all__ = (\n    \"__version__\",\n\n    \"TransferProgress\",\n    \"slugify_string\",\n    \"create_dependency_package_basename\",\n\n    \"ServerAPI\",\n\n    \"GlobalServerAPI\",\n    \"ServiceContext\",\n\n    \"init_service\",\n    \"get_service_name\",\n    \"get_service_addon_name\",\n    \"get_service_addon_version\",\n    \"get_service_addon_settings\",\n\n    \"is_connection_created\",\n    \"create_connection\",\n    \"close_connection\",\n    \"change_token\",\n    \"set_environments\",\n    \"get_server_api_connection\",\n    \"get_site_id\",\n    \"set_site_id\",\n    \"get_client_version\",\n    \"set_client_version\",\n    \"get_default_settings_variant\",\n    \"set_default_settings_variant\",\n    \"get_sender\",\n    \"set_sender\",\n\n    \"get_base_url\",\n    \"get_rest_url\",\n\n    \"raw_get\",\n    \"raw_post\",\n    \"raw_put\",\n    \"raw_patch\",\n    \"raw_delete\",\n\n    \"get\",\n    \"post\",\n    \"put\",\n    \"patch\",\n    \"delete\",\n\n    \"get_timeout\",\n    \"set_timeout\",\n    \"get_max_retries\",\n    \"set_max_retries\",\n\n    \"get_event\",\n    \"get_events\",\n    \"dispatch_event\",\n    \"update_event\",\n    \"enroll_event_job\",\n\n    \"download_file\",\n    \"upload_file\",\n\n    \"query_graphql\",\n\n    \"get_addons_info\",\n    \"get_addon_url\",\n    \"download_addon_private_file\",\n\n    \"get_installers\",\n    \"create_installer\",\n    \"update_installer\",\n    \"delete_installer\",\n    \"download_installer\",\n    \"upload_installer\",\n\n    \"get_dependency_packages\",\n    \"create_dependency_package\",\n    \"update_dependency_package\",\n    \"delete_dependency_package\",\n\n    \"download_dependency_package\",\n    \"upload_dependency_package\",\n\n    \"upload_addon_zip\",\n\n    \"get_bundles\",\n    \"create_bundle\",\n    \"update_bundle\",\n    \"delete_bundle\",\n\n    \"get_info\",\n    \"get_server_version\",\n    \"get_server_version_tuple\",\n    \"get_user\",\n    \"get_users\",\n\n    \"get_attributes_for_type\",\n    \"get_attributes_fields_for_type\",\n    \"get_default_fields_for_type\",\n\n    \"get_project_anatomy_preset\",\n    \"get_project_anatomy_presets\",\n    \"get_project_roots_by_site\",\n    \"get_project_roots_for_site\",\n\n    \"get_addon_site_settings_schema\",\n    \"get_addon_settings_schema\",\n    \"get_addon_studio_settings\",\n    \"get_addon_project_settings\",\n    \"get_addon_settings\",\n    \"get_bundle_settings\",\n    \"get_addons_studio_settings\",\n    \"get_addons_project_settings\",\n    \"get_addons_settings\",\n\n    \"get_secrets\",\n    \"get_secret\",\n    \"save_secret\",\n    \"delete_secret\",\n\n    \"get_project_names\",\n    \"get_projects\",\n    \"get_project\",\n    \"create_project\",\n    \"update_project\",\n    \"delete_project\",\n\n    \"get_folder_by_id\",\n    \"get_folder_by_name\",\n    \"get_folder_by_path\",\n    \"get_folders\",\n\n    \"get_tasks\",\n    \"get_task_by_id\",\n    \"get_task_by_name\",\n\n    \"get_folder_ids_with_products\",\n    \"get_product_by_id\",\n    \"get_product_by_name\",\n    \"get_products\",\n    \"get_product_types\",\n    \"get_project_product_types\",\n    \"get_product_type_names\",\n\n    \"get_version_by_id\",\n    \"get_version_by_name\",\n    \"version_is_latest\",\n    \"get_versions\",\n    \"get_hero_version_by_product_id\",\n    \"get_hero_version_by_id\",\n    \"get_hero_versions\",\n    \"get_last_versions\",\n    \"get_last_version_by_product_id\",\n    \"get_last_version_by_product_name\",\n    \"get_representation_by_id\",\n    \"get_representation_by_name\",\n    \"get_representations\",\n    \"get_representations_parents\",\n    \"get_representation_parents\",\n    \"get_repre_ids_by_context_filters\",\n\n    \"get_workfiles_info\",\n    \"get_workfile_info\",\n    \"get_workfile_info_by_id\",\n\n    \"get_thumbnail_by_id\",\n    \"get_thumbnail\",\n    \"get_folder_thumbnail\",\n    \"get_version_thumbnail\",\n    \"get_workfile_thumbnail\",\n    \"create_thumbnail\",\n    \"update_thumbnail\",\n\n    \"get_full_link_type_name\",\n    \"get_link_types\",\n    \"get_link_type\",\n    \"create_link_type\",\n    \"delete_link_type\",\n    \"make_sure_link_type_exists\",\n\n    \"create_link\",\n    \"delete_link\",\n    \"get_entities_links\",\n    \"get_folder_links\",\n    \"get_folders_links\",\n    \"get_task_links\",\n    \"get_tasks_links\",\n    \"get_product_links\",\n    \"get_products_links\",\n    \"get_version_links\",\n    \"get_versions_links\",\n    \"get_representations_links\",\n    \"get_representation_links\",\n\n    \"send_batch_operations\",\n)\n"
  },
  {
    "path": "openpype/vendor/python/common/ayon_api/_api.py",
    "content": "\"\"\"Singleton based server api for direct access.\n\nThis implementation will be probably the most used part of package. Gives\noption to have singleton connection to Server URL based on environment variable\nvalues. All public functions and classes are imported in '__init__.py' so\nthey're available directly in top module import.\n\"\"\"\n\nimport os\nimport socket\n\nfrom .constants import (\n    SERVER_URL_ENV_KEY,\n    SERVER_API_ENV_KEY,\n)\nfrom .server_api import ServerAPI\nfrom .exceptions import FailedServiceInit\n\n\nclass GlobalServerAPI(ServerAPI):\n    \"\"\"Extended server api which also handles storing tokens and url.\n\n    Created object expect to have set environment variables\n    'AYON_SERVER_URL'. Also is expecting filled 'AYON_API_KEY'\n    but that can be filled afterwards with calling 'login' method.\n    \"\"\"\n\n    def __init__(\n        self,\n        site_id=None,\n        client_version=None,\n        default_settings_variant=None,\n        ssl_verify=None,\n        cert=None,\n    ):\n        url = self.get_url()\n        token = self.get_token()\n\n        super(GlobalServerAPI, self).__init__(\n            url,\n            token,\n            site_id,\n            client_version,\n            default_settings_variant,\n            ssl_verify,\n            cert,\n            # We want to make sure that server and api key validation\n            #   happens all the time in 'GlobalServerAPI'.\n            create_session=False,\n        )\n        self.validate_server_availability()\n        self.create_session()\n\n    def login(self, username, password):\n        \"\"\"Login to the server or change user.\n\n        If user is the same as current user and token is available the\n        login is skipped.\n        \"\"\"\n\n        previous_token = self._access_token\n        super(GlobalServerAPI, self).login(username, password)\n        if self.has_valid_token and previous_token != self._access_token:\n            os.environ[SERVER_API_ENV_KEY] = self._access_token\n\n    @staticmethod\n    def get_url():\n        return os.environ.get(SERVER_URL_ENV_KEY)\n\n    @staticmethod\n    def get_token():\n        return os.environ.get(SERVER_API_ENV_KEY)\n\n    @staticmethod\n    def set_environments(url, token):\n        \"\"\"Change url and token environemnts in currently running process.\n\n        Args:\n            url (str): New server url.\n            token (str): User's token.\n        \"\"\"\n\n        os.environ[SERVER_URL_ENV_KEY] = url or \"\"\n        os.environ[SERVER_API_ENV_KEY] = token or \"\"\n\n\nclass GlobalContext:\n    \"\"\"Singleton connection holder.\n\n    Goal is to avoid create connection on import which can be dangerous in\n    some cases.\n    \"\"\"\n\n    _connection = None\n\n    @classmethod\n    def is_connection_created(cls):\n        return cls._connection is not None\n\n    @classmethod\n    def change_token(cls, url, token):\n        GlobalServerAPI.set_environments(url, token)\n        if cls._connection is None:\n            return\n\n        if cls._connection.get_base_url() == url:\n            cls._connection.set_token(token)\n        else:\n            cls.close_connection()\n\n    @classmethod\n    def close_connection(cls):\n        if cls._connection is not None:\n            cls._connection.close_session()\n        cls._connection = None\n\n    @classmethod\n    def create_connection(cls, *args, **kwargs):\n        if cls._connection is not None:\n            cls.close_connection()\n        cls._connection = GlobalServerAPI(*args, **kwargs)\n        return cls._connection\n\n    @classmethod\n    def get_server_api_connection(cls):\n        if cls._connection is None:\n            cls.create_connection()\n        return cls._connection\n\n\nclass ServiceContext:\n    \"\"\"Helper for services running under server.\n\n    When service is running from server the process receives information about\n    connection from environment variables. This class helps to initialize the\n    values without knowing environment variables (that may change over time).\n\n    All what must be done is to call 'init_service' function/method. The\n    arguments are for cases when the service is running in specific environment\n    and their values are e.g. loaded from private file or for testing purposes.\n    \"\"\"\n\n    token = None\n    server_url = None\n    addon_name = None\n    addon_version = None\n    service_name = None\n\n    @classmethod\n    def init_service(\n        cls,\n        token=None,\n        server_url=None,\n        addon_name=None,\n        addon_version=None,\n        service_name=None,\n        connect=True\n    ):\n        token = token or os.environ.get(\"AYON_API_KEY\")\n        server_url = server_url or os.environ.get(\"AYON_SERVER_URL\")\n        if not server_url:\n            raise FailedServiceInit(\"URL to server is not set\")\n\n        if not token:\n            raise FailedServiceInit(\n                \"Token to server {} is not set\".format(server_url)\n            )\n\n        addon_name = addon_name or os.environ.get(\"AYON_ADDON_NAME\")\n        addon_version = addon_version or os.environ.get(\"AYON_ADDON_VERSION\")\n        service_name = service_name or os.environ.get(\"AYON_SERVICE_NAME\")\n\n        cls.token = token\n        cls.server_url = server_url\n        cls.addon_name = addon_name\n        cls.addon_version = addon_version\n        cls.service_name = service_name or socket.gethostname()\n\n        # Make sure required environments for GlobalServerAPI are set\n        GlobalServerAPI.set_environments(cls.server_url, cls.token)\n\n        if connect:\n            print(\"Connecting to server \\\"{}\\\"\".format(server_url))\n            con = GlobalContext.get_server_api_connection()\n            user = con.get_user()\n            print(\"Logged in as user \\\"{}\\\"\".format(user[\"name\"]))\n\n\ndef init_service(*args, **kwargs):\n    \"\"\"Initialize current connection from service.\n\n    The service expect specific environment variables. The variables must all\n    be set to make the connection work as a service.\n    \"\"\"\n\n    ServiceContext.init_service(*args, **kwargs)\n\n\ndef get_service_addon_name():\n    \"\"\"Name of addon which initialized service connection.\n\n    Service context must be initialized to be able to use this function. Call\n    'init_service' on you service start to do so.\n\n    Returns:\n        Union[str, None]: Name of addon or None.\n    \"\"\"\n\n    return ServiceContext.addon_name\n\n\ndef get_service_addon_version():\n    \"\"\"Version of addon which initialized service connection.\n\n    Service context must be initialized to be able to use this function. Call\n    'init_service' on you service start to do so.\n\n    Returns:\n        Union[str, None]: Version of addon or None.\n    \"\"\"\n\n    return ServiceContext.addon_version\n\n\ndef get_service_name():\n    \"\"\"Name of service.\n\n    Service context must be initialized to be able to use this function. Call\n    'init_service' on you service start to do so.\n\n    Returns:\n        Union[str, None]: Name of service if service was registered.\n    \"\"\"\n\n    return ServiceContext.service_name\n\n\ndef get_service_addon_settings():\n    \"\"\"Addon settings of service which initialized service.\n\n    Service context must be initialized to be able to use this function. Call\n    'init_service' on you service start to do so.\n\n    Returns:\n        Dict[str, Any]: Addon settings.\n\n    Raises:\n        ValueError: When service was not initialized.\n    \"\"\"\n\n    addon_name = get_service_addon_name()\n    addon_version = get_service_addon_version()\n    if addon_name is None or addon_version is None:\n        raise ValueError(\"Service is not initialized\")\n    return get_addon_settings(addon_name, addon_version)\n\n\ndef is_connection_created():\n    \"\"\"Is global connection created.\n\n    Returns:\n        bool: True if connection was connected.\n    \"\"\"\n\n    return GlobalContext.is_connection_created()\n\n\ndef create_connection(site_id=None, client_version=None):\n    \"\"\"Create global connection.\n\n    Args:\n        site_id (str): Machine site id/name.\n        client_version (str): Desktop app version.\n\n    Returns:\n        GlobalServerAPI: Created connection.\n    \"\"\"\n\n    return GlobalContext.create_connection(site_id, client_version)\n\n\ndef close_connection():\n    \"\"\"Close global connection if is connected.\"\"\"\n\n    GlobalContext.close_connection()\n\n\ndef change_token(url, token):\n    \"\"\"Change connection token for url.\n\n    This function can be also used to change url.\n\n    Args:\n        url (str): Server url.\n        token (str): API key token.\n    \"\"\"\n\n    GlobalContext.change_token(url, token)\n\n\ndef set_environments(url, token):\n    \"\"\"Set global environments for global connection.\n\n    Args:\n        url (Union[str, None]): Url to server or None to unset environments.\n        token (Union[str, None]): API key token to be used for connection.\n    \"\"\"\n\n    GlobalServerAPI.set_environments(url, token)\n\n\ndef get_server_api_connection():\n    \"\"\"Access to global scope object of GlobalServerAPI.\n\n    This access expect to have set environment variables 'AYON_SERVER_URL'\n    and 'AYON_API_KEY'.\n\n    Returns:\n        GlobalServerAPI: Object of connection to server.\n    \"\"\"\n\n    return GlobalContext.get_server_api_connection()\n\n\ndef get_site_id():\n    con = get_server_api_connection()\n    return con.get_site_id()\n\n\ndef set_site_id(site_id):\n    \"\"\"Set site id of already connected client connection.\n\n    Site id is human-readable machine id used in AYON desktop application.\n\n    Args:\n        site_id (Union[str, None]): Site id used in connection.\n    \"\"\"\n\n    con = get_server_api_connection()\n    con.set_site_id(site_id)\n\n\ndef get_client_version():\n    \"\"\"Version of client used to connect to server.\n\n    Client version is AYON client build desktop application.\n\n    Returns:\n        str: Client version string used in connection.\n    \"\"\"\n\n    con = get_server_api_connection()\n    return con.get_client_version()\n\n\ndef set_client_version(client_version):\n    \"\"\"Set version of already connected client connection.\n\n    Client version is version of AYON desktop application.\n\n    Args:\n        client_version (Union[str, None]): Client version string.\n    \"\"\"\n\n    con = get_server_api_connection()\n    con.set_client_version(client_version)\n\n\ndef get_default_settings_variant():\n    \"\"\"Default variant used for settings.\n\n    Returns:\n        Union[str, None]: name of variant or None.\n    \"\"\"\n\n    con = get_server_api_connection()\n    return con.get_default_settings_variant()\n\n\ndef set_default_settings_variant(variant):\n    \"\"\"Change default variant for addon settings.\n\n    Note:\n        It is recommended to set only 'production' or 'staging' variants\n            as default variant.\n\n    Args:\n        variant (Union[str, None]): Settings variant name.\n    \"\"\"\n\n    con = get_server_api_connection()\n    return con.set_default_settings_variant(variant)\n\n\ndef get_sender():\n    \"\"\"Sender used to send requests.\n\n    Returns:\n        Union[str, None]: Sender name or None.\n    \"\"\"\n\n    con = get_server_api_connection()\n    return con.get_sender()\n\n\ndef set_sender(sender):\n    \"\"\"Change sender used for requests.\n\n    Args:\n        sender (Union[str, None]): Sender name or None.\n    \"\"\"\n\n    con = get_server_api_connection()\n    return con.set_sender(sender)\n\n\ndef get_base_url():\n    con = get_server_api_connection()\n    return con.get_base_url()\n\n\ndef get_rest_url():\n    con = get_server_api_connection()\n    return con.get_rest_url()\n\n\ndef raw_get(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.raw_get(*args, **kwargs)\n\n\ndef raw_post(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.raw_post(*args, **kwargs)\n\n\ndef raw_put(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.raw_put(*args, **kwargs)\n\n\ndef raw_patch(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.raw_patch(*args, **kwargs)\n\n\ndef raw_delete(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.raw_delete(*args, **kwargs)\n\n\ndef get(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get(*args, **kwargs)\n\n\ndef post(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.post(*args, **kwargs)\n\n\ndef put(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.put(*args, **kwargs)\n\n\ndef patch(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.patch(*args, **kwargs)\n\n\ndef delete(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.delete(*args, **kwargs)\n\n\ndef get_timeout(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_timeout(*args, **kwargs)\n\n\ndef set_timeout(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.set_timeout(*args, **kwargs)\n\n\ndef get_max_retries(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_max_retries(*args, **kwargs)\n\n\ndef set_max_retries(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.set_max_retries(*args, **kwargs)\n\n\ndef get_event(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_event(*args, **kwargs)\n\n\ndef get_events(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_events(*args, **kwargs)\n\n\ndef dispatch_event(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.dispatch_event(*args, **kwargs)\n\n\ndef update_event(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.update_event(*args, **kwargs)\n\n\ndef enroll_event_job(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.enroll_event_job(*args, **kwargs)\n\n\ndef download_file(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.download_file(*args, **kwargs)\n\n\ndef upload_file(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.upload_file(*args, **kwargs)\n\n\ndef query_graphql(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.query_graphql(*args, **kwargs)\n\n\ndef get_users(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_users(*args, **kwargs)\n\n\ndef get_user(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_user(*args, **kwargs)\n\n\ndef get_attributes_for_type(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_attributes_for_type(*args, **kwargs)\n\n\ndef get_addons_info(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_addons_info(*args, **kwargs)\n\n\ndef get_addon_url(addon_name, addon_version, *subpaths):\n    con = get_server_api_connection()\n    return con.get_addon_url(addon_name, addon_version, *subpaths)\n\n\ndef download_addon_private_file(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.download_addon_private_file(*args, **kwargs)\n\n\ndef get_info(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_info(*args, **kwargs)\n\n\ndef get_server_version(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_server_version(*args, **kwargs)\n\n\ndef get_server_version_tuple(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_server_version_tuple(*args, **kwargs)\n\n\n# Installers\ndef get_installers(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_installers(*args, **kwargs)\n\n\ndef create_installer(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.create_installer(*args, **kwargs)\n\n\ndef update_installer(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.update_installer(*args, **kwargs)\n\n\ndef delete_installer(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.delete_installer(*args, **kwargs)\n\n\ndef download_installer(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.download_installer(*args, **kwargs)\n\n\ndef upload_installer(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.upload_installer(*args, **kwargs)\n\n\n# Dependency packages\ndef download_dependency_package(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.download_dependency_package(*args, **kwargs)\n\n\ndef upload_dependency_package(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.upload_dependency_package(*args, **kwargs)\n\n\ndef get_dependency_packages(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_dependency_packages(*args, **kwargs)\n\n\ndef create_dependency_package(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.create_dependency_package(*args, **kwargs)\n\n\ndef update_dependency_package(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.update_dependency_package(*args, **kwargs)\n\n\ndef delete_dependency_package(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.delete_dependency_package(*args, **kwargs)\n\n\ndef upload_addon_zip(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.upload_addon_zip(*args, **kwargs)\n\n\ndef get_project_anatomy_presets(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_project_anatomy_presets(*args, **kwargs)\n\n\ndef get_bundles(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_bundles(*args, **kwargs)\n\n\ndef create_bundle(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.create_bundle(*args, **kwargs)\n\n\ndef update_bundle(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.update_bundle(*args, **kwargs)\n\n\ndef delete_bundle(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.delete_bundle(*args, **kwargs)\n\n\ndef get_project_anatomy_preset(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_project_anatomy_preset(*args, **kwargs)\n\n\ndef get_project_roots_by_site(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_project_roots_by_site(*args, **kwargs)\n\n\ndef get_project_roots_for_site(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_project_roots_for_site(*args, **kwargs)\n\n\ndef get_addon_settings_schema(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_addon_settings_schema(*args, **kwargs)\n\n\ndef get_addon_site_settings_schema(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_addon_site_settings_schema(*args, **kwargs)\n\n\ndef get_addon_studio_settings(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_addon_studio_settings(*args, **kwargs)\n\n\ndef get_addon_project_settings(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_addon_project_settings(*args, **kwargs)\n\n\ndef get_addon_settings(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_addon_settings(*args, **kwargs)\n\n\ndef get_addon_site_settings(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_addon_site_settings(*args, **kwargs)\n\n\ndef get_bundle_settings(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_bundle_settings(*args, **kwargs)\n\n\ndef get_addons_studio_settings(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_addons_studio_settings(*args, **kwargs)\n\n\ndef get_addons_project_settings(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_addons_project_settings(*args, **kwargs)\n\n\ndef get_addons_settings(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_addons_settings(*args, **kwargs)\n\n\ndef get_secrets(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_secrets(*args, **kwargs)\n\n\ndef get_secret(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_secret(*args, **kwargs)\n\n\ndef save_secret(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.save_secret(*args, **kwargs)\n\n\ndef delete_secret(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.delete_secret(*args, **kwargs)\n\n\ndef get_project_names(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_project_names(*args, **kwargs)\n\n\ndef get_project(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_project(*args, **kwargs)\n\n\ndef get_projects(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_projects(*args, **kwargs)\n\n\ndef get_folders(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_folders(*args, **kwargs)\n\n\ndef get_folders_hierarchy(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_folders_hierarchy(*args, **kwargs)\n\n\ndef get_tasks(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_tasks(*args, **kwargs)\n\n\ndef get_task_by_id(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_task_by_id(*args, **kwargs)\n\n\ndef get_task_by_name(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_task_by_name(*args, **kwargs)\n\n\ndef get_folder_by_id(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_folder_by_id(*args, **kwargs)\n\n\ndef get_folder_by_path(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_folder_by_path(*args, **kwargs)\n\n\ndef get_folder_by_name(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_folder_by_name(*args, **kwargs)\n\n\ndef get_folder_ids_with_products(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_folder_ids_with_products(*args, **kwargs)\n\n\ndef get_product_types(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_product_types(*args, **kwargs)\n\n\ndef get_project_product_types(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_project_product_types(*args, **kwargs)\n\n\ndef get_product_type_names(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_product_type_names(*args, **kwargs)\n\n\ndef get_products(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_products(*args, **kwargs)\n\n\ndef get_product_by_id(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_product_by_id(*args, **kwargs)\n\n\ndef get_product_by_name(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_product_by_name(*args, **kwargs)\n\n\ndef get_versions(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_versions(*args, **kwargs)\n\n\ndef get_version_by_id(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_version_by_id(*args, **kwargs)\n\n\ndef get_version_by_name(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_version_by_name(*args, **kwargs)\n\n\ndef get_hero_version_by_id(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_hero_version_by_id(*args, **kwargs)\n\n\ndef get_hero_version_by_product_id(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_hero_version_by_product_id(*args, **kwargs)\n\n\ndef get_hero_versions(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_hero_versions(*args, **kwargs)\n\n\ndef get_last_versions(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_last_versions(*args, **kwargs)\n\n\ndef get_last_version_by_product_id(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_last_version_by_product_id(*args, **kwargs)\n\n\ndef get_last_version_by_product_name(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_last_version_by_product_name(*args, **kwargs)\n\n\ndef version_is_latest(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.version_is_latest(*args, **kwargs)\n\n\ndef get_representations(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_representations(*args, **kwargs)\n\n\ndef get_representation_by_id(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_representation_by_id(*args, **kwargs)\n\n\ndef get_representation_by_name(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_representation_by_name(*args, **kwargs)\n\n\ndef get_representation_parents(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_representation_parents(*args, **kwargs)\n\n\ndef get_representations_parents(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_representations_parents(*args, **kwargs)\n\n\ndef get_repre_ids_by_context_filters(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_repre_ids_by_context_filters(*args, **kwargs)\n\n\ndef get_workfiles_info(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_workfiles_info(*args, **kwargs)\n\n\ndef get_workfile_info(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_workfile_info(*args, **kwargs)\n\n\ndef get_workfile_info_by_id(*args, **kwargs):\n    con = get_server_api_connection()\n    return con.get_workfile_info_by_id(*args, **kwargs)\n\n\ndef create_project(\n    project_name,\n    project_code,\n    library_project=False,\n    preset_name=None\n):\n    con = get_server_api_connection()\n    return con.create_project(\n        project_name,\n        project_code,\n        library_project,\n        preset_name\n    )\n\n\ndef update_project(project_name, *args, **kwargs):\n    con = get_server_api_connection()\n    return con.update_project(project_name, *args, **kwargs)\n\n\ndef delete_project(project_name):\n    con = get_server_api_connection()\n    return con.delete_project(project_name)\n\n\ndef get_thumbnail_by_id(project_name, thumbnail_id):\n    con = get_server_api_connection()\n    return con.get_thumbnail_by_id(project_name, thumbnail_id)\n\n\ndef get_thumbnail(project_name, entity_type, entity_id, thumbnail_id=None):\n    con = get_server_api_connection()\n    return con.get_thumbnail(\n        project_name, entity_type, entity_id, thumbnail_id\n    )\n\n\ndef get_folder_thumbnail(project_name, folder_id, thumbnail_id=None):\n    con = get_server_api_connection()\n    return con.get_folder_thumbnail(project_name, folder_id, thumbnail_id)\n\n\ndef get_version_thumbnail(project_name, version_id, thumbnail_id=None):\n    con = get_server_api_connection()\n    return con.get_version_thumbnail(project_name, version_id, thumbnail_id)\n\n\ndef get_workfile_thumbnail(project_name, workfile_id, thumbnail_id=None):\n    con = get_server_api_connection()\n    return con.get_workfile_thumbnail(project_name, workfile_id, thumbnail_id)\n\n\ndef create_thumbnail(project_name, src_filepath, thumbnail_id=None):\n    con = get_server_api_connection()\n    return con.create_thumbnail(project_name, src_filepath, thumbnail_id)\n\n\ndef update_thumbnail(project_name, thumbnail_id, src_filepath):\n    con = get_server_api_connection()\n    return con.update_thumbnail(project_name, thumbnail_id, src_filepath)\n\n\ndef get_attributes_fields_for_type(entity_type):\n    con = get_server_api_connection()\n    return con.get_attributes_fields_for_type(entity_type)\n\n\ndef get_default_fields_for_type(entity_type):\n    con = get_server_api_connection()\n    return con.get_default_fields_for_type(entity_type)\n\n\ndef get_full_link_type_name(link_type_name, input_type, output_type):\n    con = get_server_api_connection()\n    return con.get_full_link_type_name(\n        link_type_name, input_type, output_type)\n\n\ndef get_link_types(project_name):\n    con = get_server_api_connection()\n    return con.get_link_types(project_name)\n\n\ndef get_link_type(project_name, link_type_name, input_type, output_type):\n    con = get_server_api_connection()\n    return con.get_link_type(\n        project_name, link_type_name, input_type, output_type)\n\n\ndef create_link_type(\n    project_name, link_type_name, input_type, output_type, data=None):\n    con = get_server_api_connection()\n    return con.create_link_type(\n        project_name, link_type_name, input_type, output_type, data=data)\n\n\ndef delete_link_type(project_name, link_type_name, input_type, output_type):\n    con = get_server_api_connection()\n    return con.delete_link_type(\n        project_name, link_type_name, input_type, output_type)\n\n\ndef make_sure_link_type_exists(\n    project_name, link_type_name, input_type, output_type, data=None\n):\n    con = get_server_api_connection()\n    return con.make_sure_link_type_exists(\n        project_name, link_type_name, input_type, output_type, data=data\n    )\n\n\ndef create_link(\n    project_name,\n    link_type_name,\n    input_id,\n    input_type,\n    output_id,\n    output_type\n):\n    con = get_server_api_connection()\n    return con.create_link(\n        project_name,\n        link_type_name,\n        input_id, input_type,\n        output_id, output_type\n    )\n\n\ndef delete_link(project_name, link_id):\n    con = get_server_api_connection()\n    return con.delete_link(project_name, link_id)\n\n\ndef get_entities_links(\n    project_name,\n    entity_type,\n    entity_ids=None,\n    link_types=None,\n    link_direction=None\n):\n    con = get_server_api_connection()\n    return con.get_entities_links(\n        project_name,\n        entity_type,\n        entity_ids,\n        link_types,\n        link_direction\n    )\n\n\ndef get_folders_links(\n    project_name,\n    folder_ids=None,\n    link_types=None,\n    link_direction=None\n):\n    con = get_server_api_connection()\n    return con.get_folders_links(\n        project_name,\n        folder_ids,\n        link_types,\n        link_direction\n    )\n\n\ndef get_folder_links(\n    project_name,\n    folder_id,\n    link_types=None,\n    link_direction=None\n):\n    con = get_server_api_connection()\n    return con.get_folder_links(\n        project_name,\n        folder_id,\n        link_types,\n        link_direction\n    )\n\n\ndef get_tasks_links(\n    project_name,\n    task_ids=None,\n    link_types=None,\n    link_direction=None\n):\n    con = get_server_api_connection()\n    return con.get_tasks_links(\n        project_name,\n        task_ids,\n        link_types,\n        link_direction\n    )\n\n\ndef get_task_links(\n    project_name,\n    task_id,\n    link_types=None,\n    link_direction=None\n):\n    con = get_server_api_connection()\n    return con.get_task_links(\n        project_name,\n        task_id,\n        link_types,\n        link_direction\n    )\n\n\ndef get_products_links(\n    project_name,\n    product_ids=None,\n    link_types=None,\n    link_direction=None\n):\n    con = get_server_api_connection()\n    return con.get_products_links(\n        project_name,\n        product_ids,\n        link_types,\n        link_direction\n    )\n\n\ndef get_product_links(\n    project_name,\n    product_id,\n    link_types=None,\n    link_direction=None\n):\n    con = get_server_api_connection()\n    return con.get_product_links(\n        project_name,\n        product_id,\n        link_types,\n        link_direction\n    )\n\n\ndef get_versions_links(\n    project_name,\n    version_ids=None,\n    link_types=None,\n    link_direction=None\n):\n    con = get_server_api_connection()\n    return con.get_versions_links(\n        project_name,\n        version_ids,\n        link_types,\n        link_direction\n    )\n\n\ndef get_version_links(\n    project_name,\n    version_id,\n    link_types=None,\n    link_direction=None\n):\n    con = get_server_api_connection()\n    return con.get_version_links(\n        project_name,\n        version_id,\n        link_types,\n        link_direction\n    )\n\n\ndef get_representations_links(\n    project_name,\n    representation_ids=None,\n    link_types=None,\n    link_direction=None\n):\n    con = get_server_api_connection()\n    return con.get_representations_links(\n        project_name,\n        representation_ids,\n        link_types,\n        link_direction\n    )\n\n\ndef get_representation_links(\n    project_name,\n    representation_id,\n    link_types=None,\n    link_direction=None\n):\n    con = get_server_api_connection()\n    return con.get_representation_links(\n        project_name,\n        representation_id,\n        link_types,\n        link_direction\n    )\n\n\ndef send_batch_operations(\n    project_name,\n    operations,\n    can_fail=False,\n    raise_on_fail=True\n):\n    con = get_server_api_connection()\n    return con.send_batch_operations(\n        project_name,\n        operations,\n        can_fail=can_fail,\n        raise_on_fail=raise_on_fail\n    )\n"
  },
  {
    "path": "openpype/vendor/python/common/ayon_api/constants.py",
    "content": "# Environments where server url and api key are stored for global connection\nSERVER_URL_ENV_KEY = \"AYON_SERVER_URL\"\nSERVER_API_ENV_KEY = \"AYON_API_KEY\"\nSERVER_TIMEOUT_ENV_KEY = \"AYON_SERVER_TIMEOUT\"\nSERVER_RETRIES_ENV_KEY = \"AYON_SERVER_RETRIES\"\n# Default variant used for settings\nDEFAULT_VARIANT_ENV_KEY = \"AYON_DEFAULT_SETTINGS_VARIANT\"\n# Default site id used for connection\nSITE_ID_ENV_KEY = \"AYON_SITE_ID\"\n\n# Backwards compatibility\nSERVER_TOKEN_ENV_KEY = SERVER_API_ENV_KEY\n\n# --- User ---\nDEFAULT_USER_FIELDS = {\n    \"accessGroups\",\n    \"defaultAccessGroups\",\n    \"name\",\n    \"isService\",\n    \"isManager\",\n    \"isGuest\",\n    \"isAdmin\",\n    \"createdAt\",\n    \"active\",\n    \"hasPassword\",\n    \"updatedAt\",\n    \"apiKeyPreview\",\n    \"attrib.avatarUrl\",\n    \"attrib.email\",\n    \"attrib.fullName\",\n}\n\n# --- Product types ---\nDEFAULT_PRODUCT_TYPE_FIELDS = {\n    \"name\",\n    \"icon\",\n    \"color\",\n}\n\n# --- Project ---\nDEFAULT_PROJECT_FIELDS = {\n    \"active\",\n    \"name\",\n    \"code\",\n    \"config\",\n    \"createdAt\",\n    \"data\",\n}\n\n# --- Folders ---\nDEFAULT_FOLDER_FIELDS = {\n    \"id\",\n    \"name\",\n    \"label\",\n    \"folderType\",\n    \"path\",\n    \"parentId\",\n    \"active\",\n    \"thumbnailId\",\n    \"data\",\n}\n\n# --- Tasks ---\nDEFAULT_TASK_FIELDS = {\n    \"id\",\n    \"name\",\n    \"label\",\n    \"taskType\",\n    \"folderId\",\n    \"active\",\n    \"assignees\",\n    \"data\",\n}\n\n# --- Products ---\nDEFAULT_PRODUCT_FIELDS = {\n    \"id\",\n    \"name\",\n    \"folderId\",\n    \"active\",\n    \"productType\",\n    \"data\",\n}\n\n# --- Versions ---\nDEFAULT_VERSION_FIELDS = {\n    \"id\",\n    \"name\",\n    \"version\",\n    \"productId\",\n    \"taskId\",\n    \"active\",\n    \"author\",\n    \"thumbnailId\",\n    \"createdAt\",\n    \"updatedAt\",\n    \"data\",\n}\n\n# --- Representations ---\nDEFAULT_REPRESENTATION_FIELDS = {\n    \"id\",\n    \"name\",\n    \"context\",\n    \"createdAt\",\n    \"active\",\n    \"versionId\",\n    \"data\",\n}\n\nREPRESENTATION_FILES_FIELDS = {\n    \"files.name\",\n    \"files.hash\",\n    \"files.id\",\n    \"files.path\",\n    \"files.size\",\n}\n\n# --- Workfile info ---\nDEFAULT_WORKFILE_INFO_FIELDS = {\n    \"active\",\n    \"createdAt\",\n    \"createdBy\",\n    \"id\",\n    \"name\",\n    \"path\",\n    \"projectName\",\n    \"taskId\",\n    \"thumbnailId\",\n    \"updatedAt\",\n    \"updatedBy\",\n    \"data\",\n}\n\nDEFAULT_EVENT_FIELDS = {\n    \"id\",\n    \"hash\",\n    \"createdAt\",\n    \"dependsOn\",\n    \"description\",\n    \"project\",\n    \"retries\",\n    \"sender\",\n    \"status\",\n    \"topic\",\n    \"updatedAt\",\n    \"user\",\n}\n"
  },
  {
    "path": "openpype/vendor/python/common/ayon_api/entity_hub.py",
    "content": "import re\nimport copy\nimport collections\nfrom abc import ABCMeta, abstractmethod\n\nimport six\nfrom ._api import get_server_api_connection\nfrom .utils import create_entity_id, convert_entity_id, slugify_string\n\n\nclass _CustomNone(object):\n    def __init__(self, name=None):\n        self._name = name or \"CustomNone\"\n\n    def __repr__(self):\n        return \"<{}>\".format(self._name)\n\n    def __bool__(self):\n        return False\n\n\nUNKNOWN_VALUE = _CustomNone(\"UNKNOWN_VALUE\")\nPROJECT_PARENT_ID = _CustomNone(\"PROJECT_PARENT_ID\")\n_NOT_SET = _CustomNone(\"_NOT_SET\")\n\n\nclass EntityHub(object):\n    \"\"\"Helper to create, update or remove entities in project.\n\n    The hub is a guide to operation with folder entities and update of project.\n    Project entity must already exist on server (can be only updated).\n\n    Object is caching entities queried from server. They won't be required once\n    they were queried, so it is recommended to create new hub or clear cache\n    frequently.\n\n    Todos:\n        Listen to server events about entity changes to be able update already\n            queried entities.\n\n    Args:\n        project_name (str): Name of project where changes will happen.\n        connection (ServerAPI): Connection to server with logged user.\n        allow_data_changes (bool): This option gives ability to change 'data'\n            key on entities. This is not recommended as 'data' may be use for\n            secure information and would also slow down server queries. Content\n            of 'data' key can't be received only GraphQl.\n    \"\"\"\n\n    def __init__(\n        self, project_name, connection=None, allow_data_changes=None\n    ):\n        if not connection:\n            connection = get_server_api_connection()\n        major, minor, patch, _, _ = connection.server_version_tuple\n        path_start_with_slash = True\n        if (major, minor) < (0, 6):\n            path_start_with_slash = False\n\n        if allow_data_changes is None:\n            allow_data_changes = connection.graphql_allows_data_in_query\n\n        self._connection = connection\n        self._path_start_with_slash = path_start_with_slash\n\n        self._project_name = project_name\n        self._entities_by_id = {}\n        self._entities_by_parent_id = collections.defaultdict(list)\n        self._project_entity = UNKNOWN_VALUE\n\n        self._allow_data_changes = allow_data_changes\n\n        self._path_reset_queue = None\n\n    @property\n    def allow_data_changes(self):\n        \"\"\"Entity hub allows changes of 'data' key on entities.\n\n        Data are private and not all users may have access to them. Also to get\n        'data' for entity is required to use REST api calls, which means to\n        query each entity on-by-one from server.\n\n        Returns:\n            bool: Data changes are allowed.\n        \"\"\"\n\n        return self._allow_data_changes\n\n    @property\n    def path_start_with_slash(self):\n        \"\"\"Folder path should start with slash.\n\n        This changed in 0.6.x server version.\n\n        Returns:\n            bool: Path starts with slash.\n        \"\"\"\n\n        return self._path_start_with_slash\n\n    @property\n    def project_name(self):\n        \"\"\"Project name which is maintained by hub.\n\n        Returns:\n            str: Name of project.\n        \"\"\"\n\n        return self._project_name\n\n    @property\n    def project_entity(self):\n        \"\"\"Project entity.\n\n        Returns:\n            ProjectEntity: Project entity.\n        \"\"\"\n\n        if self._project_entity is UNKNOWN_VALUE:\n            self.fill_project_from_server()\n        return self._project_entity\n\n    def get_attributes_for_type(self, entity_type):\n        \"\"\"Get attributes available for a type.\n\n        Attributes are based on entity types.\n\n        Todos:\n            Use attribute schema to validate values on entities.\n\n        Args:\n            entity_type (Literal[\"project\", \"folder\", \"task\"]): Entity type\n                for which should be attributes received.\n\n        Returns:\n            Dict[str, Dict[str, Any]]: Attribute schemas that are available\n                for entered entity type.\n        \"\"\"\n\n        return self._connection.get_attributes_for_type(entity_type)\n\n    def get_entity_by_id(self, entity_id):\n        \"\"\"Receive entity by its id without entity type.\n\n        The entity must be already existing in cached objects.\n\n        Args:\n            entity_id (str): Id of entity.\n\n        Returns:\n            Union[BaseEntity, None]: Entity object or None.\n        \"\"\"\n\n        return self._entities_by_id.get(entity_id)\n\n    def get_folder_by_id(self, entity_id, allow_query=True):\n        \"\"\"Get folder entity by id.\n\n        Args:\n            entity_id (str): Id of folder entity.\n            allow_query (bool): Try to query entity from server if is not\n                available in cache.\n\n        Returns:\n            Union[FolderEntity, None]: Object of folder or 'None'.\n        \"\"\"\n\n        if allow_query:\n            return self.get_or_query_entity_by_id(entity_id, [\"folder\"])\n        return self._entities_by_id.get(entity_id)\n\n    def get_task_by_id(self, entity_id, allow_query=True):\n        \"\"\"Get task entity by id.\n\n        Args:\n           entity_id (str): Id of task entity.\n           allow_query (bool): Try to query entity from server if is not\n               available in cache.\n\n        Returns:\n           Union[TaskEntity, None]: Object of folder or 'None'.\n        \"\"\"\n\n        if allow_query:\n            return self.get_or_query_entity_by_id(entity_id, [\"task\"])\n        return self._entities_by_id.get(entity_id)\n\n    def get_or_query_entity_by_id(self, entity_id, entity_types):\n        \"\"\"Get or query entity based on it's id and possible entity types.\n\n        This is a helper function when entity id is known but entity type may\n        have multiple possible options.\n\n        Args:\n            entity_id (str): Entity id.\n            entity_types (Iterable[str]): Possible entity types that can the id\n                represent. e.g. '[\"folder\", \"project\"]'\n        \"\"\"\n\n        existing_entity = self._entities_by_id.get(entity_id)\n        if existing_entity is not None:\n            return existing_entity\n\n        if not entity_types:\n            return None\n\n        entity_data = None\n        for entity_type in entity_types:\n            if entity_type == \"folder\":\n                entity_data = self._connection.get_folder_by_id(\n                    self.project_name,\n                    entity_id,\n                    fields=self._get_folder_fields(),\n                    own_attributes=True\n                )\n            elif entity_type == \"task\":\n                entity_data = self._connection.get_task_by_id(\n                    self.project_name,\n                    entity_id,\n                    own_attributes=True\n                )\n            else:\n                raise ValueError(\n                    \"Unknonwn entity type \\\"{}\\\"\".format(entity_type)\n                )\n\n            if entity_data:\n                break\n\n        if not entity_data:\n            return None\n\n        if entity_type == \"folder\":\n            return self.add_folder(entity_data)\n        elif entity_type == \"task\":\n            return self.add_task(entity_data)\n\n        return None\n\n    @property\n    def entities(self):\n        \"\"\"Iterator over available entities.\n\n        Returns:\n            Iterator[BaseEntity]: All queried/created entities cached in hub.\n        \"\"\"\n\n        for entity in self._entities_by_id.values():\n            yield entity\n\n    def add_new_folder(self, *args, created=True, **kwargs):\n        \"\"\"Create folder object and add it to entity hub.\n\n        Args:\n            folder_type (str): Type of folder. Folder type must be available in\n                config of project folder types.\n            entity_id (Union[str, None]): Id of the entity. New id is created if\n                not passed.\n            parent_id (Union[str, None]): Id of parent entity.\n            name (str): Name of entity.\n            label (Optional[str]): Folder label.\n            path (Optional[str]): Folder path. Path consist of all parent names\n                with slash('/') used as separator.\n            attribs (Dict[str, Any]): Attribute values.\n            data (Dict[str, Any]): Entity data (custom data).\n            thumbnail_id (Union[str, None]): Id of entity's thumbnail.\n            active (bool): Is entity active.\n            created (Optional[bool]): Entity is new. When 'None' is passed the\n                value is defined based on value of 'entity_id'.\n\n        Returns:\n            FolderEntity: Added folder entity.\n        \"\"\"\n\n        folder_entity = FolderEntity(\n            *args, **kwargs, created=created, entity_hub=self\n        )\n        self.add_entity(folder_entity)\n        return folder_entity\n\n    def add_new_task(self, *args, created=True, **kwargs):\n        \"\"\"Create folder object and add it to entity hub.\n\n        Args:\n            task_type (str): Type of task. Task type must be available in\n                config of project folder types.\n            entity_id (Union[str, None]): Id of the entity. New id is created if\n                not passed.\n            parent_id (Union[str, None]): Id of parent entity.\n            name (str): Name of entity.\n            label (Optional[str]): Folder label.\n            attribs (Dict[str, Any]): Attribute values.\n            data (Dict[str, Any]): Entity data (custom data).\n            thumbnail_id (Union[str, None]): Id of entity's thumbnail.\n            active (bool): Is entity active.\n            created (Optional[bool]): Entity is new. When 'None' is passed the\n                value is defined based on value of 'entity_id'.\n\n        Returns:\n            TaskEntity: Added task entity.\n        \"\"\"\n\n        task_entity = TaskEntity(\n            *args, **kwargs, created=created, entity_hub=self\n        )\n        self.add_entity(task_entity)\n        return task_entity\n\n    def add_folder(self, folder):\n        \"\"\"Create folder object and add it to entity hub.\n\n        Args:\n            folder (Dict[str, Any]): Folder entity data.\n\n        Returns:\n            FolderEntity: Added folder entity.\n        \"\"\"\n\n        folder_entity = FolderEntity.from_entity_data(folder, entity_hub=self)\n        self.add_entity(folder_entity)\n        return folder_entity\n\n    def add_task(self, task):\n        \"\"\"Create task object and add it to entity hub.\n\n        Args:\n            task (Dict[str, Any]): Task entity data.\n\n        Returns:\n            TaskEntity: Added task entity.\n        \"\"\"\n\n        task_entity = TaskEntity.from_entity_data(task, entity_hub=self)\n        self.add_entity(task_entity)\n        return task_entity\n\n    def add_entity(self, entity):\n        \"\"\"Add entity to hub cache.\n\n        Args:\n            entity (BaseEntity): Entity that should be added to hub's cache.\n        \"\"\"\n\n        self._entities_by_id[entity.id] = entity\n        parent_children = self._entities_by_parent_id[entity.parent_id]\n        if entity not in parent_children:\n            parent_children.append(entity)\n\n        if entity.parent_id is PROJECT_PARENT_ID:\n            return\n\n        parent = self._entities_by_id.get(entity.parent_id)\n        if parent is not None:\n            parent.add_child(entity.id)\n\n    def folder_path_reseted(self, folder_id):\n        \"\"\"Method called from 'FolderEntity' on path reset.\n\n        This should reset cache of folder paths on all children entities.\n\n        The path cache is always propagated from top to bottom so if an entity\n        has not cached path it means that any children can't have it cached.\n        \"\"\"\n\n        if self._path_reset_queue is not None:\n            self._path_reset_queue.append(folder_id)\n            return\n\n        self._path_reset_queue = collections.deque()\n        self._path_reset_queue.append(folder_id)\n        while self._path_reset_queue:\n            children = self._entities_by_parent_id[folder_id]\n            for child in children:\n                # Get child path but don't trigger cache\n                path = child.get_path(False)\n                if path is not None:\n                    # Reset it's path cache if is set\n                    child.reset_path()\n                else:\n                    self._path_reset_queue.append(child.id)\n\n        self._path_reset_queue = None\n\n    def unset_entity_parent(self, entity_id, parent_id):\n        entity = self._entities_by_id.get(entity_id)\n        parent = self._entities_by_id.get(parent_id)\n        children_ids = UNKNOWN_VALUE\n        if parent is not None:\n            children_ids = parent.get_children_ids(False)\n\n        has_set_parent = False\n        if entity is not None:\n            has_set_parent = entity.parent_id == parent_id\n\n        new_parent_id = None\n        if has_set_parent:\n            entity.parent_id = new_parent_id\n\n        if children_ids is not UNKNOWN_VALUE and entity_id in children_ids:\n            parent.remove_child(entity_id)\n\n        if entity is None or not has_set_parent:\n            self.reset_immutable_for_hierarchy_cache(parent_id)\n            return\n\n        orig_parent_children = self._entities_by_parent_id[parent_id]\n        if entity in orig_parent_children:\n            orig_parent_children.remove(entity)\n\n        new_parent_children = self._entities_by_parent_id[new_parent_id]\n        if entity not in new_parent_children:\n            new_parent_children.append(entity)\n        self.reset_immutable_for_hierarchy_cache(parent_id)\n\n    def set_entity_parent(self, entity_id, parent_id, orig_parent_id=_NOT_SET):\n        parent = self._entities_by_id.get(parent_id)\n        entity = self._entities_by_id.get(entity_id)\n        if entity is None:\n            if parent is not None:\n                children_ids = parent.get_children_ids(False)\n                if (\n                    children_ids is not UNKNOWN_VALUE\n                    and entity_id in children_ids\n                ):\n                    parent.remove_child(entity_id)\n                self.reset_immutable_for_hierarchy_cache(parent.id)\n            return\n\n        if orig_parent_id is _NOT_SET:\n            orig_parent_id = entity.parent_id\n            if orig_parent_id == parent_id:\n                return\n\n        orig_parent_children = self._entities_by_parent_id[orig_parent_id]\n        if entity in orig_parent_children:\n            orig_parent_children.remove(entity)\n        self.reset_immutable_for_hierarchy_cache(orig_parent_id)\n\n        orig_parent = self._entities_by_id.get(orig_parent_id)\n        if orig_parent is not None:\n            orig_parent.remove_child(entity_id)\n\n        parent_children = self._entities_by_parent_id[parent_id]\n        if entity not in parent_children:\n            parent_children.append(entity)\n\n        entity.parent_id = parent_id\n        if parent is None or parent.get_children_ids(False) is UNKNOWN_VALUE:\n            return\n\n        parent.add_child(entity_id)\n        self.reset_immutable_for_hierarchy_cache(parent_id)\n\n    def _query_entity_children(self, entity):\n        folder_fields = self._get_folder_fields()\n        tasks = []\n        folders = []\n        if entity.entity_type == \"project\":\n            folders = list(self._connection.get_folders(\n                entity[\"name\"],\n                parent_ids=[entity.id],\n                fields=folder_fields,\n                own_attributes=True\n            ))\n\n        elif entity.entity_type == \"folder\":\n            folders = list(self._connection.get_folders(\n                self.project_entity[\"name\"],\n                parent_ids=[entity.id],\n                fields=folder_fields,\n                own_attributes=True\n            ))\n\n            tasks = list(self._connection.get_tasks(\n                self.project_entity[\"name\"],\n                folder_ids=[entity.id],\n                own_attributes=True\n            ))\n\n        children_ids = {\n            child.id\n            for child in self._entities_by_parent_id[entity.id]\n        }\n        for folder in folders:\n            folder_entity = self._entities_by_id.get(folder[\"id\"])\n            if folder_entity is not None:\n                if folder_entity.parent_id == entity.id:\n                    children_ids.add(folder_entity.id)\n                continue\n\n            folder_entity = self.add_folder(folder)\n            children_ids.add(folder_entity.id)\n\n        for task in tasks:\n            task_entity = self._entities_by_id.get(task[\"id\"])\n            if task_entity is not None:\n                if task_entity.parent_id == entity.id:\n                    children_ids.add(task_entity.id)\n                continue\n\n            task_entity = self.add_task(task)\n            children_ids.add(task_entity.id)\n\n        entity.fill_children_ids(children_ids)\n\n    def get_entity_children(self, entity, allow_query=True):\n        children_ids = entity.get_children_ids(allow_query=False)\n        if children_ids is not UNKNOWN_VALUE:\n            return entity.get_children()\n\n        if children_ids is UNKNOWN_VALUE and not allow_query:\n            return UNKNOWN_VALUE\n\n        self._query_entity_children(entity)\n\n        return entity.get_children()\n\n    def delete_entity(self, entity):\n        parent_id = entity.parent_id\n        if parent_id is None:\n            return\n\n        parent = self._entities_by_id.get(parent_id)\n        if parent is not None:\n            parent.remove_child(entity.id)\n\n    def reset_immutable_for_hierarchy_cache(\n        self, entity_id, bottom_to_top=True\n    ):\n        if bottom_to_top is None or entity_id is None:\n            return\n\n        reset_queue = collections.deque()\n        reset_queue.append(entity_id)\n        if bottom_to_top:\n            while reset_queue:\n                entity_id = reset_queue.popleft()\n                entity = self.get_entity_by_id(entity_id)\n                if entity is None:\n                    continue\n                entity.reset_immutable_for_hierarchy_cache(None)\n                reset_queue.append(entity.parent_id)\n        else:\n            while reset_queue:\n                entity_id = reset_queue.popleft()\n                entity = self.get_entity_by_id(entity_id)\n                if entity is None:\n                    continue\n                entity.reset_immutable_for_hierarchy_cache(None)\n                for child in self._entities_by_parent_id[entity.id]:\n                    reset_queue.append(child.id)\n\n    def fill_project_from_server(self):\n        \"\"\"Query project data from server and create project entity.\n\n        This method will invalidate previous object of Project entity.\n\n        Returns:\n            ProjectEntity: Entity that was updated with server data.\n\n        Raises:\n            ValueError: When project was not found on server.\n        \"\"\"\n\n        project_name = self.project_name\n        project = self._connection.get_project(\n            project_name,\n            own_attributes=True\n        )\n        if not project:\n            raise ValueError(\n                \"Project \\\"{}\\\" was not found.\".format(project_name)\n            )\n\n        self._project_entity = ProjectEntity(\n            project[\"code\"],\n            parent_id=PROJECT_PARENT_ID,\n            entity_id=project[\"name\"],\n            library=project[\"library\"],\n            folder_types=project[\"folderTypes\"],\n            task_types=project[\"taskTypes\"],\n            statuses=project[\"statuses\"],\n            name=project[\"name\"],\n            attribs=project[\"ownAttrib\"],\n            data=project[\"data\"],\n            active=project[\"active\"],\n            entity_hub=self\n        )\n        self.add_entity(self._project_entity)\n        return self._project_entity\n\n    def _get_folder_fields(self):\n        folder_fields = set(\n            self._connection.get_default_fields_for_type(\"folder\")\n        )\n        folder_fields.add(\"hasProducts\")\n        if self._allow_data_changes:\n            folder_fields.add(\"data\")\n        return folder_fields\n\n    def query_entities_from_server(self):\n        \"\"\"Query whole project at once.\"\"\"\n\n        project_entity = self.fill_project_from_server()\n\n        folder_fields = self._get_folder_fields()\n\n        folders = self._connection.get_folders(\n            project_entity.name,\n            fields=folder_fields,\n            own_attributes=True\n        )\n        tasks = self._connection.get_tasks(\n            project_entity.name,\n            own_attributes=True\n        )\n        folders_by_parent_id = collections.defaultdict(list)\n        for folder in folders:\n            parent_id = folder[\"parentId\"]\n            folders_by_parent_id[parent_id].append(folder)\n\n        tasks_by_parent_id = collections.defaultdict(list)\n        for task in tasks:\n            parent_id = task[\"folderId\"]\n            tasks_by_parent_id[parent_id].append(task)\n\n        lock_queue = collections.deque()\n        hierarchy_queue = collections.deque()\n        hierarchy_queue.append((None, project_entity))\n        while hierarchy_queue:\n            item = hierarchy_queue.popleft()\n            parent_id, parent_entity = item\n\n            lock_queue.append(parent_entity)\n\n            children_ids = set()\n            for folder in folders_by_parent_id[parent_id]:\n                folder_entity = self.add_folder(folder)\n                children_ids.add(folder_entity.id)\n                folder_entity.has_published_content = folder[\"hasProducts\"]\n                hierarchy_queue.append((folder_entity.id, folder_entity))\n\n            for task in tasks_by_parent_id[parent_id]:\n                task_entity = self.add_task(task)\n                lock_queue.append(task_entity)\n                children_ids.add(task_entity.id)\n\n            parent_entity.fill_children_ids(children_ids)\n\n        # Lock entities when all are added to hub\n        # - lock only entities added in this method\n        while lock_queue:\n            entity = lock_queue.popleft()\n            entity.lock()\n\n    def lock(self):\n        if self._project_entity is None:\n            return\n\n        for entity in self._entities_by_id.values():\n            entity.lock()\n\n    def _get_top_entities(self):\n        all_ids = set(self._entities_by_id.keys())\n        return [\n            entity\n            for entity in self._entities_by_id.values()\n            if entity.parent_id not in all_ids\n        ]\n\n    def _split_entities(self):\n        top_entities = self._get_top_entities()\n        entities_queue = collections.deque(top_entities)\n        removed_entity_ids = []\n        created_entity_ids = []\n        other_entity_ids = []\n        while entities_queue:\n            entity = entities_queue.popleft()\n            removed = entity.removed\n            if removed:\n                removed_entity_ids.append(entity.id)\n            elif entity.created:\n                created_entity_ids.append(entity.id)\n            else:\n                other_entity_ids.append(entity.id)\n\n            for child in tuple(self._entities_by_parent_id[entity.id]):\n                if removed:\n                    self.unset_entity_parent(child.id, entity.id)\n                entities_queue.append(child)\n        return created_entity_ids, other_entity_ids, removed_entity_ids\n\n    def _get_update_body(self, entity, changes=None):\n        if changes is None:\n            changes = entity.changes\n\n        if not changes:\n            return None\n        return {\n            \"type\": \"update\",\n            \"entityType\": entity.entity_type,\n            \"entityId\": entity.id,\n            \"data\": changes\n        }\n\n    def _get_create_body(self, entity):\n        return {\n            \"type\": \"create\",\n            \"entityType\": entity.entity_type,\n            \"entityId\": entity.id,\n            \"data\": entity.to_create_body_data()\n        }\n\n    def _get_delete_body(self, entity):\n        return {\n            \"type\": \"delete\",\n            \"entityType\": entity.entity_type,\n            \"entityId\": entity.id\n        }\n\n    def _pre_commit_types_changes(\n        self, project_changes, orig_types, changes_key, post_changes\n    ):\n        \"\"\"Compare changes of types on a project.\n\n        Compare old and new types. Change project changes content if some old\n        types were removed. In that case the  final change of types will\n        happen when all other entities have changed.\n\n        Args:\n            project_changes (dict[str, Any]): Project changes.\n            orig_types (list[dict[str, Any]]): Original types.\n            changes_key (Literal[folderTypes, taskTypes]): Key of type changes\n                in project changes.\n            post_changes (dict[str, Any]): An object where post changes will\n                be stored.\n        \"\"\"\n\n        if changes_key not in project_changes:\n            return\n\n        new_types = project_changes[changes_key]\n\n        orig_types_by_name = {\n            type_info[\"name\"]: type_info\n            for type_info in orig_types\n        }\n        new_names = {\n            type_info[\"name\"]\n            for type_info in new_types\n        }\n        diff_names = set(orig_types_by_name) - new_names\n        if not diff_names:\n            return\n\n        # Create copy of folder type changes to post changes\n        #   - post changes will be commited at the end\n        post_changes[changes_key] = copy.deepcopy(new_types)\n\n        for type_name in diff_names:\n            new_types.append(orig_types_by_name[type_name])\n\n    def _pre_commit_project(self):\n        \"\"\"Some project changes cannot be committed before hierarchy changes.\n\n        It is not possible to change folder types or task types if there are\n        existing hierarchy items using the removed types. For that purposes\n        is first committed union of all old and new types and post changes\n        are prepared when all existing entities are changed.\n\n        Returns:\n            dict[str, Any]: Changes that will be committed after hierarchy\n                changes.\n        \"\"\"\n\n        project_changes = self.project_entity.changes\n\n        post_changes = {}\n        if not project_changes:\n            return post_changes\n\n        self._pre_commit_types_changes(\n            project_changes,\n            self.project_entity.get_orig_folder_types(),\n            \"folderType\",\n            post_changes\n        )\n        self._pre_commit_types_changes(\n            project_changes,\n            self.project_entity.get_orig_task_types(),\n            \"taskType\",\n            post_changes\n        )\n        self._connection.update_project(self.project_name, **project_changes)\n        return post_changes\n\n    def commit_changes(self):\n        \"\"\"Commit any changes that happened on entities.\n\n        Todos:\n            Use Operations Session instead of known operations body.\n        \"\"\"\n\n        post_project_changes = self._pre_commit_project()\n        self.project_entity.lock()\n\n        project_changes = self.project_entity.changes\n        if project_changes:\n            response = self._connection.patch(\n                \"projects/{}\".format(self.project_name),\n                **project_changes\n            )\n            response.raise_for_status()\n\n        self.project_entity.lock()\n\n        operations_body = []\n\n        created_entity_ids, other_entity_ids, removed_entity_ids = (\n            self._split_entities()\n        )\n        processed_ids = set()\n        for entity_id in other_entity_ids:\n            if entity_id in processed_ids:\n                continue\n\n            entity = self._entities_by_id[entity_id]\n            changes = entity.changes\n            processed_ids.add(entity_id)\n            if not changes:\n                continue\n\n            bodies = [self._get_update_body(entity, changes)]\n            # Parent was created and was not yet added to operations body\n            parent_queue = collections.deque()\n            parent_queue.append(entity.parent_id)\n            while parent_queue:\n                # Make sure entity's parents are created\n                parent_id = parent_queue.popleft()\n                if (\n                    parent_id is UNKNOWN_VALUE\n                    or parent_id in processed_ids\n                    or parent_id not in created_entity_ids\n                ):\n                    continue\n\n                parent = self._entities_by_id.get(parent_id)\n                processed_ids.add(parent.id)\n                bodies.append(self._get_create_body(parent))\n                parent_queue.append(parent.id)\n\n            operations_body.extend(reversed(bodies))\n\n        for entity_id in created_entity_ids:\n            if entity_id in processed_ids:\n                continue\n            entity = self._entities_by_id[entity_id]\n            processed_ids.add(entity_id)\n            operations_body.append(self._get_create_body(entity))\n\n        for entity_id in reversed(removed_entity_ids):\n            if entity_id in processed_ids:\n                continue\n\n            entity = self._entities_by_id.pop(entity_id)\n            parent_children = self._entities_by_parent_id[entity.parent_id]\n            if entity in parent_children:\n                parent_children.remove(entity)\n\n            if not entity.created:\n                operations_body.append(self._get_delete_body(entity))\n\n        self._connection.send_batch_operations(\n            self.project_name, operations_body\n        )\n        if post_project_changes:\n            self._connection.update_project(\n                self.project_name, **post_project_changes)\n\n        self.lock()\n\n\nclass AttributeValue(object):\n    def __init__(self, value):\n        self._value = value\n        self._origin_value = copy.deepcopy(value)\n\n    def get_value(self):\n        return self._value\n\n    def set_value(self, value):\n        self._value = value\n\n    value = property(get_value, set_value)\n\n    @property\n    def changed(self):\n        return self._value != self._origin_value\n\n    def lock(self):\n        self._origin_value = copy.deepcopy(self._value)\n\n\nclass Attributes(object):\n    \"\"\"Object representing attribs of entity.\n\n    Todos:\n        This could be enhanced to know attribute schema and validate values\n        based on the schema.\n\n    Args:\n        attrib_keys (Iterable[str]): Keys that are available in attribs of the\n            entity.\n        values (Union[None, Dict[str, Any]]): Values of attributes.\n    \"\"\"\n\n    def __init__(self, attrib_keys, values=UNKNOWN_VALUE):\n        if values in (UNKNOWN_VALUE, None):\n            values = {}\n        self._attributes = {\n            key: AttributeValue(values.get(key))\n            for key in attrib_keys\n        }\n\n    def __contains__(self, key):\n        return key in self._attributes\n\n    def __getitem__(self, key):\n        return self._attributes[key].value\n\n    def __setitem__(self, key, value):\n        self._attributes[key].set_value(value)\n\n    def __iter__(self):\n        for key in self._attributes:\n            yield key\n\n    def keys(self):\n        return self._attributes.keys()\n\n    def values(self):\n        for attribute in self._attributes.values():\n            yield attribute.value\n\n    def items(self):\n        for key, attribute in self._attributes.items():\n            yield key, attribute.value\n\n    def get(self, key, default=None):\n        \"\"\"Get value of attribute.\n\n        Args:\n            key (str): Attribute name.\n            default (Any): Default value to return when attribute was not\n                found.\n        \"\"\"\n\n        attribute = self._attributes.get(key)\n        if attribute is None:\n            return default\n        return attribute.value\n\n    def set(self, key, value):\n        \"\"\"Change value of attribute.\n\n        Args:\n            key (str): Attribute name.\n            value (Any): New value of the attribute.\n        \"\"\"\n\n        self[key] = value\n\n    def get_attribute(self, key):\n        \"\"\"Access to attribute object.\n\n        Args:\n            key (str): Name of attribute.\n\n        Returns:\n            AttributeValue: Object of attribute value.\n\n        Raises:\n            KeyError: When attribute is not available.\n        \"\"\"\n\n        return self._attributes[key]\n\n    def lock(self):\n        for attribute in self._attributes.values():\n            attribute.lock()\n\n    @property\n    def changes(self):\n        \"\"\"Attribute value changes.\n\n        Returns:\n            Dict[str, Any]: Key mapping with new values.\n        \"\"\"\n\n        return {\n            attr_key: attribute.value\n            for attr_key, attribute in self._attributes.items()\n            if attribute.changed\n        }\n\n    def to_dict(self, ignore_none=True):\n        output = {}\n        for key, value in self.items():\n            if (\n                value is UNKNOWN_VALUE\n                or (ignore_none and value is None)\n            ):\n                continue\n\n            output[key] = value\n        return output\n\n\n@six.add_metaclass(ABCMeta)\nclass BaseEntity(object):\n    \"\"\"Object representation of entity from server which is capturing changes.\n\n    All data on created object are expected as \"current data\" on server entity\n    unless the entity has set 'created' to 'True'. So if new data should be\n    stored to server entity then fill entity with server data first and\n    then change them.\n\n    Calling 'lock' method will mark entity as \"saved\" and all changes made on\n    entity are set as \"current data\" on server.\n\n    Args:\n        entity_id (Union[str, None]): Id of the entity. New id is created if\n            not passed.\n        parent_id (Union[str, None]): Id of parent entity.\n        name (str): Name of entity.\n        attribs (Dict[str, Any]): Attribute values.\n        data (Dict[str, Any]): Entity data (custom data).\n        thumbnail_id (Union[str, None]): Id of entity's thumbnail.\n        active (bool): Is entity active.\n        entity_hub (EntityHub): Object of entity hub which created object of\n            the entity.\n        created (Optional[bool]): Entity is new. When 'None' is passed the\n            value is defined based on value of 'entity_id'.\n    \"\"\"\n\n    def __init__(\n        self,\n        entity_id=None,\n        parent_id=UNKNOWN_VALUE,\n        name=UNKNOWN_VALUE,\n        attribs=UNKNOWN_VALUE,\n        data=UNKNOWN_VALUE,\n        thumbnail_id=UNKNOWN_VALUE,\n        active=UNKNOWN_VALUE,\n        entity_hub=None,\n        created=None\n    ):\n        if entity_hub is None:\n            raise ValueError(\"Missing required kwarg 'entity_hub'\")\n\n        self._entity_hub = entity_hub\n\n        if created is None:\n            created = entity_id is None\n\n        entity_id = self._prepare_entity_id(entity_id)\n\n        if data is None:\n            data = {}\n\n        children_ids = UNKNOWN_VALUE\n        if created:\n            children_ids = set()\n\n        if not created and parent_id is UNKNOWN_VALUE:\n            raise ValueError(\"Existing entity is missing parent id.\")\n\n        # These are public without any validation at this moment\n        #   may change in future (e.g. name will have regex validation)\n        self._entity_id = entity_id\n\n        self._parent_id = parent_id\n        self._name = name\n        self.active = active\n        self._created = created\n        self._thumbnail_id = thumbnail_id\n        self._attribs = Attributes(\n            self._get_attributes_for_type(self.entity_type),\n            attribs\n        )\n        self._data = data\n        self._children_ids = children_ids\n\n        self._orig_parent_id = parent_id\n        self._orig_name = name\n        self._orig_data = copy.deepcopy(data)\n        self._orig_thumbnail_id = thumbnail_id\n        self._orig_active = active\n\n        self._immutable_for_hierarchy_cache = None\n\n    def __repr__(self):\n        return \"<{} - {}>\".format(self.__class__.__name__, self.id)\n\n    def __getitem__(self, item):\n        return getattr(self, item)\n\n    def __setitem__(self, item, value):\n        return setattr(self, item, value)\n\n    def _prepare_entity_id(self, entity_id):\n        entity_id = convert_entity_id(entity_id)\n        if entity_id is None:\n            entity_id = create_entity_id()\n        return entity_id\n\n    @property\n    def id(self):\n        \"\"\"Access to entity id under which is entity available on server.\n\n        Returns:\n            str: Entity id.\n        \"\"\"\n\n        return self._entity_id\n\n    @property\n    def removed(self):\n        return self._parent_id is None\n\n    @property\n    def orig_parent_id(self):\n        return self._orig_parent_id\n\n    @property\n    def attribs(self):\n        \"\"\"Entity attributes based on server configuration.\n\n        Returns:\n            Attributes: Attributes object handling changes and values of\n                attributes on entity.\n        \"\"\"\n\n        return self._attribs\n\n    @property\n    def data(self):\n        \"\"\"Entity custom data that are not stored by any deterministic model.\n\n        Be aware that 'data' can't be queried using GraphQl and cannot be\n            updated partially.\n\n        Returns:\n            Dict[str, Any]: Custom data on entity.\n        \"\"\"\n\n        return self._data\n\n    @property\n    def project_name(self):\n        \"\"\"Quick access to project from entity hub.\n\n        Returns:\n            str: Name of project under which entity lives.\n        \"\"\"\n\n        return self._entity_hub.project_name\n\n    @property\n    @abstractmethod\n    def entity_type(self):\n        \"\"\"Entity type coresponding to server.\n\n        Returns:\n            Literal[project, folder, task]: Entity type.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def parent_entity_types(self):\n        \"\"\"Entity type coresponding to server.\n\n        Returns:\n            Iterable[str]: Possible entity types of parent.\n        \"\"\"\n\n        pass\n\n    @property\n    @abstractmethod\n    def changes(self):\n        \"\"\"Receive entity changes.\n\n        Returns:\n            Union[Dict[str, Any], None]: All values that have changed on\n                entity. New entity must return None.\n        \"\"\"\n\n        pass\n\n    @classmethod\n    @abstractmethod\n    def from_entity_data(cls, entity_data, entity_hub):\n        \"\"\"Create entity based on queried data from server.\n\n        Args:\n            entity_data (Dict[str, Any]): Entity data from server.\n            entity_hub (EntityHub): Hub which handle the entity.\n\n        Returns:\n            BaseEntity: Object of the class.\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    def to_create_body_data(self):\n        \"\"\"Convert object of entity to data for server on creation.\n\n        Returns:\n            Dict[str, Any]: Entity data.\n        \"\"\"\n\n        pass\n\n    @property\n    def immutable_for_hierarchy(self):\n        \"\"\"Entity is immutable for hierarchy changes.\n\n        Hierarchy changes can be considered as change of name or parents.\n\n        Returns:\n            bool: Entity is immutable for hierarchy changes.\n        \"\"\"\n\n        if self._immutable_for_hierarchy_cache is not None:\n            return self._immutable_for_hierarchy_cache\n\n        immutable_for_hierarchy = self._immutable_for_hierarchy\n        if immutable_for_hierarchy is not None:\n            self._immutable_for_hierarchy_cache = immutable_for_hierarchy\n            return self._immutable_for_hierarchy_cache\n\n        for child in self._entity_hub.get_entity_children(self):\n            if child.immutable_for_hierarchy:\n                self._immutable_for_hierarchy_cache = True\n                return self._immutable_for_hierarchy_cache\n\n        self._immutable_for_hierarchy_cache = False\n        return self._immutable_for_hierarchy_cache\n\n    @property\n    def _immutable_for_hierarchy(self):\n        \"\"\"Override this method to define if entity object is immutable.\n\n        This property was added to define immutable state of Folder entities\n        which is used in property 'immutable_for_hierarchy'.\n\n        Returns:\n            Union[bool, None]: Bool to explicitly telling if is immutable or\n                not otherwise None.\n        \"\"\"\n\n        return None\n\n    @property\n    def has_cached_immutable_hierarchy(self):\n        return self._immutable_for_hierarchy_cache is not None\n\n    def reset_immutable_for_hierarchy_cache(self, bottom_to_top=True):\n        \"\"\"Clear cache of immutable hierarchy property.\n\n        This is used when entity changed parent or a child was added.\n\n        Args:\n            bottom_to_top (bool): Reset cache from top hierarchy to bottom or\n                from bottom hierarchy to top.\n        \"\"\"\n\n        self._immutable_for_hierarchy_cache = None\n        self._entity_hub.reset_immutable_for_hierarchy_cache(\n            self.id, bottom_to_top\n        )\n\n    def _get_default_changes(self):\n        \"\"\"Collect changes of common data on entity.\n\n        Returns:\n            Dict[str, Any]: Changes on entity. Key and it's new value.\n        \"\"\"\n\n        changes = {}\n        if self._orig_name != self._name:\n            changes[\"name\"] = self._name\n\n        if self._entity_hub.allow_data_changes:\n            if (\n                self._data is not UNKNOWN_VALUE\n                and self._orig_data != self._data\n            ):\n                changes[\"data\"] = self._data\n\n        if self._orig_thumbnail_id != self._thumbnail_id:\n            changes[\"thumbnailId\"] = self._thumbnail_id\n\n        if self._orig_active != self.active:\n            changes[\"active\"] = self.active\n\n        attrib_changes = self.attribs.changes\n        if attrib_changes:\n            changes[\"attrib\"] = attrib_changes\n        return changes\n\n    def _get_attributes_for_type(self, entity_type):\n        return self._entity_hub.get_attributes_for_type(entity_type)\n\n    def lock(self):\n        \"\"\"Lock entity as 'saved' so all changes are discarded.\"\"\"\n\n        self._orig_parent_id = self._parent_id\n        self._orig_name = self._name\n        self._orig_data = copy.deepcopy(self._data)\n        self._orig_thumbnail_id = self.thumbnail_id\n        self._attribs.lock()\n\n        self._immutable_for_hierarchy_cache = None\n        self._created = False\n\n    def _get_entity_by_id(self, entity_id):\n        return self._entity_hub.get_entity_by_id(entity_id)\n\n    def get_name(self):\n        return self._name\n\n    def set_name(self, name):\n        self._name = name\n\n    name = property(get_name, set_name)\n\n    def get_parent_id(self):\n        \"\"\"Parent entity id.\n\n        Returns:\n            Union[str, None]: Id of parent entity or none if is not set.\n        \"\"\"\n\n        return self._parent_id\n\n    def set_parent_id(self, parent_id):\n        \"\"\"Change parent by id.\n\n        Args:\n            parent_id (Union[str, None]): Id of new parent for entity.\n\n        Raises:\n            ValueError: If parent was not found by id.\n            TypeError: If validation of parent does not pass.\n        \"\"\"\n\n        if parent_id != self._parent_id:\n            orig_parent_id = self._parent_id\n            self._parent_id = parent_id\n            self._entity_hub.set_entity_parent(\n                self.id, parent_id, orig_parent_id\n            )\n\n    parent_id = property(get_parent_id, set_parent_id)\n\n    def get_parent(self, allow_query=True):\n        \"\"\"Parent entity.\n\n        Returns:\n            Union[BaseEntity, None]: Parent object.\n        \"\"\"\n\n        parent = self._entity_hub.get_entity_by_id(self._parent_id)\n        if parent is not None:\n            return parent\n\n        if not allow_query:\n            return self._parent_id\n\n        if self._parent_id is UNKNOWN_VALUE:\n            return self._parent_id\n\n        return self._entity_hub.get_or_query_entity_by_id(\n            self._parent_id, self.parent_entity_types\n        )\n\n    def set_parent(self, parent):\n        \"\"\"Change parent object.\n\n        Args:\n            parent (BaseEntity): New parent for entity.\n\n        Raises:\n            TypeError: If validation of parent does not pass.\n        \"\"\"\n\n        parent_id = None\n        if parent is not None:\n            parent_id = parent.id\n        self._entity_hub.set_entity_parent(self.id, parent_id)\n\n    parent = property(get_parent, set_parent)\n\n    def get_children_ids(self, allow_query=True):\n        \"\"\"Access to children objects.\n\n        Todos:\n            Children should be maybe handled by EntityHub instead of entities\n                themselves. That would simplify 'set_entity_parent',\n                'unset_entity_parent' and other logic related to changing\n                hierarchy.\n\n        Returns:\n            Union[List[str], Type[UNKNOWN_VALUE]]: Children iterator.\n        \"\"\"\n\n        if self._children_ids is UNKNOWN_VALUE:\n            if not allow_query:\n                return self._children_ids\n            self._entity_hub.get_entity_children(self, True)\n        return set(self._children_ids)\n\n    children_ids = property(get_children_ids)\n\n    def get_children(self, allow_query=True):\n        \"\"\"Access to children objects.\n\n        Returns:\n            Union[List[BaseEntity], Type[UNKNOWN_VALUE]]: Children iterator.\n        \"\"\"\n\n        if self._children_ids is UNKNOWN_VALUE:\n            if not allow_query:\n                return self._children_ids\n            return self._entity_hub.get_entity_children(self, True)\n\n        return [\n            self._entity_hub.get_entity_by_id(children_id)\n            for children_id in self._children_ids\n        ]\n\n    children = property(get_children)\n\n    def add_child(self, child):\n        \"\"\"Add child entity.\n\n        Args:\n            child (BaseEntity): Child object to add.\n\n        Raises:\n            TypeError: When child object has invalid type to be children.\n        \"\"\"\n\n        child_id = child\n        if isinstance(child_id, BaseEntity):\n            child_id = child.id\n\n        if self._children_ids is not UNKNOWN_VALUE:\n            self._children_ids.add(child_id)\n\n        self._entity_hub.set_entity_parent(child_id, self.id)\n\n    def remove_child(self, child):\n        \"\"\"Remove child entity.\n\n        Is ignored if child is not in children.\n\n        Args:\n            child (Union[str, BaseEntity]): Child object or child id to remove.\n        \"\"\"\n\n        child_id = child\n        if isinstance(child_id, BaseEntity):\n            child_id = child.id\n\n        if self._children_ids is not UNKNOWN_VALUE:\n            self._children_ids.discard(child_id)\n        self._entity_hub.unset_entity_parent(child_id, self.id)\n\n    def get_thumbnail_id(self):\n        \"\"\"Thumbnail id of entity.\n\n        Returns:\n            Union[str, None]: Id of parent entity or none if is not set.\n        \"\"\"\n\n        return self._thumbnail_id\n\n    def set_thumbnail_id(self, thumbnail_id):\n        \"\"\"Change thumbnail id.\n\n        Args:\n            thumbnail_id (Union[str, None]): Id of thumbnail for entity.\n        \"\"\"\n\n        self._thumbnail_id = thumbnail_id\n\n    thumbnail_id = property(get_thumbnail_id, set_thumbnail_id)\n\n    @property\n    def created(self):\n        \"\"\"Entity is new.\n\n        Returns:\n            bool: Entity is newly created.\n        \"\"\"\n\n        return self._created\n\n    def fill_children_ids(self, children_ids):\n        \"\"\"Fill children ids on entity.\n\n        Warning:\n            This is not an api call but is called from entity hub.\n        \"\"\"\n\n        self._children_ids = set(children_ids)\n\n\nclass ProjectStatus:\n    \"\"\"Project status class.\n\n    Args:\n        name (str): Name of the status. e.g. 'In progress'\n        short_name (Optional[str]): Short name of the status. e.g. 'IP'\n        state (Optional[Literal[not_started, in_progress, done, blocked]]): A\n            state of the status.\n        icon (Optional[str]): Icon of the status. e.g. 'play_arrow'.\n        color (Optional[str]): Color of the status. e.g. '#eeeeee'.\n        index (Optional[int]): Index of the status.\n        project_statuses (Optional[_ProjectStatuses]): Project statuses\n            wrapper.\n    \"\"\"\n\n    valid_states = (\"not_started\", \"in_progress\", \"done\", \"blocked\")\n    color_regex = re.compile(r\"#([a-f0-9]{6})$\")\n    default_state = \"in_progress\"\n    default_color = \"#eeeeee\"\n\n    def __init__(\n        self,\n        name,\n        short_name=None,\n        state=None,\n        icon=None,\n        color=None,\n        index=None,\n        project_statuses=None,\n        is_new=None,\n    ):\n        short_name = short_name or \"\"\n        icon = icon or \"\"\n        state = state or self.default_state\n        color = color or self.default_color\n        self._name = name\n        self._short_name = short_name\n        self._icon = icon\n        self._slugified_name = None\n        self._state = None\n        self._color = None\n        self.set_state(state)\n        self.set_color(color)\n\n        self._original_name = name\n        self._original_short_name = short_name\n        self._original_icon = icon\n        self._original_state = state\n        self._original_color = color\n        self._original_index = index\n\n        self._index = index\n        self._project_statuses = project_statuses\n        if is_new is None:\n            is_new = index is None or project_statuses is None\n        self._is_new = is_new\n\n    def __str__(self):\n        short_name = \"\"\n        if self.short_name:\n            short_name = \"({})\".format(self.short_name)\n        return \"<{} {}{}>\".format(\n            self.__class__.__name__, self.name, short_name\n        )\n\n    def __repr__(self):\n        return str(self)\n\n    def __getitem__(self, key):\n        if key in {\n            \"name\", \"short_name\", \"icon\", \"state\", \"color\", \"slugified_name\"\n        }:\n            return getattr(self, key)\n        raise KeyError(key)\n\n    def __setitem__(self, key, value):\n        if key in {\"name\", \"short_name\", \"icon\", \"state\", \"color\"}:\n            return setattr(self, key, value)\n        raise KeyError(key)\n\n    def lock(self):\n        \"\"\"Lock status.\n\n        Changes were commited and current values are now the original values.\n        \"\"\"\n\n        self._is_new = False\n        self._original_name = self.name\n        self._original_short_name = self.short_name\n        self._original_icon = self.icon\n        self._original_state = self.state\n        self._original_color = self.color\n        self._original_index = self.index\n\n    @staticmethod\n    def slugify_name(name):\n        \"\"\"Slugify status name for name comparison.\n\n        Args:\n            name (str): Name of the status.\n\n        Returns:\n            str: Slugified name.\n        \"\"\"\n\n        return slugify_string(name.lower())\n\n    def get_project_statuses(self):\n        \"\"\"Internal logic method.\n\n        Returns:\n            _ProjectStatuses: Project statuses object.\n        \"\"\"\n\n        return self._project_statuses\n\n    def set_project_statuses(self, project_statuses):\n        \"\"\"Internal logic method to change parent object.\n\n        Args:\n            project_statuses (_ProjectStatuses): Project statuses object.\n        \"\"\"\n\n        self._project_statuses = project_statuses\n\n    def unset_project_statuses(self, project_statuses):\n        \"\"\"Internal logic method to unset parent object.\n\n        Args:\n            project_statuses (_ProjectStatuses): Project statuses object.\n        \"\"\"\n\n        if self._project_statuses is project_statuses:\n            self._project_statuses = None\n            self._index = None\n\n    @property\n    def changed(self):\n        \"\"\"Status has changed.\n\n        Returns:\n            bool: Status has changed.\n        \"\"\"\n\n        return (\n            self._is_new\n            or self._original_name != self._name\n            or self._original_short_name != self._short_name\n            or self._original_index != self._index\n            or self._original_state != self._state\n            or self._original_icon != self._icon\n            or self._original_color != self._color\n        )\n\n    def delete(self):\n        \"\"\"Remove status from project statuses object.\"\"\"\n\n        if self._project_statuses is not None:\n            self._project_statuses.remove(self)\n\n    def get_index(self):\n        \"\"\"Get index of status.\n\n        Returns:\n            Union[int, None]: Index of status or None if status is not under\n                project.\n        \"\"\"\n\n        return self._index\n\n    def set_index(self, index, **kwargs):\n        \"\"\"Change status index.\n\n        Returns:\n            Union[int, None]: Index of status or None if status is not under\n                project.\n        \"\"\"\n\n        if kwargs.get(\"from_parent\"):\n            self._index = index\n        else:\n            self._project_statuses.set_status_index(self, index)\n\n    def get_name(self):\n        \"\"\"Status name.\n\n        Returns:\n            str: Status name.\n        \"\"\"\n\n        return self._name\n\n    def set_name(self, name):\n        \"\"\"Change status name.\n\n        Args:\n            name (str): New status name.\n        \"\"\"\n\n        if not isinstance(name, six.string_types):\n            raise TypeError(\"Name must be a string.\")\n        if name == self._name:\n            return\n        self._name = name\n        self._slugified_name = None\n\n    def get_short_name(self):\n        \"\"\"Status short name 3 letters tops.\n\n        Returns:\n            str: Status short name.\n        \"\"\"\n\n        return self._short_name\n\n    def set_short_name(self, short_name):\n        \"\"\"Change status short name.\n\n        Args:\n            short_name (str): New status short name. 3 letters tops.\n        \"\"\"\n\n        if not isinstance(short_name, six.string_types):\n            raise TypeError(\"Short name must be a string.\")\n        self._short_name = short_name\n\n    def get_icon(self):\n        \"\"\"Name of icon to use for status.\n\n        Returns:\n            str: Name of the icon.\n        \"\"\"\n\n        return self._icon\n\n    def set_icon(self, icon):\n        \"\"\"Change status icon name.\n\n        Args:\n            icon (str): Name of the icon.\n        \"\"\"\n\n        if icon is None:\n            icon = \"\"\n        if not isinstance(icon, six.string_types):\n            raise TypeError(\"Icon name must be a string.\")\n        self._icon = icon\n\n    @property\n    def slugified_name(self):\n        \"\"\"Slugified and lowere status name.\n\n        Can be used for comparison of existing statuses. e.g. 'In Progress'\n            vs. 'in-progress'.\n\n        Returns:\n            str: Slugified and lower status name.\n        \"\"\"\n\n        if self._slugified_name is None:\n            self._slugified_name = self.slugify_name(self.name)\n        return self._slugified_name\n\n    def get_state(self):\n        \"\"\"Get state of project status.\n\n        Return:\n            Literal[not_started, in_progress, done, blocked]: General\n                state of status.\n        \"\"\"\n\n        return self._state\n\n    def set_state(self, state):\n        \"\"\"Set color of project status.\n\n        Args:\n            state (Literal[not_started, in_progress, done, blocked]): General\n                state of status.\n        \"\"\"\n\n        if state not in self.valid_states:\n            raise ValueError(\"Invalid state '{}'\".format(str(state)))\n        self._state = state\n\n    def get_color(self):\n        \"\"\"Get color of project status.\n\n        Returns:\n            str: Status color.\n        \"\"\"\n\n        return self._color\n\n    def set_color(self, color):\n        \"\"\"Set color of project status.\n\n        Args:\n            color (str): Color in hex format. Example: '#ff0000'.\n        \"\"\"\n\n        if not isinstance(color, six.string_types):\n            raise TypeError(\n                \"Color must be string got '{}'\".format(type(color)))\n        color = color.lower()\n        if self.color_regex.fullmatch(color) is None:\n            raise ValueError(\"Invalid color value '{}'\".format(color))\n        self._color = color\n\n    name = property(get_name, set_name)\n    short_name = property(get_short_name, set_short_name)\n    project_statuses = property(get_project_statuses, set_project_statuses)\n    index = property(get_index, set_index)\n    state = property(get_state, set_state)\n    color = property(get_color, set_color)\n    icon = property(get_icon, set_icon)\n\n    def _validate_other_p_statuses(self, other):\n        \"\"\"Validate if other status can be used for move.\n\n        To be able to work with other status, and position them in relation,\n        they must belong to same existing object of '_ProjectStatuses'.\n\n        Args:\n            other (ProjectStatus): Other status to validate.\n        \"\"\"\n\n        o_project_statuses = other.project_statuses\n        m_project_statuses = self.project_statuses\n        if o_project_statuses is None and m_project_statuses is None:\n            raise ValueError(\"Both statuses are not assigned to a project.\")\n\n        missing_status = None\n        if o_project_statuses is None:\n            missing_status = other\n        elif m_project_statuses is None:\n            missing_status = self\n        if missing_status is not None:\n            raise ValueError(\n                \"Status '{}' is not assigned to a project.\".format(\n                    missing_status.name))\n        if m_project_statuses is not o_project_statuses:\n            raise ValueError(\n                \"Statuse are assigned to different projects.\"\n                \" Cannot execute move.\"\n            )\n\n    def move_before(self, other):\n        \"\"\"Move status before other status.\n\n        Args:\n            other (ProjectStatus): Status to move before.\n        \"\"\"\n\n        self._validate_other_p_statuses(other)\n        self._project_statuses.set_status_index(self, other.index)\n\n    def move_after(self, other):\n        \"\"\"Move status after other status.\n\n        Args:\n            other (ProjectStatus): Status to move after.\n        \"\"\"\n\n        self._validate_other_p_statuses(other)\n        self._project_statuses.set_status_index(self, other.index + 1)\n\n    def to_data(self):\n        \"\"\"Convert status to data.\n\n        Returns:\n            dict[str, str]: Status data.\n        \"\"\"\n\n        output = {\n            \"name\": self.name,\n            \"shortName\": self.short_name,\n            \"state\": self.state,\n            \"icon\": self.icon,\n            \"color\": self.color,\n        }\n        if (\n            not self._is_new\n            and self._original_name\n            and self.name != self._original_name\n        ):\n            output[\"original_name\"] = self._original_name\n        return output\n\n    @classmethod\n    def from_data(cls, data, index=None, project_statuses=None):\n        \"\"\"Create project status from data.\n\n        Args:\n            data (dict[str, str]): Status data.\n            index (Optional[int]): Status index.\n            project_statuses (Optional[ProjectStatuses]): Project statuses\n                object which wraps the status for a project.\n        \"\"\"\n\n        return cls(\n            data[\"name\"],\n            data.get(\"shortName\", data.get(\"short_name\")),\n            data.get(\"state\"),\n            data.get(\"icon\"),\n            data.get(\"color\"),\n            index=index,\n            project_statuses=project_statuses\n        )\n\n\nclass _ProjectStatuses:\n    \"\"\"Wrapper for project statuses.\n\n    Supports basic methods to add, change or remove statuses from a project.\n\n    To add new statuses use 'create' or 'add_status' methods. To change\n        statuses receive them by one of the getter methods and change their\n        values.\n\n    Todos:\n        Validate if statuses are duplicated.\n    \"\"\"\n\n    def __init__(self, statuses):\n        self._statuses = [\n            ProjectStatus.from_data(status, idx, self)\n            for idx, status in enumerate(statuses)\n        ]\n        self._orig_status_length = len(self._statuses)\n        self._set_called = False\n\n    def __len__(self):\n        return len(self._statuses)\n\n    def __iter__(self):\n        \"\"\"Iterate over statuses.\n\n        Yields:\n            ProjectStatus: Project status.\n        \"\"\"\n\n        for status in self._statuses:\n            yield status\n\n    def create(\n        self,\n        name,\n        short_name=None,\n        state=None,\n        icon=None,\n        color=None,\n    ):\n        \"\"\"Create project status.\n\n        Args:\n            name (str): Name of the status. e.g. 'In progress'\n            short_name (Optional[str]): Short name of the status. e.g. 'IP'\n            state (Optional[Literal[not_started, in_progress, done, blocked]]): A\n                state of the status.\n            icon (Optional[str]): Icon of the status. e.g. 'play_arrow'.\n            color (Optional[str]): Color of the status. e.g. '#eeeeee'.\n\n        Returns:\n            ProjectStatus: Created project status.\n        \"\"\"\n\n        status = ProjectStatus(\n            name, short_name, state, icon, color, is_new=True\n        )\n        self.append(status)\n        return status\n\n    def lock(self):\n        \"\"\"Lock statuses.\n\n        Changes were commited and current values are now the original values.\n        \"\"\"\n\n        self._orig_status_length = len(self._statuses)\n        self._set_called = False\n        for status in self._statuses:\n            status.lock()\n\n    def to_data(self):\n        \"\"\"Convert to project statuses data.\"\"\"\n\n        return [\n            status.to_data()\n            for status in self._statuses\n        ]\n\n    def set(self, statuses):\n        \"\"\"Explicitly override statuses.\n\n        This method does not handle if statuses changed or not.\n\n        Args:\n            statuses (list[dict[str, str]]): List of statuses data.\n        \"\"\"\n\n        self._set_called = True\n        self._statuses = [\n            ProjectStatus.from_data(status, idx, self)\n            for idx, status in enumerate(statuses)\n        ]\n\n    @property\n    def changed(self):\n        \"\"\"Statuses have changed.\n\n        Returns:\n            bool: True if statuses changed, False otherwise.\n        \"\"\"\n\n        if self._set_called:\n            return True\n\n        # Check if status length changed\n        #   - when all statuses are removed it is a changed\n        if self._orig_status_length != len(self._statuses):\n            return True\n        # Go through all statuses and check if any of them changed\n        for status in self._statuses:\n            if status.changed:\n                return True\n        return False\n\n    def get(self, name, default=None):\n        \"\"\"Get status by name.\n\n        Args:\n            name (str): Status name.\n            default (Any): Default value of status is not found.\n\n        Returns:\n            Union[ProjectStatus, Any]: Status or default value.\n        \"\"\"\n\n        return next(\n            (\n                status\n                for status in self._statuses\n                if status.name == name\n            ),\n            default\n        )\n\n    get_status_by_name = get\n\n    def index(self, status, **kwargs):\n        \"\"\"Get status index.\n\n        Args:\n            status (ProjectStatus): Status to get index of.\n            default (Optional[Any]): Default value if status is not found.\n\n        Returns:\n            Union[int, Any]: Status index.\n\n        Raises:\n            ValueError: If status is not found and default value is not\n                defined.\n        \"\"\"\n\n        output = next(\n            (\n                idx\n                for idx, st in enumerate(self._statuses)\n                if st is status\n            ),\n            None\n        )\n        if output is not None:\n            return output\n\n        if \"default\" in kwargs:\n            return kwargs[\"default\"]\n        raise ValueError(\"Status '{}' not found\".format(status.name))\n\n    def get_status_by_slugified_name(self, name):\n        \"\"\"Get status by slugified name.\n\n        Args:\n            name (str): Status name. Is slugified before search.\n\n        Returns:\n            Union[ProjectStatus, None]: Status or None if not found.\n        \"\"\"\n\n        slugified_name = ProjectStatus.slugify_name(name)\n        return next(\n            (\n                status\n                for status in self._statuses\n                if status.slugified_name == slugified_name\n            ),\n            None\n        )\n\n    def remove_by_name(self, name, ignore_missing=False):\n        \"\"\"Remove status by name.\n\n        Args:\n            name (str): Status name.\n            ignore_missing (Optional[bool]): If True, no error is raised if\n                status is not found.\n\n        Returns:\n            ProjectStatus: Removed status.\n        \"\"\"\n\n        matching_status = self.get(name)\n        if matching_status is None:\n            if ignore_missing:\n                return\n            raise ValueError(\n                \"Status '{}' not found in project\".format(name))\n        return self.remove(matching_status)\n\n    def remove(self, status, ignore_missing=False):\n        \"\"\"Remove status.\n\n        Args:\n            status (ProjectStatus): Status to remove.\n            ignore_missing (Optional[bool]): If True, no error is raised if\n                status is not found.\n\n        Returns:\n            Union[ProjectStatus, None]: Removed status.\n        \"\"\"\n\n        index = self.index(status, default=None)\n        if index is None:\n            if ignore_missing:\n                return None\n            raise ValueError(\"Status '{}' not in project\".format(status))\n\n        return self.pop(index)\n\n    def pop(self, index):\n        \"\"\"Remove status by index.\n\n        Args:\n            index (int): Status index.\n\n        Returns:\n            ProjectStatus: Removed status.\n        \"\"\"\n\n        status = self._statuses.pop(index)\n        status.unset_project_statuses(self)\n        for st in self._statuses[index:]:\n            st.set_index(st.index - 1, from_parent=True)\n        return status\n\n    def insert(self, index, status):\n        \"\"\"Insert status at index.\n\n        Args:\n            index (int): Status index.\n            status (Union[ProjectStatus, dict[str, str]]): Status to insert.\n                Can be either status object or status data.\n\n        Returns:\n            ProjectStatus: Inserted status.\n        \"\"\"\n\n        if not isinstance(status, ProjectStatus):\n            status = ProjectStatus.from_data(status)\n\n        start_index = index\n        end_index = len(self._statuses) + 1\n        matching_index = self.index(status, default=None)\n        if matching_index is not None:\n            if matching_index == index:\n                status.set_index(index, from_parent=True)\n                return\n\n            self._statuses.pop(matching_index)\n            if matching_index < index:\n                start_index = matching_index\n                end_index = index + 1\n            else:\n                end_index -= 1\n\n        status.set_project_statuses(self)\n        self._statuses.insert(index, status)\n        for idx, st in enumerate(self._statuses[start_index:end_index]):\n            st.set_index(start_index + idx, from_parent=True)\n        return status\n\n    def append(self, status):\n        \"\"\"Add new status to the end of the list.\n\n        Args:\n            status (Union[ProjectStatus, dict[str, str]]): Status to insert.\n                Can be either status object or status data.\n\n        Returns:\n            ProjectStatus: Inserted status.\n        \"\"\"\n\n        return self.insert(len(self._statuses), status)\n\n    def set_status_index(self, status, index):\n        \"\"\"Set status index.\n\n        Args:\n            status (ProjectStatus): Status to set index.\n            index (int): New status index.\n        \"\"\"\n\n        return self.insert(index, status)\n\n\nclass ProjectEntity(BaseEntity):\n    \"\"\"Entity representing project on AYON server.\n\n    Args:\n        project_code (str): Project code.\n        library (bool): Is project library project.\n        folder_types (list[dict[str, Any]]): Folder types definition.\n        task_types (list[dict[str, Any]]): Task types definition.\n        entity_id (Optional[str]): Id of the entity. New id is created if\n            not passed.\n        parent_id (Union[str, None]): Id of parent entity.\n        name (str): Name of entity.\n        attribs (Dict[str, Any]): Attribute values.\n        data (Dict[str, Any]): Entity data (custom data).\n        thumbnail_id (Union[str, None]): Id of entity's thumbnail.\n        active (bool): Is entity active.\n        entity_hub (EntityHub): Object of entity hub which created object of\n            the entity.\n        created (Optional[bool]): Entity is new. When 'None' is passed the\n            value is defined based on value of 'entity_id'.\n    \"\"\"\n\n    entity_type = \"project\"\n    parent_entity_types = []\n    # TODO These are hardcoded but maybe should be used from server???\n    default_folder_type_icon = \"folder\"\n    default_task_type_icon = \"task_alt\"\n\n    def __init__(\n        self,\n        project_code,\n        library,\n        folder_types,\n        task_types,\n        statuses,\n        *args,\n        **kwargs\n    ):\n        super(ProjectEntity, self).__init__(*args, **kwargs)\n\n        self._project_code = project_code\n        self._library_project = library\n        self._folder_types = folder_types\n        self._task_types = task_types\n        self._statuses_obj = _ProjectStatuses(statuses)\n\n        self._orig_project_code = project_code\n        self._orig_library_project = library\n        self._orig_folder_types = copy.deepcopy(folder_types)\n        self._orig_task_types = copy.deepcopy(task_types)\n        self._orig_statuses = copy.deepcopy(statuses)\n\n    def _prepare_entity_id(self, entity_id):\n        if entity_id != self.project_name:\n            raise ValueError(\n                \"Unexpected entity id value \\\"{}\\\". Expected \\\"{}\\\"\".format(\n                    entity_id, self.project_name))\n        return entity_id\n\n    def get_parent(self, *args, **kwargs):\n        return None\n\n    def set_parent(self, parent):\n        raise ValueError(\n            \"Parent of project cannot be set to {}\".format(parent)\n        )\n\n    parent = property(get_parent, set_parent)\n\n    def get_orig_folder_types(self):\n        return copy.deepcopy(self._orig_folder_types)\n\n    def get_folder_types(self):\n        return copy.deepcopy(self._folder_types)\n\n    def set_folder_types(self, folder_types):\n        new_folder_types = []\n        for folder_type in folder_types:\n            if \"icon\" not in folder_type:\n                folder_type[\"icon\"] = self.default_folder_type_icon\n            new_folder_types.append(folder_type)\n        self._folder_types = new_folder_types\n\n    def get_orig_task_types(self):\n        return copy.deepcopy(self._orig_task_types)\n\n    def get_task_types(self):\n        return copy.deepcopy(self._task_types)\n\n    def set_task_types(self, task_types):\n        new_task_types = []\n        for task_type in task_types:\n            if \"icon\" not in task_type:\n                task_type[\"icon\"] = self.default_task_type_icon\n            new_task_types.append(task_type)\n        self._task_types = new_task_types\n\n    def get_orig_statuses(self):\n        return copy.deepcopy(self._orig_statuses)\n\n    def get_statuses(self):\n        return self._statuses_obj\n\n    def set_statuses(self, statuses):\n        self._statuses_obj.set(statuses)\n\n    folder_types = property(get_folder_types, set_folder_types)\n    task_types = property(get_task_types, set_task_types)\n    statuses = property(get_statuses, set_statuses)\n\n    def lock(self):\n        super(ProjectEntity, self).lock()\n        self._orig_folder_types = copy.deepcopy(self._folder_types)\n        self._orig_task_types = copy.deepcopy(self._task_types)\n        self._statuses_obj.lock()\n\n    @property\n    def changes(self):\n        changes = self._get_default_changes()\n        if self._orig_folder_types != self._folder_types:\n            changes[\"folderTypes\"] = self.get_folder_types()\n\n        if self._orig_task_types != self._task_types:\n            changes[\"taskTypes\"] = self.get_task_types()\n\n        if self._statuses_obj.changed:\n            changes[\"statuses\"] = self._statuses_obj.to_data()\n\n        return changes\n\n    @classmethod\n    def from_entity_data(cls, project, entity_hub):\n        return cls(\n            project[\"code\"],\n            parent_id=PROJECT_PARENT_ID,\n            entity_id=project[\"name\"],\n            library=project[\"library\"],\n            folder_types=project[\"folderTypes\"],\n            task_types=project[\"taskTypes\"],\n            name=project[\"name\"],\n            attribs=project[\"ownAttrib\"],\n            data=project[\"data\"],\n            active=project[\"active\"],\n            entity_hub=entity_hub\n        )\n\n    def to_create_body_data(self):\n        raise NotImplementedError(\n            \"ProjectEntity does not support conversion to entity data\"\n        )\n\n\nclass FolderEntity(BaseEntity):\n    \"\"\"Entity representing a folder on AYON server.\n\n    Args:\n        folder_type (str): Type of folder. Folder type must be available in\n            config of project folder types.\n        entity_id (Union[str, None]): Id of the entity. New id is created if\n            not passed.\n        parent_id (Union[str, None]): Id of parent entity.\n        name (str): Name of entity.\n        attribs (Dict[str, Any]): Attribute values.\n        data (Dict[str, Any]): Entity data (custom data).\n        thumbnail_id (Union[str, None]): Id of entity's thumbnail.\n        active (bool): Is entity active.\n        label (Optional[str]): Folder label.\n        path (Optional[str]): Folder path. Path consist of all parent names\n            with slash('/') used as separator.\n        entity_hub (EntityHub): Object of entity hub which created object of\n            the entity.\n        created (Optional[bool]): Entity is new. When 'None' is passed the\n            value is defined based on value of 'entity_id'.\n    \"\"\"\n\n    entity_type = \"folder\"\n    parent_entity_types = [\"folder\", \"project\"]\n\n    def __init__(self, folder_type, *args, label=None, path=None, **kwargs):\n        super(FolderEntity, self).__init__(*args, **kwargs)\n        # Autofill project as parent of folder if is not yet set\n        # - this can be guessed only if folder was just created\n        if self.created and self._parent_id is UNKNOWN_VALUE:\n            self._parent_id = self.project_name\n\n        self._folder_type = folder_type\n        self._label = label\n\n        self._orig_folder_type = folder_type\n        self._orig_label = label\n        # Know if folder has any products\n        # - is used to know if folder allows hierarchy changes\n        self._has_published_content = False\n        self._path = path\n\n    def get_folder_type(self):\n        return self._folder_type\n\n    def set_folder_type(self, folder_type):\n        self._folder_type = folder_type\n\n    folder_type = property(get_folder_type, set_folder_type)\n\n    def get_label(self):\n        return self._label\n\n    def set_label(self, label):\n        self._label = label\n\n    label = property(get_label, set_label)\n\n    def get_path(self, dynamic_value=True):\n        if not dynamic_value:\n            return self._path\n\n        if self._path is None:\n            parent = self.parent\n            if parent.entity_type == \"folder\":\n                parent_path = parent.path\n                path = \"/\".join([parent_path, self.name])\n            elif self._entity_hub.path_start_with_slash:\n                path = \"/{}\".format(self.name)\n            else:\n                path = self.name\n            self._path = path\n        return self._path\n\n    def reset_path(self):\n        self._path = None\n        self._entity_hub.folder_path_reseted(self.id)\n\n    path = property(get_path)\n\n    def get_has_published_content(self):\n        return self._has_published_content\n\n    def set_has_published_content(self, has_published_content):\n        if self._has_published_content is has_published_content:\n            return\n\n        self._has_published_content = has_published_content\n        # Reset immutable cache of parents\n        self._entity_hub.reset_immutable_for_hierarchy_cache(self.id)\n\n    has_published_content = property(\n        get_has_published_content, set_has_published_content\n    )\n\n    @property\n    def _immutable_for_hierarchy(self):\n        if self.has_published_content:\n            return True\n        return None\n\n    def lock(self):\n        super(FolderEntity, self).lock()\n        self._orig_folder_type = self._folder_type\n\n    @property\n    def changes(self):\n        changes = self._get_default_changes()\n\n        if self._orig_parent_id != self._parent_id:\n            parent_id = self._parent_id\n            if parent_id == self.project_name:\n                parent_id = None\n            changes[\"parentId\"] = parent_id\n\n        if self._orig_folder_type != self._folder_type:\n            changes[\"folderType\"] = self._folder_type\n\n        label = self._label\n        if self._name == label:\n            label = None\n\n        if label != self._orig_label:\n            changes[\"label\"] = label\n\n        return changes\n\n    @classmethod\n    def from_entity_data(cls, folder, entity_hub):\n        parent_id = folder[\"parentId\"]\n        if parent_id is None:\n            parent_id = entity_hub.project_entity.id\n        return cls(\n            folder[\"folderType\"],\n            label=folder[\"label\"],\n            path=folder[\"path\"],\n            entity_id=folder[\"id\"],\n            parent_id=parent_id,\n            name=folder[\"name\"],\n            data=folder.get(\"data\"),\n            attribs=folder[\"ownAttrib\"],\n            active=folder[\"active\"],\n            thumbnail_id=folder[\"thumbnailId\"],\n            created=False,\n            entity_hub=entity_hub\n        )\n\n    def to_create_body_data(self):\n        parent_id = self._parent_id\n        if parent_id is UNKNOWN_VALUE:\n            raise ValueError(\"Folder does not have set 'parent_id'\")\n\n        if parent_id == self.project_name:\n            parent_id = None\n\n        if not self.name or self.name is UNKNOWN_VALUE:\n            raise ValueError(\"Folder does not have set 'name'\")\n\n        output = {\n            \"name\": self.name,\n            \"folderType\": self.folder_type,\n            \"parentId\": parent_id,\n        }\n        attrib = self.attribs.to_dict()\n        if attrib:\n            output[\"attrib\"] = attrib\n\n        if self.active is not UNKNOWN_VALUE:\n            output[\"active\"] = self.active\n\n        if self.thumbnail_id is not UNKNOWN_VALUE:\n            output[\"thumbnailId\"] = self.thumbnail_id\n\n        if (\n            self._entity_hub.allow_data_changes\n            and self._data is not UNKNOWN_VALUE\n        ):\n            output[\"data\"] = self._data\n        return output\n\n\nclass TaskEntity(BaseEntity):\n    \"\"\"Entity representing a task on AYON server.\n\n    Args:\n        task_type (str): Type of task. Task type must be available in config\n            of project task types.\n        entity_id (Union[str, None]): Id of the entity. New id is created if\n            not passed.\n        parent_id (Union[str, None]): Id of parent entity.\n        name (str): Name of entity.\n        label (Optional[str]): Task label.\n        attribs (Dict[str, Any]): Attribute values.\n        data (Dict[str, Any]): Entity data (custom data).\n        thumbnail_id (Union[str, None]): Id of entity's thumbnail.\n        active (bool): Is entity active.\n        entity_hub (EntityHub): Object of entity hub which created object of\n            the entity.\n        created (Optional[bool]): Entity is new. When 'None' is passed the\n            value is defined based on value of 'entity_id'.\n    \"\"\"\n\n    entity_type = \"task\"\n    parent_entity_types = [\"folder\"]\n\n    def __init__(self, task_type, *args, label=None, **kwargs):\n        super(TaskEntity, self).__init__(*args, **kwargs)\n\n        self._task_type = task_type\n        self._label = label\n\n        self._orig_task_type = task_type\n        self._orig_label = label\n\n        self._children_ids = set()\n\n    def lock(self):\n        super(TaskEntity, self).lock()\n        self._orig_task_type = self._task_type\n\n    def get_task_type(self):\n        return self._task_type\n\n    def set_task_type(self, task_type):\n        self._task_type = task_type\n\n    task_type = property(get_task_type, set_task_type)\n\n    def get_label(self):\n        return self._label\n\n    def set_label(self, label):\n        self._label = label\n\n    label = property(get_label, set_label)\n\n    def add_child(self, child):\n        raise ValueError(\"Task does not support to add children\")\n\n    @property\n    def changes(self):\n        changes = self._get_default_changes()\n\n        if self._orig_parent_id != self._parent_id:\n            changes[\"folderId\"] = self._parent_id\n\n        if self._orig_task_type != self._task_type:\n            changes[\"taskType\"] = self._task_type\n\n        label = self._label\n        if self._name == label:\n            label = None\n\n        if label != self._orig_label:\n            changes[\"label\"] = label\n\n        return changes\n\n    @classmethod\n    def from_entity_data(cls, task, entity_hub):\n        return cls(\n            task[\"taskType\"],\n            entity_id=task[\"id\"],\n            label=task[\"label\"],\n            parent_id=task[\"folderId\"],\n            name=task[\"name\"],\n            data=task.get(\"data\"),\n            attribs=task[\"ownAttrib\"],\n            active=task[\"active\"],\n            created=False,\n            entity_hub=entity_hub\n        )\n\n    def to_create_body_data(self):\n        if self.parent_id is UNKNOWN_VALUE:\n            raise ValueError(\"Task does not have set 'parent_id'\")\n\n        output = {\n            \"name\": self.name,\n            \"taskType\": self.task_type,\n            \"folderId\": self.parent_id,\n            \"attrib\": self.attribs.to_dict(),\n        }\n        attrib = self.attribs.to_dict()\n        if attrib:\n            output[\"attrib\"] = attrib\n\n        if self.active is not UNKNOWN_VALUE:\n            output[\"active\"] = self.active\n\n        if (\n            self._entity_hub.allow_data_changes\n            and self._data is not UNKNOWN_VALUE\n        ):\n            output[\"data\"] = self._data\n        return output\n"
  },
  {
    "path": "openpype/vendor/python/common/ayon_api/events.py",
    "content": "import copy\n\n\nclass ServerEvent(object):\n    def __init__(\n        self,\n        topic,\n        sender=None,\n        event_hash=None,\n        project_name=None,\n        username=None,\n        dependencies=None,\n        description=None,\n        summary=None,\n        payload=None,\n        finished=True,\n        store=True,\n    ):\n        if dependencies is None:\n            dependencies = []\n        if payload is None:\n            payload = {}\n        if summary is None:\n            summary = {}\n\n        self.topic = topic\n        self.sender = sender\n        self.event_hash = event_hash\n        self.project_name = project_name\n        self.username = username\n        self.dependencies = dependencies\n        self.description = description\n        self.summary = summary\n        self.payload = payload\n        self.finished = finished\n        self.store = store\n\n    def to_data(self):\n        return {\n            \"topic\": self.topic,\n            \"sender\": self.sender,\n            \"hash\": self.event_hash,\n            \"project\": self.project_name,\n            \"user\": self.username,\n            \"dependencies\": copy.deepcopy(self.dependencies),\n            \"description\": self.description,\n            \"description\": self.description,\n            \"summary\": copy.deepcopy(self.summary),\n            \"payload\": self.payload,\n            \"finished\": self.finished,\n            \"store\": self.store\n        }\n"
  },
  {
    "path": "openpype/vendor/python/common/ayon_api/exceptions.py",
    "content": "import copy\n\n\nclass UrlError(Exception):\n    \"\"\"Url cannot be parsed as url.\n\n    Exception may contain hints of possible fixes of url that can be used in\n    UI if needed.\n    \"\"\"\n\n    def __init__(self, message, title, hints=None):\n        if hints is None:\n            hints = []\n\n        self.title = title\n        self.hints = hints\n        super(UrlError, self).__init__(message)\n\n\nclass ServerError(Exception):\n    pass\n\n\nclass UnauthorizedError(ServerError):\n    pass\n\n\nclass AuthenticationError(ServerError):\n    pass\n\n\nclass ServerNotReached(ServerError):\n    pass\n\n\nclass RequestError(Exception):\n    def __init__(self, message, response):\n        self.response = response\n        super(RequestError, self).__init__(message)\n\n\nclass HTTPRequestError(RequestError):\n    pass\n\n\nclass GraphQlQueryFailed(Exception):\n    def __init__(self, errors, query, variables):\n        if variables is None:\n            variables = {}\n\n        error_messages = []\n        for error in errors:\n            msg = error[\"message\"]\n            path = error.get(\"path\")\n            if path:\n                msg += \" on item '{}'\".format(\"/\".join(path))\n            locations = error.get(\"locations\")\n            if locations:\n                _locations = [\n                    \"Line {} Column {}\".format(\n                        location[\"line\"], location[\"column\"]\n                    )\n                    for location in locations\n                ]\n\n                msg += \" ({})\".format(\" and \".join(_locations))\n            error_messages.append(msg)\n\n        message = \"GraphQl query Failed\"\n        if error_messages:\n            message = \"{}: {}\".format(message, \" | \".join(error_messages))\n\n        self.errors = errors\n        self.query = query\n        self.variables = copy.deepcopy(variables)\n        super(GraphQlQueryFailed, self).__init__(message)\n\n\nclass MissingEntityError(Exception):\n    pass\n\n\nclass ProjectNotFound(MissingEntityError):\n    def __init__(self, project_name, message=None):\n        if not message:\n            message = \"Project \\\"{}\\\" was not found\".format(project_name)\n        self.project_name = project_name\n        super(ProjectNotFound, self).__init__(message)\n\n\nclass FolderNotFound(MissingEntityError):\n    def __init__(self, project_name, folder_id, message=None):\n        self.project_name = project_name\n        self.folder_id = folder_id\n        if not message:\n            message = (\n                \"Folder with id \\\"{}\\\" was not found in project \\\"{}\\\"\"\n            ).format(folder_id, project_name)\n        super(FolderNotFound, self).__init__(message)\n\n\nclass FailedOperations(Exception):\n    pass\n\n\nclass FailedServiceInit(Exception):\n    pass\n"
  },
  {
    "path": "openpype/vendor/python/common/ayon_api/graphql.py",
    "content": "import copy\nimport numbers\nfrom abc import ABCMeta, abstractmethod\n\nimport six\n\nfrom .exceptions import GraphQlQueryFailed\n\nFIELD_VALUE = object()\n\n\ndef fields_to_dict(fields):\n    if not fields:\n        return None\n\n    output = {}\n    for field in fields:\n        hierarchy = field.split(\".\")\n        last = hierarchy.pop(-1)\n        value = output\n        for part in hierarchy:\n            if value is FIELD_VALUE:\n                break\n\n            if part not in value:\n                value[part] = {}\n            value = value[part]\n\n        if value is not FIELD_VALUE:\n            value[last] = FIELD_VALUE\n    return output\n\n\nclass QueryVariable(object):\n    \"\"\"Object representing single varible used in GraphQlQuery.\n\n    Variable definition is in GraphQl query header but it's value is used\n    in fields.\n\n    Args:\n        variable_name (str): Name of variable in query.\n    \"\"\"\n\n    def __init__(self, variable_name):\n        self._variable_name = variable_name\n        self._name = \"${}\".format(variable_name)\n\n    @property\n    def name(self):\n        \"\"\"Name used in field filter.\"\"\"\n\n        return self._name\n\n    @property\n    def variable_name(self):\n        \"\"\"Name of variable in query definition.\"\"\"\n\n        return self._variable_name\n\n    def __hash__(self):\n        return self._name.__hash__()\n\n    def __str__(self):\n        return self._name\n\n    def __format__(self, *args, **kwargs):\n        return self._name.__format__(*args, **kwargs)\n\n\nclass GraphQlQuery:\n    \"\"\"GraphQl query which can have fields to query.\n\n    Single use object which can be used only for one query. Object and children\n    objects keep track about paging and progress.\n\n    Args:\n        name (str): Name of query.\n    \"\"\"\n\n    offset = 2\n\n    def __init__(self, name):\n        self._name = name\n        self._variables = {}\n        self._children = []\n        self._has_multiple_edge_fields = None\n\n    @property\n    def indent(self):\n        \"\"\"Indentation for preparation of query string.\n\n        Returns:\n            int: Ident spaces.\n        \"\"\"\n\n        return 0\n\n    @property\n    def child_indent(self):\n        \"\"\"Indentation for preparation of query string used by children.\n\n        Returns:\n            int: Ident spaces for children.\n        \"\"\"\n\n        return self.indent\n\n    @property\n    def need_query(self):\n        \"\"\"Still need query from server.\n\n        Needed for edges which use pagination.\n\n        Returns:\n            bool: If still need query from server.\n        \"\"\"\n\n        for child in self._children:\n            if child.need_query:\n                return True\n        return False\n\n    @property\n    def has_multiple_edge_fields(self):\n        if self._has_multiple_edge_fields is None:\n            edge_counter = 0\n            for child in self._children:\n                edge_counter += child.sum_edge_fields(2)\n                if edge_counter > 1:\n                    break\n            self._has_multiple_edge_fields = edge_counter > 1\n\n        return self._has_multiple_edge_fields\n\n    def add_variable(self, key, value_type, value=None):\n        \"\"\"Add variable to query.\n\n        Args:\n            key (str): Variable name.\n            value_type (str): Type of expected value in variables. This is\n                graphql type e.g. \"[String!]\", \"Int\", \"Boolean\", etc.\n            value (Any): Default value for variable. Can be changed later.\n\n        Returns:\n            QueryVariable: Created variable object.\n\n        Raises:\n            KeyError: If variable was already added before.\n        \"\"\"\n\n        if key in self._variables:\n            raise KeyError(\n                \"Variable \\\"{}\\\" was already set with type {}.\".format(\n                    key, value_type\n                )\n            )\n\n        variable = QueryVariable(key)\n        self._variables[key] = {\n            \"type\": value_type,\n            \"variable\": variable,\n            \"value\": value\n        }\n        return variable\n\n    def get_variable(self, key):\n        \"\"\"Variable object.\n\n        Args:\n            key (str): Variable name added to headers.\n\n        Returns:\n            QueryVariable: Variable object used in query string.\n        \"\"\"\n\n        return self._variables[key][\"variable\"]\n\n    def get_variable_value(self, key, default=None):\n        \"\"\"Get Current value of variable.\n\n        Args:\n            key (str): Variable name.\n            default (Any): Default value if variable is available.\n\n        Returns:\n            Any: Variable value.\n        \"\"\"\n\n        variable_item = self._variables.get(key)\n        if variable_item:\n            return variable_item[\"value\"]\n        return default\n\n    def set_variable_value(self, key, value):\n        \"\"\"Set value for variable.\n\n        Args:\n            key (str): Variable name under which the value is stored.\n            value (Any): Variable value used in query. Variable is not used\n                if value is 'None'.\n        \"\"\"\n\n        self._variables[key][\"value\"] = value\n\n    def get_variable_keys(self):\n        \"\"\"Get all variable keys.\n\n        Returns:\n            set[str]: Variable keys.\n        \"\"\"\n\n        return set(self._variables.keys())\n\n    def get_variables_values(self):\n        \"\"\"Calculate variable values used that should be used in query.\n\n        Variables with value set to 'None' are skipped.\n\n        Returns:\n            Dict[str, Any]: Variable values by their name.\n        \"\"\"\n\n        output = {}\n        for key, item in self._variables.items():\n            value = item[\"value\"]\n            if value is not None:\n                output[key] = item[\"value\"]\n\n        return output\n\n    def add_obj_field(self, field):\n        \"\"\"Add field object to children.\n\n        Args:\n            field (BaseGraphQlQueryField): Add field to query children.\n        \"\"\"\n\n        if field in self._children:\n            return\n\n        self._children.append(field)\n        field.set_parent(self)\n\n    def add_field_with_edges(self, name):\n        \"\"\"Add field with edges to query.\n\n        Args:\n            name (str): Field name e.g. 'tasks'.\n\n        Returns:\n            GraphQlQueryEdgeField: Created field object.\n        \"\"\"\n\n        item = GraphQlQueryEdgeField(name, self)\n        self.add_obj_field(item)\n        return item\n\n    def add_field(self, name):\n        \"\"\"Add field to query.\n\n        Args:\n            name (str): Field name e.g. 'id'.\n\n        Returns:\n            GraphQlQueryField: Created field object.\n        \"\"\"\n\n        item = GraphQlQueryField(name, self)\n        self.add_obj_field(item)\n        return item\n\n    def calculate_query(self):\n        \"\"\"Calculate query string which is sent to server.\n\n        Returns:\n            str: GraphQl string with variables and headers.\n\n        Raises:\n            ValueError: Query has no fiels.\n        \"\"\"\n\n        if not self._children:\n            raise ValueError(\"Missing fields to query\")\n\n        variables = []\n        for item in self._variables.values():\n            if item[\"value\"] is None:\n                continue\n\n            variables.append(\n                \"{}: {}\".format(item[\"variable\"], item[\"type\"])\n            )\n\n        variables_str = \"\"\n        if variables:\n            variables_str = \"({})\".format(\",\".join(variables))\n        header = \"query {}{}\".format(self._name, variables_str)\n\n        output = []\n        output.append(header + \" {\")\n        for field in self._children:\n            output.append(field.calculate_query())\n        output.append(\"}\")\n\n        return \"\\n\".join(output)\n\n    def parse_result(self, data, output, progress_data):\n        \"\"\"Parse data from response for output.\n\n        Output is stored to passed 'output' variable. That's because of paging\n        during which objects must have access to both new and previous values.\n\n        Args:\n            data (Dict[str, Any]): Data received using calculated query.\n            output (Dict[str, Any]): Where parsed data are stored.\n        \"\"\"\n\n        if not data:\n            return\n\n        for child in self._children:\n            child.parse_result(data, output, progress_data)\n\n    def query(self, con):\n        \"\"\"Do a query from server.\n\n        Args:\n            con (ServerAPI): Connection to server with 'query' method.\n\n        Returns:\n            Dict[str, Any]: Parsed output from GraphQl query.\n        \"\"\"\n\n        progress_data = {}\n        output = {}\n        while self.need_query:\n            query_str = self.calculate_query()\n            variables = self.get_variables_values()\n            response = con.query_graphql(\n                query_str,\n                self.get_variables_values()\n            )\n            if response.errors:\n                raise GraphQlQueryFailed(response.errors, query_str, variables)\n            self.parse_result(response.data[\"data\"], output, progress_data)\n\n        return output\n\n    def continuous_query(self, con):\n        \"\"\"Do a query from server.\n\n        Args:\n            con (ServerAPI): Connection to server with 'query' method.\n\n        Returns:\n            Dict[str, Any]: Parsed output from GraphQl query.\n        \"\"\"\n\n        progress_data = {}\n        if self.has_multiple_edge_fields:\n            output = {}\n            while self.need_query:\n                query_str = self.calculate_query()\n                variables = self.get_variables_values()\n                response = con.query_graphql(query_str, variables)\n                if response.errors:\n                    raise GraphQlQueryFailed(\n                        response.errors, query_str, variables\n                    )\n                self.parse_result(response.data[\"data\"], output, progress_data)\n\n            yield output\n\n        else:\n            while self.need_query:\n                output = {}\n                query_str = self.calculate_query()\n                variables = self.get_variables_values()\n                response = con.query_graphql(query_str, variables)\n                if response.errors:\n                    raise GraphQlQueryFailed(\n                        response.errors, query_str, variables\n                    )\n\n                self.parse_result(response.data[\"data\"], output, progress_data)\n\n                yield output\n\n\n@six.add_metaclass(ABCMeta)\nclass BaseGraphQlQueryField(object):\n    \"\"\"Field in GraphQl query.\n\n    Args:\n        name (str): Name of field.\n        parent (Union[BaseGraphQlQueryField, GraphQlQuery]): Parent object of a\n            field.\n    \"\"\"\n\n    def __init__(self, name, parent):\n        if isinstance(parent, GraphQlQuery):\n            query_item = parent\n        else:\n            query_item = parent.query_item\n\n        self._name = name\n        self._parent = parent\n\n        self._filters = {}\n\n        self._children = []\n        # Value is changed on first parse of result\n        self._need_query = True\n\n        self._query_item = query_item\n\n        self._path = None\n\n    def __repr__(self):\n        return \"<{} {}>\".format(self.__class__.__name__, self.path)\n\n    def add_variable(self, key, value_type, value=None):\n        \"\"\"Add variable to query.\n\n        Args:\n            key (str): Variable name.\n            value_type (str): Type of expected value in variables. This is\n                graphql type e.g. \"[String!]\", \"Int\", \"Boolean\", etc.\n            value (Any): Default value for variable. Can be changed later.\n\n        Returns:\n            QueryVariable: Created variable object.\n\n        Raises:\n            KeyError: If variable was already added before.\n        \"\"\"\n\n        return self._parent.add_variable(key, value_type, value)\n\n    def get_variable(self, key):\n        \"\"\"Variable object.\n\n        Args:\n            key (str): Variable name added to headers.\n\n        Returns:\n            QueryVariable: Variable object used in query string.\n        \"\"\"\n\n        return self._parent.get_variable(key)\n\n    @property\n    def need_query(self):\n        \"\"\"Still need query from server.\n\n        Needed for edges which use pagination. Look into children values too.\n\n        Returns:\n            bool: If still need query from server.\n        \"\"\"\n\n        if self._need_query:\n            return True\n\n        for child in self._children_iter():\n            if child.need_query:\n                return True\n        return False\n\n    def _children_iter(self):\n        \"\"\"Iterate over all children fields of object.\n\n        Returns:\n            Iterator[BaseGraphQlQueryField]: Children fields.\n        \"\"\"\n\n        for child in self._children:\n            yield child\n\n    def sum_edge_fields(self, max_limit=None):\n        \"\"\"Check how many edge fields query has.\n\n        In case there are multiple edge fields or are nested the query can't\n        yield mid cursor results.\n\n        Args:\n            max_limit (int): Skip rest of counting if counter is bigger then\n                entered number.\n\n        Returns:\n            int: Counter edge fields\n        \"\"\"\n\n        counter = 0\n        if isinstance(self, GraphQlQueryEdgeField):\n            counter = 1\n\n        for child in self._children_iter():\n            counter += child.sum_edge_fields(max_limit)\n            if max_limit is not None and counter >= max_limit:\n                break\n        return counter\n\n    @property\n    def offset(self):\n        return self._query_item.offset\n\n    @property\n    def indent(self):\n        return self._parent.child_indent + self.offset\n\n    @property\n    @abstractmethod\n    def child_indent(self):\n        pass\n\n    @property\n    def query_item(self):\n        return self._query_item\n\n    @property\n    @abstractmethod\n    def has_edges(self):\n        pass\n\n    @property\n    def child_has_edges(self):\n        for child in self._children_iter():\n            if child.has_edges or child.child_has_edges:\n                return True\n        return False\n\n    @property\n    def path(self):\n        \"\"\"Field path for debugging purposes.\n\n        Returns:\n            str: Field path in query.\n        \"\"\"\n\n        if self._path is None:\n            if isinstance(self._parent, GraphQlQuery):\n                path = self._name\n            else:\n                path = \"/\".join((self._parent.path, self._name))\n            self._path = path\n        return self._path\n\n    def reset_cursor(self):\n        for child in self._children_iter():\n            child.reset_cursor()\n\n    def get_variable_value(self, *args, **kwargs):\n        return self._query_item.get_variable_value(*args, **kwargs)\n\n    def set_variable_value(self, *args, **kwargs):\n        return self._query_item.set_variable_value(*args, **kwargs)\n\n    def set_filter(self, key, value):\n        self._filters[key] = value\n\n    def has_filter(self, key):\n        return key in self._filters\n\n    def remove_filter(self, key):\n        self._filters.pop(key, None)\n\n    def set_parent(self, parent):\n        if self._parent is parent:\n            return\n        self._parent = parent\n        parent.add_obj_field(self)\n\n    def add_obj_field(self, field):\n        if field in self._children:\n            return\n\n        self._children.append(field)\n        field.set_parent(self)\n\n    def add_field_with_edges(self, name):\n        item = GraphQlQueryEdgeField(name, self)\n        self.add_obj_field(item)\n        return item\n\n    def add_field(self, name):\n        item = GraphQlQueryField(name, self)\n        self.add_obj_field(item)\n        return item\n\n    def _filter_value_to_str(self, value):\n        if isinstance(value, QueryVariable):\n            if self.get_variable_value(value.variable_name) is None:\n                return None\n            return str(value)\n\n        if isinstance(value, numbers.Number):\n            return str(value)\n\n        if isinstance(value, six.string_types):\n            return '\"{}\"'.format(value)\n\n        if isinstance(value, (list, set, tuple)):\n            return \"[{}]\".format(\n                \", \".join(\n                    self._filter_value_to_str(item)\n                    for item in iter(value)\n                )\n            )\n        raise TypeError(\n            \"Unknown type to convert '{}'\".format(str(type(value)))\n        )\n\n    def get_filters(self):\n        \"\"\"Receive filters for item.\n\n        By default just use copy of set filters.\n\n        Returns:\n            Dict[str, Any]: Fields filters.\n        \"\"\"\n\n        return copy.deepcopy(self._filters)\n\n    def _filters_to_string(self):\n        filters = self.get_filters()\n        if not filters:\n            return \"\"\n\n        filter_items = []\n        for key, value in filters.items():\n            string_value = self._filter_value_to_str(value)\n            if string_value is None:\n                continue\n\n            filter_items.append(\"{}: {}\".format(key, string_value))\n\n        if not filter_items:\n            return \"\"\n        return \"({})\".format(\", \".join(filter_items))\n\n    def _fake_children_parse(self):\n        \"\"\"Mark children as they don't need query.\"\"\"\n\n        for child in self._children_iter():\n            child.parse_result({}, {}, {})\n\n    @abstractmethod\n    def calculate_query(self):\n        pass\n\n    @abstractmethod\n    def parse_result(self, data, output, progress_data):\n        pass\n\n\nclass GraphQlQueryField(BaseGraphQlQueryField):\n    has_edges = False\n\n    @property\n    def child_indent(self):\n        return self.indent\n\n    def parse_result(self, data, output, progress_data):\n        if not isinstance(data, dict):\n            raise TypeError(\"{} Expected 'dict' type got '{}'\".format(\n                self._name, str(type(data))\n            ))\n\n        self._need_query = False\n        value = data.get(self._name)\n        if value is None:\n            self._fake_children_parse()\n            if self._name in data:\n                output[self._name] = None\n            return\n\n        if not self._children:\n            output[self._name] = value\n            return\n\n        output_value = output.get(self._name)\n        if isinstance(value, dict):\n            if output_value is None:\n                output_value = {}\n                output[self._name] = output_value\n\n            for child in self._children:\n                child.parse_result(value, output_value, progress_data)\n            return\n\n        if output_value is None:\n            output_value = []\n            output[self._name] = output_value\n\n        if not value:\n            self._fake_children_parse()\n            return\n\n        diff = len(value) - len(output_value)\n        if diff > 0:\n            for _ in range(diff):\n                output_value.append({})\n\n        for idx, item in enumerate(value):\n            item_value = output_value[idx]\n            for child in self._children:\n                child.parse_result(item, item_value, progress_data)\n\n    def calculate_query(self):\n        offset = self.indent * \" \"\n        header = \"{}{}{}\".format(\n            offset,\n            self._name,\n            self._filters_to_string()\n        )\n        if not self._children:\n            return header\n\n        output = []\n        output.append(header + \" {\")\n\n        output.extend([\n            field.calculate_query()\n            for field in self._children\n        ])\n        output.append(offset + \"}\")\n\n        return \"\\n\".join(output)\n\n\nclass GraphQlQueryEdgeField(BaseGraphQlQueryField):\n    has_edges = True\n\n    def __init__(self, *args, **kwargs):\n        super(GraphQlQueryEdgeField, self).__init__(*args, **kwargs)\n        self._cursor = None\n        self._edge_children = []\n\n    @property\n    def child_indent(self):\n        offset = self.offset * 2\n        return self.indent + offset\n\n    def _children_iter(self):\n        for child in super(GraphQlQueryEdgeField, self)._children_iter():\n            yield child\n\n        for child in self._edge_children:\n            yield child\n\n    def add_obj_field(self, field):\n        if field in self._edge_children:\n            return\n\n        super(GraphQlQueryEdgeField, self).add_obj_field(field)\n\n    def add_obj_edge_field(self, field):\n        if field in self._edge_children or field in self._children:\n            return\n\n        self._edge_children.append(field)\n        field.set_parent(self)\n\n    def add_edge_field(self, name):\n        item = GraphQlQueryField(name, self)\n        self.add_obj_edge_field(item)\n        return item\n\n    def reset_cursor(self):\n        # Reset cursor only for edges\n        self._cursor = None\n        self._need_query = True\n\n        super(GraphQlQueryEdgeField, self).reset_cursor()\n\n    def parse_result(self, data, output, progress_data):\n        if not isinstance(data, dict):\n            raise TypeError(\"{} Expected 'dict' type got '{}'\".format(\n                self._name, str(type(data))\n            ))\n\n        value = data.get(self._name)\n        if value is None:\n            self._fake_children_parse()\n            self._need_query = False\n            return\n\n        if self._name in output:\n            node_values = output[self._name]\n        else:\n            node_values = []\n            output[self._name] = node_values\n\n        handle_cursors = self.child_has_edges\n        if handle_cursors:\n            cursor_key = self._get_cursor_key()\n            if cursor_key in progress_data:\n                nodes_by_cursor = progress_data[cursor_key]\n            else:\n                nodes_by_cursor = {}\n                progress_data[cursor_key] = nodes_by_cursor\n\n        page_info = value[\"pageInfo\"]\n        new_cursor = page_info[\"endCursor\"]\n        self._need_query = page_info[\"hasNextPage\"]\n        edges = value[\"edges\"]\n        # Fake result parse\n        if not edges:\n            self._fake_children_parse()\n\n        for edge in edges:\n            if not handle_cursors:\n                edge_value = {}\n                node_values.append(edge_value)\n            else:\n                edge_cursor = edge[\"cursor\"]\n                edge_value = nodes_by_cursor.get(edge_cursor)\n                if edge_value is None:\n                    edge_value = {}\n                    nodes_by_cursor[edge_cursor] = edge_value\n                    node_values.append(edge_value)\n\n            for child in self._edge_children:\n                child.parse_result(edge, edge_value, progress_data)\n\n            for child in self._children:\n                child.parse_result(edge[\"node\"], edge_value, progress_data)\n\n        if not self._need_query:\n            return\n\n        change_cursor = True\n        for child in self._children_iter():\n            if child.need_query:\n                change_cursor = False\n\n        if change_cursor:\n            for child in self._children_iter():\n                child.reset_cursor()\n            self._cursor = new_cursor\n\n    def _get_cursor_key(self):\n        return \"{}/__cursor__\".format(self.path)\n\n    def get_filters(self):\n        filters = super(GraphQlQueryEdgeField, self).get_filters()\n\n        filters[\"first\"] = 300\n        if self._cursor:\n            filters[\"after\"] = self._cursor\n        return filters\n\n    def calculate_query(self):\n        if not self._children and not self._edge_children:\n            raise ValueError(\"Missing child definitions for edges {}\".format(\n                self.path\n            ))\n\n        offset = self.indent * \" \"\n        header = \"{}{}{}\".format(\n            offset,\n            self._name,\n            self._filters_to_string()\n        )\n\n        output = []\n        output.append(header + \" {\")\n\n        edges_offset = offset + self.offset * \" \"\n        node_offset = edges_offset + self.offset * \" \"\n        output.append(edges_offset + \"edges {\")\n        for field in self._edge_children:\n            output.append(field.calculate_query())\n\n        if self._children:\n            output.append(node_offset + \"node {\")\n\n            for field in self._children:\n                output.append(\n                    field.calculate_query()\n                )\n\n            output.append(node_offset + \"}\")\n            if self.child_has_edges:\n                output.append(node_offset + \"cursor\")\n\n        output.append(edges_offset + \"}\")\n\n        # Add page information\n        output.append(edges_offset + \"pageInfo {\")\n        for page_key in (\n            \"endCursor\",\n            \"hasNextPage\",\n        ):\n            output.append(node_offset + page_key)\n        output.append(edges_offset + \"}\")\n        output.append(offset + \"}\")\n\n        return \"\\n\".join(output)\n\n\nINTROSPECTION_QUERY = \"\"\"\n  query IntrospectionQuery {\n    __schema {\n      queryType { name }\n      mutationType { name }\n      subscriptionType { name }\n      types {\n        ...FullType\n      }\n      directives {\n        name\n        description\n        locations\n        args {\n          ...InputValue\n        }\n      }\n    }\n  }\n  fragment FullType on __Type {\n    kind\n    name\n    description\n    fields(includeDeprecated: true) {\n      name\n      description\n      args {\n        ...InputValue\n      }\n      type {\n        ...TypeRef\n      }\n      isDeprecated\n      deprecationReason\n    }\n    inputFields {\n      ...InputValue\n    }\n    interfaces {\n      ...TypeRef\n    }\n    enumValues(includeDeprecated: true) {\n      name\n      description\n      isDeprecated\n      deprecationReason\n    }\n    possibleTypes {\n      ...TypeRef\n    }\n  }\n  fragment InputValue on __InputValue {\n    name\n    description\n    type { ...TypeRef }\n    defaultValue\n  }\n  fragment TypeRef on __Type {\n    kind\n    name\n    ofType {\n      kind\n      name\n      ofType {\n        kind\n        name\n        ofType {\n          kind\n          name\n          ofType {\n            kind\n            name\n            ofType {\n              kind\n              name\n              ofType {\n                kind\n                name\n                ofType {\n                  kind\n                  name\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/common/ayon_api/graphql_queries.py",
    "content": "import collections\n\nfrom .graphql import FIELD_VALUE, GraphQlQuery\n\n\ndef fields_to_dict(fields):\n    if not fields:\n        return None\n\n    output = {}\n    for field in fields:\n        hierarchy = field.split(\".\")\n        last = hierarchy.pop(-1)\n        value = output\n        for part in hierarchy:\n            if value is FIELD_VALUE:\n                break\n\n            if part not in value:\n                value[part] = {}\n            value = value[part]\n\n        if value is not FIELD_VALUE:\n            value[last] = FIELD_VALUE\n    return output\n\n\ndef add_links_fields(entity_field, nested_fields):\n    if \"links\" not in nested_fields:\n        return\n    links_fields = nested_fields.pop(\"links\")\n\n    link_edge_fields = {\n        \"id\",\n        \"linkType\",\n        \"projectName\",\n        \"entityType\",\n        \"entityId\",\n        \"direction\",\n        \"description\",\n        \"author\",\n    }\n    if isinstance(links_fields, dict):\n        simple_fields = set(links_fields)\n        simple_variant = len(simple_fields - link_edge_fields) == 0\n    else:\n        simple_variant = True\n        simple_fields = link_edge_fields\n\n    link_field = entity_field.add_field_with_edges(\"links\")\n\n    link_type_var = link_field.add_variable(\"linkTypes\", \"[String!]\")\n    link_dir_var = link_field.add_variable(\"linkDirection\", \"String!\")\n    link_field.set_filter(\"linkTypes\", link_type_var)\n    link_field.set_filter(\"direction\", link_dir_var)\n\n    if simple_variant:\n        for key in simple_fields:\n            link_field.add_edge_field(key)\n        return\n\n    query_queue = collections.deque()\n    for key, value in links_fields.items():\n        if key in link_edge_fields:\n            link_field.add_edge_field(key)\n            continue\n        query_queue.append((key, value, link_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n\n\ndef project_graphql_query(fields):\n    query = GraphQlQuery(\"ProjectQuery\")\n    project_name_var = query.add_variable(\"projectName\", \"String!\")\n    project_field = query.add_field(\"project\")\n    project_field.set_filter(\"name\", project_name_var)\n\n    nested_fields = fields_to_dict(fields)\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, project_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n\n\ndef projects_graphql_query(fields):\n    query = GraphQlQuery(\"ProjectsQuery\")\n    projects_field = query.add_field_with_edges(\"projects\")\n\n    nested_fields = fields_to_dict(fields)\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, projects_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n\n\ndef product_types_query(fields):\n    query = GraphQlQuery(\"ProductTypes\")\n    product_types_field = query.add_field(\"productTypes\")\n\n    nested_fields = fields_to_dict(fields)\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, product_types_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n\n\ndef project_product_types_query(fields):\n    query = GraphQlQuery(\"ProjectProductTypes\")\n    project_query = query.add_field(\"project\")\n    project_name_var = query.add_variable(\"projectName\", \"String!\")\n    project_query.set_filter(\"name\", project_name_var)\n    product_types_field = project_query.add_field(\"productTypes\")\n    nested_fields = fields_to_dict(fields)\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, product_types_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n\n\ndef folders_graphql_query(fields):\n    query = GraphQlQuery(\"FoldersQuery\")\n    project_name_var = query.add_variable(\"projectName\", \"String!\")\n    folder_ids_var = query.add_variable(\"folderIds\", \"[String!]\")\n    parent_folder_ids_var = query.add_variable(\"parentFolderIds\", \"[String!]\")\n    folder_paths_var = query.add_variable(\"folderPaths\", \"[String!]\")\n    folder_path_regex_var = query.add_variable(\"folderPathRegex\", \"String!\")\n    folder_names_var = query.add_variable(\"folderNames\", \"[String!]\")\n    folder_types_var = query.add_variable(\"folderTypes\", \"[String!]\")\n    has_products_var = query.add_variable(\"folderHasProducts\", \"Boolean!\")\n    has_tasks_var = query.add_variable(\"folderHasTasks\", \"Boolean!\")\n    has_links_var = query.add_variable(\"folderHasLinks\", \"HasLinksFilter\")\n    has_children_var = query.add_variable(\"folderHasChildren\", \"Boolean!\")\n    statuses_var = query.add_variable(\"folderStatuses\", \"[String!]\")\n    tags_var = query.add_variable(\"folderTags\", \"[String!]\")\n\n    project_field = query.add_field(\"project\")\n    project_field.set_filter(\"name\", project_name_var)\n\n    folders_field = project_field.add_field_with_edges(\"folders\")\n    folders_field.set_filter(\"ids\", folder_ids_var)\n    folders_field.set_filter(\"parentIds\", parent_folder_ids_var)\n    folders_field.set_filter(\"names\", folder_names_var)\n    folders_field.set_filter(\"paths\", folder_paths_var)\n    folders_field.set_filter(\"pathEx\", folder_path_regex_var)\n    folders_field.set_filter(\"folderTypes\", folder_types_var)\n    folders_field.set_filter(\"statuses\", statuses_var)\n    folders_field.set_filter(\"tags\", tags_var)\n    folders_field.set_filter(\"hasProducts\", has_products_var)\n    folders_field.set_filter(\"hasTasks\", has_tasks_var)\n    folders_field.set_filter(\"hasLinks\", has_links_var)\n    folders_field.set_filter(\"hasChildren\", has_children_var)\n\n    nested_fields = fields_to_dict(fields)\n    add_links_fields(folders_field, nested_fields)\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, folders_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n\n\ndef tasks_graphql_query(fields):\n    query = GraphQlQuery(\"TasksQuery\")\n    project_name_var = query.add_variable(\"projectName\", \"String!\")\n    task_ids_var = query.add_variable(\"taskIds\", \"[String!]\")\n    task_names_var = query.add_variable(\"taskNames\", \"[String!]\")\n    task_types_var = query.add_variable(\"taskTypes\", \"[String!]\")\n    folder_ids_var = query.add_variable(\"folderIds\", \"[String!]\")\n    assignees_any_var = query.add_variable(\"taskAssigneesAny\", \"[String!]\")\n    assignees_all_var = query.add_variable(\"taskAssigneesAll\", \"[String!]\")\n    statuses_var = query.add_variable(\"taskStatuses\", \"[String!]\")\n    tags_var = query.add_variable(\"taskTags\", \"[String!]\")\n\n    project_field = query.add_field(\"project\")\n    project_field.set_filter(\"name\", project_name_var)\n\n    tasks_field = project_field.add_field_with_edges(\"tasks\")\n    tasks_field.set_filter(\"ids\", task_ids_var)\n    # WARNING: At moment when this been created 'names' filter is not supported\n    tasks_field.set_filter(\"names\", task_names_var)\n    tasks_field.set_filter(\"taskTypes\", task_types_var)\n    tasks_field.set_filter(\"folderIds\", folder_ids_var)\n    tasks_field.set_filter(\"assigneesAny\", assignees_any_var)\n    tasks_field.set_filter(\"assignees\", assignees_all_var)\n    tasks_field.set_filter(\"statuses\", statuses_var)\n    tasks_field.set_filter(\"tags\", tags_var)\n\n    nested_fields = fields_to_dict(fields)\n    add_links_fields(tasks_field, nested_fields)\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, tasks_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n\n\ndef products_graphql_query(fields):\n    query = GraphQlQuery(\"ProductsQuery\")\n\n    project_name_var = query.add_variable(\"projectName\", \"String!\")\n    product_ids_var = query.add_variable(\"productIds\", \"[String!]\")\n    product_names_var = query.add_variable(\"productNames\", \"[String!]\")\n    folder_ids_var = query.add_variable(\"folderIds\", \"[String!]\")\n    product_types_var = query.add_variable(\"productTypes\", \"[String!]\")\n    product_name_regex_var = query.add_variable(\"productNameRegex\", \"String!\")\n    product_path_regex_var = query.add_variable(\"productPathRegex\", \"String!\")\n    statuses_var = query.add_variable(\"productStatuses.\", \"[String!]\")\n    tags_var = query.add_variable(\"productTags.\", \"[String!]\")\n\n    project_field = query.add_field(\"project\")\n    project_field.set_filter(\"name\", project_name_var)\n\n    products_field = project_field.add_field_with_edges(\"products\")\n    products_field.set_filter(\"ids\", product_ids_var)\n    products_field.set_filter(\"names\", product_names_var)\n    products_field.set_filter(\"folderIds\", folder_ids_var)\n    products_field.set_filter(\"productTypes\", product_types_var)\n    products_field.set_filter(\"statuses\", statuses_var)\n    products_field.set_filter(\"tags\", tags_var)\n    products_field.set_filter(\"nameEx\", product_name_regex_var)\n    products_field.set_filter(\"pathEx\", product_path_regex_var)\n\n    nested_fields = fields_to_dict(set(fields))\n    add_links_fields(products_field, nested_fields)\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, products_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n\n\ndef versions_graphql_query(fields):\n    query = GraphQlQuery(\"VersionsQuery\")\n\n    project_name_var = query.add_variable(\"projectName\", \"String!\")\n    product_ids_var = query.add_variable(\"productIds\", \"[String!]\")\n    version_ids_var = query.add_variable(\"versionIds\", \"[String!]\")\n    versions_var = query.add_variable(\"versions\", \"[Int!]\")\n    hero_only_var = query.add_variable(\"heroOnly\", \"Boolean\")\n    latest_only_var = query.add_variable(\"latestOnly\", \"Boolean\")\n    hero_or_latest_only_var = query.add_variable(\n        \"heroOrLatestOnly\", \"Boolean\"\n    )\n    statuses_var = query.add_variable(\"versionStatuses\", \"[String!]\")\n    tags_var = query.add_variable(\"versionTags\", \"[String!]\")\n\n    project_field = query.add_field(\"project\")\n    project_field.set_filter(\"name\", project_name_var)\n\n    versions_field = project_field.add_field_with_edges(\"versions\")\n    versions_field.set_filter(\"ids\", version_ids_var)\n    versions_field.set_filter(\"productIds\", product_ids_var)\n    versions_field.set_filter(\"versions\", versions_var)\n    versions_field.set_filter(\"heroOnly\", hero_only_var)\n    versions_field.set_filter(\"latestOnly\", latest_only_var)\n    versions_field.set_filter(\"heroOrLatestOnly\", hero_or_latest_only_var)\n    versions_field.set_filter(\"statuses\", statuses_var)\n    versions_field.set_filter(\"tags\", tags_var)\n\n    nested_fields = fields_to_dict(set(fields))\n    add_links_fields(versions_field, nested_fields)\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, versions_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n\n\ndef representations_graphql_query(fields):\n    query = GraphQlQuery(\"RepresentationsQuery\")\n\n    project_name_var = query.add_variable(\"projectName\", \"String!\")\n    repre_ids_var = query.add_variable(\"representationIds\", \"[String!]\")\n    repre_names_var = query.add_variable(\"representationNames\", \"[String!]\")\n    version_ids_var = query.add_variable(\"versionIds\", \"[String!]\")\n    has_links_var = query.add_variable(\"representationHasLinks\", \"HasLinksFilter\")\n    statuses_var = query.add_variable(\n        \"representationStatuses\", \"[String!]\"\n    )\n    tags_var = query.add_variable(\n        \"representationTags\", \"[String!]\"\n    )\n\n    project_field = query.add_field(\"project\")\n    project_field.set_filter(\"name\", project_name_var)\n\n    repres_field = project_field.add_field_with_edges(\"representations\")\n    repres_field.set_filter(\"ids\", repre_ids_var)\n    repres_field.set_filter(\"versionIds\", version_ids_var)\n    repres_field.set_filter(\"names\", repre_names_var)\n    repres_field.set_filter(\"hasLinks\", has_links_var)\n    repres_field.set_filter(\"statuses\", statuses_var)\n    repres_field.set_filter(\"tags\", tags_var)\n\n    nested_fields = fields_to_dict(set(fields))\n    add_links_fields(repres_field, nested_fields)\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, repres_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n\n\ndef representations_parents_qraphql_query(\n    version_fields, product_fields, folder_fields\n):\n    query = GraphQlQuery(\"RepresentationsParentsQuery\")\n\n    project_name_var = query.add_variable(\"projectName\", \"String!\")\n    repre_ids_var = query.add_variable(\"representationIds\", \"[String!]\")\n\n    project_field = query.add_field(\"project\")\n    project_field.set_filter(\"name\", project_name_var)\n\n    repres_field = project_field.add_field_with_edges(\"representations\")\n    repres_field.add_field(\"id\")\n    repres_field.set_filter(\"ids\", repre_ids_var)\n    version_field = repres_field.add_field(\"version\")\n\n    fields_queue = collections.deque()\n    for key, value in fields_to_dict(version_fields).items():\n        fields_queue.append((key, value, version_field))\n\n    product_field = version_field.add_field(\"product\")\n    for key, value in fields_to_dict(product_fields).items():\n        fields_queue.append((key, value, product_field))\n\n    folder_field = product_field.add_field(\"folder\")\n    for key, value in fields_to_dict(folder_fields).items():\n        fields_queue.append((key, value, folder_field))\n\n    while fields_queue:\n        item = fields_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            fields_queue.append((k, v, field))\n\n    return query\n\n\ndef workfiles_info_graphql_query(fields):\n    query = GraphQlQuery(\"WorkfilesInfo\")\n    project_name_var = query.add_variable(\"projectName\", \"String!\")\n    workfiles_info_ids = query.add_variable(\"workfileIds\", \"[String!]\")\n    task_ids_var = query.add_variable(\"taskIds\", \"[String!]\")\n    paths_var = query.add_variable(\"paths\", \"[String!]\")\n    path_regex_var = query.add_variable(\"workfilePathRegex\", \"String!\")\n    has_links_var = query.add_variable(\"workfilehasLinks\", \"HasLinksFilter\")\n    statuses_var = query.add_variable(\"workfileStatuses\", \"[String!]\")\n    tags_var = query.add_variable(\"workfileTags\", \"[String!]\")\n\n    project_field = query.add_field(\"project\")\n    project_field.set_filter(\"name\", project_name_var)\n\n    workfiles_field = project_field.add_field_with_edges(\"workfiles\")\n    workfiles_field.set_filter(\"ids\", workfiles_info_ids)\n    workfiles_field.set_filter(\"taskIds\", task_ids_var)\n    workfiles_field.set_filter(\"paths\", paths_var)\n    workfiles_field.set_filter(\"pathEx\", path_regex_var)\n    workfiles_field.set_filter(\"hasLinks\", has_links_var)\n    workfiles_field.set_filter(\"statuses\", statuses_var)\n    workfiles_field.set_filter(\"tags\", tags_var)\n\n    nested_fields = fields_to_dict(set(fields))\n    add_links_fields(workfiles_field, nested_fields)\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, workfiles_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n\n\ndef events_graphql_query(fields):\n    query = GraphQlQuery(\"Events\")\n    topics_var = query.add_variable(\"eventTopics\", \"[String!]\")\n    projects_var = query.add_variable(\"projectNames\", \"[String!]\")\n    states_var = query.add_variable(\"eventStates\", \"[String!]\")\n    users_var = query.add_variable(\"eventUsers\", \"[String!]\")\n    include_logs_var = query.add_variable(\"includeLogsFilter\", \"Boolean!\")\n    has_children_var = query.add_variable(\"hasChildrenFilter\", \"Boolean!\")\n    newer_than_var = query.add_variable(\"newerThanFilter\", \"String!\")\n    older_than_var = query.add_variable(\"olderThanFilter\", \"String!\")\n\n    events_field = query.add_field_with_edges(\"events\")\n    events_field.set_filter(\"topics\", topics_var)\n    events_field.set_filter(\"projects\", projects_var)\n    events_field.set_filter(\"states\", states_var)\n    events_field.set_filter(\"users\", users_var)\n    events_field.set_filter(\"includeLogs\", include_logs_var)\n    events_field.set_filter(\"hasChildren\", has_children_var)\n    events_field.set_filter(\"newerThan\", newer_than_var)\n    events_field.set_filter(\"olderThan\", older_than_var)\n\n    nested_fields = fields_to_dict(set(fields))\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, events_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n\n\ndef users_graphql_query(fields):\n    query = GraphQlQuery(\"Users\")\n    names_var = query.add_variable(\"userNames\", \"[String!]\")\n\n    users_field = query.add_field_with_edges(\"users\")\n    users_field.set_filter(\"names\", names_var)\n\n    nested_fields = fields_to_dict(set(fields))\n\n    query_queue = collections.deque()\n    for key, value in nested_fields.items():\n        query_queue.append((key, value, users_field))\n\n    while query_queue:\n        item = query_queue.popleft()\n        key, value, parent = item\n        field = parent.add_field(key)\n        if value is FIELD_VALUE:\n            continue\n\n        for k, v in value.items():\n            query_queue.append((k, v, field))\n    return query\n"
  },
  {
    "path": "openpype/vendor/python/common/ayon_api/operations.py",
    "content": "import os\nimport copy\nimport collections\nimport uuid\nfrom abc import ABCMeta, abstractmethod\n\nimport six\n\nfrom ._api import get_server_api_connection\nfrom .utils import create_entity_id, REMOVED_VALUE\n\n\ndef _create_or_convert_to_id(entity_id=None):\n    if entity_id is None:\n        return create_entity_id()\n\n    # Validate if can be converted to uuid\n    uuid.UUID(entity_id)\n    return entity_id\n\n\ndef new_folder_entity(\n    name,\n    folder_type,\n    parent_id=None,\n    status=None,\n    tags=None,\n    attribs=None,\n    data=None,\n    thumbnail_id=None,\n    entity_id=None\n):\n    \"\"\"Create skeleton data of folder entity.\n\n    Args:\n        name (str): Is considered as unique identifier of folder in project.\n        folder_type (str): Type of folder.\n        parent_id (Optional[str]): Parent folder id.\n        status (Optional[str]): Product status.\n        tags (Optional[List[str]]): List of tags.\n        attribs (Optional[Dict[str, Any]]): Explicitly set attributes\n            of folder.\n        data (Optional[Dict[str, Any]]): Custom folder data. Empty dictionary\n            is used if not passed.\n        thumbnail_id (Optional[str]): Thumbnail id related to folder.\n        entity_id (Optional[str]): Predefined id of entity. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of folder entity.\n    \"\"\"\n\n    if attribs is None:\n        attribs = {}\n\n    if data is None:\n        data = {}\n\n    if parent_id is not None:\n        parent_id = _create_or_convert_to_id(parent_id)\n\n    output = {\n        \"id\": _create_or_convert_to_id(entity_id),\n        \"name\": name,\n        # This will be ignored\n        \"folderType\": folder_type,\n        \"parentId\": parent_id,\n        \"data\": data,\n        \"attrib\": attribs,\n        \"thumbnailId\": thumbnail_id\n    }\n    if status:\n        output[\"status\"] = status\n    if tags:\n        output[\"tags\"] = tags\n    return output\n\n\ndef new_product_entity(\n    name,\n    product_type,\n    folder_id,\n    status=None,\n    tags=None,\n    attribs=None,\n    data=None,\n    entity_id=None\n):\n    \"\"\"Create skeleton data of product entity.\n\n    Args:\n        name (str): Is considered as unique identifier of\n            product under folder.\n        product_type (str): Product type.\n        folder_id (str): Parent folder id.\n        status (Optional[str]): Product status.\n        tags (Optional[List[str]]): List of tags.\n        attribs (Optional[Dict[str, Any]]): Explicitly set attributes\n            of product.\n        data (Optional[Dict[str, Any]]): product entity data. Empty dictionary\n            is used if not passed.\n        entity_id (Optional[str]): Predefined id of entity. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of product entity.\n    \"\"\"\n\n    if attribs is None:\n        attribs = {}\n\n    if data is None:\n        data = {}\n\n    output = {\n        \"id\": _create_or_convert_to_id(entity_id),\n        \"name\": name,\n        \"productType\": product_type,\n        \"attrib\": attribs,\n        \"data\": data,\n        \"folderId\": _create_or_convert_to_id(folder_id),\n    }\n    if status:\n        output[\"status\"] = status\n    if tags:\n        output[\"tags\"] = tags\n    return output\n\n\ndef new_version_entity(\n    version,\n    product_id,\n    task_id=None,\n    thumbnail_id=None,\n    author=None,\n    status=None,\n    tags=None,\n    attribs=None,\n    data=None,\n    entity_id=None\n):\n    \"\"\"Create skeleton data of version entity.\n\n    Args:\n        version (int): Is considered as unique identifier of version\n            under product.\n        product_id (str): Parent product id.\n        task_id (Optional[str]): Task id under which product was created.\n        thumbnail_id (Optional[str]): Thumbnail related to version.\n        author (Optional[str]): Name of version author.\n        status (Optional[str]): Version status.\n        tags (Optional[List[str]]): List of tags.\n        attribs (Optional[Dict[str, Any]]): Explicitly set attributes\n            of version.\n        data (Optional[Dict[str, Any]]): Version entity custom data.\n        entity_id (Optional[str]): Predefined id of entity. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of version entity.\n    \"\"\"\n\n    if attribs is None:\n        attribs = {}\n\n    if data is None:\n        data = {}\n\n    if data is None:\n        data = {}\n\n    output = {\n        \"id\": _create_or_convert_to_id(entity_id),\n        \"version\": int(version),\n        \"productId\": _create_or_convert_to_id(product_id),\n        \"attrib\": attribs,\n        \"data\": data\n    }\n    if task_id:\n        output[\"taskId\"] = task_id\n    if thumbnail_id:\n        output[\"thumbnailId\"] = thumbnail_id\n    if author:\n        output[\"author\"] = author\n    if tags:\n        output[\"tags\"] = tags\n    if status:\n        output[\"status\"] = status\n    return output\n\n\ndef new_hero_version_entity(\n    version,\n    product_id,\n    task_id=None,\n    thumbnail_id=None,\n    author=None,\n    status=None,\n    tags=None,\n    attribs=None,\n    data=None,\n    entity_id=None\n):\n    \"\"\"Create skeleton data of hero version entity.\n\n    Args:\n        version (int): Is considered as unique identifier of version\n            under product. Should be same as standard version if there is any.\n        product_id (str): Parent product id.\n        task_id (Optional[str]): Task id under which product was created.\n        thumbnail_id (Optional[str]): Thumbnail related to version.\n        author (Optional[str]): Name of version author.\n        status (Optional[str]): Version status.\n        tags (Optional[List[str]]): List of tags.\n        attribs (Optional[Dict[str, Any]]): Explicitly set attributes\n            of version.\n        data (Optional[Dict[str, Any]]): Version entity data.\n        entity_id (Optional[str]): Predefined id of entity. New id is\n            created if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of version entity.\n    \"\"\"\n\n    if attribs is None:\n        attribs = {}\n\n    if data is None:\n        data = {}\n\n    output = {\n        \"id\": _create_or_convert_to_id(entity_id),\n        \"version\": -abs(int(version)),\n        \"productId\": product_id,\n        \"attrib\": attribs,\n        \"data\": data\n    }\n    if task_id:\n        output[\"taskId\"] = task_id\n    if thumbnail_id:\n        output[\"thumbnailId\"] = thumbnail_id\n    if author:\n        output[\"author\"] = author\n    if tags:\n        output[\"tags\"] = tags\n    if status:\n        output[\"status\"] = status\n    return output\n\n\ndef new_representation_entity(\n    name,\n    version_id,\n    files,\n    status=None,\n    tags=None,\n    attribs=None,\n    data=None,\n    entity_id=None\n):\n    \"\"\"Create skeleton data of representation entity.\n\n    Args:\n        name (str): Representation name considered as unique identifier\n            of representation under version.\n        version_id (str): Parent version id.\n        files (list[dict[str, str]]): List of files in representation.\n        status (Optional[str]): Representation status.\n        tags (Optional[List[str]]): List of tags.\n        attribs (Optional[Dict[str, Any]]): Explicitly set attributes\n            of representation.\n        data (Optional[Dict[str, Any]]): Representation entity data.\n        entity_id (Optional[str]): Predefined id of entity. New id is created\n            if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of representation entity.\n    \"\"\"\n\n    if attribs is None:\n        attribs = {}\n\n    if data is None:\n        data = {}\n\n    output = {\n        \"id\": _create_or_convert_to_id(entity_id),\n        \"versionId\": _create_or_convert_to_id(version_id),\n        \"files\": files,\n        \"name\": name,\n        \"data\": data,\n        \"attrib\": attribs\n    }\n    if tags:\n        output[\"tags\"] = tags\n    if status:\n        output[\"status\"] = status\n    return output\n\n\ndef new_workfile_info(\n    filepath,\n    task_id,\n    status=None,\n    tags=None,\n    attribs=None,\n    description=None,\n    data=None,\n    entity_id=None\n):\n    \"\"\"Create skeleton data of workfile info entity.\n\n    Workfile entity is at this moment used primarily for artist notes.\n\n    Args:\n        filepath (str): Rootless workfile filepath.\n        task_id (str): Task under which was workfile created.\n        status (Optional[str]): Workfile status.\n        tags (Optional[List[str]]): Workfile tags.\n        attribs (Options[dic[str, Any]]): Explicitly set attributes.\n        description (Optional[str]): Workfile description.\n        data (Optional[Dict[str, Any]]): Additional metadata.\n        entity_id (Optional[str]): Predefined id of entity. New id is created\n            if not passed.\n\n    Returns:\n        Dict[str, Any]: Skeleton of workfile info entity.\n    \"\"\"\n\n    if attribs is None:\n        attribs = {}\n\n    if \"extension\" not in attribs:\n        attribs[\"extension\"] = os.path.splitext(filepath)[-1]\n\n    if description:\n        attribs[\"description\"] = description\n\n    if not data:\n        data = {}\n\n    output = {\n        \"id\": _create_or_convert_to_id(entity_id),\n        \"taskId\": task_id,\n        \"path\": filepath,\n        \"data\": data,\n        \"attrib\": attribs\n    }\n    if status:\n        output[\"status\"] = status\n\n    if tags:\n        output[\"tags\"] = tags\n    return output\n\n\n@six.add_metaclass(ABCMeta)\nclass AbstractOperation(object):\n    \"\"\"Base operation class.\n\n    Opration represent a call into database. The call can create, change or\n    remove data.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'folder', 'representation' etc.\n    \"\"\"\n\n    def __init__(self, project_name, entity_type, session):\n        self._project_name = project_name\n        self._entity_type = entity_type\n        self._session = session\n        self._id = str(uuid.uuid4())\n\n    @property\n    def project_name(self):\n        return self._project_name\n\n    @property\n    def id(self):\n        \"\"\"Identifier of operation.\"\"\"\n\n        return self._id\n\n    @property\n    def entity_type(self):\n        return self._entity_type\n\n    @property\n    @abstractmethod\n    def operation_name(self):\n        \"\"\"Stringified type of operation.\"\"\"\n\n        pass\n\n    def to_data(self):\n        \"\"\"Convert opration to data that can be converted to json or others.\n\n        Returns:\n            Dict[str, Any]: Description of operation.\n        \"\"\"\n\n        return {\n            \"id\": self._id,\n            \"entity_type\": self.entity_type,\n            \"project_name\": self.project_name,\n            \"operation\": self.operation_name\n        }\n\n\nclass CreateOperation(AbstractOperation):\n    \"\"\"Opeartion to create an entity.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'folder', 'representation' etc.\n        data (Dict[str, Any]): Data of entity that will be created.\n    \"\"\"\n\n    operation_name = \"create\"\n\n    def __init__(self, project_name, entity_type, data, session):\n        if not data:\n            data = {}\n        else:\n            data = copy.deepcopy(dict(data))\n\n        if \"id\" not in data:\n            data[\"id\"] = create_entity_id()\n\n        self._data = data\n        super(CreateOperation, self).__init__(\n            project_name, entity_type, session\n        )\n\n    def __setitem__(self, key, value):\n        self.set_value(key, value)\n\n    def __getitem__(self, key):\n        return self.data[key]\n\n    def set_value(self, key, value):\n        self.data[key] = value\n\n    def get(self, key, *args, **kwargs):\n        return self.data.get(key, *args, **kwargs)\n\n    @property\n    def con(self):\n        return self.session.con\n\n    @property\n    def session(self):\n        return self._session\n\n    @property\n    def entity_id(self):\n        return self._data[\"id\"]\n\n    @property\n    def data(self):\n        return self._data\n\n    def to_data(self):\n        output = super(CreateOperation, self).to_data()\n        output[\"data\"] = copy.deepcopy(self.data)\n        return output\n\n    def to_server_operation(self):\n        return {\n            \"id\": self.id,\n            \"type\": \"create\",\n            \"entityType\": self.entity_type,\n            \"entityId\": self.entity_id,\n            \"data\": self._data\n        }\n\n\nclass UpdateOperation(AbstractOperation):\n    \"\"\"Operation to update an entity.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'folder', 'representation' etc.\n        entity_id (str): Identifier of an entity.\n        update_data (Dict[str, Any]): Key -> value changes that will be set in\n            database. If value is set to 'REMOVED_VALUE' the key will be\n            removed. Only first level of dictionary is checked (on purpose).\n    \"\"\"\n\n    operation_name = \"update\"\n\n    def __init__(\n        self, project_name, entity_type, entity_id, update_data, session\n    ):\n        super(UpdateOperation, self).__init__(\n            project_name, entity_type, session\n        )\n\n        self._entity_id = entity_id\n        self._update_data = update_data\n\n    @property\n    def entity_id(self):\n        return self._entity_id\n\n    @property\n    def update_data(self):\n        return self._update_data\n\n    @property\n    def con(self):\n        return self.session.con\n\n    @property\n    def session(self):\n        return self._session\n\n    def to_data(self):\n        changes = {}\n        for key, value in self._update_data.items():\n            if value is REMOVED_VALUE:\n                value = None\n            changes[key] = value\n\n        output = super(UpdateOperation, self).to_data()\n        output.update({\n            \"entity_id\": self.entity_id,\n            \"changes\": changes\n        })\n        return output\n\n    def to_server_operation(self):\n        if not self._update_data:\n            return None\n\n        update_data = {}\n        for key, value in self._update_data.items():\n            if value is REMOVED_VALUE:\n                value = None\n            update_data[key] = value\n\n        return {\n            \"id\": self.id,\n            \"type\": \"update\",\n            \"entityType\": self.entity_type,\n            \"entityId\": self.entity_id,\n            \"data\": update_data\n        }\n\n\nclass DeleteOperation(AbstractOperation):\n    \"\"\"Opeartion to delete an entity.\n\n    Args:\n        project_name (str): On which project operation will happen.\n        entity_type (str): Type of entity on which change happens.\n            e.g. 'folder', 'representation' etc.\n        entity_id (str): Entity id that will be removed.\n    \"\"\"\n\n    operation_name = \"delete\"\n\n    def __init__(self, project_name, entity_type, entity_id, session):\n        self._entity_id = entity_id\n\n        super(DeleteOperation, self).__init__(\n            project_name, entity_type, session\n        )\n\n    @property\n    def entity_id(self):\n        return self._entity_id\n\n    @property\n    def con(self):\n        return self.session.con\n\n    @property\n    def session(self):\n        return self._session\n\n    def to_data(self):\n        output = super(DeleteOperation, self).to_data()\n        output[\"entity_id\"] = self.entity_id\n        return output\n\n    def to_server_operation(self):\n        return {\n            \"id\": self.id,\n            \"type\": self.operation_name,\n            \"entityId\": self.entity_id,\n            \"entityType\": self.entity_type,\n        }\n\n\nclass OperationsSession(object):\n    \"\"\"Session storing operations that should happen in an order.\n\n    At this moment does not handle anything special can be sonsidered as\n    stupid list of operations that will happen after each other. If creation\n    of same entity is there multiple times it's handled in any way and entity\n    values are not validated.\n\n    All operations must be related to single project.\n\n    Args:\n        project_name (str): Project name to which are operations related.\n    \"\"\"\n\n    def __init__(self, con=None):\n        if con is None:\n            con = get_server_api_connection()\n        self._con = con\n        self._project_cache = {}\n        self._operations = []\n        self._nested_operations = collections.defaultdict(list)\n\n    @property\n    def con(self):\n        return self._con\n\n    def get_project(self, project_name):\n        if project_name not in self._project_cache:\n            self._project_cache[project_name] = self.con.get_project(\n                project_name)\n        return copy.deepcopy(self._project_cache[project_name])\n\n    def __len__(self):\n        return len(self._operations)\n\n    def add(self, operation):\n        \"\"\"Add operation to be processed.\n\n        Args:\n            operation (BaseOperation): Operation that should be processed.\n        \"\"\"\n        if not isinstance(\n            operation,\n            (CreateOperation, UpdateOperation, DeleteOperation)\n        ):\n            raise TypeError(\"Expected Operation object got {}\".format(\n                str(type(operation))\n            ))\n\n        self._operations.append(operation)\n\n    def append(self, operation):\n        \"\"\"Add operation to be processed.\n\n        Args:\n            operation (BaseOperation): Operation that should be processed.\n        \"\"\"\n\n        self.add(operation)\n\n    def extend(self, operations):\n        \"\"\"Add operations to be processed.\n\n        Args:\n            operations (List[BaseOperation]): Operations that should be\n                processed.\n        \"\"\"\n\n        for operation in operations:\n            self.add(operation)\n\n    def remove(self, operation):\n        \"\"\"Remove operation.\"\"\"\n\n        self._operations.remove(operation)\n\n    def clear(self):\n        \"\"\"Clear all registered operations.\"\"\"\n\n        self._operations = []\n\n    def to_data(self):\n        return [\n            operation.to_data()\n            for operation in self._operations\n        ]\n\n    def commit(self):\n        \"\"\"Commit session operations.\"\"\"\n\n        operations, self._operations = self._operations, []\n        if not operations:\n            return\n\n        operations_by_project = collections.defaultdict(list)\n        for operation in operations:\n            operations_by_project[operation.project_name].append(operation)\n\n        for project_name, operations in operations_by_project.items():\n            operations_body = []\n            for operation in operations:\n                body = operation.to_server_operation()\n                if body is not None:\n                    operations_body.append(body)\n\n            self._con.send_batch_operations(\n                project_name, operations_body, can_fail=False\n            )\n\n    def create_entity(self, project_name, entity_type, data, nested_id=None):\n        \"\"\"Fast access to 'CreateOperation'.\n\n        Args:\n            project_name (str): On which project the creation happens.\n            entity_type (str): Which entity type will be created.\n            data (Dicst[str, Any]): Entity data.\n            nested_id (str): Id of other operation from which is triggered\n                operation -> Operations can trigger suboperations but they\n                must be added to operations list after it's parent is added.\n\n        Returns:\n            CreateOperation: Object of update operation.\n        \"\"\"\n\n        operation = CreateOperation(\n            project_name, entity_type, data, self\n        )\n\n        if nested_id:\n            self._nested_operations[nested_id].append(operation)\n        else:\n            self.add(operation)\n            if operation.id in self._nested_operations:\n                self.extend(self._nested_operations.pop(operation.id))\n\n        return operation\n\n    def update_entity(\n        self, project_name, entity_type, entity_id, update_data, nested_id=None\n    ):\n        \"\"\"Fast access to 'UpdateOperation'.\n\n        Returns:\n            UpdateOperation: Object of update operation.\n        \"\"\"\n\n        operation = UpdateOperation(\n            project_name, entity_type, entity_id, update_data, self\n        )\n        if nested_id:\n            self._nested_operations[nested_id].append(operation)\n        else:\n            self.add(operation)\n            if operation.id in self._nested_operations:\n                self.extend(self._nested_operations.pop(operation.id))\n        return operation\n\n    def delete_entity(\n        self, project_name, entity_type, entity_id, nested_id=None\n    ):\n        \"\"\"Fast access to 'DeleteOperation'.\n\n        Returns:\n            DeleteOperation: Object of delete operation.\n        \"\"\"\n\n        operation = DeleteOperation(\n            project_name, entity_type, entity_id, self\n        )\n        if nested_id:\n            self._nested_operations[nested_id].append(operation)\n        else:\n            self.add(operation)\n            if operation.id in self._nested_operations:\n                self.extend(self._nested_operations.pop(operation.id))\n        return operation\n"
  },
  {
    "path": "openpype/vendor/python/common/ayon_api/server_api.py",
    "content": "import os\nimport re\nimport io\nimport json\nimport time\nimport logging\nimport collections\nimport platform\nimport copy\nimport uuid\nimport warnings\nfrom contextlib import contextmanager\n\nimport six\n\ntry:\n    from http import HTTPStatus\nexcept ImportError:\n    HTTPStatus = None\n\nimport requests\ntry:\n    # This should be used if 'requests' have it available\n    from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError\nexcept ImportError:\n    # Older versions of 'requests' don't have custom exception for json\n    #   decode error\n    try:\n        from simplejson import JSONDecodeError as RequestsJSONDecodeError\n    except ImportError:\n        from json import JSONDecodeError as RequestsJSONDecodeError\n\nfrom .constants import (\n    SERVER_RETRIES_ENV_KEY,\n    DEFAULT_PRODUCT_TYPE_FIELDS,\n    DEFAULT_PROJECT_FIELDS,\n    DEFAULT_FOLDER_FIELDS,\n    DEFAULT_TASK_FIELDS,\n    DEFAULT_PRODUCT_FIELDS,\n    DEFAULT_VERSION_FIELDS,\n    DEFAULT_REPRESENTATION_FIELDS,\n    REPRESENTATION_FILES_FIELDS,\n    DEFAULT_WORKFILE_INFO_FIELDS,\n    DEFAULT_EVENT_FIELDS,\n    DEFAULT_USER_FIELDS,\n)\nfrom .graphql import GraphQlQuery, INTROSPECTION_QUERY\nfrom .graphql_queries import (\n    project_graphql_query,\n    projects_graphql_query,\n    project_product_types_query,\n    product_types_query,\n    folders_graphql_query,\n    tasks_graphql_query,\n    products_graphql_query,\n    versions_graphql_query,\n    representations_graphql_query,\n    representations_parents_qraphql_query,\n    workfiles_info_graphql_query,\n    events_graphql_query,\n    users_graphql_query,\n)\nfrom .exceptions import (\n    FailedOperations,\n    UnauthorizedError,\n    AuthenticationError,\n    ServerNotReached,\n    ServerError,\n    HTTPRequestError,\n)\nfrom .utils import (\n    RepresentationParents,\n    prepare_query_string,\n    logout_from_server,\n    create_entity_id,\n    entity_data_json_default,\n    failed_json_default,\n    TransferProgress,\n    create_dependency_package_basename,\n    ThumbnailContent,\n    get_default_timeout,\n    get_default_settings_variant,\n    get_default_site_id,\n)\n\n_PLACEHOLDER = object()\nPatternType = type(re.compile(\"\"))\nJSONDecodeError = getattr(json, \"JSONDecodeError\", ValueError)\n# This should be collected from server schema\nPROJECT_NAME_ALLOWED_SYMBOLS = \"a-zA-Z0-9_\"\nPROJECT_NAME_REGEX = re.compile(\n    \"^[{}]+$\".format(PROJECT_NAME_ALLOWED_SYMBOLS)\n)\n\nVERSION_REGEX = re.compile(\n    r\"(?P<major>0|[1-9]\\d*)\"\n    r\"\\.(?P<minor>0|[1-9]\\d*)\"\n    r\"\\.(?P<patch>0|[1-9]\\d*)\"\n    r\"(?:-(?P<prerelease>[a-zA-Z\\d\\-.]*))?\"\n    r\"(?:\\+(?P<buildmetadata>[a-zA-Z\\d\\-.]*))?\"\n)\n\n\ndef _get_description(response):\n    if HTTPStatus is None:\n        return str(response.orig_response)\n    return HTTPStatus(response.status).description\n\n\nclass RequestType:\n    def __init__(self, name):\n        self.name = name\n\n    def __hash__(self):\n        return self.name.__hash__()\n\n\nclass RequestTypes:\n    get = RequestType(\"GET\")\n    post = RequestType(\"POST\")\n    put = RequestType(\"PUT\")\n    patch = RequestType(\"PATCH\")\n    delete = RequestType(\"DELETE\")\n\n\nclass RestApiResponse(object):\n    \"\"\"API Response.\"\"\"\n\n    def __init__(self, response, data=None):\n        if response is None:\n            status_code = 500\n        else:\n            status_code = response.status_code\n        self._response = response\n        self.status = status_code\n        self._data = data\n\n    @property\n    def text(self):\n        if self._response is None:\n            return self.detail\n        return self._response.text\n\n    @property\n    def orig_response(self):\n        return self._response\n\n    @property\n    def headers(self):\n        if self._response is None:\n            return {}\n        return self._response.headers\n\n    @property\n    def data(self):\n        if self._data is None:\n            try:\n                self._data = self.orig_response.json()\n            except RequestsJSONDecodeError:\n                self._data = {}\n        return self._data\n\n    @property\n    def content(self):\n        if self._response is None:\n            return b\"\"\n        return self._response.content\n\n    @property\n    def content_type(self):\n        return self.headers.get(\"Content-Type\")\n\n    @property\n    def detail(self):\n        detail = self.get(\"detail\")\n        if detail:\n            return detail\n        return _get_description(self)\n\n    @property\n    def status_code(self):\n        return self.status\n\n    def raise_for_status(self, message=None):\n        if self._response is None:\n            if self._data and self._data.get(\"detail\"):\n                raise ServerError(self._data[\"detail\"])\n            raise ValueError(\"Response is not available.\")\n\n        try:\n            self._response.raise_for_status()\n        except requests.exceptions.HTTPError as exc:\n            if message is None:\n                message = str(exc)\n            raise HTTPRequestError(message, exc.response)\n\n    def __enter__(self, *args, **kwargs):\n        return self._response.__enter__(*args, **kwargs)\n\n    def __contains__(self, key):\n        return key in self.data\n\n    def __repr__(self):\n        return \"<{} [{}]>\".format(self.__class__.__name__, self.status)\n\n    def __len__(self):\n        return 200 <= self.status < 400\n\n    def __getitem__(self, key):\n        return self.data[key]\n\n    def get(self, key, default=None):\n        data = self.data\n        if isinstance(data, dict):\n            return self.data.get(key, default)\n        return default\n\n\nclass GraphQlResponse:\n    def __init__(self, data):\n        self.data = data\n        self.errors = data.get(\"errors\")\n\n    def __len__(self):\n        if self.errors:\n            return 0\n        return 1\n\n    def __repr__(self):\n        if self.errors:\n            return \"<{} errors={}>\".format(\n                self.__class__.__name__, self.errors[0]['message']\n            )\n        return \"<{}>\".format(self.__class__.__name__)\n\n\ndef fill_own_attribs(entity):\n    if not entity or not entity.get(\"attrib\"):\n        return\n\n    attributes = set(entity[\"ownAttrib\"])\n\n    own_attrib = {}\n    entity[\"ownAttrib\"] = own_attrib\n\n    for key, value in entity[\"attrib\"].items():\n        if key not in attributes:\n            own_attrib[key] = None\n        else:\n            own_attrib[key] = copy.deepcopy(value)\n\n\nclass _AsUserStack:\n    \"\"\"Handle stack of users used over server api connection in service mode.\n\n    ServerAPI can behave as other users if it is using special API key.\n\n    Examples:\n        >>> stack = _AsUserStack()\n        >>> stack.set_default_username(\"DefaultName\")\n        >>> print(stack.username)\n        DefaultName\n        >>> with stack.as_user(\"Other1\"):\n        ...     print(stack.username)\n        ...     with stack.as_user(\"Other2\"):\n        ...         print(stack.username)\n        ...     print(stack.username)\n        ...     stack.clear()\n        ...     print(stack.username)\n        Other1\n        Other2\n        Other1\n        None\n        >>> print(stack.username)\n        None\n        >>> stack.set_default_username(\"DefaultName\")\n        >>> print(stack.username)\n        DefaultName\n    \"\"\"\n\n    def __init__(self):\n        self._users_by_id = {}\n        self._user_ids = []\n        self._last_user = None\n        self._default_user = None\n\n    def clear(self):\n        self._users_by_id = {}\n        self._user_ids = []\n        self._last_user = None\n        self._default_user = None\n\n    @property\n    def username(self):\n        # Use '_user_ids' for boolean check to have ability \"unset\"\n        #   default user\n        if self._user_ids:\n            return self._last_user\n        return self._default_user\n\n    def get_default_username(self):\n        return self._default_user\n\n    def set_default_username(self, username=None):\n        self._default_user = username\n\n    default_username = property(get_default_username, set_default_username)\n\n    @contextmanager\n    def as_user(self, username):\n        self._last_user = username\n        user_id = uuid.uuid4().hex\n        self._user_ids.append(user_id)\n        self._users_by_id[user_id] = username\n        try:\n            yield\n        finally:\n            self._users_by_id.pop(user_id, None)\n            if not self._user_ids:\n                return\n\n            # First check if is the user id the last one\n            was_last = self._user_ids[-1] == user_id\n            # Remove id from variables\n            if user_id in self._user_ids:\n                self._user_ids.remove(user_id)\n\n            if not was_last:\n                return\n\n            new_last_user = None\n            if self._user_ids:\n                new_last_user = self._users_by_id.get(self._user_ids[-1])\n            self._last_user = new_last_user\n\n\nclass ServerAPI(object):\n    \"\"\"Base handler of connection to server.\n\n    Requires url to server which is used as base for api and graphql calls.\n\n    Login cause that a session is used\n\n    Args:\n        base_url (str): Example: http://localhost:5000\n        token (Optional[str]): Access token (api key) to server.\n        site_id (Optional[str]): Unique name of site. Should be the same when\n            connection is created from the same machine under same user.\n        client_version (Optional[str]): Version of client application (used in\n            desktop client application).\n        default_settings_variant (Optional[Literal[\"production\", \"staging\"]]):\n            Settings variant used by default if a method for settings won't\n            get any (by default is 'production').\n        sender (Optional[str]): Sender of requests. Used in server logs and\n            propagated into events.\n        ssl_verify (Union[bool, str, None]): Verify SSL certificate\n            Looks for env variable value 'AYON_CA_FILE' by default. If not\n            available then 'True' is used.\n        cert (Optional[str]): Path to certificate file. Looks for env\n            variable value 'AYON_CERT_FILE' by default.\n        create_session (Optional[bool]): Create session for connection if\n            token is available. Default is True.\n        timeout (Optional[float]): Timeout for requests.\n        max_retries (Optional[int]): Number of retries for requests.\n    \"\"\"\n    _default_max_retries = 3\n    # 1 MB chunk by default\n    # TODO find out if these are reasonable default value\n    default_download_chunk_size = 1024 * 1024\n    default_upload_chunk_size = 1024 * 1024\n\n    def __init__(\n        self,\n        base_url,\n        token=None,\n        site_id=_PLACEHOLDER,\n        client_version=None,\n        default_settings_variant=None,\n        sender=None,\n        ssl_verify=None,\n        cert=None,\n        create_session=True,\n        timeout=None,\n        max_retries=None,\n    ):\n        if not base_url:\n            raise ValueError(\"Invalid server URL {}\".format(str(base_url)))\n\n        base_url = base_url.rstrip(\"/\")\n        self._base_url = base_url\n        self._rest_url = \"{}/api\".format(base_url)\n        self._graphql_url = \"{}/graphql\".format(base_url)\n        self._log = None\n        self._access_token = token\n        # Allow to have 'site_id' to 'None'\n        if site_id is _PLACEHOLDER:\n            site_id = get_default_site_id()\n        self._site_id = site_id\n        self._client_version = client_version\n        self._default_settings_variant = (\n            default_settings_variant\n            or get_default_settings_variant()\n        )\n        self._sender = sender\n\n        self._timeout = None\n        self._max_retries = None\n\n        # Set timeout and max retries based on passed values\n        self.set_timeout(timeout)\n        self.set_max_retries(max_retries)\n\n        if ssl_verify is None:\n            # Custom AYON env variable for CA file or 'True'\n            # - that should cover most default behaviors in 'requests'\n            #   with 'certifi'\n            ssl_verify = os.environ.get(\"AYON_CA_FILE\") or True\n\n        if cert is None:\n            cert = os.environ.get(\"AYON_CERT_FILE\")\n\n        self._ssl_verify = ssl_verify\n        self._cert = cert\n\n        self._access_token_is_service = None\n        self._token_is_valid = None\n        self._token_validation_started = False\n        self._server_available = None\n        self._server_version = None\n        self._server_version_tuple = None\n\n        self._graphql_allows_data_in_query = None\n\n        self._session = None\n\n        self._base_functions_mapping = {\n            RequestTypes.get: requests.get,\n            RequestTypes.post: requests.post,\n            RequestTypes.put: requests.put,\n            RequestTypes.patch: requests.patch,\n            RequestTypes.delete: requests.delete\n        }\n        self._session_functions_mapping = {}\n\n        # Attributes cache\n        self._attributes_schema = None\n        self._entity_type_attributes_cache = {}\n\n        self._as_user_stack = _AsUserStack()\n\n        # Create session\n        if self._access_token and create_session:\n            self.validate_server_availability()\n            self.create_session()\n\n    @property\n    def log(self):\n        if self._log is None:\n            self._log = logging.getLogger(self.__class__.__name__)\n        return self._log\n\n    def get_base_url(self):\n        return self._base_url\n\n    def get_rest_url(self):\n        return self._rest_url\n\n    base_url = property(get_base_url)\n    rest_url = property(get_rest_url)\n\n    def get_ssl_verify(self):\n        \"\"\"Enable ssl verification.\n\n        Returns:\n            bool: Current state of ssl verification.\n        \"\"\"\n\n        return self._ssl_verify\n\n    def set_ssl_verify(self, ssl_verify):\n        \"\"\"Change ssl verification state.\n\n        Args:\n            ssl_verify (Union[bool, str, None]): Enabled/disable\n                ssl verification, can be a path to file.\n        \"\"\"\n\n        if self._ssl_verify == ssl_verify:\n            return\n        self._ssl_verify = ssl_verify\n        if self._session is not None:\n            self._session.verify = ssl_verify\n\n    def get_cert(self):\n        \"\"\"Current cert file used for connection to server.\n\n        Returns:\n            Union[str, None]: Path to cert file.\n        \"\"\"\n\n        return self._cert\n\n    def set_cert(self, cert):\n        \"\"\"Change cert file used for connection to server.\n\n        Args:\n            cert (Union[str, None]): Path to cert file.\n        \"\"\"\n\n        if cert == self._cert:\n            return\n        self._cert = cert\n        if self._session is not None:\n            self._session.cert = cert\n\n    ssl_verify = property(get_ssl_verify, set_ssl_verify)\n    cert = property(get_cert, set_cert)\n\n    @classmethod\n    def get_default_timeout(cls):\n        \"\"\"Default value for requests timeout.\n\n        Utils function 'get_default_timeout' is used by default.\n\n        Returns:\n            float: Timeout value in seconds.\n        \"\"\"\n\n        return get_default_timeout()\n\n    @classmethod\n    def get_default_max_retries(cls):\n        \"\"\"Default value for requests max retries.\n\n        First looks for environment variable SERVER_RETRIES_ENV_KEY, which\n        can affect max retries value. If not available then use class\n        attribute '_default_max_retries'.\n\n        Returns:\n            int: Max retries value.\n        \"\"\"\n\n        try:\n            return int(os.environ.get(SERVER_RETRIES_ENV_KEY))\n        except (ValueError, TypeError):\n            pass\n\n        return cls._default_max_retries\n\n    def get_timeout(self):\n        \"\"\"Current value for requests timeout.\n\n        Returns:\n            float: Timeout value in seconds.\n        \"\"\"\n\n        return self._timeout\n\n    def set_timeout(self, timeout):\n        \"\"\"Change timeout value for requests.\n\n        Args:\n            timeout (Union[float, None]): Timeout value in seconds.\n        \"\"\"\n\n        if timeout is None:\n            timeout = self.get_default_timeout()\n        self._timeout = float(timeout)\n\n    def get_max_retries(self):\n        \"\"\"Current value for requests max retries.\n\n        Returns:\n            int: Max retries value.\n        \"\"\"\n\n        return self._max_retries\n\n    def set_max_retries(self, max_retries):\n        \"\"\"Change max retries value for requests.\n\n        Args:\n            max_retries (Union[int, None]): Max retries value.\n        \"\"\"\n\n        if max_retries is None:\n            max_retries = self.get_default_max_retries()\n        self._max_retries = int(max_retries)\n\n    timeout = property(get_timeout, set_timeout)\n    max_retries = property(get_max_retries, set_max_retries)\n\n    @property\n    def access_token(self):\n        \"\"\"Access token used for authorization to server.\n\n        Returns:\n            Union[str, None]: Token string or None if not authorized yet.\n        \"\"\"\n\n        return self._access_token\n\n    def get_site_id(self):\n        \"\"\"Site id used for connection.\n\n        Site id tells server from which machine/site is connection created and\n        is used for default site overrides when settings are received.\n\n        Returns:\n            Union[str, None]: Site id value or None if not filled.\n        \"\"\"\n\n        return self._site_id\n\n    def set_site_id(self, site_id):\n        \"\"\"Change site id of connection.\n\n        Behave as specific site for server. It affects default behavior of\n        settings getter methods.\n\n        Args:\n            site_id (Union[str, None]): Site id value, or 'None' to unset.\n        \"\"\"\n\n        if self._site_id == site_id:\n            return\n        self._site_id = site_id\n        # Recreate session on machine id change\n        self._update_session_headers()\n\n    site_id = property(get_site_id, set_site_id)\n\n    def get_client_version(self):\n        \"\"\"Version of client used to connect to server.\n\n        Client version is AYON client build desktop application.\n\n        Returns:\n            str: Client version string used in connection.\n        \"\"\"\n\n        return self._client_version\n\n    def set_client_version(self, client_version):\n        \"\"\"Set version of client used to connect to server.\n\n        Client version is AYON client build desktop application.\n\n        Args:\n            client_version (Union[str, None]): Client version string.\n        \"\"\"\n\n        if self._client_version == client_version:\n            return\n\n        self._client_version = client_version\n        self._update_session_headers()\n\n    client_version = property(get_client_version, set_client_version)\n\n    def get_default_settings_variant(self):\n        \"\"\"Default variant used for settings.\n\n        Returns:\n            Union[str, None]: name of variant or None.\n        \"\"\"\n\n        return self._default_settings_variant\n\n    def set_default_settings_variant(self, variant):\n        \"\"\"Change default variant for addon settings.\n\n        Note:\n            It is recommended to set only 'production' or 'staging' variants\n                as default variant.\n\n        Args:\n            variant (str): Settings variant name. It is possible to use\n                'production', 'staging' or name of dev bundle.\n        \"\"\"\n\n        self._default_settings_variant = variant\n\n    default_settings_variant = property(\n        get_default_settings_variant,\n        set_default_settings_variant\n    )\n\n    def get_sender(self):\n        \"\"\"Sender used to send requests.\n\n        Returns:\n            Union[str, None]: Sender name or None.\n        \"\"\"\n\n        return self._sender\n\n    def set_sender(self, sender):\n        \"\"\"Change sender used for requests.\n\n        Args:\n            sender (Union[str, None]): Sender name or None.\n        \"\"\"\n\n        if sender == self._sender:\n            return\n        self._sender = sender\n        self._update_session_headers()\n\n    sender = property(get_sender, set_sender)\n\n    def get_default_service_username(self):\n        \"\"\"Default username used for callbacks when used with service API key.\n\n        Returns:\n            Union[str, None]: Username if any was filled.\n        \"\"\"\n\n        return self._as_user_stack.get_default_username()\n\n    def set_default_service_username(self, username=None):\n        \"\"\"Service API will work as other user.\n\n        Service API keys can work as other user. It can be temporary using\n        context manager 'as_user' or it is possible to set default username if\n        'as_user' context manager is not entered.\n\n        Args:\n            username (Optional[str]): Username to work as when service.\n\n        Raises:\n            ValueError: When connection is not yet authenticated or api key\n                is not service token.\n        \"\"\"\n\n        current_username = self._as_user_stack.get_default_username()\n        if current_username == username:\n            return\n\n        if not self.has_valid_token:\n            raise ValueError(\n                \"Authentication of connection did not happen yet.\"\n            )\n\n        if not self._access_token_is_service:\n            raise ValueError(\n                \"Can't set service username. API key is not a service token.\"\n            )\n\n        self._as_user_stack.set_default_username(username)\n        if self._as_user_stack.username == username:\n            self._update_session_headers()\n\n    @contextmanager\n    def as_username(self, username):\n        \"\"\"Service API will temporarily work as other user.\n\n        This method can be used only if service API key is logged in.\n\n        Args:\n            username (Union[str, None]): Username to work as when service.\n\n        Raises:\n            ValueError: When connection is not yet authenticated or api key\n                is not service token.\n        \"\"\"\n\n        if not self.has_valid_token:\n            raise ValueError(\n                \"Authentication of connection did not happen yet.\"\n            )\n\n        if not self._access_token_is_service:\n            raise ValueError(\n                \"Can't set service username. API key is not a service token.\"\n            )\n\n        with self._as_user_stack.as_user(username) as o:\n            self._update_session_headers()\n            try:\n                yield o\n            finally:\n                self._update_session_headers()\n\n    @property\n    def is_server_available(self):\n        if self._server_available is None:\n            response = requests.get(\n                self._base_url,\n                cert=self._cert,\n                verify=self._ssl_verify\n            )\n            self._server_available = response.status_code == 200\n        return self._server_available\n\n    @property\n    def has_valid_token(self):\n        if self._access_token is None:\n            return False\n\n        if self._token_is_valid is None:\n            self.validate_token()\n        return self._token_is_valid\n\n    def validate_server_availability(self):\n        if not self.is_server_available:\n            raise ServerNotReached(\"Server \\\"{}\\\" can't be reached\".format(\n                self._base_url\n            ))\n\n    def validate_token(self):\n        try:\n            self._token_validation_started = True\n            # TODO add other possible validations\n            # - existence of 'user' key in info\n            # - validate that 'site_id' is in 'sites' in info\n            self.get_info()\n            self.get_user()\n            self._token_is_valid = True\n\n        except UnauthorizedError:\n            self._token_is_valid = False\n\n        finally:\n            self._token_validation_started = False\n        return self._token_is_valid\n\n    def set_token(self, token):\n        self.reset_token()\n        self._access_token = token\n        self.get_user()\n\n    def reset_token(self):\n        self._access_token = None\n        self._token_is_valid = None\n        self.close_session()\n\n    def create_session(self, ignore_existing=True, force=False):\n        \"\"\"Create a connection session.\n\n        Session helps to keep connection with server without\n            need to reconnect on each call.\n\n        Args:\n            ignore_existing (bool): If session already exists,\n                ignore creation.\n            force (bool): If session already exists, close it and\n                create new.\n        \"\"\"\n\n        if force and self._session is not None:\n            self.close_session()\n\n        if self._session is not None:\n            if ignore_existing:\n                return\n            raise ValueError(\"Session is already created.\")\n\n        self._as_user_stack.clear()\n        # Validate token before session creation\n        self.validate_token()\n\n        session = requests.Session()\n        session.cert = self._cert\n        session.verify = self._ssl_verify\n        session.headers.update(self.get_headers())\n\n        self._session_functions_mapping = {\n            RequestTypes.get: session.get,\n            RequestTypes.post: session.post,\n            RequestTypes.put: session.put,\n            RequestTypes.patch: session.patch,\n            RequestTypes.delete: session.delete\n        }\n        self._session = session\n\n    def close_session(self):\n        if self._session is None:\n            return\n\n        session = self._session\n        self._session = None\n        self._session_functions_mapping = {}\n        session.close()\n\n    def _update_session_headers(self):\n        if self._session is None:\n            return\n\n        # Header keys that may change over time\n        for key, value in (\n            (\"X-as-user\", self._as_user_stack.username),\n            (\"x-ayon-version\", self._client_version),\n            (\"x-ayon-site-id\", self._site_id),\n            (\"x-sender\", self._sender),\n        ):\n            if value is not None:\n                self._session.headers[key] = value\n            elif key in self._session.headers:\n                self._session.headers.pop(key)\n\n    def get_info(self):\n        \"\"\"Get information about current used api key.\n\n        By default, the 'info' contains only 'uptime' and 'version'. With\n        logged user info also contains information about user and machines on\n        which was logged in.\n\n        Todos:\n            Use this method for validation of token instead of 'get_user'.\n\n        Returns:\n            dict[str, Any]: Information from server.\n        \"\"\"\n\n        response = self.get(\"info\")\n        return response.data\n\n    def get_server_version(self):\n        \"\"\"Get server version.\n\n        Version should match semantic version (https://semver.org/).\n\n        Returns:\n            str: Server version.\n        \"\"\"\n\n        if self._server_version is None:\n            self._server_version = self.get_info()[\"version\"]\n        return self._server_version\n\n    def get_server_version_tuple(self):\n        \"\"\"Get server version as tuple.\n\n        Version should match semantic version (https://semver.org/).\n\n        This function only returns first three numbers of version.\n\n        Returns:\n            Tuple[int, int, int, Union[str, None], Union[str, None]]: Server\n                version.\n        \"\"\"\n\n        if self._server_version_tuple is None:\n            re_match = VERSION_REGEX.fullmatch(\n                self.get_server_version())\n            self._server_version_tuple = (\n                int(re_match.group(\"major\")),\n                int(re_match.group(\"minor\")),\n                int(re_match.group(\"patch\")),\n                re_match.group(\"prerelease\") or \"\",\n                re_match.group(\"buildmetadata\") or \"\",\n            )\n        return self._server_version_tuple\n\n    server_version = property(get_server_version)\n    server_version_tuple = property(get_server_version_tuple)\n\n    @property\n    def graphql_allows_data_in_query(self):\n        \"\"\"GraphlQl query can support 'data' field.\n\n        This applies only to project hierarchy entities 'project', 'folder',\n        'task', 'product', 'version' and 'representation'. Others like 'user'\n        still require to use rest api to access 'data'.\n\n        Returns:\n            bool: True if server supports 'data' field in GraphQl query.\n        \"\"\"\n\n        if self._graphql_allows_data_in_query is None:\n            major, minor, patch, _, _ = self.server_version_tuple\n            graphql_allows_data_in_query = True\n            if (major, minor, patch) < (0, 5, 5):\n                graphql_allows_data_in_query = False\n            self._graphql_allows_data_in_query = graphql_allows_data_in_query\n        return self._graphql_allows_data_in_query\n\n    def _get_user_info(self):\n        if self._access_token is None:\n            return None\n\n        if self._access_token_is_service is not None:\n            response = self.get(\"users/me\")\n            return response.data\n\n        self._access_token_is_service = False\n        response = self.get(\"users/me\")\n        if response.status == 200:\n            return response.data\n\n        self._access_token_is_service = True\n        response = self.get(\"users/me\")\n        if response.status == 200:\n            return response.data\n\n        self._access_token_is_service = None\n        return None\n\n    def get_users(self, usernames=None, fields=None):\n        \"\"\"Get Users.\n\n        Args:\n            usernames (Optional[Iterable[str]]): Filter by usernames.\n            fields (Optional[Iterable[str]]): fields to be queried\n                for users.\n\n        Returns:\n            Generator[dict[str, Any]]: Queried users.\n        \"\"\"\n\n        filters = {}\n        if usernames is not None:\n            usernames = set(usernames)\n            if not usernames:\n                return\n            filters[\"userNames\"] = list(usernames)\n\n        if not fields:\n            fields = self.get_default_fields_for_type(\"user\")\n\n        query = users_graphql_query(set(fields))\n        for attr, filter_value in filters.items():\n            query.set_variable_value(attr, filter_value)\n\n        for parsed_data in query.continuous_query(self):\n            for user in parsed_data[\"users\"]:\n                user[\"accessGroups\"] = json.loads(\n                    user[\"accessGroups\"])\n                yield user\n\n    def get_user(self, username=None):\n        output = None\n        if username is None:\n            output = self._get_user_info()\n        else:\n            response = self.get(\"users/{}\".format(username))\n            if response.status == 200:\n                output = response.data\n\n        if output is None:\n            raise UnauthorizedError(\"User is not authorized.\")\n        return output\n\n    def get_headers(self, content_type=None):\n        if content_type is None:\n            content_type = \"application/json\"\n\n        headers = {\n            \"Content-Type\": content_type,\n            \"x-ayon-platform\": platform.system().lower(),\n            \"x-ayon-hostname\": platform.node(),\n        }\n        if self._site_id is not None:\n            headers[\"x-ayon-site-id\"] = self._site_id\n\n        if self._client_version is not None:\n            headers[\"x-ayon-version\"] = self._client_version\n\n        if self._sender is not None:\n            headers[\"x-sender\"] = self._sender\n\n        if self._access_token:\n            if self._access_token_is_service:\n                headers[\"X-Api-Key\"] = self._access_token\n                username = self._as_user_stack.username\n                if username:\n                    headers[\"X-as-user\"] = username\n            else:\n                headers[\"Authorization\"] = \"Bearer {}\".format(\n                    self._access_token)\n        return headers\n\n    def login(self, username, password, create_session=True):\n        \"\"\"Login to server.\n\n        Args:\n            username (str): Username.\n            password (str): Password.\n            create_session (Optional[bool]): Create session after login.\n                Default: True.\n\n        Raises:\n            AuthenticationError: Login failed.\n        \"\"\"\n\n        if self.has_valid_token:\n            try:\n                user_info = self.get_user()\n            except UnauthorizedError:\n                user_info = {}\n\n            current_username = user_info.get(\"name\")\n            if current_username == username:\n                self.close_session()\n                if create_session:\n                    self.create_session()\n                return\n\n        self.reset_token()\n\n        self.validate_server_availability()\n\n        self._token_validation_started = True\n\n        try:\n            response = self.post(\n                \"auth/login\",\n                name=username,\n                password=password\n            )\n            if response.status_code != 200:\n                _detail = response.data.get(\"detail\")\n                details = \"\"\n                if _detail:\n                    details = \" {}\".format(_detail)\n\n                raise AuthenticationError(\"Login failed {}\".format(details))\n\n        finally:\n            self._token_validation_started = False\n\n        self._access_token = response[\"token\"]\n\n        if not self.has_valid_token:\n            raise AuthenticationError(\"Invalid credentials\")\n\n        if create_session:\n            self.create_session()\n\n    def logout(self, soft=False):\n        if self._access_token:\n            if not soft:\n                self._logout()\n            self.reset_token()\n\n    def _logout(self):\n        logout_from_server(self._base_url, self._access_token)\n\n    def _do_rest_request(self, function, url, **kwargs):\n        kwargs.setdefault(\"timeout\", self.timeout)\n        max_retries = kwargs.get(\"max_retries\", self.max_retries)\n        if max_retries < 1:\n            max_retries = 1\n        if self._session is None:\n            # Validate token if was not yet validated\n            #    - ignore validation if we're in middle of\n            #       validation\n            if (\n                self._token_is_valid is None\n                and not self._token_validation_started\n            ):\n                self.validate_token()\n\n            if \"headers\" not in kwargs:\n                kwargs[\"headers\"] = self.get_headers()\n\n            if isinstance(function, RequestType):\n                function = self._base_functions_mapping[function]\n\n        elif isinstance(function, RequestType):\n            function = self._session_functions_mapping[function]\n\n        response = None\n        new_response = None\n        for retry_idx in reversed(range(max_retries)):\n            try:\n                response = function(url, **kwargs)\n                break\n\n            except ConnectionRefusedError:\n                if retry_idx == 0:\n                    self.log.warning(\n                        \"Connection error happened.\", exc_info=True\n                    )\n\n                # Server may be restarting\n                new_response = RestApiResponse(\n                    None,\n                    {\"detail\": \"Unable to connect the server. Connection refused\"}\n                )\n\n            except requests.exceptions.Timeout:\n                # Connection timed out\n                new_response = RestApiResponse(\n                    None,\n                    {\"detail\": \"Connection timed out.\"}\n                )\n\n            except requests.exceptions.ConnectionError:\n                # Log warning only on last attempt\n                if retry_idx == 0:\n                    self.log.warning(\n                        \"Connection error happened.\", exc_info=True\n                    )\n\n                new_response = RestApiResponse(\n                    None,\n                    {\"detail\": \"Unable to connect the server. Connection error\"}\n                )\n\n            time.sleep(0.1)\n\n        if new_response is not None:\n            return new_response\n\n        content_type = response.headers.get(\"Content-Type\")\n        if content_type == \"application/json\":\n            try:\n                new_response = RestApiResponse(response)\n            except JSONDecodeError:\n                new_response = RestApiResponse(\n                    None,\n                    {\n                        \"detail\": \"The response is not a JSON: {}\".format(\n                            response.text)\n                    }\n                )\n\n        else:\n            new_response = RestApiResponse(response)\n\n        self.log.debug(\"Response {}\".format(str(new_response)))\n        return new_response\n\n    def raw_post(self, entrypoint, **kwargs):\n        entrypoint = entrypoint.lstrip(\"/\").rstrip(\"/\")\n        self.log.debug(\"Executing [POST] {}\".format(entrypoint))\n        url = \"{}/{}\".format(self._rest_url, entrypoint)\n        return self._do_rest_request(\n            RequestTypes.post,\n            url,\n            **kwargs\n        )\n\n    def raw_put(self, entrypoint, **kwargs):\n        entrypoint = entrypoint.lstrip(\"/\").rstrip(\"/\")\n        self.log.debug(\"Executing [PUT] {}\".format(entrypoint))\n        url = \"{}/{}\".format(self._rest_url, entrypoint)\n        return self._do_rest_request(\n            RequestTypes.put,\n            url,\n            **kwargs\n        )\n\n    def raw_patch(self, entrypoint, **kwargs):\n        entrypoint = entrypoint.lstrip(\"/\").rstrip(\"/\")\n        self.log.debug(\"Executing [PATCH] {}\".format(entrypoint))\n        url = \"{}/{}\".format(self._rest_url, entrypoint)\n        return self._do_rest_request(\n            RequestTypes.patch,\n            url,\n            **kwargs\n        )\n\n    def raw_get(self, entrypoint, **kwargs):\n        entrypoint = entrypoint.lstrip(\"/\").rstrip(\"/\")\n        self.log.debug(\"Executing [GET] {}\".format(entrypoint))\n        url = \"{}/{}\".format(self._rest_url, entrypoint)\n        return self._do_rest_request(\n            RequestTypes.get,\n            url,\n            **kwargs\n        )\n\n    def raw_delete(self, entrypoint, **kwargs):\n        entrypoint = entrypoint.lstrip(\"/\").rstrip(\"/\")\n        self.log.debug(\"Executing [DELETE] {}\".format(entrypoint))\n        url = \"{}/{}\".format(self._rest_url, entrypoint)\n        return self._do_rest_request(\n            RequestTypes.delete,\n            url,\n            **kwargs\n        )\n\n    def post(self, entrypoint, **kwargs):\n        return self.raw_post(entrypoint, json=kwargs)\n\n    def put(self, entrypoint, **kwargs):\n        return self.raw_put(entrypoint, json=kwargs)\n\n    def patch(self, entrypoint, **kwargs):\n        return self.raw_patch(entrypoint, json=kwargs)\n\n    def get(self, entrypoint, **kwargs):\n        return self.raw_get(entrypoint, params=kwargs)\n\n    def delete(self, entrypoint, **kwargs):\n        return self.raw_delete(entrypoint, params=kwargs)\n\n    def get_event(self, event_id):\n        \"\"\"Query full event data by id.\n\n        Events received using event server do not contain full information. To\n        get the full event information is required to receive it explicitly.\n\n        Args:\n            event_id (str): Id of event.\n\n        Returns:\n            dict[str, Any]: Full event data.\n        \"\"\"\n\n        response = self.get(\"events/{}\".format(event_id))\n        response.raise_for_status()\n        return response.data\n\n    def get_events(\n        self,\n        topics=None,\n        project_names=None,\n        states=None,\n        users=None,\n        include_logs=None,\n        has_children=None,\n        newer_than=None,\n        older_than=None,\n        fields=None\n    ):\n        \"\"\"Get events from server with filtering options.\n\n        Notes:\n            Not all event happen on a project.\n\n        Args:\n            topics (Optional[Iterable[str]]): Name of topics.\n            project_names (Optional[Iterable[str]]): Project on which\n                event happened.\n            states (Optional[Iterable[str]]): Filtering by states.\n            users (Optional[Iterable[str]]): Filtering by users\n                who created/triggered an event.\n            include_logs (Optional[bool]): Query also log events.\n            has_children (Optional[bool]): Event is with/without children\n                events. If 'None' then all events are returned, default.\n            newer_than (Optional[str]): Return only events newer than given\n                iso datetime string.\n            older_than (Optional[str]): Return only events older than given\n                iso datetime string.\n            fields (Optional[Iterable[str]]): Fields that should be received\n                for each event.\n\n        Returns:\n            Generator[dict[str, Any]]: Available events matching filters.\n        \"\"\"\n\n        filters = {}\n        if topics is not None:\n            topics = set(topics)\n            if not topics:\n                return\n            filters[\"eventTopics\"] = list(topics)\n\n        if project_names is not None:\n            project_names = set(project_names)\n            if not project_names:\n                return\n            filters[\"projectNames\"] = list(project_names)\n\n        if states is not None:\n            states = set(states)\n            if not states:\n                return\n            filters[\"eventStates\"] = list(states)\n\n        if users is not None:\n            users = set(users)\n            if not users:\n                return\n            filters[\"eventUsers\"] = list(users)\n\n        if include_logs is None:\n            include_logs = False\n        filters[\"includeLogsFilter\"] = include_logs\n\n        if has_children is not None:\n            filters[\"hasChildrenFilter\"] = has_children\n\n        if newer_than is not None:\n            filters[\"newerThanFilter\"] = newer_than\n\n        if older_than is not None:\n            filters[\"olderThanFilter\"] = older_than\n\n        if not fields:\n            fields = self.get_default_fields_for_type(\"event\")\n\n        query = events_graphql_query(set(fields))\n        for attr, filter_value in filters.items():\n            query.set_variable_value(attr, filter_value)\n\n        for parsed_data in query.continuous_query(self):\n            for event in parsed_data[\"events\"]:\n                yield event\n\n    def update_event(\n        self,\n        event_id,\n        sender=None,\n        project_name=None,\n        status=None,\n        description=None,\n        summary=None,\n        payload=None,\n        progress=None,\n        retries=None\n    ):\n        kwargs = {\n            key: value\n            for key, value in (\n                (\"sender\", sender),\n                (\"project\", project_name),\n                (\"status\", status),\n                (\"description\", description),\n                (\"summary\", summary),\n                (\"payload\", payload),\n                (\"progress\", progress),\n                (\"retries\", retries),\n            )\n            if value is not None\n        }\n        # 'progress' and 'retries' are available since 0.5.x server version\n        major, minor, _, _, _ = self.server_version_tuple\n        if (major, minor) < (0, 5):\n            args = []\n            if progress is not None:\n                args.append(\"progress\")\n            if retries is not None:\n                args.append(\"retries\")\n            fields = \", \".join(\"'{}'\".format(f) for f in args)\n            ending = \"s\" if len(args) > 1 else \"\"\n            raise ValueError((\n                 \"Your server version '{}' does not support update\"\n                 \" of {} field{} on event. The fields are supported since\"\n                 \" server version '0.5'.\"\n            ).format(self.get_server_version(), fields, ending))\n\n        response = self.patch(\n            \"events/{}\".format(event_id),\n            **kwargs\n        )\n        response.raise_for_status()\n\n    def dispatch_event(\n        self,\n        topic,\n        sender=None,\n        event_hash=None,\n        project_name=None,\n        username=None,\n        dependencies=None,\n        description=None,\n        summary=None,\n        payload=None,\n        finished=True,\n        store=True,\n    ):\n        \"\"\"Dispatch event to server.\n\n        Arg:\n            topic (str): Event topic used for filtering of listeners.\n            sender (Optional[str]): Sender of event.\n            hash (Optional[str]): Event hash.\n            project_name (Optional[str]): Project name.\n            username (Optional[str]): Username which triggered event.\n            dependencies (Optional[list[str]]): List of event id dependencies.\n            description (Optional[str]): Description of event.\n            summary (Optional[dict[str, Any]]): Summary of event that can be used\n                for simple filtering on listeners.\n            payload (Optional[dict[str, Any]]): Full payload of event data with\n                all details.\n            finished (Optional[bool]): Mark event as finished on dispatch.\n            store (Optional[bool]): Store event in event queue for possible\n                future processing otherwise is event send only\n                to active listeners.\n        \"\"\"\n\n        if summary is None:\n            summary = {}\n        if payload is None:\n            payload = {}\n        event_data = {\n            \"topic\": topic,\n            \"sender\": sender,\n            \"hash\": event_hash,\n            \"project\": project_name,\n            \"user\": username,\n            \"dependencies\": dependencies,\n            \"description\": description,\n            \"summary\": summary,\n            \"payload\": payload,\n            \"finished\": finished,\n            \"store\": store,\n        }\n        if self.post(\"events\", **event_data):\n            self.log.debug(\"Dispatched event {}\".format(topic))\n            return True\n        self.log.error(\"Unable to dispatch event {}\".format(topic))\n        return False\n\n    def enroll_event_job(\n        self,\n        source_topic,\n        target_topic,\n        sender,\n        description=None,\n        sequential=None,\n        events_filter=None,\n        max_retries=None,\n    ):\n        \"\"\"Enroll job based on events.\n\n        Enroll will find first unprocessed event with 'source_topic' and will\n        create new event with 'target_topic' for it and return the new event\n        data.\n\n        Use 'sequential' to control that only single target event is created\n        at same time. Creation of new target events is blocked while there is\n        at least one unfinished event with target topic, when set to 'True'.\n        This helps when order of events matter and more than one process using\n        the same target is running at the same time.\n        - Make sure the new event has updated status to '\"finished\"' status\n            when you're done with logic\n\n        Target topic should not clash with other processes/services.\n\n        Created target event have 'dependsOn' key where is id of source topic.\n\n        Use-case:\n            - Service 1 is creating events with topic 'my.leech'\n            - Service 2 process 'my.leech' and uses target topic 'my.process'\n                - this service can run on 1-n machines\n                - all events must be processed in a sequence by their creation\n                    time and only one event can be processed at a time\n                - in this case 'sequential' should be set to 'True' so only\n                    one machine is actually processing events, but if one goes\n                    down there are other that can take place\n            - Service 3 process 'my.leech' and uses target topic 'my.discover'\n                - this service can run on 1-n machines\n                - order of events is not important\n                - 'sequential' should be 'False'\n\n        Args:\n            source_topic (str): Source topic to enroll.\n            target_topic (str): Topic of dependent event.\n            sender (str): Identifier of sender (e.g. service name or username).\n            description (Optional[str]): Human readable text shown\n                in target event.\n            sequential (Optional[bool]): The source topic must be processed\n                in sequence.\n            events_filter (Optional[dict[str, Any]]): Filtering conditions\n                to filter the source event. For more technical specifications\n                look to server backed 'ayon_server.sqlfilter.Filter'.\n                TODO: Add example of filters.\n            max_retries (Optional[int]): How many times can be event retried.\n                Default value is based on server (3 at the time of this PR).\n\n        Returns:\n            Union[None, dict[str, Any]]: None if there is no event matching\n                filters. Created event with 'target_topic'.\n        \"\"\"\n\n        kwargs = {\n            \"sourceTopic\": source_topic,\n            \"targetTopic\": target_topic,\n            \"sender\": sender,\n        }\n        if max_retries is not None:\n            kwargs[\"maxRetries\"] = max_retries\n        if sequential is not None:\n            kwargs[\"sequential\"] = sequential\n        if description is not None:\n            kwargs[\"description\"] = description\n        if events_filter is not None:\n            kwargs[\"filter\"] = events_filter\n        response = self.post(\"enroll\", **kwargs)\n        if response.status_code == 204:\n            return None\n        elif response.status_code >= 400:\n            self.log.error(response.text)\n            return None\n\n        return response.data\n\n    def _download_file(self, url, filepath, chunk_size, progress):\n        dst_directory = os.path.dirname(filepath)\n        if not os.path.exists(dst_directory):\n            os.makedirs(dst_directory)\n\n        kwargs = {\"stream\": True}\n        if self._session is None:\n            kwargs[\"headers\"] = self.get_headers()\n            get_func = self._base_functions_mapping[RequestTypes.get]\n        else:\n            get_func = self._session_functions_mapping[RequestTypes.get]\n\n        with open(filepath, \"wb\") as f_stream:\n            with get_func(url, **kwargs) as response:\n                response.raise_for_status()\n                progress.set_content_size(response.headers[\"Content-length\"])\n                for chunk in response.iter_content(chunk_size=chunk_size):\n                    f_stream.write(chunk)\n                    progress.add_transferred_chunk(len(chunk))\n\n    def download_file(\n        self, endpoint, filepath, chunk_size=None, progress=None\n    ):\n        \"\"\"Download file from AYON server.\n\n        Endpoint can be full url (must start with 'base_url' of api object).\n\n        Progress object can be used to track download. Can be used when\n        download happens in thread and other thread want to catch changes over\n        time.\n\n        Todos:\n            Use retries and timeout.\n            Return RestApiResponse.\n\n        Args:\n            endpoint (str): Endpoint or URL to file that should be downloaded.\n            filepath (str): Path where file will be downloaded.\n            chunk_size (Optional[int]): Size of chunks that are received\n                in single loop.\n            progress (Optional[TransferProgress]): Object that gives ability\n                to track download progress.\n        \"\"\"\n\n        if not chunk_size:\n            chunk_size = self.default_download_chunk_size\n\n        if endpoint.startswith(self._base_url):\n            url = endpoint\n        else:\n            endpoint = endpoint.lstrip(\"/\").rstrip(\"/\")\n            url = \"{}/{}\".format(self._rest_url, endpoint)\n\n        # Create dummy object so the function does not have to check\n        #   'progress' variable everywhere\n        if progress is None:\n            progress = TransferProgress()\n\n        progress.set_source_url(url)\n        progress.set_destination_url(filepath)\n        progress.set_started()\n        try:\n            self._download_file(url, filepath, chunk_size, progress)\n\n        except Exception as exc:\n            progress.set_failed(str(exc))\n            raise\n\n        finally:\n            progress.set_transfer_done()\n        return progress\n\n    @staticmethod\n    def _upload_chunks_iter(file_stream, progress, chunk_size):\n        \"\"\"Generator that yields chunks of file.\n\n        Args:\n            file_stream (io.BinaryIO): Byte stream.\n            progress (TransferProgress): Object to track upload progress.\n            chunk_size (int): Size of chunks that are uploaded at once.\n\n        Yields:\n            bytes: Chunk of file.\n        \"\"\"\n\n        # Get size of file\n        file_stream.seek(0, io.SEEK_END)\n        size = file_stream.tell()\n        file_stream.seek(0)\n        # Set content size to progress object\n        progress.set_content_size(size)\n\n        while True:\n            chunk = file_stream.read(chunk_size)\n            if not chunk:\n                break\n            progress.add_transferred_chunk(len(chunk))\n            yield chunk\n\n    def _upload_file(\n        self,\n        url,\n        filepath,\n        progress,\n        request_type=None,\n        chunk_size=None,\n        **kwargs\n    ):\n        \"\"\"\n\n        Args:\n            url (str): Url where file will be uploaded.\n            filepath (str): Source filepath.\n            progress (TransferProgress): Object that gives ability to track\n                progress.\n            request_type (Optional[RequestType]): Type of request that will\n                be used. Default is PUT.\n            chunk_size (Optional[int]): Size of chunks that are uploaded\n                at once.\n            **kwargs (Any): Additional arguments that will be passed\n                to request function.\n\n        Returns:\n            RestApiResponse: Server response.\n        \"\"\"\n\n        if request_type is None:\n            request_type = RequestTypes.put\n\n        if self._session is None:\n            headers = kwargs.setdefault(\"headers\", {})\n            for key, value in self.get_headers().items():\n                if key not in headers:\n                    headers[key] = value\n            post_func = self._base_functions_mapping[request_type]\n        else:\n            post_func = self._session_functions_mapping[request_type]\n\n        if not chunk_size:\n            chunk_size = self.default_upload_chunk_size\n\n        with open(filepath, \"rb\") as stream:\n            response = post_func(\n                url,\n                data=self._upload_chunks_iter(stream, progress, chunk_size),\n                **kwargs\n            )\n\n        response.raise_for_status()\n        return response\n\n    def upload_file(\n        self, endpoint, filepath, progress=None, request_type=None, **kwargs\n    ):\n        \"\"\"Upload file to server.\n\n        Todos:\n            Use retries and timeout.\n            Return RestApiResponse.\n\n        Args:\n            endpoint (str): Endpoint or url where file will be uploaded.\n            filepath (str): Source filepath.\n            progress (Optional[TransferProgress]): Object that gives ability\n                to track upload progress.\n            request_type (Optional[RequestType]): Type of request that will\n                be used to upload file.\n            **kwargs (Any): Additional arguments that will be passed\n                to request function.\n\n        Returns:\n            requests.Response: Response object.\n        \"\"\"\n\n        if endpoint.startswith(self._base_url):\n            url = endpoint\n        else:\n            endpoint = endpoint.lstrip(\"/\").rstrip(\"/\")\n            url = \"{}/{}\".format(self._rest_url, endpoint)\n\n        # Create dummy object so the function does not have to check\n        #   'progress' variable everywhere\n        if progress is None:\n            progress = TransferProgress()\n\n        progress.set_source_url(filepath)\n        progress.set_destination_url(url)\n        progress.set_started()\n\n        try:\n            return self._upload_file(\n                url, filepath, progress, request_type, **kwargs\n            )\n\n        except Exception as exc:\n            progress.set_failed(str(exc))\n            raise\n\n        finally:\n            progress.set_transfer_done()\n\n    def trigger_server_restart(self):\n        \"\"\"Trigger server restart.\n\n        Restart may be required when a change of specific value happened on\n            server.\n        \"\"\"\n\n        result = self.post(\"system/restart\")\n        if result.status_code != 204:\n            # TODO add better exception\n            raise ValueError(\"Failed to restart server\")\n\n    def query_graphql(self, query, variables=None):\n        \"\"\"Execute GraphQl query.\n\n        Args:\n            query (str): GraphQl query string.\n            variables (Optional[dict[str, Any]): Variables that can be\n                used in query.\n\n        Returns:\n            GraphQlResponse: Response from server.\n        \"\"\"\n\n        data = {\"query\": query, \"variables\": variables or {}}\n        response = self._do_rest_request(\n            RequestTypes.post,\n            self._graphql_url,\n            json=data\n        )\n        response.raise_for_status()\n        return GraphQlResponse(response)\n\n    def get_graphql_schema(self):\n        return self.query_graphql(INTROSPECTION_QUERY).data\n\n    def get_server_schema(self):\n        \"\"\"Get server schema with info, url paths, components etc.\n\n        Todos:\n            Cache schema - How to find out it is outdated?\n\n        Returns:\n            dict[str, Any]: Full server schema.\n        \"\"\"\n\n        url = \"{}/openapi.json\".format(self._base_url)\n        response = self._do_rest_request(RequestTypes.get, url)\n        if response:\n            return response.data\n        return None\n\n    def get_schemas(self):\n        \"\"\"Get components schema.\n\n        Name of components does not match entity type names e.g. 'project' is\n        under 'ProjectModel'. We should find out some mapping. Also, there\n        are properties which don't have information about reference to object\n        e.g. 'config' has just object definition without reference schema.\n\n        Returns:\n            dict[str, Any]: Component schemas.\n        \"\"\"\n\n        server_schema = self.get_server_schema()\n        return server_schema[\"components\"][\"schemas\"]\n\n    def get_attributes_schema(self, use_cache=True):\n        if not use_cache:\n            self.reset_attributes_schema()\n\n        if self._attributes_schema is None:\n            result = self.get(\"attributes\")\n            if result.status_code != 200:\n                raise UnauthorizedError(\n                    \"User must be authorized to receive attributes\"\n                )\n            self._attributes_schema = result.data\n        return copy.deepcopy(self._attributes_schema)\n\n    def reset_attributes_schema(self):\n        self._attributes_schema = None\n        self._entity_type_attributes_cache = {}\n\n    def set_attribute_config(\n        self, attribute_name, data, scope, position=None, builtin=False\n    ):\n        if position is None:\n            attributes = self.get(\"attributes\").data[\"attributes\"]\n            origin_attr = next(\n                (\n                    attr for attr in attributes\n                    if attr[\"name\"] == attribute_name\n                ),\n                None\n            )\n            if origin_attr:\n                position = origin_attr[\"position\"]\n            else:\n                position = len(attributes)\n\n        response = self.put(\n            \"attributes/{}\".format(attribute_name),\n            data=data,\n            scope=scope,\n            position=position,\n            builtin=builtin\n        )\n        if response.status_code != 204:\n            # TODO raise different exception\n            raise ValueError(\n                \"Attribute \\\"{}\\\" was not created/updated. {}\".format(\n                    attribute_name, response.detail\n                )\n            )\n\n        self.reset_attributes_schema()\n\n    def remove_attribute_config(self, attribute_name):\n        \"\"\"Remove attribute from server.\n\n        This can't be un-done, please use carefully.\n\n        Args:\n            attribute_name (str): Name of attribute to remove.\n        \"\"\"\n\n        response = self.delete(\"attributes/{}\".format(attribute_name))\n        response.raise_for_status(\n            \"Attribute \\\"{}\\\" was not created/updated. {}\".format(\n                attribute_name, response.detail\n            )\n        )\n\n        self.reset_attributes_schema()\n\n    def get_attributes_for_type(self, entity_type):\n        \"\"\"Get attribute schemas available for an entity type.\n\n        ```\n        # Example attribute schema\n        {\n            # Common\n            \"type\": \"integer\",\n            \"title\": \"Clip Out\",\n            \"description\": null,\n            \"example\": 1,\n            \"default\": 1,\n            # These can be filled based on value of 'type'\n            \"gt\": null,\n            \"ge\": null,\n            \"lt\": null,\n            \"le\": null,\n            \"minLength\": null,\n            \"maxLength\": null,\n            \"minItems\": null,\n            \"maxItems\": null,\n            \"regex\": null,\n            \"enum\": null\n        }\n        ```\n\n        Args:\n            entity_type (str): Entity type for which should be attributes\n                received.\n\n        Returns:\n            dict[str, dict[str, Any]]: Attribute schemas that are available\n                for entered entity type.\n        \"\"\"\n        attributes = self._entity_type_attributes_cache.get(entity_type)\n        if attributes is None:\n            attributes_schema = self.get_attributes_schema()\n            attributes = {}\n            for attr in attributes_schema[\"attributes\"]:\n                if entity_type not in attr[\"scope\"]:\n                    continue\n                attr_name = attr[\"name\"]\n                attributes[attr_name] = attr[\"data\"]\n\n            self._entity_type_attributes_cache[entity_type] = attributes\n\n        return copy.deepcopy(attributes)\n\n    def get_attributes_fields_for_type(self, entity_type):\n        \"\"\"Prepare attribute fields for entity type.\n\n        Returns:\n            set[str]: Attributes fields for entity type.\n        \"\"\"\n\n        attributes = self.get_attributes_for_type(entity_type)\n        return {\n            \"attrib.{}\".format(attr)\n            for attr in attributes\n        }\n\n    def get_default_fields_for_type(self, entity_type):\n        \"\"\"Default fields for entity type.\n\n        Returns most of commonly used fields from server.\n\n        Args:\n            entity_type (str): Name of entity type.\n\n        Returns:\n            set[str]: Fields that should be queried from server.\n        \"\"\"\n\n        # Event does not have attributes\n        if entity_type == \"event\":\n            return set(DEFAULT_EVENT_FIELDS)\n\n        if entity_type == \"project\":\n            entity_type_defaults = set(DEFAULT_PROJECT_FIELDS)\n            if not self.graphql_allows_data_in_query:\n                entity_type_defaults.discard(\"data\")\n\n        elif entity_type == \"folder\":\n            entity_type_defaults = set(DEFAULT_FOLDER_FIELDS)\n            if not self.graphql_allows_data_in_query:\n                entity_type_defaults.discard(\"data\")\n\n        elif entity_type == \"task\":\n            entity_type_defaults = set(DEFAULT_TASK_FIELDS)\n            if not self.graphql_allows_data_in_query:\n                entity_type_defaults.discard(\"data\")\n\n        elif entity_type == \"product\":\n            entity_type_defaults = set(DEFAULT_PRODUCT_FIELDS)\n            if not self.graphql_allows_data_in_query:\n                entity_type_defaults.discard(\"data\")\n\n        elif entity_type == \"version\":\n            entity_type_defaults = set(DEFAULT_VERSION_FIELDS)\n            if not self.graphql_allows_data_in_query:\n                entity_type_defaults.discard(\"data\")\n\n        elif entity_type == \"representation\":\n            entity_type_defaults = (\n                DEFAULT_REPRESENTATION_FIELDS\n                | REPRESENTATION_FILES_FIELDS\n            )\n            if not self.graphql_allows_data_in_query:\n                entity_type_defaults.discard(\"data\")\n\n        elif entity_type == \"productType\":\n            entity_type_defaults = set(DEFAULT_PRODUCT_TYPE_FIELDS)\n\n        elif entity_type == \"workfile\":\n            entity_type_defaults = set(DEFAULT_WORKFILE_INFO_FIELDS)\n            if not self.graphql_allows_data_in_query:\n                entity_type_defaults.discard(\"data\")\n\n        elif entity_type == \"user\":\n            entity_type_defaults = set(DEFAULT_USER_FIELDS)\n\n        else:\n            raise ValueError(\"Unknown entity type \\\"{}\\\"\".format(entity_type))\n        return (\n            entity_type_defaults\n            | self.get_attributes_fields_for_type(entity_type)\n        )\n\n    def get_addons_info(self, details=True):\n        \"\"\"Get information about addons available on server.\n\n        Args:\n            details (Optional[bool]): Detailed data with information how\n                to get client code.\n        \"\"\"\n\n        endpoint = \"addons\"\n        if details:\n            endpoint += \"?details=1\"\n        response = self.get(endpoint)\n        response.raise_for_status()\n        return response.data\n\n    def get_addon_url(self, addon_name, addon_version, *subpaths):\n        \"\"\"Calculate url to addon route.\n\n        Example:\n            >>> api = ServerAPI(\"https://your.url.com\")\n            >>> api.get_addon_url(\n            ...     \"example\", \"1.0.0\", \"private\", \"my.zip\")\n            'https://your.url.com/addons/example/1.0.0/private/my.zip'\n\n        Args:\n            addon_name (str): Name of addon.\n            addon_version (str): Version of addon.\n            *subpaths (str): Any amount of subpaths that are added to\n                addon url.\n\n        Returns:\n            str: Final url.\n        \"\"\"\n\n        ending = \"\"\n        if subpaths:\n            ending = \"/{}\".format(\"/\".join(subpaths))\n        return \"{}/addons/{}/{}{}\".format(\n            self._base_url,\n            addon_name,\n            addon_version,\n            ending\n        )\n\n    def download_addon_private_file(\n        self,\n        addon_name,\n        addon_version,\n        filename,\n        destination_dir,\n        destination_filename=None,\n        chunk_size=None,\n        progress=None,\n    ):\n        \"\"\"Download a file from addon private files.\n\n        This method requires to have authorized token available. Private files\n        are not under '/api' restpoint.\n\n        Args:\n            addon_name (str): Addon name.\n            addon_version (str): Addon version.\n            filename (str): Filename in private folder on server.\n            destination_dir (str): Where the file should be downloaded.\n            destination_filename (Optional[str]): Name of destination\n                filename. Source filename is used if not passed.\n            chunk_size (Optional[int]): Download chunk size.\n            progress (Optional[TransferProgress]): Object that gives ability\n                to track download progress.\n\n        Returns:\n            str: Filepath to downloaded file.\n        \"\"\"\n\n        if not destination_filename:\n            destination_filename = filename\n        dst_filepath = os.path.join(destination_dir, destination_filename)\n        # Filename can contain \"subfolders\"\n        dst_dirpath = os.path.dirname(dst_filepath)\n        if not os.path.exists(dst_dirpath):\n            os.makedirs(dst_dirpath)\n\n        url = self.get_addon_url(\n            addon_name,\n            addon_version,\n            \"private\",\n            filename\n        )\n        self.download_file(\n            url, dst_filepath, chunk_size=chunk_size, progress=progress\n        )\n        return dst_filepath\n\n    def get_installers(self, version=None, platform_name=None):\n        \"\"\"Information about desktop application installers on server.\n\n        Desktop application installers are helpers to download/update AYON\n        desktop application for artists.\n\n        Args:\n            version (Optional[str]): Filter installers by version.\n            platform_name (Optional[str]): Filter installers by platform name.\n\n        Returns:\n            list[dict[str, Any]]:\n        \"\"\"\n\n        query_fields = [\n            \"{}={}\".format(key, value)\n            for key, value in (\n                (\"version\", version),\n                (\"platform\", platform_name),\n            )\n            if value\n        ]\n        query = \"\"\n        if query_fields:\n            query = \"?{}\".format(\",\".join(query_fields))\n\n        response = self.get(\"desktop/installers{}\".format(query))\n        response.raise_for_status()\n        return response.data\n\n    def create_installer(\n        self,\n        filename,\n        version,\n        python_version,\n        platform_name,\n        python_modules,\n        runtime_python_modules,\n        checksum,\n        checksum_algorithm,\n        file_size,\n        sources=None,\n    ):\n        \"\"\"Create new installer information on server.\n\n        This step will create only metadata. Make sure to upload installer\n            to the server using 'upload_installer' method.\n\n        Runtime python modules are modules that are required to run AYON\n            desktop application, but are not added to PYTHONPATH for any\n            subprocess.\n\n        Args:\n            filename (str): Installer filename.\n            version (str): Version of installer.\n            python_version (str): Version of Python.\n            platform_name (str): Name of platform.\n            python_modules (dict[str, str]): Python modules that are available\n                in installer.\n            runtime_python_modules (dict[str, str]): Runtime python modules\n                that are available in installer.\n            checksum (str): Installer file checksum.\n            checksum_algorithm (str): Type of checksum used to create checksum.\n            file_size (int): File size.\n            sources (Optional[list[dict[str, Any]]]): List of sources that\n                can be used to download file.\n        \"\"\"\n\n        body = {\n            \"filename\": filename,\n            \"version\": version,\n            \"pythonVersion\": python_version,\n            \"platform\": platform_name,\n            \"pythonModules\": python_modules,\n            \"runtimePythonModules\": runtime_python_modules,\n            \"checksum\": checksum,\n            \"checksumAlgorithm\": checksum_algorithm,\n            \"size\": file_size,\n        }\n        if sources:\n            body[\"sources\"] = sources\n\n        response = self.post(\"desktop/installers\", **body)\n        response.raise_for_status()\n\n    def update_installer(self, filename, sources):\n        \"\"\"Update installer information on server.\n\n        Args:\n            filename (str): Installer filename.\n            sources (list[dict[str, Any]]): List of sources that\n                can be used to download file. Fully replaces existing sources.\n        \"\"\"\n\n        response = self.patch(\n            \"desktop/installers/{}\".format(filename),\n            sources=sources\n        )\n        response.raise_for_status()\n\n    def delete_installer(self, filename):\n        \"\"\"Delete installer from server.\n\n        Args:\n            filename (str): Installer filename.\n        \"\"\"\n\n        response = self.delete(\"desktop/installers/{}\".format(filename))\n        response.raise_for_status()\n\n    def download_installer(\n        self,\n        filename,\n        dst_filepath,\n        chunk_size=None,\n        progress=None\n    ):\n        \"\"\"Download installer file from server.\n\n        Args:\n            filename (str): Installer filename.\n            dst_filepath (str): Destination filepath.\n            chunk_size (Optional[int]): Download chunk size.\n            progress (Optional[TransferProgress]): Object that gives ability\n                to track download progress.\n        \"\"\"\n\n        self.download_file(\n            \"desktop/installers/{}\".format(filename),\n            dst_filepath,\n            chunk_size=chunk_size,\n            progress=progress\n        )\n\n    def upload_installer(self, src_filepath, dst_filename, progress=None):\n        \"\"\"Upload installer file to server.\n\n        Args:\n            src_filepath (str): Source filepath.\n            dst_filename (str): Destination filename.\n            progress (Optional[TransferProgress]): Object that gives ability\n                to track download progress.\n\n        Returns:\n            requests.Response: Response object.\n        \"\"\"\n\n        return self.upload_file(\n            \"desktop/installers/{}\".format(dst_filename),\n            src_filepath,\n            progress=progress\n        )\n\n    def _get_dependency_package_route(self, filename=None):\n        endpoint = \"desktop/dependencyPackages\"\n        if filename:\n            return \"{}/{}\".format(endpoint, filename)\n        return endpoint\n\n    def get_dependency_packages(self):\n        \"\"\"Information about dependency packages on server.\n\n        To download dependency package, use 'download_dependency_package'\n        method and pass in 'filename'.\n\n        Example data structure:\n            {\n                \"packages\": [\n                    {\n                        \"filename\": str,\n                        \"platform\": str,\n                        \"checksum\": str,\n                        \"checksumAlgorithm\": str,\n                        \"size\": int,\n                        \"sources\": list[dict[str, Any]],\n                        \"supportedAddons\": dict[str, str],\n                        \"pythonModules\": dict[str, str]\n                    }\n                ]\n            }\n\n        Returns:\n            dict[str, Any]: Information about dependency packages known for\n                server.\n        \"\"\"\n\n        endpoint = self._get_dependency_package_route()\n        result = self.get(endpoint)\n        result.raise_for_status()\n        return result.data\n\n    def create_dependency_package(\n        self,\n        filename,\n        python_modules,\n        source_addons,\n        installer_version,\n        checksum,\n        checksum_algorithm,\n        file_size,\n        sources=None,\n        platform_name=None,\n    ):\n        \"\"\"Create dependency package on server.\n\n        The package will be created on a server, it is also required to upload\n        the package archive file (using 'upload_dependency_package').\n\n        Args:\n            filename (str): Filename of dependency package.\n            python_modules (dict[str, str]): Python modules in dependency\n                package.\n                '{\"<module name>\": \"<module version>\", ...}'\n            source_addons (dict[str, str]): Name of addons for which is\n                dependency package created.\n                '{\"<addon name>\": \"<addon version>\", ...}'\n            installer_version (str): Version of installer for which was\n                package created.\n            checksum (str): Checksum of archive file where dependencies are.\n            checksum_algorithm (str): Algorithm used to calculate checksum.\n            file_size (Optional[int]): Size of file.\n            sources (Optional[list[dict[str, Any]]]): Information about\n                sources from where it is possible to get file.\n            platform_name (Optional[str]): Name of platform for which is\n                dependency package targeted. Default value is\n                current platform.\n        \"\"\"\n\n        post_body = {\n            \"filename\": filename,\n            \"pythonModules\": python_modules,\n            \"sourceAddons\": source_addons,\n            \"installerVersion\": installer_version,\n            \"checksum\": checksum,\n            \"checksumAlgorithm\": checksum_algorithm,\n            \"size\": file_size,\n            \"platform\": platform_name or platform.system().lower(),\n        }\n        if sources:\n            post_body[\"sources\"] = sources\n\n        route = self._get_dependency_package_route()\n        response = self.post(route, **post_body)\n        response.raise_for_status()\n\n    def update_dependency_package(self, filename, sources):\n        \"\"\"Update dependency package metadata on server.\n\n        Args:\n            filename (str): Filename of dependency package.\n            sources (list[dict[str, Any]]): Information about\n                sources from where it is possible to get file. Fully replaces\n                existing sources.\n        \"\"\"\n\n        response = self.patch(\n            self._get_dependency_package_route(filename),\n            sources=sources\n        )\n        response.raise_for_status()\n\n    def delete_dependency_package(self, filename, platform_name=None):\n        \"\"\"Remove dependency package for specific platform.\n\n        Args:\n            filename (str): Filename of dependency package.\n            platform_name (Optional[str]): Deprecated.\n        \"\"\"\n\n        if platform_name is not None:\n            warnings.warn(\n                (\n                    \"Argument 'platform_name' is deprecated in\"\n                    \" 'delete_dependency_package'. The argument will be\"\n                    \" removed, please modify your code accordingly.\"\n                ),\n                DeprecationWarning\n            )\n\n        route = self._get_dependency_package_route(filename)\n        response = self.delete(route)\n        response.raise_for_status(\"Failed to delete dependency file\")\n        return response.data\n\n    def download_dependency_package(\n        self,\n        src_filename,\n        dst_directory,\n        dst_filename,\n        platform_name=None,\n        chunk_size=None,\n        progress=None,\n    ):\n        \"\"\"Download dependency package from server.\n\n        This method requires to have authorized token available. The package\n        is only downloaded.\n\n        Args:\n            src_filename (str): Filename of dependency pacakge.\n                For server version 0.2.0 and lower it is name of package\n                to download.\n            dst_directory (str): Where the file should be downloaded.\n            dst_filename (str): Name of destination filename.\n            platform_name (Optional[str]): Deprecated.\n            chunk_size (Optional[int]): Download chunk size.\n            progress (Optional[TransferProgress]): Object that gives ability\n                to track download progress.\n\n        Returns:\n            str: Filepath to downloaded file.\n        \"\"\"\n\n        if platform_name is not None:\n            warnings.warn(\n                (\n                    \"Argument 'platform_name' is deprecated in\"\n                    \" 'download_dependency_package'. The argument will be\"\n                    \" removed, please modify your code accordingly.\"\n                ),\n                DeprecationWarning\n            )\n        route = self._get_dependency_package_route(src_filename)\n        package_filepath = os.path.join(dst_directory, dst_filename)\n        self.download_file(\n            route,\n            package_filepath,\n            chunk_size=chunk_size,\n            progress=progress\n        )\n        return package_filepath\n\n    def upload_dependency_package(\n        self, src_filepath, dst_filename, platform_name=None, progress=None\n    ):\n        \"\"\"Upload dependency package to server.\n\n        Args:\n            src_filepath (str): Path to a package file.\n            dst_filename (str): Dependency package filename or name of package\n                for server version 0.2.0 or lower. Must be unique.\n            platform_name (Optional[str]): Deprecated.\n            progress (Optional[TransferProgress]): Object to keep track about\n                upload state.\n        \"\"\"\n\n        if platform_name is not None:\n            warnings.warn(\n                (\n                    \"Argument 'platform_name' is deprecated in\"\n                    \" 'upload_dependency_package'. The argument will be\"\n                    \" removed, please modify your code accordingly.\"\n                ),\n                DeprecationWarning\n            )\n\n        route = self._get_dependency_package_route(dst_filename)\n        self.upload_file(route, src_filepath, progress=progress)\n\n    def upload_addon_zip(self, src_filepath, progress=None):\n        \"\"\"Upload addon zip file to server.\n\n        File is validated on server. If it is valid, it is installed. It will\n            create an event job which can be tracked (tracking part is not\n            implemented yet).\n\n        Example output:\n            {'eventId': 'a1bfbdee27c611eea7580242ac120003'}\n\n        Args:\n            src_filepath (str): Path to a zip file.\n            progress (Optional[TransferProgress]): Object to keep track about\n                upload state.\n\n        Returns:\n            dict[str, Any]: Response data from server.\n        \"\"\"\n\n        response = self.upload_file(\n            \"addons/install\",\n            src_filepath,\n            progress=progress,\n            request_type=RequestTypes.post,\n        )\n        return response.json()\n\n    def get_bundles(self):\n        \"\"\"Server bundles with basic information.\n\n        Example output:\n            {\n                \"bundles\": [\n                    {\n                        \"name\": \"my_bundle\",\n                        \"createdAt\": \"2023-06-12T15:37:02.420260\",\n                        \"installerVersion\": \"1.0.0\",\n                        \"addons\": {\n                            \"core\": \"1.2.3\"\n                        },\n                        \"dependencyPackages\": {\n                            \"windows\": \"a_windows_package123.zip\",\n                            \"linux\": \"a_linux_package123.zip\",\n                            \"darwin\": \"a_mac_package123.zip\"\n                        },\n                        \"isProduction\": False,\n                        \"isStaging\": False\n                    }\n                ],\n                \"productionBundle\": \"my_bundle\",\n                \"stagingBundle\": \"test_bundle\"\n            }\n\n        Returns:\n            dict[str, Any]: Server bundles with basic information.\n        \"\"\"\n\n        response = self.get(\"bundles\")\n        response.raise_for_status()\n        return response.data\n\n    def create_bundle(\n        self,\n        name,\n        addon_versions,\n        installer_version,\n        dependency_packages=None,\n        is_production=None,\n        is_staging=None\n    ):\n        \"\"\"Create bundle on server.\n\n        Bundle cannot be changed once is created. Only isProduction, isStaging\n        and dependency packages can change after creation.\n\n        Args:\n            name (str): Name of bundle.\n            addon_versions (dict[str, str]): Addon versions.\n            installer_version (Union[str, None]): Installer version.\n            dependency_packages (Optional[dict[str, str]]): Dependency\n                package names. Keys are platform names and values are name of\n                packages.\n            is_production (Optional[bool]): Bundle will be marked as\n                production.\n            is_staging (Optional[bool]): Bundle will be marked as staging.\n        \"\"\"\n\n        body = {\n            \"name\": name,\n            \"installerVersion\": installer_version,\n            \"addons\": addon_versions,\n        }\n        for key, value in (\n            (\"dependencyPackages\", dependency_packages),\n            (\"isProduction\", is_production),\n            (\"isStaging\", is_staging),\n        ):\n            if value is not None:\n                body[key] = value\n\n        response = self.post(\"bundles\", **body)\n        response.raise_for_status()\n\n    def update_bundle(\n        self,\n        bundle_name,\n        dependency_packages=None,\n        is_production=None,\n        is_staging=None\n    ):\n        \"\"\"Update bundle on server.\n\n        Dependency packages can be update only for single platform. Others\n        will be left untouched. Use 'None' value to unset dependency package\n        from bundle.\n\n        Args:\n            bundle_name (str): Name of bundle.\n            dependency_packages (Optional[dict[str, str]]): Dependency pacakge\n                names that should be used with the bundle.\n            is_production (Optional[bool]): Bundle will be marked as\n                production.\n            is_staging (Optional[bool]): Bundle will be marked as staging.\n        \"\"\"\n\n        body = {\n            key: value\n            for key, value in (\n                (\"dependencyPackages\", dependency_packages),\n                (\"isProduction\", is_production),\n                (\"isStaging\", is_staging),\n            )\n            if value is not None\n        }\n        response = self.patch(\n            \"{}/{}\".format(\"bundles\", bundle_name),\n            **body\n        )\n        response.raise_for_status()\n\n    def delete_bundle(self, bundle_name):\n        \"\"\"Delete bundle from server.\n\n        Args:\n            bundle_name (str): Name of bundle to delete.\n        \"\"\"\n\n        response = self.delete(\n            \"{}/{}\".format(\"bundles\", bundle_name)\n        )\n        response.raise_for_status()\n\n    # Anatomy presets\n    def get_project_anatomy_presets(self):\n        \"\"\"Anatomy presets available on server.\n\n        Content has basic information about presets. Example output:\n            [\n                {\n                    \"name\": \"netflix_VFX\",\n                    \"primary\": false,\n                    \"version\": \"1.0.0\"\n                },\n                {\n                    ...\n                },\n                ...\n            ]\n\n        Returns:\n            list[dict[str, str]]: Anatomy presets available on server.\n        \"\"\"\n\n        result = self.get(\"anatomy/presets\")\n        result.raise_for_status()\n        return result.data.get(\"presets\") or []\n\n    def get_project_anatomy_preset(self, preset_name=None):\n        \"\"\"Anatomy preset values by name.\n\n        Get anatomy preset values by preset name. Primary preset is returned\n        if preset name is set to 'None'.\n\n        Args:\n            preset_name (Optional[str]): Preset name.\n\n        Returns:\n            dict[str, Any]: Anatomy preset values.\n        \"\"\"\n\n        if preset_name is None:\n            preset_name = \"_\"\n        result = self.get(\"anatomy/presets/{}\".format(preset_name))\n        result.raise_for_status()\n        return result.data\n\n    def get_project_roots_by_site(self, project_name):\n        \"\"\"Root overrides per site name.\n\n        Method is based on logged user and can't be received for any other\n        user on server.\n\n        Output will contain only roots per site id used by logged user.\n\n        Args:\n            project_name (str): Name of project.\n\n        Returns:\n             dict[str, dict[str, str]]: Root values by root name by site id.\n        \"\"\"\n\n        result = self.get(\"projects/{}/roots\".format(project_name))\n        result.raise_for_status()\n        return result.data\n\n    def get_project_roots_for_site(self, project_name, site_id=None):\n        \"\"\"Root overrides for site.\n\n        If site id is not passed a site set in current api object is used\n        instead.\n\n        Args:\n            project_name (str): Name of project.\n            site_id (Optional[str]): Id of site for which want to receive\n                site overrides.\n\n        Returns:\n            dict[str, str]: Root values by root name or None if\n                site does not have overrides.\n        \"\"\"\n\n        if site_id is None:\n            site_id = self.site_id\n\n        if site_id is None:\n            return {}\n        roots = self.get_project_roots_by_site(project_name)\n        return roots.get(site_id, {})\n\n    def get_addon_settings_schema(\n        self, addon_name, addon_version, project_name=None\n    ):\n        \"\"\"Sudio/Project settings schema of an addon.\n\n        Project schema may look differently as some enums are based on project\n        values.\n\n        Args:\n            addon_name (str): Name of addon.\n            addon_version (str): Version of addon.\n            project_name (Optional[str]): Schema for specific project or\n                default studio schemas.\n\n        Returns:\n            dict[str, Any]: Schema of studio/project settings.\n        \"\"\"\n\n        args = tuple()\n        if project_name:\n            args = (project_name, )\n\n        endpoint = self.get_addon_url(\n            addon_name, addon_version, \"schema\", *args\n        )\n        result = self.get(endpoint)\n        result.raise_for_status()\n        return result.data\n\n    def get_addon_site_settings_schema(self, addon_name, addon_version):\n        \"\"\"Site settings schema of an addon.\n\n        Args:\n            addon_name (str): Name of addon.\n            addon_version (str): Version of addon.\n\n        Returns:\n            dict[str, Any]: Schema of site settings.\n        \"\"\"\n\n        result = self.get(\"addons/{}/{}/siteSettings/schema\".format(\n            addon_name, addon_version\n        ))\n        result.raise_for_status()\n        return result.data\n\n    def get_addon_studio_settings(\n        self,\n        addon_name,\n        addon_version,\n        variant=None\n    ):\n        \"\"\"Addon studio settings.\n\n        Receive studio settings for specific version of an addon.\n\n        Args:\n            addon_name (str): Name of addon.\n            addon_version (str): Version of addon.\n            variant (Optional[Literal['production', 'staging']]): Name of\n                settings variant. Used 'default_settings_variant' by default.\n\n        Returns:\n           dict[str, Any]: Addon settings.\n        \"\"\"\n\n        if variant is None:\n            variant = self.default_settings_variant\n\n        query_items = {}\n        if variant:\n            query_items[\"variant\"] = variant\n        query = prepare_query_string(query_items)\n\n        result = self.get(\n            \"addons/{}/{}/settings{}\".format(addon_name, addon_version, query)\n        )\n        result.raise_for_status()\n        return result.data\n\n    def get_addon_project_settings(\n        self,\n        addon_name,\n        addon_version,\n        project_name,\n        variant=None,\n        site_id=None,\n        use_site=True\n    ):\n        \"\"\"Addon project settings.\n\n        Receive project settings for specific version of an addon. The settings\n        may be with site overrides when enabled.\n\n        Site id is filled with current connection site id if not passed. To\n        make sure any site id is used set 'use_site' to 'False'.\n\n        Args:\n            addon_name (str): Name of addon.\n            addon_version (str): Version of addon.\n            project_name (str): Name of project for which the settings are\n                received.\n            variant (Optional[Literal['production', 'staging']]): Name of\n                settings variant. Used 'default_settings_variant' by default.\n            site_id (Optional[str]): Name of site which is used for site\n                overrides. Is filled with connection 'site_id' attribute\n                if not passed.\n            use_site (Optional[bool]): To force disable option of using site\n                overrides set to 'False'. In that case won't be applied\n                any site overrides.\n\n        Returns:\n            dict[str, Any]: Addon settings.\n        \"\"\"\n\n        if not use_site:\n            site_id = None\n        elif not site_id:\n            site_id = self.site_id\n\n        query_items = {}\n        if site_id:\n            query_items[\"site\"] = site_id\n\n        if variant is None:\n            variant = self.default_settings_variant\n\n        if variant:\n            query_items[\"variant\"] = variant\n\n        query = prepare_query_string(query_items)\n        result = self.get(\n            \"addons/{}/{}/settings/{}{}\".format(\n                addon_name, addon_version, project_name, query\n            )\n        )\n        result.raise_for_status()\n        return result.data\n\n    def get_addon_settings(\n        self,\n        addon_name,\n        addon_version,\n        project_name=None,\n        variant=None,\n        site_id=None,\n        use_site=True\n    ):\n        \"\"\"Receive addon settings.\n\n        Receive addon settings based on project name value. Some arguments may\n        be ignored if 'project_name' is set to 'None'.\n\n        Args:\n            addon_name (str): Name of addon.\n            addon_version (str): Version of addon.\n            project_name (Optional[str]): Name of project for which the\n                settings are received. A studio settings values are received\n                if is 'None'.\n            variant (Optional[Literal['production', 'staging']]): Name of\n                settings variant. Used 'default_settings_variant' by default.\n            site_id (Optional[str]): Name of site which is used for site\n                overrides. Is filled with connection 'site_id' attribute\n                if not passed.\n            use_site (Optional[bool]): To force disable option of using\n                site overrides set to 'False'. In that case won't be applied\n                any site overrides.\n\n        Returns:\n            dict[str, Any]: Addon settings.\n        \"\"\"\n\n        if project_name is None:\n            return self.get_addon_studio_settings(\n                addon_name, addon_version, variant\n            )\n        return self.get_addon_project_settings(\n            addon_name, addon_version, project_name, variant, site_id, use_site\n        )\n\n    def get_addon_site_settings(\n        self, addon_name, addon_version, site_id=None\n    ):\n        \"\"\"Site settings of an addon.\n\n        If site id is not available an empty dictionary is returned.\n\n        Args:\n            addon_name (str): Name of addon.\n            addon_version (str): Version of addon.\n            site_id (Optional[str]): Name of site for which should be settings\n                returned. using 'site_id' attribute if not passed.\n\n        Returns:\n            dict[str, Any]: Site settings.\n        \"\"\"\n\n        if site_id is None:\n            site_id = self.site_id\n\n        if not site_id:\n            return {}\n\n        query = prepare_query_string({\"site\": site_id})\n        result = self.get(\"addons/{}/{}/siteSettings{}\".format(\n            addon_name, addon_version, query\n        ))\n        result.raise_for_status()\n        return result.data\n\n    def get_bundle_settings(\n        self,\n        bundle_name=None,\n        project_name=None,\n        variant=None,\n        site_id=None,\n        use_site=True\n    ):\n        \"\"\"Get complete set of settings for given data.\n\n        If project is not passed then studio settings are returned. If variant\n        is not passed 'default_settings_variant' is used. If bundle name is\n        not passed then current production/staging bundle is used, based on\n        variant value.\n\n        Output contains addon settings and site settings in single dictionary.\n\n        TODOs:\n            - test how it behaves if there is not any bundle.\n            - test how it behaves if there is not any production/staging\n                bundle.\n\n        Example output:\n            {\n                \"addons\": [\n                    {\n                        \"name\": \"addon-name\",\n                        \"version\": \"addon-version\",\n                        \"settings\": {...},\n                        \"siteSettings\": {...}\n                    }\n                ]\n            }\n\n        Returns:\n            dict[str, Any]: All settings for single bundle.\n        \"\"\"\n\n        query_values = {\n            key: value\n            for key, value in (\n                (\"project_name\", project_name),\n                (\"variant\", variant or self.default_settings_variant),\n                (\"bundle_name\", bundle_name),\n            )\n            if value\n        }\n        if use_site:\n            if not site_id:\n                site_id = self.site_id\n            if site_id:\n                query_values[\"site_id\"] = site_id\n\n        query = prepare_query_string(query_values)\n        response = self.get(\"settings{}\".format(query))\n        response.raise_for_status()\n        return response.data\n\n    def get_addons_studio_settings(\n        self,\n        bundle_name=None,\n        variant=None,\n        site_id=None,\n        use_site=True,\n        only_values=True\n    ):\n        \"\"\"All addons settings in one bulk.\n\n        Warnings:\n            Behavior of this function changed with AYON server version 0.3.0.\n                Structure of output from server changed. If using\n                'only_values=True' then output should be same as before.\n\n        Args:\n            bundle_name (Optional[str]): Name of bundle for which should be\n                settings received.\n            variant (Optional[Literal['production', 'staging']]): Name of\n                settings variant. Used 'default_settings_variant' by default.\n            site_id (Optional[str]): Id of site for which want to receive\n                site overrides.\n            use_site (bool): To force disable option of using site overrides\n                set to 'False'. In that case won't be applied any site\n                overrides.\n            only_values (Optional[bool]): Output will contain only settings\n                values without metadata about addons.\n\n        Returns:\n            dict[str, Any]: Settings of all addons on server.\n        \"\"\"\n\n        output = self.get_bundle_settings(\n            bundle_name=bundle_name,\n            variant=variant,\n            site_id=site_id,\n            use_site=use_site\n        )\n        if only_values:\n            output = {\n                addon[\"name\"]: addon[\"settings\"]\n                for addon in output[\"addons\"]\n            }\n        return output\n\n    def get_addons_project_settings(\n        self,\n        project_name,\n        bundle_name=None,\n        variant=None,\n        site_id=None,\n        use_site=True,\n        only_values=True\n    ):\n        \"\"\"Project settings of all addons.\n\n        Server returns information about used addon versions, so full output\n        looks like:\n            {\n                \"settings\": {...},\n                \"addons\": {...}\n            }\n\n        The output can be limited to only values. To do so is 'only_values'\n        argument which is by default set to 'True'. In that case output\n        contains only value of 'settings' key.\n\n        Warnings:\n            Behavior of this function changed with AYON server version 0.3.0.\n                Structure of output from server changed. If using\n                'only_values=True' then output should be same as before.\n\n        Args:\n            project_name (str): Name of project for which are settings\n                received.\n            bundle_name (Optional[str]): Name of bundle for which should be\n                settings received.\n            variant (Optional[Literal['production', 'staging']]): Name of\n                settings variant. Used 'default_settings_variant' by default.\n            site_id (Optional[str]): Id of site for which want to receive\n                site overrides.\n            use_site (bool): To force disable option of using site overrides\n                set to 'False'. In that case won't be applied any site\n                overrides.\n            only_values (Optional[bool]): Output will contain only settings\n                values without metadata about addons.\n\n        Returns:\n            dict[str, Any]: Settings of all addons on server for passed\n                project.\n        \"\"\"\n\n        if not project_name:\n            raise ValueError(\"Project name must be passed.\")\n\n        output = self.get_bundle_settings(\n            project_name=project_name,\n            bundle_name=bundle_name,\n            variant=variant,\n            site_id=site_id,\n            use_site=use_site\n        )\n        if only_values:\n            output = {\n                addon[\"name\"]: addon[\"settings\"]\n                for addon in output[\"addons\"]\n            }\n        return output\n\n    def get_addons_settings(\n        self,\n        bundle_name=None,\n        project_name=None,\n        variant=None,\n        site_id=None,\n        use_site=True,\n        only_values=True\n    ):\n        \"\"\"Universal function to receive all addon settings.\n\n        Based on 'project_name' will receive studio settings or project\n        settings. In case project is not passed is 'site_id' ignored.\n\n        Warnings:\n            Behavior of this function changed with AYON server version 0.3.0.\n                Structure of output from server changed. If using\n                'only_values=True' then output should be same as before.\n\n        Args:\n            bundle_name (Optional[str]): Name of bundle for which should be\n                settings received.\n            project_name (Optional[str]): Name of project for which should be\n                settings received.\n            variant (Optional[Literal['production', 'staging']]): Name of\n                settings variant. Used 'default_settings_variant' by default.\n            site_id (Optional[str]): Id of site for which want to receive\n                site overrides.\n            use_site (Optional[bool]): To force disable option of using site\n                overrides set to 'False'. In that case won't be applied\n                any site overrides.\n            only_values (Optional[bool]): Only settings values will be\n                returned. By default, is set to 'True'.\n        \"\"\"\n\n        if project_name is None:\n            return self.get_addons_studio_settings(\n                bundle_name=bundle_name,\n                variant=variant,\n                site_id=site_id,\n                use_site=use_site,\n                only_values=only_values\n            )\n\n        return self.get_addons_project_settings(\n            project_name=project_name,\n            bundle_name=bundle_name,\n            variant=variant,\n            site_id=site_id,\n            use_site=use_site,\n            only_values=only_values\n        )\n\n    def get_secrets(self):\n        \"\"\"Get all secrets.\n\n        Example output:\n            [\n                {\n                    \"name\": \"secret_1\",\n                    \"value\": \"secret_value_1\",\n                },\n                {\n                    \"name\": \"secret_2\",\n                    \"value\": \"secret_value_2\",\n                }\n            ]\n\n        Returns:\n            list[dict[str, str]]: List of secret entities.\n        \"\"\"\n\n        response = self.get(\"secrets\")\n        response.raise_for_status()\n        return response.data\n\n    def get_secret(self, secret_name):\n        \"\"\"Get secret by name.\n\n        Example output:\n            {\n                \"name\": \"secret_name\",\n                \"value\": \"secret_value\",\n            }\n\n        Args:\n            secret_name (str): Name of secret.\n\n        Returns:\n            dict[str, str]: Secret entity data.\n        \"\"\"\n\n        response = self.get(\"secrets/{}\".format(secret_name))\n        response.raise_for_status()\n        return response.data\n\n    def save_secret(self, secret_name, secret_value):\n        \"\"\"Save secret.\n\n        This endpoint can create and update secret.\n\n        Args:\n            secret_name (str): Name of secret.\n            secret_value (str): Value of secret.\n        \"\"\"\n\n        response = self.put(\n            \"secrets/{}\".format(secret_name),\n            name=secret_name,\n            value=secret_value,\n        )\n        response.raise_for_status()\n        return response.data\n\n\n    def delete_secret(self, secret_name):\n        \"\"\"Delete secret by name.\n\n        Args:\n            secret_name (str): Name of secret to delete.\n        \"\"\"\n\n        response = self.delete(\"secrets/{}\".format(secret_name))\n        response.raise_for_status()\n        return response.data\n\n    # Entity getters\n    def get_rest_project(self, project_name):\n        \"\"\"Query project by name.\n\n        This call returns project with anatomy data.\n\n        Args:\n            project_name (str): Name of project.\n\n        Returns:\n            Union[dict[str, Any], None]: Project entity data or 'None' if\n                project was not found.\n        \"\"\"\n\n        if not project_name:\n            return None\n\n        response = self.get(\"projects/{}\".format(project_name))\n        if response.status == 200:\n            return response.data\n        return None\n\n    def get_rest_projects(self, active=True, library=None):\n        \"\"\"Query available project entities.\n\n        User must be logged in.\n\n        Args:\n            active (Optional[bool]): Filter active/inactive projects. Both\n                are returned if 'None' is passed.\n            library (Optional[bool]): Filter standard/library projects. Both\n                are returned if 'None' is passed.\n\n        Returns:\n            Generator[dict[str, Any]]: Available projects.\n        \"\"\"\n\n        for project_name in self.get_project_names(active, library):\n            project = self.get_rest_project(project_name)\n            if project:\n                yield project\n\n    def get_rest_entity_by_id(self, project_name, entity_type, entity_id):\n        \"\"\"Get entity using REST on a project by its id.\n\n        Args:\n            project_name (str): Name of project where entity is.\n            entity_type (Literal[\"folder\", \"task\", \"product\", \"version\"]): The\n                entity type which should be received.\n            entity_id (str): Id of entity.\n\n        Returns:\n            dict[str, Any]: Received entity data.\n        \"\"\"\n\n        if not all((project_name, entity_type, entity_id)):\n            return None\n\n        entity_endpoint = \"{}s\".format(entity_type)\n        response = self.get(\"projects/{}/{}/{}\".format(\n            project_name, entity_endpoint, entity_id\n        ))\n        if response.status == 200:\n            return response.data\n        return None\n\n    def get_rest_folder(self, project_name, folder_id):\n        return self.get_rest_entity_by_id(project_name, \"folder\", folder_id)\n\n    def get_rest_task(self, project_name, task_id):\n        return self.get_rest_entity_by_id(project_name, \"task\", task_id)\n\n    def get_rest_product(self, project_name, product_id):\n        return self.get_rest_entity_by_id(project_name, \"product\", product_id)\n\n    def get_rest_version(self, project_name, version_id):\n        return self.get_rest_entity_by_id(project_name, \"version\", version_id)\n\n    def get_rest_representation(self, project_name, representation_id):\n        return self.get_rest_entity_by_id(\n            project_name, \"representation\", representation_id\n        )\n\n    def get_project_names(self, active=True, library=None):\n        \"\"\"Receive available project names.\n\n        User must be logged in.\n\n        Args:\n            active (Optional[bool]): Filter active/inactive projects. Both\n                are returned if 'None' is passed.\n            library (Optional[bool]): Filter standard/library projects. Both\n                are returned if 'None' is passed.\n\n        Returns:\n            list[str]: List of available project names.\n        \"\"\"\n\n        query_keys = {}\n        if active is not None:\n            query_keys[\"active\"] = \"true\" if active else \"false\"\n\n        if library is not None:\n            query_keys[\"library\"] = \"true\" if library else \"false\"\n        query = \"\"\n        if query_keys:\n            query = \"?{}\".format(\",\".join([\n                \"{}={}\".format(key, value)\n                for key, value in query_keys.items()\n            ]))\n\n        response = self.get(\"projects{}\".format(query), **query_keys)\n        response.raise_for_status()\n        data = response.data\n        project_names = []\n        if data:\n            for project in data[\"projects\"]:\n                project_names.append(project[\"name\"])\n        return project_names\n\n    def get_projects(\n        self, active=True, library=None, fields=None, own_attributes=False\n    ):\n        \"\"\"Get projects.\n\n        Args:\n            active (Optional[bool]): Filter active or inactive projects.\n                Filter is disabled when 'None' is passed.\n            library (Optional[bool]): Filter library projects. Filter is\n                disabled when 'None' is passed.\n            fields (Optional[Iterable[str]]): fields to be queried\n                for project.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Generator[dict[str, Any]]: Queried projects.\n        \"\"\"\n\n        if fields is None:\n            use_rest = True\n        else:\n            use_rest = False\n            fields = set(fields)\n            for field in fields:\n                if field.startswith(\"config\"):\n                    use_rest = True\n                    break\n\n        if use_rest:\n            for project in self.get_rest_projects(active, library):\n                if own_attributes:\n                    fill_own_attribs(project)\n                yield project\n\n        else:\n            if \"attrib\" in fields:\n                fields.remove(\"attrib\")\n                fields |= self.get_attributes_fields_for_type(\"project\")\n\n            if own_attributes:\n                fields.add(\"ownAttrib\")\n\n            query = projects_graphql_query(fields)\n            for parsed_data in query.continuous_query(self):\n                for project in parsed_data[\"projects\"]:\n                    if own_attributes:\n                        fill_own_attribs(project)\n                    yield project\n\n    def get_project(self, project_name, fields=None, own_attributes=False):\n        \"\"\"Get project.\n\n        Args:\n            project_name (str): Name of project.\n            fields (Optional[Iterable[str]]): fields to be queried\n                for project.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict[str, Any], None]: Project entity data or None\n                if project was not found.\n        \"\"\"\n\n        use_rest = True\n        if fields is not None:\n            use_rest = False\n            _fields = set()\n            for field in fields:\n                if field.startswith(\"config\") or field == \"data\":\n                    use_rest = True\n                    break\n                _fields.add(field)\n\n            fields = _fields\n\n        if use_rest:\n            project = self.get_rest_project(project_name)\n            if own_attributes:\n                fill_own_attribs(project)\n            return project\n\n        if \"attrib\" in fields:\n            fields.remove(\"attrib\")\n            fields |= self.get_attributes_fields_for_type(\"project\")\n\n        if own_attributes:\n            fields.add(\"ownAttrib\")\n        query = project_graphql_query(fields)\n        query.set_variable_value(\"projectName\", project_name)\n\n        parsed_data = query.query(self)\n\n        project = parsed_data[\"project\"]\n        if project is not None:\n            project[\"name\"] = project_name\n            if own_attributes:\n                fill_own_attribs(project)\n        return project\n\n    def get_folders_hierarchy(\n        self,\n        project_name,\n        search_string=None,\n        folder_types=None\n    ):\n        \"\"\"Get project hierarchy.\n\n        All folders in project in hierarchy data structure.\n\n        Example output:\n            {\n                \"hierarchy\": [\n                    {\n                        \"id\": \"...\",\n                        \"name\": \"...\",\n                        \"label\": \"...\",\n                        \"status\": \"...\",\n                        \"folderType\": \"...\",\n                        \"hasTasks\": False,\n                        \"taskNames\": [],\n                        \"parents\": [],\n                        \"parentId\": None,\n                        \"children\": [...children folders...]\n                    },\n                    ...\n                ]\n            }\n\n        Args:\n            project_name (str): Project where to look for folders.\n            search_string (Optional[str]): Search string to filter folders.\n            folder_types (Optional[Iterable[str]]): Folder types to filter.\n\n        Returns:\n            dict[str, Any]: Response data from server.\n        \"\"\"\n\n        if folder_types:\n            folder_types = \",\".join(folder_types)\n\n        query_fields = [\n            \"{}={}\".format(key, value)\n            for key, value in (\n                (\"search\", search_string),\n                (\"types\", folder_types),\n            )\n            if value\n        ]\n        query = \"\"\n        if query_fields:\n            query = \"?{}\".format(\",\".join(query_fields))\n\n        response = self.get(\n            \"projects/{}/hierarchy{}\".format(project_name, query)\n        )\n        response.raise_for_status()\n        return response.data\n\n    def get_folders(\n        self,\n        project_name,\n        folder_ids=None,\n        folder_paths=None,\n        folder_names=None,\n        folder_types=None,\n        parent_ids=None,\n        folder_path_regex=None,\n        has_products=None,\n        has_tasks=None,\n        has_children=None,\n        statuses=None,\n        tags=None,\n        active=True,\n        has_links=None,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query folders from server.\n\n        Todos:\n            Folder name won't be unique identifier, so we should add folder path\n                filtering.\n\n        Notes:\n            Filter 'active' don't have direct filter in GraphQl.\n\n        Args:\n            project_name (str): Name of project.\n            folder_ids (Optional[Iterable[str]]): Folder ids to filter.\n            folder_paths (Optional[Iterable[str]]): Folder paths used\n                for filtering.\n            folder_names (Optional[Iterable[str]]): Folder names used\n                for filtering.\n            folder_types (Optional[Iterable[str]]): Folder types used\n                for filtering.\n            parent_ids (Optional[Iterable[str]]): Ids of folder parents.\n                Use 'None' if folder is direct child of project.\n            folder_path_regex (Optional[str]): Folder path regex used\n                for filtering.\n            has_products (Optional[bool]): Filter folders with/without\n                products. Ignored when None, default behavior.\n            has_tasks (Optional[bool]): Filter folders with/without\n                tasks. Ignored when None, default behavior.\n            has_children (Optional[bool]): Filter folders with/without\n                children. Ignored when None, default behavior.\n            statuses (Optional[Iterable[str]]): Folder statuses used\n                for filtering.\n            tags (Optional[Iterable[str]]): Folder tags used\n                for filtering.\n            active (Optional[bool]): Filter active/inactive folders.\n                Both are returned if is set to None.\n            has_links (Optional[Literal[IN, OUT, ANY]]): Filter\n                representations with IN/OUT/ANY links.\n            fields (Optional[Iterable[str]]): Fields to be queried for\n                folder. All possible folder fields are returned\n                if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Generator[dict[str, Any]]: Queried folder entities.\n        \"\"\"\n\n        if not project_name:\n            return\n\n        filters = {\n            \"projectName\": project_name\n        }\n        if folder_ids is not None:\n            folder_ids = set(folder_ids)\n            if not folder_ids:\n                return\n            filters[\"folderIds\"] = list(folder_ids)\n\n        if folder_paths is not None:\n            folder_paths = set(folder_paths)\n            if not folder_paths:\n                return\n            filters[\"folderPaths\"] = list(folder_paths)\n\n        if folder_names is not None:\n            folder_names = set(folder_names)\n            if not folder_names:\n                return\n            filters[\"folderNames\"] = list(folder_names)\n\n        if folder_types is not None:\n            folder_types = set(folder_types)\n            if not folder_types:\n                return\n            filters[\"folderTypes\"] = list(folder_types)\n\n        if statuses is not None:\n            statuses = set(statuses)\n            if not statuses:\n                return\n            filters[\"folderStatuses\"] = list(statuses)\n\n        if tags is not None:\n            tags = set(tags)\n            if not tags:\n                return\n            filters[\"folderTags\"] = list(tags)\n\n        if parent_ids is not None:\n            parent_ids = set(parent_ids)\n            if not parent_ids:\n                return\n            if None in parent_ids:\n                # Replace 'None' with '\"root\"' which is used during GraphQl\n                #   query for parent ids filter for folders without folder\n                #   parent\n                parent_ids.remove(None)\n                parent_ids.add(\"root\")\n\n            if project_name in parent_ids:\n                # Replace project name with '\"root\"' which is used during\n                #   GraphQl query for parent ids filter for folders without\n                #   folder parent\n                parent_ids.remove(project_name)\n                parent_ids.add(\"root\")\n\n            filters[\"parentFolderIds\"] = list(parent_ids)\n\n        if folder_path_regex is not None:\n            filters[\"folderPathRegex\"] = folder_path_regex\n\n        if has_products is not None:\n            filters[\"folderHasProducts\"] = has_products\n\n        if has_tasks is not None:\n            filters[\"folderHasTasks\"] = has_tasks\n\n        if has_links is not None:\n            filters[\"folderHasLinks\"] = has_links.upper()\n\n        if has_children is not None:\n            filters[\"folderHasChildren\"] = has_children\n\n        if not fields:\n            fields = self.get_default_fields_for_type(\"folder\")\n        else:\n            fields = set(fields)\n            if \"attrib\" in fields:\n                fields.remove(\"attrib\")\n                fields |= self.get_attributes_fields_for_type(\"folder\")\n\n        use_rest = False\n        if \"data\" in fields and not self.graphql_allows_data_in_query:\n            use_rest = True\n            fields = {\"id\"}\n\n        if active is not None:\n            fields.add(\"active\")\n\n        if own_attributes and not use_rest:\n            fields.add(\"ownAttrib\")\n\n        query = folders_graphql_query(fields)\n        for attr, filter_value in filters.items():\n            query.set_variable_value(attr, filter_value)\n\n        for parsed_data in query.continuous_query(self):\n            for folder in parsed_data[\"project\"][\"folders\"]:\n                if active is not None and active is not folder[\"active\"]:\n                    continue\n\n                if use_rest:\n                    folder = self.get_rest_folder(project_name, folder[\"id\"])\n                else:\n                    self._convert_entity_data(folder)\n\n                if own_attributes:\n                    fill_own_attribs(folder)\n                yield folder\n\n    def get_folder_by_id(\n        self,\n        project_name,\n        folder_id,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query folder entity by id.\n\n        Args:\n            project_name (str): Name of project where to look for queried\n                entities.\n            folder_id (str): Folder id.\n            fields (Optional[Iterable[str]]): Fields that should be returned.\n                All fields are returned if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict, None]: Folder entity data or None if was not found.\n        \"\"\"\n\n        folders = self.get_folders(\n            project_name,\n            folder_ids=[folder_id],\n            active=None,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        for folder in folders:\n            return folder\n        return None\n\n    def get_folder_by_path(\n        self,\n        project_name,\n        folder_path,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query folder entity by path.\n\n        Folder path is a path to folder with all parent names joined by slash.\n\n        Args:\n            project_name (str): Name of project where to look for queried\n                entities.\n            folder_path (str): Folder path.\n            fields (Optional[Iterable[str]]): Fields that should be returned.\n                All fields are returned if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict, None]: Folder entity data or None if was not found.\n        \"\"\"\n\n        folders = self.get_folders(\n            project_name,\n            folder_paths=[folder_path],\n            active=None,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        for folder in folders:\n            return folder\n        return None\n\n    def get_folder_by_name(\n        self,\n        project_name,\n        folder_name,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query folder entity by path.\n\n        Warnings:\n            Folder name is not a unique identifier of a folder. Function is\n                kept for OpenPype 3 compatibility.\n\n        Args:\n            project_name (str): Name of project where to look for queried\n                entities.\n            folder_name (str): Folder name.\n            fields (Optional[Iterable[str]]): Fields that should be returned.\n                All fields are returned if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict, None]: Folder entity data or None if was not found.\n        \"\"\"\n\n        folders = self.get_folders(\n            project_name,\n            folder_names=[folder_name],\n            active=None,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        for folder in folders:\n            return folder\n        return None\n\n    def get_folder_ids_with_products(self, project_name, folder_ids=None):\n        \"\"\"Find folders which have at least one product.\n\n        Folders that have at least one product should be immutable, so they\n        should not change path -> change of name or name of any parent\n        is not possible.\n\n        Args:\n            project_name (str): Name of project.\n            folder_ids (Optional[Iterable[str]]): Limit folder ids filtering\n                to a set of folders. If set to None all folders on project are\n                checked.\n\n        Returns:\n            set[str]: Folder ids that have at least one product.\n        \"\"\"\n\n        if folder_ids is not None:\n            folder_ids = set(folder_ids)\n            if not folder_ids:\n                return set()\n\n        query = folders_graphql_query({\"id\"})\n        query.set_variable_value(\"projectName\", project_name)\n        query.set_variable_value(\"folderHasProducts\", True)\n        if folder_ids:\n            query.set_variable_value(\"folderIds\", list(folder_ids))\n\n        parsed_data = query.query(self)\n        folders = parsed_data[\"project\"][\"folders\"]\n        return {\n            folder[\"id\"]\n            for folder in folders\n        }\n\n    def get_tasks(\n        self,\n        project_name,\n        task_ids=None,\n        task_names=None,\n        task_types=None,\n        folder_ids=None,\n        assignees=None,\n        assignees_all=None,\n        statuses=None,\n        tags=None,\n        active=True,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query task entities from server.\n\n        Args:\n            project_name (str): Name of project.\n            task_ids (Iterable[str]): Task ids to filter.\n            task_names (Iterable[str]): Task names used for filtering.\n            task_types (Iterable[str]): Task types used for filtering.\n            folder_ids (Iterable[str]): Ids of task parents. Use 'None'\n                if folder is direct child of project.\n            assignees (Optional[Iterable[str]]): Task assignees used for\n                filtering. All tasks with any of passed assignees are\n                returned.\n            assignees_all (Optional[Iterable[str]]): Task assignees used\n                for filtering. Task must have all of passed assignees to be\n                returned.\n            statuses (Optional[Iterable[str]]): Task statuses used for\n                filtering.\n            tags (Optional[Iterable[str]]): Task tags used for\n                filtering.\n            active (Optional[bool]): Filter active/inactive tasks.\n                Both are returned if is set to None.\n            fields (Optional[Iterable[str]]): Fields to be queried for\n                folder. All possible folder fields are returned\n                if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Generator[dict[str, Any]]: Queried task entities.\n        \"\"\"\n\n        if not project_name:\n            return\n\n        filters = {\n            \"projectName\": project_name\n        }\n\n        if task_ids is not None:\n            task_ids = set(task_ids)\n            if not task_ids:\n                return\n            filters[\"taskIds\"] = list(task_ids)\n\n        if task_names is not None:\n            task_names = set(task_names)\n            if not task_names:\n                return\n            filters[\"taskNames\"] = list(task_names)\n\n        if task_types is not None:\n            task_types = set(task_types)\n            if not task_types:\n                return\n            filters[\"taskTypes\"] = list(task_types)\n\n        if folder_ids is not None:\n            folder_ids = set(folder_ids)\n            if not folder_ids:\n                return\n            filters[\"folderIds\"] = list(folder_ids)\n\n        if assignees is not None:\n            assignees = set(assignees)\n            if not assignees:\n                return\n            filters[\"taskAssigneesAny\"] = list(assignees)\n\n        if assignees_all is not None:\n            assignees_all = set(assignees_all)\n            if not assignees_all:\n                return\n            filters[\"taskAssigneesAll\"] = list(assignees_all)\n\n        if statuses is not None:\n            statuses = set(statuses)\n            if not statuses:\n                return\n            filters[\"taskStatuses\"] = list(statuses)\n\n        if tags is not None:\n            tags = set(tags)\n            if not tags:\n                return\n            filters[\"taskTags\"] = list(tags)\n\n        if not fields:\n            fields = self.get_default_fields_for_type(\"task\")\n        else:\n            fields = set(fields)\n            if \"attrib\" in fields:\n                fields.remove(\"attrib\")\n                fields |= self.get_attributes_fields_for_type(\"task\")\n\n        use_rest = False\n        if \"data\" in fields and not self.graphql_allows_data_in_query:\n            use_rest = True\n            fields = {\"id\"}\n\n        if active is not None:\n            fields.add(\"active\")\n\n        if own_attributes:\n            fields.add(\"ownAttrib\")\n\n        query = tasks_graphql_query(fields)\n        for attr, filter_value in filters.items():\n            query.set_variable_value(attr, filter_value)\n\n        for parsed_data in query.continuous_query(self):\n            for task in parsed_data[\"project\"][\"tasks\"]:\n                if active is not None and active is not task[\"active\"]:\n                    continue\n\n                if use_rest:\n                    task = self.get_rest_task(project_name, task[\"id\"])\n                else:\n                    self._convert_entity_data(task)\n\n                if own_attributes:\n                    fill_own_attribs(task)\n                yield task\n\n    def get_task_by_name(\n        self,\n        project_name,\n        folder_id,\n        task_name,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query task entity by name and folder id.\n\n        Args:\n            project_name (str): Name of project where to look for queried\n                entities.\n            folder_id (str): Folder id.\n            task_name (str): Task name\n            fields (Optional[Iterable[str]): Fields that should be returned.\n                All fields are returned if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict, None]: Task entity data or None if was not found.\n        \"\"\"\n\n        for task in self.get_tasks(\n            project_name,\n            folder_ids=[folder_id],\n            task_names=[task_name],\n            active=None,\n            fields=fields,\n            own_attributes=own_attributes\n        ):\n            return task\n        return None\n\n    def get_task_by_id(\n        self,\n        project_name,\n        task_id,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query task entity by id.\n\n        Args:\n            project_name (str): Name of project where to look for queried\n                entities.\n            task_id (str): Task id.\n            fields (Optional[Iterable[str]): Fields that should be returned.\n                All fields are returned if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict, None]: Task entity data or None if was not found.\n        \"\"\"\n\n        for task in self.get_tasks(\n            project_name,\n            task_ids=[task_id],\n            active=None,\n            fields=fields,\n            own_attributes=own_attributes\n        ):\n            return task\n        return None\n\n    def _filter_product(\n        self, project_name, product, active, own_attributes, use_rest\n    ):\n        if active is not None and product[\"active\"] is not active:\n            return None\n\n        if use_rest:\n            product = self.get_rest_product(project_name, product[\"id\"])\n        else:\n            self._convert_entity_data(product)\n\n        if own_attributes:\n            fill_own_attribs(product)\n\n        return product\n\n    def get_products(\n        self,\n        project_name,\n        product_ids=None,\n        product_names=None,\n        folder_ids=None,\n        product_types=None,\n        product_name_regex=None,\n        product_path_regex=None,\n        names_by_folder_ids=None,\n        statuses=None,\n        tags=None,\n        active=True,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query products from server.\n\n        Todos:\n            Separate 'name_by_folder_ids' filtering to separated method. It\n                cannot be combined with some other filters.\n\n        Args:\n            project_name (str): Name of project.\n            product_ids (Optional[Iterable[str]]): Task ids to filter.\n            product_names (Optional[Iterable[str]]): Task names used for\n                filtering.\n            folder_ids (Optional[Iterable[str]]): Ids of task parents.\n                Use 'None' if folder is direct child of project.\n            product_types (Optional[Iterable[str]]): Product types used for\n                filtering.\n            product_name_regex (Optional[str]): Filter products by name regex.\n            product_path_regex (Optional[str]): Filter products by path regex.\n                Path starts with folder path and ends with product name.\n            names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product\n                name filtering by folder id.\n            statuses (Optional[Iterable[str]]): Product statuses used\n                for filtering.\n            tags (Optional[Iterable[str]]): Product tags used\n                for filtering.\n            active (Optional[bool]): Filter active/inactive products.\n                Both are returned if is set to None.\n            fields (Optional[Iterable[str]]): Fields to be queried for\n                folder. All possible folder fields are returned\n                if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Generator[dict[str, Any]]: Queried product entities.\n        \"\"\"\n\n        if not project_name:\n            return\n\n        # Prepare these filters before 'name_by_filter_ids' filter\n        filter_product_names = None\n        if product_names is not None:\n            filter_product_names = set(product_names)\n            if not filter_product_names:\n                return\n\n        filter_folder_ids = None\n        if folder_ids is not None:\n            filter_folder_ids = set(folder_ids)\n            if not filter_folder_ids:\n                return\n\n        # This will disable 'folder_ids' and 'product_names' filters\n        #   - maybe could be enhanced in future?\n        if names_by_folder_ids is not None:\n            filter_product_names = set()\n            filter_folder_ids = set()\n\n            for folder_id, names in names_by_folder_ids.items():\n                if folder_id and names:\n                    filter_folder_ids.add(folder_id)\n                    filter_product_names |= set(names)\n\n            if not filter_product_names or not filter_folder_ids:\n                return\n\n        # Convert fields and add minimum required fields\n        if fields:\n            fields = set(fields) | {\"id\"}\n            if \"attrib\" in fields:\n                fields.remove(\"attrib\")\n                fields |= self.get_attributes_fields_for_type(\"product\")\n        else:\n            fields = self.get_default_fields_for_type(\"product\")\n\n        use_rest = False\n        if \"data\" in fields and not self.graphql_allows_data_in_query:\n            use_rest = True\n            fields = {\"id\"}\n\n        if active is not None:\n            fields.add(\"active\")\n\n        if own_attributes:\n            fields.add(\"ownAttrib\")\n\n        # Add 'name' and 'folderId' if 'names_by_folder_ids' filter is entered\n        if names_by_folder_ids:\n            fields.add(\"name\")\n            fields.add(\"folderId\")\n\n        # Prepare filters for query\n        filters = {\n            \"projectName\": project_name\n        }\n\n        if filter_folder_ids:\n            filters[\"folderIds\"] = list(filter_folder_ids)\n\n        if filter_product_names:\n            filters[\"productNames\"] = list(filter_product_names)\n\n        if product_ids is not None:\n            product_ids = set(product_ids)\n            if not product_ids:\n                return\n            filters[\"productIds\"] = list(product_ids)\n\n        if product_types is not None:\n            product_types = set(product_types)\n            if not product_types:\n                return\n            filters[\"productTypes\"] = list(product_types)\n\n        if statuses is not None:\n            statuses = set(statuses)\n            if not statuses:\n                return\n            filters[\"productStatuses\"] = list(statuses)\n\n        if tags is not None:\n            tags = set(tags)\n            if not tags:\n                return\n            filters[\"productTags\"] = list(tags)\n\n        if product_name_regex:\n            filters[\"productNameRegex\"] = product_name_regex\n\n        if product_path_regex:\n            filters[\"productPathRegex\"] = product_path_regex\n\n        query = products_graphql_query(fields)\n        for attr, filter_value in filters.items():\n            query.set_variable_value(attr, filter_value)\n\n        parsed_data = query.query(self)\n\n        products = parsed_data.get(\"project\", {}).get(\"products\", [])\n        # Filter products by 'names_by_folder_ids'\n        if names_by_folder_ids:\n            products_by_folder_id = collections.defaultdict(list)\n            for product in products:\n                filtered_product = self._filter_product(\n                    project_name, product, active, own_attributes, use_rest\n                )\n                if filtered_product is not None:\n                    folder_id = filtered_product[\"folderId\"]\n                    products_by_folder_id[folder_id].append(filtered_product)\n\n            for folder_id, names in names_by_folder_ids.items():\n                for folder_product in products_by_folder_id[folder_id]:\n                    if folder_product[\"name\"] in names:\n                        yield folder_product\n\n        else:\n            for product in products:\n                filtered_product = self._filter_product(\n                    project_name, product, active, own_attributes, use_rest\n                )\n                if filtered_product is not None:\n                    yield filtered_product\n\n    def get_product_by_id(\n        self,\n        project_name,\n        product_id,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query product entity by id.\n\n        Args:\n            project_name (str): Name of project where to look for queried\n                entities.\n            product_id (str): Product id.\n            fields (Optional[Iterable[str]]): Fields that should be returned.\n                All fields are returned if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict, None]: Product entity data or None if was not found.\n        \"\"\"\n\n        products = self.get_products(\n            project_name,\n            product_ids=[product_id],\n            active=None,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        for product in products:\n            return product\n        return None\n\n    def get_product_by_name(\n        self,\n        project_name,\n        product_name,\n        folder_id,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query product entity by name and folder id.\n\n        Args:\n            project_name (str): Name of project where to look for queried\n                entities.\n            product_name (str): Product name.\n            folder_id (str): Folder id (Folder is a parent of products).\n            fields (Optional[Iterable[str]]): Fields that should be returned.\n                All fields are returned if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict, None]: Product entity data or None if was not found.\n        \"\"\"\n\n        products = self.get_products(\n            project_name,\n            product_names=[product_name],\n            folder_ids=[folder_id],\n            active=None,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        for product in products:\n            return product\n        return None\n\n    def get_product_types(self, fields=None):\n        \"\"\"Types of products.\n\n        This is server wide information. Product types have 'name', 'icon' and\n            'color'.\n\n        Args:\n            fields (Optional[Iterable[str]]): Product types fields to query.\n\n        Returns:\n            list[dict[str, Any]]: Product types information.\n        \"\"\"\n\n        if not fields:\n            fields = self.get_default_fields_for_type(\"productType\")\n\n        query = product_types_query(fields)\n\n        parsed_data = query.query(self)\n\n        return parsed_data.get(\"productTypes\", [])\n\n    def get_project_product_types(self, project_name, fields=None):\n        \"\"\"Types of products available on a project.\n\n        Filter only product types available on project.\n\n        Args:\n            project_name (str): Name of project where to look for\n                product types.\n            fields (Optional[Iterable[str]]): Product types fields to query.\n\n        Returns:\n            list[dict[str, Any]]: Product types information.\n        \"\"\"\n\n        if not fields:\n            fields = self.get_default_fields_for_type(\"productType\")\n\n        query = project_product_types_query(fields)\n        query.set_variable_value(\"projectName\", project_name)\n\n        parsed_data = query.query(self)\n\n        return parsed_data.get(\"project\", {}).get(\"productTypes\", [])\n\n    def get_product_type_names(self, project_name=None, product_ids=None):\n        \"\"\"Product type names.\n\n        Warnings:\n            This function will be probably removed. Matters if 'products_id'\n                filter has real use-case.\n\n        Args:\n            project_name (Optional[str]): Name of project where to look for\n                queried entities.\n            product_ids (Optional[Iterable[str]]): Product ids filter. Can be\n                used only with 'project_name'.\n\n        Returns:\n            set[str]: Product type names.\n        \"\"\"\n\n        if project_name and product_ids:\n            products = self.get_products(\n                project_name,\n                product_ids=product_ids,\n                fields=[\"productType\"],\n                active=None,\n            )\n            return {\n                product[\"productType\"]\n                for product in products\n            }\n\n        return {\n            product_info[\"name\"]\n            for product_info in self.get_project_product_types(\n                project_name, fields=[\"name\"]\n            )\n        }\n\n    def get_versions(\n        self,\n        project_name,\n        version_ids=None,\n        product_ids=None,\n        versions=None,\n        hero=True,\n        standard=True,\n        latest=None,\n        statuses=None,\n        tags=None,\n        active=True,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Get version entities based on passed filters from server.\n\n        Args:\n            project_name (str): Name of project where to look for versions.\n            version_ids (Optional[Iterable[str]]): Version ids used for\n                version filtering.\n            product_ids (Optional[Iterable[str]]): Product ids used for\n                version filtering.\n            versions (Optional[Iterable[int]]): Versions we're interested in.\n            hero (Optional[bool]): Receive also hero versions when set to true.\n            standard (Optional[bool]): Receive versions which are not hero when\n                set to true.\n            latest (Optional[bool]): Return only latest version of standard\n                versions. This can be combined only with 'standard' attribute\n                set to True.\n            statuses (Optional[Iterable[str]]): Representation statuses used\n                for filtering.\n            tags (Optional[Iterable[str]]): Representation tags used\n                for filtering.\n            active (Optional[bool]): Receive active/inactive entities.\n                Both are returned when 'None' is passed.\n            fields (Optional[Iterable[str]]): Fields to be queried\n                for version. All possible folder fields are returned\n                if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Generator[dict[str, Any]]: Queried version entities.\n        \"\"\"\n\n        if not fields:\n            fields = self.get_default_fields_for_type(\"version\")\n        else:\n            fields = set(fields)\n            if \"attrib\" in fields:\n                fields.remove(\"attrib\")\n                fields |= self.get_attributes_fields_for_type(\"version\")\n\n        # Make sure fields have minimum required fields\n        fields |= {\"id\", \"version\"}\n\n        use_rest = False\n        if \"data\" in fields and not self.graphql_allows_data_in_query:\n            use_rest = True\n            fields = {\"id\"}\n\n        if active is not None:\n            fields.add(\"active\")\n\n        if own_attributes:\n            fields.add(\"ownAttrib\")\n\n        filters = {\n            \"projectName\": project_name\n        }\n        if version_ids is not None:\n            version_ids = set(version_ids)\n            if not version_ids:\n                return\n            filters[\"versionIds\"] = list(version_ids)\n\n        if product_ids is not None:\n            product_ids = set(product_ids)\n            if not product_ids:\n                return\n            filters[\"productIds\"] = list(product_ids)\n\n        # TODO versions can't be used as filter at this moment!\n        if versions is not None:\n            versions = set(versions)\n            if not versions:\n                return\n            filters[\"versions\"] = list(versions)\n\n        if statuses is not None:\n            statuses = set(statuses)\n            if not statuses:\n                return\n            filters[\"versionStatuses\"] = list(statuses)\n\n        if tags is not None:\n            tags = set(tags)\n            if not tags:\n                return\n            filters[\"versionTags\"] = list(tags)\n\n        if not hero and not standard:\n            return\n\n        queries = []\n        # Add filters based on 'hero' and 'standard'\n        # NOTE: There is not a filter to \"ignore\" hero versions or to get\n        #   latest and hero version\n        # - if latest and hero versions should be returned it must be done in\n        #       2 graphql queries\n        if standard and not latest:\n            # This query all versions standard + hero\n            # - hero must be filtered out if is not enabled during loop\n            query = versions_graphql_query(fields)\n            for attr, filter_value in filters.items():\n                query.set_variable_value(attr, filter_value)\n            queries.append(query)\n        else:\n            if hero:\n                # Add hero query if hero is enabled\n                hero_query = versions_graphql_query(fields)\n                for attr, filter_value in filters.items():\n                    hero_query.set_variable_value(attr, filter_value)\n\n                hero_query.set_variable_value(\"heroOnly\", True)\n                queries.append(hero_query)\n\n            if standard:\n                standard_query = versions_graphql_query(fields)\n                for attr, filter_value in filters.items():\n                    standard_query.set_variable_value(attr, filter_value)\n\n                if latest:\n                    standard_query.set_variable_value(\"latestOnly\", True)\n                queries.append(standard_query)\n\n        for query in queries:\n            for parsed_data in query.continuous_query(self):\n                for version in parsed_data[\"project\"][\"versions\"]:\n                    if active is not None and version[\"active\"] is not active:\n                        continue\n\n                    if not hero and version[\"version\"] < 0:\n                        continue\n\n                    if use_rest:\n                        version = self.get_rest_version(\n                            project_name, version[\"id\"]\n                        )\n                    else:\n                        self._convert_entity_data(version)\n\n                    if own_attributes:\n                        fill_own_attribs(version)\n\n                    yield version\n\n    def get_version_by_id(\n        self,\n        project_name,\n        version_id,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query version entity by id.\n\n        Args:\n            project_name (str): Name of project where to look for queried\n                entities.\n            version_id (str): Version id.\n            fields (Optional[Iterable[str]]): Fields that should be returned.\n                All fields are returned if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict, None]: Version entity data or None if was not found.\n       \"\"\"\n\n        versions = self.get_versions(\n            project_name,\n            version_ids=[version_id],\n            active=None,\n            hero=True,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        for version in versions:\n            return version\n        return None\n\n    def get_version_by_name(\n        self,\n        project_name,\n        version,\n        product_id,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query version entity by version and product id.\n\n        Args:\n            project_name (str): Name of project where to look for queried\n                entities.\n            version (int): Version of version entity.\n            product_id (str): Product id. Product is a parent of version.\n            fields (Optional[Iterable[str]]): Fields that should be returned.\n                All fields are returned if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict, None]: Version entity data or None if was not found.\n       \"\"\"\n\n        versions = self.get_versions(\n            project_name,\n            product_ids=[product_id],\n            versions=[version],\n            active=None,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        for version in versions:\n            return version\n        return None\n\n    def get_hero_version_by_id(\n        self,\n        project_name,\n        version_id,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query hero version entity by id.\n\n        Args:\n            project_name (str): Name of project where to look for queried\n                entities.\n            version_id (int): Hero version id.\n            fields (Optional[Iterable[str]]): Fields that should be returned.\n                All fields are returned if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict, None]: Version entity data or None if was not found.\n       \"\"\"\n\n        versions = self.get_hero_versions(\n            project_name,\n            version_ids=[version_id],\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        for version in versions:\n            return version\n        return None\n\n    def get_hero_version_by_product_id(\n        self,\n        project_name,\n        product_id,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query hero version entity by product id.\n\n        Only one hero version is available on a product.\n\n        Args:\n            project_name (str): Name of project where to look for queried\n                entities.\n            product_id (int): Product id.\n            fields (Optional[Iterable[str]]): Fields that should be returned.\n                All fields are returned if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict, None]: Version entity data or None if was not found.\n       \"\"\"\n\n        versions = self.get_hero_versions(\n            project_name,\n            product_ids=[product_id],\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        for version in versions:\n            return version\n        return None\n\n    def get_hero_versions(\n        self,\n        project_name,\n        product_ids=None,\n        version_ids=None,\n        active=True,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query hero versions by multiple filters.\n\n        Only one hero version is available on a product.\n\n        Args:\n            project_name (str): Name of project where to look for queried\n                entities.\n            product_ids (Optional[Iterable[str]]): Product ids.\n            version_ids (Optional[Iterable[str]]): Version ids.\n            active (Optional[bool]): Receive active/inactive entities.\n                Both are returned when 'None' is passed.\n            fields (Optional[Iterable[str]]): Fields that should be returned.\n                All fields are returned if 'None' is passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict, None]: Version entity data or None if was not found.\n       \"\"\"\n\n        return self.get_versions(\n            project_name,\n            version_ids=version_ids,\n            product_ids=product_ids,\n            hero=True,\n            standard=False,\n            active=active,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n\n    def get_last_versions(\n        self,\n        project_name,\n        product_ids,\n        active=True,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query last version entities by product ids.\n\n        Args:\n            project_name (str): Project where to look for representation.\n            product_ids (Iterable[str]): Product ids.\n            active (Optional[bool]): Receive active/inactive entities.\n                Both are returned when 'None' is passed.\n            fields (Optional[Iterable[str]]): fields to be queried\n                for representations.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            dict[str, dict[str, Any]]: Last versions by product id.\n        \"\"\"\n\n        if fields:\n            fields = set(fields)\n            fields.add(\"productId\")\n\n        versions = self.get_versions(\n            project_name,\n            product_ids=product_ids,\n            latest=True,\n            active=active,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        return {\n            version[\"productId\"]: version\n            for version in versions\n        }\n\n    def get_last_version_by_product_id(\n        self,\n        project_name,\n        product_id,\n        active=True,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query last version entity by product id.\n\n        Args:\n            project_name (str): Project where to look for representation.\n            product_id (str): Product id.\n            active (Optional[bool]): Receive active/inactive entities.\n                Both are returned when 'None' is passed.\n            fields (Optional[Iterable[str]]): fields to be queried\n                for representations.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict[str, Any], None]: Queried version entity or None.\n        \"\"\"\n\n        versions = self.get_versions(\n            project_name,\n            product_ids=[product_id],\n            latest=True,\n            active=active,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        for version in versions:\n            return version\n        return None\n\n    def get_last_version_by_product_name(\n        self,\n        project_name,\n        product_name,\n        folder_id,\n        active=True,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query last version entity by product name and folder id.\n\n        Args:\n            project_name (str): Project where to look for representation.\n            product_name (str): Product name.\n            folder_id (str): Folder id.\n            active (Optional[bool]): Receive active/inactive entities.\n                Both are returned when 'None' is passed.\n            fields (Optional[Iterable[str]): fields to be queried\n                for representations.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict[str, Any], None]: Queried version entity or None.\n        \"\"\"\n\n        if not folder_id:\n            return None\n\n        product = self.get_product_by_name(\n            project_name, product_name, folder_id, fields=[\"_id\"]\n        )\n        if not product:\n            return None\n        return self.get_last_version_by_product_id(\n            project_name,\n            product[\"id\"],\n            active=active,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n\n    def version_is_latest(self, project_name, version_id):\n        \"\"\"Is version latest from a product.\n\n        Args:\n            project_name (str): Project where to look for representation.\n            version_id (str): Version id.\n\n        Returns:\n            bool: Version is latest or not.\n        \"\"\"\n\n        query = GraphQlQuery(\"VersionIsLatest\")\n        project_name_var = query.add_variable(\n            \"projectName\", \"String!\", project_name\n        )\n        version_id_var = query.add_variable(\n            \"versionId\", \"String!\", version_id\n        )\n        project_query = query.add_field(\"project\")\n        project_query.set_filter(\"name\", project_name_var)\n        version_query = project_query.add_field(\"version\")\n        version_query.set_filter(\"id\", version_id_var)\n        product_query = version_query.add_field(\"product\")\n        latest_version_query = product_query.add_field(\"latestVersion\")\n        latest_version_query.add_field(\"id\")\n\n        parsed_data = query.query(self)\n        latest_version = (\n            parsed_data[\"project\"][\"version\"][\"product\"][\"latestVersion\"]\n        )\n        return latest_version[\"id\"] == version_id\n\n    def _representation_conversion(self, representation):\n        if \"context\" in representation:\n            orig_context = representation[\"context\"]\n            context = {}\n            if orig_context and orig_context != \"null\":\n                context = json.loads(orig_context)\n            representation[\"context\"] = context\n\n        repre_files = representation.get(\"files\")\n        if not repre_files:\n            return\n\n        for repre_file in repre_files:\n            repre_file_size = repre_file.get(\"size\")\n            if repre_file_size is not None:\n                repre_file[\"size\"] = int(repre_file[\"size\"])\n\n    def get_representations(\n        self,\n        project_name,\n        representation_ids=None,\n        representation_names=None,\n        version_ids=None,\n        names_by_version_ids=None,\n        statuses=None,\n        tags=None,\n        active=True,\n        has_links=None,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Get representation entities based on passed filters from server.\n\n        Todos:\n            Add separated function for 'names_by_version_ids' filtering.\n                Because can't be combined with others.\n\n        Args:\n            project_name (str): Name of project where to look for versions.\n            representation_ids (Optional[Iterable[str]]): Representation ids\n                used for representation filtering.\n            representation_names (Optional[Iterable[str]]): Representation\n                names used for representation filtering.\n            version_ids (Optional[Iterable[str]]): Version ids used for\n                representation filtering. Versions are parents of\n                    representations.\n            names_by_version_ids (Optional[bool]): Find representations\n                by names and version ids. This filter discard all\n                other filters.\n            statuses (Optional[Iterable[str]]): Representation statuses used\n                for filtering.\n            tags (Optional[Iterable[str]]): Representation tags used\n                for filtering.\n            active (Optional[bool]): Receive active/inactive entities.\n                Both are returned when 'None' is passed.\n            has_links (Optional[Literal[IN, OUT, ANY]]): Filter\n                representations with IN/OUT/ANY links.\n            fields (Optional[Iterable[str]]): Fields to be queried for\n                representation. All possible fields are returned if 'None' is\n                passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Generator[dict[str, Any]]: Queried representation entities.\n        \"\"\"\n\n        if not fields:\n            fields = self.get_default_fields_for_type(\"representation\")\n        else:\n            fields = set(fields)\n            if \"attrib\" in fields:\n                fields.remove(\"attrib\")\n                fields |= self.get_attributes_fields_for_type(\n                    \"representation\"\n                )\n\n        use_rest = False\n        if \"data\" in fields and not self.graphql_allows_data_in_query:\n            use_rest = True\n            fields = {\"id\"}\n\n        if active is not None:\n            fields.add(\"active\")\n\n        if own_attributes:\n            fields.add(\"ownAttrib\")\n\n        filters = {\n            \"projectName\": project_name\n        }\n\n        if representation_ids is not None:\n            representation_ids = set(representation_ids)\n            if not representation_ids:\n                return\n            filters[\"representationIds\"] = list(representation_ids)\n\n        version_ids_filter = None\n        representaion_names_filter = None\n        if names_by_version_ids is not None:\n            version_ids_filter = set()\n            representaion_names_filter = set()\n            for version_id, names in names_by_version_ids.items():\n                version_ids_filter.add(version_id)\n                representaion_names_filter |= set(names)\n\n            if not version_ids_filter or not representaion_names_filter:\n                return\n\n        else:\n            if representation_names is not None:\n                representaion_names_filter = set(representation_names)\n                if not representaion_names_filter:\n                    return\n\n            if version_ids is not None:\n                version_ids_filter = set(version_ids)\n                if not version_ids_filter:\n                    return\n\n        if version_ids_filter:\n            filters[\"versionIds\"] = list(version_ids_filter)\n\n        if representaion_names_filter:\n            filters[\"representationNames\"] = list(representaion_names_filter)\n\n        if statuses is not None:\n            statuses = set(statuses)\n            if not statuses:\n                return\n            filters[\"representationStatuses\"] = list(statuses)\n\n        if tags is not None:\n            tags = set(tags)\n            if not tags:\n                return\n            filters[\"representationTags\"] = list(tags)\n\n        if has_links is not None:\n            filters[\"representationHasLinks\"] = has_links.upper()\n\n        query = representations_graphql_query(fields)\n\n        for attr, filter_value in filters.items():\n            query.set_variable_value(attr, filter_value)\n\n        for parsed_data in query.continuous_query(self):\n            for repre in parsed_data[\"project\"][\"representations\"]:\n                if active is not None and active is not repre[\"active\"]:\n                    continue\n\n                if use_rest:\n                    repre = self.get_rest_representation(\n                        project_name, repre[\"id\"]\n                    )\n                else:\n                    self._convert_entity_data(repre)\n\n                self._representation_conversion(repre)\n\n                if own_attributes:\n                    fill_own_attribs(repre)\n                yield repre\n\n    def get_representation_by_id(\n        self,\n        project_name,\n        representation_id,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query representation entity from server based on id filter.\n\n        Args:\n            project_name (str): Project where to look for representation.\n            representation_id (str): Id of representation.\n            fields (Optional[Iterable[str]]): fields to be queried\n                for representations.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict[str, Any], None]: Queried representation entity or None.\n        \"\"\"\n\n        representations = self.get_representations(\n            project_name,\n            representation_ids=[representation_id],\n            active=None,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        for representation in representations:\n            return representation\n        return None\n\n    def get_representation_by_name(\n        self,\n        project_name,\n        representation_name,\n        version_id,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Query representation entity by name and version id.\n\n        Args:\n            project_name (str): Project where to look for representation.\n            representation_name (str): Representation name.\n            version_id (str): Version id.\n            fields (Optional[Iterable[str]]): fields to be queried\n                for representations.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict[str, Any], None]: Queried representation entity or None.\n        \"\"\"\n\n        representations = self.get_representations(\n            project_name,\n            representation_names=[representation_name],\n            version_ids=[version_id],\n            active=None,\n            fields=fields,\n            own_attributes=own_attributes\n        )\n        for representation in representations:\n            return representation\n        return None\n\n    def get_representations_parents(self, project_name, representation_ids):\n        \"\"\"Find representations parents by representation id.\n\n        Representation parent entities up to project.\n\n        Args:\n             project_name (str): Project where to look for entities.\n             representation_ids (Iterable[str]): Representation ids.\n\n        Returns:\n            dict[str, RepresentationParents]: Parent entities by\n                representation id.\n        \"\"\"\n\n        if not representation_ids:\n            return {}\n\n        project = self.get_project(project_name)\n        repre_ids = set(representation_ids)\n        output = {\n            repre_id: RepresentationParents(None, None, None, None)\n            for repre_id in representation_ids\n        }\n\n        version_fields = self.get_default_fields_for_type(\"version\")\n        product_fields = self.get_default_fields_for_type(\"product\")\n        folder_fields = self.get_default_fields_for_type(\"folder\")\n\n        query = representations_parents_qraphql_query(\n            version_fields, product_fields, folder_fields\n        )\n        query.set_variable_value(\"projectName\", project_name)\n        query.set_variable_value(\"representationIds\", list(repre_ids))\n\n        parsed_data = query.query(self)\n        for repre in parsed_data[\"project\"][\"representations\"]:\n            repre_id = repre[\"id\"]\n            version = repre.pop(\"version\")\n            product = version.pop(\"product\")\n            folder = product.pop(\"folder\")\n            self._convert_entity_data(version)\n            self._convert_entity_data(product)\n            self._convert_entity_data(folder)\n            output[repre_id] = RepresentationParents(\n                version, product, folder, project\n            )\n\n        return output\n\n    def get_representation_parents(self, project_name, representation_id):\n        \"\"\"Find representation parents by representation id.\n\n        Representation parent entities up to project.\n\n        Args:\n             project_name (str): Project where to look for entities.\n             representation_id (str): Representation id.\n\n        Returns:\n            RepresentationParents: Representation parent entities.\n        \"\"\"\n\n        if not representation_id:\n            return None\n\n        parents_by_repre_id = self.get_representations_parents(\n            project_name, [representation_id]\n        )\n        return parents_by_repre_id[representation_id]\n\n    def get_repre_ids_by_context_filters(\n        self,\n        project_name,\n        context_filters,\n        representation_names=None,\n        version_ids=None\n    ):\n        \"\"\"Find representation ids which match passed context filters.\n\n        Each representation has context integrated on representation entity in\n        database. The context may contain project, folder, task name or\n        product name, product type and many more. This implementation gives\n        option to quickly filter representation based on representation data\n        in database.\n\n        Context filters have defined structure. To define filter of nested\n            subfield use dot '.' as delimiter (For example 'task.name').\n        Filter values can be regex filters. String or 're.Pattern' can be used.\n\n        Args:\n            project_name (str): Project where to look for representations.\n            context_filters (dict[str, list[str]]): Filters of context fields.\n            representation_names (Optional[Iterable[str]]): Representation\n                names, can be used as additional filter for representations\n                by their names.\n            version_ids (Optional[Iterable[str]]): Version ids, can be used\n                as additional filter for representations by their parent ids.\n\n        Returns:\n            list[str]: Representation ids that match passed filters.\n\n        Example:\n            The function returns just representation ids so if entities are\n                required for funtionality they must be queried afterwards by\n                their ids.\n            >>> project_name = \"testProject\"\n            >>> filters = {\n            ...     \"task.name\": [\"[aA]nimation\"],\n            ...     \"product\": [\".*[Mm]ain\"]\n            ... }\n            >>> repre_ids = get_repre_ids_by_context_filters(\n            ...     project_name, filters)\n            >>> repres = get_representations(project_name, repre_ids)\n        \"\"\"\n\n        if not isinstance(context_filters, dict):\n            raise TypeError(\n                \"Expected 'dict' got {}\".format(str(type(context_filters)))\n            )\n\n        filter_body = {}\n        if representation_names is not None:\n            if not representation_names:\n                return []\n            filter_body[\"names\"] = list(set(representation_names))\n\n        if version_ids is not None:\n            if not version_ids:\n                return []\n            filter_body[\"versionIds\"] = list(set(version_ids))\n\n        body_context_filters = []\n        for key, filters in context_filters.items():\n            if not isinstance(filters, (set, list, tuple)):\n                raise TypeError(\n                    \"Expected 'set', 'list', 'tuple' got {}\".format(\n                        str(type(filters))))\n\n\n            new_filters = set()\n            for filter_value in filters:\n                if isinstance(filter_value, PatternType):\n                    filter_value = filter_value.pattern\n                new_filters.add(filter_value)\n\n            body_context_filters.append({\n                \"key\": key,\n                \"values\": list(new_filters)\n            })\n\n        response = self.post(\n            \"projects/{}/repreContextFilter\".format(project_name),\n            context=body_context_filters,\n            **filter_body\n        )\n        response.raise_for_status()\n        return response.data[\"ids\"]\n\n    def get_workfiles_info(\n        self,\n        project_name,\n        workfile_ids=None,\n        task_ids=None,\n        paths=None,\n        path_regex=None,\n        statuses=None,\n        tags=None,\n        has_links=None,\n        fields=None,\n        own_attributes=False\n    ):\n        \"\"\"Workfile info entities by passed filters.\n\n        Args:\n            project_name (str): Project under which the entity is located.\n            workfile_ids (Optional[Iterable[str]]): Workfile ids.\n            task_ids (Optional[Iterable[str]]): Task ids.\n            paths (Optional[Iterable[str]]): Rootless workfiles paths.\n            path_regex (Optional[str]): Regex filter for workfile path.\n            statuses (Optional[Iterable[str]]): Workfile info statuses used\n                for filtering.\n            tags (Optional[Iterable[str]]): Workfile info tags used\n                for filtering.\n            has_links (Optional[Literal[IN, OUT, ANY]]): Filter\n                representations with IN/OUT/ANY links.\n            fields (Optional[Iterable[str]]): Fields to be queried for\n                representation. All possible fields are returned if 'None' is\n                passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Generator[dict[str, Any]]: Queried workfile info entites.\n        \"\"\"\n\n        filters = {\"projectName\": project_name}\n        if task_ids is not None:\n            task_ids = set(task_ids)\n            if not task_ids:\n                return\n            filters[\"taskIds\"] = list(task_ids)\n\n        if paths is not None:\n            paths = set(paths)\n            if not paths:\n                return\n            filters[\"paths\"] = list(paths)\n\n        if path_regex is not None:\n            filters[\"workfilePathRegex\"] = path_regex\n\n        if workfile_ids is not None:\n            workfile_ids = set(workfile_ids)\n            if not workfile_ids:\n                return\n            filters[\"workfileIds\"] = list(workfile_ids)\n\n        if statuses is not None:\n            statuses = set(statuses)\n            if not statuses:\n                return\n            filters[\"workfileStatuses\"] = list(statuses)\n\n        if tags is not None:\n            tags = set(tags)\n            if not tags:\n                return\n            filters[\"workfileTags\"] = list(tags)\n\n        if has_links is not None:\n            filters[\"workfilehasLinks\"] = has_links.upper()\n\n        if not fields:\n            fields = self.get_default_fields_for_type(\"workfile\")\n\n        fields = set(fields)\n        if \"attrib\" in fields:\n            fields.remove(\"attrib\")\n            fields |= {\n                \"attrib.{}\".format(attr)\n                for attr in self.get_attributes_for_type(\"workfile\")\n            }\n        if own_attributes:\n            fields.add(\"ownAttrib\")\n\n        query = workfiles_info_graphql_query(fields)\n\n        for attr, filter_value in filters.items():\n            query.set_variable_value(attr, filter_value)\n\n        for parsed_data in query.continuous_query(self):\n            for workfile_info in parsed_data[\"project\"][\"workfiles\"]:\n                if own_attributes:\n                    fill_own_attribs(workfile_info)\n                yield workfile_info\n\n    def get_workfile_info(\n        self, project_name, task_id, path, fields=None, own_attributes=False\n    ):\n        \"\"\"Workfile info entity by task id and workfile path.\n\n        Args:\n            project_name (str): Project under which the entity is located.\n            task_id (str): Task id.\n            path (str): Rootless workfile path.\n            fields (Optional[Iterable[str]]): Fields to be queried for\n                representation. All possible fields are returned if 'None' is\n                passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict[str, Any], None]: Workfile info entity or None.\n        \"\"\"\n\n        if not task_id or not path:\n            return None\n\n        for workfile_info in self.get_workfiles_info(\n            project_name,\n            task_ids=[task_id],\n            paths=[path],\n            fields=fields,\n            own_attributes=own_attributes\n        ):\n            return workfile_info\n        return None\n\n    def get_workfile_info_by_id(\n        self, project_name, workfile_id, fields=None, own_attributes=False\n    ):\n        \"\"\"Workfile info entity by id.\n\n        Args:\n            project_name (str): Project under which the entity is located.\n            workfile_id (str): Workfile info id.\n            fields (Optional[Iterable[str]]): Fields to be queried for\n                representation. All possible fields are returned if 'None' is\n                passed.\n            own_attributes (Optional[bool]): Attribute values that are\n                not explicitly set on entity will have 'None' value.\n\n        Returns:\n            Union[dict[str, Any], None]: Workfile info entity or None.\n        \"\"\"\n\n        if not workfile_id:\n            return None\n\n        for workfile_info in self.get_workfiles_info(\n            project_name,\n            workfile_ids=[workfile_id],\n            fields=fields,\n            own_attributes=own_attributes\n        ):\n            return workfile_info\n        return None\n\n    def _prepare_thumbnail_content(self, project_name, response):\n        content = None\n        content_type = response.content_type\n\n        # It is expected the response contains thumbnail id otherwise the\n        #   content cannot be cached and filepath returned\n        thumbnail_id = response.headers.get(\"X-Thumbnail-Id\")\n        if thumbnail_id is not None:\n            content = response.content\n\n        return ThumbnailContent(\n            project_name, thumbnail_id, content, content_type\n        )\n\n    def get_thumbnail_by_id(self, project_name, thumbnail_id):\n        \"\"\"Get thumbnail from server by id.\n\n        Permissions of thumbnails are related to entities so thumbnails must\n        be queried per entity. So an entity type and entity type is required\n        to be passed.\n\n        Notes:\n            It is recommended to use one of prepared entity type specific\n                methods 'get_folder_thumbnail', 'get_version_thumbnail' or\n                'get_workfile_thumbnail'.\n            We do recommend pass thumbnail id if you have access to it. Each\n                entity that allows thumbnails has 'thumbnailId' field, so it\n                can be queried.\n\n        Args:\n            project_name (str): Project under which the entity is located.\n            thumbnail_id (Optional[str]): DEPRECATED Use\n                'get_thumbnail_by_id'.\n\n        Returns:\n            ThumbnailContent: Thumbnail content wrapper. Does not have to be\n                valid.\n        \"\"\"\n\n        response = self.raw_get(\n            \"projects/{}/thumbnails/{}\".format(\n                project_name,\n                thumbnail_id\n            )\n        )\n        return self._prepare_thumbnail_content(project_name, response)\n\n    def get_thumbnail(\n        self, project_name, entity_type, entity_id, thumbnail_id=None\n    ):\n        \"\"\"Get thumbnail from server.\n\n        Permissions of thumbnails are related to entities so thumbnails must\n        be queried per entity. So an entity type and entity type is required\n        to be passed.\n\n        Notes:\n            It is recommended to use one of prepared entity type specific\n                methods 'get_folder_thumbnail', 'get_version_thumbnail' or\n                'get_workfile_thumbnail'.\n            We do recommend pass thumbnail id if you have access to it. Each\n                entity that allows thumbnails has 'thumbnailId' field, so it\n                can be queried.\n\n        Args:\n            project_name (str): Project under which the entity is located.\n            entity_type (str): Entity type which passed entity id represents.\n            entity_id (str): Entity id for which thumbnail should be returned.\n            thumbnail_id (Optional[str]): DEPRECATED Use\n                'get_thumbnail_by_id'.\n\n        Returns:\n            ThumbnailContent: Thumbnail content wrapper. Does not have to be\n                valid.\n        \"\"\"\n\n        if thumbnail_id:\n            return self.get_thumbnail_by_id(project_name, thumbnail_id)\n\n        if entity_type in (\n            \"folder\",\n            \"version\",\n            \"workfile\",\n        ):\n            entity_type += \"s\"\n\n        response = self.raw_get(\"projects/{}/{}/{}/thumbnail\".format(\n            project_name,\n            entity_type,\n            entity_id\n        ))\n        return self._prepare_thumbnail_content(project_name, response)\n\n    def get_folder_thumbnail(\n        self, project_name, folder_id, thumbnail_id=None\n    ):\n        \"\"\"Prepared method to receive thumbnail for folder entity.\n\n        Args:\n            project_name (str): Project under which the entity is located.\n            folder_id (str): Folder id for which thumbnail should be returned.\n            thumbnail_id (Optional[str]): Prepared thumbnail id from entity.\n                Used only to check if thumbnail was already cached.\n\n        Returns:\n            Union[str, None]: Path to downloaded thumbnail or none if entity\n                does not have any (or if user does not have permissions).\n        \"\"\"\n\n        return self.get_thumbnail(\n            project_name, \"folder\", folder_id, thumbnail_id\n        )\n\n    def get_version_thumbnail(\n        self, project_name, version_id, thumbnail_id=None\n    ):\n        \"\"\"Prepared method to receive thumbnail for version entity.\n\n        Args:\n            project_name (str): Project under which the entity is located.\n            version_id (str): Version id for which thumbnail should be\n                returned.\n            thumbnail_id (Optional[str]): Prepared thumbnail id from entity.\n                Used only to check if thumbnail was already cached.\n\n        Returns:\n            Union[str, None]: Path to downloaded thumbnail or none if entity\n                does not have any (or if user does not have permissions).\n        \"\"\"\n\n        return self.get_thumbnail(\n            project_name, \"version\", version_id, thumbnail_id\n        )\n\n    def get_workfile_thumbnail(\n        self, project_name, workfile_id, thumbnail_id=None\n    ):\n        \"\"\"Prepared method to receive thumbnail for workfile entity.\n\n        Args:\n            project_name (str): Project under which the entity is located.\n            workfile_id (str): Worfile id for which thumbnail should be\n                returned.\n            thumbnail_id (Optional[str]): Prepared thumbnail id from entity.\n                Used only to check if thumbnail was already cached.\n\n        Returns:\n            Union[str, None]: Path to downloaded thumbnail or none if entity\n                does not have any (or if user does not have permissions).\n        \"\"\"\n\n        return self.get_thumbnail(\n            project_name, \"workfile\", workfile_id, thumbnail_id\n        )\n\n    def _get_thumbnail_mime_type(self, thumbnail_path):\n        \"\"\"Get thumbnail mime type on thumbnail creation based on source path.\n\n        Args:\n            thumbnail_path (str): Path to thumbnail source fie.\n\n        Returns:\n            str: Mime type used for thumbnail creation.\n\n        Raises:\n            ValueError: Mime type cannot be determined.\n        \"\"\"\n\n        ext = os.path.splitext(thumbnail_path)[-1].lower()\n        if ext == \".png\":\n            return \"image/png\"\n\n        elif ext in (\".jpeg\", \".jpg\"):\n            return \"image/jpeg\"\n\n        raise ValueError(\n            \"Thumbnail source file has unknown extensions {}\".format(ext))\n\n    def create_thumbnail(self, project_name, src_filepath, thumbnail_id=None):\n        \"\"\"Create new thumbnail on server from passed path.\n\n        Args:\n            project_name (str): Project where the thumbnail will be created\n                and can be used.\n            src_filepath (str): Filepath to thumbnail which should be uploaded.\n            thumbnail_id (Optional[str]): Prepared if of thumbnail.\n\n        Returns:\n            str: Created thumbnail id.\n\n        Raises:\n            ValueError: When thumbnail source cannot be processed.\n        \"\"\"\n\n        if not os.path.exists(src_filepath):\n            raise ValueError(\"Entered filepath does not exist.\")\n\n        if thumbnail_id:\n            self.update_thumbnail(\n                project_name,\n                thumbnail_id,\n                src_filepath\n            )\n            return thumbnail_id\n\n        mime_type = self._get_thumbnail_mime_type(src_filepath)\n        response = self.upload_file(\n            \"projects/{}/thumbnails\".format(project_name),\n            src_filepath,\n            request_type=RequestTypes.post,\n            headers={\"Content-Type\": mime_type},\n        )\n        response.raise_for_status()\n        return response.json()[\"id\"]\n\n    def update_thumbnail(self, project_name, thumbnail_id, src_filepath):\n        \"\"\"Change thumbnail content by id.\n\n        Update can be also used to create new thumbnail.\n\n        Args:\n            project_name (str): Project where the thumbnail will be created\n                and can be used.\n            thumbnail_id (str): Thumbnail id to update.\n            src_filepath (str): Filepath to thumbnail which should be uploaded.\n\n        Raises:\n            ValueError: When thumbnail source cannot be processed.\n        \"\"\"\n\n        if not os.path.exists(src_filepath):\n            raise ValueError(\"Entered filepath does not exist.\")\n\n        mime_type = self._get_thumbnail_mime_type(src_filepath)\n        response = self.upload_file(\n            \"projects/{}/thumbnails/{}\".format(project_name, thumbnail_id),\n            src_filepath,\n            request_type=RequestTypes.put,\n            headers={\"Content-Type\": mime_type},\n        )\n        response.raise_for_status()\n\n    def create_project(\n        self,\n        project_name,\n        project_code,\n        library_project=False,\n        preset_name=None\n    ):\n        \"\"\"Create project using Ayon settings.\n\n        This project creation function is not validating project entity on\n        creation. It is because project entity is created blindly with only\n        minimum required information about project which is name and code.\n\n        Entered project name must be unique and project must not exist yet.\n\n        Note:\n            This function is here to be OP v4 ready but in v3 has more logic\n                to do. That's why inner imports are in the body.\n\n        Args:\n            project_name (str): New project name. Should be unique.\n            project_code (str): Project's code should be unique too.\n            library_project (Optional[bool]): Project is library project.\n            preset_name (Optional[str]): Name of anatomy preset. Default is\n                used if not passed.\n\n        Raises:\n            ValueError: When project name already exists.\n\n        Returns:\n            dict[str, Any]: Created project entity.\n        \"\"\"\n\n        if self.get_project(project_name):\n            raise ValueError(\"Project with name \\\"{}\\\" already exists\".format(\n                project_name\n            ))\n\n        if not PROJECT_NAME_REGEX.match(project_name):\n            raise ValueError((\n                \"Project name \\\"{}\\\" contain invalid characters\"\n            ).format(project_name))\n\n        preset = self.get_project_anatomy_preset(preset_name)\n\n        result = self.post(\n            \"projects\",\n            name=project_name,\n            code=project_code,\n            anatomy=preset,\n            library=library_project\n        )\n\n        if result.status != 201:\n            details = \"Unknown details ({})\".format(result.status)\n            if result.data:\n                details = result.data.get(\"detail\") or details\n            raise ValueError(\"Failed to create project \\\"{}\\\": {}\".format(\n                project_name, details\n            ))\n\n        return self.get_project(project_name)\n\n    def update_project(\n        self,\n        project_name,\n        library=None,\n        folder_types=None,\n        task_types=None,\n        link_types=None,\n        statuses=None,\n        tags=None,\n        config=None,\n        attrib=None,\n        data=None,\n        active=None,\n        project_code=None,\n        **changes\n    ):\n        \"\"\"Update project entity on server.\n\n        Args:\n            project_name (str): Name of project.\n            library (Optional[bool]): Change library state.\n            folder_types (Optional[list[dict[str, Any]]]): Folder type\n                definitions.\n            task_types (Optional[list[dict[str, Any]]]): Task type\n                definitions.\n            link_types (Optional[list[dict[str, Any]]]): Link type\n                definitions.\n            statuses (Optional[list[dict[str, Any]]]): Status definitions.\n            tags (Optional[list[dict[str, Any]]]): List of tags available to\n                set on entities.\n            config (Optional[dict[dict[str, Any]]]): Project anatomy config\n                with templates and roots.\n            attrib (Optional[dict[str, Any]]): Project attributes to change.\n            data (Optional[dict[str, Any]]): Custom data of a project. This\n                value will 100% override project data.\n            active (Optional[bool]): Change active state of a project.\n            project_code (Optional[str]): Change project code. Not recommended\n                during production.\n            **changes: Other changed keys based on Rest API documentation.\n        \"\"\"\n\n        changes.update({\n            key: value\n            for key, value in (\n                (\"library\", library),\n                (\"folderTypes\", folder_types),\n                (\"taskTypes\", task_types),\n                (\"linkTypes\", link_types),\n                (\"statuses\", statuses),\n                (\"tags\", tags),\n                (\"config\", config),\n                (\"attrib\", attrib),\n                (\"data\", data),\n                (\"active\", active),\n                (\"code\", project_code),\n            )\n            if value is not None\n        })\n        response = self.patch(\n            \"projects/{}\".format(project_name),\n            **changes\n        )\n        response.raise_for_status()\n\n    def delete_project(self, project_name):\n        \"\"\"Delete project from server.\n\n        This will completely remove project from server without any step back.\n\n        Args:\n            project_name (str): Project name that will be removed.\n        \"\"\"\n\n        if not self.get_project(project_name):\n            raise ValueError(\"Project with name \\\"{}\\\" was not found\".format(\n                project_name\n            ))\n\n        result = self.delete(\"projects/{}\".format(project_name))\n        if result.status_code != 204:\n            raise ValueError(\n                \"Failed to delete project \\\"{}\\\". {}\".format(\n                    project_name, result.data[\"detail\"]\n                )\n            )\n\n    # --- Links ---\n    def get_full_link_type_name(self, link_type_name, input_type, output_type):\n        \"\"\"Calculate full link type name used for query from server.\n\n        Args:\n            link_type_name (str): Type of link.\n            input_type (str): Input entity type of link.\n            output_type (str): Output entity type of link.\n\n        Returns:\n            str: Full name of link type used for query from server.\n        \"\"\"\n\n        return \"|\".join([link_type_name, input_type, output_type])\n\n    def get_link_types(self, project_name):\n        \"\"\"All link types available on a project.\n\n        Example output:\n            [\n                {\n                    \"name\": \"reference|folder|folder\",\n                    \"link_type\": \"reference\",\n                    \"input_type\": \"folder\",\n                    \"output_type\": \"folder\",\n                    \"data\": {}\n                }\n            ]\n\n        Args:\n            project_name (str): Name of project where to look for link types.\n\n        Returns:\n            list[dict[str, Any]]: Link types available on project.\n        \"\"\"\n\n        response = self.get(\"projects/{}/links/types\".format(project_name))\n        response.raise_for_status()\n        return response.data[\"types\"]\n\n    def get_link_type(\n        self, project_name, link_type_name, input_type, output_type\n    ):\n        \"\"\"Get link type data.\n\n        There is not dedicated REST endpoint to get single link type,\n        so method 'get_link_types' is used.\n\n        Example output:\n            {\n                \"name\": \"reference|folder|folder\",\n                \"link_type\": \"reference\",\n                \"input_type\": \"folder\",\n                \"output_type\": \"folder\",\n                \"data\": {}\n            }\n\n        Args:\n            project_name (str): Project where link type is available.\n            link_type_name (str): Name of link type.\n            input_type (str): Input entity type of link.\n            output_type (str): Output entity type of link.\n\n        Returns:\n            Union[None, dict[str, Any]]: Link type information.\n        \"\"\"\n\n        full_type_name = self.get_full_link_type_name(\n            link_type_name, input_type, output_type\n        )\n        for link_type in self.get_link_types(project_name):\n            if link_type[\"name\"] == full_type_name:\n                return link_type\n        return None\n\n    def create_link_type(\n        self, project_name, link_type_name, input_type, output_type, data=None\n    ):\n        \"\"\"Create or update link type on server.\n\n        Warning:\n            Because PUT is used for creation it is also used for update.\n\n        Args:\n            project_name (str): Project where link type is created.\n            link_type_name (str): Name of link type.\n            input_type (str): Input entity type of link.\n            output_type (str): Output entity type of link.\n            data (Optional[dict[str, Any]]): Additional data related to link.\n\n        Raises:\n            HTTPRequestError: Server error happened.\n        \"\"\"\n\n        if data is None:\n            data = {}\n        full_type_name = self.get_full_link_type_name(\n            link_type_name, input_type, output_type\n        )\n        response = self.put(\n            \"projects/{}/links/types/{}\".format(project_name, full_type_name),\n            **data\n        )\n        response.raise_for_status()\n\n    def delete_link_type(\n        self, project_name, link_type_name, input_type, output_type\n    ):\n        \"\"\"Remove link type from project.\n\n        Args:\n            project_name (str): Project where link type is created.\n            link_type_name (str): Name of link type.\n            input_type (str): Input entity type of link.\n            output_type (str): Output entity type of link.\n\n        Raises:\n            HTTPRequestError: Server error happened.\n        \"\"\"\n\n        full_type_name = self.get_full_link_type_name(\n            link_type_name, input_type, output_type\n        )\n        response = self.delete(\n            \"projects/{}/links/types/{}\".format(project_name, full_type_name))\n        response.raise_for_status()\n\n    def make_sure_link_type_exists(\n        self, project_name, link_type_name, input_type, output_type, data=None\n    ):\n        \"\"\"Make sure link type exists on a project.\n\n        Args:\n            project_name (str): Name of project.\n            link_type_name (str): Name of link type.\n            input_type (str): Input entity type of link.\n            output_type (str): Output entity type of link.\n            data (Optional[dict[str, Any]]): Link type related data.\n        \"\"\"\n\n        link_type = self.get_link_type(\n            project_name, link_type_name, input_type, output_type)\n        if (\n            link_type\n            and (data is None or data == link_type[\"data\"])\n        ):\n            return\n        self.create_link_type(\n            project_name, link_type_name, input_type, output_type, data\n        )\n\n    def create_link(\n        self,\n        project_name,\n        link_type_name,\n        input_id,\n        input_type,\n        output_id,\n        output_type\n    ):\n        \"\"\"Create link between 2 entities.\n\n        Link has a type which must already exists on a project.\n\n        Example output:\n            {\n                \"id\": \"59a212c0d2e211eda0e20242ac120002\"\n            }\n\n        Args:\n            project_name (str): Project where the link is created.\n            link_type_name (str): Type of link.\n            input_id (str): Id of input entity.\n            input_type (str): Entity type of input entity.\n            output_id (str): Id of output entity.\n            output_type (str): Entity type of output entity.\n\n        Returns:\n            dict[str, str]: Information about link.\n\n        Raises:\n            HTTPRequestError: Server error happened.\n        \"\"\"\n\n        full_link_type_name = self.get_full_link_type_name(\n            link_type_name, input_type, output_type)\n        response = self.post(\n            \"projects/{}/links\".format(project_name),\n            link=full_link_type_name,\n            input=input_id,\n            output=output_id\n        )\n        response.raise_for_status()\n        return response.data\n\n    def delete_link(self, project_name, link_id):\n        \"\"\"Remove link by id.\n\n        Args:\n            project_name (str): Project where link exists.\n            link_id (str): Id of link.\n\n        Raises:\n            HTTPRequestError: Server error happened.\n        \"\"\"\n\n        response = self.delete(\n            \"projects/{}/links/{}\".format(project_name, link_id)\n        )\n        response.raise_for_status()\n\n    def _prepare_link_filters(self, filters, link_types, link_direction):\n        \"\"\"Add links filters for GraphQl queries.\n\n        Args:\n            filters (dict[str, Any]): Object where filters will be added.\n            link_types (Union[Iterable[str], None]): Link types filters.\n            link_direction (Union[Literal[\"in\", \"out\"], None]): Direction of\n                link \"in\", \"out\" or 'None' for both.\n\n        Returns:\n            bool: Links are valid, and query from server can happen.\n        \"\"\"\n\n        if link_types is not None:\n            link_types = set(link_types)\n            if not link_types:\n                return False\n            filters[\"linkTypes\"] = list(link_types)\n\n        if link_direction is not None:\n            if link_direction not in (\"in\", \"out\"):\n                return False\n            filters[\"linkDirection\"] = link_direction\n        return True\n\n    def get_entities_links(\n        self,\n        project_name,\n        entity_type,\n        entity_ids=None,\n        link_types=None,\n        link_direction=None\n    ):\n        \"\"\"Helper method to get links from server for entity types.\n\n        Example output:\n            {\n                \"59a212c0d2e211eda0e20242ac120001\": [\n                    {\n                        \"id\": \"59a212c0d2e211eda0e20242ac120002\",\n                        \"linkType\": \"reference\",\n                        \"description\": \"reference link between folders\",\n                        \"projectName\": \"my_project\",\n                        \"author\": \"frantadmin\",\n                        \"entityId\": \"b1df109676db11ed8e8c6c9466b19aa8\",\n                        \"entityType\": \"folder\",\n                        \"direction\": \"out\"\n                    },\n                    ...\n                ],\n                ...\n            }\n\n        Args:\n            project_name (str): Project where links are.\n            entity_type (Literal[\"folder\", \"task\", \"product\",\n                \"version\", \"representations\"]): Entity type.\n            entity_ids (Optional[Iterable[str]]): Ids of entities for which\n                links should be received.\n            link_types (Optional[Iterable[str]]): Link type filters.\n            link_direction (Optional[Literal[\"in\", \"out\"]]): Link direction\n                filter.\n\n        Returns:\n            dict[str, list[dict[str, Any]]]: Link info by entity ids.\n        \"\"\"\n\n        if entity_type == \"folder\":\n            query_func = folders_graphql_query\n            id_filter_key = \"folderIds\"\n            project_sub_key = \"folders\"\n        elif entity_type == \"task\":\n            query_func = tasks_graphql_query\n            id_filter_key = \"taskIds\"\n            project_sub_key = \"tasks\"\n        elif entity_type == \"product\":\n            query_func = products_graphql_query\n            id_filter_key = \"productIds\"\n            project_sub_key = \"products\"\n        elif entity_type == \"version\":\n            query_func = versions_graphql_query\n            id_filter_key = \"versionIds\"\n            project_sub_key = \"versions\"\n        elif entity_type == \"representation\":\n            query_func = representations_graphql_query\n            id_filter_key = \"representationIds\"\n            project_sub_key = \"representations\"\n        else:\n            raise ValueError(\"Unknown type \\\"{}\\\". Expected {}\".format(\n                entity_type,\n                \", \".join(\n                    (\"folder\", \"task\", \"product\", \"version\", \"representation\")\n                )\n            ))\n\n        output = collections.defaultdict(list)\n        filters = {\n            \"projectName\": project_name\n        }\n        if entity_ids is not None:\n            entity_ids = set(entity_ids)\n            if not entity_ids:\n                return output\n            filters[id_filter_key] = list(entity_ids)\n\n        if not self._prepare_link_filters(filters, link_types, link_direction):\n            return output\n\n        query = query_func({\"id\", \"links\"})\n        for attr, filter_value in filters.items():\n            query.set_variable_value(attr, filter_value)\n\n        for parsed_data in query.continuous_query(self):\n            for entity in parsed_data[\"project\"][project_sub_key]:\n                entity_id = entity[\"id\"]\n                output[entity_id].extend(entity[\"links\"])\n        return output\n\n    def get_folders_links(\n        self,\n        project_name,\n        folder_ids=None,\n        link_types=None,\n        link_direction=None\n    ):\n        \"\"\"Query folders links from server.\n\n        Args:\n            project_name (str): Project where links are.\n            folder_ids (Optional[Iterable[str]]): Ids of folders for which\n                links should be received.\n            link_types (Optional[Iterable[str]]): Link type filters.\n            link_direction (Optional[Literal[\"in\", \"out\"]]): Link direction\n                filter.\n\n        Returns:\n            dict[str, list[dict[str, Any]]]: Link info by folder ids.\n        \"\"\"\n\n        return self.get_entities_links(\n            project_name, \"folder\", folder_ids, link_types, link_direction\n        )\n\n    def get_folder_links(\n        self,\n        project_name,\n        folder_id,\n        link_types=None,\n        link_direction=None\n    ):\n        \"\"\"Query folder links from server.\n\n        Args:\n            project_name (str): Project where links are.\n            folder_id (str): Folder id for which links should be received.\n            link_types (Optional[Iterable[str]]): Link type filters.\n            link_direction (Optional[Literal[\"in\", \"out\"]]): Link direction\n                filter.\n\n        Returns:\n            list[dict[str, Any]]: Link info of folder.\n        \"\"\"\n\n        return self.get_folders_links(\n            project_name, [folder_id], link_types, link_direction\n        )[folder_id]\n\n    def get_tasks_links(\n        self,\n        project_name,\n        task_ids=None,\n        link_types=None,\n        link_direction=None\n    ):\n        \"\"\"Query tasks links from server.\n\n        Args:\n            project_name (str): Project where links are.\n            task_ids (Optional[Iterable[str]]): Ids of tasks for which\n                links should be received.\n            link_types (Optional[Iterable[str]]): Link type filters.\n            link_direction (Optional[Literal[\"in\", \"out\"]]): Link direction\n                filter.\n\n        Returns:\n            dict[str, list[dict[str, Any]]]: Link info by task ids.\n        \"\"\"\n\n        return self.get_entities_links(\n            project_name, \"task\", task_ids, link_types, link_direction\n        )\n\n    def get_task_links(\n        self,\n        project_name,\n        task_id,\n        link_types=None,\n        link_direction=None\n    ):\n        \"\"\"Query task links from server.\n\n        Args:\n            project_name (str): Project where links are.\n            task_id (str): Task id for which links should be received.\n            link_types (Optional[Iterable[str]]): Link type filters.\n            link_direction (Optional[Literal[\"in\", \"out\"]]): Link direction\n                filter.\n\n        Returns:\n            list[dict[str, Any]]: Link info of task.\n        \"\"\"\n\n        return self.get_tasks_links(\n            project_name, [task_id], link_types, link_direction\n        )[task_id]\n\n    def get_products_links(\n        self,\n        project_name,\n        product_ids=None,\n        link_types=None,\n        link_direction=None\n    ):\n        \"\"\"Query products links from server.\n\n        Args:\n            project_name (str): Project where links are.\n            product_ids (Optional[Iterable[str]]): Ids of products for which\n                links should be received.\n            link_types (Optional[Iterable[str]]): Link type filters.\n            link_direction (Optional[Literal[\"in\", \"out\"]]): Link direction\n                filter.\n\n        Returns:\n            dict[str, list[dict[str, Any]]]: Link info by product ids.\n        \"\"\"\n\n        return self.get_entities_links(\n            project_name, \"product\", product_ids, link_types, link_direction\n        )\n\n    def get_product_links(\n        self,\n        project_name,\n        product_id,\n        link_types=None,\n        link_direction=None\n    ):\n        \"\"\"Query product links from server.\n\n        Args:\n            project_name (str): Project where links are.\n            product_id (str): Product id for which links should be received.\n            link_types (Optional[Iterable[str]]): Link type filters.\n            link_direction (Optional[Literal[\"in\", \"out\"]]): Link direction\n                filter.\n\n        Returns:\n            list[dict[str, Any]]: Link info of product.\n        \"\"\"\n\n        return self.get_products_links(\n            project_name, [product_id], link_types, link_direction\n        )[product_id]\n\n    def get_versions_links(\n        self,\n        project_name,\n        version_ids=None,\n        link_types=None,\n        link_direction=None\n    ):\n        \"\"\"Query versions links from server.\n\n        Args:\n            project_name (str): Project where links are.\n            version_ids (Optional[Iterable[str]]): Ids of versions for which\n                links should be received.\n            link_types (Optional[Iterable[str]]): Link type filters.\n            link_direction (Optional[Literal[\"in\", \"out\"]]): Link direction\n                filter.\n\n        Returns:\n            dict[str, list[dict[str, Any]]]: Link info by version ids.\n        \"\"\"\n\n        return self.get_entities_links(\n            project_name, \"version\", version_ids, link_types, link_direction\n        )\n\n    def get_version_links(\n        self,\n        project_name,\n        version_id,\n        link_types=None,\n        link_direction=None\n    ):\n        \"\"\"Query version links from server.\n\n        Args:\n            project_name (str): Project where links are.\n            version_id (str): Version id for which links should be received.\n            link_types (Optional[Iterable[str]]): Link type filters.\n            link_direction (Optional[Literal[\"in\", \"out\"]]): Link direction\n                filter.\n\n        Returns:\n            list[dict[str, Any]]: Link info of version.\n        \"\"\"\n\n        return self.get_versions_links(\n            project_name, [version_id], link_types, link_direction\n        )[version_id]\n\n    def get_representations_links(\n        self,\n        project_name,\n        representation_ids=None,\n        link_types=None,\n        link_direction=None\n    ):\n        \"\"\"Query representations links from server.\n\n        Args:\n            project_name (str): Project where links are.\n            representation_ids (Optional[Iterable[str]]): Ids of\n                representations for which links should be received.\n            link_types (Optional[Iterable[str]]): Link type filters.\n            link_direction (Optional[Literal[\"in\", \"out\"]]): Link direction\n                filter.\n\n        Returns:\n            dict[str, list[dict[str, Any]]]: Link info by representation ids.\n        \"\"\"\n\n        return self.get_entities_links(\n            project_name,\n            \"representation\",\n            representation_ids,\n            link_types,\n            link_direction\n        )\n\n    def get_representation_links(\n        self,\n        project_name,\n        representation_id,\n        link_types=None,\n        link_direction=None\n    ):\n        \"\"\"Query representation links from server.\n\n        Args:\n            project_name (str): Project where links are.\n            representation_id (str): Representation id for which links\n                should be received.\n            link_types (Optional[Iterable[str]]): Link type filters.\n            link_direction (Optional[Literal[\"in\", \"out\"]]): Link direction\n                filter.\n\n        Returns:\n            list[dict[str, Any]]: Link info of representation.\n        \"\"\"\n\n        return self.get_representations_links(\n            project_name, [representation_id], link_types, link_direction\n        )[representation_id]\n\n    # --- Batch operations processing ---\n    def send_batch_operations(\n        self,\n        project_name,\n        operations,\n        can_fail=False,\n        raise_on_fail=True\n    ):\n        \"\"\"Post multiple CRUD operations to server.\n\n        When multiple changes should be made on server side this is the best\n        way to go. It is possible to pass multiple operations to process on a\n        server side and do the changes in a transaction.\n\n        Args:\n            project_name (str): On which project should be operations\n                processed.\n            operations (list[dict[str, Any]]): Operations to be processed.\n            can_fail (Optional[bool]): Server will try to process all\n                operations even if one of them fails.\n            raise_on_fail (Optional[bool]): Raise exception if an operation\n                fails. You can handle failed operations on your own\n                when set to 'False'.\n\n        Raises:\n            ValueError: Operations can't be converted to json string.\n            FailedOperations: When output does not contain server operations\n                or 'raise_on_fail' is enabled and any operation fails.\n\n        Returns:\n            list[dict[str, Any]]: Operations result with process details.\n        \"\"\"\n\n        if not operations:\n            return []\n\n        body_by_id = {}\n        operations_body = []\n        for operation in operations:\n            if not operation:\n                continue\n\n            op_id = operation.get(\"id\")\n            if not op_id:\n                op_id = create_entity_id()\n                operation[\"id\"] = op_id\n\n            try:\n                body = json.loads(\n                    json.dumps(operation, default=entity_data_json_default)\n                )\n            except:\n                raise ValueError(\"Couldn't json parse body: {}\".format(\n                    json.dumps(\n                        operation, indent=4, default=failed_json_default\n                    )\n                ))\n\n            body_by_id[op_id] = body\n            operations_body.append(body)\n\n        if not operations_body:\n            return []\n\n        result = self.post(\n            \"projects/{}/operations\".format(project_name),\n            operations=operations_body,\n            canFail=can_fail\n        )\n\n        op_results = result.get(\"operations\")\n        if op_results is None:\n            raise FailedOperations(\n                \"Operation failed. Content: {}\".format(str(result))\n            )\n\n        if result.get(\"success\") or not raise_on_fail:\n            return op_results\n\n        for op_result in op_results:\n            if not op_result[\"success\"]:\n                operation_id = op_result[\"id\"]\n                raise FailedOperations((\n                    \"Operation \\\"{}\\\" failed with data:\\n{}\\nDetail: {}.\"\n                ).format(\n                    operation_id,\n                    json.dumps(body_by_id[operation_id], indent=4),\n                    op_result[\"detail\"],\n                ))\n        return op_results\n\n    def _convert_entity_data(self, entity):\n        if not entity:\n            return\n        entity_data = entity.get(\"data\")\n        if (\n            entity_data is not None\n            and isinstance(entity_data, six.string_types)\n        ):\n            entity[\"data\"] = json.loads(entity_data)\n"
  },
  {
    "path": "openpype/vendor/python/common/ayon_api/utils.py",
    "content": "import os\nimport re\nimport datetime\nimport uuid\nimport string\nimport platform\nimport collections\ntry:\n    # Python 3\n    from urllib.parse import urlparse, urlencode\nexcept ImportError:\n    # Python 2\n    from urlparse import urlparse\n    from urllib import urlencode\n\nimport requests\nimport unidecode\n\nfrom .constants import (\n    SERVER_TIMEOUT_ENV_KEY,\n    DEFAULT_VARIANT_ENV_KEY,\n    SITE_ID_ENV_KEY,\n)\nfrom .exceptions import UrlError\n\nREMOVED_VALUE = object()\nSLUGIFY_WHITELIST = string.ascii_letters + string.digits\nSLUGIFY_SEP_WHITELIST = \" ,./\\\\;:!|*^#@~+-_=\"\n\nRepresentationParents = collections.namedtuple(\n    \"RepresentationParents\",\n    (\"version\", \"product\", \"folder\", \"project\")\n)\n\n\ndef get_default_timeout():\n    \"\"\"Default value for requests timeout.\n\n    First looks for environment variable SERVER_TIMEOUT_ENV_KEY which\n    can affect timeout value. If not available then use 10.0 s.\n\n    Returns:\n        float: Timeout value in seconds.\n    \"\"\"\n\n    try:\n        return float(os.environ.get(SERVER_TIMEOUT_ENV_KEY))\n    except (ValueError, TypeError):\n        pass\n    return 10.0\n\n\ndef get_default_settings_variant():\n    \"\"\"Default settings variant.\n\n    Returns:\n        str: Settings variant from environment variable or 'production'.\n    \"\"\"\n\n    return os.environ.get(DEFAULT_VARIANT_ENV_KEY) or \"production\"\n\n\ndef get_default_site_id():\n    \"\"\"Site id used for server connection.\n\n    Returns:\n        Union[str, None]: Site id from environment variable or None.\n    \"\"\"\n\n    return os.environ.get(SITE_ID_ENV_KEY)\n\n\nclass ThumbnailContent:\n    \"\"\"Wrapper for thumbnail content.\n\n    Args:\n        project_name (str): Project name.\n        thumbnail_id (Union[str, None]): Thumbnail id.\n        content_type (Union[str, None]): Content type e.g. 'image/png'.\n        content (Union[bytes, None]): Thumbnail content.\n    \"\"\"\n\n    def __init__(self, project_name, thumbnail_id, content, content_type):\n        self.project_name = project_name\n        self.thumbnail_id = thumbnail_id\n        self.content_type = content_type\n        self.content = content or b\"\"\n\n    @property\n    def id(self):\n        \"\"\"Wrapper for thumbnail id.\n\n        Returns:\n\n        \"\"\"\n\n        return self.thumbnail_id\n\n    @property\n    def is_valid(self):\n        \"\"\"Content of thumbnail is valid.\n\n        Returns:\n            bool: Content is valid and can be used.\n        \"\"\"\n        return (\n            self.thumbnail_id is not None\n            and self.content_type is not None\n        )\n\n\ndef prepare_query_string(key_values):\n    \"\"\"Prepare data to query string.\n\n    If there are any values a query starting with '?' is returned otherwise\n    an empty string.\n\n    Args:\n         dict[str, Any]: Query values.\n\n    Returns:\n        str: Query string.\n    \"\"\"\n\n    if not key_values:\n        return \"\"\n    return \"?{}\".format(urlencode(key_values))\n\n\ndef create_entity_id():\n    return uuid.uuid1().hex\n\n\ndef convert_entity_id(entity_id):\n    if not entity_id:\n        return None\n\n    if isinstance(entity_id, uuid.UUID):\n        return entity_id.hex\n\n    try:\n        return uuid.UUID(entity_id).hex\n\n    except (TypeError, ValueError, AttributeError):\n        pass\n    return None\n\n\ndef convert_or_create_entity_id(entity_id=None):\n    output = convert_entity_id(entity_id)\n    if output is None:\n        output = create_entity_id()\n    return output\n\n\ndef entity_data_json_default(value):\n    if isinstance(value, datetime.datetime):\n        return int(value.timestamp())\n\n    raise TypeError(\n        \"Object of type {} is not JSON serializable\".format(str(type(value)))\n    )\n\n\ndef slugify_string(\n    input_string,\n    separator=\"_\",\n    slug_whitelist=SLUGIFY_WHITELIST,\n    split_chars=SLUGIFY_SEP_WHITELIST,\n    min_length=1,\n    lower=False,\n    make_set=False,\n):\n    \"\"\"Slugify a text string.\n\n    This function removes transliterates input string to ASCII, removes\n    special characters and use join resulting elements using\n    specified separator.\n\n    Args:\n        input_string (str): Input string to slugify\n        separator (str): A string used to separate returned elements\n            (default: \"_\")\n        slug_whitelist (str): Characters allowed in the output\n            (default: ascii letters, digits and the separator)\n        split_chars (str): Set of characters used for word splitting\n            (there is a sane default)\n        lower (bool): Convert to lower-case (default: False)\n        make_set (bool): Return \"set\" object instead of string.\n        min_length (int): Minimal length of an element (word).\n\n    Returns:\n        Union[str, Set[str]]: Based on 'make_set' value returns slugified\n            string.\n    \"\"\"\n\n    tmp_string = unidecode.unidecode(input_string)\n    if lower:\n        tmp_string = tmp_string.lower()\n\n    parts = [\n        # Remove all characters that are not in whitelist\n        re.sub(\"[^{}]\".format(re.escape(slug_whitelist)), \"\", part)\n        # Split text into part by split characters\n        for part in re.split(\"[{}]\".format(re.escape(split_chars)), tmp_string)\n    ]\n    # Filter text parts by length\n    filtered_parts = [\n        part\n        for part in parts\n        if len(part) >= min_length\n    ]\n    if make_set:\n        return set(filtered_parts)\n    return separator.join(filtered_parts)\n\n\ndef failed_json_default(value):\n    return \"< Failed value {} > {}\".format(type(value), str(value))\n\n\ndef prepare_attribute_changes(old_entity, new_entity, replace=False):\n    attrib_changes = {}\n    new_attrib = new_entity.get(\"attrib\")\n    old_attrib = old_entity.get(\"attrib\")\n    if new_attrib is None:\n        if not replace:\n            return attrib_changes\n        new_attrib = {}\n\n    if old_attrib is None:\n        return new_attrib\n\n    for attr, new_attr_value in new_attrib.items():\n        old_attr_value = old_attrib.get(attr)\n        if old_attr_value != new_attr_value:\n            attrib_changes[attr] = new_attr_value\n\n    if replace:\n        for attr in old_attrib:\n            if attr not in new_attrib:\n                attrib_changes[attr] = REMOVED_VALUE\n\n    return attrib_changes\n\n\ndef prepare_entity_changes(old_entity, new_entity, replace=False):\n    \"\"\"Prepare changes of entities.\"\"\"\n\n    changes = {}\n    for key, new_value in new_entity.items():\n        if key == \"attrib\":\n            continue\n\n        old_value = old_entity.get(key)\n        if old_value != new_value:\n            changes[key] = new_value\n\n    if replace:\n        for key in old_entity:\n            if key not in new_entity:\n                changes[key] = REMOVED_VALUE\n\n    attr_changes = prepare_attribute_changes(old_entity, new_entity, replace)\n    if attr_changes:\n        changes[\"attrib\"] = attr_changes\n    return changes\n\n\ndef _try_parse_url(url):\n    try:\n        return urlparse(url)\n    except BaseException:\n        return None\n\n\ndef _try_connect_to_server(url, timeout=None):\n    if timeout is None:\n        timeout = get_default_timeout()\n    try:\n        # TODO add validation if the url lead to Ayon server\n        #   - this won't validate if the url lead to 'google.com'\n        requests.get(url, timeout=timeout)\n\n    except BaseException:\n        return False\n    return True\n\n\ndef login_to_server(url, username, password, timeout=None):\n    \"\"\"Use login to the server to receive token.\n\n    Args:\n        url (str): Server url.\n        username (str): User's username.\n        password (str): User's password.\n        timeout (Optional[float]): Timeout for request. Value from\n            'get_default_timeout' is used if not specified.\n\n    Returns:\n        Union[str, None]: User's token if login was successfull.\n            Otherwise 'None'.\n    \"\"\"\n\n    if timeout is None:\n        timeout = get_default_timeout()\n    headers = {\"Content-Type\": \"application/json\"}\n    response = requests.post(\n        \"{}/api/auth/login\".format(url),\n        headers=headers,\n        json={\n            \"name\": username,\n            \"password\": password\n        },\n        timeout=timeout,\n    )\n    token = None\n    # 200 - success\n    # 401 - invalid credentials\n    # *   - other issues\n    if response.status_code == 200:\n        token = response.json()[\"token\"]\n    return token\n\n\ndef logout_from_server(url, token, timeout=None):\n    \"\"\"Logout from server and throw token away.\n\n    Args:\n        url (str): Url from which should be logged out.\n        token (str): Token which should be used to log out.\n        timeout (Optional[float]): Timeout for request. Value from\n            'get_default_timeout' is used if not specified.\n    \"\"\"\n\n    if timeout is None:\n        timeout = get_default_timeout()\n    headers = {\n        \"Content-Type\": \"application/json\",\n        \"Authorization\": \"Bearer {}\".format(token)\n    }\n    requests.post(\n        url + \"/api/auth/logout\",\n        headers=headers,\n        timeout=timeout,\n    )\n\n\ndef is_token_valid(url, token, timeout=None):\n    \"\"\"Check if token is valid.\n\n    Token can be a user token or service api key.\n\n    Args:\n        url (str): Server url.\n        token (str): User's token.\n        timeout (Optional[float]): Timeout for request. Value from\n            'get_default_timeout' is used if not specified.\n\n    Returns:\n        bool: True if token is valid.\n    \"\"\"\n\n    if timeout is None:\n        timeout = get_default_timeout()\n\n    base_headers = {\n        \"Content-Type\": \"application/json\",\n    }\n    for header_value in (\n        {\"Authorization\": \"Bearer {}\".format(token)},\n        {\"X-Api-Key\": token},\n    ):\n        headers = base_headers.copy()\n        headers.update(header_value)\n        response = requests.get(\n            \"{}/api/users/me\".format(url),\n            headers=headers,\n            timeout=timeout,\n        )\n        if response.status_code == 200:\n            return True\n    return False\n\n\ndef validate_url(url, timeout=None):\n    \"\"\"Validate url if is valid and server is available.\n\n    Validation checks if can be parsed as url and contains scheme.\n\n    Function will try to autofix url thus will return modified url when\n    connection to server works.\n\n    ```python\n    my_url = \"my.server.url\"\n    try:\n        # Store new url\n        validated_url = validate_url(my_url)\n\n    except UrlError:\n        # Handle invalid url\n        ...\n    ```\n\n    Args:\n        url (str): Server url.\n        timeout (Optional[int]): Timeout in seconds for connection to server.\n\n    Returns:\n        Url which was used to connect to server.\n\n    Raises:\n        UrlError: Error with short description and hints for user.\n    \"\"\"\n\n    stripperd_url = url.strip()\n    if not stripperd_url:\n        raise UrlError(\n            \"Invalid url format. Url is empty.\",\n            title=\"Invalid url format\",\n            hints=[\"url seems to be empty\"]\n        )\n\n    # Not sure if this is good idea?\n    modified_url = stripperd_url.rstrip(\"/\")\n    parsed_url = _try_parse_url(modified_url)\n    universal_hints = [\n        \"does the url work in browser?\"\n    ]\n    if parsed_url is None:\n        raise UrlError(\n            \"Invalid url format. Url cannot be parsed as url \\\"{}\\\".\".format(\n                modified_url\n            ),\n            title=\"Invalid url format\",\n            hints=universal_hints\n        )\n\n    # Try add 'https://' scheme if is missing\n    # - this will trigger UrlError if both will crash\n    if not parsed_url.scheme:\n        new_url = \"https://\" + modified_url\n        if _try_connect_to_server(new_url, timeout=timeout):\n            return new_url\n\n    if _try_connect_to_server(modified_url, timeout=timeout):\n        return modified_url\n\n    hints = []\n    if \"/\" in parsed_url.path or not parsed_url.scheme:\n        new_path = parsed_url.path.split(\"/\")[0]\n        if not parsed_url.scheme:\n            new_path = \"https://\" + new_path\n\n        hints.append(\n            \"did you mean \\\"{}\\\"?\".format(parsed_url.scheme + new_path)\n        )\n\n    raise UrlError(\n        \"Couldn't connect to server on \\\"{}\\\"\".format(url),\n        title=\"Couldn't connect to server\",\n        hints=hints + universal_hints\n    )\n\n\nclass TransferProgress:\n    \"\"\"Object to store progress of download/upload from/to server.\"\"\"\n\n    def __init__(self):\n        self._started = False\n        self._transfer_done = False\n        self._transferred = 0\n        self._content_size = None\n\n        self._failed = False\n        self._fail_reason = None\n\n        self._source_url = \"N/A\"\n        self._destination_url = \"N/A\"\n\n    def get_content_size(self):\n        \"\"\"Content size in bytes.\n\n        Returns:\n            Union[int, None]: Content size in bytes or None\n                if is unknown.\n        \"\"\"\n\n        return self._content_size\n\n    def set_content_size(self, content_size):\n        \"\"\"Set content size in bytes.\n\n        Args:\n            content_size (int): Content size in bytes.\n\n        Raises:\n            ValueError: If content size was already set.\n        \"\"\"\n\n        if self._content_size is not None:\n            raise ValueError(\"Content size was set more then once\")\n        self._content_size = content_size\n\n    def get_started(self):\n        \"\"\"Transfer was started.\n\n        Returns:\n            bool: True if transfer started.\n        \"\"\"\n\n        return self._started\n\n    def set_started(self):\n        \"\"\"Mark that transfer started.\n\n        Raises:\n            ValueError: If transfer was already started.\n        \"\"\"\n\n        if self._started:\n            raise ValueError(\"Progress already started\")\n        self._started = True\n\n    def get_transfer_done(self):\n        \"\"\"Transfer finished.\n\n        Returns:\n            bool: Transfer finished.\n        \"\"\"\n\n        return self._transfer_done\n\n    def set_transfer_done(self):\n        \"\"\"Mark progress as transfer finished.\n\n        Raises:\n            ValueError: If progress was already marked as done\n                or wasn't started yet.\n        \"\"\"\n\n        if self._transfer_done:\n            raise ValueError(\"Progress was already marked as done\")\n        if not self._started:\n            raise ValueError(\"Progress didn't start yet\")\n        self._transfer_done = True\n\n    def get_failed(self):\n        \"\"\"Transfer failed.\n\n        Returns:\n            bool: True if transfer failed.\n        \"\"\"\n\n        return self._failed\n\n    def get_fail_reason(self):\n        \"\"\"Get reason why transfer failed.\n\n        Returns:\n            Union[str, None]: Reason why transfer\n                failed or None.\n        \"\"\"\n\n        return self._fail_reason\n\n    def set_failed(self, reason):\n        \"\"\"Mark progress as failed.\n\n        Args:\n            reason (str): Reason why transfer failed.\n        \"\"\"\n\n        self._fail_reason = reason\n        self._failed = True\n\n    def get_transferred_size(self):\n        \"\"\"Already transferred size in bytes.\n\n        Returns:\n            int: Already transferred size in bytes.\n        \"\"\"\n\n        return self._transferred\n\n    def set_transferred_size(self, transferred):\n        \"\"\"Set already transferred size in bytes.\n\n        Args:\n            transferred (int): Already transferred size in bytes.\n        \"\"\"\n\n        self._transferred = transferred\n\n    def add_transferred_chunk(self, chunk_size):\n        \"\"\"Add transferred chunk size in bytes.\n\n        Args:\n            chunk_size (int): Add transferred chunk size\n                in bytes.\n        \"\"\"\n\n        self._transferred += chunk_size\n\n    def get_source_url(self):\n        \"\"\"Source url from where transfer happens.\n\n        Note:\n            Consider this as title. Must be set using\n                'set_source_url' or 'N/A' will be returned.\n\n        Returns:\n            str: Source url from where transfer happens.\n        \"\"\"\n\n        return self._source_url\n\n    def set_source_url(self, url):\n        \"\"\"Set source url from where transfer happens.\n\n        Args:\n            url (str): Source url from where transfer happens.\n        \"\"\"\n\n        self._source_url = url\n\n    def get_destination_url(self):\n        \"\"\"Destination url where transfer happens.\n\n        Note:\n            Consider this as title. Must be set using\n                'set_source_url' or 'N/A' will be returned.\n\n        Returns:\n            str: Destination url where transfer happens.\n        \"\"\"\n\n        return self._destination_url\n\n    def set_destination_url(self, url):\n        \"\"\"Set destination url where transfer happens.\n\n        Args:\n            url (str): Destination url where transfer happens.\n        \"\"\"\n\n        self._destination_url = url\n\n    @property\n    def is_running(self):\n        \"\"\"Check if transfer is running.\n\n        Returns:\n            bool: True if transfer is running.\n        \"\"\"\n\n        if (\n            not self.started\n            or self.transfer_done\n            or self.failed\n        ):\n            return False\n        return True\n\n    @property\n    def transfer_progress(self):\n        \"\"\"Get transfer progress in percents.\n\n        Returns:\n            Union[float, None]: Transfer progress in percents or 'None'\n                if content size is unknown.\n        \"\"\"\n\n        if self._content_size is None:\n            return None\n        return (self._transferred * 100.0) / float(self._content_size)\n\n    content_size = property(get_content_size, set_content_size)\n    started = property(get_started)\n    transfer_done = property(get_transfer_done)\n    failed = property(get_failed)\n    fail_reason = property(get_fail_reason)\n    source_url = property(get_source_url, set_source_url)\n    destination_url = property(get_destination_url, set_destination_url)\n    transferred_size = property(get_transferred_size, set_transferred_size)\n\n\ndef create_dependency_package_basename(platform_name=None):\n    \"\"\"Create basename for dependency package file.\n\n    Args:\n        platform_name (Optional[str]): Name of platform for which the\n            bundle is targeted. Default value is current platform.\n\n    Returns:\n        str: Dependency package name with timestamp and platform.\n    \"\"\"\n\n    if platform_name is None:\n        platform_name = platform.system().lower()\n\n    now_date = datetime.datetime.now()\n    time_stamp = now_date.strftime(\"%y%m%d%H%M\")\n    return \"ayon_{}_{}\".format(time_stamp, platform_name)\n"
  },
  {
    "path": "openpype/vendor/python/common/ayon_api/version.py",
    "content": "\"\"\"Package declaring Python API for Ayon server.\"\"\"\n__version__ = \"1.0.0-rc.3\"\n"
  },
  {
    "path": "openpype/vendor/python/common/capture.py",
    "content": "\"\"\"Maya Capture\n\nPlayblasting with independent viewport, camera and display options\n\n\"\"\"\n\nimport re\nimport sys\nimport contextlib\nimport logging\n\nfrom maya import cmds\nfrom maya import mel\n\ntry:\n    from PySide2 import QtGui, QtWidgets\nexcept ImportError:\n    from PySide import QtGui\n    QtWidgets = QtGui\n\nversion_info = (2, 3, 0)\n\n__version__ = \"%s.%s.%s\" % version_info\n__license__ = \"MIT\"\nlogger = logging.getLogger(\"capture\")\n\n\ndef capture(camera=None,\n            width=None,\n            height=None,\n            filename=None,\n            start_frame=None,\n            end_frame=None,\n            frame=None,\n            format='qt',\n            compression='H.264',\n            quality=100,\n            off_screen=False,\n            viewer=True,\n            show_ornaments=True,\n            sound=None,\n            isolate=None,\n            maintain_aspect_ratio=True,\n            overwrite=False,\n            frame_padding=4,\n            raw_frame_numbers=False,\n            camera_options=None,\n            display_options=None,\n            viewport_options=None,\n            viewport2_options=None,\n            complete_filename=None,\n            log=None):\n    \"\"\"Playblast in an independent panel\n\n    Arguments:\n        camera (str, optional): Name of camera, defaults to \"persp\"\n        width (int, optional): Width of output in pixels\n        height (int, optional): Height of output in pixels\n        filename (str, optional): Name of output file. If\n            none is specified, no files are saved.\n        start_frame (float, optional): Defaults to current start frame.\n        end_frame (float, optional): Defaults to current end frame.\n        frame (float or tuple, optional): A single frame or list of frames.\n            Use this to capture a single frame or an arbitrary sequence of\n            frames.\n        format (str, optional): Name of format, defaults to \"qt\".\n        compression (str, optional): Name of compression, defaults to \"H.264\"\n        quality (int, optional): The quality of the output, defaults to 100\n        off_screen (bool, optional): Whether or not to playblast off screen\n        viewer (bool, optional): Display results in native player\n        show_ornaments (bool, optional): Whether or not model view ornaments\n            (e.g. axis icon, grid and HUD) should be displayed.\n        sound (str, optional):  Specify the sound node to be used during\n            playblast. When None (default) no sound will be used.\n        isolate (list): List of nodes to isolate upon capturing\n        maintain_aspect_ratio (bool, optional): Modify height in order to\n            maintain aspect ratio.\n        overwrite (bool, optional): Whether or not to overwrite if file\n            already exists. If disabled and file exists and error will be\n            raised.\n        frame_padding (bool, optional): Number of zeros used to pad file name\n            for image sequences.\n        raw_frame_numbers (bool, optional): Whether or not to use the exact\n            frame numbers from the scene or capture to a sequence starting at\n            zero. Defaults to False. When set to True `viewer` can't be used\n            and will be forced to False.\n        camera_options (dict, optional): Supplied camera options,\n            using `CameraOptions`\n        display_options (dict, optional): Supplied display\n            options, using `DisplayOptions`\n        viewport_options (dict, optional): Supplied viewport\n            options, using `ViewportOptions`\n        viewport2_options (dict, optional): Supplied display\n            options, using `Viewport2Options`\n        complete_filename (str, optional): Exact name of output file. Use this\n            to override the output of `filename` so it excludes frame padding.\n        log (logger, optional): pass logger for logging messages.\n\n    Example:\n        >>> # Launch default capture\n        >>> capture()\n        >>> # Launch capture with custom viewport settings\n        >>> capture('persp', 800, 600,\n        ...         viewport_options={\n        ...             \"displayAppearance\": \"wireframe\",\n        ...             \"grid\": False,\n        ...             \"polymeshes\": True,\n        ...         },\n        ...         camera_options={\n        ...             \"displayResolution\": True\n        ...         }\n        ... )\n\n\n    \"\"\"\n    global logger\n    if log:\n        logger = log\n    camera = camera or \"persp\"\n\n    # Ensure camera exists\n    if not cmds.objExists(camera):\n        raise RuntimeError(\"Camera does not exist: {0}\".format(camera))\n\n    if width and height :\n        maintain_aspect_ratio = False\n    width = width or cmds.getAttr(\"defaultResolution.width\")\n    height = height or cmds.getAttr(\"defaultResolution.height\")\n    if maintain_aspect_ratio:\n        ratio = cmds.getAttr(\"defaultResolution.deviceAspectRatio\")\n        height = round(width / ratio)\n\n    if start_frame is None:\n        start_frame = cmds.playbackOptions(minTime=True, query=True)\n    if end_frame is None:\n        end_frame = cmds.playbackOptions(maxTime=True, query=True)\n\n    # (#74) Bugfix: `maya.cmds.playblast` will raise an error when playblasting\n    # with `rawFrameNumbers` set to True but no explicit `frames` provided.\n    # Since we always know what frames will be included we can provide it\n    # explicitly\n    if raw_frame_numbers and frame is None:\n        frame = range(int(start_frame), int(end_frame) + 1)\n\n    # We need to wrap `completeFilename`, otherwise even when None is provided\n    # it will use filename as the exact name. Only when lacking as argument\n    # does it function correctly.\n    playblast_kwargs = dict()\n    if complete_filename:\n        playblast_kwargs['completeFilename'] = complete_filename\n    if frame is not None:\n        playblast_kwargs['frame'] = frame\n    if sound is not None:\n        playblast_kwargs['sound'] = sound\n\n    # We need to raise an error when the user gives a custom frame range with\n    # negative frames in combination with raw frame numbers. This will result\n    # in a minimal integer frame number : filename.-2147483648.png for any\n    # negative rendered frame\n    if frame and raw_frame_numbers:\n        check = frame if isinstance(frame, (list, tuple)) else [frame]\n        if any(f < 0 for f in check):\n            raise RuntimeError(\"Negative frames are not supported with \"\n                               \"raw frame numbers and explicit frame numbers\")\n\n    # (#21) Bugfix: `maya.cmds.playblast` suffers from undo bug where it\n    # always sets the currentTime to frame 1. By setting currentTime before\n    # the playblast call it'll undo correctly.\n    cmds.currentTime(cmds.currentTime(query=True))\n\n    padding = 10  # Extend panel to accommodate for OS window manager\n\n    with _independent_panel(width=width + padding,\n                            height=height + padding,\n                            off_screen=off_screen) as panel:\n        cmds.setFocus(panel)\n\n        all_playblast_kwargs = {\n            \"compression\": compression,\n            \"format\": format,\n            \"percent\": 100,\n            \"quality\": quality,\n            \"viewer\": viewer,\n            \"startTime\": start_frame,\n            \"endTime\": end_frame,\n            \"offScreen\": off_screen,\n            \"showOrnaments\": show_ornaments,\n            \"forceOverwrite\": overwrite,\n            \"filename\": filename,\n            \"widthHeight\": [width, height],\n            \"rawFrameNumbers\": raw_frame_numbers,\n            \"framePadding\": frame_padding\n        }\n        all_playblast_kwargs.update(playblast_kwargs)\n\n        if getattr(contextlib, \"nested\", None):\n            with contextlib.nested(\n                _disabled_inview_messages(),\n                _maintain_camera(panel, camera),\n                _applied_viewport_options(viewport_options, panel),\n                _applied_camera_options(camera_options, panel),\n                _applied_display_options(display_options),\n                _applied_viewport2_options(viewport2_options),\n                _isolated_nodes(isolate, panel),\n                _maintained_time()\n            ):\n                output = cmds.playblast(**all_playblast_kwargs)\n        else:\n            with contextlib.ExitStack() as stack:\n                stack.enter_context(_disabled_inview_messages())\n                stack.enter_context(_maintain_camera(panel, camera))\n                stack.enter_context(\n                    _applied_viewport_options(viewport_options, panel)\n                )\n                stack.enter_context(\n                    _applied_camera_options(camera_options, panel)\n                )\n                stack.enter_context(\n                    _applied_display_options(display_options)\n                )\n                stack.enter_context(\n                    _applied_viewport2_options(viewport2_options)\n                )\n                stack.enter_context(_isolated_nodes(isolate, panel))\n                stack.enter_context(_maintained_time())\n\n                output = cmds.playblast(**all_playblast_kwargs)\n\n        return output\n\n\ndef snap(*args, **kwargs):\n    \"\"\"Single frame playblast in an independent panel.\n\n    The arguments of `capture` are all valid here as well, except for\n    `start_frame` and `end_frame`.\n\n    Arguments:\n        frame (float, optional): The frame to snap. If not provided current\n            frame is used.\n        clipboard (bool, optional): Whether to add the output image to the\n            global clipboard. This allows to easily paste the snapped image\n            into another application, eg. into Photoshop.\n\n    Keywords:\n        See `capture`.\n\n    \"\"\"\n\n    # capture single frame\n    frame = kwargs.pop('frame', cmds.currentTime(q=1))\n    kwargs['start_frame'] = frame\n    kwargs['end_frame'] = frame\n    kwargs['frame'] = frame\n\n    if not isinstance(frame, (int, float)):\n        raise TypeError(\"frame must be a single frame (integer or float). \"\n                        \"Use `capture()` for sequences.\")\n\n    # override capture defaults\n    format = kwargs.pop('format', \"image\")\n    compression = kwargs.pop('compression', \"png\")\n    viewer = kwargs.pop('viewer', False)\n    raw_frame_numbers = kwargs.pop('raw_frame_numbers', True)\n    kwargs['compression'] = compression\n    kwargs['format'] = format\n    kwargs['viewer'] = viewer\n    kwargs['raw_frame_numbers'] = raw_frame_numbers\n\n    # pop snap only keyword arguments\n    clipboard = kwargs.pop('clipboard', False)\n\n    # perform capture\n    output = capture(*args, **kwargs)\n\n    def replace(m):\n        \"\"\"Substitute # with frame number\"\"\"\n        return str(int(frame)).zfill(len(m.group()))\n\n    output = re.sub(\"#+\", replace, output)\n\n    # add image to clipboard\n    if clipboard:\n        _image_to_clipboard(output)\n\n    return output\n\n\nCameraOptions = {\n    \"displayGateMask\": False,\n    \"displayResolution\": False,\n    \"displayFilmGate\": False,\n    \"displayFieldChart\": False,\n    \"displaySafeAction\": False,\n    \"displaySafeTitle\": False,\n    \"displayFilmPivot\": False,\n    \"displayFilmOrigin\": False,\n    \"overscan\": 1.0,\n    \"depthOfField\": False,\n}\n\nDisplayOptions = {\n    \"displayGradient\": True,\n    \"background\": (0.631, 0.631, 0.631),\n    \"backgroundTop\": (0.535, 0.617, 0.702),\n    \"backgroundBottom\": (0.052, 0.052, 0.052),\n}\n\n# These display options require a different command to be queried and set\n_DisplayOptionsRGB = set([\"background\", \"backgroundTop\", \"backgroundBottom\"])\n\nViewportOptions = {\n    # renderer\n    \"rendererName\": \"vp2Renderer\",\n    \"fogging\": False,\n    \"fogMode\": \"linear\",\n    \"fogDensity\": 1,\n    \"fogStart\": 1,\n    \"fogEnd\": 1,\n    \"fogColor\": (0, 0, 0, 0),\n    \"shadows\": False,\n    \"displayTextures\": True,\n    \"displayLights\": \"default\",\n    \"useDefaultMaterial\": False,\n    \"wireframeOnShaded\": False,\n    \"displayAppearance\": 'smoothShaded',\n    \"selectionHiliteDisplay\": False,\n    \"headsUpDisplay\": True,\n    # object display\n    \"imagePlane\": True,\n    \"nurbsCurves\": False,\n    \"nurbsSurfaces\": False,\n    \"polymeshes\": True,\n    \"subdivSurfaces\": False,\n    \"planes\": True,\n    \"cameras\": False,\n    \"controlVertices\": True,\n    \"lights\": False,\n    \"grid\": False,\n    \"hulls\": True,\n    \"joints\": False,\n    \"ikHandles\": False,\n    \"deformers\": False,\n    \"dynamics\": False,\n    \"fluids\": False,\n    \"hairSystems\": False,\n    \"follicles\": False,\n    \"nCloths\": False,\n    \"nParticles\": False,\n    \"nRigids\": False,\n    \"dynamicConstraints\": False,\n    \"locators\": False,\n    \"manipulators\": False,\n    \"dimensions\": False,\n    \"handles\": False,\n    \"pivots\": False,\n    \"textures\": False,\n    \"strokes\": False\n}\n\nViewport2Options = {\n    \"consolidateWorld\": True,\n    \"enableTextureMaxRes\": False,\n    \"bumpBakeResolution\": 64,\n    \"colorBakeResolution\": 64,\n    \"floatingPointRTEnable\": True,\n    \"floatingPointRTFormat\": 1,\n    \"gammaCorrectionEnable\": False,\n    \"gammaValue\": 2.2,\n    \"lineAAEnable\": False,\n    \"maxHardwareLights\": 8,\n    \"motionBlurEnable\": False,\n    \"motionBlurSampleCount\": 8,\n    \"motionBlurShutterOpenFraction\": 0.2,\n    \"motionBlurType\": 0,\n    \"multiSampleCount\": 8,\n    \"multiSampleEnable\": False,\n    \"singleSidedLighting\": False,\n    \"ssaoEnable\": False,\n    \"ssaoAmount\": 1.0,\n    \"ssaoFilterRadius\": 16,\n    \"ssaoRadius\": 16,\n    \"ssaoSamples\": 16,\n    \"textureMaxResolution\": 4096,\n    \"threadDGEvaluation\": False,\n    \"transparencyAlgorithm\": 1,\n    \"transparencyQuality\": 0.33,\n    \"useMaximumHardwareLights\": True,\n    \"vertexAnimationCache\": 0,\n    \"renderDepthOfField\": 0\n}\n\n\ndef apply_view(panel, **options):\n    \"\"\"Apply options to panel\"\"\"\n\n    camera = cmds.modelPanel(panel, camera=True, query=True)\n\n    # Display options\n    display_options = options.get(\"display_options\", {})\n    _iteritems = getattr(display_options, \"iteritems\", display_options.items)\n    for key, value in _iteritems():\n        if key in _DisplayOptionsRGB:\n            cmds.displayRGBColor(key, *value)\n        else:\n            cmds.displayPref(**{key: value})\n\n    # Camera options\n    camera_options = options.get(\"camera_options\", {})\n    _iteritems = getattr(camera_options, \"iteritems\", camera_options.items)\n    for key, value in _iteritems:\n        _safe_setAttr(\"{0}.{1}\".format(camera, key), value)\n\n    # Viewport options\n    viewport_options = options.get(\"viewport_options\", {})\n    _iteritems = getattr(viewport_options, \"iteritems\", viewport_options.items)\n    for key, value in _iteritems():\n        cmds.modelEditor(panel, edit=True, **{key: value})\n\n    viewport2_options = options.get(\"viewport2_options\", {})\n    _iteritems = getattr(\n        viewport2_options, \"iteritems\", viewport2_options.items\n    )\n    for key, value in _iteritems():\n        attr = \"hardwareRenderingGlobals.{0}\".format(key)\n        _safe_setAttr(attr, value)\n\n\ndef parse_active_panel():\n    \"\"\"Parse the active modelPanel.\n\n    Raises\n        RuntimeError: When no active modelPanel an error is raised.\n\n    Returns:\n        str: Name of modelPanel\n\n    \"\"\"\n\n    panel = cmds.getPanel(withFocus=True)\n\n    # This happens when last focus was on panel\n    # that got deleted (e.g. `capture()` then `parse_active_view()`)\n    if not panel or \"modelPanel\" not in panel:\n        raise RuntimeError(\"No active model panel found\")\n\n    return panel\n\n\ndef parse_active_view():\n    \"\"\"Parse the current settings from the active view\"\"\"\n    panel = parse_active_panel()\n    return parse_view(panel)\n\n\ndef parse_view(panel):\n    \"\"\"Parse the scene, panel and camera for their current settings\n\n    Example:\n        >>> parse_view(\"modelPanel1\")\n\n    Arguments:\n        panel (str): Name of modelPanel\n\n    \"\"\"\n\n    camera = cmds.modelPanel(panel, query=True, camera=True)\n\n    # Display options\n    display_options = {}\n    for key in DisplayOptions:\n        if key in _DisplayOptionsRGB:\n            display_options[key] = cmds.displayRGBColor(key, query=True)\n        else:\n            display_options[key] = cmds.displayPref(query=True, **{key: True})\n\n    # Camera options\n    camera_options = {}\n    for key in CameraOptions:\n        camera_options[key] = cmds.getAttr(\"{0}.{1}\".format(camera, key))\n\n    # Viewport options\n    viewport_options = {}\n\n    # capture plugin display filters first to ensure we never override\n    # built-in arguments if ever possible a plugin has similarly named\n    # plugin display filters (which it shouldn't!)\n    plugins = cmds.pluginDisplayFilter(query=True, listFilters=True)\n    for plugin in plugins:\n        plugin = str(plugin)  # unicode->str for simplicity of the dict\n        state = cmds.modelEditor(panel, query=True, queryPluginObjects=plugin)\n        viewport_options[plugin] = state\n\n    for key in ViewportOptions:\n        viewport_options[key] = cmds.modelEditor(\n            panel, query=True, **{key: True})\n\n    viewport2_options = {}\n    for key in Viewport2Options.keys():\n        attr = \"hardwareRenderingGlobals.{0}\".format(key)\n        try:\n            viewport2_options[key] = cmds.getAttr(attr)\n        except ValueError:\n            continue\n\n    return {\n        \"camera\": camera,\n        \"display_options\": display_options,\n        \"camera_options\": camera_options,\n        \"viewport_options\": viewport_options,\n        \"viewport2_options\": viewport2_options\n    }\n\n\ndef parse_active_scene():\n    \"\"\"Parse active scene for arguments for capture()\n\n    *Resolution taken from render settings.\n\n    \"\"\"\n\n    time_control = mel.eval(\"$gPlayBackSlider = $gPlayBackSlider\")\n\n    return {\n        \"start_frame\": cmds.playbackOptions(minTime=True, query=True),\n        \"end_frame\": cmds.playbackOptions(maxTime=True, query=True),\n        \"width\": cmds.getAttr(\"defaultResolution.width\"),\n        \"height\": cmds.getAttr(\"defaultResolution.height\"),\n        \"compression\": cmds.optionVar(query=\"playblastCompression\"),\n        \"filename\": (cmds.optionVar(query=\"playblastFile\")\n                     if cmds.optionVar(query=\"playblastSaveToFile\") else None),\n        \"format\": cmds.optionVar(query=\"playblastFormat\"),\n        \"off_screen\": (True if cmds.optionVar(query=\"playblastOffscreen\")\n                       else False),\n        \"show_ornaments\": (True if cmds.optionVar(query=\"playblastShowOrnaments\")\n                       else False),\n        \"quality\": cmds.optionVar(query=\"playblastQuality\"),\n        \"sound\": cmds.timeControl(time_control, q=True, sound=True) or None\n    }\n\n\ndef apply_scene(**options):\n    \"\"\"Apply options from scene\n\n    Example:\n        >>> apply_scene({\"start_frame\": 1009})\n\n    Arguments:\n        options (dict): Scene options\n\n    \"\"\"\n\n    if \"start_frame\" in options:\n        cmds.playbackOptions(minTime=options[\"start_frame\"])\n\n    if \"end_frame\" in options:\n        cmds.playbackOptions(maxTime=options[\"end_frame\"])\n\n    if \"width\" in options:\n        _safe_setAttr(\"defaultResolution.width\", options[\"width\"])\n\n    if \"height\" in options:\n        _safe_setAttr(\"defaultResolution.height\", options[\"height\"])\n\n    if \"compression\" in options:\n        cmds.optionVar(\n            stringValue=[\"playblastCompression\", options[\"compression\"]])\n\n    if \"filename\" in options:\n        cmds.optionVar(\n            stringValue=[\"playblastFile\", options[\"filename\"]])\n\n    if \"format\" in options:\n        cmds.optionVar(\n            stringValue=[\"playblastFormat\", options[\"format\"]])\n\n    if \"off_screen\" in options:\n        cmds.optionVar(\n            intValue=[\"playblastFormat\", options[\"off_screen\"]])\n\n    if \"show_ornaments\" in options:\n        cmds.optionVar(\n            intValue=[\"show_ornaments\", options[\"show_ornaments\"]])\n\n    if \"quality\" in options:\n        cmds.optionVar(\n            floatValue=[\"playblastQuality\", options[\"quality\"]])\n\n\n@contextlib.contextmanager\ndef _applied_view(panel, **options):\n    \"\"\"Apply options to panel\"\"\"\n\n    original = parse_view(panel)\n    apply_view(panel, **options)\n\n    try:\n        yield\n    finally:\n        apply_view(panel, **original)\n\n\n@contextlib.contextmanager\ndef _independent_panel(width, height, off_screen=False):\n    \"\"\"Create capture-window context without decorations\n\n    Arguments:\n        width (int): Width of panel\n        height (int): Height of panel\n\n    Example:\n        >>> with _independent_panel(800, 600):\n        ...   cmds.capture()\n\n    \"\"\"\n\n    # center panel on screen\n    screen_width, screen_height = _get_screen_size()\n    topLeft = [int((screen_height-height)/2.0),\n               int((screen_width-width)/2.0)]\n\n    window = cmds.window(width=width,\n                         height=height,\n                         topLeftCorner=topLeft,\n                         menuBarVisible=False,\n                         titleBar=False,\n                         visible=not off_screen)\n    cmds.paneLayout()\n    panel = cmds.modelPanel(menuBarVisible=False,\n                            label='CapturePanel')\n\n    # Hide icons under panel menus\n    bar_layout = cmds.modelPanel(panel, q=True, barLayout=True)\n    cmds.frameLayout(bar_layout, edit=True, collapse=True)\n\n    if not off_screen:\n        cmds.showWindow(window)\n\n    # Set the modelEditor of the modelPanel as the active view so it takes\n    # the playback focus. Does seem redundant with the `refresh` added in.\n    editor = cmds.modelPanel(panel, query=True, modelEditor=True)\n    cmds.modelEditor(editor, edit=True, activeView=True)\n\n    # Force a draw refresh of Maya so it keeps focus on the new panel\n    # This focus is required to force preview playback in the independent panel\n    cmds.refresh(force=True)\n\n    try:\n        yield panel\n    finally:\n        # Delete the panel to fix memory leak (about 5 mb per capture)\n        cmds.deleteUI(panel, panel=True)\n        cmds.deleteUI(window)\n\n\n@contextlib.contextmanager\ndef _applied_camera_options(options, panel):\n    \"\"\"Context manager for applying `options` to `camera`\"\"\"\n\n    camera = cmds.modelPanel(panel, query=True, camera=True)\n    options = dict(CameraOptions, **(options or {}))\n\n    old_options = dict()\n    for opt in options.copy():\n        try:\n            old_options[opt] = cmds.getAttr(camera + \".\" + opt)\n        except:\n            sys.stderr.write(\"Could not get camera attribute \"\n                             \"for capture: %s\" % opt)\n            options.pop(opt)\n\n    _iteritems = getattr(options, \"iteritems\", options.items)\n    for opt, value in _iteritems():\n        if cmds.getAttr(camera + \".\" + opt, lock=True):\n            continue\n        else:\n            _safe_setAttr(camera + \".\" + opt, value)\n\n    try:\n        yield\n    finally:\n        if old_options:\n            _iteritems = getattr(old_options, \"iteritems\", old_options.items)\n            for opt, value in _iteritems():\n                #\n                if cmds.getAttr(camera + \".\" + opt, lock=True):\n                    continue\n                else:\n                    _safe_setAttr(camera + \".\" + opt, value)\n\n\n@contextlib.contextmanager\ndef _applied_display_options(options):\n    \"\"\"Context manager for setting background color display options.\"\"\"\n\n    options = dict(DisplayOptions, **(options or {}))\n\n    colors = ['background', 'backgroundTop', 'backgroundBottom']\n    preferences = ['displayGradient']\n\n    # Store current settings\n    original = {}\n    for color in colors:\n        original[color] = cmds.displayRGBColor(color, query=True) or []\n\n    for preference in preferences:\n        original[preference] = cmds.displayPref(\n            query=True, **{preference: True})\n\n    # Apply settings\n    for color in colors:\n        value = options[color]\n        cmds.displayRGBColor(color, *value)\n\n    for preference in preferences:\n        value = options[preference]\n        cmds.displayPref(**{preference: value})\n\n    try:\n        yield\n\n    finally:\n        # Restore original settings\n        for color in colors:\n            cmds.displayRGBColor(color, *original[color])\n        for preference in preferences:\n            cmds.displayPref(**{preference: original[preference]})\n\n\n@contextlib.contextmanager\ndef _applied_viewport_options(options, panel):\n    \"\"\"Context manager for applying `options` to `panel`\"\"\"\n\n    options = dict(ViewportOptions, **(options or {}))\n    plugin_options = options.pop(\"pluginObjects\", {})\n\n    # BUGFIX Maya 2020 some keys in viewport options dict may not be unicode\n    #        This is a local OpenPype edit to capture.py for issue #4730\n    # TODO: Remove when dropping Maya 2020 compatibility\n    if int(cmds.about(version=True)) <= 2020:\n        options = {\n            str(key): value for key, value in options.items()\n        }\n        plugin_options = {\n            str(key): value for key, value in plugin_options.items()\n        }\n\n    # Backwards compatibility for `pluginObjects` flattened into `options`\n    # separate the plugin display filter options since they need to\n    # be set differently (see #55)\n    plugins = set(cmds.pluginDisplayFilter(query=True, listFilters=True))\n    for plugin in plugins:\n        if plugin in options:\n            plugin_options[plugin] = options.pop(plugin)\n\n    # default options\n    try:\n        cmds.modelEditor(panel, edit=True, **options)\n    except TypeError as e:\n        # Try to set as much as possible of the state by setting them one by\n        # one. This way we can also report the failing key values explicitly.\n        for key, value in options.items():\n            try:\n                cmds.modelEditor(panel, edit=True, **{key: value})\n            except TypeError:\n                logger.error(\"Failing to apply option '{}': {}\".format(key,\n                                                                       value))\n\n    # plugin display filter options\n    for plugin, state in plugin_options.items():\n        cmds.modelEditor(panel, edit=True, pluginObjects=(plugin, state))\n\n    yield\n\n\n@contextlib.contextmanager\ndef _applied_viewport2_options(options):\n    \"\"\"Context manager for setting viewport 2.0 options.\n\n    These options are applied by setting attributes on the\n    \"hardwareRenderingGlobals\" node.\n\n    \"\"\"\n\n    options = dict(Viewport2Options, **(options or {}))\n\n    # Store current settings\n    original = {}\n    for opt in options.copy():\n        try:\n            original[opt] = cmds.getAttr(\"hardwareRenderingGlobals.\" + opt)\n        except ValueError:\n            options.pop(opt)\n\n    # Apply settings\n    _iteritems = getattr(options, \"iteritems\", options.items)\n    for opt, value in _iteritems():\n        _safe_setAttr(\"hardwareRenderingGlobals.\" + opt, value)\n\n    try:\n        yield\n    finally:\n        # Restore previous settings\n        _iteritems = getattr(original, \"iteritems\", original.items)\n        for opt, value in _iteritems():\n            _safe_setAttr(\"hardwareRenderingGlobals.\" + opt, value)\n\n\n@contextlib.contextmanager\ndef _isolated_nodes(nodes, panel):\n    \"\"\"Context manager for isolating `nodes` in `panel`\"\"\"\n\n    if nodes is not None:\n        cmds.isolateSelect(panel, state=True)\n        for obj in nodes:\n            cmds.isolateSelect(panel, addDagObject=obj)\n    yield\n\n\n@contextlib.contextmanager\ndef _maintained_time():\n    \"\"\"Context manager for preserving (resetting) the time after the context\"\"\"\n\n    current_time = cmds.currentTime(query=1)\n    try:\n        yield\n    finally:\n        cmds.currentTime(current_time)\n\n\n@contextlib.contextmanager\ndef _maintain_camera(panel, camera):\n    state = {}\n\n    if not _in_standalone():\n        cmds.lookThru(panel, camera)\n    else:\n        state = dict((camera, cmds.getAttr(camera + \".rnd\"))\n                     for camera in cmds.ls(type=\"camera\"))\n        _safe_setAttr(camera + \".rnd\", True)\n\n    try:\n        yield\n    finally:\n        _iteritems = getattr(state, \"iteritems\", state.items)\n        for camera, renderable in _iteritems():\n            _safe_setAttr(camera + \".rnd\", renderable)\n\n\n@contextlib.contextmanager\ndef _disabled_inview_messages():\n    \"\"\"Disable in-view help messages during the context\"\"\"\n    original = cmds.optionVar(q=\"inViewMessageEnable\")\n    cmds.optionVar(iv=(\"inViewMessageEnable\", 0))\n    try:\n        yield\n    finally:\n        cmds.optionVar(iv=(\"inViewMessageEnable\", original))\n\n\ndef _image_to_clipboard(path):\n    \"\"\"Copies the image at path to the system's global clipboard.\"\"\"\n    if _in_standalone():\n        raise Exception(\"Cannot copy to clipboard from Maya Standalone\")\n\n    image = QtGui.QImage(path)\n    clipboard = QtWidgets.QApplication.clipboard()\n    clipboard.setImage(image, mode=QtGui.QClipboard.Clipboard)\n\n\ndef _get_screen_size():\n    \"\"\"Return available screen size without space occupied by taskbar\"\"\"\n    if _in_standalone():\n        return [0, 0]\n\n    rect = QtWidgets.QDesktopWidget().screenGeometry(-1)\n    return [rect.width(), rect.height()]\n\n\ndef _in_standalone():\n    return not hasattr(cmds, \"about\") or cmds.about(batch=True)\n\n\ndef _safe_setAttr(*args, **kwargs):\n    \"\"\"Wrapper to handle failures when attribute is locked.\n\n    Temporary hotfix until better approach (store value, unlock, set new,\n    return old, lock again) is implemented.\n    \"\"\"\n    try:\n        cmds.setAttr(*args, **kwargs)\n    except RuntimeError:\n        print(\"Cannot setAttr {}!\".format(args))\n\n\n# --------------------------------\n#\n# Apply version specific settings\n#\n# --------------------------------\n\nversion = mel.eval(\"getApplicationVersionAsFloat\")\nif version > 2015:\n    Viewport2Options.update({\n        \"hwFogAlpha\": 1.0,\n        \"hwFogFalloff\": 0,\n        \"hwFogDensity\": 0.1,\n        \"hwFogEnable\": False,\n        \"holdOutDetailMode\": 1,\n        \"hwFogEnd\": 100.0,\n        \"holdOutMode\": True,\n        \"hwFogColorR\": 0.5,\n        \"hwFogColorG\": 0.5,\n        \"hwFogColorB\": 0.5,\n        \"hwFogStart\": 0.0,\n    })\n    ViewportOptions.update({\n        \"motionTrails\": False\n    })\n"
  },
  {
    "path": "openpype/vendor/python/common/pysync.py",
    "content": "#!/usr/local/bin/python3\n# https://github.com/snullp/pySync/blob/master/pySync.py\n\nimport sys\nimport shutil\nimport os\nimport time\nimport configparser\nfrom os.path import (\n    getsize,\n    getmtime,\n    isfile,\n    isdir,\n    join,\n    abspath,\n    expanduser,\n    realpath\n)\nimport logging\n\nlog = logging.getLogger(__name__)\n\nignoreFiles = (\"Thumbs.db\", \".DS_Store\")\n\n# this feature is not yet implemented\nignorePaths = []\n\nif os.name == 'nt':\n    # msvcrt can't function correctly in IDLE\n    if 'idlelib.run' in sys.modules:\n        print(\"Please don't run this script in IDLE.\")\n        sys.exit(0)\n    import msvcrt\n\n    def flush_input(str, set=None):\n        if not set:\n            while msvcrt.kbhit():\n                ch = msvcrt.getch()\n                if ch == '\\xff':\n                    print(\"msvcrt is broken, this is weird.\")\n                    sys.exit(0)\n            return input(str)\n        else:\n            return set\nelse:\n    import select\n\n    def flush_input(str, set=None):\n        if not set:\n            while len(select.select([sys.stdin.fileno()], [], [], 0.0)[0]) > 0:\n                os.read(sys.stdin.fileno(), 4096)\n            return input(str)\n        else:\n            return set\n\n\ndef compare(fa, fb, options_input=[]):\n    if isfile(fa) == isfile(fb):\n        if isdir(fa):\n            walktree(fa, fb, options_input)\n        elif isfile(fa):\n            if getsize(fa) != getsize(fb) \\\n                    or int(getmtime(fa)) != int(getmtime(fb)):\n                log.info(str((fa, ': size=', getsize(fa), 'mtime=',\n                              time.asctime(time.localtime(getmtime(fa))))))\n                log.info(str((fb, ': size=', getsize(fb), 'mtime=',\n                              time.asctime(time.localtime(getmtime(fb))))))\n                if getmtime(fa) > getmtime(fb):\n                    act = '>'\n                else:\n                    act = '<'\n\n                set = [i for i in options_input if i in [\">\", \"<\"]][0]\n\n                s = flush_input('What to do?(>,<,r,n)[' + act + ']', set=set)\n                if len(s) > 0:\n                    act = s[0]\n                if act == '>':\n                    shutil.copy2(fa, fb)\n                elif act == '<':\n                    shutil.copy2(fb, fa)\n                elif act == 'r':\n                    if isdir(fa):\n                        shutil.rmtree(fa)\n                    elif isfile(fa):\n                        os.remove(fa)\n                    else:\n                        log.info(str(('Remove: Skipping', fa)))\n                    if isdir(fb):\n                        shutil.rmtree(fb)\n                    elif isfile(fb):\n                        os.remove(fb)\n                    else:\n                        log.info(str(('Remove: Skipping', fb)))\n\n        else:\n            log.debug(str(('Compare: Skipping non-dir and non-file', fa)))\n    else:\n        log.error(str(('Error:', fa, ',', fb, 'have different file type')))\n\n\ndef copy(fa, fb, options_input=[]):\n    set = [i for i in options_input if i in [\"y\"]][0]\n    s = flush_input('Copy ' + fa + ' to another side?(r,y,n)[y]', set=set)\n    if len(s) > 0:\n        act = s[0]\n    else:\n        act = 'y'\n    if act == 'y':\n        if isdir(fa):\n            shutil.copytree(fa, fb)\n        elif isfile(fa):\n            shutil.copy2(fa, fb)\n        else:\n            log.debug(str(('Copy: Skipping ', fa)))\n    elif act == 'r':\n        if isdir(fa):\n            shutil.rmtree(fa)\n        elif isfile(fa):\n            os.remove(fa)\n        else:\n            log.debug(str(('Remove: Skipping ', fa)))\n\n\nstoentry = []\ntarentry = []\n\n\ndef walktree(source, target, options_input=[]):\n    srclist = os.listdir(source)\n    tarlist = os.listdir(target)\n    if '!sync' in srclist:\n        return\n    if '!sync' in tarlist:\n        return\n    # files in source dir...\n    for f in srclist:\n        if f in ignoreFiles:\n            continue\n        spath = join(source, f)\n        tpath = join(target, f)\n        if spath in ignorePaths:\n            continue\n        if spath in stoentry:\n            # just in case target also have this one\n            if f in tarlist:\n                del tarlist[tarlist.index(f)]\n            continue\n\n        # if also exists in target dir\n        if f in tarlist:\n            del tarlist[tarlist.index(f)]\n            compare(spath, tpath, options_input)\n\n        # exists in source dir only\n        else:\n            copy(spath, tpath, options_input)\n\n    # exists in target dir only\n    set = [i for i in options_input if i in [\"<\"]]\n\n    for f in tarlist:\n        if f in ignoreFiles:\n            continue\n        spath = join(source, f)\n        tpath = join(target, f)\n        if tpath in ignorePaths:\n            continue\n        if tpath in tarentry:\n            continue\n        if set:\n            copy(tpath, spath, options_input)\n        else:\n            print(\"REMOVING: {}\".format(f))\n            if os.path.isdir(tpath):\n                shutil.rmtree(tpath)\n            else:\n                os.remove(tpath)\n            print(\"REMOVING: {}\".format(f))\n\n\nif __name__ == '__main__':\n    stoconf = configparser.RawConfigParser()\n    tarconf = configparser.RawConfigParser()\n    stoconf.read(\"pySync.ini\")\n    tarconf.read(expanduser(\"~/.pysync\"))\n    stoname = stoconf.sections()[0]\n    tarname = tarconf.sections()[0]\n\n    # calculate storage's base folder\n    if stoconf.has_option(stoname, 'BASE'):\n        stobase = abspath(stoconf.get(stoname, 'BASE'))\n        stoconf.remove_option(stoname, 'BASE')\n    else:\n        stobase = os.getcwd()\n\n    # same, for target's base folder\n    if tarconf.has_option(tarname, 'BASE'):\n        tarbase = abspath(tarconf.get(tarname, 'BASE'))\n        tarconf.remove_option(tarname, 'BASE')\n    else:\n        tarbase = expanduser('~/')\n\n    print(\"Syncing between\", stoname, \"and\", tarname)\n    sto_content = {x: realpath(join(stobase, stoconf.get(stoname, x)))\n                   for x in stoconf.options(stoname)}\n    tar_content = {x: realpath(join(tarbase, tarconf.get(tarname, x)))\n                   for x in tarconf.options(tarname)}\n    stoentry = [sto_content[x] for x in sto_content]\n    tarentry = [tar_content[x] for x in tar_content]\n\n    for folder in sto_content:\n        if folder in tar_content:\n            print('Processing', folder)\n            walktree(sto_content[folder], tar_content[folder], options_input)\n    print(\"Done.\")\n"
  },
  {
    "path": "openpype/vendor/python/common/qargparse.py",
    "content": "\"\"\"\nNOTE: The required `Qt` module has changed to use the one that vendorized.\n      Remember to change to relative import when updating this.\n\"\"\"\n\nimport re\nimport logging\n\nfrom collections import OrderedDict as odict\nfrom qtpy import QtCore, QtWidgets, QtGui\nimport qtawesome\n\n__version__ = \"0.5.2\"\n_log = logging.getLogger(__name__)\n_type = type  # used as argument\n\ntry:\n    # Python 2\n    _basestring = basestring\nexcept NameError:\n    _basestring = str\n\n\nclass QArgumentParser(QtWidgets.QWidget):\n    \"\"\"User interface arguments\n\n    Arguments:\n        arguments (list, optional): Instances of QArgument\n        description (str, optional): Long-form text of what this parser is for\n        storage (QSettings, optional): Persistence to disk, providing\n            value() and setValue() methods\n\n    \"\"\"\n\n    changed = QtCore.Signal(QtCore.QObject)  # A QArgument\n\n    def __init__(self,\n                 arguments=None,\n                 description=None,\n                 storage=None,\n                 parent=None):\n        super(QArgumentParser, self).__init__(parent)\n        self.setAttribute(QtCore.Qt.WA_StyledBackground)\n\n        # Create internal settings\n        if storage is True:\n            storage = QtCore.QSettings(\n                QtCore.QSettings.IniFormat,\n                QtCore.QSettings.UserScope,\n                __name__, \"QArgparse\",\n            )\n\n        if storage is not None:\n            _log.info(\"Storing settings @ %s\" % storage.fileName())\n\n        arguments = arguments or []\n\n        assert hasattr(arguments, \"__iter__\"), \"arguments must be iterable\"\n        assert isinstance(storage, (type(None), QtCore.QSettings)), (\n            \"storage must be of type QSettings\"\n        )\n\n        layout = QtWidgets.QGridLayout(self)\n        layout.setRowStretch(999, 1)\n\n        if description:\n            layout.addWidget(QtWidgets.QLabel(description), 0, 0, 1, 2)\n\n        self._row = 1\n        self._storage = storage\n        self._arguments = odict()\n        self._desciption = description\n\n        for arg in arguments or []:\n            self._addArgument(arg)\n\n        self.setStyleSheet(style)\n\n    def setDescription(self, text):\n        self._desciption.setText(text)\n\n    def addArgument(self, name, type=None, default=None, **kwargs):\n        # Infer type from default\n        if type is None and default is not None:\n            type = _type(default)\n\n        # Default to string\n        type = type or str\n\n        Argument = {\n            None: String,\n            int: Integer,\n            float: Float,\n            bool: Boolean,\n            str: String,\n            list: Enum,\n            tuple: Enum,\n        }.get(type, type)\n\n        arg = Argument(name, default=default, **kwargs)\n        self._addArgument(arg)\n        return arg\n\n    def _addArgument(self, arg):\n        if arg[\"name\"] in self._arguments:\n            raise ValueError(\"Duplicate argument '%s'\" % arg[\"name\"])\n\n        if self._storage is not None:\n            default = self._storage.value(arg[\"name\"])\n\n            if default:\n                if isinstance(arg, Boolean):\n                    default = bool({\n                        None: QtCore.Qt.Unchecked,\n\n                        0: QtCore.Qt.Unchecked,\n                        1: QtCore.Qt.Checked,\n                        2: QtCore.Qt.Checked,\n\n                        \"0\": QtCore.Qt.Unchecked,\n                        \"1\": QtCore.Qt.Checked,\n                        \"2\": QtCore.Qt.Checked,\n\n                        # May be stored as string, if used with IniFormat\n                        \"false\": QtCore.Qt.Unchecked,\n                        \"true\": QtCore.Qt.Checked,\n                    }.get(default))\n\n                arg[\"default\"] = default\n\n        arg.changed.connect(lambda: self.on_changed(arg))\n\n        label = (\n            QtWidgets.QLabel(arg[\"label\"])\n            if arg.label\n            else QtWidgets.QLabel()\n        )\n        widget = arg.create()\n        icon = qtawesome.icon(\"fa.refresh\", color=\"white\")\n        reset = QtWidgets.QPushButton(icon, \"\")  # default\n        reset.setToolTip(\"Reset\")\n        reset.setProperty(\"type\", \"reset\")\n        reset.clicked.connect(lambda: self.on_reset(arg))\n\n        # Shown on edit\n        reset.hide()\n\n        for widget in (label, widget):\n            widget.setToolTip(arg[\"help\"])\n            widget.setObjectName(arg[\"name\"])  # useful in CSS\n            widget.setProperty(\"type\", type(arg).__name__)\n            widget.setAttribute(QtCore.Qt.WA_StyledBackground)\n            widget.setEnabled(arg[\"enabled\"])\n\n        # Align label on top of row if widget is over two times heiger\n        height = (lambda w: w.sizeHint().height())\n        label_on_top = height(label) * 2 < height(widget)\n        alignment = (QtCore.Qt.AlignTop,) if label_on_top else ()\n\n        layout = self.layout()\n        layout.addWidget(label, self._row, 0, *alignment)\n        layout.addWidget(widget, self._row, 1)\n        layout.addWidget(reset, self._row, 2, *alignment)\n        layout.setColumnStretch(1, 1)\n\n        def on_changed(*_):\n            reset.setVisible(arg[\"edited\"])\n\n        arg.changed.connect(on_changed)\n\n        self._row += 1\n        self._arguments[arg[\"name\"]] = arg\n\n    def clear(self):\n        assert self._storage, \"Cannot clear without persistent storage\"\n        self._storage.clear()\n        _log.info(\"Clearing settings @ %s\" % self._storage.fileName())\n\n    def find(self, name):\n        return self._arguments[name]\n\n    def on_reset(self, arg):\n        arg.write(arg[\"default\"])\n\n    def on_changed(self, arg):\n        arg[\"edited\"] = arg.read() != arg[\"default\"]\n        self.changed.emit(arg)\n\n    # Optional PEP08 syntax\n    add_argument = addArgument\n\n\nclass QArgument(QtCore.QObject):\n    \"\"\"Base class of argument user interface\n    \"\"\"\n    changed = QtCore.Signal()\n\n    # Provide a left-hand side label for this argument\n    label = True\n    # For defining default value for each argument type\n    default = None\n\n    def __init__(self, name, default=None, **kwargs):\n        super(QArgument, self).__init__(kwargs.pop(\"parent\", None))\n\n        kwargs[\"name\"] = name\n        kwargs[\"label\"] = kwargs.get(\"label\", camel_to_title(name))\n        kwargs[\"default\"] = self.default if default is None else default\n        kwargs[\"help\"] = kwargs.get(\"help\", \"\")\n        kwargs[\"read\"] = kwargs.get(\"read\")\n        kwargs[\"write\"] = kwargs.get(\"write\")\n        kwargs[\"enabled\"] = bool(kwargs.get(\"enabled\", True))\n        kwargs[\"edited\"] = False\n\n        self._data = kwargs\n\n    def __str__(self):\n        return self[\"name\"]\n\n    def __repr__(self):\n        return \"%s(\\\"%s\\\")\" % (type(self).__name__, self[\"name\"])\n\n    def __getitem__(self, key):\n        return self._data[key]\n\n    def __setitem__(self, key, value):\n        self._data[key] = value\n\n    def __eq__(self, other):\n        if isinstance(other, _basestring):\n            return self[\"name\"] == other\n        return super(QArgument, self).__eq__(other)\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    def create(self):\n        return QtWidgets.QWidget()\n\n    def read(self):\n        return self._read()\n\n    def write(self, value):\n        self._write(value)\n        self.changed.emit()\n\n\nclass Boolean(QArgument):\n    \"\"\"Boolean type user interface\n\n    Presented by `QtWidgets.QCheckBox`.\n\n    Arguments:\n        name (str): The name of argument\n        label (str, optional): Display name, convert from `name` if not given\n        help (str, optional): Tool tip message of this argument\n        default (bool, optional): Argument's default value, default None\n        enabled (bool, optional): Whether to enable this widget, default True\n\n    \"\"\"\n    def create(self):\n        widget = QtWidgets.QCheckBox()\n        widget.clicked.connect(self.changed.emit)\n\n        if isinstance(self, Tristate):\n            self._read = lambda: widget.checkState()\n            state = {\n                0: QtCore.Qt.Unchecked,\n                1: QtCore.Qt.PartiallyChecked,\n                2: QtCore.Qt.Checked,\n                \"1\": QtCore.Qt.PartiallyChecked,\n                \"0\": QtCore.Qt.Unchecked,\n                \"2\": QtCore.Qt.Checked,\n            }\n        else:\n            self._read = lambda: bool(widget.checkState())\n            state = {\n                None: QtCore.Qt.Unchecked,\n\n                0: QtCore.Qt.Unchecked,\n                1: QtCore.Qt.Checked,\n                2: QtCore.Qt.Checked,\n\n                \"0\": QtCore.Qt.Unchecked,\n                \"1\": QtCore.Qt.Checked,\n                \"2\": QtCore.Qt.Checked,\n\n                # May be stored as string, if used with QSettings(..IniFormat)\n                \"false\": QtCore.Qt.Unchecked,\n                \"true\": QtCore.Qt.Checked,\n            }\n\n        self._write = lambda value: widget.setCheckState(state[value])\n        widget.clicked.connect(self.changed.emit)\n\n        if self[\"default\"] is not None:\n            self._write(self[\"default\"])\n\n        return widget\n\n    def read(self):\n        return self._read()\n\n\nclass Tristate(QArgument):\n    \"\"\"Not implemented\"\"\"\n\n\nclass Number(QArgument):\n    \"\"\"Base class of numeric type user interface\"\"\"\n    default = 0\n\n    def create(self):\n        if isinstance(self, Float):\n            widget = QtWidgets.QDoubleSpinBox()\n            widget.setMinimum(self._data.get(\"min\", 0.0))\n            widget.setMaximum(self._data.get(\"max\", 99.99))\n        else:\n            widget = QtWidgets.QSpinBox()\n            widget.setMinimum(self._data.get(\"min\", 0))\n            widget.setMaximum(self._data.get(\"max\", 99))\n\n        widget.editingFinished.connect(self.changed.emit)\n        self._read = lambda: widget.value()\n        self._write = lambda value: widget.setValue(value)\n\n        if self[\"default\"] != self.default:\n            self._write(self[\"default\"])\n\n        return widget\n\n\nclass Integer(Number):\n    \"\"\"Integer type user interface\n\n    A subclass of `qargparse.Number`, presented by `QtWidgets.QSpinBox`.\n\n    Arguments:\n        name (str): The name of argument\n        label (str, optional): Display name, convert from `name` if not given\n        help (str, optional): Tool tip message of this argument\n        default (int, optional): Argument's default value, default 0\n        min (int, optional): Argument's minimum value, default 0\n        max (int, optional): Argument's maximum value, default 99\n        enabled (bool, optional): Whether to enable this widget, default True\n\n    \"\"\"\n\n\nclass Float(Number):\n    \"\"\"Float type user interface\n\n    A subclass of `qargparse.Number`, presented by `QtWidgets.QDoubleSpinBox`.\n\n    Arguments:\n        name (str): The name of argument\n        label (str, optional): Display name, convert from `name` if not given\n        help (str, optional): Tool tip message of this argument\n        default (float, optional): Argument's default value, default 0.0\n        min (float, optional): Argument's minimum value, default 0.0\n        max (float, optional): Argument's maximum value, default 99.99\n        enabled (bool, optional): Whether to enable this widget, default True\n\n    \"\"\"\n\n\nclass Range(Number):\n    \"\"\"Range type user interface\n\n    A subclass of `qargparse.Number`, not production ready.\n\n    \"\"\"\n\n\nclass Double3(QArgument):\n    \"\"\"Double3 type user interface\n\n    Presented by three `QtWidgets.QLineEdit` widget with `QDoubleValidator`\n    installed.\n\n    Arguments:\n        name (str): The name of argument\n        label (str, optional): Display name, convert from `name` if not given\n        help (str, optional): Tool tip message of this argument\n        default (tuple or list, optional): Default (0, 0, 0).\n        enabled (bool, optional): Whether to enable this widget, default True\n\n    \"\"\"\n    default = (0, 0, 0)\n\n    def create(self):\n        widget = QtWidgets.QWidget()\n        layout = QtWidgets.QHBoxLayout(widget)\n        layout.setContentsMargins(0, 0, 0, 0)\n        x, y, z = (self.child_arg(layout, i) for i in range(3))\n\n        self._read = lambda: (\n            float(x.text()), float(y.text()), float(z.text()))\n        self._write = lambda value: [\n            w.setText(str(float(v))) for w, v in zip([x, y, z], value)]\n\n        if self[\"default\"] != self.default:\n            self._write(self[\"default\"])\n\n        return widget\n\n    def child_arg(self, layout, index):\n        widget = QtWidgets.QLineEdit()\n        widget.setValidator(QtGui.QDoubleValidator())\n\n        default = str(float(self[\"default\"][index]))\n        widget.setText(default)\n\n        def focusOutEvent(event):\n            if not widget.text():\n                widget.setText(default)  # Ensure value exists for `_read`\n            QtWidgets.QLineEdit.focusOutEvent(widget, event)\n        widget.focusOutEvent = focusOutEvent\n\n        widget.editingFinished.connect(self.changed.emit)\n        widget.returnPressed.connect(widget.editingFinished.emit)\n\n        layout.addWidget(widget)\n\n        return widget\n\n\nclass String(QArgument):\n    \"\"\"String type user interface\n\n    Presented by `QtWidgets.QLineEdit`.\n\n    Arguments:\n        name (str): The name of argument\n        label (str, optional): Display name, convert from `name` if not given\n        help (str, optional): Tool tip message of this argument\n        default (str, optional): Argument's default value, default None\n        placeholder (str, optional): Placeholder message for the widget\n        enabled (bool, optional): Whether to enable this widget, default True\n\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        super(String, self).__init__(*args, **kwargs)\n        self._previous = None\n\n    def create(self):\n        widget = QtWidgets.QLineEdit()\n        widget.editingFinished.connect(self.onEditingFinished)\n        widget.returnPressed.connect(widget.editingFinished.emit)\n        self._read = lambda: widget.text()\n        self._write = lambda value: widget.setText(value)\n\n        if isinstance(self, Info):\n            widget.setReadOnly(True)\n        widget.setPlaceholderText(self._data.get(\"placeholder\", \"\"))\n\n        if self[\"default\"] is not None:\n            self._write(self[\"default\"])\n            self._previous = self[\"default\"]\n\n        return widget\n\n    def onEditingFinished(self):\n        current = self._read()\n\n        if current != self._previous:\n            self.changed.emit()\n            self._previous = current\n\n\nclass Info(String):\n    \"\"\"String type user interface but read-only\n\n    A subclass of `qargparse.String`, presented by `QtWidgets.QLineEdit`.\n\n    Arguments:\n        name (str): The name of argument\n        label (str, optional): Display name, convert from `name` if not given\n        help (str, optional): Tool tip message of this argument\n        default (str, optional): Argument's default value, default None\n        enabled (bool, optional): Whether to enable this widget, default True\n\n    \"\"\"\n\n\nclass Color(String):\n    \"\"\"Color type user interface\n\n    A subclass of `qargparse.String`, not production ready.\n\n    \"\"\"\n\n\nclass Button(QArgument):\n    \"\"\"Button type user interface\n\n    Presented by `QtWidgets.QPushButton`.\n\n    Arguments:\n        name (str): The name of argument\n        label (str, optional): Display name, convert from `name` if not given\n        help (str, optional): Tool tip message of this argument\n        default (bool, optional): Argument's default value, default None\n        enabled (bool, optional): Whether to enable this widget, default True\n\n    \"\"\"\n    label = False\n\n    def create(self):\n        widget = QtWidgets.QPushButton(self[\"label\"])\n        widget.clicked.connect(self.changed.emit)\n\n        state = [\n            QtCore.Qt.Unchecked,\n            QtCore.Qt.Checked,\n        ]\n\n        if isinstance(self, Toggle):\n            widget.setCheckable(True)\n            if hasattr(widget, \"isChecked\"):\n                self._read = lambda: state[int(widget.isChecked())]\n                self._write = (\n                    lambda value: widget.setChecked(value)\n                )\n            else:\n                self._read = lambda: widget.checkState()\n                self._write = (\n                    lambda value: widget.setCheckState(state[int(value)])\n                )\n        else:\n            self._read = lambda: \"clicked\"\n            self._write = lambda value: None\n\n        if self[\"default\"] is not None:\n            self._write(self[\"default\"])\n\n        return widget\n\n\nclass Toggle(Button):\n    \"\"\"Checkable `Button` type user interface\n\n    Presented by `QtWidgets.QPushButton`.\n\n    Arguments:\n        name (str): The name of argument\n        label (str, optional): Display name, convert from `name` if not given\n        help (str, optional): Tool tip message of this argument\n        default (bool, optional): Argument's default value, default None\n        enabled (bool, optional): Whether to enable this widget, default True\n\n    \"\"\"\n\n\nclass InfoList(QArgument):\n    \"\"\"String list type user interface\n\n    Presented by `QtWidgets.QListView`, not production ready.\n\n    \"\"\"\n    def __init__(self, name, **kwargs):\n        kwargs[\"default\"] = kwargs.pop(\"default\", [\"Empty\"])\n        super(InfoList, self).__init__(name, **kwargs)\n\n    def create(self):\n        class Model(QtCore.QStringListModel):\n            def data(self, index, role):\n                return super(Model, self).data(index, role)\n\n        model = QtCore.QStringListModel(self[\"default\"])\n        widget = QtWidgets.QListView()\n        widget.setModel(model)\n        widget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\n\n        self._read = lambda: model.stringList()\n        self._write = lambda value: model.setStringList(value)\n\n        return widget\n\n\nclass Choice(QArgument):\n    \"\"\"Argument user interface for selecting one from list\n\n    Presented by `QtWidgets.QListView`.\n\n    Arguments:\n        name (str): The name of argument\n        label (str, optional): Display name, convert from `name` if not given\n        help (str, optional): Tool tip message of this argument\n        items (list, optional): List of strings for select, default `[\"Empty\"]`\n        default (str, optional): Default item in `items`, use first of `items`\n            if not given.\n        enabled (bool, optional): Whether to enable this widget, default True\n\n    \"\"\"\n    def __init__(self, name, **kwargs):\n        kwargs[\"items\"] = kwargs.get(\"items\", [\"Empty\"])\n        kwargs[\"default\"] = kwargs.pop(\"default\", kwargs[\"items\"][0])\n        super(Choice, self).__init__(name, **kwargs)\n\n    def index(self, value):\n        \"\"\"Return numerical equivalent to self.read()\"\"\"\n        return self[\"items\"].index(value)\n\n    def create(self):\n        def on_changed(selected, deselected):\n            try:\n                selected = selected.indexes()[0]\n            except IndexError:\n                # At least one item must be selected at all times\n                selected = deselected.indexes()[0]\n\n            value = selected.data(QtCore.Qt.DisplayRole)\n            set_current(value)\n            self.changed.emit()\n\n        def set_current(current):\n            options = model.stringList()\n\n            if current == \"Empty\":\n                index = 0\n            else:\n                for index, member in enumerate(options):\n                    if member == current:\n                        break\n                else:\n                    raise ValueError(\n                        \"%s not a member of %s\" % (current, options)\n                    )\n\n            qindex = model.index(index, 0, QtCore.QModelIndex())\n            smodel.setCurrentIndex(qindex, smodel.ClearAndSelect)\n            self[\"current\"] = options[index]\n\n        def reset(items, default=None):\n            items = items or [\"Empty\"]\n            model.setStringList(items)\n            set_current(default or items[0])\n\n        model = QtCore.QStringListModel()\n        widget = QtWidgets.QListView()\n        widget.setModel(model)\n        widget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\n        widget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)\n        smodel = widget.selectionModel()\n        smodel.selectionChanged.connect(on_changed)\n\n        self._read = lambda: self[\"current\"]\n        self._write = lambda value: set_current(value)\n        self.reset = reset\n\n        reset(self[\"items\"], self[\"default\"])\n\n        return widget\n\n\nclass Separator(QArgument):\n    \"\"\"Visual separator\n\n    Example:\n\n        item1\n        item2\n        ------------\n        item3\n        item4\n\n    \"\"\"\n\n    def create(self):\n        widget = QtWidgets.QWidget()\n\n        self._read = lambda: None\n        self._write = lambda value: None\n\n        return widget\n\n\nclass Enum(QArgument):\n    \"\"\"Argument user interface for selecting one from dropdown list\n\n    Presented by `QtWidgets.QComboBox`.\n\n    Arguments:\n        name (str): The name of argument\n        label (str, optional): Display name, convert from `name` if not given\n        help (str, optional): Tool tip message of this argument\n        items (list, optional): List of strings for select, default `[]`\n        default (int, optional): Index of default item, use first of `items`\n            if not given.\n        enabled (bool, optional): Whether to enable this widget, default True\n\n    \"\"\"\n    def __init__(self, name, **kwargs):\n        kwargs[\"default\"] = kwargs.pop(\"default\", 0)\n        kwargs[\"items\"] = kwargs.get(\"items\", [])\n\n        assert isinstance(kwargs[\"items\"], (tuple, list)), (\n            \"items must be list\"\n        )\n\n        super(Enum, self).__init__(name, **kwargs)\n\n    def create(self):\n        widget = QtWidgets.QComboBox()\n        widget.addItems(self[\"items\"])\n        widget.currentIndexChanged.connect(\n            lambda index: self.changed.emit())\n\n        self._read = lambda: widget.currentText()\n        self._write = lambda value: widget.setCurrentIndex(value)\n\n        if self[\"default\"] is not None:\n            self._write(self[\"default\"])\n\n        return widget\n\n\nstyle = \"\"\"\\\nQWidget {\n    /* Explicitly specify a size, to account for automatic HDPi */\n    font-size: 11px;\n}\n\n*[type=\"Button\"] {\n    text-align:left;\n}\n\n*[type=\"Info\"] {\n    background: transparent;\n    border: none;\n}\n\nQLabel[type=\"Separator\"] {\n    min-height: 20px;\n    text-decoration: underline;\n}\n\nQPushButton[type=\"reset\"] {\n    max-width: 11px;\n    max-height: 11px;\n}\n\n\"\"\"\n\n\ndef camelToTitle(text):\n    \"\"\"Convert camelCase `text` to Title Case\n\n    Example:\n        >>> camelToTitle(\"mixedCase\")\n        \"Mixed Case\"\n        >>> camelToTitle(\"myName\")\n        \"My Name\"\n        >>> camelToTitle(\"you\")\n        \"You\"\n        >>> camelToTitle(\"You\")\n        \"You\"\n        >>> camelToTitle(\"This is That\")\n        \"This Is That\"\n\n    \"\"\"\n\n    return re.sub(\n        r\"((?<=[a-z])[A-Z]|(?<!\\A)[A-Z](?=[a-z]))\",\n        r\" \\1\", text\n    ).title()\n\n\ncamel_to_title = camelToTitle\n\n\ndef _demo():\n    import sys\n    app = QtWidgets.QApplication(sys.argv)\n\n    parser = QArgumentParser()\n    parser.setWindowTitle(\"Demo\")\n    parser.setMinimumWidth(300)\n\n    parser.add_argument(\"name\", default=\"Marcus\", help=\"Your name\")\n    parser.add_argument(\"age\", default=33, help=\"Your age\")\n    parser.add_argument(\"height\", default=1.87, help=\"Your height\")\n    parser.add_argument(\"alive\", default=True, help=\"Your state\")\n    parser.add_argument(\"class\", type=Enum, items=[\n        \"Ranger\",\n        \"Warrior\",\n        \"Sorcerer\",\n        \"Monk\",\n    ], default=2, help=\"Your class\")\n\n    parser.add_argument(\"options\", type=Separator)\n    parser.add_argument(\"paths\", type=InfoList, items=[\n        \"Value A\",\n        \"Value B\",\n        \"Some other value\",\n        \"And finally, value C\",\n    ])\n    parser.add_argument(\"location\", type=Double3)\n\n    parser.show()\n    app.exec_()\n\n\nif __name__ == '__main__':\n    import argparse\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--version\", action=\"store_true\")\n    parser.add_argument(\"--demo\", action=\"store_true\")\n\n    opts = parser.parse_args()\n\n    if opts.demo:\n        _demo()\n\n    if opts.version:\n        print(__version__)\n"
  },
  {
    "path": "openpype/vendor/python/common/scriptsmenu/__init__.py",
    "content": "from .scriptsmenu import ScriptsMenu\nfrom . import version\n\n__all__ = [\"ScriptsMenu\"]\n__version__ = version.version\n"
  },
  {
    "path": "openpype/vendor/python/common/scriptsmenu/action.py",
    "content": "import os\n\nfrom qtpy import QtWidgets\n\n\nclass Action(QtWidgets.QAction):\n    \"\"\"Custom Action widget\"\"\"\n\n    def __init__(self, parent=None):\n\n        QtWidgets.QAction.__init__(self, parent)\n\n        self._root = None\n        self._tags = list()\n        self._command = None\n        self._sourcetype = None\n        self._iconfile = None\n        self._label = None\n\n        self._COMMAND = \"\"\"import imp\nf, filepath, descr = imp.find_module('{module_name}', ['{dirname}'])\nmodule = imp.load_module('{module_name}', f, filepath, descr)\nmodule.{module_name}()\"\"\"\n\n    @property\n    def root(self):\n        return self._root\n\n    @root.setter\n    def root(self, value):\n        self._root = value\n\n    @property\n    def tags(self):\n        return self._tags\n\n    @tags.setter\n    def tags(self, value):\n        self._tags = value\n\n    @property\n    def command(self):\n        return self._command\n\n    @command.setter\n    def command(self, value):\n        \"\"\"\n        Store the command in the QAction \n\n        Args:\n            value (str): the full command which will be executed when clicked\n        \n        Return:\n             None\n        \"\"\"\n        self._command = value\n\n    @property\n    def sourcetype(self):\n        return self._sourcetype\n\n    @sourcetype.setter\n    def sourcetype(self, value):\n        \"\"\"\n        Set the command type to get the correct execution of the command given\n\n        Args:\n            value (str): the name of the command type\n        \n        Returns:\n            None\n\n        \"\"\"\n        self._sourcetype = value\n\n    @property\n    def iconfile(self):\n        return self._iconfile\n\n    @iconfile.setter\n    def iconfile(self, value):\n        \"\"\"Store the path to the image file which needs to be displayed\n\n        Args:\n            value (str): the path to the image\n\n        Returns:\n            None\n        \"\"\"\n        self._iconfile = value\n\n    @property\n    def label(self):\n        return self._label\n\n    @label.setter\n    def label(self, value):\n        \"\"\"\n        Set the abbreviation which will be used as overlay text in the shelf\n\n        Args:\n            value (str): an abbreviation of the name\n        \n        Returns:\n            None\n\n        \"\"\"\n        self._label = value\n\n    def run_command(self):\n        \"\"\"\n        Run the command of the instance or copy the command to the active shelf\n        based on the current modifiers.\n\n        If callbacks have been registered with fouind modifier integer the\n        function will trigger all callbacks. When a callback function returns a\n        non zero integer it will not execute the action's command\n\n        \"\"\"\n\n        # get the current application and its linked keyboard modifiers\n        app = QtWidgets.QApplication.instance()\n        modifiers = app.keyboardModifiers()\n\n        # If the menu has a callback registered for the current modifier\n        # we run the callback instead of the action itself.\n        registered = self._root.registered_callbacks\n        callbacks = registered.get(int(modifiers), [])\n        for callback in callbacks:\n            signal = callback(self)\n            if signal != 0:\n                # Exit function on non-zero return code\n                return\n\n        exec(self.process_command())\n\n    def process_command(self):\n        \"\"\"\n        Check if the command is a file which needs to be launched and if it \n        has a relative path, if so return the full path by expanding \n        environment variables. Wrap any mel command in a executable string \n        for Python and return the string if the source type is  \n        \n        Add your own source type and required process to ensure callback\n        is stored correctly.\n        \n        An example of a process is the sourcetype is MEL \n        (Maya Embedded Language) as Python cannot run it on its own so it \n        needs to be wrapped in a string in which we explicitly import mel and \n        run it as a mel.eval. The string is then parsed to python as \n        exec(\"command\"). \n\n        Returns:\n            str: a clean command which can be used\n\n        \"\"\"\n        if self._sourcetype == \"python\":\n            return self._command\n\n        if self._sourcetype == \"mel\":\n            # Escape single quotes\n            conversion = self._command.replace(\"'\", \"\\\\'\")\n            return \"import maya; maya.mel.eval('{}')\".format(conversion)\n\n        if self._sourcetype == \"file\":\n            if os.path.isabs(self._command):\n                filepath = self._command\n            else:\n                filepath = os.path.normpath(os.path.expandvars(self._command))\n\n            return self._wrap_filepath(filepath)\n\n    def has_tag(self, tag):\n        \"\"\"Check whether the tag matches with the action's tags.\n        \n        A partial match will also return True, for example tag `a` will match\n        correctly with the `ape` tag on the Action.\n\n        Args:\n            tag (str): The tag\n        \n        Returns\n            bool: Whether the action is tagged with given tag\n        \n        \"\"\"\n\n        for tagitem in self.tags:\n            if tag not in tagitem:\n                continue\n            return True\n\n        return False\n\n    def _wrap_filepath(self, file_path):\n        \"\"\"Create a wrapped string for the python command\n\n        Args:\n            file_path (str): the filepath of a script\n\n        Returns:\n            str: the wrapped command\n        \"\"\"\n\n        dirname = os.path.dirname(r\"{}\".format(file_path))\n        dirpath = dirname.replace(\"\\\\\", \"/\")\n        module_name = os.path.splitext(os.path.basename(file_path))[0]\n\n        return self._COMMAND.format(module_name=module_name, dirname=dirpath)\n"
  },
  {
    "path": "openpype/vendor/python/common/scriptsmenu/launchformari.py",
    "content": "\n# Import third-party modules\nfrom qtpy import QtWidgets\n\n# Import local modules\nimport scriptsmenu\n\n\ndef _mari_main_window():\n    \"\"\"Get Mari main window.\n\n    Returns:\n        MriMainWindow: Mari's main window.\n\n    \"\"\"\n    for obj in QtWidgets.QApplication.topLevelWidgets():\n        if obj.metaObject().className() == 'MriMainWindow':\n            return obj\n    raise RuntimeError('Could not find Mari MainWindow instance')\n\n\ndef _mari_main_menubar():\n    \"\"\"Get Mari main menu bar.\n\n    Returns:\n        Retrieve the main menubar of the Mari window.\n\n    \"\"\"\n    mari_window = _mari_main_window()\n    menubar = [\n        i for i in mari_window.children() if isinstance(i, QtWidgets.QMenuBar)\n    ]\n    assert len(menubar) == 1, \"Error, could not find menu bar!\"\n    return menubar[0]\n\n\ndef main(title=\"Scripts\"):\n    \"\"\"Build the main scripts menu in Mari.\n\n    Args:\n        title (str): Name of the menu in the application.\n\n    Returns:\n        scriptsmenu.ScriptsMenu:  Instance object.\n\n    \"\"\"\n    mari_main_bar = _mari_main_menubar()\n    for mari_bar in mari_main_bar.children():\n        if isinstance(mari_bar, scriptsmenu.ScriptsMenu):\n            if mari_bar.title() == title:\n                menu = mari_bar\n                return menu\n    menu = scriptsmenu.ScriptsMenu(title=title, parent=mari_main_bar)\n    return menu\n"
  },
  {
    "path": "openpype/vendor/python/common/scriptsmenu/launchformaya.py",
    "content": "import logging\n\nimport maya.cmds as cmds\nimport maya.mel as mel\n\nimport scriptsmenu\nfrom qtpy import QtCore, QtWidgets\n\nlog = logging.getLogger(__name__)\n\n\ndef register_repeat_last(action):\n    \"\"\"Register the action in repeatLast to ensure the RepeatLast hotkey works\n\n    Args:\n        action (action.Action): Action wigdet instance\n\n    Returns:\n        int: 0\n\n    \"\"\"\n    command = action.process_command()\n    command = command.replace(\"\\n\", \"; \")\n    # Register command to Maya (mel)\n    cmds.repeatLast(addCommand='python(\"{}\")'.format(command),\n                    addCommandLabel=action.label)\n\n    return 0\n\n\ndef to_shelf(action):\n    \"\"\"Copy clicked menu item to the currently active Maya shelf\n    Args:\n        action (action.Action): the action instance which is clicked\n\n    Returns:\n        int: 1\n\n    \"\"\"\n\n    shelftoplevel = mel.eval(\"$gShelfTopLevel = $gShelfTopLevel;\")\n    current_active_shelf = cmds.tabLayout(shelftoplevel,\n                                          query=True,\n                                          selectTab=True)\n\n    cmds.shelfButton(command=action.process_command(),\n                     sourceType=\"python\",\n                     parent=current_active_shelf,\n                     image=action.iconfile or \"pythonFamily.png\",\n                     annotation=action.statusTip(),\n                     imageOverlayLabel=action.label or \"\")\n\n    return 1\n\n\ndef _maya_main_window():\n    \"\"\"Return Maya's main window\"\"\"\n    for obj in QtWidgets.QApplication.topLevelWidgets():\n        if obj.objectName() == 'MayaWindow':\n            return obj\n    raise RuntimeError('Could not find MayaWindow instance')\n\n\ndef _maya_main_menubar():\n    \"\"\"Retrieve the main menubar of the Maya window\"\"\"\n    mayawindow = _maya_main_window()\n    menubar = [i for i in mayawindow.children()\n               if isinstance(i, QtWidgets.QMenuBar)]\n\n    assert len(menubar) == 1, \"Error, could not find menu bar!\"\n\n    return menubar[0]\n\n\ndef find_scripts_menu(title, parent):\n    \"\"\"\n    Check if the menu exists with the given title in the parent\n\n    Args:\n        title (str): the title name of the scripts menu\n\n        parent (QtWidgets.QMenuBar): the menubar to check\n\n    Returns:\n        QtWidgets.QMenu or None\n\n    \"\"\"\n\n    menu = None\n    search = [i for i in parent.children() if\n              isinstance(i, scriptsmenu.ScriptsMenu)\n              and i.title() == title]\n\n    if search:\n        assert len(search) < 2, (\"Multiple instances of menu '{}' \"\n                                 \"in menu bar\".format(title))\n        menu = search[0]\n\n    return menu\n\n\ndef main(title=\"Scripts\", parent=None, objectName=None):\n    \"\"\"Build the main scripts menu in Maya\n\n    Args:\n        title (str): name of the menu in the application\n\n        parent (QtWidgets.QtMenuBar): the parent object for the menu\n\n        objectName (str): custom objectName for scripts menu\n\n    Returns:\n        scriptsmenu.ScriptsMenu instance\n\n    \"\"\"\n\n    mayamainbar = parent or _maya_main_menubar()\n    try:\n        # check menu already exists\n        menu = find_scripts_menu(title, mayamainbar)\n        if not menu:\n            log.info(\"Attempting to build menu ...\")\n            object_name = objectName or title.lower()\n            menu = scriptsmenu.ScriptsMenu(title=title,\n                                           parent=mayamainbar,\n                                           objectName=object_name)\n    except Exception as e:\n        log.error(e)\n        return\n\n    # Register control + shift callback to add to shelf (maya behavior)\n    modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier\n    menu.register_callback(int(modifiers), to_shelf)\n\n    menu.register_callback(0, register_repeat_last)\n\n    return menu\n"
  },
  {
    "path": "openpype/vendor/python/common/scriptsmenu/launchfornuke.py",
    "content": "import scriptsmenu\nfrom qtpy import QtWidgets\n\n\ndef _nuke_main_window():\n    \"\"\"Return Nuke's main window\"\"\"\n    for obj in QtWidgets.QApplication.topLevelWidgets():\n        if (obj.inherits('QMainWindow') and\n                    obj.metaObject().className() == 'Foundry::UI::DockMainWindow'):\n            return obj\n    raise RuntimeError('Could not find Nuke MainWindow instance')\n\n\ndef _nuke_main_menubar():\n    \"\"\"Retrieve the main menubar of the Nuke window\"\"\"\n    nuke_window = _nuke_main_window()\n    menubar = [i for i in nuke_window.children()\n               if isinstance(i, QtWidgets.QMenuBar)]\n\n    assert len(menubar) == 1, \"Error, could not find menu bar!\"\n    return menubar[0]\n\n\ndef main(title=\"Scripts\"):\n    nuke_main_bar = _nuke_main_menubar()\n    for nuke_bar in nuke_main_bar.children():\n        if isinstance(nuke_bar, scriptsmenu.ScriptsMenu):\n            if nuke_bar.title() == title:\n                menu = nuke_bar\n                return menu\n\n    menu = scriptsmenu.ScriptsMenu(title=title, parent=nuke_main_bar)\n    return menu\n"
  },
  {
    "path": "openpype/vendor/python/common/scriptsmenu/scriptsmenu.py",
    "content": "import os\nimport json\nimport logging\nfrom collections import defaultdict\n\nfrom qtpy import QtWidgets, QtCore\nfrom . import action\n\nlog = logging.getLogger(__name__)\n\n\nclass ScriptsMenu(QtWidgets.QMenu):\n    \"\"\"A Qt menu that displays a list of searchable actions\"\"\"\n\n    updated = QtCore.Signal(QtWidgets.QMenu)\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"Initialize Scripts menu\n\n        Args:\n            title (str): the name of the root menu which will be created\n\n            parent (QtWidgets.QObject) : the QObject to parent the menu to\n\n        Returns:\n            None\n\n        \"\"\"\n        QtWidgets.QMenu.__init__(self, *args, **kwargs)\n\n        self.searchbar = None\n        self.update_action = None\n\n        self._script_actions = []\n        self._callbacks = defaultdict(list)\n\n        # Automatically add it to the parent menu\n        parent = kwargs.get(\"parent\", None)\n        if parent:\n            parent.addMenu(self)\n\n        objectname = kwargs.get(\"objectName\", \"scripts\")\n        title = kwargs.get(\"title\", \"Scripts\")\n        self.setObjectName(objectname)\n        self.setTitle(title)\n\n        # add default items in the menu\n        self.create_default_items()\n\n    def on_update(self):\n        self.updated.emit(self)\n\n    @property\n    def registered_callbacks(self):\n        return self._callbacks.copy()\n\n    def create_default_items(self):\n        \"\"\"Add a search bar to the top of the menu given\"\"\"\n\n        # create widget and link function\n        searchbar = QtWidgets.QLineEdit()\n        searchbar.setFixedWidth(120)\n        searchbar.setPlaceholderText(\"Search ...\")\n        searchbar.textChanged.connect(self._update_search)\n        self.searchbar = searchbar\n\n        # create widget holder\n        searchbar_action = QtWidgets.QWidgetAction(self)\n\n        # add widget to widget holder\n        searchbar_action.setDefaultWidget(self.searchbar)\n        searchbar_action.setObjectName(\"Searchbar\")\n\n        # add update button and link function\n        update_action = QtWidgets.QAction(self)\n        update_action.setObjectName(\"Update Scripts\")\n        update_action.setText(\"Update Scripts\")\n        update_action.setVisible(False)\n        update_action.triggered.connect(self.on_update)\n        self.update_action = update_action\n\n        # add action to menu\n        self.addAction(searchbar_action)\n        self.addAction(update_action)\n\n        # add separator object\n        separator = self.addSeparator()\n        separator.setObjectName(\"separator\")\n\n    def add_menu(self, title, parent=None):\n        \"\"\"Create a sub menu for a parent widget\n\n        Args:\n            parent(QtWidgets.QWidget): the object to parent the menu to\n\n            title(str): the title of the menu\n\n        Returns:\n             QtWidget.QMenu instance\n        \"\"\"\n\n        if not parent:\n            parent = self\n\n        menu = QtWidgets.QMenu(parent, title)\n        menu.setTitle(title)\n        menu.setObjectName(title)\n        menu.setTearOffEnabled(True)\n        parent.addMenu(menu)\n\n        return menu\n\n    def add_script(self, parent, title, command, sourcetype, icon=None,\n                   tags=None, label=None, tooltip=None, shortcut=None):\n        \"\"\"Create an action item which runs a script when clicked\n\n        Args:\n            parent (QtWidget.QWidget): The widget to parent the item to\n\n            title (str): The text which will be displayed in the menu\n\n            command (str): The command which needs to be run when the item is\n                           clicked.\n\n            sourcetype (str): The type of command, the way the command is\n                              processed is based on the source type.\n\n            icon (str): The file path of an icon to display with the menu item\n\n            tags (list, tuple): Keywords which describe the action\n\n            label (str): A short description of the script which will be displayed\n                         when hovering over the menu item\n\n            tooltip (str): A tip for the user about the usage fo the tool\n\n            shortcut (str): A shortcut to run the command\n\n        Returns:\n            QtWidget.QAction instance\n\n        \"\"\"\n\n        assert tags is None or isinstance(tags, (list, tuple))\n        # Ensure tags is a list\n        tags = list() if tags is None else list(tags)\n        tags.append(title.lower())\n\n        assert icon is None or isinstance(icon, str), (\n            \"Invalid data type for icon, supported : None, string\")\n\n        # create new action\n        script_action = action.Action(parent)\n        script_action.setText(title)\n        script_action.setObjectName(title)\n        script_action.tags = tags\n\n        # link action to root for callback library\n        script_action.root = self\n\n        # Set up the command\n        script_action.sourcetype = sourcetype\n        script_action.command = command\n\n        try:\n            script_action.process_command()\n        except RuntimeError as e:\n            raise RuntimeError(\"Script action can't be \"\n                               \"processed: {}\".format(e))\n\n        if shortcut:\n            script_action.setShortcut(shortcut)\n\n        if icon:\n            iconfile = os.path.expandvars(icon)\n            script_action.iconfile = iconfile\n            script_action_icon = QtWidgets.QIcon(iconfile)\n            script_action.setIcon(script_action_icon)\n\n        if label:\n            script_action.label = label\n\n        if tooltip:\n            script_action.setStatusTip(tooltip)\n\n        script_action.triggered.connect(script_action.run_command)\n        parent.addAction(script_action)\n\n        # Add to our searchable actions\n        self._script_actions.append(script_action)\n\n        return script_action\n\n    def build_from_configuration(self, parent, configuration):\n        \"\"\"Process the configurations and store the configuration\n\n        This creates all submenus from a configuration.json file.\n\n        When the configuration holds the key `main` all scripts under `main` will\n        be added to the main menu first before adding the rest\n\n        Args:\n            parent (ScriptsMenu): script menu instance\n            configuration (list): A ScriptsMenu configuration list\n\n        Returns:\n            None\n\n        \"\"\"\n\n        for item in configuration:\n            assert isinstance(item, dict), \"Configuration is wrong!\"\n\n            # skip items which have no `type` key\n            item_type = item.get('type', None)\n            if not item_type:\n                log.warning(\"Missing 'type' from configuration item\")\n                continue\n\n            # add separator\n            # Special behavior for separators\n            if item_type == \"separator\":\n                parent.addSeparator()\n\n            # add submenu\n            # items should hold a collection of submenu items (dict)\n            elif item_type == \"menu\":\n                assert \"items\" in item, \"Menu is missing 'items' key\"\n                menu = self.add_menu(parent=parent, title=item[\"title\"])\n                self.build_from_configuration(menu, item[\"items\"])\n\n            # add script\n            elif item_type == \"action\":\n                # filter out `type` from the item dict\n                config = {key: value for key, value in\n                          item.items() if key != \"type\"}\n\n                self.add_script(parent=parent, **config)\n\n    def set_update_visible(self, state):\n        self.update_action.setVisible(state)\n\n    def clear_menu(self):\n        \"\"\"Clear all menu items which are not default\n\n        Returns:\n            None\n\n        \"\"\"\n\n        # TODO: Set up a more robust implementation for this\n        # Delete all except the first three actions\n        for _action in self.actions()[3:]:\n            self.removeAction(_action)\n\n    def register_callback(self, modifiers, callback):\n        self._callbacks[modifiers].append(callback)\n\n    def _update_search(self, search):\n        \"\"\"Hide all the samples which do not match the user's import\n\n        Returns:\n            None\n\n        \"\"\"\n\n        if not search:\n            for action in self._script_actions:\n                action.setVisible(True)\n        else:\n            for action in self._script_actions:\n                action.setVisible(action.has_tag(search.lower()))\n\n        # Set visibility for all submenus\n        for action in self.actions():\n            if not action.menu():\n                continue\n\n            menu = action.menu()\n            visible = any(action.isVisible() for action in menu.actions())\n            action.setVisible(visible)\n\n\ndef load_configuration(path):\n    \"\"\"Load the configuration from a file\n\n    Read out the JSON file which will dictate the structure of the scripts menu\n\n    Args:\n        path (str): file path of the .JSON file\n\n    Returns:\n        dict\n\n    \"\"\"\n\n    if not os.path.isfile(path):\n        raise AttributeError(\"Given configuration is not \"\n                             \"a file!\\n'{}'\".format(path))\n\n    extension = os.path.splitext(path)[-1]\n    if extension != \".json\":\n        raise AttributeError(\"Given configuration file has unsupported \"\n                             \"file type, provide a .json file\")\n\n    # retrieve and store config\n    with open(path, \"r\") as f:\n        configuration = json.load(f)\n\n    return configuration\n\n\ndef application(configuration, parent):\n    import sys\n    app = QtWidgets.QApplication(sys.argv)\n\n    scriptsmenu = ScriptsMenu(configuration, parent)\n    scriptsmenu.show()\n\n    sys.exit(app.exec_())\n"
  },
  {
    "path": "openpype/vendor/python/common/scriptsmenu/version.py",
    "content": "VERSION_MAJOR = 1\nVERSION_MINOR = 5\nVERSION_PATCH = 2\n\n\nversion = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)\n__version__ = version\n\n__all__ = ['version', '__version__']\n"
  },
  {
    "path": "openpype/vendor/python/python_2/README.md",
    "content": "## Info\n\nOnly **Python 2** specific modules are here."
  },
  {
    "path": "openpype/vendor/python/python_2/arrow/__init__.py",
    "content": "# -*- coding: utf-8 -*-\nfrom ._version import __version__\nfrom .api import get, now, utcnow\nfrom .arrow import Arrow\nfrom .factory import ArrowFactory\nfrom .formatter import (\n    FORMAT_ATOM,\n    FORMAT_COOKIE,\n    FORMAT_RFC822,\n    FORMAT_RFC850,\n    FORMAT_RFC1036,\n    FORMAT_RFC1123,\n    FORMAT_RFC2822,\n    FORMAT_RFC3339,\n    FORMAT_RSS,\n    FORMAT_W3C,\n)\nfrom .parser import ParserError\n"
  },
  {
    "path": "openpype/vendor/python/python_2/arrow/_version.py",
    "content": "__version__ = \"0.17.0\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/arrow/api.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nProvides the default implementation of :class:`ArrowFactory <arrow.factory.ArrowFactory>`\nmethods for use as a module API.\n\n\"\"\"\n\nfrom __future__ import absolute_import\n\nfrom arrow.factory import ArrowFactory\n\n# internal default factory.\n_factory = ArrowFactory()\n\n\ndef get(*args, **kwargs):\n    \"\"\"Calls the default :class:`ArrowFactory <arrow.factory.ArrowFactory>` ``get`` method.\"\"\"\n\n    return _factory.get(*args, **kwargs)\n\n\nget.__doc__ = _factory.get.__doc__\n\n\ndef utcnow():\n    \"\"\"Calls the default :class:`ArrowFactory <arrow.factory.ArrowFactory>` ``utcnow`` method.\"\"\"\n\n    return _factory.utcnow()\n\n\nutcnow.__doc__ = _factory.utcnow.__doc__\n\n\ndef now(tz=None):\n    \"\"\"Calls the default :class:`ArrowFactory <arrow.factory.ArrowFactory>` ``now`` method.\"\"\"\n\n    return _factory.now(tz)\n\n\nnow.__doc__ = _factory.now.__doc__\n\n\ndef factory(type):\n    \"\"\"Returns an :class:`.ArrowFactory` for the specified :class:`Arrow <arrow.arrow.Arrow>`\n    or derived type.\n\n    :param type: the type, :class:`Arrow <arrow.arrow.Arrow>` or derived.\n\n    \"\"\"\n\n    return ArrowFactory(type)\n\n\n__all__ = [\"get\", \"utcnow\", \"now\", \"factory\"]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/arrow/arrow.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nProvides the :class:`Arrow <arrow.arrow.Arrow>` class, an enhanced ``datetime``\nreplacement.\n\n\"\"\"\n\nfrom __future__ import absolute_import\n\nimport calendar\nimport sys\nimport warnings\nfrom datetime import datetime, timedelta\nfrom datetime import tzinfo as dt_tzinfo\nfrom math import trunc\n\nfrom dateutil import tz as dateutil_tz\nfrom dateutil.relativedelta import relativedelta\n\nfrom arrow import formatter, locales, parser, util\n\nif sys.version_info[:2] < (3, 6):  # pragma: no cover\n    with warnings.catch_warnings():\n        warnings.simplefilter(\"default\", DeprecationWarning)\n        warnings.warn(\n            \"Arrow will drop support for Python 2.7 and 3.5 in the upcoming v1.0.0 release. Please upgrade to \"\n            \"Python 3.6+ to continue receiving updates for Arrow.\",\n            DeprecationWarning,\n        )\n\n\nclass Arrow(object):\n    \"\"\"An :class:`Arrow <arrow.arrow.Arrow>` object.\n\n    Implements the ``datetime`` interface, behaving as an aware ``datetime`` while implementing\n    additional functionality.\n\n    :param year: the calendar year.\n    :param month: the calendar month.\n    :param day: the calendar day.\n    :param hour: (optional) the hour. Defaults to 0.\n    :param minute: (optional) the minute, Defaults to 0.\n    :param second: (optional) the second, Defaults to 0.\n    :param microsecond: (optional) the microsecond. Defaults to 0.\n    :param tzinfo: (optional) A timezone expression.  Defaults to UTC.\n    :param fold: (optional) 0 or 1, used to disambiguate repeated times. Defaults to 0.\n\n    .. _tz-expr:\n\n    Recognized timezone expressions:\n\n        - A ``tzinfo`` object.\n        - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.\n        - A ``str`` in ISO 8601 style, as in '+07:00'.\n        - A ``str``, one of the following:  'local', 'utc', 'UTC'.\n\n    Usage::\n\n        >>> import arrow\n        >>> arrow.Arrow(2013, 5, 5, 12, 30, 45)\n        <Arrow [2013-05-05T12:30:45+00:00]>\n\n    \"\"\"\n\n    resolution = datetime.resolution\n\n    _ATTRS = [\"year\", \"month\", \"day\", \"hour\", \"minute\", \"second\", \"microsecond\"]\n    _ATTRS_PLURAL = [\"{}s\".format(a) for a in _ATTRS]\n    _MONTHS_PER_QUARTER = 3\n    _SECS_PER_MINUTE = float(60)\n    _SECS_PER_HOUR = float(60 * 60)\n    _SECS_PER_DAY = float(60 * 60 * 24)\n    _SECS_PER_WEEK = float(60 * 60 * 24 * 7)\n    _SECS_PER_MONTH = float(60 * 60 * 24 * 30.5)\n    _SECS_PER_YEAR = float(60 * 60 * 24 * 365.25)\n\n    def __init__(\n        self,\n        year,\n        month,\n        day,\n        hour=0,\n        minute=0,\n        second=0,\n        microsecond=0,\n        tzinfo=None,\n        **kwargs\n    ):\n        if tzinfo is None:\n            tzinfo = dateutil_tz.tzutc()\n        # detect that tzinfo is a pytz object (issue #626)\n        elif (\n            isinstance(tzinfo, dt_tzinfo)\n            and hasattr(tzinfo, \"localize\")\n            and hasattr(tzinfo, \"zone\")\n            and tzinfo.zone\n        ):\n            tzinfo = parser.TzinfoParser.parse(tzinfo.zone)\n        elif util.isstr(tzinfo):\n            tzinfo = parser.TzinfoParser.parse(tzinfo)\n\n        fold = kwargs.get(\"fold\", 0)\n\n        # use enfold here to cover direct arrow.Arrow init on 2.7/3.5\n        self._datetime = dateutil_tz.enfold(\n            datetime(year, month, day, hour, minute, second, microsecond, tzinfo),\n            fold=fold,\n        )\n\n    # factories: single object, both original and from datetime.\n\n    @classmethod\n    def now(cls, tzinfo=None):\n        \"\"\"Constructs an :class:`Arrow <arrow.arrow.Arrow>` object, representing \"now\" in the given\n        timezone.\n\n        :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time.\n\n        Usage::\n\n            >>> arrow.now('Asia/Baku')\n            <Arrow [2019-01-24T20:26:31.146412+04:00]>\n\n        \"\"\"\n\n        if tzinfo is None:\n            tzinfo = dateutil_tz.tzlocal()\n\n        dt = datetime.now(tzinfo)\n\n        return cls(\n            dt.year,\n            dt.month,\n            dt.day,\n            dt.hour,\n            dt.minute,\n            dt.second,\n            dt.microsecond,\n            dt.tzinfo,\n            fold=getattr(dt, \"fold\", 0),\n        )\n\n    @classmethod\n    def utcnow(cls):\n        \"\"\"Constructs an :class:`Arrow <arrow.arrow.Arrow>` object, representing \"now\" in UTC\n        time.\n\n        Usage::\n\n            >>> arrow.utcnow()\n            <Arrow [2019-01-24T16:31:40.651108+00:00]>\n\n        \"\"\"\n\n        dt = datetime.now(dateutil_tz.tzutc())\n\n        return cls(\n            dt.year,\n            dt.month,\n            dt.day,\n            dt.hour,\n            dt.minute,\n            dt.second,\n            dt.microsecond,\n            dt.tzinfo,\n            fold=getattr(dt, \"fold\", 0),\n        )\n\n    @classmethod\n    def fromtimestamp(cls, timestamp, tzinfo=None):\n        \"\"\"Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a timestamp, converted to\n        the given timezone.\n\n        :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either.\n        :param tzinfo: (optional) a ``tzinfo`` object.  Defaults to local time.\n        \"\"\"\n\n        if tzinfo is None:\n            tzinfo = dateutil_tz.tzlocal()\n        elif util.isstr(tzinfo):\n            tzinfo = parser.TzinfoParser.parse(tzinfo)\n\n        if not util.is_timestamp(timestamp):\n            raise ValueError(\n                \"The provided timestamp '{}' is invalid.\".format(timestamp)\n            )\n\n        timestamp = util.normalize_timestamp(float(timestamp))\n        dt = datetime.fromtimestamp(timestamp, tzinfo)\n\n        return cls(\n            dt.year,\n            dt.month,\n            dt.day,\n            dt.hour,\n            dt.minute,\n            dt.second,\n            dt.microsecond,\n            dt.tzinfo,\n            fold=getattr(dt, \"fold\", 0),\n        )\n\n    @classmethod\n    def utcfromtimestamp(cls, timestamp):\n        \"\"\"Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a timestamp, in UTC time.\n\n        :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either.\n\n        \"\"\"\n\n        if not util.is_timestamp(timestamp):\n            raise ValueError(\n                \"The provided timestamp '{}' is invalid.\".format(timestamp)\n            )\n\n        timestamp = util.normalize_timestamp(float(timestamp))\n        dt = datetime.utcfromtimestamp(timestamp)\n\n        return cls(\n            dt.year,\n            dt.month,\n            dt.day,\n            dt.hour,\n            dt.minute,\n            dt.second,\n            dt.microsecond,\n            dateutil_tz.tzutc(),\n            fold=getattr(dt, \"fold\", 0),\n        )\n\n    @classmethod\n    def fromdatetime(cls, dt, tzinfo=None):\n        \"\"\"Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a ``datetime`` and\n        optional replacement timezone.\n\n        :param dt: the ``datetime``\n        :param tzinfo: (optional) A :ref:`timezone expression <tz-expr>`.  Defaults to ``dt``'s\n            timezone, or UTC if naive.\n\n        If you only want to replace the timezone of naive datetimes::\n\n            >>> dt\n            datetime.datetime(2013, 5, 5, 0, 0, tzinfo=tzutc())\n            >>> arrow.Arrow.fromdatetime(dt, dt.tzinfo or 'US/Pacific')\n            <Arrow [2013-05-05T00:00:00+00:00]>\n\n        \"\"\"\n\n        if tzinfo is None:\n            if dt.tzinfo is None:\n                tzinfo = dateutil_tz.tzutc()\n            else:\n                tzinfo = dt.tzinfo\n\n        return cls(\n            dt.year,\n            dt.month,\n            dt.day,\n            dt.hour,\n            dt.minute,\n            dt.second,\n            dt.microsecond,\n            tzinfo,\n            fold=getattr(dt, \"fold\", 0),\n        )\n\n    @classmethod\n    def fromdate(cls, date, tzinfo=None):\n        \"\"\"Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a ``date`` and optional\n        replacement timezone.  Time values are set to 0.\n\n        :param date: the ``date``\n        :param tzinfo: (optional) A :ref:`timezone expression <tz-expr>`.  Defaults to UTC.\n        \"\"\"\n\n        if tzinfo is None:\n            tzinfo = dateutil_tz.tzutc()\n\n        return cls(date.year, date.month, date.day, tzinfo=tzinfo)\n\n    @classmethod\n    def strptime(cls, date_str, fmt, tzinfo=None):\n        \"\"\"Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a date string and format,\n        in the style of ``datetime.strptime``.  Optionally replaces the parsed timezone.\n\n        :param date_str: the date string.\n        :param fmt: the format string.\n        :param tzinfo: (optional) A :ref:`timezone expression <tz-expr>`.  Defaults to the parsed\n            timezone if ``fmt`` contains a timezone directive, otherwise UTC.\n\n        Usage::\n\n            >>> arrow.Arrow.strptime('20-01-2019 15:49:10', '%d-%m-%Y %H:%M:%S')\n            <Arrow [2019-01-20T15:49:10+00:00]>\n\n        \"\"\"\n\n        dt = datetime.strptime(date_str, fmt)\n        if tzinfo is None:\n            tzinfo = dt.tzinfo\n\n        return cls(\n            dt.year,\n            dt.month,\n            dt.day,\n            dt.hour,\n            dt.minute,\n            dt.second,\n            dt.microsecond,\n            tzinfo,\n            fold=getattr(dt, \"fold\", 0),\n        )\n\n    # factories: ranges and spans\n\n    @classmethod\n    def range(cls, frame, start, end=None, tz=None, limit=None):\n        \"\"\"Returns an iterator of :class:`Arrow <arrow.arrow.Arrow>` objects, representing\n        points in time between two inputs.\n\n        :param frame: The timeframe.  Can be any ``datetime`` property (day, hour, minute...).\n        :param start: A datetime expression, the start of the range.\n        :param end: (optional) A datetime expression, the end of the range.\n        :param tz: (optional) A :ref:`timezone expression <tz-expr>`.  Defaults to\n            ``start``'s timezone, or UTC if ``start`` is naive.\n        :param limit: (optional) A maximum number of tuples to return.\n\n        **NOTE**: The ``end`` or ``limit`` must be provided.  Call with ``end`` alone to\n        return the entire range.  Call with ``limit`` alone to return a maximum # of results from\n        the start.  Call with both to cap a range at a maximum # of results.\n\n        **NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before\n        iterating.  As such, either call with naive objects and ``tz``, or aware objects from the\n        same timezone and no ``tz``.\n\n        Supported frame values: year, quarter, month, week, day, hour, minute, second.\n\n        Recognized datetime expressions:\n\n            - An :class:`Arrow <arrow.arrow.Arrow>` object.\n            - A ``datetime`` object.\n\n        Usage::\n\n            >>> start = datetime(2013, 5, 5, 12, 30)\n            >>> end = datetime(2013, 5, 5, 17, 15)\n            >>> for r in arrow.Arrow.range('hour', start, end):\n            ...     print(repr(r))\n            ...\n            <Arrow [2013-05-05T12:30:00+00:00]>\n            <Arrow [2013-05-05T13:30:00+00:00]>\n            <Arrow [2013-05-05T14:30:00+00:00]>\n            <Arrow [2013-05-05T15:30:00+00:00]>\n            <Arrow [2013-05-05T16:30:00+00:00]>\n\n        **NOTE**: Unlike Python's ``range``, ``end`` *may* be included in the returned iterator::\n\n            >>> start = datetime(2013, 5, 5, 12, 30)\n            >>> end = datetime(2013, 5, 5, 13, 30)\n            >>> for r in arrow.Arrow.range('hour', start, end):\n            ...     print(repr(r))\n            ...\n            <Arrow [2013-05-05T12:30:00+00:00]>\n            <Arrow [2013-05-05T13:30:00+00:00]>\n\n        \"\"\"\n\n        _, frame_relative, relative_steps = cls._get_frames(frame)\n\n        tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz)\n\n        start = cls._get_datetime(start).replace(tzinfo=tzinfo)\n        end, limit = cls._get_iteration_params(end, limit)\n        end = cls._get_datetime(end).replace(tzinfo=tzinfo)\n\n        current = cls.fromdatetime(start)\n        original_day = start.day\n        day_is_clipped = False\n        i = 0\n\n        while current <= end and i < limit:\n            i += 1\n            yield current\n\n            values = [getattr(current, f) for f in cls._ATTRS]\n            current = cls(*values, tzinfo=tzinfo).shift(\n                **{frame_relative: relative_steps}\n            )\n\n            if frame in [\"month\", \"quarter\", \"year\"] and current.day < original_day:\n                day_is_clipped = True\n\n            if day_is_clipped and not cls._is_last_day_of_month(current):\n                current = current.replace(day=original_day)\n\n    def span(self, frame, count=1, bounds=\"[)\"):\n        \"\"\"Returns two new :class:`Arrow <arrow.arrow.Arrow>` objects, representing the timespan\n        of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe.\n\n        :param frame: the timeframe.  Can be any ``datetime`` property (day, hour, minute...).\n        :param count: (optional) the number of frames to span.\n        :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies\n            whether to include or exclude the start and end values in the span. '(' excludes\n            the start, '[' includes the start, ')' excludes the end, and ']' includes the end.\n            If the bounds are not specified, the default bound '[)' is used.\n\n        Supported frame values: year, quarter, month, week, day, hour, minute, second.\n\n        Usage::\n\n            >>> arrow.utcnow()\n            <Arrow [2013-05-09T03:32:36.186203+00:00]>\n\n            >>> arrow.utcnow().span('hour')\n            (<Arrow [2013-05-09T03:00:00+00:00]>, <Arrow [2013-05-09T03:59:59.999999+00:00]>)\n\n            >>> arrow.utcnow().span('day')\n            (<Arrow [2013-05-09T00:00:00+00:00]>, <Arrow [2013-05-09T23:59:59.999999+00:00]>)\n\n            >>> arrow.utcnow().span('day', count=2)\n            (<Arrow [2013-05-09T00:00:00+00:00]>, <Arrow [2013-05-10T23:59:59.999999+00:00]>)\n\n            >>> arrow.utcnow().span('day', bounds='[]')\n            (<Arrow [2013-05-09T00:00:00+00:00]>, <Arrow [2013-05-10T00:00:00+00:00]>)\n\n        \"\"\"\n\n        util.validate_bounds(bounds)\n\n        frame_absolute, frame_relative, relative_steps = self._get_frames(frame)\n\n        if frame_absolute == \"week\":\n            attr = \"day\"\n        elif frame_absolute == \"quarter\":\n            attr = \"month\"\n        else:\n            attr = frame_absolute\n\n        index = self._ATTRS.index(attr)\n        frames = self._ATTRS[: index + 1]\n\n        values = [getattr(self, f) for f in frames]\n\n        for _ in range(3 - len(values)):\n            values.append(1)\n\n        floor = self.__class__(*values, tzinfo=self.tzinfo)\n\n        if frame_absolute == \"week\":\n            floor = floor.shift(days=-(self.isoweekday() - 1))\n        elif frame_absolute == \"quarter\":\n            floor = floor.shift(months=-((self.month - 1) % 3))\n\n        ceil = floor.shift(**{frame_relative: count * relative_steps})\n\n        if bounds[0] == \"(\":\n            floor = floor.shift(microseconds=+1)\n\n        if bounds[1] == \")\":\n            ceil = ceil.shift(microseconds=-1)\n\n        return floor, ceil\n\n    def floor(self, frame):\n        \"\"\"Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, representing the \"floor\"\n        of the timespan of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe.\n        Equivalent to the first element in the 2-tuple returned by\n        :func:`span <arrow.arrow.Arrow.span>`.\n\n        :param frame: the timeframe.  Can be any ``datetime`` property (day, hour, minute...).\n\n        Usage::\n\n            >>> arrow.utcnow().floor('hour')\n            <Arrow [2013-05-09T03:00:00+00:00]>\n        \"\"\"\n\n        return self.span(frame)[0]\n\n    def ceil(self, frame):\n        \"\"\"Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, representing the \"ceiling\"\n        of the timespan of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe.\n        Equivalent to the second element in the 2-tuple returned by\n        :func:`span <arrow.arrow.Arrow.span>`.\n\n        :param frame: the timeframe.  Can be any ``datetime`` property (day, hour, minute...).\n\n        Usage::\n\n            >>> arrow.utcnow().ceil('hour')\n            <Arrow [2013-05-09T03:59:59.999999+00:00]>\n        \"\"\"\n\n        return self.span(frame)[1]\n\n    @classmethod\n    def span_range(cls, frame, start, end, tz=None, limit=None, bounds=\"[)\"):\n        \"\"\"Returns an iterator of tuples, each :class:`Arrow <arrow.arrow.Arrow>` objects,\n        representing a series of timespans between two inputs.\n\n        :param frame: The timeframe.  Can be any ``datetime`` property (day, hour, minute...).\n        :param start: A datetime expression, the start of the range.\n        :param end: (optional) A datetime expression, the end of the range.\n        :param tz: (optional) A :ref:`timezone expression <tz-expr>`.  Defaults to\n            ``start``'s timezone, or UTC if ``start`` is naive.\n        :param limit: (optional) A maximum number of tuples to return.\n        :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies\n            whether to include or exclude the start and end values in each span in the range. '(' excludes\n            the start, '[' includes the start, ')' excludes the end, and ']' includes the end.\n            If the bounds are not specified, the default bound '[)' is used.\n\n        **NOTE**: The ``end`` or ``limit`` must be provided.  Call with ``end`` alone to\n        return the entire range.  Call with ``limit`` alone to return a maximum # of results from\n        the start.  Call with both to cap a range at a maximum # of results.\n\n        **NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before\n        iterating.  As such, either call with naive objects and ``tz``, or aware objects from the\n        same timezone and no ``tz``.\n\n        Supported frame values: year, quarter, month, week, day, hour, minute, second.\n\n        Recognized datetime expressions:\n\n            - An :class:`Arrow <arrow.arrow.Arrow>` object.\n            - A ``datetime`` object.\n\n        **NOTE**: Unlike Python's ``range``, ``end`` will *always* be included in the returned\n        iterator of timespans.\n\n        Usage:\n\n            >>> start = datetime(2013, 5, 5, 12, 30)\n            >>> end = datetime(2013, 5, 5, 17, 15)\n            >>> for r in arrow.Arrow.span_range('hour', start, end):\n            ...     print(r)\n            ...\n            (<Arrow [2013-05-05T12:00:00+00:00]>, <Arrow [2013-05-05T12:59:59.999999+00:00]>)\n            (<Arrow [2013-05-05T13:00:00+00:00]>, <Arrow [2013-05-05T13:59:59.999999+00:00]>)\n            (<Arrow [2013-05-05T14:00:00+00:00]>, <Arrow [2013-05-05T14:59:59.999999+00:00]>)\n            (<Arrow [2013-05-05T15:00:00+00:00]>, <Arrow [2013-05-05T15:59:59.999999+00:00]>)\n            (<Arrow [2013-05-05T16:00:00+00:00]>, <Arrow [2013-05-05T16:59:59.999999+00:00]>)\n            (<Arrow [2013-05-05T17:00:00+00:00]>, <Arrow [2013-05-05T17:59:59.999999+00:00]>)\n\n        \"\"\"\n\n        tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz)\n        start = cls.fromdatetime(start, tzinfo).span(frame)[0]\n        _range = cls.range(frame, start, end, tz, limit)\n        return (r.span(frame, bounds=bounds) for r in _range)\n\n    @classmethod\n    def interval(cls, frame, start, end, interval=1, tz=None, bounds=\"[)\"):\n        \"\"\"Returns an iterator of tuples, each :class:`Arrow <arrow.arrow.Arrow>` objects,\n        representing a series of intervals between two inputs.\n\n        :param frame: The timeframe.  Can be any ``datetime`` property (day, hour, minute...).\n        :param start: A datetime expression, the start of the range.\n        :param end: (optional) A datetime expression, the end of the range.\n        :param interval: (optional) Time interval for the given time frame.\n        :param tz: (optional) A timezone expression.  Defaults to UTC.\n        :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies\n            whether to include or exclude the start and end values in the intervals. '(' excludes\n            the start, '[' includes the start, ')' excludes the end, and ']' includes the end.\n            If the bounds are not specified, the default bound '[)' is used.\n\n        Supported frame values: year, quarter, month, week, day, hour, minute, second\n\n        Recognized datetime expressions:\n\n            - An :class:`Arrow <arrow.arrow.Arrow>` object.\n            - A ``datetime`` object.\n\n        Recognized timezone expressions:\n\n            - A ``tzinfo`` object.\n            - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.\n            - A ``str`` in ISO 8601 style, as in '+07:00'.\n            - A ``str``, one of the following:  'local', 'utc', 'UTC'.\n\n        Usage:\n\n            >>> start = datetime(2013, 5, 5, 12, 30)\n            >>> end = datetime(2013, 5, 5, 17, 15)\n            >>> for r in arrow.Arrow.interval('hour', start, end, 2):\n            ...     print r\n            ...\n            (<Arrow [2013-05-05T12:00:00+00:00]>, <Arrow [2013-05-05T13:59:59.999999+00:00]>)\n            (<Arrow [2013-05-05T14:00:00+00:00]>, <Arrow [2013-05-05T15:59:59.999999+00:00]>)\n            (<Arrow [2013-05-05T16:00:00+00:00]>, <Arrow [2013-05-05T17:59:59.999999+00:0]>)\n        \"\"\"\n        if interval < 1:\n            raise ValueError(\"interval has to be a positive integer\")\n\n        spanRange = iter(cls.span_range(frame, start, end, tz, bounds=bounds))\n        while True:\n            try:\n                intvlStart, intvlEnd = next(spanRange)\n                for _ in range(interval - 1):\n                    _, intvlEnd = next(spanRange)\n                yield intvlStart, intvlEnd\n            except StopIteration:\n                return\n\n    # representations\n\n    def __repr__(self):\n        return \"<{} [{}]>\".format(self.__class__.__name__, self.__str__())\n\n    def __str__(self):\n        return self._datetime.isoformat()\n\n    def __format__(self, formatstr):\n\n        if len(formatstr) > 0:\n            return self.format(formatstr)\n\n        return str(self)\n\n    def __hash__(self):\n        return self._datetime.__hash__()\n\n    # attributes and properties\n\n    def __getattr__(self, name):\n\n        if name == \"week\":\n            return self.isocalendar()[1]\n\n        if name == \"quarter\":\n            return int((self.month - 1) / self._MONTHS_PER_QUARTER) + 1\n\n        if not name.startswith(\"_\"):\n            value = getattr(self._datetime, name, None)\n\n            if value is not None:\n                return value\n\n        return object.__getattribute__(self, name)\n\n    @property\n    def tzinfo(self):\n        \"\"\"Gets the ``tzinfo`` of the :class:`Arrow <arrow.arrow.Arrow>` object.\n\n        Usage::\n\n            >>> arw=arrow.utcnow()\n            >>> arw.tzinfo\n            tzutc()\n\n        \"\"\"\n\n        return self._datetime.tzinfo\n\n    @tzinfo.setter\n    def tzinfo(self, tzinfo):\n        \"\"\" Sets the ``tzinfo`` of the :class:`Arrow <arrow.arrow.Arrow>` object. \"\"\"\n\n        self._datetime = self._datetime.replace(tzinfo=tzinfo)\n\n    @property\n    def datetime(self):\n        \"\"\"Returns a datetime representation of the :class:`Arrow <arrow.arrow.Arrow>` object.\n\n        Usage::\n\n            >>> arw=arrow.utcnow()\n            >>> arw.datetime\n            datetime.datetime(2019, 1, 24, 16, 35, 27, 276649, tzinfo=tzutc())\n\n        \"\"\"\n\n        return self._datetime\n\n    @property\n    def naive(self):\n        \"\"\"Returns a naive datetime representation of the :class:`Arrow <arrow.arrow.Arrow>`\n        object.\n\n        Usage::\n\n            >>> nairobi = arrow.now('Africa/Nairobi')\n            >>> nairobi\n            <Arrow [2019-01-23T19:27:12.297999+03:00]>\n            >>> nairobi.naive\n            datetime.datetime(2019, 1, 23, 19, 27, 12, 297999)\n\n        \"\"\"\n\n        return self._datetime.replace(tzinfo=None)\n\n    @property\n    def timestamp(self):\n        \"\"\"Returns a timestamp representation of the :class:`Arrow <arrow.arrow.Arrow>` object, in\n        UTC time.\n\n        Usage::\n\n            >>> arrow.utcnow().timestamp\n            1548260567\n\n        \"\"\"\n\n        warnings.warn(\n            \"For compatibility with the datetime.timestamp() method this property will be replaced with a method in \"\n            \"the 1.0.0 release, please switch to the .int_timestamp property for identical behaviour as soon as \"\n            \"possible.\",\n            DeprecationWarning,\n        )\n        return calendar.timegm(self._datetime.utctimetuple())\n\n    @property\n    def int_timestamp(self):\n        \"\"\"Returns a timestamp representation of the :class:`Arrow <arrow.arrow.Arrow>` object, in\n        UTC time.\n\n        Usage::\n\n            >>> arrow.utcnow().int_timestamp\n            1548260567\n\n        \"\"\"\n\n        return calendar.timegm(self._datetime.utctimetuple())\n\n    @property\n    def float_timestamp(self):\n        \"\"\"Returns a floating-point representation of the :class:`Arrow <arrow.arrow.Arrow>`\n        object, in UTC time.\n\n        Usage::\n\n            >>> arrow.utcnow().float_timestamp\n            1548260516.830896\n\n        \"\"\"\n\n        # IDEA get rid of this in 1.0.0 and wrap datetime.timestamp()\n        # Or for compatibility retain this but make it call the timestamp method\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", DeprecationWarning)\n            return self.timestamp + float(self.microsecond) / 1000000\n\n    @property\n    def fold(self):\n        \"\"\" Returns the ``fold`` value of the :class:`Arrow <arrow.arrow.Arrow>` object. \"\"\"\n\n        # in python < 3.6 _datetime will be a _DatetimeWithFold if fold=1 and a datetime with no fold attribute\n        # otherwise, so we need to return zero to cover the latter case\n        return getattr(self._datetime, \"fold\", 0)\n\n    @property\n    def ambiguous(self):\n        \"\"\" Returns a boolean indicating whether the :class:`Arrow <arrow.arrow.Arrow>` object is ambiguous.\"\"\"\n\n        return dateutil_tz.datetime_ambiguous(self._datetime)\n\n    @property\n    def imaginary(self):\n        \"\"\"Indicates whether the :class: `Arrow <arrow.arrow.Arrow>` object exists in the current timezone.\"\"\"\n\n        return not dateutil_tz.datetime_exists(self._datetime)\n\n    # mutation and duplication.\n\n    def clone(self):\n        \"\"\"Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, cloned from the current one.\n\n        Usage:\n\n            >>> arw = arrow.utcnow()\n            >>> cloned = arw.clone()\n\n        \"\"\"\n\n        return self.fromdatetime(self._datetime)\n\n    def replace(self, **kwargs):\n        \"\"\"Returns a new :class:`Arrow <arrow.arrow.Arrow>` object with attributes updated\n        according to inputs.\n\n        Use property names to set their value absolutely::\n\n            >>> import arrow\n            >>> arw = arrow.utcnow()\n            >>> arw\n            <Arrow [2013-05-11T22:27:34.787885+00:00]>\n            >>> arw.replace(year=2014, month=6)\n            <Arrow [2014-06-11T22:27:34.787885+00:00]>\n\n        You can also replace the timezone without conversion, using a\n        :ref:`timezone expression <tz-expr>`::\n\n            >>> arw.replace(tzinfo=tz.tzlocal())\n            <Arrow [2013-05-11T22:27:34.787885-07:00]>\n\n        \"\"\"\n\n        absolute_kwargs = {}\n\n        for key, value in kwargs.items():\n\n            if key in self._ATTRS:\n                absolute_kwargs[key] = value\n            elif key in [\"week\", \"quarter\"]:\n                raise AttributeError(\"setting absolute {} is not supported\".format(key))\n            elif key not in [\"tzinfo\", \"fold\"]:\n                raise AttributeError('unknown attribute: \"{}\"'.format(key))\n\n        current = self._datetime.replace(**absolute_kwargs)\n\n        tzinfo = kwargs.get(\"tzinfo\")\n\n        if tzinfo is not None:\n            tzinfo = self._get_tzinfo(tzinfo)\n            current = current.replace(tzinfo=tzinfo)\n\n        fold = kwargs.get(\"fold\")\n\n        # TODO revisit this once we drop support for 2.7/3.5\n        if fold is not None:\n            current = dateutil_tz.enfold(current, fold=fold)\n\n        return self.fromdatetime(current)\n\n    def shift(self, **kwargs):\n        \"\"\"Returns a new :class:`Arrow <arrow.arrow.Arrow>` object with attributes updated\n        according to inputs.\n\n        Use pluralized property names to relatively shift their current value:\n\n        >>> import arrow\n        >>> arw = arrow.utcnow()\n        >>> arw\n        <Arrow [2013-05-11T22:27:34.787885+00:00]>\n        >>> arw.shift(years=1, months=-1)\n        <Arrow [2014-04-11T22:27:34.787885+00:00]>\n\n        Day-of-the-week relative shifting can use either Python's weekday numbers\n        (Monday = 0, Tuesday = 1 .. Sunday = 6) or using dateutil.relativedelta's\n        day instances (MO, TU .. SU).  When using weekday numbers, the returned\n        date will always be greater than or equal to the starting date.\n\n        Using the above code (which is a Saturday) and asking it to shift to Saturday:\n\n        >>> arw.shift(weekday=5)\n        <Arrow [2013-05-11T22:27:34.787885+00:00]>\n\n        While asking for a Monday:\n\n        >>> arw.shift(weekday=0)\n        <Arrow [2013-05-13T22:27:34.787885+00:00]>\n\n        \"\"\"\n\n        relative_kwargs = {}\n        additional_attrs = [\"weeks\", \"quarters\", \"weekday\"]\n\n        for key, value in kwargs.items():\n\n            if key in self._ATTRS_PLURAL or key in additional_attrs:\n                relative_kwargs[key] = value\n            else:\n                raise AttributeError(\n                    \"Invalid shift time frame. Please select one of the following: {}.\".format(\n                        \", \".join(self._ATTRS_PLURAL + additional_attrs)\n                    )\n                )\n\n        # core datetime does not support quarters, translate to months.\n        relative_kwargs.setdefault(\"months\", 0)\n        relative_kwargs[\"months\"] += (\n            relative_kwargs.pop(\"quarters\", 0) * self._MONTHS_PER_QUARTER\n        )\n\n        current = self._datetime + relativedelta(**relative_kwargs)\n\n        if not dateutil_tz.datetime_exists(current):\n            current = dateutil_tz.resolve_imaginary(current)\n\n        return self.fromdatetime(current)\n\n    def to(self, tz):\n        \"\"\"Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, converted\n        to the target timezone.\n\n        :param tz: A :ref:`timezone expression <tz-expr>`.\n\n        Usage::\n\n            >>> utc = arrow.utcnow()\n            >>> utc\n            <Arrow [2013-05-09T03:49:12.311072+00:00]>\n\n            >>> utc.to('US/Pacific')\n            <Arrow [2013-05-08T20:49:12.311072-07:00]>\n\n            >>> utc.to(tz.tzlocal())\n            <Arrow [2013-05-08T20:49:12.311072-07:00]>\n\n            >>> utc.to('-07:00')\n            <Arrow [2013-05-08T20:49:12.311072-07:00]>\n\n            >>> utc.to('local')\n            <Arrow [2013-05-08T20:49:12.311072-07:00]>\n\n            >>> utc.to('local').to('utc')\n            <Arrow [2013-05-09T03:49:12.311072+00:00]>\n\n        \"\"\"\n\n        if not isinstance(tz, dt_tzinfo):\n            tz = parser.TzinfoParser.parse(tz)\n\n        dt = self._datetime.astimezone(tz)\n\n        return self.__class__(\n            dt.year,\n            dt.month,\n            dt.day,\n            dt.hour,\n            dt.minute,\n            dt.second,\n            dt.microsecond,\n            dt.tzinfo,\n            fold=getattr(dt, \"fold\", 0),\n        )\n\n    # string output and formatting\n\n    def format(self, fmt=\"YYYY-MM-DD HH:mm:ssZZ\", locale=\"en_us\"):\n        \"\"\"Returns a string representation of the :class:`Arrow <arrow.arrow.Arrow>` object,\n        formatted according to a format string.\n\n        :param fmt: the format string.\n\n        Usage::\n\n            >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ')\n            '2013-05-09 03:56:47 -00:00'\n\n            >>> arrow.utcnow().format('X')\n            '1368071882'\n\n            >>> arrow.utcnow().format('MMMM DD, YYYY')\n            'May 09, 2013'\n\n            >>> arrow.utcnow().format()\n            '2013-05-09 03:56:47 -00:00'\n\n        \"\"\"\n\n        return formatter.DateTimeFormatter(locale).format(self._datetime, fmt)\n\n    def humanize(\n        self, other=None, locale=\"en_us\", only_distance=False, granularity=\"auto\"\n    ):\n        \"\"\"Returns a localized, humanized representation of a relative difference in time.\n\n        :param other: (optional) an :class:`Arrow <arrow.arrow.Arrow>` or ``datetime`` object.\n            Defaults to now in the current :class:`Arrow <arrow.arrow.Arrow>` object's timezone.\n        :param locale: (optional) a ``str`` specifying a locale.  Defaults to 'en_us'.\n        :param only_distance: (optional) returns only time difference eg: \"11 seconds\" without \"in\" or \"ago\" part.\n        :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute',\n                           'hour', 'day', 'week', 'month' or 'year' or a list of any combination of these strings\n\n        Usage::\n\n            >>> earlier = arrow.utcnow().shift(hours=-2)\n            >>> earlier.humanize()\n            '2 hours ago'\n\n            >>> later = earlier.shift(hours=4)\n            >>> later.humanize(earlier)\n            'in 4 hours'\n\n        \"\"\"\n\n        locale_name = locale\n        locale = locales.get_locale(locale)\n\n        if other is None:\n            utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc())\n            dt = utc.astimezone(self._datetime.tzinfo)\n\n        elif isinstance(other, Arrow):\n            dt = other._datetime\n\n        elif isinstance(other, datetime):\n            if other.tzinfo is None:\n                dt = other.replace(tzinfo=self._datetime.tzinfo)\n            else:\n                dt = other.astimezone(self._datetime.tzinfo)\n\n        else:\n            raise TypeError(\n                \"Invalid 'other' argument of type '{}'. \"\n                \"Argument must be of type None, Arrow, or datetime.\".format(\n                    type(other).__name__\n                )\n            )\n\n        if isinstance(granularity, list) and len(granularity) == 1:\n            granularity = granularity[0]\n\n        delta = int(round(util.total_seconds(self._datetime - dt)))\n        sign = -1 if delta < 0 else 1\n        diff = abs(delta)\n        delta = diff\n\n        try:\n            if granularity == \"auto\":\n                if diff < 10:\n                    return locale.describe(\"now\", only_distance=only_distance)\n\n                if diff < 45:\n                    seconds = sign * delta\n                    return locale.describe(\n                        \"seconds\", seconds, only_distance=only_distance\n                    )\n\n                elif diff < 90:\n                    return locale.describe(\"minute\", sign, only_distance=only_distance)\n                elif diff < 2700:\n                    minutes = sign * int(max(delta / 60, 2))\n                    return locale.describe(\n                        \"minutes\", minutes, only_distance=only_distance\n                    )\n\n                elif diff < 5400:\n                    return locale.describe(\"hour\", sign, only_distance=only_distance)\n                elif diff < 79200:\n                    hours = sign * int(max(delta / 3600, 2))\n                    return locale.describe(\"hours\", hours, only_distance=only_distance)\n\n                # anything less than 48 hours should be 1 day\n                elif diff < 172800:\n                    return locale.describe(\"day\", sign, only_distance=only_distance)\n                elif diff < 554400:\n                    days = sign * int(max(delta / 86400, 2))\n                    return locale.describe(\"days\", days, only_distance=only_distance)\n\n                elif diff < 907200:\n                    return locale.describe(\"week\", sign, only_distance=only_distance)\n                elif diff < 2419200:\n                    weeks = sign * int(max(delta / 604800, 2))\n                    return locale.describe(\"weeks\", weeks, only_distance=only_distance)\n\n                elif diff < 3888000:\n                    return locale.describe(\"month\", sign, only_distance=only_distance)\n                elif diff < 29808000:\n                    self_months = self._datetime.year * 12 + self._datetime.month\n                    other_months = dt.year * 12 + dt.month\n\n                    months = sign * int(max(abs(other_months - self_months), 2))\n\n                    return locale.describe(\n                        \"months\", months, only_distance=only_distance\n                    )\n\n                elif diff < 47260800:\n                    return locale.describe(\"year\", sign, only_distance=only_distance)\n                else:\n                    years = sign * int(max(delta / 31536000, 2))\n                    return locale.describe(\"years\", years, only_distance=only_distance)\n\n            elif util.isstr(granularity):\n                if granularity == \"second\":\n                    delta = sign * delta\n                    if abs(delta) < 2:\n                        return locale.describe(\"now\", only_distance=only_distance)\n                elif granularity == \"minute\":\n                    delta = sign * delta / self._SECS_PER_MINUTE\n                elif granularity == \"hour\":\n                    delta = sign * delta / self._SECS_PER_HOUR\n                elif granularity == \"day\":\n                    delta = sign * delta / self._SECS_PER_DAY\n                elif granularity == \"week\":\n                    delta = sign * delta / self._SECS_PER_WEEK\n                elif granularity == \"month\":\n                    delta = sign * delta / self._SECS_PER_MONTH\n                elif granularity == \"year\":\n                    delta = sign * delta / self._SECS_PER_YEAR\n                else:\n                    raise AttributeError(\n                        \"Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'\"\n                    )\n\n                if trunc(abs(delta)) != 1:\n                    granularity += \"s\"\n                return locale.describe(granularity, delta, only_distance=only_distance)\n\n            else:\n                timeframes = []\n                if \"year\" in granularity:\n                    years = sign * delta / self._SECS_PER_YEAR\n                    delta %= self._SECS_PER_YEAR\n                    timeframes.append([\"year\", years])\n\n                if \"month\" in granularity:\n                    months = sign * delta / self._SECS_PER_MONTH\n                    delta %= self._SECS_PER_MONTH\n                    timeframes.append([\"month\", months])\n\n                if \"week\" in granularity:\n                    weeks = sign * delta / self._SECS_PER_WEEK\n                    delta %= self._SECS_PER_WEEK\n                    timeframes.append([\"week\", weeks])\n\n                if \"day\" in granularity:\n                    days = sign * delta / self._SECS_PER_DAY\n                    delta %= self._SECS_PER_DAY\n                    timeframes.append([\"day\", days])\n\n                if \"hour\" in granularity:\n                    hours = sign * delta / self._SECS_PER_HOUR\n                    delta %= self._SECS_PER_HOUR\n                    timeframes.append([\"hour\", hours])\n\n                if \"minute\" in granularity:\n                    minutes = sign * delta / self._SECS_PER_MINUTE\n                    delta %= self._SECS_PER_MINUTE\n                    timeframes.append([\"minute\", minutes])\n\n                if \"second\" in granularity:\n                    seconds = sign * delta\n                    timeframes.append([\"second\", seconds])\n\n                if len(timeframes) < len(granularity):\n                    raise AttributeError(\n                        \"Invalid level of granularity. \"\n                        \"Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'.\"\n                    )\n\n                for tf in timeframes:\n                    # Make granularity plural if the delta is not equal to 1\n                    if trunc(abs(tf[1])) != 1:\n                        tf[0] += \"s\"\n                return locale.describe_multi(timeframes, only_distance=only_distance)\n\n        except KeyError as e:\n            raise ValueError(\n                \"Humanization of the {} granularity is not currently translated in the '{}' locale. \"\n                \"Please consider making a contribution to this locale.\".format(\n                    e, locale_name\n                )\n            )\n\n    # query functions\n\n    def is_between(self, start, end, bounds=\"()\"):\n        \"\"\"Returns a boolean denoting whether the specified date and time is between\n        the start and end dates and times.\n\n        :param start: an :class:`Arrow <arrow.arrow.Arrow>` object.\n        :param end: an :class:`Arrow <arrow.arrow.Arrow>` object.\n        :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies\n            whether to include or exclude the start and end values in the range. '(' excludes\n            the start, '[' includes the start, ')' excludes the end, and ']' includes the end.\n            If the bounds are not specified, the default bound '()' is used.\n\n        Usage::\n\n            >>> start = arrow.get(datetime(2013, 5, 5, 12, 30, 10))\n            >>> end = arrow.get(datetime(2013, 5, 5, 12, 30, 36))\n            >>> arrow.get(datetime(2013, 5, 5, 12, 30, 27)).is_between(start, end)\n            True\n\n            >>> start = arrow.get(datetime(2013, 5, 5))\n            >>> end = arrow.get(datetime(2013, 5, 8))\n            >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[]')\n            True\n\n            >>> start = arrow.get(datetime(2013, 5, 5))\n            >>> end = arrow.get(datetime(2013, 5, 8))\n            >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[)')\n            False\n\n        \"\"\"\n\n        util.validate_bounds(bounds)\n\n        if not isinstance(start, Arrow):\n            raise TypeError(\n                \"Can't parse start date argument type of '{}'\".format(type(start))\n            )\n\n        if not isinstance(end, Arrow):\n            raise TypeError(\n                \"Can't parse end date argument type of '{}'\".format(type(end))\n            )\n\n        include_start = bounds[0] == \"[\"\n        include_end = bounds[1] == \"]\"\n\n        target_timestamp = self.float_timestamp\n        start_timestamp = start.float_timestamp\n        end_timestamp = end.float_timestamp\n\n        if include_start and include_end:\n            return (\n                target_timestamp >= start_timestamp\n                and target_timestamp <= end_timestamp\n            )\n        elif include_start and not include_end:\n            return (\n                target_timestamp >= start_timestamp and target_timestamp < end_timestamp\n            )\n        elif not include_start and include_end:\n            return (\n                target_timestamp > start_timestamp and target_timestamp <= end_timestamp\n            )\n        else:\n            return (\n                target_timestamp > start_timestamp and target_timestamp < end_timestamp\n            )\n\n    # datetime methods\n\n    def date(self):\n        \"\"\"Returns a ``date`` object with the same year, month and day.\n\n        Usage::\n\n            >>> arrow.utcnow().date()\n            datetime.date(2019, 1, 23)\n\n        \"\"\"\n\n        return self._datetime.date()\n\n    def time(self):\n        \"\"\"Returns a ``time`` object with the same hour, minute, second, microsecond.\n\n        Usage::\n\n            >>> arrow.utcnow().time()\n            datetime.time(12, 15, 34, 68352)\n\n        \"\"\"\n\n        return self._datetime.time()\n\n    def timetz(self):\n        \"\"\"Returns a ``time`` object with the same hour, minute, second, microsecond and\n        tzinfo.\n\n        Usage::\n\n            >>> arrow.utcnow().timetz()\n            datetime.time(12, 5, 18, 298893, tzinfo=tzutc())\n\n        \"\"\"\n\n        return self._datetime.timetz()\n\n    def astimezone(self, tz):\n        \"\"\"Returns a ``datetime`` object, converted to the specified timezone.\n\n        :param tz: a ``tzinfo`` object.\n\n        Usage::\n\n            >>> pacific=arrow.now('US/Pacific')\n            >>> nyc=arrow.now('America/New_York').tzinfo\n            >>> pacific.astimezone(nyc)\n            datetime.datetime(2019, 1, 20, 10, 24, 22, 328172, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York'))\n\n        \"\"\"\n\n        return self._datetime.astimezone(tz)\n\n    def utcoffset(self):\n        \"\"\"Returns a ``timedelta`` object representing the whole number of minutes difference from\n        UTC time.\n\n        Usage::\n\n            >>> arrow.now('US/Pacific').utcoffset()\n            datetime.timedelta(-1, 57600)\n\n        \"\"\"\n\n        return self._datetime.utcoffset()\n\n    def dst(self):\n        \"\"\"Returns the daylight savings time adjustment.\n\n        Usage::\n\n            >>> arrow.utcnow().dst()\n            datetime.timedelta(0)\n\n        \"\"\"\n\n        return self._datetime.dst()\n\n    def timetuple(self):\n        \"\"\"Returns a ``time.struct_time``, in the current timezone.\n\n        Usage::\n\n            >>> arrow.utcnow().timetuple()\n            time.struct_time(tm_year=2019, tm_mon=1, tm_mday=20, tm_hour=15, tm_min=17, tm_sec=8, tm_wday=6, tm_yday=20, tm_isdst=0)\n\n        \"\"\"\n\n        return self._datetime.timetuple()\n\n    def utctimetuple(self):\n        \"\"\"Returns a ``time.struct_time``, in UTC time.\n\n        Usage::\n\n            >>> arrow.utcnow().utctimetuple()\n            time.struct_time(tm_year=2019, tm_mon=1, tm_mday=19, tm_hour=21, tm_min=41, tm_sec=7, tm_wday=5, tm_yday=19, tm_isdst=0)\n\n        \"\"\"\n\n        return self._datetime.utctimetuple()\n\n    def toordinal(self):\n        \"\"\"Returns the proleptic Gregorian ordinal of the date.\n\n        Usage::\n\n            >>> arrow.utcnow().toordinal()\n            737078\n\n        \"\"\"\n\n        return self._datetime.toordinal()\n\n    def weekday(self):\n        \"\"\"Returns the day of the week as an integer (0-6).\n\n        Usage::\n\n            >>> arrow.utcnow().weekday()\n            5\n\n        \"\"\"\n\n        return self._datetime.weekday()\n\n    def isoweekday(self):\n        \"\"\"Returns the ISO day of the week as an integer (1-7).\n\n        Usage::\n\n            >>> arrow.utcnow().isoweekday()\n            6\n\n        \"\"\"\n\n        return self._datetime.isoweekday()\n\n    def isocalendar(self):\n        \"\"\"Returns a 3-tuple, (ISO year, ISO week number, ISO weekday).\n\n        Usage::\n\n            >>> arrow.utcnow().isocalendar()\n            (2019, 3, 6)\n\n        \"\"\"\n\n        return self._datetime.isocalendar()\n\n    def isoformat(self, sep=\"T\"):\n        \"\"\"Returns an ISO 8601 formatted representation of the date and time.\n\n        Usage::\n\n            >>> arrow.utcnow().isoformat()\n            '2019-01-19T18:30:52.442118+00:00'\n\n        \"\"\"\n\n        return self._datetime.isoformat(sep)\n\n    def ctime(self):\n        \"\"\"Returns a ctime formatted representation of the date and time.\n\n        Usage::\n\n            >>> arrow.utcnow().ctime()\n            'Sat Jan 19 18:26:50 2019'\n\n        \"\"\"\n\n        return self._datetime.ctime()\n\n    def strftime(self, format):\n        \"\"\"Formats in the style of ``datetime.strftime``.\n\n        :param format: the format string.\n\n        Usage::\n\n            >>> arrow.utcnow().strftime('%d-%m-%Y %H:%M:%S')\n            '23-01-2019 12:28:17'\n\n        \"\"\"\n\n        return self._datetime.strftime(format)\n\n    def for_json(self):\n        \"\"\"Serializes for the ``for_json`` protocol of simplejson.\n\n        Usage::\n\n            >>> arrow.utcnow().for_json()\n            '2019-01-19T18:25:36.760079+00:00'\n\n        \"\"\"\n\n        return self.isoformat()\n\n    # math\n\n    def __add__(self, other):\n\n        if isinstance(other, (timedelta, relativedelta)):\n            return self.fromdatetime(self._datetime + other, self._datetime.tzinfo)\n\n        return NotImplemented\n\n    def __radd__(self, other):\n        return self.__add__(other)\n\n    def __sub__(self, other):\n\n        if isinstance(other, (timedelta, relativedelta)):\n            return self.fromdatetime(self._datetime - other, self._datetime.tzinfo)\n\n        elif isinstance(other, datetime):\n            return self._datetime - other\n\n        elif isinstance(other, Arrow):\n            return self._datetime - other._datetime\n\n        return NotImplemented\n\n    def __rsub__(self, other):\n\n        if isinstance(other, datetime):\n            return other - self._datetime\n\n        return NotImplemented\n\n    # comparisons\n\n    def __eq__(self, other):\n\n        if not isinstance(other, (Arrow, datetime)):\n            return False\n\n        return self._datetime == self._get_datetime(other)\n\n    def __ne__(self, other):\n\n        if not isinstance(other, (Arrow, datetime)):\n            return True\n\n        return not self.__eq__(other)\n\n    def __gt__(self, other):\n\n        if not isinstance(other, (Arrow, datetime)):\n            return NotImplemented\n\n        return self._datetime > self._get_datetime(other)\n\n    def __ge__(self, other):\n\n        if not isinstance(other, (Arrow, datetime)):\n            return NotImplemented\n\n        return self._datetime >= self._get_datetime(other)\n\n    def __lt__(self, other):\n\n        if not isinstance(other, (Arrow, datetime)):\n            return NotImplemented\n\n        return self._datetime < self._get_datetime(other)\n\n    def __le__(self, other):\n\n        if not isinstance(other, (Arrow, datetime)):\n            return NotImplemented\n\n        return self._datetime <= self._get_datetime(other)\n\n    def __cmp__(self, other):\n        if sys.version_info[0] < 3:  # pragma: no cover\n            if not isinstance(other, (Arrow, datetime)):\n                raise TypeError(\n                    \"can't compare '{}' to '{}'\".format(type(self), type(other))\n                )\n\n    # internal methods\n\n    @staticmethod\n    def _get_tzinfo(tz_expr):\n\n        if tz_expr is None:\n            return dateutil_tz.tzutc()\n        if isinstance(tz_expr, dt_tzinfo):\n            return tz_expr\n        else:\n            try:\n                return parser.TzinfoParser.parse(tz_expr)\n            except parser.ParserError:\n                raise ValueError(\"'{}' not recognized as a timezone\".format(tz_expr))\n\n    @classmethod\n    def _get_datetime(cls, expr):\n        \"\"\"Get datetime object for a specified expression.\"\"\"\n        if isinstance(expr, Arrow):\n            return expr.datetime\n        elif isinstance(expr, datetime):\n            return expr\n        elif util.is_timestamp(expr):\n            timestamp = float(expr)\n            return cls.utcfromtimestamp(timestamp).datetime\n        else:\n            raise ValueError(\n                \"'{}' not recognized as a datetime or timestamp.\".format(expr)\n            )\n\n    @classmethod\n    def _get_frames(cls, name):\n\n        if name in cls._ATTRS:\n            return name, \"{}s\".format(name), 1\n        elif name[-1] == \"s\" and name[:-1] in cls._ATTRS:\n            return name[:-1], name, 1\n        elif name in [\"week\", \"weeks\"]:\n            return \"week\", \"weeks\", 1\n        elif name in [\"quarter\", \"quarters\"]:\n            return \"quarter\", \"months\", 3\n\n        supported = \", \".join(\n            [\n                \"year(s)\",\n                \"month(s)\",\n                \"day(s)\",\n                \"hour(s)\",\n                \"minute(s)\",\n                \"second(s)\",\n                \"microsecond(s)\",\n                \"week(s)\",\n                \"quarter(s)\",\n            ]\n        )\n        raise AttributeError(\n            \"range/span over frame {} not supported. Supported frames: {}\".format(\n                name, supported\n            )\n        )\n\n    @classmethod\n    def _get_iteration_params(cls, end, limit):\n\n        if end is None:\n\n            if limit is None:\n                raise ValueError(\"one of 'end' or 'limit' is required\")\n\n            return cls.max, limit\n\n        else:\n            if limit is None:\n                return end, sys.maxsize\n            return end, limit\n\n    @staticmethod\n    def _is_last_day_of_month(date):\n        return date.day == calendar.monthrange(date.year, date.month)[1]\n\n\nArrow.min = Arrow.fromdatetime(datetime.min)\nArrow.max = Arrow.fromdatetime(datetime.max)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/arrow/constants.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Output of time.mktime(datetime.max.timetuple()) on macOS\n# This value must be hardcoded for compatibility with Windows\n# Platform-independent max timestamps are hard to form\n# https://stackoverflow.com/q/46133223\nMAX_TIMESTAMP = 253402318799.0\nMAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000\nMAX_TIMESTAMP_US = MAX_TIMESTAMP * 1000000\n"
  },
  {
    "path": "openpype/vendor/python/python_2/arrow/factory.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nImplements the :class:`ArrowFactory <arrow.factory.ArrowFactory>` class,\nproviding factory methods for common :class:`Arrow <arrow.arrow.Arrow>`\nconstruction scenarios.\n\n\"\"\"\n\nfrom __future__ import absolute_import\n\nimport calendar\nfrom datetime import date, datetime\nfrom datetime import tzinfo as dt_tzinfo\nfrom time import struct_time\n\nfrom dateutil import tz as dateutil_tz\n\nfrom arrow import parser\nfrom arrow.arrow import Arrow\nfrom arrow.util import is_timestamp, iso_to_gregorian, isstr\n\n\nclass ArrowFactory(object):\n    \"\"\"A factory for generating :class:`Arrow <arrow.arrow.Arrow>` objects.\n\n    :param type: (optional) the :class:`Arrow <arrow.arrow.Arrow>`-based class to construct from.\n        Defaults to :class:`Arrow <arrow.arrow.Arrow>`.\n\n    \"\"\"\n\n    def __init__(self, type=Arrow):\n        self.type = type\n\n    def get(self, *args, **kwargs):\n        \"\"\"Returns an :class:`Arrow <arrow.arrow.Arrow>` object based on flexible inputs.\n\n        :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to 'en_us'.\n        :param tzinfo: (optional) a :ref:`timezone expression <tz-expr>` or tzinfo object.\n            Replaces the timezone unless using an input form that is explicitly UTC or specifies\n            the timezone in a positional argument. Defaults to UTC.\n        :param normalize_whitespace: (optional) a ``bool`` specifying whether or not to normalize\n            redundant whitespace (spaces, tabs, and newlines) in a datetime string before parsing.\n            Defaults to false.\n\n        Usage::\n\n            >>> import arrow\n\n        **No inputs** to get current UTC time::\n\n            >>> arrow.get()\n            <Arrow [2013-05-08T05:51:43.316458+00:00]>\n\n        **None** to also get current UTC time::\n\n            >>> arrow.get(None)\n            <Arrow [2013-05-08T05:51:49.016458+00:00]>\n\n        **One** :class:`Arrow <arrow.arrow.Arrow>` object, to get a copy.\n\n            >>> arw = arrow.utcnow()\n            >>> arrow.get(arw)\n            <Arrow [2013-10-23T15:21:54.354846+00:00]>\n\n        **One** ``float`` or ``int``, convertible to a floating-point timestamp, to get\n        that timestamp in UTC::\n\n            >>> arrow.get(1367992474.293378)\n            <Arrow [2013-05-08T05:54:34.293378+00:00]>\n\n            >>> arrow.get(1367992474)\n            <Arrow [2013-05-08T05:54:34+00:00]>\n\n        **One** ISO 8601-formatted ``str``, to parse it::\n\n            >>> arrow.get('2013-09-29T01:26:43.830580')\n            <Arrow [2013-09-29T01:26:43.830580+00:00]>\n\n        **One** ISO 8601-formatted ``str``, in basic format, to parse it::\n\n            >>> arrow.get('20160413T133656.456289')\n            <Arrow [2016-04-13T13:36:56.456289+00:00]>\n\n        **One** ``tzinfo``, to get the current time **converted** to that timezone::\n\n            >>> arrow.get(tz.tzlocal())\n            <Arrow [2013-05-07T22:57:28.484717-07:00]>\n\n        **One** naive ``datetime``, to get that datetime in UTC::\n\n            >>> arrow.get(datetime(2013, 5, 5))\n            <Arrow [2013-05-05T00:00:00+00:00]>\n\n        **One** aware ``datetime``, to get that datetime::\n\n            >>> arrow.get(datetime(2013, 5, 5, tzinfo=tz.tzlocal()))\n            <Arrow [2013-05-05T00:00:00-07:00]>\n\n        **One** naive ``date``, to get that date in UTC::\n\n            >>> arrow.get(date(2013, 5, 5))\n            <Arrow [2013-05-05T00:00:00+00:00]>\n\n        **One** time.struct time::\n\n            >>> arrow.get(gmtime(0))\n            <Arrow [1970-01-01T00:00:00+00:00]>\n\n        **One** iso calendar ``tuple``, to get that week date in UTC::\n\n            >>> arrow.get((2013, 18, 7))\n            <Arrow [2013-05-05T00:00:00+00:00]>\n\n        **Two** arguments, a naive or aware ``datetime``, and a replacement\n        :ref:`timezone expression <tz-expr>`::\n\n            >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific')\n            <Arrow [2013-05-05T00:00:00-07:00]>\n\n        **Two** arguments, a naive ``date``, and a replacement\n        :ref:`timezone expression <tz-expr>`::\n\n            >>> arrow.get(date(2013, 5, 5), 'US/Pacific')\n            <Arrow [2013-05-05T00:00:00-07:00]>\n\n        **Two** arguments, both ``str``, to parse the first according to the format of the second::\n\n            >>> arrow.get('2013-05-05 12:30:45 America/Chicago', 'YYYY-MM-DD HH:mm:ss ZZZ')\n            <Arrow [2013-05-05T12:30:45-05:00]>\n\n        **Two** arguments, first a ``str`` to parse and second a ``list`` of formats to try::\n\n            >>> arrow.get('2013-05-05 12:30:45', ['MM/DD/YYYY', 'YYYY-MM-DD HH:mm:ss'])\n            <Arrow [2013-05-05T12:30:45+00:00]>\n\n        **Three or more** arguments, as for the constructor of a ``datetime``::\n\n            >>> arrow.get(2013, 5, 5, 12, 30, 45)\n            <Arrow [2013-05-05T12:30:45+00:00]>\n\n        \"\"\"\n\n        arg_count = len(args)\n        locale = kwargs.pop(\"locale\", \"en_us\")\n        tz = kwargs.get(\"tzinfo\", None)\n        normalize_whitespace = kwargs.pop(\"normalize_whitespace\", False)\n\n        # if kwargs given, send to constructor unless only tzinfo provided\n        if len(kwargs) > 1:\n            arg_count = 3\n\n        # tzinfo kwarg is not provided\n        if len(kwargs) == 1 and tz is None:\n            arg_count = 3\n\n        # () -> now, @ utc.\n        if arg_count == 0:\n            if isstr(tz):\n                tz = parser.TzinfoParser.parse(tz)\n                return self.type.now(tz)\n\n            if isinstance(tz, dt_tzinfo):\n                return self.type.now(tz)\n\n            return self.type.utcnow()\n\n        if arg_count == 1:\n            arg = args[0]\n\n            # (None) -> now, @ utc.\n            if arg is None:\n                return self.type.utcnow()\n\n            # try (int, float) -> from timestamp with tz\n            elif not isstr(arg) and is_timestamp(arg):\n                if tz is None:\n                    # set to UTC by default\n                    tz = dateutil_tz.tzutc()\n                return self.type.fromtimestamp(arg, tzinfo=tz)\n\n            # (Arrow) -> from the object's datetime.\n            elif isinstance(arg, Arrow):\n                return self.type.fromdatetime(arg.datetime)\n\n            # (datetime) -> from datetime.\n            elif isinstance(arg, datetime):\n                return self.type.fromdatetime(arg)\n\n            # (date) -> from date.\n            elif isinstance(arg, date):\n                return self.type.fromdate(arg)\n\n            # (tzinfo) -> now, @ tzinfo.\n            elif isinstance(arg, dt_tzinfo):\n                return self.type.now(arg)\n\n            # (str) -> parse.\n            elif isstr(arg):\n                dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace)\n                return self.type.fromdatetime(dt, tz)\n\n            # (struct_time) -> from struct_time\n            elif isinstance(arg, struct_time):\n                return self.type.utcfromtimestamp(calendar.timegm(arg))\n\n            # (iso calendar) -> convert then from date\n            elif isinstance(arg, tuple) and len(arg) == 3:\n                dt = iso_to_gregorian(*arg)\n                return self.type.fromdate(dt)\n\n            else:\n                raise TypeError(\n                    \"Can't parse single argument of type '{}'\".format(type(arg))\n                )\n\n        elif arg_count == 2:\n\n            arg_1, arg_2 = args[0], args[1]\n\n            if isinstance(arg_1, datetime):\n\n                # (datetime, tzinfo/str) -> fromdatetime replace tzinfo.\n                if isinstance(arg_2, dt_tzinfo) or isstr(arg_2):\n                    return self.type.fromdatetime(arg_1, arg_2)\n                else:\n                    raise TypeError(\n                        \"Can't parse two arguments of types 'datetime', '{}'\".format(\n                            type(arg_2)\n                        )\n                    )\n\n            elif isinstance(arg_1, date):\n\n                # (date, tzinfo/str) -> fromdate replace tzinfo.\n                if isinstance(arg_2, dt_tzinfo) or isstr(arg_2):\n                    return self.type.fromdate(arg_1, tzinfo=arg_2)\n                else:\n                    raise TypeError(\n                        \"Can't parse two arguments of types 'date', '{}'\".format(\n                            type(arg_2)\n                        )\n                    )\n\n            # (str, format) -> parse.\n            elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)):\n                dt = parser.DateTimeParser(locale).parse(\n                    args[0], args[1], normalize_whitespace\n                )\n                return self.type.fromdatetime(dt, tzinfo=tz)\n\n            else:\n                raise TypeError(\n                    \"Can't parse two arguments of types '{}' and '{}'\".format(\n                        type(arg_1), type(arg_2)\n                    )\n                )\n\n        # 3+ args -> datetime-like via constructor.\n        else:\n            return self.type(*args, **kwargs)\n\n    def utcnow(self):\n        \"\"\"Returns an :class:`Arrow <arrow.arrow.Arrow>` object, representing \"now\" in UTC time.\n\n        Usage::\n\n            >>> import arrow\n            >>> arrow.utcnow()\n            <Arrow [2013-05-08T05:19:07.018993+00:00]>\n        \"\"\"\n\n        return self.type.utcnow()\n\n    def now(self, tz=None):\n        \"\"\"Returns an :class:`Arrow <arrow.arrow.Arrow>` object, representing \"now\" in the given\n        timezone.\n\n        :param tz: (optional) A :ref:`timezone expression <tz-expr>`.  Defaults to local time.\n\n        Usage::\n\n            >>> import arrow\n            >>> arrow.now()\n            <Arrow [2013-05-07T22:19:11.363410-07:00]>\n\n            >>> arrow.now('US/Pacific')\n            <Arrow [2013-05-07T22:19:15.251821-07:00]>\n\n            >>> arrow.now('+02:00')\n            <Arrow [2013-05-08T07:19:25.618646+02:00]>\n\n            >>> arrow.now('local')\n            <Arrow [2013-05-07T22:19:39.130059-07:00]>\n        \"\"\"\n\n        if tz is None:\n            tz = dateutil_tz.tzlocal()\n        elif not isinstance(tz, dt_tzinfo):\n            tz = parser.TzinfoParser.parse(tz)\n\n        return self.type.now(tz)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/arrow/formatter.py",
    "content": "# -*- coding: utf-8 -*-\nfrom __future__ import absolute_import, division\n\nimport calendar\nimport re\n\nfrom dateutil import tz as dateutil_tz\n\nfrom arrow import locales, util\n\nFORMAT_ATOM = \"YYYY-MM-DD HH:mm:ssZZ\"\nFORMAT_COOKIE = \"dddd, DD-MMM-YYYY HH:mm:ss ZZZ\"\nFORMAT_RFC822 = \"ddd, DD MMM YY HH:mm:ss Z\"\nFORMAT_RFC850 = \"dddd, DD-MMM-YY HH:mm:ss ZZZ\"\nFORMAT_RFC1036 = \"ddd, DD MMM YY HH:mm:ss Z\"\nFORMAT_RFC1123 = \"ddd, DD MMM YYYY HH:mm:ss Z\"\nFORMAT_RFC2822 = \"ddd, DD MMM YYYY HH:mm:ss Z\"\nFORMAT_RFC3339 = \"YYYY-MM-DD HH:mm:ssZZ\"\nFORMAT_RSS = \"ddd, DD MMM YYYY HH:mm:ss Z\"\nFORMAT_W3C = \"YYYY-MM-DD HH:mm:ssZZ\"\n\n\nclass DateTimeFormatter(object):\n\n    # This pattern matches characters enclosed in square brackets are matched as\n    # an atomic group. For more info on atomic groups and how to they are\n    # emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578\n\n    _FORMAT_RE = re.compile(\n        r\"(\\[(?:(?=(?P<literal>[^]]))(?P=literal))*\\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X|x|W)\"\n    )\n\n    def __init__(self, locale=\"en_us\"):\n\n        self.locale = locales.get_locale(locale)\n\n    def format(cls, dt, fmt):\n\n        return cls._FORMAT_RE.sub(lambda m: cls._format_token(dt, m.group(0)), fmt)\n\n    def _format_token(self, dt, token):\n\n        if token and token.startswith(\"[\") and token.endswith(\"]\"):\n            return token[1:-1]\n\n        if token == \"YYYY\":\n            return self.locale.year_full(dt.year)\n        if token == \"YY\":\n            return self.locale.year_abbreviation(dt.year)\n\n        if token == \"MMMM\":\n            return self.locale.month_name(dt.month)\n        if token == \"MMM\":\n            return self.locale.month_abbreviation(dt.month)\n        if token == \"MM\":\n            return \"{:02d}\".format(dt.month)\n        if token == \"M\":\n            return str(dt.month)\n\n        if token == \"DDDD\":\n            return \"{:03d}\".format(dt.timetuple().tm_yday)\n        if token == \"DDD\":\n            return str(dt.timetuple().tm_yday)\n        if token == \"DD\":\n            return \"{:02d}\".format(dt.day)\n        if token == \"D\":\n            return str(dt.day)\n\n        if token == \"Do\":\n            return self.locale.ordinal_number(dt.day)\n\n        if token == \"dddd\":\n            return self.locale.day_name(dt.isoweekday())\n        if token == \"ddd\":\n            return self.locale.day_abbreviation(dt.isoweekday())\n        if token == \"d\":\n            return str(dt.isoweekday())\n\n        if token == \"HH\":\n            return \"{:02d}\".format(dt.hour)\n        if token == \"H\":\n            return str(dt.hour)\n        if token == \"hh\":\n            return \"{:02d}\".format(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12))\n        if token == \"h\":\n            return str(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12))\n\n        if token == \"mm\":\n            return \"{:02d}\".format(dt.minute)\n        if token == \"m\":\n            return str(dt.minute)\n\n        if token == \"ss\":\n            return \"{:02d}\".format(dt.second)\n        if token == \"s\":\n            return str(dt.second)\n\n        if token == \"SSSSSS\":\n            return str(\"{:06d}\".format(int(dt.microsecond)))\n        if token == \"SSSSS\":\n            return str(\"{:05d}\".format(int(dt.microsecond / 10)))\n        if token == \"SSSS\":\n            return str(\"{:04d}\".format(int(dt.microsecond / 100)))\n        if token == \"SSS\":\n            return str(\"{:03d}\".format(int(dt.microsecond / 1000)))\n        if token == \"SS\":\n            return str(\"{:02d}\".format(int(dt.microsecond / 10000)))\n        if token == \"S\":\n            return str(int(dt.microsecond / 100000))\n\n        if token == \"X\":\n            # TODO: replace with a call to dt.timestamp() when we drop Python 2.7\n            return str(calendar.timegm(dt.utctimetuple()))\n\n        if token == \"x\":\n            # TODO: replace with a call to dt.timestamp() when we drop Python 2.7\n            ts = calendar.timegm(dt.utctimetuple()) + (dt.microsecond / 1000000)\n            return str(int(ts * 1000000))\n\n        if token == \"ZZZ\":\n            return dt.tzname()\n\n        if token in [\"ZZ\", \"Z\"]:\n            separator = \":\" if token == \"ZZ\" else \"\"\n            tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo\n            total_minutes = int(util.total_seconds(tz.utcoffset(dt)) / 60)\n\n            sign = \"+\" if total_minutes >= 0 else \"-\"\n            total_minutes = abs(total_minutes)\n            hour, minute = divmod(total_minutes, 60)\n\n            return \"{}{:02d}{}{:02d}\".format(sign, hour, separator, minute)\n\n        if token in (\"a\", \"A\"):\n            return self.locale.meridian(dt.hour, token)\n\n        if token == \"W\":\n            year, week, day = dt.isocalendar()\n            return \"{}-W{:02d}-{}\".format(year, week, day)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/arrow/locales.py",
    "content": "# -*- coding: utf-8 -*-\nfrom __future__ import absolute_import, unicode_literals\n\nimport inspect\nimport sys\nfrom math import trunc\n\n\ndef get_locale(name):\n    \"\"\"Returns an appropriate :class:`Locale <arrow.locales.Locale>`\n    corresponding to an inpute locale name.\n\n    :param name: the name of the locale.\n\n    \"\"\"\n\n    locale_cls = _locales.get(name.lower())\n\n    if locale_cls is None:\n        raise ValueError(\"Unsupported locale '{}'\".format(name))\n\n    return locale_cls()\n\n\ndef get_locale_by_class_name(name):\n    \"\"\"Returns an appropriate :class:`Locale <arrow.locales.Locale>`\n    corresponding to an locale class name.\n\n    :param name: the name of the locale class.\n\n    \"\"\"\n    locale_cls = globals().get(name)\n\n    if locale_cls is None:\n        raise ValueError(\"Unsupported locale '{}'\".format(name))\n\n    return locale_cls()\n\n\n# base locale type.\n\n\nclass Locale(object):\n    \"\"\" Represents locale-specific data and functionality. \"\"\"\n\n    names = []\n\n    timeframes = {\n        \"now\": \"\",\n        \"second\": \"\",\n        \"seconds\": \"\",\n        \"minute\": \"\",\n        \"minutes\": \"\",\n        \"hour\": \"\",\n        \"hours\": \"\",\n        \"day\": \"\",\n        \"days\": \"\",\n        \"week\": \"\",\n        \"weeks\": \"\",\n        \"month\": \"\",\n        \"months\": \"\",\n        \"year\": \"\",\n        \"years\": \"\",\n    }\n\n    meridians = {\"am\": \"\", \"pm\": \"\", \"AM\": \"\", \"PM\": \"\"}\n\n    past = None\n    future = None\n    and_word = None\n\n    month_names = []\n    month_abbreviations = []\n\n    day_names = []\n    day_abbreviations = []\n\n    ordinal_day_re = r\"(\\d+)\"\n\n    def __init__(self):\n\n        self._month_name_to_ordinal = None\n\n    def describe(self, timeframe, delta=0, only_distance=False):\n        \"\"\"Describes a delta within a timeframe in plain language.\n\n        :param timeframe: a string representing a timeframe.\n        :param delta: a quantity representing a delta in a timeframe.\n        :param only_distance: return only distance eg: \"11 seconds\" without \"in\" or \"ago\" keywords\n        \"\"\"\n\n        humanized = self._format_timeframe(timeframe, delta)\n        if not only_distance:\n            humanized = self._format_relative(humanized, timeframe, delta)\n\n        return humanized\n\n    def describe_multi(self, timeframes, only_distance=False):\n        \"\"\"Describes a delta within multiple timeframes in plain language.\n\n        :param timeframes: a list of string, quantity pairs each representing a timeframe and delta.\n        :param only_distance: return only distance eg: \"2 hours and 11 seconds\" without \"in\" or \"ago\" keywords\n        \"\"\"\n\n        humanized = \"\"\n        for index, (timeframe, delta) in enumerate(timeframes):\n            humanized += self._format_timeframe(timeframe, delta)\n            if index == len(timeframes) - 2 and self.and_word:\n                humanized += \" \" + self.and_word + \" \"\n            elif index < len(timeframes) - 1:\n                humanized += \" \"\n\n        if not only_distance:\n            humanized = self._format_relative(humanized, timeframe, delta)\n\n        return humanized\n\n    def day_name(self, day):\n        \"\"\"Returns the day name for a specified day of the week.\n\n        :param day: the ``int`` day of the week (1-7).\n\n        \"\"\"\n\n        return self.day_names[day]\n\n    def day_abbreviation(self, day):\n        \"\"\"Returns the day abbreviation for a specified day of the week.\n\n        :param day: the ``int`` day of the week (1-7).\n\n        \"\"\"\n\n        return self.day_abbreviations[day]\n\n    def month_name(self, month):\n        \"\"\"Returns the month name for a specified month of the year.\n\n        :param month: the ``int`` month of the year (1-12).\n\n        \"\"\"\n\n        return self.month_names[month]\n\n    def month_abbreviation(self, month):\n        \"\"\"Returns the month abbreviation for a specified month of the year.\n\n        :param month: the ``int`` month of the year (1-12).\n\n        \"\"\"\n\n        return self.month_abbreviations[month]\n\n    def month_number(self, name):\n        \"\"\"Returns the month number for a month specified by name or abbreviation.\n\n        :param name: the month name or abbreviation.\n\n        \"\"\"\n\n        if self._month_name_to_ordinal is None:\n            self._month_name_to_ordinal = self._name_to_ordinal(self.month_names)\n            self._month_name_to_ordinal.update(\n                self._name_to_ordinal(self.month_abbreviations)\n            )\n\n        return self._month_name_to_ordinal.get(name)\n\n    def year_full(self, year):\n        \"\"\"Returns the year for specific locale if available\n\n        :param name: the ``int`` year (4-digit)\n        \"\"\"\n        return \"{:04d}\".format(year)\n\n    def year_abbreviation(self, year):\n        \"\"\"Returns the year for specific locale if available\n\n        :param name: the ``int`` year (4-digit)\n        \"\"\"\n        return \"{:04d}\".format(year)[2:]\n\n    def meridian(self, hour, token):\n        \"\"\"Returns the meridian indicator for a specified hour and format token.\n\n        :param hour: the ``int`` hour of the day.\n        :param token: the format token.\n        \"\"\"\n\n        if token == \"a\":\n            return self.meridians[\"am\"] if hour < 12 else self.meridians[\"pm\"]\n        if token == \"A\":\n            return self.meridians[\"AM\"] if hour < 12 else self.meridians[\"PM\"]\n\n    def ordinal_number(self, n):\n        \"\"\"Returns the ordinal format of a given integer\n\n        :param n: an integer\n        \"\"\"\n        return self._ordinal_number(n)\n\n    def _ordinal_number(self, n):\n        return \"{}\".format(n)\n\n    def _name_to_ordinal(self, lst):\n        return dict(map(lambda i: (i[1].lower(), i[0] + 1), enumerate(lst[1:])))\n\n    def _format_timeframe(self, timeframe, delta):\n        return self.timeframes[timeframe].format(trunc(abs(delta)))\n\n    def _format_relative(self, humanized, timeframe, delta):\n\n        if timeframe == \"now\":\n            return humanized\n\n        direction = self.past if delta < 0 else self.future\n\n        return direction.format(humanized)\n\n\n# base locale type implementations.\n\n\nclass EnglishLocale(Locale):\n\n    names = [\n        \"en\",\n        \"en_us\",\n        \"en_gb\",\n        \"en_au\",\n        \"en_be\",\n        \"en_jp\",\n        \"en_za\",\n        \"en_ca\",\n        \"en_ph\",\n    ]\n\n    past = \"{0} ago\"\n    future = \"in {0}\"\n    and_word = \"and\"\n\n    timeframes = {\n        \"now\": \"just now\",\n        \"second\": \"a second\",\n        \"seconds\": \"{0} seconds\",\n        \"minute\": \"a minute\",\n        \"minutes\": \"{0} minutes\",\n        \"hour\": \"an hour\",\n        \"hours\": \"{0} hours\",\n        \"day\": \"a day\",\n        \"days\": \"{0} days\",\n        \"week\": \"a week\",\n        \"weeks\": \"{0} weeks\",\n        \"month\": \"a month\",\n        \"months\": \"{0} months\",\n        \"year\": \"a year\",\n        \"years\": \"{0} years\",\n    }\n\n    meridians = {\"am\": \"am\", \"pm\": \"pm\", \"AM\": \"AM\", \"PM\": \"PM\"}\n\n    month_names = [\n        \"\",\n        \"January\",\n        \"February\",\n        \"March\",\n        \"April\",\n        \"May\",\n        \"June\",\n        \"July\",\n        \"August\",\n        \"September\",\n        \"October\",\n        \"November\",\n        \"December\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"Jan\",\n        \"Feb\",\n        \"Mar\",\n        \"Apr\",\n        \"May\",\n        \"Jun\",\n        \"Jul\",\n        \"Aug\",\n        \"Sep\",\n        \"Oct\",\n        \"Nov\",\n        \"Dec\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Monday\",\n        \"Tuesday\",\n        \"Wednesday\",\n        \"Thursday\",\n        \"Friday\",\n        \"Saturday\",\n        \"Sunday\",\n    ]\n    day_abbreviations = [\"\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\n\n    ordinal_day_re = r\"((?P<value>[2-3]?1(?=st)|[2-3]?2(?=nd)|[2-3]?3(?=rd)|[1-3]?[04-9](?=th)|1[1-3](?=th))(st|nd|rd|th))\"\n\n    def _ordinal_number(self, n):\n        if n % 100 not in (11, 12, 13):\n            remainder = abs(n) % 10\n            if remainder == 1:\n                return \"{}st\".format(n)\n            elif remainder == 2:\n                return \"{}nd\".format(n)\n            elif remainder == 3:\n                return \"{}rd\".format(n)\n        return \"{}th\".format(n)\n\n    def describe(self, timeframe, delta=0, only_distance=False):\n        \"\"\"Describes a delta within a timeframe in plain language.\n\n        :param timeframe: a string representing a timeframe.\n        :param delta: a quantity representing a delta in a timeframe.\n        :param only_distance: return only distance eg: \"11 seconds\" without \"in\" or \"ago\" keywords\n        \"\"\"\n\n        humanized = super(EnglishLocale, self).describe(timeframe, delta, only_distance)\n        if only_distance and timeframe == \"now\":\n            humanized = \"instantly\"\n\n        return humanized\n\n\nclass ItalianLocale(Locale):\n    names = [\"it\", \"it_it\"]\n    past = \"{0} fa\"\n    future = \"tra {0}\"\n    and_word = \"e\"\n\n    timeframes = {\n        \"now\": \"adesso\",\n        \"second\": \"un secondo\",\n        \"seconds\": \"{0} qualche secondo\",\n        \"minute\": \"un minuto\",\n        \"minutes\": \"{0} minuti\",\n        \"hour\": \"un'ora\",\n        \"hours\": \"{0} ore\",\n        \"day\": \"un giorno\",\n        \"days\": \"{0} giorni\",\n        \"week\": \"una settimana,\",\n        \"weeks\": \"{0} settimane\",\n        \"month\": \"un mese\",\n        \"months\": \"{0} mesi\",\n        \"year\": \"un anno\",\n        \"years\": \"{0} anni\",\n    }\n\n    month_names = [\n        \"\",\n        \"gennaio\",\n        \"febbraio\",\n        \"marzo\",\n        \"aprile\",\n        \"maggio\",\n        \"giugno\",\n        \"luglio\",\n        \"agosto\",\n        \"settembre\",\n        \"ottobre\",\n        \"novembre\",\n        \"dicembre\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"gen\",\n        \"feb\",\n        \"mar\",\n        \"apr\",\n        \"mag\",\n        \"giu\",\n        \"lug\",\n        \"ago\",\n        \"set\",\n        \"ott\",\n        \"nov\",\n        \"dic\",\n    ]\n\n    day_names = [\n        \"\",\n        \"lunedì\",\n        \"martedì\",\n        \"mercoledì\",\n        \"giovedì\",\n        \"venerdì\",\n        \"sabato\",\n        \"domenica\",\n    ]\n    day_abbreviations = [\"\", \"lun\", \"mar\", \"mer\", \"gio\", \"ven\", \"sab\", \"dom\"]\n\n    ordinal_day_re = r\"((?P<value>[1-3]?[0-9](?=[ºª]))[ºª])\"\n\n    def _ordinal_number(self, n):\n        return \"{}º\".format(n)\n\n\nclass SpanishLocale(Locale):\n    names = [\"es\", \"es_es\"]\n    past = \"hace {0}\"\n    future = \"en {0}\"\n    and_word = \"y\"\n\n    timeframes = {\n        \"now\": \"ahora\",\n        \"second\": \"un segundo\",\n        \"seconds\": \"{0} segundos\",\n        \"minute\": \"un minuto\",\n        \"minutes\": \"{0} minutos\",\n        \"hour\": \"una hora\",\n        \"hours\": \"{0} horas\",\n        \"day\": \"un día\",\n        \"days\": \"{0} días\",\n        \"week\": \"una semana\",\n        \"weeks\": \"{0} semanas\",\n        \"month\": \"un mes\",\n        \"months\": \"{0} meses\",\n        \"year\": \"un año\",\n        \"years\": \"{0} años\",\n    }\n\n    meridians = {\"am\": \"am\", \"pm\": \"pm\", \"AM\": \"AM\", \"PM\": \"PM\"}\n\n    month_names = [\n        \"\",\n        \"enero\",\n        \"febrero\",\n        \"marzo\",\n        \"abril\",\n        \"mayo\",\n        \"junio\",\n        \"julio\",\n        \"agosto\",\n        \"septiembre\",\n        \"octubre\",\n        \"noviembre\",\n        \"diciembre\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"ene\",\n        \"feb\",\n        \"mar\",\n        \"abr\",\n        \"may\",\n        \"jun\",\n        \"jul\",\n        \"ago\",\n        \"sep\",\n        \"oct\",\n        \"nov\",\n        \"dic\",\n    ]\n\n    day_names = [\n        \"\",\n        \"lunes\",\n        \"martes\",\n        \"miércoles\",\n        \"jueves\",\n        \"viernes\",\n        \"sábado\",\n        \"domingo\",\n    ]\n    day_abbreviations = [\"\", \"lun\", \"mar\", \"mie\", \"jue\", \"vie\", \"sab\", \"dom\"]\n\n    ordinal_day_re = r\"((?P<value>[1-3]?[0-9](?=[ºª]))[ºª])\"\n\n    def _ordinal_number(self, n):\n        return \"{}º\".format(n)\n\n\nclass FrenchBaseLocale(Locale):\n\n    past = \"il y a {0}\"\n    future = \"dans {0}\"\n    and_word = \"et\"\n\n    timeframes = {\n        \"now\": \"maintenant\",\n        \"second\": \"une seconde\",\n        \"seconds\": \"{0} quelques secondes\",\n        \"minute\": \"une minute\",\n        \"minutes\": \"{0} minutes\",\n        \"hour\": \"une heure\",\n        \"hours\": \"{0} heures\",\n        \"day\": \"un jour\",\n        \"days\": \"{0} jours\",\n        \"week\": \"une semaine\",\n        \"weeks\": \"{0} semaines\",\n        \"month\": \"un mois\",\n        \"months\": \"{0} mois\",\n        \"year\": \"un an\",\n        \"years\": \"{0} ans\",\n    }\n\n    month_names = [\n        \"\",\n        \"janvier\",\n        \"février\",\n        \"mars\",\n        \"avril\",\n        \"mai\",\n        \"juin\",\n        \"juillet\",\n        \"août\",\n        \"septembre\",\n        \"octobre\",\n        \"novembre\",\n        \"décembre\",\n    ]\n\n    day_names = [\n        \"\",\n        \"lundi\",\n        \"mardi\",\n        \"mercredi\",\n        \"jeudi\",\n        \"vendredi\",\n        \"samedi\",\n        \"dimanche\",\n    ]\n    day_abbreviations = [\"\", \"lun\", \"mar\", \"mer\", \"jeu\", \"ven\", \"sam\", \"dim\"]\n\n    ordinal_day_re = (\n        r\"((?P<value>\\b1(?=er\\b)|[1-3]?[02-9](?=e\\b)|[1-3]1(?=e\\b))(er|e)\\b)\"\n    )\n\n    def _ordinal_number(self, n):\n        if abs(n) == 1:\n            return \"{}er\".format(n)\n        return \"{}e\".format(n)\n\n\nclass FrenchLocale(FrenchBaseLocale, Locale):\n\n    names = [\"fr\", \"fr_fr\"]\n\n    month_abbreviations = [\n        \"\",\n        \"janv\",\n        \"févr\",\n        \"mars\",\n        \"avr\",\n        \"mai\",\n        \"juin\",\n        \"juil\",\n        \"août\",\n        \"sept\",\n        \"oct\",\n        \"nov\",\n        \"déc\",\n    ]\n\n\nclass FrenchCanadianLocale(FrenchBaseLocale, Locale):\n\n    names = [\"fr_ca\"]\n\n    month_abbreviations = [\n        \"\",\n        \"janv\",\n        \"févr\",\n        \"mars\",\n        \"avr\",\n        \"mai\",\n        \"juin\",\n        \"juill\",\n        \"août\",\n        \"sept\",\n        \"oct\",\n        \"nov\",\n        \"déc\",\n    ]\n\n\nclass GreekLocale(Locale):\n\n    names = [\"el\", \"el_gr\"]\n\n    past = \"{0} πριν\"\n    future = \"σε {0}\"\n    and_word = \"και\"\n\n    timeframes = {\n        \"now\": \"τώρα\",\n        \"second\": \"ένα δεύτερο\",\n        \"seconds\": \"{0} δευτερόλεπτα\",\n        \"minute\": \"ένα λεπτό\",\n        \"minutes\": \"{0} λεπτά\",\n        \"hour\": \"μία ώρα\",\n        \"hours\": \"{0} ώρες\",\n        \"day\": \"μία μέρα\",\n        \"days\": \"{0} μέρες\",\n        \"month\": \"ένα μήνα\",\n        \"months\": \"{0} μήνες\",\n        \"year\": \"ένα χρόνο\",\n        \"years\": \"{0} χρόνια\",\n    }\n\n    month_names = [\n        \"\",\n        \"Ιανουαρίου\",\n        \"Φεβρουαρίου\",\n        \"Μαρτίου\",\n        \"Απριλίου\",\n        \"Μαΐου\",\n        \"Ιουνίου\",\n        \"Ιουλίου\",\n        \"Αυγούστου\",\n        \"Σεπτεμβρίου\",\n        \"Οκτωβρίου\",\n        \"Νοεμβρίου\",\n        \"Δεκεμβρίου\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"Ιαν\",\n        \"Φεβ\",\n        \"Μαρ\",\n        \"Απρ\",\n        \"Μαϊ\",\n        \"Ιον\",\n        \"Ιολ\",\n        \"Αυγ\",\n        \"Σεπ\",\n        \"Οκτ\",\n        \"Νοε\",\n        \"Δεκ\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Δευτέρα\",\n        \"Τρίτη\",\n        \"Τετάρτη\",\n        \"Πέμπτη\",\n        \"Παρασκευή\",\n        \"Σάββατο\",\n        \"Κυριακή\",\n    ]\n    day_abbreviations = [\"\", \"Δευ\", \"Τρι\", \"Τετ\", \"Πεμ\", \"Παρ\", \"Σαβ\", \"Κυρ\"]\n\n\nclass JapaneseLocale(Locale):\n\n    names = [\"ja\", \"ja_jp\"]\n\n    past = \"{0}前\"\n    future = \"{0}後\"\n\n    timeframes = {\n        \"now\": \"現在\",\n        \"second\": \"二番目の\",\n        \"seconds\": \"{0}数秒\",\n        \"minute\": \"1分\",\n        \"minutes\": \"{0}分\",\n        \"hour\": \"1時間\",\n        \"hours\": \"{0}時間\",\n        \"day\": \"1日\",\n        \"days\": \"{0}日\",\n        \"week\": \"1週間\",\n        \"weeks\": \"{0}週間\",\n        \"month\": \"1ヶ月\",\n        \"months\": \"{0}ヶ月\",\n        \"year\": \"1年\",\n        \"years\": \"{0}年\",\n    }\n\n    month_names = [\n        \"\",\n        \"1月\",\n        \"2月\",\n        \"3月\",\n        \"4月\",\n        \"5月\",\n        \"6月\",\n        \"7月\",\n        \"8月\",\n        \"9月\",\n        \"10月\",\n        \"11月\",\n        \"12月\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \" 1\",\n        \" 2\",\n        \" 3\",\n        \" 4\",\n        \" 5\",\n        \" 6\",\n        \" 7\",\n        \" 8\",\n        \" 9\",\n        \"10\",\n        \"11\",\n        \"12\",\n    ]\n\n    day_names = [\"\", \"月曜日\", \"火曜日\", \"水曜日\", \"木曜日\", \"金曜日\", \"土曜日\", \"日曜日\"]\n    day_abbreviations = [\"\", \"月\", \"火\", \"水\", \"木\", \"金\", \"土\", \"日\"]\n\n\nclass SwedishLocale(Locale):\n\n    names = [\"sv\", \"sv_se\"]\n\n    past = \"för {0} sen\"\n    future = \"om {0}\"\n    and_word = \"och\"\n\n    timeframes = {\n        \"now\": \"just nu\",\n        \"second\": \"en sekund\",\n        \"seconds\": \"{0} några sekunder\",\n        \"minute\": \"en minut\",\n        \"minutes\": \"{0} minuter\",\n        \"hour\": \"en timme\",\n        \"hours\": \"{0} timmar\",\n        \"day\": \"en dag\",\n        \"days\": \"{0} dagar\",\n        \"week\": \"en vecka\",\n        \"weeks\": \"{0} veckor\",\n        \"month\": \"en månad\",\n        \"months\": \"{0} månader\",\n        \"year\": \"ett år\",\n        \"years\": \"{0} år\",\n    }\n\n    month_names = [\n        \"\",\n        \"januari\",\n        \"februari\",\n        \"mars\",\n        \"april\",\n        \"maj\",\n        \"juni\",\n        \"juli\",\n        \"augusti\",\n        \"september\",\n        \"oktober\",\n        \"november\",\n        \"december\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"jan\",\n        \"feb\",\n        \"mar\",\n        \"apr\",\n        \"maj\",\n        \"jun\",\n        \"jul\",\n        \"aug\",\n        \"sep\",\n        \"okt\",\n        \"nov\",\n        \"dec\",\n    ]\n\n    day_names = [\n        \"\",\n        \"måndag\",\n        \"tisdag\",\n        \"onsdag\",\n        \"torsdag\",\n        \"fredag\",\n        \"lördag\",\n        \"söndag\",\n    ]\n    day_abbreviations = [\"\", \"mån\", \"tis\", \"ons\", \"tor\", \"fre\", \"lör\", \"sön\"]\n\n\nclass FinnishLocale(Locale):\n\n    names = [\"fi\", \"fi_fi\"]\n\n    # The finnish grammar is very complex, and its hard to convert\n    # 1-to-1 to something like English.\n\n    past = \"{0} sitten\"\n    future = \"{0} kuluttua\"\n\n    timeframes = {\n        \"now\": [\"juuri nyt\", \"juuri nyt\"],\n        \"second\": [\"sekunti\", \"sekunti\"],\n        \"seconds\": [\"{0} muutama sekunti\", \"{0} muutaman sekunnin\"],\n        \"minute\": [\"minuutti\", \"minuutin\"],\n        \"minutes\": [\"{0} minuuttia\", \"{0} minuutin\"],\n        \"hour\": [\"tunti\", \"tunnin\"],\n        \"hours\": [\"{0} tuntia\", \"{0} tunnin\"],\n        \"day\": [\"päivä\", \"päivä\"],\n        \"days\": [\"{0} päivää\", \"{0} päivän\"],\n        \"month\": [\"kuukausi\", \"kuukauden\"],\n        \"months\": [\"{0} kuukautta\", \"{0} kuukauden\"],\n        \"year\": [\"vuosi\", \"vuoden\"],\n        \"years\": [\"{0} vuotta\", \"{0} vuoden\"],\n    }\n\n    # Months and days are lowercase in Finnish\n    month_names = [\n        \"\",\n        \"tammikuu\",\n        \"helmikuu\",\n        \"maaliskuu\",\n        \"huhtikuu\",\n        \"toukokuu\",\n        \"kesäkuu\",\n        \"heinäkuu\",\n        \"elokuu\",\n        \"syyskuu\",\n        \"lokakuu\",\n        \"marraskuu\",\n        \"joulukuu\",\n    ]\n\n    month_abbreviations = [\n        \"\",\n        \"tammi\",\n        \"helmi\",\n        \"maalis\",\n        \"huhti\",\n        \"touko\",\n        \"kesä\",\n        \"heinä\",\n        \"elo\",\n        \"syys\",\n        \"loka\",\n        \"marras\",\n        \"joulu\",\n    ]\n\n    day_names = [\n        \"\",\n        \"maanantai\",\n        \"tiistai\",\n        \"keskiviikko\",\n        \"torstai\",\n        \"perjantai\",\n        \"lauantai\",\n        \"sunnuntai\",\n    ]\n\n    day_abbreviations = [\"\", \"ma\", \"ti\", \"ke\", \"to\", \"pe\", \"la\", \"su\"]\n\n    def _format_timeframe(self, timeframe, delta):\n        return (\n            self.timeframes[timeframe][0].format(abs(delta)),\n            self.timeframes[timeframe][1].format(abs(delta)),\n        )\n\n    def _format_relative(self, humanized, timeframe, delta):\n        if timeframe == \"now\":\n            return humanized[0]\n\n        direction = self.past if delta < 0 else self.future\n        which = 0 if delta < 0 else 1\n\n        return direction.format(humanized[which])\n\n    def _ordinal_number(self, n):\n        return \"{}.\".format(n)\n\n\nclass ChineseCNLocale(Locale):\n\n    names = [\"zh\", \"zh_cn\"]\n\n    past = \"{0}前\"\n    future = \"{0}后\"\n\n    timeframes = {\n        \"now\": \"刚才\",\n        \"second\": \"一秒\",\n        \"seconds\": \"{0}秒\",\n        \"minute\": \"1分钟\",\n        \"minutes\": \"{0}分钟\",\n        \"hour\": \"1小时\",\n        \"hours\": \"{0}小时\",\n        \"day\": \"1天\",\n        \"days\": \"{0}天\",\n        \"week\": \"一周\",\n        \"weeks\": \"{0}周\",\n        \"month\": \"1个月\",\n        \"months\": \"{0}个月\",\n        \"year\": \"1年\",\n        \"years\": \"{0}年\",\n    }\n\n    month_names = [\n        \"\",\n        \"一月\",\n        \"二月\",\n        \"三月\",\n        \"四月\",\n        \"五月\",\n        \"六月\",\n        \"七月\",\n        \"八月\",\n        \"九月\",\n        \"十月\",\n        \"十一月\",\n        \"十二月\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \" 1\",\n        \" 2\",\n        \" 3\",\n        \" 4\",\n        \" 5\",\n        \" 6\",\n        \" 7\",\n        \" 8\",\n        \" 9\",\n        \"10\",\n        \"11\",\n        \"12\",\n    ]\n\n    day_names = [\"\", \"星期一\", \"星期二\", \"星期三\", \"星期四\", \"星期五\", \"星期六\", \"星期日\"]\n    day_abbreviations = [\"\", \"一\", \"二\", \"三\", \"四\", \"五\", \"六\", \"日\"]\n\n\nclass ChineseTWLocale(Locale):\n\n    names = [\"zh_tw\"]\n\n    past = \"{0}前\"\n    future = \"{0}後\"\n    and_word = \"和\"\n\n    timeframes = {\n        \"now\": \"剛才\",\n        \"second\": \"1秒\",\n        \"seconds\": \"{0}秒\",\n        \"minute\": \"1分鐘\",\n        \"minutes\": \"{0}分鐘\",\n        \"hour\": \"1小時\",\n        \"hours\": \"{0}小時\",\n        \"day\": \"1天\",\n        \"days\": \"{0}天\",\n        \"week\": \"1週\",\n        \"weeks\": \"{0}週\",\n        \"month\": \"1個月\",\n        \"months\": \"{0}個月\",\n        \"year\": \"1年\",\n        \"years\": \"{0}年\",\n    }\n\n    month_names = [\n        \"\",\n        \"1月\",\n        \"2月\",\n        \"3月\",\n        \"4月\",\n        \"5月\",\n        \"6月\",\n        \"7月\",\n        \"8月\",\n        \"9月\",\n        \"10月\",\n        \"11月\",\n        \"12月\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \" 1\",\n        \" 2\",\n        \" 3\",\n        \" 4\",\n        \" 5\",\n        \" 6\",\n        \" 7\",\n        \" 8\",\n        \" 9\",\n        \"10\",\n        \"11\",\n        \"12\",\n    ]\n\n    day_names = [\"\", \"週一\", \"週二\", \"週三\", \"週四\", \"週五\", \"週六\", \"週日\"]\n    day_abbreviations = [\"\", \"一\", \"二\", \"三\", \"四\", \"五\", \"六\", \"日\"]\n\n\nclass HongKongLocale(Locale):\n\n    names = [\"zh_hk\"]\n\n    past = \"{0}前\"\n    future = \"{0}後\"\n\n    timeframes = {\n        \"now\": \"剛才\",\n        \"second\": \"1秒\",\n        \"seconds\": \"{0}秒\",\n        \"minute\": \"1分鐘\",\n        \"minutes\": \"{0}分鐘\",\n        \"hour\": \"1小時\",\n        \"hours\": \"{0}小時\",\n        \"day\": \"1天\",\n        \"days\": \"{0}天\",\n        \"week\": \"1星期\",\n        \"weeks\": \"{0}星期\",\n        \"month\": \"1個月\",\n        \"months\": \"{0}個月\",\n        \"year\": \"1年\",\n        \"years\": \"{0}年\",\n    }\n\n    month_names = [\n        \"\",\n        \"1月\",\n        \"2月\",\n        \"3月\",\n        \"4月\",\n        \"5月\",\n        \"6月\",\n        \"7月\",\n        \"8月\",\n        \"9月\",\n        \"10月\",\n        \"11月\",\n        \"12月\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \" 1\",\n        \" 2\",\n        \" 3\",\n        \" 4\",\n        \" 5\",\n        \" 6\",\n        \" 7\",\n        \" 8\",\n        \" 9\",\n        \"10\",\n        \"11\",\n        \"12\",\n    ]\n\n    day_names = [\"\", \"星期一\", \"星期二\", \"星期三\", \"星期四\", \"星期五\", \"星期六\", \"星期日\"]\n    day_abbreviations = [\"\", \"一\", \"二\", \"三\", \"四\", \"五\", \"六\", \"日\"]\n\n\nclass KoreanLocale(Locale):\n\n    names = [\"ko\", \"ko_kr\"]\n\n    past = \"{0} 전\"\n    future = \"{0} 후\"\n\n    timeframes = {\n        \"now\": \"지금\",\n        \"second\": \"1초\",\n        \"seconds\": \"{0}초\",\n        \"minute\": \"1분\",\n        \"minutes\": \"{0}분\",\n        \"hour\": \"한시간\",\n        \"hours\": \"{0}시간\",\n        \"day\": \"하루\",\n        \"days\": \"{0}일\",\n        \"week\": \"1주\",\n        \"weeks\": \"{0}주\",\n        \"month\": \"한달\",\n        \"months\": \"{0}개월\",\n        \"year\": \"1년\",\n        \"years\": \"{0}년\",\n    }\n\n    special_dayframes = {\n        -3: \"그끄제\",\n        -2: \"그제\",\n        -1: \"어제\",\n        1: \"내일\",\n        2: \"모레\",\n        3: \"글피\",\n        4: \"그글피\",\n    }\n\n    special_yearframes = {-2: \"제작년\", -1: \"작년\", 1: \"내년\", 2: \"내후년\"}\n\n    month_names = [\n        \"\",\n        \"1월\",\n        \"2월\",\n        \"3월\",\n        \"4월\",\n        \"5월\",\n        \"6월\",\n        \"7월\",\n        \"8월\",\n        \"9월\",\n        \"10월\",\n        \"11월\",\n        \"12월\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \" 1\",\n        \" 2\",\n        \" 3\",\n        \" 4\",\n        \" 5\",\n        \" 6\",\n        \" 7\",\n        \" 8\",\n        \" 9\",\n        \"10\",\n        \"11\",\n        \"12\",\n    ]\n\n    day_names = [\"\", \"월요일\", \"화요일\", \"수요일\", \"목요일\", \"금요일\", \"토요일\", \"일요일\"]\n    day_abbreviations = [\"\", \"월\", \"화\", \"수\", \"목\", \"금\", \"토\", \"일\"]\n\n    def _ordinal_number(self, n):\n        ordinals = [\"0\", \"첫\", \"두\", \"세\", \"네\", \"다섯\", \"여섯\", \"일곱\", \"여덟\", \"아홉\", \"열\"]\n        if n < len(ordinals):\n            return \"{}번째\".format(ordinals[n])\n        return \"{}번째\".format(n)\n\n    def _format_relative(self, humanized, timeframe, delta):\n        if timeframe in (\"day\", \"days\"):\n            special = self.special_dayframes.get(delta)\n            if special:\n                return special\n        elif timeframe in (\"year\", \"years\"):\n            special = self.special_yearframes.get(delta)\n            if special:\n                return special\n\n        return super(KoreanLocale, self)._format_relative(humanized, timeframe, delta)\n\n\n# derived locale types & implementations.\nclass DutchLocale(Locale):\n\n    names = [\"nl\", \"nl_nl\"]\n\n    past = \"{0} geleden\"\n    future = \"over {0}\"\n\n    timeframes = {\n        \"now\": \"nu\",\n        \"second\": \"een seconde\",\n        \"seconds\": \"{0} seconden\",\n        \"minute\": \"een minuut\",\n        \"minutes\": \"{0} minuten\",\n        \"hour\": \"een uur\",\n        \"hours\": \"{0} uur\",\n        \"day\": \"een dag\",\n        \"days\": \"{0} dagen\",\n        \"week\": \"een week\",\n        \"weeks\": \"{0} weken\",\n        \"month\": \"een maand\",\n        \"months\": \"{0} maanden\",\n        \"year\": \"een jaar\",\n        \"years\": \"{0} jaar\",\n    }\n\n    # In Dutch names of months and days are not starting with a capital letter\n    # like in the English language.\n    month_names = [\n        \"\",\n        \"januari\",\n        \"februari\",\n        \"maart\",\n        \"april\",\n        \"mei\",\n        \"juni\",\n        \"juli\",\n        \"augustus\",\n        \"september\",\n        \"oktober\",\n        \"november\",\n        \"december\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"jan\",\n        \"feb\",\n        \"mrt\",\n        \"apr\",\n        \"mei\",\n        \"jun\",\n        \"jul\",\n        \"aug\",\n        \"sep\",\n        \"okt\",\n        \"nov\",\n        \"dec\",\n    ]\n\n    day_names = [\n        \"\",\n        \"maandag\",\n        \"dinsdag\",\n        \"woensdag\",\n        \"donderdag\",\n        \"vrijdag\",\n        \"zaterdag\",\n        \"zondag\",\n    ]\n    day_abbreviations = [\"\", \"ma\", \"di\", \"wo\", \"do\", \"vr\", \"za\", \"zo\"]\n\n\nclass SlavicBaseLocale(Locale):\n    def _format_timeframe(self, timeframe, delta):\n\n        form = self.timeframes[timeframe]\n        delta = abs(delta)\n\n        if isinstance(form, list):\n\n            if delta % 10 == 1 and delta % 100 != 11:\n                form = form[0]\n            elif 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20):\n                form = form[1]\n            else:\n                form = form[2]\n\n        return form.format(delta)\n\n\nclass BelarusianLocale(SlavicBaseLocale):\n\n    names = [\"be\", \"be_by\"]\n\n    past = \"{0} таму\"\n    future = \"праз {0}\"\n\n    timeframes = {\n        \"now\": \"зараз\",\n        \"second\": \"секунду\",\n        \"seconds\": \"{0} некалькі секунд\",\n        \"minute\": \"хвіліну\",\n        \"minutes\": [\"{0} хвіліну\", \"{0} хвіліны\", \"{0} хвілін\"],\n        \"hour\": \"гадзіну\",\n        \"hours\": [\"{0} гадзіну\", \"{0} гадзіны\", \"{0} гадзін\"],\n        \"day\": \"дзень\",\n        \"days\": [\"{0} дзень\", \"{0} дні\", \"{0} дзён\"],\n        \"month\": \"месяц\",\n        \"months\": [\"{0} месяц\", \"{0} месяцы\", \"{0} месяцаў\"],\n        \"year\": \"год\",\n        \"years\": [\"{0} год\", \"{0} гады\", \"{0} гадоў\"],\n    }\n\n    month_names = [\n        \"\",\n        \"студзеня\",\n        \"лютага\",\n        \"сакавіка\",\n        \"красавіка\",\n        \"траўня\",\n        \"чэрвеня\",\n        \"ліпеня\",\n        \"жніўня\",\n        \"верасня\",\n        \"кастрычніка\",\n        \"лістапада\",\n        \"снежня\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"студ\",\n        \"лют\",\n        \"сак\",\n        \"крас\",\n        \"трав\",\n        \"чэрв\",\n        \"ліп\",\n        \"жнів\",\n        \"вер\",\n        \"каст\",\n        \"ліст\",\n        \"снеж\",\n    ]\n\n    day_names = [\n        \"\",\n        \"панядзелак\",\n        \"аўторак\",\n        \"серада\",\n        \"чацвер\",\n        \"пятніца\",\n        \"субота\",\n        \"нядзеля\",\n    ]\n    day_abbreviations = [\"\", \"пн\", \"ат\", \"ср\", \"чц\", \"пт\", \"сб\", \"нд\"]\n\n\nclass PolishLocale(SlavicBaseLocale):\n\n    names = [\"pl\", \"pl_pl\"]\n\n    past = \"{0} temu\"\n    future = \"za {0}\"\n\n    # The nouns should be in genitive case (Polish: \"dopełniacz\")\n    # in order to correctly form `past` & `future` expressions.\n    timeframes = {\n        \"now\": \"teraz\",\n        \"second\": \"sekundę\",\n        \"seconds\": [\"{0} sekund\", \"{0} sekundy\", \"{0} sekund\"],\n        \"minute\": \"minutę\",\n        \"minutes\": [\"{0} minut\", \"{0} minuty\", \"{0} minut\"],\n        \"hour\": \"godzinę\",\n        \"hours\": [\"{0} godzin\", \"{0} godziny\", \"{0} godzin\"],\n        \"day\": \"dzień\",\n        \"days\": \"{0} dni\",\n        \"week\": \"tydzień\",\n        \"weeks\": [\"{0} tygodni\", \"{0} tygodnie\", \"{0} tygodni\"],\n        \"month\": \"miesiąc\",\n        \"months\": [\"{0} miesięcy\", \"{0} miesiące\", \"{0} miesięcy\"],\n        \"year\": \"rok\",\n        \"years\": [\"{0} lat\", \"{0} lata\", \"{0} lat\"],\n    }\n\n    month_names = [\n        \"\",\n        \"styczeń\",\n        \"luty\",\n        \"marzec\",\n        \"kwiecień\",\n        \"maj\",\n        \"czerwiec\",\n        \"lipiec\",\n        \"sierpień\",\n        \"wrzesień\",\n        \"październik\",\n        \"listopad\",\n        \"grudzień\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"sty\",\n        \"lut\",\n        \"mar\",\n        \"kwi\",\n        \"maj\",\n        \"cze\",\n        \"lip\",\n        \"sie\",\n        \"wrz\",\n        \"paź\",\n        \"lis\",\n        \"gru\",\n    ]\n\n    day_names = [\n        \"\",\n        \"poniedziałek\",\n        \"wtorek\",\n        \"środa\",\n        \"czwartek\",\n        \"piątek\",\n        \"sobota\",\n        \"niedziela\",\n    ]\n    day_abbreviations = [\"\", \"Pn\", \"Wt\", \"Śr\", \"Czw\", \"Pt\", \"So\", \"Nd\"]\n\n\nclass RussianLocale(SlavicBaseLocale):\n\n    names = [\"ru\", \"ru_ru\"]\n\n    past = \"{0} назад\"\n    future = \"через {0}\"\n\n    timeframes = {\n        \"now\": \"сейчас\",\n        \"second\": \"Второй\",\n        \"seconds\": \"{0} несколько секунд\",\n        \"minute\": \"минуту\",\n        \"minutes\": [\"{0} минуту\", \"{0} минуты\", \"{0} минут\"],\n        \"hour\": \"час\",\n        \"hours\": [\"{0} час\", \"{0} часа\", \"{0} часов\"],\n        \"day\": \"день\",\n        \"days\": [\"{0} день\", \"{0} дня\", \"{0} дней\"],\n        \"week\": \"неделю\",\n        \"weeks\": [\"{0} неделю\", \"{0} недели\", \"{0} недель\"],\n        \"month\": \"месяц\",\n        \"months\": [\"{0} месяц\", \"{0} месяца\", \"{0} месяцев\"],\n        \"year\": \"год\",\n        \"years\": [\"{0} год\", \"{0} года\", \"{0} лет\"],\n    }\n\n    month_names = [\n        \"\",\n        \"января\",\n        \"февраля\",\n        \"марта\",\n        \"апреля\",\n        \"мая\",\n        \"июня\",\n        \"июля\",\n        \"августа\",\n        \"сентября\",\n        \"октября\",\n        \"ноября\",\n        \"декабря\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"янв\",\n        \"фев\",\n        \"мар\",\n        \"апр\",\n        \"май\",\n        \"июн\",\n        \"июл\",\n        \"авг\",\n        \"сен\",\n        \"окт\",\n        \"ноя\",\n        \"дек\",\n    ]\n\n    day_names = [\n        \"\",\n        \"понедельник\",\n        \"вторник\",\n        \"среда\",\n        \"четверг\",\n        \"пятница\",\n        \"суббота\",\n        \"воскресенье\",\n    ]\n    day_abbreviations = [\"\", \"пн\", \"вт\", \"ср\", \"чт\", \"пт\", \"сб\", \"вс\"]\n\n\nclass AfrikaansLocale(Locale):\n\n    names = [\"af\", \"af_nl\"]\n\n    past = \"{0} gelede\"\n    future = \"in {0}\"\n\n    timeframes = {\n        \"now\": \"nou\",\n        \"second\": \"n sekonde\",\n        \"seconds\": \"{0} sekondes\",\n        \"minute\": \"minuut\",\n        \"minutes\": \"{0} minute\",\n        \"hour\": \"uur\",\n        \"hours\": \"{0} ure\",\n        \"day\": \"een dag\",\n        \"days\": \"{0} dae\",\n        \"month\": \"een maand\",\n        \"months\": \"{0} maande\",\n        \"year\": \"een jaar\",\n        \"years\": \"{0} jaar\",\n    }\n\n    month_names = [\n        \"\",\n        \"Januarie\",\n        \"Februarie\",\n        \"Maart\",\n        \"April\",\n        \"Mei\",\n        \"Junie\",\n        \"Julie\",\n        \"Augustus\",\n        \"September\",\n        \"Oktober\",\n        \"November\",\n        \"Desember\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"Jan\",\n        \"Feb\",\n        \"Mrt\",\n        \"Apr\",\n        \"Mei\",\n        \"Jun\",\n        \"Jul\",\n        \"Aug\",\n        \"Sep\",\n        \"Okt\",\n        \"Nov\",\n        \"Des\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Maandag\",\n        \"Dinsdag\",\n        \"Woensdag\",\n        \"Donderdag\",\n        \"Vrydag\",\n        \"Saterdag\",\n        \"Sondag\",\n    ]\n    day_abbreviations = [\"\", \"Ma\", \"Di\", \"Wo\", \"Do\", \"Vr\", \"Za\", \"So\"]\n\n\nclass BulgarianLocale(SlavicBaseLocale):\n\n    names = [\"bg\", \"bg_BG\"]\n\n    past = \"{0} назад\"\n    future = \"напред {0}\"\n\n    timeframes = {\n        \"now\": \"сега\",\n        \"second\": \"секунда\",\n        \"seconds\": \"{0} няколко секунди\",\n        \"minute\": \"минута\",\n        \"minutes\": [\"{0} минута\", \"{0} минути\", \"{0} минути\"],\n        \"hour\": \"час\",\n        \"hours\": [\"{0} час\", \"{0} часа\", \"{0} часа\"],\n        \"day\": \"ден\",\n        \"days\": [\"{0} ден\", \"{0} дни\", \"{0} дни\"],\n        \"month\": \"месец\",\n        \"months\": [\"{0} месец\", \"{0} месеца\", \"{0} месеца\"],\n        \"year\": \"година\",\n        \"years\": [\"{0} година\", \"{0} години\", \"{0} години\"],\n    }\n\n    month_names = [\n        \"\",\n        \"януари\",\n        \"февруари\",\n        \"март\",\n        \"април\",\n        \"май\",\n        \"юни\",\n        \"юли\",\n        \"август\",\n        \"септември\",\n        \"октомври\",\n        \"ноември\",\n        \"декември\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"ян\",\n        \"февр\",\n        \"март\",\n        \"апр\",\n        \"май\",\n        \"юни\",\n        \"юли\",\n        \"авг\",\n        \"септ\",\n        \"окт\",\n        \"ноем\",\n        \"дек\",\n    ]\n\n    day_names = [\n        \"\",\n        \"понеделник\",\n        \"вторник\",\n        \"сряда\",\n        \"четвъртък\",\n        \"петък\",\n        \"събота\",\n        \"неделя\",\n    ]\n    day_abbreviations = [\"\", \"пон\", \"вт\", \"ср\", \"четв\", \"пет\", \"съб\", \"нед\"]\n\n\nclass UkrainianLocale(SlavicBaseLocale):\n\n    names = [\"ua\", \"uk_ua\"]\n\n    past = \"{0} тому\"\n    future = \"за {0}\"\n\n    timeframes = {\n        \"now\": \"зараз\",\n        \"second\": \"секунда\",\n        \"seconds\": \"{0} кілька секунд\",\n        \"minute\": \"хвилину\",\n        \"minutes\": [\"{0} хвилину\", \"{0} хвилини\", \"{0} хвилин\"],\n        \"hour\": \"годину\",\n        \"hours\": [\"{0} годину\", \"{0} години\", \"{0} годин\"],\n        \"day\": \"день\",\n        \"days\": [\"{0} день\", \"{0} дні\", \"{0} днів\"],\n        \"month\": \"місяць\",\n        \"months\": [\"{0} місяць\", \"{0} місяці\", \"{0} місяців\"],\n        \"year\": \"рік\",\n        \"years\": [\"{0} рік\", \"{0} роки\", \"{0} років\"],\n    }\n\n    month_names = [\n        \"\",\n        \"січня\",\n        \"лютого\",\n        \"березня\",\n        \"квітня\",\n        \"травня\",\n        \"червня\",\n        \"липня\",\n        \"серпня\",\n        \"вересня\",\n        \"жовтня\",\n        \"листопада\",\n        \"грудня\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"січ\",\n        \"лют\",\n        \"бер\",\n        \"квіт\",\n        \"трав\",\n        \"черв\",\n        \"лип\",\n        \"серп\",\n        \"вер\",\n        \"жовт\",\n        \"лист\",\n        \"груд\",\n    ]\n\n    day_names = [\n        \"\",\n        \"понеділок\",\n        \"вівторок\",\n        \"середа\",\n        \"четвер\",\n        \"п’ятниця\",\n        \"субота\",\n        \"неділя\",\n    ]\n    day_abbreviations = [\"\", \"пн\", \"вт\", \"ср\", \"чт\", \"пт\", \"сб\", \"нд\"]\n\n\nclass MacedonianLocale(SlavicBaseLocale):\n    names = [\"mk\", \"mk_mk\"]\n\n    past = \"пред {0}\"\n    future = \"за {0}\"\n\n    timeframes = {\n        \"now\": \"сега\",\n        \"second\": \"една секунда\",\n        \"seconds\": [\"{0} секунда\", \"{0} секунди\", \"{0} секунди\"],\n        \"minute\": \"една минута\",\n        \"minutes\": [\"{0} минута\", \"{0} минути\", \"{0} минути\"],\n        \"hour\": \"еден саат\",\n        \"hours\": [\"{0} саат\", \"{0} саати\", \"{0} саати\"],\n        \"day\": \"еден ден\",\n        \"days\": [\"{0} ден\", \"{0} дена\", \"{0} дена\"],\n        \"week\": \"една недела\",\n        \"weeks\": [\"{0} недела\", \"{0} недели\", \"{0} недели\"],\n        \"month\": \"еден месец\",\n        \"months\": [\"{0} месец\", \"{0} месеци\", \"{0} месеци\"],\n        \"year\": \"една година\",\n        \"years\": [\"{0} година\", \"{0} години\", \"{0} години\"],\n    }\n\n    meridians = {\"am\": \"дп\", \"pm\": \"пп\", \"AM\": \"претпладне\", \"PM\": \"попладне\"}\n\n    month_names = [\n        \"\",\n        \"Јануари\",\n        \"Февруари\",\n        \"Март\",\n        \"Април\",\n        \"Мај\",\n        \"Јуни\",\n        \"Јули\",\n        \"Август\",\n        \"Септември\",\n        \"Октомври\",\n        \"Ноември\",\n        \"Декември\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"Јан\",\n        \"Фев\",\n        \"Мар\",\n        \"Апр\",\n        \"Мај\",\n        \"Јун\",\n        \"Јул\",\n        \"Авг\",\n        \"Септ\",\n        \"Окт\",\n        \"Ноем\",\n        \"Декем\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Понеделник\",\n        \"Вторник\",\n        \"Среда\",\n        \"Четврток\",\n        \"Петок\",\n        \"Сабота\",\n        \"Недела\",\n    ]\n    day_abbreviations = [\n        \"\",\n        \"Пон\",\n        \"Вт\",\n        \"Сре\",\n        \"Чет\",\n        \"Пет\",\n        \"Саб\",\n        \"Нед\",\n    ]\n\n\nclass GermanBaseLocale(Locale):\n\n    past = \"vor {0}\"\n    future = \"in {0}\"\n    and_word = \"und\"\n\n    timeframes = {\n        \"now\": \"gerade eben\",\n        \"second\": \"eine Sekunde\",\n        \"seconds\": \"{0} Sekunden\",\n        \"minute\": \"einer Minute\",\n        \"minutes\": \"{0} Minuten\",\n        \"hour\": \"einer Stunde\",\n        \"hours\": \"{0} Stunden\",\n        \"day\": \"einem Tag\",\n        \"days\": \"{0} Tagen\",\n        \"week\": \"einer Woche\",\n        \"weeks\": \"{0} Wochen\",\n        \"month\": \"einem Monat\",\n        \"months\": \"{0} Monaten\",\n        \"year\": \"einem Jahr\",\n        \"years\": \"{0} Jahren\",\n    }\n\n    timeframes_only_distance = timeframes.copy()\n    timeframes_only_distance[\"minute\"] = \"eine Minute\"\n    timeframes_only_distance[\"hour\"] = \"eine Stunde\"\n    timeframes_only_distance[\"day\"] = \"ein Tag\"\n    timeframes_only_distance[\"week\"] = \"eine Woche\"\n    timeframes_only_distance[\"month\"] = \"ein Monat\"\n    timeframes_only_distance[\"year\"] = \"ein Jahr\"\n\n    month_names = [\n        \"\",\n        \"Januar\",\n        \"Februar\",\n        \"März\",\n        \"April\",\n        \"Mai\",\n        \"Juni\",\n        \"Juli\",\n        \"August\",\n        \"September\",\n        \"Oktober\",\n        \"November\",\n        \"Dezember\",\n    ]\n\n    month_abbreviations = [\n        \"\",\n        \"Jan\",\n        \"Feb\",\n        \"Mär\",\n        \"Apr\",\n        \"Mai\",\n        \"Jun\",\n        \"Jul\",\n        \"Aug\",\n        \"Sep\",\n        \"Okt\",\n        \"Nov\",\n        \"Dez\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Montag\",\n        \"Dienstag\",\n        \"Mittwoch\",\n        \"Donnerstag\",\n        \"Freitag\",\n        \"Samstag\",\n        \"Sonntag\",\n    ]\n\n    day_abbreviations = [\"\", \"Mo\", \"Di\", \"Mi\", \"Do\", \"Fr\", \"Sa\", \"So\"]\n\n    def _ordinal_number(self, n):\n        return \"{}.\".format(n)\n\n    def describe(self, timeframe, delta=0, only_distance=False):\n        \"\"\"Describes a delta within a timeframe in plain language.\n\n        :param timeframe: a string representing a timeframe.\n        :param delta: a quantity representing a delta in a timeframe.\n        :param only_distance: return only distance eg: \"11 seconds\" without \"in\" or \"ago\" keywords\n        \"\"\"\n\n        if not only_distance:\n            return super(GermanBaseLocale, self).describe(\n                timeframe, delta, only_distance\n            )\n\n        # German uses a different case without 'in' or 'ago'\n        humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta)))\n\n        return humanized\n\n\nclass GermanLocale(GermanBaseLocale, Locale):\n\n    names = [\"de\", \"de_de\"]\n\n\nclass SwissLocale(GermanBaseLocale, Locale):\n\n    names = [\"de_ch\"]\n\n\nclass AustrianLocale(GermanBaseLocale, Locale):\n\n    names = [\"de_at\"]\n\n    month_names = [\n        \"\",\n        \"Jänner\",\n        \"Februar\",\n        \"März\",\n        \"April\",\n        \"Mai\",\n        \"Juni\",\n        \"Juli\",\n        \"August\",\n        \"September\",\n        \"Oktober\",\n        \"November\",\n        \"Dezember\",\n    ]\n\n\nclass NorwegianLocale(Locale):\n\n    names = [\"nb\", \"nb_no\"]\n\n    past = \"for {0} siden\"\n    future = \"om {0}\"\n\n    timeframes = {\n        \"now\": \"nå nettopp\",\n        \"second\": \"et sekund\",\n        \"seconds\": \"{0} noen sekunder\",\n        \"minute\": \"ett minutt\",\n        \"minutes\": \"{0} minutter\",\n        \"hour\": \"en time\",\n        \"hours\": \"{0} timer\",\n        \"day\": \"en dag\",\n        \"days\": \"{0} dager\",\n        \"month\": \"en måned\",\n        \"months\": \"{0} måneder\",\n        \"year\": \"ett år\",\n        \"years\": \"{0} år\",\n    }\n\n    month_names = [\n        \"\",\n        \"januar\",\n        \"februar\",\n        \"mars\",\n        \"april\",\n        \"mai\",\n        \"juni\",\n        \"juli\",\n        \"august\",\n        \"september\",\n        \"oktober\",\n        \"november\",\n        \"desember\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"jan\",\n        \"feb\",\n        \"mar\",\n        \"apr\",\n        \"mai\",\n        \"jun\",\n        \"jul\",\n        \"aug\",\n        \"sep\",\n        \"okt\",\n        \"nov\",\n        \"des\",\n    ]\n\n    day_names = [\n        \"\",\n        \"mandag\",\n        \"tirsdag\",\n        \"onsdag\",\n        \"torsdag\",\n        \"fredag\",\n        \"lørdag\",\n        \"søndag\",\n    ]\n    day_abbreviations = [\"\", \"ma\", \"ti\", \"on\", \"to\", \"fr\", \"lø\", \"sø\"]\n\n\nclass NewNorwegianLocale(Locale):\n\n    names = [\"nn\", \"nn_no\"]\n\n    past = \"for {0} sidan\"\n    future = \"om {0}\"\n\n    timeframes = {\n        \"now\": \"no nettopp\",\n        \"second\": \"et sekund\",\n        \"seconds\": \"{0} nokre sekund\",\n        \"minute\": \"ett minutt\",\n        \"minutes\": \"{0} minutt\",\n        \"hour\": \"ein time\",\n        \"hours\": \"{0} timar\",\n        \"day\": \"ein dag\",\n        \"days\": \"{0} dagar\",\n        \"month\": \"en månad\",\n        \"months\": \"{0} månader\",\n        \"year\": \"eit år\",\n        \"years\": \"{0} år\",\n    }\n\n    month_names = [\n        \"\",\n        \"januar\",\n        \"februar\",\n        \"mars\",\n        \"april\",\n        \"mai\",\n        \"juni\",\n        \"juli\",\n        \"august\",\n        \"september\",\n        \"oktober\",\n        \"november\",\n        \"desember\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"jan\",\n        \"feb\",\n        \"mar\",\n        \"apr\",\n        \"mai\",\n        \"jun\",\n        \"jul\",\n        \"aug\",\n        \"sep\",\n        \"okt\",\n        \"nov\",\n        \"des\",\n    ]\n\n    day_names = [\n        \"\",\n        \"måndag\",\n        \"tysdag\",\n        \"onsdag\",\n        \"torsdag\",\n        \"fredag\",\n        \"laurdag\",\n        \"sundag\",\n    ]\n    day_abbreviations = [\"\", \"må\", \"ty\", \"on\", \"to\", \"fr\", \"la\", \"su\"]\n\n\nclass PortugueseLocale(Locale):\n    names = [\"pt\", \"pt_pt\"]\n\n    past = \"há {0}\"\n    future = \"em {0}\"\n    and_word = \"e\"\n\n    timeframes = {\n        \"now\": \"agora\",\n        \"second\": \"um segundo\",\n        \"seconds\": \"{0} segundos\",\n        \"minute\": \"um minuto\",\n        \"minutes\": \"{0} minutos\",\n        \"hour\": \"uma hora\",\n        \"hours\": \"{0} horas\",\n        \"day\": \"um dia\",\n        \"days\": \"{0} dias\",\n        \"week\": \"uma semana\",\n        \"weeks\": \"{0} semanas\",\n        \"month\": \"um mês\",\n        \"months\": \"{0} meses\",\n        \"year\": \"um ano\",\n        \"years\": \"{0} anos\",\n    }\n\n    month_names = [\n        \"\",\n        \"Janeiro\",\n        \"Fevereiro\",\n        \"Março\",\n        \"Abril\",\n        \"Maio\",\n        \"Junho\",\n        \"Julho\",\n        \"Agosto\",\n        \"Setembro\",\n        \"Outubro\",\n        \"Novembro\",\n        \"Dezembro\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"Jan\",\n        \"Fev\",\n        \"Mar\",\n        \"Abr\",\n        \"Mai\",\n        \"Jun\",\n        \"Jul\",\n        \"Ago\",\n        \"Set\",\n        \"Out\",\n        \"Nov\",\n        \"Dez\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Segunda-feira\",\n        \"Terça-feira\",\n        \"Quarta-feira\",\n        \"Quinta-feira\",\n        \"Sexta-feira\",\n        \"Sábado\",\n        \"Domingo\",\n    ]\n    day_abbreviations = [\"\", \"Seg\", \"Ter\", \"Qua\", \"Qui\", \"Sex\", \"Sab\", \"Dom\"]\n\n\nclass BrazilianPortugueseLocale(PortugueseLocale):\n    names = [\"pt_br\"]\n\n    past = \"faz {0}\"\n\n\nclass TagalogLocale(Locale):\n\n    names = [\"tl\", \"tl_ph\"]\n\n    past = \"nakaraang {0}\"\n    future = \"{0} mula ngayon\"\n\n    timeframes = {\n        \"now\": \"ngayon lang\",\n        \"second\": \"isang segundo\",\n        \"seconds\": \"{0} segundo\",\n        \"minute\": \"isang minuto\",\n        \"minutes\": \"{0} minuto\",\n        \"hour\": \"isang oras\",\n        \"hours\": \"{0} oras\",\n        \"day\": \"isang araw\",\n        \"days\": \"{0} araw\",\n        \"week\": \"isang linggo\",\n        \"weeks\": \"{0} linggo\",\n        \"month\": \"isang buwan\",\n        \"months\": \"{0} buwan\",\n        \"year\": \"isang taon\",\n        \"years\": \"{0} taon\",\n    }\n\n    month_names = [\n        \"\",\n        \"Enero\",\n        \"Pebrero\",\n        \"Marso\",\n        \"Abril\",\n        \"Mayo\",\n        \"Hunyo\",\n        \"Hulyo\",\n        \"Agosto\",\n        \"Setyembre\",\n        \"Oktubre\",\n        \"Nobyembre\",\n        \"Disyembre\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"Ene\",\n        \"Peb\",\n        \"Mar\",\n        \"Abr\",\n        \"May\",\n        \"Hun\",\n        \"Hul\",\n        \"Ago\",\n        \"Set\",\n        \"Okt\",\n        \"Nob\",\n        \"Dis\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Lunes\",\n        \"Martes\",\n        \"Miyerkules\",\n        \"Huwebes\",\n        \"Biyernes\",\n        \"Sabado\",\n        \"Linggo\",\n    ]\n    day_abbreviations = [\"\", \"Lun\", \"Mar\", \"Miy\", \"Huw\", \"Biy\", \"Sab\", \"Lin\"]\n\n    meridians = {\"am\": \"nu\", \"pm\": \"nh\", \"AM\": \"ng umaga\", \"PM\": \"ng hapon\"}\n\n    def _ordinal_number(self, n):\n        return \"ika-{}\".format(n)\n\n\nclass VietnameseLocale(Locale):\n\n    names = [\"vi\", \"vi_vn\"]\n\n    past = \"{0} trước\"\n    future = \"{0} nữa\"\n\n    timeframes = {\n        \"now\": \"hiện tại\",\n        \"second\": \"một giây\",\n        \"seconds\": \"{0} giây\",\n        \"minute\": \"một phút\",\n        \"minutes\": \"{0} phút\",\n        \"hour\": \"một giờ\",\n        \"hours\": \"{0} giờ\",\n        \"day\": \"một ngày\",\n        \"days\": \"{0} ngày\",\n        \"week\": \"một tuần\",\n        \"weeks\": \"{0} tuần\",\n        \"month\": \"một tháng\",\n        \"months\": \"{0} tháng\",\n        \"year\": \"một năm\",\n        \"years\": \"{0} năm\",\n    }\n\n    month_names = [\n        \"\",\n        \"Tháng Một\",\n        \"Tháng Hai\",\n        \"Tháng Ba\",\n        \"Tháng Tư\",\n        \"Tháng Năm\",\n        \"Tháng Sáu\",\n        \"Tháng Bảy\",\n        \"Tháng Tám\",\n        \"Tháng Chín\",\n        \"Tháng Mười\",\n        \"Tháng Mười Một\",\n        \"Tháng Mười Hai\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"Tháng 1\",\n        \"Tháng 2\",\n        \"Tháng 3\",\n        \"Tháng 4\",\n        \"Tháng 5\",\n        \"Tháng 6\",\n        \"Tháng 7\",\n        \"Tháng 8\",\n        \"Tháng 9\",\n        \"Tháng 10\",\n        \"Tháng 11\",\n        \"Tháng 12\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Thứ Hai\",\n        \"Thứ Ba\",\n        \"Thứ Tư\",\n        \"Thứ Năm\",\n        \"Thứ Sáu\",\n        \"Thứ Bảy\",\n        \"Chủ Nhật\",\n    ]\n    day_abbreviations = [\"\", \"Thứ 2\", \"Thứ 3\", \"Thứ 4\", \"Thứ 5\", \"Thứ 6\", \"Thứ 7\", \"CN\"]\n\n\nclass TurkishLocale(Locale):\n\n    names = [\"tr\", \"tr_tr\"]\n\n    past = \"{0} önce\"\n    future = \"{0} sonra\"\n\n    timeframes = {\n        \"now\": \"şimdi\",\n        \"second\": \"bir saniye\",\n        \"seconds\": \"{0} saniye\",\n        \"minute\": \"bir dakika\",\n        \"minutes\": \"{0} dakika\",\n        \"hour\": \"bir saat\",\n        \"hours\": \"{0} saat\",\n        \"day\": \"bir gün\",\n        \"days\": \"{0} gün\",\n        \"month\": \"bir ay\",\n        \"months\": \"{0} ay\",\n        \"year\": \"yıl\",\n        \"years\": \"{0} yıl\",\n    }\n\n    month_names = [\n        \"\",\n        \"Ocak\",\n        \"Şubat\",\n        \"Mart\",\n        \"Nisan\",\n        \"Mayıs\",\n        \"Haziran\",\n        \"Temmuz\",\n        \"Ağustos\",\n        \"Eylül\",\n        \"Ekim\",\n        \"Kasım\",\n        \"Aralık\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"Oca\",\n        \"Şub\",\n        \"Mar\",\n        \"Nis\",\n        \"May\",\n        \"Haz\",\n        \"Tem\",\n        \"Ağu\",\n        \"Eyl\",\n        \"Eki\",\n        \"Kas\",\n        \"Ara\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Pazartesi\",\n        \"Salı\",\n        \"Çarşamba\",\n        \"Perşembe\",\n        \"Cuma\",\n        \"Cumartesi\",\n        \"Pazar\",\n    ]\n    day_abbreviations = [\"\", \"Pzt\", \"Sal\", \"Çar\", \"Per\", \"Cum\", \"Cmt\", \"Paz\"]\n\n\nclass AzerbaijaniLocale(Locale):\n\n    names = [\"az\", \"az_az\"]\n\n    past = \"{0} əvvəl\"\n    future = \"{0} sonra\"\n\n    timeframes = {\n        \"now\": \"indi\",\n        \"second\": \"saniyə\",\n        \"seconds\": \"{0} saniyə\",\n        \"minute\": \"bir dəqiqə\",\n        \"minutes\": \"{0} dəqiqə\",\n        \"hour\": \"bir saat\",\n        \"hours\": \"{0} saat\",\n        \"day\": \"bir gün\",\n        \"days\": \"{0} gün\",\n        \"month\": \"bir ay\",\n        \"months\": \"{0} ay\",\n        \"year\": \"il\",\n        \"years\": \"{0} il\",\n    }\n\n    month_names = [\n        \"\",\n        \"Yanvar\",\n        \"Fevral\",\n        \"Mart\",\n        \"Aprel\",\n        \"May\",\n        \"İyun\",\n        \"İyul\",\n        \"Avqust\",\n        \"Sentyabr\",\n        \"Oktyabr\",\n        \"Noyabr\",\n        \"Dekabr\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"Yan\",\n        \"Fev\",\n        \"Mar\",\n        \"Apr\",\n        \"May\",\n        \"İyn\",\n        \"İyl\",\n        \"Avq\",\n        \"Sen\",\n        \"Okt\",\n        \"Noy\",\n        \"Dek\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Bazar ertəsi\",\n        \"Çərşənbə axşamı\",\n        \"Çərşənbə\",\n        \"Cümə axşamı\",\n        \"Cümə\",\n        \"Şənbə\",\n        \"Bazar\",\n    ]\n    day_abbreviations = [\"\", \"Ber\", \"Çax\", \"Çər\", \"Cax\", \"Cüm\", \"Şnb\", \"Bzr\"]\n\n\nclass ArabicLocale(Locale):\n    names = [\n        \"ar\",\n        \"ar_ae\",\n        \"ar_bh\",\n        \"ar_dj\",\n        \"ar_eg\",\n        \"ar_eh\",\n        \"ar_er\",\n        \"ar_km\",\n        \"ar_kw\",\n        \"ar_ly\",\n        \"ar_om\",\n        \"ar_qa\",\n        \"ar_sa\",\n        \"ar_sd\",\n        \"ar_so\",\n        \"ar_ss\",\n        \"ar_td\",\n        \"ar_ye\",\n    ]\n\n    past = \"منذ {0}\"\n    future = \"خلال {0}\"\n\n    timeframes = {\n        \"now\": \"الآن\",\n        \"second\": \"ثانية\",\n        \"seconds\": {\"double\": \"ثانيتين\", \"ten\": \"{0} ثوان\", \"higher\": \"{0} ثانية\"},\n        \"minute\": \"دقيقة\",\n        \"minutes\": {\"double\": \"دقيقتين\", \"ten\": \"{0} دقائق\", \"higher\": \"{0} دقيقة\"},\n        \"hour\": \"ساعة\",\n        \"hours\": {\"double\": \"ساعتين\", \"ten\": \"{0} ساعات\", \"higher\": \"{0} ساعة\"},\n        \"day\": \"يوم\",\n        \"days\": {\"double\": \"يومين\", \"ten\": \"{0} أيام\", \"higher\": \"{0} يوم\"},\n        \"month\": \"شهر\",\n        \"months\": {\"double\": \"شهرين\", \"ten\": \"{0} أشهر\", \"higher\": \"{0} شهر\"},\n        \"year\": \"سنة\",\n        \"years\": {\"double\": \"سنتين\", \"ten\": \"{0} سنوات\", \"higher\": \"{0} سنة\"},\n    }\n\n    month_names = [\n        \"\",\n        \"يناير\",\n        \"فبراير\",\n        \"مارس\",\n        \"أبريل\",\n        \"مايو\",\n        \"يونيو\",\n        \"يوليو\",\n        \"أغسطس\",\n        \"سبتمبر\",\n        \"أكتوبر\",\n        \"نوفمبر\",\n        \"ديسمبر\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"يناير\",\n        \"فبراير\",\n        \"مارس\",\n        \"أبريل\",\n        \"مايو\",\n        \"يونيو\",\n        \"يوليو\",\n        \"أغسطس\",\n        \"سبتمبر\",\n        \"أكتوبر\",\n        \"نوفمبر\",\n        \"ديسمبر\",\n    ]\n\n    day_names = [\n        \"\",\n        \"الإثنين\",\n        \"الثلاثاء\",\n        \"الأربعاء\",\n        \"الخميس\",\n        \"الجمعة\",\n        \"السبت\",\n        \"الأحد\",\n    ]\n    day_abbreviations = [\"\", \"إثنين\", \"ثلاثاء\", \"أربعاء\", \"خميس\", \"جمعة\", \"سبت\", \"أحد\"]\n\n    def _format_timeframe(self, timeframe, delta):\n        form = self.timeframes[timeframe]\n        delta = abs(delta)\n        if isinstance(form, dict):\n            if delta == 2:\n                form = form[\"double\"]\n            elif delta > 2 and delta <= 10:\n                form = form[\"ten\"]\n            else:\n                form = form[\"higher\"]\n\n        return form.format(delta)\n\n\nclass LevantArabicLocale(ArabicLocale):\n    names = [\"ar_iq\", \"ar_jo\", \"ar_lb\", \"ar_ps\", \"ar_sy\"]\n    month_names = [\n        \"\",\n        \"كانون الثاني\",\n        \"شباط\",\n        \"آذار\",\n        \"نيسان\",\n        \"أيار\",\n        \"حزيران\",\n        \"تموز\",\n        \"آب\",\n        \"أيلول\",\n        \"تشرين الأول\",\n        \"تشرين الثاني\",\n        \"كانون الأول\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"كانون الثاني\",\n        \"شباط\",\n        \"آذار\",\n        \"نيسان\",\n        \"أيار\",\n        \"حزيران\",\n        \"تموز\",\n        \"آب\",\n        \"أيلول\",\n        \"تشرين الأول\",\n        \"تشرين الثاني\",\n        \"كانون الأول\",\n    ]\n\n\nclass AlgeriaTunisiaArabicLocale(ArabicLocale):\n    names = [\"ar_tn\", \"ar_dz\"]\n    month_names = [\n        \"\",\n        \"جانفي\",\n        \"فيفري\",\n        \"مارس\",\n        \"أفريل\",\n        \"ماي\",\n        \"جوان\",\n        \"جويلية\",\n        \"أوت\",\n        \"سبتمبر\",\n        \"أكتوبر\",\n        \"نوفمبر\",\n        \"ديسمبر\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"جانفي\",\n        \"فيفري\",\n        \"مارس\",\n        \"أفريل\",\n        \"ماي\",\n        \"جوان\",\n        \"جويلية\",\n        \"أوت\",\n        \"سبتمبر\",\n        \"أكتوبر\",\n        \"نوفمبر\",\n        \"ديسمبر\",\n    ]\n\n\nclass MauritaniaArabicLocale(ArabicLocale):\n    names = [\"ar_mr\"]\n    month_names = [\n        \"\",\n        \"يناير\",\n        \"فبراير\",\n        \"مارس\",\n        \"إبريل\",\n        \"مايو\",\n        \"يونيو\",\n        \"يوليو\",\n        \"أغشت\",\n        \"شتمبر\",\n        \"أكتوبر\",\n        \"نوفمبر\",\n        \"دجمبر\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"يناير\",\n        \"فبراير\",\n        \"مارس\",\n        \"إبريل\",\n        \"مايو\",\n        \"يونيو\",\n        \"يوليو\",\n        \"أغشت\",\n        \"شتمبر\",\n        \"أكتوبر\",\n        \"نوفمبر\",\n        \"دجمبر\",\n    ]\n\n\nclass MoroccoArabicLocale(ArabicLocale):\n    names = [\"ar_ma\"]\n    month_names = [\n        \"\",\n        \"يناير\",\n        \"فبراير\",\n        \"مارس\",\n        \"أبريل\",\n        \"ماي\",\n        \"يونيو\",\n        \"يوليوز\",\n        \"غشت\",\n        \"شتنبر\",\n        \"أكتوبر\",\n        \"نونبر\",\n        \"دجنبر\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"يناير\",\n        \"فبراير\",\n        \"مارس\",\n        \"أبريل\",\n        \"ماي\",\n        \"يونيو\",\n        \"يوليوز\",\n        \"غشت\",\n        \"شتنبر\",\n        \"أكتوبر\",\n        \"نونبر\",\n        \"دجنبر\",\n    ]\n\n\nclass IcelandicLocale(Locale):\n    def _format_timeframe(self, timeframe, delta):\n\n        timeframe = self.timeframes[timeframe]\n        if delta < 0:\n            timeframe = timeframe[0]\n        elif delta > 0:\n            timeframe = timeframe[1]\n\n        return timeframe.format(abs(delta))\n\n    names = [\"is\", \"is_is\"]\n\n    past = \"fyrir {0} síðan\"\n    future = \"eftir {0}\"\n\n    timeframes = {\n        \"now\": \"rétt í þessu\",\n        \"second\": (\"sekúndu\", \"sekúndu\"),\n        \"seconds\": (\"{0} nokkrum sekúndum\", \"nokkrar sekúndur\"),\n        \"minute\": (\"einni mínútu\", \"eina mínútu\"),\n        \"minutes\": (\"{0} mínútum\", \"{0} mínútur\"),\n        \"hour\": (\"einum tíma\", \"einn tíma\"),\n        \"hours\": (\"{0} tímum\", \"{0} tíma\"),\n        \"day\": (\"einum degi\", \"einn dag\"),\n        \"days\": (\"{0} dögum\", \"{0} daga\"),\n        \"month\": (\"einum mánuði\", \"einn mánuð\"),\n        \"months\": (\"{0} mánuðum\", \"{0} mánuði\"),\n        \"year\": (\"einu ári\", \"eitt ár\"),\n        \"years\": (\"{0} árum\", \"{0} ár\"),\n    }\n\n    meridians = {\"am\": \"f.h.\", \"pm\": \"e.h.\", \"AM\": \"f.h.\", \"PM\": \"e.h.\"}\n\n    month_names = [\n        \"\",\n        \"janúar\",\n        \"febrúar\",\n        \"mars\",\n        \"apríl\",\n        \"maí\",\n        \"júní\",\n        \"júlí\",\n        \"ágúst\",\n        \"september\",\n        \"október\",\n        \"nóvember\",\n        \"desember\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"jan\",\n        \"feb\",\n        \"mar\",\n        \"apr\",\n        \"maí\",\n        \"jún\",\n        \"júl\",\n        \"ágú\",\n        \"sep\",\n        \"okt\",\n        \"nóv\",\n        \"des\",\n    ]\n\n    day_names = [\n        \"\",\n        \"mánudagur\",\n        \"þriðjudagur\",\n        \"miðvikudagur\",\n        \"fimmtudagur\",\n        \"föstudagur\",\n        \"laugardagur\",\n        \"sunnudagur\",\n    ]\n    day_abbreviations = [\"\", \"mán\", \"þri\", \"mið\", \"fim\", \"fös\", \"lau\", \"sun\"]\n\n\nclass DanishLocale(Locale):\n\n    names = [\"da\", \"da_dk\"]\n\n    past = \"for {0} siden\"\n    future = \"efter {0}\"\n    and_word = \"og\"\n\n    timeframes = {\n        \"now\": \"lige nu\",\n        \"second\": \"et sekund\",\n        \"seconds\": \"{0} et par sekunder\",\n        \"minute\": \"et minut\",\n        \"minutes\": \"{0} minutter\",\n        \"hour\": \"en time\",\n        \"hours\": \"{0} timer\",\n        \"day\": \"en dag\",\n        \"days\": \"{0} dage\",\n        \"month\": \"en måned\",\n        \"months\": \"{0} måneder\",\n        \"year\": \"et år\",\n        \"years\": \"{0} år\",\n    }\n\n    month_names = [\n        \"\",\n        \"januar\",\n        \"februar\",\n        \"marts\",\n        \"april\",\n        \"maj\",\n        \"juni\",\n        \"juli\",\n        \"august\",\n        \"september\",\n        \"oktober\",\n        \"november\",\n        \"december\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"jan\",\n        \"feb\",\n        \"mar\",\n        \"apr\",\n        \"maj\",\n        \"jun\",\n        \"jul\",\n        \"aug\",\n        \"sep\",\n        \"okt\",\n        \"nov\",\n        \"dec\",\n    ]\n\n    day_names = [\n        \"\",\n        \"mandag\",\n        \"tirsdag\",\n        \"onsdag\",\n        \"torsdag\",\n        \"fredag\",\n        \"lørdag\",\n        \"søndag\",\n    ]\n    day_abbreviations = [\"\", \"man\", \"tir\", \"ons\", \"tor\", \"fre\", \"lør\", \"søn\"]\n\n\nclass MalayalamLocale(Locale):\n\n    names = [\"ml\"]\n\n    past = \"{0} മുമ്പ്\"\n    future = \"{0} ശേഷം\"\n\n    timeframes = {\n        \"now\": \"ഇപ്പോൾ\",\n        \"second\": \"ഒരു നിമിഷം\",\n        \"seconds\": \"{0} സെക്കന്റ്‌\",\n        \"minute\": \"ഒരു മിനിറ്റ്\",\n        \"minutes\": \"{0} മിനിറ്റ്\",\n        \"hour\": \"ഒരു മണിക്കൂർ\",\n        \"hours\": \"{0} മണിക്കൂർ\",\n        \"day\": \"ഒരു ദിവസം \",\n        \"days\": \"{0} ദിവസം \",\n        \"month\": \"ഒരു മാസം \",\n        \"months\": \"{0} മാസം \",\n        \"year\": \"ഒരു വർഷം \",\n        \"years\": \"{0} വർഷം \",\n    }\n\n    meridians = {\n        \"am\": \"രാവിലെ\",\n        \"pm\": \"ഉച്ചക്ക് ശേഷം\",\n        \"AM\": \"രാവിലെ\",\n        \"PM\": \"ഉച്ചക്ക് ശേഷം\",\n    }\n\n    month_names = [\n        \"\",\n        \"ജനുവരി\",\n        \"ഫെബ്രുവരി\",\n        \"മാർച്ച്‌\",\n        \"ഏപ്രിൽ \",\n        \"മെയ്‌ \",\n        \"ജൂണ്‍\",\n        \"ജൂലൈ\",\n        \"ഓഗസ്റ്റ്‌\",\n        \"സെപ്റ്റംബർ\",\n        \"ഒക്ടോബർ\",\n        \"നവംബർ\",\n        \"ഡിസംബർ\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"ജനു\",\n        \"ഫെബ് \",\n        \"മാർ\",\n        \"ഏപ്രിൽ\",\n        \"മേയ്\",\n        \"ജൂണ്‍\",\n        \"ജൂലൈ\",\n        \"ഓഗസ്റ\",\n        \"സെപ്റ്റ\",\n        \"ഒക്ടോ\",\n        \"നവം\",\n        \"ഡിസം\",\n    ]\n\n    day_names = [\"\", \"തിങ്കള്‍\", \"ചൊവ്വ\", \"ബുധന്‍\", \"വ്യാഴം\", \"വെള്ളി\", \"ശനി\", \"ഞായര്‍\"]\n    day_abbreviations = [\n        \"\",\n        \"തിങ്കള്‍\",\n        \"ചൊവ്വ\",\n        \"ബുധന്‍\",\n        \"വ്യാഴം\",\n        \"വെള്ളി\",\n        \"ശനി\",\n        \"ഞായര്‍\",\n    ]\n\n\nclass HindiLocale(Locale):\n\n    names = [\"hi\"]\n\n    past = \"{0} पहले\"\n    future = \"{0} बाद\"\n\n    timeframes = {\n        \"now\": \"अभी\",\n        \"second\": \"एक पल\",\n        \"seconds\": \"{0} सेकंड्\",\n        \"minute\": \"एक मिनट \",\n        \"minutes\": \"{0} मिनट \",\n        \"hour\": \"एक घंटा\",\n        \"hours\": \"{0} घंटे\",\n        \"day\": \"एक दिन\",\n        \"days\": \"{0} दिन\",\n        \"month\": \"एक माह \",\n        \"months\": \"{0} महीने \",\n        \"year\": \"एक वर्ष \",\n        \"years\": \"{0} साल \",\n    }\n\n    meridians = {\"am\": \"सुबह\", \"pm\": \"शाम\", \"AM\": \"सुबह\", \"PM\": \"शाम\"}\n\n    month_names = [\n        \"\",\n        \"जनवरी\",\n        \"फरवरी\",\n        \"मार्च\",\n        \"अप्रैल \",\n        \"मई\",\n        \"जून\",\n        \"जुलाई\",\n        \"अगस्त\",\n        \"सितंबर\",\n        \"अक्टूबर\",\n        \"नवंबर\",\n        \"दिसंबर\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"जन\",\n        \"फ़र\",\n        \"मार्च\",\n        \"अप्रै\",\n        \"मई\",\n        \"जून\",\n        \"जुलाई\",\n        \"आग\",\n        \"सित\",\n        \"अकत\",\n        \"नवे\",\n        \"दिस\",\n    ]\n\n    day_names = [\n        \"\",\n        \"सोमवार\",\n        \"मंगलवार\",\n        \"बुधवार\",\n        \"गुरुवार\",\n        \"शुक्रवार\",\n        \"शनिवार\",\n        \"रविवार\",\n    ]\n    day_abbreviations = [\"\", \"सोम\", \"मंगल\", \"बुध\", \"गुरुवार\", \"शुक्र\", \"शनि\", \"रवि\"]\n\n\nclass CzechLocale(Locale):\n    names = [\"cs\", \"cs_cz\"]\n\n    timeframes = {\n        \"now\": \"Teď\",\n        \"second\": {\"past\": \"vteřina\", \"future\": \"vteřina\", \"zero\": \"vteřina\"},\n        \"seconds\": {\"past\": \"{0} sekundami\", \"future\": [\"{0} sekundy\", \"{0} sekund\"]},\n        \"minute\": {\"past\": \"minutou\", \"future\": \"minutu\", \"zero\": \"{0} minut\"},\n        \"minutes\": {\"past\": \"{0} minutami\", \"future\": [\"{0} minuty\", \"{0} minut\"]},\n        \"hour\": {\"past\": \"hodinou\", \"future\": \"hodinu\", \"zero\": \"{0} hodin\"},\n        \"hours\": {\"past\": \"{0} hodinami\", \"future\": [\"{0} hodiny\", \"{0} hodin\"]},\n        \"day\": {\"past\": \"dnem\", \"future\": \"den\", \"zero\": \"{0} dnů\"},\n        \"days\": {\"past\": \"{0} dny\", \"future\": [\"{0} dny\", \"{0} dnů\"]},\n        \"week\": {\"past\": \"týdnem\", \"future\": \"týden\", \"zero\": \"{0} týdnů\"},\n        \"weeks\": {\"past\": \"{0} týdny\", \"future\": [\"{0} týdny\", \"{0} týdnů\"]},\n        \"month\": {\"past\": \"měsícem\", \"future\": \"měsíc\", \"zero\": \"{0} měsíců\"},\n        \"months\": {\"past\": \"{0} měsíci\", \"future\": [\"{0} měsíce\", \"{0} měsíců\"]},\n        \"year\": {\"past\": \"rokem\", \"future\": \"rok\", \"zero\": \"{0} let\"},\n        \"years\": {\"past\": \"{0} lety\", \"future\": [\"{0} roky\", \"{0} let\"]},\n    }\n\n    past = \"Před {0}\"\n    future = \"Za {0}\"\n\n    month_names = [\n        \"\",\n        \"leden\",\n        \"únor\",\n        \"březen\",\n        \"duben\",\n        \"květen\",\n        \"červen\",\n        \"červenec\",\n        \"srpen\",\n        \"září\",\n        \"říjen\",\n        \"listopad\",\n        \"prosinec\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"led\",\n        \"úno\",\n        \"bře\",\n        \"dub\",\n        \"kvě\",\n        \"čvn\",\n        \"čvc\",\n        \"srp\",\n        \"zář\",\n        \"říj\",\n        \"lis\",\n        \"pro\",\n    ]\n\n    day_names = [\n        \"\",\n        \"pondělí\",\n        \"úterý\",\n        \"středa\",\n        \"čtvrtek\",\n        \"pátek\",\n        \"sobota\",\n        \"neděle\",\n    ]\n    day_abbreviations = [\"\", \"po\", \"út\", \"st\", \"čt\", \"pá\", \"so\", \"ne\"]\n\n    def _format_timeframe(self, timeframe, delta):\n        \"\"\"Czech aware time frame format function, takes into account\n        the differences between past and future forms.\"\"\"\n        form = self.timeframes[timeframe]\n        if isinstance(form, dict):\n            if delta == 0:\n                form = form[\"zero\"]  # And *never* use 0 in the singular!\n            elif delta > 0:\n                form = form[\"future\"]\n            else:\n                form = form[\"past\"]\n        delta = abs(delta)\n\n        if isinstance(form, list):\n            if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20):\n                form = form[0]\n            else:\n                form = form[1]\n\n        return form.format(delta)\n\n\nclass SlovakLocale(Locale):\n    names = [\"sk\", \"sk_sk\"]\n\n    timeframes = {\n        \"now\": \"Teraz\",\n        \"second\": {\"past\": \"sekundou\", \"future\": \"sekundu\", \"zero\": \"{0} sekúnd\"},\n        \"seconds\": {\"past\": \"{0} sekundami\", \"future\": [\"{0} sekundy\", \"{0} sekúnd\"]},\n        \"minute\": {\"past\": \"minútou\", \"future\": \"minútu\", \"zero\": \"{0} minút\"},\n        \"minutes\": {\"past\": \"{0} minútami\", \"future\": [\"{0} minúty\", \"{0} minút\"]},\n        \"hour\": {\"past\": \"hodinou\", \"future\": \"hodinu\", \"zero\": \"{0} hodín\"},\n        \"hours\": {\"past\": \"{0} hodinami\", \"future\": [\"{0} hodiny\", \"{0} hodín\"]},\n        \"day\": {\"past\": \"dňom\", \"future\": \"deň\", \"zero\": \"{0} dní\"},\n        \"days\": {\"past\": \"{0} dňami\", \"future\": [\"{0} dni\", \"{0} dní\"]},\n        \"week\": {\"past\": \"týždňom\", \"future\": \"týždeň\", \"zero\": \"{0} týždňov\"},\n        \"weeks\": {\"past\": \"{0} týždňami\", \"future\": [\"{0} týždne\", \"{0} týždňov\"]},\n        \"month\": {\"past\": \"mesiacom\", \"future\": \"mesiac\", \"zero\": \"{0} mesiacov\"},\n        \"months\": {\"past\": \"{0} mesiacmi\", \"future\": [\"{0} mesiace\", \"{0} mesiacov\"]},\n        \"year\": {\"past\": \"rokom\", \"future\": \"rok\", \"zero\": \"{0} rokov\"},\n        \"years\": {\"past\": \"{0} rokmi\", \"future\": [\"{0} roky\", \"{0} rokov\"]},\n    }\n\n    past = \"Pred {0}\"\n    future = \"O {0}\"\n    and_word = \"a\"\n\n    month_names = [\n        \"\",\n        \"január\",\n        \"február\",\n        \"marec\",\n        \"apríl\",\n        \"máj\",\n        \"jún\",\n        \"júl\",\n        \"august\",\n        \"september\",\n        \"október\",\n        \"november\",\n        \"december\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"jan\",\n        \"feb\",\n        \"mar\",\n        \"apr\",\n        \"máj\",\n        \"jún\",\n        \"júl\",\n        \"aug\",\n        \"sep\",\n        \"okt\",\n        \"nov\",\n        \"dec\",\n    ]\n\n    day_names = [\n        \"\",\n        \"pondelok\",\n        \"utorok\",\n        \"streda\",\n        \"štvrtok\",\n        \"piatok\",\n        \"sobota\",\n        \"nedeľa\",\n    ]\n    day_abbreviations = [\"\", \"po\", \"ut\", \"st\", \"št\", \"pi\", \"so\", \"ne\"]\n\n    def _format_timeframe(self, timeframe, delta):\n        \"\"\"Slovak aware time frame format function, takes into account\n        the differences between past and future forms.\"\"\"\n        form = self.timeframes[timeframe]\n        if isinstance(form, dict):\n            if delta == 0:\n                form = form[\"zero\"]  # And *never* use 0 in the singular!\n            elif delta > 0:\n                form = form[\"future\"]\n            else:\n                form = form[\"past\"]\n        delta = abs(delta)\n\n        if isinstance(form, list):\n            if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20):\n                form = form[0]\n            else:\n                form = form[1]\n\n        return form.format(delta)\n\n\nclass FarsiLocale(Locale):\n\n    names = [\"fa\", \"fa_ir\"]\n\n    past = \"{0} قبل\"\n    future = \"در {0}\"\n\n    timeframes = {\n        \"now\": \"اکنون\",\n        \"second\": \"یک لحظه\",\n        \"seconds\": \"{0} ثانیه\",\n        \"minute\": \"یک دقیقه\",\n        \"minutes\": \"{0} دقیقه\",\n        \"hour\": \"یک ساعت\",\n        \"hours\": \"{0} ساعت\",\n        \"day\": \"یک روز\",\n        \"days\": \"{0} روز\",\n        \"month\": \"یک ماه\",\n        \"months\": \"{0} ماه\",\n        \"year\": \"یک سال\",\n        \"years\": \"{0} سال\",\n    }\n\n    meridians = {\n        \"am\": \"قبل از ظهر\",\n        \"pm\": \"بعد از ظهر\",\n        \"AM\": \"قبل از ظهر\",\n        \"PM\": \"بعد از ظهر\",\n    }\n\n    month_names = [\n        \"\",\n        \"January\",\n        \"February\",\n        \"March\",\n        \"April\",\n        \"May\",\n        \"June\",\n        \"July\",\n        \"August\",\n        \"September\",\n        \"October\",\n        \"November\",\n        \"December\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"Jan\",\n        \"Feb\",\n        \"Mar\",\n        \"Apr\",\n        \"May\",\n        \"Jun\",\n        \"Jul\",\n        \"Aug\",\n        \"Sep\",\n        \"Oct\",\n        \"Nov\",\n        \"Dec\",\n    ]\n\n    day_names = [\n        \"\",\n        \"دو شنبه\",\n        \"سه شنبه\",\n        \"چهارشنبه\",\n        \"پنجشنبه\",\n        \"جمعه\",\n        \"شنبه\",\n        \"یکشنبه\",\n    ]\n    day_abbreviations = [\"\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\n\n\nclass HebrewLocale(Locale):\n\n    names = [\"he\", \"he_IL\"]\n\n    past = \"לפני {0}\"\n    future = \"בעוד {0}\"\n    and_word = \"ו\"\n\n    timeframes = {\n        \"now\": \"הרגע\",\n        \"second\": \"שנייה\",\n        \"seconds\": \"{0} שניות\",\n        \"minute\": \"דקה\",\n        \"minutes\": \"{0} דקות\",\n        \"hour\": \"שעה\",\n        \"hours\": \"{0} שעות\",\n        \"2-hours\": \"שעתיים\",\n        \"day\": \"יום\",\n        \"days\": \"{0} ימים\",\n        \"2-days\": \"יומיים\",\n        \"week\": \"שבוע\",\n        \"weeks\": \"{0} שבועות\",\n        \"2-weeks\": \"שבועיים\",\n        \"month\": \"חודש\",\n        \"months\": \"{0} חודשים\",\n        \"2-months\": \"חודשיים\",\n        \"year\": \"שנה\",\n        \"years\": \"{0} שנים\",\n        \"2-years\": \"שנתיים\",\n    }\n\n    meridians = {\n        \"am\": 'לפנ\"צ',\n        \"pm\": 'אחר\"צ',\n        \"AM\": \"לפני הצהריים\",\n        \"PM\": \"אחרי הצהריים\",\n    }\n\n    month_names = [\n        \"\",\n        \"ינואר\",\n        \"פברואר\",\n        \"מרץ\",\n        \"אפריל\",\n        \"מאי\",\n        \"יוני\",\n        \"יולי\",\n        \"אוגוסט\",\n        \"ספטמבר\",\n        \"אוקטובר\",\n        \"נובמבר\",\n        \"דצמבר\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"ינו׳\",\n        \"פבר׳\",\n        \"מרץ\",\n        \"אפר׳\",\n        \"מאי\",\n        \"יוני\",\n        \"יולי\",\n        \"אוג׳\",\n        \"ספט׳\",\n        \"אוק׳\",\n        \"נוב׳\",\n        \"דצמ׳\",\n    ]\n\n    day_names = [\"\", \"שני\", \"שלישי\", \"רביעי\", \"חמישי\", \"שישי\", \"שבת\", \"ראשון\"]\n    day_abbreviations = [\"\", \"ב׳\", \"ג׳\", \"ד׳\", \"ה׳\", \"ו׳\", \"ש׳\", \"א׳\"]\n\n    def _format_timeframe(self, timeframe, delta):\n        \"\"\"Hebrew couple of <timeframe> aware\"\"\"\n        couple = \"2-{}\".format(timeframe)\n        single = timeframe.rstrip(\"s\")\n        if abs(delta) == 2 and couple in self.timeframes:\n            key = couple\n        elif abs(delta) == 1 and single in self.timeframes:\n            key = single\n        else:\n            key = timeframe\n\n        return self.timeframes[key].format(trunc(abs(delta)))\n\n    def describe_multi(self, timeframes, only_distance=False):\n        \"\"\"Describes a delta within multiple timeframes in plain language.\n        In Hebrew, the and word behaves a bit differently.\n\n        :param timeframes: a list of string, quantity pairs each representing a timeframe and delta.\n        :param only_distance: return only distance eg: \"2 hours and 11 seconds\" without \"in\" or \"ago\" keywords\n        \"\"\"\n\n        humanized = \"\"\n        for index, (timeframe, delta) in enumerate(timeframes):\n            last_humanized = self._format_timeframe(timeframe, delta)\n            if index == 0:\n                humanized = last_humanized\n            elif index == len(timeframes) - 1:  # Must have at least 2 items\n                humanized += \" \" + self.and_word\n                if last_humanized[0].isdecimal():\n                    humanized += \"־\"\n                humanized += last_humanized\n            else:  # Don't add for the last one\n                humanized += \", \" + last_humanized\n\n        if not only_distance:\n            humanized = self._format_relative(humanized, timeframe, delta)\n\n        return humanized\n\n\nclass MarathiLocale(Locale):\n\n    names = [\"mr\"]\n\n    past = \"{0} आधी\"\n    future = \"{0} नंतर\"\n\n    timeframes = {\n        \"now\": \"सद्य\",\n        \"second\": \"एक सेकंद\",\n        \"seconds\": \"{0} सेकंद\",\n        \"minute\": \"एक मिनिट \",\n        \"minutes\": \"{0} मिनिट \",\n        \"hour\": \"एक तास\",\n        \"hours\": \"{0} तास\",\n        \"day\": \"एक दिवस\",\n        \"days\": \"{0} दिवस\",\n        \"month\": \"एक महिना \",\n        \"months\": \"{0} महिने \",\n        \"year\": \"एक वर्ष \",\n        \"years\": \"{0} वर्ष \",\n    }\n\n    meridians = {\"am\": \"सकाळ\", \"pm\": \"संध्याकाळ\", \"AM\": \"सकाळ\", \"PM\": \"संध्याकाळ\"}\n\n    month_names = [\n        \"\",\n        \"जानेवारी\",\n        \"फेब्रुवारी\",\n        \"मार्च\",\n        \"एप्रिल\",\n        \"मे\",\n        \"जून\",\n        \"जुलै\",\n        \"अॉगस्ट\",\n        \"सप्टेंबर\",\n        \"अॉक्टोबर\",\n        \"नोव्हेंबर\",\n        \"डिसेंबर\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"जान\",\n        \"फेब्रु\",\n        \"मार्च\",\n        \"एप्रि\",\n        \"मे\",\n        \"जून\",\n        \"जुलै\",\n        \"अॉग\",\n        \"सप्टें\",\n        \"अॉक्टो\",\n        \"नोव्हें\",\n        \"डिसें\",\n    ]\n\n    day_names = [\n        \"\",\n        \"सोमवार\",\n        \"मंगळवार\",\n        \"बुधवार\",\n        \"गुरुवार\",\n        \"शुक्रवार\",\n        \"शनिवार\",\n        \"रविवार\",\n    ]\n    day_abbreviations = [\"\", \"सोम\", \"मंगळ\", \"बुध\", \"गुरु\", \"शुक्र\", \"शनि\", \"रवि\"]\n\n\ndef _map_locales():\n\n    locales = {}\n\n    for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass):\n        if issubclass(cls, Locale):  # pragma: no branch\n            for name in cls.names:\n                locales[name.lower()] = cls\n\n    return locales\n\n\nclass CatalanLocale(Locale):\n    names = [\"ca\", \"ca_es\", \"ca_ad\", \"ca_fr\", \"ca_it\"]\n    past = \"Fa {0}\"\n    future = \"En {0}\"\n    and_word = \"i\"\n\n    timeframes = {\n        \"now\": \"Ara mateix\",\n        \"second\": \"un segon\",\n        \"seconds\": \"{0} segons\",\n        \"minute\": \"1 minut\",\n        \"minutes\": \"{0} minuts\",\n        \"hour\": \"una hora\",\n        \"hours\": \"{0} hores\",\n        \"day\": \"un dia\",\n        \"days\": \"{0} dies\",\n        \"month\": \"un mes\",\n        \"months\": \"{0} mesos\",\n        \"year\": \"un any\",\n        \"years\": \"{0} anys\",\n    }\n\n    month_names = [\n        \"\",\n        \"gener\",\n        \"febrer\",\n        \"març\",\n        \"abril\",\n        \"maig\",\n        \"juny\",\n        \"juliol\",\n        \"agost\",\n        \"setembre\",\n        \"octubre\",\n        \"novembre\",\n        \"desembre\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"gen.\",\n        \"febr.\",\n        \"març\",\n        \"abr.\",\n        \"maig\",\n        \"juny\",\n        \"jul.\",\n        \"ag.\",\n        \"set.\",\n        \"oct.\",\n        \"nov.\",\n        \"des.\",\n    ]\n    day_names = [\n        \"\",\n        \"dilluns\",\n        \"dimarts\",\n        \"dimecres\",\n        \"dijous\",\n        \"divendres\",\n        \"dissabte\",\n        \"diumenge\",\n    ]\n    day_abbreviations = [\n        \"\",\n        \"dl.\",\n        \"dt.\",\n        \"dc.\",\n        \"dj.\",\n        \"dv.\",\n        \"ds.\",\n        \"dg.\",\n    ]\n\n\nclass BasqueLocale(Locale):\n    names = [\"eu\", \"eu_eu\"]\n    past = \"duela {0}\"\n    future = \"{0}\"  # I don't know what's the right phrase in Basque for the future.\n\n    timeframes = {\n        \"now\": \"Orain\",\n        \"second\": \"segundo bat\",\n        \"seconds\": \"{0} segundu\",\n        \"minute\": \"minutu bat\",\n        \"minutes\": \"{0} minutu\",\n        \"hour\": \"ordu bat\",\n        \"hours\": \"{0} ordu\",\n        \"day\": \"egun bat\",\n        \"days\": \"{0} egun\",\n        \"month\": \"hilabete bat\",\n        \"months\": \"{0} hilabet\",\n        \"year\": \"urte bat\",\n        \"years\": \"{0} urte\",\n    }\n\n    month_names = [\n        \"\",\n        \"urtarrilak\",\n        \"otsailak\",\n        \"martxoak\",\n        \"apirilak\",\n        \"maiatzak\",\n        \"ekainak\",\n        \"uztailak\",\n        \"abuztuak\",\n        \"irailak\",\n        \"urriak\",\n        \"azaroak\",\n        \"abenduak\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"urt\",\n        \"ots\",\n        \"mar\",\n        \"api\",\n        \"mai\",\n        \"eka\",\n        \"uzt\",\n        \"abu\",\n        \"ira\",\n        \"urr\",\n        \"aza\",\n        \"abe\",\n    ]\n    day_names = [\n        \"\",\n        \"astelehena\",\n        \"asteartea\",\n        \"asteazkena\",\n        \"osteguna\",\n        \"ostirala\",\n        \"larunbata\",\n        \"igandea\",\n    ]\n    day_abbreviations = [\"\", \"al\", \"ar\", \"az\", \"og\", \"ol\", \"lr\", \"ig\"]\n\n\nclass HungarianLocale(Locale):\n\n    names = [\"hu\", \"hu_hu\"]\n\n    past = \"{0} ezelőtt\"\n    future = \"{0} múlva\"\n\n    timeframes = {\n        \"now\": \"éppen most\",\n        \"second\": {\"past\": \"egy második\", \"future\": \"egy második\"},\n        \"seconds\": {\"past\": \"{0} másodpercekkel\", \"future\": \"{0} pár másodperc\"},\n        \"minute\": {\"past\": \"egy perccel\", \"future\": \"egy perc\"},\n        \"minutes\": {\"past\": \"{0} perccel\", \"future\": \"{0} perc\"},\n        \"hour\": {\"past\": \"egy órával\", \"future\": \"egy óra\"},\n        \"hours\": {\"past\": \"{0} órával\", \"future\": \"{0} óra\"},\n        \"day\": {\"past\": \"egy nappal\", \"future\": \"egy nap\"},\n        \"days\": {\"past\": \"{0} nappal\", \"future\": \"{0} nap\"},\n        \"month\": {\"past\": \"egy hónappal\", \"future\": \"egy hónap\"},\n        \"months\": {\"past\": \"{0} hónappal\", \"future\": \"{0} hónap\"},\n        \"year\": {\"past\": \"egy évvel\", \"future\": \"egy év\"},\n        \"years\": {\"past\": \"{0} évvel\", \"future\": \"{0} év\"},\n    }\n\n    month_names = [\n        \"\",\n        \"január\",\n        \"február\",\n        \"március\",\n        \"április\",\n        \"május\",\n        \"június\",\n        \"július\",\n        \"augusztus\",\n        \"szeptember\",\n        \"október\",\n        \"november\",\n        \"december\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"jan\",\n        \"febr\",\n        \"márc\",\n        \"ápr\",\n        \"máj\",\n        \"jún\",\n        \"júl\",\n        \"aug\",\n        \"szept\",\n        \"okt\",\n        \"nov\",\n        \"dec\",\n    ]\n\n    day_names = [\n        \"\",\n        \"hétfő\",\n        \"kedd\",\n        \"szerda\",\n        \"csütörtök\",\n        \"péntek\",\n        \"szombat\",\n        \"vasárnap\",\n    ]\n    day_abbreviations = [\"\", \"hét\", \"kedd\", \"szer\", \"csüt\", \"pént\", \"szom\", \"vas\"]\n\n    meridians = {\"am\": \"de\", \"pm\": \"du\", \"AM\": \"DE\", \"PM\": \"DU\"}\n\n    def _format_timeframe(self, timeframe, delta):\n        form = self.timeframes[timeframe]\n\n        if isinstance(form, dict):\n            if delta > 0:\n                form = form[\"future\"]\n            else:\n                form = form[\"past\"]\n\n        return form.format(abs(delta))\n\n\nclass EsperantoLocale(Locale):\n    names = [\"eo\", \"eo_xx\"]\n    past = \"antaŭ {0}\"\n    future = \"post {0}\"\n\n    timeframes = {\n        \"now\": \"nun\",\n        \"second\": \"sekundo\",\n        \"seconds\": \"{0} kelkaj sekundoj\",\n        \"minute\": \"unu minuto\",\n        \"minutes\": \"{0} minutoj\",\n        \"hour\": \"un horo\",\n        \"hours\": \"{0} horoj\",\n        \"day\": \"unu tago\",\n        \"days\": \"{0} tagoj\",\n        \"month\": \"unu monato\",\n        \"months\": \"{0} monatoj\",\n        \"year\": \"unu jaro\",\n        \"years\": \"{0} jaroj\",\n    }\n\n    month_names = [\n        \"\",\n        \"januaro\",\n        \"februaro\",\n        \"marto\",\n        \"aprilo\",\n        \"majo\",\n        \"junio\",\n        \"julio\",\n        \"aŭgusto\",\n        \"septembro\",\n        \"oktobro\",\n        \"novembro\",\n        \"decembro\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"jan\",\n        \"feb\",\n        \"mar\",\n        \"apr\",\n        \"maj\",\n        \"jun\",\n        \"jul\",\n        \"aŭg\",\n        \"sep\",\n        \"okt\",\n        \"nov\",\n        \"dec\",\n    ]\n\n    day_names = [\n        \"\",\n        \"lundo\",\n        \"mardo\",\n        \"merkredo\",\n        \"ĵaŭdo\",\n        \"vendredo\",\n        \"sabato\",\n        \"dimanĉo\",\n    ]\n    day_abbreviations = [\"\", \"lun\", \"mar\", \"mer\", \"ĵaŭ\", \"ven\", \"sab\", \"dim\"]\n\n    meridians = {\"am\": \"atm\", \"pm\": \"ptm\", \"AM\": \"ATM\", \"PM\": \"PTM\"}\n\n    ordinal_day_re = r\"((?P<value>[1-3]?[0-9](?=a))a)\"\n\n    def _ordinal_number(self, n):\n        return \"{}a\".format(n)\n\n\nclass ThaiLocale(Locale):\n\n    names = [\"th\", \"th_th\"]\n\n    past = \"{0}{1}ที่ผ่านมา\"\n    future = \"ในอีก{1}{0}\"\n\n    timeframes = {\n        \"now\": \"ขณะนี้\",\n        \"second\": \"วินาที\",\n        \"seconds\": \"{0} ไม่กี่วินาที\",\n        \"minute\": \"1 นาที\",\n        \"minutes\": \"{0} นาที\",\n        \"hour\": \"1 ชั่วโมง\",\n        \"hours\": \"{0} ชั่วโมง\",\n        \"day\": \"1 วัน\",\n        \"days\": \"{0} วัน\",\n        \"month\": \"1 เดือน\",\n        \"months\": \"{0} เดือน\",\n        \"year\": \"1 ปี\",\n        \"years\": \"{0} ปี\",\n    }\n\n    month_names = [\n        \"\",\n        \"มกราคม\",\n        \"กุมภาพันธ์\",\n        \"มีนาคม\",\n        \"เมษายน\",\n        \"พฤษภาคม\",\n        \"มิถุนายน\",\n        \"กรกฎาคม\",\n        \"สิงหาคม\",\n        \"กันยายน\",\n        \"ตุลาคม\",\n        \"พฤศจิกายน\",\n        \"ธันวาคม\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"ม.ค.\",\n        \"ก.พ.\",\n        \"มี.ค.\",\n        \"เม.ย.\",\n        \"พ.ค.\",\n        \"มิ.ย.\",\n        \"ก.ค.\",\n        \"ส.ค.\",\n        \"ก.ย.\",\n        \"ต.ค.\",\n        \"พ.ย.\",\n        \"ธ.ค.\",\n    ]\n\n    day_names = [\"\", \"จันทร์\", \"อังคาร\", \"พุธ\", \"พฤหัสบดี\", \"ศุกร์\", \"เสาร์\", \"อาทิตย์\"]\n    day_abbreviations = [\"\", \"จ\", \"อ\", \"พ\", \"พฤ\", \"ศ\", \"ส\", \"อา\"]\n\n    meridians = {\"am\": \"am\", \"pm\": \"pm\", \"AM\": \"AM\", \"PM\": \"PM\"}\n\n    BE_OFFSET = 543\n\n    def year_full(self, year):\n        \"\"\"Thai always use Buddhist Era (BE) which is CE + 543\"\"\"\n        year += self.BE_OFFSET\n        return \"{:04d}\".format(year)\n\n    def year_abbreviation(self, year):\n        \"\"\"Thai always use Buddhist Era (BE) which is CE + 543\"\"\"\n        year += self.BE_OFFSET\n        return \"{:04d}\".format(year)[2:]\n\n    def _format_relative(self, humanized, timeframe, delta):\n        \"\"\"Thai normally doesn't have any space between words\"\"\"\n        if timeframe == \"now\":\n            return humanized\n        space = \"\" if timeframe == \"seconds\" else \" \"\n        direction = self.past if delta < 0 else self.future\n\n        return direction.format(humanized, space)\n\n\nclass BengaliLocale(Locale):\n\n    names = [\"bn\", \"bn_bd\", \"bn_in\"]\n\n    past = \"{0} আগে\"\n    future = \"{0} পরে\"\n\n    timeframes = {\n        \"now\": \"এখন\",\n        \"second\": \"একটি দ্বিতীয়\",\n        \"seconds\": \"{0} সেকেন্ড\",\n        \"minute\": \"এক মিনিট\",\n        \"minutes\": \"{0} মিনিট\",\n        \"hour\": \"এক ঘণ্টা\",\n        \"hours\": \"{0} ঘণ্টা\",\n        \"day\": \"এক দিন\",\n        \"days\": \"{0} দিন\",\n        \"month\": \"এক মাস\",\n        \"months\": \"{0} মাস \",\n        \"year\": \"এক বছর\",\n        \"years\": \"{0} বছর\",\n    }\n\n    meridians = {\"am\": \"সকাল\", \"pm\": \"বিকাল\", \"AM\": \"সকাল\", \"PM\": \"বিকাল\"}\n\n    month_names = [\n        \"\",\n        \"জানুয়ারি\",\n        \"ফেব্রুয়ারি\",\n        \"মার্চ\",\n        \"এপ্রিল\",\n        \"মে\",\n        \"জুন\",\n        \"জুলাই\",\n        \"আগস্ট\",\n        \"সেপ্টেম্বর\",\n        \"অক্টোবর\",\n        \"নভেম্বর\",\n        \"ডিসেম্বর\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"জানু\",\n        \"ফেব\",\n        \"মার্চ\",\n        \"এপ্রি\",\n        \"মে\",\n        \"জুন\",\n        \"জুল\",\n        \"অগা\",\n        \"সেপ্ট\",\n        \"অক্টো\",\n        \"নভে\",\n        \"ডিসে\",\n    ]\n\n    day_names = [\n        \"\",\n        \"সোমবার\",\n        \"মঙ্গলবার\",\n        \"বুধবার\",\n        \"বৃহস্পতিবার\",\n        \"শুক্রবার\",\n        \"শনিবার\",\n        \"রবিবার\",\n    ]\n    day_abbreviations = [\"\", \"সোম\", \"মঙ্গল\", \"বুধ\", \"বৃহঃ\", \"শুক্র\", \"শনি\", \"রবি\"]\n\n    def _ordinal_number(self, n):\n        if n > 10 or n == 0:\n            return \"{}তম\".format(n)\n        if n in [1, 5, 7, 8, 9, 10]:\n            return \"{}ম\".format(n)\n        if n in [2, 3]:\n            return \"{}য়\".format(n)\n        if n == 4:\n            return \"{}র্থ\".format(n)\n        if n == 6:\n            return \"{}ষ্ঠ\".format(n)\n\n\nclass RomanshLocale(Locale):\n\n    names = [\"rm\", \"rm_ch\"]\n\n    past = \"avant {0}\"\n    future = \"en {0}\"\n\n    timeframes = {\n        \"now\": \"en quest mument\",\n        \"second\": \"in secunda\",\n        \"seconds\": \"{0} secundas\",\n        \"minute\": \"ina minuta\",\n        \"minutes\": \"{0} minutas\",\n        \"hour\": \"in'ura\",\n        \"hours\": \"{0} ura\",\n        \"day\": \"in di\",\n        \"days\": \"{0} dis\",\n        \"month\": \"in mais\",\n        \"months\": \"{0} mais\",\n        \"year\": \"in onn\",\n        \"years\": \"{0} onns\",\n    }\n\n    month_names = [\n        \"\",\n        \"schaner\",\n        \"favrer\",\n        \"mars\",\n        \"avrigl\",\n        \"matg\",\n        \"zercladur\",\n        \"fanadur\",\n        \"avust\",\n        \"settember\",\n        \"october\",\n        \"november\",\n        \"december\",\n    ]\n\n    month_abbreviations = [\n        \"\",\n        \"schan\",\n        \"fav\",\n        \"mars\",\n        \"avr\",\n        \"matg\",\n        \"zer\",\n        \"fan\",\n        \"avu\",\n        \"set\",\n        \"oct\",\n        \"nov\",\n        \"dec\",\n    ]\n\n    day_names = [\n        \"\",\n        \"glindesdi\",\n        \"mardi\",\n        \"mesemna\",\n        \"gievgia\",\n        \"venderdi\",\n        \"sonda\",\n        \"dumengia\",\n    ]\n\n    day_abbreviations = [\"\", \"gli\", \"ma\", \"me\", \"gie\", \"ve\", \"so\", \"du\"]\n\n\nclass RomanianLocale(Locale):\n    names = [\"ro\", \"ro_ro\"]\n\n    past = \"{0} în urmă\"\n    future = \"peste {0}\"\n    and_word = \"și\"\n\n    timeframes = {\n        \"now\": \"acum\",\n        \"second\": \"o secunda\",\n        \"seconds\": \"{0} câteva secunde\",\n        \"minute\": \"un minut\",\n        \"minutes\": \"{0} minute\",\n        \"hour\": \"o oră\",\n        \"hours\": \"{0} ore\",\n        \"day\": \"o zi\",\n        \"days\": \"{0} zile\",\n        \"month\": \"o lună\",\n        \"months\": \"{0} luni\",\n        \"year\": \"un an\",\n        \"years\": \"{0} ani\",\n    }\n\n    month_names = [\n        \"\",\n        \"ianuarie\",\n        \"februarie\",\n        \"martie\",\n        \"aprilie\",\n        \"mai\",\n        \"iunie\",\n        \"iulie\",\n        \"august\",\n        \"septembrie\",\n        \"octombrie\",\n        \"noiembrie\",\n        \"decembrie\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"ian\",\n        \"febr\",\n        \"mart\",\n        \"apr\",\n        \"mai\",\n        \"iun\",\n        \"iul\",\n        \"aug\",\n        \"sept\",\n        \"oct\",\n        \"nov\",\n        \"dec\",\n    ]\n\n    day_names = [\n        \"\",\n        \"luni\",\n        \"marți\",\n        \"miercuri\",\n        \"joi\",\n        \"vineri\",\n        \"sâmbătă\",\n        \"duminică\",\n    ]\n    day_abbreviations = [\"\", \"Lun\", \"Mar\", \"Mie\", \"Joi\", \"Vin\", \"Sâm\", \"Dum\"]\n\n\nclass SlovenianLocale(Locale):\n    names = [\"sl\", \"sl_si\"]\n\n    past = \"pred {0}\"\n    future = \"čez {0}\"\n    and_word = \"in\"\n\n    timeframes = {\n        \"now\": \"zdaj\",\n        \"second\": \"sekundo\",\n        \"seconds\": \"{0} sekund\",\n        \"minute\": \"minuta\",\n        \"minutes\": \"{0} minutami\",\n        \"hour\": \"uro\",\n        \"hours\": \"{0} ur\",\n        \"day\": \"dan\",\n        \"days\": \"{0} dni\",\n        \"month\": \"mesec\",\n        \"months\": \"{0} mesecev\",\n        \"year\": \"leto\",\n        \"years\": \"{0} let\",\n    }\n\n    meridians = {\"am\": \"\", \"pm\": \"\", \"AM\": \"\", \"PM\": \"\"}\n\n    month_names = [\n        \"\",\n        \"Januar\",\n        \"Februar\",\n        \"Marec\",\n        \"April\",\n        \"Maj\",\n        \"Junij\",\n        \"Julij\",\n        \"Avgust\",\n        \"September\",\n        \"Oktober\",\n        \"November\",\n        \"December\",\n    ]\n\n    month_abbreviations = [\n        \"\",\n        \"Jan\",\n        \"Feb\",\n        \"Mar\",\n        \"Apr\",\n        \"Maj\",\n        \"Jun\",\n        \"Jul\",\n        \"Avg\",\n        \"Sep\",\n        \"Okt\",\n        \"Nov\",\n        \"Dec\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Ponedeljek\",\n        \"Torek\",\n        \"Sreda\",\n        \"Četrtek\",\n        \"Petek\",\n        \"Sobota\",\n        \"Nedelja\",\n    ]\n\n    day_abbreviations = [\"\", \"Pon\", \"Tor\", \"Sre\", \"Čet\", \"Pet\", \"Sob\", \"Ned\"]\n\n\nclass IndonesianLocale(Locale):\n\n    names = [\"id\", \"id_id\"]\n\n    past = \"{0} yang lalu\"\n    future = \"dalam {0}\"\n    and_word = \"dan\"\n\n    timeframes = {\n        \"now\": \"baru saja\",\n        \"second\": \"1 sebentar\",\n        \"seconds\": \"{0} detik\",\n        \"minute\": \"1 menit\",\n        \"minutes\": \"{0} menit\",\n        \"hour\": \"1 jam\",\n        \"hours\": \"{0} jam\",\n        \"day\": \"1 hari\",\n        \"days\": \"{0} hari\",\n        \"month\": \"1 bulan\",\n        \"months\": \"{0} bulan\",\n        \"year\": \"1 tahun\",\n        \"years\": \"{0} tahun\",\n    }\n\n    meridians = {\"am\": \"\", \"pm\": \"\", \"AM\": \"\", \"PM\": \"\"}\n\n    month_names = [\n        \"\",\n        \"Januari\",\n        \"Februari\",\n        \"Maret\",\n        \"April\",\n        \"Mei\",\n        \"Juni\",\n        \"Juli\",\n        \"Agustus\",\n        \"September\",\n        \"Oktober\",\n        \"November\",\n        \"Desember\",\n    ]\n\n    month_abbreviations = [\n        \"\",\n        \"Jan\",\n        \"Feb\",\n        \"Mar\",\n        \"Apr\",\n        \"Mei\",\n        \"Jun\",\n        \"Jul\",\n        \"Ags\",\n        \"Sept\",\n        \"Okt\",\n        \"Nov\",\n        \"Des\",\n    ]\n\n    day_names = [\"\", \"Senin\", \"Selasa\", \"Rabu\", \"Kamis\", \"Jumat\", \"Sabtu\", \"Minggu\"]\n\n    day_abbreviations = [\n        \"\",\n        \"Senin\",\n        \"Selasa\",\n        \"Rabu\",\n        \"Kamis\",\n        \"Jumat\",\n        \"Sabtu\",\n        \"Minggu\",\n    ]\n\n\nclass NepaliLocale(Locale):\n    names = [\"ne\", \"ne_np\"]\n\n    past = \"{0} पहिले\"\n    future = \"{0} पछी\"\n\n    timeframes = {\n        \"now\": \"अहिले\",\n        \"second\": \"एक सेकेन्ड\",\n        \"seconds\": \"{0} सेकण्ड\",\n        \"minute\": \"मिनेट\",\n        \"minutes\": \"{0} मिनेट\",\n        \"hour\": \"एक घण्टा\",\n        \"hours\": \"{0} घण्टा\",\n        \"day\": \"एक दिन\",\n        \"days\": \"{0} दिन\",\n        \"month\": \"एक महिना\",\n        \"months\": \"{0} महिना\",\n        \"year\": \"एक बर्ष\",\n        \"years\": \"बर्ष\",\n    }\n\n    meridians = {\"am\": \"पूर्वाह्न\", \"pm\": \"अपरान्ह\", \"AM\": \"पूर्वाह्न\", \"PM\": \"अपरान्ह\"}\n\n    month_names = [\n        \"\",\n        \"जनवरी\",\n        \"फेब्रुअरी\",\n        \"मार्च\",\n        \"एप्रील\",\n        \"मे\",\n        \"जुन\",\n        \"जुलाई\",\n        \"अगष्ट\",\n        \"सेप्टेम्बर\",\n        \"अक्टोबर\",\n        \"नोवेम्बर\",\n        \"डिसेम्बर\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"जन\",\n        \"फेब\",\n        \"मार्च\",\n        \"एप्रील\",\n        \"मे\",\n        \"जुन\",\n        \"जुलाई\",\n        \"अग\",\n        \"सेप\",\n        \"अक्ट\",\n        \"नोव\",\n        \"डिस\",\n    ]\n\n    day_names = [\n        \"\",\n        \"सोमवार\",\n        \"मंगलवार\",\n        \"बुधवार\",\n        \"बिहिवार\",\n        \"शुक्रवार\",\n        \"शनिवार\",\n        \"आइतवार\",\n    ]\n\n    day_abbreviations = [\"\", \"सोम\", \"मंगल\", \"बुध\", \"बिहि\", \"शुक्र\", \"शनि\", \"आइत\"]\n\n\nclass EstonianLocale(Locale):\n    names = [\"ee\", \"et\"]\n\n    past = \"{0} tagasi\"\n    future = \"{0} pärast\"\n    and_word = \"ja\"\n\n    timeframes = {\n        \"now\": {\"past\": \"just nüüd\", \"future\": \"just nüüd\"},\n        \"second\": {\"past\": \"üks sekund\", \"future\": \"ühe sekundi\"},\n        \"seconds\": {\"past\": \"{0} sekundit\", \"future\": \"{0} sekundi\"},\n        \"minute\": {\"past\": \"üks minut\", \"future\": \"ühe minuti\"},\n        \"minutes\": {\"past\": \"{0} minutit\", \"future\": \"{0} minuti\"},\n        \"hour\": {\"past\": \"tund aega\", \"future\": \"tunni aja\"},\n        \"hours\": {\"past\": \"{0} tundi\", \"future\": \"{0} tunni\"},\n        \"day\": {\"past\": \"üks päev\", \"future\": \"ühe päeva\"},\n        \"days\": {\"past\": \"{0} päeva\", \"future\": \"{0} päeva\"},\n        \"month\": {\"past\": \"üks kuu\", \"future\": \"ühe kuu\"},\n        \"months\": {\"past\": \"{0} kuud\", \"future\": \"{0} kuu\"},\n        \"year\": {\"past\": \"üks aasta\", \"future\": \"ühe aasta\"},\n        \"years\": {\"past\": \"{0} aastat\", \"future\": \"{0} aasta\"},\n    }\n\n    month_names = [\n        \"\",\n        \"Jaanuar\",\n        \"Veebruar\",\n        \"Märts\",\n        \"Aprill\",\n        \"Mai\",\n        \"Juuni\",\n        \"Juuli\",\n        \"August\",\n        \"September\",\n        \"Oktoober\",\n        \"November\",\n        \"Detsember\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"Jan\",\n        \"Veb\",\n        \"Mär\",\n        \"Apr\",\n        \"Mai\",\n        \"Jun\",\n        \"Jul\",\n        \"Aug\",\n        \"Sep\",\n        \"Okt\",\n        \"Nov\",\n        \"Dets\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Esmaspäev\",\n        \"Teisipäev\",\n        \"Kolmapäev\",\n        \"Neljapäev\",\n        \"Reede\",\n        \"Laupäev\",\n        \"Pühapäev\",\n    ]\n    day_abbreviations = [\"\", \"Esm\", \"Teis\", \"Kolm\", \"Nelj\", \"Re\", \"Lau\", \"Püh\"]\n\n    def _format_timeframe(self, timeframe, delta):\n        form = self.timeframes[timeframe]\n        if delta > 0:\n            form = form[\"future\"]\n        else:\n            form = form[\"past\"]\n        return form.format(abs(delta))\n\n\nclass SwahiliLocale(Locale):\n\n    names = [\n        \"sw\",\n        \"sw_ke\",\n        \"sw_tz\",\n    ]\n\n    past = \"{0} iliyopita\"\n    future = \"muda wa {0}\"\n    and_word = \"na\"\n\n    timeframes = {\n        \"now\": \"sasa hivi\",\n        \"second\": \"sekunde\",\n        \"seconds\": \"sekunde {0}\",\n        \"minute\": \"dakika moja\",\n        \"minutes\": \"dakika {0}\",\n        \"hour\": \"saa moja\",\n        \"hours\": \"saa {0}\",\n        \"day\": \"siku moja\",\n        \"days\": \"siku {0}\",\n        \"week\": \"wiki moja\",\n        \"weeks\": \"wiki {0}\",\n        \"month\": \"mwezi moja\",\n        \"months\": \"miezi {0}\",\n        \"year\": \"mwaka moja\",\n        \"years\": \"miaka {0}\",\n    }\n\n    meridians = {\"am\": \"asu\", \"pm\": \"mch\", \"AM\": \"ASU\", \"PM\": \"MCH\"}\n\n    month_names = [\n        \"\",\n        \"Januari\",\n        \"Februari\",\n        \"Machi\",\n        \"Aprili\",\n        \"Mei\",\n        \"Juni\",\n        \"Julai\",\n        \"Agosti\",\n        \"Septemba\",\n        \"Oktoba\",\n        \"Novemba\",\n        \"Desemba\",\n    ]\n    month_abbreviations = [\n        \"\",\n        \"Jan\",\n        \"Feb\",\n        \"Mac\",\n        \"Apr\",\n        \"Mei\",\n        \"Jun\",\n        \"Jul\",\n        \"Ago\",\n        \"Sep\",\n        \"Okt\",\n        \"Nov\",\n        \"Des\",\n    ]\n\n    day_names = [\n        \"\",\n        \"Jumatatu\",\n        \"Jumanne\",\n        \"Jumatano\",\n        \"Alhamisi\",\n        \"Ijumaa\",\n        \"Jumamosi\",\n        \"Jumapili\",\n    ]\n    day_abbreviations = [\n        \"\",\n        \"Jumatatu\",\n        \"Jumanne\",\n        \"Jumatano\",\n        \"Alhamisi\",\n        \"Ijumaa\",\n        \"Jumamosi\",\n        \"Jumapili\",\n    ]\n\n\n_locales = _map_locales()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/arrow/parser.py",
    "content": "# -*- coding: utf-8 -*-\nfrom __future__ import absolute_import, unicode_literals\n\nimport re\nfrom datetime import datetime, timedelta\n\nfrom dateutil import tz\n\nfrom arrow import locales\nfrom arrow.util import iso_to_gregorian, next_weekday, normalize_timestamp\n\ntry:\n    from functools import lru_cache\nexcept ImportError:  # pragma: no cover\n    from backports.functools_lru_cache import lru_cache  # pragma: no cover\n\n\nclass ParserError(ValueError):\n    pass\n\n\n# Allows for ParserErrors to be propagated from _build_datetime()\n# when day_of_year errors occur.\n# Before this, the ParserErrors were caught by the try/except in\n# _parse_multiformat() and the appropriate error message was not\n# transmitted to the user.\nclass ParserMatchError(ParserError):\n    pass\n\n\nclass DateTimeParser(object):\n\n    _FORMAT_RE = re.compile(\n        r\"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|x|X|W)\"\n    )\n    _ESCAPE_RE = re.compile(r\"\\[[^\\[\\]]*\\]\")\n\n    _ONE_OR_TWO_DIGIT_RE = re.compile(r\"\\d{1,2}\")\n    _ONE_OR_TWO_OR_THREE_DIGIT_RE = re.compile(r\"\\d{1,3}\")\n    _ONE_OR_MORE_DIGIT_RE = re.compile(r\"\\d+\")\n    _TWO_DIGIT_RE = re.compile(r\"\\d{2}\")\n    _THREE_DIGIT_RE = re.compile(r\"\\d{3}\")\n    _FOUR_DIGIT_RE = re.compile(r\"\\d{4}\")\n    _TZ_Z_RE = re.compile(r\"([\\+\\-])(\\d{2})(?:(\\d{2}))?|Z\")\n    _TZ_ZZ_RE = re.compile(r\"([\\+\\-])(\\d{2})(?:\\:(\\d{2}))?|Z\")\n    _TZ_NAME_RE = re.compile(r\"\\w[\\w+\\-/]+\")\n    # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will\n    # break cases like \"15 Jul 2000\" and a format list (see issue #447)\n    _TIMESTAMP_RE = re.compile(r\"^\\-?\\d+\\.?\\d+$\")\n    _TIMESTAMP_EXPANDED_RE = re.compile(r\"^\\-?\\d+$\")\n    _TIME_RE = re.compile(r\"^(\\d{2})(?:\\:?(\\d{2}))?(?:\\:?(\\d{2}))?(?:([\\.\\,])(\\d+))?$\")\n    _WEEK_DATE_RE = re.compile(r\"(?P<year>\\d{4})[\\-]?W(?P<week>\\d{2})[\\-]?(?P<day>\\d)?\")\n\n    _BASE_INPUT_RE_MAP = {\n        \"YYYY\": _FOUR_DIGIT_RE,\n        \"YY\": _TWO_DIGIT_RE,\n        \"MM\": _TWO_DIGIT_RE,\n        \"M\": _ONE_OR_TWO_DIGIT_RE,\n        \"DDDD\": _THREE_DIGIT_RE,\n        \"DDD\": _ONE_OR_TWO_OR_THREE_DIGIT_RE,\n        \"DD\": _TWO_DIGIT_RE,\n        \"D\": _ONE_OR_TWO_DIGIT_RE,\n        \"HH\": _TWO_DIGIT_RE,\n        \"H\": _ONE_OR_TWO_DIGIT_RE,\n        \"hh\": _TWO_DIGIT_RE,\n        \"h\": _ONE_OR_TWO_DIGIT_RE,\n        \"mm\": _TWO_DIGIT_RE,\n        \"m\": _ONE_OR_TWO_DIGIT_RE,\n        \"ss\": _TWO_DIGIT_RE,\n        \"s\": _ONE_OR_TWO_DIGIT_RE,\n        \"X\": _TIMESTAMP_RE,\n        \"x\": _TIMESTAMP_EXPANDED_RE,\n        \"ZZZ\": _TZ_NAME_RE,\n        \"ZZ\": _TZ_ZZ_RE,\n        \"Z\": _TZ_Z_RE,\n        \"S\": _ONE_OR_MORE_DIGIT_RE,\n        \"W\": _WEEK_DATE_RE,\n    }\n\n    SEPARATORS = [\"-\", \"/\", \".\"]\n\n    def __init__(self, locale=\"en_us\", cache_size=0):\n\n        self.locale = locales.get_locale(locale)\n        self._input_re_map = self._BASE_INPUT_RE_MAP.copy()\n        self._input_re_map.update(\n            {\n                \"MMMM\": self._generate_choice_re(\n                    self.locale.month_names[1:], re.IGNORECASE\n                ),\n                \"MMM\": self._generate_choice_re(\n                    self.locale.month_abbreviations[1:], re.IGNORECASE\n                ),\n                \"Do\": re.compile(self.locale.ordinal_day_re),\n                \"dddd\": self._generate_choice_re(\n                    self.locale.day_names[1:], re.IGNORECASE\n                ),\n                \"ddd\": self._generate_choice_re(\n                    self.locale.day_abbreviations[1:], re.IGNORECASE\n                ),\n                \"d\": re.compile(r\"[1-7]\"),\n                \"a\": self._generate_choice_re(\n                    (self.locale.meridians[\"am\"], self.locale.meridians[\"pm\"])\n                ),\n                # note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to\n                # ensure backwards compatibility of this token\n                \"A\": self._generate_choice_re(self.locale.meridians.values()),\n            }\n        )\n        if cache_size > 0:\n            self._generate_pattern_re = lru_cache(maxsize=cache_size)(\n                self._generate_pattern_re\n            )\n\n    # TODO: since we support more than ISO 8601, we should rename this function\n    # IDEA: break into multiple functions\n    def parse_iso(self, datetime_string, normalize_whitespace=False):\n\n        if normalize_whitespace:\n            datetime_string = re.sub(r\"\\s+\", \" \", datetime_string.strip())\n\n        has_space_divider = \" \" in datetime_string\n        has_t_divider = \"T\" in datetime_string\n\n        num_spaces = datetime_string.count(\" \")\n        if has_space_divider and num_spaces != 1 or has_t_divider and num_spaces > 0:\n            raise ParserError(\n                \"Expected an ISO 8601-like string, but was given '{}'. Try passing in a format string to resolve this.\".format(\n                    datetime_string\n                )\n            )\n\n        has_time = has_space_divider or has_t_divider\n        has_tz = False\n\n        # date formats (ISO 8601 and others) to test against\n        # NOTE: YYYYMM is omitted to avoid confusion with YYMMDD (no longer part of ISO 8601, but is still often used)\n        formats = [\n            \"YYYY-MM-DD\",\n            \"YYYY-M-DD\",\n            \"YYYY-M-D\",\n            \"YYYY/MM/DD\",\n            \"YYYY/M/DD\",\n            \"YYYY/M/D\",\n            \"YYYY.MM.DD\",\n            \"YYYY.M.DD\",\n            \"YYYY.M.D\",\n            \"YYYYMMDD\",\n            \"YYYY-DDDD\",\n            \"YYYYDDDD\",\n            \"YYYY-MM\",\n            \"YYYY/MM\",\n            \"YYYY.MM\",\n            \"YYYY\",\n            \"W\",\n        ]\n\n        if has_time:\n\n            if has_space_divider:\n                date_string, time_string = datetime_string.split(\" \", 1)\n            else:\n                date_string, time_string = datetime_string.split(\"T\", 1)\n\n            time_parts = re.split(r\"[\\+\\-Z]\", time_string, 1, re.IGNORECASE)\n\n            time_components = self._TIME_RE.match(time_parts[0])\n\n            if time_components is None:\n                raise ParserError(\n                    \"Invalid time component provided. Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format.\"\n                )\n\n            (\n                hours,\n                minutes,\n                seconds,\n                subseconds_sep,\n                subseconds,\n            ) = time_components.groups()\n\n            has_tz = len(time_parts) == 2\n            has_minutes = minutes is not None\n            has_seconds = seconds is not None\n            has_subseconds = subseconds is not None\n\n            is_basic_time_format = \":\" not in time_parts[0]\n            tz_format = \"Z\"\n\n            # use 'ZZ' token instead since tz offset is present in non-basic format\n            if has_tz and \":\" in time_parts[1]:\n                tz_format = \"ZZ\"\n\n            time_sep = \"\" if is_basic_time_format else \":\"\n\n            if has_subseconds:\n                time_string = \"HH{time_sep}mm{time_sep}ss{subseconds_sep}S\".format(\n                    time_sep=time_sep, subseconds_sep=subseconds_sep\n                )\n            elif has_seconds:\n                time_string = \"HH{time_sep}mm{time_sep}ss\".format(time_sep=time_sep)\n            elif has_minutes:\n                time_string = \"HH{time_sep}mm\".format(time_sep=time_sep)\n            else:\n                time_string = \"HH\"\n\n            if has_space_divider:\n                formats = [\"{} {}\".format(f, time_string) for f in formats]\n            else:\n                formats = [\"{}T{}\".format(f, time_string) for f in formats]\n\n        if has_time and has_tz:\n            # Add \"Z\" or \"ZZ\" to the format strings to indicate to\n            # _parse_token() that a timezone needs to be parsed\n            formats = [\"{}{}\".format(f, tz_format) for f in formats]\n\n        return self._parse_multiformat(datetime_string, formats)\n\n    def parse(self, datetime_string, fmt, normalize_whitespace=False):\n\n        if normalize_whitespace:\n            datetime_string = re.sub(r\"\\s+\", \" \", datetime_string)\n\n        if isinstance(fmt, list):\n            return self._parse_multiformat(datetime_string, fmt)\n\n        fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt)\n\n        match = fmt_pattern_re.search(datetime_string)\n\n        if match is None:\n            raise ParserMatchError(\n                \"Failed to match '{}' when parsing '{}'\".format(fmt, datetime_string)\n            )\n\n        parts = {}\n        for token in fmt_tokens:\n            if token == \"Do\":\n                value = match.group(\"value\")\n            elif token == \"W\":\n                value = (match.group(\"year\"), match.group(\"week\"), match.group(\"day\"))\n            else:\n                value = match.group(token)\n            self._parse_token(token, value, parts)\n\n        return self._build_datetime(parts)\n\n    def _generate_pattern_re(self, fmt):\n\n        # fmt is a string of tokens like 'YYYY-MM-DD'\n        # we construct a new string by replacing each\n        # token by its pattern:\n        # 'YYYY-MM-DD' -> '(?P<YYYY>\\d{4})-(?P<MM>\\d{2})-(?P<DD>\\d{2})'\n        tokens = []\n        offset = 0\n\n        # Escape all special RegEx chars\n        escaped_fmt = re.escape(fmt)\n\n        # Extract the bracketed expressions to be reinserted later.\n        escaped_fmt = re.sub(self._ESCAPE_RE, \"#\", escaped_fmt)\n\n        # Any number of S is the same as one.\n        # TODO: allow users to specify the number of digits to parse\n        escaped_fmt = re.sub(r\"S+\", \"S\", escaped_fmt)\n\n        escaped_data = re.findall(self._ESCAPE_RE, fmt)\n\n        fmt_pattern = escaped_fmt\n\n        for m in self._FORMAT_RE.finditer(escaped_fmt):\n            token = m.group(0)\n            try:\n                input_re = self._input_re_map[token]\n            except KeyError:\n                raise ParserError(\"Unrecognized token '{}'\".format(token))\n            input_pattern = \"(?P<{}>{})\".format(token, input_re.pattern)\n            tokens.append(token)\n            # a pattern doesn't have the same length as the token\n            # it replaces! We keep the difference in the offset variable.\n            # This works because the string is scanned left-to-right and matches\n            # are returned in the order found by finditer.\n            fmt_pattern = (\n                fmt_pattern[: m.start() + offset]\n                + input_pattern\n                + fmt_pattern[m.end() + offset :]\n            )\n            offset += len(input_pattern) - (m.end() - m.start())\n\n        final_fmt_pattern = \"\"\n        split_fmt = fmt_pattern.split(r\"\\#\")\n\n        # Due to the way Python splits, 'split_fmt' will always be longer\n        for i in range(len(split_fmt)):\n            final_fmt_pattern += split_fmt[i]\n            if i < len(escaped_data):\n                final_fmt_pattern += escaped_data[i][1:-1]\n\n        # Wrap final_fmt_pattern in a custom word boundary to strictly\n        # match the formatting pattern and filter out date and time formats\n        # that include junk such as: blah1998-09-12 blah, blah 1998-09-12blah,\n        # blah1998-09-12blah. The custom word boundary matches every character\n        # that is not a whitespace character to allow for searching for a date\n        # and time string in a natural language sentence. Therefore, searching\n        # for a string of the form YYYY-MM-DD in \"blah 1998-09-12 blah\" will\n        # work properly.\n        # Certain punctuation before or after the target pattern such as\n        # \"1998-09-12,\" is permitted. For the full list of valid punctuation,\n        # see the documentation.\n\n        starting_word_boundary = (\n            r\"(?<!\\S\\S)\"  # Don't have two consecutive non-whitespace characters. This ensures that we allow cases like .11.25.2019 but not 1.11.25.2019 (for pattern MM.DD.YYYY)\n            r\"(?<![^\\,\\.\\;\\:\\?\\!\\\"\\'\\`\\[\\]\\{\\}\\(\\)<>\\s])\"  # This is the list of punctuation that is ok before the pattern (i.e. \"It can't not be these characters before the pattern\")\n            r\"(\\b|^)\"  # The \\b is to block cases like 1201912 but allow 201912 for pattern YYYYMM. The ^ was necessary to allow a negative number through i.e. before epoch numbers\n        )\n        ending_word_boundary = (\n            r\"(?=[\\,\\.\\;\\:\\?\\!\\\"\\'\\`\\[\\]\\{\\}\\(\\)\\<\\>]?\"  # Positive lookahead stating that these punctuation marks can appear after the pattern at most 1 time\n            r\"(?!\\S))\"  # Don't allow any non-whitespace character after the punctuation\n        )\n        bounded_fmt_pattern = r\"{}{}{}\".format(\n            starting_word_boundary, final_fmt_pattern, ending_word_boundary\n        )\n\n        return tokens, re.compile(bounded_fmt_pattern, flags=re.IGNORECASE)\n\n    def _parse_token(self, token, value, parts):\n\n        if token == \"YYYY\":\n            parts[\"year\"] = int(value)\n\n        elif token == \"YY\":\n            value = int(value)\n            parts[\"year\"] = 1900 + value if value > 68 else 2000 + value\n\n        elif token in [\"MMMM\", \"MMM\"]:\n            parts[\"month\"] = self.locale.month_number(value.lower())\n\n        elif token in [\"MM\", \"M\"]:\n            parts[\"month\"] = int(value)\n\n        elif token in [\"DDDD\", \"DDD\"]:\n            parts[\"day_of_year\"] = int(value)\n\n        elif token in [\"DD\", \"D\"]:\n            parts[\"day\"] = int(value)\n\n        elif token == \"Do\":\n            parts[\"day\"] = int(value)\n\n        elif token == \"dddd\":\n            # locale day names are 1-indexed\n            day_of_week = [x.lower() for x in self.locale.day_names].index(\n                value.lower()\n            )\n            parts[\"day_of_week\"] = day_of_week - 1\n\n        elif token == \"ddd\":\n            # locale day abbreviations are 1-indexed\n            day_of_week = [x.lower() for x in self.locale.day_abbreviations].index(\n                value.lower()\n            )\n            parts[\"day_of_week\"] = day_of_week - 1\n\n        elif token.upper() in [\"HH\", \"H\"]:\n            parts[\"hour\"] = int(value)\n\n        elif token in [\"mm\", \"m\"]:\n            parts[\"minute\"] = int(value)\n\n        elif token in [\"ss\", \"s\"]:\n            parts[\"second\"] = int(value)\n\n        elif token == \"S\":\n            # We have the *most significant* digits of an arbitrary-precision integer.\n            # We want the six most significant digits as an integer, rounded.\n            # IDEA: add nanosecond support somehow? Need datetime support for it first.\n            value = value.ljust(7, str(\"0\"))\n\n            # floating-point (IEEE-754) defaults to half-to-even rounding\n            seventh_digit = int(value[6])\n            if seventh_digit == 5:\n                rounding = int(value[5]) % 2\n            elif seventh_digit > 5:\n                rounding = 1\n            else:\n                rounding = 0\n\n            parts[\"microsecond\"] = int(value[:6]) + rounding\n\n        elif token == \"X\":\n            parts[\"timestamp\"] = float(value)\n\n        elif token == \"x\":\n            parts[\"expanded_timestamp\"] = int(value)\n\n        elif token in [\"ZZZ\", \"ZZ\", \"Z\"]:\n            parts[\"tzinfo\"] = TzinfoParser.parse(value)\n\n        elif token in [\"a\", \"A\"]:\n            if value in (self.locale.meridians[\"am\"], self.locale.meridians[\"AM\"]):\n                parts[\"am_pm\"] = \"am\"\n            elif value in (self.locale.meridians[\"pm\"], self.locale.meridians[\"PM\"]):\n                parts[\"am_pm\"] = \"pm\"\n\n        elif token == \"W\":\n            parts[\"weekdate\"] = value\n\n    @staticmethod\n    def _build_datetime(parts):\n\n        weekdate = parts.get(\"weekdate\")\n\n        if weekdate is not None:\n            # we can use strptime (%G, %V, %u) in python 3.6 but these tokens aren't available before that\n            year, week = int(weekdate[0]), int(weekdate[1])\n\n            if weekdate[2] is not None:\n                day = int(weekdate[2])\n            else:\n                # day not given, default to 1\n                day = 1\n\n            dt = iso_to_gregorian(year, week, day)\n            parts[\"year\"] = dt.year\n            parts[\"month\"] = dt.month\n            parts[\"day\"] = dt.day\n\n        timestamp = parts.get(\"timestamp\")\n\n        if timestamp is not None:\n            return datetime.fromtimestamp(timestamp, tz=tz.tzutc())\n\n        expanded_timestamp = parts.get(\"expanded_timestamp\")\n\n        if expanded_timestamp is not None:\n            return datetime.fromtimestamp(\n                normalize_timestamp(expanded_timestamp),\n                tz=tz.tzutc(),\n            )\n\n        day_of_year = parts.get(\"day_of_year\")\n\n        if day_of_year is not None:\n            year = parts.get(\"year\")\n            month = parts.get(\"month\")\n            if year is None:\n                raise ParserError(\n                    \"Year component is required with the DDD and DDDD tokens.\"\n                )\n\n            if month is not None:\n                raise ParserError(\n                    \"Month component is not allowed with the DDD and DDDD tokens.\"\n                )\n\n            date_string = \"{}-{}\".format(year, day_of_year)\n            try:\n                dt = datetime.strptime(date_string, \"%Y-%j\")\n            except ValueError:\n                raise ParserError(\n                    \"The provided day of year '{}' is invalid.\".format(day_of_year)\n                )\n\n            parts[\"year\"] = dt.year\n            parts[\"month\"] = dt.month\n            parts[\"day\"] = dt.day\n\n        day_of_week = parts.get(\"day_of_week\")\n        day = parts.get(\"day\")\n\n        # If day is passed, ignore day of week\n        if day_of_week is not None and day is None:\n            year = parts.get(\"year\", 1970)\n            month = parts.get(\"month\", 1)\n            day = 1\n\n            # dddd => first day of week after epoch\n            # dddd YYYY => first day of week in specified year\n            # dddd MM YYYY => first day of week in specified year and month\n            # dddd MM => first day after epoch in specified month\n            next_weekday_dt = next_weekday(datetime(year, month, day), day_of_week)\n            parts[\"year\"] = next_weekday_dt.year\n            parts[\"month\"] = next_weekday_dt.month\n            parts[\"day\"] = next_weekday_dt.day\n\n        am_pm = parts.get(\"am_pm\")\n        hour = parts.get(\"hour\", 0)\n\n        if am_pm == \"pm\" and hour < 12:\n            hour += 12\n        elif am_pm == \"am\" and hour == 12:\n            hour = 0\n\n        # Support for midnight at the end of day\n        if hour == 24:\n            if parts.get(\"minute\", 0) != 0:\n                raise ParserError(\"Midnight at the end of day must not contain minutes\")\n            if parts.get(\"second\", 0) != 0:\n                raise ParserError(\"Midnight at the end of day must not contain seconds\")\n            if parts.get(\"microsecond\", 0) != 0:\n                raise ParserError(\n                    \"Midnight at the end of day must not contain microseconds\"\n                )\n            hour = 0\n            day_increment = 1\n        else:\n            day_increment = 0\n\n        # account for rounding up to 1000000\n        microsecond = parts.get(\"microsecond\", 0)\n        if microsecond == 1000000:\n            microsecond = 0\n            second_increment = 1\n        else:\n            second_increment = 0\n\n        increment = timedelta(days=day_increment, seconds=second_increment)\n\n        return (\n            datetime(\n                year=parts.get(\"year\", 1),\n                month=parts.get(\"month\", 1),\n                day=parts.get(\"day\", 1),\n                hour=hour,\n                minute=parts.get(\"minute\", 0),\n                second=parts.get(\"second\", 0),\n                microsecond=microsecond,\n                tzinfo=parts.get(\"tzinfo\"),\n            )\n            + increment\n        )\n\n    def _parse_multiformat(self, string, formats):\n\n        _datetime = None\n\n        for fmt in formats:\n            try:\n                _datetime = self.parse(string, fmt)\n                break\n            except ParserMatchError:\n                pass\n\n        if _datetime is None:\n            raise ParserError(\n                \"Could not match input '{}' to any of the following formats: {}\".format(\n                    string, \", \".join(formats)\n                )\n            )\n\n        return _datetime\n\n    # generates a capture group of choices separated by an OR operator\n    @staticmethod\n    def _generate_choice_re(choices, flags=0):\n        return re.compile(r\"({})\".format(\"|\".join(choices)), flags=flags)\n\n\nclass TzinfoParser(object):\n    _TZINFO_RE = re.compile(r\"^([\\+\\-])?(\\d{2})(?:\\:?(\\d{2}))?$\")\n\n    @classmethod\n    def parse(cls, tzinfo_string):\n\n        tzinfo = None\n\n        if tzinfo_string == \"local\":\n            tzinfo = tz.tzlocal()\n\n        elif tzinfo_string in [\"utc\", \"UTC\", \"Z\"]:\n            tzinfo = tz.tzutc()\n\n        else:\n\n            iso_match = cls._TZINFO_RE.match(tzinfo_string)\n\n            if iso_match:\n                sign, hours, minutes = iso_match.groups()\n                if minutes is None:\n                    minutes = 0\n                seconds = int(hours) * 3600 + int(minutes) * 60\n\n                if sign == \"-\":\n                    seconds *= -1\n\n                tzinfo = tz.tzoffset(None, seconds)\n\n            else:\n                tzinfo = tz.gettz(tzinfo_string)\n\n        if tzinfo is None:\n            raise ParserError(\n                'Could not parse timezone expression \"{}\"'.format(tzinfo_string)\n            )\n\n        return tzinfo\n"
  },
  {
    "path": "openpype/vendor/python/python_2/arrow/util.py",
    "content": "# -*- coding: utf-8 -*-\nfrom __future__ import absolute_import\n\nimport datetime\nimport numbers\n\nfrom dateutil.rrule import WEEKLY, rrule\n\nfrom arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US\n\n\ndef next_weekday(start_date, weekday):\n    \"\"\"Get next weekday from the specified start date.\n\n    :param start_date: Datetime object representing the start date.\n    :param weekday: Next weekday to obtain. Can be a value between 0 (Monday) and 6 (Sunday).\n    :return: Datetime object corresponding to the next weekday after start_date.\n\n    Usage::\n\n        # Get first Monday after epoch\n        >>> next_weekday(datetime(1970, 1, 1), 0)\n        1970-01-05 00:00:00\n\n        # Get first Thursday after epoch\n        >>> next_weekday(datetime(1970, 1, 1), 3)\n        1970-01-01 00:00:00\n\n        # Get first Sunday after epoch\n        >>> next_weekday(datetime(1970, 1, 1), 6)\n        1970-01-04 00:00:00\n    \"\"\"\n    if weekday < 0 or weekday > 6:\n        raise ValueError(\"Weekday must be between 0 (Monday) and 6 (Sunday).\")\n    return rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0]\n\n\ndef total_seconds(td):\n    \"\"\"Get total seconds for timedelta.\"\"\"\n    return td.total_seconds()\n\n\ndef is_timestamp(value):\n    \"\"\"Check if value is a valid timestamp.\"\"\"\n    if isinstance(value, bool):\n        return False\n    if not (\n        isinstance(value, numbers.Integral)\n        or isinstance(value, float)\n        or isinstance(value, str)\n    ):\n        return False\n    try:\n        float(value)\n        return True\n    except ValueError:\n        return False\n\n\ndef normalize_timestamp(timestamp):\n    \"\"\"Normalize millisecond and microsecond timestamps into normal timestamps.\"\"\"\n    if timestamp > MAX_TIMESTAMP:\n        if timestamp < MAX_TIMESTAMP_MS:\n            timestamp /= 1e3\n        elif timestamp < MAX_TIMESTAMP_US:\n            timestamp /= 1e6\n        else:\n            raise ValueError(\n                \"The specified timestamp '{}' is too large.\".format(timestamp)\n            )\n    return timestamp\n\n\n# Credit to https://stackoverflow.com/a/1700069\ndef iso_to_gregorian(iso_year, iso_week, iso_day):\n    \"\"\"Converts an ISO week date tuple into a datetime object.\"\"\"\n\n    if not 1 <= iso_week <= 53:\n        raise ValueError(\"ISO Calendar week value must be between 1-53.\")\n\n    if not 1 <= iso_day <= 7:\n        raise ValueError(\"ISO Calendar day value must be between 1-7\")\n\n    # The first week of the year always contains 4 Jan.\n    fourth_jan = datetime.date(iso_year, 1, 4)\n    delta = datetime.timedelta(fourth_jan.isoweekday() - 1)\n    year_start = fourth_jan - delta\n    gregorian = year_start + datetime.timedelta(days=iso_day - 1, weeks=iso_week - 1)\n\n    return gregorian\n\n\ndef validate_bounds(bounds):\n    if bounds != \"()\" and bounds != \"(]\" and bounds != \"[)\" and bounds != \"[]\":\n        raise ValueError(\n            'Invalid bounds. Please select between \"()\", \"(]\", \"[)\", or \"[]\".'\n        )\n\n\n# Python 2.7 / 3.0+ definitions for isstr function.\n\ntry:  # pragma: no cover\n    basestring\n\n    def isstr(s):\n        return isinstance(s, basestring)  # noqa: F821\n\n\nexcept NameError:  # pragma: no cover\n\n    def isstr(s):\n        return isinstance(s, str)\n\n\n__all__ = [\"next_weekday\", \"total_seconds\", \"is_timestamp\", \"isstr\", \"iso_to_gregorian\"]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom __future__ import absolute_import, division, print_function\n\nimport sys\n\nfrom functools import partial\n\nfrom . import converters, exceptions, filters, setters, validators\nfrom ._cmp import cmp_using\nfrom ._config import get_run_validators, set_run_validators\nfrom ._funcs import asdict, assoc, astuple, evolve, has, resolve_types\nfrom ._make import (\n    NOTHING,\n    Attribute,\n    Factory,\n    attrib,\n    attrs,\n    fields,\n    fields_dict,\n    make_class,\n    validate,\n)\nfrom ._version_info import VersionInfo\n\n\n__version__ = \"21.4.0\"\n__version_info__ = VersionInfo._from_version_string(__version__)\n\n__title__ = \"attrs\"\n__description__ = \"Classes Without Boilerplate\"\n__url__ = \"https://www.attrs.org/\"\n__uri__ = __url__\n__doc__ = __description__ + \" <\" + __uri__ + \">\"\n\n__author__ = \"Hynek Schlawack\"\n__email__ = \"hs@ox.cx\"\n\n__license__ = \"MIT\"\n__copyright__ = \"Copyright (c) 2015 Hynek Schlawack\"\n\n\ns = attributes = attrs\nib = attr = attrib\ndataclass = partial(attrs, auto_attribs=True)  # happy Easter ;)\n\n__all__ = [\n    \"Attribute\",\n    \"Factory\",\n    \"NOTHING\",\n    \"asdict\",\n    \"assoc\",\n    \"astuple\",\n    \"attr\",\n    \"attrib\",\n    \"attributes\",\n    \"attrs\",\n    \"cmp_using\",\n    \"converters\",\n    \"evolve\",\n    \"exceptions\",\n    \"fields\",\n    \"fields_dict\",\n    \"filters\",\n    \"get_run_validators\",\n    \"has\",\n    \"ib\",\n    \"make_class\",\n    \"resolve_types\",\n    \"s\",\n    \"set_run_validators\",\n    \"setters\",\n    \"validate\",\n    \"validators\",\n]\n\nif sys.version_info[:2] >= (3, 6):\n    from ._next_gen import define, field, frozen, mutable  # noqa: F401\n\n    __all__.extend((\"define\", \"field\", \"frozen\", \"mutable\"))\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/__init__.pyi",
    "content": "import sys\n\nfrom typing import (\n    Any,\n    Callable,\n    Dict,\n    Generic,\n    List,\n    Mapping,\n    Optional,\n    Sequence,\n    Tuple,\n    Type,\n    TypeVar,\n    Union,\n    overload,\n)\n\n# `import X as X` is required to make these public\nfrom . import converters as converters\nfrom . import exceptions as exceptions\nfrom . import filters as filters\nfrom . import setters as setters\nfrom . import validators as validators\nfrom ._version_info import VersionInfo\n\n__version__: str\n__version_info__: VersionInfo\n__title__: str\n__description__: str\n__url__: str\n__uri__: str\n__author__: str\n__email__: str\n__license__: str\n__copyright__: str\n\n_T = TypeVar(\"_T\")\n_C = TypeVar(\"_C\", bound=type)\n\n_EqOrderType = Union[bool, Callable[[Any], Any]]\n_ValidatorType = Callable[[Any, Attribute[_T], _T], Any]\n_ConverterType = Callable[[Any], Any]\n_FilterType = Callable[[Attribute[_T], _T], bool]\n_ReprType = Callable[[Any], str]\n_ReprArgType = Union[bool, _ReprType]\n_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any]\n_OnSetAttrArgType = Union[\n    _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType\n]\n_FieldTransformer = Callable[\n    [type, List[Attribute[Any]]], List[Attribute[Any]]\n]\n_CompareWithType = Callable[[Any, Any], bool]\n# FIXME: in reality, if multiple validators are passed they must be in a list\n# or tuple, but those are invariant and so would prevent subtypes of\n# _ValidatorType from working when passed in a list or tuple.\n_ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]]\n\n# _make --\n\nNOTHING: object\n\n# NOTE: Factory lies about its return type to make this possible:\n# `x: List[int] # = Factory(list)`\n# Work around mypy issue #4554 in the common case by using an overload.\nif sys.version_info >= (3, 8):\n    from typing import Literal\n    @overload\n    def Factory(factory: Callable[[], _T]) -> _T: ...\n    @overload\n    def Factory(\n        factory: Callable[[Any], _T],\n        takes_self: Literal[True],\n    ) -> _T: ...\n    @overload\n    def Factory(\n        factory: Callable[[], _T],\n        takes_self: Literal[False],\n    ) -> _T: ...\n\nelse:\n    @overload\n    def Factory(factory: Callable[[], _T]) -> _T: ...\n    @overload\n    def Factory(\n        factory: Union[Callable[[Any], _T], Callable[[], _T]],\n        takes_self: bool = ...,\n    ) -> _T: ...\n\n# Static type inference support via __dataclass_transform__ implemented as per:\n# https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md\n# This annotation must be applied to all overloads of \"define\" and \"attrs\"\n#\n# NOTE: This is a typing construct and does not exist at runtime.  Extensions\n# wrapping attrs decorators should declare a separate __dataclass_transform__\n# signature in the extension module using the specification linked above to\n# provide pyright support.\ndef __dataclass_transform__(\n    *,\n    eq_default: bool = True,\n    order_default: bool = False,\n    kw_only_default: bool = False,\n    field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()),\n) -> Callable[[_T], _T]: ...\n\nclass Attribute(Generic[_T]):\n    name: str\n    default: Optional[_T]\n    validator: Optional[_ValidatorType[_T]]\n    repr: _ReprArgType\n    cmp: _EqOrderType\n    eq: _EqOrderType\n    order: _EqOrderType\n    hash: Optional[bool]\n    init: bool\n    converter: Optional[_ConverterType]\n    metadata: Dict[Any, Any]\n    type: Optional[Type[_T]]\n    kw_only: bool\n    on_setattr: _OnSetAttrType\n    def evolve(self, **changes: Any) -> \"Attribute[Any]\": ...\n\n# NOTE: We had several choices for the annotation to use for type arg:\n# 1) Type[_T]\n#   - Pros: Handles simple cases correctly\n#   - Cons: Might produce less informative errors in the case of conflicting\n#     TypeVars e.g. `attr.ib(default='bad', type=int)`\n# 2) Callable[..., _T]\n#   - Pros: Better error messages than #1 for conflicting TypeVars\n#   - Cons: Terrible error messages for validator checks.\n#   e.g. attr.ib(type=int, validator=validate_str)\n#        -> error: Cannot infer function type argument\n# 3) type (and do all of the work in the mypy plugin)\n#   - Pros: Simple here, and we could customize the plugin with our own errors.\n#   - Cons: Would need to write mypy plugin code to handle all the cases.\n# We chose option #1.\n\n# `attr` lies about its return type to make the following possible:\n#     attr()    -> Any\n#     attr(8)   -> int\n#     attr(validator=<some callable>)  -> Whatever the callable expects.\n# This makes this type of assignments possible:\n#     x: int = attr(8)\n#\n# This form catches explicit None or no default but with no other arguments\n# returns Any.\n@overload\ndef attrib(\n    default: None = ...,\n    validator: None = ...,\n    repr: _ReprArgType = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    type: None = ...,\n    converter: None = ...,\n    factory: None = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> Any: ...\n\n# This form catches an explicit None or no default and infers the type from the\n# other arguments.\n@overload\ndef attrib(\n    default: None = ...,\n    validator: Optional[_ValidatorArgType[_T]] = ...,\n    repr: _ReprArgType = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    type: Optional[Type[_T]] = ...,\n    converter: Optional[_ConverterType] = ...,\n    factory: Optional[Callable[[], _T]] = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> _T: ...\n\n# This form catches an explicit default argument.\n@overload\ndef attrib(\n    default: _T,\n    validator: Optional[_ValidatorArgType[_T]] = ...,\n    repr: _ReprArgType = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    type: Optional[Type[_T]] = ...,\n    converter: Optional[_ConverterType] = ...,\n    factory: Optional[Callable[[], _T]] = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> _T: ...\n\n# This form covers type=non-Type: e.g. forward references (str), Any\n@overload\ndef attrib(\n    default: Optional[_T] = ...,\n    validator: Optional[_ValidatorArgType[_T]] = ...,\n    repr: _ReprArgType = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    type: object = ...,\n    converter: Optional[_ConverterType] = ...,\n    factory: Optional[Callable[[], _T]] = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> Any: ...\n@overload\ndef field(\n    *,\n    default: None = ...,\n    validator: None = ...,\n    repr: _ReprArgType = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    converter: None = ...,\n    factory: None = ...,\n    kw_only: bool = ...,\n    eq: Optional[bool] = ...,\n    order: Optional[bool] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> Any: ...\n\n# This form catches an explicit None or no default and infers the type from the\n# other arguments.\n@overload\ndef field(\n    *,\n    default: None = ...,\n    validator: Optional[_ValidatorArgType[_T]] = ...,\n    repr: _ReprArgType = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    converter: Optional[_ConverterType] = ...,\n    factory: Optional[Callable[[], _T]] = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> _T: ...\n\n# This form catches an explicit default argument.\n@overload\ndef field(\n    *,\n    default: _T,\n    validator: Optional[_ValidatorArgType[_T]] = ...,\n    repr: _ReprArgType = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    converter: Optional[_ConverterType] = ...,\n    factory: Optional[Callable[[], _T]] = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> _T: ...\n\n# This form covers type=non-Type: e.g. forward references (str), Any\n@overload\ndef field(\n    *,\n    default: Optional[_T] = ...,\n    validator: Optional[_ValidatorArgType[_T]] = ...,\n    repr: _ReprArgType = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    metadata: Optional[Mapping[Any, Any]] = ...,\n    converter: Optional[_ConverterType] = ...,\n    factory: Optional[Callable[[], _T]] = ...,\n    kw_only: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n) -> Any: ...\n@overload\n@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))\ndef attrs(\n    maybe_cls: _C,\n    these: Optional[Dict[str, Any]] = ...,\n    repr_ns: Optional[str] = ...,\n    repr: bool = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    slots: bool = ...,\n    frozen: bool = ...,\n    weakref_slot: bool = ...,\n    str: bool = ...,\n    auto_attribs: bool = ...,\n    kw_only: bool = ...,\n    cache_hash: bool = ...,\n    auto_exc: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    auto_detect: bool = ...,\n    collect_by_mro: bool = ...,\n    getstate_setstate: Optional[bool] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n    field_transformer: Optional[_FieldTransformer] = ...,\n    match_args: bool = ...,\n) -> _C: ...\n@overload\n@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))\ndef attrs(\n    maybe_cls: None = ...,\n    these: Optional[Dict[str, Any]] = ...,\n    repr_ns: Optional[str] = ...,\n    repr: bool = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    slots: bool = ...,\n    frozen: bool = ...,\n    weakref_slot: bool = ...,\n    str: bool = ...,\n    auto_attribs: bool = ...,\n    kw_only: bool = ...,\n    cache_hash: bool = ...,\n    auto_exc: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    auto_detect: bool = ...,\n    collect_by_mro: bool = ...,\n    getstate_setstate: Optional[bool] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n    field_transformer: Optional[_FieldTransformer] = ...,\n    match_args: bool = ...,\n) -> Callable[[_C], _C]: ...\n@overload\n@__dataclass_transform__(field_descriptors=(attrib, field))\ndef define(\n    maybe_cls: _C,\n    *,\n    these: Optional[Dict[str, Any]] = ...,\n    repr: bool = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    slots: bool = ...,\n    frozen: bool = ...,\n    weakref_slot: bool = ...,\n    str: bool = ...,\n    auto_attribs: bool = ...,\n    kw_only: bool = ...,\n    cache_hash: bool = ...,\n    auto_exc: bool = ...,\n    eq: Optional[bool] = ...,\n    order: Optional[bool] = ...,\n    auto_detect: bool = ...,\n    getstate_setstate: Optional[bool] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n    field_transformer: Optional[_FieldTransformer] = ...,\n    match_args: bool = ...,\n) -> _C: ...\n@overload\n@__dataclass_transform__(field_descriptors=(attrib, field))\ndef define(\n    maybe_cls: None = ...,\n    *,\n    these: Optional[Dict[str, Any]] = ...,\n    repr: bool = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    slots: bool = ...,\n    frozen: bool = ...,\n    weakref_slot: bool = ...,\n    str: bool = ...,\n    auto_attribs: bool = ...,\n    kw_only: bool = ...,\n    cache_hash: bool = ...,\n    auto_exc: bool = ...,\n    eq: Optional[bool] = ...,\n    order: Optional[bool] = ...,\n    auto_detect: bool = ...,\n    getstate_setstate: Optional[bool] = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n    field_transformer: Optional[_FieldTransformer] = ...,\n    match_args: bool = ...,\n) -> Callable[[_C], _C]: ...\n\nmutable = define\nfrozen = define  # they differ only in their defaults\n\n# TODO: add support for returning NamedTuple from the mypy plugin\nclass _Fields(Tuple[Attribute[Any], ...]):\n    def __getattr__(self, name: str) -> Attribute[Any]: ...\n\ndef fields(cls: type) -> _Fields: ...\ndef fields_dict(cls: type) -> Dict[str, Attribute[Any]]: ...\ndef validate(inst: Any) -> None: ...\ndef resolve_types(\n    cls: _C,\n    globalns: Optional[Dict[str, Any]] = ...,\n    localns: Optional[Dict[str, Any]] = ...,\n    attribs: Optional[List[Attribute[Any]]] = ...,\n) -> _C: ...\n\n# TODO: add support for returning a proper attrs class from the mypy plugin\n# we use Any instead of _CountingAttr so that e.g. `make_class('Foo',\n# [attr.ib()])` is valid\ndef make_class(\n    name: str,\n    attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]],\n    bases: Tuple[type, ...] = ...,\n    repr_ns: Optional[str] = ...,\n    repr: bool = ...,\n    cmp: Optional[_EqOrderType] = ...,\n    hash: Optional[bool] = ...,\n    init: bool = ...,\n    slots: bool = ...,\n    frozen: bool = ...,\n    weakref_slot: bool = ...,\n    str: bool = ...,\n    auto_attribs: bool = ...,\n    kw_only: bool = ...,\n    cache_hash: bool = ...,\n    auto_exc: bool = ...,\n    eq: Optional[_EqOrderType] = ...,\n    order: Optional[_EqOrderType] = ...,\n    collect_by_mro: bool = ...,\n    on_setattr: Optional[_OnSetAttrArgType] = ...,\n    field_transformer: Optional[_FieldTransformer] = ...,\n) -> type: ...\n\n# _funcs --\n\n# TODO: add support for returning TypedDict from the mypy plugin\n# FIXME: asdict/astuple do not honor their factory args. Waiting on one of\n# these:\n# https://github.com/python/mypy/issues/4236\n# https://github.com/python/typing/issues/253\n# XXX: remember to fix attrs.asdict/astuple too!\ndef asdict(\n    inst: Any,\n    recurse: bool = ...,\n    filter: Optional[_FilterType[Any]] = ...,\n    dict_factory: Type[Mapping[Any, Any]] = ...,\n    retain_collection_types: bool = ...,\n    value_serializer: Optional[\n        Callable[[type, Attribute[Any], Any], Any]\n    ] = ...,\n    tuple_keys: Optional[bool] = ...,\n) -> Dict[str, Any]: ...\n\n# TODO: add support for returning NamedTuple from the mypy plugin\ndef astuple(\n    inst: Any,\n    recurse: bool = ...,\n    filter: Optional[_FilterType[Any]] = ...,\n    tuple_factory: Type[Sequence[Any]] = ...,\n    retain_collection_types: bool = ...,\n) -> Tuple[Any, ...]: ...\ndef has(cls: type) -> bool: ...\ndef assoc(inst: _T, **changes: Any) -> _T: ...\ndef evolve(inst: _T, **changes: Any) -> _T: ...\n\n# _config --\n\ndef set_run_validators(run: bool) -> None: ...\ndef get_run_validators() -> bool: ...\n\n# aliases --\n\ns = attributes = attrs\nib = attr = attrib\ndataclass = attrs  # Technically, partial(attrs, auto_attribs=True) ;)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/_cmp.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom __future__ import absolute_import, division, print_function\n\nimport functools\n\nfrom ._compat import new_class\nfrom ._make import _make_ne\n\n\n_operation_names = {\"eq\": \"==\", \"lt\": \"<\", \"le\": \"<=\", \"gt\": \">\", \"ge\": \">=\"}\n\n\ndef cmp_using(\n    eq=None,\n    lt=None,\n    le=None,\n    gt=None,\n    ge=None,\n    require_same_type=True,\n    class_name=\"Comparable\",\n):\n    \"\"\"\n    Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and\n    ``cmp`` arguments to customize field comparison.\n\n    The resulting class will have a full set of ordering methods if\n    at least one of ``{lt, le, gt, ge}`` and ``eq``  are provided.\n\n    :param Optional[callable] eq: `callable` used to evaluate equality\n        of two objects.\n    :param Optional[callable] lt: `callable` used to evaluate whether\n        one object is less than another object.\n    :param Optional[callable] le: `callable` used to evaluate whether\n        one object is less than or equal to another object.\n    :param Optional[callable] gt: `callable` used to evaluate whether\n        one object is greater than another object.\n    :param Optional[callable] ge: `callable` used to evaluate whether\n        one object is greater than or equal to another object.\n\n    :param bool require_same_type: When `True`, equality and ordering methods\n        will return `NotImplemented` if objects are not of the same type.\n\n    :param Optional[str] class_name: Name of class. Defaults to 'Comparable'.\n\n    See `comparison` for more details.\n\n    .. versionadded:: 21.1.0\n    \"\"\"\n\n    body = {\n        \"__slots__\": [\"value\"],\n        \"__init__\": _make_init(),\n        \"_requirements\": [],\n        \"_is_comparable_to\": _is_comparable_to,\n    }\n\n    # Add operations.\n    num_order_functions = 0\n    has_eq_function = False\n\n    if eq is not None:\n        has_eq_function = True\n        body[\"__eq__\"] = _make_operator(\"eq\", eq)\n        body[\"__ne__\"] = _make_ne()\n\n    if lt is not None:\n        num_order_functions += 1\n        body[\"__lt__\"] = _make_operator(\"lt\", lt)\n\n    if le is not None:\n        num_order_functions += 1\n        body[\"__le__\"] = _make_operator(\"le\", le)\n\n    if gt is not None:\n        num_order_functions += 1\n        body[\"__gt__\"] = _make_operator(\"gt\", gt)\n\n    if ge is not None:\n        num_order_functions += 1\n        body[\"__ge__\"] = _make_operator(\"ge\", ge)\n\n    type_ = new_class(class_name, (object,), {}, lambda ns: ns.update(body))\n\n    # Add same type requirement.\n    if require_same_type:\n        type_._requirements.append(_check_same_type)\n\n    # Add total ordering if at least one operation was defined.\n    if 0 < num_order_functions < 4:\n        if not has_eq_function:\n            # functools.total_ordering requires __eq__ to be defined,\n            # so raise early error here to keep a nice stack.\n            raise ValueError(\n                \"eq must be define is order to complete ordering from \"\n                \"lt, le, gt, ge.\"\n            )\n        type_ = functools.total_ordering(type_)\n\n    return type_\n\n\ndef _make_init():\n    \"\"\"\n    Create __init__ method.\n    \"\"\"\n\n    def __init__(self, value):\n        \"\"\"\n        Initialize object with *value*.\n        \"\"\"\n        self.value = value\n\n    return __init__\n\n\ndef _make_operator(name, func):\n    \"\"\"\n    Create operator method.\n    \"\"\"\n\n    def method(self, other):\n        if not self._is_comparable_to(other):\n            return NotImplemented\n\n        result = func(self.value, other.value)\n        if result is NotImplemented:\n            return NotImplemented\n\n        return result\n\n    method.__name__ = \"__%s__\" % (name,)\n    method.__doc__ = \"Return a %s b.  Computed by attrs.\" % (\n        _operation_names[name],\n    )\n\n    return method\n\n\ndef _is_comparable_to(self, other):\n    \"\"\"\n    Check whether `other` is comparable to `self`.\n    \"\"\"\n    for func in self._requirements:\n        if not func(self, other):\n            return False\n    return True\n\n\ndef _check_same_type(self, other):\n    \"\"\"\n    Return True if *self* and *other* are of the same type, False otherwise.\n    \"\"\"\n    return other.value.__class__ is self.value.__class__\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/_cmp.pyi",
    "content": "from typing import Type\n\nfrom . import _CompareWithType\n\ndef cmp_using(\n    eq: Optional[_CompareWithType],\n    lt: Optional[_CompareWithType],\n    le: Optional[_CompareWithType],\n    gt: Optional[_CompareWithType],\n    ge: Optional[_CompareWithType],\n    require_same_type: bool,\n    class_name: str,\n) -> Type: ...\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/_compat.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom __future__ import absolute_import, division, print_function\n\nimport platform\nimport sys\nimport threading\nimport types\nimport warnings\n\n\nPY2 = sys.version_info[0] == 2\nPYPY = platform.python_implementation() == \"PyPy\"\nPY36 = sys.version_info[:2] >= (3, 6)\nHAS_F_STRINGS = PY36\nPY310 = sys.version_info[:2] >= (3, 10)\n\n\nif PYPY or PY36:\n    ordered_dict = dict\nelse:\n    from collections import OrderedDict\n\n    ordered_dict = OrderedDict\n\n\nif PY2:\n    from collections import Mapping, Sequence\n\n    from UserDict import IterableUserDict\n\n    # We 'bundle' isclass instead of using inspect as importing inspect is\n    # fairly expensive (order of 10-15 ms for a modern machine in 2016)\n    def isclass(klass):\n        return isinstance(klass, (type, types.ClassType))\n\n    def new_class(name, bases, kwds, exec_body):\n        \"\"\"\n        A minimal stub of types.new_class that we need for make_class.\n        \"\"\"\n        ns = {}\n        exec_body(ns)\n\n        return type(name, bases, ns)\n\n    # TYPE is used in exceptions, repr(int) is different on Python 2 and 3.\n    TYPE = \"type\"\n\n    def iteritems(d):\n        return d.iteritems()\n\n    # Python 2 is bereft of a read-only dict proxy, so we make one!\n    class ReadOnlyDict(IterableUserDict):\n        \"\"\"\n        Best-effort read-only dict wrapper.\n        \"\"\"\n\n        def __setitem__(self, key, val):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise TypeError(\n                \"'mappingproxy' object does not support item assignment\"\n            )\n\n        def update(self, _):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise AttributeError(\n                \"'mappingproxy' object has no attribute 'update'\"\n            )\n\n        def __delitem__(self, _):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise TypeError(\n                \"'mappingproxy' object does not support item deletion\"\n            )\n\n        def clear(self):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise AttributeError(\n                \"'mappingproxy' object has no attribute 'clear'\"\n            )\n\n        def pop(self, key, default=None):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise AttributeError(\n                \"'mappingproxy' object has no attribute 'pop'\"\n            )\n\n        def popitem(self):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise AttributeError(\n                \"'mappingproxy' object has no attribute 'popitem'\"\n            )\n\n        def setdefault(self, key, default=None):\n            # We gently pretend we're a Python 3 mappingproxy.\n            raise AttributeError(\n                \"'mappingproxy' object has no attribute 'setdefault'\"\n            )\n\n        def __repr__(self):\n            # Override to be identical to the Python 3 version.\n            return \"mappingproxy(\" + repr(self.data) + \")\"\n\n    def metadata_proxy(d):\n        res = ReadOnlyDict()\n        res.data.update(d)  # We blocked update, so we have to do it like this.\n        return res\n\n    def just_warn(*args, **kw):  # pragma: no cover\n        \"\"\"\n        We only warn on Python 3 because we are not aware of any concrete\n        consequences of not setting the cell on Python 2.\n        \"\"\"\n\nelse:  # Python 3 and later.\n    from collections.abc import Mapping, Sequence  # noqa\n\n    def just_warn(*args, **kw):\n        \"\"\"\n        We only warn on Python 3 because we are not aware of any concrete\n        consequences of not setting the cell on Python 2.\n        \"\"\"\n        warnings.warn(\n            \"Running interpreter doesn't sufficiently support code object \"\n            \"introspection.  Some features like bare super() or accessing \"\n            \"__class__ will not work with slotted classes.\",\n            RuntimeWarning,\n            stacklevel=2,\n        )\n\n    def isclass(klass):\n        return isinstance(klass, type)\n\n    TYPE = \"class\"\n\n    def iteritems(d):\n        return d.items()\n\n    new_class = types.new_class\n\n    def metadata_proxy(d):\n        return types.MappingProxyType(dict(d))\n\n\ndef make_set_closure_cell():\n    \"\"\"Return a function of two arguments (cell, value) which sets\n    the value stored in the closure cell `cell` to `value`.\n    \"\"\"\n    # pypy makes this easy. (It also supports the logic below, but\n    # why not do the easy/fast thing?)\n    if PYPY:\n\n        def set_closure_cell(cell, value):\n            cell.__setstate__((value,))\n\n        return set_closure_cell\n\n    # Otherwise gotta do it the hard way.\n\n    # Create a function that will set its first cellvar to `value`.\n    def set_first_cellvar_to(value):\n        x = value\n        return\n\n        # This function will be eliminated as dead code, but\n        # not before its reference to `x` forces `x` to be\n        # represented as a closure cell rather than a local.\n        def force_x_to_be_a_cell():  # pragma: no cover\n            return x\n\n    try:\n        # Extract the code object and make sure our assumptions about\n        # the closure behavior are correct.\n        if PY2:\n            co = set_first_cellvar_to.func_code\n        else:\n            co = set_first_cellvar_to.__code__\n        if co.co_cellvars != (\"x\",) or co.co_freevars != ():\n            raise AssertionError  # pragma: no cover\n\n        # Convert this code object to a code object that sets the\n        # function's first _freevar_ (not cellvar) to the argument.\n        if sys.version_info >= (3, 8):\n            # CPython 3.8+ has an incompatible CodeType signature\n            # (added a posonlyargcount argument) but also added\n            # CodeType.replace() to do this without counting parameters.\n            set_first_freevar_code = co.replace(\n                co_cellvars=co.co_freevars, co_freevars=co.co_cellvars\n            )\n        else:\n            args = [co.co_argcount]\n            if not PY2:\n                args.append(co.co_kwonlyargcount)\n            args.extend(\n                [\n                    co.co_nlocals,\n                    co.co_stacksize,\n                    co.co_flags,\n                    co.co_code,\n                    co.co_consts,\n                    co.co_names,\n                    co.co_varnames,\n                    co.co_filename,\n                    co.co_name,\n                    co.co_firstlineno,\n                    co.co_lnotab,\n                    # These two arguments are reversed:\n                    co.co_cellvars,\n                    co.co_freevars,\n                ]\n            )\n            set_first_freevar_code = types.CodeType(*args)\n\n        def set_closure_cell(cell, value):\n            # Create a function using the set_first_freevar_code,\n            # whose first closure cell is `cell`. Calling it will\n            # change the value of that cell.\n            setter = types.FunctionType(\n                set_first_freevar_code, {}, \"setter\", (), (cell,)\n            )\n            # And call it to set the cell.\n            setter(value)\n\n        # Make sure it works on this interpreter:\n        def make_func_with_cell():\n            x = None\n\n            def func():\n                return x  # pragma: no cover\n\n            return func\n\n        if PY2:\n            cell = make_func_with_cell().func_closure[0]\n        else:\n            cell = make_func_with_cell().__closure__[0]\n        set_closure_cell(cell, 100)\n        if cell.cell_contents != 100:\n            raise AssertionError  # pragma: no cover\n\n    except Exception:\n        return just_warn\n    else:\n        return set_closure_cell\n\n\nset_closure_cell = make_set_closure_cell()\n\n# Thread-local global to track attrs instances which are already being repr'd.\n# This is needed because there is no other (thread-safe) way to pass info\n# about the instances that are already being repr'd through the call stack\n# in order to ensure we don't perform infinite recursion.\n#\n# For instance, if an instance contains a dict which contains that instance,\n# we need to know that we're already repr'ing the outside instance from within\n# the dict's repr() call.\n#\n# This lives here rather than in _make.py so that the functions in _make.py\n# don't have a direct reference to the thread-local in their globals dict.\n# If they have such a reference, it breaks cloudpickle.\nrepr_context = threading.local()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/_config.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom __future__ import absolute_import, division, print_function\n\n\n__all__ = [\"set_run_validators\", \"get_run_validators\"]\n\n_run_validators = True\n\n\ndef set_run_validators(run):\n    \"\"\"\n    Set whether or not validators are run.  By default, they are run.\n\n    .. deprecated:: 21.3.0 It will not be removed, but it also will not be\n        moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()`\n        instead.\n    \"\"\"\n    if not isinstance(run, bool):\n        raise TypeError(\"'run' must be bool.\")\n    global _run_validators\n    _run_validators = run\n\n\ndef get_run_validators():\n    \"\"\"\n    Return whether or not validators are run.\n\n    .. deprecated:: 21.3.0 It will not be removed, but it also will not be\n        moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()`\n        instead.\n    \"\"\"\n    return _run_validators\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/_funcs.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom __future__ import absolute_import, division, print_function\n\nimport copy\n\nfrom ._compat import iteritems\nfrom ._make import NOTHING, _obj_setattr, fields\nfrom .exceptions import AttrsAttributeNotFoundError\n\n\ndef asdict(\n    inst,\n    recurse=True,\n    filter=None,\n    dict_factory=dict,\n    retain_collection_types=False,\n    value_serializer=None,\n):\n    \"\"\"\n    Return the ``attrs`` attribute values of *inst* as a dict.\n\n    Optionally recurse into other ``attrs``-decorated classes.\n\n    :param inst: Instance of an ``attrs``-decorated class.\n    :param bool recurse: Recurse into classes that are also\n        ``attrs``-decorated.\n    :param callable filter: A callable whose return code determines whether an\n        attribute or element is included (``True``) or dropped (``False``).  Is\n        called with the `attrs.Attribute` as the first argument and the\n        value as the second argument.\n    :param callable dict_factory: A callable to produce dictionaries from.  For\n        example, to produce ordered dictionaries instead of normal Python\n        dictionaries, pass in ``collections.OrderedDict``.\n    :param bool retain_collection_types: Do not convert to ``list`` when\n        encountering an attribute whose type is ``tuple`` or ``set``.  Only\n        meaningful if ``recurse`` is ``True``.\n    :param Optional[callable] value_serializer: A hook that is called for every\n        attribute or dict key/value.  It receives the current instance, field\n        and value and must return the (updated) value.  The hook is run *after*\n        the optional *filter* has been applied.\n\n    :rtype: return type of *dict_factory*\n\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class.\n\n    ..  versionadded:: 16.0.0 *dict_factory*\n    ..  versionadded:: 16.1.0 *retain_collection_types*\n    ..  versionadded:: 20.3.0 *value_serializer*\n    ..  versionadded:: 21.3.0 If a dict has a collection for a key, it is\n        serialized as a tuple.\n    \"\"\"\n    attrs = fields(inst.__class__)\n    rv = dict_factory()\n    for a in attrs:\n        v = getattr(inst, a.name)\n        if filter is not None and not filter(a, v):\n            continue\n\n        if value_serializer is not None:\n            v = value_serializer(inst, a, v)\n\n        if recurse is True:\n            if has(v.__class__):\n                rv[a.name] = asdict(\n                    v,\n                    recurse=True,\n                    filter=filter,\n                    dict_factory=dict_factory,\n                    retain_collection_types=retain_collection_types,\n                    value_serializer=value_serializer,\n                )\n            elif isinstance(v, (tuple, list, set, frozenset)):\n                cf = v.__class__ if retain_collection_types is True else list\n                rv[a.name] = cf(\n                    [\n                        _asdict_anything(\n                            i,\n                            is_key=False,\n                            filter=filter,\n                            dict_factory=dict_factory,\n                            retain_collection_types=retain_collection_types,\n                            value_serializer=value_serializer,\n                        )\n                        for i in v\n                    ]\n                )\n            elif isinstance(v, dict):\n                df = dict_factory\n                rv[a.name] = df(\n                    (\n                        _asdict_anything(\n                            kk,\n                            is_key=True,\n                            filter=filter,\n                            dict_factory=df,\n                            retain_collection_types=retain_collection_types,\n                            value_serializer=value_serializer,\n                        ),\n                        _asdict_anything(\n                            vv,\n                            is_key=False,\n                            filter=filter,\n                            dict_factory=df,\n                            retain_collection_types=retain_collection_types,\n                            value_serializer=value_serializer,\n                        ),\n                    )\n                    for kk, vv in iteritems(v)\n                )\n            else:\n                rv[a.name] = v\n        else:\n            rv[a.name] = v\n    return rv\n\n\ndef _asdict_anything(\n    val,\n    is_key,\n    filter,\n    dict_factory,\n    retain_collection_types,\n    value_serializer,\n):\n    \"\"\"\n    ``asdict`` only works on attrs instances, this works on anything.\n    \"\"\"\n    if getattr(val.__class__, \"__attrs_attrs__\", None) is not None:\n        # Attrs class.\n        rv = asdict(\n            val,\n            recurse=True,\n            filter=filter,\n            dict_factory=dict_factory,\n            retain_collection_types=retain_collection_types,\n            value_serializer=value_serializer,\n        )\n    elif isinstance(val, (tuple, list, set, frozenset)):\n        if retain_collection_types is True:\n            cf = val.__class__\n        elif is_key:\n            cf = tuple\n        else:\n            cf = list\n\n        rv = cf(\n            [\n                _asdict_anything(\n                    i,\n                    is_key=False,\n                    filter=filter,\n                    dict_factory=dict_factory,\n                    retain_collection_types=retain_collection_types,\n                    value_serializer=value_serializer,\n                )\n                for i in val\n            ]\n        )\n    elif isinstance(val, dict):\n        df = dict_factory\n        rv = df(\n            (\n                _asdict_anything(\n                    kk,\n                    is_key=True,\n                    filter=filter,\n                    dict_factory=df,\n                    retain_collection_types=retain_collection_types,\n                    value_serializer=value_serializer,\n                ),\n                _asdict_anything(\n                    vv,\n                    is_key=False,\n                    filter=filter,\n                    dict_factory=df,\n                    retain_collection_types=retain_collection_types,\n                    value_serializer=value_serializer,\n                ),\n            )\n            for kk, vv in iteritems(val)\n        )\n    else:\n        rv = val\n        if value_serializer is not None:\n            rv = value_serializer(None, None, rv)\n\n    return rv\n\n\ndef astuple(\n    inst,\n    recurse=True,\n    filter=None,\n    tuple_factory=tuple,\n    retain_collection_types=False,\n):\n    \"\"\"\n    Return the ``attrs`` attribute values of *inst* as a tuple.\n\n    Optionally recurse into other ``attrs``-decorated classes.\n\n    :param inst: Instance of an ``attrs``-decorated class.\n    :param bool recurse: Recurse into classes that are also\n        ``attrs``-decorated.\n    :param callable filter: A callable whose return code determines whether an\n        attribute or element is included (``True``) or dropped (``False``).  Is\n        called with the `attrs.Attribute` as the first argument and the\n        value as the second argument.\n    :param callable tuple_factory: A callable to produce tuples from.  For\n        example, to produce lists instead of tuples.\n    :param bool retain_collection_types: Do not convert to ``list``\n        or ``dict`` when encountering an attribute which type is\n        ``tuple``, ``dict`` or ``set``.  Only meaningful if ``recurse`` is\n        ``True``.\n\n    :rtype: return type of *tuple_factory*\n\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class.\n\n    ..  versionadded:: 16.2.0\n    \"\"\"\n    attrs = fields(inst.__class__)\n    rv = []\n    retain = retain_collection_types  # Very long. :/\n    for a in attrs:\n        v = getattr(inst, a.name)\n        if filter is not None and not filter(a, v):\n            continue\n        if recurse is True:\n            if has(v.__class__):\n                rv.append(\n                    astuple(\n                        v,\n                        recurse=True,\n                        filter=filter,\n                        tuple_factory=tuple_factory,\n                        retain_collection_types=retain,\n                    )\n                )\n            elif isinstance(v, (tuple, list, set, frozenset)):\n                cf = v.__class__ if retain is True else list\n                rv.append(\n                    cf(\n                        [\n                            astuple(\n                                j,\n                                recurse=True,\n                                filter=filter,\n                                tuple_factory=tuple_factory,\n                                retain_collection_types=retain,\n                            )\n                            if has(j.__class__)\n                            else j\n                            for j in v\n                        ]\n                    )\n                )\n            elif isinstance(v, dict):\n                df = v.__class__ if retain is True else dict\n                rv.append(\n                    df(\n                        (\n                            astuple(\n                                kk,\n                                tuple_factory=tuple_factory,\n                                retain_collection_types=retain,\n                            )\n                            if has(kk.__class__)\n                            else kk,\n                            astuple(\n                                vv,\n                                tuple_factory=tuple_factory,\n                                retain_collection_types=retain,\n                            )\n                            if has(vv.__class__)\n                            else vv,\n                        )\n                        for kk, vv in iteritems(v)\n                    )\n                )\n            else:\n                rv.append(v)\n        else:\n            rv.append(v)\n\n    return rv if tuple_factory is list else tuple_factory(rv)\n\n\ndef has(cls):\n    \"\"\"\n    Check whether *cls* is a class with ``attrs`` attributes.\n\n    :param type cls: Class to introspect.\n    :raise TypeError: If *cls* is not a class.\n\n    :rtype: bool\n    \"\"\"\n    return getattr(cls, \"__attrs_attrs__\", None) is not None\n\n\ndef assoc(inst, **changes):\n    \"\"\"\n    Copy *inst* and apply *changes*.\n\n    :param inst: Instance of a class with ``attrs`` attributes.\n    :param changes: Keyword changes in the new copy.\n\n    :return: A copy of inst with *changes* incorporated.\n\n    :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't\n        be found on *cls*.\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class.\n\n    ..  deprecated:: 17.1.0\n        Use `attrs.evolve` instead if you can.\n        This function will not be removed du to the slightly different approach\n        compared to `attrs.evolve`.\n    \"\"\"\n    import warnings\n\n    warnings.warn(\n        \"assoc is deprecated and will be removed after 2018/01.\",\n        DeprecationWarning,\n        stacklevel=2,\n    )\n    new = copy.copy(inst)\n    attrs = fields(inst.__class__)\n    for k, v in iteritems(changes):\n        a = getattr(attrs, k, NOTHING)\n        if a is NOTHING:\n            raise AttrsAttributeNotFoundError(\n                \"{k} is not an attrs attribute on {cl}.\".format(\n                    k=k, cl=new.__class__\n                )\n            )\n        _obj_setattr(new, k, v)\n    return new\n\n\ndef evolve(inst, **changes):\n    \"\"\"\n    Create a new instance, based on *inst* with *changes* applied.\n\n    :param inst: Instance of a class with ``attrs`` attributes.\n    :param changes: Keyword changes in the new copy.\n\n    :return: A copy of inst with *changes* incorporated.\n\n    :raise TypeError: If *attr_name* couldn't be found in the class\n        ``__init__``.\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class.\n\n    ..  versionadded:: 17.1.0\n    \"\"\"\n    cls = inst.__class__\n    attrs = fields(cls)\n    for a in attrs:\n        if not a.init:\n            continue\n        attr_name = a.name  # To deal with private attributes.\n        init_name = attr_name if attr_name[0] != \"_\" else attr_name[1:]\n        if init_name not in changes:\n            changes[init_name] = getattr(inst, attr_name)\n\n    return cls(**changes)\n\n\ndef resolve_types(cls, globalns=None, localns=None, attribs=None):\n    \"\"\"\n    Resolve any strings and forward annotations in type annotations.\n\n    This is only required if you need concrete types in `Attribute`'s *type*\n    field. In other words, you don't need to resolve your types if you only\n    use them for static type checking.\n\n    With no arguments, names will be looked up in the module in which the class\n    was created. If this is not what you want, e.g. if the name only exists\n    inside a method, you may pass *globalns* or *localns* to specify other\n    dictionaries in which to look up these names. See the docs of\n    `typing.get_type_hints` for more details.\n\n    :param type cls: Class to resolve.\n    :param Optional[dict] globalns: Dictionary containing global variables.\n    :param Optional[dict] localns: Dictionary containing local variables.\n    :param Optional[list] attribs: List of attribs for the given class.\n        This is necessary when calling from inside a ``field_transformer``\n        since *cls* is not an ``attrs`` class yet.\n\n    :raise TypeError: If *cls* is not a class.\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class and you didn't pass any attribs.\n    :raise NameError: If types cannot be resolved because of missing variables.\n\n    :returns: *cls* so you can use this function also as a class decorator.\n        Please note that you have to apply it **after** `attrs.define`. That\n        means the decorator has to come in the line **before** `attrs.define`.\n\n    ..  versionadded:: 20.1.0\n    ..  versionadded:: 21.1.0 *attribs*\n\n    \"\"\"\n    # Since calling get_type_hints is expensive we cache whether we've\n    # done it already.\n    if getattr(cls, \"__attrs_types_resolved__\", None) != cls:\n        import typing\n\n        hints = typing.get_type_hints(cls, globalns=globalns, localns=localns)\n        for field in fields(cls) if attribs is None else attribs:\n            if field.name in hints:\n                # Since fields have been frozen we must work around it.\n                _obj_setattr(field, \"type\", hints[field.name])\n        # We store the class we resolved so that subclasses know they haven't\n        # been resolved.\n        cls.__attrs_types_resolved__ = cls\n\n    # Return the class so you can use it as a decorator too.\n    return cls\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/_make.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom __future__ import absolute_import, division, print_function\n\nimport copy\nimport inspect\nimport linecache\nimport sys\nimport warnings\n\nfrom operator import itemgetter\n\n# We need to import _compat itself in addition to the _compat members to avoid\n# having the thread-local in the globals here.\nfrom . import _compat, _config, setters\nfrom ._compat import (\n    HAS_F_STRINGS,\n    PY2,\n    PY310,\n    PYPY,\n    isclass,\n    iteritems,\n    metadata_proxy,\n    new_class,\n    ordered_dict,\n    set_closure_cell,\n)\nfrom .exceptions import (\n    DefaultAlreadySetError,\n    FrozenInstanceError,\n    NotAnAttrsClassError,\n    PythonTooOldError,\n    UnannotatedAttributeError,\n)\n\n\nif not PY2:\n    import typing\n\n\n# This is used at least twice, so cache it here.\n_obj_setattr = object.__setattr__\n_init_converter_pat = \"__attr_converter_%s\"\n_init_factory_pat = \"__attr_factory_{}\"\n_tuple_property_pat = (\n    \"    {attr_name} = _attrs_property(_attrs_itemgetter({index}))\"\n)\n_classvar_prefixes = (\n    \"typing.ClassVar\",\n    \"t.ClassVar\",\n    \"ClassVar\",\n    \"typing_extensions.ClassVar\",\n)\n# we don't use a double-underscore prefix because that triggers\n# name mangling when trying to create a slot for the field\n# (when slots=True)\n_hash_cache_field = \"_attrs_cached_hash\"\n\n_empty_metadata_singleton = metadata_proxy({})\n\n# Unique object for unequivocal getattr() defaults.\n_sentinel = object()\n\n_ng_default_on_setattr = setters.pipe(setters.convert, setters.validate)\n\n\nclass _Nothing(object):\n    \"\"\"\n    Sentinel class to indicate the lack of a value when ``None`` is ambiguous.\n\n    ``_Nothing`` is a singleton. There is only ever one of it.\n\n    .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False.\n    \"\"\"\n\n    _singleton = None\n\n    def __new__(cls):\n        if _Nothing._singleton is None:\n            _Nothing._singleton = super(_Nothing, cls).__new__(cls)\n        return _Nothing._singleton\n\n    def __repr__(self):\n        return \"NOTHING\"\n\n    def __bool__(self):\n        return False\n\n    def __len__(self):\n        return 0  # __bool__ for Python 2\n\n\nNOTHING = _Nothing()\n\"\"\"\nSentinel to indicate the lack of a value when ``None`` is ambiguous.\n\"\"\"\n\n\nclass _CacheHashWrapper(int):\n    \"\"\"\n    An integer subclass that pickles / copies as None\n\n    This is used for non-slots classes with ``cache_hash=True``, to avoid\n    serializing a potentially (even likely) invalid hash value. Since ``None``\n    is the default value for uncalculated hashes, whenever this is copied,\n    the copy's value for the hash should automatically reset.\n\n    See GH #613 for more details.\n    \"\"\"\n\n    if PY2:\n        # For some reason `type(None)` isn't callable in Python 2, but we don't\n        # actually need a constructor for None objects, we just need any\n        # available function that returns None.\n        def __reduce__(self, _none_constructor=getattr, _args=(0, \"\", None)):\n            return _none_constructor, _args\n\n    else:\n\n        def __reduce__(self, _none_constructor=type(None), _args=()):\n            return _none_constructor, _args\n\n\ndef attrib(\n    default=NOTHING,\n    validator=None,\n    repr=True,\n    cmp=None,\n    hash=None,\n    init=True,\n    metadata=None,\n    type=None,\n    converter=None,\n    factory=None,\n    kw_only=False,\n    eq=None,\n    order=None,\n    on_setattr=None,\n):\n    \"\"\"\n    Create a new attribute on a class.\n\n    ..  warning::\n\n        Does *not* do anything unless the class is also decorated with\n        `attr.s`!\n\n    :param default: A value that is used if an ``attrs``-generated ``__init__``\n        is used and no value is passed while instantiating or the attribute is\n        excluded using ``init=False``.\n\n        If the value is an instance of `attrs.Factory`, its callable will be\n        used to construct a new value (useful for mutable data types like lists\n        or dicts).\n\n        If a default is not set (or set manually to `attrs.NOTHING`), a value\n        *must* be supplied when instantiating; otherwise a `TypeError`\n        will be raised.\n\n        The default can also be set using decorator notation as shown below.\n\n    :type default: Any value\n\n    :param callable factory: Syntactic sugar for\n        ``default=attr.Factory(factory)``.\n\n    :param validator: `callable` that is called by ``attrs``-generated\n        ``__init__`` methods after the instance has been initialized.  They\n        receive the initialized instance, the :func:`~attrs.Attribute`, and the\n        passed value.\n\n        The return value is *not* inspected so the validator has to throw an\n        exception itself.\n\n        If a `list` is passed, its items are treated as validators and must\n        all pass.\n\n        Validators can be globally disabled and re-enabled using\n        `get_run_validators`.\n\n        The validator can also be set using decorator notation as shown below.\n\n    :type validator: `callable` or a `list` of `callable`\\\\ s.\n\n    :param repr: Include this attribute in the generated ``__repr__``\n        method. If ``True``, include the attribute; if ``False``, omit it. By\n        default, the built-in ``repr()`` function is used. To override how the\n        attribute value is formatted, pass a ``callable`` that takes a single\n        value and returns a string. Note that the resulting string is used\n        as-is, i.e. it will be used directly *instead* of calling ``repr()``\n        (the default).\n    :type repr: a `bool` or a `callable` to use a custom function.\n\n    :param eq: If ``True`` (default), include this attribute in the\n        generated ``__eq__`` and ``__ne__`` methods that check two instances\n        for equality. To override how the attribute value is compared,\n        pass a ``callable`` that takes a single value and returns the value\n        to be compared.\n    :type eq: a `bool` or a `callable`.\n\n    :param order: If ``True`` (default), include this attributes in the\n        generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods.\n        To override how the attribute value is ordered,\n        pass a ``callable`` that takes a single value and returns the value\n        to be ordered.\n    :type order: a `bool` or a `callable`.\n\n    :param cmp: Setting *cmp* is equivalent to setting *eq* and *order* to the\n        same value. Must not be mixed with *eq* or *order*.\n    :type cmp: a `bool` or a `callable`.\n\n    :param Optional[bool] hash: Include this attribute in the generated\n        ``__hash__`` method.  If ``None`` (default), mirror *eq*'s value.  This\n        is the correct behavior according the Python spec.  Setting this value\n        to anything else than ``None`` is *discouraged*.\n    :param bool init: Include this attribute in the generated ``__init__``\n        method.  It is possible to set this to ``False`` and set a default\n        value.  In that case this attributed is unconditionally initialized\n        with the specified default value or factory.\n    :param callable converter: `callable` that is called by\n        ``attrs``-generated ``__init__`` methods to convert attribute's value\n        to the desired format.  It is given the passed-in value, and the\n        returned value will be used as the new value of the attribute.  The\n        value is converted before being passed to the validator, if any.\n    :param metadata: An arbitrary mapping, to be used by third-party\n        components.  See `extending_metadata`.\n    :param type: The type of the attribute.  In Python 3.6 or greater, the\n        preferred method to specify the type is using a variable annotation\n        (see `PEP 526 <https://www.python.org/dev/peps/pep-0526/>`_).\n        This argument is provided for backward compatibility.\n        Regardless of the approach used, the type will be stored on\n        ``Attribute.type``.\n\n        Please note that ``attrs`` doesn't do anything with this metadata by\n        itself. You can use it as part of your own code or for\n        `static type checking <types>`.\n    :param kw_only: Make this attribute keyword-only (Python 3+)\n        in the generated ``__init__`` (if ``init`` is ``False``, this\n        parameter is ignored).\n    :param on_setattr: Allows to overwrite the *on_setattr* setting from\n        `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used.\n        Set to `attrs.setters.NO_OP` to run **no** `setattr` hooks for this\n        attribute -- regardless of the setting in `attr.s`.\n    :type on_setattr: `callable`, or a list of callables, or `None`, or\n        `attrs.setters.NO_OP`\n\n    .. versionadded:: 15.2.0 *convert*\n    .. versionadded:: 16.3.0 *metadata*\n    .. versionchanged:: 17.1.0 *validator* can be a ``list`` now.\n    .. versionchanged:: 17.1.0\n       *hash* is ``None`` and therefore mirrors *eq* by default.\n    .. versionadded:: 17.3.0 *type*\n    .. deprecated:: 17.4.0 *convert*\n    .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated\n       *convert* to achieve consistency with other noun-based arguments.\n    .. versionadded:: 18.1.0\n       ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``.\n    .. versionadded:: 18.2.0 *kw_only*\n    .. versionchanged:: 19.2.0 *convert* keyword argument removed.\n    .. versionchanged:: 19.2.0 *repr* also accepts a custom callable.\n    .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.\n    .. versionadded:: 19.2.0 *eq* and *order*\n    .. versionadded:: 20.1.0 *on_setattr*\n    .. versionchanged:: 20.3.0 *kw_only* backported to Python 2\n    .. versionchanged:: 21.1.0\n       *eq*, *order*, and *cmp* also accept a custom callable\n    .. versionchanged:: 21.1.0 *cmp* undeprecated\n    \"\"\"\n    eq, eq_key, order, order_key = _determine_attrib_eq_order(\n        cmp, eq, order, True\n    )\n\n    if hash is not None and hash is not True and hash is not False:\n        raise TypeError(\n            \"Invalid value for hash.  Must be True, False, or None.\"\n        )\n\n    if factory is not None:\n        if default is not NOTHING:\n            raise ValueError(\n                \"The `default` and `factory` arguments are mutually \"\n                \"exclusive.\"\n            )\n        if not callable(factory):\n            raise ValueError(\"The `factory` argument must be a callable.\")\n        default = Factory(factory)\n\n    if metadata is None:\n        metadata = {}\n\n    # Apply syntactic sugar by auto-wrapping.\n    if isinstance(on_setattr, (list, tuple)):\n        on_setattr = setters.pipe(*on_setattr)\n\n    if validator and isinstance(validator, (list, tuple)):\n        validator = and_(*validator)\n\n    if converter and isinstance(converter, (list, tuple)):\n        converter = pipe(*converter)\n\n    return _CountingAttr(\n        default=default,\n        validator=validator,\n        repr=repr,\n        cmp=None,\n        hash=hash,\n        init=init,\n        converter=converter,\n        metadata=metadata,\n        type=type,\n        kw_only=kw_only,\n        eq=eq,\n        eq_key=eq_key,\n        order=order,\n        order_key=order_key,\n        on_setattr=on_setattr,\n    )\n\n\ndef _compile_and_eval(script, globs, locs=None, filename=\"\"):\n    \"\"\"\n    \"Exec\" the script with the given global (globs) and local (locs) variables.\n    \"\"\"\n    bytecode = compile(script, filename, \"exec\")\n    eval(bytecode, globs, locs)\n\n\ndef _make_method(name, script, filename, globs=None):\n    \"\"\"\n    Create the method with the script given and return the method object.\n    \"\"\"\n    locs = {}\n    if globs is None:\n        globs = {}\n\n    # In order of debuggers like PDB being able to step through the code,\n    # we add a fake linecache entry.\n    count = 1\n    base_filename = filename\n    while True:\n        linecache_tuple = (\n            len(script),\n            None,\n            script.splitlines(True),\n            filename,\n        )\n        old_val = linecache.cache.setdefault(filename, linecache_tuple)\n        if old_val == linecache_tuple:\n            break\n        else:\n            filename = \"{}-{}>\".format(base_filename[:-1], count)\n            count += 1\n\n    _compile_and_eval(script, globs, locs, filename)\n\n    return locs[name]\n\n\ndef _make_attr_tuple_class(cls_name, attr_names):\n    \"\"\"\n    Create a tuple subclass to hold `Attribute`s for an `attrs` class.\n\n    The subclass is a bare tuple with properties for names.\n\n    class MyClassAttributes(tuple):\n        __slots__ = ()\n        x = property(itemgetter(0))\n    \"\"\"\n    attr_class_name = \"{}Attributes\".format(cls_name)\n    attr_class_template = [\n        \"class {}(tuple):\".format(attr_class_name),\n        \"    __slots__ = ()\",\n    ]\n    if attr_names:\n        for i, attr_name in enumerate(attr_names):\n            attr_class_template.append(\n                _tuple_property_pat.format(index=i, attr_name=attr_name)\n            )\n    else:\n        attr_class_template.append(\"    pass\")\n    globs = {\"_attrs_itemgetter\": itemgetter, \"_attrs_property\": property}\n    _compile_and_eval(\"\\n\".join(attr_class_template), globs)\n    return globs[attr_class_name]\n\n\n# Tuple class for extracted attributes from a class definition.\n# `base_attrs` is a subset of `attrs`.\n_Attributes = _make_attr_tuple_class(\n    \"_Attributes\",\n    [\n        # all attributes to build dunder methods for\n        \"attrs\",\n        # attributes that have been inherited\n        \"base_attrs\",\n        # map inherited attributes to their originating classes\n        \"base_attrs_map\",\n    ],\n)\n\n\ndef _is_class_var(annot):\n    \"\"\"\n    Check whether *annot* is a typing.ClassVar.\n\n    The string comparison hack is used to avoid evaluating all string\n    annotations which would put attrs-based classes at a performance\n    disadvantage compared to plain old classes.\n    \"\"\"\n    annot = str(annot)\n\n    # Annotation can be quoted.\n    if annot.startswith((\"'\", '\"')) and annot.endswith((\"'\", '\"')):\n        annot = annot[1:-1]\n\n    return annot.startswith(_classvar_prefixes)\n\n\ndef _has_own_attribute(cls, attrib_name):\n    \"\"\"\n    Check whether *cls* defines *attrib_name* (and doesn't just inherit it).\n\n    Requires Python 3.\n    \"\"\"\n    attr = getattr(cls, attrib_name, _sentinel)\n    if attr is _sentinel:\n        return False\n\n    for base_cls in cls.__mro__[1:]:\n        a = getattr(base_cls, attrib_name, None)\n        if attr is a:\n            return False\n\n    return True\n\n\ndef _get_annotations(cls):\n    \"\"\"\n    Get annotations for *cls*.\n    \"\"\"\n    if _has_own_attribute(cls, \"__annotations__\"):\n        return cls.__annotations__\n\n    return {}\n\n\ndef _counter_getter(e):\n    \"\"\"\n    Key function for sorting to avoid re-creating a lambda for every class.\n    \"\"\"\n    return e[1].counter\n\n\ndef _collect_base_attrs(cls, taken_attr_names):\n    \"\"\"\n    Collect attr.ibs from base classes of *cls*, except *taken_attr_names*.\n    \"\"\"\n    base_attrs = []\n    base_attr_map = {}  # A dictionary of base attrs to their classes.\n\n    # Traverse the MRO and collect attributes.\n    for base_cls in reversed(cls.__mro__[1:-1]):\n        for a in getattr(base_cls, \"__attrs_attrs__\", []):\n            if a.inherited or a.name in taken_attr_names:\n                continue\n\n            a = a.evolve(inherited=True)\n            base_attrs.append(a)\n            base_attr_map[a.name] = base_cls\n\n    # For each name, only keep the freshest definition i.e. the furthest at the\n    # back.  base_attr_map is fine because it gets overwritten with every new\n    # instance.\n    filtered = []\n    seen = set()\n    for a in reversed(base_attrs):\n        if a.name in seen:\n            continue\n        filtered.insert(0, a)\n        seen.add(a.name)\n\n    return filtered, base_attr_map\n\n\ndef _collect_base_attrs_broken(cls, taken_attr_names):\n    \"\"\"\n    Collect attr.ibs from base classes of *cls*, except *taken_attr_names*.\n\n    N.B. *taken_attr_names* will be mutated.\n\n    Adhere to the old incorrect behavior.\n\n    Notably it collects from the front and considers inherited attributes which\n    leads to the buggy behavior reported in #428.\n    \"\"\"\n    base_attrs = []\n    base_attr_map = {}  # A dictionary of base attrs to their classes.\n\n    # Traverse the MRO and collect attributes.\n    for base_cls in cls.__mro__[1:-1]:\n        for a in getattr(base_cls, \"__attrs_attrs__\", []):\n            if a.name in taken_attr_names:\n                continue\n\n            a = a.evolve(inherited=True)\n            taken_attr_names.add(a.name)\n            base_attrs.append(a)\n            base_attr_map[a.name] = base_cls\n\n    return base_attrs, base_attr_map\n\n\ndef _transform_attrs(\n    cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer\n):\n    \"\"\"\n    Transform all `_CountingAttr`s on a class into `Attribute`s.\n\n    If *these* is passed, use that and don't look for them on the class.\n\n    *collect_by_mro* is True, collect them in the correct MRO order, otherwise\n    use the old -- incorrect -- order.  See #428.\n\n    Return an `_Attributes`.\n    \"\"\"\n    cd = cls.__dict__\n    anns = _get_annotations(cls)\n\n    if these is not None:\n        ca_list = [(name, ca) for name, ca in iteritems(these)]\n\n        if not isinstance(these, ordered_dict):\n            ca_list.sort(key=_counter_getter)\n    elif auto_attribs is True:\n        ca_names = {\n            name\n            for name, attr in cd.items()\n            if isinstance(attr, _CountingAttr)\n        }\n        ca_list = []\n        annot_names = set()\n        for attr_name, type in anns.items():\n            if _is_class_var(type):\n                continue\n            annot_names.add(attr_name)\n            a = cd.get(attr_name, NOTHING)\n\n            if not isinstance(a, _CountingAttr):\n                if a is NOTHING:\n                    a = attrib()\n                else:\n                    a = attrib(default=a)\n            ca_list.append((attr_name, a))\n\n        unannotated = ca_names - annot_names\n        if len(unannotated) > 0:\n            raise UnannotatedAttributeError(\n                \"The following `attr.ib`s lack a type annotation: \"\n                + \", \".join(\n                    sorted(unannotated, key=lambda n: cd.get(n).counter)\n                )\n                + \".\"\n            )\n    else:\n        ca_list = sorted(\n            (\n                (name, attr)\n                for name, attr in cd.items()\n                if isinstance(attr, _CountingAttr)\n            ),\n            key=lambda e: e[1].counter,\n        )\n\n    own_attrs = [\n        Attribute.from_counting_attr(\n            name=attr_name, ca=ca, type=anns.get(attr_name)\n        )\n        for attr_name, ca in ca_list\n    ]\n\n    if collect_by_mro:\n        base_attrs, base_attr_map = _collect_base_attrs(\n            cls, {a.name for a in own_attrs}\n        )\n    else:\n        base_attrs, base_attr_map = _collect_base_attrs_broken(\n            cls, {a.name for a in own_attrs}\n        )\n\n    if kw_only:\n        own_attrs = [a.evolve(kw_only=True) for a in own_attrs]\n        base_attrs = [a.evolve(kw_only=True) for a in base_attrs]\n\n    attrs = base_attrs + own_attrs\n\n    # Mandatory vs non-mandatory attr order only matters when they are part of\n    # the __init__ signature and when they aren't kw_only (which are moved to\n    # the end and can be mandatory or non-mandatory in any order, as they will\n    # be specified as keyword args anyway). Check the order of those attrs:\n    had_default = False\n    for a in (a for a in attrs if a.init is not False and a.kw_only is False):\n        if had_default is True and a.default is NOTHING:\n            raise ValueError(\n                \"No mandatory attributes allowed after an attribute with a \"\n                \"default value or factory.  Attribute in question: %r\" % (a,)\n            )\n\n        if had_default is False and a.default is not NOTHING:\n            had_default = True\n\n    if field_transformer is not None:\n        attrs = field_transformer(cls, attrs)\n\n    # Create AttrsClass *after* applying the field_transformer since it may\n    # add or remove attributes!\n    attr_names = [a.name for a in attrs]\n    AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names)\n\n    return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map))\n\n\nif PYPY:\n\n    def _frozen_setattrs(self, name, value):\n        \"\"\"\n        Attached to frozen classes as __setattr__.\n        \"\"\"\n        if isinstance(self, BaseException) and name in (\n            \"__cause__\",\n            \"__context__\",\n        ):\n            BaseException.__setattr__(self, name, value)\n            return\n\n        raise FrozenInstanceError()\n\nelse:\n\n    def _frozen_setattrs(self, name, value):\n        \"\"\"\n        Attached to frozen classes as __setattr__.\n        \"\"\"\n        raise FrozenInstanceError()\n\n\ndef _frozen_delattrs(self, name):\n    \"\"\"\n    Attached to frozen classes as __delattr__.\n    \"\"\"\n    raise FrozenInstanceError()\n\n\nclass _ClassBuilder(object):\n    \"\"\"\n    Iteratively build *one* class.\n    \"\"\"\n\n    __slots__ = (\n        \"_attr_names\",\n        \"_attrs\",\n        \"_base_attr_map\",\n        \"_base_names\",\n        \"_cache_hash\",\n        \"_cls\",\n        \"_cls_dict\",\n        \"_delete_attribs\",\n        \"_frozen\",\n        \"_has_pre_init\",\n        \"_has_post_init\",\n        \"_is_exc\",\n        \"_on_setattr\",\n        \"_slots\",\n        \"_weakref_slot\",\n        \"_wrote_own_setattr\",\n        \"_has_custom_setattr\",\n    )\n\n    def __init__(\n        self,\n        cls,\n        these,\n        slots,\n        frozen,\n        weakref_slot,\n        getstate_setstate,\n        auto_attribs,\n        kw_only,\n        cache_hash,\n        is_exc,\n        collect_by_mro,\n        on_setattr,\n        has_custom_setattr,\n        field_transformer,\n    ):\n        attrs, base_attrs, base_map = _transform_attrs(\n            cls,\n            these,\n            auto_attribs,\n            kw_only,\n            collect_by_mro,\n            field_transformer,\n        )\n\n        self._cls = cls\n        self._cls_dict = dict(cls.__dict__) if slots else {}\n        self._attrs = attrs\n        self._base_names = set(a.name for a in base_attrs)\n        self._base_attr_map = base_map\n        self._attr_names = tuple(a.name for a in attrs)\n        self._slots = slots\n        self._frozen = frozen\n        self._weakref_slot = weakref_slot\n        self._cache_hash = cache_hash\n        self._has_pre_init = bool(getattr(cls, \"__attrs_pre_init__\", False))\n        self._has_post_init = bool(getattr(cls, \"__attrs_post_init__\", False))\n        self._delete_attribs = not bool(these)\n        self._is_exc = is_exc\n        self._on_setattr = on_setattr\n\n        self._has_custom_setattr = has_custom_setattr\n        self._wrote_own_setattr = False\n\n        self._cls_dict[\"__attrs_attrs__\"] = self._attrs\n\n        if frozen:\n            self._cls_dict[\"__setattr__\"] = _frozen_setattrs\n            self._cls_dict[\"__delattr__\"] = _frozen_delattrs\n\n            self._wrote_own_setattr = True\n        elif on_setattr in (\n            _ng_default_on_setattr,\n            setters.validate,\n            setters.convert,\n        ):\n            has_validator = has_converter = False\n            for a in attrs:\n                if a.validator is not None:\n                    has_validator = True\n                if a.converter is not None:\n                    has_converter = True\n\n                if has_validator and has_converter:\n                    break\n            if (\n                (\n                    on_setattr == _ng_default_on_setattr\n                    and not (has_validator or has_converter)\n                )\n                or (on_setattr == setters.validate and not has_validator)\n                or (on_setattr == setters.convert and not has_converter)\n            ):\n                # If class-level on_setattr is set to convert + validate, but\n                # there's no field to convert or validate, pretend like there's\n                # no on_setattr.\n                self._on_setattr = None\n\n        if getstate_setstate:\n            (\n                self._cls_dict[\"__getstate__\"],\n                self._cls_dict[\"__setstate__\"],\n            ) = self._make_getstate_setstate()\n\n    def __repr__(self):\n        return \"<_ClassBuilder(cls={cls})>\".format(cls=self._cls.__name__)\n\n    def build_class(self):\n        \"\"\"\n        Finalize class based on the accumulated configuration.\n\n        Builder cannot be used after calling this method.\n        \"\"\"\n        if self._slots is True:\n            return self._create_slots_class()\n        else:\n            return self._patch_original_class()\n\n    def _patch_original_class(self):\n        \"\"\"\n        Apply accumulated methods and return the class.\n        \"\"\"\n        cls = self._cls\n        base_names = self._base_names\n\n        # Clean class of attribute definitions (`attr.ib()`s).\n        if self._delete_attribs:\n            for name in self._attr_names:\n                if (\n                    name not in base_names\n                    and getattr(cls, name, _sentinel) is not _sentinel\n                ):\n                    try:\n                        delattr(cls, name)\n                    except AttributeError:\n                        # This can happen if a base class defines a class\n                        # variable and we want to set an attribute with the\n                        # same name by using only a type annotation.\n                        pass\n\n        # Attach our dunder methods.\n        for name, value in self._cls_dict.items():\n            setattr(cls, name, value)\n\n        # If we've inherited an attrs __setattr__ and don't write our own,\n        # reset it to object's.\n        if not self._wrote_own_setattr and getattr(\n            cls, \"__attrs_own_setattr__\", False\n        ):\n            cls.__attrs_own_setattr__ = False\n\n            if not self._has_custom_setattr:\n                cls.__setattr__ = object.__setattr__\n\n        return cls\n\n    def _create_slots_class(self):\n        \"\"\"\n        Build and return a new class with a `__slots__` attribute.\n        \"\"\"\n        cd = {\n            k: v\n            for k, v in iteritems(self._cls_dict)\n            if k not in tuple(self._attr_names) + (\"__dict__\", \"__weakref__\")\n        }\n\n        # If our class doesn't have its own implementation of __setattr__\n        # (either from the user or by us), check the bases, if one of them has\n        # an attrs-made __setattr__, that needs to be reset. We don't walk the\n        # MRO because we only care about our immediate base classes.\n        # XXX: This can be confused by subclassing a slotted attrs class with\n        # XXX: a non-attrs class and subclass the resulting class with an attrs\n        # XXX: class.  See `test_slotted_confused` for details.  For now that's\n        # XXX: OK with us.\n        if not self._wrote_own_setattr:\n            cd[\"__attrs_own_setattr__\"] = False\n\n            if not self._has_custom_setattr:\n                for base_cls in self._cls.__bases__:\n                    if base_cls.__dict__.get(\"__attrs_own_setattr__\", False):\n                        cd[\"__setattr__\"] = object.__setattr__\n                        break\n\n        # Traverse the MRO to collect existing slots\n        # and check for an existing __weakref__.\n        existing_slots = dict()\n        weakref_inherited = False\n        for base_cls in self._cls.__mro__[1:-1]:\n            if base_cls.__dict__.get(\"__weakref__\", None) is not None:\n                weakref_inherited = True\n            existing_slots.update(\n                {\n                    name: getattr(base_cls, name)\n                    for name in getattr(base_cls, \"__slots__\", [])\n                }\n            )\n\n        base_names = set(self._base_names)\n\n        names = self._attr_names\n        if (\n            self._weakref_slot\n            and \"__weakref__\" not in getattr(self._cls, \"__slots__\", ())\n            and \"__weakref__\" not in names\n            and not weakref_inherited\n        ):\n            names += (\"__weakref__\",)\n\n        # We only add the names of attributes that aren't inherited.\n        # Setting __slots__ to inherited attributes wastes memory.\n        slot_names = [name for name in names if name not in base_names]\n        # There are slots for attributes from current class\n        # that are defined in parent classes.\n        # As their descriptors may be overriden by a child class,\n        # we collect them here and update the class dict\n        reused_slots = {\n            slot: slot_descriptor\n            for slot, slot_descriptor in iteritems(existing_slots)\n            if slot in slot_names\n        }\n        slot_names = [name for name in slot_names if name not in reused_slots]\n        cd.update(reused_slots)\n        if self._cache_hash:\n            slot_names.append(_hash_cache_field)\n        cd[\"__slots__\"] = tuple(slot_names)\n\n        qualname = getattr(self._cls, \"__qualname__\", None)\n        if qualname is not None:\n            cd[\"__qualname__\"] = qualname\n\n        # Create new class based on old class and our methods.\n        cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd)\n\n        # The following is a fix for\n        # <https://github.com/python-attrs/attrs/issues/102>.  On Python 3,\n        # if a method mentions `__class__` or uses the no-arg super(), the\n        # compiler will bake a reference to the class in the method itself\n        # as `method.__closure__`.  Since we replace the class with a\n        # clone, we rewrite these references so it keeps working.\n        for item in cls.__dict__.values():\n            if isinstance(item, (classmethod, staticmethod)):\n                # Class- and staticmethods hide their functions inside.\n                # These might need to be rewritten as well.\n                closure_cells = getattr(item.__func__, \"__closure__\", None)\n            elif isinstance(item, property):\n                # Workaround for property `super()` shortcut (PY3-only).\n                # There is no universal way for other descriptors.\n                closure_cells = getattr(item.fget, \"__closure__\", None)\n            else:\n                closure_cells = getattr(item, \"__closure__\", None)\n\n            if not closure_cells:  # Catch None or the empty list.\n                continue\n            for cell in closure_cells:\n                try:\n                    match = cell.cell_contents is self._cls\n                except ValueError:  # ValueError: Cell is empty\n                    pass\n                else:\n                    if match:\n                        set_closure_cell(cell, cls)\n\n        return cls\n\n    def add_repr(self, ns):\n        self._cls_dict[\"__repr__\"] = self._add_method_dunders(\n            _make_repr(self._attrs, ns, self._cls)\n        )\n        return self\n\n    def add_str(self):\n        repr = self._cls_dict.get(\"__repr__\")\n        if repr is None:\n            raise ValueError(\n                \"__str__ can only be generated if a __repr__ exists.\"\n            )\n\n        def __str__(self):\n            return self.__repr__()\n\n        self._cls_dict[\"__str__\"] = self._add_method_dunders(__str__)\n        return self\n\n    def _make_getstate_setstate(self):\n        \"\"\"\n        Create custom __setstate__ and __getstate__ methods.\n        \"\"\"\n        # __weakref__ is not writable.\n        state_attr_names = tuple(\n            an for an in self._attr_names if an != \"__weakref__\"\n        )\n\n        def slots_getstate(self):\n            \"\"\"\n            Automatically created by attrs.\n            \"\"\"\n            return tuple(getattr(self, name) for name in state_attr_names)\n\n        hash_caching_enabled = self._cache_hash\n\n        def slots_setstate(self, state):\n            \"\"\"\n            Automatically created by attrs.\n            \"\"\"\n            __bound_setattr = _obj_setattr.__get__(self, Attribute)\n            for name, value in zip(state_attr_names, state):\n                __bound_setattr(name, value)\n\n            # The hash code cache is not included when the object is\n            # serialized, but it still needs to be initialized to None to\n            # indicate that the first call to __hash__ should be a cache\n            # miss.\n            if hash_caching_enabled:\n                __bound_setattr(_hash_cache_field, None)\n\n        return slots_getstate, slots_setstate\n\n    def make_unhashable(self):\n        self._cls_dict[\"__hash__\"] = None\n        return self\n\n    def add_hash(self):\n        self._cls_dict[\"__hash__\"] = self._add_method_dunders(\n            _make_hash(\n                self._cls,\n                self._attrs,\n                frozen=self._frozen,\n                cache_hash=self._cache_hash,\n            )\n        )\n\n        return self\n\n    def add_init(self):\n        self._cls_dict[\"__init__\"] = self._add_method_dunders(\n            _make_init(\n                self._cls,\n                self._attrs,\n                self._has_pre_init,\n                self._has_post_init,\n                self._frozen,\n                self._slots,\n                self._cache_hash,\n                self._base_attr_map,\n                self._is_exc,\n                self._on_setattr,\n                attrs_init=False,\n            )\n        )\n\n        return self\n\n    def add_match_args(self):\n        self._cls_dict[\"__match_args__\"] = tuple(\n            field.name\n            for field in self._attrs\n            if field.init and not field.kw_only\n        )\n\n    def add_attrs_init(self):\n        self._cls_dict[\"__attrs_init__\"] = self._add_method_dunders(\n            _make_init(\n                self._cls,\n                self._attrs,\n                self._has_pre_init,\n                self._has_post_init,\n                self._frozen,\n                self._slots,\n                self._cache_hash,\n                self._base_attr_map,\n                self._is_exc,\n                self._on_setattr,\n                attrs_init=True,\n            )\n        )\n\n        return self\n\n    def add_eq(self):\n        cd = self._cls_dict\n\n        cd[\"__eq__\"] = self._add_method_dunders(\n            _make_eq(self._cls, self._attrs)\n        )\n        cd[\"__ne__\"] = self._add_method_dunders(_make_ne())\n\n        return self\n\n    def add_order(self):\n        cd = self._cls_dict\n\n        cd[\"__lt__\"], cd[\"__le__\"], cd[\"__gt__\"], cd[\"__ge__\"] = (\n            self._add_method_dunders(meth)\n            for meth in _make_order(self._cls, self._attrs)\n        )\n\n        return self\n\n    def add_setattr(self):\n        if self._frozen:\n            return self\n\n        sa_attrs = {}\n        for a in self._attrs:\n            on_setattr = a.on_setattr or self._on_setattr\n            if on_setattr and on_setattr is not setters.NO_OP:\n                sa_attrs[a.name] = a, on_setattr\n\n        if not sa_attrs:\n            return self\n\n        if self._has_custom_setattr:\n            # We need to write a __setattr__ but there already is one!\n            raise ValueError(\n                \"Can't combine custom __setattr__ with on_setattr hooks.\"\n            )\n\n        # docstring comes from _add_method_dunders\n        def __setattr__(self, name, val):\n            try:\n                a, hook = sa_attrs[name]\n            except KeyError:\n                nval = val\n            else:\n                nval = hook(self, a, val)\n\n            _obj_setattr(self, name, nval)\n\n        self._cls_dict[\"__attrs_own_setattr__\"] = True\n        self._cls_dict[\"__setattr__\"] = self._add_method_dunders(__setattr__)\n        self._wrote_own_setattr = True\n\n        return self\n\n    def _add_method_dunders(self, method):\n        \"\"\"\n        Add __module__ and __qualname__ to a *method* if possible.\n        \"\"\"\n        try:\n            method.__module__ = self._cls.__module__\n        except AttributeError:\n            pass\n\n        try:\n            method.__qualname__ = \".\".join(\n                (self._cls.__qualname__, method.__name__)\n            )\n        except AttributeError:\n            pass\n\n        try:\n            method.__doc__ = \"Method generated by attrs for class %s.\" % (\n                self._cls.__qualname__,\n            )\n        except AttributeError:\n            pass\n\n        return method\n\n\n_CMP_DEPRECATION = (\n    \"The usage of `cmp` is deprecated and will be removed on or after \"\n    \"2021-06-01.  Please use `eq` and `order` instead.\"\n)\n\n\ndef _determine_attrs_eq_order(cmp, eq, order, default_eq):\n    \"\"\"\n    Validate the combination of *cmp*, *eq*, and *order*. Derive the effective\n    values of eq and order.  If *eq* is None, set it to *default_eq*.\n    \"\"\"\n    if cmp is not None and any((eq is not None, order is not None)):\n        raise ValueError(\"Don't mix `cmp` with `eq' and `order`.\")\n\n    # cmp takes precedence due to bw-compatibility.\n    if cmp is not None:\n        return cmp, cmp\n\n    # If left None, equality is set to the specified default and ordering\n    # mirrors equality.\n    if eq is None:\n        eq = default_eq\n\n    if order is None:\n        order = eq\n\n    if eq is False and order is True:\n        raise ValueError(\"`order` can only be True if `eq` is True too.\")\n\n    return eq, order\n\n\ndef _determine_attrib_eq_order(cmp, eq, order, default_eq):\n    \"\"\"\n    Validate the combination of *cmp*, *eq*, and *order*. Derive the effective\n    values of eq and order.  If *eq* is None, set it to *default_eq*.\n    \"\"\"\n    if cmp is not None and any((eq is not None, order is not None)):\n        raise ValueError(\"Don't mix `cmp` with `eq' and `order`.\")\n\n    def decide_callable_or_boolean(value):\n        \"\"\"\n        Decide whether a key function is used.\n        \"\"\"\n        if callable(value):\n            value, key = True, value\n        else:\n            key = None\n        return value, key\n\n    # cmp takes precedence due to bw-compatibility.\n    if cmp is not None:\n        cmp, cmp_key = decide_callable_or_boolean(cmp)\n        return cmp, cmp_key, cmp, cmp_key\n\n    # If left None, equality is set to the specified default and ordering\n    # mirrors equality.\n    if eq is None:\n        eq, eq_key = default_eq, None\n    else:\n        eq, eq_key = decide_callable_or_boolean(eq)\n\n    if order is None:\n        order, order_key = eq, eq_key\n    else:\n        order, order_key = decide_callable_or_boolean(order)\n\n    if eq is False and order is True:\n        raise ValueError(\"`order` can only be True if `eq` is True too.\")\n\n    return eq, eq_key, order, order_key\n\n\ndef _determine_whether_to_implement(\n    cls, flag, auto_detect, dunders, default=True\n):\n    \"\"\"\n    Check whether we should implement a set of methods for *cls*.\n\n    *flag* is the argument passed into @attr.s like 'init', *auto_detect* the\n    same as passed into @attr.s and *dunders* is a tuple of attribute names\n    whose presence signal that the user has implemented it themselves.\n\n    Return *default* if no reason for either for or against is found.\n\n    auto_detect must be False on Python 2.\n    \"\"\"\n    if flag is True or flag is False:\n        return flag\n\n    if flag is None and auto_detect is False:\n        return default\n\n    # Logically, flag is None and auto_detect is True here.\n    for dunder in dunders:\n        if _has_own_attribute(cls, dunder):\n            return False\n\n    return default\n\n\ndef attrs(\n    maybe_cls=None,\n    these=None,\n    repr_ns=None,\n    repr=None,\n    cmp=None,\n    hash=None,\n    init=None,\n    slots=False,\n    frozen=False,\n    weakref_slot=True,\n    str=False,\n    auto_attribs=False,\n    kw_only=False,\n    cache_hash=False,\n    auto_exc=False,\n    eq=None,\n    order=None,\n    auto_detect=False,\n    collect_by_mro=False,\n    getstate_setstate=None,\n    on_setattr=None,\n    field_transformer=None,\n    match_args=True,\n):\n    r\"\"\"\n    A class decorator that adds `dunder\n    <https://wiki.python.org/moin/DunderAlias>`_\\ -methods according to the\n    specified attributes using `attr.ib` or the *these* argument.\n\n    :param these: A dictionary of name to `attr.ib` mappings.  This is\n        useful to avoid the definition of your attributes within the class body\n        because you can't (e.g. if you want to add ``__repr__`` methods to\n        Django models) or don't want to.\n\n        If *these* is not ``None``, ``attrs`` will *not* search the class body\n        for attributes and will *not* remove any attributes from it.\n\n        If *these* is an ordered dict (`dict` on Python 3.6+,\n        `collections.OrderedDict` otherwise), the order is deduced from\n        the order of the attributes inside *these*.  Otherwise the order\n        of the definition of the attributes is used.\n\n    :type these: `dict` of `str` to `attr.ib`\n\n    :param str repr_ns: When using nested classes, there's no way in Python 2\n        to automatically detect that.  Therefore it's possible to set the\n        namespace explicitly for a more meaningful ``repr`` output.\n    :param bool auto_detect: Instead of setting the *init*, *repr*, *eq*,\n        *order*, and *hash* arguments explicitly, assume they are set to\n        ``True`` **unless any** of the involved methods for one of the\n        arguments is implemented in the *current* class (i.e. it is *not*\n        inherited from some base class).\n\n        So for example by implementing ``__eq__`` on a class yourself,\n        ``attrs`` will deduce ``eq=False`` and will create *neither*\n        ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible\n        ``__ne__`` by default, so it *should* be enough to only implement\n        ``__eq__`` in most cases).\n\n        .. warning::\n\n           If you prevent ``attrs`` from creating the ordering methods for you\n           (``order=False``, e.g. by implementing ``__le__``), it becomes\n           *your* responsibility to make sure its ordering is sound. The best\n           way is to use the `functools.total_ordering` decorator.\n\n\n        Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*,\n        *cmp*, or *hash* overrides whatever *auto_detect* would determine.\n\n        *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises\n        an `attrs.exceptions.PythonTooOldError`.\n\n    :param bool repr: Create a ``__repr__`` method with a human readable\n        representation of ``attrs`` attributes..\n    :param bool str: Create a ``__str__`` method that is identical to\n        ``__repr__``.  This is usually not necessary except for\n        `Exception`\\ s.\n    :param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__``\n        and ``__ne__`` methods that check two instances for equality.\n\n        They compare the instances as if they were tuples of their ``attrs``\n        attributes if and only if the types of both classes are *identical*!\n    :param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``,\n        ``__gt__``, and ``__ge__`` methods that behave like *eq* above and\n        allow instances to be ordered. If ``None`` (default) mirror value of\n        *eq*.\n    :param Optional[bool] cmp: Setting *cmp* is equivalent to setting *eq*\n        and *order* to the same value. Must not be mixed with *eq* or *order*.\n    :param Optional[bool] hash: If ``None`` (default), the ``__hash__`` method\n        is generated according how *eq* and *frozen* are set.\n\n        1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you.\n        2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to\n           None, marking it unhashable (which it is).\n        3. If *eq* is False, ``__hash__`` will be left untouched meaning the\n           ``__hash__`` method of the base class will be used (if base class is\n           ``object``, this means it will fall back to id-based hashing.).\n\n        Although not recommended, you can decide for yourself and force\n        ``attrs`` to create one (e.g. if the class is immutable even though you\n        didn't freeze it programmatically) by passing ``True`` or not.  Both of\n        these cases are rather special and should be used carefully.\n\n        See our documentation on `hashing`, Python's documentation on\n        `object.__hash__`, and the `GitHub issue that led to the default \\\n        behavior <https://github.com/python-attrs/attrs/issues/136>`_ for more\n        details.\n    :param bool init: Create a ``__init__`` method that initializes the\n        ``attrs`` attributes. Leading underscores are stripped for the argument\n        name. If a ``__attrs_pre_init__`` method exists on the class, it will\n        be called before the class is initialized. If a ``__attrs_post_init__``\n        method exists on the class, it will be called after the class is fully\n        initialized.\n\n        If ``init`` is ``False``, an ``__attrs_init__`` method will be\n        injected instead. This allows you to define a custom ``__init__``\n        method that can do pre-init work such as ``super().__init__()``,\n        and then call ``__attrs_init__()`` and ``__attrs_post_init__()``.\n    :param bool slots: Create a `slotted class <slotted classes>` that's more\n        memory-efficient. Slotted classes are generally superior to the default\n        dict classes, but have some gotchas you should know about, so we\n        encourage you to read the `glossary entry <slotted classes>`.\n    :param bool frozen: Make instances immutable after initialization.  If\n        someone attempts to modify a frozen instance,\n        `attr.exceptions.FrozenInstanceError` is raised.\n\n        .. note::\n\n            1. This is achieved by installing a custom ``__setattr__`` method\n               on your class, so you can't implement your own.\n\n            2. True immutability is impossible in Python.\n\n            3. This *does* have a minor a runtime performance `impact\n               <how-frozen>` when initializing new instances.  In other words:\n               ``__init__`` is slightly slower with ``frozen=True``.\n\n            4. If a class is frozen, you cannot modify ``self`` in\n               ``__attrs_post_init__`` or a self-written ``__init__``. You can\n               circumvent that limitation by using\n               ``object.__setattr__(self, \"attribute_name\", value)``.\n\n            5. Subclasses of a frozen class are frozen too.\n\n    :param bool weakref_slot: Make instances weak-referenceable.  This has no\n        effect unless ``slots`` is also enabled.\n    :param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated\n        attributes (Python 3.6 and later only) from the class body.\n\n        In this case, you **must** annotate every field.  If ``attrs``\n        encounters a field that is set to an `attr.ib` but lacks a type\n        annotation, an `attr.exceptions.UnannotatedAttributeError` is\n        raised.  Use ``field_name: typing.Any = attr.ib(...)`` if you don't\n        want to set a type.\n\n        If you assign a value to those attributes (e.g. ``x: int = 42``), that\n        value becomes the default value like if it were passed using\n        ``attr.ib(default=42)``.  Passing an instance of `attrs.Factory` also\n        works as expected in most cases (see warning below).\n\n        Attributes annotated as `typing.ClassVar`, and attributes that are\n        neither annotated nor set to an `attr.ib` are **ignored**.\n\n        .. warning::\n           For features that use the attribute name to create decorators (e.g.\n           `validators <validators>`), you still *must* assign `attr.ib` to\n           them. Otherwise Python will either not find the name or try to use\n           the default value to call e.g. ``validator`` on it.\n\n           These errors can be quite confusing and probably the most common bug\n           report on our bug tracker.\n\n        .. _`PEP 526`: https://www.python.org/dev/peps/pep-0526/\n    :param bool kw_only: Make all attributes keyword-only (Python 3+)\n        in the generated ``__init__`` (if ``init`` is ``False``, this\n        parameter is ignored).\n    :param bool cache_hash: Ensure that the object's hash code is computed\n        only once and stored on the object.  If this is set to ``True``,\n        hashing must be either explicitly or implicitly enabled for this\n        class.  If the hash code is cached, avoid any reassignments of\n        fields involved in hash code computation or mutations of the objects\n        those fields point to after object creation.  If such changes occur,\n        the behavior of the object's hash code is undefined.\n    :param bool auto_exc: If the class subclasses `BaseException`\n        (which implicitly includes any subclass of any exception), the\n        following happens to behave like a well-behaved Python exceptions\n        class:\n\n        - the values for *eq*, *order*, and *hash* are ignored and the\n          instances compare and hash by the instance's ids (N.B. ``attrs`` will\n          *not* remove existing implementations of ``__hash__`` or the equality\n          methods. It just won't add own ones.),\n        - all attributes that are either passed into ``__init__`` or have a\n          default value are additionally available as a tuple in the ``args``\n          attribute,\n        - the value of *str* is ignored leaving ``__str__`` to base classes.\n    :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs``\n       collects attributes from base classes.  The default behavior is\n       incorrect in certain cases of multiple inheritance.  It should be on by\n       default but is kept off for backward-compatibility.\n\n       See issue `#428 <https://github.com/python-attrs/attrs/issues/428>`_ for\n       more details.\n\n    :param Optional[bool] getstate_setstate:\n       .. note::\n          This is usually only interesting for slotted classes and you should\n          probably just set *auto_detect* to `True`.\n\n       If `True`, ``__getstate__`` and\n       ``__setstate__`` are generated and attached to the class. This is\n       necessary for slotted classes to be pickleable. If left `None`, it's\n       `True` by default for slotted classes and ``False`` for dict classes.\n\n       If *auto_detect* is `True`, and *getstate_setstate* is left `None`,\n       and **either** ``__getstate__`` or ``__setstate__`` is detected directly\n       on the class (i.e. not inherited), it is set to `False` (this is usually\n       what you want).\n\n    :param on_setattr: A callable that is run whenever the user attempts to set\n        an attribute (either by assignment like ``i.x = 42`` or by using\n        `setattr` like ``setattr(i, \"x\", 42)``). It receives the same arguments\n        as validators: the instance, the attribute that is being modified, and\n        the new value.\n\n        If no exception is raised, the attribute is set to the return value of\n        the callable.\n\n        If a list of callables is passed, they're automatically wrapped in an\n        `attrs.setters.pipe`.\n\n    :param Optional[callable] field_transformer:\n        A function that is called with the original class object and all\n        fields right before ``attrs`` finalizes the class.  You can use\n        this, e.g., to automatically add converters or validators to\n        fields based on their types.  See `transform-fields` for more details.\n\n    :param bool match_args:\n        If `True` (default), set ``__match_args__`` on the class to support\n        `PEP 634 <https://www.python.org/dev/peps/pep-0634/>`_ (Structural\n        Pattern Matching). It is a tuple of all positional-only ``__init__``\n        parameter names on Python 3.10 and later. Ignored on older Python\n        versions.\n\n    .. versionadded:: 16.0.0 *slots*\n    .. versionadded:: 16.1.0 *frozen*\n    .. versionadded:: 16.3.0 *str*\n    .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``.\n    .. versionchanged:: 17.1.0\n       *hash* supports ``None`` as value which is also the default now.\n    .. versionadded:: 17.3.0 *auto_attribs*\n    .. versionchanged:: 18.1.0\n       If *these* is passed, no attributes are deleted from the class body.\n    .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained.\n    .. versionadded:: 18.2.0 *weakref_slot*\n    .. deprecated:: 18.2.0\n       ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a\n       `DeprecationWarning` if the classes compared are subclasses of\n       each other. ``__eq`` and ``__ne__`` never tried to compared subclasses\n       to each other.\n    .. versionchanged:: 19.2.0\n       ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider\n       subclasses comparable anymore.\n    .. versionadded:: 18.2.0 *kw_only*\n    .. versionadded:: 18.2.0 *cache_hash*\n    .. versionadded:: 19.1.0 *auto_exc*\n    .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.\n    .. versionadded:: 19.2.0 *eq* and *order*\n    .. versionadded:: 20.1.0 *auto_detect*\n    .. versionadded:: 20.1.0 *collect_by_mro*\n    .. versionadded:: 20.1.0 *getstate_setstate*\n    .. versionadded:: 20.1.0 *on_setattr*\n    .. versionadded:: 20.3.0 *field_transformer*\n    .. versionchanged:: 21.1.0\n       ``init=False`` injects ``__attrs_init__``\n    .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__``\n    .. versionchanged:: 21.1.0 *cmp* undeprecated\n    .. versionadded:: 21.3.0 *match_args*\n    \"\"\"\n    if auto_detect and PY2:\n        raise PythonTooOldError(\n            \"auto_detect only works on Python 3 and later.\"\n        )\n\n    eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None)\n    hash_ = hash  # work around the lack of nonlocal\n\n    if isinstance(on_setattr, (list, tuple)):\n        on_setattr = setters.pipe(*on_setattr)\n\n    def wrap(cls):\n\n        if getattr(cls, \"__class__\", None) is None:\n            raise TypeError(\"attrs only works with new-style classes.\")\n\n        is_frozen = frozen or _has_frozen_base_class(cls)\n        is_exc = auto_exc is True and issubclass(cls, BaseException)\n        has_own_setattr = auto_detect and _has_own_attribute(\n            cls, \"__setattr__\"\n        )\n\n        if has_own_setattr and is_frozen:\n            raise ValueError(\"Can't freeze a class with a custom __setattr__.\")\n\n        builder = _ClassBuilder(\n            cls,\n            these,\n            slots,\n            is_frozen,\n            weakref_slot,\n            _determine_whether_to_implement(\n                cls,\n                getstate_setstate,\n                auto_detect,\n                (\"__getstate__\", \"__setstate__\"),\n                default=slots,\n            ),\n            auto_attribs,\n            kw_only,\n            cache_hash,\n            is_exc,\n            collect_by_mro,\n            on_setattr,\n            has_own_setattr,\n            field_transformer,\n        )\n        if _determine_whether_to_implement(\n            cls, repr, auto_detect, (\"__repr__\",)\n        ):\n            builder.add_repr(repr_ns)\n        if str is True:\n            builder.add_str()\n\n        eq = _determine_whether_to_implement(\n            cls, eq_, auto_detect, (\"__eq__\", \"__ne__\")\n        )\n        if not is_exc and eq is True:\n            builder.add_eq()\n        if not is_exc and _determine_whether_to_implement(\n            cls, order_, auto_detect, (\"__lt__\", \"__le__\", \"__gt__\", \"__ge__\")\n        ):\n            builder.add_order()\n\n        builder.add_setattr()\n\n        if (\n            hash_ is None\n            and auto_detect is True\n            and _has_own_attribute(cls, \"__hash__\")\n        ):\n            hash = False\n        else:\n            hash = hash_\n        if hash is not True and hash is not False and hash is not None:\n            # Can't use `hash in` because 1 == True for example.\n            raise TypeError(\n                \"Invalid value for hash.  Must be True, False, or None.\"\n            )\n        elif hash is False or (hash is None and eq is False) or is_exc:\n            # Don't do anything. Should fall back to __object__'s __hash__\n            # which is by id.\n            if cache_hash:\n                raise TypeError(\n                    \"Invalid value for cache_hash.  To use hash caching,\"\n                    \" hashing must be either explicitly or implicitly \"\n                    \"enabled.\"\n                )\n        elif hash is True or (\n            hash is None and eq is True and is_frozen is True\n        ):\n            # Build a __hash__ if told so, or if it's safe.\n            builder.add_hash()\n        else:\n            # Raise TypeError on attempts to hash.\n            if cache_hash:\n                raise TypeError(\n                    \"Invalid value for cache_hash.  To use hash caching,\"\n                    \" hashing must be either explicitly or implicitly \"\n                    \"enabled.\"\n                )\n            builder.make_unhashable()\n\n        if _determine_whether_to_implement(\n            cls, init, auto_detect, (\"__init__\",)\n        ):\n            builder.add_init()\n        else:\n            builder.add_attrs_init()\n            if cache_hash:\n                raise TypeError(\n                    \"Invalid value for cache_hash.  To use hash caching,\"\n                    \" init must be True.\"\n                )\n\n        if (\n            PY310\n            and match_args\n            and not _has_own_attribute(cls, \"__match_args__\")\n        ):\n            builder.add_match_args()\n\n        return builder.build_class()\n\n    # maybe_cls's type depends on the usage of the decorator.  It's a class\n    # if it's used as `@attrs` but ``None`` if used as `@attrs()`.\n    if maybe_cls is None:\n        return wrap\n    else:\n        return wrap(maybe_cls)\n\n\n_attrs = attrs\n\"\"\"\nInternal alias so we can use it in functions that take an argument called\n*attrs*.\n\"\"\"\n\n\nif PY2:\n\n    def _has_frozen_base_class(cls):\n        \"\"\"\n        Check whether *cls* has a frozen ancestor by looking at its\n        __setattr__.\n        \"\"\"\n        return (\n            getattr(cls.__setattr__, \"__module__\", None)\n            == _frozen_setattrs.__module__\n            and cls.__setattr__.__name__ == _frozen_setattrs.__name__\n        )\n\nelse:\n\n    def _has_frozen_base_class(cls):\n        \"\"\"\n        Check whether *cls* has a frozen ancestor by looking at its\n        __setattr__.\n        \"\"\"\n        return cls.__setattr__ == _frozen_setattrs\n\n\ndef _generate_unique_filename(cls, func_name):\n    \"\"\"\n    Create a \"filename\" suitable for a function being generated.\n    \"\"\"\n    unique_filename = \"<attrs generated {0} {1}.{2}>\".format(\n        func_name,\n        cls.__module__,\n        getattr(cls, \"__qualname__\", cls.__name__),\n    )\n    return unique_filename\n\n\ndef _make_hash(cls, attrs, frozen, cache_hash):\n    attrs = tuple(\n        a for a in attrs if a.hash is True or (a.hash is None and a.eq is True)\n    )\n\n    tab = \"        \"\n\n    unique_filename = _generate_unique_filename(cls, \"hash\")\n    type_hash = hash(unique_filename)\n\n    hash_def = \"def __hash__(self\"\n    hash_func = \"hash((\"\n    closing_braces = \"))\"\n    if not cache_hash:\n        hash_def += \"):\"\n    else:\n        if not PY2:\n            hash_def += \", *\"\n\n        hash_def += (\n            \", _cache_wrapper=\"\n            + \"__import__('attr._make')._make._CacheHashWrapper):\"\n        )\n        hash_func = \"_cache_wrapper(\" + hash_func\n        closing_braces += \")\"\n\n    method_lines = [hash_def]\n\n    def append_hash_computation_lines(prefix, indent):\n        \"\"\"\n        Generate the code for actually computing the hash code.\n        Below this will either be returned directly or used to compute\n        a value which is then cached, depending on the value of cache_hash\n        \"\"\"\n\n        method_lines.extend(\n            [\n                indent + prefix + hash_func,\n                indent + \"        %d,\" % (type_hash,),\n            ]\n        )\n\n        for a in attrs:\n            method_lines.append(indent + \"        self.%s,\" % a.name)\n\n        method_lines.append(indent + \"    \" + closing_braces)\n\n    if cache_hash:\n        method_lines.append(tab + \"if self.%s is None:\" % _hash_cache_field)\n        if frozen:\n            append_hash_computation_lines(\n                \"object.__setattr__(self, '%s', \" % _hash_cache_field, tab * 2\n            )\n            method_lines.append(tab * 2 + \")\")  # close __setattr__\n        else:\n            append_hash_computation_lines(\n                \"self.%s = \" % _hash_cache_field, tab * 2\n            )\n        method_lines.append(tab + \"return self.%s\" % _hash_cache_field)\n    else:\n        append_hash_computation_lines(\"return \", tab)\n\n    script = \"\\n\".join(method_lines)\n    return _make_method(\"__hash__\", script, unique_filename)\n\n\ndef _add_hash(cls, attrs):\n    \"\"\"\n    Add a hash method to *cls*.\n    \"\"\"\n    cls.__hash__ = _make_hash(cls, attrs, frozen=False, cache_hash=False)\n    return cls\n\n\ndef _make_ne():\n    \"\"\"\n    Create __ne__ method.\n    \"\"\"\n\n    def __ne__(self, other):\n        \"\"\"\n        Check equality and either forward a NotImplemented or\n        return the result negated.\n        \"\"\"\n        result = self.__eq__(other)\n        if result is NotImplemented:\n            return NotImplemented\n\n        return not result\n\n    return __ne__\n\n\ndef _make_eq(cls, attrs):\n    \"\"\"\n    Create __eq__ method for *cls* with *attrs*.\n    \"\"\"\n    attrs = [a for a in attrs if a.eq]\n\n    unique_filename = _generate_unique_filename(cls, \"eq\")\n    lines = [\n        \"def __eq__(self, other):\",\n        \"    if other.__class__ is not self.__class__:\",\n        \"        return NotImplemented\",\n    ]\n\n    # We can't just do a big self.x = other.x and... clause due to\n    # irregularities like nan == nan is false but (nan,) == (nan,) is true.\n    globs = {}\n    if attrs:\n        lines.append(\"    return  (\")\n        others = [\"    ) == (\"]\n        for a in attrs:\n            if a.eq_key:\n                cmp_name = \"_%s_key\" % (a.name,)\n                # Add the key function to the global namespace\n                # of the evaluated function.\n                globs[cmp_name] = a.eq_key\n                lines.append(\n                    \"        %s(self.%s),\"\n                    % (\n                        cmp_name,\n                        a.name,\n                    )\n                )\n                others.append(\n                    \"        %s(other.%s),\"\n                    % (\n                        cmp_name,\n                        a.name,\n                    )\n                )\n            else:\n                lines.append(\"        self.%s,\" % (a.name,))\n                others.append(\"        other.%s,\" % (a.name,))\n\n        lines += others + [\"    )\"]\n    else:\n        lines.append(\"    return True\")\n\n    script = \"\\n\".join(lines)\n\n    return _make_method(\"__eq__\", script, unique_filename, globs)\n\n\ndef _make_order(cls, attrs):\n    \"\"\"\n    Create ordering methods for *cls* with *attrs*.\n    \"\"\"\n    attrs = [a for a in attrs if a.order]\n\n    def attrs_to_tuple(obj):\n        \"\"\"\n        Save us some typing.\n        \"\"\"\n        return tuple(\n            key(value) if key else value\n            for value, key in (\n                (getattr(obj, a.name), a.order_key) for a in attrs\n            )\n        )\n\n    def __lt__(self, other):\n        \"\"\"\n        Automatically created by attrs.\n        \"\"\"\n        if other.__class__ is self.__class__:\n            return attrs_to_tuple(self) < attrs_to_tuple(other)\n\n        return NotImplemented\n\n    def __le__(self, other):\n        \"\"\"\n        Automatically created by attrs.\n        \"\"\"\n        if other.__class__ is self.__class__:\n            return attrs_to_tuple(self) <= attrs_to_tuple(other)\n\n        return NotImplemented\n\n    def __gt__(self, other):\n        \"\"\"\n        Automatically created by attrs.\n        \"\"\"\n        if other.__class__ is self.__class__:\n            return attrs_to_tuple(self) > attrs_to_tuple(other)\n\n        return NotImplemented\n\n    def __ge__(self, other):\n        \"\"\"\n        Automatically created by attrs.\n        \"\"\"\n        if other.__class__ is self.__class__:\n            return attrs_to_tuple(self) >= attrs_to_tuple(other)\n\n        return NotImplemented\n\n    return __lt__, __le__, __gt__, __ge__\n\n\ndef _add_eq(cls, attrs=None):\n    \"\"\"\n    Add equality methods to *cls* with *attrs*.\n    \"\"\"\n    if attrs is None:\n        attrs = cls.__attrs_attrs__\n\n    cls.__eq__ = _make_eq(cls, attrs)\n    cls.__ne__ = _make_ne()\n\n    return cls\n\n\nif HAS_F_STRINGS:\n\n    def _make_repr(attrs, ns, cls):\n        unique_filename = _generate_unique_filename(cls, \"repr\")\n        # Figure out which attributes to include, and which function to use to\n        # format them. The a.repr value can be either bool or a custom\n        # callable.\n        attr_names_with_reprs = tuple(\n            (a.name, (repr if a.repr is True else a.repr), a.init)\n            for a in attrs\n            if a.repr is not False\n        )\n        globs = {\n            name + \"_repr\": r\n            for name, r, _ in attr_names_with_reprs\n            if r != repr\n        }\n        globs[\"_compat\"] = _compat\n        globs[\"AttributeError\"] = AttributeError\n        globs[\"NOTHING\"] = NOTHING\n        attribute_fragments = []\n        for name, r, i in attr_names_with_reprs:\n            accessor = (\n                \"self.\" + name\n                if i\n                else 'getattr(self, \"' + name + '\", NOTHING)'\n            )\n            fragment = (\n                \"%s={%s!r}\" % (name, accessor)\n                if r == repr\n                else \"%s={%s_repr(%s)}\" % (name, name, accessor)\n            )\n            attribute_fragments.append(fragment)\n        repr_fragment = \", \".join(attribute_fragments)\n\n        if ns is None:\n            cls_name_fragment = (\n                '{self.__class__.__qualname__.rsplit(\">.\", 1)[-1]}'\n            )\n        else:\n            cls_name_fragment = ns + \".{self.__class__.__name__}\"\n\n        lines = [\n            \"def __repr__(self):\",\n            \"  try:\",\n            \"    already_repring = _compat.repr_context.already_repring\",\n            \"  except AttributeError:\",\n            \"    already_repring = {id(self),}\",\n            \"    _compat.repr_context.already_repring = already_repring\",\n            \"  else:\",\n            \"    if id(self) in already_repring:\",\n            \"      return '...'\",\n            \"    else:\",\n            \"      already_repring.add(id(self))\",\n            \"  try:\",\n            \"    return f'%s(%s)'\" % (cls_name_fragment, repr_fragment),\n            \"  finally:\",\n            \"    already_repring.remove(id(self))\",\n        ]\n\n        return _make_method(\n            \"__repr__\", \"\\n\".join(lines), unique_filename, globs=globs\n        )\n\nelse:\n\n    def _make_repr(attrs, ns, _):\n        \"\"\"\n        Make a repr method that includes relevant *attrs*, adding *ns* to the\n        full name.\n        \"\"\"\n\n        # Figure out which attributes to include, and which function to use to\n        # format them. The a.repr value can be either bool or a custom\n        # callable.\n        attr_names_with_reprs = tuple(\n            (a.name, repr if a.repr is True else a.repr)\n            for a in attrs\n            if a.repr is not False\n        )\n\n        def __repr__(self):\n            \"\"\"\n            Automatically created by attrs.\n            \"\"\"\n            try:\n                already_repring = _compat.repr_context.already_repring\n            except AttributeError:\n                already_repring = set()\n                _compat.repr_context.already_repring = already_repring\n\n            if id(self) in already_repring:\n                return \"...\"\n            real_cls = self.__class__\n            if ns is None:\n                qualname = getattr(real_cls, \"__qualname__\", None)\n                if qualname is not None:  # pragma: no cover\n                    # This case only happens on Python 3.5 and 3.6. We exclude\n                    # it from coverage, because we don't want to slow down our\n                    # test suite by running them under coverage too for this\n                    # one line.\n                    class_name = qualname.rsplit(\">.\", 1)[-1]\n                else:\n                    class_name = real_cls.__name__\n            else:\n                class_name = ns + \".\" + real_cls.__name__\n\n            # Since 'self' remains on the stack (i.e.: strongly referenced)\n            # for the duration of this call, it's safe to depend on id(...)\n            # stability, and not need to track the instance and therefore\n            # worry about properties like weakref- or hash-ability.\n            already_repring.add(id(self))\n            try:\n                result = [class_name, \"(\"]\n                first = True\n                for name, attr_repr in attr_names_with_reprs:\n                    if first:\n                        first = False\n                    else:\n                        result.append(\", \")\n                    result.extend(\n                        (name, \"=\", attr_repr(getattr(self, name, NOTHING)))\n                    )\n                return \"\".join(result) + \")\"\n            finally:\n                already_repring.remove(id(self))\n\n        return __repr__\n\n\ndef _add_repr(cls, ns=None, attrs=None):\n    \"\"\"\n    Add a repr method to *cls*.\n    \"\"\"\n    if attrs is None:\n        attrs = cls.__attrs_attrs__\n\n    cls.__repr__ = _make_repr(attrs, ns, cls)\n    return cls\n\n\ndef fields(cls):\n    \"\"\"\n    Return the tuple of ``attrs`` attributes for a class.\n\n    The tuple also allows accessing the fields by their names (see below for\n    examples).\n\n    :param type cls: Class to introspect.\n\n    :raise TypeError: If *cls* is not a class.\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class.\n\n    :rtype: tuple (with name accessors) of `attrs.Attribute`\n\n    ..  versionchanged:: 16.2.0 Returned tuple allows accessing the fields\n        by name.\n    \"\"\"\n    if not isclass(cls):\n        raise TypeError(\"Passed object must be a class.\")\n    attrs = getattr(cls, \"__attrs_attrs__\", None)\n    if attrs is None:\n        raise NotAnAttrsClassError(\n            \"{cls!r} is not an attrs-decorated class.\".format(cls=cls)\n        )\n    return attrs\n\n\ndef fields_dict(cls):\n    \"\"\"\n    Return an ordered dictionary of ``attrs`` attributes for a class, whose\n    keys are the attribute names.\n\n    :param type cls: Class to introspect.\n\n    :raise TypeError: If *cls* is not a class.\n    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``\n        class.\n\n    :rtype: an ordered dict where keys are attribute names and values are\n        `attrs.Attribute`\\\\ s. This will be a `dict` if it's\n        naturally ordered like on Python 3.6+ or an\n        :class:`~collections.OrderedDict` otherwise.\n\n    .. versionadded:: 18.1.0\n    \"\"\"\n    if not isclass(cls):\n        raise TypeError(\"Passed object must be a class.\")\n    attrs = getattr(cls, \"__attrs_attrs__\", None)\n    if attrs is None:\n        raise NotAnAttrsClassError(\n            \"{cls!r} is not an attrs-decorated class.\".format(cls=cls)\n        )\n    return ordered_dict(((a.name, a) for a in attrs))\n\n\ndef validate(inst):\n    \"\"\"\n    Validate all attributes on *inst* that have a validator.\n\n    Leaves all exceptions through.\n\n    :param inst: Instance of a class with ``attrs`` attributes.\n    \"\"\"\n    if _config._run_validators is False:\n        return\n\n    for a in fields(inst.__class__):\n        v = a.validator\n        if v is not None:\n            v(inst, a, getattr(inst, a.name))\n\n\ndef _is_slot_cls(cls):\n    return \"__slots__\" in cls.__dict__\n\n\ndef _is_slot_attr(a_name, base_attr_map):\n    \"\"\"\n    Check if the attribute name comes from a slot class.\n    \"\"\"\n    return a_name in base_attr_map and _is_slot_cls(base_attr_map[a_name])\n\n\ndef _make_init(\n    cls,\n    attrs,\n    pre_init,\n    post_init,\n    frozen,\n    slots,\n    cache_hash,\n    base_attr_map,\n    is_exc,\n    cls_on_setattr,\n    attrs_init,\n):\n    has_cls_on_setattr = (\n        cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP\n    )\n\n    if frozen and has_cls_on_setattr:\n        raise ValueError(\"Frozen classes can't use on_setattr.\")\n\n    needs_cached_setattr = cache_hash or frozen\n    filtered_attrs = []\n    attr_dict = {}\n    for a in attrs:\n        if not a.init and a.default is NOTHING:\n            continue\n\n        filtered_attrs.append(a)\n        attr_dict[a.name] = a\n\n        if a.on_setattr is not None:\n            if frozen is True:\n                raise ValueError(\"Frozen classes can't use on_setattr.\")\n\n            needs_cached_setattr = True\n        elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP:\n            needs_cached_setattr = True\n\n    unique_filename = _generate_unique_filename(cls, \"init\")\n\n    script, globs, annotations = _attrs_to_init_script(\n        filtered_attrs,\n        frozen,\n        slots,\n        pre_init,\n        post_init,\n        cache_hash,\n        base_attr_map,\n        is_exc,\n        needs_cached_setattr,\n        has_cls_on_setattr,\n        attrs_init,\n    )\n    if cls.__module__ in sys.modules:\n        # This makes typing.get_type_hints(CLS.__init__) resolve string types.\n        globs.update(sys.modules[cls.__module__].__dict__)\n\n    globs.update({\"NOTHING\": NOTHING, \"attr_dict\": attr_dict})\n\n    if needs_cached_setattr:\n        # Save the lookup overhead in __init__ if we need to circumvent\n        # setattr hooks.\n        globs[\"_cached_setattr\"] = _obj_setattr\n\n    init = _make_method(\n        \"__attrs_init__\" if attrs_init else \"__init__\",\n        script,\n        unique_filename,\n        globs,\n    )\n    init.__annotations__ = annotations\n\n    return init\n\n\ndef _setattr(attr_name, value_var, has_on_setattr):\n    \"\"\"\n    Use the cached object.setattr to set *attr_name* to *value_var*.\n    \"\"\"\n    return \"_setattr('%s', %s)\" % (attr_name, value_var)\n\n\ndef _setattr_with_converter(attr_name, value_var, has_on_setattr):\n    \"\"\"\n    Use the cached object.setattr to set *attr_name* to *value_var*, but run\n    its converter first.\n    \"\"\"\n    return \"_setattr('%s', %s(%s))\" % (\n        attr_name,\n        _init_converter_pat % (attr_name,),\n        value_var,\n    )\n\n\ndef _assign(attr_name, value, has_on_setattr):\n    \"\"\"\n    Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise\n    relegate to _setattr.\n    \"\"\"\n    if has_on_setattr:\n        return _setattr(attr_name, value, True)\n\n    return \"self.%s = %s\" % (attr_name, value)\n\n\ndef _assign_with_converter(attr_name, value_var, has_on_setattr):\n    \"\"\"\n    Unless *attr_name* has an on_setattr hook, use normal assignment after\n    conversion. Otherwise relegate to _setattr_with_converter.\n    \"\"\"\n    if has_on_setattr:\n        return _setattr_with_converter(attr_name, value_var, True)\n\n    return \"self.%s = %s(%s)\" % (\n        attr_name,\n        _init_converter_pat % (attr_name,),\n        value_var,\n    )\n\n\nif PY2:\n\n    def _unpack_kw_only_py2(attr_name, default=None):\n        \"\"\"\n        Unpack *attr_name* from _kw_only dict.\n        \"\"\"\n        if default is not None:\n            arg_default = \", %s\" % default\n        else:\n            arg_default = \"\"\n        return \"%s = _kw_only.pop('%s'%s)\" % (\n            attr_name,\n            attr_name,\n            arg_default,\n        )\n\n    def _unpack_kw_only_lines_py2(kw_only_args):\n        \"\"\"\n        Unpack all *kw_only_args* from _kw_only dict and handle errors.\n\n        Given a list of strings \"{attr_name}\" and \"{attr_name}={default}\"\n        generates list of lines of code that pop attrs from _kw_only dict and\n        raise TypeError similar to builtin if required attr is missing or\n        extra key is passed.\n\n        >>> print(\"\\n\".join(_unpack_kw_only_lines_py2([\"a\", \"b=42\"])))\n        try:\n            a = _kw_only.pop('a')\n            b = _kw_only.pop('b', 42)\n        except KeyError as _key_error:\n            raise TypeError(\n                ...\n        if _kw_only:\n            raise TypeError(\n                ...\n        \"\"\"\n        lines = [\"try:\"]\n        lines.extend(\n            \"    \" + _unpack_kw_only_py2(*arg.split(\"=\"))\n            for arg in kw_only_args\n        )\n        lines += \"\"\"\\\nexcept KeyError as _key_error:\n    raise TypeError(\n        '__init__() missing required keyword-only argument: %s' % _key_error\n    )\nif _kw_only:\n    raise TypeError(\n        '__init__() got an unexpected keyword argument %r'\n        % next(iter(_kw_only))\n    )\n\"\"\".split(\n            \"\\n\"\n        )\n        return lines\n\n\ndef _attrs_to_init_script(\n    attrs,\n    frozen,\n    slots,\n    pre_init,\n    post_init,\n    cache_hash,\n    base_attr_map,\n    is_exc,\n    needs_cached_setattr,\n    has_cls_on_setattr,\n    attrs_init,\n):\n    \"\"\"\n    Return a script of an initializer for *attrs* and a dict of globals.\n\n    The globals are expected by the generated script.\n\n    If *frozen* is True, we cannot set the attributes directly so we use\n    a cached ``object.__setattr__``.\n    \"\"\"\n    lines = []\n    if pre_init:\n        lines.append(\"self.__attrs_pre_init__()\")\n\n    if needs_cached_setattr:\n        lines.append(\n            # Circumvent the __setattr__ descriptor to save one lookup per\n            # assignment.\n            # Note _setattr will be used again below if cache_hash is True\n            \"_setattr = _cached_setattr.__get__(self, self.__class__)\"\n        )\n\n    if frozen is True:\n        if slots is True:\n            fmt_setter = _setattr\n            fmt_setter_with_converter = _setattr_with_converter\n        else:\n            # Dict frozen classes assign directly to __dict__.\n            # But only if the attribute doesn't come from an ancestor slot\n            # class.\n            # Note _inst_dict will be used again below if cache_hash is True\n            lines.append(\"_inst_dict = self.__dict__\")\n\n            def fmt_setter(attr_name, value_var, has_on_setattr):\n                if _is_slot_attr(attr_name, base_attr_map):\n                    return _setattr(attr_name, value_var, has_on_setattr)\n\n                return \"_inst_dict['%s'] = %s\" % (attr_name, value_var)\n\n            def fmt_setter_with_converter(\n                attr_name, value_var, has_on_setattr\n            ):\n                if has_on_setattr or _is_slot_attr(attr_name, base_attr_map):\n                    return _setattr_with_converter(\n                        attr_name, value_var, has_on_setattr\n                    )\n\n                return \"_inst_dict['%s'] = %s(%s)\" % (\n                    attr_name,\n                    _init_converter_pat % (attr_name,),\n                    value_var,\n                )\n\n    else:\n        # Not frozen.\n        fmt_setter = _assign\n        fmt_setter_with_converter = _assign_with_converter\n\n    args = []\n    kw_only_args = []\n    attrs_to_validate = []\n\n    # This is a dictionary of names to validator and converter callables.\n    # Injecting this into __init__ globals lets us avoid lookups.\n    names_for_globals = {}\n    annotations = {\"return\": None}\n\n    for a in attrs:\n        if a.validator:\n            attrs_to_validate.append(a)\n\n        attr_name = a.name\n        has_on_setattr = a.on_setattr is not None or (\n            a.on_setattr is not setters.NO_OP and has_cls_on_setattr\n        )\n        arg_name = a.name.lstrip(\"_\")\n\n        has_factory = isinstance(a.default, Factory)\n        if has_factory and a.default.takes_self:\n            maybe_self = \"self\"\n        else:\n            maybe_self = \"\"\n\n        if a.init is False:\n            if has_factory:\n                init_factory_name = _init_factory_pat.format(a.name)\n                if a.converter is not None:\n                    lines.append(\n                        fmt_setter_with_converter(\n                            attr_name,\n                            init_factory_name + \"(%s)\" % (maybe_self,),\n                            has_on_setattr,\n                        )\n                    )\n                    conv_name = _init_converter_pat % (a.name,)\n                    names_for_globals[conv_name] = a.converter\n                else:\n                    lines.append(\n                        fmt_setter(\n                            attr_name,\n                            init_factory_name + \"(%s)\" % (maybe_self,),\n                            has_on_setattr,\n                        )\n                    )\n                names_for_globals[init_factory_name] = a.default.factory\n            else:\n                if a.converter is not None:\n                    lines.append(\n                        fmt_setter_with_converter(\n                            attr_name,\n                            \"attr_dict['%s'].default\" % (attr_name,),\n                            has_on_setattr,\n                        )\n                    )\n                    conv_name = _init_converter_pat % (a.name,)\n                    names_for_globals[conv_name] = a.converter\n                else:\n                    lines.append(\n                        fmt_setter(\n                            attr_name,\n                            \"attr_dict['%s'].default\" % (attr_name,),\n                            has_on_setattr,\n                        )\n                    )\n        elif a.default is not NOTHING and not has_factory:\n            arg = \"%s=attr_dict['%s'].default\" % (arg_name, attr_name)\n            if a.kw_only:\n                kw_only_args.append(arg)\n            else:\n                args.append(arg)\n\n            if a.converter is not None:\n                lines.append(\n                    fmt_setter_with_converter(\n                        attr_name, arg_name, has_on_setattr\n                    )\n                )\n                names_for_globals[\n                    _init_converter_pat % (a.name,)\n                ] = a.converter\n            else:\n                lines.append(fmt_setter(attr_name, arg_name, has_on_setattr))\n\n        elif has_factory:\n            arg = \"%s=NOTHING\" % (arg_name,)\n            if a.kw_only:\n                kw_only_args.append(arg)\n            else:\n                args.append(arg)\n            lines.append(\"if %s is not NOTHING:\" % (arg_name,))\n\n            init_factory_name = _init_factory_pat.format(a.name)\n            if a.converter is not None:\n                lines.append(\n                    \"    \"\n                    + fmt_setter_with_converter(\n                        attr_name, arg_name, has_on_setattr\n                    )\n                )\n                lines.append(\"else:\")\n                lines.append(\n                    \"    \"\n                    + fmt_setter_with_converter(\n                        attr_name,\n                        init_factory_name + \"(\" + maybe_self + \")\",\n                        has_on_setattr,\n                    )\n                )\n                names_for_globals[\n                    _init_converter_pat % (a.name,)\n                ] = a.converter\n            else:\n                lines.append(\n                    \"    \" + fmt_setter(attr_name, arg_name, has_on_setattr)\n                )\n                lines.append(\"else:\")\n                lines.append(\n                    \"    \"\n                    + fmt_setter(\n                        attr_name,\n                        init_factory_name + \"(\" + maybe_self + \")\",\n                        has_on_setattr,\n                    )\n                )\n            names_for_globals[init_factory_name] = a.default.factory\n        else:\n            if a.kw_only:\n                kw_only_args.append(arg_name)\n            else:\n                args.append(arg_name)\n\n            if a.converter is not None:\n                lines.append(\n                    fmt_setter_with_converter(\n                        attr_name, arg_name, has_on_setattr\n                    )\n                )\n                names_for_globals[\n                    _init_converter_pat % (a.name,)\n                ] = a.converter\n            else:\n                lines.append(fmt_setter(attr_name, arg_name, has_on_setattr))\n\n        if a.init is True:\n            if a.type is not None and a.converter is None:\n                annotations[arg_name] = a.type\n            elif a.converter is not None and not PY2:\n                # Try to get the type from the converter.\n                sig = None\n                try:\n                    sig = inspect.signature(a.converter)\n                except (ValueError, TypeError):  # inspect failed\n                    pass\n                if sig:\n                    sig_params = list(sig.parameters.values())\n                    if (\n                        sig_params\n                        and sig_params[0].annotation\n                        is not inspect.Parameter.empty\n                    ):\n                        annotations[arg_name] = sig_params[0].annotation\n\n    if attrs_to_validate:  # we can skip this if there are no validators.\n        names_for_globals[\"_config\"] = _config\n        lines.append(\"if _config._run_validators is True:\")\n        for a in attrs_to_validate:\n            val_name = \"__attr_validator_\" + a.name\n            attr_name = \"__attr_\" + a.name\n            lines.append(\n                \"    %s(self, %s, self.%s)\" % (val_name, attr_name, a.name)\n            )\n            names_for_globals[val_name] = a.validator\n            names_for_globals[attr_name] = a\n\n    if post_init:\n        lines.append(\"self.__attrs_post_init__()\")\n\n    # because this is set only after __attrs_post_init is called, a crash\n    # will result if post-init tries to access the hash code.  This seemed\n    # preferable to setting this beforehand, in which case alteration to\n    # field values during post-init combined with post-init accessing the\n    # hash code would result in silent bugs.\n    if cache_hash:\n        if frozen:\n            if slots:\n                # if frozen and slots, then _setattr defined above\n                init_hash_cache = \"_setattr('%s', %s)\"\n            else:\n                # if frozen and not slots, then _inst_dict defined above\n                init_hash_cache = \"_inst_dict['%s'] = %s\"\n        else:\n            init_hash_cache = \"self.%s = %s\"\n        lines.append(init_hash_cache % (_hash_cache_field, \"None\"))\n\n    # For exceptions we rely on BaseException.__init__ for proper\n    # initialization.\n    if is_exc:\n        vals = \",\".join(\"self.\" + a.name for a in attrs if a.init)\n\n        lines.append(\"BaseException.__init__(self, %s)\" % (vals,))\n\n    args = \", \".join(args)\n    if kw_only_args:\n        if PY2:\n            lines = _unpack_kw_only_lines_py2(kw_only_args) + lines\n\n            args += \"%s**_kw_only\" % (\", \" if args else \"\",)  # leading comma\n        else:\n            args += \"%s*, %s\" % (\n                \", \" if args else \"\",  # leading comma\n                \", \".join(kw_only_args),  # kw_only args\n            )\n    return (\n        \"\"\"\\\ndef {init_name}(self, {args}):\n    {lines}\n\"\"\".format(\n            init_name=(\"__attrs_init__\" if attrs_init else \"__init__\"),\n            args=args,\n            lines=\"\\n    \".join(lines) if lines else \"pass\",\n        ),\n        names_for_globals,\n        annotations,\n    )\n\n\nclass Attribute(object):\n    \"\"\"\n    *Read-only* representation of an attribute.\n\n    The class has *all* arguments of `attr.ib` (except for ``factory``\n    which is only syntactic sugar for ``default=Factory(...)`` plus the\n    following:\n\n    - ``name`` (`str`): The name of the attribute.\n    - ``inherited`` (`bool`): Whether or not that attribute has been inherited\n      from a base class.\n    - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables\n      that are used for comparing and ordering objects by this attribute,\n      respectively. These are set by passing a callable to `attr.ib`'s ``eq``,\n      ``order``, or ``cmp`` arguments. See also :ref:`comparison customization\n      <custom-comparison>`.\n\n    Instances of this class are frequently used for introspection purposes\n    like:\n\n    - `fields` returns a tuple of them.\n    - Validators get them passed as the first argument.\n    - The :ref:`field transformer <transform-fields>` hook receives a list of\n      them.\n\n    .. versionadded:: 20.1.0 *inherited*\n    .. versionadded:: 20.1.0 *on_setattr*\n    .. versionchanged:: 20.2.0 *inherited* is not taken into account for\n        equality checks and hashing anymore.\n    .. versionadded:: 21.1.0 *eq_key* and *order_key*\n\n    For the full version history of the fields, see `attr.ib`.\n    \"\"\"\n\n    __slots__ = (\n        \"name\",\n        \"default\",\n        \"validator\",\n        \"repr\",\n        \"eq\",\n        \"eq_key\",\n        \"order\",\n        \"order_key\",\n        \"hash\",\n        \"init\",\n        \"metadata\",\n        \"type\",\n        \"converter\",\n        \"kw_only\",\n        \"inherited\",\n        \"on_setattr\",\n    )\n\n    def __init__(\n        self,\n        name,\n        default,\n        validator,\n        repr,\n        cmp,  # XXX: unused, remove along with other cmp code.\n        hash,\n        init,\n        inherited,\n        metadata=None,\n        type=None,\n        converter=None,\n        kw_only=False,\n        eq=None,\n        eq_key=None,\n        order=None,\n        order_key=None,\n        on_setattr=None,\n    ):\n        eq, eq_key, order, order_key = _determine_attrib_eq_order(\n            cmp, eq_key or eq, order_key or order, True\n        )\n\n        # Cache this descriptor here to speed things up later.\n        bound_setattr = _obj_setattr.__get__(self, Attribute)\n\n        # Despite the big red warning, people *do* instantiate `Attribute`\n        # themselves.\n        bound_setattr(\"name\", name)\n        bound_setattr(\"default\", default)\n        bound_setattr(\"validator\", validator)\n        bound_setattr(\"repr\", repr)\n        bound_setattr(\"eq\", eq)\n        bound_setattr(\"eq_key\", eq_key)\n        bound_setattr(\"order\", order)\n        bound_setattr(\"order_key\", order_key)\n        bound_setattr(\"hash\", hash)\n        bound_setattr(\"init\", init)\n        bound_setattr(\"converter\", converter)\n        bound_setattr(\n            \"metadata\",\n            (\n                metadata_proxy(metadata)\n                if metadata\n                else _empty_metadata_singleton\n            ),\n        )\n        bound_setattr(\"type\", type)\n        bound_setattr(\"kw_only\", kw_only)\n        bound_setattr(\"inherited\", inherited)\n        bound_setattr(\"on_setattr\", on_setattr)\n\n    def __setattr__(self, name, value):\n        raise FrozenInstanceError()\n\n    @classmethod\n    def from_counting_attr(cls, name, ca, type=None):\n        # type holds the annotated value. deal with conflicts:\n        if type is None:\n            type = ca.type\n        elif ca.type is not None:\n            raise ValueError(\n                \"Type annotation and type argument cannot both be present\"\n            )\n        inst_dict = {\n            k: getattr(ca, k)\n            for k in Attribute.__slots__\n            if k\n            not in (\n                \"name\",\n                \"validator\",\n                \"default\",\n                \"type\",\n                \"inherited\",\n            )  # exclude methods and deprecated alias\n        }\n        return cls(\n            name=name,\n            validator=ca._validator,\n            default=ca._default,\n            type=type,\n            cmp=None,\n            inherited=False,\n            **inst_dict\n        )\n\n    @property\n    def cmp(self):\n        \"\"\"\n        Simulate the presence of a cmp attribute and warn.\n        \"\"\"\n        warnings.warn(_CMP_DEPRECATION, DeprecationWarning, stacklevel=2)\n\n        return self.eq and self.order\n\n    # Don't use attr.evolve since fields(Attribute) doesn't work\n    def evolve(self, **changes):\n        \"\"\"\n        Copy *self* and apply *changes*.\n\n        This works similarly to `attr.evolve` but that function does not work\n        with ``Attribute``.\n\n        It is mainly meant to be used for `transform-fields`.\n\n        .. versionadded:: 20.3.0\n        \"\"\"\n        new = copy.copy(self)\n\n        new._setattrs(changes.items())\n\n        return new\n\n    # Don't use _add_pickle since fields(Attribute) doesn't work\n    def __getstate__(self):\n        \"\"\"\n        Play nice with pickle.\n        \"\"\"\n        return tuple(\n            getattr(self, name) if name != \"metadata\" else dict(self.metadata)\n            for name in self.__slots__\n        )\n\n    def __setstate__(self, state):\n        \"\"\"\n        Play nice with pickle.\n        \"\"\"\n        self._setattrs(zip(self.__slots__, state))\n\n    def _setattrs(self, name_values_pairs):\n        bound_setattr = _obj_setattr.__get__(self, Attribute)\n        for name, value in name_values_pairs:\n            if name != \"metadata\":\n                bound_setattr(name, value)\n            else:\n                bound_setattr(\n                    name,\n                    metadata_proxy(value)\n                    if value\n                    else _empty_metadata_singleton,\n                )\n\n\n_a = [\n    Attribute(\n        name=name,\n        default=NOTHING,\n        validator=None,\n        repr=True,\n        cmp=None,\n        eq=True,\n        order=False,\n        hash=(name != \"metadata\"),\n        init=True,\n        inherited=False,\n    )\n    for name in Attribute.__slots__\n]\n\nAttribute = _add_hash(\n    _add_eq(\n        _add_repr(Attribute, attrs=_a),\n        attrs=[a for a in _a if a.name != \"inherited\"],\n    ),\n    attrs=[a for a in _a if a.hash and a.name != \"inherited\"],\n)\n\n\nclass _CountingAttr(object):\n    \"\"\"\n    Intermediate representation of attributes that uses a counter to preserve\n    the order in which the attributes have been defined.\n\n    *Internal* data structure of the attrs library.  Running into is most\n    likely the result of a bug like a forgotten `@attr.s` decorator.\n    \"\"\"\n\n    __slots__ = (\n        \"counter\",\n        \"_default\",\n        \"repr\",\n        \"eq\",\n        \"eq_key\",\n        \"order\",\n        \"order_key\",\n        \"hash\",\n        \"init\",\n        \"metadata\",\n        \"_validator\",\n        \"converter\",\n        \"type\",\n        \"kw_only\",\n        \"on_setattr\",\n    )\n    __attrs_attrs__ = tuple(\n        Attribute(\n            name=name,\n            default=NOTHING,\n            validator=None,\n            repr=True,\n            cmp=None,\n            hash=True,\n            init=True,\n            kw_only=False,\n            eq=True,\n            eq_key=None,\n            order=False,\n            order_key=None,\n            inherited=False,\n            on_setattr=None,\n        )\n        for name in (\n            \"counter\",\n            \"_default\",\n            \"repr\",\n            \"eq\",\n            \"order\",\n            \"hash\",\n            \"init\",\n            \"on_setattr\",\n        )\n    ) + (\n        Attribute(\n            name=\"metadata\",\n            default=None,\n            validator=None,\n            repr=True,\n            cmp=None,\n            hash=False,\n            init=True,\n            kw_only=False,\n            eq=True,\n            eq_key=None,\n            order=False,\n            order_key=None,\n            inherited=False,\n            on_setattr=None,\n        ),\n    )\n    cls_counter = 0\n\n    def __init__(\n        self,\n        default,\n        validator,\n        repr,\n        cmp,\n        hash,\n        init,\n        converter,\n        metadata,\n        type,\n        kw_only,\n        eq,\n        eq_key,\n        order,\n        order_key,\n        on_setattr,\n    ):\n        _CountingAttr.cls_counter += 1\n        self.counter = _CountingAttr.cls_counter\n        self._default = default\n        self._validator = validator\n        self.converter = converter\n        self.repr = repr\n        self.eq = eq\n        self.eq_key = eq_key\n        self.order = order\n        self.order_key = order_key\n        self.hash = hash\n        self.init = init\n        self.metadata = metadata\n        self.type = type\n        self.kw_only = kw_only\n        self.on_setattr = on_setattr\n\n    def validator(self, meth):\n        \"\"\"\n        Decorator that adds *meth* to the list of validators.\n\n        Returns *meth* unchanged.\n\n        .. versionadded:: 17.1.0\n        \"\"\"\n        if self._validator is None:\n            self._validator = meth\n        else:\n            self._validator = and_(self._validator, meth)\n        return meth\n\n    def default(self, meth):\n        \"\"\"\n        Decorator that allows to set the default for an attribute.\n\n        Returns *meth* unchanged.\n\n        :raises DefaultAlreadySetError: If default has been set before.\n\n        .. versionadded:: 17.1.0\n        \"\"\"\n        if self._default is not NOTHING:\n            raise DefaultAlreadySetError()\n\n        self._default = Factory(meth, takes_self=True)\n\n        return meth\n\n\n_CountingAttr = _add_eq(_add_repr(_CountingAttr))\n\n\nclass Factory(object):\n    \"\"\"\n    Stores a factory callable.\n\n    If passed as the default value to `attrs.field`, the factory is used to\n    generate a new value.\n\n    :param callable factory: A callable that takes either none or exactly one\n        mandatory positional argument depending on *takes_self*.\n    :param bool takes_self: Pass the partially initialized instance that is\n        being initialized as a positional argument.\n\n    .. versionadded:: 17.1.0  *takes_self*\n    \"\"\"\n\n    __slots__ = (\"factory\", \"takes_self\")\n\n    def __init__(self, factory, takes_self=False):\n        \"\"\"\n        `Factory` is part of the default machinery so if we want a default\n        value here, we have to implement it ourselves.\n        \"\"\"\n        self.factory = factory\n        self.takes_self = takes_self\n\n    def __getstate__(self):\n        \"\"\"\n        Play nice with pickle.\n        \"\"\"\n        return tuple(getattr(self, name) for name in self.__slots__)\n\n    def __setstate__(self, state):\n        \"\"\"\n        Play nice with pickle.\n        \"\"\"\n        for name, value in zip(self.__slots__, state):\n            setattr(self, name, value)\n\n\n_f = [\n    Attribute(\n        name=name,\n        default=NOTHING,\n        validator=None,\n        repr=True,\n        cmp=None,\n        eq=True,\n        order=False,\n        hash=True,\n        init=True,\n        inherited=False,\n    )\n    for name in Factory.__slots__\n]\n\nFactory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f)\n\n\ndef make_class(name, attrs, bases=(object,), **attributes_arguments):\n    \"\"\"\n    A quick way to create a new class called *name* with *attrs*.\n\n    :param str name: The name for the new class.\n\n    :param attrs: A list of names or a dictionary of mappings of names to\n        attributes.\n\n        If *attrs* is a list or an ordered dict (`dict` on Python 3.6+,\n        `collections.OrderedDict` otherwise), the order is deduced from\n        the order of the names or attributes inside *attrs*.  Otherwise the\n        order of the definition of the attributes is used.\n    :type attrs: `list` or `dict`\n\n    :param tuple bases: Classes that the new class will subclass.\n\n    :param attributes_arguments: Passed unmodified to `attr.s`.\n\n    :return: A new class with *attrs*.\n    :rtype: type\n\n    .. versionadded:: 17.1.0 *bases*\n    .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained.\n    \"\"\"\n    if isinstance(attrs, dict):\n        cls_dict = attrs\n    elif isinstance(attrs, (list, tuple)):\n        cls_dict = dict((a, attrib()) for a in attrs)\n    else:\n        raise TypeError(\"attrs argument must be a dict or a list.\")\n\n    pre_init = cls_dict.pop(\"__attrs_pre_init__\", None)\n    post_init = cls_dict.pop(\"__attrs_post_init__\", None)\n    user_init = cls_dict.pop(\"__init__\", None)\n\n    body = {}\n    if pre_init is not None:\n        body[\"__attrs_pre_init__\"] = pre_init\n    if post_init is not None:\n        body[\"__attrs_post_init__\"] = post_init\n    if user_init is not None:\n        body[\"__init__\"] = user_init\n\n    type_ = new_class(name, bases, {}, lambda ns: ns.update(body))\n\n    # For pickling to work, the __module__ variable needs to be set to the\n    # frame where the class is created.  Bypass this step in environments where\n    # sys._getframe is not defined (Jython for example) or sys._getframe is not\n    # defined for arguments greater than 0 (IronPython).\n    try:\n        type_.__module__ = sys._getframe(1).f_globals.get(\n            \"__name__\", \"__main__\"\n        )\n    except (AttributeError, ValueError):\n        pass\n\n    # We do it here for proper warnings with meaningful stacklevel.\n    cmp = attributes_arguments.pop(\"cmp\", None)\n    (\n        attributes_arguments[\"eq\"],\n        attributes_arguments[\"order\"],\n    ) = _determine_attrs_eq_order(\n        cmp,\n        attributes_arguments.get(\"eq\"),\n        attributes_arguments.get(\"order\"),\n        True,\n    )\n\n    return _attrs(these=cls_dict, **attributes_arguments)(type_)\n\n\n# These are required by within this module so we define them here and merely\n# import into .validators / .converters.\n\n\n@attrs(slots=True, hash=True)\nclass _AndValidator(object):\n    \"\"\"\n    Compose many validators to a single one.\n    \"\"\"\n\n    _validators = attrib()\n\n    def __call__(self, inst, attr, value):\n        for v in self._validators:\n            v(inst, attr, value)\n\n\ndef and_(*validators):\n    \"\"\"\n    A validator that composes multiple validators into one.\n\n    When called on a value, it runs all wrapped validators.\n\n    :param callables validators: Arbitrary number of validators.\n\n    .. versionadded:: 17.1.0\n    \"\"\"\n    vals = []\n    for validator in validators:\n        vals.extend(\n            validator._validators\n            if isinstance(validator, _AndValidator)\n            else [validator]\n        )\n\n    return _AndValidator(tuple(vals))\n\n\ndef pipe(*converters):\n    \"\"\"\n    A converter that composes multiple converters into one.\n\n    When called on a value, it runs all wrapped converters, returning the\n    *last* value.\n\n    Type annotations will be inferred from the wrapped converters', if\n    they have any.\n\n    :param callables converters: Arbitrary number of converters.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n\n    def pipe_converter(val):\n        for converter in converters:\n            val = converter(val)\n\n        return val\n\n    if not PY2:\n        if not converters:\n            # If the converter list is empty, pipe_converter is the identity.\n            A = typing.TypeVar(\"A\")\n            pipe_converter.__annotations__ = {\"val\": A, \"return\": A}\n        else:\n            # Get parameter type.\n            sig = None\n            try:\n                sig = inspect.signature(converters[0])\n            except (ValueError, TypeError):  # inspect failed\n                pass\n            if sig:\n                params = list(sig.parameters.values())\n                if (\n                    params\n                    and params[0].annotation is not inspect.Parameter.empty\n                ):\n                    pipe_converter.__annotations__[\"val\"] = params[\n                        0\n                    ].annotation\n            # Get return type.\n            sig = None\n            try:\n                sig = inspect.signature(converters[-1])\n            except (ValueError, TypeError):  # inspect failed\n                pass\n            if sig and sig.return_annotation is not inspect.Signature().empty:\n                pipe_converter.__annotations__[\n                    \"return\"\n                ] = sig.return_annotation\n\n    return pipe_converter\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/_next_gen.py",
    "content": "# SPDX-License-Identifier: MIT\n\n\"\"\"\nThese are Python 3.6+-only and keyword-only APIs that call `attr.s` and\n`attr.ib` with different default values.\n\"\"\"\n\n\nfrom functools import partial\n\nfrom . import setters\nfrom ._funcs import asdict as _asdict\nfrom ._funcs import astuple as _astuple\nfrom ._make import (\n    NOTHING,\n    _frozen_setattrs,\n    _ng_default_on_setattr,\n    attrib,\n    attrs,\n)\nfrom .exceptions import UnannotatedAttributeError\n\n\ndef define(\n    maybe_cls=None,\n    *,\n    these=None,\n    repr=None,\n    hash=None,\n    init=None,\n    slots=True,\n    frozen=False,\n    weakref_slot=True,\n    str=False,\n    auto_attribs=None,\n    kw_only=False,\n    cache_hash=False,\n    auto_exc=True,\n    eq=None,\n    order=False,\n    auto_detect=True,\n    getstate_setstate=None,\n    on_setattr=None,\n    field_transformer=None,\n    match_args=True,\n):\n    r\"\"\"\n    Define an ``attrs`` class.\n\n    Differences to the classic `attr.s` that it uses underneath:\n\n    - Automatically detect whether or not *auto_attribs* should be `True`\n      (c.f. *auto_attribs* parameter).\n    - If *frozen* is `False`, run converters and validators when setting an\n      attribute by default.\n    - *slots=True* (see :term:`slotted classes` for potentially surprising\n      behaviors)\n    - *auto_exc=True*\n    - *auto_detect=True*\n    - *order=False*\n    - *match_args=True*\n    - Some options that were only relevant on Python 2 or were kept around for\n      backwards-compatibility have been removed.\n\n    Please note that these are all defaults and you can change them as you\n    wish.\n\n    :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves\n       exactly like `attr.s`. If left `None`, `attr.s` will try to guess:\n\n       1. If any attributes are annotated and no unannotated `attrs.fields`\\ s\n          are found, it assumes *auto_attribs=True*.\n       2. Otherwise it assumes *auto_attribs=False* and tries to collect\n          `attrs.fields`\\ s.\n\n    For now, please refer to `attr.s` for the rest of the parameters.\n\n    .. versionadded:: 20.1.0\n    .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``.\n    \"\"\"\n\n    def do_it(cls, auto_attribs):\n        return attrs(\n            maybe_cls=cls,\n            these=these,\n            repr=repr,\n            hash=hash,\n            init=init,\n            slots=slots,\n            frozen=frozen,\n            weakref_slot=weakref_slot,\n            str=str,\n            auto_attribs=auto_attribs,\n            kw_only=kw_only,\n            cache_hash=cache_hash,\n            auto_exc=auto_exc,\n            eq=eq,\n            order=order,\n            auto_detect=auto_detect,\n            collect_by_mro=True,\n            getstate_setstate=getstate_setstate,\n            on_setattr=on_setattr,\n            field_transformer=field_transformer,\n            match_args=match_args,\n        )\n\n    def wrap(cls):\n        \"\"\"\n        Making this a wrapper ensures this code runs during class creation.\n\n        We also ensure that frozen-ness of classes is inherited.\n        \"\"\"\n        nonlocal frozen, on_setattr\n\n        had_on_setattr = on_setattr not in (None, setters.NO_OP)\n\n        # By default, mutable classes convert & validate on setattr.\n        if frozen is False and on_setattr is None:\n            on_setattr = _ng_default_on_setattr\n\n        # However, if we subclass a frozen class, we inherit the immutability\n        # and disable on_setattr.\n        for base_cls in cls.__bases__:\n            if base_cls.__setattr__ is _frozen_setattrs:\n                if had_on_setattr:\n                    raise ValueError(\n                        \"Frozen classes can't use on_setattr \"\n                        \"(frozen-ness was inherited).\"\n                    )\n\n                on_setattr = setters.NO_OP\n                break\n\n        if auto_attribs is not None:\n            return do_it(cls, auto_attribs)\n\n        try:\n            return do_it(cls, True)\n        except UnannotatedAttributeError:\n            return do_it(cls, False)\n\n    # maybe_cls's type depends on the usage of the decorator.  It's a class\n    # if it's used as `@attrs` but ``None`` if used as `@attrs()`.\n    if maybe_cls is None:\n        return wrap\n    else:\n        return wrap(maybe_cls)\n\n\nmutable = define\nfrozen = partial(define, frozen=True, on_setattr=None)\n\n\ndef field(\n    *,\n    default=NOTHING,\n    validator=None,\n    repr=True,\n    hash=None,\n    init=True,\n    metadata=None,\n    converter=None,\n    factory=None,\n    kw_only=False,\n    eq=None,\n    order=None,\n    on_setattr=None,\n):\n    \"\"\"\n    Identical to `attr.ib`, except keyword-only and with some arguments\n    removed.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n    return attrib(\n        default=default,\n        validator=validator,\n        repr=repr,\n        hash=hash,\n        init=init,\n        metadata=metadata,\n        converter=converter,\n        factory=factory,\n        kw_only=kw_only,\n        eq=eq,\n        order=order,\n        on_setattr=on_setattr,\n    )\n\n\ndef asdict(inst, *, recurse=True, filter=None, value_serializer=None):\n    \"\"\"\n    Same as `attr.asdict`, except that collections types are always retained\n    and dict is always used as *dict_factory*.\n\n    .. versionadded:: 21.3.0\n    \"\"\"\n    return _asdict(\n        inst=inst,\n        recurse=recurse,\n        filter=filter,\n        value_serializer=value_serializer,\n        retain_collection_types=True,\n    )\n\n\ndef astuple(inst, *, recurse=True, filter=None):\n    \"\"\"\n    Same as `attr.astuple`, except that collections types are always retained\n    and `tuple` is always used as the *tuple_factory*.\n\n    .. versionadded:: 21.3.0\n    \"\"\"\n    return _astuple(\n        inst=inst, recurse=recurse, filter=filter, retain_collection_types=True\n    )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/_version_info.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom __future__ import absolute_import, division, print_function\n\nfrom functools import total_ordering\n\nfrom ._funcs import astuple\nfrom ._make import attrib, attrs\n\n\n@total_ordering\n@attrs(eq=False, order=False, slots=True, frozen=True)\nclass VersionInfo(object):\n    \"\"\"\n    A version object that can be compared to tuple of length 1--4:\n\n    >>> attr.VersionInfo(19, 1, 0, \"final\")  <= (19, 2)\n    True\n    >>> attr.VersionInfo(19, 1, 0, \"final\") < (19, 1, 1)\n    True\n    >>> vi = attr.VersionInfo(19, 2, 0, \"final\")\n    >>> vi < (19, 1, 1)\n    False\n    >>> vi < (19,)\n    False\n    >>> vi == (19, 2,)\n    True\n    >>> vi == (19, 2, 1)\n    False\n\n    .. versionadded:: 19.2\n    \"\"\"\n\n    year = attrib(type=int)\n    minor = attrib(type=int)\n    micro = attrib(type=int)\n    releaselevel = attrib(type=str)\n\n    @classmethod\n    def _from_version_string(cls, s):\n        \"\"\"\n        Parse *s* and return a _VersionInfo.\n        \"\"\"\n        v = s.split(\".\")\n        if len(v) == 3:\n            v.append(\"final\")\n\n        return cls(\n            year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3]\n        )\n\n    def _ensure_tuple(self, other):\n        \"\"\"\n        Ensure *other* is a tuple of a valid length.\n\n        Returns a possibly transformed *other* and ourselves as a tuple of\n        the same length as *other*.\n        \"\"\"\n\n        if self.__class__ is other.__class__:\n            other = astuple(other)\n\n        if not isinstance(other, tuple):\n            raise NotImplementedError\n\n        if not (1 <= len(other) <= 4):\n            raise NotImplementedError\n\n        return astuple(self)[: len(other)], other\n\n    def __eq__(self, other):\n        try:\n            us, them = self._ensure_tuple(other)\n        except NotImplementedError:\n            return NotImplemented\n\n        return us == them\n\n    def __lt__(self, other):\n        try:\n            us, them = self._ensure_tuple(other)\n        except NotImplementedError:\n            return NotImplemented\n\n        # Since alphabetically \"dev0\" < \"final\" < \"post1\" < \"post2\", we don't\n        # have to do anything special with releaselevel for now.\n        return us < them\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/_version_info.pyi",
    "content": "class VersionInfo:\n    @property\n    def year(self) -> int: ...\n    @property\n    def minor(self) -> int: ...\n    @property\n    def micro(self) -> int: ...\n    @property\n    def releaselevel(self) -> str: ...\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/converters.py",
    "content": "# SPDX-License-Identifier: MIT\n\n\"\"\"\nCommonly useful converters.\n\"\"\"\n\nfrom __future__ import absolute_import, division, print_function\n\nfrom ._compat import PY2\nfrom ._make import NOTHING, Factory, pipe\n\n\nif not PY2:\n    import inspect\n    import typing\n\n\n__all__ = [\n    \"default_if_none\",\n    \"optional\",\n    \"pipe\",\n    \"to_bool\",\n]\n\n\ndef optional(converter):\n    \"\"\"\n    A converter that allows an attribute to be optional. An optional attribute\n    is one which can be set to ``None``.\n\n    Type annotations will be inferred from the wrapped converter's, if it\n    has any.\n\n    :param callable converter: the converter that is used for non-``None``\n        values.\n\n    .. versionadded:: 17.1.0\n    \"\"\"\n\n    def optional_converter(val):\n        if val is None:\n            return None\n        return converter(val)\n\n    if not PY2:\n        sig = None\n        try:\n            sig = inspect.signature(converter)\n        except (ValueError, TypeError):  # inspect failed\n            pass\n        if sig:\n            params = list(sig.parameters.values())\n            if params and params[0].annotation is not inspect.Parameter.empty:\n                optional_converter.__annotations__[\"val\"] = typing.Optional[\n                    params[0].annotation\n                ]\n            if sig.return_annotation is not inspect.Signature.empty:\n                optional_converter.__annotations__[\"return\"] = typing.Optional[\n                    sig.return_annotation\n                ]\n\n    return optional_converter\n\n\ndef default_if_none(default=NOTHING, factory=None):\n    \"\"\"\n    A converter that allows to replace ``None`` values by *default* or the\n    result of *factory*.\n\n    :param default: Value to be used if ``None`` is passed. Passing an instance\n       of `attrs.Factory` is supported, however the ``takes_self`` option\n       is *not*.\n    :param callable factory: A callable that takes no parameters whose result\n       is used if ``None`` is passed.\n\n    :raises TypeError: If **neither** *default* or *factory* is passed.\n    :raises TypeError: If **both** *default* and *factory* are passed.\n    :raises ValueError: If an instance of `attrs.Factory` is passed with\n       ``takes_self=True``.\n\n    .. versionadded:: 18.2.0\n    \"\"\"\n    if default is NOTHING and factory is None:\n        raise TypeError(\"Must pass either `default` or `factory`.\")\n\n    if default is not NOTHING and factory is not None:\n        raise TypeError(\n            \"Must pass either `default` or `factory` but not both.\"\n        )\n\n    if factory is not None:\n        default = Factory(factory)\n\n    if isinstance(default, Factory):\n        if default.takes_self:\n            raise ValueError(\n                \"`takes_self` is not supported by default_if_none.\"\n            )\n\n        def default_if_none_converter(val):\n            if val is not None:\n                return val\n\n            return default.factory()\n\n    else:\n\n        def default_if_none_converter(val):\n            if val is not None:\n                return val\n\n            return default\n\n    return default_if_none_converter\n\n\ndef to_bool(val):\n    \"\"\"\n    Convert \"boolean\" strings (e.g., from env. vars.) to real booleans.\n\n    Values mapping to :code:`True`:\n\n    - :code:`True`\n    - :code:`\"true\"` / :code:`\"t\"`\n    - :code:`\"yes\"` / :code:`\"y\"`\n    - :code:`\"on\"`\n    - :code:`\"1\"`\n    - :code:`1`\n\n    Values mapping to :code:`False`:\n\n    - :code:`False`\n    - :code:`\"false\"` / :code:`\"f\"`\n    - :code:`\"no\"` / :code:`\"n\"`\n    - :code:`\"off\"`\n    - :code:`\"0\"`\n    - :code:`0`\n\n    :raises ValueError: for any other value.\n\n    .. versionadded:: 21.3.0\n    \"\"\"\n    if isinstance(val, str):\n        val = val.lower()\n    truthy = {True, \"true\", \"t\", \"yes\", \"y\", \"on\", \"1\", 1}\n    falsy = {False, \"false\", \"f\", \"no\", \"n\", \"off\", \"0\", 0}\n    try:\n        if val in truthy:\n            return True\n        if val in falsy:\n            return False\n    except TypeError:\n        # Raised when \"val\" is not hashable (e.g., lists)\n        pass\n    raise ValueError(\"Cannot convert value to bool: {}\".format(val))\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/converters.pyi",
    "content": "from typing import Callable, Optional, TypeVar, overload\n\nfrom . import _ConverterType\n\n_T = TypeVar(\"_T\")\n\ndef pipe(*validators: _ConverterType) -> _ConverterType: ...\ndef optional(converter: _ConverterType) -> _ConverterType: ...\n@overload\ndef default_if_none(default: _T) -> _ConverterType: ...\n@overload\ndef default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ...\ndef to_bool(val: str) -> bool: ...\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/exceptions.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom __future__ import absolute_import, division, print_function\n\n\nclass FrozenError(AttributeError):\n    \"\"\"\n    A frozen/immutable instance or attribute have been attempted to be\n    modified.\n\n    It mirrors the behavior of ``namedtuples`` by using the same error message\n    and subclassing `AttributeError`.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n\n    msg = \"can't set attribute\"\n    args = [msg]\n\n\nclass FrozenInstanceError(FrozenError):\n    \"\"\"\n    A frozen instance has been attempted to be modified.\n\n    .. versionadded:: 16.1.0\n    \"\"\"\n\n\nclass FrozenAttributeError(FrozenError):\n    \"\"\"\n    A frozen attribute has been attempted to be modified.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n\n\nclass AttrsAttributeNotFoundError(ValueError):\n    \"\"\"\n    An ``attrs`` function couldn't find an attribute that the user asked for.\n\n    .. versionadded:: 16.2.0\n    \"\"\"\n\n\nclass NotAnAttrsClassError(ValueError):\n    \"\"\"\n    A non-``attrs`` class has been passed into an ``attrs`` function.\n\n    .. versionadded:: 16.2.0\n    \"\"\"\n\n\nclass DefaultAlreadySetError(RuntimeError):\n    \"\"\"\n    A default has been set using ``attr.ib()`` and is attempted to be reset\n    using the decorator.\n\n    .. versionadded:: 17.1.0\n    \"\"\"\n\n\nclass UnannotatedAttributeError(RuntimeError):\n    \"\"\"\n    A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type\n    annotation.\n\n    .. versionadded:: 17.3.0\n    \"\"\"\n\n\nclass PythonTooOldError(RuntimeError):\n    \"\"\"\n    It was attempted to use an ``attrs`` feature that requires a newer Python\n    version.\n\n    .. versionadded:: 18.2.0\n    \"\"\"\n\n\nclass NotCallableError(TypeError):\n    \"\"\"\n    A ``attr.ib()`` requiring a callable has been set with a value\n    that is not callable.\n\n    .. versionadded:: 19.2.0\n    \"\"\"\n\n    def __init__(self, msg, value):\n        super(TypeError, self).__init__(msg, value)\n        self.msg = msg\n        self.value = value\n\n    def __str__(self):\n        return str(self.msg)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/exceptions.pyi",
    "content": "from typing import Any\n\nclass FrozenError(AttributeError):\n    msg: str = ...\n\nclass FrozenInstanceError(FrozenError): ...\nclass FrozenAttributeError(FrozenError): ...\nclass AttrsAttributeNotFoundError(ValueError): ...\nclass NotAnAttrsClassError(ValueError): ...\nclass DefaultAlreadySetError(RuntimeError): ...\nclass UnannotatedAttributeError(RuntimeError): ...\nclass PythonTooOldError(RuntimeError): ...\n\nclass NotCallableError(TypeError):\n    msg: str = ...\n    value: Any = ...\n    def __init__(self, msg: str, value: Any) -> None: ...\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/filters.py",
    "content": "# SPDX-License-Identifier: MIT\n\n\"\"\"\nCommonly useful filters for `attr.asdict`.\n\"\"\"\n\nfrom __future__ import absolute_import, division, print_function\n\nfrom ._compat import isclass\nfrom ._make import Attribute\n\n\ndef _split_what(what):\n    \"\"\"\n    Returns a tuple of `frozenset`s of classes and attributes.\n    \"\"\"\n    return (\n        frozenset(cls for cls in what if isclass(cls)),\n        frozenset(cls for cls in what if isinstance(cls, Attribute)),\n    )\n\n\ndef include(*what):\n    \"\"\"\n    Include *what*.\n\n    :param what: What to include.\n    :type what: `list` of `type` or `attrs.Attribute`\\\\ s\n\n    :rtype: `callable`\n    \"\"\"\n    cls, attrs = _split_what(what)\n\n    def include_(attribute, value):\n        return value.__class__ in cls or attribute in attrs\n\n    return include_\n\n\ndef exclude(*what):\n    \"\"\"\n    Exclude *what*.\n\n    :param what: What to exclude.\n    :type what: `list` of classes or `attrs.Attribute`\\\\ s.\n\n    :rtype: `callable`\n    \"\"\"\n    cls, attrs = _split_what(what)\n\n    def exclude_(attribute, value):\n        return value.__class__ not in cls and attribute not in attrs\n\n    return exclude_\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/filters.pyi",
    "content": "from typing import Any, Union\n\nfrom . import Attribute, _FilterType\n\ndef include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ...\ndef exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ...\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/py.typed",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/attr/setters.py",
    "content": "# SPDX-License-Identifier: MIT\n\n\"\"\"\nCommonly used hooks for on_setattr.\n\"\"\"\n\nfrom __future__ import absolute_import, division, print_function\n\nfrom . import _config\nfrom .exceptions import FrozenAttributeError\n\n\ndef pipe(*setters):\n    \"\"\"\n    Run all *setters* and return the return value of the last one.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n\n    def wrapped_pipe(instance, attrib, new_value):\n        rv = new_value\n\n        for setter in setters:\n            rv = setter(instance, attrib, rv)\n\n        return rv\n\n    return wrapped_pipe\n\n\ndef frozen(_, __, ___):\n    \"\"\"\n    Prevent an attribute to be modified.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n    raise FrozenAttributeError()\n\n\ndef validate(instance, attrib, new_value):\n    \"\"\"\n    Run *attrib*'s validator on *new_value* if it has one.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n    if _config._run_validators is False:\n        return new_value\n\n    v = attrib.validator\n    if not v:\n        return new_value\n\n    v(instance, attrib, new_value)\n\n    return new_value\n\n\ndef convert(instance, attrib, new_value):\n    \"\"\"\n    Run *attrib*'s converter -- if it has one --  on *new_value* and return the\n    result.\n\n    .. versionadded:: 20.1.0\n    \"\"\"\n    c = attrib.converter\n    if c:\n        return c(new_value)\n\n    return new_value\n\n\nNO_OP = object()\n\"\"\"\nSentinel for disabling class-wide *on_setattr* hooks for certain attributes.\n\nDoes not work in `pipe` or within lists.\n\n.. versionadded:: 20.1.0\n\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/setters.pyi",
    "content": "from typing import Any, NewType, NoReturn, TypeVar, cast\n\nfrom . import Attribute, _OnSetAttrType\n\n_T = TypeVar(\"_T\")\n\ndef frozen(\n    instance: Any, attribute: Attribute[Any], new_value: Any\n) -> NoReturn: ...\ndef pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ...\ndef validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ...\n\n# convert is allowed to return Any, because they can be chained using pipe.\ndef convert(\n    instance: Any, attribute: Attribute[Any], new_value: Any\n) -> Any: ...\n\n_NoOpType = NewType(\"_NoOpType\", object)\nNO_OP: _NoOpType\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/validators.py",
    "content": "# SPDX-License-Identifier: MIT\n\n\"\"\"\nCommonly useful validators.\n\"\"\"\n\nfrom __future__ import absolute_import, division, print_function\n\nimport operator\nimport re\n\nfrom contextlib import contextmanager\n\nfrom ._config import get_run_validators, set_run_validators\nfrom ._make import _AndValidator, and_, attrib, attrs\nfrom .exceptions import NotCallableError\n\n\ntry:\n    Pattern = re.Pattern\nexcept AttributeError:  # Python <3.7 lacks a Pattern type.\n    Pattern = type(re.compile(\"\"))\n\n\n__all__ = [\n    \"and_\",\n    \"deep_iterable\",\n    \"deep_mapping\",\n    \"disabled\",\n    \"ge\",\n    \"get_disabled\",\n    \"gt\",\n    \"in_\",\n    \"instance_of\",\n    \"is_callable\",\n    \"le\",\n    \"lt\",\n    \"matches_re\",\n    \"max_len\",\n    \"optional\",\n    \"provides\",\n    \"set_disabled\",\n]\n\n\ndef set_disabled(disabled):\n    \"\"\"\n    Globally disable or enable running validators.\n\n    By default, they are run.\n\n    :param disabled: If ``True``, disable running all validators.\n    :type disabled: bool\n\n    .. warning::\n\n        This function is not thread-safe!\n\n    .. versionadded:: 21.3.0\n    \"\"\"\n    set_run_validators(not disabled)\n\n\ndef get_disabled():\n    \"\"\"\n    Return a bool indicating whether validators are currently disabled or not.\n\n    :return: ``True`` if validators are currently disabled.\n    :rtype: bool\n\n    .. versionadded:: 21.3.0\n    \"\"\"\n    return not get_run_validators()\n\n\n@contextmanager\ndef disabled():\n    \"\"\"\n    Context manager that disables running validators within its context.\n\n    .. warning::\n\n        This context manager is not thread-safe!\n\n    .. versionadded:: 21.3.0\n    \"\"\"\n    set_run_validators(False)\n    try:\n        yield\n    finally:\n        set_run_validators(True)\n\n\n@attrs(repr=False, slots=True, hash=True)\nclass _InstanceOfValidator(object):\n    type = attrib()\n\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if not isinstance(value, self.type):\n            raise TypeError(\n                \"'{name}' must be {type!r} (got {value!r} that is a \"\n                \"{actual!r}).\".format(\n                    name=attr.name,\n                    type=self.type,\n                    actual=value.__class__,\n                    value=value,\n                ),\n                attr,\n                self.type,\n                value,\n            )\n\n    def __repr__(self):\n        return \"<instance_of validator for type {type!r}>\".format(\n            type=self.type\n        )\n\n\ndef instance_of(type):\n    \"\"\"\n    A validator that raises a `TypeError` if the initializer is called\n    with a wrong type for this particular attribute (checks are performed using\n    `isinstance` therefore it's also valid to pass a tuple of types).\n\n    :param type: The type to check for.\n    :type type: type or tuple of types\n\n    :raises TypeError: With a human readable error message, the attribute\n        (of type `attrs.Attribute`), the expected type, and the value it\n        got.\n    \"\"\"\n    return _InstanceOfValidator(type)\n\n\n@attrs(repr=False, frozen=True, slots=True)\nclass _MatchesReValidator(object):\n    pattern = attrib()\n    match_func = attrib()\n\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if not self.match_func(value):\n            raise ValueError(\n                \"'{name}' must match regex {pattern!r}\"\n                \" ({value!r} doesn't)\".format(\n                    name=attr.name, pattern=self.pattern.pattern, value=value\n                ),\n                attr,\n                self.pattern,\n                value,\n            )\n\n    def __repr__(self):\n        return \"<matches_re validator for pattern {pattern!r}>\".format(\n            pattern=self.pattern\n        )\n\n\ndef matches_re(regex, flags=0, func=None):\n    r\"\"\"\n    A validator that raises `ValueError` if the initializer is called\n    with a string that doesn't match *regex*.\n\n    :param regex: a regex string or precompiled pattern to match against\n    :param int flags: flags that will be passed to the underlying re function\n        (default 0)\n    :param callable func: which underlying `re` function to call (options\n        are `re.fullmatch`, `re.search`, `re.match`, default\n        is ``None`` which means either `re.fullmatch` or an emulation of\n        it on Python 2). For performance reasons, they won't be used directly\n        but on a pre-`re.compile`\\ ed pattern.\n\n    .. versionadded:: 19.2.0\n    .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern.\n    \"\"\"\n    fullmatch = getattr(re, \"fullmatch\", None)\n    valid_funcs = (fullmatch, None, re.search, re.match)\n    if func not in valid_funcs:\n        raise ValueError(\n            \"'func' must be one of {}.\".format(\n                \", \".join(\n                    sorted(\n                        e and e.__name__ or \"None\" for e in set(valid_funcs)\n                    )\n                )\n            )\n        )\n\n    if isinstance(regex, Pattern):\n        if flags:\n            raise TypeError(\n                \"'flags' can only be used with a string pattern; \"\n                \"pass flags to re.compile() instead\"\n            )\n        pattern = regex\n    else:\n        pattern = re.compile(regex, flags)\n\n    if func is re.match:\n        match_func = pattern.match\n    elif func is re.search:\n        match_func = pattern.search\n    elif fullmatch:\n        match_func = pattern.fullmatch\n    else:  # Python 2 fullmatch emulation (https://bugs.python.org/issue16203)\n        pattern = re.compile(\n            r\"(?:{})\\Z\".format(pattern.pattern), pattern.flags\n        )\n        match_func = pattern.match\n\n    return _MatchesReValidator(pattern, match_func)\n\n\n@attrs(repr=False, slots=True, hash=True)\nclass _ProvidesValidator(object):\n    interface = attrib()\n\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if not self.interface.providedBy(value):\n            raise TypeError(\n                \"'{name}' must provide {interface!r} which {value!r} \"\n                \"doesn't.\".format(\n                    name=attr.name, interface=self.interface, value=value\n                ),\n                attr,\n                self.interface,\n                value,\n            )\n\n    def __repr__(self):\n        return \"<provides validator for interface {interface!r}>\".format(\n            interface=self.interface\n        )\n\n\ndef provides(interface):\n    \"\"\"\n    A validator that raises a `TypeError` if the initializer is called\n    with an object that does not provide the requested *interface* (checks are\n    performed using ``interface.providedBy(value)`` (see `zope.interface\n    <https://zopeinterface.readthedocs.io/en/latest/>`_).\n\n    :param interface: The interface to check for.\n    :type interface: ``zope.interface.Interface``\n\n    :raises TypeError: With a human readable error message, the attribute\n        (of type `attrs.Attribute`), the expected interface, and the\n        value it got.\n    \"\"\"\n    return _ProvidesValidator(interface)\n\n\n@attrs(repr=False, slots=True, hash=True)\nclass _OptionalValidator(object):\n    validator = attrib()\n\n    def __call__(self, inst, attr, value):\n        if value is None:\n            return\n\n        self.validator(inst, attr, value)\n\n    def __repr__(self):\n        return \"<optional validator for {what} or None>\".format(\n            what=repr(self.validator)\n        )\n\n\ndef optional(validator):\n    \"\"\"\n    A validator that makes an attribute optional.  An optional attribute is one\n    which can be set to ``None`` in addition to satisfying the requirements of\n    the sub-validator.\n\n    :param validator: A validator (or a list of validators) that is used for\n        non-``None`` values.\n    :type validator: callable or `list` of callables.\n\n    .. versionadded:: 15.1.0\n    .. versionchanged:: 17.1.0 *validator* can be a list of validators.\n    \"\"\"\n    if isinstance(validator, list):\n        return _OptionalValidator(_AndValidator(validator))\n    return _OptionalValidator(validator)\n\n\n@attrs(repr=False, slots=True, hash=True)\nclass _InValidator(object):\n    options = attrib()\n\n    def __call__(self, inst, attr, value):\n        try:\n            in_options = value in self.options\n        except TypeError:  # e.g. `1 in \"abc\"`\n            in_options = False\n\n        if not in_options:\n            raise ValueError(\n                \"'{name}' must be in {options!r} (got {value!r})\".format(\n                    name=attr.name, options=self.options, value=value\n                )\n            )\n\n    def __repr__(self):\n        return \"<in_ validator with options {options!r}>\".format(\n            options=self.options\n        )\n\n\ndef in_(options):\n    \"\"\"\n    A validator that raises a `ValueError` if the initializer is called\n    with a value that does not belong in the options provided.  The check is\n    performed using ``value in options``.\n\n    :param options: Allowed options.\n    :type options: list, tuple, `enum.Enum`, ...\n\n    :raises ValueError: With a human readable error message, the attribute (of\n       type `attrs.Attribute`), the expected options, and the value it\n       got.\n\n    .. versionadded:: 17.1.0\n    \"\"\"\n    return _InValidator(options)\n\n\n@attrs(repr=False, slots=False, hash=True)\nclass _IsCallableValidator(object):\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if not callable(value):\n            message = (\n                \"'{name}' must be callable \"\n                \"(got {value!r} that is a {actual!r}).\"\n            )\n            raise NotCallableError(\n                msg=message.format(\n                    name=attr.name, value=value, actual=value.__class__\n                ),\n                value=value,\n            )\n\n    def __repr__(self):\n        return \"<is_callable validator>\"\n\n\ndef is_callable():\n    \"\"\"\n    A validator that raises a `attr.exceptions.NotCallableError` if the\n    initializer is called with a value for this particular attribute\n    that is not callable.\n\n    .. versionadded:: 19.1.0\n\n    :raises `attr.exceptions.NotCallableError`: With a human readable error\n        message containing the attribute (`attrs.Attribute`) name,\n        and the value it got.\n    \"\"\"\n    return _IsCallableValidator()\n\n\n@attrs(repr=False, slots=True, hash=True)\nclass _DeepIterable(object):\n    member_validator = attrib(validator=is_callable())\n    iterable_validator = attrib(\n        default=None, validator=optional(is_callable())\n    )\n\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if self.iterable_validator is not None:\n            self.iterable_validator(inst, attr, value)\n\n        for member in value:\n            self.member_validator(inst, attr, member)\n\n    def __repr__(self):\n        iterable_identifier = (\n            \"\"\n            if self.iterable_validator is None\n            else \" {iterable!r}\".format(iterable=self.iterable_validator)\n        )\n        return (\n            \"<deep_iterable validator for{iterable_identifier}\"\n            \" iterables of {member!r}>\"\n        ).format(\n            iterable_identifier=iterable_identifier,\n            member=self.member_validator,\n        )\n\n\ndef deep_iterable(member_validator, iterable_validator=None):\n    \"\"\"\n    A validator that performs deep validation of an iterable.\n\n    :param member_validator: Validator to apply to iterable members\n    :param iterable_validator: Validator to apply to iterable itself\n        (optional)\n\n    .. versionadded:: 19.1.0\n\n    :raises TypeError: if any sub-validators fail\n    \"\"\"\n    return _DeepIterable(member_validator, iterable_validator)\n\n\n@attrs(repr=False, slots=True, hash=True)\nclass _DeepMapping(object):\n    key_validator = attrib(validator=is_callable())\n    value_validator = attrib(validator=is_callable())\n    mapping_validator = attrib(default=None, validator=optional(is_callable()))\n\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if self.mapping_validator is not None:\n            self.mapping_validator(inst, attr, value)\n\n        for key in value:\n            self.key_validator(inst, attr, key)\n            self.value_validator(inst, attr, value[key])\n\n    def __repr__(self):\n        return (\n            \"<deep_mapping validator for objects mapping {key!r} to {value!r}>\"\n        ).format(key=self.key_validator, value=self.value_validator)\n\n\ndef deep_mapping(key_validator, value_validator, mapping_validator=None):\n    \"\"\"\n    A validator that performs deep validation of a dictionary.\n\n    :param key_validator: Validator to apply to dictionary keys\n    :param value_validator: Validator to apply to dictionary values\n    :param mapping_validator: Validator to apply to top-level mapping\n        attribute (optional)\n\n    .. versionadded:: 19.1.0\n\n    :raises TypeError: if any sub-validators fail\n    \"\"\"\n    return _DeepMapping(key_validator, value_validator, mapping_validator)\n\n\n@attrs(repr=False, frozen=True, slots=True)\nclass _NumberValidator(object):\n    bound = attrib()\n    compare_op = attrib()\n    compare_func = attrib()\n\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if not self.compare_func(value, self.bound):\n            raise ValueError(\n                \"'{name}' must be {op} {bound}: {value}\".format(\n                    name=attr.name,\n                    op=self.compare_op,\n                    bound=self.bound,\n                    value=value,\n                )\n            )\n\n    def __repr__(self):\n        return \"<Validator for x {op} {bound}>\".format(\n            op=self.compare_op, bound=self.bound\n        )\n\n\ndef lt(val):\n    \"\"\"\n    A validator that raises `ValueError` if the initializer is called\n    with a number larger or equal to *val*.\n\n    :param val: Exclusive upper bound for values\n\n    .. versionadded:: 21.3.0\n    \"\"\"\n    return _NumberValidator(val, \"<\", operator.lt)\n\n\ndef le(val):\n    \"\"\"\n    A validator that raises `ValueError` if the initializer is called\n    with a number greater than *val*.\n\n    :param val: Inclusive upper bound for values\n\n    .. versionadded:: 21.3.0\n    \"\"\"\n    return _NumberValidator(val, \"<=\", operator.le)\n\n\ndef ge(val):\n    \"\"\"\n    A validator that raises `ValueError` if the initializer is called\n    with a number smaller than *val*.\n\n    :param val: Inclusive lower bound for values\n\n    .. versionadded:: 21.3.0\n    \"\"\"\n    return _NumberValidator(val, \">=\", operator.ge)\n\n\ndef gt(val):\n    \"\"\"\n    A validator that raises `ValueError` if the initializer is called\n    with a number smaller or equal to *val*.\n\n    :param val: Exclusive lower bound for values\n\n    .. versionadded:: 21.3.0\n    \"\"\"\n    return _NumberValidator(val, \">\", operator.gt)\n\n\n@attrs(repr=False, frozen=True, slots=True)\nclass _MaxLengthValidator(object):\n    max_length = attrib()\n\n    def __call__(self, inst, attr, value):\n        \"\"\"\n        We use a callable class to be able to change the ``__repr__``.\n        \"\"\"\n        if len(value) > self.max_length:\n            raise ValueError(\n                \"Length of '{name}' must be <= {max}: {len}\".format(\n                    name=attr.name, max=self.max_length, len=len(value)\n                )\n            )\n\n    def __repr__(self):\n        return \"<max_len validator for {max}>\".format(max=self.max_length)\n\n\ndef max_len(length):\n    \"\"\"\n    A validator that raises `ValueError` if the initializer is called\n    with a string or iterable that is longer than *length*.\n\n    :param int length: Maximum length of the string or iterable\n\n    .. versionadded:: 21.3.0\n    \"\"\"\n    return _MaxLengthValidator(length)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attr/validators.pyi",
    "content": "from typing import (\n    Any,\n    AnyStr,\n    Callable,\n    Container,\n    ContextManager,\n    Iterable,\n    List,\n    Mapping,\n    Match,\n    Optional,\n    Pattern,\n    Tuple,\n    Type,\n    TypeVar,\n    Union,\n    overload,\n)\n\nfrom . import _ValidatorType\n\n_T = TypeVar(\"_T\")\n_T1 = TypeVar(\"_T1\")\n_T2 = TypeVar(\"_T2\")\n_T3 = TypeVar(\"_T3\")\n_I = TypeVar(\"_I\", bound=Iterable)\n_K = TypeVar(\"_K\")\n_V = TypeVar(\"_V\")\n_M = TypeVar(\"_M\", bound=Mapping)\n\ndef set_disabled(run: bool) -> None: ...\ndef get_disabled() -> bool: ...\ndef disabled() -> ContextManager[None]: ...\n\n# To be more precise on instance_of use some overloads.\n# If there are more than 3 items in the tuple then we fall back to Any\n@overload\ndef instance_of(type: Type[_T]) -> _ValidatorType[_T]: ...\n@overload\ndef instance_of(type: Tuple[Type[_T]]) -> _ValidatorType[_T]: ...\n@overload\ndef instance_of(\n    type: Tuple[Type[_T1], Type[_T2]]\n) -> _ValidatorType[Union[_T1, _T2]]: ...\n@overload\ndef instance_of(\n    type: Tuple[Type[_T1], Type[_T2], Type[_T3]]\n) -> _ValidatorType[Union[_T1, _T2, _T3]]: ...\n@overload\ndef instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ...\ndef provides(interface: Any) -> _ValidatorType[Any]: ...\ndef optional(\n    validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]]\n) -> _ValidatorType[Optional[_T]]: ...\ndef in_(options: Container[_T]) -> _ValidatorType[_T]: ...\ndef and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...\ndef matches_re(\n    regex: Union[Pattern[AnyStr], AnyStr],\n    flags: int = ...,\n    func: Optional[\n        Callable[[AnyStr, AnyStr, int], Optional[Match[AnyStr]]]\n    ] = ...,\n) -> _ValidatorType[AnyStr]: ...\ndef deep_iterable(\n    member_validator: _ValidatorType[_T],\n    iterable_validator: Optional[_ValidatorType[_I]] = ...,\n) -> _ValidatorType[_I]: ...\ndef deep_mapping(\n    key_validator: _ValidatorType[_K],\n    value_validator: _ValidatorType[_V],\n    mapping_validator: Optional[_ValidatorType[_M]] = ...,\n) -> _ValidatorType[_M]: ...\ndef is_callable() -> _ValidatorType[_T]: ...\ndef lt(val: _T) -> _ValidatorType[_T]: ...\ndef le(val: _T) -> _ValidatorType[_T]: ...\ndef ge(val: _T) -> _ValidatorType[_T]: ...\ndef gt(val: _T) -> _ValidatorType[_T]: ...\ndef max_len(length: int) -> _ValidatorType[_T]: ...\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attrs/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom attr import (\n    NOTHING,\n    Attribute,\n    Factory,\n    __author__,\n    __copyright__,\n    __description__,\n    __doc__,\n    __email__,\n    __license__,\n    __title__,\n    __url__,\n    __version__,\n    __version_info__,\n    assoc,\n    cmp_using,\n    define,\n    evolve,\n    field,\n    fields,\n    fields_dict,\n    frozen,\n    has,\n    make_class,\n    mutable,\n    resolve_types,\n    validate,\n)\nfrom attr._next_gen import asdict, astuple\n\nfrom . import converters, exceptions, filters, setters, validators\n\n\n__all__ = [\n    \"__author__\",\n    \"__copyright__\",\n    \"__description__\",\n    \"__doc__\",\n    \"__email__\",\n    \"__license__\",\n    \"__title__\",\n    \"__url__\",\n    \"__version__\",\n    \"__version_info__\",\n    \"asdict\",\n    \"assoc\",\n    \"astuple\",\n    \"Attribute\",\n    \"cmp_using\",\n    \"converters\",\n    \"define\",\n    \"evolve\",\n    \"exceptions\",\n    \"Factory\",\n    \"field\",\n    \"fields_dict\",\n    \"fields\",\n    \"filters\",\n    \"frozen\",\n    \"has\",\n    \"make_class\",\n    \"mutable\",\n    \"NOTHING\",\n    \"resolve_types\",\n    \"setters\",\n    \"validate\",\n    \"validators\",\n]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attrs/__init__.pyi",
    "content": "from typing import (\n    Any,\n    Callable,\n    Dict,\n    Mapping,\n    Optional,\n    Sequence,\n    Tuple,\n    Type,\n)\n\n# Because we need to type our own stuff, we have to make everything from\n# attr explicitly public too.\nfrom attr import __author__ as __author__\nfrom attr import __copyright__ as __copyright__\nfrom attr import __description__ as __description__\nfrom attr import __email__ as __email__\nfrom attr import __license__ as __license__\nfrom attr import __title__ as __title__\nfrom attr import __url__ as __url__\nfrom attr import __version__ as __version__\nfrom attr import __version_info__ as __version_info__\nfrom attr import _FilterType\nfrom attr import assoc as assoc\nfrom attr import Attribute as Attribute\nfrom attr import define as define\nfrom attr import evolve as evolve\nfrom attr import Factory as Factory\nfrom attr import exceptions as exceptions\nfrom attr import field as field\nfrom attr import fields as fields\nfrom attr import fields_dict as fields_dict\nfrom attr import frozen as frozen\nfrom attr import has as has\nfrom attr import make_class as make_class\nfrom attr import mutable as mutable\nfrom attr import NOTHING as NOTHING\nfrom attr import resolve_types as resolve_types\nfrom attr import setters as setters\nfrom attr import validate as validate\nfrom attr import validators as validators\n\n# TODO: see definition of attr.asdict/astuple\ndef asdict(\n    inst: Any,\n    recurse: bool = ...,\n    filter: Optional[_FilterType[Any]] = ...,\n    dict_factory: Type[Mapping[Any, Any]] = ...,\n    retain_collection_types: bool = ...,\n    value_serializer: Optional[\n        Callable[[type, Attribute[Any], Any], Any]\n    ] = ...,\n    tuple_keys: bool = ...,\n) -> Dict[str, Any]: ...\n\n# TODO: add support for returning NamedTuple from the mypy plugin\ndef astuple(\n    inst: Any,\n    recurse: bool = ...,\n    filter: Optional[_FilterType[Any]] = ...,\n    tuple_factory: Type[Sequence[Any]] = ...,\n    retain_collection_types: bool = ...,\n) -> Tuple[Any, ...]: ...\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attrs/converters.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom attr.converters import *  # noqa\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attrs/exceptions.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom attr.exceptions import *  # noqa\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attrs/filters.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom attr.filters import *  # noqa\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attrs/py.typed",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/attrs/setters.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom attr.setters import *  # noqa\n"
  },
  {
    "path": "openpype/vendor/python/python_2/attrs/validators.py",
    "content": "# SPDX-License-Identifier: MIT\n\nfrom attr.validators import *  # noqa\n"
  },
  {
    "path": "openpype/vendor/python/python_2/backports/__init__.py",
    "content": "__path__ = __import__('pkgutil').extend_path(__path__, __name__)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/backports/configparser/__init__.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n\"\"\"Configuration file parser.\n\nA configuration file consists of sections, lead by a \"[section]\" header,\nand followed by \"name: value\" entries, with continuations and such in\nthe style of RFC 822.\n\nIntrinsic defaults can be specified by passing them into the\nConfigParser constructor as a dictionary.\n\nclass:\n\nConfigParser -- responsible for parsing a list of\n                    configuration files, and managing the parsed database.\n\n    methods:\n\n    __init__(defaults=None, dict_type=_default_dict, allow_no_value=False,\n             delimiters=('=', ':'), comment_prefixes=('#', ';'),\n             inline_comment_prefixes=None, strict=True,\n             empty_lines_in_values=True, default_section='DEFAULT',\n             interpolation=<unset>, converters=<unset>):\n        Create the parser. When `defaults' is given, it is initialized into the\n        dictionary or intrinsic defaults. The keys must be strings, the values\n        must be appropriate for %()s string interpolation.\n\n        When `dict_type' is given, it will be used to create the dictionary\n        objects for the list of sections, for the options within a section, and\n        for the default values.\n\n        When `delimiters' is given, it will be used as the set of substrings\n        that divide keys from values.\n\n        When `comment_prefixes' is given, it will be used as the set of\n        substrings that prefix comments in empty lines. Comments can be\n        indented.\n\n        When `inline_comment_prefixes' is given, it will be used as the set of\n        substrings that prefix comments in non-empty lines.\n\n        When `strict` is True, the parser won't allow for any section or option\n        duplicates while reading from a single source (file, string or\n        dictionary). Default is True.\n\n        When `empty_lines_in_values' is False (default: True), each empty line\n        marks the end of an option. Otherwise, internal empty lines of\n        a multiline option are kept as part of the value.\n\n        When `allow_no_value' is True (default: False), options without\n        values are accepted; the value presented for these is None.\n\n    sections()\n        Return all the configuration section names, sans DEFAULT.\n\n    has_section(section)\n        Return whether the given section exists.\n\n    has_option(section, option)\n        Return whether the given option exists in the given section.\n\n    options(section)\n        Return list of configuration options for the named section.\n\n    read(filenames, encoding=None)\n        Read and parse the list of named configuration files, given by\n        name.  A single filename is also allowed.  Non-existing files\n        are ignored.  Return list of successfully read files.\n\n    read_file(f, filename=None)\n        Read and parse one configuration file, given as a file object.\n        The filename defaults to f.name; it is only used in error\n        messages (if f has no `name' attribute, the string `<???>' is used).\n\n    read_string(string)\n        Read configuration from a given string.\n\n    read_dict(dictionary)\n        Read configuration from a dictionary. Keys are section names,\n        values are dictionaries with keys and values that should be present\n        in the section. If the used dictionary type preserves order, sections\n        and their keys will be added in order. Values are automatically\n        converted to strings.\n\n    get(section, option, raw=False, vars=None, fallback=_UNSET)\n        Return a string value for the named option.  All % interpolations are\n        expanded in the return values, based on the defaults passed into the\n        constructor and the DEFAULT section.  Additional substitutions may be\n        provided using the `vars' argument, which must be a dictionary whose\n        contents override any pre-existing defaults. If `option' is a key in\n        `vars', the value from `vars' is used.\n\n    getint(section, options, raw=False, vars=None, fallback=_UNSET)\n        Like get(), but convert value to an integer.\n\n    getfloat(section, options, raw=False, vars=None, fallback=_UNSET)\n        Like get(), but convert value to a float.\n\n    getboolean(section, options, raw=False, vars=None, fallback=_UNSET)\n        Like get(), but convert value to a boolean (currently case\n        insensitively defined as 0, false, no, off for False, and 1, true,\n        yes, on for True).  Returns False or True.\n\n    items(section=_UNSET, raw=False, vars=None)\n        If section is given, return a list of tuples with (name, value) for\n        each option in the section. Otherwise, return a list of tuples with\n        (section_name, section_proxy) for each section, including DEFAULTSECT.\n\n    remove_section(section)\n        Remove the given file section and all its options.\n\n    remove_option(section, option)\n        Remove the given option from the given section.\n\n    set(section, option, value)\n        Set the given option.\n\n    write(fp, space_around_delimiters=True)\n        Write the configuration state in .ini format. If\n        `space_around_delimiters' is True (the default), delimiters\n        between keys and values are surrounded by spaces.\n\"\"\"\n\nfrom __future__ import absolute_import\nfrom __future__ import division\nfrom __future__ import print_function\nfrom __future__ import unicode_literals\n\nfrom collections import MutableMapping\nimport functools\nimport io\nimport itertools\nimport re\nimport sys\nimport warnings\n\nfrom backports.configparser.helpers import OrderedDict as _default_dict\nfrom backports.configparser.helpers import ChainMap as _ChainMap\nfrom backports.configparser.helpers import from_none, open, str, PY2\n\n__all__ = [\"NoSectionError\", \"DuplicateOptionError\", \"DuplicateSectionError\",\n           \"NoOptionError\", \"InterpolationError\", \"InterpolationDepthError\",\n           \"InterpolationMissingOptionError\", \"InterpolationSyntaxError\",\n           \"ParsingError\", \"MissingSectionHeaderError\",\n           \"ConfigParser\", \"SafeConfigParser\", \"RawConfigParser\",\n           \"Interpolation\", \"BasicInterpolation\",  \"ExtendedInterpolation\",\n           \"LegacyInterpolation\", \"SectionProxy\", \"ConverterMapping\",\n           \"DEFAULTSECT\", \"MAX_INTERPOLATION_DEPTH\"]\n\nDEFAULTSECT = \"DEFAULT\"\n\nMAX_INTERPOLATION_DEPTH = 10\n\n\n# exception classes\nclass Error(Exception):\n    \"\"\"Base class for ConfigParser exceptions.\"\"\"\n\n    def __init__(self, msg=''):\n        self.message = msg\n        Exception.__init__(self, msg)\n\n    def __repr__(self):\n        return self.message\n\n    __str__ = __repr__\n\n\nclass NoSectionError(Error):\n    \"\"\"Raised when no section matches a requested option.\"\"\"\n\n    def __init__(self, section):\n        Error.__init__(self, 'No section: %r' % (section,))\n        self.section = section\n        self.args = (section, )\n\n\nclass DuplicateSectionError(Error):\n    \"\"\"Raised when a section is repeated in an input source.\n\n    Possible repetitions that raise this exception are: multiple creation\n    using the API or in strict parsers when a section is found more than once\n    in a single input file, string or dictionary.\n    \"\"\"\n\n    def __init__(self, section, source=None, lineno=None):\n        msg = [repr(section), \" already exists\"]\n        if source is not None:\n            message = [\"While reading from \", repr(source)]\n            if lineno is not None:\n                message.append(\" [line {0:2d}]\".format(lineno))\n            message.append(\": section \")\n            message.extend(msg)\n            msg = message\n        else:\n            msg.insert(0, \"Section \")\n        Error.__init__(self, \"\".join(msg))\n        self.section = section\n        self.source = source\n        self.lineno = lineno\n        self.args = (section, source, lineno)\n\n\nclass DuplicateOptionError(Error):\n    \"\"\"Raised by strict parsers when an option is repeated in an input source.\n\n    Current implementation raises this exception only when an option is found\n    more than once in a single file, string or dictionary.\n    \"\"\"\n\n    def __init__(self, section, option, source=None, lineno=None):\n        msg = [repr(option), \" in section \", repr(section),\n               \" already exists\"]\n        if source is not None:\n            message = [\"While reading from \", repr(source)]\n            if lineno is not None:\n                message.append(\" [line {0:2d}]\".format(lineno))\n            message.append(\": option \")\n            message.extend(msg)\n            msg = message\n        else:\n            msg.insert(0, \"Option \")\n        Error.__init__(self, \"\".join(msg))\n        self.section = section\n        self.option = option\n        self.source = source\n        self.lineno = lineno\n        self.args = (section, option, source, lineno)\n\n\nclass NoOptionError(Error):\n    \"\"\"A requested option was not found.\"\"\"\n\n    def __init__(self, option, section):\n        Error.__init__(self, \"No option %r in section: %r\" %\n                       (option, section))\n        self.option = option\n        self.section = section\n        self.args = (option, section)\n\n\nclass InterpolationError(Error):\n    \"\"\"Base class for interpolation-related exceptions.\"\"\"\n\n    def __init__(self, option, section, msg):\n        Error.__init__(self, msg)\n        self.option = option\n        self.section = section\n        self.args = (option, section, msg)\n\n\nclass InterpolationMissingOptionError(InterpolationError):\n    \"\"\"A string substitution required a setting which was not available.\"\"\"\n\n    def __init__(self, option, section, rawval, reference):\n        msg = (\"Bad value substitution: option {0!r} in section {1!r} contains \"\n               \"an interpolation key {2!r} which is not a valid option name. \"\n               \"Raw value: {3!r}\".format(option, section, reference, rawval))\n        InterpolationError.__init__(self, option, section, msg)\n        self.reference = reference\n        self.args = (option, section, rawval, reference)\n\n\nclass InterpolationSyntaxError(InterpolationError):\n    \"\"\"Raised when the source text contains invalid syntax.\n\n    Current implementation raises this exception when the source text into\n    which substitutions are made does not conform to the required syntax.\n    \"\"\"\n\n\nclass InterpolationDepthError(InterpolationError):\n    \"\"\"Raised when substitutions are nested too deeply.\"\"\"\n\n    def __init__(self, option, section, rawval):\n        msg = (\"Recursion limit exceeded in value substitution: option {0!r} \"\n               \"in section {1!r} contains an interpolation key which \"\n               \"cannot be substituted in {2} steps. Raw value: {3!r}\"\n               \"\".format(option, section, MAX_INTERPOLATION_DEPTH,\n                         rawval))\n        InterpolationError.__init__(self, option, section, msg)\n        self.args = (option, section, rawval)\n\n\nclass ParsingError(Error):\n    \"\"\"Raised when a configuration file does not follow legal syntax.\"\"\"\n\n    def __init__(self, source=None, filename=None):\n        # Exactly one of `source'/`filename' arguments has to be given.\n        # `filename' kept for compatibility.\n        if filename and source:\n            raise ValueError(\"Cannot specify both `filename' and `source'. \"\n                             \"Use `source'.\")\n        elif not filename and not source:\n            raise ValueError(\"Required argument `source' not given.\")\n        elif filename:\n            source = filename\n        Error.__init__(self, 'Source contains parsing errors: %r' % source)\n        self.source = source\n        self.errors = []\n        self.args = (source, )\n\n    @property\n    def filename(self):\n        \"\"\"Deprecated, use `source'.\"\"\"\n        warnings.warn(\n            \"The 'filename' attribute will be removed in future versions.  \"\n            \"Use 'source' instead.\",\n            DeprecationWarning, stacklevel=2\n        )\n        return self.source\n\n    @filename.setter\n    def filename(self, value):\n        \"\"\"Deprecated, user `source'.\"\"\"\n        warnings.warn(\n            \"The 'filename' attribute will be removed in future versions.  \"\n            \"Use 'source' instead.\",\n            DeprecationWarning, stacklevel=2\n        )\n        self.source = value\n\n    def append(self, lineno, line):\n        self.errors.append((lineno, line))\n        self.message += '\\n\\t[line %2d]: %s' % (lineno, line)\n\n\nclass MissingSectionHeaderError(ParsingError):\n    \"\"\"Raised when a key-value pair is found before any section header.\"\"\"\n\n    def __init__(self, filename, lineno, line):\n        Error.__init__(\n            self,\n            'File contains no section headers.\\nfile: %r, line: %d\\n%r' %\n            (filename, lineno, line))\n        self.source = filename\n        self.lineno = lineno\n        self.line = line\n        self.args = (filename, lineno, line)\n\n\n# Used in parser getters to indicate the default behaviour when a specific\n# option is not found it to raise an exception. Created to enable `None' as\n# a valid fallback value.\n_UNSET = object()\n\n\nclass Interpolation(object):\n    \"\"\"Dummy interpolation that passes the value through with no changes.\"\"\"\n\n    def before_get(self, parser, section, option, value, defaults):\n        return value\n\n    def before_set(self, parser, section, option, value):\n        return value\n\n    def before_read(self, parser, section, option, value):\n        return value\n\n    def before_write(self, parser, section, option, value):\n        return value\n\n\nclass BasicInterpolation(Interpolation):\n    \"\"\"Interpolation as implemented in the classic ConfigParser.\n\n    The option values can contain format strings which refer to other values in\n    the same section, or values in the special default section.\n\n    For example:\n\n        something: %(dir)s/whatever\n\n    would resolve the \"%(dir)s\" to the value of dir.  All reference\n    expansions are done late, on demand. If a user needs to use a bare % in\n    a configuration file, she can escape it by writing %%. Other % usage\n    is considered a user error and raises `InterpolationSyntaxError'.\"\"\"\n\n    _KEYCRE = re.compile(r\"%\\(([^)]+)\\)s\")\n\n    def before_get(self, parser, section, option, value, defaults):\n        L = []\n        self._interpolate_some(parser, option, L, value, section, defaults, 1)\n        return ''.join(L)\n\n    def before_set(self, parser, section, option, value):\n        tmp_value = value.replace('%%', '') # escaped percent signs\n        tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax\n        if '%' in tmp_value:\n            raise ValueError(\"invalid interpolation syntax in %r at \"\n                             \"position %d\" % (value, tmp_value.find('%')))\n        return value\n\n    def _interpolate_some(self, parser, option, accum, rest, section, map,\n                          depth):\n        rawval = parser.get(section, option, raw=True, fallback=rest)\n        if depth > MAX_INTERPOLATION_DEPTH:\n            raise InterpolationDepthError(option, section, rawval)\n        while rest:\n            p = rest.find(\"%\")\n            if p < 0:\n                accum.append(rest)\n                return\n            if p > 0:\n                accum.append(rest[:p])\n                rest = rest[p:]\n            # p is no longer used\n            c = rest[1:2]\n            if c == \"%\":\n                accum.append(\"%\")\n                rest = rest[2:]\n            elif c == \"(\":\n                m = self._KEYCRE.match(rest)\n                if m is None:\n                    raise InterpolationSyntaxError(option, section,\n                        \"bad interpolation variable reference %r\" % rest)\n                var = parser.optionxform(m.group(1))\n                rest = rest[m.end():]\n                try:\n                    v = map[var]\n                except KeyError:\n                    raise from_none(InterpolationMissingOptionError(\n                        option, section, rawval, var))\n                if \"%\" in v:\n                    self._interpolate_some(parser, option, accum, v,\n                                           section, map, depth + 1)\n                else:\n                    accum.append(v)\n            else:\n                raise InterpolationSyntaxError(\n                    option, section,\n                    \"'%%' must be followed by '%%' or '(', \"\n                    \"found: %r\" % (rest,))\n\n\nclass ExtendedInterpolation(Interpolation):\n    \"\"\"Advanced variant of interpolation, supports the syntax used by\n    `zc.buildout'. Enables interpolation between sections.\"\"\"\n\n    _KEYCRE = re.compile(r\"\\$\\{([^}]+)\\}\")\n\n    def before_get(self, parser, section, option, value, defaults):\n        L = []\n        self._interpolate_some(parser, option, L, value, section, defaults, 1)\n        return ''.join(L)\n\n    def before_set(self, parser, section, option, value):\n        tmp_value = value.replace('$$', '') # escaped dollar signs\n        tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax\n        if '$' in tmp_value:\n            raise ValueError(\"invalid interpolation syntax in %r at \"\n                             \"position %d\" % (value, tmp_value.find('$')))\n        return value\n\n    def _interpolate_some(self, parser, option, accum, rest, section, map,\n                          depth):\n        rawval = parser.get(section, option, raw=True, fallback=rest)\n        if depth > MAX_INTERPOLATION_DEPTH:\n            raise InterpolationDepthError(option, section, rawval)\n        while rest:\n            p = rest.find(\"$\")\n            if p < 0:\n                accum.append(rest)\n                return\n            if p > 0:\n                accum.append(rest[:p])\n                rest = rest[p:]\n            # p is no longer used\n            c = rest[1:2]\n            if c == \"$\":\n                accum.append(\"$\")\n                rest = rest[2:]\n            elif c == \"{\":\n                m = self._KEYCRE.match(rest)\n                if m is None:\n                    raise InterpolationSyntaxError(option, section,\n                        \"bad interpolation variable reference %r\" % rest)\n                path = m.group(1).split(':')\n                rest = rest[m.end():]\n                sect = section\n                opt = option\n                try:\n                    if len(path) == 1:\n                        opt = parser.optionxform(path[0])\n                        v = map[opt]\n                    elif len(path) == 2:\n                        sect = path[0]\n                        opt = parser.optionxform(path[1])\n                        v = parser.get(sect, opt, raw=True)\n                    else:\n                        raise InterpolationSyntaxError(\n                            option, section,\n                            \"More than one ':' found: %r\" % (rest,))\n                except (KeyError, NoSectionError, NoOptionError):\n                    raise from_none(InterpolationMissingOptionError(\n                        option, section, rawval, \":\".join(path)))\n                if \"$\" in v:\n                    self._interpolate_some(parser, opt, accum, v, sect,\n                                           dict(parser.items(sect, raw=True)),\n                                           depth + 1)\n                else:\n                    accum.append(v)\n            else:\n                raise InterpolationSyntaxError(\n                    option, section,\n                    \"'$' must be followed by '$' or '{', \"\n                    \"found: %r\" % (rest,))\n\n\nclass LegacyInterpolation(Interpolation):\n    \"\"\"Deprecated interpolation used in old versions of ConfigParser.\n    Use BasicInterpolation or ExtendedInterpolation instead.\"\"\"\n\n    _KEYCRE = re.compile(r\"%\\(([^)]*)\\)s|.\")\n\n    def before_get(self, parser, section, option, value, vars):\n        rawval = value\n        depth = MAX_INTERPOLATION_DEPTH\n        while depth:                    # Loop through this until it's done\n            depth -= 1\n            if value and \"%(\" in value:\n                replace = functools.partial(self._interpolation_replace,\n                                            parser=parser)\n                value = self._KEYCRE.sub(replace, value)\n                try:\n                    value = value % vars\n                except KeyError as e:\n                    raise from_none(InterpolationMissingOptionError(\n                        option, section, rawval, e.args[0]))\n            else:\n                break\n        if value and \"%(\" in value:\n            raise InterpolationDepthError(option, section, rawval)\n        return value\n\n    def before_set(self, parser, section, option, value):\n        return value\n\n    @staticmethod\n    def _interpolation_replace(match, parser):\n        s = match.group(1)\n        if s is None:\n            return match.group()\n        else:\n            return \"%%(%s)s\" % parser.optionxform(s)\n\n\nclass RawConfigParser(MutableMapping):\n    \"\"\"ConfigParser that does not do interpolation.\"\"\"\n\n    # Regular expressions for parsing section headers and options\n    _SECT_TMPL = r\"\"\"\n        \\[                                 # [\n        (?P<header>[^]]+)                  # very permissive!\n        \\]                                 # ]\n        \"\"\"\n    _OPT_TMPL = r\"\"\"\n        (?P<option>.*?)                    # very permissive!\n        \\s*(?P<vi>{delim})\\s*              # any number of space/tab,\n                                           # followed by any of the\n                                           # allowed delimiters,\n                                           # followed by any space/tab\n        (?P<value>.*)$                     # everything up to eol\n        \"\"\"\n    _OPT_NV_TMPL = r\"\"\"\n        (?P<option>.*?)                    # very permissive!\n        \\s*(?:                             # any number of space/tab,\n        (?P<vi>{delim})\\s*                 # optionally followed by\n                                           # any of the allowed\n                                           # delimiters, followed by any\n                                           # space/tab\n        (?P<value>.*))?$                   # everything up to eol\n        \"\"\"\n    # Interpolation algorithm to be used if the user does not specify another\n    _DEFAULT_INTERPOLATION = Interpolation()\n    # Compiled regular expression for matching sections\n    SECTCRE = re.compile(_SECT_TMPL, re.VERBOSE)\n    # Compiled regular expression for matching options with typical separators\n    OPTCRE = re.compile(_OPT_TMPL.format(delim=\"=|:\"), re.VERBOSE)\n    # Compiled regular expression for matching options with optional values\n    # delimited using typical separators\n    OPTCRE_NV = re.compile(_OPT_NV_TMPL.format(delim=\"=|:\"), re.VERBOSE)\n    # Compiled regular expression for matching leading whitespace in a line\n    NONSPACECRE = re.compile(r\"\\S\")\n    # Possible boolean values in the configuration.\n    BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,\n                      '0': False, 'no': False, 'false': False, 'off': False}\n\n    def __init__(self, defaults=None, dict_type=_default_dict,\n                 allow_no_value=False, **kwargs):\n\n        # keyword-only arguments\n        delimiters = kwargs.get('delimiters', ('=', ':'))\n        comment_prefixes = kwargs.get('comment_prefixes', ('#', ';'))\n        inline_comment_prefixes = kwargs.get('inline_comment_prefixes', None)\n        strict = kwargs.get('strict', True)\n        empty_lines_in_values = kwargs.get('empty_lines_in_values', True)\n        default_section = kwargs.get('default_section', DEFAULTSECT)\n        interpolation = kwargs.get('interpolation', _UNSET)\n        converters = kwargs.get('converters', _UNSET)\n\n        self._dict = dict_type\n        self._sections = self._dict()\n        self._defaults = self._dict()\n        self._converters = ConverterMapping(self)\n        self._proxies = self._dict()\n        self._proxies[default_section] = SectionProxy(self, default_section)\n        if defaults:\n            for key, value in defaults.items():\n                self._defaults[self.optionxform(key)] = value\n        self._delimiters = tuple(delimiters)\n        if delimiters == ('=', ':'):\n            self._optcre = self.OPTCRE_NV if allow_no_value else self.OPTCRE\n        else:\n            d = \"|\".join(re.escape(d) for d in delimiters)\n            if allow_no_value:\n                self._optcre = re.compile(self._OPT_NV_TMPL.format(delim=d),\n                                          re.VERBOSE)\n            else:\n                self._optcre = re.compile(self._OPT_TMPL.format(delim=d),\n                                          re.VERBOSE)\n        self._comment_prefixes = tuple(comment_prefixes or ())\n        self._inline_comment_prefixes = tuple(inline_comment_prefixes or ())\n        self._strict = strict\n        self._allow_no_value = allow_no_value\n        self._empty_lines_in_values = empty_lines_in_values\n        self.default_section=default_section\n        self._interpolation = interpolation\n        if self._interpolation is _UNSET:\n            self._interpolation = self._DEFAULT_INTERPOLATION\n        if self._interpolation is None:\n            self._interpolation = Interpolation()\n        if converters is not _UNSET:\n            self._converters.update(converters)\n\n    def defaults(self):\n        return self._defaults\n\n    def sections(self):\n        \"\"\"Return a list of section names, excluding [DEFAULT]\"\"\"\n        # self._sections will never have [DEFAULT] in it\n        return list(self._sections.keys())\n\n    def add_section(self, section):\n        \"\"\"Create a new section in the configuration.\n\n        Raise DuplicateSectionError if a section by the specified name\n        already exists. Raise ValueError if name is DEFAULT.\n        \"\"\"\n        if section == self.default_section:\n            raise ValueError('Invalid section name: %r' % section)\n\n        if section in self._sections:\n            raise DuplicateSectionError(section)\n        self._sections[section] = self._dict()\n        self._proxies[section] = SectionProxy(self, section)\n\n    def has_section(self, section):\n        \"\"\"Indicate whether the named section is present in the configuration.\n\n        The DEFAULT section is not acknowledged.\n        \"\"\"\n        return section in self._sections\n\n    def options(self, section):\n        \"\"\"Return a list of option names for the given section name.\"\"\"\n        try:\n            opts = self._sections[section].copy()\n        except KeyError:\n            raise from_none(NoSectionError(section))\n        opts.update(self._defaults)\n        return list(opts.keys())\n\n    def read(self, filenames, encoding=None):\n        \"\"\"Read and parse a filename or a list of filenames.\n\n        Files that cannot be opened are silently ignored; this is\n        designed so that you can specify a list of potential\n        configuration file locations (e.g. current directory, user's\n        home directory, systemwide directory), and all existing\n        configuration files in the list will be read.  A single\n        filename may also be given.\n\n        Return list of successfully read files.\n        \"\"\"\n        if PY2 and isinstance(filenames, bytes):\n            # we allow for a little unholy magic for Python 2 so that\n            # people not using unicode_literals can still use the library\n            # conveniently\n            warnings.warn(\n                \"You passed a bytestring as `filenames`. This will not work\"\n                \" on Python 3. Use `cp.read_file()` or switch to using Unicode\"\n                \" strings across the board.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n            filenames = [filenames]\n        elif isinstance(filenames, str):\n            filenames = [filenames]\n        read_ok = []\n        for filename in filenames:\n            try:\n                with open(filename, encoding=encoding) as fp:\n                    self._read(fp, filename)\n            except IOError:\n                continue\n            read_ok.append(filename)\n        return read_ok\n\n    def read_file(self, f, source=None):\n        \"\"\"Like read() but the argument must be a file-like object.\n\n        The `f' argument must be iterable, returning one line at a time.\n        Optional second argument is the `source' specifying the name of the\n        file being read. If not given, it is taken from f.name. If `f' has no\n        `name' attribute, `<???>' is used.\n        \"\"\"\n        if source is None:\n            try:\n                source = f.name\n            except AttributeError:\n                source = '<???>'\n        self._read(f, source)\n\n    def read_string(self, string, source='<string>'):\n        \"\"\"Read configuration from a given string.\"\"\"\n        sfile = io.StringIO(string)\n        self.read_file(sfile, source)\n\n    def read_dict(self, dictionary, source='<dict>'):\n        \"\"\"Read configuration from a dictionary.\n\n        Keys are section names, values are dictionaries with keys and values\n        that should be present in the section. If the used dictionary type\n        preserves order, sections and their keys will be added in order.\n\n        All types held in the dictionary are converted to strings during\n        reading, including section names, option names and keys.\n\n        Optional second argument is the `source' specifying the name of the\n        dictionary being read.\n        \"\"\"\n        elements_added = set()\n        for section, keys in dictionary.items():\n            section = str(section)\n            try:\n                self.add_section(section)\n            except (DuplicateSectionError, ValueError):\n                if self._strict and section in elements_added:\n                    raise\n            elements_added.add(section)\n            for key, value in keys.items():\n                key = self.optionxform(str(key))\n                if value is not None:\n                    value = str(value)\n                if self._strict and (section, key) in elements_added:\n                    raise DuplicateOptionError(section, key, source)\n                elements_added.add((section, key))\n                self.set(section, key, value)\n\n    def readfp(self, fp, filename=None):\n        \"\"\"Deprecated, use read_file instead.\"\"\"\n        warnings.warn(\n            \"This method will be removed in future versions.  \"\n            \"Use 'parser.read_file()' instead.\",\n            DeprecationWarning, stacklevel=2\n        )\n        self.read_file(fp, source=filename)\n\n    def get(self, section, option, **kwargs):\n        \"\"\"Get an option value for a given section.\n\n        If `vars' is provided, it must be a dictionary. The option is looked up\n        in `vars' (if provided), `section', and in `DEFAULTSECT' in that order.\n        If the key is not found and `fallback' is provided, it is used as\n        a fallback value. `None' can be provided as a `fallback' value.\n\n        If interpolation is enabled and the optional argument `raw' is False,\n        all interpolations are expanded in the return values.\n\n        Arguments `raw', `vars', and `fallback' are keyword only.\n\n        The section DEFAULT is special.\n        \"\"\"\n        # keyword-only arguments\n        raw = kwargs.get('raw', False)\n        vars = kwargs.get('vars', None)\n        fallback = kwargs.get('fallback', _UNSET)\n\n        try:\n            d = self._unify_values(section, vars)\n        except NoSectionError:\n            if fallback is _UNSET:\n                raise\n            else:\n                return fallback\n        option = self.optionxform(option)\n        try:\n            value = d[option]\n        except KeyError:\n            if fallback is _UNSET:\n                raise NoOptionError(option, section)\n            else:\n                return fallback\n\n        if raw or value is None:\n            return value\n        else:\n            return self._interpolation.before_get(self, section, option, value,\n                                                  d)\n\n    def _get(self, section, conv, option, **kwargs):\n        return conv(self.get(section, option, **kwargs))\n\n    def _get_conv(self, section, option, conv, **kwargs):\n        # keyword-only arguments\n        kwargs.setdefault('raw', False)\n        kwargs.setdefault('vars', None)\n        fallback = kwargs.pop('fallback', _UNSET)\n        try:\n            return self._get(section, conv, option, **kwargs)\n        except (NoSectionError, NoOptionError):\n            if fallback is _UNSET:\n                raise\n            return fallback\n\n    # getint, getfloat and getboolean provided directly for backwards compat\n    def getint(self, section, option, **kwargs):\n        # keyword-only arguments\n        kwargs.setdefault('raw', False)\n        kwargs.setdefault('vars', None)\n        kwargs.setdefault('fallback', _UNSET)\n        return self._get_conv(section, option, int, **kwargs)\n\n    def getfloat(self, section, option, **kwargs):\n        # keyword-only arguments\n        kwargs.setdefault('raw', False)\n        kwargs.setdefault('vars', None)\n        kwargs.setdefault('fallback', _UNSET)\n        return self._get_conv(section, option, float, **kwargs)\n\n    def getboolean(self, section, option, **kwargs):\n        # keyword-only arguments\n        kwargs.setdefault('raw', False)\n        kwargs.setdefault('vars', None)\n        kwargs.setdefault('fallback', _UNSET)\n        return self._get_conv(section, option, self._convert_to_boolean,\n                              **kwargs)\n\n    def items(self, section=_UNSET, raw=False, vars=None):\n        \"\"\"Return a list of (name, value) tuples for each option in a section.\n\n        All % interpolations are expanded in the return values, based on the\n        defaults passed into the constructor, unless the optional argument\n        `raw' is true.  Additional substitutions may be provided using the\n        `vars' argument, which must be a dictionary whose contents overrides\n        any pre-existing defaults.\n\n        The section DEFAULT is special.\n        \"\"\"\n        if section is _UNSET:\n            return super(RawConfigParser, self).items()\n        d = self._defaults.copy()\n        try:\n            d.update(self._sections[section])\n        except KeyError:\n            if section != self.default_section:\n                raise NoSectionError(section)\n        # Update with the entry specific variables\n        if vars:\n            for key, value in vars.items():\n                d[self.optionxform(key)] = value\n        value_getter = lambda option: self._interpolation.before_get(self,\n            section, option, d[option], d)\n        if raw:\n            value_getter = lambda option: d[option]\n        return [(option, value_getter(option)) for option in d.keys()]\n\n    def popitem(self):\n        \"\"\"Remove a section from the parser and return it as\n        a (section_name, section_proxy) tuple. If no section is present, raise\n        KeyError.\n\n        The section DEFAULT is never returned because it cannot be removed.\n        \"\"\"\n        for key in self.sections():\n            value = self[key]\n            del self[key]\n            return key, value\n        raise KeyError\n\n    def optionxform(self, optionstr):\n        return optionstr.lower()\n\n    def has_option(self, section, option):\n        \"\"\"Check for the existence of a given option in a given section.\n        If the specified `section' is None or an empty string, DEFAULT is\n        assumed. If the specified `section' does not exist, returns False.\"\"\"\n        if not section or section == self.default_section:\n            option = self.optionxform(option)\n            return option in self._defaults\n        elif section not in self._sections:\n            return False\n        else:\n            option = self.optionxform(option)\n            return (option in self._sections[section]\n                    or option in self._defaults)\n\n    def set(self, section, option, value=None):\n        \"\"\"Set an option.\"\"\"\n        if value:\n            value = self._interpolation.before_set(self, section, option,\n                                                   value)\n        if not section or section == self.default_section:\n            sectdict = self._defaults\n        else:\n            try:\n                sectdict = self._sections[section]\n            except KeyError:\n                raise from_none(NoSectionError(section))\n        sectdict[self.optionxform(option)] = value\n\n    def write(self, fp, space_around_delimiters=True):\n        \"\"\"Write an .ini-format representation of the configuration state.\n\n        If `space_around_delimiters' is True (the default), delimiters\n        between keys and values are surrounded by spaces.\n        \"\"\"\n        if space_around_delimiters:\n            d = \" {0} \".format(self._delimiters[0])\n        else:\n            d = self._delimiters[0]\n        if self._defaults:\n            self._write_section(fp, self.default_section,\n                                    self._defaults.items(), d)\n        for section in self._sections:\n            self._write_section(fp, section,\n                                self._sections[section].items(), d)\n\n    def _write_section(self, fp, section_name, section_items, delimiter):\n        \"\"\"Write a single section to the specified `fp'.\"\"\"\n        fp.write(\"[{0}]\\n\".format(section_name))\n        for key, value in section_items:\n            value = self._interpolation.before_write(self, section_name, key,\n                                                     value)\n            if value is not None or not self._allow_no_value:\n                value = delimiter + str(value).replace('\\n', '\\n\\t')\n            else:\n                value = \"\"\n            fp.write(\"{0}{1}\\n\".format(key, value))\n        fp.write(\"\\n\")\n\n    def remove_option(self, section, option):\n        \"\"\"Remove an option.\"\"\"\n        if not section or section == self.default_section:\n            sectdict = self._defaults\n        else:\n            try:\n                sectdict = self._sections[section]\n            except KeyError:\n                raise from_none(NoSectionError(section))\n        option = self.optionxform(option)\n        existed = option in sectdict\n        if existed:\n            del sectdict[option]\n        return existed\n\n    def remove_section(self, section):\n        \"\"\"Remove a file section.\"\"\"\n        existed = section in self._sections\n        if existed:\n            del self._sections[section]\n            del self._proxies[section]\n        return existed\n\n    def __getitem__(self, key):\n        if key != self.default_section and not self.has_section(key):\n            raise KeyError(key)\n        return self._proxies[key]\n\n    def __setitem__(self, key, value):\n        # To conform with the mapping protocol, overwrites existing values in\n        # the section.\n\n        # XXX this is not atomic if read_dict fails at any point. Then again,\n        # no update method in configparser is atomic in this implementation.\n        if key == self.default_section:\n            self._defaults.clear()\n        elif key in self._sections:\n            self._sections[key].clear()\n        self.read_dict({key: value})\n\n    def __delitem__(self, key):\n        if key == self.default_section:\n            raise ValueError(\"Cannot remove the default section.\")\n        if not self.has_section(key):\n            raise KeyError(key)\n        self.remove_section(key)\n\n    def __contains__(self, key):\n        return key == self.default_section or self.has_section(key)\n\n    def __len__(self):\n        return len(self._sections) + 1 # the default section\n\n    def __iter__(self):\n        # XXX does it break when underlying container state changed?\n        return itertools.chain((self.default_section,), self._sections.keys())\n\n    def _read(self, fp, fpname):\n        \"\"\"Parse a sectioned configuration file.\n\n        Each section in a configuration file contains a header, indicated by\n        a name in square brackets (`[]'), plus key/value options, indicated by\n        `name' and `value' delimited with a specific substring (`=' or `:' by\n        default).\n\n        Values can span multiple lines, as long as they are indented deeper\n        than the first line of the value. Depending on the parser's mode, blank\n        lines may be treated as parts of multiline values or ignored.\n\n        Configuration files may include comments, prefixed by specific\n        characters (`#' and `;' by default). Comments may appear on their own\n        in an otherwise empty line or may be entered in lines holding values or\n        section names.\n        \"\"\"\n        elements_added = set()\n        cursect = None                        # None, or a dictionary\n        sectname = None\n        optname = None\n        lineno = 0\n        indent_level = 0\n        e = None                              # None, or an exception\n        for lineno, line in enumerate(fp, start=1):\n            comment_start = sys.maxsize\n            # strip inline comments\n            inline_prefixes = dict(\n                (p, -1) for p in self._inline_comment_prefixes)\n            while comment_start == sys.maxsize and inline_prefixes:\n                next_prefixes = {}\n                for prefix, index in inline_prefixes.items():\n                    index = line.find(prefix, index+1)\n                    if index == -1:\n                        continue\n                    next_prefixes[prefix] = index\n                    if index == 0 or (index > 0 and line[index-1].isspace()):\n                        comment_start = min(comment_start, index)\n                inline_prefixes = next_prefixes\n            # strip full line comments\n            for prefix in self._comment_prefixes:\n                if line.strip().startswith(prefix):\n                    comment_start = 0\n                    break\n            if comment_start == sys.maxsize:\n                comment_start = None\n            value = line[:comment_start].strip()\n            if not value:\n                if self._empty_lines_in_values:\n                    # add empty line to the value, but only if there was no\n                    # comment on the line\n                    if (comment_start is None and\n                        cursect is not None and\n                        optname and\n                        cursect[optname] is not None):\n                        cursect[optname].append('') # newlines added at join\n                else:\n                    # empty line marks end of value\n                    indent_level = sys.maxsize\n                continue\n            # continuation line?\n            first_nonspace = self.NONSPACECRE.search(line)\n            cur_indent_level = first_nonspace.start() if first_nonspace else 0\n            if (cursect is not None and optname and\n                cur_indent_level > indent_level):\n                cursect[optname].append(value)\n            # a section header or option header?\n            else:\n                indent_level = cur_indent_level\n                # is it a section header?\n                mo = self.SECTCRE.match(value)\n                if mo:\n                    sectname = mo.group('header')\n                    if sectname in self._sections:\n                        if self._strict and sectname in elements_added:\n                            raise DuplicateSectionError(sectname, fpname,\n                                                        lineno)\n                        cursect = self._sections[sectname]\n                        elements_added.add(sectname)\n                    elif sectname == self.default_section:\n                        cursect = self._defaults\n                    else:\n                        cursect = self._dict()\n                        self._sections[sectname] = cursect\n                        self._proxies[sectname] = SectionProxy(self, sectname)\n                        elements_added.add(sectname)\n                    # So sections can't start with a continuation line\n                    optname = None\n                # no section header in the file?\n                elif cursect is None:\n                    raise MissingSectionHeaderError(fpname, lineno, line)\n                # an option line?\n                else:\n                    mo = self._optcre.match(value)\n                    if mo:\n                        optname, vi, optval = mo.group('option', 'vi', 'value')\n                        if not optname:\n                            e = self._handle_error(e, fpname, lineno, line)\n                        optname = self.optionxform(optname.rstrip())\n                        if (self._strict and\n                            (sectname, optname) in elements_added):\n                            raise DuplicateOptionError(sectname, optname,\n                                                       fpname, lineno)\n                        elements_added.add((sectname, optname))\n                        # This check is fine because the OPTCRE cannot\n                        # match if it would set optval to None\n                        if optval is not None:\n                            optval = optval.strip()\n                            cursect[optname] = [optval]\n                        else:\n                            # valueless option handling\n                            cursect[optname] = None\n                    else:\n                        # a non-fatal parsing error occurred. set up the\n                        # exception but keep going. the exception will be\n                        # raised at the end of the file and will contain a\n                        # list of all bogus lines\n                        e = self._handle_error(e, fpname, lineno, line)\n        # if any parsing errors occurred, raise an exception\n        if e:\n            raise e\n        self._join_multiline_values()\n\n    def _join_multiline_values(self):\n        defaults = self.default_section, self._defaults\n        all_sections = itertools.chain((defaults,),\n                                       self._sections.items())\n        for section, options in all_sections:\n            for name, val in options.items():\n                if isinstance(val, list):\n                    val = '\\n'.join(val).rstrip()\n                options[name] = self._interpolation.before_read(self,\n                                                                section,\n                                                                name, val)\n\n    def _handle_error(self, exc, fpname, lineno, line):\n        if not exc:\n            exc = ParsingError(fpname)\n        exc.append(lineno, repr(line))\n        return exc\n\n    def _unify_values(self, section, vars):\n        \"\"\"Create a sequence of lookups with 'vars' taking priority over\n        the 'section' which takes priority over the DEFAULTSECT.\n\n        \"\"\"\n        sectiondict = {}\n        try:\n            sectiondict = self._sections[section]\n        except KeyError:\n            if section != self.default_section:\n                raise NoSectionError(section)\n        # Update with the entry specific variables\n        vardict = {}\n        if vars:\n            for key, value in vars.items():\n                if value is not None:\n                    value = str(value)\n                vardict[self.optionxform(key)] = value\n        return _ChainMap(vardict, sectiondict, self._defaults)\n\n    def _convert_to_boolean(self, value):\n        \"\"\"Return a boolean value translating from other types if necessary.\n        \"\"\"\n        if value.lower() not in self.BOOLEAN_STATES:\n            raise ValueError('Not a boolean: %s' % value)\n        return self.BOOLEAN_STATES[value.lower()]\n\n    def _validate_value_types(self, **kwargs):\n        \"\"\"Raises a TypeError for non-string values.\n\n        The only legal non-string value if we allow valueless\n        options is None, so we need to check if the value is a\n        string if:\n        - we do not allow valueless options, or\n        - we allow valueless options but the value is not None\n\n        For compatibility reasons this method is not used in classic set()\n        for RawConfigParsers. It is invoked in every case for mapping protocol\n        access and in ConfigParser.set().\n        \"\"\"\n        # keyword-only arguments\n        section = kwargs.get('section', \"\")\n        option = kwargs.get('option', \"\")\n        value = kwargs.get('value', \"\")\n\n        if PY2 and bytes in (type(section), type(option), type(value)):\n            # we allow for a little unholy magic for Python 2 so that\n            # people not using unicode_literals can still use the library\n            # conveniently\n            warnings.warn(\n                \"You passed a bytestring. Implicitly decoding as UTF-8 string.\"\n                \" This will not work on Python 3. Please switch to using\"\n                \" Unicode strings across the board.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n            if isinstance(section, bytes):\n                section = section.decode('utf8')\n            if isinstance(option, bytes):\n                option = option.decode('utf8')\n            if isinstance(value, bytes):\n                value = value.decode('utf8')\n\n        if not isinstance(section, str):\n            raise TypeError(\"section names must be strings\")\n        if not isinstance(option, str):\n            raise TypeError(\"option keys must be strings\")\n        if not self._allow_no_value or value:\n            if not isinstance(value, str):\n                raise TypeError(\"option values must be strings\")\n\n        return section, option, value\n\n    @property\n    def converters(self):\n        return self._converters\n\n\nclass ConfigParser(RawConfigParser):\n    \"\"\"ConfigParser implementing interpolation.\"\"\"\n\n    _DEFAULT_INTERPOLATION = BasicInterpolation()\n\n    def set(self, section, option, value=None):\n        \"\"\"Set an option.  Extends RawConfigParser.set by validating type and\n        interpolation syntax on the value.\"\"\"\n        _, option, value = self._validate_value_types(option=option, value=value)\n        super(ConfigParser, self).set(section, option, value)\n\n    def add_section(self, section):\n        \"\"\"Create a new section in the configuration.  Extends\n        RawConfigParser.add_section by validating if the section name is\n        a string.\"\"\"\n        section, _, _ = self._validate_value_types(section=section)\n        super(ConfigParser, self).add_section(section)\n\n\nclass SafeConfigParser(ConfigParser):\n    \"\"\"ConfigParser alias for backwards compatibility purposes.\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(SafeConfigParser, self).__init__(*args, **kwargs)\n        warnings.warn(\n            \"The SafeConfigParser class has been renamed to ConfigParser \"\n            \"in Python 3.2. This alias will be removed in future versions.\"\n            \" Use ConfigParser directly instead.\",\n            DeprecationWarning, stacklevel=2\n        )\n\n\nclass SectionProxy(MutableMapping):\n    \"\"\"A proxy for a single section from a parser.\"\"\"\n\n    def __init__(self, parser, name):\n        \"\"\"Creates a view on a section of the specified `name` in `parser`.\"\"\"\n        self._parser = parser\n        self._name = name\n        for conv in parser.converters:\n            key = 'get' + conv\n            getter = functools.partial(self.get, _impl=getattr(parser, key))\n            setattr(self, key, getter)\n\n    def __repr__(self):\n        return '<Section: {0}>'.format(self._name)\n\n    def __getitem__(self, key):\n        if not self._parser.has_option(self._name, key):\n            raise KeyError(key)\n        return self._parser.get(self._name, key)\n\n    def __setitem__(self, key, value):\n        _, key, value = self._parser._validate_value_types(option=key, value=value)\n        return self._parser.set(self._name, key, value)\n\n    def __delitem__(self, key):\n        if not (self._parser.has_option(self._name, key) and\n                self._parser.remove_option(self._name, key)):\n            raise KeyError(key)\n\n    def __contains__(self, key):\n        return self._parser.has_option(self._name, key)\n\n    def __len__(self):\n        return len(self._options())\n\n    def __iter__(self):\n        return self._options().__iter__()\n\n    def _options(self):\n        if self._name != self._parser.default_section:\n            return self._parser.options(self._name)\n        else:\n            return self._parser.defaults()\n\n    @property\n    def parser(self):\n        # The parser object of the proxy is read-only.\n        return self._parser\n\n    @property\n    def name(self):\n        # The name of the section on a proxy is read-only.\n        return self._name\n\n    def get(self, option, fallback=None, **kwargs):\n        \"\"\"Get an option value.\n\n        Unless `fallback` is provided, `None` will be returned if the option\n        is not found.\n\n        \"\"\"\n        # keyword-only arguments\n        kwargs.setdefault('raw', False)\n        kwargs.setdefault('vars', None)\n        _impl = kwargs.pop('_impl', None)\n        # If `_impl` is provided, it should be a getter method on the parser\n        # object that provides the desired type conversion.\n        if not _impl:\n            _impl = self._parser.get\n        return _impl(self._name, option, fallback=fallback, **kwargs)\n\n\nclass ConverterMapping(MutableMapping):\n    \"\"\"Enables reuse of get*() methods between the parser and section proxies.\n\n    If a parser class implements a getter directly, the value for the given\n    key will be ``None``. The presence of the converter name here enables\n    section proxies to find and use the implementation on the parser class.\n    \"\"\"\n\n    GETTERCRE = re.compile(r\"^get(?P<name>.+)$\")\n\n    def __init__(self, parser):\n        self._parser = parser\n        self._data = {}\n        for getter in dir(self._parser):\n            m = self.GETTERCRE.match(getter)\n            if not m or not callable(getattr(self._parser, getter)):\n                continue\n            self._data[m.group('name')] = None   # See class docstring.\n\n    def __getitem__(self, key):\n        return self._data[key]\n\n    def __setitem__(self, key, value):\n        try:\n            k = 'get' + key\n        except TypeError:\n            raise ValueError('Incompatible key: {} (type: {})'\n                             ''.format(key, type(key)))\n        if k == 'get':\n            raise ValueError('Incompatible key: cannot use \"\" as a name')\n        self._data[key] = value\n        func = functools.partial(self._parser._get_conv, conv=value)\n        func.converter = value\n        setattr(self._parser, k, func)\n        for proxy in self._parser.values():\n            getter = functools.partial(proxy.get, _impl=func)\n            setattr(proxy, k, getter)\n\n    def __delitem__(self, key):\n        try:\n            k = 'get' + (key or None)\n        except TypeError:\n            raise KeyError(key)\n        del self._data[key]\n        for inst in itertools.chain((self._parser,), self._parser.values()):\n            try:\n                delattr(inst, k)\n            except AttributeError:\n                # don't raise since the entry was present in _data, silently\n                # clean up\n                continue\n\n    def __iter__(self):\n        return iter(self._data)\n\n    def __len__(self):\n        return len(self._data)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/backports/configparser/helpers.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nfrom __future__ import absolute_import\nfrom __future__ import division\nfrom __future__ import print_function\nfrom __future__ import unicode_literals\n\nfrom collections import MutableMapping\ntry:\n    from collections import UserDict\nexcept ImportError:\n    from UserDict import UserDict\n\ntry:\n    from collections import OrderedDict\nexcept ImportError:\n    from ordereddict import OrderedDict\n\nfrom io import open\nimport sys\ntry:\n    from thread import get_ident\nexcept ImportError:\n    try:\n        from _thread import get_ident\n    except ImportError:\n        from _dummy_thread import get_ident\n\n\nPY2 = sys.version_info[0] == 2\nPY3 = sys.version_info[0] == 3\n\nstr = type('str')\n\n\ndef from_none(exc):\n    \"\"\"raise from_none(ValueError('a')) == raise ValueError('a') from None\"\"\"\n    exc.__cause__ = None\n    exc.__suppress_context__ = True\n    return exc\n\n\n# from reprlib 3.2.1\ndef recursive_repr(fillvalue='...'):\n    'Decorator to make a repr function return fillvalue for a recursive call'\n\n    def decorating_function(user_function):\n        repr_running = set()\n\n        def wrapper(self):\n            key = id(self), get_ident()\n            if key in repr_running:\n                return fillvalue\n            repr_running.add(key)\n            try:\n                result = user_function(self)\n            finally:\n                repr_running.discard(key)\n            return result\n\n        # Can't use functools.wraps() here because of bootstrap issues\n        wrapper.__module__ = getattr(user_function, '__module__')\n        wrapper.__doc__ = getattr(user_function, '__doc__')\n        wrapper.__name__ = getattr(user_function, '__name__')\n        wrapper.__annotations__ = getattr(user_function, '__annotations__', {})\n        return wrapper\n\n    return decorating_function\n\n# from collections 3.2.1\nclass _ChainMap(MutableMapping):\n    ''' A ChainMap groups multiple dicts (or other mappings) together\n    to create a single, updateable view.\n\n    The underlying mappings are stored in a list.  That list is public and can\n    accessed or updated using the *maps* attribute.  There is no other state.\n\n    Lookups search the underlying mappings successively until a key is found.\n    In contrast, writes, updates, and deletions only operate on the first\n    mapping.\n\n    '''\n\n    def __init__(self, *maps):\n        '''Initialize a ChainMap by setting *maps* to the given mappings.\n        If no mappings are provided, a single empty dictionary is used.\n\n        '''\n        self.maps = list(maps) or [{}]          # always at least one map\n\n    def __missing__(self, key):\n        raise KeyError(key)\n\n    def __getitem__(self, key):\n        for mapping in self.maps:\n            try:\n                return mapping[key]             # can't use 'key in mapping' with defaultdict\n            except KeyError:\n                pass\n        return self.__missing__(key)            # support subclasses that define __missing__\n\n    def get(self, key, default=None):\n        return self[key] if key in self else default\n\n    def __len__(self):\n        return len(set().union(*self.maps))     # reuses stored hash values if possible\n\n    def __iter__(self):\n        return iter(set().union(*self.maps))\n\n    def __contains__(self, key):\n        return any(key in m for m in self.maps)\n\n    @recursive_repr()\n    def __repr__(self):\n        return '{0.__class__.__name__}({1})'.format(\n            self, ', '.join(map(repr, self.maps)))\n\n    @classmethod\n    def fromkeys(cls, iterable, *args):\n        'Create a ChainMap with a single dict created from the iterable.'\n        return cls(dict.fromkeys(iterable, *args))\n\n    def copy(self):\n        'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]'\n        return self.__class__(self.maps[0].copy(), *self.maps[1:])\n\n    __copy__ = copy\n\n    def new_child(self):                        # like Django's Context.push()\n        'New ChainMap with a new dict followed by all previous maps.'\n        return self.__class__({}, *self.maps)\n\n    @property\n    def parents(self):                          # like Django's Context.pop()\n        'New ChainMap from maps[1:].'\n        return self.__class__(*self.maps[1:])\n\n    def __setitem__(self, key, value):\n        self.maps[0][key] = value\n\n    def __delitem__(self, key):\n        try:\n            del self.maps[0][key]\n        except KeyError:\n            raise KeyError('Key not found in the first mapping: {!r}'.format(key))\n\n    def popitem(self):\n        'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.'\n        try:\n            return self.maps[0].popitem()\n        except KeyError:\n            raise KeyError('No keys found in the first mapping.')\n\n    def pop(self, key, *args):\n        'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].'\n        try:\n            return self.maps[0].pop(key, *args)\n        except KeyError:\n            raise KeyError('Key not found in the first mapping: {!r}'.format(key))\n\n    def clear(self):\n        'Clear maps[0], leaving maps[1:] intact.'\n        self.maps[0].clear()\n\n\ntry:\n    from collections import ChainMap\nexcept ImportError:\n    ChainMap = _ChainMap\n"
  },
  {
    "path": "openpype/vendor/python/python_2/backports/functools_lru_cache.py",
    "content": "from __future__ import absolute_import\n\nimport functools\nfrom collections import namedtuple\nfrom threading import RLock\n\n_CacheInfo = namedtuple(\"CacheInfo\", [\"hits\", \"misses\", \"maxsize\", \"currsize\"])\n\n\n@functools.wraps(functools.update_wrapper)\ndef update_wrapper(wrapper,\n                   wrapped,\n                   assigned = functools.WRAPPER_ASSIGNMENTS,\n                   updated = functools.WRAPPER_UPDATES):\n    \"\"\"\n    Patch two bugs in functools.update_wrapper.\n    \"\"\"\n    # workaround for http://bugs.python.org/issue3445\n    assigned = tuple(attr for attr in assigned if hasattr(wrapped, attr))\n    wrapper = functools.update_wrapper(wrapper, wrapped, assigned, updated)\n    # workaround for https://bugs.python.org/issue17482\n    wrapper.__wrapped__ = wrapped\n    return wrapper\n\n\nclass _HashedSeq(list):\n    __slots__ = 'hashvalue'\n\n    def __init__(self, tup, hash=hash):\n        self[:] = tup\n        self.hashvalue = hash(tup)\n\n    def __hash__(self):\n        return self.hashvalue\n\n\ndef _make_key(args, kwds, typed,\n              kwd_mark=(object(),),\n              fasttypes=set([int, str, frozenset, type(None)]),\n              sorted=sorted, tuple=tuple, type=type, len=len):\n    'Make a cache key from optionally typed positional and keyword arguments'\n    key = args\n    if kwds:\n        sorted_items = sorted(kwds.items())\n        key += kwd_mark\n        for item in sorted_items:\n            key += item\n    if typed:\n        key += tuple(type(v) for v in args)\n        if kwds:\n            key += tuple(type(v) for k, v in sorted_items)\n    elif len(key) == 1 and type(key[0]) in fasttypes:\n        return key[0]\n    return _HashedSeq(key)\n\n\ndef lru_cache(maxsize=100, typed=False):\n    \"\"\"Least-recently-used cache decorator.\n\n    If *maxsize* is set to None, the LRU features are disabled and the cache\n    can grow without bound.\n\n    If *typed* is True, arguments of different types will be cached separately.\n    For example, f(3.0) and f(3) will be treated as distinct calls with\n    distinct results.\n\n    Arguments to the cached function must be hashable.\n\n    View the cache statistics named tuple (hits, misses, maxsize, currsize) with\n    f.cache_info().  Clear the cache and statistics with f.cache_clear().\n    Access the underlying function with f.__wrapped__.\n\n    See:  http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used\n\n    \"\"\"\n\n    # Users should only access the lru_cache through its public API:\n    #       cache_info, cache_clear, and f.__wrapped__\n    # The internals of the lru_cache are encapsulated for thread safety and\n    # to allow the implementation to change (including a possible C version).\n\n    def decorating_function(user_function):\n\n        cache = dict()\n        stats = [0, 0]                  # make statistics updateable non-locally\n        HITS, MISSES = 0, 1             # names for the stats fields\n        make_key = _make_key\n        cache_get = cache.get           # bound method to lookup key or return None\n        _len = len                      # localize the global len() function\n        lock = RLock()                  # because linkedlist updates aren't threadsafe\n        root = []                       # root of the circular doubly linked list\n        root[:] = [root, root, None, None]      # initialize by pointing to self\n        nonlocal_root = [root]                  # make updateable non-locally\n        PREV, NEXT, KEY, RESULT = 0, 1, 2, 3    # names for the link fields\n\n        if maxsize == 0:\n\n            def wrapper(*args, **kwds):\n                # no caching, just do a statistics update after a successful call\n                result = user_function(*args, **kwds)\n                stats[MISSES] += 1\n                return result\n\n        elif maxsize is None:\n\n            def wrapper(*args, **kwds):\n                # simple caching without ordering or size limit\n                key = make_key(args, kwds, typed)\n                result = cache_get(key, root)   # root used here as a unique not-found sentinel\n                if result is not root:\n                    stats[HITS] += 1\n                    return result\n                result = user_function(*args, **kwds)\n                cache[key] = result\n                stats[MISSES] += 1\n                return result\n\n        else:\n\n            def wrapper(*args, **kwds):\n                # size limited caching that tracks accesses by recency\n                key = make_key(args, kwds, typed) if kwds or typed else args\n                with lock:\n                    link = cache_get(key)\n                    if link is not None:\n                        # record recent use of the key by moving it to the front of the list\n                        root, = nonlocal_root\n                        link_prev, link_next, key, result = link\n                        link_prev[NEXT] = link_next\n                        link_next[PREV] = link_prev\n                        last = root[PREV]\n                        last[NEXT] = root[PREV] = link\n                        link[PREV] = last\n                        link[NEXT] = root\n                        stats[HITS] += 1\n                        return result\n                result = user_function(*args, **kwds)\n                with lock:\n                    root, = nonlocal_root\n                    if key in cache:\n                        # getting here means that this same key was added to the\n                        # cache while the lock was released.  since the link\n                        # update is already done, we need only return the\n                        # computed result and update the count of misses.\n                        pass\n                    elif _len(cache) >= maxsize:\n                        # use the old root to store the new key and result\n                        oldroot = root\n                        oldroot[KEY] = key\n                        oldroot[RESULT] = result\n                        # empty the oldest link and make it the new root\n                        root = nonlocal_root[0] = oldroot[NEXT]\n                        oldkey = root[KEY]\n                        root[KEY] = root[RESULT] = None\n                        # now update the cache dictionary for the new links\n                        del cache[oldkey]\n                        cache[key] = oldroot\n                    else:\n                        # put result in a new link at the front of the list\n                        last = root[PREV]\n                        link = [last, root, key, result]\n                        last[NEXT] = root[PREV] = cache[key] = link\n                    stats[MISSES] += 1\n                return result\n\n        def cache_info():\n            \"\"\"Report cache statistics\"\"\"\n            with lock:\n                return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache))\n\n        def cache_clear():\n            \"\"\"Clear the cache and cache statistics\"\"\"\n            with lock:\n                cache.clear()\n                root = nonlocal_root[0]\n                root[:] = [root, root, None, None]\n                stats[:] = [0, 0]\n\n        wrapper.__wrapped__ = user_function\n        wrapper.cache_info = cache_info\n        wrapper.cache_clear = cache_clear\n        return update_wrapper(wrapper, user_function)\n\n    return decorating_function\n"
  },
  {
    "path": "openpype/vendor/python/python_2/builtins/__init__.py",
    "content": "from __future__ import absolute_import\nimport sys\n__future_module__ = True\n\nif sys.version_info[0] < 3:\n    from __builtin__ import *\n    # Overwrite any old definitions with the equivalent future.builtins ones:\n    from future.builtins import *\nelse:\n    raise ImportError('This package should not be accessible on Python 3. '\n                      'Either you are trying to run from the python-future src folder '\n                      'or your installation of python-future is corrupted.')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/certifi/__init__.py",
    "content": "from .core import contents, where\n\n__version__ = \"2021.10.08\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/certifi/__main__.py",
    "content": "import argparse\n\nfrom certifi import contents, where\n\nparser = argparse.ArgumentParser()\nparser.add_argument(\"-c\", \"--contents\", action=\"store_true\")\nargs = parser.parse_args()\n\nif args.contents:\n    print(contents())\nelse:\n    print(where())\n"
  },
  {
    "path": "openpype/vendor/python/python_2/certifi/cacert.pem",
    "content": "\n# Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA\n# Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA\n# Label: \"GlobalSign Root CA\"\n# Serial: 4835703278459707669005204\n# MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a\n# SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c\n# SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99\n-----BEGIN CERTIFICATE-----\nMIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG\nA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv\nb3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw\nMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i\nYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT\naWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ\njc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp\nxy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp\n1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG\nsnUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ\nU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8\n9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E\nBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B\nAQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz\nyj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE\n38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP\nAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad\nDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME\nHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==\n-----END CERTIFICATE-----\n\n# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2\n# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2\n# Label: \"GlobalSign Root CA - R2\"\n# Serial: 4835703278459682885658125\n# MD5 Fingerprint: 94:14:77:7e:3e:5e:fd:8f:30:bd:41:b0:cf:e7:d0:30\n# SHA1 Fingerprint: 75:e0:ab:b6:13:85:12:27:1c:04:f8:5f:dd:de:38:e4:b7:24:2e:fe\n# SHA256 Fingerprint: ca:42:dd:41:74:5f:d0:b8:1e:b9:02:36:2c:f9:d8:bf:71:9d:a1:bd:1b:1e:fc:94:6f:5b:4c:99:f4:2c:1b:9e\n-----BEGIN CERTIFICATE-----\nMIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G\nA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp\nZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1\nMDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG\nA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL\nv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8\neoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq\ntTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd\nC9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa\nzq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB\nmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH\nV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n\nbG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG\n3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs\nJ0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO\n291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS\not+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd\nAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7\nTBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==\n-----END CERTIFICATE-----\n\n# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited\n# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited\n# Label: \"Entrust.net Premium 2048 Secure Server CA\"\n# Serial: 946069240\n# MD5 Fingerprint: ee:29:31:bc:32:7e:9a:e6:e8:b5:f7:51:b4:34:71:90\n# SHA1 Fingerprint: 50:30:06:09:1d:97:d4:f5:ae:39:f7:cb:e7:92:7d:7d:65:2d:34:31\n# SHA256 Fingerprint: 6d:c4:71:72:e0:1c:bc:b0:bf:62:58:0d:89:5f:e2:b8:ac:9a:d4:f8:73:80:1e:0c:10:b9:c8:37:d2:1e:b1:77\n-----BEGIN CERTIFICATE-----\nMIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML\nRW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp\nbmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5\nIEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp\nZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3\nMjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3\nLmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp\nYWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG\nA1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq\nK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe\nsYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX\nMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT\nXTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/\nHoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH\n4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub\nj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo\nU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf\nzX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b\nu/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+\nbYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er\nfF6adulZkMV8gzURZVE=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust\n# Subject: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust\n# Label: \"Baltimore CyberTrust Root\"\n# Serial: 33554617\n# MD5 Fingerprint: ac:b6:94:a5:9c:17:e0:d7:91:52:9b:b1:97:06:a6:e4\n# SHA1 Fingerprint: d4:de:20:d0:5e:66:fc:53:fe:1a:50:88:2c:78:db:28:52:ca:e4:74\n# SHA256 Fingerprint: 16:af:57:a9:f6:76:b0:ab:12:60:95:aa:5e:ba:de:f2:2a:b3:11:19:d6:44:ac:95:cd:4b:93:db:f3:f2:6a:eb\n-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ\nRTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD\nVQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX\nDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y\nZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy\nVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr\nmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr\nIZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK\nmpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu\nXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy\ndc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye\njl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1\nBE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\nDQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92\n9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx\njkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0\nEpn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz\nksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS\nR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp\n-----END CERTIFICATE-----\n\n# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.\n# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.\n# Label: \"Entrust Root Certification Authority\"\n# Serial: 1164660820\n# MD5 Fingerprint: d6:a5:c3:ed:5d:dd:3e:00:c1:3d:87:92:1f:1d:3f:e4\n# SHA1 Fingerprint: b3:1e:b1:b7:40:e3:6c:84:02:da:dc:37:d4:4d:f5:d4:67:49:52:f9\n# SHA256 Fingerprint: 73:c1:76:43:4f:1b:c6:d5:ad:f4:5b:0e:76:e7:27:28:7c:8d:e5:76:16:c1:e6:e6:14:1a:2b:2c:bc:7d:8e:4c\n-----BEGIN CERTIFICATE-----\nMIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC\nVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0\nLm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW\nKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl\ncnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw\nNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw\nNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy\nZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV\nBAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo\nNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4\n4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9\nKlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI\nrb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi\n94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB\nsDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi\ngA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo\nkORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE\nvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA\nA4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t\nO1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua\nAGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP\n9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/\neu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m\n0vdXcDazv/wor3ElhVsT/h5/WrQ8\n-----END CERTIFICATE-----\n\n# Issuer: CN=AAA Certificate Services O=Comodo CA Limited\n# Subject: CN=AAA Certificate Services O=Comodo CA Limited\n# Label: \"Comodo AAA Services root\"\n# Serial: 1\n# MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0\n# SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49\n# SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4\n-----BEGIN CERTIFICATE-----\nMIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb\nMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow\nGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj\nYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL\nMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE\nBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM\nGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua\nBtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe\n3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4\nYgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR\nrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm\nez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU\noBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF\nMAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v\nQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t\nb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF\nAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q\nGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz\nRt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2\nG9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi\nl2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3\nsmPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==\n-----END CERTIFICATE-----\n\n# Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited\n# Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited\n# Label: \"QuoVadis Root CA 2\"\n# Serial: 1289\n# MD5 Fingerprint: 5e:39:7b:dd:f8:ba:ec:82:e9:ac:62:ba:0c:54:00:2b\n# SHA1 Fingerprint: ca:3a:fb:cf:12:40:36:4b:44:b2:16:20:88:80:48:39:19:93:7c:f7\n# SHA256 Fingerprint: 85:a0:dd:7d:d7:20:ad:b7:ff:05:f8:3d:54:2b:20:9d:c7:ff:45:28:f7:d6:77:b1:83:89:fe:a5:e5:c4:9e:86\n-----BEGIN CERTIFICATE-----\nMIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x\nGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv\nb3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV\nBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W\nYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa\nGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg\nFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J\nWpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB\nrrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp\n+ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1\nksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i\nUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz\nPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og\n/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH\noycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI\nyV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud\nEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2\nA8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL\nMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT\nElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f\nBluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn\ng/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl\nfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K\nWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha\nB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc\nhLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR\nTUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD\nmbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z\nohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y\n4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza\n8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u\n-----END CERTIFICATE-----\n\n# Issuer: CN=QuoVadis Root CA 3 O=QuoVadis Limited\n# Subject: CN=QuoVadis Root CA 3 O=QuoVadis Limited\n# Label: \"QuoVadis Root CA 3\"\n# Serial: 1478\n# MD5 Fingerprint: 31:85:3c:62:94:97:63:b9:aa:fd:89:4e:af:6f:e0:cf\n# SHA1 Fingerprint: 1f:49:14:f7:d8:74:95:1d:dd:ae:02:c0:be:fd:3a:2d:82:75:51:85\n# SHA256 Fingerprint: 18:f1:fc:7f:20:5d:f8:ad:dd:eb:7f:e0:07:dd:57:e3:af:37:5a:9c:4d:8d:73:54:6b:f4:f1:fe:d1:e1:8d:35\n-----BEGIN CERTIFICATE-----\nMIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x\nGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv\nb3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV\nBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W\nYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM\nV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB\n4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr\nH556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd\n8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv\nvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT\nmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe\nbtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc\nT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt\nWAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ\nc6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A\n4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD\nVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG\nCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0\naXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0\naWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu\ndC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw\nczALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G\nA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC\nTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg\nUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0\n7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem\nd1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd\n+LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B\n4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN\nt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x\nDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57\nk8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s\nzHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j\nWy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT\nmJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK\n4SVhM7JZG+Ju1zdXtg2pEto=\n-----END CERTIFICATE-----\n\n# Issuer: O=SECOM Trust.net OU=Security Communication RootCA1\n# Subject: O=SECOM Trust.net OU=Security Communication RootCA1\n# Label: \"Security Communication Root CA\"\n# Serial: 0\n# MD5 Fingerprint: f1:bc:63:6a:54:e0:b5:27:f5:cd:e7:1a:e3:4d:6e:4a\n# SHA1 Fingerprint: 36:b1:2b:49:f9:81:9e:d7:4c:9e:bc:38:0f:c6:56:8f:5d:ac:b2:f7\n# SHA256 Fingerprint: e7:5e:72:ed:9f:56:0e:ec:6e:b4:80:00:73:a4:3f:c3:ad:19:19:5a:39:22:82:01:78:95:97:4a:99:02:6b:6c\n-----BEGIN CERTIFICATE-----\nMIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY\nMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t\ndW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5\nWjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD\nVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8\n9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ\nDKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9\nMs+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N\nQV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ\nxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G\nA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T\nAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG\nkl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr\nUj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5\nBw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU\nJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot\nRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw==\n-----END CERTIFICATE-----\n\n# Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com\n# Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com\n# Label: \"XRamp Global CA Root\"\n# Serial: 107108908803651509692980124233745014957\n# MD5 Fingerprint: a1:0b:44:b3:ca:10:d8:00:6e:9d:0f:d8:0f:92:0a:d1\n# SHA1 Fingerprint: b8:01:86:d1:eb:9c:86:a5:41:04:cf:30:54:f3:4c:52:b7:e5:58:c6\n# SHA256 Fingerprint: ce:cd:dc:90:50:99:d8:da:df:c5:b1:d2:09:b7:37:cb:e2:c1:8c:fb:2c:10:c0:ff:0b:cf:0d:32:86:fc:1a:a2\n-----BEGIN CERTIFICATE-----\nMIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB\ngjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk\nMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY\nUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx\nNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3\ndy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy\ndmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB\ndXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6\n38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP\nKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q\nDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4\nqEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa\nJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi\nPvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P\nBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs\njVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0\neS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD\nggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR\nvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt\nqZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa\nIR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy\ni6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ\nO+7ETPTsJ3xCwnR8gooJybQDJbw=\n-----END CERTIFICATE-----\n\n# Issuer: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority\n# Subject: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority\n# Label: \"Go Daddy Class 2 CA\"\n# Serial: 0\n# MD5 Fingerprint: 91:de:06:25:ab:da:fd:32:17:0c:bb:25:17:2a:84:67\n# SHA1 Fingerprint: 27:96:ba:e6:3f:18:01:e2:77:26:1b:a0:d7:77:70:02:8f:20:ee:e4\n# SHA256 Fingerprint: c3:84:6b:f2:4b:9e:93:ca:64:27:4c:0e:c6:7c:1e:cc:5e:02:4f:fc:ac:d2:d7:40:19:35:0e:81:fe:54:6a:e4\n-----BEGIN CERTIFICATE-----\nMIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh\nMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE\nYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3\nMDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo\nZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg\nMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN\nADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA\nPVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w\nwdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi\nEqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY\navx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+\nYihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE\nsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h\n/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5\nIEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj\nYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD\nggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy\nOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P\nTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ\nHmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER\ndEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf\nReYNnyicsbkqWletNw+vHX/bvZ8=\n-----END CERTIFICATE-----\n\n# Issuer: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority\n# Subject: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority\n# Label: \"Starfield Class 2 CA\"\n# Serial: 0\n# MD5 Fingerprint: 32:4a:4b:bb:c8:63:69:9b:be:74:9a:c6:dd:1d:46:24\n# SHA1 Fingerprint: ad:7e:1c:28:b0:64:ef:8f:60:03:40:20:14:c3:d0:e3:37:0e:b5:8a\n# SHA256 Fingerprint: 14:65:fa:20:53:97:b8:76:fa:a6:f0:a9:95:8e:55:90:e4:0f:cc:7f:aa:4f:b7:c2:c8:67:75:21:fb:5f:b6:58\n-----BEGIN CERTIFICATE-----\nMIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl\nMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp\nU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw\nNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE\nChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp\nZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3\nDQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf\n8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN\n+lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0\nX9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa\nK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA\n1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G\nA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR\nzt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0\nYXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD\nbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w\nDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3\nL7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D\neruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl\nxy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp\nVSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY\nWQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=\n-----END CERTIFICATE-----\n\n# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com\n# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com\n# Label: \"DigiCert Assured ID Root CA\"\n# Serial: 17154717934120587862167794914071425081\n# MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72\n# SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43\n# SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c\n-----BEGIN CERTIFICATE-----\nMIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv\nb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG\nEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl\ncnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c\nJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP\nmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+\nwRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4\nVYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/\nAUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB\nAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\nBBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun\npyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC\ndWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf\nfwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm\nNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx\nH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe\n+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==\n-----END CERTIFICATE-----\n\n# Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com\n# Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com\n# Label: \"DigiCert Global Root CA\"\n# Serial: 10944719598952040374951832963794454346\n# MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e\n# SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36\n# SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61\n-----BEGIN CERTIFICATE-----\nMIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\nQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\nMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\nb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\nCSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\nnh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\nT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\ngdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\nBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\nTLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\nDQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\nhMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\n06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\nPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\nYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\nCAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n-----END CERTIFICATE-----\n\n# Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com\n# Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com\n# Label: \"DigiCert High Assurance EV Root CA\"\n# Serial: 3553400076410547919724730734378100087\n# MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a\n# SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25\n# SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf\n-----BEGIN CERTIFICATE-----\nMIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\nZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL\nMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\nLmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\nRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm\n+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW\nPNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\nxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB\nIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3\nhzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg\nEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF\nMAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA\nFLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec\nnzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z\neM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF\nhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\nYzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe\nvEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep\n+OkuE6N36B9K\n-----END CERTIFICATE-----\n\n# Issuer: CN=DST Root CA X3 O=Digital Signature Trust Co.\n# Subject: CN=DST Root CA X3 O=Digital Signature Trust Co.\n# Label: \"DST Root CA X3\"\n# Serial: 91299735575339953335919266965803778155\n# MD5 Fingerprint: 41:03:52:dc:0f:f7:50:1b:16:f0:02:8e:ba:6f:45:c5\n# SHA1 Fingerprint: da:c9:02:4f:54:d8:f6:df:94:93:5f:b1:73:26:38:ca:6a:d7:7c:13\n# SHA256 Fingerprint: 06:87:26:03:31:a7:24:03:d9:09:f1:05:e6:9b:cf:0d:32:e1:bd:24:93:ff:c6:d9:20:6d:11:bc:d6:77:07:39\n-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n\n# Issuer: CN=SwissSign Gold CA - G2 O=SwissSign AG\n# Subject: CN=SwissSign Gold CA - G2 O=SwissSign AG\n# Label: \"SwissSign Gold CA - G2\"\n# Serial: 13492815561806991280\n# MD5 Fingerprint: 24:77:d9:a8:91:d1:3b:fa:88:2d:c2:ff:f8:cd:33:93\n# SHA1 Fingerprint: d8:c5:38:8a:b7:30:1b:1b:6e:d4:7a:e6:45:25:3a:6f:9f:1a:27:61\n# SHA256 Fingerprint: 62:dd:0b:e9:b9:f5:0a:16:3e:a0:f8:e7:5c:05:3b:1e:ca:57:ea:55:c8:68:8f:64:7c:68:81:f2:c8:35:7b:95\n-----BEGIN CERTIFICATE-----\nMIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV\nBAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln\nbiBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF\nMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT\nd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC\nCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8\n76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+\nbbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c\n6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE\nemA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd\nMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt\nMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y\nMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y\nFGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi\naG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM\ngI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB\nqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7\nlqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn\n8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov\nL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6\n45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO\nUYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5\nO1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC\nbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv\nGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a\n77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC\nhdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3\n92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp\nLd6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w\nZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt\nQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ\n-----END CERTIFICATE-----\n\n# Issuer: CN=SwissSign Silver CA - G2 O=SwissSign AG\n# Subject: CN=SwissSign Silver CA - G2 O=SwissSign AG\n# Label: \"SwissSign Silver CA - G2\"\n# Serial: 5700383053117599563\n# MD5 Fingerprint: e0:06:a1:c9:7d:cf:c9:fc:0d:c0:56:75:96:d8:62:13\n# SHA1 Fingerprint: 9b:aa:e5:9f:56:ee:21:cb:43:5a:be:25:93:df:a7:f0:40:d1:1d:cb\n# SHA256 Fingerprint: be:6c:4d:a2:bb:b9:ba:59:b6:f3:93:97:68:37:42:46:c3:c0:05:99:3f:a9:8f:02:0d:1d:ed:be:d4:8a:81:d5\n-----BEGIN CERTIFICATE-----\nMIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE\nBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu\nIFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow\nRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY\nU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A\nMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv\nFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br\nYT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF\nnbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH\n6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt\neJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/\nc8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ\nMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH\nHTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf\njNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6\n5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB\nrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU\nF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c\nwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0\ncDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB\nAHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp\nWJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9\nxCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ\n2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ\nIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8\naRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X\nem1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR\ndAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/\nOMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+\nhAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy\ntGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u\n-----END CERTIFICATE-----\n\n# Issuer: CN=SecureTrust CA O=SecureTrust Corporation\n# Subject: CN=SecureTrust CA O=SecureTrust Corporation\n# Label: \"SecureTrust CA\"\n# Serial: 17199774589125277788362757014266862032\n# MD5 Fingerprint: dc:32:c3:a7:6d:25:57:c7:68:09:9d:ea:2d:a9:a2:d1\n# SHA1 Fingerprint: 87:82:c6:c3:04:35:3b:cf:d2:96:92:d2:59:3e:7d:44:d9:34:ff:11\n# SHA256 Fingerprint: f1:c1:b5:0a:e5:a2:0d:d8:03:0e:c9:f6:bc:24:82:3d:d3:67:b5:25:57:59:b4:e7:1b:61:fc:e9:f7:37:5d:73\n-----BEGIN CERTIFICATE-----\nMIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI\nMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x\nFzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz\nMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv\ncnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz\nZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO\n0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao\nwW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj\n7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS\n8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT\nBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB\n/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg\nJYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC\nNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3\n6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/\n3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm\nD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS\nCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR\n3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Secure Global CA O=SecureTrust Corporation\n# Subject: CN=Secure Global CA O=SecureTrust Corporation\n# Label: \"Secure Global CA\"\n# Serial: 9751836167731051554232119481456978597\n# MD5 Fingerprint: cf:f4:27:0d:d4:ed:dc:65:16:49:6d:3d:da:bf:6e:de\n# SHA1 Fingerprint: 3a:44:73:5a:e5:81:90:1f:24:86:61:46:1e:3b:9c:c4:5f:f5:3a:1b\n# SHA256 Fingerprint: 42:00:f5:04:3a:c8:59:0e:bb:52:7d:20:9e:d1:50:30:29:fb:cb:d4:1c:a1:b5:06:ec:27:f1:5a:de:7d:ac:69\n-----BEGIN CERTIFICATE-----\nMIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK\nMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x\nGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx\nMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg\nQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ\niQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa\n/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ\njnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI\nHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7\nsFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w\ngZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF\nMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw\nKaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG\nAQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L\nURYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO\nH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm\nI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY\niNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc\nf8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW\n-----END CERTIFICATE-----\n\n# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited\n# Subject: CN=COMODO Certification Authority O=COMODO CA Limited\n# Label: \"COMODO Certification Authority\"\n# Serial: 104350513648249232941998508985834464573\n# MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75\n# SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b\n# SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66\n-----BEGIN CERTIFICATE-----\nMIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB\ngTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV\nBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw\nMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl\nYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P\nRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0\naG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3\nUcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI\n2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8\nQ5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp\n+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+\nDT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O\nnKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW\n/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g\nPKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u\nQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY\nSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv\nIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/\nRxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4\nzJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd\nBA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB\nZQ==\n-----END CERTIFICATE-----\n\n# Issuer: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C.\n# Subject: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C.\n# Label: \"Network Solutions Certificate Authority\"\n# Serial: 116697915152937497490437556386812487904\n# MD5 Fingerprint: d3:f3:a6:16:c0:fa:6b:1d:59:b1:2d:96:4d:0e:11:2e\n# SHA1 Fingerprint: 74:f8:a3:c3:ef:e7:b3:90:06:4b:83:90:3c:21:64:60:20:e5:df:ce\n# SHA256 Fingerprint: 15:f0:ba:00:a3:ac:7a:f3:ac:88:4c:07:2b:10:11:a0:77:bd:77:c0:97:f4:01:64:b2:f8:59:8a:bd:83:86:0c\n-----BEGIN CERTIFICATE-----\nMIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi\nMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu\nMTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp\ndHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV\nUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO\nZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz\nc7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP\nOCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl\nmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF\nBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4\nqY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw\ngZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB\nBjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu\nbmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp\ndHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8\n6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/\nh1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH\n/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv\nwKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN\npGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey\n-----END CERTIFICATE-----\n\n# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited\n# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited\n# Label: \"COMODO ECC Certification Authority\"\n# Serial: 41578283867086692638256921589707938090\n# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23\n# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11\n# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7\n-----BEGIN CERTIFICATE-----\nMIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL\nMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE\nBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT\nIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw\nMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy\nZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N\nT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv\nbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR\nFtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J\ncfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW\nBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/\nBAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm\nfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv\nGDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Certigna O=Dhimyotis\n# Subject: CN=Certigna O=Dhimyotis\n# Label: \"Certigna\"\n# Serial: 18364802974209362175\n# MD5 Fingerprint: ab:57:a6:5b:7d:42:82:19:b5:d8:58:26:28:5e:fd:ff\n# SHA1 Fingerprint: b1:2e:13:63:45:86:a4:6f:1a:b2:60:68:37:58:2d:c4:ac:fd:94:97\n# SHA256 Fingerprint: e3:b6:a2:db:2e:d7:ce:48:84:2f:7a:c5:32:41:c7:b7:1d:54:14:4b:fb:40:c1:1f:3f:1d:0b:42:f5:ee:a1:2d\n-----BEGIN CERTIFICATE-----\nMIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV\nBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X\nDTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ\nBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4\nQCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny\ngQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw\nzBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q\n130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2\nJsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw\nDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw\nZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT\nAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj\nAQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG\n9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h\nbV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc\nfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu\nHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w\nt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw\nWyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==\n-----END CERTIFICATE-----\n\n# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc\n# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc\n# Label: \"Cybertrust Global Root\"\n# Serial: 4835703278459682877484360\n# MD5 Fingerprint: 72:e4:4a:87:e3:69:40:80:77:ea:bc:e3:f4:ff:f0:e1\n# SHA1 Fingerprint: 5f:43:e5:b1:bf:f8:78:8c:ac:1c:c7:ca:4a:9a:c6:22:2b:cc:34:c6\n# SHA256 Fingerprint: 96:0a:df:00:63:e9:63:56:75:0c:29:65:dd:0a:08:67:da:0b:9c:bd:6e:77:71:4a:ea:fb:23:49:ab:39:3d:a3\n-----BEGIN CERTIFICATE-----\nMIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG\nA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh\nbCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE\nChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS\nb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5\n7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS\nJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y\nHLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP\nt3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz\nFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY\nXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/\nMB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw\nhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js\nMB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA\nA4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj\nWqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx\nXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o\nomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc\nA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW\nWL1WMRJOEcgh4LMRkWXbtKaIOM5V\n-----END CERTIFICATE-----\n\n# Issuer: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority\n# Subject: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority\n# Label: \"ePKI Root Certification Authority\"\n# Serial: 28956088682735189655030529057352760477\n# MD5 Fingerprint: 1b:2e:00:ca:26:06:90:3d:ad:fe:6f:15:68:d3:6b:b3\n# SHA1 Fingerprint: 67:65:0d:f1:7e:8e:7e:5b:82:40:a4:f4:56:4b:cf:e2:3d:69:c6:f0\n# SHA256 Fingerprint: c0:a6:f4:dc:63:a2:4b:fd:cf:54:ef:2a:6a:08:2a:0a:72:de:35:80:3e:2f:f5:ff:52:7a:e5:d8:72:06:df:d5\n-----BEGIN CERTIFICATE-----\nMIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe\nMQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0\nZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe\nFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw\nIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL\nSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH\nSyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh\nijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X\nDZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1\nTBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ\nfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA\nsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU\nWH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS\nnT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH\ndmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip\nNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC\nAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF\nMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH\nClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB\nuvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl\nPwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP\nJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/\ngpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2\nj6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6\n5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB\no2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS\n/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z\nGp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE\nW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D\nhNQ+IIX3Sj0rnP0qCglN6oH4EZw=\n-----END CERTIFICATE-----\n\n# Issuer: O=certSIGN OU=certSIGN ROOT CA\n# Subject: O=certSIGN OU=certSIGN ROOT CA\n# Label: \"certSIGN ROOT CA\"\n# Serial: 35210227249154\n# MD5 Fingerprint: 18:98:c0:d6:e9:3a:fc:f9:b0:f5:0c:f7:4b:01:44:17\n# SHA1 Fingerprint: fa:b7:ee:36:97:26:62:fb:2d:b0:2a:f6:bf:03:fd:e8:7c:4b:2f:9b\n# SHA256 Fingerprint: ea:a9:62:c4:fa:4a:6b:af:eb:e4:15:19:6d:35:1c:cd:88:8d:4f:53:f3:fa:8a:e6:d7:c4:66:a9:4e:60:42:bb\n-----BEGIN CERTIFICATE-----\nMIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT\nAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD\nQTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP\nMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do\n0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ\nUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d\nRdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ\nOA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv\nJoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C\nAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O\nBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ\nLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY\nMnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ\n44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I\nJd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw\ni/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN\n9u6wWk5JRFRYX0KD\n-----END CERTIFICATE-----\n\n# Issuer: CN=NetLock Arany (Class Gold) F\\u0151tan\\xfas\\xedtv\\xe1ny O=NetLock Kft. OU=Tan\\xfas\\xedtv\\xe1nykiad\\xf3k (Certification Services)\n# Subject: CN=NetLock Arany (Class Gold) F\\u0151tan\\xfas\\xedtv\\xe1ny O=NetLock Kft. OU=Tan\\xfas\\xedtv\\xe1nykiad\\xf3k (Certification Services)\n# Label: \"NetLock Arany (Class Gold) F\\u0151tan\\xfas\\xedtv\\xe1ny\"\n# Serial: 80544274841616\n# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88\n# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91\n# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98\n-----BEGIN CERTIFICATE-----\nMIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG\nEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3\nMDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl\ncnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR\ndGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB\npzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM\nb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm\naWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz\nIEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT\nlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz\nAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5\nVA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG\nILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2\nBJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG\nAQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M\nU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh\nbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C\n+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC\nbLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F\nuLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2\nXjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Hongkong Post Root CA 1 O=Hongkong Post\n# Subject: CN=Hongkong Post Root CA 1 O=Hongkong Post\n# Label: \"Hongkong Post Root CA 1\"\n# Serial: 1000\n# MD5 Fingerprint: a8:0d:6f:39:78:b9:43:6d:77:42:6d:98:5a:cc:23:ca\n# SHA1 Fingerprint: d6:da:a8:20:8d:09:d2:15:4d:24:b5:2f:cb:34:6e:b2:58:b2:8a:58\n# SHA256 Fingerprint: f9:e6:7d:33:6c:51:00:2a:c0:54:c6:32:02:2d:66:dd:a2:e7:e3:ff:f1:0a:d0:61:ed:31:d8:bb:b4:10:cf:b2\n-----BEGIN CERTIFICATE-----\nMIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx\nFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg\nUm9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG\nA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr\nb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ\njVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn\nPzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh\nZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9\nnnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h\nq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED\nMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC\nmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3\n7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB\noiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs\nEhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO\nfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi\nAmvZWg==\n-----END CERTIFICATE-----\n\n# Issuer: CN=SecureSign RootCA11 O=Japan Certification Services, Inc.\n# Subject: CN=SecureSign RootCA11 O=Japan Certification Services, Inc.\n# Label: \"SecureSign RootCA11\"\n# Serial: 1\n# MD5 Fingerprint: b7:52:74:e2:92:b4:80:93:f2:75:e4:cc:d7:f2:ea:26\n# SHA1 Fingerprint: 3b:c4:9f:48:f8:f3:73:a0:9c:1e:bd:f8:5b:b1:c3:65:c7:d8:11:b3\n# SHA256 Fingerprint: bf:0f:ee:fb:9e:3a:58:1a:d5:f9:e9:db:75:89:98:57:43:d2:61:08:5c:4d:31:4f:6f:5d:72:59:aa:42:16:12\n-----BEGIN CERTIFICATE-----\nMIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr\nMCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG\nA1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0\nMDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp\nY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD\nQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz\ni1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8\nh9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV\nMdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9\nUK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni\n8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC\nh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD\nVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB\nAKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm\nKbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ\nX5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr\nQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5\npPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN\nQSdJQO7e5iNEOdyhIta6A/I=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd.\n# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd.\n# Label: \"Microsec e-Szigno Root CA 2009\"\n# Serial: 14014712776195784473\n# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1\n# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e\n# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78\n-----BEGIN CERTIFICATE-----\nMIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD\nVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0\nZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G\nCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y\nOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx\nFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp\nZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o\ndTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP\nkd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc\ncbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U\nfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7\nN4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC\nxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1\n+rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\nA1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM\nPcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG\nSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h\nmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk\nddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775\ntyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c\n2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t\nHMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW\n-----END CERTIFICATE-----\n\n# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3\n# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3\n# Label: \"GlobalSign Root CA - R3\"\n# Serial: 4835703278459759426209954\n# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28\n# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad\n# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b\n-----BEGIN CERTIFICATE-----\nMIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G\nA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp\nZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4\nMTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG\nA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8\nRgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT\ngHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm\nKPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd\nQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ\nXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw\nDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o\nLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU\nRUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp\njjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK\n6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX\nmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs\nMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH\nWD9f\n-----END CERTIFICATE-----\n\n# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068\n# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068\n# Label: \"Autoridad de Certificacion Firmaprofesional CIF A62634068\"\n# Serial: 6047274297262753887\n# MD5 Fingerprint: 73:3a:74:7a:ec:bb:a3:96:a6:c2:e4:e2:c8:9b:c0:c3\n# SHA1 Fingerprint: ae:c5:fb:3f:c8:e1:bf:c4:e5:4f:03:07:5a:9a:e8:00:b7:f7:b6:fa\n# SHA256 Fingerprint: 04:04:80:28:bf:1f:28:64:d4:8f:9a:d4:d8:32:94:36:6a:82:88:56:55:3f:3b:14:30:3f:90:14:7f:5d:40:ef\n-----BEGIN CERTIFICATE-----\nMIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE\nBhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h\ncHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy\nMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg\nQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9\nthDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM\ncas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG\nL9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i\nNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h\nX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b\nm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy\nZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja\nEbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T\nKI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF\n6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh\nOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD\nVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD\nVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp\ncm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv\nACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl\nAGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF\n661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9\nam58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1\nILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481\nPyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS\n3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k\nSeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF\n3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM\nZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g\nStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz\nQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB\njLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V\n-----END CERTIFICATE-----\n\n# Issuer: CN=Izenpe.com O=IZENPE S.A.\n# Subject: CN=Izenpe.com O=IZENPE S.A.\n# Label: \"Izenpe.com\"\n# Serial: 917563065490389241595536686991402621\n# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73\n# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19\n# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f\n-----BEGIN CERTIFICATE-----\nMIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4\nMQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6\nZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD\nVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j\nb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq\nscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO\nxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H\nLmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX\nuaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD\nyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+\nJrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q\nrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN\nBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L\nhij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB\nQFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+\nHMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu\nZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg\nQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB\nBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx\nMCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA\nA4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb\nlaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56\nawmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo\nJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw\nLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT\nVyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk\nLhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb\nUjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/\nQnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+\nnaM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls\nQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw==\n-----END CERTIFICATE-----\n\n# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc.\n# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc.\n# Label: \"Go Daddy Root Certificate Authority - G2\"\n# Serial: 0\n# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01\n# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b\n# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da\n-----BEGIN CERTIFICATE-----\nMIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT\nEUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp\nZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz\nNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH\nEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE\nAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD\nE6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH\n/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy\nDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh\nGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR\ntDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA\nAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE\nFDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX\nWWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu\n9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr\ngIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo\n2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO\nLPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI\n4uJEvlz36hz1\n-----END CERTIFICATE-----\n\n# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc.\n# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc.\n# Label: \"Starfield Root Certificate Authority - G2\"\n# Serial: 0\n# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96\n# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e\n# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5\n-----BEGIN CERTIFICATE-----\nMIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\nHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs\nZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw\nMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6\nb25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj\naG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp\nY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg\nnLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1\nHOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N\nHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN\ndloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0\nHZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO\nBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G\nCSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU\nsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3\n4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg\n8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K\npL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1\nmMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0\n-----END CERTIFICATE-----\n\n# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc.\n# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc.\n# Label: \"Starfield Services Root Certificate Authority - G2\"\n# Serial: 0\n# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2\n# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f\n# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5\n-----BEGIN CERTIFICATE-----\nMIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\nHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs\nZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5\nMDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD\nVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy\nZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy\ndmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p\nOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2\n8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K\nTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe\nhRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk\n6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw\nDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q\nAdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI\nbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB\nve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z\nqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd\niEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn\n0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN\nsSi6\n-----END CERTIFICATE-----\n\n# Issuer: CN=AffirmTrust Commercial O=AffirmTrust\n# Subject: CN=AffirmTrust Commercial O=AffirmTrust\n# Label: \"AffirmTrust Commercial\"\n# Serial: 8608355977964138876\n# MD5 Fingerprint: 82:92:ba:5b:ef:cd:8a:6f:a6:3d:55:f9:84:f6:d6:b7\n# SHA1 Fingerprint: f9:b5:b6:32:45:5f:9c:be:ec:57:5f:80:dc:e9:6e:2c:c7:b2:78:b7\n# SHA256 Fingerprint: 03:76:ab:1d:54:c5:f9:80:3c:e4:b2:e2:01:a0:ee:7e:ef:7b:57:b6:36:e8:a9:3c:9b:8d:48:60:c9:6f:5f:a7\n-----BEGIN CERTIFICATE-----\nMIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE\nBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz\ndCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL\nMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp\ncm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP\nHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr\nba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL\nMeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1\nyHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr\nVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/\nnx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ\nKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG\nXUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj\nvbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt\nZ8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g\nN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC\nnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8=\n-----END CERTIFICATE-----\n\n# Issuer: CN=AffirmTrust Networking O=AffirmTrust\n# Subject: CN=AffirmTrust Networking O=AffirmTrust\n# Label: \"AffirmTrust Networking\"\n# Serial: 8957382827206547757\n# MD5 Fingerprint: 42:65:ca:be:01:9a:9a:4c:a9:8c:41:49:cd:c0:d5:7f\n# SHA1 Fingerprint: 29:36:21:02:8b:20:ed:02:f5:66:c5:32:d1:d6:ed:90:9f:45:00:2f\n# SHA256 Fingerprint: 0a:81:ec:5a:92:97:77:f1:45:90:4a:f3:8d:5d:50:9f:66:b5:e2:c5:8f:cd:b5:31:05:8b:0e:17:f3:f0:b4:1b\n-----BEGIN CERTIFICATE-----\nMIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE\nBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz\ndCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL\nMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp\ncm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y\nYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua\nkCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL\nQESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp\n6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG\nyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i\nQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ\nKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO\ntDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu\nQY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ\nLgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u\nolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48\nx3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s=\n-----END CERTIFICATE-----\n\n# Issuer: CN=AffirmTrust Premium O=AffirmTrust\n# Subject: CN=AffirmTrust Premium O=AffirmTrust\n# Label: \"AffirmTrust Premium\"\n# Serial: 7893706540734352110\n# MD5 Fingerprint: c4:5d:0e:48:b6:ac:28:30:4e:0a:bc:f9:38:16:87:57\n# SHA1 Fingerprint: d8:a6:33:2c:e0:03:6f:b1:85:f6:63:4f:7d:6a:06:65:26:32:28:27\n# SHA256 Fingerprint: 70:a7:3f:7f:37:6b:60:07:42:48:90:45:34:b1:14:82:d5:bf:0e:69:8e:cc:49:8d:f5:25:77:eb:f2:e9:3b:9a\n-----BEGIN CERTIFICATE-----\nMIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE\nBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz\ndCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG\nA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U\ncnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf\nqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ\nJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ\n+jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS\ns8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5\nHMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7\n70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG\nV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S\nqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S\n5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia\nC1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX\nOwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE\nFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/\nBAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2\nKI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg\nNt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B\n8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ\nMKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc\n0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ\nu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF\nu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH\nYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8\nGKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO\nRtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e\nKeC2uAloGRwYQw==\n-----END CERTIFICATE-----\n\n# Issuer: CN=AffirmTrust Premium ECC O=AffirmTrust\n# Subject: CN=AffirmTrust Premium ECC O=AffirmTrust\n# Label: \"AffirmTrust Premium ECC\"\n# Serial: 8401224907861490260\n# MD5 Fingerprint: 64:b0:09:55:cf:b1:d5:99:e2:be:13:ab:a6:5d:ea:4d\n# SHA1 Fingerprint: b8:23:6b:00:2f:1d:16:86:53:01:55:6c:11:a4:37:ca:eb:ff:c3:bb\n# SHA256 Fingerprint: bd:71:fd:f6:da:97:e4:cf:62:d1:64:7a:dd:25:81:b0:7d:79:ad:f8:39:7e:b4:ec:ba:9c:5e:84:88:82:14:23\n-----BEGIN CERTIFICATE-----\nMIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC\nVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ\ncmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ\nBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt\nVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D\n0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9\nss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G\nA1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G\nA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs\naobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I\nflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ==\n-----END CERTIFICATE-----\n\n# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority\n# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority\n# Label: \"Certum Trusted Network CA\"\n# Serial: 279744\n# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78\n# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e\n# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e\n-----BEGIN CERTIFICATE-----\nMIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM\nMSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D\nZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU\ncnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3\nWjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg\nUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw\nIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH\nUV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM\nTXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU\nBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM\nkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x\nAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV\nHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y\nsHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL\nI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8\nJ9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY\nVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI\n03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw=\n-----END CERTIFICATE-----\n\n# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA\n# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA\n# Label: \"TWCA Root Certification Authority\"\n# Serial: 1\n# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79\n# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48\n# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44\n-----BEGIN CERTIFICATE-----\nMIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES\nMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU\nV0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz\nWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO\nLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm\naWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB\nAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE\nAcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH\nK3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX\nRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z\nrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx\n3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq\nhkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC\nMErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls\nXebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D\nlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn\naspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ\nYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw==\n-----END CERTIFICATE-----\n\n# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2\n# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2\n# Label: \"Security Communication RootCA2\"\n# Serial: 0\n# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43\n# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74\n# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6\n-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl\nMCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe\nU2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX\nDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy\ndXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj\nYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV\nOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr\nzbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM\nVAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ\nhNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO\nojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw\nawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs\nOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3\nDQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF\ncoJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc\nokgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8\nt/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy\n1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/\nSjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03\n-----END CERTIFICATE-----\n\n# Issuer: CN=EC-ACC O=Agencia Catalana de Certificacio (NIF Q-0801176-I) OU=Serveis Publics de Certificacio/Vegeu https://www.catcert.net/verarrel (c)03/Jerarquia Entitats de Certificacio Catalanes\n# Subject: CN=EC-ACC O=Agencia Catalana de Certificacio (NIF Q-0801176-I) OU=Serveis Publics de Certificacio/Vegeu https://www.catcert.net/verarrel (c)03/Jerarquia Entitats de Certificacio Catalanes\n# Label: \"EC-ACC\"\n# Serial: -23701579247955709139626555126524820479\n# MD5 Fingerprint: eb:f5:9d:29:0d:61:f9:42:1f:7c:c2:ba:6d:e3:15:09\n# SHA1 Fingerprint: 28:90:3a:63:5b:52:80:fa:e6:77:4c:0b:6d:a7:d6:ba:a6:4a:f2:e8\n# SHA256 Fingerprint: 88:49:7f:01:60:2f:31:54:24:6a:e2:8c:4d:5a:ef:10:f1:d8:7e:bb:76:62:6f:4a:e0:b7:f9:5b:a7:96:87:99\n-----BEGIN CERTIFICATE-----\nMIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB\n8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy\ndGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1\nYmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3\ndy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh\nIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD\nLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG\nEwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g\nKE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD\nZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu\nbmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg\nZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R\n85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm\n4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV\nHMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd\nQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t\nlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB\no4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E\nBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4\nopvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo\ndHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW\nZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN\nAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y\n/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k\nSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy\nRp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS\nAgu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl\nnJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority\n# Subject: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority\n# Label: \"Hellenic Academic and Research Institutions RootCA 2011\"\n# Serial: 0\n# MD5 Fingerprint: 73:9f:4c:4b:73:5b:79:e9:fa:ba:1c:ef:6e:cb:d5:c9\n# SHA1 Fingerprint: fe:45:65:9b:79:03:5b:98:a1:61:b5:51:2e:ac:da:58:09:48:22:4d\n# SHA256 Fingerprint: bc:10:4f:15:a4:8b:e7:09:dc:a5:42:a7:e1:d4:b9:df:6f:05:45:27:e8:02:ea:a9:2d:59:54:44:25:8a:fe:71\n-----BEGIN CERTIFICATE-----\nMIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix\nRDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1\ndGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p\nYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw\nNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK\nEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl\ncnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl\nc2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz\ndYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ\nfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns\nbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD\n75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP\nFEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV\nHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp\n5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu\nb3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA\nA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p\n6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8\nTqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7\ndIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys\nNnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI\nl7WdmplNsDz4SgCbZN2fOUvRJ9e4\n-----END CERTIFICATE-----\n\n# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967\n# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967\n# Label: \"Actalis Authentication Root CA\"\n# Serial: 6271844772424770508\n# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6\n# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac\n# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66\n-----BEGIN CERTIFICATE-----\nMIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE\nBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w\nMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290\nIENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC\nSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1\nODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv\nUTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX\n4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9\nKK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/\ngCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb\nrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ\n51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F\nbe8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe\nKF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F\nv6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn\nfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7\njPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz\nezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt\nifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL\ne3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70\njsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz\nWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V\nSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j\npwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX\nX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok\nfcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R\nK4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU\nZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU\nLysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT\nLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==\n-----END CERTIFICATE-----\n\n# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327\n# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327\n# Label: \"Buypass Class 2 Root CA\"\n# Serial: 2\n# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29\n# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99\n# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48\n-----BEGIN CERTIFICATE-----\nMIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd\nMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg\nQ2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow\nTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw\nHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB\nBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr\n6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV\nL4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91\n1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx\nMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ\nQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB\narcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr\nUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi\nFRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS\nP/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN\n9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP\nAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz\nuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h\n9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s\nA20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t\nOluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo\n+fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7\nKcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2\nDISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us\nH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ\nI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7\n5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h\n3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz\nY11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327\n# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327\n# Label: \"Buypass Class 3 Root CA\"\n# Serial: 2\n# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec\n# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57\n# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d\n-----BEGIN CERTIFICATE-----\nMIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd\nMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg\nQ2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow\nTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw\nHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB\nBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y\nZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E\nN3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9\ntznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX\n0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c\n/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X\nKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY\nzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS\nO1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D\n34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP\nK9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3\nAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv\nTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj\nQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV\ncSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS\nIGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2\nHJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa\nO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv\n033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u\ndmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE\nkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41\n3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD\nu79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq\n4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc=\n-----END CERTIFICATE-----\n\n# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center\n# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center\n# Label: \"T-TeleSec GlobalRoot Class 3\"\n# Serial: 1\n# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef\n# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1\n# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd\n-----BEGIN CERTIFICATE-----\nMIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx\nKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd\nBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl\nYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1\nOTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy\naXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50\nZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN\n8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/\nRLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4\nhqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5\nZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM\nEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1\nA/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy\nWL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ\n1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30\n6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT\n91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml\ne9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p\nTpPDpFQUWw==\n-----END CERTIFICATE-----\n\n# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH\n# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH\n# Label: \"D-TRUST Root Class 3 CA 2 2009\"\n# Serial: 623603\n# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f\n# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0\n# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1\n-----BEGIN CERTIFICATE-----\nMIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF\nMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD\nbGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha\nME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM\nHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03\nUAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42\ntSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R\nySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM\nlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp\n/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G\nA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G\nA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj\ndG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy\nMENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl\ncmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js\nL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL\nBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni\nacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0\no3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K\nzCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8\nPIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y\nJohw1+qRzT65ysCQblrGXnRl11z+o+I=\n-----END CERTIFICATE-----\n\n# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH\n# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH\n# Label: \"D-TRUST Root Class 3 CA 2 EV 2009\"\n# Serial: 623604\n# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6\n# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83\n# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81\n-----BEGIN CERTIFICATE-----\nMIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF\nMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD\nbGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw\nNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV\nBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn\nljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0\n3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z\nqQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR\np75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8\nHgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw\nggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea\nHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw\nOi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh\nc3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E\nRT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt\ndHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku\nY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp\n3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05\nnsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF\nCSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na\nxpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX\nKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1\n-----END CERTIFICATE-----\n\n# Issuer: CN=CA Disig Root R2 O=Disig a.s.\n# Subject: CN=CA Disig Root R2 O=Disig a.s.\n# Label: \"CA Disig Root R2\"\n# Serial: 10572350602393338211\n# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03\n# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71\n# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03\n-----BEGIN CERTIFICATE-----\nMIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV\nBAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu\nMRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy\nMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx\nEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw\nggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe\nNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH\nPWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I\nx2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe\nQTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR\nyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO\nQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912\nH9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ\nQfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD\ni/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs\nnLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1\nrqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud\nDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI\nhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM\ntCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf\nGopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb\nlvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka\n+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal\nTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i\nnSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3\ngzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr\nG5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os\nzMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x\nL4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL\n-----END CERTIFICATE-----\n\n# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV\n# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV\n# Label: \"ACCVRAIZ1\"\n# Serial: 6828503384748696800\n# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02\n# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17\n# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13\n-----BEGIN CERTIFICATE-----\nMIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE\nAwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw\nCQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ\nBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND\nVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb\nqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY\nHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo\nG2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA\nlHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr\nIA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/\n0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH\nk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47\n4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO\nm3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa\ncXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl\nuUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI\nKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls\nZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG\nAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2\nVuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT\nVfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG\nCCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA\ncgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA\nQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA\n7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA\ncgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA\nQwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA\nczAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu\naHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt\naW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud\nDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF\nBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp\nD70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU\nJyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m\nAM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD\nvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms\ntn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH\n7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h\nI6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA\nh1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF\nd3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H\npPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7\n-----END CERTIFICATE-----\n\n# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA\n# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA\n# Label: \"TWCA Global Root CA\"\n# Serial: 3262\n# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96\n# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65\n# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b\n-----BEGIN CERTIFICATE-----\nMIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx\nEjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT\nVFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5\nNTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT\nB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF\n10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz\n0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh\nMBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH\nzIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc\n46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2\nyKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi\nlaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP\noA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA\nBDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE\nqYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm\n4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB\n/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL\n1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn\nLhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF\nH6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo\nRI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+\nnile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh\n15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW\n6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW\nnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j\nwa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz\naGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy\nKwbQBM0=\n-----END CERTIFICATE-----\n\n# Issuer: CN=TeliaSonera Root CA v1 O=TeliaSonera\n# Subject: CN=TeliaSonera Root CA v1 O=TeliaSonera\n# Label: \"TeliaSonera Root CA v1\"\n# Serial: 199041966741090107964904287217786801558\n# MD5 Fingerprint: 37:41:49:1b:18:56:9a:26:f5:ad:c2:66:fb:40:a5:4c\n# SHA1 Fingerprint: 43:13:bb:96:f1:d5:86:9b:c1:4e:6a:92:f6:cf:f6:34:69:87:82:37\n# SHA256 Fingerprint: dd:69:36:fe:21:f8:f0:77:c1:23:a1:a5:21:c1:22:24:f7:22:55:b7:3e:03:a7:26:06:93:e8:a2:4b:0f:a3:89\n-----BEGIN CERTIFICATE-----\nMIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw\nNzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv\nb3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD\nVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2\nMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F\nVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1\n7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X\nZ75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+\n/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs\n81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm\ndtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe\nOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu\nsDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4\npgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs\nslESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ\narMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD\nVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG\n9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl\ndxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx\n0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj\nTQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed\nY2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7\nQ4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI\nOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7\nvVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW\nt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn\nHL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx\nSK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY=\n-----END CERTIFICATE-----\n\n# Issuer: CN=E-Tugra Certification Authority O=E-Tu\\u011fra EBG Bili\\u015fim Teknolojileri ve Hizmetleri A.\\u015e. OU=E-Tugra Sertifikasyon Merkezi\n# Subject: CN=E-Tugra Certification Authority O=E-Tu\\u011fra EBG Bili\\u015fim Teknolojileri ve Hizmetleri A.\\u015e. OU=E-Tugra Sertifikasyon Merkezi\n# Label: \"E-Tugra Certification Authority\"\n# Serial: 7667447206703254355\n# MD5 Fingerprint: b8:a1:03:63:b0:bd:21:71:70:8a:6f:13:3a:bb:79:49\n# SHA1 Fingerprint: 51:c6:e7:08:49:06:6e:f3:92:d4:5c:a0:0d:6d:a3:62:8f:c3:52:39\n# SHA256 Fingerprint: b0:bf:d5:2b:b0:d7:d9:bd:92:bf:5d:4d:c1:3d:a2:55:c0:2c:54:2f:37:83:65:ea:89:39:11:f5:5e:55:f2:3c\n-----BEGIN CERTIFICATE-----\nMIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV\nBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC\naWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV\nBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1\nZ3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz\nMDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+\nBgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp\nem1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN\nZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY\nB4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH\nD5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF\nQ9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo\nq1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D\nk14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH\nfC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut\ndEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM\nti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8\nzLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn\nrFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX\nU8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6\nJyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5\nXPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF\nNzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR\nHTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY\nGwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c\n77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3\n+GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK\nvJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6\nFiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl\nyb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P\nAJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD\ny4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d\nNL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA==\n-----END CERTIFICATE-----\n\n# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center\n# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center\n# Label: \"T-TeleSec GlobalRoot Class 2\"\n# Serial: 1\n# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a\n# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9\n# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52\n-----BEGIN CERTIFICATE-----\nMIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx\nKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd\nBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl\nYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1\nOTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy\naXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50\nZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd\nAqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC\nFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi\n1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq\njnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ\nwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/\nWSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy\nNsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC\nuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw\nIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6\ng1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN\n9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP\nBSeOE6Fuwg==\n-----END CERTIFICATE-----\n\n# Issuer: CN=Atos TrustedRoot 2011 O=Atos\n# Subject: CN=Atos TrustedRoot 2011 O=Atos\n# Label: \"Atos TrustedRoot 2011\"\n# Serial: 6643877497813316402\n# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56\n# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21\n# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74\n-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE\nAwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG\nEwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM\nFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC\nREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp\nNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM\nVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+\nSZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ\n4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L\ncp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi\neowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV\nHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG\nA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3\nDQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j\nvZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP\nDpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc\nmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D\nlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv\nKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed\n-----END CERTIFICATE-----\n\n# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited\n# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited\n# Label: \"QuoVadis Root CA 1 G3\"\n# Serial: 687049649626669250736271037606554624078720034195\n# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab\n# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67\n# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74\n-----BEGIN CERTIFICATE-----\nMIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL\nBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc\nBgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00\nMjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM\naW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV\nwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe\nrNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341\n68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh\n4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp\nUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o\nabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc\n3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G\nKubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt\nhfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO\nTk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt\nzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD\nggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC\nMTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2\ncDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN\nqXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5\nYCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv\nb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2\n8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k\nNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj\nZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp\nq1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt\nnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD\n-----END CERTIFICATE-----\n\n# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited\n# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited\n# Label: \"QuoVadis Root CA 2 G3\"\n# Serial: 390156079458959257446133169266079962026824725800\n# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06\n# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36\n# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40\n-----BEGIN CERTIFICATE-----\nMIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL\nBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc\nBgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00\nMjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM\naW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf\nqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW\nn4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym\nc5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+\nO7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1\no9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j\nIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq\nIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz\n8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh\nvNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l\n7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG\ncC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD\nggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66\nAarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC\nroijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga\nW/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n\nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE\n+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV\ncsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd\ndbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg\nKCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM\nHVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4\nWSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M\n-----END CERTIFICATE-----\n\n# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited\n# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited\n# Label: \"QuoVadis Root CA 3 G3\"\n# Serial: 268090761170461462463995952157327242137089239581\n# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7\n# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d\n# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46\n-----BEGIN CERTIFICATE-----\nMIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL\nBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc\nBgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00\nMjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM\naW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR\n/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu\nFoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR\nU7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c\nra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR\nFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k\nA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw\neyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl\nsSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp\nVzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q\nA4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+\nydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD\nggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px\nKGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI\nFUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv\noxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg\nu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP\n0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf\n3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl\n8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+\nDhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN\nPlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/\nywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0\n-----END CERTIFICATE-----\n\n# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com\n# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com\n# Label: \"DigiCert Assured ID Root G2\"\n# Serial: 15385348160840213938643033620894905419\n# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d\n# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f\n# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85\n-----BEGIN CERTIFICATE-----\nMIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv\nb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG\nEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl\ncnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA\nn61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc\nbiJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp\nEgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA\nbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu\nYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB\nAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW\nBBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI\nQW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I\n0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni\nlmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9\nB5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv\nON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo\nIhNzbM8m9Yop5w==\n-----END CERTIFICATE-----\n\n# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com\n# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com\n# Label: \"DigiCert Assured ID Root G3\"\n# Serial: 15459312981008553731928384953135426796\n# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb\n# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89\n# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2\n-----BEGIN CERTIFICATE-----\nMIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw\nCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu\nZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg\nRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV\nUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\nY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq\nhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf\nZn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q\nRSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/\nBAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD\nAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY\nJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv\n6pZjamVFkpUBtA==\n-----END CERTIFICATE-----\n\n# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com\n# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com\n# Label: \"DigiCert Global Root G2\"\n# Serial: 4293743540046975378534879503202253541\n# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44\n# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4\n# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f\n-----BEGIN CERTIFICATE-----\nMIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\nMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\nMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\nb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\nq2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\ntCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\nvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\nBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\nNeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\nFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\npLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\nMrY=\n-----END CERTIFICATE-----\n\n# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com\n# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com\n# Label: \"DigiCert Global Root G3\"\n# Serial: 7089244469030293291760083333884364146\n# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca\n# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e\n# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0\n-----BEGIN CERTIFICATE-----\nMIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw\nCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu\nZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe\nFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw\nEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x\nIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF\nK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG\nfp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO\nZ9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd\nBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx\nAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/\noAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8\nsycX\n-----END CERTIFICATE-----\n\n# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com\n# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com\n# Label: \"DigiCert Trusted Root G4\"\n# Serial: 7451500558977370777930084869016614236\n# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49\n# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4\n# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88\n-----BEGIN CERTIFICATE-----\nMIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg\nRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV\nUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\nY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y\nithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If\nxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV\nySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO\nDCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ\njdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/\nCNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi\nEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM\nfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY\nuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK\nchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t\n9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD\nggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2\nSV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd\n+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc\nfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa\nsjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N\ncCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N\n0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie\n4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI\nr/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1\n/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm\ngKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+\n-----END CERTIFICATE-----\n\n# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited\n# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited\n# Label: \"COMODO RSA Certification Authority\"\n# Serial: 101909084537582093308941363524873193117\n# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18\n# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4\n# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34\n-----BEGIN CERTIFICATE-----\nMIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB\nhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV\nBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5\nMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT\nEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR\nQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh\ndGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR\n6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X\npz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC\n9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV\n/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf\nZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z\n+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w\nqP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah\nSL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC\nu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf\nFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq\ncrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E\nFgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB\n/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl\nwFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM\n4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV\n2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna\nFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ\nCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK\nboHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke\njkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL\nS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb\nQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl\n0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB\nNVOFBkpdn627G190\n-----END CERTIFICATE-----\n\n# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network\n# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network\n# Label: \"USERTrust RSA Certification Authority\"\n# Serial: 2645093764781058787591871645665788717\n# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5\n# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e\n# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2\n-----BEGIN CERTIFICATE-----\nMIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB\niDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\ncnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\nBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw\nMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV\nBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU\naGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy\ndGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B\n3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY\ntJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/\nFp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2\nVN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT\n79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6\nc0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT\nYo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l\nc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee\nUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE\nHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd\nBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G\nA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF\nUp/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO\nVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3\nATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs\n8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR\niQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze\nSf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ\nXHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/\nqS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB\nVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB\nL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG\njjxDah2nGN59PRbxYvnKkKj9\n-----END CERTIFICATE-----\n\n# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network\n# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network\n# Label: \"USERTrust ECC Certification Authority\"\n# Serial: 123013823720199481456569720443997572134\n# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1\n# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0\n# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a\n-----BEGIN CERTIFICATE-----\nMIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl\neSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT\nJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx\nMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT\nCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg\nVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm\naWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo\nI+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng\no4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G\nA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD\nVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB\nzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW\nRNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=\n-----END CERTIFICATE-----\n\n# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4\n# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4\n# Label: \"GlobalSign ECC Root CA - R4\"\n# Serial: 14367148294922964480859022125800977897474\n# MD5 Fingerprint: 20:f0:27:68:d1:7e:a0:9d:0e:e6:2a:ca:df:5c:89:8e\n# SHA1 Fingerprint: 69:69:56:2e:40:80:f4:24:a1:e7:19:9f:14:ba:f3:ee:58:ab:6a:bb\n# SHA256 Fingerprint: be:c9:49:11:c2:95:56:76:db:6c:0a:55:09:86:d7:6e:3b:a0:05:66:7c:44:2c:97:62:b4:fb:b7:73:de:22:8c\n-----BEGIN CERTIFICATE-----\nMIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk\nMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH\nbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX\nDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD\nQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ\nFspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw\nDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F\nuOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX\nkPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs\newv4n4Q=\n-----END CERTIFICATE-----\n\n# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5\n# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5\n# Label: \"GlobalSign ECC Root CA - R5\"\n# Serial: 32785792099990507226680698011560947931244\n# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08\n# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa\n# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24\n-----BEGIN CERTIFICATE-----\nMIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk\nMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH\nbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX\nDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD\nQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc\n8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke\nhOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD\nVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI\nKoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg\n515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO\nxwy8p2Fp8fc74SrL+SvzZpA3\n-----END CERTIFICATE-----\n\n# Issuer: CN=Staat der Nederlanden EV Root CA O=Staat der Nederlanden\n# Subject: CN=Staat der Nederlanden EV Root CA O=Staat der Nederlanden\n# Label: \"Staat der Nederlanden EV Root CA\"\n# Serial: 10000013\n# MD5 Fingerprint: fc:06:af:7b:e8:1a:f1:9a:b4:e8:d2:70:1f:c0:f5:ba\n# SHA1 Fingerprint: 76:e2:7e:c1:4f:db:82:c1:c0:a6:75:b5:05:be:3d:29:b4:ed:db:bb\n# SHA256 Fingerprint: 4d:24:91:41:4c:fe:95:67:46:ec:4c:ef:a6:cf:6f:72:e2:8a:13:29:43:2f:9d:8a:90:7a:c4:cb:5d:ad:c1:5a\n-----BEGIN CERTIFICATE-----\nMIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO\nTDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh\ndCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y\nMjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg\nTmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS\nb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS\nM4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC\nUiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d\nZ//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p\nrfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l\npJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb\nj5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC\nKFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS\n/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X\ncgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH\n1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP\npx9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB\n/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7\nMA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI\neK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u\n2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS\nv4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC\nwPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy\nCqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e\nvTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6\nZ2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa\nGl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL\neG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8\nFVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc\n7uzXLg==\n-----END CERTIFICATE-----\n\n# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust\n# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust\n# Label: \"IdenTrust Commercial Root CA 1\"\n# Serial: 13298821034946342390520003877796839426\n# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7\n# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25\n# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae\n-----BEGIN CERTIFICATE-----\nMIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK\nMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu\nVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw\nMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw\nJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT\n3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU\n+ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp\nS0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1\nbVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi\nT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL\nvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK\nVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK\ndHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT\nc+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv\nl7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N\niGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB\n/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD\nggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH\n6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt\nLRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93\nnAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3\n+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK\nW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT\nAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq\nl1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG\n4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ\nmUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A\n7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H\n-----END CERTIFICATE-----\n\n# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust\n# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust\n# Label: \"IdenTrust Public Sector Root CA 1\"\n# Serial: 13298821034946342390521976156843933698\n# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba\n# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd\n# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f\n-----BEGIN CERTIFICATE-----\nMIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu\nVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN\nMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0\nMSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7\nekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy\nRBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS\nbdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF\n/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R\n3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw\nEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy\n9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V\nGxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ\n2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV\nWaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD\nW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/\nBAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN\nAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj\nt2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV\nDRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9\nTaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G\nlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW\nmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df\nWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5\n+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ\ntshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA\nGaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv\n8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c\n-----END CERTIFICATE-----\n\n# Issuer: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only\n# Subject: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only\n# Label: \"Entrust Root Certification Authority - G2\"\n# Serial: 1246989352\n# MD5 Fingerprint: 4b:e2:c9:91:96:65:0c:f4:0e:5a:93:92:a0:0a:fe:b2\n# SHA1 Fingerprint: 8c:f4:27:fd:79:0c:3a:d1:66:06:8d:e8:1e:57:ef:bb:93:22:72:d4\n# SHA256 Fingerprint: 43:df:57:74:b0:3e:7f:ef:5f:e4:0d:93:1a:7b:ed:f1:bb:2e:6b:42:73:8c:4e:6d:38:41:10:3d:3a:a7:f3:39\n-----BEGIN CERTIFICATE-----\nMIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC\nVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50\ncnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs\nIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz\ndCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy\nNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu\ndHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt\ndGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0\naG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj\nYXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T\nRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN\ncCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW\nwcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1\nU1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0\njaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP\nBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN\nBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/\njTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ\nRkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v\n1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R\nnAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH\nVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g==\n-----END CERTIFICATE-----\n\n# Issuer: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only\n# Subject: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only\n# Label: \"Entrust Root Certification Authority - EC1\"\n# Serial: 51543124481930649114116133369\n# MD5 Fingerprint: b6:7e:1d:f0:58:c5:49:6c:24:3b:3d:ed:98:18:ed:bc\n# SHA1 Fingerprint: 20:d8:06:40:df:9b:25:f5:12:25:3a:11:ea:f7:59:8a:eb:14:b5:47\n# SHA256 Fingerprint: 02:ed:0e:b2:8c:14:da:45:16:5c:56:67:91:70:0d:64:51:d7:fb:56:f0:b2:ab:1d:3b:8e:b0:70:e5:6e:df:f5\n-----BEGIN CERTIFICATE-----\nMIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG\nA1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3\nd3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu\ndHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq\nRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy\nMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD\nVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0\nL2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g\nZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD\nZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi\nA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt\nByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH\nBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\nBBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC\nR98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX\nhTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G\n-----END CERTIFICATE-----\n\n# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority\n# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority\n# Label: \"CFCA EV ROOT\"\n# Serial: 407555286\n# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30\n# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83\n# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd\n-----BEGIN CERTIFICATE-----\nMIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD\nTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y\naXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx\nMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j\naWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP\nT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03\nsQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL\nTIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5\n/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp\n7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz\nEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt\nhxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP\na931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot\naK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg\nTnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV\nPKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv\ncWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL\ntbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd\nBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB\nACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT\nej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL\njOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS\nESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy\nP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19\nxIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d\nCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN\n5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe\n/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z\nAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ\n5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su\n-----END CERTIFICATE-----\n\n# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed\n# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed\n# Label: \"OISTE WISeKey Global Root GB CA\"\n# Serial: 157768595616588414422159278966750757568\n# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d\n# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed\n# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6\n-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt\nMQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg\nRm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i\nYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x\nCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG\nb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh\nbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3\nHEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx\nWuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX\n1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk\nu7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P\n99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r\nM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw\nAwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB\nBAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh\ncViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5\ngSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO\nZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf\naPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic\nNc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM=\n-----END CERTIFICATE-----\n\n# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A.\n# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A.\n# Label: \"SZAFIR ROOT CA2\"\n# Serial: 357043034767186914217277344587386743377558296292\n# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99\n# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de\n# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe\n-----BEGIN CERTIFICATE-----\nMIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL\nBQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6\nZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw\nNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L\ncmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg\nUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN\nQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT\n3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw\n3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6\n3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5\nBSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN\nXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD\nAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF\nAAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw\n8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG\nnXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP\noky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy\nd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg\nLvWpCz/UXeHPhJ/iGcJfitYgHuNztw==\n-----END CERTIFICATE-----\n\n# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority\n# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority\n# Label: \"Certum Trusted Network CA 2\"\n# Serial: 44979900017204383099463764357512596969\n# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2\n# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92\n# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04\n-----BEGIN CERTIFICATE-----\nMIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB\ngDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu\nQS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG\nA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz\nOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ\nVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp\nZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3\nb3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA\nDGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn\n0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB\nOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE\nfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E\nSv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m\no130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i\nsx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW\nOZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez\nTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS\nadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n\n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD\nAQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC\nAQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ\nF/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf\nCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29\nXN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm\ndjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/\nWjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb\nAoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq\nP/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko\nb7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj\nXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P\n5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi\nDrW5viSP\n-----END CERTIFICATE-----\n\n# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority\n# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority\n# Label: \"Hellenic Academic and Research Institutions RootCA 2015\"\n# Serial: 0\n# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce\n# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6\n# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36\n-----BEGIN CERTIFICATE-----\nMIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix\nDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k\nIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT\nN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v\ndENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG\nA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh\nZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx\nQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1\ndGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC\nAQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA\n4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0\nAoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10\n4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C\nojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV\n9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD\ngfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6\nY5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq\nNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko\nLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc\nBw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV\nHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd\nctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I\nXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI\nM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot\n9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V\nZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea\nj8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh\nX9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ\nl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf\nbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4\npcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK\ne7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0\nvm9qp/UsQu0yrbYhnr68\n-----END CERTIFICATE-----\n\n# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority\n# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority\n# Label: \"Hellenic Academic and Research Institutions ECC RootCA 2015\"\n# Serial: 0\n# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef\n# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66\n# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33\n-----BEGIN CERTIFICATE-----\nMIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN\nBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl\nc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl\nbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv\nb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ\nBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj\nYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5\nMUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0\ndXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg\nQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa\njq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC\nMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi\nC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep\nlSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof\nTUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR\n-----END CERTIFICATE-----\n\n# Issuer: CN=ISRG Root X1 O=Internet Security Research Group\n# Subject: CN=ISRG Root X1 O=Internet Security Research Group\n# Label: \"ISRG Root X1\"\n# Serial: 172886928669790476064670243504169061120\n# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e\n# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8\n# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6\n-----BEGIN CERTIFICATE-----\nMIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\nWhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\nZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\nMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\nh77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\nA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\nT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\nB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\nB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\nKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\nOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\njh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\nqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\nrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\nhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\nubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\nNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\nORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\nTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\njNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\noyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\nmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\nemyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n-----END CERTIFICATE-----\n\n# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM\n# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM\n# Label: \"AC RAIZ FNMT-RCM\"\n# Serial: 485876308206448804701554682760554759\n# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d\n# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20\n# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa\n-----BEGIN CERTIFICATE-----\nMIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx\nCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ\nWiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ\nBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG\nTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/\nyBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf\nBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz\nWHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF\ntBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z\n374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC\nIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL\nmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7\nwk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS\nMKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2\nZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet\nUqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw\nAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H\nYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3\nLmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD\nnFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1\nRXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM\nLVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf\n77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N\nJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm\nfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp\n6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp\n1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B\n9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok\nRqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv\nuu8wd+RU4riEmViAqhOLUTpPSPaLtrM=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Amazon Root CA 1 O=Amazon\n# Subject: CN=Amazon Root CA 1 O=Amazon\n# Label: \"Amazon Root CA 1\"\n# Serial: 143266978916655856878034712317230054538369994\n# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6\n# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16\n# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e\n-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\njgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\nA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\nU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\nN+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\no/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\nrqXRfboQnoZsG4q5WTP468SQvvG5\n-----END CERTIFICATE-----\n\n# Issuer: CN=Amazon Root CA 2 O=Amazon\n# Subject: CN=Amazon Root CA 2 O=Amazon\n# Label: \"Amazon Root CA 2\"\n# Serial: 143266982885963551818349160658925006970653239\n# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66\n# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a\n# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4\n-----BEGIN CERTIFICATE-----\nMIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK\ngXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ\nW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg\n1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K\n8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r\n2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me\nz/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR\n8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj\nmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz\n7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6\n+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI\n0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB\nAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm\nUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2\nLIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY\n+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS\nk5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl\n7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm\nbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl\nurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+\nfUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63\nn749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE\n76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H\n9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT\n4PsJYGw=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Amazon Root CA 3 O=Amazon\n# Subject: CN=Amazon Root CA 3 O=Amazon\n# Label: \"Amazon Root CA 3\"\n# Serial: 143266986699090766294700635381230934788665930\n# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87\n# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e\n# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4\n-----BEGIN CERTIFICATE-----\nMIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5\nMQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g\nUm9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG\nA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg\nQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl\nui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr\nttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr\nBqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM\nYyRIHN8wfdVoOw==\n-----END CERTIFICATE-----\n\n# Issuer: CN=Amazon Root CA 4 O=Amazon\n# Subject: CN=Amazon Root CA 4 O=Amazon\n# Label: \"Amazon Root CA 4\"\n# Serial: 143266989758080763974105200630763877849284878\n# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd\n# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be\n# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92\n-----BEGIN CERTIFICATE-----\nMIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5\nMQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g\nUm9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG\nA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg\nQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi\n9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk\nM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB\n/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB\nMAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw\nCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW\n1KyLa2tJElMzrdfkviT8tQp21KW8EA==\n-----END CERTIFICATE-----\n\n# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM\n# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM\n# Label: \"TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1\"\n# Serial: 1\n# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49\n# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca\n# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16\n-----BEGIN CERTIFICATE-----\nMIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx\nGDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp\nbXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w\nKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0\nBgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy\ndW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG\nEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll\nIEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU\nQUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT\nTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg\nLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7\na9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr\nLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr\nN3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X\nYacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/\niSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f\nAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH\nV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL\nBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh\nAHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf\nIPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4\nlzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c\n8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf\nlo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM=\n-----END CERTIFICATE-----\n\n# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD.\n# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD.\n# Label: \"GDCA TrustAUTH R5 ROOT\"\n# Serial: 9009899650740120186\n# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4\n# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4\n# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93\n-----BEGIN CERTIFICATE-----\nMIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE\nBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ\nIENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0\nMTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV\nBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w\nHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj\nDp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj\nTnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u\nKU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj\nqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm\nMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12\nZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP\nzgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk\nL30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC\njGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA\nHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC\nAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB\n/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg\np8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm\nDRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5\nCOmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry\nL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf\nJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg\nIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io\n2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV\n09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ\nXR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq\nT8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe\nMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g==\n-----END CERTIFICATE-----\n\n# Issuer: CN=TrustCor RootCert CA-1 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority\n# Subject: CN=TrustCor RootCert CA-1 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority\n# Label: \"TrustCor RootCert CA-1\"\n# Serial: 15752444095811006489\n# MD5 Fingerprint: 6e:85:f1:dc:1a:00:d3:22:d5:b2:b2:ac:6b:37:05:45\n# SHA1 Fingerprint: ff:bd:cd:e7:82:c8:43:5e:3c:6f:26:86:5c:ca:a8:3a:45:5b:c3:0a\n# SHA256 Fingerprint: d4:0e:9c:86:cd:8f:e4:68:c1:77:69:59:f4:9e:a7:74:fa:54:86:84:b6:c4:06:f3:90:92:61:f4:dc:e2:57:5c\n-----BEGIN CERTIFICATE-----\nMIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYD\nVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk\nMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U\ncnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29y\nIFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkxMjMxMTcyMzE2WjCB\npDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFuYW1h\nIENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUG\nA1UECwweVHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZU\ncnVzdENvciBSb290Q2VydCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAv463leLCJhJrMxnHQFgKq1mqjQCj/IDHUHuO1CAmujIS2CNUSSUQIpid\nRtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4pQa81QBeCQryJ3pS/C3V\nseq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0JEsq1pme\n9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CV\nEY4hgLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorW\nhnAbJN7+KIor0Gqw/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/\nDeOxCbeKyKsZn3MzUOcwHwYDVR0jBBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcw\nDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD\nggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5mDo4Nvu7Zp5I\n/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf\nke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZ\nyonnMlo2HD6CqFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djts\nL1Ac59v2Z3kf9YKVmgenFK+P3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdN\nzl/HHk484IkzlQsPpTLWPFp5LBk=\n-----END CERTIFICATE-----\n\n# Issuer: CN=TrustCor RootCert CA-2 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority\n# Subject: CN=TrustCor RootCert CA-2 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority\n# Label: \"TrustCor RootCert CA-2\"\n# Serial: 2711694510199101698\n# MD5 Fingerprint: a2:e1:f8:18:0b:ba:45:d5:c7:41:2a:bb:37:52:45:64\n# SHA1 Fingerprint: b8:be:6d:cb:56:f1:55:b9:63:d4:12:ca:4e:06:34:c7:94:b2:1c:c0\n# SHA256 Fingerprint: 07:53:e9:40:37:8c:1b:d5:e3:83:6e:39:5d:ae:a5:cb:83:9e:50:46:f1:bd:0e:ae:19:51:cf:10:fe:c7:c9:65\n-----BEGIN CERTIFICATE-----\nMIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNV\nBAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw\nIgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy\ndXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEfMB0GA1UEAwwWVHJ1c3RDb3Ig\nUm9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEyMzExNzI2MzlaMIGk\nMQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEg\nQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYD\nVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRy\ndXN0Q29yIFJvb3RDZXJ0IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQCnIG7CKqJiJJWQdsg4foDSq8GbZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+\nQVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9NkRvRUqdw6VC0xK5mC8tkq\n1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1oYxOdqHp\n2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nK\nDOObXUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hape\naz6LMvYHL1cEksr1/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF\n3wP+TfSvPd9cW436cOGlfifHhi5qjxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88\noWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQPeSghYA2FFn3XVDjxklb9tTNM\ng9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+CtgrKAmrhQhJ8Z3\nmjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh\n8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAd\nBgNVHQ4EFgQU2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6U\nnrybPZx9mCAZ5YwwYrIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw\nDQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/hOsh80QA9z+LqBrWyOrsGS2h60COX\ndKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnpkpfbsEZC89NiqpX+\nMWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv2wnL\n/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RX\nCI/hOWB3S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYa\nZH9bDTMJBzN7Bj8RpFxwPIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW\n2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dvDDqPys/cA8GiCcjl/YBeyGBCARsaU1q7\nN6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYURpFHmygk71dSTlxCnKr3\nSewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANExdqtvArB\nAs8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp\n5KeXRKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu\n1uwJ\n-----END CERTIFICATE-----\n\n# Issuer: CN=TrustCor ECA-1 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority\n# Subject: CN=TrustCor ECA-1 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority\n# Label: \"TrustCor ECA-1\"\n# Serial: 9548242946988625984\n# MD5 Fingerprint: 27:92:23:1d:0a:f5:40:7c:e9:e6:6b:9d:d8:f5:e7:6c\n# SHA1 Fingerprint: 58:d1:df:95:95:67:6b:63:c0:f0:5b:1c:17:4d:8b:84:0b:c8:78:bd\n# SHA256 Fingerprint: 5a:88:5d:b1:9c:01:d9:12:c5:75:93:88:93:8c:af:bb:df:03:1a:b2:d4:8e:91:ee:15:58:9b:42:97:1d:03:9c\n-----BEGIN CERTIFICATE-----\nMIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYD\nVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk\nMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U\ncnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxFzAVBgNVBAMMDlRydXN0Q29y\nIEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3MjgwN1owgZwxCzAJBgNV\nBAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw\nIgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy\ndXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3Ig\nRUNBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb\n3w9U73NjKYKtR8aja+3+XzP4Q1HpGjORMRegdMTUpwHmspI+ap3tDvl0mEDTPwOA\nBoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23xFUfJ3zSCNV2HykVh0A5\n3ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmcp0yJF4Ou\nowReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/\nwZ0+fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZF\nZtS6mFjBAgMBAAGjYzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAf\nBgNVHSMEGDAWgBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/\nMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAQEABT41XBVwm8nHc2Fv\ncivUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u/ukZMjgDfxT2\nAHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F\nhcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50\nsoIipX1TH0XsJ5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BI\nWJZpTdwHjFGTot+fDz2LYLSCjaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1Wi\ntJ/X5g==\n-----END CERTIFICATE-----\n\n# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation\n# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation\n# Label: \"SSL.com Root Certification Authority RSA\"\n# Serial: 8875640296558310041\n# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29\n# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb\n# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69\n-----BEGIN CERTIFICATE-----\nMIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE\nBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK\nDA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp\nY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz\nOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv\ndXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv\nbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R\nxFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX\nqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC\nC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3\n6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh\n/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF\nYD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E\nJNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc\nUS4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8\nZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm\n+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi\nM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV\nHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G\nA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV\ncpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc\nHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs\nPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/\nq5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0\ncuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr\na6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I\nH37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y\nK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu\nnLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf\noYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY\nIc2wBlX7Jz9TkHCpBB5XJ7k=\n-----END CERTIFICATE-----\n\n# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation\n# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation\n# Label: \"SSL.com Root Certification Authority ECC\"\n# Serial: 8495723813297216424\n# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e\n# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a\n# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65\n-----BEGIN CERTIFICATE-----\nMIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC\nVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T\nU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0\naW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz\nWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0\nb24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS\nb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB\nBAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI\n7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg\nCemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud\nEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD\nVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T\nkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+\ngA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl\n-----END CERTIFICATE-----\n\n# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation\n# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation\n# Label: \"SSL.com EV Root Certification Authority RSA R2\"\n# Serial: 6248227494352943350\n# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95\n# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a\n# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c\n-----BEGIN CERTIFICATE-----\nMIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV\nBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE\nCgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy\ndGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy\nMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G\nA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD\nDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq\nM0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf\nOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa\n4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9\nHSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR\naZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA\nb9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ\nGp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV\nPWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO\npgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu\nUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY\nMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV\nHSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4\n9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW\ns47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5\nSm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg\ncLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM\n79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz\n/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt\nll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm\nKf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK\nQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ\nw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi\nS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07\nmKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w==\n-----END CERTIFICATE-----\n\n# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation\n# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation\n# Label: \"SSL.com EV Root Certification Authority ECC\"\n# Serial: 3182246526754555285\n# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90\n# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d\n# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8\n-----BEGIN CERTIFICATE-----\nMIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC\nVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T\nU0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp\nY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx\nNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv\ndXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv\nbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49\nAgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA\nVIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku\nWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP\nMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX\n5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ\nytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg\nh5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg==\n-----END CERTIFICATE-----\n\n# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6\n# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6\n# Label: \"GlobalSign Root CA - R6\"\n# Serial: 1417766617973444989252670301619537\n# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae\n# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1\n# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69\n-----BEGIN CERTIFICATE-----\nMIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg\nMB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh\nbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx\nMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET\nMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI\nxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k\nZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD\naNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw\nLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw\n1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX\nk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2\nSXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h\nbguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n\nWUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY\nrZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce\nMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD\nAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu\nbAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN\nnsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt\nIxg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61\n55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj\nvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf\ncDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz\noHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp\nnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs\npA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v\nJJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R\n8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4\n5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA=\n-----END CERTIFICATE-----\n\n# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed\n# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed\n# Label: \"OISTE WISeKey Global Root GC CA\"\n# Serial: 44084345621038548146064804565436152554\n# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23\n# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31\n# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d\n-----BEGIN CERTIFICATE-----\nMIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw\nCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91\nbmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg\nUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ\nBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu\nZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS\nb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni\neUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W\np2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E\nBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T\nrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV\n57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg\nMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9\n-----END CERTIFICATE-----\n\n# Issuer: CN=GTS Root R1 O=Google Trust Services LLC\n# Subject: CN=GTS Root R1 O=Google Trust Services LLC\n# Label: \"GTS Root R1\"\n# Serial: 146587175971765017618439757810265552097\n# MD5 Fingerprint: 82:1a:ef:d4:d2:4a:f2:9f:e2:3d:97:06:14:70:72:85\n# SHA1 Fingerprint: e1:c9:50:e6:ef:22:f8:4c:56:45:72:8b:92:20:60:d7:d5:a7:a3:e8\n# SHA256 Fingerprint: 2a:57:54:71:e3:13:40:bc:21:58:1c:bd:2c:f1:3e:15:84:63:20:3e:ce:94:bc:f9:d3:cc:19:6b:f0:9a:54:72\n-----BEGIN CERTIFICATE-----\nMIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBH\nMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM\nQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy\nMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl\ncnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM\nf/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vX\nmX7wCl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7\nzUjwTcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0P\nfyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtc\nvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4\nZor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUsp\nzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOO\nRc92wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYW\nk70paDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+\nDVrNVjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgF\nlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBADiW\nCu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1\nd5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6Z\nXPYfcX3v73svfuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZR\ngyFmxhE+885H7pwoHyXa/6xmld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3\nd8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9bgsiG1eGZbYwE8na6SfZu6W0eX6Dv\nJ4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq4BjFbkerQUIpm/Zg\nDdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWErtXvM\n+SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyy\nF62ARPBopY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9\nSQ98POyDGCBDTtWTurQ0sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdws\nE3PYJ/HQcu51OyLemGhmW/HGY0dVHLqlCFF1pkgl\n-----END CERTIFICATE-----\n\n# Issuer: CN=GTS Root R2 O=Google Trust Services LLC\n# Subject: CN=GTS Root R2 O=Google Trust Services LLC\n# Label: \"GTS Root R2\"\n# Serial: 146587176055767053814479386953112547951\n# MD5 Fingerprint: 44:ed:9a:0e:a4:09:3b:00:f2:ae:4c:a3:c6:61:b0:8b\n# SHA1 Fingerprint: d2:73:96:2a:2a:5e:39:9f:73:3f:e1:c7:1e:64:3f:03:38:34:fc:4d\n# SHA256 Fingerprint: c4:5d:7b:b0:8e:6d:67:e6:2e:42:35:11:0b:56:4e:5f:78:fd:92:ef:05:8c:84:0a:ea:4e:64:55:d7:58:5c:60\n-----BEGIN CERTIFICATE-----\nMIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBH\nMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM\nQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy\nMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl\ncnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv\nCvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3Kg\nGjSY6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9Bu\nXvAuMC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOd\nre7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXu\nPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1\nmKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K\n8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqj\nx5RWIr9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsR\nnTKaG73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0\nkzCqgc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9Ok\ntwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBALZp\n8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT\nvhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiT\nz9D2PGcDFWEJ+YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiA\npJiS4wGWAqoC7o87xdFtCjMwc3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvb\npxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3DaWsYDQvTtN6LwG1BUSw7YhN4ZKJmB\nR64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5rn/WkhLx3+WuXrD5R\nRaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56GtmwfuNmsk\n0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC\n5AwiWVIQ7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiF\nizoHCBy69Y9Vmhh1fuXsgWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLn\nyOd/xCxgXS/Dr55FBcOEArf9LAhST4Ldo/DUhgkC\n-----END CERTIFICATE-----\n\n# Issuer: CN=GTS Root R3 O=Google Trust Services LLC\n# Subject: CN=GTS Root R3 O=Google Trust Services LLC\n# Label: \"GTS Root R3\"\n# Serial: 146587176140553309517047991083707763997\n# MD5 Fingerprint: 1a:79:5b:6b:04:52:9c:5d:c7:74:33:1b:25:9a:f9:25\n# SHA1 Fingerprint: 30:d4:24:6f:07:ff:db:91:89:8a:0b:e9:49:66:11:eb:8c:5e:46:e5\n# SHA256 Fingerprint: 15:d5:b8:77:46:19:ea:7d:54:ce:1c:a6:d0:b0:c4:03:e0:37:a9:17:f1:31:e8:a0:4e:1e:6b:7a:71:ba:bc:e5\n-----BEGIN CERTIFICATE-----\nMIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQsw\nCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU\nMBIGA1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw\nMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp\nY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout\n736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2A\nDDL24CejQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\nDgQWBBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFuk\nfCPAlaUs3L6JbyO5o91lAFJekazInXJ0glMLfalAvWhgxeG4VDvBNhcl2MG9AjEA\nnjWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOaKaqW04MjyaR7YbPMAuhd\n-----END CERTIFICATE-----\n\n# Issuer: CN=GTS Root R4 O=Google Trust Services LLC\n# Subject: CN=GTS Root R4 O=Google Trust Services LLC\n# Label: \"GTS Root R4\"\n# Serial: 146587176229350439916519468929765261721\n# MD5 Fingerprint: 5d:b6:6a:c4:60:17:24:6a:1a:99:a8:4b:ee:5e:b4:26\n# SHA1 Fingerprint: 2a:1d:60:27:d9:4a:b1:0a:1c:4d:91:5c:cd:33:a0:cb:3e:2d:54:cb\n# SHA256 Fingerprint: 71:cc:a5:39:1f:9e:79:4b:04:80:25:30:b3:63:e1:21:da:8a:30:43:bb:26:66:2f:ea:4d:ca:7f:c9:51:a4:bd\n-----BEGIN CERTIFICATE-----\nMIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQsw\nCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU\nMBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw\nMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp\nY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu\nhXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/l\nxKvRHYqjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\nDgQWBBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0\nCMRw3J5QdCHojXohw0+WbhXRIjVhLfoIN+4Zba3bssx9BzT1YBkstTTZbyACMANx\nsbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11xzPKwTdb+mciUqXWi4w==\n-----END CERTIFICATE-----\n\n# Issuer: CN=UCA Global G2 Root O=UniTrust\n# Subject: CN=UCA Global G2 Root O=UniTrust\n# Label: \"UCA Global G2 Root\"\n# Serial: 124779693093741543919145257850076631279\n# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8\n# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a\n# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c\n-----BEGIN CERTIFICATE-----\nMIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9\nMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH\nbG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x\nCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds\nb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr\nb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9\nkmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm\nVHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R\nVogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc\nC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj\ntm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY\nD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv\nj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl\nNaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6\niIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP\nO6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/\nBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV\nZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj\nL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5\n1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl\n1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU\nb3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV\nPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj\ny88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb\nEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg\nDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI\n+Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy\nYiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX\nUB+K+wb1whnw0A==\n-----END CERTIFICATE-----\n\n# Issuer: CN=UCA Extended Validation Root O=UniTrust\n# Subject: CN=UCA Extended Validation Root O=UniTrust\n# Label: \"UCA Extended Validation Root\"\n# Serial: 106100277556486529736699587978573607008\n# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2\n# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a\n# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24\n-----BEGIN CERTIFICATE-----\nMIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH\nMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF\neHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx\nMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV\nBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog\nD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS\nsPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop\nO2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk\nsHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi\nc0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj\nVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz\nKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/\nTuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G\nsx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs\n1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD\nfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T\nAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN\nl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR\nap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ\nVBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5\nc6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp\n4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s\nt2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj\n2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO\nvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C\nxR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx\ncmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM\nfjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax\n-----END CERTIFICATE-----\n\n# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036\n# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036\n# Label: \"Certigna Root CA\"\n# Serial: 269714418870597844693661054334862075617\n# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77\n# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43\n# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68\n-----BEGIN CERTIFICATE-----\nMIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw\nWjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw\nMiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x\nMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD\nVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX\nBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\nggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO\nty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M\nCiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu\nI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm\nTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh\nC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf\nePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz\nIoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT\nCo/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k\nJWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5\nhwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB\nGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE\nFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of\n1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov\nL3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo\ndHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr\naHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq\nhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L\n6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG\nHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6\n0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB\nlA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi\no2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1\ngPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v\nfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63\nNwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh\njWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw\n3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0=\n-----END CERTIFICATE-----\n\n# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI\n# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI\n# Label: \"emSign Root CA - G1\"\n# Serial: 235931866688319308814040\n# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac\n# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c\n# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67\n-----BEGIN CERTIFICATE-----\nMIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD\nVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU\nZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH\nMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO\nMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv\nZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz\nf2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO\n8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq\nd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM\ntTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt\nOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB\no0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD\nAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x\nPaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM\nwiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d\nGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH\n6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby\nRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx\niN66zB+Afko=\n-----END CERTIFICATE-----\n\n# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI\n# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI\n# Label: \"emSign ECC Root CA - G3\"\n# Serial: 287880440101571086945156\n# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40\n# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1\n# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b\n-----BEGIN CERTIFICATE-----\nMIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG\nEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo\nbm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g\nRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ\nTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s\nb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw\ndjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0\nWXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS\nfvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB\nzhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq\nhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB\nCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD\n+JbNR6iC8hZVdyR+EhCVBCyj\n-----END CERTIFICATE-----\n\n# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI\n# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI\n# Label: \"emSign Root CA - C1\"\n# Serial: 825510296613316004955058\n# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68\n# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01\n# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f\n-----BEGIN CERTIFICATE-----\nMIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG\nA1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg\nSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw\nMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln\nbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v\ndCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ\nBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ\nHdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH\n3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH\nGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c\nxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1\naylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq\nTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL\nBQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87\n/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4\nkqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG\nYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT\n+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo\nWXzhriKi4gp6D/piq1JM4fHfyr6DDUI=\n-----END CERTIFICATE-----\n\n# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI\n# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI\n# Label: \"emSign ECC Root CA - C3\"\n# Serial: 582948710642506000014504\n# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5\n# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66\n# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3\n-----BEGIN CERTIFICATE-----\nMIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG\nEwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx\nIDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw\nMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln\nbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND\nIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci\nMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti\nsIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O\nBBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c\n3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J\n0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ==\n-----END CERTIFICATE-----\n\n# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post\n# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post\n# Label: \"Hongkong Post Root CA 3\"\n# Serial: 46170865288971385588281144162979347873371282084\n# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0\n# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02\n# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6\n-----BEGIN CERTIFICATE-----\nMIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL\nBQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ\nSG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n\na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5\nNDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT\nCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u\nZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO\ndem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI\nVoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV\n9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY\n2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY\nvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt\nbNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb\nx39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+\nl2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK\nTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj\nHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP\nBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e\ni9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw\nDQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG\n7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk\nMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr\ngZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk\nGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS\n3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm\nOzj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+\nl6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c\nJfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP\nL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa\nLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG\nmpv0\n-----END CERTIFICATE-----\n\n# Issuer: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only\n# Subject: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only\n# Label: \"Entrust Root Certification Authority - G4\"\n# Serial: 289383649854506086828220374796556676440\n# MD5 Fingerprint: 89:53:f1:83:23:b7:7c:8e:05:f1:8c:71:38:4e:1f:88\n# SHA1 Fingerprint: 14:88:4e:86:26:37:b0:26:af:59:62:5c:40:77:ec:35:29:ba:96:01\n# SHA256 Fingerprint: db:35:17:d1:f6:73:2a:2d:5a:b9:7c:53:3e:c7:07:79:ee:32:70:a6:2f:b4:ac:42:38:37:24:60:e6:f0:1e:88\n-----BEGIN CERTIFICATE-----\nMIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw\ngb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL\nEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg\nMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw\nBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0\nMB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT\nMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1\nc3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ\nbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg\nUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B\nAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ\n2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E\nT+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j\n5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM\nC1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T\nDtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX\nwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A\n2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm\nnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8\ndWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl\nN4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj\nc0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD\nVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS\n5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS\nGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr\nhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/\nB7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI\nAeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw\nH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+\nb7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk\n2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol\nIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk\n5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY\nn/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw==\n-----END CERTIFICATE-----\n\n# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation\n# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation\n# Label: \"Microsoft ECC Root Certificate Authority 2017\"\n# Serial: 136839042543790627607696632466672567020\n# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67\n# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5\n# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02\n-----BEGIN CERTIFICATE-----\nMIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw\nCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD\nVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw\nMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV\nUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy\nb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq\nhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR\nogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb\nhGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E\nBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3\nFQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV\nL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB\niudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation\n# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation\n# Label: \"Microsoft RSA Root Certificate Authority 2017\"\n# Serial: 40975477897264996090493496164228220339\n# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47\n# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74\n# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0\n-----BEGIN CERTIFICATE-----\nMIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl\nMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw\nNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5\nIDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG\nEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N\naWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ\nNt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0\nZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1\nHLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm\ngGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ\njEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc\naDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG\nYaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6\nW6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K\nUGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH\n+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q\nW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/\nBAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC\nNxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC\nLgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC\ngMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6\ntZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh\nSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2\nTaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3\npvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR\nxpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp\nGWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9\ndOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN\nAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB\nRA+GsCyRxj3qrg+E\n-----END CERTIFICATE-----\n\n# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd.\n# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd.\n# Label: \"e-Szigno Root CA 2017\"\n# Serial: 411379200276854331539784714\n# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98\n# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1\n# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99\n-----BEGIN CERTIFICATE-----\nMIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV\nBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk\nLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv\nb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ\nBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg\nTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v\nIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv\nxie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H\nWyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\nA1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB\neAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo\njbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ\n+efcMQ==\n-----END CERTIFICATE-----\n\n# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2\n# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2\n# Label: \"certSIGN Root CA G2\"\n# Serial: 313609486401300475190\n# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7\n# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32\n# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05\n-----BEGIN CERTIFICATE-----\nMIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV\nBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g\nUk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ\nBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ\nR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF\ndRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw\nvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ\nuIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp\nn+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs\ncpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW\nxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P\nrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF\nDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx\nDTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy\nLcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C\neWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB\n/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ\nd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq\nkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC\nb6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl\nqiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0\nOJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c\nNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk\nltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO\npwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj\n03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk\nPuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE\n1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX\nQRBdJ3NghVdJIgc=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc.\n# Subject: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc.\n# Label: \"Trustwave Global Certification Authority\"\n# Serial: 1846098327275375458322922162\n# MD5 Fingerprint: f8:1c:18:2d:2f:ba:5f:6d:a1:6c:bc:c7:ab:91:c7:0e\n# SHA1 Fingerprint: 2f:8f:36:4f:e1:58:97:44:21:59:87:a5:2a:9a:d0:69:95:26:7f:b5\n# SHA256 Fingerprint: 97:55:20:15:f5:dd:fc:3c:87:88:c0:06:94:45:55:40:88:94:45:00:84:f1:00:86:70:86:bc:1a:2b:b5:8d:c8\n-----BEGIN CERTIFICATE-----\nMIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw\nCQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x\nITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1\nc3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx\nOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI\nSWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI\nb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp\nY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB\nALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn\nswuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu\n7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8\n1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW\n80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP\nJqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l\nRtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw\nhI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10\ncoos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc\nBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n\ntwiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud\nEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud\nDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W\n0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe\nuyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q\nlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB\naCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE\nsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT\nMaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe\nqu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh\nVicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8\nh6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9\nEEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK\nyeC2nOnOcXHebD8WpHk=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc.\n# Subject: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc.\n# Label: \"Trustwave Global ECC P256 Certification Authority\"\n# Serial: 4151900041497450638097112925\n# MD5 Fingerprint: 5b:44:e3:8d:5d:36:86:26:e8:0d:05:d2:59:a7:83:54\n# SHA1 Fingerprint: b4:90:82:dd:45:0c:be:8b:5b:b1:66:d3:e2:a4:08:26:cd:ed:42:cf\n# SHA256 Fingerprint: 94:5b:bc:82:5e:a5:54:f4:89:d1:fd:51:a7:3d:df:2e:a6:24:ac:70:19:a0:52:05:22:5c:22:a7:8c:cf:a8:b4\n-----BEGIN CERTIFICATE-----\nMIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD\nVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf\nBgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3\nYXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x\nNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G\nA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0\nd2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF\nQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG\nSM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN\nFWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w\nDwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw\nCgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh\nDDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7\n-----END CERTIFICATE-----\n\n# Issuer: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc.\n# Subject: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc.\n# Label: \"Trustwave Global ECC P384 Certification Authority\"\n# Serial: 2704997926503831671788816187\n# MD5 Fingerprint: ea:cf:60:c4:3b:b9:15:29:40:a1:97:ed:78:27:93:d6\n# SHA1 Fingerprint: e7:f3:a3:c8:cf:6f:c3:04:2e:6d:0e:67:32:c5:9e:68:95:0d:5e:d2\n# SHA256 Fingerprint: 55:90:38:59:c8:c0:c3:eb:b8:75:9e:ce:4e:25:57:22:5f:f5:75:8b:bd:38:eb:d4:82:76:60:1e:1b:d5:80:97\n-----BEGIN CERTIFICATE-----\nMIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD\nVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf\nBgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3\nYXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x\nNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G\nA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0\nd2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF\nQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB\nBAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ\nj9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF\n1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G\nA1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3\nAZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC\nMGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu\nSw==\n-----END CERTIFICATE-----\n\n# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp.\n# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp.\n# Label: \"NAVER Global Root Certification Authority\"\n# Serial: 9013692873798656336226253319739695165984492813\n# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b\n# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1\n# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65\n-----BEGIN CERTIFICATE-----\nMIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM\nBQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG\nT1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0\naW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx\nCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD\nb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB\ndXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA\niQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH\n38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE\nHoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz\nkVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP\nszuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq\nvC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf\nnZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG\nYQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo\n0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a\nCJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K\nAQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I\n36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB\nAf8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN\nqo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj\ncu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm\n+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL\nhr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe\nlHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7\np/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8\npiKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR\nLBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX\n5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO\ndh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul\n9XXeifdy\n-----END CERTIFICATE-----\n\n# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres\n# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres\n# Label: \"AC RAIZ FNMT-RCM SERVIDORES SEGUROS\"\n# Serial: 131542671362353147877283741781055151509\n# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb\n# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a\n# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb\n-----BEGIN CERTIFICATE-----\nMIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw\nCQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw\nFgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S\nQ00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5\nMzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL\nDAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS\nQUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB\nBAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH\nsbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK\nUm8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD\nVR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu\nSuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC\nMQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy\nv+c=\n-----END CERTIFICATE-----\n\n# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa\n# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa\n# Label: \"GlobalSign Root R46\"\n# Serial: 1552617688466950547958867513931858518042577\n# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef\n# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90\n# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9\n-----BEGIN CERTIFICATE-----\nMIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA\nMEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD\nVQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy\nMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt\nc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ\nOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG\nvGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud\n316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo\n0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE\ny132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF\nzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE\n+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN\nI/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs\nx2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa\nByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC\n4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4\n7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg\nJuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti\n2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk\npnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF\nFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt\nrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk\nZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5\nu+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP\n4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6\nN3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3\nvouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6\n-----END CERTIFICATE-----\n\n# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa\n# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa\n# Label: \"GlobalSign Root E46\"\n# Serial: 1552617690338932563915843282459653771421763\n# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f\n# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84\n# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58\n-----BEGIN CERTIFICATE-----\nMIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx\nCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD\nExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw\nMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex\nHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq\nR+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd\nyXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\nDgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ\n7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8\n+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A=\n-----END CERTIFICATE-----\n\n# Issuer: CN=GLOBALTRUST 2020 O=e-commerce monitoring GmbH\n# Subject: CN=GLOBALTRUST 2020 O=e-commerce monitoring GmbH\n# Label: \"GLOBALTRUST 2020\"\n# Serial: 109160994242082918454945253\n# MD5 Fingerprint: 8a:c7:6f:cb:6d:e3:cc:a2:f1:7c:83:fa:0e:78:d7:e8\n# SHA1 Fingerprint: d0:67:c1:13:51:01:0c:aa:d0:c7:6a:65:37:31:16:26:4f:53:71:a2\n# SHA256 Fingerprint: 9a:29:6a:51:82:d1:d4:51:a2:e3:7f:43:9b:74:da:af:a2:67:52:33:29:f9:0f:9a:0d:20:07:c3:34:e2:3c:9a\n-----BEGIN CERTIFICATE-----\nMIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG\nA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw\nFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx\nMDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u\naXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b\nRatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z\nYybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3\nQWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw\nyJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+\nBlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ\nSaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH\nr96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0\n4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me\ndKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw\nq7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2\nnKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu\nH9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA\nVC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC\nXtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd\n6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf\n+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi\nkvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7\nwry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB\nTI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C\nMUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn\n4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I\naFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy\nqfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg==\n-----END CERTIFICATE-----\n\n# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz\n# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz\n# Label: \"ANF Secure Server Root CA\"\n# Serial: 996390341000653745\n# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96\n# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74\n# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99\n-----BEGIN CERTIFICATE-----\nMIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV\nBAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk\nYWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV\nBAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN\nMzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF\nUzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD\nVQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v\ndCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj\ncqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q\nyGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH\n2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX\nH1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL\nzc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR\np1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz\nW7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/\nSiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn\nLNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3\nn5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B\nu8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj\no1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO\nBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC\nAgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L\n9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej\nrw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK\npFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0\nvPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq\nOknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ\n/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9\n2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI\n+PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2\nMjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo\ntt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority\n# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority\n# Label: \"Certum EC-384 CA\"\n# Serial: 160250656287871593594747141429395092468\n# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1\n# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed\n# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6\n-----BEGIN CERTIFICATE-----\nMIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw\nCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw\nJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT\nEENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0\nWjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT\nLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX\nBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE\nKI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm\nFy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8\nEF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J\nUG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn\nnvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k=\n-----END CERTIFICATE-----\n\n# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority\n# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority\n# Label: \"Certum Trusted Root CA\"\n# Serial: 40870380103424195783807378461123655149\n# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29\n# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5\n# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd\n-----BEGIN CERTIFICATE-----\nMIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6\nMQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu\nMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV\nBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw\nMzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg\nU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo\nb3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ\nn0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q\np1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq\nNwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF\n8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3\nHAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa\nmqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi\n7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF\nytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P\nqafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ\nv3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6\nTsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1\nvALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD\nggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4\nWxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo\nzMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR\n5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ\nGfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf\n5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq\n0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D\nP78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM\nqJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP\n0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf\nE2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb\n-----END CERTIFICATE-----\n\n# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique\n# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique\n# Label: \"TunTrust Root CA\"\n# Serial: 108534058042236574382096126452369648152337120275\n# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4\n# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb\n# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41\n-----BEGIN CERTIFICATE-----\nMIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL\nBQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg\nQ2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv\nb3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG\nEwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u\nIEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ\nn56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd\n2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF\nVwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ\nGoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF\nli/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU\nr8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2\neY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb\nMlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg\njwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB\n7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW\n5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE\nITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0\n90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z\nxiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu\nQEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4\nFstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH\n22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP\nxOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn\ndFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5\nXc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b\nnV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ\nCvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH\nu/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj\nd9qDRIueVSjAi1jTkD5OGwDxFa2DK5o=\n-----END CERTIFICATE-----\n\n# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA\n# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA\n# Label: \"HARICA TLS RSA Root CA 2021\"\n# Serial: 76817823531813593706434026085292783742\n# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91\n# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d\n# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d\n-----BEGIN CERTIFICATE-----\nMIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs\nMQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl\nc2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg\nUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL\nMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl\nYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv\nb3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l\nmwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE\n4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv\na9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M\npbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw\nKh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b\nLW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY\nAuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB\nAGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq\nE613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr\nW2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ\nCoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF\nMAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE\nAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU\nX15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3\nf5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja\nH6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP\nJzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P\nzzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt\njBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0\n/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT\nBGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79\naPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW\nxw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU\n63ZTGI0RmLo=\n-----END CERTIFICATE-----\n\n# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA\n# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA\n# Label: \"HARICA TLS ECC Root CA 2021\"\n# Serial: 137515985548005187474074462014555733966\n# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0\n# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48\n# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01\n-----BEGIN CERTIFICATE-----\nMIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw\nCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh\ncmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v\ndCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG\nA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj\naCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg\nQ0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7\nKKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y\nSTHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw\nAwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD\nAgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw\nSaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN\nnxS2PFOiTAZpffpskcYqSUXm7LcT4Tps\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "openpype/vendor/python/python_2/certifi/core.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\ncertifi.py\n~~~~~~~~~~\n\nThis module returns the installation location of cacert.pem or its contents.\n\"\"\"\nimport os\n\ntry:\n    from importlib.resources import path as get_path, read_text\n\n    _CACERT_CTX = None\n    _CACERT_PATH = None\n\n    def where():\n        # This is slightly terrible, but we want to delay extracting the file\n        # in cases where we're inside of a zipimport situation until someone\n        # actually calls where(), but we don't want to re-extract the file\n        # on every call of where(), so we'll do it once then store it in a\n        # global variable.\n        global _CACERT_CTX\n        global _CACERT_PATH\n        if _CACERT_PATH is None:\n            # This is slightly janky, the importlib.resources API wants you to\n            # manage the cleanup of this file, so it doesn't actually return a\n            # path, it returns a context manager that will give you the path\n            # when you enter it and will do any cleanup when you leave it. In\n            # the common case of not needing a temporary file, it will just\n            # return the file system location and the __exit__() is a no-op.\n            #\n            # We also have to hold onto the actual context manager, because\n            # it will do the cleanup whenever it gets garbage collected, so\n            # we will also store that at the global level as well.\n            _CACERT_CTX = get_path(\"certifi\", \"cacert.pem\")\n            _CACERT_PATH = str(_CACERT_CTX.__enter__())\n\n        return _CACERT_PATH\n\n\nexcept ImportError:\n    # This fallback will work for Python versions prior to 3.7 that lack the\n    # importlib.resources module but relies on the existing `where` function\n    # so won't address issues with environments like PyOxidizer that don't set\n    # __file__ on modules.\n    def read_text(_module, _path, encoding=\"ascii\"):\n        with open(where(), \"r\", encoding=encoding) as data:\n            return data.read()\n\n    # If we don't have importlib.resources, then we will just do the old logic\n    # of assuming we're on the filesystem and munge the path directly.\n    def where():\n        f = os.path.dirname(__file__)\n\n        return os.path.join(f, \"cacert.pem\")\n\n\ndef contents():\n    return read_text(\"certifi\", \"cacert.pem\", encoding=\"ascii\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/__init__.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\n\nfrom .universaldetector import UniversalDetector\nfrom .enums import InputState\nfrom .version import __version__, VERSION\n\n\n__all__ = ['UniversalDetector', 'detect', 'detect_all', '__version__', 'VERSION']\n\n\ndef detect(byte_str):\n    \"\"\"\n    Detect the encoding of the given byte string.\n\n    :param byte_str:     The byte sequence to examine.\n    :type byte_str:      ``bytes`` or ``bytearray``\n    \"\"\"\n    if not isinstance(byte_str, bytearray):\n        if not isinstance(byte_str, bytes):\n            raise TypeError('Expected object of type bytes or bytearray, got: '\n                            '{}'.format(type(byte_str)))\n        else:\n            byte_str = bytearray(byte_str)\n    detector = UniversalDetector()\n    detector.feed(byte_str)\n    return detector.close()\n\n\ndef detect_all(byte_str):\n    \"\"\"\n    Detect all the possible encodings of the given byte string.\n\n    :param byte_str:     The byte sequence to examine.\n    :type byte_str:      ``bytes`` or ``bytearray``\n    \"\"\"\n    if not isinstance(byte_str, bytearray):\n        if not isinstance(byte_str, bytes):\n            raise TypeError('Expected object of type bytes or bytearray, got: '\n                            '{}'.format(type(byte_str)))\n        else:\n            byte_str = bytearray(byte_str)\n\n    detector = UniversalDetector()\n    detector.feed(byte_str)\n    detector.close()\n\n    if detector._input_state == InputState.HIGH_BYTE:\n        results = []\n        for prober in detector._charset_probers:\n            if prober.get_confidence() > detector.MINIMUM_THRESHOLD:\n                charset_name = prober.charset_name\n                lower_charset_name = prober.charset_name.lower()\n                # Use Windows encoding name instead of ISO-8859 if we saw any\n                # extra Windows-specific bytes\n                if lower_charset_name.startswith('iso-8859'):\n                    if detector._has_win_bytes:\n                        charset_name = detector.ISO_WIN_MAP.get(lower_charset_name,\n                                                            charset_name)\n                results.append({\n                    'encoding': charset_name,\n                    'confidence': prober.get_confidence(),\n                    'language': prober.language,\n                })\n        if len(results) > 0:\n            return sorted(results, key=lambda result: -result['confidence'])\n\n    return [detector.result]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/big5freq.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Communicator client code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\n# Big5 frequency table\n# by Taiwan's Mandarin Promotion Council\n# <http://www.edu.tw:81/mandr/>\n#\n# 128  --> 0.42261\n# 256  --> 0.57851\n# 512  --> 0.74851\n# 1024 --> 0.89384\n# 2048 --> 0.97583\n#\n# Ideal Distribution Ratio = 0.74851/(1-0.74851) =2.98\n# Random Distribution Ration = 512/(5401-512)=0.105\n#\n# Typical Distribution Ratio about 25% of Ideal one, still much higher than RDR\n\nBIG5_TYPICAL_DISTRIBUTION_RATIO = 0.75\n\n#Char to FreqOrder table\nBIG5_TABLE_SIZE = 5376\n\nBIG5_CHAR_TO_FREQ_ORDER = (\n   1,1801,1506, 255,1431, 198,   9,  82,   6,5008, 177, 202,3681,1256,2821, 110, #   16\n3814,  33,3274, 261,  76,  44,2114,  16,2946,2187,1176, 659,3971,  26,3451,2653, #   32\n1198,3972,3350,4202, 410,2215, 302, 590, 361,1964,   8, 204,  58,4510,5009,1932, #   48\n  63,5010,5011, 317,1614,  75, 222, 159,4203,2417,1480,5012,3555,3091, 224,2822, #   64\n3682,   3,  10,3973,1471,  29,2787,1135,2866,1940, 873, 130,3275,1123, 312,5013, #   80\n4511,2052, 507, 252, 682,5014, 142,1915, 124, 206,2947,  34,3556,3204,  64, 604, #   96\n5015,2501,1977,1978, 155,1991, 645, 641,1606,5016,3452, 337,  72, 406,5017,  80, #  112\n 630, 238,3205,1509, 263, 939,1092,2654, 756,1440,1094,3453, 449,  69,2987, 591, #  128\n 179,2096, 471, 115,2035,1844,  60,  50,2988, 134, 806,1869, 734,2036,3454, 180, #  144\n 995,1607, 156, 537,2907, 688,5018, 319,1305, 779,2145, 514,2379, 298,4512, 359, #  160\n2502,  90,2716,1338, 663,  11, 906,1099,2553,  20,2441, 182, 532,1716,5019, 732, #  176\n1376,4204,1311,1420,3206,  25,2317,1056, 113, 399, 382,1950, 242,3455,2474, 529, #  192\n3276, 475,1447,3683,5020, 117,  21, 656, 810,1297,2300,2334,3557,5021, 126,4205, #  208\n 706, 456, 150, 613,4513,  71,1118,2037,4206, 145,3092,  85, 835, 486,2115,1246, #  224\n1426, 428, 727,1285,1015, 800, 106, 623, 303,1281,5022,2128,2359, 347,3815, 221, #  240\n3558,3135,5023,1956,1153,4207,  83, 296,1199,3093, 192, 624,  93,5024, 822,1898, #  256\n2823,3136, 795,2065, 991,1554,1542,1592,  27,  43,2867, 859, 139,1456, 860,4514, #  272\n 437, 712,3974, 164,2397,3137, 695, 211,3037,2097, 195,3975,1608,3559,3560,3684, #  288\n3976, 234, 811,2989,2098,3977,2233,1441,3561,1615,2380, 668,2077,1638, 305, 228, #  304\n1664,4515, 467, 415,5025, 262,2099,1593, 239, 108, 300, 200,1033, 512,1247,2078, #  320\n5026,5027,2176,3207,3685,2682, 593, 845,1062,3277,  88,1723,2038,3978,1951, 212, #  336\n 266, 152, 149, 468,1899,4208,4516,  77, 187,5028,3038,  37,   5,2990,5029,3979, #  352\n5030,5031,  39,2524,4517,2908,3208,2079,  55, 148,  74,4518, 545, 483,1474,1029, #  368\n1665, 217,1870,1531,3138,1104,2655,4209,  24, 172,3562, 900,3980,3563,3564,4519, #  384\n  32,1408,2824,1312, 329, 487,2360,2251,2717, 784,2683,   4,3039,3351,1427,1789, #  400\n 188, 109, 499,5032,3686,1717,1790, 888,1217,3040,4520,5033,3565,5034,3352,1520, #  416\n3687,3981, 196,1034, 775,5035,5036, 929,1816, 249, 439,  38,5037,1063,5038, 794, #  432\n3982,1435,2301,  46, 178,3278,2066,5039,2381,5040, 214,1709,4521, 804,  35, 707, #  448\n 324,3688,1601,2554, 140, 459,4210,5041,5042,1365, 839, 272, 978,2262,2580,3456, #  464\n2129,1363,3689,1423, 697, 100,3094,  48,  70,1231, 495,3139,2196,5043,1294,5044, #  480\n2080, 462, 586,1042,3279, 853, 256, 988, 185,2382,3457,1698, 434,1084,5045,3458, #  496\n 314,2625,2788,4522,2335,2336, 569,2285, 637,1817,2525, 757,1162,1879,1616,3459, #  512\n 287,1577,2116, 768,4523,1671,2868,3566,2526,1321,3816, 909,2418,5046,4211, 933, #  528\n3817,4212,2053,2361,1222,4524, 765,2419,1322, 786,4525,5047,1920,1462,1677,2909, #  544\n1699,5048,4526,1424,2442,3140,3690,2600,3353,1775,1941,3460,3983,4213, 309,1369, #  560\n1130,2825, 364,2234,1653,1299,3984,3567,3985,3986,2656, 525,1085,3041, 902,2001, #  576\n1475, 964,4527, 421,1845,1415,1057,2286, 940,1364,3141, 376,4528,4529,1381,   7, #  592\n2527, 983,2383, 336,1710,2684,1846, 321,3461, 559,1131,3042,2752,1809,1132,1313, #  608\n 265,1481,1858,5049, 352,1203,2826,3280, 167,1089, 420,2827, 776, 792,1724,3568, #  624\n4214,2443,3281,5050,4215,5051, 446, 229, 333,2753, 901,3818,1200,1557,4530,2657, #  640\n1921, 395,2754,2685,3819,4216,1836, 125, 916,3209,2626,4531,5052,5053,3820,5054, #  656\n5055,5056,4532,3142,3691,1133,2555,1757,3462,1510,2318,1409,3569,5057,2146, 438, #  672\n2601,2910,2384,3354,1068, 958,3043, 461, 311,2869,2686,4217,1916,3210,4218,1979, #  688\n 383, 750,2755,2627,4219, 274, 539, 385,1278,1442,5058,1154,1965, 384, 561, 210, #  704\n  98,1295,2556,3570,5059,1711,2420,1482,3463,3987,2911,1257, 129,5060,3821, 642, #  720\n 523,2789,2790,2658,5061, 141,2235,1333,  68, 176, 441, 876, 907,4220, 603,2602, #  736\n 710, 171,3464, 404, 549,  18,3143,2398,1410,3692,1666,5062,3571,4533,2912,4534, #  752\n5063,2991, 368,5064, 146, 366,  99, 871,3693,1543, 748, 807,1586,1185,  22,2263, #  768\n 379,3822,3211,5065,3212, 505,1942,2628,1992,1382,2319,5066, 380,2362, 218, 702, #  784\n1818,1248,3465,3044,3572,3355,3282,5067,2992,3694, 930,3283,3823,5068,  59,5069, #  800\n 585, 601,4221, 497,3466,1112,1314,4535,1802,5070,1223,1472,2177,5071, 749,1837, #  816\n 690,1900,3824,1773,3988,1476, 429,1043,1791,2236,2117, 917,4222, 447,1086,1629, #  832\n5072, 556,5073,5074,2021,1654, 844,1090, 105, 550, 966,1758,2828,1008,1783, 686, #  848\n1095,5075,2287, 793,1602,5076,3573,2603,4536,4223,2948,2302,4537,3825, 980,2503, #  864\n 544, 353, 527,4538, 908,2687,2913,5077, 381,2629,1943,1348,5078,1341,1252, 560, #  880\n3095,5079,3467,2870,5080,2054, 973, 886,2081, 143,4539,5081,5082, 157,3989, 496, #  896\n4224,  57, 840, 540,2039,4540,4541,3468,2118,1445, 970,2264,1748,1966,2082,4225, #  912\n3144,1234,1776,3284,2829,3695, 773,1206,2130,1066,2040,1326,3990,1738,1725,4226, #  928\n 279,3145,  51,1544,2604, 423,1578,2131,2067, 173,4542,1880,5083,5084,1583, 264, #  944\n 610,3696,4543,2444, 280, 154,5085,5086,5087,1739, 338,1282,3096, 693,2871,1411, #  960\n1074,3826,2445,5088,4544,5089,5090,1240, 952,2399,5091,2914,1538,2688, 685,1483, #  976\n4227,2475,1436, 953,4228,2055,4545, 671,2400,  79,4229,2446,3285, 608, 567,2689, #  992\n3469,4230,4231,1691, 393,1261,1792,2401,5092,4546,5093,5094,5095,5096,1383,1672, # 1008\n3827,3213,1464, 522,1119, 661,1150, 216, 675,4547,3991,1432,3574, 609,4548,2690, # 1024\n2402,5097,5098,5099,4232,3045,   0,5100,2476, 315, 231,2447, 301,3356,4549,2385, # 1040\n5101, 233,4233,3697,1819,4550,4551,5102,  96,1777,1315,2083,5103, 257,5104,1810, # 1056\n3698,2718,1139,1820,4234,2022,1124,2164,2791,1778,2659,5105,3097, 363,1655,3214, # 1072\n5106,2993,5107,5108,5109,3992,1567,3993, 718, 103,3215, 849,1443, 341,3357,2949, # 1088\n1484,5110,1712, 127,  67, 339,4235,2403, 679,1412, 821,5111,5112, 834, 738, 351, # 1104\n2994,2147, 846, 235,1497,1881, 418,1993,3828,2719, 186,1100,2148,2756,3575,1545, # 1120\n1355,2950,2872,1377, 583,3994,4236,2581,2995,5113,1298,3699,1078,2557,3700,2363, # 1136\n  78,3829,3830, 267,1289,2100,2002,1594,4237, 348, 369,1274,2197,2178,1838,4552, # 1152\n1821,2830,3701,2757,2288,2003,4553,2951,2758, 144,3358, 882,4554,3995,2759,3470, # 1168\n4555,2915,5114,4238,1726, 320,5115,3996,3046, 788,2996,5116,2831,1774,1327,2873, # 1184\n3997,2832,5117,1306,4556,2004,1700,3831,3576,2364,2660, 787,2023, 506, 824,3702, # 1200\n 534, 323,4557,1044,3359,2024,1901, 946,3471,5118,1779,1500,1678,5119,1882,4558, # 1216\n 165, 243,4559,3703,2528, 123, 683,4239, 764,4560,  36,3998,1793, 589,2916, 816, # 1232\n 626,1667,3047,2237,1639,1555,1622,3832,3999,5120,4000,2874,1370,1228,1933, 891, # 1248\n2084,2917, 304,4240,5121, 292,2997,2720,3577, 691,2101,4241,1115,4561, 118, 662, # 1264\n5122, 611,1156, 854,2386,1316,2875,   2, 386, 515,2918,5123,5124,3286, 868,2238, # 1280\n1486, 855,2661, 785,2216,3048,5125,1040,3216,3578,5126,3146, 448,5127,1525,5128, # 1296\n2165,4562,5129,3833,5130,4242,2833,3579,3147, 503, 818,4001,3148,1568, 814, 676, # 1312\n1444, 306,1749,5131,3834,1416,1030, 197,1428, 805,2834,1501,4563,5132,5133,5134, # 1328\n1994,5135,4564,5136,5137,2198,  13,2792,3704,2998,3149,1229,1917,5138,3835,2132, # 1344\n5139,4243,4565,2404,3580,5140,2217,1511,1727,1120,5141,5142, 646,3836,2448, 307, # 1360\n5143,5144,1595,3217,5145,5146,5147,3705,1113,1356,4002,1465,2529,2530,5148, 519, # 1376\n5149, 128,2133,  92,2289,1980,5150,4003,1512, 342,3150,2199,5151,2793,2218,1981, # 1392\n3360,4244, 290,1656,1317, 789, 827,2365,5152,3837,4566, 562, 581,4004,5153, 401, # 1408\n4567,2252,  94,4568,5154,1399,2794,5155,1463,2025,4569,3218,1944,5156, 828,1105, # 1424\n4245,1262,1394,5157,4246, 605,4570,5158,1784,2876,5159,2835, 819,2102, 578,2200, # 1440\n2952,5160,1502, 436,3287,4247,3288,2836,4005,2919,3472,3473,5161,2721,2320,5162, # 1456\n5163,2337,2068,  23,4571, 193, 826,3838,2103, 699,1630,4248,3098, 390,1794,1064, # 1472\n3581,5164,1579,3099,3100,1400,5165,4249,1839,1640,2877,5166,4572,4573, 137,4250, # 1488\n 598,3101,1967, 780, 104, 974,2953,5167, 278, 899, 253, 402, 572, 504, 493,1339, # 1504\n5168,4006,1275,4574,2582,2558,5169,3706,3049,3102,2253, 565,1334,2722, 863,  41, # 1520\n5170,5171,4575,5172,1657,2338,  19, 463,2760,4251, 606,5173,2999,3289,1087,2085, # 1536\n1323,2662,3000,5174,1631,1623,1750,4252,2691,5175,2878, 791,2723,2663,2339, 232, # 1552\n2421,5176,3001,1498,5177,2664,2630, 755,1366,3707,3290,3151,2026,1609, 119,1918, # 1568\n3474, 862,1026,4253,5178,4007,3839,4576,4008,4577,2265,1952,2477,5179,1125, 817, # 1584\n4254,4255,4009,1513,1766,2041,1487,4256,3050,3291,2837,3840,3152,5180,5181,1507, # 1600\n5182,2692, 733,  40,1632,1106,2879, 345,4257, 841,2531, 230,4578,3002,1847,3292, # 1616\n3475,5183,1263, 986,3476,5184, 735, 879, 254,1137, 857, 622,1300,1180,1388,1562, # 1632\n4010,4011,2954, 967,2761,2665,1349, 592,2134,1692,3361,3003,1995,4258,1679,4012, # 1648\n1902,2188,5185, 739,3708,2724,1296,1290,5186,4259,2201,2202,1922,1563,2605,2559, # 1664\n1871,2762,3004,5187, 435,5188, 343,1108, 596,  17,1751,4579,2239,3477,3709,5189, # 1680\n4580, 294,3582,2955,1693, 477, 979, 281,2042,3583, 643,2043,3710,2631,2795,2266, # 1696\n1031,2340,2135,2303,3584,4581, 367,1249,2560,5190,3585,5191,4582,1283,3362,2005, # 1712\n 240,1762,3363,4583,4584, 836,1069,3153, 474,5192,2149,2532, 268,3586,5193,3219, # 1728\n1521,1284,5194,1658,1546,4260,5195,3587,3588,5196,4261,3364,2693,1685,4262, 961, # 1744\n1673,2632, 190,2006,2203,3841,4585,4586,5197, 570,2504,3711,1490,5198,4587,2633, # 1760\n3293,1957,4588, 584,1514, 396,1045,1945,5199,4589,1968,2449,5200,5201,4590,4013, # 1776\n 619,5202,3154,3294, 215,2007,2796,2561,3220,4591,3221,4592, 763,4263,3842,4593, # 1792\n5203,5204,1958,1767,2956,3365,3712,1174, 452,1477,4594,3366,3155,5205,2838,1253, # 1808\n2387,2189,1091,2290,4264, 492,5206, 638,1169,1825,2136,1752,4014, 648, 926,1021, # 1824\n1324,4595, 520,4596, 997, 847,1007, 892,4597,3843,2267,1872,3713,2405,1785,4598, # 1840\n1953,2957,3103,3222,1728,4265,2044,3714,4599,2008,1701,3156,1551,  30,2268,4266, # 1856\n5207,2027,4600,3589,5208, 501,5209,4267, 594,3478,2166,1822,3590,3479,3591,3223, # 1872\n 829,2839,4268,5210,1680,3157,1225,4269,5211,3295,4601,4270,3158,2341,5212,4602, # 1888\n4271,5213,4015,4016,5214,1848,2388,2606,3367,5215,4603, 374,4017, 652,4272,4273, # 1904\n 375,1140, 798,5216,5217,5218,2366,4604,2269, 546,1659, 138,3051,2450,4605,5219, # 1920\n2254, 612,1849, 910, 796,3844,1740,1371, 825,3845,3846,5220,2920,2562,5221, 692, # 1936\n 444,3052,2634, 801,4606,4274,5222,1491, 244,1053,3053,4275,4276, 340,5223,4018, # 1952\n1041,3005, 293,1168,  87,1357,5224,1539, 959,5225,2240, 721, 694,4277,3847, 219, # 1968\n1478, 644,1417,3368,2666,1413,1401,1335,1389,4019,5226,5227,3006,2367,3159,1826, # 1984\n 730,1515, 184,2840,  66,4607,5228,1660,2958, 246,3369, 378,1457, 226,3480, 975, # 2000\n4020,2959,1264,3592, 674, 696,5229, 163,5230,1141,2422,2167, 713,3593,3370,4608, # 2016\n4021,5231,5232,1186,  15,5233,1079,1070,5234,1522,3224,3594, 276,1050,2725, 758, # 2032\n1126, 653,2960,3296,5235,2342, 889,3595,4022,3104,3007, 903,1250,4609,4023,3481, # 2048\n3596,1342,1681,1718, 766,3297, 286,  89,2961,3715,5236,1713,5237,2607,3371,3008, # 2064\n5238,2962,2219,3225,2880,5239,4610,2505,2533, 181, 387,1075,4024, 731,2190,3372, # 2080\n5240,3298, 310, 313,3482,2304, 770,4278,  54,3054, 189,4611,3105,3848,4025,5241, # 2096\n1230,1617,1850, 355,3597,4279,4612,3373, 111,4280,3716,1350,3160,3483,3055,4281, # 2112\n2150,3299,3598,5242,2797,4026,4027,3009, 722,2009,5243,1071, 247,1207,2343,2478, # 2128\n1378,4613,2010, 864,1437,1214,4614, 373,3849,1142,2220, 667,4615, 442,2763,2563, # 2144\n3850,4028,1969,4282,3300,1840, 837, 170,1107, 934,1336,1883,5244,5245,2119,4283, # 2160\n2841, 743,1569,5246,4616,4284, 582,2389,1418,3484,5247,1803,5248, 357,1395,1729, # 2176\n3717,3301,2423,1564,2241,5249,3106,3851,1633,4617,1114,2086,4285,1532,5250, 482, # 2192\n2451,4618,5251,5252,1492, 833,1466,5253,2726,3599,1641,2842,5254,1526,1272,3718, # 2208\n4286,1686,1795, 416,2564,1903,1954,1804,5255,3852,2798,3853,1159,2321,5256,2881, # 2224\n4619,1610,1584,3056,2424,2764, 443,3302,1163,3161,5257,5258,4029,5259,4287,2506, # 2240\n3057,4620,4030,3162,2104,1647,3600,2011,1873,4288,5260,4289, 431,3485,5261, 250, # 2256\n  97,  81,4290,5262,1648,1851,1558, 160, 848,5263, 866, 740,1694,5264,2204,2843, # 2272\n3226,4291,4621,3719,1687, 950,2479, 426, 469,3227,3720,3721,4031,5265,5266,1188, # 2288\n 424,1996, 861,3601,4292,3854,2205,2694, 168,1235,3602,4293,5267,2087,1674,4622, # 2304\n3374,3303, 220,2565,1009,5268,3855, 670,3010, 332,1208, 717,5269,5270,3603,2452, # 2320\n4032,3375,5271, 513,5272,1209,2882,3376,3163,4623,1080,5273,5274,5275,5276,2534, # 2336\n3722,3604, 815,1587,4033,4034,5277,3605,3486,3856,1254,4624,1328,3058,1390,4035, # 2352\n1741,4036,3857,4037,5278, 236,3858,2453,3304,5279,5280,3723,3859,1273,3860,4625, # 2368\n5281, 308,5282,4626, 245,4627,1852,2480,1307,2583, 430, 715,2137,2454,5283, 270, # 2384\n 199,2883,4038,5284,3606,2727,1753, 761,1754, 725,1661,1841,4628,3487,3724,5285, # 2400\n5286, 587,  14,3305, 227,2608, 326, 480,2270, 943,2765,3607, 291, 650,1884,5287, # 2416\n1702,1226, 102,1547,  62,3488, 904,4629,3489,1164,4294,5288,5289,1224,1548,2766, # 2432\n 391, 498,1493,5290,1386,1419,5291,2056,1177,4630, 813, 880,1081,2368, 566,1145, # 2448\n4631,2291,1001,1035,2566,2609,2242, 394,1286,5292,5293,2069,5294,  86,1494,1730, # 2464\n4039, 491,1588, 745, 897,2963, 843,3377,4040,2767,2884,3306,1768, 998,2221,2070, # 2480\n 397,1827,1195,1970,3725,3011,3378, 284,5295,3861,2507,2138,2120,1904,5296,4041, # 2496\n2151,4042,4295,1036,3490,1905, 114,2567,4296, 209,1527,5297,5298,2964,2844,2635, # 2512\n2390,2728,3164, 812,2568,5299,3307,5300,1559, 737,1885,3726,1210, 885,  28,2695, # 2528\n3608,3862,5301,4297,1004,1780,4632,5302, 346,1982,2222,2696,4633,3863,1742, 797, # 2544\n1642,4043,1934,1072,1384,2152, 896,4044,3308,3727,3228,2885,3609,5303,2569,1959, # 2560\n4634,2455,1786,5304,5305,5306,4045,4298,1005,1308,3728,4299,2729,4635,4636,1528, # 2576\n2610, 161,1178,4300,1983, 987,4637,1101,4301, 631,4046,1157,3229,2425,1343,1241, # 2592\n1016,2243,2570, 372, 877,2344,2508,1160, 555,1935, 911,4047,5307, 466,1170, 169, # 2608\n1051,2921,2697,3729,2481,3012,1182,2012,2571,1251,2636,5308, 992,2345,3491,1540, # 2624\n2730,1201,2071,2406,1997,2482,5309,4638, 528,1923,2191,1503,1874,1570,2369,3379, # 2640\n3309,5310, 557,1073,5311,1828,3492,2088,2271,3165,3059,3107, 767,3108,2799,4639, # 2656\n1006,4302,4640,2346,1267,2179,3730,3230, 778,4048,3231,2731,1597,2667,5312,4641, # 2672\n5313,3493,5314,5315,5316,3310,2698,1433,3311, 131,  95,1504,4049, 723,4303,3166, # 2688\n1842,3610,2768,2192,4050,2028,2105,3731,5317,3013,4051,1218,5318,3380,3232,4052, # 2704\n4304,2584, 248,1634,3864, 912,5319,2845,3732,3060,3865, 654,  53,5320,3014,5321, # 2720\n1688,4642, 777,3494,1032,4053,1425,5322, 191, 820,2121,2846, 971,4643, 931,3233, # 2736\n 135, 664, 783,3866,1998, 772,2922,1936,4054,3867,4644,2923,3234, 282,2732, 640, # 2752\n1372,3495,1127, 922, 325,3381,5323,5324, 711,2045,5325,5326,4055,2223,2800,1937, # 2768\n4056,3382,2224,2255,3868,2305,5327,4645,3869,1258,3312,4057,3235,2139,2965,4058, # 2784\n4059,5328,2225, 258,3236,4646, 101,1227,5329,3313,1755,5330,1391,3314,5331,2924, # 2800\n2057, 893,5332,5333,5334,1402,4305,2347,5335,5336,3237,3611,5337,5338, 878,1325, # 2816\n1781,2801,4647, 259,1385,2585, 744,1183,2272,4648,5339,4060,2509,5340, 684,1024, # 2832\n4306,5341, 472,3612,3496,1165,3315,4061,4062, 322,2153, 881, 455,1695,1152,1340, # 2848\n 660, 554,2154,4649,1058,4650,4307, 830,1065,3383,4063,4651,1924,5342,1703,1919, # 2864\n5343, 932,2273, 122,5344,4652, 947, 677,5345,3870,2637, 297,1906,1925,2274,4653, # 2880\n2322,3316,5346,5347,4308,5348,4309,  84,4310, 112, 989,5349, 547,1059,4064, 701, # 2896\n3613,1019,5350,4311,5351,3497, 942, 639, 457,2306,2456, 993,2966, 407, 851, 494, # 2912\n4654,3384, 927,5352,1237,5353,2426,3385, 573,4312, 680, 921,2925,1279,1875, 285, # 2928\n 790,1448,1984, 719,2168,5354,5355,4655,4065,4066,1649,5356,1541, 563,5357,1077, # 2944\n5358,3386,3061,3498, 511,3015,4067,4068,3733,4069,1268,2572,3387,3238,4656,4657, # 2960\n5359, 535,1048,1276,1189,2926,2029,3167,1438,1373,2847,2967,1134,2013,5360,4313, # 2976\n1238,2586,3109,1259,5361, 700,5362,2968,3168,3734,4314,5363,4315,1146,1876,1907, # 2992\n4658,2611,4070, 781,2427, 132,1589, 203, 147, 273,2802,2407, 898,1787,2155,4071, # 3008\n4072,5364,3871,2803,5365,5366,4659,4660,5367,3239,5368,1635,3872, 965,5369,1805, # 3024\n2699,1516,3614,1121,1082,1329,3317,4073,1449,3873,  65,1128,2848,2927,2769,1590, # 3040\n3874,5370,5371,  12,2668,  45, 976,2587,3169,4661, 517,2535,1013,1037,3240,5372, # 3056\n3875,2849,5373,3876,5374,3499,5375,2612, 614,1999,2323,3877,3110,2733,2638,5376, # 3072\n2588,4316, 599,1269,5377,1811,3735,5378,2700,3111, 759,1060, 489,1806,3388,3318, # 3088\n1358,5379,5380,2391,1387,1215,2639,2256, 490,5381,5382,4317,1759,2392,2348,5383, # 3104\n4662,3878,1908,4074,2640,1807,3241,4663,3500,3319,2770,2349, 874,5384,5385,3501, # 3120\n3736,1859,  91,2928,3737,3062,3879,4664,5386,3170,4075,2669,5387,3502,1202,1403, # 3136\n3880,2969,2536,1517,2510,4665,3503,2511,5388,4666,5389,2701,1886,1495,1731,4076, # 3152\n2370,4667,5390,2030,5391,5392,4077,2702,1216, 237,2589,4318,2324,4078,3881,4668, # 3168\n4669,2703,3615,3504, 445,4670,5393,5394,5395,5396,2771,  61,4079,3738,1823,4080, # 3184\n5397, 687,2046, 935, 925, 405,2670, 703,1096,1860,2734,4671,4081,1877,1367,2704, # 3200\n3389, 918,2106,1782,2483, 334,3320,1611,1093,4672, 564,3171,3505,3739,3390, 945, # 3216\n2641,2058,4673,5398,1926, 872,4319,5399,3506,2705,3112, 349,4320,3740,4082,4674, # 3232\n3882,4321,3741,2156,4083,4675,4676,4322,4677,2408,2047, 782,4084, 400, 251,4323, # 3248\n1624,5400,5401, 277,3742, 299,1265, 476,1191,3883,2122,4324,4325,1109, 205,5402, # 3264\n2590,1000,2157,3616,1861,5403,5404,5405,4678,5406,4679,2573, 107,2484,2158,4085, # 3280\n3507,3172,5407,1533, 541,1301, 158, 753,4326,2886,3617,5408,1696, 370,1088,4327, # 3296\n4680,3618, 579, 327, 440, 162,2244, 269,1938,1374,3508, 968,3063,  56,1396,3113, # 3312\n2107,3321,3391,5409,1927,2159,4681,3016,5410,3619,5411,5412,3743,4682,2485,5413, # 3328\n2804,5414,1650,4683,5415,2613,5416,5417,4086,2671,3392,1149,3393,4087,3884,4088, # 3344\n5418,1076,  49,5419, 951,3242,3322,3323, 450,2850, 920,5420,1812,2805,2371,4328, # 3360\n1909,1138,2372,3885,3509,5421,3243,4684,1910,1147,1518,2428,4685,3886,5422,4686, # 3376\n2393,2614, 260,1796,3244,5423,5424,3887,3324, 708,5425,3620,1704,5426,3621,1351, # 3392\n1618,3394,3017,1887, 944,4329,3395,4330,3064,3396,4331,5427,3744, 422, 413,1714, # 3408\n3325, 500,2059,2350,4332,2486,5428,1344,1911, 954,5429,1668,5430,5431,4089,2409, # 3424\n4333,3622,3888,4334,5432,2307,1318,2512,3114, 133,3115,2887,4687, 629,  31,2851, # 3440\n2706,3889,4688, 850, 949,4689,4090,2970,1732,2089,4335,1496,1853,5433,4091, 620, # 3456\n3245, 981,1242,3745,3397,1619,3746,1643,3326,2140,2457,1971,1719,3510,2169,5434, # 3472\n3246,5435,5436,3398,1829,5437,1277,4690,1565,2048,5438,1636,3623,3116,5439, 869, # 3488\n2852, 655,3890,3891,3117,4092,3018,3892,1310,3624,4691,5440,5441,5442,1733, 558, # 3504\n4692,3747, 335,1549,3065,1756,4336,3748,1946,3511,1830,1291,1192, 470,2735,2108, # 3520\n2806, 913,1054,4093,5443,1027,5444,3066,4094,4693, 982,2672,3399,3173,3512,3247, # 3536\n3248,1947,2807,5445, 571,4694,5446,1831,5447,3625,2591,1523,2429,5448,2090, 984, # 3552\n4695,3749,1960,5449,3750, 852, 923,2808,3513,3751, 969,1519, 999,2049,2325,1705, # 3568\n5450,3118, 615,1662, 151, 597,4095,2410,2326,1049, 275,4696,3752,4337, 568,3753, # 3584\n3626,2487,4338,3754,5451,2430,2275, 409,3249,5452,1566,2888,3514,1002, 769,2853, # 3600\n 194,2091,3174,3755,2226,3327,4339, 628,1505,5453,5454,1763,2180,3019,4096, 521, # 3616\n1161,2592,1788,2206,2411,4697,4097,1625,4340,4341, 412,  42,3119, 464,5455,2642, # 3632\n4698,3400,1760,1571,2889,3515,2537,1219,2207,3893,2643,2141,2373,4699,4700,3328, # 3648\n1651,3401,3627,5456,5457,3628,2488,3516,5458,3756,5459,5460,2276,2092, 460,5461, # 3664\n4701,5462,3020, 962, 588,3629, 289,3250,2644,1116,  52,5463,3067,1797,5464,5465, # 3680\n5466,1467,5467,1598,1143,3757,4342,1985,1734,1067,4702,1280,3402, 465,4703,1572, # 3696\n 510,5468,1928,2245,1813,1644,3630,5469,4704,3758,5470,5471,2673,1573,1534,5472, # 3712\n5473, 536,1808,1761,3517,3894,3175,2645,5474,5475,5476,4705,3518,2929,1912,2809, # 3728\n5477,3329,1122, 377,3251,5478, 360,5479,5480,4343,1529, 551,5481,2060,3759,1769, # 3744\n2431,5482,2930,4344,3330,3120,2327,2109,2031,4706,1404, 136,1468,1479, 672,1171, # 3760\n3252,2308, 271,3176,5483,2772,5484,2050, 678,2736, 865,1948,4707,5485,2014,4098, # 3776\n2971,5486,2737,2227,1397,3068,3760,4708,4709,1735,2931,3403,3631,5487,3895, 509, # 3792\n2854,2458,2890,3896,5488,5489,3177,3178,4710,4345,2538,4711,2309,1166,1010, 552, # 3808\n 681,1888,5490,5491,2972,2973,4099,1287,1596,1862,3179, 358, 453, 736, 175, 478, # 3824\n1117, 905,1167,1097,5492,1854,1530,5493,1706,5494,2181,3519,2292,3761,3520,3632, # 3840\n4346,2093,4347,5495,3404,1193,2489,4348,1458,2193,2208,1863,1889,1421,3331,2932, # 3856\n3069,2182,3521, 595,2123,5496,4100,5497,5498,4349,1707,2646, 223,3762,1359, 751, # 3872\n3121, 183,3522,5499,2810,3021, 419,2374, 633, 704,3897,2394, 241,5500,5501,5502, # 3888\n 838,3022,3763,2277,2773,2459,3898,1939,2051,4101,1309,3122,2246,1181,5503,1136, # 3904\n2209,3899,2375,1446,4350,2310,4712,5504,5505,4351,1055,2615, 484,3764,5506,4102, # 3920\n 625,4352,2278,3405,1499,4353,4103,5507,4104,4354,3253,2279,2280,3523,5508,5509, # 3936\n2774, 808,2616,3765,3406,4105,4355,3123,2539, 526,3407,3900,4356, 955,5510,1620, # 3952\n4357,2647,2432,5511,1429,3766,1669,1832, 994, 928,5512,3633,1260,5513,5514,5515, # 3968\n1949,2293, 741,2933,1626,4358,2738,2460, 867,1184, 362,3408,1392,5516,5517,4106, # 3984\n4359,1770,1736,3254,2934,4713,4714,1929,2707,1459,1158,5518,3070,3409,2891,1292, # 4000\n1930,2513,2855,3767,1986,1187,2072,2015,2617,4360,5519,2574,2514,2170,3768,2490, # 4016\n3332,5520,3769,4715,5521,5522, 666,1003,3023,1022,3634,4361,5523,4716,1814,2257, # 4032\n 574,3901,1603, 295,1535, 705,3902,4362, 283, 858, 417,5524,5525,3255,4717,4718, # 4048\n3071,1220,1890,1046,2281,2461,4107,1393,1599, 689,2575, 388,4363,5526,2491, 802, # 4064\n5527,2811,3903,2061,1405,2258,5528,4719,3904,2110,1052,1345,3256,1585,5529, 809, # 4080\n5530,5531,5532, 575,2739,3524, 956,1552,1469,1144,2328,5533,2329,1560,2462,3635, # 4096\n3257,4108, 616,2210,4364,3180,2183,2294,5534,1833,5535,3525,4720,5536,1319,3770, # 4112\n3771,1211,3636,1023,3258,1293,2812,5537,5538,5539,3905, 607,2311,3906, 762,2892, # 4128\n1439,4365,1360,4721,1485,3072,5540,4722,1038,4366,1450,2062,2648,4367,1379,4723, # 4144\n2593,5541,5542,4368,1352,1414,2330,2935,1172,5543,5544,3907,3908,4724,1798,1451, # 4160\n5545,5546,5547,5548,2936,4109,4110,2492,2351, 411,4111,4112,3637,3333,3124,4725, # 4176\n1561,2674,1452,4113,1375,5549,5550,  47,2974, 316,5551,1406,1591,2937,3181,5552, # 4192\n1025,2142,3125,3182, 354,2740, 884,2228,4369,2412, 508,3772, 726,3638, 996,2433, # 4208\n3639, 729,5553, 392,2194,1453,4114,4726,3773,5554,5555,2463,3640,2618,1675,2813, # 4224\n 919,2352,2975,2353,1270,4727,4115,  73,5556,5557, 647,5558,3259,2856,2259,1550, # 4240\n1346,3024,5559,1332, 883,3526,5560,5561,5562,5563,3334,2775,5564,1212, 831,1347, # 4256\n4370,4728,2331,3909,1864,3073, 720,3910,4729,4730,3911,5565,4371,5566,5567,4731, # 4272\n5568,5569,1799,4732,3774,2619,4733,3641,1645,2376,4734,5570,2938, 669,2211,2675, # 4288\n2434,5571,2893,5572,5573,1028,3260,5574,4372,2413,5575,2260,1353,5576,5577,4735, # 4304\n3183, 518,5578,4116,5579,4373,1961,5580,2143,4374,5581,5582,3025,2354,2355,3912, # 4320\n 516,1834,1454,4117,2708,4375,4736,2229,2620,1972,1129,3642,5583,2776,5584,2976, # 4336\n1422, 577,1470,3026,1524,3410,5585,5586, 432,4376,3074,3527,5587,2594,1455,2515, # 4352\n2230,1973,1175,5588,1020,2741,4118,3528,4737,5589,2742,5590,1743,1361,3075,3529, # 4368\n2649,4119,4377,4738,2295, 895, 924,4378,2171, 331,2247,3076, 166,1627,3077,1098, # 4384\n5591,1232,2894,2231,3411,4739, 657, 403,1196,2377, 542,3775,3412,1600,4379,3530, # 4400\n5592,4740,2777,3261, 576, 530,1362,4741,4742,2540,2676,3776,4120,5593, 842,3913, # 4416\n5594,2814,2032,1014,4121, 213,2709,3413, 665, 621,4380,5595,3777,2939,2435,5596, # 4432\n2436,3335,3643,3414,4743,4381,2541,4382,4744,3644,1682,4383,3531,1380,5597, 724, # 4448\n2282, 600,1670,5598,1337,1233,4745,3126,2248,5599,1621,4746,5600, 651,4384,5601, # 4464\n1612,4385,2621,5602,2857,5603,2743,2312,3078,5604, 716,2464,3079, 174,1255,2710, # 4480\n4122,3645, 548,1320,1398, 728,4123,1574,5605,1891,1197,3080,4124,5606,3081,3082, # 4496\n3778,3646,3779, 747,5607, 635,4386,4747,5608,5609,5610,4387,5611,5612,4748,5613, # 4512\n3415,4749,2437, 451,5614,3780,2542,2073,4388,2744,4389,4125,5615,1764,4750,5616, # 4528\n4390, 350,4751,2283,2395,2493,5617,4391,4126,2249,1434,4127, 488,4752, 458,4392, # 4544\n4128,3781, 771,1330,2396,3914,2576,3184,2160,2414,1553,2677,3185,4393,5618,2494, # 4560\n2895,2622,1720,2711,4394,3416,4753,5619,2543,4395,5620,3262,4396,2778,5621,2016, # 4576\n2745,5622,1155,1017,3782,3915,5623,3336,2313, 201,1865,4397,1430,5624,4129,5625, # 4592\n5626,5627,5628,5629,4398,1604,5630, 414,1866, 371,2595,4754,4755,3532,2017,3127, # 4608\n4756,1708, 960,4399, 887, 389,2172,1536,1663,1721,5631,2232,4130,2356,2940,1580, # 4624\n5632,5633,1744,4757,2544,4758,4759,5634,4760,5635,2074,5636,4761,3647,3417,2896, # 4640\n4400,5637,4401,2650,3418,2815, 673,2712,2465, 709,3533,4131,3648,4402,5638,1148, # 4656\n 502, 634,5639,5640,1204,4762,3649,1575,4763,2623,3783,5641,3784,3128, 948,3263, # 4672\n 121,1745,3916,1110,5642,4403,3083,2516,3027,4132,3785,1151,1771,3917,1488,4133, # 4688\n1987,5643,2438,3534,5644,5645,2094,5646,4404,3918,1213,1407,2816, 531,2746,2545, # 4704\n3264,1011,1537,4764,2779,4405,3129,1061,5647,3786,3787,1867,2897,5648,2018, 120, # 4720\n4406,4407,2063,3650,3265,2314,3919,2678,3419,1955,4765,4134,5649,3535,1047,2713, # 4736\n1266,5650,1368,4766,2858, 649,3420,3920,2546,2747,1102,2859,2679,5651,5652,2000, # 4752\n5653,1111,3651,2977,5654,2495,3921,3652,2817,1855,3421,3788,5655,5656,3422,2415, # 4768\n2898,3337,3266,3653,5657,2577,5658,3654,2818,4135,1460, 856,5659,3655,5660,2899, # 4784\n2978,5661,2900,3922,5662,4408, 632,2517, 875,3923,1697,3924,2296,5663,5664,4767, # 4800\n3028,1239, 580,4768,4409,5665, 914, 936,2075,1190,4136,1039,2124,5666,5667,5668, # 4816\n5669,3423,1473,5670,1354,4410,3925,4769,2173,3084,4137, 915,3338,4411,4412,3339, # 4832\n1605,1835,5671,2748, 398,3656,4413,3926,4138, 328,1913,2860,4139,3927,1331,4414, # 4848\n3029, 937,4415,5672,3657,4140,4141,3424,2161,4770,3425, 524, 742, 538,3085,1012, # 4864\n5673,5674,3928,2466,5675, 658,1103, 225,3929,5676,5677,4771,5678,4772,5679,3267, # 4880\n1243,5680,4142, 963,2250,4773,5681,2714,3658,3186,5682,5683,2596,2332,5684,4774, # 4896\n5685,5686,5687,3536, 957,3426,2547,2033,1931,2941,2467, 870,2019,3659,1746,2780, # 4912\n2781,2439,2468,5688,3930,5689,3789,3130,3790,3537,3427,3791,5690,1179,3086,5691, # 4928\n3187,2378,4416,3792,2548,3188,3131,2749,4143,5692,3428,1556,2549,2297, 977,2901, # 4944\n2034,4144,1205,3429,5693,1765,3430,3189,2125,1271, 714,1689,4775,3538,5694,2333, # 4960\n3931, 533,4417,3660,2184, 617,5695,2469,3340,3539,2315,5696,5697,3190,5698,5699, # 4976\n3932,1988, 618, 427,2651,3540,3431,5700,5701,1244,1690,5702,2819,4418,4776,5703, # 4992\n3541,4777,5704,2284,1576, 473,3661,4419,3432, 972,5705,3662,5706,3087,5707,5708, # 5008\n4778,4779,5709,3793,4145,4146,5710, 153,4780, 356,5711,1892,2902,4420,2144, 408, # 5024\n 803,2357,5712,3933,5713,4421,1646,2578,2518,4781,4782,3934,5714,3935,4422,5715, # 5040\n2416,3433, 752,5716,5717,1962,3341,2979,5718, 746,3030,2470,4783,4423,3794, 698, # 5056\n4784,1893,4424,3663,2550,4785,3664,3936,5719,3191,3434,5720,1824,1302,4147,2715, # 5072\n3937,1974,4425,5721,4426,3192, 823,1303,1288,1236,2861,3542,4148,3435, 774,3938, # 5088\n5722,1581,4786,1304,2862,3939,4787,5723,2440,2162,1083,3268,4427,4149,4428, 344, # 5104\n1173, 288,2316, 454,1683,5724,5725,1461,4788,4150,2597,5726,5727,4789, 985, 894, # 5120\n5728,3436,3193,5729,1914,2942,3795,1989,5730,2111,1975,5731,4151,5732,2579,1194, # 5136\n 425,5733,4790,3194,1245,3796,4429,5734,5735,2863,5736, 636,4791,1856,3940, 760, # 5152\n1800,5737,4430,2212,1508,4792,4152,1894,1684,2298,5738,5739,4793,4431,4432,2213, # 5168\n 479,5740,5741, 832,5742,4153,2496,5743,2980,2497,3797, 990,3132, 627,1815,2652, # 5184\n4433,1582,4434,2126,2112,3543,4794,5744, 799,4435,3195,5745,4795,2113,1737,3031, # 5200\n1018, 543, 754,4436,3342,1676,4796,4797,4154,4798,1489,5746,3544,5747,2624,2903, # 5216\n4155,5748,5749,2981,5750,5751,5752,5753,3196,4799,4800,2185,1722,5754,3269,3270, # 5232\n1843,3665,1715, 481, 365,1976,1857,5755,5756,1963,2498,4801,5757,2127,3666,3271, # 5248\n 433,1895,2064,2076,5758, 602,2750,5759,5760,5761,5762,5763,3032,1628,3437,5764, # 5264\n3197,4802,4156,2904,4803,2519,5765,2551,2782,5766,5767,5768,3343,4804,2905,5769, # 5280\n4805,5770,2864,4806,4807,1221,2982,4157,2520,5771,5772,5773,1868,1990,5774,5775, # 5296\n5776,1896,5777,5778,4808,1897,4158, 318,5779,2095,4159,4437,5780,5781, 485,5782, # 5312\n 938,3941, 553,2680, 116,5783,3942,3667,5784,3545,2681,2783,3438,3344,2820,5785, # 5328\n3668,2943,4160,1747,2944,2983,5786,5787, 207,5788,4809,5789,4810,2521,5790,3033, # 5344\n 890,3669,3943,5791,1878,3798,3439,5792,2186,2358,3440,1652,5793,5794,5795, 941, # 5360\n2299, 208,3546,4161,2020, 330,4438,3944,2906,2499,3799,4439,4811,5796,5797,5798, # 5376\n)\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/big5prober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Communicator client code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .mbcharsetprober import MultiByteCharSetProber\nfrom .codingstatemachine import CodingStateMachine\nfrom .chardistribution import Big5DistributionAnalysis\nfrom .mbcssm import BIG5_SM_MODEL\n\n\nclass Big5Prober(MultiByteCharSetProber):\n    def __init__(self):\n        super(Big5Prober, self).__init__()\n        self.coding_sm = CodingStateMachine(BIG5_SM_MODEL)\n        self.distribution_analyzer = Big5DistributionAnalysis()\n        self.reset()\n\n    @property\n    def charset_name(self):\n        return \"Big5\"\n\n    @property\n    def language(self):\n        return \"Chinese\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/chardistribution.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Communicator client code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .euctwfreq import (EUCTW_CHAR_TO_FREQ_ORDER, EUCTW_TABLE_SIZE,\n                        EUCTW_TYPICAL_DISTRIBUTION_RATIO)\nfrom .euckrfreq import (EUCKR_CHAR_TO_FREQ_ORDER, EUCKR_TABLE_SIZE,\n                        EUCKR_TYPICAL_DISTRIBUTION_RATIO)\nfrom .gb2312freq import (GB2312_CHAR_TO_FREQ_ORDER, GB2312_TABLE_SIZE,\n                         GB2312_TYPICAL_DISTRIBUTION_RATIO)\nfrom .big5freq import (BIG5_CHAR_TO_FREQ_ORDER, BIG5_TABLE_SIZE,\n                       BIG5_TYPICAL_DISTRIBUTION_RATIO)\nfrom .jisfreq import (JIS_CHAR_TO_FREQ_ORDER, JIS_TABLE_SIZE,\n                      JIS_TYPICAL_DISTRIBUTION_RATIO)\n\n\nclass CharDistributionAnalysis(object):\n    ENOUGH_DATA_THRESHOLD = 1024\n    SURE_YES = 0.99\n    SURE_NO = 0.01\n    MINIMUM_DATA_THRESHOLD = 3\n\n    def __init__(self):\n        # Mapping table to get frequency order from char order (get from\n        # GetOrder())\n        self._char_to_freq_order = None\n        self._table_size = None  # Size of above table\n        # This is a constant value which varies from language to language,\n        # used in calculating confidence.  See\n        # http://www.mozilla.org/projects/intl/UniversalCharsetDetection.html\n        # for further detail.\n        self.typical_distribution_ratio = None\n        self._done = None\n        self._total_chars = None\n        self._freq_chars = None\n        self.reset()\n\n    def reset(self):\n        \"\"\"reset analyser, clear any state\"\"\"\n        # If this flag is set to True, detection is done and conclusion has\n        # been made\n        self._done = False\n        self._total_chars = 0  # Total characters encountered\n        # The number of characters whose frequency order is less than 512\n        self._freq_chars = 0\n\n    def feed(self, char, char_len):\n        \"\"\"feed a character with known length\"\"\"\n        if char_len == 2:\n            # we only care about 2-bytes character in our distribution analysis\n            order = self.get_order(char)\n        else:\n            order = -1\n        if order >= 0:\n            self._total_chars += 1\n            # order is valid\n            if order < self._table_size:\n                if 512 > self._char_to_freq_order[order]:\n                    self._freq_chars += 1\n\n    def get_confidence(self):\n        \"\"\"return confidence based on existing data\"\"\"\n        # if we didn't receive any character in our consideration range,\n        # return negative answer\n        if self._total_chars <= 0 or self._freq_chars <= self.MINIMUM_DATA_THRESHOLD:\n            return self.SURE_NO\n\n        if self._total_chars != self._freq_chars:\n            r = (self._freq_chars / ((self._total_chars - self._freq_chars)\n                 * self.typical_distribution_ratio))\n            if r < self.SURE_YES:\n                return r\n\n        # normalize confidence (we don't want to be 100% sure)\n        return self.SURE_YES\n\n    def got_enough_data(self):\n        # It is not necessary to receive all data to draw conclusion.\n        # For charset detection, certain amount of data is enough\n        return self._total_chars > self.ENOUGH_DATA_THRESHOLD\n\n    def get_order(self, byte_str):\n        # We do not handle characters based on the original encoding string,\n        # but convert this encoding string to a number, here called order.\n        # This allows multiple encodings of a language to share one frequency\n        # table.\n        return -1\n\n\nclass EUCTWDistributionAnalysis(CharDistributionAnalysis):\n    def __init__(self):\n        super(EUCTWDistributionAnalysis, self).__init__()\n        self._char_to_freq_order = EUCTW_CHAR_TO_FREQ_ORDER\n        self._table_size = EUCTW_TABLE_SIZE\n        self.typical_distribution_ratio = EUCTW_TYPICAL_DISTRIBUTION_RATIO\n\n    def get_order(self, byte_str):\n        # for euc-TW encoding, we are interested\n        #   first  byte range: 0xc4 -- 0xfe\n        #   second byte range: 0xa1 -- 0xfe\n        # no validation needed here. State machine has done that\n        first_char = byte_str[0]\n        if first_char >= 0xC4:\n            return 94 * (first_char - 0xC4) + byte_str[1] - 0xA1\n        else:\n            return -1\n\n\nclass EUCKRDistributionAnalysis(CharDistributionAnalysis):\n    def __init__(self):\n        super(EUCKRDistributionAnalysis, self).__init__()\n        self._char_to_freq_order = EUCKR_CHAR_TO_FREQ_ORDER\n        self._table_size = EUCKR_TABLE_SIZE\n        self.typical_distribution_ratio = EUCKR_TYPICAL_DISTRIBUTION_RATIO\n\n    def get_order(self, byte_str):\n        # for euc-KR encoding, we are interested\n        #   first  byte range: 0xb0 -- 0xfe\n        #   second byte range: 0xa1 -- 0xfe\n        # no validation needed here. State machine has done that\n        first_char = byte_str[0]\n        if first_char >= 0xB0:\n            return 94 * (first_char - 0xB0) + byte_str[1] - 0xA1\n        else:\n            return -1\n\n\nclass GB2312DistributionAnalysis(CharDistributionAnalysis):\n    def __init__(self):\n        super(GB2312DistributionAnalysis, self).__init__()\n        self._char_to_freq_order = GB2312_CHAR_TO_FREQ_ORDER\n        self._table_size = GB2312_TABLE_SIZE\n        self.typical_distribution_ratio = GB2312_TYPICAL_DISTRIBUTION_RATIO\n\n    def get_order(self, byte_str):\n        # for GB2312 encoding, we are interested\n        #  first  byte range: 0xb0 -- 0xfe\n        #  second byte range: 0xa1 -- 0xfe\n        # no validation needed here. State machine has done that\n        first_char, second_char = byte_str[0], byte_str[1]\n        if (first_char >= 0xB0) and (second_char >= 0xA1):\n            return 94 * (first_char - 0xB0) + second_char - 0xA1\n        else:\n            return -1\n\n\nclass Big5DistributionAnalysis(CharDistributionAnalysis):\n    def __init__(self):\n        super(Big5DistributionAnalysis, self).__init__()\n        self._char_to_freq_order = BIG5_CHAR_TO_FREQ_ORDER\n        self._table_size = BIG5_TABLE_SIZE\n        self.typical_distribution_ratio = BIG5_TYPICAL_DISTRIBUTION_RATIO\n\n    def get_order(self, byte_str):\n        # for big5 encoding, we are interested\n        #   first  byte range: 0xa4 -- 0xfe\n        #   second byte range: 0x40 -- 0x7e , 0xa1 -- 0xfe\n        # no validation needed here. State machine has done that\n        first_char, second_char = byte_str[0], byte_str[1]\n        if first_char >= 0xA4:\n            if second_char >= 0xA1:\n                return 157 * (first_char - 0xA4) + second_char - 0xA1 + 63\n            else:\n                return 157 * (first_char - 0xA4) + second_char - 0x40\n        else:\n            return -1\n\n\nclass SJISDistributionAnalysis(CharDistributionAnalysis):\n    def __init__(self):\n        super(SJISDistributionAnalysis, self).__init__()\n        self._char_to_freq_order = JIS_CHAR_TO_FREQ_ORDER\n        self._table_size = JIS_TABLE_SIZE\n        self.typical_distribution_ratio = JIS_TYPICAL_DISTRIBUTION_RATIO\n\n    def get_order(self, byte_str):\n        # for sjis encoding, we are interested\n        #   first  byte range: 0x81 -- 0x9f , 0xe0 -- 0xfe\n        #   second byte range: 0x40 -- 0x7e,  0x81 -- oxfe\n        # no validation needed here. State machine has done that\n        first_char, second_char = byte_str[0], byte_str[1]\n        if (first_char >= 0x81) and (first_char <= 0x9F):\n            order = 188 * (first_char - 0x81)\n        elif (first_char >= 0xE0) and (first_char <= 0xEF):\n            order = 188 * (first_char - 0xE0 + 31)\n        else:\n            return -1\n        order = order + second_char - 0x40\n        if second_char > 0x7F:\n            order = -1\n        return order\n\n\nclass EUCJPDistributionAnalysis(CharDistributionAnalysis):\n    def __init__(self):\n        super(EUCJPDistributionAnalysis, self).__init__()\n        self._char_to_freq_order = JIS_CHAR_TO_FREQ_ORDER\n        self._table_size = JIS_TABLE_SIZE\n        self.typical_distribution_ratio = JIS_TYPICAL_DISTRIBUTION_RATIO\n\n    def get_order(self, byte_str):\n        # for euc-JP encoding, we are interested\n        #   first  byte range: 0xa0 -- 0xfe\n        #   second byte range: 0xa1 -- 0xfe\n        # no validation needed here. State machine has done that\n        char = byte_str[0]\n        if char >= 0xA0:\n            return 94 * (char - 0xA1) + byte_str[1] - 0xa1\n        else:\n            return -1\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/charsetgroupprober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Communicator client code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .enums import ProbingState\nfrom .charsetprober import CharSetProber\n\n\nclass CharSetGroupProber(CharSetProber):\n    def __init__(self, lang_filter=None):\n        super(CharSetGroupProber, self).__init__(lang_filter=lang_filter)\n        self._active_num = 0\n        self.probers = []\n        self._best_guess_prober = None\n\n    def reset(self):\n        super(CharSetGroupProber, self).reset()\n        self._active_num = 0\n        for prober in self.probers:\n            if prober:\n                prober.reset()\n                prober.active = True\n                self._active_num += 1\n        self._best_guess_prober = None\n\n    @property\n    def charset_name(self):\n        if not self._best_guess_prober:\n            self.get_confidence()\n            if not self._best_guess_prober:\n                return None\n        return self._best_guess_prober.charset_name\n\n    @property\n    def language(self):\n        if not self._best_guess_prober:\n            self.get_confidence()\n            if not self._best_guess_prober:\n                return None\n        return self._best_guess_prober.language\n\n    def feed(self, byte_str):\n        for prober in self.probers:\n            if not prober:\n                continue\n            if not prober.active:\n                continue\n            state = prober.feed(byte_str)\n            if not state:\n                continue\n            if state == ProbingState.FOUND_IT:\n                self._best_guess_prober = prober\n                self._state = ProbingState.FOUND_IT\n                return self.state\n            elif state == ProbingState.NOT_ME:\n                prober.active = False\n                self._active_num -= 1\n                if self._active_num <= 0:\n                    self._state = ProbingState.NOT_ME\n                    return self.state\n        return self.state\n\n    def get_confidence(self):\n        state = self.state\n        if state == ProbingState.FOUND_IT:\n            return 0.99\n        elif state == ProbingState.NOT_ME:\n            return 0.01\n        best_conf = 0.0\n        self._best_guess_prober = None\n        for prober in self.probers:\n            if not prober:\n                continue\n            if not prober.active:\n                self.logger.debug('%s not active', prober.charset_name)\n                continue\n            conf = prober.get_confidence()\n            self.logger.debug('%s %s confidence = %s', prober.charset_name, prober.language, conf)\n            if best_conf < conf:\n                best_conf = conf\n                self._best_guess_prober = prober\n        if not self._best_guess_prober:\n            return 0.0\n        return best_conf\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/charsetprober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Universal charset detector code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 2001\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#   Shy Shalom - original C code\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nimport logging\nimport re\n\nfrom .enums import ProbingState\n\n\nclass CharSetProber(object):\n\n    SHORTCUT_THRESHOLD = 0.95\n\n    def __init__(self, lang_filter=None):\n        self._state = None\n        self.lang_filter = lang_filter\n        self.logger = logging.getLogger(__name__)\n\n    def reset(self):\n        self._state = ProbingState.DETECTING\n\n    @property\n    def charset_name(self):\n        return None\n\n    def feed(self, buf):\n        pass\n\n    @property\n    def state(self):\n        return self._state\n\n    def get_confidence(self):\n        return 0.0\n\n    @staticmethod\n    def filter_high_byte_only(buf):\n        buf = re.sub(b'([\\x00-\\x7F])+', b' ', buf)\n        return buf\n\n    @staticmethod\n    def filter_international_words(buf):\n        \"\"\"\n        We define three types of bytes:\n        alphabet: english alphabets [a-zA-Z]\n        international: international characters [\\x80-\\xFF]\n        marker: everything else [^a-zA-Z\\x80-\\xFF]\n\n        The input buffer can be thought to contain a series of words delimited\n        by markers. This function works to filter all words that contain at\n        least one international character. All contiguous sequences of markers\n        are replaced by a single space ascii character.\n\n        This filter applies to all scripts which do not use English characters.\n        \"\"\"\n        filtered = bytearray()\n\n        # This regex expression filters out only words that have at-least one\n        # international character. The word may include one marker character at\n        # the end.\n        words = re.findall(b'[a-zA-Z]*[\\x80-\\xFF]+[a-zA-Z]*[^a-zA-Z\\x80-\\xFF]?',\n                           buf)\n\n        for word in words:\n            filtered.extend(word[:-1])\n\n            # If the last character in the word is a marker, replace it with a\n            # space as markers shouldn't affect our analysis (they are used\n            # similarly across all languages and may thus have similar\n            # frequencies).\n            last_char = word[-1:]\n            if not last_char.isalpha() and last_char < b'\\x80':\n                last_char = b' '\n            filtered.extend(last_char)\n\n        return filtered\n\n    @staticmethod\n    def filter_with_english_letters(buf):\n        \"\"\"\n        Returns a copy of ``buf`` that retains only the sequences of English\n        alphabet and high byte characters that are not between <> characters.\n        Also retains English alphabet and high byte characters immediately\n        before occurrences of >.\n\n        This filter can be applied to all scripts which contain both English\n        characters and extended ASCII characters, but is currently only used by\n        ``Latin1Prober``.\n        \"\"\"\n        filtered = bytearray()\n        in_tag = False\n        prev = 0\n\n        for curr in range(len(buf)):\n            # Slice here to get bytes instead of an int with Python 3\n            buf_char = buf[curr:curr + 1]\n            # Check if we're coming out of or entering an HTML tag\n            if buf_char == b'>':\n                in_tag = False\n            elif buf_char == b'<':\n                in_tag = True\n\n            # If current character is not extended-ASCII and not alphabetic...\n            if buf_char < b'\\x80' and not buf_char.isalpha():\n                # ...and we're not in a tag\n                if curr > prev and not in_tag:\n                    # Keep everything after last non-extended-ASCII,\n                    # non-alphabetic character\n                    filtered.extend(buf[prev:curr])\n                    # Output a space to delimit stretch we kept\n                    filtered.extend(b' ')\n                prev = curr + 1\n\n        # If we're not in a tag...\n        if not in_tag:\n            # Keep everything after last non-extended-ASCII, non-alphabetic\n            # character\n            filtered.extend(buf[prev:])\n\n        return filtered\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/cli/__init__.py",
    "content": "\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/cli/chardetect.py",
    "content": "\"\"\"\nScript which takes one or more file paths and reports on their detected\nencodings\n\nExample::\n\n    % chardetect somefile someotherfile\n    somefile: windows-1252 with confidence 0.5\n    someotherfile: ascii with confidence 1.0\n\nIf no paths are provided, it takes its input from stdin.\n\n\"\"\"\n\nfrom __future__ import absolute_import, print_function, unicode_literals\n\nimport argparse\nimport sys\n\nfrom chardet import __version__\nfrom chardet.compat import PY2\nfrom chardet.universaldetector import UniversalDetector\n\n\ndef description_of(lines, name='stdin'):\n    \"\"\"\n    Return a string describing the probable encoding of a file or\n    list of strings.\n\n    :param lines: The lines to get the encoding of.\n    :type lines: Iterable of bytes\n    :param name: Name of file or collection of lines\n    :type name: str\n    \"\"\"\n    u = UniversalDetector()\n    for line in lines:\n        line = bytearray(line)\n        u.feed(line)\n        # shortcut out of the loop to save reading further - particularly useful if we read a BOM.\n        if u.done:\n            break\n    u.close()\n    result = u.result\n    if PY2:\n        name = name.decode(sys.getfilesystemencoding(), 'ignore')\n    if result['encoding']:\n        return '{}: {} with confidence {}'.format(name, result['encoding'],\n                                                     result['confidence'])\n    else:\n        return '{}: no result'.format(name)\n\n\ndef main(argv=None):\n    \"\"\"\n    Handles command line arguments and gets things started.\n\n    :param argv: List of arguments, as if specified on the command-line.\n                 If None, ``sys.argv[1:]`` is used instead.\n    :type argv: list of str\n    \"\"\"\n    # Get command line arguments\n    parser = argparse.ArgumentParser(\n        description=\"Takes one or more file paths and reports their detected \\\n                     encodings\")\n    parser.add_argument('input',\n                        help='File whose encoding we would like to determine. \\\n                              (default: stdin)',\n                        type=argparse.FileType('rb'), nargs='*',\n                        default=[sys.stdin if PY2 else sys.stdin.buffer])\n    parser.add_argument('--version', action='version',\n                        version='%(prog)s {}'.format(__version__))\n    args = parser.parse_args(argv)\n\n    for f in args.input:\n        if f.isatty():\n            print(\"You are running chardetect interactively. Press \" +\n                  \"CTRL-D twice at the start of a blank line to signal the \" +\n                  \"end of your input. If you want help, run chardetect \" +\n                  \"--help\\n\", file=sys.stderr)\n        print(description_of(f, f.name))\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/codingstatemachine.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is mozilla.org code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nimport logging\n\nfrom .enums import MachineState\n\n\nclass CodingStateMachine(object):\n    \"\"\"\n    A state machine to verify a byte sequence for a particular encoding. For\n    each byte the detector receives, it will feed that byte to every active\n    state machine available, one byte at a time. The state machine changes its\n    state based on its previous state and the byte it receives. There are 3\n    states in a state machine that are of interest to an auto-detector:\n\n    START state: This is the state to start with, or a legal byte sequence\n                 (i.e. a valid code point) for character has been identified.\n\n    ME state:  This indicates that the state machine identified a byte sequence\n               that is specific to the charset it is designed for and that\n               there is no other possible encoding which can contain this byte\n               sequence. This will to lead to an immediate positive answer for\n               the detector.\n\n    ERROR state: This indicates the state machine identified an illegal byte\n                 sequence for that encoding. This will lead to an immediate\n                 negative answer for this encoding. Detector will exclude this\n                 encoding from consideration from here on.\n    \"\"\"\n    def __init__(self, sm):\n        self._model = sm\n        self._curr_byte_pos = 0\n        self._curr_char_len = 0\n        self._curr_state = None\n        self.logger = logging.getLogger(__name__)\n        self.reset()\n\n    def reset(self):\n        self._curr_state = MachineState.START\n\n    def next_state(self, c):\n        # for each byte we get its class\n        # if it is first byte, we also get byte length\n        byte_class = self._model['class_table'][c]\n        if self._curr_state == MachineState.START:\n            self._curr_byte_pos = 0\n            self._curr_char_len = self._model['char_len_table'][byte_class]\n        # from byte's class and state_table, we get its next state\n        curr_state = (self._curr_state * self._model['class_factor']\n                      + byte_class)\n        self._curr_state = self._model['state_table'][curr_state]\n        self._curr_byte_pos += 1\n        return self._curr_state\n\n    def get_current_charlen(self):\n        return self._curr_char_len\n\n    def get_coding_state_machine(self):\n        return self._model['name']\n\n    @property\n    def language(self):\n        return self._model['language']\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/compat.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# Contributor(s):\n#   Dan Blanchard\n#   Ian Cordasco\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nimport sys\n\n\nif sys.version_info < (3, 0):\n    PY2 = True\n    PY3 = False\n    string_types = (str, unicode)\n    text_type = unicode\n    iteritems = dict.iteritems\nelse:\n    PY2 = False\n    PY3 = True\n    string_types = (bytes, str)\n    text_type = str\n    iteritems = dict.items\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/cp949prober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is mozilla.org code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .chardistribution import EUCKRDistributionAnalysis\nfrom .codingstatemachine import CodingStateMachine\nfrom .mbcharsetprober import MultiByteCharSetProber\nfrom .mbcssm import CP949_SM_MODEL\n\n\nclass CP949Prober(MultiByteCharSetProber):\n    def __init__(self):\n        super(CP949Prober, self).__init__()\n        self.coding_sm = CodingStateMachine(CP949_SM_MODEL)\n        # NOTE: CP949 is a superset of EUC-KR, so the distribution should be\n        #       not different.\n        self.distribution_analyzer = EUCKRDistributionAnalysis()\n        self.reset()\n\n    @property\n    def charset_name(self):\n        return \"CP949\"\n\n    @property\n    def language(self):\n        return \"Korean\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/enums.py",
    "content": "\"\"\"\nAll of the Enums that are used throughout the chardet package.\n\n:author: Dan Blanchard (dan.blanchard@gmail.com)\n\"\"\"\n\n\nclass InputState(object):\n    \"\"\"\n    This enum represents the different states a universal detector can be in.\n    \"\"\"\n    PURE_ASCII = 0\n    ESC_ASCII = 1\n    HIGH_BYTE = 2\n\n\nclass LanguageFilter(object):\n    \"\"\"\n    This enum represents the different language filters we can apply to a\n    ``UniversalDetector``.\n    \"\"\"\n    CHINESE_SIMPLIFIED = 0x01\n    CHINESE_TRADITIONAL = 0x02\n    JAPANESE = 0x04\n    KOREAN = 0x08\n    NON_CJK = 0x10\n    ALL = 0x1F\n    CHINESE = CHINESE_SIMPLIFIED | CHINESE_TRADITIONAL\n    CJK = CHINESE | JAPANESE | KOREAN\n\n\nclass ProbingState(object):\n    \"\"\"\n    This enum represents the different states a prober can be in.\n    \"\"\"\n    DETECTING = 0\n    FOUND_IT = 1\n    NOT_ME = 2\n\n\nclass MachineState(object):\n    \"\"\"\n    This enum represents the different states a state machine can be in.\n    \"\"\"\n    START = 0\n    ERROR = 1\n    ITS_ME = 2\n\n\nclass SequenceLikelihood(object):\n    \"\"\"\n    This enum represents the likelihood of a character following the previous one.\n    \"\"\"\n    NEGATIVE = 0\n    UNLIKELY = 1\n    LIKELY = 2\n    POSITIVE = 3\n\n    @classmethod\n    def get_num_categories(cls):\n        \"\"\":returns: The number of likelihood categories in the enum.\"\"\"\n        return 4\n\n\nclass CharacterCategory(object):\n    \"\"\"\n    This enum represents the different categories language models for\n    ``SingleByteCharsetProber`` put characters into.\n\n    Anything less than CONTROL is considered a letter.\n    \"\"\"\n    UNDEFINED = 255\n    LINE_BREAK = 254\n    SYMBOL = 253\n    DIGIT = 252\n    CONTROL = 251\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/escprober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is mozilla.org code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .charsetprober import CharSetProber\nfrom .codingstatemachine import CodingStateMachine\nfrom .enums import LanguageFilter, ProbingState, MachineState\nfrom .escsm import (HZ_SM_MODEL, ISO2022CN_SM_MODEL, ISO2022JP_SM_MODEL,\n                    ISO2022KR_SM_MODEL)\n\n\nclass EscCharSetProber(CharSetProber):\n    \"\"\"\n    This CharSetProber uses a \"code scheme\" approach for detecting encodings,\n    whereby easily recognizable escape or shift sequences are relied on to\n    identify these encodings.\n    \"\"\"\n\n    def __init__(self, lang_filter=None):\n        super(EscCharSetProber, self).__init__(lang_filter=lang_filter)\n        self.coding_sm = []\n        if self.lang_filter & LanguageFilter.CHINESE_SIMPLIFIED:\n            self.coding_sm.append(CodingStateMachine(HZ_SM_MODEL))\n            self.coding_sm.append(CodingStateMachine(ISO2022CN_SM_MODEL))\n        if self.lang_filter & LanguageFilter.JAPANESE:\n            self.coding_sm.append(CodingStateMachine(ISO2022JP_SM_MODEL))\n        if self.lang_filter & LanguageFilter.KOREAN:\n            self.coding_sm.append(CodingStateMachine(ISO2022KR_SM_MODEL))\n        self.active_sm_count = None\n        self._detected_charset = None\n        self._detected_language = None\n        self._state = None\n        self.reset()\n\n    def reset(self):\n        super(EscCharSetProber, self).reset()\n        for coding_sm in self.coding_sm:\n            if not coding_sm:\n                continue\n            coding_sm.active = True\n            coding_sm.reset()\n        self.active_sm_count = len(self.coding_sm)\n        self._detected_charset = None\n        self._detected_language = None\n\n    @property\n    def charset_name(self):\n        return self._detected_charset\n\n    @property\n    def language(self):\n        return self._detected_language\n\n    def get_confidence(self):\n        if self._detected_charset:\n            return 0.99\n        else:\n            return 0.00\n\n    def feed(self, byte_str):\n        for c in byte_str:\n            for coding_sm in self.coding_sm:\n                if not coding_sm or not coding_sm.active:\n                    continue\n                coding_state = coding_sm.next_state(c)\n                if coding_state == MachineState.ERROR:\n                    coding_sm.active = False\n                    self.active_sm_count -= 1\n                    if self.active_sm_count <= 0:\n                        self._state = ProbingState.NOT_ME\n                        return self.state\n                elif coding_state == MachineState.ITS_ME:\n                    self._state = ProbingState.FOUND_IT\n                    self._detected_charset = coding_sm.get_coding_state_machine()\n                    self._detected_language = coding_sm.language\n                    return self.state\n\n        return self.state\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/escsm.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is mozilla.org code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .enums import MachineState\n\nHZ_CLS = (\n1,0,0,0,0,0,0,0,  # 00 - 07\n0,0,0,0,0,0,0,0,  # 08 - 0f\n0,0,0,0,0,0,0,0,  # 10 - 17\n0,0,0,1,0,0,0,0,  # 18 - 1f\n0,0,0,0,0,0,0,0,  # 20 - 27\n0,0,0,0,0,0,0,0,  # 28 - 2f\n0,0,0,0,0,0,0,0,  # 30 - 37\n0,0,0,0,0,0,0,0,  # 38 - 3f\n0,0,0,0,0,0,0,0,  # 40 - 47\n0,0,0,0,0,0,0,0,  # 48 - 4f\n0,0,0,0,0,0,0,0,  # 50 - 57\n0,0,0,0,0,0,0,0,  # 58 - 5f\n0,0,0,0,0,0,0,0,  # 60 - 67\n0,0,0,0,0,0,0,0,  # 68 - 6f\n0,0,0,0,0,0,0,0,  # 70 - 77\n0,0,0,4,0,5,2,0,  # 78 - 7f\n1,1,1,1,1,1,1,1,  # 80 - 87\n1,1,1,1,1,1,1,1,  # 88 - 8f\n1,1,1,1,1,1,1,1,  # 90 - 97\n1,1,1,1,1,1,1,1,  # 98 - 9f\n1,1,1,1,1,1,1,1,  # a0 - a7\n1,1,1,1,1,1,1,1,  # a8 - af\n1,1,1,1,1,1,1,1,  # b0 - b7\n1,1,1,1,1,1,1,1,  # b8 - bf\n1,1,1,1,1,1,1,1,  # c0 - c7\n1,1,1,1,1,1,1,1,  # c8 - cf\n1,1,1,1,1,1,1,1,  # d0 - d7\n1,1,1,1,1,1,1,1,  # d8 - df\n1,1,1,1,1,1,1,1,  # e0 - e7\n1,1,1,1,1,1,1,1,  # e8 - ef\n1,1,1,1,1,1,1,1,  # f0 - f7\n1,1,1,1,1,1,1,1,  # f8 - ff\n)\n\nHZ_ST = (\nMachineState.START,MachineState.ERROR,     3,MachineState.START,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,# 00-07\nMachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,# 08-0f\nMachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,     4,MachineState.ERROR,# 10-17\n     5,MachineState.ERROR,     6,MachineState.ERROR,     5,     5,     4,MachineState.ERROR,# 18-1f\n     4,MachineState.ERROR,     4,     4,     4,MachineState.ERROR,     4,MachineState.ERROR,# 20-27\n     4,MachineState.ITS_ME,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,# 28-2f\n)\n\nHZ_CHAR_LEN_TABLE = (0, 0, 0, 0, 0, 0)\n\nHZ_SM_MODEL = {'class_table': HZ_CLS,\n               'class_factor': 6,\n               'state_table': HZ_ST,\n               'char_len_table': HZ_CHAR_LEN_TABLE,\n               'name': \"HZ-GB-2312\",\n               'language': 'Chinese'}\n\nISO2022CN_CLS = (\n2,0,0,0,0,0,0,0,  # 00 - 07\n0,0,0,0,0,0,0,0,  # 08 - 0f\n0,0,0,0,0,0,0,0,  # 10 - 17\n0,0,0,1,0,0,0,0,  # 18 - 1f\n0,0,0,0,0,0,0,0,  # 20 - 27\n0,3,0,0,0,0,0,0,  # 28 - 2f\n0,0,0,0,0,0,0,0,  # 30 - 37\n0,0,0,0,0,0,0,0,  # 38 - 3f\n0,0,0,4,0,0,0,0,  # 40 - 47\n0,0,0,0,0,0,0,0,  # 48 - 4f\n0,0,0,0,0,0,0,0,  # 50 - 57\n0,0,0,0,0,0,0,0,  # 58 - 5f\n0,0,0,0,0,0,0,0,  # 60 - 67\n0,0,0,0,0,0,0,0,  # 68 - 6f\n0,0,0,0,0,0,0,0,  # 70 - 77\n0,0,0,0,0,0,0,0,  # 78 - 7f\n2,2,2,2,2,2,2,2,  # 80 - 87\n2,2,2,2,2,2,2,2,  # 88 - 8f\n2,2,2,2,2,2,2,2,  # 90 - 97\n2,2,2,2,2,2,2,2,  # 98 - 9f\n2,2,2,2,2,2,2,2,  # a0 - a7\n2,2,2,2,2,2,2,2,  # a8 - af\n2,2,2,2,2,2,2,2,  # b0 - b7\n2,2,2,2,2,2,2,2,  # b8 - bf\n2,2,2,2,2,2,2,2,  # c0 - c7\n2,2,2,2,2,2,2,2,  # c8 - cf\n2,2,2,2,2,2,2,2,  # d0 - d7\n2,2,2,2,2,2,2,2,  # d8 - df\n2,2,2,2,2,2,2,2,  # e0 - e7\n2,2,2,2,2,2,2,2,  # e8 - ef\n2,2,2,2,2,2,2,2,  # f0 - f7\n2,2,2,2,2,2,2,2,  # f8 - ff\n)\n\nISO2022CN_ST = (\nMachineState.START,     3,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,# 00-07\nMachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 08-0f\nMachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,# 10-17\nMachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,     4,MachineState.ERROR,# 18-1f\nMachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 20-27\n     5,     6,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 28-2f\nMachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 30-37\nMachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,MachineState.START,# 38-3f\n)\n\nISO2022CN_CHAR_LEN_TABLE = (0, 0, 0, 0, 0, 0, 0, 0, 0)\n\nISO2022CN_SM_MODEL = {'class_table': ISO2022CN_CLS,\n                      'class_factor': 9,\n                      'state_table': ISO2022CN_ST,\n                      'char_len_table': ISO2022CN_CHAR_LEN_TABLE,\n                      'name': \"ISO-2022-CN\",\n                      'language': 'Chinese'}\n\nISO2022JP_CLS = (\n2,0,0,0,0,0,0,0,  # 00 - 07\n0,0,0,0,0,0,2,2,  # 08 - 0f\n0,0,0,0,0,0,0,0,  # 10 - 17\n0,0,0,1,0,0,0,0,  # 18 - 1f\n0,0,0,0,7,0,0,0,  # 20 - 27\n3,0,0,0,0,0,0,0,  # 28 - 2f\n0,0,0,0,0,0,0,0,  # 30 - 37\n0,0,0,0,0,0,0,0,  # 38 - 3f\n6,0,4,0,8,0,0,0,  # 40 - 47\n0,9,5,0,0,0,0,0,  # 48 - 4f\n0,0,0,0,0,0,0,0,  # 50 - 57\n0,0,0,0,0,0,0,0,  # 58 - 5f\n0,0,0,0,0,0,0,0,  # 60 - 67\n0,0,0,0,0,0,0,0,  # 68 - 6f\n0,0,0,0,0,0,0,0,  # 70 - 77\n0,0,0,0,0,0,0,0,  # 78 - 7f\n2,2,2,2,2,2,2,2,  # 80 - 87\n2,2,2,2,2,2,2,2,  # 88 - 8f\n2,2,2,2,2,2,2,2,  # 90 - 97\n2,2,2,2,2,2,2,2,  # 98 - 9f\n2,2,2,2,2,2,2,2,  # a0 - a7\n2,2,2,2,2,2,2,2,  # a8 - af\n2,2,2,2,2,2,2,2,  # b0 - b7\n2,2,2,2,2,2,2,2,  # b8 - bf\n2,2,2,2,2,2,2,2,  # c0 - c7\n2,2,2,2,2,2,2,2,  # c8 - cf\n2,2,2,2,2,2,2,2,  # d0 - d7\n2,2,2,2,2,2,2,2,  # d8 - df\n2,2,2,2,2,2,2,2,  # e0 - e7\n2,2,2,2,2,2,2,2,  # e8 - ef\n2,2,2,2,2,2,2,2,  # f0 - f7\n2,2,2,2,2,2,2,2,  # f8 - ff\n)\n\nISO2022JP_ST = (\nMachineState.START,     3,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,# 00-07\nMachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 08-0f\nMachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,# 10-17\nMachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,# 18-1f\nMachineState.ERROR,     5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,     4,MachineState.ERROR,MachineState.ERROR,# 20-27\nMachineState.ERROR,MachineState.ERROR,MachineState.ERROR,     6,MachineState.ITS_ME,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,# 28-2f\nMachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,# 30-37\nMachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 38-3f\nMachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,MachineState.START,MachineState.START,# 40-47\n)\n\nISO2022JP_CHAR_LEN_TABLE = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0)\n\nISO2022JP_SM_MODEL = {'class_table': ISO2022JP_CLS,\n                      'class_factor': 10,\n                      'state_table': ISO2022JP_ST,\n                      'char_len_table': ISO2022JP_CHAR_LEN_TABLE,\n                      'name': \"ISO-2022-JP\",\n                      'language': 'Japanese'}\n\nISO2022KR_CLS = (\n2,0,0,0,0,0,0,0,  # 00 - 07\n0,0,0,0,0,0,0,0,  # 08 - 0f\n0,0,0,0,0,0,0,0,  # 10 - 17\n0,0,0,1,0,0,0,0,  # 18 - 1f\n0,0,0,0,3,0,0,0,  # 20 - 27\n0,4,0,0,0,0,0,0,  # 28 - 2f\n0,0,0,0,0,0,0,0,  # 30 - 37\n0,0,0,0,0,0,0,0,  # 38 - 3f\n0,0,0,5,0,0,0,0,  # 40 - 47\n0,0,0,0,0,0,0,0,  # 48 - 4f\n0,0,0,0,0,0,0,0,  # 50 - 57\n0,0,0,0,0,0,0,0,  # 58 - 5f\n0,0,0,0,0,0,0,0,  # 60 - 67\n0,0,0,0,0,0,0,0,  # 68 - 6f\n0,0,0,0,0,0,0,0,  # 70 - 77\n0,0,0,0,0,0,0,0,  # 78 - 7f\n2,2,2,2,2,2,2,2,  # 80 - 87\n2,2,2,2,2,2,2,2,  # 88 - 8f\n2,2,2,2,2,2,2,2,  # 90 - 97\n2,2,2,2,2,2,2,2,  # 98 - 9f\n2,2,2,2,2,2,2,2,  # a0 - a7\n2,2,2,2,2,2,2,2,  # a8 - af\n2,2,2,2,2,2,2,2,  # b0 - b7\n2,2,2,2,2,2,2,2,  # b8 - bf\n2,2,2,2,2,2,2,2,  # c0 - c7\n2,2,2,2,2,2,2,2,  # c8 - cf\n2,2,2,2,2,2,2,2,  # d0 - d7\n2,2,2,2,2,2,2,2,  # d8 - df\n2,2,2,2,2,2,2,2,  # e0 - e7\n2,2,2,2,2,2,2,2,  # e8 - ef\n2,2,2,2,2,2,2,2,  # f0 - f7\n2,2,2,2,2,2,2,2,  # f8 - ff\n)\n\nISO2022KR_ST = (\nMachineState.START,     3,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,# 00-07\nMachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,# 08-0f\nMachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,     4,MachineState.ERROR,MachineState.ERROR,# 10-17\nMachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,     5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 18-1f\nMachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.START,MachineState.START,MachineState.START,MachineState.START,# 20-27\n)\n\nISO2022KR_CHAR_LEN_TABLE = (0, 0, 0, 0, 0, 0)\n\nISO2022KR_SM_MODEL = {'class_table': ISO2022KR_CLS,\n                      'class_factor': 6,\n                      'state_table': ISO2022KR_ST,\n                      'char_len_table': ISO2022KR_CHAR_LEN_TABLE,\n                      'name': \"ISO-2022-KR\",\n                      'language': 'Korean'}\n\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/eucjpprober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is mozilla.org code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .enums import ProbingState, MachineState\nfrom .mbcharsetprober import MultiByteCharSetProber\nfrom .codingstatemachine import CodingStateMachine\nfrom .chardistribution import EUCJPDistributionAnalysis\nfrom .jpcntx import EUCJPContextAnalysis\nfrom .mbcssm import EUCJP_SM_MODEL\n\n\nclass EUCJPProber(MultiByteCharSetProber):\n    def __init__(self):\n        super(EUCJPProber, self).__init__()\n        self.coding_sm = CodingStateMachine(EUCJP_SM_MODEL)\n        self.distribution_analyzer = EUCJPDistributionAnalysis()\n        self.context_analyzer = EUCJPContextAnalysis()\n        self.reset()\n\n    def reset(self):\n        super(EUCJPProber, self).reset()\n        self.context_analyzer.reset()\n\n    @property\n    def charset_name(self):\n        return \"EUC-JP\"\n\n    @property\n    def language(self):\n        return \"Japanese\"\n\n    def feed(self, byte_str):\n        for i in range(len(byte_str)):\n            # PY3K: byte_str is a byte array, so byte_str[i] is an int, not a byte\n            coding_state = self.coding_sm.next_state(byte_str[i])\n            if coding_state == MachineState.ERROR:\n                self.logger.debug('%s %s prober hit error at byte %s',\n                                  self.charset_name, self.language, i)\n                self._state = ProbingState.NOT_ME\n                break\n            elif coding_state == MachineState.ITS_ME:\n                self._state = ProbingState.FOUND_IT\n                break\n            elif coding_state == MachineState.START:\n                char_len = self.coding_sm.get_current_charlen()\n                if i == 0:\n                    self._last_char[1] = byte_str[0]\n                    self.context_analyzer.feed(self._last_char, char_len)\n                    self.distribution_analyzer.feed(self._last_char, char_len)\n                else:\n                    self.context_analyzer.feed(byte_str[i - 1:i + 1],\n                                                char_len)\n                    self.distribution_analyzer.feed(byte_str[i - 1:i + 1],\n                                                     char_len)\n\n        self._last_char[0] = byte_str[-1]\n\n        if self.state == ProbingState.DETECTING:\n            if (self.context_analyzer.got_enough_data() and\n               (self.get_confidence() > self.SHORTCUT_THRESHOLD)):\n                self._state = ProbingState.FOUND_IT\n\n        return self.state\n\n    def get_confidence(self):\n        context_conf = self.context_analyzer.get_confidence()\n        distrib_conf = self.distribution_analyzer.get_confidence()\n        return max(context_conf, distrib_conf)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/euckrfreq.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Communicator client code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\n# Sampling from about 20M text materials include literature and computer technology\n\n# 128  --> 0.79\n# 256  --> 0.92\n# 512  --> 0.986\n# 1024 --> 0.99944\n# 2048 --> 0.99999\n#\n# Idea Distribution Ratio = 0.98653 / (1-0.98653) = 73.24\n# Random Distribution Ration = 512 / (2350-512) = 0.279.\n#\n# Typical Distribution Ratio\n\nEUCKR_TYPICAL_DISTRIBUTION_RATIO = 6.0\n\nEUCKR_TABLE_SIZE = 2352\n\n# Char to FreqOrder table ,\nEUCKR_CHAR_TO_FREQ_ORDER = (\n  13, 130, 120,1396, 481,1719,1720, 328, 609, 212,1721, 707, 400, 299,1722,  87,\n1397,1723, 104, 536,1117,1203,1724,1267, 685,1268, 508,1725,1726,1727,1728,1398,\n1399,1729,1730,1731, 141, 621, 326,1057, 368,1732, 267, 488,  20,1733,1269,1734,\n 945,1400,1735,  47, 904,1270,1736,1737, 773, 248,1738, 409, 313, 786, 429,1739,\n 116, 987, 813,1401, 683,  75,1204, 145,1740,1741,1742,1743,  16, 847, 667, 622,\n 708,1744,1745,1746, 966, 787, 304, 129,1747,  60, 820, 123, 676,1748,1749,1750,\n1751, 617,1752, 626,1753,1754,1755,1756, 653,1757,1758,1759,1760,1761,1762, 856,\n 344,1763,1764,1765,1766,  89, 401, 418, 806, 905, 848,1767,1768,1769, 946,1205,\n 709,1770,1118,1771, 241,1772,1773,1774,1271,1775, 569,1776, 999,1777,1778,1779,\n1780, 337, 751,1058,  28, 628, 254,1781, 177, 906, 270, 349, 891,1079,1782,  19,\n1783, 379,1784, 315,1785, 629, 754,1402, 559,1786, 636, 203,1206,1787, 710, 567,\n1788, 935, 814,1789,1790,1207, 766, 528,1791,1792,1208,1793,1794,1795,1796,1797,\n1403,1798,1799, 533,1059,1404,1405,1156,1406, 936, 884,1080,1800, 351,1801,1802,\n1803,1804,1805, 801,1806,1807,1808,1119,1809,1157, 714, 474,1407,1810, 298, 899,\n 885,1811,1120, 802,1158,1812, 892,1813,1814,1408, 659,1815,1816,1121,1817,1818,\n1819,1820,1821,1822, 319,1823, 594, 545,1824, 815, 937,1209,1825,1826, 573,1409,\n1022,1827,1210,1828,1829,1830,1831,1832,1833, 556, 722, 807,1122,1060,1834, 697,\n1835, 900, 557, 715,1836,1410, 540,1411, 752,1159, 294, 597,1211, 976, 803, 770,\n1412,1837,1838,  39, 794,1413, 358,1839, 371, 925,1840, 453, 661, 788, 531, 723,\n 544,1023,1081, 869,  91,1841, 392, 430, 790, 602,1414, 677,1082, 457,1415,1416,\n1842,1843, 475, 327,1024,1417, 795, 121,1844, 733, 403,1418,1845,1846,1847, 300,\n 119, 711,1212, 627,1848,1272, 207,1849,1850, 796,1213, 382,1851, 519,1852,1083,\n 893,1853,1854,1855, 367, 809, 487, 671,1856, 663,1857,1858, 956, 471, 306, 857,\n1859,1860,1160,1084,1861,1862,1863,1864,1865,1061,1866,1867,1868,1869,1870,1871,\n 282,  96, 574,1872, 502,1085,1873,1214,1874, 907,1875,1876, 827, 977,1419,1420,\n1421, 268,1877,1422,1878,1879,1880, 308,1881,   2, 537,1882,1883,1215,1884,1885,\n 127, 791,1886,1273,1423,1887,  34, 336, 404, 643,1888, 571, 654, 894, 840,1889,\n   0, 886,1274, 122, 575, 260, 908, 938,1890,1275, 410, 316,1891,1892, 100,1893,\n1894,1123,  48,1161,1124,1025,1895, 633, 901,1276,1896,1897, 115, 816,1898, 317,\n1899, 694,1900, 909, 734,1424, 572, 866,1425, 691,  85, 524,1010, 543, 394, 841,\n1901,1902,1903,1026,1904,1905,1906,1907,1908,1909,  30, 451, 651, 988, 310,1910,\n1911,1426, 810,1216,  93,1912,1913,1277,1217,1914, 858, 759,  45,  58, 181, 610,\n 269,1915,1916, 131,1062, 551, 443,1000, 821,1427, 957, 895,1086,1917,1918, 375,\n1919, 359,1920, 687,1921, 822,1922, 293,1923,1924,  40, 662, 118, 692,  29, 939,\n 887, 640, 482, 174,1925,  69,1162, 728,1428, 910,1926,1278,1218,1279, 386, 870,\n 217, 854,1163, 823,1927,1928,1929,1930, 834,1931,  78,1932, 859,1933,1063,1934,\n1935,1936,1937, 438,1164, 208, 595,1938,1939,1940,1941,1219,1125,1942, 280, 888,\n1429,1430,1220,1431,1943,1944,1945,1946,1947,1280, 150, 510,1432,1948,1949,1950,\n1951,1952,1953,1954,1011,1087,1955,1433,1043,1956, 881,1957, 614, 958,1064,1065,\n1221,1958, 638,1001, 860, 967, 896,1434, 989, 492, 553,1281,1165,1959,1282,1002,\n1283,1222,1960,1961,1962,1963,  36, 383, 228, 753, 247, 454,1964, 876, 678,1965,\n1966,1284, 126, 464, 490, 835, 136, 672, 529, 940,1088,1435, 473,1967,1968, 467,\n  50, 390, 227, 587, 279, 378, 598, 792, 968, 240, 151, 160, 849, 882,1126,1285,\n 639,1044, 133, 140, 288, 360, 811, 563,1027, 561, 142, 523,1969,1970,1971,   7,\n 103, 296, 439, 407, 506, 634, 990,1972,1973,1974,1975, 645,1976,1977,1978,1979,\n1980,1981, 236,1982,1436,1983,1984,1089, 192, 828, 618, 518,1166, 333,1127,1985,\n 818,1223,1986,1987,1988,1989,1990,1991,1992,1993, 342,1128,1286, 746, 842,1994,\n1995, 560, 223,1287,  98,   8, 189, 650, 978,1288,1996,1437,1997,  17, 345, 250,\n 423, 277, 234, 512, 226,  97, 289,  42, 167,1998, 201,1999,2000, 843, 836, 824,\n 532, 338, 783,1090, 182, 576, 436,1438,1439, 527, 500,2001, 947, 889,2002,2003,\n2004,2005, 262, 600, 314, 447,2006, 547,2007, 693, 738,1129,2008,  71,1440, 745,\n 619, 688,2009, 829,2010,2011, 147,2012,  33, 948,2013,2014,  74, 224,2015,  61,\n 191, 918, 399, 637,2016,1028,1130, 257, 902,2017,2018,2019,2020,2021,2022,2023,\n2024,2025,2026, 837,2027,2028,2029,2030, 179, 874, 591,  52, 724, 246,2031,2032,\n2033,2034,1167, 969,2035,1289, 630, 605, 911,1091,1168,2036,2037,2038,1441, 912,\n2039, 623,2040,2041, 253,1169,1290,2042,1442, 146, 620, 611, 577, 433,2043,1224,\n 719,1170, 959, 440, 437, 534,  84, 388, 480,1131, 159, 220, 198, 679,2044,1012,\n 819,1066,1443, 113,1225, 194, 318,1003,1029,2045,2046,2047,2048,1067,2049,2050,\n2051,2052,2053,  59, 913, 112,2054, 632,2055, 455, 144, 739,1291,2056, 273, 681,\n 499,2057, 448,2058,2059, 760,2060,2061, 970, 384, 169, 245,1132,2062,2063, 414,\n1444,2064,2065,  41, 235,2066, 157, 252, 877, 568, 919, 789, 580,2067, 725,2068,\n2069,1292,2070,2071,1445,2072,1446,2073,2074,  55, 588,  66,1447, 271,1092,2075,\n1226,2076, 960,1013, 372,2077,2078,2079,2080,2081,1293,2082,2083,2084,2085, 850,\n2086,2087,2088,2089,2090, 186,2091,1068, 180,2092,2093,2094, 109,1227, 522, 606,\n2095, 867,1448,1093, 991,1171, 926, 353,1133,2096, 581,2097,2098,2099,1294,1449,\n1450,2100, 596,1172,1014,1228,2101,1451,1295,1173,1229,2102,2103,1296,1134,1452,\n 949,1135,2104,2105,1094,1453,1454,1455,2106,1095,2107,2108,2109,2110,2111,2112,\n2113,2114,2115,2116,2117, 804,2118,2119,1230,1231, 805,1456, 405,1136,2120,2121,\n2122,2123,2124, 720, 701,1297, 992,1457, 927,1004,2125,2126,2127,2128,2129,2130,\n  22, 417,2131, 303,2132, 385,2133, 971, 520, 513,2134,1174,  73,1096, 231, 274,\n 962,1458, 673,2135,1459,2136, 152,1137,2137,2138,2139,2140,1005,1138,1460,1139,\n2141,2142,2143,2144,  11, 374, 844,2145, 154,1232,  46,1461,2146, 838, 830, 721,\n1233, 106,2147,  90, 428, 462, 578, 566,1175, 352,2148,2149, 538,1234, 124,1298,\n2150,1462, 761, 565,2151, 686,2152, 649,2153,  72, 173,2154, 460, 415,2155,1463,\n2156,1235, 305,2157,2158,2159,2160,2161,2162, 579,2163,2164,2165,2166,2167, 747,\n2168,2169,2170,2171,1464, 669,2172,2173,2174,2175,2176,1465,2177,  23, 530, 285,\n2178, 335, 729,2179, 397,2180,2181,2182,1030,2183,2184, 698,2185,2186, 325,2187,\n2188, 369,2189, 799,1097,1015, 348,2190,1069, 680,2191, 851,1466,2192,2193,  10,\n2194, 613, 424,2195, 979, 108, 449, 589,  27, 172,  81,1031,  80, 774, 281, 350,\n1032, 525, 301, 582,1176,2196, 674,1045,2197,2198,1467, 730, 762,2199,2200,2201,\n2202,1468,2203, 993,2204,2205, 266,1070, 963,1140,2206,2207,2208, 664,1098, 972,\n2209,2210,2211,1177,1469,1470, 871,2212,2213,2214,2215,2216,1471,2217,2218,2219,\n2220,2221,2222,2223,2224,2225,2226,2227,1472,1236,2228,2229,2230,2231,2232,2233,\n2234,2235,1299,2236,2237, 200,2238, 477, 373,2239,2240, 731, 825, 777,2241,2242,\n2243, 521, 486, 548,2244,2245,2246,1473,1300,  53, 549, 137, 875,  76, 158,2247,\n1301,1474, 469, 396,1016, 278, 712,2248, 321, 442, 503, 767, 744, 941,1237,1178,\n1475,2249,  82, 178,1141,1179, 973,2250,1302,2251, 297,2252,2253, 570,2254,2255,\n2256,  18, 450, 206,2257, 290, 292,1142,2258, 511, 162,  99, 346, 164, 735,2259,\n1476,1477,   4, 554, 343, 798,1099,2260,1100,2261,  43, 171,1303, 139, 215,2262,\n2263, 717, 775,2264,1033, 322, 216,2265, 831,2266, 149,2267,1304,2268,2269, 702,\n1238, 135, 845, 347, 309,2270, 484,2271, 878, 655, 238,1006,1478,2272,  67,2273,\n 295,2274,2275, 461,2276, 478, 942, 412,2277,1034,2278,2279,2280, 265,2281, 541,\n2282,2283,2284,2285,2286,  70, 852,1071,2287,2288,2289,2290,  21,  56, 509, 117,\n 432,2291,2292, 331, 980, 552,1101, 148, 284, 105, 393,1180,1239, 755,2293, 187,\n2294,1046,1479,2295, 340,2296,  63,1047, 230,2297,2298,1305, 763,1306, 101, 800,\n 808, 494,2299,2300,2301, 903,2302,  37,1072,  14,   5,2303,  79, 675,2304, 312,\n2305,2306,2307,2308,2309,1480,   6,1307,2310,2311,2312,   1, 470,  35,  24, 229,\n2313, 695, 210,  86, 778,  15, 784, 592, 779,  32,  77, 855, 964,2314, 259,2315,\n 501, 380,2316,2317,  83, 981, 153, 689,1308,1481,1482,1483,2318,2319, 716,1484,\n2320,2321,2322,2323,2324,2325,1485,2326,2327, 128,  57,  68, 261,1048, 211, 170,\n1240,  31,2328,  51, 435, 742,2329,2330,2331, 635,2332, 264, 456,2333,2334,2335,\n 425,2336,1486, 143, 507, 263, 943,2337, 363, 920,1487, 256,1488,1102, 243, 601,\n1489,2338,2339,2340,2341,2342,2343,2344, 861,2345,2346,2347,2348,2349,2350, 395,\n2351,1490,1491,  62, 535, 166, 225,2352,2353, 668, 419,1241, 138, 604, 928,2354,\n1181,2355,1492,1493,2356,2357,2358,1143,2359, 696,2360, 387, 307,1309, 682, 476,\n2361,2362, 332,  12, 222, 156,2363, 232,2364, 641, 276, 656, 517,1494,1495,1035,\n 416, 736,1496,2365,1017, 586,2366,2367,2368,1497,2369, 242,2370,2371,2372,1498,\n2373, 965, 713,2374,2375,2376,2377, 740, 982,1499, 944,1500,1007,2378,2379,1310,\n1501,2380,2381,2382, 785, 329,2383,2384,1502,2385,2386,2387, 932,2388,1503,2389,\n2390,2391,2392,1242,2393,2394,2395,2396,2397, 994, 950,2398,2399,2400,2401,1504,\n1311,2402,2403,2404,2405,1049, 749,2406,2407, 853, 718,1144,1312,2408,1182,1505,\n2409,2410, 255, 516, 479, 564, 550, 214,1506,1507,1313, 413, 239, 444, 339,1145,\n1036,1508,1509,1314,1037,1510,1315,2411,1511,2412,2413,2414, 176, 703, 497, 624,\n 593, 921, 302,2415, 341, 165,1103,1512,2416,1513,2417,2418,2419, 376,2420, 700,\n2421,2422,2423, 258, 768,1316,2424,1183,2425, 995, 608,2426,2427,2428,2429, 221,\n2430,2431,2432,2433,2434,2435,2436,2437, 195, 323, 726, 188, 897, 983,1317, 377,\n 644,1050, 879,2438, 452,2439,2440,2441,2442,2443,2444, 914,2445,2446,2447,2448,\n 915, 489,2449,1514,1184,2450,2451, 515,  64, 427, 495,2452, 583,2453, 483, 485,\n1038, 562, 213,1515, 748, 666,2454,2455,2456,2457, 334,2458, 780, 996,1008, 705,\n1243,2459,2460,2461,2462,2463, 114,2464, 493,1146, 366, 163,1516, 961,1104,2465,\n 291,2466,1318,1105,2467,1517, 365,2468, 355, 951,1244,2469,1319,2470, 631,2471,\n2472, 218,1320, 364, 320, 756,1518,1519,1321,1520,1322,2473,2474,2475,2476, 997,\n2477,2478,2479,2480, 665,1185,2481, 916,1521,2482,2483,2484, 584, 684,2485,2486,\n 797,2487,1051,1186,2488,2489,2490,1522,2491,2492, 370,2493,1039,1187,  65,2494,\n 434, 205, 463,1188,2495, 125, 812, 391, 402, 826, 699, 286, 398, 155, 781, 771,\n 585,2496, 590, 505,1073,2497, 599, 244, 219, 917,1018, 952, 646,1523,2498,1323,\n2499,2500,  49, 984, 354, 741,2501, 625,2502,1324,2503,1019, 190, 357, 757, 491,\n  95, 782, 868,2504,2505,2506,2507,2508,2509, 134,1524,1074, 422,1525, 898,2510,\n 161,2511,2512,2513,2514, 769,2515,1526,2516,2517, 411,1325,2518, 472,1527,2519,\n2520,2521,2522,2523,2524, 985,2525,2526,2527,2528,2529,2530, 764,2531,1245,2532,\n2533,  25, 204, 311,2534, 496,2535,1052,2536,2537,2538,2539,2540,2541,2542, 199,\n 704, 504, 468, 758, 657,1528, 196,  44, 839,1246, 272, 750,2543, 765, 862,2544,\n2545,1326,2546, 132, 615, 933,2547, 732,2548,2549,2550,1189,1529,2551, 283,1247,\n1053, 607, 929,2552,2553,2554, 930, 183, 872, 616,1040,1147,2555,1148,1020, 441,\n 249,1075,2556,2557,2558, 466, 743,2559,2560,2561,  92, 514, 426, 420, 526,2562,\n2563,2564,2565,2566,2567,2568, 185,2569,2570,2571,2572, 776,1530, 658,2573, 362,\n2574, 361, 922,1076, 793,2575,2576,2577,2578,2579,2580,1531, 251,2581,2582,2583,\n2584,1532,  54, 612, 237,1327,2585,2586, 275, 408, 647, 111,2587,1533,1106, 465,\n   3, 458,   9,  38,2588, 107, 110, 890, 209,  26, 737, 498,2589,1534,2590, 431,\n 202,  88,1535, 356, 287,1107, 660,1149,2591, 381,1536, 986,1150, 445,1248,1151,\n 974,2592,2593, 846,2594, 446, 953, 184,1249,1250, 727,2595, 923, 193, 883,2596,\n2597,2598, 102, 324, 539, 817,2599, 421,1041,2600, 832,2601,  94, 175, 197, 406,\n2602, 459,2603,2604,2605,2606,2607, 330, 555,2608,2609,2610, 706,1108, 389,2611,\n2612,2613,2614, 233,2615, 833, 558, 931, 954,1251,2616,2617,1537, 546,2618,2619,\n1009,2620,2621,2622,1538, 690,1328,2623, 955,2624,1539,2625,2626, 772,2627,2628,\n2629,2630,2631, 924, 648, 863, 603,2632,2633, 934,1540, 864, 865,2634, 642,1042,\n 670,1190,2635,2636,2637,2638, 168,2639, 652, 873, 542,1054,1541,2640,2641,2642,  # 512, 256\n)\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/euckrprober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is mozilla.org code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .mbcharsetprober import MultiByteCharSetProber\nfrom .codingstatemachine import CodingStateMachine\nfrom .chardistribution import EUCKRDistributionAnalysis\nfrom .mbcssm import EUCKR_SM_MODEL\n\n\nclass EUCKRProber(MultiByteCharSetProber):\n    def __init__(self):\n        super(EUCKRProber, self).__init__()\n        self.coding_sm = CodingStateMachine(EUCKR_SM_MODEL)\n        self.distribution_analyzer = EUCKRDistributionAnalysis()\n        self.reset()\n\n    @property\n    def charset_name(self):\n        return \"EUC-KR\"\n\n    @property\n    def language(self):\n        return \"Korean\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/euctwfreq.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Communicator client code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\n# EUCTW frequency table\n# Converted from big5 work\n# by Taiwan's Mandarin Promotion Council\n# <http:#www.edu.tw:81/mandr/>\n\n# 128  --> 0.42261\n# 256  --> 0.57851\n# 512  --> 0.74851\n# 1024 --> 0.89384\n# 2048 --> 0.97583\n#\n# Idea Distribution Ratio = 0.74851/(1-0.74851) =2.98\n# Random Distribution Ration = 512/(5401-512)=0.105\n#\n# Typical Distribution Ratio about 25% of Ideal one, still much higher than RDR\n\nEUCTW_TYPICAL_DISTRIBUTION_RATIO = 0.75\n\n# Char to FreqOrder table ,\nEUCTW_TABLE_SIZE = 5376\n\nEUCTW_CHAR_TO_FREQ_ORDER = (\n   1,1800,1506, 255,1431, 198,   9,  82,   6,7310, 177, 202,3615,1256,2808, 110,  # 2742\n3735,  33,3241, 261,  76,  44,2113,  16,2931,2184,1176, 659,3868,  26,3404,2643,  # 2758\n1198,3869,3313,4060, 410,2211, 302, 590, 361,1963,   8, 204,  58,4296,7311,1931,  # 2774\n  63,7312,7313, 317,1614,  75, 222, 159,4061,2412,1480,7314,3500,3068, 224,2809,  # 2790\n3616,   3,  10,3870,1471,  29,2774,1135,2852,1939, 873, 130,3242,1123, 312,7315,  # 2806\n4297,2051, 507, 252, 682,7316, 142,1914, 124, 206,2932,  34,3501,3173,  64, 604,  # 2822\n7317,2494,1976,1977, 155,1990, 645, 641,1606,7318,3405, 337,  72, 406,7319,  80,  # 2838\n 630, 238,3174,1509, 263, 939,1092,2644, 756,1440,1094,3406, 449,  69,2969, 591,  # 2854\n 179,2095, 471, 115,2034,1843,  60,  50,2970, 134, 806,1868, 734,2035,3407, 180,  # 2870\n 995,1607, 156, 537,2893, 688,7320, 319,1305, 779,2144, 514,2374, 298,4298, 359,  # 2886\n2495,  90,2707,1338, 663,  11, 906,1099,2545,  20,2436, 182, 532,1716,7321, 732,  # 2902\n1376,4062,1311,1420,3175,  25,2312,1056, 113, 399, 382,1949, 242,3408,2467, 529,  # 2918\n3243, 475,1447,3617,7322, 117,  21, 656, 810,1297,2295,2329,3502,7323, 126,4063,  # 2934\n 706, 456, 150, 613,4299,  71,1118,2036,4064, 145,3069,  85, 835, 486,2114,1246,  # 2950\n1426, 428, 727,1285,1015, 800, 106, 623, 303,1281,7324,2127,2354, 347,3736, 221,  # 2966\n3503,3110,7325,1955,1153,4065,  83, 296,1199,3070, 192, 624,  93,7326, 822,1897,  # 2982\n2810,3111, 795,2064, 991,1554,1542,1592,  27,  43,2853, 859, 139,1456, 860,4300,  # 2998\n 437, 712,3871, 164,2392,3112, 695, 211,3017,2096, 195,3872,1608,3504,3505,3618,  # 3014\n3873, 234, 811,2971,2097,3874,2229,1441,3506,1615,2375, 668,2076,1638, 305, 228,  # 3030\n1664,4301, 467, 415,7327, 262,2098,1593, 239, 108, 300, 200,1033, 512,1247,2077,  # 3046\n7328,7329,2173,3176,3619,2673, 593, 845,1062,3244,  88,1723,2037,3875,1950, 212,  # 3062\n 266, 152, 149, 468,1898,4066,4302,  77, 187,7330,3018,  37,   5,2972,7331,3876,  # 3078\n7332,7333,  39,2517,4303,2894,3177,2078,  55, 148,  74,4304, 545, 483,1474,1029,  # 3094\n1665, 217,1869,1531,3113,1104,2645,4067,  24, 172,3507, 900,3877,3508,3509,4305,  # 3110\n  32,1408,2811,1312, 329, 487,2355,2247,2708, 784,2674,   4,3019,3314,1427,1788,  # 3126\n 188, 109, 499,7334,3620,1717,1789, 888,1217,3020,4306,7335,3510,7336,3315,1520,  # 3142\n3621,3878, 196,1034, 775,7337,7338, 929,1815, 249, 439,  38,7339,1063,7340, 794,  # 3158\n3879,1435,2296,  46, 178,3245,2065,7341,2376,7342, 214,1709,4307, 804,  35, 707,  # 3174\n 324,3622,1601,2546, 140, 459,4068,7343,7344,1365, 839, 272, 978,2257,2572,3409,  # 3190\n2128,1363,3623,1423, 697, 100,3071,  48,  70,1231, 495,3114,2193,7345,1294,7346,  # 3206\n2079, 462, 586,1042,3246, 853, 256, 988, 185,2377,3410,1698, 434,1084,7347,3411,  # 3222\n 314,2615,2775,4308,2330,2331, 569,2280, 637,1816,2518, 757,1162,1878,1616,3412,  # 3238\n 287,1577,2115, 768,4309,1671,2854,3511,2519,1321,3737, 909,2413,7348,4069, 933,  # 3254\n3738,7349,2052,2356,1222,4310, 765,2414,1322, 786,4311,7350,1919,1462,1677,2895,  # 3270\n1699,7351,4312,1424,2437,3115,3624,2590,3316,1774,1940,3413,3880,4070, 309,1369,  # 3286\n1130,2812, 364,2230,1653,1299,3881,3512,3882,3883,2646, 525,1085,3021, 902,2000,  # 3302\n1475, 964,4313, 421,1844,1415,1057,2281, 940,1364,3116, 376,4314,4315,1381,   7,  # 3318\n2520, 983,2378, 336,1710,2675,1845, 321,3414, 559,1131,3022,2742,1808,1132,1313,  # 3334\n 265,1481,1857,7352, 352,1203,2813,3247, 167,1089, 420,2814, 776, 792,1724,3513,  # 3350\n4071,2438,3248,7353,4072,7354, 446, 229, 333,2743, 901,3739,1200,1557,4316,2647,  # 3366\n1920, 395,2744,2676,3740,4073,1835, 125, 916,3178,2616,4317,7355,7356,3741,7357,  # 3382\n7358,7359,4318,3117,3625,1133,2547,1757,3415,1510,2313,1409,3514,7360,2145, 438,  # 3398\n2591,2896,2379,3317,1068, 958,3023, 461, 311,2855,2677,4074,1915,3179,4075,1978,  # 3414\n 383, 750,2745,2617,4076, 274, 539, 385,1278,1442,7361,1154,1964, 384, 561, 210,  # 3430\n  98,1295,2548,3515,7362,1711,2415,1482,3416,3884,2897,1257, 129,7363,3742, 642,  # 3446\n 523,2776,2777,2648,7364, 141,2231,1333,  68, 176, 441, 876, 907,4077, 603,2592,  # 3462\n 710, 171,3417, 404, 549,  18,3118,2393,1410,3626,1666,7365,3516,4319,2898,4320,  # 3478\n7366,2973, 368,7367, 146, 366,  99, 871,3627,1543, 748, 807,1586,1185,  22,2258,  # 3494\n 379,3743,3180,7368,3181, 505,1941,2618,1991,1382,2314,7369, 380,2357, 218, 702,  # 3510\n1817,1248,3418,3024,3517,3318,3249,7370,2974,3628, 930,3250,3744,7371,  59,7372,  # 3526\n 585, 601,4078, 497,3419,1112,1314,4321,1801,7373,1223,1472,2174,7374, 749,1836,  # 3542\n 690,1899,3745,1772,3885,1476, 429,1043,1790,2232,2116, 917,4079, 447,1086,1629,  # 3558\n7375, 556,7376,7377,2020,1654, 844,1090, 105, 550, 966,1758,2815,1008,1782, 686,  # 3574\n1095,7378,2282, 793,1602,7379,3518,2593,4322,4080,2933,2297,4323,3746, 980,2496,  # 3590\n 544, 353, 527,4324, 908,2678,2899,7380, 381,2619,1942,1348,7381,1341,1252, 560,  # 3606\n3072,7382,3420,2856,7383,2053, 973, 886,2080, 143,4325,7384,7385, 157,3886, 496,  # 3622\n4081,  57, 840, 540,2038,4326,4327,3421,2117,1445, 970,2259,1748,1965,2081,4082,  # 3638\n3119,1234,1775,3251,2816,3629, 773,1206,2129,1066,2039,1326,3887,1738,1725,4083,  # 3654\n 279,3120,  51,1544,2594, 423,1578,2130,2066, 173,4328,1879,7386,7387,1583, 264,  # 3670\n 610,3630,4329,2439, 280, 154,7388,7389,7390,1739, 338,1282,3073, 693,2857,1411,  # 3686\n1074,3747,2440,7391,4330,7392,7393,1240, 952,2394,7394,2900,1538,2679, 685,1483,  # 3702\n4084,2468,1436, 953,4085,2054,4331, 671,2395,  79,4086,2441,3252, 608, 567,2680,  # 3718\n3422,4087,4088,1691, 393,1261,1791,2396,7395,4332,7396,7397,7398,7399,1383,1672,  # 3734\n3748,3182,1464, 522,1119, 661,1150, 216, 675,4333,3888,1432,3519, 609,4334,2681,  # 3750\n2397,7400,7401,7402,4089,3025,   0,7403,2469, 315, 231,2442, 301,3319,4335,2380,  # 3766\n7404, 233,4090,3631,1818,4336,4337,7405,  96,1776,1315,2082,7406, 257,7407,1809,  # 3782\n3632,2709,1139,1819,4091,2021,1124,2163,2778,1777,2649,7408,3074, 363,1655,3183,  # 3798\n7409,2975,7410,7411,7412,3889,1567,3890, 718, 103,3184, 849,1443, 341,3320,2934,  # 3814\n1484,7413,1712, 127,  67, 339,4092,2398, 679,1412, 821,7414,7415, 834, 738, 351,  # 3830\n2976,2146, 846, 235,1497,1880, 418,1992,3749,2710, 186,1100,2147,2746,3520,1545,  # 3846\n1355,2935,2858,1377, 583,3891,4093,2573,2977,7416,1298,3633,1078,2549,3634,2358,  # 3862\n  78,3750,3751, 267,1289,2099,2001,1594,4094, 348, 369,1274,2194,2175,1837,4338,  # 3878\n1820,2817,3635,2747,2283,2002,4339,2936,2748, 144,3321, 882,4340,3892,2749,3423,  # 3894\n4341,2901,7417,4095,1726, 320,7418,3893,3026, 788,2978,7419,2818,1773,1327,2859,  # 3910\n3894,2819,7420,1306,4342,2003,1700,3752,3521,2359,2650, 787,2022, 506, 824,3636,  # 3926\n 534, 323,4343,1044,3322,2023,1900, 946,3424,7421,1778,1500,1678,7422,1881,4344,  # 3942\n 165, 243,4345,3637,2521, 123, 683,4096, 764,4346,  36,3895,1792, 589,2902, 816,  # 3958\n 626,1667,3027,2233,1639,1555,1622,3753,3896,7423,3897,2860,1370,1228,1932, 891,  # 3974\n2083,2903, 304,4097,7424, 292,2979,2711,3522, 691,2100,4098,1115,4347, 118, 662,  # 3990\n7425, 611,1156, 854,2381,1316,2861,   2, 386, 515,2904,7426,7427,3253, 868,2234,  # 4006\n1486, 855,2651, 785,2212,3028,7428,1040,3185,3523,7429,3121, 448,7430,1525,7431,  # 4022\n2164,4348,7432,3754,7433,4099,2820,3524,3122, 503, 818,3898,3123,1568, 814, 676,  # 4038\n1444, 306,1749,7434,3755,1416,1030, 197,1428, 805,2821,1501,4349,7435,7436,7437,  # 4054\n1993,7438,4350,7439,7440,2195,  13,2779,3638,2980,3124,1229,1916,7441,3756,2131,  # 4070\n7442,4100,4351,2399,3525,7443,2213,1511,1727,1120,7444,7445, 646,3757,2443, 307,  # 4086\n7446,7447,1595,3186,7448,7449,7450,3639,1113,1356,3899,1465,2522,2523,7451, 519,  # 4102\n7452, 128,2132,  92,2284,1979,7453,3900,1512, 342,3125,2196,7454,2780,2214,1980,  # 4118\n3323,7455, 290,1656,1317, 789, 827,2360,7456,3758,4352, 562, 581,3901,7457, 401,  # 4134\n4353,2248,  94,4354,1399,2781,7458,1463,2024,4355,3187,1943,7459, 828,1105,4101,  # 4150\n1262,1394,7460,4102, 605,4356,7461,1783,2862,7462,2822, 819,2101, 578,2197,2937,  # 4166\n7463,1502, 436,3254,4103,3255,2823,3902,2905,3425,3426,7464,2712,2315,7465,7466,  # 4182\n2332,2067,  23,4357, 193, 826,3759,2102, 699,1630,4104,3075, 390,1793,1064,3526,  # 4198\n7467,1579,3076,3077,1400,7468,4105,1838,1640,2863,7469,4358,4359, 137,4106, 598,  # 4214\n3078,1966, 780, 104, 974,2938,7470, 278, 899, 253, 402, 572, 504, 493,1339,7471,  # 4230\n3903,1275,4360,2574,2550,7472,3640,3029,3079,2249, 565,1334,2713, 863,  41,7473,  # 4246\n7474,4361,7475,1657,2333,  19, 463,2750,4107, 606,7476,2981,3256,1087,2084,1323,  # 4262\n2652,2982,7477,1631,1623,1750,4108,2682,7478,2864, 791,2714,2653,2334, 232,2416,  # 4278\n7479,2983,1498,7480,2654,2620, 755,1366,3641,3257,3126,2025,1609, 119,1917,3427,  # 4294\n 862,1026,4109,7481,3904,3760,4362,3905,4363,2260,1951,2470,7482,1125, 817,4110,  # 4310\n4111,3906,1513,1766,2040,1487,4112,3030,3258,2824,3761,3127,7483,7484,1507,7485,  # 4326\n2683, 733,  40,1632,1106,2865, 345,4113, 841,2524, 230,4364,2984,1846,3259,3428,  # 4342\n7486,1263, 986,3429,7487, 735, 879, 254,1137, 857, 622,1300,1180,1388,1562,3907,  # 4358\n3908,2939, 967,2751,2655,1349, 592,2133,1692,3324,2985,1994,4114,1679,3909,1901,  # 4374\n2185,7488, 739,3642,2715,1296,1290,7489,4115,2198,2199,1921,1563,2595,2551,1870,  # 4390\n2752,2986,7490, 435,7491, 343,1108, 596,  17,1751,4365,2235,3430,3643,7492,4366,  # 4406\n 294,3527,2940,1693, 477, 979, 281,2041,3528, 643,2042,3644,2621,2782,2261,1031,  # 4422\n2335,2134,2298,3529,4367, 367,1249,2552,7493,3530,7494,4368,1283,3325,2004, 240,  # 4438\n1762,3326,4369,4370, 836,1069,3128, 474,7495,2148,2525, 268,3531,7496,3188,1521,  # 4454\n1284,7497,1658,1546,4116,7498,3532,3533,7499,4117,3327,2684,1685,4118, 961,1673,  # 4470\n2622, 190,2005,2200,3762,4371,4372,7500, 570,2497,3645,1490,7501,4373,2623,3260,  # 4486\n1956,4374, 584,1514, 396,1045,1944,7502,4375,1967,2444,7503,7504,4376,3910, 619,  # 4502\n7505,3129,3261, 215,2006,2783,2553,3189,4377,3190,4378, 763,4119,3763,4379,7506,  # 4518\n7507,1957,1767,2941,3328,3646,1174, 452,1477,4380,3329,3130,7508,2825,1253,2382,  # 4534\n2186,1091,2285,4120, 492,7509, 638,1169,1824,2135,1752,3911, 648, 926,1021,1324,  # 4550\n4381, 520,4382, 997, 847,1007, 892,4383,3764,2262,1871,3647,7510,2400,1784,4384,  # 4566\n1952,2942,3080,3191,1728,4121,2043,3648,4385,2007,1701,3131,1551,  30,2263,4122,  # 4582\n7511,2026,4386,3534,7512, 501,7513,4123, 594,3431,2165,1821,3535,3432,3536,3192,  # 4598\n 829,2826,4124,7514,1680,3132,1225,4125,7515,3262,4387,4126,3133,2336,7516,4388,  # 4614\n4127,7517,3912,3913,7518,1847,2383,2596,3330,7519,4389, 374,3914, 652,4128,4129,  # 4630\n 375,1140, 798,7520,7521,7522,2361,4390,2264, 546,1659, 138,3031,2445,4391,7523,  # 4646\n2250, 612,1848, 910, 796,3765,1740,1371, 825,3766,3767,7524,2906,2554,7525, 692,  # 4662\n 444,3032,2624, 801,4392,4130,7526,1491, 244,1053,3033,4131,4132, 340,7527,3915,  # 4678\n1041,2987, 293,1168,  87,1357,7528,1539, 959,7529,2236, 721, 694,4133,3768, 219,  # 4694\n1478, 644,1417,3331,2656,1413,1401,1335,1389,3916,7530,7531,2988,2362,3134,1825,  # 4710\n 730,1515, 184,2827,  66,4393,7532,1660,2943, 246,3332, 378,1457, 226,3433, 975,  # 4726\n3917,2944,1264,3537, 674, 696,7533, 163,7534,1141,2417,2166, 713,3538,3333,4394,  # 4742\n3918,7535,7536,1186,  15,7537,1079,1070,7538,1522,3193,3539, 276,1050,2716, 758,  # 4758\n1126, 653,2945,3263,7539,2337, 889,3540,3919,3081,2989, 903,1250,4395,3920,3434,  # 4774\n3541,1342,1681,1718, 766,3264, 286,  89,2946,3649,7540,1713,7541,2597,3334,2990,  # 4790\n7542,2947,2215,3194,2866,7543,4396,2498,2526, 181, 387,1075,3921, 731,2187,3335,  # 4806\n7544,3265, 310, 313,3435,2299, 770,4134,  54,3034, 189,4397,3082,3769,3922,7545,  # 4822\n1230,1617,1849, 355,3542,4135,4398,3336, 111,4136,3650,1350,3135,3436,3035,4137,  # 4838\n2149,3266,3543,7546,2784,3923,3924,2991, 722,2008,7547,1071, 247,1207,2338,2471,  # 4854\n1378,4399,2009, 864,1437,1214,4400, 373,3770,1142,2216, 667,4401, 442,2753,2555,  # 4870\n3771,3925,1968,4138,3267,1839, 837, 170,1107, 934,1336,1882,7548,7549,2118,4139,  # 4886\n2828, 743,1569,7550,4402,4140, 582,2384,1418,3437,7551,1802,7552, 357,1395,1729,  # 4902\n3651,3268,2418,1564,2237,7553,3083,3772,1633,4403,1114,2085,4141,1532,7554, 482,  # 4918\n2446,4404,7555,7556,1492, 833,1466,7557,2717,3544,1641,2829,7558,1526,1272,3652,  # 4934\n4142,1686,1794, 416,2556,1902,1953,1803,7559,3773,2785,3774,1159,2316,7560,2867,  # 4950\n4405,1610,1584,3036,2419,2754, 443,3269,1163,3136,7561,7562,3926,7563,4143,2499,  # 4966\n3037,4406,3927,3137,2103,1647,3545,2010,1872,4144,7564,4145, 431,3438,7565, 250,  # 4982\n  97,  81,4146,7566,1648,1850,1558, 160, 848,7567, 866, 740,1694,7568,2201,2830,  # 4998\n3195,4147,4407,3653,1687, 950,2472, 426, 469,3196,3654,3655,3928,7569,7570,1188,  # 5014\n 424,1995, 861,3546,4148,3775,2202,2685, 168,1235,3547,4149,7571,2086,1674,4408,  # 5030\n3337,3270, 220,2557,1009,7572,3776, 670,2992, 332,1208, 717,7573,7574,3548,2447,  # 5046\n3929,3338,7575, 513,7576,1209,2868,3339,3138,4409,1080,7577,7578,7579,7580,2527,  # 5062\n3656,3549, 815,1587,3930,3931,7581,3550,3439,3777,1254,4410,1328,3038,1390,3932,  # 5078\n1741,3933,3778,3934,7582, 236,3779,2448,3271,7583,7584,3657,3780,1273,3781,4411,  # 5094\n7585, 308,7586,4412, 245,4413,1851,2473,1307,2575, 430, 715,2136,2449,7587, 270,  # 5110\n 199,2869,3935,7588,3551,2718,1753, 761,1754, 725,1661,1840,4414,3440,3658,7589,  # 5126\n7590, 587,  14,3272, 227,2598, 326, 480,2265, 943,2755,3552, 291, 650,1883,7591,  # 5142\n1702,1226, 102,1547,  62,3441, 904,4415,3442,1164,4150,7592,7593,1224,1548,2756,  # 5158\n 391, 498,1493,7594,1386,1419,7595,2055,1177,4416, 813, 880,1081,2363, 566,1145,  # 5174\n4417,2286,1001,1035,2558,2599,2238, 394,1286,7596,7597,2068,7598,  86,1494,1730,  # 5190\n3936, 491,1588, 745, 897,2948, 843,3340,3937,2757,2870,3273,1768, 998,2217,2069,  # 5206\n 397,1826,1195,1969,3659,2993,3341, 284,7599,3782,2500,2137,2119,1903,7600,3938,  # 5222\n2150,3939,4151,1036,3443,1904, 114,2559,4152, 209,1527,7601,7602,2949,2831,2625,  # 5238\n2385,2719,3139, 812,2560,7603,3274,7604,1559, 737,1884,3660,1210, 885,  28,2686,  # 5254\n3553,3783,7605,4153,1004,1779,4418,7606, 346,1981,2218,2687,4419,3784,1742, 797,  # 5270\n1642,3940,1933,1072,1384,2151, 896,3941,3275,3661,3197,2871,3554,7607,2561,1958,  # 5286\n4420,2450,1785,7608,7609,7610,3942,4154,1005,1308,3662,4155,2720,4421,4422,1528,  # 5302\n2600, 161,1178,4156,1982, 987,4423,1101,4157, 631,3943,1157,3198,2420,1343,1241,  # 5318\n1016,2239,2562, 372, 877,2339,2501,1160, 555,1934, 911,3944,7611, 466,1170, 169,  # 5334\n1051,2907,2688,3663,2474,2994,1182,2011,2563,1251,2626,7612, 992,2340,3444,1540,  # 5350\n2721,1201,2070,2401,1996,2475,7613,4424, 528,1922,2188,1503,1873,1570,2364,3342,  # 5366\n3276,7614, 557,1073,7615,1827,3445,2087,2266,3140,3039,3084, 767,3085,2786,4425,  # 5382\n1006,4158,4426,2341,1267,2176,3664,3199, 778,3945,3200,2722,1597,2657,7616,4427,  # 5398\n7617,3446,7618,7619,7620,3277,2689,1433,3278, 131,  95,1504,3946, 723,4159,3141,  # 5414\n1841,3555,2758,2189,3947,2027,2104,3665,7621,2995,3948,1218,7622,3343,3201,3949,  # 5430\n4160,2576, 248,1634,3785, 912,7623,2832,3666,3040,3786, 654,  53,7624,2996,7625,  # 5446\n1688,4428, 777,3447,1032,3950,1425,7626, 191, 820,2120,2833, 971,4429, 931,3202,  # 5462\n 135, 664, 783,3787,1997, 772,2908,1935,3951,3788,4430,2909,3203, 282,2723, 640,  # 5478\n1372,3448,1127, 922, 325,3344,7627,7628, 711,2044,7629,7630,3952,2219,2787,1936,  # 5494\n3953,3345,2220,2251,3789,2300,7631,4431,3790,1258,3279,3954,3204,2138,2950,3955,  # 5510\n3956,7632,2221, 258,3205,4432, 101,1227,7633,3280,1755,7634,1391,3281,7635,2910,  # 5526\n2056, 893,7636,7637,7638,1402,4161,2342,7639,7640,3206,3556,7641,7642, 878,1325,  # 5542\n1780,2788,4433, 259,1385,2577, 744,1183,2267,4434,7643,3957,2502,7644, 684,1024,  # 5558\n4162,7645, 472,3557,3449,1165,3282,3958,3959, 322,2152, 881, 455,1695,1152,1340,  # 5574\n 660, 554,2153,4435,1058,4436,4163, 830,1065,3346,3960,4437,1923,7646,1703,1918,  # 5590\n7647, 932,2268, 122,7648,4438, 947, 677,7649,3791,2627, 297,1905,1924,2269,4439,  # 5606\n2317,3283,7650,7651,4164,7652,4165,  84,4166, 112, 989,7653, 547,1059,3961, 701,  # 5622\n3558,1019,7654,4167,7655,3450, 942, 639, 457,2301,2451, 993,2951, 407, 851, 494,  # 5638\n4440,3347, 927,7656,1237,7657,2421,3348, 573,4168, 680, 921,2911,1279,1874, 285,  # 5654\n 790,1448,1983, 719,2167,7658,7659,4441,3962,3963,1649,7660,1541, 563,7661,1077,  # 5670\n7662,3349,3041,3451, 511,2997,3964,3965,3667,3966,1268,2564,3350,3207,4442,4443,  # 5686\n7663, 535,1048,1276,1189,2912,2028,3142,1438,1373,2834,2952,1134,2012,7664,4169,  # 5702\n1238,2578,3086,1259,7665, 700,7666,2953,3143,3668,4170,7667,4171,1146,1875,1906,  # 5718\n4444,2601,3967, 781,2422, 132,1589, 203, 147, 273,2789,2402, 898,1786,2154,3968,  # 5734\n3969,7668,3792,2790,7669,7670,4445,4446,7671,3208,7672,1635,3793, 965,7673,1804,  # 5750\n2690,1516,3559,1121,1082,1329,3284,3970,1449,3794,  65,1128,2835,2913,2759,1590,  # 5766\n3795,7674,7675,  12,2658,  45, 976,2579,3144,4447, 517,2528,1013,1037,3209,7676,  # 5782\n3796,2836,7677,3797,7678,3452,7679,2602, 614,1998,2318,3798,3087,2724,2628,7680,  # 5798\n2580,4172, 599,1269,7681,1810,3669,7682,2691,3088, 759,1060, 489,1805,3351,3285,  # 5814\n1358,7683,7684,2386,1387,1215,2629,2252, 490,7685,7686,4173,1759,2387,2343,7687,  # 5830\n4448,3799,1907,3971,2630,1806,3210,4449,3453,3286,2760,2344, 874,7688,7689,3454,  # 5846\n3670,1858,  91,2914,3671,3042,3800,4450,7690,3145,3972,2659,7691,3455,1202,1403,  # 5862\n3801,2954,2529,1517,2503,4451,3456,2504,7692,4452,7693,2692,1885,1495,1731,3973,  # 5878\n2365,4453,7694,2029,7695,7696,3974,2693,1216, 237,2581,4174,2319,3975,3802,4454,  # 5894\n4455,2694,3560,3457, 445,4456,7697,7698,7699,7700,2761,  61,3976,3672,1822,3977,  # 5910\n7701, 687,2045, 935, 925, 405,2660, 703,1096,1859,2725,4457,3978,1876,1367,2695,  # 5926\n3352, 918,2105,1781,2476, 334,3287,1611,1093,4458, 564,3146,3458,3673,3353, 945,  # 5942\n2631,2057,4459,7702,1925, 872,4175,7703,3459,2696,3089, 349,4176,3674,3979,4460,  # 5958\n3803,4177,3675,2155,3980,4461,4462,4178,4463,2403,2046, 782,3981, 400, 251,4179,  # 5974\n1624,7704,7705, 277,3676, 299,1265, 476,1191,3804,2121,4180,4181,1109, 205,7706,  # 5990\n2582,1000,2156,3561,1860,7707,7708,7709,4464,7710,4465,2565, 107,2477,2157,3982,  # 6006\n3460,3147,7711,1533, 541,1301, 158, 753,4182,2872,3562,7712,1696, 370,1088,4183,  # 6022\n4466,3563, 579, 327, 440, 162,2240, 269,1937,1374,3461, 968,3043,  56,1396,3090,  # 6038\n2106,3288,3354,7713,1926,2158,4467,2998,7714,3564,7715,7716,3677,4468,2478,7717,  # 6054\n2791,7718,1650,4469,7719,2603,7720,7721,3983,2661,3355,1149,3356,3984,3805,3985,  # 6070\n7722,1076,  49,7723, 951,3211,3289,3290, 450,2837, 920,7724,1811,2792,2366,4184,  # 6086\n1908,1138,2367,3806,3462,7725,3212,4470,1909,1147,1518,2423,4471,3807,7726,4472,  # 6102\n2388,2604, 260,1795,3213,7727,7728,3808,3291, 708,7729,3565,1704,7730,3566,1351,  # 6118\n1618,3357,2999,1886, 944,4185,3358,4186,3044,3359,4187,7731,3678, 422, 413,1714,  # 6134\n3292, 500,2058,2345,4188,2479,7732,1344,1910, 954,7733,1668,7734,7735,3986,2404,  # 6150\n4189,3567,3809,4190,7736,2302,1318,2505,3091, 133,3092,2873,4473, 629,  31,2838,  # 6166\n2697,3810,4474, 850, 949,4475,3987,2955,1732,2088,4191,1496,1852,7737,3988, 620,  # 6182\n3214, 981,1242,3679,3360,1619,3680,1643,3293,2139,2452,1970,1719,3463,2168,7738,  # 6198\n3215,7739,7740,3361,1828,7741,1277,4476,1565,2047,7742,1636,3568,3093,7743, 869,  # 6214\n2839, 655,3811,3812,3094,3989,3000,3813,1310,3569,4477,7744,7745,7746,1733, 558,  # 6230\n4478,3681, 335,1549,3045,1756,4192,3682,1945,3464,1829,1291,1192, 470,2726,2107,  # 6246\n2793, 913,1054,3990,7747,1027,7748,3046,3991,4479, 982,2662,3362,3148,3465,3216,  # 6262\n3217,1946,2794,7749, 571,4480,7750,1830,7751,3570,2583,1523,2424,7752,2089, 984,  # 6278\n4481,3683,1959,7753,3684, 852, 923,2795,3466,3685, 969,1519, 999,2048,2320,1705,  # 6294\n7754,3095, 615,1662, 151, 597,3992,2405,2321,1049, 275,4482,3686,4193, 568,3687,  # 6310\n3571,2480,4194,3688,7755,2425,2270, 409,3218,7756,1566,2874,3467,1002, 769,2840,  # 6326\n 194,2090,3149,3689,2222,3294,4195, 628,1505,7757,7758,1763,2177,3001,3993, 521,  # 6342\n1161,2584,1787,2203,2406,4483,3994,1625,4196,4197, 412,  42,3096, 464,7759,2632,  # 6358\n4484,3363,1760,1571,2875,3468,2530,1219,2204,3814,2633,2140,2368,4485,4486,3295,  # 6374\n1651,3364,3572,7760,7761,3573,2481,3469,7762,3690,7763,7764,2271,2091, 460,7765,  # 6390\n4487,7766,3002, 962, 588,3574, 289,3219,2634,1116,  52,7767,3047,1796,7768,7769,  # 6406\n7770,1467,7771,1598,1143,3691,4198,1984,1734,1067,4488,1280,3365, 465,4489,1572,  # 6422\n 510,7772,1927,2241,1812,1644,3575,7773,4490,3692,7774,7775,2663,1573,1534,7776,  # 6438\n7777,4199, 536,1807,1761,3470,3815,3150,2635,7778,7779,7780,4491,3471,2915,1911,  # 6454\n2796,7781,3296,1122, 377,3220,7782, 360,7783,7784,4200,1529, 551,7785,2059,3693,  # 6470\n1769,2426,7786,2916,4201,3297,3097,2322,2108,2030,4492,1404, 136,1468,1479, 672,  # 6486\n1171,3221,2303, 271,3151,7787,2762,7788,2049, 678,2727, 865,1947,4493,7789,2013,  # 6502\n3995,2956,7790,2728,2223,1397,3048,3694,4494,4495,1735,2917,3366,3576,7791,3816,  # 6518\n 509,2841,2453,2876,3817,7792,7793,3152,3153,4496,4202,2531,4497,2304,1166,1010,  # 6534\n 552, 681,1887,7794,7795,2957,2958,3996,1287,1596,1861,3154, 358, 453, 736, 175,  # 6550\n 478,1117, 905,1167,1097,7796,1853,1530,7797,1706,7798,2178,3472,2287,3695,3473,  # 6566\n3577,4203,2092,4204,7799,3367,1193,2482,4205,1458,2190,2205,1862,1888,1421,3298,  # 6582\n2918,3049,2179,3474, 595,2122,7800,3997,7801,7802,4206,1707,2636, 223,3696,1359,  # 6598\n 751,3098, 183,3475,7803,2797,3003, 419,2369, 633, 704,3818,2389, 241,7804,7805,  # 6614\n7806, 838,3004,3697,2272,2763,2454,3819,1938,2050,3998,1309,3099,2242,1181,7807,  # 6630\n1136,2206,3820,2370,1446,4207,2305,4498,7808,7809,4208,1055,2605, 484,3698,7810,  # 6646\n3999, 625,4209,2273,3368,1499,4210,4000,7811,4001,4211,3222,2274,2275,3476,7812,  # 6662\n7813,2764, 808,2606,3699,3369,4002,4212,3100,2532, 526,3370,3821,4213, 955,7814,  # 6678\n1620,4214,2637,2427,7815,1429,3700,1669,1831, 994, 928,7816,3578,1260,7817,7818,  # 6694\n7819,1948,2288, 741,2919,1626,4215,2729,2455, 867,1184, 362,3371,1392,7820,7821,  # 6710\n4003,4216,1770,1736,3223,2920,4499,4500,1928,2698,1459,1158,7822,3050,3372,2877,  # 6726\n1292,1929,2506,2842,3701,1985,1187,2071,2014,2607,4217,7823,2566,2507,2169,3702,  # 6742\n2483,3299,7824,3703,4501,7825,7826, 666,1003,3005,1022,3579,4218,7827,4502,1813,  # 6758\n2253, 574,3822,1603, 295,1535, 705,3823,4219, 283, 858, 417,7828,7829,3224,4503,  # 6774\n4504,3051,1220,1889,1046,2276,2456,4004,1393,1599, 689,2567, 388,4220,7830,2484,  # 6790\n 802,7831,2798,3824,2060,1405,2254,7832,4505,3825,2109,1052,1345,3225,1585,7833,  # 6806\n 809,7834,7835,7836, 575,2730,3477, 956,1552,1469,1144,2323,7837,2324,1560,2457,  # 6822\n3580,3226,4005, 616,2207,3155,2180,2289,7838,1832,7839,3478,4506,7840,1319,3704,  # 6838\n3705,1211,3581,1023,3227,1293,2799,7841,7842,7843,3826, 607,2306,3827, 762,2878,  # 6854\n1439,4221,1360,7844,1485,3052,7845,4507,1038,4222,1450,2061,2638,4223,1379,4508,  # 6870\n2585,7846,7847,4224,1352,1414,2325,2921,1172,7848,7849,3828,3829,7850,1797,1451,  # 6886\n7851,7852,7853,7854,2922,4006,4007,2485,2346, 411,4008,4009,3582,3300,3101,4509,  # 6902\n1561,2664,1452,4010,1375,7855,7856,  47,2959, 316,7857,1406,1591,2923,3156,7858,  # 6918\n1025,2141,3102,3157, 354,2731, 884,2224,4225,2407, 508,3706, 726,3583, 996,2428,  # 6934\n3584, 729,7859, 392,2191,1453,4011,4510,3707,7860,7861,2458,3585,2608,1675,2800,  # 6950\n 919,2347,2960,2348,1270,4511,4012,  73,7862,7863, 647,7864,3228,2843,2255,1550,  # 6966\n1346,3006,7865,1332, 883,3479,7866,7867,7868,7869,3301,2765,7870,1212, 831,1347,  # 6982\n4226,4512,2326,3830,1863,3053, 720,3831,4513,4514,3832,7871,4227,7872,7873,4515,  # 6998\n7874,7875,1798,4516,3708,2609,4517,3586,1645,2371,7876,7877,2924, 669,2208,2665,  # 7014\n2429,7878,2879,7879,7880,1028,3229,7881,4228,2408,7882,2256,1353,7883,7884,4518,  # 7030\n3158, 518,7885,4013,7886,4229,1960,7887,2142,4230,7888,7889,3007,2349,2350,3833,  # 7046\n 516,1833,1454,4014,2699,4231,4519,2225,2610,1971,1129,3587,7890,2766,7891,2961,  # 7062\n1422, 577,1470,3008,1524,3373,7892,7893, 432,4232,3054,3480,7894,2586,1455,2508,  # 7078\n2226,1972,1175,7895,1020,2732,4015,3481,4520,7896,2733,7897,1743,1361,3055,3482,  # 7094\n2639,4016,4233,4521,2290, 895, 924,4234,2170, 331,2243,3056, 166,1627,3057,1098,  # 7110\n7898,1232,2880,2227,3374,4522, 657, 403,1196,2372, 542,3709,3375,1600,4235,3483,  # 7126\n7899,4523,2767,3230, 576, 530,1362,7900,4524,2533,2666,3710,4017,7901, 842,3834,  # 7142\n7902,2801,2031,1014,4018, 213,2700,3376, 665, 621,4236,7903,3711,2925,2430,7904,  # 7158\n2431,3302,3588,3377,7905,4237,2534,4238,4525,3589,1682,4239,3484,1380,7906, 724,  # 7174\n2277, 600,1670,7907,1337,1233,4526,3103,2244,7908,1621,4527,7909, 651,4240,7910,  # 7190\n1612,4241,2611,7911,2844,7912,2734,2307,3058,7913, 716,2459,3059, 174,1255,2701,  # 7206\n4019,3590, 548,1320,1398, 728,4020,1574,7914,1890,1197,3060,4021,7915,3061,3062,  # 7222\n3712,3591,3713, 747,7916, 635,4242,4528,7917,7918,7919,4243,7920,7921,4529,7922,  # 7238\n3378,4530,2432, 451,7923,3714,2535,2072,4244,2735,4245,4022,7924,1764,4531,7925,  # 7254\n4246, 350,7926,2278,2390,2486,7927,4247,4023,2245,1434,4024, 488,4532, 458,4248,  # 7270\n4025,3715, 771,1330,2391,3835,2568,3159,2159,2409,1553,2667,3160,4249,7928,2487,  # 7286\n2881,2612,1720,2702,4250,3379,4533,7929,2536,4251,7930,3231,4252,2768,7931,2015,  # 7302\n2736,7932,1155,1017,3716,3836,7933,3303,2308, 201,1864,4253,1430,7934,4026,7935,  # 7318\n7936,7937,7938,7939,4254,1604,7940, 414,1865, 371,2587,4534,4535,3485,2016,3104,  # 7334\n4536,1708, 960,4255, 887, 389,2171,1536,1663,1721,7941,2228,4027,2351,2926,1580,  # 7350\n7942,7943,7944,1744,7945,2537,4537,4538,7946,4539,7947,2073,7948,7949,3592,3380,  # 7366\n2882,4256,7950,4257,2640,3381,2802, 673,2703,2460, 709,3486,4028,3593,4258,7951,  # 7382\n1148, 502, 634,7952,7953,1204,4540,3594,1575,4541,2613,3717,7954,3718,3105, 948,  # 7398\n3232, 121,1745,3837,1110,7955,4259,3063,2509,3009,4029,3719,1151,1771,3838,1488,  # 7414\n4030,1986,7956,2433,3487,7957,7958,2093,7959,4260,3839,1213,1407,2803, 531,2737,  # 7430\n2538,3233,1011,1537,7960,2769,4261,3106,1061,7961,3720,3721,1866,2883,7962,2017,  # 7446\n 120,4262,4263,2062,3595,3234,2309,3840,2668,3382,1954,4542,7963,7964,3488,1047,  # 7462\n2704,1266,7965,1368,4543,2845, 649,3383,3841,2539,2738,1102,2846,2669,7966,7967,  # 7478\n1999,7968,1111,3596,2962,7969,2488,3842,3597,2804,1854,3384,3722,7970,7971,3385,  # 7494\n2410,2884,3304,3235,3598,7972,2569,7973,3599,2805,4031,1460, 856,7974,3600,7975,  # 7510\n2885,2963,7976,2886,3843,7977,4264, 632,2510, 875,3844,1697,3845,2291,7978,7979,  # 7526\n4544,3010,1239, 580,4545,4265,7980, 914, 936,2074,1190,4032,1039,2123,7981,7982,  # 7542\n7983,3386,1473,7984,1354,4266,3846,7985,2172,3064,4033, 915,3305,4267,4268,3306,  # 7558\n1605,1834,7986,2739, 398,3601,4269,3847,4034, 328,1912,2847,4035,3848,1331,4270,  # 7574\n3011, 937,4271,7987,3602,4036,4037,3387,2160,4546,3388, 524, 742, 538,3065,1012,  # 7590\n7988,7989,3849,2461,7990, 658,1103, 225,3850,7991,7992,4547,7993,4548,7994,3236,  # 7606\n1243,7995,4038, 963,2246,4549,7996,2705,3603,3161,7997,7998,2588,2327,7999,4550,  # 7622\n8000,8001,8002,3489,3307, 957,3389,2540,2032,1930,2927,2462, 870,2018,3604,1746,  # 7638\n2770,2771,2434,2463,8003,3851,8004,3723,3107,3724,3490,3390,3725,8005,1179,3066,  # 7654\n8006,3162,2373,4272,3726,2541,3163,3108,2740,4039,8007,3391,1556,2542,2292, 977,  # 7670\n2887,2033,4040,1205,3392,8008,1765,3393,3164,2124,1271,1689, 714,4551,3491,8009,  # 7686\n2328,3852, 533,4273,3605,2181, 617,8010,2464,3308,3492,2310,8011,8012,3165,8013,  # 7702\n8014,3853,1987, 618, 427,2641,3493,3394,8015,8016,1244,1690,8017,2806,4274,4552,  # 7718\n8018,3494,8019,8020,2279,1576, 473,3606,4275,3395, 972,8021,3607,8022,3067,8023,  # 7734\n8024,4553,4554,8025,3727,4041,4042,8026, 153,4555, 356,8027,1891,2888,4276,2143,  # 7750\n 408, 803,2352,8028,3854,8029,4277,1646,2570,2511,4556,4557,3855,8030,3856,4278,  # 7766\n8031,2411,3396, 752,8032,8033,1961,2964,8034, 746,3012,2465,8035,4279,3728, 698,  # 7782\n4558,1892,4280,3608,2543,4559,3609,3857,8036,3166,3397,8037,1823,1302,4043,2706,  # 7798\n3858,1973,4281,8038,4282,3167, 823,1303,1288,1236,2848,3495,4044,3398, 774,3859,  # 7814\n8039,1581,4560,1304,2849,3860,4561,8040,2435,2161,1083,3237,4283,4045,4284, 344,  # 7830\n1173, 288,2311, 454,1683,8041,8042,1461,4562,4046,2589,8043,8044,4563, 985, 894,  # 7846\n8045,3399,3168,8046,1913,2928,3729,1988,8047,2110,1974,8048,4047,8049,2571,1194,  # 7862\n 425,8050,4564,3169,1245,3730,4285,8051,8052,2850,8053, 636,4565,1855,3861, 760,  # 7878\n1799,8054,4286,2209,1508,4566,4048,1893,1684,2293,8055,8056,8057,4287,4288,2210,  # 7894\n 479,8058,8059, 832,8060,4049,2489,8061,2965,2490,3731, 990,3109, 627,1814,2642,  # 7910\n4289,1582,4290,2125,2111,3496,4567,8062, 799,4291,3170,8063,4568,2112,1737,3013,  # 7926\n1018, 543, 754,4292,3309,1676,4569,4570,4050,8064,1489,8065,3497,8066,2614,2889,  # 7942\n4051,8067,8068,2966,8069,8070,8071,8072,3171,4571,4572,2182,1722,8073,3238,3239,  # 7958\n1842,3610,1715, 481, 365,1975,1856,8074,8075,1962,2491,4573,8076,2126,3611,3240,  # 7974\n 433,1894,2063,2075,8077, 602,2741,8078,8079,8080,8081,8082,3014,1628,3400,8083,  # 7990\n3172,4574,4052,2890,4575,2512,8084,2544,2772,8085,8086,8087,3310,4576,2891,8088,  # 8006\n4577,8089,2851,4578,4579,1221,2967,4053,2513,8090,8091,8092,1867,1989,8093,8094,  # 8022\n8095,1895,8096,8097,4580,1896,4054, 318,8098,2094,4055,4293,8099,8100, 485,8101,  # 8038\n 938,3862, 553,2670, 116,8102,3863,3612,8103,3498,2671,2773,3401,3311,2807,8104,  # 8054\n3613,2929,4056,1747,2930,2968,8105,8106, 207,8107,8108,2672,4581,2514,8109,3015,  # 8070\n 890,3614,3864,8110,1877,3732,3402,8111,2183,2353,3403,1652,8112,8113,8114, 941,  # 8086\n2294, 208,3499,4057,2019, 330,4294,3865,2892,2492,3733,4295,8115,8116,8117,8118,  # 8102\n)\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/euctwprober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is mozilla.org code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .mbcharsetprober import MultiByteCharSetProber\nfrom .codingstatemachine import CodingStateMachine\nfrom .chardistribution import EUCTWDistributionAnalysis\nfrom .mbcssm import EUCTW_SM_MODEL\n\nclass EUCTWProber(MultiByteCharSetProber):\n    def __init__(self):\n        super(EUCTWProber, self).__init__()\n        self.coding_sm = CodingStateMachine(EUCTW_SM_MODEL)\n        self.distribution_analyzer = EUCTWDistributionAnalysis()\n        self.reset()\n\n    @property\n    def charset_name(self):\n        return \"EUC-TW\"\n\n    @property\n    def language(self):\n        return \"Taiwan\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/gb2312freq.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Communicator client code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\n# GB2312 most frequently used character table\n#\n# Char to FreqOrder table , from hz6763\n\n# 512  --> 0.79  -- 0.79\n# 1024 --> 0.92  -- 0.13\n# 2048 --> 0.98  -- 0.06\n# 6768 --> 1.00  -- 0.02\n#\n# Ideal Distribution Ratio = 0.79135/(1-0.79135) = 3.79\n# Random Distribution Ration = 512 / (3755 - 512) = 0.157\n#\n# Typical Distribution Ratio about 25% of Ideal one, still much higher that RDR\n\nGB2312_TYPICAL_DISTRIBUTION_RATIO = 0.9\n\nGB2312_TABLE_SIZE = 3760\n\nGB2312_CHAR_TO_FREQ_ORDER = (\n1671, 749,1443,2364,3924,3807,2330,3921,1704,3463,2691,1511,1515, 572,3191,2205,\n2361, 224,2558, 479,1711, 963,3162, 440,4060,1905,2966,2947,3580,2647,3961,3842,\n2204, 869,4207, 970,2678,5626,2944,2956,1479,4048, 514,3595, 588,1346,2820,3409,\n 249,4088,1746,1873,2047,1774, 581,1813, 358,1174,3590,1014,1561,4844,2245, 670,\n1636,3112, 889,1286, 953, 556,2327,3060,1290,3141, 613, 185,3477,1367, 850,3820,\n1715,2428,2642,2303,2732,3041,2562,2648,3566,3946,1349, 388,3098,2091,1360,3585,\n 152,1687,1539, 738,1559,  59,1232,2925,2267,1388,1249,1741,1679,2960, 151,1566,\n1125,1352,4271, 924,4296, 385,3166,4459, 310,1245,2850,  70,3285,2729,3534,3575,\n2398,3298,3466,1960,2265, 217,3647, 864,1909,2084,4401,2773,1010,3269,5152, 853,\n3051,3121,1244,4251,1895, 364,1499,1540,2313,1180,3655,2268, 562, 715,2417,3061,\n 544, 336,3768,2380,1752,4075, 950, 280,2425,4382, 183,2759,3272, 333,4297,2155,\n1688,2356,1444,1039,4540, 736,1177,3349,2443,2368,2144,2225, 565, 196,1482,3406,\n 927,1335,4147, 692, 878,1311,1653,3911,3622,1378,4200,1840,2969,3149,2126,1816,\n2534,1546,2393,2760, 737,2494,  13, 447, 245,2747,  38,2765,2129,2589,1079, 606,\n 360, 471,3755,2890, 404, 848, 699,1785,1236, 370,2221,1023,3746,2074,2026,2023,\n2388,1581,2119, 812,1141,3091,2536,1519, 804,2053, 406,1596,1090, 784, 548,4414,\n1806,2264,2936,1100, 343,4114,5096, 622,3358, 743,3668,1510,1626,5020,3567,2513,\n3195,4115,5627,2489,2991,  24,2065,2697,1087,2719,  48,1634, 315,  68, 985,2052,\n 198,2239,1347,1107,1439, 597,2366,2172, 871,3307, 919,2487,2790,1867, 236,2570,\n1413,3794, 906,3365,3381,1701,1982,1818,1524,2924,1205, 616,2586,2072,2004, 575,\n 253,3099,  32,1365,1182, 197,1714,2454,1201, 554,3388,3224,2748, 756,2587, 250,\n2567,1507,1517,3529,1922,2761,2337,3416,1961,1677,2452,2238,3153, 615, 911,1506,\n1474,2495,1265,1906,2749,3756,3280,2161, 898,2714,1759,3450,2243,2444, 563,  26,\n3286,2266,3769,3344,2707,3677, 611,1402, 531,1028,2871,4548,1375, 261,2948, 835,\n1190,4134, 353, 840,2684,1900,3082,1435,2109,1207,1674, 329,1872,2781,4055,2686,\n2104, 608,3318,2423,2957,2768,1108,3739,3512,3271,3985,2203,1771,3520,1418,2054,\n1681,1153, 225,1627,2929, 162,2050,2511,3687,1954, 124,1859,2431,1684,3032,2894,\n 585,4805,3969,2869,2704,2088,2032,2095,3656,2635,4362,2209, 256, 518,2042,2105,\n3777,3657, 643,2298,1148,1779, 190, 989,3544, 414,  11,2135,2063,2979,1471, 403,\n3678, 126, 770,1563, 671,2499,3216,2877, 600,1179, 307,2805,4937,1268,1297,2694,\n 252,4032,1448,1494,1331,1394, 127,2256, 222,1647,1035,1481,3056,1915,1048, 873,\n3651, 210,  33,1608,2516, 200,1520, 415, 102,   0,3389,1287, 817,  91,3299,2940,\n 836,1814, 549,2197,1396,1669,2987,3582,2297,2848,4528,1070, 687,  20,1819, 121,\n1552,1364,1461,1968,2617,3540,2824,2083, 177, 948,4938,2291, 110,4549,2066, 648,\n3359,1755,2110,2114,4642,4845,1693,3937,3308,1257,1869,2123, 208,1804,3159,2992,\n2531,2549,3361,2418,1350,2347,2800,2568,1291,2036,2680,  72, 842,1990, 212,1233,\n1154,1586,  75,2027,3410,4900,1823,1337,2710,2676, 728,2810,1522,3026,4995, 157,\n 755,1050,4022, 710, 785,1936,2194,2085,1406,2777,2400, 150,1250,4049,1206, 807,\n1910, 534, 529,3309,1721,1660, 274,  39,2827, 661,2670,1578, 925,3248,3815,1094,\n4278,4901,4252,  41,1150,3747,2572,2227,4501,3658,4902,3813,3357,3617,2884,2258,\n 887, 538,4187,3199,1294,2439,3042,2329,2343,2497,1255, 107, 543,1527, 521,3478,\n3568, 194,5062,  15, 961,3870,1241,1192,2664,  66,5215,3260,2111,1295,1127,2152,\n3805,4135, 901,1164,1976, 398,1278, 530,1460, 748, 904,1054,1966,1426,  53,2909,\n 509, 523,2279,1534, 536,1019, 239,1685, 460,2353, 673,1065,2401,3600,4298,2272,\n1272,2363, 284,1753,3679,4064,1695,  81, 815,2677,2757,2731,1386, 859, 500,4221,\n2190,2566, 757,1006,2519,2068,1166,1455, 337,2654,3203,1863,1682,1914,3025,1252,\n1409,1366, 847, 714,2834,2038,3209, 964,2970,1901, 885,2553,1078,1756,3049, 301,\n1572,3326, 688,2130,1996,2429,1805,1648,2930,3421,2750,3652,3088, 262,1158,1254,\n 389,1641,1812, 526,1719, 923,2073,1073,1902, 468, 489,4625,1140, 857,2375,3070,\n3319,2863, 380, 116,1328,2693,1161,2244, 273,1212,1884,2769,3011,1775,1142, 461,\n3066,1200,2147,2212, 790, 702,2695,4222,1601,1058, 434,2338,5153,3640,  67,2360,\n4099,2502, 618,3472,1329, 416,1132, 830,2782,1807,2653,3211,3510,1662, 192,2124,\n 296,3979,1739,1611,3684,  23, 118, 324, 446,1239,1225, 293,2520,3814,3795,2535,\n3116,  17,1074, 467,2692,2201, 387,2922,  45,1326,3055,1645,3659,2817, 958, 243,\n1903,2320,1339,2825,1784,3289, 356, 576, 865,2315,2381,3377,3916,1088,3122,1713,\n1655, 935, 628,4689,1034,1327, 441, 800, 720, 894,1979,2183,1528,5289,2702,1071,\n4046,3572,2399,1571,3281,  79, 761,1103, 327, 134, 758,1899,1371,1615, 879, 442,\n 215,2605,2579, 173,2048,2485,1057,2975,3317,1097,2253,3801,4263,1403,1650,2946,\n 814,4968,3487,1548,2644,1567,1285,   2, 295,2636,  97, 946,3576, 832, 141,4257,\n3273, 760,3821,3521,3156,2607, 949,1024,1733,1516,1803,1920,2125,2283,2665,3180,\n1501,2064,3560,2171,1592, 803,3518,1416, 732,3897,4258,1363,1362,2458, 119,1427,\n 602,1525,2608,1605,1639,3175, 694,3064,  10, 465,  76,2000,4846,4208, 444,3781,\n1619,3353,2206,1273,3796, 740,2483, 320,1723,2377,3660,2619,1359,1137,1762,1724,\n2345,2842,1850,1862, 912, 821,1866, 612,2625,1735,2573,3369,1093, 844,  89, 937,\n 930,1424,3564,2413,2972,1004,3046,3019,2011, 711,3171,1452,4178, 428, 801,1943,\n 432, 445,2811, 206,4136,1472, 730, 349,  73, 397,2802,2547, 998,1637,1167, 789,\n 396,3217, 154,1218, 716,1120,1780,2819,4826,1931,3334,3762,2139,1215,2627, 552,\n3664,3628,3232,1405,2383,3111,1356,2652,3577,3320,3101,1703, 640,1045,1370,1246,\n4996, 371,1575,2436,1621,2210, 984,4033,1734,2638,  16,4529, 663,2755,3255,1451,\n3917,2257,1253,1955,2234,1263,2951, 214,1229, 617, 485, 359,1831,1969, 473,2310,\n 750,2058, 165,  80,2864,2419, 361,4344,2416,2479,1134, 796,3726,1266,2943, 860,\n2715, 938, 390,2734,1313,1384, 248, 202, 877,1064,2854, 522,3907, 279,1602, 297,\n2357, 395,3740, 137,2075, 944,4089,2584,1267,3802,  62,1533,2285, 178, 176, 780,\n2440, 201,3707, 590, 478,1560,4354,2117,1075,  30,  74,4643,4004,1635,1441,2745,\n 776,2596, 238,1077,1692,1912,2844, 605, 499,1742,3947, 241,3053, 980,1749, 936,\n2640,4511,2582, 515,1543,2162,5322,2892,2993, 890,2148,1924, 665,1827,3581,1032,\n 968,3163, 339,1044,1896, 270, 583,1791,1720,4367,1194,3488,3669,  43,2523,1657,\n 163,2167, 290,1209,1622,3378, 550, 634,2508,2510, 695,2634,2384,2512,1476,1414,\n 220,1469,2341,2138,2852,3183,2900,4939,2865,3502,1211,3680, 854,3227,1299,2976,\n3172, 186,2998,1459, 443,1067,3251,1495, 321,1932,3054, 909, 753,1410,1828, 436,\n2441,1119,1587,3164,2186,1258, 227, 231,1425,1890,3200,3942, 247, 959, 725,5254,\n2741, 577,2158,2079, 929, 120, 174, 838,2813, 591,1115, 417,2024,  40,3240,1536,\n1037, 291,4151,2354, 632,1298,2406,2500,3535,1825,1846,3451, 205,1171, 345,4238,\n  18,1163, 811, 685,2208,1217, 425,1312,1508,1175,4308,2552,1033, 587,1381,3059,\n2984,3482, 340,1316,4023,3972, 792,3176, 519, 777,4690, 918, 933,4130,2981,3741,\n  90,3360,2911,2200,5184,4550, 609,3079,2030, 272,3379,2736, 363,3881,1130,1447,\n 286, 779, 357,1169,3350,3137,1630,1220,2687,2391, 747,1277,3688,2618,2682,2601,\n1156,3196,5290,4034,3102,1689,3596,3128, 874, 219,2783, 798, 508,1843,2461, 269,\n1658,1776,1392,1913,2983,3287,2866,2159,2372, 829,4076,  46,4253,2873,1889,1894,\n 915,1834,1631,2181,2318, 298, 664,2818,3555,2735, 954,3228,3117, 527,3511,2173,\n 681,2712,3033,2247,2346,3467,1652, 155,2164,3382, 113,1994, 450, 899, 494, 994,\n1237,2958,1875,2336,1926,3727, 545,1577,1550, 633,3473, 204,1305,3072,2410,1956,\n2471, 707,2134, 841,2195,2196,2663,3843,1026,4940, 990,3252,4997, 368,1092, 437,\n3212,3258,1933,1829, 675,2977,2893, 412, 943,3723,4644,3294,3283,2230,2373,5154,\n2389,2241,2661,2323,1404,2524, 593, 787, 677,3008,1275,2059, 438,2709,2609,2240,\n2269,2246,1446,  36,1568,1373,3892,1574,2301,1456,3962, 693,2276,5216,2035,1143,\n2720,1919,1797,1811,2763,4137,2597,1830,1699,1488,1198,2090, 424,1694, 312,3634,\n3390,4179,3335,2252,1214, 561,1059,3243,2295,2561, 975,5155,2321,2751,3772, 472,\n1537,3282,3398,1047,2077,2348,2878,1323,3340,3076, 690,2906,  51, 369, 170,3541,\n1060,2187,2688,3670,2541,1083,1683, 928,3918, 459, 109,4427, 599,3744,4286, 143,\n2101,2730,2490,  82,1588,3036,2121, 281,1860, 477,4035,1238,2812,3020,2716,3312,\n1530,2188,2055,1317, 843, 636,1808,1173,3495, 649, 181,1002, 147,3641,1159,2414,\n3750,2289,2795, 813,3123,2610,1136,4368,   5,3391,4541,2174, 420, 429,1728, 754,\n1228,2115,2219, 347,2223,2733, 735,1518,3003,2355,3134,1764,3948,3329,1888,2424,\n1001,1234,1972,3321,3363,1672,1021,1450,1584, 226, 765, 655,2526,3404,3244,2302,\n3665, 731, 594,2184, 319,1576, 621, 658,2656,4299,2099,3864,1279,2071,2598,2739,\n 795,3086,3699,3908,1707,2352,2402,1382,3136,2475,1465,4847,3496,3865,1085,3004,\n2591,1084, 213,2287,1963,3565,2250, 822, 793,4574,3187,1772,1789,3050, 595,1484,\n1959,2770,1080,2650, 456, 422,2996, 940,3322,4328,4345,3092,2742, 965,2784, 739,\n4124, 952,1358,2498,2949,2565, 332,2698,2378, 660,2260,2473,4194,3856,2919, 535,\n1260,2651,1208,1428,1300,1949,1303,2942, 433,2455,2450,1251,1946, 614,1269, 641,\n1306,1810,2737,3078,2912, 564,2365,1419,1415,1497,4460,2367,2185,1379,3005,1307,\n3218,2175,1897,3063, 682,1157,4040,4005,1712,1160,1941,1399, 394, 402,2952,1573,\n1151,2986,2404, 862, 299,2033,1489,3006, 346, 171,2886,3401,1726,2932, 168,2533,\n  47,2507,1030,3735,1145,3370,1395,1318,1579,3609,4560,2857,4116,1457,2529,1965,\n 504,1036,2690,2988,2405, 745,5871, 849,2397,2056,3081, 863,2359,3857,2096,  99,\n1397,1769,2300,4428,1643,3455,1978,1757,3718,1440,  35,4879,3742,1296,4228,2280,\n 160,5063,1599,2013, 166, 520,3479,1646,3345,3012, 490,1937,1545,1264,2182,2505,\n1096,1188,1369,1436,2421,1667,2792,2460,1270,2122, 727,3167,2143, 806,1706,1012,\n1800,3037, 960,2218,1882, 805, 139,2456,1139,1521, 851,1052,3093,3089, 342,2039,\n 744,5097,1468,1502,1585,2087, 223, 939, 326,2140,2577, 892,2481,1623,4077, 982,\n3708, 135,2131,  87,2503,3114,2326,1106, 876,1616, 547,2997,2831,2093,3441,4530,\n4314,   9,3256,4229,4148, 659,1462,1986,1710,2046,2913,2231,4090,4880,5255,3392,\n3274,1368,3689,4645,1477, 705,3384,3635,1068,1529,2941,1458,3782,1509, 100,1656,\n2548, 718,2339, 408,1590,2780,3548,1838,4117,3719,1345,3530, 717,3442,2778,3220,\n2898,1892,4590,3614,3371,2043,1998,1224,3483, 891, 635, 584,2559,3355, 733,1766,\n1729,1172,3789,1891,2307, 781,2982,2271,1957,1580,5773,2633,2005,4195,3097,1535,\n3213,1189,1934,5693,3262, 586,3118,1324,1598, 517,1564,2217,1868,1893,4445,3728,\n2703,3139,1526,1787,1992,3882,2875,1549,1199,1056,2224,1904,2711,5098,4287, 338,\n1993,3129,3489,2689,1809,2815,1997, 957,1855,3898,2550,3275,3057,1105,1319, 627,\n1505,1911,1883,3526, 698,3629,3456,1833,1431, 746,  77,1261,2017,2296,1977,1885,\n 125,1334,1600, 525,1798,1109,2222,1470,1945, 559,2236,1186,3443,2476,1929,1411,\n2411,3135,1777,3372,2621,1841,1613,3229, 668,1430,1839,2643,2916, 195,1989,2671,\n2358,1387, 629,3205,2293,5256,4439, 123,1310, 888,1879,4300,3021,3605,1003,1162,\n3192,2910,2010, 140,2395,2859,  55,1082,2012,2901, 662, 419,2081,1438, 680,2774,\n4654,3912,1620,1731,1625,5035,4065,2328, 512,1344, 802,5443,2163,2311,2537, 524,\n3399,  98,1155,2103,1918,2606,3925,2816,1393,2465,1504,3773,2177,3963,1478,4346,\n 180,1113,4655,3461,2028,1698, 833,2696,1235,1322,1594,4408,3623,3013,3225,2040,\n3022, 541,2881, 607,3632,2029,1665,1219, 639,1385,1686,1099,2803,3231,1938,3188,\n2858, 427, 676,2772,1168,2025, 454,3253,2486,3556, 230,1950, 580, 791,1991,1280,\n1086,1974,2034, 630, 257,3338,2788,4903,1017,  86,4790, 966,2789,1995,1696,1131,\n 259,3095,4188,1308, 179,1463,5257, 289,4107,1248,  42,3413,1725,2288, 896,1947,\n 774,4474,4254, 604,3430,4264, 392,2514,2588, 452, 237,1408,3018, 988,4531,1970,\n3034,3310, 540,2370,1562,1288,2990, 502,4765,1147,   4,1853,2708, 207, 294,2814,\n4078,2902,2509, 684,  34,3105,3532,2551, 644, 709,2801,2344, 573,1727,3573,3557,\n2021,1081,3100,4315,2100,3681, 199,2263,1837,2385, 146,3484,1195,2776,3949, 997,\n1939,3973,1008,1091,1202,1962,1847,1149,4209,5444,1076, 493, 117,5400,2521, 972,\n1490,2934,1796,4542,2374,1512,2933,2657, 413,2888,1135,2762,2314,2156,1355,2369,\n 766,2007,2527,2170,3124,2491,2593,2632,4757,2437, 234,3125,3591,1898,1750,1376,\n1942,3468,3138, 570,2127,2145,3276,4131, 962, 132,1445,4196,  19, 941,3624,3480,\n3366,1973,1374,4461,3431,2629, 283,2415,2275, 808,2887,3620,2112,2563,1353,3610,\n 955,1089,3103,1053,  96,  88,4097, 823,3808,1583, 399, 292,4091,3313, 421,1128,\n 642,4006, 903,2539,1877,2082, 596,  29,4066,1790, 722,2157, 130, 995,1569, 769,\n1485, 464, 513,2213, 288,1923,1101,2453,4316, 133, 486,2445,  50, 625, 487,2207,\n  57, 423, 481,2962, 159,3729,1558, 491, 303, 482, 501, 240,2837, 112,3648,2392,\n1783, 362,   8,3433,3422, 610,2793,3277,1390,1284,1654,  21,3823, 734, 367, 623,\n 193, 287, 374,1009,1483, 816, 476, 313,2255,2340,1262,2150,2899,1146,2581, 782,\n2116,1659,2018,1880, 255,3586,3314,1110,2867,2137,2564, 986,2767,5185,2006, 650,\n 158, 926, 762, 881,3157,2717,2362,3587, 306,3690,3245,1542,3077,2427,1691,2478,\n2118,2985,3490,2438, 539,2305, 983, 129,1754, 355,4201,2386, 827,2923, 104,1773,\n2838,2771, 411,2905,3919, 376, 767, 122,1114, 828,2422,1817,3506, 266,3460,1007,\n1609,4998, 945,2612,4429,2274, 726,1247,1964,2914,2199,2070,4002,4108, 657,3323,\n1422, 579, 455,2764,4737,1222,2895,1670, 824,1223,1487,2525, 558, 861,3080, 598,\n2659,2515,1967, 752,2583,2376,2214,4180, 977, 704,2464,4999,2622,4109,1210,2961,\n 819,1541, 142,2284,  44, 418, 457,1126,3730,4347,4626,1644,1876,3671,1864, 302,\n1063,5694, 624, 723,1984,3745,1314,1676,2488,1610,1449,3558,3569,2166,2098, 409,\n1011,2325,3704,2306, 818,1732,1383,1824,1844,3757, 999,2705,3497,1216,1423,2683,\n2426,2954,2501,2726,2229,1475,2554,5064,1971,1794,1666,2014,1343, 783, 724, 191,\n2434,1354,2220,5065,1763,2752,2472,4152, 131, 175,2885,3434,  92,1466,4920,2616,\n3871,3872,3866, 128,1551,1632, 669,1854,3682,4691,4125,1230, 188,2973,3290,1302,\n1213, 560,3266, 917, 763,3909,3249,1760, 868,1958, 764,1782,2097, 145,2277,3774,\n4462,  64,1491,3062, 971,2132,3606,2442, 221,1226,1617, 218, 323,1185,3207,3147,\n 571, 619,1473,1005,1744,2281, 449,1887,2396,3685, 275, 375,3816,1743,3844,3731,\n 845,1983,2350,4210,1377, 773, 967,3499,3052,3743,2725,4007,1697,1022,3943,1464,\n3264,2855,2722,1952,1029,2839,2467,  84,4383,2215, 820,1391,2015,2448,3672, 377,\n1948,2168, 797,2545,3536,2578,2645,  94,2874,1678, 405,1259,3071, 771, 546,1315,\n 470,1243,3083, 895,2468, 981, 969,2037, 846,4181, 653,1276,2928,  14,2594, 557,\n3007,2474, 156, 902,1338,1740,2574, 537,2518, 973,2282,2216,2433,1928, 138,2903,\n1293,2631,1612, 646,3457, 839,2935, 111, 496,2191,2847, 589,3186, 149,3994,2060,\n4031,2641,4067,3145,1870,  37,3597,2136,1025,2051,3009,3383,3549,1121,1016,3261,\n1301, 251,2446,2599,2153, 872,3246, 637, 334,3705, 831, 884, 921,3065,3140,4092,\n2198,1944, 246,2964, 108,2045,1152,1921,2308,1031, 203,3173,4170,1907,3890, 810,\n1401,2003,1690, 506, 647,1242,2828,1761,1649,3208,2249,1589,3709,2931,5156,1708,\n 498, 666,2613, 834,3817,1231, 184,2851,1124, 883,3197,2261,3710,1765,1553,2658,\n1178,2639,2351,  93,1193, 942,2538,2141,4402, 235,1821, 870,1591,2192,1709,1871,\n3341,1618,4126,2595,2334, 603, 651,  69, 701, 268,2662,3411,2555,1380,1606, 503,\n 448, 254,2371,2646, 574,1187,2309,1770, 322,2235,1292,1801, 305, 566,1133, 229,\n2067,2057, 706, 167, 483,2002,2672,3295,1820,3561,3067, 316, 378,2746,3452,1112,\n 136,1981, 507,1651,2917,1117, 285,4591, 182,2580,3522,1304, 335,3303,1835,2504,\n1795,1792,2248, 674,1018,2106,2449,1857,2292,2845, 976,3047,1781,2600,2727,1389,\n1281,  52,3152, 153, 265,3950, 672,3485,3951,4463, 430,1183, 365, 278,2169,  27,\n1407,1336,2304, 209,1340,1730,2202,1852,2403,2883, 979,1737,1062, 631,2829,2542,\n3876,2592, 825,2086,2226,3048,3625, 352,1417,3724, 542, 991, 431,1351,3938,1861,\n2294, 826,1361,2927,3142,3503,1738, 463,2462,2723, 582,1916,1595,2808, 400,3845,\n3891,2868,3621,2254,  58,2492,1123, 910,2160,2614,1372,1603,1196,1072,3385,1700,\n3267,1980, 696, 480,2430, 920, 799,1570,2920,1951,2041,4047,2540,1321,4223,2469,\n3562,2228,1271,2602, 401,2833,3351,2575,5157, 907,2312,1256, 410, 263,3507,1582,\n 996, 678,1849,2316,1480, 908,3545,2237, 703,2322, 667,1826,2849,1531,2604,2999,\n2407,3146,2151,2630,1786,3711, 469,3542, 497,3899,2409, 858, 837,4446,3393,1274,\n 786, 620,1845,2001,3311, 484, 308,3367,1204,1815,3691,2332,1532,2557,1842,2020,\n2724,1927,2333,4440, 567,  22,1673,2728,4475,1987,1858,1144,1597, 101,1832,3601,\n  12, 974,3783,4391, 951,1412,   1,3720, 453,4608,4041, 528,1041,1027,3230,2628,\n1129, 875,1051,3291,1203,2262,1069,2860,2799,2149,2615,3278, 144,1758,3040,  31,\n 475,1680, 366,2685,3184, 311,1642,4008,2466,5036,1593,1493,2809, 216,1420,1668,\n 233, 304,2128,3284, 232,1429,1768,1040,2008,3407,2740,2967,2543, 242,2133, 778,\n1565,2022,2620, 505,2189,2756,1098,2273, 372,1614, 708, 553,2846,2094,2278, 169,\n3626,2835,4161, 228,2674,3165, 809,1454,1309, 466,1705,1095, 900,3423, 880,2667,\n3751,5258,2317,3109,2571,4317,2766,1503,1342, 866,4447,1118,  63,2076, 314,1881,\n1348,1061, 172, 978,3515,1747, 532, 511,3970,   6, 601, 905,2699,3300,1751, 276,\n1467,3725,2668,  65,4239,2544,2779,2556,1604, 578,2451,1802, 992,2331,2624,1320,\n3446, 713,1513,1013, 103,2786,2447,1661, 886,1702, 916, 654,3574,2031,1556, 751,\n2178,2821,2179,1498,1538,2176, 271, 914,2251,2080,1325, 638,1953,2937,3877,2432,\n2754,  95,3265,1716, 260,1227,4083, 775, 106,1357,3254, 426,1607, 555,2480, 772,\n1985, 244,2546, 474, 495,1046,2611,1851,2061,  71,2089,1675,2590, 742,3758,2843,\n3222,1433, 267,2180,2576,2826,2233,2092,3913,2435, 956,1745,3075, 856,2113,1116,\n 451,   3,1988,2896,1398, 993,2463,1878,2049,1341,2718,2721,2870,2108, 712,2904,\n4363,2753,2324, 277,2872,2349,2649, 384, 987, 435, 691,3000, 922, 164,3939, 652,\n1500,1184,4153,2482,3373,2165,4848,2335,3775,3508,3154,2806,2830,1554,2102,1664,\n2530,1434,2408, 893,1547,2623,3447,2832,2242,2532,3169,2856,3223,2078,  49,3770,\n3469, 462, 318, 656,2259,3250,3069, 679,1629,2758, 344,1138,1104,3120,1836,1283,\n3115,2154,1437,4448, 934, 759,1999, 794,2862,1038, 533,2560,1722,2342, 855,2626,\n1197,1663,4476,3127,  85,4240,2528,  25,1111,1181,3673, 407,3470,4561,2679,2713,\n 768,1925,2841,3986,1544,1165, 932, 373,1240,2146,1930,2673, 721,4766, 354,4333,\n 391,2963, 187,  61,3364,1442,1102, 330,1940,1767, 341,3809,4118, 393,2496,2062,\n2211, 105, 331, 300, 439, 913,1332, 626, 379,3304,1557, 328, 689,3952, 309,1555,\n 931, 317,2517,3027, 325, 569, 686,2107,3084,  60,1042,1333,2794, 264,3177,4014,\n1628, 258,3712,   7,4464,1176,1043,1778, 683, 114,1975,  78,1492, 383,1886, 510,\n 386, 645,5291,2891,2069,3305,4138,3867,2939,2603,2493,1935,1066,1848,3588,1015,\n1282,1289,4609, 697,1453,3044,2666,3611,1856,2412,  54, 719,1330, 568,3778,2459,\n1748, 788, 492, 551,1191,1000, 488,3394,3763, 282,1799, 348,2016,1523,3155,2390,\n1049, 382,2019,1788,1170, 729,2968,3523, 897,3926,2785,2938,3292, 350,2319,3238,\n1718,1717,2655,3453,3143,4465, 161,2889,2980,2009,1421,  56,1908,1640,2387,2232,\n1917,1874,2477,4921, 148,  83,3438, 592,4245,2882,1822,1055, 741, 115,1496,1624,\n 381,1638,4592,1020, 516,3214, 458, 947,4575,1432, 211,1514,2926,1865,2142, 189,\n 852,1221,1400,1486, 882,2299,4036, 351,  28,1122, 700,6479,6480,6481,6482,6483,  #last 512\n)\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/gb2312prober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is mozilla.org code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .mbcharsetprober import MultiByteCharSetProber\nfrom .codingstatemachine import CodingStateMachine\nfrom .chardistribution import GB2312DistributionAnalysis\nfrom .mbcssm import GB2312_SM_MODEL\n\nclass GB2312Prober(MultiByteCharSetProber):\n    def __init__(self):\n        super(GB2312Prober, self).__init__()\n        self.coding_sm = CodingStateMachine(GB2312_SM_MODEL)\n        self.distribution_analyzer = GB2312DistributionAnalysis()\n        self.reset()\n\n    @property\n    def charset_name(self):\n        return \"GB2312\"\n\n    @property\n    def language(self):\n        return \"Chinese\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/hebrewprober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Universal charset detector code.\n#\n# The Initial Developer of the Original Code is\n#          Shy Shalom\n# Portions created by the Initial Developer are Copyright (C) 2005\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .charsetprober import CharSetProber\nfrom .enums import ProbingState\n\n# This prober doesn't actually recognize a language or a charset.\n# It is a helper prober for the use of the Hebrew model probers\n\n### General ideas of the Hebrew charset recognition ###\n#\n# Four main charsets exist in Hebrew:\n# \"ISO-8859-8\" - Visual Hebrew\n# \"windows-1255\" - Logical Hebrew\n# \"ISO-8859-8-I\" - Logical Hebrew\n# \"x-mac-hebrew\" - ?? Logical Hebrew ??\n#\n# Both \"ISO\" charsets use a completely identical set of code points, whereas\n# \"windows-1255\" and \"x-mac-hebrew\" are two different proper supersets of\n# these code points. windows-1255 defines additional characters in the range\n# 0x80-0x9F as some misc punctuation marks as well as some Hebrew-specific\n# diacritics and additional 'Yiddish' ligature letters in the range 0xc0-0xd6.\n# x-mac-hebrew defines similar additional code points but with a different\n# mapping.\n#\n# As far as an average Hebrew text with no diacritics is concerned, all four\n# charsets are identical with respect to code points. Meaning that for the\n# main Hebrew alphabet, all four map the same values to all 27 Hebrew letters\n# (including final letters).\n#\n# The dominant difference between these charsets is their directionality.\n# \"Visual\" directionality means that the text is ordered as if the renderer is\n# not aware of a BIDI rendering algorithm. The renderer sees the text and\n# draws it from left to right. The text itself when ordered naturally is read\n# backwards. A buffer of Visual Hebrew generally looks like so:\n# \"[last word of first line spelled backwards] [whole line ordered backwards\n# and spelled backwards] [first word of first line spelled backwards]\n# [end of line] [last word of second line] ... etc' \"\n# adding punctuation marks, numbers and English text to visual text is\n# naturally also \"visual\" and from left to right.\n#\n# \"Logical\" directionality means the text is ordered \"naturally\" according to\n# the order it is read. It is the responsibility of the renderer to display\n# the text from right to left. A BIDI algorithm is used to place general\n# punctuation marks, numbers and English text in the text.\n#\n# Texts in x-mac-hebrew are almost impossible to find on the Internet. From\n# what little evidence I could find, it seems that its general directionality\n# is Logical.\n#\n# To sum up all of the above, the Hebrew probing mechanism knows about two\n# charsets:\n# Visual Hebrew - \"ISO-8859-8\" - backwards text - Words and sentences are\n#    backwards while line order is natural. For charset recognition purposes\n#    the line order is unimportant (In fact, for this implementation, even\n#    word order is unimportant).\n# Logical Hebrew - \"windows-1255\" - normal, naturally ordered text.\n#\n# \"ISO-8859-8-I\" is a subset of windows-1255 and doesn't need to be\n#    specifically identified.\n# \"x-mac-hebrew\" is also identified as windows-1255. A text in x-mac-hebrew\n#    that contain special punctuation marks or diacritics is displayed with\n#    some unconverted characters showing as question marks. This problem might\n#    be corrected using another model prober for x-mac-hebrew. Due to the fact\n#    that x-mac-hebrew texts are so rare, writing another model prober isn't\n#    worth the effort and performance hit.\n#\n#### The Prober ####\n#\n# The prober is divided between two SBCharSetProbers and a HebrewProber,\n# all of which are managed, created, fed data, inquired and deleted by the\n# SBCSGroupProber. The two SBCharSetProbers identify that the text is in\n# fact some kind of Hebrew, Logical or Visual. The final decision about which\n# one is it is made by the HebrewProber by combining final-letter scores\n# with the scores of the two SBCharSetProbers to produce a final answer.\n#\n# The SBCSGroupProber is responsible for stripping the original text of HTML\n# tags, English characters, numbers, low-ASCII punctuation characters, spaces\n# and new lines. It reduces any sequence of such characters to a single space.\n# The buffer fed to each prober in the SBCS group prober is pure text in\n# high-ASCII.\n# The two SBCharSetProbers (model probers) share the same language model:\n# Win1255Model.\n# The first SBCharSetProber uses the model normally as any other\n# SBCharSetProber does, to recognize windows-1255, upon which this model was\n# built. The second SBCharSetProber is told to make the pair-of-letter\n# lookup in the language model backwards. This in practice exactly simulates\n# a visual Hebrew model using the windows-1255 logical Hebrew model.\n#\n# The HebrewProber is not using any language model. All it does is look for\n# final-letter evidence suggesting the text is either logical Hebrew or visual\n# Hebrew. Disjointed from the model probers, the results of the HebrewProber\n# alone are meaningless. HebrewProber always returns 0.00 as confidence\n# since it never identifies a charset by itself. Instead, the pointer to the\n# HebrewProber is passed to the model probers as a helper \"Name Prober\".\n# When the Group prober receives a positive identification from any prober,\n# it asks for the name of the charset identified. If the prober queried is a\n# Hebrew model prober, the model prober forwards the call to the\n# HebrewProber to make the final decision. In the HebrewProber, the\n# decision is made according to the final-letters scores maintained and Both\n# model probers scores. The answer is returned in the form of the name of the\n# charset identified, either \"windows-1255\" or \"ISO-8859-8\".\n\nclass HebrewProber(CharSetProber):\n    # windows-1255 / ISO-8859-8 code points of interest\n    FINAL_KAF = 0xea\n    NORMAL_KAF = 0xeb\n    FINAL_MEM = 0xed\n    NORMAL_MEM = 0xee\n    FINAL_NUN = 0xef\n    NORMAL_NUN = 0xf0\n    FINAL_PE = 0xf3\n    NORMAL_PE = 0xf4\n    FINAL_TSADI = 0xf5\n    NORMAL_TSADI = 0xf6\n\n    # Minimum Visual vs Logical final letter score difference.\n    # If the difference is below this, don't rely solely on the final letter score\n    # distance.\n    MIN_FINAL_CHAR_DISTANCE = 5\n\n    # Minimum Visual vs Logical model score difference.\n    # If the difference is below this, don't rely at all on the model score\n    # distance.\n    MIN_MODEL_DISTANCE = 0.01\n\n    VISUAL_HEBREW_NAME = \"ISO-8859-8\"\n    LOGICAL_HEBREW_NAME = \"windows-1255\"\n\n    def __init__(self):\n        super(HebrewProber, self).__init__()\n        self._final_char_logical_score = None\n        self._final_char_visual_score = None\n        self._prev = None\n        self._before_prev = None\n        self._logical_prober = None\n        self._visual_prober = None\n        self.reset()\n\n    def reset(self):\n        self._final_char_logical_score = 0\n        self._final_char_visual_score = 0\n        # The two last characters seen in the previous buffer,\n        # mPrev and mBeforePrev are initialized to space in order to simulate\n        # a word delimiter at the beginning of the data\n        self._prev = ' '\n        self._before_prev = ' '\n        # These probers are owned by the group prober.\n\n    def set_model_probers(self, logicalProber, visualProber):\n        self._logical_prober = logicalProber\n        self._visual_prober = visualProber\n\n    def is_final(self, c):\n        return c in [self.FINAL_KAF, self.FINAL_MEM, self.FINAL_NUN,\n                     self.FINAL_PE, self.FINAL_TSADI]\n\n    def is_non_final(self, c):\n        # The normal Tsadi is not a good Non-Final letter due to words like\n        # 'lechotet' (to chat) containing an apostrophe after the tsadi. This\n        # apostrophe is converted to a space in FilterWithoutEnglishLetters\n        # causing the Non-Final tsadi to appear at an end of a word even\n        # though this is not the case in the original text.\n        # The letters Pe and Kaf rarely display a related behavior of not being\n        # a good Non-Final letter. Words like 'Pop', 'Winamp' and 'Mubarak'\n        # for example legally end with a Non-Final Pe or Kaf. However, the\n        # benefit of these letters as Non-Final letters outweighs the damage\n        # since these words are quite rare.\n        return c in [self.NORMAL_KAF, self.NORMAL_MEM,\n                     self.NORMAL_NUN, self.NORMAL_PE]\n\n    def feed(self, byte_str):\n        # Final letter analysis for logical-visual decision.\n        # Look for evidence that the received buffer is either logical Hebrew\n        # or visual Hebrew.\n        # The following cases are checked:\n        # 1) A word longer than 1 letter, ending with a final letter. This is\n        #    an indication that the text is laid out \"naturally\" since the\n        #    final letter really appears at the end. +1 for logical score.\n        # 2) A word longer than 1 letter, ending with a Non-Final letter. In\n        #    normal Hebrew, words ending with Kaf, Mem, Nun, Pe or Tsadi,\n        #    should not end with the Non-Final form of that letter. Exceptions\n        #    to this rule are mentioned above in isNonFinal(). This is an\n        #    indication that the text is laid out backwards. +1 for visual\n        #    score\n        # 3) A word longer than 1 letter, starting with a final letter. Final\n        #    letters should not appear at the beginning of a word. This is an\n        #    indication that the text is laid out backwards. +1 for visual\n        #    score.\n        #\n        # The visual score and logical score are accumulated throughout the\n        # text and are finally checked against each other in GetCharSetName().\n        # No checking for final letters in the middle of words is done since\n        # that case is not an indication for either Logical or Visual text.\n        #\n        # We automatically filter out all 7-bit characters (replace them with\n        # spaces) so the word boundary detection works properly. [MAP]\n\n        if self.state == ProbingState.NOT_ME:\n            # Both model probers say it's not them. No reason to continue.\n            return ProbingState.NOT_ME\n\n        byte_str = self.filter_high_byte_only(byte_str)\n\n        for cur in byte_str:\n            if cur == ' ':\n                # We stand on a space - a word just ended\n                if self._before_prev != ' ':\n                    # next-to-last char was not a space so self._prev is not a\n                    # 1 letter word\n                    if self.is_final(self._prev):\n                        # case (1) [-2:not space][-1:final letter][cur:space]\n                        self._final_char_logical_score += 1\n                    elif self.is_non_final(self._prev):\n                        # case (2) [-2:not space][-1:Non-Final letter][\n                        #  cur:space]\n                        self._final_char_visual_score += 1\n            else:\n                # Not standing on a space\n                if ((self._before_prev == ' ') and\n                        (self.is_final(self._prev)) and (cur != ' ')):\n                    # case (3) [-2:space][-1:final letter][cur:not space]\n                    self._final_char_visual_score += 1\n            self._before_prev = self._prev\n            self._prev = cur\n\n        # Forever detecting, till the end or until both model probers return\n        # ProbingState.NOT_ME (handled above)\n        return ProbingState.DETECTING\n\n    @property\n    def charset_name(self):\n        # Make the decision: is it Logical or Visual?\n        # If the final letter score distance is dominant enough, rely on it.\n        finalsub = self._final_char_logical_score - self._final_char_visual_score\n        if finalsub >= self.MIN_FINAL_CHAR_DISTANCE:\n            return self.LOGICAL_HEBREW_NAME\n        if finalsub <= -self.MIN_FINAL_CHAR_DISTANCE:\n            return self.VISUAL_HEBREW_NAME\n\n        # It's not dominant enough, try to rely on the model scores instead.\n        modelsub = (self._logical_prober.get_confidence()\n                    - self._visual_prober.get_confidence())\n        if modelsub > self.MIN_MODEL_DISTANCE:\n            return self.LOGICAL_HEBREW_NAME\n        if modelsub < -self.MIN_MODEL_DISTANCE:\n            return self.VISUAL_HEBREW_NAME\n\n        # Still no good, back to final letter distance, maybe it'll save the\n        # day.\n        if finalsub < 0.0:\n            return self.VISUAL_HEBREW_NAME\n\n        # (finalsub > 0 - Logical) or (don't know what to do) default to\n        # Logical.\n        return self.LOGICAL_HEBREW_NAME\n\n    @property\n    def language(self):\n        return 'Hebrew'\n\n    @property\n    def state(self):\n        # Remain active as long as any of the model probers are active.\n        if (self._logical_prober.state == ProbingState.NOT_ME) and \\\n           (self._visual_prober.state == ProbingState.NOT_ME):\n            return ProbingState.NOT_ME\n        return ProbingState.DETECTING\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/jisfreq.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Communicator client code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\n# Sampling from about 20M text materials include literature and computer technology\n#\n# Japanese frequency table, applied to both S-JIS and EUC-JP\n# They are sorted in order.\n\n# 128  --> 0.77094\n# 256  --> 0.85710\n# 512  --> 0.92635\n# 1024 --> 0.97130\n# 2048 --> 0.99431\n#\n# Ideal Distribution Ratio = 0.92635 / (1-0.92635) = 12.58\n# Random Distribution Ration = 512 / (2965+62+83+86-512) = 0.191\n#\n# Typical Distribution Ratio, 25% of IDR\n\nJIS_TYPICAL_DISTRIBUTION_RATIO = 3.0\n\n# Char to FreqOrder table ,\nJIS_TABLE_SIZE = 4368\n\nJIS_CHAR_TO_FREQ_ORDER = (\n  40,   1,   6, 182, 152, 180, 295,2127, 285, 381,3295,4304,3068,4606,3165,3510, #   16\n3511,1822,2785,4607,1193,2226,5070,4608, 171,2996,1247,  18, 179,5071, 856,1661, #   32\n1262,5072, 619, 127,3431,3512,3230,1899,1700, 232, 228,1294,1298, 284, 283,2041, #   48\n2042,1061,1062,  48,  49,  44,  45, 433, 434,1040,1041, 996, 787,2997,1255,4305, #   64\n2108,4609,1684,1648,5073,5074,5075,5076,5077,5078,3687,5079,4610,5080,3927,3928, #   80\n5081,3296,3432, 290,2285,1471,2187,5082,2580,2825,1303,2140,1739,1445,2691,3375, #   96\n1691,3297,4306,4307,4611, 452,3376,1182,2713,3688,3069,4308,5083,5084,5085,5086, #  112\n5087,5088,5089,5090,5091,5092,5093,5094,5095,5096,5097,5098,5099,5100,5101,5102, #  128\n5103,5104,5105,5106,5107,5108,5109,5110,5111,5112,4097,5113,5114,5115,5116,5117, #  144\n5118,5119,5120,5121,5122,5123,5124,5125,5126,5127,5128,5129,5130,5131,5132,5133, #  160\n5134,5135,5136,5137,5138,5139,5140,5141,5142,5143,5144,5145,5146,5147,5148,5149, #  176\n5150,5151,5152,4612,5153,5154,5155,5156,5157,5158,5159,5160,5161,5162,5163,5164, #  192\n5165,5166,5167,5168,5169,5170,5171,5172,5173,5174,5175,1472, 598, 618, 820,1205, #  208\n1309,1412,1858,1307,1692,5176,5177,5178,5179,5180,5181,5182,1142,1452,1234,1172, #  224\n1875,2043,2149,1793,1382,2973, 925,2404,1067,1241, 960,1377,2935,1491, 919,1217, #  240\n1865,2030,1406,1499,2749,4098,5183,5184,5185,5186,5187,5188,2561,4099,3117,1804, #  256\n2049,3689,4309,3513,1663,5189,3166,3118,3298,1587,1561,3433,5190,3119,1625,2998, #  272\n3299,4613,1766,3690,2786,4614,5191,5192,5193,5194,2161,  26,3377,   2,3929,  20, #  288\n3691,  47,4100,  50,  17,  16,  35, 268,  27, 243,  42, 155,  24, 154,  29, 184, #  304\n   4,  91,  14,  92,  53, 396,  33, 289,   9,  37,  64, 620,  21,  39, 321,   5, #  320\n  12,  11,  52,  13,   3, 208, 138,   0,   7,  60, 526, 141, 151,1069, 181, 275, #  336\n1591,  83, 132,1475, 126, 331, 829,  15,  69, 160,  59,  22, 157,  55,1079, 312, #  352\n 109,  38,  23,  25,  10,  19,  79,5195,  61, 382,1124,   8,  30,5196,5197,5198, #  368\n5199,5200,5201,5202,5203,5204,5205,5206,  89,  62,  74,  34,2416, 112, 139, 196, #  384\n 271, 149,  84, 607, 131, 765,  46,  88, 153, 683,  76, 874, 101, 258,  57,  80, #  400\n  32, 364, 121,1508, 169,1547,  68, 235, 145,2999,  41, 360,3027,  70,  63,  31, #  416\n  43, 259, 262,1383,  99, 533, 194,  66,  93, 846, 217, 192,  56, 106,  58, 565, #  432\n 280, 272, 311, 256, 146,  82, 308,  71, 100, 128, 214, 655, 110, 261, 104,1140, #  448\n  54,  51,  36,  87,  67,3070, 185,2618,2936,2020,  28,1066,2390,2059,5207,5208, #  464\n5209,5210,5211,5212,5213,5214,5215,5216,4615,5217,5218,5219,5220,5221,5222,5223, #  480\n5224,5225,5226,5227,5228,5229,5230,5231,5232,5233,5234,5235,5236,3514,5237,5238, #  496\n5239,5240,5241,5242,5243,5244,2297,2031,4616,4310,3692,5245,3071,5246,3598,5247, #  512\n4617,3231,3515,5248,4101,4311,4618,3808,4312,4102,5249,4103,4104,3599,5250,5251, #  528\n5252,5253,5254,5255,5256,5257,5258,5259,5260,5261,5262,5263,5264,5265,5266,5267, #  544\n5268,5269,5270,5271,5272,5273,5274,5275,5276,5277,5278,5279,5280,5281,5282,5283, #  560\n5284,5285,5286,5287,5288,5289,5290,5291,5292,5293,5294,5295,5296,5297,5298,5299, #  576\n5300,5301,5302,5303,5304,5305,5306,5307,5308,5309,5310,5311,5312,5313,5314,5315, #  592\n5316,5317,5318,5319,5320,5321,5322,5323,5324,5325,5326,5327,5328,5329,5330,5331, #  608\n5332,5333,5334,5335,5336,5337,5338,5339,5340,5341,5342,5343,5344,5345,5346,5347, #  624\n5348,5349,5350,5351,5352,5353,5354,5355,5356,5357,5358,5359,5360,5361,5362,5363, #  640\n5364,5365,5366,5367,5368,5369,5370,5371,5372,5373,5374,5375,5376,5377,5378,5379, #  656\n5380,5381, 363, 642,2787,2878,2788,2789,2316,3232,2317,3434,2011, 165,1942,3930, #  672\n3931,3932,3933,5382,4619,5383,4620,5384,5385,5386,5387,5388,5389,5390,5391,5392, #  688\n5393,5394,5395,5396,5397,5398,5399,5400,5401,5402,5403,5404,5405,5406,5407,5408, #  704\n5409,5410,5411,5412,5413,5414,5415,5416,5417,5418,5419,5420,5421,5422,5423,5424, #  720\n5425,5426,5427,5428,5429,5430,5431,5432,5433,5434,5435,5436,5437,5438,5439,5440, #  736\n5441,5442,5443,5444,5445,5446,5447,5448,5449,5450,5451,5452,5453,5454,5455,5456, #  752\n5457,5458,5459,5460,5461,5462,5463,5464,5465,5466,5467,5468,5469,5470,5471,5472, #  768\n5473,5474,5475,5476,5477,5478,5479,5480,5481,5482,5483,5484,5485,5486,5487,5488, #  784\n5489,5490,5491,5492,5493,5494,5495,5496,5497,5498,5499,5500,5501,5502,5503,5504, #  800\n5505,5506,5507,5508,5509,5510,5511,5512,5513,5514,5515,5516,5517,5518,5519,5520, #  816\n5521,5522,5523,5524,5525,5526,5527,5528,5529,5530,5531,5532,5533,5534,5535,5536, #  832\n5537,5538,5539,5540,5541,5542,5543,5544,5545,5546,5547,5548,5549,5550,5551,5552, #  848\n5553,5554,5555,5556,5557,5558,5559,5560,5561,5562,5563,5564,5565,5566,5567,5568, #  864\n5569,5570,5571,5572,5573,5574,5575,5576,5577,5578,5579,5580,5581,5582,5583,5584, #  880\n5585,5586,5587,5588,5589,5590,5591,5592,5593,5594,5595,5596,5597,5598,5599,5600, #  896\n5601,5602,5603,5604,5605,5606,5607,5608,5609,5610,5611,5612,5613,5614,5615,5616, #  912\n5617,5618,5619,5620,5621,5622,5623,5624,5625,5626,5627,5628,5629,5630,5631,5632, #  928\n5633,5634,5635,5636,5637,5638,5639,5640,5641,5642,5643,5644,5645,5646,5647,5648, #  944\n5649,5650,5651,5652,5653,5654,5655,5656,5657,5658,5659,5660,5661,5662,5663,5664, #  960\n5665,5666,5667,5668,5669,5670,5671,5672,5673,5674,5675,5676,5677,5678,5679,5680, #  976\n5681,5682,5683,5684,5685,5686,5687,5688,5689,5690,5691,5692,5693,5694,5695,5696, #  992\n5697,5698,5699,5700,5701,5702,5703,5704,5705,5706,5707,5708,5709,5710,5711,5712, # 1008\n5713,5714,5715,5716,5717,5718,5719,5720,5721,5722,5723,5724,5725,5726,5727,5728, # 1024\n5729,5730,5731,5732,5733,5734,5735,5736,5737,5738,5739,5740,5741,5742,5743,5744, # 1040\n5745,5746,5747,5748,5749,5750,5751,5752,5753,5754,5755,5756,5757,5758,5759,5760, # 1056\n5761,5762,5763,5764,5765,5766,5767,5768,5769,5770,5771,5772,5773,5774,5775,5776, # 1072\n5777,5778,5779,5780,5781,5782,5783,5784,5785,5786,5787,5788,5789,5790,5791,5792, # 1088\n5793,5794,5795,5796,5797,5798,5799,5800,5801,5802,5803,5804,5805,5806,5807,5808, # 1104\n5809,5810,5811,5812,5813,5814,5815,5816,5817,5818,5819,5820,5821,5822,5823,5824, # 1120\n5825,5826,5827,5828,5829,5830,5831,5832,5833,5834,5835,5836,5837,5838,5839,5840, # 1136\n5841,5842,5843,5844,5845,5846,5847,5848,5849,5850,5851,5852,5853,5854,5855,5856, # 1152\n5857,5858,5859,5860,5861,5862,5863,5864,5865,5866,5867,5868,5869,5870,5871,5872, # 1168\n5873,5874,5875,5876,5877,5878,5879,5880,5881,5882,5883,5884,5885,5886,5887,5888, # 1184\n5889,5890,5891,5892,5893,5894,5895,5896,5897,5898,5899,5900,5901,5902,5903,5904, # 1200\n5905,5906,5907,5908,5909,5910,5911,5912,5913,5914,5915,5916,5917,5918,5919,5920, # 1216\n5921,5922,5923,5924,5925,5926,5927,5928,5929,5930,5931,5932,5933,5934,5935,5936, # 1232\n5937,5938,5939,5940,5941,5942,5943,5944,5945,5946,5947,5948,5949,5950,5951,5952, # 1248\n5953,5954,5955,5956,5957,5958,5959,5960,5961,5962,5963,5964,5965,5966,5967,5968, # 1264\n5969,5970,5971,5972,5973,5974,5975,5976,5977,5978,5979,5980,5981,5982,5983,5984, # 1280\n5985,5986,5987,5988,5989,5990,5991,5992,5993,5994,5995,5996,5997,5998,5999,6000, # 1296\n6001,6002,6003,6004,6005,6006,6007,6008,6009,6010,6011,6012,6013,6014,6015,6016, # 1312\n6017,6018,6019,6020,6021,6022,6023,6024,6025,6026,6027,6028,6029,6030,6031,6032, # 1328\n6033,6034,6035,6036,6037,6038,6039,6040,6041,6042,6043,6044,6045,6046,6047,6048, # 1344\n6049,6050,6051,6052,6053,6054,6055,6056,6057,6058,6059,6060,6061,6062,6063,6064, # 1360\n6065,6066,6067,6068,6069,6070,6071,6072,6073,6074,6075,6076,6077,6078,6079,6080, # 1376\n6081,6082,6083,6084,6085,6086,6087,6088,6089,6090,6091,6092,6093,6094,6095,6096, # 1392\n6097,6098,6099,6100,6101,6102,6103,6104,6105,6106,6107,6108,6109,6110,6111,6112, # 1408\n6113,6114,2044,2060,4621, 997,1235, 473,1186,4622, 920,3378,6115,6116, 379,1108, # 1424\n4313,2657,2735,3934,6117,3809, 636,3233, 573,1026,3693,3435,2974,3300,2298,4105, # 1440\n 854,2937,2463, 393,2581,2417, 539, 752,1280,2750,2480, 140,1161, 440, 708,1569, # 1456\n 665,2497,1746,1291,1523,3000, 164,1603, 847,1331, 537,1997, 486, 508,1693,2418, # 1472\n1970,2227, 878,1220, 299,1030, 969, 652,2751, 624,1137,3301,2619,  65,3302,2045, # 1488\n1761,1859,3120,1930,3694,3516, 663,1767, 852, 835,3695, 269, 767,2826,2339,1305, # 1504\n 896,1150, 770,1616,6118, 506,1502,2075,1012,2519, 775,2520,2975,2340,2938,4314, # 1520\n3028,2086,1224,1943,2286,6119,3072,4315,2240,1273,1987,3935,1557, 175, 597, 985, # 1536\n3517,2419,2521,1416,3029, 585, 938,1931,1007,1052,1932,1685,6120,3379,4316,4623, # 1552\n 804, 599,3121,1333,2128,2539,1159,1554,2032,3810, 687,2033,2904, 952, 675,1467, # 1568\n3436,6121,2241,1096,1786,2440,1543,1924, 980,1813,2228, 781,2692,1879, 728,1918, # 1584\n3696,4624, 548,1950,4625,1809,1088,1356,3303,2522,1944, 502, 972, 373, 513,2827, # 1600\n 586,2377,2391,1003,1976,1631,6122,2464,1084, 648,1776,4626,2141, 324, 962,2012, # 1616\n2177,2076,1384, 742,2178,1448,1173,1810, 222, 102, 301, 445, 125,2420, 662,2498, # 1632\n 277, 200,1476,1165,1068, 224,2562,1378,1446, 450,1880, 659, 791, 582,4627,2939, # 1648\n3936,1516,1274, 555,2099,3697,1020,1389,1526,3380,1762,1723,1787,2229, 412,2114, # 1664\n1900,2392,3518, 512,2597, 427,1925,2341,3122,1653,1686,2465,2499, 697, 330, 273, # 1680\n 380,2162, 951, 832, 780, 991,1301,3073, 965,2270,3519, 668,2523,2636,1286, 535, # 1696\n1407, 518, 671, 957,2658,2378, 267, 611,2197,3030,6123, 248,2299, 967,1799,2356, # 1712\n 850,1418,3437,1876,1256,1480,2828,1718,6124,6125,1755,1664,2405,6126,4628,2879, # 1728\n2829, 499,2179, 676,4629, 557,2329,2214,2090, 325,3234, 464, 811,3001, 992,2342, # 1744\n2481,1232,1469, 303,2242, 466,1070,2163, 603,1777,2091,4630,2752,4631,2714, 322, # 1760\n2659,1964,1768, 481,2188,1463,2330,2857,3600,2092,3031,2421,4632,2318,2070,1849, # 1776\n2598,4633,1302,2254,1668,1701,2422,3811,2905,3032,3123,2046,4106,1763,1694,4634, # 1792\n1604, 943,1724,1454, 917, 868,2215,1169,2940, 552,1145,1800,1228,1823,1955, 316, # 1808\n1080,2510, 361,1807,2830,4107,2660,3381,1346,1423,1134,4108,6127, 541,1263,1229, # 1824\n1148,2540, 545, 465,1833,2880,3438,1901,3074,2482, 816,3937, 713,1788,2500, 122, # 1840\n1575, 195,1451,2501,1111,6128, 859, 374,1225,2243,2483,4317, 390,1033,3439,3075, # 1856\n2524,1687, 266, 793,1440,2599, 946, 779, 802, 507, 897,1081, 528,2189,1292, 711, # 1872\n1866,1725,1167,1640, 753, 398,2661,1053, 246, 348,4318, 137,1024,3440,1600,2077, # 1888\n2129, 825,4319, 698, 238, 521, 187,2300,1157,2423,1641,1605,1464,1610,1097,2541, # 1904\n1260,1436, 759,2255,1814,2150, 705,3235, 409,2563,3304, 561,3033,2005,2564, 726, # 1920\n1956,2343,3698,4109, 949,3812,3813,3520,1669, 653,1379,2525, 881,2198, 632,2256, # 1936\n1027, 778,1074, 733,1957, 514,1481,2466, 554,2180, 702,3938,1606,1017,1398,6129, # 1952\n1380,3521, 921, 993,1313, 594, 449,1489,1617,1166, 768,1426,1360, 495,1794,3601, # 1968\n1177,3602,1170,4320,2344, 476, 425,3167,4635,3168,1424, 401,2662,1171,3382,1998, # 1984\n1089,4110, 477,3169, 474,6130,1909, 596,2831,1842, 494, 693,1051,1028,1207,3076, # 2000\n 606,2115, 727,2790,1473,1115, 743,3522, 630, 805,1532,4321,2021, 366,1057, 838, # 2016\n 684,1114,2142,4322,2050,1492,1892,1808,2271,3814,2424,1971,1447,1373,3305,1090, # 2032\n1536,3939,3523,3306,1455,2199, 336, 369,2331,1035, 584,2393, 902, 718,2600,6131, # 2048\n2753, 463,2151,1149,1611,2467, 715,1308,3124,1268, 343,1413,3236,1517,1347,2663, # 2064\n2093,3940,2022,1131,1553,2100,2941,1427,3441,2942,1323,2484,6132,1980, 872,2368, # 2080\n2441,2943, 320,2369,2116,1082, 679,1933,3941,2791,3815, 625,1143,2023, 422,2200, # 2096\n3816,6133, 730,1695, 356,2257,1626,2301,2858,2637,1627,1778, 937, 883,2906,2693, # 2112\n3002,1769,1086, 400,1063,1325,3307,2792,4111,3077, 456,2345,1046, 747,6134,1524, # 2128\n 884,1094,3383,1474,2164,1059, 974,1688,2181,2258,1047, 345,1665,1187, 358, 875, # 2144\n3170, 305, 660,3524,2190,1334,1135,3171,1540,1649,2542,1527, 927, 968,2793, 885, # 2160\n1972,1850, 482, 500,2638,1218,1109,1085,2543,1654,2034, 876,  78,2287,1482,1277, # 2176\n 861,1675,1083,1779, 724,2754, 454, 397,1132,1612,2332, 893, 672,1237, 257,2259, # 2192\n2370, 135,3384, 337,2244, 547, 352, 340, 709,2485,1400, 788,1138,2511, 540, 772, # 2208\n1682,2260,2272,2544,2013,1843,1902,4636,1999,1562,2288,4637,2201,1403,1533, 407, # 2224\n 576,3308,1254,2071, 978,3385, 170, 136,1201,3125,2664,3172,2394, 213, 912, 873, # 2240\n3603,1713,2202, 699,3604,3699, 813,3442, 493, 531,1054, 468,2907,1483, 304, 281, # 2256\n4112,1726,1252,2094, 339,2319,2130,2639, 756,1563,2944, 748, 571,2976,1588,2425, # 2272\n2715,1851,1460,2426,1528,1392,1973,3237, 288,3309, 685,3386, 296, 892,2716,2216, # 2288\n1570,2245, 722,1747,2217, 905,3238,1103,6135,1893,1441,1965, 251,1805,2371,3700, # 2304\n2601,1919,1078,  75,2182,1509,1592,1270,2640,4638,2152,6136,3310,3817, 524, 706, # 2320\n1075, 292,3818,1756,2602, 317,  98,3173,3605,3525,1844,2218,3819,2502, 814, 567, # 2336\n 385,2908,1534,6137, 534,1642,3239, 797,6138,1670,1529, 953,4323, 188,1071, 538, # 2352\n 178, 729,3240,2109,1226,1374,2000,2357,2977, 731,2468,1116,2014,2051,6139,1261, # 2368\n1593, 803,2859,2736,3443, 556, 682, 823,1541,6140,1369,2289,1706,2794, 845, 462, # 2384\n2603,2665,1361, 387, 162,2358,1740, 739,1770,1720,1304,1401,3241,1049, 627,1571, # 2400\n2427,3526,1877,3942,1852,1500, 431,1910,1503, 677, 297,2795, 286,1433,1038,1198, # 2416\n2290,1133,1596,4113,4639,2469,1510,1484,3943,6141,2442, 108, 712,4640,2372, 866, # 2432\n3701,2755,3242,1348, 834,1945,1408,3527,2395,3243,1811, 824, 994,1179,2110,1548, # 2448\n1453, 790,3003, 690,4324,4325,2832,2909,3820,1860,3821, 225,1748, 310, 346,1780, # 2464\n2470, 821,1993,2717,2796, 828, 877,3528,2860,2471,1702,2165,2910,2486,1789, 453, # 2480\n 359,2291,1676,  73,1164,1461,1127,3311, 421, 604, 314,1037, 589, 116,2487, 737, # 2496\n 837,1180, 111, 244, 735,6142,2261,1861,1362, 986, 523, 418, 581,2666,3822, 103, # 2512\n 855, 503,1414,1867,2488,1091, 657,1597, 979, 605,1316,4641,1021,2443,2078,2001, # 2528\n1209,  96, 587,2166,1032, 260,1072,2153, 173,  94, 226,3244, 819,2006,4642,4114, # 2544\n2203, 231,1744, 782,  97,2667, 786,3387, 887, 391, 442,2219,4326,1425,6143,2694, # 2560\n 633,1544,1202, 483,2015, 592,2052,1958,2472,1655, 419, 129,4327,3444,3312,1714, # 2576\n1257,3078,4328,1518,1098, 865,1310,1019,1885,1512,1734, 469,2444, 148, 773, 436, # 2592\n1815,1868,1128,1055,4329,1245,2756,3445,2154,1934,1039,4643, 579,1238, 932,2320, # 2608\n 353, 205, 801, 115,2428, 944,2321,1881, 399,2565,1211, 678, 766,3944, 335,2101, # 2624\n1459,1781,1402,3945,2737,2131,1010, 844, 981,1326,1013, 550,1816,1545,2620,1335, # 2640\n1008, 371,2881, 936,1419,1613,3529,1456,1395,2273,1834,2604,1317,2738,2503, 416, # 2656\n1643,4330, 806,1126, 229, 591,3946,1314,1981,1576,1837,1666, 347,1790, 977,3313, # 2672\n 764,2861,1853, 688,2429,1920,1462,  77, 595, 415,2002,3034, 798,1192,4115,6144, # 2688\n2978,4331,3035,2695,2582,2072,2566, 430,2430,1727, 842,1396,3947,3702, 613, 377, # 2704\n 278, 236,1417,3388,3314,3174, 757,1869, 107,3530,6145,1194, 623,2262, 207,1253, # 2720\n2167,3446,3948, 492,1117,1935, 536,1838,2757,1246,4332, 696,2095,2406,1393,1572, # 2736\n3175,1782, 583, 190, 253,1390,2230, 830,3126,3389, 934,3245,1703,1749,2979,1870, # 2752\n2545,1656,2204, 869,2346,4116,3176,1817, 496,1764,4644, 942,1504, 404,1903,1122, # 2768\n1580,3606,2945,1022, 515, 372,1735, 955,2431,3036,6146,2797,1110,2302,2798, 617, # 2784\n6147, 441, 762,1771,3447,3607,3608,1904, 840,3037,  86, 939,1385, 572,1370,2445, # 2800\n1336, 114,3703, 898, 294, 203,3315, 703,1583,2274, 429, 961,4333,1854,1951,3390, # 2816\n2373,3704,4334,1318,1381, 966,1911,2322,1006,1155, 309, 989, 458,2718,1795,1372, # 2832\n1203, 252,1689,1363,3177, 517,1936, 168,1490, 562, 193,3823,1042,4117,1835, 551, # 2848\n 470,4645, 395, 489,3448,1871,1465,2583,2641, 417,1493, 279,1295, 511,1236,1119, # 2864\n  72,1231,1982,1812,3004, 871,1564, 984,3449,1667,2696,2096,4646,2347,2833,1673, # 2880\n3609, 695,3246,2668, 807,1183,4647, 890, 388,2333,1801,1457,2911,1765,1477,1031, # 2896\n3316,3317,1278,3391,2799,2292,2526, 163,3450,4335,2669,1404,1802,6148,2323,2407, # 2912\n1584,1728,1494,1824,1269, 298, 909,3318,1034,1632, 375, 776,1683,2061, 291, 210, # 2928\n1123, 809,1249,1002,2642,3038, 206,1011,2132, 144, 975, 882,1565, 342, 667, 754, # 2944\n1442,2143,1299,2303,2062, 447, 626,2205,1221,2739,2912,1144,1214,2206,2584, 760, # 2960\n1715, 614, 950,1281,2670,2621, 810, 577,1287,2546,4648, 242,2168, 250,2643, 691, # 2976\n 123,2644, 647, 313,1029, 689,1357,2946,1650, 216, 771,1339,1306, 808,2063, 549, # 2992\n 913,1371,2913,2914,6149,1466,1092,1174,1196,1311,2605,2396,1783,1796,3079, 406, # 3008\n2671,2117,3949,4649, 487,1825,2220,6150,2915, 448,2348,1073,6151,2397,1707, 130, # 3024\n 900,1598, 329, 176,1959,2527,1620,6152,2275,4336,3319,1983,2191,3705,3610,2155, # 3040\n3706,1912,1513,1614,6153,1988, 646, 392,2304,1589,3320,3039,1826,1239,1352,1340, # 3056\n2916, 505,2567,1709,1437,2408,2547, 906,6154,2672, 384,1458,1594,1100,1329, 710, # 3072\n 423,3531,2064,2231,2622,1989,2673,1087,1882, 333, 841,3005,1296,2882,2379, 580, # 3088\n1937,1827,1293,2585, 601, 574, 249,1772,4118,2079,1120, 645, 901,1176,1690, 795, # 3104\n2207, 478,1434, 516,1190,1530, 761,2080, 930,1264, 355, 435,1552, 644,1791, 987, # 3120\n 220,1364,1163,1121,1538, 306,2169,1327,1222, 546,2645, 218, 241, 610,1704,3321, # 3136\n1984,1839,1966,2528, 451,6155,2586,3707,2568, 907,3178, 254,2947, 186,1845,4650, # 3152\n 745, 432,1757, 428,1633, 888,2246,2221,2489,3611,2118,1258,1265, 956,3127,1784, # 3168\n4337,2490, 319, 510, 119, 457,3612, 274,2035,2007,4651,1409,3128, 970,2758, 590, # 3184\n2800, 661,2247,4652,2008,3950,1420,1549,3080,3322,3951,1651,1375,2111, 485,2491, # 3200\n1429,1156,6156,2548,2183,1495, 831,1840,2529,2446, 501,1657, 307,1894,3247,1341, # 3216\n 666, 899,2156,1539,2549,1559, 886, 349,2208,3081,2305,1736,3824,2170,2759,1014, # 3232\n1913,1386, 542,1397,2948, 490, 368, 716, 362, 159, 282,2569,1129,1658,1288,1750, # 3248\n2674, 276, 649,2016, 751,1496, 658,1818,1284,1862,2209,2087,2512,3451, 622,2834, # 3264\n 376, 117,1060,2053,1208,1721,1101,1443, 247,1250,3179,1792,3952,2760,2398,3953, # 3280\n6157,2144,3708, 446,2432,1151,2570,3452,2447,2761,2835,1210,2448,3082, 424,2222, # 3296\n1251,2449,2119,2836, 504,1581,4338, 602, 817, 857,3825,2349,2306, 357,3826,1470, # 3312\n1883,2883, 255, 958, 929,2917,3248, 302,4653,1050,1271,1751,2307,1952,1430,2697, # 3328\n2719,2359, 354,3180, 777, 158,2036,4339,1659,4340,4654,2308,2949,2248,1146,2232, # 3344\n3532,2720,1696,2623,3827,6158,3129,1550,2698,1485,1297,1428, 637, 931,2721,2145, # 3360\n 914,2550,2587,  81,2450, 612, 827,2646,1242,4655,1118,2884, 472,1855,3181,3533, # 3376\n3534, 569,1353,2699,1244,1758,2588,4119,2009,2762,2171,3709,1312,1531,6159,1152, # 3392\n1938, 134,1830, 471,3710,2276,1112,1535,3323,3453,3535, 982,1337,2950, 488, 826, # 3408\n 674,1058,1628,4120,2017, 522,2399, 211, 568,1367,3454, 350, 293,1872,1139,3249, # 3424\n1399,1946,3006,1300,2360,3324, 588, 736,6160,2606, 744, 669,3536,3828,6161,1358, # 3440\n 199, 723, 848, 933, 851,1939,1505,1514,1338,1618,1831,4656,1634,3613, 443,2740, # 3456\n3829, 717,1947, 491,1914,6162,2551,1542,4121,1025,6163,1099,1223, 198,3040,2722, # 3472\n 370, 410,1905,2589, 998,1248,3182,2380, 519,1449,4122,1710, 947, 928,1153,4341, # 3488\n2277, 344,2624,1511, 615, 105, 161,1212,1076,1960,3130,2054,1926,1175,1906,2473, # 3504\n 414,1873,2801,6164,2309, 315,1319,3325, 318,2018,2146,2157, 963, 631, 223,4342, # 3520\n4343,2675, 479,3711,1197,2625,3712,2676,2361,6165,4344,4123,6166,2451,3183,1886, # 3536\n2184,1674,1330,1711,1635,1506, 799, 219,3250,3083,3954,1677,3713,3326,2081,3614, # 3552\n1652,2073,4657,1147,3041,1752, 643,1961, 147,1974,3955,6167,1716,2037, 918,3007, # 3568\n1994, 120,1537, 118, 609,3184,4345, 740,3455,1219, 332,1615,3830,6168,1621,2980, # 3584\n1582, 783, 212, 553,2350,3714,1349,2433,2082,4124, 889,6169,2310,1275,1410, 973, # 3600\n 166,1320,3456,1797,1215,3185,2885,1846,2590,2763,4658, 629, 822,3008, 763, 940, # 3616\n1990,2862, 439,2409,1566,1240,1622, 926,1282,1907,2764, 654,2210,1607, 327,1130, # 3632\n3956,1678,1623,6170,2434,2192, 686, 608,3831,3715, 903,3957,3042,6171,2741,1522, # 3648\n1915,1105,1555,2552,1359, 323,3251,4346,3457, 738,1354,2553,2311,2334,1828,2003, # 3664\n3832,1753,2351,1227,6172,1887,4125,1478,6173,2410,1874,1712,1847, 520,1204,2607, # 3680\n 264,4659, 836,2677,2102, 600,4660,3833,2278,3084,6174,4347,3615,1342, 640, 532, # 3696\n 543,2608,1888,2400,2591,1009,4348,1497, 341,1737,3616,2723,1394, 529,3252,1321, # 3712\n 983,4661,1515,2120, 971,2592, 924, 287,1662,3186,4349,2700,4350,1519, 908,1948, # 3728\n2452, 156, 796,1629,1486,2223,2055, 694,4126,1259,1036,3392,1213,2249,2742,1889, # 3744\n1230,3958,1015, 910, 408, 559,3617,4662, 746, 725, 935,4663,3959,3009,1289, 563, # 3760\n 867,4664,3960,1567,2981,2038,2626, 988,2263,2381,4351, 143,2374, 704,1895,6175, # 3776\n1188,3716,2088, 673,3085,2362,4352, 484,1608,1921,2765,2918, 215, 904,3618,3537, # 3792\n 894, 509, 976,3043,2701,3961,4353,2837,2982, 498,6176,6177,1102,3538,1332,3393, # 3808\n1487,1636,1637, 233, 245,3962, 383, 650, 995,3044, 460,1520,1206,2352, 749,3327, # 3824\n 530, 700, 389,1438,1560,1773,3963,2264, 719,2951,2724,3834, 870,1832,1644,1000, # 3840\n 839,2474,3717, 197,1630,3394, 365,2886,3964,1285,2133, 734, 922, 818,1106, 732, # 3856\n 480,2083,1774,3458, 923,2279,1350, 221,3086,  85,2233,2234,3835,1585,3010,2147, # 3872\n1387,1705,2382,1619,2475, 133, 239,2802,1991,1016,2084,2383, 411,2838,1113, 651, # 3888\n1985,1160,3328, 990,1863,3087,1048,1276,2647, 265,2627,1599,3253,2056, 150, 638, # 3904\n2019, 656, 853, 326,1479, 680,1439,4354,1001,1759, 413,3459,3395,2492,1431, 459, # 3920\n4355,1125,3329,2265,1953,1450,2065,2863, 849, 351,2678,3131,3254,3255,1104,1577, # 3936\n 227,1351,1645,2453,2193,1421,2887, 812,2121, 634,  95,2435, 201,2312,4665,1646, # 3952\n1671,2743,1601,2554,2702,2648,2280,1315,1366,2089,3132,1573,3718,3965,1729,1189, # 3968\n 328,2679,1077,1940,1136, 558,1283, 964,1195, 621,2074,1199,1743,3460,3619,1896, # 3984\n1916,1890,3836,2952,1154,2112,1064, 862, 378,3011,2066,2113,2803,1568,2839,6178, # 4000\n3088,2919,1941,1660,2004,1992,2194, 142, 707,1590,1708,1624,1922,1023,1836,1233, # 4016\n1004,2313, 789, 741,3620,6179,1609,2411,1200,4127,3719,3720,4666,2057,3721, 593, # 4032\n2840, 367,2920,1878,6180,3461,1521, 628,1168, 692,2211,2649, 300, 720,2067,2571, # 4048\n2953,3396, 959,2504,3966,3539,3462,1977, 701,6181, 954,1043, 800, 681, 183,3722, # 4064\n1803,1730,3540,4128,2103, 815,2314, 174, 467, 230,2454,1093,2134, 755,3541,3397, # 4080\n1141,1162,6182,1738,2039, 270,3256,2513,1005,1647,2185,3837, 858,1679,1897,1719, # 4096\n2954,2324,1806, 402, 670, 167,4129,1498,2158,2104, 750,6183, 915, 189,1680,1551, # 4112\n 455,4356,1501,2455, 405,1095,2955, 338,1586,1266,1819, 570, 641,1324, 237,1556, # 4128\n2650,1388,3723,6184,1368,2384,1343,1978,3089,2436, 879,3724, 792,1191, 758,3012, # 4144\n1411,2135,1322,4357, 240,4667,1848,3725,1574,6185, 420,3045,1546,1391, 714,4358, # 4160\n1967, 941,1864, 863, 664, 426, 560,1731,2680,1785,2864,1949,2363, 403,3330,1415, # 4176\n1279,2136,1697,2335, 204, 721,2097,3838,  90,6186,2085,2505, 191,3967, 124,2148, # 4192\n1376,1798,1178,1107,1898,1405, 860,4359,1243,1272,2375,2983,1558,2456,1638, 113, # 4208\n3621, 578,1923,2609, 880, 386,4130, 784,2186,2266,1422,2956,2172,1722, 497, 263, # 4224\n2514,1267,2412,2610, 177,2703,3542, 774,1927,1344, 616,1432,1595,1018, 172,4360, # 4240\n2325, 911,4361, 438,1468,3622, 794,3968,2024,2173,1681,1829,2957, 945, 895,3090, # 4256\n 575,2212,2476, 475,2401,2681, 785,2744,1745,2293,2555,1975,3133,2865, 394,4668, # 4272\n3839, 635,4131, 639, 202,1507,2195,2766,1345,1435,2572,3726,1908,1184,1181,2457, # 4288\n3727,3134,4362, 843,2611, 437, 916,4669, 234, 769,1884,3046,3047,3623, 833,6187, # 4304\n1639,2250,2402,1355,1185,2010,2047, 999, 525,1732,1290,1488,2612, 948,1578,3728, # 4320\n2413,2477,1216,2725,2159, 334,3840,1328,3624,2921,1525,4132, 564,1056, 891,4363, # 4336\n1444,1698,2385,2251,3729,1365,2281,2235,1717,6188, 864,3841,2515, 444, 527,2767, # 4352\n2922,3625, 544, 461,6189, 566, 209,2437,3398,2098,1065,2068,3331,3626,3257,2137, # 4368  #last 512\n)\n\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/jpcntx.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Communicator client code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\n\n# This is hiragana 2-char sequence table, the number in each cell represents its frequency category\njp2CharContext = (\n(0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1),\n(2,4,0,4,0,3,0,4,0,3,4,4,4,2,4,3,3,4,3,2,3,3,4,2,3,3,3,2,4,1,4,3,3,1,5,4,3,4,3,4,3,5,3,0,3,5,4,2,0,3,1,0,3,3,0,3,3,0,1,1,0,4,3,0,3,3,0,4,0,2,0,3,5,5,5,5,4,0,4,1,0,3,4),\n(0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2),\n(0,4,0,5,0,5,0,4,0,4,5,4,4,3,5,3,5,1,5,3,4,3,4,4,3,4,3,3,4,3,5,4,4,3,5,5,3,5,5,5,3,5,5,3,4,5,5,3,1,3,2,0,3,4,0,4,2,0,4,2,1,5,3,2,3,5,0,4,0,2,0,5,4,4,5,4,5,0,4,0,0,4,4),\n(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),\n(0,3,0,4,0,3,0,3,0,4,5,4,3,3,3,3,4,3,5,4,4,3,5,4,4,3,4,3,4,4,4,4,5,3,4,4,3,4,5,5,4,5,5,1,4,5,4,3,0,3,3,1,3,3,0,4,4,0,3,3,1,5,3,3,3,5,0,4,0,3,0,4,4,3,4,3,3,0,4,1,1,3,4),\n(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),\n(0,4,0,3,0,3,0,4,0,3,4,4,3,2,2,1,2,1,3,1,3,3,3,3,3,4,3,1,3,3,5,3,3,0,4,3,0,5,4,3,3,5,4,4,3,4,4,5,0,1,2,0,1,2,0,2,2,0,1,0,0,5,2,2,1,4,0,3,0,1,0,4,4,3,5,4,3,0,2,1,0,4,3),\n(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),\n(0,3,0,5,0,4,0,2,1,4,4,2,4,1,4,2,4,2,4,3,3,3,4,3,3,3,3,1,4,2,3,3,3,1,4,4,1,1,1,4,3,3,2,0,2,4,3,2,0,3,3,0,3,1,1,0,0,0,3,3,0,4,2,2,3,4,0,4,0,3,0,4,4,5,3,4,4,0,3,0,0,1,4),\n(1,4,0,4,0,4,0,4,0,3,5,4,4,3,4,3,5,4,3,3,4,3,5,4,4,4,4,3,4,2,4,3,3,1,5,4,3,2,4,5,4,5,5,4,4,5,4,4,0,3,2,2,3,3,0,4,3,1,3,2,1,4,3,3,4,5,0,3,0,2,0,4,5,5,4,5,4,0,4,0,0,5,4),\n(0,5,0,5,0,4,0,3,0,4,4,3,4,3,3,3,4,0,4,4,4,3,4,3,4,3,3,1,4,2,4,3,4,0,5,4,1,4,5,4,4,5,3,2,4,3,4,3,2,4,1,3,3,3,2,3,2,0,4,3,3,4,3,3,3,4,0,4,0,3,0,4,5,4,4,4,3,0,4,1,0,1,3),\n(0,3,1,4,0,3,0,2,0,3,4,4,3,1,4,2,3,3,4,3,4,3,4,3,4,4,3,2,3,1,5,4,4,1,4,4,3,5,4,4,3,5,5,4,3,4,4,3,1,2,3,1,2,2,0,3,2,0,3,1,0,5,3,3,3,4,3,3,3,3,4,4,4,4,5,4,2,0,3,3,2,4,3),\n(0,2,0,3,0,1,0,1,0,0,3,2,0,0,2,0,1,0,2,1,3,3,3,1,2,3,1,0,1,0,4,2,1,1,3,3,0,4,3,3,1,4,3,3,0,3,3,2,0,0,0,0,1,0,0,2,0,0,0,0,0,4,1,0,2,3,2,2,2,1,3,3,3,4,4,3,2,0,3,1,0,3,3),\n(0,4,0,4,0,3,0,3,0,4,4,4,3,3,3,3,3,3,4,3,4,2,4,3,4,3,3,2,4,3,4,5,4,1,4,5,3,5,4,5,3,5,4,0,3,5,5,3,1,3,3,2,2,3,0,3,4,1,3,3,2,4,3,3,3,4,0,4,0,3,0,4,5,4,4,5,3,0,4,1,0,3,4),\n(0,2,0,3,0,3,0,0,0,2,2,2,1,0,1,0,0,0,3,0,3,0,3,0,1,3,1,0,3,1,3,3,3,1,3,3,3,0,1,3,1,3,4,0,0,3,1,1,0,3,2,0,0,0,0,1,3,0,1,0,0,3,3,2,0,3,0,0,0,0,0,3,4,3,4,3,3,0,3,0,0,2,3),\n(2,3,0,3,0,2,0,1,0,3,3,4,3,1,3,1,1,1,3,1,4,3,4,3,3,3,0,0,3,1,5,4,3,1,4,3,2,5,5,4,4,4,4,3,3,4,4,4,0,2,1,1,3,2,0,1,2,0,0,1,0,4,1,3,3,3,0,3,0,1,0,4,4,4,5,5,3,0,2,0,0,4,4),\n(0,2,0,1,0,3,1,3,0,2,3,3,3,0,3,1,0,0,3,0,3,2,3,1,3,2,1,1,0,0,4,2,1,0,2,3,1,4,3,2,0,4,4,3,1,3,1,3,0,1,0,0,1,0,0,0,1,0,0,0,0,4,1,1,1,2,0,3,0,0,0,3,4,2,4,3,2,0,1,0,0,3,3),\n(0,1,0,4,0,5,0,4,0,2,4,4,2,3,3,2,3,3,5,3,3,3,4,3,4,2,3,0,4,3,3,3,4,1,4,3,2,1,5,5,3,4,5,1,3,5,4,2,0,3,3,0,1,3,0,4,2,0,1,3,1,4,3,3,3,3,0,3,0,1,0,3,4,4,4,5,5,0,3,0,1,4,5),\n(0,2,0,3,0,3,0,0,0,2,3,1,3,0,4,0,1,1,3,0,3,4,3,2,3,1,0,3,3,2,3,1,3,0,2,3,0,2,1,4,1,2,2,0,0,3,3,0,0,2,0,0,0,1,0,0,0,0,2,2,0,3,2,1,3,3,0,2,0,2,0,0,3,3,1,2,4,0,3,0,2,2,3),\n(2,4,0,5,0,4,0,4,0,2,4,4,4,3,4,3,3,3,1,2,4,3,4,3,4,4,5,0,3,3,3,3,2,0,4,3,1,4,3,4,1,4,4,3,3,4,4,3,1,2,3,0,4,2,0,4,1,0,3,3,0,4,3,3,3,4,0,4,0,2,0,3,5,3,4,5,2,0,3,0,0,4,5),\n(0,3,0,4,0,1,0,1,0,1,3,2,2,1,3,0,3,0,2,0,2,0,3,0,2,0,0,0,1,0,1,1,0,0,3,1,0,0,0,4,0,3,1,0,2,1,3,0,0,0,0,0,0,3,0,0,0,0,0,0,0,4,2,2,3,1,0,3,0,0,0,1,4,4,4,3,0,0,4,0,0,1,4),\n(1,4,1,5,0,3,0,3,0,4,5,4,4,3,5,3,3,4,4,3,4,1,3,3,3,3,2,1,4,1,5,4,3,1,4,4,3,5,4,4,3,5,4,3,3,4,4,4,0,3,3,1,2,3,0,3,1,0,3,3,0,5,4,4,4,4,4,4,3,3,5,4,4,3,3,5,4,0,3,2,0,4,4),\n(0,2,0,3,0,1,0,0,0,1,3,3,3,2,4,1,3,0,3,1,3,0,2,2,1,1,0,0,2,0,4,3,1,0,4,3,0,4,4,4,1,4,3,1,1,3,3,1,0,2,0,0,1,3,0,0,0,0,2,0,0,4,3,2,4,3,5,4,3,3,3,4,3,3,4,3,3,0,2,1,0,3,3),\n(0,2,0,4,0,3,0,2,0,2,5,5,3,4,4,4,4,1,4,3,3,0,4,3,4,3,1,3,3,2,4,3,0,3,4,3,0,3,4,4,2,4,4,0,4,5,3,3,2,2,1,1,1,2,0,1,5,0,3,3,2,4,3,3,3,4,0,3,0,2,0,4,4,3,5,5,0,0,3,0,2,3,3),\n(0,3,0,4,0,3,0,1,0,3,4,3,3,1,3,3,3,0,3,1,3,0,4,3,3,1,1,0,3,0,3,3,0,0,4,4,0,1,5,4,3,3,5,0,3,3,4,3,0,2,0,1,1,1,0,1,3,0,1,2,1,3,3,2,3,3,0,3,0,1,0,1,3,3,4,4,1,0,1,2,2,1,3),\n(0,1,0,4,0,4,0,3,0,1,3,3,3,2,3,1,1,0,3,0,3,3,4,3,2,4,2,0,1,0,4,3,2,0,4,3,0,5,3,3,2,4,4,4,3,3,3,4,0,1,3,0,0,1,0,0,1,0,0,0,0,4,2,3,3,3,0,3,0,0,0,4,4,4,5,3,2,0,3,3,0,3,5),\n(0,2,0,3,0,0,0,3,0,1,3,0,2,0,0,0,1,0,3,1,1,3,3,0,0,3,0,0,3,0,2,3,1,0,3,1,0,3,3,2,0,4,2,2,0,2,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,2,1,2,0,1,0,1,0,0,0,1,3,1,2,0,0,0,1,0,0,1,4),\n(0,3,0,3,0,5,0,1,0,2,4,3,1,3,3,2,1,1,5,2,1,0,5,1,2,0,0,0,3,3,2,2,3,2,4,3,0,0,3,3,1,3,3,0,2,5,3,4,0,3,3,0,1,2,0,2,2,0,3,2,0,2,2,3,3,3,0,2,0,1,0,3,4,4,2,5,4,0,3,0,0,3,5),\n(0,3,0,3,0,3,0,1,0,3,3,3,3,0,3,0,2,0,2,1,1,0,2,0,1,0,0,0,2,1,0,0,1,0,3,2,0,0,3,3,1,2,3,1,0,3,3,0,0,1,0,0,0,0,0,2,0,0,0,0,0,2,3,1,2,3,0,3,0,1,0,3,2,1,0,4,3,0,1,1,0,3,3),\n(0,4,0,5,0,3,0,3,0,4,5,5,4,3,5,3,4,3,5,3,3,2,5,3,4,4,4,3,4,3,4,5,5,3,4,4,3,4,4,5,4,4,4,3,4,5,5,4,2,3,4,2,3,4,0,3,3,1,4,3,2,4,3,3,5,5,0,3,0,3,0,5,5,5,5,4,4,0,4,0,1,4,4),\n(0,4,0,4,0,3,0,3,0,3,5,4,4,2,3,2,5,1,3,2,5,1,4,2,3,2,3,3,4,3,3,3,3,2,5,4,1,3,3,5,3,4,4,0,4,4,3,1,1,3,1,0,2,3,0,2,3,0,3,0,0,4,3,1,3,4,0,3,0,2,0,4,4,4,3,4,5,0,4,0,0,3,4),\n(0,3,0,3,0,3,1,2,0,3,4,4,3,3,3,0,2,2,4,3,3,1,3,3,3,1,1,0,3,1,4,3,2,3,4,4,2,4,4,4,3,4,4,3,2,4,4,3,1,3,3,1,3,3,0,4,1,0,2,2,1,4,3,2,3,3,5,4,3,3,5,4,4,3,3,0,4,0,3,2,2,4,4),\n(0,2,0,1,0,0,0,0,0,1,2,1,3,0,0,0,0,0,2,0,1,2,1,0,0,1,0,0,0,0,3,0,0,1,0,1,1,3,1,0,0,0,1,1,0,1,1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,1,1,2,2,0,3,4,0,0,0,1,1,0,0,1,0,0,0,0,0,1,1),\n(0,1,0,0,0,1,0,0,0,0,4,0,4,1,4,0,3,0,4,0,3,0,4,0,3,0,3,0,4,1,5,1,4,0,0,3,0,5,0,5,2,0,1,0,0,0,2,1,4,0,1,3,0,0,3,0,0,3,1,1,4,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0),\n(1,4,0,5,0,3,0,2,0,3,5,4,4,3,4,3,5,3,4,3,3,0,4,3,3,3,3,3,3,2,4,4,3,1,3,4,4,5,4,4,3,4,4,1,3,5,4,3,3,3,1,2,2,3,3,1,3,1,3,3,3,5,3,3,4,5,0,3,0,3,0,3,4,3,4,4,3,0,3,0,2,4,3),\n(0,1,0,4,0,0,0,0,0,1,4,0,4,1,4,2,4,0,3,0,1,0,1,0,0,0,0,0,2,0,3,1,1,1,0,3,0,0,0,1,2,1,0,0,1,1,1,1,0,1,0,0,0,1,0,0,3,0,0,0,0,3,2,0,2,2,0,1,0,0,0,2,3,2,3,3,0,0,0,0,2,1,0),\n(0,5,1,5,0,3,0,3,0,5,4,4,5,1,5,3,3,0,4,3,4,3,5,3,4,3,3,2,4,3,4,3,3,0,3,3,1,4,4,3,4,4,4,3,4,5,5,3,2,3,1,1,3,3,1,3,1,1,3,3,2,4,5,3,3,5,0,4,0,3,0,4,4,3,5,3,3,0,3,4,0,4,3),\n(0,5,0,5,0,3,0,2,0,4,4,3,5,2,4,3,3,3,4,4,4,3,5,3,5,3,3,1,4,0,4,3,3,0,3,3,0,4,4,4,4,5,4,3,3,5,5,3,2,3,1,2,3,2,0,1,0,0,3,2,2,4,4,3,1,5,0,4,0,3,0,4,3,1,3,2,1,0,3,3,0,3,3),\n(0,4,0,5,0,5,0,4,0,4,5,5,5,3,4,3,3,2,5,4,4,3,5,3,5,3,4,0,4,3,4,4,3,2,4,4,3,4,5,4,4,5,5,0,3,5,5,4,1,3,3,2,3,3,1,3,1,0,4,3,1,4,4,3,4,5,0,4,0,2,0,4,3,4,4,3,3,0,4,0,0,5,5),\n(0,4,0,4,0,5,0,1,1,3,3,4,4,3,4,1,3,0,5,1,3,0,3,1,3,1,1,0,3,0,3,3,4,0,4,3,0,4,4,4,3,4,4,0,3,5,4,1,0,3,0,0,2,3,0,3,1,0,3,1,0,3,2,1,3,5,0,3,0,1,0,3,2,3,3,4,4,0,2,2,0,4,4),\n(2,4,0,5,0,4,0,3,0,4,5,5,4,3,5,3,5,3,5,3,5,2,5,3,4,3,3,4,3,4,5,3,2,1,5,4,3,2,3,4,5,3,4,1,2,5,4,3,0,3,3,0,3,2,0,2,3,0,4,1,0,3,4,3,3,5,0,3,0,1,0,4,5,5,5,4,3,0,4,2,0,3,5),\n(0,5,0,4,0,4,0,2,0,5,4,3,4,3,4,3,3,3,4,3,4,2,5,3,5,3,4,1,4,3,4,4,4,0,3,5,0,4,4,4,4,5,3,1,3,4,5,3,3,3,3,3,3,3,0,2,2,0,3,3,2,4,3,3,3,5,3,4,1,3,3,5,3,2,0,0,0,0,4,3,1,3,3),\n(0,1,0,3,0,3,0,1,0,1,3,3,3,2,3,3,3,0,3,0,0,0,3,1,3,0,0,0,2,2,2,3,0,0,3,2,0,1,2,4,1,3,3,0,0,3,3,3,0,1,0,0,2,1,0,0,3,0,3,1,0,3,0,0,1,3,0,2,0,1,0,3,3,1,3,3,0,0,1,1,0,3,3),\n(0,2,0,3,0,2,1,4,0,2,2,3,1,1,3,1,1,0,2,0,3,1,2,3,1,3,0,0,1,0,4,3,2,3,3,3,1,4,2,3,3,3,3,1,0,3,1,4,0,1,1,0,1,2,0,1,1,0,1,1,0,3,1,3,2,2,0,1,0,0,0,2,3,3,3,1,0,0,0,0,0,2,3),\n(0,5,0,4,0,5,0,2,0,4,5,5,3,3,4,3,3,1,5,4,4,2,4,4,4,3,4,2,4,3,5,5,4,3,3,4,3,3,5,5,4,5,5,1,3,4,5,3,1,4,3,1,3,3,0,3,3,1,4,3,1,4,5,3,3,5,0,4,0,3,0,5,3,3,1,4,3,0,4,0,1,5,3),\n(0,5,0,5,0,4,0,2,0,4,4,3,4,3,3,3,3,3,5,4,4,4,4,4,4,5,3,3,5,2,4,4,4,3,4,4,3,3,4,4,5,5,3,3,4,3,4,3,3,4,3,3,3,3,1,2,2,1,4,3,3,5,4,4,3,4,0,4,0,3,0,4,4,4,4,4,1,0,4,2,0,2,4),\n(0,4,0,4,0,3,0,1,0,3,5,2,3,0,3,0,2,1,4,2,3,3,4,1,4,3,3,2,4,1,3,3,3,0,3,3,0,0,3,3,3,5,3,3,3,3,3,2,0,2,0,0,2,0,0,2,0,0,1,0,0,3,1,2,2,3,0,3,0,2,0,4,4,3,3,4,1,0,3,0,0,2,4),\n(0,0,0,4,0,0,0,0,0,0,1,0,1,0,2,0,0,0,0,0,1,0,2,0,1,0,0,0,0,0,3,1,3,0,3,2,0,0,0,1,0,3,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,4,0,2,0,0,0,0,0,0,2),\n(0,2,1,3,0,2,0,2,0,3,3,3,3,1,3,1,3,3,3,3,3,3,4,2,2,1,2,1,4,0,4,3,1,3,3,3,2,4,3,5,4,3,3,3,3,3,3,3,0,1,3,0,2,0,0,1,0,0,1,0,0,4,2,0,2,3,0,3,3,0,3,3,4,2,3,1,4,0,1,2,0,2,3),\n(0,3,0,3,0,1,0,3,0,2,3,3,3,0,3,1,2,0,3,3,2,3,3,2,3,2,3,1,3,0,4,3,2,0,3,3,1,4,3,3,2,3,4,3,1,3,3,1,1,0,1,1,0,1,0,1,0,1,0,0,0,4,1,1,0,3,0,3,1,0,2,3,3,3,3,3,1,0,0,2,0,3,3),\n(0,0,0,0,0,0,0,0,0,0,3,0,2,0,3,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,3,0,3,0,3,1,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,2,0,2,3,0,0,0,0,0,0,0,0,3),\n(0,2,0,3,1,3,0,3,0,2,3,3,3,1,3,1,3,1,3,1,3,3,3,1,3,0,2,3,1,1,4,3,3,2,3,3,1,2,2,4,1,3,3,0,1,4,2,3,0,1,3,0,3,0,0,1,3,0,2,0,0,3,3,2,1,3,0,3,0,2,0,3,4,4,4,3,1,0,3,0,0,3,3),\n(0,2,0,1,0,2,0,0,0,1,3,2,2,1,3,0,1,1,3,0,3,2,3,1,2,0,2,0,1,1,3,3,3,0,3,3,1,1,2,3,2,3,3,1,2,3,2,0,0,1,0,0,0,0,0,0,3,0,1,0,0,2,1,2,1,3,0,3,0,0,0,3,4,4,4,3,2,0,2,0,0,2,4),\n(0,0,0,1,0,1,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,2,2,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,3,1,0,0,0,0,0,0,0,3),\n(0,3,0,3,0,2,0,3,0,3,3,3,2,3,2,2,2,0,3,1,3,3,3,2,3,3,0,0,3,0,3,2,2,0,2,3,1,4,3,4,3,3,2,3,1,5,4,4,0,3,1,2,1,3,0,3,1,1,2,0,2,3,1,3,1,3,0,3,0,1,0,3,3,4,4,2,1,0,2,1,0,2,4),\n(0,1,0,3,0,1,0,2,0,1,4,2,5,1,4,0,2,0,2,1,3,1,4,0,2,1,0,0,2,1,4,1,1,0,3,3,0,5,1,3,2,3,3,1,0,3,2,3,0,1,0,0,0,0,0,0,1,0,0,0,0,4,0,1,0,3,0,2,0,1,0,3,3,3,4,3,3,0,0,0,0,2,3),\n(0,0,0,1,0,0,0,0,0,0,2,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,0,1,0,0,0,0,0,3),\n(0,1,0,3,0,4,0,3,0,2,4,3,1,0,3,2,2,1,3,1,2,2,3,1,1,1,2,1,3,0,1,2,0,1,3,2,1,3,0,5,5,1,0,0,1,3,2,1,0,3,0,0,1,0,0,0,0,0,3,4,0,1,1,1,3,2,0,2,0,1,0,2,3,3,1,2,3,0,1,0,1,0,4),\n(0,0,0,1,0,3,0,3,0,2,2,1,0,0,4,0,3,0,3,1,3,0,3,0,3,0,1,0,3,0,3,1,3,0,3,3,0,0,1,2,1,1,1,0,1,2,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,2,2,1,2,0,0,2,0,0,0,0,2,3,3,3,3,0,0,0,0,1,4),\n(0,0,0,3,0,3,0,0,0,0,3,1,1,0,3,0,1,0,2,0,1,0,0,0,0,0,0,0,1,0,3,0,2,0,2,3,0,0,2,2,3,1,2,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,2,0,0,0,0,2,3),\n(2,4,0,5,0,5,0,4,0,3,4,3,3,3,4,3,3,3,4,3,4,4,5,4,5,5,5,2,3,0,5,5,4,1,5,4,3,1,5,4,3,4,4,3,3,4,3,3,0,3,2,0,2,3,0,3,0,0,3,3,0,5,3,2,3,3,0,3,0,3,0,3,4,5,4,5,3,0,4,3,0,3,4),\n(0,3,0,3,0,3,0,3,0,3,3,4,3,2,3,2,3,0,4,3,3,3,3,3,3,3,3,0,3,2,4,3,3,1,3,4,3,4,4,4,3,4,4,3,2,4,4,1,0,2,0,0,1,1,0,2,0,0,3,1,0,5,3,2,1,3,0,3,0,1,2,4,3,2,4,3,3,0,3,2,0,4,4),\n(0,3,0,3,0,1,0,0,0,1,4,3,3,2,3,1,3,1,4,2,3,2,4,2,3,4,3,0,2,2,3,3,3,0,3,3,3,0,3,4,1,3,3,0,3,4,3,3,0,1,1,0,1,0,0,0,4,0,3,0,0,3,1,2,1,3,0,4,0,1,0,4,3,3,4,3,3,0,2,0,0,3,3),\n(0,3,0,4,0,1,0,3,0,3,4,3,3,0,3,3,3,1,3,1,3,3,4,3,3,3,0,0,3,1,5,3,3,1,3,3,2,5,4,3,3,4,5,3,2,5,3,4,0,1,0,0,0,0,0,2,0,0,1,1,0,4,2,2,1,3,0,3,0,2,0,4,4,3,5,3,2,0,1,1,0,3,4),\n(0,5,0,4,0,5,0,2,0,4,4,3,3,2,3,3,3,1,4,3,4,1,5,3,4,3,4,0,4,2,4,3,4,1,5,4,0,4,4,4,4,5,4,1,3,5,4,2,1,4,1,1,3,2,0,3,1,0,3,2,1,4,3,3,3,4,0,4,0,3,0,4,4,4,3,3,3,0,4,2,0,3,4),\n(1,4,0,4,0,3,0,1,0,3,3,3,1,1,3,3,2,2,3,3,1,0,3,2,2,1,2,0,3,1,2,1,2,0,3,2,0,2,2,3,3,4,3,0,3,3,1,2,0,1,1,3,1,2,0,0,3,0,1,1,0,3,2,2,3,3,0,3,0,0,0,2,3,3,4,3,3,0,1,0,0,1,4),\n(0,4,0,4,0,4,0,0,0,3,4,4,3,1,4,2,3,2,3,3,3,1,4,3,4,0,3,0,4,2,3,3,2,2,5,4,2,1,3,4,3,4,3,1,3,3,4,2,0,2,1,0,3,3,0,0,2,0,3,1,0,4,4,3,4,3,0,4,0,1,0,2,4,4,4,4,4,0,3,2,0,3,3),\n(0,0,0,1,0,4,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,3,2,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,2),\n(0,2,0,3,0,4,0,4,0,1,3,3,3,0,4,0,2,1,2,1,1,1,2,0,3,1,1,0,1,0,3,1,0,0,3,3,2,0,1,1,0,0,0,0,0,1,0,2,0,2,2,0,3,1,0,0,1,0,1,1,0,1,2,0,3,0,0,0,0,1,0,0,3,3,4,3,1,0,1,0,3,0,2),\n(0,0,0,3,0,5,0,0,0,0,1,0,2,0,3,1,0,1,3,0,0,0,2,0,0,0,1,0,0,0,1,1,0,0,4,0,0,0,2,3,0,1,4,1,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,1,0,0,0,0,0,0,0,2,0,0,3,0,0,0,0,0,3),\n(0,2,0,5,0,5,0,1,0,2,4,3,3,2,5,1,3,2,3,3,3,0,4,1,2,0,3,0,4,0,2,2,1,1,5,3,0,0,1,4,2,3,2,0,3,3,3,2,0,2,4,1,1,2,0,1,1,0,3,1,0,1,3,1,2,3,0,2,0,0,0,1,3,5,4,4,4,0,3,0,0,1,3),\n(0,4,0,5,0,4,0,4,0,4,5,4,3,3,4,3,3,3,4,3,4,4,5,3,4,5,4,2,4,2,3,4,3,1,4,4,1,3,5,4,4,5,5,4,4,5,5,5,2,3,3,1,4,3,1,3,3,0,3,3,1,4,3,4,4,4,0,3,0,4,0,3,3,4,4,5,0,0,4,3,0,4,5),\n(0,4,0,4,0,3,0,3,0,3,4,4,4,3,3,2,4,3,4,3,4,3,5,3,4,3,2,1,4,2,4,4,3,1,3,4,2,4,5,5,3,4,5,4,1,5,4,3,0,3,2,2,3,2,1,3,1,0,3,3,3,5,3,3,3,5,4,4,2,3,3,4,3,3,3,2,1,0,3,2,1,4,3),\n(0,4,0,5,0,4,0,3,0,3,5,5,3,2,4,3,4,0,5,4,4,1,4,4,4,3,3,3,4,3,5,5,2,3,3,4,1,2,5,5,3,5,5,2,3,5,5,4,0,3,2,0,3,3,1,1,5,1,4,1,0,4,3,2,3,5,0,4,0,3,0,5,4,3,4,3,0,0,4,1,0,4,4),\n(1,3,0,4,0,2,0,2,0,2,5,5,3,3,3,3,3,0,4,2,3,4,4,4,3,4,0,0,3,4,5,4,3,3,3,3,2,5,5,4,5,5,5,4,3,5,5,5,1,3,1,0,1,0,0,3,2,0,4,2,0,5,2,3,2,4,1,3,0,3,0,4,5,4,5,4,3,0,4,2,0,5,4),\n(0,3,0,4,0,5,0,3,0,3,4,4,3,2,3,2,3,3,3,3,3,2,4,3,3,2,2,0,3,3,3,3,3,1,3,3,3,0,4,4,3,4,4,1,1,4,4,2,0,3,1,0,1,1,0,4,1,0,2,3,1,3,3,1,3,4,0,3,0,1,0,3,1,3,0,0,1,0,2,0,0,4,4),\n(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),\n(0,3,0,3,0,2,0,3,0,1,5,4,3,3,3,1,4,2,1,2,3,4,4,2,4,4,5,0,3,1,4,3,4,0,4,3,3,3,2,3,2,5,3,4,3,2,2,3,0,0,3,0,2,1,0,1,2,0,0,0,0,2,1,1,3,1,0,2,0,4,0,3,4,4,4,5,2,0,2,0,0,1,3),\n(0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,1,0,0,1,1,0,0,0,4,2,1,1,0,1,0,3,2,0,0,3,1,1,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,1,0,0,0,2,0,0,0,1,4,0,4,2,1,0,0,0,0,0,1),\n(0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,1,0,0,0,0,0,0,1,0,1,0,0,0,0,3,1,0,0,0,2,0,2,1,0,0,1,2,1,0,1,1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,1,3,1,0,0,0,0,0,1,0,0,2,1,0,0,0,0,0,0,0,0,2),\n(0,4,0,4,0,4,0,3,0,4,4,3,4,2,4,3,2,0,4,4,4,3,5,3,5,3,3,2,4,2,4,3,4,3,1,4,0,2,3,4,4,4,3,3,3,4,4,4,3,4,1,3,4,3,2,1,2,1,3,3,3,4,4,3,3,5,0,4,0,3,0,4,3,3,3,2,1,0,3,0,0,3,3),\n(0,4,0,3,0,3,0,3,0,3,5,5,3,3,3,3,4,3,4,3,3,3,4,4,4,3,3,3,3,4,3,5,3,3,1,3,2,4,5,5,5,5,4,3,4,5,5,3,2,2,3,3,3,3,2,3,3,1,2,3,2,4,3,3,3,4,0,4,0,2,0,4,3,2,2,1,2,0,3,0,0,4,1),\n)\n\nclass JapaneseContextAnalysis(object):\n    NUM_OF_CATEGORY = 6\n    DONT_KNOW = -1\n    ENOUGH_REL_THRESHOLD = 100\n    MAX_REL_THRESHOLD = 1000\n    MINIMUM_DATA_THRESHOLD = 4\n\n    def __init__(self):\n        self._total_rel = None\n        self._rel_sample = None\n        self._need_to_skip_char_num = None\n        self._last_char_order = None\n        self._done = None\n        self.reset()\n\n    def reset(self):\n        self._total_rel = 0  # total sequence received\n        # category counters, each integer counts sequence in its category\n        self._rel_sample = [0] * self.NUM_OF_CATEGORY\n        # if last byte in current buffer is not the last byte of a character,\n        # we need to know how many bytes to skip in next buffer\n        self._need_to_skip_char_num = 0\n        self._last_char_order = -1  # The order of previous char\n        # If this flag is set to True, detection is done and conclusion has\n        # been made\n        self._done = False\n\n    def feed(self, byte_str, num_bytes):\n        if self._done:\n            return\n\n        # The buffer we got is byte oriented, and a character may span in more than one\n        # buffers. In case the last one or two byte in last buffer is not\n        # complete, we record how many byte needed to complete that character\n        # and skip these bytes here.  We can choose to record those bytes as\n        # well and analyse the character once it is complete, but since a\n        # character will not make much difference, by simply skipping\n        # this character will simply our logic and improve performance.\n        i = self._need_to_skip_char_num\n        while i < num_bytes:\n            order, char_len = self.get_order(byte_str[i:i + 2])\n            i += char_len\n            if i > num_bytes:\n                self._need_to_skip_char_num = i - num_bytes\n                self._last_char_order = -1\n            else:\n                if (order != -1) and (self._last_char_order != -1):\n                    self._total_rel += 1\n                    if self._total_rel > self.MAX_REL_THRESHOLD:\n                        self._done = True\n                        break\n                    self._rel_sample[jp2CharContext[self._last_char_order][order]] += 1\n                self._last_char_order = order\n\n    def got_enough_data(self):\n        return self._total_rel > self.ENOUGH_REL_THRESHOLD\n\n    def get_confidence(self):\n        # This is just one way to calculate confidence. It works well for me.\n        if self._total_rel > self.MINIMUM_DATA_THRESHOLD:\n            return (self._total_rel - self._rel_sample[0]) / self._total_rel\n        else:\n            return self.DONT_KNOW\n\n    def get_order(self, byte_str):\n        return -1, 1\n\nclass SJISContextAnalysis(JapaneseContextAnalysis):\n    def __init__(self):\n        super(SJISContextAnalysis, self).__init__()\n        self._charset_name = \"SHIFT_JIS\"\n\n    @property\n    def charset_name(self):\n        return self._charset_name\n\n    def get_order(self, byte_str):\n        if not byte_str:\n            return -1, 1\n        # find out current char's byte length\n        first_char = byte_str[0]\n        if (0x81 <= first_char <= 0x9F) or (0xE0 <= first_char <= 0xFC):\n            char_len = 2\n            if (first_char == 0x87) or (0xFA <= first_char <= 0xFC):\n                self._charset_name = \"CP932\"\n        else:\n            char_len = 1\n\n        # return its order if it is hiragana\n        if len(byte_str) > 1:\n            second_char = byte_str[1]\n            if (first_char == 202) and (0x9F <= second_char <= 0xF1):\n                return second_char - 0x9F, char_len\n\n        return -1, char_len\n\nclass EUCJPContextAnalysis(JapaneseContextAnalysis):\n    def get_order(self, byte_str):\n        if not byte_str:\n            return -1, 1\n        # find out current char's byte length\n        first_char = byte_str[0]\n        if (first_char == 0x8E) or (0xA1 <= first_char <= 0xFE):\n            char_len = 2\n        elif first_char == 0x8F:\n            char_len = 3\n        else:\n            char_len = 1\n\n        # return its order if it is hiragana\n        if len(byte_str) > 1:\n            second_char = byte_str[1]\n            if (first_char == 0xA4) and (0xA1 <= second_char <= 0xF3):\n                return second_char - 0xA1, char_len\n\n        return -1, char_len\n\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/langbulgarianmodel.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nfrom chardet.sbcharsetprober import SingleByteCharSetModel\n\n\n# 3: Positive\n# 2: Likely\n# 1: Unlikely\n# 0: Negative\n\nBULGARIAN_LANG_MODEL = {\n    63: {  # 'e'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 0,  # 'а'\n        18: 1,  # 'б'\n        9: 1,  # 'в'\n        20: 1,  # 'г'\n        11: 1,  # 'д'\n        3: 1,  # 'е'\n        23: 1,  # 'ж'\n        15: 1,  # 'з'\n        2: 0,  # 'и'\n        26: 1,  # 'й'\n        12: 1,  # 'к'\n        10: 1,  # 'л'\n        14: 1,  # 'м'\n        6: 1,  # 'н'\n        4: 1,  # 'о'\n        13: 1,  # 'п'\n        7: 1,  # 'р'\n        8: 1,  # 'с'\n        5: 1,  # 'т'\n        19: 0,  # 'у'\n        29: 1,  # 'ф'\n        25: 1,  # 'х'\n        22: 0,  # 'ц'\n        21: 1,  # 'ч'\n        27: 1,  # 'ш'\n        24: 1,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    45: {  # '\\xad'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 1,  # 'Б'\n        35: 1,  # 'В'\n        43: 0,  # 'Г'\n        37: 1,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 1,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 0,  # 'Л'\n        38: 1,  # 'М'\n        36: 0,  # 'Н'\n        41: 1,  # 'О'\n        30: 1,  # 'П'\n        39: 1,  # 'Р'\n        28: 1,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 0,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 0,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 0,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 0,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 0,  # 'о'\n        13: 0,  # 'п'\n        7: 0,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 0,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    31: {  # 'А'\n        63: 0,  # 'e'\n        45: 1,  # '\\xad'\n        31: 1,  # 'А'\n        32: 1,  # 'Б'\n        35: 2,  # 'В'\n        43: 1,  # 'Г'\n        37: 2,  # 'Д'\n        44: 2,  # 'Е'\n        55: 1,  # 'Ж'\n        47: 2,  # 'З'\n        40: 1,  # 'И'\n        59: 1,  # 'Й'\n        33: 1,  # 'К'\n        46: 2,  # 'Л'\n        38: 1,  # 'М'\n        36: 2,  # 'Н'\n        41: 1,  # 'О'\n        30: 2,  # 'П'\n        39: 2,  # 'Р'\n        28: 2,  # 'С'\n        34: 2,  # 'Т'\n        51: 1,  # 'У'\n        48: 2,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 1,  # 'Ш'\n        57: 2,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 1,  # 'а'\n        18: 2,  # 'б'\n        9: 2,  # 'в'\n        20: 2,  # 'г'\n        11: 2,  # 'д'\n        3: 1,  # 'е'\n        23: 1,  # 'ж'\n        15: 2,  # 'з'\n        2: 0,  # 'и'\n        26: 2,  # 'й'\n        12: 2,  # 'к'\n        10: 3,  # 'л'\n        14: 2,  # 'м'\n        6: 3,  # 'н'\n        4: 0,  # 'о'\n        13: 2,  # 'п'\n        7: 2,  # 'р'\n        8: 2,  # 'с'\n        5: 2,  # 'т'\n        19: 1,  # 'у'\n        29: 2,  # 'ф'\n        25: 1,  # 'х'\n        22: 1,  # 'ц'\n        21: 1,  # 'ч'\n        27: 1,  # 'ш'\n        24: 0,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    32: {  # 'Б'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 2,  # 'А'\n        32: 2,  # 'Б'\n        35: 1,  # 'В'\n        43: 1,  # 'Г'\n        37: 2,  # 'Д'\n        44: 1,  # 'Е'\n        55: 1,  # 'Ж'\n        47: 2,  # 'З'\n        40: 1,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 2,  # 'Н'\n        41: 2,  # 'О'\n        30: 1,  # 'П'\n        39: 1,  # 'Р'\n        28: 2,  # 'С'\n        34: 2,  # 'Т'\n        51: 1,  # 'У'\n        48: 2,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 1,  # 'Щ'\n        61: 2,  # 'Ъ'\n        60: 1,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 3,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 1,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 2,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 3,  # 'о'\n        13: 0,  # 'п'\n        7: 2,  # 'р'\n        8: 1,  # 'с'\n        5: 0,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 1,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 1,  # 'ю'\n        16: 2,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    35: {  # 'В'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 2,  # 'А'\n        32: 1,  # 'Б'\n        35: 1,  # 'В'\n        43: 0,  # 'Г'\n        37: 1,  # 'Д'\n        44: 2,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 2,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 1,  # 'О'\n        30: 1,  # 'П'\n        39: 2,  # 'Р'\n        28: 2,  # 'С'\n        34: 1,  # 'Т'\n        51: 1,  # 'У'\n        48: 2,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 1,  # 'Ъ'\n        60: 1,  # 'Ю'\n        56: 2,  # 'Я'\n        1: 3,  # 'а'\n        18: 1,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 1,  # 'д'\n        3: 3,  # 'е'\n        23: 1,  # 'ж'\n        15: 2,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 1,  # 'к'\n        10: 2,  # 'л'\n        14: 1,  # 'м'\n        6: 2,  # 'н'\n        4: 2,  # 'о'\n        13: 1,  # 'п'\n        7: 2,  # 'р'\n        8: 2,  # 'с'\n        5: 2,  # 'т'\n        19: 1,  # 'у'\n        29: 0,  # 'ф'\n        25: 1,  # 'х'\n        22: 0,  # 'ц'\n        21: 2,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 1,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    43: {  # 'Г'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 2,  # 'А'\n        32: 1,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 1,  # 'Д'\n        44: 2,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 1,  # 'З'\n        40: 1,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 0,  # 'М'\n        36: 1,  # 'Н'\n        41: 1,  # 'О'\n        30: 0,  # 'П'\n        39: 1,  # 'Р'\n        28: 1,  # 'С'\n        34: 0,  # 'Т'\n        51: 1,  # 'У'\n        48: 1,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 1,  # 'Щ'\n        61: 1,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 2,  # 'а'\n        18: 1,  # 'б'\n        9: 1,  # 'в'\n        20: 0,  # 'г'\n        11: 1,  # 'д'\n        3: 3,  # 'е'\n        23: 1,  # 'ж'\n        15: 0,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 1,  # 'к'\n        10: 2,  # 'л'\n        14: 1,  # 'м'\n        6: 1,  # 'н'\n        4: 2,  # 'о'\n        13: 0,  # 'п'\n        7: 2,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 1,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 1,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    37: {  # 'Д'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 2,  # 'А'\n        32: 1,  # 'Б'\n        35: 2,  # 'В'\n        43: 1,  # 'Г'\n        37: 2,  # 'Д'\n        44: 2,  # 'Е'\n        55: 2,  # 'Ж'\n        47: 1,  # 'З'\n        40: 2,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 2,  # 'О'\n        30: 2,  # 'П'\n        39: 1,  # 'Р'\n        28: 2,  # 'С'\n        34: 1,  # 'Т'\n        51: 1,  # 'У'\n        48: 1,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 1,  # 'Ъ'\n        60: 1,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 3,  # 'а'\n        18: 0,  # 'б'\n        9: 2,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 3,  # 'е'\n        23: 3,  # 'ж'\n        15: 1,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 1,  # 'л'\n        14: 1,  # 'м'\n        6: 2,  # 'н'\n        4: 3,  # 'о'\n        13: 0,  # 'п'\n        7: 2,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 2,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    44: {  # 'Е'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 1,  # 'А'\n        32: 1,  # 'Б'\n        35: 2,  # 'В'\n        43: 1,  # 'Г'\n        37: 1,  # 'Д'\n        44: 1,  # 'Е'\n        55: 1,  # 'Ж'\n        47: 1,  # 'З'\n        40: 1,  # 'И'\n        59: 1,  # 'Й'\n        33: 2,  # 'К'\n        46: 2,  # 'Л'\n        38: 1,  # 'М'\n        36: 2,  # 'Н'\n        41: 2,  # 'О'\n        30: 1,  # 'П'\n        39: 2,  # 'Р'\n        28: 2,  # 'С'\n        34: 2,  # 'Т'\n        51: 1,  # 'У'\n        48: 2,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 2,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 1,  # 'Ш'\n        57: 1,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 0,  # 'а'\n        18: 1,  # 'б'\n        9: 2,  # 'в'\n        20: 1,  # 'г'\n        11: 2,  # 'д'\n        3: 0,  # 'е'\n        23: 1,  # 'ж'\n        15: 1,  # 'з'\n        2: 0,  # 'и'\n        26: 1,  # 'й'\n        12: 2,  # 'к'\n        10: 2,  # 'л'\n        14: 2,  # 'м'\n        6: 2,  # 'н'\n        4: 0,  # 'о'\n        13: 1,  # 'п'\n        7: 2,  # 'р'\n        8: 2,  # 'с'\n        5: 1,  # 'т'\n        19: 1,  # 'у'\n        29: 1,  # 'ф'\n        25: 1,  # 'х'\n        22: 0,  # 'ц'\n        21: 1,  # 'ч'\n        27: 1,  # 'ш'\n        24: 1,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    55: {  # 'Ж'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 1,  # 'А'\n        32: 0,  # 'Б'\n        35: 1,  # 'В'\n        43: 0,  # 'Г'\n        37: 1,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 1,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 1,  # 'Н'\n        41: 1,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 1,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 2,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 1,  # 'д'\n        3: 2,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 0,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 2,  # 'о'\n        13: 1,  # 'п'\n        7: 1,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 1,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 1,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    47: {  # 'З'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 2,  # 'А'\n        32: 1,  # 'Б'\n        35: 1,  # 'В'\n        43: 1,  # 'Г'\n        37: 1,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 1,  # 'З'\n        40: 1,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 2,  # 'Н'\n        41: 1,  # 'О'\n        30: 1,  # 'П'\n        39: 1,  # 'Р'\n        28: 1,  # 'С'\n        34: 1,  # 'Т'\n        51: 1,  # 'У'\n        48: 0,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 1,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 3,  # 'а'\n        18: 1,  # 'б'\n        9: 2,  # 'в'\n        20: 1,  # 'г'\n        11: 2,  # 'д'\n        3: 2,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 1,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 2,  # 'л'\n        14: 1,  # 'м'\n        6: 1,  # 'н'\n        4: 1,  # 'о'\n        13: 0,  # 'п'\n        7: 1,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 1,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    40: {  # 'И'\n        63: 0,  # 'e'\n        45: 1,  # '\\xad'\n        31: 1,  # 'А'\n        32: 1,  # 'Б'\n        35: 1,  # 'В'\n        43: 1,  # 'Г'\n        37: 1,  # 'Д'\n        44: 2,  # 'Е'\n        55: 1,  # 'Ж'\n        47: 2,  # 'З'\n        40: 1,  # 'И'\n        59: 1,  # 'Й'\n        33: 2,  # 'К'\n        46: 2,  # 'Л'\n        38: 2,  # 'М'\n        36: 2,  # 'Н'\n        41: 1,  # 'О'\n        30: 1,  # 'П'\n        39: 2,  # 'Р'\n        28: 2,  # 'С'\n        34: 2,  # 'Т'\n        51: 0,  # 'У'\n        48: 1,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 1,  # 'Ш'\n        57: 1,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 2,  # 'Я'\n        1: 1,  # 'а'\n        18: 1,  # 'б'\n        9: 3,  # 'в'\n        20: 2,  # 'г'\n        11: 1,  # 'д'\n        3: 1,  # 'е'\n        23: 0,  # 'ж'\n        15: 3,  # 'з'\n        2: 0,  # 'и'\n        26: 1,  # 'й'\n        12: 1,  # 'к'\n        10: 2,  # 'л'\n        14: 2,  # 'м'\n        6: 2,  # 'н'\n        4: 0,  # 'о'\n        13: 1,  # 'п'\n        7: 2,  # 'р'\n        8: 2,  # 'с'\n        5: 2,  # 'т'\n        19: 0,  # 'у'\n        29: 1,  # 'ф'\n        25: 1,  # 'х'\n        22: 1,  # 'ц'\n        21: 1,  # 'ч'\n        27: 1,  # 'ш'\n        24: 1,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    59: {  # 'Й'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 1,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 1,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 1,  # 'С'\n        34: 1,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 0,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 1,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 0,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 0,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 2,  # 'о'\n        13: 0,  # 'п'\n        7: 0,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 0,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    33: {  # 'К'\n        63: 0,  # 'e'\n        45: 1,  # '\\xad'\n        31: 2,  # 'А'\n        32: 1,  # 'Б'\n        35: 1,  # 'В'\n        43: 1,  # 'Г'\n        37: 1,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 1,  # 'З'\n        40: 2,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 0,  # 'М'\n        36: 2,  # 'Н'\n        41: 2,  # 'О'\n        30: 2,  # 'П'\n        39: 1,  # 'Р'\n        28: 2,  # 'С'\n        34: 1,  # 'Т'\n        51: 1,  # 'У'\n        48: 1,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 1,  # 'Ъ'\n        60: 1,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 0,  # 'б'\n        9: 1,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 2,  # 'е'\n        23: 1,  # 'ж'\n        15: 0,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 2,  # 'л'\n        14: 1,  # 'м'\n        6: 2,  # 'н'\n        4: 3,  # 'о'\n        13: 0,  # 'п'\n        7: 3,  # 'р'\n        8: 1,  # 'с'\n        5: 0,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 1,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 1,  # 'ш'\n        24: 0,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 2,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    46: {  # 'Л'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 2,  # 'А'\n        32: 1,  # 'Б'\n        35: 1,  # 'В'\n        43: 2,  # 'Г'\n        37: 1,  # 'Д'\n        44: 2,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 1,  # 'З'\n        40: 2,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 0,  # 'М'\n        36: 1,  # 'Н'\n        41: 2,  # 'О'\n        30: 1,  # 'П'\n        39: 0,  # 'Р'\n        28: 1,  # 'С'\n        34: 1,  # 'Т'\n        51: 1,  # 'У'\n        48: 0,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 1,  # 'Ъ'\n        60: 1,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 2,  # 'а'\n        18: 0,  # 'б'\n        9: 1,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 0,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 2,  # 'о'\n        13: 0,  # 'п'\n        7: 0,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 2,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    38: {  # 'М'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 2,  # 'А'\n        32: 1,  # 'Б'\n        35: 2,  # 'В'\n        43: 0,  # 'Г'\n        37: 1,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 1,  # 'З'\n        40: 2,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 2,  # 'О'\n        30: 1,  # 'П'\n        39: 1,  # 'Р'\n        28: 2,  # 'С'\n        34: 1,  # 'Т'\n        51: 1,  # 'У'\n        48: 1,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 1,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 3,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 2,  # 'л'\n        14: 0,  # 'м'\n        6: 2,  # 'н'\n        4: 3,  # 'о'\n        13: 0,  # 'п'\n        7: 1,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 2,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    36: {  # 'Н'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 2,  # 'А'\n        32: 2,  # 'Б'\n        35: 1,  # 'В'\n        43: 1,  # 'Г'\n        37: 2,  # 'Д'\n        44: 2,  # 'Е'\n        55: 1,  # 'Ж'\n        47: 1,  # 'З'\n        40: 2,  # 'И'\n        59: 1,  # 'Й'\n        33: 2,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 2,  # 'О'\n        30: 1,  # 'П'\n        39: 1,  # 'Р'\n        28: 2,  # 'С'\n        34: 2,  # 'Т'\n        51: 1,  # 'У'\n        48: 1,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 1,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 1,  # 'Ъ'\n        60: 1,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 3,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 1,  # 'г'\n        11: 0,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 0,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 3,  # 'о'\n        13: 0,  # 'п'\n        7: 0,  # 'р'\n        8: 0,  # 'с'\n        5: 1,  # 'т'\n        19: 1,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 1,  # 'ш'\n        24: 0,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 2,  # 'ю'\n        16: 2,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    41: {  # 'О'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 1,  # 'А'\n        32: 1,  # 'Б'\n        35: 2,  # 'В'\n        43: 1,  # 'Г'\n        37: 2,  # 'Д'\n        44: 1,  # 'Е'\n        55: 1,  # 'Ж'\n        47: 1,  # 'З'\n        40: 1,  # 'И'\n        59: 1,  # 'Й'\n        33: 2,  # 'К'\n        46: 2,  # 'Л'\n        38: 2,  # 'М'\n        36: 2,  # 'Н'\n        41: 2,  # 'О'\n        30: 1,  # 'П'\n        39: 2,  # 'Р'\n        28: 2,  # 'С'\n        34: 2,  # 'Т'\n        51: 1,  # 'У'\n        48: 1,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 1,  # 'Ш'\n        57: 1,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 1,  # 'а'\n        18: 2,  # 'б'\n        9: 2,  # 'в'\n        20: 2,  # 'г'\n        11: 1,  # 'д'\n        3: 1,  # 'е'\n        23: 1,  # 'ж'\n        15: 1,  # 'з'\n        2: 0,  # 'и'\n        26: 1,  # 'й'\n        12: 2,  # 'к'\n        10: 2,  # 'л'\n        14: 1,  # 'м'\n        6: 1,  # 'н'\n        4: 0,  # 'о'\n        13: 2,  # 'п'\n        7: 2,  # 'р'\n        8: 2,  # 'с'\n        5: 3,  # 'т'\n        19: 1,  # 'у'\n        29: 1,  # 'ф'\n        25: 1,  # 'х'\n        22: 1,  # 'ц'\n        21: 2,  # 'ч'\n        27: 0,  # 'ш'\n        24: 2,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    30: {  # 'П'\n        63: 0,  # 'e'\n        45: 1,  # '\\xad'\n        31: 2,  # 'А'\n        32: 1,  # 'Б'\n        35: 1,  # 'В'\n        43: 1,  # 'Г'\n        37: 1,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 1,  # 'З'\n        40: 2,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 2,  # 'О'\n        30: 2,  # 'П'\n        39: 2,  # 'Р'\n        28: 2,  # 'С'\n        34: 1,  # 'Т'\n        51: 2,  # 'У'\n        48: 1,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 1,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 1,  # 'Ъ'\n        60: 1,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 2,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 1,  # 'к'\n        10: 3,  # 'л'\n        14: 0,  # 'м'\n        6: 1,  # 'н'\n        4: 3,  # 'о'\n        13: 0,  # 'п'\n        7: 3,  # 'р'\n        8: 1,  # 'с'\n        5: 1,  # 'т'\n        19: 2,  # 'у'\n        29: 1,  # 'ф'\n        25: 1,  # 'х'\n        22: 0,  # 'ц'\n        21: 1,  # 'ч'\n        27: 1,  # 'ш'\n        24: 0,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 1,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    39: {  # 'Р'\n        63: 0,  # 'e'\n        45: 1,  # '\\xad'\n        31: 2,  # 'А'\n        32: 1,  # 'Б'\n        35: 1,  # 'В'\n        43: 2,  # 'Г'\n        37: 2,  # 'Д'\n        44: 2,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 1,  # 'З'\n        40: 2,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 0,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 2,  # 'О'\n        30: 2,  # 'П'\n        39: 1,  # 'Р'\n        28: 1,  # 'С'\n        34: 1,  # 'Т'\n        51: 1,  # 'У'\n        48: 1,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 1,  # 'Ъ'\n        60: 1,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 3,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 2,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 0,  # 'л'\n        14: 0,  # 'м'\n        6: 1,  # 'н'\n        4: 3,  # 'о'\n        13: 0,  # 'п'\n        7: 0,  # 'р'\n        8: 1,  # 'с'\n        5: 0,  # 'т'\n        19: 3,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    28: {  # 'С'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 3,  # 'А'\n        32: 2,  # 'Б'\n        35: 2,  # 'В'\n        43: 1,  # 'Г'\n        37: 2,  # 'Д'\n        44: 2,  # 'Е'\n        55: 1,  # 'Ж'\n        47: 1,  # 'З'\n        40: 2,  # 'И'\n        59: 0,  # 'Й'\n        33: 2,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 2,  # 'О'\n        30: 2,  # 'П'\n        39: 1,  # 'Р'\n        28: 2,  # 'С'\n        34: 2,  # 'Т'\n        51: 1,  # 'У'\n        48: 1,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 1,  # 'Ъ'\n        60: 1,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 3,  # 'а'\n        18: 1,  # 'б'\n        9: 2,  # 'в'\n        20: 1,  # 'г'\n        11: 1,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 2,  # 'к'\n        10: 3,  # 'л'\n        14: 2,  # 'м'\n        6: 1,  # 'н'\n        4: 3,  # 'о'\n        13: 3,  # 'п'\n        7: 2,  # 'р'\n        8: 0,  # 'с'\n        5: 3,  # 'т'\n        19: 2,  # 'у'\n        29: 2,  # 'ф'\n        25: 1,  # 'х'\n        22: 1,  # 'ц'\n        21: 1,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 1,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    34: {  # 'Т'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 2,  # 'А'\n        32: 2,  # 'Б'\n        35: 1,  # 'В'\n        43: 0,  # 'Г'\n        37: 1,  # 'Д'\n        44: 2,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 2,  # 'И'\n        59: 0,  # 'Й'\n        33: 2,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 2,  # 'О'\n        30: 1,  # 'П'\n        39: 2,  # 'Р'\n        28: 2,  # 'С'\n        34: 1,  # 'Т'\n        51: 1,  # 'У'\n        48: 1,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 1,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 1,  # 'Я'\n        1: 3,  # 'а'\n        18: 1,  # 'б'\n        9: 1,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 1,  # 'к'\n        10: 1,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 3,  # 'о'\n        13: 0,  # 'п'\n        7: 3,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 2,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    51: {  # 'У'\n        63: 0,  # 'e'\n        45: 1,  # '\\xad'\n        31: 1,  # 'А'\n        32: 1,  # 'Б'\n        35: 1,  # 'В'\n        43: 1,  # 'Г'\n        37: 1,  # 'Д'\n        44: 2,  # 'Е'\n        55: 1,  # 'Ж'\n        47: 1,  # 'З'\n        40: 1,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 0,  # 'О'\n        30: 1,  # 'П'\n        39: 1,  # 'Р'\n        28: 1,  # 'С'\n        34: 2,  # 'Т'\n        51: 0,  # 'У'\n        48: 1,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 1,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 1,  # 'а'\n        18: 1,  # 'б'\n        9: 2,  # 'в'\n        20: 1,  # 'г'\n        11: 1,  # 'д'\n        3: 2,  # 'е'\n        23: 1,  # 'ж'\n        15: 1,  # 'з'\n        2: 2,  # 'и'\n        26: 1,  # 'й'\n        12: 2,  # 'к'\n        10: 1,  # 'л'\n        14: 1,  # 'м'\n        6: 2,  # 'н'\n        4: 2,  # 'о'\n        13: 1,  # 'п'\n        7: 1,  # 'р'\n        8: 2,  # 'с'\n        5: 1,  # 'т'\n        19: 1,  # 'у'\n        29: 0,  # 'ф'\n        25: 1,  # 'х'\n        22: 0,  # 'ц'\n        21: 2,  # 'ч'\n        27: 1,  # 'ш'\n        24: 0,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    48: {  # 'Ф'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 2,  # 'А'\n        32: 1,  # 'Б'\n        35: 1,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 2,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 0,  # 'М'\n        36: 1,  # 'Н'\n        41: 1,  # 'О'\n        30: 2,  # 'П'\n        39: 1,  # 'Р'\n        28: 2,  # 'С'\n        34: 1,  # 'Т'\n        51: 1,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 2,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 2,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 2,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 2,  # 'о'\n        13: 0,  # 'п'\n        7: 2,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 1,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 1,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    49: {  # 'Х'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 1,  # 'А'\n        32: 0,  # 'Б'\n        35: 1,  # 'В'\n        43: 1,  # 'Г'\n        37: 1,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 1,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 1,  # 'О'\n        30: 1,  # 'П'\n        39: 1,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 2,  # 'а'\n        18: 0,  # 'б'\n        9: 1,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 2,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 1,  # 'л'\n        14: 1,  # 'м'\n        6: 0,  # 'н'\n        4: 2,  # 'о'\n        13: 0,  # 'п'\n        7: 2,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 1,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    53: {  # 'Ц'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 1,  # 'А'\n        32: 0,  # 'Б'\n        35: 1,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 2,  # 'И'\n        59: 0,  # 'Й'\n        33: 2,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 1,  # 'Р'\n        28: 2,  # 'С'\n        34: 0,  # 'Т'\n        51: 1,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 2,  # 'а'\n        18: 0,  # 'б'\n        9: 2,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 2,  # 'е'\n        23: 0,  # 'ж'\n        15: 1,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 0,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 1,  # 'о'\n        13: 0,  # 'п'\n        7: 1,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 1,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    50: {  # 'Ч'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 2,  # 'А'\n        32: 1,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 1,  # 'З'\n        40: 1,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 0,  # 'М'\n        36: 1,  # 'Н'\n        41: 1,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 1,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 2,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 3,  # 'е'\n        23: 1,  # 'ж'\n        15: 0,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 1,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 2,  # 'о'\n        13: 0,  # 'п'\n        7: 1,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 0,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    54: {  # 'Ш'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 1,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 1,  # 'З'\n        40: 1,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 1,  # 'Н'\n        41: 1,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 1,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 2,  # 'а'\n        18: 0,  # 'б'\n        9: 2,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 2,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 2,  # 'и'\n        26: 0,  # 'й'\n        12: 1,  # 'к'\n        10: 1,  # 'л'\n        14: 1,  # 'м'\n        6: 1,  # 'н'\n        4: 2,  # 'о'\n        13: 1,  # 'п'\n        7: 1,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 1,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 0,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    57: {  # 'Щ'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 1,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 1,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 1,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 2,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 2,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 1,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 0,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 1,  # 'о'\n        13: 0,  # 'п'\n        7: 1,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 1,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    61: {  # 'Ъ'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 1,  # 'Б'\n        35: 1,  # 'В'\n        43: 0,  # 'Г'\n        37: 1,  # 'Д'\n        44: 0,  # 'Е'\n        55: 1,  # 'Ж'\n        47: 1,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 2,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 0,  # 'О'\n        30: 1,  # 'П'\n        39: 2,  # 'Р'\n        28: 1,  # 'С'\n        34: 1,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 1,  # 'Х'\n        53: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        54: 1,  # 'Ш'\n        57: 1,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 0,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 0,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 0,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 1,  # 'л'\n        14: 0,  # 'м'\n        6: 1,  # 'н'\n        4: 0,  # 'о'\n        13: 0,  # 'п'\n        7: 1,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 0,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    60: {  # 'Ю'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 1,  # 'А'\n        32: 1,  # 'Б'\n        35: 0,  # 'В'\n        43: 1,  # 'Г'\n        37: 1,  # 'Д'\n        44: 0,  # 'Е'\n        55: 1,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 0,  # 'М'\n        36: 1,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 1,  # 'Р'\n        28: 1,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 0,  # 'а'\n        18: 1,  # 'б'\n        9: 1,  # 'в'\n        20: 2,  # 'г'\n        11: 1,  # 'д'\n        3: 0,  # 'е'\n        23: 2,  # 'ж'\n        15: 1,  # 'з'\n        2: 1,  # 'и'\n        26: 0,  # 'й'\n        12: 1,  # 'к'\n        10: 1,  # 'л'\n        14: 1,  # 'м'\n        6: 1,  # 'н'\n        4: 0,  # 'о'\n        13: 1,  # 'п'\n        7: 1,  # 'р'\n        8: 1,  # 'с'\n        5: 1,  # 'т'\n        19: 0,  # 'у'\n        29: 0,  # 'ф'\n        25: 1,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    56: {  # 'Я'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 1,  # 'Б'\n        35: 1,  # 'В'\n        43: 1,  # 'Г'\n        37: 1,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 1,  # 'Л'\n        38: 1,  # 'М'\n        36: 1,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 1,  # 'С'\n        34: 2,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 0,  # 'а'\n        18: 1,  # 'б'\n        9: 1,  # 'в'\n        20: 1,  # 'г'\n        11: 1,  # 'д'\n        3: 0,  # 'е'\n        23: 0,  # 'ж'\n        15: 1,  # 'з'\n        2: 1,  # 'и'\n        26: 1,  # 'й'\n        12: 1,  # 'к'\n        10: 1,  # 'л'\n        14: 2,  # 'м'\n        6: 2,  # 'н'\n        4: 0,  # 'о'\n        13: 2,  # 'п'\n        7: 1,  # 'р'\n        8: 1,  # 'с'\n        5: 1,  # 'т'\n        19: 0,  # 'у'\n        29: 0,  # 'ф'\n        25: 1,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 1,  # 'ш'\n        24: 0,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    1: {  # 'а'\n        63: 1,  # 'e'\n        45: 1,  # '\\xad'\n        31: 1,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 1,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 1,  # 'а'\n        18: 3,  # 'б'\n        9: 3,  # 'в'\n        20: 3,  # 'г'\n        11: 3,  # 'д'\n        3: 3,  # 'е'\n        23: 3,  # 'ж'\n        15: 3,  # 'з'\n        2: 3,  # 'и'\n        26: 3,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 3,  # 'м'\n        6: 3,  # 'н'\n        4: 2,  # 'о'\n        13: 3,  # 'п'\n        7: 3,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 3,  # 'у'\n        29: 3,  # 'ф'\n        25: 3,  # 'х'\n        22: 3,  # 'ц'\n        21: 3,  # 'ч'\n        27: 3,  # 'ш'\n        24: 3,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    18: {  # 'б'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 0,  # 'б'\n        9: 3,  # 'в'\n        20: 1,  # 'г'\n        11: 2,  # 'д'\n        3: 3,  # 'е'\n        23: 1,  # 'ж'\n        15: 1,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 1,  # 'к'\n        10: 3,  # 'л'\n        14: 2,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 1,  # 'п'\n        7: 3,  # 'р'\n        8: 3,  # 'с'\n        5: 0,  # 'т'\n        19: 3,  # 'у'\n        29: 0,  # 'ф'\n        25: 2,  # 'х'\n        22: 1,  # 'ц'\n        21: 1,  # 'ч'\n        27: 1,  # 'ш'\n        24: 3,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 2,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    9: {  # 'в'\n        63: 1,  # 'e'\n        45: 1,  # '\\xad'\n        31: 0,  # 'А'\n        32: 1,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 1,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 1,  # 'б'\n        9: 0,  # 'в'\n        20: 2,  # 'г'\n        11: 3,  # 'д'\n        3: 3,  # 'е'\n        23: 1,  # 'ж'\n        15: 3,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 2,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 2,  # 'п'\n        7: 3,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 2,  # 'х'\n        22: 2,  # 'ц'\n        21: 3,  # 'ч'\n        27: 2,  # 'ш'\n        24: 1,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 2,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    20: {  # 'г'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 1,  # 'б'\n        9: 2,  # 'в'\n        20: 1,  # 'г'\n        11: 2,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 1,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 1,  # 'к'\n        10: 3,  # 'л'\n        14: 1,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 1,  # 'п'\n        7: 3,  # 'р'\n        8: 2,  # 'с'\n        5: 2,  # 'т'\n        19: 3,  # 'у'\n        29: 1,  # 'ф'\n        25: 1,  # 'х'\n        22: 0,  # 'ц'\n        21: 1,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 1,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    11: {  # 'д'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 2,  # 'б'\n        9: 3,  # 'в'\n        20: 2,  # 'г'\n        11: 2,  # 'д'\n        3: 3,  # 'е'\n        23: 3,  # 'ж'\n        15: 2,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 3,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 3,  # 'п'\n        7: 3,  # 'р'\n        8: 3,  # 'с'\n        5: 1,  # 'т'\n        19: 3,  # 'у'\n        29: 1,  # 'ф'\n        25: 2,  # 'х'\n        22: 2,  # 'ц'\n        21: 2,  # 'ч'\n        27: 1,  # 'ш'\n        24: 1,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 1,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    3: {  # 'е'\n        63: 0,  # 'e'\n        45: 1,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 2,  # 'а'\n        18: 3,  # 'б'\n        9: 3,  # 'в'\n        20: 3,  # 'г'\n        11: 3,  # 'д'\n        3: 2,  # 'е'\n        23: 3,  # 'ж'\n        15: 3,  # 'з'\n        2: 2,  # 'и'\n        26: 3,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 3,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 3,  # 'п'\n        7: 3,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 2,  # 'у'\n        29: 3,  # 'ф'\n        25: 3,  # 'х'\n        22: 3,  # 'ц'\n        21: 3,  # 'ч'\n        27: 3,  # 'ш'\n        24: 3,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    23: {  # 'ж'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 3,  # 'б'\n        9: 2,  # 'в'\n        20: 1,  # 'г'\n        11: 3,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 2,  # 'к'\n        10: 1,  # 'л'\n        14: 1,  # 'м'\n        6: 3,  # 'н'\n        4: 2,  # 'о'\n        13: 1,  # 'п'\n        7: 1,  # 'р'\n        8: 1,  # 'с'\n        5: 1,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 1,  # 'ц'\n        21: 1,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    15: {  # 'з'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 3,  # 'б'\n        9: 3,  # 'в'\n        20: 3,  # 'г'\n        11: 3,  # 'д'\n        3: 3,  # 'е'\n        23: 1,  # 'ж'\n        15: 1,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 3,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 3,  # 'п'\n        7: 3,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 3,  # 'у'\n        29: 1,  # 'ф'\n        25: 2,  # 'х'\n        22: 2,  # 'ц'\n        21: 2,  # 'ч'\n        27: 2,  # 'ш'\n        24: 1,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 1,  # 'ю'\n        16: 2,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    2: {  # 'и'\n        63: 1,  # 'e'\n        45: 1,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 1,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 1,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 1,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 1,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 3,  # 'б'\n        9: 3,  # 'в'\n        20: 3,  # 'г'\n        11: 3,  # 'д'\n        3: 3,  # 'е'\n        23: 3,  # 'ж'\n        15: 3,  # 'з'\n        2: 3,  # 'и'\n        26: 3,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 3,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 3,  # 'п'\n        7: 3,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 2,  # 'у'\n        29: 3,  # 'ф'\n        25: 3,  # 'х'\n        22: 3,  # 'ц'\n        21: 3,  # 'ч'\n        27: 3,  # 'ш'\n        24: 3,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    26: {  # 'й'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 1,  # 'а'\n        18: 2,  # 'б'\n        9: 2,  # 'в'\n        20: 1,  # 'г'\n        11: 2,  # 'д'\n        3: 2,  # 'е'\n        23: 0,  # 'ж'\n        15: 2,  # 'з'\n        2: 1,  # 'и'\n        26: 0,  # 'й'\n        12: 3,  # 'к'\n        10: 2,  # 'л'\n        14: 2,  # 'м'\n        6: 3,  # 'н'\n        4: 2,  # 'о'\n        13: 1,  # 'п'\n        7: 2,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 1,  # 'у'\n        29: 2,  # 'ф'\n        25: 1,  # 'х'\n        22: 2,  # 'ц'\n        21: 2,  # 'ч'\n        27: 1,  # 'ш'\n        24: 1,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    12: {  # 'к'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 1,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 1,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 1,  # 'б'\n        9: 3,  # 'в'\n        20: 2,  # 'г'\n        11: 1,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 2,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 1,  # 'к'\n        10: 3,  # 'л'\n        14: 2,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 1,  # 'п'\n        7: 3,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 3,  # 'у'\n        29: 1,  # 'ф'\n        25: 1,  # 'х'\n        22: 3,  # 'ц'\n        21: 2,  # 'ч'\n        27: 1,  # 'ш'\n        24: 0,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 2,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    10: {  # 'л'\n        63: 1,  # 'e'\n        45: 1,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 1,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 3,  # 'б'\n        9: 3,  # 'в'\n        20: 3,  # 'г'\n        11: 2,  # 'д'\n        3: 3,  # 'е'\n        23: 3,  # 'ж'\n        15: 2,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 3,  # 'к'\n        10: 1,  # 'л'\n        14: 2,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 2,  # 'п'\n        7: 2,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 3,  # 'у'\n        29: 2,  # 'ф'\n        25: 2,  # 'х'\n        22: 2,  # 'ц'\n        21: 2,  # 'ч'\n        27: 2,  # 'ш'\n        24: 1,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 2,  # 'ь'\n        42: 3,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    14: {  # 'м'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 1,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 3,  # 'б'\n        9: 3,  # 'в'\n        20: 1,  # 'г'\n        11: 1,  # 'д'\n        3: 3,  # 'е'\n        23: 1,  # 'ж'\n        15: 1,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 2,  # 'к'\n        10: 3,  # 'л'\n        14: 1,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 3,  # 'п'\n        7: 2,  # 'р'\n        8: 2,  # 'с'\n        5: 1,  # 'т'\n        19: 3,  # 'у'\n        29: 2,  # 'ф'\n        25: 1,  # 'х'\n        22: 2,  # 'ц'\n        21: 2,  # 'ч'\n        27: 2,  # 'ш'\n        24: 1,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 2,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    6: {  # 'н'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 1,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 2,  # 'б'\n        9: 2,  # 'в'\n        20: 3,  # 'г'\n        11: 3,  # 'д'\n        3: 3,  # 'е'\n        23: 2,  # 'ж'\n        15: 2,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 3,  # 'к'\n        10: 2,  # 'л'\n        14: 1,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 1,  # 'п'\n        7: 2,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 3,  # 'у'\n        29: 3,  # 'ф'\n        25: 2,  # 'х'\n        22: 3,  # 'ц'\n        21: 3,  # 'ч'\n        27: 2,  # 'ш'\n        24: 1,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 2,  # 'ь'\n        42: 2,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    4: {  # 'о'\n        63: 0,  # 'e'\n        45: 1,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 2,  # 'а'\n        18: 3,  # 'б'\n        9: 3,  # 'в'\n        20: 3,  # 'г'\n        11: 3,  # 'д'\n        3: 3,  # 'е'\n        23: 3,  # 'ж'\n        15: 3,  # 'з'\n        2: 3,  # 'и'\n        26: 3,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 3,  # 'м'\n        6: 3,  # 'н'\n        4: 2,  # 'о'\n        13: 3,  # 'п'\n        7: 3,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 2,  # 'у'\n        29: 3,  # 'ф'\n        25: 3,  # 'х'\n        22: 3,  # 'ц'\n        21: 3,  # 'ч'\n        27: 3,  # 'ш'\n        24: 3,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    13: {  # 'п'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 1,  # 'б'\n        9: 2,  # 'в'\n        20: 1,  # 'г'\n        11: 1,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 1,  # 'з'\n        2: 3,  # 'и'\n        26: 1,  # 'й'\n        12: 2,  # 'к'\n        10: 3,  # 'л'\n        14: 1,  # 'м'\n        6: 2,  # 'н'\n        4: 3,  # 'о'\n        13: 1,  # 'п'\n        7: 3,  # 'р'\n        8: 2,  # 'с'\n        5: 2,  # 'т'\n        19: 3,  # 'у'\n        29: 1,  # 'ф'\n        25: 1,  # 'х'\n        22: 2,  # 'ц'\n        21: 2,  # 'ч'\n        27: 1,  # 'ш'\n        24: 1,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 2,  # 'ю'\n        16: 2,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    7: {  # 'р'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 3,  # 'б'\n        9: 3,  # 'в'\n        20: 3,  # 'г'\n        11: 3,  # 'д'\n        3: 3,  # 'е'\n        23: 3,  # 'ж'\n        15: 2,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 3,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 2,  # 'п'\n        7: 1,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 3,  # 'у'\n        29: 2,  # 'ф'\n        25: 3,  # 'х'\n        22: 3,  # 'ц'\n        21: 2,  # 'ч'\n        27: 3,  # 'ш'\n        24: 1,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 2,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    8: {  # 'с'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 2,  # 'б'\n        9: 3,  # 'в'\n        20: 2,  # 'г'\n        11: 2,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 1,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 3,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 3,  # 'п'\n        7: 3,  # 'р'\n        8: 1,  # 'с'\n        5: 3,  # 'т'\n        19: 3,  # 'у'\n        29: 2,  # 'ф'\n        25: 2,  # 'х'\n        22: 2,  # 'ц'\n        21: 2,  # 'ч'\n        27: 2,  # 'ш'\n        24: 0,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 2,  # 'ь'\n        42: 2,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    5: {  # 'т'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 3,  # 'б'\n        9: 3,  # 'в'\n        20: 2,  # 'г'\n        11: 2,  # 'д'\n        3: 3,  # 'е'\n        23: 1,  # 'ж'\n        15: 1,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 2,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 2,  # 'п'\n        7: 3,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 3,  # 'у'\n        29: 1,  # 'ф'\n        25: 2,  # 'х'\n        22: 2,  # 'ц'\n        21: 2,  # 'ч'\n        27: 1,  # 'ш'\n        24: 1,  # 'щ'\n        17: 3,  # 'ъ'\n        52: 2,  # 'ь'\n        42: 2,  # 'ю'\n        16: 3,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    19: {  # 'у'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 3,  # 'б'\n        9: 3,  # 'в'\n        20: 3,  # 'г'\n        11: 3,  # 'д'\n        3: 2,  # 'е'\n        23: 3,  # 'ж'\n        15: 3,  # 'з'\n        2: 2,  # 'и'\n        26: 2,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 3,  # 'м'\n        6: 3,  # 'н'\n        4: 2,  # 'о'\n        13: 3,  # 'п'\n        7: 3,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 1,  # 'у'\n        29: 2,  # 'ф'\n        25: 2,  # 'х'\n        22: 2,  # 'ц'\n        21: 3,  # 'ч'\n        27: 3,  # 'ш'\n        24: 2,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    29: {  # 'ф'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 1,  # 'б'\n        9: 1,  # 'в'\n        20: 1,  # 'г'\n        11: 0,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 2,  # 'к'\n        10: 2,  # 'л'\n        14: 1,  # 'м'\n        6: 1,  # 'н'\n        4: 3,  # 'о'\n        13: 0,  # 'п'\n        7: 2,  # 'р'\n        8: 2,  # 'с'\n        5: 2,  # 'т'\n        19: 2,  # 'у'\n        29: 0,  # 'ф'\n        25: 1,  # 'х'\n        22: 0,  # 'ц'\n        21: 1,  # 'ч'\n        27: 1,  # 'ш'\n        24: 0,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 2,  # 'ь'\n        42: 1,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    25: {  # 'х'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 1,  # 'б'\n        9: 3,  # 'в'\n        20: 0,  # 'г'\n        11: 1,  # 'д'\n        3: 2,  # 'е'\n        23: 0,  # 'ж'\n        15: 1,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 1,  # 'к'\n        10: 2,  # 'л'\n        14: 2,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 1,  # 'п'\n        7: 3,  # 'р'\n        8: 1,  # 'с'\n        5: 2,  # 'т'\n        19: 3,  # 'у'\n        29: 0,  # 'ф'\n        25: 1,  # 'х'\n        22: 0,  # 'ц'\n        21: 1,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    22: {  # 'ц'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 1,  # 'б'\n        9: 2,  # 'в'\n        20: 1,  # 'г'\n        11: 1,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 1,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 2,  # 'к'\n        10: 1,  # 'л'\n        14: 1,  # 'м'\n        6: 1,  # 'н'\n        4: 2,  # 'о'\n        13: 1,  # 'п'\n        7: 1,  # 'р'\n        8: 1,  # 'с'\n        5: 1,  # 'т'\n        19: 2,  # 'у'\n        29: 1,  # 'ф'\n        25: 1,  # 'х'\n        22: 1,  # 'ц'\n        21: 1,  # 'ч'\n        27: 1,  # 'ш'\n        24: 1,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 0,  # 'ю'\n        16: 2,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    21: {  # 'ч'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 1,  # 'б'\n        9: 3,  # 'в'\n        20: 1,  # 'г'\n        11: 0,  # 'д'\n        3: 3,  # 'е'\n        23: 1,  # 'ж'\n        15: 0,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 3,  # 'к'\n        10: 2,  # 'л'\n        14: 2,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 0,  # 'п'\n        7: 2,  # 'р'\n        8: 0,  # 'с'\n        5: 2,  # 'т'\n        19: 3,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 1,  # 'ш'\n        24: 0,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    27: {  # 'ш'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 0,  # 'б'\n        9: 2,  # 'в'\n        20: 0,  # 'г'\n        11: 1,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 3,  # 'к'\n        10: 2,  # 'л'\n        14: 1,  # 'м'\n        6: 3,  # 'н'\n        4: 2,  # 'о'\n        13: 2,  # 'п'\n        7: 1,  # 'р'\n        8: 0,  # 'с'\n        5: 1,  # 'т'\n        19: 2,  # 'у'\n        29: 1,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 1,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 2,  # 'ъ'\n        52: 1,  # 'ь'\n        42: 1,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    24: {  # 'щ'\n        63: 1,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 3,  # 'а'\n        18: 0,  # 'б'\n        9: 1,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 3,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 3,  # 'и'\n        26: 0,  # 'й'\n        12: 1,  # 'к'\n        10: 0,  # 'л'\n        14: 0,  # 'м'\n        6: 2,  # 'н'\n        4: 3,  # 'о'\n        13: 0,  # 'п'\n        7: 1,  # 'р'\n        8: 0,  # 'с'\n        5: 2,  # 'т'\n        19: 3,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 1,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 2,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    17: {  # 'ъ'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 1,  # 'а'\n        18: 3,  # 'б'\n        9: 3,  # 'в'\n        20: 3,  # 'г'\n        11: 3,  # 'д'\n        3: 2,  # 'е'\n        23: 3,  # 'ж'\n        15: 3,  # 'з'\n        2: 1,  # 'и'\n        26: 2,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 3,  # 'м'\n        6: 3,  # 'н'\n        4: 3,  # 'о'\n        13: 3,  # 'п'\n        7: 3,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 1,  # 'у'\n        29: 1,  # 'ф'\n        25: 2,  # 'х'\n        22: 2,  # 'ц'\n        21: 3,  # 'ч'\n        27: 2,  # 'ш'\n        24: 3,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 2,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    52: {  # 'ь'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 0,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 1,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 0,  # 'и'\n        26: 0,  # 'й'\n        12: 1,  # 'к'\n        10: 0,  # 'л'\n        14: 0,  # 'м'\n        6: 1,  # 'н'\n        4: 3,  # 'о'\n        13: 0,  # 'п'\n        7: 0,  # 'р'\n        8: 0,  # 'с'\n        5: 1,  # 'т'\n        19: 0,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 1,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 1,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    42: {  # 'ю'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 1,  # 'а'\n        18: 2,  # 'б'\n        9: 1,  # 'в'\n        20: 2,  # 'г'\n        11: 2,  # 'д'\n        3: 1,  # 'е'\n        23: 2,  # 'ж'\n        15: 2,  # 'з'\n        2: 1,  # 'и'\n        26: 1,  # 'й'\n        12: 2,  # 'к'\n        10: 2,  # 'л'\n        14: 2,  # 'м'\n        6: 2,  # 'н'\n        4: 1,  # 'о'\n        13: 1,  # 'п'\n        7: 2,  # 'р'\n        8: 2,  # 'с'\n        5: 2,  # 'т'\n        19: 1,  # 'у'\n        29: 1,  # 'ф'\n        25: 1,  # 'х'\n        22: 2,  # 'ц'\n        21: 3,  # 'ч'\n        27: 1,  # 'ш'\n        24: 1,  # 'щ'\n        17: 1,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    16: {  # 'я'\n        63: 0,  # 'e'\n        45: 1,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 0,  # 'а'\n        18: 3,  # 'б'\n        9: 3,  # 'в'\n        20: 2,  # 'г'\n        11: 3,  # 'д'\n        3: 2,  # 'е'\n        23: 1,  # 'ж'\n        15: 2,  # 'з'\n        2: 1,  # 'и'\n        26: 2,  # 'й'\n        12: 3,  # 'к'\n        10: 3,  # 'л'\n        14: 3,  # 'м'\n        6: 3,  # 'н'\n        4: 1,  # 'о'\n        13: 2,  # 'п'\n        7: 2,  # 'р'\n        8: 3,  # 'с'\n        5: 3,  # 'т'\n        19: 1,  # 'у'\n        29: 1,  # 'ф'\n        25: 3,  # 'х'\n        22: 2,  # 'ц'\n        21: 1,  # 'ч'\n        27: 1,  # 'ш'\n        24: 2,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 1,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    58: {  # 'є'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 0,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 0,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 0,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 0,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 0,  # 'о'\n        13: 0,  # 'п'\n        7: 0,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 0,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n    62: {  # '№'\n        63: 0,  # 'e'\n        45: 0,  # '\\xad'\n        31: 0,  # 'А'\n        32: 0,  # 'Б'\n        35: 0,  # 'В'\n        43: 0,  # 'Г'\n        37: 0,  # 'Д'\n        44: 0,  # 'Е'\n        55: 0,  # 'Ж'\n        47: 0,  # 'З'\n        40: 0,  # 'И'\n        59: 0,  # 'Й'\n        33: 0,  # 'К'\n        46: 0,  # 'Л'\n        38: 0,  # 'М'\n        36: 0,  # 'Н'\n        41: 0,  # 'О'\n        30: 0,  # 'П'\n        39: 0,  # 'Р'\n        28: 0,  # 'С'\n        34: 0,  # 'Т'\n        51: 0,  # 'У'\n        48: 0,  # 'Ф'\n        49: 0,  # 'Х'\n        53: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        54: 0,  # 'Ш'\n        57: 0,  # 'Щ'\n        61: 0,  # 'Ъ'\n        60: 0,  # 'Ю'\n        56: 0,  # 'Я'\n        1: 0,  # 'а'\n        18: 0,  # 'б'\n        9: 0,  # 'в'\n        20: 0,  # 'г'\n        11: 0,  # 'д'\n        3: 0,  # 'е'\n        23: 0,  # 'ж'\n        15: 0,  # 'з'\n        2: 0,  # 'и'\n        26: 0,  # 'й'\n        12: 0,  # 'к'\n        10: 0,  # 'л'\n        14: 0,  # 'м'\n        6: 0,  # 'н'\n        4: 0,  # 'о'\n        13: 0,  # 'п'\n        7: 0,  # 'р'\n        8: 0,  # 'с'\n        5: 0,  # 'т'\n        19: 0,  # 'у'\n        29: 0,  # 'ф'\n        25: 0,  # 'х'\n        22: 0,  # 'ц'\n        21: 0,  # 'ч'\n        27: 0,  # 'ш'\n        24: 0,  # 'щ'\n        17: 0,  # 'ъ'\n        52: 0,  # 'ь'\n        42: 0,  # 'ю'\n        16: 0,  # 'я'\n        58: 0,  # 'є'\n        62: 0,  # '№'\n    },\n}\n\n# 255: Undefined characters that did not exist in training text\n# 254: Carriage/Return\n# 253: symbol (punctuation) that does not belong to word\n# 252: 0 - 9\n# 251: Control characters\n\n# Character Mapping Table(s):\nISO_8859_5_BULGARIAN_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 77,  # 'A'\n     66: 90,  # 'B'\n     67: 99,  # 'C'\n     68: 100,  # 'D'\n     69: 72,  # 'E'\n     70: 109,  # 'F'\n     71: 107,  # 'G'\n     72: 101,  # 'H'\n     73: 79,  # 'I'\n     74: 185,  # 'J'\n     75: 81,  # 'K'\n     76: 102,  # 'L'\n     77: 76,  # 'M'\n     78: 94,  # 'N'\n     79: 82,  # 'O'\n     80: 110,  # 'P'\n     81: 186,  # 'Q'\n     82: 108,  # 'R'\n     83: 91,  # 'S'\n     84: 74,  # 'T'\n     85: 119,  # 'U'\n     86: 84,  # 'V'\n     87: 96,  # 'W'\n     88: 111,  # 'X'\n     89: 187,  # 'Y'\n     90: 115,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 65,  # 'a'\n     98: 69,  # 'b'\n     99: 70,  # 'c'\n     100: 66,  # 'd'\n     101: 63,  # 'e'\n     102: 68,  # 'f'\n     103: 112,  # 'g'\n     104: 103,  # 'h'\n     105: 92,  # 'i'\n     106: 194,  # 'j'\n     107: 104,  # 'k'\n     108: 95,  # 'l'\n     109: 86,  # 'm'\n     110: 87,  # 'n'\n     111: 71,  # 'o'\n     112: 116,  # 'p'\n     113: 195,  # 'q'\n     114: 85,  # 'r'\n     115: 93,  # 's'\n     116: 97,  # 't'\n     117: 113,  # 'u'\n     118: 196,  # 'v'\n     119: 197,  # 'w'\n     120: 198,  # 'x'\n     121: 199,  # 'y'\n     122: 200,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 194,  # '\\x80'\n     129: 195,  # '\\x81'\n     130: 196,  # '\\x82'\n     131: 197,  # '\\x83'\n     132: 198,  # '\\x84'\n     133: 199,  # '\\x85'\n     134: 200,  # '\\x86'\n     135: 201,  # '\\x87'\n     136: 202,  # '\\x88'\n     137: 203,  # '\\x89'\n     138: 204,  # '\\x8a'\n     139: 205,  # '\\x8b'\n     140: 206,  # '\\x8c'\n     141: 207,  # '\\x8d'\n     142: 208,  # '\\x8e'\n     143: 209,  # '\\x8f'\n     144: 210,  # '\\x90'\n     145: 211,  # '\\x91'\n     146: 212,  # '\\x92'\n     147: 213,  # '\\x93'\n     148: 214,  # '\\x94'\n     149: 215,  # '\\x95'\n     150: 216,  # '\\x96'\n     151: 217,  # '\\x97'\n     152: 218,  # '\\x98'\n     153: 219,  # '\\x99'\n     154: 220,  # '\\x9a'\n     155: 221,  # '\\x9b'\n     156: 222,  # '\\x9c'\n     157: 223,  # '\\x9d'\n     158: 224,  # '\\x9e'\n     159: 225,  # '\\x9f'\n     160: 81,  # '\\xa0'\n     161: 226,  # 'Ё'\n     162: 227,  # 'Ђ'\n     163: 228,  # 'Ѓ'\n     164: 229,  # 'Є'\n     165: 230,  # 'Ѕ'\n     166: 105,  # 'І'\n     167: 231,  # 'Ї'\n     168: 232,  # 'Ј'\n     169: 233,  # 'Љ'\n     170: 234,  # 'Њ'\n     171: 235,  # 'Ћ'\n     172: 236,  # 'Ќ'\n     173: 45,  # '\\xad'\n     174: 237,  # 'Ў'\n     175: 238,  # 'Џ'\n     176: 31,  # 'А'\n     177: 32,  # 'Б'\n     178: 35,  # 'В'\n     179: 43,  # 'Г'\n     180: 37,  # 'Д'\n     181: 44,  # 'Е'\n     182: 55,  # 'Ж'\n     183: 47,  # 'З'\n     184: 40,  # 'И'\n     185: 59,  # 'Й'\n     186: 33,  # 'К'\n     187: 46,  # 'Л'\n     188: 38,  # 'М'\n     189: 36,  # 'Н'\n     190: 41,  # 'О'\n     191: 30,  # 'П'\n     192: 39,  # 'Р'\n     193: 28,  # 'С'\n     194: 34,  # 'Т'\n     195: 51,  # 'У'\n     196: 48,  # 'Ф'\n     197: 49,  # 'Х'\n     198: 53,  # 'Ц'\n     199: 50,  # 'Ч'\n     200: 54,  # 'Ш'\n     201: 57,  # 'Щ'\n     202: 61,  # 'Ъ'\n     203: 239,  # 'Ы'\n     204: 67,  # 'Ь'\n     205: 240,  # 'Э'\n     206: 60,  # 'Ю'\n     207: 56,  # 'Я'\n     208: 1,  # 'а'\n     209: 18,  # 'б'\n     210: 9,  # 'в'\n     211: 20,  # 'г'\n     212: 11,  # 'д'\n     213: 3,  # 'е'\n     214: 23,  # 'ж'\n     215: 15,  # 'з'\n     216: 2,  # 'и'\n     217: 26,  # 'й'\n     218: 12,  # 'к'\n     219: 10,  # 'л'\n     220: 14,  # 'м'\n     221: 6,  # 'н'\n     222: 4,  # 'о'\n     223: 13,  # 'п'\n     224: 7,  # 'р'\n     225: 8,  # 'с'\n     226: 5,  # 'т'\n     227: 19,  # 'у'\n     228: 29,  # 'ф'\n     229: 25,  # 'х'\n     230: 22,  # 'ц'\n     231: 21,  # 'ч'\n     232: 27,  # 'ш'\n     233: 24,  # 'щ'\n     234: 17,  # 'ъ'\n     235: 75,  # 'ы'\n     236: 52,  # 'ь'\n     237: 241,  # 'э'\n     238: 42,  # 'ю'\n     239: 16,  # 'я'\n     240: 62,  # '№'\n     241: 242,  # 'ё'\n     242: 243,  # 'ђ'\n     243: 244,  # 'ѓ'\n     244: 58,  # 'є'\n     245: 245,  # 'ѕ'\n     246: 98,  # 'і'\n     247: 246,  # 'ї'\n     248: 247,  # 'ј'\n     249: 248,  # 'љ'\n     250: 249,  # 'њ'\n     251: 250,  # 'ћ'\n     252: 251,  # 'ќ'\n     253: 91,  # '§'\n     254: 252,  # 'ў'\n     255: 253,  # 'џ'\n}\n\nISO_8859_5_BULGARIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-5',\n                                                    language='Bulgarian',\n                                                    char_to_order_map=ISO_8859_5_BULGARIAN_CHAR_TO_ORDER,\n                                                    language_model=BULGARIAN_LANG_MODEL,\n                                                    typical_positive_ratio=0.969392,\n                                                    keep_ascii_letters=False,\n                                                    alphabet='АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя')\n\nWINDOWS_1251_BULGARIAN_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 77,  # 'A'\n     66: 90,  # 'B'\n     67: 99,  # 'C'\n     68: 100,  # 'D'\n     69: 72,  # 'E'\n     70: 109,  # 'F'\n     71: 107,  # 'G'\n     72: 101,  # 'H'\n     73: 79,  # 'I'\n     74: 185,  # 'J'\n     75: 81,  # 'K'\n     76: 102,  # 'L'\n     77: 76,  # 'M'\n     78: 94,  # 'N'\n     79: 82,  # 'O'\n     80: 110,  # 'P'\n     81: 186,  # 'Q'\n     82: 108,  # 'R'\n     83: 91,  # 'S'\n     84: 74,  # 'T'\n     85: 119,  # 'U'\n     86: 84,  # 'V'\n     87: 96,  # 'W'\n     88: 111,  # 'X'\n     89: 187,  # 'Y'\n     90: 115,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 65,  # 'a'\n     98: 69,  # 'b'\n     99: 70,  # 'c'\n     100: 66,  # 'd'\n     101: 63,  # 'e'\n     102: 68,  # 'f'\n     103: 112,  # 'g'\n     104: 103,  # 'h'\n     105: 92,  # 'i'\n     106: 194,  # 'j'\n     107: 104,  # 'k'\n     108: 95,  # 'l'\n     109: 86,  # 'm'\n     110: 87,  # 'n'\n     111: 71,  # 'o'\n     112: 116,  # 'p'\n     113: 195,  # 'q'\n     114: 85,  # 'r'\n     115: 93,  # 's'\n     116: 97,  # 't'\n     117: 113,  # 'u'\n     118: 196,  # 'v'\n     119: 197,  # 'w'\n     120: 198,  # 'x'\n     121: 199,  # 'y'\n     122: 200,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 206,  # 'Ђ'\n     129: 207,  # 'Ѓ'\n     130: 208,  # '‚'\n     131: 209,  # 'ѓ'\n     132: 210,  # '„'\n     133: 211,  # '…'\n     134: 212,  # '†'\n     135: 213,  # '‡'\n     136: 120,  # '€'\n     137: 214,  # '‰'\n     138: 215,  # 'Љ'\n     139: 216,  # '‹'\n     140: 217,  # 'Њ'\n     141: 218,  # 'Ќ'\n     142: 219,  # 'Ћ'\n     143: 220,  # 'Џ'\n     144: 221,  # 'ђ'\n     145: 78,  # '‘'\n     146: 64,  # '’'\n     147: 83,  # '“'\n     148: 121,  # '”'\n     149: 98,  # '•'\n     150: 117,  # '–'\n     151: 105,  # '—'\n     152: 222,  # None\n     153: 223,  # '™'\n     154: 224,  # 'љ'\n     155: 225,  # '›'\n     156: 226,  # 'њ'\n     157: 227,  # 'ќ'\n     158: 228,  # 'ћ'\n     159: 229,  # 'џ'\n     160: 88,  # '\\xa0'\n     161: 230,  # 'Ў'\n     162: 231,  # 'ў'\n     163: 232,  # 'Ј'\n     164: 233,  # '¤'\n     165: 122,  # 'Ґ'\n     166: 89,  # '¦'\n     167: 106,  # '§'\n     168: 234,  # 'Ё'\n     169: 235,  # '©'\n     170: 236,  # 'Є'\n     171: 237,  # '«'\n     172: 238,  # '¬'\n     173: 45,  # '\\xad'\n     174: 239,  # '®'\n     175: 240,  # 'Ї'\n     176: 73,  # '°'\n     177: 80,  # '±'\n     178: 118,  # 'І'\n     179: 114,  # 'і'\n     180: 241,  # 'ґ'\n     181: 242,  # 'µ'\n     182: 243,  # '¶'\n     183: 244,  # '·'\n     184: 245,  # 'ё'\n     185: 62,  # '№'\n     186: 58,  # 'є'\n     187: 246,  # '»'\n     188: 247,  # 'ј'\n     189: 248,  # 'Ѕ'\n     190: 249,  # 'ѕ'\n     191: 250,  # 'ї'\n     192: 31,  # 'А'\n     193: 32,  # 'Б'\n     194: 35,  # 'В'\n     195: 43,  # 'Г'\n     196: 37,  # 'Д'\n     197: 44,  # 'Е'\n     198: 55,  # 'Ж'\n     199: 47,  # 'З'\n     200: 40,  # 'И'\n     201: 59,  # 'Й'\n     202: 33,  # 'К'\n     203: 46,  # 'Л'\n     204: 38,  # 'М'\n     205: 36,  # 'Н'\n     206: 41,  # 'О'\n     207: 30,  # 'П'\n     208: 39,  # 'Р'\n     209: 28,  # 'С'\n     210: 34,  # 'Т'\n     211: 51,  # 'У'\n     212: 48,  # 'Ф'\n     213: 49,  # 'Х'\n     214: 53,  # 'Ц'\n     215: 50,  # 'Ч'\n     216: 54,  # 'Ш'\n     217: 57,  # 'Щ'\n     218: 61,  # 'Ъ'\n     219: 251,  # 'Ы'\n     220: 67,  # 'Ь'\n     221: 252,  # 'Э'\n     222: 60,  # 'Ю'\n     223: 56,  # 'Я'\n     224: 1,  # 'а'\n     225: 18,  # 'б'\n     226: 9,  # 'в'\n     227: 20,  # 'г'\n     228: 11,  # 'д'\n     229: 3,  # 'е'\n     230: 23,  # 'ж'\n     231: 15,  # 'з'\n     232: 2,  # 'и'\n     233: 26,  # 'й'\n     234: 12,  # 'к'\n     235: 10,  # 'л'\n     236: 14,  # 'м'\n     237: 6,  # 'н'\n     238: 4,  # 'о'\n     239: 13,  # 'п'\n     240: 7,  # 'р'\n     241: 8,  # 'с'\n     242: 5,  # 'т'\n     243: 19,  # 'у'\n     244: 29,  # 'ф'\n     245: 25,  # 'х'\n     246: 22,  # 'ц'\n     247: 21,  # 'ч'\n     248: 27,  # 'ш'\n     249: 24,  # 'щ'\n     250: 17,  # 'ъ'\n     251: 75,  # 'ы'\n     252: 52,  # 'ь'\n     253: 253,  # 'э'\n     254: 42,  # 'ю'\n     255: 16,  # 'я'\n}\n\nWINDOWS_1251_BULGARIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1251',\n                                                      language='Bulgarian',\n                                                      char_to_order_map=WINDOWS_1251_BULGARIAN_CHAR_TO_ORDER,\n                                                      language_model=BULGARIAN_LANG_MODEL,\n                                                      typical_positive_ratio=0.969392,\n                                                      keep_ascii_letters=False,\n                                                      alphabet='АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя')\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/langgreekmodel.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nfrom chardet.sbcharsetprober import SingleByteCharSetModel\n\n\n# 3: Positive\n# 2: Likely\n# 1: Unlikely\n# 0: Negative\n\nGREEK_LANG_MODEL = {\n    60: {  # 'e'\n        60: 2,  # 'e'\n        55: 1,  # 'o'\n        58: 2,  # 't'\n        36: 1,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 1,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    55: {  # 'o'\n        60: 0,  # 'e'\n        55: 2,  # 'o'\n        58: 2,  # 't'\n        36: 1,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 1,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 1,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    58: {  # 't'\n        60: 2,  # 'e'\n        55: 1,  # 'o'\n        58: 1,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 1,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    36: {  # '·'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    61: {  # 'Ά'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 1,  # 'γ'\n        21: 2,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 2,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 1,  # 'π'\n        8: 2,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    46: {  # 'Έ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 2,  # 'β'\n        20: 2,  # 'γ'\n        21: 0,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 2,  # 'κ'\n        16: 2,  # 'λ'\n        10: 0,  # 'μ'\n        6: 3,  # 'ν'\n        30: 2,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 2,  # 'π'\n        8: 2,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 1,  # 'σ'\n        2: 2,  # 'τ'\n        12: 0,  # 'υ'\n        28: 2,  # 'φ'\n        23: 3,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    54: {  # 'Ό'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 2,  # 'λ'\n        10: 2,  # 'μ'\n        6: 2,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 2,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 2,  # 'σ'\n        2: 3,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 2,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    31: {  # 'Α'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 2,  # 'Β'\n        43: 2,  # 'Γ'\n        41: 1,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 2,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 2,  # 'Κ'\n        53: 2,  # 'Λ'\n        38: 2,  # 'Μ'\n        49: 2,  # 'Ν'\n        59: 1,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 2,  # 'Π'\n        48: 2,  # 'Ρ'\n        37: 2,  # 'Σ'\n        33: 2,  # 'Τ'\n        45: 2,  # 'Υ'\n        56: 2,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 2,  # 'γ'\n        21: 0,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 1,  # 'θ'\n        5: 0,  # 'ι'\n        11: 2,  # 'κ'\n        16: 3,  # 'λ'\n        10: 2,  # 'μ'\n        6: 3,  # 'ν'\n        30: 2,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 2,  # 'ς'\n        7: 2,  # 'σ'\n        2: 0,  # 'τ'\n        12: 3,  # 'υ'\n        28: 2,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    51: {  # 'Β'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 2,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 1,  # 'Ε'\n        40: 1,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 1,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 1,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 2,  # 'έ'\n        22: 2,  # 'ή'\n        15: 0,  # 'ί'\n        1: 2,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 2,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 2,  # 'ι'\n        11: 0,  # 'κ'\n        16: 2,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 0,  # 'π'\n        8: 2,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    43: {  # 'Γ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 1,  # 'Α'\n        51: 0,  # 'Β'\n        43: 2,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 1,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 1,  # 'Κ'\n        53: 1,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 1,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 2,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 2,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 1,  # 'Χ'\n        57: 2,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 2,  # 'ί'\n        1: 2,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 2,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 3,  # 'ι'\n        11: 0,  # 'κ'\n        16: 2,  # 'λ'\n        10: 0,  # 'μ'\n        6: 2,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 0,  # 'π'\n        8: 2,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    41: {  # 'Δ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 2,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 2,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 2,  # 'ή'\n        15: 2,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 2,  # 'η'\n        25: 0,  # 'θ'\n        5: 3,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 0,  # 'π'\n        8: 2,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 2,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 2,  # 'ω'\n        19: 1,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 2,  # 'ώ'\n    },\n    34: {  # 'Ε'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 2,  # 'Α'\n        51: 0,  # 'Β'\n        43: 2,  # 'Γ'\n        41: 2,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 2,  # 'Κ'\n        53: 2,  # 'Λ'\n        38: 2,  # 'Μ'\n        49: 2,  # 'Ν'\n        59: 1,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 2,  # 'Π'\n        48: 2,  # 'Ρ'\n        37: 2,  # 'Σ'\n        33: 2,  # 'Τ'\n        45: 2,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 2,  # 'Χ'\n        57: 2,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 3,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 3,  # 'γ'\n        21: 2,  # 'δ'\n        3: 1,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 1,  # 'θ'\n        5: 2,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 2,  # 'μ'\n        6: 3,  # 'ν'\n        30: 2,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 3,  # 'π'\n        8: 2,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 2,  # 'σ'\n        2: 2,  # 'τ'\n        12: 2,  # 'υ'\n        28: 2,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 1,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    40: {  # 'Η'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 1,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 2,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 2,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 2,  # 'Μ'\n        49: 2,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 2,  # 'Π'\n        48: 2,  # 'Ρ'\n        37: 2,  # 'Σ'\n        33: 2,  # 'Τ'\n        45: 1,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 2,  # 'λ'\n        10: 0,  # 'μ'\n        6: 1,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 1,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    52: {  # 'Θ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 2,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 2,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 1,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 1,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 2,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 2,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 2,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    47: {  # 'Ι'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 2,  # 'Α'\n        51: 1,  # 'Β'\n        43: 1,  # 'Γ'\n        41: 2,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 2,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 2,  # 'Κ'\n        53: 2,  # 'Λ'\n        38: 2,  # 'Μ'\n        49: 2,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 2,  # 'Ρ'\n        37: 2,  # 'Σ'\n        33: 2,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 2,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 2,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 2,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 2,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 1,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 2,  # 'σ'\n        2: 1,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 1,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    44: {  # 'Κ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 2,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 1,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 2,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 1,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 2,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 1,  # 'Τ'\n        45: 2,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 1,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 2,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 2,  # 'ι'\n        11: 0,  # 'κ'\n        16: 2,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 0,  # 'π'\n        8: 2,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 2,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 2,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 2,  # 'ώ'\n    },\n    53: {  # 'Λ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 2,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 2,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 2,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 2,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 2,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 2,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 2,  # 'έ'\n        22: 0,  # 'ή'\n        15: 2,  # 'ί'\n        1: 2,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 2,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 1,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 2,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 2,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    38: {  # 'Μ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 2,  # 'Α'\n        51: 2,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 2,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 2,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 2,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 2,  # 'έ'\n        22: 2,  # 'ή'\n        15: 2,  # 'ί'\n        1: 2,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 2,  # 'η'\n        25: 0,  # 'θ'\n        5: 3,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 3,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 2,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 2,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    49: {  # 'Ν'\n        60: 2,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 2,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 2,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 2,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 2,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 2,  # 'έ'\n        22: 0,  # 'ή'\n        15: 2,  # 'ί'\n        1: 2,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 1,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 1,  # 'ω'\n        19: 2,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    59: {  # 'Ξ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 1,  # 'Ε'\n        40: 1,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 1,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 2,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 2,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 2,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    39: {  # 'Ο'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 1,  # 'Β'\n        43: 2,  # 'Γ'\n        41: 2,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 1,  # 'Η'\n        52: 2,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 2,  # 'Κ'\n        53: 2,  # 'Λ'\n        38: 2,  # 'Μ'\n        49: 2,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 2,  # 'Π'\n        48: 2,  # 'Ρ'\n        37: 2,  # 'Σ'\n        33: 2,  # 'Τ'\n        45: 2,  # 'Υ'\n        56: 2,  # 'Φ'\n        50: 2,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 2,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 3,  # 'ι'\n        11: 2,  # 'κ'\n        16: 2,  # 'λ'\n        10: 2,  # 'μ'\n        6: 2,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 2,  # 'π'\n        8: 2,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 2,  # 'τ'\n        12: 2,  # 'υ'\n        28: 1,  # 'φ'\n        23: 1,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    35: {  # 'Π'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 2,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 2,  # 'Λ'\n        38: 1,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 2,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 1,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 1,  # 'Χ'\n        57: 2,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 1,  # 'έ'\n        22: 1,  # 'ή'\n        15: 2,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 2,  # 'η'\n        25: 0,  # 'θ'\n        5: 2,  # 'ι'\n        11: 0,  # 'κ'\n        16: 2,  # 'λ'\n        10: 0,  # 'μ'\n        6: 2,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 3,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 2,  # 'υ'\n        28: 0,  # 'φ'\n        23: 2,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 2,  # 'ω'\n        19: 2,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    48: {  # 'Ρ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 2,  # 'Α'\n        51: 0,  # 'Β'\n        43: 1,  # 'Γ'\n        41: 1,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 2,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 2,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 2,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 1,  # 'Τ'\n        45: 1,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 1,  # 'Χ'\n        57: 1,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 2,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 1,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 3,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 2,  # 'ω'\n        19: 0,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    37: {  # 'Σ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 2,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 1,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 2,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 2,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 2,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 2,  # 'Σ'\n        33: 2,  # 'Τ'\n        45: 2,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 2,  # 'Χ'\n        57: 2,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 2,  # 'ή'\n        15: 2,  # 'ί'\n        1: 2,  # 'α'\n        29: 2,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 0,  # 'θ'\n        5: 2,  # 'ι'\n        11: 2,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 2,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 0,  # 'φ'\n        23: 2,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 2,  # 'ω'\n        19: 0,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 2,  # 'ώ'\n    },\n    33: {  # 'Τ'\n        60: 0,  # 'e'\n        55: 1,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 2,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 2,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 2,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 2,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 1,  # 'Τ'\n        45: 1,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 2,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 2,  # 'έ'\n        22: 0,  # 'ή'\n        15: 2,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 2,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 2,  # 'η'\n        25: 0,  # 'θ'\n        5: 2,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 2,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 2,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 2,  # 'σ'\n        2: 0,  # 'τ'\n        12: 2,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 2,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    45: {  # 'Υ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 2,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 1,  # 'Ε'\n        40: 2,  # 'Η'\n        52: 2,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 1,  # 'Λ'\n        38: 2,  # 'Μ'\n        49: 2,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 2,  # 'Π'\n        48: 1,  # 'Ρ'\n        37: 2,  # 'Σ'\n        33: 2,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 1,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 2,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 3,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    56: {  # 'Φ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 1,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 1,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 2,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 2,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 2,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 2,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 2,  # 'τ'\n        12: 2,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 1,  # 'ύ'\n        27: 1,  # 'ώ'\n    },\n    50: {  # 'Χ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 1,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 2,  # 'Ε'\n        40: 2,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 2,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 1,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 1,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 2,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 1,  # 'Χ'\n        57: 1,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 2,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 2,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 0,  # 'π'\n        8: 3,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 2,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 2,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    57: {  # 'Ω'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 1,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 1,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 2,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 2,  # 'Ρ'\n        37: 2,  # 'Σ'\n        33: 2,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 0,  # 'π'\n        8: 2,  # 'ρ'\n        14: 2,  # 'ς'\n        7: 2,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 1,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    17: {  # 'ά'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 2,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 3,  # 'β'\n        20: 3,  # 'γ'\n        21: 3,  # 'δ'\n        3: 3,  # 'ε'\n        32: 3,  # 'ζ'\n        13: 0,  # 'η'\n        25: 3,  # 'θ'\n        5: 2,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 3,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 0,  # 'υ'\n        28: 3,  # 'φ'\n        23: 3,  # 'χ'\n        42: 3,  # 'ψ'\n        24: 2,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    18: {  # 'έ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 3,  # 'α'\n        29: 2,  # 'β'\n        20: 3,  # 'γ'\n        21: 2,  # 'δ'\n        3: 3,  # 'ε'\n        32: 2,  # 'ζ'\n        13: 0,  # 'η'\n        25: 3,  # 'θ'\n        5: 0,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 3,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 0,  # 'υ'\n        28: 3,  # 'φ'\n        23: 3,  # 'χ'\n        42: 3,  # 'ψ'\n        24: 2,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    22: {  # 'ή'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 1,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 3,  # 'γ'\n        21: 3,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 3,  # 'θ'\n        5: 0,  # 'ι'\n        11: 3,  # 'κ'\n        16: 2,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 2,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 0,  # 'υ'\n        28: 2,  # 'φ'\n        23: 3,  # 'χ'\n        42: 2,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    15: {  # 'ί'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 3,  # 'α'\n        29: 2,  # 'β'\n        20: 3,  # 'γ'\n        21: 3,  # 'δ'\n        3: 3,  # 'ε'\n        32: 3,  # 'ζ'\n        13: 3,  # 'η'\n        25: 3,  # 'θ'\n        5: 0,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 3,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 0,  # 'υ'\n        28: 1,  # 'φ'\n        23: 3,  # 'χ'\n        42: 2,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    1: {  # 'α'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 2,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 2,  # 'έ'\n        22: 0,  # 'ή'\n        15: 3,  # 'ί'\n        1: 0,  # 'α'\n        29: 3,  # 'β'\n        20: 3,  # 'γ'\n        21: 3,  # 'δ'\n        3: 2,  # 'ε'\n        32: 3,  # 'ζ'\n        13: 1,  # 'η'\n        25: 3,  # 'θ'\n        5: 3,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 3,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 3,  # 'φ'\n        23: 3,  # 'χ'\n        42: 2,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 2,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    29: {  # 'β'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 2,  # 'έ'\n        22: 3,  # 'ή'\n        15: 2,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 2,  # 'γ'\n        21: 2,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 2,  # 'η'\n        25: 0,  # 'θ'\n        5: 3,  # 'ι'\n        11: 0,  # 'κ'\n        16: 3,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 3,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 2,  # 'ω'\n        19: 2,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 2,  # 'ώ'\n    },\n    20: {  # 'γ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 3,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 0,  # 'θ'\n        5: 3,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 3,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 3,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 2,  # 'υ'\n        28: 0,  # 'φ'\n        23: 3,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    21: {  # 'δ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 0,  # 'θ'\n        5: 3,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 3,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 3,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 3,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    3: {  # 'ε'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 2,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 3,  # 'ί'\n        1: 2,  # 'α'\n        29: 3,  # 'β'\n        20: 3,  # 'γ'\n        21: 3,  # 'δ'\n        3: 2,  # 'ε'\n        32: 2,  # 'ζ'\n        13: 0,  # 'η'\n        25: 3,  # 'θ'\n        5: 3,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 3,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 3,  # 'φ'\n        23: 3,  # 'χ'\n        42: 2,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 2,  # 'ό'\n        26: 3,  # 'ύ'\n        27: 2,  # 'ώ'\n    },\n    32: {  # 'ζ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 2,  # 'έ'\n        22: 2,  # 'ή'\n        15: 2,  # 'ί'\n        1: 2,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 0,  # 'θ'\n        5: 2,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 1,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 2,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 2,  # 'ώ'\n    },\n    13: {  # 'η'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 2,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 3,  # 'γ'\n        21: 2,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 3,  # 'θ'\n        5: 0,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 2,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 2,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 0,  # 'υ'\n        28: 2,  # 'φ'\n        23: 3,  # 'χ'\n        42: 2,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    25: {  # 'θ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 2,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 0,  # 'θ'\n        5: 3,  # 'ι'\n        11: 0,  # 'κ'\n        16: 1,  # 'λ'\n        10: 3,  # 'μ'\n        6: 2,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 3,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 3,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 3,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    5: {  # 'ι'\n        60: 0,  # 'e'\n        55: 1,  # 'o'\n        58: 0,  # 't'\n        36: 2,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 1,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 0,  # 'ί'\n        1: 3,  # 'α'\n        29: 3,  # 'β'\n        20: 3,  # 'γ'\n        21: 3,  # 'δ'\n        3: 3,  # 'ε'\n        32: 2,  # 'ζ'\n        13: 3,  # 'η'\n        25: 3,  # 'θ'\n        5: 0,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 3,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 0,  # 'υ'\n        28: 2,  # 'φ'\n        23: 3,  # 'χ'\n        42: 2,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    11: {  # 'κ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 3,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 2,  # 'θ'\n        5: 3,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 2,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 2,  # 'π'\n        8: 3,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 2,  # 'φ'\n        23: 2,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 3,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    16: {  # 'λ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 3,  # 'α'\n        29: 1,  # 'β'\n        20: 2,  # 'γ'\n        21: 1,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 2,  # 'θ'\n        5: 3,  # 'ι'\n        11: 2,  # 'κ'\n        16: 3,  # 'λ'\n        10: 2,  # 'μ'\n        6: 2,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 3,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 2,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 3,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    10: {  # 'μ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 1,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 3,  # 'α'\n        29: 3,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 0,  # 'θ'\n        5: 3,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 3,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 2,  # 'υ'\n        28: 3,  # 'φ'\n        23: 0,  # 'χ'\n        42: 2,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 2,  # 'ώ'\n    },\n    6: {  # 'ν'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 2,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 3,  # 'δ'\n        3: 3,  # 'ε'\n        32: 2,  # 'ζ'\n        13: 3,  # 'η'\n        25: 3,  # 'θ'\n        5: 3,  # 'ι'\n        11: 0,  # 'κ'\n        16: 1,  # 'λ'\n        10: 0,  # 'μ'\n        6: 2,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 3,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    30: {  # 'ξ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 2,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 0,  # 'θ'\n        5: 2,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 3,  # 'τ'\n        12: 2,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 2,  # 'ό'\n        26: 3,  # 'ύ'\n        27: 1,  # 'ώ'\n    },\n    4: {  # 'ο'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 2,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 2,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 2,  # 'α'\n        29: 3,  # 'β'\n        20: 3,  # 'γ'\n        21: 3,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 3,  # 'θ'\n        5: 3,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 2,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 3,  # 'φ'\n        23: 3,  # 'χ'\n        42: 2,  # 'ψ'\n        24: 2,  # 'ω'\n        19: 1,  # 'ό'\n        26: 3,  # 'ύ'\n        27: 2,  # 'ώ'\n    },\n    9: {  # 'π'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 0,  # 'θ'\n        5: 3,  # 'ι'\n        11: 0,  # 'κ'\n        16: 3,  # 'λ'\n        10: 0,  # 'μ'\n        6: 2,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 3,  # 'ρ'\n        14: 2,  # 'ς'\n        7: 0,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 0,  # 'φ'\n        23: 2,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    8: {  # 'ρ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 3,  # 'α'\n        29: 2,  # 'β'\n        20: 3,  # 'γ'\n        21: 2,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 3,  # 'θ'\n        5: 3,  # 'ι'\n        11: 3,  # 'κ'\n        16: 1,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 2,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 2,  # 'π'\n        8: 2,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 2,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 3,  # 'φ'\n        23: 3,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 3,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    14: {  # 'ς'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 2,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 0,  # 'θ'\n        5: 0,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 0,  # 'τ'\n        12: 0,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    7: {  # 'σ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 2,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 3,  # 'α'\n        29: 3,  # 'β'\n        20: 0,  # 'γ'\n        21: 2,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 3,  # 'θ'\n        5: 3,  # 'ι'\n        11: 3,  # 'κ'\n        16: 2,  # 'λ'\n        10: 3,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 3,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 3,  # 'φ'\n        23: 3,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 3,  # 'ύ'\n        27: 2,  # 'ώ'\n    },\n    2: {  # 'τ'\n        60: 0,  # 'e'\n        55: 2,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 2,  # 'ζ'\n        13: 3,  # 'η'\n        25: 0,  # 'θ'\n        5: 3,  # 'ι'\n        11: 2,  # 'κ'\n        16: 2,  # 'λ'\n        10: 3,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 3,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 2,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 3,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    12: {  # 'υ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 2,  # 'έ'\n        22: 3,  # 'ή'\n        15: 2,  # 'ί'\n        1: 3,  # 'α'\n        29: 2,  # 'β'\n        20: 3,  # 'γ'\n        21: 2,  # 'δ'\n        3: 2,  # 'ε'\n        32: 2,  # 'ζ'\n        13: 2,  # 'η'\n        25: 3,  # 'θ'\n        5: 2,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 3,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 0,  # 'υ'\n        28: 2,  # 'φ'\n        23: 3,  # 'χ'\n        42: 2,  # 'ψ'\n        24: 2,  # 'ω'\n        19: 2,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 2,  # 'ώ'\n    },\n    28: {  # 'φ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 3,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 2,  # 'η'\n        25: 2,  # 'θ'\n        5: 3,  # 'ι'\n        11: 0,  # 'κ'\n        16: 2,  # 'λ'\n        10: 0,  # 'μ'\n        6: 1,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 3,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 1,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 2,  # 'ύ'\n        27: 2,  # 'ώ'\n    },\n    23: {  # 'χ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 3,  # 'ά'\n        18: 2,  # 'έ'\n        22: 3,  # 'ή'\n        15: 3,  # 'ί'\n        1: 3,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 2,  # 'η'\n        25: 2,  # 'θ'\n        5: 3,  # 'ι'\n        11: 0,  # 'κ'\n        16: 2,  # 'λ'\n        10: 2,  # 'μ'\n        6: 3,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 0,  # 'π'\n        8: 3,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 3,  # 'τ'\n        12: 3,  # 'υ'\n        28: 0,  # 'φ'\n        23: 2,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 3,  # 'ω'\n        19: 3,  # 'ό'\n        26: 3,  # 'ύ'\n        27: 3,  # 'ώ'\n    },\n    42: {  # 'ψ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 2,  # 'ά'\n        18: 2,  # 'έ'\n        22: 1,  # 'ή'\n        15: 2,  # 'ί'\n        1: 2,  # 'α'\n        29: 0,  # 'β'\n        20: 0,  # 'γ'\n        21: 0,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 3,  # 'η'\n        25: 0,  # 'θ'\n        5: 2,  # 'ι'\n        11: 0,  # 'κ'\n        16: 0,  # 'λ'\n        10: 0,  # 'μ'\n        6: 0,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 0,  # 'π'\n        8: 0,  # 'ρ'\n        14: 0,  # 'ς'\n        7: 0,  # 'σ'\n        2: 2,  # 'τ'\n        12: 1,  # 'υ'\n        28: 0,  # 'φ'\n        23: 0,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 2,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    24: {  # 'ω'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 1,  # 'ά'\n        18: 0,  # 'έ'\n        22: 2,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 2,  # 'β'\n        20: 3,  # 'γ'\n        21: 2,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 0,  # 'η'\n        25: 3,  # 'θ'\n        5: 2,  # 'ι'\n        11: 0,  # 'κ'\n        16: 2,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 0,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 0,  # 'υ'\n        28: 2,  # 'φ'\n        23: 2,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    19: {  # 'ό'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 3,  # 'β'\n        20: 3,  # 'γ'\n        21: 3,  # 'δ'\n        3: 1,  # 'ε'\n        32: 2,  # 'ζ'\n        13: 2,  # 'η'\n        25: 2,  # 'θ'\n        5: 2,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 1,  # 'ξ'\n        4: 2,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 0,  # 'υ'\n        28: 2,  # 'φ'\n        23: 3,  # 'χ'\n        42: 2,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    26: {  # 'ύ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 2,  # 'α'\n        29: 2,  # 'β'\n        20: 2,  # 'γ'\n        21: 1,  # 'δ'\n        3: 3,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 2,  # 'η'\n        25: 3,  # 'θ'\n        5: 0,  # 'ι'\n        11: 3,  # 'κ'\n        16: 3,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 2,  # 'ξ'\n        4: 3,  # 'ο'\n        9: 3,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 0,  # 'υ'\n        28: 2,  # 'φ'\n        23: 2,  # 'χ'\n        42: 2,  # 'ψ'\n        24: 2,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n    27: {  # 'ώ'\n        60: 0,  # 'e'\n        55: 0,  # 'o'\n        58: 0,  # 't'\n        36: 0,  # '·'\n        61: 0,  # 'Ά'\n        46: 0,  # 'Έ'\n        54: 0,  # 'Ό'\n        31: 0,  # 'Α'\n        51: 0,  # 'Β'\n        43: 0,  # 'Γ'\n        41: 0,  # 'Δ'\n        34: 0,  # 'Ε'\n        40: 0,  # 'Η'\n        52: 0,  # 'Θ'\n        47: 0,  # 'Ι'\n        44: 0,  # 'Κ'\n        53: 0,  # 'Λ'\n        38: 0,  # 'Μ'\n        49: 0,  # 'Ν'\n        59: 0,  # 'Ξ'\n        39: 0,  # 'Ο'\n        35: 0,  # 'Π'\n        48: 0,  # 'Ρ'\n        37: 0,  # 'Σ'\n        33: 0,  # 'Τ'\n        45: 0,  # 'Υ'\n        56: 0,  # 'Φ'\n        50: 0,  # 'Χ'\n        57: 0,  # 'Ω'\n        17: 0,  # 'ά'\n        18: 0,  # 'έ'\n        22: 0,  # 'ή'\n        15: 0,  # 'ί'\n        1: 0,  # 'α'\n        29: 1,  # 'β'\n        20: 0,  # 'γ'\n        21: 3,  # 'δ'\n        3: 0,  # 'ε'\n        32: 0,  # 'ζ'\n        13: 1,  # 'η'\n        25: 2,  # 'θ'\n        5: 2,  # 'ι'\n        11: 0,  # 'κ'\n        16: 2,  # 'λ'\n        10: 3,  # 'μ'\n        6: 3,  # 'ν'\n        30: 1,  # 'ξ'\n        4: 0,  # 'ο'\n        9: 2,  # 'π'\n        8: 3,  # 'ρ'\n        14: 3,  # 'ς'\n        7: 3,  # 'σ'\n        2: 3,  # 'τ'\n        12: 0,  # 'υ'\n        28: 1,  # 'φ'\n        23: 1,  # 'χ'\n        42: 0,  # 'ψ'\n        24: 0,  # 'ω'\n        19: 0,  # 'ό'\n        26: 0,  # 'ύ'\n        27: 0,  # 'ώ'\n    },\n}\n\n# 255: Undefined characters that did not exist in training text\n# 254: Carriage/Return\n# 253: symbol (punctuation) that does not belong to word\n# 252: 0 - 9\n# 251: Control characters\n\n# Character Mapping Table(s):\nWINDOWS_1253_GREEK_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 82,  # 'A'\n     66: 100,  # 'B'\n     67: 104,  # 'C'\n     68: 94,  # 'D'\n     69: 98,  # 'E'\n     70: 101,  # 'F'\n     71: 116,  # 'G'\n     72: 102,  # 'H'\n     73: 111,  # 'I'\n     74: 187,  # 'J'\n     75: 117,  # 'K'\n     76: 92,  # 'L'\n     77: 88,  # 'M'\n     78: 113,  # 'N'\n     79: 85,  # 'O'\n     80: 79,  # 'P'\n     81: 118,  # 'Q'\n     82: 105,  # 'R'\n     83: 83,  # 'S'\n     84: 67,  # 'T'\n     85: 114,  # 'U'\n     86: 119,  # 'V'\n     87: 95,  # 'W'\n     88: 99,  # 'X'\n     89: 109,  # 'Y'\n     90: 188,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 72,  # 'a'\n     98: 70,  # 'b'\n     99: 80,  # 'c'\n     100: 81,  # 'd'\n     101: 60,  # 'e'\n     102: 96,  # 'f'\n     103: 93,  # 'g'\n     104: 89,  # 'h'\n     105: 68,  # 'i'\n     106: 120,  # 'j'\n     107: 97,  # 'k'\n     108: 77,  # 'l'\n     109: 86,  # 'm'\n     110: 69,  # 'n'\n     111: 55,  # 'o'\n     112: 78,  # 'p'\n     113: 115,  # 'q'\n     114: 65,  # 'r'\n     115: 66,  # 's'\n     116: 58,  # 't'\n     117: 76,  # 'u'\n     118: 106,  # 'v'\n     119: 103,  # 'w'\n     120: 87,  # 'x'\n     121: 107,  # 'y'\n     122: 112,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 255,  # '€'\n     129: 255,  # None\n     130: 255,  # '‚'\n     131: 255,  # 'ƒ'\n     132: 255,  # '„'\n     133: 255,  # '…'\n     134: 255,  # '†'\n     135: 255,  # '‡'\n     136: 255,  # None\n     137: 255,  # '‰'\n     138: 255,  # None\n     139: 255,  # '‹'\n     140: 255,  # None\n     141: 255,  # None\n     142: 255,  # None\n     143: 255,  # None\n     144: 255,  # None\n     145: 255,  # '‘'\n     146: 255,  # '’'\n     147: 255,  # '“'\n     148: 255,  # '”'\n     149: 255,  # '•'\n     150: 255,  # '–'\n     151: 255,  # '—'\n     152: 255,  # None\n     153: 255,  # '™'\n     154: 255,  # None\n     155: 255,  # '›'\n     156: 255,  # None\n     157: 255,  # None\n     158: 255,  # None\n     159: 255,  # None\n     160: 253,  # '\\xa0'\n     161: 233,  # '΅'\n     162: 61,  # 'Ά'\n     163: 253,  # '£'\n     164: 253,  # '¤'\n     165: 253,  # '¥'\n     166: 253,  # '¦'\n     167: 253,  # '§'\n     168: 253,  # '¨'\n     169: 253,  # '©'\n     170: 253,  # None\n     171: 253,  # '«'\n     172: 253,  # '¬'\n     173: 74,  # '\\xad'\n     174: 253,  # '®'\n     175: 253,  # '―'\n     176: 253,  # '°'\n     177: 253,  # '±'\n     178: 253,  # '²'\n     179: 253,  # '³'\n     180: 247,  # '΄'\n     181: 253,  # 'µ'\n     182: 253,  # '¶'\n     183: 36,  # '·'\n     184: 46,  # 'Έ'\n     185: 71,  # 'Ή'\n     186: 73,  # 'Ί'\n     187: 253,  # '»'\n     188: 54,  # 'Ό'\n     189: 253,  # '½'\n     190: 108,  # 'Ύ'\n     191: 123,  # 'Ώ'\n     192: 110,  # 'ΐ'\n     193: 31,  # 'Α'\n     194: 51,  # 'Β'\n     195: 43,  # 'Γ'\n     196: 41,  # 'Δ'\n     197: 34,  # 'Ε'\n     198: 91,  # 'Ζ'\n     199: 40,  # 'Η'\n     200: 52,  # 'Θ'\n     201: 47,  # 'Ι'\n     202: 44,  # 'Κ'\n     203: 53,  # 'Λ'\n     204: 38,  # 'Μ'\n     205: 49,  # 'Ν'\n     206: 59,  # 'Ξ'\n     207: 39,  # 'Ο'\n     208: 35,  # 'Π'\n     209: 48,  # 'Ρ'\n     210: 250,  # None\n     211: 37,  # 'Σ'\n     212: 33,  # 'Τ'\n     213: 45,  # 'Υ'\n     214: 56,  # 'Φ'\n     215: 50,  # 'Χ'\n     216: 84,  # 'Ψ'\n     217: 57,  # 'Ω'\n     218: 120,  # 'Ϊ'\n     219: 121,  # 'Ϋ'\n     220: 17,  # 'ά'\n     221: 18,  # 'έ'\n     222: 22,  # 'ή'\n     223: 15,  # 'ί'\n     224: 124,  # 'ΰ'\n     225: 1,  # 'α'\n     226: 29,  # 'β'\n     227: 20,  # 'γ'\n     228: 21,  # 'δ'\n     229: 3,  # 'ε'\n     230: 32,  # 'ζ'\n     231: 13,  # 'η'\n     232: 25,  # 'θ'\n     233: 5,  # 'ι'\n     234: 11,  # 'κ'\n     235: 16,  # 'λ'\n     236: 10,  # 'μ'\n     237: 6,  # 'ν'\n     238: 30,  # 'ξ'\n     239: 4,  # 'ο'\n     240: 9,  # 'π'\n     241: 8,  # 'ρ'\n     242: 14,  # 'ς'\n     243: 7,  # 'σ'\n     244: 2,  # 'τ'\n     245: 12,  # 'υ'\n     246: 28,  # 'φ'\n     247: 23,  # 'χ'\n     248: 42,  # 'ψ'\n     249: 24,  # 'ω'\n     250: 64,  # 'ϊ'\n     251: 75,  # 'ϋ'\n     252: 19,  # 'ό'\n     253: 26,  # 'ύ'\n     254: 27,  # 'ώ'\n     255: 253,  # None\n}\n\nWINDOWS_1253_GREEK_MODEL = SingleByteCharSetModel(charset_name='windows-1253',\n                                                  language='Greek',\n                                                  char_to_order_map=WINDOWS_1253_GREEK_CHAR_TO_ORDER,\n                                                  language_model=GREEK_LANG_MODEL,\n                                                  typical_positive_ratio=0.982851,\n                                                  keep_ascii_letters=False,\n                                                  alphabet='ΆΈΉΊΌΎΏΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπρςστυφχψωόύώ')\n\nISO_8859_7_GREEK_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 82,  # 'A'\n     66: 100,  # 'B'\n     67: 104,  # 'C'\n     68: 94,  # 'D'\n     69: 98,  # 'E'\n     70: 101,  # 'F'\n     71: 116,  # 'G'\n     72: 102,  # 'H'\n     73: 111,  # 'I'\n     74: 187,  # 'J'\n     75: 117,  # 'K'\n     76: 92,  # 'L'\n     77: 88,  # 'M'\n     78: 113,  # 'N'\n     79: 85,  # 'O'\n     80: 79,  # 'P'\n     81: 118,  # 'Q'\n     82: 105,  # 'R'\n     83: 83,  # 'S'\n     84: 67,  # 'T'\n     85: 114,  # 'U'\n     86: 119,  # 'V'\n     87: 95,  # 'W'\n     88: 99,  # 'X'\n     89: 109,  # 'Y'\n     90: 188,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 72,  # 'a'\n     98: 70,  # 'b'\n     99: 80,  # 'c'\n     100: 81,  # 'd'\n     101: 60,  # 'e'\n     102: 96,  # 'f'\n     103: 93,  # 'g'\n     104: 89,  # 'h'\n     105: 68,  # 'i'\n     106: 120,  # 'j'\n     107: 97,  # 'k'\n     108: 77,  # 'l'\n     109: 86,  # 'm'\n     110: 69,  # 'n'\n     111: 55,  # 'o'\n     112: 78,  # 'p'\n     113: 115,  # 'q'\n     114: 65,  # 'r'\n     115: 66,  # 's'\n     116: 58,  # 't'\n     117: 76,  # 'u'\n     118: 106,  # 'v'\n     119: 103,  # 'w'\n     120: 87,  # 'x'\n     121: 107,  # 'y'\n     122: 112,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 255,  # '\\x80'\n     129: 255,  # '\\x81'\n     130: 255,  # '\\x82'\n     131: 255,  # '\\x83'\n     132: 255,  # '\\x84'\n     133: 255,  # '\\x85'\n     134: 255,  # '\\x86'\n     135: 255,  # '\\x87'\n     136: 255,  # '\\x88'\n     137: 255,  # '\\x89'\n     138: 255,  # '\\x8a'\n     139: 255,  # '\\x8b'\n     140: 255,  # '\\x8c'\n     141: 255,  # '\\x8d'\n     142: 255,  # '\\x8e'\n     143: 255,  # '\\x8f'\n     144: 255,  # '\\x90'\n     145: 255,  # '\\x91'\n     146: 255,  # '\\x92'\n     147: 255,  # '\\x93'\n     148: 255,  # '\\x94'\n     149: 255,  # '\\x95'\n     150: 255,  # '\\x96'\n     151: 255,  # '\\x97'\n     152: 255,  # '\\x98'\n     153: 255,  # '\\x99'\n     154: 255,  # '\\x9a'\n     155: 255,  # '\\x9b'\n     156: 255,  # '\\x9c'\n     157: 255,  # '\\x9d'\n     158: 255,  # '\\x9e'\n     159: 255,  # '\\x9f'\n     160: 253,  # '\\xa0'\n     161: 233,  # '‘'\n     162: 90,  # '’'\n     163: 253,  # '£'\n     164: 253,  # '€'\n     165: 253,  # '₯'\n     166: 253,  # '¦'\n     167: 253,  # '§'\n     168: 253,  # '¨'\n     169: 253,  # '©'\n     170: 253,  # 'ͺ'\n     171: 253,  # '«'\n     172: 253,  # '¬'\n     173: 74,  # '\\xad'\n     174: 253,  # None\n     175: 253,  # '―'\n     176: 253,  # '°'\n     177: 253,  # '±'\n     178: 253,  # '²'\n     179: 253,  # '³'\n     180: 247,  # '΄'\n     181: 248,  # '΅'\n     182: 61,  # 'Ά'\n     183: 36,  # '·'\n     184: 46,  # 'Έ'\n     185: 71,  # 'Ή'\n     186: 73,  # 'Ί'\n     187: 253,  # '»'\n     188: 54,  # 'Ό'\n     189: 253,  # '½'\n     190: 108,  # 'Ύ'\n     191: 123,  # 'Ώ'\n     192: 110,  # 'ΐ'\n     193: 31,  # 'Α'\n     194: 51,  # 'Β'\n     195: 43,  # 'Γ'\n     196: 41,  # 'Δ'\n     197: 34,  # 'Ε'\n     198: 91,  # 'Ζ'\n     199: 40,  # 'Η'\n     200: 52,  # 'Θ'\n     201: 47,  # 'Ι'\n     202: 44,  # 'Κ'\n     203: 53,  # 'Λ'\n     204: 38,  # 'Μ'\n     205: 49,  # 'Ν'\n     206: 59,  # 'Ξ'\n     207: 39,  # 'Ο'\n     208: 35,  # 'Π'\n     209: 48,  # 'Ρ'\n     210: 250,  # None\n     211: 37,  # 'Σ'\n     212: 33,  # 'Τ'\n     213: 45,  # 'Υ'\n     214: 56,  # 'Φ'\n     215: 50,  # 'Χ'\n     216: 84,  # 'Ψ'\n     217: 57,  # 'Ω'\n     218: 120,  # 'Ϊ'\n     219: 121,  # 'Ϋ'\n     220: 17,  # 'ά'\n     221: 18,  # 'έ'\n     222: 22,  # 'ή'\n     223: 15,  # 'ί'\n     224: 124,  # 'ΰ'\n     225: 1,  # 'α'\n     226: 29,  # 'β'\n     227: 20,  # 'γ'\n     228: 21,  # 'δ'\n     229: 3,  # 'ε'\n     230: 32,  # 'ζ'\n     231: 13,  # 'η'\n     232: 25,  # 'θ'\n     233: 5,  # 'ι'\n     234: 11,  # 'κ'\n     235: 16,  # 'λ'\n     236: 10,  # 'μ'\n     237: 6,  # 'ν'\n     238: 30,  # 'ξ'\n     239: 4,  # 'ο'\n     240: 9,  # 'π'\n     241: 8,  # 'ρ'\n     242: 14,  # 'ς'\n     243: 7,  # 'σ'\n     244: 2,  # 'τ'\n     245: 12,  # 'υ'\n     246: 28,  # 'φ'\n     247: 23,  # 'χ'\n     248: 42,  # 'ψ'\n     249: 24,  # 'ω'\n     250: 64,  # 'ϊ'\n     251: 75,  # 'ϋ'\n     252: 19,  # 'ό'\n     253: 26,  # 'ύ'\n     254: 27,  # 'ώ'\n     255: 253,  # None\n}\n\nISO_8859_7_GREEK_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-7',\n                                                language='Greek',\n                                                char_to_order_map=ISO_8859_7_GREEK_CHAR_TO_ORDER,\n                                                language_model=GREEK_LANG_MODEL,\n                                                typical_positive_ratio=0.982851,\n                                                keep_ascii_letters=False,\n                                                alphabet='ΆΈΉΊΌΎΏΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπρςστυφχψωόύώ')\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/langhebrewmodel.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nfrom chardet.sbcharsetprober import SingleByteCharSetModel\n\n\n# 3: Positive\n# 2: Likely\n# 1: Unlikely\n# 0: Negative\n\nHEBREW_LANG_MODEL = {\n    50: {  # 'a'\n        50: 0,  # 'a'\n        60: 1,  # 'c'\n        61: 1,  # 'd'\n        42: 1,  # 'e'\n        53: 1,  # 'i'\n        56: 2,  # 'l'\n        54: 2,  # 'n'\n        49: 0,  # 'o'\n        51: 2,  # 'r'\n        43: 1,  # 's'\n        44: 2,  # 't'\n        63: 1,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 1,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 1,  # 'ק'\n        7: 0,  # 'ר'\n        10: 1,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 1,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    60: {  # 'c'\n        50: 1,  # 'a'\n        60: 1,  # 'c'\n        61: 0,  # 'd'\n        42: 1,  # 'e'\n        53: 1,  # 'i'\n        56: 1,  # 'l'\n        54: 0,  # 'n'\n        49: 1,  # 'o'\n        51: 1,  # 'r'\n        43: 1,  # 's'\n        44: 2,  # 't'\n        63: 1,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 1,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 1,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    61: {  # 'd'\n        50: 1,  # 'a'\n        60: 0,  # 'c'\n        61: 1,  # 'd'\n        42: 1,  # 'e'\n        53: 1,  # 'i'\n        56: 1,  # 'l'\n        54: 1,  # 'n'\n        49: 2,  # 'o'\n        51: 1,  # 'r'\n        43: 1,  # 's'\n        44: 0,  # 't'\n        63: 1,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 1,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 1,  # '–'\n        52: 1,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    42: {  # 'e'\n        50: 1,  # 'a'\n        60: 1,  # 'c'\n        61: 2,  # 'd'\n        42: 1,  # 'e'\n        53: 1,  # 'i'\n        56: 2,  # 'l'\n        54: 2,  # 'n'\n        49: 1,  # 'o'\n        51: 2,  # 'r'\n        43: 2,  # 's'\n        44: 2,  # 't'\n        63: 1,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 1,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 1,  # '–'\n        52: 2,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    53: {  # 'i'\n        50: 1,  # 'a'\n        60: 2,  # 'c'\n        61: 1,  # 'd'\n        42: 1,  # 'e'\n        53: 0,  # 'i'\n        56: 1,  # 'l'\n        54: 2,  # 'n'\n        49: 2,  # 'o'\n        51: 1,  # 'r'\n        43: 2,  # 's'\n        44: 2,  # 't'\n        63: 1,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 1,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 1,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    56: {  # 'l'\n        50: 1,  # 'a'\n        60: 1,  # 'c'\n        61: 1,  # 'd'\n        42: 2,  # 'e'\n        53: 2,  # 'i'\n        56: 2,  # 'l'\n        54: 1,  # 'n'\n        49: 1,  # 'o'\n        51: 0,  # 'r'\n        43: 1,  # 's'\n        44: 1,  # 't'\n        63: 1,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 1,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    54: {  # 'n'\n        50: 1,  # 'a'\n        60: 1,  # 'c'\n        61: 1,  # 'd'\n        42: 1,  # 'e'\n        53: 1,  # 'i'\n        56: 1,  # 'l'\n        54: 1,  # 'n'\n        49: 1,  # 'o'\n        51: 0,  # 'r'\n        43: 1,  # 's'\n        44: 2,  # 't'\n        63: 1,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 1,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 2,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    49: {  # 'o'\n        50: 1,  # 'a'\n        60: 1,  # 'c'\n        61: 1,  # 'd'\n        42: 1,  # 'e'\n        53: 1,  # 'i'\n        56: 1,  # 'l'\n        54: 2,  # 'n'\n        49: 1,  # 'o'\n        51: 2,  # 'r'\n        43: 1,  # 's'\n        44: 1,  # 't'\n        63: 1,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 1,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    51: {  # 'r'\n        50: 2,  # 'a'\n        60: 1,  # 'c'\n        61: 1,  # 'd'\n        42: 2,  # 'e'\n        53: 1,  # 'i'\n        56: 1,  # 'l'\n        54: 1,  # 'n'\n        49: 2,  # 'o'\n        51: 1,  # 'r'\n        43: 1,  # 's'\n        44: 1,  # 't'\n        63: 1,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 2,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    43: {  # 's'\n        50: 1,  # 'a'\n        60: 1,  # 'c'\n        61: 0,  # 'd'\n        42: 2,  # 'e'\n        53: 1,  # 'i'\n        56: 1,  # 'l'\n        54: 1,  # 'n'\n        49: 1,  # 'o'\n        51: 1,  # 'r'\n        43: 1,  # 's'\n        44: 2,  # 't'\n        63: 1,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 1,  # '’'\n        47: 0,  # '“'\n        46: 2,  # '”'\n        58: 0,  # '†'\n        40: 2,  # '…'\n    },\n    44: {  # 't'\n        50: 1,  # 'a'\n        60: 1,  # 'c'\n        61: 0,  # 'd'\n        42: 2,  # 'e'\n        53: 2,  # 'i'\n        56: 1,  # 'l'\n        54: 0,  # 'n'\n        49: 1,  # 'o'\n        51: 1,  # 'r'\n        43: 1,  # 's'\n        44: 1,  # 't'\n        63: 1,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 2,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    63: {  # 'u'\n        50: 1,  # 'a'\n        60: 1,  # 'c'\n        61: 1,  # 'd'\n        42: 1,  # 'e'\n        53: 1,  # 'i'\n        56: 1,  # 'l'\n        54: 1,  # 'n'\n        49: 0,  # 'o'\n        51: 1,  # 'r'\n        43: 2,  # 's'\n        44: 1,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 1,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    34: {  # '\\xa0'\n        50: 1,  # 'a'\n        60: 0,  # 'c'\n        61: 1,  # 'd'\n        42: 0,  # 'e'\n        53: 1,  # 'i'\n        56: 0,  # 'l'\n        54: 1,  # 'n'\n        49: 1,  # 'o'\n        51: 0,  # 'r'\n        43: 1,  # 's'\n        44: 1,  # 't'\n        63: 0,  # 'u'\n        34: 2,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 1,  # 'ב'\n        20: 1,  # 'ג'\n        16: 1,  # 'ד'\n        3: 1,  # 'ה'\n        2: 1,  # 'ו'\n        24: 1,  # 'ז'\n        14: 1,  # 'ח'\n        22: 1,  # 'ט'\n        1: 2,  # 'י'\n        25: 0,  # 'ך'\n        15: 1,  # 'כ'\n        4: 1,  # 'ל'\n        11: 0,  # 'ם'\n        6: 2,  # 'מ'\n        23: 0,  # 'ן'\n        12: 1,  # 'נ'\n        19: 1,  # 'ס'\n        13: 1,  # 'ע'\n        26: 0,  # 'ף'\n        18: 1,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 1,  # 'צ'\n        17: 1,  # 'ק'\n        7: 1,  # 'ר'\n        10: 1,  # 'ש'\n        5: 1,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    55: {  # '´'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 1,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 1,  # 'ה'\n        2: 1,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 2,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 1,  # 'ל'\n        11: 0,  # 'ם'\n        6: 1,  # 'מ'\n        23: 1,  # 'ן'\n        12: 1,  # 'נ'\n        19: 1,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 1,  # 'ר'\n        10: 1,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    48: {  # '¼'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 1,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 1,  # 'כ'\n        4: 1,  # 'ל'\n        11: 0,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    39: {  # '½'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 1,  # 'כ'\n        4: 1,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 1,  # 'צ'\n        17: 1,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    57: {  # '¾'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    30: {  # 'ְ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 1,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 2,  # 'ב'\n        20: 2,  # 'ג'\n        16: 2,  # 'ד'\n        3: 2,  # 'ה'\n        2: 2,  # 'ו'\n        24: 2,  # 'ז'\n        14: 2,  # 'ח'\n        22: 2,  # 'ט'\n        1: 2,  # 'י'\n        25: 2,  # 'ך'\n        15: 2,  # 'כ'\n        4: 2,  # 'ל'\n        11: 1,  # 'ם'\n        6: 2,  # 'מ'\n        23: 0,  # 'ן'\n        12: 2,  # 'נ'\n        19: 2,  # 'ס'\n        13: 2,  # 'ע'\n        26: 0,  # 'ף'\n        18: 2,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 2,  # 'ק'\n        7: 2,  # 'ר'\n        10: 2,  # 'ש'\n        5: 2,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    59: {  # 'ֱ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 1,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 1,  # 'ב'\n        20: 1,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 1,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 1,  # 'י'\n        25: 0,  # 'ך'\n        15: 1,  # 'כ'\n        4: 2,  # 'ל'\n        11: 0,  # 'ם'\n        6: 2,  # 'מ'\n        23: 0,  # 'ן'\n        12: 1,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 1,  # 'ר'\n        10: 1,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    41: {  # 'ֲ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 2,  # 'ב'\n        20: 1,  # 'ג'\n        16: 2,  # 'ד'\n        3: 1,  # 'ה'\n        2: 1,  # 'ו'\n        24: 1,  # 'ז'\n        14: 1,  # 'ח'\n        22: 1,  # 'ט'\n        1: 1,  # 'י'\n        25: 1,  # 'ך'\n        15: 1,  # 'כ'\n        4: 2,  # 'ל'\n        11: 0,  # 'ם'\n        6: 2,  # 'מ'\n        23: 0,  # 'ן'\n        12: 2,  # 'נ'\n        19: 1,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 1,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 1,  # 'ק'\n        7: 2,  # 'ר'\n        10: 2,  # 'ש'\n        5: 1,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    33: {  # 'ִ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 1,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 1,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 1,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 1,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 1,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 2,  # 'ב'\n        20: 2,  # 'ג'\n        16: 2,  # 'ד'\n        3: 1,  # 'ה'\n        2: 1,  # 'ו'\n        24: 2,  # 'ז'\n        14: 1,  # 'ח'\n        22: 1,  # 'ט'\n        1: 3,  # 'י'\n        25: 1,  # 'ך'\n        15: 2,  # 'כ'\n        4: 2,  # 'ל'\n        11: 2,  # 'ם'\n        6: 2,  # 'מ'\n        23: 2,  # 'ן'\n        12: 2,  # 'נ'\n        19: 2,  # 'ס'\n        13: 1,  # 'ע'\n        26: 0,  # 'ף'\n        18: 2,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 2,  # 'ק'\n        7: 2,  # 'ר'\n        10: 2,  # 'ש'\n        5: 2,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    37: {  # 'ֵ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 1,  # 'ֶ'\n        31: 1,  # 'ַ'\n        29: 1,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 2,  # 'ב'\n        20: 1,  # 'ג'\n        16: 2,  # 'ד'\n        3: 2,  # 'ה'\n        2: 1,  # 'ו'\n        24: 1,  # 'ז'\n        14: 2,  # 'ח'\n        22: 1,  # 'ט'\n        1: 3,  # 'י'\n        25: 2,  # 'ך'\n        15: 1,  # 'כ'\n        4: 2,  # 'ל'\n        11: 2,  # 'ם'\n        6: 1,  # 'מ'\n        23: 2,  # 'ן'\n        12: 2,  # 'נ'\n        19: 1,  # 'ס'\n        13: 2,  # 'ע'\n        26: 1,  # 'ף'\n        18: 1,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 1,  # 'צ'\n        17: 1,  # 'ק'\n        7: 2,  # 'ר'\n        10: 2,  # 'ש'\n        5: 2,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    36: {  # 'ֶ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 1,  # 'ֶ'\n        31: 1,  # 'ַ'\n        29: 1,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 2,  # 'ב'\n        20: 1,  # 'ג'\n        16: 2,  # 'ד'\n        3: 2,  # 'ה'\n        2: 1,  # 'ו'\n        24: 1,  # 'ז'\n        14: 2,  # 'ח'\n        22: 1,  # 'ט'\n        1: 2,  # 'י'\n        25: 2,  # 'ך'\n        15: 1,  # 'כ'\n        4: 2,  # 'ל'\n        11: 2,  # 'ם'\n        6: 2,  # 'מ'\n        23: 2,  # 'ן'\n        12: 2,  # 'נ'\n        19: 2,  # 'ס'\n        13: 1,  # 'ע'\n        26: 1,  # 'ף'\n        18: 1,  # 'פ'\n        27: 2,  # 'ץ'\n        21: 1,  # 'צ'\n        17: 1,  # 'ק'\n        7: 2,  # 'ר'\n        10: 2,  # 'ש'\n        5: 2,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    31: {  # 'ַ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 1,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 1,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 2,  # 'ב'\n        20: 2,  # 'ג'\n        16: 2,  # 'ד'\n        3: 2,  # 'ה'\n        2: 1,  # 'ו'\n        24: 2,  # 'ז'\n        14: 2,  # 'ח'\n        22: 2,  # 'ט'\n        1: 3,  # 'י'\n        25: 1,  # 'ך'\n        15: 2,  # 'כ'\n        4: 2,  # 'ל'\n        11: 2,  # 'ם'\n        6: 2,  # 'מ'\n        23: 2,  # 'ן'\n        12: 2,  # 'נ'\n        19: 2,  # 'ס'\n        13: 2,  # 'ע'\n        26: 2,  # 'ף'\n        18: 2,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 2,  # 'ק'\n        7: 2,  # 'ר'\n        10: 2,  # 'ש'\n        5: 2,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    29: {  # 'ָ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 1,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 1,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 2,  # 'ב'\n        20: 2,  # 'ג'\n        16: 2,  # 'ד'\n        3: 3,  # 'ה'\n        2: 2,  # 'ו'\n        24: 2,  # 'ז'\n        14: 2,  # 'ח'\n        22: 1,  # 'ט'\n        1: 2,  # 'י'\n        25: 2,  # 'ך'\n        15: 2,  # 'כ'\n        4: 2,  # 'ל'\n        11: 2,  # 'ם'\n        6: 2,  # 'מ'\n        23: 2,  # 'ן'\n        12: 2,  # 'נ'\n        19: 1,  # 'ס'\n        13: 2,  # 'ע'\n        26: 1,  # 'ף'\n        18: 2,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 2,  # 'ק'\n        7: 2,  # 'ר'\n        10: 2,  # 'ש'\n        5: 2,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    35: {  # 'ֹ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 2,  # 'ב'\n        20: 1,  # 'ג'\n        16: 2,  # 'ד'\n        3: 2,  # 'ה'\n        2: 1,  # 'ו'\n        24: 1,  # 'ז'\n        14: 1,  # 'ח'\n        22: 1,  # 'ט'\n        1: 1,  # 'י'\n        25: 1,  # 'ך'\n        15: 2,  # 'כ'\n        4: 2,  # 'ל'\n        11: 2,  # 'ם'\n        6: 2,  # 'מ'\n        23: 2,  # 'ן'\n        12: 2,  # 'נ'\n        19: 2,  # 'ס'\n        13: 2,  # 'ע'\n        26: 1,  # 'ף'\n        18: 2,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 2,  # 'ק'\n        7: 2,  # 'ר'\n        10: 2,  # 'ש'\n        5: 2,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    62: {  # 'ֻ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 1,  # 'ב'\n        20: 1,  # 'ג'\n        16: 1,  # 'ד'\n        3: 1,  # 'ה'\n        2: 1,  # 'ו'\n        24: 1,  # 'ז'\n        14: 1,  # 'ח'\n        22: 0,  # 'ט'\n        1: 1,  # 'י'\n        25: 0,  # 'ך'\n        15: 1,  # 'כ'\n        4: 2,  # 'ל'\n        11: 1,  # 'ם'\n        6: 1,  # 'מ'\n        23: 1,  # 'ן'\n        12: 1,  # 'נ'\n        19: 1,  # 'ס'\n        13: 1,  # 'ע'\n        26: 0,  # 'ף'\n        18: 1,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 1,  # 'צ'\n        17: 1,  # 'ק'\n        7: 1,  # 'ר'\n        10: 1,  # 'ש'\n        5: 1,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    28: {  # 'ּ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 3,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 1,  # 'ֲ'\n        33: 3,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 3,  # 'ַ'\n        29: 3,  # 'ָ'\n        35: 2,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 2,  # 'ׁ'\n        45: 1,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 2,  # 'ב'\n        20: 1,  # 'ג'\n        16: 2,  # 'ד'\n        3: 1,  # 'ה'\n        2: 2,  # 'ו'\n        24: 1,  # 'ז'\n        14: 1,  # 'ח'\n        22: 1,  # 'ט'\n        1: 2,  # 'י'\n        25: 2,  # 'ך'\n        15: 2,  # 'כ'\n        4: 2,  # 'ל'\n        11: 1,  # 'ם'\n        6: 2,  # 'מ'\n        23: 1,  # 'ן'\n        12: 2,  # 'נ'\n        19: 1,  # 'ס'\n        13: 2,  # 'ע'\n        26: 1,  # 'ף'\n        18: 1,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 1,  # 'צ'\n        17: 1,  # 'ק'\n        7: 2,  # 'ר'\n        10: 2,  # 'ש'\n        5: 2,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    38: {  # 'ׁ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 2,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 1,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 1,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    45: {  # 'ׂ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 1,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 1,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 0,  # 'ב'\n        20: 1,  # 'ג'\n        16: 0,  # 'ד'\n        3: 1,  # 'ה'\n        2: 2,  # 'ו'\n        24: 0,  # 'ז'\n        14: 1,  # 'ח'\n        22: 0,  # 'ט'\n        1: 1,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 1,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 1,  # 'נ'\n        19: 0,  # 'ס'\n        13: 1,  # 'ע'\n        26: 0,  # 'ף'\n        18: 1,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 1,  # 'ר'\n        10: 0,  # 'ש'\n        5: 1,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    9: {  # 'א'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 1,  # '´'\n        48: 1,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 2,  # 'ֱ'\n        41: 2,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 2,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 3,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 3,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 2,  # 'ע'\n        26: 3,  # 'ף'\n        18: 3,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    8: {  # 'ב'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 1,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 1,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 2,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 3,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 2,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 2,  # 'ם'\n        6: 3,  # 'מ'\n        23: 3,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 3,  # 'ע'\n        26: 1,  # 'ף'\n        18: 3,  # 'פ'\n        27: 2,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 1,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    20: {  # 'ג'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 2,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 1,  # 'ִ'\n        37: 1,  # 'ֵ'\n        36: 1,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 3,  # 'ב'\n        20: 2,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 2,  # 'ח'\n        22: 2,  # 'ט'\n        1: 3,  # 'י'\n        25: 1,  # 'ך'\n        15: 1,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 3,  # 'ן'\n        12: 3,  # 'נ'\n        19: 2,  # 'ס'\n        13: 3,  # 'ע'\n        26: 2,  # 'ף'\n        18: 2,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 1,  # 'צ'\n        17: 1,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 1,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    16: {  # 'ד'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 2,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 1,  # 'ז'\n        14: 2,  # 'ח'\n        22: 2,  # 'ט'\n        1: 3,  # 'י'\n        25: 2,  # 'ך'\n        15: 2,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 2,  # 'ן'\n        12: 3,  # 'נ'\n        19: 2,  # 'ס'\n        13: 3,  # 'ע'\n        26: 2,  # 'ף'\n        18: 3,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    3: {  # 'ה'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 1,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 0,  # '´'\n        48: 1,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 1,  # 'ְ'\n        59: 1,  # 'ֱ'\n        41: 2,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 3,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 1,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 3,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 3,  # 'ע'\n        26: 0,  # 'ף'\n        18: 3,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 1,  # '–'\n        52: 1,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 2,  # '…'\n    },\n    2: {  # 'ו'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 1,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 1,  # '´'\n        48: 1,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 1,  # 'ֵ'\n        36: 1,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 3,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 3,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 3,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 3,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 3,  # 'ע'\n        26: 3,  # 'ף'\n        18: 3,  # 'פ'\n        27: 3,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 1,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 2,  # '…'\n    },\n    24: {  # 'ז'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 1,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 1,  # 'ֲ'\n        33: 1,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 2,  # 'ב'\n        20: 2,  # 'ג'\n        16: 2,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 2,  # 'ז'\n        14: 2,  # 'ח'\n        22: 1,  # 'ט'\n        1: 3,  # 'י'\n        25: 1,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 2,  # 'ם'\n        6: 3,  # 'מ'\n        23: 2,  # 'ן'\n        12: 2,  # 'נ'\n        19: 1,  # 'ס'\n        13: 2,  # 'ע'\n        26: 1,  # 'ף'\n        18: 1,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 1,  # 'ש'\n        5: 2,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    14: {  # 'ח'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 1,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 1,  # 'ֱ'\n        41: 2,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 2,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 3,  # 'ב'\n        20: 2,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 2,  # 'ח'\n        22: 2,  # 'ט'\n        1: 3,  # 'י'\n        25: 1,  # 'ך'\n        15: 2,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 2,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 1,  # 'ע'\n        26: 2,  # 'ף'\n        18: 2,  # 'פ'\n        27: 2,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 1,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    22: {  # 'ט'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 1,  # 'ֵ'\n        36: 1,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 1,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 1,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 1,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 2,  # 'ז'\n        14: 3,  # 'ח'\n        22: 2,  # 'ט'\n        1: 3,  # 'י'\n        25: 1,  # 'ך'\n        15: 2,  # 'כ'\n        4: 3,  # 'ל'\n        11: 2,  # 'ם'\n        6: 2,  # 'מ'\n        23: 2,  # 'ן'\n        12: 3,  # 'נ'\n        19: 2,  # 'ס'\n        13: 3,  # 'ע'\n        26: 2,  # 'ף'\n        18: 3,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 2,  # 'ק'\n        7: 3,  # 'ר'\n        10: 2,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    1: {  # 'י'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 1,  # '´'\n        48: 1,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 1,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 2,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 3,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 3,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 3,  # 'ע'\n        26: 3,  # 'ף'\n        18: 3,  # 'פ'\n        27: 3,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 1,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 2,  # '…'\n    },\n    25: {  # 'ך'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 1,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 1,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 1,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 1,  # 'ל'\n        11: 0,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 1,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    15: {  # 'כ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 3,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 2,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 3,  # 'ח'\n        22: 2,  # 'ט'\n        1: 3,  # 'י'\n        25: 3,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 3,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 2,  # 'ע'\n        26: 3,  # 'ף'\n        18: 3,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 2,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    4: {  # 'ל'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 1,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 3,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 2,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 3,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 2,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 3,  # 'ע'\n        26: 2,  # 'ף'\n        18: 3,  # 'פ'\n        27: 2,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 1,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    11: {  # 'ם'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 1,  # 'ב'\n        20: 1,  # 'ג'\n        16: 0,  # 'ד'\n        3: 1,  # 'ה'\n        2: 1,  # 'ו'\n        24: 1,  # 'ז'\n        14: 1,  # 'ח'\n        22: 0,  # 'ט'\n        1: 1,  # 'י'\n        25: 0,  # 'ך'\n        15: 1,  # 'כ'\n        4: 1,  # 'ל'\n        11: 1,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 1,  # 'נ'\n        19: 0,  # 'ס'\n        13: 1,  # 'ע'\n        26: 0,  # 'ף'\n        18: 1,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 1,  # 'צ'\n        17: 1,  # 'ק'\n        7: 1,  # 'ר'\n        10: 1,  # 'ש'\n        5: 1,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 2,  # '…'\n    },\n    6: {  # 'מ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 1,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 2,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 2,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 3,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 3,  # 'ע'\n        26: 0,  # 'ף'\n        18: 3,  # 'פ'\n        27: 2,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    23: {  # 'ן'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 0,  # '´'\n        48: 1,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 1,  # 'ב'\n        20: 1,  # 'ג'\n        16: 1,  # 'ד'\n        3: 1,  # 'ה'\n        2: 1,  # 'ו'\n        24: 0,  # 'ז'\n        14: 1,  # 'ח'\n        22: 1,  # 'ט'\n        1: 1,  # 'י'\n        25: 0,  # 'ך'\n        15: 1,  # 'כ'\n        4: 1,  # 'ל'\n        11: 1,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 1,  # 'נ'\n        19: 1,  # 'ס'\n        13: 1,  # 'ע'\n        26: 1,  # 'ף'\n        18: 1,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 1,  # 'ק'\n        7: 1,  # 'ר'\n        10: 1,  # 'ש'\n        5: 1,  # 'ת'\n        32: 1,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 2,  # '…'\n    },\n    12: {  # 'נ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 2,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 3,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 3,  # 'ע'\n        26: 2,  # 'ף'\n        18: 3,  # 'פ'\n        27: 2,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    19: {  # 'ס'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 1,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 1,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 1,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 2,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 1,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 2,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 2,  # 'ם'\n        6: 3,  # 'מ'\n        23: 2,  # 'ן'\n        12: 3,  # 'נ'\n        19: 2,  # 'ס'\n        13: 3,  # 'ע'\n        26: 3,  # 'ף'\n        18: 3,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 1,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    13: {  # 'ע'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 1,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 1,  # 'ְ'\n        59: 1,  # 'ֱ'\n        41: 2,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 2,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 1,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 2,  # 'ך'\n        15: 2,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 2,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 2,  # 'ע'\n        26: 1,  # 'ף'\n        18: 2,  # 'פ'\n        27: 2,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    26: {  # 'ף'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 1,  # 'ו'\n        24: 0,  # 'ז'\n        14: 1,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 1,  # 'כ'\n        4: 1,  # 'ל'\n        11: 0,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 1,  # 'ס'\n        13: 0,  # 'ע'\n        26: 1,  # 'ף'\n        18: 1,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 1,  # 'ק'\n        7: 1,  # 'ר'\n        10: 1,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    18: {  # 'פ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 1,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 1,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 1,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 2,  # 'ב'\n        20: 3,  # 'ג'\n        16: 2,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 2,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 2,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 2,  # 'ם'\n        6: 2,  # 'מ'\n        23: 3,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 3,  # 'ע'\n        26: 2,  # 'ף'\n        18: 2,  # 'פ'\n        27: 2,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    27: {  # 'ץ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 1,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 1,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 1,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 1,  # 'ר'\n        10: 0,  # 'ש'\n        5: 1,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    21: {  # 'צ'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 1,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 1,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 2,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 1,  # 'ז'\n        14: 3,  # 'ח'\n        22: 2,  # 'ט'\n        1: 3,  # 'י'\n        25: 1,  # 'ך'\n        15: 1,  # 'כ'\n        4: 3,  # 'ל'\n        11: 2,  # 'ם'\n        6: 3,  # 'מ'\n        23: 2,  # 'ן'\n        12: 3,  # 'נ'\n        19: 1,  # 'ס'\n        13: 3,  # 'ע'\n        26: 2,  # 'ף'\n        18: 3,  # 'פ'\n        27: 2,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 0,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    17: {  # 'ק'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 1,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 1,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 2,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 2,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 2,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 1,  # 'ך'\n        15: 1,  # 'כ'\n        4: 3,  # 'ל'\n        11: 2,  # 'ם'\n        6: 3,  # 'מ'\n        23: 2,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 3,  # 'ע'\n        26: 2,  # 'ף'\n        18: 3,  # 'פ'\n        27: 2,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 2,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 1,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    7: {  # 'ר'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 2,  # '´'\n        48: 1,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 1,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 2,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 3,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 3,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 3,  # 'ן'\n        12: 3,  # 'נ'\n        19: 3,  # 'ס'\n        13: 3,  # 'ע'\n        26: 2,  # 'ף'\n        18: 3,  # 'פ'\n        27: 3,  # 'ץ'\n        21: 3,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 2,  # '…'\n    },\n    10: {  # 'ש'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 1,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 1,  # 'ִ'\n        37: 1,  # 'ֵ'\n        36: 1,  # 'ֶ'\n        31: 1,  # 'ַ'\n        29: 1,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 3,  # 'ׁ'\n        45: 2,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 3,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 2,  # 'ז'\n        14: 3,  # 'ח'\n        22: 3,  # 'ט'\n        1: 3,  # 'י'\n        25: 3,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 2,  # 'ן'\n        12: 3,  # 'נ'\n        19: 2,  # 'ס'\n        13: 3,  # 'ע'\n        26: 2,  # 'ף'\n        18: 3,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 1,  # '…'\n    },\n    5: {  # 'ת'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 1,  # '\\xa0'\n        55: 0,  # '´'\n        48: 1,  # '¼'\n        39: 1,  # '½'\n        57: 0,  # '¾'\n        30: 2,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 2,  # 'ִ'\n        37: 2,  # 'ֵ'\n        36: 2,  # 'ֶ'\n        31: 2,  # 'ַ'\n        29: 2,  # 'ָ'\n        35: 1,  # 'ֹ'\n        62: 1,  # 'ֻ'\n        28: 2,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 3,  # 'א'\n        8: 3,  # 'ב'\n        20: 3,  # 'ג'\n        16: 2,  # 'ד'\n        3: 3,  # 'ה'\n        2: 3,  # 'ו'\n        24: 2,  # 'ז'\n        14: 3,  # 'ח'\n        22: 2,  # 'ט'\n        1: 3,  # 'י'\n        25: 2,  # 'ך'\n        15: 3,  # 'כ'\n        4: 3,  # 'ל'\n        11: 3,  # 'ם'\n        6: 3,  # 'מ'\n        23: 3,  # 'ן'\n        12: 3,  # 'נ'\n        19: 2,  # 'ס'\n        13: 3,  # 'ע'\n        26: 2,  # 'ף'\n        18: 3,  # 'פ'\n        27: 1,  # 'ץ'\n        21: 2,  # 'צ'\n        17: 3,  # 'ק'\n        7: 3,  # 'ר'\n        10: 3,  # 'ש'\n        5: 3,  # 'ת'\n        32: 1,  # '–'\n        52: 1,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 2,  # '…'\n    },\n    32: {  # '–'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 1,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 1,  # 'ב'\n        20: 1,  # 'ג'\n        16: 1,  # 'ד'\n        3: 1,  # 'ה'\n        2: 1,  # 'ו'\n        24: 0,  # 'ז'\n        14: 1,  # 'ח'\n        22: 0,  # 'ט'\n        1: 1,  # 'י'\n        25: 0,  # 'ך'\n        15: 1,  # 'כ'\n        4: 1,  # 'ל'\n        11: 0,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 1,  # 'ס'\n        13: 1,  # 'ע'\n        26: 0,  # 'ף'\n        18: 1,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 1,  # 'צ'\n        17: 0,  # 'ק'\n        7: 1,  # 'ר'\n        10: 1,  # 'ש'\n        5: 1,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    52: {  # '’'\n        50: 1,  # 'a'\n        60: 0,  # 'c'\n        61: 1,  # 'd'\n        42: 1,  # 'e'\n        53: 1,  # 'i'\n        56: 1,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 1,  # 'r'\n        43: 2,  # 's'\n        44: 2,  # 't'\n        63: 1,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 1,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 1,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    47: {  # '“'\n        50: 1,  # 'a'\n        60: 1,  # 'c'\n        61: 1,  # 'd'\n        42: 1,  # 'e'\n        53: 1,  # 'i'\n        56: 1,  # 'l'\n        54: 1,  # 'n'\n        49: 1,  # 'o'\n        51: 1,  # 'r'\n        43: 1,  # 's'\n        44: 1,  # 't'\n        63: 1,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 2,  # 'א'\n        8: 1,  # 'ב'\n        20: 1,  # 'ג'\n        16: 1,  # 'ד'\n        3: 1,  # 'ה'\n        2: 1,  # 'ו'\n        24: 1,  # 'ז'\n        14: 1,  # 'ח'\n        22: 1,  # 'ט'\n        1: 1,  # 'י'\n        25: 0,  # 'ך'\n        15: 1,  # 'כ'\n        4: 1,  # 'ל'\n        11: 0,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 1,  # 'נ'\n        19: 1,  # 'ס'\n        13: 1,  # 'ע'\n        26: 0,  # 'ף'\n        18: 1,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 1,  # 'צ'\n        17: 1,  # 'ק'\n        7: 1,  # 'ר'\n        10: 1,  # 'ש'\n        5: 1,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    46: {  # '”'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 1,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 1,  # 'ב'\n        20: 1,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 1,  # 'י'\n        25: 0,  # 'ך'\n        15: 1,  # 'כ'\n        4: 1,  # 'ל'\n        11: 0,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 1,  # 'צ'\n        17: 0,  # 'ק'\n        7: 1,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 0,  # '†'\n        40: 0,  # '…'\n    },\n    58: {  # '†'\n        50: 0,  # 'a'\n        60: 0,  # 'c'\n        61: 0,  # 'd'\n        42: 0,  # 'e'\n        53: 0,  # 'i'\n        56: 0,  # 'l'\n        54: 0,  # 'n'\n        49: 0,  # 'o'\n        51: 0,  # 'r'\n        43: 0,  # 's'\n        44: 0,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 0,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 0,  # 'ה'\n        2: 0,  # 'ו'\n        24: 0,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 0,  # 'י'\n        25: 0,  # 'ך'\n        15: 0,  # 'כ'\n        4: 0,  # 'ל'\n        11: 0,  # 'ם'\n        6: 0,  # 'מ'\n        23: 0,  # 'ן'\n        12: 0,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 0,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 0,  # 'ר'\n        10: 0,  # 'ש'\n        5: 0,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 0,  # '”'\n        58: 2,  # '†'\n        40: 0,  # '…'\n    },\n    40: {  # '…'\n        50: 1,  # 'a'\n        60: 1,  # 'c'\n        61: 1,  # 'd'\n        42: 1,  # 'e'\n        53: 1,  # 'i'\n        56: 0,  # 'l'\n        54: 1,  # 'n'\n        49: 0,  # 'o'\n        51: 1,  # 'r'\n        43: 1,  # 's'\n        44: 1,  # 't'\n        63: 0,  # 'u'\n        34: 0,  # '\\xa0'\n        55: 0,  # '´'\n        48: 0,  # '¼'\n        39: 0,  # '½'\n        57: 0,  # '¾'\n        30: 0,  # 'ְ'\n        59: 0,  # 'ֱ'\n        41: 0,  # 'ֲ'\n        33: 0,  # 'ִ'\n        37: 0,  # 'ֵ'\n        36: 0,  # 'ֶ'\n        31: 0,  # 'ַ'\n        29: 0,  # 'ָ'\n        35: 0,  # 'ֹ'\n        62: 0,  # 'ֻ'\n        28: 0,  # 'ּ'\n        38: 0,  # 'ׁ'\n        45: 0,  # 'ׂ'\n        9: 1,  # 'א'\n        8: 0,  # 'ב'\n        20: 0,  # 'ג'\n        16: 0,  # 'ד'\n        3: 1,  # 'ה'\n        2: 1,  # 'ו'\n        24: 1,  # 'ז'\n        14: 0,  # 'ח'\n        22: 0,  # 'ט'\n        1: 1,  # 'י'\n        25: 0,  # 'ך'\n        15: 1,  # 'כ'\n        4: 1,  # 'ל'\n        11: 0,  # 'ם'\n        6: 1,  # 'מ'\n        23: 0,  # 'ן'\n        12: 1,  # 'נ'\n        19: 0,  # 'ס'\n        13: 0,  # 'ע'\n        26: 0,  # 'ף'\n        18: 1,  # 'פ'\n        27: 0,  # 'ץ'\n        21: 0,  # 'צ'\n        17: 0,  # 'ק'\n        7: 1,  # 'ר'\n        10: 1,  # 'ש'\n        5: 1,  # 'ת'\n        32: 0,  # '–'\n        52: 0,  # '’'\n        47: 0,  # '“'\n        46: 1,  # '”'\n        58: 0,  # '†'\n        40: 2,  # '…'\n    },\n}\n\n# 255: Undefined characters that did not exist in training text\n# 254: Carriage/Return\n# 253: symbol (punctuation) that does not belong to word\n# 252: 0 - 9\n# 251: Control characters\n\n# Character Mapping Table(s):\nWINDOWS_1255_HEBREW_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 69,  # 'A'\n     66: 91,  # 'B'\n     67: 79,  # 'C'\n     68: 80,  # 'D'\n     69: 92,  # 'E'\n     70: 89,  # 'F'\n     71: 97,  # 'G'\n     72: 90,  # 'H'\n     73: 68,  # 'I'\n     74: 111,  # 'J'\n     75: 112,  # 'K'\n     76: 82,  # 'L'\n     77: 73,  # 'M'\n     78: 95,  # 'N'\n     79: 85,  # 'O'\n     80: 78,  # 'P'\n     81: 121,  # 'Q'\n     82: 86,  # 'R'\n     83: 71,  # 'S'\n     84: 67,  # 'T'\n     85: 102,  # 'U'\n     86: 107,  # 'V'\n     87: 84,  # 'W'\n     88: 114,  # 'X'\n     89: 103,  # 'Y'\n     90: 115,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 50,  # 'a'\n     98: 74,  # 'b'\n     99: 60,  # 'c'\n     100: 61,  # 'd'\n     101: 42,  # 'e'\n     102: 76,  # 'f'\n     103: 70,  # 'g'\n     104: 64,  # 'h'\n     105: 53,  # 'i'\n     106: 105,  # 'j'\n     107: 93,  # 'k'\n     108: 56,  # 'l'\n     109: 65,  # 'm'\n     110: 54,  # 'n'\n     111: 49,  # 'o'\n     112: 66,  # 'p'\n     113: 110,  # 'q'\n     114: 51,  # 'r'\n     115: 43,  # 's'\n     116: 44,  # 't'\n     117: 63,  # 'u'\n     118: 81,  # 'v'\n     119: 77,  # 'w'\n     120: 98,  # 'x'\n     121: 75,  # 'y'\n     122: 108,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 124,  # '€'\n     129: 202,  # None\n     130: 203,  # '‚'\n     131: 204,  # 'ƒ'\n     132: 205,  # '„'\n     133: 40,  # '…'\n     134: 58,  # '†'\n     135: 206,  # '‡'\n     136: 207,  # 'ˆ'\n     137: 208,  # '‰'\n     138: 209,  # None\n     139: 210,  # '‹'\n     140: 211,  # None\n     141: 212,  # None\n     142: 213,  # None\n     143: 214,  # None\n     144: 215,  # None\n     145: 83,  # '‘'\n     146: 52,  # '’'\n     147: 47,  # '“'\n     148: 46,  # '”'\n     149: 72,  # '•'\n     150: 32,  # '–'\n     151: 94,  # '—'\n     152: 216,  # '˜'\n     153: 113,  # '™'\n     154: 217,  # None\n     155: 109,  # '›'\n     156: 218,  # None\n     157: 219,  # None\n     158: 220,  # None\n     159: 221,  # None\n     160: 34,  # '\\xa0'\n     161: 116,  # '¡'\n     162: 222,  # '¢'\n     163: 118,  # '£'\n     164: 100,  # '₪'\n     165: 223,  # '¥'\n     166: 224,  # '¦'\n     167: 117,  # '§'\n     168: 119,  # '¨'\n     169: 104,  # '©'\n     170: 125,  # '×'\n     171: 225,  # '«'\n     172: 226,  # '¬'\n     173: 87,  # '\\xad'\n     174: 99,  # '®'\n     175: 227,  # '¯'\n     176: 106,  # '°'\n     177: 122,  # '±'\n     178: 123,  # '²'\n     179: 228,  # '³'\n     180: 55,  # '´'\n     181: 229,  # 'µ'\n     182: 230,  # '¶'\n     183: 101,  # '·'\n     184: 231,  # '¸'\n     185: 232,  # '¹'\n     186: 120,  # '÷'\n     187: 233,  # '»'\n     188: 48,  # '¼'\n     189: 39,  # '½'\n     190: 57,  # '¾'\n     191: 234,  # '¿'\n     192: 30,  # 'ְ'\n     193: 59,  # 'ֱ'\n     194: 41,  # 'ֲ'\n     195: 88,  # 'ֳ'\n     196: 33,  # 'ִ'\n     197: 37,  # 'ֵ'\n     198: 36,  # 'ֶ'\n     199: 31,  # 'ַ'\n     200: 29,  # 'ָ'\n     201: 35,  # 'ֹ'\n     202: 235,  # None\n     203: 62,  # 'ֻ'\n     204: 28,  # 'ּ'\n     205: 236,  # 'ֽ'\n     206: 126,  # '־'\n     207: 237,  # 'ֿ'\n     208: 238,  # '׀'\n     209: 38,  # 'ׁ'\n     210: 45,  # 'ׂ'\n     211: 239,  # '׃'\n     212: 240,  # 'װ'\n     213: 241,  # 'ױ'\n     214: 242,  # 'ײ'\n     215: 243,  # '׳'\n     216: 127,  # '״'\n     217: 244,  # None\n     218: 245,  # None\n     219: 246,  # None\n     220: 247,  # None\n     221: 248,  # None\n     222: 249,  # None\n     223: 250,  # None\n     224: 9,  # 'א'\n     225: 8,  # 'ב'\n     226: 20,  # 'ג'\n     227: 16,  # 'ד'\n     228: 3,  # 'ה'\n     229: 2,  # 'ו'\n     230: 24,  # 'ז'\n     231: 14,  # 'ח'\n     232: 22,  # 'ט'\n     233: 1,  # 'י'\n     234: 25,  # 'ך'\n     235: 15,  # 'כ'\n     236: 4,  # 'ל'\n     237: 11,  # 'ם'\n     238: 6,  # 'מ'\n     239: 23,  # 'ן'\n     240: 12,  # 'נ'\n     241: 19,  # 'ס'\n     242: 13,  # 'ע'\n     243: 26,  # 'ף'\n     244: 18,  # 'פ'\n     245: 27,  # 'ץ'\n     246: 21,  # 'צ'\n     247: 17,  # 'ק'\n     248: 7,  # 'ר'\n     249: 10,  # 'ש'\n     250: 5,  # 'ת'\n     251: 251,  # None\n     252: 252,  # None\n     253: 128,  # '\\u200e'\n     254: 96,  # '\\u200f'\n     255: 253,  # None\n}\n\nWINDOWS_1255_HEBREW_MODEL = SingleByteCharSetModel(charset_name='windows-1255',\n                                                   language='Hebrew',\n                                                   char_to_order_map=WINDOWS_1255_HEBREW_CHAR_TO_ORDER,\n                                                   language_model=HEBREW_LANG_MODEL,\n                                                   typical_positive_ratio=0.984004,\n                                                   keep_ascii_letters=False,\n                                                   alphabet='אבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ')\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/langhungarianmodel.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nfrom chardet.sbcharsetprober import SingleByteCharSetModel\n\n\n# 3: Positive\n# 2: Likely\n# 1: Unlikely\n# 0: Negative\n\nHUNGARIAN_LANG_MODEL = {\n    28: {  # 'A'\n        28: 0,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 2,  # 'D'\n        32: 1,  # 'E'\n        50: 1,  # 'F'\n        49: 2,  # 'G'\n        38: 1,  # 'H'\n        39: 2,  # 'I'\n        53: 1,  # 'J'\n        36: 2,  # 'K'\n        41: 2,  # 'L'\n        34: 1,  # 'M'\n        35: 2,  # 'N'\n        47: 1,  # 'O'\n        46: 2,  # 'P'\n        43: 2,  # 'R'\n        33: 2,  # 'S'\n        37: 2,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 1,  # 'Y'\n        52: 2,  # 'Z'\n        2: 0,  # 'a'\n        18: 1,  # 'b'\n        26: 1,  # 'c'\n        17: 2,  # 'd'\n        1: 1,  # 'e'\n        27: 1,  # 'f'\n        12: 1,  # 'g'\n        20: 1,  # 'h'\n        9: 1,  # 'i'\n        22: 1,  # 'j'\n        7: 2,  # 'k'\n        6: 2,  # 'l'\n        13: 2,  # 'm'\n        4: 2,  # 'n'\n        8: 0,  # 'o'\n        23: 2,  # 'p'\n        10: 2,  # 'r'\n        5: 1,  # 's'\n        3: 1,  # 't'\n        21: 1,  # 'u'\n        19: 1,  # 'v'\n        62: 1,  # 'x'\n        16: 0,  # 'y'\n        11: 3,  # 'z'\n        51: 1,  # 'Á'\n        44: 0,  # 'É'\n        61: 1,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    40: {  # 'B'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 2,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 0,  # 'M'\n        35: 1,  # 'N'\n        47: 2,  # 'O'\n        46: 0,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 2,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 3,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 2,  # 'i'\n        22: 1,  # 'j'\n        7: 0,  # 'k'\n        6: 1,  # 'l'\n        13: 0,  # 'm'\n        4: 0,  # 'n'\n        8: 2,  # 'o'\n        23: 1,  # 'p'\n        10: 2,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 3,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 0,  # 'z'\n        51: 1,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 2,  # 'á'\n        15: 2,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 1,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    54: {  # 'C'\n        28: 1,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 1,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 1,  # 'H'\n        39: 2,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 0,  # 'N'\n        47: 1,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 2,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 0,  # 'V'\n        55: 1,  # 'Y'\n        52: 1,  # 'Z'\n        2: 2,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 1,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 1,  # 'h'\n        9: 1,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 1,  # 'l'\n        13: 0,  # 'm'\n        4: 0,  # 'n'\n        8: 2,  # 'o'\n        23: 0,  # 'p'\n        10: 1,  # 'r'\n        5: 3,  # 's'\n        3: 0,  # 't'\n        21: 1,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 1,  # 'z'\n        51: 1,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 1,  # 'á'\n        15: 1,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    45: {  # 'D'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 0,  # 'C'\n        45: 1,  # 'D'\n        32: 2,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 2,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 0,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 2,  # 'O'\n        46: 0,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 1,  # 'Y'\n        52: 1,  # 'Z'\n        2: 2,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 3,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 1,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 0,  # 'n'\n        8: 1,  # 'o'\n        23: 0,  # 'p'\n        10: 2,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 2,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 1,  # 'z'\n        51: 1,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 1,  # 'á'\n        15: 1,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 1,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    32: {  # 'E'\n        28: 1,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 1,  # 'E'\n        50: 1,  # 'F'\n        49: 2,  # 'G'\n        38: 1,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 2,  # 'K'\n        41: 2,  # 'L'\n        34: 2,  # 'M'\n        35: 2,  # 'N'\n        47: 1,  # 'O'\n        46: 1,  # 'P'\n        43: 2,  # 'R'\n        33: 2,  # 'S'\n        37: 2,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 1,  # 'Y'\n        52: 1,  # 'Z'\n        2: 1,  # 'a'\n        18: 1,  # 'b'\n        26: 1,  # 'c'\n        17: 2,  # 'd'\n        1: 1,  # 'e'\n        27: 1,  # 'f'\n        12: 3,  # 'g'\n        20: 1,  # 'h'\n        9: 1,  # 'i'\n        22: 1,  # 'j'\n        7: 1,  # 'k'\n        6: 2,  # 'l'\n        13: 2,  # 'm'\n        4: 2,  # 'n'\n        8: 0,  # 'o'\n        23: 1,  # 'p'\n        10: 2,  # 'r'\n        5: 2,  # 's'\n        3: 1,  # 't'\n        21: 2,  # 'u'\n        19: 1,  # 'v'\n        62: 1,  # 'x'\n        16: 0,  # 'y'\n        11: 3,  # 'z'\n        51: 1,  # 'Á'\n        44: 1,  # 'É'\n        61: 0,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 1,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    50: {  # 'F'\n        28: 1,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 1,  # 'E'\n        50: 1,  # 'F'\n        49: 0,  # 'G'\n        38: 1,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 1,  # 'O'\n        46: 0,  # 'P'\n        43: 1,  # 'R'\n        33: 0,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 0,  # 'V'\n        55: 1,  # 'Y'\n        52: 0,  # 'Z'\n        2: 2,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 2,  # 'e'\n        27: 1,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 2,  # 'i'\n        22: 1,  # 'j'\n        7: 0,  # 'k'\n        6: 1,  # 'l'\n        13: 0,  # 'm'\n        4: 0,  # 'n'\n        8: 2,  # 'o'\n        23: 0,  # 'p'\n        10: 2,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 1,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 0,  # 'z'\n        51: 1,  # 'Á'\n        44: 1,  # 'É'\n        61: 0,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 1,  # 'á'\n        15: 1,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 2,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 1,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    49: {  # 'G'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 2,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 1,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 2,  # 'Y'\n        52: 1,  # 'Z'\n        2: 2,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 2,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 1,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 1,  # 'l'\n        13: 0,  # 'm'\n        4: 0,  # 'n'\n        8: 2,  # 'o'\n        23: 0,  # 'p'\n        10: 2,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 1,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 2,  # 'y'\n        11: 0,  # 'z'\n        51: 1,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 1,  # 'á'\n        15: 1,  # 'é'\n        30: 0,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 1,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    38: {  # 'H'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 0,  # 'D'\n        32: 1,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 1,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 1,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 1,  # 'O'\n        46: 0,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 0,  # 'V'\n        55: 1,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 2,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 2,  # 'i'\n        22: 1,  # 'j'\n        7: 0,  # 'k'\n        6: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 0,  # 'n'\n        8: 3,  # 'o'\n        23: 0,  # 'p'\n        10: 1,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 2,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 0,  # 'z'\n        51: 2,  # 'Á'\n        44: 2,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 2,  # 'á'\n        15: 1,  # 'é'\n        30: 2,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 1,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    39: {  # 'I'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 1,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 2,  # 'I'\n        53: 1,  # 'J'\n        36: 2,  # 'K'\n        41: 2,  # 'L'\n        34: 1,  # 'M'\n        35: 2,  # 'N'\n        47: 1,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 2,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 0,  # 'Y'\n        52: 2,  # 'Z'\n        2: 0,  # 'a'\n        18: 1,  # 'b'\n        26: 1,  # 'c'\n        17: 2,  # 'd'\n        1: 0,  # 'e'\n        27: 1,  # 'f'\n        12: 2,  # 'g'\n        20: 1,  # 'h'\n        9: 0,  # 'i'\n        22: 1,  # 'j'\n        7: 1,  # 'k'\n        6: 2,  # 'l'\n        13: 2,  # 'm'\n        4: 1,  # 'n'\n        8: 0,  # 'o'\n        23: 1,  # 'p'\n        10: 2,  # 'r'\n        5: 2,  # 's'\n        3: 2,  # 't'\n        21: 0,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 1,  # 'z'\n        51: 1,  # 'Á'\n        44: 1,  # 'É'\n        61: 0,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    53: {  # 'J'\n        28: 2,  # 'A'\n        40: 0,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 2,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 1,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 1,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 1,  # 'Z'\n        2: 2,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 2,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 1,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 0,  # 'n'\n        8: 1,  # 'o'\n        23: 0,  # 'p'\n        10: 0,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 2,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 0,  # 'z'\n        51: 1,  # 'Á'\n        44: 1,  # 'É'\n        61: 0,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 2,  # 'á'\n        15: 1,  # 'é'\n        30: 0,  # 'í'\n        25: 2,  # 'ó'\n        24: 2,  # 'ö'\n        31: 1,  # 'ú'\n        29: 0,  # 'ü'\n        42: 1,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    36: {  # 'K'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 2,  # 'E'\n        50: 1,  # 'F'\n        49: 0,  # 'G'\n        38: 1,  # 'H'\n        39: 2,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 2,  # 'O'\n        46: 0,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 1,  # 'Y'\n        52: 0,  # 'Z'\n        2: 2,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 2,  # 'e'\n        27: 1,  # 'f'\n        12: 0,  # 'g'\n        20: 1,  # 'h'\n        9: 3,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        8: 2,  # 'o'\n        23: 0,  # 'p'\n        10: 2,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 1,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 0,  # 'z'\n        51: 1,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 2,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 2,  # 'á'\n        15: 2,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 2,  # 'ö'\n        31: 1,  # 'ú'\n        29: 2,  # 'ü'\n        42: 1,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    41: {  # 'L'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 2,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 2,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 2,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 2,  # 'O'\n        46: 0,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 2,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 1,  # 'Y'\n        52: 1,  # 'Z'\n        2: 2,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 3,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 2,  # 'i'\n        22: 1,  # 'j'\n        7: 0,  # 'k'\n        6: 1,  # 'l'\n        13: 0,  # 'm'\n        4: 0,  # 'n'\n        8: 2,  # 'o'\n        23: 0,  # 'p'\n        10: 0,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 2,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 0,  # 'z'\n        51: 2,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 2,  # 'á'\n        15: 1,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 0,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    34: {  # 'M'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 2,  # 'E'\n        50: 1,  # 'F'\n        49: 0,  # 'G'\n        38: 1,  # 'H'\n        39: 2,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 1,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 1,  # 'Y'\n        52: 1,  # 'Z'\n        2: 3,  # 'a'\n        18: 0,  # 'b'\n        26: 1,  # 'c'\n        17: 0,  # 'd'\n        1: 3,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 3,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 0,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        8: 3,  # 'o'\n        23: 0,  # 'p'\n        10: 1,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 2,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 0,  # 'z'\n        51: 2,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 2,  # 'á'\n        15: 2,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    35: {  # 'N'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 2,  # 'D'\n        32: 2,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 1,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 2,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 2,  # 'Y'\n        52: 1,  # 'Z'\n        2: 3,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 3,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 2,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 1,  # 'n'\n        8: 2,  # 'o'\n        23: 0,  # 'p'\n        10: 0,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 1,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 2,  # 'y'\n        11: 0,  # 'z'\n        51: 1,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 1,  # 'á'\n        15: 2,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 1,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    47: {  # 'O'\n        28: 1,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 1,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 2,  # 'K'\n        41: 2,  # 'L'\n        34: 2,  # 'M'\n        35: 2,  # 'N'\n        47: 1,  # 'O'\n        46: 1,  # 'P'\n        43: 2,  # 'R'\n        33: 2,  # 'S'\n        37: 2,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 1,  # 'Y'\n        52: 1,  # 'Z'\n        2: 0,  # 'a'\n        18: 1,  # 'b'\n        26: 1,  # 'c'\n        17: 1,  # 'd'\n        1: 1,  # 'e'\n        27: 1,  # 'f'\n        12: 1,  # 'g'\n        20: 1,  # 'h'\n        9: 1,  # 'i'\n        22: 1,  # 'j'\n        7: 2,  # 'k'\n        6: 2,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        8: 1,  # 'o'\n        23: 1,  # 'p'\n        10: 2,  # 'r'\n        5: 1,  # 's'\n        3: 2,  # 't'\n        21: 1,  # 'u'\n        19: 0,  # 'v'\n        62: 1,  # 'x'\n        16: 0,  # 'y'\n        11: 1,  # 'z'\n        51: 1,  # 'Á'\n        44: 1,  # 'É'\n        61: 0,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    46: {  # 'P'\n        28: 1,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 1,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 0,  # 'M'\n        35: 1,  # 'N'\n        47: 1,  # 'O'\n        46: 1,  # 'P'\n        43: 2,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 0,  # 'Y'\n        52: 1,  # 'Z'\n        2: 2,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 2,  # 'e'\n        27: 1,  # 'f'\n        12: 0,  # 'g'\n        20: 1,  # 'h'\n        9: 2,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 1,  # 'l'\n        13: 0,  # 'm'\n        4: 1,  # 'n'\n        8: 2,  # 'o'\n        23: 0,  # 'p'\n        10: 2,  # 'r'\n        5: 1,  # 's'\n        3: 0,  # 't'\n        21: 1,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 0,  # 'z'\n        51: 2,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 3,  # 'á'\n        15: 2,  # 'é'\n        30: 0,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 0,  # 'ú'\n        29: 1,  # 'ü'\n        42: 1,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    43: {  # 'R'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 2,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 2,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 2,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 2,  # 'S'\n        37: 2,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 1,  # 'Y'\n        52: 1,  # 'Z'\n        2: 2,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 2,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 1,  # 'h'\n        9: 2,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 0,  # 'n'\n        8: 2,  # 'o'\n        23: 0,  # 'p'\n        10: 0,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 1,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 0,  # 'z'\n        51: 2,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 2,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 2,  # 'á'\n        15: 2,  # 'é'\n        30: 1,  # 'í'\n        25: 2,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    33: {  # 'S'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 2,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 2,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 2,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 2,  # 'S'\n        37: 2,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 1,  # 'Y'\n        52: 3,  # 'Z'\n        2: 2,  # 'a'\n        18: 0,  # 'b'\n        26: 1,  # 'c'\n        17: 0,  # 'd'\n        1: 2,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 1,  # 'h'\n        9: 2,  # 'i'\n        22: 0,  # 'j'\n        7: 1,  # 'k'\n        6: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 0,  # 'n'\n        8: 2,  # 'o'\n        23: 1,  # 'p'\n        10: 0,  # 'r'\n        5: 0,  # 's'\n        3: 1,  # 't'\n        21: 1,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 3,  # 'z'\n        51: 2,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 2,  # 'á'\n        15: 1,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 1,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    37: {  # 'T'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 2,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 2,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 2,  # 'O'\n        46: 1,  # 'P'\n        43: 2,  # 'R'\n        33: 1,  # 'S'\n        37: 2,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 1,  # 'Y'\n        52: 1,  # 'Z'\n        2: 2,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 2,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 1,  # 'h'\n        9: 2,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 0,  # 'n'\n        8: 2,  # 'o'\n        23: 0,  # 'p'\n        10: 1,  # 'r'\n        5: 1,  # 's'\n        3: 0,  # 't'\n        21: 2,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 1,  # 'z'\n        51: 2,  # 'Á'\n        44: 2,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 2,  # 'á'\n        15: 1,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 2,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 1,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    57: {  # 'U'\n        28: 1,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 1,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 1,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 2,  # 'S'\n        37: 1,  # 'T'\n        57: 0,  # 'U'\n        48: 1,  # 'V'\n        55: 0,  # 'Y'\n        52: 1,  # 'Z'\n        2: 0,  # 'a'\n        18: 1,  # 'b'\n        26: 1,  # 'c'\n        17: 1,  # 'd'\n        1: 1,  # 'e'\n        27: 0,  # 'f'\n        12: 2,  # 'g'\n        20: 0,  # 'h'\n        9: 0,  # 'i'\n        22: 1,  # 'j'\n        7: 1,  # 'k'\n        6: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        8: 0,  # 'o'\n        23: 1,  # 'p'\n        10: 1,  # 'r'\n        5: 1,  # 's'\n        3: 1,  # 't'\n        21: 0,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 1,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 1,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    48: {  # 'V'\n        28: 2,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 1,  # 'D'\n        32: 2,  # 'E'\n        50: 1,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 2,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 0,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 1,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 1,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 2,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 2,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 1,  # 'l'\n        13: 0,  # 'm'\n        4: 0,  # 'n'\n        8: 2,  # 'o'\n        23: 0,  # 'p'\n        10: 0,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 1,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 0,  # 'z'\n        51: 2,  # 'Á'\n        44: 2,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 2,  # 'á'\n        15: 2,  # 'é'\n        30: 1,  # 'í'\n        25: 0,  # 'ó'\n        24: 1,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    55: {  # 'Y'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 2,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 1,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 0,  # 'Y'\n        52: 2,  # 'Z'\n        2: 1,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 1,  # 'd'\n        1: 1,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 0,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 0,  # 'n'\n        8: 1,  # 'o'\n        23: 1,  # 'p'\n        10: 0,  # 'r'\n        5: 0,  # 's'\n        3: 0,  # 't'\n        21: 0,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 0,  # 'z'\n        51: 1,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    52: {  # 'Z'\n        28: 2,  # 'A'\n        40: 1,  # 'B'\n        54: 0,  # 'C'\n        45: 1,  # 'D'\n        32: 2,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 2,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 2,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 2,  # 'S'\n        37: 1,  # 'T'\n        57: 1,  # 'U'\n        48: 1,  # 'V'\n        55: 1,  # 'Y'\n        52: 1,  # 'Z'\n        2: 1,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 1,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 1,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 1,  # 'n'\n        8: 1,  # 'o'\n        23: 0,  # 'p'\n        10: 1,  # 'r'\n        5: 2,  # 's'\n        3: 0,  # 't'\n        21: 1,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 0,  # 'z'\n        51: 2,  # 'Á'\n        44: 1,  # 'É'\n        61: 1,  # 'Í'\n        58: 1,  # 'Ó'\n        59: 1,  # 'Ö'\n        60: 1,  # 'Ú'\n        63: 1,  # 'Ü'\n        14: 1,  # 'á'\n        15: 1,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    2: {  # 'a'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 1,  # 'a'\n        18: 3,  # 'b'\n        26: 3,  # 'c'\n        17: 3,  # 'd'\n        1: 2,  # 'e'\n        27: 2,  # 'f'\n        12: 3,  # 'g'\n        20: 3,  # 'h'\n        9: 3,  # 'i'\n        22: 3,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        8: 2,  # 'o'\n        23: 3,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 3,  # 'v'\n        62: 1,  # 'x'\n        16: 2,  # 'y'\n        11: 3,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 1,  # 'á'\n        15: 1,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    18: {  # 'b'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 3,  # 'b'\n        26: 1,  # 'c'\n        17: 1,  # 'd'\n        1: 3,  # 'e'\n        27: 1,  # 'f'\n        12: 1,  # 'g'\n        20: 1,  # 'h'\n        9: 3,  # 'i'\n        22: 2,  # 'j'\n        7: 2,  # 'k'\n        6: 2,  # 'l'\n        13: 1,  # 'm'\n        4: 2,  # 'n'\n        8: 3,  # 'o'\n        23: 1,  # 'p'\n        10: 3,  # 'r'\n        5: 2,  # 's'\n        3: 1,  # 't'\n        21: 3,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 1,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 2,  # 'í'\n        25: 3,  # 'ó'\n        24: 2,  # 'ö'\n        31: 2,  # 'ú'\n        29: 2,  # 'ü'\n        42: 2,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    26: {  # 'c'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 1,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 1,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 2,  # 'a'\n        18: 1,  # 'b'\n        26: 2,  # 'c'\n        17: 1,  # 'd'\n        1: 3,  # 'e'\n        27: 1,  # 'f'\n        12: 1,  # 'g'\n        20: 3,  # 'h'\n        9: 3,  # 'i'\n        22: 1,  # 'j'\n        7: 2,  # 'k'\n        6: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        8: 3,  # 'o'\n        23: 1,  # 'p'\n        10: 2,  # 'r'\n        5: 3,  # 's'\n        3: 2,  # 't'\n        21: 2,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 2,  # 'á'\n        15: 2,  # 'é'\n        30: 2,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    17: {  # 'd'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 2,  # 'b'\n        26: 1,  # 'c'\n        17: 2,  # 'd'\n        1: 3,  # 'e'\n        27: 1,  # 'f'\n        12: 1,  # 'g'\n        20: 2,  # 'h'\n        9: 3,  # 'i'\n        22: 3,  # 'j'\n        7: 2,  # 'k'\n        6: 1,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        8: 3,  # 'o'\n        23: 1,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 3,  # 'v'\n        62: 0,  # 'x'\n        16: 2,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 3,  # 'í'\n        25: 3,  # 'ó'\n        24: 3,  # 'ö'\n        31: 2,  # 'ú'\n        29: 2,  # 'ü'\n        42: 2,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    1: {  # 'e'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 2,  # 'a'\n        18: 3,  # 'b'\n        26: 3,  # 'c'\n        17: 3,  # 'd'\n        1: 2,  # 'e'\n        27: 3,  # 'f'\n        12: 3,  # 'g'\n        20: 3,  # 'h'\n        9: 3,  # 'i'\n        22: 3,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        8: 2,  # 'o'\n        23: 3,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 2,  # 'u'\n        19: 3,  # 'v'\n        62: 2,  # 'x'\n        16: 2,  # 'y'\n        11: 3,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 1,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    27: {  # 'f'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 1,  # 'b'\n        26: 1,  # 'c'\n        17: 1,  # 'd'\n        1: 3,  # 'e'\n        27: 2,  # 'f'\n        12: 1,  # 'g'\n        20: 1,  # 'h'\n        9: 3,  # 'i'\n        22: 2,  # 'j'\n        7: 1,  # 'k'\n        6: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        8: 3,  # 'o'\n        23: 0,  # 'p'\n        10: 3,  # 'r'\n        5: 1,  # 's'\n        3: 1,  # 't'\n        21: 2,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 0,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 3,  # 'ö'\n        31: 1,  # 'ú'\n        29: 2,  # 'ü'\n        42: 1,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    12: {  # 'g'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 3,  # 'b'\n        26: 2,  # 'c'\n        17: 2,  # 'd'\n        1: 3,  # 'e'\n        27: 2,  # 'f'\n        12: 3,  # 'g'\n        20: 3,  # 'h'\n        9: 3,  # 'i'\n        22: 3,  # 'j'\n        7: 2,  # 'k'\n        6: 3,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        8: 3,  # 'o'\n        23: 1,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 3,  # 'v'\n        62: 0,  # 'x'\n        16: 3,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 2,  # 'í'\n        25: 3,  # 'ó'\n        24: 2,  # 'ö'\n        31: 2,  # 'ú'\n        29: 2,  # 'ü'\n        42: 2,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    20: {  # 'h'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 1,  # 'b'\n        26: 1,  # 'c'\n        17: 0,  # 'd'\n        1: 3,  # 'e'\n        27: 0,  # 'f'\n        12: 1,  # 'g'\n        20: 2,  # 'h'\n        9: 3,  # 'i'\n        22: 1,  # 'j'\n        7: 1,  # 'k'\n        6: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        8: 3,  # 'o'\n        23: 0,  # 'p'\n        10: 1,  # 'r'\n        5: 2,  # 's'\n        3: 1,  # 't'\n        21: 3,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 2,  # 'y'\n        11: 0,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 3,  # 'í'\n        25: 2,  # 'ó'\n        24: 2,  # 'ö'\n        31: 2,  # 'ú'\n        29: 1,  # 'ü'\n        42: 1,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    9: {  # 'i'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 3,  # 'b'\n        26: 3,  # 'c'\n        17: 3,  # 'd'\n        1: 3,  # 'e'\n        27: 3,  # 'f'\n        12: 3,  # 'g'\n        20: 3,  # 'h'\n        9: 2,  # 'i'\n        22: 2,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        8: 2,  # 'o'\n        23: 2,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 3,  # 'v'\n        62: 1,  # 'x'\n        16: 1,  # 'y'\n        11: 3,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 2,  # 'é'\n        30: 1,  # 'í'\n        25: 3,  # 'ó'\n        24: 1,  # 'ö'\n        31: 2,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    22: {  # 'j'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 2,  # 'b'\n        26: 1,  # 'c'\n        17: 3,  # 'd'\n        1: 3,  # 'e'\n        27: 1,  # 'f'\n        12: 1,  # 'g'\n        20: 2,  # 'h'\n        9: 1,  # 'i'\n        22: 2,  # 'j'\n        7: 2,  # 'k'\n        6: 2,  # 'l'\n        13: 1,  # 'm'\n        4: 2,  # 'n'\n        8: 3,  # 'o'\n        23: 1,  # 'p'\n        10: 2,  # 'r'\n        5: 2,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 1,  # 'í'\n        25: 3,  # 'ó'\n        24: 3,  # 'ö'\n        31: 3,  # 'ú'\n        29: 2,  # 'ü'\n        42: 1,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    7: {  # 'k'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 3,  # 'b'\n        26: 2,  # 'c'\n        17: 1,  # 'd'\n        1: 3,  # 'e'\n        27: 1,  # 'f'\n        12: 1,  # 'g'\n        20: 2,  # 'h'\n        9: 3,  # 'i'\n        22: 2,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 1,  # 'm'\n        4: 3,  # 'n'\n        8: 3,  # 'o'\n        23: 1,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 2,  # 'v'\n        62: 0,  # 'x'\n        16: 2,  # 'y'\n        11: 1,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 3,  # 'í'\n        25: 2,  # 'ó'\n        24: 3,  # 'ö'\n        31: 1,  # 'ú'\n        29: 3,  # 'ü'\n        42: 1,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    6: {  # 'l'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 1,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 1,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 2,  # 'b'\n        26: 3,  # 'c'\n        17: 3,  # 'd'\n        1: 3,  # 'e'\n        27: 3,  # 'f'\n        12: 3,  # 'g'\n        20: 3,  # 'h'\n        9: 3,  # 'i'\n        22: 3,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        8: 3,  # 'o'\n        23: 2,  # 'p'\n        10: 2,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 3,  # 'v'\n        62: 0,  # 'x'\n        16: 3,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 3,  # 'í'\n        25: 3,  # 'ó'\n        24: 3,  # 'ö'\n        31: 2,  # 'ú'\n        29: 2,  # 'ü'\n        42: 3,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    13: {  # 'm'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 3,  # 'b'\n        26: 2,  # 'c'\n        17: 1,  # 'd'\n        1: 3,  # 'e'\n        27: 1,  # 'f'\n        12: 1,  # 'g'\n        20: 2,  # 'h'\n        9: 3,  # 'i'\n        22: 2,  # 'j'\n        7: 1,  # 'k'\n        6: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 2,  # 'n'\n        8: 3,  # 'o'\n        23: 3,  # 'p'\n        10: 2,  # 'r'\n        5: 2,  # 's'\n        3: 2,  # 't'\n        21: 3,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 2,  # 'í'\n        25: 2,  # 'ó'\n        24: 2,  # 'ö'\n        31: 2,  # 'ú'\n        29: 2,  # 'ü'\n        42: 1,  # 'ő'\n        56: 2,  # 'ű'\n    },\n    4: {  # 'n'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 3,  # 'b'\n        26: 3,  # 'c'\n        17: 3,  # 'd'\n        1: 3,  # 'e'\n        27: 2,  # 'f'\n        12: 3,  # 'g'\n        20: 3,  # 'h'\n        9: 3,  # 'i'\n        22: 2,  # 'j'\n        7: 3,  # 'k'\n        6: 2,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        8: 3,  # 'o'\n        23: 2,  # 'p'\n        10: 2,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 2,  # 'v'\n        62: 1,  # 'x'\n        16: 3,  # 'y'\n        11: 3,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 2,  # 'í'\n        25: 2,  # 'ó'\n        24: 3,  # 'ö'\n        31: 2,  # 'ú'\n        29: 3,  # 'ü'\n        42: 2,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    8: {  # 'o'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 1,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 2,  # 'a'\n        18: 3,  # 'b'\n        26: 3,  # 'c'\n        17: 3,  # 'd'\n        1: 2,  # 'e'\n        27: 2,  # 'f'\n        12: 3,  # 'g'\n        20: 3,  # 'h'\n        9: 2,  # 'i'\n        22: 2,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        8: 1,  # 'o'\n        23: 3,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 2,  # 'u'\n        19: 3,  # 'v'\n        62: 1,  # 'x'\n        16: 1,  # 'y'\n        11: 3,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 1,  # 'á'\n        15: 2,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    23: {  # 'p'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 1,  # 'b'\n        26: 2,  # 'c'\n        17: 1,  # 'd'\n        1: 3,  # 'e'\n        27: 1,  # 'f'\n        12: 1,  # 'g'\n        20: 2,  # 'h'\n        9: 3,  # 'i'\n        22: 2,  # 'j'\n        7: 2,  # 'k'\n        6: 3,  # 'l'\n        13: 1,  # 'm'\n        4: 2,  # 'n'\n        8: 3,  # 'o'\n        23: 3,  # 'p'\n        10: 3,  # 'r'\n        5: 2,  # 's'\n        3: 2,  # 't'\n        21: 3,  # 'u'\n        19: 2,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 2,  # 'í'\n        25: 2,  # 'ó'\n        24: 2,  # 'ö'\n        31: 1,  # 'ú'\n        29: 2,  # 'ü'\n        42: 1,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    10: {  # 'r'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 3,  # 'b'\n        26: 3,  # 'c'\n        17: 3,  # 'd'\n        1: 3,  # 'e'\n        27: 2,  # 'f'\n        12: 3,  # 'g'\n        20: 2,  # 'h'\n        9: 3,  # 'i'\n        22: 3,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        8: 3,  # 'o'\n        23: 2,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 3,  # 'v'\n        62: 1,  # 'x'\n        16: 2,  # 'y'\n        11: 3,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 2,  # 'í'\n        25: 3,  # 'ó'\n        24: 3,  # 'ö'\n        31: 3,  # 'ú'\n        29: 3,  # 'ü'\n        42: 2,  # 'ő'\n        56: 2,  # 'ű'\n    },\n    5: {  # 's'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 3,  # 'b'\n        26: 2,  # 'c'\n        17: 2,  # 'd'\n        1: 3,  # 'e'\n        27: 2,  # 'f'\n        12: 2,  # 'g'\n        20: 2,  # 'h'\n        9: 3,  # 'i'\n        22: 1,  # 'j'\n        7: 3,  # 'k'\n        6: 2,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        8: 3,  # 'o'\n        23: 2,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 2,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 3,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 3,  # 'í'\n        25: 3,  # 'ó'\n        24: 3,  # 'ö'\n        31: 3,  # 'ú'\n        29: 3,  # 'ü'\n        42: 2,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    3: {  # 't'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 3,  # 'b'\n        26: 2,  # 'c'\n        17: 1,  # 'd'\n        1: 3,  # 'e'\n        27: 2,  # 'f'\n        12: 1,  # 'g'\n        20: 3,  # 'h'\n        9: 3,  # 'i'\n        22: 3,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        8: 3,  # 'o'\n        23: 1,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 3,  # 'v'\n        62: 0,  # 'x'\n        16: 3,  # 'y'\n        11: 1,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 2,  # 'í'\n        25: 3,  # 'ó'\n        24: 3,  # 'ö'\n        31: 3,  # 'ú'\n        29: 3,  # 'ü'\n        42: 3,  # 'ő'\n        56: 2,  # 'ű'\n    },\n    21: {  # 'u'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 1,  # 'a'\n        18: 2,  # 'b'\n        26: 2,  # 'c'\n        17: 3,  # 'd'\n        1: 2,  # 'e'\n        27: 1,  # 'f'\n        12: 3,  # 'g'\n        20: 2,  # 'h'\n        9: 2,  # 'i'\n        22: 2,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        8: 1,  # 'o'\n        23: 2,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 1,  # 'u'\n        19: 3,  # 'v'\n        62: 1,  # 'x'\n        16: 1,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 2,  # 'á'\n        15: 1,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 0,  # 'ö'\n        31: 1,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    19: {  # 'v'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 2,  # 'b'\n        26: 1,  # 'c'\n        17: 1,  # 'd'\n        1: 3,  # 'e'\n        27: 1,  # 'f'\n        12: 1,  # 'g'\n        20: 1,  # 'h'\n        9: 3,  # 'i'\n        22: 1,  # 'j'\n        7: 1,  # 'k'\n        6: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        8: 3,  # 'o'\n        23: 1,  # 'p'\n        10: 1,  # 'r'\n        5: 2,  # 's'\n        3: 2,  # 't'\n        21: 2,  # 'u'\n        19: 2,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 1,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 2,  # 'í'\n        25: 2,  # 'ó'\n        24: 2,  # 'ö'\n        31: 1,  # 'ú'\n        29: 2,  # 'ü'\n        42: 1,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    62: {  # 'x'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 1,  # 'a'\n        18: 1,  # 'b'\n        26: 1,  # 'c'\n        17: 0,  # 'd'\n        1: 1,  # 'e'\n        27: 1,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 1,  # 'i'\n        22: 0,  # 'j'\n        7: 1,  # 'k'\n        6: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        8: 1,  # 'o'\n        23: 1,  # 'p'\n        10: 1,  # 'r'\n        5: 1,  # 's'\n        3: 1,  # 't'\n        21: 1,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 0,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 1,  # 'á'\n        15: 1,  # 'é'\n        30: 1,  # 'í'\n        25: 1,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    16: {  # 'y'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 2,  # 'b'\n        26: 1,  # 'c'\n        17: 1,  # 'd'\n        1: 3,  # 'e'\n        27: 2,  # 'f'\n        12: 2,  # 'g'\n        20: 2,  # 'h'\n        9: 3,  # 'i'\n        22: 2,  # 'j'\n        7: 2,  # 'k'\n        6: 2,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        8: 3,  # 'o'\n        23: 2,  # 'p'\n        10: 2,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 3,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 2,  # 'í'\n        25: 2,  # 'ó'\n        24: 3,  # 'ö'\n        31: 2,  # 'ú'\n        29: 2,  # 'ü'\n        42: 1,  # 'ő'\n        56: 2,  # 'ű'\n    },\n    11: {  # 'z'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 3,  # 'a'\n        18: 2,  # 'b'\n        26: 1,  # 'c'\n        17: 3,  # 'd'\n        1: 3,  # 'e'\n        27: 1,  # 'f'\n        12: 2,  # 'g'\n        20: 2,  # 'h'\n        9: 3,  # 'i'\n        22: 1,  # 'j'\n        7: 3,  # 'k'\n        6: 2,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        8: 3,  # 'o'\n        23: 1,  # 'p'\n        10: 2,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 3,  # 'u'\n        19: 2,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 3,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 3,  # 'á'\n        15: 3,  # 'é'\n        30: 3,  # 'í'\n        25: 3,  # 'ó'\n        24: 3,  # 'ö'\n        31: 2,  # 'ú'\n        29: 3,  # 'ü'\n        42: 2,  # 'ő'\n        56: 1,  # 'ű'\n    },\n    51: {  # 'Á'\n        28: 0,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 0,  # 'E'\n        50: 1,  # 'F'\n        49: 2,  # 'G'\n        38: 1,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 2,  # 'L'\n        34: 1,  # 'M'\n        35: 2,  # 'N'\n        47: 0,  # 'O'\n        46: 1,  # 'P'\n        43: 2,  # 'R'\n        33: 2,  # 'S'\n        37: 1,  # 'T'\n        57: 0,  # 'U'\n        48: 1,  # 'V'\n        55: 0,  # 'Y'\n        52: 1,  # 'Z'\n        2: 0,  # 'a'\n        18: 1,  # 'b'\n        26: 1,  # 'c'\n        17: 1,  # 'd'\n        1: 0,  # 'e'\n        27: 0,  # 'f'\n        12: 1,  # 'g'\n        20: 1,  # 'h'\n        9: 0,  # 'i'\n        22: 1,  # 'j'\n        7: 1,  # 'k'\n        6: 2,  # 'l'\n        13: 2,  # 'm'\n        4: 0,  # 'n'\n        8: 0,  # 'o'\n        23: 1,  # 'p'\n        10: 1,  # 'r'\n        5: 1,  # 's'\n        3: 1,  # 't'\n        21: 0,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 1,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 1,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    44: {  # 'É'\n        28: 0,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 1,  # 'E'\n        50: 0,  # 'F'\n        49: 2,  # 'G'\n        38: 1,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 2,  # 'L'\n        34: 1,  # 'M'\n        35: 2,  # 'N'\n        47: 0,  # 'O'\n        46: 1,  # 'P'\n        43: 2,  # 'R'\n        33: 2,  # 'S'\n        37: 2,  # 'T'\n        57: 0,  # 'U'\n        48: 1,  # 'V'\n        55: 0,  # 'Y'\n        52: 1,  # 'Z'\n        2: 0,  # 'a'\n        18: 1,  # 'b'\n        26: 1,  # 'c'\n        17: 1,  # 'd'\n        1: 0,  # 'e'\n        27: 0,  # 'f'\n        12: 1,  # 'g'\n        20: 1,  # 'h'\n        9: 0,  # 'i'\n        22: 1,  # 'j'\n        7: 1,  # 'k'\n        6: 2,  # 'l'\n        13: 1,  # 'm'\n        4: 2,  # 'n'\n        8: 0,  # 'o'\n        23: 1,  # 'p'\n        10: 2,  # 'r'\n        5: 3,  # 's'\n        3: 1,  # 't'\n        21: 0,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 0,  # 'z'\n        51: 0,  # 'Á'\n        44: 1,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    61: {  # 'Í'\n        28: 0,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 0,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 1,  # 'J'\n        36: 0,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 0,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 0,  # 'U'\n        48: 1,  # 'V'\n        55: 0,  # 'Y'\n        52: 1,  # 'Z'\n        2: 0,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 0,  # 'e'\n        27: 0,  # 'f'\n        12: 2,  # 'g'\n        20: 0,  # 'h'\n        9: 0,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 0,  # 'l'\n        13: 1,  # 'm'\n        4: 0,  # 'n'\n        8: 0,  # 'o'\n        23: 0,  # 'p'\n        10: 1,  # 'r'\n        5: 0,  # 's'\n        3: 1,  # 't'\n        21: 0,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 1,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    58: {  # 'Ó'\n        28: 1,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 0,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 1,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 2,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 0,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 0,  # 'U'\n        48: 1,  # 'V'\n        55: 0,  # 'Y'\n        52: 1,  # 'Z'\n        2: 0,  # 'a'\n        18: 1,  # 'b'\n        26: 1,  # 'c'\n        17: 1,  # 'd'\n        1: 0,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 2,  # 'h'\n        9: 0,  # 'i'\n        22: 0,  # 'j'\n        7: 1,  # 'k'\n        6: 1,  # 'l'\n        13: 0,  # 'm'\n        4: 1,  # 'n'\n        8: 0,  # 'o'\n        23: 1,  # 'p'\n        10: 1,  # 'r'\n        5: 1,  # 's'\n        3: 0,  # 't'\n        21: 0,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 1,  # 'z'\n        51: 0,  # 'Á'\n        44: 1,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    59: {  # 'Ö'\n        28: 0,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 0,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 0,  # 'O'\n        46: 1,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 0,  # 'U'\n        48: 1,  # 'V'\n        55: 0,  # 'Y'\n        52: 1,  # 'Z'\n        2: 0,  # 'a'\n        18: 0,  # 'b'\n        26: 1,  # 'c'\n        17: 1,  # 'd'\n        1: 0,  # 'e'\n        27: 0,  # 'f'\n        12: 0,  # 'g'\n        20: 0,  # 'h'\n        9: 0,  # 'i'\n        22: 0,  # 'j'\n        7: 1,  # 'k'\n        6: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        8: 0,  # 'o'\n        23: 0,  # 'p'\n        10: 2,  # 'r'\n        5: 1,  # 's'\n        3: 1,  # 't'\n        21: 0,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 1,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    60: {  # 'Ú'\n        28: 0,  # 'A'\n        40: 1,  # 'B'\n        54: 1,  # 'C'\n        45: 1,  # 'D'\n        32: 0,  # 'E'\n        50: 1,  # 'F'\n        49: 1,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 0,  # 'U'\n        48: 1,  # 'V'\n        55: 0,  # 'Y'\n        52: 1,  # 'Z'\n        2: 0,  # 'a'\n        18: 0,  # 'b'\n        26: 0,  # 'c'\n        17: 0,  # 'd'\n        1: 0,  # 'e'\n        27: 0,  # 'f'\n        12: 2,  # 'g'\n        20: 0,  # 'h'\n        9: 0,  # 'i'\n        22: 2,  # 'j'\n        7: 0,  # 'k'\n        6: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 1,  # 'n'\n        8: 0,  # 'o'\n        23: 0,  # 'p'\n        10: 1,  # 'r'\n        5: 1,  # 's'\n        3: 1,  # 't'\n        21: 0,  # 'u'\n        19: 0,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 0,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    63: {  # 'Ü'\n        28: 0,  # 'A'\n        40: 1,  # 'B'\n        54: 0,  # 'C'\n        45: 1,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 1,  # 'G'\n        38: 1,  # 'H'\n        39: 0,  # 'I'\n        53: 1,  # 'J'\n        36: 1,  # 'K'\n        41: 1,  # 'L'\n        34: 1,  # 'M'\n        35: 1,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 1,  # 'R'\n        33: 1,  # 'S'\n        37: 1,  # 'T'\n        57: 0,  # 'U'\n        48: 1,  # 'V'\n        55: 0,  # 'Y'\n        52: 1,  # 'Z'\n        2: 0,  # 'a'\n        18: 1,  # 'b'\n        26: 0,  # 'c'\n        17: 1,  # 'd'\n        1: 0,  # 'e'\n        27: 0,  # 'f'\n        12: 1,  # 'g'\n        20: 0,  # 'h'\n        9: 0,  # 'i'\n        22: 0,  # 'j'\n        7: 0,  # 'k'\n        6: 1,  # 'l'\n        13: 0,  # 'm'\n        4: 1,  # 'n'\n        8: 0,  # 'o'\n        23: 0,  # 'p'\n        10: 1,  # 'r'\n        5: 1,  # 's'\n        3: 1,  # 't'\n        21: 0,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 1,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    14: {  # 'á'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 1,  # 'a'\n        18: 3,  # 'b'\n        26: 3,  # 'c'\n        17: 3,  # 'd'\n        1: 1,  # 'e'\n        27: 2,  # 'f'\n        12: 3,  # 'g'\n        20: 2,  # 'h'\n        9: 2,  # 'i'\n        22: 3,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        8: 1,  # 'o'\n        23: 2,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 2,  # 'u'\n        19: 3,  # 'v'\n        62: 0,  # 'x'\n        16: 1,  # 'y'\n        11: 3,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 1,  # 'á'\n        15: 2,  # 'é'\n        30: 1,  # 'í'\n        25: 0,  # 'ó'\n        24: 1,  # 'ö'\n        31: 0,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    15: {  # 'é'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 1,  # 'a'\n        18: 3,  # 'b'\n        26: 2,  # 'c'\n        17: 3,  # 'd'\n        1: 1,  # 'e'\n        27: 1,  # 'f'\n        12: 3,  # 'g'\n        20: 3,  # 'h'\n        9: 2,  # 'i'\n        22: 2,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        8: 1,  # 'o'\n        23: 3,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 0,  # 'u'\n        19: 3,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 3,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 1,  # 'á'\n        15: 1,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    30: {  # 'í'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 0,  # 'a'\n        18: 1,  # 'b'\n        26: 2,  # 'c'\n        17: 1,  # 'd'\n        1: 0,  # 'e'\n        27: 1,  # 'f'\n        12: 3,  # 'g'\n        20: 0,  # 'h'\n        9: 0,  # 'i'\n        22: 1,  # 'j'\n        7: 1,  # 'k'\n        6: 2,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        8: 0,  # 'o'\n        23: 1,  # 'p'\n        10: 3,  # 'r'\n        5: 2,  # 's'\n        3: 3,  # 't'\n        21: 0,  # 'u'\n        19: 3,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    25: {  # 'ó'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 2,  # 'a'\n        18: 3,  # 'b'\n        26: 2,  # 'c'\n        17: 3,  # 'd'\n        1: 1,  # 'e'\n        27: 2,  # 'f'\n        12: 2,  # 'g'\n        20: 2,  # 'h'\n        9: 2,  # 'i'\n        22: 2,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        8: 1,  # 'o'\n        23: 2,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 1,  # 'u'\n        19: 2,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 3,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 1,  # 'á'\n        15: 1,  # 'é'\n        30: 1,  # 'í'\n        25: 0,  # 'ó'\n        24: 1,  # 'ö'\n        31: 1,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    24: {  # 'ö'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 0,  # 'a'\n        18: 3,  # 'b'\n        26: 1,  # 'c'\n        17: 2,  # 'd'\n        1: 0,  # 'e'\n        27: 1,  # 'f'\n        12: 2,  # 'g'\n        20: 1,  # 'h'\n        9: 0,  # 'i'\n        22: 1,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        8: 0,  # 'o'\n        23: 2,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 3,  # 't'\n        21: 0,  # 'u'\n        19: 3,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 3,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    31: {  # 'ú'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 1,  # 'a'\n        18: 1,  # 'b'\n        26: 2,  # 'c'\n        17: 1,  # 'd'\n        1: 1,  # 'e'\n        27: 2,  # 'f'\n        12: 3,  # 'g'\n        20: 1,  # 'h'\n        9: 1,  # 'i'\n        22: 3,  # 'j'\n        7: 1,  # 'k'\n        6: 3,  # 'l'\n        13: 1,  # 'm'\n        4: 2,  # 'n'\n        8: 0,  # 'o'\n        23: 1,  # 'p'\n        10: 3,  # 'r'\n        5: 3,  # 's'\n        3: 2,  # 't'\n        21: 1,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 1,  # 'á'\n        15: 1,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    29: {  # 'ü'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 1,  # 'a'\n        18: 1,  # 'b'\n        26: 1,  # 'c'\n        17: 2,  # 'd'\n        1: 1,  # 'e'\n        27: 1,  # 'f'\n        12: 3,  # 'g'\n        20: 2,  # 'h'\n        9: 1,  # 'i'\n        22: 1,  # 'j'\n        7: 3,  # 'k'\n        6: 3,  # 'l'\n        13: 1,  # 'm'\n        4: 3,  # 'n'\n        8: 0,  # 'o'\n        23: 1,  # 'p'\n        10: 2,  # 'r'\n        5: 2,  # 's'\n        3: 2,  # 't'\n        21: 0,  # 'u'\n        19: 2,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 1,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    42: {  # 'ő'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 1,  # 'a'\n        18: 2,  # 'b'\n        26: 1,  # 'c'\n        17: 2,  # 'd'\n        1: 1,  # 'e'\n        27: 1,  # 'f'\n        12: 1,  # 'g'\n        20: 1,  # 'h'\n        9: 1,  # 'i'\n        22: 1,  # 'j'\n        7: 2,  # 'k'\n        6: 3,  # 'l'\n        13: 1,  # 'm'\n        4: 2,  # 'n'\n        8: 1,  # 'o'\n        23: 1,  # 'p'\n        10: 2,  # 'r'\n        5: 2,  # 's'\n        3: 2,  # 't'\n        21: 1,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 1,  # 'é'\n        30: 1,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 1,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n    56: {  # 'ű'\n        28: 0,  # 'A'\n        40: 0,  # 'B'\n        54: 0,  # 'C'\n        45: 0,  # 'D'\n        32: 0,  # 'E'\n        50: 0,  # 'F'\n        49: 0,  # 'G'\n        38: 0,  # 'H'\n        39: 0,  # 'I'\n        53: 0,  # 'J'\n        36: 0,  # 'K'\n        41: 0,  # 'L'\n        34: 0,  # 'M'\n        35: 0,  # 'N'\n        47: 0,  # 'O'\n        46: 0,  # 'P'\n        43: 0,  # 'R'\n        33: 0,  # 'S'\n        37: 0,  # 'T'\n        57: 0,  # 'U'\n        48: 0,  # 'V'\n        55: 0,  # 'Y'\n        52: 0,  # 'Z'\n        2: 1,  # 'a'\n        18: 1,  # 'b'\n        26: 0,  # 'c'\n        17: 1,  # 'd'\n        1: 1,  # 'e'\n        27: 1,  # 'f'\n        12: 1,  # 'g'\n        20: 1,  # 'h'\n        9: 1,  # 'i'\n        22: 1,  # 'j'\n        7: 1,  # 'k'\n        6: 1,  # 'l'\n        13: 0,  # 'm'\n        4: 2,  # 'n'\n        8: 0,  # 'o'\n        23: 0,  # 'p'\n        10: 1,  # 'r'\n        5: 1,  # 's'\n        3: 1,  # 't'\n        21: 0,  # 'u'\n        19: 1,  # 'v'\n        62: 0,  # 'x'\n        16: 0,  # 'y'\n        11: 2,  # 'z'\n        51: 0,  # 'Á'\n        44: 0,  # 'É'\n        61: 0,  # 'Í'\n        58: 0,  # 'Ó'\n        59: 0,  # 'Ö'\n        60: 0,  # 'Ú'\n        63: 0,  # 'Ü'\n        14: 0,  # 'á'\n        15: 0,  # 'é'\n        30: 0,  # 'í'\n        25: 0,  # 'ó'\n        24: 0,  # 'ö'\n        31: 0,  # 'ú'\n        29: 0,  # 'ü'\n        42: 0,  # 'ő'\n        56: 0,  # 'ű'\n    },\n}\n\n# 255: Undefined characters that did not exist in training text\n# 254: Carriage/Return\n# 253: symbol (punctuation) that does not belong to word\n# 252: 0 - 9\n# 251: Control characters\n\n# Character Mapping Table(s):\nWINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 28,  # 'A'\n     66: 40,  # 'B'\n     67: 54,  # 'C'\n     68: 45,  # 'D'\n     69: 32,  # 'E'\n     70: 50,  # 'F'\n     71: 49,  # 'G'\n     72: 38,  # 'H'\n     73: 39,  # 'I'\n     74: 53,  # 'J'\n     75: 36,  # 'K'\n     76: 41,  # 'L'\n     77: 34,  # 'M'\n     78: 35,  # 'N'\n     79: 47,  # 'O'\n     80: 46,  # 'P'\n     81: 72,  # 'Q'\n     82: 43,  # 'R'\n     83: 33,  # 'S'\n     84: 37,  # 'T'\n     85: 57,  # 'U'\n     86: 48,  # 'V'\n     87: 64,  # 'W'\n     88: 68,  # 'X'\n     89: 55,  # 'Y'\n     90: 52,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 2,  # 'a'\n     98: 18,  # 'b'\n     99: 26,  # 'c'\n     100: 17,  # 'd'\n     101: 1,  # 'e'\n     102: 27,  # 'f'\n     103: 12,  # 'g'\n     104: 20,  # 'h'\n     105: 9,  # 'i'\n     106: 22,  # 'j'\n     107: 7,  # 'k'\n     108: 6,  # 'l'\n     109: 13,  # 'm'\n     110: 4,  # 'n'\n     111: 8,  # 'o'\n     112: 23,  # 'p'\n     113: 67,  # 'q'\n     114: 10,  # 'r'\n     115: 5,  # 's'\n     116: 3,  # 't'\n     117: 21,  # 'u'\n     118: 19,  # 'v'\n     119: 65,  # 'w'\n     120: 62,  # 'x'\n     121: 16,  # 'y'\n     122: 11,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 161,  # '€'\n     129: 162,  # None\n     130: 163,  # '‚'\n     131: 164,  # None\n     132: 165,  # '„'\n     133: 166,  # '…'\n     134: 167,  # '†'\n     135: 168,  # '‡'\n     136: 169,  # None\n     137: 170,  # '‰'\n     138: 171,  # 'Š'\n     139: 172,  # '‹'\n     140: 173,  # 'Ś'\n     141: 174,  # 'Ť'\n     142: 175,  # 'Ž'\n     143: 176,  # 'Ź'\n     144: 177,  # None\n     145: 178,  # '‘'\n     146: 179,  # '’'\n     147: 180,  # '“'\n     148: 78,  # '”'\n     149: 181,  # '•'\n     150: 69,  # '–'\n     151: 182,  # '—'\n     152: 183,  # None\n     153: 184,  # '™'\n     154: 185,  # 'š'\n     155: 186,  # '›'\n     156: 187,  # 'ś'\n     157: 188,  # 'ť'\n     158: 189,  # 'ž'\n     159: 190,  # 'ź'\n     160: 191,  # '\\xa0'\n     161: 192,  # 'ˇ'\n     162: 193,  # '˘'\n     163: 194,  # 'Ł'\n     164: 195,  # '¤'\n     165: 196,  # 'Ą'\n     166: 197,  # '¦'\n     167: 76,  # '§'\n     168: 198,  # '¨'\n     169: 199,  # '©'\n     170: 200,  # 'Ş'\n     171: 201,  # '«'\n     172: 202,  # '¬'\n     173: 203,  # '\\xad'\n     174: 204,  # '®'\n     175: 205,  # 'Ż'\n     176: 81,  # '°'\n     177: 206,  # '±'\n     178: 207,  # '˛'\n     179: 208,  # 'ł'\n     180: 209,  # '´'\n     181: 210,  # 'µ'\n     182: 211,  # '¶'\n     183: 212,  # '·'\n     184: 213,  # '¸'\n     185: 214,  # 'ą'\n     186: 215,  # 'ş'\n     187: 216,  # '»'\n     188: 217,  # 'Ľ'\n     189: 218,  # '˝'\n     190: 219,  # 'ľ'\n     191: 220,  # 'ż'\n     192: 221,  # 'Ŕ'\n     193: 51,  # 'Á'\n     194: 83,  # 'Â'\n     195: 222,  # 'Ă'\n     196: 80,  # 'Ä'\n     197: 223,  # 'Ĺ'\n     198: 224,  # 'Ć'\n     199: 225,  # 'Ç'\n     200: 226,  # 'Č'\n     201: 44,  # 'É'\n     202: 227,  # 'Ę'\n     203: 228,  # 'Ë'\n     204: 229,  # 'Ě'\n     205: 61,  # 'Í'\n     206: 230,  # 'Î'\n     207: 231,  # 'Ď'\n     208: 232,  # 'Đ'\n     209: 233,  # 'Ń'\n     210: 234,  # 'Ň'\n     211: 58,  # 'Ó'\n     212: 235,  # 'Ô'\n     213: 66,  # 'Ő'\n     214: 59,  # 'Ö'\n     215: 236,  # '×'\n     216: 237,  # 'Ř'\n     217: 238,  # 'Ů'\n     218: 60,  # 'Ú'\n     219: 70,  # 'Ű'\n     220: 63,  # 'Ü'\n     221: 239,  # 'Ý'\n     222: 240,  # 'Ţ'\n     223: 241,  # 'ß'\n     224: 84,  # 'ŕ'\n     225: 14,  # 'á'\n     226: 75,  # 'â'\n     227: 242,  # 'ă'\n     228: 71,  # 'ä'\n     229: 82,  # 'ĺ'\n     230: 243,  # 'ć'\n     231: 73,  # 'ç'\n     232: 244,  # 'č'\n     233: 15,  # 'é'\n     234: 85,  # 'ę'\n     235: 79,  # 'ë'\n     236: 86,  # 'ě'\n     237: 30,  # 'í'\n     238: 77,  # 'î'\n     239: 87,  # 'ď'\n     240: 245,  # 'đ'\n     241: 246,  # 'ń'\n     242: 247,  # 'ň'\n     243: 25,  # 'ó'\n     244: 74,  # 'ô'\n     245: 42,  # 'ő'\n     246: 24,  # 'ö'\n     247: 248,  # '÷'\n     248: 249,  # 'ř'\n     249: 250,  # 'ů'\n     250: 31,  # 'ú'\n     251: 56,  # 'ű'\n     252: 29,  # 'ü'\n     253: 251,  # 'ý'\n     254: 252,  # 'ţ'\n     255: 253,  # '˙'\n}\n\nWINDOWS_1250_HUNGARIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1250',\n                                                      language='Hungarian',\n                                                      char_to_order_map=WINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER,\n                                                      language_model=HUNGARIAN_LANG_MODEL,\n                                                      typical_positive_ratio=0.947368,\n                                                      keep_ascii_letters=True,\n                                                      alphabet='ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÁÉÍÓÖÚÜáéíóöúüŐőŰű')\n\nISO_8859_2_HUNGARIAN_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 28,  # 'A'\n     66: 40,  # 'B'\n     67: 54,  # 'C'\n     68: 45,  # 'D'\n     69: 32,  # 'E'\n     70: 50,  # 'F'\n     71: 49,  # 'G'\n     72: 38,  # 'H'\n     73: 39,  # 'I'\n     74: 53,  # 'J'\n     75: 36,  # 'K'\n     76: 41,  # 'L'\n     77: 34,  # 'M'\n     78: 35,  # 'N'\n     79: 47,  # 'O'\n     80: 46,  # 'P'\n     81: 71,  # 'Q'\n     82: 43,  # 'R'\n     83: 33,  # 'S'\n     84: 37,  # 'T'\n     85: 57,  # 'U'\n     86: 48,  # 'V'\n     87: 64,  # 'W'\n     88: 68,  # 'X'\n     89: 55,  # 'Y'\n     90: 52,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 2,  # 'a'\n     98: 18,  # 'b'\n     99: 26,  # 'c'\n     100: 17,  # 'd'\n     101: 1,  # 'e'\n     102: 27,  # 'f'\n     103: 12,  # 'g'\n     104: 20,  # 'h'\n     105: 9,  # 'i'\n     106: 22,  # 'j'\n     107: 7,  # 'k'\n     108: 6,  # 'l'\n     109: 13,  # 'm'\n     110: 4,  # 'n'\n     111: 8,  # 'o'\n     112: 23,  # 'p'\n     113: 67,  # 'q'\n     114: 10,  # 'r'\n     115: 5,  # 's'\n     116: 3,  # 't'\n     117: 21,  # 'u'\n     118: 19,  # 'v'\n     119: 65,  # 'w'\n     120: 62,  # 'x'\n     121: 16,  # 'y'\n     122: 11,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 159,  # '\\x80'\n     129: 160,  # '\\x81'\n     130: 161,  # '\\x82'\n     131: 162,  # '\\x83'\n     132: 163,  # '\\x84'\n     133: 164,  # '\\x85'\n     134: 165,  # '\\x86'\n     135: 166,  # '\\x87'\n     136: 167,  # '\\x88'\n     137: 168,  # '\\x89'\n     138: 169,  # '\\x8a'\n     139: 170,  # '\\x8b'\n     140: 171,  # '\\x8c'\n     141: 172,  # '\\x8d'\n     142: 173,  # '\\x8e'\n     143: 174,  # '\\x8f'\n     144: 175,  # '\\x90'\n     145: 176,  # '\\x91'\n     146: 177,  # '\\x92'\n     147: 178,  # '\\x93'\n     148: 179,  # '\\x94'\n     149: 180,  # '\\x95'\n     150: 181,  # '\\x96'\n     151: 182,  # '\\x97'\n     152: 183,  # '\\x98'\n     153: 184,  # '\\x99'\n     154: 185,  # '\\x9a'\n     155: 186,  # '\\x9b'\n     156: 187,  # '\\x9c'\n     157: 188,  # '\\x9d'\n     158: 189,  # '\\x9e'\n     159: 190,  # '\\x9f'\n     160: 191,  # '\\xa0'\n     161: 192,  # 'Ą'\n     162: 193,  # '˘'\n     163: 194,  # 'Ł'\n     164: 195,  # '¤'\n     165: 196,  # 'Ľ'\n     166: 197,  # 'Ś'\n     167: 75,  # '§'\n     168: 198,  # '¨'\n     169: 199,  # 'Š'\n     170: 200,  # 'Ş'\n     171: 201,  # 'Ť'\n     172: 202,  # 'Ź'\n     173: 203,  # '\\xad'\n     174: 204,  # 'Ž'\n     175: 205,  # 'Ż'\n     176: 79,  # '°'\n     177: 206,  # 'ą'\n     178: 207,  # '˛'\n     179: 208,  # 'ł'\n     180: 209,  # '´'\n     181: 210,  # 'ľ'\n     182: 211,  # 'ś'\n     183: 212,  # 'ˇ'\n     184: 213,  # '¸'\n     185: 214,  # 'š'\n     186: 215,  # 'ş'\n     187: 216,  # 'ť'\n     188: 217,  # 'ź'\n     189: 218,  # '˝'\n     190: 219,  # 'ž'\n     191: 220,  # 'ż'\n     192: 221,  # 'Ŕ'\n     193: 51,  # 'Á'\n     194: 81,  # 'Â'\n     195: 222,  # 'Ă'\n     196: 78,  # 'Ä'\n     197: 223,  # 'Ĺ'\n     198: 224,  # 'Ć'\n     199: 225,  # 'Ç'\n     200: 226,  # 'Č'\n     201: 44,  # 'É'\n     202: 227,  # 'Ę'\n     203: 228,  # 'Ë'\n     204: 229,  # 'Ě'\n     205: 61,  # 'Í'\n     206: 230,  # 'Î'\n     207: 231,  # 'Ď'\n     208: 232,  # 'Đ'\n     209: 233,  # 'Ń'\n     210: 234,  # 'Ň'\n     211: 58,  # 'Ó'\n     212: 235,  # 'Ô'\n     213: 66,  # 'Ő'\n     214: 59,  # 'Ö'\n     215: 236,  # '×'\n     216: 237,  # 'Ř'\n     217: 238,  # 'Ů'\n     218: 60,  # 'Ú'\n     219: 69,  # 'Ű'\n     220: 63,  # 'Ü'\n     221: 239,  # 'Ý'\n     222: 240,  # 'Ţ'\n     223: 241,  # 'ß'\n     224: 82,  # 'ŕ'\n     225: 14,  # 'á'\n     226: 74,  # 'â'\n     227: 242,  # 'ă'\n     228: 70,  # 'ä'\n     229: 80,  # 'ĺ'\n     230: 243,  # 'ć'\n     231: 72,  # 'ç'\n     232: 244,  # 'č'\n     233: 15,  # 'é'\n     234: 83,  # 'ę'\n     235: 77,  # 'ë'\n     236: 84,  # 'ě'\n     237: 30,  # 'í'\n     238: 76,  # 'î'\n     239: 85,  # 'ď'\n     240: 245,  # 'đ'\n     241: 246,  # 'ń'\n     242: 247,  # 'ň'\n     243: 25,  # 'ó'\n     244: 73,  # 'ô'\n     245: 42,  # 'ő'\n     246: 24,  # 'ö'\n     247: 248,  # '÷'\n     248: 249,  # 'ř'\n     249: 250,  # 'ů'\n     250: 31,  # 'ú'\n     251: 56,  # 'ű'\n     252: 29,  # 'ü'\n     253: 251,  # 'ý'\n     254: 252,  # 'ţ'\n     255: 253,  # '˙'\n}\n\nISO_8859_2_HUNGARIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-2',\n                                                    language='Hungarian',\n                                                    char_to_order_map=ISO_8859_2_HUNGARIAN_CHAR_TO_ORDER,\n                                                    language_model=HUNGARIAN_LANG_MODEL,\n                                                    typical_positive_ratio=0.947368,\n                                                    keep_ascii_letters=True,\n                                                    alphabet='ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÁÉÍÓÖÚÜáéíóöúüŐőŰű')\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/langrussianmodel.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nfrom chardet.sbcharsetprober import SingleByteCharSetModel\n\n\n# 3: Positive\n# 2: Likely\n# 1: Unlikely\n# 0: Negative\n\nRUSSIAN_LANG_MODEL = {\n    37: {  # 'А'\n        37: 0,  # 'А'\n        44: 1,  # 'Б'\n        33: 1,  # 'В'\n        46: 1,  # 'Г'\n        41: 1,  # 'Д'\n        48: 1,  # 'Е'\n        56: 1,  # 'Ж'\n        51: 1,  # 'З'\n        42: 1,  # 'И'\n        60: 1,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 2,  # 'Н'\n        34: 1,  # 'О'\n        35: 1,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 1,  # 'У'\n        53: 1,  # 'Ф'\n        55: 1,  # 'Х'\n        58: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 1,  # 'Ш'\n        63: 1,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 1,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 1,  # 'а'\n        21: 2,  # 'б'\n        10: 2,  # 'в'\n        19: 2,  # 'г'\n        13: 2,  # 'д'\n        2: 0,  # 'е'\n        24: 1,  # 'ж'\n        20: 1,  # 'з'\n        4: 0,  # 'и'\n        23: 1,  # 'й'\n        11: 2,  # 'к'\n        8: 3,  # 'л'\n        12: 2,  # 'м'\n        5: 2,  # 'н'\n        1: 0,  # 'о'\n        15: 2,  # 'п'\n        9: 2,  # 'р'\n        7: 2,  # 'с'\n        6: 2,  # 'т'\n        14: 2,  # 'у'\n        39: 2,  # 'ф'\n        26: 2,  # 'х'\n        28: 0,  # 'ц'\n        22: 1,  # 'ч'\n        25: 2,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 1,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    44: {  # 'Б'\n        37: 1,  # 'А'\n        44: 0,  # 'Б'\n        33: 1,  # 'В'\n        46: 1,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 0,  # 'П'\n        45: 1,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 1,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 2,  # 'а'\n        21: 0,  # 'б'\n        10: 0,  # 'в'\n        19: 0,  # 'г'\n        13: 1,  # 'д'\n        2: 3,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 2,  # 'л'\n        12: 0,  # 'м'\n        5: 0,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 2,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 2,  # 'ы'\n        17: 1,  # 'ь'\n        30: 2,  # 'э'\n        27: 1,  # 'ю'\n        16: 1,  # 'я'\n    },\n    33: {  # 'В'\n        37: 2,  # 'А'\n        44: 0,  # 'Б'\n        33: 1,  # 'В'\n        46: 0,  # 'Г'\n        41: 1,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 1,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 1,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 1,  # 'Ы'\n        61: 1,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 2,  # 'а'\n        21: 1,  # 'б'\n        10: 1,  # 'в'\n        19: 1,  # 'г'\n        13: 2,  # 'д'\n        2: 3,  # 'е'\n        24: 0,  # 'ж'\n        20: 2,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 1,  # 'к'\n        8: 2,  # 'л'\n        12: 2,  # 'м'\n        5: 2,  # 'н'\n        1: 3,  # 'о'\n        15: 2,  # 'п'\n        9: 2,  # 'р'\n        7: 3,  # 'с'\n        6: 2,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 1,  # 'х'\n        28: 1,  # 'ц'\n        22: 2,  # 'ч'\n        25: 1,  # 'ш'\n        29: 0,  # 'щ'\n        54: 1,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 1,  # 'ь'\n        30: 2,  # 'э'\n        27: 0,  # 'ю'\n        16: 1,  # 'я'\n    },\n    46: {  # 'Г'\n        37: 1,  # 'А'\n        44: 1,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 1,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 1,  # 'П'\n        45: 1,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 2,  # 'а'\n        21: 0,  # 'б'\n        10: 1,  # 'в'\n        19: 0,  # 'г'\n        13: 2,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 2,  # 'л'\n        12: 1,  # 'м'\n        5: 1,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 2,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 1,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 0,  # 'я'\n    },\n    41: {  # 'Д'\n        37: 1,  # 'А'\n        44: 0,  # 'Б'\n        33: 1,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 2,  # 'Е'\n        56: 1,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 0,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 0,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 0,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 1,  # 'Ы'\n        61: 1,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 3,  # 'а'\n        21: 0,  # 'б'\n        10: 2,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 2,  # 'е'\n        24: 3,  # 'ж'\n        20: 1,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 2,  # 'л'\n        12: 1,  # 'м'\n        5: 1,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 2,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 1,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 1,  # 'ы'\n        17: 1,  # 'ь'\n        30: 2,  # 'э'\n        27: 1,  # 'ю'\n        16: 1,  # 'я'\n    },\n    48: {  # 'Е'\n        37: 1,  # 'А'\n        44: 1,  # 'Б'\n        33: 1,  # 'В'\n        46: 1,  # 'Г'\n        41: 1,  # 'Д'\n        48: 1,  # 'Е'\n        56: 1,  # 'Ж'\n        51: 1,  # 'З'\n        42: 1,  # 'И'\n        60: 1,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 2,  # 'Н'\n        34: 1,  # 'О'\n        35: 1,  # 'П'\n        45: 2,  # 'Р'\n        32: 2,  # 'С'\n        40: 1,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 1,  # 'Х'\n        58: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 1,  # 'Ш'\n        63: 1,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 0,  # 'а'\n        21: 0,  # 'б'\n        10: 2,  # 'в'\n        19: 2,  # 'г'\n        13: 2,  # 'д'\n        2: 2,  # 'е'\n        24: 1,  # 'ж'\n        20: 1,  # 'з'\n        4: 0,  # 'и'\n        23: 2,  # 'й'\n        11: 1,  # 'к'\n        8: 2,  # 'л'\n        12: 2,  # 'м'\n        5: 1,  # 'н'\n        1: 0,  # 'о'\n        15: 1,  # 'п'\n        9: 1,  # 'р'\n        7: 3,  # 'с'\n        6: 0,  # 'т'\n        14: 0,  # 'у'\n        39: 1,  # 'ф'\n        26: 1,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 1,  # 'ш'\n        29: 2,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 1,  # 'ю'\n        16: 0,  # 'я'\n    },\n    56: {  # 'Ж'\n        37: 1,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 1,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 1,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 2,  # 'а'\n        21: 1,  # 'б'\n        10: 0,  # 'в'\n        19: 1,  # 'г'\n        13: 1,  # 'д'\n        2: 2,  # 'е'\n        24: 1,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 0,  # 'л'\n        12: 1,  # 'м'\n        5: 0,  # 'н'\n        1: 2,  # 'о'\n        15: 0,  # 'п'\n        9: 1,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 2,  # 'ю'\n        16: 0,  # 'я'\n    },\n    51: {  # 'З'\n        37: 1,  # 'А'\n        44: 0,  # 'Б'\n        33: 1,  # 'В'\n        46: 1,  # 'Г'\n        41: 1,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 0,  # 'П'\n        45: 1,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 1,  # 'Ы'\n        61: 1,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 1,  # 'б'\n        10: 2,  # 'в'\n        19: 0,  # 'г'\n        13: 2,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 1,  # 'л'\n        12: 1,  # 'м'\n        5: 2,  # 'н'\n        1: 2,  # 'о'\n        15: 0,  # 'п'\n        9: 1,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 1,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 1,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 1,  # 'я'\n    },\n    42: {  # 'И'\n        37: 1,  # 'А'\n        44: 1,  # 'Б'\n        33: 1,  # 'В'\n        46: 1,  # 'Г'\n        41: 1,  # 'Д'\n        48: 2,  # 'Е'\n        56: 1,  # 'Ж'\n        51: 1,  # 'З'\n        42: 1,  # 'И'\n        60: 1,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 1,  # 'П'\n        45: 1,  # 'Р'\n        32: 2,  # 'С'\n        40: 1,  # 'Т'\n        52: 0,  # 'У'\n        53: 1,  # 'Ф'\n        55: 1,  # 'Х'\n        58: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 1,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 1,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 1,  # 'а'\n        21: 2,  # 'б'\n        10: 2,  # 'в'\n        19: 2,  # 'г'\n        13: 2,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 2,  # 'з'\n        4: 1,  # 'и'\n        23: 0,  # 'й'\n        11: 1,  # 'к'\n        8: 2,  # 'л'\n        12: 2,  # 'м'\n        5: 2,  # 'н'\n        1: 1,  # 'о'\n        15: 1,  # 'п'\n        9: 2,  # 'р'\n        7: 2,  # 'с'\n        6: 2,  # 'т'\n        14: 1,  # 'у'\n        39: 1,  # 'ф'\n        26: 2,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 1,  # 'ш'\n        29: 1,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 1,  # 'ю'\n        16: 0,  # 'я'\n    },\n    60: {  # 'Й'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 1,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 0,  # 'М'\n        31: 1,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 1,  # 'Х'\n        58: 1,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 0,  # 'а'\n        21: 0,  # 'б'\n        10: 0,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 1,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 0,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 0,  # 'л'\n        12: 0,  # 'м'\n        5: 0,  # 'н'\n        1: 2,  # 'о'\n        15: 0,  # 'п'\n        9: 0,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 0,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    36: {  # 'К'\n        37: 2,  # 'А'\n        44: 0,  # 'Б'\n        33: 1,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 1,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 1,  # 'Л'\n        38: 0,  # 'М'\n        31: 1,  # 'Н'\n        34: 2,  # 'О'\n        35: 1,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 1,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 0,  # 'б'\n        10: 1,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 2,  # 'л'\n        12: 0,  # 'м'\n        5: 1,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 2,  # 'р'\n        7: 2,  # 'с'\n        6: 2,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 1,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 1,  # 'ы'\n        17: 1,  # 'ь'\n        30: 2,  # 'э'\n        27: 1,  # 'ю'\n        16: 0,  # 'я'\n    },\n    49: {  # 'Л'\n        37: 2,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 1,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 1,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 0,  # 'Н'\n        34: 1,  # 'О'\n        35: 1,  # 'П'\n        45: 0,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 1,  # 'Ы'\n        61: 1,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 1,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 2,  # 'а'\n        21: 0,  # 'б'\n        10: 0,  # 'в'\n        19: 1,  # 'г'\n        13: 0,  # 'д'\n        2: 2,  # 'е'\n        24: 1,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 1,  # 'л'\n        12: 0,  # 'м'\n        5: 1,  # 'н'\n        1: 2,  # 'о'\n        15: 0,  # 'п'\n        9: 0,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 1,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 1,  # 'ы'\n        17: 1,  # 'ь'\n        30: 2,  # 'э'\n        27: 2,  # 'ю'\n        16: 1,  # 'я'\n    },\n    38: {  # 'М'\n        37: 1,  # 'А'\n        44: 1,  # 'Б'\n        33: 1,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 1,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 1,  # 'У'\n        53: 1,  # 'Ф'\n        55: 1,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 1,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 1,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 3,  # 'а'\n        21: 0,  # 'б'\n        10: 0,  # 'в'\n        19: 1,  # 'г'\n        13: 0,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 1,  # 'л'\n        12: 1,  # 'м'\n        5: 2,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 1,  # 'р'\n        7: 1,  # 'с'\n        6: 0,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 1,  # 'ь'\n        30: 2,  # 'э'\n        27: 1,  # 'ю'\n        16: 1,  # 'я'\n    },\n    31: {  # 'Н'\n        37: 2,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 1,  # 'Г'\n        41: 1,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 1,  # 'З'\n        42: 2,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 0,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 1,  # 'У'\n        53: 1,  # 'Ф'\n        55: 1,  # 'Х'\n        58: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 1,  # 'Ы'\n        61: 1,  # 'Ь'\n        47: 1,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 3,  # 'а'\n        21: 0,  # 'б'\n        10: 0,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 3,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 0,  # 'л'\n        12: 0,  # 'м'\n        5: 0,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 1,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 3,  # 'у'\n        39: 0,  # 'ф'\n        26: 1,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 1,  # 'ы'\n        17: 2,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 1,  # 'я'\n    },\n    34: {  # 'О'\n        37: 0,  # 'А'\n        44: 1,  # 'Б'\n        33: 1,  # 'В'\n        46: 1,  # 'Г'\n        41: 2,  # 'Д'\n        48: 1,  # 'Е'\n        56: 1,  # 'Ж'\n        51: 1,  # 'З'\n        42: 1,  # 'И'\n        60: 1,  # 'Й'\n        36: 1,  # 'К'\n        49: 2,  # 'Л'\n        38: 1,  # 'М'\n        31: 2,  # 'Н'\n        34: 1,  # 'О'\n        35: 1,  # 'П'\n        45: 2,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 1,  # 'У'\n        53: 1,  # 'Ф'\n        55: 1,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 1,  # 'Ш'\n        63: 1,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 1,  # 'а'\n        21: 2,  # 'б'\n        10: 1,  # 'в'\n        19: 2,  # 'г'\n        13: 2,  # 'д'\n        2: 0,  # 'е'\n        24: 1,  # 'ж'\n        20: 1,  # 'з'\n        4: 0,  # 'и'\n        23: 1,  # 'й'\n        11: 2,  # 'к'\n        8: 2,  # 'л'\n        12: 1,  # 'м'\n        5: 3,  # 'н'\n        1: 0,  # 'о'\n        15: 2,  # 'п'\n        9: 2,  # 'р'\n        7: 2,  # 'с'\n        6: 2,  # 'т'\n        14: 1,  # 'у'\n        39: 1,  # 'ф'\n        26: 2,  # 'х'\n        28: 1,  # 'ц'\n        22: 2,  # 'ч'\n        25: 2,  # 'ш'\n        29: 1,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    35: {  # 'П'\n        37: 1,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 1,  # 'Л'\n        38: 0,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 1,  # 'П'\n        45: 2,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 1,  # 'Ы'\n        61: 1,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 2,  # 'а'\n        21: 0,  # 'б'\n        10: 0,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 2,  # 'л'\n        12: 0,  # 'м'\n        5: 1,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 3,  # 'р'\n        7: 1,  # 'с'\n        6: 1,  # 'т'\n        14: 2,  # 'у'\n        39: 1,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 1,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 1,  # 'ы'\n        17: 2,  # 'ь'\n        30: 1,  # 'э'\n        27: 0,  # 'ю'\n        16: 2,  # 'я'\n    },\n    45: {  # 'Р'\n        37: 2,  # 'А'\n        44: 1,  # 'Б'\n        33: 1,  # 'В'\n        46: 1,  # 'Г'\n        41: 1,  # 'Д'\n        48: 2,  # 'Е'\n        56: 1,  # 'Ж'\n        51: 0,  # 'З'\n        42: 2,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 2,  # 'О'\n        35: 0,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 1,  # 'Х'\n        58: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 1,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 1,  # 'Ы'\n        61: 1,  # 'Ь'\n        47: 1,  # 'Э'\n        59: 1,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 3,  # 'а'\n        21: 0,  # 'б'\n        10: 1,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 2,  # 'е'\n        24: 1,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 0,  # 'л'\n        12: 0,  # 'м'\n        5: 0,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 1,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 2,  # 'ы'\n        17: 0,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 2,  # 'я'\n    },\n    32: {  # 'С'\n        37: 1,  # 'А'\n        44: 1,  # 'Б'\n        33: 1,  # 'В'\n        46: 1,  # 'Г'\n        41: 1,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 1,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 2,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 1,  # 'Х'\n        58: 1,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 1,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 1,  # 'Ы'\n        61: 1,  # 'Ь'\n        47: 1,  # 'Э'\n        59: 1,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 2,  # 'а'\n        21: 1,  # 'б'\n        10: 2,  # 'в'\n        19: 1,  # 'г'\n        13: 2,  # 'д'\n        2: 3,  # 'е'\n        24: 1,  # 'ж'\n        20: 1,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 2,  # 'к'\n        8: 2,  # 'л'\n        12: 2,  # 'м'\n        5: 2,  # 'н'\n        1: 2,  # 'о'\n        15: 2,  # 'п'\n        9: 2,  # 'р'\n        7: 1,  # 'с'\n        6: 3,  # 'т'\n        14: 2,  # 'у'\n        39: 1,  # 'ф'\n        26: 1,  # 'х'\n        28: 1,  # 'ц'\n        22: 1,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 1,  # 'ъ'\n        18: 1,  # 'ы'\n        17: 1,  # 'ь'\n        30: 2,  # 'э'\n        27: 1,  # 'ю'\n        16: 1,  # 'я'\n    },\n    40: {  # 'Т'\n        37: 1,  # 'А'\n        44: 0,  # 'Б'\n        33: 1,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 2,  # 'О'\n        35: 0,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 1,  # 'Ы'\n        61: 1,  # 'Ь'\n        47: 1,  # 'Э'\n        59: 1,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 3,  # 'а'\n        21: 1,  # 'б'\n        10: 2,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 3,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 1,  # 'к'\n        8: 1,  # 'л'\n        12: 0,  # 'м'\n        5: 0,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 2,  # 'р'\n        7: 1,  # 'с'\n        6: 0,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 1,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 1,  # 'ь'\n        30: 2,  # 'э'\n        27: 1,  # 'ю'\n        16: 1,  # 'я'\n    },\n    52: {  # 'У'\n        37: 1,  # 'А'\n        44: 1,  # 'Б'\n        33: 1,  # 'В'\n        46: 1,  # 'Г'\n        41: 1,  # 'Д'\n        48: 1,  # 'Е'\n        56: 1,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 1,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 1,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 1,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 1,  # 'Ш'\n        63: 1,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 1,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 1,  # 'а'\n        21: 2,  # 'б'\n        10: 2,  # 'в'\n        19: 1,  # 'г'\n        13: 2,  # 'д'\n        2: 1,  # 'е'\n        24: 2,  # 'ж'\n        20: 2,  # 'з'\n        4: 2,  # 'и'\n        23: 1,  # 'й'\n        11: 1,  # 'к'\n        8: 2,  # 'л'\n        12: 2,  # 'м'\n        5: 1,  # 'н'\n        1: 2,  # 'о'\n        15: 1,  # 'п'\n        9: 2,  # 'р'\n        7: 2,  # 'с'\n        6: 2,  # 'т'\n        14: 0,  # 'у'\n        39: 1,  # 'ф'\n        26: 1,  # 'х'\n        28: 1,  # 'ц'\n        22: 2,  # 'ч'\n        25: 1,  # 'ш'\n        29: 1,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 2,  # 'э'\n        27: 1,  # 'ю'\n        16: 0,  # 'я'\n    },\n    53: {  # 'Ф'\n        37: 1,  # 'А'\n        44: 1,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 1,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 1,  # 'О'\n        35: 0,  # 'П'\n        45: 1,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 2,  # 'а'\n        21: 0,  # 'б'\n        10: 0,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 2,  # 'л'\n        12: 0,  # 'м'\n        5: 0,  # 'н'\n        1: 2,  # 'о'\n        15: 0,  # 'п'\n        9: 2,  # 'р'\n        7: 0,  # 'с'\n        6: 1,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 1,  # 'ь'\n        30: 2,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    55: {  # 'Х'\n        37: 1,  # 'А'\n        44: 0,  # 'Б'\n        33: 1,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 2,  # 'а'\n        21: 0,  # 'б'\n        10: 2,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 2,  # 'л'\n        12: 1,  # 'м'\n        5: 0,  # 'н'\n        1: 2,  # 'о'\n        15: 0,  # 'п'\n        9: 2,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 1,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 1,  # 'ь'\n        30: 1,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    58: {  # 'Ц'\n        37: 1,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 1,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 1,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 1,  # 'а'\n        21: 0,  # 'б'\n        10: 1,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 0,  # 'л'\n        12: 0,  # 'м'\n        5: 0,  # 'н'\n        1: 0,  # 'о'\n        15: 0,  # 'п'\n        9: 0,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 1,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 1,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 1,  # 'ю'\n        16: 0,  # 'я'\n    },\n    50: {  # 'Ч'\n        37: 1,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 1,  # 'Н'\n        34: 0,  # 'О'\n        35: 1,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 1,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 1,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 2,  # 'а'\n        21: 0,  # 'б'\n        10: 0,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 1,  # 'л'\n        12: 0,  # 'м'\n        5: 0,  # 'н'\n        1: 1,  # 'о'\n        15: 0,  # 'п'\n        9: 1,  # 'р'\n        7: 0,  # 'с'\n        6: 3,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 1,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    57: {  # 'Ш'\n        37: 1,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 0,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 1,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 2,  # 'а'\n        21: 0,  # 'б'\n        10: 1,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 1,  # 'и'\n        23: 0,  # 'й'\n        11: 1,  # 'к'\n        8: 2,  # 'л'\n        12: 1,  # 'м'\n        5: 1,  # 'н'\n        1: 2,  # 'о'\n        15: 2,  # 'п'\n        9: 1,  # 'р'\n        7: 0,  # 'с'\n        6: 2,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 1,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 1,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 1,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    63: {  # 'Щ'\n        37: 1,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 1,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 1,  # 'а'\n        21: 0,  # 'б'\n        10: 0,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 1,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 1,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 0,  # 'л'\n        12: 0,  # 'м'\n        5: 0,  # 'н'\n        1: 1,  # 'о'\n        15: 0,  # 'п'\n        9: 0,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 1,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    62: {  # 'Ы'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 1,  # 'В'\n        46: 1,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 1,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 0,  # 'О'\n        35: 1,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 1,  # 'Х'\n        58: 1,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 1,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 0,  # 'а'\n        21: 0,  # 'б'\n        10: 0,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 0,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 0,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 0,  # 'л'\n        12: 0,  # 'м'\n        5: 0,  # 'н'\n        1: 0,  # 'о'\n        15: 0,  # 'п'\n        9: 0,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 0,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    61: {  # 'Ь'\n        37: 0,  # 'А'\n        44: 1,  # 'Б'\n        33: 1,  # 'В'\n        46: 0,  # 'Г'\n        41: 1,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 0,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 1,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 1,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 1,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 1,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 1,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 0,  # 'а'\n        21: 0,  # 'б'\n        10: 0,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 0,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 0,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 0,  # 'л'\n        12: 0,  # 'м'\n        5: 0,  # 'н'\n        1: 0,  # 'о'\n        15: 0,  # 'п'\n        9: 0,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 0,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    47: {  # 'Э'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 1,  # 'В'\n        46: 0,  # 'Г'\n        41: 1,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 1,  # 'Й'\n        36: 1,  # 'К'\n        49: 1,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 0,  # 'О'\n        35: 1,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 1,  # 'а'\n        21: 1,  # 'б'\n        10: 2,  # 'в'\n        19: 1,  # 'г'\n        13: 2,  # 'д'\n        2: 0,  # 'е'\n        24: 1,  # 'ж'\n        20: 0,  # 'з'\n        4: 0,  # 'и'\n        23: 2,  # 'й'\n        11: 2,  # 'к'\n        8: 2,  # 'л'\n        12: 2,  # 'м'\n        5: 2,  # 'н'\n        1: 0,  # 'о'\n        15: 1,  # 'п'\n        9: 2,  # 'р'\n        7: 1,  # 'с'\n        6: 3,  # 'т'\n        14: 1,  # 'у'\n        39: 1,  # 'ф'\n        26: 1,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 1,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    59: {  # 'Ю'\n        37: 1,  # 'А'\n        44: 1,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 1,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 1,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 1,  # 'Р'\n        32: 0,  # 'С'\n        40: 1,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 1,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 0,  # 'а'\n        21: 1,  # 'б'\n        10: 0,  # 'в'\n        19: 1,  # 'г'\n        13: 1,  # 'д'\n        2: 0,  # 'е'\n        24: 1,  # 'ж'\n        20: 0,  # 'з'\n        4: 0,  # 'и'\n        23: 0,  # 'й'\n        11: 1,  # 'к'\n        8: 2,  # 'л'\n        12: 1,  # 'м'\n        5: 2,  # 'н'\n        1: 0,  # 'о'\n        15: 1,  # 'п'\n        9: 1,  # 'р'\n        7: 1,  # 'с'\n        6: 0,  # 'т'\n        14: 0,  # 'у'\n        39: 0,  # 'ф'\n        26: 1,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    43: {  # 'Я'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 1,  # 'В'\n        46: 1,  # 'Г'\n        41: 0,  # 'Д'\n        48: 1,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 1,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 1,  # 'С'\n        40: 1,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 1,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 1,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 1,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 1,  # 'Ю'\n        43: 1,  # 'Я'\n        3: 0,  # 'а'\n        21: 1,  # 'б'\n        10: 1,  # 'в'\n        19: 1,  # 'г'\n        13: 1,  # 'д'\n        2: 0,  # 'е'\n        24: 0,  # 'ж'\n        20: 1,  # 'з'\n        4: 0,  # 'и'\n        23: 1,  # 'й'\n        11: 1,  # 'к'\n        8: 1,  # 'л'\n        12: 1,  # 'м'\n        5: 2,  # 'н'\n        1: 0,  # 'о'\n        15: 1,  # 'п'\n        9: 1,  # 'р'\n        7: 1,  # 'с'\n        6: 0,  # 'т'\n        14: 0,  # 'у'\n        39: 0,  # 'ф'\n        26: 1,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 1,  # 'ш'\n        29: 1,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    3: {  # 'а'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 1,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 1,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 2,  # 'а'\n        21: 3,  # 'б'\n        10: 3,  # 'в'\n        19: 3,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 3,  # 'ж'\n        20: 3,  # 'з'\n        4: 3,  # 'и'\n        23: 3,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 3,  # 'м'\n        5: 3,  # 'н'\n        1: 2,  # 'о'\n        15: 3,  # 'п'\n        9: 3,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 3,  # 'у'\n        39: 2,  # 'ф'\n        26: 3,  # 'х'\n        28: 3,  # 'ц'\n        22: 3,  # 'ч'\n        25: 3,  # 'ш'\n        29: 3,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 2,  # 'э'\n        27: 3,  # 'ю'\n        16: 3,  # 'я'\n    },\n    21: {  # 'б'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 1,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 2,  # 'б'\n        10: 2,  # 'в'\n        19: 1,  # 'г'\n        13: 2,  # 'д'\n        2: 3,  # 'е'\n        24: 2,  # 'ж'\n        20: 1,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 2,  # 'к'\n        8: 3,  # 'л'\n        12: 2,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 1,  # 'п'\n        9: 3,  # 'р'\n        7: 3,  # 'с'\n        6: 2,  # 'т'\n        14: 3,  # 'у'\n        39: 0,  # 'ф'\n        26: 2,  # 'х'\n        28: 1,  # 'ц'\n        22: 1,  # 'ч'\n        25: 2,  # 'ш'\n        29: 3,  # 'щ'\n        54: 2,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 2,  # 'ь'\n        30: 1,  # 'э'\n        27: 2,  # 'ю'\n        16: 3,  # 'я'\n    },\n    10: {  # 'в'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 2,  # 'б'\n        10: 2,  # 'в'\n        19: 2,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 1,  # 'ж'\n        20: 3,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 2,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 3,  # 'п'\n        9: 3,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 3,  # 'у'\n        39: 1,  # 'ф'\n        26: 2,  # 'х'\n        28: 2,  # 'ц'\n        22: 2,  # 'ч'\n        25: 3,  # 'ш'\n        29: 2,  # 'щ'\n        54: 2,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 3,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 3,  # 'я'\n    },\n    19: {  # 'г'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 1,  # 'б'\n        10: 2,  # 'в'\n        19: 1,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 0,  # 'ж'\n        20: 1,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 2,  # 'к'\n        8: 3,  # 'л'\n        12: 2,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 3,  # 'р'\n        7: 2,  # 'с'\n        6: 2,  # 'т'\n        14: 3,  # 'у'\n        39: 1,  # 'ф'\n        26: 1,  # 'х'\n        28: 1,  # 'ц'\n        22: 2,  # 'ч'\n        25: 1,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 1,  # 'ы'\n        17: 1,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 0,  # 'я'\n    },\n    13: {  # 'д'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 2,  # 'б'\n        10: 3,  # 'в'\n        19: 2,  # 'г'\n        13: 2,  # 'д'\n        2: 3,  # 'е'\n        24: 2,  # 'ж'\n        20: 2,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 2,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 2,  # 'п'\n        9: 3,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 3,  # 'у'\n        39: 1,  # 'ф'\n        26: 2,  # 'х'\n        28: 3,  # 'ц'\n        22: 2,  # 'ч'\n        25: 2,  # 'ш'\n        29: 1,  # 'щ'\n        54: 2,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 3,  # 'ь'\n        30: 1,  # 'э'\n        27: 2,  # 'ю'\n        16: 3,  # 'я'\n    },\n    2: {  # 'е'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 2,  # 'а'\n        21: 3,  # 'б'\n        10: 3,  # 'в'\n        19: 3,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 3,  # 'ж'\n        20: 3,  # 'з'\n        4: 2,  # 'и'\n        23: 3,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 3,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 3,  # 'п'\n        9: 3,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 2,  # 'у'\n        39: 2,  # 'ф'\n        26: 3,  # 'х'\n        28: 3,  # 'ц'\n        22: 3,  # 'ч'\n        25: 3,  # 'ш'\n        29: 3,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 1,  # 'э'\n        27: 2,  # 'ю'\n        16: 3,  # 'я'\n    },\n    24: {  # 'ж'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 2,  # 'б'\n        10: 1,  # 'в'\n        19: 2,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 2,  # 'ж'\n        20: 1,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 2,  # 'к'\n        8: 2,  # 'л'\n        12: 1,  # 'м'\n        5: 3,  # 'н'\n        1: 2,  # 'о'\n        15: 1,  # 'п'\n        9: 2,  # 'р'\n        7: 2,  # 'с'\n        6: 1,  # 'т'\n        14: 3,  # 'у'\n        39: 1,  # 'ф'\n        26: 0,  # 'х'\n        28: 1,  # 'ц'\n        22: 2,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 1,  # 'ы'\n        17: 2,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 1,  # 'я'\n    },\n    20: {  # 'з'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 3,  # 'б'\n        10: 3,  # 'в'\n        19: 3,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 2,  # 'ж'\n        20: 2,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 3,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 3,  # 'р'\n        7: 2,  # 'с'\n        6: 2,  # 'т'\n        14: 3,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 1,  # 'ц'\n        22: 2,  # 'ч'\n        25: 1,  # 'ш'\n        29: 0,  # 'щ'\n        54: 2,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 2,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 3,  # 'я'\n    },\n    4: {  # 'и'\n        37: 1,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 1,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 3,  # 'б'\n        10: 3,  # 'в'\n        19: 3,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 3,  # 'ж'\n        20: 3,  # 'з'\n        4: 3,  # 'и'\n        23: 3,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 3,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 3,  # 'п'\n        9: 3,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 2,  # 'у'\n        39: 2,  # 'ф'\n        26: 3,  # 'х'\n        28: 3,  # 'ц'\n        22: 3,  # 'ч'\n        25: 3,  # 'ш'\n        29: 3,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 2,  # 'э'\n        27: 3,  # 'ю'\n        16: 3,  # 'я'\n    },\n    23: {  # 'й'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 1,  # 'а'\n        21: 1,  # 'б'\n        10: 1,  # 'в'\n        19: 2,  # 'г'\n        13: 3,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 2,  # 'з'\n        4: 1,  # 'и'\n        23: 0,  # 'й'\n        11: 2,  # 'к'\n        8: 2,  # 'л'\n        12: 2,  # 'м'\n        5: 3,  # 'н'\n        1: 2,  # 'о'\n        15: 1,  # 'п'\n        9: 2,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 1,  # 'у'\n        39: 2,  # 'ф'\n        26: 1,  # 'х'\n        28: 2,  # 'ц'\n        22: 3,  # 'ч'\n        25: 2,  # 'ш'\n        29: 1,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 2,  # 'я'\n    },\n    11: {  # 'к'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 1,  # 'б'\n        10: 3,  # 'в'\n        19: 1,  # 'г'\n        13: 1,  # 'д'\n        2: 3,  # 'е'\n        24: 2,  # 'ж'\n        20: 2,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 2,  # 'к'\n        8: 3,  # 'л'\n        12: 1,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 3,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 3,  # 'у'\n        39: 1,  # 'ф'\n        26: 2,  # 'х'\n        28: 2,  # 'ц'\n        22: 1,  # 'ч'\n        25: 2,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 1,  # 'ы'\n        17: 1,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 1,  # 'я'\n    },\n    8: {  # 'л'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 2,  # 'б'\n        10: 2,  # 'в'\n        19: 3,  # 'г'\n        13: 2,  # 'д'\n        2: 3,  # 'е'\n        24: 3,  # 'ж'\n        20: 2,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 2,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 2,  # 'п'\n        9: 1,  # 'р'\n        7: 3,  # 'с'\n        6: 2,  # 'т'\n        14: 3,  # 'у'\n        39: 2,  # 'ф'\n        26: 2,  # 'х'\n        28: 1,  # 'ц'\n        22: 3,  # 'ч'\n        25: 2,  # 'ш'\n        29: 1,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 3,  # 'ь'\n        30: 1,  # 'э'\n        27: 3,  # 'ю'\n        16: 3,  # 'я'\n    },\n    12: {  # 'м'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 2,  # 'б'\n        10: 2,  # 'в'\n        19: 2,  # 'г'\n        13: 1,  # 'д'\n        2: 3,  # 'е'\n        24: 1,  # 'ж'\n        20: 1,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 2,  # 'к'\n        8: 3,  # 'л'\n        12: 2,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 2,  # 'п'\n        9: 2,  # 'р'\n        7: 3,  # 'с'\n        6: 2,  # 'т'\n        14: 3,  # 'у'\n        39: 2,  # 'ф'\n        26: 2,  # 'х'\n        28: 2,  # 'ц'\n        22: 2,  # 'ч'\n        25: 1,  # 'ш'\n        29: 1,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 2,  # 'ь'\n        30: 2,  # 'э'\n        27: 1,  # 'ю'\n        16: 3,  # 'я'\n    },\n    5: {  # 'н'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 2,  # 'б'\n        10: 2,  # 'в'\n        19: 3,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 2,  # 'ж'\n        20: 2,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 3,  # 'к'\n        8: 2,  # 'л'\n        12: 1,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 1,  # 'п'\n        9: 2,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 3,  # 'у'\n        39: 2,  # 'ф'\n        26: 2,  # 'х'\n        28: 3,  # 'ц'\n        22: 3,  # 'ч'\n        25: 2,  # 'ш'\n        29: 2,  # 'щ'\n        54: 1,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 3,  # 'ь'\n        30: 1,  # 'э'\n        27: 3,  # 'ю'\n        16: 3,  # 'я'\n    },\n    1: {  # 'о'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 2,  # 'а'\n        21: 3,  # 'б'\n        10: 3,  # 'в'\n        19: 3,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 3,  # 'ж'\n        20: 3,  # 'з'\n        4: 3,  # 'и'\n        23: 3,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 3,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 3,  # 'п'\n        9: 3,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 2,  # 'у'\n        39: 2,  # 'ф'\n        26: 3,  # 'х'\n        28: 2,  # 'ц'\n        22: 3,  # 'ч'\n        25: 3,  # 'ш'\n        29: 3,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 2,  # 'э'\n        27: 3,  # 'ю'\n        16: 3,  # 'я'\n    },\n    15: {  # 'п'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 1,  # 'б'\n        10: 0,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 3,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 2,  # 'к'\n        8: 3,  # 'л'\n        12: 1,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 2,  # 'п'\n        9: 3,  # 'р'\n        7: 2,  # 'с'\n        6: 2,  # 'т'\n        14: 3,  # 'у'\n        39: 1,  # 'ф'\n        26: 0,  # 'х'\n        28: 2,  # 'ц'\n        22: 2,  # 'ч'\n        25: 1,  # 'ш'\n        29: 1,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 2,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 3,  # 'я'\n    },\n    9: {  # 'р'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 2,  # 'б'\n        10: 3,  # 'в'\n        19: 3,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 3,  # 'ж'\n        20: 2,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 3,  # 'к'\n        8: 2,  # 'л'\n        12: 3,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 2,  # 'п'\n        9: 2,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 3,  # 'у'\n        39: 2,  # 'ф'\n        26: 3,  # 'х'\n        28: 2,  # 'ц'\n        22: 2,  # 'ч'\n        25: 3,  # 'ш'\n        29: 2,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 3,  # 'ь'\n        30: 2,  # 'э'\n        27: 2,  # 'ю'\n        16: 3,  # 'я'\n    },\n    7: {  # 'с'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 1,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 2,  # 'б'\n        10: 3,  # 'в'\n        19: 2,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 2,  # 'ж'\n        20: 2,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 3,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 3,  # 'п'\n        9: 3,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 3,  # 'у'\n        39: 2,  # 'ф'\n        26: 3,  # 'х'\n        28: 2,  # 'ц'\n        22: 3,  # 'ч'\n        25: 2,  # 'ш'\n        29: 1,  # 'щ'\n        54: 2,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 3,  # 'ь'\n        30: 2,  # 'э'\n        27: 3,  # 'ю'\n        16: 3,  # 'я'\n    },\n    6: {  # 'т'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 2,  # 'б'\n        10: 3,  # 'в'\n        19: 2,  # 'г'\n        13: 2,  # 'д'\n        2: 3,  # 'е'\n        24: 1,  # 'ж'\n        20: 1,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 2,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 2,  # 'п'\n        9: 3,  # 'р'\n        7: 3,  # 'с'\n        6: 2,  # 'т'\n        14: 3,  # 'у'\n        39: 2,  # 'ф'\n        26: 2,  # 'х'\n        28: 2,  # 'ц'\n        22: 2,  # 'ч'\n        25: 2,  # 'ш'\n        29: 2,  # 'щ'\n        54: 2,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 3,  # 'ь'\n        30: 2,  # 'э'\n        27: 2,  # 'ю'\n        16: 3,  # 'я'\n    },\n    14: {  # 'у'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 2,  # 'а'\n        21: 3,  # 'б'\n        10: 3,  # 'в'\n        19: 3,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 3,  # 'ж'\n        20: 3,  # 'з'\n        4: 2,  # 'и'\n        23: 2,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 3,  # 'м'\n        5: 3,  # 'н'\n        1: 2,  # 'о'\n        15: 3,  # 'п'\n        9: 3,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 1,  # 'у'\n        39: 2,  # 'ф'\n        26: 3,  # 'х'\n        28: 2,  # 'ц'\n        22: 3,  # 'ч'\n        25: 3,  # 'ш'\n        29: 3,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 2,  # 'э'\n        27: 3,  # 'ю'\n        16: 2,  # 'я'\n    },\n    39: {  # 'ф'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 1,  # 'б'\n        10: 0,  # 'в'\n        19: 1,  # 'г'\n        13: 0,  # 'д'\n        2: 3,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 1,  # 'к'\n        8: 2,  # 'л'\n        12: 1,  # 'м'\n        5: 1,  # 'н'\n        1: 3,  # 'о'\n        15: 1,  # 'п'\n        9: 2,  # 'р'\n        7: 2,  # 'с'\n        6: 2,  # 'т'\n        14: 2,  # 'у'\n        39: 2,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 1,  # 'ч'\n        25: 1,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 2,  # 'ы'\n        17: 1,  # 'ь'\n        30: 2,  # 'э'\n        27: 1,  # 'ю'\n        16: 1,  # 'я'\n    },\n    26: {  # 'х'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 0,  # 'б'\n        10: 3,  # 'в'\n        19: 1,  # 'г'\n        13: 1,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 1,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 1,  # 'к'\n        8: 2,  # 'л'\n        12: 2,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 1,  # 'п'\n        9: 3,  # 'р'\n        7: 2,  # 'с'\n        6: 2,  # 'т'\n        14: 2,  # 'у'\n        39: 1,  # 'ф'\n        26: 1,  # 'х'\n        28: 1,  # 'ц'\n        22: 1,  # 'ч'\n        25: 2,  # 'ш'\n        29: 0,  # 'щ'\n        54: 1,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 1,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 0,  # 'я'\n    },\n    28: {  # 'ц'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 1,  # 'б'\n        10: 2,  # 'в'\n        19: 1,  # 'г'\n        13: 1,  # 'д'\n        2: 3,  # 'е'\n        24: 0,  # 'ж'\n        20: 1,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 2,  # 'к'\n        8: 1,  # 'л'\n        12: 1,  # 'м'\n        5: 1,  # 'н'\n        1: 3,  # 'о'\n        15: 0,  # 'п'\n        9: 1,  # 'р'\n        7: 0,  # 'с'\n        6: 1,  # 'т'\n        14: 3,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 1,  # 'ц'\n        22: 0,  # 'ч'\n        25: 1,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 3,  # 'ы'\n        17: 1,  # 'ь'\n        30: 0,  # 'э'\n        27: 1,  # 'ю'\n        16: 0,  # 'я'\n    },\n    22: {  # 'ч'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 1,  # 'б'\n        10: 1,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 3,  # 'е'\n        24: 1,  # 'ж'\n        20: 0,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 3,  # 'к'\n        8: 2,  # 'л'\n        12: 1,  # 'м'\n        5: 3,  # 'н'\n        1: 2,  # 'о'\n        15: 0,  # 'п'\n        9: 2,  # 'р'\n        7: 1,  # 'с'\n        6: 3,  # 'т'\n        14: 3,  # 'у'\n        39: 1,  # 'ф'\n        26: 1,  # 'х'\n        28: 0,  # 'ц'\n        22: 1,  # 'ч'\n        25: 2,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 3,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    25: {  # 'ш'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 1,  # 'б'\n        10: 2,  # 'в'\n        19: 1,  # 'г'\n        13: 0,  # 'д'\n        2: 3,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 2,  # 'м'\n        5: 3,  # 'н'\n        1: 3,  # 'о'\n        15: 2,  # 'п'\n        9: 2,  # 'р'\n        7: 1,  # 'с'\n        6: 2,  # 'т'\n        14: 3,  # 'у'\n        39: 2,  # 'ф'\n        26: 1,  # 'х'\n        28: 1,  # 'ц'\n        22: 1,  # 'ч'\n        25: 1,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 3,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 0,  # 'я'\n    },\n    29: {  # 'щ'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 3,  # 'а'\n        21: 0,  # 'б'\n        10: 1,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 3,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 3,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 0,  # 'л'\n        12: 1,  # 'м'\n        5: 2,  # 'н'\n        1: 1,  # 'о'\n        15: 0,  # 'п'\n        9: 2,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 2,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 2,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 0,  # 'я'\n    },\n    54: {  # 'ъ'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 0,  # 'а'\n        21: 0,  # 'б'\n        10: 0,  # 'в'\n        19: 0,  # 'г'\n        13: 0,  # 'д'\n        2: 2,  # 'е'\n        24: 0,  # 'ж'\n        20: 0,  # 'з'\n        4: 0,  # 'и'\n        23: 0,  # 'й'\n        11: 0,  # 'к'\n        8: 0,  # 'л'\n        12: 0,  # 'м'\n        5: 0,  # 'н'\n        1: 0,  # 'о'\n        15: 0,  # 'п'\n        9: 0,  # 'р'\n        7: 0,  # 'с'\n        6: 0,  # 'т'\n        14: 0,  # 'у'\n        39: 0,  # 'ф'\n        26: 0,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 0,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 1,  # 'ю'\n        16: 2,  # 'я'\n    },\n    18: {  # 'ы'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 0,  # 'а'\n        21: 3,  # 'б'\n        10: 3,  # 'в'\n        19: 2,  # 'г'\n        13: 2,  # 'д'\n        2: 3,  # 'е'\n        24: 2,  # 'ж'\n        20: 2,  # 'з'\n        4: 2,  # 'и'\n        23: 3,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 3,  # 'м'\n        5: 3,  # 'н'\n        1: 1,  # 'о'\n        15: 3,  # 'п'\n        9: 3,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 1,  # 'у'\n        39: 0,  # 'ф'\n        26: 3,  # 'х'\n        28: 2,  # 'ц'\n        22: 3,  # 'ч'\n        25: 3,  # 'ш'\n        29: 2,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 0,  # 'ю'\n        16: 2,  # 'я'\n    },\n    17: {  # 'ь'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 0,  # 'а'\n        21: 2,  # 'б'\n        10: 2,  # 'в'\n        19: 2,  # 'г'\n        13: 2,  # 'д'\n        2: 3,  # 'е'\n        24: 1,  # 'ж'\n        20: 3,  # 'з'\n        4: 2,  # 'и'\n        23: 0,  # 'й'\n        11: 3,  # 'к'\n        8: 0,  # 'л'\n        12: 3,  # 'м'\n        5: 3,  # 'н'\n        1: 2,  # 'о'\n        15: 2,  # 'п'\n        9: 1,  # 'р'\n        7: 3,  # 'с'\n        6: 2,  # 'т'\n        14: 0,  # 'у'\n        39: 2,  # 'ф'\n        26: 1,  # 'х'\n        28: 2,  # 'ц'\n        22: 2,  # 'ч'\n        25: 3,  # 'ш'\n        29: 2,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 1,  # 'э'\n        27: 3,  # 'ю'\n        16: 3,  # 'я'\n    },\n    30: {  # 'э'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 1,  # 'М'\n        31: 1,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 1,  # 'Р'\n        32: 1,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 1,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 0,  # 'а'\n        21: 1,  # 'б'\n        10: 1,  # 'в'\n        19: 1,  # 'г'\n        13: 2,  # 'д'\n        2: 1,  # 'е'\n        24: 0,  # 'ж'\n        20: 1,  # 'з'\n        4: 0,  # 'и'\n        23: 2,  # 'й'\n        11: 2,  # 'к'\n        8: 2,  # 'л'\n        12: 2,  # 'м'\n        5: 2,  # 'н'\n        1: 0,  # 'о'\n        15: 2,  # 'п'\n        9: 2,  # 'р'\n        7: 2,  # 'с'\n        6: 3,  # 'т'\n        14: 1,  # 'у'\n        39: 2,  # 'ф'\n        26: 1,  # 'х'\n        28: 0,  # 'ц'\n        22: 0,  # 'ч'\n        25: 1,  # 'ш'\n        29: 0,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 1,  # 'э'\n        27: 1,  # 'ю'\n        16: 1,  # 'я'\n    },\n    27: {  # 'ю'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 2,  # 'а'\n        21: 3,  # 'б'\n        10: 1,  # 'в'\n        19: 2,  # 'г'\n        13: 3,  # 'д'\n        2: 1,  # 'е'\n        24: 2,  # 'ж'\n        20: 2,  # 'з'\n        4: 1,  # 'и'\n        23: 1,  # 'й'\n        11: 2,  # 'к'\n        8: 2,  # 'л'\n        12: 2,  # 'м'\n        5: 2,  # 'н'\n        1: 1,  # 'о'\n        15: 2,  # 'п'\n        9: 2,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 0,  # 'у'\n        39: 1,  # 'ф'\n        26: 2,  # 'х'\n        28: 2,  # 'ц'\n        22: 2,  # 'ч'\n        25: 2,  # 'ш'\n        29: 3,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 1,  # 'э'\n        27: 2,  # 'ю'\n        16: 1,  # 'я'\n    },\n    16: {  # 'я'\n        37: 0,  # 'А'\n        44: 0,  # 'Б'\n        33: 0,  # 'В'\n        46: 0,  # 'Г'\n        41: 0,  # 'Д'\n        48: 0,  # 'Е'\n        56: 0,  # 'Ж'\n        51: 0,  # 'З'\n        42: 0,  # 'И'\n        60: 0,  # 'Й'\n        36: 0,  # 'К'\n        49: 0,  # 'Л'\n        38: 0,  # 'М'\n        31: 0,  # 'Н'\n        34: 0,  # 'О'\n        35: 0,  # 'П'\n        45: 0,  # 'Р'\n        32: 0,  # 'С'\n        40: 0,  # 'Т'\n        52: 0,  # 'У'\n        53: 0,  # 'Ф'\n        55: 0,  # 'Х'\n        58: 0,  # 'Ц'\n        50: 0,  # 'Ч'\n        57: 0,  # 'Ш'\n        63: 0,  # 'Щ'\n        62: 0,  # 'Ы'\n        61: 0,  # 'Ь'\n        47: 0,  # 'Э'\n        59: 0,  # 'Ю'\n        43: 0,  # 'Я'\n        3: 0,  # 'а'\n        21: 2,  # 'б'\n        10: 3,  # 'в'\n        19: 2,  # 'г'\n        13: 3,  # 'д'\n        2: 3,  # 'е'\n        24: 3,  # 'ж'\n        20: 3,  # 'з'\n        4: 2,  # 'и'\n        23: 2,  # 'й'\n        11: 3,  # 'к'\n        8: 3,  # 'л'\n        12: 3,  # 'м'\n        5: 3,  # 'н'\n        1: 0,  # 'о'\n        15: 2,  # 'п'\n        9: 2,  # 'р'\n        7: 3,  # 'с'\n        6: 3,  # 'т'\n        14: 1,  # 'у'\n        39: 1,  # 'ф'\n        26: 3,  # 'х'\n        28: 2,  # 'ц'\n        22: 2,  # 'ч'\n        25: 2,  # 'ш'\n        29: 3,  # 'щ'\n        54: 0,  # 'ъ'\n        18: 0,  # 'ы'\n        17: 0,  # 'ь'\n        30: 0,  # 'э'\n        27: 2,  # 'ю'\n        16: 2,  # 'я'\n    },\n}\n\n# 255: Undefined characters that did not exist in training text\n# 254: Carriage/Return\n# 253: symbol (punctuation) that does not belong to word\n# 252: 0 - 9\n# 251: Control characters\n\n# Character Mapping Table(s):\nIBM866_RUSSIAN_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 142,  # 'A'\n     66: 143,  # 'B'\n     67: 144,  # 'C'\n     68: 145,  # 'D'\n     69: 146,  # 'E'\n     70: 147,  # 'F'\n     71: 148,  # 'G'\n     72: 149,  # 'H'\n     73: 150,  # 'I'\n     74: 151,  # 'J'\n     75: 152,  # 'K'\n     76: 74,  # 'L'\n     77: 153,  # 'M'\n     78: 75,  # 'N'\n     79: 154,  # 'O'\n     80: 155,  # 'P'\n     81: 156,  # 'Q'\n     82: 157,  # 'R'\n     83: 158,  # 'S'\n     84: 159,  # 'T'\n     85: 160,  # 'U'\n     86: 161,  # 'V'\n     87: 162,  # 'W'\n     88: 163,  # 'X'\n     89: 164,  # 'Y'\n     90: 165,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 71,  # 'a'\n     98: 172,  # 'b'\n     99: 66,  # 'c'\n     100: 173,  # 'd'\n     101: 65,  # 'e'\n     102: 174,  # 'f'\n     103: 76,  # 'g'\n     104: 175,  # 'h'\n     105: 64,  # 'i'\n     106: 176,  # 'j'\n     107: 177,  # 'k'\n     108: 77,  # 'l'\n     109: 72,  # 'm'\n     110: 178,  # 'n'\n     111: 69,  # 'o'\n     112: 67,  # 'p'\n     113: 179,  # 'q'\n     114: 78,  # 'r'\n     115: 73,  # 's'\n     116: 180,  # 't'\n     117: 181,  # 'u'\n     118: 79,  # 'v'\n     119: 182,  # 'w'\n     120: 183,  # 'x'\n     121: 184,  # 'y'\n     122: 185,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 37,  # 'А'\n     129: 44,  # 'Б'\n     130: 33,  # 'В'\n     131: 46,  # 'Г'\n     132: 41,  # 'Д'\n     133: 48,  # 'Е'\n     134: 56,  # 'Ж'\n     135: 51,  # 'З'\n     136: 42,  # 'И'\n     137: 60,  # 'Й'\n     138: 36,  # 'К'\n     139: 49,  # 'Л'\n     140: 38,  # 'М'\n     141: 31,  # 'Н'\n     142: 34,  # 'О'\n     143: 35,  # 'П'\n     144: 45,  # 'Р'\n     145: 32,  # 'С'\n     146: 40,  # 'Т'\n     147: 52,  # 'У'\n     148: 53,  # 'Ф'\n     149: 55,  # 'Х'\n     150: 58,  # 'Ц'\n     151: 50,  # 'Ч'\n     152: 57,  # 'Ш'\n     153: 63,  # 'Щ'\n     154: 70,  # 'Ъ'\n     155: 62,  # 'Ы'\n     156: 61,  # 'Ь'\n     157: 47,  # 'Э'\n     158: 59,  # 'Ю'\n     159: 43,  # 'Я'\n     160: 3,  # 'а'\n     161: 21,  # 'б'\n     162: 10,  # 'в'\n     163: 19,  # 'г'\n     164: 13,  # 'д'\n     165: 2,  # 'е'\n     166: 24,  # 'ж'\n     167: 20,  # 'з'\n     168: 4,  # 'и'\n     169: 23,  # 'й'\n     170: 11,  # 'к'\n     171: 8,  # 'л'\n     172: 12,  # 'м'\n     173: 5,  # 'н'\n     174: 1,  # 'о'\n     175: 15,  # 'п'\n     176: 191,  # '░'\n     177: 192,  # '▒'\n     178: 193,  # '▓'\n     179: 194,  # '│'\n     180: 195,  # '┤'\n     181: 196,  # '╡'\n     182: 197,  # '╢'\n     183: 198,  # '╖'\n     184: 199,  # '╕'\n     185: 200,  # '╣'\n     186: 201,  # '║'\n     187: 202,  # '╗'\n     188: 203,  # '╝'\n     189: 204,  # '╜'\n     190: 205,  # '╛'\n     191: 206,  # '┐'\n     192: 207,  # '└'\n     193: 208,  # '┴'\n     194: 209,  # '┬'\n     195: 210,  # '├'\n     196: 211,  # '─'\n     197: 212,  # '┼'\n     198: 213,  # '╞'\n     199: 214,  # '╟'\n     200: 215,  # '╚'\n     201: 216,  # '╔'\n     202: 217,  # '╩'\n     203: 218,  # '╦'\n     204: 219,  # '╠'\n     205: 220,  # '═'\n     206: 221,  # '╬'\n     207: 222,  # '╧'\n     208: 223,  # '╨'\n     209: 224,  # '╤'\n     210: 225,  # '╥'\n     211: 226,  # '╙'\n     212: 227,  # '╘'\n     213: 228,  # '╒'\n     214: 229,  # '╓'\n     215: 230,  # '╫'\n     216: 231,  # '╪'\n     217: 232,  # '┘'\n     218: 233,  # '┌'\n     219: 234,  # '█'\n     220: 235,  # '▄'\n     221: 236,  # '▌'\n     222: 237,  # '▐'\n     223: 238,  # '▀'\n     224: 9,  # 'р'\n     225: 7,  # 'с'\n     226: 6,  # 'т'\n     227: 14,  # 'у'\n     228: 39,  # 'ф'\n     229: 26,  # 'х'\n     230: 28,  # 'ц'\n     231: 22,  # 'ч'\n     232: 25,  # 'ш'\n     233: 29,  # 'щ'\n     234: 54,  # 'ъ'\n     235: 18,  # 'ы'\n     236: 17,  # 'ь'\n     237: 30,  # 'э'\n     238: 27,  # 'ю'\n     239: 16,  # 'я'\n     240: 239,  # 'Ё'\n     241: 68,  # 'ё'\n     242: 240,  # 'Є'\n     243: 241,  # 'є'\n     244: 242,  # 'Ї'\n     245: 243,  # 'ї'\n     246: 244,  # 'Ў'\n     247: 245,  # 'ў'\n     248: 246,  # '°'\n     249: 247,  # '∙'\n     250: 248,  # '·'\n     251: 249,  # '√'\n     252: 250,  # '№'\n     253: 251,  # '¤'\n     254: 252,  # '■'\n     255: 255,  # '\\xa0'\n}\n\nIBM866_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='IBM866',\n                                              language='Russian',\n                                              char_to_order_map=IBM866_RUSSIAN_CHAR_TO_ORDER,\n                                              language_model=RUSSIAN_LANG_MODEL,\n                                              typical_positive_ratio=0.976601,\n                                              keep_ascii_letters=False,\n                                              alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё')\n\nWINDOWS_1251_RUSSIAN_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 142,  # 'A'\n     66: 143,  # 'B'\n     67: 144,  # 'C'\n     68: 145,  # 'D'\n     69: 146,  # 'E'\n     70: 147,  # 'F'\n     71: 148,  # 'G'\n     72: 149,  # 'H'\n     73: 150,  # 'I'\n     74: 151,  # 'J'\n     75: 152,  # 'K'\n     76: 74,  # 'L'\n     77: 153,  # 'M'\n     78: 75,  # 'N'\n     79: 154,  # 'O'\n     80: 155,  # 'P'\n     81: 156,  # 'Q'\n     82: 157,  # 'R'\n     83: 158,  # 'S'\n     84: 159,  # 'T'\n     85: 160,  # 'U'\n     86: 161,  # 'V'\n     87: 162,  # 'W'\n     88: 163,  # 'X'\n     89: 164,  # 'Y'\n     90: 165,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 71,  # 'a'\n     98: 172,  # 'b'\n     99: 66,  # 'c'\n     100: 173,  # 'd'\n     101: 65,  # 'e'\n     102: 174,  # 'f'\n     103: 76,  # 'g'\n     104: 175,  # 'h'\n     105: 64,  # 'i'\n     106: 176,  # 'j'\n     107: 177,  # 'k'\n     108: 77,  # 'l'\n     109: 72,  # 'm'\n     110: 178,  # 'n'\n     111: 69,  # 'o'\n     112: 67,  # 'p'\n     113: 179,  # 'q'\n     114: 78,  # 'r'\n     115: 73,  # 's'\n     116: 180,  # 't'\n     117: 181,  # 'u'\n     118: 79,  # 'v'\n     119: 182,  # 'w'\n     120: 183,  # 'x'\n     121: 184,  # 'y'\n     122: 185,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 191,  # 'Ђ'\n     129: 192,  # 'Ѓ'\n     130: 193,  # '‚'\n     131: 194,  # 'ѓ'\n     132: 195,  # '„'\n     133: 196,  # '…'\n     134: 197,  # '†'\n     135: 198,  # '‡'\n     136: 199,  # '€'\n     137: 200,  # '‰'\n     138: 201,  # 'Љ'\n     139: 202,  # '‹'\n     140: 203,  # 'Њ'\n     141: 204,  # 'Ќ'\n     142: 205,  # 'Ћ'\n     143: 206,  # 'Џ'\n     144: 207,  # 'ђ'\n     145: 208,  # '‘'\n     146: 209,  # '’'\n     147: 210,  # '“'\n     148: 211,  # '”'\n     149: 212,  # '•'\n     150: 213,  # '–'\n     151: 214,  # '—'\n     152: 215,  # None\n     153: 216,  # '™'\n     154: 217,  # 'љ'\n     155: 218,  # '›'\n     156: 219,  # 'њ'\n     157: 220,  # 'ќ'\n     158: 221,  # 'ћ'\n     159: 222,  # 'џ'\n     160: 223,  # '\\xa0'\n     161: 224,  # 'Ў'\n     162: 225,  # 'ў'\n     163: 226,  # 'Ј'\n     164: 227,  # '¤'\n     165: 228,  # 'Ґ'\n     166: 229,  # '¦'\n     167: 230,  # '§'\n     168: 231,  # 'Ё'\n     169: 232,  # '©'\n     170: 233,  # 'Є'\n     171: 234,  # '«'\n     172: 235,  # '¬'\n     173: 236,  # '\\xad'\n     174: 237,  # '®'\n     175: 238,  # 'Ї'\n     176: 239,  # '°'\n     177: 240,  # '±'\n     178: 241,  # 'І'\n     179: 242,  # 'і'\n     180: 243,  # 'ґ'\n     181: 244,  # 'µ'\n     182: 245,  # '¶'\n     183: 246,  # '·'\n     184: 68,  # 'ё'\n     185: 247,  # '№'\n     186: 248,  # 'є'\n     187: 249,  # '»'\n     188: 250,  # 'ј'\n     189: 251,  # 'Ѕ'\n     190: 252,  # 'ѕ'\n     191: 253,  # 'ї'\n     192: 37,  # 'А'\n     193: 44,  # 'Б'\n     194: 33,  # 'В'\n     195: 46,  # 'Г'\n     196: 41,  # 'Д'\n     197: 48,  # 'Е'\n     198: 56,  # 'Ж'\n     199: 51,  # 'З'\n     200: 42,  # 'И'\n     201: 60,  # 'Й'\n     202: 36,  # 'К'\n     203: 49,  # 'Л'\n     204: 38,  # 'М'\n     205: 31,  # 'Н'\n     206: 34,  # 'О'\n     207: 35,  # 'П'\n     208: 45,  # 'Р'\n     209: 32,  # 'С'\n     210: 40,  # 'Т'\n     211: 52,  # 'У'\n     212: 53,  # 'Ф'\n     213: 55,  # 'Х'\n     214: 58,  # 'Ц'\n     215: 50,  # 'Ч'\n     216: 57,  # 'Ш'\n     217: 63,  # 'Щ'\n     218: 70,  # 'Ъ'\n     219: 62,  # 'Ы'\n     220: 61,  # 'Ь'\n     221: 47,  # 'Э'\n     222: 59,  # 'Ю'\n     223: 43,  # 'Я'\n     224: 3,  # 'а'\n     225: 21,  # 'б'\n     226: 10,  # 'в'\n     227: 19,  # 'г'\n     228: 13,  # 'д'\n     229: 2,  # 'е'\n     230: 24,  # 'ж'\n     231: 20,  # 'з'\n     232: 4,  # 'и'\n     233: 23,  # 'й'\n     234: 11,  # 'к'\n     235: 8,  # 'л'\n     236: 12,  # 'м'\n     237: 5,  # 'н'\n     238: 1,  # 'о'\n     239: 15,  # 'п'\n     240: 9,  # 'р'\n     241: 7,  # 'с'\n     242: 6,  # 'т'\n     243: 14,  # 'у'\n     244: 39,  # 'ф'\n     245: 26,  # 'х'\n     246: 28,  # 'ц'\n     247: 22,  # 'ч'\n     248: 25,  # 'ш'\n     249: 29,  # 'щ'\n     250: 54,  # 'ъ'\n     251: 18,  # 'ы'\n     252: 17,  # 'ь'\n     253: 30,  # 'э'\n     254: 27,  # 'ю'\n     255: 16,  # 'я'\n}\n\nWINDOWS_1251_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1251',\n                                                    language='Russian',\n                                                    char_to_order_map=WINDOWS_1251_RUSSIAN_CHAR_TO_ORDER,\n                                                    language_model=RUSSIAN_LANG_MODEL,\n                                                    typical_positive_ratio=0.976601,\n                                                    keep_ascii_letters=False,\n                                                    alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё')\n\nIBM855_RUSSIAN_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 142,  # 'A'\n     66: 143,  # 'B'\n     67: 144,  # 'C'\n     68: 145,  # 'D'\n     69: 146,  # 'E'\n     70: 147,  # 'F'\n     71: 148,  # 'G'\n     72: 149,  # 'H'\n     73: 150,  # 'I'\n     74: 151,  # 'J'\n     75: 152,  # 'K'\n     76: 74,  # 'L'\n     77: 153,  # 'M'\n     78: 75,  # 'N'\n     79: 154,  # 'O'\n     80: 155,  # 'P'\n     81: 156,  # 'Q'\n     82: 157,  # 'R'\n     83: 158,  # 'S'\n     84: 159,  # 'T'\n     85: 160,  # 'U'\n     86: 161,  # 'V'\n     87: 162,  # 'W'\n     88: 163,  # 'X'\n     89: 164,  # 'Y'\n     90: 165,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 71,  # 'a'\n     98: 172,  # 'b'\n     99: 66,  # 'c'\n     100: 173,  # 'd'\n     101: 65,  # 'e'\n     102: 174,  # 'f'\n     103: 76,  # 'g'\n     104: 175,  # 'h'\n     105: 64,  # 'i'\n     106: 176,  # 'j'\n     107: 177,  # 'k'\n     108: 77,  # 'l'\n     109: 72,  # 'm'\n     110: 178,  # 'n'\n     111: 69,  # 'o'\n     112: 67,  # 'p'\n     113: 179,  # 'q'\n     114: 78,  # 'r'\n     115: 73,  # 's'\n     116: 180,  # 't'\n     117: 181,  # 'u'\n     118: 79,  # 'v'\n     119: 182,  # 'w'\n     120: 183,  # 'x'\n     121: 184,  # 'y'\n     122: 185,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 191,  # 'ђ'\n     129: 192,  # 'Ђ'\n     130: 193,  # 'ѓ'\n     131: 194,  # 'Ѓ'\n     132: 68,  # 'ё'\n     133: 195,  # 'Ё'\n     134: 196,  # 'є'\n     135: 197,  # 'Є'\n     136: 198,  # 'ѕ'\n     137: 199,  # 'Ѕ'\n     138: 200,  # 'і'\n     139: 201,  # 'І'\n     140: 202,  # 'ї'\n     141: 203,  # 'Ї'\n     142: 204,  # 'ј'\n     143: 205,  # 'Ј'\n     144: 206,  # 'љ'\n     145: 207,  # 'Љ'\n     146: 208,  # 'њ'\n     147: 209,  # 'Њ'\n     148: 210,  # 'ћ'\n     149: 211,  # 'Ћ'\n     150: 212,  # 'ќ'\n     151: 213,  # 'Ќ'\n     152: 214,  # 'ў'\n     153: 215,  # 'Ў'\n     154: 216,  # 'џ'\n     155: 217,  # 'Џ'\n     156: 27,  # 'ю'\n     157: 59,  # 'Ю'\n     158: 54,  # 'ъ'\n     159: 70,  # 'Ъ'\n     160: 3,  # 'а'\n     161: 37,  # 'А'\n     162: 21,  # 'б'\n     163: 44,  # 'Б'\n     164: 28,  # 'ц'\n     165: 58,  # 'Ц'\n     166: 13,  # 'д'\n     167: 41,  # 'Д'\n     168: 2,  # 'е'\n     169: 48,  # 'Е'\n     170: 39,  # 'ф'\n     171: 53,  # 'Ф'\n     172: 19,  # 'г'\n     173: 46,  # 'Г'\n     174: 218,  # '«'\n     175: 219,  # '»'\n     176: 220,  # '░'\n     177: 221,  # '▒'\n     178: 222,  # '▓'\n     179: 223,  # '│'\n     180: 224,  # '┤'\n     181: 26,  # 'х'\n     182: 55,  # 'Х'\n     183: 4,  # 'и'\n     184: 42,  # 'И'\n     185: 225,  # '╣'\n     186: 226,  # '║'\n     187: 227,  # '╗'\n     188: 228,  # '╝'\n     189: 23,  # 'й'\n     190: 60,  # 'Й'\n     191: 229,  # '┐'\n     192: 230,  # '└'\n     193: 231,  # '┴'\n     194: 232,  # '┬'\n     195: 233,  # '├'\n     196: 234,  # '─'\n     197: 235,  # '┼'\n     198: 11,  # 'к'\n     199: 36,  # 'К'\n     200: 236,  # '╚'\n     201: 237,  # '╔'\n     202: 238,  # '╩'\n     203: 239,  # '╦'\n     204: 240,  # '╠'\n     205: 241,  # '═'\n     206: 242,  # '╬'\n     207: 243,  # '¤'\n     208: 8,  # 'л'\n     209: 49,  # 'Л'\n     210: 12,  # 'м'\n     211: 38,  # 'М'\n     212: 5,  # 'н'\n     213: 31,  # 'Н'\n     214: 1,  # 'о'\n     215: 34,  # 'О'\n     216: 15,  # 'п'\n     217: 244,  # '┘'\n     218: 245,  # '┌'\n     219: 246,  # '█'\n     220: 247,  # '▄'\n     221: 35,  # 'П'\n     222: 16,  # 'я'\n     223: 248,  # '▀'\n     224: 43,  # 'Я'\n     225: 9,  # 'р'\n     226: 45,  # 'Р'\n     227: 7,  # 'с'\n     228: 32,  # 'С'\n     229: 6,  # 'т'\n     230: 40,  # 'Т'\n     231: 14,  # 'у'\n     232: 52,  # 'У'\n     233: 24,  # 'ж'\n     234: 56,  # 'Ж'\n     235: 10,  # 'в'\n     236: 33,  # 'В'\n     237: 17,  # 'ь'\n     238: 61,  # 'Ь'\n     239: 249,  # '№'\n     240: 250,  # '\\xad'\n     241: 18,  # 'ы'\n     242: 62,  # 'Ы'\n     243: 20,  # 'з'\n     244: 51,  # 'З'\n     245: 25,  # 'ш'\n     246: 57,  # 'Ш'\n     247: 30,  # 'э'\n     248: 47,  # 'Э'\n     249: 29,  # 'щ'\n     250: 63,  # 'Щ'\n     251: 22,  # 'ч'\n     252: 50,  # 'Ч'\n     253: 251,  # '§'\n     254: 252,  # '■'\n     255: 255,  # '\\xa0'\n}\n\nIBM855_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='IBM855',\n                                              language='Russian',\n                                              char_to_order_map=IBM855_RUSSIAN_CHAR_TO_ORDER,\n                                              language_model=RUSSIAN_LANG_MODEL,\n                                              typical_positive_ratio=0.976601,\n                                              keep_ascii_letters=False,\n                                              alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё')\n\nKOI8_R_RUSSIAN_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 142,  # 'A'\n     66: 143,  # 'B'\n     67: 144,  # 'C'\n     68: 145,  # 'D'\n     69: 146,  # 'E'\n     70: 147,  # 'F'\n     71: 148,  # 'G'\n     72: 149,  # 'H'\n     73: 150,  # 'I'\n     74: 151,  # 'J'\n     75: 152,  # 'K'\n     76: 74,  # 'L'\n     77: 153,  # 'M'\n     78: 75,  # 'N'\n     79: 154,  # 'O'\n     80: 155,  # 'P'\n     81: 156,  # 'Q'\n     82: 157,  # 'R'\n     83: 158,  # 'S'\n     84: 159,  # 'T'\n     85: 160,  # 'U'\n     86: 161,  # 'V'\n     87: 162,  # 'W'\n     88: 163,  # 'X'\n     89: 164,  # 'Y'\n     90: 165,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 71,  # 'a'\n     98: 172,  # 'b'\n     99: 66,  # 'c'\n     100: 173,  # 'd'\n     101: 65,  # 'e'\n     102: 174,  # 'f'\n     103: 76,  # 'g'\n     104: 175,  # 'h'\n     105: 64,  # 'i'\n     106: 176,  # 'j'\n     107: 177,  # 'k'\n     108: 77,  # 'l'\n     109: 72,  # 'm'\n     110: 178,  # 'n'\n     111: 69,  # 'o'\n     112: 67,  # 'p'\n     113: 179,  # 'q'\n     114: 78,  # 'r'\n     115: 73,  # 's'\n     116: 180,  # 't'\n     117: 181,  # 'u'\n     118: 79,  # 'v'\n     119: 182,  # 'w'\n     120: 183,  # 'x'\n     121: 184,  # 'y'\n     122: 185,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 191,  # '─'\n     129: 192,  # '│'\n     130: 193,  # '┌'\n     131: 194,  # '┐'\n     132: 195,  # '└'\n     133: 196,  # '┘'\n     134: 197,  # '├'\n     135: 198,  # '┤'\n     136: 199,  # '┬'\n     137: 200,  # '┴'\n     138: 201,  # '┼'\n     139: 202,  # '▀'\n     140: 203,  # '▄'\n     141: 204,  # '█'\n     142: 205,  # '▌'\n     143: 206,  # '▐'\n     144: 207,  # '░'\n     145: 208,  # '▒'\n     146: 209,  # '▓'\n     147: 210,  # '⌠'\n     148: 211,  # '■'\n     149: 212,  # '∙'\n     150: 213,  # '√'\n     151: 214,  # '≈'\n     152: 215,  # '≤'\n     153: 216,  # '≥'\n     154: 217,  # '\\xa0'\n     155: 218,  # '⌡'\n     156: 219,  # '°'\n     157: 220,  # '²'\n     158: 221,  # '·'\n     159: 222,  # '÷'\n     160: 223,  # '═'\n     161: 224,  # '║'\n     162: 225,  # '╒'\n     163: 68,  # 'ё'\n     164: 226,  # '╓'\n     165: 227,  # '╔'\n     166: 228,  # '╕'\n     167: 229,  # '╖'\n     168: 230,  # '╗'\n     169: 231,  # '╘'\n     170: 232,  # '╙'\n     171: 233,  # '╚'\n     172: 234,  # '╛'\n     173: 235,  # '╜'\n     174: 236,  # '╝'\n     175: 237,  # '╞'\n     176: 238,  # '╟'\n     177: 239,  # '╠'\n     178: 240,  # '╡'\n     179: 241,  # 'Ё'\n     180: 242,  # '╢'\n     181: 243,  # '╣'\n     182: 244,  # '╤'\n     183: 245,  # '╥'\n     184: 246,  # '╦'\n     185: 247,  # '╧'\n     186: 248,  # '╨'\n     187: 249,  # '╩'\n     188: 250,  # '╪'\n     189: 251,  # '╫'\n     190: 252,  # '╬'\n     191: 253,  # '©'\n     192: 27,  # 'ю'\n     193: 3,  # 'а'\n     194: 21,  # 'б'\n     195: 28,  # 'ц'\n     196: 13,  # 'д'\n     197: 2,  # 'е'\n     198: 39,  # 'ф'\n     199: 19,  # 'г'\n     200: 26,  # 'х'\n     201: 4,  # 'и'\n     202: 23,  # 'й'\n     203: 11,  # 'к'\n     204: 8,  # 'л'\n     205: 12,  # 'м'\n     206: 5,  # 'н'\n     207: 1,  # 'о'\n     208: 15,  # 'п'\n     209: 16,  # 'я'\n     210: 9,  # 'р'\n     211: 7,  # 'с'\n     212: 6,  # 'т'\n     213: 14,  # 'у'\n     214: 24,  # 'ж'\n     215: 10,  # 'в'\n     216: 17,  # 'ь'\n     217: 18,  # 'ы'\n     218: 20,  # 'з'\n     219: 25,  # 'ш'\n     220: 30,  # 'э'\n     221: 29,  # 'щ'\n     222: 22,  # 'ч'\n     223: 54,  # 'ъ'\n     224: 59,  # 'Ю'\n     225: 37,  # 'А'\n     226: 44,  # 'Б'\n     227: 58,  # 'Ц'\n     228: 41,  # 'Д'\n     229: 48,  # 'Е'\n     230: 53,  # 'Ф'\n     231: 46,  # 'Г'\n     232: 55,  # 'Х'\n     233: 42,  # 'И'\n     234: 60,  # 'Й'\n     235: 36,  # 'К'\n     236: 49,  # 'Л'\n     237: 38,  # 'М'\n     238: 31,  # 'Н'\n     239: 34,  # 'О'\n     240: 35,  # 'П'\n     241: 43,  # 'Я'\n     242: 45,  # 'Р'\n     243: 32,  # 'С'\n     244: 40,  # 'Т'\n     245: 52,  # 'У'\n     246: 56,  # 'Ж'\n     247: 33,  # 'В'\n     248: 61,  # 'Ь'\n     249: 62,  # 'Ы'\n     250: 51,  # 'З'\n     251: 57,  # 'Ш'\n     252: 47,  # 'Э'\n     253: 63,  # 'Щ'\n     254: 50,  # 'Ч'\n     255: 70,  # 'Ъ'\n}\n\nKOI8_R_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='KOI8-R',\n                                              language='Russian',\n                                              char_to_order_map=KOI8_R_RUSSIAN_CHAR_TO_ORDER,\n                                              language_model=RUSSIAN_LANG_MODEL,\n                                              typical_positive_ratio=0.976601,\n                                              keep_ascii_letters=False,\n                                              alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё')\n\nMACCYRILLIC_RUSSIAN_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 142,  # 'A'\n     66: 143,  # 'B'\n     67: 144,  # 'C'\n     68: 145,  # 'D'\n     69: 146,  # 'E'\n     70: 147,  # 'F'\n     71: 148,  # 'G'\n     72: 149,  # 'H'\n     73: 150,  # 'I'\n     74: 151,  # 'J'\n     75: 152,  # 'K'\n     76: 74,  # 'L'\n     77: 153,  # 'M'\n     78: 75,  # 'N'\n     79: 154,  # 'O'\n     80: 155,  # 'P'\n     81: 156,  # 'Q'\n     82: 157,  # 'R'\n     83: 158,  # 'S'\n     84: 159,  # 'T'\n     85: 160,  # 'U'\n     86: 161,  # 'V'\n     87: 162,  # 'W'\n     88: 163,  # 'X'\n     89: 164,  # 'Y'\n     90: 165,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 71,  # 'a'\n     98: 172,  # 'b'\n     99: 66,  # 'c'\n     100: 173,  # 'd'\n     101: 65,  # 'e'\n     102: 174,  # 'f'\n     103: 76,  # 'g'\n     104: 175,  # 'h'\n     105: 64,  # 'i'\n     106: 176,  # 'j'\n     107: 177,  # 'k'\n     108: 77,  # 'l'\n     109: 72,  # 'm'\n     110: 178,  # 'n'\n     111: 69,  # 'o'\n     112: 67,  # 'p'\n     113: 179,  # 'q'\n     114: 78,  # 'r'\n     115: 73,  # 's'\n     116: 180,  # 't'\n     117: 181,  # 'u'\n     118: 79,  # 'v'\n     119: 182,  # 'w'\n     120: 183,  # 'x'\n     121: 184,  # 'y'\n     122: 185,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 37,  # 'А'\n     129: 44,  # 'Б'\n     130: 33,  # 'В'\n     131: 46,  # 'Г'\n     132: 41,  # 'Д'\n     133: 48,  # 'Е'\n     134: 56,  # 'Ж'\n     135: 51,  # 'З'\n     136: 42,  # 'И'\n     137: 60,  # 'Й'\n     138: 36,  # 'К'\n     139: 49,  # 'Л'\n     140: 38,  # 'М'\n     141: 31,  # 'Н'\n     142: 34,  # 'О'\n     143: 35,  # 'П'\n     144: 45,  # 'Р'\n     145: 32,  # 'С'\n     146: 40,  # 'Т'\n     147: 52,  # 'У'\n     148: 53,  # 'Ф'\n     149: 55,  # 'Х'\n     150: 58,  # 'Ц'\n     151: 50,  # 'Ч'\n     152: 57,  # 'Ш'\n     153: 63,  # 'Щ'\n     154: 70,  # 'Ъ'\n     155: 62,  # 'Ы'\n     156: 61,  # 'Ь'\n     157: 47,  # 'Э'\n     158: 59,  # 'Ю'\n     159: 43,  # 'Я'\n     160: 191,  # '†'\n     161: 192,  # '°'\n     162: 193,  # 'Ґ'\n     163: 194,  # '£'\n     164: 195,  # '§'\n     165: 196,  # '•'\n     166: 197,  # '¶'\n     167: 198,  # 'І'\n     168: 199,  # '®'\n     169: 200,  # '©'\n     170: 201,  # '™'\n     171: 202,  # 'Ђ'\n     172: 203,  # 'ђ'\n     173: 204,  # '≠'\n     174: 205,  # 'Ѓ'\n     175: 206,  # 'ѓ'\n     176: 207,  # '∞'\n     177: 208,  # '±'\n     178: 209,  # '≤'\n     179: 210,  # '≥'\n     180: 211,  # 'і'\n     181: 212,  # 'µ'\n     182: 213,  # 'ґ'\n     183: 214,  # 'Ј'\n     184: 215,  # 'Є'\n     185: 216,  # 'є'\n     186: 217,  # 'Ї'\n     187: 218,  # 'ї'\n     188: 219,  # 'Љ'\n     189: 220,  # 'љ'\n     190: 221,  # 'Њ'\n     191: 222,  # 'њ'\n     192: 223,  # 'ј'\n     193: 224,  # 'Ѕ'\n     194: 225,  # '¬'\n     195: 226,  # '√'\n     196: 227,  # 'ƒ'\n     197: 228,  # '≈'\n     198: 229,  # '∆'\n     199: 230,  # '«'\n     200: 231,  # '»'\n     201: 232,  # '…'\n     202: 233,  # '\\xa0'\n     203: 234,  # 'Ћ'\n     204: 235,  # 'ћ'\n     205: 236,  # 'Ќ'\n     206: 237,  # 'ќ'\n     207: 238,  # 'ѕ'\n     208: 239,  # '–'\n     209: 240,  # '—'\n     210: 241,  # '“'\n     211: 242,  # '”'\n     212: 243,  # '‘'\n     213: 244,  # '’'\n     214: 245,  # '÷'\n     215: 246,  # '„'\n     216: 247,  # 'Ў'\n     217: 248,  # 'ў'\n     218: 249,  # 'Џ'\n     219: 250,  # 'џ'\n     220: 251,  # '№'\n     221: 252,  # 'Ё'\n     222: 68,  # 'ё'\n     223: 16,  # 'я'\n     224: 3,  # 'а'\n     225: 21,  # 'б'\n     226: 10,  # 'в'\n     227: 19,  # 'г'\n     228: 13,  # 'д'\n     229: 2,  # 'е'\n     230: 24,  # 'ж'\n     231: 20,  # 'з'\n     232: 4,  # 'и'\n     233: 23,  # 'й'\n     234: 11,  # 'к'\n     235: 8,  # 'л'\n     236: 12,  # 'м'\n     237: 5,  # 'н'\n     238: 1,  # 'о'\n     239: 15,  # 'п'\n     240: 9,  # 'р'\n     241: 7,  # 'с'\n     242: 6,  # 'т'\n     243: 14,  # 'у'\n     244: 39,  # 'ф'\n     245: 26,  # 'х'\n     246: 28,  # 'ц'\n     247: 22,  # 'ч'\n     248: 25,  # 'ш'\n     249: 29,  # 'щ'\n     250: 54,  # 'ъ'\n     251: 18,  # 'ы'\n     252: 17,  # 'ь'\n     253: 30,  # 'э'\n     254: 27,  # 'ю'\n     255: 255,  # '€'\n}\n\nMACCYRILLIC_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='MacCyrillic',\n                                                   language='Russian',\n                                                   char_to_order_map=MACCYRILLIC_RUSSIAN_CHAR_TO_ORDER,\n                                                   language_model=RUSSIAN_LANG_MODEL,\n                                                   typical_positive_ratio=0.976601,\n                                                   keep_ascii_letters=False,\n                                                   alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё')\n\nISO_8859_5_RUSSIAN_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 142,  # 'A'\n     66: 143,  # 'B'\n     67: 144,  # 'C'\n     68: 145,  # 'D'\n     69: 146,  # 'E'\n     70: 147,  # 'F'\n     71: 148,  # 'G'\n     72: 149,  # 'H'\n     73: 150,  # 'I'\n     74: 151,  # 'J'\n     75: 152,  # 'K'\n     76: 74,  # 'L'\n     77: 153,  # 'M'\n     78: 75,  # 'N'\n     79: 154,  # 'O'\n     80: 155,  # 'P'\n     81: 156,  # 'Q'\n     82: 157,  # 'R'\n     83: 158,  # 'S'\n     84: 159,  # 'T'\n     85: 160,  # 'U'\n     86: 161,  # 'V'\n     87: 162,  # 'W'\n     88: 163,  # 'X'\n     89: 164,  # 'Y'\n     90: 165,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 71,  # 'a'\n     98: 172,  # 'b'\n     99: 66,  # 'c'\n     100: 173,  # 'd'\n     101: 65,  # 'e'\n     102: 174,  # 'f'\n     103: 76,  # 'g'\n     104: 175,  # 'h'\n     105: 64,  # 'i'\n     106: 176,  # 'j'\n     107: 177,  # 'k'\n     108: 77,  # 'l'\n     109: 72,  # 'm'\n     110: 178,  # 'n'\n     111: 69,  # 'o'\n     112: 67,  # 'p'\n     113: 179,  # 'q'\n     114: 78,  # 'r'\n     115: 73,  # 's'\n     116: 180,  # 't'\n     117: 181,  # 'u'\n     118: 79,  # 'v'\n     119: 182,  # 'w'\n     120: 183,  # 'x'\n     121: 184,  # 'y'\n     122: 185,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 191,  # '\\x80'\n     129: 192,  # '\\x81'\n     130: 193,  # '\\x82'\n     131: 194,  # '\\x83'\n     132: 195,  # '\\x84'\n     133: 196,  # '\\x85'\n     134: 197,  # '\\x86'\n     135: 198,  # '\\x87'\n     136: 199,  # '\\x88'\n     137: 200,  # '\\x89'\n     138: 201,  # '\\x8a'\n     139: 202,  # '\\x8b'\n     140: 203,  # '\\x8c'\n     141: 204,  # '\\x8d'\n     142: 205,  # '\\x8e'\n     143: 206,  # '\\x8f'\n     144: 207,  # '\\x90'\n     145: 208,  # '\\x91'\n     146: 209,  # '\\x92'\n     147: 210,  # '\\x93'\n     148: 211,  # '\\x94'\n     149: 212,  # '\\x95'\n     150: 213,  # '\\x96'\n     151: 214,  # '\\x97'\n     152: 215,  # '\\x98'\n     153: 216,  # '\\x99'\n     154: 217,  # '\\x9a'\n     155: 218,  # '\\x9b'\n     156: 219,  # '\\x9c'\n     157: 220,  # '\\x9d'\n     158: 221,  # '\\x9e'\n     159: 222,  # '\\x9f'\n     160: 223,  # '\\xa0'\n     161: 224,  # 'Ё'\n     162: 225,  # 'Ђ'\n     163: 226,  # 'Ѓ'\n     164: 227,  # 'Є'\n     165: 228,  # 'Ѕ'\n     166: 229,  # 'І'\n     167: 230,  # 'Ї'\n     168: 231,  # 'Ј'\n     169: 232,  # 'Љ'\n     170: 233,  # 'Њ'\n     171: 234,  # 'Ћ'\n     172: 235,  # 'Ќ'\n     173: 236,  # '\\xad'\n     174: 237,  # 'Ў'\n     175: 238,  # 'Џ'\n     176: 37,  # 'А'\n     177: 44,  # 'Б'\n     178: 33,  # 'В'\n     179: 46,  # 'Г'\n     180: 41,  # 'Д'\n     181: 48,  # 'Е'\n     182: 56,  # 'Ж'\n     183: 51,  # 'З'\n     184: 42,  # 'И'\n     185: 60,  # 'Й'\n     186: 36,  # 'К'\n     187: 49,  # 'Л'\n     188: 38,  # 'М'\n     189: 31,  # 'Н'\n     190: 34,  # 'О'\n     191: 35,  # 'П'\n     192: 45,  # 'Р'\n     193: 32,  # 'С'\n     194: 40,  # 'Т'\n     195: 52,  # 'У'\n     196: 53,  # 'Ф'\n     197: 55,  # 'Х'\n     198: 58,  # 'Ц'\n     199: 50,  # 'Ч'\n     200: 57,  # 'Ш'\n     201: 63,  # 'Щ'\n     202: 70,  # 'Ъ'\n     203: 62,  # 'Ы'\n     204: 61,  # 'Ь'\n     205: 47,  # 'Э'\n     206: 59,  # 'Ю'\n     207: 43,  # 'Я'\n     208: 3,  # 'а'\n     209: 21,  # 'б'\n     210: 10,  # 'в'\n     211: 19,  # 'г'\n     212: 13,  # 'д'\n     213: 2,  # 'е'\n     214: 24,  # 'ж'\n     215: 20,  # 'з'\n     216: 4,  # 'и'\n     217: 23,  # 'й'\n     218: 11,  # 'к'\n     219: 8,  # 'л'\n     220: 12,  # 'м'\n     221: 5,  # 'н'\n     222: 1,  # 'о'\n     223: 15,  # 'п'\n     224: 9,  # 'р'\n     225: 7,  # 'с'\n     226: 6,  # 'т'\n     227: 14,  # 'у'\n     228: 39,  # 'ф'\n     229: 26,  # 'х'\n     230: 28,  # 'ц'\n     231: 22,  # 'ч'\n     232: 25,  # 'ш'\n     233: 29,  # 'щ'\n     234: 54,  # 'ъ'\n     235: 18,  # 'ы'\n     236: 17,  # 'ь'\n     237: 30,  # 'э'\n     238: 27,  # 'ю'\n     239: 16,  # 'я'\n     240: 239,  # '№'\n     241: 68,  # 'ё'\n     242: 240,  # 'ђ'\n     243: 241,  # 'ѓ'\n     244: 242,  # 'є'\n     245: 243,  # 'ѕ'\n     246: 244,  # 'і'\n     247: 245,  # 'ї'\n     248: 246,  # 'ј'\n     249: 247,  # 'љ'\n     250: 248,  # 'њ'\n     251: 249,  # 'ћ'\n     252: 250,  # 'ќ'\n     253: 251,  # '§'\n     254: 252,  # 'ў'\n     255: 255,  # 'џ'\n}\n\nISO_8859_5_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-5',\n                                                  language='Russian',\n                                                  char_to_order_map=ISO_8859_5_RUSSIAN_CHAR_TO_ORDER,\n                                                  language_model=RUSSIAN_LANG_MODEL,\n                                                  typical_positive_ratio=0.976601,\n                                                  keep_ascii_letters=False,\n                                                  alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё')\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/langthaimodel.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nfrom chardet.sbcharsetprober import SingleByteCharSetModel\n\n\n# 3: Positive\n# 2: Likely\n# 1: Unlikely\n# 0: Negative\n\nTHAI_LANG_MODEL = {\n    5: {  # 'ก'\n        5: 2,  # 'ก'\n        30: 2,  # 'ข'\n        24: 2,  # 'ค'\n        8: 2,  # 'ง'\n        26: 2,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 3,  # 'ฎ'\n        57: 2,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 2,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 3,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 2,  # 'น'\n        17: 1,  # 'บ'\n        25: 2,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 1,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 1,  # 'ย'\n        2: 3,  # 'ร'\n        61: 2,  # 'ฤ'\n        15: 3,  # 'ล'\n        12: 3,  # 'ว'\n        42: 2,  # 'ศ'\n        46: 3,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 2,  # 'ห'\n        4: 3,  # 'อ'\n        63: 1,  # 'ฯ'\n        22: 2,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 3,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 3,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 2,  # 'ื'\n        32: 2,  # 'ุ'\n        35: 1,  # 'ู'\n        11: 2,  # 'เ'\n        28: 2,  # 'แ'\n        41: 1,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 1,  # 'ๆ'\n        37: 3,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 2,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    30: {  # 'ข'\n        5: 1,  # 'ก'\n        30: 0,  # 'ข'\n        24: 1,  # 'ค'\n        8: 1,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 2,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 2,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 2,  # 'น'\n        17: 1,  # 'บ'\n        25: 1,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 2,  # 'ย'\n        2: 1,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 2,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 1,  # 'ห'\n        4: 3,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 2,  # 'ี'\n        40: 3,  # 'ึ'\n        27: 1,  # 'ื'\n        32: 1,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 1,  # '็'\n        6: 2,  # '่'\n        7: 3,  # '้'\n        38: 1,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    24: {  # 'ค'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 2,  # 'ค'\n        8: 2,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 2,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 2,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 0,  # 'บ'\n        25: 1,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 2,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 3,  # 'ล'\n        12: 3,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 0,  # 'ห'\n        4: 2,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 2,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 2,  # 'า'\n        36: 3,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 2,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 3,  # 'ื'\n        32: 3,  # 'ุ'\n        35: 2,  # 'ู'\n        11: 1,  # 'เ'\n        28: 0,  # 'แ'\n        41: 3,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 1,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 3,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    8: {  # 'ง'\n        5: 3,  # 'ก'\n        30: 2,  # 'ข'\n        24: 3,  # 'ค'\n        8: 2,  # 'ง'\n        26: 2,  # 'จ'\n        52: 1,  # 'ฉ'\n        34: 2,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 2,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 3,  # 'ท'\n        48: 1,  # 'ธ'\n        3: 3,  # 'น'\n        17: 2,  # 'บ'\n        25: 2,  # 'ป'\n        39: 2,  # 'ผ'\n        62: 1,  # 'ฝ'\n        31: 2,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 1,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 2,  # 'ว'\n        42: 2,  # 'ศ'\n        46: 1,  # 'ษ'\n        18: 3,  # 'ส'\n        21: 3,  # 'ห'\n        4: 2,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 1,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 2,  # 'ิ'\n        13: 1,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 1,  # 'ื'\n        32: 1,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 3,  # 'เ'\n        28: 2,  # 'แ'\n        41: 1,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 3,  # 'ๆ'\n        37: 0,  # '็'\n        6: 2,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    26: {  # 'จ'\n        5: 2,  # 'ก'\n        30: 1,  # 'ข'\n        24: 0,  # 'ค'\n        8: 2,  # 'ง'\n        26: 3,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 1,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 1,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 1,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 1,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 1,  # 'ห'\n        4: 2,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 3,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 3,  # 'ำ'\n        23: 2,  # 'ิ'\n        13: 1,  # 'ี'\n        40: 3,  # 'ึ'\n        27: 1,  # 'ื'\n        32: 3,  # 'ุ'\n        35: 2,  # 'ู'\n        11: 1,  # 'เ'\n        28: 1,  # 'แ'\n        41: 0,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 2,  # '่'\n        7: 2,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    52: {  # 'ฉ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 3,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 3,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 1,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 1,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 1,  # 'ะ'\n        10: 1,  # 'ั'\n        1: 1,  # 'า'\n        36: 0,  # 'ำ'\n        23: 1,  # 'ิ'\n        13: 1,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 1,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    34: {  # 'ช'\n        5: 1,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 1,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 1,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 2,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 1,  # 'ย'\n        2: 1,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 1,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 2,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 2,  # 'ั'\n        1: 3,  # 'า'\n        36: 1,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 2,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 3,  # 'ื'\n        32: 3,  # 'ุ'\n        35: 1,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 1,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    51: {  # 'ซ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 1,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 1,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 0,  # 'ห'\n        4: 2,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 1,  # 'ั'\n        1: 1,  # 'า'\n        36: 0,  # 'ำ'\n        23: 1,  # 'ิ'\n        13: 2,  # 'ี'\n        40: 3,  # 'ึ'\n        27: 2,  # 'ื'\n        32: 1,  # 'ุ'\n        35: 1,  # 'ู'\n        11: 1,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 1,  # '็'\n        6: 1,  # '่'\n        7: 2,  # '้'\n        38: 1,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    47: {  # 'ญ'\n        5: 1,  # 'ก'\n        30: 1,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 3,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 1,  # 'บ'\n        25: 1,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 1,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 2,  # 'ห'\n        4: 1,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 1,  # 'ะ'\n        10: 2,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 1,  # 'ิ'\n        13: 1,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 1,  # 'เ'\n        28: 1,  # 'แ'\n        41: 0,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 1,  # 'ๆ'\n        37: 0,  # '็'\n        6: 2,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    58: {  # 'ฎ'\n        5: 2,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 1,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 1,  # 'ิ'\n        13: 2,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    57: {  # 'ฏ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 1,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    49: {  # 'ฐ'\n        5: 1,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 2,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 1,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 1,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 1,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    53: {  # 'ฑ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 2,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 3,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    55: {  # 'ฒ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 1,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    43: {  # 'ณ'\n        5: 1,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 3,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 3,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 1,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 1,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 1,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 3,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 1,  # 'ิ'\n        13: 2,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 1,  # 'เ'\n        28: 1,  # 'แ'\n        41: 0,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 3,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    20: {  # 'ด'\n        5: 2,  # 'ก'\n        30: 2,  # 'ข'\n        24: 2,  # 'ค'\n        8: 3,  # 'ง'\n        26: 2,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 1,  # 'ด'\n        19: 2,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 1,  # 'น'\n        17: 1,  # 'บ'\n        25: 1,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 3,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 2,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 2,  # 'ห'\n        4: 1,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 2,  # 'า'\n        36: 2,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 3,  # 'ี'\n        40: 1,  # 'ึ'\n        27: 2,  # 'ื'\n        32: 3,  # 'ุ'\n        35: 2,  # 'ู'\n        11: 2,  # 'เ'\n        28: 2,  # 'แ'\n        41: 1,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 2,  # 'ๆ'\n        37: 2,  # '็'\n        6: 1,  # '่'\n        7: 3,  # '้'\n        38: 1,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    19: {  # 'ต'\n        5: 2,  # 'ก'\n        30: 1,  # 'ข'\n        24: 1,  # 'ค'\n        8: 0,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 1,  # 'ด'\n        19: 1,  # 'ต'\n        44: 2,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 2,  # 'น'\n        17: 1,  # 'บ'\n        25: 1,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 2,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 1,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 1,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 3,  # 'ส'\n        21: 0,  # 'ห'\n        4: 3,  # 'อ'\n        63: 1,  # 'ฯ'\n        22: 2,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 2,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 2,  # 'ี'\n        40: 1,  # 'ึ'\n        27: 1,  # 'ื'\n        32: 3,  # 'ุ'\n        35: 2,  # 'ู'\n        11: 1,  # 'เ'\n        28: 1,  # 'แ'\n        41: 1,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 2,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 2,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    44: {  # 'ถ'\n        5: 1,  # 'ก'\n        30: 0,  # 'ข'\n        24: 1,  # 'ค'\n        8: 0,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 1,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 1,  # 'น'\n        17: 2,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 1,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 1,  # 'ล'\n        12: 1,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 0,  # 'ห'\n        4: 1,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 2,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 2,  # 'ิ'\n        13: 1,  # 'ี'\n        40: 3,  # 'ึ'\n        27: 2,  # 'ื'\n        32: 2,  # 'ุ'\n        35: 3,  # 'ู'\n        11: 1,  # 'เ'\n        28: 1,  # 'แ'\n        41: 0,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 2,  # '่'\n        7: 3,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    14: {  # 'ท'\n        5: 1,  # 'ก'\n        30: 1,  # 'ข'\n        24: 3,  # 'ค'\n        8: 1,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 1,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 3,  # 'ธ'\n        3: 3,  # 'น'\n        17: 2,  # 'บ'\n        25: 2,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 2,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 3,  # 'ย'\n        2: 3,  # 'ร'\n        61: 1,  # 'ฤ'\n        15: 1,  # 'ล'\n        12: 2,  # 'ว'\n        42: 3,  # 'ศ'\n        46: 1,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 0,  # 'ห'\n        4: 2,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 2,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 3,  # 'ำ'\n        23: 2,  # 'ิ'\n        13: 3,  # 'ี'\n        40: 2,  # 'ึ'\n        27: 1,  # 'ื'\n        32: 3,  # 'ุ'\n        35: 1,  # 'ู'\n        11: 0,  # 'เ'\n        28: 1,  # 'แ'\n        41: 0,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 1,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 2,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    48: {  # 'ธ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 1,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 1,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 2,  # 'า'\n        36: 0,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 3,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 2,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 3,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    3: {  # 'น'\n        5: 3,  # 'ก'\n        30: 2,  # 'ข'\n        24: 3,  # 'ค'\n        8: 1,  # 'ง'\n        26: 2,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 1,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 3,  # 'ด'\n        19: 3,  # 'ต'\n        44: 2,  # 'ถ'\n        14: 3,  # 'ท'\n        48: 3,  # 'ธ'\n        3: 2,  # 'น'\n        17: 2,  # 'บ'\n        25: 2,  # 'ป'\n        39: 2,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 2,  # 'พ'\n        54: 1,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 2,  # 'ย'\n        2: 2,  # 'ร'\n        61: 1,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 3,  # 'ว'\n        42: 1,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 2,  # 'ห'\n        4: 3,  # 'อ'\n        63: 1,  # 'ฯ'\n        22: 2,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 3,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 3,  # 'ี'\n        40: 3,  # 'ึ'\n        27: 3,  # 'ื'\n        32: 3,  # 'ุ'\n        35: 2,  # 'ู'\n        11: 3,  # 'เ'\n        28: 2,  # 'แ'\n        41: 3,  # 'โ'\n        29: 3,  # 'ใ'\n        33: 3,  # 'ไ'\n        50: 2,  # 'ๆ'\n        37: 1,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 2,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    17: {  # 'บ'\n        5: 3,  # 'ก'\n        30: 2,  # 'ข'\n        24: 2,  # 'ค'\n        8: 1,  # 'ง'\n        26: 1,  # 'จ'\n        52: 1,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 1,  # 'ด'\n        19: 2,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 3,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 3,  # 'บ'\n        25: 2,  # 'ป'\n        39: 2,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 1,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 0,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 3,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 2,  # 'ห'\n        4: 2,  # 'อ'\n        63: 1,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 2,  # 'ำ'\n        23: 2,  # 'ิ'\n        13: 2,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 2,  # 'ื'\n        32: 3,  # 'ุ'\n        35: 2,  # 'ู'\n        11: 2,  # 'เ'\n        28: 2,  # 'แ'\n        41: 1,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 1,  # '็'\n        6: 2,  # '่'\n        7: 2,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    25: {  # 'ป'\n        5: 2,  # 'ก'\n        30: 0,  # 'ข'\n        24: 1,  # 'ค'\n        8: 0,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 1,  # 'ฎ'\n        57: 3,  # 'ฏ'\n        49: 1,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 1,  # 'ด'\n        19: 1,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 2,  # 'น'\n        17: 0,  # 'บ'\n        25: 1,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 1,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 0,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 3,  # 'ล'\n        12: 1,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 1,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 1,  # 'ห'\n        4: 2,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 1,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 1,  # 'า'\n        36: 0,  # 'ำ'\n        23: 2,  # 'ิ'\n        13: 3,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 1,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 1,  # 'เ'\n        28: 2,  # 'แ'\n        41: 0,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 3,  # '็'\n        6: 1,  # '่'\n        7: 2,  # '้'\n        38: 1,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    39: {  # 'ผ'\n        5: 1,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 1,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 2,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 2,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 3,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 1,  # 'ะ'\n        10: 1,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 2,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 1,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 3,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 3,  # '่'\n        7: 1,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    62: {  # 'ฝ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 1,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 1,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 1,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 1,  # 'ี'\n        40: 2,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 2,  # '่'\n        7: 1,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    31: {  # 'พ'\n        5: 1,  # 'ก'\n        30: 1,  # 'ข'\n        24: 1,  # 'ค'\n        8: 1,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 1,  # 'ณ'\n        20: 1,  # 'ด'\n        19: 1,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 1,  # 'ธ'\n        3: 3,  # 'น'\n        17: 2,  # 'บ'\n        25: 0,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 2,  # 'ย'\n        2: 3,  # 'ร'\n        61: 2,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 2,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 1,  # 'ห'\n        4: 2,  # 'อ'\n        63: 1,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 2,  # 'ี'\n        40: 1,  # 'ึ'\n        27: 3,  # 'ื'\n        32: 1,  # 'ุ'\n        35: 2,  # 'ู'\n        11: 1,  # 'เ'\n        28: 1,  # 'แ'\n        41: 0,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 1,  # '็'\n        6: 0,  # '่'\n        7: 1,  # '้'\n        38: 3,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    54: {  # 'ฟ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 1,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 2,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 1,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 0,  # 'ห'\n        4: 1,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 2,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 1,  # 'ิ'\n        13: 1,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 1,  # 'ื'\n        32: 1,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 1,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 2,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    45: {  # 'ภ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 1,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 3,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 1,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 1,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 2,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 1,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    9: {  # 'ม'\n        5: 2,  # 'ก'\n        30: 2,  # 'ข'\n        24: 2,  # 'ค'\n        8: 2,  # 'ง'\n        26: 2,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 1,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 2,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 1,  # 'ธ'\n        3: 3,  # 'น'\n        17: 2,  # 'บ'\n        25: 2,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 3,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 1,  # 'ย'\n        2: 2,  # 'ร'\n        61: 2,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 2,  # 'ว'\n        42: 1,  # 'ศ'\n        46: 1,  # 'ษ'\n        18: 3,  # 'ส'\n        21: 3,  # 'ห'\n        4: 3,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 1,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 3,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 3,  # 'ื'\n        32: 3,  # 'ุ'\n        35: 3,  # 'ู'\n        11: 2,  # 'เ'\n        28: 2,  # 'แ'\n        41: 2,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 1,  # 'ๆ'\n        37: 1,  # '็'\n        6: 3,  # '่'\n        7: 2,  # '้'\n        38: 1,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    16: {  # 'ย'\n        5: 3,  # 'ก'\n        30: 1,  # 'ข'\n        24: 2,  # 'ค'\n        8: 3,  # 'ง'\n        26: 2,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 2,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 2,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 2,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 1,  # 'ธ'\n        3: 3,  # 'น'\n        17: 3,  # 'บ'\n        25: 1,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 0,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 1,  # 'ล'\n        12: 3,  # 'ว'\n        42: 1,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 1,  # 'ห'\n        4: 2,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 2,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 2,  # 'ิ'\n        13: 3,  # 'ี'\n        40: 1,  # 'ึ'\n        27: 2,  # 'ื'\n        32: 2,  # 'ุ'\n        35: 3,  # 'ู'\n        11: 2,  # 'เ'\n        28: 1,  # 'แ'\n        41: 1,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 2,  # 'ๆ'\n        37: 1,  # '็'\n        6: 3,  # '่'\n        7: 2,  # '้'\n        38: 3,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    2: {  # 'ร'\n        5: 3,  # 'ก'\n        30: 2,  # 'ข'\n        24: 2,  # 'ค'\n        8: 3,  # 'ง'\n        26: 2,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 2,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 3,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 3,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 2,  # 'ต'\n        44: 3,  # 'ถ'\n        14: 3,  # 'ท'\n        48: 1,  # 'ธ'\n        3: 2,  # 'น'\n        17: 2,  # 'บ'\n        25: 3,  # 'ป'\n        39: 2,  # 'ผ'\n        62: 1,  # 'ฝ'\n        31: 2,  # 'พ'\n        54: 1,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 2,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 3,  # 'ว'\n        42: 2,  # 'ศ'\n        46: 2,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 2,  # 'ห'\n        4: 3,  # 'อ'\n        63: 1,  # 'ฯ'\n        22: 3,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 3,  # 'ี'\n        40: 2,  # 'ึ'\n        27: 3,  # 'ื'\n        32: 3,  # 'ุ'\n        35: 3,  # 'ู'\n        11: 3,  # 'เ'\n        28: 3,  # 'แ'\n        41: 1,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 3,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 3,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    61: {  # 'ฤ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 2,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 2,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    15: {  # 'ล'\n        5: 2,  # 'ก'\n        30: 3,  # 'ข'\n        24: 1,  # 'ค'\n        8: 3,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 2,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 1,  # 'น'\n        17: 2,  # 'บ'\n        25: 2,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 3,  # 'ย'\n        2: 1,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 1,  # 'ล'\n        12: 1,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 1,  # 'ห'\n        4: 3,  # 'อ'\n        63: 2,  # 'ฯ'\n        22: 3,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 2,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 3,  # 'ี'\n        40: 2,  # 'ึ'\n        27: 3,  # 'ื'\n        32: 2,  # 'ุ'\n        35: 3,  # 'ู'\n        11: 2,  # 'เ'\n        28: 1,  # 'แ'\n        41: 1,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 2,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 2,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    12: {  # 'ว'\n        5: 3,  # 'ก'\n        30: 2,  # 'ข'\n        24: 1,  # 'ค'\n        8: 3,  # 'ง'\n        26: 2,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 1,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 1,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 2,  # 'บ'\n        25: 1,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 1,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 3,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 3,  # 'ล'\n        12: 1,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 2,  # 'ห'\n        4: 2,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 2,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 2,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 2,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 3,  # 'เ'\n        28: 2,  # 'แ'\n        41: 1,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 1,  # 'ๆ'\n        37: 0,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 1,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    42: {  # 'ศ'\n        5: 1,  # 'ก'\n        30: 0,  # 'ข'\n        24: 1,  # 'ค'\n        8: 0,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 1,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 1,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 2,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 2,  # 'ว'\n        42: 1,  # 'ศ'\n        46: 2,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 2,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 2,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 3,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 2,  # 'ู'\n        11: 0,  # 'เ'\n        28: 1,  # 'แ'\n        41: 0,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 1,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    46: {  # 'ษ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 2,  # 'ฎ'\n        57: 1,  # 'ฏ'\n        49: 2,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 3,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 1,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 2,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 1,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 2,  # 'ะ'\n        10: 2,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 1,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 1,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 2,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    18: {  # 'ส'\n        5: 2,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 2,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 3,  # 'ด'\n        19: 3,  # 'ต'\n        44: 3,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 2,  # 'บ'\n        25: 1,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 2,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 1,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 1,  # 'ล'\n        12: 2,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 2,  # 'ห'\n        4: 3,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 2,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 3,  # 'ำ'\n        23: 3,  # 'ิ'\n        13: 3,  # 'ี'\n        40: 2,  # 'ึ'\n        27: 3,  # 'ื'\n        32: 3,  # 'ุ'\n        35: 3,  # 'ู'\n        11: 2,  # 'เ'\n        28: 0,  # 'แ'\n        41: 1,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 3,  # '่'\n        7: 1,  # '้'\n        38: 2,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    21: {  # 'ห'\n        5: 3,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 1,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 2,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 1,  # 'ด'\n        19: 3,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 0,  # 'บ'\n        25: 1,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 2,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 3,  # 'ล'\n        12: 2,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 3,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 1,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 0,  # 'ำ'\n        23: 1,  # 'ิ'\n        13: 1,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 1,  # 'ุ'\n        35: 1,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 3,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 2,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    4: {  # 'อ'\n        5: 3,  # 'ก'\n        30: 1,  # 'ข'\n        24: 2,  # 'ค'\n        8: 3,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 3,  # 'ด'\n        19: 2,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 1,  # 'ธ'\n        3: 3,  # 'น'\n        17: 3,  # 'บ'\n        25: 1,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 1,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 3,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 2,  # 'ว'\n        42: 1,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 2,  # 'ห'\n        4: 3,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 2,  # 'ะ'\n        10: 3,  # 'ั'\n        1: 3,  # 'า'\n        36: 2,  # 'ำ'\n        23: 2,  # 'ิ'\n        13: 3,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 3,  # 'ื'\n        32: 3,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 3,  # 'เ'\n        28: 1,  # 'แ'\n        41: 1,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 1,  # 'ๆ'\n        37: 1,  # '็'\n        6: 2,  # '่'\n        7: 2,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    63: {  # 'ฯ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    22: {  # 'ะ'\n        5: 3,  # 'ก'\n        30: 1,  # 'ข'\n        24: 2,  # 'ค'\n        8: 1,  # 'ง'\n        26: 2,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 3,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 3,  # 'ด'\n        19: 3,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 3,  # 'ท'\n        48: 1,  # 'ธ'\n        3: 2,  # 'น'\n        17: 3,  # 'บ'\n        25: 2,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 2,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 2,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 2,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 3,  # 'ส'\n        21: 3,  # 'ห'\n        4: 2,  # 'อ'\n        63: 1,  # 'ฯ'\n        22: 1,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 3,  # 'เ'\n        28: 2,  # 'แ'\n        41: 1,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    10: {  # 'ั'\n        5: 3,  # 'ก'\n        30: 0,  # 'ข'\n        24: 1,  # 'ค'\n        8: 3,  # 'ง'\n        26: 3,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 3,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 2,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 3,  # 'ฒ'\n        43: 3,  # 'ณ'\n        20: 3,  # 'ด'\n        19: 3,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 3,  # 'บ'\n        25: 1,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 2,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 3,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 3,  # 'ว'\n        42: 2,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 3,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    1: {  # 'า'\n        5: 3,  # 'ก'\n        30: 2,  # 'ข'\n        24: 3,  # 'ค'\n        8: 3,  # 'ง'\n        26: 3,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 3,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 2,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 3,  # 'ณ'\n        20: 3,  # 'ด'\n        19: 3,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 3,  # 'ท'\n        48: 2,  # 'ธ'\n        3: 3,  # 'น'\n        17: 3,  # 'บ'\n        25: 2,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 1,  # 'ฝ'\n        31: 3,  # 'พ'\n        54: 1,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 3,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 3,  # 'ล'\n        12: 3,  # 'ว'\n        42: 2,  # 'ศ'\n        46: 3,  # 'ษ'\n        18: 3,  # 'ส'\n        21: 3,  # 'ห'\n        4: 2,  # 'อ'\n        63: 1,  # 'ฯ'\n        22: 3,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 3,  # 'เ'\n        28: 2,  # 'แ'\n        41: 1,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 1,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    36: {  # 'ำ'\n        5: 2,  # 'ก'\n        30: 1,  # 'ข'\n        24: 3,  # 'ค'\n        8: 2,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 1,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 1,  # 'ด'\n        19: 1,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 1,  # 'บ'\n        25: 1,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 0,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 1,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 3,  # 'ห'\n        4: 1,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 3,  # 'เ'\n        28: 2,  # 'แ'\n        41: 1,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    23: {  # 'ิ'\n        5: 3,  # 'ก'\n        30: 1,  # 'ข'\n        24: 2,  # 'ค'\n        8: 3,  # 'ง'\n        26: 3,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 3,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 2,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 3,  # 'ด'\n        19: 3,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 3,  # 'ท'\n        48: 3,  # 'ธ'\n        3: 3,  # 'น'\n        17: 3,  # 'บ'\n        25: 2,  # 'ป'\n        39: 2,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 3,  # 'พ'\n        54: 1,  # 'ฟ'\n        45: 2,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 2,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 3,  # 'ว'\n        42: 3,  # 'ศ'\n        46: 2,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 3,  # 'ห'\n        4: 1,  # 'อ'\n        63: 1,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 3,  # 'เ'\n        28: 1,  # 'แ'\n        41: 1,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 3,  # '่'\n        7: 2,  # '้'\n        38: 2,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    13: {  # 'ี'\n        5: 3,  # 'ก'\n        30: 2,  # 'ข'\n        24: 2,  # 'ค'\n        8: 0,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 1,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 1,  # 'น'\n        17: 2,  # 'บ'\n        25: 2,  # 'ป'\n        39: 1,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 2,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 3,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 1,  # 'ล'\n        12: 2,  # 'ว'\n        42: 1,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 1,  # 'ห'\n        4: 2,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 2,  # 'เ'\n        28: 2,  # 'แ'\n        41: 1,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 1,  # 'ๆ'\n        37: 0,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    40: {  # 'ึ'\n        5: 3,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 3,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 1,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    27: {  # 'ื'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 1,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 2,  # 'น'\n        17: 3,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 3,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    32: {  # 'ุ'\n        5: 3,  # 'ก'\n        30: 2,  # 'ข'\n        24: 3,  # 'ค'\n        8: 3,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 2,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 1,  # 'ฒ'\n        43: 3,  # 'ณ'\n        20: 3,  # 'ด'\n        19: 3,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 1,  # 'ธ'\n        3: 2,  # 'น'\n        17: 2,  # 'บ'\n        25: 2,  # 'ป'\n        39: 2,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 1,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 1,  # 'ว'\n        42: 1,  # 'ศ'\n        46: 2,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 1,  # 'ห'\n        4: 1,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 1,  # 'เ'\n        28: 0,  # 'แ'\n        41: 1,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 3,  # '่'\n        7: 2,  # '้'\n        38: 1,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    35: {  # 'ู'\n        5: 3,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 2,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 2,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 1,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 2,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 2,  # 'น'\n        17: 0,  # 'บ'\n        25: 3,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 0,  # 'ย'\n        2: 1,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 3,  # 'ล'\n        12: 1,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 1,  # 'เ'\n        28: 1,  # 'แ'\n        41: 1,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 3,  # '่'\n        7: 3,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    11: {  # 'เ'\n        5: 3,  # 'ก'\n        30: 3,  # 'ข'\n        24: 3,  # 'ค'\n        8: 2,  # 'ง'\n        26: 3,  # 'จ'\n        52: 3,  # 'ฉ'\n        34: 3,  # 'ช'\n        51: 2,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 1,  # 'ณ'\n        20: 3,  # 'ด'\n        19: 3,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 3,  # 'ท'\n        48: 1,  # 'ธ'\n        3: 3,  # 'น'\n        17: 3,  # 'บ'\n        25: 3,  # 'ป'\n        39: 2,  # 'ผ'\n        62: 1,  # 'ฝ'\n        31: 3,  # 'พ'\n        54: 1,  # 'ฟ'\n        45: 3,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 2,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 3,  # 'ล'\n        12: 3,  # 'ว'\n        42: 2,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 3,  # 'ส'\n        21: 3,  # 'ห'\n        4: 3,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    28: {  # 'แ'\n        5: 3,  # 'ก'\n        30: 2,  # 'ข'\n        24: 2,  # 'ค'\n        8: 1,  # 'ง'\n        26: 2,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 3,  # 'ต'\n        44: 2,  # 'ถ'\n        14: 3,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 3,  # 'บ'\n        25: 2,  # 'ป'\n        39: 3,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 2,  # 'พ'\n        54: 2,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 2,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 3,  # 'ล'\n        12: 2,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 3,  # 'ส'\n        21: 3,  # 'ห'\n        4: 1,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    41: {  # 'โ'\n        5: 2,  # 'ก'\n        30: 1,  # 'ข'\n        24: 2,  # 'ค'\n        8: 0,  # 'ง'\n        26: 1,  # 'จ'\n        52: 1,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 3,  # 'ด'\n        19: 2,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 1,  # 'บ'\n        25: 3,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 1,  # 'ฟ'\n        45: 1,  # 'ภ'\n        9: 1,  # 'ม'\n        16: 2,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 3,  # 'ล'\n        12: 0,  # 'ว'\n        42: 1,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 0,  # 'ห'\n        4: 2,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    29: {  # 'ใ'\n        5: 2,  # 'ก'\n        30: 0,  # 'ข'\n        24: 1,  # 'ค'\n        8: 0,  # 'ง'\n        26: 3,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 3,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 3,  # 'ด'\n        19: 1,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 2,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 1,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 3,  # 'ส'\n        21: 3,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    33: {  # 'ไ'\n        5: 1,  # 'ก'\n        30: 2,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 3,  # 'ด'\n        19: 1,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 3,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 1,  # 'บ'\n        25: 3,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 2,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 0,  # 'ย'\n        2: 3,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 1,  # 'ล'\n        12: 3,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 2,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    50: {  # 'ๆ'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    37: {  # '็'\n        5: 2,  # 'ก'\n        30: 1,  # 'ข'\n        24: 2,  # 'ค'\n        8: 2,  # 'ง'\n        26: 3,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 1,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 1,  # 'ด'\n        19: 2,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 3,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 1,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 2,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 0,  # 'ห'\n        4: 1,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 1,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    6: {  # '่'\n        5: 2,  # 'ก'\n        30: 1,  # 'ข'\n        24: 2,  # 'ค'\n        8: 3,  # 'ง'\n        26: 2,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 1,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 1,  # 'ด'\n        19: 2,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 1,  # 'ธ'\n        3: 3,  # 'น'\n        17: 1,  # 'บ'\n        25: 2,  # 'ป'\n        39: 2,  # 'ผ'\n        62: 1,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 3,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 2,  # 'ล'\n        12: 3,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 1,  # 'ห'\n        4: 3,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 1,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 3,  # 'า'\n        36: 2,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 3,  # 'เ'\n        28: 2,  # 'แ'\n        41: 1,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 1,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    7: {  # '้'\n        5: 2,  # 'ก'\n        30: 1,  # 'ข'\n        24: 2,  # 'ค'\n        8: 3,  # 'ง'\n        26: 2,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 1,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 1,  # 'ด'\n        19: 2,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 2,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 3,  # 'น'\n        17: 2,  # 'บ'\n        25: 2,  # 'ป'\n        39: 2,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 1,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 3,  # 'ม'\n        16: 2,  # 'ย'\n        2: 2,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 1,  # 'ล'\n        12: 3,  # 'ว'\n        42: 1,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 2,  # 'ส'\n        21: 2,  # 'ห'\n        4: 3,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 3,  # 'า'\n        36: 2,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 2,  # 'เ'\n        28: 2,  # 'แ'\n        41: 1,  # 'โ'\n        29: 2,  # 'ใ'\n        33: 2,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    38: {  # '์'\n        5: 2,  # 'ก'\n        30: 1,  # 'ข'\n        24: 1,  # 'ค'\n        8: 0,  # 'ง'\n        26: 1,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 1,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 2,  # 'ด'\n        19: 1,  # 'ต'\n        44: 1,  # 'ถ'\n        14: 1,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 1,  # 'น'\n        17: 1,  # 'บ'\n        25: 1,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 1,  # 'พ'\n        54: 1,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 2,  # 'ม'\n        16: 0,  # 'ย'\n        2: 1,  # 'ร'\n        61: 1,  # 'ฤ'\n        15: 1,  # 'ล'\n        12: 1,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 1,  # 'ส'\n        21: 1,  # 'ห'\n        4: 2,  # 'อ'\n        63: 1,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 2,  # 'เ'\n        28: 2,  # 'แ'\n        41: 1,  # 'โ'\n        29: 1,  # 'ใ'\n        33: 1,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 0,  # '๑'\n        59: 0,  # '๒'\n        60: 0,  # '๕'\n    },\n    56: {  # '๑'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 2,  # '๑'\n        59: 1,  # '๒'\n        60: 1,  # '๕'\n    },\n    59: {  # '๒'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 1,  # '๑'\n        59: 1,  # '๒'\n        60: 3,  # '๕'\n    },\n    60: {  # '๕'\n        5: 0,  # 'ก'\n        30: 0,  # 'ข'\n        24: 0,  # 'ค'\n        8: 0,  # 'ง'\n        26: 0,  # 'จ'\n        52: 0,  # 'ฉ'\n        34: 0,  # 'ช'\n        51: 0,  # 'ซ'\n        47: 0,  # 'ญ'\n        58: 0,  # 'ฎ'\n        57: 0,  # 'ฏ'\n        49: 0,  # 'ฐ'\n        53: 0,  # 'ฑ'\n        55: 0,  # 'ฒ'\n        43: 0,  # 'ณ'\n        20: 0,  # 'ด'\n        19: 0,  # 'ต'\n        44: 0,  # 'ถ'\n        14: 0,  # 'ท'\n        48: 0,  # 'ธ'\n        3: 0,  # 'น'\n        17: 0,  # 'บ'\n        25: 0,  # 'ป'\n        39: 0,  # 'ผ'\n        62: 0,  # 'ฝ'\n        31: 0,  # 'พ'\n        54: 0,  # 'ฟ'\n        45: 0,  # 'ภ'\n        9: 0,  # 'ม'\n        16: 0,  # 'ย'\n        2: 0,  # 'ร'\n        61: 0,  # 'ฤ'\n        15: 0,  # 'ล'\n        12: 0,  # 'ว'\n        42: 0,  # 'ศ'\n        46: 0,  # 'ษ'\n        18: 0,  # 'ส'\n        21: 0,  # 'ห'\n        4: 0,  # 'อ'\n        63: 0,  # 'ฯ'\n        22: 0,  # 'ะ'\n        10: 0,  # 'ั'\n        1: 0,  # 'า'\n        36: 0,  # 'ำ'\n        23: 0,  # 'ิ'\n        13: 0,  # 'ี'\n        40: 0,  # 'ึ'\n        27: 0,  # 'ื'\n        32: 0,  # 'ุ'\n        35: 0,  # 'ู'\n        11: 0,  # 'เ'\n        28: 0,  # 'แ'\n        41: 0,  # 'โ'\n        29: 0,  # 'ใ'\n        33: 0,  # 'ไ'\n        50: 0,  # 'ๆ'\n        37: 0,  # '็'\n        6: 0,  # '่'\n        7: 0,  # '้'\n        38: 0,  # '์'\n        56: 2,  # '๑'\n        59: 1,  # '๒'\n        60: 0,  # '๕'\n    },\n}\n\n# 255: Undefined characters that did not exist in training text\n# 254: Carriage/Return\n# 253: symbol (punctuation) that does not belong to word\n# 252: 0 - 9\n# 251: Control characters\n\n# Character Mapping Table(s):\nTIS_620_THAI_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 254,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 254,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 253,  # ' '\n     33: 253,  # '!'\n     34: 253,  # '\"'\n     35: 253,  # '#'\n     36: 253,  # '$'\n     37: 253,  # '%'\n     38: 253,  # '&'\n     39: 253,  # \"'\"\n     40: 253,  # '('\n     41: 253,  # ')'\n     42: 253,  # '*'\n     43: 253,  # '+'\n     44: 253,  # ','\n     45: 253,  # '-'\n     46: 253,  # '.'\n     47: 253,  # '/'\n     48: 252,  # '0'\n     49: 252,  # '1'\n     50: 252,  # '2'\n     51: 252,  # '3'\n     52: 252,  # '4'\n     53: 252,  # '5'\n     54: 252,  # '6'\n     55: 252,  # '7'\n     56: 252,  # '8'\n     57: 252,  # '9'\n     58: 253,  # ':'\n     59: 253,  # ';'\n     60: 253,  # '<'\n     61: 253,  # '='\n     62: 253,  # '>'\n     63: 253,  # '?'\n     64: 253,  # '@'\n     65: 182,  # 'A'\n     66: 106,  # 'B'\n     67: 107,  # 'C'\n     68: 100,  # 'D'\n     69: 183,  # 'E'\n     70: 184,  # 'F'\n     71: 185,  # 'G'\n     72: 101,  # 'H'\n     73: 94,  # 'I'\n     74: 186,  # 'J'\n     75: 187,  # 'K'\n     76: 108,  # 'L'\n     77: 109,  # 'M'\n     78: 110,  # 'N'\n     79: 111,  # 'O'\n     80: 188,  # 'P'\n     81: 189,  # 'Q'\n     82: 190,  # 'R'\n     83: 89,  # 'S'\n     84: 95,  # 'T'\n     85: 112,  # 'U'\n     86: 113,  # 'V'\n     87: 191,  # 'W'\n     88: 192,  # 'X'\n     89: 193,  # 'Y'\n     90: 194,  # 'Z'\n     91: 253,  # '['\n     92: 253,  # '\\\\'\n     93: 253,  # ']'\n     94: 253,  # '^'\n     95: 253,  # '_'\n     96: 253,  # '`'\n     97: 64,  # 'a'\n     98: 72,  # 'b'\n     99: 73,  # 'c'\n     100: 114,  # 'd'\n     101: 74,  # 'e'\n     102: 115,  # 'f'\n     103: 116,  # 'g'\n     104: 102,  # 'h'\n     105: 81,  # 'i'\n     106: 201,  # 'j'\n     107: 117,  # 'k'\n     108: 90,  # 'l'\n     109: 103,  # 'm'\n     110: 78,  # 'n'\n     111: 82,  # 'o'\n     112: 96,  # 'p'\n     113: 202,  # 'q'\n     114: 91,  # 'r'\n     115: 79,  # 's'\n     116: 84,  # 't'\n     117: 104,  # 'u'\n     118: 105,  # 'v'\n     119: 97,  # 'w'\n     120: 98,  # 'x'\n     121: 92,  # 'y'\n     122: 203,  # 'z'\n     123: 253,  # '{'\n     124: 253,  # '|'\n     125: 253,  # '}'\n     126: 253,  # '~'\n     127: 253,  # '\\x7f'\n     128: 209,  # '\\x80'\n     129: 210,  # '\\x81'\n     130: 211,  # '\\x82'\n     131: 212,  # '\\x83'\n     132: 213,  # '\\x84'\n     133: 88,  # '\\x85'\n     134: 214,  # '\\x86'\n     135: 215,  # '\\x87'\n     136: 216,  # '\\x88'\n     137: 217,  # '\\x89'\n     138: 218,  # '\\x8a'\n     139: 219,  # '\\x8b'\n     140: 220,  # '\\x8c'\n     141: 118,  # '\\x8d'\n     142: 221,  # '\\x8e'\n     143: 222,  # '\\x8f'\n     144: 223,  # '\\x90'\n     145: 224,  # '\\x91'\n     146: 99,  # '\\x92'\n     147: 85,  # '\\x93'\n     148: 83,  # '\\x94'\n     149: 225,  # '\\x95'\n     150: 226,  # '\\x96'\n     151: 227,  # '\\x97'\n     152: 228,  # '\\x98'\n     153: 229,  # '\\x99'\n     154: 230,  # '\\x9a'\n     155: 231,  # '\\x9b'\n     156: 232,  # '\\x9c'\n     157: 233,  # '\\x9d'\n     158: 234,  # '\\x9e'\n     159: 235,  # '\\x9f'\n     160: 236,  # None\n     161: 5,  # 'ก'\n     162: 30,  # 'ข'\n     163: 237,  # 'ฃ'\n     164: 24,  # 'ค'\n     165: 238,  # 'ฅ'\n     166: 75,  # 'ฆ'\n     167: 8,  # 'ง'\n     168: 26,  # 'จ'\n     169: 52,  # 'ฉ'\n     170: 34,  # 'ช'\n     171: 51,  # 'ซ'\n     172: 119,  # 'ฌ'\n     173: 47,  # 'ญ'\n     174: 58,  # 'ฎ'\n     175: 57,  # 'ฏ'\n     176: 49,  # 'ฐ'\n     177: 53,  # 'ฑ'\n     178: 55,  # 'ฒ'\n     179: 43,  # 'ณ'\n     180: 20,  # 'ด'\n     181: 19,  # 'ต'\n     182: 44,  # 'ถ'\n     183: 14,  # 'ท'\n     184: 48,  # 'ธ'\n     185: 3,  # 'น'\n     186: 17,  # 'บ'\n     187: 25,  # 'ป'\n     188: 39,  # 'ผ'\n     189: 62,  # 'ฝ'\n     190: 31,  # 'พ'\n     191: 54,  # 'ฟ'\n     192: 45,  # 'ภ'\n     193: 9,  # 'ม'\n     194: 16,  # 'ย'\n     195: 2,  # 'ร'\n     196: 61,  # 'ฤ'\n     197: 15,  # 'ล'\n     198: 239,  # 'ฦ'\n     199: 12,  # 'ว'\n     200: 42,  # 'ศ'\n     201: 46,  # 'ษ'\n     202: 18,  # 'ส'\n     203: 21,  # 'ห'\n     204: 76,  # 'ฬ'\n     205: 4,  # 'อ'\n     206: 66,  # 'ฮ'\n     207: 63,  # 'ฯ'\n     208: 22,  # 'ะ'\n     209: 10,  # 'ั'\n     210: 1,  # 'า'\n     211: 36,  # 'ำ'\n     212: 23,  # 'ิ'\n     213: 13,  # 'ี'\n     214: 40,  # 'ึ'\n     215: 27,  # 'ื'\n     216: 32,  # 'ุ'\n     217: 35,  # 'ู'\n     218: 86,  # 'ฺ'\n     219: 240,  # None\n     220: 241,  # None\n     221: 242,  # None\n     222: 243,  # None\n     223: 244,  # '฿'\n     224: 11,  # 'เ'\n     225: 28,  # 'แ'\n     226: 41,  # 'โ'\n     227: 29,  # 'ใ'\n     228: 33,  # 'ไ'\n     229: 245,  # 'ๅ'\n     230: 50,  # 'ๆ'\n     231: 37,  # '็'\n     232: 6,  # '่'\n     233: 7,  # '้'\n     234: 67,  # '๊'\n     235: 77,  # '๋'\n     236: 38,  # '์'\n     237: 93,  # 'ํ'\n     238: 246,  # '๎'\n     239: 247,  # '๏'\n     240: 68,  # '๐'\n     241: 56,  # '๑'\n     242: 59,  # '๒'\n     243: 65,  # '๓'\n     244: 69,  # '๔'\n     245: 60,  # '๕'\n     246: 70,  # '๖'\n     247: 80,  # '๗'\n     248: 71,  # '๘'\n     249: 87,  # '๙'\n     250: 248,  # '๚'\n     251: 249,  # '๛'\n     252: 250,  # None\n     253: 251,  # None\n     254: 252,  # None\n     255: 253,  # None\n}\n\nTIS_620_THAI_MODEL = SingleByteCharSetModel(charset_name='TIS-620',\n                                            language='Thai',\n                                            char_to_order_map=TIS_620_THAI_CHAR_TO_ORDER,\n                                            language_model=THAI_LANG_MODEL,\n                                            typical_positive_ratio=0.926386,\n                                            keep_ascii_letters=False,\n                                            alphabet='กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛')\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/langturkishmodel.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nfrom chardet.sbcharsetprober import SingleByteCharSetModel\n\n\n# 3: Positive\n# 2: Likely\n# 1: Unlikely\n# 0: Negative\n\nTURKISH_LANG_MODEL = {\n    23: {  # 'A'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 0,  # 'c'\n        12: 2,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 1,  # 'g'\n        25: 1,  # 'h'\n        3: 1,  # 'i'\n        24: 0,  # 'j'\n        10: 2,  # 'k'\n        5: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 1,  # 'r'\n        8: 1,  # 's'\n        9: 1,  # 't'\n        14: 1,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 3,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 0,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    37: {  # 'B'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 2,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 2,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 1,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 1,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 1,  # 'Y'\n        56: 0,  # 'Z'\n        1: 2,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 0,  # 'k'\n        5: 0,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 2,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 1,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 1,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 0,  # 'ı'\n        40: 1,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    47: {  # 'C'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 1,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 1,  # 'L'\n        20: 0,  # 'M'\n        46: 1,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 1,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 1,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 2,  # 'j'\n        10: 1,  # 'k'\n        5: 2,  # 'l'\n        13: 2,  # 'm'\n        4: 2,  # 'n'\n        15: 1,  # 'o'\n        26: 0,  # 'p'\n        7: 2,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 1,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    39: {  # 'D'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 1,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 1,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 2,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 2,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 0,  # 'k'\n        5: 1,  # 'l'\n        13: 3,  # 'm'\n        4: 0,  # 'n'\n        15: 1,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 1,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 1,  # 'z'\n        63: 0,  # '·'\n        54: 1,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 1,  # 'ı'\n        40: 1,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    29: {  # 'E'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 1,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 0,  # 'c'\n        12: 2,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 1,  # 'g'\n        25: 0,  # 'h'\n        3: 1,  # 'i'\n        24: 1,  # 'j'\n        10: 0,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 1,  # 's'\n        9: 1,  # 't'\n        14: 1,  # 'u'\n        32: 1,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 2,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    52: {  # 'F'\n        23: 0,  # 'A'\n        37: 1,  # 'B'\n        47: 1,  # 'C'\n        39: 1,  # 'D'\n        29: 1,  # 'E'\n        52: 2,  # 'F'\n        36: 0,  # 'G'\n        45: 2,  # 'H'\n        53: 1,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 1,  # 'N'\n        42: 1,  # 'O'\n        48: 2,  # 'P'\n        44: 1,  # 'R'\n        35: 1,  # 'S'\n        31: 1,  # 'T'\n        51: 1,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 2,  # 'Y'\n        56: 0,  # 'Z'\n        1: 0,  # 'a'\n        21: 1,  # 'b'\n        28: 1,  # 'c'\n        12: 1,  # 'd'\n        2: 0,  # 'e'\n        18: 1,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 2,  # 'i'\n        24: 1,  # 'j'\n        10: 0,  # 'k'\n        5: 0,  # 'l'\n        13: 1,  # 'm'\n        4: 2,  # 'n'\n        15: 1,  # 'o'\n        26: 0,  # 'p'\n        7: 2,  # 'r'\n        8: 1,  # 's'\n        9: 1,  # 't'\n        14: 1,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 1,  # 'y'\n        22: 1,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 1,  # 'Ö'\n        55: 2,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 2,  # 'ö'\n        17: 0,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 2,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 2,  # 'ş'\n    },\n    36: {  # 'G'\n        23: 1,  # 'A'\n        37: 0,  # 'B'\n        47: 1,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 1,  # 'F'\n        36: 2,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 2,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 2,  # 'N'\n        42: 1,  # 'O'\n        48: 1,  # 'P'\n        44: 1,  # 'R'\n        35: 1,  # 'S'\n        31: 0,  # 'T'\n        51: 1,  # 'U'\n        38: 2,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 1,  # 'c'\n        12: 0,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 1,  # 'j'\n        10: 1,  # 'k'\n        5: 0,  # 'l'\n        13: 3,  # 'm'\n        4: 2,  # 'n'\n        15: 0,  # 'o'\n        26: 1,  # 'p'\n        7: 0,  # 'r'\n        8: 1,  # 's'\n        9: 1,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 1,  # 'x'\n        11: 0,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 1,  # 'Ç'\n        50: 2,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 1,  # 'â'\n        33: 2,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 2,  # 'ı'\n        40: 2,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    45: {  # 'H'\n        23: 0,  # 'A'\n        37: 1,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 2,  # 'F'\n        36: 2,  # 'G'\n        45: 1,  # 'H'\n        53: 1,  # 'I'\n        60: 0,  # 'J'\n        16: 2,  # 'K'\n        49: 1,  # 'L'\n        20: 0,  # 'M'\n        46: 1,  # 'N'\n        42: 1,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 2,  # 'S'\n        31: 0,  # 'T'\n        51: 1,  # 'U'\n        38: 2,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 2,  # 'i'\n        24: 0,  # 'j'\n        10: 1,  # 'k'\n        5: 0,  # 'l'\n        13: 2,  # 'm'\n        4: 0,  # 'n'\n        15: 1,  # 'o'\n        26: 1,  # 'p'\n        7: 1,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 1,  # 'Ç'\n        50: 1,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 0,  # 'ü'\n        30: 2,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 0,  # 'ı'\n        40: 2,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    53: {  # 'I'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 1,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 2,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 2,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 2,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 0,  # 'k'\n        5: 2,  # 'l'\n        13: 2,  # 'm'\n        4: 0,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 2,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 1,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 2,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 0,  # 'ı'\n        40: 1,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    60: {  # 'J'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 0,  # 'a'\n        21: 1,  # 'b'\n        28: 0,  # 'c'\n        12: 1,  # 'd'\n        2: 0,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 1,  # 'i'\n        24: 0,  # 'j'\n        10: 0,  # 'k'\n        5: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 1,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 1,  # 's'\n        9: 0,  # 't'\n        14: 0,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 0,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    16: {  # 'K'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 3,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 2,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 2,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 2,  # 'a'\n        21: 3,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 1,  # 'e'\n        18: 3,  # 'f'\n        27: 3,  # 'g'\n        25: 3,  # 'h'\n        3: 3,  # 'i'\n        24: 2,  # 'j'\n        10: 3,  # 'k'\n        5: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 1,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 0,  # 'u'\n        32: 3,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 2,  # 'y'\n        22: 1,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 2,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    49: {  # 'L'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 2,  # 'E'\n        52: 0,  # 'F'\n        36: 1,  # 'G'\n        45: 1,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 0,  # 'N'\n        42: 2,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 1,  # 'Y'\n        56: 0,  # 'Z'\n        1: 0,  # 'a'\n        21: 3,  # 'b'\n        28: 0,  # 'c'\n        12: 2,  # 'd'\n        2: 0,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 2,  # 'i'\n        24: 0,  # 'j'\n        10: 1,  # 'k'\n        5: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 2,  # 'n'\n        15: 1,  # 'o'\n        26: 1,  # 'p'\n        7: 1,  # 'r'\n        8: 1,  # 's'\n        9: 1,  # 't'\n        14: 0,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 2,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 2,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 1,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 2,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    20: {  # 'M'\n        23: 1,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 1,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 2,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 1,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 2,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 1,  # 'g'\n        25: 1,  # 'h'\n        3: 2,  # 'i'\n        24: 2,  # 'j'\n        10: 2,  # 'k'\n        5: 2,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 1,  # 'p'\n        7: 3,  # 'r'\n        8: 0,  # 's'\n        9: 2,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 2,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 3,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    46: {  # 'N'\n        23: 0,  # 'A'\n        37: 1,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 1,  # 'F'\n        36: 1,  # 'G'\n        45: 1,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 2,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 1,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 1,  # 'R'\n        35: 1,  # 'S'\n        31: 0,  # 'T'\n        51: 1,  # 'U'\n        38: 2,  # 'V'\n        62: 0,  # 'W'\n        43: 1,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 1,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 2,  # 'j'\n        10: 1,  # 'k'\n        5: 1,  # 'l'\n        13: 3,  # 'm'\n        4: 2,  # 'n'\n        15: 1,  # 'o'\n        26: 1,  # 'p'\n        7: 1,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 1,  # 'x'\n        11: 1,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 1,  # 'Ç'\n        50: 1,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 2,  # 'ı'\n        40: 1,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    42: {  # 'O'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 1,  # 'F'\n        36: 0,  # 'G'\n        45: 1,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 2,  # 'K'\n        49: 1,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 2,  # 'P'\n        44: 1,  # 'R'\n        35: 1,  # 'S'\n        31: 0,  # 'T'\n        51: 1,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 2,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 0,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 0,  # 'n'\n        15: 1,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 2,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 2,  # 'Ç'\n        50: 1,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 2,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 0,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 2,  # 'İ'\n        6: 1,  # 'ı'\n        40: 1,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    48: {  # 'P'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 2,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 2,  # 'F'\n        36: 1,  # 'G'\n        45: 1,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 2,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 1,  # 'N'\n        42: 1,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 1,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 2,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 1,  # 'k'\n        5: 0,  # 'l'\n        13: 2,  # 'm'\n        4: 0,  # 'n'\n        15: 2,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 2,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 2,  # 'x'\n        11: 0,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 1,  # 'Ç'\n        50: 2,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 2,  # 'ö'\n        17: 0,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 0,  # 'ı'\n        40: 2,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    44: {  # 'R'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 1,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 1,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 1,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 1,  # 'b'\n        28: 1,  # 'c'\n        12: 0,  # 'd'\n        2: 2,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 1,  # 'k'\n        5: 2,  # 'l'\n        13: 2,  # 'm'\n        4: 0,  # 'n'\n        15: 1,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 2,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 1,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 1,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 1,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 2,  # 'ı'\n        40: 1,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    35: {  # 'S'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 1,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 1,  # 'F'\n        36: 1,  # 'G'\n        45: 1,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 1,  # 'L'\n        20: 1,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 1,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 1,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 1,  # 'k'\n        5: 1,  # 'l'\n        13: 2,  # 'm'\n        4: 1,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 1,  # 't'\n        14: 2,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 1,  # 'z'\n        63: 0,  # '·'\n        54: 2,  # 'Ç'\n        50: 2,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 3,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 2,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    31: {  # 'T'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 1,  # 'J'\n        16: 2,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 2,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 2,  # 'b'\n        28: 0,  # 'c'\n        12: 1,  # 'd'\n        2: 3,  # 'e'\n        18: 2,  # 'f'\n        27: 2,  # 'g'\n        25: 0,  # 'h'\n        3: 1,  # 'i'\n        24: 1,  # 'j'\n        10: 2,  # 'k'\n        5: 2,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 2,  # 'p'\n        7: 2,  # 'r'\n        8: 0,  # 's'\n        9: 2,  # 't'\n        14: 2,  # 'u'\n        32: 1,  # 'v'\n        57: 1,  # 'w'\n        58: 1,  # 'x'\n        11: 2,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 1,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    51: {  # 'U'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 1,  # 'F'\n        36: 1,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 1,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 1,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 1,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 1,  # 'c'\n        12: 0,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 2,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 1,  # 'k'\n        5: 1,  # 'l'\n        13: 3,  # 'm'\n        4: 2,  # 'n'\n        15: 0,  # 'o'\n        26: 1,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 2,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 1,  # 'Ç'\n        50: 1,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 2,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    38: {  # 'V'\n        23: 1,  # 'A'\n        37: 1,  # 'B'\n        47: 1,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 2,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 3,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 1,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 1,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 0,  # 'k'\n        5: 2,  # 'l'\n        13: 2,  # 'm'\n        4: 0,  # 'n'\n        15: 2,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 1,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 1,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 1,  # 'Ç'\n        50: 1,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 1,  # 'â'\n        33: 2,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 0,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 3,  # 'ı'\n        40: 2,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    62: {  # 'W'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 0,  # 'a'\n        21: 0,  # 'b'\n        28: 0,  # 'c'\n        12: 0,  # 'd'\n        2: 0,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 0,  # 'k'\n        5: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 0,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 0,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 0,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    43: {  # 'Y'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 1,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 2,  # 'F'\n        36: 0,  # 'G'\n        45: 1,  # 'H'\n        53: 1,  # 'I'\n        60: 0,  # 'J'\n        16: 2,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 2,  # 'N'\n        42: 0,  # 'O'\n        48: 2,  # 'P'\n        44: 1,  # 'R'\n        35: 1,  # 'S'\n        31: 0,  # 'T'\n        51: 1,  # 'U'\n        38: 2,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 2,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 1,  # 'j'\n        10: 1,  # 'k'\n        5: 1,  # 'l'\n        13: 3,  # 'm'\n        4: 0,  # 'n'\n        15: 2,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 1,  # 'x'\n        11: 0,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 1,  # 'Ç'\n        50: 2,  # 'Ö'\n        55: 1,  # 'Ü'\n        59: 1,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 0,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 0,  # 'ı'\n        40: 2,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    56: {  # 'Z'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 2,  # 'Z'\n        1: 2,  # 'a'\n        21: 1,  # 'b'\n        28: 0,  # 'c'\n        12: 0,  # 'd'\n        2: 2,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 2,  # 'i'\n        24: 1,  # 'j'\n        10: 0,  # 'k'\n        5: 0,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 1,  # 'r'\n        8: 1,  # 's'\n        9: 0,  # 't'\n        14: 2,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 1,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 1,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    1: {  # 'a'\n        23: 3,  # 'A'\n        37: 0,  # 'B'\n        47: 1,  # 'C'\n        39: 0,  # 'D'\n        29: 3,  # 'E'\n        52: 0,  # 'F'\n        36: 1,  # 'G'\n        45: 1,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 3,  # 'M'\n        46: 1,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 3,  # 'T'\n        51: 0,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 2,  # 'Z'\n        1: 2,  # 'a'\n        21: 3,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 2,  # 'e'\n        18: 3,  # 'f'\n        27: 3,  # 'g'\n        25: 3,  # 'h'\n        3: 3,  # 'i'\n        24: 3,  # 'j'\n        10: 3,  # 'k'\n        5: 0,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        15: 1,  # 'o'\n        26: 3,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 3,  # 'u'\n        32: 3,  # 'v'\n        57: 2,  # 'w'\n        58: 0,  # 'x'\n        11: 3,  # 'y'\n        22: 0,  # 'z'\n        63: 1,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 1,  # 'î'\n        34: 1,  # 'ö'\n        17: 3,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    21: {  # 'b'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 1,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 1,  # 'J'\n        16: 2,  # 'K'\n        49: 0,  # 'L'\n        20: 2,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 1,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 1,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 2,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 3,  # 'g'\n        25: 1,  # 'h'\n        3: 3,  # 'i'\n        24: 2,  # 'j'\n        10: 3,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 3,  # 'p'\n        7: 1,  # 'r'\n        8: 2,  # 's'\n        9: 2,  # 't'\n        14: 2,  # 'u'\n        32: 1,  # 'v'\n        57: 0,  # 'w'\n        58: 1,  # 'x'\n        11: 3,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 2,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    28: {  # 'c'\n        23: 0,  # 'A'\n        37: 1,  # 'B'\n        47: 1,  # 'C'\n        39: 1,  # 'D'\n        29: 2,  # 'E'\n        52: 0,  # 'F'\n        36: 2,  # 'G'\n        45: 2,  # 'H'\n        53: 1,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 2,  # 'M'\n        46: 1,  # 'N'\n        42: 1,  # 'O'\n        48: 2,  # 'P'\n        44: 1,  # 'R'\n        35: 1,  # 'S'\n        31: 2,  # 'T'\n        51: 2,  # 'U'\n        38: 2,  # 'V'\n        62: 0,  # 'W'\n        43: 3,  # 'Y'\n        56: 0,  # 'Z'\n        1: 1,  # 'a'\n        21: 1,  # 'b'\n        28: 2,  # 'c'\n        12: 2,  # 'd'\n        2: 1,  # 'e'\n        18: 1,  # 'f'\n        27: 2,  # 'g'\n        25: 2,  # 'h'\n        3: 3,  # 'i'\n        24: 1,  # 'j'\n        10: 3,  # 'k'\n        5: 0,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        15: 2,  # 'o'\n        26: 2,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 1,  # 'u'\n        32: 0,  # 'v'\n        57: 1,  # 'w'\n        58: 0,  # 'x'\n        11: 2,  # 'y'\n        22: 1,  # 'z'\n        63: 1,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 1,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 1,  # 'î'\n        34: 2,  # 'ö'\n        17: 2,  # 'ü'\n        30: 2,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 2,  # 'ş'\n    },\n    12: {  # 'd'\n        23: 1,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 2,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 3,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 1,  # 'S'\n        31: 1,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 2,  # 'b'\n        28: 1,  # 'c'\n        12: 3,  # 'd'\n        2: 3,  # 'e'\n        18: 1,  # 'f'\n        27: 3,  # 'g'\n        25: 3,  # 'h'\n        3: 2,  # 'i'\n        24: 3,  # 'j'\n        10: 2,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 1,  # 'o'\n        26: 2,  # 'p'\n        7: 3,  # 'r'\n        8: 2,  # 's'\n        9: 2,  # 't'\n        14: 3,  # 'u'\n        32: 1,  # 'v'\n        57: 0,  # 'w'\n        58: 1,  # 'x'\n        11: 3,  # 'y'\n        22: 1,  # 'z'\n        63: 1,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 1,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 2,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    2: {  # 'e'\n        23: 2,  # 'A'\n        37: 0,  # 'B'\n        47: 2,  # 'C'\n        39: 0,  # 'D'\n        29: 3,  # 'E'\n        52: 1,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 1,  # 'K'\n        49: 0,  # 'L'\n        20: 3,  # 'M'\n        46: 1,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 1,  # 'R'\n        35: 0,  # 'S'\n        31: 3,  # 'T'\n        51: 0,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 1,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 3,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 2,  # 'e'\n        18: 3,  # 'f'\n        27: 3,  # 'g'\n        25: 3,  # 'h'\n        3: 3,  # 'i'\n        24: 3,  # 'j'\n        10: 3,  # 'k'\n        5: 0,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        15: 1,  # 'o'\n        26: 3,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 3,  # 'u'\n        32: 3,  # 'v'\n        57: 2,  # 'w'\n        58: 0,  # 'x'\n        11: 3,  # 'y'\n        22: 1,  # 'z'\n        63: 1,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 3,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    18: {  # 'f'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 2,  # 'K'\n        49: 0,  # 'L'\n        20: 2,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 2,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 1,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 3,  # 'e'\n        18: 2,  # 'f'\n        27: 1,  # 'g'\n        25: 1,  # 'h'\n        3: 1,  # 'i'\n        24: 1,  # 'j'\n        10: 1,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 2,  # 'p'\n        7: 1,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 1,  # 'u'\n        32: 2,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 1,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 1,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 1,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    27: {  # 'g'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 1,  # 'S'\n        31: 1,  # 'T'\n        51: 0,  # 'U'\n        38: 2,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 1,  # 'b'\n        28: 0,  # 'c'\n        12: 1,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 2,  # 'g'\n        25: 1,  # 'h'\n        3: 2,  # 'i'\n        24: 3,  # 'j'\n        10: 2,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 2,  # 'n'\n        15: 0,  # 'o'\n        26: 1,  # 'p'\n        7: 2,  # 'r'\n        8: 2,  # 's'\n        9: 3,  # 't'\n        14: 3,  # 'u'\n        32: 1,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 1,  # 'y'\n        22: 0,  # 'z'\n        63: 1,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 2,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    25: {  # 'h'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 2,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 0,  # 'c'\n        12: 2,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 1,  # 'g'\n        25: 2,  # 'h'\n        3: 2,  # 'i'\n        24: 3,  # 'j'\n        10: 3,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 1,  # 'o'\n        26: 1,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 2,  # 't'\n        14: 3,  # 'u'\n        32: 2,  # 'v'\n        57: 1,  # 'w'\n        58: 0,  # 'x'\n        11: 1,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    3: {  # 'i'\n        23: 2,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 1,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 3,  # 'M'\n        46: 0,  # 'N'\n        42: 1,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 1,  # 'S'\n        31: 2,  # 'T'\n        51: 0,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 2,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 3,  # 'e'\n        18: 2,  # 'f'\n        27: 3,  # 'g'\n        25: 1,  # 'h'\n        3: 3,  # 'i'\n        24: 2,  # 'j'\n        10: 3,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 1,  # 'o'\n        26: 3,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 3,  # 'u'\n        32: 2,  # 'v'\n        57: 1,  # 'w'\n        58: 1,  # 'x'\n        11: 3,  # 'y'\n        22: 1,  # 'z'\n        63: 1,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 1,  # 'Ü'\n        59: 0,  # 'â'\n        33: 2,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 3,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 2,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    24: {  # 'j'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 1,  # 'J'\n        16: 2,  # 'K'\n        49: 0,  # 'L'\n        20: 2,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 1,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 1,  # 'Z'\n        1: 3,  # 'a'\n        21: 1,  # 'b'\n        28: 1,  # 'c'\n        12: 3,  # 'd'\n        2: 3,  # 'e'\n        18: 2,  # 'f'\n        27: 1,  # 'g'\n        25: 1,  # 'h'\n        3: 2,  # 'i'\n        24: 1,  # 'j'\n        10: 2,  # 'k'\n        5: 2,  # 'l'\n        13: 3,  # 'm'\n        4: 2,  # 'n'\n        15: 0,  # 'o'\n        26: 1,  # 'p'\n        7: 2,  # 'r'\n        8: 3,  # 's'\n        9: 2,  # 't'\n        14: 3,  # 'u'\n        32: 2,  # 'v'\n        57: 0,  # 'w'\n        58: 2,  # 'x'\n        11: 1,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 1,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    10: {  # 'k'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 2,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 3,  # 'T'\n        51: 0,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 1,  # 'Z'\n        1: 3,  # 'a'\n        21: 2,  # 'b'\n        28: 0,  # 'c'\n        12: 2,  # 'd'\n        2: 3,  # 'e'\n        18: 1,  # 'f'\n        27: 2,  # 'g'\n        25: 2,  # 'h'\n        3: 3,  # 'i'\n        24: 2,  # 'j'\n        10: 2,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 3,  # 'p'\n        7: 2,  # 'r'\n        8: 2,  # 's'\n        9: 2,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 1,  # 'x'\n        11: 3,  # 'y'\n        22: 0,  # 'z'\n        63: 1,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 3,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 3,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    5: {  # 'l'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 3,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 2,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 1,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 0,  # 'a'\n        21: 3,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 1,  # 'e'\n        18: 3,  # 'f'\n        27: 3,  # 'g'\n        25: 2,  # 'h'\n        3: 3,  # 'i'\n        24: 2,  # 'j'\n        10: 3,  # 'k'\n        5: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 2,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 2,  # 'u'\n        32: 2,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 3,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 2,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    13: {  # 'm'\n        23: 1,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 3,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 3,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 3,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 1,  # 'Y'\n        56: 0,  # 'Z'\n        1: 2,  # 'a'\n        21: 3,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 2,  # 'e'\n        18: 3,  # 'f'\n        27: 3,  # 'g'\n        25: 3,  # 'h'\n        3: 3,  # 'i'\n        24: 3,  # 'j'\n        10: 3,  # 'k'\n        5: 0,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        15: 1,  # 'o'\n        26: 2,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 2,  # 'u'\n        32: 2,  # 'v'\n        57: 1,  # 'w'\n        58: 0,  # 'x'\n        11: 3,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 3,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    4: {  # 'n'\n        23: 1,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 1,  # 'H'\n        53: 0,  # 'I'\n        60: 2,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 3,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 2,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 2,  # 'b'\n        28: 1,  # 'c'\n        12: 3,  # 'd'\n        2: 3,  # 'e'\n        18: 1,  # 'f'\n        27: 2,  # 'g'\n        25: 3,  # 'h'\n        3: 2,  # 'i'\n        24: 2,  # 'j'\n        10: 3,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 1,  # 'o'\n        26: 3,  # 'p'\n        7: 2,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 3,  # 'u'\n        32: 2,  # 'v'\n        57: 0,  # 'w'\n        58: 2,  # 'x'\n        11: 3,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 2,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 1,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    15: {  # 'o'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 1,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 2,  # 'F'\n        36: 1,  # 'G'\n        45: 1,  # 'H'\n        53: 1,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 2,  # 'L'\n        20: 0,  # 'M'\n        46: 2,  # 'N'\n        42: 1,  # 'O'\n        48: 2,  # 'P'\n        44: 1,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 1,  # 'i'\n        24: 2,  # 'j'\n        10: 1,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 2,  # 'n'\n        15: 2,  # 'o'\n        26: 0,  # 'p'\n        7: 1,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 2,  # 'x'\n        11: 0,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 1,  # 'Ç'\n        50: 2,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 3,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 0,  # 'ü'\n        30: 2,  # 'ğ'\n        41: 2,  # 'İ'\n        6: 3,  # 'ı'\n        40: 2,  # 'Ş'\n        19: 2,  # 'ş'\n    },\n    26: {  # 'p'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 1,  # 'b'\n        28: 0,  # 'c'\n        12: 1,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 1,  # 'g'\n        25: 1,  # 'h'\n        3: 2,  # 'i'\n        24: 3,  # 'j'\n        10: 1,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 2,  # 'n'\n        15: 0,  # 'o'\n        26: 2,  # 'p'\n        7: 2,  # 'r'\n        8: 1,  # 's'\n        9: 1,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 1,  # 'x'\n        11: 1,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 3,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 1,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    7: {  # 'r'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 1,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 2,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 2,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 2,  # 'T'\n        51: 1,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 1,  # 'Z'\n        1: 3,  # 'a'\n        21: 1,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 2,  # 'g'\n        25: 3,  # 'h'\n        3: 2,  # 'i'\n        24: 2,  # 'j'\n        10: 3,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 2,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 3,  # 'u'\n        32: 2,  # 'v'\n        57: 0,  # 'w'\n        58: 1,  # 'x'\n        11: 2,  # 'y'\n        22: 0,  # 'z'\n        63: 1,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 2,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 3,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 2,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    8: {  # 's'\n        23: 1,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 1,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 3,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 2,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 1,  # 'Z'\n        1: 3,  # 'a'\n        21: 2,  # 'b'\n        28: 1,  # 'c'\n        12: 3,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 2,  # 'g'\n        25: 2,  # 'h'\n        3: 2,  # 'i'\n        24: 3,  # 'j'\n        10: 3,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 3,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 3,  # 'u'\n        32: 2,  # 'v'\n        57: 0,  # 'w'\n        58: 1,  # 'x'\n        11: 2,  # 'y'\n        22: 1,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 2,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 2,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    9: {  # 't'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 1,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 2,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 2,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 1,  # 'Z'\n        1: 3,  # 'a'\n        21: 3,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 3,  # 'e'\n        18: 2,  # 'f'\n        27: 2,  # 'g'\n        25: 2,  # 'h'\n        3: 2,  # 'i'\n        24: 2,  # 'j'\n        10: 3,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 2,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 3,  # 'u'\n        32: 3,  # 'v'\n        57: 0,  # 'w'\n        58: 2,  # 'x'\n        11: 2,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 3,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 2,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    14: {  # 'u'\n        23: 3,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 3,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 1,  # 'H'\n        53: 0,  # 'I'\n        60: 1,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 3,  # 'M'\n        46: 2,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 3,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 1,  # 'Y'\n        56: 2,  # 'Z'\n        1: 2,  # 'a'\n        21: 3,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 2,  # 'e'\n        18: 2,  # 'f'\n        27: 3,  # 'g'\n        25: 3,  # 'h'\n        3: 3,  # 'i'\n        24: 2,  # 'j'\n        10: 3,  # 'k'\n        5: 0,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 3,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 3,  # 'u'\n        32: 2,  # 'v'\n        57: 2,  # 'w'\n        58: 0,  # 'x'\n        11: 3,  # 'y'\n        22: 0,  # 'z'\n        63: 1,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 3,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    32: {  # 'v'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 0,  # 'c'\n        12: 3,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 1,  # 'j'\n        10: 1,  # 'k'\n        5: 3,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 1,  # 'p'\n        7: 1,  # 'r'\n        8: 2,  # 's'\n        9: 3,  # 't'\n        14: 3,  # 'u'\n        32: 1,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 2,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 1,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    57: {  # 'w'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 1,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 1,  # 'a'\n        21: 0,  # 'b'\n        28: 0,  # 'c'\n        12: 0,  # 'd'\n        2: 2,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 1,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 1,  # 'k'\n        5: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 1,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 1,  # 's'\n        9: 0,  # 't'\n        14: 1,  # 'u'\n        32: 0,  # 'v'\n        57: 2,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 0,  # 'z'\n        63: 1,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 1,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 0,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    58: {  # 'x'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 1,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 1,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 0,  # 'a'\n        21: 1,  # 'b'\n        28: 0,  # 'c'\n        12: 2,  # 'd'\n        2: 1,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 2,  # 'i'\n        24: 2,  # 'j'\n        10: 1,  # 'k'\n        5: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 2,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 1,  # 'r'\n        8: 2,  # 's'\n        9: 1,  # 't'\n        14: 0,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 2,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 1,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 2,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    11: {  # 'y'\n        23: 1,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 1,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 1,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 1,  # 'Y'\n        56: 1,  # 'Z'\n        1: 3,  # 'a'\n        21: 1,  # 'b'\n        28: 0,  # 'c'\n        12: 2,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 2,  # 'g'\n        25: 2,  # 'h'\n        3: 2,  # 'i'\n        24: 1,  # 'j'\n        10: 2,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 1,  # 'p'\n        7: 2,  # 'r'\n        8: 1,  # 's'\n        9: 2,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 1,  # 'x'\n        11: 3,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 3,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 2,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    22: {  # 'z'\n        23: 2,  # 'A'\n        37: 2,  # 'B'\n        47: 1,  # 'C'\n        39: 2,  # 'D'\n        29: 3,  # 'E'\n        52: 1,  # 'F'\n        36: 2,  # 'G'\n        45: 2,  # 'H'\n        53: 1,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 3,  # 'M'\n        46: 2,  # 'N'\n        42: 2,  # 'O'\n        48: 2,  # 'P'\n        44: 1,  # 'R'\n        35: 1,  # 'S'\n        31: 3,  # 'T'\n        51: 2,  # 'U'\n        38: 2,  # 'V'\n        62: 0,  # 'W'\n        43: 2,  # 'Y'\n        56: 1,  # 'Z'\n        1: 1,  # 'a'\n        21: 2,  # 'b'\n        28: 1,  # 'c'\n        12: 2,  # 'd'\n        2: 2,  # 'e'\n        18: 3,  # 'f'\n        27: 2,  # 'g'\n        25: 2,  # 'h'\n        3: 3,  # 'i'\n        24: 2,  # 'j'\n        10: 3,  # 'k'\n        5: 0,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        15: 2,  # 'o'\n        26: 2,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 0,  # 'u'\n        32: 2,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 3,  # 'y'\n        22: 2,  # 'z'\n        63: 1,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 2,  # 'Ü'\n        59: 1,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 2,  # 'ö'\n        17: 2,  # 'ü'\n        30: 2,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 3,  # 'ı'\n        40: 1,  # 'Ş'\n        19: 2,  # 'ş'\n    },\n    63: {  # '·'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 0,  # 'a'\n        21: 0,  # 'b'\n        28: 0,  # 'c'\n        12: 0,  # 'd'\n        2: 1,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 0,  # 'k'\n        5: 0,  # 'l'\n        13: 2,  # 'm'\n        4: 0,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 2,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 0,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    54: {  # 'Ç'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 1,  # 'C'\n        39: 1,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 1,  # 'G'\n        45: 1,  # 'H'\n        53: 1,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 1,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 1,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 2,  # 'Y'\n        56: 0,  # 'Z'\n        1: 0,  # 'a'\n        21: 1,  # 'b'\n        28: 0,  # 'c'\n        12: 1,  # 'd'\n        2: 0,  # 'e'\n        18: 0,  # 'f'\n        27: 1,  # 'g'\n        25: 0,  # 'h'\n        3: 3,  # 'i'\n        24: 0,  # 'j'\n        10: 1,  # 'k'\n        5: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 2,  # 'n'\n        15: 1,  # 'o'\n        26: 0,  # 'p'\n        7: 2,  # 'r'\n        8: 0,  # 's'\n        9: 1,  # 't'\n        14: 0,  # 'u'\n        32: 2,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 2,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 2,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    50: {  # 'Ö'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 1,  # 'C'\n        39: 1,  # 'D'\n        29: 2,  # 'E'\n        52: 0,  # 'F'\n        36: 1,  # 'G'\n        45: 2,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 1,  # 'N'\n        42: 2,  # 'O'\n        48: 2,  # 'P'\n        44: 1,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 1,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 2,  # 'Y'\n        56: 0,  # 'Z'\n        1: 0,  # 'a'\n        21: 2,  # 'b'\n        28: 1,  # 'c'\n        12: 2,  # 'd'\n        2: 0,  # 'e'\n        18: 1,  # 'f'\n        27: 1,  # 'g'\n        25: 1,  # 'h'\n        3: 2,  # 'i'\n        24: 0,  # 'j'\n        10: 2,  # 'k'\n        5: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 3,  # 'n'\n        15: 2,  # 'o'\n        26: 2,  # 'p'\n        7: 3,  # 'r'\n        8: 1,  # 's'\n        9: 2,  # 't'\n        14: 0,  # 'u'\n        32: 1,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 1,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 2,  # 'ö'\n        17: 2,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 2,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    55: {  # 'Ü'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 2,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 1,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 1,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 2,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 2,  # 'e'\n        18: 0,  # 'f'\n        27: 1,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 0,  # 'k'\n        5: 1,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 1,  # 't'\n        14: 2,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 1,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 1,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 0,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 0,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    59: {  # 'â'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 1,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 1,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 2,  # 'a'\n        21: 0,  # 'b'\n        28: 0,  # 'c'\n        12: 0,  # 'd'\n        2: 2,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 0,  # 'j'\n        10: 0,  # 'k'\n        5: 0,  # 'l'\n        13: 2,  # 'm'\n        4: 0,  # 'n'\n        15: 1,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 2,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 1,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 1,  # 'ı'\n        40: 1,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    33: {  # 'ç'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 3,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 2,  # 'T'\n        51: 0,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 0,  # 'Z'\n        1: 0,  # 'a'\n        21: 3,  # 'b'\n        28: 0,  # 'c'\n        12: 2,  # 'd'\n        2: 0,  # 'e'\n        18: 2,  # 'f'\n        27: 1,  # 'g'\n        25: 3,  # 'h'\n        3: 3,  # 'i'\n        24: 0,  # 'j'\n        10: 3,  # 'k'\n        5: 0,  # 'l'\n        13: 0,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 1,  # 'p'\n        7: 3,  # 'r'\n        8: 2,  # 's'\n        9: 3,  # 't'\n        14: 0,  # 'u'\n        32: 2,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 2,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 1,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    61: {  # 'î'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 0,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 0,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 1,  # 'Z'\n        1: 2,  # 'a'\n        21: 0,  # 'b'\n        28: 0,  # 'c'\n        12: 0,  # 'd'\n        2: 2,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 1,  # 'j'\n        10: 0,  # 'k'\n        5: 0,  # 'l'\n        13: 1,  # 'm'\n        4: 1,  # 'n'\n        15: 0,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 1,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 1,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 1,  # 'î'\n        34: 0,  # 'ö'\n        17: 0,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 1,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    34: {  # 'ö'\n        23: 0,  # 'A'\n        37: 1,  # 'B'\n        47: 1,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 2,  # 'F'\n        36: 1,  # 'G'\n        45: 1,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 1,  # 'L'\n        20: 0,  # 'M'\n        46: 1,  # 'N'\n        42: 1,  # 'O'\n        48: 2,  # 'P'\n        44: 1,  # 'R'\n        35: 1,  # 'S'\n        31: 1,  # 'T'\n        51: 1,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 1,  # 'Z'\n        1: 3,  # 'a'\n        21: 1,  # 'b'\n        28: 2,  # 'c'\n        12: 1,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 2,  # 'g'\n        25: 2,  # 'h'\n        3: 1,  # 'i'\n        24: 2,  # 'j'\n        10: 1,  # 'k'\n        5: 2,  # 'l'\n        13: 3,  # 'm'\n        4: 2,  # 'n'\n        15: 2,  # 'o'\n        26: 0,  # 'p'\n        7: 0,  # 'r'\n        8: 3,  # 's'\n        9: 1,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 1,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 1,  # 'Ç'\n        50: 2,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 2,  # 'ç'\n        61: 0,  # 'î'\n        34: 2,  # 'ö'\n        17: 0,  # 'ü'\n        30: 2,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 1,  # 'ı'\n        40: 2,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    17: {  # 'ü'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 1,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 0,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 1,  # 'J'\n        16: 1,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 0,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 1,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 0,  # 'Y'\n        56: 1,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 0,  # 'c'\n        12: 1,  # 'd'\n        2: 3,  # 'e'\n        18: 1,  # 'f'\n        27: 2,  # 'g'\n        25: 0,  # 'h'\n        3: 1,  # 'i'\n        24: 1,  # 'j'\n        10: 2,  # 'k'\n        5: 3,  # 'l'\n        13: 2,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 2,  # 'p'\n        7: 2,  # 'r'\n        8: 3,  # 's'\n        9: 2,  # 't'\n        14: 3,  # 'u'\n        32: 1,  # 'v'\n        57: 1,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 2,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 2,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    30: {  # 'ğ'\n        23: 0,  # 'A'\n        37: 2,  # 'B'\n        47: 1,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 2,  # 'F'\n        36: 1,  # 'G'\n        45: 0,  # 'H'\n        53: 1,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 1,  # 'M'\n        46: 2,  # 'N'\n        42: 2,  # 'O'\n        48: 1,  # 'P'\n        44: 1,  # 'R'\n        35: 0,  # 'S'\n        31: 1,  # 'T'\n        51: 0,  # 'U'\n        38: 2,  # 'V'\n        62: 0,  # 'W'\n        43: 2,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 0,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 2,  # 'e'\n        18: 0,  # 'f'\n        27: 0,  # 'g'\n        25: 0,  # 'h'\n        3: 0,  # 'i'\n        24: 3,  # 'j'\n        10: 1,  # 'k'\n        5: 2,  # 'l'\n        13: 3,  # 'm'\n        4: 0,  # 'n'\n        15: 1,  # 'o'\n        26: 0,  # 'p'\n        7: 1,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 2,  # 'Ç'\n        50: 2,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 0,  # 'î'\n        34: 2,  # 'ö'\n        17: 0,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 2,  # 'İ'\n        6: 2,  # 'ı'\n        40: 2,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    41: {  # 'İ'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 1,  # 'C'\n        39: 1,  # 'D'\n        29: 1,  # 'E'\n        52: 0,  # 'F'\n        36: 2,  # 'G'\n        45: 2,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 2,  # 'M'\n        46: 1,  # 'N'\n        42: 1,  # 'O'\n        48: 2,  # 'P'\n        44: 0,  # 'R'\n        35: 1,  # 'S'\n        31: 1,  # 'T'\n        51: 1,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 2,  # 'Y'\n        56: 0,  # 'Z'\n        1: 1,  # 'a'\n        21: 2,  # 'b'\n        28: 1,  # 'c'\n        12: 2,  # 'd'\n        2: 1,  # 'e'\n        18: 0,  # 'f'\n        27: 3,  # 'g'\n        25: 2,  # 'h'\n        3: 2,  # 'i'\n        24: 2,  # 'j'\n        10: 2,  # 'k'\n        5: 0,  # 'l'\n        13: 1,  # 'm'\n        4: 3,  # 'n'\n        15: 1,  # 'o'\n        26: 1,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 2,  # 't'\n        14: 0,  # 'u'\n        32: 0,  # 'v'\n        57: 1,  # 'w'\n        58: 0,  # 'x'\n        11: 2,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 1,  # 'Ü'\n        59: 1,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 1,  # 'ö'\n        17: 1,  # 'ü'\n        30: 2,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n    6: {  # 'ı'\n        23: 2,  # 'A'\n        37: 0,  # 'B'\n        47: 0,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 0,  # 'F'\n        36: 1,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 2,  # 'J'\n        16: 3,  # 'K'\n        49: 0,  # 'L'\n        20: 3,  # 'M'\n        46: 1,  # 'N'\n        42: 0,  # 'O'\n        48: 0,  # 'P'\n        44: 0,  # 'R'\n        35: 0,  # 'S'\n        31: 2,  # 'T'\n        51: 0,  # 'U'\n        38: 0,  # 'V'\n        62: 0,  # 'W'\n        43: 2,  # 'Y'\n        56: 1,  # 'Z'\n        1: 3,  # 'a'\n        21: 2,  # 'b'\n        28: 1,  # 'c'\n        12: 3,  # 'd'\n        2: 3,  # 'e'\n        18: 3,  # 'f'\n        27: 3,  # 'g'\n        25: 2,  # 'h'\n        3: 3,  # 'i'\n        24: 3,  # 'j'\n        10: 3,  # 'k'\n        5: 3,  # 'l'\n        13: 3,  # 'm'\n        4: 3,  # 'n'\n        15: 0,  # 'o'\n        26: 3,  # 'p'\n        7: 3,  # 'r'\n        8: 3,  # 's'\n        9: 3,  # 't'\n        14: 3,  # 'u'\n        32: 3,  # 'v'\n        57: 1,  # 'w'\n        58: 1,  # 'x'\n        11: 3,  # 'y'\n        22: 0,  # 'z'\n        63: 1,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 2,  # 'ç'\n        61: 0,  # 'î'\n        34: 0,  # 'ö'\n        17: 3,  # 'ü'\n        30: 0,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 3,  # 'ı'\n        40: 0,  # 'Ş'\n        19: 0,  # 'ş'\n    },\n    40: {  # 'Ş'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 1,  # 'C'\n        39: 1,  # 'D'\n        29: 1,  # 'E'\n        52: 0,  # 'F'\n        36: 1,  # 'G'\n        45: 2,  # 'H'\n        53: 1,  # 'I'\n        60: 0,  # 'J'\n        16: 0,  # 'K'\n        49: 0,  # 'L'\n        20: 2,  # 'M'\n        46: 1,  # 'N'\n        42: 1,  # 'O'\n        48: 2,  # 'P'\n        44: 2,  # 'R'\n        35: 1,  # 'S'\n        31: 1,  # 'T'\n        51: 0,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 2,  # 'Y'\n        56: 1,  # 'Z'\n        1: 0,  # 'a'\n        21: 2,  # 'b'\n        28: 0,  # 'c'\n        12: 2,  # 'd'\n        2: 0,  # 'e'\n        18: 3,  # 'f'\n        27: 0,  # 'g'\n        25: 2,  # 'h'\n        3: 3,  # 'i'\n        24: 2,  # 'j'\n        10: 1,  # 'k'\n        5: 0,  # 'l'\n        13: 1,  # 'm'\n        4: 3,  # 'n'\n        15: 2,  # 'o'\n        26: 0,  # 'p'\n        7: 3,  # 'r'\n        8: 2,  # 's'\n        9: 2,  # 't'\n        14: 1,  # 'u'\n        32: 3,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 2,  # 'y'\n        22: 0,  # 'z'\n        63: 0,  # '·'\n        54: 0,  # 'Ç'\n        50: 0,  # 'Ö'\n        55: 1,  # 'Ü'\n        59: 0,  # 'â'\n        33: 0,  # 'ç'\n        61: 0,  # 'î'\n        34: 2,  # 'ö'\n        17: 1,  # 'ü'\n        30: 2,  # 'ğ'\n        41: 0,  # 'İ'\n        6: 2,  # 'ı'\n        40: 1,  # 'Ş'\n        19: 2,  # 'ş'\n    },\n    19: {  # 'ş'\n        23: 0,  # 'A'\n        37: 0,  # 'B'\n        47: 1,  # 'C'\n        39: 0,  # 'D'\n        29: 0,  # 'E'\n        52: 2,  # 'F'\n        36: 1,  # 'G'\n        45: 0,  # 'H'\n        53: 0,  # 'I'\n        60: 0,  # 'J'\n        16: 3,  # 'K'\n        49: 2,  # 'L'\n        20: 0,  # 'M'\n        46: 1,  # 'N'\n        42: 1,  # 'O'\n        48: 1,  # 'P'\n        44: 1,  # 'R'\n        35: 1,  # 'S'\n        31: 0,  # 'T'\n        51: 1,  # 'U'\n        38: 1,  # 'V'\n        62: 0,  # 'W'\n        43: 1,  # 'Y'\n        56: 0,  # 'Z'\n        1: 3,  # 'a'\n        21: 1,  # 'b'\n        28: 2,  # 'c'\n        12: 0,  # 'd'\n        2: 3,  # 'e'\n        18: 0,  # 'f'\n        27: 2,  # 'g'\n        25: 1,  # 'h'\n        3: 1,  # 'i'\n        24: 0,  # 'j'\n        10: 2,  # 'k'\n        5: 2,  # 'l'\n        13: 3,  # 'm'\n        4: 0,  # 'n'\n        15: 0,  # 'o'\n        26: 1,  # 'p'\n        7: 3,  # 'r'\n        8: 0,  # 's'\n        9: 0,  # 't'\n        14: 3,  # 'u'\n        32: 0,  # 'v'\n        57: 0,  # 'w'\n        58: 0,  # 'x'\n        11: 0,  # 'y'\n        22: 2,  # 'z'\n        63: 0,  # '·'\n        54: 1,  # 'Ç'\n        50: 2,  # 'Ö'\n        55: 0,  # 'Ü'\n        59: 0,  # 'â'\n        33: 1,  # 'ç'\n        61: 1,  # 'î'\n        34: 2,  # 'ö'\n        17: 0,  # 'ü'\n        30: 1,  # 'ğ'\n        41: 1,  # 'İ'\n        6: 1,  # 'ı'\n        40: 1,  # 'Ş'\n        19: 1,  # 'ş'\n    },\n}\n\n# 255: Undefined characters that did not exist in training text\n# 254: Carriage/Return\n# 253: symbol (punctuation) that does not belong to word\n# 252: 0 - 9\n# 251: Control characters\n\n# Character Mapping Table(s):\nISO_8859_9_TURKISH_CHAR_TO_ORDER = {\n     0: 255,  # '\\x00'\n     1: 255,  # '\\x01'\n     2: 255,  # '\\x02'\n     3: 255,  # '\\x03'\n     4: 255,  # '\\x04'\n     5: 255,  # '\\x05'\n     6: 255,  # '\\x06'\n     7: 255,  # '\\x07'\n     8: 255,  # '\\x08'\n     9: 255,  # '\\t'\n     10: 255,  # '\\n'\n     11: 255,  # '\\x0b'\n     12: 255,  # '\\x0c'\n     13: 255,  # '\\r'\n     14: 255,  # '\\x0e'\n     15: 255,  # '\\x0f'\n     16: 255,  # '\\x10'\n     17: 255,  # '\\x11'\n     18: 255,  # '\\x12'\n     19: 255,  # '\\x13'\n     20: 255,  # '\\x14'\n     21: 255,  # '\\x15'\n     22: 255,  # '\\x16'\n     23: 255,  # '\\x17'\n     24: 255,  # '\\x18'\n     25: 255,  # '\\x19'\n     26: 255,  # '\\x1a'\n     27: 255,  # '\\x1b'\n     28: 255,  # '\\x1c'\n     29: 255,  # '\\x1d'\n     30: 255,  # '\\x1e'\n     31: 255,  # '\\x1f'\n     32: 255,  # ' '\n     33: 255,  # '!'\n     34: 255,  # '\"'\n     35: 255,  # '#'\n     36: 255,  # '$'\n     37: 255,  # '%'\n     38: 255,  # '&'\n     39: 255,  # \"'\"\n     40: 255,  # '('\n     41: 255,  # ')'\n     42: 255,  # '*'\n     43: 255,  # '+'\n     44: 255,  # ','\n     45: 255,  # '-'\n     46: 255,  # '.'\n     47: 255,  # '/'\n     48: 255,  # '0'\n     49: 255,  # '1'\n     50: 255,  # '2'\n     51: 255,  # '3'\n     52: 255,  # '4'\n     53: 255,  # '5'\n     54: 255,  # '6'\n     55: 255,  # '7'\n     56: 255,  # '8'\n     57: 255,  # '9'\n     58: 255,  # ':'\n     59: 255,  # ';'\n     60: 255,  # '<'\n     61: 255,  # '='\n     62: 255,  # '>'\n     63: 255,  # '?'\n     64: 255,  # '@'\n     65: 23,  # 'A'\n     66: 37,  # 'B'\n     67: 47,  # 'C'\n     68: 39,  # 'D'\n     69: 29,  # 'E'\n     70: 52,  # 'F'\n     71: 36,  # 'G'\n     72: 45,  # 'H'\n     73: 53,  # 'I'\n     74: 60,  # 'J'\n     75: 16,  # 'K'\n     76: 49,  # 'L'\n     77: 20,  # 'M'\n     78: 46,  # 'N'\n     79: 42,  # 'O'\n     80: 48,  # 'P'\n     81: 69,  # 'Q'\n     82: 44,  # 'R'\n     83: 35,  # 'S'\n     84: 31,  # 'T'\n     85: 51,  # 'U'\n     86: 38,  # 'V'\n     87: 62,  # 'W'\n     88: 65,  # 'X'\n     89: 43,  # 'Y'\n     90: 56,  # 'Z'\n     91: 255,  # '['\n     92: 255,  # '\\\\'\n     93: 255,  # ']'\n     94: 255,  # '^'\n     95: 255,  # '_'\n     96: 255,  # '`'\n     97: 1,  # 'a'\n     98: 21,  # 'b'\n     99: 28,  # 'c'\n     100: 12,  # 'd'\n     101: 2,  # 'e'\n     102: 18,  # 'f'\n     103: 27,  # 'g'\n     104: 25,  # 'h'\n     105: 3,  # 'i'\n     106: 24,  # 'j'\n     107: 10,  # 'k'\n     108: 5,  # 'l'\n     109: 13,  # 'm'\n     110: 4,  # 'n'\n     111: 15,  # 'o'\n     112: 26,  # 'p'\n     113: 64,  # 'q'\n     114: 7,  # 'r'\n     115: 8,  # 's'\n     116: 9,  # 't'\n     117: 14,  # 'u'\n     118: 32,  # 'v'\n     119: 57,  # 'w'\n     120: 58,  # 'x'\n     121: 11,  # 'y'\n     122: 22,  # 'z'\n     123: 255,  # '{'\n     124: 255,  # '|'\n     125: 255,  # '}'\n     126: 255,  # '~'\n     127: 255,  # '\\x7f'\n     128: 180,  # '\\x80'\n     129: 179,  # '\\x81'\n     130: 178,  # '\\x82'\n     131: 177,  # '\\x83'\n     132: 176,  # '\\x84'\n     133: 175,  # '\\x85'\n     134: 174,  # '\\x86'\n     135: 173,  # '\\x87'\n     136: 172,  # '\\x88'\n     137: 171,  # '\\x89'\n     138: 170,  # '\\x8a'\n     139: 169,  # '\\x8b'\n     140: 168,  # '\\x8c'\n     141: 167,  # '\\x8d'\n     142: 166,  # '\\x8e'\n     143: 165,  # '\\x8f'\n     144: 164,  # '\\x90'\n     145: 163,  # '\\x91'\n     146: 162,  # '\\x92'\n     147: 161,  # '\\x93'\n     148: 160,  # '\\x94'\n     149: 159,  # '\\x95'\n     150: 101,  # '\\x96'\n     151: 158,  # '\\x97'\n     152: 157,  # '\\x98'\n     153: 156,  # '\\x99'\n     154: 155,  # '\\x9a'\n     155: 154,  # '\\x9b'\n     156: 153,  # '\\x9c'\n     157: 152,  # '\\x9d'\n     158: 151,  # '\\x9e'\n     159: 106,  # '\\x9f'\n     160: 150,  # '\\xa0'\n     161: 149,  # '¡'\n     162: 148,  # '¢'\n     163: 147,  # '£'\n     164: 146,  # '¤'\n     165: 145,  # '¥'\n     166: 144,  # '¦'\n     167: 100,  # '§'\n     168: 143,  # '¨'\n     169: 142,  # '©'\n     170: 141,  # 'ª'\n     171: 140,  # '«'\n     172: 139,  # '¬'\n     173: 138,  # '\\xad'\n     174: 137,  # '®'\n     175: 136,  # '¯'\n     176: 94,  # '°'\n     177: 80,  # '±'\n     178: 93,  # '²'\n     179: 135,  # '³'\n     180: 105,  # '´'\n     181: 134,  # 'µ'\n     182: 133,  # '¶'\n     183: 63,  # '·'\n     184: 132,  # '¸'\n     185: 131,  # '¹'\n     186: 130,  # 'º'\n     187: 129,  # '»'\n     188: 128,  # '¼'\n     189: 127,  # '½'\n     190: 126,  # '¾'\n     191: 125,  # '¿'\n     192: 124,  # 'À'\n     193: 104,  # 'Á'\n     194: 73,  # 'Â'\n     195: 99,  # 'Ã'\n     196: 79,  # 'Ä'\n     197: 85,  # 'Å'\n     198: 123,  # 'Æ'\n     199: 54,  # 'Ç'\n     200: 122,  # 'È'\n     201: 98,  # 'É'\n     202: 92,  # 'Ê'\n     203: 121,  # 'Ë'\n     204: 120,  # 'Ì'\n     205: 91,  # 'Í'\n     206: 103,  # 'Î'\n     207: 119,  # 'Ï'\n     208: 68,  # 'Ğ'\n     209: 118,  # 'Ñ'\n     210: 117,  # 'Ò'\n     211: 97,  # 'Ó'\n     212: 116,  # 'Ô'\n     213: 115,  # 'Õ'\n     214: 50,  # 'Ö'\n     215: 90,  # '×'\n     216: 114,  # 'Ø'\n     217: 113,  # 'Ù'\n     218: 112,  # 'Ú'\n     219: 111,  # 'Û'\n     220: 55,  # 'Ü'\n     221: 41,  # 'İ'\n     222: 40,  # 'Ş'\n     223: 86,  # 'ß'\n     224: 89,  # 'à'\n     225: 70,  # 'á'\n     226: 59,  # 'â'\n     227: 78,  # 'ã'\n     228: 71,  # 'ä'\n     229: 82,  # 'å'\n     230: 88,  # 'æ'\n     231: 33,  # 'ç'\n     232: 77,  # 'è'\n     233: 66,  # 'é'\n     234: 84,  # 'ê'\n     235: 83,  # 'ë'\n     236: 110,  # 'ì'\n     237: 75,  # 'í'\n     238: 61,  # 'î'\n     239: 96,  # 'ï'\n     240: 30,  # 'ğ'\n     241: 67,  # 'ñ'\n     242: 109,  # 'ò'\n     243: 74,  # 'ó'\n     244: 87,  # 'ô'\n     245: 102,  # 'õ'\n     246: 34,  # 'ö'\n     247: 95,  # '÷'\n     248: 81,  # 'ø'\n     249: 108,  # 'ù'\n     250: 76,  # 'ú'\n     251: 72,  # 'û'\n     252: 17,  # 'ü'\n     253: 6,  # 'ı'\n     254: 19,  # 'ş'\n     255: 107,  # 'ÿ'\n}\n\nISO_8859_9_TURKISH_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-9',\n                                                  language='Turkish',\n                                                  char_to_order_map=ISO_8859_9_TURKISH_CHAR_TO_ORDER,\n                                                  language_model=TURKISH_LANG_MODEL,\n                                                  typical_positive_ratio=0.97029,\n                                                  keep_ascii_letters=True,\n                                                  alphabet='ABCDEFGHIJKLMNOPRSTUVYZabcdefghijklmnoprstuvyzÂÇÎÖÛÜâçîöûüĞğİıŞş')\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/latin1prober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Universal charset detector code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 2001\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#   Shy Shalom - original C code\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .charsetprober import CharSetProber\nfrom .enums import ProbingState\n\nFREQ_CAT_NUM = 4\n\nUDF = 0  # undefined\nOTH = 1  # other\nASC = 2  # ascii capital letter\nASS = 3  # ascii small letter\nACV = 4  # accent capital vowel\nACO = 5  # accent capital other\nASV = 6  # accent small vowel\nASO = 7  # accent small other\nCLASS_NUM = 8  # total classes\n\nLatin1_CharToClass = (\n    OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # 00 - 07\n    OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # 08 - 0F\n    OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # 10 - 17\n    OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # 18 - 1F\n    OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # 20 - 27\n    OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # 28 - 2F\n    OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # 30 - 37\n    OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # 38 - 3F\n    OTH, ASC, ASC, ASC, ASC, ASC, ASC, ASC,   # 40 - 47\n    ASC, ASC, ASC, ASC, ASC, ASC, ASC, ASC,   # 48 - 4F\n    ASC, ASC, ASC, ASC, ASC, ASC, ASC, ASC,   # 50 - 57\n    ASC, ASC, ASC, OTH, OTH, OTH, OTH, OTH,   # 58 - 5F\n    OTH, ASS, ASS, ASS, ASS, ASS, ASS, ASS,   # 60 - 67\n    ASS, ASS, ASS, ASS, ASS, ASS, ASS, ASS,   # 68 - 6F\n    ASS, ASS, ASS, ASS, ASS, ASS, ASS, ASS,   # 70 - 77\n    ASS, ASS, ASS, OTH, OTH, OTH, OTH, OTH,   # 78 - 7F\n    OTH, UDF, OTH, ASO, OTH, OTH, OTH, OTH,   # 80 - 87\n    OTH, OTH, ACO, OTH, ACO, UDF, ACO, UDF,   # 88 - 8F\n    UDF, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # 90 - 97\n    OTH, OTH, ASO, OTH, ASO, UDF, ASO, ACO,   # 98 - 9F\n    OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # A0 - A7\n    OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # A8 - AF\n    OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # B0 - B7\n    OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH,   # B8 - BF\n    ACV, ACV, ACV, ACV, ACV, ACV, ACO, ACO,   # C0 - C7\n    ACV, ACV, ACV, ACV, ACV, ACV, ACV, ACV,   # C8 - CF\n    ACO, ACO, ACV, ACV, ACV, ACV, ACV, OTH,   # D0 - D7\n    ACV, ACV, ACV, ACV, ACV, ACO, ACO, ACO,   # D8 - DF\n    ASV, ASV, ASV, ASV, ASV, ASV, ASO, ASO,   # E0 - E7\n    ASV, ASV, ASV, ASV, ASV, ASV, ASV, ASV,   # E8 - EF\n    ASO, ASO, ASV, ASV, ASV, ASV, ASV, OTH,   # F0 - F7\n    ASV, ASV, ASV, ASV, ASV, ASO, ASO, ASO,   # F8 - FF\n)\n\n# 0 : illegal\n# 1 : very unlikely\n# 2 : normal\n# 3 : very likely\nLatin1ClassModel = (\n# UDF OTH ASC ASS ACV ACO ASV ASO\n    0,  0,  0,  0,  0,  0,  0,  0,  # UDF\n    0,  3,  3,  3,  3,  3,  3,  3,  # OTH\n    0,  3,  3,  3,  3,  3,  3,  3,  # ASC\n    0,  3,  3,  3,  1,  1,  3,  3,  # ASS\n    0,  3,  3,  3,  1,  2,  1,  2,  # ACV\n    0,  3,  3,  3,  3,  3,  3,  3,  # ACO\n    0,  3,  1,  3,  1,  1,  1,  3,  # ASV\n    0,  3,  1,  3,  1,  1,  3,  3,  # ASO\n)\n\n\nclass Latin1Prober(CharSetProber):\n    def __init__(self):\n        super(Latin1Prober, self).__init__()\n        self._last_char_class = None\n        self._freq_counter = None\n        self.reset()\n\n    def reset(self):\n        self._last_char_class = OTH\n        self._freq_counter = [0] * FREQ_CAT_NUM\n        CharSetProber.reset(self)\n\n    @property\n    def charset_name(self):\n        return \"ISO-8859-1\"\n\n    @property\n    def language(self):\n        return \"\"\n\n    def feed(self, byte_str):\n        byte_str = self.filter_with_english_letters(byte_str)\n        for c in byte_str:\n            char_class = Latin1_CharToClass[c]\n            freq = Latin1ClassModel[(self._last_char_class * CLASS_NUM)\n                                    + char_class]\n            if freq == 0:\n                self._state = ProbingState.NOT_ME\n                break\n            self._freq_counter[freq] += 1\n            self._last_char_class = char_class\n\n        return self.state\n\n    def get_confidence(self):\n        if self.state == ProbingState.NOT_ME:\n            return 0.01\n\n        total = sum(self._freq_counter)\n        if total < 0.01:\n            confidence = 0.0\n        else:\n            confidence = ((self._freq_counter[3] - self._freq_counter[1] * 20.0)\n                          / total)\n        if confidence < 0.0:\n            confidence = 0.0\n        # lower the confidence of latin1 so that other more accurate\n        # detector can take priority.\n        confidence = confidence * 0.73\n        return confidence\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/mbcharsetprober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Universal charset detector code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 2001\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#   Shy Shalom - original C code\n#   Proofpoint, Inc.\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .charsetprober import CharSetProber\nfrom .enums import ProbingState, MachineState\n\n\nclass MultiByteCharSetProber(CharSetProber):\n    \"\"\"\n    MultiByteCharSetProber\n    \"\"\"\n\n    def __init__(self, lang_filter=None):\n        super(MultiByteCharSetProber, self).__init__(lang_filter=lang_filter)\n        self.distribution_analyzer = None\n        self.coding_sm = None\n        self._last_char = [0, 0]\n\n    def reset(self):\n        super(MultiByteCharSetProber, self).reset()\n        if self.coding_sm:\n            self.coding_sm.reset()\n        if self.distribution_analyzer:\n            self.distribution_analyzer.reset()\n        self._last_char = [0, 0]\n\n    @property\n    def charset_name(self):\n        raise NotImplementedError\n\n    @property\n    def language(self):\n        raise NotImplementedError\n\n    def feed(self, byte_str):\n        for i in range(len(byte_str)):\n            coding_state = self.coding_sm.next_state(byte_str[i])\n            if coding_state == MachineState.ERROR:\n                self.logger.debug('%s %s prober hit error at byte %s',\n                                  self.charset_name, self.language, i)\n                self._state = ProbingState.NOT_ME\n                break\n            elif coding_state == MachineState.ITS_ME:\n                self._state = ProbingState.FOUND_IT\n                break\n            elif coding_state == MachineState.START:\n                char_len = self.coding_sm.get_current_charlen()\n                if i == 0:\n                    self._last_char[1] = byte_str[0]\n                    self.distribution_analyzer.feed(self._last_char, char_len)\n                else:\n                    self.distribution_analyzer.feed(byte_str[i - 1:i + 1],\n                                                    char_len)\n\n        self._last_char[0] = byte_str[-1]\n\n        if self.state == ProbingState.DETECTING:\n            if (self.distribution_analyzer.got_enough_data() and\n                    (self.get_confidence() > self.SHORTCUT_THRESHOLD)):\n                self._state = ProbingState.FOUND_IT\n\n        return self.state\n\n    def get_confidence(self):\n        return self.distribution_analyzer.get_confidence()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/mbcsgroupprober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Universal charset detector code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 2001\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#   Shy Shalom - original C code\n#   Proofpoint, Inc.\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .charsetgroupprober import CharSetGroupProber\nfrom .utf8prober import UTF8Prober\nfrom .sjisprober import SJISProber\nfrom .eucjpprober import EUCJPProber\nfrom .gb2312prober import GB2312Prober\nfrom .euckrprober import EUCKRProber\nfrom .cp949prober import CP949Prober\nfrom .big5prober import Big5Prober\nfrom .euctwprober import EUCTWProber\n\n\nclass MBCSGroupProber(CharSetGroupProber):\n    def __init__(self, lang_filter=None):\n        super(MBCSGroupProber, self).__init__(lang_filter=lang_filter)\n        self.probers = [\n            UTF8Prober(),\n            SJISProber(),\n            EUCJPProber(),\n            GB2312Prober(),\n            EUCKRProber(),\n            CP949Prober(),\n            Big5Prober(),\n            EUCTWProber()\n        ]\n        self.reset()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/mbcssm.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is mozilla.org code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .enums import MachineState\n\n# BIG5\n\nBIG5_CLS = (\n    1,1,1,1,1,1,1,1,  # 00 - 07    #allow 0x00 as legal value\n    1,1,1,1,1,1,0,0,  # 08 - 0f\n    1,1,1,1,1,1,1,1,  # 10 - 17\n    1,1,1,0,1,1,1,1,  # 18 - 1f\n    1,1,1,1,1,1,1,1,  # 20 - 27\n    1,1,1,1,1,1,1,1,  # 28 - 2f\n    1,1,1,1,1,1,1,1,  # 30 - 37\n    1,1,1,1,1,1,1,1,  # 38 - 3f\n    2,2,2,2,2,2,2,2,  # 40 - 47\n    2,2,2,2,2,2,2,2,  # 48 - 4f\n    2,2,2,2,2,2,2,2,  # 50 - 57\n    2,2,2,2,2,2,2,2,  # 58 - 5f\n    2,2,2,2,2,2,2,2,  # 60 - 67\n    2,2,2,2,2,2,2,2,  # 68 - 6f\n    2,2,2,2,2,2,2,2,  # 70 - 77\n    2,2,2,2,2,2,2,1,  # 78 - 7f\n    4,4,4,4,4,4,4,4,  # 80 - 87\n    4,4,4,4,4,4,4,4,  # 88 - 8f\n    4,4,4,4,4,4,4,4,  # 90 - 97\n    4,4,4,4,4,4,4,4,  # 98 - 9f\n    4,3,3,3,3,3,3,3,  # a0 - a7\n    3,3,3,3,3,3,3,3,  # a8 - af\n    3,3,3,3,3,3,3,3,  # b0 - b7\n    3,3,3,3,3,3,3,3,  # b8 - bf\n    3,3,3,3,3,3,3,3,  # c0 - c7\n    3,3,3,3,3,3,3,3,  # c8 - cf\n    3,3,3,3,3,3,3,3,  # d0 - d7\n    3,3,3,3,3,3,3,3,  # d8 - df\n    3,3,3,3,3,3,3,3,  # e0 - e7\n    3,3,3,3,3,3,3,3,  # e8 - ef\n    3,3,3,3,3,3,3,3,  # f0 - f7\n    3,3,3,3,3,3,3,0  # f8 - ff\n)\n\nBIG5_ST = (\n    MachineState.ERROR,MachineState.START,MachineState.START,     3,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#00-07\n    MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,#08-0f\n    MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START#10-17\n)\n\nBIG5_CHAR_LEN_TABLE = (0, 1, 1, 2, 0)\n\nBIG5_SM_MODEL = {'class_table': BIG5_CLS,\n                 'class_factor': 5,\n                 'state_table': BIG5_ST,\n                 'char_len_table': BIG5_CHAR_LEN_TABLE,\n                 'name': 'Big5'}\n\n# CP949\n\nCP949_CLS  = (\n    1,1,1,1,1,1,1,1, 1,1,1,1,1,1,0,0,  # 00 - 0f\n    1,1,1,1,1,1,1,1, 1,1,1,0,1,1,1,1,  # 10 - 1f\n    1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,  # 20 - 2f\n    1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,  # 30 - 3f\n    1,4,4,4,4,4,4,4, 4,4,4,4,4,4,4,4,  # 40 - 4f\n    4,4,5,5,5,5,5,5, 5,5,5,1,1,1,1,1,  # 50 - 5f\n    1,5,5,5,5,5,5,5, 5,5,5,5,5,5,5,5,  # 60 - 6f\n    5,5,5,5,5,5,5,5, 5,5,5,1,1,1,1,1,  # 70 - 7f\n    0,6,6,6,6,6,6,6, 6,6,6,6,6,6,6,6,  # 80 - 8f\n    6,6,6,6,6,6,6,6, 6,6,6,6,6,6,6,6,  # 90 - 9f\n    6,7,7,7,7,7,7,7, 7,7,7,7,7,8,8,8,  # a0 - af\n    7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,  # b0 - bf\n    7,7,7,7,7,7,9,2, 2,3,2,2,2,2,2,2,  # c0 - cf\n    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,  # d0 - df\n    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,  # e0 - ef\n    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,0,  # f0 - ff\n)\n\nCP949_ST = (\n#cls=    0      1      2      3      4      5      6      7      8      9  # previous state =\n    MachineState.ERROR,MachineState.START,     3,MachineState.ERROR,MachineState.START,MachineState.START,     4,     5,MachineState.ERROR,     6, # MachineState.START\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, # MachineState.ERROR\n    MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME, # MachineState.ITS_ME\n    MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START, # 3\n    MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START, # 4\n    MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START, # 5\n    MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START, # 6\n)\n\nCP949_CHAR_LEN_TABLE = (0, 1, 2, 0, 1, 1, 2, 2, 0, 2)\n\nCP949_SM_MODEL = {'class_table': CP949_CLS,\n                  'class_factor': 10,\n                  'state_table': CP949_ST,\n                  'char_len_table': CP949_CHAR_LEN_TABLE,\n                  'name': 'CP949'}\n\n# EUC-JP\n\nEUCJP_CLS = (\n    4,4,4,4,4,4,4,4,  # 00 - 07\n    4,4,4,4,4,4,5,5,  # 08 - 0f\n    4,4,4,4,4,4,4,4,  # 10 - 17\n    4,4,4,5,4,4,4,4,  # 18 - 1f\n    4,4,4,4,4,4,4,4,  # 20 - 27\n    4,4,4,4,4,4,4,4,  # 28 - 2f\n    4,4,4,4,4,4,4,4,  # 30 - 37\n    4,4,4,4,4,4,4,4,  # 38 - 3f\n    4,4,4,4,4,4,4,4,  # 40 - 47\n    4,4,4,4,4,4,4,4,  # 48 - 4f\n    4,4,4,4,4,4,4,4,  # 50 - 57\n    4,4,4,4,4,4,4,4,  # 58 - 5f\n    4,4,4,4,4,4,4,4,  # 60 - 67\n    4,4,4,4,4,4,4,4,  # 68 - 6f\n    4,4,4,4,4,4,4,4,  # 70 - 77\n    4,4,4,4,4,4,4,4,  # 78 - 7f\n    5,5,5,5,5,5,5,5,  # 80 - 87\n    5,5,5,5,5,5,1,3,  # 88 - 8f\n    5,5,5,5,5,5,5,5,  # 90 - 97\n    5,5,5,5,5,5,5,5,  # 98 - 9f\n    5,2,2,2,2,2,2,2,  # a0 - a7\n    2,2,2,2,2,2,2,2,  # a8 - af\n    2,2,2,2,2,2,2,2,  # b0 - b7\n    2,2,2,2,2,2,2,2,  # b8 - bf\n    2,2,2,2,2,2,2,2,  # c0 - c7\n    2,2,2,2,2,2,2,2,  # c8 - cf\n    2,2,2,2,2,2,2,2,  # d0 - d7\n    2,2,2,2,2,2,2,2,  # d8 - df\n    0,0,0,0,0,0,0,0,  # e0 - e7\n    0,0,0,0,0,0,0,0,  # e8 - ef\n    0,0,0,0,0,0,0,0,  # f0 - f7\n    0,0,0,0,0,0,0,5  # f8 - ff\n)\n\nEUCJP_ST = (\n          3,     4,     3,     5,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#00-07\n     MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f\n     MachineState.ITS_ME,MachineState.ITS_ME,MachineState.START,MachineState.ERROR,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#10-17\n     MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,     3,MachineState.ERROR,#18-1f\n          3,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START#20-27\n)\n\nEUCJP_CHAR_LEN_TABLE = (2, 2, 2, 3, 1, 0)\n\nEUCJP_SM_MODEL = {'class_table': EUCJP_CLS,\n                  'class_factor': 6,\n                  'state_table': EUCJP_ST,\n                  'char_len_table': EUCJP_CHAR_LEN_TABLE,\n                  'name': 'EUC-JP'}\n\n# EUC-KR\n\nEUCKR_CLS  = (\n    1,1,1,1,1,1,1,1,  # 00 - 07\n    1,1,1,1,1,1,0,0,  # 08 - 0f\n    1,1,1,1,1,1,1,1,  # 10 - 17\n    1,1,1,0,1,1,1,1,  # 18 - 1f\n    1,1,1,1,1,1,1,1,  # 20 - 27\n    1,1,1,1,1,1,1,1,  # 28 - 2f\n    1,1,1,1,1,1,1,1,  # 30 - 37\n    1,1,1,1,1,1,1,1,  # 38 - 3f\n    1,1,1,1,1,1,1,1,  # 40 - 47\n    1,1,1,1,1,1,1,1,  # 48 - 4f\n    1,1,1,1,1,1,1,1,  # 50 - 57\n    1,1,1,1,1,1,1,1,  # 58 - 5f\n    1,1,1,1,1,1,1,1,  # 60 - 67\n    1,1,1,1,1,1,1,1,  # 68 - 6f\n    1,1,1,1,1,1,1,1,  # 70 - 77\n    1,1,1,1,1,1,1,1,  # 78 - 7f\n    0,0,0,0,0,0,0,0,  # 80 - 87\n    0,0,0,0,0,0,0,0,  # 88 - 8f\n    0,0,0,0,0,0,0,0,  # 90 - 97\n    0,0,0,0,0,0,0,0,  # 98 - 9f\n    0,2,2,2,2,2,2,2,  # a0 - a7\n    2,2,2,2,2,3,3,3,  # a8 - af\n    2,2,2,2,2,2,2,2,  # b0 - b7\n    2,2,2,2,2,2,2,2,  # b8 - bf\n    2,2,2,2,2,2,2,2,  # c0 - c7\n    2,3,2,2,2,2,2,2,  # c8 - cf\n    2,2,2,2,2,2,2,2,  # d0 - d7\n    2,2,2,2,2,2,2,2,  # d8 - df\n    2,2,2,2,2,2,2,2,  # e0 - e7\n    2,2,2,2,2,2,2,2,  # e8 - ef\n    2,2,2,2,2,2,2,2,  # f0 - f7\n    2,2,2,2,2,2,2,0   # f8 - ff\n)\n\nEUCKR_ST = (\n    MachineState.ERROR,MachineState.START,     3,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#00-07\n    MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START #08-0f\n)\n\nEUCKR_CHAR_LEN_TABLE = (0, 1, 2, 0)\n\nEUCKR_SM_MODEL = {'class_table': EUCKR_CLS,\n                'class_factor': 4,\n                'state_table': EUCKR_ST,\n                'char_len_table': EUCKR_CHAR_LEN_TABLE,\n                'name': 'EUC-KR'}\n\n# EUC-TW\n\nEUCTW_CLS = (\n    2,2,2,2,2,2,2,2,  # 00 - 07\n    2,2,2,2,2,2,0,0,  # 08 - 0f\n    2,2,2,2,2,2,2,2,  # 10 - 17\n    2,2,2,0,2,2,2,2,  # 18 - 1f\n    2,2,2,2,2,2,2,2,  # 20 - 27\n    2,2,2,2,2,2,2,2,  # 28 - 2f\n    2,2,2,2,2,2,2,2,  # 30 - 37\n    2,2,2,2,2,2,2,2,  # 38 - 3f\n    2,2,2,2,2,2,2,2,  # 40 - 47\n    2,2,2,2,2,2,2,2,  # 48 - 4f\n    2,2,2,2,2,2,2,2,  # 50 - 57\n    2,2,2,2,2,2,2,2,  # 58 - 5f\n    2,2,2,2,2,2,2,2,  # 60 - 67\n    2,2,2,2,2,2,2,2,  # 68 - 6f\n    2,2,2,2,2,2,2,2,  # 70 - 77\n    2,2,2,2,2,2,2,2,  # 78 - 7f\n    0,0,0,0,0,0,0,0,  # 80 - 87\n    0,0,0,0,0,0,6,0,  # 88 - 8f\n    0,0,0,0,0,0,0,0,  # 90 - 97\n    0,0,0,0,0,0,0,0,  # 98 - 9f\n    0,3,4,4,4,4,4,4,  # a0 - a7\n    5,5,1,1,1,1,1,1,  # a8 - af\n    1,1,1,1,1,1,1,1,  # b0 - b7\n    1,1,1,1,1,1,1,1,  # b8 - bf\n    1,1,3,1,3,3,3,3,  # c0 - c7\n    3,3,3,3,3,3,3,3,  # c8 - cf\n    3,3,3,3,3,3,3,3,  # d0 - d7\n    3,3,3,3,3,3,3,3,  # d8 - df\n    3,3,3,3,3,3,3,3,  # e0 - e7\n    3,3,3,3,3,3,3,3,  # e8 - ef\n    3,3,3,3,3,3,3,3,  # f0 - f7\n    3,3,3,3,3,3,3,0   # f8 - ff\n)\n\nEUCTW_ST = (\n    MachineState.ERROR,MachineState.ERROR,MachineState.START,     3,     3,     3,     4,MachineState.ERROR,#00-07\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f\n    MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.START,MachineState.ERROR,#10-17\n    MachineState.START,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#18-1f\n         5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.ERROR,MachineState.START,MachineState.START,#20-27\n    MachineState.START,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START #28-2f\n)\n\nEUCTW_CHAR_LEN_TABLE = (0, 0, 1, 2, 2, 2, 3)\n\nEUCTW_SM_MODEL = {'class_table': EUCTW_CLS,\n                'class_factor': 7,\n                'state_table': EUCTW_ST,\n                'char_len_table': EUCTW_CHAR_LEN_TABLE,\n                'name': 'x-euc-tw'}\n\n# GB2312\n\nGB2312_CLS = (\n    1,1,1,1,1,1,1,1,  # 00 - 07\n    1,1,1,1,1,1,0,0,  # 08 - 0f\n    1,1,1,1,1,1,1,1,  # 10 - 17\n    1,1,1,0,1,1,1,1,  # 18 - 1f\n    1,1,1,1,1,1,1,1,  # 20 - 27\n    1,1,1,1,1,1,1,1,  # 28 - 2f\n    3,3,3,3,3,3,3,3,  # 30 - 37\n    3,3,1,1,1,1,1,1,  # 38 - 3f\n    2,2,2,2,2,2,2,2,  # 40 - 47\n    2,2,2,2,2,2,2,2,  # 48 - 4f\n    2,2,2,2,2,2,2,2,  # 50 - 57\n    2,2,2,2,2,2,2,2,  # 58 - 5f\n    2,2,2,2,2,2,2,2,  # 60 - 67\n    2,2,2,2,2,2,2,2,  # 68 - 6f\n    2,2,2,2,2,2,2,2,  # 70 - 77\n    2,2,2,2,2,2,2,4,  # 78 - 7f\n    5,6,6,6,6,6,6,6,  # 80 - 87\n    6,6,6,6,6,6,6,6,  # 88 - 8f\n    6,6,6,6,6,6,6,6,  # 90 - 97\n    6,6,6,6,6,6,6,6,  # 98 - 9f\n    6,6,6,6,6,6,6,6,  # a0 - a7\n    6,6,6,6,6,6,6,6,  # a8 - af\n    6,6,6,6,6,6,6,6,  # b0 - b7\n    6,6,6,6,6,6,6,6,  # b8 - bf\n    6,6,6,6,6,6,6,6,  # c0 - c7\n    6,6,6,6,6,6,6,6,  # c8 - cf\n    6,6,6,6,6,6,6,6,  # d0 - d7\n    6,6,6,6,6,6,6,6,  # d8 - df\n    6,6,6,6,6,6,6,6,  # e0 - e7\n    6,6,6,6,6,6,6,6,  # e8 - ef\n    6,6,6,6,6,6,6,6,  # f0 - f7\n    6,6,6,6,6,6,6,0   # f8 - ff\n)\n\nGB2312_ST = (\n    MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,     3,MachineState.ERROR,#00-07\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f\n    MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.START,#10-17\n         4,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#18-1f\n    MachineState.ERROR,MachineState.ERROR,     5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,#20-27\n    MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START #28-2f\n)\n\n# To be accurate, the length of class 6 can be either 2 or 4.\n# But it is not necessary to discriminate between the two since\n# it is used for frequency analysis only, and we are validating\n# each code range there as well. So it is safe to set it to be\n# 2 here.\nGB2312_CHAR_LEN_TABLE = (0, 1, 1, 1, 1, 1, 2)\n\nGB2312_SM_MODEL = {'class_table': GB2312_CLS,\n                   'class_factor': 7,\n                   'state_table': GB2312_ST,\n                   'char_len_table': GB2312_CHAR_LEN_TABLE,\n                   'name': 'GB2312'}\n\n# Shift_JIS\n\nSJIS_CLS = (\n    1,1,1,1,1,1,1,1,  # 00 - 07\n    1,1,1,1,1,1,0,0,  # 08 - 0f\n    1,1,1,1,1,1,1,1,  # 10 - 17\n    1,1,1,0,1,1,1,1,  # 18 - 1f\n    1,1,1,1,1,1,1,1,  # 20 - 27\n    1,1,1,1,1,1,1,1,  # 28 - 2f\n    1,1,1,1,1,1,1,1,  # 30 - 37\n    1,1,1,1,1,1,1,1,  # 38 - 3f\n    2,2,2,2,2,2,2,2,  # 40 - 47\n    2,2,2,2,2,2,2,2,  # 48 - 4f\n    2,2,2,2,2,2,2,2,  # 50 - 57\n    2,2,2,2,2,2,2,2,  # 58 - 5f\n    2,2,2,2,2,2,2,2,  # 60 - 67\n    2,2,2,2,2,2,2,2,  # 68 - 6f\n    2,2,2,2,2,2,2,2,  # 70 - 77\n    2,2,2,2,2,2,2,1,  # 78 - 7f\n    3,3,3,3,3,2,2,3,  # 80 - 87\n    3,3,3,3,3,3,3,3,  # 88 - 8f\n    3,3,3,3,3,3,3,3,  # 90 - 97\n    3,3,3,3,3,3,3,3,  # 98 - 9f\n    #0xa0 is illegal in sjis encoding, but some pages does\n    #contain such byte. We need to be more error forgiven.\n    2,2,2,2,2,2,2,2,  # a0 - a7\n    2,2,2,2,2,2,2,2,  # a8 - af\n    2,2,2,2,2,2,2,2,  # b0 - b7\n    2,2,2,2,2,2,2,2,  # b8 - bf\n    2,2,2,2,2,2,2,2,  # c0 - c7\n    2,2,2,2,2,2,2,2,  # c8 - cf\n    2,2,2,2,2,2,2,2,  # d0 - d7\n    2,2,2,2,2,2,2,2,  # d8 - df\n    3,3,3,3,3,3,3,3,  # e0 - e7\n    3,3,3,3,3,4,4,4,  # e8 - ef\n    3,3,3,3,3,3,3,3,  # f0 - f7\n    3,3,3,3,3,0,0,0)  # f8 - ff\n\n\nSJIS_ST = (\n    MachineState.ERROR,MachineState.START,MachineState.START,     3,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#00-07\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f\n    MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START #10-17\n)\n\nSJIS_CHAR_LEN_TABLE = (0, 1, 1, 2, 0, 0)\n\nSJIS_SM_MODEL = {'class_table': SJIS_CLS,\n               'class_factor': 6,\n               'state_table': SJIS_ST,\n               'char_len_table': SJIS_CHAR_LEN_TABLE,\n               'name': 'Shift_JIS'}\n\n# UCS2-BE\n\nUCS2BE_CLS = (\n    0,0,0,0,0,0,0,0,  # 00 - 07\n    0,0,1,0,0,2,0,0,  # 08 - 0f\n    0,0,0,0,0,0,0,0,  # 10 - 17\n    0,0,0,3,0,0,0,0,  # 18 - 1f\n    0,0,0,0,0,0,0,0,  # 20 - 27\n    0,3,3,3,3,3,0,0,  # 28 - 2f\n    0,0,0,0,0,0,0,0,  # 30 - 37\n    0,0,0,0,0,0,0,0,  # 38 - 3f\n    0,0,0,0,0,0,0,0,  # 40 - 47\n    0,0,0,0,0,0,0,0,  # 48 - 4f\n    0,0,0,0,0,0,0,0,  # 50 - 57\n    0,0,0,0,0,0,0,0,  # 58 - 5f\n    0,0,0,0,0,0,0,0,  # 60 - 67\n    0,0,0,0,0,0,0,0,  # 68 - 6f\n    0,0,0,0,0,0,0,0,  # 70 - 77\n    0,0,0,0,0,0,0,0,  # 78 - 7f\n    0,0,0,0,0,0,0,0,  # 80 - 87\n    0,0,0,0,0,0,0,0,  # 88 - 8f\n    0,0,0,0,0,0,0,0,  # 90 - 97\n    0,0,0,0,0,0,0,0,  # 98 - 9f\n    0,0,0,0,0,0,0,0,  # a0 - a7\n    0,0,0,0,0,0,0,0,  # a8 - af\n    0,0,0,0,0,0,0,0,  # b0 - b7\n    0,0,0,0,0,0,0,0,  # b8 - bf\n    0,0,0,0,0,0,0,0,  # c0 - c7\n    0,0,0,0,0,0,0,0,  # c8 - cf\n    0,0,0,0,0,0,0,0,  # d0 - d7\n    0,0,0,0,0,0,0,0,  # d8 - df\n    0,0,0,0,0,0,0,0,  # e0 - e7\n    0,0,0,0,0,0,0,0,  # e8 - ef\n    0,0,0,0,0,0,0,0,  # f0 - f7\n    0,0,0,0,0,0,4,5   # f8 - ff\n)\n\nUCS2BE_ST  = (\n          5,     7,     7,MachineState.ERROR,     4,     3,MachineState.ERROR,MachineState.ERROR,#00-07\n     MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f\n     MachineState.ITS_ME,MachineState.ITS_ME,     6,     6,     6,     6,MachineState.ERROR,MachineState.ERROR,#10-17\n          6,     6,     6,     6,     6,MachineState.ITS_ME,     6,     6,#18-1f\n          6,     6,     6,     6,     5,     7,     7,MachineState.ERROR,#20-27\n          5,     8,     6,     6,MachineState.ERROR,     6,     6,     6,#28-2f\n          6,     6,     6,     6,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START #30-37\n)\n\nUCS2BE_CHAR_LEN_TABLE = (2, 2, 2, 0, 2, 2)\n\nUCS2BE_SM_MODEL = {'class_table': UCS2BE_CLS,\n                   'class_factor': 6,\n                   'state_table': UCS2BE_ST,\n                   'char_len_table': UCS2BE_CHAR_LEN_TABLE,\n                   'name': 'UTF-16BE'}\n\n# UCS2-LE\n\nUCS2LE_CLS = (\n    0,0,0,0,0,0,0,0,  # 00 - 07\n    0,0,1,0,0,2,0,0,  # 08 - 0f\n    0,0,0,0,0,0,0,0,  # 10 - 17\n    0,0,0,3,0,0,0,0,  # 18 - 1f\n    0,0,0,0,0,0,0,0,  # 20 - 27\n    0,3,3,3,3,3,0,0,  # 28 - 2f\n    0,0,0,0,0,0,0,0,  # 30 - 37\n    0,0,0,0,0,0,0,0,  # 38 - 3f\n    0,0,0,0,0,0,0,0,  # 40 - 47\n    0,0,0,0,0,0,0,0,  # 48 - 4f\n    0,0,0,0,0,0,0,0,  # 50 - 57\n    0,0,0,0,0,0,0,0,  # 58 - 5f\n    0,0,0,0,0,0,0,0,  # 60 - 67\n    0,0,0,0,0,0,0,0,  # 68 - 6f\n    0,0,0,0,0,0,0,0,  # 70 - 77\n    0,0,0,0,0,0,0,0,  # 78 - 7f\n    0,0,0,0,0,0,0,0,  # 80 - 87\n    0,0,0,0,0,0,0,0,  # 88 - 8f\n    0,0,0,0,0,0,0,0,  # 90 - 97\n    0,0,0,0,0,0,0,0,  # 98 - 9f\n    0,0,0,0,0,0,0,0,  # a0 - a7\n    0,0,0,0,0,0,0,0,  # a8 - af\n    0,0,0,0,0,0,0,0,  # b0 - b7\n    0,0,0,0,0,0,0,0,  # b8 - bf\n    0,0,0,0,0,0,0,0,  # c0 - c7\n    0,0,0,0,0,0,0,0,  # c8 - cf\n    0,0,0,0,0,0,0,0,  # d0 - d7\n    0,0,0,0,0,0,0,0,  # d8 - df\n    0,0,0,0,0,0,0,0,  # e0 - e7\n    0,0,0,0,0,0,0,0,  # e8 - ef\n    0,0,0,0,0,0,0,0,  # f0 - f7\n    0,0,0,0,0,0,4,5   # f8 - ff\n)\n\nUCS2LE_ST = (\n          6,     6,     7,     6,     4,     3,MachineState.ERROR,MachineState.ERROR,#00-07\n     MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f\n     MachineState.ITS_ME,MachineState.ITS_ME,     5,     5,     5,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,#10-17\n          5,     5,     5,MachineState.ERROR,     5,MachineState.ERROR,     6,     6,#18-1f\n          7,     6,     8,     8,     5,     5,     5,MachineState.ERROR,#20-27\n          5,     5,     5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,     5,     5,#28-2f\n          5,     5,     5,MachineState.ERROR,     5,MachineState.ERROR,MachineState.START,MachineState.START #30-37\n)\n\nUCS2LE_CHAR_LEN_TABLE = (2, 2, 2, 2, 2, 2)\n\nUCS2LE_SM_MODEL = {'class_table': UCS2LE_CLS,\n                 'class_factor': 6,\n                 'state_table': UCS2LE_ST,\n                 'char_len_table': UCS2LE_CHAR_LEN_TABLE,\n                 'name': 'UTF-16LE'}\n\n# UTF-8\n\nUTF8_CLS = (\n    1,1,1,1,1,1,1,1,  # 00 - 07  #allow 0x00 as a legal value\n    1,1,1,1,1,1,0,0,  # 08 - 0f\n    1,1,1,1,1,1,1,1,  # 10 - 17\n    1,1,1,0,1,1,1,1,  # 18 - 1f\n    1,1,1,1,1,1,1,1,  # 20 - 27\n    1,1,1,1,1,1,1,1,  # 28 - 2f\n    1,1,1,1,1,1,1,1,  # 30 - 37\n    1,1,1,1,1,1,1,1,  # 38 - 3f\n    1,1,1,1,1,1,1,1,  # 40 - 47\n    1,1,1,1,1,1,1,1,  # 48 - 4f\n    1,1,1,1,1,1,1,1,  # 50 - 57\n    1,1,1,1,1,1,1,1,  # 58 - 5f\n    1,1,1,1,1,1,1,1,  # 60 - 67\n    1,1,1,1,1,1,1,1,  # 68 - 6f\n    1,1,1,1,1,1,1,1,  # 70 - 77\n    1,1,1,1,1,1,1,1,  # 78 - 7f\n    2,2,2,2,3,3,3,3,  # 80 - 87\n    4,4,4,4,4,4,4,4,  # 88 - 8f\n    4,4,4,4,4,4,4,4,  # 90 - 97\n    4,4,4,4,4,4,4,4,  # 98 - 9f\n    5,5,5,5,5,5,5,5,  # a0 - a7\n    5,5,5,5,5,5,5,5,  # a8 - af\n    5,5,5,5,5,5,5,5,  # b0 - b7\n    5,5,5,5,5,5,5,5,  # b8 - bf\n    0,0,6,6,6,6,6,6,  # c0 - c7\n    6,6,6,6,6,6,6,6,  # c8 - cf\n    6,6,6,6,6,6,6,6,  # d0 - d7\n    6,6,6,6,6,6,6,6,  # d8 - df\n    7,8,8,8,8,8,8,8,  # e0 - e7\n    8,8,8,8,8,9,8,8,  # e8 - ef\n    10,11,11,11,11,11,11,11,  # f0 - f7\n    12,13,13,13,14,15,0,0    # f8 - ff\n)\n\nUTF8_ST = (\n    MachineState.ERROR,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,     12,   10,#00-07\n         9,     11,     8,     7,     6,     5,     4,    3,#08-0f\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#10-17\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#18-1f\n    MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#20-27\n    MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#28-2f\n    MachineState.ERROR,MachineState.ERROR,     5,     5,     5,     5,MachineState.ERROR,MachineState.ERROR,#30-37\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#38-3f\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,     5,     5,     5,MachineState.ERROR,MachineState.ERROR,#40-47\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#48-4f\n    MachineState.ERROR,MachineState.ERROR,     7,     7,     7,     7,MachineState.ERROR,MachineState.ERROR,#50-57\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#58-5f\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,     7,     7,MachineState.ERROR,MachineState.ERROR,#60-67\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#68-6f\n    MachineState.ERROR,MachineState.ERROR,     9,     9,     9,     9,MachineState.ERROR,MachineState.ERROR,#70-77\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#78-7f\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,     9,MachineState.ERROR,MachineState.ERROR,#80-87\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#88-8f\n    MachineState.ERROR,MachineState.ERROR,    12,    12,    12,    12,MachineState.ERROR,MachineState.ERROR,#90-97\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#98-9f\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,    12,MachineState.ERROR,MachineState.ERROR,#a0-a7\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#a8-af\n    MachineState.ERROR,MachineState.ERROR,    12,    12,    12,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#b0-b7\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#b8-bf\n    MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,#c0-c7\n    MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR #c8-cf\n)\n\nUTF8_CHAR_LEN_TABLE = (0, 1, 0, 0, 0, 0, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6)\n\nUTF8_SM_MODEL = {'class_table': UTF8_CLS,\n                 'class_factor': 16,\n                 'state_table': UTF8_ST,\n                 'char_len_table': UTF8_CHAR_LEN_TABLE,\n                 'name': 'UTF-8'}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/metadata/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/metadata/languages.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\"\"\"\nMetadata about languages used by our model training code for our\nSingleByteCharSetProbers.  Could be used for other things in the future.\n\nThis code is based on the language metadata from the uchardet project.\n\"\"\"\nfrom __future__ import absolute_import, print_function\n\nfrom string import ascii_letters\n\n\n# TODO: Add Ukranian (KOI8-U)\n\nclass Language(object):\n    \"\"\"Metadata about a language useful for training models\n\n    :ivar name: The human name for the language, in English.\n    :type name: str\n    :ivar iso_code: 2-letter ISO 639-1 if possible, 3-letter ISO code otherwise,\n                    or use another catalog as a last resort.\n    :type iso_code: str\n    :ivar use_ascii: Whether or not ASCII letters should be included in trained\n                     models.\n    :type use_ascii: bool\n    :ivar charsets: The charsets we want to support and create data for.\n    :type charsets: list of str\n    :ivar alphabet: The characters in the language's alphabet. If `use_ascii` is\n                    `True`, you only need to add those not in the ASCII set.\n    :type alphabet: str\n    :ivar wiki_start_pages: The Wikipedia pages to start from if we're crawling\n                            Wikipedia for training data.\n    :type wiki_start_pages: list of str\n    \"\"\"\n    def __init__(self, name=None, iso_code=None, use_ascii=True, charsets=None,\n                 alphabet=None, wiki_start_pages=None):\n        super(Language, self).__init__()\n        self.name = name\n        self.iso_code = iso_code\n        self.use_ascii = use_ascii\n        self.charsets = charsets\n        if self.use_ascii:\n            if alphabet:\n                alphabet += ascii_letters\n            else:\n                alphabet = ascii_letters\n        elif not alphabet:\n            raise ValueError('Must supply alphabet if use_ascii is False')\n        self.alphabet = ''.join(sorted(set(alphabet))) if alphabet else None\n        self.wiki_start_pages = wiki_start_pages\n\n    def __repr__(self):\n        return '{}({})'.format(self.__class__.__name__,\n                               ', '.join('{}={!r}'.format(k, v)\n                                         for k, v in self.__dict__.items()\n                                         if not k.startswith('_')))\n\n\nLANGUAGES = {'Arabic': Language(name='Arabic',\n                                iso_code='ar',\n                                use_ascii=False,\n                                # We only support encodings that use isolated\n                                # forms, because the current recommendation is\n                                # that the rendering system handles presentation\n                                # forms. This means we purposefully skip IBM864.\n                                charsets=['ISO-8859-6', 'WINDOWS-1256',\n                                          'CP720', 'CP864'],\n                                alphabet=u'ءآأؤإئابةتثجحخدذرزسشصضطظعغػؼؽؾؿـفقكلمنهوىيًٌٍَُِّ',\n                                wiki_start_pages=[u'الصفحة_الرئيسية']),\n             'Belarusian': Language(name='Belarusian',\n                                    iso_code='be',\n                                    use_ascii=False,\n                                    charsets=['ISO-8859-5', 'WINDOWS-1251',\n                                              'IBM866', 'MacCyrillic'],\n                                    alphabet=(u'АБВГДЕЁЖЗІЙКЛМНОПРСТУЎФХЦЧШЫЬЭЮЯ'\n                                              u'абвгдеёжзійклмнопрстуўфхцчшыьэюяʼ'),\n                                    wiki_start_pages=[u'Галоўная_старонка']),\n             'Bulgarian': Language(name='Bulgarian',\n                                   iso_code='bg',\n                                   use_ascii=False,\n                                   charsets=['ISO-8859-5', 'WINDOWS-1251',\n                                             'IBM855'],\n                                   alphabet=(u'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯ'\n                                             u'абвгдежзийклмнопрстуфхцчшщъьюя'),\n                                   wiki_start_pages=[u'Начална_страница']),\n             'Czech': Language(name='Czech',\n                               iso_code='cz',\n                               use_ascii=True,\n                               charsets=['ISO-8859-2', 'WINDOWS-1250'],\n                               alphabet=u'áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ',\n                               wiki_start_pages=[u'Hlavní_strana']),\n             'Danish': Language(name='Danish',\n                                iso_code='da',\n                                use_ascii=True,\n                                charsets=['ISO-8859-1', 'ISO-8859-15',\n                                          'WINDOWS-1252'],\n                                alphabet=u'æøåÆØÅ',\n                                wiki_start_pages=[u'Forside']),\n             'German': Language(name='German',\n                                iso_code='de',\n                                use_ascii=True,\n                                charsets=['ISO-8859-1', 'WINDOWS-1252'],\n                                alphabet=u'äöüßÄÖÜ',\n                                wiki_start_pages=[u'Wikipedia:Hauptseite']),\n             'Greek': Language(name='Greek',\n                               iso_code='el',\n                               use_ascii=False,\n                               charsets=['ISO-8859-7', 'WINDOWS-1253'],\n                               alphabet=(u'αβγδεζηθικλμνξοπρσςτυφχψωάέήίόύώ'\n                                         u'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΣΤΥΦΧΨΩΆΈΉΊΌΎΏ'),\n                               wiki_start_pages=[u'Πύλη:Κύρια']),\n             'English': Language(name='English',\n                                 iso_code='en',\n                                 use_ascii=True,\n                                 charsets=['ISO-8859-1', 'WINDOWS-1252'],\n                                 wiki_start_pages=[u'Main_Page']),\n             'Esperanto': Language(name='Esperanto',\n                                   iso_code='eo',\n                                   # Q, W, X, and Y not used at all\n                                   use_ascii=False,\n                                   charsets=['ISO-8859-3'],\n                                   alphabet=(u'abcĉdefgĝhĥijĵklmnoprsŝtuŭvz'\n                                             u'ABCĈDEFGĜHĤIJĴKLMNOPRSŜTUŬVZ'),\n                                   wiki_start_pages=[u'Vikipedio:Ĉefpaĝo']),\n             'Spanish': Language(name='Spanish',\n                                 iso_code='es',\n                                 use_ascii=True,\n                                 charsets=['ISO-8859-1', 'ISO-8859-15',\n                                           'WINDOWS-1252'],\n                                 alphabet=u'ñáéíóúüÑÁÉÍÓÚÜ',\n                                 wiki_start_pages=[u'Wikipedia:Portada']),\n             'Estonian': Language(name='Estonian',\n                                  iso_code='et',\n                                  use_ascii=False,\n                                  charsets=['ISO-8859-4', 'ISO-8859-13',\n                                            'WINDOWS-1257'],\n                                  # C, F, Š, Q, W, X, Y, Z, Ž are only for\n                                  # loanwords\n                                  alphabet=(u'ABDEGHIJKLMNOPRSTUVÕÄÖÜ'\n                                            u'abdeghijklmnoprstuvõäöü'),\n                                  wiki_start_pages=[u'Esileht']),\n             'Finnish': Language(name='Finnish',\n                                 iso_code='fi',\n                                 use_ascii=True,\n                                 charsets=['ISO-8859-1', 'ISO-8859-15',\n                                           'WINDOWS-1252'],\n                                 alphabet=u'ÅÄÖŠŽåäöšž',\n                                 wiki_start_pages=[u'Wikipedia:Etusivu']),\n             'French': Language(name='French',\n                                iso_code='fr',\n                                use_ascii=True,\n                                charsets=['ISO-8859-1', 'ISO-8859-15',\n                                          'WINDOWS-1252'],\n                                alphabet=u'œàâçèéîïùûêŒÀÂÇÈÉÎÏÙÛÊ',\n                                wiki_start_pages=[u'Wikipédia:Accueil_principal',\n                                                  u'Bœuf (animal)']),\n             'Hebrew': Language(name='Hebrew',\n                                iso_code='he',\n                                use_ascii=False,\n                                charsets=['ISO-8859-8', 'WINDOWS-1255'],\n                                alphabet=u'אבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ',\n                                wiki_start_pages=[u'עמוד_ראשי']),\n             'Croatian': Language(name='Croatian',\n                                  iso_code='hr',\n                                  # Q, W, X, Y are only used for foreign words.\n                                  use_ascii=False,\n                                  charsets=['ISO-8859-2', 'WINDOWS-1250'],\n                                  alphabet=(u'abcčćdđefghijklmnoprsštuvzž'\n                                            u'ABCČĆDĐEFGHIJKLMNOPRSŠTUVZŽ'),\n                                  wiki_start_pages=[u'Glavna_stranica']),\n             'Hungarian': Language(name='Hungarian',\n                                   iso_code='hu',\n                                   # Q, W, X, Y are only used for foreign words.\n                                   use_ascii=False,\n                                   charsets=['ISO-8859-2', 'WINDOWS-1250'],\n                                   alphabet=(u'abcdefghijklmnoprstuvzáéíóöőúüű'\n                                             u'ABCDEFGHIJKLMNOPRSTUVZÁÉÍÓÖŐÚÜŰ'),\n                                   wiki_start_pages=[u'Kezdőlap']),\n             'Italian': Language(name='Italian',\n                                 iso_code='it',\n                                 use_ascii=True,\n                                 charsets=['ISO-8859-1', 'ISO-8859-15',\n                                           'WINDOWS-1252'],\n                                 alphabet=u'ÀÈÉÌÒÓÙàèéìòóù',\n                                 wiki_start_pages=[u'Pagina_principale']),\n             'Lithuanian': Language(name='Lithuanian',\n                                    iso_code='lt',\n                                    use_ascii=False,\n                                    charsets=['ISO-8859-13', 'WINDOWS-1257',\n                                              'ISO-8859-4'],\n                                    # Q, W, and X not used at all\n                                    alphabet=(u'AĄBCČDEĘĖFGHIĮYJKLMNOPRSŠTUŲŪVZŽ'\n                                              u'aąbcčdeęėfghiįyjklmnoprsštuųūvzž'),\n                                    wiki_start_pages=[u'Pagrindinis_puslapis']),\n             'Latvian': Language(name='Latvian',\n                                 iso_code='lv',\n                                 use_ascii=False,\n                                 charsets=['ISO-8859-13', 'WINDOWS-1257',\n                                           'ISO-8859-4'],\n                                 # Q, W, X, Y are only for loanwords\n                                 alphabet=(u'AĀBCČDEĒFGĢHIĪJKĶLĻMNŅOPRSŠTUŪVZŽ'\n                                           u'aābcčdeēfgģhiījkķlļmnņoprsštuūvzž'),\n                                 wiki_start_pages=[u'Sākumlapa']),\n             'Macedonian': Language(name='Macedonian',\n                                    iso_code='mk',\n                                    use_ascii=False,\n                                    charsets=['ISO-8859-5', 'WINDOWS-1251',\n                                              'MacCyrillic', 'IBM855'],\n                                    alphabet=(u'АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЦЧЏШ'\n                                              u'абвгдѓежзѕијклљмнњопрстќуфхцчџш'),\n                                    wiki_start_pages=[u'Главна_страница']),\n             'Dutch': Language(name='Dutch',\n                               iso_code='nl',\n                               use_ascii=True,\n                               charsets=['ISO-8859-1', 'WINDOWS-1252'],\n                               wiki_start_pages=[u'Hoofdpagina']),\n             'Polish': Language(name='Polish',\n                                iso_code='pl',\n                                # Q and X are only used for foreign words.\n                                use_ascii=False,\n                                charsets=['ISO-8859-2', 'WINDOWS-1250'],\n                                alphabet=(u'AĄBCĆDEĘFGHIJKLŁMNŃOÓPRSŚTUWYZŹŻ'\n                                          u'aąbcćdeęfghijklłmnńoóprsśtuwyzźż'),\n                                wiki_start_pages=[u'Wikipedia:Strona_główna']),\n             'Portuguese': Language(name='Portuguese',\n                                 iso_code='pt',\n                                 use_ascii=True,\n                                 charsets=['ISO-8859-1', 'ISO-8859-15',\n                                           'WINDOWS-1252'],\n                                 alphabet=u'ÁÂÃÀÇÉÊÍÓÔÕÚáâãàçéêíóôõú',\n                                 wiki_start_pages=[u'Wikipédia:Página_principal']),\n             'Romanian': Language(name='Romanian',\n                                  iso_code='ro',\n                                  use_ascii=True,\n                                  charsets=['ISO-8859-2', 'WINDOWS-1250'],\n                                  alphabet=u'ăâîșțĂÂÎȘȚ',\n                                  wiki_start_pages=[u'Pagina_principală']),\n             'Russian': Language(name='Russian',\n                                 iso_code='ru',\n                                 use_ascii=False,\n                                 charsets=['ISO-8859-5', 'WINDOWS-1251',\n                                           'KOI8-R', 'MacCyrillic', 'IBM866',\n                                           'IBM855'],\n                                 alphabet=(u'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'\n                                           u'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'),\n                                 wiki_start_pages=[u'Заглавная_страница']),\n             'Slovak': Language(name='Slovak',\n                                iso_code='sk',\n                                use_ascii=True,\n                                charsets=['ISO-8859-2', 'WINDOWS-1250'],\n                                alphabet=u'áäčďéíĺľňóôŕšťúýžÁÄČĎÉÍĹĽŇÓÔŔŠŤÚÝŽ',\n                                wiki_start_pages=[u'Hlavná_stránka']),\n             'Slovene': Language(name='Slovene',\n                                 iso_code='sl',\n                                 # Q, W, X, Y are only used for foreign words.\n                                 use_ascii=False,\n                                 charsets=['ISO-8859-2', 'WINDOWS-1250'],\n                                 alphabet=(u'abcčdefghijklmnoprsštuvzž'\n                                           u'ABCČDEFGHIJKLMNOPRSŠTUVZŽ'),\n                                 wiki_start_pages=[u'Glavna_stran']),\n             # Serbian can be written in both Latin and Cyrillic, but there's no\n             # simple way to get the Latin alphabet pages from Wikipedia through\n             # the API, so for now we just support Cyrillic.\n             'Serbian': Language(name='Serbian',\n                                 iso_code='sr',\n                                 alphabet=(u'АБВГДЂЕЖЗИЈКЛЉМНЊОПРСТЋУФХЦЧЏШ'\n                                           u'абвгдђежзијклљмнњопрстћуфхцчџш'),\n                                 charsets=['ISO-8859-5', 'WINDOWS-1251',\n                                           'MacCyrillic', 'IBM855'],\n                                 wiki_start_pages=[u'Главна_страна']),\n             'Thai': Language(name='Thai',\n                              iso_code='th',\n                              use_ascii=False,\n                              charsets=['ISO-8859-11', 'TIS-620', 'CP874'],\n                              alphabet=u'กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛',\n                              wiki_start_pages=[u'หน้าหลัก']),\n             'Turkish': Language(name='Turkish',\n                                 iso_code='tr',\n                                 # Q, W, and X are not used by Turkish\n                                 use_ascii=False,\n                                 charsets=['ISO-8859-3', 'ISO-8859-9',\n                                           'WINDOWS-1254'],\n                                 alphabet=(u'abcçdefgğhıijklmnoöprsştuüvyzâîû'\n                                           u'ABCÇDEFGĞHIİJKLMNOÖPRSŞTUÜVYZÂÎÛ'),\n                                 wiki_start_pages=[u'Ana_Sayfa']),\n             'Vietnamese': Language(name='Vietnamese',\n                                    iso_code='vi',\n                                    use_ascii=False,\n                                    # Windows-1258 is the only common 8-bit\n                                    # Vietnamese encoding supported by Python.\n                                    # From Wikipedia:\n                                    # For systems that lack support for Unicode,\n                                    # dozens of 8-bit Vietnamese code pages are\n                                    # available.[1] The most common are VISCII\n                                    # (TCVN 5712:1993), VPS, and Windows-1258.[3]\n                                    # Where ASCII is required, such as when\n                                    # ensuring readability in plain text e-mail,\n                                    # Vietnamese letters are often encoded\n                                    # according to Vietnamese Quoted-Readable\n                                    # (VIQR) or VSCII Mnemonic (VSCII-MNEM),[4]\n                                    # though usage of either variable-width\n                                    # scheme has declined dramatically following\n                                    # the adoption of Unicode on the World Wide\n                                    # Web.\n                                    charsets=['WINDOWS-1258'],\n                                    alphabet=(u'aăâbcdđeêghiklmnoôơpqrstuưvxy'\n                                              u'AĂÂBCDĐEÊGHIKLMNOÔƠPQRSTUƯVXY'),\n                                    wiki_start_pages=[u'Chữ_Quốc_ngữ']),\n            }\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/sbcharsetprober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Universal charset detector code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 2001\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#   Shy Shalom - original C code\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom collections import namedtuple\n\nfrom .charsetprober import CharSetProber\nfrom .enums import CharacterCategory, ProbingState, SequenceLikelihood\n\n\nSingleByteCharSetModel = namedtuple('SingleByteCharSetModel',\n                                    ['charset_name',\n                                     'language',\n                                     'char_to_order_map',\n                                     'language_model',\n                                     'typical_positive_ratio',\n                                     'keep_ascii_letters',\n                                     'alphabet'])\n\n\nclass SingleByteCharSetProber(CharSetProber):\n    SAMPLE_SIZE = 64\n    SB_ENOUGH_REL_THRESHOLD = 1024  #  0.25 * SAMPLE_SIZE^2\n    POSITIVE_SHORTCUT_THRESHOLD = 0.95\n    NEGATIVE_SHORTCUT_THRESHOLD = 0.05\n\n    def __init__(self, model, reversed=False, name_prober=None):\n        super(SingleByteCharSetProber, self).__init__()\n        self._model = model\n        # TRUE if we need to reverse every pair in the model lookup\n        self._reversed = reversed\n        # Optional auxiliary prober for name decision\n        self._name_prober = name_prober\n        self._last_order = None\n        self._seq_counters = None\n        self._total_seqs = None\n        self._total_char = None\n        self._freq_char = None\n        self.reset()\n\n    def reset(self):\n        super(SingleByteCharSetProber, self).reset()\n        # char order of last character\n        self._last_order = 255\n        self._seq_counters = [0] * SequenceLikelihood.get_num_categories()\n        self._total_seqs = 0\n        self._total_char = 0\n        # characters that fall in our sampling range\n        self._freq_char = 0\n\n    @property\n    def charset_name(self):\n        if self._name_prober:\n            return self._name_prober.charset_name\n        else:\n            return self._model.charset_name\n\n    @property\n    def language(self):\n        if self._name_prober:\n            return self._name_prober.language\n        else:\n            return self._model.language\n\n    def feed(self, byte_str):\n        # TODO: Make filter_international_words keep things in self.alphabet\n        if not self._model.keep_ascii_letters:\n            byte_str = self.filter_international_words(byte_str)\n        if not byte_str:\n            return self.state\n        char_to_order_map = self._model.char_to_order_map\n        language_model = self._model.language_model\n        for char in byte_str:\n            order = char_to_order_map.get(char, CharacterCategory.UNDEFINED)\n            # XXX: This was SYMBOL_CAT_ORDER before, with a value of 250, but\n            #      CharacterCategory.SYMBOL is actually 253, so we use CONTROL\n            #      to make it closer to the original intent. The only difference\n            #      is whether or not we count digits and control characters for\n            #      _total_char purposes.\n            if order < CharacterCategory.CONTROL:\n                self._total_char += 1\n            # TODO: Follow uchardet's lead and discount confidence for frequent\n            #       control characters.\n            #       See https://github.com/BYVoid/uchardet/commit/55b4f23971db61\n            if order < self.SAMPLE_SIZE:\n                self._freq_char += 1\n                if self._last_order < self.SAMPLE_SIZE:\n                    self._total_seqs += 1\n                    if not self._reversed:\n                        lm_cat = language_model[self._last_order][order]\n                    else:\n                        lm_cat = language_model[order][self._last_order]\n                    self._seq_counters[lm_cat] += 1\n            self._last_order = order\n\n        charset_name = self._model.charset_name\n        if self.state == ProbingState.DETECTING:\n            if self._total_seqs > self.SB_ENOUGH_REL_THRESHOLD:\n                confidence = self.get_confidence()\n                if confidence > self.POSITIVE_SHORTCUT_THRESHOLD:\n                    self.logger.debug('%s confidence = %s, we have a winner',\n                                      charset_name, confidence)\n                    self._state = ProbingState.FOUND_IT\n                elif confidence < self.NEGATIVE_SHORTCUT_THRESHOLD:\n                    self.logger.debug('%s confidence = %s, below negative '\n                                      'shortcut threshhold %s', charset_name,\n                                      confidence,\n                                      self.NEGATIVE_SHORTCUT_THRESHOLD)\n                    self._state = ProbingState.NOT_ME\n\n        return self.state\n\n    def get_confidence(self):\n        r = 0.01\n        if self._total_seqs > 0:\n            r = ((1.0 * self._seq_counters[SequenceLikelihood.POSITIVE]) /\n                 self._total_seqs / self._model.typical_positive_ratio)\n            r = r * self._freq_char / self._total_char\n            if r >= 1.0:\n                r = 0.99\n        return r\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/sbcsgroupprober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Universal charset detector code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 2001\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#   Shy Shalom - original C code\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .charsetgroupprober import CharSetGroupProber\nfrom .hebrewprober import HebrewProber\nfrom .langbulgarianmodel import (ISO_8859_5_BULGARIAN_MODEL,\n                                 WINDOWS_1251_BULGARIAN_MODEL)\nfrom .langgreekmodel import ISO_8859_7_GREEK_MODEL, WINDOWS_1253_GREEK_MODEL\nfrom .langhebrewmodel import WINDOWS_1255_HEBREW_MODEL\n# from .langhungarianmodel import (ISO_8859_2_HUNGARIAN_MODEL,\n#                                  WINDOWS_1250_HUNGARIAN_MODEL)\nfrom .langrussianmodel import (IBM855_RUSSIAN_MODEL, IBM866_RUSSIAN_MODEL,\n                               ISO_8859_5_RUSSIAN_MODEL, KOI8_R_RUSSIAN_MODEL,\n                               MACCYRILLIC_RUSSIAN_MODEL,\n                               WINDOWS_1251_RUSSIAN_MODEL)\nfrom .langthaimodel import TIS_620_THAI_MODEL\nfrom .langturkishmodel import ISO_8859_9_TURKISH_MODEL\nfrom .sbcharsetprober import SingleByteCharSetProber\n\n\nclass SBCSGroupProber(CharSetGroupProber):\n    def __init__(self):\n        super(SBCSGroupProber, self).__init__()\n        hebrew_prober = HebrewProber()\n        logical_hebrew_prober = SingleByteCharSetProber(WINDOWS_1255_HEBREW_MODEL,\n                                                        False, hebrew_prober)\n        # TODO: See if using ISO-8859-8 Hebrew model works better here, since\n        #       it's actually the visual one\n        visual_hebrew_prober = SingleByteCharSetProber(WINDOWS_1255_HEBREW_MODEL,\n                                                       True, hebrew_prober)\n        hebrew_prober.set_model_probers(logical_hebrew_prober,\n                                        visual_hebrew_prober)\n        # TODO: ORDER MATTERS HERE. I changed the order vs what was in master\n        #       and several tests failed that did not before. Some thought\n        #       should be put into the ordering, and we should consider making\n        #       order not matter here, because that is very counter-intuitive.\n        self.probers = [\n            SingleByteCharSetProber(WINDOWS_1251_RUSSIAN_MODEL),\n            SingleByteCharSetProber(KOI8_R_RUSSIAN_MODEL),\n            SingleByteCharSetProber(ISO_8859_5_RUSSIAN_MODEL),\n            SingleByteCharSetProber(MACCYRILLIC_RUSSIAN_MODEL),\n            SingleByteCharSetProber(IBM866_RUSSIAN_MODEL),\n            SingleByteCharSetProber(IBM855_RUSSIAN_MODEL),\n            SingleByteCharSetProber(ISO_8859_7_GREEK_MODEL),\n            SingleByteCharSetProber(WINDOWS_1253_GREEK_MODEL),\n            SingleByteCharSetProber(ISO_8859_5_BULGARIAN_MODEL),\n            SingleByteCharSetProber(WINDOWS_1251_BULGARIAN_MODEL),\n            # TODO: Restore Hungarian encodings (iso-8859-2 and windows-1250)\n            #       after we retrain model.\n            # SingleByteCharSetProber(ISO_8859_2_HUNGARIAN_MODEL),\n            # SingleByteCharSetProber(WINDOWS_1250_HUNGARIAN_MODEL),\n            SingleByteCharSetProber(TIS_620_THAI_MODEL),\n            SingleByteCharSetProber(ISO_8859_9_TURKISH_MODEL),\n            hebrew_prober,\n            logical_hebrew_prober,\n            visual_hebrew_prober,\n        ]\n        self.reset()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/sjisprober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is mozilla.org code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .mbcharsetprober import MultiByteCharSetProber\nfrom .codingstatemachine import CodingStateMachine\nfrom .chardistribution import SJISDistributionAnalysis\nfrom .jpcntx import SJISContextAnalysis\nfrom .mbcssm import SJIS_SM_MODEL\nfrom .enums import ProbingState, MachineState\n\n\nclass SJISProber(MultiByteCharSetProber):\n    def __init__(self):\n        super(SJISProber, self).__init__()\n        self.coding_sm = CodingStateMachine(SJIS_SM_MODEL)\n        self.distribution_analyzer = SJISDistributionAnalysis()\n        self.context_analyzer = SJISContextAnalysis()\n        self.reset()\n\n    def reset(self):\n        super(SJISProber, self).reset()\n        self.context_analyzer.reset()\n\n    @property\n    def charset_name(self):\n        return self.context_analyzer.charset_name\n\n    @property\n    def language(self):\n        return \"Japanese\"\n\n    def feed(self, byte_str):\n        for i in range(len(byte_str)):\n            coding_state = self.coding_sm.next_state(byte_str[i])\n            if coding_state == MachineState.ERROR:\n                self.logger.debug('%s %s prober hit error at byte %s',\n                                  self.charset_name, self.language, i)\n                self._state = ProbingState.NOT_ME\n                break\n            elif coding_state == MachineState.ITS_ME:\n                self._state = ProbingState.FOUND_IT\n                break\n            elif coding_state == MachineState.START:\n                char_len = self.coding_sm.get_current_charlen()\n                if i == 0:\n                    self._last_char[1] = byte_str[0]\n                    self.context_analyzer.feed(self._last_char[2 - char_len:],\n                                               char_len)\n                    self.distribution_analyzer.feed(self._last_char, char_len)\n                else:\n                    self.context_analyzer.feed(byte_str[i + 1 - char_len:i + 3\n                                                        - char_len], char_len)\n                    self.distribution_analyzer.feed(byte_str[i - 1:i + 1],\n                                                    char_len)\n\n        self._last_char[0] = byte_str[-1]\n\n        if self.state == ProbingState.DETECTING:\n            if (self.context_analyzer.got_enough_data() and\n               (self.get_confidence() > self.SHORTCUT_THRESHOLD)):\n                self._state = ProbingState.FOUND_IT\n\n        return self.state\n\n    def get_confidence(self):\n        context_conf = self.context_analyzer.get_confidence()\n        distrib_conf = self.distribution_analyzer.get_confidence()\n        return max(context_conf, distrib_conf)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/universaldetector.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is Mozilla Universal charset detector code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 2001\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#   Shy Shalom - original C code\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\"\"\"\nModule containing the UniversalDetector detector class, which is the primary\nclass a user of ``chardet`` should use.\n\n:author: Mark Pilgrim (initial port to Python)\n:author: Shy Shalom (original C code)\n:author: Dan Blanchard (major refactoring for 3.0)\n:author: Ian Cordasco\n\"\"\"\n\n\nimport codecs\nimport logging\nimport re\n\nfrom .charsetgroupprober import CharSetGroupProber\nfrom .enums import InputState, LanguageFilter, ProbingState\nfrom .escprober import EscCharSetProber\nfrom .latin1prober import Latin1Prober\nfrom .mbcsgroupprober import MBCSGroupProber\nfrom .sbcsgroupprober import SBCSGroupProber\n\n\nclass UniversalDetector(object):\n    \"\"\"\n    The ``UniversalDetector`` class underlies the ``chardet.detect`` function\n    and coordinates all of the different charset probers.\n\n    To get a ``dict`` containing an encoding and its confidence, you can simply\n    run:\n\n    .. code::\n\n            u = UniversalDetector()\n            u.feed(some_bytes)\n            u.close()\n            detected = u.result\n\n    \"\"\"\n\n    MINIMUM_THRESHOLD = 0.20\n    HIGH_BYTE_DETECTOR = re.compile(b'[\\x80-\\xFF]')\n    ESC_DETECTOR = re.compile(b'(\\033|~{)')\n    WIN_BYTE_DETECTOR = re.compile(b'[\\x80-\\x9F]')\n    ISO_WIN_MAP = {'iso-8859-1': 'Windows-1252',\n                   'iso-8859-2': 'Windows-1250',\n                   'iso-8859-5': 'Windows-1251',\n                   'iso-8859-6': 'Windows-1256',\n                   'iso-8859-7': 'Windows-1253',\n                   'iso-8859-8': 'Windows-1255',\n                   'iso-8859-9': 'Windows-1254',\n                   'iso-8859-13': 'Windows-1257'}\n\n    def __init__(self, lang_filter=LanguageFilter.ALL):\n        self._esc_charset_prober = None\n        self._charset_probers = []\n        self.result = None\n        self.done = None\n        self._got_data = None\n        self._input_state = None\n        self._last_char = None\n        self.lang_filter = lang_filter\n        self.logger = logging.getLogger(__name__)\n        self._has_win_bytes = None\n        self.reset()\n\n    def reset(self):\n        \"\"\"\n        Reset the UniversalDetector and all of its probers back to their\n        initial states.  This is called by ``__init__``, so you only need to\n        call this directly in between analyses of different documents.\n        \"\"\"\n        self.result = {'encoding': None, 'confidence': 0.0, 'language': None}\n        self.done = False\n        self._got_data = False\n        self._has_win_bytes = False\n        self._input_state = InputState.PURE_ASCII\n        self._last_char = b''\n        if self._esc_charset_prober:\n            self._esc_charset_prober.reset()\n        for prober in self._charset_probers:\n            prober.reset()\n\n    def feed(self, byte_str):\n        \"\"\"\n        Takes a chunk of a document and feeds it through all of the relevant\n        charset probers.\n\n        After calling ``feed``, you can check the value of the ``done``\n        attribute to see if you need to continue feeding the\n        ``UniversalDetector`` more data, or if it has made a prediction\n        (in the ``result`` attribute).\n\n        .. note::\n           You should always call ``close`` when you're done feeding in your\n           document if ``done`` is not already ``True``.\n        \"\"\"\n        if self.done:\n            return\n\n        if not len(byte_str):\n            return\n\n        if not isinstance(byte_str, bytearray):\n            byte_str = bytearray(byte_str)\n\n        # First check for known BOMs, since these are guaranteed to be correct\n        if not self._got_data:\n            # If the data starts with BOM, we know it is UTF\n            if byte_str.startswith(codecs.BOM_UTF8):\n                # EF BB BF  UTF-8 with BOM\n                self.result = {'encoding': \"UTF-8-SIG\",\n                               'confidence': 1.0,\n                               'language': ''}\n            elif byte_str.startswith((codecs.BOM_UTF32_LE,\n                                      codecs.BOM_UTF32_BE)):\n                # FF FE 00 00  UTF-32, little-endian BOM\n                # 00 00 FE FF  UTF-32, big-endian BOM\n                self.result = {'encoding': \"UTF-32\",\n                               'confidence': 1.0,\n                               'language': ''}\n            elif byte_str.startswith(b'\\xFE\\xFF\\x00\\x00'):\n                # FE FF 00 00  UCS-4, unusual octet order BOM (3412)\n                self.result = {'encoding': \"X-ISO-10646-UCS-4-3412\",\n                               'confidence': 1.0,\n                               'language': ''}\n            elif byte_str.startswith(b'\\x00\\x00\\xFF\\xFE'):\n                # 00 00 FF FE  UCS-4, unusual octet order BOM (2143)\n                self.result = {'encoding': \"X-ISO-10646-UCS-4-2143\",\n                               'confidence': 1.0,\n                               'language': ''}\n            elif byte_str.startswith((codecs.BOM_LE, codecs.BOM_BE)):\n                # FF FE  UTF-16, little endian BOM\n                # FE FF  UTF-16, big endian BOM\n                self.result = {'encoding': \"UTF-16\",\n                               'confidence': 1.0,\n                               'language': ''}\n\n            self._got_data = True\n            if self.result['encoding'] is not None:\n                self.done = True\n                return\n\n        # If none of those matched and we've only see ASCII so far, check\n        # for high bytes and escape sequences\n        if self._input_state == InputState.PURE_ASCII:\n            if self.HIGH_BYTE_DETECTOR.search(byte_str):\n                self._input_state = InputState.HIGH_BYTE\n            elif self._input_state == InputState.PURE_ASCII and \\\n                    self.ESC_DETECTOR.search(self._last_char + byte_str):\n                self._input_state = InputState.ESC_ASCII\n\n        self._last_char = byte_str[-1:]\n\n        # If we've seen escape sequences, use the EscCharSetProber, which\n        # uses a simple state machine to check for known escape sequences in\n        # HZ and ISO-2022 encodings, since those are the only encodings that\n        # use such sequences.\n        if self._input_state == InputState.ESC_ASCII:\n            if not self._esc_charset_prober:\n                self._esc_charset_prober = EscCharSetProber(self.lang_filter)\n            if self._esc_charset_prober.feed(byte_str) == ProbingState.FOUND_IT:\n                self.result = {'encoding':\n                               self._esc_charset_prober.charset_name,\n                               'confidence':\n                               self._esc_charset_prober.get_confidence(),\n                               'language':\n                               self._esc_charset_prober.language}\n                self.done = True\n        # If we've seen high bytes (i.e., those with values greater than 127),\n        # we need to do more complicated checks using all our multi-byte and\n        # single-byte probers that are left.  The single-byte probers\n        # use character bigram distributions to determine the encoding, whereas\n        # the multi-byte probers use a combination of character unigram and\n        # bigram distributions.\n        elif self._input_state == InputState.HIGH_BYTE:\n            if not self._charset_probers:\n                self._charset_probers = [MBCSGroupProber(self.lang_filter)]\n                # If we're checking non-CJK encodings, use single-byte prober\n                if self.lang_filter & LanguageFilter.NON_CJK:\n                    self._charset_probers.append(SBCSGroupProber())\n                self._charset_probers.append(Latin1Prober())\n            for prober in self._charset_probers:\n                if prober.feed(byte_str) == ProbingState.FOUND_IT:\n                    self.result = {'encoding': prober.charset_name,\n                                   'confidence': prober.get_confidence(),\n                                   'language': prober.language}\n                    self.done = True\n                    break\n            if self.WIN_BYTE_DETECTOR.search(byte_str):\n                self._has_win_bytes = True\n\n    def close(self):\n        \"\"\"\n        Stop analyzing the current document and come up with a final\n        prediction.\n\n        :returns:  The ``result`` attribute, a ``dict`` with the keys\n                   `encoding`, `confidence`, and `language`.\n        \"\"\"\n        # Don't bother with checks if we're already done\n        if self.done:\n            return self.result\n        self.done = True\n\n        if not self._got_data:\n            self.logger.debug('no data received!')\n\n        # Default to ASCII if it is all we've seen so far\n        elif self._input_state == InputState.PURE_ASCII:\n            self.result = {'encoding': 'ascii',\n                           'confidence': 1.0,\n                           'language': ''}\n\n        # If we have seen non-ASCII, return the best that met MINIMUM_THRESHOLD\n        elif self._input_state == InputState.HIGH_BYTE:\n            prober_confidence = None\n            max_prober_confidence = 0.0\n            max_prober = None\n            for prober in self._charset_probers:\n                if not prober:\n                    continue\n                prober_confidence = prober.get_confidence()\n                if prober_confidence > max_prober_confidence:\n                    max_prober_confidence = prober_confidence\n                    max_prober = prober\n            if max_prober and (max_prober_confidence > self.MINIMUM_THRESHOLD):\n                charset_name = max_prober.charset_name\n                lower_charset_name = max_prober.charset_name.lower()\n                confidence = max_prober.get_confidence()\n                # Use Windows encoding name instead of ISO-8859 if we saw any\n                # extra Windows-specific bytes\n                if lower_charset_name.startswith('iso-8859'):\n                    if self._has_win_bytes:\n                        charset_name = self.ISO_WIN_MAP.get(lower_charset_name,\n                                                            charset_name)\n                self.result = {'encoding': charset_name,\n                               'confidence': confidence,\n                               'language': max_prober.language}\n\n        # Log all prober confidences if none met MINIMUM_THRESHOLD\n        if self.logger.getEffectiveLevel() <= logging.DEBUG:\n            if self.result['encoding'] is None:\n                self.logger.debug('no probers hit minimum threshold')\n                for group_prober in self._charset_probers:\n                    if not group_prober:\n                        continue\n                    if isinstance(group_prober, CharSetGroupProber):\n                        for prober in group_prober.probers:\n                            self.logger.debug('%s %s confidence = %s',\n                                              prober.charset_name,\n                                              prober.language,\n                                              prober.get_confidence())\n                    else:\n                        self.logger.debug('%s %s confidence = %s',\n                                          group_prober.charset_name,\n                                          group_prober.language,\n                                          group_prober.get_confidence())\n        return self.result\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/utf8prober.py",
    "content": "######################## BEGIN LICENSE BLOCK ########################\n# The Original Code is mozilla.org code.\n#\n# The Initial Developer of the Original Code is\n# Netscape Communications Corporation.\n# Portions created by the Initial Developer are Copyright (C) 1998\n# the Initial Developer. All Rights Reserved.\n#\n# Contributor(s):\n#   Mark Pilgrim - port to Python\n#\n# This library is free software; you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public\n# License as published by the Free Software Foundation; either\n# version 2.1 of the License, or (at your option) any later version.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n# Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with this library; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n# 02110-1301  USA\n######################### END LICENSE BLOCK #########################\n\nfrom .charsetprober import CharSetProber\nfrom .enums import ProbingState, MachineState\nfrom .codingstatemachine import CodingStateMachine\nfrom .mbcssm import UTF8_SM_MODEL\n\n\n\nclass UTF8Prober(CharSetProber):\n    ONE_CHAR_PROB = 0.5\n\n    def __init__(self):\n        super(UTF8Prober, self).__init__()\n        self.coding_sm = CodingStateMachine(UTF8_SM_MODEL)\n        self._num_mb_chars = None\n        self.reset()\n\n    def reset(self):\n        super(UTF8Prober, self).reset()\n        self.coding_sm.reset()\n        self._num_mb_chars = 0\n\n    @property\n    def charset_name(self):\n        return \"utf-8\"\n\n    @property\n    def language(self):\n        return \"\"\n\n    def feed(self, byte_str):\n        for c in byte_str:\n            coding_state = self.coding_sm.next_state(c)\n            if coding_state == MachineState.ERROR:\n                self._state = ProbingState.NOT_ME\n                break\n            elif coding_state == MachineState.ITS_ME:\n                self._state = ProbingState.FOUND_IT\n                break\n            elif coding_state == MachineState.START:\n                if self.coding_sm.get_current_charlen() >= 2:\n                    self._num_mb_chars += 1\n\n        if self.state == ProbingState.DETECTING:\n            if self.get_confidence() > self.SHORTCUT_THRESHOLD:\n                self._state = ProbingState.FOUND_IT\n\n        return self.state\n\n    def get_confidence(self):\n        unlike = 0.99\n        if self._num_mb_chars < 6:\n            unlike *= self.ONE_CHAR_PROB ** self._num_mb_chars\n            return 1.0 - unlike\n        else:\n            return unlike\n"
  },
  {
    "path": "openpype/vendor/python/python_2/chardet/version.py",
    "content": "\"\"\"\nThis module exists only to simplify retrieving the version number of chardet\nfrom within setup.py and from chardet subpackages.\n\n:author: Dan Blanchard (dan.blanchard@gmail.com)\n\"\"\"\n\n__version__ = \"4.0.0\"\nVERSION = __version__.split('.')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/charset_normalizer.py",
    "content": "\"\"\"Fake 'charset_normalizer' for Python 2 to raise ImportError.\n\nNeeded for 'requests' module which first checks for existence of\n'charset_normalizer' (Python 3) but does not raise 'ImportError' but\n'SyntaxError'.\n\"\"\"\n\nraise ImportError(\"charset_normalizer not available\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/__init__.py",
    "content": "\"\"\"\nClick is a simple Python module inspired by the stdlib optparse to make\nwriting command line scripts fun. Unlike other modules, it's based\naround a simple API that does not come with too much magic and is\ncomposable.\n\"\"\"\nfrom .core import Argument\nfrom .core import BaseCommand\nfrom .core import Command\nfrom .core import CommandCollection\nfrom .core import Context\nfrom .core import Group\nfrom .core import MultiCommand\nfrom .core import Option\nfrom .core import Parameter\nfrom .decorators import argument\nfrom .decorators import command\nfrom .decorators import confirmation_option\nfrom .decorators import group\nfrom .decorators import help_option\nfrom .decorators import make_pass_decorator\nfrom .decorators import option\nfrom .decorators import pass_context\nfrom .decorators import pass_obj\nfrom .decorators import password_option\nfrom .decorators import version_option\nfrom .exceptions import Abort\nfrom .exceptions import BadArgumentUsage\nfrom .exceptions import BadOptionUsage\nfrom .exceptions import BadParameter\nfrom .exceptions import ClickException\nfrom .exceptions import FileError\nfrom .exceptions import MissingParameter\nfrom .exceptions import NoSuchOption\nfrom .exceptions import UsageError\nfrom .formatting import HelpFormatter\nfrom .formatting import wrap_text\nfrom .globals import get_current_context\nfrom .parser import OptionParser\nfrom .termui import clear\nfrom .termui import confirm\nfrom .termui import echo_via_pager\nfrom .termui import edit\nfrom .termui import get_terminal_size\nfrom .termui import getchar\nfrom .termui import launch\nfrom .termui import pause\nfrom .termui import progressbar\nfrom .termui import prompt\nfrom .termui import secho\nfrom .termui import style\nfrom .termui import unstyle\nfrom .types import BOOL\nfrom .types import Choice\nfrom .types import DateTime\nfrom .types import File\nfrom .types import FLOAT\nfrom .types import FloatRange\nfrom .types import INT\nfrom .types import IntRange\nfrom .types import ParamType\nfrom .types import Path\nfrom .types import STRING\nfrom .types import Tuple\nfrom .types import UNPROCESSED\nfrom .types import UUID\nfrom .utils import echo\nfrom .utils import format_filename\nfrom .utils import get_app_dir\nfrom .utils import get_binary_stream\nfrom .utils import get_os_args\nfrom .utils import get_text_stream\nfrom .utils import open_file\n\n# Controls if click should emit the warning about the use of unicode\n# literals.\ndisable_unicode_literals_warning = False\n\n__version__ = \"7.1.2\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/_bashcomplete.py",
    "content": "import copy\nimport os\nimport re\n\nfrom .core import Argument\nfrom .core import MultiCommand\nfrom .core import Option\nfrom .parser import split_arg_string\nfrom .types import Choice\nfrom .utils import echo\n\ntry:\n    from collections import abc\nexcept ImportError:\n    import collections as abc\n\nWORDBREAK = \"=\"\n\n# Note, only BASH version 4.4 and later have the nosort option.\nCOMPLETION_SCRIPT_BASH = \"\"\"\n%(complete_func)s() {\n    local IFS=$'\\n'\n    COMPREPLY=( $( env COMP_WORDS=\"${COMP_WORDS[*]}\" \\\\\n                   COMP_CWORD=$COMP_CWORD \\\\\n                   %(autocomplete_var)s=complete $1 ) )\n    return 0\n}\n\n%(complete_func)setup() {\n    local COMPLETION_OPTIONS=\"\"\n    local BASH_VERSION_ARR=(${BASH_VERSION//./ })\n    # Only BASH version 4.4 and later have the nosort option.\n    if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] \\\n&& [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then\n        COMPLETION_OPTIONS=\"-o nosort\"\n    fi\n\n    complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s\n}\n\n%(complete_func)setup\n\"\"\"\n\nCOMPLETION_SCRIPT_ZSH = \"\"\"\n#compdef %(script_names)s\n\n%(complete_func)s() {\n    local -a completions\n    local -a completions_with_descriptions\n    local -a response\n    (( ! $+commands[%(script_names)s] )) && return 1\n\n    response=(\"${(@f)$( env COMP_WORDS=\\\"${words[*]}\\\" \\\\\n                        COMP_CWORD=$((CURRENT-1)) \\\\\n                        %(autocomplete_var)s=\\\"complete_zsh\\\" \\\\\n                        %(script_names)s )}\")\n\n    for key descr in ${(kv)response}; do\n      if [[ \"$descr\" == \"_\" ]]; then\n          completions+=(\"$key\")\n      else\n          completions_with_descriptions+=(\"$key\":\"$descr\")\n      fi\n    done\n\n    if [ -n \"$completions_with_descriptions\" ]; then\n        _describe -V unsorted completions_with_descriptions -U\n    fi\n\n    if [ -n \"$completions\" ]; then\n        compadd -U -V unsorted -a completions\n    fi\n    compstate[insert]=\"automenu\"\n}\n\ncompdef %(complete_func)s %(script_names)s\n\"\"\"\n\nCOMPLETION_SCRIPT_FISH = (\n    \"complete --no-files --command %(script_names)s --arguments\"\n    ' \"(env %(autocomplete_var)s=complete_fish'\n    \" COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t)\"\n    ' %(script_names)s)\"'\n)\n\n_completion_scripts = {\n    \"bash\": COMPLETION_SCRIPT_BASH,\n    \"zsh\": COMPLETION_SCRIPT_ZSH,\n    \"fish\": COMPLETION_SCRIPT_FISH,\n}\n\n_invalid_ident_char_re = re.compile(r\"[^a-zA-Z0-9_]\")\n\n\ndef get_completion_script(prog_name, complete_var, shell):\n    cf_name = _invalid_ident_char_re.sub(\"\", prog_name.replace(\"-\", \"_\"))\n    script = _completion_scripts.get(shell, COMPLETION_SCRIPT_BASH)\n    return (\n        script\n        % {\n            \"complete_func\": \"_{}_completion\".format(cf_name),\n            \"script_names\": prog_name,\n            \"autocomplete_var\": complete_var,\n        }\n    ).strip() + \";\"\n\n\ndef resolve_ctx(cli, prog_name, args):\n    \"\"\"Parse into a hierarchy of contexts. Contexts are connected\n    through the parent variable.\n\n    :param cli: command definition\n    :param prog_name: the program that is running\n    :param args: full list of args\n    :return: the final context/command parsed\n    \"\"\"\n    ctx = cli.make_context(prog_name, args, resilient_parsing=True)\n    args = ctx.protected_args + ctx.args\n    while args:\n        if isinstance(ctx.command, MultiCommand):\n            if not ctx.command.chain:\n                cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)\n                if cmd is None:\n                    return ctx\n                ctx = cmd.make_context(\n                    cmd_name, args, parent=ctx, resilient_parsing=True\n                )\n                args = ctx.protected_args + ctx.args\n            else:\n                # Walk chained subcommand contexts saving the last one.\n                while args:\n                    cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)\n                    if cmd is None:\n                        return ctx\n                    sub_ctx = cmd.make_context(\n                        cmd_name,\n                        args,\n                        parent=ctx,\n                        allow_extra_args=True,\n                        allow_interspersed_args=False,\n                        resilient_parsing=True,\n                    )\n                    args = sub_ctx.args\n                ctx = sub_ctx\n                args = sub_ctx.protected_args + sub_ctx.args\n        else:\n            break\n    return ctx\n\n\ndef start_of_option(param_str):\n    \"\"\"\n    :param param_str: param_str to check\n    :return: whether or not this is the start of an option declaration\n        (i.e. starts \"-\" or \"--\")\n    \"\"\"\n    return param_str and param_str[:1] == \"-\"\n\n\ndef is_incomplete_option(all_args, cmd_param):\n    \"\"\"\n    :param all_args: the full original list of args supplied\n    :param cmd_param: the current command paramter\n    :return: whether or not the last option declaration (i.e. starts\n        \"-\" or \"--\") is incomplete and corresponds to this cmd_param. In\n        other words whether this cmd_param option can still accept\n        values\n    \"\"\"\n    if not isinstance(cmd_param, Option):\n        return False\n    if cmd_param.is_flag:\n        return False\n    last_option = None\n    for index, arg_str in enumerate(\n        reversed([arg for arg in all_args if arg != WORDBREAK])\n    ):\n        if index + 1 > cmd_param.nargs:\n            break\n        if start_of_option(arg_str):\n            last_option = arg_str\n\n    return True if last_option and last_option in cmd_param.opts else False\n\n\ndef is_incomplete_argument(current_params, cmd_param):\n    \"\"\"\n    :param current_params: the current params and values for this\n        argument as already entered\n    :param cmd_param: the current command parameter\n    :return: whether or not the last argument is incomplete and\n        corresponds to this cmd_param. In other words whether or not the\n        this cmd_param argument can still accept values\n    \"\"\"\n    if not isinstance(cmd_param, Argument):\n        return False\n    current_param_values = current_params[cmd_param.name]\n    if current_param_values is None:\n        return True\n    if cmd_param.nargs == -1:\n        return True\n    if (\n        isinstance(current_param_values, abc.Iterable)\n        and cmd_param.nargs > 1\n        and len(current_param_values) < cmd_param.nargs\n    ):\n        return True\n    return False\n\n\ndef get_user_autocompletions(ctx, args, incomplete, cmd_param):\n    \"\"\"\n    :param ctx: context associated with the parsed command\n    :param args: full list of args\n    :param incomplete: the incomplete text to autocomplete\n    :param cmd_param: command definition\n    :return: all the possible user-specified completions for the param\n    \"\"\"\n    results = []\n    if isinstance(cmd_param.type, Choice):\n        # Choices don't support descriptions.\n        results = [\n            (c, None) for c in cmd_param.type.choices if str(c).startswith(incomplete)\n        ]\n    elif cmd_param.autocompletion is not None:\n        dynamic_completions = cmd_param.autocompletion(\n            ctx=ctx, args=args, incomplete=incomplete\n        )\n        results = [\n            c if isinstance(c, tuple) else (c, None) for c in dynamic_completions\n        ]\n    return results\n\n\ndef get_visible_commands_starting_with(ctx, starts_with):\n    \"\"\"\n    :param ctx: context associated with the parsed command\n    :starts_with: string that visible commands must start with.\n    :return: all visible (not hidden) commands that start with starts_with.\n    \"\"\"\n    for c in ctx.command.list_commands(ctx):\n        if c.startswith(starts_with):\n            command = ctx.command.get_command(ctx, c)\n            if not command.hidden:\n                yield command\n\n\ndef add_subcommand_completions(ctx, incomplete, completions_out):\n    # Add subcommand completions.\n    if isinstance(ctx.command, MultiCommand):\n        completions_out.extend(\n            [\n                (c.name, c.get_short_help_str())\n                for c in get_visible_commands_starting_with(ctx, incomplete)\n            ]\n        )\n\n    # Walk up the context list and add any other completion\n    # possibilities from chained commands\n    while ctx.parent is not None:\n        ctx = ctx.parent\n        if isinstance(ctx.command, MultiCommand) and ctx.command.chain:\n            remaining_commands = [\n                c\n                for c in get_visible_commands_starting_with(ctx, incomplete)\n                if c.name not in ctx.protected_args\n            ]\n            completions_out.extend(\n                [(c.name, c.get_short_help_str()) for c in remaining_commands]\n            )\n\n\ndef get_choices(cli, prog_name, args, incomplete):\n    \"\"\"\n    :param cli: command definition\n    :param prog_name: the program that is running\n    :param args: full list of args\n    :param incomplete: the incomplete text to autocomplete\n    :return: all the possible completions for the incomplete\n    \"\"\"\n    all_args = copy.deepcopy(args)\n\n    ctx = resolve_ctx(cli, prog_name, args)\n    if ctx is None:\n        return []\n\n    has_double_dash = \"--\" in all_args\n\n    # In newer versions of bash long opts with '='s are partitioned, but\n    # it's easier to parse without the '='\n    if start_of_option(incomplete) and WORDBREAK in incomplete:\n        partition_incomplete = incomplete.partition(WORDBREAK)\n        all_args.append(partition_incomplete[0])\n        incomplete = partition_incomplete[2]\n    elif incomplete == WORDBREAK:\n        incomplete = \"\"\n\n    completions = []\n    if not has_double_dash and start_of_option(incomplete):\n        # completions for partial options\n        for param in ctx.command.params:\n            if isinstance(param, Option) and not param.hidden:\n                param_opts = [\n                    param_opt\n                    for param_opt in param.opts + param.secondary_opts\n                    if param_opt not in all_args or param.multiple\n                ]\n                completions.extend(\n                    [(o, param.help) for o in param_opts if o.startswith(incomplete)]\n                )\n        return completions\n    # completion for option values from user supplied values\n    for param in ctx.command.params:\n        if is_incomplete_option(all_args, param):\n            return get_user_autocompletions(ctx, all_args, incomplete, param)\n    # completion for argument values from user supplied values\n    for param in ctx.command.params:\n        if is_incomplete_argument(ctx.params, param):\n            return get_user_autocompletions(ctx, all_args, incomplete, param)\n\n    add_subcommand_completions(ctx, incomplete, completions)\n    # Sort before returning so that proper ordering can be enforced in custom types.\n    return sorted(completions)\n\n\ndef do_complete(cli, prog_name, include_descriptions):\n    cwords = split_arg_string(os.environ[\"COMP_WORDS\"])\n    cword = int(os.environ[\"COMP_CWORD\"])\n    args = cwords[1:cword]\n    try:\n        incomplete = cwords[cword]\n    except IndexError:\n        incomplete = \"\"\n\n    for item in get_choices(cli, prog_name, args, incomplete):\n        echo(item[0])\n        if include_descriptions:\n            # ZSH has trouble dealing with empty array parameters when\n            # returned from commands, use '_' to indicate no description\n            # is present.\n            echo(item[1] if item[1] else \"_\")\n\n    return True\n\n\ndef do_complete_fish(cli, prog_name):\n    cwords = split_arg_string(os.environ[\"COMP_WORDS\"])\n    incomplete = os.environ[\"COMP_CWORD\"]\n    args = cwords[1:]\n\n    for item in get_choices(cli, prog_name, args, incomplete):\n        if item[1]:\n            echo(\"{arg}\\t{desc}\".format(arg=item[0], desc=item[1]))\n        else:\n            echo(item[0])\n\n    return True\n\n\ndef bashcomplete(cli, prog_name, complete_var, complete_instr):\n    if \"_\" in complete_instr:\n        command, shell = complete_instr.split(\"_\", 1)\n    else:\n        command = complete_instr\n        shell = \"bash\"\n\n    if command == \"source\":\n        echo(get_completion_script(prog_name, complete_var, shell))\n        return True\n    elif command == \"complete\":\n        if shell == \"fish\":\n            return do_complete_fish(cli, prog_name)\n        elif shell in {\"bash\", \"zsh\"}:\n            return do_complete(cli, prog_name, shell == \"zsh\")\n\n    return False\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/_compat.py",
    "content": "# flake8: noqa\nimport codecs\nimport io\nimport os\nimport re\nimport sys\nfrom weakref import WeakKeyDictionary\n\nPY2 = sys.version_info[0] == 2\nCYGWIN = sys.platform.startswith(\"cygwin\")\nMSYS2 = sys.platform.startswith(\"win\") and (\"GCC\" in sys.version)\n# Determine local App Engine environment, per Google's own suggestion\nAPP_ENGINE = \"APPENGINE_RUNTIME\" in os.environ and \"Development/\" in os.environ.get(\n    \"SERVER_SOFTWARE\", \"\"\n)\nWIN = sys.platform.startswith(\"win\") and not APP_ENGINE and not MSYS2\nDEFAULT_COLUMNS = 80\n\n\n_ansi_re = re.compile(r\"\\033\\[[;?0-9]*[a-zA-Z]\")\n\n\ndef get_filesystem_encoding():\n    return sys.getfilesystemencoding() or sys.getdefaultencoding()\n\n\ndef _make_text_stream(\n    stream, encoding, errors, force_readable=False, force_writable=False\n):\n    if encoding is None:\n        encoding = get_best_encoding(stream)\n    if errors is None:\n        errors = \"replace\"\n    return _NonClosingTextIOWrapper(\n        stream,\n        encoding,\n        errors,\n        line_buffering=True,\n        force_readable=force_readable,\n        force_writable=force_writable,\n    )\n\n\ndef is_ascii_encoding(encoding):\n    \"\"\"Checks if a given encoding is ascii.\"\"\"\n    try:\n        return codecs.lookup(encoding).name == \"ascii\"\n    except LookupError:\n        return False\n\n\ndef get_best_encoding(stream):\n    \"\"\"Returns the default stream encoding if not found.\"\"\"\n    rv = getattr(stream, \"encoding\", None) or sys.getdefaultencoding()\n    if is_ascii_encoding(rv):\n        return \"utf-8\"\n    return rv\n\n\nclass _NonClosingTextIOWrapper(io.TextIOWrapper):\n    def __init__(\n        self,\n        stream,\n        encoding,\n        errors,\n        force_readable=False,\n        force_writable=False,\n        **extra\n    ):\n        self._stream = stream = _FixupStream(stream, force_readable, force_writable)\n        io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra)\n\n    # The io module is a place where the Python 3 text behavior\n    # was forced upon Python 2, so we need to unbreak\n    # it to look like Python 2.\n    if PY2:\n\n        def write(self, x):\n            if isinstance(x, str) or is_bytes(x):\n                try:\n                    self.flush()\n                except Exception:\n                    pass\n                return self.buffer.write(str(x))\n            return io.TextIOWrapper.write(self, x)\n\n        def writelines(self, lines):\n            for line in lines:\n                self.write(line)\n\n    def __del__(self):\n        try:\n            self.detach()\n        except Exception:\n            pass\n\n    def isatty(self):\n        # https://bitbucket.org/pypy/pypy/issue/1803\n        return self._stream.isatty()\n\n\nclass _FixupStream(object):\n    \"\"\"The new io interface needs more from streams than streams\n    traditionally implement.  As such, this fix-up code is necessary in\n    some circumstances.\n\n    The forcing of readable and writable flags are there because some tools\n    put badly patched objects on sys (one such offender are certain version\n    of jupyter notebook).\n    \"\"\"\n\n    def __init__(self, stream, force_readable=False, force_writable=False):\n        self._stream = stream\n        self._force_readable = force_readable\n        self._force_writable = force_writable\n\n    def __getattr__(self, name):\n        return getattr(self._stream, name)\n\n    def read1(self, size):\n        f = getattr(self._stream, \"read1\", None)\n        if f is not None:\n            return f(size)\n        # We only dispatch to readline instead of read in Python 2 as we\n        # do not want cause problems with the different implementation\n        # of line buffering.\n        if PY2:\n            return self._stream.readline(size)\n        return self._stream.read(size)\n\n    def readable(self):\n        if self._force_readable:\n            return True\n        x = getattr(self._stream, \"readable\", None)\n        if x is not None:\n            return x()\n        try:\n            self._stream.read(0)\n        except Exception:\n            return False\n        return True\n\n    def writable(self):\n        if self._force_writable:\n            return True\n        x = getattr(self._stream, \"writable\", None)\n        if x is not None:\n            return x()\n        try:\n            self._stream.write(\"\")\n        except Exception:\n            try:\n                self._stream.write(b\"\")\n            except Exception:\n                return False\n        return True\n\n    def seekable(self):\n        x = getattr(self._stream, \"seekable\", None)\n        if x is not None:\n            return x()\n        try:\n            self._stream.seek(self._stream.tell())\n        except Exception:\n            return False\n        return True\n\n\nif PY2:\n    text_type = unicode\n    raw_input = raw_input\n    string_types = (str, unicode)\n    int_types = (int, long)\n    iteritems = lambda x: x.iteritems()\n    range_type = xrange\n\n    def is_bytes(x):\n        return isinstance(x, (buffer, bytearray))\n\n    _identifier_re = re.compile(r\"^[a-zA-Z_][a-zA-Z0-9_]*$\")\n\n    # For Windows, we need to force stdout/stdin/stderr to binary if it's\n    # fetched for that.  This obviously is not the most correct way to do\n    # it as it changes global state.  Unfortunately, there does not seem to\n    # be a clear better way to do it as just reopening the file in binary\n    # mode does not change anything.\n    #\n    # An option would be to do what Python 3 does and to open the file as\n    # binary only, patch it back to the system, and then use a wrapper\n    # stream that converts newlines.  It's not quite clear what's the\n    # correct option here.\n    #\n    # This code also lives in _winconsole for the fallback to the console\n    # emulation stream.\n    #\n    # There are also Windows environments where the `msvcrt` module is not\n    # available (which is why we use try-catch instead of the WIN variable\n    # here), such as the Google App Engine development server on Windows. In\n    # those cases there is just nothing we can do.\n    def set_binary_mode(f):\n        return f\n\n    try:\n        import msvcrt\n    except ImportError:\n        pass\n    else:\n\n        def set_binary_mode(f):\n            try:\n                fileno = f.fileno()\n            except Exception:\n                pass\n            else:\n                msvcrt.setmode(fileno, os.O_BINARY)\n            return f\n\n    try:\n        import fcntl\n    except ImportError:\n        pass\n    else:\n\n        def set_binary_mode(f):\n            try:\n                fileno = f.fileno()\n            except Exception:\n                pass\n            else:\n                flags = fcntl.fcntl(fileno, fcntl.F_GETFL)\n                fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)\n            return f\n\n    def isidentifier(x):\n        return _identifier_re.search(x) is not None\n\n    def get_binary_stdin():\n        return set_binary_mode(sys.stdin)\n\n    def get_binary_stdout():\n        _wrap_std_stream(\"stdout\")\n        return set_binary_mode(sys.stdout)\n\n    def get_binary_stderr():\n        _wrap_std_stream(\"stderr\")\n        return set_binary_mode(sys.stderr)\n\n    def get_text_stdin(encoding=None, errors=None):\n        rv = _get_windows_console_stream(sys.stdin, encoding, errors)\n        if rv is not None:\n            return rv\n        return _make_text_stream(sys.stdin, encoding, errors, force_readable=True)\n\n    def get_text_stdout(encoding=None, errors=None):\n        _wrap_std_stream(\"stdout\")\n        rv = _get_windows_console_stream(sys.stdout, encoding, errors)\n        if rv is not None:\n            return rv\n        return _make_text_stream(sys.stdout, encoding, errors, force_writable=True)\n\n    def get_text_stderr(encoding=None, errors=None):\n        _wrap_std_stream(\"stderr\")\n        rv = _get_windows_console_stream(sys.stderr, encoding, errors)\n        if rv is not None:\n            return rv\n        return _make_text_stream(sys.stderr, encoding, errors, force_writable=True)\n\n    def filename_to_ui(value):\n        if isinstance(value, bytes):\n            value = value.decode(get_filesystem_encoding(), \"replace\")\n        return value\n\n\nelse:\n    import io\n\n    text_type = str\n    raw_input = input\n    string_types = (str,)\n    int_types = (int,)\n    range_type = range\n    isidentifier = lambda x: x.isidentifier()\n    iteritems = lambda x: iter(x.items())\n\n    def is_bytes(x):\n        return isinstance(x, (bytes, memoryview, bytearray))\n\n    def _is_binary_reader(stream, default=False):\n        try:\n            return isinstance(stream.read(0), bytes)\n        except Exception:\n            return default\n            # This happens in some cases where the stream was already\n            # closed.  In this case, we assume the default.\n\n    def _is_binary_writer(stream, default=False):\n        try:\n            stream.write(b\"\")\n        except Exception:\n            try:\n                stream.write(\"\")\n                return False\n            except Exception:\n                pass\n            return default\n        return True\n\n    def _find_binary_reader(stream):\n        # We need to figure out if the given stream is already binary.\n        # This can happen because the official docs recommend detaching\n        # the streams to get binary streams.  Some code might do this, so\n        # we need to deal with this case explicitly.\n        if _is_binary_reader(stream, False):\n            return stream\n\n        buf = getattr(stream, \"buffer\", None)\n\n        # Same situation here; this time we assume that the buffer is\n        # actually binary in case it's closed.\n        if buf is not None and _is_binary_reader(buf, True):\n            return buf\n\n    def _find_binary_writer(stream):\n        # We need to figure out if the given stream is already binary.\n        # This can happen because the official docs recommend detatching\n        # the streams to get binary streams.  Some code might do this, so\n        # we need to deal with this case explicitly.\n        if _is_binary_writer(stream, False):\n            return stream\n\n        buf = getattr(stream, \"buffer\", None)\n\n        # Same situation here; this time we assume that the buffer is\n        # actually binary in case it's closed.\n        if buf is not None and _is_binary_writer(buf, True):\n            return buf\n\n    def _stream_is_misconfigured(stream):\n        \"\"\"A stream is misconfigured if its encoding is ASCII.\"\"\"\n        # If the stream does not have an encoding set, we assume it's set\n        # to ASCII.  This appears to happen in certain unittest\n        # environments.  It's not quite clear what the correct behavior is\n        # but this at least will force Click to recover somehow.\n        return is_ascii_encoding(getattr(stream, \"encoding\", None) or \"ascii\")\n\n    def _is_compat_stream_attr(stream, attr, value):\n        \"\"\"A stream attribute is compatible if it is equal to the\n        desired value or the desired value is unset and the attribute\n        has a value.\n        \"\"\"\n        stream_value = getattr(stream, attr, None)\n        return stream_value == value or (value is None and stream_value is not None)\n\n    def _is_compatible_text_stream(stream, encoding, errors):\n        \"\"\"Check if a stream's encoding and errors attributes are\n        compatible with the desired values.\n        \"\"\"\n        return _is_compat_stream_attr(\n            stream, \"encoding\", encoding\n        ) and _is_compat_stream_attr(stream, \"errors\", errors)\n\n    def _force_correct_text_stream(\n        text_stream,\n        encoding,\n        errors,\n        is_binary,\n        find_binary,\n        force_readable=False,\n        force_writable=False,\n    ):\n        if is_binary(text_stream, False):\n            binary_reader = text_stream\n        else:\n            # If the stream looks compatible, and won't default to a\n            # misconfigured ascii encoding, return it as-is.\n            if _is_compatible_text_stream(text_stream, encoding, errors) and not (\n                encoding is None and _stream_is_misconfigured(text_stream)\n            ):\n                return text_stream\n\n            # Otherwise, get the underlying binary reader.\n            binary_reader = find_binary(text_stream)\n\n            # If that's not possible, silently use the original reader\n            # and get mojibake instead of exceptions.\n            if binary_reader is None:\n                return text_stream\n\n        # Default errors to replace instead of strict in order to get\n        # something that works.\n        if errors is None:\n            errors = \"replace\"\n\n        # Wrap the binary stream in a text stream with the correct\n        # encoding parameters.\n        return _make_text_stream(\n            binary_reader,\n            encoding,\n            errors,\n            force_readable=force_readable,\n            force_writable=force_writable,\n        )\n\n    def _force_correct_text_reader(text_reader, encoding, errors, force_readable=False):\n        return _force_correct_text_stream(\n            text_reader,\n            encoding,\n            errors,\n            _is_binary_reader,\n            _find_binary_reader,\n            force_readable=force_readable,\n        )\n\n    def _force_correct_text_writer(text_writer, encoding, errors, force_writable=False):\n        return _force_correct_text_stream(\n            text_writer,\n            encoding,\n            errors,\n            _is_binary_writer,\n            _find_binary_writer,\n            force_writable=force_writable,\n        )\n\n    def get_binary_stdin():\n        reader = _find_binary_reader(sys.stdin)\n        if reader is None:\n            raise RuntimeError(\"Was not able to determine binary stream for sys.stdin.\")\n        return reader\n\n    def get_binary_stdout():\n        writer = _find_binary_writer(sys.stdout)\n        if writer is None:\n            raise RuntimeError(\n                \"Was not able to determine binary stream for sys.stdout.\"\n            )\n        return writer\n\n    def get_binary_stderr():\n        writer = _find_binary_writer(sys.stderr)\n        if writer is None:\n            raise RuntimeError(\n                \"Was not able to determine binary stream for sys.stderr.\"\n            )\n        return writer\n\n    def get_text_stdin(encoding=None, errors=None):\n        rv = _get_windows_console_stream(sys.stdin, encoding, errors)\n        if rv is not None:\n            return rv\n        return _force_correct_text_reader(\n            sys.stdin, encoding, errors, force_readable=True\n        )\n\n    def get_text_stdout(encoding=None, errors=None):\n        rv = _get_windows_console_stream(sys.stdout, encoding, errors)\n        if rv is not None:\n            return rv\n        return _force_correct_text_writer(\n            sys.stdout, encoding, errors, force_writable=True\n        )\n\n    def get_text_stderr(encoding=None, errors=None):\n        rv = _get_windows_console_stream(sys.stderr, encoding, errors)\n        if rv is not None:\n            return rv\n        return _force_correct_text_writer(\n            sys.stderr, encoding, errors, force_writable=True\n        )\n\n    def filename_to_ui(value):\n        if isinstance(value, bytes):\n            value = value.decode(get_filesystem_encoding(), \"replace\")\n        else:\n            value = value.encode(\"utf-8\", \"surrogateescape\").decode(\"utf-8\", \"replace\")\n        return value\n\n\ndef get_streerror(e, default=None):\n    if hasattr(e, \"strerror\"):\n        msg = e.strerror\n    else:\n        if default is not None:\n            msg = default\n        else:\n            msg = str(e)\n    if isinstance(msg, bytes):\n        msg = msg.decode(\"utf-8\", \"replace\")\n    return msg\n\n\ndef _wrap_io_open(file, mode, encoding, errors):\n    \"\"\"On Python 2, :func:`io.open` returns a text file wrapper that\n    requires passing ``unicode`` to ``write``. Need to open the file in\n    binary mode then wrap it in a subclass that can write ``str`` and\n    ``unicode``.\n\n    Also handles not passing ``encoding`` and ``errors`` in binary mode.\n    \"\"\"\n    binary = \"b\" in mode\n\n    if binary:\n        kwargs = {}\n    else:\n        kwargs = {\"encoding\": encoding, \"errors\": errors}\n\n    if not PY2 or binary:\n        return io.open(file, mode, **kwargs)\n\n    f = io.open(file, \"{}b\".format(mode.replace(\"t\", \"\")))\n    return _make_text_stream(f, **kwargs)\n\n\ndef open_stream(filename, mode=\"r\", encoding=None, errors=\"strict\", atomic=False):\n    binary = \"b\" in mode\n\n    # Standard streams first.  These are simple because they don't need\n    # special handling for the atomic flag.  It's entirely ignored.\n    if filename == \"-\":\n        if any(m in mode for m in [\"w\", \"a\", \"x\"]):\n            if binary:\n                return get_binary_stdout(), False\n            return get_text_stdout(encoding=encoding, errors=errors), False\n        if binary:\n            return get_binary_stdin(), False\n        return get_text_stdin(encoding=encoding, errors=errors), False\n\n    # Non-atomic writes directly go out through the regular open functions.\n    if not atomic:\n        return _wrap_io_open(filename, mode, encoding, errors), True\n\n    # Some usability stuff for atomic writes\n    if \"a\" in mode:\n        raise ValueError(\n            \"Appending to an existing file is not supported, because that\"\n            \" would involve an expensive `copy`-operation to a temporary\"\n            \" file. Open the file in normal `w`-mode and copy explicitly\"\n            \" if that's what you're after.\"\n        )\n    if \"x\" in mode:\n        raise ValueError(\"Use the `overwrite`-parameter instead.\")\n    if \"w\" not in mode:\n        raise ValueError(\"Atomic writes only make sense with `w`-mode.\")\n\n    # Atomic writes are more complicated.  They work by opening a file\n    # as a proxy in the same folder and then using the fdopen\n    # functionality to wrap it in a Python file.  Then we wrap it in an\n    # atomic file that moves the file over on close.\n    import errno\n    import random\n\n    try:\n        perm = os.stat(filename).st_mode\n    except OSError:\n        perm = None\n\n    flags = os.O_RDWR | os.O_CREAT | os.O_EXCL\n\n    if binary:\n        flags |= getattr(os, \"O_BINARY\", 0)\n\n    while True:\n        tmp_filename = os.path.join(\n            os.path.dirname(filename),\n            \".__atomic-write{:08x}\".format(random.randrange(1 << 32)),\n        )\n        try:\n            fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)\n            break\n        except OSError as e:\n            if e.errno == errno.EEXIST or (\n                os.name == \"nt\"\n                and e.errno == errno.EACCES\n                and os.path.isdir(e.filename)\n                and os.access(e.filename, os.W_OK)\n            ):\n                continue\n            raise\n\n    if perm is not None:\n        os.chmod(tmp_filename, perm)  # in case perm includes bits in umask\n\n    f = _wrap_io_open(fd, mode, encoding, errors)\n    return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True\n\n\n# Used in a destructor call, needs extra protection from interpreter cleanup.\nif hasattr(os, \"replace\"):\n    _replace = os.replace\n    _can_replace = True\nelse:\n    _replace = os.rename\n    _can_replace = not WIN\n\n\nclass _AtomicFile(object):\n    def __init__(self, f, tmp_filename, real_filename):\n        self._f = f\n        self._tmp_filename = tmp_filename\n        self._real_filename = real_filename\n        self.closed = False\n\n    @property\n    def name(self):\n        return self._real_filename\n\n    def close(self, delete=False):\n        if self.closed:\n            return\n        self._f.close()\n        if not _can_replace:\n            try:\n                os.remove(self._real_filename)\n            except OSError:\n                pass\n        _replace(self._tmp_filename, self._real_filename)\n        self.closed = True\n\n    def __getattr__(self, name):\n        return getattr(self._f, name)\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_value, tb):\n        self.close(delete=exc_type is not None)\n\n    def __repr__(self):\n        return repr(self._f)\n\n\nauto_wrap_for_ansi = None\ncolorama = None\nget_winterm_size = None\n\n\ndef strip_ansi(value):\n    return _ansi_re.sub(\"\", value)\n\n\ndef _is_jupyter_kernel_output(stream):\n    if WIN:\n        # TODO: Couldn't test on Windows, should't try to support until\n        # someone tests the details wrt colorama.\n        return\n\n    while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):\n        stream = stream._stream\n\n    return stream.__class__.__module__.startswith(\"ipykernel.\")\n\n\ndef should_strip_ansi(stream=None, color=None):\n    if color is None:\n        if stream is None:\n            stream = sys.stdin\n        return not isatty(stream) and not _is_jupyter_kernel_output(stream)\n    return not color\n\n\n# If we're on Windows, we provide transparent integration through\n# colorama.  This will make ANSI colors through the echo function\n# work automatically.\nif WIN:\n    # Windows has a smaller terminal\n    DEFAULT_COLUMNS = 79\n\n    from ._winconsole import _get_windows_console_stream, _wrap_std_stream\n\n    def _get_argv_encoding():\n        import locale\n\n        return locale.getpreferredencoding()\n\n    if PY2:\n\n        def raw_input(prompt=\"\"):\n            sys.stderr.flush()\n            if prompt:\n                stdout = _default_text_stdout()\n                stdout.write(prompt)\n            stdin = _default_text_stdin()\n            return stdin.readline().rstrip(\"\\r\\n\")\n\n    try:\n        import colorama\n    except ImportError:\n        pass\n    else:\n        _ansi_stream_wrappers = WeakKeyDictionary()\n\n        def auto_wrap_for_ansi(stream, color=None):\n            \"\"\"This function wraps a stream so that calls through colorama\n            are issued to the win32 console API to recolor on demand.  It\n            also ensures to reset the colors if a write call is interrupted\n            to not destroy the console afterwards.\n            \"\"\"\n            try:\n                cached = _ansi_stream_wrappers.get(stream)\n            except Exception:\n                cached = None\n            if cached is not None:\n                return cached\n            strip = should_strip_ansi(stream, color)\n            ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)\n            rv = ansi_wrapper.stream\n            _write = rv.write\n\n            def _safe_write(s):\n                try:\n                    return _write(s)\n                except:\n                    ansi_wrapper.reset_all()\n                    raise\n\n            rv.write = _safe_write\n            try:\n                _ansi_stream_wrappers[stream] = rv\n            except Exception:\n                pass\n            return rv\n\n        def get_winterm_size():\n            win = colorama.win32.GetConsoleScreenBufferInfo(\n                colorama.win32.STDOUT\n            ).srWindow\n            return win.Right - win.Left, win.Bottom - win.Top\n\n\nelse:\n\n    def _get_argv_encoding():\n        return getattr(sys.stdin, \"encoding\", None) or get_filesystem_encoding()\n\n    _get_windows_console_stream = lambda *x: None\n    _wrap_std_stream = lambda *x: None\n\n\ndef term_len(x):\n    return len(strip_ansi(x))\n\n\ndef isatty(stream):\n    try:\n        return stream.isatty()\n    except Exception:\n        return False\n\n\ndef _make_cached_stream_func(src_func, wrapper_func):\n    cache = WeakKeyDictionary()\n\n    def func():\n        stream = src_func()\n        try:\n            rv = cache.get(stream)\n        except Exception:\n            rv = None\n        if rv is not None:\n            return rv\n        rv = wrapper_func()\n        try:\n            stream = src_func()  # In case wrapper_func() modified the stream\n            cache[stream] = rv\n        except Exception:\n            pass\n        return rv\n\n    return func\n\n\n_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin)\n_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout)\n_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)\n\n\nbinary_streams = {\n    \"stdin\": get_binary_stdin,\n    \"stdout\": get_binary_stdout,\n    \"stderr\": get_binary_stderr,\n}\n\ntext_streams = {\n    \"stdin\": get_text_stdin,\n    \"stdout\": get_text_stdout,\n    \"stderr\": get_text_stderr,\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/_termui_impl.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nThis module contains implementations for the termui module. To keep the\nimport time of Click down, some infrequently used functionality is\nplaced in this module and only imported as needed.\n\"\"\"\nimport contextlib\nimport math\nimport os\nimport sys\nimport time\n\nfrom ._compat import _default_text_stdout\nfrom ._compat import CYGWIN\nfrom ._compat import get_best_encoding\nfrom ._compat import int_types\nfrom ._compat import isatty\nfrom ._compat import open_stream\nfrom ._compat import range_type\nfrom ._compat import strip_ansi\nfrom ._compat import term_len\nfrom ._compat import WIN\nfrom .exceptions import ClickException\nfrom .utils import echo\n\nif os.name == \"nt\":\n    BEFORE_BAR = \"\\r\"\n    AFTER_BAR = \"\\n\"\nelse:\n    BEFORE_BAR = \"\\r\\033[?25l\"\n    AFTER_BAR = \"\\033[?25h\\n\"\n\n\ndef _length_hint(obj):\n    \"\"\"Returns the length hint of an object.\"\"\"\n    try:\n        return len(obj)\n    except (AttributeError, TypeError):\n        try:\n            get_hint = type(obj).__length_hint__\n        except AttributeError:\n            return None\n        try:\n            hint = get_hint(obj)\n        except TypeError:\n            return None\n        if hint is NotImplemented or not isinstance(hint, int_types) or hint < 0:\n            return None\n        return hint\n\n\nclass ProgressBar(object):\n    def __init__(\n        self,\n        iterable,\n        length=None,\n        fill_char=\"#\",\n        empty_char=\" \",\n        bar_template=\"%(bar)s\",\n        info_sep=\"  \",\n        show_eta=True,\n        show_percent=None,\n        show_pos=False,\n        item_show_func=None,\n        label=None,\n        file=None,\n        color=None,\n        width=30,\n    ):\n        self.fill_char = fill_char\n        self.empty_char = empty_char\n        self.bar_template = bar_template\n        self.info_sep = info_sep\n        self.show_eta = show_eta\n        self.show_percent = show_percent\n        self.show_pos = show_pos\n        self.item_show_func = item_show_func\n        self.label = label or \"\"\n        if file is None:\n            file = _default_text_stdout()\n        self.file = file\n        self.color = color\n        self.width = width\n        self.autowidth = width == 0\n\n        if length is None:\n            length = _length_hint(iterable)\n        if iterable is None:\n            if length is None:\n                raise TypeError(\"iterable or length is required\")\n            iterable = range_type(length)\n        self.iter = iter(iterable)\n        self.length = length\n        self.length_known = length is not None\n        self.pos = 0\n        self.avg = []\n        self.start = self.last_eta = time.time()\n        self.eta_known = False\n        self.finished = False\n        self.max_width = None\n        self.entered = False\n        self.current_item = None\n        self.is_hidden = not isatty(self.file)\n        self._last_line = None\n        self.short_limit = 0.5\n\n    def __enter__(self):\n        self.entered = True\n        self.render_progress()\n        return self\n\n    def __exit__(self, exc_type, exc_value, tb):\n        self.render_finish()\n\n    def __iter__(self):\n        if not self.entered:\n            raise RuntimeError(\"You need to use progress bars in a with block.\")\n        self.render_progress()\n        return self.generator()\n\n    def __next__(self):\n        # Iteration is defined in terms of a generator function,\n        # returned by iter(self); use that to define next(). This works\n        # because `self.iter` is an iterable consumed by that generator,\n        # so it is re-entry safe. Calling `next(self.generator())`\n        # twice works and does \"what you want\".\n        return next(iter(self))\n\n    # Python 2 compat\n    next = __next__\n\n    def is_fast(self):\n        return time.time() - self.start <= self.short_limit\n\n    def render_finish(self):\n        if self.is_hidden or self.is_fast():\n            return\n        self.file.write(AFTER_BAR)\n        self.file.flush()\n\n    @property\n    def pct(self):\n        if self.finished:\n            return 1.0\n        return min(self.pos / (float(self.length) or 1), 1.0)\n\n    @property\n    def time_per_iteration(self):\n        if not self.avg:\n            return 0.0\n        return sum(self.avg) / float(len(self.avg))\n\n    @property\n    def eta(self):\n        if self.length_known and not self.finished:\n            return self.time_per_iteration * (self.length - self.pos)\n        return 0.0\n\n    def format_eta(self):\n        if self.eta_known:\n            t = int(self.eta)\n            seconds = t % 60\n            t //= 60\n            minutes = t % 60\n            t //= 60\n            hours = t % 24\n            t //= 24\n            if t > 0:\n                return \"{}d {:02}:{:02}:{:02}\".format(t, hours, minutes, seconds)\n            else:\n                return \"{:02}:{:02}:{:02}\".format(hours, minutes, seconds)\n        return \"\"\n\n    def format_pos(self):\n        pos = str(self.pos)\n        if self.length_known:\n            pos += \"/{}\".format(self.length)\n        return pos\n\n    def format_pct(self):\n        return \"{: 4}%\".format(int(self.pct * 100))[1:]\n\n    def format_bar(self):\n        if self.length_known:\n            bar_length = int(self.pct * self.width)\n            bar = self.fill_char * bar_length\n            bar += self.empty_char * (self.width - bar_length)\n        elif self.finished:\n            bar = self.fill_char * self.width\n        else:\n            bar = list(self.empty_char * (self.width or 1))\n            if self.time_per_iteration != 0:\n                bar[\n                    int(\n                        (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)\n                        * self.width\n                    )\n                ] = self.fill_char\n            bar = \"\".join(bar)\n        return bar\n\n    def format_progress_line(self):\n        show_percent = self.show_percent\n\n        info_bits = []\n        if self.length_known and show_percent is None:\n            show_percent = not self.show_pos\n\n        if self.show_pos:\n            info_bits.append(self.format_pos())\n        if show_percent:\n            info_bits.append(self.format_pct())\n        if self.show_eta and self.eta_known and not self.finished:\n            info_bits.append(self.format_eta())\n        if self.item_show_func is not None:\n            item_info = self.item_show_func(self.current_item)\n            if item_info is not None:\n                info_bits.append(item_info)\n\n        return (\n            self.bar_template\n            % {\n                \"label\": self.label,\n                \"bar\": self.format_bar(),\n                \"info\": self.info_sep.join(info_bits),\n            }\n        ).rstrip()\n\n    def render_progress(self):\n        from .termui import get_terminal_size\n\n        if self.is_hidden:\n            return\n\n        buf = []\n        # Update width in case the terminal has been resized\n        if self.autowidth:\n            old_width = self.width\n            self.width = 0\n            clutter_length = term_len(self.format_progress_line())\n            new_width = max(0, get_terminal_size()[0] - clutter_length)\n            if new_width < old_width:\n                buf.append(BEFORE_BAR)\n                buf.append(\" \" * self.max_width)\n                self.max_width = new_width\n            self.width = new_width\n\n        clear_width = self.width\n        if self.max_width is not None:\n            clear_width = self.max_width\n\n        buf.append(BEFORE_BAR)\n        line = self.format_progress_line()\n        line_len = term_len(line)\n        if self.max_width is None or self.max_width < line_len:\n            self.max_width = line_len\n\n        buf.append(line)\n        buf.append(\" \" * (clear_width - line_len))\n        line = \"\".join(buf)\n        # Render the line only if it changed.\n\n        if line != self._last_line and not self.is_fast():\n            self._last_line = line\n            echo(line, file=self.file, color=self.color, nl=False)\n            self.file.flush()\n\n    def make_step(self, n_steps):\n        self.pos += n_steps\n        if self.length_known and self.pos >= self.length:\n            self.finished = True\n\n        if (time.time() - self.last_eta) < 1.0:\n            return\n\n        self.last_eta = time.time()\n\n        # self.avg is a rolling list of length <= 7 of steps where steps are\n        # defined as time elapsed divided by the total progress through\n        # self.length.\n        if self.pos:\n            step = (time.time() - self.start) / self.pos\n        else:\n            step = time.time() - self.start\n\n        self.avg = self.avg[-6:] + [step]\n\n        self.eta_known = self.length_known\n\n    def update(self, n_steps):\n        self.make_step(n_steps)\n        self.render_progress()\n\n    def finish(self):\n        self.eta_known = 0\n        self.current_item = None\n        self.finished = True\n\n    def generator(self):\n        \"\"\"Return a generator which yields the items added to the bar\n        during construction, and updates the progress bar *after* the\n        yielded block returns.\n        \"\"\"\n        # WARNING: the iterator interface for `ProgressBar` relies on\n        # this and only works because this is a simple generator which\n        # doesn't create or manage additional state. If this function\n        # changes, the impact should be evaluated both against\n        # `iter(bar)` and `next(bar)`. `next()` in particular may call\n        # `self.generator()` repeatedly, and this must remain safe in\n        # order for that interface to work.\n        if not self.entered:\n            raise RuntimeError(\"You need to use progress bars in a with block.\")\n\n        if self.is_hidden:\n            for rv in self.iter:\n                yield rv\n        else:\n            for rv in self.iter:\n                self.current_item = rv\n                yield rv\n                self.update(1)\n            self.finish()\n            self.render_progress()\n\n\ndef pager(generator, color=None):\n    \"\"\"Decide what method to use for paging through text.\"\"\"\n    stdout = _default_text_stdout()\n    if not isatty(sys.stdin) or not isatty(stdout):\n        return _nullpager(stdout, generator, color)\n    pager_cmd = (os.environ.get(\"PAGER\", None) or \"\").strip()\n    if pager_cmd:\n        if WIN:\n            return _tempfilepager(generator, pager_cmd, color)\n        return _pipepager(generator, pager_cmd, color)\n    if os.environ.get(\"TERM\") in (\"dumb\", \"emacs\"):\n        return _nullpager(stdout, generator, color)\n    if WIN or sys.platform.startswith(\"os2\"):\n        return _tempfilepager(generator, \"more <\", color)\n    if hasattr(os, \"system\") and os.system(\"(less) 2>/dev/null\") == 0:\n        return _pipepager(generator, \"less\", color)\n\n    import tempfile\n\n    fd, filename = tempfile.mkstemp()\n    os.close(fd)\n    try:\n        if hasattr(os, \"system\") and os.system('more \"{}\"'.format(filename)) == 0:\n            return _pipepager(generator, \"more\", color)\n        return _nullpager(stdout, generator, color)\n    finally:\n        os.unlink(filename)\n\n\ndef _pipepager(generator, cmd, color):\n    \"\"\"Page through text by feeding it to another program.  Invoking a\n    pager through this might support colors.\n    \"\"\"\n    import subprocess\n\n    env = dict(os.environ)\n\n    # If we're piping to less we might support colors under the\n    # condition that\n    cmd_detail = cmd.rsplit(\"/\", 1)[-1].split()\n    if color is None and cmd_detail[0] == \"less\":\n        less_flags = \"{}{}\".format(os.environ.get(\"LESS\", \"\"), \" \".join(cmd_detail[1:]))\n        if not less_flags:\n            env[\"LESS\"] = \"-R\"\n            color = True\n        elif \"r\" in less_flags or \"R\" in less_flags:\n            color = True\n\n    c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env)\n    encoding = get_best_encoding(c.stdin)\n    try:\n        for text in generator:\n            if not color:\n                text = strip_ansi(text)\n\n            c.stdin.write(text.encode(encoding, \"replace\"))\n    except (IOError, KeyboardInterrupt):\n        pass\n    else:\n        c.stdin.close()\n\n    # Less doesn't respect ^C, but catches it for its own UI purposes (aborting\n    # search or other commands inside less).\n    #\n    # That means when the user hits ^C, the parent process (click) terminates,\n    # but less is still alive, paging the output and messing up the terminal.\n    #\n    # If the user wants to make the pager exit on ^C, they should set\n    # `LESS='-K'`. It's not our decision to make.\n    while True:\n        try:\n            c.wait()\n        except KeyboardInterrupt:\n            pass\n        else:\n            break\n\n\ndef _tempfilepager(generator, cmd, color):\n    \"\"\"Page through text by invoking a program on a temporary file.\"\"\"\n    import tempfile\n\n    filename = tempfile.mktemp()\n    # TODO: This never terminates if the passed generator never terminates.\n    text = \"\".join(generator)\n    if not color:\n        text = strip_ansi(text)\n    encoding = get_best_encoding(sys.stdout)\n    with open_stream(filename, \"wb\")[0] as f:\n        f.write(text.encode(encoding))\n    try:\n        os.system('{} \"{}\"'.format(cmd, filename))\n    finally:\n        os.unlink(filename)\n\n\ndef _nullpager(stream, generator, color):\n    \"\"\"Simply print unformatted text.  This is the ultimate fallback.\"\"\"\n    for text in generator:\n        if not color:\n            text = strip_ansi(text)\n        stream.write(text)\n\n\nclass Editor(object):\n    def __init__(self, editor=None, env=None, require_save=True, extension=\".txt\"):\n        self.editor = editor\n        self.env = env\n        self.require_save = require_save\n        self.extension = extension\n\n    def get_editor(self):\n        if self.editor is not None:\n            return self.editor\n        for key in \"VISUAL\", \"EDITOR\":\n            rv = os.environ.get(key)\n            if rv:\n                return rv\n        if WIN:\n            return \"notepad\"\n        for editor in \"sensible-editor\", \"vim\", \"nano\":\n            if os.system(\"which {} >/dev/null 2>&1\".format(editor)) == 0:\n                return editor\n        return \"vi\"\n\n    def edit_file(self, filename):\n        import subprocess\n\n        editor = self.get_editor()\n        if self.env:\n            environ = os.environ.copy()\n            environ.update(self.env)\n        else:\n            environ = None\n        try:\n            c = subprocess.Popen(\n                '{} \"{}\"'.format(editor, filename), env=environ, shell=True,\n            )\n            exit_code = c.wait()\n            if exit_code != 0:\n                raise ClickException(\"{}: Editing failed!\".format(editor))\n        except OSError as e:\n            raise ClickException(\"{}: Editing failed: {}\".format(editor, e))\n\n    def edit(self, text):\n        import tempfile\n\n        text = text or \"\"\n        if text and not text.endswith(\"\\n\"):\n            text += \"\\n\"\n\n        fd, name = tempfile.mkstemp(prefix=\"editor-\", suffix=self.extension)\n        try:\n            if WIN:\n                encoding = \"utf-8-sig\"\n                text = text.replace(\"\\n\", \"\\r\\n\")\n            else:\n                encoding = \"utf-8\"\n            text = text.encode(encoding)\n\n            f = os.fdopen(fd, \"wb\")\n            f.write(text)\n            f.close()\n            timestamp = os.path.getmtime(name)\n\n            self.edit_file(name)\n\n            if self.require_save and os.path.getmtime(name) == timestamp:\n                return None\n\n            f = open(name, \"rb\")\n            try:\n                rv = f.read()\n            finally:\n                f.close()\n            return rv.decode(\"utf-8-sig\").replace(\"\\r\\n\", \"\\n\")\n        finally:\n            os.unlink(name)\n\n\ndef open_url(url, wait=False, locate=False):\n    import subprocess\n\n    def _unquote_file(url):\n        try:\n            import urllib\n        except ImportError:\n            import urllib\n        if url.startswith(\"file://\"):\n            url = urllib.unquote(url[7:])\n        return url\n\n    if sys.platform == \"darwin\":\n        args = [\"open\"]\n        if wait:\n            args.append(\"-W\")\n        if locate:\n            args.append(\"-R\")\n        args.append(_unquote_file(url))\n        null = open(\"/dev/null\", \"w\")\n        try:\n            return subprocess.Popen(args, stderr=null).wait()\n        finally:\n            null.close()\n    elif WIN:\n        if locate:\n            url = _unquote_file(url)\n            args = 'explorer /select,\"{}\"'.format(_unquote_file(url.replace('\"', \"\")))\n        else:\n            args = 'start {} \"\" \"{}\"'.format(\n                \"/WAIT\" if wait else \"\", url.replace('\"', \"\")\n            )\n        return os.system(args)\n    elif CYGWIN:\n        if locate:\n            url = _unquote_file(url)\n            args = 'cygstart \"{}\"'.format(os.path.dirname(url).replace('\"', \"\"))\n        else:\n            args = 'cygstart {} \"{}\"'.format(\"-w\" if wait else \"\", url.replace('\"', \"\"))\n        return os.system(args)\n\n    try:\n        if locate:\n            url = os.path.dirname(_unquote_file(url)) or \".\"\n        else:\n            url = _unquote_file(url)\n        c = subprocess.Popen([\"xdg-open\", url])\n        if wait:\n            return c.wait()\n        return 0\n    except OSError:\n        if url.startswith((\"http://\", \"https://\")) and not locate and not wait:\n            import webbrowser\n\n            webbrowser.open(url)\n            return 0\n        return 1\n\n\ndef _translate_ch_to_exc(ch):\n    if ch == u\"\\x03\":\n        raise KeyboardInterrupt()\n    if ch == u\"\\x04\" and not WIN:  # Unix-like, Ctrl+D\n        raise EOFError()\n    if ch == u\"\\x1a\" and WIN:  # Windows, Ctrl+Z\n        raise EOFError()\n\n\nif WIN:\n    import msvcrt\n\n    @contextlib.contextmanager\n    def raw_terminal():\n        yield\n\n    def getchar(echo):\n        # The function `getch` will return a bytes object corresponding to\n        # the pressed character. Since Windows 10 build 1803, it will also\n        # return \\x00 when called a second time after pressing a regular key.\n        #\n        # `getwch` does not share this probably-bugged behavior. Moreover, it\n        # returns a Unicode object by default, which is what we want.\n        #\n        # Either of these functions will return \\x00 or \\xe0 to indicate\n        # a special key, and you need to call the same function again to get\n        # the \"rest\" of the code. The fun part is that \\u00e0 is\n        # \"latin small letter a with grave\", so if you type that on a French\n        # keyboard, you _also_ get a \\xe0.\n        # E.g., consider the Up arrow. This returns \\xe0 and then \\x48. The\n        # resulting Unicode string reads as \"a with grave\" + \"capital H\".\n        # This is indistinguishable from when the user actually types\n        # \"a with grave\" and then \"capital H\".\n        #\n        # When \\xe0 is returned, we assume it's part of a special-key sequence\n        # and call `getwch` again, but that means that when the user types\n        # the \\u00e0 character, `getchar` doesn't return until a second\n        # character is typed.\n        # The alternative is returning immediately, but that would mess up\n        # cross-platform handling of arrow keys and others that start with\n        # \\xe0. Another option is using `getch`, but then we can't reliably\n        # read non-ASCII characters, because return values of `getch` are\n        # limited to the current 8-bit codepage.\n        #\n        # Anyway, Click doesn't claim to do this Right(tm), and using `getwch`\n        # is doing the right thing in more situations than with `getch`.\n        if echo:\n            func = msvcrt.getwche\n        else:\n            func = msvcrt.getwch\n\n        rv = func()\n        if rv in (u\"\\x00\", u\"\\xe0\"):\n            # \\x00 and \\xe0 are control characters that indicate special key,\n            # see above.\n            rv += func()\n        _translate_ch_to_exc(rv)\n        return rv\n\n\nelse:\n    import tty\n    import termios\n\n    @contextlib.contextmanager\n    def raw_terminal():\n        if not isatty(sys.stdin):\n            f = open(\"/dev/tty\")\n            fd = f.fileno()\n        else:\n            fd = sys.stdin.fileno()\n            f = None\n        try:\n            old_settings = termios.tcgetattr(fd)\n            try:\n                tty.setraw(fd)\n                yield fd\n            finally:\n                termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)\n                sys.stdout.flush()\n                if f is not None:\n                    f.close()\n        except termios.error:\n            pass\n\n    def getchar(echo):\n        with raw_terminal() as fd:\n            ch = os.read(fd, 32)\n            ch = ch.decode(get_best_encoding(sys.stdin), \"replace\")\n            if echo and isatty(sys.stdout):\n                sys.stdout.write(ch)\n            _translate_ch_to_exc(ch)\n            return ch\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/_textwrap.py",
    "content": "import textwrap\nfrom contextlib import contextmanager\n\n\nclass TextWrapper(textwrap.TextWrapper):\n    def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):\n        space_left = max(width - cur_len, 1)\n\n        if self.break_long_words:\n            last = reversed_chunks[-1]\n            cut = last[:space_left]\n            res = last[space_left:]\n            cur_line.append(cut)\n            reversed_chunks[-1] = res\n        elif not cur_line:\n            cur_line.append(reversed_chunks.pop())\n\n    @contextmanager\n    def extra_indent(self, indent):\n        old_initial_indent = self.initial_indent\n        old_subsequent_indent = self.subsequent_indent\n        self.initial_indent += indent\n        self.subsequent_indent += indent\n        try:\n            yield\n        finally:\n            self.initial_indent = old_initial_indent\n            self.subsequent_indent = old_subsequent_indent\n\n    def indent_only(self, text):\n        rv = []\n        for idx, line in enumerate(text.splitlines()):\n            indent = self.initial_indent\n            if idx > 0:\n                indent = self.subsequent_indent\n            rv.append(indent + line)\n        return \"\\n\".join(rv)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/_unicodefun.py",
    "content": "import codecs\nimport os\nimport sys\n\nfrom ._compat import PY2\n\n\ndef _find_unicode_literals_frame():\n    import __future__\n\n    if not hasattr(sys, \"_getframe\"):  # not all Python implementations have it\n        return 0\n    frm = sys._getframe(1)\n    idx = 1\n    while frm is not None:\n        if frm.f_globals.get(\"__name__\", \"\").startswith(\"click.\"):\n            frm = frm.f_back\n            idx += 1\n        elif frm.f_code.co_flags & __future__.unicode_literals.compiler_flag:\n            return idx\n        else:\n            break\n    return 0\n\n\ndef _check_for_unicode_literals():\n    if not __debug__:\n        return\n\n    from . import disable_unicode_literals_warning\n\n    if not PY2 or disable_unicode_literals_warning:\n        return\n    bad_frame = _find_unicode_literals_frame()\n    if bad_frame <= 0:\n        return\n    from warnings import warn\n\n    warn(\n        Warning(\n            \"Click detected the use of the unicode_literals __future__\"\n            \" import. This is heavily discouraged because it can\"\n            \" introduce subtle bugs in your code. You should instead\"\n            ' use explicit u\"\" literals for your unicode strings. For'\n            \" more information see\"\n            \" https://click.palletsprojects.com/python3/\"\n        ),\n        stacklevel=bad_frame,\n    )\n\n\ndef _verify_python3_env():\n    \"\"\"Ensures that the environment is good for unicode on Python 3.\"\"\"\n    if PY2:\n        return\n    try:\n        import locale\n\n        fs_enc = codecs.lookup(locale.getpreferredencoding()).name\n    except Exception:\n        fs_enc = \"ascii\"\n    if fs_enc != \"ascii\":\n        return\n\n    extra = \"\"\n    if os.name == \"posix\":\n        import subprocess\n\n        try:\n            rv = subprocess.Popen(\n                [\"locale\", \"-a\"], stdout=subprocess.PIPE, stderr=subprocess.PIPE\n            ).communicate()[0]\n        except OSError:\n            rv = b\"\"\n        good_locales = set()\n        has_c_utf8 = False\n\n        # Make sure we're operating on text here.\n        if isinstance(rv, bytes):\n            rv = rv.decode(\"ascii\", \"replace\")\n\n        for line in rv.splitlines():\n            locale = line.strip()\n            if locale.lower().endswith((\".utf-8\", \".utf8\")):\n                good_locales.add(locale)\n                if locale.lower() in (\"c.utf8\", \"c.utf-8\"):\n                    has_c_utf8 = True\n\n        extra += \"\\n\\n\"\n        if not good_locales:\n            extra += (\n                \"Additional information: on this system no suitable\"\n                \" UTF-8 locales were discovered. This most likely\"\n                \" requires resolving by reconfiguring the locale\"\n                \" system.\"\n            )\n        elif has_c_utf8:\n            extra += (\n                \"This system supports the C.UTF-8 locale which is\"\n                \" recommended. You might be able to resolve your issue\"\n                \" by exporting the following environment variables:\\n\\n\"\n                \"    export LC_ALL=C.UTF-8\\n\"\n                \"    export LANG=C.UTF-8\"\n            )\n        else:\n            extra += (\n                \"This system lists a couple of UTF-8 supporting locales\"\n                \" that you can pick from. The following suitable\"\n                \" locales were discovered: {}\".format(\", \".join(sorted(good_locales)))\n            )\n\n        bad_locale = None\n        for locale in os.environ.get(\"LC_ALL\"), os.environ.get(\"LANG\"):\n            if locale and locale.lower().endswith((\".utf-8\", \".utf8\")):\n                bad_locale = locale\n            if locale is not None:\n                break\n        if bad_locale is not None:\n            extra += (\n                \"\\n\\nClick discovered that you exported a UTF-8 locale\"\n                \" but the locale system could not pick up from it\"\n                \" because it does not exist. The exported locale is\"\n                \" '{}' but it is not supported\".format(bad_locale)\n            )\n\n    raise RuntimeError(\n        \"Click will abort further execution because Python 3 was\"\n        \" configured to use ASCII as encoding for the environment.\"\n        \" Consult https://click.palletsprojects.com/python3/ for\"\n        \" mitigation steps.{}\".format(extra)\n    )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/_winconsole.py",
    "content": "# -*- coding: utf-8 -*-\n# This module is based on the excellent work by Adam Bartoš who\n# provided a lot of what went into the implementation here in\n# the discussion to issue1602 in the Python bug tracker.\n#\n# There are some general differences in regards to how this works\n# compared to the original patches as we do not need to patch\n# the entire interpreter but just work in our little world of\n# echo and prmopt.\nimport ctypes\nimport io\nimport os\nimport sys\nimport time\nimport zlib\nfrom ctypes import byref\nfrom ctypes import c_char\nfrom ctypes import c_char_p\nfrom ctypes import c_int\nfrom ctypes import c_ssize_t\nfrom ctypes import c_ulong\nfrom ctypes import c_void_p\nfrom ctypes import POINTER\nfrom ctypes import py_object\nfrom ctypes import windll\nfrom ctypes import WinError\nfrom ctypes import WINFUNCTYPE\nfrom ctypes.wintypes import DWORD\nfrom ctypes.wintypes import HANDLE\nfrom ctypes.wintypes import LPCWSTR\nfrom ctypes.wintypes import LPWSTR\n\nimport msvcrt\n\nfrom ._compat import _NonClosingTextIOWrapper\nfrom ._compat import PY2\nfrom ._compat import text_type\n\ntry:\n    from ctypes import pythonapi\n\n    PyObject_GetBuffer = pythonapi.PyObject_GetBuffer\n    PyBuffer_Release = pythonapi.PyBuffer_Release\nexcept ImportError:\n    pythonapi = None\n\n\nc_ssize_p = POINTER(c_ssize_t)\n\nkernel32 = windll.kernel32\nGetStdHandle = kernel32.GetStdHandle\nReadConsoleW = kernel32.ReadConsoleW\nWriteConsoleW = kernel32.WriteConsoleW\nGetConsoleMode = kernel32.GetConsoleMode\nGetLastError = kernel32.GetLastError\nGetCommandLineW = WINFUNCTYPE(LPWSTR)((\"GetCommandLineW\", windll.kernel32))\nCommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(\n    (\"CommandLineToArgvW\", windll.shell32)\n)\nLocalFree = WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)(\n    (\"LocalFree\", windll.kernel32)\n)\n\n\nSTDIN_HANDLE = GetStdHandle(-10)\nSTDOUT_HANDLE = GetStdHandle(-11)\nSTDERR_HANDLE = GetStdHandle(-12)\n\n\nPyBUF_SIMPLE = 0\nPyBUF_WRITABLE = 1\n\nERROR_SUCCESS = 0\nERROR_NOT_ENOUGH_MEMORY = 8\nERROR_OPERATION_ABORTED = 995\n\nSTDIN_FILENO = 0\nSTDOUT_FILENO = 1\nSTDERR_FILENO = 2\n\nEOF = b\"\\x1a\"\nMAX_BYTES_WRITTEN = 32767\n\n\nclass Py_buffer(ctypes.Structure):\n    _fields_ = [\n        (\"buf\", c_void_p),\n        (\"obj\", py_object),\n        (\"len\", c_ssize_t),\n        (\"itemsize\", c_ssize_t),\n        (\"readonly\", c_int),\n        (\"ndim\", c_int),\n        (\"format\", c_char_p),\n        (\"shape\", c_ssize_p),\n        (\"strides\", c_ssize_p),\n        (\"suboffsets\", c_ssize_p),\n        (\"internal\", c_void_p),\n    ]\n\n    if PY2:\n        _fields_.insert(-1, (\"smalltable\", c_ssize_t * 2))\n\n\n# On PyPy we cannot get buffers so our ability to operate here is\n# serverly limited.\nif pythonapi is None:\n    get_buffer = None\nelse:\n\n    def get_buffer(obj, writable=False):\n        buf = Py_buffer()\n        flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE\n        PyObject_GetBuffer(py_object(obj), byref(buf), flags)\n        try:\n            buffer_type = c_char * buf.len\n            return buffer_type.from_address(buf.buf)\n        finally:\n            PyBuffer_Release(byref(buf))\n\n\nclass _WindowsConsoleRawIOBase(io.RawIOBase):\n    def __init__(self, handle):\n        self.handle = handle\n\n    def isatty(self):\n        io.RawIOBase.isatty(self)\n        return True\n\n\nclass _WindowsConsoleReader(_WindowsConsoleRawIOBase):\n    def readable(self):\n        return True\n\n    def readinto(self, b):\n        bytes_to_be_read = len(b)\n        if not bytes_to_be_read:\n            return 0\n        elif bytes_to_be_read % 2:\n            raise ValueError(\n                \"cannot read odd number of bytes from UTF-16-LE encoded console\"\n            )\n\n        buffer = get_buffer(b, writable=True)\n        code_units_to_be_read = bytes_to_be_read // 2\n        code_units_read = c_ulong()\n\n        rv = ReadConsoleW(\n            HANDLE(self.handle),\n            buffer,\n            code_units_to_be_read,\n            byref(code_units_read),\n            None,\n        )\n        if GetLastError() == ERROR_OPERATION_ABORTED:\n            # wait for KeyboardInterrupt\n            time.sleep(0.1)\n        if not rv:\n            raise OSError(\"Windows error: {}\".format(GetLastError()))\n\n        if buffer[0] == EOF:\n            return 0\n        return 2 * code_units_read.value\n\n\nclass _WindowsConsoleWriter(_WindowsConsoleRawIOBase):\n    def writable(self):\n        return True\n\n    @staticmethod\n    def _get_error_message(errno):\n        if errno == ERROR_SUCCESS:\n            return \"ERROR_SUCCESS\"\n        elif errno == ERROR_NOT_ENOUGH_MEMORY:\n            return \"ERROR_NOT_ENOUGH_MEMORY\"\n        return \"Windows error {}\".format(errno)\n\n    def write(self, b):\n        bytes_to_be_written = len(b)\n        buf = get_buffer(b)\n        code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2\n        code_units_written = c_ulong()\n\n        WriteConsoleW(\n            HANDLE(self.handle),\n            buf,\n            code_units_to_be_written,\n            byref(code_units_written),\n            None,\n        )\n        bytes_written = 2 * code_units_written.value\n\n        if bytes_written == 0 and bytes_to_be_written > 0:\n            raise OSError(self._get_error_message(GetLastError()))\n        return bytes_written\n\n\nclass ConsoleStream(object):\n    def __init__(self, text_stream, byte_stream):\n        self._text_stream = text_stream\n        self.buffer = byte_stream\n\n    @property\n    def name(self):\n        return self.buffer.name\n\n    def write(self, x):\n        if isinstance(x, text_type):\n            return self._text_stream.write(x)\n        try:\n            self.flush()\n        except Exception:\n            pass\n        return self.buffer.write(x)\n\n    def writelines(self, lines):\n        for line in lines:\n            self.write(line)\n\n    def __getattr__(self, name):\n        return getattr(self._text_stream, name)\n\n    def isatty(self):\n        return self.buffer.isatty()\n\n    def __repr__(self):\n        return \"<ConsoleStream name={!r} encoding={!r}>\".format(\n            self.name, self.encoding\n        )\n\n\nclass WindowsChunkedWriter(object):\n    \"\"\"\n    Wraps a stream (such as stdout), acting as a transparent proxy for all\n    attribute access apart from method 'write()' which we wrap to write in\n    limited chunks due to a Windows limitation on binary console streams.\n    \"\"\"\n\n    def __init__(self, wrapped):\n        # double-underscore everything to prevent clashes with names of\n        # attributes on the wrapped stream object.\n        self.__wrapped = wrapped\n\n    def __getattr__(self, name):\n        return getattr(self.__wrapped, name)\n\n    def write(self, text):\n        total_to_write = len(text)\n        written = 0\n\n        while written < total_to_write:\n            to_write = min(total_to_write - written, MAX_BYTES_WRITTEN)\n            self.__wrapped.write(text[written : written + to_write])\n            written += to_write\n\n\n_wrapped_std_streams = set()\n\n\ndef _wrap_std_stream(name):\n    # Python 2 & Windows 7 and below\n    if (\n        PY2\n        and sys.getwindowsversion()[:2] <= (6, 1)\n        and name not in _wrapped_std_streams\n    ):\n        setattr(sys, name, WindowsChunkedWriter(getattr(sys, name)))\n        _wrapped_std_streams.add(name)\n\n\ndef _get_text_stdin(buffer_stream):\n    text_stream = _NonClosingTextIOWrapper(\n        io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),\n        \"utf-16-le\",\n        \"strict\",\n        line_buffering=True,\n    )\n    return ConsoleStream(text_stream, buffer_stream)\n\n\ndef _get_text_stdout(buffer_stream):\n    text_stream = _NonClosingTextIOWrapper(\n        io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),\n        \"utf-16-le\",\n        \"strict\",\n        line_buffering=True,\n    )\n    return ConsoleStream(text_stream, buffer_stream)\n\n\ndef _get_text_stderr(buffer_stream):\n    text_stream = _NonClosingTextIOWrapper(\n        io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),\n        \"utf-16-le\",\n        \"strict\",\n        line_buffering=True,\n    )\n    return ConsoleStream(text_stream, buffer_stream)\n\n\nif PY2:\n\n    def _hash_py_argv():\n        return zlib.crc32(\"\\x00\".join(sys.argv[1:]))\n\n    _initial_argv_hash = _hash_py_argv()\n\n    def _get_windows_argv():\n        argc = c_int(0)\n        argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc))\n        if not argv_unicode:\n            raise WinError()\n        try:\n            argv = [argv_unicode[i] for i in range(0, argc.value)]\n        finally:\n            LocalFree(argv_unicode)\n            del argv_unicode\n\n        if not hasattr(sys, \"frozen\"):\n            argv = argv[1:]\n            while len(argv) > 0:\n                arg = argv[0]\n                if not arg.startswith(\"-\") or arg == \"-\":\n                    break\n                argv = argv[1:]\n                if arg.startswith((\"-c\", \"-m\")):\n                    break\n\n        return argv[1:]\n\n\n_stream_factories = {\n    0: _get_text_stdin,\n    1: _get_text_stdout,\n    2: _get_text_stderr,\n}\n\n\ndef _is_console(f):\n    if not hasattr(f, \"fileno\"):\n        return False\n\n    try:\n        fileno = f.fileno()\n    except OSError:\n        return False\n\n    handle = msvcrt.get_osfhandle(fileno)\n    return bool(GetConsoleMode(handle, byref(DWORD())))\n\n\ndef _get_windows_console_stream(f, encoding, errors):\n    if (\n        get_buffer is not None\n        and encoding in (\"utf-16-le\", None)\n        and errors in (\"strict\", None)\n        and _is_console(f)\n    ):\n        func = _stream_factories.get(f.fileno())\n        if func is not None:\n            if not PY2:\n                f = getattr(f, \"buffer\", None)\n                if f is None:\n                    return None\n            else:\n                # If we are on Python 2 we need to set the stream that we\n                # deal with to binary mode as otherwise the exercise if a\n                # bit moot.  The same problems apply as for\n                # get_binary_stdin and friends from _compat.\n                msvcrt.setmode(f.fileno(), os.O_BINARY)\n            return func(f)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/core.py",
    "content": "import errno\nimport inspect\nimport os\nimport sys\nfrom contextlib import contextmanager\nfrom functools import update_wrapper\nfrom itertools import repeat\n\nfrom ._compat import isidentifier\nfrom ._compat import iteritems\nfrom ._compat import PY2\nfrom ._compat import string_types\nfrom ._unicodefun import _check_for_unicode_literals\nfrom ._unicodefun import _verify_python3_env\nfrom .exceptions import Abort\nfrom .exceptions import BadParameter\nfrom .exceptions import ClickException\nfrom .exceptions import Exit\nfrom .exceptions import MissingParameter\nfrom .exceptions import UsageError\nfrom .formatting import HelpFormatter\nfrom .formatting import join_options\nfrom .globals import pop_context\nfrom .globals import push_context\nfrom .parser import OptionParser\nfrom .parser import split_opt\nfrom .termui import confirm\nfrom .termui import prompt\nfrom .termui import style\nfrom .types import BOOL\nfrom .types import convert_type\nfrom .types import IntRange\nfrom .utils import echo\nfrom .utils import get_os_args\nfrom .utils import make_default_short_help\nfrom .utils import make_str\nfrom .utils import PacifyFlushWrapper\n\n_missing = object()\n\nSUBCOMMAND_METAVAR = \"COMMAND [ARGS]...\"\nSUBCOMMANDS_METAVAR = \"COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...\"\n\nDEPRECATED_HELP_NOTICE = \" (DEPRECATED)\"\nDEPRECATED_INVOKE_NOTICE = \"DeprecationWarning: The command %(name)s is deprecated.\"\n\n\ndef _maybe_show_deprecated_notice(cmd):\n    if cmd.deprecated:\n        echo(style(DEPRECATED_INVOKE_NOTICE % {\"name\": cmd.name}, fg=\"red\"), err=True)\n\n\ndef fast_exit(code):\n    \"\"\"Exit without garbage collection, this speeds up exit by about 10ms for\n    things like bash completion.\n    \"\"\"\n    sys.stdout.flush()\n    sys.stderr.flush()\n    os._exit(code)\n\n\ndef _bashcomplete(cmd, prog_name, complete_var=None):\n    \"\"\"Internal handler for the bash completion support.\"\"\"\n    if complete_var is None:\n        complete_var = \"_{}_COMPLETE\".format(prog_name.replace(\"-\", \"_\").upper())\n    complete_instr = os.environ.get(complete_var)\n    if not complete_instr:\n        return\n\n    from ._bashcomplete import bashcomplete\n\n    if bashcomplete(cmd, prog_name, complete_var, complete_instr):\n        fast_exit(1)\n\n\ndef _check_multicommand(base_command, cmd_name, cmd, register=False):\n    if not base_command.chain or not isinstance(cmd, MultiCommand):\n        return\n    if register:\n        hint = (\n            \"It is not possible to add multi commands as children to\"\n            \" another multi command that is in chain mode.\"\n        )\n    else:\n        hint = (\n            \"Found a multi command as subcommand to a multi command\"\n            \" that is in chain mode. This is not supported.\"\n        )\n    raise RuntimeError(\n        \"{}. Command '{}' is set to chain and '{}' was added as\"\n        \" subcommand but it in itself is a multi command. ('{}' is a {}\"\n        \" within a chained {} named '{}').\".format(\n            hint,\n            base_command.name,\n            cmd_name,\n            cmd_name,\n            cmd.__class__.__name__,\n            base_command.__class__.__name__,\n            base_command.name,\n        )\n    )\n\n\ndef batch(iterable, batch_size):\n    return list(zip(*repeat(iter(iterable), batch_size)))\n\n\ndef invoke_param_callback(callback, ctx, param, value):\n    code = getattr(callback, \"__code__\", None)\n    args = getattr(code, \"co_argcount\", 3)\n\n    if args < 3:\n        from warnings import warn\n\n        warn(\n            \"Parameter callbacks take 3 args, (ctx, param, value). The\"\n            \" 2-arg style is deprecated and will be removed in 8.0.\".format(callback),\n            DeprecationWarning,\n            stacklevel=3,\n        )\n        return callback(ctx, value)\n\n    return callback(ctx, param, value)\n\n\n@contextmanager\ndef augment_usage_errors(ctx, param=None):\n    \"\"\"Context manager that attaches extra information to exceptions.\"\"\"\n    try:\n        yield\n    except BadParameter as e:\n        if e.ctx is None:\n            e.ctx = ctx\n        if param is not None and e.param is None:\n            e.param = param\n        raise\n    except UsageError as e:\n        if e.ctx is None:\n            e.ctx = ctx\n        raise\n\n\ndef iter_params_for_processing(invocation_order, declaration_order):\n    \"\"\"Given a sequence of parameters in the order as should be considered\n    for processing and an iterable of parameters that exist, this returns\n    a list in the correct order as they should be processed.\n    \"\"\"\n\n    def sort_key(item):\n        try:\n            idx = invocation_order.index(item)\n        except ValueError:\n            idx = float(\"inf\")\n        return (not item.is_eager, idx)\n\n    return sorted(declaration_order, key=sort_key)\n\n\nclass Context(object):\n    \"\"\"The context is a special internal object that holds state relevant\n    for the script execution at every single level.  It's normally invisible\n    to commands unless they opt-in to getting access to it.\n\n    The context is useful as it can pass internal objects around and can\n    control special execution features such as reading data from\n    environment variables.\n\n    A context can be used as context manager in which case it will call\n    :meth:`close` on teardown.\n\n    .. versionadded:: 2.0\n       Added the `resilient_parsing`, `help_option_names`,\n       `token_normalize_func` parameters.\n\n    .. versionadded:: 3.0\n       Added the `allow_extra_args` and `allow_interspersed_args`\n       parameters.\n\n    .. versionadded:: 4.0\n       Added the `color`, `ignore_unknown_options`, and\n       `max_content_width` parameters.\n\n    .. versionadded:: 7.1\n       Added the `show_default` parameter.\n\n    :param command: the command class for this context.\n    :param parent: the parent context.\n    :param info_name: the info name for this invocation.  Generally this\n                      is the most descriptive name for the script or\n                      command.  For the toplevel script it is usually\n                      the name of the script, for commands below it it's\n                      the name of the script.\n    :param obj: an arbitrary object of user data.\n    :param auto_envvar_prefix: the prefix to use for automatic environment\n                               variables.  If this is `None` then reading\n                               from environment variables is disabled.  This\n                               does not affect manually set environment\n                               variables which are always read.\n    :param default_map: a dictionary (like object) with default values\n                        for parameters.\n    :param terminal_width: the width of the terminal.  The default is\n                           inherit from parent context.  If no context\n                           defines the terminal width then auto\n                           detection will be applied.\n    :param max_content_width: the maximum width for content rendered by\n                              Click (this currently only affects help\n                              pages).  This defaults to 80 characters if\n                              not overridden.  In other words: even if the\n                              terminal is larger than that, Click will not\n                              format things wider than 80 characters by\n                              default.  In addition to that, formatters might\n                              add some safety mapping on the right.\n    :param resilient_parsing: if this flag is enabled then Click will\n                              parse without any interactivity or callback\n                              invocation.  Default values will also be\n                              ignored.  This is useful for implementing\n                              things such as completion support.\n    :param allow_extra_args: if this is set to `True` then extra arguments\n                             at the end will not raise an error and will be\n                             kept on the context.  The default is to inherit\n                             from the command.\n    :param allow_interspersed_args: if this is set to `False` then options\n                                    and arguments cannot be mixed.  The\n                                    default is to inherit from the command.\n    :param ignore_unknown_options: instructs click to ignore options it does\n                                   not know and keeps them for later\n                                   processing.\n    :param help_option_names: optionally a list of strings that define how\n                              the default help parameter is named.  The\n                              default is ``['--help']``.\n    :param token_normalize_func: an optional function that is used to\n                                 normalize tokens (options, choices,\n                                 etc.).  This for instance can be used to\n                                 implement case insensitive behavior.\n    :param color: controls if the terminal supports ANSI colors or not.  The\n                  default is autodetection.  This is only needed if ANSI\n                  codes are used in texts that Click prints which is by\n                  default not the case.  This for instance would affect\n                  help output.\n    :param show_default: if True, shows defaults for all options.\n                    Even if an option is later created with show_default=False,\n                    this command-level setting overrides it.\n    \"\"\"\n\n    def __init__(\n        self,\n        command,\n        parent=None,\n        info_name=None,\n        obj=None,\n        auto_envvar_prefix=None,\n        default_map=None,\n        terminal_width=None,\n        max_content_width=None,\n        resilient_parsing=False,\n        allow_extra_args=None,\n        allow_interspersed_args=None,\n        ignore_unknown_options=None,\n        help_option_names=None,\n        token_normalize_func=None,\n        color=None,\n        show_default=None,\n    ):\n        #: the parent context or `None` if none exists.\n        self.parent = parent\n        #: the :class:`Command` for this context.\n        self.command = command\n        #: the descriptive information name\n        self.info_name = info_name\n        #: the parsed parameters except if the value is hidden in which\n        #: case it's not remembered.\n        self.params = {}\n        #: the leftover arguments.\n        self.args = []\n        #: protected arguments.  These are arguments that are prepended\n        #: to `args` when certain parsing scenarios are encountered but\n        #: must be never propagated to another arguments.  This is used\n        #: to implement nested parsing.\n        self.protected_args = []\n        if obj is None and parent is not None:\n            obj = parent.obj\n        #: the user object stored.\n        self.obj = obj\n        self._meta = getattr(parent, \"meta\", {})\n\n        #: A dictionary (-like object) with defaults for parameters.\n        if (\n            default_map is None\n            and parent is not None\n            and parent.default_map is not None\n        ):\n            default_map = parent.default_map.get(info_name)\n        self.default_map = default_map\n\n        #: This flag indicates if a subcommand is going to be executed. A\n        #: group callback can use this information to figure out if it's\n        #: being executed directly or because the execution flow passes\n        #: onwards to a subcommand. By default it's None, but it can be\n        #: the name of the subcommand to execute.\n        #:\n        #: If chaining is enabled this will be set to ``'*'`` in case\n        #: any commands are executed.  It is however not possible to\n        #: figure out which ones.  If you require this knowledge you\n        #: should use a :func:`resultcallback`.\n        self.invoked_subcommand = None\n\n        if terminal_width is None and parent is not None:\n            terminal_width = parent.terminal_width\n        #: The width of the terminal (None is autodetection).\n        self.terminal_width = terminal_width\n\n        if max_content_width is None and parent is not None:\n            max_content_width = parent.max_content_width\n        #: The maximum width of formatted content (None implies a sensible\n        #: default which is 80 for most things).\n        self.max_content_width = max_content_width\n\n        if allow_extra_args is None:\n            allow_extra_args = command.allow_extra_args\n        #: Indicates if the context allows extra args or if it should\n        #: fail on parsing.\n        #:\n        #: .. versionadded:: 3.0\n        self.allow_extra_args = allow_extra_args\n\n        if allow_interspersed_args is None:\n            allow_interspersed_args = command.allow_interspersed_args\n        #: Indicates if the context allows mixing of arguments and\n        #: options or not.\n        #:\n        #: .. versionadded:: 3.0\n        self.allow_interspersed_args = allow_interspersed_args\n\n        if ignore_unknown_options is None:\n            ignore_unknown_options = command.ignore_unknown_options\n        #: Instructs click to ignore options that a command does not\n        #: understand and will store it on the context for later\n        #: processing.  This is primarily useful for situations where you\n        #: want to call into external programs.  Generally this pattern is\n        #: strongly discouraged because it's not possibly to losslessly\n        #: forward all arguments.\n        #:\n        #: .. versionadded:: 4.0\n        self.ignore_unknown_options = ignore_unknown_options\n\n        if help_option_names is None:\n            if parent is not None:\n                help_option_names = parent.help_option_names\n            else:\n                help_option_names = [\"--help\"]\n\n        #: The names for the help options.\n        self.help_option_names = help_option_names\n\n        if token_normalize_func is None and parent is not None:\n            token_normalize_func = parent.token_normalize_func\n\n        #: An optional normalization function for tokens.  This is\n        #: options, choices, commands etc.\n        self.token_normalize_func = token_normalize_func\n\n        #: Indicates if resilient parsing is enabled.  In that case Click\n        #: will do its best to not cause any failures and default values\n        #: will be ignored. Useful for completion.\n        self.resilient_parsing = resilient_parsing\n\n        # If there is no envvar prefix yet, but the parent has one and\n        # the command on this level has a name, we can expand the envvar\n        # prefix automatically.\n        if auto_envvar_prefix is None:\n            if (\n                parent is not None\n                and parent.auto_envvar_prefix is not None\n                and self.info_name is not None\n            ):\n                auto_envvar_prefix = \"{}_{}\".format(\n                    parent.auto_envvar_prefix, self.info_name.upper()\n                )\n        else:\n            auto_envvar_prefix = auto_envvar_prefix.upper()\n        if auto_envvar_prefix is not None:\n            auto_envvar_prefix = auto_envvar_prefix.replace(\"-\", \"_\")\n        self.auto_envvar_prefix = auto_envvar_prefix\n\n        if color is None and parent is not None:\n            color = parent.color\n\n        #: Controls if styling output is wanted or not.\n        self.color = color\n\n        self.show_default = show_default\n\n        self._close_callbacks = []\n        self._depth = 0\n\n    def __enter__(self):\n        self._depth += 1\n        push_context(self)\n        return self\n\n    def __exit__(self, exc_type, exc_value, tb):\n        self._depth -= 1\n        if self._depth == 0:\n            self.close()\n        pop_context()\n\n    @contextmanager\n    def scope(self, cleanup=True):\n        \"\"\"This helper method can be used with the context object to promote\n        it to the current thread local (see :func:`get_current_context`).\n        The default behavior of this is to invoke the cleanup functions which\n        can be disabled by setting `cleanup` to `False`.  The cleanup\n        functions are typically used for things such as closing file handles.\n\n        If the cleanup is intended the context object can also be directly\n        used as a context manager.\n\n        Example usage::\n\n            with ctx.scope():\n                assert get_current_context() is ctx\n\n        This is equivalent::\n\n            with ctx:\n                assert get_current_context() is ctx\n\n        .. versionadded:: 5.0\n\n        :param cleanup: controls if the cleanup functions should be run or\n                        not.  The default is to run these functions.  In\n                        some situations the context only wants to be\n                        temporarily pushed in which case this can be disabled.\n                        Nested pushes automatically defer the cleanup.\n        \"\"\"\n        if not cleanup:\n            self._depth += 1\n        try:\n            with self as rv:\n                yield rv\n        finally:\n            if not cleanup:\n                self._depth -= 1\n\n    @property\n    def meta(self):\n        \"\"\"This is a dictionary which is shared with all the contexts\n        that are nested.  It exists so that click utilities can store some\n        state here if they need to.  It is however the responsibility of\n        that code to manage this dictionary well.\n\n        The keys are supposed to be unique dotted strings.  For instance\n        module paths are a good choice for it.  What is stored in there is\n        irrelevant for the operation of click.  However what is important is\n        that code that places data here adheres to the general semantics of\n        the system.\n\n        Example usage::\n\n            LANG_KEY = f'{__name__}.lang'\n\n            def set_language(value):\n                ctx = get_current_context()\n                ctx.meta[LANG_KEY] = value\n\n            def get_language():\n                return get_current_context().meta.get(LANG_KEY, 'en_US')\n\n        .. versionadded:: 5.0\n        \"\"\"\n        return self._meta\n\n    def make_formatter(self):\n        \"\"\"Creates the formatter for the help and usage output.\"\"\"\n        return HelpFormatter(\n            width=self.terminal_width, max_width=self.max_content_width\n        )\n\n    def call_on_close(self, f):\n        \"\"\"This decorator remembers a function as callback that should be\n        executed when the context tears down.  This is most useful to bind\n        resource handling to the script execution.  For instance, file objects\n        opened by the :class:`File` type will register their close callbacks\n        here.\n\n        :param f: the function to execute on teardown.\n        \"\"\"\n        self._close_callbacks.append(f)\n        return f\n\n    def close(self):\n        \"\"\"Invokes all close callbacks.\"\"\"\n        for cb in self._close_callbacks:\n            cb()\n        self._close_callbacks = []\n\n    @property\n    def command_path(self):\n        \"\"\"The computed command path.  This is used for the ``usage``\n        information on the help page.  It's automatically created by\n        combining the info names of the chain of contexts to the root.\n        \"\"\"\n        rv = \"\"\n        if self.info_name is not None:\n            rv = self.info_name\n        if self.parent is not None:\n            rv = \"{} {}\".format(self.parent.command_path, rv)\n        return rv.lstrip()\n\n    def find_root(self):\n        \"\"\"Finds the outermost context.\"\"\"\n        node = self\n        while node.parent is not None:\n            node = node.parent\n        return node\n\n    def find_object(self, object_type):\n        \"\"\"Finds the closest object of a given type.\"\"\"\n        node = self\n        while node is not None:\n            if isinstance(node.obj, object_type):\n                return node.obj\n            node = node.parent\n\n    def ensure_object(self, object_type):\n        \"\"\"Like :meth:`find_object` but sets the innermost object to a\n        new instance of `object_type` if it does not exist.\n        \"\"\"\n        rv = self.find_object(object_type)\n        if rv is None:\n            self.obj = rv = object_type()\n        return rv\n\n    def lookup_default(self, name):\n        \"\"\"Looks up the default for a parameter name.  This by default\n        looks into the :attr:`default_map` if available.\n        \"\"\"\n        if self.default_map is not None:\n            rv = self.default_map.get(name)\n            if callable(rv):\n                rv = rv()\n            return rv\n\n    def fail(self, message):\n        \"\"\"Aborts the execution of the program with a specific error\n        message.\n\n        :param message: the error message to fail with.\n        \"\"\"\n        raise UsageError(message, self)\n\n    def abort(self):\n        \"\"\"Aborts the script.\"\"\"\n        raise Abort()\n\n    def exit(self, code=0):\n        \"\"\"Exits the application with a given exit code.\"\"\"\n        raise Exit(code)\n\n    def get_usage(self):\n        \"\"\"Helper method to get formatted usage string for the current\n        context and command.\n        \"\"\"\n        return self.command.get_usage(self)\n\n    def get_help(self):\n        \"\"\"Helper method to get formatted help page for the current\n        context and command.\n        \"\"\"\n        return self.command.get_help(self)\n\n    def invoke(*args, **kwargs):  # noqa: B902\n        \"\"\"Invokes a command callback in exactly the way it expects.  There\n        are two ways to invoke this method:\n\n        1.  the first argument can be a callback and all other arguments and\n            keyword arguments are forwarded directly to the function.\n        2.  the first argument is a click command object.  In that case all\n            arguments are forwarded as well but proper click parameters\n            (options and click arguments) must be keyword arguments and Click\n            will fill in defaults.\n\n        Note that before Click 3.2 keyword arguments were not properly filled\n        in against the intention of this code and no context was created.  For\n        more information about this change and why it was done in a bugfix\n        release see :ref:`upgrade-to-3.2`.\n        \"\"\"\n        self, callback = args[:2]\n        ctx = self\n\n        # It's also possible to invoke another command which might or\n        # might not have a callback.  In that case we also fill\n        # in defaults and make a new context for this command.\n        if isinstance(callback, Command):\n            other_cmd = callback\n            callback = other_cmd.callback\n            ctx = Context(other_cmd, info_name=other_cmd.name, parent=self)\n            if callback is None:\n                raise TypeError(\n                    \"The given command does not have a callback that can be invoked.\"\n                )\n\n            for param in other_cmd.params:\n                if param.name not in kwargs and param.expose_value:\n                    kwargs[param.name] = param.get_default(ctx)\n\n        args = args[2:]\n        with augment_usage_errors(self):\n            with ctx:\n                return callback(*args, **kwargs)\n\n    def forward(*args, **kwargs):  # noqa: B902\n        \"\"\"Similar to :meth:`invoke` but fills in default keyword\n        arguments from the current context if the other command expects\n        it.  This cannot invoke callbacks directly, only other commands.\n        \"\"\"\n        self, cmd = args[:2]\n\n        # It's also possible to invoke another command which might or\n        # might not have a callback.\n        if not isinstance(cmd, Command):\n            raise TypeError(\"Callback is not a command.\")\n\n        for param in self.params:\n            if param not in kwargs:\n                kwargs[param] = self.params[param]\n\n        return self.invoke(cmd, **kwargs)\n\n\nclass BaseCommand(object):\n    \"\"\"The base command implements the minimal API contract of commands.\n    Most code will never use this as it does not implement a lot of useful\n    functionality but it can act as the direct subclass of alternative\n    parsing methods that do not depend on the Click parser.\n\n    For instance, this can be used to bridge Click and other systems like\n    argparse or docopt.\n\n    Because base commands do not implement a lot of the API that other\n    parts of Click take for granted, they are not supported for all\n    operations.  For instance, they cannot be used with the decorators\n    usually and they have no built-in callback system.\n\n    .. versionchanged:: 2.0\n       Added the `context_settings` parameter.\n\n    :param name: the name of the command to use unless a group overrides it.\n    :param context_settings: an optional dictionary with defaults that are\n                             passed to the context object.\n    \"\"\"\n\n    #: the default for the :attr:`Context.allow_extra_args` flag.\n    allow_extra_args = False\n    #: the default for the :attr:`Context.allow_interspersed_args` flag.\n    allow_interspersed_args = True\n    #: the default for the :attr:`Context.ignore_unknown_options` flag.\n    ignore_unknown_options = False\n\n    def __init__(self, name, context_settings=None):\n        #: the name the command thinks it has.  Upon registering a command\n        #: on a :class:`Group` the group will default the command name\n        #: with this information.  You should instead use the\n        #: :class:`Context`\\'s :attr:`~Context.info_name` attribute.\n        self.name = name\n        if context_settings is None:\n            context_settings = {}\n        #: an optional dictionary with defaults passed to the context.\n        self.context_settings = context_settings\n\n    def __repr__(self):\n        return \"<{} {}>\".format(self.__class__.__name__, self.name)\n\n    def get_usage(self, ctx):\n        raise NotImplementedError(\"Base commands cannot get usage\")\n\n    def get_help(self, ctx):\n        raise NotImplementedError(\"Base commands cannot get help\")\n\n    def make_context(self, info_name, args, parent=None, **extra):\n        \"\"\"This function when given an info name and arguments will kick\n        off the parsing and create a new :class:`Context`.  It does not\n        invoke the actual command callback though.\n\n        :param info_name: the info name for this invokation.  Generally this\n                          is the most descriptive name for the script or\n                          command.  For the toplevel script it's usually\n                          the name of the script, for commands below it it's\n                          the name of the script.\n        :param args: the arguments to parse as list of strings.\n        :param parent: the parent context if available.\n        :param extra: extra keyword arguments forwarded to the context\n                      constructor.\n        \"\"\"\n        for key, value in iteritems(self.context_settings):\n            if key not in extra:\n                extra[key] = value\n        ctx = Context(self, info_name=info_name, parent=parent, **extra)\n        with ctx.scope(cleanup=False):\n            self.parse_args(ctx, args)\n        return ctx\n\n    def parse_args(self, ctx, args):\n        \"\"\"Given a context and a list of arguments this creates the parser\n        and parses the arguments, then modifies the context as necessary.\n        This is automatically invoked by :meth:`make_context`.\n        \"\"\"\n        raise NotImplementedError(\"Base commands do not know how to parse arguments.\")\n\n    def invoke(self, ctx):\n        \"\"\"Given a context, this invokes the command.  The default\n        implementation is raising a not implemented error.\n        \"\"\"\n        raise NotImplementedError(\"Base commands are not invokable by default\")\n\n    def main(\n        self,\n        args=None,\n        prog_name=None,\n        complete_var=None,\n        standalone_mode=True,\n        **extra\n    ):\n        \"\"\"This is the way to invoke a script with all the bells and\n        whistles as a command line application.  This will always terminate\n        the application after a call.  If this is not wanted, ``SystemExit``\n        needs to be caught.\n\n        This method is also available by directly calling the instance of\n        a :class:`Command`.\n\n        .. versionadded:: 3.0\n           Added the `standalone_mode` flag to control the standalone mode.\n\n        :param args: the arguments that should be used for parsing.  If not\n                     provided, ``sys.argv[1:]`` is used.\n        :param prog_name: the program name that should be used.  By default\n                          the program name is constructed by taking the file\n                          name from ``sys.argv[0]``.\n        :param complete_var: the environment variable that controls the\n                             bash completion support.  The default is\n                             ``\"_<prog_name>_COMPLETE\"`` with prog_name in\n                             uppercase.\n        :param standalone_mode: the default behavior is to invoke the script\n                                in standalone mode.  Click will then\n                                handle exceptions and convert them into\n                                error messages and the function will never\n                                return but shut down the interpreter.  If\n                                this is set to `False` they will be\n                                propagated to the caller and the return\n                                value of this function is the return value\n                                of :meth:`invoke`.\n        :param extra: extra keyword arguments are forwarded to the context\n                      constructor.  See :class:`Context` for more information.\n        \"\"\"\n        # If we are in Python 3, we will verify that the environment is\n        # sane at this point or reject further execution to avoid a\n        # broken script.\n        if not PY2:\n            _verify_python3_env()\n        else:\n            _check_for_unicode_literals()\n\n        if args is None:\n            args = get_os_args()\n        else:\n            args = list(args)\n\n        if prog_name is None:\n            prog_name = make_str(\n                os.path.basename(sys.argv[0] if sys.argv else __file__)\n            )\n\n        # Hook for the Bash completion.  This only activates if the Bash\n        # completion is actually enabled, otherwise this is quite a fast\n        # noop.\n        _bashcomplete(self, prog_name, complete_var)\n\n        try:\n            try:\n                with self.make_context(prog_name, args, **extra) as ctx:\n                    rv = self.invoke(ctx)\n                    if not standalone_mode:\n                        return rv\n                    # it's not safe to `ctx.exit(rv)` here!\n                    # note that `rv` may actually contain data like \"1\" which\n                    # has obvious effects\n                    # more subtle case: `rv=[None, None]` can come out of\n                    # chained commands which all returned `None` -- so it's not\n                    # even always obvious that `rv` indicates success/failure\n                    # by its truthiness/falsiness\n                    ctx.exit()\n            except (EOFError, KeyboardInterrupt):\n                echo(file=sys.stderr)\n                raise Abort()\n            except ClickException as e:\n                if not standalone_mode:\n                    raise\n                e.show()\n                sys.exit(e.exit_code)\n            except IOError as e:\n                if e.errno == errno.EPIPE:\n                    sys.stdout = PacifyFlushWrapper(sys.stdout)\n                    sys.stderr = PacifyFlushWrapper(sys.stderr)\n                    sys.exit(1)\n                else:\n                    raise\n        except Exit as e:\n            if standalone_mode:\n                sys.exit(e.exit_code)\n            else:\n                # in non-standalone mode, return the exit code\n                # note that this is only reached if `self.invoke` above raises\n                # an Exit explicitly -- thus bypassing the check there which\n                # would return its result\n                # the results of non-standalone execution may therefore be\n                # somewhat ambiguous: if there are codepaths which lead to\n                # `ctx.exit(1)` and to `return 1`, the caller won't be able to\n                # tell the difference between the two\n                return e.exit_code\n        except Abort:\n            if not standalone_mode:\n                raise\n            echo(\"Aborted!\", file=sys.stderr)\n            sys.exit(1)\n\n    def __call__(self, *args, **kwargs):\n        \"\"\"Alias for :meth:`main`.\"\"\"\n        return self.main(*args, **kwargs)\n\n\nclass Command(BaseCommand):\n    \"\"\"Commands are the basic building block of command line interfaces in\n    Click.  A basic command handles command line parsing and might dispatch\n    more parsing to commands nested below it.\n\n    .. versionchanged:: 2.0\n       Added the `context_settings` parameter.\n    .. versionchanged:: 7.1\n       Added the `no_args_is_help` parameter.\n\n    :param name: the name of the command to use unless a group overrides it.\n    :param context_settings: an optional dictionary with defaults that are\n                             passed to the context object.\n    :param callback: the callback to invoke.  This is optional.\n    :param params: the parameters to register with this command.  This can\n                   be either :class:`Option` or :class:`Argument` objects.\n    :param help: the help string to use for this command.\n    :param epilog: like the help string but it's printed at the end of the\n                   help page after everything else.\n    :param short_help: the short help to use for this command.  This is\n                       shown on the command listing of the parent command.\n    :param add_help_option: by default each command registers a ``--help``\n                            option.  This can be disabled by this parameter.\n    :param no_args_is_help: this controls what happens if no arguments are\n                            provided.  This option is disabled by default.\n                            If enabled this will add ``--help`` as argument\n                            if no arguments are passed\n    :param hidden: hide this command from help outputs.\n\n    :param deprecated: issues a message indicating that\n                             the command is deprecated.\n    \"\"\"\n\n    def __init__(\n        self,\n        name,\n        context_settings=None,\n        callback=None,\n        params=None,\n        help=None,\n        epilog=None,\n        short_help=None,\n        options_metavar=\"[OPTIONS]\",\n        add_help_option=True,\n        no_args_is_help=False,\n        hidden=False,\n        deprecated=False,\n    ):\n        BaseCommand.__init__(self, name, context_settings)\n        #: the callback to execute when the command fires.  This might be\n        #: `None` in which case nothing happens.\n        self.callback = callback\n        #: the list of parameters for this command in the order they\n        #: should show up in the help page and execute.  Eager parameters\n        #: will automatically be handled before non eager ones.\n        self.params = params or []\n        # if a form feed (page break) is found in the help text, truncate help\n        # text to the content preceding the first form feed\n        if help and \"\\f\" in help:\n            help = help.split(\"\\f\", 1)[0]\n        self.help = help\n        self.epilog = epilog\n        self.options_metavar = options_metavar\n        self.short_help = short_help\n        self.add_help_option = add_help_option\n        self.no_args_is_help = no_args_is_help\n        self.hidden = hidden\n        self.deprecated = deprecated\n\n    def get_usage(self, ctx):\n        \"\"\"Formats the usage line into a string and returns it.\n\n        Calls :meth:`format_usage` internally.\n        \"\"\"\n        formatter = ctx.make_formatter()\n        self.format_usage(ctx, formatter)\n        return formatter.getvalue().rstrip(\"\\n\")\n\n    def get_params(self, ctx):\n        rv = self.params\n        help_option = self.get_help_option(ctx)\n        if help_option is not None:\n            rv = rv + [help_option]\n        return rv\n\n    def format_usage(self, ctx, formatter):\n        \"\"\"Writes the usage line into the formatter.\n\n        This is a low-level method called by :meth:`get_usage`.\n        \"\"\"\n        pieces = self.collect_usage_pieces(ctx)\n        formatter.write_usage(ctx.command_path, \" \".join(pieces))\n\n    def collect_usage_pieces(self, ctx):\n        \"\"\"Returns all the pieces that go into the usage line and returns\n        it as a list of strings.\n        \"\"\"\n        rv = [self.options_metavar]\n        for param in self.get_params(ctx):\n            rv.extend(param.get_usage_pieces(ctx))\n        return rv\n\n    def get_help_option_names(self, ctx):\n        \"\"\"Returns the names for the help option.\"\"\"\n        all_names = set(ctx.help_option_names)\n        for param in self.params:\n            all_names.difference_update(param.opts)\n            all_names.difference_update(param.secondary_opts)\n        return all_names\n\n    def get_help_option(self, ctx):\n        \"\"\"Returns the help option object.\"\"\"\n        help_options = self.get_help_option_names(ctx)\n        if not help_options or not self.add_help_option:\n            return\n\n        def show_help(ctx, param, value):\n            if value and not ctx.resilient_parsing:\n                echo(ctx.get_help(), color=ctx.color)\n                ctx.exit()\n\n        return Option(\n            help_options,\n            is_flag=True,\n            is_eager=True,\n            expose_value=False,\n            callback=show_help,\n            help=\"Show this message and exit.\",\n        )\n\n    def make_parser(self, ctx):\n        \"\"\"Creates the underlying option parser for this command.\"\"\"\n        parser = OptionParser(ctx)\n        for param in self.get_params(ctx):\n            param.add_to_parser(parser, ctx)\n        return parser\n\n    def get_help(self, ctx):\n        \"\"\"Formats the help into a string and returns it.\n\n        Calls :meth:`format_help` internally.\n        \"\"\"\n        formatter = ctx.make_formatter()\n        self.format_help(ctx, formatter)\n        return formatter.getvalue().rstrip(\"\\n\")\n\n    def get_short_help_str(self, limit=45):\n        \"\"\"Gets short help for the command or makes it by shortening the\n        long help string.\n        \"\"\"\n        return (\n            self.short_help\n            or self.help\n            and make_default_short_help(self.help, limit)\n            or \"\"\n        )\n\n    def format_help(self, ctx, formatter):\n        \"\"\"Writes the help into the formatter if it exists.\n\n        This is a low-level method called by :meth:`get_help`.\n\n        This calls the following methods:\n\n        -   :meth:`format_usage`\n        -   :meth:`format_help_text`\n        -   :meth:`format_options`\n        -   :meth:`format_epilog`\n        \"\"\"\n        self.format_usage(ctx, formatter)\n        self.format_help_text(ctx, formatter)\n        self.format_options(ctx, formatter)\n        self.format_epilog(ctx, formatter)\n\n    def format_help_text(self, ctx, formatter):\n        \"\"\"Writes the help text to the formatter if it exists.\"\"\"\n        if self.help:\n            formatter.write_paragraph()\n            with formatter.indentation():\n                help_text = self.help\n                if self.deprecated:\n                    help_text += DEPRECATED_HELP_NOTICE\n                formatter.write_text(help_text)\n        elif self.deprecated:\n            formatter.write_paragraph()\n            with formatter.indentation():\n                formatter.write_text(DEPRECATED_HELP_NOTICE)\n\n    def format_options(self, ctx, formatter):\n        \"\"\"Writes all the options into the formatter if they exist.\"\"\"\n        opts = []\n        for param in self.get_params(ctx):\n            rv = param.get_help_record(ctx)\n            if rv is not None:\n                opts.append(rv)\n\n        if opts:\n            with formatter.section(\"Options\"):\n                formatter.write_dl(opts)\n\n    def format_epilog(self, ctx, formatter):\n        \"\"\"Writes the epilog into the formatter if it exists.\"\"\"\n        if self.epilog:\n            formatter.write_paragraph()\n            with formatter.indentation():\n                formatter.write_text(self.epilog)\n\n    def parse_args(self, ctx, args):\n        if not args and self.no_args_is_help and not ctx.resilient_parsing:\n            echo(ctx.get_help(), color=ctx.color)\n            ctx.exit()\n\n        parser = self.make_parser(ctx)\n        opts, args, param_order = parser.parse_args(args=args)\n\n        for param in iter_params_for_processing(param_order, self.get_params(ctx)):\n            value, args = param.handle_parse_result(ctx, opts, args)\n\n        if args and not ctx.allow_extra_args and not ctx.resilient_parsing:\n            ctx.fail(\n                \"Got unexpected extra argument{} ({})\".format(\n                    \"s\" if len(args) != 1 else \"\", \" \".join(map(make_str, args))\n                )\n            )\n\n        ctx.args = args\n        return args\n\n    def invoke(self, ctx):\n        \"\"\"Given a context, this invokes the attached callback (if it exists)\n        in the right way.\n        \"\"\"\n        _maybe_show_deprecated_notice(self)\n        if self.callback is not None:\n            return ctx.invoke(self.callback, **ctx.params)\n\n\nclass MultiCommand(Command):\n    \"\"\"A multi command is the basic implementation of a command that\n    dispatches to subcommands.  The most common version is the\n    :class:`Group`.\n\n    :param invoke_without_command: this controls how the multi command itself\n                                   is invoked.  By default it's only invoked\n                                   if a subcommand is provided.\n    :param no_args_is_help: this controls what happens if no arguments are\n                            provided.  This option is enabled by default if\n                            `invoke_without_command` is disabled or disabled\n                            if it's enabled.  If enabled this will add\n                            ``--help`` as argument if no arguments are\n                            passed.\n    :param subcommand_metavar: the string that is used in the documentation\n                               to indicate the subcommand place.\n    :param chain: if this is set to `True` chaining of multiple subcommands\n                  is enabled.  This restricts the form of commands in that\n                  they cannot have optional arguments but it allows\n                  multiple commands to be chained together.\n    :param result_callback: the result callback to attach to this multi\n                            command.\n    \"\"\"\n\n    allow_extra_args = True\n    allow_interspersed_args = False\n\n    def __init__(\n        self,\n        name=None,\n        invoke_without_command=False,\n        no_args_is_help=None,\n        subcommand_metavar=None,\n        chain=False,\n        result_callback=None,\n        **attrs\n    ):\n        Command.__init__(self, name, **attrs)\n        if no_args_is_help is None:\n            no_args_is_help = not invoke_without_command\n        self.no_args_is_help = no_args_is_help\n        self.invoke_without_command = invoke_without_command\n        if subcommand_metavar is None:\n            if chain:\n                subcommand_metavar = SUBCOMMANDS_METAVAR\n            else:\n                subcommand_metavar = SUBCOMMAND_METAVAR\n        self.subcommand_metavar = subcommand_metavar\n        self.chain = chain\n        #: The result callback that is stored.  This can be set or\n        #: overridden with the :func:`resultcallback` decorator.\n        self.result_callback = result_callback\n\n        if self.chain:\n            for param in self.params:\n                if isinstance(param, Argument) and not param.required:\n                    raise RuntimeError(\n                        \"Multi commands in chain mode cannot have\"\n                        \" optional arguments.\"\n                    )\n\n    def collect_usage_pieces(self, ctx):\n        rv = Command.collect_usage_pieces(self, ctx)\n        rv.append(self.subcommand_metavar)\n        return rv\n\n    def format_options(self, ctx, formatter):\n        Command.format_options(self, ctx, formatter)\n        self.format_commands(ctx, formatter)\n\n    def resultcallback(self, replace=False):\n        \"\"\"Adds a result callback to the chain command.  By default if a\n        result callback is already registered this will chain them but\n        this can be disabled with the `replace` parameter.  The result\n        callback is invoked with the return value of the subcommand\n        (or the list of return values from all subcommands if chaining\n        is enabled) as well as the parameters as they would be passed\n        to the main callback.\n\n        Example::\n\n            @click.group()\n            @click.option('-i', '--input', default=23)\n            def cli(input):\n                return 42\n\n            @cli.resultcallback()\n            def process_result(result, input):\n                return result + input\n\n        .. versionadded:: 3.0\n\n        :param replace: if set to `True` an already existing result\n                        callback will be removed.\n        \"\"\"\n\n        def decorator(f):\n            old_callback = self.result_callback\n            if old_callback is None or replace:\n                self.result_callback = f\n                return f\n\n            def function(__value, *args, **kwargs):\n                return f(old_callback(__value, *args, **kwargs), *args, **kwargs)\n\n            self.result_callback = rv = update_wrapper(function, f)\n            return rv\n\n        return decorator\n\n    def format_commands(self, ctx, formatter):\n        \"\"\"Extra format methods for multi methods that adds all the commands\n        after the options.\n        \"\"\"\n        commands = []\n        for subcommand in self.list_commands(ctx):\n            cmd = self.get_command(ctx, subcommand)\n            # What is this, the tool lied about a command.  Ignore it\n            if cmd is None:\n                continue\n            if cmd.hidden:\n                continue\n\n            commands.append((subcommand, cmd))\n\n        # allow for 3 times the default spacing\n        if len(commands):\n            limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands)\n\n            rows = []\n            for subcommand, cmd in commands:\n                help = cmd.get_short_help_str(limit)\n                rows.append((subcommand, help))\n\n            if rows:\n                with formatter.section(\"Commands\"):\n                    formatter.write_dl(rows)\n\n    def parse_args(self, ctx, args):\n        if not args and self.no_args_is_help and not ctx.resilient_parsing:\n            echo(ctx.get_help(), color=ctx.color)\n            ctx.exit()\n\n        rest = Command.parse_args(self, ctx, args)\n        if self.chain:\n            ctx.protected_args = rest\n            ctx.args = []\n        elif rest:\n            ctx.protected_args, ctx.args = rest[:1], rest[1:]\n\n        return ctx.args\n\n    def invoke(self, ctx):\n        def _process_result(value):\n            if self.result_callback is not None:\n                value = ctx.invoke(self.result_callback, value, **ctx.params)\n            return value\n\n        if not ctx.protected_args:\n            # If we are invoked without command the chain flag controls\n            # how this happens.  If we are not in chain mode, the return\n            # value here is the return value of the command.\n            # If however we are in chain mode, the return value is the\n            # return value of the result processor invoked with an empty\n            # list (which means that no subcommand actually was executed).\n            if self.invoke_without_command:\n                if not self.chain:\n                    return Command.invoke(self, ctx)\n                with ctx:\n                    Command.invoke(self, ctx)\n                    return _process_result([])\n            ctx.fail(\"Missing command.\")\n\n        # Fetch args back out\n        args = ctx.protected_args + ctx.args\n        ctx.args = []\n        ctx.protected_args = []\n\n        # If we're not in chain mode, we only allow the invocation of a\n        # single command but we also inform the current context about the\n        # name of the command to invoke.\n        if not self.chain:\n            # Make sure the context is entered so we do not clean up\n            # resources until the result processor has worked.\n            with ctx:\n                cmd_name, cmd, args = self.resolve_command(ctx, args)\n                ctx.invoked_subcommand = cmd_name\n                Command.invoke(self, ctx)\n                sub_ctx = cmd.make_context(cmd_name, args, parent=ctx)\n                with sub_ctx:\n                    return _process_result(sub_ctx.command.invoke(sub_ctx))\n\n        # In chain mode we create the contexts step by step, but after the\n        # base command has been invoked.  Because at that point we do not\n        # know the subcommands yet, the invoked subcommand attribute is\n        # set to ``*`` to inform the command that subcommands are executed\n        # but nothing else.\n        with ctx:\n            ctx.invoked_subcommand = \"*\" if args else None\n            Command.invoke(self, ctx)\n\n            # Otherwise we make every single context and invoke them in a\n            # chain.  In that case the return value to the result processor\n            # is the list of all invoked subcommand's results.\n            contexts = []\n            while args:\n                cmd_name, cmd, args = self.resolve_command(ctx, args)\n                sub_ctx = cmd.make_context(\n                    cmd_name,\n                    args,\n                    parent=ctx,\n                    allow_extra_args=True,\n                    allow_interspersed_args=False,\n                )\n                contexts.append(sub_ctx)\n                args, sub_ctx.args = sub_ctx.args, []\n\n            rv = []\n            for sub_ctx in contexts:\n                with sub_ctx:\n                    rv.append(sub_ctx.command.invoke(sub_ctx))\n            return _process_result(rv)\n\n    def resolve_command(self, ctx, args):\n        cmd_name = make_str(args[0])\n        original_cmd_name = cmd_name\n\n        # Get the command\n        cmd = self.get_command(ctx, cmd_name)\n\n        # If we can't find the command but there is a normalization\n        # function available, we try with that one.\n        if cmd is None and ctx.token_normalize_func is not None:\n            cmd_name = ctx.token_normalize_func(cmd_name)\n            cmd = self.get_command(ctx, cmd_name)\n\n        # If we don't find the command we want to show an error message\n        # to the user that it was not provided.  However, there is\n        # something else we should do: if the first argument looks like\n        # an option we want to kick off parsing again for arguments to\n        # resolve things like --help which now should go to the main\n        # place.\n        if cmd is None and not ctx.resilient_parsing:\n            if split_opt(cmd_name)[0]:\n                self.parse_args(ctx, ctx.args)\n            ctx.fail(\"No such command '{}'.\".format(original_cmd_name))\n\n        return cmd_name, cmd, args[1:]\n\n    def get_command(self, ctx, cmd_name):\n        \"\"\"Given a context and a command name, this returns a\n        :class:`Command` object if it exists or returns `None`.\n        \"\"\"\n        raise NotImplementedError()\n\n    def list_commands(self, ctx):\n        \"\"\"Returns a list of subcommand names in the order they should\n        appear.\n        \"\"\"\n        return []\n\n\nclass Group(MultiCommand):\n    \"\"\"A group allows a command to have subcommands attached.  This is the\n    most common way to implement nesting in Click.\n\n    :param commands: a dictionary of commands.\n    \"\"\"\n\n    def __init__(self, name=None, commands=None, **attrs):\n        MultiCommand.__init__(self, name, **attrs)\n        #: the registered subcommands by their exported names.\n        self.commands = commands or {}\n\n    def add_command(self, cmd, name=None):\n        \"\"\"Registers another :class:`Command` with this group.  If the name\n        is not provided, the name of the command is used.\n        \"\"\"\n        name = name or cmd.name\n        if name is None:\n            raise TypeError(\"Command has no name.\")\n        _check_multicommand(self, name, cmd, register=True)\n        self.commands[name] = cmd\n\n    def command(self, *args, **kwargs):\n        \"\"\"A shortcut decorator for declaring and attaching a command to\n        the group.  This takes the same arguments as :func:`command` but\n        immediately registers the created command with this instance by\n        calling into :meth:`add_command`.\n        \"\"\"\n        from .decorators import command\n\n        def decorator(f):\n            cmd = command(*args, **kwargs)(f)\n            self.add_command(cmd)\n            return cmd\n\n        return decorator\n\n    def group(self, *args, **kwargs):\n        \"\"\"A shortcut decorator for declaring and attaching a group to\n        the group.  This takes the same arguments as :func:`group` but\n        immediately registers the created command with this instance by\n        calling into :meth:`add_command`.\n        \"\"\"\n        from .decorators import group\n\n        def decorator(f):\n            cmd = group(*args, **kwargs)(f)\n            self.add_command(cmd)\n            return cmd\n\n        return decorator\n\n    def get_command(self, ctx, cmd_name):\n        return self.commands.get(cmd_name)\n\n    def list_commands(self, ctx):\n        return sorted(self.commands)\n\n\nclass CommandCollection(MultiCommand):\n    \"\"\"A command collection is a multi command that merges multiple multi\n    commands together into one.  This is a straightforward implementation\n    that accepts a list of different multi commands as sources and\n    provides all the commands for each of them.\n    \"\"\"\n\n    def __init__(self, name=None, sources=None, **attrs):\n        MultiCommand.__init__(self, name, **attrs)\n        #: The list of registered multi commands.\n        self.sources = sources or []\n\n    def add_source(self, multi_cmd):\n        \"\"\"Adds a new multi command to the chain dispatcher.\"\"\"\n        self.sources.append(multi_cmd)\n\n    def get_command(self, ctx, cmd_name):\n        for source in self.sources:\n            rv = source.get_command(ctx, cmd_name)\n            if rv is not None:\n                if self.chain:\n                    _check_multicommand(self, cmd_name, rv)\n                return rv\n\n    def list_commands(self, ctx):\n        rv = set()\n        for source in self.sources:\n            rv.update(source.list_commands(ctx))\n        return sorted(rv)\n\n\nclass Parameter(object):\n    r\"\"\"A parameter to a command comes in two versions: they are either\n    :class:`Option`\\s or :class:`Argument`\\s.  Other subclasses are currently\n    not supported by design as some of the internals for parsing are\n    intentionally not finalized.\n\n    Some settings are supported by both options and arguments.\n\n    :param param_decls: the parameter declarations for this option or\n                        argument.  This is a list of flags or argument\n                        names.\n    :param type: the type that should be used.  Either a :class:`ParamType`\n                 or a Python type.  The later is converted into the former\n                 automatically if supported.\n    :param required: controls if this is optional or not.\n    :param default: the default value if omitted.  This can also be a callable,\n                    in which case it's invoked when the default is needed\n                    without any arguments.\n    :param callback: a callback that should be executed after the parameter\n                     was matched.  This is called as ``fn(ctx, param,\n                     value)`` and needs to return the value.\n    :param nargs: the number of arguments to match.  If not ``1`` the return\n                  value is a tuple instead of single value.  The default for\n                  nargs is ``1`` (except if the type is a tuple, then it's\n                  the arity of the tuple).\n    :param metavar: how the value is represented in the help page.\n    :param expose_value: if this is `True` then the value is passed onwards\n                         to the command callback and stored on the context,\n                         otherwise it's skipped.\n    :param is_eager: eager values are processed before non eager ones.  This\n                     should not be set for arguments or it will inverse the\n                     order of processing.\n    :param envvar: a string or list of strings that are environment variables\n                   that should be checked.\n\n    .. versionchanged:: 7.1\n        Empty environment variables are ignored rather than taking the\n        empty string value. This makes it possible for scripts to clear\n        variables if they can't unset them.\n\n    .. versionchanged:: 2.0\n        Changed signature for parameter callback to also be passed the\n        parameter. The old callback format will still work, but it will\n        raise a warning to give you a chance to migrate the code easier.\n    \"\"\"\n    param_type_name = \"parameter\"\n\n    def __init__(\n        self,\n        param_decls=None,\n        type=None,\n        required=False,\n        default=None,\n        callback=None,\n        nargs=None,\n        metavar=None,\n        expose_value=True,\n        is_eager=False,\n        envvar=None,\n        autocompletion=None,\n    ):\n        self.name, self.opts, self.secondary_opts = self._parse_decls(\n            param_decls or (), expose_value\n        )\n\n        self.type = convert_type(type, default)\n\n        # Default nargs to what the type tells us if we have that\n        # information available.\n        if nargs is None:\n            if self.type.is_composite:\n                nargs = self.type.arity\n            else:\n                nargs = 1\n\n        self.required = required\n        self.callback = callback\n        self.nargs = nargs\n        self.multiple = False\n        self.expose_value = expose_value\n        self.default = default\n        self.is_eager = is_eager\n        self.metavar = metavar\n        self.envvar = envvar\n        self.autocompletion = autocompletion\n\n    def __repr__(self):\n        return \"<{} {}>\".format(self.__class__.__name__, self.name)\n\n    @property\n    def human_readable_name(self):\n        \"\"\"Returns the human readable name of this parameter.  This is the\n        same as the name for options, but the metavar for arguments.\n        \"\"\"\n        return self.name\n\n    def make_metavar(self):\n        if self.metavar is not None:\n            return self.metavar\n        metavar = self.type.get_metavar(self)\n        if metavar is None:\n            metavar = self.type.name.upper()\n        if self.nargs != 1:\n            metavar += \"...\"\n        return metavar\n\n    def get_default(self, ctx):\n        \"\"\"Given a context variable this calculates the default value.\"\"\"\n        # Otherwise go with the regular default.\n        if callable(self.default):\n            rv = self.default()\n        else:\n            rv = self.default\n        return self.type_cast_value(ctx, rv)\n\n    def add_to_parser(self, parser, ctx):\n        pass\n\n    def consume_value(self, ctx, opts):\n        value = opts.get(self.name)\n        if value is None:\n            value = self.value_from_envvar(ctx)\n        if value is None:\n            value = ctx.lookup_default(self.name)\n        return value\n\n    def type_cast_value(self, ctx, value):\n        \"\"\"Given a value this runs it properly through the type system.\n        This automatically handles things like `nargs` and `multiple` as\n        well as composite types.\n        \"\"\"\n        if self.type.is_composite:\n            if self.nargs <= 1:\n                raise TypeError(\n                    \"Attempted to invoke composite type but nargs has\"\n                    \" been set to {}. This is not supported; nargs\"\n                    \" needs to be set to a fixed value > 1.\".format(self.nargs)\n                )\n            if self.multiple:\n                return tuple(self.type(x or (), self, ctx) for x in value or ())\n            return self.type(value or (), self, ctx)\n\n        def _convert(value, level):\n            if level == 0:\n                return self.type(value, self, ctx)\n            return tuple(_convert(x, level - 1) for x in value or ())\n\n        return _convert(value, (self.nargs != 1) + bool(self.multiple))\n\n    def process_value(self, ctx, value):\n        \"\"\"Given a value and context this runs the logic to convert the\n        value as necessary.\n        \"\"\"\n        # If the value we were given is None we do nothing.  This way\n        # code that calls this can easily figure out if something was\n        # not provided.  Otherwise it would be converted into an empty\n        # tuple for multiple invocations which is inconvenient.\n        if value is not None:\n            return self.type_cast_value(ctx, value)\n\n    def value_is_missing(self, value):\n        if value is None:\n            return True\n        if (self.nargs != 1 or self.multiple) and value == ():\n            return True\n        return False\n\n    def full_process_value(self, ctx, value):\n        value = self.process_value(ctx, value)\n\n        if value is None and not ctx.resilient_parsing:\n            value = self.get_default(ctx)\n\n        if self.required and self.value_is_missing(value):\n            raise MissingParameter(ctx=ctx, param=self)\n\n        return value\n\n    def resolve_envvar_value(self, ctx):\n        if self.envvar is None:\n            return\n        if isinstance(self.envvar, (tuple, list)):\n            for envvar in self.envvar:\n                rv = os.environ.get(envvar)\n                if rv is not None:\n                    return rv\n        else:\n            rv = os.environ.get(self.envvar)\n\n            if rv != \"\":\n                return rv\n\n    def value_from_envvar(self, ctx):\n        rv = self.resolve_envvar_value(ctx)\n        if rv is not None and self.nargs != 1:\n            rv = self.type.split_envvar_value(rv)\n        return rv\n\n    def handle_parse_result(self, ctx, opts, args):\n        with augment_usage_errors(ctx, param=self):\n            value = self.consume_value(ctx, opts)\n            try:\n                value = self.full_process_value(ctx, value)\n            except Exception:\n                if not ctx.resilient_parsing:\n                    raise\n                value = None\n            if self.callback is not None:\n                try:\n                    value = invoke_param_callback(self.callback, ctx, self, value)\n                except Exception:\n                    if not ctx.resilient_parsing:\n                        raise\n\n        if self.expose_value:\n            ctx.params[self.name] = value\n        return value, args\n\n    def get_help_record(self, ctx):\n        pass\n\n    def get_usage_pieces(self, ctx):\n        return []\n\n    def get_error_hint(self, ctx):\n        \"\"\"Get a stringified version of the param for use in error messages to\n        indicate which param caused the error.\n        \"\"\"\n        hint_list = self.opts or [self.human_readable_name]\n        return \" / \".join(repr(x) for x in hint_list)\n\n\nclass Option(Parameter):\n    \"\"\"Options are usually optional values on the command line and\n    have some extra features that arguments don't have.\n\n    All other parameters are passed onwards to the parameter constructor.\n\n    :param show_default: controls if the default value should be shown on the\n                         help page. Normally, defaults are not shown. If this\n                         value is a string, it shows the string instead of the\n                         value. This is particularly useful for dynamic options.\n    :param show_envvar: controls if an environment variable should be shown on\n                        the help page.  Normally, environment variables\n                        are not shown.\n    :param prompt: if set to `True` or a non empty string then the user will be\n                   prompted for input.  If set to `True` the prompt will be the\n                   option name capitalized.\n    :param confirmation_prompt: if set then the value will need to be confirmed\n                                if it was prompted for.\n    :param hide_input: if this is `True` then the input on the prompt will be\n                       hidden from the user.  This is useful for password\n                       input.\n    :param is_flag: forces this option to act as a flag.  The default is\n                    auto detection.\n    :param flag_value: which value should be used for this flag if it's\n                       enabled.  This is set to a boolean automatically if\n                       the option string contains a slash to mark two options.\n    :param multiple: if this is set to `True` then the argument is accepted\n                     multiple times and recorded.  This is similar to ``nargs``\n                     in how it works but supports arbitrary number of\n                     arguments.\n    :param count: this flag makes an option increment an integer.\n    :param allow_from_autoenv: if this is enabled then the value of this\n                               parameter will be pulled from an environment\n                               variable in case a prefix is defined on the\n                               context.\n    :param help: the help string.\n    :param hidden: hide this option from help outputs.\n    \"\"\"\n\n    param_type_name = \"option\"\n\n    def __init__(\n        self,\n        param_decls=None,\n        show_default=False,\n        prompt=False,\n        confirmation_prompt=False,\n        hide_input=False,\n        is_flag=None,\n        flag_value=None,\n        multiple=False,\n        count=False,\n        allow_from_autoenv=True,\n        type=None,\n        help=None,\n        hidden=False,\n        show_choices=True,\n        show_envvar=False,\n        **attrs\n    ):\n        default_is_missing = attrs.get(\"default\", _missing) is _missing\n        Parameter.__init__(self, param_decls, type=type, **attrs)\n\n        if prompt is True:\n            prompt_text = self.name.replace(\"_\", \" \").capitalize()\n        elif prompt is False:\n            prompt_text = None\n        else:\n            prompt_text = prompt\n        self.prompt = prompt_text\n        self.confirmation_prompt = confirmation_prompt\n        self.hide_input = hide_input\n        self.hidden = hidden\n\n        # Flags\n        if is_flag is None:\n            if flag_value is not None:\n                is_flag = True\n            else:\n                is_flag = bool(self.secondary_opts)\n        if is_flag and default_is_missing:\n            self.default = False\n        if flag_value is None:\n            flag_value = not self.default\n        self.is_flag = is_flag\n        self.flag_value = flag_value\n        if self.is_flag and isinstance(self.flag_value, bool) and type in [None, bool]:\n            self.type = BOOL\n            self.is_bool_flag = True\n        else:\n            self.is_bool_flag = False\n\n        # Counting\n        self.count = count\n        if count:\n            if type is None:\n                self.type = IntRange(min=0)\n            if default_is_missing:\n                self.default = 0\n\n        self.multiple = multiple\n        self.allow_from_autoenv = allow_from_autoenv\n        self.help = help\n        self.show_default = show_default\n        self.show_choices = show_choices\n        self.show_envvar = show_envvar\n\n        # Sanity check for stuff we don't support\n        if __debug__:\n            if self.nargs < 0:\n                raise TypeError(\"Options cannot have nargs < 0\")\n            if self.prompt and self.is_flag and not self.is_bool_flag:\n                raise TypeError(\"Cannot prompt for flags that are not bools.\")\n            if not self.is_bool_flag and self.secondary_opts:\n                raise TypeError(\"Got secondary option for non boolean flag.\")\n            if self.is_bool_flag and self.hide_input and self.prompt is not None:\n                raise TypeError(\"Hidden input does not work with boolean flag prompts.\")\n            if self.count:\n                if self.multiple:\n                    raise TypeError(\n                        \"Options cannot be multiple and count at the same time.\"\n                    )\n                elif self.is_flag:\n                    raise TypeError(\n                        \"Options cannot be count and flags at the same time.\"\n                    )\n\n    def _parse_decls(self, decls, expose_value):\n        opts = []\n        secondary_opts = []\n        name = None\n        possible_names = []\n\n        for decl in decls:\n            if isidentifier(decl):\n                if name is not None:\n                    raise TypeError(\"Name defined twice\")\n                name = decl\n            else:\n                split_char = \";\" if decl[:1] == \"/\" else \"/\"\n                if split_char in decl:\n                    first, second = decl.split(split_char, 1)\n                    first = first.rstrip()\n                    if first:\n                        possible_names.append(split_opt(first))\n                        opts.append(first)\n                    second = second.lstrip()\n                    if second:\n                        secondary_opts.append(second.lstrip())\n                else:\n                    possible_names.append(split_opt(decl))\n                    opts.append(decl)\n\n        if name is None and possible_names:\n            possible_names.sort(key=lambda x: -len(x[0]))  # group long options first\n            name = possible_names[0][1].replace(\"-\", \"_\").lower()\n            if not isidentifier(name):\n                name = None\n\n        if name is None:\n            if not expose_value:\n                return None, opts, secondary_opts\n            raise TypeError(\"Could not determine name for option\")\n\n        if not opts and not secondary_opts:\n            raise TypeError(\n                \"No options defined but a name was passed ({}). Did you\"\n                \" mean to declare an argument instead of an option?\".format(name)\n            )\n\n        return name, opts, secondary_opts\n\n    def add_to_parser(self, parser, ctx):\n        kwargs = {\n            \"dest\": self.name,\n            \"nargs\": self.nargs,\n            \"obj\": self,\n        }\n\n        if self.multiple:\n            action = \"append\"\n        elif self.count:\n            action = \"count\"\n        else:\n            action = \"store\"\n\n        if self.is_flag:\n            kwargs.pop(\"nargs\", None)\n            action_const = \"{}_const\".format(action)\n            if self.is_bool_flag and self.secondary_opts:\n                parser.add_option(self.opts, action=action_const, const=True, **kwargs)\n                parser.add_option(\n                    self.secondary_opts, action=action_const, const=False, **kwargs\n                )\n            else:\n                parser.add_option(\n                    self.opts, action=action_const, const=self.flag_value, **kwargs\n                )\n        else:\n            kwargs[\"action\"] = action\n            parser.add_option(self.opts, **kwargs)\n\n    def get_help_record(self, ctx):\n        if self.hidden:\n            return\n        any_prefix_is_slash = []\n\n        def _write_opts(opts):\n            rv, any_slashes = join_options(opts)\n            if any_slashes:\n                any_prefix_is_slash[:] = [True]\n            if not self.is_flag and not self.count:\n                rv += \" {}\".format(self.make_metavar())\n            return rv\n\n        rv = [_write_opts(self.opts)]\n        if self.secondary_opts:\n            rv.append(_write_opts(self.secondary_opts))\n\n        help = self.help or \"\"\n        extra = []\n        if self.show_envvar:\n            envvar = self.envvar\n            if envvar is None:\n                if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None:\n                    envvar = \"{}_{}\".format(ctx.auto_envvar_prefix, self.name.upper())\n            if envvar is not None:\n                extra.append(\n                    \"env var: {}\".format(\n                        \", \".join(str(d) for d in envvar)\n                        if isinstance(envvar, (list, tuple))\n                        else envvar\n                    )\n                )\n        if self.default is not None and (self.show_default or ctx.show_default):\n            if isinstance(self.show_default, string_types):\n                default_string = \"({})\".format(self.show_default)\n            elif isinstance(self.default, (list, tuple)):\n                default_string = \", \".join(str(d) for d in self.default)\n            elif inspect.isfunction(self.default):\n                default_string = \"(dynamic)\"\n            else:\n                default_string = self.default\n            extra.append(\"default: {}\".format(default_string))\n\n        if self.required:\n            extra.append(\"required\")\n        if extra:\n            help = \"{}[{}]\".format(\n                \"{}  \".format(help) if help else \"\", \"; \".join(extra)\n            )\n\n        return (\"; \" if any_prefix_is_slash else \" / \").join(rv), help\n\n    def get_default(self, ctx):\n        # If we're a non boolean flag our default is more complex because\n        # we need to look at all flags in the same group to figure out\n        # if we're the the default one in which case we return the flag\n        # value as default.\n        if self.is_flag and not self.is_bool_flag:\n            for param in ctx.command.params:\n                if param.name == self.name and param.default:\n                    return param.flag_value\n            return None\n        return Parameter.get_default(self, ctx)\n\n    def prompt_for_value(self, ctx):\n        \"\"\"This is an alternative flow that can be activated in the full\n        value processing if a value does not exist.  It will prompt the\n        user until a valid value exists and then returns the processed\n        value as result.\n        \"\"\"\n        # Calculate the default before prompting anything to be stable.\n        default = self.get_default(ctx)\n\n        # If this is a prompt for a flag we need to handle this\n        # differently.\n        if self.is_bool_flag:\n            return confirm(self.prompt, default)\n\n        return prompt(\n            self.prompt,\n            default=default,\n            type=self.type,\n            hide_input=self.hide_input,\n            show_choices=self.show_choices,\n            confirmation_prompt=self.confirmation_prompt,\n            value_proc=lambda x: self.process_value(ctx, x),\n        )\n\n    def resolve_envvar_value(self, ctx):\n        rv = Parameter.resolve_envvar_value(self, ctx)\n        if rv is not None:\n            return rv\n        if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None:\n            envvar = \"{}_{}\".format(ctx.auto_envvar_prefix, self.name.upper())\n            return os.environ.get(envvar)\n\n    def value_from_envvar(self, ctx):\n        rv = self.resolve_envvar_value(ctx)\n        if rv is None:\n            return None\n        value_depth = (self.nargs != 1) + bool(self.multiple)\n        if value_depth > 0 and rv is not None:\n            rv = self.type.split_envvar_value(rv)\n            if self.multiple and self.nargs != 1:\n                rv = batch(rv, self.nargs)\n        return rv\n\n    def full_process_value(self, ctx, value):\n        if value is None and self.prompt is not None and not ctx.resilient_parsing:\n            return self.prompt_for_value(ctx)\n        return Parameter.full_process_value(self, ctx, value)\n\n\nclass Argument(Parameter):\n    \"\"\"Arguments are positional parameters to a command.  They generally\n    provide fewer features than options but can have infinite ``nargs``\n    and are required by default.\n\n    All parameters are passed onwards to the parameter constructor.\n    \"\"\"\n\n    param_type_name = \"argument\"\n\n    def __init__(self, param_decls, required=None, **attrs):\n        if required is None:\n            if attrs.get(\"default\") is not None:\n                required = False\n            else:\n                required = attrs.get(\"nargs\", 1) > 0\n        Parameter.__init__(self, param_decls, required=required, **attrs)\n        if self.default is not None and self.nargs < 0:\n            raise TypeError(\n                \"nargs=-1 in combination with a default value is not supported.\"\n            )\n\n    @property\n    def human_readable_name(self):\n        if self.metavar is not None:\n            return self.metavar\n        return self.name.upper()\n\n    def make_metavar(self):\n        if self.metavar is not None:\n            return self.metavar\n        var = self.type.get_metavar(self)\n        if not var:\n            var = self.name.upper()\n        if not self.required:\n            var = \"[{}]\".format(var)\n        if self.nargs != 1:\n            var += \"...\"\n        return var\n\n    def _parse_decls(self, decls, expose_value):\n        if not decls:\n            if not expose_value:\n                return None, [], []\n            raise TypeError(\"Could not determine name for argument\")\n        if len(decls) == 1:\n            name = arg = decls[0]\n            name = name.replace(\"-\", \"_\").lower()\n        else:\n            raise TypeError(\n                \"Arguments take exactly one parameter declaration, got\"\n                \" {}\".format(len(decls))\n            )\n        return name, [arg], []\n\n    def get_usage_pieces(self, ctx):\n        return [self.make_metavar()]\n\n    def get_error_hint(self, ctx):\n        return repr(self.make_metavar())\n\n    def add_to_parser(self, parser, ctx):\n        parser.add_argument(dest=self.name, nargs=self.nargs, obj=self)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/decorators.py",
    "content": "import inspect\nimport sys\nfrom functools import update_wrapper\n\nfrom ._compat import iteritems\nfrom ._unicodefun import _check_for_unicode_literals\nfrom .core import Argument\nfrom .core import Command\nfrom .core import Group\nfrom .core import Option\nfrom .globals import get_current_context\nfrom .utils import echo\n\n\ndef pass_context(f):\n    \"\"\"Marks a callback as wanting to receive the current context\n    object as first argument.\n    \"\"\"\n\n    def new_func(*args, **kwargs):\n        return f(get_current_context(), *args, **kwargs)\n\n    return update_wrapper(new_func, f)\n\n\ndef pass_obj(f):\n    \"\"\"Similar to :func:`pass_context`, but only pass the object on the\n    context onwards (:attr:`Context.obj`).  This is useful if that object\n    represents the state of a nested system.\n    \"\"\"\n\n    def new_func(*args, **kwargs):\n        return f(get_current_context().obj, *args, **kwargs)\n\n    return update_wrapper(new_func, f)\n\n\ndef make_pass_decorator(object_type, ensure=False):\n    \"\"\"Given an object type this creates a decorator that will work\n    similar to :func:`pass_obj` but instead of passing the object of the\n    current context, it will find the innermost context of type\n    :func:`object_type`.\n\n    This generates a decorator that works roughly like this::\n\n        from functools import update_wrapper\n\n        def decorator(f):\n            @pass_context\n            def new_func(ctx, *args, **kwargs):\n                obj = ctx.find_object(object_type)\n                return ctx.invoke(f, obj, *args, **kwargs)\n            return update_wrapper(new_func, f)\n        return decorator\n\n    :param object_type: the type of the object to pass.\n    :param ensure: if set to `True`, a new object will be created and\n                   remembered on the context if it's not there yet.\n    \"\"\"\n\n    def decorator(f):\n        def new_func(*args, **kwargs):\n            ctx = get_current_context()\n            if ensure:\n                obj = ctx.ensure_object(object_type)\n            else:\n                obj = ctx.find_object(object_type)\n            if obj is None:\n                raise RuntimeError(\n                    \"Managed to invoke callback without a context\"\n                    \" object of type '{}' existing\".format(object_type.__name__)\n                )\n            return ctx.invoke(f, obj, *args, **kwargs)\n\n        return update_wrapper(new_func, f)\n\n    return decorator\n\n\ndef _make_command(f, name, attrs, cls):\n    if isinstance(f, Command):\n        raise TypeError(\"Attempted to convert a callback into a command twice.\")\n    try:\n        params = f.__click_params__\n        params.reverse()\n        del f.__click_params__\n    except AttributeError:\n        params = []\n    help = attrs.get(\"help\")\n    if help is None:\n        help = inspect.getdoc(f)\n        if isinstance(help, bytes):\n            help = help.decode(\"utf-8\")\n    else:\n        help = inspect.cleandoc(help)\n    attrs[\"help\"] = help\n    _check_for_unicode_literals()\n    return cls(\n        name=name or f.__name__.lower().replace(\"_\", \"-\"),\n        callback=f,\n        params=params,\n        **attrs\n    )\n\n\ndef command(name=None, cls=None, **attrs):\n    r\"\"\"Creates a new :class:`Command` and uses the decorated function as\n    callback.  This will also automatically attach all decorated\n    :func:`option`\\s and :func:`argument`\\s as parameters to the command.\n\n    The name of the command defaults to the name of the function with\n    underscores replaced by dashes.  If you want to change that, you can\n    pass the intended name as the first argument.\n\n    All keyword arguments are forwarded to the underlying command class.\n\n    Once decorated the function turns into a :class:`Command` instance\n    that can be invoked as a command line utility or be attached to a\n    command :class:`Group`.\n\n    :param name: the name of the command.  This defaults to the function\n                 name with underscores replaced by dashes.\n    :param cls: the command class to instantiate.  This defaults to\n                :class:`Command`.\n    \"\"\"\n    if cls is None:\n        cls = Command\n\n    def decorator(f):\n        cmd = _make_command(f, name, attrs, cls)\n        cmd.__doc__ = f.__doc__\n        return cmd\n\n    return decorator\n\n\ndef group(name=None, **attrs):\n    \"\"\"Creates a new :class:`Group` with a function as callback.  This\n    works otherwise the same as :func:`command` just that the `cls`\n    parameter is set to :class:`Group`.\n    \"\"\"\n    attrs.setdefault(\"cls\", Group)\n    return command(name, **attrs)\n\n\ndef _param_memo(f, param):\n    if isinstance(f, Command):\n        f.params.append(param)\n    else:\n        if not hasattr(f, \"__click_params__\"):\n            f.__click_params__ = []\n        f.__click_params__.append(param)\n\n\ndef argument(*param_decls, **attrs):\n    \"\"\"Attaches an argument to the command.  All positional arguments are\n    passed as parameter declarations to :class:`Argument`; all keyword\n    arguments are forwarded unchanged (except ``cls``).\n    This is equivalent to creating an :class:`Argument` instance manually\n    and attaching it to the :attr:`Command.params` list.\n\n    :param cls: the argument class to instantiate.  This defaults to\n                :class:`Argument`.\n    \"\"\"\n\n    def decorator(f):\n        ArgumentClass = attrs.pop(\"cls\", Argument)\n        _param_memo(f, ArgumentClass(param_decls, **attrs))\n        return f\n\n    return decorator\n\n\ndef option(*param_decls, **attrs):\n    \"\"\"Attaches an option to the command.  All positional arguments are\n    passed as parameter declarations to :class:`Option`; all keyword\n    arguments are forwarded unchanged (except ``cls``).\n    This is equivalent to creating an :class:`Option` instance manually\n    and attaching it to the :attr:`Command.params` list.\n\n    :param cls: the option class to instantiate.  This defaults to\n                :class:`Option`.\n    \"\"\"\n\n    def decorator(f):\n        # Issue 926, copy attrs, so pre-defined options can re-use the same cls=\n        option_attrs = attrs.copy()\n\n        if \"help\" in option_attrs:\n            option_attrs[\"help\"] = inspect.cleandoc(option_attrs[\"help\"])\n        OptionClass = option_attrs.pop(\"cls\", Option)\n        _param_memo(f, OptionClass(param_decls, **option_attrs))\n        return f\n\n    return decorator\n\n\ndef confirmation_option(*param_decls, **attrs):\n    \"\"\"Shortcut for confirmation prompts that can be ignored by passing\n    ``--yes`` as parameter.\n\n    This is equivalent to decorating a function with :func:`option` with\n    the following parameters::\n\n        def callback(ctx, param, value):\n            if not value:\n                ctx.abort()\n\n        @click.command()\n        @click.option('--yes', is_flag=True, callback=callback,\n                      expose_value=False, prompt='Do you want to continue?')\n        def dropdb():\n            pass\n    \"\"\"\n\n    def decorator(f):\n        def callback(ctx, param, value):\n            if not value:\n                ctx.abort()\n\n        attrs.setdefault(\"is_flag\", True)\n        attrs.setdefault(\"callback\", callback)\n        attrs.setdefault(\"expose_value\", False)\n        attrs.setdefault(\"prompt\", \"Do you want to continue?\")\n        attrs.setdefault(\"help\", \"Confirm the action without prompting.\")\n        return option(*(param_decls or (\"--yes\",)), **attrs)(f)\n\n    return decorator\n\n\ndef password_option(*param_decls, **attrs):\n    \"\"\"Shortcut for password prompts.\n\n    This is equivalent to decorating a function with :func:`option` with\n    the following parameters::\n\n        @click.command()\n        @click.option('--password', prompt=True, confirmation_prompt=True,\n                      hide_input=True)\n        def changeadmin(password):\n            pass\n    \"\"\"\n\n    def decorator(f):\n        attrs.setdefault(\"prompt\", True)\n        attrs.setdefault(\"confirmation_prompt\", True)\n        attrs.setdefault(\"hide_input\", True)\n        return option(*(param_decls or (\"--password\",)), **attrs)(f)\n\n    return decorator\n\n\ndef version_option(version=None, *param_decls, **attrs):\n    \"\"\"Adds a ``--version`` option which immediately ends the program\n    printing out the version number.  This is implemented as an eager\n    option that prints the version and exits the program in the callback.\n\n    :param version: the version number to show.  If not provided Click\n                    attempts an auto discovery via setuptools.\n    :param prog_name: the name of the program (defaults to autodetection)\n    :param message: custom message to show instead of the default\n                    (``'%(prog)s, version %(version)s'``)\n    :param others: everything else is forwarded to :func:`option`.\n    \"\"\"\n    if version is None:\n        if hasattr(sys, \"_getframe\"):\n            module = sys._getframe(1).f_globals.get(\"__name__\")\n        else:\n            module = \"\"\n\n    def decorator(f):\n        prog_name = attrs.pop(\"prog_name\", None)\n        message = attrs.pop(\"message\", \"%(prog)s, version %(version)s\")\n\n        def callback(ctx, param, value):\n            if not value or ctx.resilient_parsing:\n                return\n            prog = prog_name\n            if prog is None:\n                prog = ctx.find_root().info_name\n            ver = version\n            if ver is None:\n                try:\n                    import pkg_resources\n                except ImportError:\n                    pass\n                else:\n                    for dist in pkg_resources.working_set:\n                        scripts = dist.get_entry_map().get(\"console_scripts\") or {}\n                        for _, entry_point in iteritems(scripts):\n                            if entry_point.module_name == module:\n                                ver = dist.version\n                                break\n                if ver is None:\n                    raise RuntimeError(\"Could not determine version\")\n            echo(message % {\"prog\": prog, \"version\": ver}, color=ctx.color)\n            ctx.exit()\n\n        attrs.setdefault(\"is_flag\", True)\n        attrs.setdefault(\"expose_value\", False)\n        attrs.setdefault(\"is_eager\", True)\n        attrs.setdefault(\"help\", \"Show the version and exit.\")\n        attrs[\"callback\"] = callback\n        return option(*(param_decls or (\"--version\",)), **attrs)(f)\n\n    return decorator\n\n\ndef help_option(*param_decls, **attrs):\n    \"\"\"Adds a ``--help`` option which immediately ends the program\n    printing out the help page.  This is usually unnecessary to add as\n    this is added by default to all commands unless suppressed.\n\n    Like :func:`version_option`, this is implemented as eager option that\n    prints in the callback and exits.\n\n    All arguments are forwarded to :func:`option`.\n    \"\"\"\n\n    def decorator(f):\n        def callback(ctx, param, value):\n            if value and not ctx.resilient_parsing:\n                echo(ctx.get_help(), color=ctx.color)\n                ctx.exit()\n\n        attrs.setdefault(\"is_flag\", True)\n        attrs.setdefault(\"expose_value\", False)\n        attrs.setdefault(\"help\", \"Show this message and exit.\")\n        attrs.setdefault(\"is_eager\", True)\n        attrs[\"callback\"] = callback\n        return option(*(param_decls or (\"--help\",)), **attrs)(f)\n\n    return decorator\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/exceptions.py",
    "content": "from ._compat import filename_to_ui\nfrom ._compat import get_text_stderr\nfrom ._compat import PY2\nfrom .utils import echo\n\n\ndef _join_param_hints(param_hint):\n    if isinstance(param_hint, (tuple, list)):\n        return \" / \".join(repr(x) for x in param_hint)\n    return param_hint\n\n\nclass ClickException(Exception):\n    \"\"\"An exception that Click can handle and show to the user.\"\"\"\n\n    #: The exit code for this exception\n    exit_code = 1\n\n    def __init__(self, message):\n        ctor_msg = message\n        if PY2:\n            if ctor_msg is not None:\n                ctor_msg = ctor_msg.encode(\"utf-8\")\n        Exception.__init__(self, ctor_msg)\n        self.message = message\n\n    def format_message(self):\n        return self.message\n\n    def __str__(self):\n        return self.message\n\n    if PY2:\n        __unicode__ = __str__\n\n        def __str__(self):\n            return self.message.encode(\"utf-8\")\n\n    def show(self, file=None):\n        if file is None:\n            file = get_text_stderr()\n        echo(\"Error: {}\".format(self.format_message()), file=file)\n\n\nclass UsageError(ClickException):\n    \"\"\"An internal exception that signals a usage error.  This typically\n    aborts any further handling.\n\n    :param message: the error message to display.\n    :param ctx: optionally the context that caused this error.  Click will\n                fill in the context automatically in some situations.\n    \"\"\"\n\n    exit_code = 2\n\n    def __init__(self, message, ctx=None):\n        ClickException.__init__(self, message)\n        self.ctx = ctx\n        self.cmd = self.ctx.command if self.ctx else None\n\n    def show(self, file=None):\n        if file is None:\n            file = get_text_stderr()\n        color = None\n        hint = \"\"\n        if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None:\n            hint = \"Try '{} {}' for help.\\n\".format(\n                self.ctx.command_path, self.ctx.help_option_names[0]\n            )\n        if self.ctx is not None:\n            color = self.ctx.color\n            echo(\"{}\\n{}\".format(self.ctx.get_usage(), hint), file=file, color=color)\n        echo(\"Error: {}\".format(self.format_message()), file=file, color=color)\n\n\nclass BadParameter(UsageError):\n    \"\"\"An exception that formats out a standardized error message for a\n    bad parameter.  This is useful when thrown from a callback or type as\n    Click will attach contextual information to it (for instance, which\n    parameter it is).\n\n    .. versionadded:: 2.0\n\n    :param param: the parameter object that caused this error.  This can\n                  be left out, and Click will attach this info itself\n                  if possible.\n    :param param_hint: a string that shows up as parameter name.  This\n                       can be used as alternative to `param` in cases\n                       where custom validation should happen.  If it is\n                       a string it's used as such, if it's a list then\n                       each item is quoted and separated.\n    \"\"\"\n\n    def __init__(self, message, ctx=None, param=None, param_hint=None):\n        UsageError.__init__(self, message, ctx)\n        self.param = param\n        self.param_hint = param_hint\n\n    def format_message(self):\n        if self.param_hint is not None:\n            param_hint = self.param_hint\n        elif self.param is not None:\n            param_hint = self.param.get_error_hint(self.ctx)\n        else:\n            return \"Invalid value: {}\".format(self.message)\n        param_hint = _join_param_hints(param_hint)\n\n        return \"Invalid value for {}: {}\".format(param_hint, self.message)\n\n\nclass MissingParameter(BadParameter):\n    \"\"\"Raised if click required an option or argument but it was not\n    provided when invoking the script.\n\n    .. versionadded:: 4.0\n\n    :param param_type: a string that indicates the type of the parameter.\n                       The default is to inherit the parameter type from\n                       the given `param`.  Valid values are ``'parameter'``,\n                       ``'option'`` or ``'argument'``.\n    \"\"\"\n\n    def __init__(\n        self, message=None, ctx=None, param=None, param_hint=None, param_type=None\n    ):\n        BadParameter.__init__(self, message, ctx, param, param_hint)\n        self.param_type = param_type\n\n    def format_message(self):\n        if self.param_hint is not None:\n            param_hint = self.param_hint\n        elif self.param is not None:\n            param_hint = self.param.get_error_hint(self.ctx)\n        else:\n            param_hint = None\n        param_hint = _join_param_hints(param_hint)\n\n        param_type = self.param_type\n        if param_type is None and self.param is not None:\n            param_type = self.param.param_type_name\n\n        msg = self.message\n        if self.param is not None:\n            msg_extra = self.param.type.get_missing_message(self.param)\n            if msg_extra:\n                if msg:\n                    msg += \".  {}\".format(msg_extra)\n                else:\n                    msg = msg_extra\n\n        return \"Missing {}{}{}{}\".format(\n            param_type,\n            \" {}\".format(param_hint) if param_hint else \"\",\n            \".  \" if msg else \".\",\n            msg or \"\",\n        )\n\n    def __str__(self):\n        if self.message is None:\n            param_name = self.param.name if self.param else None\n            return \"missing parameter: {}\".format(param_name)\n        else:\n            return self.message\n\n    if PY2:\n        __unicode__ = __str__\n\n        def __str__(self):\n            return self.__unicode__().encode(\"utf-8\")\n\n\nclass NoSuchOption(UsageError):\n    \"\"\"Raised if click attempted to handle an option that does not\n    exist.\n\n    .. versionadded:: 4.0\n    \"\"\"\n\n    def __init__(self, option_name, message=None, possibilities=None, ctx=None):\n        if message is None:\n            message = \"no such option: {}\".format(option_name)\n        UsageError.__init__(self, message, ctx)\n        self.option_name = option_name\n        self.possibilities = possibilities\n\n    def format_message(self):\n        bits = [self.message]\n        if self.possibilities:\n            if len(self.possibilities) == 1:\n                bits.append(\"Did you mean {}?\".format(self.possibilities[0]))\n            else:\n                possibilities = sorted(self.possibilities)\n                bits.append(\"(Possible options: {})\".format(\", \".join(possibilities)))\n        return \"  \".join(bits)\n\n\nclass BadOptionUsage(UsageError):\n    \"\"\"Raised if an option is generally supplied but the use of the option\n    was incorrect.  This is for instance raised if the number of arguments\n    for an option is not correct.\n\n    .. versionadded:: 4.0\n\n    :param option_name: the name of the option being used incorrectly.\n    \"\"\"\n\n    def __init__(self, option_name, message, ctx=None):\n        UsageError.__init__(self, message, ctx)\n        self.option_name = option_name\n\n\nclass BadArgumentUsage(UsageError):\n    \"\"\"Raised if an argument is generally supplied but the use of the argument\n    was incorrect.  This is for instance raised if the number of values\n    for an argument is not correct.\n\n    .. versionadded:: 6.0\n    \"\"\"\n\n    def __init__(self, message, ctx=None):\n        UsageError.__init__(self, message, ctx)\n\n\nclass FileError(ClickException):\n    \"\"\"Raised if a file cannot be opened.\"\"\"\n\n    def __init__(self, filename, hint=None):\n        ui_filename = filename_to_ui(filename)\n        if hint is None:\n            hint = \"unknown error\"\n        ClickException.__init__(self, hint)\n        self.ui_filename = ui_filename\n        self.filename = filename\n\n    def format_message(self):\n        return \"Could not open file {}: {}\".format(self.ui_filename, self.message)\n\n\nclass Abort(RuntimeError):\n    \"\"\"An internal signalling exception that signals Click to abort.\"\"\"\n\n\nclass Exit(RuntimeError):\n    \"\"\"An exception that indicates that the application should exit with some\n    status code.\n\n    :param code: the status code to exit with.\n    \"\"\"\n\n    __slots__ = (\"exit_code\",)\n\n    def __init__(self, code=0):\n        self.exit_code = code\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/formatting.py",
    "content": "from contextlib import contextmanager\n\nfrom ._compat import term_len\nfrom .parser import split_opt\nfrom .termui import get_terminal_size\n\n# Can force a width.  This is used by the test system\nFORCED_WIDTH = None\n\n\ndef measure_table(rows):\n    widths = {}\n    for row in rows:\n        for idx, col in enumerate(row):\n            widths[idx] = max(widths.get(idx, 0), term_len(col))\n    return tuple(y for x, y in sorted(widths.items()))\n\n\ndef iter_rows(rows, col_count):\n    for row in rows:\n        row = tuple(row)\n        yield row + (\"\",) * (col_count - len(row))\n\n\ndef wrap_text(\n    text, width=78, initial_indent=\"\", subsequent_indent=\"\", preserve_paragraphs=False\n):\n    \"\"\"A helper function that intelligently wraps text.  By default, it\n    assumes that it operates on a single paragraph of text but if the\n    `preserve_paragraphs` parameter is provided it will intelligently\n    handle paragraphs (defined by two empty lines).\n\n    If paragraphs are handled, a paragraph can be prefixed with an empty\n    line containing the ``\\\\b`` character (``\\\\x08``) to indicate that\n    no rewrapping should happen in that block.\n\n    :param text: the text that should be rewrapped.\n    :param width: the maximum width for the text.\n    :param initial_indent: the initial indent that should be placed on the\n                           first line as a string.\n    :param subsequent_indent: the indent string that should be placed on\n                              each consecutive line.\n    :param preserve_paragraphs: if this flag is set then the wrapping will\n                                intelligently handle paragraphs.\n    \"\"\"\n    from ._textwrap import TextWrapper\n\n    text = text.expandtabs()\n    wrapper = TextWrapper(\n        width,\n        initial_indent=initial_indent,\n        subsequent_indent=subsequent_indent,\n        replace_whitespace=False,\n    )\n    if not preserve_paragraphs:\n        return wrapper.fill(text)\n\n    p = []\n    buf = []\n    indent = None\n\n    def _flush_par():\n        if not buf:\n            return\n        if buf[0].strip() == \"\\b\":\n            p.append((indent or 0, True, \"\\n\".join(buf[1:])))\n        else:\n            p.append((indent or 0, False, \" \".join(buf)))\n        del buf[:]\n\n    for line in text.splitlines():\n        if not line:\n            _flush_par()\n            indent = None\n        else:\n            if indent is None:\n                orig_len = term_len(line)\n                line = line.lstrip()\n                indent = orig_len - term_len(line)\n            buf.append(line)\n    _flush_par()\n\n    rv = []\n    for indent, raw, text in p:\n        with wrapper.extra_indent(\" \" * indent):\n            if raw:\n                rv.append(wrapper.indent_only(text))\n            else:\n                rv.append(wrapper.fill(text))\n\n    return \"\\n\\n\".join(rv)\n\n\nclass HelpFormatter(object):\n    \"\"\"This class helps with formatting text-based help pages.  It's\n    usually just needed for very special internal cases, but it's also\n    exposed so that developers can write their own fancy outputs.\n\n    At present, it always writes into memory.\n\n    :param indent_increment: the additional increment for each level.\n    :param width: the width for the text.  This defaults to the terminal\n                  width clamped to a maximum of 78.\n    \"\"\"\n\n    def __init__(self, indent_increment=2, width=None, max_width=None):\n        self.indent_increment = indent_increment\n        if max_width is None:\n            max_width = 80\n        if width is None:\n            width = FORCED_WIDTH\n            if width is None:\n                width = max(min(get_terminal_size()[0], max_width) - 2, 50)\n        self.width = width\n        self.current_indent = 0\n        self.buffer = []\n\n    def write(self, string):\n        \"\"\"Writes a unicode string into the internal buffer.\"\"\"\n        self.buffer.append(string)\n\n    def indent(self):\n        \"\"\"Increases the indentation.\"\"\"\n        self.current_indent += self.indent_increment\n\n    def dedent(self):\n        \"\"\"Decreases the indentation.\"\"\"\n        self.current_indent -= self.indent_increment\n\n    def write_usage(self, prog, args=\"\", prefix=\"Usage: \"):\n        \"\"\"Writes a usage line into the buffer.\n\n        :param prog: the program name.\n        :param args: whitespace separated list of arguments.\n        :param prefix: the prefix for the first line.\n        \"\"\"\n        usage_prefix = \"{:>{w}}{} \".format(prefix, prog, w=self.current_indent)\n        text_width = self.width - self.current_indent\n\n        if text_width >= (term_len(usage_prefix) + 20):\n            # The arguments will fit to the right of the prefix.\n            indent = \" \" * term_len(usage_prefix)\n            self.write(\n                wrap_text(\n                    args,\n                    text_width,\n                    initial_indent=usage_prefix,\n                    subsequent_indent=indent,\n                )\n            )\n        else:\n            # The prefix is too long, put the arguments on the next line.\n            self.write(usage_prefix)\n            self.write(\"\\n\")\n            indent = \" \" * (max(self.current_indent, term_len(prefix)) + 4)\n            self.write(\n                wrap_text(\n                    args, text_width, initial_indent=indent, subsequent_indent=indent\n                )\n            )\n\n        self.write(\"\\n\")\n\n    def write_heading(self, heading):\n        \"\"\"Writes a heading into the buffer.\"\"\"\n        self.write(\"{:>{w}}{}:\\n\".format(\"\", heading, w=self.current_indent))\n\n    def write_paragraph(self):\n        \"\"\"Writes a paragraph into the buffer.\"\"\"\n        if self.buffer:\n            self.write(\"\\n\")\n\n    def write_text(self, text):\n        \"\"\"Writes re-indented text into the buffer.  This rewraps and\n        preserves paragraphs.\n        \"\"\"\n        text_width = max(self.width - self.current_indent, 11)\n        indent = \" \" * self.current_indent\n        self.write(\n            wrap_text(\n                text,\n                text_width,\n                initial_indent=indent,\n                subsequent_indent=indent,\n                preserve_paragraphs=True,\n            )\n        )\n        self.write(\"\\n\")\n\n    def write_dl(self, rows, col_max=30, col_spacing=2):\n        \"\"\"Writes a definition list into the buffer.  This is how options\n        and commands are usually formatted.\n\n        :param rows: a list of two item tuples for the terms and values.\n        :param col_max: the maximum width of the first column.\n        :param col_spacing: the number of spaces between the first and\n                            second column.\n        \"\"\"\n        rows = list(rows)\n        widths = measure_table(rows)\n        if len(widths) != 2:\n            raise TypeError(\"Expected two columns for definition list\")\n\n        first_col = min(widths[0], col_max) + col_spacing\n\n        for first, second in iter_rows(rows, len(widths)):\n            self.write(\"{:>{w}}{}\".format(\"\", first, w=self.current_indent))\n            if not second:\n                self.write(\"\\n\")\n                continue\n            if term_len(first) <= first_col - col_spacing:\n                self.write(\" \" * (first_col - term_len(first)))\n            else:\n                self.write(\"\\n\")\n                self.write(\" \" * (first_col + self.current_indent))\n\n            text_width = max(self.width - first_col - 2, 10)\n            wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True)\n            lines = wrapped_text.splitlines()\n\n            if lines:\n                self.write(\"{}\\n\".format(lines[0]))\n\n                for line in lines[1:]:\n                    self.write(\n                        \"{:>{w}}{}\\n\".format(\n                            \"\", line, w=first_col + self.current_indent\n                        )\n                    )\n\n                if len(lines) > 1:\n                    # separate long help from next option\n                    self.write(\"\\n\")\n            else:\n                self.write(\"\\n\")\n\n    @contextmanager\n    def section(self, name):\n        \"\"\"Helpful context manager that writes a paragraph, a heading,\n        and the indents.\n\n        :param name: the section name that is written as heading.\n        \"\"\"\n        self.write_paragraph()\n        self.write_heading(name)\n        self.indent()\n        try:\n            yield\n        finally:\n            self.dedent()\n\n    @contextmanager\n    def indentation(self):\n        \"\"\"A context manager that increases the indentation.\"\"\"\n        self.indent()\n        try:\n            yield\n        finally:\n            self.dedent()\n\n    def getvalue(self):\n        \"\"\"Returns the buffer contents.\"\"\"\n        return \"\".join(self.buffer)\n\n\ndef join_options(options):\n    \"\"\"Given a list of option strings this joins them in the most appropriate\n    way and returns them in the form ``(formatted_string,\n    any_prefix_is_slash)`` where the second item in the tuple is a flag that\n    indicates if any of the option prefixes was a slash.\n    \"\"\"\n    rv = []\n    any_prefix_is_slash = False\n    for opt in options:\n        prefix = split_opt(opt)[0]\n        if prefix == \"/\":\n            any_prefix_is_slash = True\n        rv.append((len(prefix), opt))\n\n    rv.sort(key=lambda x: x[0])\n\n    rv = \", \".join(x[1] for x in rv)\n    return rv, any_prefix_is_slash\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/globals.py",
    "content": "from threading import local\n\n_local = local()\n\n\ndef get_current_context(silent=False):\n    \"\"\"Returns the current click context.  This can be used as a way to\n    access the current context object from anywhere.  This is a more implicit\n    alternative to the :func:`pass_context` decorator.  This function is\n    primarily useful for helpers such as :func:`echo` which might be\n    interested in changing its behavior based on the current context.\n\n    To push the current context, :meth:`Context.scope` can be used.\n\n    .. versionadded:: 5.0\n\n    :param silent: if set to `True` the return value is `None` if no context\n                   is available.  The default behavior is to raise a\n                   :exc:`RuntimeError`.\n    \"\"\"\n    try:\n        return _local.stack[-1]\n    except (AttributeError, IndexError):\n        if not silent:\n            raise RuntimeError(\"There is no active click context.\")\n\n\ndef push_context(ctx):\n    \"\"\"Pushes a new context to the current stack.\"\"\"\n    _local.__dict__.setdefault(\"stack\", []).append(ctx)\n\n\ndef pop_context():\n    \"\"\"Removes the top level from the stack.\"\"\"\n    _local.stack.pop()\n\n\ndef resolve_color_default(color=None):\n    \"\"\"\"Internal helper to get the default value of the color flag.  If a\n    value is passed it's returned unchanged, otherwise it's looked up from\n    the current context.\n    \"\"\"\n    if color is not None:\n        return color\n    ctx = get_current_context(silent=True)\n    if ctx is not None:\n        return ctx.color\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/parser.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nThis module started out as largely a copy paste from the stdlib's\noptparse module with the features removed that we do not need from\noptparse because we implement them in Click on a higher level (for\ninstance type handling, help formatting and a lot more).\n\nThe plan is to remove more and more from here over time.\n\nThe reason this is a different module and not optparse from the stdlib\nis that there are differences in 2.x and 3.x about the error messages\ngenerated and optparse in the stdlib uses gettext for no good reason\nand might cause us issues.\n\nClick uses parts of optparse written by Gregory P. Ward and maintained\nby the Python Software Foundation. This is limited to code in parser.py.\n\nCopyright 2001-2006 Gregory P. Ward. All rights reserved.\nCopyright 2002-2006 Python Software Foundation. All rights reserved.\n\"\"\"\nimport re\nfrom collections import deque\n\nfrom .exceptions import BadArgumentUsage\nfrom .exceptions import BadOptionUsage\nfrom .exceptions import NoSuchOption\nfrom .exceptions import UsageError\n\n\ndef _unpack_args(args, nargs_spec):\n    \"\"\"Given an iterable of arguments and an iterable of nargs specifications,\n    it returns a tuple with all the unpacked arguments at the first index\n    and all remaining arguments as the second.\n\n    The nargs specification is the number of arguments that should be consumed\n    or `-1` to indicate that this position should eat up all the remainders.\n\n    Missing items are filled with `None`.\n    \"\"\"\n    args = deque(args)\n    nargs_spec = deque(nargs_spec)\n    rv = []\n    spos = None\n\n    def _fetch(c):\n        try:\n            if spos is None:\n                return c.popleft()\n            else:\n                return c.pop()\n        except IndexError:\n            return None\n\n    while nargs_spec:\n        nargs = _fetch(nargs_spec)\n        if nargs == 1:\n            rv.append(_fetch(args))\n        elif nargs > 1:\n            x = [_fetch(args) for _ in range(nargs)]\n            # If we're reversed, we're pulling in the arguments in reverse,\n            # so we need to turn them around.\n            if spos is not None:\n                x.reverse()\n            rv.append(tuple(x))\n        elif nargs < 0:\n            if spos is not None:\n                raise TypeError(\"Cannot have two nargs < 0\")\n            spos = len(rv)\n            rv.append(None)\n\n    # spos is the position of the wildcard (star).  If it's not `None`,\n    # we fill it with the remainder.\n    if spos is not None:\n        rv[spos] = tuple(args)\n        args = []\n        rv[spos + 1 :] = reversed(rv[spos + 1 :])\n\n    return tuple(rv), list(args)\n\n\ndef _error_opt_args(nargs, opt):\n    if nargs == 1:\n        raise BadOptionUsage(opt, \"{} option requires an argument\".format(opt))\n    raise BadOptionUsage(opt, \"{} option requires {} arguments\".format(opt, nargs))\n\n\ndef split_opt(opt):\n    first = opt[:1]\n    if first.isalnum():\n        return \"\", opt\n    if opt[1:2] == first:\n        return opt[:2], opt[2:]\n    return first, opt[1:]\n\n\ndef normalize_opt(opt, ctx):\n    if ctx is None or ctx.token_normalize_func is None:\n        return opt\n    prefix, opt = split_opt(opt)\n    return prefix + ctx.token_normalize_func(opt)\n\n\ndef split_arg_string(string):\n    \"\"\"Given an argument string this attempts to split it into small parts.\"\"\"\n    rv = []\n    for match in re.finditer(\n        r\"('([^'\\\\]*(?:\\\\.[^'\\\\]*)*)'|\\\"([^\\\"\\\\]*(?:\\\\.[^\\\"\\\\]*)*)\\\"|\\S+)\\s*\",\n        string,\n        re.S,\n    ):\n        arg = match.group().strip()\n        if arg[:1] == arg[-1:] and arg[:1] in \"\\\"'\":\n            arg = arg[1:-1].encode(\"ascii\", \"backslashreplace\").decode(\"unicode-escape\")\n        try:\n            arg = type(string)(arg)\n        except UnicodeError:\n            pass\n        rv.append(arg)\n    return rv\n\n\nclass Option(object):\n    def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None):\n        self._short_opts = []\n        self._long_opts = []\n        self.prefixes = set()\n\n        for opt in opts:\n            prefix, value = split_opt(opt)\n            if not prefix:\n                raise ValueError(\"Invalid start character for option ({})\".format(opt))\n            self.prefixes.add(prefix[0])\n            if len(prefix) == 1 and len(value) == 1:\n                self._short_opts.append(opt)\n            else:\n                self._long_opts.append(opt)\n                self.prefixes.add(prefix)\n\n        if action is None:\n            action = \"store\"\n\n        self.dest = dest\n        self.action = action\n        self.nargs = nargs\n        self.const = const\n        self.obj = obj\n\n    @property\n    def takes_value(self):\n        return self.action in (\"store\", \"append\")\n\n    def process(self, value, state):\n        if self.action == \"store\":\n            state.opts[self.dest] = value\n        elif self.action == \"store_const\":\n            state.opts[self.dest] = self.const\n        elif self.action == \"append\":\n            state.opts.setdefault(self.dest, []).append(value)\n        elif self.action == \"append_const\":\n            state.opts.setdefault(self.dest, []).append(self.const)\n        elif self.action == \"count\":\n            state.opts[self.dest] = state.opts.get(self.dest, 0) + 1\n        else:\n            raise ValueError(\"unknown action '{}'\".format(self.action))\n        state.order.append(self.obj)\n\n\nclass Argument(object):\n    def __init__(self, dest, nargs=1, obj=None):\n        self.dest = dest\n        self.nargs = nargs\n        self.obj = obj\n\n    def process(self, value, state):\n        if self.nargs > 1:\n            holes = sum(1 for x in value if x is None)\n            if holes == len(value):\n                value = None\n            elif holes != 0:\n                raise BadArgumentUsage(\n                    \"argument {} takes {} values\".format(self.dest, self.nargs)\n                )\n        state.opts[self.dest] = value\n        state.order.append(self.obj)\n\n\nclass ParsingState(object):\n    def __init__(self, rargs):\n        self.opts = {}\n        self.largs = []\n        self.rargs = rargs\n        self.order = []\n\n\nclass OptionParser(object):\n    \"\"\"The option parser is an internal class that is ultimately used to\n    parse options and arguments.  It's modelled after optparse and brings\n    a similar but vastly simplified API.  It should generally not be used\n    directly as the high level Click classes wrap it for you.\n\n    It's not nearly as extensible as optparse or argparse as it does not\n    implement features that are implemented on a higher level (such as\n    types or defaults).\n\n    :param ctx: optionally the :class:`~click.Context` where this parser\n                should go with.\n    \"\"\"\n\n    def __init__(self, ctx=None):\n        #: The :class:`~click.Context` for this parser.  This might be\n        #: `None` for some advanced use cases.\n        self.ctx = ctx\n        #: This controls how the parser deals with interspersed arguments.\n        #: If this is set to `False`, the parser will stop on the first\n        #: non-option.  Click uses this to implement nested subcommands\n        #: safely.\n        self.allow_interspersed_args = True\n        #: This tells the parser how to deal with unknown options.  By\n        #: default it will error out (which is sensible), but there is a\n        #: second mode where it will ignore it and continue processing\n        #: after shifting all the unknown options into the resulting args.\n        self.ignore_unknown_options = False\n        if ctx is not None:\n            self.allow_interspersed_args = ctx.allow_interspersed_args\n            self.ignore_unknown_options = ctx.ignore_unknown_options\n        self._short_opt = {}\n        self._long_opt = {}\n        self._opt_prefixes = {\"-\", \"--\"}\n        self._args = []\n\n    def add_option(self, opts, dest, action=None, nargs=1, const=None, obj=None):\n        \"\"\"Adds a new option named `dest` to the parser.  The destination\n        is not inferred (unlike with optparse) and needs to be explicitly\n        provided.  Action can be any of ``store``, ``store_const``,\n        ``append``, ``appnd_const`` or ``count``.\n\n        The `obj` can be used to identify the option in the order list\n        that is returned from the parser.\n        \"\"\"\n        if obj is None:\n            obj = dest\n        opts = [normalize_opt(opt, self.ctx) for opt in opts]\n        option = Option(opts, dest, action=action, nargs=nargs, const=const, obj=obj)\n        self._opt_prefixes.update(option.prefixes)\n        for opt in option._short_opts:\n            self._short_opt[opt] = option\n        for opt in option._long_opts:\n            self._long_opt[opt] = option\n\n    def add_argument(self, dest, nargs=1, obj=None):\n        \"\"\"Adds a positional argument named `dest` to the parser.\n\n        The `obj` can be used to identify the option in the order list\n        that is returned from the parser.\n        \"\"\"\n        if obj is None:\n            obj = dest\n        self._args.append(Argument(dest=dest, nargs=nargs, obj=obj))\n\n    def parse_args(self, args):\n        \"\"\"Parses positional arguments and returns ``(values, args, order)``\n        for the parsed options and arguments as well as the leftover\n        arguments if there are any.  The order is a list of objects as they\n        appear on the command line.  If arguments appear multiple times they\n        will be memorized multiple times as well.\n        \"\"\"\n        state = ParsingState(args)\n        try:\n            self._process_args_for_options(state)\n            self._process_args_for_args(state)\n        except UsageError:\n            if self.ctx is None or not self.ctx.resilient_parsing:\n                raise\n        return state.opts, state.largs, state.order\n\n    def _process_args_for_args(self, state):\n        pargs, args = _unpack_args(\n            state.largs + state.rargs, [x.nargs for x in self._args]\n        )\n\n        for idx, arg in enumerate(self._args):\n            arg.process(pargs[idx], state)\n\n        state.largs = args\n        state.rargs = []\n\n    def _process_args_for_options(self, state):\n        while state.rargs:\n            arg = state.rargs.pop(0)\n            arglen = len(arg)\n            # Double dashes always handled explicitly regardless of what\n            # prefixes are valid.\n            if arg == \"--\":\n                return\n            elif arg[:1] in self._opt_prefixes and arglen > 1:\n                self._process_opts(arg, state)\n            elif self.allow_interspersed_args:\n                state.largs.append(arg)\n            else:\n                state.rargs.insert(0, arg)\n                return\n\n        # Say this is the original argument list:\n        # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]\n        #                            ^\n        # (we are about to process arg(i)).\n        #\n        # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of\n        # [arg0, ..., arg(i-1)] (any options and their arguments will have\n        # been removed from largs).\n        #\n        # The while loop will usually consume 1 or more arguments per pass.\n        # If it consumes 1 (eg. arg is an option that takes no arguments),\n        # then after _process_arg() is done the situation is:\n        #\n        #   largs = subset of [arg0, ..., arg(i)]\n        #   rargs = [arg(i+1), ..., arg(N-1)]\n        #\n        # If allow_interspersed_args is false, largs will always be\n        # *empty* -- still a subset of [arg0, ..., arg(i-1)], but\n        # not a very interesting subset!\n\n    def _match_long_opt(self, opt, explicit_value, state):\n        if opt not in self._long_opt:\n            possibilities = [word for word in self._long_opt if word.startswith(opt)]\n            raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)\n\n        option = self._long_opt[opt]\n        if option.takes_value:\n            # At this point it's safe to modify rargs by injecting the\n            # explicit value, because no exception is raised in this\n            # branch.  This means that the inserted value will be fully\n            # consumed.\n            if explicit_value is not None:\n                state.rargs.insert(0, explicit_value)\n\n            nargs = option.nargs\n            if len(state.rargs) < nargs:\n                _error_opt_args(nargs, opt)\n            elif nargs == 1:\n                value = state.rargs.pop(0)\n            else:\n                value = tuple(state.rargs[:nargs])\n                del state.rargs[:nargs]\n\n        elif explicit_value is not None:\n            raise BadOptionUsage(opt, \"{} option does not take a value\".format(opt))\n\n        else:\n            value = None\n\n        option.process(value, state)\n\n    def _match_short_opt(self, arg, state):\n        stop = False\n        i = 1\n        prefix = arg[0]\n        unknown_options = []\n\n        for ch in arg[1:]:\n            opt = normalize_opt(prefix + ch, self.ctx)\n            option = self._short_opt.get(opt)\n            i += 1\n\n            if not option:\n                if self.ignore_unknown_options:\n                    unknown_options.append(ch)\n                    continue\n                raise NoSuchOption(opt, ctx=self.ctx)\n            if option.takes_value:\n                # Any characters left in arg?  Pretend they're the\n                # next arg, and stop consuming characters of arg.\n                if i < len(arg):\n                    state.rargs.insert(0, arg[i:])\n                    stop = True\n\n                nargs = option.nargs\n                if len(state.rargs) < nargs:\n                    _error_opt_args(nargs, opt)\n                elif nargs == 1:\n                    value = state.rargs.pop(0)\n                else:\n                    value = tuple(state.rargs[:nargs])\n                    del state.rargs[:nargs]\n\n            else:\n                value = None\n\n            option.process(value, state)\n\n            if stop:\n                break\n\n        # If we got any unknown options we re-combinate the string of the\n        # remaining options and re-attach the prefix, then report that\n        # to the state as new larg.  This way there is basic combinatorics\n        # that can be achieved while still ignoring unknown arguments.\n        if self.ignore_unknown_options and unknown_options:\n            state.largs.append(\"{}{}\".format(prefix, \"\".join(unknown_options)))\n\n    def _process_opts(self, arg, state):\n        explicit_value = None\n        # Long option handling happens in two parts.  The first part is\n        # supporting explicitly attached values.  In any case, we will try\n        # to long match the option first.\n        if \"=\" in arg:\n            long_opt, explicit_value = arg.split(\"=\", 1)\n        else:\n            long_opt = arg\n        norm_long_opt = normalize_opt(long_opt, self.ctx)\n\n        # At this point we will match the (assumed) long option through\n        # the long option matching code.  Note that this allows options\n        # like \"-foo\" to be matched as long options.\n        try:\n            self._match_long_opt(norm_long_opt, explicit_value, state)\n        except NoSuchOption:\n            # At this point the long option matching failed, and we need\n            # to try with short options.  However there is a special rule\n            # which says, that if we have a two character options prefix\n            # (applies to \"--foo\" for instance), we do not dispatch to the\n            # short option code and will instead raise the no option\n            # error.\n            if arg[:2] not in self._opt_prefixes:\n                return self._match_short_opt(arg, state)\n            if not self.ignore_unknown_options:\n                raise\n            state.largs.append(arg)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/termui.py",
    "content": "import inspect\nimport io\nimport itertools\nimport os\nimport struct\nimport sys\n\nfrom ._compat import DEFAULT_COLUMNS\nfrom ._compat import get_winterm_size\nfrom ._compat import isatty\nfrom ._compat import raw_input\nfrom ._compat import string_types\nfrom ._compat import strip_ansi\nfrom ._compat import text_type\nfrom ._compat import WIN\nfrom .exceptions import Abort\nfrom .exceptions import UsageError\nfrom .globals import resolve_color_default\nfrom .types import Choice\nfrom .types import convert_type\nfrom .types import Path\nfrom .utils import echo\nfrom .utils import LazyFile\n\n# The prompt functions to use.  The doc tools currently override these\n# functions to customize how they work.\nvisible_prompt_func = raw_input\n\n_ansi_colors = {\n    \"black\": 30,\n    \"red\": 31,\n    \"green\": 32,\n    \"yellow\": 33,\n    \"blue\": 34,\n    \"magenta\": 35,\n    \"cyan\": 36,\n    \"white\": 37,\n    \"reset\": 39,\n    \"bright_black\": 90,\n    \"bright_red\": 91,\n    \"bright_green\": 92,\n    \"bright_yellow\": 93,\n    \"bright_blue\": 94,\n    \"bright_magenta\": 95,\n    \"bright_cyan\": 96,\n    \"bright_white\": 97,\n}\n_ansi_reset_all = \"\\033[0m\"\n\n\ndef hidden_prompt_func(prompt):\n    import getpass\n\n    return getpass.getpass(prompt)\n\n\ndef _build_prompt(\n    text, suffix, show_default=False, default=None, show_choices=True, type=None\n):\n    prompt = text\n    if type is not None and show_choices and isinstance(type, Choice):\n        prompt += \" ({})\".format(\", \".join(map(str, type.choices)))\n    if default is not None and show_default:\n        prompt = \"{} [{}]\".format(prompt, _format_default(default))\n    return prompt + suffix\n\n\ndef _format_default(default):\n    if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, \"name\"):\n        return default.name\n\n    return default\n\n\ndef prompt(\n    text,\n    default=None,\n    hide_input=False,\n    confirmation_prompt=False,\n    type=None,\n    value_proc=None,\n    prompt_suffix=\": \",\n    show_default=True,\n    err=False,\n    show_choices=True,\n):\n    \"\"\"Prompts a user for input.  This is a convenience function that can\n    be used to prompt a user for input later.\n\n    If the user aborts the input by sending a interrupt signal, this\n    function will catch it and raise a :exc:`Abort` exception.\n\n    .. versionadded:: 7.0\n       Added the show_choices parameter.\n\n    .. versionadded:: 6.0\n       Added unicode support for cmd.exe on Windows.\n\n    .. versionadded:: 4.0\n       Added the `err` parameter.\n\n    :param text: the text to show for the prompt.\n    :param default: the default value to use if no input happens.  If this\n                    is not given it will prompt until it's aborted.\n    :param hide_input: if this is set to true then the input value will\n                       be hidden.\n    :param confirmation_prompt: asks for confirmation for the value.\n    :param type: the type to use to check the value against.\n    :param value_proc: if this parameter is provided it's a function that\n                       is invoked instead of the type conversion to\n                       convert a value.\n    :param prompt_suffix: a suffix that should be added to the prompt.\n    :param show_default: shows or hides the default value in the prompt.\n    :param err: if set to true the file defaults to ``stderr`` instead of\n                ``stdout``, the same as with echo.\n    :param show_choices: Show or hide choices if the passed type is a Choice.\n                         For example if type is a Choice of either day or week,\n                         show_choices is true and text is \"Group by\" then the\n                         prompt will be \"Group by (day, week): \".\n    \"\"\"\n    result = None\n\n    def prompt_func(text):\n        f = hidden_prompt_func if hide_input else visible_prompt_func\n        try:\n            # Write the prompt separately so that we get nice\n            # coloring through colorama on Windows\n            echo(text, nl=False, err=err)\n            return f(\"\")\n        except (KeyboardInterrupt, EOFError):\n            # getpass doesn't print a newline if the user aborts input with ^C.\n            # Allegedly this behavior is inherited from getpass(3).\n            # A doc bug has been filed at https://bugs.python.org/issue24711\n            if hide_input:\n                echo(None, err=err)\n            raise Abort()\n\n    if value_proc is None:\n        value_proc = convert_type(type, default)\n\n    prompt = _build_prompt(\n        text, prompt_suffix, show_default, default, show_choices, type\n    )\n\n    while 1:\n        while 1:\n            value = prompt_func(prompt)\n            if value:\n                break\n            elif default is not None:\n                if isinstance(value_proc, Path):\n                    # validate Path default value(exists, dir_okay etc.)\n                    value = default\n                    break\n                return default\n        try:\n            result = value_proc(value)\n        except UsageError as e:\n            echo(\"Error: {}\".format(e.message), err=err)  # noqa: B306\n            continue\n        if not confirmation_prompt:\n            return result\n        while 1:\n            value2 = prompt_func(\"Repeat for confirmation: \")\n            if value2:\n                break\n        if value == value2:\n            return result\n        echo(\"Error: the two entered values do not match\", err=err)\n\n\ndef confirm(\n    text, default=False, abort=False, prompt_suffix=\": \", show_default=True, err=False\n):\n    \"\"\"Prompts for confirmation (yes/no question).\n\n    If the user aborts the input by sending a interrupt signal this\n    function will catch it and raise a :exc:`Abort` exception.\n\n    .. versionadded:: 4.0\n       Added the `err` parameter.\n\n    :param text: the question to ask.\n    :param default: the default for the prompt.\n    :param abort: if this is set to `True` a negative answer aborts the\n                  exception by raising :exc:`Abort`.\n    :param prompt_suffix: a suffix that should be added to the prompt.\n    :param show_default: shows or hides the default value in the prompt.\n    :param err: if set to true the file defaults to ``stderr`` instead of\n                ``stdout``, the same as with echo.\n    \"\"\"\n    prompt = _build_prompt(\n        text, prompt_suffix, show_default, \"Y/n\" if default else \"y/N\"\n    )\n    while 1:\n        try:\n            # Write the prompt separately so that we get nice\n            # coloring through colorama on Windows\n            echo(prompt, nl=False, err=err)\n            value = visible_prompt_func(\"\").lower().strip()\n        except (KeyboardInterrupt, EOFError):\n            raise Abort()\n        if value in (\"y\", \"yes\"):\n            rv = True\n        elif value in (\"n\", \"no\"):\n            rv = False\n        elif value == \"\":\n            rv = default\n        else:\n            echo(\"Error: invalid input\", err=err)\n            continue\n        break\n    if abort and not rv:\n        raise Abort()\n    return rv\n\n\ndef get_terminal_size():\n    \"\"\"Returns the current size of the terminal as tuple in the form\n    ``(width, height)`` in columns and rows.\n    \"\"\"\n    # If shutil has get_terminal_size() (Python 3.3 and later) use that\n    if sys.version_info >= (3, 3):\n        import shutil\n\n        shutil_get_terminal_size = getattr(shutil, \"get_terminal_size\", None)\n        if shutil_get_terminal_size:\n            sz = shutil_get_terminal_size()\n            return sz.columns, sz.lines\n\n    # We provide a sensible default for get_winterm_size() when being invoked\n    # inside a subprocess. Without this, it would not provide a useful input.\n    if get_winterm_size is not None:\n        size = get_winterm_size()\n        if size == (0, 0):\n            return (79, 24)\n        else:\n            return size\n\n    def ioctl_gwinsz(fd):\n        try:\n            import fcntl\n            import termios\n\n            cr = struct.unpack(\"hh\", fcntl.ioctl(fd, termios.TIOCGWINSZ, \"1234\"))\n        except Exception:\n            return\n        return cr\n\n    cr = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2)\n    if not cr:\n        try:\n            fd = os.open(os.ctermid(), os.O_RDONLY)\n            try:\n                cr = ioctl_gwinsz(fd)\n            finally:\n                os.close(fd)\n        except Exception:\n            pass\n    if not cr or not cr[0] or not cr[1]:\n        cr = (os.environ.get(\"LINES\", 25), os.environ.get(\"COLUMNS\", DEFAULT_COLUMNS))\n    return int(cr[1]), int(cr[0])\n\n\ndef echo_via_pager(text_or_generator, color=None):\n    \"\"\"This function takes a text and shows it via an environment specific\n    pager on stdout.\n\n    .. versionchanged:: 3.0\n       Added the `color` flag.\n\n    :param text_or_generator: the text to page, or alternatively, a\n                              generator emitting the text to page.\n    :param color: controls if the pager supports ANSI colors or not.  The\n                  default is autodetection.\n    \"\"\"\n    color = resolve_color_default(color)\n\n    if inspect.isgeneratorfunction(text_or_generator):\n        i = text_or_generator()\n    elif isinstance(text_or_generator, string_types):\n        i = [text_or_generator]\n    else:\n        i = iter(text_or_generator)\n\n    # convert every element of i to a text type if necessary\n    text_generator = (el if isinstance(el, string_types) else text_type(el) for el in i)\n\n    from ._termui_impl import pager\n\n    return pager(itertools.chain(text_generator, \"\\n\"), color)\n\n\ndef progressbar(\n    iterable=None,\n    length=None,\n    label=None,\n    show_eta=True,\n    show_percent=None,\n    show_pos=False,\n    item_show_func=None,\n    fill_char=\"#\",\n    empty_char=\"-\",\n    bar_template=\"%(label)s  [%(bar)s]  %(info)s\",\n    info_sep=\"  \",\n    width=36,\n    file=None,\n    color=None,\n):\n    \"\"\"This function creates an iterable context manager that can be used\n    to iterate over something while showing a progress bar.  It will\n    either iterate over the `iterable` or `length` items (that are counted\n    up).  While iteration happens, this function will print a rendered\n    progress bar to the given `file` (defaults to stdout) and will attempt\n    to calculate remaining time and more.  By default, this progress bar\n    will not be rendered if the file is not a terminal.\n\n    The context manager creates the progress bar.  When the context\n    manager is entered the progress bar is already created.  With every\n    iteration over the progress bar, the iterable passed to the bar is\n    advanced and the bar is updated.  When the context manager exits,\n    a newline is printed and the progress bar is finalized on screen.\n\n    Note: The progress bar is currently designed for use cases where the\n    total progress can be expected to take at least several seconds.\n    Because of this, the ProgressBar class object won't display\n    progress that is considered too fast, and progress where the time\n    between steps is less than a second.\n\n    No printing must happen or the progress bar will be unintentionally\n    destroyed.\n\n    Example usage::\n\n        with progressbar(items) as bar:\n            for item in bar:\n                do_something_with(item)\n\n    Alternatively, if no iterable is specified, one can manually update the\n    progress bar through the `update()` method instead of directly\n    iterating over the progress bar.  The update method accepts the number\n    of steps to increment the bar with::\n\n        with progressbar(length=chunks.total_bytes) as bar:\n            for chunk in chunks:\n                process_chunk(chunk)\n                bar.update(chunks.bytes)\n\n    .. versionadded:: 2.0\n\n    .. versionadded:: 4.0\n       Added the `color` parameter.  Added a `update` method to the\n       progressbar object.\n\n    :param iterable: an iterable to iterate over.  If not provided the length\n                     is required.\n    :param length: the number of items to iterate over.  By default the\n                   progressbar will attempt to ask the iterator about its\n                   length, which might or might not work.  If an iterable is\n                   also provided this parameter can be used to override the\n                   length.  If an iterable is not provided the progress bar\n                   will iterate over a range of that length.\n    :param label: the label to show next to the progress bar.\n    :param show_eta: enables or disables the estimated time display.  This is\n                     automatically disabled if the length cannot be\n                     determined.\n    :param show_percent: enables or disables the percentage display.  The\n                         default is `True` if the iterable has a length or\n                         `False` if not.\n    :param show_pos: enables or disables the absolute position display.  The\n                     default is `False`.\n    :param item_show_func: a function called with the current item which\n                           can return a string to show the current item\n                           next to the progress bar.  Note that the current\n                           item can be `None`!\n    :param fill_char: the character to use to show the filled part of the\n                      progress bar.\n    :param empty_char: the character to use to show the non-filled part of\n                       the progress bar.\n    :param bar_template: the format string to use as template for the bar.\n                         The parameters in it are ``label`` for the label,\n                         ``bar`` for the progress bar and ``info`` for the\n                         info section.\n    :param info_sep: the separator between multiple info items (eta etc.)\n    :param width: the width of the progress bar in characters, 0 means full\n                  terminal width\n    :param file: the file to write to.  If this is not a terminal then\n                 only the label is printed.\n    :param color: controls if the terminal supports ANSI colors or not.  The\n                  default is autodetection.  This is only needed if ANSI\n                  codes are included anywhere in the progress bar output\n                  which is not the case by default.\n    \"\"\"\n    from ._termui_impl import ProgressBar\n\n    color = resolve_color_default(color)\n    return ProgressBar(\n        iterable=iterable,\n        length=length,\n        show_eta=show_eta,\n        show_percent=show_percent,\n        show_pos=show_pos,\n        item_show_func=item_show_func,\n        fill_char=fill_char,\n        empty_char=empty_char,\n        bar_template=bar_template,\n        info_sep=info_sep,\n        file=file,\n        label=label,\n        width=width,\n        color=color,\n    )\n\n\ndef clear():\n    \"\"\"Clears the terminal screen.  This will have the effect of clearing\n    the whole visible space of the terminal and moving the cursor to the\n    top left.  This does not do anything if not connected to a terminal.\n\n    .. versionadded:: 2.0\n    \"\"\"\n    if not isatty(sys.stdout):\n        return\n    # If we're on Windows and we don't have colorama available, then we\n    # clear the screen by shelling out.  Otherwise we can use an escape\n    # sequence.\n    if WIN:\n        os.system(\"cls\")\n    else:\n        sys.stdout.write(\"\\033[2J\\033[1;1H\")\n\n\ndef style(\n    text,\n    fg=None,\n    bg=None,\n    bold=None,\n    dim=None,\n    underline=None,\n    blink=None,\n    reverse=None,\n    reset=True,\n):\n    \"\"\"Styles a text with ANSI styles and returns the new string.  By\n    default the styling is self contained which means that at the end\n    of the string a reset code is issued.  This can be prevented by\n    passing ``reset=False``.\n\n    Examples::\n\n        click.echo(click.style('Hello World!', fg='green'))\n        click.echo(click.style('ATTENTION!', blink=True))\n        click.echo(click.style('Some things', reverse=True, fg='cyan'))\n\n    Supported color names:\n\n    * ``black`` (might be a gray)\n    * ``red``\n    * ``green``\n    * ``yellow`` (might be an orange)\n    * ``blue``\n    * ``magenta``\n    * ``cyan``\n    * ``white`` (might be light gray)\n    * ``bright_black``\n    * ``bright_red``\n    * ``bright_green``\n    * ``bright_yellow``\n    * ``bright_blue``\n    * ``bright_magenta``\n    * ``bright_cyan``\n    * ``bright_white``\n    * ``reset`` (reset the color code only)\n\n    .. versionadded:: 2.0\n\n    .. versionadded:: 7.0\n       Added support for bright colors.\n\n    :param text: the string to style with ansi codes.\n    :param fg: if provided this will become the foreground color.\n    :param bg: if provided this will become the background color.\n    :param bold: if provided this will enable or disable bold mode.\n    :param dim: if provided this will enable or disable dim mode.  This is\n                badly supported.\n    :param underline: if provided this will enable or disable underline.\n    :param blink: if provided this will enable or disable blinking.\n    :param reverse: if provided this will enable or disable inverse\n                    rendering (foreground becomes background and the\n                    other way round).\n    :param reset: by default a reset-all code is added at the end of the\n                  string which means that styles do not carry over.  This\n                  can be disabled to compose styles.\n    \"\"\"\n    bits = []\n    if fg:\n        try:\n            bits.append(\"\\033[{}m\".format(_ansi_colors[fg]))\n        except KeyError:\n            raise TypeError(\"Unknown color '{}'\".format(fg))\n    if bg:\n        try:\n            bits.append(\"\\033[{}m\".format(_ansi_colors[bg] + 10))\n        except KeyError:\n            raise TypeError(\"Unknown color '{}'\".format(bg))\n    if bold is not None:\n        bits.append(\"\\033[{}m\".format(1 if bold else 22))\n    if dim is not None:\n        bits.append(\"\\033[{}m\".format(2 if dim else 22))\n    if underline is not None:\n        bits.append(\"\\033[{}m\".format(4 if underline else 24))\n    if blink is not None:\n        bits.append(\"\\033[{}m\".format(5 if blink else 25))\n    if reverse is not None:\n        bits.append(\"\\033[{}m\".format(7 if reverse else 27))\n    bits.append(text)\n    if reset:\n        bits.append(_ansi_reset_all)\n    return \"\".join(bits)\n\n\ndef unstyle(text):\n    \"\"\"Removes ANSI styling information from a string.  Usually it's not\n    necessary to use this function as Click's echo function will\n    automatically remove styling if necessary.\n\n    .. versionadded:: 2.0\n\n    :param text: the text to remove style information from.\n    \"\"\"\n    return strip_ansi(text)\n\n\ndef secho(message=None, file=None, nl=True, err=False, color=None, **styles):\n    \"\"\"This function combines :func:`echo` and :func:`style` into one\n    call.  As such the following two calls are the same::\n\n        click.secho('Hello World!', fg='green')\n        click.echo(click.style('Hello World!', fg='green'))\n\n    All keyword arguments are forwarded to the underlying functions\n    depending on which one they go with.\n\n    .. versionadded:: 2.0\n    \"\"\"\n    if message is not None:\n        message = style(message, **styles)\n    return echo(message, file=file, nl=nl, err=err, color=color)\n\n\ndef edit(\n    text=None, editor=None, env=None, require_save=True, extension=\".txt\", filename=None\n):\n    r\"\"\"Edits the given text in the defined editor.  If an editor is given\n    (should be the full path to the executable but the regular operating\n    system search path is used for finding the executable) it overrides\n    the detected editor.  Optionally, some environment variables can be\n    used.  If the editor is closed without changes, `None` is returned.  In\n    case a file is edited directly the return value is always `None` and\n    `require_save` and `extension` are ignored.\n\n    If the editor cannot be opened a :exc:`UsageError` is raised.\n\n    Note for Windows: to simplify cross-platform usage, the newlines are\n    automatically converted from POSIX to Windows and vice versa.  As such,\n    the message here will have ``\\n`` as newline markers.\n\n    :param text: the text to edit.\n    :param editor: optionally the editor to use.  Defaults to automatic\n                   detection.\n    :param env: environment variables to forward to the editor.\n    :param require_save: if this is true, then not saving in the editor\n                         will make the return value become `None`.\n    :param extension: the extension to tell the editor about.  This defaults\n                      to `.txt` but changing this might change syntax\n                      highlighting.\n    :param filename: if provided it will edit this file instead of the\n                     provided text contents.  It will not use a temporary\n                     file as an indirection in that case.\n    \"\"\"\n    from ._termui_impl import Editor\n\n    editor = Editor(\n        editor=editor, env=env, require_save=require_save, extension=extension\n    )\n    if filename is None:\n        return editor.edit(text)\n    editor.edit_file(filename)\n\n\ndef launch(url, wait=False, locate=False):\n    \"\"\"This function launches the given URL (or filename) in the default\n    viewer application for this file type.  If this is an executable, it\n    might launch the executable in a new session.  The return value is\n    the exit code of the launched application.  Usually, ``0`` indicates\n    success.\n\n    Examples::\n\n        click.launch('https://click.palletsprojects.com/')\n        click.launch('/my/downloaded/file', locate=True)\n\n    .. versionadded:: 2.0\n\n    :param url: URL or filename of the thing to launch.\n    :param wait: waits for the program to stop.\n    :param locate: if this is set to `True` then instead of launching the\n                   application associated with the URL it will attempt to\n                   launch a file manager with the file located.  This\n                   might have weird effects if the URL does not point to\n                   the filesystem.\n    \"\"\"\n    from ._termui_impl import open_url\n\n    return open_url(url, wait=wait, locate=locate)\n\n\n# If this is provided, getchar() calls into this instead.  This is used\n# for unittesting purposes.\n_getchar = None\n\n\ndef getchar(echo=False):\n    \"\"\"Fetches a single character from the terminal and returns it.  This\n    will always return a unicode character and under certain rare\n    circumstances this might return more than one character.  The\n    situations which more than one character is returned is when for\n    whatever reason multiple characters end up in the terminal buffer or\n    standard input was not actually a terminal.\n\n    Note that this will always read from the terminal, even if something\n    is piped into the standard input.\n\n    Note for Windows: in rare cases when typing non-ASCII characters, this\n    function might wait for a second character and then return both at once.\n    This is because certain Unicode characters look like special-key markers.\n\n    .. versionadded:: 2.0\n\n    :param echo: if set to `True`, the character read will also show up on\n                 the terminal.  The default is to not show it.\n    \"\"\"\n    f = _getchar\n    if f is None:\n        from ._termui_impl import getchar as f\n    return f(echo)\n\n\ndef raw_terminal():\n    from ._termui_impl import raw_terminal as f\n\n    return f()\n\n\ndef pause(info=\"Press any key to continue ...\", err=False):\n    \"\"\"This command stops execution and waits for the user to press any\n    key to continue.  This is similar to the Windows batch \"pause\"\n    command.  If the program is not run through a terminal, this command\n    will instead do nothing.\n\n    .. versionadded:: 2.0\n\n    .. versionadded:: 4.0\n       Added the `err` parameter.\n\n    :param info: the info string to print before pausing.\n    :param err: if set to message goes to ``stderr`` instead of\n                ``stdout``, the same as with echo.\n    \"\"\"\n    if not isatty(sys.stdin) or not isatty(sys.stdout):\n        return\n    try:\n        if info:\n            echo(info, nl=False, err=err)\n        try:\n            getchar()\n        except (KeyboardInterrupt, EOFError):\n            pass\n    finally:\n        if info:\n            echo(err=err)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/testing.py",
    "content": "import contextlib\nimport os\nimport shlex\nimport shutil\nimport sys\nimport tempfile\n\nfrom . import formatting\nfrom . import termui\nfrom . import utils\nfrom ._compat import iteritems\nfrom ._compat import PY2\nfrom ._compat import string_types\n\n\nif PY2:\n    from cStringIO import StringIO\nelse:\n    import io\n    from ._compat import _find_binary_reader\n\n\nclass EchoingStdin(object):\n    def __init__(self, input, output):\n        self._input = input\n        self._output = output\n\n    def __getattr__(self, x):\n        return getattr(self._input, x)\n\n    def _echo(self, rv):\n        self._output.write(rv)\n        return rv\n\n    def read(self, n=-1):\n        return self._echo(self._input.read(n))\n\n    def readline(self, n=-1):\n        return self._echo(self._input.readline(n))\n\n    def readlines(self):\n        return [self._echo(x) for x in self._input.readlines()]\n\n    def __iter__(self):\n        return iter(self._echo(x) for x in self._input)\n\n    def __repr__(self):\n        return repr(self._input)\n\n\ndef make_input_stream(input, charset):\n    # Is already an input stream.\n    if hasattr(input, \"read\"):\n        if PY2:\n            return input\n        rv = _find_binary_reader(input)\n        if rv is not None:\n            return rv\n        raise TypeError(\"Could not find binary reader for input stream.\")\n\n    if input is None:\n        input = b\"\"\n    elif not isinstance(input, bytes):\n        input = input.encode(charset)\n    if PY2:\n        return StringIO(input)\n    return io.BytesIO(input)\n\n\nclass Result(object):\n    \"\"\"Holds the captured result of an invoked CLI script.\"\"\"\n\n    def __init__(\n        self, runner, stdout_bytes, stderr_bytes, exit_code, exception, exc_info=None\n    ):\n        #: The runner that created the result\n        self.runner = runner\n        #: The standard output as bytes.\n        self.stdout_bytes = stdout_bytes\n        #: The standard error as bytes, or None if not available\n        self.stderr_bytes = stderr_bytes\n        #: The exit code as integer.\n        self.exit_code = exit_code\n        #: The exception that happened if one did.\n        self.exception = exception\n        #: The traceback\n        self.exc_info = exc_info\n\n    @property\n    def output(self):\n        \"\"\"The (standard) output as unicode string.\"\"\"\n        return self.stdout\n\n    @property\n    def stdout(self):\n        \"\"\"The standard output as unicode string.\"\"\"\n        return self.stdout_bytes.decode(self.runner.charset, \"replace\").replace(\n            \"\\r\\n\", \"\\n\"\n        )\n\n    @property\n    def stderr(self):\n        \"\"\"The standard error as unicode string.\"\"\"\n        if self.stderr_bytes is None:\n            raise ValueError(\"stderr not separately captured\")\n        return self.stderr_bytes.decode(self.runner.charset, \"replace\").replace(\n            \"\\r\\n\", \"\\n\"\n        )\n\n    def __repr__(self):\n        return \"<{} {}>\".format(\n            type(self).__name__, repr(self.exception) if self.exception else \"okay\"\n        )\n\n\nclass CliRunner(object):\n    \"\"\"The CLI runner provides functionality to invoke a Click command line\n    script for unittesting purposes in a isolated environment.  This only\n    works in single-threaded systems without any concurrency as it changes the\n    global interpreter state.\n\n    :param charset: the character set for the input and output data.  This is\n                    UTF-8 by default and should not be changed currently as\n                    the reporting to Click only works in Python 2 properly.\n    :param env: a dictionary with environment variables for overriding.\n    :param echo_stdin: if this is set to `True`, then reading from stdin writes\n                       to stdout.  This is useful for showing examples in\n                       some circumstances.  Note that regular prompts\n                       will automatically echo the input.\n    :param mix_stderr: if this is set to `False`, then stdout and stderr are\n                       preserved as independent streams.  This is useful for\n                       Unix-philosophy apps that have predictable stdout and\n                       noisy stderr, such that each may be measured\n                       independently\n    \"\"\"\n\n    def __init__(self, charset=None, env=None, echo_stdin=False, mix_stderr=True):\n        if charset is None:\n            charset = \"utf-8\"\n        self.charset = charset\n        self.env = env or {}\n        self.echo_stdin = echo_stdin\n        self.mix_stderr = mix_stderr\n\n    def get_default_prog_name(self, cli):\n        \"\"\"Given a command object it will return the default program name\n        for it.  The default is the `name` attribute or ``\"root\"`` if not\n        set.\n        \"\"\"\n        return cli.name or \"root\"\n\n    def make_env(self, overrides=None):\n        \"\"\"Returns the environment overrides for invoking a script.\"\"\"\n        rv = dict(self.env)\n        if overrides:\n            rv.update(overrides)\n        return rv\n\n    @contextlib.contextmanager\n    def isolation(self, input=None, env=None, color=False):\n        \"\"\"A context manager that sets up the isolation for invoking of a\n        command line tool.  This sets up stdin with the given input data\n        and `os.environ` with the overrides from the given dictionary.\n        This also rebinds some internals in Click to be mocked (like the\n        prompt functionality).\n\n        This is automatically done in the :meth:`invoke` method.\n\n        .. versionadded:: 4.0\n           The ``color`` parameter was added.\n\n        :param input: the input stream to put into sys.stdin.\n        :param env: the environment overrides as dictionary.\n        :param color: whether the output should contain color codes. The\n                      application can still override this explicitly.\n        \"\"\"\n        input = make_input_stream(input, self.charset)\n\n        old_stdin = sys.stdin\n        old_stdout = sys.stdout\n        old_stderr = sys.stderr\n        old_forced_width = formatting.FORCED_WIDTH\n        formatting.FORCED_WIDTH = 80\n\n        env = self.make_env(env)\n\n        if PY2:\n            bytes_output = StringIO()\n            if self.echo_stdin:\n                input = EchoingStdin(input, bytes_output)\n            sys.stdout = bytes_output\n            if not self.mix_stderr:\n                bytes_error = StringIO()\n                sys.stderr = bytes_error\n        else:\n            bytes_output = io.BytesIO()\n            if self.echo_stdin:\n                input = EchoingStdin(input, bytes_output)\n            input = io.TextIOWrapper(input, encoding=self.charset)\n            sys.stdout = io.TextIOWrapper(bytes_output, encoding=self.charset)\n            if not self.mix_stderr:\n                bytes_error = io.BytesIO()\n                sys.stderr = io.TextIOWrapper(bytes_error, encoding=self.charset)\n\n        if self.mix_stderr:\n            sys.stderr = sys.stdout\n\n        sys.stdin = input\n\n        def visible_input(prompt=None):\n            sys.stdout.write(prompt or \"\")\n            val = input.readline().rstrip(\"\\r\\n\")\n            sys.stdout.write(\"{}\\n\".format(val))\n            sys.stdout.flush()\n            return val\n\n        def hidden_input(prompt=None):\n            sys.stdout.write(\"{}\\n\".format(prompt or \"\"))\n            sys.stdout.flush()\n            return input.readline().rstrip(\"\\r\\n\")\n\n        def _getchar(echo):\n            char = sys.stdin.read(1)\n            if echo:\n                sys.stdout.write(char)\n                sys.stdout.flush()\n            return char\n\n        default_color = color\n\n        def should_strip_ansi(stream=None, color=None):\n            if color is None:\n                return not default_color\n            return not color\n\n        old_visible_prompt_func = termui.visible_prompt_func\n        old_hidden_prompt_func = termui.hidden_prompt_func\n        old__getchar_func = termui._getchar\n        old_should_strip_ansi = utils.should_strip_ansi\n        termui.visible_prompt_func = visible_input\n        termui.hidden_prompt_func = hidden_input\n        termui._getchar = _getchar\n        utils.should_strip_ansi = should_strip_ansi\n\n        old_env = {}\n        try:\n            for key, value in iteritems(env):\n                old_env[key] = os.environ.get(key)\n                if value is None:\n                    try:\n                        del os.environ[key]\n                    except Exception:\n                        pass\n                else:\n                    os.environ[key] = value\n            yield (bytes_output, not self.mix_stderr and bytes_error)\n        finally:\n            for key, value in iteritems(old_env):\n                if value is None:\n                    try:\n                        del os.environ[key]\n                    except Exception:\n                        pass\n                else:\n                    os.environ[key] = value\n            sys.stdout = old_stdout\n            sys.stderr = old_stderr\n            sys.stdin = old_stdin\n            termui.visible_prompt_func = old_visible_prompt_func\n            termui.hidden_prompt_func = old_hidden_prompt_func\n            termui._getchar = old__getchar_func\n            utils.should_strip_ansi = old_should_strip_ansi\n            formatting.FORCED_WIDTH = old_forced_width\n\n    def invoke(\n        self,\n        cli,\n        args=None,\n        input=None,\n        env=None,\n        catch_exceptions=True,\n        color=False,\n        **extra\n    ):\n        \"\"\"Invokes a command in an isolated environment.  The arguments are\n        forwarded directly to the command line script, the `extra` keyword\n        arguments are passed to the :meth:`~clickpkg.Command.main` function of\n        the command.\n\n        This returns a :class:`Result` object.\n\n        .. versionadded:: 3.0\n           The ``catch_exceptions`` parameter was added.\n\n        .. versionchanged:: 3.0\n           The result object now has an `exc_info` attribute with the\n           traceback if available.\n\n        .. versionadded:: 4.0\n           The ``color`` parameter was added.\n\n        :param cli: the command to invoke\n        :param args: the arguments to invoke. It may be given as an iterable\n                     or a string. When given as string it will be interpreted\n                     as a Unix shell command. More details at\n                     :func:`shlex.split`.\n        :param input: the input data for `sys.stdin`.\n        :param env: the environment overrides.\n        :param catch_exceptions: Whether to catch any other exceptions than\n                                 ``SystemExit``.\n        :param extra: the keyword arguments to pass to :meth:`main`.\n        :param color: whether the output should contain color codes. The\n                      application can still override this explicitly.\n        \"\"\"\n        exc_info = None\n        with self.isolation(input=input, env=env, color=color) as outstreams:\n            exception = None\n            exit_code = 0\n\n            if isinstance(args, string_types):\n                args = shlex.split(args)\n\n            try:\n                prog_name = extra.pop(\"prog_name\")\n            except KeyError:\n                prog_name = self.get_default_prog_name(cli)\n\n            try:\n                cli.main(args=args or (), prog_name=prog_name, **extra)\n            except SystemExit as e:\n                exc_info = sys.exc_info()\n                exit_code = e.code\n                if exit_code is None:\n                    exit_code = 0\n\n                if exit_code != 0:\n                    exception = e\n\n                if not isinstance(exit_code, int):\n                    sys.stdout.write(str(exit_code))\n                    sys.stdout.write(\"\\n\")\n                    exit_code = 1\n\n            except Exception as e:\n                if not catch_exceptions:\n                    raise\n                exception = e\n                exit_code = 1\n                exc_info = sys.exc_info()\n            finally:\n                sys.stdout.flush()\n                stdout = outstreams[0].getvalue()\n                if self.mix_stderr:\n                    stderr = None\n                else:\n                    stderr = outstreams[1].getvalue()\n\n        return Result(\n            runner=self,\n            stdout_bytes=stdout,\n            stderr_bytes=stderr,\n            exit_code=exit_code,\n            exception=exception,\n            exc_info=exc_info,\n        )\n\n    @contextlib.contextmanager\n    def isolated_filesystem(self):\n        \"\"\"A context manager that creates a temporary folder and changes\n        the current working directory to it for isolated filesystem tests.\n        \"\"\"\n        cwd = os.getcwd()\n        t = tempfile.mkdtemp()\n        os.chdir(t)\n        try:\n            yield t\n        finally:\n            os.chdir(cwd)\n            try:\n                shutil.rmtree(t)\n            except (OSError, IOError):  # noqa: B014\n                pass\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/types.py",
    "content": "import os\nimport stat\nfrom datetime import datetime\n\nfrom ._compat import _get_argv_encoding\nfrom ._compat import filename_to_ui\nfrom ._compat import get_filesystem_encoding\nfrom ._compat import get_streerror\nfrom ._compat import open_stream\nfrom ._compat import PY2\nfrom ._compat import text_type\nfrom .exceptions import BadParameter\nfrom .utils import LazyFile\nfrom .utils import safecall\n\n\nclass ParamType(object):\n    \"\"\"Helper for converting values through types.  The following is\n    necessary for a valid type:\n\n    *   it needs a name\n    *   it needs to pass through None unchanged\n    *   it needs to convert from a string\n    *   it needs to convert its result type through unchanged\n        (eg: needs to be idempotent)\n    *   it needs to be able to deal with param and context being `None`.\n        This can be the case when the object is used with prompt\n        inputs.\n    \"\"\"\n\n    is_composite = False\n\n    #: the descriptive name of this type\n    name = None\n\n    #: if a list of this type is expected and the value is pulled from a\n    #: string environment variable, this is what splits it up.  `None`\n    #: means any whitespace.  For all parameters the general rule is that\n    #: whitespace splits them up.  The exception are paths and files which\n    #: are split by ``os.path.pathsep`` by default (\":\" on Unix and \";\" on\n    #: Windows).\n    envvar_list_splitter = None\n\n    def __call__(self, value, param=None, ctx=None):\n        if value is not None:\n            return self.convert(value, param, ctx)\n\n    def get_metavar(self, param):\n        \"\"\"Returns the metavar default for this param if it provides one.\"\"\"\n\n    def get_missing_message(self, param):\n        \"\"\"Optionally might return extra information about a missing\n        parameter.\n\n        .. versionadded:: 2.0\n        \"\"\"\n\n    def convert(self, value, param, ctx):\n        \"\"\"Converts the value.  This is not invoked for values that are\n        `None` (the missing value).\n        \"\"\"\n        return value\n\n    def split_envvar_value(self, rv):\n        \"\"\"Given a value from an environment variable this splits it up\n        into small chunks depending on the defined envvar list splitter.\n\n        If the splitter is set to `None`, which means that whitespace splits,\n        then leading and trailing whitespace is ignored.  Otherwise, leading\n        and trailing splitters usually lead to empty items being included.\n        \"\"\"\n        return (rv or \"\").split(self.envvar_list_splitter)\n\n    def fail(self, message, param=None, ctx=None):\n        \"\"\"Helper method to fail with an invalid value message.\"\"\"\n        raise BadParameter(message, ctx=ctx, param=param)\n\n\nclass CompositeParamType(ParamType):\n    is_composite = True\n\n    @property\n    def arity(self):\n        raise NotImplementedError()\n\n\nclass FuncParamType(ParamType):\n    def __init__(self, func):\n        self.name = func.__name__\n        self.func = func\n\n    def convert(self, value, param, ctx):\n        try:\n            return self.func(value)\n        except ValueError:\n            try:\n                value = text_type(value)\n            except UnicodeError:\n                value = str(value).decode(\"utf-8\", \"replace\")\n            self.fail(value, param, ctx)\n\n\nclass UnprocessedParamType(ParamType):\n    name = \"text\"\n\n    def convert(self, value, param, ctx):\n        return value\n\n    def __repr__(self):\n        return \"UNPROCESSED\"\n\n\nclass StringParamType(ParamType):\n    name = \"text\"\n\n    def convert(self, value, param, ctx):\n        if isinstance(value, bytes):\n            enc = _get_argv_encoding()\n            try:\n                value = value.decode(enc)\n            except UnicodeError:\n                fs_enc = get_filesystem_encoding()\n                if fs_enc != enc:\n                    try:\n                        value = value.decode(fs_enc)\n                    except UnicodeError:\n                        value = value.decode(\"utf-8\", \"replace\")\n                else:\n                    value = value.decode(\"utf-8\", \"replace\")\n            return value\n        return value\n\n    def __repr__(self):\n        return \"STRING\"\n\n\nclass Choice(ParamType):\n    \"\"\"The choice type allows a value to be checked against a fixed set\n    of supported values. All of these values have to be strings.\n\n    You should only pass a list or tuple of choices. Other iterables\n    (like generators) may lead to surprising results.\n\n    The resulting value will always be one of the originally passed choices\n    regardless of ``case_sensitive`` or any ``ctx.token_normalize_func``\n    being specified.\n\n    See :ref:`choice-opts` for an example.\n\n    :param case_sensitive: Set to false to make choices case\n        insensitive. Defaults to true.\n    \"\"\"\n\n    name = \"choice\"\n\n    def __init__(self, choices, case_sensitive=True):\n        self.choices = choices\n        self.case_sensitive = case_sensitive\n\n    def get_metavar(self, param):\n        return \"[{}]\".format(\"|\".join(self.choices))\n\n    def get_missing_message(self, param):\n        return \"Choose from:\\n\\t{}.\".format(\",\\n\\t\".join(self.choices))\n\n    def convert(self, value, param, ctx):\n        # Match through normalization and case sensitivity\n        # first do token_normalize_func, then lowercase\n        # preserve original `value` to produce an accurate message in\n        # `self.fail`\n        normed_value = value\n        normed_choices = {choice: choice for choice in self.choices}\n\n        if ctx is not None and ctx.token_normalize_func is not None:\n            normed_value = ctx.token_normalize_func(value)\n            normed_choices = {\n                ctx.token_normalize_func(normed_choice): original\n                for normed_choice, original in normed_choices.items()\n            }\n\n        if not self.case_sensitive:\n            if PY2:\n                lower = str.lower\n            else:\n                lower = str.casefold\n\n            normed_value = lower(normed_value)\n            normed_choices = {\n                lower(normed_choice): original\n                for normed_choice, original in normed_choices.items()\n            }\n\n        if normed_value in normed_choices:\n            return normed_choices[normed_value]\n\n        self.fail(\n            \"invalid choice: {}. (choose from {})\".format(\n                value, \", \".join(self.choices)\n            ),\n            param,\n            ctx,\n        )\n\n    def __repr__(self):\n        return \"Choice('{}')\".format(list(self.choices))\n\n\nclass DateTime(ParamType):\n    \"\"\"The DateTime type converts date strings into `datetime` objects.\n\n    The format strings which are checked are configurable, but default to some\n    common (non-timezone aware) ISO 8601 formats.\n\n    When specifying *DateTime* formats, you should only pass a list or a tuple.\n    Other iterables, like generators, may lead to surprising results.\n\n    The format strings are processed using ``datetime.strptime``, and this\n    consequently defines the format strings which are allowed.\n\n    Parsing is tried using each format, in order, and the first format which\n    parses successfully is used.\n\n    :param formats: A list or tuple of date format strings, in the order in\n                    which they should be tried. Defaults to\n                    ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``,\n                    ``'%Y-%m-%d %H:%M:%S'``.\n    \"\"\"\n\n    name = \"datetime\"\n\n    def __init__(self, formats=None):\n        self.formats = formats or [\"%Y-%m-%d\", \"%Y-%m-%dT%H:%M:%S\", \"%Y-%m-%d %H:%M:%S\"]\n\n    def get_metavar(self, param):\n        return \"[{}]\".format(\"|\".join(self.formats))\n\n    def _try_to_convert_date(self, value, format):\n        try:\n            return datetime.strptime(value, format)\n        except ValueError:\n            return None\n\n    def convert(self, value, param, ctx):\n        # Exact match\n        for format in self.formats:\n            dtime = self._try_to_convert_date(value, format)\n            if dtime:\n                return dtime\n\n        self.fail(\n            \"invalid datetime format: {}. (choose from {})\".format(\n                value, \", \".join(self.formats)\n            )\n        )\n\n    def __repr__(self):\n        return \"DateTime\"\n\n\nclass IntParamType(ParamType):\n    name = \"integer\"\n\n    def convert(self, value, param, ctx):\n        try:\n            return int(value)\n        except ValueError:\n            self.fail(\"{} is not a valid integer\".format(value), param, ctx)\n\n    def __repr__(self):\n        return \"INT\"\n\n\nclass IntRange(IntParamType):\n    \"\"\"A parameter that works similar to :data:`click.INT` but restricts\n    the value to fit into a range.  The default behavior is to fail if the\n    value falls outside the range, but it can also be silently clamped\n    between the two edges.\n\n    See :ref:`ranges` for an example.\n    \"\"\"\n\n    name = \"integer range\"\n\n    def __init__(self, min=None, max=None, clamp=False):\n        self.min = min\n        self.max = max\n        self.clamp = clamp\n\n    def convert(self, value, param, ctx):\n        rv = IntParamType.convert(self, value, param, ctx)\n        if self.clamp:\n            if self.min is not None and rv < self.min:\n                return self.min\n            if self.max is not None and rv > self.max:\n                return self.max\n        if (\n            self.min is not None\n            and rv < self.min\n            or self.max is not None\n            and rv > self.max\n        ):\n            if self.min is None:\n                self.fail(\n                    \"{} is bigger than the maximum valid value {}.\".format(\n                        rv, self.max\n                    ),\n                    param,\n                    ctx,\n                )\n            elif self.max is None:\n                self.fail(\n                    \"{} is smaller than the minimum valid value {}.\".format(\n                        rv, self.min\n                    ),\n                    param,\n                    ctx,\n                )\n            else:\n                self.fail(\n                    \"{} is not in the valid range of {} to {}.\".format(\n                        rv, self.min, self.max\n                    ),\n                    param,\n                    ctx,\n                )\n        return rv\n\n    def __repr__(self):\n        return \"IntRange({}, {})\".format(self.min, self.max)\n\n\nclass FloatParamType(ParamType):\n    name = \"float\"\n\n    def convert(self, value, param, ctx):\n        try:\n            return float(value)\n        except ValueError:\n            self.fail(\n                \"{} is not a valid floating point value\".format(value), param, ctx\n            )\n\n    def __repr__(self):\n        return \"FLOAT\"\n\n\nclass FloatRange(FloatParamType):\n    \"\"\"A parameter that works similar to :data:`click.FLOAT` but restricts\n    the value to fit into a range.  The default behavior is to fail if the\n    value falls outside the range, but it can also be silently clamped\n    between the two edges.\n\n    See :ref:`ranges` for an example.\n    \"\"\"\n\n    name = \"float range\"\n\n    def __init__(self, min=None, max=None, clamp=False):\n        self.min = min\n        self.max = max\n        self.clamp = clamp\n\n    def convert(self, value, param, ctx):\n        rv = FloatParamType.convert(self, value, param, ctx)\n        if self.clamp:\n            if self.min is not None and rv < self.min:\n                return self.min\n            if self.max is not None and rv > self.max:\n                return self.max\n        if (\n            self.min is not None\n            and rv < self.min\n            or self.max is not None\n            and rv > self.max\n        ):\n            if self.min is None:\n                self.fail(\n                    \"{} is bigger than the maximum valid value {}.\".format(\n                        rv, self.max\n                    ),\n                    param,\n                    ctx,\n                )\n            elif self.max is None:\n                self.fail(\n                    \"{} is smaller than the minimum valid value {}.\".format(\n                        rv, self.min\n                    ),\n                    param,\n                    ctx,\n                )\n            else:\n                self.fail(\n                    \"{} is not in the valid range of {} to {}.\".format(\n                        rv, self.min, self.max\n                    ),\n                    param,\n                    ctx,\n                )\n        return rv\n\n    def __repr__(self):\n        return \"FloatRange({}, {})\".format(self.min, self.max)\n\n\nclass BoolParamType(ParamType):\n    name = \"boolean\"\n\n    def convert(self, value, param, ctx):\n        if isinstance(value, bool):\n            return bool(value)\n        value = value.lower()\n        if value in (\"true\", \"t\", \"1\", \"yes\", \"y\"):\n            return True\n        elif value in (\"false\", \"f\", \"0\", \"no\", \"n\"):\n            return False\n        self.fail(\"{} is not a valid boolean\".format(value), param, ctx)\n\n    def __repr__(self):\n        return \"BOOL\"\n\n\nclass UUIDParameterType(ParamType):\n    name = \"uuid\"\n\n    def convert(self, value, param, ctx):\n        import uuid\n\n        try:\n            if PY2 and isinstance(value, text_type):\n                value = value.encode(\"ascii\")\n            return uuid.UUID(value)\n        except ValueError:\n            self.fail(\"{} is not a valid UUID value\".format(value), param, ctx)\n\n    def __repr__(self):\n        return \"UUID\"\n\n\nclass File(ParamType):\n    \"\"\"Declares a parameter to be a file for reading or writing.  The file\n    is automatically closed once the context tears down (after the command\n    finished working).\n\n    Files can be opened for reading or writing.  The special value ``-``\n    indicates stdin or stdout depending on the mode.\n\n    By default, the file is opened for reading text data, but it can also be\n    opened in binary mode or for writing.  The encoding parameter can be used\n    to force a specific encoding.\n\n    The `lazy` flag controls if the file should be opened immediately or upon\n    first IO. The default is to be non-lazy for standard input and output\n    streams as well as files opened for reading, `lazy` otherwise. When opening a\n    file lazily for reading, it is still opened temporarily for validation, but\n    will not be held open until first IO. lazy is mainly useful when opening\n    for writing to avoid creating the file until it is needed.\n\n    Starting with Click 2.0, files can also be opened atomically in which\n    case all writes go into a separate file in the same folder and upon\n    completion the file will be moved over to the original location.  This\n    is useful if a file regularly read by other users is modified.\n\n    See :ref:`file-args` for more information.\n    \"\"\"\n\n    name = \"filename\"\n    envvar_list_splitter = os.path.pathsep\n\n    def __init__(\n        self, mode=\"r\", encoding=None, errors=\"strict\", lazy=None, atomic=False\n    ):\n        self.mode = mode\n        self.encoding = encoding\n        self.errors = errors\n        self.lazy = lazy\n        self.atomic = atomic\n\n    def resolve_lazy_flag(self, value):\n        if self.lazy is not None:\n            return self.lazy\n        if value == \"-\":\n            return False\n        elif \"w\" in self.mode:\n            return True\n        return False\n\n    def convert(self, value, param, ctx):\n        try:\n            if hasattr(value, \"read\") or hasattr(value, \"write\"):\n                return value\n\n            lazy = self.resolve_lazy_flag(value)\n\n            if lazy:\n                f = LazyFile(\n                    value, self.mode, self.encoding, self.errors, atomic=self.atomic\n                )\n                if ctx is not None:\n                    ctx.call_on_close(f.close_intelligently)\n                return f\n\n            f, should_close = open_stream(\n                value, self.mode, self.encoding, self.errors, atomic=self.atomic\n            )\n            # If a context is provided, we automatically close the file\n            # at the end of the context execution (or flush out).  If a\n            # context does not exist, it's the caller's responsibility to\n            # properly close the file.  This for instance happens when the\n            # type is used with prompts.\n            if ctx is not None:\n                if should_close:\n                    ctx.call_on_close(safecall(f.close))\n                else:\n                    ctx.call_on_close(safecall(f.flush))\n            return f\n        except (IOError, OSError) as e:  # noqa: B014\n            self.fail(\n                \"Could not open file: {}: {}\".format(\n                    filename_to_ui(value), get_streerror(e)\n                ),\n                param,\n                ctx,\n            )\n\n\nclass Path(ParamType):\n    \"\"\"The path type is similar to the :class:`File` type but it performs\n    different checks.  First of all, instead of returning an open file\n    handle it returns just the filename.  Secondly, it can perform various\n    basic checks about what the file or directory should be.\n\n    .. versionchanged:: 6.0\n       `allow_dash` was added.\n\n    :param exists: if set to true, the file or directory needs to exist for\n                   this value to be valid.  If this is not required and a\n                   file does indeed not exist, then all further checks are\n                   silently skipped.\n    :param file_okay: controls if a file is a possible value.\n    :param dir_okay: controls if a directory is a possible value.\n    :param writable: if true, a writable check is performed.\n    :param readable: if true, a readable check is performed.\n    :param resolve_path: if this is true, then the path is fully resolved\n                         before the value is passed onwards.  This means\n                         that it's absolute and symlinks are resolved.  It\n                         will not expand a tilde-prefix, as this is\n                         supposed to be done by the shell only.\n    :param allow_dash: If this is set to `True`, a single dash to indicate\n                       standard streams is permitted.\n    :param path_type: optionally a string type that should be used to\n                      represent the path.  The default is `None` which\n                      means the return value will be either bytes or\n                      unicode depending on what makes most sense given the\n                      input data Click deals with.\n    \"\"\"\n\n    envvar_list_splitter = os.path.pathsep\n\n    def __init__(\n        self,\n        exists=False,\n        file_okay=True,\n        dir_okay=True,\n        writable=False,\n        readable=True,\n        resolve_path=False,\n        allow_dash=False,\n        path_type=None,\n    ):\n        self.exists = exists\n        self.file_okay = file_okay\n        self.dir_okay = dir_okay\n        self.writable = writable\n        self.readable = readable\n        self.resolve_path = resolve_path\n        self.allow_dash = allow_dash\n        self.type = path_type\n\n        if self.file_okay and not self.dir_okay:\n            self.name = \"file\"\n            self.path_type = \"File\"\n        elif self.dir_okay and not self.file_okay:\n            self.name = \"directory\"\n            self.path_type = \"Directory\"\n        else:\n            self.name = \"path\"\n            self.path_type = \"Path\"\n\n    def coerce_path_result(self, rv):\n        if self.type is not None and not isinstance(rv, self.type):\n            if self.type is text_type:\n                rv = rv.decode(get_filesystem_encoding())\n            else:\n                rv = rv.encode(get_filesystem_encoding())\n        return rv\n\n    def convert(self, value, param, ctx):\n        rv = value\n\n        is_dash = self.file_okay and self.allow_dash and rv in (b\"-\", \"-\")\n\n        if not is_dash:\n            if self.resolve_path:\n                rv = os.path.realpath(rv)\n\n            try:\n                st = os.stat(rv)\n            except OSError:\n                if not self.exists:\n                    return self.coerce_path_result(rv)\n                self.fail(\n                    \"{} '{}' does not exist.\".format(\n                        self.path_type, filename_to_ui(value)\n                    ),\n                    param,\n                    ctx,\n                )\n\n            if not self.file_okay and stat.S_ISREG(st.st_mode):\n                self.fail(\n                    \"{} '{}' is a file.\".format(self.path_type, filename_to_ui(value)),\n                    param,\n                    ctx,\n                )\n            if not self.dir_okay and stat.S_ISDIR(st.st_mode):\n                self.fail(\n                    \"{} '{}' is a directory.\".format(\n                        self.path_type, filename_to_ui(value)\n                    ),\n                    param,\n                    ctx,\n                )\n            if self.writable and not os.access(value, os.W_OK):\n                self.fail(\n                    \"{} '{}' is not writable.\".format(\n                        self.path_type, filename_to_ui(value)\n                    ),\n                    param,\n                    ctx,\n                )\n            if self.readable and not os.access(value, os.R_OK):\n                self.fail(\n                    \"{} '{}' is not readable.\".format(\n                        self.path_type, filename_to_ui(value)\n                    ),\n                    param,\n                    ctx,\n                )\n\n        return self.coerce_path_result(rv)\n\n\nclass Tuple(CompositeParamType):\n    \"\"\"The default behavior of Click is to apply a type on a value directly.\n    This works well in most cases, except for when `nargs` is set to a fixed\n    count and different types should be used for different items.  In this\n    case the :class:`Tuple` type can be used.  This type can only be used\n    if `nargs` is set to a fixed number.\n\n    For more information see :ref:`tuple-type`.\n\n    This can be selected by using a Python tuple literal as a type.\n\n    :param types: a list of types that should be used for the tuple items.\n    \"\"\"\n\n    def __init__(self, types):\n        self.types = [convert_type(ty) for ty in types]\n\n    @property\n    def name(self):\n        return \"<{}>\".format(\" \".join(ty.name for ty in self.types))\n\n    @property\n    def arity(self):\n        return len(self.types)\n\n    def convert(self, value, param, ctx):\n        if len(value) != len(self.types):\n            raise TypeError(\n                \"It would appear that nargs is set to conflict with the\"\n                \" composite type arity.\"\n            )\n        return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value))\n\n\ndef convert_type(ty, default=None):\n    \"\"\"Converts a callable or python type into the most appropriate\n    param type.\n    \"\"\"\n    guessed_type = False\n    if ty is None and default is not None:\n        if isinstance(default, tuple):\n            ty = tuple(map(type, default))\n        else:\n            ty = type(default)\n        guessed_type = True\n\n    if isinstance(ty, tuple):\n        return Tuple(ty)\n    if isinstance(ty, ParamType):\n        return ty\n    if ty is text_type or ty is str or ty is None:\n        return STRING\n    if ty is int:\n        return INT\n    # Booleans are only okay if not guessed.  This is done because for\n    # flags the default value is actually a bit of a lie in that it\n    # indicates which of the flags is the one we want.  See get_default()\n    # for more information.\n    if ty is bool and not guessed_type:\n        return BOOL\n    if ty is float:\n        return FLOAT\n    if guessed_type:\n        return STRING\n\n    # Catch a common mistake\n    if __debug__:\n        try:\n            if issubclass(ty, ParamType):\n                raise AssertionError(\n                    \"Attempted to use an uninstantiated parameter type ({}).\".format(ty)\n                )\n        except TypeError:\n            pass\n    return FuncParamType(ty)\n\n\n#: A dummy parameter type that just does nothing.  From a user's\n#: perspective this appears to just be the same as `STRING` but internally\n#: no string conversion takes place.  This is necessary to achieve the\n#: same bytes/unicode behavior on Python 2/3 in situations where you want\n#: to not convert argument types.  This is usually useful when working\n#: with file paths as they can appear in bytes and unicode.\n#:\n#: For path related uses the :class:`Path` type is a better choice but\n#: there are situations where an unprocessed type is useful which is why\n#: it is is provided.\n#:\n#: .. versionadded:: 4.0\nUNPROCESSED = UnprocessedParamType()\n\n#: A unicode string parameter type which is the implicit default.  This\n#: can also be selected by using ``str`` as type.\nSTRING = StringParamType()\n\n#: An integer parameter.  This can also be selected by using ``int`` as\n#: type.\nINT = IntParamType()\n\n#: A floating point value parameter.  This can also be selected by using\n#: ``float`` as type.\nFLOAT = FloatParamType()\n\n#: A boolean parameter.  This is the default for boolean flags.  This can\n#: also be selected by using ``bool`` as a type.\nBOOL = BoolParamType()\n\n#: A UUID parameter.\nUUID = UUIDParameterType()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/click/utils.py",
    "content": "import os\nimport sys\n\nfrom ._compat import _default_text_stderr\nfrom ._compat import _default_text_stdout\nfrom ._compat import auto_wrap_for_ansi\nfrom ._compat import binary_streams\nfrom ._compat import filename_to_ui\nfrom ._compat import get_filesystem_encoding\nfrom ._compat import get_streerror\nfrom ._compat import is_bytes\nfrom ._compat import open_stream\nfrom ._compat import PY2\nfrom ._compat import should_strip_ansi\nfrom ._compat import string_types\nfrom ._compat import strip_ansi\nfrom ._compat import text_streams\nfrom ._compat import text_type\nfrom ._compat import WIN\nfrom .globals import resolve_color_default\n\nif not PY2:\n    from ._compat import _find_binary_writer\nelif WIN:\n    from ._winconsole import _get_windows_argv\n    from ._winconsole import _hash_py_argv\n    from ._winconsole import _initial_argv_hash\n\necho_native_types = string_types + (bytes, bytearray)\n\n\ndef _posixify(name):\n    return \"-\".join(name.split()).lower()\n\n\ndef safecall(func):\n    \"\"\"Wraps a function so that it swallows exceptions.\"\"\"\n\n    def wrapper(*args, **kwargs):\n        try:\n            return func(*args, **kwargs)\n        except Exception:\n            pass\n\n    return wrapper\n\n\ndef make_str(value):\n    \"\"\"Converts a value into a valid string.\"\"\"\n    if isinstance(value, bytes):\n        try:\n            return value.decode(get_filesystem_encoding())\n        except UnicodeError:\n            return value.decode(\"utf-8\", \"replace\")\n    return text_type(value)\n\n\ndef make_default_short_help(help, max_length=45):\n    \"\"\"Return a condensed version of help string.\"\"\"\n    words = help.split()\n    total_length = 0\n    result = []\n    done = False\n\n    for word in words:\n        if word[-1:] == \".\":\n            done = True\n        new_length = 1 + len(word) if result else len(word)\n        if total_length + new_length > max_length:\n            result.append(\"...\")\n            done = True\n        else:\n            if result:\n                result.append(\" \")\n            result.append(word)\n        if done:\n            break\n        total_length += new_length\n\n    return \"\".join(result)\n\n\nclass LazyFile(object):\n    \"\"\"A lazy file works like a regular file but it does not fully open\n    the file but it does perform some basic checks early to see if the\n    filename parameter does make sense.  This is useful for safely opening\n    files for writing.\n    \"\"\"\n\n    def __init__(\n        self, filename, mode=\"r\", encoding=None, errors=\"strict\", atomic=False\n    ):\n        self.name = filename\n        self.mode = mode\n        self.encoding = encoding\n        self.errors = errors\n        self.atomic = atomic\n\n        if filename == \"-\":\n            self._f, self.should_close = open_stream(filename, mode, encoding, errors)\n        else:\n            if \"r\" in mode:\n                # Open and close the file in case we're opening it for\n                # reading so that we can catch at least some errors in\n                # some cases early.\n                open(filename, mode).close()\n            self._f = None\n            self.should_close = True\n\n    def __getattr__(self, name):\n        return getattr(self.open(), name)\n\n    def __repr__(self):\n        if self._f is not None:\n            return repr(self._f)\n        return \"<unopened file '{}' {}>\".format(self.name, self.mode)\n\n    def open(self):\n        \"\"\"Opens the file if it's not yet open.  This call might fail with\n        a :exc:`FileError`.  Not handling this error will produce an error\n        that Click shows.\n        \"\"\"\n        if self._f is not None:\n            return self._f\n        try:\n            rv, self.should_close = open_stream(\n                self.name, self.mode, self.encoding, self.errors, atomic=self.atomic\n            )\n        except (IOError, OSError) as e:  # noqa: E402\n            from .exceptions import FileError\n\n            raise FileError(self.name, hint=get_streerror(e))\n        self._f = rv\n        return rv\n\n    def close(self):\n        \"\"\"Closes the underlying file, no matter what.\"\"\"\n        if self._f is not None:\n            self._f.close()\n\n    def close_intelligently(self):\n        \"\"\"This function only closes the file if it was opened by the lazy\n        file wrapper.  For instance this will never close stdin.\n        \"\"\"\n        if self.should_close:\n            self.close()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_value, tb):\n        self.close_intelligently()\n\n    def __iter__(self):\n        self.open()\n        return iter(self._f)\n\n\nclass KeepOpenFile(object):\n    def __init__(self, file):\n        self._file = file\n\n    def __getattr__(self, name):\n        return getattr(self._file, name)\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_value, tb):\n        pass\n\n    def __repr__(self):\n        return repr(self._file)\n\n    def __iter__(self):\n        return iter(self._file)\n\n\ndef echo(message=None, file=None, nl=True, err=False, color=None):\n    \"\"\"Prints a message plus a newline to the given file or stdout.  On\n    first sight, this looks like the print function, but it has improved\n    support for handling Unicode and binary data that does not fail no\n    matter how badly configured the system is.\n\n    Primarily it means that you can print binary data as well as Unicode\n    data on both 2.x and 3.x to the given file in the most appropriate way\n    possible.  This is a very carefree function in that it will try its\n    best to not fail.  As of Click 6.0 this includes support for unicode\n    output on the Windows console.\n\n    In addition to that, if `colorama`_ is installed, the echo function will\n    also support clever handling of ANSI codes.  Essentially it will then\n    do the following:\n\n    -   add transparent handling of ANSI color codes on Windows.\n    -   hide ANSI codes automatically if the destination file is not a\n        terminal.\n\n    .. _colorama: https://pypi.org/project/colorama/\n\n    .. versionchanged:: 6.0\n       As of Click 6.0 the echo function will properly support unicode\n       output on the windows console.  Not that click does not modify\n       the interpreter in any way which means that `sys.stdout` or the\n       print statement or function will still not provide unicode support.\n\n    .. versionchanged:: 2.0\n       Starting with version 2.0 of Click, the echo function will work\n       with colorama if it's installed.\n\n    .. versionadded:: 3.0\n       The `err` parameter was added.\n\n    .. versionchanged:: 4.0\n       Added the `color` flag.\n\n    :param message: the message to print\n    :param file: the file to write to (defaults to ``stdout``)\n    :param err: if set to true the file defaults to ``stderr`` instead of\n                ``stdout``.  This is faster and easier than calling\n                :func:`get_text_stderr` yourself.\n    :param nl: if set to `True` (the default) a newline is printed afterwards.\n    :param color: controls if the terminal supports ANSI colors or not.  The\n                  default is autodetection.\n    \"\"\"\n    if file is None:\n        if err:\n            file = _default_text_stderr()\n        else:\n            file = _default_text_stdout()\n\n    # Convert non bytes/text into the native string type.\n    if message is not None and not isinstance(message, echo_native_types):\n        message = text_type(message)\n\n    if nl:\n        message = message or u\"\"\n        if isinstance(message, text_type):\n            message += u\"\\n\"\n        else:\n            message += b\"\\n\"\n\n    # If there is a message, and we're in Python 3, and the value looks\n    # like bytes, we manually need to find the binary stream and write the\n    # message in there.  This is done separately so that most stream\n    # types will work as you would expect.  Eg: you can write to StringIO\n    # for other cases.\n    if message and not PY2 and is_bytes(message):\n        binary_file = _find_binary_writer(file)\n        if binary_file is not None:\n            file.flush()\n            binary_file.write(message)\n            binary_file.flush()\n            return\n\n    # ANSI-style support.  If there is no message or we are dealing with\n    # bytes nothing is happening.  If we are connected to a file we want\n    # to strip colors.  If we are on windows we either wrap the stream\n    # to strip the color or we use the colorama support to translate the\n    # ansi codes to API calls.\n    if message and not is_bytes(message):\n        color = resolve_color_default(color)\n        if should_strip_ansi(file, color):\n            message = strip_ansi(message)\n        elif WIN:\n            if auto_wrap_for_ansi is not None:\n                file = auto_wrap_for_ansi(file)\n            elif not color:\n                message = strip_ansi(message)\n\n    if message:\n        file.write(message)\n    file.flush()\n\n\ndef get_binary_stream(name):\n    \"\"\"Returns a system stream for byte processing.  This essentially\n    returns the stream from the sys module with the given name but it\n    solves some compatibility issues between different Python versions.\n    Primarily this function is necessary for getting binary streams on\n    Python 3.\n\n    :param name: the name of the stream to open.  Valid names are ``'stdin'``,\n                 ``'stdout'`` and ``'stderr'``\n    \"\"\"\n    opener = binary_streams.get(name)\n    if opener is None:\n        raise TypeError(\"Unknown standard stream '{}'\".format(name))\n    return opener()\n\n\ndef get_text_stream(name, encoding=None, errors=\"strict\"):\n    \"\"\"Returns a system stream for text processing.  This usually returns\n    a wrapped stream around a binary stream returned from\n    :func:`get_binary_stream` but it also can take shortcuts on Python 3\n    for already correctly configured streams.\n\n    :param name: the name of the stream to open.  Valid names are ``'stdin'``,\n                 ``'stdout'`` and ``'stderr'``\n    :param encoding: overrides the detected default encoding.\n    :param errors: overrides the default error mode.\n    \"\"\"\n    opener = text_streams.get(name)\n    if opener is None:\n        raise TypeError(\"Unknown standard stream '{}'\".format(name))\n    return opener(encoding, errors)\n\n\ndef open_file(\n    filename, mode=\"r\", encoding=None, errors=\"strict\", lazy=False, atomic=False\n):\n    \"\"\"This is similar to how the :class:`File` works but for manual\n    usage.  Files are opened non lazy by default.  This can open regular\n    files as well as stdin/stdout if ``'-'`` is passed.\n\n    If stdin/stdout is returned the stream is wrapped so that the context\n    manager will not close the stream accidentally.  This makes it possible\n    to always use the function like this without having to worry to\n    accidentally close a standard stream::\n\n        with open_file(filename) as f:\n            ...\n\n    .. versionadded:: 3.0\n\n    :param filename: the name of the file to open (or ``'-'`` for stdin/stdout).\n    :param mode: the mode in which to open the file.\n    :param encoding: the encoding to use.\n    :param errors: the error handling for this file.\n    :param lazy: can be flipped to true to open the file lazily.\n    :param atomic: in atomic mode writes go into a temporary file and it's\n                   moved on close.\n    \"\"\"\n    if lazy:\n        return LazyFile(filename, mode, encoding, errors, atomic=atomic)\n    f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)\n    if not should_close:\n        f = KeepOpenFile(f)\n    return f\n\n\ndef get_os_args():\n    \"\"\"This returns the argument part of sys.argv in the most appropriate\n    form for processing.  What this means is that this return value is in\n    a format that works for Click to process but does not necessarily\n    correspond well to what's actually standard for the interpreter.\n\n    On most environments the return value is ``sys.argv[:1]`` unchanged.\n    However if you are on Windows and running Python 2 the return value\n    will actually be a list of unicode strings instead because the\n    default behavior on that platform otherwise will not be able to\n    carry all possible values that sys.argv can have.\n\n    .. versionadded:: 6.0\n    \"\"\"\n    # We can only extract the unicode argv if sys.argv has not been\n    # changed since the startup of the application.\n    if PY2 and WIN and _initial_argv_hash == _hash_py_argv():\n        return _get_windows_argv()\n    return sys.argv[1:]\n\n\ndef format_filename(filename, shorten=False):\n    \"\"\"Formats a filename for user display.  The main purpose of this\n    function is to ensure that the filename can be displayed at all.  This\n    will decode the filename to unicode if necessary in a way that it will\n    not fail.  Optionally, it can shorten the filename to not include the\n    full path to the filename.\n\n    :param filename: formats a filename for UI display.  This will also convert\n                     the filename into unicode without failing.\n    :param shorten: this optionally shortens the filename to strip of the\n                    path that leads up to it.\n    \"\"\"\n    if shorten:\n        filename = os.path.basename(filename)\n    return filename_to_ui(filename)\n\n\ndef get_app_dir(app_name, roaming=True, force_posix=False):\n    r\"\"\"Returns the config folder for the application.  The default behavior\n    is to return whatever is most appropriate for the operating system.\n\n    To give you an idea, for an app called ``\"Foo Bar\"``, something like\n    the following folders could be returned:\n\n    Mac OS X:\n      ``~/Library/Application Support/Foo Bar``\n    Mac OS X (POSIX):\n      ``~/.foo-bar``\n    Unix:\n      ``~/.config/foo-bar``\n    Unix (POSIX):\n      ``~/.foo-bar``\n    Win XP (roaming):\n      ``C:\\Documents and Settings\\<user>\\Local Settings\\Application Data\\Foo Bar``\n    Win XP (not roaming):\n      ``C:\\Documents and Settings\\<user>\\Application Data\\Foo Bar``\n    Win 7 (roaming):\n      ``C:\\Users\\<user>\\AppData\\Roaming\\Foo Bar``\n    Win 7 (not roaming):\n      ``C:\\Users\\<user>\\AppData\\Local\\Foo Bar``\n\n    .. versionadded:: 2.0\n\n    :param app_name: the application name.  This should be properly capitalized\n                     and can contain whitespace.\n    :param roaming: controls if the folder should be roaming or not on Windows.\n                    Has no affect otherwise.\n    :param force_posix: if this is set to `True` then on any POSIX system the\n                        folder will be stored in the home folder with a leading\n                        dot instead of the XDG config home or darwin's\n                        application support folder.\n    \"\"\"\n    if WIN:\n        key = \"APPDATA\" if roaming else \"LOCALAPPDATA\"\n        folder = os.environ.get(key)\n        if folder is None:\n            folder = os.path.expanduser(\"~\")\n        return os.path.join(folder, app_name)\n    if force_posix:\n        return os.path.join(os.path.expanduser(\"~/.{}\".format(_posixify(app_name))))\n    if sys.platform == \"darwin\":\n        return os.path.join(\n            os.path.expanduser(\"~/Library/Application Support\"), app_name\n        )\n    return os.path.join(\n        os.environ.get(\"XDG_CONFIG_HOME\", os.path.expanduser(\"~/.config\")),\n        _posixify(app_name),\n    )\n\n\nclass PacifyFlushWrapper(object):\n    \"\"\"This wrapper is used to catch and suppress BrokenPipeErrors resulting\n    from ``.flush()`` being called on broken pipe during the shutdown/final-GC\n    of the Python interpreter. Notably ``.flush()`` is always called on\n    ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any\n    other cleanup code, and the case where the underlying file is not a broken\n    pipe, all calls and attributes are proxied.\n    \"\"\"\n\n    def __init__(self, wrapped):\n        self.wrapped = wrapped\n\n    def flush(self):\n        try:\n            self.wrapped.flush()\n        except IOError as e:\n            import errno\n\n            if e.errno != errno.EPIPE:\n                raise\n\n    def __getattr__(self, attr):\n        return getattr(self.wrapped, attr)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/__init__.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009, 2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"dnspython DNS toolkit\"\"\"\n\n__all__ = [\n    'dnssec',\n    'e164',\n    'edns',\n    'entropy',\n    'exception',\n    'flags',\n    'hash',\n    'inet',\n    'ipv4',\n    'ipv6',\n    'message',\n    'name',\n    'namedict',\n    'node',\n    'opcode',\n    'query',\n    'rcode',\n    'rdata',\n    'rdataclass',\n    'rdataset',\n    'rdatatype',\n    'renderer',\n    'resolver',\n    'reversename',\n    'rrset',\n    'set',\n    'tokenizer',\n    'tsig',\n    'tsigkeyring',\n    'ttl',\n    'rdtypes',\n    'update',\n    'version',\n    'wiredata',\n    'zone',\n]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/_compat.py",
    "content": "import sys\nimport decimal\nfrom decimal import Context\n\nPY3 = sys.version_info[0] == 3\nPY2 = sys.version_info[0] == 2\n\n\nif PY3:\n    long = int\n    xrange = range\nelse:\n    long = long  # pylint: disable=long-builtin\n    xrange = xrange  # pylint: disable=xrange-builtin\n\n# unicode / binary types\nif PY3:\n    text_type = str\n    binary_type = bytes\n    string_types = (str,)\n    unichr = chr\n    def maybe_decode(x):\n        return x.decode()\n    def maybe_encode(x):\n        return x.encode()\n    def maybe_chr(x):\n        return x\n    def maybe_ord(x):\n        return x\nelse:\n    text_type = unicode  # pylint: disable=unicode-builtin, undefined-variable\n    binary_type = str\n    string_types = (\n        basestring,  # pylint: disable=basestring-builtin, undefined-variable\n    )\n    unichr = unichr  # pylint: disable=unichr-builtin\n    def maybe_decode(x):\n        return x\n    def maybe_encode(x):\n        return x\n    def maybe_chr(x):\n        return chr(x)\n    def maybe_ord(x):\n        return ord(x)\n\n\ndef round_py2_compat(what):\n    \"\"\"\n    Python 2 and Python 3 use different rounding strategies in round(). This\n    function ensures that results are python2/3 compatible and backward\n    compatible with previous py2 releases\n    :param what: float\n    :return: rounded long\n    \"\"\"\n    d = Context(\n        prec=len(str(long(what))),  # round to integer with max precision\n        rounding=decimal.ROUND_HALF_UP\n    ).create_decimal(str(what))  # str(): python 2.6 compat\n    return long(d)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/dnssec.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"Common DNSSEC-related functions and constants.\"\"\"\n\nfrom io import BytesIO\nimport struct\nimport time\n\nimport dns.exception\nimport dns.name\nimport dns.node\nimport dns.rdataset\nimport dns.rdata\nimport dns.rdatatype\nimport dns.rdataclass\nfrom ._compat import string_types\n\n\nclass UnsupportedAlgorithm(dns.exception.DNSException):\n    \"\"\"The DNSSEC algorithm is not supported.\"\"\"\n\n\nclass ValidationFailure(dns.exception.DNSException):\n    \"\"\"The DNSSEC signature is invalid.\"\"\"\n\n\n#: RSAMD5\nRSAMD5 = 1\n#: DH\nDH = 2\n#: DSA\nDSA = 3\n#: ECC\nECC = 4\n#: RSASHA1\nRSASHA1 = 5\n#: DSANSEC3SHA1\nDSANSEC3SHA1 = 6\n#: RSASHA1NSEC3SHA1\nRSASHA1NSEC3SHA1 = 7\n#: RSASHA256\nRSASHA256 = 8\n#: RSASHA512\nRSASHA512 = 10\n#: ECDSAP256SHA256\nECDSAP256SHA256 = 13\n#: ECDSAP384SHA384\nECDSAP384SHA384 = 14\n#: INDIRECT\nINDIRECT = 252\n#: PRIVATEDNS\nPRIVATEDNS = 253\n#: PRIVATEOID\nPRIVATEOID = 254\n\n_algorithm_by_text = {\n    'RSAMD5': RSAMD5,\n    'DH': DH,\n    'DSA': DSA,\n    'ECC': ECC,\n    'RSASHA1': RSASHA1,\n    'DSANSEC3SHA1': DSANSEC3SHA1,\n    'RSASHA1NSEC3SHA1': RSASHA1NSEC3SHA1,\n    'RSASHA256': RSASHA256,\n    'RSASHA512': RSASHA512,\n    'INDIRECT': INDIRECT,\n    'ECDSAP256SHA256': ECDSAP256SHA256,\n    'ECDSAP384SHA384': ECDSAP384SHA384,\n    'PRIVATEDNS': PRIVATEDNS,\n    'PRIVATEOID': PRIVATEOID,\n}\n\n# We construct the inverse mapping programmatically to ensure that we\n# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that\n# would cause the mapping not to be true inverse.\n\n_algorithm_by_value = {y: x for x, y in _algorithm_by_text.items()}\n\n\ndef algorithm_from_text(text):\n    \"\"\"Convert text into a DNSSEC algorithm value.\n\n    Returns an ``int``.\n    \"\"\"\n\n    value = _algorithm_by_text.get(text.upper())\n    if value is None:\n        value = int(text)\n    return value\n\n\ndef algorithm_to_text(value):\n    \"\"\"Convert a DNSSEC algorithm value to text\n\n    Returns a ``str``.\n    \"\"\"\n\n    text = _algorithm_by_value.get(value)\n    if text is None:\n        text = str(value)\n    return text\n\n\ndef _to_rdata(record, origin):\n    s = BytesIO()\n    record.to_wire(s, origin=origin)\n    return s.getvalue()\n\n\ndef key_id(key, origin=None):\n    \"\"\"Return the key id (a 16-bit number) for the specified key.\n\n    Note the *origin* parameter of this function is historical and\n    is not needed.\n\n    Returns an ``int`` between 0 and 65535.\n    \"\"\"\n\n    rdata = _to_rdata(key, origin)\n    rdata = bytearray(rdata)\n    if key.algorithm == RSAMD5:\n        return (rdata[-3] << 8) + rdata[-2]\n    else:\n        total = 0\n        for i in range(len(rdata) // 2):\n            total += (rdata[2 * i] << 8) + \\\n                rdata[2 * i + 1]\n        if len(rdata) % 2 != 0:\n            total += rdata[len(rdata) - 1] << 8\n        total += ((total >> 16) & 0xffff)\n        return total & 0xffff\n\n\ndef make_ds(name, key, algorithm, origin=None):\n    \"\"\"Create a DS record for a DNSSEC key.\n\n    *name* is the owner name of the DS record.\n\n    *key* is a ``dns.rdtypes.ANY.DNSKEY``.\n\n    *algorithm* is a string describing which hash algorithm to use.  The\n    currently supported hashes are \"SHA1\" and \"SHA256\".  Case does not\n    matter for these strings.\n\n    *origin* is a ``dns.name.Name`` and will be used as the origin\n    if *key* is a relative name.\n\n    Returns a ``dns.rdtypes.ANY.DS``.\n    \"\"\"\n\n    if algorithm.upper() == 'SHA1':\n        dsalg = 1\n        hash = SHA1.new()\n    elif algorithm.upper() == 'SHA256':\n        dsalg = 2\n        hash = SHA256.new()\n    else:\n        raise UnsupportedAlgorithm('unsupported algorithm \"%s\"' % algorithm)\n\n    if isinstance(name, string_types):\n        name = dns.name.from_text(name, origin)\n    hash.update(name.canonicalize().to_wire())\n    hash.update(_to_rdata(key, origin))\n    digest = hash.digest()\n\n    dsrdata = struct.pack(\"!HBB\", key_id(key), key.algorithm, dsalg) + digest\n    return dns.rdata.from_wire(dns.rdataclass.IN, dns.rdatatype.DS, dsrdata, 0,\n                               len(dsrdata))\n\n\ndef _find_candidate_keys(keys, rrsig):\n    candidate_keys = []\n    value = keys.get(rrsig.signer)\n    if value is None:\n        return None\n    if isinstance(value, dns.node.Node):\n        try:\n            rdataset = value.find_rdataset(dns.rdataclass.IN,\n                                           dns.rdatatype.DNSKEY)\n        except KeyError:\n            return None\n    else:\n        rdataset = value\n    for rdata in rdataset:\n        if rdata.algorithm == rrsig.algorithm and \\\n                key_id(rdata) == rrsig.key_tag:\n            candidate_keys.append(rdata)\n    return candidate_keys\n\n\ndef _is_rsa(algorithm):\n    return algorithm in (RSAMD5, RSASHA1,\n                         RSASHA1NSEC3SHA1, RSASHA256,\n                         RSASHA512)\n\n\ndef _is_dsa(algorithm):\n    return algorithm in (DSA, DSANSEC3SHA1)\n\n\ndef _is_ecdsa(algorithm):\n    return _have_ecdsa and (algorithm in (ECDSAP256SHA256, ECDSAP384SHA384))\n\n\ndef _is_md5(algorithm):\n    return algorithm == RSAMD5\n\n\ndef _is_sha1(algorithm):\n    return algorithm in (DSA, RSASHA1,\n                         DSANSEC3SHA1, RSASHA1NSEC3SHA1)\n\n\ndef _is_sha256(algorithm):\n    return algorithm in (RSASHA256, ECDSAP256SHA256)\n\n\ndef _is_sha384(algorithm):\n    return algorithm == ECDSAP384SHA384\n\n\ndef _is_sha512(algorithm):\n    return algorithm == RSASHA512\n\n\ndef _make_hash(algorithm):\n    if _is_md5(algorithm):\n        return MD5.new()\n    if _is_sha1(algorithm):\n        return SHA1.new()\n    if _is_sha256(algorithm):\n        return SHA256.new()\n    if _is_sha384(algorithm):\n        return SHA384.new()\n    if _is_sha512(algorithm):\n        return SHA512.new()\n    raise ValidationFailure('unknown hash for algorithm %u' % algorithm)\n\n\ndef _make_algorithm_id(algorithm):\n    if _is_md5(algorithm):\n        oid = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05]\n    elif _is_sha1(algorithm):\n        oid = [0x2b, 0x0e, 0x03, 0x02, 0x1a]\n    elif _is_sha256(algorithm):\n        oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]\n    elif _is_sha512(algorithm):\n        oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03]\n    else:\n        raise ValidationFailure('unknown algorithm %u' % algorithm)\n    olen = len(oid)\n    dlen = _make_hash(algorithm).digest_size\n    idbytes = [0x30] + [8 + olen + dlen] + \\\n              [0x30, olen + 4] + [0x06, olen] + oid + \\\n              [0x05, 0x00] + [0x04, dlen]\n    return struct.pack('!%dB' % len(idbytes), *idbytes)\n\n\ndef _validate_rrsig(rrset, rrsig, keys, origin=None, now=None):\n    \"\"\"Validate an RRset against a single signature rdata\n\n    The owner name of *rrsig* is assumed to be the same as the owner name\n    of *rrset*.\n\n    *rrset* is the RRset to validate.  It can be a ``dns.rrset.RRset`` or\n    a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple.\n\n    *rrsig* is a ``dns.rdata.Rdata``, the signature to validate.\n\n    *keys* is the key dictionary, used to find the DNSKEY associated with\n    a given name.  The dictionary is keyed by a ``dns.name.Name``, and has\n    ``dns.node.Node`` or ``dns.rdataset.Rdataset`` values.\n\n    *origin* is a ``dns.name.Name``, the origin to use for relative names.\n\n    *now* is an ``int``, the time to use when validating the signatures,\n    in seconds since the UNIX epoch.  The default is the current time.\n    \"\"\"\n\n    if isinstance(origin, string_types):\n        origin = dns.name.from_text(origin, dns.name.root)\n\n    candidate_keys = _find_candidate_keys(keys, rrsig)\n    if candidate_keys is None:\n        raise ValidationFailure('unknown key')\n\n    for candidate_key in candidate_keys:\n        # For convenience, allow the rrset to be specified as a (name,\n        # rdataset) tuple as well as a proper rrset\n        if isinstance(rrset, tuple):\n            rrname = rrset[0]\n            rdataset = rrset[1]\n        else:\n            rrname = rrset.name\n            rdataset = rrset\n\n        if now is None:\n            now = time.time()\n        if rrsig.expiration < now:\n            raise ValidationFailure('expired')\n        if rrsig.inception > now:\n            raise ValidationFailure('not yet valid')\n\n        hash = _make_hash(rrsig.algorithm)\n\n        if _is_rsa(rrsig.algorithm):\n            keyptr = candidate_key.key\n            (bytes_,) = struct.unpack('!B', keyptr[0:1])\n            keyptr = keyptr[1:]\n            if bytes_ == 0:\n                (bytes_,) = struct.unpack('!H', keyptr[0:2])\n                keyptr = keyptr[2:]\n            rsa_e = keyptr[0:bytes_]\n            rsa_n = keyptr[bytes_:]\n            try:\n                pubkey = CryptoRSA.construct(\n                    (number.bytes_to_long(rsa_n),\n                     number.bytes_to_long(rsa_e)))\n            except ValueError:\n                raise ValidationFailure('invalid public key')\n            sig = rrsig.signature\n        elif _is_dsa(rrsig.algorithm):\n            keyptr = candidate_key.key\n            (t,) = struct.unpack('!B', keyptr[0:1])\n            keyptr = keyptr[1:]\n            octets = 64 + t * 8\n            dsa_q = keyptr[0:20]\n            keyptr = keyptr[20:]\n            dsa_p = keyptr[0:octets]\n            keyptr = keyptr[octets:]\n            dsa_g = keyptr[0:octets]\n            keyptr = keyptr[octets:]\n            dsa_y = keyptr[0:octets]\n            pubkey = CryptoDSA.construct(\n                (number.bytes_to_long(dsa_y),\n                 number.bytes_to_long(dsa_g),\n                 number.bytes_to_long(dsa_p),\n                 number.bytes_to_long(dsa_q)))\n            sig = rrsig.signature[1:]\n        elif _is_ecdsa(rrsig.algorithm):\n            # use ecdsa for NIST-384p -- not currently supported by pycryptodome\n\n            keyptr = candidate_key.key\n\n            if rrsig.algorithm == ECDSAP256SHA256:\n                curve = ecdsa.curves.NIST256p\n                key_len = 32\n            elif rrsig.algorithm == ECDSAP384SHA384:\n                curve = ecdsa.curves.NIST384p\n                key_len = 48\n\n            x = number.bytes_to_long(keyptr[0:key_len])\n            y = number.bytes_to_long(keyptr[key_len:key_len * 2])\n            if not ecdsa.ecdsa.point_is_valid(curve.generator, x, y):\n                raise ValidationFailure('invalid ECDSA key')\n            point = ecdsa.ellipticcurve.Point(curve.curve, x, y, curve.order)\n            verifying_key = ecdsa.keys.VerifyingKey.from_public_point(point,\n                                                                      curve)\n            pubkey = ECKeyWrapper(verifying_key, key_len)\n            r = rrsig.signature[:key_len]\n            s = rrsig.signature[key_len:]\n            sig = ecdsa.ecdsa.Signature(number.bytes_to_long(r),\n                                        number.bytes_to_long(s))\n\n        else:\n            raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm)\n\n        hash.update(_to_rdata(rrsig, origin)[:18])\n        hash.update(rrsig.signer.to_digestable(origin))\n\n        if rrsig.labels < len(rrname) - 1:\n            suffix = rrname.split(rrsig.labels + 1)[1]\n            rrname = dns.name.from_text('*', suffix)\n        rrnamebuf = rrname.to_digestable(origin)\n        rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass,\n                              rrsig.original_ttl)\n        rrlist = sorted(rdataset)\n        for rr in rrlist:\n            hash.update(rrnamebuf)\n            hash.update(rrfixed)\n            rrdata = rr.to_digestable(origin)\n            rrlen = struct.pack('!H', len(rrdata))\n            hash.update(rrlen)\n            hash.update(rrdata)\n\n        try:\n            if _is_rsa(rrsig.algorithm):\n                verifier = pkcs1_15.new(pubkey)\n                # will raise ValueError if verify fails:\n                verifier.verify(hash, sig)\n            elif _is_dsa(rrsig.algorithm):\n                verifier = DSS.new(pubkey, 'fips-186-3')\n                verifier.verify(hash, sig)\n            elif _is_ecdsa(rrsig.algorithm):\n                digest = hash.digest()\n                if not pubkey.verify(digest, sig):\n                    raise ValueError\n            else:\n                # Raise here for code clarity; this won't actually ever happen\n                # since if the algorithm is really unknown we'd already have\n                # raised an exception above\n                raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm)\n            # If we got here, we successfully verified so we can return without error\n            return\n        except ValueError:\n            # this happens on an individual validation failure\n            continue\n    # nothing verified -- raise failure:\n    raise ValidationFailure('verify failure')\n\n\ndef _validate(rrset, rrsigset, keys, origin=None, now=None):\n    \"\"\"Validate an RRset.\n\n    *rrset* is the RRset to validate.  It can be a ``dns.rrset.RRset`` or\n    a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple.\n\n    *rrsigset* is the signature RRset to be validated.  It can be a\n    ``dns.rrset.RRset`` or a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple.\n\n    *keys* is the key dictionary, used to find the DNSKEY associated with\n    a given name.  The dictionary is keyed by a ``dns.name.Name``, and has\n    ``dns.node.Node`` or ``dns.rdataset.Rdataset`` values.\n\n    *origin* is a ``dns.name.Name``, the origin to use for relative names.\n\n    *now* is an ``int``, the time to use when validating the signatures,\n    in seconds since the UNIX epoch.  The default is the current time.\n    \"\"\"\n\n    if isinstance(origin, string_types):\n        origin = dns.name.from_text(origin, dns.name.root)\n\n    if isinstance(rrset, tuple):\n        rrname = rrset[0]\n    else:\n        rrname = rrset.name\n\n    if isinstance(rrsigset, tuple):\n        rrsigname = rrsigset[0]\n        rrsigrdataset = rrsigset[1]\n    else:\n        rrsigname = rrsigset.name\n        rrsigrdataset = rrsigset\n\n    rrname = rrname.choose_relativity(origin)\n    rrsigname = rrsigname.choose_relativity(origin)\n    if rrname != rrsigname:\n        raise ValidationFailure(\"owner names do not match\")\n\n    for rrsig in rrsigrdataset:\n        try:\n            _validate_rrsig(rrset, rrsig, keys, origin, now)\n            return\n        except ValidationFailure:\n            pass\n    raise ValidationFailure(\"no RRSIGs validated\")\n\n\ndef _need_pycrypto(*args, **kwargs):\n    raise NotImplementedError(\"DNSSEC validation requires pycryptodome/pycryptodomex\")\n\n\ntry:\n    try:\n        # test we're using pycryptodome, not pycrypto (which misses SHA1 for example)\n        from Crypto.Hash import MD5, SHA1, SHA256, SHA384, SHA512\n        from Crypto.PublicKey import RSA as CryptoRSA, DSA as CryptoDSA\n        from Crypto.Signature import pkcs1_15, DSS\n        from Crypto.Util import number\n    except ImportError:\n        from Cryptodome.Hash import MD5, SHA1, SHA256, SHA384, SHA512\n        from Cryptodome.PublicKey import RSA as CryptoRSA, DSA as CryptoDSA\n        from Cryptodome.Signature import pkcs1_15, DSS\n        from Cryptodome.Util import number\nexcept ImportError:\n    validate = _need_pycrypto\n    validate_rrsig = _need_pycrypto\n    _have_pycrypto = False\n    _have_ecdsa = False\nelse:\n    validate = _validate\n    validate_rrsig = _validate_rrsig\n    _have_pycrypto = True\n\n    try:\n        import ecdsa\n        import ecdsa.ecdsa\n        import ecdsa.ellipticcurve\n        import ecdsa.keys\n    except ImportError:\n        _have_ecdsa = False\n    else:\n        _have_ecdsa = True\n\n        class ECKeyWrapper(object):\n\n            def __init__(self, key, key_len):\n                self.key = key\n                self.key_len = key_len\n\n            def verify(self, digest, sig):\n                diglong = number.bytes_to_long(digest)\n                return self.key.pubkey.verifies(diglong, sig)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/e164.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2006-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS E.164 helpers.\"\"\"\n\nimport dns.exception\nimport dns.name\nimport dns.resolver\nfrom ._compat import string_types, maybe_decode\n\n#: The public E.164 domain.\npublic_enum_domain = dns.name.from_text('e164.arpa.')\n\n\ndef from_e164(text, origin=public_enum_domain):\n    \"\"\"Convert an E.164 number in textual form into a Name object whose\n    value is the ENUM domain name for that number.\n\n    Non-digits in the text are ignored, i.e. \"16505551212\",\n    \"+1.650.555.1212\" and \"1 (650) 555-1212\" are all the same.\n\n    *text*, a ``text``, is an E.164 number in textual form.\n\n    *origin*, a ``dns.name.Name``, the domain in which the number\n    should be constructed.  The default is ``e164.arpa.``.\n\n    Returns a ``dns.name.Name``.\n    \"\"\"\n\n    parts = [d for d in text if d.isdigit()]\n    parts.reverse()\n    return dns.name.from_text('.'.join(parts), origin=origin)\n\n\ndef to_e164(name, origin=public_enum_domain, want_plus_prefix=True):\n    \"\"\"Convert an ENUM domain name into an E.164 number.\n\n    Note that dnspython does not have any information about preferred\n    number formats within national numbering plans, so all numbers are\n    emitted as a simple string of digits, prefixed by a '+' (unless\n    *want_plus_prefix* is ``False``).\n\n    *name* is a ``dns.name.Name``, the ENUM domain name.\n\n    *origin* is a ``dns.name.Name``, a domain containing the ENUM\n    domain name.  The name is relativized to this domain before being\n    converted to text.  If ``None``, no relativization is done.\n\n    *want_plus_prefix* is a ``bool``.  If True, add a '+' to the beginning of\n    the returned number.\n\n    Returns a ``text``.\n\n    \"\"\"\n    if origin is not None:\n        name = name.relativize(origin)\n    dlabels = [d for d in name.labels if d.isdigit() and len(d) == 1]\n    if len(dlabels) != len(name.labels):\n        raise dns.exception.SyntaxError('non-digit labels in ENUM domain name')\n    dlabels.reverse()\n    text = b''.join(dlabels)\n    if want_plus_prefix:\n        text = b'+' + text\n    return maybe_decode(text)\n\n\ndef query(number, domains, resolver=None):\n    \"\"\"Look for NAPTR RRs for the specified number in the specified domains.\n\n    e.g. lookup('16505551212', ['e164.dnspython.org.', 'e164.arpa.'])\n\n    *number*, a ``text`` is the number to look for.\n\n    *domains* is an iterable containing ``dns.name.Name`` values.\n\n    *resolver*, a ``dns.resolver.Resolver``, is the resolver to use.  If\n    ``None``, the default resolver is used.\n    \"\"\"\n\n    if resolver is None:\n        resolver = dns.resolver.get_default_resolver()\n    e_nx = dns.resolver.NXDOMAIN()\n    for domain in domains:\n        if isinstance(domain, string_types):\n            domain = dns.name.from_text(domain)\n        qname = dns.e164.from_e164(number, domain)\n        try:\n            return resolver.query(qname, 'NAPTR')\n        except dns.resolver.NXDOMAIN as e:\n            e_nx += e\n    raise e_nx\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/edns.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2009-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"EDNS Options\"\"\"\n\nfrom __future__ import absolute_import\n\nimport math\nimport struct\n\nimport dns.inet\n\n#: NSID\nNSID = 3\n#: DAU\nDAU = 5\n#: DHU\nDHU = 6\n#: N3U\nN3U = 7\n#: ECS (client-subnet)\nECS = 8\n#: EXPIRE\nEXPIRE = 9\n#: COOKIE\nCOOKIE = 10\n#: KEEPALIVE\nKEEPALIVE = 11\n#: PADDING\nPADDING = 12\n#: CHAIN\nCHAIN = 13\n\nclass Option(object):\n\n    \"\"\"Base class for all EDNS option types.\"\"\"\n\n    def __init__(self, otype):\n        \"\"\"Initialize an option.\n\n        *otype*, an ``int``, is the option type.\n        \"\"\"\n        self.otype = otype\n\n    def to_wire(self, file):\n        \"\"\"Convert an option to wire format.\n        \"\"\"\n        raise NotImplementedError\n\n    @classmethod\n    def from_wire(cls, otype, wire, current, olen):\n        \"\"\"Build an EDNS option object from wire format.\n\n        *otype*, an ``int``, is the option type.\n\n        *wire*, a ``binary``, is the wire-format message.\n\n        *current*, an ``int``, is the offset in *wire* of the beginning\n        of the rdata.\n\n        *olen*, an ``int``, is the length of the wire-format option data\n\n        Returns a ``dns.edns.Option``.\n        \"\"\"\n\n        raise NotImplementedError\n\n    def _cmp(self, other):\n        \"\"\"Compare an EDNS option with another option of the same type.\n\n        Returns < 0 if < *other*, 0 if == *other*, and > 0 if > *other*.\n        \"\"\"\n        raise NotImplementedError\n\n    def __eq__(self, other):\n        if not isinstance(other, Option):\n            return False\n        if self.otype != other.otype:\n            return False\n        return self._cmp(other) == 0\n\n    def __ne__(self, other):\n        if not isinstance(other, Option):\n            return False\n        if self.otype != other.otype:\n            return False\n        return self._cmp(other) != 0\n\n    def __lt__(self, other):\n        if not isinstance(other, Option) or \\\n                self.otype != other.otype:\n            return NotImplemented\n        return self._cmp(other) < 0\n\n    def __le__(self, other):\n        if not isinstance(other, Option) or \\\n                self.otype != other.otype:\n            return NotImplemented\n        return self._cmp(other) <= 0\n\n    def __ge__(self, other):\n        if not isinstance(other, Option) or \\\n                self.otype != other.otype:\n            return NotImplemented\n        return self._cmp(other) >= 0\n\n    def __gt__(self, other):\n        if not isinstance(other, Option) or \\\n                self.otype != other.otype:\n            return NotImplemented\n        return self._cmp(other) > 0\n\n\nclass GenericOption(Option):\n\n    \"\"\"Generic Option Class\n\n    This class is used for EDNS option types for which we have no better\n    implementation.\n    \"\"\"\n\n    def __init__(self, otype, data):\n        super(GenericOption, self).__init__(otype)\n        self.data = data\n\n    def to_wire(self, file):\n        file.write(self.data)\n\n    def to_text(self):\n        return \"Generic %d\" % self.otype\n\n    @classmethod\n    def from_wire(cls, otype, wire, current, olen):\n        return cls(otype, wire[current: current + olen])\n\n    def _cmp(self, other):\n        if self.data == other.data:\n            return 0\n        if self.data > other.data:\n            return 1\n        return -1\n\n\nclass ECSOption(Option):\n    \"\"\"EDNS Client Subnet (ECS, RFC7871)\"\"\"\n\n    def __init__(self, address, srclen=None, scopelen=0):\n        \"\"\"*address*, a ``text``, is the client address information.\n\n        *srclen*, an ``int``, the source prefix length, which is the\n        leftmost number of bits of the address to be used for the\n        lookup.  The default is 24 for IPv4 and 56 for IPv6.\n\n        *scopelen*, an ``int``, the scope prefix length.  This value\n        must be 0 in queries, and should be set in responses.\n        \"\"\"\n\n        super(ECSOption, self).__init__(ECS)\n        af = dns.inet.af_for_address(address)\n\n        if af == dns.inet.AF_INET6:\n            self.family = 2\n            if srclen is None:\n                srclen = 56\n        elif af == dns.inet.AF_INET:\n            self.family = 1\n            if srclen is None:\n                srclen = 24\n        else:\n            raise ValueError('Bad ip family')\n\n        self.address = address\n        self.srclen = srclen\n        self.scopelen = scopelen\n\n        addrdata = dns.inet.inet_pton(af, address)\n        nbytes = int(math.ceil(srclen/8.0))\n\n        # Truncate to srclen and pad to the end of the last octet needed\n        # See RFC section 6\n        self.addrdata = addrdata[:nbytes]\n        nbits = srclen % 8\n        if nbits != 0:\n            last = struct.pack('B', ord(self.addrdata[-1:]) & (0xff << nbits))\n            self.addrdata = self.addrdata[:-1] + last\n\n    def to_text(self):\n        return \"ECS {}/{} scope/{}\".format(self.address, self.srclen,\n                                           self.scopelen)\n\n    def to_wire(self, file):\n        file.write(struct.pack('!H', self.family))\n        file.write(struct.pack('!BB', self.srclen, self.scopelen))\n        file.write(self.addrdata)\n\n    @classmethod\n    def from_wire(cls, otype, wire, cur, olen):\n        family, src, scope = struct.unpack('!HBB', wire[cur:cur+4])\n        cur += 4\n\n        addrlen = int(math.ceil(src/8.0))\n\n        if family == 1:\n            af = dns.inet.AF_INET\n            pad = 4 - addrlen\n        elif family == 2:\n            af = dns.inet.AF_INET6\n            pad = 16 - addrlen\n        else:\n            raise ValueError('unsupported family')\n\n        addr = dns.inet.inet_ntop(af, wire[cur:cur+addrlen] + b'\\x00' * pad)\n        return cls(addr, src, scope)\n\n    def _cmp(self, other):\n        if self.addrdata == other.addrdata:\n            return 0\n        if self.addrdata > other.addrdata:\n            return 1\n        return -1\n\n_type_to_class = {\n        ECS: ECSOption\n}\n\ndef get_option_class(otype):\n    \"\"\"Return the class for the specified option type.\n\n    The GenericOption class is used if a more specific class is not\n    known.\n    \"\"\"\n\n    cls = _type_to_class.get(otype)\n    if cls is None:\n        cls = GenericOption\n    return cls\n\n\ndef option_from_wire(otype, wire, current, olen):\n    \"\"\"Build an EDNS option object from wire format.\n\n    *otype*, an ``int``, is the option type.\n\n    *wire*, a ``binary``, is the wire-format message.\n\n    *current*, an ``int``, is the offset in *wire* of the beginning\n    of the rdata.\n\n    *olen*, an ``int``, is the length of the wire-format option data\n\n    Returns an instance of a subclass of ``dns.edns.Option``.\n    \"\"\"\n\n    cls = get_option_class(otype)\n    return cls.from_wire(otype, wire, current, olen)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/entropy.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2009-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport os\nimport random\nimport time\nfrom ._compat import long, binary_type\ntry:\n    import threading as _threading\nexcept ImportError:\n    import dummy_threading as _threading\n\n\nclass EntropyPool(object):\n\n    # This is an entropy pool for Python implementations that do not\n    # have a working SystemRandom.  I'm not sure there are any, but\n    # leaving this code doesn't hurt anything as the library code\n    # is used if present.\n\n    def __init__(self, seed=None):\n        self.pool_index = 0\n        self.digest = None\n        self.next_byte = 0\n        self.lock = _threading.Lock()\n        try:\n            import hashlib\n            self.hash = hashlib.sha1()\n            self.hash_len = 20\n        except ImportError:\n            try:\n                import sha\n                self.hash = sha.new()\n                self.hash_len = 20\n            except ImportError:\n                import md5  # pylint: disable=import-error\n                self.hash = md5.new()\n                self.hash_len = 16\n        self.pool = bytearray(b'\\0' * self.hash_len)\n        if seed is not None:\n            self.stir(bytearray(seed))\n            self.seeded = True\n            self.seed_pid = os.getpid()\n        else:\n            self.seeded = False\n            self.seed_pid = 0\n\n    def stir(self, entropy, already_locked=False):\n        if not already_locked:\n            self.lock.acquire()\n        try:\n            for c in entropy:\n                if self.pool_index == self.hash_len:\n                    self.pool_index = 0\n                b = c & 0xff\n                self.pool[self.pool_index] ^= b\n                self.pool_index += 1\n        finally:\n            if not already_locked:\n                self.lock.release()\n\n    def _maybe_seed(self):\n        if not self.seeded or self.seed_pid != os.getpid():\n            try:\n                seed = os.urandom(16)\n            except Exception:\n                try:\n                    r = open('/dev/urandom', 'rb', 0)\n                    try:\n                        seed = r.read(16)\n                    finally:\n                        r.close()\n                except Exception:\n                    seed = str(time.time())\n            self.seeded = True\n            self.seed_pid = os.getpid()\n            self.digest = None\n            seed = bytearray(seed)\n            self.stir(seed, True)\n\n    def random_8(self):\n        self.lock.acquire()\n        try:\n            self._maybe_seed()\n            if self.digest is None or self.next_byte == self.hash_len:\n                self.hash.update(binary_type(self.pool))\n                self.digest = bytearray(self.hash.digest())\n                self.stir(self.digest, True)\n                self.next_byte = 0\n            value = self.digest[self.next_byte]\n            self.next_byte += 1\n        finally:\n            self.lock.release()\n        return value\n\n    def random_16(self):\n        return self.random_8() * 256 + self.random_8()\n\n    def random_32(self):\n        return self.random_16() * 65536 + self.random_16()\n\n    def random_between(self, first, last):\n        size = last - first + 1\n        if size > long(4294967296):\n            raise ValueError('too big')\n        if size > 65536:\n            rand = self.random_32\n            max = long(4294967295)\n        elif size > 256:\n            rand = self.random_16\n            max = 65535\n        else:\n            rand = self.random_8\n            max = 255\n        return first + size * rand() // (max + 1)\n\npool = EntropyPool()\n\ntry:\n    system_random = random.SystemRandom()\nexcept Exception:\n    system_random = None\n\ndef random_16():\n    if system_random is not None:\n        return system_random.randrange(0, 65536)\n    else:\n        return pool.random_16()\n\ndef between(first, last):\n    if system_random is not None:\n        return system_random.randrange(first, last + 1)\n    else:\n        return pool.random_between(first, last)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/exception.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"Common DNS Exceptions.\n\nDnspython modules may also define their own exceptions, which will\nalways be subclasses of ``DNSException``.\n\"\"\"\n\nclass DNSException(Exception):\n    \"\"\"Abstract base class shared by all dnspython exceptions.\n\n    It supports two basic modes of operation:\n\n    a) Old/compatible mode is used if ``__init__`` was called with\n    empty *kwargs*.  In compatible mode all *args* are passed\n    to the standard Python Exception class as before and all *args* are\n    printed by the standard ``__str__`` implementation.  Class variable\n    ``msg`` (or doc string if ``msg`` is ``None``) is returned from ``str()``\n    if *args* is empty.\n\n    b) New/parametrized mode is used if ``__init__`` was called with\n    non-empty *kwargs*.\n    In the new mode *args* must be empty and all kwargs must match\n    those set in class variable ``supp_kwargs``. All kwargs are stored inside\n    ``self.kwargs`` and used in a new ``__str__`` implementation to construct\n    a formatted message based on the ``fmt`` class variable, a ``string``.\n\n    In the simplest case it is enough to override the ``supp_kwargs``\n    and ``fmt`` class variables to get nice parametrized messages.\n    \"\"\"\n\n    msg = None  # non-parametrized message\n    supp_kwargs = set()  # accepted parameters for _fmt_kwargs (sanity check)\n    fmt = None  # message parametrized with results from _fmt_kwargs\n\n    def __init__(self, *args, **kwargs):\n        self._check_params(*args, **kwargs)\n        if kwargs:\n            self.kwargs = self._check_kwargs(**kwargs)\n            self.msg = str(self)\n        else:\n            self.kwargs = dict()  # defined but empty for old mode exceptions\n        if self.msg is None:\n            # doc string is better implicit message than empty string\n            self.msg = self.__doc__\n        if args:\n            super(DNSException, self).__init__(*args)\n        else:\n            super(DNSException, self).__init__(self.msg)\n\n    def _check_params(self, *args, **kwargs):\n        \"\"\"Old exceptions supported only args and not kwargs.\n\n        For sanity we do not allow to mix old and new behavior.\"\"\"\n        if args or kwargs:\n            assert bool(args) != bool(kwargs), \\\n                'keyword arguments are mutually exclusive with positional args'\n\n    def _check_kwargs(self, **kwargs):\n        if kwargs:\n            assert set(kwargs.keys()) == self.supp_kwargs, \\\n                'following set of keyword args is required: %s' % (\n                    self.supp_kwargs)\n        return kwargs\n\n    def _fmt_kwargs(self, **kwargs):\n        \"\"\"Format kwargs before printing them.\n\n        Resulting dictionary has to have keys necessary for str.format call\n        on fmt class variable.\n        \"\"\"\n        fmtargs = {}\n        for kw, data in kwargs.items():\n            if isinstance(data, (list, set)):\n                # convert list of <someobj> to list of str(<someobj>)\n                fmtargs[kw] = list(map(str, data))\n                if len(fmtargs[kw]) == 1:\n                    # remove list brackets [] from single-item lists\n                    fmtargs[kw] = fmtargs[kw].pop()\n            else:\n                fmtargs[kw] = data\n        return fmtargs\n\n    def __str__(self):\n        if self.kwargs and self.fmt:\n            # provide custom message constructed from keyword arguments\n            fmtargs = self._fmt_kwargs(**self.kwargs)\n            return self.fmt.format(**fmtargs)\n        else:\n            # print *args directly in the same way as old DNSException\n            return super(DNSException, self).__str__()\n\n\nclass FormError(DNSException):\n    \"\"\"DNS message is malformed.\"\"\"\n\n\nclass SyntaxError(DNSException):\n    \"\"\"Text input is malformed.\"\"\"\n\n\nclass UnexpectedEnd(SyntaxError):\n    \"\"\"Text input ended unexpectedly.\"\"\"\n\n\nclass TooBig(DNSException):\n    \"\"\"The DNS message is too big.\"\"\"\n\n\nclass Timeout(DNSException):\n    \"\"\"The DNS operation timed out.\"\"\"\n    supp_kwargs = {'timeout'}\n    fmt = \"The DNS operation timed out after {timeout} seconds\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/flags.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2001-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS Message Flags.\"\"\"\n\n# Standard DNS flags\n\n#: Query Response\nQR = 0x8000\n#: Authoritative Answer\nAA = 0x0400\n#: Truncated Response\nTC = 0x0200\n#: Recursion Desired\nRD = 0x0100\n#: Recursion Available\nRA = 0x0080\n#: Authentic Data\nAD = 0x0020\n#: Checking Disabled\nCD = 0x0010\n\n# EDNS flags\n\n#: DNSSEC answer OK\nDO = 0x8000\n\n_by_text = {\n    'QR': QR,\n    'AA': AA,\n    'TC': TC,\n    'RD': RD,\n    'RA': RA,\n    'AD': AD,\n    'CD': CD\n}\n\n_edns_by_text = {\n    'DO': DO\n}\n\n\n# We construct the inverse mappings programmatically to ensure that we\n# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that\n# would cause the mappings not to be true inverses.\n\n_by_value = {y: x for x, y in _by_text.items()}\n\n_edns_by_value = {y: x for x, y in _edns_by_text.items()}\n\n\ndef _order_flags(table):\n    order = list(table.items())\n    order.sort()\n    order.reverse()\n    return order\n\n_flags_order = _order_flags(_by_value)\n\n_edns_flags_order = _order_flags(_edns_by_value)\n\n\ndef _from_text(text, table):\n    flags = 0\n    tokens = text.split()\n    for t in tokens:\n        flags = flags | table[t.upper()]\n    return flags\n\n\ndef _to_text(flags, table, order):\n    text_flags = []\n    for k, v in order:\n        if flags & k != 0:\n            text_flags.append(v)\n    return ' '.join(text_flags)\n\n\ndef from_text(text):\n    \"\"\"Convert a space-separated list of flag text values into a flags\n    value.\n\n    Returns an ``int``\n    \"\"\"\n\n    return _from_text(text, _by_text)\n\n\ndef to_text(flags):\n    \"\"\"Convert a flags value into a space-separated list of flag text\n    values.\n\n    Returns a ``text``.\n    \"\"\"\n\n    return _to_text(flags, _by_value, _flags_order)\n\n\ndef edns_from_text(text):\n    \"\"\"Convert a space-separated list of EDNS flag text values into a EDNS\n    flags value.\n\n    Returns an ``int``\n    \"\"\"\n\n    return _from_text(text, _edns_by_text)\n\n\ndef edns_to_text(flags):\n    \"\"\"Convert an EDNS flags value into a space-separated list of EDNS flag\n    text values.\n\n    Returns a ``text``.\n    \"\"\"\n\n    return _to_text(flags, _edns_by_value, _edns_flags_order)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/grange.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2012-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS GENERATE range conversion.\"\"\"\n\nimport dns\n\ndef from_text(text):\n    \"\"\"Convert the text form of a range in a ``$GENERATE`` statement to an\n    integer.\n\n    *text*, a ``str``, the textual range in ``$GENERATE`` form.\n\n    Returns a tuple of three ``int`` values ``(start, stop, step)``.\n    \"\"\"\n\n    # TODO, figure out the bounds on start, stop and step.\n    step = 1\n    cur = ''\n    state = 0\n    # state   0 1 2 3 4\n    #         x - y / z\n\n    if text and text[0] == '-':\n        raise dns.exception.SyntaxError(\"Start cannot be a negative number\")\n\n    for c in text:\n        if c == '-' and state == 0:\n            start = int(cur)\n            cur = ''\n            state = 2\n        elif c == '/':\n            stop = int(cur)\n            cur = ''\n            state = 4\n        elif c.isdigit():\n            cur += c\n        else:\n            raise dns.exception.SyntaxError(\"Could not parse %s\" % (c))\n\n    if state in (1, 3):\n        raise dns.exception.SyntaxError()\n\n    if state == 2:\n        stop = int(cur)\n\n    if state == 4:\n        step = int(cur)\n\n    assert step >= 1\n    assert start >= 0\n    assert start <= stop\n    # TODO, can start == stop?\n\n    return (start, stop, step)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/hash.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"Hashing backwards compatibility wrapper\"\"\"\n\nimport hashlib\nimport warnings\n\nwarnings.warn(\n    \"dns.hash module will be removed in future versions. Please use hashlib instead.\",\n    DeprecationWarning)\n\nhashes = {}\nhashes['MD5'] = hashlib.md5\nhashes['SHA1'] = hashlib.sha1\nhashes['SHA224'] = hashlib.sha224\nhashes['SHA256'] = hashlib.sha256\nhashes['SHA384'] = hashlib.sha384\nhashes['SHA512'] = hashlib.sha512\n\n\ndef get(algorithm):\n    return hashes[algorithm.upper()]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/inet.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"Generic Internet address helper functions.\"\"\"\n\nimport socket\n\nimport dns.ipv4\nimport dns.ipv6\n\nfrom ._compat import maybe_ord\n\n# We assume that AF_INET is always defined.\n\nAF_INET = socket.AF_INET\n\n# AF_INET6 might not be defined in the socket module, but we need it.\n# We'll try to use the socket module's value, and if it doesn't work,\n# we'll use our own value.\n\ntry:\n    AF_INET6 = socket.AF_INET6\nexcept AttributeError:\n    AF_INET6 = 9999\n\n\ndef inet_pton(family, text):\n    \"\"\"Convert the textual form of a network address into its binary form.\n\n    *family* is an ``int``, the address family.\n\n    *text* is a ``text``, the textual address.\n\n    Raises ``NotImplementedError`` if the address family specified is not\n    implemented.\n\n    Returns a ``binary``.\n    \"\"\"\n\n    if family == AF_INET:\n        return dns.ipv4.inet_aton(text)\n    elif family == AF_INET6:\n        return dns.ipv6.inet_aton(text)\n    else:\n        raise NotImplementedError\n\n\ndef inet_ntop(family, address):\n    \"\"\"Convert the binary form of a network address into its textual form.\n\n    *family* is an ``int``, the address family.\n\n    *address* is a ``binary``, the network address in binary form.\n\n    Raises ``NotImplementedError`` if the address family specified is not\n    implemented.\n\n    Returns a ``text``.\n    \"\"\"\n\n    if family == AF_INET:\n        return dns.ipv4.inet_ntoa(address)\n    elif family == AF_INET6:\n        return dns.ipv6.inet_ntoa(address)\n    else:\n        raise NotImplementedError\n\n\ndef af_for_address(text):\n    \"\"\"Determine the address family of a textual-form network address.\n\n    *text*, a ``text``, the textual address.\n\n    Raises ``ValueError`` if the address family cannot be determined\n    from the input.\n\n    Returns an ``int``.\n    \"\"\"\n\n    try:\n        dns.ipv4.inet_aton(text)\n        return AF_INET\n    except Exception:\n        try:\n            dns.ipv6.inet_aton(text)\n            return AF_INET6\n        except:\n            raise ValueError\n\n\ndef is_multicast(text):\n    \"\"\"Is the textual-form network address a multicast address?\n\n    *text*, a ``text``, the textual address.\n\n    Raises ``ValueError`` if the address family cannot be determined\n    from the input.\n\n    Returns a ``bool``.\n    \"\"\"\n\n    try:\n        first = maybe_ord(dns.ipv4.inet_aton(text)[0])\n        return first >= 224 and first <= 239\n    except Exception:\n        try:\n            first = maybe_ord(dns.ipv6.inet_aton(text)[0])\n            return first == 255\n        except Exception:\n            raise ValueError\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/ipv4.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"IPv4 helper functions.\"\"\"\n\nimport struct\n\nimport dns.exception\nfrom ._compat import binary_type\n\ndef inet_ntoa(address):\n    \"\"\"Convert an IPv4 address in binary form to text form.\n\n    *address*, a ``binary``, the IPv4 address in binary form.\n\n    Returns a ``text``.\n    \"\"\"\n\n    if len(address) != 4:\n        raise dns.exception.SyntaxError\n    if not isinstance(address, bytearray):\n        address = bytearray(address)\n    return ('%u.%u.%u.%u' % (address[0], address[1],\n                             address[2], address[3]))\n\ndef inet_aton(text):\n    \"\"\"Convert an IPv4 address in text form to binary form.\n\n    *text*, a ``text``, the IPv4 address in textual form.\n\n    Returns a ``binary``.\n    \"\"\"\n\n    if not isinstance(text, binary_type):\n        text = text.encode()\n    parts = text.split(b'.')\n    if len(parts) != 4:\n        raise dns.exception.SyntaxError\n    for part in parts:\n        if not part.isdigit():\n            raise dns.exception.SyntaxError\n        if len(part) > 1 and part[0] == '0':\n            # No leading zeros\n            raise dns.exception.SyntaxError\n    try:\n        bytes = [int(part) for part in parts]\n        return struct.pack('BBBB', *bytes)\n    except:\n        raise dns.exception.SyntaxError\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/ipv6.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"IPv6 helper functions.\"\"\"\n\nimport re\nimport binascii\n\nimport dns.exception\nimport dns.ipv4\nfrom ._compat import xrange, binary_type, maybe_decode\n\n_leading_zero = re.compile(r'0+([0-9a-f]+)')\n\ndef inet_ntoa(address):\n    \"\"\"Convert an IPv6 address in binary form to text form.\n\n    *address*, a ``binary``, the IPv6 address in binary form.\n\n    Raises ``ValueError`` if the address isn't 16 bytes long.\n    Returns a ``text``.\n    \"\"\"\n\n    if len(address) != 16:\n        raise ValueError(\"IPv6 addresses are 16 bytes long\")\n    hex = binascii.hexlify(address)\n    chunks = []\n    i = 0\n    l = len(hex)\n    while i < l:\n        chunk = maybe_decode(hex[i : i + 4])\n        # strip leading zeros.  we do this with an re instead of\n        # with lstrip() because lstrip() didn't support chars until\n        # python 2.2.2\n        m = _leading_zero.match(chunk)\n        if not m is None:\n            chunk = m.group(1)\n        chunks.append(chunk)\n        i += 4\n    #\n    # Compress the longest subsequence of 0-value chunks to ::\n    #\n    best_start = 0\n    best_len = 0\n    start = -1\n    last_was_zero = False\n    for i in xrange(8):\n        if chunks[i] != '0':\n            if last_was_zero:\n                end = i\n                current_len = end - start\n                if current_len > best_len:\n                    best_start = start\n                    best_len = current_len\n                last_was_zero = False\n        elif not last_was_zero:\n            start = i\n            last_was_zero = True\n    if last_was_zero:\n        end = 8\n        current_len = end - start\n        if current_len > best_len:\n            best_start = start\n            best_len = current_len\n    if best_len > 1:\n        if best_start == 0 and \\\n           (best_len == 6 or\n            best_len == 5 and chunks[5] == 'ffff'):\n            # We have an embedded IPv4 address\n            if best_len == 6:\n                prefix = '::'\n            else:\n                prefix = '::ffff:'\n            hex = prefix + dns.ipv4.inet_ntoa(address[12:])\n        else:\n            hex = ':'.join(chunks[:best_start]) + '::' + \\\n                  ':'.join(chunks[best_start + best_len:])\n    else:\n        hex = ':'.join(chunks)\n    return hex\n\n_v4_ending = re.compile(br'(.*):(\\d+\\.\\d+\\.\\d+\\.\\d+)$')\n_colon_colon_start = re.compile(br'::.*')\n_colon_colon_end = re.compile(br'.*::$')\n\ndef inet_aton(text):\n    \"\"\"Convert an IPv6 address in text form to binary form.\n\n    *text*, a ``text``, the IPv6 address in textual form.\n\n    Returns a ``binary``.\n    \"\"\"\n\n    #\n    # Our aim here is not something fast; we just want something that works.\n    #\n    if not isinstance(text, binary_type):\n        text = text.encode()\n\n    if text == b'::':\n        text = b'0::'\n    #\n    # Get rid of the icky dot-quad syntax if we have it.\n    #\n    m = _v4_ending.match(text)\n    if not m is None:\n        b = bytearray(dns.ipv4.inet_aton(m.group(2)))\n        text = (u\"{}:{:02x}{:02x}:{:02x}{:02x}\".format(m.group(1).decode(),\n                                                       b[0], b[1], b[2],\n                                                       b[3])).encode()\n    #\n    # Try to turn '::<whatever>' into ':<whatever>'; if no match try to\n    # turn '<whatever>::' into '<whatever>:'\n    #\n    m = _colon_colon_start.match(text)\n    if not m is None:\n        text = text[1:]\n    else:\n        m = _colon_colon_end.match(text)\n        if not m is None:\n            text = text[:-1]\n    #\n    # Now canonicalize into 8 chunks of 4 hex digits each\n    #\n    chunks = text.split(b':')\n    l = len(chunks)\n    if l > 8:\n        raise dns.exception.SyntaxError\n    seen_empty = False\n    canonical = []\n    for c in chunks:\n        if c == b'':\n            if seen_empty:\n                raise dns.exception.SyntaxError\n            seen_empty = True\n            for i in xrange(0, 8 - l + 1):\n                canonical.append(b'0000')\n        else:\n            lc = len(c)\n            if lc > 4:\n                raise dns.exception.SyntaxError\n            if lc != 4:\n                c = (b'0' * (4 - lc)) + c\n            canonical.append(c)\n    if l < 8 and not seen_empty:\n        raise dns.exception.SyntaxError\n    text = b''.join(canonical)\n\n    #\n    # Finally we can go to binary.\n    #\n    try:\n        return binascii.unhexlify(text)\n    except (binascii.Error, TypeError):\n        raise dns.exception.SyntaxError\n\n_mapped_prefix = b'\\x00' * 10 + b'\\xff\\xff'\n\ndef is_mapped(address):\n    \"\"\"Is the specified address a mapped IPv4 address?\n\n    *address*, a ``binary`` is an IPv6 address in binary form.\n\n    Returns a ``bool``.\n    \"\"\"\n\n    return address.startswith(_mapped_prefix)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/message.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2001-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS Messages\"\"\"\n\nfrom __future__ import absolute_import\n\nfrom io import StringIO\nimport struct\nimport time\n\nimport dns.edns\nimport dns.exception\nimport dns.flags\nimport dns.name\nimport dns.opcode\nimport dns.entropy\nimport dns.rcode\nimport dns.rdata\nimport dns.rdataclass\nimport dns.rdatatype\nimport dns.rrset\nimport dns.renderer\nimport dns.tsig\nimport dns.wiredata\n\nfrom ._compat import long, xrange, string_types\n\n\nclass ShortHeader(dns.exception.FormError):\n    \"\"\"The DNS packet passed to from_wire() is too short.\"\"\"\n\n\nclass TrailingJunk(dns.exception.FormError):\n    \"\"\"The DNS packet passed to from_wire() has extra junk at the end of it.\"\"\"\n\n\nclass UnknownHeaderField(dns.exception.DNSException):\n    \"\"\"The header field name was not recognized when converting from text\n    into a message.\"\"\"\n\n\nclass BadEDNS(dns.exception.FormError):\n    \"\"\"An OPT record occurred somewhere other than the start of\n    the additional data section.\"\"\"\n\n\nclass BadTSIG(dns.exception.FormError):\n    \"\"\"A TSIG record occurred somewhere other than the end of\n    the additional data section.\"\"\"\n\n\nclass UnknownTSIGKey(dns.exception.DNSException):\n    \"\"\"A TSIG with an unknown key was received.\"\"\"\n\n\n#: The question section number\nQUESTION = 0\n\n#: The answer section number\nANSWER = 1\n\n#: The authority section number\nAUTHORITY = 2\n\n#: The additional section number\nADDITIONAL = 3\n\nclass Message(object):\n    \"\"\"A DNS message.\"\"\"\n\n    def __init__(self, id=None):\n        if id is None:\n            self.id = dns.entropy.random_16()\n        else:\n            self.id = id\n        self.flags = 0\n        self.question = []\n        self.answer = []\n        self.authority = []\n        self.additional = []\n        self.edns = -1\n        self.ednsflags = 0\n        self.payload = 0\n        self.options = []\n        self.request_payload = 0\n        self.keyring = None\n        self.keyname = None\n        self.keyalgorithm = dns.tsig.default_algorithm\n        self.request_mac = b''\n        self.other_data = b''\n        self.tsig_error = 0\n        self.fudge = 300\n        self.original_id = self.id\n        self.mac = b''\n        self.xfr = False\n        self.origin = None\n        self.tsig_ctx = None\n        self.had_tsig = False\n        self.multi = False\n        self.first = True\n        self.index = {}\n\n    def __repr__(self):\n        return '<DNS message, ID ' + repr(self.id) + '>'\n\n    def __str__(self):\n        return self.to_text()\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        \"\"\"Convert the message to text.\n\n        The *origin*, *relativize*, and any other keyword\n        arguments are passed to the RRset ``to_wire()`` method.\n\n        Returns a ``text``.\n        \"\"\"\n\n        s = StringIO()\n        s.write(u'id %d\\n' % self.id)\n        s.write(u'opcode %s\\n' %\n                dns.opcode.to_text(dns.opcode.from_flags(self.flags)))\n        rc = dns.rcode.from_flags(self.flags, self.ednsflags)\n        s.write(u'rcode %s\\n' % dns.rcode.to_text(rc))\n        s.write(u'flags %s\\n' % dns.flags.to_text(self.flags))\n        if self.edns >= 0:\n            s.write(u'edns %s\\n' % self.edns)\n            if self.ednsflags != 0:\n                s.write(u'eflags %s\\n' %\n                        dns.flags.edns_to_text(self.ednsflags))\n            s.write(u'payload %d\\n' % self.payload)\n        for opt in self.options:\n            s.write(u'option %s\\n' % opt.to_text())\n        is_update = dns.opcode.is_update(self.flags)\n        if is_update:\n            s.write(u';ZONE\\n')\n        else:\n            s.write(u';QUESTION\\n')\n        for rrset in self.question:\n            s.write(rrset.to_text(origin, relativize, **kw))\n            s.write(u'\\n')\n        if is_update:\n            s.write(u';PREREQ\\n')\n        else:\n            s.write(u';ANSWER\\n')\n        for rrset in self.answer:\n            s.write(rrset.to_text(origin, relativize, **kw))\n            s.write(u'\\n')\n        if is_update:\n            s.write(u';UPDATE\\n')\n        else:\n            s.write(u';AUTHORITY\\n')\n        for rrset in self.authority:\n            s.write(rrset.to_text(origin, relativize, **kw))\n            s.write(u'\\n')\n        s.write(u';ADDITIONAL\\n')\n        for rrset in self.additional:\n            s.write(rrset.to_text(origin, relativize, **kw))\n            s.write(u'\\n')\n        #\n        # We strip off the final \\n so the caller can print the result without\n        # doing weird things to get around eccentricities in Python print\n        # formatting\n        #\n        return s.getvalue()[:-1]\n\n    def __eq__(self, other):\n        \"\"\"Two messages are equal if they have the same content in the\n        header, question, answer, and authority sections.\n\n        Returns a ``bool``.\n        \"\"\"\n\n        if not isinstance(other, Message):\n            return False\n        if self.id != other.id:\n            return False\n        if self.flags != other.flags:\n            return False\n        for n in self.question:\n            if n not in other.question:\n                return False\n        for n in other.question:\n            if n not in self.question:\n                return False\n        for n in self.answer:\n            if n not in other.answer:\n                return False\n        for n in other.answer:\n            if n not in self.answer:\n                return False\n        for n in self.authority:\n            if n not in other.authority:\n                return False\n        for n in other.authority:\n            if n not in self.authority:\n                return False\n        return True\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    def is_response(self, other):\n        \"\"\"Is this message a response to *other*?\n\n        Returns a ``bool``.\n        \"\"\"\n\n        if other.flags & dns.flags.QR == 0 or \\\n           self.id != other.id or \\\n           dns.opcode.from_flags(self.flags) != \\\n           dns.opcode.from_flags(other.flags):\n            return False\n        if dns.rcode.from_flags(other.flags, other.ednsflags) != \\\n                dns.rcode.NOERROR:\n            return True\n        if dns.opcode.is_update(self.flags):\n            return True\n        for n in self.question:\n            if n not in other.question:\n                return False\n        for n in other.question:\n            if n not in self.question:\n                return False\n        return True\n\n    def section_number(self, section):\n        \"\"\"Return the \"section number\" of the specified section for use\n        in indexing.  The question section is 0, the answer section is 1,\n        the authority section is 2, and the additional section is 3.\n\n        *section* is one of the section attributes of this message.\n\n        Raises ``ValueError`` if the section isn't known.\n\n        Returns an ``int``.\n        \"\"\"\n\n        if section is self.question:\n            return QUESTION\n        elif section is self.answer:\n            return ANSWER\n        elif section is self.authority:\n            return AUTHORITY\n        elif section is self.additional:\n            return ADDITIONAL\n        else:\n            raise ValueError('unknown section')\n\n    def section_from_number(self, number):\n        \"\"\"Return the \"section number\" of the specified section for use\n        in indexing.  The question section is 0, the answer section is 1,\n        the authority section is 2, and the additional section is 3.\n\n        *section* is one of the section attributes of this message.\n\n        Raises ``ValueError`` if the section isn't known.\n\n        Returns an ``int``.\n        \"\"\"\n\n        if number == QUESTION:\n            return self.question\n        elif number == ANSWER:\n            return self.answer\n        elif number == AUTHORITY:\n            return self.authority\n        elif number == ADDITIONAL:\n            return self.additional\n        else:\n            raise ValueError('unknown section')\n\n    def find_rrset(self, section, name, rdclass, rdtype,\n                   covers=dns.rdatatype.NONE, deleting=None, create=False,\n                   force_unique=False):\n        \"\"\"Find the RRset with the given attributes in the specified section.\n\n        *section*, an ``int`` section number, or one of the section\n        attributes of this message.  This specifies the\n        the section of the message to search.  For example::\n\n            my_message.find_rrset(my_message.answer, name, rdclass, rdtype)\n            my_message.find_rrset(dns.message.ANSWER, name, rdclass, rdtype)\n\n        *name*, a ``dns.name.Name``, the name of the RRset.\n\n        *rdclass*, an ``int``, the class of the RRset.\n\n        *rdtype*, an ``int``, the type of the RRset.\n\n        *covers*, an ``int`` or ``None``, the covers value of the RRset.\n        The default is ``None``.\n\n        *deleting*, an ``int`` or ``None``, the deleting value of the RRset.\n        The default is ``None``.\n\n        *create*, a ``bool``.  If ``True``, create the RRset if it is not found.\n        The created RRset is appended to *section*.\n\n        *force_unique*, a ``bool``.  If ``True`` and *create* is also ``True``,\n        create a new RRset regardless of whether a matching RRset exists\n        already.  The default is ``False``.  This is useful when creating\n        DDNS Update messages, as order matters for them.\n\n        Raises ``KeyError`` if the RRset was not found and create was\n        ``False``.\n\n        Returns a ``dns.rrset.RRset object``.\n        \"\"\"\n\n        if isinstance(section, int):\n            section_number = section\n            section = self.section_from_number(section_number)\n        else:\n            section_number = self.section_number(section)\n        key = (section_number, name, rdclass, rdtype, covers, deleting)\n        if not force_unique:\n            if self.index is not None:\n                rrset = self.index.get(key)\n                if rrset is not None:\n                    return rrset\n            else:\n                for rrset in section:\n                    if rrset.match(name, rdclass, rdtype, covers, deleting):\n                        return rrset\n        if not create:\n            raise KeyError\n        rrset = dns.rrset.RRset(name, rdclass, rdtype, covers, deleting)\n        section.append(rrset)\n        if self.index is not None:\n            self.index[key] = rrset\n        return rrset\n\n    def get_rrset(self, section, name, rdclass, rdtype,\n                  covers=dns.rdatatype.NONE, deleting=None, create=False,\n                  force_unique=False):\n        \"\"\"Get the RRset with the given attributes in the specified section.\n\n        If the RRset is not found, None is returned.\n\n        *section*, an ``int`` section number, or one of the section\n        attributes of this message.  This specifies the\n        the section of the message to search.  For example::\n\n            my_message.get_rrset(my_message.answer, name, rdclass, rdtype)\n            my_message.get_rrset(dns.message.ANSWER, name, rdclass, rdtype)\n\n        *name*, a ``dns.name.Name``, the name of the RRset.\n\n        *rdclass*, an ``int``, the class of the RRset.\n\n        *rdtype*, an ``int``, the type of the RRset.\n\n        *covers*, an ``int`` or ``None``, the covers value of the RRset.\n        The default is ``None``.\n\n        *deleting*, an ``int`` or ``None``, the deleting value of the RRset.\n        The default is ``None``.\n\n        *create*, a ``bool``.  If ``True``, create the RRset if it is not found.\n        The created RRset is appended to *section*.\n\n        *force_unique*, a ``bool``.  If ``True`` and *create* is also ``True``,\n        create a new RRset regardless of whether a matching RRset exists\n        already.  The default is ``False``.  This is useful when creating\n        DDNS Update messages, as order matters for them.\n\n        Returns a ``dns.rrset.RRset object`` or ``None``.\n        \"\"\"\n\n        try:\n            rrset = self.find_rrset(section, name, rdclass, rdtype, covers,\n                                    deleting, create, force_unique)\n        except KeyError:\n            rrset = None\n        return rrset\n\n    def to_wire(self, origin=None, max_size=0, **kw):\n        \"\"\"Return a string containing the message in DNS compressed wire\n        format.\n\n        Additional keyword arguments are passed to the RRset ``to_wire()``\n        method.\n\n        *origin*, a ``dns.name.Name`` or ``None``, the origin to be appended\n        to any relative names.\n\n        *max_size*, an ``int``, the maximum size of the wire format\n        output; default is 0, which means \"the message's request\n        payload, if nonzero, or 65535\".\n\n        Raises ``dns.exception.TooBig`` if *max_size* was exceeded.\n\n        Returns a ``binary``.\n        \"\"\"\n\n        if max_size == 0:\n            if self.request_payload != 0:\n                max_size = self.request_payload\n            else:\n                max_size = 65535\n        if max_size < 512:\n            max_size = 512\n        elif max_size > 65535:\n            max_size = 65535\n        r = dns.renderer.Renderer(self.id, self.flags, max_size, origin)\n        for rrset in self.question:\n            r.add_question(rrset.name, rrset.rdtype, rrset.rdclass)\n        for rrset in self.answer:\n            r.add_rrset(dns.renderer.ANSWER, rrset, **kw)\n        for rrset in self.authority:\n            r.add_rrset(dns.renderer.AUTHORITY, rrset, **kw)\n        if self.edns >= 0:\n            r.add_edns(self.edns, self.ednsflags, self.payload, self.options)\n        for rrset in self.additional:\n            r.add_rrset(dns.renderer.ADDITIONAL, rrset, **kw)\n        r.write_header()\n        if self.keyname is not None:\n            r.add_tsig(self.keyname, self.keyring[self.keyname],\n                       self.fudge, self.original_id, self.tsig_error,\n                       self.other_data, self.request_mac,\n                       self.keyalgorithm)\n            self.mac = r.mac\n        return r.get_wire()\n\n    def use_tsig(self, keyring, keyname=None, fudge=300,\n                 original_id=None, tsig_error=0, other_data=b'',\n                 algorithm=dns.tsig.default_algorithm):\n        \"\"\"When sending, a TSIG signature using the specified keyring\n        and keyname should be added.\n\n        See the documentation of the Message class for a complete\n        description of the keyring dictionary.\n\n        *keyring*, a ``dict``, the TSIG keyring to use.  If a\n        *keyring* is specified but a *keyname* is not, then the key\n        used will be the first key in the *keyring*.  Note that the\n        order of keys in a dictionary is not defined, so applications\n        should supply a keyname when a keyring is used, unless they\n        know the keyring contains only one key.\n\n        *keyname*, a ``dns.name.Name`` or ``None``, the name of the TSIG key\n        to use; defaults to ``None``. The key must be defined in the keyring.\n\n        *fudge*, an ``int``, the TSIG time fudge.\n\n        *original_id*, an ``int``, the TSIG original id.  If ``None``,\n        the message's id is used.\n\n        *tsig_error*, an ``int``, the TSIG error code.\n\n        *other_data*, a ``binary``, the TSIG other data.\n\n        *algorithm*, a ``dns.name.Name``, the TSIG algorithm to use.\n        \"\"\"\n\n        self.keyring = keyring\n        if keyname is None:\n            self.keyname = list(self.keyring.keys())[0]\n        else:\n            if isinstance(keyname, string_types):\n                keyname = dns.name.from_text(keyname)\n            self.keyname = keyname\n        self.keyalgorithm = algorithm\n        self.fudge = fudge\n        if original_id is None:\n            self.original_id = self.id\n        else:\n            self.original_id = original_id\n        self.tsig_error = tsig_error\n        self.other_data = other_data\n\n    def use_edns(self, edns=0, ednsflags=0, payload=1280, request_payload=None,\n                 options=None):\n        \"\"\"Configure EDNS behavior.\n\n        *edns*, an ``int``, is the EDNS level to use.  Specifying\n        ``None``, ``False``, or ``-1`` means \"do not use EDNS\", and in this case\n        the other parameters are ignored.  Specifying ``True`` is\n        equivalent to specifying 0, i.e. \"use EDNS0\".\n\n        *ednsflags*, an ``int``, the EDNS flag values.\n\n        *payload*, an ``int``, is the EDNS sender's payload field, which is the\n        maximum size of UDP datagram the sender can handle.  I.e. how big\n        a response to this message can be.\n\n        *request_payload*, an ``int``, is the EDNS payload size to use when\n        sending this message.  If not specified, defaults to the value of\n        *payload*.\n\n        *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS\n        options.\n        \"\"\"\n\n        if edns is None or edns is False:\n            edns = -1\n        if edns is True:\n            edns = 0\n        if request_payload is None:\n            request_payload = payload\n        if edns < 0:\n            ednsflags = 0\n            payload = 0\n            request_payload = 0\n            options = []\n        else:\n            # make sure the EDNS version in ednsflags agrees with edns\n            ednsflags &= long(0xFF00FFFF)\n            ednsflags |= (edns << 16)\n            if options is None:\n                options = []\n        self.edns = edns\n        self.ednsflags = ednsflags\n        self.payload = payload\n        self.options = options\n        self.request_payload = request_payload\n\n    def want_dnssec(self, wanted=True):\n        \"\"\"Enable or disable 'DNSSEC desired' flag in requests.\n\n        *wanted*, a ``bool``.  If ``True``, then DNSSEC data is\n        desired in the response, EDNS is enabled if required, and then\n        the DO bit is set.  If ``False``, the DO bit is cleared if\n        EDNS is enabled.\n        \"\"\"\n\n        if wanted:\n            if self.edns < 0:\n                self.use_edns()\n            self.ednsflags |= dns.flags.DO\n        elif self.edns >= 0:\n            self.ednsflags &= ~dns.flags.DO\n\n    def rcode(self):\n        \"\"\"Return the rcode.\n\n        Returns an ``int``.\n        \"\"\"\n        return dns.rcode.from_flags(self.flags, self.ednsflags)\n\n    def set_rcode(self, rcode):\n        \"\"\"Set the rcode.\n\n        *rcode*, an ``int``, is the rcode to set.\n        \"\"\"\n        (value, evalue) = dns.rcode.to_flags(rcode)\n        self.flags &= 0xFFF0\n        self.flags |= value\n        self.ednsflags &= long(0x00FFFFFF)\n        self.ednsflags |= evalue\n        if self.ednsflags != 0 and self.edns < 0:\n            self.edns = 0\n\n    def opcode(self):\n        \"\"\"Return the opcode.\n\n        Returns an ``int``.\n        \"\"\"\n        return dns.opcode.from_flags(self.flags)\n\n    def set_opcode(self, opcode):\n        \"\"\"Set the opcode.\n\n        *opcode*, an ``int``, is the opcode to set.\n        \"\"\"\n        self.flags &= 0x87FF\n        self.flags |= dns.opcode.to_flags(opcode)\n\n\nclass _WireReader(object):\n\n    \"\"\"Wire format reader.\n\n    wire: a binary, is the wire-format message.\n    message: The message object being built\n    current: When building a message object from wire format, this\n    variable contains the offset from the beginning of wire of the next octet\n    to be read.\n    updating: Is the message a dynamic update?\n    one_rr_per_rrset: Put each RR into its own RRset?\n    ignore_trailing: Ignore trailing junk at end of request?\n    zone_rdclass: The class of the zone in messages which are\n    DNS dynamic updates.\n    \"\"\"\n\n    def __init__(self, wire, message, question_only=False,\n                 one_rr_per_rrset=False, ignore_trailing=False):\n        self.wire = dns.wiredata.maybe_wrap(wire)\n        self.message = message\n        self.current = 0\n        self.updating = False\n        self.zone_rdclass = dns.rdataclass.IN\n        self.question_only = question_only\n        self.one_rr_per_rrset = one_rr_per_rrset\n        self.ignore_trailing = ignore_trailing\n\n    def _get_question(self, qcount):\n        \"\"\"Read the next *qcount* records from the wire data and add them to\n        the question section.\n        \"\"\"\n\n        if self.updating and qcount > 1:\n            raise dns.exception.FormError\n\n        for i in xrange(0, qcount):\n            (qname, used) = dns.name.from_wire(self.wire, self.current)\n            if self.message.origin is not None:\n                qname = qname.relativize(self.message.origin)\n            self.current = self.current + used\n            (rdtype, rdclass) = \\\n                struct.unpack('!HH',\n                              self.wire[self.current:self.current + 4])\n            self.current = self.current + 4\n            self.message.find_rrset(self.message.question, qname,\n                                    rdclass, rdtype, create=True,\n                                    force_unique=True)\n            if self.updating:\n                self.zone_rdclass = rdclass\n\n    def _get_section(self, section, count):\n        \"\"\"Read the next I{count} records from the wire data and add them to\n        the specified section.\n\n        section: the section of the message to which to add records\n        count: the number of records to read\n        \"\"\"\n\n        if self.updating or self.one_rr_per_rrset:\n            force_unique = True\n        else:\n            force_unique = False\n        seen_opt = False\n        for i in xrange(0, count):\n            rr_start = self.current\n            (name, used) = dns.name.from_wire(self.wire, self.current)\n            absolute_name = name\n            if self.message.origin is not None:\n                name = name.relativize(self.message.origin)\n            self.current = self.current + used\n            (rdtype, rdclass, ttl, rdlen) = \\\n                struct.unpack('!HHIH',\n                              self.wire[self.current:self.current + 10])\n            self.current = self.current + 10\n            if rdtype == dns.rdatatype.OPT:\n                if section is not self.message.additional or seen_opt:\n                    raise BadEDNS\n                self.message.payload = rdclass\n                self.message.ednsflags = ttl\n                self.message.edns = (ttl & 0xff0000) >> 16\n                self.message.options = []\n                current = self.current\n                optslen = rdlen\n                while optslen > 0:\n                    (otype, olen) = \\\n                        struct.unpack('!HH',\n                                      self.wire[current:current + 4])\n                    current = current + 4\n                    opt = dns.edns.option_from_wire(\n                        otype, self.wire, current, olen)\n                    self.message.options.append(opt)\n                    current = current + olen\n                    optslen = optslen - 4 - olen\n                seen_opt = True\n            elif rdtype == dns.rdatatype.TSIG:\n                if not (section is self.message.additional and\n                        i == (count - 1)):\n                    raise BadTSIG\n                if self.message.keyring is None:\n                    raise UnknownTSIGKey('got signed message without keyring')\n                secret = self.message.keyring.get(absolute_name)\n                if secret is None:\n                    raise UnknownTSIGKey(\"key '%s' unknown\" % name)\n                self.message.keyname = absolute_name\n                (self.message.keyalgorithm, self.message.mac) = \\\n                    dns.tsig.get_algorithm_and_mac(self.wire, self.current,\n                                                   rdlen)\n                self.message.tsig_ctx = \\\n                    dns.tsig.validate(self.wire,\n                                      absolute_name,\n                                      secret,\n                                      int(time.time()),\n                                      self.message.request_mac,\n                                      rr_start,\n                                      self.current,\n                                      rdlen,\n                                      self.message.tsig_ctx,\n                                      self.message.multi,\n                                      self.message.first)\n                self.message.had_tsig = True\n            else:\n                if ttl < 0:\n                    ttl = 0\n                if self.updating and \\\n                   (rdclass == dns.rdataclass.ANY or\n                        rdclass == dns.rdataclass.NONE):\n                    deleting = rdclass\n                    rdclass = self.zone_rdclass\n                else:\n                    deleting = None\n                if deleting == dns.rdataclass.ANY or \\\n                   (deleting == dns.rdataclass.NONE and\n                        section is self.message.answer):\n                    covers = dns.rdatatype.NONE\n                    rd = None\n                else:\n                    rd = dns.rdata.from_wire(rdclass, rdtype, self.wire,\n                                             self.current, rdlen,\n                                             self.message.origin)\n                    covers = rd.covers()\n                if self.message.xfr and rdtype == dns.rdatatype.SOA:\n                    force_unique = True\n                rrset = self.message.find_rrset(section, name,\n                                                rdclass, rdtype, covers,\n                                                deleting, True, force_unique)\n                if rd is not None:\n                    rrset.add(rd, ttl)\n            self.current = self.current + rdlen\n\n    def read(self):\n        \"\"\"Read a wire format DNS message and build a dns.message.Message\n        object.\"\"\"\n\n        l = len(self.wire)\n        if l < 12:\n            raise ShortHeader\n        (self.message.id, self.message.flags, qcount, ancount,\n         aucount, adcount) = struct.unpack('!HHHHHH', self.wire[:12])\n        self.current = 12\n        if dns.opcode.is_update(self.message.flags):\n            self.updating = True\n        self._get_question(qcount)\n        if self.question_only:\n            return\n        self._get_section(self.message.answer, ancount)\n        self._get_section(self.message.authority, aucount)\n        self._get_section(self.message.additional, adcount)\n        if not self.ignore_trailing and self.current != l:\n            raise TrailingJunk\n        if self.message.multi and self.message.tsig_ctx and \\\n                not self.message.had_tsig:\n            self.message.tsig_ctx.update(self.wire)\n\n\ndef from_wire(wire, keyring=None, request_mac=b'', xfr=False, origin=None,\n              tsig_ctx=None, multi=False, first=True,\n              question_only=False, one_rr_per_rrset=False,\n              ignore_trailing=False):\n    \"\"\"Convert a DNS wire format message into a message\n    object.\n\n    *keyring*, a ``dict``, the keyring to use if the message is signed.\n\n    *request_mac*, a ``binary``.  If the message is a response to a\n    TSIG-signed request, *request_mac* should be set to the MAC of\n    that request.\n\n    *xfr*, a ``bool``, should be set to ``True`` if this message is part of\n    a zone transfer.\n\n    *origin*, a ``dns.name.Name`` or ``None``.  If the message is part\n    of a zone transfer, *origin* should be the origin name of the\n    zone.\n\n    *tsig_ctx*, a ``hmac.HMAC`` objext, the ongoing TSIG context, used\n    when validating zone transfers.\n\n    *multi*, a ``bool``, should be set to ``True`` if this message\n    part of a multiple message sequence.\n\n    *first*, a ``bool``, should be set to ``True`` if this message is\n    stand-alone, or the first message in a multi-message sequence.\n\n    *question_only*, a ``bool``.  If ``True``, read only up to\n    the end of the question section.\n\n    *one_rr_per_rrset*, a ``bool``.  If ``True``, put each RR into its\n    own RRset.\n\n    *ignore_trailing*, a ``bool``.  If ``True``, ignore trailing\n    junk at end of the message.\n\n    Raises ``dns.message.ShortHeader`` if the message is less than 12 octets\n    long.\n\n    Raises ``dns.messaage.TrailingJunk`` if there were octets in the message\n    past the end of the proper DNS message, and *ignore_trailing* is ``False``.\n\n    Raises ``dns.message.BadEDNS`` if an OPT record was in the\n    wrong section, or occurred more than once.\n\n    Raises ``dns.message.BadTSIG`` if a TSIG record was not the last\n    record of the additional data section.\n\n    Returns a ``dns.message.Message``.\n    \"\"\"\n\n    m = Message(id=0)\n    m.keyring = keyring\n    m.request_mac = request_mac\n    m.xfr = xfr\n    m.origin = origin\n    m.tsig_ctx = tsig_ctx\n    m.multi = multi\n    m.first = first\n\n    reader = _WireReader(wire, m, question_only, one_rr_per_rrset,\n                         ignore_trailing)\n    reader.read()\n\n    return m\n\n\nclass _TextReader(object):\n\n    \"\"\"Text format reader.\n\n    tok: the tokenizer.\n    message: The message object being built.\n    updating: Is the message a dynamic update?\n    zone_rdclass: The class of the zone in messages which are\n    DNS dynamic updates.\n    last_name: The most recently read name when building a message object.\n    \"\"\"\n\n    def __init__(self, text, message):\n        self.message = message\n        self.tok = dns.tokenizer.Tokenizer(text)\n        self.last_name = None\n        self.zone_rdclass = dns.rdataclass.IN\n        self.updating = False\n\n    def _header_line(self, section):\n        \"\"\"Process one line from the text format header section.\"\"\"\n\n        token = self.tok.get()\n        what = token.value\n        if what == 'id':\n            self.message.id = self.tok.get_int()\n        elif what == 'flags':\n            while True:\n                token = self.tok.get()\n                if not token.is_identifier():\n                    self.tok.unget(token)\n                    break\n                self.message.flags = self.message.flags | \\\n                    dns.flags.from_text(token.value)\n            if dns.opcode.is_update(self.message.flags):\n                self.updating = True\n        elif what == 'edns':\n            self.message.edns = self.tok.get_int()\n            self.message.ednsflags = self.message.ednsflags | \\\n                (self.message.edns << 16)\n        elif what == 'eflags':\n            if self.message.edns < 0:\n                self.message.edns = 0\n            while True:\n                token = self.tok.get()\n                if not token.is_identifier():\n                    self.tok.unget(token)\n                    break\n                self.message.ednsflags = self.message.ednsflags | \\\n                    dns.flags.edns_from_text(token.value)\n        elif what == 'payload':\n            self.message.payload = self.tok.get_int()\n            if self.message.edns < 0:\n                self.message.edns = 0\n        elif what == 'opcode':\n            text = self.tok.get_string()\n            self.message.flags = self.message.flags | \\\n                dns.opcode.to_flags(dns.opcode.from_text(text))\n        elif what == 'rcode':\n            text = self.tok.get_string()\n            self.message.set_rcode(dns.rcode.from_text(text))\n        else:\n            raise UnknownHeaderField\n        self.tok.get_eol()\n\n    def _question_line(self, section):\n        \"\"\"Process one line from the text format question section.\"\"\"\n\n        token = self.tok.get(want_leading=True)\n        if not token.is_whitespace():\n            self.last_name = dns.name.from_text(token.value, None)\n        name = self.last_name\n        token = self.tok.get()\n        if not token.is_identifier():\n            raise dns.exception.SyntaxError\n        # Class\n        try:\n            rdclass = dns.rdataclass.from_text(token.value)\n            token = self.tok.get()\n            if not token.is_identifier():\n                raise dns.exception.SyntaxError\n        except dns.exception.SyntaxError:\n            raise dns.exception.SyntaxError\n        except Exception:\n            rdclass = dns.rdataclass.IN\n        # Type\n        rdtype = dns.rdatatype.from_text(token.value)\n        self.message.find_rrset(self.message.question, name,\n                                rdclass, rdtype, create=True,\n                                force_unique=True)\n        if self.updating:\n            self.zone_rdclass = rdclass\n        self.tok.get_eol()\n\n    def _rr_line(self, section):\n        \"\"\"Process one line from the text format answer, authority, or\n        additional data sections.\n        \"\"\"\n\n        deleting = None\n        # Name\n        token = self.tok.get(want_leading=True)\n        if not token.is_whitespace():\n            self.last_name = dns.name.from_text(token.value, None)\n        name = self.last_name\n        token = self.tok.get()\n        if not token.is_identifier():\n            raise dns.exception.SyntaxError\n        # TTL\n        try:\n            ttl = int(token.value, 0)\n            token = self.tok.get()\n            if not token.is_identifier():\n                raise dns.exception.SyntaxError\n        except dns.exception.SyntaxError:\n            raise dns.exception.SyntaxError\n        except Exception:\n            ttl = 0\n        # Class\n        try:\n            rdclass = dns.rdataclass.from_text(token.value)\n            token = self.tok.get()\n            if not token.is_identifier():\n                raise dns.exception.SyntaxError\n            if rdclass == dns.rdataclass.ANY or rdclass == dns.rdataclass.NONE:\n                deleting = rdclass\n                rdclass = self.zone_rdclass\n        except dns.exception.SyntaxError:\n            raise dns.exception.SyntaxError\n        except Exception:\n            rdclass = dns.rdataclass.IN\n        # Type\n        rdtype = dns.rdatatype.from_text(token.value)\n        token = self.tok.get()\n        if not token.is_eol_or_eof():\n            self.tok.unget(token)\n            rd = dns.rdata.from_text(rdclass, rdtype, self.tok, None)\n            covers = rd.covers()\n        else:\n            rd = None\n            covers = dns.rdatatype.NONE\n        rrset = self.message.find_rrset(section, name,\n                                        rdclass, rdtype, covers,\n                                        deleting, True, self.updating)\n        if rd is not None:\n            rrset.add(rd, ttl)\n\n    def read(self):\n        \"\"\"Read a text format DNS message and build a dns.message.Message\n        object.\"\"\"\n\n        line_method = self._header_line\n        section = None\n        while 1:\n            token = self.tok.get(True, True)\n            if token.is_eol_or_eof():\n                break\n            if token.is_comment():\n                u = token.value.upper()\n                if u == 'HEADER':\n                    line_method = self._header_line\n                elif u == 'QUESTION' or u == 'ZONE':\n                    line_method = self._question_line\n                    section = self.message.question\n                elif u == 'ANSWER' or u == 'PREREQ':\n                    line_method = self._rr_line\n                    section = self.message.answer\n                elif u == 'AUTHORITY' or u == 'UPDATE':\n                    line_method = self._rr_line\n                    section = self.message.authority\n                elif u == 'ADDITIONAL':\n                    line_method = self._rr_line\n                    section = self.message.additional\n                self.tok.get_eol()\n                continue\n            self.tok.unget(token)\n            line_method(section)\n\n\ndef from_text(text):\n    \"\"\"Convert the text format message into a message object.\n\n    *text*, a ``text``, the text format message.\n\n    Raises ``dns.message.UnknownHeaderField`` if a header is unknown.\n\n    Raises ``dns.exception.SyntaxError`` if the text is badly formed.\n\n    Returns a ``dns.message.Message object``\n    \"\"\"\n\n    # 'text' can also be a file, but we don't publish that fact\n    # since it's an implementation detail.  The official file\n    # interface is from_file().\n\n    m = Message()\n\n    reader = _TextReader(text, m)\n    reader.read()\n\n    return m\n\n\ndef from_file(f):\n    \"\"\"Read the next text format message from the specified file.\n\n    *f*, a ``file`` or ``text``.  If *f* is text, it is treated as the\n    pathname of a file to open.\n\n    Raises ``dns.message.UnknownHeaderField`` if a header is unknown.\n\n    Raises ``dns.exception.SyntaxError`` if the text is badly formed.\n\n    Returns a ``dns.message.Message object``\n    \"\"\"\n\n    str_type = string_types\n    opts = 'rU'\n\n    if isinstance(f, str_type):\n        f = open(f, opts)\n        want_close = True\n    else:\n        want_close = False\n\n    try:\n        m = from_text(f)\n    finally:\n        if want_close:\n            f.close()\n    return m\n\n\ndef make_query(qname, rdtype, rdclass=dns.rdataclass.IN, use_edns=None,\n               want_dnssec=False, ednsflags=None, payload=None,\n               request_payload=None, options=None):\n    \"\"\"Make a query message.\n\n    The query name, type, and class may all be specified either\n    as objects of the appropriate type, or as strings.\n\n    The query will have a randomly chosen query id, and its DNS flags\n    will be set to dns.flags.RD.\n\n    qname, a ``dns.name.Name`` or ``text``, the query name.\n\n    *rdtype*, an ``int`` or ``text``, the desired rdata type.\n\n    *rdclass*, an ``int`` or ``text``,  the desired rdata class; the default\n    is class IN.\n\n    *use_edns*, an ``int``, ``bool`` or ``None``.  The EDNS level to use; the\n    default is None (no EDNS).\n    See the description of dns.message.Message.use_edns() for the possible\n    values for use_edns and their meanings.\n\n    *want_dnssec*, a ``bool``.  If ``True``, DNSSEC data is desired.\n\n    *ednsflags*, an ``int``, the EDNS flag values.\n\n    *payload*, an ``int``, is the EDNS sender's payload field, which is the\n    maximum size of UDP datagram the sender can handle.  I.e. how big\n    a response to this message can be.\n\n    *request_payload*, an ``int``, is the EDNS payload size to use when\n    sending this message.  If not specified, defaults to the value of\n    *payload*.\n\n    *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS\n    options.\n\n    Returns a ``dns.message.Message``\n    \"\"\"\n\n    if isinstance(qname, string_types):\n        qname = dns.name.from_text(qname)\n    if isinstance(rdtype, string_types):\n        rdtype = dns.rdatatype.from_text(rdtype)\n    if isinstance(rdclass, string_types):\n        rdclass = dns.rdataclass.from_text(rdclass)\n    m = Message()\n    m.flags |= dns.flags.RD\n    m.find_rrset(m.question, qname, rdclass, rdtype, create=True,\n                 force_unique=True)\n    # only pass keywords on to use_edns if they have been set to a\n    # non-None value.  Setting a field will turn EDNS on if it hasn't\n    # been configured.\n    kwargs = {}\n    if ednsflags is not None:\n        kwargs['ednsflags'] = ednsflags\n        if use_edns is None:\n            use_edns = 0\n    if payload is not None:\n        kwargs['payload'] = payload\n        if use_edns is None:\n            use_edns = 0\n    if request_payload is not None:\n        kwargs['request_payload'] = request_payload\n        if use_edns is None:\n            use_edns = 0\n    if options is not None:\n        kwargs['options'] = options\n        if use_edns is None:\n            use_edns = 0\n    kwargs['edns'] = use_edns\n    m.use_edns(**kwargs)\n    m.want_dnssec(want_dnssec)\n    return m\n\n\ndef make_response(query, recursion_available=False, our_payload=8192,\n                  fudge=300):\n    \"\"\"Make a message which is a response for the specified query.\n    The message returned is really a response skeleton; it has all\n    of the infrastructure required of a response, but none of the\n    content.\n\n    The response's question section is a shallow copy of the query's\n    question section, so the query's question RRsets should not be\n    changed.\n\n    *query*, a ``dns.message.Message``, the query to respond to.\n\n    *recursion_available*, a ``bool``, should RA be set in the response?\n\n    *our_payload*, an ``int``, the payload size to advertise in EDNS\n    responses.\n\n    *fudge*, an ``int``, the TSIG time fudge.\n\n    Returns a ``dns.message.Message`` object.\n    \"\"\"\n\n    if query.flags & dns.flags.QR:\n        raise dns.exception.FormError('specified query message is not a query')\n    response = dns.message.Message(query.id)\n    response.flags = dns.flags.QR | (query.flags & dns.flags.RD)\n    if recursion_available:\n        response.flags |= dns.flags.RA\n    response.set_opcode(query.opcode())\n    response.question = list(query.question)\n    if query.edns >= 0:\n        response.use_edns(0, 0, our_payload, query.payload)\n    if query.had_tsig:\n        response.use_tsig(query.keyring, query.keyname, fudge, None, 0, b'',\n                          query.keyalgorithm)\n        response.request_mac = query.mac\n    return response\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/name.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2001-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS Names.\n\"\"\"\n\nfrom io import BytesIO\nimport struct\nimport sys\nimport copy\nimport encodings.idna\ntry:\n    import idna\n    have_idna_2008 = True\nexcept ImportError:\n    have_idna_2008 = False\n\nimport dns.exception\nimport dns.wiredata\n\nfrom ._compat import long, binary_type, text_type, unichr, maybe_decode\n\ntry:\n    maxint = sys.maxint  # pylint: disable=sys-max-int\nexcept AttributeError:\n    maxint = (1 << (8 * struct.calcsize(\"P\"))) // 2 - 1\n\n\n# fullcompare() result values\n\n#: The compared names have no relationship to each other.\nNAMERELN_NONE = 0\n#: the first name is a superdomain of the second.\nNAMERELN_SUPERDOMAIN = 1\n#: The first name is a subdomain of the second.\nNAMERELN_SUBDOMAIN = 2\n#: The compared names are equal.\nNAMERELN_EQUAL = 3\n#: The compared names have a common ancestor.\nNAMERELN_COMMONANCESTOR = 4\n\n\nclass EmptyLabel(dns.exception.SyntaxError):\n    \"\"\"A DNS label is empty.\"\"\"\n\n\nclass BadEscape(dns.exception.SyntaxError):\n    \"\"\"An escaped code in a text format of DNS name is invalid.\"\"\"\n\n\nclass BadPointer(dns.exception.FormError):\n    \"\"\"A DNS compression pointer points forward instead of backward.\"\"\"\n\n\nclass BadLabelType(dns.exception.FormError):\n    \"\"\"The label type in DNS name wire format is unknown.\"\"\"\n\n\nclass NeedAbsoluteNameOrOrigin(dns.exception.DNSException):\n    \"\"\"An attempt was made to convert a non-absolute name to\n    wire when there was also a non-absolute (or missing) origin.\"\"\"\n\n\nclass NameTooLong(dns.exception.FormError):\n    \"\"\"A DNS name is > 255 octets long.\"\"\"\n\n\nclass LabelTooLong(dns.exception.SyntaxError):\n    \"\"\"A DNS label is > 63 octets long.\"\"\"\n\n\nclass AbsoluteConcatenation(dns.exception.DNSException):\n    \"\"\"An attempt was made to append anything other than the\n    empty name to an absolute DNS name.\"\"\"\n\n\nclass NoParent(dns.exception.DNSException):\n    \"\"\"An attempt was made to get the parent of the root name\n    or the empty name.\"\"\"\n\nclass NoIDNA2008(dns.exception.DNSException):\n    \"\"\"IDNA 2008 processing was requested but the idna module is not\n    available.\"\"\"\n\n\nclass IDNAException(dns.exception.DNSException):\n    \"\"\"IDNA processing raised an exception.\"\"\"\n\n    supp_kwargs = {'idna_exception'}\n    fmt = \"IDNA processing exception: {idna_exception}\"\n\n\nclass IDNACodec(object):\n    \"\"\"Abstract base class for IDNA encoder/decoders.\"\"\"\n\n    def __init__(self):\n        pass\n\n    def encode(self, label):\n        raise NotImplementedError\n\n    def decode(self, label):\n        # We do not apply any IDNA policy on decode; we just\n        downcased = label.lower()\n        if downcased.startswith(b'xn--'):\n            try:\n                label = downcased[4:].decode('punycode')\n            except Exception as e:\n                raise IDNAException(idna_exception=e)\n        else:\n            label = maybe_decode(label)\n        return _escapify(label, True)\n\n\nclass IDNA2003Codec(IDNACodec):\n    \"\"\"IDNA 2003 encoder/decoder.\"\"\"\n\n    def __init__(self, strict_decode=False):\n        \"\"\"Initialize the IDNA 2003 encoder/decoder.\n\n        *strict_decode* is a ``bool``. If `True`, then IDNA2003 checking\n        is done when decoding.  This can cause failures if the name\n        was encoded with IDNA2008.  The default is `False`.\n        \"\"\"\n\n        super(IDNA2003Codec, self).__init__()\n        self.strict_decode = strict_decode\n\n    def encode(self, label):\n        \"\"\"Encode *label*.\"\"\"\n\n        if label == '':\n            return b''\n        try:\n            return encodings.idna.ToASCII(label)\n        except UnicodeError:\n            raise LabelTooLong\n\n    def decode(self, label):\n        \"\"\"Decode *label*.\"\"\"\n        if not self.strict_decode:\n            return super(IDNA2003Codec, self).decode(label)\n        if label == b'':\n            return u''\n        try:\n            return _escapify(encodings.idna.ToUnicode(label), True)\n        except Exception as e:\n            raise IDNAException(idna_exception=e)\n\n\nclass IDNA2008Codec(IDNACodec):\n    \"\"\"IDNA 2008 encoder/decoder.\n\n        *uts_46* is a ``bool``.  If True, apply Unicode IDNA\n        compatibility processing as described in Unicode Technical\n        Standard #46 (http://unicode.org/reports/tr46/).\n        If False, do not apply the mapping.  The default is False.\n\n        *transitional* is a ``bool``: If True, use the\n        \"transitional\" mode described in Unicode Technical Standard\n        #46.  The default is False.\n\n        *allow_pure_ascii* is a ``bool``.  If True, then a label which\n        consists of only ASCII characters is allowed.  This is less\n        strict than regular IDNA 2008, but is also necessary for mixed\n        names, e.g. a name with starting with \"_sip._tcp.\" and ending\n        in an IDN suffix which would otherwise be disallowed.  The\n        default is False.\n\n        *strict_decode* is a ``bool``: If True, then IDNA2008 checking\n        is done when decoding.  This can cause failures if the name\n        was encoded with IDNA2003.  The default is False.\n        \"\"\"\n\n    def __init__(self, uts_46=False, transitional=False,\n                 allow_pure_ascii=False, strict_decode=False):\n        \"\"\"Initialize the IDNA 2008 encoder/decoder.\"\"\"\n        super(IDNA2008Codec, self).__init__()\n        self.uts_46 = uts_46\n        self.transitional = transitional\n        self.allow_pure_ascii = allow_pure_ascii\n        self.strict_decode = strict_decode\n\n    def is_all_ascii(self, label):\n        for c in label:\n            if ord(c) > 0x7f:\n                return False\n        return True\n\n    def encode(self, label):\n        if label == '':\n            return b''\n        if self.allow_pure_ascii and self.is_all_ascii(label):\n            return label.encode('ascii')\n        if not have_idna_2008:\n            raise NoIDNA2008\n        try:\n            if self.uts_46:\n                label = idna.uts46_remap(label, False, self.transitional)\n            return idna.alabel(label)\n        except idna.IDNAError as e:\n            raise IDNAException(idna_exception=e)\n\n    def decode(self, label):\n        if not self.strict_decode:\n            return super(IDNA2008Codec, self).decode(label)\n        if label == b'':\n            return u''\n        if not have_idna_2008:\n            raise NoIDNA2008\n        try:\n            if self.uts_46:\n                label = idna.uts46_remap(label, False, False)\n            return _escapify(idna.ulabel(label), True)\n        except idna.IDNAError as e:\n            raise IDNAException(idna_exception=e)\n\n_escaped = bytearray(b'\"().;\\\\@$')\n\nIDNA_2003_Practical = IDNA2003Codec(False)\nIDNA_2003_Strict = IDNA2003Codec(True)\nIDNA_2003 = IDNA_2003_Practical\nIDNA_2008_Practical = IDNA2008Codec(True, False, True, False)\nIDNA_2008_UTS_46 = IDNA2008Codec(True, False, False, False)\nIDNA_2008_Strict = IDNA2008Codec(False, False, False, True)\nIDNA_2008_Transitional = IDNA2008Codec(True, True, False, False)\nIDNA_2008 = IDNA_2008_Practical\n\ndef _escapify(label, unicode_mode=False):\n    \"\"\"Escape the characters in label which need it.\n    @param unicode_mode: escapify only special and whitespace (<= 0x20)\n    characters\n    @returns: the escaped string\n    @rtype: string\"\"\"\n    if not unicode_mode:\n        text = ''\n        if isinstance(label, text_type):\n            label = label.encode()\n        for c in bytearray(label):\n            if c in _escaped:\n                text += '\\\\' + chr(c)\n            elif c > 0x20 and c < 0x7F:\n                text += chr(c)\n            else:\n                text += '\\\\%03d' % c\n        return text.encode()\n\n    text = u''\n    if isinstance(label, binary_type):\n        label = label.decode()\n    for c in label:\n        if c > u'\\x20' and c < u'\\x7f':\n            text += c\n        else:\n            if c >= u'\\x7f':\n                text += c\n            else:\n                text += u'\\\\%03d' % ord(c)\n    return text\n\ndef _validate_labels(labels):\n    \"\"\"Check for empty labels in the middle of a label sequence,\n    labels that are too long, and for too many labels.\n\n    Raises ``dns.name.NameTooLong`` if the name as a whole is too long.\n\n    Raises ``dns.name.EmptyLabel`` if a label is empty (i.e. the root\n    label) and appears in a position other than the end of the label\n    sequence\n\n    \"\"\"\n\n    l = len(labels)\n    total = 0\n    i = -1\n    j = 0\n    for label in labels:\n        ll = len(label)\n        total += ll + 1\n        if ll > 63:\n            raise LabelTooLong\n        if i < 0 and label == b'':\n            i = j\n        j += 1\n    if total > 255:\n        raise NameTooLong\n    if i >= 0 and i != l - 1:\n        raise EmptyLabel\n\n\ndef _maybe_convert_to_binary(label):\n    \"\"\"If label is ``text``, convert it to ``binary``.  If it is already\n    ``binary`` just return it.\n\n    \"\"\"\n\n    if isinstance(label, binary_type):\n        return label\n    if isinstance(label, text_type):\n        return label.encode()\n    raise ValueError\n\n\nclass Name(object):\n\n    \"\"\"A DNS name.\n\n    The dns.name.Name class represents a DNS name as a tuple of\n    labels.  Each label is a `binary` in DNS wire format.  Instances\n    of the class are immutable.\n    \"\"\"\n\n    __slots__ = ['labels']\n\n    def __init__(self, labels):\n        \"\"\"*labels* is any iterable whose values are ``text`` or ``binary``.\n        \"\"\"\n\n        labels = [_maybe_convert_to_binary(x) for x in labels]\n        super(Name, self).__setattr__('labels', tuple(labels))\n        _validate_labels(self.labels)\n\n    def __setattr__(self, name, value):\n        # Names are immutable\n        raise TypeError(\"object doesn't support attribute assignment\")\n\n    def __copy__(self):\n        return Name(self.labels)\n\n    def __deepcopy__(self, memo):\n        return Name(copy.deepcopy(self.labels, memo))\n\n    def __getstate__(self):\n        # Names can be pickled\n        return {'labels': self.labels}\n\n    def __setstate__(self, state):\n        super(Name, self).__setattr__('labels', state['labels'])\n        _validate_labels(self.labels)\n\n    def is_absolute(self):\n        \"\"\"Is the most significant label of this name the root label?\n\n        Returns a ``bool``.\n        \"\"\"\n\n        return len(self.labels) > 0 and self.labels[-1] == b''\n\n    def is_wild(self):\n        \"\"\"Is this name wild?  (I.e. Is the least significant label '*'?)\n\n        Returns a ``bool``.\n        \"\"\"\n\n        return len(self.labels) > 0 and self.labels[0] == b'*'\n\n    def __hash__(self):\n        \"\"\"Return a case-insensitive hash of the name.\n\n        Returns an ``int``.\n        \"\"\"\n\n        h = long(0)\n        for label in self.labels:\n            for c in bytearray(label.lower()):\n                h += (h << 3) + c\n        return int(h % maxint)\n\n    def fullcompare(self, other):\n        \"\"\"Compare two names, returning a 3-tuple\n        ``(relation, order, nlabels)``.\n\n        *relation* describes the relation ship between the names,\n        and is one of: ``dns.name.NAMERELN_NONE``,\n        ``dns.name.NAMERELN_SUPERDOMAIN``, ``dns.name.NAMERELN_SUBDOMAIN``,\n        ``dns.name.NAMERELN_EQUAL``, or ``dns.name.NAMERELN_COMMONANCESTOR``.\n\n        *order* is < 0 if *self* < *other*, > 0 if *self* > *other*, and ==\n        0 if *self* == *other*.  A relative name is always less than an\n        absolute name.  If both names have the same relativity, then\n        the DNSSEC order relation is used to order them.\n\n        *nlabels* is the number of significant labels that the two names\n        have in common.\n\n        Here are some examples.  Names ending in \".\" are absolute names,\n        those not ending in \".\" are relative names.\n\n        =============  =============  ===========  =====  =======\n        self           other          relation     order  nlabels\n        =============  =============  ===========  =====  =======\n        www.example.   www.example.   equal        0      3\n        www.example.   example.       subdomain    > 0    2\n        example.       www.example.   superdomain  < 0    2\n        example1.com.  example2.com.  common anc.  < 0    2\n        example1       example2.      none         < 0    0\n        example1.      example2       none         > 0    0\n        =============  =============  ===========  =====  =======\n        \"\"\"\n\n        sabs = self.is_absolute()\n        oabs = other.is_absolute()\n        if sabs != oabs:\n            if sabs:\n                return (NAMERELN_NONE, 1, 0)\n            else:\n                return (NAMERELN_NONE, -1, 0)\n        l1 = len(self.labels)\n        l2 = len(other.labels)\n        ldiff = l1 - l2\n        if ldiff < 0:\n            l = l1\n        else:\n            l = l2\n\n        order = 0\n        nlabels = 0\n        namereln = NAMERELN_NONE\n        while l > 0:\n            l -= 1\n            l1 -= 1\n            l2 -= 1\n            label1 = self.labels[l1].lower()\n            label2 = other.labels[l2].lower()\n            if label1 < label2:\n                order = -1\n                if nlabels > 0:\n                    namereln = NAMERELN_COMMONANCESTOR\n                return (namereln, order, nlabels)\n            elif label1 > label2:\n                order = 1\n                if nlabels > 0:\n                    namereln = NAMERELN_COMMONANCESTOR\n                return (namereln, order, nlabels)\n            nlabels += 1\n        order = ldiff\n        if ldiff < 0:\n            namereln = NAMERELN_SUPERDOMAIN\n        elif ldiff > 0:\n            namereln = NAMERELN_SUBDOMAIN\n        else:\n            namereln = NAMERELN_EQUAL\n        return (namereln, order, nlabels)\n\n    def is_subdomain(self, other):\n        \"\"\"Is self a subdomain of other?\n\n        Note that the notion of subdomain includes equality, e.g.\n        \"dnpython.org\" is a subdomain of itself.\n\n        Returns a ``bool``.\n        \"\"\"\n\n        (nr, o, nl) = self.fullcompare(other)\n        if nr == NAMERELN_SUBDOMAIN or nr == NAMERELN_EQUAL:\n            return True\n        return False\n\n    def is_superdomain(self, other):\n        \"\"\"Is self a superdomain of other?\n\n        Note that the notion of superdomain includes equality, e.g.\n        \"dnpython.org\" is a superdomain of itself.\n\n        Returns a ``bool``.\n        \"\"\"\n\n        (nr, o, nl) = self.fullcompare(other)\n        if nr == NAMERELN_SUPERDOMAIN or nr == NAMERELN_EQUAL:\n            return True\n        return False\n\n    def canonicalize(self):\n        \"\"\"Return a name which is equal to the current name, but is in\n        DNSSEC canonical form.\n        \"\"\"\n\n        return Name([x.lower() for x in self.labels])\n\n    def __eq__(self, other):\n        if isinstance(other, Name):\n            return self.fullcompare(other)[1] == 0\n        else:\n            return False\n\n    def __ne__(self, other):\n        if isinstance(other, Name):\n            return self.fullcompare(other)[1] != 0\n        else:\n            return True\n\n    def __lt__(self, other):\n        if isinstance(other, Name):\n            return self.fullcompare(other)[1] < 0\n        else:\n            return NotImplemented\n\n    def __le__(self, other):\n        if isinstance(other, Name):\n            return self.fullcompare(other)[1] <= 0\n        else:\n            return NotImplemented\n\n    def __ge__(self, other):\n        if isinstance(other, Name):\n            return self.fullcompare(other)[1] >= 0\n        else:\n            return NotImplemented\n\n    def __gt__(self, other):\n        if isinstance(other, Name):\n            return self.fullcompare(other)[1] > 0\n        else:\n            return NotImplemented\n\n    def __repr__(self):\n        return '<DNS name ' + self.__str__() + '>'\n\n    def __str__(self):\n        return self.to_text(False)\n\n    def to_text(self, omit_final_dot=False):\n        \"\"\"Convert name to DNS text format.\n\n        *omit_final_dot* is a ``bool``.  If True, don't emit the final\n        dot (denoting the root label) for absolute names.  The default\n        is False.\n\n        Returns a ``text``.\n        \"\"\"\n\n        if len(self.labels) == 0:\n            return maybe_decode(b'@')\n        if len(self.labels) == 1 and self.labels[0] == b'':\n            return maybe_decode(b'.')\n        if omit_final_dot and self.is_absolute():\n            l = self.labels[:-1]\n        else:\n            l = self.labels\n        s = b'.'.join(map(_escapify, l))\n        return maybe_decode(s)\n\n    def to_unicode(self, omit_final_dot=False, idna_codec=None):\n        \"\"\"Convert name to Unicode text format.\n\n        IDN ACE labels are converted to Unicode.\n\n        *omit_final_dot* is a ``bool``.  If True, don't emit the final\n        dot (denoting the root label) for absolute names.  The default\n        is False.\n        *idna_codec* specifies the IDNA encoder/decoder.  If None, the\n        dns.name.IDNA_2003_Practical encoder/decoder is used.\n        The IDNA_2003_Practical decoder does\n        not impose any policy, it just decodes punycode, so if you\n        don't want checking for compliance, you can use this decoder\n        for IDNA2008 as well.\n\n        Returns a ``text``.\n        \"\"\"\n\n        if len(self.labels) == 0:\n            return u'@'\n        if len(self.labels) == 1 and self.labels[0] == b'':\n            return u'.'\n        if omit_final_dot and self.is_absolute():\n            l = self.labels[:-1]\n        else:\n            l = self.labels\n        if idna_codec is None:\n            idna_codec = IDNA_2003_Practical\n        return u'.'.join([idna_codec.decode(x) for x in l])\n\n    def to_digestable(self, origin=None):\n        \"\"\"Convert name to a format suitable for digesting in hashes.\n\n        The name is canonicalized and converted to uncompressed wire\n        format.  All names in wire format are absolute.  If the name\n        is a relative name, then an origin must be supplied.\n\n        *origin* is a ``dns.name.Name`` or ``None``.  If the name is\n        relative and origin is not ``None``, then origin will be appended\n        to the name.\n\n        Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is\n        relative and no origin was provided.\n\n        Returns a ``binary``.\n        \"\"\"\n\n        if not self.is_absolute():\n            if origin is None or not origin.is_absolute():\n                raise NeedAbsoluteNameOrOrigin\n            labels = list(self.labels)\n            labels.extend(list(origin.labels))\n        else:\n            labels = self.labels\n        dlabels = [struct.pack('!B%ds' % len(x), len(x), x.lower())\n                   for x in labels]\n        return b''.join(dlabels)\n\n    def to_wire(self, file=None, compress=None, origin=None):\n        \"\"\"Convert name to wire format, possibly compressing it.\n\n        *file* is the file where the name is emitted (typically a\n        BytesIO file).  If ``None`` (the default), a ``binary``\n        containing the wire name will be returned.\n\n        *compress*, a ``dict``, is the compression table to use.  If\n        ``None`` (the default), names will not be compressed.\n\n        *origin* is a ``dns.name.Name`` or ``None``.  If the name is\n        relative and origin is not ``None``, then *origin* will be appended\n        to it.\n\n        Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is\n        relative and no origin was provided.\n\n        Returns a ``binary`` or ``None``.\n        \"\"\"\n\n        if file is None:\n            file = BytesIO()\n            want_return = True\n        else:\n            want_return = False\n\n        if not self.is_absolute():\n            if origin is None or not origin.is_absolute():\n                raise NeedAbsoluteNameOrOrigin\n            labels = list(self.labels)\n            labels.extend(list(origin.labels))\n        else:\n            labels = self.labels\n        i = 0\n        for label in labels:\n            n = Name(labels[i:])\n            i += 1\n            if compress is not None:\n                pos = compress.get(n)\n            else:\n                pos = None\n            if pos is not None:\n                value = 0xc000 + pos\n                s = struct.pack('!H', value)\n                file.write(s)\n                break\n            else:\n                if compress is not None and len(n) > 1:\n                    pos = file.tell()\n                    if pos <= 0x3fff:\n                        compress[n] = pos\n                l = len(label)\n                file.write(struct.pack('!B', l))\n                if l > 0:\n                    file.write(label)\n        if want_return:\n            return file.getvalue()\n\n    def __len__(self):\n        \"\"\"The length of the name (in labels).\n\n        Returns an ``int``.\n        \"\"\"\n\n        return len(self.labels)\n\n    def __getitem__(self, index):\n        return self.labels[index]\n\n    def __add__(self, other):\n        return self.concatenate(other)\n\n    def __sub__(self, other):\n        return self.relativize(other)\n\n    def split(self, depth):\n        \"\"\"Split a name into a prefix and suffix names at the specified depth.\n\n        *depth* is an ``int`` specifying the number of labels in the suffix\n\n        Raises ``ValueError`` if *depth* was not >= 0 and <= the length of the\n        name.\n\n        Returns the tuple ``(prefix, suffix)``.\n        \"\"\"\n\n        l = len(self.labels)\n        if depth == 0:\n            return (self, dns.name.empty)\n        elif depth == l:\n            return (dns.name.empty, self)\n        elif depth < 0 or depth > l:\n            raise ValueError(\n                'depth must be >= 0 and <= the length of the name')\n        return (Name(self[: -depth]), Name(self[-depth:]))\n\n    def concatenate(self, other):\n        \"\"\"Return a new name which is the concatenation of self and other.\n\n        Raises ``dns.name.AbsoluteConcatenation`` if the name is\n        absolute and *other* is not the empty name.\n\n        Returns a ``dns.name.Name``.\n        \"\"\"\n\n        if self.is_absolute() and len(other) > 0:\n            raise AbsoluteConcatenation\n        labels = list(self.labels)\n        labels.extend(list(other.labels))\n        return Name(labels)\n\n    def relativize(self, origin):\n        \"\"\"If the name is a subdomain of *origin*, return a new name which is\n        the name relative to origin.  Otherwise return the name.\n\n        For example, relativizing ``www.dnspython.org.`` to origin\n        ``dnspython.org.`` returns the name ``www``.  Relativizing ``example.``\n        to origin ``dnspython.org.`` returns ``example.``.\n\n        Returns a ``dns.name.Name``.\n        \"\"\"\n\n        if origin is not None and self.is_subdomain(origin):\n            return Name(self[: -len(origin)])\n        else:\n            return self\n\n    def derelativize(self, origin):\n        \"\"\"If the name is a relative name, return a new name which is the\n        concatenation of the name and origin.  Otherwise return the name.\n\n        For example, derelativizing ``www`` to origin ``dnspython.org.``\n        returns the name ``www.dnspython.org.``.  Derelativizing ``example.``\n        to origin ``dnspython.org.`` returns ``example.``.\n\n        Returns a ``dns.name.Name``.\n        \"\"\"\n\n        if not self.is_absolute():\n            return self.concatenate(origin)\n        else:\n            return self\n\n    def choose_relativity(self, origin=None, relativize=True):\n        \"\"\"Return a name with the relativity desired by the caller.\n\n        If *origin* is ``None``, then the name is returned.\n        Otherwise, if *relativize* is ``True`` the name is\n        relativized, and if *relativize* is ``False`` the name is\n        derelativized.\n\n        Returns a ``dns.name.Name``.\n        \"\"\"\n\n        if origin:\n            if relativize:\n                return self.relativize(origin)\n            else:\n                return self.derelativize(origin)\n        else:\n            return self\n\n    def parent(self):\n        \"\"\"Return the parent of the name.\n\n        For example, the parent of ``www.dnspython.org.`` is ``dnspython.org``.\n\n        Raises ``dns.name.NoParent`` if the name is either the root name or the\n        empty name, and thus has no parent.\n\n        Returns a ``dns.name.Name``.\n        \"\"\"\n\n        if self == root or self == empty:\n            raise NoParent\n        return Name(self.labels[1:])\n\n#: The root name, '.'\nroot = Name([b''])\n\n#: The empty name.\nempty = Name([])\n\ndef from_unicode(text, origin=root, idna_codec=None):\n    \"\"\"Convert unicode text into a Name object.\n\n    Labels are encoded in IDN ACE form according to rules specified by\n    the IDNA codec.\n\n    *text*, a ``text``, is the text to convert into a name.\n\n    *origin*, a ``dns.name.Name``, specifies the origin to\n    append to non-absolute names.  The default is the root name.\n\n    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA\n    encoder/decoder.  If ``None``, the default IDNA 2003 encoder/decoder\n    is used.\n\n    Returns a ``dns.name.Name``.\n    \"\"\"\n\n    if not isinstance(text, text_type):\n        raise ValueError(\"input to from_unicode() must be a unicode string\")\n    if not (origin is None or isinstance(origin, Name)):\n        raise ValueError(\"origin must be a Name or None\")\n    labels = []\n    label = u''\n    escaping = False\n    edigits = 0\n    total = 0\n    if idna_codec is None:\n        idna_codec = IDNA_2003\n    if text == u'@':\n        text = u''\n    if text:\n        if text == u'.':\n            return Name([b''])        # no Unicode \"u\" on this constant!\n        for c in text:\n            if escaping:\n                if edigits == 0:\n                    if c.isdigit():\n                        total = int(c)\n                        edigits += 1\n                    else:\n                        label += c\n                        escaping = False\n                else:\n                    if not c.isdigit():\n                        raise BadEscape\n                    total *= 10\n                    total += int(c)\n                    edigits += 1\n                    if edigits == 3:\n                        escaping = False\n                        label += unichr(total)\n            elif c in [u'.', u'\\u3002', u'\\uff0e', u'\\uff61']:\n                if len(label) == 0:\n                    raise EmptyLabel\n                labels.append(idna_codec.encode(label))\n                label = u''\n            elif c == u'\\\\':\n                escaping = True\n                edigits = 0\n                total = 0\n            else:\n                label += c\n        if escaping:\n            raise BadEscape\n        if len(label) > 0:\n            labels.append(idna_codec.encode(label))\n        else:\n            labels.append(b'')\n\n    if (len(labels) == 0 or labels[-1] != b'') and origin is not None:\n        labels.extend(list(origin.labels))\n    return Name(labels)\n\n\ndef from_text(text, origin=root, idna_codec=None):\n    \"\"\"Convert text into a Name object.\n\n    *text*, a ``text``, is the text to convert into a name.\n\n    *origin*, a ``dns.name.Name``, specifies the origin to\n    append to non-absolute names.  The default is the root name.\n\n    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA\n    encoder/decoder.  If ``None``, the default IDNA 2003 encoder/decoder\n    is used.\n\n    Returns a ``dns.name.Name``.\n    \"\"\"\n\n    if isinstance(text, text_type):\n        return from_unicode(text, origin, idna_codec)\n    if not isinstance(text, binary_type):\n        raise ValueError(\"input to from_text() must be a string\")\n    if not (origin is None or isinstance(origin, Name)):\n        raise ValueError(\"origin must be a Name or None\")\n    labels = []\n    label = b''\n    escaping = False\n    edigits = 0\n    total = 0\n    if text == b'@':\n        text = b''\n    if text:\n        if text == b'.':\n            return Name([b''])\n        for c in bytearray(text):\n            byte_ = struct.pack('!B', c)\n            if escaping:\n                if edigits == 0:\n                    if byte_.isdigit():\n                        total = int(byte_)\n                        edigits += 1\n                    else:\n                        label += byte_\n                        escaping = False\n                else:\n                    if not byte_.isdigit():\n                        raise BadEscape\n                    total *= 10\n                    total += int(byte_)\n                    edigits += 1\n                    if edigits == 3:\n                        escaping = False\n                        label += struct.pack('!B', total)\n            elif byte_ == b'.':\n                if len(label) == 0:\n                    raise EmptyLabel\n                labels.append(label)\n                label = b''\n            elif byte_ == b'\\\\':\n                escaping = True\n                edigits = 0\n                total = 0\n            else:\n                label += byte_\n        if escaping:\n            raise BadEscape\n        if len(label) > 0:\n            labels.append(label)\n        else:\n            labels.append(b'')\n    if (len(labels) == 0 or labels[-1] != b'') and origin is not None:\n        labels.extend(list(origin.labels))\n    return Name(labels)\n\n\ndef from_wire(message, current):\n    \"\"\"Convert possibly compressed wire format into a Name.\n\n    *message* is a ``binary`` containing an entire DNS message in DNS\n    wire form.\n\n    *current*, an ``int``, is the offset of the beginning of the name\n    from the start of the message\n\n    Raises ``dns.name.BadPointer`` if a compression pointer did not\n    point backwards in the message.\n\n    Raises ``dns.name.BadLabelType`` if an invalid label type was encountered.\n\n    Returns a ``(dns.name.Name, int)`` tuple consisting of the name\n    that was read and the number of bytes of the wire format message\n    which were consumed reading it.\n    \"\"\"\n\n    if not isinstance(message, binary_type):\n        raise ValueError(\"input to from_wire() must be a byte string\")\n    message = dns.wiredata.maybe_wrap(message)\n    labels = []\n    biggest_pointer = current\n    hops = 0\n    count = message[current]\n    current += 1\n    cused = 1\n    while count != 0:\n        if count < 64:\n            labels.append(message[current: current + count].unwrap())\n            current += count\n            if hops == 0:\n                cused += count\n        elif count >= 192:\n            current = (count & 0x3f) * 256 + message[current]\n            if hops == 0:\n                cused += 1\n            if current >= biggest_pointer:\n                raise BadPointer\n            biggest_pointer = current\n            hops += 1\n        else:\n            raise BadLabelType\n        count = message[current]\n        current += 1\n        if hops == 0:\n            cused += 1\n    labels.append('')\n    return (Name(labels), cused)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/namedict.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n# Copyright (C) 2016 Coresec Systems AB\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND CORESEC SYSTEMS AB DISCLAIMS ALL\n# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED\n# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL CORESEC\n# SYSTEMS AB BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR\n# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS\n# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,\n# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION\n# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS name dictionary\"\"\"\n\nimport collections\nimport dns.name\nfrom ._compat import xrange\n\n\nclass NameDict(collections.MutableMapping):\n    \"\"\"A dictionary whose keys are dns.name.Name objects.\n\n    In addition to being like a regular Python dictionary, this\n    dictionary can also get the deepest match for a given key.\n    \"\"\"\n\n    __slots__ = [\"max_depth\", \"max_depth_items\", \"__store\"]\n\n    def __init__(self, *args, **kwargs):\n        super(NameDict, self).__init__()\n        self.__store = dict()\n        #: the maximum depth of the keys that have ever been added\n        self.max_depth = 0\n        #: the number of items of maximum depth\n        self.max_depth_items = 0\n        self.update(dict(*args, **kwargs))\n\n    def __update_max_depth(self, key):\n        if len(key) == self.max_depth:\n            self.max_depth_items = self.max_depth_items + 1\n        elif len(key) > self.max_depth:\n            self.max_depth = len(key)\n            self.max_depth_items = 1\n\n    def __getitem__(self, key):\n        return self.__store[key]\n\n    def __setitem__(self, key, value):\n        if not isinstance(key, dns.name.Name):\n            raise ValueError('NameDict key must be a name')\n        self.__store[key] = value\n        self.__update_max_depth(key)\n\n    def __delitem__(self, key):\n        value = self.__store.pop(key)\n        if len(value) == self.max_depth:\n            self.max_depth_items = self.max_depth_items - 1\n        if self.max_depth_items == 0:\n            self.max_depth = 0\n            for k in self.__store:\n                self.__update_max_depth(k)\n\n    def __iter__(self):\n        return iter(self.__store)\n\n    def __len__(self):\n        return len(self.__store)\n\n    def has_key(self, key):\n        return key in self.__store\n\n    def get_deepest_match(self, name):\n        \"\"\"Find the deepest match to *fname* in the dictionary.\n\n        The deepest match is the longest name in the dictionary which is\n        a superdomain of *name*.  Note that *superdomain* includes matching\n        *name* itself.\n\n        *name*, a ``dns.name.Name``, the name to find.\n\n        Returns a ``(key, value)`` where *key* is the deepest\n        ``dns.name.Name``, and *value* is the value associated with *key*.\n        \"\"\"\n\n        depth = len(name)\n        if depth > self.max_depth:\n            depth = self.max_depth\n        for i in xrange(-depth, 0):\n            n = dns.name.Name(name[i:])\n            if n in self:\n                return (n, self[n])\n        v = self[dns.name.empty]\n        return (dns.name.empty, v)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/node.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2001-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS nodes.  A node is a set of rdatasets.\"\"\"\n\nfrom io import StringIO\n\nimport dns.rdataset\nimport dns.rdatatype\nimport dns.renderer\n\n\nclass Node(object):\n\n    \"\"\"A Node is a set of rdatasets.\"\"\"\n\n    __slots__ = ['rdatasets']\n\n    def __init__(self):\n        #: the set of rdatsets, represented as a list.\n        self.rdatasets = []\n\n    def to_text(self, name, **kw):\n        \"\"\"Convert a node to text format.\n\n        Each rdataset at the node is printed.  Any keyword arguments\n        to this method are passed on to the rdataset's to_text() method.\n\n        *name*, a ``dns.name.Name`` or ``text``, the owner name of the rdatasets.\n\n        Returns a ``text``.\n        \"\"\"\n\n        s = StringIO()\n        for rds in self.rdatasets:\n            if len(rds) > 0:\n                s.write(rds.to_text(name, **kw))\n                s.write(u'\\n')\n        return s.getvalue()[:-1]\n\n    def __repr__(self):\n        return '<DNS node ' + str(id(self)) + '>'\n\n    def __eq__(self, other):\n        #\n        # This is inefficient.  Good thing we don't need to do it much.\n        #\n        for rd in self.rdatasets:\n            if rd not in other.rdatasets:\n                return False\n        for rd in other.rdatasets:\n            if rd not in self.rdatasets:\n                return False\n        return True\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    def __len__(self):\n        return len(self.rdatasets)\n\n    def __iter__(self):\n        return iter(self.rdatasets)\n\n    def find_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE,\n                      create=False):\n        \"\"\"Find an rdataset matching the specified properties in the\n        current node.\n\n        *rdclass*, an ``int``, the class of the rdataset.\n\n        *rdtype*, an ``int``, the type of the rdataset.\n\n        *covers*, an ``int``, the covered type.  Usually this value is\n        dns.rdatatype.NONE, but if the rdtype is dns.rdatatype.SIG or\n        dns.rdatatype.RRSIG, then the covers value will be the rdata\n        type the SIG/RRSIG covers.  The library treats the SIG and RRSIG\n        types as if they were a family of\n        types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).  This makes RRSIGs much\n        easier to work with than if RRSIGs covering different rdata\n        types were aggregated into a single RRSIG rdataset.\n\n        *create*, a ``bool``.  If True, create the rdataset if it is not found.\n\n        Raises ``KeyError`` if an rdataset of the desired type and class does\n        not exist and *create* is not ``True``.\n\n        Returns a ``dns.rdataset.Rdataset``.\n        \"\"\"\n\n        for rds in self.rdatasets:\n            if rds.match(rdclass, rdtype, covers):\n                return rds\n        if not create:\n            raise KeyError\n        rds = dns.rdataset.Rdataset(rdclass, rdtype)\n        self.rdatasets.append(rds)\n        return rds\n\n    def get_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE,\n                     create=False):\n        \"\"\"Get an rdataset matching the specified properties in the\n        current node.\n\n        None is returned if an rdataset of the specified type and\n        class does not exist and *create* is not ``True``.\n\n        *rdclass*, an ``int``, the class of the rdataset.\n\n        *rdtype*, an ``int``, the type of the rdataset.\n\n        *covers*, an ``int``, the covered type.  Usually this value is\n        dns.rdatatype.NONE, but if the rdtype is dns.rdatatype.SIG or\n        dns.rdatatype.RRSIG, then the covers value will be the rdata\n        type the SIG/RRSIG covers.  The library treats the SIG and RRSIG\n        types as if they were a family of\n        types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).  This makes RRSIGs much\n        easier to work with than if RRSIGs covering different rdata\n        types were aggregated into a single RRSIG rdataset.\n\n        *create*, a ``bool``.  If True, create the rdataset if it is not found.\n\n        Returns a ``dns.rdataset.Rdataset`` or ``None``.\n        \"\"\"\n\n        try:\n            rds = self.find_rdataset(rdclass, rdtype, covers, create)\n        except KeyError:\n            rds = None\n        return rds\n\n    def delete_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE):\n        \"\"\"Delete the rdataset matching the specified properties in the\n        current node.\n\n        If a matching rdataset does not exist, it is not an error.\n\n        *rdclass*, an ``int``, the class of the rdataset.\n\n        *rdtype*, an ``int``, the type of the rdataset.\n\n        *covers*, an ``int``, the covered type.\n        \"\"\"\n\n        rds = self.get_rdataset(rdclass, rdtype, covers)\n        if rds is not None:\n            self.rdatasets.remove(rds)\n\n    def replace_rdataset(self, replacement):\n        \"\"\"Replace an rdataset.\n\n        It is not an error if there is no rdataset matching *replacement*.\n\n        Ownership of the *replacement* object is transferred to the node;\n        in other words, this method does not store a copy of *replacement*\n        at the node, it stores *replacement* itself.\n\n        *replacement*, a ``dns.rdataset.Rdataset``.\n\n        Raises ``ValueError`` if *replacement* is not a\n        ``dns.rdataset.Rdataset``.\n        \"\"\"\n\n        if not isinstance(replacement, dns.rdataset.Rdataset):\n            raise ValueError('replacement is not an rdataset')\n        self.delete_rdataset(replacement.rdclass, replacement.rdtype,\n                             replacement.covers)\n        self.rdatasets.append(replacement)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/opcode.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2001-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS Opcodes.\"\"\"\n\nimport dns.exception\n\n#: Query\nQUERY = 0\n#: Inverse Query (historical)\nIQUERY = 1\n#: Server Status (unspecified and unimplemented anywhere)\nSTATUS = 2\n#: Notify\nNOTIFY = 4\n#: Dynamic Update\nUPDATE = 5\n\n_by_text = {\n    'QUERY': QUERY,\n    'IQUERY': IQUERY,\n    'STATUS': STATUS,\n    'NOTIFY': NOTIFY,\n    'UPDATE': UPDATE\n}\n\n# We construct the inverse mapping programmatically to ensure that we\n# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that\n# would cause the mapping not to be true inverse.\n\n_by_value = {y: x for x, y in _by_text.items()}\n\n\nclass UnknownOpcode(dns.exception.DNSException):\n    \"\"\"An DNS opcode is unknown.\"\"\"\n\n\ndef from_text(text):\n    \"\"\"Convert text into an opcode.\n\n    *text*, a ``text``, the textual opcode\n\n    Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown.\n\n    Returns an ``int``.\n    \"\"\"\n\n    if text.isdigit():\n        value = int(text)\n        if value >= 0 and value <= 15:\n            return value\n    value = _by_text.get(text.upper())\n    if value is None:\n        raise UnknownOpcode\n    return value\n\n\ndef from_flags(flags):\n    \"\"\"Extract an opcode from DNS message flags.\n\n    *flags*, an ``int``, the DNS flags.\n\n    Returns an ``int``.\n    \"\"\"\n\n    return (flags & 0x7800) >> 11\n\n\ndef to_flags(value):\n    \"\"\"Convert an opcode to a value suitable for ORing into DNS message\n    flags.\n\n    *value*, an ``int``, the DNS opcode value.\n\n    Returns an ``int``.\n    \"\"\"\n\n    return (value << 11) & 0x7800\n\n\ndef to_text(value):\n    \"\"\"Convert an opcode to text.\n\n    *value*, an ``int`` the opcode value,\n\n    Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown.\n\n    Returns a ``text``.\n    \"\"\"\n\n    text = _by_value.get(value)\n    if text is None:\n        text = str(value)\n    return text\n\n\ndef is_update(flags):\n    \"\"\"Is the opcode in flags UPDATE?\n\n    *flags*, an ``int``, the DNS message flags.\n\n    Returns a ``bool``.\n    \"\"\"\n\n    return from_flags(flags) == UPDATE\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/py.typed",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/dns/query.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"Talk to a DNS server.\"\"\"\n\nfrom __future__ import generators\n\nimport errno\nimport select\nimport socket\nimport struct\nimport sys\nimport time\n\nimport dns.exception\nimport dns.inet\nimport dns.name\nimport dns.message\nimport dns.rcode\nimport dns.rdataclass\nimport dns.rdatatype\nfrom ._compat import long, string_types, PY3\n\nif PY3:\n    select_error = OSError\nelse:\n    select_error = select.error\n\n# Function used to create a socket.  Can be overridden if needed in special\n# situations.\nsocket_factory = socket.socket\n\nclass UnexpectedSource(dns.exception.DNSException):\n    \"\"\"A DNS query response came from an unexpected address or port.\"\"\"\n\n\nclass BadResponse(dns.exception.FormError):\n    \"\"\"A DNS query response does not respond to the question asked.\"\"\"\n\n\nclass TransferError(dns.exception.DNSException):\n    \"\"\"A zone transfer response got a non-zero rcode.\"\"\"\n\n    def __init__(self, rcode):\n        message = 'Zone transfer error: %s' % dns.rcode.to_text(rcode)\n        super(TransferError, self).__init__(message)\n        self.rcode = rcode\n\n\ndef _compute_expiration(timeout):\n    if timeout is None:\n        return None\n    else:\n        return time.time() + timeout\n\n# This module can use either poll() or select() as the \"polling backend\".\n#\n# A backend function takes an fd, bools for readability, writablity, and\n# error detection, and a timeout.\n\ndef _poll_for(fd, readable, writable, error, timeout):\n    \"\"\"Poll polling backend.\"\"\"\n\n    event_mask = 0\n    if readable:\n        event_mask |= select.POLLIN\n    if writable:\n        event_mask |= select.POLLOUT\n    if error:\n        event_mask |= select.POLLERR\n\n    pollable = select.poll()\n    pollable.register(fd, event_mask)\n\n    if timeout:\n        event_list = pollable.poll(long(timeout * 1000))\n    else:\n        event_list = pollable.poll()\n\n    return bool(event_list)\n\n\ndef _select_for(fd, readable, writable, error, timeout):\n    \"\"\"Select polling backend.\"\"\"\n\n    rset, wset, xset = [], [], []\n\n    if readable:\n        rset = [fd]\n    if writable:\n        wset = [fd]\n    if error:\n        xset = [fd]\n\n    if timeout is None:\n        (rcount, wcount, xcount) = select.select(rset, wset, xset)\n    else:\n        (rcount, wcount, xcount) = select.select(rset, wset, xset, timeout)\n\n    return bool((rcount or wcount or xcount))\n\n\ndef _wait_for(fd, readable, writable, error, expiration):\n    # Use the selected polling backend to wait for any of the specified\n    # events.  An \"expiration\" absolute time is converted into a relative\n    # timeout.\n\n    done = False\n    while not done:\n        if expiration is None:\n            timeout = None\n        else:\n            timeout = expiration - time.time()\n            if timeout <= 0.0:\n                raise dns.exception.Timeout\n        try:\n            if not _polling_backend(fd, readable, writable, error, timeout):\n                raise dns.exception.Timeout\n        except select_error as e:\n            if e.args[0] != errno.EINTR:\n                raise e\n        done = True\n\n\ndef _set_polling_backend(fn):\n    # Internal API. Do not use.\n\n    global _polling_backend\n\n    _polling_backend = fn\n\nif hasattr(select, 'poll'):\n    # Prefer poll() on platforms that support it because it has no\n    # limits on the maximum value of a file descriptor (plus it will\n    # be more efficient for high values).\n    _polling_backend = _poll_for\nelse:\n    _polling_backend = _select_for\n\n\ndef _wait_for_readable(s, expiration):\n    _wait_for(s, True, False, True, expiration)\n\n\ndef _wait_for_writable(s, expiration):\n    _wait_for(s, False, True, True, expiration)\n\n\ndef _addresses_equal(af, a1, a2):\n    # Convert the first value of the tuple, which is a textual format\n    # address into binary form, so that we are not confused by different\n    # textual representations of the same address\n    try:\n        n1 = dns.inet.inet_pton(af, a1[0])\n        n2 = dns.inet.inet_pton(af, a2[0])\n    except dns.exception.SyntaxError:\n        return False\n    return n1 == n2 and a1[1:] == a2[1:]\n\n\ndef _destination_and_source(af, where, port, source, source_port):\n    # Apply defaults and compute destination and source tuples\n    # suitable for use in connect(), sendto(), or bind().\n    if af is None:\n        try:\n            af = dns.inet.af_for_address(where)\n        except Exception:\n            af = dns.inet.AF_INET\n    if af == dns.inet.AF_INET:\n        destination = (where, port)\n        if source is not None or source_port != 0:\n            if source is None:\n                source = '0.0.0.0'\n            source = (source, source_port)\n    elif af == dns.inet.AF_INET6:\n        destination = (where, port, 0, 0)\n        if source is not None or source_port != 0:\n            if source is None:\n                source = '::'\n            source = (source, source_port, 0, 0)\n    return (af, destination, source)\n\n\ndef send_udp(sock, what, destination, expiration=None):\n    \"\"\"Send a DNS message to the specified UDP socket.\n\n    *sock*, a ``socket``.\n\n    *what*, a ``binary`` or ``dns.message.Message``, the message to send.\n\n    *destination*, a destination tuple appropriate for the address family\n    of the socket, specifying where to send the query.\n\n    *expiration*, a ``float`` or ``None``, the absolute time at which\n    a timeout exception should be raised.  If ``None``, no timeout will\n    occur.\n\n    Returns an ``(int, float)`` tuple of bytes sent and the sent time.\n    \"\"\"\n\n    if isinstance(what, dns.message.Message):\n        what = what.to_wire()\n    _wait_for_writable(sock, expiration)\n    sent_time = time.time()\n    n = sock.sendto(what, destination)\n    return (n, sent_time)\n\n\ndef receive_udp(sock, destination, expiration=None,\n                ignore_unexpected=False, one_rr_per_rrset=False,\n                keyring=None, request_mac=b'', ignore_trailing=False):\n    \"\"\"Read a DNS message from a UDP socket.\n\n    *sock*, a ``socket``.\n\n    *destination*, a destination tuple appropriate for the address family\n    of the socket, specifying where the associated query was sent.\n\n    *expiration*, a ``float`` or ``None``, the absolute time at which\n    a timeout exception should be raised.  If ``None``, no timeout will\n    occur.\n\n    *ignore_unexpected*, a ``bool``.  If ``True``, ignore responses from\n    unexpected sources.\n\n    *one_rr_per_rrset*, a ``bool``.  If ``True``, put each RR into its own\n    RRset.\n\n    *keyring*, a ``dict``, the keyring to use for TSIG.\n\n    *request_mac*, a ``binary``, the MAC of the request (for TSIG).\n\n    *ignore_trailing*, a ``bool``.  If ``True``, ignore trailing\n    junk at end of the received message.\n\n    Raises if the message is malformed, if network errors occur, of if\n    there is a timeout.\n\n    Returns a ``dns.message.Message`` object.\n    \"\"\"\n\n    wire = b''\n    while 1:\n        _wait_for_readable(sock, expiration)\n        (wire, from_address) = sock.recvfrom(65535)\n        if _addresses_equal(sock.family, from_address, destination) or \\\n           (dns.inet.is_multicast(destination[0]) and\n            from_address[1:] == destination[1:]):\n            break\n        if not ignore_unexpected:\n            raise UnexpectedSource('got a response from '\n                                   '%s instead of %s' % (from_address,\n                                                         destination))\n    received_time = time.time()\n    r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac,\n                              one_rr_per_rrset=one_rr_per_rrset,\n                              ignore_trailing=ignore_trailing)\n    return (r, received_time)\n\ndef udp(q, where, timeout=None, port=53, af=None, source=None, source_port=0,\n        ignore_unexpected=False, one_rr_per_rrset=False, ignore_trailing=False):\n    \"\"\"Return the response obtained after sending a query via UDP.\n\n    *q*, a ``dns.message.Message``, the query to send\n\n    *where*, a ``text`` containing an IPv4 or IPv6 address,  where\n    to send the message.\n\n    *timeout*, a ``float`` or ``None``, the number of seconds to wait before the\n    query times out.  If ``None``, the default, wait forever.\n\n    *port*, an ``int``, the port send the message to.  The default is 53.\n\n    *af*, an ``int``, the address family to use.  The default is ``None``,\n    which causes the address family to use to be inferred from the form of\n    *where*.  If the inference attempt fails, AF_INET is used.  This\n    parameter is historical; you need never set it.\n\n    *source*, a ``text`` containing an IPv4 or IPv6 address, specifying\n    the source address.  The default is the wildcard address.\n\n    *source_port*, an ``int``, the port from which to send the message.\n    The default is 0.\n\n    *ignore_unexpected*, a ``bool``.  If ``True``, ignore responses from\n    unexpected sources.\n\n    *one_rr_per_rrset*, a ``bool``.  If ``True``, put each RR into its own\n    RRset.\n\n    *ignore_trailing*, a ``bool``.  If ``True``, ignore trailing\n    junk at end of the received message.\n\n    Returns a ``dns.message.Message``.\n    \"\"\"\n\n    wire = q.to_wire()\n    (af, destination, source) = _destination_and_source(af, where, port,\n                                                        source, source_port)\n    s = socket_factory(af, socket.SOCK_DGRAM, 0)\n    received_time = None\n    sent_time = None\n    try:\n        expiration = _compute_expiration(timeout)\n        s.setblocking(0)\n        if source is not None:\n            s.bind(source)\n        (_, sent_time) = send_udp(s, wire, destination, expiration)\n        (r, received_time) = receive_udp(s, destination, expiration,\n                                         ignore_unexpected, one_rr_per_rrset,\n                                         q.keyring, q.mac, ignore_trailing)\n    finally:\n        if sent_time is None or received_time is None:\n            response_time = 0\n        else:\n            response_time = received_time - sent_time\n        s.close()\n    r.time = response_time\n    if not q.is_response(r):\n        raise BadResponse\n    return r\n\n\ndef _net_read(sock, count, expiration):\n    \"\"\"Read the specified number of bytes from sock.  Keep trying until we\n    either get the desired amount, or we hit EOF.\n    A Timeout exception will be raised if the operation is not completed\n    by the expiration time.\n    \"\"\"\n    s = b''\n    while count > 0:\n        _wait_for_readable(sock, expiration)\n        n = sock.recv(count)\n        if n == b'':\n            raise EOFError\n        count = count - len(n)\n        s = s + n\n    return s\n\n\ndef _net_write(sock, data, expiration):\n    \"\"\"Write the specified data to the socket.\n    A Timeout exception will be raised if the operation is not completed\n    by the expiration time.\n    \"\"\"\n    current = 0\n    l = len(data)\n    while current < l:\n        _wait_for_writable(sock, expiration)\n        current += sock.send(data[current:])\n\n\ndef send_tcp(sock, what, expiration=None):\n    \"\"\"Send a DNS message to the specified TCP socket.\n\n    *sock*, a ``socket``.\n\n    *what*, a ``binary`` or ``dns.message.Message``, the message to send.\n\n    *expiration*, a ``float`` or ``None``, the absolute time at which\n    a timeout exception should be raised.  If ``None``, no timeout will\n    occur.\n\n    Returns an ``(int, float)`` tuple of bytes sent and the sent time.\n    \"\"\"\n\n    if isinstance(what, dns.message.Message):\n        what = what.to_wire()\n    l = len(what)\n    # copying the wire into tcpmsg is inefficient, but lets us\n    # avoid writev() or doing a short write that would get pushed\n    # onto the net\n    tcpmsg = struct.pack(\"!H\", l) + what\n    _wait_for_writable(sock, expiration)\n    sent_time = time.time()\n    _net_write(sock, tcpmsg, expiration)\n    return (len(tcpmsg), sent_time)\n\ndef receive_tcp(sock, expiration=None, one_rr_per_rrset=False,\n                keyring=None, request_mac=b'', ignore_trailing=False):\n    \"\"\"Read a DNS message from a TCP socket.\n\n    *sock*, a ``socket``.\n\n    *expiration*, a ``float`` or ``None``, the absolute time at which\n    a timeout exception should be raised.  If ``None``, no timeout will\n    occur.\n\n    *one_rr_per_rrset*, a ``bool``.  If ``True``, put each RR into its own\n    RRset.\n\n    *keyring*, a ``dict``, the keyring to use for TSIG.\n\n    *request_mac*, a ``binary``, the MAC of the request (for TSIG).\n\n    *ignore_trailing*, a ``bool``.  If ``True``, ignore trailing\n    junk at end of the received message.\n\n    Raises if the message is malformed, if network errors occur, of if\n    there is a timeout.\n\n    Returns a ``dns.message.Message`` object.\n    \"\"\"\n\n    ldata = _net_read(sock, 2, expiration)\n    (l,) = struct.unpack(\"!H\", ldata)\n    wire = _net_read(sock, l, expiration)\n    received_time = time.time()\n    r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac,\n                              one_rr_per_rrset=one_rr_per_rrset,\n                              ignore_trailing=ignore_trailing)\n    return (r, received_time)\n\ndef _connect(s, address):\n    try:\n        s.connect(address)\n    except socket.error:\n        (ty, v) = sys.exc_info()[:2]\n\n        if hasattr(v, 'errno'):\n            v_err = v.errno\n        else:\n            v_err = v[0]\n        if v_err not in [errno.EINPROGRESS, errno.EWOULDBLOCK, errno.EALREADY]:\n            raise v\n\n\ndef tcp(q, where, timeout=None, port=53, af=None, source=None, source_port=0,\n        one_rr_per_rrset=False, ignore_trailing=False):\n    \"\"\"Return the response obtained after sending a query via TCP.\n\n    *q*, a ``dns.message.Message``, the query to send\n\n    *where*, a ``text`` containing an IPv4 or IPv6 address,  where\n    to send the message.\n\n    *timeout*, a ``float`` or ``None``, the number of seconds to wait before the\n    query times out.  If ``None``, the default, wait forever.\n\n    *port*, an ``int``, the port send the message to.  The default is 53.\n\n    *af*, an ``int``, the address family to use.  The default is ``None``,\n    which causes the address family to use to be inferred from the form of\n    *where*.  If the inference attempt fails, AF_INET is used.  This\n    parameter is historical; you need never set it.\n\n    *source*, a ``text`` containing an IPv4 or IPv6 address, specifying\n    the source address.  The default is the wildcard address.\n\n    *source_port*, an ``int``, the port from which to send the message.\n    The default is 0.\n\n    *one_rr_per_rrset*, a ``bool``.  If ``True``, put each RR into its own\n    RRset.\n\n    *ignore_trailing*, a ``bool``.  If ``True``, ignore trailing\n    junk at end of the received message.\n\n    Returns a ``dns.message.Message``.\n    \"\"\"\n\n    wire = q.to_wire()\n    (af, destination, source) = _destination_and_source(af, where, port,\n                                                        source, source_port)\n    s = socket_factory(af, socket.SOCK_STREAM, 0)\n    begin_time = None\n    received_time = None\n    try:\n        expiration = _compute_expiration(timeout)\n        s.setblocking(0)\n        begin_time = time.time()\n        if source is not None:\n            s.bind(source)\n        _connect(s, destination)\n        send_tcp(s, wire, expiration)\n        (r, received_time) = receive_tcp(s, expiration, one_rr_per_rrset,\n                                         q.keyring, q.mac, ignore_trailing)\n    finally:\n        if begin_time is None or received_time is None:\n            response_time = 0\n        else:\n            response_time = received_time - begin_time\n        s.close()\n    r.time = response_time\n    if not q.is_response(r):\n        raise BadResponse\n    return r\n\n\ndef xfr(where, zone, rdtype=dns.rdatatype.AXFR, rdclass=dns.rdataclass.IN,\n        timeout=None, port=53, keyring=None, keyname=None, relativize=True,\n        af=None, lifetime=None, source=None, source_port=0, serial=0,\n        use_udp=False, keyalgorithm=dns.tsig.default_algorithm):\n    \"\"\"Return a generator for the responses to a zone transfer.\n\n    *where*.  If the inference attempt fails, AF_INET is used.  This\n    parameter is historical; you need never set it.\n\n    *zone*, a ``dns.name.Name`` or ``text``, the name of the zone to transfer.\n\n    *rdtype*, an ``int`` or ``text``, the type of zone transfer.  The\n    default is ``dns.rdatatype.AXFR``.  ``dns.rdatatype.IXFR`` can be\n    used to do an incremental transfer instead.\n\n    *rdclass*, an ``int`` or ``text``, the class of the zone transfer.\n    The default is ``dns.rdataclass.IN``.\n\n    *timeout*, a ``float``, the number of seconds to wait for each\n    response message.  If None, the default, wait forever.\n\n    *port*, an ``int``, the port send the message to.  The default is 53.\n\n    *keyring*, a ``dict``, the keyring to use for TSIG.\n\n    *keyname*, a ``dns.name.Name`` or ``text``, the name of the TSIG\n    key to use.\n\n    *relativize*, a ``bool``.  If ``True``, all names in the zone will be\n    relativized to the zone origin.  It is essential that the\n    relativize setting matches the one specified to\n    ``dns.zone.from_xfr()`` if using this generator to make a zone.\n\n    *af*, an ``int``, the address family to use.  The default is ``None``,\n    which causes the address family to use to be inferred from the form of\n    *where*.  If the inference attempt fails, AF_INET is used.  This\n    parameter is historical; you need never set it.\n\n    *lifetime*, a ``float``, the total number of seconds to spend\n    doing the transfer.  If ``None``, the default, then there is no\n    limit on the time the transfer may take.\n\n    *source*, a ``text`` containing an IPv4 or IPv6 address, specifying\n    the source address.  The default is the wildcard address.\n\n    *source_port*, an ``int``, the port from which to send the message.\n    The default is 0.\n\n    *serial*, an ``int``, the SOA serial number to use as the base for\n    an IXFR diff sequence (only meaningful if *rdtype* is\n    ``dns.rdatatype.IXFR``).\n\n    *use_udp*, a ``bool``.  If ``True``, use UDP (only meaningful for IXFR).\n\n    *keyalgorithm*, a ``dns.name.Name`` or ``text``, the TSIG algorithm to use.\n\n    Raises on errors, and so does the generator.\n\n    Returns a generator of ``dns.message.Message`` objects.\n    \"\"\"\n\n    if isinstance(zone, string_types):\n        zone = dns.name.from_text(zone)\n    if isinstance(rdtype, string_types):\n        rdtype = dns.rdatatype.from_text(rdtype)\n    q = dns.message.make_query(zone, rdtype, rdclass)\n    if rdtype == dns.rdatatype.IXFR:\n        rrset = dns.rrset.from_text(zone, 0, 'IN', 'SOA',\n                                    '. . %u 0 0 0 0' % serial)\n        q.authority.append(rrset)\n    if keyring is not None:\n        q.use_tsig(keyring, keyname, algorithm=keyalgorithm)\n    wire = q.to_wire()\n    (af, destination, source) = _destination_and_source(af, where, port,\n                                                        source, source_port)\n    if use_udp:\n        if rdtype != dns.rdatatype.IXFR:\n            raise ValueError('cannot do a UDP AXFR')\n        s = socket_factory(af, socket.SOCK_DGRAM, 0)\n    else:\n        s = socket_factory(af, socket.SOCK_STREAM, 0)\n    s.setblocking(0)\n    if source is not None:\n        s.bind(source)\n    expiration = _compute_expiration(lifetime)\n    _connect(s, destination)\n    l = len(wire)\n    if use_udp:\n        _wait_for_writable(s, expiration)\n        s.send(wire)\n    else:\n        tcpmsg = struct.pack(\"!H\", l) + wire\n        _net_write(s, tcpmsg, expiration)\n    done = False\n    delete_mode = True\n    expecting_SOA = False\n    soa_rrset = None\n    if relativize:\n        origin = zone\n        oname = dns.name.empty\n    else:\n        origin = None\n        oname = zone\n    tsig_ctx = None\n    first = True\n    while not done:\n        mexpiration = _compute_expiration(timeout)\n        if mexpiration is None or mexpiration > expiration:\n            mexpiration = expiration\n        if use_udp:\n            _wait_for_readable(s, expiration)\n            (wire, from_address) = s.recvfrom(65535)\n        else:\n            ldata = _net_read(s, 2, mexpiration)\n            (l,) = struct.unpack(\"!H\", ldata)\n            wire = _net_read(s, l, mexpiration)\n        is_ixfr = (rdtype == dns.rdatatype.IXFR)\n        r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac,\n                                  xfr=True, origin=origin, tsig_ctx=tsig_ctx,\n                                  multi=True, first=first,\n                                  one_rr_per_rrset=is_ixfr)\n        rcode = r.rcode()\n        if rcode != dns.rcode.NOERROR:\n            raise TransferError(rcode)\n        tsig_ctx = r.tsig_ctx\n        first = False\n        answer_index = 0\n        if soa_rrset is None:\n            if not r.answer or r.answer[0].name != oname:\n                raise dns.exception.FormError(\n                    \"No answer or RRset not for qname\")\n            rrset = r.answer[0]\n            if rrset.rdtype != dns.rdatatype.SOA:\n                raise dns.exception.FormError(\"first RRset is not an SOA\")\n            answer_index = 1\n            soa_rrset = rrset.copy()\n            if rdtype == dns.rdatatype.IXFR:\n                if soa_rrset[0].serial <= serial:\n                    #\n                    # We're already up-to-date.\n                    #\n                    done = True\n                else:\n                    expecting_SOA = True\n        #\n        # Process SOAs in the answer section (other than the initial\n        # SOA in the first message).\n        #\n        for rrset in r.answer[answer_index:]:\n            if done:\n                raise dns.exception.FormError(\"answers after final SOA\")\n            if rrset.rdtype == dns.rdatatype.SOA and rrset.name == oname:\n                if expecting_SOA:\n                    if rrset[0].serial != serial:\n                        raise dns.exception.FormError(\n                            \"IXFR base serial mismatch\")\n                    expecting_SOA = False\n                elif rdtype == dns.rdatatype.IXFR:\n                    delete_mode = not delete_mode\n                #\n                # If this SOA RRset is equal to the first we saw then we're\n                # finished. If this is an IXFR we also check that we're seeing\n                # the record in the expected part of the response.\n                #\n                if rrset == soa_rrset and \\\n                        (rdtype == dns.rdatatype.AXFR or\n                         (rdtype == dns.rdatatype.IXFR and delete_mode)):\n                    done = True\n            elif expecting_SOA:\n                #\n                # We made an IXFR request and are expecting another\n                # SOA RR, but saw something else, so this must be an\n                # AXFR response.\n                #\n                rdtype = dns.rdatatype.AXFR\n                expecting_SOA = False\n        if done and q.keyring and not r.had_tsig:\n            raise dns.exception.FormError(\"missing TSIG\")\n        yield r\n    s.close()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rcode.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2001-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS Result Codes.\"\"\"\n\nimport dns.exception\nfrom ._compat import long\n\n#: No error\nNOERROR = 0\n#: Form error\nFORMERR = 1\n#: Server failure\nSERVFAIL = 2\n#: Name does not exist (\"Name Error\" in RFC 1025 terminology).\nNXDOMAIN = 3\n#: Not implemented\nNOTIMP = 4\n#: Refused\nREFUSED = 5\n#: Name exists.\nYXDOMAIN = 6\n#: RRset exists.\nYXRRSET = 7\n#: RRset does not exist.\nNXRRSET = 8\n#: Not authoritative.\nNOTAUTH = 9\n#: Name not in zone.\nNOTZONE = 10\n#: Bad EDNS version.\nBADVERS = 16\n\n_by_text = {\n    'NOERROR': NOERROR,\n    'FORMERR': FORMERR,\n    'SERVFAIL': SERVFAIL,\n    'NXDOMAIN': NXDOMAIN,\n    'NOTIMP': NOTIMP,\n    'REFUSED': REFUSED,\n    'YXDOMAIN': YXDOMAIN,\n    'YXRRSET': YXRRSET,\n    'NXRRSET': NXRRSET,\n    'NOTAUTH': NOTAUTH,\n    'NOTZONE': NOTZONE,\n    'BADVERS': BADVERS\n}\n\n# We construct the inverse mapping programmatically to ensure that we\n# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that\n# would cause the mapping not to be a true inverse.\n\n_by_value = {y: x for x, y in _by_text.items()}\n\n\nclass UnknownRcode(dns.exception.DNSException):\n    \"\"\"A DNS rcode is unknown.\"\"\"\n\n\ndef from_text(text):\n    \"\"\"Convert text into an rcode.\n\n    *text*, a ``text``, the textual rcode or an integer in textual form.\n\n    Raises ``dns.rcode.UnknownRcode`` if the rcode mnemonic is unknown.\n\n    Returns an ``int``.\n    \"\"\"\n\n    if text.isdigit():\n        v = int(text)\n        if v >= 0 and v <= 4095:\n            return v\n    v = _by_text.get(text.upper())\n    if v is None:\n        raise UnknownRcode\n    return v\n\n\ndef from_flags(flags, ednsflags):\n    \"\"\"Return the rcode value encoded by flags and ednsflags.\n\n    *flags*, an ``int``, the DNS flags field.\n\n    *ednsflags*, an ``int``, the EDNS flags field.\n\n    Raises ``ValueError`` if rcode is < 0 or > 4095\n\n    Returns an ``int``.\n    \"\"\"\n\n    value = (flags & 0x000f) | ((ednsflags >> 20) & 0xff0)\n    if value < 0 or value > 4095:\n        raise ValueError('rcode must be >= 0 and <= 4095')\n    return value\n\n\ndef to_flags(value):\n    \"\"\"Return a (flags, ednsflags) tuple which encodes the rcode.\n\n    *value*, an ``int``, the rcode.\n\n    Raises ``ValueError`` if rcode is < 0 or > 4095.\n\n    Returns an ``(int, int)`` tuple.\n    \"\"\"\n\n    if value < 0 or value > 4095:\n        raise ValueError('rcode must be >= 0 and <= 4095')\n    v = value & 0xf\n    ev = long(value & 0xff0) << 20\n    return (v, ev)\n\n\ndef to_text(value):\n    \"\"\"Convert rcode into text.\n\n    *value*, and ``int``, the rcode.\n\n    Raises ``ValueError`` if rcode is < 0 or > 4095.\n\n    Returns a ``text``.\n    \"\"\"\n\n    if value < 0 or value > 4095:\n        raise ValueError('rcode must be >= 0 and <= 4095')\n    text = _by_value.get(value)\n    if text is None:\n        text = str(value)\n    return text\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdata.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2001-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS rdata.\"\"\"\n\nfrom io import BytesIO\nimport base64\nimport binascii\n\nimport dns.exception\nimport dns.name\nimport dns.rdataclass\nimport dns.rdatatype\nimport dns.tokenizer\nimport dns.wiredata\nfrom ._compat import xrange, string_types, text_type\n\ntry:\n    import threading as _threading\nexcept ImportError:\n    import dummy_threading as _threading\n\n_hex_chunksize = 32\n\n\ndef _hexify(data, chunksize=_hex_chunksize):\n    \"\"\"Convert a binary string into its hex encoding, broken up into chunks\n    of chunksize characters separated by a space.\n    \"\"\"\n\n    line = binascii.hexlify(data)\n    return b' '.join([line[i:i + chunksize]\n                      for i\n                      in range(0, len(line), chunksize)]).decode()\n\n_base64_chunksize = 32\n\n\ndef _base64ify(data, chunksize=_base64_chunksize):\n    \"\"\"Convert a binary string into its base64 encoding, broken up into chunks\n    of chunksize characters separated by a space.\n    \"\"\"\n\n    line = base64.b64encode(data)\n    return b' '.join([line[i:i + chunksize]\n                      for i\n                      in range(0, len(line), chunksize)]).decode()\n\n__escaped = bytearray(b'\"\\\\')\n\ndef _escapify(qstring):\n    \"\"\"Escape the characters in a quoted string which need it.\"\"\"\n\n    if isinstance(qstring, text_type):\n        qstring = qstring.encode()\n    if not isinstance(qstring, bytearray):\n        qstring = bytearray(qstring)\n\n    text = ''\n    for c in qstring:\n        if c in __escaped:\n            text += '\\\\' + chr(c)\n        elif c >= 0x20 and c < 0x7F:\n            text += chr(c)\n        else:\n            text += '\\\\%03d' % c\n    return text\n\n\ndef _truncate_bitmap(what):\n    \"\"\"Determine the index of greatest byte that isn't all zeros, and\n    return the bitmap that contains all the bytes less than that index.\n    \"\"\"\n\n    for i in xrange(len(what) - 1, -1, -1):\n        if what[i] != 0:\n            return what[0: i + 1]\n    return what[0:1]\n\n\nclass Rdata(object):\n    \"\"\"Base class for all DNS rdata types.\"\"\"\n\n    __slots__ = ['rdclass', 'rdtype']\n\n    def __init__(self, rdclass, rdtype):\n        \"\"\"Initialize an rdata.\n\n        *rdclass*, an ``int`` is the rdataclass of the Rdata.\n        *rdtype*, an ``int`` is the rdatatype of the Rdata.\n        \"\"\"\n\n        self.rdclass = rdclass\n        self.rdtype = rdtype\n\n    def covers(self):\n        \"\"\"Return the type a Rdata covers.\n\n        DNS SIG/RRSIG rdatas apply to a specific type; this type is\n        returned by the covers() function.  If the rdata type is not\n        SIG or RRSIG, dns.rdatatype.NONE is returned.  This is useful when\n        creating rdatasets, allowing the rdataset to contain only RRSIGs\n        of a particular type, e.g. RRSIG(NS).\n\n        Returns an ``int``.\n        \"\"\"\n\n        return dns.rdatatype.NONE\n\n    def extended_rdatatype(self):\n        \"\"\"Return a 32-bit type value, the least significant 16 bits of\n        which are the ordinary DNS type, and the upper 16 bits of which are\n        the \"covered\" type, if any.\n\n        Returns an ``int``.\n        \"\"\"\n\n        return self.covers() << 16 | self.rdtype\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        \"\"\"Convert an rdata to text format.\n\n        Returns a ``text``.\n        \"\"\"\n\n        raise NotImplementedError\n\n    def to_wire(self, file, compress=None, origin=None):\n        \"\"\"Convert an rdata to wire format.\n\n        Returns a ``binary``.\n        \"\"\"\n\n        raise NotImplementedError\n\n    def to_digestable(self, origin=None):\n        \"\"\"Convert rdata to a format suitable for digesting in hashes.  This\n        is also the DNSSEC canonical form.\n\n        Returns a ``binary``.\n        \"\"\"\n\n        f = BytesIO()\n        self.to_wire(f, None, origin)\n        return f.getvalue()\n\n    def validate(self):\n        \"\"\"Check that the current contents of the rdata's fields are\n        valid.\n\n        If you change an rdata by assigning to its fields,\n        it is a good idea to call validate() when you are done making\n        changes.\n\n        Raises various exceptions if there are problems.\n\n        Returns ``None``.\n        \"\"\"\n\n        dns.rdata.from_text(self.rdclass, self.rdtype, self.to_text())\n\n    def __repr__(self):\n        covers = self.covers()\n        if covers == dns.rdatatype.NONE:\n            ctext = ''\n        else:\n            ctext = '(' + dns.rdatatype.to_text(covers) + ')'\n        return '<DNS ' + dns.rdataclass.to_text(self.rdclass) + ' ' + \\\n               dns.rdatatype.to_text(self.rdtype) + ctext + ' rdata: ' + \\\n               str(self) + '>'\n\n    def __str__(self):\n        return self.to_text()\n\n    def _cmp(self, other):\n        \"\"\"Compare an rdata with another rdata of the same rdtype and\n        rdclass.\n\n        Return < 0 if self < other in the DNSSEC ordering, 0 if self\n        == other, and > 0 if self > other.\n\n        \"\"\"\n        our = self.to_digestable(dns.name.root)\n        their = other.to_digestable(dns.name.root)\n        if our == their:\n            return 0\n        elif our > their:\n            return 1\n        else:\n            return -1\n\n    def __eq__(self, other):\n        if not isinstance(other, Rdata):\n            return False\n        if self.rdclass != other.rdclass or self.rdtype != other.rdtype:\n            return False\n        return self._cmp(other) == 0\n\n    def __ne__(self, other):\n        if not isinstance(other, Rdata):\n            return True\n        if self.rdclass != other.rdclass or self.rdtype != other.rdtype:\n            return True\n        return self._cmp(other) != 0\n\n    def __lt__(self, other):\n        if not isinstance(other, Rdata) or \\\n                self.rdclass != other.rdclass or self.rdtype != other.rdtype:\n\n            return NotImplemented\n        return self._cmp(other) < 0\n\n    def __le__(self, other):\n        if not isinstance(other, Rdata) or \\\n                self.rdclass != other.rdclass or self.rdtype != other.rdtype:\n            return NotImplemented\n        return self._cmp(other) <= 0\n\n    def __ge__(self, other):\n        if not isinstance(other, Rdata) or \\\n                self.rdclass != other.rdclass or self.rdtype != other.rdtype:\n            return NotImplemented\n        return self._cmp(other) >= 0\n\n    def __gt__(self, other):\n        if not isinstance(other, Rdata) or \\\n                self.rdclass != other.rdclass or self.rdtype != other.rdtype:\n            return NotImplemented\n        return self._cmp(other) > 0\n\n    def __hash__(self):\n        return hash(self.to_digestable(dns.name.root))\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        raise NotImplementedError\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        raise NotImplementedError\n\n    def choose_relativity(self, origin=None, relativize=True):\n        \"\"\"Convert any domain names in the rdata to the specified\n        relativization.\n        \"\"\"\n\nclass GenericRdata(Rdata):\n\n    \"\"\"Generic Rdata Class\n\n    This class is used for rdata types for which we have no better\n    implementation.  It implements the DNS \"unknown RRs\" scheme.\n    \"\"\"\n\n    __slots__ = ['data']\n\n    def __init__(self, rdclass, rdtype, data):\n        super(GenericRdata, self).__init__(rdclass, rdtype)\n        self.data = data\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return r'\\# %d ' % len(self.data) + _hexify(self.data)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        token = tok.get()\n        if not token.is_identifier() or token.value != r'\\#':\n            raise dns.exception.SyntaxError(\n                r'generic rdata does not start with \\#')\n        length = tok.get_int()\n        chunks = []\n        while 1:\n            token = tok.get()\n            if token.is_eol_or_eof():\n                break\n            chunks.append(token.value.encode())\n        hex = b''.join(chunks)\n        data = binascii.unhexlify(hex)\n        if len(data) != length:\n            raise dns.exception.SyntaxError(\n                'generic rdata hex data has wrong length')\n        return cls(rdclass, rdtype, data)\n\n    def to_wire(self, file, compress=None, origin=None):\n        file.write(self.data)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        return cls(rdclass, rdtype, wire[current: current + rdlen])\n\n_rdata_modules = {}\n_module_prefix = 'dns.rdtypes'\n_import_lock = _threading.Lock()\n\ndef get_rdata_class(rdclass, rdtype):\n\n    def import_module(name):\n        with _import_lock:\n            mod = __import__(name)\n            components = name.split('.')\n            for comp in components[1:]:\n                mod = getattr(mod, comp)\n            return mod\n\n    mod = _rdata_modules.get((rdclass, rdtype))\n    rdclass_text = dns.rdataclass.to_text(rdclass)\n    rdtype_text = dns.rdatatype.to_text(rdtype)\n    rdtype_text = rdtype_text.replace('-', '_')\n    if not mod:\n        mod = _rdata_modules.get((dns.rdatatype.ANY, rdtype))\n        if not mod:\n            try:\n                mod = import_module('.'.join([_module_prefix,\n                                              rdclass_text, rdtype_text]))\n                _rdata_modules[(rdclass, rdtype)] = mod\n            except ImportError:\n                try:\n                    mod = import_module('.'.join([_module_prefix,\n                                                  'ANY', rdtype_text]))\n                    _rdata_modules[(dns.rdataclass.ANY, rdtype)] = mod\n                except ImportError:\n                    mod = None\n    if mod:\n        cls = getattr(mod, rdtype_text)\n    else:\n        cls = GenericRdata\n    return cls\n\n\ndef from_text(rdclass, rdtype, tok, origin=None, relativize=True):\n    \"\"\"Build an rdata object from text format.\n\n    This function attempts to dynamically load a class which\n    implements the specified rdata class and type.  If there is no\n    class-and-type-specific implementation, the GenericRdata class\n    is used.\n\n    Once a class is chosen, its from_text() class method is called\n    with the parameters to this function.\n\n    If *tok* is a ``text``, then a tokenizer is created and the string\n    is used as its input.\n\n    *rdclass*, an ``int``, the rdataclass.\n\n    *rdtype*, an ``int``, the rdatatype.\n\n    *tok*, a ``dns.tokenizer.Tokenizer`` or a ``text``.\n\n    *origin*, a ``dns.name.Name`` (or ``None``), the\n    origin to use for relative names.\n\n    *relativize*, a ``bool``.  If true, name will be relativized to\n    the specified origin.\n\n    Returns an instance of the chosen Rdata subclass.\n    \"\"\"\n\n    if isinstance(tok, string_types):\n        tok = dns.tokenizer.Tokenizer(tok)\n    cls = get_rdata_class(rdclass, rdtype)\n    if cls != GenericRdata:\n        # peek at first token\n        token = tok.get()\n        tok.unget(token)\n        if token.is_identifier() and \\\n           token.value == r'\\#':\n            #\n            # Known type using the generic syntax.  Extract the\n            # wire form from the generic syntax, and then run\n            # from_wire on it.\n            #\n            rdata = GenericRdata.from_text(rdclass, rdtype, tok, origin,\n                                           relativize)\n            return from_wire(rdclass, rdtype, rdata.data, 0, len(rdata.data),\n                             origin)\n    return cls.from_text(rdclass, rdtype, tok, origin, relativize)\n\n\ndef from_wire(rdclass, rdtype, wire, current, rdlen, origin=None):\n    \"\"\"Build an rdata object from wire format\n\n    This function attempts to dynamically load a class which\n    implements the specified rdata class and type.  If there is no\n    class-and-type-specific implementation, the GenericRdata class\n    is used.\n\n    Once a class is chosen, its from_wire() class method is called\n    with the parameters to this function.\n\n    *rdclass*, an ``int``, the rdataclass.\n\n    *rdtype*, an ``int``, the rdatatype.\n\n    *wire*, a ``binary``, the wire-format message.\n\n    *current*, an ``int``, the offset in wire of the beginning of\n    the rdata.\n\n    *rdlen*, an ``int``, the length of the wire-format rdata\n\n    *origin*, a ``dns.name.Name`` (or ``None``).  If not ``None``,\n    then names will be relativized to this origin.\n\n    Returns an instance of the chosen Rdata subclass.\n    \"\"\"\n\n    wire = dns.wiredata.maybe_wrap(wire)\n    cls = get_rdata_class(rdclass, rdtype)\n    return cls.from_wire(rdclass, rdtype, wire, current, rdlen, origin)\n\n\nclass RdatatypeExists(dns.exception.DNSException):\n    \"\"\"DNS rdatatype already exists.\"\"\"\n    supp_kwargs = {'rdclass', 'rdtype'}\n    fmt = \"The rdata type with class {rdclass} and rdtype {rdtype} \" + \\\n        \"already exists.\"\n\n\ndef register_type(implementation, rdtype, rdtype_text, is_singleton=False,\n                  rdclass=dns.rdataclass.IN):\n    \"\"\"Dynamically register a module to handle an rdatatype.\n\n    *implementation*, a module implementing the type in the usual dnspython\n    way.\n\n    *rdtype*, an ``int``, the rdatatype to register.\n\n    *rdtype_text*, a ``text``, the textual form of the rdatatype.\n\n    *is_singleton*, a ``bool``, indicating if the type is a singleton (i.e.\n    RRsets of the type can have only one member.)\n\n    *rdclass*, the rdataclass of the type, or ``dns.rdataclass.ANY`` if\n    it applies to all classes.\n    \"\"\"\n\n    existing_cls = get_rdata_class(rdclass, rdtype)\n    if existing_cls != GenericRdata:\n        raise RdatatypeExists(rdclass=rdclass, rdtype=rdtype)\n    _rdata_modules[(rdclass, rdtype)] = implementation\n    dns.rdatatype.register_type(rdtype, rdtype_text, is_singleton)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdataclass.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2001-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS Rdata Classes.\"\"\"\n\nimport re\n\nimport dns.exception\n\nRESERVED0 = 0\nIN = 1\nCH = 3\nHS = 4\nNONE = 254\nANY = 255\n\n_by_text = {\n    'RESERVED0': RESERVED0,\n    'IN': IN,\n    'CH': CH,\n    'HS': HS,\n    'NONE': NONE,\n    'ANY': ANY\n}\n\n# We construct the inverse mapping programmatically to ensure that we\n# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that\n# would cause the mapping not to be true inverse.\n\n_by_value = {y: x for x, y in _by_text.items()}\n\n# Now that we've built the inverse map, we can add class aliases to\n# the _by_text mapping.\n\n_by_text.update({\n    'INTERNET': IN,\n    'CHAOS': CH,\n    'HESIOD': HS\n})\n\n_metaclasses = {\n    NONE: True,\n    ANY: True\n}\n\n_unknown_class_pattern = re.compile('CLASS([0-9]+)$', re.I)\n\n\nclass UnknownRdataclass(dns.exception.DNSException):\n    \"\"\"A DNS class is unknown.\"\"\"\n\n\ndef from_text(text):\n    \"\"\"Convert text into a DNS rdata class value.\n\n    The input text can be a defined DNS RR class mnemonic or\n    instance of the DNS generic class syntax.\n\n    For example, \"IN\" and \"CLASS1\" will both result in a value of 1.\n\n    Raises ``dns.rdatatype.UnknownRdataclass`` if the class is unknown.\n\n    Raises ``ValueError`` if the rdata class value is not >= 0 and <= 65535.\n\n    Returns an ``int``.\n    \"\"\"\n\n    value = _by_text.get(text.upper())\n    if value is None:\n        match = _unknown_class_pattern.match(text)\n        if match is None:\n            raise UnknownRdataclass\n        value = int(match.group(1))\n        if value < 0 or value > 65535:\n            raise ValueError(\"class must be between >= 0 and <= 65535\")\n    return value\n\n\ndef to_text(value):\n    \"\"\"Convert a DNS rdata type value to text.\n\n    If the value has a known mnemonic, it will be used, otherwise the\n    DNS generic class syntax will be used.\n\n    Raises ``ValueError`` if the rdata class value is not >= 0 and <= 65535.\n\n    Returns a ``str``.\n    \"\"\"\n\n    if value < 0 or value > 65535:\n        raise ValueError(\"class must be between >= 0 and <= 65535\")\n    text = _by_value.get(value)\n    if text is None:\n        text = 'CLASS' + repr(value)\n    return text\n\n\ndef is_metaclass(rdclass):\n    \"\"\"True if the specified class is a metaclass.\n\n    The currently defined metaclasses are ANY and NONE.\n\n    *rdclass* is an ``int``.\n    \"\"\"\n\n    if rdclass in _metaclasses:\n        return True\n    return False\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdataset.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2001-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS rdatasets (an rdataset is a set of rdatas of a given type and class)\"\"\"\n\nimport random\nfrom io import StringIO\nimport struct\n\nimport dns.exception\nimport dns.rdatatype\nimport dns.rdataclass\nimport dns.rdata\nimport dns.set\nfrom ._compat import string_types\n\n# define SimpleSet here for backwards compatibility\nSimpleSet = dns.set.Set\n\n\nclass DifferingCovers(dns.exception.DNSException):\n    \"\"\"An attempt was made to add a DNS SIG/RRSIG whose covered type\n    is not the same as that of the other rdatas in the rdataset.\"\"\"\n\n\nclass IncompatibleTypes(dns.exception.DNSException):\n    \"\"\"An attempt was made to add DNS RR data of an incompatible type.\"\"\"\n\n\nclass Rdataset(dns.set.Set):\n\n    \"\"\"A DNS rdataset.\"\"\"\n\n    __slots__ = ['rdclass', 'rdtype', 'covers', 'ttl']\n\n    def __init__(self, rdclass, rdtype, covers=dns.rdatatype.NONE, ttl=0):\n        \"\"\"Create a new rdataset of the specified class and type.\n\n        *rdclass*, an ``int``, the rdataclass.\n\n        *rdtype*, an ``int``, the rdatatype.\n\n        *covers*, an ``int``, the covered rdatatype.\n\n        *ttl*, an ``int``, the TTL.\n        \"\"\"\n\n        super(Rdataset, self).__init__()\n        self.rdclass = rdclass\n        self.rdtype = rdtype\n        self.covers = covers\n        self.ttl = ttl\n\n    def _clone(self):\n        obj = super(Rdataset, self)._clone()\n        obj.rdclass = self.rdclass\n        obj.rdtype = self.rdtype\n        obj.covers = self.covers\n        obj.ttl = self.ttl\n        return obj\n\n    def update_ttl(self, ttl):\n        \"\"\"Perform TTL minimization.\n\n        Set the TTL of the rdataset to be the lesser of the set's current\n        TTL or the specified TTL.  If the set contains no rdatas, set the TTL\n        to the specified TTL.\n\n        *ttl*, an ``int``.\n        \"\"\"\n\n        if len(self) == 0:\n            self.ttl = ttl\n        elif ttl < self.ttl:\n            self.ttl = ttl\n\n    def add(self, rd, ttl=None):\n        \"\"\"Add the specified rdata to the rdataset.\n\n        If the optional *ttl* parameter is supplied, then\n        ``self.update_ttl(ttl)`` will be called prior to adding the rdata.\n\n        *rd*, a ``dns.rdata.Rdata``, the rdata\n\n        *ttl*, an ``int``, the TTL.\n\n        Raises ``dns.rdataset.IncompatibleTypes`` if the type and class\n        do not match the type and class of the rdataset.\n\n        Raises ``dns.rdataset.DifferingCovers`` if the type is a signature\n        type and the covered type does not match that of the rdataset.\n        \"\"\"\n\n        #\n        # If we're adding a signature, do some special handling to\n        # check that the signature covers the same type as the\n        # other rdatas in this rdataset.  If this is the first rdata\n        # in the set, initialize the covers field.\n        #\n        if self.rdclass != rd.rdclass or self.rdtype != rd.rdtype:\n            raise IncompatibleTypes\n        if ttl is not None:\n            self.update_ttl(ttl)\n        if self.rdtype == dns.rdatatype.RRSIG or \\\n           self.rdtype == dns.rdatatype.SIG:\n            covers = rd.covers()\n            if len(self) == 0 and self.covers == dns.rdatatype.NONE:\n                self.covers = covers\n            elif self.covers != covers:\n                raise DifferingCovers\n        if dns.rdatatype.is_singleton(rd.rdtype) and len(self) > 0:\n            self.clear()\n        super(Rdataset, self).add(rd)\n\n    def union_update(self, other):\n        self.update_ttl(other.ttl)\n        super(Rdataset, self).union_update(other)\n\n    def intersection_update(self, other):\n        self.update_ttl(other.ttl)\n        super(Rdataset, self).intersection_update(other)\n\n    def update(self, other):\n        \"\"\"Add all rdatas in other to self.\n\n        *other*, a ``dns.rdataset.Rdataset``, the rdataset from which\n        to update.\n        \"\"\"\n\n        self.update_ttl(other.ttl)\n        super(Rdataset, self).update(other)\n\n    def __repr__(self):\n        if self.covers == 0:\n            ctext = ''\n        else:\n            ctext = '(' + dns.rdatatype.to_text(self.covers) + ')'\n        return '<DNS ' + dns.rdataclass.to_text(self.rdclass) + ' ' + \\\n               dns.rdatatype.to_text(self.rdtype) + ctext + ' rdataset>'\n\n    def __str__(self):\n        return self.to_text()\n\n    def __eq__(self, other):\n        if not isinstance(other, Rdataset):\n            return False\n        if self.rdclass != other.rdclass or \\\n           self.rdtype != other.rdtype or \\\n           self.covers != other.covers:\n            return False\n        return super(Rdataset, self).__eq__(other)\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    def to_text(self, name=None, origin=None, relativize=True,\n                override_rdclass=None, **kw):\n        \"\"\"Convert the rdataset into DNS master file format.\n\n        See ``dns.name.Name.choose_relativity`` for more information\n        on how *origin* and *relativize* determine the way names\n        are emitted.\n\n        Any additional keyword arguments are passed on to the rdata\n        ``to_text()`` method.\n\n        *name*, a ``dns.name.Name``.  If name is not ``None``, emit RRs with\n        *name* as the owner name.\n\n        *origin*, a ``dns.name.Name`` or ``None``, the origin for relative\n        names.\n\n        *relativize*, a ``bool``.  If ``True``, names will be relativized\n        to *origin*.\n        \"\"\"\n\n        if name is not None:\n            name = name.choose_relativity(origin, relativize)\n            ntext = str(name)\n            pad = ' '\n        else:\n            ntext = ''\n            pad = ''\n        s = StringIO()\n        if override_rdclass is not None:\n            rdclass = override_rdclass\n        else:\n            rdclass = self.rdclass\n        if len(self) == 0:\n            #\n            # Empty rdatasets are used for the question section, and in\n            # some dynamic updates, so we don't need to print out the TTL\n            # (which is meaningless anyway).\n            #\n            s.write(u'{}{}{} {}\\n'.format(ntext, pad,\n                                          dns.rdataclass.to_text(rdclass),\n                                          dns.rdatatype.to_text(self.rdtype)))\n        else:\n            for rd in self:\n                s.write(u'%s%s%d %s %s %s\\n' %\n                        (ntext, pad, self.ttl, dns.rdataclass.to_text(rdclass),\n                         dns.rdatatype.to_text(self.rdtype),\n                         rd.to_text(origin=origin, relativize=relativize,\n                         **kw)))\n        #\n        # We strip off the final \\n for the caller's convenience in printing\n        #\n        return s.getvalue()[:-1]\n\n    def to_wire(self, name, file, compress=None, origin=None,\n                override_rdclass=None, want_shuffle=True):\n        \"\"\"Convert the rdataset to wire format.\n\n        *name*, a ``dns.name.Name`` is the owner name to use.\n\n        *file* is the file where the name is emitted (typically a\n        BytesIO file).\n\n        *compress*, a ``dict``, is the compression table to use.  If\n        ``None`` (the default), names will not be compressed.\n\n        *origin* is a ``dns.name.Name`` or ``None``.  If the name is\n        relative and origin is not ``None``, then *origin* will be appended\n        to it.\n\n        *override_rdclass*, an ``int``, is used as the class instead of the\n        class of the rdataset.  This is useful when rendering rdatasets\n        associated with dynamic updates.\n\n        *want_shuffle*, a ``bool``.  If ``True``, then the order of the\n        Rdatas within the Rdataset will be shuffled before rendering.\n\n        Returns an ``int``, the number of records emitted.\n        \"\"\"\n\n        if override_rdclass is not None:\n            rdclass = override_rdclass\n            want_shuffle = False\n        else:\n            rdclass = self.rdclass\n        file.seek(0, 2)\n        if len(self) == 0:\n            name.to_wire(file, compress, origin)\n            stuff = struct.pack(\"!HHIH\", self.rdtype, rdclass, 0, 0)\n            file.write(stuff)\n            return 1\n        else:\n            if want_shuffle:\n                l = list(self)\n                random.shuffle(l)\n            else:\n                l = self\n            for rd in l:\n                name.to_wire(file, compress, origin)\n                stuff = struct.pack(\"!HHIH\", self.rdtype, rdclass,\n                                    self.ttl, 0)\n                file.write(stuff)\n                start = file.tell()\n                rd.to_wire(file, compress, origin)\n                end = file.tell()\n                assert end - start < 65536\n                file.seek(start - 2)\n                stuff = struct.pack(\"!H\", end - start)\n                file.write(stuff)\n                file.seek(0, 2)\n            return len(self)\n\n    def match(self, rdclass, rdtype, covers):\n        \"\"\"Returns ``True`` if this rdataset matches the specified class,\n        type, and covers.\n        \"\"\"\n        if self.rdclass == rdclass and \\\n           self.rdtype == rdtype and \\\n           self.covers == covers:\n            return True\n        return False\n\n\ndef from_text_list(rdclass, rdtype, ttl, text_rdatas):\n    \"\"\"Create an rdataset with the specified class, type, and TTL, and with\n    the specified list of rdatas in text format.\n\n    Returns a ``dns.rdataset.Rdataset`` object.\n    \"\"\"\n\n    if isinstance(rdclass, string_types):\n        rdclass = dns.rdataclass.from_text(rdclass)\n    if isinstance(rdtype, string_types):\n        rdtype = dns.rdatatype.from_text(rdtype)\n    r = Rdataset(rdclass, rdtype)\n    r.update_ttl(ttl)\n    for t in text_rdatas:\n        rd = dns.rdata.from_text(r.rdclass, r.rdtype, t)\n        r.add(rd)\n    return r\n\n\ndef from_text(rdclass, rdtype, ttl, *text_rdatas):\n    \"\"\"Create an rdataset with the specified class, type, and TTL, and with\n    the specified rdatas in text format.\n\n    Returns a ``dns.rdataset.Rdataset`` object.\n    \"\"\"\n\n    return from_text_list(rdclass, rdtype, ttl, text_rdatas)\n\n\ndef from_rdata_list(ttl, rdatas):\n    \"\"\"Create an rdataset with the specified TTL, and with\n    the specified list of rdata objects.\n\n    Returns a ``dns.rdataset.Rdataset`` object.\n    \"\"\"\n\n    if len(rdatas) == 0:\n        raise ValueError(\"rdata list must not be empty\")\n    r = None\n    for rd in rdatas:\n        if r is None:\n            r = Rdataset(rd.rdclass, rd.rdtype)\n            r.update_ttl(ttl)\n        r.add(rd)\n    return r\n\n\ndef from_rdata(ttl, *rdatas):\n    \"\"\"Create an rdataset with the specified TTL, and with\n    the specified rdata objects.\n\n    Returns a ``dns.rdataset.Rdataset`` object.\n    \"\"\"\n\n    return from_rdata_list(ttl, rdatas)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdatatype.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2001-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS Rdata Types.\"\"\"\n\nimport re\n\nimport dns.exception\n\nNONE = 0\nA = 1\nNS = 2\nMD = 3\nMF = 4\nCNAME = 5\nSOA = 6\nMB = 7\nMG = 8\nMR = 9\nNULL = 10\nWKS = 11\nPTR = 12\nHINFO = 13\nMINFO = 14\nMX = 15\nTXT = 16\nRP = 17\nAFSDB = 18\nX25 = 19\nISDN = 20\nRT = 21\nNSAP = 22\nNSAP_PTR = 23\nSIG = 24\nKEY = 25\nPX = 26\nGPOS = 27\nAAAA = 28\nLOC = 29\nNXT = 30\nSRV = 33\nNAPTR = 35\nKX = 36\nCERT = 37\nA6 = 38\nDNAME = 39\nOPT = 41\nAPL = 42\nDS = 43\nSSHFP = 44\nIPSECKEY = 45\nRRSIG = 46\nNSEC = 47\nDNSKEY = 48\nDHCID = 49\nNSEC3 = 50\nNSEC3PARAM = 51\nTLSA = 52\nHIP = 55\nCDS = 59\nCDNSKEY = 60\nOPENPGPKEY = 61\nCSYNC = 62\nSPF = 99\nUNSPEC = 103\nEUI48 = 108\nEUI64 = 109\nTKEY = 249\nTSIG = 250\nIXFR = 251\nAXFR = 252\nMAILB = 253\nMAILA = 254\nANY = 255\nURI = 256\nCAA = 257\nAVC = 258\nTA = 32768\nDLV = 32769\n\n_by_text = {\n    'NONE': NONE,\n    'A': A,\n    'NS': NS,\n    'MD': MD,\n    'MF': MF,\n    'CNAME': CNAME,\n    'SOA': SOA,\n    'MB': MB,\n    'MG': MG,\n    'MR': MR,\n    'NULL': NULL,\n    'WKS': WKS,\n    'PTR': PTR,\n    'HINFO': HINFO,\n    'MINFO': MINFO,\n    'MX': MX,\n    'TXT': TXT,\n    'RP': RP,\n    'AFSDB': AFSDB,\n    'X25': X25,\n    'ISDN': ISDN,\n    'RT': RT,\n    'NSAP': NSAP,\n    'NSAP-PTR': NSAP_PTR,\n    'SIG': SIG,\n    'KEY': KEY,\n    'PX': PX,\n    'GPOS': GPOS,\n    'AAAA': AAAA,\n    'LOC': LOC,\n    'NXT': NXT,\n    'SRV': SRV,\n    'NAPTR': NAPTR,\n    'KX': KX,\n    'CERT': CERT,\n    'A6': A6,\n    'DNAME': DNAME,\n    'OPT': OPT,\n    'APL': APL,\n    'DS': DS,\n    'SSHFP': SSHFP,\n    'IPSECKEY': IPSECKEY,\n    'RRSIG': RRSIG,\n    'NSEC': NSEC,\n    'DNSKEY': DNSKEY,\n    'DHCID': DHCID,\n    'NSEC3': NSEC3,\n    'NSEC3PARAM': NSEC3PARAM,\n    'TLSA': TLSA,\n    'HIP': HIP,\n    'CDS': CDS,\n    'CDNSKEY': CDNSKEY,\n    'OPENPGPKEY': OPENPGPKEY,\n    'CSYNC': CSYNC,\n    'SPF': SPF,\n    'UNSPEC': UNSPEC,\n    'EUI48': EUI48,\n    'EUI64': EUI64,\n    'TKEY': TKEY,\n    'TSIG': TSIG,\n    'IXFR': IXFR,\n    'AXFR': AXFR,\n    'MAILB': MAILB,\n    'MAILA': MAILA,\n    'ANY': ANY,\n    'URI': URI,\n    'CAA': CAA,\n    'AVC': AVC,\n    'TA': TA,\n    'DLV': DLV,\n}\n\n# We construct the inverse mapping programmatically to ensure that we\n# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that\n# would cause the mapping not to be true inverse.\n\n_by_value = {y: x for x, y in _by_text.items()}\n\n_metatypes = {\n    OPT: True\n}\n\n_singletons = {\n    SOA: True,\n    NXT: True,\n    DNAME: True,\n    NSEC: True,\n    CNAME: True,\n}\n\n_unknown_type_pattern = re.compile('TYPE([0-9]+)$', re.I)\n\n\nclass UnknownRdatatype(dns.exception.DNSException):\n    \"\"\"DNS resource record type is unknown.\"\"\"\n\n\ndef from_text(text):\n    \"\"\"Convert text into a DNS rdata type value.\n\n    The input text can be a defined DNS RR type mnemonic or\n    instance of the DNS generic type syntax.\n\n    For example, \"NS\" and \"TYPE2\" will both result in a value of 2.\n\n    Raises ``dns.rdatatype.UnknownRdatatype`` if the type is unknown.\n\n    Raises ``ValueError`` if the rdata type value is not >= 0 and <= 65535.\n\n    Returns an ``int``.\n    \"\"\"\n\n    value = _by_text.get(text.upper())\n    if value is None:\n        match = _unknown_type_pattern.match(text)\n        if match is None:\n            raise UnknownRdatatype\n        value = int(match.group(1))\n        if value < 0 or value > 65535:\n            raise ValueError(\"type must be between >= 0 and <= 65535\")\n    return value\n\n\ndef to_text(value):\n    \"\"\"Convert a DNS rdata type value to text.\n\n    If the value has a known mnemonic, it will be used, otherwise the\n    DNS generic type syntax will be used.\n\n    Raises ``ValueError`` if the rdata type value is not >= 0 and <= 65535.\n\n    Returns a ``str``.\n    \"\"\"\n\n    if value < 0 or value > 65535:\n        raise ValueError(\"type must be between >= 0 and <= 65535\")\n    text = _by_value.get(value)\n    if text is None:\n        text = 'TYPE' + repr(value)\n    return text\n\n\ndef is_metatype(rdtype):\n    \"\"\"True if the specified type is a metatype.\n\n    *rdtype* is an ``int``.\n\n    The currently defined metatypes are TKEY, TSIG, IXFR, AXFR, MAILA,\n    MAILB, ANY, and OPT.\n\n    Returns a ``bool``.\n    \"\"\"\n\n    if rdtype >= TKEY and rdtype <= ANY or rdtype in _metatypes:\n        return True\n    return False\n\n\ndef is_singleton(rdtype):\n    \"\"\"Is the specified type a singleton type?\n\n    Singleton types can only have a single rdata in an rdataset, or a single\n    RR in an RRset.\n\n    The currently defined singleton types are CNAME, DNAME, NSEC, NXT, and\n    SOA.\n\n    *rdtype* is an ``int``.\n\n    Returns a ``bool``.\n    \"\"\"\n\n    if rdtype in _singletons:\n        return True\n    return False\n\n\ndef register_type(rdtype, rdtype_text, is_singleton=False):  # pylint: disable=redefined-outer-name\n    \"\"\"Dynamically register an rdatatype.\n\n    *rdtype*, an ``int``, the rdatatype to register.\n\n    *rdtype_text*, a ``text``, the textual form of the rdatatype.\n\n    *is_singleton*, a ``bool``, indicating if the type is a singleton (i.e.\n    RRsets of the type can have only one member.)\n    \"\"\"\n\n    _by_text[rdtype_text] = rdtype\n    _by_value[rdtype] = rdtype_text\n    if is_singleton:\n        _singletons[rdtype] = True\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/AFSDB.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.mxbase\n\n\nclass AFSDB(dns.rdtypes.mxbase.UncompressedDowncasingMX):\n\n    \"\"\"AFSDB record\n\n    @ivar subtype: the subtype value\n    @type subtype: int\n    @ivar hostname: the hostname name\n    @type hostname: dns.name.Name object\"\"\"\n\n    # Use the property mechanism to make \"subtype\" an alias for the\n    # \"preference\" attribute, and \"hostname\" an alias for the \"exchange\"\n    # attribute.\n    #\n    # This lets us inherit the UncompressedMX implementation but lets\n    # the caller use appropriate attribute names for the rdata type.\n    #\n    # We probably lose some performance vs. a cut-and-paste\n    # implementation, but this way we don't copy code, and that's\n    # good.\n\n    def get_subtype(self):\n        return self.preference\n\n    def set_subtype(self, subtype):\n        self.preference = subtype\n\n    subtype = property(get_subtype, set_subtype)\n\n    def get_hostname(self):\n        return self.exchange\n\n    def set_hostname(self, hostname):\n        self.exchange = hostname\n\n    hostname = property(get_hostname, set_hostname)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/AVC.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2016 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.txtbase\n\n\nclass AVC(dns.rdtypes.txtbase.TXTBase):\n\n    \"\"\"AVC record\n\n    @see: U{http://www.iana.org/assignments/dns-parameters/AVC/avc-completed-template}\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/CAA.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.tokenizer\n\n\nclass CAA(dns.rdata.Rdata):\n\n    \"\"\"CAA (Certification Authority Authorization) record\n\n    @ivar flags: the flags\n    @type flags: int\n    @ivar tag: the tag\n    @type tag: string\n    @ivar value: the value\n    @type value: string\n    @see: RFC 6844\"\"\"\n\n    __slots__ = ['flags', 'tag', 'value']\n\n    def __init__(self, rdclass, rdtype, flags, tag, value):\n        super(CAA, self).__init__(rdclass, rdtype)\n        self.flags = flags\n        self.tag = tag\n        self.value = value\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return '%u %s \"%s\"' % (self.flags,\n                               dns.rdata._escapify(self.tag),\n                               dns.rdata._escapify(self.value))\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        flags = tok.get_uint8()\n        tag = tok.get_string().encode()\n        if len(tag) > 255:\n            raise dns.exception.SyntaxError(\"tag too long\")\n        if not tag.isalnum():\n            raise dns.exception.SyntaxError(\"tag is not alphanumeric\")\n        value = tok.get_string().encode()\n        return cls(rdclass, rdtype, flags, tag, value)\n\n    def to_wire(self, file, compress=None, origin=None):\n        file.write(struct.pack('!B', self.flags))\n        l = len(self.tag)\n        assert l < 256\n        file.write(struct.pack('!B', l))\n        file.write(self.tag)\n        file.write(self.value)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (flags, l) = struct.unpack('!BB', wire[current: current + 2])\n        current += 2\n        tag = wire[current: current + l]\n        value = wire[current + l:current + rdlen - 2]\n        return cls(rdclass, rdtype, flags, tag, value)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/CDNSKEY.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.dnskeybase\nfrom dns.rdtypes.dnskeybase import flags_to_text_set, flags_from_text_set\n\n\n__all__ = ['flags_to_text_set', 'flags_from_text_set']\n\n\nclass CDNSKEY(dns.rdtypes.dnskeybase.DNSKEYBase):\n\n    \"\"\"CDNSKEY record\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/CDS.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.dsbase\n\n\nclass CDS(dns.rdtypes.dsbase.DSBase):\n\n    \"\"\"CDS record\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/CERT.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\nimport base64\n\nimport dns.exception\nimport dns.dnssec\nimport dns.rdata\nimport dns.tokenizer\n\n_ctype_by_value = {\n    1: 'PKIX',\n    2: 'SPKI',\n    3: 'PGP',\n    253: 'URI',\n    254: 'OID',\n}\n\n_ctype_by_name = {\n    'PKIX': 1,\n    'SPKI': 2,\n    'PGP': 3,\n    'URI': 253,\n    'OID': 254,\n}\n\n\ndef _ctype_from_text(what):\n    v = _ctype_by_name.get(what)\n    if v is not None:\n        return v\n    return int(what)\n\n\ndef _ctype_to_text(what):\n    v = _ctype_by_value.get(what)\n    if v is not None:\n        return v\n    return str(what)\n\n\nclass CERT(dns.rdata.Rdata):\n\n    \"\"\"CERT record\n\n    @ivar certificate_type: certificate type\n    @type certificate_type: int\n    @ivar key_tag: key tag\n    @type key_tag: int\n    @ivar algorithm: algorithm\n    @type algorithm: int\n    @ivar certificate: the certificate or CRL\n    @type certificate: string\n    @see: RFC 2538\"\"\"\n\n    __slots__ = ['certificate_type', 'key_tag', 'algorithm', 'certificate']\n\n    def __init__(self, rdclass, rdtype, certificate_type, key_tag, algorithm,\n                 certificate):\n        super(CERT, self).__init__(rdclass, rdtype)\n        self.certificate_type = certificate_type\n        self.key_tag = key_tag\n        self.algorithm = algorithm\n        self.certificate = certificate\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        certificate_type = _ctype_to_text(self.certificate_type)\n        return \"%s %d %s %s\" % (certificate_type, self.key_tag,\n                                dns.dnssec.algorithm_to_text(self.algorithm),\n                                dns.rdata._base64ify(self.certificate))\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        certificate_type = _ctype_from_text(tok.get_string())\n        key_tag = tok.get_uint16()\n        algorithm = dns.dnssec.algorithm_from_text(tok.get_string())\n        if algorithm < 0 or algorithm > 255:\n            raise dns.exception.SyntaxError(\"bad algorithm type\")\n        chunks = []\n        while 1:\n            t = tok.get().unescape()\n            if t.is_eol_or_eof():\n                break\n            if not t.is_identifier():\n                raise dns.exception.SyntaxError\n            chunks.append(t.value.encode())\n        b64 = b''.join(chunks)\n        certificate = base64.b64decode(b64)\n        return cls(rdclass, rdtype, certificate_type, key_tag,\n                   algorithm, certificate)\n\n    def to_wire(self, file, compress=None, origin=None):\n        prefix = struct.pack(\"!HHB\", self.certificate_type, self.key_tag,\n                             self.algorithm)\n        file.write(prefix)\n        file.write(self.certificate)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        prefix = wire[current: current + 5].unwrap()\n        current += 5\n        rdlen -= 5\n        if rdlen < 0:\n            raise dns.exception.FormError\n        (certificate_type, key_tag, algorithm) = struct.unpack(\"!HHB\", prefix)\n        certificate = wire[current: current + rdlen].unwrap()\n        return cls(rdclass, rdtype, certificate_type, key_tag, algorithm,\n                   certificate)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/CNAME.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.nsbase\n\n\nclass CNAME(dns.rdtypes.nsbase.NSBase):\n\n    \"\"\"CNAME record\n\n    Note: although CNAME is officially a singleton type, dnspython allows\n    non-singleton CNAME rdatasets because such sets have been commonly\n    used by BIND and other nameservers for load balancing.\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/CSYNC.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2004-2007, 2009-2011, 2016 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.rdatatype\nimport dns.name\nfrom dns._compat import xrange\n\nclass CSYNC(dns.rdata.Rdata):\n\n    \"\"\"CSYNC record\n\n    @ivar serial: the SOA serial number\n    @type serial: int\n    @ivar flags: the CSYNC flags\n    @type flags: int\n    @ivar windows: the windowed bitmap list\n    @type windows: list of (window number, string) tuples\"\"\"\n\n    __slots__ = ['serial', 'flags', 'windows']\n\n    def __init__(self, rdclass, rdtype, serial, flags, windows):\n        super(CSYNC, self).__init__(rdclass, rdtype)\n        self.serial = serial\n        self.flags = flags\n        self.windows = windows\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        text = ''\n        for (window, bitmap) in self.windows:\n            bits = []\n            for i in xrange(0, len(bitmap)):\n                byte = bitmap[i]\n                for j in xrange(0, 8):\n                    if byte & (0x80 >> j):\n                        bits.append(dns.rdatatype.to_text(window * 256 +\n                                                          i * 8 + j))\n            text += (' ' + ' '.join(bits))\n        return '%d %d%s' % (self.serial, self.flags, text)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        serial = tok.get_uint32()\n        flags = tok.get_uint16()\n        rdtypes = []\n        while 1:\n            token = tok.get().unescape()\n            if token.is_eol_or_eof():\n                break\n            nrdtype = dns.rdatatype.from_text(token.value)\n            if nrdtype == 0:\n                raise dns.exception.SyntaxError(\"CSYNC with bit 0\")\n            if nrdtype > 65535:\n                raise dns.exception.SyntaxError(\"CSYNC with bit > 65535\")\n            rdtypes.append(nrdtype)\n        rdtypes.sort()\n        window = 0\n        octets = 0\n        prior_rdtype = 0\n        bitmap = bytearray(b'\\0' * 32)\n        windows = []\n        for nrdtype in rdtypes:\n            if nrdtype == prior_rdtype:\n                continue\n            prior_rdtype = nrdtype\n            new_window = nrdtype // 256\n            if new_window != window:\n                windows.append((window, bitmap[0:octets]))\n                bitmap = bytearray(b'\\0' * 32)\n                window = new_window\n            offset = nrdtype % 256\n            byte = offset // 8\n            bit = offset % 8\n            octets = byte + 1\n            bitmap[byte] = bitmap[byte] | (0x80 >> bit)\n\n        windows.append((window, bitmap[0:octets]))\n        return cls(rdclass, rdtype, serial, flags, windows)\n\n    def to_wire(self, file, compress=None, origin=None):\n        file.write(struct.pack('!IH', self.serial, self.flags))\n        for (window, bitmap) in self.windows:\n            file.write(struct.pack('!BB', window, len(bitmap)))\n            file.write(bitmap)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        if rdlen < 6:\n            raise dns.exception.FormError(\"CSYNC too short\")\n        (serial, flags) = struct.unpack(\"!IH\", wire[current: current + 6])\n        current += 6\n        rdlen -= 6\n        windows = []\n        while rdlen > 0:\n            if rdlen < 3:\n                raise dns.exception.FormError(\"CSYNC too short\")\n            window = wire[current]\n            octets = wire[current + 1]\n            if octets == 0 or octets > 32:\n                raise dns.exception.FormError(\"bad CSYNC octets\")\n            current += 2\n            rdlen -= 2\n            if rdlen < octets:\n                raise dns.exception.FormError(\"bad CSYNC bitmap length\")\n            bitmap = bytearray(wire[current: current + octets].unwrap())\n            current += octets\n            rdlen -= octets\n            windows.append((window, bitmap))\n        return cls(rdclass, rdtype, serial, flags, windows)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/DLV.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.dsbase\n\n\nclass DLV(dns.rdtypes.dsbase.DSBase):\n\n    \"\"\"DLV record\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/DNAME.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.nsbase\n\n\nclass DNAME(dns.rdtypes.nsbase.UncompressedNS):\n\n    \"\"\"DNAME record\"\"\"\n\n    def to_digestable(self, origin=None):\n        return self.target.to_digestable(origin)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/DNSKEY.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.dnskeybase\nfrom dns.rdtypes.dnskeybase import flags_to_text_set, flags_from_text_set\n\n\n__all__ = ['flags_to_text_set', 'flags_from_text_set']\n\n\nclass DNSKEY(dns.rdtypes.dnskeybase.DNSKEYBase):\n\n    \"\"\"DNSKEY record\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/DS.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.dsbase\n\n\nclass DS(dns.rdtypes.dsbase.DSBase):\n\n    \"\"\"DS record\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/EUI48.py",
    "content": "# Copyright (C) 2015 Red Hat, Inc.\n# Author: Petr Spacek <pspacek@redhat.com>\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.euibase\n\n\nclass EUI48(dns.rdtypes.euibase.EUIBase):\n\n    \"\"\"EUI48 record\n\n    @ivar fingerprint: 48-bit Extended Unique Identifier (EUI-48)\n    @type fingerprint: string\n    @see: rfc7043.txt\"\"\"\n\n    byte_len = 6  # 0123456789ab (in hex)\n    text_len = byte_len * 3 - 1  # 01-23-45-67-89-ab\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/EUI64.py",
    "content": "# Copyright (C) 2015 Red Hat, Inc.\n# Author: Petr Spacek <pspacek@redhat.com>\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.euibase\n\n\nclass EUI64(dns.rdtypes.euibase.EUIBase):\n\n    \"\"\"EUI64 record\n\n    @ivar fingerprint: 64-bit Extended Unique Identifier (EUI-64)\n    @type fingerprint: string\n    @see: rfc7043.txt\"\"\"\n\n    byte_len = 8  # 0123456789abcdef (in hex)\n    text_len = byte_len * 3 - 1  # 01-23-45-67-89-ab-cd-ef\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/GPOS.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.tokenizer\nfrom dns._compat import long, text_type\n\n\ndef _validate_float_string(what):\n    if what[0] == b'-'[0] or what[0] == b'+'[0]:\n        what = what[1:]\n    if what.isdigit():\n        return\n    (left, right) = what.split(b'.')\n    if left == b'' and right == b'':\n        raise dns.exception.FormError\n    if not left == b'' and not left.decode().isdigit():\n        raise dns.exception.FormError\n    if not right == b'' and not right.decode().isdigit():\n        raise dns.exception.FormError\n\n\ndef _sanitize(value):\n    if isinstance(value, text_type):\n        return value.encode()\n    return value\n\n\nclass GPOS(dns.rdata.Rdata):\n\n    \"\"\"GPOS record\n\n    @ivar latitude: latitude\n    @type latitude: string\n    @ivar longitude: longitude\n    @type longitude: string\n    @ivar altitude: altitude\n    @type altitude: string\n    @see: RFC 1712\"\"\"\n\n    __slots__ = ['latitude', 'longitude', 'altitude']\n\n    def __init__(self, rdclass, rdtype, latitude, longitude, altitude):\n        super(GPOS, self).__init__(rdclass, rdtype)\n        if isinstance(latitude, float) or \\\n           isinstance(latitude, int) or \\\n           isinstance(latitude, long):\n            latitude = str(latitude)\n        if isinstance(longitude, float) or \\\n           isinstance(longitude, int) or \\\n           isinstance(longitude, long):\n            longitude = str(longitude)\n        if isinstance(altitude, float) or \\\n           isinstance(altitude, int) or \\\n           isinstance(altitude, long):\n            altitude = str(altitude)\n        latitude = _sanitize(latitude)\n        longitude = _sanitize(longitude)\n        altitude = _sanitize(altitude)\n        _validate_float_string(latitude)\n        _validate_float_string(longitude)\n        _validate_float_string(altitude)\n        self.latitude = latitude\n        self.longitude = longitude\n        self.altitude = altitude\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return '{} {} {}'.format(self.latitude.decode(),\n                             self.longitude.decode(),\n                             self.altitude.decode())\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        latitude = tok.get_string()\n        longitude = tok.get_string()\n        altitude = tok.get_string()\n        tok.get_eol()\n        return cls(rdclass, rdtype, latitude, longitude, altitude)\n\n    def to_wire(self, file, compress=None, origin=None):\n        l = len(self.latitude)\n        assert l < 256\n        file.write(struct.pack('!B', l))\n        file.write(self.latitude)\n        l = len(self.longitude)\n        assert l < 256\n        file.write(struct.pack('!B', l))\n        file.write(self.longitude)\n        l = len(self.altitude)\n        assert l < 256\n        file.write(struct.pack('!B', l))\n        file.write(self.altitude)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        l = wire[current]\n        current += 1\n        rdlen -= 1\n        if l > rdlen:\n            raise dns.exception.FormError\n        latitude = wire[current: current + l].unwrap()\n        current += l\n        rdlen -= l\n        l = wire[current]\n        current += 1\n        rdlen -= 1\n        if l > rdlen:\n            raise dns.exception.FormError\n        longitude = wire[current: current + l].unwrap()\n        current += l\n        rdlen -= l\n        l = wire[current]\n        current += 1\n        rdlen -= 1\n        if l != rdlen:\n            raise dns.exception.FormError\n        altitude = wire[current: current + l].unwrap()\n        return cls(rdclass, rdtype, latitude, longitude, altitude)\n\n    def _get_float_latitude(self):\n        return float(self.latitude)\n\n    def _set_float_latitude(self, value):\n        self.latitude = str(value)\n\n    float_latitude = property(_get_float_latitude, _set_float_latitude,\n                              doc=\"latitude as a floating point value\")\n\n    def _get_float_longitude(self):\n        return float(self.longitude)\n\n    def _set_float_longitude(self, value):\n        self.longitude = str(value)\n\n    float_longitude = property(_get_float_longitude, _set_float_longitude,\n                               doc=\"longitude as a floating point value\")\n\n    def _get_float_altitude(self):\n        return float(self.altitude)\n\n    def _set_float_altitude(self, value):\n        self.altitude = str(value)\n\n    float_altitude = property(_get_float_altitude, _set_float_altitude,\n                              doc=\"altitude as a floating point value\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/HINFO.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.tokenizer\nfrom dns._compat import text_type\n\n\nclass HINFO(dns.rdata.Rdata):\n\n    \"\"\"HINFO record\n\n    @ivar cpu: the CPU type\n    @type cpu: string\n    @ivar os: the OS type\n    @type os: string\n    @see: RFC 1035\"\"\"\n\n    __slots__ = ['cpu', 'os']\n\n    def __init__(self, rdclass, rdtype, cpu, os):\n        super(HINFO, self).__init__(rdclass, rdtype)\n        if isinstance(cpu, text_type):\n            self.cpu = cpu.encode()\n        else:\n            self.cpu = cpu\n        if isinstance(os, text_type):\n            self.os = os.encode()\n        else:\n            self.os = os\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return '\"{}\" \"{}\"'.format(dns.rdata._escapify(self.cpu),\n                                  dns.rdata._escapify(self.os))\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        cpu = tok.get_string()\n        os = tok.get_string()\n        tok.get_eol()\n        return cls(rdclass, rdtype, cpu, os)\n\n    def to_wire(self, file, compress=None, origin=None):\n        l = len(self.cpu)\n        assert l < 256\n        file.write(struct.pack('!B', l))\n        file.write(self.cpu)\n        l = len(self.os)\n        assert l < 256\n        file.write(struct.pack('!B', l))\n        file.write(self.os)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        l = wire[current]\n        current += 1\n        rdlen -= 1\n        if l > rdlen:\n            raise dns.exception.FormError\n        cpu = wire[current:current + l].unwrap()\n        current += l\n        rdlen -= l\n        l = wire[current]\n        current += 1\n        rdlen -= 1\n        if l != rdlen:\n            raise dns.exception.FormError\n        os = wire[current: current + l].unwrap()\n        return cls(rdclass, rdtype, cpu, os)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/HIP.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2010, 2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\nimport base64\nimport binascii\n\nimport dns.exception\nimport dns.rdata\nimport dns.rdatatype\n\n\nclass HIP(dns.rdata.Rdata):\n\n    \"\"\"HIP record\n\n    @ivar hit: the host identity tag\n    @type hit: string\n    @ivar algorithm: the public key cryptographic algorithm\n    @type algorithm: int\n    @ivar key: the public key\n    @type key: string\n    @ivar servers: the rendezvous servers\n    @type servers: list of dns.name.Name objects\n    @see: RFC 5205\"\"\"\n\n    __slots__ = ['hit', 'algorithm', 'key', 'servers']\n\n    def __init__(self, rdclass, rdtype, hit, algorithm, key, servers):\n        super(HIP, self).__init__(rdclass, rdtype)\n        self.hit = hit\n        self.algorithm = algorithm\n        self.key = key\n        self.servers = servers\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        hit = binascii.hexlify(self.hit).decode()\n        key = base64.b64encode(self.key).replace(b'\\n', b'').decode()\n        text = u''\n        servers = []\n        for server in self.servers:\n            servers.append(server.choose_relativity(origin, relativize))\n        if len(servers) > 0:\n            text += (u' ' + u' '.join((x.to_unicode() for x in servers)))\n        return u'%u %s %s%s' % (self.algorithm, hit, key, text)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        algorithm = tok.get_uint8()\n        hit = binascii.unhexlify(tok.get_string().encode())\n        if len(hit) > 255:\n            raise dns.exception.SyntaxError(\"HIT too long\")\n        key = base64.b64decode(tok.get_string().encode())\n        servers = []\n        while 1:\n            token = tok.get()\n            if token.is_eol_or_eof():\n                break\n            server = dns.name.from_text(token.value, origin)\n            server.choose_relativity(origin, relativize)\n            servers.append(server)\n        return cls(rdclass, rdtype, hit, algorithm, key, servers)\n\n    def to_wire(self, file, compress=None, origin=None):\n        lh = len(self.hit)\n        lk = len(self.key)\n        file.write(struct.pack(\"!BBH\", lh, self.algorithm, lk))\n        file.write(self.hit)\n        file.write(self.key)\n        for server in self.servers:\n            server.to_wire(file, None, origin)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (lh, algorithm, lk) = struct.unpack('!BBH',\n                                            wire[current: current + 4])\n        current += 4\n        rdlen -= 4\n        hit = wire[current: current + lh].unwrap()\n        current += lh\n        rdlen -= lh\n        key = wire[current: current + lk].unwrap()\n        current += lk\n        rdlen -= lk\n        servers = []\n        while rdlen > 0:\n            (server, cused) = dns.name.from_wire(wire[: current + rdlen],\n                                                 current)\n            current += cused\n            rdlen -= cused\n            if origin is not None:\n                server = server.relativize(origin)\n            servers.append(server)\n        return cls(rdclass, rdtype, hit, algorithm, key, servers)\n\n    def choose_relativity(self, origin=None, relativize=True):\n        servers = []\n        for server in self.servers:\n            server = server.choose_relativity(origin, relativize)\n            servers.append(server)\n        self.servers = servers\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/ISDN.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.tokenizer\nfrom dns._compat import text_type\n\n\nclass ISDN(dns.rdata.Rdata):\n\n    \"\"\"ISDN record\n\n    @ivar address: the ISDN address\n    @type address: string\n    @ivar subaddress: the ISDN subaddress (or '' if not present)\n    @type subaddress: string\n    @see: RFC 1183\"\"\"\n\n    __slots__ = ['address', 'subaddress']\n\n    def __init__(self, rdclass, rdtype, address, subaddress):\n        super(ISDN, self).__init__(rdclass, rdtype)\n        if isinstance(address, text_type):\n            self.address = address.encode()\n        else:\n            self.address = address\n        if isinstance(address, text_type):\n            self.subaddress = subaddress.encode()\n        else:\n            self.subaddress = subaddress\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        if self.subaddress:\n            return '\"{}\" \"{}\"'.format(dns.rdata._escapify(self.address),\n                                  dns.rdata._escapify(self.subaddress))\n        else:\n            return '\"%s\"' % dns.rdata._escapify(self.address)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        address = tok.get_string()\n        t = tok.get()\n        if not t.is_eol_or_eof():\n            tok.unget(t)\n            subaddress = tok.get_string()\n        else:\n            tok.unget(t)\n            subaddress = ''\n        tok.get_eol()\n        return cls(rdclass, rdtype, address, subaddress)\n\n    def to_wire(self, file, compress=None, origin=None):\n        l = len(self.address)\n        assert l < 256\n        file.write(struct.pack('!B', l))\n        file.write(self.address)\n        l = len(self.subaddress)\n        if l > 0:\n            assert l < 256\n            file.write(struct.pack('!B', l))\n            file.write(self.subaddress)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        l = wire[current]\n        current += 1\n        rdlen -= 1\n        if l > rdlen:\n            raise dns.exception.FormError\n        address = wire[current: current + l].unwrap()\n        current += l\n        rdlen -= l\n        if rdlen > 0:\n            l = wire[current]\n            current += 1\n            rdlen -= 1\n            if l != rdlen:\n                raise dns.exception.FormError\n            subaddress = wire[current: current + l].unwrap()\n        else:\n            subaddress = ''\n        return cls(rdclass, rdtype, address, subaddress)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/LOC.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nfrom __future__ import division\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nfrom dns._compat import long, xrange, round_py2_compat\n\n\n_pows = tuple(long(10**i) for i in range(0, 11))\n\n# default values are in centimeters\n_default_size = 100.0\n_default_hprec = 1000000.0\n_default_vprec = 1000.0\n\n\ndef _exponent_of(what, desc):\n    if what == 0:\n        return 0\n    exp = None\n    for i in xrange(len(_pows)):\n        if what // _pows[i] == long(0):\n            exp = i - 1\n            break\n    if exp is None or exp < 0:\n        raise dns.exception.SyntaxError(\"%s value out of bounds\" % desc)\n    return exp\n\n\ndef _float_to_tuple(what):\n    if what < 0:\n        sign = -1\n        what *= -1\n    else:\n        sign = 1\n    what = round_py2_compat(what * 3600000)\n    degrees = int(what // 3600000)\n    what -= degrees * 3600000\n    minutes = int(what // 60000)\n    what -= minutes * 60000\n    seconds = int(what // 1000)\n    what -= int(seconds * 1000)\n    what = int(what)\n    return (degrees, minutes, seconds, what, sign)\n\n\ndef _tuple_to_float(what):\n    value = float(what[0])\n    value += float(what[1]) / 60.0\n    value += float(what[2]) / 3600.0\n    value += float(what[3]) / 3600000.0\n    return float(what[4]) * value\n\n\ndef _encode_size(what, desc):\n    what = long(what)\n    exponent = _exponent_of(what, desc) & 0xF\n    base = what // pow(10, exponent) & 0xF\n    return base * 16 + exponent\n\n\ndef _decode_size(what, desc):\n    exponent = what & 0x0F\n    if exponent > 9:\n        raise dns.exception.SyntaxError(\"bad %s exponent\" % desc)\n    base = (what & 0xF0) >> 4\n    if base > 9:\n        raise dns.exception.SyntaxError(\"bad %s base\" % desc)\n    return long(base) * pow(10, exponent)\n\n\nclass LOC(dns.rdata.Rdata):\n\n    \"\"\"LOC record\n\n    @ivar latitude: latitude\n    @type latitude: (int, int, int, int, sign) tuple specifying the degrees, minutes,\n    seconds, milliseconds, and sign of the coordinate.\n    @ivar longitude: longitude\n    @type longitude: (int, int, int, int, sign) tuple specifying the degrees,\n    minutes, seconds, milliseconds, and sign of the coordinate.\n    @ivar altitude: altitude\n    @type altitude: float\n    @ivar size: size of the sphere\n    @type size: float\n    @ivar horizontal_precision: horizontal precision\n    @type horizontal_precision: float\n    @ivar vertical_precision: vertical precision\n    @type vertical_precision: float\n    @see: RFC 1876\"\"\"\n\n    __slots__ = ['latitude', 'longitude', 'altitude', 'size',\n                 'horizontal_precision', 'vertical_precision']\n\n    def __init__(self, rdclass, rdtype, latitude, longitude, altitude,\n                 size=_default_size, hprec=_default_hprec,\n                 vprec=_default_vprec):\n        \"\"\"Initialize a LOC record instance.\n\n        The parameters I{latitude} and I{longitude} may be either a 4-tuple\n        of integers specifying (degrees, minutes, seconds, milliseconds),\n        or they may be floating point values specifying the number of\n        degrees. The other parameters are floats. Size, horizontal precision,\n        and vertical precision are specified in centimeters.\"\"\"\n\n        super(LOC, self).__init__(rdclass, rdtype)\n        if isinstance(latitude, int) or isinstance(latitude, long):\n            latitude = float(latitude)\n        if isinstance(latitude, float):\n            latitude = _float_to_tuple(latitude)\n        self.latitude = latitude\n        if isinstance(longitude, int) or isinstance(longitude, long):\n            longitude = float(longitude)\n        if isinstance(longitude, float):\n            longitude = _float_to_tuple(longitude)\n        self.longitude = longitude\n        self.altitude = float(altitude)\n        self.size = float(size)\n        self.horizontal_precision = float(hprec)\n        self.vertical_precision = float(vprec)\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        if self.latitude[4] > 0:\n            lat_hemisphere = 'N'\n        else:\n            lat_hemisphere = 'S'\n        if self.longitude[4] > 0:\n            long_hemisphere = 'E'\n        else:\n            long_hemisphere = 'W'\n        text = \"%d %d %d.%03d %s %d %d %d.%03d %s %0.2fm\" % (\n            self.latitude[0], self.latitude[1],\n            self.latitude[2], self.latitude[3], lat_hemisphere,\n            self.longitude[0], self.longitude[1], self.longitude[2],\n            self.longitude[3], long_hemisphere,\n            self.altitude / 100.0\n        )\n\n        # do not print default values\n        if self.size != _default_size or \\\n            self.horizontal_precision != _default_hprec or \\\n                self.vertical_precision != _default_vprec:\n            text += \" {:0.2f}m {:0.2f}m {:0.2f}m\".format(\n                self.size / 100.0, self.horizontal_precision / 100.0,\n                self.vertical_precision / 100.0\n            )\n        return text\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        latitude = [0, 0, 0, 0, 1]\n        longitude = [0, 0, 0, 0, 1]\n        size = _default_size\n        hprec = _default_hprec\n        vprec = _default_vprec\n\n        latitude[0] = tok.get_int()\n        t = tok.get_string()\n        if t.isdigit():\n            latitude[1] = int(t)\n            t = tok.get_string()\n            if '.' in t:\n                (seconds, milliseconds) = t.split('.')\n                if not seconds.isdigit():\n                    raise dns.exception.SyntaxError(\n                        'bad latitude seconds value')\n                latitude[2] = int(seconds)\n                if latitude[2] >= 60:\n                    raise dns.exception.SyntaxError('latitude seconds >= 60')\n                l = len(milliseconds)\n                if l == 0 or l > 3 or not milliseconds.isdigit():\n                    raise dns.exception.SyntaxError(\n                        'bad latitude milliseconds value')\n                if l == 1:\n                    m = 100\n                elif l == 2:\n                    m = 10\n                else:\n                    m = 1\n                latitude[3] = m * int(milliseconds)\n                t = tok.get_string()\n            elif t.isdigit():\n                latitude[2] = int(t)\n                t = tok.get_string()\n        if t == 'S':\n            latitude[4] = -1\n        elif t != 'N':\n            raise dns.exception.SyntaxError('bad latitude hemisphere value')\n\n        longitude[0] = tok.get_int()\n        t = tok.get_string()\n        if t.isdigit():\n            longitude[1] = int(t)\n            t = tok.get_string()\n            if '.' in t:\n                (seconds, milliseconds) = t.split('.')\n                if not seconds.isdigit():\n                    raise dns.exception.SyntaxError(\n                        'bad longitude seconds value')\n                longitude[2] = int(seconds)\n                if longitude[2] >= 60:\n                    raise dns.exception.SyntaxError('longitude seconds >= 60')\n                l = len(milliseconds)\n                if l == 0 or l > 3 or not milliseconds.isdigit():\n                    raise dns.exception.SyntaxError(\n                        'bad longitude milliseconds value')\n                if l == 1:\n                    m = 100\n                elif l == 2:\n                    m = 10\n                else:\n                    m = 1\n                longitude[3] = m * int(milliseconds)\n                t = tok.get_string()\n            elif t.isdigit():\n                longitude[2] = int(t)\n                t = tok.get_string()\n        if t == 'W':\n            longitude[4] = -1\n        elif t != 'E':\n            raise dns.exception.SyntaxError('bad longitude hemisphere value')\n\n        t = tok.get_string()\n        if t[-1] == 'm':\n            t = t[0: -1]\n        altitude = float(t) * 100.0        # m -> cm\n\n        token = tok.get().unescape()\n        if not token.is_eol_or_eof():\n            value = token.value\n            if value[-1] == 'm':\n                value = value[0: -1]\n            size = float(value) * 100.0        # m -> cm\n            token = tok.get().unescape()\n            if not token.is_eol_or_eof():\n                value = token.value\n                if value[-1] == 'm':\n                    value = value[0: -1]\n                hprec = float(value) * 100.0        # m -> cm\n                token = tok.get().unescape()\n                if not token.is_eol_or_eof():\n                    value = token.value\n                    if value[-1] == 'm':\n                        value = value[0: -1]\n                    vprec = float(value) * 100.0        # m -> cm\n                    tok.get_eol()\n\n        return cls(rdclass, rdtype, latitude, longitude, altitude,\n                   size, hprec, vprec)\n\n    def to_wire(self, file, compress=None, origin=None):\n        milliseconds = (self.latitude[0] * 3600000 +\n                        self.latitude[1] * 60000 +\n                        self.latitude[2] * 1000 +\n                        self.latitude[3]) * self.latitude[4]\n        latitude = long(0x80000000) + milliseconds\n        milliseconds = (self.longitude[0] * 3600000 +\n                        self.longitude[1] * 60000 +\n                        self.longitude[2] * 1000 +\n                        self.longitude[3]) * self.longitude[4]\n        longitude = long(0x80000000) + milliseconds\n        altitude = long(self.altitude) + long(10000000)\n        size = _encode_size(self.size, \"size\")\n        hprec = _encode_size(self.horizontal_precision, \"horizontal precision\")\n        vprec = _encode_size(self.vertical_precision, \"vertical precision\")\n        wire = struct.pack(\"!BBBBIII\", 0, size, hprec, vprec, latitude,\n                           longitude, altitude)\n        file.write(wire)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (version, size, hprec, vprec, latitude, longitude, altitude) = \\\n            struct.unpack(\"!BBBBIII\", wire[current: current + rdlen])\n        if latitude > long(0x80000000):\n            latitude = float(latitude - long(0x80000000)) / 3600000\n        else:\n            latitude = -1 * float(long(0x80000000) - latitude) / 3600000\n        if latitude < -90.0 or latitude > 90.0:\n            raise dns.exception.FormError(\"bad latitude\")\n        if longitude > long(0x80000000):\n            longitude = float(longitude - long(0x80000000)) / 3600000\n        else:\n            longitude = -1 * float(long(0x80000000) - longitude) / 3600000\n        if longitude < -180.0 or longitude > 180.0:\n            raise dns.exception.FormError(\"bad longitude\")\n        altitude = float(altitude) - 10000000.0\n        size = _decode_size(size, \"size\")\n        hprec = _decode_size(hprec, \"horizontal precision\")\n        vprec = _decode_size(vprec, \"vertical precision\")\n        return cls(rdclass, rdtype, latitude, longitude, altitude,\n                   size, hprec, vprec)\n\n    def _get_float_latitude(self):\n        return _tuple_to_float(self.latitude)\n\n    def _set_float_latitude(self, value):\n        self.latitude = _float_to_tuple(value)\n\n    float_latitude = property(_get_float_latitude, _set_float_latitude,\n                              doc=\"latitude as a floating point value\")\n\n    def _get_float_longitude(self):\n        return _tuple_to_float(self.longitude)\n\n    def _set_float_longitude(self, value):\n        self.longitude = _float_to_tuple(value)\n\n    float_longitude = property(_get_float_longitude, _set_float_longitude,\n                               doc=\"longitude as a floating point value\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/MX.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.mxbase\n\n\nclass MX(dns.rdtypes.mxbase.MXBase):\n\n    \"\"\"MX record\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/NS.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.nsbase\n\n\nclass NS(dns.rdtypes.nsbase.NSBase):\n\n    \"\"\"NS record\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/NSEC.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.rdatatype\nimport dns.name\nfrom dns._compat import xrange\n\n\nclass NSEC(dns.rdata.Rdata):\n\n    \"\"\"NSEC record\n\n    @ivar next: the next name\n    @type next: dns.name.Name object\n    @ivar windows: the windowed bitmap list\n    @type windows: list of (window number, string) tuples\"\"\"\n\n    __slots__ = ['next', 'windows']\n\n    def __init__(self, rdclass, rdtype, next, windows):\n        super(NSEC, self).__init__(rdclass, rdtype)\n        self.next = next\n        self.windows = windows\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        next = self.next.choose_relativity(origin, relativize)\n        text = ''\n        for (window, bitmap) in self.windows:\n            bits = []\n            for i in xrange(0, len(bitmap)):\n                byte = bitmap[i]\n                for j in xrange(0, 8):\n                    if byte & (0x80 >> j):\n                        bits.append(dns.rdatatype.to_text(window * 256 +\n                                                          i * 8 + j))\n            text += (' ' + ' '.join(bits))\n        return '{}{}'.format(next, text)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        next = tok.get_name()\n        next = next.choose_relativity(origin, relativize)\n        rdtypes = []\n        while 1:\n            token = tok.get().unescape()\n            if token.is_eol_or_eof():\n                break\n            nrdtype = dns.rdatatype.from_text(token.value)\n            if nrdtype == 0:\n                raise dns.exception.SyntaxError(\"NSEC with bit 0\")\n            if nrdtype > 65535:\n                raise dns.exception.SyntaxError(\"NSEC with bit > 65535\")\n            rdtypes.append(nrdtype)\n        rdtypes.sort()\n        window = 0\n        octets = 0\n        prior_rdtype = 0\n        bitmap = bytearray(b'\\0' * 32)\n        windows = []\n        for nrdtype in rdtypes:\n            if nrdtype == prior_rdtype:\n                continue\n            prior_rdtype = nrdtype\n            new_window = nrdtype // 256\n            if new_window != window:\n                windows.append((window, bitmap[0:octets]))\n                bitmap = bytearray(b'\\0' * 32)\n                window = new_window\n            offset = nrdtype % 256\n            byte = offset // 8\n            bit = offset % 8\n            octets = byte + 1\n            bitmap[byte] = bitmap[byte] | (0x80 >> bit)\n\n        windows.append((window, bitmap[0:octets]))\n        return cls(rdclass, rdtype, next, windows)\n\n    def to_wire(self, file, compress=None, origin=None):\n        self.next.to_wire(file, None, origin)\n        for (window, bitmap) in self.windows:\n            file.write(struct.pack('!BB', window, len(bitmap)))\n            file.write(bitmap)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (next, cused) = dns.name.from_wire(wire[: current + rdlen], current)\n        current += cused\n        rdlen -= cused\n        windows = []\n        while rdlen > 0:\n            if rdlen < 3:\n                raise dns.exception.FormError(\"NSEC too short\")\n            window = wire[current]\n            octets = wire[current + 1]\n            if octets == 0 or octets > 32:\n                raise dns.exception.FormError(\"bad NSEC octets\")\n            current += 2\n            rdlen -= 2\n            if rdlen < octets:\n                raise dns.exception.FormError(\"bad NSEC bitmap length\")\n            bitmap = bytearray(wire[current: current + octets].unwrap())\n            current += octets\n            rdlen -= octets\n            windows.append((window, bitmap))\n        if origin is not None:\n            next = next.relativize(origin)\n        return cls(rdclass, rdtype, next, windows)\n\n    def choose_relativity(self, origin=None, relativize=True):\n        self.next = self.next.choose_relativity(origin, relativize)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/NSEC3.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2004-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport base64\nimport binascii\nimport string\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.rdatatype\nfrom dns._compat import xrange, text_type, PY3\n\n# pylint: disable=deprecated-string-function\nif PY3:\n    b32_hex_to_normal = bytes.maketrans(b'0123456789ABCDEFGHIJKLMNOPQRSTUV',\n                                        b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')\n    b32_normal_to_hex = bytes.maketrans(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',\n                                        b'0123456789ABCDEFGHIJKLMNOPQRSTUV')\nelse:\n    b32_hex_to_normal = string.maketrans('0123456789ABCDEFGHIJKLMNOPQRSTUV',\n                                         'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')\n    b32_normal_to_hex = string.maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',\n                                         '0123456789ABCDEFGHIJKLMNOPQRSTUV')\n# pylint: enable=deprecated-string-function\n\n\n# hash algorithm constants\nSHA1 = 1\n\n# flag constants\nOPTOUT = 1\n\n\nclass NSEC3(dns.rdata.Rdata):\n\n    \"\"\"NSEC3 record\n\n    @ivar algorithm: the hash algorithm number\n    @type algorithm: int\n    @ivar flags: the flags\n    @type flags: int\n    @ivar iterations: the number of iterations\n    @type iterations: int\n    @ivar salt: the salt\n    @type salt: string\n    @ivar next: the next name hash\n    @type next: string\n    @ivar windows: the windowed bitmap list\n    @type windows: list of (window number, string) tuples\"\"\"\n\n    __slots__ = ['algorithm', 'flags', 'iterations', 'salt', 'next', 'windows']\n\n    def __init__(self, rdclass, rdtype, algorithm, flags, iterations, salt,\n                 next, windows):\n        super(NSEC3, self).__init__(rdclass, rdtype)\n        self.algorithm = algorithm\n        self.flags = flags\n        self.iterations = iterations\n        if isinstance(salt, text_type):\n            self.salt = salt.encode()\n        else:\n            self.salt = salt\n        self.next = next\n        self.windows = windows\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        next = base64.b32encode(self.next).translate(\n            b32_normal_to_hex).lower().decode()\n        if self.salt == b'':\n            salt = '-'\n        else:\n            salt = binascii.hexlify(self.salt).decode()\n        text = u''\n        for (window, bitmap) in self.windows:\n            bits = []\n            for i in xrange(0, len(bitmap)):\n                byte = bitmap[i]\n                for j in xrange(0, 8):\n                    if byte & (0x80 >> j):\n                        bits.append(dns.rdatatype.to_text(window * 256 +\n                                                          i * 8 + j))\n            text += (u' ' + u' '.join(bits))\n        return u'%u %u %u %s %s%s' % (self.algorithm, self.flags,\n                                      self.iterations, salt, next, text)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        algorithm = tok.get_uint8()\n        flags = tok.get_uint8()\n        iterations = tok.get_uint16()\n        salt = tok.get_string()\n        if salt == u'-':\n            salt = b''\n        else:\n            salt = binascii.unhexlify(salt.encode('ascii'))\n        next = tok.get_string().encode(\n            'ascii').upper().translate(b32_hex_to_normal)\n        next = base64.b32decode(next)\n        rdtypes = []\n        while 1:\n            token = tok.get().unescape()\n            if token.is_eol_or_eof():\n                break\n            nrdtype = dns.rdatatype.from_text(token.value)\n            if nrdtype == 0:\n                raise dns.exception.SyntaxError(\"NSEC3 with bit 0\")\n            if nrdtype > 65535:\n                raise dns.exception.SyntaxError(\"NSEC3 with bit > 65535\")\n            rdtypes.append(nrdtype)\n        rdtypes.sort()\n        window = 0\n        octets = 0\n        prior_rdtype = 0\n        bitmap = bytearray(b'\\0' * 32)\n        windows = []\n        for nrdtype in rdtypes:\n            if nrdtype == prior_rdtype:\n                continue\n            prior_rdtype = nrdtype\n            new_window = nrdtype // 256\n            if new_window != window:\n                if octets != 0:\n                    windows.append((window, bitmap[0:octets]))\n                bitmap = bytearray(b'\\0' * 32)\n                window = new_window\n            offset = nrdtype % 256\n            byte = offset // 8\n            bit = offset % 8\n            octets = byte + 1\n            bitmap[byte] = bitmap[byte] | (0x80 >> bit)\n        if octets != 0:\n            windows.append((window, bitmap[0:octets]))\n        return cls(rdclass, rdtype, algorithm, flags, iterations, salt, next,\n                   windows)\n\n    def to_wire(self, file, compress=None, origin=None):\n        l = len(self.salt)\n        file.write(struct.pack(\"!BBHB\", self.algorithm, self.flags,\n                               self.iterations, l))\n        file.write(self.salt)\n        l = len(self.next)\n        file.write(struct.pack(\"!B\", l))\n        file.write(self.next)\n        for (window, bitmap) in self.windows:\n            file.write(struct.pack(\"!BB\", window, len(bitmap)))\n            file.write(bitmap)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (algorithm, flags, iterations, slen) = \\\n            struct.unpack('!BBHB', wire[current: current + 5])\n\n        current += 5\n        rdlen -= 5\n        salt = wire[current: current + slen].unwrap()\n        current += slen\n        rdlen -= slen\n        nlen = wire[current]\n        current += 1\n        rdlen -= 1\n        next = wire[current: current + nlen].unwrap()\n        current += nlen\n        rdlen -= nlen\n        windows = []\n        while rdlen > 0:\n            if rdlen < 3:\n                raise dns.exception.FormError(\"NSEC3 too short\")\n            window = wire[current]\n            octets = wire[current + 1]\n            if octets == 0 or octets > 32:\n                raise dns.exception.FormError(\"bad NSEC3 octets\")\n            current += 2\n            rdlen -= 2\n            if rdlen < octets:\n                raise dns.exception.FormError(\"bad NSEC3 bitmap length\")\n            bitmap = bytearray(wire[current: current + octets].unwrap())\n            current += octets\n            rdlen -= octets\n            windows.append((window, bitmap))\n        return cls(rdclass, rdtype, algorithm, flags, iterations, salt, next,\n                   windows)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/NSEC3PARAM.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\nimport binascii\n\nimport dns.exception\nimport dns.rdata\nfrom dns._compat import text_type\n\n\nclass NSEC3PARAM(dns.rdata.Rdata):\n\n    \"\"\"NSEC3PARAM record\n\n    @ivar algorithm: the hash algorithm number\n    @type algorithm: int\n    @ivar flags: the flags\n    @type flags: int\n    @ivar iterations: the number of iterations\n    @type iterations: int\n    @ivar salt: the salt\n    @type salt: string\"\"\"\n\n    __slots__ = ['algorithm', 'flags', 'iterations', 'salt']\n\n    def __init__(self, rdclass, rdtype, algorithm, flags, iterations, salt):\n        super(NSEC3PARAM, self).__init__(rdclass, rdtype)\n        self.algorithm = algorithm\n        self.flags = flags\n        self.iterations = iterations\n        if isinstance(salt, text_type):\n            self.salt = salt.encode()\n        else:\n            self.salt = salt\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        if self.salt == b'':\n            salt = '-'\n        else:\n            salt = binascii.hexlify(self.salt).decode()\n        return '%u %u %u %s' % (self.algorithm, self.flags, self.iterations,\n                                salt)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        algorithm = tok.get_uint8()\n        flags = tok.get_uint8()\n        iterations = tok.get_uint16()\n        salt = tok.get_string()\n        if salt == '-':\n            salt = ''\n        else:\n            salt = binascii.unhexlify(salt.encode())\n        tok.get_eol()\n        return cls(rdclass, rdtype, algorithm, flags, iterations, salt)\n\n    def to_wire(self, file, compress=None, origin=None):\n        l = len(self.salt)\n        file.write(struct.pack(\"!BBHB\", self.algorithm, self.flags,\n                               self.iterations, l))\n        file.write(self.salt)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (algorithm, flags, iterations, slen) = \\\n             struct.unpack('!BBHB',\n                           wire[current: current + 5])\n        current += 5\n        rdlen -= 5\n        salt = wire[current: current + slen].unwrap()\n        current += slen\n        rdlen -= slen\n        if rdlen != 0:\n            raise dns.exception.FormError\n        return cls(rdclass, rdtype, algorithm, flags, iterations, salt)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/OPENPGPKEY.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2016 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport base64\n\nimport dns.exception\nimport dns.rdata\nimport dns.tokenizer\n\nclass OPENPGPKEY(dns.rdata.Rdata):\n\n    \"\"\"OPENPGPKEY record\n\n    @ivar key: the key\n    @type key: bytes\n    @see: RFC 7929\n    \"\"\"\n\n    def __init__(self, rdclass, rdtype, key):\n        super(OPENPGPKEY, self).__init__(rdclass, rdtype)\n        self.key = key\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return dns.rdata._base64ify(self.key)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        chunks = []\n        while 1:\n            t = tok.get().unescape()\n            if t.is_eol_or_eof():\n                break\n            if not t.is_identifier():\n                raise dns.exception.SyntaxError\n            chunks.append(t.value.encode())\n        b64 = b''.join(chunks)\n        key = base64.b64decode(b64)\n        return cls(rdclass, rdtype, key)\n\n    def to_wire(self, file, compress=None, origin=None):\n        file.write(self.key)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        key = wire[current: current + rdlen].unwrap()\n        return cls(rdclass, rdtype, key)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/PTR.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.nsbase\n\n\nclass PTR(dns.rdtypes.nsbase.NSBase):\n\n    \"\"\"PTR record\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/RP.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.exception\nimport dns.rdata\nimport dns.name\n\n\nclass RP(dns.rdata.Rdata):\n\n    \"\"\"RP record\n\n    @ivar mbox: The responsible person's mailbox\n    @type mbox: dns.name.Name object\n    @ivar txt: The owner name of a node with TXT records, or the root name\n    if no TXT records are associated with this RP.\n    @type txt: dns.name.Name object\n    @see: RFC 1183\"\"\"\n\n    __slots__ = ['mbox', 'txt']\n\n    def __init__(self, rdclass, rdtype, mbox, txt):\n        super(RP, self).__init__(rdclass, rdtype)\n        self.mbox = mbox\n        self.txt = txt\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        mbox = self.mbox.choose_relativity(origin, relativize)\n        txt = self.txt.choose_relativity(origin, relativize)\n        return \"{} {}\".format(str(mbox), str(txt))\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        mbox = tok.get_name()\n        txt = tok.get_name()\n        mbox = mbox.choose_relativity(origin, relativize)\n        txt = txt.choose_relativity(origin, relativize)\n        tok.get_eol()\n        return cls(rdclass, rdtype, mbox, txt)\n\n    def to_wire(self, file, compress=None, origin=None):\n        self.mbox.to_wire(file, None, origin)\n        self.txt.to_wire(file, None, origin)\n\n    def to_digestable(self, origin=None):\n        return self.mbox.to_digestable(origin) + \\\n            self.txt.to_digestable(origin)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (mbox, cused) = dns.name.from_wire(wire[: current + rdlen],\n                                           current)\n        current += cused\n        rdlen -= cused\n        if rdlen <= 0:\n            raise dns.exception.FormError\n        (txt, cused) = dns.name.from_wire(wire[: current + rdlen],\n                                          current)\n        if cused != rdlen:\n            raise dns.exception.FormError\n        if origin is not None:\n            mbox = mbox.relativize(origin)\n            txt = txt.relativize(origin)\n        return cls(rdclass, rdtype, mbox, txt)\n\n    def choose_relativity(self, origin=None, relativize=True):\n        self.mbox = self.mbox.choose_relativity(origin, relativize)\n        self.txt = self.txt.choose_relativity(origin, relativize)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/RRSIG.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport base64\nimport calendar\nimport struct\nimport time\n\nimport dns.dnssec\nimport dns.exception\nimport dns.rdata\nimport dns.rdatatype\n\n\nclass BadSigTime(dns.exception.DNSException):\n\n    \"\"\"Time in DNS SIG or RRSIG resource record cannot be parsed.\"\"\"\n\n\ndef sigtime_to_posixtime(what):\n    if len(what) != 14:\n        raise BadSigTime\n    year = int(what[0:4])\n    month = int(what[4:6])\n    day = int(what[6:8])\n    hour = int(what[8:10])\n    minute = int(what[10:12])\n    second = int(what[12:14])\n    return calendar.timegm((year, month, day, hour, minute, second,\n                            0, 0, 0))\n\n\ndef posixtime_to_sigtime(what):\n    return time.strftime('%Y%m%d%H%M%S', time.gmtime(what))\n\n\nclass RRSIG(dns.rdata.Rdata):\n\n    \"\"\"RRSIG record\n\n    @ivar type_covered: the rdata type this signature covers\n    @type type_covered: int\n    @ivar algorithm: the algorithm used for the sig\n    @type algorithm: int\n    @ivar labels: number of labels\n    @type labels: int\n    @ivar original_ttl: the original TTL\n    @type original_ttl: long\n    @ivar expiration: signature expiration time\n    @type expiration: long\n    @ivar inception: signature inception time\n    @type inception: long\n    @ivar key_tag: the key tag\n    @type key_tag: int\n    @ivar signer: the signer\n    @type signer: dns.name.Name object\n    @ivar signature: the signature\n    @type signature: string\"\"\"\n\n    __slots__ = ['type_covered', 'algorithm', 'labels', 'original_ttl',\n                 'expiration', 'inception', 'key_tag', 'signer',\n                 'signature']\n\n    def __init__(self, rdclass, rdtype, type_covered, algorithm, labels,\n                 original_ttl, expiration, inception, key_tag, signer,\n                 signature):\n        super(RRSIG, self).__init__(rdclass, rdtype)\n        self.type_covered = type_covered\n        self.algorithm = algorithm\n        self.labels = labels\n        self.original_ttl = original_ttl\n        self.expiration = expiration\n        self.inception = inception\n        self.key_tag = key_tag\n        self.signer = signer\n        self.signature = signature\n\n    def covers(self):\n        return self.type_covered\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return '%s %d %d %d %s %s %d %s %s' % (\n            dns.rdatatype.to_text(self.type_covered),\n            self.algorithm,\n            self.labels,\n            self.original_ttl,\n            posixtime_to_sigtime(self.expiration),\n            posixtime_to_sigtime(self.inception),\n            self.key_tag,\n            self.signer.choose_relativity(origin, relativize),\n            dns.rdata._base64ify(self.signature)\n        )\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        type_covered = dns.rdatatype.from_text(tok.get_string())\n        algorithm = dns.dnssec.algorithm_from_text(tok.get_string())\n        labels = tok.get_int()\n        original_ttl = tok.get_ttl()\n        expiration = sigtime_to_posixtime(tok.get_string())\n        inception = sigtime_to_posixtime(tok.get_string())\n        key_tag = tok.get_int()\n        signer = tok.get_name()\n        signer = signer.choose_relativity(origin, relativize)\n        chunks = []\n        while 1:\n            t = tok.get().unescape()\n            if t.is_eol_or_eof():\n                break\n            if not t.is_identifier():\n                raise dns.exception.SyntaxError\n            chunks.append(t.value.encode())\n        b64 = b''.join(chunks)\n        signature = base64.b64decode(b64)\n        return cls(rdclass, rdtype, type_covered, algorithm, labels,\n                   original_ttl, expiration, inception, key_tag, signer,\n                   signature)\n\n    def to_wire(self, file, compress=None, origin=None):\n        header = struct.pack('!HBBIIIH', self.type_covered,\n                             self.algorithm, self.labels,\n                             self.original_ttl, self.expiration,\n                             self.inception, self.key_tag)\n        file.write(header)\n        self.signer.to_wire(file, None, origin)\n        file.write(self.signature)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        header = struct.unpack('!HBBIIIH', wire[current: current + 18])\n        current += 18\n        rdlen -= 18\n        (signer, cused) = dns.name.from_wire(wire[: current + rdlen], current)\n        current += cused\n        rdlen -= cused\n        if origin is not None:\n            signer = signer.relativize(origin)\n        signature = wire[current: current + rdlen].unwrap()\n        return cls(rdclass, rdtype, header[0], header[1], header[2],\n                   header[3], header[4], header[5], header[6], signer,\n                   signature)\n\n    def choose_relativity(self, origin=None, relativize=True):\n        self.signer = self.signer.choose_relativity(origin, relativize)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/RT.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.mxbase\n\n\nclass RT(dns.rdtypes.mxbase.UncompressedDowncasingMX):\n\n    \"\"\"RT record\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/SOA.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.name\n\n\nclass SOA(dns.rdata.Rdata):\n\n    \"\"\"SOA record\n\n    @ivar mname: the SOA MNAME (master name) field\n    @type mname: dns.name.Name object\n    @ivar rname: the SOA RNAME (responsible name) field\n    @type rname: dns.name.Name object\n    @ivar serial: The zone's serial number\n    @type serial: int\n    @ivar refresh: The zone's refresh value (in seconds)\n    @type refresh: int\n    @ivar retry: The zone's retry value (in seconds)\n    @type retry: int\n    @ivar expire: The zone's expiration value (in seconds)\n    @type expire: int\n    @ivar minimum: The zone's negative caching time (in seconds, called\n    \"minimum\" for historical reasons)\n    @type minimum: int\n    @see: RFC 1035\"\"\"\n\n    __slots__ = ['mname', 'rname', 'serial', 'refresh', 'retry', 'expire',\n                 'minimum']\n\n    def __init__(self, rdclass, rdtype, mname, rname, serial, refresh, retry,\n                 expire, minimum):\n        super(SOA, self).__init__(rdclass, rdtype)\n        self.mname = mname\n        self.rname = rname\n        self.serial = serial\n        self.refresh = refresh\n        self.retry = retry\n        self.expire = expire\n        self.minimum = minimum\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        mname = self.mname.choose_relativity(origin, relativize)\n        rname = self.rname.choose_relativity(origin, relativize)\n        return '%s %s %d %d %d %d %d' % (\n            mname, rname, self.serial, self.refresh, self.retry,\n            self.expire, self.minimum)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        mname = tok.get_name()\n        rname = tok.get_name()\n        mname = mname.choose_relativity(origin, relativize)\n        rname = rname.choose_relativity(origin, relativize)\n        serial = tok.get_uint32()\n        refresh = tok.get_ttl()\n        retry = tok.get_ttl()\n        expire = tok.get_ttl()\n        minimum = tok.get_ttl()\n        tok.get_eol()\n        return cls(rdclass, rdtype, mname, rname, serial, refresh, retry,\n                   expire, minimum)\n\n    def to_wire(self, file, compress=None, origin=None):\n        self.mname.to_wire(file, compress, origin)\n        self.rname.to_wire(file, compress, origin)\n        five_ints = struct.pack('!IIIII', self.serial, self.refresh,\n                                self.retry, self.expire, self.minimum)\n        file.write(five_ints)\n\n    def to_digestable(self, origin=None):\n        return self.mname.to_digestable(origin) + \\\n            self.rname.to_digestable(origin) + \\\n            struct.pack('!IIIII', self.serial, self.refresh,\n                        self.retry, self.expire, self.minimum)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (mname, cused) = dns.name.from_wire(wire[: current + rdlen], current)\n        current += cused\n        rdlen -= cused\n        (rname, cused) = dns.name.from_wire(wire[: current + rdlen], current)\n        current += cused\n        rdlen -= cused\n        if rdlen != 20:\n            raise dns.exception.FormError\n        five_ints = struct.unpack('!IIIII',\n                                  wire[current: current + rdlen])\n        if origin is not None:\n            mname = mname.relativize(origin)\n            rname = rname.relativize(origin)\n        return cls(rdclass, rdtype, mname, rname,\n                   five_ints[0], five_ints[1], five_ints[2], five_ints[3],\n                   five_ints[4])\n\n    def choose_relativity(self, origin=None, relativize=True):\n        self.mname = self.mname.choose_relativity(origin, relativize)\n        self.rname = self.rname.choose_relativity(origin, relativize)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/SPF.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.txtbase\n\n\nclass SPF(dns.rdtypes.txtbase.TXTBase):\n\n    \"\"\"SPF record\n\n    @see: RFC 4408\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/SSHFP.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2005-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\nimport binascii\n\nimport dns.rdata\nimport dns.rdatatype\n\n\nclass SSHFP(dns.rdata.Rdata):\n\n    \"\"\"SSHFP record\n\n    @ivar algorithm: the algorithm\n    @type algorithm: int\n    @ivar fp_type: the digest type\n    @type fp_type: int\n    @ivar fingerprint: the fingerprint\n    @type fingerprint: string\n    @see: draft-ietf-secsh-dns-05.txt\"\"\"\n\n    __slots__ = ['algorithm', 'fp_type', 'fingerprint']\n\n    def __init__(self, rdclass, rdtype, algorithm, fp_type,\n                 fingerprint):\n        super(SSHFP, self).__init__(rdclass, rdtype)\n        self.algorithm = algorithm\n        self.fp_type = fp_type\n        self.fingerprint = fingerprint\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return '%d %d %s' % (self.algorithm,\n                             self.fp_type,\n                             dns.rdata._hexify(self.fingerprint,\n                                               chunksize=128))\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        algorithm = tok.get_uint8()\n        fp_type = tok.get_uint8()\n        chunks = []\n        while 1:\n            t = tok.get().unescape()\n            if t.is_eol_or_eof():\n                break\n            if not t.is_identifier():\n                raise dns.exception.SyntaxError\n            chunks.append(t.value.encode())\n        fingerprint = b''.join(chunks)\n        fingerprint = binascii.unhexlify(fingerprint)\n        return cls(rdclass, rdtype, algorithm, fp_type, fingerprint)\n\n    def to_wire(self, file, compress=None, origin=None):\n        header = struct.pack(\"!BB\", self.algorithm, self.fp_type)\n        file.write(header)\n        file.write(self.fingerprint)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        header = struct.unpack(\"!BB\", wire[current: current + 2])\n        current += 2\n        rdlen -= 2\n        fingerprint = wire[current: current + rdlen].unwrap()\n        return cls(rdclass, rdtype, header[0], header[1], fingerprint)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/TLSA.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2005-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\nimport binascii\n\nimport dns.rdata\nimport dns.rdatatype\n\n\nclass TLSA(dns.rdata.Rdata):\n\n    \"\"\"TLSA record\n\n    @ivar usage: The certificate usage\n    @type usage: int\n    @ivar selector: The selector field\n    @type selector: int\n    @ivar mtype: The 'matching type' field\n    @type mtype: int\n    @ivar cert: The 'Certificate Association Data' field\n    @type cert: string\n    @see: RFC 6698\"\"\"\n\n    __slots__ = ['usage', 'selector', 'mtype', 'cert']\n\n    def __init__(self, rdclass, rdtype, usage, selector,\n                 mtype, cert):\n        super(TLSA, self).__init__(rdclass, rdtype)\n        self.usage = usage\n        self.selector = selector\n        self.mtype = mtype\n        self.cert = cert\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return '%d %d %d %s' % (self.usage,\n                                self.selector,\n                                self.mtype,\n                                dns.rdata._hexify(self.cert,\n                                                  chunksize=128))\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        usage = tok.get_uint8()\n        selector = tok.get_uint8()\n        mtype = tok.get_uint8()\n        cert_chunks = []\n        while 1:\n            t = tok.get().unescape()\n            if t.is_eol_or_eof():\n                break\n            if not t.is_identifier():\n                raise dns.exception.SyntaxError\n            cert_chunks.append(t.value.encode())\n        cert = b''.join(cert_chunks)\n        cert = binascii.unhexlify(cert)\n        return cls(rdclass, rdtype, usage, selector, mtype, cert)\n\n    def to_wire(self, file, compress=None, origin=None):\n        header = struct.pack(\"!BBB\", self.usage, self.selector, self.mtype)\n        file.write(header)\n        file.write(self.cert)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        header = struct.unpack(\"!BBB\", wire[current: current + 3])\n        current += 3\n        rdlen -= 3\n        cert = wire[current: current + rdlen].unwrap()\n        return cls(rdclass, rdtype, header[0], header[1], header[2], cert)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/TXT.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.txtbase\n\n\nclass TXT(dns.rdtypes.txtbase.TXTBase):\n\n    \"\"\"TXT record\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/URI.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n# Copyright (C) 2015 Red Hat, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.name\nfrom dns._compat import text_type\n\n\nclass URI(dns.rdata.Rdata):\n\n    \"\"\"URI record\n\n    @ivar priority: the priority\n    @type priority: int\n    @ivar weight: the weight\n    @type weight: int\n    @ivar target: the target host\n    @type target: dns.name.Name object\n    @see: draft-faltstrom-uri-13\"\"\"\n\n    __slots__ = ['priority', 'weight', 'target']\n\n    def __init__(self, rdclass, rdtype, priority, weight, target):\n        super(URI, self).__init__(rdclass, rdtype)\n        self.priority = priority\n        self.weight = weight\n        if len(target) < 1:\n            raise dns.exception.SyntaxError(\"URI target cannot be empty\")\n        if isinstance(target, text_type):\n            self.target = target.encode()\n        else:\n            self.target = target\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return '%d %d \"%s\"' % (self.priority, self.weight,\n                               self.target.decode())\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        priority = tok.get_uint16()\n        weight = tok.get_uint16()\n        target = tok.get().unescape()\n        if not (target.is_quoted_string() or target.is_identifier()):\n            raise dns.exception.SyntaxError(\"URI target must be a string\")\n        tok.get_eol()\n        return cls(rdclass, rdtype, priority, weight, target.value)\n\n    def to_wire(self, file, compress=None, origin=None):\n        two_ints = struct.pack(\"!HH\", self.priority, self.weight)\n        file.write(two_ints)\n        file.write(self.target)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        if rdlen < 5:\n            raise dns.exception.FormError('URI RR is shorter than 5 octets')\n\n        (priority, weight) = struct.unpack('!HH', wire[current: current + 4])\n        current += 4\n        rdlen -= 4\n        target = wire[current: current + rdlen]\n        current += rdlen\n\n        return cls(rdclass, rdtype, priority, weight, target)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/X25.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.tokenizer\nfrom dns._compat import text_type\n\n\nclass X25(dns.rdata.Rdata):\n\n    \"\"\"X25 record\n\n    @ivar address: the PSDN address\n    @type address: string\n    @see: RFC 1183\"\"\"\n\n    __slots__ = ['address']\n\n    def __init__(self, rdclass, rdtype, address):\n        super(X25, self).__init__(rdclass, rdtype)\n        if isinstance(address, text_type):\n            self.address = address.encode()\n        else:\n            self.address = address\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return '\"%s\"' % dns.rdata._escapify(self.address)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        address = tok.get_string()\n        tok.get_eol()\n        return cls(rdclass, rdtype, address)\n\n    def to_wire(self, file, compress=None, origin=None):\n        l = len(self.address)\n        assert l < 256\n        file.write(struct.pack('!B', l))\n        file.write(self.address)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        l = wire[current]\n        current += 1\n        rdlen -= 1\n        if l != rdlen:\n            raise dns.exception.FormError\n        address = wire[current: current + l].unwrap()\n        return cls(rdclass, rdtype, address)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/ANY/__init__.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"Class ANY (generic) rdata type classes.\"\"\"\n\n__all__ = [\n    'AFSDB',\n    'AVC',\n    'CAA',\n    'CDNSKEY',\n    'CDS',\n    'CERT',\n    'CNAME',\n    'CSYNC',\n    'DLV',\n    'DNAME',\n    'DNSKEY',\n    'DS',\n    'EUI48',\n    'EUI64',\n    'GPOS',\n    'HINFO',\n    'HIP',\n    'ISDN',\n    'LOC',\n    'MX',\n    'NS',\n    'NSEC',\n    'NSEC3',\n    'NSEC3PARAM',\n    'OPENPGPKEY',\n    'PTR',\n    'RP',\n    'RRSIG',\n    'RT',\n    'SOA',\n    'SPF',\n    'SSHFP',\n    'TLSA',\n    'TXT',\n    'URI',\n    'X25',\n]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/CH/A.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.mxbase\nimport struct\n\nclass A(dns.rdtypes.mxbase.MXBase):\n\n    \"\"\"A record for Chaosnet\n    @ivar domain: the domain of the address\n    @type domain: dns.name.Name object\n    @ivar address: the 16-bit address\n    @type address: int\"\"\"\n\n    __slots__ = ['domain', 'address']\n\n    def __init__(self, rdclass, rdtype, address, domain):\n        super(A, self).__init__(rdclass, rdtype, address, domain)\n        self.domain = domain\n        self.address = address\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        domain = self.domain.choose_relativity(origin, relativize)\n        return '%s %o' % (domain, self.address)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        domain = tok.get_name()\n        address = tok.get_uint16(base=8)\n        domain = domain.choose_relativity(origin, relativize)\n        tok.get_eol()\n        return cls(rdclass, rdtype, address, domain)\n\n    def to_wire(self, file, compress=None, origin=None):\n        self.domain.to_wire(file, compress, origin)\n        pref = struct.pack(\"!H\", self.address)\n        file.write(pref)\n\n    def to_digestable(self, origin=None):\n        return self.domain.to_digestable(origin) + \\\n            struct.pack(\"!H\", self.address)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (domain, cused) = dns.name.from_wire(wire[: current + rdlen-2],\n                                               current)\n        current += cused\n        (address,) = struct.unpack('!H', wire[current: current + 2])\n        if cused+2 != rdlen:\n            raise dns.exception.FormError\n        if origin is not None:\n            domain = domain.relativize(origin)\n        return cls(rdclass, rdtype, address, domain)\n\n    def choose_relativity(self, origin=None, relativize=True):\n        self.domain = self.domain.choose_relativity(origin, relativize)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/CH/__init__.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"Class CH rdata type classes.\"\"\"\n\n__all__ = [\n    'A',\n]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/A.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.exception\nimport dns.ipv4\nimport dns.rdata\nimport dns.tokenizer\n\n\nclass A(dns.rdata.Rdata):\n\n    \"\"\"A record.\n\n    @ivar address: an IPv4 address\n    @type address: string (in the standard \"dotted quad\" format)\"\"\"\n\n    __slots__ = ['address']\n\n    def __init__(self, rdclass, rdtype, address):\n        super(A, self).__init__(rdclass, rdtype)\n        # check that it's OK\n        dns.ipv4.inet_aton(address)\n        self.address = address\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return self.address\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        address = tok.get_identifier()\n        tok.get_eol()\n        return cls(rdclass, rdtype, address)\n\n    def to_wire(self, file, compress=None, origin=None):\n        file.write(dns.ipv4.inet_aton(self.address))\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        address = dns.ipv4.inet_ntoa(wire[current: current + rdlen])\n        return cls(rdclass, rdtype, address)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/AAAA.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.exception\nimport dns.inet\nimport dns.rdata\nimport dns.tokenizer\n\n\nclass AAAA(dns.rdata.Rdata):\n\n    \"\"\"AAAA record.\n\n    @ivar address: an IPv6 address\n    @type address: string (in the standard IPv6 format)\"\"\"\n\n    __slots__ = ['address']\n\n    def __init__(self, rdclass, rdtype, address):\n        super(AAAA, self).__init__(rdclass, rdtype)\n        # check that it's OK\n        dns.inet.inet_pton(dns.inet.AF_INET6, address)\n        self.address = address\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return self.address\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        address = tok.get_identifier()\n        tok.get_eol()\n        return cls(rdclass, rdtype, address)\n\n    def to_wire(self, file, compress=None, origin=None):\n        file.write(dns.inet.inet_pton(dns.inet.AF_INET6, self.address))\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        address = dns.inet.inet_ntop(dns.inet.AF_INET6,\n                                     wire[current: current + rdlen])\n        return cls(rdclass, rdtype, address)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/APL.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport binascii\nimport codecs\nimport struct\n\nimport dns.exception\nimport dns.inet\nimport dns.rdata\nimport dns.tokenizer\nfrom dns._compat import xrange, maybe_chr\n\n\nclass APLItem(object):\n\n    \"\"\"An APL list item.\n\n    @ivar family: the address family (IANA address family registry)\n    @type family: int\n    @ivar negation: is this item negated?\n    @type negation: bool\n    @ivar address: the address\n    @type address: string\n    @ivar prefix: the prefix length\n    @type prefix: int\n    \"\"\"\n\n    __slots__ = ['family', 'negation', 'address', 'prefix']\n\n    def __init__(self, family, negation, address, prefix):\n        self.family = family\n        self.negation = negation\n        self.address = address\n        self.prefix = prefix\n\n    def __str__(self):\n        if self.negation:\n            return \"!%d:%s/%s\" % (self.family, self.address, self.prefix)\n        else:\n            return \"%d:%s/%s\" % (self.family, self.address, self.prefix)\n\n    def to_wire(self, file):\n        if self.family == 1:\n            address = dns.inet.inet_pton(dns.inet.AF_INET, self.address)\n        elif self.family == 2:\n            address = dns.inet.inet_pton(dns.inet.AF_INET6, self.address)\n        else:\n            address = binascii.unhexlify(self.address)\n        #\n        # Truncate least significant zero bytes.\n        #\n        last = 0\n        for i in xrange(len(address) - 1, -1, -1):\n            if address[i] != maybe_chr(0):\n                last = i + 1\n                break\n        address = address[0: last]\n        l = len(address)\n        assert l < 128\n        if self.negation:\n            l |= 0x80\n        header = struct.pack('!HBB', self.family, self.prefix, l)\n        file.write(header)\n        file.write(address)\n\n\nclass APL(dns.rdata.Rdata):\n\n    \"\"\"APL record.\n\n    @ivar items: a list of APL items\n    @type items: list of APL_Item\n    @see: RFC 3123\"\"\"\n\n    __slots__ = ['items']\n\n    def __init__(self, rdclass, rdtype, items):\n        super(APL, self).__init__(rdclass, rdtype)\n        self.items = items\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return ' '.join(map(str, self.items))\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        items = []\n        while 1:\n            token = tok.get().unescape()\n            if token.is_eol_or_eof():\n                break\n            item = token.value\n            if item[0] == '!':\n                negation = True\n                item = item[1:]\n            else:\n                negation = False\n            (family, rest) = item.split(':', 1)\n            family = int(family)\n            (address, prefix) = rest.split('/', 1)\n            prefix = int(prefix)\n            item = APLItem(family, negation, address, prefix)\n            items.append(item)\n\n        return cls(rdclass, rdtype, items)\n\n    def to_wire(self, file, compress=None, origin=None):\n        for item in self.items:\n            item.to_wire(file)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n\n        items = []\n        while 1:\n            if rdlen == 0:\n                break\n            if rdlen < 4:\n                raise dns.exception.FormError\n            header = struct.unpack('!HBB', wire[current: current + 4])\n            afdlen = header[2]\n            if afdlen > 127:\n                negation = True\n                afdlen -= 128\n            else:\n                negation = False\n            current += 4\n            rdlen -= 4\n            if rdlen < afdlen:\n                raise dns.exception.FormError\n            address = wire[current: current + afdlen].unwrap()\n            l = len(address)\n            if header[0] == 1:\n                if l < 4:\n                    address += b'\\x00' * (4 - l)\n                address = dns.inet.inet_ntop(dns.inet.AF_INET, address)\n            elif header[0] == 2:\n                if l < 16:\n                    address += b'\\x00' * (16 - l)\n                address = dns.inet.inet_ntop(dns.inet.AF_INET6, address)\n            else:\n                #\n                # This isn't really right according to the RFC, but it\n                # seems better than throwing an exception\n                #\n                address = codecs.encode(address, 'hex_codec')\n            current += afdlen\n            rdlen -= afdlen\n            item = APLItem(header[0], negation, address, header[1])\n            items.append(item)\n        return cls(rdclass, rdtype, items)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/DHCID.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport base64\n\nimport dns.exception\n\n\nclass DHCID(dns.rdata.Rdata):\n\n    \"\"\"DHCID record\n\n    @ivar data: the data (the content of the RR is opaque as far as the\n    DNS is concerned)\n    @type data: string\n    @see: RFC 4701\"\"\"\n\n    __slots__ = ['data']\n\n    def __init__(self, rdclass, rdtype, data):\n        super(DHCID, self).__init__(rdclass, rdtype)\n        self.data = data\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return dns.rdata._base64ify(self.data)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        chunks = []\n        while 1:\n            t = tok.get().unescape()\n            if t.is_eol_or_eof():\n                break\n            if not t.is_identifier():\n                raise dns.exception.SyntaxError\n            chunks.append(t.value.encode())\n        b64 = b''.join(chunks)\n        data = base64.b64decode(b64)\n        return cls(rdclass, rdtype, data)\n\n    def to_wire(self, file, compress=None, origin=None):\n        file.write(self.data)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        data = wire[current: current + rdlen].unwrap()\n        return cls(rdclass, rdtype, data)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/IPSECKEY.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\nimport base64\n\nimport dns.exception\nimport dns.inet\nimport dns.name\n\n\nclass IPSECKEY(dns.rdata.Rdata):\n\n    \"\"\"IPSECKEY record\n\n    @ivar precedence: the precedence for this key data\n    @type precedence: int\n    @ivar gateway_type: the gateway type\n    @type gateway_type: int\n    @ivar algorithm: the algorithm to use\n    @type algorithm: int\n    @ivar gateway: the public key\n    @type gateway: None, IPv4 address, IPV6 address, or domain name\n    @ivar key: the public key\n    @type key: string\n    @see: RFC 4025\"\"\"\n\n    __slots__ = ['precedence', 'gateway_type', 'algorithm', 'gateway', 'key']\n\n    def __init__(self, rdclass, rdtype, precedence, gateway_type, algorithm,\n                 gateway, key):\n        super(IPSECKEY, self).__init__(rdclass, rdtype)\n        if gateway_type == 0:\n            if gateway != '.' and gateway is not None:\n                raise SyntaxError('invalid gateway for gateway type 0')\n            gateway = None\n        elif gateway_type == 1:\n            # check that it's OK\n            dns.inet.inet_pton(dns.inet.AF_INET, gateway)\n        elif gateway_type == 2:\n            # check that it's OK\n            dns.inet.inet_pton(dns.inet.AF_INET6, gateway)\n        elif gateway_type == 3:\n            pass\n        else:\n            raise SyntaxError(\n                'invalid IPSECKEY gateway type: %d' % gateway_type)\n        self.precedence = precedence\n        self.gateway_type = gateway_type\n        self.algorithm = algorithm\n        self.gateway = gateway\n        self.key = key\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        if self.gateway_type == 0:\n            gateway = '.'\n        elif self.gateway_type == 1:\n            gateway = self.gateway\n        elif self.gateway_type == 2:\n            gateway = self.gateway\n        elif self.gateway_type == 3:\n            gateway = str(self.gateway.choose_relativity(origin, relativize))\n        else:\n            raise ValueError('invalid gateway type')\n        return '%d %d %d %s %s' % (self.precedence, self.gateway_type,\n                                   self.algorithm, gateway,\n                                   dns.rdata._base64ify(self.key))\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        precedence = tok.get_uint8()\n        gateway_type = tok.get_uint8()\n        algorithm = tok.get_uint8()\n        if gateway_type == 3:\n            gateway = tok.get_name().choose_relativity(origin, relativize)\n        else:\n            gateway = tok.get_string()\n        chunks = []\n        while 1:\n            t = tok.get().unescape()\n            if t.is_eol_or_eof():\n                break\n            if not t.is_identifier():\n                raise dns.exception.SyntaxError\n            chunks.append(t.value.encode())\n        b64 = b''.join(chunks)\n        key = base64.b64decode(b64)\n        return cls(rdclass, rdtype, precedence, gateway_type, algorithm,\n                   gateway, key)\n\n    def to_wire(self, file, compress=None, origin=None):\n        header = struct.pack(\"!BBB\", self.precedence, self.gateway_type,\n                             self.algorithm)\n        file.write(header)\n        if self.gateway_type == 0:\n            pass\n        elif self.gateway_type == 1:\n            file.write(dns.inet.inet_pton(dns.inet.AF_INET, self.gateway))\n        elif self.gateway_type == 2:\n            file.write(dns.inet.inet_pton(dns.inet.AF_INET6, self.gateway))\n        elif self.gateway_type == 3:\n            self.gateway.to_wire(file, None, origin)\n        else:\n            raise ValueError('invalid gateway type')\n        file.write(self.key)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        if rdlen < 3:\n            raise dns.exception.FormError\n        header = struct.unpack('!BBB', wire[current: current + 3])\n        gateway_type = header[1]\n        current += 3\n        rdlen -= 3\n        if gateway_type == 0:\n            gateway = None\n        elif gateway_type == 1:\n            gateway = dns.inet.inet_ntop(dns.inet.AF_INET,\n                                         wire[current: current + 4])\n            current += 4\n            rdlen -= 4\n        elif gateway_type == 2:\n            gateway = dns.inet.inet_ntop(dns.inet.AF_INET6,\n                                         wire[current: current + 16])\n            current += 16\n            rdlen -= 16\n        elif gateway_type == 3:\n            (gateway, cused) = dns.name.from_wire(wire[: current + rdlen],\n                                                  current)\n            current += cused\n            rdlen -= cused\n        else:\n            raise dns.exception.FormError('invalid IPSECKEY gateway type')\n        key = wire[current: current + rdlen].unwrap()\n        return cls(rdclass, rdtype, header[0], gateway_type, header[2],\n                   gateway, key)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/KX.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.mxbase\n\n\nclass KX(dns.rdtypes.mxbase.UncompressedMX):\n\n    \"\"\"KX record\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/NAPTR.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\n\nimport dns.exception\nimport dns.name\nimport dns.rdata\nfrom dns._compat import xrange, text_type\n\n\ndef _write_string(file, s):\n    l = len(s)\n    assert l < 256\n    file.write(struct.pack('!B', l))\n    file.write(s)\n\n\ndef _sanitize(value):\n    if isinstance(value, text_type):\n        return value.encode()\n    return value\n\n\nclass NAPTR(dns.rdata.Rdata):\n\n    \"\"\"NAPTR record\n\n    @ivar order: order\n    @type order: int\n    @ivar preference: preference\n    @type preference: int\n    @ivar flags: flags\n    @type flags: string\n    @ivar service: service\n    @type service: string\n    @ivar regexp: regular expression\n    @type regexp: string\n    @ivar replacement: replacement name\n    @type replacement: dns.name.Name object\n    @see: RFC 3403\"\"\"\n\n    __slots__ = ['order', 'preference', 'flags', 'service', 'regexp',\n                 'replacement']\n\n    def __init__(self, rdclass, rdtype, order, preference, flags, service,\n                 regexp, replacement):\n        super(NAPTR, self).__init__(rdclass, rdtype)\n        self.flags = _sanitize(flags)\n        self.service = _sanitize(service)\n        self.regexp = _sanitize(regexp)\n        self.order = order\n        self.preference = preference\n        self.replacement = replacement\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        replacement = self.replacement.choose_relativity(origin, relativize)\n        return '%d %d \"%s\" \"%s\" \"%s\" %s' % \\\n               (self.order, self.preference,\n                dns.rdata._escapify(self.flags),\n                dns.rdata._escapify(self.service),\n                dns.rdata._escapify(self.regexp),\n                replacement)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        order = tok.get_uint16()\n        preference = tok.get_uint16()\n        flags = tok.get_string()\n        service = tok.get_string()\n        regexp = tok.get_string()\n        replacement = tok.get_name()\n        replacement = replacement.choose_relativity(origin, relativize)\n        tok.get_eol()\n        return cls(rdclass, rdtype, order, preference, flags, service,\n                   regexp, replacement)\n\n    def to_wire(self, file, compress=None, origin=None):\n        two_ints = struct.pack(\"!HH\", self.order, self.preference)\n        file.write(two_ints)\n        _write_string(file, self.flags)\n        _write_string(file, self.service)\n        _write_string(file, self.regexp)\n        self.replacement.to_wire(file, compress, origin)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (order, preference) = struct.unpack('!HH', wire[current: current + 4])\n        current += 4\n        rdlen -= 4\n        strings = []\n        for i in xrange(3):\n            l = wire[current]\n            current += 1\n            rdlen -= 1\n            if l > rdlen or rdlen < 0:\n                raise dns.exception.FormError\n            s = wire[current: current + l].unwrap()\n            current += l\n            rdlen -= l\n            strings.append(s)\n        (replacement, cused) = dns.name.from_wire(wire[: current + rdlen],\n                                                  current)\n        if cused != rdlen:\n            raise dns.exception.FormError\n        if origin is not None:\n            replacement = replacement.relativize(origin)\n        return cls(rdclass, rdtype, order, preference, strings[0], strings[1],\n                   strings[2], replacement)\n\n    def choose_relativity(self, origin=None, relativize=True):\n        self.replacement = self.replacement.choose_relativity(origin,\n                                                              relativize)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/NSAP.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport binascii\n\nimport dns.exception\nimport dns.rdata\nimport dns.tokenizer\n\n\nclass NSAP(dns.rdata.Rdata):\n\n    \"\"\"NSAP record.\n\n    @ivar address: a NASP\n    @type address: string\n    @see: RFC 1706\"\"\"\n\n    __slots__ = ['address']\n\n    def __init__(self, rdclass, rdtype, address):\n        super(NSAP, self).__init__(rdclass, rdtype)\n        self.address = address\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return \"0x%s\" % binascii.hexlify(self.address).decode()\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        address = tok.get_string()\n        tok.get_eol()\n        if address[0:2] != '0x':\n            raise dns.exception.SyntaxError('string does not start with 0x')\n        address = address[2:].replace('.', '')\n        if len(address) % 2 != 0:\n            raise dns.exception.SyntaxError('hexstring has odd length')\n        address = binascii.unhexlify(address.encode())\n        return cls(rdclass, rdtype, address)\n\n    def to_wire(self, file, compress=None, origin=None):\n        file.write(self.address)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        address = wire[current: current + rdlen].unwrap()\n        return cls(rdclass, rdtype, address)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/NSAP_PTR.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport dns.rdtypes.nsbase\n\n\nclass NSAP_PTR(dns.rdtypes.nsbase.UncompressedNS):\n\n    \"\"\"NSAP-PTR record\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/PX.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.name\n\n\nclass PX(dns.rdata.Rdata):\n\n    \"\"\"PX record.\n\n    @ivar preference: the preference value\n    @type preference: int\n    @ivar map822: the map822 name\n    @type map822: dns.name.Name object\n    @ivar mapx400: the mapx400 name\n    @type mapx400: dns.name.Name object\n    @see: RFC 2163\"\"\"\n\n    __slots__ = ['preference', 'map822', 'mapx400']\n\n    def __init__(self, rdclass, rdtype, preference, map822, mapx400):\n        super(PX, self).__init__(rdclass, rdtype)\n        self.preference = preference\n        self.map822 = map822\n        self.mapx400 = mapx400\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        map822 = self.map822.choose_relativity(origin, relativize)\n        mapx400 = self.mapx400.choose_relativity(origin, relativize)\n        return '%d %s %s' % (self.preference, map822, mapx400)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        preference = tok.get_uint16()\n        map822 = tok.get_name()\n        map822 = map822.choose_relativity(origin, relativize)\n        mapx400 = tok.get_name(None)\n        mapx400 = mapx400.choose_relativity(origin, relativize)\n        tok.get_eol()\n        return cls(rdclass, rdtype, preference, map822, mapx400)\n\n    def to_wire(self, file, compress=None, origin=None):\n        pref = struct.pack(\"!H\", self.preference)\n        file.write(pref)\n        self.map822.to_wire(file, None, origin)\n        self.mapx400.to_wire(file, None, origin)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (preference, ) = struct.unpack('!H', wire[current: current + 2])\n        current += 2\n        rdlen -= 2\n        (map822, cused) = dns.name.from_wire(wire[: current + rdlen],\n                                             current)\n        if cused > rdlen:\n            raise dns.exception.FormError\n        current += cused\n        rdlen -= cused\n        if origin is not None:\n            map822 = map822.relativize(origin)\n        (mapx400, cused) = dns.name.from_wire(wire[: current + rdlen],\n                                              current)\n        if cused != rdlen:\n            raise dns.exception.FormError\n        if origin is not None:\n            mapx400 = mapx400.relativize(origin)\n        return cls(rdclass, rdtype, preference, map822, mapx400)\n\n    def choose_relativity(self, origin=None, relativize=True):\n        self.map822 = self.map822.choose_relativity(origin, relativize)\n        self.mapx400 = self.mapx400.choose_relativity(origin, relativize)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/SRV.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.name\n\n\nclass SRV(dns.rdata.Rdata):\n\n    \"\"\"SRV record\n\n    @ivar priority: the priority\n    @type priority: int\n    @ivar weight: the weight\n    @type weight: int\n    @ivar port: the port of the service\n    @type port: int\n    @ivar target: the target host\n    @type target: dns.name.Name object\n    @see: RFC 2782\"\"\"\n\n    __slots__ = ['priority', 'weight', 'port', 'target']\n\n    def __init__(self, rdclass, rdtype, priority, weight, port, target):\n        super(SRV, self).__init__(rdclass, rdtype)\n        self.priority = priority\n        self.weight = weight\n        self.port = port\n        self.target = target\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        target = self.target.choose_relativity(origin, relativize)\n        return '%d %d %d %s' % (self.priority, self.weight, self.port,\n                                target)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        priority = tok.get_uint16()\n        weight = tok.get_uint16()\n        port = tok.get_uint16()\n        target = tok.get_name(None)\n        target = target.choose_relativity(origin, relativize)\n        tok.get_eol()\n        return cls(rdclass, rdtype, priority, weight, port, target)\n\n    def to_wire(self, file, compress=None, origin=None):\n        three_ints = struct.pack(\"!HHH\", self.priority, self.weight, self.port)\n        file.write(three_ints)\n        self.target.to_wire(file, compress, origin)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (priority, weight, port) = struct.unpack('!HHH',\n                                                 wire[current: current + 6])\n        current += 6\n        rdlen -= 6\n        (target, cused) = dns.name.from_wire(wire[: current + rdlen],\n                                             current)\n        if cused != rdlen:\n            raise dns.exception.FormError\n        if origin is not None:\n            target = target.relativize(origin)\n        return cls(rdclass, rdtype, priority, weight, port, target)\n\n    def choose_relativity(self, origin=None, relativize=True):\n        self.target = self.target.choose_relativity(origin, relativize)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/WKS.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport socket\nimport struct\n\nimport dns.ipv4\nimport dns.rdata\nfrom dns._compat import xrange\n\n_proto_tcp = socket.getprotobyname('tcp')\n_proto_udp = socket.getprotobyname('udp')\n\n\nclass WKS(dns.rdata.Rdata):\n\n    \"\"\"WKS record\n\n    @ivar address: the address\n    @type address: string\n    @ivar protocol: the protocol\n    @type protocol: int\n    @ivar bitmap: the bitmap\n    @type bitmap: string\n    @see: RFC 1035\"\"\"\n\n    __slots__ = ['address', 'protocol', 'bitmap']\n\n    def __init__(self, rdclass, rdtype, address, protocol, bitmap):\n        super(WKS, self).__init__(rdclass, rdtype)\n        self.address = address\n        self.protocol = protocol\n        if not isinstance(bitmap, bytearray):\n            self.bitmap = bytearray(bitmap)\n        else:\n            self.bitmap = bitmap\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        bits = []\n        for i in xrange(0, len(self.bitmap)):\n            byte = self.bitmap[i]\n            for j in xrange(0, 8):\n                if byte & (0x80 >> j):\n                    bits.append(str(i * 8 + j))\n        text = ' '.join(bits)\n        return '%s %d %s' % (self.address, self.protocol, text)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        address = tok.get_string()\n        protocol = tok.get_string()\n        if protocol.isdigit():\n            protocol = int(protocol)\n        else:\n            protocol = socket.getprotobyname(protocol)\n        bitmap = bytearray()\n        while 1:\n            token = tok.get().unescape()\n            if token.is_eol_or_eof():\n                break\n            if token.value.isdigit():\n                serv = int(token.value)\n            else:\n                if protocol != _proto_udp and protocol != _proto_tcp:\n                    raise NotImplementedError(\"protocol must be TCP or UDP\")\n                if protocol == _proto_udp:\n                    protocol_text = \"udp\"\n                else:\n                    protocol_text = \"tcp\"\n                serv = socket.getservbyname(token.value, protocol_text)\n            i = serv // 8\n            l = len(bitmap)\n            if l < i + 1:\n                for j in xrange(l, i + 1):\n                    bitmap.append(0)\n            bitmap[i] = bitmap[i] | (0x80 >> (serv % 8))\n        bitmap = dns.rdata._truncate_bitmap(bitmap)\n        return cls(rdclass, rdtype, address, protocol, bitmap)\n\n    def to_wire(self, file, compress=None, origin=None):\n        file.write(dns.ipv4.inet_aton(self.address))\n        protocol = struct.pack('!B', self.protocol)\n        file.write(protocol)\n        file.write(self.bitmap)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        address = dns.ipv4.inet_ntoa(wire[current: current + 4])\n        protocol, = struct.unpack('!B', wire[current + 4: current + 5])\n        current += 5\n        rdlen -= 5\n        bitmap = wire[current: current + rdlen].unwrap()\n        return cls(rdclass, rdtype, address, protocol, bitmap)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/IN/__init__.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"Class IN rdata type classes.\"\"\"\n\n__all__ = [\n    'A',\n    'AAAA',\n    'APL',\n    'DHCID',\n    'IPSECKEY',\n    'KX',\n    'NAPTR',\n    'NSAP',\n    'NSAP_PTR',\n    'PX',\n    'SRV',\n    'WKS',\n]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/__init__.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS rdata type classes\"\"\"\n\n__all__ = [\n    'ANY',\n    'IN',\n    'CH',\n    'euibase',\n    'mxbase',\n    'nsbase',\n]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/dnskeybase.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport base64\nimport struct\n\nimport dns.exception\nimport dns.dnssec\nimport dns.rdata\n\n# wildcard import\n__all__ = [\"SEP\", \"REVOKE\", \"ZONE\",\n           \"flags_to_text_set\", \"flags_from_text_set\"]\n\n# flag constants\nSEP = 0x0001\nREVOKE = 0x0080\nZONE = 0x0100\n\n_flag_by_text = {\n    'SEP': SEP,\n    'REVOKE': REVOKE,\n    'ZONE': ZONE\n}\n\n# We construct the inverse mapping programmatically to ensure that we\n# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that\n# would cause the mapping not to be true inverse.\n_flag_by_value = {y: x for x, y in _flag_by_text.items()}\n\n\ndef flags_to_text_set(flags):\n    \"\"\"Convert a DNSKEY flags value to set texts\n    @rtype: set([string])\"\"\"\n\n    flags_set = set()\n    mask = 0x1\n    while mask <= 0x8000:\n        if flags & mask:\n            text = _flag_by_value.get(mask)\n            if not text:\n                text = hex(mask)\n            flags_set.add(text)\n        mask <<= 1\n    return flags_set\n\n\ndef flags_from_text_set(texts_set):\n    \"\"\"Convert set of DNSKEY flag mnemonic texts to DNSKEY flag value\n    @rtype: int\"\"\"\n\n    flags = 0\n    for text in texts_set:\n        try:\n            flags += _flag_by_text[text]\n        except KeyError:\n            raise NotImplementedError(\n                \"DNSKEY flag '%s' is not supported\" % text)\n    return flags\n\n\nclass DNSKEYBase(dns.rdata.Rdata):\n\n    \"\"\"Base class for rdata that is like a DNSKEY record\n\n    @ivar flags: the key flags\n    @type flags: int\n    @ivar protocol: the protocol for which this key may be used\n    @type protocol: int\n    @ivar algorithm: the algorithm used for the key\n    @type algorithm: int\n    @ivar key: the public key\n    @type key: string\"\"\"\n\n    __slots__ = ['flags', 'protocol', 'algorithm', 'key']\n\n    def __init__(self, rdclass, rdtype, flags, protocol, algorithm, key):\n        super(DNSKEYBase, self).__init__(rdclass, rdtype)\n        self.flags = flags\n        self.protocol = protocol\n        self.algorithm = algorithm\n        self.key = key\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return '%d %d %d %s' % (self.flags, self.protocol, self.algorithm,\n                                dns.rdata._base64ify(self.key))\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        flags = tok.get_uint16()\n        protocol = tok.get_uint8()\n        algorithm = dns.dnssec.algorithm_from_text(tok.get_string())\n        chunks = []\n        while 1:\n            t = tok.get().unescape()\n            if t.is_eol_or_eof():\n                break\n            if not t.is_identifier():\n                raise dns.exception.SyntaxError\n            chunks.append(t.value.encode())\n        b64 = b''.join(chunks)\n        key = base64.b64decode(b64)\n        return cls(rdclass, rdtype, flags, protocol, algorithm, key)\n\n    def to_wire(self, file, compress=None, origin=None):\n        header = struct.pack(\"!HBB\", self.flags, self.protocol, self.algorithm)\n        file.write(header)\n        file.write(self.key)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        if rdlen < 4:\n            raise dns.exception.FormError\n        header = struct.unpack('!HBB', wire[current: current + 4])\n        current += 4\n        rdlen -= 4\n        key = wire[current: current + rdlen].unwrap()\n        return cls(rdclass, rdtype, header[0], header[1], header[2],\n                   key)\n\n    def flags_to_text_set(self):\n        \"\"\"Convert a DNSKEY flags value to set texts\n        @rtype: set([string])\"\"\"\n        return flags_to_text_set(self.flags)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/dsbase.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2010, 2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport struct\nimport binascii\n\nimport dns.rdata\nimport dns.rdatatype\n\n\nclass DSBase(dns.rdata.Rdata):\n\n    \"\"\"Base class for rdata that is like a DS record\n\n    @ivar key_tag: the key tag\n    @type key_tag: int\n    @ivar algorithm: the algorithm\n    @type algorithm: int\n    @ivar digest_type: the digest type\n    @type digest_type: int\n    @ivar digest: the digest\n    @type digest: int\n    @see: draft-ietf-dnsext-delegation-signer-14.txt\"\"\"\n\n    __slots__ = ['key_tag', 'algorithm', 'digest_type', 'digest']\n\n    def __init__(self, rdclass, rdtype, key_tag, algorithm, digest_type,\n                 digest):\n        super(DSBase, self).__init__(rdclass, rdtype)\n        self.key_tag = key_tag\n        self.algorithm = algorithm\n        self.digest_type = digest_type\n        self.digest = digest\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return '%d %d %d %s' % (self.key_tag, self.algorithm,\n                                self.digest_type,\n                                dns.rdata._hexify(self.digest,\n                                                  chunksize=128))\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        key_tag = tok.get_uint16()\n        algorithm = tok.get_uint8()\n        digest_type = tok.get_uint8()\n        chunks = []\n        while 1:\n            t = tok.get().unescape()\n            if t.is_eol_or_eof():\n                break\n            if not t.is_identifier():\n                raise dns.exception.SyntaxError\n            chunks.append(t.value.encode())\n        digest = b''.join(chunks)\n        digest = binascii.unhexlify(digest)\n        return cls(rdclass, rdtype, key_tag, algorithm, digest_type,\n                   digest)\n\n    def to_wire(self, file, compress=None, origin=None):\n        header = struct.pack(\"!HBB\", self.key_tag, self.algorithm,\n                             self.digest_type)\n        file.write(header)\n        file.write(self.digest)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        header = struct.unpack(\"!HBB\", wire[current: current + 4])\n        current += 4\n        rdlen -= 4\n        digest = wire[current: current + rdlen].unwrap()\n        return cls(rdclass, rdtype, header[0], header[1], header[2], digest)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/euibase.py",
    "content": "# Copyright (C) 2015 Red Hat, Inc.\n# Author: Petr Spacek <pspacek@redhat.com>\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nimport binascii\n\nimport dns.rdata\nfrom dns._compat import xrange\n\n\nclass EUIBase(dns.rdata.Rdata):\n\n    \"\"\"EUIxx record\n\n    @ivar fingerprint: xx-bit Extended Unique Identifier (EUI-xx)\n    @type fingerprint: string\n    @see: rfc7043.txt\"\"\"\n\n    __slots__ = ['eui']\n    # define these in subclasses\n    # byte_len = 6  # 0123456789ab (in hex)\n    # text_len = byte_len * 3 - 1  # 01-23-45-67-89-ab\n\n    def __init__(self, rdclass, rdtype, eui):\n        super(EUIBase, self).__init__(rdclass, rdtype)\n        if len(eui) != self.byte_len:\n            raise dns.exception.FormError('EUI%s rdata has to have %s bytes'\n                                          % (self.byte_len * 8, self.byte_len))\n        self.eui = eui\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        return dns.rdata._hexify(self.eui, chunksize=2).replace(' ', '-')\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        text = tok.get_string()\n        tok.get_eol()\n        if len(text) != cls.text_len:\n            raise dns.exception.SyntaxError(\n                'Input text must have %s characters' % cls.text_len)\n        expected_dash_idxs = xrange(2, cls.byte_len * 3 - 1, 3)\n        for i in expected_dash_idxs:\n            if text[i] != '-':\n                raise dns.exception.SyntaxError('Dash expected at position %s'\n                                                % i)\n        text = text.replace('-', '')\n        try:\n            data = binascii.unhexlify(text.encode())\n        except (ValueError, TypeError) as ex:\n            raise dns.exception.SyntaxError('Hex decoding error: %s' % str(ex))\n        return cls(rdclass, rdtype, data)\n\n    def to_wire(self, file, compress=None, origin=None):\n        file.write(self.eui)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        eui = wire[current:current + rdlen].unwrap()\n        return cls(rdclass, rdtype, eui)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/mxbase.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"MX-like base classes.\"\"\"\n\nfrom io import BytesIO\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.name\n\n\nclass MXBase(dns.rdata.Rdata):\n\n    \"\"\"Base class for rdata that is like an MX record.\n\n    @ivar preference: the preference value\n    @type preference: int\n    @ivar exchange: the exchange name\n    @type exchange: dns.name.Name object\"\"\"\n\n    __slots__ = ['preference', 'exchange']\n\n    def __init__(self, rdclass, rdtype, preference, exchange):\n        super(MXBase, self).__init__(rdclass, rdtype)\n        self.preference = preference\n        self.exchange = exchange\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        exchange = self.exchange.choose_relativity(origin, relativize)\n        return '%d %s' % (self.preference, exchange)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        preference = tok.get_uint16()\n        exchange = tok.get_name()\n        exchange = exchange.choose_relativity(origin, relativize)\n        tok.get_eol()\n        return cls(rdclass, rdtype, preference, exchange)\n\n    def to_wire(self, file, compress=None, origin=None):\n        pref = struct.pack(\"!H\", self.preference)\n        file.write(pref)\n        self.exchange.to_wire(file, compress, origin)\n\n    def to_digestable(self, origin=None):\n        return struct.pack(\"!H\", self.preference) + \\\n            self.exchange.to_digestable(origin)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (preference, ) = struct.unpack('!H', wire[current: current + 2])\n        current += 2\n        rdlen -= 2\n        (exchange, cused) = dns.name.from_wire(wire[: current + rdlen],\n                                               current)\n        if cused != rdlen:\n            raise dns.exception.FormError\n        if origin is not None:\n            exchange = exchange.relativize(origin)\n        return cls(rdclass, rdtype, preference, exchange)\n\n    def choose_relativity(self, origin=None, relativize=True):\n        self.exchange = self.exchange.choose_relativity(origin, relativize)\n\n\nclass UncompressedMX(MXBase):\n\n    \"\"\"Base class for rdata that is like an MX record, but whose name\n    is not compressed when converted to DNS wire format, and whose\n    digestable form is not downcased.\"\"\"\n\n    def to_wire(self, file, compress=None, origin=None):\n        super(UncompressedMX, self).to_wire(file, None, origin)\n\n    def to_digestable(self, origin=None):\n        f = BytesIO()\n        self.to_wire(f, None, origin)\n        return f.getvalue()\n\n\nclass UncompressedDowncasingMX(MXBase):\n\n    \"\"\"Base class for rdata that is like an MX record, but whose name\n    is not compressed when convert to DNS wire format.\"\"\"\n\n    def to_wire(self, file, compress=None, origin=None):\n        super(UncompressedDowncasingMX, self).to_wire(file, None, origin)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/nsbase.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"NS-like base classes.\"\"\"\n\nfrom io import BytesIO\n\nimport dns.exception\nimport dns.rdata\nimport dns.name\n\n\nclass NSBase(dns.rdata.Rdata):\n\n    \"\"\"Base class for rdata that is like an NS record.\n\n    @ivar target: the target name of the rdata\n    @type target: dns.name.Name object\"\"\"\n\n    __slots__ = ['target']\n\n    def __init__(self, rdclass, rdtype, target):\n        super(NSBase, self).__init__(rdclass, rdtype)\n        self.target = target\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        target = self.target.choose_relativity(origin, relativize)\n        return str(target)\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        target = tok.get_name()\n        target = target.choose_relativity(origin, relativize)\n        tok.get_eol()\n        return cls(rdclass, rdtype, target)\n\n    def to_wire(self, file, compress=None, origin=None):\n        self.target.to_wire(file, compress, origin)\n\n    def to_digestable(self, origin=None):\n        return self.target.to_digestable(origin)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        (target, cused) = dns.name.from_wire(wire[: current + rdlen],\n                                             current)\n        if cused != rdlen:\n            raise dns.exception.FormError\n        if origin is not None:\n            target = target.relativize(origin)\n        return cls(rdclass, rdtype, target)\n\n    def choose_relativity(self, origin=None, relativize=True):\n        self.target = self.target.choose_relativity(origin, relativize)\n\n\nclass UncompressedNS(NSBase):\n\n    \"\"\"Base class for rdata that is like an NS record, but whose name\n    is not compressed when convert to DNS wire format, and whose\n    digestable form is not downcased.\"\"\"\n\n    def to_wire(self, file, compress=None, origin=None):\n        super(UncompressedNS, self).to_wire(file, None, origin)\n\n    def to_digestable(self, origin=None):\n        f = BytesIO()\n        self.to_wire(f, None, origin)\n        return f.getvalue()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rdtypes/txtbase.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2006-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"TXT-like base class.\"\"\"\n\nimport struct\n\nimport dns.exception\nimport dns.rdata\nimport dns.tokenizer\nfrom dns._compat import binary_type, string_types\n\n\nclass TXTBase(dns.rdata.Rdata):\n\n    \"\"\"Base class for rdata that is like a TXT record\n\n    @ivar strings: the strings\n    @type strings: list of binary\n    @see: RFC 1035\"\"\"\n\n    __slots__ = ['strings']\n\n    def __init__(self, rdclass, rdtype, strings):\n        super(TXTBase, self).__init__(rdclass, rdtype)\n        if isinstance(strings, binary_type) or \\\n           isinstance(strings, string_types):\n            strings = [strings]\n        self.strings = []\n        for string in strings:\n            if isinstance(string, string_types):\n                string = string.encode()\n            self.strings.append(string)\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        txt = ''\n        prefix = ''\n        for s in self.strings:\n            txt += '{}\"{}\"'.format(prefix, dns.rdata._escapify(s))\n            prefix = ' '\n        return txt\n\n    @classmethod\n    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):\n        strings = []\n        while 1:\n            token = tok.get().unescape()\n            if token.is_eol_or_eof():\n                break\n            if not (token.is_quoted_string() or token.is_identifier()):\n                raise dns.exception.SyntaxError(\"expected a string\")\n            if len(token.value) > 255:\n                raise dns.exception.SyntaxError(\"string too long\")\n            value = token.value\n            if isinstance(value, binary_type):\n                strings.append(value)\n            else:\n                strings.append(value.encode())\n        if len(strings) == 0:\n            raise dns.exception.UnexpectedEnd\n        return cls(rdclass, rdtype, strings)\n\n    def to_wire(self, file, compress=None, origin=None):\n        for s in self.strings:\n            l = len(s)\n            assert l < 256\n            file.write(struct.pack('!B', l))\n            file.write(s)\n\n    @classmethod\n    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):\n        strings = []\n        while rdlen > 0:\n            l = wire[current]\n            current += 1\n            rdlen -= 1\n            if l > rdlen:\n                raise dns.exception.FormError\n            s = wire[current: current + l].unwrap()\n            current += l\n            rdlen -= l\n            strings.append(s)\n        return cls(rdclass, rdtype, strings)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/renderer.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2001-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"Help for building DNS wire format messages\"\"\"\n\nfrom io import BytesIO\nimport struct\nimport random\nimport time\n\nimport dns.exception\nimport dns.tsig\nfrom ._compat import long\n\n\nQUESTION = 0\nANSWER = 1\nAUTHORITY = 2\nADDITIONAL = 3\n\n\nclass Renderer(object):\n    \"\"\"Helper class for building DNS wire-format messages.\n\n    Most applications can use the higher-level L{dns.message.Message}\n    class and its to_wire() method to generate wire-format messages.\n    This class is for those applications which need finer control\n    over the generation of messages.\n\n    Typical use::\n\n        r = dns.renderer.Renderer(id=1, flags=0x80, max_size=512)\n        r.add_question(qname, qtype, qclass)\n        r.add_rrset(dns.renderer.ANSWER, rrset_1)\n        r.add_rrset(dns.renderer.ANSWER, rrset_2)\n        r.add_rrset(dns.renderer.AUTHORITY, ns_rrset)\n        r.add_edns(0, 0, 4096)\n        r.add_rrset(dns.renderer.ADDTIONAL, ad_rrset_1)\n        r.add_rrset(dns.renderer.ADDTIONAL, ad_rrset_2)\n        r.write_header()\n        r.add_tsig(keyname, secret, 300, 1, 0, '', request_mac)\n        wire = r.get_wire()\n\n    output, a BytesIO, where rendering is written\n\n    id: the message id\n\n    flags: the message flags\n\n    max_size: the maximum size of the message\n\n    origin: the origin to use when rendering relative names\n\n    compress: the compression table\n\n    section: an int, the section currently being rendered\n\n    counts: list of the number of RRs in each section\n\n    mac: the MAC of the rendered message (if TSIG was used)\n    \"\"\"\n\n    def __init__(self, id=None, flags=0, max_size=65535, origin=None):\n        \"\"\"Initialize a new renderer.\"\"\"\n\n        self.output = BytesIO()\n        if id is None:\n            self.id = random.randint(0, 65535)\n        else:\n            self.id = id\n        self.flags = flags\n        self.max_size = max_size\n        self.origin = origin\n        self.compress = {}\n        self.section = QUESTION\n        self.counts = [0, 0, 0, 0]\n        self.output.write(b'\\x00' * 12)\n        self.mac = ''\n\n    def _rollback(self, where):\n        \"\"\"Truncate the output buffer at offset *where*, and remove any\n        compression table entries that pointed beyond the truncation\n        point.\n        \"\"\"\n\n        self.output.seek(where)\n        self.output.truncate()\n        keys_to_delete = []\n        for k, v in self.compress.items():\n            if v >= where:\n                keys_to_delete.append(k)\n        for k in keys_to_delete:\n            del self.compress[k]\n\n    def _set_section(self, section):\n        \"\"\"Set the renderer's current section.\n\n        Sections must be rendered order: QUESTION, ANSWER, AUTHORITY,\n        ADDITIONAL.  Sections may be empty.\n\n        Raises dns.exception.FormError if an attempt was made to set\n        a section value less than the current section.\n        \"\"\"\n\n        if self.section != section:\n            if self.section > section:\n                raise dns.exception.FormError\n            self.section = section\n\n    def add_question(self, qname, rdtype, rdclass=dns.rdataclass.IN):\n        \"\"\"Add a question to the message.\"\"\"\n\n        self._set_section(QUESTION)\n        before = self.output.tell()\n        qname.to_wire(self.output, self.compress, self.origin)\n        self.output.write(struct.pack(\"!HH\", rdtype, rdclass))\n        after = self.output.tell()\n        if after >= self.max_size:\n            self._rollback(before)\n            raise dns.exception.TooBig\n        self.counts[QUESTION] += 1\n\n    def add_rrset(self, section, rrset, **kw):\n        \"\"\"Add the rrset to the specified section.\n\n        Any keyword arguments are passed on to the rdataset's to_wire()\n        routine.\n        \"\"\"\n\n        self._set_section(section)\n        before = self.output.tell()\n        n = rrset.to_wire(self.output, self.compress, self.origin, **kw)\n        after = self.output.tell()\n        if after >= self.max_size:\n            self._rollback(before)\n            raise dns.exception.TooBig\n        self.counts[section] += n\n\n    def add_rdataset(self, section, name, rdataset, **kw):\n        \"\"\"Add the rdataset to the specified section, using the specified\n        name as the owner name.\n\n        Any keyword arguments are passed on to the rdataset's to_wire()\n        routine.\n        \"\"\"\n\n        self._set_section(section)\n        before = self.output.tell()\n        n = rdataset.to_wire(name, self.output, self.compress, self.origin,\n                             **kw)\n        after = self.output.tell()\n        if after >= self.max_size:\n            self._rollback(before)\n            raise dns.exception.TooBig\n        self.counts[section] += n\n\n    def add_edns(self, edns, ednsflags, payload, options=None):\n        \"\"\"Add an EDNS OPT record to the message.\"\"\"\n\n        # make sure the EDNS version in ednsflags agrees with edns\n        ednsflags &= long(0xFF00FFFF)\n        ednsflags |= (edns << 16)\n        self._set_section(ADDITIONAL)\n        before = self.output.tell()\n        self.output.write(struct.pack('!BHHIH', 0, dns.rdatatype.OPT, payload,\n                                      ednsflags, 0))\n        if options is not None:\n            lstart = self.output.tell()\n            for opt in options:\n                stuff = struct.pack(\"!HH\", opt.otype, 0)\n                self.output.write(stuff)\n                start = self.output.tell()\n                opt.to_wire(self.output)\n                end = self.output.tell()\n                assert end - start < 65536\n                self.output.seek(start - 2)\n                stuff = struct.pack(\"!H\", end - start)\n                self.output.write(stuff)\n                self.output.seek(0, 2)\n            lend = self.output.tell()\n            assert lend - lstart < 65536\n            self.output.seek(lstart - 2)\n            stuff = struct.pack(\"!H\", lend - lstart)\n            self.output.write(stuff)\n            self.output.seek(0, 2)\n        after = self.output.tell()\n        if after >= self.max_size:\n            self._rollback(before)\n            raise dns.exception.TooBig\n        self.counts[ADDITIONAL] += 1\n\n    def add_tsig(self, keyname, secret, fudge, id, tsig_error, other_data,\n                 request_mac, algorithm=dns.tsig.default_algorithm):\n        \"\"\"Add a TSIG signature to the message.\"\"\"\n\n        s = self.output.getvalue()\n        (tsig_rdata, self.mac, ctx) = dns.tsig.sign(s,\n                                                    keyname,\n                                                    secret,\n                                                    int(time.time()),\n                                                    fudge,\n                                                    id,\n                                                    tsig_error,\n                                                    other_data,\n                                                    request_mac,\n                                                    algorithm=algorithm)\n        self._write_tsig(tsig_rdata, keyname)\n\n    def add_multi_tsig(self, ctx, keyname, secret, fudge, id, tsig_error,\n                       other_data, request_mac,\n                       algorithm=dns.tsig.default_algorithm):\n        \"\"\"Add a TSIG signature to the message. Unlike add_tsig(), this can be\n        used for a series of consecutive DNS envelopes, e.g. for a zone\n        transfer over TCP [RFC2845, 4.4].\n\n        For the first message in the sequence, give ctx=None. For each\n        subsequent message, give the ctx that was returned from the\n        add_multi_tsig() call for the previous message.\"\"\"\n\n        s = self.output.getvalue()\n        (tsig_rdata, self.mac, ctx) = dns.tsig.sign(s,\n                                                    keyname,\n                                                    secret,\n                                                    int(time.time()),\n                                                    fudge,\n                                                    id,\n                                                    tsig_error,\n                                                    other_data,\n                                                    request_mac,\n                                                    ctx=ctx,\n                                                    first=ctx is None,\n                                                    multi=True,\n                                                    algorithm=algorithm)\n        self._write_tsig(tsig_rdata, keyname)\n        return ctx\n\n    def _write_tsig(self, tsig_rdata, keyname):\n        self._set_section(ADDITIONAL)\n        before = self.output.tell()\n\n        keyname.to_wire(self.output, self.compress, self.origin)\n        self.output.write(struct.pack('!HHIH', dns.rdatatype.TSIG,\n                                      dns.rdataclass.ANY, 0, 0))\n        rdata_start = self.output.tell()\n        self.output.write(tsig_rdata)\n\n        after = self.output.tell()\n        assert after - rdata_start < 65536\n        if after >= self.max_size:\n            self._rollback(before)\n            raise dns.exception.TooBig\n\n        self.output.seek(rdata_start - 2)\n        self.output.write(struct.pack('!H', after - rdata_start))\n        self.counts[ADDITIONAL] += 1\n        self.output.seek(10)\n        self.output.write(struct.pack('!H', self.counts[ADDITIONAL]))\n        self.output.seek(0, 2)\n\n    def write_header(self):\n        \"\"\"Write the DNS message header.\n\n        Writing the DNS message header is done after all sections\n        have been rendered, but before the optional TSIG signature\n        is added.\n        \"\"\"\n\n        self.output.seek(0)\n        self.output.write(struct.pack('!HHHHHH', self.id, self.flags,\n                                      self.counts[0], self.counts[1],\n                                      self.counts[2], self.counts[3]))\n        self.output.seek(0, 2)\n\n    def get_wire(self):\n        \"\"\"Return the wire format message.\"\"\"\n\n        return self.output.getvalue()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/resolver.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS stub resolver.\"\"\"\n\nimport socket\nimport sys\nimport time\nimport random\n\ntry:\n    import threading as _threading\nexcept ImportError:\n    import dummy_threading as _threading\n\nimport dns.exception\nimport dns.flags\nimport dns.ipv4\nimport dns.ipv6\nimport dns.message\nimport dns.name\nimport dns.query\nimport dns.rcode\nimport dns.rdataclass\nimport dns.rdatatype\nimport dns.reversename\nimport dns.tsig\nfrom ._compat import xrange, string_types\n\nif sys.platform == 'win32':\n    try:\n        import winreg as _winreg\n    except ImportError:\n        import _winreg  # pylint: disable=import-error\n\nclass NXDOMAIN(dns.exception.DNSException):\n    \"\"\"The DNS query name does not exist.\"\"\"\n    supp_kwargs = {'qnames', 'responses'}\n    fmt = None  # we have our own __str__ implementation\n\n    def _check_kwargs(self, qnames, responses=None):\n        if not isinstance(qnames, (list, tuple, set)):\n            raise AttributeError(\"qnames must be a list, tuple or set\")\n        if len(qnames) == 0:\n            raise AttributeError(\"qnames must contain at least one element\")\n        if responses is None:\n            responses = {}\n        elif not isinstance(responses, dict):\n            raise AttributeError(\"responses must be a dict(qname=response)\")\n        kwargs = dict(qnames=qnames, responses=responses)\n        return kwargs\n\n    def __str__(self):\n        if 'qnames' not in self.kwargs:\n            return super(NXDOMAIN, self).__str__()\n        qnames = self.kwargs['qnames']\n        if len(qnames) > 1:\n            msg = 'None of DNS query names exist'\n        else:\n            msg = 'The DNS query name does not exist'\n        qnames = ', '.join(map(str, qnames))\n        return \"{}: {}\".format(msg, qnames)\n\n    def canonical_name(self):\n        if not 'qnames' in self.kwargs:\n            raise TypeError(\"parametrized exception required\")\n        IN = dns.rdataclass.IN\n        CNAME = dns.rdatatype.CNAME\n        cname = None\n        for qname in self.kwargs['qnames']:\n            response = self.kwargs['responses'][qname]\n            for answer in response.answer:\n                if answer.rdtype != CNAME or answer.rdclass != IN:\n                    continue\n                cname = answer.items[0].target.to_text()\n            if cname is not None:\n                return dns.name.from_text(cname)\n        return self.kwargs['qnames'][0]\n    canonical_name = property(canonical_name, doc=(\n        \"Return the unresolved canonical name.\"))\n\n    def __add__(self, e_nx):\n        \"\"\"Augment by results from another NXDOMAIN exception.\"\"\"\n        qnames0 = list(self.kwargs.get('qnames', []))\n        responses0 = dict(self.kwargs.get('responses', {}))\n        responses1 = e_nx.kwargs.get('responses', {})\n        for qname1 in e_nx.kwargs.get('qnames', []):\n            if qname1 not in qnames0:\n                qnames0.append(qname1)\n            if qname1 in responses1:\n                responses0[qname1] = responses1[qname1]\n        return NXDOMAIN(qnames=qnames0, responses=responses0)\n\n    def qnames(self):\n        \"\"\"All of the names that were tried.\n\n        Returns a list of ``dns.name.Name``.\n        \"\"\"\n        return self.kwargs['qnames']\n\n    def responses(self):\n        \"\"\"A map from queried names to their NXDOMAIN responses.\n\n        Returns a dict mapping a ``dns.name.Name`` to a\n        ``dns.message.Message``.\n        \"\"\"\n        return self.kwargs['responses']\n\n    def response(self, qname):\n        \"\"\"The response for query *qname*.\n\n        Returns a ``dns.message.Message``.\n        \"\"\"\n        return self.kwargs['responses'][qname]\n\n\nclass YXDOMAIN(dns.exception.DNSException):\n    \"\"\"The DNS query name is too long after DNAME substitution.\"\"\"\n\n# The definition of the Timeout exception has moved from here to the\n# dns.exception module.  We keep dns.resolver.Timeout defined for\n# backwards compatibility.\n\nTimeout = dns.exception.Timeout\n\n\nclass NoAnswer(dns.exception.DNSException):\n    \"\"\"The DNS response does not contain an answer to the question.\"\"\"\n    fmt = 'The DNS response does not contain an answer ' + \\\n          'to the question: {query}'\n    supp_kwargs = {'response'}\n\n    def _fmt_kwargs(self, **kwargs):\n        return super(NoAnswer, self)._fmt_kwargs(\n            query=kwargs['response'].question)\n\n\nclass NoNameservers(dns.exception.DNSException):\n    \"\"\"All nameservers failed to answer the query.\n\n    errors: list of servers and respective errors\n    The type of errors is\n    [(server IP address, any object convertible to string)].\n    Non-empty errors list will add explanatory message ()\n    \"\"\"\n\n    msg = \"All nameservers failed to answer the query.\"\n    fmt = \"%s {query}: {errors}\" % msg[:-1]\n    supp_kwargs = {'request', 'errors'}\n\n    def _fmt_kwargs(self, **kwargs):\n        srv_msgs = []\n        for err in kwargs['errors']:\n            srv_msgs.append('Server {} {} port {} answered {}'.format(err[0],\n                            'TCP' if err[1] else 'UDP', err[2], err[3]))\n        return super(NoNameservers, self)._fmt_kwargs(\n            query=kwargs['request'].question, errors='; '.join(srv_msgs))\n\n\nclass NotAbsolute(dns.exception.DNSException):\n    \"\"\"An absolute domain name is required but a relative name was provided.\"\"\"\n\n\nclass NoRootSOA(dns.exception.DNSException):\n    \"\"\"There is no SOA RR at the DNS root name. This should never happen!\"\"\"\n\n\nclass NoMetaqueries(dns.exception.DNSException):\n    \"\"\"DNS metaqueries are not allowed.\"\"\"\n\n\nclass Answer(object):\n    \"\"\"DNS stub resolver answer.\n\n    Instances of this class bundle up the result of a successful DNS\n    resolution.\n\n    For convenience, the answer object implements much of the sequence\n    protocol, forwarding to its ``rrset`` attribute.  E.g.\n    ``for a in answer`` is equivalent to ``for a in answer.rrset``.\n    ``answer[i]`` is equivalent to ``answer.rrset[i]``, and\n    ``answer[i:j]`` is equivalent to ``answer.rrset[i:j]``.\n\n    Note that CNAMEs or DNAMEs in the response may mean that answer\n    RRset's name might not be the query name.\n    \"\"\"\n\n    def __init__(self, qname, rdtype, rdclass, response,\n                 raise_on_no_answer=True):\n        self.qname = qname\n        self.rdtype = rdtype\n        self.rdclass = rdclass\n        self.response = response\n        min_ttl = -1\n        rrset = None\n        for count in xrange(0, 15):\n            try:\n                rrset = response.find_rrset(response.answer, qname,\n                                            rdclass, rdtype)\n                if min_ttl == -1 or rrset.ttl < min_ttl:\n                    min_ttl = rrset.ttl\n                break\n            except KeyError:\n                if rdtype != dns.rdatatype.CNAME:\n                    try:\n                        crrset = response.find_rrset(response.answer,\n                                                     qname,\n                                                     rdclass,\n                                                     dns.rdatatype.CNAME)\n                        if min_ttl == -1 or crrset.ttl < min_ttl:\n                            min_ttl = crrset.ttl\n                        for rd in crrset:\n                            qname = rd.target\n                            break\n                        continue\n                    except KeyError:\n                        if raise_on_no_answer:\n                            raise NoAnswer(response=response)\n                if raise_on_no_answer:\n                    raise NoAnswer(response=response)\n        if rrset is None and raise_on_no_answer:\n            raise NoAnswer(response=response)\n        self.canonical_name = qname\n        self.rrset = rrset\n        if rrset is None:\n            while 1:\n                # Look for a SOA RR whose owner name is a superdomain\n                # of qname.\n                try:\n                    srrset = response.find_rrset(response.authority, qname,\n                                                 rdclass, dns.rdatatype.SOA)\n                    if min_ttl == -1 or srrset.ttl < min_ttl:\n                        min_ttl = srrset.ttl\n                    if srrset[0].minimum < min_ttl:\n                        min_ttl = srrset[0].minimum\n                    break\n                except KeyError:\n                    try:\n                        qname = qname.parent()\n                    except dns.name.NoParent:\n                        break\n        self.expiration = time.time() + min_ttl\n\n    def __getattr__(self, attr):\n        if attr == 'name':\n            return self.rrset.name\n        elif attr == 'ttl':\n            return self.rrset.ttl\n        elif attr == 'covers':\n            return self.rrset.covers\n        elif attr == 'rdclass':\n            return self.rrset.rdclass\n        elif attr == 'rdtype':\n            return self.rrset.rdtype\n        else:\n            raise AttributeError(attr)\n\n    def __len__(self):\n        return self.rrset and len(self.rrset) or 0\n\n    def __iter__(self):\n        return self.rrset and iter(self.rrset) or iter(tuple())\n\n    def __getitem__(self, i):\n        if self.rrset is None:\n            raise IndexError\n        return self.rrset[i]\n\n    def __delitem__(self, i):\n        if self.rrset is None:\n            raise IndexError\n        del self.rrset[i]\n\n\nclass Cache(object):\n    \"\"\"Simple thread-safe DNS answer cache.\"\"\"\n\n    def __init__(self, cleaning_interval=300.0):\n        \"\"\"*cleaning_interval*, a ``float`` is the number of seconds between\n        periodic cleanings.\n        \"\"\"\n\n        self.data = {}\n        self.cleaning_interval = cleaning_interval\n        self.next_cleaning = time.time() + self.cleaning_interval\n        self.lock = _threading.Lock()\n\n    def _maybe_clean(self):\n        \"\"\"Clean the cache if it's time to do so.\"\"\"\n\n        now = time.time()\n        if self.next_cleaning <= now:\n            keys_to_delete = []\n            for (k, v) in self.data.items():\n                if v.expiration <= now:\n                    keys_to_delete.append(k)\n            for k in keys_to_delete:\n                del self.data[k]\n            now = time.time()\n            self.next_cleaning = now + self.cleaning_interval\n\n    def get(self, key):\n        \"\"\"Get the answer associated with *key*.\n\n        Returns None if no answer is cached for the key.\n\n        *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the\n        query name, rdtype, and rdclass respectively.\n\n        Returns a ``dns.resolver.Answer`` or ``None``.\n        \"\"\"\n\n        try:\n            self.lock.acquire()\n            self._maybe_clean()\n            v = self.data.get(key)\n            if v is None or v.expiration <= time.time():\n                return None\n            return v\n        finally:\n            self.lock.release()\n\n    def put(self, key, value):\n        \"\"\"Associate key and value in the cache.\n\n        *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the\n        query name, rdtype, and rdclass respectively.\n\n        *value*, a ``dns.resolver.Answer``, the answer.\n        \"\"\"\n\n        try:\n            self.lock.acquire()\n            self._maybe_clean()\n            self.data[key] = value\n        finally:\n            self.lock.release()\n\n    def flush(self, key=None):\n        \"\"\"Flush the cache.\n\n        If *key* is not ``None``, only that item is flushed.  Otherwise\n        the entire cache is flushed.\n\n        *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the\n        query name, rdtype, and rdclass respectively.\n        \"\"\"\n\n        try:\n            self.lock.acquire()\n            if key is not None:\n                if key in self.data:\n                    del self.data[key]\n            else:\n                self.data = {}\n                self.next_cleaning = time.time() + self.cleaning_interval\n        finally:\n            self.lock.release()\n\n\nclass LRUCacheNode(object):\n    \"\"\"LRUCache node.\"\"\"\n\n    def __init__(self, key, value):\n        self.key = key\n        self.value = value\n        self.prev = self\n        self.next = self\n\n    def link_before(self, node):\n        self.prev = node.prev\n        self.next = node\n        node.prev.next = self\n        node.prev = self\n\n    def link_after(self, node):\n        self.prev = node\n        self.next = node.next\n        node.next.prev = self\n        node.next = self\n\n    def unlink(self):\n        self.next.prev = self.prev\n        self.prev.next = self.next\n\n\nclass LRUCache(object):\n    \"\"\"Thread-safe, bounded, least-recently-used DNS answer cache.\n\n    This cache is better than the simple cache (above) if you're\n    running a web crawler or other process that does a lot of\n    resolutions.  The LRUCache has a maximum number of nodes, and when\n    it is full, the least-recently used node is removed to make space\n    for a new one.\n    \"\"\"\n\n    def __init__(self, max_size=100000):\n        \"\"\"*max_size*, an ``int``, is the maximum number of nodes to cache;\n        it must be greater than 0.\n        \"\"\"\n\n        self.data = {}\n        self.set_max_size(max_size)\n        self.sentinel = LRUCacheNode(None, None)\n        self.lock = _threading.Lock()\n\n    def set_max_size(self, max_size):\n        if max_size < 1:\n            max_size = 1\n        self.max_size = max_size\n\n    def get(self, key):\n        \"\"\"Get the answer associated with *key*.\n\n        Returns None if no answer is cached for the key.\n\n        *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the\n        query name, rdtype, and rdclass respectively.\n\n        Returns a ``dns.resolver.Answer`` or ``None``.\n        \"\"\"\n\n        try:\n            self.lock.acquire()\n            node = self.data.get(key)\n            if node is None:\n                return None\n            # Unlink because we're either going to move the node to the front\n            # of the LRU list or we're going to free it.\n            node.unlink()\n            if node.value.expiration <= time.time():\n                del self.data[node.key]\n                return None\n            node.link_after(self.sentinel)\n            return node.value\n        finally:\n            self.lock.release()\n\n    def put(self, key, value):\n        \"\"\"Associate key and value in the cache.\n\n        *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the\n        query name, rdtype, and rdclass respectively.\n\n        *value*, a ``dns.resolver.Answer``, the answer.\n        \"\"\"\n\n        try:\n            self.lock.acquire()\n            node = self.data.get(key)\n            if node is not None:\n                node.unlink()\n                del self.data[node.key]\n            while len(self.data) >= self.max_size:\n                node = self.sentinel.prev\n                node.unlink()\n                del self.data[node.key]\n            node = LRUCacheNode(key, value)\n            node.link_after(self.sentinel)\n            self.data[key] = node\n        finally:\n            self.lock.release()\n\n    def flush(self, key=None):\n        \"\"\"Flush the cache.\n\n        If *key* is not ``None``, only that item is flushed.  Otherwise\n        the entire cache is flushed.\n\n        *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the\n        query name, rdtype, and rdclass respectively.\n        \"\"\"\n\n        try:\n            self.lock.acquire()\n            if key is not None:\n                node = self.data.get(key)\n                if node is not None:\n                    node.unlink()\n                    del self.data[node.key]\n            else:\n                node = self.sentinel.next\n                while node != self.sentinel:\n                    next = node.next\n                    node.prev = None\n                    node.next = None\n                    node = next\n                self.data = {}\n        finally:\n            self.lock.release()\n\n\nclass Resolver(object):\n    \"\"\"DNS stub resolver.\"\"\"\n\n    def __init__(self, filename='/etc/resolv.conf', configure=True):\n        \"\"\"*filename*, a ``text`` or file object, specifying a file\n        in standard /etc/resolv.conf format.  This parameter is meaningful\n        only when *configure* is true and the platform is POSIX.\n\n        *configure*, a ``bool``.  If True (the default), the resolver\n        instance is configured in the normal fashion for the operating\n        system the resolver is running on.  (I.e. by reading a\n        /etc/resolv.conf file on POSIX systems and from the registry\n        on Windows systems.)\n        \"\"\"\n\n        self.domain = None\n        self.nameservers = None\n        self.nameserver_ports = None\n        self.port = None\n        self.search = None\n        self.timeout = None\n        self.lifetime = None\n        self.keyring = None\n        self.keyname = None\n        self.keyalgorithm = None\n        self.edns = None\n        self.ednsflags = None\n        self.payload = None\n        self.cache = None\n        self.flags = None\n        self.retry_servfail = False\n        self.rotate = False\n\n        self.reset()\n        if configure:\n            if sys.platform == 'win32':\n                self.read_registry()\n            elif filename:\n                self.read_resolv_conf(filename)\n\n    def reset(self):\n        \"\"\"Reset all resolver configuration to the defaults.\"\"\"\n\n        self.domain = \\\n            dns.name.Name(dns.name.from_text(socket.gethostname())[1:])\n        if len(self.domain) == 0:\n            self.domain = dns.name.root\n        self.nameservers = []\n        self.nameserver_ports = {}\n        self.port = 53\n        self.search = []\n        self.timeout = 2.0\n        self.lifetime = 30.0\n        self.keyring = None\n        self.keyname = None\n        self.keyalgorithm = dns.tsig.default_algorithm\n        self.edns = -1\n        self.ednsflags = 0\n        self.payload = 0\n        self.cache = None\n        self.flags = None\n        self.retry_servfail = False\n        self.rotate = False\n\n    def read_resolv_conf(self, f):\n        \"\"\"Process *f* as a file in the /etc/resolv.conf format.  If f is\n        a ``text``, it is used as the name of the file to open; otherwise it\n        is treated as the file itself.\"\"\"\n\n        if isinstance(f, string_types):\n            try:\n                f = open(f, 'r')\n            except IOError:\n                # /etc/resolv.conf doesn't exist, can't be read, etc.\n                # We'll just use the default resolver configuration.\n                self.nameservers = ['127.0.0.1']\n                return\n            want_close = True\n        else:\n            want_close = False\n        try:\n            for l in f:\n                if len(l) == 0 or l[0] == '#' or l[0] == ';':\n                    continue\n                tokens = l.split()\n\n                # Any line containing less than 2 tokens is malformed\n                if len(tokens) < 2:\n                    continue\n\n                if tokens[0] == 'nameserver':\n                    self.nameservers.append(tokens[1])\n                elif tokens[0] == 'domain':\n                    self.domain = dns.name.from_text(tokens[1])\n                elif tokens[0] == 'search':\n                    for suffix in tokens[1:]:\n                        self.search.append(dns.name.from_text(suffix))\n                elif tokens[0] == 'options':\n                    if 'rotate' in tokens[1:]:\n                        self.rotate = True\n        finally:\n            if want_close:\n                f.close()\n        if len(self.nameservers) == 0:\n            self.nameservers.append('127.0.0.1')\n\n    def _determine_split_char(self, entry):\n        #\n        # The windows registry irritatingly changes the list element\n        # delimiter in between ' ' and ',' (and vice-versa) in various\n        # versions of windows.\n        #\n        if entry.find(' ') >= 0:\n            split_char = ' '\n        elif entry.find(',') >= 0:\n            split_char = ','\n        else:\n            # probably a singleton; treat as a space-separated list.\n            split_char = ' '\n        return split_char\n\n    def _config_win32_nameservers(self, nameservers):\n        # we call str() on nameservers to convert it from unicode to ascii\n        nameservers = str(nameservers)\n        split_char = self._determine_split_char(nameservers)\n        ns_list = nameservers.split(split_char)\n        for ns in ns_list:\n            if ns not in self.nameservers:\n                self.nameservers.append(ns)\n\n    def _config_win32_domain(self, domain):\n        # we call str() on domain to convert it from unicode to ascii\n        self.domain = dns.name.from_text(str(domain))\n\n    def _config_win32_search(self, search):\n        # we call str() on search to convert it from unicode to ascii\n        search = str(search)\n        split_char = self._determine_split_char(search)\n        search_list = search.split(split_char)\n        for s in search_list:\n            if s not in self.search:\n                self.search.append(dns.name.from_text(s))\n\n    def _config_win32_fromkey(self, key, always_try_domain):\n        try:\n            servers, rtype = _winreg.QueryValueEx(key, 'NameServer')\n        except WindowsError:  # pylint: disable=undefined-variable\n            servers = None\n        if servers:\n            self._config_win32_nameservers(servers)\n        if servers or always_try_domain:\n            try:\n                dom, rtype = _winreg.QueryValueEx(key, 'Domain')\n                if dom:\n                    self._config_win32_domain(dom)\n            except WindowsError:  # pylint: disable=undefined-variable\n                pass\n        else:\n            try:\n                servers, rtype = _winreg.QueryValueEx(key, 'DhcpNameServer')\n            except WindowsError:  # pylint: disable=undefined-variable\n                servers = None\n            if servers:\n                self._config_win32_nameservers(servers)\n                try:\n                    dom, rtype = _winreg.QueryValueEx(key, 'DhcpDomain')\n                    if dom:\n                        self._config_win32_domain(dom)\n                except WindowsError:  # pylint: disable=undefined-variable\n                    pass\n        try:\n            search, rtype = _winreg.QueryValueEx(key, 'SearchList')\n        except WindowsError:  # pylint: disable=undefined-variable\n            search = None\n        if search:\n            self._config_win32_search(search)\n\n    def read_registry(self):\n        \"\"\"Extract resolver configuration from the Windows registry.\"\"\"\n\n        lm = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)\n        want_scan = False\n        try:\n            try:\n                # XP, 2000\n                tcp_params = _winreg.OpenKey(lm,\n                                             r'SYSTEM\\CurrentControlSet'\n                                             r'\\Services\\Tcpip\\Parameters')\n                want_scan = True\n            except EnvironmentError:\n                # ME\n                tcp_params = _winreg.OpenKey(lm,\n                                             r'SYSTEM\\CurrentControlSet'\n                                             r'\\Services\\VxD\\MSTCP')\n            try:\n                self._config_win32_fromkey(tcp_params, True)\n            finally:\n                tcp_params.Close()\n            if want_scan:\n                interfaces = _winreg.OpenKey(lm,\n                                             r'SYSTEM\\CurrentControlSet'\n                                             r'\\Services\\Tcpip\\Parameters'\n                                             r'\\Interfaces')\n                try:\n                    i = 0\n                    while True:\n                        try:\n                            guid = _winreg.EnumKey(interfaces, i)\n                            i += 1\n                            key = _winreg.OpenKey(interfaces, guid)\n                            if not self._win32_is_nic_enabled(lm, guid, key):\n                                continue\n                            try:\n                                self._config_win32_fromkey(key, False)\n                            finally:\n                                key.Close()\n                        except EnvironmentError:\n                            break\n                finally:\n                    interfaces.Close()\n        finally:\n            lm.Close()\n\n    def _win32_is_nic_enabled(self, lm, guid, interface_key):\n        # Look in the Windows Registry to determine whether the network\n        # interface corresponding to the given guid is enabled.\n        #\n        # (Code contributed by Paul Marks, thanks!)\n        #\n        try:\n            # This hard-coded location seems to be consistent, at least\n            # from Windows 2000 through Vista.\n            connection_key = _winreg.OpenKey(\n                lm,\n                r'SYSTEM\\CurrentControlSet\\Control\\Network'\n                r'\\{4D36E972-E325-11CE-BFC1-08002BE10318}'\n                r'\\%s\\Connection' % guid)\n\n            try:\n                # The PnpInstanceID points to a key inside Enum\n                (pnp_id, ttype) = _winreg.QueryValueEx(\n                    connection_key, 'PnpInstanceID')\n\n                if ttype != _winreg.REG_SZ:\n                    raise ValueError\n\n                device_key = _winreg.OpenKey(\n                    lm, r'SYSTEM\\CurrentControlSet\\Enum\\%s' % pnp_id)\n\n                try:\n                    # Get ConfigFlags for this device\n                    (flags, ttype) = _winreg.QueryValueEx(\n                        device_key, 'ConfigFlags')\n\n                    if ttype != _winreg.REG_DWORD:\n                        raise ValueError\n\n                    # Based on experimentation, bit 0x1 indicates that the\n                    # device is disabled.\n                    return not flags & 0x1\n\n                finally:\n                    device_key.Close()\n            finally:\n                connection_key.Close()\n        except (EnvironmentError, ValueError):\n            # Pre-vista, enabled interfaces seem to have a non-empty\n            # NTEContextList; this was how dnspython detected enabled\n            # nics before the code above was contributed.  We've retained\n            # the old method since we don't know if the code above works\n            # on Windows 95/98/ME.\n            try:\n                (nte, ttype) = _winreg.QueryValueEx(interface_key,\n                                                    'NTEContextList')\n                return nte is not None\n            except WindowsError:  # pylint: disable=undefined-variable\n                return False\n\n    def _compute_timeout(self, start, lifetime=None):\n        lifetime = self.lifetime if lifetime is None else lifetime\n        now = time.time()\n        duration = now - start\n        if duration < 0:\n            if duration < -1:\n                # Time going backwards is bad.  Just give up.\n                raise Timeout(timeout=duration)\n            else:\n                # Time went backwards, but only a little.  This can\n                # happen, e.g. under vmware with older linux kernels.\n                # Pretend it didn't happen.\n                now = start\n        if duration >= lifetime:\n            raise Timeout(timeout=duration)\n        return min(lifetime - duration, self.timeout)\n\n    def query(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN,\n              tcp=False, source=None, raise_on_no_answer=True, source_port=0,\n              lifetime=None):\n        \"\"\"Query nameservers to find the answer to the question.\n\n        The *qname*, *rdtype*, and *rdclass* parameters may be objects\n        of the appropriate type, or strings that can be converted into objects\n        of the appropriate type.\n\n        *qname*, a ``dns.name.Name`` or ``text``, the query name.\n\n        *rdtype*, an ``int`` or ``text``,  the query type.\n\n        *rdclass*, an ``int`` or ``text``,  the query class.\n\n        *tcp*, a ``bool``.  If ``True``, use TCP to make the query.\n\n        *source*, a ``text`` or ``None``.  If not ``None``, bind to this IP\n        address when making queries.\n\n        *raise_on_no_answer*, a ``bool``.  If ``True``, raise\n        ``dns.resolver.NoAnswer`` if there's no answer to the question.\n\n        *source_port*, an ``int``, the port from which to send the message.\n\n        *lifetime*, a ``float``, how long query should run before timing out.\n\n        Raises ``dns.exception.Timeout`` if no answers could be found\n        in the specified lifetime.\n\n        Raises ``dns.resolver.NXDOMAIN`` if the query name does not exist.\n\n        Raises ``dns.resolver.YXDOMAIN`` if the query name is too long after\n        DNAME substitution.\n\n        Raises ``dns.resolver.NoAnswer`` if *raise_on_no_answer* is\n        ``True`` and the query name exists but has no RRset of the\n        desired type and class.\n\n        Raises ``dns.resolver.NoNameservers`` if no non-broken\n        nameservers are available to answer the question.\n\n        Returns a ``dns.resolver.Answer`` instance.\n        \"\"\"\n\n        if isinstance(qname, string_types):\n            qname = dns.name.from_text(qname, None)\n        if isinstance(rdtype, string_types):\n            rdtype = dns.rdatatype.from_text(rdtype)\n        if dns.rdatatype.is_metatype(rdtype):\n            raise NoMetaqueries\n        if isinstance(rdclass, string_types):\n            rdclass = dns.rdataclass.from_text(rdclass)\n        if dns.rdataclass.is_metaclass(rdclass):\n            raise NoMetaqueries\n        qnames_to_try = []\n        if qname.is_absolute():\n            qnames_to_try.append(qname)\n        else:\n            if len(qname) > 1:\n                qnames_to_try.append(qname.concatenate(dns.name.root))\n            if self.search:\n                for suffix in self.search:\n                    qnames_to_try.append(qname.concatenate(suffix))\n            else:\n                qnames_to_try.append(qname.concatenate(self.domain))\n        all_nxdomain = True\n        nxdomain_responses = {}\n        start = time.time()\n        _qname = None # make pylint happy\n        for _qname in qnames_to_try:\n            if self.cache:\n                answer = self.cache.get((_qname, rdtype, rdclass))\n                if answer is not None:\n                    if answer.rrset is None and raise_on_no_answer:\n                        raise NoAnswer(response=answer.response)\n                    else:\n                        return answer\n            request = dns.message.make_query(_qname, rdtype, rdclass)\n            if self.keyname is not None:\n                request.use_tsig(self.keyring, self.keyname,\n                                 algorithm=self.keyalgorithm)\n            request.use_edns(self.edns, self.ednsflags, self.payload)\n            if self.flags is not None:\n                request.flags = self.flags\n            response = None\n            #\n            # make a copy of the servers list so we can alter it later.\n            #\n            nameservers = self.nameservers[:]\n            errors = []\n            if self.rotate:\n                random.shuffle(nameservers)\n            backoff = 0.10\n            while response is None:\n                if len(nameservers) == 0:\n                    raise NoNameservers(request=request, errors=errors)\n                for nameserver in nameservers[:]:\n                    timeout = self._compute_timeout(start, lifetime)\n                    port = self.nameserver_ports.get(nameserver, self.port)\n                    try:\n                        tcp_attempt = tcp\n                        if tcp:\n                            response = dns.query.tcp(request, nameserver,\n                                                     timeout, port,\n                                                     source=source,\n                                                     source_port=source_port)\n                        else:\n                            response = dns.query.udp(request, nameserver,\n                                                     timeout, port,\n                                                     source=source,\n                                                     source_port=source_port)\n                            if response.flags & dns.flags.TC:\n                                # Response truncated; retry with TCP.\n                                tcp_attempt = True\n                                timeout = self._compute_timeout(start, lifetime)\n                                response = \\\n                                    dns.query.tcp(request, nameserver,\n                                                  timeout, port,\n                                                  source=source,\n                                                  source_port=source_port)\n                    except (socket.error, dns.exception.Timeout) as ex:\n                        #\n                        # Communication failure or timeout.  Go to the\n                        # next server\n                        #\n                        errors.append((nameserver, tcp_attempt, port, ex,\n                                       response))\n                        response = None\n                        continue\n                    except dns.query.UnexpectedSource as ex:\n                        #\n                        # Who knows?  Keep going.\n                        #\n                        errors.append((nameserver, tcp_attempt, port, ex,\n                                       response))\n                        response = None\n                        continue\n                    except dns.exception.FormError as ex:\n                        #\n                        # We don't understand what this server is\n                        # saying.  Take it out of the mix and\n                        # continue.\n                        #\n                        nameservers.remove(nameserver)\n                        errors.append((nameserver, tcp_attempt, port, ex,\n                                       response))\n                        response = None\n                        continue\n                    except EOFError as ex:\n                        #\n                        # We're using TCP and they hung up on us.\n                        # Probably they don't support TCP (though\n                        # they're supposed to!).  Take it out of the\n                        # mix and continue.\n                        #\n                        nameservers.remove(nameserver)\n                        errors.append((nameserver, tcp_attempt, port, ex,\n                                       response))\n                        response = None\n                        continue\n                    rcode = response.rcode()\n                    if rcode == dns.rcode.YXDOMAIN:\n                        ex = YXDOMAIN()\n                        errors.append((nameserver, tcp_attempt, port, ex,\n                                       response))\n                        raise ex\n                    if rcode == dns.rcode.NOERROR or \\\n                            rcode == dns.rcode.NXDOMAIN:\n                        break\n                    #\n                    # We got a response, but we're not happy with the\n                    # rcode in it.  Remove the server from the mix if\n                    # the rcode isn't SERVFAIL.\n                    #\n                    if rcode != dns.rcode.SERVFAIL or not self.retry_servfail:\n                        nameservers.remove(nameserver)\n                    errors.append((nameserver, tcp_attempt, port,\n                                   dns.rcode.to_text(rcode), response))\n                    response = None\n                if response is not None:\n                    break\n                #\n                # All nameservers failed!\n                #\n                if len(nameservers) > 0:\n                    #\n                    # But we still have servers to try.  Sleep a bit\n                    # so we don't pound them!\n                    #\n                    timeout = self._compute_timeout(start, lifetime)\n                    sleep_time = min(timeout, backoff)\n                    backoff *= 2\n                    time.sleep(sleep_time)\n            if response.rcode() == dns.rcode.NXDOMAIN:\n                nxdomain_responses[_qname] = response\n                continue\n            all_nxdomain = False\n            break\n        if all_nxdomain:\n            raise NXDOMAIN(qnames=qnames_to_try, responses=nxdomain_responses)\n        answer = Answer(_qname, rdtype, rdclass, response,\n                        raise_on_no_answer)\n        if self.cache:\n            self.cache.put((_qname, rdtype, rdclass), answer)\n        return answer\n\n    def use_tsig(self, keyring, keyname=None,\n                 algorithm=dns.tsig.default_algorithm):\n        \"\"\"Add a TSIG signature to the query.\n\n        See the documentation of the Message class for a complete\n        description of the keyring dictionary.\n\n        *keyring*, a ``dict``, the TSIG keyring to use.  If a\n        *keyring* is specified but a *keyname* is not, then the key\n        used will be the first key in the *keyring*.  Note that the\n        order of keys in a dictionary is not defined, so applications\n        should supply a keyname when a keyring is used, unless they\n        know the keyring contains only one key.\n\n        *keyname*, a ``dns.name.Name`` or ``None``, the name of the TSIG key\n        to use; defaults to ``None``. The key must be defined in the keyring.\n\n        *algorithm*, a ``dns.name.Name``, the TSIG algorithm to use.\n        \"\"\"\n\n        self.keyring = keyring\n        if keyname is None:\n            self.keyname = list(self.keyring.keys())[0]\n        else:\n            self.keyname = keyname\n        self.keyalgorithm = algorithm\n\n    def use_edns(self, edns, ednsflags, payload):\n        \"\"\"Configure EDNS behavior.\n\n        *edns*, an ``int``, is the EDNS level to use.  Specifying\n        ``None``, ``False``, or ``-1`` means \"do not use EDNS\", and in this case\n        the other parameters are ignored.  Specifying ``True`` is\n        equivalent to specifying 0, i.e. \"use EDNS0\".\n\n        *ednsflags*, an ``int``, the EDNS flag values.\n\n        *payload*, an ``int``, is the EDNS sender's payload field, which is the\n        maximum size of UDP datagram the sender can handle.  I.e. how big\n        a response to this message can be.\n        \"\"\"\n\n        if edns is None:\n            edns = -1\n        self.edns = edns\n        self.ednsflags = ednsflags\n        self.payload = payload\n\n    def set_flags(self, flags):\n        \"\"\"Overrides the default flags with your own.\n\n        *flags*, an ``int``, the message flags to use.\n        \"\"\"\n\n        self.flags = flags\n\n\n#: The default resolver.\ndefault_resolver = None\n\n\ndef get_default_resolver():\n    \"\"\"Get the default resolver, initializing it if necessary.\"\"\"\n    if default_resolver is None:\n        reset_default_resolver()\n    return default_resolver\n\n\ndef reset_default_resolver():\n    \"\"\"Re-initialize default resolver.\n\n    Note that the resolver configuration (i.e. /etc/resolv.conf on UNIX\n    systems) will be re-read immediately.\n    \"\"\"\n\n    global default_resolver\n    default_resolver = Resolver()\n\n\ndef query(qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN,\n          tcp=False, source=None, raise_on_no_answer=True,\n          source_port=0, lifetime=None):\n    \"\"\"Query nameservers to find the answer to the question.\n\n    This is a convenience function that uses the default resolver\n    object to make the query.\n\n    See ``dns.resolver.Resolver.query`` for more information on the\n    parameters.\n    \"\"\"\n\n    return get_default_resolver().query(qname, rdtype, rdclass, tcp, source,\n                                        raise_on_no_answer, source_port,\n                                        lifetime)\n\n\ndef zone_for_name(name, rdclass=dns.rdataclass.IN, tcp=False, resolver=None):\n    \"\"\"Find the name of the zone which contains the specified name.\n\n    *name*, an absolute ``dns.name.Name`` or ``text``, the query name.\n\n    *rdclass*, an ``int``, the query class.\n\n    *tcp*, a ``bool``.  If ``True``, use TCP to make the query.\n\n    *resolver*, a ``dns.resolver.Resolver`` or ``None``, the resolver to use.\n    If ``None``, the default resolver is used.\n\n    Raises ``dns.resolver.NoRootSOA`` if there is no SOA RR at the DNS\n    root.  (This is only likely to happen if you're using non-default\n    root servers in your network and they are misconfigured.)\n\n    Returns a ``dns.name.Name``.\n    \"\"\"\n\n    if isinstance(name, string_types):\n        name = dns.name.from_text(name, dns.name.root)\n    if resolver is None:\n        resolver = get_default_resolver()\n    if not name.is_absolute():\n        raise NotAbsolute(name)\n    while 1:\n        try:\n            answer = resolver.query(name, dns.rdatatype.SOA, rdclass, tcp)\n            if answer.rrset.name == name:\n                return name\n            # otherwise we were CNAMEd or DNAMEd and need to look higher\n        except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):\n            pass\n        try:\n            name = name.parent()\n        except dns.name.NoParent:\n            raise NoRootSOA\n\n#\n# Support for overriding the system resolver for all python code in the\n# running process.\n#\n\n_protocols_for_socktype = {\n    socket.SOCK_DGRAM: [socket.SOL_UDP],\n    socket.SOCK_STREAM: [socket.SOL_TCP],\n}\n\n_resolver = None\n_original_getaddrinfo = socket.getaddrinfo\n_original_getnameinfo = socket.getnameinfo\n_original_getfqdn = socket.getfqdn\n_original_gethostbyname = socket.gethostbyname\n_original_gethostbyname_ex = socket.gethostbyname_ex\n_original_gethostbyaddr = socket.gethostbyaddr\n\n\ndef _getaddrinfo(host=None, service=None, family=socket.AF_UNSPEC, socktype=0,\n                 proto=0, flags=0):\n    if flags & (socket.AI_ADDRCONFIG | socket.AI_V4MAPPED) != 0:\n        raise NotImplementedError\n    if host is None and service is None:\n        raise socket.gaierror(socket.EAI_NONAME)\n    v6addrs = []\n    v4addrs = []\n    canonical_name = None\n    try:\n        # Is host None or a V6 address literal?\n        if host is None:\n            canonical_name = 'localhost'\n            if flags & socket.AI_PASSIVE != 0:\n                v6addrs.append('::')\n                v4addrs.append('0.0.0.0')\n            else:\n                v6addrs.append('::1')\n                v4addrs.append('127.0.0.1')\n        else:\n            parts = host.split('%')\n            if len(parts) == 2:\n                ahost = parts[0]\n            else:\n                ahost = host\n            addr = dns.ipv6.inet_aton(ahost)\n            v6addrs.append(host)\n            canonical_name = host\n    except Exception:\n        try:\n            # Is it a V4 address literal?\n            addr = dns.ipv4.inet_aton(host)\n            v4addrs.append(host)\n            canonical_name = host\n        except Exception:\n            if flags & socket.AI_NUMERICHOST == 0:\n                try:\n                    if family == socket.AF_INET6 or family == socket.AF_UNSPEC:\n                        v6 = _resolver.query(host, dns.rdatatype.AAAA,\n                                             raise_on_no_answer=False)\n                        # Note that setting host ensures we query the same name\n                        # for A as we did for AAAA.\n                        host = v6.qname\n                        canonical_name = v6.canonical_name.to_text(True)\n                        if v6.rrset is not None:\n                            for rdata in v6.rrset:\n                                v6addrs.append(rdata.address)\n                    if family == socket.AF_INET or family == socket.AF_UNSPEC:\n                        v4 = _resolver.query(host, dns.rdatatype.A,\n                                             raise_on_no_answer=False)\n                        host = v4.qname\n                        canonical_name = v4.canonical_name.to_text(True)\n                        if v4.rrset is not None:\n                            for rdata in v4.rrset:\n                                v4addrs.append(rdata.address)\n                except dns.resolver.NXDOMAIN:\n                    raise socket.gaierror(socket.EAI_NONAME)\n                except Exception:\n                    raise socket.gaierror(socket.EAI_SYSTEM)\n    port = None\n    try:\n        # Is it a port literal?\n        if service is None:\n            port = 0\n        else:\n            port = int(service)\n    except Exception:\n        if flags & socket.AI_NUMERICSERV == 0:\n            try:\n                port = socket.getservbyname(service)\n            except Exception:\n                pass\n    if port is None:\n        raise socket.gaierror(socket.EAI_NONAME)\n    tuples = []\n    if socktype == 0:\n        socktypes = [socket.SOCK_DGRAM, socket.SOCK_STREAM]\n    else:\n        socktypes = [socktype]\n    if flags & socket.AI_CANONNAME != 0:\n        cname = canonical_name\n    else:\n        cname = ''\n    if family == socket.AF_INET6 or family == socket.AF_UNSPEC:\n        for addr in v6addrs:\n            for socktype in socktypes:\n                for proto in _protocols_for_socktype[socktype]:\n                    tuples.append((socket.AF_INET6, socktype, proto,\n                                   cname, (addr, port, 0, 0)))\n    if family == socket.AF_INET or family == socket.AF_UNSPEC:\n        for addr in v4addrs:\n            for socktype in socktypes:\n                for proto in _protocols_for_socktype[socktype]:\n                    tuples.append((socket.AF_INET, socktype, proto,\n                                   cname, (addr, port)))\n    if len(tuples) == 0:\n        raise socket.gaierror(socket.EAI_NONAME)\n    return tuples\n\n\ndef _getnameinfo(sockaddr, flags=0):\n    host = sockaddr[0]\n    port = sockaddr[1]\n    if len(sockaddr) == 4:\n        scope = sockaddr[3]\n        family = socket.AF_INET6\n    else:\n        scope = None\n        family = socket.AF_INET\n    tuples = _getaddrinfo(host, port, family, socket.SOCK_STREAM,\n                          socket.SOL_TCP, 0)\n    if len(tuples) > 1:\n        raise socket.error('sockaddr resolved to multiple addresses')\n    addr = tuples[0][4][0]\n    if flags & socket.NI_DGRAM:\n        pname = 'udp'\n    else:\n        pname = 'tcp'\n    qname = dns.reversename.from_address(addr)\n    if flags & socket.NI_NUMERICHOST == 0:\n        try:\n            answer = _resolver.query(qname, 'PTR')\n            hostname = answer.rrset[0].target.to_text(True)\n        except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):\n            if flags & socket.NI_NAMEREQD:\n                raise socket.gaierror(socket.EAI_NONAME)\n            hostname = addr\n            if scope is not None:\n                hostname += '%' + str(scope)\n    else:\n        hostname = addr\n        if scope is not None:\n            hostname += '%' + str(scope)\n    if flags & socket.NI_NUMERICSERV:\n        service = str(port)\n    else:\n        service = socket.getservbyport(port, pname)\n    return (hostname, service)\n\n\ndef _getfqdn(name=None):\n    if name is None:\n        name = socket.gethostname()\n    try:\n        return _getnameinfo(_getaddrinfo(name, 80)[0][4])[0]\n    except Exception:\n        return name\n\n\ndef _gethostbyname(name):\n    return _gethostbyname_ex(name)[2][0]\n\n\ndef _gethostbyname_ex(name):\n    aliases = []\n    addresses = []\n    tuples = _getaddrinfo(name, 0, socket.AF_INET, socket.SOCK_STREAM,\n                          socket.SOL_TCP, socket.AI_CANONNAME)\n    canonical = tuples[0][3]\n    for item in tuples:\n        addresses.append(item[4][0])\n    # XXX we just ignore aliases\n    return (canonical, aliases, addresses)\n\n\ndef _gethostbyaddr(ip):\n    try:\n        dns.ipv6.inet_aton(ip)\n        sockaddr = (ip, 80, 0, 0)\n        family = socket.AF_INET6\n    except Exception:\n        sockaddr = (ip, 80)\n        family = socket.AF_INET\n    (name, port) = _getnameinfo(sockaddr, socket.NI_NAMEREQD)\n    aliases = []\n    addresses = []\n    tuples = _getaddrinfo(name, 0, family, socket.SOCK_STREAM, socket.SOL_TCP,\n                          socket.AI_CANONNAME)\n    canonical = tuples[0][3]\n    for item in tuples:\n        addresses.append(item[4][0])\n    # XXX we just ignore aliases\n    return (canonical, aliases, addresses)\n\n\ndef override_system_resolver(resolver=None):\n    \"\"\"Override the system resolver routines in the socket module with\n    versions which use dnspython's resolver.\n\n    This can be useful in testing situations where you want to control\n    the resolution behavior of python code without having to change\n    the system's resolver settings (e.g. /etc/resolv.conf).\n\n    The resolver to use may be specified; if it's not, the default\n    resolver will be used.\n\n    resolver, a ``dns.resolver.Resolver`` or ``None``, the resolver to use.\n    \"\"\"\n\n    if resolver is None:\n        resolver = get_default_resolver()\n    global _resolver\n    _resolver = resolver\n    socket.getaddrinfo = _getaddrinfo\n    socket.getnameinfo = _getnameinfo\n    socket.getfqdn = _getfqdn\n    socket.gethostbyname = _gethostbyname\n    socket.gethostbyname_ex = _gethostbyname_ex\n    socket.gethostbyaddr = _gethostbyaddr\n\n\ndef restore_system_resolver():\n    \"\"\"Undo the effects of prior override_system_resolver().\"\"\"\n\n    global _resolver\n    _resolver = None\n    socket.getaddrinfo = _original_getaddrinfo\n    socket.getnameinfo = _original_getnameinfo\n    socket.getfqdn = _original_getfqdn\n    socket.gethostbyname = _original_gethostbyname\n    socket.gethostbyname_ex = _original_gethostbyname_ex\n    socket.gethostbyaddr = _original_gethostbyaddr\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/reversename.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2006-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS Reverse Map Names.\"\"\"\n\nimport binascii\n\nimport dns.name\nimport dns.ipv6\nimport dns.ipv4\n\nfrom dns._compat import PY3\n\nipv4_reverse_domain = dns.name.from_text('in-addr.arpa.')\nipv6_reverse_domain = dns.name.from_text('ip6.arpa.')\n\n\ndef from_address(text):\n    \"\"\"Convert an IPv4 or IPv6 address in textual form into a Name object whose\n    value is the reverse-map domain name of the address.\n\n    *text*, a ``text``, is an IPv4 or IPv6 address in textual form\n    (e.g. '127.0.0.1', '::1')\n\n    Raises ``dns.exception.SyntaxError`` if the address is badly formed.\n\n    Returns a ``dns.name.Name``.\n    \"\"\"\n\n    try:\n        v6 = dns.ipv6.inet_aton(text)\n        if dns.ipv6.is_mapped(v6):\n            if PY3:\n                parts = ['%d' % byte for byte in v6[12:]]\n            else:\n                parts = ['%d' % ord(byte) for byte in v6[12:]]\n            origin = ipv4_reverse_domain\n        else:\n            parts = [x for x in str(binascii.hexlify(v6).decode())]\n            origin = ipv6_reverse_domain\n    except Exception:\n        parts = ['%d' %\n                 byte for byte in bytearray(dns.ipv4.inet_aton(text))]\n        origin = ipv4_reverse_domain\n    parts.reverse()\n    return dns.name.from_text('.'.join(parts), origin=origin)\n\n\ndef to_address(name):\n    \"\"\"Convert a reverse map domain name into textual address form.\n\n    *name*, a ``dns.name.Name``, an IPv4 or IPv6 address in reverse-map name\n    form.\n\n    Raises ``dns.exception.SyntaxError`` if the name does not have a\n    reverse-map form.\n\n    Returns a ``text``.\n    \"\"\"\n\n    if name.is_subdomain(ipv4_reverse_domain):\n        name = name.relativize(ipv4_reverse_domain)\n        labels = list(name.labels)\n        labels.reverse()\n        text = b'.'.join(labels)\n        # run through inet_aton() to check syntax and make pretty.\n        return dns.ipv4.inet_ntoa(dns.ipv4.inet_aton(text))\n    elif name.is_subdomain(ipv6_reverse_domain):\n        name = name.relativize(ipv6_reverse_domain)\n        labels = list(name.labels)\n        labels.reverse()\n        parts = []\n        i = 0\n        l = len(labels)\n        while i < l:\n            parts.append(b''.join(labels[i:i + 4]))\n            i += 4\n        text = b':'.join(parts)\n        # run through inet_aton() to check syntax and make pretty.\n        return dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(text))\n    else:\n        raise dns.exception.SyntaxError('unknown reverse-map address family')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/rrset.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS RRsets (an RRset is a named rdataset)\"\"\"\n\n\nimport dns.name\nimport dns.rdataset\nimport dns.rdataclass\nimport dns.renderer\nfrom ._compat import string_types\n\n\nclass RRset(dns.rdataset.Rdataset):\n\n    \"\"\"A DNS RRset (named rdataset).\n\n    RRset inherits from Rdataset, and RRsets can be treated as\n    Rdatasets in most cases.  There are, however, a few notable\n    exceptions.  RRsets have different to_wire() and to_text() method\n    arguments, reflecting the fact that RRsets always have an owner\n    name.\n    \"\"\"\n\n    __slots__ = ['name', 'deleting']\n\n    def __init__(self, name, rdclass, rdtype, covers=dns.rdatatype.NONE,\n                 deleting=None):\n        \"\"\"Create a new RRset.\"\"\"\n\n        super(RRset, self).__init__(rdclass, rdtype, covers)\n        self.name = name\n        self.deleting = deleting\n\n    def _clone(self):\n        obj = super(RRset, self)._clone()\n        obj.name = self.name\n        obj.deleting = self.deleting\n        return obj\n\n    def __repr__(self):\n        if self.covers == 0:\n            ctext = ''\n        else:\n            ctext = '(' + dns.rdatatype.to_text(self.covers) + ')'\n        if self.deleting is not None:\n            dtext = ' delete=' + dns.rdataclass.to_text(self.deleting)\n        else:\n            dtext = ''\n        return '<DNS ' + str(self.name) + ' ' + \\\n               dns.rdataclass.to_text(self.rdclass) + ' ' + \\\n               dns.rdatatype.to_text(self.rdtype) + ctext + dtext + ' RRset>'\n\n    def __str__(self):\n        return self.to_text()\n\n    def __eq__(self, other):\n        if not isinstance(other, RRset):\n            return False\n        if self.name != other.name:\n            return False\n        return super(RRset, self).__eq__(other)\n\n    def match(self, name, rdclass, rdtype, covers, deleting=None):\n        \"\"\"Returns ``True`` if this rrset matches the specified class, type,\n        covers, and deletion state.\n        \"\"\"\n\n        if not super(RRset, self).match(rdclass, rdtype, covers):\n            return False\n        if self.name != name or self.deleting != deleting:\n            return False\n        return True\n\n    def to_text(self, origin=None, relativize=True, **kw):\n        \"\"\"Convert the RRset into DNS master file format.\n\n        See ``dns.name.Name.choose_relativity`` for more information\n        on how *origin* and *relativize* determine the way names\n        are emitted.\n\n        Any additional keyword arguments are passed on to the rdata\n        ``to_text()`` method.\n\n        *origin*, a ``dns.name.Name`` or ``None``, the origin for relative\n        names.\n\n        *relativize*, a ``bool``.  If ``True``, names will be relativized\n        to *origin*.\n        \"\"\"\n\n        return super(RRset, self).to_text(self.name, origin, relativize,\n                                          self.deleting, **kw)\n\n    def to_wire(self, file, compress=None, origin=None, **kw):\n        \"\"\"Convert the RRset to wire format.\n\n        All keyword arguments are passed to ``dns.rdataset.to_wire()``; see\n        that function for details.\n\n        Returns an ``int``, the number of records emitted.\n        \"\"\"\n\n        return super(RRset, self).to_wire(self.name, file, compress, origin,\n                                          self.deleting, **kw)\n\n    def to_rdataset(self):\n        \"\"\"Convert an RRset into an Rdataset.\n\n        Returns a ``dns.rdataset.Rdataset``.\n        \"\"\"\n        return dns.rdataset.from_rdata_list(self.ttl, list(self))\n\n\ndef from_text_list(name, ttl, rdclass, rdtype, text_rdatas,\n                   idna_codec=None):\n    \"\"\"Create an RRset with the specified name, TTL, class, and type, and with\n    the specified list of rdatas in text format.\n\n    Returns a ``dns.rrset.RRset`` object.\n    \"\"\"\n\n    if isinstance(name, string_types):\n        name = dns.name.from_text(name, None, idna_codec=idna_codec)\n    if isinstance(rdclass, string_types):\n        rdclass = dns.rdataclass.from_text(rdclass)\n    if isinstance(rdtype, string_types):\n        rdtype = dns.rdatatype.from_text(rdtype)\n    r = RRset(name, rdclass, rdtype)\n    r.update_ttl(ttl)\n    for t in text_rdatas:\n        rd = dns.rdata.from_text(r.rdclass, r.rdtype, t)\n        r.add(rd)\n    return r\n\n\ndef from_text(name, ttl, rdclass, rdtype, *text_rdatas):\n    \"\"\"Create an RRset with the specified name, TTL, class, and type and with\n    the specified rdatas in text format.\n\n    Returns a ``dns.rrset.RRset`` object.\n    \"\"\"\n\n    return from_text_list(name, ttl, rdclass, rdtype, text_rdatas)\n\n\ndef from_rdata_list(name, ttl, rdatas, idna_codec=None):\n    \"\"\"Create an RRset with the specified name and TTL, and with\n    the specified list of rdata objects.\n\n    Returns a ``dns.rrset.RRset`` object.\n    \"\"\"\n\n    if isinstance(name, string_types):\n        name = dns.name.from_text(name, None, idna_codec=idna_codec)\n\n    if len(rdatas) == 0:\n        raise ValueError(\"rdata list must not be empty\")\n    r = None\n    for rd in rdatas:\n        if r is None:\n            r = RRset(name, rd.rdclass, rd.rdtype)\n            r.update_ttl(ttl)\n        r.add(rd)\n    return r\n\n\ndef from_rdata(name, ttl, *rdatas):\n    \"\"\"Create an RRset with the specified name and TTL, and with\n    the specified rdata objects.\n\n    Returns a ``dns.rrset.RRset`` object.\n    \"\"\"\n\n    return from_rdata_list(name, ttl, rdatas)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/set.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nclass Set(object):\n\n    \"\"\"A simple set class.\n\n    This class was originally used to deal with sets being missing in\n    ancient versions of python, but dnspython will continue to use it\n    as these sets are based on lists and are thus indexable, and this\n    ability is widely used in dnspython applications.\n    \"\"\"\n\n    __slots__ = ['items']\n\n    def __init__(self, items=None):\n        \"\"\"Initialize the set.\n\n        *items*, an iterable or ``None``, the initial set of items.\n        \"\"\"\n\n        self.items = []\n        if items is not None:\n            for item in items:\n                self.add(item)\n\n    def __repr__(self):\n        return \"dns.simpleset.Set(%s)\" % repr(self.items)\n\n    def add(self, item):\n        \"\"\"Add an item to the set.\n        \"\"\"\n\n        if item not in self.items:\n            self.items.append(item)\n\n    def remove(self, item):\n        \"\"\"Remove an item from the set.\n        \"\"\"\n\n        self.items.remove(item)\n\n    def discard(self, item):\n        \"\"\"Remove an item from the set if present.\n        \"\"\"\n\n        try:\n            self.items.remove(item)\n        except ValueError:\n            pass\n\n    def _clone(self):\n        \"\"\"Make a (shallow) copy of the set.\n\n        There is a 'clone protocol' that subclasses of this class\n        should use.  To make a copy, first call your super's _clone()\n        method, and use the object returned as the new instance.  Then\n        make shallow copies of the attributes defined in the subclass.\n\n        This protocol allows us to write the set algorithms that\n        return new instances (e.g. union) once, and keep using them in\n        subclasses.\n        \"\"\"\n\n        cls = self.__class__\n        obj = cls.__new__(cls)\n        obj.items = list(self.items)\n        return obj\n\n    def __copy__(self):\n        \"\"\"Make a (shallow) copy of the set.\n        \"\"\"\n\n        return self._clone()\n\n    def copy(self):\n        \"\"\"Make a (shallow) copy of the set.\n        \"\"\"\n\n        return self._clone()\n\n    def union_update(self, other):\n        \"\"\"Update the set, adding any elements from other which are not\n        already in the set.\n        \"\"\"\n\n        if not isinstance(other, Set):\n            raise ValueError('other must be a Set instance')\n        if self is other:\n            return\n        for item in other.items:\n            self.add(item)\n\n    def intersection_update(self, other):\n        \"\"\"Update the set, removing any elements from other which are not\n        in both sets.\n        \"\"\"\n\n        if not isinstance(other, Set):\n            raise ValueError('other must be a Set instance')\n        if self is other:\n            return\n        # we make a copy of the list so that we can remove items from\n        # the list without breaking the iterator.\n        for item in list(self.items):\n            if item not in other.items:\n                self.items.remove(item)\n\n    def difference_update(self, other):\n        \"\"\"Update the set, removing any elements from other which are in\n        the set.\n        \"\"\"\n\n        if not isinstance(other, Set):\n            raise ValueError('other must be a Set instance')\n        if self is other:\n            self.items = []\n        else:\n            for item in other.items:\n                self.discard(item)\n\n    def union(self, other):\n        \"\"\"Return a new set which is the union of ``self`` and ``other``.\n\n        Returns the same Set type as this set.\n        \"\"\"\n\n        obj = self._clone()\n        obj.union_update(other)\n        return obj\n\n    def intersection(self, other):\n        \"\"\"Return a new set which is the intersection of ``self`` and\n        ``other``.\n\n        Returns the same Set type as this set.\n        \"\"\"\n\n        obj = self._clone()\n        obj.intersection_update(other)\n        return obj\n\n    def difference(self, other):\n        \"\"\"Return a new set which ``self`` - ``other``, i.e. the items\n        in ``self`` which are not also in ``other``.\n\n        Returns the same Set type as this set.\n        \"\"\"\n\n        obj = self._clone()\n        obj.difference_update(other)\n        return obj\n\n    def __or__(self, other):\n        return self.union(other)\n\n    def __and__(self, other):\n        return self.intersection(other)\n\n    def __add__(self, other):\n        return self.union(other)\n\n    def __sub__(self, other):\n        return self.difference(other)\n\n    def __ior__(self, other):\n        self.union_update(other)\n        return self\n\n    def __iand__(self, other):\n        self.intersection_update(other)\n        return self\n\n    def __iadd__(self, other):\n        self.union_update(other)\n        return self\n\n    def __isub__(self, other):\n        self.difference_update(other)\n        return self\n\n    def update(self, other):\n        \"\"\"Update the set, adding any elements from other which are not\n        already in the set.\n\n        *other*, the collection of items with which to update the set, which\n        may be any iterable type.\n        \"\"\"\n\n        for item in other:\n            self.add(item)\n\n    def clear(self):\n        \"\"\"Make the set empty.\"\"\"\n        self.items = []\n\n    def __eq__(self, other):\n        # Yes, this is inefficient but the sets we're dealing with are\n        # usually quite small, so it shouldn't hurt too much.\n        for item in self.items:\n            if item not in other.items:\n                return False\n        for item in other.items:\n            if item not in self.items:\n                return False\n        return True\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    def __len__(self):\n        return len(self.items)\n\n    def __iter__(self):\n        return iter(self.items)\n\n    def __getitem__(self, i):\n        return self.items[i]\n\n    def __delitem__(self, i):\n        del self.items[i]\n\n    def issubset(self, other):\n        \"\"\"Is this set a subset of *other*?\n\n        Returns a ``bool``.\n        \"\"\"\n\n        if not isinstance(other, Set):\n            raise ValueError('other must be a Set instance')\n        for item in self.items:\n            if item not in other.items:\n                return False\n        return True\n\n    def issuperset(self, other):\n        \"\"\"Is this set a superset of *other*?\n\n        Returns a ``bool``.\n        \"\"\"\n\n        if not isinstance(other, Set):\n            raise ValueError('other must be a Set instance')\n        for item in other.items:\n            if item not in self.items:\n                return False\n        return True\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/tokenizer.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"Tokenize DNS master file format\"\"\"\n\nfrom io import StringIO\nimport sys\n\nimport dns.exception\nimport dns.name\nimport dns.ttl\nfrom ._compat import long, text_type, binary_type\n\n_DELIMITERS = {\n    ' ': True,\n    '\\t': True,\n    '\\n': True,\n    ';': True,\n    '(': True,\n    ')': True,\n    '\"': True}\n\n_QUOTING_DELIMITERS = {'\"': True}\n\nEOF = 0\nEOL = 1\nWHITESPACE = 2\nIDENTIFIER = 3\nQUOTED_STRING = 4\nCOMMENT = 5\nDELIMITER = 6\n\n\nclass UngetBufferFull(dns.exception.DNSException):\n    \"\"\"An attempt was made to unget a token when the unget buffer was full.\"\"\"\n\n\nclass Token(object):\n    \"\"\"A DNS master file format token.\n\n    ttype: The token type\n    value: The token value\n    has_escape: Does the token value contain escapes?\n    \"\"\"\n\n    def __init__(self, ttype, value='', has_escape=False):\n        \"\"\"Initialize a token instance.\"\"\"\n\n        self.ttype = ttype\n        self.value = value\n        self.has_escape = has_escape\n\n    def is_eof(self):\n        return self.ttype == EOF\n\n    def is_eol(self):\n        return self.ttype == EOL\n\n    def is_whitespace(self):\n        return self.ttype == WHITESPACE\n\n    def is_identifier(self):\n        return self.ttype == IDENTIFIER\n\n    def is_quoted_string(self):\n        return self.ttype == QUOTED_STRING\n\n    def is_comment(self):\n        return self.ttype == COMMENT\n\n    def is_delimiter(self):\n        return self.ttype == DELIMITER\n\n    def is_eol_or_eof(self):\n        return self.ttype == EOL or self.ttype == EOF\n\n    def __eq__(self, other):\n        if not isinstance(other, Token):\n            return False\n        return (self.ttype == other.ttype and\n                self.value == other.value)\n\n    def __ne__(self, other):\n        if not isinstance(other, Token):\n            return True\n        return (self.ttype != other.ttype or\n                self.value != other.value)\n\n    def __str__(self):\n        return '%d \"%s\"' % (self.ttype, self.value)\n\n    def unescape(self):\n        if not self.has_escape:\n            return self\n        unescaped = ''\n        l = len(self.value)\n        i = 0\n        while i < l:\n            c = self.value[i]\n            i += 1\n            if c == '\\\\':\n                if i >= l:\n                    raise dns.exception.UnexpectedEnd\n                c = self.value[i]\n                i += 1\n                if c.isdigit():\n                    if i >= l:\n                        raise dns.exception.UnexpectedEnd\n                    c2 = self.value[i]\n                    i += 1\n                    if i >= l:\n                        raise dns.exception.UnexpectedEnd\n                    c3 = self.value[i]\n                    i += 1\n                    if not (c2.isdigit() and c3.isdigit()):\n                        raise dns.exception.SyntaxError\n                    c = chr(int(c) * 100 + int(c2) * 10 + int(c3))\n            unescaped += c\n        return Token(self.ttype, unescaped)\n\n    # compatibility for old-style tuple tokens\n\n    def __len__(self):\n        return 2\n\n    def __iter__(self):\n        return iter((self.ttype, self.value))\n\n    def __getitem__(self, i):\n        if i == 0:\n            return self.ttype\n        elif i == 1:\n            return self.value\n        else:\n            raise IndexError\n\n\nclass Tokenizer(object):\n    \"\"\"A DNS master file format tokenizer.\n\n    A token object is basically a (type, value) tuple.  The valid\n    types are EOF, EOL, WHITESPACE, IDENTIFIER, QUOTED_STRING,\n    COMMENT, and DELIMITER.\n\n    file: The file to tokenize\n\n    ungotten_char: The most recently ungotten character, or None.\n\n    ungotten_token: The most recently ungotten token, or None.\n\n    multiline: The current multiline level.  This value is increased\n    by one every time a '(' delimiter is read, and decreased by one every time\n    a ')' delimiter is read.\n\n    quoting: This variable is true if the tokenizer is currently\n    reading a quoted string.\n\n    eof: This variable is true if the tokenizer has encountered EOF.\n\n    delimiters: The current delimiter dictionary.\n\n    line_number: The current line number\n\n    filename: A filename that will be returned by the where() method.\n    \"\"\"\n\n    def __init__(self, f=sys.stdin, filename=None):\n        \"\"\"Initialize a tokenizer instance.\n\n        f: The file to tokenize.  The default is sys.stdin.\n        This parameter may also be a string, in which case the tokenizer\n        will take its input from the contents of the string.\n\n        filename: the name of the filename that the where() method\n        will return.\n        \"\"\"\n\n        if isinstance(f, text_type):\n            f = StringIO(f)\n            if filename is None:\n                filename = '<string>'\n        elif isinstance(f, binary_type):\n            f = StringIO(f.decode())\n            if filename is None:\n                filename = '<string>'\n        else:\n            if filename is None:\n                if f is sys.stdin:\n                    filename = '<stdin>'\n                else:\n                    filename = '<file>'\n        self.file = f\n        self.ungotten_char = None\n        self.ungotten_token = None\n        self.multiline = 0\n        self.quoting = False\n        self.eof = False\n        self.delimiters = _DELIMITERS\n        self.line_number = 1\n        self.filename = filename\n\n    def _get_char(self):\n        \"\"\"Read a character from input.\n        \"\"\"\n\n        if self.ungotten_char is None:\n            if self.eof:\n                c = ''\n            else:\n                c = self.file.read(1)\n                if c == '':\n                    self.eof = True\n                elif c == '\\n':\n                    self.line_number += 1\n        else:\n            c = self.ungotten_char\n            self.ungotten_char = None\n        return c\n\n    def where(self):\n        \"\"\"Return the current location in the input.\n\n        Returns a (string, int) tuple.  The first item is the filename of\n        the input, the second is the current line number.\n        \"\"\"\n\n        return (self.filename, self.line_number)\n\n    def _unget_char(self, c):\n        \"\"\"Unget a character.\n\n        The unget buffer for characters is only one character large; it is\n        an error to try to unget a character when the unget buffer is not\n        empty.\n\n        c: the character to unget\n        raises UngetBufferFull: there is already an ungotten char\n        \"\"\"\n\n        if self.ungotten_char is not None:\n            raise UngetBufferFull\n        self.ungotten_char = c\n\n    def skip_whitespace(self):\n        \"\"\"Consume input until a non-whitespace character is encountered.\n\n        The non-whitespace character is then ungotten, and the number of\n        whitespace characters consumed is returned.\n\n        If the tokenizer is in multiline mode, then newlines are whitespace.\n\n        Returns the number of characters skipped.\n        \"\"\"\n\n        skipped = 0\n        while True:\n            c = self._get_char()\n            if c != ' ' and c != '\\t':\n                if (c != '\\n') or not self.multiline:\n                    self._unget_char(c)\n                    return skipped\n            skipped += 1\n\n    def get(self, want_leading=False, want_comment=False):\n        \"\"\"Get the next token.\n\n        want_leading: If True, return a WHITESPACE token if the\n        first character read is whitespace.  The default is False.\n\n        want_comment: If True, return a COMMENT token if the\n        first token read is a comment.  The default is False.\n\n        Raises dns.exception.UnexpectedEnd: input ended prematurely\n\n        Raises dns.exception.SyntaxError: input was badly formed\n\n        Returns a Token.\n        \"\"\"\n\n        if self.ungotten_token is not None:\n            token = self.ungotten_token\n            self.ungotten_token = None\n            if token.is_whitespace():\n                if want_leading:\n                    return token\n            elif token.is_comment():\n                if want_comment:\n                    return token\n            else:\n                return token\n        skipped = self.skip_whitespace()\n        if want_leading and skipped > 0:\n            return Token(WHITESPACE, ' ')\n        token = ''\n        ttype = IDENTIFIER\n        has_escape = False\n        while True:\n            c = self._get_char()\n            if c == '' or c in self.delimiters:\n                if c == '' and self.quoting:\n                    raise dns.exception.UnexpectedEnd\n                if token == '' and ttype != QUOTED_STRING:\n                    if c == '(':\n                        self.multiline += 1\n                        self.skip_whitespace()\n                        continue\n                    elif c == ')':\n                        if self.multiline <= 0:\n                            raise dns.exception.SyntaxError\n                        self.multiline -= 1\n                        self.skip_whitespace()\n                        continue\n                    elif c == '\"':\n                        if not self.quoting:\n                            self.quoting = True\n                            self.delimiters = _QUOTING_DELIMITERS\n                            ttype = QUOTED_STRING\n                            continue\n                        else:\n                            self.quoting = False\n                            self.delimiters = _DELIMITERS\n                            self.skip_whitespace()\n                            continue\n                    elif c == '\\n':\n                        return Token(EOL, '\\n')\n                    elif c == ';':\n                        while 1:\n                            c = self._get_char()\n                            if c == '\\n' or c == '':\n                                break\n                            token += c\n                        if want_comment:\n                            self._unget_char(c)\n                            return Token(COMMENT, token)\n                        elif c == '':\n                            if self.multiline:\n                                raise dns.exception.SyntaxError(\n                                    'unbalanced parentheses')\n                            return Token(EOF)\n                        elif self.multiline:\n                            self.skip_whitespace()\n                            token = ''\n                            continue\n                        else:\n                            return Token(EOL, '\\n')\n                    else:\n                        # This code exists in case we ever want a\n                        # delimiter to be returned.  It never produces\n                        # a token currently.\n                        token = c\n                        ttype = DELIMITER\n                else:\n                    self._unget_char(c)\n                break\n            elif self.quoting:\n                if c == '\\\\':\n                    c = self._get_char()\n                    if c == '':\n                        raise dns.exception.UnexpectedEnd\n                    if c.isdigit():\n                        c2 = self._get_char()\n                        if c2 == '':\n                            raise dns.exception.UnexpectedEnd\n                        c3 = self._get_char()\n                        if c == '':\n                            raise dns.exception.UnexpectedEnd\n                        if not (c2.isdigit() and c3.isdigit()):\n                            raise dns.exception.SyntaxError\n                        c = chr(int(c) * 100 + int(c2) * 10 + int(c3))\n                elif c == '\\n':\n                    raise dns.exception.SyntaxError('newline in quoted string')\n            elif c == '\\\\':\n                #\n                # It's an escape.  Put it and the next character into\n                # the token; it will be checked later for goodness.\n                #\n                token += c\n                has_escape = True\n                c = self._get_char()\n                if c == '' or c == '\\n':\n                    raise dns.exception.UnexpectedEnd\n            token += c\n        if token == '' and ttype != QUOTED_STRING:\n            if self.multiline:\n                raise dns.exception.SyntaxError('unbalanced parentheses')\n            ttype = EOF\n        return Token(ttype, token, has_escape)\n\n    def unget(self, token):\n        \"\"\"Unget a token.\n\n        The unget buffer for tokens is only one token large; it is\n        an error to try to unget a token when the unget buffer is not\n        empty.\n\n        token: the token to unget\n\n        Raises UngetBufferFull: there is already an ungotten token\n        \"\"\"\n\n        if self.ungotten_token is not None:\n            raise UngetBufferFull\n        self.ungotten_token = token\n\n    def next(self):\n        \"\"\"Return the next item in an iteration.\n\n        Returns a Token.\n        \"\"\"\n\n        token = self.get()\n        if token.is_eof():\n            raise StopIteration\n        return token\n\n    __next__ = next\n\n    def __iter__(self):\n        return self\n\n    # Helpers\n\n    def get_int(self, base=10):\n        \"\"\"Read the next token and interpret it as an integer.\n\n        Raises dns.exception.SyntaxError if not an integer.\n\n        Returns an int.\n        \"\"\"\n\n        token = self.get().unescape()\n        if not token.is_identifier():\n            raise dns.exception.SyntaxError('expecting an identifier')\n        if not token.value.isdigit():\n            raise dns.exception.SyntaxError('expecting an integer')\n        return int(token.value, base)\n\n    def get_uint8(self):\n        \"\"\"Read the next token and interpret it as an 8-bit unsigned\n        integer.\n\n        Raises dns.exception.SyntaxError if not an 8-bit unsigned integer.\n\n        Returns an int.\n        \"\"\"\n\n        value = self.get_int()\n        if value < 0 or value > 255:\n            raise dns.exception.SyntaxError(\n                '%d is not an unsigned 8-bit integer' % value)\n        return value\n\n    def get_uint16(self, base=10):\n        \"\"\"Read the next token and interpret it as a 16-bit unsigned\n        integer.\n\n        Raises dns.exception.SyntaxError if not a 16-bit unsigned integer.\n\n        Returns an int.\n        \"\"\"\n\n        value = self.get_int(base=base)\n        if value < 0 or value > 65535:\n            if base == 8:\n                raise dns.exception.SyntaxError(\n                    '%o is not an octal unsigned 16-bit integer' % value)\n            else:\n                raise dns.exception.SyntaxError(\n                    '%d is not an unsigned 16-bit integer' % value)\n        return value\n\n    def get_uint32(self):\n        \"\"\"Read the next token and interpret it as a 32-bit unsigned\n        integer.\n\n        Raises dns.exception.SyntaxError if not a 32-bit unsigned integer.\n\n        Returns an int.\n        \"\"\"\n\n        token = self.get().unescape()\n        if not token.is_identifier():\n            raise dns.exception.SyntaxError('expecting an identifier')\n        if not token.value.isdigit():\n            raise dns.exception.SyntaxError('expecting an integer')\n        value = long(token.value)\n        if value < 0 or value > long(4294967296):\n            raise dns.exception.SyntaxError(\n                '%d is not an unsigned 32-bit integer' % value)\n        return value\n\n    def get_string(self, origin=None):\n        \"\"\"Read the next token and interpret it as a string.\n\n        Raises dns.exception.SyntaxError if not a string.\n\n        Returns a string.\n        \"\"\"\n\n        token = self.get().unescape()\n        if not (token.is_identifier() or token.is_quoted_string()):\n            raise dns.exception.SyntaxError('expecting a string')\n        return token.value\n\n    def get_identifier(self, origin=None):\n        \"\"\"Read the next token, which should be an identifier.\n\n        Raises dns.exception.SyntaxError if not an identifier.\n\n        Returns a string.\n        \"\"\"\n\n        token = self.get().unescape()\n        if not token.is_identifier():\n            raise dns.exception.SyntaxError('expecting an identifier')\n        return token.value\n\n    def get_name(self, origin=None):\n        \"\"\"Read the next token and interpret it as a DNS name.\n\n        Raises dns.exception.SyntaxError if not a name.\n\n        Returns a dns.name.Name.\n        \"\"\"\n\n        token = self.get()\n        if not token.is_identifier():\n            raise dns.exception.SyntaxError('expecting an identifier')\n        return dns.name.from_text(token.value, origin)\n\n    def get_eol(self):\n        \"\"\"Read the next token and raise an exception if it isn't EOL or\n        EOF.\n\n        Returns a string.\n        \"\"\"\n\n        token = self.get()\n        if not token.is_eol_or_eof():\n            raise dns.exception.SyntaxError(\n                'expected EOL or EOF, got %d \"%s\"' % (token.ttype,\n                                                      token.value))\n        return token.value\n\n    def get_ttl(self):\n        \"\"\"Read the next token and interpret it as a DNS TTL.\n\n        Raises dns.exception.SyntaxError or dns.ttl.BadTTL if not an\n        identifier or badly formed.\n\n        Returns an int.\n        \"\"\"\n\n        token = self.get().unescape()\n        if not token.is_identifier():\n            raise dns.exception.SyntaxError('expecting an identifier')\n        return dns.ttl.from_text(token.value)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/tsig.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS TSIG support.\"\"\"\n\nimport hashlib\nimport hmac\nimport struct\n\nimport dns.exception\nimport dns.rdataclass\nimport dns.name\nfrom ._compat import long, string_types, text_type\n\nclass BadTime(dns.exception.DNSException):\n\n    \"\"\"The current time is not within the TSIG's validity time.\"\"\"\n\n\nclass BadSignature(dns.exception.DNSException):\n\n    \"\"\"The TSIG signature fails to verify.\"\"\"\n\n\nclass PeerError(dns.exception.DNSException):\n\n    \"\"\"Base class for all TSIG errors generated by the remote peer\"\"\"\n\n\nclass PeerBadKey(PeerError):\n\n    \"\"\"The peer didn't know the key we used\"\"\"\n\n\nclass PeerBadSignature(PeerError):\n\n    \"\"\"The peer didn't like the signature we sent\"\"\"\n\n\nclass PeerBadTime(PeerError):\n\n    \"\"\"The peer didn't like the time we sent\"\"\"\n\n\nclass PeerBadTruncation(PeerError):\n\n    \"\"\"The peer didn't like amount of truncation in the TSIG we sent\"\"\"\n\n# TSIG Algorithms\n\nHMAC_MD5 = dns.name.from_text(\"HMAC-MD5.SIG-ALG.REG.INT\")\nHMAC_SHA1 = dns.name.from_text(\"hmac-sha1\")\nHMAC_SHA224 = dns.name.from_text(\"hmac-sha224\")\nHMAC_SHA256 = dns.name.from_text(\"hmac-sha256\")\nHMAC_SHA384 = dns.name.from_text(\"hmac-sha384\")\nHMAC_SHA512 = dns.name.from_text(\"hmac-sha512\")\n\n_hashes = {\n    HMAC_SHA224: hashlib.sha224,\n    HMAC_SHA256: hashlib.sha256,\n    HMAC_SHA384: hashlib.sha384,\n    HMAC_SHA512: hashlib.sha512,\n    HMAC_SHA1: hashlib.sha1,\n    HMAC_MD5: hashlib.md5,\n}\n\ndefault_algorithm = HMAC_MD5\n\nBADSIG = 16\nBADKEY = 17\nBADTIME = 18\nBADTRUNC = 22\n\n\ndef sign(wire, keyname, secret, time, fudge, original_id, error,\n         other_data, request_mac, ctx=None, multi=False, first=True,\n         algorithm=default_algorithm):\n    \"\"\"Return a (tsig_rdata, mac, ctx) tuple containing the HMAC TSIG rdata\n    for the input parameters, the HMAC MAC calculated by applying the\n    TSIG signature algorithm, and the TSIG digest context.\n    @rtype: (string, string, hmac.HMAC object)\n    @raises ValueError: I{other_data} is too long\n    @raises NotImplementedError: I{algorithm} is not supported\n    \"\"\"\n\n    if isinstance(other_data, text_type):\n        other_data = other_data.encode()\n    (algorithm_name, digestmod) = get_algorithm(algorithm)\n    if first:\n        ctx = hmac.new(secret, digestmod=digestmod)\n        ml = len(request_mac)\n        if ml > 0:\n            ctx.update(struct.pack('!H', ml))\n            ctx.update(request_mac)\n    id = struct.pack('!H', original_id)\n    ctx.update(id)\n    ctx.update(wire[2:])\n    if first:\n        ctx.update(keyname.to_digestable())\n        ctx.update(struct.pack('!H', dns.rdataclass.ANY))\n        ctx.update(struct.pack('!I', 0))\n    long_time = time + long(0)\n    upper_time = (long_time >> 32) & long(0xffff)\n    lower_time = long_time & long(0xffffffff)\n    time_mac = struct.pack('!HIH', upper_time, lower_time, fudge)\n    pre_mac = algorithm_name + time_mac\n    ol = len(other_data)\n    if ol > 65535:\n        raise ValueError('TSIG Other Data is > 65535 bytes')\n    post_mac = struct.pack('!HH', error, ol) + other_data\n    if first:\n        ctx.update(pre_mac)\n        ctx.update(post_mac)\n    else:\n        ctx.update(time_mac)\n    mac = ctx.digest()\n    mpack = struct.pack('!H', len(mac))\n    tsig_rdata = pre_mac + mpack + mac + id + post_mac\n    if multi:\n        ctx = hmac.new(secret, digestmod=digestmod)\n        ml = len(mac)\n        ctx.update(struct.pack('!H', ml))\n        ctx.update(mac)\n    else:\n        ctx = None\n    return (tsig_rdata, mac, ctx)\n\n\ndef hmac_md5(wire, keyname, secret, time, fudge, original_id, error,\n             other_data, request_mac, ctx=None, multi=False, first=True,\n             algorithm=default_algorithm):\n    return sign(wire, keyname, secret, time, fudge, original_id, error,\n                other_data, request_mac, ctx, multi, first, algorithm)\n\n\ndef validate(wire, keyname, secret, now, request_mac, tsig_start, tsig_rdata,\n             tsig_rdlen, ctx=None, multi=False, first=True):\n    \"\"\"Validate the specified TSIG rdata against the other input parameters.\n\n    @raises FormError: The TSIG is badly formed.\n    @raises BadTime: There is too much time skew between the client and the\n    server.\n    @raises BadSignature: The TSIG signature did not validate\n    @rtype: hmac.HMAC object\"\"\"\n\n    (adcount,) = struct.unpack(\"!H\", wire[10:12])\n    if adcount == 0:\n        raise dns.exception.FormError\n    adcount -= 1\n    new_wire = wire[0:10] + struct.pack(\"!H\", adcount) + wire[12:tsig_start]\n    current = tsig_rdata\n    (aname, used) = dns.name.from_wire(wire, current)\n    current = current + used\n    (upper_time, lower_time, fudge, mac_size) = \\\n        struct.unpack(\"!HIHH\", wire[current:current + 10])\n    time = ((upper_time + long(0)) << 32) + (lower_time + long(0))\n    current += 10\n    mac = wire[current:current + mac_size]\n    current += mac_size\n    (original_id, error, other_size) = \\\n        struct.unpack(\"!HHH\", wire[current:current + 6])\n    current += 6\n    other_data = wire[current:current + other_size]\n    current += other_size\n    if current != tsig_rdata + tsig_rdlen:\n        raise dns.exception.FormError\n    if error != 0:\n        if error == BADSIG:\n            raise PeerBadSignature\n        elif error == BADKEY:\n            raise PeerBadKey\n        elif error == BADTIME:\n            raise PeerBadTime\n        elif error == BADTRUNC:\n            raise PeerBadTruncation\n        else:\n            raise PeerError('unknown TSIG error code %d' % error)\n    time_low = time - fudge\n    time_high = time + fudge\n    if now < time_low or now > time_high:\n        raise BadTime\n    (junk, our_mac, ctx) = sign(new_wire, keyname, secret, time, fudge,\n                                original_id, error, other_data,\n                                request_mac, ctx, multi, first, aname)\n    if our_mac != mac:\n        raise BadSignature\n    return ctx\n\n\ndef get_algorithm(algorithm):\n    \"\"\"Returns the wire format string and the hash module to use for the\n    specified TSIG algorithm\n\n    @rtype: (string, hash constructor)\n    @raises NotImplementedError: I{algorithm} is not supported\n    \"\"\"\n\n    if isinstance(algorithm, string_types):\n        algorithm = dns.name.from_text(algorithm)\n\n    try:\n        return (algorithm.to_digestable(), _hashes[algorithm])\n    except KeyError:\n        raise NotImplementedError(\"TSIG algorithm \" + str(algorithm) +\n                                  \" is not supported\")\n\n\ndef get_algorithm_and_mac(wire, tsig_rdata, tsig_rdlen):\n    \"\"\"Return the tsig algorithm for the specified tsig_rdata\n    @raises FormError: The TSIG is badly formed.\n    \"\"\"\n    current = tsig_rdata\n    (aname, used) = dns.name.from_wire(wire, current)\n    current = current + used\n    (upper_time, lower_time, fudge, mac_size) = \\\n        struct.unpack(\"!HIHH\", wire[current:current + 10])\n    current += 10\n    mac = wire[current:current + mac_size]\n    current += mac_size\n    if current > tsig_rdata + tsig_rdlen:\n        raise dns.exception.FormError\n    return (aname, mac)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/tsigkeyring.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"A place to store TSIG keys.\"\"\"\n\nfrom dns._compat import maybe_decode, maybe_encode\n\nimport base64\n\nimport dns.name\n\n\ndef from_text(textring):\n    \"\"\"Convert a dictionary containing (textual DNS name, base64 secret) pairs\n    into a binary keyring which has (dns.name.Name, binary secret) pairs.\n    @rtype: dict\"\"\"\n\n    keyring = {}\n    for keytext in textring:\n        keyname = dns.name.from_text(keytext)\n        secret = base64.decodestring(maybe_encode(textring[keytext]))\n        keyring[keyname] = secret\n    return keyring\n\n\ndef to_text(keyring):\n    \"\"\"Convert a dictionary containing (dns.name.Name, binary secret) pairs\n    into a text keyring which has (textual DNS name, base64 secret) pairs.\n    @rtype: dict\"\"\"\n\n    textring = {}\n    for keyname in keyring:\n        keytext = maybe_decode(keyname.to_text())\n        secret = maybe_decode(base64.encodestring(keyring[keyname]))\n        textring[keytext] = secret\n    return textring\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/ttl.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS TTL conversion.\"\"\"\n\nimport dns.exception\nfrom ._compat import long\n\n\nclass BadTTL(dns.exception.SyntaxError):\n    \"\"\"DNS TTL value is not well-formed.\"\"\"\n\n\ndef from_text(text):\n    \"\"\"Convert the text form of a TTL to an integer.\n\n    The BIND 8 units syntax for TTLs (e.g. '1w6d4h3m10s') is supported.\n\n    *text*, a ``text``, the textual TTL.\n\n    Raises ``dns.ttl.BadTTL`` if the TTL is not well-formed.\n\n    Returns an ``int``.\n    \"\"\"\n\n    if text.isdigit():\n        total = long(text)\n    else:\n        if not text[0].isdigit():\n            raise BadTTL\n        total = long(0)\n        current = long(0)\n        for c in text:\n            if c.isdigit():\n                current *= 10\n                current += long(c)\n            else:\n                c = c.lower()\n                if c == 'w':\n                    total += current * long(604800)\n                elif c == 'd':\n                    total += current * long(86400)\n                elif c == 'h':\n                    total += current * long(3600)\n                elif c == 'm':\n                    total += current * long(60)\n                elif c == 's':\n                    total += current\n                else:\n                    raise BadTTL(\"unknown unit '%s'\" % c)\n                current = 0\n        if not current == 0:\n            raise BadTTL(\"trailing integer\")\n    if total < long(0) or total > long(2147483647):\n        raise BadTTL(\"TTL should be between 0 and 2^31 - 1 (inclusive)\")\n    return total\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/update.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS Dynamic Update Support\"\"\"\n\n\nimport dns.message\nimport dns.name\nimport dns.opcode\nimport dns.rdata\nimport dns.rdataclass\nimport dns.rdataset\nimport dns.tsig\nfrom ._compat import string_types\n\n\nclass Update(dns.message.Message):\n\n    def __init__(self, zone, rdclass=dns.rdataclass.IN, keyring=None,\n                 keyname=None, keyalgorithm=dns.tsig.default_algorithm):\n        \"\"\"Initialize a new DNS Update object.\n\n        See the documentation of the Message class for a complete\n        description of the keyring dictionary.\n\n        *zone*, a ``dns.name.Name`` or ``text``, the zone which is being\n        updated.\n\n        *rdclass*, an ``int`` or ``text``, the class of the zone.\n\n        *keyring*, a ``dict``, the TSIG keyring to use.  If a\n        *keyring* is specified but a *keyname* is not, then the key\n        used will be the first key in the *keyring*.  Note that the\n        order of keys in a dictionary is not defined, so applications\n        should supply a keyname when a keyring is used, unless they\n        know the keyring contains only one key.\n\n        *keyname*, a ``dns.name.Name`` or ``None``, the name of the TSIG key\n        to use; defaults to ``None``. The key must be defined in the keyring.\n\n        *keyalgorithm*, a ``dns.name.Name``, the TSIG algorithm to use.\n        \"\"\"\n        super(Update, self).__init__()\n        self.flags |= dns.opcode.to_flags(dns.opcode.UPDATE)\n        if isinstance(zone, string_types):\n            zone = dns.name.from_text(zone)\n        self.origin = zone\n        if isinstance(rdclass, string_types):\n            rdclass = dns.rdataclass.from_text(rdclass)\n        self.zone_rdclass = rdclass\n        self.find_rrset(self.question, self.origin, rdclass, dns.rdatatype.SOA,\n                        create=True, force_unique=True)\n        if keyring is not None:\n            self.use_tsig(keyring, keyname, algorithm=keyalgorithm)\n\n    def _add_rr(self, name, ttl, rd, deleting=None, section=None):\n        \"\"\"Add a single RR to the update section.\"\"\"\n\n        if section is None:\n            section = self.authority\n        covers = rd.covers()\n        rrset = self.find_rrset(section, name, self.zone_rdclass, rd.rdtype,\n                                covers, deleting, True, True)\n        rrset.add(rd, ttl)\n\n    def _add(self, replace, section, name, *args):\n        \"\"\"Add records.\n\n        *replace* is the replacement mode.  If ``False``,\n        RRs are added to an existing RRset; if ``True``, the RRset\n        is replaced with the specified contents.  The second\n        argument is the section to add to.  The third argument\n        is always a name.  The other arguments can be:\n\n                - rdataset...\n\n                - ttl, rdata...\n\n                - ttl, rdtype, string...\n        \"\"\"\n\n        if isinstance(name, string_types):\n            name = dns.name.from_text(name, None)\n        if isinstance(args[0], dns.rdataset.Rdataset):\n            for rds in args:\n                if replace:\n                    self.delete(name, rds.rdtype)\n                for rd in rds:\n                    self._add_rr(name, rds.ttl, rd, section=section)\n        else:\n            args = list(args)\n            ttl = int(args.pop(0))\n            if isinstance(args[0], dns.rdata.Rdata):\n                if replace:\n                    self.delete(name, args[0].rdtype)\n                for rd in args:\n                    self._add_rr(name, ttl, rd, section=section)\n            else:\n                rdtype = args.pop(0)\n                if isinstance(rdtype, string_types):\n                    rdtype = dns.rdatatype.from_text(rdtype)\n                if replace:\n                    self.delete(name, rdtype)\n                for s in args:\n                    rd = dns.rdata.from_text(self.zone_rdclass, rdtype, s,\n                                             self.origin)\n                    self._add_rr(name, ttl, rd, section=section)\n\n    def add(self, name, *args):\n        \"\"\"Add records.\n\n        The first argument is always a name.  The other\n        arguments can be:\n\n                - rdataset...\n\n                - ttl, rdata...\n\n                - ttl, rdtype, string...\n        \"\"\"\n\n        self._add(False, self.authority, name, *args)\n\n    def delete(self, name, *args):\n        \"\"\"Delete records.\n\n        The first argument is always a name.  The other\n        arguments can be:\n\n                - *empty*\n\n                - rdataset...\n\n                - rdata...\n\n                - rdtype, [string...]\n        \"\"\"\n\n        if isinstance(name, string_types):\n            name = dns.name.from_text(name, None)\n        if len(args) == 0:\n            self.find_rrset(self.authority, name, dns.rdataclass.ANY,\n                            dns.rdatatype.ANY, dns.rdatatype.NONE,\n                            dns.rdatatype.ANY, True, True)\n        elif isinstance(args[0], dns.rdataset.Rdataset):\n            for rds in args:\n                for rd in rds:\n                    self._add_rr(name, 0, rd, dns.rdataclass.NONE)\n        else:\n            args = list(args)\n            if isinstance(args[0], dns.rdata.Rdata):\n                for rd in args:\n                    self._add_rr(name, 0, rd, dns.rdataclass.NONE)\n            else:\n                rdtype = args.pop(0)\n                if isinstance(rdtype, string_types):\n                    rdtype = dns.rdatatype.from_text(rdtype)\n                if len(args) == 0:\n                    self.find_rrset(self.authority, name,\n                                    self.zone_rdclass, rdtype,\n                                    dns.rdatatype.NONE,\n                                    dns.rdataclass.ANY,\n                                    True, True)\n                else:\n                    for s in args:\n                        rd = dns.rdata.from_text(self.zone_rdclass, rdtype, s,\n                                                 self.origin)\n                        self._add_rr(name, 0, rd, dns.rdataclass.NONE)\n\n    def replace(self, name, *args):\n        \"\"\"Replace records.\n\n        The first argument is always a name.  The other\n        arguments can be:\n\n                - rdataset...\n\n                - ttl, rdata...\n\n                - ttl, rdtype, string...\n\n        Note that if you want to replace the entire node, you should do\n        a delete of the name followed by one or more calls to add.\n        \"\"\"\n\n        self._add(True, self.authority, name, *args)\n\n    def present(self, name, *args):\n        \"\"\"Require that an owner name (and optionally an rdata type,\n        or specific rdataset) exists as a prerequisite to the\n        execution of the update.\n\n        The first argument is always a name.\n        The other arguments can be:\n\n                - rdataset...\n\n                - rdata...\n\n                - rdtype, string...\n        \"\"\"\n\n        if isinstance(name, string_types):\n            name = dns.name.from_text(name, None)\n        if len(args) == 0:\n            self.find_rrset(self.answer, name,\n                            dns.rdataclass.ANY, dns.rdatatype.ANY,\n                            dns.rdatatype.NONE, None,\n                            True, True)\n        elif isinstance(args[0], dns.rdataset.Rdataset) or \\\n            isinstance(args[0], dns.rdata.Rdata) or \\\n                len(args) > 1:\n            if not isinstance(args[0], dns.rdataset.Rdataset):\n                # Add a 0 TTL\n                args = list(args)\n                args.insert(0, 0)\n            self._add(False, self.answer, name, *args)\n        else:\n            rdtype = args[0]\n            if isinstance(rdtype, string_types):\n                rdtype = dns.rdatatype.from_text(rdtype)\n            self.find_rrset(self.answer, name,\n                            dns.rdataclass.ANY, rdtype,\n                            dns.rdatatype.NONE, None,\n                            True, True)\n\n    def absent(self, name, rdtype=None):\n        \"\"\"Require that an owner name (and optionally an rdata type) does\n        not exist as a prerequisite to the execution of the update.\"\"\"\n\n        if isinstance(name, string_types):\n            name = dns.name.from_text(name, None)\n        if rdtype is None:\n            self.find_rrset(self.answer, name,\n                            dns.rdataclass.NONE, dns.rdatatype.ANY,\n                            dns.rdatatype.NONE, None,\n                            True, True)\n        else:\n            if isinstance(rdtype, string_types):\n                rdtype = dns.rdatatype.from_text(rdtype)\n            self.find_rrset(self.answer, name,\n                            dns.rdataclass.NONE, rdtype,\n                            dns.rdatatype.NONE, None,\n                            True, True)\n\n    def to_wire(self, origin=None, max_size=65535):\n        \"\"\"Return a string containing the update in DNS compressed wire\n        format.\n\n        *origin*, a ``dns.name.Name`` or ``None``, the origin to be\n        appended to any relative names.  If *origin* is ``None``, then\n        the origin of the ``dns.update.Update`` message object is used\n        (i.e. the *zone* parameter passed when the Update object was\n        created).\n\n        *max_size*, an ``int``, the maximum size of the wire format\n        output; default is 0, which means \"the message's request\n        payload, if nonzero, or 65535\".\n\n        Returns a ``binary``.\n        \"\"\"\n\n        if origin is None:\n            origin = self.origin\n        return super(Update, self).to_wire(origin, max_size)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/version.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"dnspython release version information.\"\"\"\n\n#: MAJOR\nMAJOR = 1\n#: MINOR\nMINOR = 16\n#: MICRO\nMICRO = 0\n#: RELEASELEVEL\nRELEASELEVEL = 0x0f\n#: SERIAL\nSERIAL = 0\n\nif RELEASELEVEL == 0x0f:\n    #: version\n    version = '%d.%d.%d' % (MAJOR, MINOR, MICRO)\nelif RELEASELEVEL == 0x00:\n    version = '%d.%d.%dx%d' % \\\n              (MAJOR, MINOR, MICRO, SERIAL)\nelse:\n    version = '%d.%d.%d%x%d' % \\\n              (MAJOR, MINOR, MICRO, RELEASELEVEL, SERIAL)\n\n#: hexversion\nhexversion = MAJOR << 24 | MINOR << 16 | MICRO << 8 | RELEASELEVEL << 4 | \\\n    SERIAL\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/wiredata.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2011,2017 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS Wire Data Helper\"\"\"\n\nimport dns.exception\nfrom ._compat import binary_type, string_types, PY2\n\n# Figure out what constant python passes for an unspecified slice bound.\n# It's supposed to be sys.maxint, yet on 64-bit windows sys.maxint is 2^31 - 1\n# but Python uses 2^63 - 1 as the constant.  Rather than making pointless\n# extra comparisons, duplicating code, or weakening WireData, we just figure\n# out what constant Python will use.\n\n\nclass _SliceUnspecifiedBound(binary_type):\n\n    def __getitem__(self, key):\n        return key.stop\n\n    if PY2:\n        def __getslice__(self, i, j):  # pylint: disable=getslice-method\n            return self.__getitem__(slice(i, j))\n\n_unspecified_bound = _SliceUnspecifiedBound()[1:]\n\n\nclass WireData(binary_type):\n    # WireData is a binary type with stricter slicing\n\n    def __getitem__(self, key):\n        try:\n            if isinstance(key, slice):\n                # make sure we are not going outside of valid ranges,\n                # do stricter control of boundaries than python does\n                # by default\n                start = key.start\n                stop = key.stop\n\n                if PY2:\n                    if stop == _unspecified_bound:\n                        # handle the case where the right bound is unspecified\n                        stop = len(self)\n\n                    if start < 0 or stop < 0:\n                        raise dns.exception.FormError\n                    # If it's not an empty slice, access left and right bounds\n                    # to make sure they're valid\n                    if start != stop:\n                        super(WireData, self).__getitem__(start)\n                        super(WireData, self).__getitem__(stop - 1)\n                else:\n                    for index in (start, stop):\n                        if index is None:\n                            continue\n                        elif abs(index) > len(self):\n                            raise dns.exception.FormError\n\n                return WireData(super(WireData, self).__getitem__(\n                    slice(start, stop)))\n            return bytearray(self.unwrap())[key]\n        except IndexError:\n            raise dns.exception.FormError\n\n    if PY2:\n        def __getslice__(self, i, j):  # pylint: disable=getslice-method\n            return self.__getitem__(slice(i, j))\n\n    def __iter__(self):\n        i = 0\n        while 1:\n            try:\n                yield self[i]\n                i += 1\n            except dns.exception.FormError:\n                raise StopIteration\n\n    def unwrap(self):\n        return binary_type(self)\n\n\ndef maybe_wrap(wire):\n    if isinstance(wire, WireData):\n        return wire\n    elif isinstance(wire, binary_type):\n        return WireData(wire)\n    elif isinstance(wire, string_types):\n        return WireData(wire.encode())\n    raise ValueError(\"unhandled type %s\" % type(wire))\n"
  },
  {
    "path": "openpype/vendor/python/python_2/dns/zone.py",
    "content": "# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n\n# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n#\n# Permission to use, copy, modify, and distribute this software and its\n# documentation for any purpose with or without fee is hereby granted,\n# provided that the above copyright notice and this permission notice\n# appear in all copies.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\"\"\"DNS Zones.\"\"\"\n\nfrom __future__ import generators\n\nimport sys\nimport re\nimport os\nfrom io import BytesIO\n\nimport dns.exception\nimport dns.name\nimport dns.node\nimport dns.rdataclass\nimport dns.rdatatype\nimport dns.rdata\nimport dns.rdtypes.ANY.SOA\nimport dns.rrset\nimport dns.tokenizer\nimport dns.ttl\nimport dns.grange\nfrom ._compat import string_types, text_type, PY3\n\n\nclass BadZone(dns.exception.DNSException):\n\n    \"\"\"The DNS zone is malformed.\"\"\"\n\n\nclass NoSOA(BadZone):\n\n    \"\"\"The DNS zone has no SOA RR at its origin.\"\"\"\n\n\nclass NoNS(BadZone):\n\n    \"\"\"The DNS zone has no NS RRset at its origin.\"\"\"\n\n\nclass UnknownOrigin(BadZone):\n\n    \"\"\"The DNS zone's origin is unknown.\"\"\"\n\n\nclass Zone(object):\n\n    \"\"\"A DNS zone.\n\n    A Zone is a mapping from names to nodes.  The zone object may be\n    treated like a Python dictionary, e.g. zone[name] will retrieve\n    the node associated with that name.  The I{name} may be a\n    dns.name.Name object, or it may be a string.  In the either case,\n    if the name is relative it is treated as relative to the origin of\n    the zone.\n\n    @ivar rdclass: The zone's rdata class; the default is class IN.\n    @type rdclass: int\n    @ivar origin: The origin of the zone.\n    @type origin: dns.name.Name object\n    @ivar nodes: A dictionary mapping the names of nodes in the zone to the\n    nodes themselves.\n    @type nodes: dict\n    @ivar relativize: should names in the zone be relativized?\n    @type relativize: bool\n    @cvar node_factory: the factory used to create a new node\n    @type node_factory: class or callable\n    \"\"\"\n\n    node_factory = dns.node.Node\n\n    __slots__ = ['rdclass', 'origin', 'nodes', 'relativize']\n\n    def __init__(self, origin, rdclass=dns.rdataclass.IN, relativize=True):\n        \"\"\"Initialize a zone object.\n\n        @param origin: The origin of the zone.\n        @type origin: dns.name.Name object\n        @param rdclass: The zone's rdata class; the default is class IN.\n        @type rdclass: int\"\"\"\n\n        if origin is not None:\n            if isinstance(origin, string_types):\n                origin = dns.name.from_text(origin)\n            elif not isinstance(origin, dns.name.Name):\n                raise ValueError(\"origin parameter must be convertible to a \"\n                                 \"DNS name\")\n            if not origin.is_absolute():\n                raise ValueError(\"origin parameter must be an absolute name\")\n        self.origin = origin\n        self.rdclass = rdclass\n        self.nodes = {}\n        self.relativize = relativize\n\n    def __eq__(self, other):\n        \"\"\"Two zones are equal if they have the same origin, class, and\n        nodes.\n        @rtype: bool\n        \"\"\"\n\n        if not isinstance(other, Zone):\n            return False\n        if self.rdclass != other.rdclass or \\\n           self.origin != other.origin or \\\n           self.nodes != other.nodes:\n            return False\n        return True\n\n    def __ne__(self, other):\n        \"\"\"Are two zones not equal?\n        @rtype: bool\n        \"\"\"\n\n        return not self.__eq__(other)\n\n    def _validate_name(self, name):\n        if isinstance(name, string_types):\n            name = dns.name.from_text(name, None)\n        elif not isinstance(name, dns.name.Name):\n            raise KeyError(\"name parameter must be convertible to a DNS name\")\n        if name.is_absolute():\n            if not name.is_subdomain(self.origin):\n                raise KeyError(\n                    \"name parameter must be a subdomain of the zone origin\")\n            if self.relativize:\n                name = name.relativize(self.origin)\n        return name\n\n    def __getitem__(self, key):\n        key = self._validate_name(key)\n        return self.nodes[key]\n\n    def __setitem__(self, key, value):\n        key = self._validate_name(key)\n        self.nodes[key] = value\n\n    def __delitem__(self, key):\n        key = self._validate_name(key)\n        del self.nodes[key]\n\n    def __iter__(self):\n        return self.nodes.__iter__()\n\n    def iterkeys(self):\n        if PY3:\n            return self.nodes.keys() # pylint: disable=dict-keys-not-iterating\n        else:\n            return self.nodes.iterkeys()  # pylint: disable=dict-iter-method\n\n    def keys(self):\n        return self.nodes.keys() # pylint: disable=dict-keys-not-iterating\n\n    def itervalues(self):\n        if PY3:\n            return self.nodes.values() # pylint: disable=dict-values-not-iterating\n        else:\n            return self.nodes.itervalues()  # pylint: disable=dict-iter-method\n\n    def values(self):\n        return self.nodes.values() # pylint: disable=dict-values-not-iterating\n\n    def items(self):\n        return self.nodes.items() # pylint: disable=dict-items-not-iterating\n\n    iteritems = items\n\n    def get(self, key):\n        key = self._validate_name(key)\n        return self.nodes.get(key)\n\n    def __contains__(self, other):\n        return other in self.nodes\n\n    def find_node(self, name, create=False):\n        \"\"\"Find a node in the zone, possibly creating it.\n\n        @param name: the name of the node to find\n        @type name: dns.name.Name object or string\n        @param create: should the node be created if it doesn't exist?\n        @type create: bool\n        @raises KeyError: the name is not known and create was not specified.\n        @rtype: dns.node.Node object\n        \"\"\"\n\n        name = self._validate_name(name)\n        node = self.nodes.get(name)\n        if node is None:\n            if not create:\n                raise KeyError\n            node = self.node_factory()\n            self.nodes[name] = node\n        return node\n\n    def get_node(self, name, create=False):\n        \"\"\"Get a node in the zone, possibly creating it.\n\n        This method is like L{find_node}, except it returns None instead\n        of raising an exception if the node does not exist and creation\n        has not been requested.\n\n        @param name: the name of the node to find\n        @type name: dns.name.Name object or string\n        @param create: should the node be created if it doesn't exist?\n        @type create: bool\n        @rtype: dns.node.Node object or None\n        \"\"\"\n\n        try:\n            node = self.find_node(name, create)\n        except KeyError:\n            node = None\n        return node\n\n    def delete_node(self, name):\n        \"\"\"Delete the specified node if it exists.\n\n        It is not an error if the node does not exist.\n        \"\"\"\n\n        name = self._validate_name(name)\n        if name in self.nodes:\n            del self.nodes[name]\n\n    def find_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,\n                      create=False):\n        \"\"\"Look for rdata with the specified name and type in the zone,\n        and return an rdataset encapsulating it.\n\n        The I{name}, I{rdtype}, and I{covers} parameters may be\n        strings, in which case they will be converted to their proper\n        type.\n\n        The rdataset returned is not a copy; changes to it will change\n        the zone.\n\n        KeyError is raised if the name or type are not found.\n        Use L{get_rdataset} if you want to have None returned instead.\n\n        @param name: the owner name to look for\n        @type name: DNS.name.Name object or string\n        @param rdtype: the rdata type desired\n        @type rdtype: int or string\n        @param covers: the covered type (defaults to None)\n        @type covers: int or string\n        @param create: should the node and rdataset be created if they do not\n        exist?\n        @type create: bool\n        @raises KeyError: the node or rdata could not be found\n        @rtype: dns.rdataset.Rdataset object\n        \"\"\"\n\n        name = self._validate_name(name)\n        if isinstance(rdtype, string_types):\n            rdtype = dns.rdatatype.from_text(rdtype)\n        if isinstance(covers, string_types):\n            covers = dns.rdatatype.from_text(covers)\n        node = self.find_node(name, create)\n        return node.find_rdataset(self.rdclass, rdtype, covers, create)\n\n    def get_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,\n                     create=False):\n        \"\"\"Look for rdata with the specified name and type in the zone,\n        and return an rdataset encapsulating it.\n\n        The I{name}, I{rdtype}, and I{covers} parameters may be\n        strings, in which case they will be converted to their proper\n        type.\n\n        The rdataset returned is not a copy; changes to it will change\n        the zone.\n\n        None is returned if the name or type are not found.\n        Use L{find_rdataset} if you want to have KeyError raised instead.\n\n        @param name: the owner name to look for\n        @type name: DNS.name.Name object or string\n        @param rdtype: the rdata type desired\n        @type rdtype: int or string\n        @param covers: the covered type (defaults to None)\n        @type covers: int or string\n        @param create: should the node and rdataset be created if they do not\n        exist?\n        @type create: bool\n        @rtype: dns.rdataset.Rdataset object or None\n        \"\"\"\n\n        try:\n            rdataset = self.find_rdataset(name, rdtype, covers, create)\n        except KeyError:\n            rdataset = None\n        return rdataset\n\n    def delete_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE):\n        \"\"\"Delete the rdataset matching I{rdtype} and I{covers}, if it\n        exists at the node specified by I{name}.\n\n        The I{name}, I{rdtype}, and I{covers} parameters may be\n        strings, in which case they will be converted to their proper\n        type.\n\n        It is not an error if the node does not exist, or if there is no\n        matching rdataset at the node.\n\n        If the node has no rdatasets after the deletion, it will itself\n        be deleted.\n\n        @param name: the owner name to look for\n        @type name: DNS.name.Name object or string\n        @param rdtype: the rdata type desired\n        @type rdtype: int or string\n        @param covers: the covered type (defaults to None)\n        @type covers: int or string\n        \"\"\"\n\n        name = self._validate_name(name)\n        if isinstance(rdtype, string_types):\n            rdtype = dns.rdatatype.from_text(rdtype)\n        if isinstance(covers, string_types):\n            covers = dns.rdatatype.from_text(covers)\n        node = self.get_node(name)\n        if node is not None:\n            node.delete_rdataset(self.rdclass, rdtype, covers)\n            if len(node) == 0:\n                self.delete_node(name)\n\n    def replace_rdataset(self, name, replacement):\n        \"\"\"Replace an rdataset at name.\n\n        It is not an error if there is no rdataset matching I{replacement}.\n\n        Ownership of the I{replacement} object is transferred to the zone;\n        in other words, this method does not store a copy of I{replacement}\n        at the node, it stores I{replacement} itself.\n\n        If the I{name} node does not exist, it is created.\n\n        @param name: the owner name\n        @type name: DNS.name.Name object or string\n        @param replacement: the replacement rdataset\n        @type replacement: dns.rdataset.Rdataset\n        \"\"\"\n\n        if replacement.rdclass != self.rdclass:\n            raise ValueError('replacement.rdclass != zone.rdclass')\n        node = self.find_node(name, True)\n        node.replace_rdataset(replacement)\n\n    def find_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):\n        \"\"\"Look for rdata with the specified name and type in the zone,\n        and return an RRset encapsulating it.\n\n        The I{name}, I{rdtype}, and I{covers} parameters may be\n        strings, in which case they will be converted to their proper\n        type.\n\n        This method is less efficient than the similar\n        L{find_rdataset} because it creates an RRset instead of\n        returning the matching rdataset.  It may be more convenient\n        for some uses since it returns an object which binds the owner\n        name to the rdata.\n\n        This method may not be used to create new nodes or rdatasets;\n        use L{find_rdataset} instead.\n\n        KeyError is raised if the name or type are not found.\n        Use L{get_rrset} if you want to have None returned instead.\n\n        @param name: the owner name to look for\n        @type name: DNS.name.Name object or string\n        @param rdtype: the rdata type desired\n        @type rdtype: int or string\n        @param covers: the covered type (defaults to None)\n        @type covers: int or string\n        @raises KeyError: the node or rdata could not be found\n        @rtype: dns.rrset.RRset object\n        \"\"\"\n\n        name = self._validate_name(name)\n        if isinstance(rdtype, string_types):\n            rdtype = dns.rdatatype.from_text(rdtype)\n        if isinstance(covers, string_types):\n            covers = dns.rdatatype.from_text(covers)\n        rdataset = self.nodes[name].find_rdataset(self.rdclass, rdtype, covers)\n        rrset = dns.rrset.RRset(name, self.rdclass, rdtype, covers)\n        rrset.update(rdataset)\n        return rrset\n\n    def get_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):\n        \"\"\"Look for rdata with the specified name and type in the zone,\n        and return an RRset encapsulating it.\n\n        The I{name}, I{rdtype}, and I{covers} parameters may be\n        strings, in which case they will be converted to their proper\n        type.\n\n        This method is less efficient than the similar L{get_rdataset}\n        because it creates an RRset instead of returning the matching\n        rdataset.  It may be more convenient for some uses since it\n        returns an object which binds the owner name to the rdata.\n\n        This method may not be used to create new nodes or rdatasets;\n        use L{find_rdataset} instead.\n\n        None is returned if the name or type are not found.\n        Use L{find_rrset} if you want to have KeyError raised instead.\n\n        @param name: the owner name to look for\n        @type name: DNS.name.Name object or string\n        @param rdtype: the rdata type desired\n        @type rdtype: int or string\n        @param covers: the covered type (defaults to None)\n        @type covers: int or string\n        @rtype: dns.rrset.RRset object\n        \"\"\"\n\n        try:\n            rrset = self.find_rrset(name, rdtype, covers)\n        except KeyError:\n            rrset = None\n        return rrset\n\n    def iterate_rdatasets(self, rdtype=dns.rdatatype.ANY,\n                          covers=dns.rdatatype.NONE):\n        \"\"\"Return a generator which yields (name, rdataset) tuples for\n        all rdatasets in the zone which have the specified I{rdtype}\n        and I{covers}.  If I{rdtype} is dns.rdatatype.ANY, the default,\n        then all rdatasets will be matched.\n\n        @param rdtype: int or string\n        @type rdtype: int or string\n        @param covers: the covered type (defaults to None)\n        @type covers: int or string\n        \"\"\"\n\n        if isinstance(rdtype, string_types):\n            rdtype = dns.rdatatype.from_text(rdtype)\n        if isinstance(covers, string_types):\n            covers = dns.rdatatype.from_text(covers)\n        for (name, node) in self.iteritems(): # pylint: disable=dict-iter-method\n            for rds in node:\n                if rdtype == dns.rdatatype.ANY or \\\n                   (rds.rdtype == rdtype and rds.covers == covers):\n                    yield (name, rds)\n\n    def iterate_rdatas(self, rdtype=dns.rdatatype.ANY,\n                       covers=dns.rdatatype.NONE):\n        \"\"\"Return a generator which yields (name, ttl, rdata) tuples for\n        all rdatas in the zone which have the specified I{rdtype}\n        and I{covers}.  If I{rdtype} is dns.rdatatype.ANY, the default,\n        then all rdatas will be matched.\n\n        @param rdtype: int or string\n        @type rdtype: int or string\n        @param covers: the covered type (defaults to None)\n        @type covers: int or string\n        \"\"\"\n\n        if isinstance(rdtype, string_types):\n            rdtype = dns.rdatatype.from_text(rdtype)\n        if isinstance(covers, string_types):\n            covers = dns.rdatatype.from_text(covers)\n        for (name, node) in self.iteritems(): # pylint: disable=dict-iter-method\n            for rds in node:\n                if rdtype == dns.rdatatype.ANY or \\\n                   (rds.rdtype == rdtype and rds.covers == covers):\n                    for rdata in rds:\n                        yield (name, rds.ttl, rdata)\n\n    def to_file(self, f, sorted=True, relativize=True, nl=None):\n        \"\"\"Write a zone to a file.\n\n        @param f: file or string.  If I{f} is a string, it is treated\n        as the name of a file to open.\n        @param sorted: if True, the file will be written with the\n        names sorted in DNSSEC order from least to greatest.  Otherwise\n        the names will be written in whatever order they happen to have\n        in the zone's dictionary.\n        @param relativize: if True, domain names in the output will be\n        relativized to the zone's origin (if possible).\n        @type relativize: bool\n        @param nl: The end of line string.  If not specified, the\n        output will use the platform's native end-of-line marker (i.e.\n        LF on POSIX, CRLF on Windows, CR on Macintosh).\n        @type nl: string or None\n        \"\"\"\n\n        if isinstance(f, string_types):\n            f = open(f, 'wb')\n            want_close = True\n        else:\n            want_close = False\n\n        # must be in this way, f.encoding may contain None, or even attribute\n        # may not be there\n        file_enc = getattr(f, 'encoding', None)\n        if file_enc is None:\n            file_enc = 'utf-8'\n\n        if nl is None:\n            nl_b = os.linesep.encode(file_enc)  # binary mode, '\\n' is not enough\n            nl = u'\\n'\n        elif isinstance(nl, string_types):\n            nl_b = nl.encode(file_enc)\n        else:\n            nl_b = nl\n            nl = nl.decode()\n\n        try:\n            if sorted:\n                names = list(self.keys())\n                names.sort()\n            else:\n                names = self.iterkeys() # pylint: disable=dict-iter-method\n            for n in names:\n                l = self[n].to_text(n, origin=self.origin,\n                                    relativize=relativize)\n                if isinstance(l, text_type):\n                    l_b = l.encode(file_enc)\n                else:\n                    l_b = l\n                    l = l.decode()\n\n                try:\n                    f.write(l_b)\n                    f.write(nl_b)\n                except TypeError:  # textual mode\n                    f.write(l)\n                    f.write(nl)\n        finally:\n            if want_close:\n                f.close()\n\n    def to_text(self, sorted=True, relativize=True, nl=None):\n        \"\"\"Return a zone's text as though it were written to a file.\n\n        @param sorted: if True, the file will be written with the\n        names sorted in DNSSEC order from least to greatest.  Otherwise\n        the names will be written in whatever order they happen to have\n        in the zone's dictionary.\n        @param relativize: if True, domain names in the output will be\n        relativized to the zone's origin (if possible).\n        @type relativize: bool\n        @param nl: The end of line string.  If not specified, the\n        output will use the platform's native end-of-line marker (i.e.\n        LF on POSIX, CRLF on Windows, CR on Macintosh).\n        @type nl: string or None\n        \"\"\"\n        temp_buffer = BytesIO()\n        self.to_file(temp_buffer, sorted, relativize, nl)\n        return_value = temp_buffer.getvalue()\n        temp_buffer.close()\n        return return_value\n\n    def check_origin(self):\n        \"\"\"Do some simple checking of the zone's origin.\n\n        @raises dns.zone.NoSOA: there is no SOA RR\n        @raises dns.zone.NoNS: there is no NS RRset\n        @raises KeyError: there is no origin node\n        \"\"\"\n        if self.relativize:\n            name = dns.name.empty\n        else:\n            name = self.origin\n        if self.get_rdataset(name, dns.rdatatype.SOA) is None:\n            raise NoSOA\n        if self.get_rdataset(name, dns.rdatatype.NS) is None:\n            raise NoNS\n\n\nclass _MasterReader(object):\n\n    \"\"\"Read a DNS master file\n\n    @ivar tok: The tokenizer\n    @type tok: dns.tokenizer.Tokenizer object\n    @ivar last_ttl: The last seen explicit TTL for an RR\n    @type last_ttl: int\n    @ivar last_ttl_known: Has last TTL been detected\n    @type last_ttl_known: bool\n    @ivar default_ttl: The default TTL from a $TTL directive or SOA RR\n    @type default_ttl: int\n    @ivar default_ttl_known: Has default TTL been detected\n    @type default_ttl_known: bool\n    @ivar last_name: The last name read\n    @type last_name: dns.name.Name object\n    @ivar current_origin: The current origin\n    @type current_origin: dns.name.Name object\n    @ivar relativize: should names in the zone be relativized?\n    @type relativize: bool\n    @ivar zone: the zone\n    @type zone: dns.zone.Zone object\n    @ivar saved_state: saved reader state (used when processing $INCLUDE)\n    @type saved_state: list of (tokenizer, current_origin, last_name, file,\n    last_ttl, last_ttl_known, default_ttl, default_ttl_known) tuples.\n    @ivar current_file: the file object of the $INCLUDed file being parsed\n    (None if no $INCLUDE is active).\n    @ivar allow_include: is $INCLUDE allowed?\n    @type allow_include: bool\n    @ivar check_origin: should sanity checks of the origin node be done?\n    The default is True.\n    @type check_origin: bool\n    \"\"\"\n\n    def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone,\n                 allow_include=False, check_origin=True):\n        if isinstance(origin, string_types):\n            origin = dns.name.from_text(origin)\n        self.tok = tok\n        self.current_origin = origin\n        self.relativize = relativize\n        self.last_ttl = 0\n        self.last_ttl_known = False\n        self.default_ttl = 0\n        self.default_ttl_known = False\n        self.last_name = self.current_origin\n        self.zone = zone_factory(origin, rdclass, relativize=relativize)\n        self.saved_state = []\n        self.current_file = None\n        self.allow_include = allow_include\n        self.check_origin = check_origin\n\n    def _eat_line(self):\n        while 1:\n            token = self.tok.get()\n            if token.is_eol_or_eof():\n                break\n\n    def _rr_line(self):\n        \"\"\"Process one line from a DNS master file.\"\"\"\n        # Name\n        if self.current_origin is None:\n            raise UnknownOrigin\n        token = self.tok.get(want_leading=True)\n        if not token.is_whitespace():\n            self.last_name = dns.name.from_text(\n                token.value, self.current_origin)\n        else:\n            token = self.tok.get()\n            if token.is_eol_or_eof():\n                # treat leading WS followed by EOL/EOF as if they were EOL/EOF.\n                return\n            self.tok.unget(token)\n        name = self.last_name\n        if not name.is_subdomain(self.zone.origin):\n            self._eat_line()\n            return\n        if self.relativize:\n            name = name.relativize(self.zone.origin)\n        token = self.tok.get()\n        if not token.is_identifier():\n            raise dns.exception.SyntaxError\n        # TTL\n        try:\n            ttl = dns.ttl.from_text(token.value)\n            self.last_ttl = ttl\n            self.last_ttl_known = True\n            token = self.tok.get()\n            if not token.is_identifier():\n                raise dns.exception.SyntaxError\n        except dns.ttl.BadTTL:\n            if not (self.last_ttl_known or self.default_ttl_known):\n                raise dns.exception.SyntaxError(\"Missing default TTL value\")\n            if self.default_ttl_known:\n                ttl = self.default_ttl\n            else:\n                ttl = self.last_ttl\n        # Class\n        try:\n            rdclass = dns.rdataclass.from_text(token.value)\n            token = self.tok.get()\n            if not token.is_identifier():\n                raise dns.exception.SyntaxError\n        except dns.exception.SyntaxError:\n            raise dns.exception.SyntaxError\n        except Exception:\n            rdclass = self.zone.rdclass\n        if rdclass != self.zone.rdclass:\n            raise dns.exception.SyntaxError(\"RR class is not zone's class\")\n        # Type\n        try:\n            rdtype = dns.rdatatype.from_text(token.value)\n        except:\n            raise dns.exception.SyntaxError(\n                \"unknown rdatatype '%s'\" % token.value)\n        n = self.zone.nodes.get(name)\n        if n is None:\n            n = self.zone.node_factory()\n            self.zone.nodes[name] = n\n        try:\n            rd = dns.rdata.from_text(rdclass, rdtype, self.tok,\n                                     self.current_origin, False)\n        except dns.exception.SyntaxError:\n            # Catch and reraise.\n            (ty, va) = sys.exc_info()[:2]\n            raise va\n        except:\n            # All exceptions that occur in the processing of rdata\n            # are treated as syntax errors.  This is not strictly\n            # correct, but it is correct almost all of the time.\n            # We convert them to syntax errors so that we can emit\n            # helpful filename:line info.\n            (ty, va) = sys.exc_info()[:2]\n            raise dns.exception.SyntaxError(\n                \"caught exception {}: {}\".format(str(ty), str(va)))\n\n        if not self.default_ttl_known and isinstance(rd, dns.rdtypes.ANY.SOA.SOA):\n            # The pre-RFC2308 and pre-BIND9 behavior inherits the zone default\n            # TTL from the SOA minttl if no $TTL statement is present before the\n            # SOA is parsed.\n            self.default_ttl = rd.minimum\n            self.default_ttl_known = True\n\n        rd.choose_relativity(self.zone.origin, self.relativize)\n        covers = rd.covers()\n        rds = n.find_rdataset(rdclass, rdtype, covers, True)\n        rds.add(rd, ttl)\n\n    def _parse_modify(self, side):\n        # Here we catch everything in '{' '}' in a group so we can replace it\n        # with ''.\n        is_generate1 = re.compile(\"^.*\\$({(\\+|-?)(\\d+),(\\d+),(.)}).*$\")\n        is_generate2 = re.compile(\"^.*\\$({(\\+|-?)(\\d+)}).*$\")\n        is_generate3 = re.compile(\"^.*\\$({(\\+|-?)(\\d+),(\\d+)}).*$\")\n        # Sometimes there are modifiers in the hostname. These come after\n        # the dollar sign. They are in the form: ${offset[,width[,base]]}.\n        # Make names\n        g1 = is_generate1.match(side)\n        if g1:\n            mod, sign, offset, width, base = g1.groups()\n            if sign == '':\n                sign = '+'\n        g2 = is_generate2.match(side)\n        if g2:\n            mod, sign, offset = g2.groups()\n            if sign == '':\n                sign = '+'\n            width = 0\n            base = 'd'\n        g3 = is_generate3.match(side)\n        if g3:\n            mod, sign, offset, width = g1.groups()\n            if sign == '':\n                sign = '+'\n            width = g1.groups()[2]\n            base = 'd'\n\n        if not (g1 or g2 or g3):\n            mod = ''\n            sign = '+'\n            offset = 0\n            width = 0\n            base = 'd'\n\n        if base != 'd':\n            raise NotImplementedError()\n\n        return mod, sign, offset, width, base\n\n    def _generate_line(self):\n        # range lhs [ttl] [class] type rhs [ comment ]\n        \"\"\"Process one line containing the GENERATE statement from a DNS\n        master file.\"\"\"\n        if self.current_origin is None:\n            raise UnknownOrigin\n\n        token = self.tok.get()\n        # Range (required)\n        try:\n            start, stop, step = dns.grange.from_text(token.value)\n            token = self.tok.get()\n            if not token.is_identifier():\n                raise dns.exception.SyntaxError\n        except:\n            raise dns.exception.SyntaxError\n\n        # lhs (required)\n        try:\n            lhs = token.value\n            token = self.tok.get()\n            if not token.is_identifier():\n                raise dns.exception.SyntaxError\n        except:\n            raise dns.exception.SyntaxError\n\n        # TTL\n        try:\n            ttl = dns.ttl.from_text(token.value)\n            self.last_ttl = ttl\n            self.last_ttl_known = True\n            token = self.tok.get()\n            if not token.is_identifier():\n                raise dns.exception.SyntaxError\n        except dns.ttl.BadTTL:\n            if not (self.last_ttl_known or self.default_ttl_known):\n                raise dns.exception.SyntaxError(\"Missing default TTL value\")\n            if self.default_ttl_known:\n                ttl = self.default_ttl\n            else:\n                ttl = self.last_ttl\n        # Class\n        try:\n            rdclass = dns.rdataclass.from_text(token.value)\n            token = self.tok.get()\n            if not token.is_identifier():\n                raise dns.exception.SyntaxError\n        except dns.exception.SyntaxError:\n            raise dns.exception.SyntaxError\n        except Exception:\n            rdclass = self.zone.rdclass\n        if rdclass != self.zone.rdclass:\n            raise dns.exception.SyntaxError(\"RR class is not zone's class\")\n        # Type\n        try:\n            rdtype = dns.rdatatype.from_text(token.value)\n            token = self.tok.get()\n            if not token.is_identifier():\n                raise dns.exception.SyntaxError\n        except Exception:\n            raise dns.exception.SyntaxError(\"unknown rdatatype '%s'\" %\n                                            token.value)\n\n        # lhs (required)\n        try:\n            rhs = token.value\n        except:\n            raise dns.exception.SyntaxError\n\n        lmod, lsign, loffset, lwidth, lbase = self._parse_modify(lhs)\n        rmod, rsign, roffset, rwidth, rbase = self._parse_modify(rhs)\n        for i in range(start, stop + 1, step):\n            # +1 because bind is inclusive and python is exclusive\n\n            if lsign == u'+':\n                lindex = i + int(loffset)\n            elif lsign == u'-':\n                lindex = i - int(loffset)\n\n            if rsign == u'-':\n                rindex = i - int(roffset)\n            elif rsign == u'+':\n                rindex = i + int(roffset)\n\n            lzfindex = str(lindex).zfill(int(lwidth))\n            rzfindex = str(rindex).zfill(int(rwidth))\n\n            name = lhs.replace(u'$%s' % (lmod), lzfindex)\n            rdata = rhs.replace(u'$%s' % (rmod), rzfindex)\n\n            self.last_name = dns.name.from_text(name, self.current_origin)\n            name = self.last_name\n            if not name.is_subdomain(self.zone.origin):\n                self._eat_line()\n                return\n            if self.relativize:\n                name = name.relativize(self.zone.origin)\n\n            n = self.zone.nodes.get(name)\n            if n is None:\n                n = self.zone.node_factory()\n                self.zone.nodes[name] = n\n            try:\n                rd = dns.rdata.from_text(rdclass, rdtype, rdata,\n                                         self.current_origin, False)\n            except dns.exception.SyntaxError:\n                # Catch and reraise.\n                (ty, va) = sys.exc_info()[:2]\n                raise va\n            except:\n                # All exceptions that occur in the processing of rdata\n                # are treated as syntax errors.  This is not strictly\n                # correct, but it is correct almost all of the time.\n                # We convert them to syntax errors so that we can emit\n                # helpful filename:line info.\n                (ty, va) = sys.exc_info()[:2]\n                raise dns.exception.SyntaxError(\"caught exception %s: %s\" %\n                                                (str(ty), str(va)))\n\n            rd.choose_relativity(self.zone.origin, self.relativize)\n            covers = rd.covers()\n            rds = n.find_rdataset(rdclass, rdtype, covers, True)\n            rds.add(rd, ttl)\n\n    def read(self):\n        \"\"\"Read a DNS master file and build a zone object.\n\n        @raises dns.zone.NoSOA: No SOA RR was found at the zone origin\n        @raises dns.zone.NoNS: No NS RRset was found at the zone origin\n        \"\"\"\n\n        try:\n            while 1:\n                token = self.tok.get(True, True)\n                if token.is_eof():\n                    if self.current_file is not None:\n                        self.current_file.close()\n                    if len(self.saved_state) > 0:\n                        (self.tok,\n                         self.current_origin,\n                         self.last_name,\n                         self.current_file,\n                         self.last_ttl,\n                         self.last_ttl_known,\n                         self.default_ttl,\n                         self.default_ttl_known) = self.saved_state.pop(-1)\n                        continue\n                    break\n                elif token.is_eol():\n                    continue\n                elif token.is_comment():\n                    self.tok.get_eol()\n                    continue\n                elif token.value[0] == u'$':\n                    c = token.value.upper()\n                    if c == u'$TTL':\n                        token = self.tok.get()\n                        if not token.is_identifier():\n                            raise dns.exception.SyntaxError(\"bad $TTL\")\n                        self.default_ttl = dns.ttl.from_text(token.value)\n                        self.default_ttl_known = True\n                        self.tok.get_eol()\n                    elif c == u'$ORIGIN':\n                        self.current_origin = self.tok.get_name()\n                        self.tok.get_eol()\n                        if self.zone.origin is None:\n                            self.zone.origin = self.current_origin\n                    elif c == u'$INCLUDE' and self.allow_include:\n                        token = self.tok.get()\n                        filename = token.value\n                        token = self.tok.get()\n                        if token.is_identifier():\n                            new_origin =\\\n                                dns.name.from_text(token.value,\n                                                   self.current_origin)\n                            self.tok.get_eol()\n                        elif not token.is_eol_or_eof():\n                            raise dns.exception.SyntaxError(\n                                \"bad origin in $INCLUDE\")\n                        else:\n                            new_origin = self.current_origin\n                        self.saved_state.append((self.tok,\n                                                 self.current_origin,\n                                                 self.last_name,\n                                                 self.current_file,\n                                                 self.last_ttl,\n                                                 self.last_ttl_known,\n                                                 self.default_ttl,\n                                                 self.default_ttl_known))\n                        self.current_file = open(filename, 'r')\n                        self.tok = dns.tokenizer.Tokenizer(self.current_file,\n                                                           filename)\n                        self.current_origin = new_origin\n                    elif c == u'$GENERATE':\n                        self._generate_line()\n                    else:\n                        raise dns.exception.SyntaxError(\n                            \"Unknown master file directive '\" + c + \"'\")\n                    continue\n                self.tok.unget(token)\n                self._rr_line()\n        except dns.exception.SyntaxError as detail:\n            (filename, line_number) = self.tok.where()\n            if detail is None:\n                detail = \"syntax error\"\n            raise dns.exception.SyntaxError(\n                \"%s:%d: %s\" % (filename, line_number, detail))\n\n        # Now that we're done reading, do some basic checking of the zone.\n        if self.check_origin:\n            self.zone.check_origin()\n\n\ndef from_text(text, origin=None, rdclass=dns.rdataclass.IN,\n              relativize=True, zone_factory=Zone, filename=None,\n              allow_include=False, check_origin=True):\n    \"\"\"Build a zone object from a master file format string.\n\n    @param text: the master file format input\n    @type text: string.\n    @param origin: The origin of the zone; if not specified, the first\n    $ORIGIN statement in the master file will determine the origin of the\n    zone.\n    @type origin: dns.name.Name object or string\n    @param rdclass: The zone's rdata class; the default is class IN.\n    @type rdclass: int\n    @param relativize: should names be relativized?  The default is True\n    @type relativize: bool\n    @param zone_factory: The zone factory to use\n    @type zone_factory: function returning a Zone\n    @param filename: The filename to emit when describing where an error\n    occurred; the default is '<string>'.\n    @type filename: string\n    @param allow_include: is $INCLUDE allowed?\n    @type allow_include: bool\n    @param check_origin: should sanity checks of the origin node be done?\n    The default is True.\n    @type check_origin: bool\n    @raises dns.zone.NoSOA: No SOA RR was found at the zone origin\n    @raises dns.zone.NoNS: No NS RRset was found at the zone origin\n    @rtype: dns.zone.Zone object\n    \"\"\"\n\n    # 'text' can also be a file, but we don't publish that fact\n    # since it's an implementation detail.  The official file\n    # interface is from_file().\n\n    if filename is None:\n        filename = '<string>'\n    tok = dns.tokenizer.Tokenizer(text, filename)\n    reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory,\n                           allow_include=allow_include,\n                           check_origin=check_origin)\n    reader.read()\n    return reader.zone\n\n\ndef from_file(f, origin=None, rdclass=dns.rdataclass.IN,\n              relativize=True, zone_factory=Zone, filename=None,\n              allow_include=True, check_origin=True):\n    \"\"\"Read a master file and build a zone object.\n\n    @param f: file or string.  If I{f} is a string, it is treated\n    as the name of a file to open.\n    @param origin: The origin of the zone; if not specified, the first\n    $ORIGIN statement in the master file will determine the origin of the\n    zone.\n    @type origin: dns.name.Name object or string\n    @param rdclass: The zone's rdata class; the default is class IN.\n    @type rdclass: int\n    @param relativize: should names be relativized?  The default is True\n    @type relativize: bool\n    @param zone_factory: The zone factory to use\n    @type zone_factory: function returning a Zone\n    @param filename: The filename to emit when describing where an error\n    occurred; the default is '<file>', or the value of I{f} if I{f} is a\n    string.\n    @type filename: string\n    @param allow_include: is $INCLUDE allowed?\n    @type allow_include: bool\n    @param check_origin: should sanity checks of the origin node be done?\n    The default is True.\n    @type check_origin: bool\n    @raises dns.zone.NoSOA: No SOA RR was found at the zone origin\n    @raises dns.zone.NoNS: No NS RRset was found at the zone origin\n    @rtype: dns.zone.Zone object\n    \"\"\"\n\n    str_type = string_types\n    if PY3:\n        opts = 'r'\n    else:\n        opts = 'rU'\n\n    if isinstance(f, str_type):\n        if filename is None:\n            filename = f\n        f = open(f, opts)\n        want_close = True\n    else:\n        if filename is None:\n            filename = '<file>'\n        want_close = False\n\n    try:\n        z = from_text(f, origin, rdclass, relativize, zone_factory,\n                      filename, allow_include, check_origin)\n    finally:\n        if want_close:\n            f.close()\n    return z\n\n\ndef from_xfr(xfr, zone_factory=Zone, relativize=True, check_origin=True):\n    \"\"\"Convert the output of a zone transfer generator into a zone object.\n\n    @param xfr: The xfr generator\n    @type xfr: generator of dns.message.Message objects\n    @param relativize: should names be relativized?  The default is True.\n    It is essential that the relativize setting matches the one specified\n    to dns.query.xfr().\n    @type relativize: bool\n    @param check_origin: should sanity checks of the origin node be done?\n    The default is True.\n    @type check_origin: bool\n    @raises dns.zone.NoSOA: No SOA RR was found at the zone origin\n    @raises dns.zone.NoNS: No NS RRset was found at the zone origin\n    @rtype: dns.zone.Zone object\n    \"\"\"\n\n    z = None\n    for r in xfr:\n        if z is None:\n            if relativize:\n                origin = r.origin\n            else:\n                origin = r.answer[0].name\n            rdclass = r.answer[0].rdclass\n            z = zone_factory(origin, rdclass, relativize=relativize)\n        for rrset in r.answer:\n            znode = z.nodes.get(rrset.name)\n            if not znode:\n                znode = z.node_factory()\n                z.nodes[rrset.name] = znode\n            zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype,\n                                       rrset.covers, True)\n            zrds.update_ttl(rrset.ttl)\n            for rd in rrset:\n                rd.choose_relativity(z.origin, relativize)\n                zrds.add(rd)\n    if check_origin:\n        z.check_origin()\n    return z\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/__init__.py",
    "content": "import sys\n\nfrom .client import Client\nfrom .middleware import WSGIApp, Middleware\nfrom .server import Server\nif sys.version_info >= (3, 5):  # pragma: no cover\n    from .asyncio_server import AsyncServer\n    from .asyncio_client import AsyncClient\n    from .async_drivers.asgi import ASGIApp\n    try:\n        from .async_drivers.tornado import get_tornado_handler\n    except ImportError:\n        get_tornado_handler = None\nelse:  # pragma: no cover\n    AsyncServer = None\n    AsyncClient = None\n    get_tornado_handler = None\n    ASGIApp = None\n\n__version__ = '3.8.2.post1'\n\n__all__ = ['__version__', 'Server', 'WSGIApp', 'Middleware', 'Client']\nif AsyncServer is not None:  # pragma: no cover\n    __all__ += ['AsyncServer', 'ASGIApp', 'get_tornado_handler',\n                'AsyncClient'],\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_aiohttp.py",
    "content": "import asyncio\nimport sys\nfrom urllib.parse import urlsplit\n\nfrom aiohttp.web import Response, WebSocketResponse\nimport six\n\n\ndef create_route(app, engineio_server, engineio_endpoint):\n    \"\"\"This function sets up the engine.io endpoint as a route for the\n    application.\n\n    Note that both GET and POST requests must be hooked up on the engine.io\n    endpoint.\n    \"\"\"\n    app.router.add_get(engineio_endpoint, engineio_server.handle_request)\n    app.router.add_post(engineio_endpoint, engineio_server.handle_request)\n    app.router.add_route('OPTIONS', engineio_endpoint,\n                         engineio_server.handle_request)\n\n\ndef translate_request(request):\n    \"\"\"This function takes the arguments passed to the request handler and\n    uses them to generate a WSGI compatible environ dictionary.\n    \"\"\"\n    message = request._message\n    payload = request._payload\n\n    uri_parts = urlsplit(message.path)\n    environ = {\n        'wsgi.input': payload,\n        'wsgi.errors': sys.stderr,\n        'wsgi.version': (1, 0),\n        'wsgi.async': True,\n        'wsgi.multithread': False,\n        'wsgi.multiprocess': False,\n        'wsgi.run_once': False,\n        'SERVER_SOFTWARE': 'aiohttp',\n        'REQUEST_METHOD': message.method,\n        'QUERY_STRING': uri_parts.query or '',\n        'RAW_URI': message.path,\n        'SERVER_PROTOCOL': 'HTTP/%s.%s' % message.version,\n        'REMOTE_ADDR': '127.0.0.1',\n        'REMOTE_PORT': '0',\n        'SERVER_NAME': 'aiohttp',\n        'SERVER_PORT': '0',\n        'aiohttp.request': request\n    }\n\n    for hdr_name, hdr_value in message.headers.items():\n        hdr_name = hdr_name.upper()\n        if hdr_name == 'CONTENT-TYPE':\n            environ['CONTENT_TYPE'] = hdr_value\n            continue\n        elif hdr_name == 'CONTENT-LENGTH':\n            environ['CONTENT_LENGTH'] = hdr_value\n            continue\n\n        key = 'HTTP_%s' % hdr_name.replace('-', '_')\n        if key in environ:\n            hdr_value = '%s,%s' % (environ[key], hdr_value)\n\n        environ[key] = hdr_value\n\n    environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http')\n\n    path_info = uri_parts.path\n\n    environ['PATH_INFO'] = path_info\n    environ['SCRIPT_NAME'] = ''\n\n    return environ\n\n\ndef make_response(status, headers, payload, environ):\n    \"\"\"This function generates an appropriate response object for this async\n    mode.\n    \"\"\"\n    return Response(body=payload, status=int(status.split()[0]),\n                    headers=headers)\n\n\nclass WebSocket(object):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides a aiohttp WebSocket interface that is\n    somewhat compatible with eventlet's implementation.\n    \"\"\"\n    def __init__(self, handler):\n        self.handler = handler\n        self._sock = None\n\n    async def __call__(self, environ):\n        request = environ['aiohttp.request']\n        self._sock = WebSocketResponse()\n        await self._sock.prepare(request)\n\n        self.environ = environ\n        await self.handler(self)\n        return self._sock\n\n    async def close(self):\n        await self._sock.close()\n\n    async def send(self, message):\n        if isinstance(message, bytes):\n            f = self._sock.send_bytes\n        else:\n            f = self._sock.send_str\n        if asyncio.iscoroutinefunction(f):\n            await f(message)\n        else:\n            f(message)\n\n    async def wait(self):\n        msg = await self._sock.receive()\n        if not isinstance(msg.data, six.binary_type) and \\\n                not isinstance(msg.data, six.text_type):\n            raise IOError()\n        return msg.data\n\n\n_async = {\n    'asyncio': True,\n    'create_route': create_route,\n    'translate_request': translate_request,\n    'make_response': make_response,\n    'websocket': sys.modules[__name__],\n    'websocket_class': 'WebSocket'\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_asgi.py",
    "content": "import sys\n\n\nclass ASGIApp:\n    \"\"\"ASGI application middleware for Engine.IO.\n\n    This middleware dispatches traffic to an Engine.IO application. It can\n    also serve a list of static files to the client, or forward unrelated\n    HTTP traffic to another ASGI application.\n\n    :param engineio_server: The Engine.IO server. Must be an instance of the\n                            ``engineio.AsyncServer`` class.\n    :param static_files: A dictionary where the keys are URLs that should be\n                         served as static files. For each URL, the value is\n                         a dictionary with ``content_type`` and ``filename``\n                         keys. This option is intended to be used for serving\n                         client files during development.\n    :param other_asgi_app: A separate ASGI app that receives all other traffic.\n    :param engineio_path: The endpoint where the Engine.IO application should\n                          be installed. The default value is appropriate for\n                          most cases.\n\n    Example usage::\n\n        import engineio\n        import uvicorn\n\n        eio = engineio.AsyncServer()\n        app = engineio.ASGIApp(eio, static_files={\n            '/': {'content_type': 'text/html', 'filename': 'index.html'},\n            '/index.html': {'content_type': 'text/html',\n                            'filename': 'index.html'},\n        })\n        uvicorn.run(app, '127.0.0.1', 5000)\n    \"\"\"\n    def __init__(self, engineio_server, other_asgi_app=None,\n                 static_files=None, engineio_path='engine.io'):\n        self.engineio_server = engineio_server\n        self.other_asgi_app = other_asgi_app\n        self.engineio_path = engineio_path.strip('/')\n        self.static_files = static_files or {}\n\n    def __call__(self, scope):\n        if scope['type'] in ['http', 'websocket'] and \\\n                scope['path'].startswith('/{0}/'.format(self.engineio_path)):\n            return self.engineio_asgi_app(scope)\n        elif scope['type'] == 'http' and scope['path'] in self.static_files:\n            return self.serve_static_file(scope)\n        elif self.other_asgi_app is not None:\n            return self.other_asgi_app(scope)\n        elif scope['type'] == 'lifespan':\n            return self.lifespan\n        else:\n            return self.not_found\n\n    def engineio_asgi_app(self, scope):\n        async def _app(receive, send):\n            await self.engineio_server.handle_request(scope, receive, send)\n        return _app\n\n    def serve_static_file(self, scope):\n        async def _send_static_file(receive, send):  # pragma: no cover\n            event = await receive()\n            if event['type'] == 'http.request':\n                if scope['path'] in self.static_files:\n                    content_type = self.static_files[scope['path']][\n                        'content_type'].encode('utf-8')\n                    filename = self.static_files[scope['path']]['filename']\n                    status_code = 200\n                    with open(filename, 'rb') as f:\n                        payload = f.read()\n                else:\n                    content_type = b'text/plain'\n                    status_code = 404\n                    payload = b'not found'\n                await send({'type': 'http.response.start',\n                            'status': status_code,\n                            'headers': [(b'Content-Type', content_type)]})\n                await send({'type': 'http.response.body',\n                            'body': payload})\n        return _send_static_file\n\n    async def lifespan(self, receive, send):\n        event = await receive()\n        if event['type'] == 'lifespan.startup':\n            await send({'type': 'lifespan.startup.complete'})\n        elif event['type'] == 'lifespan.shutdown':\n            await send({'type': 'lifespan.shutdown.complete'})\n\n    async def not_found(self, receive, send):\n        \"\"\"Return a 404 Not Found error to the client.\"\"\"\n        await send({'type': 'http.response.start',\n                    'status': 404,\n                    'headers': [(b'Content-Type', b'text/plain')]})\n        await send({'type': 'http.response.body',\n                    'body': b'not found'})\n\n\nasync def translate_request(scope, receive, send):\n    class AwaitablePayload(object):  # pragma: no cover\n        def __init__(self, payload):\n            self.payload = payload or b''\n\n        async def read(self, length=None):\n            if length is None:\n                r = self.payload\n                self.payload = b''\n            else:\n                r = self.payload[:length]\n                self.payload = self.payload[length:]\n            return r\n\n    event = await receive()\n    payload = b''\n    if event['type'] == 'http.request':\n        payload += event.get('body') or b''\n        while event.get('more_body'):\n            event = await receive()\n            if event['type'] == 'http.request':\n                payload += event.get('body') or b''\n    elif event['type'] == 'websocket.connect':\n        await send({'type': 'websocket.accept'})\n    else:\n        return {}\n\n    raw_uri = scope['path'].encode('utf-8')\n    if 'query_string' in scope and scope['query_string']:\n        raw_uri += b'?' + scope['query_string']\n    environ = {\n        'wsgi.input': AwaitablePayload(payload),\n        'wsgi.errors': sys.stderr,\n        'wsgi.version': (1, 0),\n        'wsgi.async': True,\n        'wsgi.multithread': False,\n        'wsgi.multiprocess': False,\n        'wsgi.run_once': False,\n        'SERVER_SOFTWARE': 'asgi',\n        'REQUEST_METHOD': scope.get('method', 'GET'),\n        'PATH_INFO': scope['path'],\n        'QUERY_STRING': scope.get('query_string', b'').decode('utf-8'),\n        'RAW_URI': raw_uri.decode('utf-8'),\n        'SCRIPT_NAME': '',\n        'SERVER_PROTOCOL': 'HTTP/1.1',\n        'REMOTE_ADDR': '127.0.0.1',\n        'REMOTE_PORT': '0',\n        'SERVER_NAME': 'asgi',\n        'SERVER_PORT': '0',\n        'asgi.receive': receive,\n        'asgi.send': send,\n    }\n\n    for hdr_name, hdr_value in scope['headers']:\n        hdr_name = hdr_name.upper().decode('utf-8')\n        hdr_value = hdr_value.decode('utf-8')\n        if hdr_name == 'CONTENT-TYPE':\n            environ['CONTENT_TYPE'] = hdr_value\n            continue\n        elif hdr_name == 'CONTENT-LENGTH':\n            environ['CONTENT_LENGTH'] = hdr_value\n            continue\n\n        key = 'HTTP_%s' % hdr_name.replace('-', '_')\n        if key in environ:\n            hdr_value = '%s,%s' % (environ[key], hdr_value)\n\n        environ[key] = hdr_value\n\n    environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http')\n    return environ\n\n\nasync def make_response(status, headers, payload, environ):\n    headers = [(h[0].encode('utf-8'), h[1].encode('utf-8')) for h in headers]\n    await environ['asgi.send']({'type': 'http.response.start',\n                                'status': int(status.split(' ')[0]),\n                                'headers': headers})\n    await environ['asgi.send']({'type': 'http.response.body',\n                                'body': payload})\n\n\nclass WebSocket(object):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides an asgi WebSocket interface that is\n    somewhat compatible with eventlet's implementation.\n    \"\"\"\n    def __init__(self, handler):\n        self.handler = handler\n        self.asgi_receive = None\n        self.asgi_send = None\n\n    async def __call__(self, environ):\n        self.asgi_receive = environ['asgi.receive']\n        self.asgi_send = environ['asgi.send']\n        await self.handler(self)\n\n    async def close(self):\n        await self.asgi_send({'type': 'websocket.close'})\n\n    async def send(self, message):\n        msg_bytes = None\n        msg_text = None\n        if isinstance(message, bytes):\n            msg_bytes = message\n        else:\n            msg_text = message\n        await self.asgi_send({'type': 'websocket.send',\n                              'bytes': msg_bytes,\n                              'text': msg_text})\n\n    async def wait(self):\n        event = await self.asgi_receive()\n        if event['type'] != 'websocket.receive':\n            raise IOError()\n        return event.get('bytes') or event.get('text')\n\n\n_async = {\n    'asyncio': True,\n    'translate_request': translate_request,\n    'make_response': make_response,\n    'websocket': sys.modules[__name__],\n    'websocket_class': 'WebSocket'\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_drivers/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_drivers/aiohttp.py",
    "content": "import asyncio\nimport sys\nfrom urllib.parse import urlsplit\n\nfrom aiohttp.web import Response, WebSocketResponse\nimport six\n\n\ndef create_route(app, engineio_server, engineio_endpoint):\n    \"\"\"This function sets up the engine.io endpoint as a route for the\n    application.\n\n    Note that both GET and POST requests must be hooked up on the engine.io\n    endpoint.\n    \"\"\"\n    app.router.add_get(engineio_endpoint, engineio_server.handle_request)\n    app.router.add_post(engineio_endpoint, engineio_server.handle_request)\n    app.router.add_route('OPTIONS', engineio_endpoint,\n                         engineio_server.handle_request)\n\n\ndef translate_request(request):\n    \"\"\"This function takes the arguments passed to the request handler and\n    uses them to generate a WSGI compatible environ dictionary.\n    \"\"\"\n    message = request._message\n    payload = request._payload\n\n    uri_parts = urlsplit(message.path)\n    environ = {\n        'wsgi.input': payload,\n        'wsgi.errors': sys.stderr,\n        'wsgi.version': (1, 0),\n        'wsgi.async': True,\n        'wsgi.multithread': False,\n        'wsgi.multiprocess': False,\n        'wsgi.run_once': False,\n        'SERVER_SOFTWARE': 'aiohttp',\n        'REQUEST_METHOD': message.method,\n        'QUERY_STRING': uri_parts.query or '',\n        'RAW_URI': message.path,\n        'SERVER_PROTOCOL': 'HTTP/%s.%s' % message.version,\n        'REMOTE_ADDR': '127.0.0.1',\n        'REMOTE_PORT': '0',\n        'SERVER_NAME': 'aiohttp',\n        'SERVER_PORT': '0',\n        'aiohttp.request': request\n    }\n\n    for hdr_name, hdr_value in message.headers.items():\n        hdr_name = hdr_name.upper()\n        if hdr_name == 'CONTENT-TYPE':\n            environ['CONTENT_TYPE'] = hdr_value\n            continue\n        elif hdr_name == 'CONTENT-LENGTH':\n            environ['CONTENT_LENGTH'] = hdr_value\n            continue\n\n        key = 'HTTP_%s' % hdr_name.replace('-', '_')\n        if key in environ:\n            hdr_value = '%s,%s' % (environ[key], hdr_value)\n\n        environ[key] = hdr_value\n\n    environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http')\n\n    path_info = uri_parts.path\n\n    environ['PATH_INFO'] = path_info\n    environ['SCRIPT_NAME'] = ''\n\n    return environ\n\n\ndef make_response(status, headers, payload, environ):\n    \"\"\"This function generates an appropriate response object for this async\n    mode.\n    \"\"\"\n    return Response(body=payload, status=int(status.split()[0]),\n                    headers=headers)\n\n\nclass WebSocket(object):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides a aiohttp WebSocket interface that is\n    somewhat compatible with eventlet's implementation.\n    \"\"\"\n    def __init__(self, handler):\n        self.handler = handler\n        self._sock = None\n\n    async def __call__(self, environ):\n        request = environ['aiohttp.request']\n        self._sock = WebSocketResponse()\n        await self._sock.prepare(request)\n\n        self.environ = environ\n        await self.handler(self)\n        return self._sock\n\n    async def close(self):\n        await self._sock.close()\n\n    async def send(self, message):\n        if isinstance(message, bytes):\n            f = self._sock.send_bytes\n        else:\n            f = self._sock.send_str\n        if asyncio.iscoroutinefunction(f):\n            await f(message)\n        else:\n            f(message)\n\n    async def wait(self):\n        msg = await self._sock.receive()\n        if not isinstance(msg.data, six.binary_type) and \\\n                not isinstance(msg.data, six.text_type):\n            raise IOError()\n        return msg.data\n\n\n_async = {\n    'asyncio': True,\n    'create_route': create_route,\n    'translate_request': translate_request,\n    'make_response': make_response,\n    'websocket': WebSocket,\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_drivers/asgi.py",
    "content": "import os\nimport sys\n\nfrom engineio.static_files import get_static_file\n\n\nclass ASGIApp:\n    \"\"\"ASGI application middleware for Engine.IO.\n\n    This middleware dispatches traffic to an Engine.IO application. It can\n    also serve a list of static files to the client, or forward unrelated\n    HTTP traffic to another ASGI application.\n\n    :param engineio_server: The Engine.IO server. Must be an instance of the\n                            ``engineio.AsyncServer`` class.\n    :param static_files: A dictionary with static file mapping rules. See the\n                         documentation for details on this argument.\n    :param other_asgi_app: A separate ASGI app that receives all other traffic.\n    :param engineio_path: The endpoint where the Engine.IO application should\n                          be installed. The default value is appropriate for\n                          most cases.\n\n    Example usage::\n\n        import engineio\n        import uvicorn\n\n        eio = engineio.AsyncServer()\n        app = engineio.ASGIApp(eio, static_files={\n            '/': {'content_type': 'text/html', 'filename': 'index.html'},\n            '/index.html': {'content_type': 'text/html',\n                            'filename': 'index.html'},\n        })\n        uvicorn.run(app, '127.0.0.1', 5000)\n    \"\"\"\n    def __init__(self, engineio_server, other_asgi_app=None,\n                 static_files=None, engineio_path='engine.io'):\n        self.engineio_server = engineio_server\n        self.other_asgi_app = other_asgi_app\n        self.engineio_path = engineio_path.strip('/')\n        self.static_files = static_files or {}\n\n    async def __call__(self, scope, receive, send):\n        if scope['type'] in ['http', 'websocket'] and \\\n                scope['path'].startswith('/{0}/'.format(self.engineio_path)):\n            await self.engineio_server.handle_request(scope, receive, send)\n        else:\n            static_file = get_static_file(scope['path'], self.static_files) \\\n                if scope['type'] == 'http' and self.static_files else None\n            if static_file:\n                await self.serve_static_file(static_file, receive, send)\n            elif self.other_asgi_app is not None:\n                await self.other_asgi_app(scope, receive, send)\n            elif scope['type'] == 'lifespan':\n                await self.lifespan(receive, send)\n            else:\n                await self.not_found(receive, send)\n\n    async def serve_static_file(self, static_file, receive,\n                                send):  # pragma: no cover\n        event = await receive()\n        if event['type'] == 'http.request':\n            if os.path.exists(static_file['filename']):\n                with open(static_file['filename'], 'rb') as f:\n                    payload = f.read()\n                await send({'type': 'http.response.start',\n                            'status': 200,\n                            'headers': [(b'Content-Type', static_file[\n                                'content_type'].encode('utf-8'))]})\n                await send({'type': 'http.response.body',\n                            'body': payload})\n            else:\n                await self.not_found(receive, send)\n\n    async def lifespan(self, receive, send):\n        event = await receive()\n        if event['type'] == 'lifespan.startup':\n            await send({'type': 'lifespan.startup.complete'})\n        elif event['type'] == 'lifespan.shutdown':\n            await send({'type': 'lifespan.shutdown.complete'})\n\n    async def not_found(self, receive, send):\n        \"\"\"Return a 404 Not Found error to the client.\"\"\"\n        await send({'type': 'http.response.start',\n                    'status': 404,\n                    'headers': [(b'Content-Type', b'text/plain')]})\n        await send({'type': 'http.response.body',\n                    'body': b'Not Found'})\n\n\nasync def translate_request(scope, receive, send):\n    class AwaitablePayload(object):  # pragma: no cover\n        def __init__(self, payload):\n            self.payload = payload or b''\n\n        async def read(self, length=None):\n            if length is None:\n                r = self.payload\n                self.payload = b''\n            else:\n                r = self.payload[:length]\n                self.payload = self.payload[length:]\n            return r\n\n    event = await receive()\n    payload = b''\n    if event['type'] == 'http.request':\n        payload += event.get('body') or b''\n        while event.get('more_body'):\n            event = await receive()\n            if event['type'] == 'http.request':\n                payload += event.get('body') or b''\n    elif event['type'] == 'websocket.connect':\n        await send({'type': 'websocket.accept'})\n    else:\n        return {}\n\n    raw_uri = scope['path'].encode('utf-8')\n    if 'query_string' in scope and scope['query_string']:\n        raw_uri += b'?' + scope['query_string']\n    environ = {\n        'wsgi.input': AwaitablePayload(payload),\n        'wsgi.errors': sys.stderr,\n        'wsgi.version': (1, 0),\n        'wsgi.async': True,\n        'wsgi.multithread': False,\n        'wsgi.multiprocess': False,\n        'wsgi.run_once': False,\n        'SERVER_SOFTWARE': 'asgi',\n        'REQUEST_METHOD': scope.get('method', 'GET'),\n        'PATH_INFO': scope['path'],\n        'QUERY_STRING': scope.get('query_string', b'').decode('utf-8'),\n        'RAW_URI': raw_uri.decode('utf-8'),\n        'SCRIPT_NAME': '',\n        'SERVER_PROTOCOL': 'HTTP/1.1',\n        'REMOTE_ADDR': '127.0.0.1',\n        'REMOTE_PORT': '0',\n        'SERVER_NAME': 'asgi',\n        'SERVER_PORT': '0',\n        'asgi.receive': receive,\n        'asgi.send': send,\n    }\n\n    for hdr_name, hdr_value in scope['headers']:\n        hdr_name = hdr_name.upper().decode('utf-8')\n        hdr_value = hdr_value.decode('utf-8')\n        if hdr_name == 'CONTENT-TYPE':\n            environ['CONTENT_TYPE'] = hdr_value\n            continue\n        elif hdr_name == 'CONTENT-LENGTH':\n            environ['CONTENT_LENGTH'] = hdr_value\n            continue\n\n        key = 'HTTP_%s' % hdr_name.replace('-', '_')\n        if key in environ:\n            hdr_value = '%s,%s' % (environ[key], hdr_value)\n\n        environ[key] = hdr_value\n\n    environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http')\n    return environ\n\n\nasync def make_response(status, headers, payload, environ):\n    headers = [(h[0].encode('utf-8'), h[1].encode('utf-8')) for h in headers]\n    await environ['asgi.send']({'type': 'http.response.start',\n                                'status': int(status.split(' ')[0]),\n                                'headers': headers})\n    await environ['asgi.send']({'type': 'http.response.body',\n                                'body': payload})\n\n\nclass WebSocket(object):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides an asgi WebSocket interface that is\n    somewhat compatible with eventlet's implementation.\n    \"\"\"\n    def __init__(self, handler):\n        self.handler = handler\n        self.asgi_receive = None\n        self.asgi_send = None\n\n    async def __call__(self, environ):\n        self.asgi_receive = environ['asgi.receive']\n        self.asgi_send = environ['asgi.send']\n        await self.handler(self)\n\n    async def close(self):\n        await self.asgi_send({'type': 'websocket.close'})\n\n    async def send(self, message):\n        msg_bytes = None\n        msg_text = None\n        if isinstance(message, bytes):\n            msg_bytes = message\n        else:\n            msg_text = message\n        await self.asgi_send({'type': 'websocket.send',\n                              'bytes': msg_bytes,\n                              'text': msg_text})\n\n    async def wait(self):\n        event = await self.asgi_receive()\n        if event['type'] != 'websocket.receive':\n            raise IOError()\n        return event.get('bytes') or event.get('text')\n\n\n_async = {\n    'asyncio': True,\n    'translate_request': translate_request,\n    'make_response': make_response,\n    'websocket': WebSocket,\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_drivers/eventlet.py",
    "content": "from __future__ import absolute_import\n\nfrom eventlet.green.threading import Thread, Event\nfrom eventlet import queue\nfrom eventlet import sleep\nfrom eventlet.websocket import WebSocketWSGI as _WebSocketWSGI\n\n\nclass WebSocketWSGI(_WebSocketWSGI):\n    def __init__(self, *args, **kwargs):\n        super(WebSocketWSGI, self).__init__(*args, **kwargs)\n        self._sock = None\n\n    def __call__(self, environ, start_response):\n        if 'eventlet.input' not in environ:\n            raise RuntimeError('You need to use the eventlet server. '\n                               'See the Deployment section of the '\n                               'documentation for more information.')\n        self._sock = environ['eventlet.input'].get_socket()\n        return super(WebSocketWSGI, self).__call__(environ, start_response)\n\n\n_async = {\n    'thread': Thread,\n    'queue': queue.Queue,\n    'queue_empty': queue.Empty,\n    'event': Event,\n    'websocket': WebSocketWSGI,\n    'sleep': sleep,\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_drivers/gevent.py",
    "content": "from __future__ import absolute_import\n\nimport gevent\nfrom gevent import queue\nfrom gevent.event import Event\ntry:\n    import geventwebsocket  # noqa\n    _websocket_available = True\nexcept ImportError:\n    _websocket_available = False\n\n\nclass Thread(gevent.Greenlet):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides gevent Greenlet interface that is compatible\n    with the standard library's Thread class.\n    \"\"\"\n    def __init__(self, target, args=[], kwargs={}):\n        super(Thread, self).__init__(target, *args, **kwargs)\n\n    def _run(self):\n        return self.run()\n\n\nclass WebSocketWSGI(object):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides a gevent WebSocket interface that is\n    compatible with eventlet's implementation.\n    \"\"\"\n    def __init__(self, app):\n        self.app = app\n\n    def __call__(self, environ, start_response):\n        if 'wsgi.websocket' not in environ:\n            raise RuntimeError('You need to use the gevent-websocket server. '\n                               'See the Deployment section of the '\n                               'documentation for more information.')\n        self._sock = environ['wsgi.websocket']\n        self.environ = environ\n        self.version = self._sock.version\n        self.path = self._sock.path\n        self.origin = self._sock.origin\n        self.protocol = self._sock.protocol\n        return self.app(self)\n\n    def close(self):\n        return self._sock.close()\n\n    def send(self, message):\n        return self._sock.send(message)\n\n    def wait(self):\n        return self._sock.receive()\n\n\n_async = {\n    'thread': Thread,\n    'queue': queue.JoinableQueue,\n    'queue_empty': queue.Empty,\n    'event': Event,\n    'websocket': WebSocketWSGI if _websocket_available else None,\n    'sleep': gevent.sleep,\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_drivers/gevent_uwsgi.py",
    "content": "from __future__ import absolute_import\n\nimport six\n\nimport gevent\nfrom gevent import queue\nfrom gevent.event import Event\nimport uwsgi\n_websocket_available = hasattr(uwsgi, 'websocket_handshake')\n\n\nclass Thread(gevent.Greenlet):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides gevent Greenlet interface that is compatible\n    with the standard library's Thread class.\n    \"\"\"\n    def __init__(self, target, args=[], kwargs={}):\n        super(Thread, self).__init__(target, *args, **kwargs)\n\n    def _run(self):\n        return self.run()\n\n\nclass uWSGIWebSocket(object):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides a uWSGI WebSocket interface that is\n    compatible with eventlet's implementation.\n    \"\"\"\n    def __init__(self, app):\n        self.app = app\n        self._sock = None\n\n    def __call__(self, environ, start_response):\n        self._sock = uwsgi.connection_fd()\n        self.environ = environ\n\n        uwsgi.websocket_handshake()\n\n        self._req_ctx = None\n        if hasattr(uwsgi, 'request_context'):\n            # uWSGI >= 2.1.x with support for api access across-greenlets\n            self._req_ctx = uwsgi.request_context()\n        else:\n            # use event and queue for sending messages\n            from gevent.event import Event\n            from gevent.queue import Queue\n            from gevent.select import select\n            self._event = Event()\n            self._send_queue = Queue()\n\n            # spawn a select greenlet\n            def select_greenlet_runner(fd, event):\n                \"\"\"Sets event when data becomes available to read on fd.\"\"\"\n                while True:\n                    event.set()\n                    try:\n                        select([fd], [], [])[0]\n                    except ValueError:\n                        break\n            self._select_greenlet = gevent.spawn(\n                select_greenlet_runner,\n                self._sock,\n                self._event)\n\n        self.app(self)\n\n    def close(self):\n        \"\"\"Disconnects uWSGI from the client.\"\"\"\n        uwsgi.disconnect()\n        if self._req_ctx is None:\n            # better kill it here in case wait() is not called again\n            self._select_greenlet.kill()\n            self._event.set()\n\n    def _send(self, msg):\n        \"\"\"Transmits message either in binary or UTF-8 text mode,\n        depending on its type.\"\"\"\n        if isinstance(msg, six.binary_type):\n            method = uwsgi.websocket_send_binary\n        else:\n            method = uwsgi.websocket_send\n        if self._req_ctx is not None:\n            method(msg, request_context=self._req_ctx)\n        else:\n            method(msg)\n\n    def _decode_received(self, msg):\n        \"\"\"Returns either bytes or str, depending on message type.\"\"\"\n        if not isinstance(msg, six.binary_type):\n            # already decoded - do nothing\n            return msg\n        # only decode from utf-8 if message is not binary data\n        type = six.byte2int(msg[0:1])\n        if type >= 48:  # no binary\n            return msg.decode('utf-8')\n        # binary message, don't try to decode\n        return msg\n\n    def send(self, msg):\n        \"\"\"Queues a message for sending. Real transmission is done in\n        wait method.\n        Sends directly if uWSGI version is new enough.\"\"\"\n        if self._req_ctx is not None:\n            self._send(msg)\n        else:\n            self._send_queue.put(msg)\n            self._event.set()\n\n    def wait(self):\n        \"\"\"Waits and returns received messages.\n        If running in compatibility mode for older uWSGI versions,\n        it also sends messages that have been queued by send().\n        A return value of None means that connection was closed.\n        This must be called repeatedly. For uWSGI < 2.1.x it must\n        be called from the main greenlet.\"\"\"\n        while True:\n            if self._req_ctx is not None:\n                try:\n                    msg = uwsgi.websocket_recv(request_context=self._req_ctx)\n                except IOError:  # connection closed\n                    return None\n                return self._decode_received(msg)\n            else:\n                # we wake up at least every 3 seconds to let uWSGI\n                # do its ping/ponging\n                event_set = self._event.wait(timeout=3)\n                if event_set:\n                    self._event.clear()\n                    # maybe there is something to send\n                    msgs = []\n                    while True:\n                        try:\n                            msgs.append(self._send_queue.get(block=False))\n                        except gevent.queue.Empty:\n                            break\n                    for msg in msgs:\n                        self._send(msg)\n                # maybe there is something to receive, if not, at least\n                # ensure uWSGI does its ping/ponging\n                try:\n                    msg = uwsgi.websocket_recv_nb()\n                except IOError:  # connection closed\n                    self._select_greenlet.kill()\n                    return None\n                if msg:  # message available\n                    return self._decode_received(msg)\n\n\n_async = {\n    'thread': Thread,\n    'queue': queue.JoinableQueue,\n    'queue_empty': queue.Empty,\n    'event': Event,\n    'websocket': uWSGIWebSocket if _websocket_available else None,\n    'sleep': gevent.sleep,\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_drivers/sanic.py",
    "content": "import sys\nfrom urllib.parse import urlsplit\n\nfrom sanic.response import HTTPResponse\ntry:\n    from sanic.websocket import WebSocketProtocol\nexcept ImportError:\n    # the installed version of sanic does not have websocket support\n    WebSocketProtocol = None\nimport six\n\n\ndef create_route(app, engineio_server, engineio_endpoint):\n    \"\"\"This function sets up the engine.io endpoint as a route for the\n    application.\n\n    Note that both GET and POST requests must be hooked up on the engine.io\n    endpoint.\n    \"\"\"\n    app.add_route(engineio_server.handle_request, engineio_endpoint,\n                  methods=['GET', 'POST', 'OPTIONS'])\n    try:\n        app.enable_websocket()\n    except AttributeError:\n        # ignore, this version does not support websocket\n        pass\n\n\ndef translate_request(request):\n    \"\"\"This function takes the arguments passed to the request handler and\n    uses them to generate a WSGI compatible environ dictionary.\n    \"\"\"\n    class AwaitablePayload(object):\n        def __init__(self, payload):\n            self.payload = payload or b''\n\n        async def read(self, length=None):\n            if length is None:\n                r = self.payload\n                self.payload = b''\n            else:\n                r = self.payload[:length]\n                self.payload = self.payload[length:]\n            return r\n\n    uri_parts = urlsplit(request.url)\n    environ = {\n        'wsgi.input': AwaitablePayload(request.body),\n        'wsgi.errors': sys.stderr,\n        'wsgi.version': (1, 0),\n        'wsgi.async': True,\n        'wsgi.multithread': False,\n        'wsgi.multiprocess': False,\n        'wsgi.run_once': False,\n        'SERVER_SOFTWARE': 'sanic',\n        'REQUEST_METHOD': request.method,\n        'QUERY_STRING': uri_parts.query or '',\n        'RAW_URI': request.url,\n        'SERVER_PROTOCOL': 'HTTP/' + request.version,\n        'REMOTE_ADDR': '127.0.0.1',\n        'REMOTE_PORT': '0',\n        'SERVER_NAME': 'sanic',\n        'SERVER_PORT': '0',\n        'sanic.request': request\n    }\n\n    for hdr_name, hdr_value in request.headers.items():\n        hdr_name = hdr_name.upper()\n        if hdr_name == 'CONTENT-TYPE':\n            environ['CONTENT_TYPE'] = hdr_value\n            continue\n        elif hdr_name == 'CONTENT-LENGTH':\n            environ['CONTENT_LENGTH'] = hdr_value\n            continue\n\n        key = 'HTTP_%s' % hdr_name.replace('-', '_')\n        if key in environ:\n            hdr_value = '%s,%s' % (environ[key], hdr_value)\n\n        environ[key] = hdr_value\n\n    environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http')\n\n    path_info = uri_parts.path\n\n    environ['PATH_INFO'] = path_info\n    environ['SCRIPT_NAME'] = ''\n\n    return environ\n\n\ndef make_response(status, headers, payload, environ):\n    \"\"\"This function generates an appropriate response object for this async\n    mode.\n    \"\"\"\n    headers_dict = {}\n    content_type = None\n    for h in headers:\n        if h[0].lower() == 'content-type':\n            content_type = h[1]\n        else:\n            headers_dict[h[0]] = h[1]\n    return HTTPResponse(body_bytes=payload, content_type=content_type,\n                        status=int(status.split()[0]), headers=headers_dict)\n\n\nclass WebSocket(object):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides a sanic WebSocket interface that is\n    somewhat compatible with eventlet's implementation.\n    \"\"\"\n    def __init__(self, handler):\n        self.handler = handler\n        self._sock = None\n\n    async def __call__(self, environ):\n        request = environ['sanic.request']\n        protocol = request.transport.get_protocol()\n        self._sock = await protocol.websocket_handshake(request)\n\n        self.environ = environ\n        await self.handler(self)\n\n    async def close(self):\n        await self._sock.close()\n\n    async def send(self, message):\n        await self._sock.send(message)\n\n    async def wait(self):\n        data = await self._sock.recv()\n        if not isinstance(data, six.binary_type) and \\\n                not isinstance(data, six.text_type):\n            raise IOError()\n        return data\n\n\n_async = {\n    'asyncio': True,\n    'create_route': create_route,\n    'translate_request': translate_request,\n    'make_response': make_response,\n    'websocket': WebSocket if WebSocketProtocol else None,\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_drivers/threading.py",
    "content": "from __future__ import absolute_import\nimport threading\nimport time\n\ntry:\n    import queue\nexcept ImportError:  # pragma: no cover\n    import Queue as queue\n\n_async = {\n    'thread': threading.Thread,\n    'queue': queue.Queue,\n    'queue_empty': queue.Empty,\n    'event': threading.Event,\n    'websocket': None,\n    'sleep': time.sleep,\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_drivers/tornado.py",
    "content": "import asyncio\nimport sys\nfrom urllib.parse import urlsplit\nfrom .. import exceptions\n\nimport tornado.web\nimport tornado.websocket\nimport six\n\n\ndef get_tornado_handler(engineio_server):\n    class Handler(tornado.websocket.WebSocketHandler):  # pragma: no cover\n        def __init__(self, *args, **kwargs):\n            super().__init__(*args, **kwargs)\n            if isinstance(engineio_server.cors_allowed_origins,\n                          six.string_types):\n                if engineio_server.cors_allowed_origins == '*':\n                    self.allowed_origins = None\n                else:\n                    self.allowed_origins = [\n                        engineio_server.cors_allowed_origins]\n            else:\n                self.allowed_origins = engineio_server.cors_allowed_origins\n            self.receive_queue = asyncio.Queue()\n\n        async def get(self, *args, **kwargs):\n            if self.request.headers.get('Upgrade', '').lower() == 'websocket':\n                ret = super().get(*args, **kwargs)\n                if asyncio.iscoroutine(ret):\n                    await ret\n            else:\n                await engineio_server.handle_request(self)\n\n        async def open(self, *args, **kwargs):\n            # this is the handler for the websocket request\n            asyncio.ensure_future(engineio_server.handle_request(self))\n\n        async def post(self, *args, **kwargs):\n            await engineio_server.handle_request(self)\n\n        async def options(self, *args, **kwargs):\n            await engineio_server.handle_request(self)\n\n        async def on_message(self, message):\n            await self.receive_queue.put(message)\n\n        async def get_next_message(self):\n            return await self.receive_queue.get()\n\n        def on_close(self):\n            self.receive_queue.put_nowait(None)\n\n        def check_origin(self, origin):\n            if self.allowed_origins is None or origin in self.allowed_origins:\n                return True\n            return super().check_origin(origin)\n\n        def get_compression_options(self):\n            # enable compression\n            return {}\n\n    return Handler\n\n\ndef translate_request(handler):\n    \"\"\"This function takes the arguments passed to the request handler and\n    uses them to generate a WSGI compatible environ dictionary.\n    \"\"\"\n    class AwaitablePayload(object):\n        def __init__(self, payload):\n            self.payload = payload or b''\n\n        async def read(self, length=None):\n            if length is None:\n                r = self.payload\n                self.payload = b''\n            else:\n                r = self.payload[:length]\n                self.payload = self.payload[length:]\n            return r\n\n    payload = handler.request.body\n\n    uri_parts = urlsplit(handler.request.path)\n    full_uri = handler.request.path\n    if handler.request.query:  # pragma: no cover\n        full_uri += '?' + handler.request.query\n    environ = {\n        'wsgi.input': AwaitablePayload(payload),\n        'wsgi.errors': sys.stderr,\n        'wsgi.version': (1, 0),\n        'wsgi.async': True,\n        'wsgi.multithread': False,\n        'wsgi.multiprocess': False,\n        'wsgi.run_once': False,\n        'SERVER_SOFTWARE': 'aiohttp',\n        'REQUEST_METHOD': handler.request.method,\n        'QUERY_STRING': handler.request.query or '',\n        'RAW_URI': full_uri,\n        'SERVER_PROTOCOL': 'HTTP/%s' % handler.request.version,\n        'REMOTE_ADDR': '127.0.0.1',\n        'REMOTE_PORT': '0',\n        'SERVER_NAME': 'aiohttp',\n        'SERVER_PORT': '0',\n        'tornado.handler': handler\n    }\n\n    for hdr_name, hdr_value in handler.request.headers.items():\n        hdr_name = hdr_name.upper()\n        if hdr_name == 'CONTENT-TYPE':\n            environ['CONTENT_TYPE'] = hdr_value\n            continue\n        elif hdr_name == 'CONTENT-LENGTH':\n            environ['CONTENT_LENGTH'] = hdr_value\n            continue\n\n        key = 'HTTP_%s' % hdr_name.replace('-', '_')\n        environ[key] = hdr_value\n\n    environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http')\n\n    path_info = uri_parts.path\n\n    environ['PATH_INFO'] = path_info\n    environ['SCRIPT_NAME'] = ''\n\n    return environ\n\n\ndef make_response(status, headers, payload, environ):\n    \"\"\"This function generates an appropriate response object for this async\n    mode.\n    \"\"\"\n    tornado_handler = environ['tornado.handler']\n    try:\n        tornado_handler.set_status(int(status.split()[0]))\n    except RuntimeError:  # pragma: no cover\n        # for websocket connections Tornado does not accept a response, since\n        # it already emitted the 101 status code\n        return\n    for header, value in headers:\n        tornado_handler.set_header(header, value)\n    tornado_handler.write(payload)\n    tornado_handler.finish()\n\n\nclass WebSocket(object):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides a tornado WebSocket interface that is\n    somewhat compatible with eventlet's implementation.\n    \"\"\"\n    def __init__(self, handler):\n        self.handler = handler\n        self.tornado_handler = None\n\n    async def __call__(self, environ):\n        self.tornado_handler = environ['tornado.handler']\n        self.environ = environ\n        await self.handler(self)\n\n    async def close(self):\n        self.tornado_handler.close()\n\n    async def send(self, message):\n        try:\n            self.tornado_handler.write_message(\n                message, binary=isinstance(message, bytes))\n        except tornado.websocket.WebSocketClosedError:\n            raise exceptions.EngineIOError()\n\n    async def wait(self):\n        msg = await self.tornado_handler.get_next_message()\n        if not isinstance(msg, six.binary_type) and \\\n                not isinstance(msg, six.text_type):\n            raise IOError()\n        return msg\n\n\n_async = {\n    'asyncio': True,\n    'translate_request': translate_request,\n    'make_response': make_response,\n    'websocket': WebSocket,\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_eventlet.py",
    "content": "import importlib\nimport sys\n\nfrom eventlet import sleep\nfrom eventlet.websocket import WebSocketWSGI as _WebSocketWSGI\n\n\nclass WebSocketWSGI(_WebSocketWSGI):\n    def __init__(self, *args, **kwargs):\n        super(WebSocketWSGI, self).__init__(*args, **kwargs)\n        self._sock = None\n\n    def __call__(self, environ, start_response):\n        if 'eventlet.input' not in environ:\n            raise RuntimeError('You need to use the eventlet server. '\n                               'See the Deployment section of the '\n                               'documentation for more information.')\n        self._sock = environ['eventlet.input'].get_socket()\n        return super(WebSocketWSGI, self).__call__(environ, start_response)\n\n\n_async = {\n    'threading': importlib.import_module('eventlet.green.threading'),\n    'thread_class': 'Thread',\n    'queue': importlib.import_module('eventlet.queue'),\n    'queue_class': 'Queue',\n    'websocket': sys.modules[__name__],\n    'websocket_class': 'WebSocketWSGI',\n    'sleep': sleep\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_gevent.py",
    "content": "import importlib\nimport sys\n\nimport gevent\ntry:\n    import geventwebsocket  # noqa\n    _websocket_available = True\nexcept ImportError:\n    _websocket_available = False\n\n\nclass Thread(gevent.Greenlet):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides gevent Greenlet interface that is compatible\n    with the standard library's Thread class.\n    \"\"\"\n    def __init__(self, target, args=[], kwargs={}):\n        super(Thread, self).__init__(target, *args, **kwargs)\n\n    def _run(self):\n        return self.run()\n\n\nclass WebSocketWSGI(object):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides a gevent WebSocket interface that is\n    compatible with eventlet's implementation.\n    \"\"\"\n    def __init__(self, app):\n        self.app = app\n\n    def __call__(self, environ, start_response):\n        if 'wsgi.websocket' not in environ:\n            raise RuntimeError('You need to use the gevent-websocket server. '\n                               'See the Deployment section of the '\n                               'documentation for more information.')\n        self._sock = environ['wsgi.websocket']\n        self.environ = environ\n        self.version = self._sock.version\n        self.path = self._sock.path\n        self.origin = self._sock.origin\n        self.protocol = self._sock.protocol\n        return self.app(self)\n\n    def close(self):\n        return self._sock.close()\n\n    def send(self, message):\n        return self._sock.send(message)\n\n    def wait(self):\n        return self._sock.receive()\n\n\n_async = {\n    'threading': sys.modules[__name__],\n    'thread_class': 'Thread',\n    'queue': importlib.import_module('gevent.queue'),\n    'queue_class': 'JoinableQueue',\n    'websocket': sys.modules[__name__] if _websocket_available else None,\n    'websocket_class': 'WebSocketWSGI' if _websocket_available else None,\n    'sleep': gevent.sleep\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_gevent_uwsgi.py",
    "content": "import importlib\nimport sys\nimport six\n\nimport gevent\nimport uwsgi\n_websocket_available = hasattr(uwsgi, 'websocket_handshake')\n\n\nclass Thread(gevent.Greenlet):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides gevent Greenlet interface that is compatible\n    with the standard library's Thread class.\n    \"\"\"\n    def __init__(self, target, args=[], kwargs={}):\n        super(Thread, self).__init__(target, *args, **kwargs)\n\n    def _run(self):\n        return self.run()\n\n\nclass uWSGIWebSocket(object):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides a uWSGI WebSocket interface that is\n    compatible with eventlet's implementation.\n    \"\"\"\n    def __init__(self, app):\n        self.app = app\n        self._sock = None\n\n    def __call__(self, environ, start_response):\n        self._sock = uwsgi.connection_fd()\n        self.environ = environ\n\n        uwsgi.websocket_handshake()\n\n        self._req_ctx = None\n        if hasattr(uwsgi, 'request_context'):\n            # uWSGI >= 2.1.x with support for api access across-greenlets\n            self._req_ctx = uwsgi.request_context()\n        else:\n            # use event and queue for sending messages\n            from gevent.event import Event\n            from gevent.queue import Queue\n            from gevent.select import select\n            self._event = Event()\n            self._send_queue = Queue()\n\n            # spawn a select greenlet\n            def select_greenlet_runner(fd, event):\n                \"\"\"Sets event when data becomes available to read on fd.\"\"\"\n                while True:\n                    event.set()\n                    try:\n                        select([fd], [], [])[0]\n                    except ValueError:\n                        break\n            self._select_greenlet = gevent.spawn(\n                select_greenlet_runner,\n                self._sock,\n                self._event)\n\n        self.app(self)\n\n    def close(self):\n        \"\"\"Disconnects uWSGI from the client.\"\"\"\n        uwsgi.disconnect()\n        if self._req_ctx is None:\n            # better kill it here in case wait() is not called again\n            self._select_greenlet.kill()\n            self._event.set()\n\n    def _send(self, msg):\n        \"\"\"Transmits message either in binary or UTF-8 text mode,\n        depending on its type.\"\"\"\n        if isinstance(msg, six.binary_type):\n            method = uwsgi.websocket_send_binary\n        else:\n            method = uwsgi.websocket_send\n        if self._req_ctx is not None:\n            method(msg, request_context=self._req_ctx)\n        else:\n            method(msg)\n\n    def _decode_received(self, msg):\n        \"\"\"Returns either bytes or str, depending on message type.\"\"\"\n        if not isinstance(msg, six.binary_type):\n            # already decoded - do nothing\n            return msg\n        # only decode from utf-8 if message is not binary data\n        type = six.byte2int(msg[0:1])\n        if type >= 48:  # no binary\n            return msg.decode('utf-8')\n        # binary message, don't try to decode\n        return msg\n\n    def send(self, msg):\n        \"\"\"Queues a message for sending. Real transmission is done in\n        wait method.\n        Sends directly if uWSGI version is new enough.\"\"\"\n        if self._req_ctx is not None:\n            self._send(msg)\n        else:\n            self._send_queue.put(msg)\n            self._event.set()\n\n    def wait(self):\n        \"\"\"Waits and returns received messages.\n        If running in compatibility mode for older uWSGI versions,\n        it also sends messages that have been queued by send().\n        A return value of None means that connection was closed.\n        This must be called repeatedly. For uWSGI < 2.1.x it must\n        be called from the main greenlet.\"\"\"\n        while True:\n            if self._req_ctx is not None:\n                try:\n                    msg = uwsgi.websocket_recv(request_context=self._req_ctx)\n                except IOError:  # connection closed\n                    return None\n                return self._decode_received(msg)\n            else:\n                # we wake up at least every 3 seconds to let uWSGI\n                # do its ping/ponging\n                event_set = self._event.wait(timeout=3)\n                if event_set:\n                    self._event.clear()\n                    # maybe there is something to send\n                    msgs = []\n                    while True:\n                        try:\n                            msgs.append(self._send_queue.get(block=False))\n                        except gevent.queue.Empty:\n                            break\n                    for msg in msgs:\n                        self._send(msg)\n                # maybe there is something to receive, if not, at least\n                # ensure uWSGI does its ping/ponging\n                try:\n                    msg = uwsgi.websocket_recv_nb()\n                except IOError:  # connection closed\n                    self._select_greenlet.kill()\n                    return None\n                if msg:  # message available\n                    return self._decode_received(msg)\n\n\n_async = {\n    'threading': sys.modules[__name__],\n    'thread_class': 'Thread',\n    'queue': importlib.import_module('gevent.queue'),\n    'queue_class': 'JoinableQueue',\n    'websocket': sys.modules[__name__] if _websocket_available else None,\n    'websocket_class': 'uWSGIWebSocket' if _websocket_available else None,\n    'sleep': gevent.sleep\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_sanic.py",
    "content": "import sys\nfrom urllib.parse import urlsplit\n\nfrom sanic.response import HTTPResponse\ntry:\n    from sanic.websocket import WebSocketProtocol\nexcept ImportError:\n    # the installed version of sanic does not have websocket support\n    WebSocketProtocol = None\nimport six\n\n\ndef create_route(app, engineio_server, engineio_endpoint):\n    \"\"\"This function sets up the engine.io endpoint as a route for the\n    application.\n\n    Note that both GET and POST requests must be hooked up on the engine.io\n    endpoint.\n    \"\"\"\n    app.add_route(engineio_server.handle_request, engineio_endpoint,\n                  methods=['GET', 'POST', 'OPTIONS'])\n    try:\n        app.enable_websocket()\n    except AttributeError:\n        # ignore, this version does not support websocket\n        pass\n\n\ndef translate_request(request):\n    \"\"\"This function takes the arguments passed to the request handler and\n    uses them to generate a WSGI compatible environ dictionary.\n    \"\"\"\n    class AwaitablePayload(object):\n        def __init__(self, payload):\n            self.payload = payload or b''\n\n        async def read(self, length=None):\n            if length is None:\n                r = self.payload\n                self.payload = b''\n            else:\n                r = self.payload[:length]\n                self.payload = self.payload[length:]\n            return r\n\n    uri_parts = urlsplit(request.url)\n    environ = {\n        'wsgi.input': AwaitablePayload(request.body),\n        'wsgi.errors': sys.stderr,\n        'wsgi.version': (1, 0),\n        'wsgi.async': True,\n        'wsgi.multithread': False,\n        'wsgi.multiprocess': False,\n        'wsgi.run_once': False,\n        'SERVER_SOFTWARE': 'sanic',\n        'REQUEST_METHOD': request.method,\n        'QUERY_STRING': uri_parts.query or '',\n        'RAW_URI': request.url,\n        'SERVER_PROTOCOL': 'HTTP/' + request.version,\n        'REMOTE_ADDR': '127.0.0.1',\n        'REMOTE_PORT': '0',\n        'SERVER_NAME': 'sanic',\n        'SERVER_PORT': '0',\n        'sanic.request': request\n    }\n\n    for hdr_name, hdr_value in request.headers.items():\n        hdr_name = hdr_name.upper()\n        if hdr_name == 'CONTENT-TYPE':\n            environ['CONTENT_TYPE'] = hdr_value\n            continue\n        elif hdr_name == 'CONTENT-LENGTH':\n            environ['CONTENT_LENGTH'] = hdr_value\n            continue\n\n        key = 'HTTP_%s' % hdr_name.replace('-', '_')\n        if key in environ:\n            hdr_value = '%s,%s' % (environ[key], hdr_value)\n\n        environ[key] = hdr_value\n\n    environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http')\n\n    path_info = uri_parts.path\n\n    environ['PATH_INFO'] = path_info\n    environ['SCRIPT_NAME'] = ''\n\n    return environ\n\n\ndef make_response(status, headers, payload, environ):\n    \"\"\"This function generates an appropriate response object for this async\n    mode.\n    \"\"\"\n    headers_dict = {}\n    content_type = None\n    for h in headers:\n        if h[0].lower() == 'content-type':\n            content_type = h[1]\n        else:\n            headers_dict[h[0]] = h[1]\n    return HTTPResponse(body_bytes=payload, content_type=content_type,\n                        status=int(status.split()[0]), headers=headers_dict)\n\n\nclass WebSocket(object):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides a sanic WebSocket interface that is\n    somewhat compatible with eventlet's implementation.\n    \"\"\"\n    def __init__(self, handler):\n        self.handler = handler\n        self._sock = None\n\n    async def __call__(self, environ):\n        request = environ['sanic.request']\n        protocol = request.transport.get_protocol()\n        self._sock = await protocol.websocket_handshake(request)\n\n        self.environ = environ\n        await self.handler(self)\n\n    async def close(self):\n        await self._sock.close()\n\n    async def send(self, message):\n        await self._sock.send(message)\n\n    async def wait(self):\n        data = await self._sock.recv()\n        if not isinstance(data, six.binary_type) and \\\n                not isinstance(data, six.text_type):\n            raise IOError()\n        return data\n\n\n_async = {\n    'asyncio': True,\n    'create_route': create_route,\n    'translate_request': translate_request,\n    'make_response': make_response,\n    'websocket': sys.modules[__name__] if WebSocketProtocol else None,\n    'websocket_class': 'WebSocket' if WebSocketProtocol else None\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_threading.py",
    "content": "import importlib\nimport time\n\ntry:\n    queue = importlib.import_module('queue')\nexcept ImportError:  # pragma: no cover\n    queue = importlib.import_module('Queue')  # pragma: no cover\n\n_async = {\n    'threading': importlib.import_module('threading'),\n    'thread_class': 'Thread',\n    'queue': queue,\n    'queue_class': 'Queue',\n    'websocket': None,\n    'websocket_class': None,\n    'sleep': time.sleep\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/async_tornado.py",
    "content": "import asyncio\nimport sys\nfrom urllib.parse import urlsplit\n\ntry:\n    import tornado.web\n    import tornado.websocket\nexcept ImportError:  # pragma: no cover\n    pass\nimport six\n\n\ndef get_tornado_handler(engineio_server):\n    class Handler(tornado.websocket.WebSocketHandler):  # pragma: no cover\n        def __init__(self, *args, **kwargs):\n            super().__init__(*args, **kwargs)\n            self.receive_queue = asyncio.Queue()\n\n        async def get(self):\n            if self.request.headers.get('Upgrade', '').lower() == 'websocket':\n                super().get()\n            await engineio_server.handle_request(self)\n\n        async def post(self):\n            await engineio_server.handle_request(self)\n\n        async def options(self):\n            await engineio_server.handle_request(self)\n\n        async def on_message(self, message):\n            await self.receive_queue.put(message)\n\n        async def get_next_message(self):\n            return await self.receive_queue.get()\n\n        def on_close(self):\n            self.receive_queue.put_nowait(None)\n\n    return Handler\n\n\ndef translate_request(handler):\n    \"\"\"This function takes the arguments passed to the request handler and\n    uses them to generate a WSGI compatible environ dictionary.\n    \"\"\"\n    class AwaitablePayload(object):\n        def __init__(self, payload):\n            self.payload = payload or b''\n\n        async def read(self, length=None):\n            if length is None:\n                r = self.payload\n                self.payload = b''\n            else:\n                r = self.payload[:length]\n                self.payload = self.payload[length:]\n            return r\n\n    payload = handler.request.body\n\n    uri_parts = urlsplit(handler.request.path)\n    full_uri = handler.request.path\n    if handler.request.query:  # pragma: no cover\n        full_uri += '?' + handler.request.query\n    environ = {\n        'wsgi.input': AwaitablePayload(payload),\n        'wsgi.errors': sys.stderr,\n        'wsgi.version': (1, 0),\n        'wsgi.async': True,\n        'wsgi.multithread': False,\n        'wsgi.multiprocess': False,\n        'wsgi.run_once': False,\n        'SERVER_SOFTWARE': 'aiohttp',\n        'REQUEST_METHOD': handler.request.method,\n        'QUERY_STRING': handler.request.query or '',\n        'RAW_URI': full_uri,\n        'SERVER_PROTOCOL': 'HTTP/%s' % handler.request.version,\n        'REMOTE_ADDR': '127.0.0.1',\n        'REMOTE_PORT': '0',\n        'SERVER_NAME': 'aiohttp',\n        'SERVER_PORT': '0',\n        'tornado.handler': handler\n    }\n\n    for hdr_name, hdr_value in handler.request.headers.items():\n        hdr_name = hdr_name.upper()\n        if hdr_name == 'CONTENT-TYPE':\n            environ['CONTENT_TYPE'] = hdr_value\n            continue\n        elif hdr_name == 'CONTENT-LENGTH':\n            environ['CONTENT_LENGTH'] = hdr_value\n            continue\n\n        key = 'HTTP_%s' % hdr_name.replace('-', '_')\n        environ[key] = hdr_value\n\n    environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http')\n\n    path_info = uri_parts.path\n\n    environ['PATH_INFO'] = path_info\n    environ['SCRIPT_NAME'] = ''\n\n    return environ\n\n\ndef make_response(status, headers, payload, environ):\n    \"\"\"This function generates an appropriate response object for this async\n    mode.\n    \"\"\"\n    tornado_handler = environ['tornado.handler']\n    tornado_handler.set_status(int(status.split()[0]))\n    for header, value in headers:\n        tornado_handler.set_header(header, value)\n    tornado_handler.write(payload)\n    tornado_handler.finish()\n\n\nclass WebSocket(object):  # pragma: no cover\n    \"\"\"\n    This wrapper class provides a tornado WebSocket interface that is\n    somewhat compatible with eventlet's implementation.\n    \"\"\"\n    def __init__(self, handler):\n        self.handler = handler\n        self.tornado_handler = None\n\n    async def __call__(self, environ):\n        self.tornado_handler = environ['tornado.handler']\n        self.environ = environ\n        await self.handler(self)\n\n    async def close(self):\n        self.tornado_handler.close()\n\n    async def send(self, message):\n        self.tornado_handler.write_message(\n            message, binary=isinstance(message, bytes))\n\n    async def wait(self):\n        msg = await self.tornado_handler.get_next_message()\n        if not isinstance(msg, six.binary_type) and \\\n                not isinstance(msg, six.text_type):\n            raise IOError()\n        return msg\n\n\n_async = {\n    'asyncio': True,\n    'translate_request': translate_request,\n    'make_response': make_response,\n    'websocket': sys.modules[__name__],\n    'websocket_class': 'WebSocket'\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/asyncio_client.py",
    "content": "import asyncio\n\ntry:\n    import aiohttp\nexcept ImportError:  # pragma: no cover\n    aiohttp = None\nimport six\ntry:\n    import websockets\nexcept ImportError:  # pragma: no cover\n    websockets = None\n\nfrom . import client\nfrom . import exceptions\nfrom . import packet\nfrom . import payload\n\n\nclass AsyncClient(client.Client):\n    \"\"\"An Engine.IO client for asyncio.\n\n    This class implements a fully compliant Engine.IO web client with support\n    for websocket and long-polling transports, compatible with the asyncio\n    framework on Python 3.5 or newer.\n\n    :param logger: To enable logging set to ``True`` or pass a logger object to\n                   use. To disable logging set to ``False``. The default is\n                   ``False``.\n    :param json: An alternative json module to use for encoding and decoding\n                 packets. Custom json modules must have ``dumps`` and ``loads``\n                 functions that are compatible with the standard library\n                 versions.\n    \"\"\"\n    def is_asyncio_based(self):\n        return True\n\n    async def connect(self, url, headers={}, transports=None,\n                      engineio_path='engine.io'):\n        \"\"\"Connect to an Engine.IO server.\n\n        :param url: The URL of the Engine.IO server. It can include custom\n                    query string parameters if required by the server.\n        :param headers: A dictionary with custom headers to send with the\n                        connection request.\n        :param transports: The list of allowed transports. Valid transports\n                           are ``'polling'`` and ``'websocket'``. If not\n                           given, the polling transport is connected first,\n                           then an upgrade to websocket is attempted.\n        :param engineio_path: The endpoint where the Engine.IO server is\n                              installed. The default value is appropriate for\n                              most cases.\n\n        Note: this method is a coroutine.\n\n        Example usage::\n\n            eio = engineio.Client()\n            await eio.connect('http://localhost:5000')\n        \"\"\"\n        if self.state != 'disconnected':\n            raise ValueError('Client is not in a disconnected state')\n        valid_transports = ['polling', 'websocket']\n        if transports is not None:\n            if isinstance(transports, six.text_type):\n                transports = [transports]\n            transports = [transport for transport in transports\n                          if transport in valid_transports]\n            if not transports:\n                raise ValueError('No valid transports provided')\n        self.transports = transports or valid_transports\n        self.queue = self.create_queue()\n        return await getattr(self, '_connect_' + self.transports[0])(\n            url, headers, engineio_path)\n\n    async def wait(self):\n        \"\"\"Wait until the connection with the server ends.\n\n        Client applications can use this function to block the main thread\n        during the life of the connection.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        if self.read_loop_task:\n            await self.read_loop_task\n\n    async def send(self, data, binary=None):\n        \"\"\"Send a message to a client.\n\n        :param data: The data to send to the client. Data can be of type\n                     ``str``, ``bytes``, ``list`` or ``dict``. If a ``list``\n                     or ``dict``, the data will be serialized as JSON.\n        :param binary: ``True`` to send packet as binary, ``False`` to send\n                       as text. If not given, unicode (Python 2) and str\n                       (Python 3) are sent as text, and str (Python 2) and\n                       bytes (Python 3) are sent as binary.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        await self._send_packet(packet.Packet(packet.MESSAGE, data=data,\n                                              binary=binary))\n\n    async def disconnect(self, abort=False):\n        \"\"\"Disconnect from the server.\n\n        :param abort: If set to ``True``, do not wait for background tasks\n                      associated with the connection to end.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        if self.state == 'connected':\n            await self._send_packet(packet.Packet(packet.CLOSE))\n            await self.queue.put(None)\n            self.state = 'disconnecting'\n            await self._trigger_event('disconnect', run_async=False)\n            if self.current_transport == 'websocket':\n                await self.ws.close()\n            if not abort:\n                await self.read_loop_task\n            self.state = 'disconnected'\n            try:\n                client.connected_clients.remove(self)\n            except ValueError:  # pragma: no cover\n                pass\n        self._reset()\n\n    def start_background_task(self, target, *args, **kwargs):\n        \"\"\"Start a background task.\n\n        This is a utility function that applications can use to start a\n        background task.\n\n        :param target: the target function to execute.\n        :param args: arguments to pass to the function.\n        :param kwargs: keyword arguments to pass to the function.\n\n        This function returns an object compatible with the `Thread` class in\n        the Python standard library. The `start()` method on this object is\n        already called by this function.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return asyncio.ensure_future(target(*args, **kwargs))\n\n    async def sleep(self, seconds=0):\n        \"\"\"Sleep for the requested amount of time.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await asyncio.sleep(seconds)\n\n    def create_queue(self):\n        \"\"\"Create a queue object.\"\"\"\n        q = asyncio.Queue()\n        q.Empty = asyncio.QueueEmpty\n        return q\n\n    def create_event(self):\n        \"\"\"Create an event object.\"\"\"\n        return asyncio.Event()\n\n    def _reset(self):\n        if self.http:  # pragma: no cover\n            asyncio.ensure_future(self.http.close())\n        super()._reset()\n\n    async def _connect_polling(self, url, headers, engineio_path):\n        \"\"\"Establish a long-polling connection to the Engine.IO server.\"\"\"\n        if aiohttp is None:  # pragma: no cover\n            self.logger.error('aiohttp not installed -- cannot make HTTP '\n                              'requests!')\n            return\n        self.base_url = self._get_engineio_url(url, engineio_path, 'polling')\n        self.logger.info('Attempting polling connection to ' + self.base_url)\n        r = await self._send_request(\n            'GET', self.base_url + self._get_url_timestamp(), headers=headers)\n        if r is None:\n            self._reset()\n            raise exceptions.ConnectionError(\n                'Connection refused by the server')\n        if r.status != 200:\n            raise exceptions.ConnectionError(\n                'Unexpected status code {} in server response'.format(\n                    r.status))\n        try:\n            p = payload.Payload(encoded_payload=await r.read())\n        except ValueError:\n            six.raise_from(exceptions.ConnectionError(\n                'Unexpected response from server'), None)\n        open_packet = p.packets[0]\n        if open_packet.packet_type != packet.OPEN:\n            raise exceptions.ConnectionError(\n                'OPEN packet not returned by server')\n        self.logger.info(\n            'Polling connection accepted with ' + str(open_packet.data))\n        self.sid = open_packet.data['sid']\n        self.upgrades = open_packet.data['upgrades']\n        self.ping_interval = open_packet.data['pingInterval'] / 1000.0\n        self.ping_timeout = open_packet.data['pingTimeout'] / 1000.0\n        self.current_transport = 'polling'\n        self.base_url += '&sid=' + self.sid\n\n        self.state = 'connected'\n        client.connected_clients.append(self)\n        await self._trigger_event('connect', run_async=False)\n\n        for pkt in p.packets[1:]:\n            await self._receive_packet(pkt)\n\n        if 'websocket' in self.upgrades and 'websocket' in self.transports:\n            # attempt to upgrade to websocket\n            if await self._connect_websocket(url, headers, engineio_path):\n                # upgrade to websocket succeeded, we're done here\n                return\n\n        self.ping_loop_task = self.start_background_task(self._ping_loop)\n        self.write_loop_task = self.start_background_task(self._write_loop)\n        self.read_loop_task = self.start_background_task(\n            self._read_loop_polling)\n\n    async def _connect_websocket(self, url, headers, engineio_path):\n        \"\"\"Establish or upgrade to a WebSocket connection with the server.\"\"\"\n        if websockets is None:  # pragma: no cover\n            self.logger.error('websockets package not installed')\n            return False\n        websocket_url = self._get_engineio_url(url, engineio_path,\n                                               'websocket')\n        if self.sid:\n            self.logger.info(\n                'Attempting WebSocket upgrade to ' + websocket_url)\n            upgrade = True\n            websocket_url += '&sid=' + self.sid\n        else:\n            upgrade = False\n            self.base_url = websocket_url\n            self.logger.info(\n                'Attempting WebSocket connection to ' + websocket_url)\n\n        # get the cookies from the long-polling connection so that they can\n        # also be sent the the WebSocket route\n        cookies = None\n        if self.http:\n            cookies = '; '.join([\"{}={}\".format(cookie.key, cookie.value)\n                                 for cookie in self.http._cookie_jar])\n            headers = headers.copy()\n            headers['Cookie'] = cookies\n\n        try:\n            ws = await websockets.connect(\n                websocket_url + self._get_url_timestamp(),\n                extra_headers=headers)\n        except (websockets.exceptions.InvalidURI,\n                websockets.exceptions.InvalidHandshake):\n            if upgrade:\n                self.logger.warning(\n                    'WebSocket upgrade failed: connection error')\n                return False\n            else:\n                raise exceptions.ConnectionError('Connection error')\n        if upgrade:\n            p = packet.Packet(packet.PING, data='probe').encode(\n                always_bytes=False)\n            try:\n                await ws.send(p)\n            except Exception as e:  # pragma: no cover\n                self.logger.warning(\n                    'WebSocket upgrade failed: unexpected send exception: %s',\n                    str(e))\n                return False\n            try:\n                p = await ws.recv()\n            except Exception as e:  # pragma: no cover\n                self.logger.warning(\n                    'WebSocket upgrade failed: unexpected recv exception: %s',\n                    str(e))\n                return False\n            pkt = packet.Packet(encoded_packet=p)\n            if pkt.packet_type != packet.PONG or pkt.data != 'probe':\n                self.logger.warning(\n                    'WebSocket upgrade failed: no PONG packet')\n                return False\n            p = packet.Packet(packet.UPGRADE).encode(always_bytes=False)\n            try:\n                await ws.send(p)\n            except Exception as e:  # pragma: no cover\n                self.logger.warning(\n                    'WebSocket upgrade failed: unexpected send exception: %s',\n                    str(e))\n                return False\n            self.current_transport = 'websocket'\n            if self.http:  # pragma: no cover\n                await self.http.close()\n            self.logger.info('WebSocket upgrade was successful')\n        else:\n            try:\n                p = await ws.recv()\n            except Exception as e:  # pragma: no cover\n                raise exceptions.ConnectionError(\n                    'Unexpected recv exception: ' + str(e))\n            open_packet = packet.Packet(encoded_packet=p)\n            if open_packet.packet_type != packet.OPEN:\n                raise exceptions.ConnectionError('no OPEN packet')\n            self.logger.info(\n                'WebSocket connection accepted with ' + str(open_packet.data))\n            self.sid = open_packet.data['sid']\n            self.upgrades = open_packet.data['upgrades']\n            self.ping_interval = open_packet.data['pingInterval'] / 1000.0\n            self.ping_timeout = open_packet.data['pingTimeout'] / 1000.0\n            self.current_transport = 'websocket'\n\n            self.state = 'connected'\n            client.connected_clients.append(self)\n            await self._trigger_event('connect', run_async=False)\n\n        self.ws = ws\n        self.ping_loop_task = self.start_background_task(self._ping_loop)\n        self.write_loop_task = self.start_background_task(self._write_loop)\n        self.read_loop_task = self.start_background_task(\n            self._read_loop_websocket)\n        return True\n\n    async def _receive_packet(self, pkt):\n        \"\"\"Handle incoming packets from the server.\"\"\"\n        packet_name = packet.packet_names[pkt.packet_type] \\\n            if pkt.packet_type < len(packet.packet_names) else 'UNKNOWN'\n        self.logger.info(\n            'Received packet %s data %s', packet_name,\n            pkt.data if not isinstance(pkt.data, bytes) else '<binary>')\n        if pkt.packet_type == packet.MESSAGE:\n            await self._trigger_event('message', pkt.data, run_async=True)\n        elif pkt.packet_type == packet.PONG:\n            self.pong_received = True\n        elif pkt.packet_type == packet.CLOSE:\n            await self.disconnect(abort=True)\n        elif pkt.packet_type == packet.NOOP:\n            pass\n        else:\n            self.logger.error('Received unexpected packet of type %s',\n                              pkt.packet_type)\n\n    async def _send_packet(self, pkt):\n        \"\"\"Queue a packet to be sent to the server.\"\"\"\n        if self.state != 'connected':\n            return\n        await self.queue.put(pkt)\n        self.logger.info(\n            'Sending packet %s data %s',\n            packet.packet_names[pkt.packet_type],\n            pkt.data if not isinstance(pkt.data, bytes) else '<binary>')\n\n    async def _send_request(\n            self, method, url, headers=None, body=None):  # pragma: no cover\n        if self.http is None or self.http.closed:\n            self.http = aiohttp.ClientSession()\n        method = getattr(self.http, method.lower())\n        try:\n            return await method(url, headers=headers, data=body)\n        except aiohttp.ClientError:\n            return\n\n    async def _trigger_event(self, event, *args, **kwargs):\n        \"\"\"Invoke an event handler.\"\"\"\n        run_async = kwargs.pop('run_async', False)\n        ret = None\n        if event in self.handlers:\n            if asyncio.iscoroutinefunction(self.handlers[event]) is True:\n                if run_async:\n                    return self.start_background_task(self.handlers[event],\n                                                      *args)\n                else:\n                    try:\n                        ret = await self.handlers[event](*args)\n                    except asyncio.CancelledError:  # pragma: no cover\n                        pass\n                    except:\n                        self.logger.exception(event + ' async handler error')\n                        if event == 'connect':\n                            # if connect handler raised error we reject the\n                            # connection\n                            return False\n            else:\n                if run_async:\n                    async def async_handler():\n                        return self.handlers[event](*args)\n\n                    return self.start_background_task(async_handler)\n                else:\n                    try:\n                        ret = self.handlers[event](*args)\n                    except:\n                        self.logger.exception(event + ' handler error')\n                        if event == 'connect':\n                            # if connect handler raised error we reject the\n                            # connection\n                            return False\n        return ret\n\n    async def _ping_loop(self):\n        \"\"\"This background task sends a PING to the server at the requested\n        interval.\n        \"\"\"\n        self.pong_received = True\n        self.ping_loop_event.clear()\n        while self.state == 'connected':\n            if not self.pong_received:\n                self.logger.info(\n                    'PONG response has not been received, aborting')\n                if self.ws:\n                    await self.ws.close()\n                await self.queue.put(None)\n                break\n            self.pong_received = False\n            await self._send_packet(packet.Packet(packet.PING))\n            try:\n                await asyncio.wait_for(self.ping_loop_event.wait(),\n                                       self.ping_interval)\n            except (asyncio.TimeoutError,\n                    asyncio.CancelledError):  # pragma: no cover\n                pass\n        self.logger.info('Exiting ping task')\n\n    async def _read_loop_polling(self):\n        \"\"\"Read packets by polling the Engine.IO server.\"\"\"\n        while self.state == 'connected':\n            self.logger.info(\n                'Sending polling GET request to ' + self.base_url)\n            r = await self._send_request(\n                'GET', self.base_url + self._get_url_timestamp())\n            if r is None:\n                self.logger.warning(\n                    'Connection refused by the server, aborting')\n                await self.queue.put(None)\n                break\n            if r.status != 200:\n                self.logger.warning('Unexpected status code %s in server '\n                                    'response, aborting', r.status)\n                await self.queue.put(None)\n                break\n            try:\n                p = payload.Payload(encoded_payload=await r.read())\n            except ValueError:\n                self.logger.warning(\n                    'Unexpected packet from server, aborting')\n                await self.queue.put(None)\n                break\n            for pkt in p.packets:\n                await self._receive_packet(pkt)\n\n        self.logger.info('Waiting for write loop task to end')\n        await self.write_loop_task\n        self.logger.info('Waiting for ping loop task to end')\n        self.ping_loop_event.set()\n        await self.ping_loop_task\n        if self.state == 'connected':\n            await self._trigger_event('disconnect', run_async=False)\n            try:\n                client.connected_clients.remove(self)\n            except ValueError:  # pragma: no cover\n                pass\n            self._reset()\n        self.logger.info('Exiting read loop task')\n\n    async def _read_loop_websocket(self):\n        \"\"\"Read packets from the Engine.IO WebSocket connection.\"\"\"\n        while self.state == 'connected':\n            p = None\n            try:\n                p = await self.ws.recv()\n            except websockets.exceptions.ConnectionClosed:\n                self.logger.info(\n                    'Read loop: WebSocket connection was closed, aborting')\n                await self.queue.put(None)\n                break\n            except Exception as e:\n                self.logger.info(\n                    'Unexpected error \"%s\", aborting', str(e))\n                await self.queue.put(None)\n                break\n            if isinstance(p, six.text_type):  # pragma: no cover\n                p = p.encode('utf-8')\n            pkt = packet.Packet(encoded_packet=p)\n            await self._receive_packet(pkt)\n\n        self.logger.info('Waiting for write loop task to end')\n        await self.write_loop_task\n        self.logger.info('Waiting for ping loop task to end')\n        self.ping_loop_event.set()\n        await self.ping_loop_task\n        if self.state == 'connected':\n            await self._trigger_event('disconnect', run_async=False)\n            try:\n                client.connected_clients.remove(self)\n            except ValueError:  # pragma: no cover\n                pass\n            self._reset()\n        self.logger.info('Exiting read loop task')\n\n    async def _write_loop(self):\n        \"\"\"This background task sends packages to the server as they are\n        pushed to the send queue.\n        \"\"\"\n        while self.state == 'connected':\n            # to simplify the timeout handling, use the maximum of the\n            # ping interval and ping timeout as timeout, with an extra 5\n            # seconds grace period\n            timeout = max(self.ping_interval, self.ping_timeout) + 5\n            packets = None\n            try:\n                packets = [await asyncio.wait_for(self.queue.get(), timeout)]\n            except (self.queue.Empty, asyncio.TimeoutError,\n                    asyncio.CancelledError):\n                self.logger.error('packet queue is empty, aborting')\n                break\n            if packets == [None]:\n                self.queue.task_done()\n                packets = []\n            else:\n                while True:\n                    try:\n                        packets.append(self.queue.get_nowait())\n                    except self.queue.Empty:\n                        break\n                    if packets[-1] is None:\n                        packets = packets[:-1]\n                        self.queue.task_done()\n                        break\n            if not packets:\n                # empty packet list returned -> connection closed\n                break\n            if self.current_transport == 'polling':\n                p = payload.Payload(packets=packets)\n                r = await self._send_request(\n                    'POST', self.base_url, body=p.encode(),\n                    headers={'Content-Type': 'application/octet-stream'})\n                for pkt in packets:\n                    self.queue.task_done()\n                if r is None:\n                    self.logger.warning(\n                        'Connection refused by the server, aborting')\n                    break\n                if r.status != 200:\n                    self.logger.warning('Unexpected status code %s in server '\n                                        'response, aborting', r.status)\n                    self._reset()\n                    break\n            else:\n                # websocket\n                try:\n                    for pkt in packets:\n                        await self.ws.send(pkt.encode(always_bytes=False))\n                        self.queue.task_done()\n                except websockets.exceptions.ConnectionClosed:\n                    self.logger.info(\n                        'Write loop: WebSocket connection was closed, '\n                        'aborting')\n                    break\n        self.logger.info('Exiting write loop task')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/asyncio_server.py",
    "content": "import asyncio\n\nimport six\nfrom six.moves import urllib\n\nfrom . import exceptions\nfrom . import packet\nfrom . import server\nfrom . import asyncio_socket\n\n\nclass AsyncServer(server.Server):\n    \"\"\"An Engine.IO server for asyncio.\n\n    This class implements a fully compliant Engine.IO web server with support\n    for websocket and long-polling transports, compatible with the asyncio\n    framework on Python 3.5 or newer.\n\n    :param async_mode: The asynchronous model to use. See the Deployment\n                       section in the documentation for a description of the\n                       available options. Valid async modes are \"aiohttp\",\n                       \"sanic\", \"tornado\" and \"asgi\". If this argument is not\n                       given, \"aiohttp\" is tried first, followed by \"sanic\",\n                       \"tornado\", and finally \"asgi\". The first async mode that\n                       has all its dependencies installed is the one that is\n                       chosen.\n    :param ping_timeout: The time in seconds that the client waits for the\n                         server to respond before disconnecting.\n    :param ping_interval: The interval in seconds at which the client pings\n                          the server.\n    :param max_http_buffer_size: The maximum size of a message when using the\n                                 polling transport.\n    :param allow_upgrades: Whether to allow transport upgrades or not.\n    :param http_compression: Whether to compress packages when using the\n                             polling transport.\n    :param compression_threshold: Only compress messages when their byte size\n                                  is greater than this value.\n    :param cookie: Name of the HTTP cookie that contains the client session\n                   id. If set to ``None``, a cookie is not sent to the client.\n    :param cors_allowed_origins: List of origins that are allowed to connect\n                                 to this server. All origins are allowed by\n                                 default.\n    :param cors_credentials: Whether credentials (cookies, authentication) are\n                             allowed in requests to this server.\n    :param logger: To enable logging set to ``True`` or pass a logger object to\n                   use. To disable logging set to ``False``.\n    :param json: An alternative json module to use for encoding and decoding\n                 packets. Custom json modules must have ``dumps`` and ``loads``\n                 functions that are compatible with the standard library\n                 versions.\n    :param async_handlers: If set to ``True``, run message event handlers in\n                           non-blocking threads. To run handlers synchronously,\n                           set to ``False``. The default is ``True``.\n    :param kwargs: Reserved for future extensions, any additional parameters\n                   given as keyword arguments will be silently ignored.\n    \"\"\"\n    def is_asyncio_based(self):\n        return True\n\n    def async_modes(self):\n        return ['aiohttp', 'sanic', 'tornado', 'asgi']\n\n    def attach(self, app, engineio_path='engine.io'):\n        \"\"\"Attach the Engine.IO server to an application.\"\"\"\n        engineio_path = engineio_path.strip('/')\n        self._async['create_route'](app, self, '/{}/'.format(engineio_path))\n\n    async def send(self, sid, data, binary=None):\n        \"\"\"Send a message to a client.\n\n        :param sid: The session id of the recipient client.\n        :param data: The data to send to the client. Data can be of type\n                     ``str``, ``bytes``, ``list`` or ``dict``. If a ``list``\n                     or ``dict``, the data will be serialized as JSON.\n        :param binary: ``True`` to send packet as binary, ``False`` to send\n                       as text. If not given, unicode (Python 2) and str\n                       (Python 3) are sent as text, and str (Python 2) and\n                       bytes (Python 3) are sent as binary.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        try:\n            socket = self._get_socket(sid)\n        except KeyError:\n            # the socket is not available\n            self.logger.warning('Cannot send to sid %s', sid)\n            return\n        await socket.send(packet.Packet(packet.MESSAGE, data=data,\n                                        binary=binary))\n\n    async def get_session(self, sid):\n        \"\"\"Return the user session for a client.\n\n        :param sid: The session id of the client.\n\n        The return value is a dictionary. Modifications made to this\n        dictionary are not guaranteed to be preserved. If you want to modify\n        the user session, use the ``session`` context manager instead.\n        \"\"\"\n        socket = self._get_socket(sid)\n        return socket.session\n\n    async def save_session(self, sid, session):\n        \"\"\"Store the user session for a client.\n\n        :param sid: The session id of the client.\n        :param session: The session dictionary.\n        \"\"\"\n        socket = self._get_socket(sid)\n        socket.session = session\n\n    def session(self, sid):\n        \"\"\"Return the user session for a client with context manager syntax.\n\n        :param sid: The session id of the client.\n\n        This is a context manager that returns the user session dictionary for\n        the client. Any changes that are made to this dictionary inside the\n        context manager block are saved back to the session. Example usage::\n\n            @eio.on('connect')\n            def on_connect(sid, environ):\n                username = authenticate_user(environ)\n                if not username:\n                    return False\n                with eio.session(sid) as session:\n                    session['username'] = username\n\n            @eio.on('message')\n            def on_message(sid, msg):\n                async with eio.session(sid) as session:\n                    print('received message from ', session['username'])\n        \"\"\"\n        class _session_context_manager(object):\n            def __init__(self, server, sid):\n                self.server = server\n                self.sid = sid\n                self.session = None\n\n            async def __aenter__(self):\n                self.session = await self.server.get_session(sid)\n                return self.session\n\n            async def __aexit__(self, *args):\n                await self.server.save_session(sid, self.session)\n\n        return _session_context_manager(self, sid)\n\n    async def disconnect(self, sid=None):\n        \"\"\"Disconnect a client.\n\n        :param sid: The session id of the client to close. If this parameter\n                    is not given, then all clients are closed.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        if sid is not None:\n            try:\n                socket = self._get_socket(sid)\n            except KeyError:  # pragma: no cover\n                # the socket was already closed or gone\n                pass\n            else:\n                await socket.close()\n                del self.sockets[sid]\n        else:\n            await asyncio.wait([client.close()\n                                for client in six.itervalues(self.sockets)])\n            self.sockets = {}\n\n    async def handle_request(self, *args, **kwargs):\n        \"\"\"Handle an HTTP request from the client.\n\n        This is the entry point of the Engine.IO application. This function\n        returns the HTTP response to deliver to the client.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        translate_request = self._async['translate_request']\n        if asyncio.iscoroutinefunction(translate_request):\n            environ = await translate_request(*args, **kwargs)\n        else:\n            environ = translate_request(*args, **kwargs)\n        method = environ['REQUEST_METHOD']\n        query = urllib.parse.parse_qs(environ.get('QUERY_STRING', ''))\n\n        sid = query['sid'][0] if 'sid' in query else None\n        b64 = False\n        jsonp = False\n        jsonp_index = None\n\n        if 'b64' in query:\n            if query['b64'][0] == \"1\" or query['b64'][0].lower() == \"true\":\n                b64 = True\n        if 'j' in query:\n            jsonp = True\n            try:\n                jsonp_index = int(query['j'][0])\n            except (ValueError, KeyError, IndexError):\n                # Invalid JSONP index number\n                pass\n\n        if jsonp and jsonp_index is None:\n            self.logger.warning('Invalid JSONP index number')\n            r = self._bad_request()\n        elif method == 'GET':\n            if sid is None:\n                transport = query.get('transport', ['polling'])[0]\n                if transport != 'polling' and transport != 'websocket':\n                    self.logger.warning('Invalid transport %s', transport)\n                    r = self._bad_request()\n                else:\n                    r = await self._handle_connect(environ, transport,\n                                                   b64, jsonp_index)\n            else:\n                if sid not in self.sockets:\n                    self.logger.warning('Invalid session %s', sid)\n                    r = self._bad_request()\n                else:\n                    socket = self._get_socket(sid)\n                    try:\n                        packets = await socket.handle_get_request(environ)\n                        if isinstance(packets, list):\n                            r = self._ok(packets, b64=b64,\n                                         jsonp_index=jsonp_index)\n                        else:\n                            r = packets\n                    except exceptions.EngineIOError:\n                        if sid in self.sockets:  # pragma: no cover\n                            await self.disconnect(sid)\n                        r = self._bad_request()\n                    if sid in self.sockets and self.sockets[sid].closed:\n                        del self.sockets[sid]\n        elif method == 'POST':\n            if sid is None or sid not in self.sockets:\n                self.logger.warning('Invalid session %s', sid)\n                r = self._bad_request()\n            else:\n                socket = self._get_socket(sid)\n                try:\n                    await socket.handle_post_request(environ)\n                    r = self._ok(jsonp_index=jsonp_index)\n                except exceptions.EngineIOError:\n                    if sid in self.sockets:  # pragma: no cover\n                        await self.disconnect(sid)\n                    r = self._bad_request()\n                except:  # pragma: no cover\n                    # for any other unexpected errors, we log the error\n                    # and keep going\n                    self.logger.exception('post request handler error')\n                    r = self._ok(jsonp_index=jsonp_index)\n        elif method == 'OPTIONS':\n            r = self._ok()\n        else:\n            self.logger.warning('Method %s not supported', method)\n            r = self._method_not_found()\n        if not isinstance(r, dict):\n            return r\n        if self.http_compression and \\\n                len(r['response']) >= self.compression_threshold:\n            encodings = [e.split(';')[0].strip() for e in\n                         environ.get('HTTP_ACCEPT_ENCODING', '').split(',')]\n            for encoding in encodings:\n                if encoding in self.compression_methods:\n                    r['response'] = \\\n                        getattr(self, '_' + encoding)(r['response'])\n                    r['headers'] += [('Content-Encoding', encoding)]\n                    break\n        cors_headers = self._cors_headers(environ)\n        make_response = self._async['make_response']\n        if asyncio.iscoroutinefunction(make_response):\n            response = await make_response(r['status'],\n                                           r['headers'] + cors_headers,\n                                           r['response'], environ)\n        else:\n            response = make_response(r['status'], r['headers'] + cors_headers,\n                                     r['response'], environ)\n        return response\n\n    def start_background_task(self, target, *args, **kwargs):\n        \"\"\"Start a background task using the appropriate async model.\n\n        This is a utility function that applications can use to start a\n        background task using the method that is compatible with the\n        selected async mode.\n\n        :param target: the target function to execute.\n        :param args: arguments to pass to the function.\n        :param kwargs: keyword arguments to pass to the function.\n\n        The return value is a ``asyncio.Task`` object.\n        \"\"\"\n        return asyncio.ensure_future(target(*args, **kwargs))\n\n    async def sleep(self, seconds=0):\n        \"\"\"Sleep for the requested amount of time using the appropriate async\n        model.\n\n        This is a utility function that applications can use to put a task to\n        sleep without having to worry about using the correct call for the\n        selected async mode.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await asyncio.sleep(seconds)\n\n    def create_queue(self, *args, **kwargs):\n        \"\"\"Create a queue object using the appropriate async model.\n\n        This is a utility function that applications can use to create a queue\n        without having to worry about using the correct call for the selected\n        async mode. For asyncio based async modes, this returns an instance of\n        ``asyncio.Queue``.\n        \"\"\"\n        return asyncio.Queue(*args, **kwargs)\n\n    def get_queue_empty_exception(self):\n        \"\"\"Return the queue empty exception for the appropriate async model.\n\n        This is a utility function that applications can use to work with a\n        queue without having to worry about using the correct call for the\n        selected async mode. For asyncio based async modes, this returns an\n        instance of ``asyncio.QueueEmpty``.\n        \"\"\"\n        return asyncio.QueueEmpty\n\n    def create_event(self, *args, **kwargs):\n        \"\"\"Create an event object using the appropriate async model.\n\n        This is a utility function that applications can use to create an\n        event without having to worry about using the correct call for the\n        selected async mode. For asyncio based async modes, this returns\n        an instance of ``asyncio.Event``.\n        \"\"\"\n        return asyncio.Event(*args, **kwargs)\n\n    async def _handle_connect(self, environ, transport, b64=False,\n                              jsonp_index=None):\n        \"\"\"Handle a client connection request.\"\"\"\n        if self.start_service_task:\n            # start the service task to monitor connected clients\n            self.start_service_task = False\n            self.start_background_task(self._service_task)\n\n        sid = self._generate_id()\n        s = asyncio_socket.AsyncSocket(self, sid)\n        self.sockets[sid] = s\n\n        pkt = packet.Packet(\n            packet.OPEN, {'sid': sid,\n                          'upgrades': self._upgrades(sid, transport),\n                          'pingTimeout': int(self.ping_timeout * 1000),\n                          'pingInterval': int(self.ping_interval * 1000)})\n        await s.send(pkt)\n\n        ret = await self._trigger_event('connect', sid, environ,\n                                        run_async=False)\n        if ret is False:\n            del self.sockets[sid]\n            self.logger.warning('Application rejected connection')\n            return self._unauthorized()\n\n        if transport == 'websocket':\n            ret = await s.handle_get_request(environ)\n            if s.closed:\n                # websocket connection ended, so we are done\n                del self.sockets[sid]\n            return ret\n        else:\n            s.connected = True\n            headers = None\n            if self.cookie:\n                headers = [('Set-Cookie', self.cookie + '=' + sid)]\n            try:\n                return self._ok(await s.poll(), headers=headers, b64=b64,\n                                jsonp_index=jsonp_index)\n            except exceptions.QueueEmpty:\n                return self._bad_request()\n\n    async def _trigger_event(self, event, *args, **kwargs):\n        \"\"\"Invoke an event handler.\"\"\"\n        run_async = kwargs.pop('run_async', False)\n        ret = None\n        if event in self.handlers:\n            if asyncio.iscoroutinefunction(self.handlers[event]) is True:\n                if run_async:\n                    return self.start_background_task(self.handlers[event],\n                                                      *args)\n                else:\n                    try:\n                        ret = await self.handlers[event](*args)\n                    except asyncio.CancelledError:  # pragma: no cover\n                        pass\n                    except:\n                        self.logger.exception(event + ' async handler error')\n                        if event == 'connect':\n                            # if connect handler raised error we reject the\n                            # connection\n                            return False\n            else:\n                if run_async:\n                    async def async_handler():\n                        return self.handlers[event](*args)\n\n                    return self.start_background_task(async_handler)\n                else:\n                    try:\n                        ret = self.handlers[event](*args)\n                    except:\n                        self.logger.exception(event + ' handler error')\n                        if event == 'connect':\n                            # if connect handler raised error we reject the\n                            # connection\n                            return False\n        return ret\n\n    async def _service_task(self):  # pragma: no cover\n        \"\"\"Monitor connected clients and clean up those that time out.\"\"\"\n        while True:\n            if len(self.sockets) == 0:\n                # nothing to do\n                await self.sleep(self.ping_timeout)\n                continue\n\n            # go through the entire client list in a ping interval cycle\n            sleep_interval = self.ping_timeout / len(self.sockets)\n\n            try:\n                # iterate over the current clients\n                for socket in self.sockets.copy().values():\n                    if not socket.closing and not socket.closed:\n                        await socket.check_ping_timeout()\n                    await self.sleep(sleep_interval)\n            except (SystemExit, KeyboardInterrupt, asyncio.CancelledError):\n                self.logger.info('service task canceled')\n                break\n            except:\n                if asyncio.get_event_loop().is_closed():\n                    self.logger.info('event loop is closed, exiting service '\n                                     'task')\n                    break\n\n                # an unexpected exception has occurred, log it and continue\n                self.logger.exception('service task exception')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/asyncio_socket.py",
    "content": "import asyncio\nimport six\nimport sys\nimport time\n\nfrom . import exceptions\nfrom . import packet\nfrom . import payload\nfrom . import socket\n\n\nclass AsyncSocket(socket.Socket):\n    async def poll(self):\n        \"\"\"Wait for packets to send to the client.\"\"\"\n        try:\n            packets = [await asyncio.wait_for(self.queue.get(),\n                                              self.server.ping_timeout)]\n            self.queue.task_done()\n        except (asyncio.TimeoutError, asyncio.CancelledError):\n            raise exceptions.QueueEmpty()\n        if packets == [None]:\n            return []\n        try:\n            packets.append(self.queue.get_nowait())\n            self.queue.task_done()\n        except asyncio.QueueEmpty:\n            pass\n        return packets\n\n    async def receive(self, pkt):\n        \"\"\"Receive packet from the client.\"\"\"\n        self.server.logger.info('%s: Received packet %s data %s',\n                                self.sid, packet.packet_names[pkt.packet_type],\n                                pkt.data if not isinstance(pkt.data, bytes)\n                                else '<binary>')\n        if pkt.packet_type == packet.PING:\n            self.last_ping = time.time()\n            await self.send(packet.Packet(packet.PONG, pkt.data))\n        elif pkt.packet_type == packet.MESSAGE:\n            await self.server._trigger_event(\n                'message', self.sid, pkt.data,\n                run_async=self.server.async_handlers)\n        elif pkt.packet_type == packet.UPGRADE:\n            await self.send(packet.Packet(packet.NOOP))\n        elif pkt.packet_type == packet.CLOSE:\n            await self.close(wait=False, abort=True)\n        else:\n            raise exceptions.UnknownPacketError()\n\n    async def check_ping_timeout(self):\n        \"\"\"Make sure the client is still sending pings.\n\n        This helps detect disconnections for long-polling clients.\n        \"\"\"\n        if self.closed:\n            raise exceptions.SocketIsClosedError()\n        if time.time() - self.last_ping > self.server.ping_interval + 5:\n            self.server.logger.info('%s: Client is gone, closing socket',\n                                    self.sid)\n            # Passing abort=False here will cause close() to write a\n            # CLOSE packet. This has the effect of updating half-open sockets\n            # to their correct state of disconnected\n            await self.close(wait=False, abort=False)\n            return False\n        return True\n\n    async def send(self, pkt):\n        \"\"\"Send a packet to the client.\"\"\"\n        if not await self.check_ping_timeout():\n            return\n        if self.upgrading:\n            self.packet_backlog.append(pkt)\n        else:\n            await self.queue.put(pkt)\n        self.server.logger.info('%s: Sending packet %s data %s',\n                                self.sid, packet.packet_names[pkt.packet_type],\n                                pkt.data if not isinstance(pkt.data, bytes)\n                                else '<binary>')\n\n    async def handle_get_request(self, environ):\n        \"\"\"Handle a long-polling GET request from the client.\"\"\"\n        connections = [\n            s.strip()\n            for s in environ.get('HTTP_CONNECTION', '').lower().split(',')]\n        transport = environ.get('HTTP_UPGRADE', '').lower()\n        if 'upgrade' in connections and transport in self.upgrade_protocols:\n            self.server.logger.info('%s: Received request to upgrade to %s',\n                                    self.sid, transport)\n            return await getattr(self, '_upgrade_' + transport)(environ)\n        try:\n            packets = await self.poll()\n        except exceptions.QueueEmpty:\n            exc = sys.exc_info()\n            await self.close(wait=False)\n            six.reraise(*exc)\n        return packets\n\n    async def handle_post_request(self, environ):\n        \"\"\"Handle a long-polling POST request from the client.\"\"\"\n        length = int(environ.get('CONTENT_LENGTH', '0'))\n        if length > self.server.max_http_buffer_size:\n            raise exceptions.ContentTooLongError()\n        else:\n            body = await environ['wsgi.input'].read(length)\n            p = payload.Payload(encoded_payload=body)\n            for pkt in p.packets:\n                await self.receive(pkt)\n\n    async def close(self, wait=True, abort=False):\n        \"\"\"Close the socket connection.\"\"\"\n        if not self.closed and not self.closing:\n            self.closing = True\n            await self.server._trigger_event('disconnect', self.sid)\n            if not abort:\n                await self.send(packet.Packet(packet.CLOSE))\n            self.closed = True\n            if wait:\n                await self.queue.join()\n\n    async def _upgrade_websocket(self, environ):\n        \"\"\"Upgrade the connection from polling to websocket.\"\"\"\n        if self.upgraded:\n            raise IOError('Socket has been upgraded already')\n        if self.server._async['websocket'] is None:\n            # the selected async mode does not support websocket\n            return self.server._bad_request()\n        ws = self.server._async['websocket'](self._websocket_handler)\n        return await ws(environ)\n\n    async def _websocket_handler(self, ws):\n        \"\"\"Engine.IO handler for websocket transport.\"\"\"\n        if self.connected:\n            # the socket was already connected, so this is an upgrade\n            self.upgrading = True  # hold packet sends during the upgrade\n\n            try:\n                pkt = await ws.wait()\n            except IOError:  # pragma: no cover\n                return\n            decoded_pkt = packet.Packet(encoded_packet=pkt)\n            if decoded_pkt.packet_type != packet.PING or \\\n                    decoded_pkt.data != 'probe':\n                self.server.logger.info(\n                    '%s: Failed websocket upgrade, no PING packet', self.sid)\n                return\n            await ws.send(packet.Packet(\n                packet.PONG,\n                data=six.text_type('probe')).encode(always_bytes=False))\n            await self.queue.put(packet.Packet(packet.NOOP))  # end poll\n\n            try:\n                pkt = await ws.wait()\n            except IOError:  # pragma: no cover\n                return\n            decoded_pkt = packet.Packet(encoded_packet=pkt)\n            if decoded_pkt.packet_type != packet.UPGRADE:\n                self.upgraded = False\n                self.server.logger.info(\n                    ('%s: Failed websocket upgrade, expected UPGRADE packet, '\n                     'received %s instead.'),\n                    self.sid, pkt)\n                return\n            self.upgraded = True\n\n            # flush any packets that were sent during the upgrade\n            for pkt in self.packet_backlog:\n                await self.queue.put(pkt)\n            self.packet_backlog = []\n            self.upgrading = False\n        else:\n            self.connected = True\n            self.upgraded = True\n\n        # start separate writer thread\n        async def writer():\n            while True:\n                packets = None\n                try:\n                    packets = await self.poll()\n                except exceptions.QueueEmpty:\n                    break\n                if not packets:\n                    # empty packet list returned -> connection closed\n                    break\n                try:\n                    for pkt in packets:\n                        await ws.send(pkt.encode(always_bytes=False))\n                except:\n                    break\n        writer_task = asyncio.ensure_future(writer())\n\n        self.server.logger.info(\n            '%s: Upgrade to websocket successful', self.sid)\n\n        while True:\n            p = None\n            wait_task = asyncio.ensure_future(ws.wait())\n            try:\n                p = await asyncio.wait_for(wait_task, self.server.ping_timeout)\n            except asyncio.CancelledError:  # pragma: no cover\n                # there is a bug (https://bugs.python.org/issue30508) in\n                # asyncio that causes a \"Task exception never retrieved\" error\n                # to appear when wait_task raises an exception before it gets\n                # cancelled. Calling wait_task.exception() prevents the error\n                # from being issued in Python 3.6, but causes other errors in\n                # other versions, so we run it with all errors suppressed and\n                # hope for the best.\n                try:\n                    wait_task.exception()\n                except:\n                    pass\n                break\n            except:\n                break\n            if p is None:\n                # connection closed by client\n                break\n            if isinstance(p, six.text_type):  # pragma: no cover\n                p = p.encode('utf-8')\n            pkt = packet.Packet(encoded_packet=p)\n            try:\n                await self.receive(pkt)\n            except exceptions.UnknownPacketError:  # pragma: no cover\n                pass\n            except exceptions.SocketIsClosedError:  # pragma: no cover\n                self.server.logger.info('Receive error -- socket is closed')\n                break\n            except:  # pragma: no cover\n                # if we get an unexpected exception we log the error and exit\n                # the connection properly\n                self.server.logger.exception('Unknown receive error')\n\n        await self.queue.put(None)  # unlock the writer task so it can exit\n        await asyncio.wait_for(writer_task, timeout=None)\n        await self.close(wait=False, abort=True)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/client.py",
    "content": "import logging\ntry:\n    import queue\nexcept ImportError:  # pragma: no cover\n    import Queue as queue\nimport signal\nimport threading\nimport time\n\nimport six\nfrom six.moves import urllib\ntry:\n    import requests\nexcept ImportError:  # pragma: no cover\n    requests = None\ntry:\n    import websocket\nexcept ImportError:  # pragma: no cover\n    websocket = None\nfrom . import exceptions\nfrom . import packet\nfrom . import payload\n\ndefault_logger = logging.getLogger('engineio.client')\nconnected_clients = []\n\nif six.PY2:  # pragma: no cover\n    ConnectionError = OSError\n\n\ndef signal_handler(sig, frame):\n    \"\"\"SIGINT handler.\n\n    Disconnect all active clients and then invoke the original signal handler.\n    \"\"\"\n    for client in connected_clients[:]:\n        if client.is_asyncio_based():\n            client.start_background_task(client.disconnect, abort=True)\n        else:\n            client.disconnect(abort=True)\n    return original_signal_handler(sig, frame)\n\n\noriginal_signal_handler = signal.signal(signal.SIGINT, signal_handler)\n\n\nclass Client(object):\n    \"\"\"An Engine.IO client.\n\n    This class implements a fully compliant Engine.IO web client with support\n    for websocket and long-polling transports.\n\n    :param logger: To enable logging set to ``True`` or pass a logger object to\n                   use. To disable logging set to ``False``. The default is\n                   ``False``.\n    :param json: An alternative json module to use for encoding and decoding\n                 packets. Custom json modules must have ``dumps`` and ``loads``\n                 functions that are compatible with the standard library\n                 versions.\n    \"\"\"\n    event_names = ['connect', 'disconnect', 'message']\n\n    def __init__(self, logger=False, json=None):\n        self.handlers = {}\n        self.base_url = None\n        self.transports = None\n        self.current_transport = None\n        self.sid = None\n        self.upgrades = None\n        self.ping_interval = None\n        self.ping_timeout = None\n        self.pong_received = True\n        self.http = None\n        self.ws = None\n        self.read_loop_task = None\n        self.write_loop_task = None\n        self.ping_loop_task = None\n        self.ping_loop_event = self.create_event()\n        self.queue = None\n        self.state = 'disconnected'\n\n        if json is not None:\n            packet.Packet.json = json\n        if not isinstance(logger, bool):\n            self.logger = logger\n        else:\n            self.logger = default_logger\n            if not logging.root.handlers and \\\n                    self.logger.level == logging.NOTSET:\n                if logger:\n                    self.logger.setLevel(logging.INFO)\n                else:\n                    self.logger.setLevel(logging.ERROR)\n                self.logger.addHandler(logging.StreamHandler())\n\n    def is_asyncio_based(self):\n        return False\n\n    def on(self, event, handler=None):\n        \"\"\"Register an event handler.\n\n        :param event: The event name. Can be ``'connect'``, ``'message'`` or\n                      ``'disconnect'``.\n        :param handler: The function that should be invoked to handle the\n                        event. When this parameter is not given, the method\n                        acts as a decorator for the handler function.\n\n        Example usage::\n\n            # as a decorator:\n            @eio.on('connect')\n            def connect_handler():\n                print('Connection request')\n\n            # as a method:\n            def message_handler(msg):\n                print('Received message: ', msg)\n                eio.send('response')\n            eio.on('message', message_handler)\n        \"\"\"\n        if event not in self.event_names:\n            raise ValueError('Invalid event')\n\n        def set_handler(handler):\n            self.handlers[event] = handler\n            return handler\n\n        if handler is None:\n            return set_handler\n        set_handler(handler)\n\n    def connect(self, url, headers={}, transports=None,\n                engineio_path='engine.io'):\n        \"\"\"Connect to an Engine.IO server.\n\n        :param url: The URL of the Engine.IO server. It can include custom\n                    query string parameters if required by the server.\n        :param headers: A dictionary with custom headers to send with the\n                        connection request.\n        :param transports: The list of allowed transports. Valid transports\n                           are ``'polling'`` and ``'websocket'``. If not\n                           given, the polling transport is connected first,\n                           then an upgrade to websocket is attempted.\n        :param engineio_path: The endpoint where the Engine.IO server is\n                              installed. The default value is appropriate for\n                              most cases.\n\n        Example usage::\n\n            eio = engineio.Client()\n            eio.connect('http://localhost:5000')\n        \"\"\"\n        if self.state != 'disconnected':\n            raise ValueError('Client is not in a disconnected state')\n        valid_transports = ['polling', 'websocket']\n        if transports is not None:\n            if isinstance(transports, six.string_types):\n                transports = [transports]\n            transports = [transport for transport in transports\n                          if transport in valid_transports]\n            if not transports:\n                raise ValueError('No valid transports provided')\n        self.transports = transports or valid_transports\n        self.queue = self.create_queue()\n        return getattr(self, '_connect_' + self.transports[0])(\n            url, headers, engineio_path)\n\n    def wait(self):\n        \"\"\"Wait until the connection with the server ends.\n\n        Client applications can use this function to block the main thread\n        during the life of the connection.\n        \"\"\"\n        if self.read_loop_task:\n            self.read_loop_task.join()\n\n    def send(self, data, binary=None):\n        \"\"\"Send a message to a client.\n\n        :param data: The data to send to the client. Data can be of type\n                     ``str``, ``bytes``, ``list`` or ``dict``. If a ``list``\n                     or ``dict``, the data will be serialized as JSON.\n        :param binary: ``True`` to send packet as binary, ``False`` to send\n                       as text. If not given, unicode (Python 2) and str\n                       (Python 3) are sent as text, and str (Python 2) and\n                       bytes (Python 3) are sent as binary.\n        \"\"\"\n        self._send_packet(packet.Packet(packet.MESSAGE, data=data,\n                                        binary=binary))\n\n    def disconnect(self, abort=False):\n        \"\"\"Disconnect from the server.\n\n        :param abort: If set to ``True``, do not wait for background tasks\n                      associated with the connection to end.\n        \"\"\"\n        if self.state == 'connected':\n            self._send_packet(packet.Packet(packet.CLOSE))\n            self.queue.put(None)\n            self.state = 'disconnecting'\n            self._trigger_event('disconnect', run_async=False)\n            if self.current_transport == 'websocket':\n                self.ws.close()\n            if not abort:\n                self.read_loop_task.join()\n            self.state = 'disconnected'\n            try:\n                connected_clients.remove(self)\n            except ValueError:  # pragma: no cover\n                pass\n        self._reset()\n\n    def transport(self):\n        \"\"\"Return the name of the transport currently in use.\n\n        The possible values returned by this function are ``'polling'`` and\n        ``'websocket'``.\n        \"\"\"\n        return self.current_transport\n\n    def start_background_task(self, target, *args, **kwargs):\n        \"\"\"Start a background task.\n\n        This is a utility function that applications can use to start a\n        background task.\n\n        :param target: the target function to execute.\n        :param args: arguments to pass to the function.\n        :param kwargs: keyword arguments to pass to the function.\n\n        This function returns an object compatible with the `Thread` class in\n        the Python standard library. The `start()` method on this object is\n        already called by this function.\n        \"\"\"\n        th = threading.Thread(target=target, args=args, kwargs=kwargs)\n        th.start()\n        return th\n\n    def sleep(self, seconds=0):\n        \"\"\"Sleep for the requested amount of time.\"\"\"\n        return time.sleep(seconds)\n\n    def create_queue(self, *args, **kwargs):\n        \"\"\"Create a queue object.\"\"\"\n        q = queue.Queue(*args, **kwargs)\n        q.Empty = queue.Empty\n        return q\n\n    def create_event(self, *args, **kwargs):\n        \"\"\"Create an event object.\"\"\"\n        return threading.Event(*args, **kwargs)\n\n    def _reset(self):\n        self.state = 'disconnected'\n        self.sid = None\n\n    def _connect_polling(self, url, headers, engineio_path):\n        \"\"\"Establish a long-polling connection to the Engine.IO server.\"\"\"\n        if requests is None:  # pragma: no cover\n            # not installed\n            self.logger.error('requests package is not installed -- cannot '\n                              'send HTTP requests!')\n            return\n        self.base_url = self._get_engineio_url(url, engineio_path, 'polling')\n        self.logger.info('Attempting polling connection to ' + self.base_url)\n        r = self._send_request(\n            'GET', self.base_url + self._get_url_timestamp(), headers=headers)\n        if r is None:\n            self._reset()\n            raise exceptions.ConnectionError(\n                'Connection refused by the server')\n        if r.status_code != 200:\n            raise exceptions.ConnectionError(\n                'Unexpected status code {} in server response'.format(\n                    r.status_code))\n        try:\n            p = payload.Payload(encoded_payload=r.content)\n        except ValueError:\n            six.raise_from(exceptions.ConnectionError(\n                'Unexpected response from server'), None)\n        open_packet = p.packets[0]\n        if open_packet.packet_type != packet.OPEN:\n            raise exceptions.ConnectionError(\n                'OPEN packet not returned by server')\n        self.logger.info(\n            'Polling connection accepted with ' + str(open_packet.data))\n        self.sid = open_packet.data['sid']\n        self.upgrades = open_packet.data['upgrades']\n        self.ping_interval = open_packet.data['pingInterval'] / 1000.0\n        self.ping_timeout = open_packet.data['pingTimeout'] / 1000.0\n        self.current_transport = 'polling'\n        self.base_url += '&sid=' + self.sid\n\n        self.state = 'connected'\n        connected_clients.append(self)\n        self._trigger_event('connect', run_async=False)\n\n        for pkt in p.packets[1:]:\n            self._receive_packet(pkt)\n\n        if 'websocket' in self.upgrades and 'websocket' in self.transports:\n            # attempt to upgrade to websocket\n            if self._connect_websocket(url, headers, engineio_path):\n                # upgrade to websocket succeeded, we're done here\n                return\n\n        # start background tasks associated with this client\n        self.ping_loop_task = self.start_background_task(self._ping_loop)\n        self.write_loop_task = self.start_background_task(self._write_loop)\n        self.read_loop_task = self.start_background_task(\n            self._read_loop_polling)\n\n    def _connect_websocket(self, url, headers, engineio_path):\n        \"\"\"Establish or upgrade to a WebSocket connection with the server.\"\"\"\n        if websocket is None:  # pragma: no cover\n            # not installed\n            self.logger.warning('websocket-client package not installed, only '\n                                'polling transport is available')\n            return False\n        websocket_url = self._get_engineio_url(url, engineio_path, 'websocket')\n        if self.sid:\n            self.logger.info(\n                'Attempting WebSocket upgrade to ' + websocket_url)\n            upgrade = True\n            websocket_url += '&sid=' + self.sid\n        else:\n            upgrade = False\n            self.base_url = websocket_url\n            self.logger.info(\n                'Attempting WebSocket connection to ' + websocket_url)\n\n        # get the cookies from the long-polling connection so that they can\n        # also be sent the the WebSocket route\n        cookies = None\n        if self.http:\n            cookies = '; '.join([\"{}={}\".format(cookie.name, cookie.value)\n                                 for cookie in self.http.cookies])\n        try:\n            ws = websocket.create_connection(\n                websocket_url + self._get_url_timestamp(), header=headers,\n                cookie=cookies)\n        except ConnectionError:\n            if upgrade:\n                self.logger.warning(\n                    'WebSocket upgrade failed: connection error')\n                return False\n            else:\n                raise exceptions.ConnectionError('Connection error')\n        if upgrade:\n            p = packet.Packet(packet.PING,\n                              data=six.text_type('probe')).encode()\n            try:\n                ws.send(p)\n            except Exception as e:  # pragma: no cover\n                self.logger.warning(\n                    'WebSocket upgrade failed: unexpected send exception: %s',\n                    str(e))\n                return False\n            try:\n                p = ws.recv()\n            except Exception as e:  # pragma: no cover\n                self.logger.warning(\n                    'WebSocket upgrade failed: unexpected recv exception: %s',\n                    str(e))\n                return False\n            pkt = packet.Packet(encoded_packet=p)\n            if pkt.packet_type != packet.PONG or pkt.data != 'probe':\n                self.logger.warning(\n                    'WebSocket upgrade failed: no PONG packet')\n                return False\n            p = packet.Packet(packet.UPGRADE).encode()\n            try:\n                ws.send(p)\n            except Exception as e:  # pragma: no cover\n                self.logger.warning(\n                    'WebSocket upgrade failed: unexpected send exception: %s',\n                    str(e))\n                return False\n            self.current_transport = 'websocket'\n            self.logger.info('WebSocket upgrade was successful')\n        else:\n            try:\n                p = ws.recv()\n            except Exception as e:  # pragma: no cover\n                raise exceptions.ConnectionError(\n                    'Unexpected recv exception: ' + str(e))\n            open_packet = packet.Packet(encoded_packet=p)\n            if open_packet.packet_type != packet.OPEN:\n                raise exceptions.ConnectionError('no OPEN packet')\n            self.logger.info(\n                'WebSocket connection accepted with ' + str(open_packet.data))\n            self.sid = open_packet.data['sid']\n            self.upgrades = open_packet.data['upgrades']\n            self.ping_interval = open_packet.data['pingInterval'] / 1000.0\n            self.ping_timeout = open_packet.data['pingTimeout'] / 1000.0\n            self.current_transport = 'websocket'\n\n            self.state = 'connected'\n            connected_clients.append(self)\n            self._trigger_event('connect', run_async=False)\n        self.ws = ws\n\n        # start background tasks associated with this client\n        self.ping_loop_task = self.start_background_task(self._ping_loop)\n        self.write_loop_task = self.start_background_task(self._write_loop)\n        self.read_loop_task = self.start_background_task(\n            self._read_loop_websocket)\n        return True\n\n    def _receive_packet(self, pkt):\n        \"\"\"Handle incoming packets from the server.\"\"\"\n        packet_name = packet.packet_names[pkt.packet_type] \\\n            if pkt.packet_type < len(packet.packet_names) else 'UNKNOWN'\n        self.logger.info(\n            'Received packet %s data %s', packet_name,\n            pkt.data if not isinstance(pkt.data, bytes) else '<binary>')\n        if pkt.packet_type == packet.MESSAGE:\n            self._trigger_event('message', pkt.data, run_async=True)\n        elif pkt.packet_type == packet.PONG:\n            self.pong_received = True\n        elif pkt.packet_type == packet.CLOSE:\n            self.disconnect(abort=True)\n        elif pkt.packet_type == packet.NOOP:\n            pass\n        else:\n            self.logger.error('Received unexpected packet of type %s',\n                              pkt.packet_type)\n\n    def _send_packet(self, pkt):\n        \"\"\"Queue a packet to be sent to the server.\"\"\"\n        if self.state != 'connected':\n            return\n        self.queue.put(pkt)\n        self.logger.info(\n            'Sending packet %s data %s',\n            packet.packet_names[pkt.packet_type],\n            pkt.data if not isinstance(pkt.data, bytes) else '<binary>')\n\n    def _send_request(\n            self, method, url, headers=None, body=None):  # pragma: no cover\n        if self.http is None:\n            self.http = requests.Session()\n        try:\n            return self.http.request(method, url, headers=headers, data=body)\n        except requests.exceptions.RequestException:\n            pass\n\n    def _trigger_event(self, event, *args, **kwargs):\n        \"\"\"Invoke an event handler.\"\"\"\n        run_async = kwargs.pop('run_async', False)\n        if event in self.handlers:\n            if run_async:\n                return self.start_background_task(self.handlers[event], *args)\n            else:\n                try:\n                    return self.handlers[event](*args)\n                except:\n                    self.logger.exception(event + ' handler error')\n\n    def _get_engineio_url(self, url, engineio_path, transport):\n        \"\"\"Generate the Engine.IO connection URL.\"\"\"\n        engineio_path = engineio_path.strip('/')\n        parsed_url = urllib.parse.urlparse(url)\n\n        if transport == 'polling':\n            scheme = 'http'\n        elif transport == 'websocket':\n            scheme = 'ws'\n        else:  # pragma: no cover\n            raise ValueError('invalid transport')\n        if parsed_url.scheme in ['https', 'wss']:\n            scheme += 's'\n\n        return ('{scheme}://{netloc}/{path}/?{query}'\n                '{sep}transport={transport}&EIO=3').format(\n                    scheme=scheme, netloc=parsed_url.netloc,\n                    path=engineio_path, query=parsed_url.query,\n                    sep='&' if parsed_url.query else '',\n                    transport=transport)\n\n    def _get_url_timestamp(self):\n        \"\"\"Generate the Engine.IO query string timestamp.\"\"\"\n        return '&t=' + str(time.time())\n\n    def _ping_loop(self):\n        \"\"\"This background task sends a PING to the server at the requested\n        interval.\n        \"\"\"\n        self.pong_received = True\n        self.ping_loop_event.clear()\n        while self.state == 'connected':\n            if not self.pong_received:\n                self.logger.info(\n                    'PONG response has not been received, aborting')\n                if self.ws:\n                    self.ws.close()\n                self.queue.put(None)\n                break\n            self.pong_received = False\n            self._send_packet(packet.Packet(packet.PING))\n            self.ping_loop_event.wait(timeout=self.ping_interval)\n        self.logger.info('Exiting ping task')\n\n    def _read_loop_polling(self):\n        \"\"\"Read packets by polling the Engine.IO server.\"\"\"\n        while self.state == 'connected':\n            self.logger.info(\n                'Sending polling GET request to ' + self.base_url)\n            r = self._send_request(\n                'GET', self.base_url + self._get_url_timestamp())\n            if r is None:\n                self.logger.warning(\n                    'Connection refused by the server, aborting')\n                self.queue.put(None)\n                break\n            if r.status_code != 200:\n                self.logger.warning('Unexpected status code %s in server '\n                                    'response, aborting', r.status_code)\n                self.queue.put(None)\n                break\n            try:\n                p = payload.Payload(encoded_payload=r.content)\n            except ValueError:\n                self.logger.warning(\n                    'Unexpected packet from server, aborting')\n                self.queue.put(None)\n                break\n            for pkt in p.packets:\n                self._receive_packet(pkt)\n\n        self.logger.info('Waiting for write loop task to end')\n        self.write_loop_task.join()\n        self.logger.info('Waiting for ping loop task to end')\n        self.ping_loop_event.set()\n        self.ping_loop_task.join()\n        if self.state == 'connected':\n            self._trigger_event('disconnect', run_async=False)\n            try:\n                connected_clients.remove(self)\n            except ValueError:  # pragma: no cover\n                pass\n            self._reset()\n        self.logger.info('Exiting read loop task')\n\n    def _read_loop_websocket(self):\n        \"\"\"Read packets from the Engine.IO WebSocket connection.\"\"\"\n        while self.state == 'connected':\n            p = None\n            try:\n                p = self.ws.recv()\n            except websocket.WebSocketConnectionClosedException:\n                self.logger.warning(\n                    'WebSocket connection was closed, aborting')\n                self.queue.put(None)\n                break\n            except Exception as e:\n                self.logger.info(\n                    'Unexpected error \"%s\", aborting', str(e))\n                self.queue.put(None)\n                break\n            if isinstance(p, six.text_type):  # pragma: no cover\n                p = p.encode('utf-8')\n            pkt = packet.Packet(encoded_packet=p)\n            self._receive_packet(pkt)\n\n        self.logger.info('Waiting for write loop task to end')\n        self.write_loop_task.join()\n        self.logger.info('Waiting for ping loop task to end')\n        self.ping_loop_event.set()\n        self.ping_loop_task.join()\n        if self.state == 'connected':\n            self._trigger_event('disconnect', run_async=False)\n            try:\n                connected_clients.remove(self)\n            except ValueError:  # pragma: no cover\n                pass\n            self._reset()\n        self.logger.info('Exiting read loop task')\n\n    def _write_loop(self):\n        \"\"\"This background task sends packages to the server as they are\n        pushed to the send queue.\n        \"\"\"\n        while self.state == 'connected':\n            # to simplify the timeout handling, use the maximum of the\n            # ping interval and ping timeout as timeout, with an extra 5\n            # seconds grace period\n            timeout = max(self.ping_interval, self.ping_timeout) + 5\n            packets = None\n            try:\n                packets = [self.queue.get(timeout=timeout)]\n            except self.queue.Empty:\n                self.logger.error('packet queue is empty, aborting')\n                break\n            if packets == [None]:\n                self.queue.task_done()\n                packets = []\n            else:\n                while True:\n                    try:\n                        packets.append(self.queue.get(block=False))\n                    except self.queue.Empty:\n                        break\n                    if packets[-1] is None:\n                        packets = packets[:-1]\n                        self.queue.task_done()\n                        break\n            if not packets:\n                # empty packet list returned -> connection closed\n                break\n            if self.current_transport == 'polling':\n                p = payload.Payload(packets=packets)\n                r = self._send_request(\n                    'POST', self.base_url, body=p.encode(),\n                    headers={'Content-Type': 'application/octet-stream'})\n                for pkt in packets:\n                    self.queue.task_done()\n                if r is None:\n                    self.logger.warning(\n                        'Connection refused by the server, aborting')\n                    break\n                if r.status_code != 200:\n                    self.logger.warning('Unexpected status code %s in server '\n                                        'response, aborting', r.status_code)\n                    self._reset()\n                    break\n            else:\n                # websocket\n                try:\n                    for pkt in packets:\n                        encoded_packet = pkt.encode(always_bytes=False)\n                        if pkt.binary:\n                            self.ws.send_binary(encoded_packet)\n                        else:\n                            self.ws.send(encoded_packet)\n                        self.queue.task_done()\n                except websocket.WebSocketConnectionClosedException:\n                    self.logger.warning(\n                        'WebSocket connection was closed, aborting')\n                    break\n        self.logger.info('Exiting write loop task')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/exceptions.py",
    "content": "class EngineIOError(Exception):\n    pass\n\n\nclass ContentTooLongError(EngineIOError):\n    pass\n\n\nclass UnknownPacketError(EngineIOError):\n    pass\n\n\nclass QueueEmpty(EngineIOError):\n    pass\n\n\nclass SocketIsClosedError(EngineIOError):\n    pass\n\n\nclass ConnectionError(EngineIOError):\n    pass\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/middleware.py",
    "content": "import os\nfrom engineio.static_files import get_static_file\n\n\nclass WSGIApp(object):\n    \"\"\"WSGI application middleware for Engine.IO.\n\n    This middleware dispatches traffic to an Engine.IO application. It can\n    also serve a list of static files to the client, or forward unrelated\n    HTTP traffic to another WSGI application.\n\n    :param engineio_app: The Engine.IO server. Must be an instance of the\n                         ``engineio.Server`` class.\n    :param wsgi_app: The WSGI app that receives all other traffic.\n    :param static_files: A dictionary with static file mapping rules. See the\n                         documentation for details on this argument.\n    :param engineio_path: The endpoint where the Engine.IO application should\n                          be installed. The default value is appropriate for\n                          most cases.\n\n    Example usage::\n\n        import engineio\n        import eventlet\n\n        eio = engineio.Server()\n        app = engineio.WSGIApp(eio, static_files={\n            '/': {'content_type': 'text/html', 'filename': 'index.html'},\n            '/index.html': {'content_type': 'text/html',\n                            'filename': 'index.html'},\n        })\n        eventlet.wsgi.server(eventlet.listen(('', 8000)), app)\n    \"\"\"\n    def __init__(self, engineio_app, wsgi_app=None, static_files=None,\n                 engineio_path='engine.io'):\n        self.engineio_app = engineio_app\n        self.wsgi_app = wsgi_app\n        self.engineio_path = engineio_path.strip('/')\n        self.static_files = static_files or {}\n\n    def __call__(self, environ, start_response):\n        if 'gunicorn.socket' in environ:\n            # gunicorn saves the socket under environ['gunicorn.socket'], while\n            # eventlet saves it under environ['eventlet.input']. Eventlet also\n            # stores the socket inside a wrapper class, while gunicon writes it\n            # directly into the environment. To give eventlet's WebSocket\n            # module access to this socket when running under gunicorn, here we\n            # copy the socket to the eventlet format.\n            class Input(object):\n                def __init__(self, socket):\n                    self.socket = socket\n\n                def get_socket(self):\n                    return self.socket\n\n            environ['eventlet.input'] = Input(environ['gunicorn.socket'])\n        path = environ['PATH_INFO']\n        if path is not None and \\\n                path.startswith('/{0}/'.format(self.engineio_path)):\n            return self.engineio_app.handle_request(environ, start_response)\n        else:\n            static_file = get_static_file(path, self.static_files) \\\n                if self.static_files else None\n            if static_file:\n                if os.path.exists(static_file['filename']):\n                    start_response(\n                        '200 OK',\n                        [('Content-Type', static_file['content_type'])])\n                    with open(static_file['filename'], 'rb') as f:\n                        return [f.read()]\n                else:\n                    return self.not_found(start_response)\n            elif self.wsgi_app is not None:\n                return self.wsgi_app(environ, start_response)\n        return self.not_found(start_response)\n\n    def not_found(self, start_response):\n        start_response(\"404 Not Found\", [('Content-Type', 'text/plain')])\n        return [b'Not Found']\n\n\nclass Middleware(WSGIApp):\n    \"\"\"This class has been renamed to ``WSGIApp`` and is now deprecated.\"\"\"\n    def __init__(self, engineio_app, wsgi_app=None,\n                 engineio_path='engine.io'):\n        super(Middleware, self).__init__(engineio_app, wsgi_app,\n                                         engineio_path=engineio_path)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/packet.py",
    "content": "import base64\nimport json as _json\n\nimport six\n\n(OPEN, CLOSE, PING, PONG, MESSAGE, UPGRADE, NOOP) = (0, 1, 2, 3, 4, 5, 6)\npacket_names = ['OPEN', 'CLOSE', 'PING', 'PONG', 'MESSAGE', 'UPGRADE', 'NOOP']\n\nbinary_types = (six.binary_type, bytearray)\n\n\nclass Packet(object):\n    \"\"\"Engine.IO packet.\"\"\"\n\n    json = _json\n\n    def __init__(self, packet_type=NOOP, data=None, binary=None,\n                 encoded_packet=None):\n        self.packet_type = packet_type\n        self.data = data\n        if binary is not None:\n            self.binary = binary\n        elif isinstance(data, six.text_type):\n            self.binary = False\n        elif isinstance(data, binary_types):\n            self.binary = True\n        else:\n            self.binary = False\n        if encoded_packet:\n            self.decode(encoded_packet)\n\n    def encode(self, b64=False, always_bytes=True):\n        \"\"\"Encode the packet for transmission.\"\"\"\n        if self.binary and not b64:\n            encoded_packet = six.int2byte(self.packet_type)\n        else:\n            encoded_packet = six.text_type(self.packet_type)\n            if self.binary and b64:\n                encoded_packet = 'b' + encoded_packet\n        if self.binary:\n            if b64:\n                encoded_packet += base64.b64encode(self.data).decode('utf-8')\n            else:\n                encoded_packet += self.data\n        elif isinstance(self.data, six.string_types):\n            encoded_packet += self.data\n        elif isinstance(self.data, dict) or isinstance(self.data, list):\n            encoded_packet += self.json.dumps(self.data,\n                                              separators=(',', ':'))\n        elif self.data is not None:\n            encoded_packet += str(self.data)\n        if always_bytes and not isinstance(encoded_packet, binary_types):\n            encoded_packet = encoded_packet.encode('utf-8')\n        return encoded_packet\n\n    def decode(self, encoded_packet):\n        \"\"\"Decode a transmitted package.\"\"\"\n        b64 = False\n        if not isinstance(encoded_packet, binary_types):\n            encoded_packet = encoded_packet.encode('utf-8')\n        elif not isinstance(encoded_packet, bytes):\n            encoded_packet = bytes(encoded_packet)\n        self.packet_type = six.byte2int(encoded_packet[0:1])\n        if self.packet_type == 98:  # 'b' --> binary base64 encoded packet\n            self.binary = True\n            encoded_packet = encoded_packet[1:]\n            self.packet_type = six.byte2int(encoded_packet[0:1])\n            self.packet_type -= 48\n            b64 = True\n        elif self.packet_type >= 48:\n            self.packet_type -= 48\n            self.binary = False\n        else:\n            self.binary = True\n        self.data = None\n        if len(encoded_packet) > 1:\n            if self.binary:\n                if b64:\n                    self.data = base64.b64decode(encoded_packet[1:])\n                else:\n                    self.data = encoded_packet[1:]\n            else:\n                try:\n                    self.data = self.json.loads(\n                        encoded_packet[1:].decode('utf-8'))\n                    if isinstance(self.data, int):\n                        # do not allow integer payloads, see\n                        # github.com/miguelgrinberg/python-engineio/issues/75\n                        # for background on this decision\n                        raise ValueError\n                except ValueError:\n                    self.data = encoded_packet[1:].decode('utf-8')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/payload.py",
    "content": "import six\n\nfrom . import packet\n\nfrom six.moves import urllib\n\n\nclass Payload(object):\n    \"\"\"Engine.IO payload.\"\"\"\n    def __init__(self, packets=None, encoded_payload=None):\n        self.packets = packets or []\n        if encoded_payload is not None:\n            self.decode(encoded_payload)\n\n    def encode(self, b64=False, jsonp_index=None):\n        \"\"\"Encode the payload for transmission.\"\"\"\n        encoded_payload = b''\n        for pkt in self.packets:\n            encoded_packet = pkt.encode(b64=b64)\n            packet_len = len(encoded_packet)\n            if b64:\n                encoded_payload += str(packet_len).encode('utf-8') + b':' + \\\n                    encoded_packet\n            else:\n                binary_len = b''\n                while packet_len != 0:\n                    binary_len = six.int2byte(packet_len % 10) + binary_len\n                    packet_len = int(packet_len / 10)\n                if not pkt.binary:\n                    encoded_payload += b'\\0'\n                else:\n                    encoded_payload += b'\\1'\n                encoded_payload += binary_len + b'\\xff' + encoded_packet\n        if jsonp_index is not None:\n            encoded_payload = b'___eio[' + \\\n                              str(jsonp_index).encode() + \\\n                              b'](\"' + \\\n                              encoded_payload.replace(b'\"', b'\\\\\"') + \\\n                              b'\");'\n        return encoded_payload\n\n    def decode(self, encoded_payload):\n        \"\"\"Decode a transmitted payload.\"\"\"\n        self.packets = []\n        while encoded_payload:\n            # JSONP POST payload starts with 'd='\n            if encoded_payload.startswith(b'd='):\n                encoded_payload = urllib.parse.parse_qs(\n                    encoded_payload)[b'd'][0]\n\n            if six.byte2int(encoded_payload[0:1]) <= 1:\n                packet_len = 0\n                i = 1\n                while six.byte2int(encoded_payload[i:i + 1]) != 255:\n                    packet_len = packet_len * 10 + six.byte2int(\n                        encoded_payload[i:i + 1])\n                    i += 1\n                self.packets.append(packet.Packet(\n                    encoded_packet=encoded_payload[i + 1:i + 1 + packet_len]))\n            else:\n                i = encoded_payload.find(b':')\n                if i == -1:\n                    raise ValueError('invalid payload')\n\n                # extracting the packet out of the payload is extremely\n                # inefficient, because the payload needs to be treated as\n                # binary, but the non-binary packets have to be parsed as\n                # unicode. Luckily this complication only applies to long\n                # polling, as the websocket transport sends packets\n                # individually wrapped.\n                packet_len = int(encoded_payload[0:i])\n                pkt = encoded_payload.decode('utf-8', errors='ignore')[\n                    i + 1: i + 1 + packet_len].encode('utf-8')\n                self.packets.append(packet.Packet(encoded_packet=pkt))\n\n                # the engine.io protocol sends the packet length in\n                # utf-8 characters, but we need it in bytes to be able to\n                # jump to the next packet in the payload\n                packet_len = len(pkt)\n            encoded_payload = encoded_payload[i + 1 + packet_len:]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/server.py",
    "content": "import gzip\nimport importlib\nimport logging\nimport uuid\nimport zlib\n\nimport six\nfrom six.moves import urllib\n\nfrom . import exceptions\nfrom . import packet\nfrom . import payload\nfrom . import socket\n\ndefault_logger = logging.getLogger('engineio.server')\n\n\nclass Server(object):\n    \"\"\"An Engine.IO server.\n\n    This class implements a fully compliant Engine.IO web server with support\n    for websocket and long-polling transports.\n\n    :param async_mode: The asynchronous model to use. See the Deployment\n                       section in the documentation for a description of the\n                       available options. Valid async modes are \"threading\",\n                       \"eventlet\", \"gevent\" and \"gevent_uwsgi\". If this\n                       argument is not given, \"eventlet\" is tried first, then\n                       \"gevent_uwsgi\", then \"gevent\", and finally \"threading\".\n                       The first async mode that has all its dependencies\n                       installed is the one that is chosen.\n    :param ping_timeout: The time in seconds that the client waits for the\n                         server to respond before disconnecting. The default\n                         is 60 seconds.\n    :param ping_interval: The interval in seconds at which the client pings\n                          the server. The default is 25 seconds.\n    :param max_http_buffer_size: The maximum size of a message when using the\n                                 polling transport. The default is 100,000,000\n                                 bytes.\n    :param allow_upgrades: Whether to allow transport upgrades or not. The\n                           default is ``True``.\n    :param http_compression: Whether to compress packages when using the\n                             polling transport. The default is ``True``.\n    :param compression_threshold: Only compress messages when their byte size\n                                  is greater than this value. The default is\n                                  1024 bytes.\n    :param cookie: Name of the HTTP cookie that contains the client session\n                   id. If set to ``None``, a cookie is not sent to the client.\n                   The default is ``'io'``.\n    :param cors_allowed_origins: Origin or list of origins that are allowed to\n                                 connect to this server. All origins are\n                                 allowed by default, which is equivalent to\n                                 setting this argument to ``'*'``.\n    :param cors_credentials: Whether credentials (cookies, authentication) are\n                             allowed in requests to this server. The default\n                             is ``True``.\n    :param logger: To enable logging set to ``True`` or pass a logger object to\n                   use. To disable logging set to ``False``. The default is\n                   ``False``.\n    :param json: An alternative json module to use for encoding and decoding\n                 packets. Custom json modules must have ``dumps`` and ``loads``\n                 functions that are compatible with the standard library\n                 versions.\n    :param async_handlers: If set to ``True``, run message event handlers in\n                           non-blocking threads. To run handlers synchronously,\n                           set to ``False``. The default is ``True``.\n    :param monitor_clients: If set to ``True``, a background task will ensure\n                            inactive clients are closed. Set to ``False`` to\n                            disable the monitoring task (not recommended). The\n                            default is ``True``.\n    :param kwargs: Reserved for future extensions, any additional parameters\n                   given as keyword arguments will be silently ignored.\n    \"\"\"\n    compression_methods = ['gzip', 'deflate']\n    event_names = ['connect', 'disconnect', 'message']\n    _default_monitor_clients = True\n\n    def __init__(self, async_mode=None, ping_timeout=60, ping_interval=25,\n                 max_http_buffer_size=100000000, allow_upgrades=True,\n                 http_compression=True, compression_threshold=1024,\n                 cookie='io', cors_allowed_origins=None,\n                 cors_credentials=True, logger=False, json=None,\n                 async_handlers=True, monitor_clients=None, **kwargs):\n        self.ping_timeout = ping_timeout\n        self.ping_interval = ping_interval\n        self.max_http_buffer_size = max_http_buffer_size\n        self.allow_upgrades = allow_upgrades\n        self.http_compression = http_compression\n        self.compression_threshold = compression_threshold\n        self.cookie = cookie\n        self.cors_allowed_origins = cors_allowed_origins\n        self.cors_credentials = cors_credentials\n        self.async_handlers = async_handlers\n        self.sockets = {}\n        self.handlers = {}\n        self.start_service_task = monitor_clients \\\n            if monitor_clients is not None else self._default_monitor_clients\n        if json is not None:\n            packet.Packet.json = json\n        if not isinstance(logger, bool):\n            self.logger = logger\n        else:\n            self.logger = default_logger\n            if not logging.root.handlers and \\\n                    self.logger.level == logging.NOTSET:\n                if logger:\n                    self.logger.setLevel(logging.INFO)\n                else:\n                    self.logger.setLevel(logging.ERROR)\n                self.logger.addHandler(logging.StreamHandler())\n        modes = self.async_modes()\n        if async_mode is not None:\n            modes = [async_mode] if async_mode in modes else []\n        self._async = None\n        self.async_mode = None\n        for mode in modes:\n            try:\n                self._async = importlib.import_module(\n                    'engineio.async_drivers.' + mode)._async\n                asyncio_based = self._async['asyncio'] \\\n                    if 'asyncio' in self._async else False\n                if asyncio_based != self.is_asyncio_based():\n                    continue  # pragma: no cover\n                self.async_mode = mode\n                break\n            except ImportError:\n                pass\n        if self.async_mode is None:\n            raise ValueError('Invalid async_mode specified')\n        if self.is_asyncio_based() and \\\n                ('asyncio' not in self._async or not\n                 self._async['asyncio']):  # pragma: no cover\n            raise ValueError('The selected async_mode is not asyncio '\n                             'compatible')\n        if not self.is_asyncio_based() and 'asyncio' in self._async and \\\n                self._async['asyncio']:  # pragma: no cover\n            raise ValueError('The selected async_mode requires asyncio and '\n                             'must use the AsyncServer class')\n        self.logger.info('Server initialized for %s.', self.async_mode)\n\n    def is_asyncio_based(self):\n        return False\n\n    def async_modes(self):\n        return ['eventlet', 'gevent_uwsgi', 'gevent', 'threading']\n\n    def on(self, event, handler=None):\n        \"\"\"Register an event handler.\n\n        :param event: The event name. Can be ``'connect'``, ``'message'`` or\n                      ``'disconnect'``.\n        :param handler: The function that should be invoked to handle the\n                        event. When this parameter is not given, the method\n                        acts as a decorator for the handler function.\n\n        Example usage::\n\n            # as a decorator:\n            @eio.on('connect')\n            def connect_handler(sid, environ):\n                print('Connection request')\n                if environ['REMOTE_ADDR'] in blacklisted:\n                    return False  # reject\n\n            # as a method:\n            def message_handler(sid, msg):\n                print('Received message: ', msg)\n                eio.send(sid, 'response')\n            eio.on('message', message_handler)\n\n        The handler function receives the ``sid`` (session ID) for the\n        client as first argument. The ``'connect'`` event handler receives the\n        WSGI environment as a second argument, and can return ``False`` to\n        reject the connection. The ``'message'`` handler receives the message\n        payload as a second argument. The ``'disconnect'`` handler does not\n        take a second argument.\n        \"\"\"\n        if event not in self.event_names:\n            raise ValueError('Invalid event')\n\n        def set_handler(handler):\n            self.handlers[event] = handler\n            return handler\n\n        if handler is None:\n            return set_handler\n        set_handler(handler)\n\n    def send(self, sid, data, binary=None):\n        \"\"\"Send a message to a client.\n\n        :param sid: The session id of the recipient client.\n        :param data: The data to send to the client. Data can be of type\n                     ``str``, ``bytes``, ``list`` or ``dict``. If a ``list``\n                     or ``dict``, the data will be serialized as JSON.\n        :param binary: ``True`` to send packet as binary, ``False`` to send\n                       as text. If not given, unicode (Python 2) and str\n                       (Python 3) are sent as text, and str (Python 2) and\n                       bytes (Python 3) are sent as binary.\n        \"\"\"\n        try:\n            socket = self._get_socket(sid)\n        except KeyError:\n            # the socket is not available\n            self.logger.warning('Cannot send to sid %s', sid)\n            return\n        socket.send(packet.Packet(packet.MESSAGE, data=data, binary=binary))\n\n    def get_session(self, sid):\n        \"\"\"Return the user session for a client.\n\n        :param sid: The session id of the client.\n\n        The return value is a dictionary. Modifications made to this\n        dictionary are not guaranteed to be preserved unless\n        ``save_session()`` is called, or when the ``session`` context manager\n        is used.\n        \"\"\"\n        socket = self._get_socket(sid)\n        return socket.session\n\n    def save_session(self, sid, session):\n        \"\"\"Store the user session for a client.\n\n        :param sid: The session id of the client.\n        :param session: The session dictionary.\n        \"\"\"\n        socket = self._get_socket(sid)\n        socket.session = session\n\n    def session(self, sid):\n        \"\"\"Return the user session for a client with context manager syntax.\n\n        :param sid: The session id of the client.\n\n        This is a context manager that returns the user session dictionary for\n        the client. Any changes that are made to this dictionary inside the\n        context manager block are saved back to the session. Example usage::\n\n            @eio.on('connect')\n            def on_connect(sid, environ):\n                username = authenticate_user(environ)\n                if not username:\n                    return False\n                with eio.session(sid) as session:\n                    session['username'] = username\n\n            @eio.on('message')\n            def on_message(sid, msg):\n                with eio.session(sid) as session:\n                    print('received message from ', session['username'])\n        \"\"\"\n        class _session_context_manager(object):\n            def __init__(self, server, sid):\n                self.server = server\n                self.sid = sid\n                self.session = None\n\n            def __enter__(self):\n                self.session = self.server.get_session(sid)\n                return self.session\n\n            def __exit__(self, *args):\n                self.server.save_session(sid, self.session)\n\n        return _session_context_manager(self, sid)\n\n    def disconnect(self, sid=None):\n        \"\"\"Disconnect a client.\n\n        :param sid: The session id of the client to close. If this parameter\n                    is not given, then all clients are closed.\n        \"\"\"\n        if sid is not None:\n            try:\n                socket = self._get_socket(sid)\n            except KeyError:  # pragma: no cover\n                # the socket was already closed or gone\n                pass\n            else:\n                socket.close()\n                del self.sockets[sid]\n        else:\n            for client in six.itervalues(self.sockets):\n                client.close()\n            self.sockets = {}\n\n    def transport(self, sid):\n        \"\"\"Return the name of the transport used by the client.\n\n        The two possible values returned by this function are ``'polling'``\n        and ``'websocket'``.\n\n        :param sid: The session of the client.\n        \"\"\"\n        return 'websocket' if self._get_socket(sid).upgraded else 'polling'\n\n    def handle_request(self, environ, start_response):\n        \"\"\"Handle an HTTP request from the client.\n\n        This is the entry point of the Engine.IO application, using the same\n        interface as a WSGI application. For the typical usage, this function\n        is invoked by the :class:`Middleware` instance, but it can be invoked\n        directly when the middleware is not used.\n\n        :param environ: The WSGI environment.\n        :param start_response: The WSGI ``start_response`` function.\n\n        This function returns the HTTP response body to deliver to the client\n        as a byte sequence.\n        \"\"\"\n        method = environ['REQUEST_METHOD']\n        query = urllib.parse.parse_qs(environ.get('QUERY_STRING', ''))\n\n        sid = query['sid'][0] if 'sid' in query else None\n        b64 = False\n        jsonp = False\n        jsonp_index = None\n\n        if 'b64' in query:\n            if query['b64'][0] == \"1\" or query['b64'][0].lower() == \"true\":\n                b64 = True\n        if 'j' in query:\n            jsonp = True\n            try:\n                jsonp_index = int(query['j'][0])\n            except (ValueError, KeyError, IndexError):\n                # Invalid JSONP index number\n                pass\n\n        if jsonp and jsonp_index is None:\n            self.logger.warning('Invalid JSONP index number')\n            r = self._bad_request()\n        elif method == 'GET':\n            if sid is None:\n                transport = query.get('transport', ['polling'])[0]\n                if transport != 'polling' and transport != 'websocket':\n                    self.logger.warning('Invalid transport %s', transport)\n                    r = self._bad_request()\n                else:\n                    r = self._handle_connect(environ, start_response,\n                                             transport, b64, jsonp_index)\n            else:\n                if sid not in self.sockets:\n                    self.logger.warning('Invalid session %s', sid)\n                    r = self._bad_request()\n                else:\n                    socket = self._get_socket(sid)\n                    try:\n                        packets = socket.handle_get_request(\n                            environ, start_response)\n                        if isinstance(packets, list):\n                            r = self._ok(packets, b64=b64,\n                                         jsonp_index=jsonp_index)\n                        else:\n                            r = packets\n                    except exceptions.EngineIOError:\n                        if sid in self.sockets:  # pragma: no cover\n                            self.disconnect(sid)\n                        r = self._bad_request()\n                    if sid in self.sockets and self.sockets[sid].closed:\n                        del self.sockets[sid]\n        elif method == 'POST':\n            if sid is None or sid not in self.sockets:\n                self.logger.warning('Invalid session %s', sid)\n                r = self._bad_request()\n            else:\n                socket = self._get_socket(sid)\n                try:\n                    socket.handle_post_request(environ)\n                    r = self._ok(jsonp_index=jsonp_index)\n                except exceptions.EngineIOError:\n                    if sid in self.sockets:  # pragma: no cover\n                        self.disconnect(sid)\n                    r = self._bad_request()\n                except:  # pragma: no cover\n                    # for any other unexpected errors, we log the error\n                    # and keep going\n                    self.logger.exception('post request handler error')\n                    r = self._ok(jsonp_index=jsonp_index)\n        elif method == 'OPTIONS':\n            r = self._ok()\n        else:\n            self.logger.warning('Method %s not supported', method)\n            r = self._method_not_found()\n\n        if not isinstance(r, dict):\n            return r or []\n        if self.http_compression and \\\n                len(r['response']) >= self.compression_threshold:\n            encodings = [e.split(';')[0].strip() for e in\n                         environ.get('HTTP_ACCEPT_ENCODING', '').split(',')]\n            for encoding in encodings:\n                if encoding in self.compression_methods:\n                    r['response'] = \\\n                        getattr(self, '_' + encoding)(r['response'])\n                    r['headers'] += [('Content-Encoding', encoding)]\n                    break\n        cors_headers = self._cors_headers(environ)\n        start_response(r['status'], r['headers'] + cors_headers)\n        return [r['response']]\n\n    def start_background_task(self, target, *args, **kwargs):\n        \"\"\"Start a background task using the appropriate async model.\n\n        This is a utility function that applications can use to start a\n        background task using the method that is compatible with the\n        selected async mode.\n\n        :param target: the target function to execute.\n        :param args: arguments to pass to the function.\n        :param kwargs: keyword arguments to pass to the function.\n\n        This function returns an object compatible with the `Thread` class in\n        the Python standard library. The `start()` method on this object is\n        already called by this function.\n        \"\"\"\n        th = self._async['thread'](target=target, args=args, kwargs=kwargs)\n        th.start()\n        return th  # pragma: no cover\n\n    def sleep(self, seconds=0):\n        \"\"\"Sleep for the requested amount of time using the appropriate async\n        model.\n\n        This is a utility function that applications can use to put a task to\n        sleep without having to worry about using the correct call for the\n        selected async mode.\n        \"\"\"\n        return self._async['sleep'](seconds)\n\n    def create_queue(self, *args, **kwargs):\n        \"\"\"Create a queue object using the appropriate async model.\n\n        This is a utility function that applications can use to create a queue\n        without having to worry about using the correct call for the selected\n        async mode.\n        \"\"\"\n        return self._async['queue'](*args, **kwargs)\n\n    def get_queue_empty_exception(self):\n        \"\"\"Return the queue empty exception for the appropriate async model.\n\n        This is a utility function that applications can use to work with a\n        queue without having to worry about using the correct call for the\n        selected async mode.\n        \"\"\"\n        return self._async['queue_empty']\n\n    def create_event(self, *args, **kwargs):\n        \"\"\"Create an event object using the appropriate async model.\n\n        This is a utility function that applications can use to create an\n        event without having to worry about using the correct call for the\n        selected async mode.\n        \"\"\"\n        return self._async['event'](*args, **kwargs)\n\n    def _generate_id(self):\n        \"\"\"Generate a unique session id.\"\"\"\n        return uuid.uuid4().hex\n\n    def _handle_connect(self, environ, start_response, transport, b64=False,\n                        jsonp_index=None):\n        \"\"\"Handle a client connection request.\"\"\"\n        if self.start_service_task:\n            # start the service task to monitor connected clients\n            self.start_service_task = False\n            self.start_background_task(self._service_task)\n\n        sid = self._generate_id()\n        s = socket.Socket(self, sid)\n        self.sockets[sid] = s\n\n        pkt = packet.Packet(\n            packet.OPEN, {'sid': sid,\n                          'upgrades': self._upgrades(sid, transport),\n                          'pingTimeout': int(self.ping_timeout * 1000),\n                          'pingInterval': int(self.ping_interval * 1000)})\n        s.send(pkt)\n\n        ret = self._trigger_event('connect', sid, environ, run_async=False)\n        if ret is False:\n            del self.sockets[sid]\n            self.logger.warning('Application rejected connection')\n            return self._unauthorized()\n\n        if transport == 'websocket':\n            ret = s.handle_get_request(environ, start_response)\n            if s.closed:\n                # websocket connection ended, so we are done\n                del self.sockets[sid]\n            return ret\n        else:\n            s.connected = True\n            headers = None\n            if self.cookie:\n                headers = [('Set-Cookie', self.cookie + '=' + sid)]\n            try:\n                return self._ok(s.poll(), headers=headers, b64=b64,\n                                jsonp_index=jsonp_index)\n            except exceptions.QueueEmpty:\n                return self._bad_request()\n\n    def _upgrades(self, sid, transport):\n        \"\"\"Return the list of possible upgrades for a client connection.\"\"\"\n        if not self.allow_upgrades or self._get_socket(sid).upgraded or \\\n                self._async['websocket'] is None or transport == 'websocket':\n            return []\n        return ['websocket']\n\n    def _trigger_event(self, event, *args, **kwargs):\n        \"\"\"Invoke an event handler.\"\"\"\n        run_async = kwargs.pop('run_async', False)\n        if event in self.handlers:\n            if run_async:\n                return self.start_background_task(self.handlers[event], *args)\n            else:\n                try:\n                    return self.handlers[event](*args)\n                except:\n                    self.logger.exception(event + ' handler error')\n                    if event == 'connect':\n                        # if connect handler raised error we reject the\n                        # connection\n                        return False\n\n    def _get_socket(self, sid):\n        \"\"\"Return the socket object for a given session.\"\"\"\n        try:\n            s = self.sockets[sid]\n        except KeyError:\n            raise KeyError('Session not found')\n        if s.closed:\n            del self.sockets[sid]\n            raise KeyError('Session is disconnected')\n        return s\n\n    def _ok(self, packets=None, headers=None, b64=False, jsonp_index=None):\n        \"\"\"Generate a successful HTTP response.\"\"\"\n        if packets is not None:\n            if headers is None:\n                headers = []\n            if b64:\n                headers += [('Content-Type', 'text/plain; charset=UTF-8')]\n            else:\n                headers += [('Content-Type', 'application/octet-stream')]\n            return {'status': '200 OK',\n                    'headers': headers,\n                    'response': payload.Payload(packets=packets).encode(\n                        b64=b64, jsonp_index=jsonp_index)}\n        else:\n            return {'status': '200 OK',\n                    'headers': [('Content-Type', 'text/plain')],\n                    'response': b'OK'}\n\n    def _bad_request(self):\n        \"\"\"Generate a bad request HTTP error response.\"\"\"\n        return {'status': '400 BAD REQUEST',\n                'headers': [('Content-Type', 'text/plain')],\n                'response': b'Bad Request'}\n\n    def _method_not_found(self):\n        \"\"\"Generate a method not found HTTP error response.\"\"\"\n        return {'status': '405 METHOD NOT FOUND',\n                'headers': [('Content-Type', 'text/plain')],\n                'response': b'Method Not Found'}\n\n    def _unauthorized(self):\n        \"\"\"Generate a unauthorized HTTP error response.\"\"\"\n        return {'status': '401 UNAUTHORIZED',\n                'headers': [('Content-Type', 'text/plain')],\n                'response': b'Unauthorized'}\n\n    def _cors_headers(self, environ):\n        \"\"\"Return the cross-origin-resource-sharing headers.\"\"\"\n        if isinstance(self.cors_allowed_origins, six.string_types):\n            if self.cors_allowed_origins == '*':\n                allowed_origins = None\n            else:\n                allowed_origins = [self.cors_allowed_origins]\n        else:\n            allowed_origins = self.cors_allowed_origins\n        if allowed_origins is not None and \\\n                environ.get('HTTP_ORIGIN', '') not in allowed_origins:\n            return []\n        if 'HTTP_ORIGIN' in environ:\n            headers = [('Access-Control-Allow-Origin', environ['HTTP_ORIGIN'])]\n        else:\n            headers = [('Access-Control-Allow-Origin', '*')]\n        if environ['REQUEST_METHOD'] == 'OPTIONS':\n            headers += [('Access-Control-Allow-Methods', 'OPTIONS, GET, POST')]\n        if 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS' in environ:\n            headers += [('Access-Control-Allow-Headers',\n                         environ['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])]\n        if self.cors_credentials:\n            headers += [('Access-Control-Allow-Credentials', 'true')]\n        return headers\n\n    def _gzip(self, response):\n        \"\"\"Apply gzip compression to a response.\"\"\"\n        bytesio = six.BytesIO()\n        with gzip.GzipFile(fileobj=bytesio, mode='w') as gz:\n            gz.write(response)\n        return bytesio.getvalue()\n\n    def _deflate(self, response):\n        \"\"\"Apply deflate compression to a response.\"\"\"\n        return zlib.compress(response)\n\n    def _service_task(self):  # pragma: no cover\n        \"\"\"Monitor connected clients and clean up those that time out.\"\"\"\n        while True:\n            if len(self.sockets) == 0:\n                # nothing to do\n                self.sleep(self.ping_timeout)\n                continue\n\n            # go through the entire client list in a ping interval cycle\n            sleep_interval = self.ping_timeout / len(self.sockets)\n\n            try:\n                # iterate over the current clients\n                for s in self.sockets.copy().values():\n                    if not s.closing and not s.closed:\n                        s.check_ping_timeout()\n                    self.sleep(sleep_interval)\n            except (SystemExit, KeyboardInterrupt):\n                self.logger.info('service task canceled')\n                break\n            except:\n                # an unexpected exception has occurred, log it and continue\n                self.logger.exception('service task exception')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/socket.py",
    "content": "import six\nimport sys\nimport time\n\nfrom . import exceptions\nfrom . import packet\nfrom . import payload\n\n\nclass Socket(object):\n    \"\"\"An Engine.IO socket.\"\"\"\n    upgrade_protocols = ['websocket']\n\n    def __init__(self, server, sid):\n        self.server = server\n        self.sid = sid\n        self.queue = self.server.create_queue()\n        self.last_ping = time.time()\n        self.connected = False\n        self.upgrading = False\n        self.upgraded = False\n        self.packet_backlog = []\n        self.closing = False\n        self.closed = False\n        self.session = {}\n\n    def poll(self):\n        \"\"\"Wait for packets to send to the client.\"\"\"\n        queue_empty = self.server.get_queue_empty_exception()\n        try:\n            packets = [self.queue.get(timeout=self.server.ping_timeout)]\n            self.queue.task_done()\n        except queue_empty:\n            raise exceptions.QueueEmpty()\n        if packets == [None]:\n            return []\n        while True:\n            try:\n                packets.append(self.queue.get(block=False))\n                self.queue.task_done()\n            except queue_empty:\n                break\n        return packets\n\n    def receive(self, pkt):\n        \"\"\"Receive packet from the client.\"\"\"\n        packet_name = packet.packet_names[pkt.packet_type] \\\n            if pkt.packet_type < len(packet.packet_names) else 'UNKNOWN'\n        self.server.logger.info('%s: Received packet %s data %s',\n                                self.sid, packet_name,\n                                pkt.data if not isinstance(pkt.data, bytes)\n                                else '<binary>')\n        if pkt.packet_type == packet.PING:\n            self.last_ping = time.time()\n            self.send(packet.Packet(packet.PONG, pkt.data))\n        elif pkt.packet_type == packet.MESSAGE:\n            self.server._trigger_event('message', self.sid, pkt.data,\n                                       run_async=self.server.async_handlers)\n        elif pkt.packet_type == packet.UPGRADE:\n            self.send(packet.Packet(packet.NOOP))\n        elif pkt.packet_type == packet.CLOSE:\n            self.close(wait=False, abort=True)\n        else:\n            raise exceptions.UnknownPacketError()\n\n    def check_ping_timeout(self):\n        \"\"\"Make sure the client is still sending pings.\n\n        This helps detect disconnections for long-polling clients.\n        \"\"\"\n        if self.closed:\n            raise exceptions.SocketIsClosedError()\n        if time.time() - self.last_ping > self.server.ping_interval + 5:\n            self.server.logger.info('%s: Client is gone, closing socket',\n                                    self.sid)\n            # Passing abort=False here will cause close() to write a\n            # CLOSE packet. This has the effect of updating half-open sockets\n            # to their correct state of disconnected\n            self.close(wait=False, abort=False)\n            return False\n        return True\n\n    def send(self, pkt):\n        \"\"\"Send a packet to the client.\"\"\"\n        if not self.check_ping_timeout():\n            return\n        if self.upgrading:\n            self.packet_backlog.append(pkt)\n        else:\n            self.queue.put(pkt)\n        self.server.logger.info('%s: Sending packet %s data %s',\n                                self.sid, packet.packet_names[pkt.packet_type],\n                                pkt.data if not isinstance(pkt.data, bytes)\n                                else '<binary>')\n\n    def handle_get_request(self, environ, start_response):\n        \"\"\"Handle a long-polling GET request from the client.\"\"\"\n        connections = [\n            s.strip()\n            for s in environ.get('HTTP_CONNECTION', '').lower().split(',')]\n        transport = environ.get('HTTP_UPGRADE', '').lower()\n        if 'upgrade' in connections and transport in self.upgrade_protocols:\n            self.server.logger.info('%s: Received request to upgrade to %s',\n                                    self.sid, transport)\n            return getattr(self, '_upgrade_' + transport)(environ,\n                                                          start_response)\n        try:\n            packets = self.poll()\n        except exceptions.QueueEmpty:\n            exc = sys.exc_info()\n            self.close(wait=False)\n            six.reraise(*exc)\n        return packets\n\n    def handle_post_request(self, environ):\n        \"\"\"Handle a long-polling POST request from the client.\"\"\"\n        length = int(environ.get('CONTENT_LENGTH', '0'))\n        if length > self.server.max_http_buffer_size:\n            raise exceptions.ContentTooLongError()\n        else:\n            body = environ['wsgi.input'].read(length)\n            p = payload.Payload(encoded_payload=body)\n            for pkt in p.packets:\n                self.receive(pkt)\n\n    def close(self, wait=True, abort=False):\n        \"\"\"Close the socket connection.\"\"\"\n        if not self.closed and not self.closing:\n            self.closing = True\n            self.server._trigger_event('disconnect', self.sid, run_async=False)\n            if not abort:\n                self.send(packet.Packet(packet.CLOSE))\n            self.closed = True\n            self.queue.put(None)\n            if wait:\n                self.queue.join()\n\n    def _upgrade_websocket(self, environ, start_response):\n        \"\"\"Upgrade the connection from polling to websocket.\"\"\"\n        if self.upgraded:\n            raise IOError('Socket has been upgraded already')\n        if self.server._async['websocket'] is None:\n            # the selected async mode does not support websocket\n            return self.server._bad_request()\n        ws = self.server._async['websocket'](self._websocket_handler)\n        return ws(environ, start_response)\n\n    def _websocket_handler(self, ws):\n        \"\"\"Engine.IO handler for websocket transport.\"\"\"\n        # try to set a socket timeout matching the configured ping interval\n        for attr in ['_sock', 'socket']:  # pragma: no cover\n            if hasattr(ws, attr) and hasattr(getattr(ws, attr), 'settimeout'):\n                getattr(ws, attr).settimeout(self.server.ping_timeout)\n\n        if self.connected:\n            # the socket was already connected, so this is an upgrade\n            self.upgrading = True  # hold packet sends during the upgrade\n\n            pkt = ws.wait()\n            decoded_pkt = packet.Packet(encoded_packet=pkt)\n            if decoded_pkt.packet_type != packet.PING or \\\n                    decoded_pkt.data != 'probe':\n                self.server.logger.info(\n                    '%s: Failed websocket upgrade, no PING packet', self.sid)\n                return []\n            ws.send(packet.Packet(\n                packet.PONG,\n                data=six.text_type('probe')).encode(always_bytes=False))\n            self.queue.put(packet.Packet(packet.NOOP))  # end poll\n\n            pkt = ws.wait()\n            decoded_pkt = packet.Packet(encoded_packet=pkt)\n            if decoded_pkt.packet_type != packet.UPGRADE:\n                self.upgraded = False\n                self.server.logger.info(\n                    ('%s: Failed websocket upgrade, expected UPGRADE packet, '\n                     'received %s instead.'),\n                    self.sid, pkt)\n                return []\n            self.upgraded = True\n\n            # flush any packets that were sent during the upgrade\n            for pkt in self.packet_backlog:\n                self.queue.put(pkt)\n            self.packet_backlog = []\n            self.upgrading = False\n        else:\n            self.connected = True\n            self.upgraded = True\n\n        # start separate writer thread\n        def writer():\n            while True:\n                packets = None\n                try:\n                    packets = self.poll()\n                except exceptions.QueueEmpty:\n                    break\n                if not packets:\n                    # empty packet list returned -> connection closed\n                    break\n                try:\n                    for pkt in packets:\n                        ws.send(pkt.encode(always_bytes=False))\n                except:\n                    break\n        writer_task = self.server.start_background_task(writer)\n\n        self.server.logger.info(\n            '%s: Upgrade to websocket successful', self.sid)\n\n        while True:\n            p = None\n            try:\n                p = ws.wait()\n            except Exception as e:\n                # if the socket is already closed, we can assume this is a\n                # downstream error of that\n                if not self.closed:  # pragma: no cover\n                    self.server.logger.info(\n                        '%s: Unexpected error \"%s\", closing connection',\n                        self.sid, str(e))\n                break\n            if p is None:\n                # connection closed by client\n                break\n            if isinstance(p, six.text_type):  # pragma: no cover\n                p = p.encode('utf-8')\n            pkt = packet.Packet(encoded_packet=p)\n            try:\n                self.receive(pkt)\n            except exceptions.UnknownPacketError:  # pragma: no cover\n                pass\n            except exceptions.SocketIsClosedError:  # pragma: no cover\n                self.server.logger.info('Receive error -- socket is closed')\n                break\n            except:  # pragma: no cover\n                # if we get an unexpected exception we log the error and exit\n                # the connection properly\n                self.server.logger.exception('Unknown receive error')\n                break\n\n        self.queue.put(None)  # unlock the writer task so that it can exit\n        writer_task.join()\n        self.close(wait=False, abort=True)\n\n        return []\n"
  },
  {
    "path": "openpype/vendor/python/python_2/engineio/static_files.py",
    "content": "content_types = {\n    'css': 'text/css',\n    'gif': 'image/gif',\n    'html': 'text/html',\n    'jpg': 'image/jpeg',\n    'js': 'application/javascript',\n    'json': 'application/json',\n    'png': 'image/png',\n    'txt': 'text/plain',\n}\n\n\ndef get_static_file(path, static_files):\n    \"\"\"Return the local filename and content type for the requested static\n    file URL.\n\n    :param path: the path portion of the requested URL.\n    :param static_files: a static file configuration dictionary.\n\n    This function returns a dictionary with two keys, \"filename\" and\n    \"content_type\". If the requested URL does not match any static file, the\n    return value is None.\n    \"\"\"\n    if path in static_files:\n        f = static_files[path]\n    else:\n        f = None\n        rest = ''\n        while path != '':\n            path, last = path.rsplit('/', 1)\n            rest = '/' + last + rest\n            if path in static_files:\n                f = static_files[path] + rest\n                break\n            elif path + '/' in static_files:\n                f = static_files[path + '/'] + rest[1:]\n                break\n    if f:\n        if isinstance(f, str):\n            f = {'filename': f}\n        if f['filename'].endswith('/'):\n            if '' in static_files:\n                if isinstance(static_files[''], str):\n                    f['filename'] += static_files['']\n                else:\n                    f['filename'] += static_files['']['filename']\n                    if 'content_type' in static_files['']:\n                        f['content_type'] = static_files['']['content_type']\n            else:\n                f['filename'] += 'index.html'\n        if 'content_type' not in f:\n            ext = f['filename'].rsplit('.')[-1]\n            f['content_type'] = content_types.get(\n                ext, 'application/octet-stream')\n    return f\n"
  },
  {
    "path": "openpype/vendor/python/python_2/functools32/__init__.py",
    "content": "from .functools32 import *\n"
  },
  {
    "path": "openpype/vendor/python/python_2/functools32/_dummy_thread32.py",
    "content": "\"\"\"Drop-in replacement for the thread module.\n\nMeant to be used as a brain-dead substitute so that threaded code does\nnot need to be rewritten for when the thread module is not present.\n\nSuggested usage is::\n\n    try:\n        try:\n            import _thread  # Python >= 3\n        except:\n            import thread as _thread  # Python < 3\n    except ImportError:\n        import _dummy_thread as _thread\n\n\"\"\"\n# Exports only things specified by thread documentation;\n# skipping obsolete synonyms allocate(), start_new(), exit_thread().\n__all__ = ['error', 'start_new_thread', 'exit', 'get_ident', 'allocate_lock',\n           'interrupt_main', 'LockType']\n\n# A dummy value\nTIMEOUT_MAX = 2**31\n\n# NOTE: this module can be imported early in the extension building process,\n# and so top level imports of other modules should be avoided.  Instead, all\n# imports are done when needed on a function-by-function basis.  Since threads\n# are disabled, the import lock should not be an issue anyway (??).\n\nclass error(Exception):\n    \"\"\"Dummy implementation of _thread.error.\"\"\"\n\n    def __init__(self, *args):\n        self.args = args\n\ndef start_new_thread(function, args, kwargs={}):\n    \"\"\"Dummy implementation of _thread.start_new_thread().\n\n    Compatibility is maintained by making sure that ``args`` is a\n    tuple and ``kwargs`` is a dictionary.  If an exception is raised\n    and it is SystemExit (which can be done by _thread.exit()) it is\n    caught and nothing is done; all other exceptions are printed out\n    by using traceback.print_exc().\n\n    If the executed function calls interrupt_main the KeyboardInterrupt will be\n    raised when the function returns.\n\n    \"\"\"\n    if type(args) != type(tuple()):\n        raise TypeError(\"2nd arg must be a tuple\")\n    if type(kwargs) != type(dict()):\n        raise TypeError(\"3rd arg must be a dict\")\n    global _main\n    _main = False\n    try:\n        function(*args, **kwargs)\n    except SystemExit:\n        pass\n    except:\n        import traceback\n        traceback.print_exc()\n    _main = True\n    global _interrupt\n    if _interrupt:\n        _interrupt = False\n        raise KeyboardInterrupt\n\ndef exit():\n    \"\"\"Dummy implementation of _thread.exit().\"\"\"\n    raise SystemExit\n\ndef get_ident():\n    \"\"\"Dummy implementation of _thread.get_ident().\n\n    Since this module should only be used when _threadmodule is not\n    available, it is safe to assume that the current process is the\n    only thread.  Thus a constant can be safely returned.\n    \"\"\"\n    return -1\n\ndef allocate_lock():\n    \"\"\"Dummy implementation of _thread.allocate_lock().\"\"\"\n    return LockType()\n\ndef stack_size(size=None):\n    \"\"\"Dummy implementation of _thread.stack_size().\"\"\"\n    if size is not None:\n        raise error(\"setting thread stack size not supported\")\n    return 0\n\nclass LockType(object):\n    \"\"\"Class implementing dummy implementation of _thread.LockType.\n\n    Compatibility is maintained by maintaining self.locked_status\n    which is a boolean that stores the state of the lock.  Pickling of\n    the lock, though, should not be done since if the _thread module is\n    then used with an unpickled ``lock()`` from here problems could\n    occur from this class not having atomic methods.\n\n    \"\"\"\n\n    def __init__(self):\n        self.locked_status = False\n\n    def acquire(self, waitflag=None, timeout=-1):\n        \"\"\"Dummy implementation of acquire().\n\n        For blocking calls, self.locked_status is automatically set to\n        True and returned appropriately based on value of\n        ``waitflag``.  If it is non-blocking, then the value is\n        actually checked and not set if it is already acquired.  This\n        is all done so that threading.Condition's assert statements\n        aren't triggered and throw a little fit.\n\n        \"\"\"\n        if waitflag is None or waitflag:\n            self.locked_status = True\n            return True\n        else:\n            if not self.locked_status:\n                self.locked_status = True\n                return True\n            else:\n                if timeout > 0:\n                    import time\n                    time.sleep(timeout)\n                return False\n\n    __enter__ = acquire\n\n    def __exit__(self, typ, val, tb):\n        self.release()\n\n    def release(self):\n        \"\"\"Release the dummy lock.\"\"\"\n        # XXX Perhaps shouldn't actually bother to test?  Could lead\n        #     to problems for complex, threaded code.\n        if not self.locked_status:\n            raise error\n        self.locked_status = False\n        return True\n\n    def locked(self):\n        return self.locked_status\n\n# Used to signal that interrupt_main was called in a \"thread\"\n_interrupt = False\n# True when not executing in a \"thread\"\n_main = True\n\ndef interrupt_main():\n    \"\"\"Set _interrupt flag to True to have start_new_thread raise\n    KeyboardInterrupt upon exiting.\"\"\"\n    if _main:\n        raise KeyboardInterrupt\n    else:\n        global _interrupt\n        _interrupt = True\n"
  },
  {
    "path": "openpype/vendor/python/python_2/functools32/functools32.py",
    "content": "\"\"\"functools.py - Tools for working with functions and callable objects\n\"\"\"\n# Python module wrapper for _functools C module\n# to allow utilities written in Python to be added\n# to the functools module.\n# Written by Nick Coghlan <ncoghlan at gmail.com>\n# and Raymond Hettinger <python at rcn.com>\n#   Copyright (C) 2006-2010 Python Software Foundation.\n# See C source code for _functools credits/copyright\n\n__all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES',\n           'total_ordering', 'cmp_to_key', 'lru_cache', 'reduce', 'partial']\n\nfrom _functools import partial, reduce\nfrom collections import MutableMapping, namedtuple\nfrom .reprlib32 import recursive_repr as _recursive_repr\nfrom weakref import proxy as _proxy\nimport sys as _sys\ntry:\n    from thread import allocate_lock as Lock\nexcept ImportError:\n    from ._dummy_thread32 import allocate_lock as Lock\n\n################################################################################\n### OrderedDict\n################################################################################\n\nclass _Link(object):\n    __slots__ = 'prev', 'next', 'key', '__weakref__'\n\nclass OrderedDict(dict):\n    'Dictionary that remembers insertion order'\n    # An inherited dict maps keys to values.\n    # The inherited dict provides __getitem__, __len__, __contains__, and get.\n    # The remaining methods are order-aware.\n    # Big-O running times for all methods are the same as regular dictionaries.\n\n    # The internal self.__map dict maps keys to links in a doubly linked list.\n    # The circular doubly linked list starts and ends with a sentinel element.\n    # The sentinel element never gets deleted (this simplifies the algorithm).\n    # The sentinel is in self.__hardroot with a weakref proxy in self.__root.\n    # The prev links are weakref proxies (to prevent circular references).\n    # Individual links are kept alive by the hard reference in self.__map.\n    # Those hard references disappear when a key is deleted from an OrderedDict.\n\n    def __init__(self, *args, **kwds):\n        '''Initialize an ordered dictionary.  The signature is the same as\n        regular dictionaries, but keyword arguments are not recommended because\n        their insertion order is arbitrary.\n\n        '''\n        if len(args) > 1:\n            raise TypeError('expected at most 1 arguments, got %d' % len(args))\n        try:\n            self.__root\n        except AttributeError:\n            self.__hardroot = _Link()\n            self.__root = root = _proxy(self.__hardroot)\n            root.prev = root.next = root\n            self.__map = {}\n        self.__update(*args, **kwds)\n\n    def __setitem__(self, key, value,\n                    dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link):\n        'od.__setitem__(i, y) <==> od[i]=y'\n        # Setting a new item creates a new link at the end of the linked list,\n        # and the inherited dictionary is updated with the new key/value pair.\n        if key not in self:\n            self.__map[key] = link = Link()\n            root = self.__root\n            last = root.prev\n            link.prev, link.next, link.key = last, root, key\n            last.next = link\n            root.prev = proxy(link)\n        dict_setitem(self, key, value)\n\n    def __delitem__(self, key, dict_delitem=dict.__delitem__):\n        'od.__delitem__(y) <==> del od[y]'\n        # Deleting an existing item uses self.__map to find the link which gets\n        # removed by updating the links in the predecessor and successor nodes.\n        dict_delitem(self, key)\n        link = self.__map.pop(key)\n        link_prev = link.prev\n        link_next = link.next\n        link_prev.next = link_next\n        link_next.prev = link_prev\n\n    def __iter__(self):\n        'od.__iter__() <==> iter(od)'\n        # Traverse the linked list in order.\n        root = self.__root\n        curr = root.next\n        while curr is not root:\n            yield curr.key\n            curr = curr.next\n\n    def __reversed__(self):\n        'od.__reversed__() <==> reversed(od)'\n        # Traverse the linked list in reverse order.\n        root = self.__root\n        curr = root.prev\n        while curr is not root:\n            yield curr.key\n            curr = curr.prev\n\n    def clear(self):\n        'od.clear() -> None.  Remove all items from od.'\n        root = self.__root\n        root.prev = root.next = root\n        self.__map.clear()\n        dict.clear(self)\n\n    def popitem(self, last=True):\n        '''od.popitem() -> (k, v), return and remove a (key, value) pair.\n        Pairs are returned in LIFO order if last is true or FIFO order if false.\n\n        '''\n        if not self:\n            raise KeyError('dictionary is empty')\n        root = self.__root\n        if last:\n            link = root.prev\n            link_prev = link.prev\n            link_prev.next = root\n            root.prev = link_prev\n        else:\n            link = root.next\n            link_next = link.next\n            root.next = link_next\n            link_next.prev = root\n        key = link.key\n        del self.__map[key]\n        value = dict.pop(self, key)\n        return key, value\n\n    def move_to_end(self, key, last=True):\n        '''Move an existing element to the end (or beginning if last==False).\n\n        Raises KeyError if the element does not exist.\n        When last=True, acts like a fast version of self[key]=self.pop(key).\n\n        '''\n        link = self.__map[key]\n        link_prev = link.prev\n        link_next = link.next\n        link_prev.next = link_next\n        link_next.prev = link_prev\n        root = self.__root\n        if last:\n            last = root.prev\n            link.prev = last\n            link.next = root\n            last.next = root.prev = link\n        else:\n            first = root.next\n            link.prev = root\n            link.next = first\n            root.next = first.prev = link\n\n    def __sizeof__(self):\n        sizeof = _sys.getsizeof\n        n = len(self) + 1                       # number of links including root\n        size = sizeof(self.__dict__)            # instance dictionary\n        size += sizeof(self.__map) * 2          # internal dict and inherited dict\n        size += sizeof(self.__hardroot) * n     # link objects\n        size += sizeof(self.__root) * n         # proxy objects\n        return size\n\n    update = __update = MutableMapping.update\n    keys = MutableMapping.keys\n    values = MutableMapping.values\n    items = MutableMapping.items\n    __ne__ = MutableMapping.__ne__\n\n    __marker = object()\n\n    def pop(self, key, default=__marker):\n        '''od.pop(k[,d]) -> v, remove specified key and return the corresponding\n        value.  If key is not found, d is returned if given, otherwise KeyError\n        is raised.\n\n        '''\n        if key in self:\n            result = self[key]\n            del self[key]\n            return result\n        if default is self.__marker:\n            raise KeyError(key)\n        return default\n\n    def setdefault(self, key, default=None):\n        'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'\n        if key in self:\n            return self[key]\n        self[key] = default\n        return default\n\n    @_recursive_repr()\n    def __repr__(self):\n        'od.__repr__() <==> repr(od)'\n        if not self:\n            return '%s()' % (self.__class__.__name__,)\n        return '%s(%r)' % (self.__class__.__name__, list(self.items()))\n\n    def __reduce__(self):\n        'Return state information for pickling'\n        items = [[k, self[k]] for k in self]\n        inst_dict = vars(self).copy()\n        for k in vars(OrderedDict()):\n            inst_dict.pop(k, None)\n        if inst_dict:\n            return (self.__class__, (items,), inst_dict)\n        return self.__class__, (items,)\n\n    def copy(self):\n        'od.copy() -> a shallow copy of od'\n        return self.__class__(self)\n\n    @classmethod\n    def fromkeys(cls, iterable, value=None):\n        '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S.\n        If not specified, the value defaults to None.\n\n        '''\n        self = cls()\n        for key in iterable:\n            self[key] = value\n        return self\n\n    def __eq__(self, other):\n        '''od.__eq__(y) <==> od==y.  Comparison to another OD is order-sensitive\n        while comparison to a regular mapping is order-insensitive.\n\n        '''\n        if isinstance(other, OrderedDict):\n            return len(self)==len(other) and \\\n                   all(p==q for p, q in zip(self.items(), other.items()))\n        return dict.__eq__(self, other)\n\n# update_wrapper() and wraps() are tools to help write\n# wrapper functions that can handle naive introspection\n\nWRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')\nWRAPPER_UPDATES = ('__dict__',)\ndef update_wrapper(wrapper,\n                   wrapped,\n                   assigned = WRAPPER_ASSIGNMENTS,\n                   updated = WRAPPER_UPDATES):\n    \"\"\"Update a wrapper function to look like the wrapped function\n\n       wrapper is the function to be updated\n       wrapped is the original function\n       assigned is a tuple naming the attributes assigned directly\n       from the wrapped function to the wrapper function (defaults to\n       functools.WRAPPER_ASSIGNMENTS)\n       updated is a tuple naming the attributes of the wrapper that\n       are updated with the corresponding attribute from the wrapped\n       function (defaults to functools.WRAPPER_UPDATES)\n    \"\"\"\n    wrapper.__wrapped__ = wrapped\n    for attr in assigned:\n        try:\n            value = getattr(wrapped, attr)\n        except AttributeError:\n            pass\n        else:\n            setattr(wrapper, attr, value)\n    for attr in updated:\n        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))\n    # Return the wrapper so this can be used as a decorator via partial()\n    return wrapper\n\ndef wraps(wrapped,\n          assigned = WRAPPER_ASSIGNMENTS,\n          updated = WRAPPER_UPDATES):\n    \"\"\"Decorator factory to apply update_wrapper() to a wrapper function\n\n       Returns a decorator that invokes update_wrapper() with the decorated\n       function as the wrapper argument and the arguments to wraps() as the\n       remaining arguments. Default arguments are as for update_wrapper().\n       This is a convenience function to simplify applying partial() to\n       update_wrapper().\n    \"\"\"\n    return partial(update_wrapper, wrapped=wrapped,\n                   assigned=assigned, updated=updated)\n\ndef total_ordering(cls):\n    \"\"\"Class decorator that fills in missing ordering methods\"\"\"\n    convert = {\n        '__lt__': [('__gt__', lambda self, other: not (self < other or self == other)),\n                   ('__le__', lambda self, other: self < other or self == other),\n                   ('__ge__', lambda self, other: not self < other)],\n        '__le__': [('__ge__', lambda self, other: not self <= other or self == other),\n                   ('__lt__', lambda self, other: self <= other and not self == other),\n                   ('__gt__', lambda self, other: not self <= other)],\n        '__gt__': [('__lt__', lambda self, other: not (self > other or self == other)),\n                   ('__ge__', lambda self, other: self > other or self == other),\n                   ('__le__', lambda self, other: not self > other)],\n        '__ge__': [('__le__', lambda self, other: (not self >= other) or self == other),\n                   ('__gt__', lambda self, other: self >= other and not self == other),\n                   ('__lt__', lambda self, other: not self >= other)]\n    }\n    roots = set(dir(cls)) & set(convert)\n    if not roots:\n        raise ValueError('must define at least one ordering operation: < > <= >=')\n    root = max(roots)       # prefer __lt__ to __le__ to __gt__ to __ge__\n    for opname, opfunc in convert[root]:\n        if opname not in roots:\n            opfunc.__name__ = opname\n            opfunc.__doc__ = getattr(int, opname).__doc__\n            setattr(cls, opname, opfunc)\n    return cls\n\ndef cmp_to_key(mycmp):\n    \"\"\"Convert a cmp= function into a key= function\"\"\"\n    class K(object):\n        __slots__ = ['obj']\n        def __init__(self, obj):\n            self.obj = obj\n        def __lt__(self, other):\n            return mycmp(self.obj, other.obj) < 0\n        def __gt__(self, other):\n            return mycmp(self.obj, other.obj) > 0\n        def __eq__(self, other):\n            return mycmp(self.obj, other.obj) == 0\n        def __le__(self, other):\n            return mycmp(self.obj, other.obj) <= 0\n        def __ge__(self, other):\n            return mycmp(self.obj, other.obj) >= 0\n        def __ne__(self, other):\n            return mycmp(self.obj, other.obj) != 0\n        __hash__ = None\n    return K\n\n_CacheInfo = namedtuple(\"CacheInfo\", \"hits misses maxsize currsize\")\n\ndef lru_cache(maxsize=100):\n    \"\"\"Least-recently-used cache decorator.\n\n    If *maxsize* is set to None, the LRU features are disabled and the cache\n    can grow without bound.\n\n    Arguments to the cached function must be hashable.\n\n    View the cache statistics named tuple (hits, misses, maxsize, currsize) with\n    f.cache_info().  Clear the cache and statistics with f.cache_clear().\n    Access the underlying function with f.__wrapped__.\n\n    See:  http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used\n\n    \"\"\"\n    # Users should only access the lru_cache through its public API:\n    #       cache_info, cache_clear, and f.__wrapped__\n    # The internals of the lru_cache are encapsulated for thread safety and\n    # to allow the implementation to change (including a possible C version).\n\n    def decorating_function(user_function,\n                tuple=tuple, sorted=sorted, len=len, KeyError=KeyError):\n\n        hits, misses = [0], [0]\n        kwd_mark = (object(),)          # separates positional and keyword args\n        lock = Lock()                   # needed because OrderedDict isn't threadsafe\n\n        if maxsize is None:\n            cache = dict()              # simple cache without ordering or size limit\n\n            @wraps(user_function)\n            def wrapper(*args, **kwds):\n                key = args\n                if kwds:\n                    key += kwd_mark + tuple(sorted(kwds.items()))\n                try:\n                    result = cache[key]\n                    hits[0] += 1\n                    return result\n                except KeyError:\n                    pass\n                result = user_function(*args, **kwds)\n                cache[key] = result\n                misses[0] += 1\n                return result\n        else:\n            cache = OrderedDict()           # ordered least recent to most recent\n            cache_popitem = cache.popitem\n            cache_renew = cache.move_to_end\n\n            @wraps(user_function)\n            def wrapper(*args, **kwds):\n                key = args\n                if kwds:\n                    key += kwd_mark + tuple(sorted(kwds.items()))\n                with lock:\n                    try:\n                        result = cache[key]\n                        cache_renew(key)    # record recent use of this key\n                        hits[0] += 1\n                        return result\n                    except KeyError:\n                        pass\n                result = user_function(*args, **kwds)\n                with lock:\n                    cache[key] = result     # record recent use of this key\n                    misses[0] += 1\n                    if len(cache) > maxsize:\n                        cache_popitem(0)    # purge least recently used cache entry\n                return result\n\n        def cache_info():\n            \"\"\"Report cache statistics\"\"\"\n            with lock:\n                return _CacheInfo(hits[0], misses[0], maxsize, len(cache))\n\n        def cache_clear():\n            \"\"\"Clear the cache and cache statistics\"\"\"\n            with lock:\n                cache.clear()\n                hits[0] = misses[0] = 0\n\n        wrapper.cache_info = cache_info\n        wrapper.cache_clear = cache_clear\n        return wrapper\n\n    return decorating_function\n"
  },
  {
    "path": "openpype/vendor/python/python_2/functools32/reprlib32.py",
    "content": "\"\"\"Redo the builtin repr() (representation) but with limits on most sizes.\"\"\"\n\n__all__ = [\"Repr\", \"repr\", \"recursive_repr\"]\n\nimport __builtin__ as builtins\nfrom itertools import islice\ntry:\n    from thread import get_ident\nexcept ImportError:\n    from _dummy_thread32 import get_ident\n\ndef recursive_repr(fillvalue='...'):\n    'Decorator to make a repr function return fillvalue for a recursive call'\n\n    def decorating_function(user_function):\n        repr_running = set()\n\n        def wrapper(self):\n            key = id(self), get_ident()\n            if key in repr_running:\n                return fillvalue\n            repr_running.add(key)\n            try:\n                result = user_function(self)\n            finally:\n                repr_running.discard(key)\n            return result\n\n        # Can't use functools.wraps() here because of bootstrap issues\n        wrapper.__module__ = getattr(user_function, '__module__')\n        wrapper.__doc__ = getattr(user_function, '__doc__')\n        wrapper.__name__ = getattr(user_function, '__name__')\n        wrapper.__annotations__ = getattr(user_function, '__annotations__', {})\n        return wrapper\n\n    return decorating_function\n\nclass Repr:\n\n    def __init__(self):\n        self.maxlevel = 6\n        self.maxtuple = 6\n        self.maxlist = 6\n        self.maxarray = 5\n        self.maxdict = 4\n        self.maxset = 6\n        self.maxfrozenset = 6\n        self.maxdeque = 6\n        self.maxstring = 30\n        self.maxlong = 40\n        self.maxother = 30\n\n    def repr(self, x):\n        return self.repr1(x, self.maxlevel)\n\n    def repr1(self, x, level):\n        typename = type(x).__name__\n        if ' ' in typename:\n            parts = typename.split()\n            typename = '_'.join(parts)\n        if hasattr(self, 'repr_' + typename):\n            return getattr(self, 'repr_' + typename)(x, level)\n        else:\n            return self.repr_instance(x, level)\n\n    def _repr_iterable(self, x, level, left, right, maxiter, trail=''):\n        n = len(x)\n        if level <= 0 and n:\n            s = '...'\n        else:\n            newlevel = level - 1\n            repr1 = self.repr1\n            pieces = [repr1(elem, newlevel) for elem in islice(x, maxiter)]\n            if n > maxiter:  pieces.append('...')\n            s = ', '.join(pieces)\n            if n == 1 and trail:  right = trail + right\n        return '%s%s%s' % (left, s, right)\n\n    def repr_tuple(self, x, level):\n        return self._repr_iterable(x, level, '(', ')', self.maxtuple, ',')\n\n    def repr_list(self, x, level):\n        return self._repr_iterable(x, level, '[', ']', self.maxlist)\n\n    def repr_array(self, x, level):\n        header = \"array('%s', [\" % x.typecode\n        return self._repr_iterable(x, level, header, '])', self.maxarray)\n\n    def repr_set(self, x, level):\n        x = _possibly_sorted(x)\n        return self._repr_iterable(x, level, 'set([', '])', self.maxset)\n\n    def repr_frozenset(self, x, level):\n        x = _possibly_sorted(x)\n        return self._repr_iterable(x, level, 'frozenset([', '])',\n                                   self.maxfrozenset)\n\n    def repr_deque(self, x, level):\n        return self._repr_iterable(x, level, 'deque([', '])', self.maxdeque)\n\n    def repr_dict(self, x, level):\n        n = len(x)\n        if n == 0: return '{}'\n        if level <= 0: return '{...}'\n        newlevel = level - 1\n        repr1 = self.repr1\n        pieces = []\n        for key in islice(_possibly_sorted(x), self.maxdict):\n            keyrepr = repr1(key, newlevel)\n            valrepr = repr1(x[key], newlevel)\n            pieces.append('%s: %s' % (keyrepr, valrepr))\n        if n > self.maxdict: pieces.append('...')\n        s = ', '.join(pieces)\n        return '{%s}' % (s,)\n\n    def repr_str(self, x, level):\n        s = builtins.repr(x[:self.maxstring])\n        if len(s) > self.maxstring:\n            i = max(0, (self.maxstring-3)//2)\n            j = max(0, self.maxstring-3-i)\n            s = builtins.repr(x[:i] + x[len(x)-j:])\n            s = s[:i] + '...' + s[len(s)-j:]\n        return s\n\n    def repr_int(self, x, level):\n        s = builtins.repr(x) # XXX Hope this isn't too slow...\n        if len(s) > self.maxlong:\n            i = max(0, (self.maxlong-3)//2)\n            j = max(0, self.maxlong-3-i)\n            s = s[:i] + '...' + s[len(s)-j:]\n        return s\n\n    def repr_instance(self, x, level):\n        try:\n            s = builtins.repr(x)\n            # Bugs in x.__repr__() can cause arbitrary\n            # exceptions -- then make up something\n        except Exception:\n            return '<%s instance at %x>' % (x.__class__.__name__, id(x))\n        if len(s) > self.maxother:\n            i = max(0, (self.maxother-3)//2)\n            j = max(0, self.maxother-3-i)\n            s = s[:i] + '...' + s[len(s)-j:]\n        return s\n\n\ndef _possibly_sorted(x):\n    # Since not all sequences of items can be sorted and comparison\n    # functions may raise arbitrary exceptions, return an unsorted\n    # sequence in that case.\n    try:\n        return sorted(x)\n    except Exception:\n        return list(x)\n\naRepr = Repr()\nrepr = aRepr.repr\n"
  },
  {
    "path": "openpype/vendor/python/python_2/idna/__init__.py",
    "content": "from .package_data import __version__\nfrom .core import *\n"
  },
  {
    "path": "openpype/vendor/python/python_2/idna/codec.py",
    "content": "from .core import encode, decode, alabel, ulabel, IDNAError\nimport codecs\nimport re\n\n_unicode_dots_re = re.compile(u'[\\u002e\\u3002\\uff0e\\uff61]')\n\nclass Codec(codecs.Codec):\n\n    def encode(self, data, errors='strict'):\n\n        if errors != 'strict':\n            raise IDNAError(\"Unsupported error handling \\\"{0}\\\"\".format(errors))\n\n        if not data:\n            return \"\", 0\n\n        return encode(data), len(data)\n\n    def decode(self, data, errors='strict'):\n\n        if errors != 'strict':\n            raise IDNAError(\"Unsupported error handling \\\"{0}\\\"\".format(errors))\n\n        if not data:\n            return u\"\", 0\n\n        return decode(data), len(data)\n\nclass IncrementalEncoder(codecs.BufferedIncrementalEncoder):\n    def _buffer_encode(self, data, errors, final):\n        if errors != 'strict':\n            raise IDNAError(\"Unsupported error handling \\\"{0}\\\"\".format(errors))\n\n        if not data:\n            return (\"\", 0)\n\n        labels = _unicode_dots_re.split(data)\n        trailing_dot = u''\n        if labels:\n            if not labels[-1]:\n                trailing_dot = '.'\n                del labels[-1]\n            elif not final:\n                # Keep potentially unfinished label until the next call\n                del labels[-1]\n                if labels:\n                    trailing_dot = '.'\n\n        result = []\n        size = 0\n        for label in labels:\n            result.append(alabel(label))\n            if size:\n                size += 1\n            size += len(label)\n\n        # Join with U+002E\n        result = \".\".join(result) + trailing_dot\n        size += len(trailing_dot)\n        return (result, size)\n\nclass IncrementalDecoder(codecs.BufferedIncrementalDecoder):\n    def _buffer_decode(self, data, errors, final):\n        if errors != 'strict':\n            raise IDNAError(\"Unsupported error handling \\\"{0}\\\"\".format(errors))\n\n        if not data:\n            return (u\"\", 0)\n\n        # IDNA allows decoding to operate on Unicode strings, too.\n        if isinstance(data, unicode):\n            labels = _unicode_dots_re.split(data)\n        else:\n            # Must be ASCII string\n            data = str(data)\n            unicode(data, \"ascii\")\n            labels = data.split(\".\")\n\n        trailing_dot = u''\n        if labels:\n            if not labels[-1]:\n                trailing_dot = u'.'\n                del labels[-1]\n            elif not final:\n                # Keep potentially unfinished label until the next call\n                del labels[-1]\n                if labels:\n                    trailing_dot = u'.'\n\n        result = []\n        size = 0\n        for label in labels:\n            result.append(ulabel(label))\n            if size:\n                size += 1\n            size += len(label)\n\n        result = u\".\".join(result) + trailing_dot\n        size += len(trailing_dot)\n        return (result, size)\n\n\nclass StreamWriter(Codec, codecs.StreamWriter):\n    pass\n\nclass StreamReader(Codec, codecs.StreamReader):\n    pass\n\ndef getregentry():\n    return codecs.CodecInfo(\n        name='idna',\n        encode=Codec().encode,\n        decode=Codec().decode,\n        incrementalencoder=IncrementalEncoder,\n        incrementaldecoder=IncrementalDecoder,\n        streamwriter=StreamWriter,\n        streamreader=StreamReader,\n    )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/idna/compat.py",
    "content": "from .core import *\nfrom .codec import *\n\ndef ToASCII(label):\n    return encode(label)\n\ndef ToUnicode(label):\n    return decode(label)\n\ndef nameprep(s):\n    raise NotImplementedError(\"IDNA 2008 does not utilise nameprep protocol\")\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/idna/core.py",
    "content": "from . import idnadata\nimport bisect\nimport unicodedata\nimport re\nimport sys\nfrom .intranges import intranges_contain\n\n_virama_combining_class = 9\n_alabel_prefix = b'xn--'\n_unicode_dots_re = re.compile(u'[\\u002e\\u3002\\uff0e\\uff61]')\n\nif sys.version_info[0] >= 3:\n    unicode = str\n    unichr = chr\n\nclass IDNAError(UnicodeError):\n    \"\"\" Base exception for all IDNA-encoding related problems \"\"\"\n    pass\n\n\nclass IDNABidiError(IDNAError):\n    \"\"\" Exception when bidirectional requirements are not satisfied \"\"\"\n    pass\n\n\nclass InvalidCodepoint(IDNAError):\n    \"\"\" Exception when a disallowed or unallocated codepoint is used \"\"\"\n    pass\n\n\nclass InvalidCodepointContext(IDNAError):\n    \"\"\" Exception when the codepoint is not valid in the context it is used \"\"\"\n    pass\n\n\ndef _combining_class(cp):\n    v = unicodedata.combining(unichr(cp))\n    if v == 0:\n        if not unicodedata.name(unichr(cp)):\n            raise ValueError(\"Unknown character in unicodedata\")\n    return v\n\ndef _is_script(cp, script):\n    return intranges_contain(ord(cp), idnadata.scripts[script])\n\ndef _punycode(s):\n    return s.encode('punycode')\n\ndef _unot(s):\n    return 'U+{0:04X}'.format(s)\n\n\ndef valid_label_length(label):\n\n    if len(label) > 63:\n        return False\n    return True\n\n\ndef valid_string_length(label, trailing_dot):\n\n    if len(label) > (254 if trailing_dot else 253):\n        return False\n    return True\n\n\ndef check_bidi(label, check_ltr=False):\n\n    # Bidi rules should only be applied if string contains RTL characters\n    bidi_label = False\n    for (idx, cp) in enumerate(label, 1):\n        direction = unicodedata.bidirectional(cp)\n        if direction == '':\n            # String likely comes from a newer version of Unicode\n            raise IDNABidiError('Unknown directionality in label {0} at position {1}'.format(repr(label), idx))\n        if direction in ['R', 'AL', 'AN']:\n            bidi_label = True\n    if not bidi_label and not check_ltr:\n        return True\n\n    # Bidi rule 1\n    direction = unicodedata.bidirectional(label[0])\n    if direction in ['R', 'AL']:\n        rtl = True\n    elif direction == 'L':\n        rtl = False\n    else:\n        raise IDNABidiError('First codepoint in label {0} must be directionality L, R or AL'.format(repr(label)))\n\n    valid_ending = False\n    number_type = False\n    for (idx, cp) in enumerate(label, 1):\n        direction = unicodedata.bidirectional(cp)\n\n        if rtl:\n            # Bidi rule 2\n            if not direction in ['R', 'AL', 'AN', 'EN', 'ES', 'CS', 'ET', 'ON', 'BN', 'NSM']:\n                raise IDNABidiError('Invalid direction for codepoint at position {0} in a right-to-left label'.format(idx))\n            # Bidi rule 3\n            if direction in ['R', 'AL', 'EN', 'AN']:\n                valid_ending = True\n            elif direction != 'NSM':\n                valid_ending = False\n            # Bidi rule 4\n            if direction in ['AN', 'EN']:\n                if not number_type:\n                    number_type = direction\n                else:\n                    if number_type != direction:\n                        raise IDNABidiError('Can not mix numeral types in a right-to-left label')\n        else:\n            # Bidi rule 5\n            if not direction in ['L', 'EN', 'ES', 'CS', 'ET', 'ON', 'BN', 'NSM']:\n                raise IDNABidiError('Invalid direction for codepoint at position {0} in a left-to-right label'.format(idx))\n            # Bidi rule 6\n            if direction in ['L', 'EN']:\n                valid_ending = True\n            elif direction != 'NSM':\n                valid_ending = False\n\n    if not valid_ending:\n        raise IDNABidiError('Label ends with illegal codepoint directionality')\n\n    return True\n\n\ndef check_initial_combiner(label):\n\n    if unicodedata.category(label[0])[0] == 'M':\n        raise IDNAError('Label begins with an illegal combining character')\n    return True\n\n\ndef check_hyphen_ok(label):\n\n    if label[2:4] == '--':\n        raise IDNAError('Label has disallowed hyphens in 3rd and 4th position')\n    if label[0] == '-' or label[-1] == '-':\n        raise IDNAError('Label must not start or end with a hyphen')\n    return True\n\n\ndef check_nfc(label):\n\n    if unicodedata.normalize('NFC', label) != label:\n        raise IDNAError('Label must be in Normalization Form C')\n\n\ndef valid_contextj(label, pos):\n\n    cp_value = ord(label[pos])\n\n    if cp_value == 0x200c:\n\n        if pos > 0:\n            if _combining_class(ord(label[pos - 1])) == _virama_combining_class:\n                return True\n\n        ok = False\n        for i in range(pos-1, -1, -1):\n            joining_type = idnadata.joining_types.get(ord(label[i]))\n            if joining_type == ord('T'):\n                continue\n            if joining_type in [ord('L'), ord('D')]:\n                ok = True\n                break\n\n        if not ok:\n            return False\n\n        ok = False\n        for i in range(pos+1, len(label)):\n            joining_type = idnadata.joining_types.get(ord(label[i]))\n            if joining_type == ord('T'):\n                continue\n            if joining_type in [ord('R'), ord('D')]:\n                ok = True\n                break\n        return ok\n\n    if cp_value == 0x200d:\n\n        if pos > 0:\n            if _combining_class(ord(label[pos - 1])) == _virama_combining_class:\n                return True\n        return False\n\n    else:\n\n        return False\n\n\ndef valid_contexto(label, pos, exception=False):\n\n    cp_value = ord(label[pos])\n\n    if cp_value == 0x00b7:\n        if 0 < pos < len(label)-1:\n            if ord(label[pos - 1]) == 0x006c and ord(label[pos + 1]) == 0x006c:\n                return True\n        return False\n\n    elif cp_value == 0x0375:\n        if pos < len(label)-1 and len(label) > 1:\n            return _is_script(label[pos + 1], 'Greek')\n        return False\n\n    elif cp_value == 0x05f3 or cp_value == 0x05f4:\n        if pos > 0:\n            return _is_script(label[pos - 1], 'Hebrew')\n        return False\n\n    elif cp_value == 0x30fb:\n        for cp in label:\n            if cp == u'\\u30fb':\n                continue\n            if _is_script(cp, 'Hiragana') or _is_script(cp, 'Katakana') or _is_script(cp, 'Han'):\n                return True\n        return False\n\n    elif 0x660 <= cp_value <= 0x669:\n        for cp in label:\n            if 0x6f0 <= ord(cp) <= 0x06f9:\n                return False\n        return True\n\n    elif 0x6f0 <= cp_value <= 0x6f9:\n        for cp in label:\n            if 0x660 <= ord(cp) <= 0x0669:\n                return False\n        return True\n\n\ndef check_label(label):\n\n    if isinstance(label, (bytes, bytearray)):\n        label = label.decode('utf-8')\n    if len(label) == 0:\n        raise IDNAError('Empty Label')\n\n    check_nfc(label)\n    check_hyphen_ok(label)\n    check_initial_combiner(label)\n\n    for (pos, cp) in enumerate(label):\n        cp_value = ord(cp)\n        if intranges_contain(cp_value, idnadata.codepoint_classes['PVALID']):\n            continue\n        elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTJ']):\n            try:\n                if not valid_contextj(label, pos):\n                    raise InvalidCodepointContext('Joiner {0} not allowed at position {1} in {2}'.format(\n                        _unot(cp_value), pos+1, repr(label)))\n            except ValueError:\n                raise IDNAError('Unknown codepoint adjacent to joiner {0} at position {1} in {2}'.format(\n                    _unot(cp_value), pos+1, repr(label)))\n        elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTO']):\n            if not valid_contexto(label, pos):\n                raise InvalidCodepointContext('Codepoint {0} not allowed at position {1} in {2}'.format(_unot(cp_value), pos+1, repr(label)))\n        else:\n            raise InvalidCodepoint('Codepoint {0} at position {1} of {2} not allowed'.format(_unot(cp_value), pos+1, repr(label)))\n\n    check_bidi(label)\n\n\ndef alabel(label):\n\n    try:\n        label = label.encode('ascii')\n        ulabel(label)\n        if not valid_label_length(label):\n            raise IDNAError('Label too long')\n        return label\n    except UnicodeEncodeError:\n        pass\n\n    if not label:\n        raise IDNAError('No Input')\n\n    label = unicode(label)\n    check_label(label)\n    label = _punycode(label)\n    label = _alabel_prefix + label\n\n    if not valid_label_length(label):\n        raise IDNAError('Label too long')\n\n    return label\n\n\ndef ulabel(label):\n\n    if not isinstance(label, (bytes, bytearray)):\n        try:\n            label = label.encode('ascii')\n        except UnicodeEncodeError:\n            check_label(label)\n            return label\n\n    label = label.lower()\n    if label.startswith(_alabel_prefix):\n        label = label[len(_alabel_prefix):]\n        if not label:\n            raise IDNAError('Malformed A-label, no Punycode eligible content found')\n        if label.decode('ascii')[-1] == '-':\n            raise IDNAError('A-label must not end with a hyphen')\n    else:\n        check_label(label)\n        return label.decode('ascii')\n\n    label = label.decode('punycode')\n    check_label(label)\n    return label\n\n\ndef uts46_remap(domain, std3_rules=True, transitional=False):\n    \"\"\"Re-map the characters in the string according to UTS46 processing.\"\"\"\n    from .uts46data import uts46data\n    output = u\"\"\n    try:\n        for pos, char in enumerate(domain):\n            code_point = ord(char)\n            uts46row = uts46data[code_point if code_point < 256 else\n                bisect.bisect_left(uts46data, (code_point, \"Z\")) - 1]\n            status = uts46row[1]\n            replacement = uts46row[2] if len(uts46row) == 3 else None\n            if (status == \"V\" or\n                    (status == \"D\" and not transitional) or\n                    (status == \"3\" and not std3_rules and replacement is None)):\n                output += char\n            elif replacement is not None and (status == \"M\" or\n                    (status == \"3\" and not std3_rules) or\n                    (status == \"D\" and transitional)):\n                output += replacement\n            elif status != \"I\":\n                raise IndexError()\n        return unicodedata.normalize(\"NFC\", output)\n    except IndexError:\n        raise InvalidCodepoint(\n            \"Codepoint {0} not allowed at position {1} in {2}\".format(\n            _unot(code_point), pos + 1, repr(domain)))\n\n\ndef encode(s, strict=False, uts46=False, std3_rules=False, transitional=False):\n\n    if isinstance(s, (bytes, bytearray)):\n        s = s.decode(\"ascii\")\n    if uts46:\n        s = uts46_remap(s, std3_rules, transitional)\n    trailing_dot = False\n    result = []\n    if strict:\n        labels = s.split('.')\n    else:\n        labels = _unicode_dots_re.split(s)\n    if not labels or labels == ['']:\n        raise IDNAError('Empty domain')\n    if labels[-1] == '':\n        del labels[-1]\n        trailing_dot = True\n    for label in labels:\n        s = alabel(label)\n        if s:\n            result.append(s)\n        else:\n            raise IDNAError('Empty label')\n    if trailing_dot:\n        result.append(b'')\n    s = b'.'.join(result)\n    if not valid_string_length(s, trailing_dot):\n        raise IDNAError('Domain too long')\n    return s\n\n\ndef decode(s, strict=False, uts46=False, std3_rules=False):\n\n    if isinstance(s, (bytes, bytearray)):\n        s = s.decode(\"ascii\")\n    if uts46:\n        s = uts46_remap(s, std3_rules, False)\n    trailing_dot = False\n    result = []\n    if not strict:\n        labels = _unicode_dots_re.split(s)\n    else:\n        labels = s.split(u'.')\n    if not labels or labels == ['']:\n        raise IDNAError('Empty domain')\n    if not labels[-1]:\n        del labels[-1]\n        trailing_dot = True\n    for label in labels:\n        s = ulabel(label)\n        if s:\n            result.append(s)\n        else:\n            raise IDNAError('Empty label')\n    if trailing_dot:\n        result.append(u'')\n    return u'.'.join(result)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/idna/idnadata.py",
    "content": "# This file is automatically generated by tools/idna-data\n\n__version__ = \"13.0.0\"\nscripts = {\n    'Greek': (\n        0x37000000374,\n        0x37500000378,\n        0x37a0000037e,\n        0x37f00000380,\n        0x38400000385,\n        0x38600000387,\n        0x3880000038b,\n        0x38c0000038d,\n        0x38e000003a2,\n        0x3a3000003e2,\n        0x3f000000400,\n        0x1d2600001d2b,\n        0x1d5d00001d62,\n        0x1d6600001d6b,\n        0x1dbf00001dc0,\n        0x1f0000001f16,\n        0x1f1800001f1e,\n        0x1f2000001f46,\n        0x1f4800001f4e,\n        0x1f5000001f58,\n        0x1f5900001f5a,\n        0x1f5b00001f5c,\n        0x1f5d00001f5e,\n        0x1f5f00001f7e,\n        0x1f8000001fb5,\n        0x1fb600001fc5,\n        0x1fc600001fd4,\n        0x1fd600001fdc,\n        0x1fdd00001ff0,\n        0x1ff200001ff5,\n        0x1ff600001fff,\n        0x212600002127,\n        0xab650000ab66,\n        0x101400001018f,\n        0x101a0000101a1,\n        0x1d2000001d246,\n    ),\n    'Han': (\n        0x2e8000002e9a,\n        0x2e9b00002ef4,\n        0x2f0000002fd6,\n        0x300500003006,\n        0x300700003008,\n        0x30210000302a,\n        0x30380000303c,\n        0x340000004dc0,\n        0x4e0000009ffd,\n        0xf9000000fa6e,\n        0xfa700000fada,\n        0x16ff000016ff2,\n        0x200000002a6de,\n        0x2a7000002b735,\n        0x2b7400002b81e,\n        0x2b8200002cea2,\n        0x2ceb00002ebe1,\n        0x2f8000002fa1e,\n        0x300000003134b,\n    ),\n    'Hebrew': (\n        0x591000005c8,\n        0x5d0000005eb,\n        0x5ef000005f5,\n        0xfb1d0000fb37,\n        0xfb380000fb3d,\n        0xfb3e0000fb3f,\n        0xfb400000fb42,\n        0xfb430000fb45,\n        0xfb460000fb50,\n    ),\n    'Hiragana': (\n        0x304100003097,\n        0x309d000030a0,\n        0x1b0010001b11f,\n        0x1b1500001b153,\n        0x1f2000001f201,\n    ),\n    'Katakana': (\n        0x30a1000030fb,\n        0x30fd00003100,\n        0x31f000003200,\n        0x32d0000032ff,\n        0x330000003358,\n        0xff660000ff70,\n        0xff710000ff9e,\n        0x1b0000001b001,\n        0x1b1640001b168,\n    ),\n}\njoining_types = {\n    0x600: 85,\n    0x601: 85,\n    0x602: 85,\n    0x603: 85,\n    0x604: 85,\n    0x605: 85,\n    0x608: 85,\n    0x60b: 85,\n    0x620: 68,\n    0x621: 85,\n    0x622: 82,\n    0x623: 82,\n    0x624: 82,\n    0x625: 82,\n    0x626: 68,\n    0x627: 82,\n    0x628: 68,\n    0x629: 82,\n    0x62a: 68,\n    0x62b: 68,\n    0x62c: 68,\n    0x62d: 68,\n    0x62e: 68,\n    0x62f: 82,\n    0x630: 82,\n    0x631: 82,\n    0x632: 82,\n    0x633: 68,\n    0x634: 68,\n    0x635: 68,\n    0x636: 68,\n    0x637: 68,\n    0x638: 68,\n    0x639: 68,\n    0x63a: 68,\n    0x63b: 68,\n    0x63c: 68,\n    0x63d: 68,\n    0x63e: 68,\n    0x63f: 68,\n    0x640: 67,\n    0x641: 68,\n    0x642: 68,\n    0x643: 68,\n    0x644: 68,\n    0x645: 68,\n    0x646: 68,\n    0x647: 68,\n    0x648: 82,\n    0x649: 68,\n    0x64a: 68,\n    0x66e: 68,\n    0x66f: 68,\n    0x671: 82,\n    0x672: 82,\n    0x673: 82,\n    0x674: 85,\n    0x675: 82,\n    0x676: 82,\n    0x677: 82,\n    0x678: 68,\n    0x679: 68,\n    0x67a: 68,\n    0x67b: 68,\n    0x67c: 68,\n    0x67d: 68,\n    0x67e: 68,\n    0x67f: 68,\n    0x680: 68,\n    0x681: 68,\n    0x682: 68,\n    0x683: 68,\n    0x684: 68,\n    0x685: 68,\n    0x686: 68,\n    0x687: 68,\n    0x688: 82,\n    0x689: 82,\n    0x68a: 82,\n    0x68b: 82,\n    0x68c: 82,\n    0x68d: 82,\n    0x68e: 82,\n    0x68f: 82,\n    0x690: 82,\n    0x691: 82,\n    0x692: 82,\n    0x693: 82,\n    0x694: 82,\n    0x695: 82,\n    0x696: 82,\n    0x697: 82,\n    0x698: 82,\n    0x699: 82,\n    0x69a: 68,\n    0x69b: 68,\n    0x69c: 68,\n    0x69d: 68,\n    0x69e: 68,\n    0x69f: 68,\n    0x6a0: 68,\n    0x6a1: 68,\n    0x6a2: 68,\n    0x6a3: 68,\n    0x6a4: 68,\n    0x6a5: 68,\n    0x6a6: 68,\n    0x6a7: 68,\n    0x6a8: 68,\n    0x6a9: 68,\n    0x6aa: 68,\n    0x6ab: 68,\n    0x6ac: 68,\n    0x6ad: 68,\n    0x6ae: 68,\n    0x6af: 68,\n    0x6b0: 68,\n    0x6b1: 68,\n    0x6b2: 68,\n    0x6b3: 68,\n    0x6b4: 68,\n    0x6b5: 68,\n    0x6b6: 68,\n    0x6b7: 68,\n    0x6b8: 68,\n    0x6b9: 68,\n    0x6ba: 68,\n    0x6bb: 68,\n    0x6bc: 68,\n    0x6bd: 68,\n    0x6be: 68,\n    0x6bf: 68,\n    0x6c0: 82,\n    0x6c1: 68,\n    0x6c2: 68,\n    0x6c3: 82,\n    0x6c4: 82,\n    0x6c5: 82,\n    0x6c6: 82,\n    0x6c7: 82,\n    0x6c8: 82,\n    0x6c9: 82,\n    0x6ca: 82,\n    0x6cb: 82,\n    0x6cc: 68,\n    0x6cd: 82,\n    0x6ce: 68,\n    0x6cf: 82,\n    0x6d0: 68,\n    0x6d1: 68,\n    0x6d2: 82,\n    0x6d3: 82,\n    0x6d5: 82,\n    0x6dd: 85,\n    0x6ee: 82,\n    0x6ef: 82,\n    0x6fa: 68,\n    0x6fb: 68,\n    0x6fc: 68,\n    0x6ff: 68,\n    0x70f: 84,\n    0x710: 82,\n    0x712: 68,\n    0x713: 68,\n    0x714: 68,\n    0x715: 82,\n    0x716: 82,\n    0x717: 82,\n    0x718: 82,\n    0x719: 82,\n    0x71a: 68,\n    0x71b: 68,\n    0x71c: 68,\n    0x71d: 68,\n    0x71e: 82,\n    0x71f: 68,\n    0x720: 68,\n    0x721: 68,\n    0x722: 68,\n    0x723: 68,\n    0x724: 68,\n    0x725: 68,\n    0x726: 68,\n    0x727: 68,\n    0x728: 82,\n    0x729: 68,\n    0x72a: 82,\n    0x72b: 68,\n    0x72c: 82,\n    0x72d: 68,\n    0x72e: 68,\n    0x72f: 82,\n    0x74d: 82,\n    0x74e: 68,\n    0x74f: 68,\n    0x750: 68,\n    0x751: 68,\n    0x752: 68,\n    0x753: 68,\n    0x754: 68,\n    0x755: 68,\n    0x756: 68,\n    0x757: 68,\n    0x758: 68,\n    0x759: 82,\n    0x75a: 82,\n    0x75b: 82,\n    0x75c: 68,\n    0x75d: 68,\n    0x75e: 68,\n    0x75f: 68,\n    0x760: 68,\n    0x761: 68,\n    0x762: 68,\n    0x763: 68,\n    0x764: 68,\n    0x765: 68,\n    0x766: 68,\n    0x767: 68,\n    0x768: 68,\n    0x769: 68,\n    0x76a: 68,\n    0x76b: 82,\n    0x76c: 82,\n    0x76d: 68,\n    0x76e: 68,\n    0x76f: 68,\n    0x770: 68,\n    0x771: 82,\n    0x772: 68,\n    0x773: 82,\n    0x774: 82,\n    0x775: 68,\n    0x776: 68,\n    0x777: 68,\n    0x778: 82,\n    0x779: 82,\n    0x77a: 68,\n    0x77b: 68,\n    0x77c: 68,\n    0x77d: 68,\n    0x77e: 68,\n    0x77f: 68,\n    0x7ca: 68,\n    0x7cb: 68,\n    0x7cc: 68,\n    0x7cd: 68,\n    0x7ce: 68,\n    0x7cf: 68,\n    0x7d0: 68,\n    0x7d1: 68,\n    0x7d2: 68,\n    0x7d3: 68,\n    0x7d4: 68,\n    0x7d5: 68,\n    0x7d6: 68,\n    0x7d7: 68,\n    0x7d8: 68,\n    0x7d9: 68,\n    0x7da: 68,\n    0x7db: 68,\n    0x7dc: 68,\n    0x7dd: 68,\n    0x7de: 68,\n    0x7df: 68,\n    0x7e0: 68,\n    0x7e1: 68,\n    0x7e2: 68,\n    0x7e3: 68,\n    0x7e4: 68,\n    0x7e5: 68,\n    0x7e6: 68,\n    0x7e7: 68,\n    0x7e8: 68,\n    0x7e9: 68,\n    0x7ea: 68,\n    0x7fa: 67,\n    0x840: 82,\n    0x841: 68,\n    0x842: 68,\n    0x843: 68,\n    0x844: 68,\n    0x845: 68,\n    0x846: 82,\n    0x847: 82,\n    0x848: 68,\n    0x849: 82,\n    0x84a: 68,\n    0x84b: 68,\n    0x84c: 68,\n    0x84d: 68,\n    0x84e: 68,\n    0x84f: 68,\n    0x850: 68,\n    0x851: 68,\n    0x852: 68,\n    0x853: 68,\n    0x854: 82,\n    0x855: 68,\n    0x856: 82,\n    0x857: 82,\n    0x858: 82,\n    0x860: 68,\n    0x861: 85,\n    0x862: 68,\n    0x863: 68,\n    0x864: 68,\n    0x865: 68,\n    0x866: 85,\n    0x867: 82,\n    0x868: 68,\n    0x869: 82,\n    0x86a: 82,\n    0x8a0: 68,\n    0x8a1: 68,\n    0x8a2: 68,\n    0x8a3: 68,\n    0x8a4: 68,\n    0x8a5: 68,\n    0x8a6: 68,\n    0x8a7: 68,\n    0x8a8: 68,\n    0x8a9: 68,\n    0x8aa: 82,\n    0x8ab: 82,\n    0x8ac: 82,\n    0x8ad: 85,\n    0x8ae: 82,\n    0x8af: 68,\n    0x8b0: 68,\n    0x8b1: 82,\n    0x8b2: 82,\n    0x8b3: 68,\n    0x8b4: 68,\n    0x8b6: 68,\n    0x8b7: 68,\n    0x8b8: 68,\n    0x8b9: 82,\n    0x8ba: 68,\n    0x8bb: 68,\n    0x8bc: 68,\n    0x8bd: 68,\n    0x8be: 68,\n    0x8bf: 68,\n    0x8c0: 68,\n    0x8c1: 68,\n    0x8c2: 68,\n    0x8c3: 68,\n    0x8c4: 68,\n    0x8c5: 68,\n    0x8c6: 68,\n    0x8c7: 68,\n    0x8e2: 85,\n    0x1806: 85,\n    0x1807: 68,\n    0x180a: 67,\n    0x180e: 85,\n    0x1820: 68,\n    0x1821: 68,\n    0x1822: 68,\n    0x1823: 68,\n    0x1824: 68,\n    0x1825: 68,\n    0x1826: 68,\n    0x1827: 68,\n    0x1828: 68,\n    0x1829: 68,\n    0x182a: 68,\n    0x182b: 68,\n    0x182c: 68,\n    0x182d: 68,\n    0x182e: 68,\n    0x182f: 68,\n    0x1830: 68,\n    0x1831: 68,\n    0x1832: 68,\n    0x1833: 68,\n    0x1834: 68,\n    0x1835: 68,\n    0x1836: 68,\n    0x1837: 68,\n    0x1838: 68,\n    0x1839: 68,\n    0x183a: 68,\n    0x183b: 68,\n    0x183c: 68,\n    0x183d: 68,\n    0x183e: 68,\n    0x183f: 68,\n    0x1840: 68,\n    0x1841: 68,\n    0x1842: 68,\n    0x1843: 68,\n    0x1844: 68,\n    0x1845: 68,\n    0x1846: 68,\n    0x1847: 68,\n    0x1848: 68,\n    0x1849: 68,\n    0x184a: 68,\n    0x184b: 68,\n    0x184c: 68,\n    0x184d: 68,\n    0x184e: 68,\n    0x184f: 68,\n    0x1850: 68,\n    0x1851: 68,\n    0x1852: 68,\n    0x1853: 68,\n    0x1854: 68,\n    0x1855: 68,\n    0x1856: 68,\n    0x1857: 68,\n    0x1858: 68,\n    0x1859: 68,\n    0x185a: 68,\n    0x185b: 68,\n    0x185c: 68,\n    0x185d: 68,\n    0x185e: 68,\n    0x185f: 68,\n    0x1860: 68,\n    0x1861: 68,\n    0x1862: 68,\n    0x1863: 68,\n    0x1864: 68,\n    0x1865: 68,\n    0x1866: 68,\n    0x1867: 68,\n    0x1868: 68,\n    0x1869: 68,\n    0x186a: 68,\n    0x186b: 68,\n    0x186c: 68,\n    0x186d: 68,\n    0x186e: 68,\n    0x186f: 68,\n    0x1870: 68,\n    0x1871: 68,\n    0x1872: 68,\n    0x1873: 68,\n    0x1874: 68,\n    0x1875: 68,\n    0x1876: 68,\n    0x1877: 68,\n    0x1878: 68,\n    0x1880: 85,\n    0x1881: 85,\n    0x1882: 85,\n    0x1883: 85,\n    0x1884: 85,\n    0x1885: 84,\n    0x1886: 84,\n    0x1887: 68,\n    0x1888: 68,\n    0x1889: 68,\n    0x188a: 68,\n    0x188b: 68,\n    0x188c: 68,\n    0x188d: 68,\n    0x188e: 68,\n    0x188f: 68,\n    0x1890: 68,\n    0x1891: 68,\n    0x1892: 68,\n    0x1893: 68,\n    0x1894: 68,\n    0x1895: 68,\n    0x1896: 68,\n    0x1897: 68,\n    0x1898: 68,\n    0x1899: 68,\n    0x189a: 68,\n    0x189b: 68,\n    0x189c: 68,\n    0x189d: 68,\n    0x189e: 68,\n    0x189f: 68,\n    0x18a0: 68,\n    0x18a1: 68,\n    0x18a2: 68,\n    0x18a3: 68,\n    0x18a4: 68,\n    0x18a5: 68,\n    0x18a6: 68,\n    0x18a7: 68,\n    0x18a8: 68,\n    0x18aa: 68,\n    0x200c: 85,\n    0x200d: 67,\n    0x202f: 85,\n    0x2066: 85,\n    0x2067: 85,\n    0x2068: 85,\n    0x2069: 85,\n    0xa840: 68,\n    0xa841: 68,\n    0xa842: 68,\n    0xa843: 68,\n    0xa844: 68,\n    0xa845: 68,\n    0xa846: 68,\n    0xa847: 68,\n    0xa848: 68,\n    0xa849: 68,\n    0xa84a: 68,\n    0xa84b: 68,\n    0xa84c: 68,\n    0xa84d: 68,\n    0xa84e: 68,\n    0xa84f: 68,\n    0xa850: 68,\n    0xa851: 68,\n    0xa852: 68,\n    0xa853: 68,\n    0xa854: 68,\n    0xa855: 68,\n    0xa856: 68,\n    0xa857: 68,\n    0xa858: 68,\n    0xa859: 68,\n    0xa85a: 68,\n    0xa85b: 68,\n    0xa85c: 68,\n    0xa85d: 68,\n    0xa85e: 68,\n    0xa85f: 68,\n    0xa860: 68,\n    0xa861: 68,\n    0xa862: 68,\n    0xa863: 68,\n    0xa864: 68,\n    0xa865: 68,\n    0xa866: 68,\n    0xa867: 68,\n    0xa868: 68,\n    0xa869: 68,\n    0xa86a: 68,\n    0xa86b: 68,\n    0xa86c: 68,\n    0xa86d: 68,\n    0xa86e: 68,\n    0xa86f: 68,\n    0xa870: 68,\n    0xa871: 68,\n    0xa872: 76,\n    0xa873: 85,\n    0x10ac0: 68,\n    0x10ac1: 68,\n    0x10ac2: 68,\n    0x10ac3: 68,\n    0x10ac4: 68,\n    0x10ac5: 82,\n    0x10ac6: 85,\n    0x10ac7: 82,\n    0x10ac8: 85,\n    0x10ac9: 82,\n    0x10aca: 82,\n    0x10acb: 85,\n    0x10acc: 85,\n    0x10acd: 76,\n    0x10ace: 82,\n    0x10acf: 82,\n    0x10ad0: 82,\n    0x10ad1: 82,\n    0x10ad2: 82,\n    0x10ad3: 68,\n    0x10ad4: 68,\n    0x10ad5: 68,\n    0x10ad6: 68,\n    0x10ad7: 76,\n    0x10ad8: 68,\n    0x10ad9: 68,\n    0x10ada: 68,\n    0x10adb: 68,\n    0x10adc: 68,\n    0x10add: 82,\n    0x10ade: 68,\n    0x10adf: 68,\n    0x10ae0: 68,\n    0x10ae1: 82,\n    0x10ae2: 85,\n    0x10ae3: 85,\n    0x10ae4: 82,\n    0x10aeb: 68,\n    0x10aec: 68,\n    0x10aed: 68,\n    0x10aee: 68,\n    0x10aef: 82,\n    0x10b80: 68,\n    0x10b81: 82,\n    0x10b82: 68,\n    0x10b83: 82,\n    0x10b84: 82,\n    0x10b85: 82,\n    0x10b86: 68,\n    0x10b87: 68,\n    0x10b88: 68,\n    0x10b89: 82,\n    0x10b8a: 68,\n    0x10b8b: 68,\n    0x10b8c: 82,\n    0x10b8d: 68,\n    0x10b8e: 82,\n    0x10b8f: 82,\n    0x10b90: 68,\n    0x10b91: 82,\n    0x10ba9: 82,\n    0x10baa: 82,\n    0x10bab: 82,\n    0x10bac: 82,\n    0x10bad: 68,\n    0x10bae: 68,\n    0x10baf: 85,\n    0x10d00: 76,\n    0x10d01: 68,\n    0x10d02: 68,\n    0x10d03: 68,\n    0x10d04: 68,\n    0x10d05: 68,\n    0x10d06: 68,\n    0x10d07: 68,\n    0x10d08: 68,\n    0x10d09: 68,\n    0x10d0a: 68,\n    0x10d0b: 68,\n    0x10d0c: 68,\n    0x10d0d: 68,\n    0x10d0e: 68,\n    0x10d0f: 68,\n    0x10d10: 68,\n    0x10d11: 68,\n    0x10d12: 68,\n    0x10d13: 68,\n    0x10d14: 68,\n    0x10d15: 68,\n    0x10d16: 68,\n    0x10d17: 68,\n    0x10d18: 68,\n    0x10d19: 68,\n    0x10d1a: 68,\n    0x10d1b: 68,\n    0x10d1c: 68,\n    0x10d1d: 68,\n    0x10d1e: 68,\n    0x10d1f: 68,\n    0x10d20: 68,\n    0x10d21: 68,\n    0x10d22: 82,\n    0x10d23: 68,\n    0x10f30: 68,\n    0x10f31: 68,\n    0x10f32: 68,\n    0x10f33: 82,\n    0x10f34: 68,\n    0x10f35: 68,\n    0x10f36: 68,\n    0x10f37: 68,\n    0x10f38: 68,\n    0x10f39: 68,\n    0x10f3a: 68,\n    0x10f3b: 68,\n    0x10f3c: 68,\n    0x10f3d: 68,\n    0x10f3e: 68,\n    0x10f3f: 68,\n    0x10f40: 68,\n    0x10f41: 68,\n    0x10f42: 68,\n    0x10f43: 68,\n    0x10f44: 68,\n    0x10f45: 85,\n    0x10f51: 68,\n    0x10f52: 68,\n    0x10f53: 68,\n    0x10f54: 82,\n    0x10fb0: 68,\n    0x10fb1: 85,\n    0x10fb2: 68,\n    0x10fb3: 68,\n    0x10fb4: 82,\n    0x10fb5: 82,\n    0x10fb6: 82,\n    0x10fb7: 85,\n    0x10fb8: 68,\n    0x10fb9: 82,\n    0x10fba: 82,\n    0x10fbb: 68,\n    0x10fbc: 68,\n    0x10fbd: 82,\n    0x10fbe: 68,\n    0x10fbf: 68,\n    0x10fc0: 85,\n    0x10fc1: 68,\n    0x10fc2: 82,\n    0x10fc3: 82,\n    0x10fc4: 68,\n    0x10fc5: 85,\n    0x10fc6: 85,\n    0x10fc7: 85,\n    0x10fc8: 85,\n    0x10fc9: 82,\n    0x10fca: 68,\n    0x10fcb: 76,\n    0x110bd: 85,\n    0x110cd: 85,\n    0x1e900: 68,\n    0x1e901: 68,\n    0x1e902: 68,\n    0x1e903: 68,\n    0x1e904: 68,\n    0x1e905: 68,\n    0x1e906: 68,\n    0x1e907: 68,\n    0x1e908: 68,\n    0x1e909: 68,\n    0x1e90a: 68,\n    0x1e90b: 68,\n    0x1e90c: 68,\n    0x1e90d: 68,\n    0x1e90e: 68,\n    0x1e90f: 68,\n    0x1e910: 68,\n    0x1e911: 68,\n    0x1e912: 68,\n    0x1e913: 68,\n    0x1e914: 68,\n    0x1e915: 68,\n    0x1e916: 68,\n    0x1e917: 68,\n    0x1e918: 68,\n    0x1e919: 68,\n    0x1e91a: 68,\n    0x1e91b: 68,\n    0x1e91c: 68,\n    0x1e91d: 68,\n    0x1e91e: 68,\n    0x1e91f: 68,\n    0x1e920: 68,\n    0x1e921: 68,\n    0x1e922: 68,\n    0x1e923: 68,\n    0x1e924: 68,\n    0x1e925: 68,\n    0x1e926: 68,\n    0x1e927: 68,\n    0x1e928: 68,\n    0x1e929: 68,\n    0x1e92a: 68,\n    0x1e92b: 68,\n    0x1e92c: 68,\n    0x1e92d: 68,\n    0x1e92e: 68,\n    0x1e92f: 68,\n    0x1e930: 68,\n    0x1e931: 68,\n    0x1e932: 68,\n    0x1e933: 68,\n    0x1e934: 68,\n    0x1e935: 68,\n    0x1e936: 68,\n    0x1e937: 68,\n    0x1e938: 68,\n    0x1e939: 68,\n    0x1e93a: 68,\n    0x1e93b: 68,\n    0x1e93c: 68,\n    0x1e93d: 68,\n    0x1e93e: 68,\n    0x1e93f: 68,\n    0x1e940: 68,\n    0x1e941: 68,\n    0x1e942: 68,\n    0x1e943: 68,\n    0x1e94b: 84,\n}\ncodepoint_classes = {\n    'PVALID': (\n        0x2d0000002e,\n        0x300000003a,\n        0x610000007b,\n        0xdf000000f7,\n        0xf800000100,\n        0x10100000102,\n        0x10300000104,\n        0x10500000106,\n        0x10700000108,\n        0x1090000010a,\n        0x10b0000010c,\n        0x10d0000010e,\n        0x10f00000110,\n        0x11100000112,\n        0x11300000114,\n        0x11500000116,\n        0x11700000118,\n        0x1190000011a,\n        0x11b0000011c,\n        0x11d0000011e,\n        0x11f00000120,\n        0x12100000122,\n        0x12300000124,\n        0x12500000126,\n        0x12700000128,\n        0x1290000012a,\n        0x12b0000012c,\n        0x12d0000012e,\n        0x12f00000130,\n        0x13100000132,\n        0x13500000136,\n        0x13700000139,\n        0x13a0000013b,\n        0x13c0000013d,\n        0x13e0000013f,\n        0x14200000143,\n        0x14400000145,\n        0x14600000147,\n        0x14800000149,\n        0x14b0000014c,\n        0x14d0000014e,\n        0x14f00000150,\n        0x15100000152,\n        0x15300000154,\n        0x15500000156,\n        0x15700000158,\n        0x1590000015a,\n        0x15b0000015c,\n        0x15d0000015e,\n        0x15f00000160,\n        0x16100000162,\n        0x16300000164,\n        0x16500000166,\n        0x16700000168,\n        0x1690000016a,\n        0x16b0000016c,\n        0x16d0000016e,\n        0x16f00000170,\n        0x17100000172,\n        0x17300000174,\n        0x17500000176,\n        0x17700000178,\n        0x17a0000017b,\n        0x17c0000017d,\n        0x17e0000017f,\n        0x18000000181,\n        0x18300000184,\n        0x18500000186,\n        0x18800000189,\n        0x18c0000018e,\n        0x19200000193,\n        0x19500000196,\n        0x1990000019c,\n        0x19e0000019f,\n        0x1a1000001a2,\n        0x1a3000001a4,\n        0x1a5000001a6,\n        0x1a8000001a9,\n        0x1aa000001ac,\n        0x1ad000001ae,\n        0x1b0000001b1,\n        0x1b4000001b5,\n        0x1b6000001b7,\n        0x1b9000001bc,\n        0x1bd000001c4,\n        0x1ce000001cf,\n        0x1d0000001d1,\n        0x1d2000001d3,\n        0x1d4000001d5,\n        0x1d6000001d7,\n        0x1d8000001d9,\n        0x1da000001db,\n        0x1dc000001de,\n        0x1df000001e0,\n        0x1e1000001e2,\n        0x1e3000001e4,\n        0x1e5000001e6,\n        0x1e7000001e8,\n        0x1e9000001ea,\n        0x1eb000001ec,\n        0x1ed000001ee,\n        0x1ef000001f1,\n        0x1f5000001f6,\n        0x1f9000001fa,\n        0x1fb000001fc,\n        0x1fd000001fe,\n        0x1ff00000200,\n        0x20100000202,\n        0x20300000204,\n        0x20500000206,\n        0x20700000208,\n        0x2090000020a,\n        0x20b0000020c,\n        0x20d0000020e,\n        0x20f00000210,\n        0x21100000212,\n        0x21300000214,\n        0x21500000216,\n        0x21700000218,\n        0x2190000021a,\n        0x21b0000021c,\n        0x21d0000021e,\n        0x21f00000220,\n        0x22100000222,\n        0x22300000224,\n        0x22500000226,\n        0x22700000228,\n        0x2290000022a,\n        0x22b0000022c,\n        0x22d0000022e,\n        0x22f00000230,\n        0x23100000232,\n        0x2330000023a,\n        0x23c0000023d,\n        0x23f00000241,\n        0x24200000243,\n        0x24700000248,\n        0x2490000024a,\n        0x24b0000024c,\n        0x24d0000024e,\n        0x24f000002b0,\n        0x2b9000002c2,\n        0x2c6000002d2,\n        0x2ec000002ed,\n        0x2ee000002ef,\n        0x30000000340,\n        0x34200000343,\n        0x3460000034f,\n        0x35000000370,\n        0x37100000372,\n        0x37300000374,\n        0x37700000378,\n        0x37b0000037e,\n        0x39000000391,\n        0x3ac000003cf,\n        0x3d7000003d8,\n        0x3d9000003da,\n        0x3db000003dc,\n        0x3dd000003de,\n        0x3df000003e0,\n        0x3e1000003e2,\n        0x3e3000003e4,\n        0x3e5000003e6,\n        0x3e7000003e8,\n        0x3e9000003ea,\n        0x3eb000003ec,\n        0x3ed000003ee,\n        0x3ef000003f0,\n        0x3f3000003f4,\n        0x3f8000003f9,\n        0x3fb000003fd,\n        0x43000000460,\n        0x46100000462,\n        0x46300000464,\n        0x46500000466,\n        0x46700000468,\n        0x4690000046a,\n        0x46b0000046c,\n        0x46d0000046e,\n        0x46f00000470,\n        0x47100000472,\n        0x47300000474,\n        0x47500000476,\n        0x47700000478,\n        0x4790000047a,\n        0x47b0000047c,\n        0x47d0000047e,\n        0x47f00000480,\n        0x48100000482,\n        0x48300000488,\n        0x48b0000048c,\n        0x48d0000048e,\n        0x48f00000490,\n        0x49100000492,\n        0x49300000494,\n        0x49500000496,\n        0x49700000498,\n        0x4990000049a,\n        0x49b0000049c,\n        0x49d0000049e,\n        0x49f000004a0,\n        0x4a1000004a2,\n        0x4a3000004a4,\n        0x4a5000004a6,\n        0x4a7000004a8,\n        0x4a9000004aa,\n        0x4ab000004ac,\n        0x4ad000004ae,\n        0x4af000004b0,\n        0x4b1000004b2,\n        0x4b3000004b4,\n        0x4b5000004b6,\n        0x4b7000004b8,\n        0x4b9000004ba,\n        0x4bb000004bc,\n        0x4bd000004be,\n        0x4bf000004c0,\n        0x4c2000004c3,\n        0x4c4000004c5,\n        0x4c6000004c7,\n        0x4c8000004c9,\n        0x4ca000004cb,\n        0x4cc000004cd,\n        0x4ce000004d0,\n        0x4d1000004d2,\n        0x4d3000004d4,\n        0x4d5000004d6,\n        0x4d7000004d8,\n        0x4d9000004da,\n        0x4db000004dc,\n        0x4dd000004de,\n        0x4df000004e0,\n        0x4e1000004e2,\n        0x4e3000004e4,\n        0x4e5000004e6,\n        0x4e7000004e8,\n        0x4e9000004ea,\n        0x4eb000004ec,\n        0x4ed000004ee,\n        0x4ef000004f0,\n        0x4f1000004f2,\n        0x4f3000004f4,\n        0x4f5000004f6,\n        0x4f7000004f8,\n        0x4f9000004fa,\n        0x4fb000004fc,\n        0x4fd000004fe,\n        0x4ff00000500,\n        0x50100000502,\n        0x50300000504,\n        0x50500000506,\n        0x50700000508,\n        0x5090000050a,\n        0x50b0000050c,\n        0x50d0000050e,\n        0x50f00000510,\n        0x51100000512,\n        0x51300000514,\n        0x51500000516,\n        0x51700000518,\n        0x5190000051a,\n        0x51b0000051c,\n        0x51d0000051e,\n        0x51f00000520,\n        0x52100000522,\n        0x52300000524,\n        0x52500000526,\n        0x52700000528,\n        0x5290000052a,\n        0x52b0000052c,\n        0x52d0000052e,\n        0x52f00000530,\n        0x5590000055a,\n        0x56000000587,\n        0x58800000589,\n        0x591000005be,\n        0x5bf000005c0,\n        0x5c1000005c3,\n        0x5c4000005c6,\n        0x5c7000005c8,\n        0x5d0000005eb,\n        0x5ef000005f3,\n        0x6100000061b,\n        0x62000000640,\n        0x64100000660,\n        0x66e00000675,\n        0x679000006d4,\n        0x6d5000006dd,\n        0x6df000006e9,\n        0x6ea000006f0,\n        0x6fa00000700,\n        0x7100000074b,\n        0x74d000007b2,\n        0x7c0000007f6,\n        0x7fd000007fe,\n        0x8000000082e,\n        0x8400000085c,\n        0x8600000086b,\n        0x8a0000008b5,\n        0x8b6000008c8,\n        0x8d3000008e2,\n        0x8e300000958,\n        0x96000000964,\n        0x96600000970,\n        0x97100000984,\n        0x9850000098d,\n        0x98f00000991,\n        0x993000009a9,\n        0x9aa000009b1,\n        0x9b2000009b3,\n        0x9b6000009ba,\n        0x9bc000009c5,\n        0x9c7000009c9,\n        0x9cb000009cf,\n        0x9d7000009d8,\n        0x9e0000009e4,\n        0x9e6000009f2,\n        0x9fc000009fd,\n        0x9fe000009ff,\n        0xa0100000a04,\n        0xa0500000a0b,\n        0xa0f00000a11,\n        0xa1300000a29,\n        0xa2a00000a31,\n        0xa3200000a33,\n        0xa3500000a36,\n        0xa3800000a3a,\n        0xa3c00000a3d,\n        0xa3e00000a43,\n        0xa4700000a49,\n        0xa4b00000a4e,\n        0xa5100000a52,\n        0xa5c00000a5d,\n        0xa6600000a76,\n        0xa8100000a84,\n        0xa8500000a8e,\n        0xa8f00000a92,\n        0xa9300000aa9,\n        0xaaa00000ab1,\n        0xab200000ab4,\n        0xab500000aba,\n        0xabc00000ac6,\n        0xac700000aca,\n        0xacb00000ace,\n        0xad000000ad1,\n        0xae000000ae4,\n        0xae600000af0,\n        0xaf900000b00,\n        0xb0100000b04,\n        0xb0500000b0d,\n        0xb0f00000b11,\n        0xb1300000b29,\n        0xb2a00000b31,\n        0xb3200000b34,\n        0xb3500000b3a,\n        0xb3c00000b45,\n        0xb4700000b49,\n        0xb4b00000b4e,\n        0xb5500000b58,\n        0xb5f00000b64,\n        0xb6600000b70,\n        0xb7100000b72,\n        0xb8200000b84,\n        0xb8500000b8b,\n        0xb8e00000b91,\n        0xb9200000b96,\n        0xb9900000b9b,\n        0xb9c00000b9d,\n        0xb9e00000ba0,\n        0xba300000ba5,\n        0xba800000bab,\n        0xbae00000bba,\n        0xbbe00000bc3,\n        0xbc600000bc9,\n        0xbca00000bce,\n        0xbd000000bd1,\n        0xbd700000bd8,\n        0xbe600000bf0,\n        0xc0000000c0d,\n        0xc0e00000c11,\n        0xc1200000c29,\n        0xc2a00000c3a,\n        0xc3d00000c45,\n        0xc4600000c49,\n        0xc4a00000c4e,\n        0xc5500000c57,\n        0xc5800000c5b,\n        0xc6000000c64,\n        0xc6600000c70,\n        0xc8000000c84,\n        0xc8500000c8d,\n        0xc8e00000c91,\n        0xc9200000ca9,\n        0xcaa00000cb4,\n        0xcb500000cba,\n        0xcbc00000cc5,\n        0xcc600000cc9,\n        0xcca00000cce,\n        0xcd500000cd7,\n        0xcde00000cdf,\n        0xce000000ce4,\n        0xce600000cf0,\n        0xcf100000cf3,\n        0xd0000000d0d,\n        0xd0e00000d11,\n        0xd1200000d45,\n        0xd4600000d49,\n        0xd4a00000d4f,\n        0xd5400000d58,\n        0xd5f00000d64,\n        0xd6600000d70,\n        0xd7a00000d80,\n        0xd8100000d84,\n        0xd8500000d97,\n        0xd9a00000db2,\n        0xdb300000dbc,\n        0xdbd00000dbe,\n        0xdc000000dc7,\n        0xdca00000dcb,\n        0xdcf00000dd5,\n        0xdd600000dd7,\n        0xdd800000de0,\n        0xde600000df0,\n        0xdf200000df4,\n        0xe0100000e33,\n        0xe3400000e3b,\n        0xe4000000e4f,\n        0xe5000000e5a,\n        0xe8100000e83,\n        0xe8400000e85,\n        0xe8600000e8b,\n        0xe8c00000ea4,\n        0xea500000ea6,\n        0xea700000eb3,\n        0xeb400000ebe,\n        0xec000000ec5,\n        0xec600000ec7,\n        0xec800000ece,\n        0xed000000eda,\n        0xede00000ee0,\n        0xf0000000f01,\n        0xf0b00000f0c,\n        0xf1800000f1a,\n        0xf2000000f2a,\n        0xf3500000f36,\n        0xf3700000f38,\n        0xf3900000f3a,\n        0xf3e00000f43,\n        0xf4400000f48,\n        0xf4900000f4d,\n        0xf4e00000f52,\n        0xf5300000f57,\n        0xf5800000f5c,\n        0xf5d00000f69,\n        0xf6a00000f6d,\n        0xf7100000f73,\n        0xf7400000f75,\n        0xf7a00000f81,\n        0xf8200000f85,\n        0xf8600000f93,\n        0xf9400000f98,\n        0xf9900000f9d,\n        0xf9e00000fa2,\n        0xfa300000fa7,\n        0xfa800000fac,\n        0xfad00000fb9,\n        0xfba00000fbd,\n        0xfc600000fc7,\n        0x10000000104a,\n        0x10500000109e,\n        0x10d0000010fb,\n        0x10fd00001100,\n        0x120000001249,\n        0x124a0000124e,\n        0x125000001257,\n        0x125800001259,\n        0x125a0000125e,\n        0x126000001289,\n        0x128a0000128e,\n        0x1290000012b1,\n        0x12b2000012b6,\n        0x12b8000012bf,\n        0x12c0000012c1,\n        0x12c2000012c6,\n        0x12c8000012d7,\n        0x12d800001311,\n        0x131200001316,\n        0x13180000135b,\n        0x135d00001360,\n        0x138000001390,\n        0x13a0000013f6,\n        0x14010000166d,\n        0x166f00001680,\n        0x16810000169b,\n        0x16a0000016eb,\n        0x16f1000016f9,\n        0x17000000170d,\n        0x170e00001715,\n        0x172000001735,\n        0x174000001754,\n        0x17600000176d,\n        0x176e00001771,\n        0x177200001774,\n        0x1780000017b4,\n        0x17b6000017d4,\n        0x17d7000017d8,\n        0x17dc000017de,\n        0x17e0000017ea,\n        0x18100000181a,\n        0x182000001879,\n        0x1880000018ab,\n        0x18b0000018f6,\n        0x19000000191f,\n        0x19200000192c,\n        0x19300000193c,\n        0x19460000196e,\n        0x197000001975,\n        0x1980000019ac,\n        0x19b0000019ca,\n        0x19d0000019da,\n        0x1a0000001a1c,\n        0x1a2000001a5f,\n        0x1a6000001a7d,\n        0x1a7f00001a8a,\n        0x1a9000001a9a,\n        0x1aa700001aa8,\n        0x1ab000001abe,\n        0x1abf00001ac1,\n        0x1b0000001b4c,\n        0x1b5000001b5a,\n        0x1b6b00001b74,\n        0x1b8000001bf4,\n        0x1c0000001c38,\n        0x1c4000001c4a,\n        0x1c4d00001c7e,\n        0x1cd000001cd3,\n        0x1cd400001cfb,\n        0x1d0000001d2c,\n        0x1d2f00001d30,\n        0x1d3b00001d3c,\n        0x1d4e00001d4f,\n        0x1d6b00001d78,\n        0x1d7900001d9b,\n        0x1dc000001dfa,\n        0x1dfb00001e00,\n        0x1e0100001e02,\n        0x1e0300001e04,\n        0x1e0500001e06,\n        0x1e0700001e08,\n        0x1e0900001e0a,\n        0x1e0b00001e0c,\n        0x1e0d00001e0e,\n        0x1e0f00001e10,\n        0x1e1100001e12,\n        0x1e1300001e14,\n        0x1e1500001e16,\n        0x1e1700001e18,\n        0x1e1900001e1a,\n        0x1e1b00001e1c,\n        0x1e1d00001e1e,\n        0x1e1f00001e20,\n        0x1e2100001e22,\n        0x1e2300001e24,\n        0x1e2500001e26,\n        0x1e2700001e28,\n        0x1e2900001e2a,\n        0x1e2b00001e2c,\n        0x1e2d00001e2e,\n        0x1e2f00001e30,\n        0x1e3100001e32,\n        0x1e3300001e34,\n        0x1e3500001e36,\n        0x1e3700001e38,\n        0x1e3900001e3a,\n        0x1e3b00001e3c,\n        0x1e3d00001e3e,\n        0x1e3f00001e40,\n        0x1e4100001e42,\n        0x1e4300001e44,\n        0x1e4500001e46,\n        0x1e4700001e48,\n        0x1e4900001e4a,\n        0x1e4b00001e4c,\n        0x1e4d00001e4e,\n        0x1e4f00001e50,\n        0x1e5100001e52,\n        0x1e5300001e54,\n        0x1e5500001e56,\n        0x1e5700001e58,\n        0x1e5900001e5a,\n        0x1e5b00001e5c,\n        0x1e5d00001e5e,\n        0x1e5f00001e60,\n        0x1e6100001e62,\n        0x1e6300001e64,\n        0x1e6500001e66,\n        0x1e6700001e68,\n        0x1e6900001e6a,\n        0x1e6b00001e6c,\n        0x1e6d00001e6e,\n        0x1e6f00001e70,\n        0x1e7100001e72,\n        0x1e7300001e74,\n        0x1e7500001e76,\n        0x1e7700001e78,\n        0x1e7900001e7a,\n        0x1e7b00001e7c,\n        0x1e7d00001e7e,\n        0x1e7f00001e80,\n        0x1e8100001e82,\n        0x1e8300001e84,\n        0x1e8500001e86,\n        0x1e8700001e88,\n        0x1e8900001e8a,\n        0x1e8b00001e8c,\n        0x1e8d00001e8e,\n        0x1e8f00001e90,\n        0x1e9100001e92,\n        0x1e9300001e94,\n        0x1e9500001e9a,\n        0x1e9c00001e9e,\n        0x1e9f00001ea0,\n        0x1ea100001ea2,\n        0x1ea300001ea4,\n        0x1ea500001ea6,\n        0x1ea700001ea8,\n        0x1ea900001eaa,\n        0x1eab00001eac,\n        0x1ead00001eae,\n        0x1eaf00001eb0,\n        0x1eb100001eb2,\n        0x1eb300001eb4,\n        0x1eb500001eb6,\n        0x1eb700001eb8,\n        0x1eb900001eba,\n        0x1ebb00001ebc,\n        0x1ebd00001ebe,\n        0x1ebf00001ec0,\n        0x1ec100001ec2,\n        0x1ec300001ec4,\n        0x1ec500001ec6,\n        0x1ec700001ec8,\n        0x1ec900001eca,\n        0x1ecb00001ecc,\n        0x1ecd00001ece,\n        0x1ecf00001ed0,\n        0x1ed100001ed2,\n        0x1ed300001ed4,\n        0x1ed500001ed6,\n        0x1ed700001ed8,\n        0x1ed900001eda,\n        0x1edb00001edc,\n        0x1edd00001ede,\n        0x1edf00001ee0,\n        0x1ee100001ee2,\n        0x1ee300001ee4,\n        0x1ee500001ee6,\n        0x1ee700001ee8,\n        0x1ee900001eea,\n        0x1eeb00001eec,\n        0x1eed00001eee,\n        0x1eef00001ef0,\n        0x1ef100001ef2,\n        0x1ef300001ef4,\n        0x1ef500001ef6,\n        0x1ef700001ef8,\n        0x1ef900001efa,\n        0x1efb00001efc,\n        0x1efd00001efe,\n        0x1eff00001f08,\n        0x1f1000001f16,\n        0x1f2000001f28,\n        0x1f3000001f38,\n        0x1f4000001f46,\n        0x1f5000001f58,\n        0x1f6000001f68,\n        0x1f7000001f71,\n        0x1f7200001f73,\n        0x1f7400001f75,\n        0x1f7600001f77,\n        0x1f7800001f79,\n        0x1f7a00001f7b,\n        0x1f7c00001f7d,\n        0x1fb000001fb2,\n        0x1fb600001fb7,\n        0x1fc600001fc7,\n        0x1fd000001fd3,\n        0x1fd600001fd8,\n        0x1fe000001fe3,\n        0x1fe400001fe8,\n        0x1ff600001ff7,\n        0x214e0000214f,\n        0x218400002185,\n        0x2c3000002c5f,\n        0x2c6100002c62,\n        0x2c6500002c67,\n        0x2c6800002c69,\n        0x2c6a00002c6b,\n        0x2c6c00002c6d,\n        0x2c7100002c72,\n        0x2c7300002c75,\n        0x2c7600002c7c,\n        0x2c8100002c82,\n        0x2c8300002c84,\n        0x2c8500002c86,\n        0x2c8700002c88,\n        0x2c8900002c8a,\n        0x2c8b00002c8c,\n        0x2c8d00002c8e,\n        0x2c8f00002c90,\n        0x2c9100002c92,\n        0x2c9300002c94,\n        0x2c9500002c96,\n        0x2c9700002c98,\n        0x2c9900002c9a,\n        0x2c9b00002c9c,\n        0x2c9d00002c9e,\n        0x2c9f00002ca0,\n        0x2ca100002ca2,\n        0x2ca300002ca4,\n        0x2ca500002ca6,\n        0x2ca700002ca8,\n        0x2ca900002caa,\n        0x2cab00002cac,\n        0x2cad00002cae,\n        0x2caf00002cb0,\n        0x2cb100002cb2,\n        0x2cb300002cb4,\n        0x2cb500002cb6,\n        0x2cb700002cb8,\n        0x2cb900002cba,\n        0x2cbb00002cbc,\n        0x2cbd00002cbe,\n        0x2cbf00002cc0,\n        0x2cc100002cc2,\n        0x2cc300002cc4,\n        0x2cc500002cc6,\n        0x2cc700002cc8,\n        0x2cc900002cca,\n        0x2ccb00002ccc,\n        0x2ccd00002cce,\n        0x2ccf00002cd0,\n        0x2cd100002cd2,\n        0x2cd300002cd4,\n        0x2cd500002cd6,\n        0x2cd700002cd8,\n        0x2cd900002cda,\n        0x2cdb00002cdc,\n        0x2cdd00002cde,\n        0x2cdf00002ce0,\n        0x2ce100002ce2,\n        0x2ce300002ce5,\n        0x2cec00002ced,\n        0x2cee00002cf2,\n        0x2cf300002cf4,\n        0x2d0000002d26,\n        0x2d2700002d28,\n        0x2d2d00002d2e,\n        0x2d3000002d68,\n        0x2d7f00002d97,\n        0x2da000002da7,\n        0x2da800002daf,\n        0x2db000002db7,\n        0x2db800002dbf,\n        0x2dc000002dc7,\n        0x2dc800002dcf,\n        0x2dd000002dd7,\n        0x2dd800002ddf,\n        0x2de000002e00,\n        0x2e2f00002e30,\n        0x300500003008,\n        0x302a0000302e,\n        0x303c0000303d,\n        0x304100003097,\n        0x30990000309b,\n        0x309d0000309f,\n        0x30a1000030fb,\n        0x30fc000030ff,\n        0x310500003130,\n        0x31a0000031c0,\n        0x31f000003200,\n        0x340000004dc0,\n        0x4e0000009ffd,\n        0xa0000000a48d,\n        0xa4d00000a4fe,\n        0xa5000000a60d,\n        0xa6100000a62c,\n        0xa6410000a642,\n        0xa6430000a644,\n        0xa6450000a646,\n        0xa6470000a648,\n        0xa6490000a64a,\n        0xa64b0000a64c,\n        0xa64d0000a64e,\n        0xa64f0000a650,\n        0xa6510000a652,\n        0xa6530000a654,\n        0xa6550000a656,\n        0xa6570000a658,\n        0xa6590000a65a,\n        0xa65b0000a65c,\n        0xa65d0000a65e,\n        0xa65f0000a660,\n        0xa6610000a662,\n        0xa6630000a664,\n        0xa6650000a666,\n        0xa6670000a668,\n        0xa6690000a66a,\n        0xa66b0000a66c,\n        0xa66d0000a670,\n        0xa6740000a67e,\n        0xa67f0000a680,\n        0xa6810000a682,\n        0xa6830000a684,\n        0xa6850000a686,\n        0xa6870000a688,\n        0xa6890000a68a,\n        0xa68b0000a68c,\n        0xa68d0000a68e,\n        0xa68f0000a690,\n        0xa6910000a692,\n        0xa6930000a694,\n        0xa6950000a696,\n        0xa6970000a698,\n        0xa6990000a69a,\n        0xa69b0000a69c,\n        0xa69e0000a6e6,\n        0xa6f00000a6f2,\n        0xa7170000a720,\n        0xa7230000a724,\n        0xa7250000a726,\n        0xa7270000a728,\n        0xa7290000a72a,\n        0xa72b0000a72c,\n        0xa72d0000a72e,\n        0xa72f0000a732,\n        0xa7330000a734,\n        0xa7350000a736,\n        0xa7370000a738,\n        0xa7390000a73a,\n        0xa73b0000a73c,\n        0xa73d0000a73e,\n        0xa73f0000a740,\n        0xa7410000a742,\n        0xa7430000a744,\n        0xa7450000a746,\n        0xa7470000a748,\n        0xa7490000a74a,\n        0xa74b0000a74c,\n        0xa74d0000a74e,\n        0xa74f0000a750,\n        0xa7510000a752,\n        0xa7530000a754,\n        0xa7550000a756,\n        0xa7570000a758,\n        0xa7590000a75a,\n        0xa75b0000a75c,\n        0xa75d0000a75e,\n        0xa75f0000a760,\n        0xa7610000a762,\n        0xa7630000a764,\n        0xa7650000a766,\n        0xa7670000a768,\n        0xa7690000a76a,\n        0xa76b0000a76c,\n        0xa76d0000a76e,\n        0xa76f0000a770,\n        0xa7710000a779,\n        0xa77a0000a77b,\n        0xa77c0000a77d,\n        0xa77f0000a780,\n        0xa7810000a782,\n        0xa7830000a784,\n        0xa7850000a786,\n        0xa7870000a789,\n        0xa78c0000a78d,\n        0xa78e0000a790,\n        0xa7910000a792,\n        0xa7930000a796,\n        0xa7970000a798,\n        0xa7990000a79a,\n        0xa79b0000a79c,\n        0xa79d0000a79e,\n        0xa79f0000a7a0,\n        0xa7a10000a7a2,\n        0xa7a30000a7a4,\n        0xa7a50000a7a6,\n        0xa7a70000a7a8,\n        0xa7a90000a7aa,\n        0xa7af0000a7b0,\n        0xa7b50000a7b6,\n        0xa7b70000a7b8,\n        0xa7b90000a7ba,\n        0xa7bb0000a7bc,\n        0xa7bd0000a7be,\n        0xa7bf0000a7c0,\n        0xa7c30000a7c4,\n        0xa7c80000a7c9,\n        0xa7ca0000a7cb,\n        0xa7f60000a7f8,\n        0xa7fa0000a828,\n        0xa82c0000a82d,\n        0xa8400000a874,\n        0xa8800000a8c6,\n        0xa8d00000a8da,\n        0xa8e00000a8f8,\n        0xa8fb0000a8fc,\n        0xa8fd0000a92e,\n        0xa9300000a954,\n        0xa9800000a9c1,\n        0xa9cf0000a9da,\n        0xa9e00000a9ff,\n        0xaa000000aa37,\n        0xaa400000aa4e,\n        0xaa500000aa5a,\n        0xaa600000aa77,\n        0xaa7a0000aac3,\n        0xaadb0000aade,\n        0xaae00000aaf0,\n        0xaaf20000aaf7,\n        0xab010000ab07,\n        0xab090000ab0f,\n        0xab110000ab17,\n        0xab200000ab27,\n        0xab280000ab2f,\n        0xab300000ab5b,\n        0xab600000ab6a,\n        0xabc00000abeb,\n        0xabec0000abee,\n        0xabf00000abfa,\n        0xac000000d7a4,\n        0xfa0e0000fa10,\n        0xfa110000fa12,\n        0xfa130000fa15,\n        0xfa1f0000fa20,\n        0xfa210000fa22,\n        0xfa230000fa25,\n        0xfa270000fa2a,\n        0xfb1e0000fb1f,\n        0xfe200000fe30,\n        0xfe730000fe74,\n        0x100000001000c,\n        0x1000d00010027,\n        0x100280001003b,\n        0x1003c0001003e,\n        0x1003f0001004e,\n        0x100500001005e,\n        0x10080000100fb,\n        0x101fd000101fe,\n        0x102800001029d,\n        0x102a0000102d1,\n        0x102e0000102e1,\n        0x1030000010320,\n        0x1032d00010341,\n        0x103420001034a,\n        0x103500001037b,\n        0x103800001039e,\n        0x103a0000103c4,\n        0x103c8000103d0,\n        0x104280001049e,\n        0x104a0000104aa,\n        0x104d8000104fc,\n        0x1050000010528,\n        0x1053000010564,\n        0x1060000010737,\n        0x1074000010756,\n        0x1076000010768,\n        0x1080000010806,\n        0x1080800010809,\n        0x1080a00010836,\n        0x1083700010839,\n        0x1083c0001083d,\n        0x1083f00010856,\n        0x1086000010877,\n        0x108800001089f,\n        0x108e0000108f3,\n        0x108f4000108f6,\n        0x1090000010916,\n        0x109200001093a,\n        0x10980000109b8,\n        0x109be000109c0,\n        0x10a0000010a04,\n        0x10a0500010a07,\n        0x10a0c00010a14,\n        0x10a1500010a18,\n        0x10a1900010a36,\n        0x10a3800010a3b,\n        0x10a3f00010a40,\n        0x10a6000010a7d,\n        0x10a8000010a9d,\n        0x10ac000010ac8,\n        0x10ac900010ae7,\n        0x10b0000010b36,\n        0x10b4000010b56,\n        0x10b6000010b73,\n        0x10b8000010b92,\n        0x10c0000010c49,\n        0x10cc000010cf3,\n        0x10d0000010d28,\n        0x10d3000010d3a,\n        0x10e8000010eaa,\n        0x10eab00010ead,\n        0x10eb000010eb2,\n        0x10f0000010f1d,\n        0x10f2700010f28,\n        0x10f3000010f51,\n        0x10fb000010fc5,\n        0x10fe000010ff7,\n        0x1100000011047,\n        0x1106600011070,\n        0x1107f000110bb,\n        0x110d0000110e9,\n        0x110f0000110fa,\n        0x1110000011135,\n        0x1113600011140,\n        0x1114400011148,\n        0x1115000011174,\n        0x1117600011177,\n        0x11180000111c5,\n        0x111c9000111cd,\n        0x111ce000111db,\n        0x111dc000111dd,\n        0x1120000011212,\n        0x1121300011238,\n        0x1123e0001123f,\n        0x1128000011287,\n        0x1128800011289,\n        0x1128a0001128e,\n        0x1128f0001129e,\n        0x1129f000112a9,\n        0x112b0000112eb,\n        0x112f0000112fa,\n        0x1130000011304,\n        0x113050001130d,\n        0x1130f00011311,\n        0x1131300011329,\n        0x1132a00011331,\n        0x1133200011334,\n        0x113350001133a,\n        0x1133b00011345,\n        0x1134700011349,\n        0x1134b0001134e,\n        0x1135000011351,\n        0x1135700011358,\n        0x1135d00011364,\n        0x113660001136d,\n        0x1137000011375,\n        0x114000001144b,\n        0x114500001145a,\n        0x1145e00011462,\n        0x11480000114c6,\n        0x114c7000114c8,\n        0x114d0000114da,\n        0x11580000115b6,\n        0x115b8000115c1,\n        0x115d8000115de,\n        0x1160000011641,\n        0x1164400011645,\n        0x116500001165a,\n        0x11680000116b9,\n        0x116c0000116ca,\n        0x117000001171b,\n        0x1171d0001172c,\n        0x117300001173a,\n        0x118000001183b,\n        0x118c0000118ea,\n        0x118ff00011907,\n        0x119090001190a,\n        0x1190c00011914,\n        0x1191500011917,\n        0x1191800011936,\n        0x1193700011939,\n        0x1193b00011944,\n        0x119500001195a,\n        0x119a0000119a8,\n        0x119aa000119d8,\n        0x119da000119e2,\n        0x119e3000119e5,\n        0x11a0000011a3f,\n        0x11a4700011a48,\n        0x11a5000011a9a,\n        0x11a9d00011a9e,\n        0x11ac000011af9,\n        0x11c0000011c09,\n        0x11c0a00011c37,\n        0x11c3800011c41,\n        0x11c5000011c5a,\n        0x11c7200011c90,\n        0x11c9200011ca8,\n        0x11ca900011cb7,\n        0x11d0000011d07,\n        0x11d0800011d0a,\n        0x11d0b00011d37,\n        0x11d3a00011d3b,\n        0x11d3c00011d3e,\n        0x11d3f00011d48,\n        0x11d5000011d5a,\n        0x11d6000011d66,\n        0x11d6700011d69,\n        0x11d6a00011d8f,\n        0x11d9000011d92,\n        0x11d9300011d99,\n        0x11da000011daa,\n        0x11ee000011ef7,\n        0x11fb000011fb1,\n        0x120000001239a,\n        0x1248000012544,\n        0x130000001342f,\n        0x1440000014647,\n        0x1680000016a39,\n        0x16a4000016a5f,\n        0x16a6000016a6a,\n        0x16ad000016aee,\n        0x16af000016af5,\n        0x16b0000016b37,\n        0x16b4000016b44,\n        0x16b5000016b5a,\n        0x16b6300016b78,\n        0x16b7d00016b90,\n        0x16e6000016e80,\n        0x16f0000016f4b,\n        0x16f4f00016f88,\n        0x16f8f00016fa0,\n        0x16fe000016fe2,\n        0x16fe300016fe5,\n        0x16ff000016ff2,\n        0x17000000187f8,\n        0x1880000018cd6,\n        0x18d0000018d09,\n        0x1b0000001b11f,\n        0x1b1500001b153,\n        0x1b1640001b168,\n        0x1b1700001b2fc,\n        0x1bc000001bc6b,\n        0x1bc700001bc7d,\n        0x1bc800001bc89,\n        0x1bc900001bc9a,\n        0x1bc9d0001bc9f,\n        0x1da000001da37,\n        0x1da3b0001da6d,\n        0x1da750001da76,\n        0x1da840001da85,\n        0x1da9b0001daa0,\n        0x1daa10001dab0,\n        0x1e0000001e007,\n        0x1e0080001e019,\n        0x1e01b0001e022,\n        0x1e0230001e025,\n        0x1e0260001e02b,\n        0x1e1000001e12d,\n        0x1e1300001e13e,\n        0x1e1400001e14a,\n        0x1e14e0001e14f,\n        0x1e2c00001e2fa,\n        0x1e8000001e8c5,\n        0x1e8d00001e8d7,\n        0x1e9220001e94c,\n        0x1e9500001e95a,\n        0x1fbf00001fbfa,\n        0x200000002a6de,\n        0x2a7000002b735,\n        0x2b7400002b81e,\n        0x2b8200002cea2,\n        0x2ceb00002ebe1,\n        0x300000003134b,\n    ),\n    'CONTEXTJ': (\n        0x200c0000200e,\n    ),\n    'CONTEXTO': (\n        0xb7000000b8,\n        0x37500000376,\n        0x5f3000005f5,\n        0x6600000066a,\n        0x6f0000006fa,\n        0x30fb000030fc,\n    ),\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/idna/intranges.py",
    "content": "\"\"\"\nGiven a list of integers, made up of (hopefully) a small number of long runs\nof consecutive integers, compute a representation of the form\n((start1, end1), (start2, end2) ...). Then answer the question \"was x present\nin the original list?\" in time O(log(# runs)).\n\"\"\"\n\nimport bisect\n\ndef intranges_from_list(list_):\n    \"\"\"Represent a list of integers as a sequence of ranges:\n    ((start_0, end_0), (start_1, end_1), ...), such that the original\n    integers are exactly those x such that start_i <= x < end_i for some i.\n\n    Ranges are encoded as single integers (start << 32 | end), not as tuples.\n    \"\"\"\n\n    sorted_list = sorted(list_)\n    ranges = []\n    last_write = -1\n    for i in range(len(sorted_list)):\n        if i+1 < len(sorted_list):\n            if sorted_list[i] == sorted_list[i+1]-1:\n                continue\n        current_range = sorted_list[last_write+1:i+1]\n        ranges.append(_encode_range(current_range[0], current_range[-1] + 1))\n        last_write = i\n\n    return tuple(ranges)\n\ndef _encode_range(start, end):\n    return (start << 32) | end\n\ndef _decode_range(r):\n    return (r >> 32), (r & ((1 << 32) - 1))\n\n\ndef intranges_contain(int_, ranges):\n    \"\"\"Determine if `int_` falls into one of the ranges in `ranges`.\"\"\"\n    tuple_ = _encode_range(int_, 0)\n    pos = bisect.bisect_left(ranges, tuple_)\n    # we could be immediately ahead of a tuple (start, end)\n    # with start < int_ <= end\n    if pos > 0:\n        left, right = _decode_range(ranges[pos-1])\n        if left <= int_ < right:\n            return True\n    # or we could be immediately behind a tuple (int_, end)\n    if pos < len(ranges):\n        left, _ = _decode_range(ranges[pos])\n        if left == int_:\n            return True\n    return False\n"
  },
  {
    "path": "openpype/vendor/python/python_2/idna/package_data.py",
    "content": "__version__ = '2.10'\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/idna/uts46data.py",
    "content": "# This file is automatically generated by tools/idna-data\n# vim: set fileencoding=utf-8 :\n\n\"\"\"IDNA Mapping Table from UTS46.\"\"\"\n\n\n__version__ = \"13.0.0\"\ndef _seg_0():\n    return [\n    (0x0, '3'),\n    (0x1, '3'),\n    (0x2, '3'),\n    (0x3, '3'),\n    (0x4, '3'),\n    (0x5, '3'),\n    (0x6, '3'),\n    (0x7, '3'),\n    (0x8, '3'),\n    (0x9, '3'),\n    (0xA, '3'),\n    (0xB, '3'),\n    (0xC, '3'),\n    (0xD, '3'),\n    (0xE, '3'),\n    (0xF, '3'),\n    (0x10, '3'),\n    (0x11, '3'),\n    (0x12, '3'),\n    (0x13, '3'),\n    (0x14, '3'),\n    (0x15, '3'),\n    (0x16, '3'),\n    (0x17, '3'),\n    (0x18, '3'),\n    (0x19, '3'),\n    (0x1A, '3'),\n    (0x1B, '3'),\n    (0x1C, '3'),\n    (0x1D, '3'),\n    (0x1E, '3'),\n    (0x1F, '3'),\n    (0x20, '3'),\n    (0x21, '3'),\n    (0x22, '3'),\n    (0x23, '3'),\n    (0x24, '3'),\n    (0x25, '3'),\n    (0x26, '3'),\n    (0x27, '3'),\n    (0x28, '3'),\n    (0x29, '3'),\n    (0x2A, '3'),\n    (0x2B, '3'),\n    (0x2C, '3'),\n    (0x2D, 'V'),\n    (0x2E, 'V'),\n    (0x2F, '3'),\n    (0x30, 'V'),\n    (0x31, 'V'),\n    (0x32, 'V'),\n    (0x33, 'V'),\n    (0x34, 'V'),\n    (0x35, 'V'),\n    (0x36, 'V'),\n    (0x37, 'V'),\n    (0x38, 'V'),\n    (0x39, 'V'),\n    (0x3A, '3'),\n    (0x3B, '3'),\n    (0x3C, '3'),\n    (0x3D, '3'),\n    (0x3E, '3'),\n    (0x3F, '3'),\n    (0x40, '3'),\n    (0x41, 'M', u'a'),\n    (0x42, 'M', u'b'),\n    (0x43, 'M', u'c'),\n    (0x44, 'M', u'd'),\n    (0x45, 'M', u'e'),\n    (0x46, 'M', u'f'),\n    (0x47, 'M', u'g'),\n    (0x48, 'M', u'h'),\n    (0x49, 'M', u'i'),\n    (0x4A, 'M', u'j'),\n    (0x4B, 'M', u'k'),\n    (0x4C, 'M', u'l'),\n    (0x4D, 'M', u'm'),\n    (0x4E, 'M', u'n'),\n    (0x4F, 'M', u'o'),\n    (0x50, 'M', u'p'),\n    (0x51, 'M', u'q'),\n    (0x52, 'M', u'r'),\n    (0x53, 'M', u's'),\n    (0x54, 'M', u't'),\n    (0x55, 'M', u'u'),\n    (0x56, 'M', u'v'),\n    (0x57, 'M', u'w'),\n    (0x58, 'M', u'x'),\n    (0x59, 'M', u'y'),\n    (0x5A, 'M', u'z'),\n    (0x5B, '3'),\n    (0x5C, '3'),\n    (0x5D, '3'),\n    (0x5E, '3'),\n    (0x5F, '3'),\n    (0x60, '3'),\n    (0x61, 'V'),\n    (0x62, 'V'),\n    (0x63, 'V'),\n    ]\n\ndef _seg_1():\n    return [\n    (0x64, 'V'),\n    (0x65, 'V'),\n    (0x66, 'V'),\n    (0x67, 'V'),\n    (0x68, 'V'),\n    (0x69, 'V'),\n    (0x6A, 'V'),\n    (0x6B, 'V'),\n    (0x6C, 'V'),\n    (0x6D, 'V'),\n    (0x6E, 'V'),\n    (0x6F, 'V'),\n    (0x70, 'V'),\n    (0x71, 'V'),\n    (0x72, 'V'),\n    (0x73, 'V'),\n    (0x74, 'V'),\n    (0x75, 'V'),\n    (0x76, 'V'),\n    (0x77, 'V'),\n    (0x78, 'V'),\n    (0x79, 'V'),\n    (0x7A, 'V'),\n    (0x7B, '3'),\n    (0x7C, '3'),\n    (0x7D, '3'),\n    (0x7E, '3'),\n    (0x7F, '3'),\n    (0x80, 'X'),\n    (0x81, 'X'),\n    (0x82, 'X'),\n    (0x83, 'X'),\n    (0x84, 'X'),\n    (0x85, 'X'),\n    (0x86, 'X'),\n    (0x87, 'X'),\n    (0x88, 'X'),\n    (0x89, 'X'),\n    (0x8A, 'X'),\n    (0x8B, 'X'),\n    (0x8C, 'X'),\n    (0x8D, 'X'),\n    (0x8E, 'X'),\n    (0x8F, 'X'),\n    (0x90, 'X'),\n    (0x91, 'X'),\n    (0x92, 'X'),\n    (0x93, 'X'),\n    (0x94, 'X'),\n    (0x95, 'X'),\n    (0x96, 'X'),\n    (0x97, 'X'),\n    (0x98, 'X'),\n    (0x99, 'X'),\n    (0x9A, 'X'),\n    (0x9B, 'X'),\n    (0x9C, 'X'),\n    (0x9D, 'X'),\n    (0x9E, 'X'),\n    (0x9F, 'X'),\n    (0xA0, '3', u' '),\n    (0xA1, 'V'),\n    (0xA2, 'V'),\n    (0xA3, 'V'),\n    (0xA4, 'V'),\n    (0xA5, 'V'),\n    (0xA6, 'V'),\n    (0xA7, 'V'),\n    (0xA8, '3', u' ̈'),\n    (0xA9, 'V'),\n    (0xAA, 'M', u'a'),\n    (0xAB, 'V'),\n    (0xAC, 'V'),\n    (0xAD, 'I'),\n    (0xAE, 'V'),\n    (0xAF, '3', u' ̄'),\n    (0xB0, 'V'),\n    (0xB1, 'V'),\n    (0xB2, 'M', u'2'),\n    (0xB3, 'M', u'3'),\n    (0xB4, '3', u' ́'),\n    (0xB5, 'M', u'μ'),\n    (0xB6, 'V'),\n    (0xB7, 'V'),\n    (0xB8, '3', u' ̧'),\n    (0xB9, 'M', u'1'),\n    (0xBA, 'M', u'o'),\n    (0xBB, 'V'),\n    (0xBC, 'M', u'1⁄4'),\n    (0xBD, 'M', u'1⁄2'),\n    (0xBE, 'M', u'3⁄4'),\n    (0xBF, 'V'),\n    (0xC0, 'M', u'à'),\n    (0xC1, 'M', u'á'),\n    (0xC2, 'M', u'â'),\n    (0xC3, 'M', u'ã'),\n    (0xC4, 'M', u'ä'),\n    (0xC5, 'M', u'å'),\n    (0xC6, 'M', u'æ'),\n    (0xC7, 'M', u'ç'),\n    ]\n\ndef _seg_2():\n    return [\n    (0xC8, 'M', u'è'),\n    (0xC9, 'M', u'é'),\n    (0xCA, 'M', u'ê'),\n    (0xCB, 'M', u'ë'),\n    (0xCC, 'M', u'ì'),\n    (0xCD, 'M', u'í'),\n    (0xCE, 'M', u'î'),\n    (0xCF, 'M', u'ï'),\n    (0xD0, 'M', u'ð'),\n    (0xD1, 'M', u'ñ'),\n    (0xD2, 'M', u'ò'),\n    (0xD3, 'M', u'ó'),\n    (0xD4, 'M', u'ô'),\n    (0xD5, 'M', u'õ'),\n    (0xD6, 'M', u'ö'),\n    (0xD7, 'V'),\n    (0xD8, 'M', u'ø'),\n    (0xD9, 'M', u'ù'),\n    (0xDA, 'M', u'ú'),\n    (0xDB, 'M', u'û'),\n    (0xDC, 'M', u'ü'),\n    (0xDD, 'M', u'ý'),\n    (0xDE, 'M', u'þ'),\n    (0xDF, 'D', u'ss'),\n    (0xE0, 'V'),\n    (0xE1, 'V'),\n    (0xE2, 'V'),\n    (0xE3, 'V'),\n    (0xE4, 'V'),\n    (0xE5, 'V'),\n    (0xE6, 'V'),\n    (0xE7, 'V'),\n    (0xE8, 'V'),\n    (0xE9, 'V'),\n    (0xEA, 'V'),\n    (0xEB, 'V'),\n    (0xEC, 'V'),\n    (0xED, 'V'),\n    (0xEE, 'V'),\n    (0xEF, 'V'),\n    (0xF0, 'V'),\n    (0xF1, 'V'),\n    (0xF2, 'V'),\n    (0xF3, 'V'),\n    (0xF4, 'V'),\n    (0xF5, 'V'),\n    (0xF6, 'V'),\n    (0xF7, 'V'),\n    (0xF8, 'V'),\n    (0xF9, 'V'),\n    (0xFA, 'V'),\n    (0xFB, 'V'),\n    (0xFC, 'V'),\n    (0xFD, 'V'),\n    (0xFE, 'V'),\n    (0xFF, 'V'),\n    (0x100, 'M', u'ā'),\n    (0x101, 'V'),\n    (0x102, 'M', u'ă'),\n    (0x103, 'V'),\n    (0x104, 'M', u'ą'),\n    (0x105, 'V'),\n    (0x106, 'M', u'ć'),\n    (0x107, 'V'),\n    (0x108, 'M', u'ĉ'),\n    (0x109, 'V'),\n    (0x10A, 'M', u'ċ'),\n    (0x10B, 'V'),\n    (0x10C, 'M', u'č'),\n    (0x10D, 'V'),\n    (0x10E, 'M', u'ď'),\n    (0x10F, 'V'),\n    (0x110, 'M', u'đ'),\n    (0x111, 'V'),\n    (0x112, 'M', u'ē'),\n    (0x113, 'V'),\n    (0x114, 'M', u'ĕ'),\n    (0x115, 'V'),\n    (0x116, 'M', u'ė'),\n    (0x117, 'V'),\n    (0x118, 'M', u'ę'),\n    (0x119, 'V'),\n    (0x11A, 'M', u'ě'),\n    (0x11B, 'V'),\n    (0x11C, 'M', u'ĝ'),\n    (0x11D, 'V'),\n    (0x11E, 'M', u'ğ'),\n    (0x11F, 'V'),\n    (0x120, 'M', u'ġ'),\n    (0x121, 'V'),\n    (0x122, 'M', u'ģ'),\n    (0x123, 'V'),\n    (0x124, 'M', u'ĥ'),\n    (0x125, 'V'),\n    (0x126, 'M', u'ħ'),\n    (0x127, 'V'),\n    (0x128, 'M', u'ĩ'),\n    (0x129, 'V'),\n    (0x12A, 'M', u'ī'),\n    (0x12B, 'V'),\n    ]\n\ndef _seg_3():\n    return [\n    (0x12C, 'M', u'ĭ'),\n    (0x12D, 'V'),\n    (0x12E, 'M', u'į'),\n    (0x12F, 'V'),\n    (0x130, 'M', u'i̇'),\n    (0x131, 'V'),\n    (0x132, 'M', u'ij'),\n    (0x134, 'M', u'ĵ'),\n    (0x135, 'V'),\n    (0x136, 'M', u'ķ'),\n    (0x137, 'V'),\n    (0x139, 'M', u'ĺ'),\n    (0x13A, 'V'),\n    (0x13B, 'M', u'ļ'),\n    (0x13C, 'V'),\n    (0x13D, 'M', u'ľ'),\n    (0x13E, 'V'),\n    (0x13F, 'M', u'l·'),\n    (0x141, 'M', u'ł'),\n    (0x142, 'V'),\n    (0x143, 'M', u'ń'),\n    (0x144, 'V'),\n    (0x145, 'M', u'ņ'),\n    (0x146, 'V'),\n    (0x147, 'M', u'ň'),\n    (0x148, 'V'),\n    (0x149, 'M', u'ʼn'),\n    (0x14A, 'M', u'ŋ'),\n    (0x14B, 'V'),\n    (0x14C, 'M', u'ō'),\n    (0x14D, 'V'),\n    (0x14E, 'M', u'ŏ'),\n    (0x14F, 'V'),\n    (0x150, 'M', u'ő'),\n    (0x151, 'V'),\n    (0x152, 'M', u'œ'),\n    (0x153, 'V'),\n    (0x154, 'M', u'ŕ'),\n    (0x155, 'V'),\n    (0x156, 'M', u'ŗ'),\n    (0x157, 'V'),\n    (0x158, 'M', u'ř'),\n    (0x159, 'V'),\n    (0x15A, 'M', u'ś'),\n    (0x15B, 'V'),\n    (0x15C, 'M', u'ŝ'),\n    (0x15D, 'V'),\n    (0x15E, 'M', u'ş'),\n    (0x15F, 'V'),\n    (0x160, 'M', u'š'),\n    (0x161, 'V'),\n    (0x162, 'M', u'ţ'),\n    (0x163, 'V'),\n    (0x164, 'M', u'ť'),\n    (0x165, 'V'),\n    (0x166, 'M', u'ŧ'),\n    (0x167, 'V'),\n    (0x168, 'M', u'ũ'),\n    (0x169, 'V'),\n    (0x16A, 'M', u'ū'),\n    (0x16B, 'V'),\n    (0x16C, 'M', u'ŭ'),\n    (0x16D, 'V'),\n    (0x16E, 'M', u'ů'),\n    (0x16F, 'V'),\n    (0x170, 'M', u'ű'),\n    (0x171, 'V'),\n    (0x172, 'M', u'ų'),\n    (0x173, 'V'),\n    (0x174, 'M', u'ŵ'),\n    (0x175, 'V'),\n    (0x176, 'M', u'ŷ'),\n    (0x177, 'V'),\n    (0x178, 'M', u'ÿ'),\n    (0x179, 'M', u'ź'),\n    (0x17A, 'V'),\n    (0x17B, 'M', u'ż'),\n    (0x17C, 'V'),\n    (0x17D, 'M', u'ž'),\n    (0x17E, 'V'),\n    (0x17F, 'M', u's'),\n    (0x180, 'V'),\n    (0x181, 'M', u'ɓ'),\n    (0x182, 'M', u'ƃ'),\n    (0x183, 'V'),\n    (0x184, 'M', u'ƅ'),\n    (0x185, 'V'),\n    (0x186, 'M', u'ɔ'),\n    (0x187, 'M', u'ƈ'),\n    (0x188, 'V'),\n    (0x189, 'M', u'ɖ'),\n    (0x18A, 'M', u'ɗ'),\n    (0x18B, 'M', u'ƌ'),\n    (0x18C, 'V'),\n    (0x18E, 'M', u'ǝ'),\n    (0x18F, 'M', u'ə'),\n    (0x190, 'M', u'ɛ'),\n    (0x191, 'M', u'ƒ'),\n    (0x192, 'V'),\n    (0x193, 'M', u'ɠ'),\n    ]\n\ndef _seg_4():\n    return [\n    (0x194, 'M', u'ɣ'),\n    (0x195, 'V'),\n    (0x196, 'M', u'ɩ'),\n    (0x197, 'M', u'ɨ'),\n    (0x198, 'M', u'ƙ'),\n    (0x199, 'V'),\n    (0x19C, 'M', u'ɯ'),\n    (0x19D, 'M', u'ɲ'),\n    (0x19E, 'V'),\n    (0x19F, 'M', u'ɵ'),\n    (0x1A0, 'M', u'ơ'),\n    (0x1A1, 'V'),\n    (0x1A2, 'M', u'ƣ'),\n    (0x1A3, 'V'),\n    (0x1A4, 'M', u'ƥ'),\n    (0x1A5, 'V'),\n    (0x1A6, 'M', u'ʀ'),\n    (0x1A7, 'M', u'ƨ'),\n    (0x1A8, 'V'),\n    (0x1A9, 'M', u'ʃ'),\n    (0x1AA, 'V'),\n    (0x1AC, 'M', u'ƭ'),\n    (0x1AD, 'V'),\n    (0x1AE, 'M', u'ʈ'),\n    (0x1AF, 'M', u'ư'),\n    (0x1B0, 'V'),\n    (0x1B1, 'M', u'ʊ'),\n    (0x1B2, 'M', u'ʋ'),\n    (0x1B3, 'M', u'ƴ'),\n    (0x1B4, 'V'),\n    (0x1B5, 'M', u'ƶ'),\n    (0x1B6, 'V'),\n    (0x1B7, 'M', u'ʒ'),\n    (0x1B8, 'M', u'ƹ'),\n    (0x1B9, 'V'),\n    (0x1BC, 'M', u'ƽ'),\n    (0x1BD, 'V'),\n    (0x1C4, 'M', u'dž'),\n    (0x1C7, 'M', u'lj'),\n    (0x1CA, 'M', u'nj'),\n    (0x1CD, 'M', u'ǎ'),\n    (0x1CE, 'V'),\n    (0x1CF, 'M', u'ǐ'),\n    (0x1D0, 'V'),\n    (0x1D1, 'M', u'ǒ'),\n    (0x1D2, 'V'),\n    (0x1D3, 'M', u'ǔ'),\n    (0x1D4, 'V'),\n    (0x1D5, 'M', u'ǖ'),\n    (0x1D6, 'V'),\n    (0x1D7, 'M', u'ǘ'),\n    (0x1D8, 'V'),\n    (0x1D9, 'M', u'ǚ'),\n    (0x1DA, 'V'),\n    (0x1DB, 'M', u'ǜ'),\n    (0x1DC, 'V'),\n    (0x1DE, 'M', u'ǟ'),\n    (0x1DF, 'V'),\n    (0x1E0, 'M', u'ǡ'),\n    (0x1E1, 'V'),\n    (0x1E2, 'M', u'ǣ'),\n    (0x1E3, 'V'),\n    (0x1E4, 'M', u'ǥ'),\n    (0x1E5, 'V'),\n    (0x1E6, 'M', u'ǧ'),\n    (0x1E7, 'V'),\n    (0x1E8, 'M', u'ǩ'),\n    (0x1E9, 'V'),\n    (0x1EA, 'M', u'ǫ'),\n    (0x1EB, 'V'),\n    (0x1EC, 'M', u'ǭ'),\n    (0x1ED, 'V'),\n    (0x1EE, 'M', u'ǯ'),\n    (0x1EF, 'V'),\n    (0x1F1, 'M', u'dz'),\n    (0x1F4, 'M', u'ǵ'),\n    (0x1F5, 'V'),\n    (0x1F6, 'M', u'ƕ'),\n    (0x1F7, 'M', u'ƿ'),\n    (0x1F8, 'M', u'ǹ'),\n    (0x1F9, 'V'),\n    (0x1FA, 'M', u'ǻ'),\n    (0x1FB, 'V'),\n    (0x1FC, 'M', u'ǽ'),\n    (0x1FD, 'V'),\n    (0x1FE, 'M', u'ǿ'),\n    (0x1FF, 'V'),\n    (0x200, 'M', u'ȁ'),\n    (0x201, 'V'),\n    (0x202, 'M', u'ȃ'),\n    (0x203, 'V'),\n    (0x204, 'M', u'ȅ'),\n    (0x205, 'V'),\n    (0x206, 'M', u'ȇ'),\n    (0x207, 'V'),\n    (0x208, 'M', u'ȉ'),\n    (0x209, 'V'),\n    (0x20A, 'M', u'ȋ'),\n    (0x20B, 'V'),\n    (0x20C, 'M', u'ȍ'),\n    ]\n\ndef _seg_5():\n    return [\n    (0x20D, 'V'),\n    (0x20E, 'M', u'ȏ'),\n    (0x20F, 'V'),\n    (0x210, 'M', u'ȑ'),\n    (0x211, 'V'),\n    (0x212, 'M', u'ȓ'),\n    (0x213, 'V'),\n    (0x214, 'M', u'ȕ'),\n    (0x215, 'V'),\n    (0x216, 'M', u'ȗ'),\n    (0x217, 'V'),\n    (0x218, 'M', u'ș'),\n    (0x219, 'V'),\n    (0x21A, 'M', u'ț'),\n    (0x21B, 'V'),\n    (0x21C, 'M', u'ȝ'),\n    (0x21D, 'V'),\n    (0x21E, 'M', u'ȟ'),\n    (0x21F, 'V'),\n    (0x220, 'M', u'ƞ'),\n    (0x221, 'V'),\n    (0x222, 'M', u'ȣ'),\n    (0x223, 'V'),\n    (0x224, 'M', u'ȥ'),\n    (0x225, 'V'),\n    (0x226, 'M', u'ȧ'),\n    (0x227, 'V'),\n    (0x228, 'M', u'ȩ'),\n    (0x229, 'V'),\n    (0x22A, 'M', u'ȫ'),\n    (0x22B, 'V'),\n    (0x22C, 'M', u'ȭ'),\n    (0x22D, 'V'),\n    (0x22E, 'M', u'ȯ'),\n    (0x22F, 'V'),\n    (0x230, 'M', u'ȱ'),\n    (0x231, 'V'),\n    (0x232, 'M', u'ȳ'),\n    (0x233, 'V'),\n    (0x23A, 'M', u'ⱥ'),\n    (0x23B, 'M', u'ȼ'),\n    (0x23C, 'V'),\n    (0x23D, 'M', u'ƚ'),\n    (0x23E, 'M', u'ⱦ'),\n    (0x23F, 'V'),\n    (0x241, 'M', u'ɂ'),\n    (0x242, 'V'),\n    (0x243, 'M', u'ƀ'),\n    (0x244, 'M', u'ʉ'),\n    (0x245, 'M', u'ʌ'),\n    (0x246, 'M', u'ɇ'),\n    (0x247, 'V'),\n    (0x248, 'M', u'ɉ'),\n    (0x249, 'V'),\n    (0x24A, 'M', u'ɋ'),\n    (0x24B, 'V'),\n    (0x24C, 'M', u'ɍ'),\n    (0x24D, 'V'),\n    (0x24E, 'M', u'ɏ'),\n    (0x24F, 'V'),\n    (0x2B0, 'M', u'h'),\n    (0x2B1, 'M', u'ɦ'),\n    (0x2B2, 'M', u'j'),\n    (0x2B3, 'M', u'r'),\n    (0x2B4, 'M', u'ɹ'),\n    (0x2B5, 'M', u'ɻ'),\n    (0x2B6, 'M', u'ʁ'),\n    (0x2B7, 'M', u'w'),\n    (0x2B8, 'M', u'y'),\n    (0x2B9, 'V'),\n    (0x2D8, '3', u' ̆'),\n    (0x2D9, '3', u' ̇'),\n    (0x2DA, '3', u' ̊'),\n    (0x2DB, '3', u' ̨'),\n    (0x2DC, '3', u' ̃'),\n    (0x2DD, '3', u' ̋'),\n    (0x2DE, 'V'),\n    (0x2E0, 'M', u'ɣ'),\n    (0x2E1, 'M', u'l'),\n    (0x2E2, 'M', u's'),\n    (0x2E3, 'M', u'x'),\n    (0x2E4, 'M', u'ʕ'),\n    (0x2E5, 'V'),\n    (0x340, 'M', u'̀'),\n    (0x341, 'M', u'́'),\n    (0x342, 'V'),\n    (0x343, 'M', u'̓'),\n    (0x344, 'M', u'̈́'),\n    (0x345, 'M', u'ι'),\n    (0x346, 'V'),\n    (0x34F, 'I'),\n    (0x350, 'V'),\n    (0x370, 'M', u'ͱ'),\n    (0x371, 'V'),\n    (0x372, 'M', u'ͳ'),\n    (0x373, 'V'),\n    (0x374, 'M', u'ʹ'),\n    (0x375, 'V'),\n    (0x376, 'M', u'ͷ'),\n    (0x377, 'V'),\n    ]\n\ndef _seg_6():\n    return [\n    (0x378, 'X'),\n    (0x37A, '3', u' ι'),\n    (0x37B, 'V'),\n    (0x37E, '3', u';'),\n    (0x37F, 'M', u'ϳ'),\n    (0x380, 'X'),\n    (0x384, '3', u' ́'),\n    (0x385, '3', u' ̈́'),\n    (0x386, 'M', u'ά'),\n    (0x387, 'M', u'·'),\n    (0x388, 'M', u'έ'),\n    (0x389, 'M', u'ή'),\n    (0x38A, 'M', u'ί'),\n    (0x38B, 'X'),\n    (0x38C, 'M', u'ό'),\n    (0x38D, 'X'),\n    (0x38E, 'M', u'ύ'),\n    (0x38F, 'M', u'ώ'),\n    (0x390, 'V'),\n    (0x391, 'M', u'α'),\n    (0x392, 'M', u'β'),\n    (0x393, 'M', u'γ'),\n    (0x394, 'M', u'δ'),\n    (0x395, 'M', u'ε'),\n    (0x396, 'M', u'ζ'),\n    (0x397, 'M', u'η'),\n    (0x398, 'M', u'θ'),\n    (0x399, 'M', u'ι'),\n    (0x39A, 'M', u'κ'),\n    (0x39B, 'M', u'λ'),\n    (0x39C, 'M', u'μ'),\n    (0x39D, 'M', u'ν'),\n    (0x39E, 'M', u'ξ'),\n    (0x39F, 'M', u'ο'),\n    (0x3A0, 'M', u'π'),\n    (0x3A1, 'M', u'ρ'),\n    (0x3A2, 'X'),\n    (0x3A3, 'M', u'σ'),\n    (0x3A4, 'M', u'τ'),\n    (0x3A5, 'M', u'υ'),\n    (0x3A6, 'M', u'φ'),\n    (0x3A7, 'M', u'χ'),\n    (0x3A8, 'M', u'ψ'),\n    (0x3A9, 'M', u'ω'),\n    (0x3AA, 'M', u'ϊ'),\n    (0x3AB, 'M', u'ϋ'),\n    (0x3AC, 'V'),\n    (0x3C2, 'D', u'σ'),\n    (0x3C3, 'V'),\n    (0x3CF, 'M', u'ϗ'),\n    (0x3D0, 'M', u'β'),\n    (0x3D1, 'M', u'θ'),\n    (0x3D2, 'M', u'υ'),\n    (0x3D3, 'M', u'ύ'),\n    (0x3D4, 'M', u'ϋ'),\n    (0x3D5, 'M', u'φ'),\n    (0x3D6, 'M', u'π'),\n    (0x3D7, 'V'),\n    (0x3D8, 'M', u'ϙ'),\n    (0x3D9, 'V'),\n    (0x3DA, 'M', u'ϛ'),\n    (0x3DB, 'V'),\n    (0x3DC, 'M', u'ϝ'),\n    (0x3DD, 'V'),\n    (0x3DE, 'M', u'ϟ'),\n    (0x3DF, 'V'),\n    (0x3E0, 'M', u'ϡ'),\n    (0x3E1, 'V'),\n    (0x3E2, 'M', u'ϣ'),\n    (0x3E3, 'V'),\n    (0x3E4, 'M', u'ϥ'),\n    (0x3E5, 'V'),\n    (0x3E6, 'M', u'ϧ'),\n    (0x3E7, 'V'),\n    (0x3E8, 'M', u'ϩ'),\n    (0x3E9, 'V'),\n    (0x3EA, 'M', u'ϫ'),\n    (0x3EB, 'V'),\n    (0x3EC, 'M', u'ϭ'),\n    (0x3ED, 'V'),\n    (0x3EE, 'M', u'ϯ'),\n    (0x3EF, 'V'),\n    (0x3F0, 'M', u'κ'),\n    (0x3F1, 'M', u'ρ'),\n    (0x3F2, 'M', u'σ'),\n    (0x3F3, 'V'),\n    (0x3F4, 'M', u'θ'),\n    (0x3F5, 'M', u'ε'),\n    (0x3F6, 'V'),\n    (0x3F7, 'M', u'ϸ'),\n    (0x3F8, 'V'),\n    (0x3F9, 'M', u'σ'),\n    (0x3FA, 'M', u'ϻ'),\n    (0x3FB, 'V'),\n    (0x3FD, 'M', u'ͻ'),\n    (0x3FE, 'M', u'ͼ'),\n    (0x3FF, 'M', u'ͽ'),\n    (0x400, 'M', u'ѐ'),\n    (0x401, 'M', u'ё'),\n    (0x402, 'M', u'ђ'),\n    ]\n\ndef _seg_7():\n    return [\n    (0x403, 'M', u'ѓ'),\n    (0x404, 'M', u'є'),\n    (0x405, 'M', u'ѕ'),\n    (0x406, 'M', u'і'),\n    (0x407, 'M', u'ї'),\n    (0x408, 'M', u'ј'),\n    (0x409, 'M', u'љ'),\n    (0x40A, 'M', u'њ'),\n    (0x40B, 'M', u'ћ'),\n    (0x40C, 'M', u'ќ'),\n    (0x40D, 'M', u'ѝ'),\n    (0x40E, 'M', u'ў'),\n    (0x40F, 'M', u'џ'),\n    (0x410, 'M', u'а'),\n    (0x411, 'M', u'б'),\n    (0x412, 'M', u'в'),\n    (0x413, 'M', u'г'),\n    (0x414, 'M', u'д'),\n    (0x415, 'M', u'е'),\n    (0x416, 'M', u'ж'),\n    (0x417, 'M', u'з'),\n    (0x418, 'M', u'и'),\n    (0x419, 'M', u'й'),\n    (0x41A, 'M', u'к'),\n    (0x41B, 'M', u'л'),\n    (0x41C, 'M', u'м'),\n    (0x41D, 'M', u'н'),\n    (0x41E, 'M', u'о'),\n    (0x41F, 'M', u'п'),\n    (0x420, 'M', u'р'),\n    (0x421, 'M', u'с'),\n    (0x422, 'M', u'т'),\n    (0x423, 'M', u'у'),\n    (0x424, 'M', u'ф'),\n    (0x425, 'M', u'х'),\n    (0x426, 'M', u'ц'),\n    (0x427, 'M', u'ч'),\n    (0x428, 'M', u'ш'),\n    (0x429, 'M', u'щ'),\n    (0x42A, 'M', u'ъ'),\n    (0x42B, 'M', u'ы'),\n    (0x42C, 'M', u'ь'),\n    (0x42D, 'M', u'э'),\n    (0x42E, 'M', u'ю'),\n    (0x42F, 'M', u'я'),\n    (0x430, 'V'),\n    (0x460, 'M', u'ѡ'),\n    (0x461, 'V'),\n    (0x462, 'M', u'ѣ'),\n    (0x463, 'V'),\n    (0x464, 'M', u'ѥ'),\n    (0x465, 'V'),\n    (0x466, 'M', u'ѧ'),\n    (0x467, 'V'),\n    (0x468, 'M', u'ѩ'),\n    (0x469, 'V'),\n    (0x46A, 'M', u'ѫ'),\n    (0x46B, 'V'),\n    (0x46C, 'M', u'ѭ'),\n    (0x46D, 'V'),\n    (0x46E, 'M', u'ѯ'),\n    (0x46F, 'V'),\n    (0x470, 'M', u'ѱ'),\n    (0x471, 'V'),\n    (0x472, 'M', u'ѳ'),\n    (0x473, 'V'),\n    (0x474, 'M', u'ѵ'),\n    (0x475, 'V'),\n    (0x476, 'M', u'ѷ'),\n    (0x477, 'V'),\n    (0x478, 'M', u'ѹ'),\n    (0x479, 'V'),\n    (0x47A, 'M', u'ѻ'),\n    (0x47B, 'V'),\n    (0x47C, 'M', u'ѽ'),\n    (0x47D, 'V'),\n    (0x47E, 'M', u'ѿ'),\n    (0x47F, 'V'),\n    (0x480, 'M', u'ҁ'),\n    (0x481, 'V'),\n    (0x48A, 'M', u'ҋ'),\n    (0x48B, 'V'),\n    (0x48C, 'M', u'ҍ'),\n    (0x48D, 'V'),\n    (0x48E, 'M', u'ҏ'),\n    (0x48F, 'V'),\n    (0x490, 'M', u'ґ'),\n    (0x491, 'V'),\n    (0x492, 'M', u'ғ'),\n    (0x493, 'V'),\n    (0x494, 'M', u'ҕ'),\n    (0x495, 'V'),\n    (0x496, 'M', u'җ'),\n    (0x497, 'V'),\n    (0x498, 'M', u'ҙ'),\n    (0x499, 'V'),\n    (0x49A, 'M', u'қ'),\n    (0x49B, 'V'),\n    (0x49C, 'M', u'ҝ'),\n    (0x49D, 'V'),\n    ]\n\ndef _seg_8():\n    return [\n    (0x49E, 'M', u'ҟ'),\n    (0x49F, 'V'),\n    (0x4A0, 'M', u'ҡ'),\n    (0x4A1, 'V'),\n    (0x4A2, 'M', u'ң'),\n    (0x4A3, 'V'),\n    (0x4A4, 'M', u'ҥ'),\n    (0x4A5, 'V'),\n    (0x4A6, 'M', u'ҧ'),\n    (0x4A7, 'V'),\n    (0x4A8, 'M', u'ҩ'),\n    (0x4A9, 'V'),\n    (0x4AA, 'M', u'ҫ'),\n    (0x4AB, 'V'),\n    (0x4AC, 'M', u'ҭ'),\n    (0x4AD, 'V'),\n    (0x4AE, 'M', u'ү'),\n    (0x4AF, 'V'),\n    (0x4B0, 'M', u'ұ'),\n    (0x4B1, 'V'),\n    (0x4B2, 'M', u'ҳ'),\n    (0x4B3, 'V'),\n    (0x4B4, 'M', u'ҵ'),\n    (0x4B5, 'V'),\n    (0x4B6, 'M', u'ҷ'),\n    (0x4B7, 'V'),\n    (0x4B8, 'M', u'ҹ'),\n    (0x4B9, 'V'),\n    (0x4BA, 'M', u'һ'),\n    (0x4BB, 'V'),\n    (0x4BC, 'M', u'ҽ'),\n    (0x4BD, 'V'),\n    (0x4BE, 'M', u'ҿ'),\n    (0x4BF, 'V'),\n    (0x4C0, 'X'),\n    (0x4C1, 'M', u'ӂ'),\n    (0x4C2, 'V'),\n    (0x4C3, 'M', u'ӄ'),\n    (0x4C4, 'V'),\n    (0x4C5, 'M', u'ӆ'),\n    (0x4C6, 'V'),\n    (0x4C7, 'M', u'ӈ'),\n    (0x4C8, 'V'),\n    (0x4C9, 'M', u'ӊ'),\n    (0x4CA, 'V'),\n    (0x4CB, 'M', u'ӌ'),\n    (0x4CC, 'V'),\n    (0x4CD, 'M', u'ӎ'),\n    (0x4CE, 'V'),\n    (0x4D0, 'M', u'ӑ'),\n    (0x4D1, 'V'),\n    (0x4D2, 'M', u'ӓ'),\n    (0x4D3, 'V'),\n    (0x4D4, 'M', u'ӕ'),\n    (0x4D5, 'V'),\n    (0x4D6, 'M', u'ӗ'),\n    (0x4D7, 'V'),\n    (0x4D8, 'M', u'ә'),\n    (0x4D9, 'V'),\n    (0x4DA, 'M', u'ӛ'),\n    (0x4DB, 'V'),\n    (0x4DC, 'M', u'ӝ'),\n    (0x4DD, 'V'),\n    (0x4DE, 'M', u'ӟ'),\n    (0x4DF, 'V'),\n    (0x4E0, 'M', u'ӡ'),\n    (0x4E1, 'V'),\n    (0x4E2, 'M', u'ӣ'),\n    (0x4E3, 'V'),\n    (0x4E4, 'M', u'ӥ'),\n    (0x4E5, 'V'),\n    (0x4E6, 'M', u'ӧ'),\n    (0x4E7, 'V'),\n    (0x4E8, 'M', u'ө'),\n    (0x4E9, 'V'),\n    (0x4EA, 'M', u'ӫ'),\n    (0x4EB, 'V'),\n    (0x4EC, 'M', u'ӭ'),\n    (0x4ED, 'V'),\n    (0x4EE, 'M', u'ӯ'),\n    (0x4EF, 'V'),\n    (0x4F0, 'M', u'ӱ'),\n    (0x4F1, 'V'),\n    (0x4F2, 'M', u'ӳ'),\n    (0x4F3, 'V'),\n    (0x4F4, 'M', u'ӵ'),\n    (0x4F5, 'V'),\n    (0x4F6, 'M', u'ӷ'),\n    (0x4F7, 'V'),\n    (0x4F8, 'M', u'ӹ'),\n    (0x4F9, 'V'),\n    (0x4FA, 'M', u'ӻ'),\n    (0x4FB, 'V'),\n    (0x4FC, 'M', u'ӽ'),\n    (0x4FD, 'V'),\n    (0x4FE, 'M', u'ӿ'),\n    (0x4FF, 'V'),\n    (0x500, 'M', u'ԁ'),\n    (0x501, 'V'),\n    (0x502, 'M', u'ԃ'),\n    ]\n\ndef _seg_9():\n    return [\n    (0x503, 'V'),\n    (0x504, 'M', u'ԅ'),\n    (0x505, 'V'),\n    (0x506, 'M', u'ԇ'),\n    (0x507, 'V'),\n    (0x508, 'M', u'ԉ'),\n    (0x509, 'V'),\n    (0x50A, 'M', u'ԋ'),\n    (0x50B, 'V'),\n    (0x50C, 'M', u'ԍ'),\n    (0x50D, 'V'),\n    (0x50E, 'M', u'ԏ'),\n    (0x50F, 'V'),\n    (0x510, 'M', u'ԑ'),\n    (0x511, 'V'),\n    (0x512, 'M', u'ԓ'),\n    (0x513, 'V'),\n    (0x514, 'M', u'ԕ'),\n    (0x515, 'V'),\n    (0x516, 'M', u'ԗ'),\n    (0x517, 'V'),\n    (0x518, 'M', u'ԙ'),\n    (0x519, 'V'),\n    (0x51A, 'M', u'ԛ'),\n    (0x51B, 'V'),\n    (0x51C, 'M', u'ԝ'),\n    (0x51D, 'V'),\n    (0x51E, 'M', u'ԟ'),\n    (0x51F, 'V'),\n    (0x520, 'M', u'ԡ'),\n    (0x521, 'V'),\n    (0x522, 'M', u'ԣ'),\n    (0x523, 'V'),\n    (0x524, 'M', u'ԥ'),\n    (0x525, 'V'),\n    (0x526, 'M', u'ԧ'),\n    (0x527, 'V'),\n    (0x528, 'M', u'ԩ'),\n    (0x529, 'V'),\n    (0x52A, 'M', u'ԫ'),\n    (0x52B, 'V'),\n    (0x52C, 'M', u'ԭ'),\n    (0x52D, 'V'),\n    (0x52E, 'M', u'ԯ'),\n    (0x52F, 'V'),\n    (0x530, 'X'),\n    (0x531, 'M', u'ա'),\n    (0x532, 'M', u'բ'),\n    (0x533, 'M', u'գ'),\n    (0x534, 'M', u'դ'),\n    (0x535, 'M', u'ե'),\n    (0x536, 'M', u'զ'),\n    (0x537, 'M', u'է'),\n    (0x538, 'M', u'ը'),\n    (0x539, 'M', u'թ'),\n    (0x53A, 'M', u'ժ'),\n    (0x53B, 'M', u'ի'),\n    (0x53C, 'M', u'լ'),\n    (0x53D, 'M', u'խ'),\n    (0x53E, 'M', u'ծ'),\n    (0x53F, 'M', u'կ'),\n    (0x540, 'M', u'հ'),\n    (0x541, 'M', u'ձ'),\n    (0x542, 'M', u'ղ'),\n    (0x543, 'M', u'ճ'),\n    (0x544, 'M', u'մ'),\n    (0x545, 'M', u'յ'),\n    (0x546, 'M', u'ն'),\n    (0x547, 'M', u'շ'),\n    (0x548, 'M', u'ո'),\n    (0x549, 'M', u'չ'),\n    (0x54A, 'M', u'պ'),\n    (0x54B, 'M', u'ջ'),\n    (0x54C, 'M', u'ռ'),\n    (0x54D, 'M', u'ս'),\n    (0x54E, 'M', u'վ'),\n    (0x54F, 'M', u'տ'),\n    (0x550, 'M', u'ր'),\n    (0x551, 'M', u'ց'),\n    (0x552, 'M', u'ւ'),\n    (0x553, 'M', u'փ'),\n    (0x554, 'M', u'ք'),\n    (0x555, 'M', u'օ'),\n    (0x556, 'M', u'ֆ'),\n    (0x557, 'X'),\n    (0x559, 'V'),\n    (0x587, 'M', u'եւ'),\n    (0x588, 'V'),\n    (0x58B, 'X'),\n    (0x58D, 'V'),\n    (0x590, 'X'),\n    (0x591, 'V'),\n    (0x5C8, 'X'),\n    (0x5D0, 'V'),\n    (0x5EB, 'X'),\n    (0x5EF, 'V'),\n    (0x5F5, 'X'),\n    (0x606, 'V'),\n    (0x61C, 'X'),\n    (0x61E, 'V'),\n    ]\n\ndef _seg_10():\n    return [\n    (0x675, 'M', u'اٴ'),\n    (0x676, 'M', u'وٴ'),\n    (0x677, 'M', u'ۇٴ'),\n    (0x678, 'M', u'يٴ'),\n    (0x679, 'V'),\n    (0x6DD, 'X'),\n    (0x6DE, 'V'),\n    (0x70E, 'X'),\n    (0x710, 'V'),\n    (0x74B, 'X'),\n    (0x74D, 'V'),\n    (0x7B2, 'X'),\n    (0x7C0, 'V'),\n    (0x7FB, 'X'),\n    (0x7FD, 'V'),\n    (0x82E, 'X'),\n    (0x830, 'V'),\n    (0x83F, 'X'),\n    (0x840, 'V'),\n    (0x85C, 'X'),\n    (0x85E, 'V'),\n    (0x85F, 'X'),\n    (0x860, 'V'),\n    (0x86B, 'X'),\n    (0x8A0, 'V'),\n    (0x8B5, 'X'),\n    (0x8B6, 'V'),\n    (0x8C8, 'X'),\n    (0x8D3, 'V'),\n    (0x8E2, 'X'),\n    (0x8E3, 'V'),\n    (0x958, 'M', u'क़'),\n    (0x959, 'M', u'ख़'),\n    (0x95A, 'M', u'ग़'),\n    (0x95B, 'M', u'ज़'),\n    (0x95C, 'M', u'ड़'),\n    (0x95D, 'M', u'ढ़'),\n    (0x95E, 'M', u'फ़'),\n    (0x95F, 'M', u'य़'),\n    (0x960, 'V'),\n    (0x984, 'X'),\n    (0x985, 'V'),\n    (0x98D, 'X'),\n    (0x98F, 'V'),\n    (0x991, 'X'),\n    (0x993, 'V'),\n    (0x9A9, 'X'),\n    (0x9AA, 'V'),\n    (0x9B1, 'X'),\n    (0x9B2, 'V'),\n    (0x9B3, 'X'),\n    (0x9B6, 'V'),\n    (0x9BA, 'X'),\n    (0x9BC, 'V'),\n    (0x9C5, 'X'),\n    (0x9C7, 'V'),\n    (0x9C9, 'X'),\n    (0x9CB, 'V'),\n    (0x9CF, 'X'),\n    (0x9D7, 'V'),\n    (0x9D8, 'X'),\n    (0x9DC, 'M', u'ড়'),\n    (0x9DD, 'M', u'ঢ়'),\n    (0x9DE, 'X'),\n    (0x9DF, 'M', u'য়'),\n    (0x9E0, 'V'),\n    (0x9E4, 'X'),\n    (0x9E6, 'V'),\n    (0x9FF, 'X'),\n    (0xA01, 'V'),\n    (0xA04, 'X'),\n    (0xA05, 'V'),\n    (0xA0B, 'X'),\n    (0xA0F, 'V'),\n    (0xA11, 'X'),\n    (0xA13, 'V'),\n    (0xA29, 'X'),\n    (0xA2A, 'V'),\n    (0xA31, 'X'),\n    (0xA32, 'V'),\n    (0xA33, 'M', u'ਲ਼'),\n    (0xA34, 'X'),\n    (0xA35, 'V'),\n    (0xA36, 'M', u'ਸ਼'),\n    (0xA37, 'X'),\n    (0xA38, 'V'),\n    (0xA3A, 'X'),\n    (0xA3C, 'V'),\n    (0xA3D, 'X'),\n    (0xA3E, 'V'),\n    (0xA43, 'X'),\n    (0xA47, 'V'),\n    (0xA49, 'X'),\n    (0xA4B, 'V'),\n    (0xA4E, 'X'),\n    (0xA51, 'V'),\n    (0xA52, 'X'),\n    (0xA59, 'M', u'ਖ਼'),\n    (0xA5A, 'M', u'ਗ਼'),\n    (0xA5B, 'M', u'ਜ਼'),\n    ]\n\ndef _seg_11():\n    return [\n    (0xA5C, 'V'),\n    (0xA5D, 'X'),\n    (0xA5E, 'M', u'ਫ਼'),\n    (0xA5F, 'X'),\n    (0xA66, 'V'),\n    (0xA77, 'X'),\n    (0xA81, 'V'),\n    (0xA84, 'X'),\n    (0xA85, 'V'),\n    (0xA8E, 'X'),\n    (0xA8F, 'V'),\n    (0xA92, 'X'),\n    (0xA93, 'V'),\n    (0xAA9, 'X'),\n    (0xAAA, 'V'),\n    (0xAB1, 'X'),\n    (0xAB2, 'V'),\n    (0xAB4, 'X'),\n    (0xAB5, 'V'),\n    (0xABA, 'X'),\n    (0xABC, 'V'),\n    (0xAC6, 'X'),\n    (0xAC7, 'V'),\n    (0xACA, 'X'),\n    (0xACB, 'V'),\n    (0xACE, 'X'),\n    (0xAD0, 'V'),\n    (0xAD1, 'X'),\n    (0xAE0, 'V'),\n    (0xAE4, 'X'),\n    (0xAE6, 'V'),\n    (0xAF2, 'X'),\n    (0xAF9, 'V'),\n    (0xB00, 'X'),\n    (0xB01, 'V'),\n    (0xB04, 'X'),\n    (0xB05, 'V'),\n    (0xB0D, 'X'),\n    (0xB0F, 'V'),\n    (0xB11, 'X'),\n    (0xB13, 'V'),\n    (0xB29, 'X'),\n    (0xB2A, 'V'),\n    (0xB31, 'X'),\n    (0xB32, 'V'),\n    (0xB34, 'X'),\n    (0xB35, 'V'),\n    (0xB3A, 'X'),\n    (0xB3C, 'V'),\n    (0xB45, 'X'),\n    (0xB47, 'V'),\n    (0xB49, 'X'),\n    (0xB4B, 'V'),\n    (0xB4E, 'X'),\n    (0xB55, 'V'),\n    (0xB58, 'X'),\n    (0xB5C, 'M', u'ଡ଼'),\n    (0xB5D, 'M', u'ଢ଼'),\n    (0xB5E, 'X'),\n    (0xB5F, 'V'),\n    (0xB64, 'X'),\n    (0xB66, 'V'),\n    (0xB78, 'X'),\n    (0xB82, 'V'),\n    (0xB84, 'X'),\n    (0xB85, 'V'),\n    (0xB8B, 'X'),\n    (0xB8E, 'V'),\n    (0xB91, 'X'),\n    (0xB92, 'V'),\n    (0xB96, 'X'),\n    (0xB99, 'V'),\n    (0xB9B, 'X'),\n    (0xB9C, 'V'),\n    (0xB9D, 'X'),\n    (0xB9E, 'V'),\n    (0xBA0, 'X'),\n    (0xBA3, 'V'),\n    (0xBA5, 'X'),\n    (0xBA8, 'V'),\n    (0xBAB, 'X'),\n    (0xBAE, 'V'),\n    (0xBBA, 'X'),\n    (0xBBE, 'V'),\n    (0xBC3, 'X'),\n    (0xBC6, 'V'),\n    (0xBC9, 'X'),\n    (0xBCA, 'V'),\n    (0xBCE, 'X'),\n    (0xBD0, 'V'),\n    (0xBD1, 'X'),\n    (0xBD7, 'V'),\n    (0xBD8, 'X'),\n    (0xBE6, 'V'),\n    (0xBFB, 'X'),\n    (0xC00, 'V'),\n    (0xC0D, 'X'),\n    (0xC0E, 'V'),\n    (0xC11, 'X'),\n    (0xC12, 'V'),\n    ]\n\ndef _seg_12():\n    return [\n    (0xC29, 'X'),\n    (0xC2A, 'V'),\n    (0xC3A, 'X'),\n    (0xC3D, 'V'),\n    (0xC45, 'X'),\n    (0xC46, 'V'),\n    (0xC49, 'X'),\n    (0xC4A, 'V'),\n    (0xC4E, 'X'),\n    (0xC55, 'V'),\n    (0xC57, 'X'),\n    (0xC58, 'V'),\n    (0xC5B, 'X'),\n    (0xC60, 'V'),\n    (0xC64, 'X'),\n    (0xC66, 'V'),\n    (0xC70, 'X'),\n    (0xC77, 'V'),\n    (0xC8D, 'X'),\n    (0xC8E, 'V'),\n    (0xC91, 'X'),\n    (0xC92, 'V'),\n    (0xCA9, 'X'),\n    (0xCAA, 'V'),\n    (0xCB4, 'X'),\n    (0xCB5, 'V'),\n    (0xCBA, 'X'),\n    (0xCBC, 'V'),\n    (0xCC5, 'X'),\n    (0xCC6, 'V'),\n    (0xCC9, 'X'),\n    (0xCCA, 'V'),\n    (0xCCE, 'X'),\n    (0xCD5, 'V'),\n    (0xCD7, 'X'),\n    (0xCDE, 'V'),\n    (0xCDF, 'X'),\n    (0xCE0, 'V'),\n    (0xCE4, 'X'),\n    (0xCE6, 'V'),\n    (0xCF0, 'X'),\n    (0xCF1, 'V'),\n    (0xCF3, 'X'),\n    (0xD00, 'V'),\n    (0xD0D, 'X'),\n    (0xD0E, 'V'),\n    (0xD11, 'X'),\n    (0xD12, 'V'),\n    (0xD45, 'X'),\n    (0xD46, 'V'),\n    (0xD49, 'X'),\n    (0xD4A, 'V'),\n    (0xD50, 'X'),\n    (0xD54, 'V'),\n    (0xD64, 'X'),\n    (0xD66, 'V'),\n    (0xD80, 'X'),\n    (0xD81, 'V'),\n    (0xD84, 'X'),\n    (0xD85, 'V'),\n    (0xD97, 'X'),\n    (0xD9A, 'V'),\n    (0xDB2, 'X'),\n    (0xDB3, 'V'),\n    (0xDBC, 'X'),\n    (0xDBD, 'V'),\n    (0xDBE, 'X'),\n    (0xDC0, 'V'),\n    (0xDC7, 'X'),\n    (0xDCA, 'V'),\n    (0xDCB, 'X'),\n    (0xDCF, 'V'),\n    (0xDD5, 'X'),\n    (0xDD6, 'V'),\n    (0xDD7, 'X'),\n    (0xDD8, 'V'),\n    (0xDE0, 'X'),\n    (0xDE6, 'V'),\n    (0xDF0, 'X'),\n    (0xDF2, 'V'),\n    (0xDF5, 'X'),\n    (0xE01, 'V'),\n    (0xE33, 'M', u'ํา'),\n    (0xE34, 'V'),\n    (0xE3B, 'X'),\n    (0xE3F, 'V'),\n    (0xE5C, 'X'),\n    (0xE81, 'V'),\n    (0xE83, 'X'),\n    (0xE84, 'V'),\n    (0xE85, 'X'),\n    (0xE86, 'V'),\n    (0xE8B, 'X'),\n    (0xE8C, 'V'),\n    (0xEA4, 'X'),\n    (0xEA5, 'V'),\n    (0xEA6, 'X'),\n    (0xEA7, 'V'),\n    (0xEB3, 'M', u'ໍາ'),\n    (0xEB4, 'V'),\n    ]\n\ndef _seg_13():\n    return [\n    (0xEBE, 'X'),\n    (0xEC0, 'V'),\n    (0xEC5, 'X'),\n    (0xEC6, 'V'),\n    (0xEC7, 'X'),\n    (0xEC8, 'V'),\n    (0xECE, 'X'),\n    (0xED0, 'V'),\n    (0xEDA, 'X'),\n    (0xEDC, 'M', u'ຫນ'),\n    (0xEDD, 'M', u'ຫມ'),\n    (0xEDE, 'V'),\n    (0xEE0, 'X'),\n    (0xF00, 'V'),\n    (0xF0C, 'M', u'་'),\n    (0xF0D, 'V'),\n    (0xF43, 'M', u'གྷ'),\n    (0xF44, 'V'),\n    (0xF48, 'X'),\n    (0xF49, 'V'),\n    (0xF4D, 'M', u'ཌྷ'),\n    (0xF4E, 'V'),\n    (0xF52, 'M', u'དྷ'),\n    (0xF53, 'V'),\n    (0xF57, 'M', u'བྷ'),\n    (0xF58, 'V'),\n    (0xF5C, 'M', u'ཛྷ'),\n    (0xF5D, 'V'),\n    (0xF69, 'M', u'ཀྵ'),\n    (0xF6A, 'V'),\n    (0xF6D, 'X'),\n    (0xF71, 'V'),\n    (0xF73, 'M', u'ཱི'),\n    (0xF74, 'V'),\n    (0xF75, 'M', u'ཱུ'),\n    (0xF76, 'M', u'ྲྀ'),\n    (0xF77, 'M', u'ྲཱྀ'),\n    (0xF78, 'M', u'ླྀ'),\n    (0xF79, 'M', u'ླཱྀ'),\n    (0xF7A, 'V'),\n    (0xF81, 'M', u'ཱྀ'),\n    (0xF82, 'V'),\n    (0xF93, 'M', u'ྒྷ'),\n    (0xF94, 'V'),\n    (0xF98, 'X'),\n    (0xF99, 'V'),\n    (0xF9D, 'M', u'ྜྷ'),\n    (0xF9E, 'V'),\n    (0xFA2, 'M', u'ྡྷ'),\n    (0xFA3, 'V'),\n    (0xFA7, 'M', u'ྦྷ'),\n    (0xFA8, 'V'),\n    (0xFAC, 'M', u'ྫྷ'),\n    (0xFAD, 'V'),\n    (0xFB9, 'M', u'ྐྵ'),\n    (0xFBA, 'V'),\n    (0xFBD, 'X'),\n    (0xFBE, 'V'),\n    (0xFCD, 'X'),\n    (0xFCE, 'V'),\n    (0xFDB, 'X'),\n    (0x1000, 'V'),\n    (0x10A0, 'X'),\n    (0x10C7, 'M', u'ⴧ'),\n    (0x10C8, 'X'),\n    (0x10CD, 'M', u'ⴭ'),\n    (0x10CE, 'X'),\n    (0x10D0, 'V'),\n    (0x10FC, 'M', u'ნ'),\n    (0x10FD, 'V'),\n    (0x115F, 'X'),\n    (0x1161, 'V'),\n    (0x1249, 'X'),\n    (0x124A, 'V'),\n    (0x124E, 'X'),\n    (0x1250, 'V'),\n    (0x1257, 'X'),\n    (0x1258, 'V'),\n    (0x1259, 'X'),\n    (0x125A, 'V'),\n    (0x125E, 'X'),\n    (0x1260, 'V'),\n    (0x1289, 'X'),\n    (0x128A, 'V'),\n    (0x128E, 'X'),\n    (0x1290, 'V'),\n    (0x12B1, 'X'),\n    (0x12B2, 'V'),\n    (0x12B6, 'X'),\n    (0x12B8, 'V'),\n    (0x12BF, 'X'),\n    (0x12C0, 'V'),\n    (0x12C1, 'X'),\n    (0x12C2, 'V'),\n    (0x12C6, 'X'),\n    (0x12C8, 'V'),\n    (0x12D7, 'X'),\n    (0x12D8, 'V'),\n    (0x1311, 'X'),\n    (0x1312, 'V'),\n    ]\n\ndef _seg_14():\n    return [\n    (0x1316, 'X'),\n    (0x1318, 'V'),\n    (0x135B, 'X'),\n    (0x135D, 'V'),\n    (0x137D, 'X'),\n    (0x1380, 'V'),\n    (0x139A, 'X'),\n    (0x13A0, 'V'),\n    (0x13F6, 'X'),\n    (0x13F8, 'M', u'Ᏸ'),\n    (0x13F9, 'M', u'Ᏹ'),\n    (0x13FA, 'M', u'Ᏺ'),\n    (0x13FB, 'M', u'Ᏻ'),\n    (0x13FC, 'M', u'Ᏼ'),\n    (0x13FD, 'M', u'Ᏽ'),\n    (0x13FE, 'X'),\n    (0x1400, 'V'),\n    (0x1680, 'X'),\n    (0x1681, 'V'),\n    (0x169D, 'X'),\n    (0x16A0, 'V'),\n    (0x16F9, 'X'),\n    (0x1700, 'V'),\n    (0x170D, 'X'),\n    (0x170E, 'V'),\n    (0x1715, 'X'),\n    (0x1720, 'V'),\n    (0x1737, 'X'),\n    (0x1740, 'V'),\n    (0x1754, 'X'),\n    (0x1760, 'V'),\n    (0x176D, 'X'),\n    (0x176E, 'V'),\n    (0x1771, 'X'),\n    (0x1772, 'V'),\n    (0x1774, 'X'),\n    (0x1780, 'V'),\n    (0x17B4, 'X'),\n    (0x17B6, 'V'),\n    (0x17DE, 'X'),\n    (0x17E0, 'V'),\n    (0x17EA, 'X'),\n    (0x17F0, 'V'),\n    (0x17FA, 'X'),\n    (0x1800, 'V'),\n    (0x1806, 'X'),\n    (0x1807, 'V'),\n    (0x180B, 'I'),\n    (0x180E, 'X'),\n    (0x1810, 'V'),\n    (0x181A, 'X'),\n    (0x1820, 'V'),\n    (0x1879, 'X'),\n    (0x1880, 'V'),\n    (0x18AB, 'X'),\n    (0x18B0, 'V'),\n    (0x18F6, 'X'),\n    (0x1900, 'V'),\n    (0x191F, 'X'),\n    (0x1920, 'V'),\n    (0x192C, 'X'),\n    (0x1930, 'V'),\n    (0x193C, 'X'),\n    (0x1940, 'V'),\n    (0x1941, 'X'),\n    (0x1944, 'V'),\n    (0x196E, 'X'),\n    (0x1970, 'V'),\n    (0x1975, 'X'),\n    (0x1980, 'V'),\n    (0x19AC, 'X'),\n    (0x19B0, 'V'),\n    (0x19CA, 'X'),\n    (0x19D0, 'V'),\n    (0x19DB, 'X'),\n    (0x19DE, 'V'),\n    (0x1A1C, 'X'),\n    (0x1A1E, 'V'),\n    (0x1A5F, 'X'),\n    (0x1A60, 'V'),\n    (0x1A7D, 'X'),\n    (0x1A7F, 'V'),\n    (0x1A8A, 'X'),\n    (0x1A90, 'V'),\n    (0x1A9A, 'X'),\n    (0x1AA0, 'V'),\n    (0x1AAE, 'X'),\n    (0x1AB0, 'V'),\n    (0x1AC1, 'X'),\n    (0x1B00, 'V'),\n    (0x1B4C, 'X'),\n    (0x1B50, 'V'),\n    (0x1B7D, 'X'),\n    (0x1B80, 'V'),\n    (0x1BF4, 'X'),\n    (0x1BFC, 'V'),\n    (0x1C38, 'X'),\n    (0x1C3B, 'V'),\n    (0x1C4A, 'X'),\n    (0x1C4D, 'V'),\n    ]\n\ndef _seg_15():\n    return [\n    (0x1C80, 'M', u'в'),\n    (0x1C81, 'M', u'д'),\n    (0x1C82, 'M', u'о'),\n    (0x1C83, 'M', u'с'),\n    (0x1C84, 'M', u'т'),\n    (0x1C86, 'M', u'ъ'),\n    (0x1C87, 'M', u'ѣ'),\n    (0x1C88, 'M', u'ꙋ'),\n    (0x1C89, 'X'),\n    (0x1C90, 'M', u'ა'),\n    (0x1C91, 'M', u'ბ'),\n    (0x1C92, 'M', u'გ'),\n    (0x1C93, 'M', u'დ'),\n    (0x1C94, 'M', u'ე'),\n    (0x1C95, 'M', u'ვ'),\n    (0x1C96, 'M', u'ზ'),\n    (0x1C97, 'M', u'თ'),\n    (0x1C98, 'M', u'ი'),\n    (0x1C99, 'M', u'კ'),\n    (0x1C9A, 'M', u'ლ'),\n    (0x1C9B, 'M', u'მ'),\n    (0x1C9C, 'M', u'ნ'),\n    (0x1C9D, 'M', u'ო'),\n    (0x1C9E, 'M', u'პ'),\n    (0x1C9F, 'M', u'ჟ'),\n    (0x1CA0, 'M', u'რ'),\n    (0x1CA1, 'M', u'ს'),\n    (0x1CA2, 'M', u'ტ'),\n    (0x1CA3, 'M', u'უ'),\n    (0x1CA4, 'M', u'ფ'),\n    (0x1CA5, 'M', u'ქ'),\n    (0x1CA6, 'M', u'ღ'),\n    (0x1CA7, 'M', u'ყ'),\n    (0x1CA8, 'M', u'შ'),\n    (0x1CA9, 'M', u'ჩ'),\n    (0x1CAA, 'M', u'ც'),\n    (0x1CAB, 'M', u'ძ'),\n    (0x1CAC, 'M', u'წ'),\n    (0x1CAD, 'M', u'ჭ'),\n    (0x1CAE, 'M', u'ხ'),\n    (0x1CAF, 'M', u'ჯ'),\n    (0x1CB0, 'M', u'ჰ'),\n    (0x1CB1, 'M', u'ჱ'),\n    (0x1CB2, 'M', u'ჲ'),\n    (0x1CB3, 'M', u'ჳ'),\n    (0x1CB4, 'M', u'ჴ'),\n    (0x1CB5, 'M', u'ჵ'),\n    (0x1CB6, 'M', u'ჶ'),\n    (0x1CB7, 'M', u'ჷ'),\n    (0x1CB8, 'M', u'ჸ'),\n    (0x1CB9, 'M', u'ჹ'),\n    (0x1CBA, 'M', u'ჺ'),\n    (0x1CBB, 'X'),\n    (0x1CBD, 'M', u'ჽ'),\n    (0x1CBE, 'M', u'ჾ'),\n    (0x1CBF, 'M', u'ჿ'),\n    (0x1CC0, 'V'),\n    (0x1CC8, 'X'),\n    (0x1CD0, 'V'),\n    (0x1CFB, 'X'),\n    (0x1D00, 'V'),\n    (0x1D2C, 'M', u'a'),\n    (0x1D2D, 'M', u'æ'),\n    (0x1D2E, 'M', u'b'),\n    (0x1D2F, 'V'),\n    (0x1D30, 'M', u'd'),\n    (0x1D31, 'M', u'e'),\n    (0x1D32, 'M', u'ǝ'),\n    (0x1D33, 'M', u'g'),\n    (0x1D34, 'M', u'h'),\n    (0x1D35, 'M', u'i'),\n    (0x1D36, 'M', u'j'),\n    (0x1D37, 'M', u'k'),\n    (0x1D38, 'M', u'l'),\n    (0x1D39, 'M', u'm'),\n    (0x1D3A, 'M', u'n'),\n    (0x1D3B, 'V'),\n    (0x1D3C, 'M', u'o'),\n    (0x1D3D, 'M', u'ȣ'),\n    (0x1D3E, 'M', u'p'),\n    (0x1D3F, 'M', u'r'),\n    (0x1D40, 'M', u't'),\n    (0x1D41, 'M', u'u'),\n    (0x1D42, 'M', u'w'),\n    (0x1D43, 'M', u'a'),\n    (0x1D44, 'M', u'ɐ'),\n    (0x1D45, 'M', u'ɑ'),\n    (0x1D46, 'M', u'ᴂ'),\n    (0x1D47, 'M', u'b'),\n    (0x1D48, 'M', u'd'),\n    (0x1D49, 'M', u'e'),\n    (0x1D4A, 'M', u'ə'),\n    (0x1D4B, 'M', u'ɛ'),\n    (0x1D4C, 'M', u'ɜ'),\n    (0x1D4D, 'M', u'g'),\n    (0x1D4E, 'V'),\n    (0x1D4F, 'M', u'k'),\n    (0x1D50, 'M', u'm'),\n    (0x1D51, 'M', u'ŋ'),\n    (0x1D52, 'M', u'o'),\n    ]\n\ndef _seg_16():\n    return [\n    (0x1D53, 'M', u'ɔ'),\n    (0x1D54, 'M', u'ᴖ'),\n    (0x1D55, 'M', u'ᴗ'),\n    (0x1D56, 'M', u'p'),\n    (0x1D57, 'M', u't'),\n    (0x1D58, 'M', u'u'),\n    (0x1D59, 'M', u'ᴝ'),\n    (0x1D5A, 'M', u'ɯ'),\n    (0x1D5B, 'M', u'v'),\n    (0x1D5C, 'M', u'ᴥ'),\n    (0x1D5D, 'M', u'β'),\n    (0x1D5E, 'M', u'γ'),\n    (0x1D5F, 'M', u'δ'),\n    (0x1D60, 'M', u'φ'),\n    (0x1D61, 'M', u'χ'),\n    (0x1D62, 'M', u'i'),\n    (0x1D63, 'M', u'r'),\n    (0x1D64, 'M', u'u'),\n    (0x1D65, 'M', u'v'),\n    (0x1D66, 'M', u'β'),\n    (0x1D67, 'M', u'γ'),\n    (0x1D68, 'M', u'ρ'),\n    (0x1D69, 'M', u'φ'),\n    (0x1D6A, 'M', u'χ'),\n    (0x1D6B, 'V'),\n    (0x1D78, 'M', u'н'),\n    (0x1D79, 'V'),\n    (0x1D9B, 'M', u'ɒ'),\n    (0x1D9C, 'M', u'c'),\n    (0x1D9D, 'M', u'ɕ'),\n    (0x1D9E, 'M', u'ð'),\n    (0x1D9F, 'M', u'ɜ'),\n    (0x1DA0, 'M', u'f'),\n    (0x1DA1, 'M', u'ɟ'),\n    (0x1DA2, 'M', u'ɡ'),\n    (0x1DA3, 'M', u'ɥ'),\n    (0x1DA4, 'M', u'ɨ'),\n    (0x1DA5, 'M', u'ɩ'),\n    (0x1DA6, 'M', u'ɪ'),\n    (0x1DA7, 'M', u'ᵻ'),\n    (0x1DA8, 'M', u'ʝ'),\n    (0x1DA9, 'M', u'ɭ'),\n    (0x1DAA, 'M', u'ᶅ'),\n    (0x1DAB, 'M', u'ʟ'),\n    (0x1DAC, 'M', u'ɱ'),\n    (0x1DAD, 'M', u'ɰ'),\n    (0x1DAE, 'M', u'ɲ'),\n    (0x1DAF, 'M', u'ɳ'),\n    (0x1DB0, 'M', u'ɴ'),\n    (0x1DB1, 'M', u'ɵ'),\n    (0x1DB2, 'M', u'ɸ'),\n    (0x1DB3, 'M', u'ʂ'),\n    (0x1DB4, 'M', u'ʃ'),\n    (0x1DB5, 'M', u'ƫ'),\n    (0x1DB6, 'M', u'ʉ'),\n    (0x1DB7, 'M', u'ʊ'),\n    (0x1DB8, 'M', u'ᴜ'),\n    (0x1DB9, 'M', u'ʋ'),\n    (0x1DBA, 'M', u'ʌ'),\n    (0x1DBB, 'M', u'z'),\n    (0x1DBC, 'M', u'ʐ'),\n    (0x1DBD, 'M', u'ʑ'),\n    (0x1DBE, 'M', u'ʒ'),\n    (0x1DBF, 'M', u'θ'),\n    (0x1DC0, 'V'),\n    (0x1DFA, 'X'),\n    (0x1DFB, 'V'),\n    (0x1E00, 'M', u'ḁ'),\n    (0x1E01, 'V'),\n    (0x1E02, 'M', u'ḃ'),\n    (0x1E03, 'V'),\n    (0x1E04, 'M', u'ḅ'),\n    (0x1E05, 'V'),\n    (0x1E06, 'M', u'ḇ'),\n    (0x1E07, 'V'),\n    (0x1E08, 'M', u'ḉ'),\n    (0x1E09, 'V'),\n    (0x1E0A, 'M', u'ḋ'),\n    (0x1E0B, 'V'),\n    (0x1E0C, 'M', u'ḍ'),\n    (0x1E0D, 'V'),\n    (0x1E0E, 'M', u'ḏ'),\n    (0x1E0F, 'V'),\n    (0x1E10, 'M', u'ḑ'),\n    (0x1E11, 'V'),\n    (0x1E12, 'M', u'ḓ'),\n    (0x1E13, 'V'),\n    (0x1E14, 'M', u'ḕ'),\n    (0x1E15, 'V'),\n    (0x1E16, 'M', u'ḗ'),\n    (0x1E17, 'V'),\n    (0x1E18, 'M', u'ḙ'),\n    (0x1E19, 'V'),\n    (0x1E1A, 'M', u'ḛ'),\n    (0x1E1B, 'V'),\n    (0x1E1C, 'M', u'ḝ'),\n    (0x1E1D, 'V'),\n    (0x1E1E, 'M', u'ḟ'),\n    (0x1E1F, 'V'),\n    (0x1E20, 'M', u'ḡ'),\n    ]\n\ndef _seg_17():\n    return [\n    (0x1E21, 'V'),\n    (0x1E22, 'M', u'ḣ'),\n    (0x1E23, 'V'),\n    (0x1E24, 'M', u'ḥ'),\n    (0x1E25, 'V'),\n    (0x1E26, 'M', u'ḧ'),\n    (0x1E27, 'V'),\n    (0x1E28, 'M', u'ḩ'),\n    (0x1E29, 'V'),\n    (0x1E2A, 'M', u'ḫ'),\n    (0x1E2B, 'V'),\n    (0x1E2C, 'M', u'ḭ'),\n    (0x1E2D, 'V'),\n    (0x1E2E, 'M', u'ḯ'),\n    (0x1E2F, 'V'),\n    (0x1E30, 'M', u'ḱ'),\n    (0x1E31, 'V'),\n    (0x1E32, 'M', u'ḳ'),\n    (0x1E33, 'V'),\n    (0x1E34, 'M', u'ḵ'),\n    (0x1E35, 'V'),\n    (0x1E36, 'M', u'ḷ'),\n    (0x1E37, 'V'),\n    (0x1E38, 'M', u'ḹ'),\n    (0x1E39, 'V'),\n    (0x1E3A, 'M', u'ḻ'),\n    (0x1E3B, 'V'),\n    (0x1E3C, 'M', u'ḽ'),\n    (0x1E3D, 'V'),\n    (0x1E3E, 'M', u'ḿ'),\n    (0x1E3F, 'V'),\n    (0x1E40, 'M', u'ṁ'),\n    (0x1E41, 'V'),\n    (0x1E42, 'M', u'ṃ'),\n    (0x1E43, 'V'),\n    (0x1E44, 'M', u'ṅ'),\n    (0x1E45, 'V'),\n    (0x1E46, 'M', u'ṇ'),\n    (0x1E47, 'V'),\n    (0x1E48, 'M', u'ṉ'),\n    (0x1E49, 'V'),\n    (0x1E4A, 'M', u'ṋ'),\n    (0x1E4B, 'V'),\n    (0x1E4C, 'M', u'ṍ'),\n    (0x1E4D, 'V'),\n    (0x1E4E, 'M', u'ṏ'),\n    (0x1E4F, 'V'),\n    (0x1E50, 'M', u'ṑ'),\n    (0x1E51, 'V'),\n    (0x1E52, 'M', u'ṓ'),\n    (0x1E53, 'V'),\n    (0x1E54, 'M', u'ṕ'),\n    (0x1E55, 'V'),\n    (0x1E56, 'M', u'ṗ'),\n    (0x1E57, 'V'),\n    (0x1E58, 'M', u'ṙ'),\n    (0x1E59, 'V'),\n    (0x1E5A, 'M', u'ṛ'),\n    (0x1E5B, 'V'),\n    (0x1E5C, 'M', u'ṝ'),\n    (0x1E5D, 'V'),\n    (0x1E5E, 'M', u'ṟ'),\n    (0x1E5F, 'V'),\n    (0x1E60, 'M', u'ṡ'),\n    (0x1E61, 'V'),\n    (0x1E62, 'M', u'ṣ'),\n    (0x1E63, 'V'),\n    (0x1E64, 'M', u'ṥ'),\n    (0x1E65, 'V'),\n    (0x1E66, 'M', u'ṧ'),\n    (0x1E67, 'V'),\n    (0x1E68, 'M', u'ṩ'),\n    (0x1E69, 'V'),\n    (0x1E6A, 'M', u'ṫ'),\n    (0x1E6B, 'V'),\n    (0x1E6C, 'M', u'ṭ'),\n    (0x1E6D, 'V'),\n    (0x1E6E, 'M', u'ṯ'),\n    (0x1E6F, 'V'),\n    (0x1E70, 'M', u'ṱ'),\n    (0x1E71, 'V'),\n    (0x1E72, 'M', u'ṳ'),\n    (0x1E73, 'V'),\n    (0x1E74, 'M', u'ṵ'),\n    (0x1E75, 'V'),\n    (0x1E76, 'M', u'ṷ'),\n    (0x1E77, 'V'),\n    (0x1E78, 'M', u'ṹ'),\n    (0x1E79, 'V'),\n    (0x1E7A, 'M', u'ṻ'),\n    (0x1E7B, 'V'),\n    (0x1E7C, 'M', u'ṽ'),\n    (0x1E7D, 'V'),\n    (0x1E7E, 'M', u'ṿ'),\n    (0x1E7F, 'V'),\n    (0x1E80, 'M', u'ẁ'),\n    (0x1E81, 'V'),\n    (0x1E82, 'M', u'ẃ'),\n    (0x1E83, 'V'),\n    (0x1E84, 'M', u'ẅ'),\n    ]\n\ndef _seg_18():\n    return [\n    (0x1E85, 'V'),\n    (0x1E86, 'M', u'ẇ'),\n    (0x1E87, 'V'),\n    (0x1E88, 'M', u'ẉ'),\n    (0x1E89, 'V'),\n    (0x1E8A, 'M', u'ẋ'),\n    (0x1E8B, 'V'),\n    (0x1E8C, 'M', u'ẍ'),\n    (0x1E8D, 'V'),\n    (0x1E8E, 'M', u'ẏ'),\n    (0x1E8F, 'V'),\n    (0x1E90, 'M', u'ẑ'),\n    (0x1E91, 'V'),\n    (0x1E92, 'M', u'ẓ'),\n    (0x1E93, 'V'),\n    (0x1E94, 'M', u'ẕ'),\n    (0x1E95, 'V'),\n    (0x1E9A, 'M', u'aʾ'),\n    (0x1E9B, 'M', u'ṡ'),\n    (0x1E9C, 'V'),\n    (0x1E9E, 'M', u'ss'),\n    (0x1E9F, 'V'),\n    (0x1EA0, 'M', u'ạ'),\n    (0x1EA1, 'V'),\n    (0x1EA2, 'M', u'ả'),\n    (0x1EA3, 'V'),\n    (0x1EA4, 'M', u'ấ'),\n    (0x1EA5, 'V'),\n    (0x1EA6, 'M', u'ầ'),\n    (0x1EA7, 'V'),\n    (0x1EA8, 'M', u'ẩ'),\n    (0x1EA9, 'V'),\n    (0x1EAA, 'M', u'ẫ'),\n    (0x1EAB, 'V'),\n    (0x1EAC, 'M', u'ậ'),\n    (0x1EAD, 'V'),\n    (0x1EAE, 'M', u'ắ'),\n    (0x1EAF, 'V'),\n    (0x1EB0, 'M', u'ằ'),\n    (0x1EB1, 'V'),\n    (0x1EB2, 'M', u'ẳ'),\n    (0x1EB3, 'V'),\n    (0x1EB4, 'M', u'ẵ'),\n    (0x1EB5, 'V'),\n    (0x1EB6, 'M', u'ặ'),\n    (0x1EB7, 'V'),\n    (0x1EB8, 'M', u'ẹ'),\n    (0x1EB9, 'V'),\n    (0x1EBA, 'M', u'ẻ'),\n    (0x1EBB, 'V'),\n    (0x1EBC, 'M', u'ẽ'),\n    (0x1EBD, 'V'),\n    (0x1EBE, 'M', u'ế'),\n    (0x1EBF, 'V'),\n    (0x1EC0, 'M', u'ề'),\n    (0x1EC1, 'V'),\n    (0x1EC2, 'M', u'ể'),\n    (0x1EC3, 'V'),\n    (0x1EC4, 'M', u'ễ'),\n    (0x1EC5, 'V'),\n    (0x1EC6, 'M', u'ệ'),\n    (0x1EC7, 'V'),\n    (0x1EC8, 'M', u'ỉ'),\n    (0x1EC9, 'V'),\n    (0x1ECA, 'M', u'ị'),\n    (0x1ECB, 'V'),\n    (0x1ECC, 'M', u'ọ'),\n    (0x1ECD, 'V'),\n    (0x1ECE, 'M', u'ỏ'),\n    (0x1ECF, 'V'),\n    (0x1ED0, 'M', u'ố'),\n    (0x1ED1, 'V'),\n    (0x1ED2, 'M', u'ồ'),\n    (0x1ED3, 'V'),\n    (0x1ED4, 'M', u'ổ'),\n    (0x1ED5, 'V'),\n    (0x1ED6, 'M', u'ỗ'),\n    (0x1ED7, 'V'),\n    (0x1ED8, 'M', u'ộ'),\n    (0x1ED9, 'V'),\n    (0x1EDA, 'M', u'ớ'),\n    (0x1EDB, 'V'),\n    (0x1EDC, 'M', u'ờ'),\n    (0x1EDD, 'V'),\n    (0x1EDE, 'M', u'ở'),\n    (0x1EDF, 'V'),\n    (0x1EE0, 'M', u'ỡ'),\n    (0x1EE1, 'V'),\n    (0x1EE2, 'M', u'ợ'),\n    (0x1EE3, 'V'),\n    (0x1EE4, 'M', u'ụ'),\n    (0x1EE5, 'V'),\n    (0x1EE6, 'M', u'ủ'),\n    (0x1EE7, 'V'),\n    (0x1EE8, 'M', u'ứ'),\n    (0x1EE9, 'V'),\n    (0x1EEA, 'M', u'ừ'),\n    (0x1EEB, 'V'),\n    (0x1EEC, 'M', u'ử'),\n    (0x1EED, 'V'),\n    ]\n\ndef _seg_19():\n    return [\n    (0x1EEE, 'M', u'ữ'),\n    (0x1EEF, 'V'),\n    (0x1EF0, 'M', u'ự'),\n    (0x1EF1, 'V'),\n    (0x1EF2, 'M', u'ỳ'),\n    (0x1EF3, 'V'),\n    (0x1EF4, 'M', u'ỵ'),\n    (0x1EF5, 'V'),\n    (0x1EF6, 'M', u'ỷ'),\n    (0x1EF7, 'V'),\n    (0x1EF8, 'M', u'ỹ'),\n    (0x1EF9, 'V'),\n    (0x1EFA, 'M', u'ỻ'),\n    (0x1EFB, 'V'),\n    (0x1EFC, 'M', u'ỽ'),\n    (0x1EFD, 'V'),\n    (0x1EFE, 'M', u'ỿ'),\n    (0x1EFF, 'V'),\n    (0x1F08, 'M', u'ἀ'),\n    (0x1F09, 'M', u'ἁ'),\n    (0x1F0A, 'M', u'ἂ'),\n    (0x1F0B, 'M', u'ἃ'),\n    (0x1F0C, 'M', u'ἄ'),\n    (0x1F0D, 'M', u'ἅ'),\n    (0x1F0E, 'M', u'ἆ'),\n    (0x1F0F, 'M', u'ἇ'),\n    (0x1F10, 'V'),\n    (0x1F16, 'X'),\n    (0x1F18, 'M', u'ἐ'),\n    (0x1F19, 'M', u'ἑ'),\n    (0x1F1A, 'M', u'ἒ'),\n    (0x1F1B, 'M', u'ἓ'),\n    (0x1F1C, 'M', u'ἔ'),\n    (0x1F1D, 'M', u'ἕ'),\n    (0x1F1E, 'X'),\n    (0x1F20, 'V'),\n    (0x1F28, 'M', u'ἠ'),\n    (0x1F29, 'M', u'ἡ'),\n    (0x1F2A, 'M', u'ἢ'),\n    (0x1F2B, 'M', u'ἣ'),\n    (0x1F2C, 'M', u'ἤ'),\n    (0x1F2D, 'M', u'ἥ'),\n    (0x1F2E, 'M', u'ἦ'),\n    (0x1F2F, 'M', u'ἧ'),\n    (0x1F30, 'V'),\n    (0x1F38, 'M', u'ἰ'),\n    (0x1F39, 'M', u'ἱ'),\n    (0x1F3A, 'M', u'ἲ'),\n    (0x1F3B, 'M', u'ἳ'),\n    (0x1F3C, 'M', u'ἴ'),\n    (0x1F3D, 'M', u'ἵ'),\n    (0x1F3E, 'M', u'ἶ'),\n    (0x1F3F, 'M', u'ἷ'),\n    (0x1F40, 'V'),\n    (0x1F46, 'X'),\n    (0x1F48, 'M', u'ὀ'),\n    (0x1F49, 'M', u'ὁ'),\n    (0x1F4A, 'M', u'ὂ'),\n    (0x1F4B, 'M', u'ὃ'),\n    (0x1F4C, 'M', u'ὄ'),\n    (0x1F4D, 'M', u'ὅ'),\n    (0x1F4E, 'X'),\n    (0x1F50, 'V'),\n    (0x1F58, 'X'),\n    (0x1F59, 'M', u'ὑ'),\n    (0x1F5A, 'X'),\n    (0x1F5B, 'M', u'ὓ'),\n    (0x1F5C, 'X'),\n    (0x1F5D, 'M', u'ὕ'),\n    (0x1F5E, 'X'),\n    (0x1F5F, 'M', u'ὗ'),\n    (0x1F60, 'V'),\n    (0x1F68, 'M', u'ὠ'),\n    (0x1F69, 'M', u'ὡ'),\n    (0x1F6A, 'M', u'ὢ'),\n    (0x1F6B, 'M', u'ὣ'),\n    (0x1F6C, 'M', u'ὤ'),\n    (0x1F6D, 'M', u'ὥ'),\n    (0x1F6E, 'M', u'ὦ'),\n    (0x1F6F, 'M', u'ὧ'),\n    (0x1F70, 'V'),\n    (0x1F71, 'M', u'ά'),\n    (0x1F72, 'V'),\n    (0x1F73, 'M', u'έ'),\n    (0x1F74, 'V'),\n    (0x1F75, 'M', u'ή'),\n    (0x1F76, 'V'),\n    (0x1F77, 'M', u'ί'),\n    (0x1F78, 'V'),\n    (0x1F79, 'M', u'ό'),\n    (0x1F7A, 'V'),\n    (0x1F7B, 'M', u'ύ'),\n    (0x1F7C, 'V'),\n    (0x1F7D, 'M', u'ώ'),\n    (0x1F7E, 'X'),\n    (0x1F80, 'M', u'ἀι'),\n    (0x1F81, 'M', u'ἁι'),\n    (0x1F82, 'M', u'ἂι'),\n    (0x1F83, 'M', u'ἃι'),\n    (0x1F84, 'M', u'ἄι'),\n    ]\n\ndef _seg_20():\n    return [\n    (0x1F85, 'M', u'ἅι'),\n    (0x1F86, 'M', u'ἆι'),\n    (0x1F87, 'M', u'ἇι'),\n    (0x1F88, 'M', u'ἀι'),\n    (0x1F89, 'M', u'ἁι'),\n    (0x1F8A, 'M', u'ἂι'),\n    (0x1F8B, 'M', u'ἃι'),\n    (0x1F8C, 'M', u'ἄι'),\n    (0x1F8D, 'M', u'ἅι'),\n    (0x1F8E, 'M', u'ἆι'),\n    (0x1F8F, 'M', u'ἇι'),\n    (0x1F90, 'M', u'ἠι'),\n    (0x1F91, 'M', u'ἡι'),\n    (0x1F92, 'M', u'ἢι'),\n    (0x1F93, 'M', u'ἣι'),\n    (0x1F94, 'M', u'ἤι'),\n    (0x1F95, 'M', u'ἥι'),\n    (0x1F96, 'M', u'ἦι'),\n    (0x1F97, 'M', u'ἧι'),\n    (0x1F98, 'M', u'ἠι'),\n    (0x1F99, 'M', u'ἡι'),\n    (0x1F9A, 'M', u'ἢι'),\n    (0x1F9B, 'M', u'ἣι'),\n    (0x1F9C, 'M', u'ἤι'),\n    (0x1F9D, 'M', u'ἥι'),\n    (0x1F9E, 'M', u'ἦι'),\n    (0x1F9F, 'M', u'ἧι'),\n    (0x1FA0, 'M', u'ὠι'),\n    (0x1FA1, 'M', u'ὡι'),\n    (0x1FA2, 'M', u'ὢι'),\n    (0x1FA3, 'M', u'ὣι'),\n    (0x1FA4, 'M', u'ὤι'),\n    (0x1FA5, 'M', u'ὥι'),\n    (0x1FA6, 'M', u'ὦι'),\n    (0x1FA7, 'M', u'ὧι'),\n    (0x1FA8, 'M', u'ὠι'),\n    (0x1FA9, 'M', u'ὡι'),\n    (0x1FAA, 'M', u'ὢι'),\n    (0x1FAB, 'M', u'ὣι'),\n    (0x1FAC, 'M', u'ὤι'),\n    (0x1FAD, 'M', u'ὥι'),\n    (0x1FAE, 'M', u'ὦι'),\n    (0x1FAF, 'M', u'ὧι'),\n    (0x1FB0, 'V'),\n    (0x1FB2, 'M', u'ὰι'),\n    (0x1FB3, 'M', u'αι'),\n    (0x1FB4, 'M', u'άι'),\n    (0x1FB5, 'X'),\n    (0x1FB6, 'V'),\n    (0x1FB7, 'M', u'ᾶι'),\n    (0x1FB8, 'M', u'ᾰ'),\n    (0x1FB9, 'M', u'ᾱ'),\n    (0x1FBA, 'M', u'ὰ'),\n    (0x1FBB, 'M', u'ά'),\n    (0x1FBC, 'M', u'αι'),\n    (0x1FBD, '3', u' ̓'),\n    (0x1FBE, 'M', u'ι'),\n    (0x1FBF, '3', u' ̓'),\n    (0x1FC0, '3', u' ͂'),\n    (0x1FC1, '3', u' ̈͂'),\n    (0x1FC2, 'M', u'ὴι'),\n    (0x1FC3, 'M', u'ηι'),\n    (0x1FC4, 'M', u'ήι'),\n    (0x1FC5, 'X'),\n    (0x1FC6, 'V'),\n    (0x1FC7, 'M', u'ῆι'),\n    (0x1FC8, 'M', u'ὲ'),\n    (0x1FC9, 'M', u'έ'),\n    (0x1FCA, 'M', u'ὴ'),\n    (0x1FCB, 'M', u'ή'),\n    (0x1FCC, 'M', u'ηι'),\n    (0x1FCD, '3', u' ̓̀'),\n    (0x1FCE, '3', u' ̓́'),\n    (0x1FCF, '3', u' ̓͂'),\n    (0x1FD0, 'V'),\n    (0x1FD3, 'M', u'ΐ'),\n    (0x1FD4, 'X'),\n    (0x1FD6, 'V'),\n    (0x1FD8, 'M', u'ῐ'),\n    (0x1FD9, 'M', u'ῑ'),\n    (0x1FDA, 'M', u'ὶ'),\n    (0x1FDB, 'M', u'ί'),\n    (0x1FDC, 'X'),\n    (0x1FDD, '3', u' ̔̀'),\n    (0x1FDE, '3', u' ̔́'),\n    (0x1FDF, '3', u' ̔͂'),\n    (0x1FE0, 'V'),\n    (0x1FE3, 'M', u'ΰ'),\n    (0x1FE4, 'V'),\n    (0x1FE8, 'M', u'ῠ'),\n    (0x1FE9, 'M', u'ῡ'),\n    (0x1FEA, 'M', u'ὺ'),\n    (0x1FEB, 'M', u'ύ'),\n    (0x1FEC, 'M', u'ῥ'),\n    (0x1FED, '3', u' ̈̀'),\n    (0x1FEE, '3', u' ̈́'),\n    (0x1FEF, '3', u'`'),\n    (0x1FF0, 'X'),\n    (0x1FF2, 'M', u'ὼι'),\n    (0x1FF3, 'M', u'ωι'),\n    ]\n\ndef _seg_21():\n    return [\n    (0x1FF4, 'M', u'ώι'),\n    (0x1FF5, 'X'),\n    (0x1FF6, 'V'),\n    (0x1FF7, 'M', u'ῶι'),\n    (0x1FF8, 'M', u'ὸ'),\n    (0x1FF9, 'M', u'ό'),\n    (0x1FFA, 'M', u'ὼ'),\n    (0x1FFB, 'M', u'ώ'),\n    (0x1FFC, 'M', u'ωι'),\n    (0x1FFD, '3', u' ́'),\n    (0x1FFE, '3', u' ̔'),\n    (0x1FFF, 'X'),\n    (0x2000, '3', u' '),\n    (0x200B, 'I'),\n    (0x200C, 'D', u''),\n    (0x200E, 'X'),\n    (0x2010, 'V'),\n    (0x2011, 'M', u'‐'),\n    (0x2012, 'V'),\n    (0x2017, '3', u' ̳'),\n    (0x2018, 'V'),\n    (0x2024, 'X'),\n    (0x2027, 'V'),\n    (0x2028, 'X'),\n    (0x202F, '3', u' '),\n    (0x2030, 'V'),\n    (0x2033, 'M', u'′′'),\n    (0x2034, 'M', u'′′′'),\n    (0x2035, 'V'),\n    (0x2036, 'M', u'‵‵'),\n    (0x2037, 'M', u'‵‵‵'),\n    (0x2038, 'V'),\n    (0x203C, '3', u'!!'),\n    (0x203D, 'V'),\n    (0x203E, '3', u' ̅'),\n    (0x203F, 'V'),\n    (0x2047, '3', u'??'),\n    (0x2048, '3', u'?!'),\n    (0x2049, '3', u'!?'),\n    (0x204A, 'V'),\n    (0x2057, 'M', u'′′′′'),\n    (0x2058, 'V'),\n    (0x205F, '3', u' '),\n    (0x2060, 'I'),\n    (0x2061, 'X'),\n    (0x2064, 'I'),\n    (0x2065, 'X'),\n    (0x2070, 'M', u'0'),\n    (0x2071, 'M', u'i'),\n    (0x2072, 'X'),\n    (0x2074, 'M', u'4'),\n    (0x2075, 'M', u'5'),\n    (0x2076, 'M', u'6'),\n    (0x2077, 'M', u'7'),\n    (0x2078, 'M', u'8'),\n    (0x2079, 'M', u'9'),\n    (0x207A, '3', u'+'),\n    (0x207B, 'M', u'−'),\n    (0x207C, '3', u'='),\n    (0x207D, '3', u'('),\n    (0x207E, '3', u')'),\n    (0x207F, 'M', u'n'),\n    (0x2080, 'M', u'0'),\n    (0x2081, 'M', u'1'),\n    (0x2082, 'M', u'2'),\n    (0x2083, 'M', u'3'),\n    (0x2084, 'M', u'4'),\n    (0x2085, 'M', u'5'),\n    (0x2086, 'M', u'6'),\n    (0x2087, 'M', u'7'),\n    (0x2088, 'M', u'8'),\n    (0x2089, 'M', u'9'),\n    (0x208A, '3', u'+'),\n    (0x208B, 'M', u'−'),\n    (0x208C, '3', u'='),\n    (0x208D, '3', u'('),\n    (0x208E, '3', u')'),\n    (0x208F, 'X'),\n    (0x2090, 'M', u'a'),\n    (0x2091, 'M', u'e'),\n    (0x2092, 'M', u'o'),\n    (0x2093, 'M', u'x'),\n    (0x2094, 'M', u'ə'),\n    (0x2095, 'M', u'h'),\n    (0x2096, 'M', u'k'),\n    (0x2097, 'M', u'l'),\n    (0x2098, 'M', u'm'),\n    (0x2099, 'M', u'n'),\n    (0x209A, 'M', u'p'),\n    (0x209B, 'M', u's'),\n    (0x209C, 'M', u't'),\n    (0x209D, 'X'),\n    (0x20A0, 'V'),\n    (0x20A8, 'M', u'rs'),\n    (0x20A9, 'V'),\n    (0x20C0, 'X'),\n    (0x20D0, 'V'),\n    (0x20F1, 'X'),\n    (0x2100, '3', u'a/c'),\n    (0x2101, '3', u'a/s'),\n    ]\n\ndef _seg_22():\n    return [\n    (0x2102, 'M', u'c'),\n    (0x2103, 'M', u'°c'),\n    (0x2104, 'V'),\n    (0x2105, '3', u'c/o'),\n    (0x2106, '3', u'c/u'),\n    (0x2107, 'M', u'ɛ'),\n    (0x2108, 'V'),\n    (0x2109, 'M', u'°f'),\n    (0x210A, 'M', u'g'),\n    (0x210B, 'M', u'h'),\n    (0x210F, 'M', u'ħ'),\n    (0x2110, 'M', u'i'),\n    (0x2112, 'M', u'l'),\n    (0x2114, 'V'),\n    (0x2115, 'M', u'n'),\n    (0x2116, 'M', u'no'),\n    (0x2117, 'V'),\n    (0x2119, 'M', u'p'),\n    (0x211A, 'M', u'q'),\n    (0x211B, 'M', u'r'),\n    (0x211E, 'V'),\n    (0x2120, 'M', u'sm'),\n    (0x2121, 'M', u'tel'),\n    (0x2122, 'M', u'tm'),\n    (0x2123, 'V'),\n    (0x2124, 'M', u'z'),\n    (0x2125, 'V'),\n    (0x2126, 'M', u'ω'),\n    (0x2127, 'V'),\n    (0x2128, 'M', u'z'),\n    (0x2129, 'V'),\n    (0x212A, 'M', u'k'),\n    (0x212B, 'M', u'å'),\n    (0x212C, 'M', u'b'),\n    (0x212D, 'M', u'c'),\n    (0x212E, 'V'),\n    (0x212F, 'M', u'e'),\n    (0x2131, 'M', u'f'),\n    (0x2132, 'X'),\n    (0x2133, 'M', u'm'),\n    (0x2134, 'M', u'o'),\n    (0x2135, 'M', u'א'),\n    (0x2136, 'M', u'ב'),\n    (0x2137, 'M', u'ג'),\n    (0x2138, 'M', u'ד'),\n    (0x2139, 'M', u'i'),\n    (0x213A, 'V'),\n    (0x213B, 'M', u'fax'),\n    (0x213C, 'M', u'π'),\n    (0x213D, 'M', u'γ'),\n    (0x213F, 'M', u'π'),\n    (0x2140, 'M', u'∑'),\n    (0x2141, 'V'),\n    (0x2145, 'M', u'd'),\n    (0x2147, 'M', u'e'),\n    (0x2148, 'M', u'i'),\n    (0x2149, 'M', u'j'),\n    (0x214A, 'V'),\n    (0x2150, 'M', u'1⁄7'),\n    (0x2151, 'M', u'1⁄9'),\n    (0x2152, 'M', u'1⁄10'),\n    (0x2153, 'M', u'1⁄3'),\n    (0x2154, 'M', u'2⁄3'),\n    (0x2155, 'M', u'1⁄5'),\n    (0x2156, 'M', u'2⁄5'),\n    (0x2157, 'M', u'3⁄5'),\n    (0x2158, 'M', u'4⁄5'),\n    (0x2159, 'M', u'1⁄6'),\n    (0x215A, 'M', u'5⁄6'),\n    (0x215B, 'M', u'1⁄8'),\n    (0x215C, 'M', u'3⁄8'),\n    (0x215D, 'M', u'5⁄8'),\n    (0x215E, 'M', u'7⁄8'),\n    (0x215F, 'M', u'1⁄'),\n    (0x2160, 'M', u'i'),\n    (0x2161, 'M', u'ii'),\n    (0x2162, 'M', u'iii'),\n    (0x2163, 'M', u'iv'),\n    (0x2164, 'M', u'v'),\n    (0x2165, 'M', u'vi'),\n    (0x2166, 'M', u'vii'),\n    (0x2167, 'M', u'viii'),\n    (0x2168, 'M', u'ix'),\n    (0x2169, 'M', u'x'),\n    (0x216A, 'M', u'xi'),\n    (0x216B, 'M', u'xii'),\n    (0x216C, 'M', u'l'),\n    (0x216D, 'M', u'c'),\n    (0x216E, 'M', u'd'),\n    (0x216F, 'M', u'm'),\n    (0x2170, 'M', u'i'),\n    (0x2171, 'M', u'ii'),\n    (0x2172, 'M', u'iii'),\n    (0x2173, 'M', u'iv'),\n    (0x2174, 'M', u'v'),\n    (0x2175, 'M', u'vi'),\n    (0x2176, 'M', u'vii'),\n    (0x2177, 'M', u'viii'),\n    (0x2178, 'M', u'ix'),\n    (0x2179, 'M', u'x'),\n    ]\n\ndef _seg_23():\n    return [\n    (0x217A, 'M', u'xi'),\n    (0x217B, 'M', u'xii'),\n    (0x217C, 'M', u'l'),\n    (0x217D, 'M', u'c'),\n    (0x217E, 'M', u'd'),\n    (0x217F, 'M', u'm'),\n    (0x2180, 'V'),\n    (0x2183, 'X'),\n    (0x2184, 'V'),\n    (0x2189, 'M', u'0⁄3'),\n    (0x218A, 'V'),\n    (0x218C, 'X'),\n    (0x2190, 'V'),\n    (0x222C, 'M', u'∫∫'),\n    (0x222D, 'M', u'∫∫∫'),\n    (0x222E, 'V'),\n    (0x222F, 'M', u'∮∮'),\n    (0x2230, 'M', u'∮∮∮'),\n    (0x2231, 'V'),\n    (0x2260, '3'),\n    (0x2261, 'V'),\n    (0x226E, '3'),\n    (0x2270, 'V'),\n    (0x2329, 'M', u'〈'),\n    (0x232A, 'M', u'〉'),\n    (0x232B, 'V'),\n    (0x2427, 'X'),\n    (0x2440, 'V'),\n    (0x244B, 'X'),\n    (0x2460, 'M', u'1'),\n    (0x2461, 'M', u'2'),\n    (0x2462, 'M', u'3'),\n    (0x2463, 'M', u'4'),\n    (0x2464, 'M', u'5'),\n    (0x2465, 'M', u'6'),\n    (0x2466, 'M', u'7'),\n    (0x2467, 'M', u'8'),\n    (0x2468, 'M', u'9'),\n    (0x2469, 'M', u'10'),\n    (0x246A, 'M', u'11'),\n    (0x246B, 'M', u'12'),\n    (0x246C, 'M', u'13'),\n    (0x246D, 'M', u'14'),\n    (0x246E, 'M', u'15'),\n    (0x246F, 'M', u'16'),\n    (0x2470, 'M', u'17'),\n    (0x2471, 'M', u'18'),\n    (0x2472, 'M', u'19'),\n    (0x2473, 'M', u'20'),\n    (0x2474, '3', u'(1)'),\n    (0x2475, '3', u'(2)'),\n    (0x2476, '3', u'(3)'),\n    (0x2477, '3', u'(4)'),\n    (0x2478, '3', u'(5)'),\n    (0x2479, '3', u'(6)'),\n    (0x247A, '3', u'(7)'),\n    (0x247B, '3', u'(8)'),\n    (0x247C, '3', u'(9)'),\n    (0x247D, '3', u'(10)'),\n    (0x247E, '3', u'(11)'),\n    (0x247F, '3', u'(12)'),\n    (0x2480, '3', u'(13)'),\n    (0x2481, '3', u'(14)'),\n    (0x2482, '3', u'(15)'),\n    (0x2483, '3', u'(16)'),\n    (0x2484, '3', u'(17)'),\n    (0x2485, '3', u'(18)'),\n    (0x2486, '3', u'(19)'),\n    (0x2487, '3', u'(20)'),\n    (0x2488, 'X'),\n    (0x249C, '3', u'(a)'),\n    (0x249D, '3', u'(b)'),\n    (0x249E, '3', u'(c)'),\n    (0x249F, '3', u'(d)'),\n    (0x24A0, '3', u'(e)'),\n    (0x24A1, '3', u'(f)'),\n    (0x24A2, '3', u'(g)'),\n    (0x24A3, '3', u'(h)'),\n    (0x24A4, '3', u'(i)'),\n    (0x24A5, '3', u'(j)'),\n    (0x24A6, '3', u'(k)'),\n    (0x24A7, '3', u'(l)'),\n    (0x24A8, '3', u'(m)'),\n    (0x24A9, '3', u'(n)'),\n    (0x24AA, '3', u'(o)'),\n    (0x24AB, '3', u'(p)'),\n    (0x24AC, '3', u'(q)'),\n    (0x24AD, '3', u'(r)'),\n    (0x24AE, '3', u'(s)'),\n    (0x24AF, '3', u'(t)'),\n    (0x24B0, '3', u'(u)'),\n    (0x24B1, '3', u'(v)'),\n    (0x24B2, '3', u'(w)'),\n    (0x24B3, '3', u'(x)'),\n    (0x24B4, '3', u'(y)'),\n    (0x24B5, '3', u'(z)'),\n    (0x24B6, 'M', u'a'),\n    (0x24B7, 'M', u'b'),\n    (0x24B8, 'M', u'c'),\n    (0x24B9, 'M', u'd'),\n    ]\n\ndef _seg_24():\n    return [\n    (0x24BA, 'M', u'e'),\n    (0x24BB, 'M', u'f'),\n    (0x24BC, 'M', u'g'),\n    (0x24BD, 'M', u'h'),\n    (0x24BE, 'M', u'i'),\n    (0x24BF, 'M', u'j'),\n    (0x24C0, 'M', u'k'),\n    (0x24C1, 'M', u'l'),\n    (0x24C2, 'M', u'm'),\n    (0x24C3, 'M', u'n'),\n    (0x24C4, 'M', u'o'),\n    (0x24C5, 'M', u'p'),\n    (0x24C6, 'M', u'q'),\n    (0x24C7, 'M', u'r'),\n    (0x24C8, 'M', u's'),\n    (0x24C9, 'M', u't'),\n    (0x24CA, 'M', u'u'),\n    (0x24CB, 'M', u'v'),\n    (0x24CC, 'M', u'w'),\n    (0x24CD, 'M', u'x'),\n    (0x24CE, 'M', u'y'),\n    (0x24CF, 'M', u'z'),\n    (0x24D0, 'M', u'a'),\n    (0x24D1, 'M', u'b'),\n    (0x24D2, 'M', u'c'),\n    (0x24D3, 'M', u'd'),\n    (0x24D4, 'M', u'e'),\n    (0x24D5, 'M', u'f'),\n    (0x24D6, 'M', u'g'),\n    (0x24D7, 'M', u'h'),\n    (0x24D8, 'M', u'i'),\n    (0x24D9, 'M', u'j'),\n    (0x24DA, 'M', u'k'),\n    (0x24DB, 'M', u'l'),\n    (0x24DC, 'M', u'm'),\n    (0x24DD, 'M', u'n'),\n    (0x24DE, 'M', u'o'),\n    (0x24DF, 'M', u'p'),\n    (0x24E0, 'M', u'q'),\n    (0x24E1, 'M', u'r'),\n    (0x24E2, 'M', u's'),\n    (0x24E3, 'M', u't'),\n    (0x24E4, 'M', u'u'),\n    (0x24E5, 'M', u'v'),\n    (0x24E6, 'M', u'w'),\n    (0x24E7, 'M', u'x'),\n    (0x24E8, 'M', u'y'),\n    (0x24E9, 'M', u'z'),\n    (0x24EA, 'M', u'0'),\n    (0x24EB, 'V'),\n    (0x2A0C, 'M', u'∫∫∫∫'),\n    (0x2A0D, 'V'),\n    (0x2A74, '3', u'::='),\n    (0x2A75, '3', u'=='),\n    (0x2A76, '3', u'==='),\n    (0x2A77, 'V'),\n    (0x2ADC, 'M', u'⫝̸'),\n    (0x2ADD, 'V'),\n    (0x2B74, 'X'),\n    (0x2B76, 'V'),\n    (0x2B96, 'X'),\n    (0x2B97, 'V'),\n    (0x2C00, 'M', u'ⰰ'),\n    (0x2C01, 'M', u'ⰱ'),\n    (0x2C02, 'M', u'ⰲ'),\n    (0x2C03, 'M', u'ⰳ'),\n    (0x2C04, 'M', u'ⰴ'),\n    (0x2C05, 'M', u'ⰵ'),\n    (0x2C06, 'M', u'ⰶ'),\n    (0x2C07, 'M', u'ⰷ'),\n    (0x2C08, 'M', u'ⰸ'),\n    (0x2C09, 'M', u'ⰹ'),\n    (0x2C0A, 'M', u'ⰺ'),\n    (0x2C0B, 'M', u'ⰻ'),\n    (0x2C0C, 'M', u'ⰼ'),\n    (0x2C0D, 'M', u'ⰽ'),\n    (0x2C0E, 'M', u'ⰾ'),\n    (0x2C0F, 'M', u'ⰿ'),\n    (0x2C10, 'M', u'ⱀ'),\n    (0x2C11, 'M', u'ⱁ'),\n    (0x2C12, 'M', u'ⱂ'),\n    (0x2C13, 'M', u'ⱃ'),\n    (0x2C14, 'M', u'ⱄ'),\n    (0x2C15, 'M', u'ⱅ'),\n    (0x2C16, 'M', u'ⱆ'),\n    (0x2C17, 'M', u'ⱇ'),\n    (0x2C18, 'M', u'ⱈ'),\n    (0x2C19, 'M', u'ⱉ'),\n    (0x2C1A, 'M', u'ⱊ'),\n    (0x2C1B, 'M', u'ⱋ'),\n    (0x2C1C, 'M', u'ⱌ'),\n    (0x2C1D, 'M', u'ⱍ'),\n    (0x2C1E, 'M', u'ⱎ'),\n    (0x2C1F, 'M', u'ⱏ'),\n    (0x2C20, 'M', u'ⱐ'),\n    (0x2C21, 'M', u'ⱑ'),\n    (0x2C22, 'M', u'ⱒ'),\n    (0x2C23, 'M', u'ⱓ'),\n    (0x2C24, 'M', u'ⱔ'),\n    (0x2C25, 'M', u'ⱕ'),\n    ]\n\ndef _seg_25():\n    return [\n    (0x2C26, 'M', u'ⱖ'),\n    (0x2C27, 'M', u'ⱗ'),\n    (0x2C28, 'M', u'ⱘ'),\n    (0x2C29, 'M', u'ⱙ'),\n    (0x2C2A, 'M', u'ⱚ'),\n    (0x2C2B, 'M', u'ⱛ'),\n    (0x2C2C, 'M', u'ⱜ'),\n    (0x2C2D, 'M', u'ⱝ'),\n    (0x2C2E, 'M', u'ⱞ'),\n    (0x2C2F, 'X'),\n    (0x2C30, 'V'),\n    (0x2C5F, 'X'),\n    (0x2C60, 'M', u'ⱡ'),\n    (0x2C61, 'V'),\n    (0x2C62, 'M', u'ɫ'),\n    (0x2C63, 'M', u'ᵽ'),\n    (0x2C64, 'M', u'ɽ'),\n    (0x2C65, 'V'),\n    (0x2C67, 'M', u'ⱨ'),\n    (0x2C68, 'V'),\n    (0x2C69, 'M', u'ⱪ'),\n    (0x2C6A, 'V'),\n    (0x2C6B, 'M', u'ⱬ'),\n    (0x2C6C, 'V'),\n    (0x2C6D, 'M', u'ɑ'),\n    (0x2C6E, 'M', u'ɱ'),\n    (0x2C6F, 'M', u'ɐ'),\n    (0x2C70, 'M', u'ɒ'),\n    (0x2C71, 'V'),\n    (0x2C72, 'M', u'ⱳ'),\n    (0x2C73, 'V'),\n    (0x2C75, 'M', u'ⱶ'),\n    (0x2C76, 'V'),\n    (0x2C7C, 'M', u'j'),\n    (0x2C7D, 'M', u'v'),\n    (0x2C7E, 'M', u'ȿ'),\n    (0x2C7F, 'M', u'ɀ'),\n    (0x2C80, 'M', u'ⲁ'),\n    (0x2C81, 'V'),\n    (0x2C82, 'M', u'ⲃ'),\n    (0x2C83, 'V'),\n    (0x2C84, 'M', u'ⲅ'),\n    (0x2C85, 'V'),\n    (0x2C86, 'M', u'ⲇ'),\n    (0x2C87, 'V'),\n    (0x2C88, 'M', u'ⲉ'),\n    (0x2C89, 'V'),\n    (0x2C8A, 'M', u'ⲋ'),\n    (0x2C8B, 'V'),\n    (0x2C8C, 'M', u'ⲍ'),\n    (0x2C8D, 'V'),\n    (0x2C8E, 'M', u'ⲏ'),\n    (0x2C8F, 'V'),\n    (0x2C90, 'M', u'ⲑ'),\n    (0x2C91, 'V'),\n    (0x2C92, 'M', u'ⲓ'),\n    (0x2C93, 'V'),\n    (0x2C94, 'M', u'ⲕ'),\n    (0x2C95, 'V'),\n    (0x2C96, 'M', u'ⲗ'),\n    (0x2C97, 'V'),\n    (0x2C98, 'M', u'ⲙ'),\n    (0x2C99, 'V'),\n    (0x2C9A, 'M', u'ⲛ'),\n    (0x2C9B, 'V'),\n    (0x2C9C, 'M', u'ⲝ'),\n    (0x2C9D, 'V'),\n    (0x2C9E, 'M', u'ⲟ'),\n    (0x2C9F, 'V'),\n    (0x2CA0, 'M', u'ⲡ'),\n    (0x2CA1, 'V'),\n    (0x2CA2, 'M', u'ⲣ'),\n    (0x2CA3, 'V'),\n    (0x2CA4, 'M', u'ⲥ'),\n    (0x2CA5, 'V'),\n    (0x2CA6, 'M', u'ⲧ'),\n    (0x2CA7, 'V'),\n    (0x2CA8, 'M', u'ⲩ'),\n    (0x2CA9, 'V'),\n    (0x2CAA, 'M', u'ⲫ'),\n    (0x2CAB, 'V'),\n    (0x2CAC, 'M', u'ⲭ'),\n    (0x2CAD, 'V'),\n    (0x2CAE, 'M', u'ⲯ'),\n    (0x2CAF, 'V'),\n    (0x2CB0, 'M', u'ⲱ'),\n    (0x2CB1, 'V'),\n    (0x2CB2, 'M', u'ⲳ'),\n    (0x2CB3, 'V'),\n    (0x2CB4, 'M', u'ⲵ'),\n    (0x2CB5, 'V'),\n    (0x2CB6, 'M', u'ⲷ'),\n    (0x2CB7, 'V'),\n    (0x2CB8, 'M', u'ⲹ'),\n    (0x2CB9, 'V'),\n    (0x2CBA, 'M', u'ⲻ'),\n    (0x2CBB, 'V'),\n    (0x2CBC, 'M', u'ⲽ'),\n    (0x2CBD, 'V'),\n    (0x2CBE, 'M', u'ⲿ'),\n    ]\n\ndef _seg_26():\n    return [\n    (0x2CBF, 'V'),\n    (0x2CC0, 'M', u'ⳁ'),\n    (0x2CC1, 'V'),\n    (0x2CC2, 'M', u'ⳃ'),\n    (0x2CC3, 'V'),\n    (0x2CC4, 'M', u'ⳅ'),\n    (0x2CC5, 'V'),\n    (0x2CC6, 'M', u'ⳇ'),\n    (0x2CC7, 'V'),\n    (0x2CC8, 'M', u'ⳉ'),\n    (0x2CC9, 'V'),\n    (0x2CCA, 'M', u'ⳋ'),\n    (0x2CCB, 'V'),\n    (0x2CCC, 'M', u'ⳍ'),\n    (0x2CCD, 'V'),\n    (0x2CCE, 'M', u'ⳏ'),\n    (0x2CCF, 'V'),\n    (0x2CD0, 'M', u'ⳑ'),\n    (0x2CD1, 'V'),\n    (0x2CD2, 'M', u'ⳓ'),\n    (0x2CD3, 'V'),\n    (0x2CD4, 'M', u'ⳕ'),\n    (0x2CD5, 'V'),\n    (0x2CD6, 'M', u'ⳗ'),\n    (0x2CD7, 'V'),\n    (0x2CD8, 'M', u'ⳙ'),\n    (0x2CD9, 'V'),\n    (0x2CDA, 'M', u'ⳛ'),\n    (0x2CDB, 'V'),\n    (0x2CDC, 'M', u'ⳝ'),\n    (0x2CDD, 'V'),\n    (0x2CDE, 'M', u'ⳟ'),\n    (0x2CDF, 'V'),\n    (0x2CE0, 'M', u'ⳡ'),\n    (0x2CE1, 'V'),\n    (0x2CE2, 'M', u'ⳣ'),\n    (0x2CE3, 'V'),\n    (0x2CEB, 'M', u'ⳬ'),\n    (0x2CEC, 'V'),\n    (0x2CED, 'M', u'ⳮ'),\n    (0x2CEE, 'V'),\n    (0x2CF2, 'M', u'ⳳ'),\n    (0x2CF3, 'V'),\n    (0x2CF4, 'X'),\n    (0x2CF9, 'V'),\n    (0x2D26, 'X'),\n    (0x2D27, 'V'),\n    (0x2D28, 'X'),\n    (0x2D2D, 'V'),\n    (0x2D2E, 'X'),\n    (0x2D30, 'V'),\n    (0x2D68, 'X'),\n    (0x2D6F, 'M', u'ⵡ'),\n    (0x2D70, 'V'),\n    (0x2D71, 'X'),\n    (0x2D7F, 'V'),\n    (0x2D97, 'X'),\n    (0x2DA0, 'V'),\n    (0x2DA7, 'X'),\n    (0x2DA8, 'V'),\n    (0x2DAF, 'X'),\n    (0x2DB0, 'V'),\n    (0x2DB7, 'X'),\n    (0x2DB8, 'V'),\n    (0x2DBF, 'X'),\n    (0x2DC0, 'V'),\n    (0x2DC7, 'X'),\n    (0x2DC8, 'V'),\n    (0x2DCF, 'X'),\n    (0x2DD0, 'V'),\n    (0x2DD7, 'X'),\n    (0x2DD8, 'V'),\n    (0x2DDF, 'X'),\n    (0x2DE0, 'V'),\n    (0x2E53, 'X'),\n    (0x2E80, 'V'),\n    (0x2E9A, 'X'),\n    (0x2E9B, 'V'),\n    (0x2E9F, 'M', u'母'),\n    (0x2EA0, 'V'),\n    (0x2EF3, 'M', u'龟'),\n    (0x2EF4, 'X'),\n    (0x2F00, 'M', u'一'),\n    (0x2F01, 'M', u'丨'),\n    (0x2F02, 'M', u'丶'),\n    (0x2F03, 'M', u'丿'),\n    (0x2F04, 'M', u'乙'),\n    (0x2F05, 'M', u'亅'),\n    (0x2F06, 'M', u'二'),\n    (0x2F07, 'M', u'亠'),\n    (0x2F08, 'M', u'人'),\n    (0x2F09, 'M', u'儿'),\n    (0x2F0A, 'M', u'入'),\n    (0x2F0B, 'M', u'八'),\n    (0x2F0C, 'M', u'冂'),\n    (0x2F0D, 'M', u'冖'),\n    (0x2F0E, 'M', u'冫'),\n    (0x2F0F, 'M', u'几'),\n    (0x2F10, 'M', u'凵'),\n    (0x2F11, 'M', u'刀'),\n    ]\n\ndef _seg_27():\n    return [\n    (0x2F12, 'M', u'力'),\n    (0x2F13, 'M', u'勹'),\n    (0x2F14, 'M', u'匕'),\n    (0x2F15, 'M', u'匚'),\n    (0x2F16, 'M', u'匸'),\n    (0x2F17, 'M', u'十'),\n    (0x2F18, 'M', u'卜'),\n    (0x2F19, 'M', u'卩'),\n    (0x2F1A, 'M', u'厂'),\n    (0x2F1B, 'M', u'厶'),\n    (0x2F1C, 'M', u'又'),\n    (0x2F1D, 'M', u'口'),\n    (0x2F1E, 'M', u'囗'),\n    (0x2F1F, 'M', u'土'),\n    (0x2F20, 'M', u'士'),\n    (0x2F21, 'M', u'夂'),\n    (0x2F22, 'M', u'夊'),\n    (0x2F23, 'M', u'夕'),\n    (0x2F24, 'M', u'大'),\n    (0x2F25, 'M', u'女'),\n    (0x2F26, 'M', u'子'),\n    (0x2F27, 'M', u'宀'),\n    (0x2F28, 'M', u'寸'),\n    (0x2F29, 'M', u'小'),\n    (0x2F2A, 'M', u'尢'),\n    (0x2F2B, 'M', u'尸'),\n    (0x2F2C, 'M', u'屮'),\n    (0x2F2D, 'M', u'山'),\n    (0x2F2E, 'M', u'巛'),\n    (0x2F2F, 'M', u'工'),\n    (0x2F30, 'M', u'己'),\n    (0x2F31, 'M', u'巾'),\n    (0x2F32, 'M', u'干'),\n    (0x2F33, 'M', u'幺'),\n    (0x2F34, 'M', u'广'),\n    (0x2F35, 'M', u'廴'),\n    (0x2F36, 'M', u'廾'),\n    (0x2F37, 'M', u'弋'),\n    (0x2F38, 'M', u'弓'),\n    (0x2F39, 'M', u'彐'),\n    (0x2F3A, 'M', u'彡'),\n    (0x2F3B, 'M', u'彳'),\n    (0x2F3C, 'M', u'心'),\n    (0x2F3D, 'M', u'戈'),\n    (0x2F3E, 'M', u'戶'),\n    (0x2F3F, 'M', u'手'),\n    (0x2F40, 'M', u'支'),\n    (0x2F41, 'M', u'攴'),\n    (0x2F42, 'M', u'文'),\n    (0x2F43, 'M', u'斗'),\n    (0x2F44, 'M', u'斤'),\n    (0x2F45, 'M', u'方'),\n    (0x2F46, 'M', u'无'),\n    (0x2F47, 'M', u'日'),\n    (0x2F48, 'M', u'曰'),\n    (0x2F49, 'M', u'月'),\n    (0x2F4A, 'M', u'木'),\n    (0x2F4B, 'M', u'欠'),\n    (0x2F4C, 'M', u'止'),\n    (0x2F4D, 'M', u'歹'),\n    (0x2F4E, 'M', u'殳'),\n    (0x2F4F, 'M', u'毋'),\n    (0x2F50, 'M', u'比'),\n    (0x2F51, 'M', u'毛'),\n    (0x2F52, 'M', u'氏'),\n    (0x2F53, 'M', u'气'),\n    (0x2F54, 'M', u'水'),\n    (0x2F55, 'M', u'火'),\n    (0x2F56, 'M', u'爪'),\n    (0x2F57, 'M', u'父'),\n    (0x2F58, 'M', u'爻'),\n    (0x2F59, 'M', u'爿'),\n    (0x2F5A, 'M', u'片'),\n    (0x2F5B, 'M', u'牙'),\n    (0x2F5C, 'M', u'牛'),\n    (0x2F5D, 'M', u'犬'),\n    (0x2F5E, 'M', u'玄'),\n    (0x2F5F, 'M', u'玉'),\n    (0x2F60, 'M', u'瓜'),\n    (0x2F61, 'M', u'瓦'),\n    (0x2F62, 'M', u'甘'),\n    (0x2F63, 'M', u'生'),\n    (0x2F64, 'M', u'用'),\n    (0x2F65, 'M', u'田'),\n    (0x2F66, 'M', u'疋'),\n    (0x2F67, 'M', u'疒'),\n    (0x2F68, 'M', u'癶'),\n    (0x2F69, 'M', u'白'),\n    (0x2F6A, 'M', u'皮'),\n    (0x2F6B, 'M', u'皿'),\n    (0x2F6C, 'M', u'目'),\n    (0x2F6D, 'M', u'矛'),\n    (0x2F6E, 'M', u'矢'),\n    (0x2F6F, 'M', u'石'),\n    (0x2F70, 'M', u'示'),\n    (0x2F71, 'M', u'禸'),\n    (0x2F72, 'M', u'禾'),\n    (0x2F73, 'M', u'穴'),\n    (0x2F74, 'M', u'立'),\n    (0x2F75, 'M', u'竹'),\n    ]\n\ndef _seg_28():\n    return [\n    (0x2F76, 'M', u'米'),\n    (0x2F77, 'M', u'糸'),\n    (0x2F78, 'M', u'缶'),\n    (0x2F79, 'M', u'网'),\n    (0x2F7A, 'M', u'羊'),\n    (0x2F7B, 'M', u'羽'),\n    (0x2F7C, 'M', u'老'),\n    (0x2F7D, 'M', u'而'),\n    (0x2F7E, 'M', u'耒'),\n    (0x2F7F, 'M', u'耳'),\n    (0x2F80, 'M', u'聿'),\n    (0x2F81, 'M', u'肉'),\n    (0x2F82, 'M', u'臣'),\n    (0x2F83, 'M', u'自'),\n    (0x2F84, 'M', u'至'),\n    (0x2F85, 'M', u'臼'),\n    (0x2F86, 'M', u'舌'),\n    (0x2F87, 'M', u'舛'),\n    (0x2F88, 'M', u'舟'),\n    (0x2F89, 'M', u'艮'),\n    (0x2F8A, 'M', u'色'),\n    (0x2F8B, 'M', u'艸'),\n    (0x2F8C, 'M', u'虍'),\n    (0x2F8D, 'M', u'虫'),\n    (0x2F8E, 'M', u'血'),\n    (0x2F8F, 'M', u'行'),\n    (0x2F90, 'M', u'衣'),\n    (0x2F91, 'M', u'襾'),\n    (0x2F92, 'M', u'見'),\n    (0x2F93, 'M', u'角'),\n    (0x2F94, 'M', u'言'),\n    (0x2F95, 'M', u'谷'),\n    (0x2F96, 'M', u'豆'),\n    (0x2F97, 'M', u'豕'),\n    (0x2F98, 'M', u'豸'),\n    (0x2F99, 'M', u'貝'),\n    (0x2F9A, 'M', u'赤'),\n    (0x2F9B, 'M', u'走'),\n    (0x2F9C, 'M', u'足'),\n    (0x2F9D, 'M', u'身'),\n    (0x2F9E, 'M', u'車'),\n    (0x2F9F, 'M', u'辛'),\n    (0x2FA0, 'M', u'辰'),\n    (0x2FA1, 'M', u'辵'),\n    (0x2FA2, 'M', u'邑'),\n    (0x2FA3, 'M', u'酉'),\n    (0x2FA4, 'M', u'釆'),\n    (0x2FA5, 'M', u'里'),\n    (0x2FA6, 'M', u'金'),\n    (0x2FA7, 'M', u'長'),\n    (0x2FA8, 'M', u'門'),\n    (0x2FA9, 'M', u'阜'),\n    (0x2FAA, 'M', u'隶'),\n    (0x2FAB, 'M', u'隹'),\n    (0x2FAC, 'M', u'雨'),\n    (0x2FAD, 'M', u'靑'),\n    (0x2FAE, 'M', u'非'),\n    (0x2FAF, 'M', u'面'),\n    (0x2FB0, 'M', u'革'),\n    (0x2FB1, 'M', u'韋'),\n    (0x2FB2, 'M', u'韭'),\n    (0x2FB3, 'M', u'音'),\n    (0x2FB4, 'M', u'頁'),\n    (0x2FB5, 'M', u'風'),\n    (0x2FB6, 'M', u'飛'),\n    (0x2FB7, 'M', u'食'),\n    (0x2FB8, 'M', u'首'),\n    (0x2FB9, 'M', u'香'),\n    (0x2FBA, 'M', u'馬'),\n    (0x2FBB, 'M', u'骨'),\n    (0x2FBC, 'M', u'高'),\n    (0x2FBD, 'M', u'髟'),\n    (0x2FBE, 'M', u'鬥'),\n    (0x2FBF, 'M', u'鬯'),\n    (0x2FC0, 'M', u'鬲'),\n    (0x2FC1, 'M', u'鬼'),\n    (0x2FC2, 'M', u'魚'),\n    (0x2FC3, 'M', u'鳥'),\n    (0x2FC4, 'M', u'鹵'),\n    (0x2FC5, 'M', u'鹿'),\n    (0x2FC6, 'M', u'麥'),\n    (0x2FC7, 'M', u'麻'),\n    (0x2FC8, 'M', u'黃'),\n    (0x2FC9, 'M', u'黍'),\n    (0x2FCA, 'M', u'黑'),\n    (0x2FCB, 'M', u'黹'),\n    (0x2FCC, 'M', u'黽'),\n    (0x2FCD, 'M', u'鼎'),\n    (0x2FCE, 'M', u'鼓'),\n    (0x2FCF, 'M', u'鼠'),\n    (0x2FD0, 'M', u'鼻'),\n    (0x2FD1, 'M', u'齊'),\n    (0x2FD2, 'M', u'齒'),\n    (0x2FD3, 'M', u'龍'),\n    (0x2FD4, 'M', u'龜'),\n    (0x2FD5, 'M', u'龠'),\n    (0x2FD6, 'X'),\n    (0x3000, '3', u' '),\n    (0x3001, 'V'),\n    (0x3002, 'M', u'.'),\n    ]\n\ndef _seg_29():\n    return [\n    (0x3003, 'V'),\n    (0x3036, 'M', u'〒'),\n    (0x3037, 'V'),\n    (0x3038, 'M', u'十'),\n    (0x3039, 'M', u'卄'),\n    (0x303A, 'M', u'卅'),\n    (0x303B, 'V'),\n    (0x3040, 'X'),\n    (0x3041, 'V'),\n    (0x3097, 'X'),\n    (0x3099, 'V'),\n    (0x309B, '3', u' ゙'),\n    (0x309C, '3', u' ゚'),\n    (0x309D, 'V'),\n    (0x309F, 'M', u'より'),\n    (0x30A0, 'V'),\n    (0x30FF, 'M', u'コト'),\n    (0x3100, 'X'),\n    (0x3105, 'V'),\n    (0x3130, 'X'),\n    (0x3131, 'M', u'ᄀ'),\n    (0x3132, 'M', u'ᄁ'),\n    (0x3133, 'M', u'ᆪ'),\n    (0x3134, 'M', u'ᄂ'),\n    (0x3135, 'M', u'ᆬ'),\n    (0x3136, 'M', u'ᆭ'),\n    (0x3137, 'M', u'ᄃ'),\n    (0x3138, 'M', u'ᄄ'),\n    (0x3139, 'M', u'ᄅ'),\n    (0x313A, 'M', u'ᆰ'),\n    (0x313B, 'M', u'ᆱ'),\n    (0x313C, 'M', u'ᆲ'),\n    (0x313D, 'M', u'ᆳ'),\n    (0x313E, 'M', u'ᆴ'),\n    (0x313F, 'M', u'ᆵ'),\n    (0x3140, 'M', u'ᄚ'),\n    (0x3141, 'M', u'ᄆ'),\n    (0x3142, 'M', u'ᄇ'),\n    (0x3143, 'M', u'ᄈ'),\n    (0x3144, 'M', u'ᄡ'),\n    (0x3145, 'M', u'ᄉ'),\n    (0x3146, 'M', u'ᄊ'),\n    (0x3147, 'M', u'ᄋ'),\n    (0x3148, 'M', u'ᄌ'),\n    (0x3149, 'M', u'ᄍ'),\n    (0x314A, 'M', u'ᄎ'),\n    (0x314B, 'M', u'ᄏ'),\n    (0x314C, 'M', u'ᄐ'),\n    (0x314D, 'M', u'ᄑ'),\n    (0x314E, 'M', u'ᄒ'),\n    (0x314F, 'M', u'ᅡ'),\n    (0x3150, 'M', u'ᅢ'),\n    (0x3151, 'M', u'ᅣ'),\n    (0x3152, 'M', u'ᅤ'),\n    (0x3153, 'M', u'ᅥ'),\n    (0x3154, 'M', u'ᅦ'),\n    (0x3155, 'M', u'ᅧ'),\n    (0x3156, 'M', u'ᅨ'),\n    (0x3157, 'M', u'ᅩ'),\n    (0x3158, 'M', u'ᅪ'),\n    (0x3159, 'M', u'ᅫ'),\n    (0x315A, 'M', u'ᅬ'),\n    (0x315B, 'M', u'ᅭ'),\n    (0x315C, 'M', u'ᅮ'),\n    (0x315D, 'M', u'ᅯ'),\n    (0x315E, 'M', u'ᅰ'),\n    (0x315F, 'M', u'ᅱ'),\n    (0x3160, 'M', u'ᅲ'),\n    (0x3161, 'M', u'ᅳ'),\n    (0x3162, 'M', u'ᅴ'),\n    (0x3163, 'M', u'ᅵ'),\n    (0x3164, 'X'),\n    (0x3165, 'M', u'ᄔ'),\n    (0x3166, 'M', u'ᄕ'),\n    (0x3167, 'M', u'ᇇ'),\n    (0x3168, 'M', u'ᇈ'),\n    (0x3169, 'M', u'ᇌ'),\n    (0x316A, 'M', u'ᇎ'),\n    (0x316B, 'M', u'ᇓ'),\n    (0x316C, 'M', u'ᇗ'),\n    (0x316D, 'M', u'ᇙ'),\n    (0x316E, 'M', u'ᄜ'),\n    (0x316F, 'M', u'ᇝ'),\n    (0x3170, 'M', u'ᇟ'),\n    (0x3171, 'M', u'ᄝ'),\n    (0x3172, 'M', u'ᄞ'),\n    (0x3173, 'M', u'ᄠ'),\n    (0x3174, 'M', u'ᄢ'),\n    (0x3175, 'M', u'ᄣ'),\n    (0x3176, 'M', u'ᄧ'),\n    (0x3177, 'M', u'ᄩ'),\n    (0x3178, 'M', u'ᄫ'),\n    (0x3179, 'M', u'ᄬ'),\n    (0x317A, 'M', u'ᄭ'),\n    (0x317B, 'M', u'ᄮ'),\n    (0x317C, 'M', u'ᄯ'),\n    (0x317D, 'M', u'ᄲ'),\n    (0x317E, 'M', u'ᄶ'),\n    (0x317F, 'M', u'ᅀ'),\n    (0x3180, 'M', u'ᅇ'),\n    ]\n\ndef _seg_30():\n    return [\n    (0x3181, 'M', u'ᅌ'),\n    (0x3182, 'M', u'ᇱ'),\n    (0x3183, 'M', u'ᇲ'),\n    (0x3184, 'M', u'ᅗ'),\n    (0x3185, 'M', u'ᅘ'),\n    (0x3186, 'M', u'ᅙ'),\n    (0x3187, 'M', u'ᆄ'),\n    (0x3188, 'M', u'ᆅ'),\n    (0x3189, 'M', u'ᆈ'),\n    (0x318A, 'M', u'ᆑ'),\n    (0x318B, 'M', u'ᆒ'),\n    (0x318C, 'M', u'ᆔ'),\n    (0x318D, 'M', u'ᆞ'),\n    (0x318E, 'M', u'ᆡ'),\n    (0x318F, 'X'),\n    (0x3190, 'V'),\n    (0x3192, 'M', u'一'),\n    (0x3193, 'M', u'二'),\n    (0x3194, 'M', u'三'),\n    (0x3195, 'M', u'四'),\n    (0x3196, 'M', u'上'),\n    (0x3197, 'M', u'中'),\n    (0x3198, 'M', u'下'),\n    (0x3199, 'M', u'甲'),\n    (0x319A, 'M', u'乙'),\n    (0x319B, 'M', u'丙'),\n    (0x319C, 'M', u'丁'),\n    (0x319D, 'M', u'天'),\n    (0x319E, 'M', u'地'),\n    (0x319F, 'M', u'人'),\n    (0x31A0, 'V'),\n    (0x31E4, 'X'),\n    (0x31F0, 'V'),\n    (0x3200, '3', u'(ᄀ)'),\n    (0x3201, '3', u'(ᄂ)'),\n    (0x3202, '3', u'(ᄃ)'),\n    (0x3203, '3', u'(ᄅ)'),\n    (0x3204, '3', u'(ᄆ)'),\n    (0x3205, '3', u'(ᄇ)'),\n    (0x3206, '3', u'(ᄉ)'),\n    (0x3207, '3', u'(ᄋ)'),\n    (0x3208, '3', u'(ᄌ)'),\n    (0x3209, '3', u'(ᄎ)'),\n    (0x320A, '3', u'(ᄏ)'),\n    (0x320B, '3', u'(ᄐ)'),\n    (0x320C, '3', u'(ᄑ)'),\n    (0x320D, '3', u'(ᄒ)'),\n    (0x320E, '3', u'(가)'),\n    (0x320F, '3', u'(나)'),\n    (0x3210, '3', u'(다)'),\n    (0x3211, '3', u'(라)'),\n    (0x3212, '3', u'(마)'),\n    (0x3213, '3', u'(바)'),\n    (0x3214, '3', u'(사)'),\n    (0x3215, '3', u'(아)'),\n    (0x3216, '3', u'(자)'),\n    (0x3217, '3', u'(차)'),\n    (0x3218, '3', u'(카)'),\n    (0x3219, '3', u'(타)'),\n    (0x321A, '3', u'(파)'),\n    (0x321B, '3', u'(하)'),\n    (0x321C, '3', u'(주)'),\n    (0x321D, '3', u'(오전)'),\n    (0x321E, '3', u'(오후)'),\n    (0x321F, 'X'),\n    (0x3220, '3', u'(一)'),\n    (0x3221, '3', u'(二)'),\n    (0x3222, '3', u'(三)'),\n    (0x3223, '3', u'(四)'),\n    (0x3224, '3', u'(五)'),\n    (0x3225, '3', u'(六)'),\n    (0x3226, '3', u'(七)'),\n    (0x3227, '3', u'(八)'),\n    (0x3228, '3', u'(九)'),\n    (0x3229, '3', u'(十)'),\n    (0x322A, '3', u'(月)'),\n    (0x322B, '3', u'(火)'),\n    (0x322C, '3', u'(水)'),\n    (0x322D, '3', u'(木)'),\n    (0x322E, '3', u'(金)'),\n    (0x322F, '3', u'(土)'),\n    (0x3230, '3', u'(日)'),\n    (0x3231, '3', u'(株)'),\n    (0x3232, '3', u'(有)'),\n    (0x3233, '3', u'(社)'),\n    (0x3234, '3', u'(名)'),\n    (0x3235, '3', u'(特)'),\n    (0x3236, '3', u'(財)'),\n    (0x3237, '3', u'(祝)'),\n    (0x3238, '3', u'(労)'),\n    (0x3239, '3', u'(代)'),\n    (0x323A, '3', u'(呼)'),\n    (0x323B, '3', u'(学)'),\n    (0x323C, '3', u'(監)'),\n    (0x323D, '3', u'(企)'),\n    (0x323E, '3', u'(資)'),\n    (0x323F, '3', u'(協)'),\n    (0x3240, '3', u'(祭)'),\n    (0x3241, '3', u'(休)'),\n    (0x3242, '3', u'(自)'),\n    ]\n\ndef _seg_31():\n    return [\n    (0x3243, '3', u'(至)'),\n    (0x3244, 'M', u'問'),\n    (0x3245, 'M', u'幼'),\n    (0x3246, 'M', u'文'),\n    (0x3247, 'M', u'箏'),\n    (0x3248, 'V'),\n    (0x3250, 'M', u'pte'),\n    (0x3251, 'M', u'21'),\n    (0x3252, 'M', u'22'),\n    (0x3253, 'M', u'23'),\n    (0x3254, 'M', u'24'),\n    (0x3255, 'M', u'25'),\n    (0x3256, 'M', u'26'),\n    (0x3257, 'M', u'27'),\n    (0x3258, 'M', u'28'),\n    (0x3259, 'M', u'29'),\n    (0x325A, 'M', u'30'),\n    (0x325B, 'M', u'31'),\n    (0x325C, 'M', u'32'),\n    (0x325D, 'M', u'33'),\n    (0x325E, 'M', u'34'),\n    (0x325F, 'M', u'35'),\n    (0x3260, 'M', u'ᄀ'),\n    (0x3261, 'M', u'ᄂ'),\n    (0x3262, 'M', u'ᄃ'),\n    (0x3263, 'M', u'ᄅ'),\n    (0x3264, 'M', u'ᄆ'),\n    (0x3265, 'M', u'ᄇ'),\n    (0x3266, 'M', u'ᄉ'),\n    (0x3267, 'M', u'ᄋ'),\n    (0x3268, 'M', u'ᄌ'),\n    (0x3269, 'M', u'ᄎ'),\n    (0x326A, 'M', u'ᄏ'),\n    (0x326B, 'M', u'ᄐ'),\n    (0x326C, 'M', u'ᄑ'),\n    (0x326D, 'M', u'ᄒ'),\n    (0x326E, 'M', u'가'),\n    (0x326F, 'M', u'나'),\n    (0x3270, 'M', u'다'),\n    (0x3271, 'M', u'라'),\n    (0x3272, 'M', u'마'),\n    (0x3273, 'M', u'바'),\n    (0x3274, 'M', u'사'),\n    (0x3275, 'M', u'아'),\n    (0x3276, 'M', u'자'),\n    (0x3277, 'M', u'차'),\n    (0x3278, 'M', u'카'),\n    (0x3279, 'M', u'타'),\n    (0x327A, 'M', u'파'),\n    (0x327B, 'M', u'하'),\n    (0x327C, 'M', u'참고'),\n    (0x327D, 'M', u'주의'),\n    (0x327E, 'M', u'우'),\n    (0x327F, 'V'),\n    (0x3280, 'M', u'一'),\n    (0x3281, 'M', u'二'),\n    (0x3282, 'M', u'三'),\n    (0x3283, 'M', u'四'),\n    (0x3284, 'M', u'五'),\n    (0x3285, 'M', u'六'),\n    (0x3286, 'M', u'七'),\n    (0x3287, 'M', u'八'),\n    (0x3288, 'M', u'九'),\n    (0x3289, 'M', u'十'),\n    (0x328A, 'M', u'月'),\n    (0x328B, 'M', u'火'),\n    (0x328C, 'M', u'水'),\n    (0x328D, 'M', u'木'),\n    (0x328E, 'M', u'金'),\n    (0x328F, 'M', u'土'),\n    (0x3290, 'M', u'日'),\n    (0x3291, 'M', u'株'),\n    (0x3292, 'M', u'有'),\n    (0x3293, 'M', u'社'),\n    (0x3294, 'M', u'名'),\n    (0x3295, 'M', u'特'),\n    (0x3296, 'M', u'財'),\n    (0x3297, 'M', u'祝'),\n    (0x3298, 'M', u'労'),\n    (0x3299, 'M', u'秘'),\n    (0x329A, 'M', u'男'),\n    (0x329B, 'M', u'女'),\n    (0x329C, 'M', u'適'),\n    (0x329D, 'M', u'優'),\n    (0x329E, 'M', u'印'),\n    (0x329F, 'M', u'注'),\n    (0x32A0, 'M', u'項'),\n    (0x32A1, 'M', u'休'),\n    (0x32A2, 'M', u'写'),\n    (0x32A3, 'M', u'正'),\n    (0x32A4, 'M', u'上'),\n    (0x32A5, 'M', u'中'),\n    (0x32A6, 'M', u'下'),\n    (0x32A7, 'M', u'左'),\n    (0x32A8, 'M', u'右'),\n    (0x32A9, 'M', u'医'),\n    (0x32AA, 'M', u'宗'),\n    (0x32AB, 'M', u'学'),\n    (0x32AC, 'M', u'監'),\n    (0x32AD, 'M', u'企'),\n    ]\n\ndef _seg_32():\n    return [\n    (0x32AE, 'M', u'資'),\n    (0x32AF, 'M', u'協'),\n    (0x32B0, 'M', u'夜'),\n    (0x32B1, 'M', u'36'),\n    (0x32B2, 'M', u'37'),\n    (0x32B3, 'M', u'38'),\n    (0x32B4, 'M', u'39'),\n    (0x32B5, 'M', u'40'),\n    (0x32B6, 'M', u'41'),\n    (0x32B7, 'M', u'42'),\n    (0x32B8, 'M', u'43'),\n    (0x32B9, 'M', u'44'),\n    (0x32BA, 'M', u'45'),\n    (0x32BB, 'M', u'46'),\n    (0x32BC, 'M', u'47'),\n    (0x32BD, 'M', u'48'),\n    (0x32BE, 'M', u'49'),\n    (0x32BF, 'M', u'50'),\n    (0x32C0, 'M', u'1月'),\n    (0x32C1, 'M', u'2月'),\n    (0x32C2, 'M', u'3月'),\n    (0x32C3, 'M', u'4月'),\n    (0x32C4, 'M', u'5月'),\n    (0x32C5, 'M', u'6月'),\n    (0x32C6, 'M', u'7月'),\n    (0x32C7, 'M', u'8月'),\n    (0x32C8, 'M', u'9月'),\n    (0x32C9, 'M', u'10月'),\n    (0x32CA, 'M', u'11月'),\n    (0x32CB, 'M', u'12月'),\n    (0x32CC, 'M', u'hg'),\n    (0x32CD, 'M', u'erg'),\n    (0x32CE, 'M', u'ev'),\n    (0x32CF, 'M', u'ltd'),\n    (0x32D0, 'M', u'ア'),\n    (0x32D1, 'M', u'イ'),\n    (0x32D2, 'M', u'ウ'),\n    (0x32D3, 'M', u'エ'),\n    (0x32D4, 'M', u'オ'),\n    (0x32D5, 'M', u'カ'),\n    (0x32D6, 'M', u'キ'),\n    (0x32D7, 'M', u'ク'),\n    (0x32D8, 'M', u'ケ'),\n    (0x32D9, 'M', u'コ'),\n    (0x32DA, 'M', u'サ'),\n    (0x32DB, 'M', u'シ'),\n    (0x32DC, 'M', u'ス'),\n    (0x32DD, 'M', u'セ'),\n    (0x32DE, 'M', u'ソ'),\n    (0x32DF, 'M', u'タ'),\n    (0x32E0, 'M', u'チ'),\n    (0x32E1, 'M', u'ツ'),\n    (0x32E2, 'M', u'テ'),\n    (0x32E3, 'M', u'ト'),\n    (0x32E4, 'M', u'ナ'),\n    (0x32E5, 'M', u'ニ'),\n    (0x32E6, 'M', u'ヌ'),\n    (0x32E7, 'M', u'ネ'),\n    (0x32E8, 'M', u'ノ'),\n    (0x32E9, 'M', u'ハ'),\n    (0x32EA, 'M', u'ヒ'),\n    (0x32EB, 'M', u'フ'),\n    (0x32EC, 'M', u'ヘ'),\n    (0x32ED, 'M', u'ホ'),\n    (0x32EE, 'M', u'マ'),\n    (0x32EF, 'M', u'ミ'),\n    (0x32F0, 'M', u'ム'),\n    (0x32F1, 'M', u'メ'),\n    (0x32F2, 'M', u'モ'),\n    (0x32F3, 'M', u'ヤ'),\n    (0x32F4, 'M', u'ユ'),\n    (0x32F5, 'M', u'ヨ'),\n    (0x32F6, 'M', u'ラ'),\n    (0x32F7, 'M', u'リ'),\n    (0x32F8, 'M', u'ル'),\n    (0x32F9, 'M', u'レ'),\n    (0x32FA, 'M', u'ロ'),\n    (0x32FB, 'M', u'ワ'),\n    (0x32FC, 'M', u'ヰ'),\n    (0x32FD, 'M', u'ヱ'),\n    (0x32FE, 'M', u'ヲ'),\n    (0x32FF, 'M', u'令和'),\n    (0x3300, 'M', u'アパート'),\n    (0x3301, 'M', u'アルファ'),\n    (0x3302, 'M', u'アンペア'),\n    (0x3303, 'M', u'アール'),\n    (0x3304, 'M', u'イニング'),\n    (0x3305, 'M', u'インチ'),\n    (0x3306, 'M', u'ウォン'),\n    (0x3307, 'M', u'エスクード'),\n    (0x3308, 'M', u'エーカー'),\n    (0x3309, 'M', u'オンス'),\n    (0x330A, 'M', u'オーム'),\n    (0x330B, 'M', u'カイリ'),\n    (0x330C, 'M', u'カラット'),\n    (0x330D, 'M', u'カロリー'),\n    (0x330E, 'M', u'ガロン'),\n    (0x330F, 'M', u'ガンマ'),\n    (0x3310, 'M', u'ギガ'),\n    (0x3311, 'M', u'ギニー'),\n    ]\n\ndef _seg_33():\n    return [\n    (0x3312, 'M', u'キュリー'),\n    (0x3313, 'M', u'ギルダー'),\n    (0x3314, 'M', u'キロ'),\n    (0x3315, 'M', u'キログラム'),\n    (0x3316, 'M', u'キロメートル'),\n    (0x3317, 'M', u'キロワット'),\n    (0x3318, 'M', u'グラム'),\n    (0x3319, 'M', u'グラムトン'),\n    (0x331A, 'M', u'クルゼイロ'),\n    (0x331B, 'M', u'クローネ'),\n    (0x331C, 'M', u'ケース'),\n    (0x331D, 'M', u'コルナ'),\n    (0x331E, 'M', u'コーポ'),\n    (0x331F, 'M', u'サイクル'),\n    (0x3320, 'M', u'サンチーム'),\n    (0x3321, 'M', u'シリング'),\n    (0x3322, 'M', u'センチ'),\n    (0x3323, 'M', u'セント'),\n    (0x3324, 'M', u'ダース'),\n    (0x3325, 'M', u'デシ'),\n    (0x3326, 'M', u'ドル'),\n    (0x3327, 'M', u'トン'),\n    (0x3328, 'M', u'ナノ'),\n    (0x3329, 'M', u'ノット'),\n    (0x332A, 'M', u'ハイツ'),\n    (0x332B, 'M', u'パーセント'),\n    (0x332C, 'M', u'パーツ'),\n    (0x332D, 'M', u'バーレル'),\n    (0x332E, 'M', u'ピアストル'),\n    (0x332F, 'M', u'ピクル'),\n    (0x3330, 'M', u'ピコ'),\n    (0x3331, 'M', u'ビル'),\n    (0x3332, 'M', u'ファラッド'),\n    (0x3333, 'M', u'フィート'),\n    (0x3334, 'M', u'ブッシェル'),\n    (0x3335, 'M', u'フラン'),\n    (0x3336, 'M', u'ヘクタール'),\n    (0x3337, 'M', u'ペソ'),\n    (0x3338, 'M', u'ペニヒ'),\n    (0x3339, 'M', u'ヘルツ'),\n    (0x333A, 'M', u'ペンス'),\n    (0x333B, 'M', u'ページ'),\n    (0x333C, 'M', u'ベータ'),\n    (0x333D, 'M', u'ポイント'),\n    (0x333E, 'M', u'ボルト'),\n    (0x333F, 'M', u'ホン'),\n    (0x3340, 'M', u'ポンド'),\n    (0x3341, 'M', u'ホール'),\n    (0x3342, 'M', u'ホーン'),\n    (0x3343, 'M', u'マイクロ'),\n    (0x3344, 'M', u'マイル'),\n    (0x3345, 'M', u'マッハ'),\n    (0x3346, 'M', u'マルク'),\n    (0x3347, 'M', u'マンション'),\n    (0x3348, 'M', u'ミクロン'),\n    (0x3349, 'M', u'ミリ'),\n    (0x334A, 'M', u'ミリバール'),\n    (0x334B, 'M', u'メガ'),\n    (0x334C, 'M', u'メガトン'),\n    (0x334D, 'M', u'メートル'),\n    (0x334E, 'M', u'ヤード'),\n    (0x334F, 'M', u'ヤール'),\n    (0x3350, 'M', u'ユアン'),\n    (0x3351, 'M', u'リットル'),\n    (0x3352, 'M', u'リラ'),\n    (0x3353, 'M', u'ルピー'),\n    (0x3354, 'M', u'ルーブル'),\n    (0x3355, 'M', u'レム'),\n    (0x3356, 'M', u'レントゲン'),\n    (0x3357, 'M', u'ワット'),\n    (0x3358, 'M', u'0点'),\n    (0x3359, 'M', u'1点'),\n    (0x335A, 'M', u'2点'),\n    (0x335B, 'M', u'3点'),\n    (0x335C, 'M', u'4点'),\n    (0x335D, 'M', u'5点'),\n    (0x335E, 'M', u'6点'),\n    (0x335F, 'M', u'7点'),\n    (0x3360, 'M', u'8点'),\n    (0x3361, 'M', u'9点'),\n    (0x3362, 'M', u'10点'),\n    (0x3363, 'M', u'11点'),\n    (0x3364, 'M', u'12点'),\n    (0x3365, 'M', u'13点'),\n    (0x3366, 'M', u'14点'),\n    (0x3367, 'M', u'15点'),\n    (0x3368, 'M', u'16点'),\n    (0x3369, 'M', u'17点'),\n    (0x336A, 'M', u'18点'),\n    (0x336B, 'M', u'19点'),\n    (0x336C, 'M', u'20点'),\n    (0x336D, 'M', u'21点'),\n    (0x336E, 'M', u'22点'),\n    (0x336F, 'M', u'23点'),\n    (0x3370, 'M', u'24点'),\n    (0x3371, 'M', u'hpa'),\n    (0x3372, 'M', u'da'),\n    (0x3373, 'M', u'au'),\n    (0x3374, 'M', u'bar'),\n    (0x3375, 'M', u'ov'),\n    ]\n\ndef _seg_34():\n    return [\n    (0x3376, 'M', u'pc'),\n    (0x3377, 'M', u'dm'),\n    (0x3378, 'M', u'dm2'),\n    (0x3379, 'M', u'dm3'),\n    (0x337A, 'M', u'iu'),\n    (0x337B, 'M', u'平成'),\n    (0x337C, 'M', u'昭和'),\n    (0x337D, 'M', u'大正'),\n    (0x337E, 'M', u'明治'),\n    (0x337F, 'M', u'株式会社'),\n    (0x3380, 'M', u'pa'),\n    (0x3381, 'M', u'na'),\n    (0x3382, 'M', u'μa'),\n    (0x3383, 'M', u'ma'),\n    (0x3384, 'M', u'ka'),\n    (0x3385, 'M', u'kb'),\n    (0x3386, 'M', u'mb'),\n    (0x3387, 'M', u'gb'),\n    (0x3388, 'M', u'cal'),\n    (0x3389, 'M', u'kcal'),\n    (0x338A, 'M', u'pf'),\n    (0x338B, 'M', u'nf'),\n    (0x338C, 'M', u'μf'),\n    (0x338D, 'M', u'μg'),\n    (0x338E, 'M', u'mg'),\n    (0x338F, 'M', u'kg'),\n    (0x3390, 'M', u'hz'),\n    (0x3391, 'M', u'khz'),\n    (0x3392, 'M', u'mhz'),\n    (0x3393, 'M', u'ghz'),\n    (0x3394, 'M', u'thz'),\n    (0x3395, 'M', u'μl'),\n    (0x3396, 'M', u'ml'),\n    (0x3397, 'M', u'dl'),\n    (0x3398, 'M', u'kl'),\n    (0x3399, 'M', u'fm'),\n    (0x339A, 'M', u'nm'),\n    (0x339B, 'M', u'μm'),\n    (0x339C, 'M', u'mm'),\n    (0x339D, 'M', u'cm'),\n    (0x339E, 'M', u'km'),\n    (0x339F, 'M', u'mm2'),\n    (0x33A0, 'M', u'cm2'),\n    (0x33A1, 'M', u'm2'),\n    (0x33A2, 'M', u'km2'),\n    (0x33A3, 'M', u'mm3'),\n    (0x33A4, 'M', u'cm3'),\n    (0x33A5, 'M', u'm3'),\n    (0x33A6, 'M', u'km3'),\n    (0x33A7, 'M', u'm∕s'),\n    (0x33A8, 'M', u'm∕s2'),\n    (0x33A9, 'M', u'pa'),\n    (0x33AA, 'M', u'kpa'),\n    (0x33AB, 'M', u'mpa'),\n    (0x33AC, 'M', u'gpa'),\n    (0x33AD, 'M', u'rad'),\n    (0x33AE, 'M', u'rad∕s'),\n    (0x33AF, 'M', u'rad∕s2'),\n    (0x33B0, 'M', u'ps'),\n    (0x33B1, 'M', u'ns'),\n    (0x33B2, 'M', u'μs'),\n    (0x33B3, 'M', u'ms'),\n    (0x33B4, 'M', u'pv'),\n    (0x33B5, 'M', u'nv'),\n    (0x33B6, 'M', u'μv'),\n    (0x33B7, 'M', u'mv'),\n    (0x33B8, 'M', u'kv'),\n    (0x33B9, 'M', u'mv'),\n    (0x33BA, 'M', u'pw'),\n    (0x33BB, 'M', u'nw'),\n    (0x33BC, 'M', u'μw'),\n    (0x33BD, 'M', u'mw'),\n    (0x33BE, 'M', u'kw'),\n    (0x33BF, 'M', u'mw'),\n    (0x33C0, 'M', u'kω'),\n    (0x33C1, 'M', u'mω'),\n    (0x33C2, 'X'),\n    (0x33C3, 'M', u'bq'),\n    (0x33C4, 'M', u'cc'),\n    (0x33C5, 'M', u'cd'),\n    (0x33C6, 'M', u'c∕kg'),\n    (0x33C7, 'X'),\n    (0x33C8, 'M', u'db'),\n    (0x33C9, 'M', u'gy'),\n    (0x33CA, 'M', u'ha'),\n    (0x33CB, 'M', u'hp'),\n    (0x33CC, 'M', u'in'),\n    (0x33CD, 'M', u'kk'),\n    (0x33CE, 'M', u'km'),\n    (0x33CF, 'M', u'kt'),\n    (0x33D0, 'M', u'lm'),\n    (0x33D1, 'M', u'ln'),\n    (0x33D2, 'M', u'log'),\n    (0x33D3, 'M', u'lx'),\n    (0x33D4, 'M', u'mb'),\n    (0x33D5, 'M', u'mil'),\n    (0x33D6, 'M', u'mol'),\n    (0x33D7, 'M', u'ph'),\n    (0x33D8, 'X'),\n    (0x33D9, 'M', u'ppm'),\n    ]\n\ndef _seg_35():\n    return [\n    (0x33DA, 'M', u'pr'),\n    (0x33DB, 'M', u'sr'),\n    (0x33DC, 'M', u'sv'),\n    (0x33DD, 'M', u'wb'),\n    (0x33DE, 'M', u'v∕m'),\n    (0x33DF, 'M', u'a∕m'),\n    (0x33E0, 'M', u'1日'),\n    (0x33E1, 'M', u'2日'),\n    (0x33E2, 'M', u'3日'),\n    (0x33E3, 'M', u'4日'),\n    (0x33E4, 'M', u'5日'),\n    (0x33E5, 'M', u'6日'),\n    (0x33E6, 'M', u'7日'),\n    (0x33E7, 'M', u'8日'),\n    (0x33E8, 'M', u'9日'),\n    (0x33E9, 'M', u'10日'),\n    (0x33EA, 'M', u'11日'),\n    (0x33EB, 'M', u'12日'),\n    (0x33EC, 'M', u'13日'),\n    (0x33ED, 'M', u'14日'),\n    (0x33EE, 'M', u'15日'),\n    (0x33EF, 'M', u'16日'),\n    (0x33F0, 'M', u'17日'),\n    (0x33F1, 'M', u'18日'),\n    (0x33F2, 'M', u'19日'),\n    (0x33F3, 'M', u'20日'),\n    (0x33F4, 'M', u'21日'),\n    (0x33F5, 'M', u'22日'),\n    (0x33F6, 'M', u'23日'),\n    (0x33F7, 'M', u'24日'),\n    (0x33F8, 'M', u'25日'),\n    (0x33F9, 'M', u'26日'),\n    (0x33FA, 'M', u'27日'),\n    (0x33FB, 'M', u'28日'),\n    (0x33FC, 'M', u'29日'),\n    (0x33FD, 'M', u'30日'),\n    (0x33FE, 'M', u'31日'),\n    (0x33FF, 'M', u'gal'),\n    (0x3400, 'V'),\n    (0x9FFD, 'X'),\n    (0xA000, 'V'),\n    (0xA48D, 'X'),\n    (0xA490, 'V'),\n    (0xA4C7, 'X'),\n    (0xA4D0, 'V'),\n    (0xA62C, 'X'),\n    (0xA640, 'M', u'ꙁ'),\n    (0xA641, 'V'),\n    (0xA642, 'M', u'ꙃ'),\n    (0xA643, 'V'),\n    (0xA644, 'M', u'ꙅ'),\n    (0xA645, 'V'),\n    (0xA646, 'M', u'ꙇ'),\n    (0xA647, 'V'),\n    (0xA648, 'M', u'ꙉ'),\n    (0xA649, 'V'),\n    (0xA64A, 'M', u'ꙋ'),\n    (0xA64B, 'V'),\n    (0xA64C, 'M', u'ꙍ'),\n    (0xA64D, 'V'),\n    (0xA64E, 'M', u'ꙏ'),\n    (0xA64F, 'V'),\n    (0xA650, 'M', u'ꙑ'),\n    (0xA651, 'V'),\n    (0xA652, 'M', u'ꙓ'),\n    (0xA653, 'V'),\n    (0xA654, 'M', u'ꙕ'),\n    (0xA655, 'V'),\n    (0xA656, 'M', u'ꙗ'),\n    (0xA657, 'V'),\n    (0xA658, 'M', u'ꙙ'),\n    (0xA659, 'V'),\n    (0xA65A, 'M', u'ꙛ'),\n    (0xA65B, 'V'),\n    (0xA65C, 'M', u'ꙝ'),\n    (0xA65D, 'V'),\n    (0xA65E, 'M', u'ꙟ'),\n    (0xA65F, 'V'),\n    (0xA660, 'M', u'ꙡ'),\n    (0xA661, 'V'),\n    (0xA662, 'M', u'ꙣ'),\n    (0xA663, 'V'),\n    (0xA664, 'M', u'ꙥ'),\n    (0xA665, 'V'),\n    (0xA666, 'M', u'ꙧ'),\n    (0xA667, 'V'),\n    (0xA668, 'M', u'ꙩ'),\n    (0xA669, 'V'),\n    (0xA66A, 'M', u'ꙫ'),\n    (0xA66B, 'V'),\n    (0xA66C, 'M', u'ꙭ'),\n    (0xA66D, 'V'),\n    (0xA680, 'M', u'ꚁ'),\n    (0xA681, 'V'),\n    (0xA682, 'M', u'ꚃ'),\n    (0xA683, 'V'),\n    (0xA684, 'M', u'ꚅ'),\n    (0xA685, 'V'),\n    (0xA686, 'M', u'ꚇ'),\n    (0xA687, 'V'),\n    ]\n\ndef _seg_36():\n    return [\n    (0xA688, 'M', u'ꚉ'),\n    (0xA689, 'V'),\n    (0xA68A, 'M', u'ꚋ'),\n    (0xA68B, 'V'),\n    (0xA68C, 'M', u'ꚍ'),\n    (0xA68D, 'V'),\n    (0xA68E, 'M', u'ꚏ'),\n    (0xA68F, 'V'),\n    (0xA690, 'M', u'ꚑ'),\n    (0xA691, 'V'),\n    (0xA692, 'M', u'ꚓ'),\n    (0xA693, 'V'),\n    (0xA694, 'M', u'ꚕ'),\n    (0xA695, 'V'),\n    (0xA696, 'M', u'ꚗ'),\n    (0xA697, 'V'),\n    (0xA698, 'M', u'ꚙ'),\n    (0xA699, 'V'),\n    (0xA69A, 'M', u'ꚛ'),\n    (0xA69B, 'V'),\n    (0xA69C, 'M', u'ъ'),\n    (0xA69D, 'M', u'ь'),\n    (0xA69E, 'V'),\n    (0xA6F8, 'X'),\n    (0xA700, 'V'),\n    (0xA722, 'M', u'ꜣ'),\n    (0xA723, 'V'),\n    (0xA724, 'M', u'ꜥ'),\n    (0xA725, 'V'),\n    (0xA726, 'M', u'ꜧ'),\n    (0xA727, 'V'),\n    (0xA728, 'M', u'ꜩ'),\n    (0xA729, 'V'),\n    (0xA72A, 'M', u'ꜫ'),\n    (0xA72B, 'V'),\n    (0xA72C, 'M', u'ꜭ'),\n    (0xA72D, 'V'),\n    (0xA72E, 'M', u'ꜯ'),\n    (0xA72F, 'V'),\n    (0xA732, 'M', u'ꜳ'),\n    (0xA733, 'V'),\n    (0xA734, 'M', u'ꜵ'),\n    (0xA735, 'V'),\n    (0xA736, 'M', u'ꜷ'),\n    (0xA737, 'V'),\n    (0xA738, 'M', u'ꜹ'),\n    (0xA739, 'V'),\n    (0xA73A, 'M', u'ꜻ'),\n    (0xA73B, 'V'),\n    (0xA73C, 'M', u'ꜽ'),\n    (0xA73D, 'V'),\n    (0xA73E, 'M', u'ꜿ'),\n    (0xA73F, 'V'),\n    (0xA740, 'M', u'ꝁ'),\n    (0xA741, 'V'),\n    (0xA742, 'M', u'ꝃ'),\n    (0xA743, 'V'),\n    (0xA744, 'M', u'ꝅ'),\n    (0xA745, 'V'),\n    (0xA746, 'M', u'ꝇ'),\n    (0xA747, 'V'),\n    (0xA748, 'M', u'ꝉ'),\n    (0xA749, 'V'),\n    (0xA74A, 'M', u'ꝋ'),\n    (0xA74B, 'V'),\n    (0xA74C, 'M', u'ꝍ'),\n    (0xA74D, 'V'),\n    (0xA74E, 'M', u'ꝏ'),\n    (0xA74F, 'V'),\n    (0xA750, 'M', u'ꝑ'),\n    (0xA751, 'V'),\n    (0xA752, 'M', u'ꝓ'),\n    (0xA753, 'V'),\n    (0xA754, 'M', u'ꝕ'),\n    (0xA755, 'V'),\n    (0xA756, 'M', u'ꝗ'),\n    (0xA757, 'V'),\n    (0xA758, 'M', u'ꝙ'),\n    (0xA759, 'V'),\n    (0xA75A, 'M', u'ꝛ'),\n    (0xA75B, 'V'),\n    (0xA75C, 'M', u'ꝝ'),\n    (0xA75D, 'V'),\n    (0xA75E, 'M', u'ꝟ'),\n    (0xA75F, 'V'),\n    (0xA760, 'M', u'ꝡ'),\n    (0xA761, 'V'),\n    (0xA762, 'M', u'ꝣ'),\n    (0xA763, 'V'),\n    (0xA764, 'M', u'ꝥ'),\n    (0xA765, 'V'),\n    (0xA766, 'M', u'ꝧ'),\n    (0xA767, 'V'),\n    (0xA768, 'M', u'ꝩ'),\n    (0xA769, 'V'),\n    (0xA76A, 'M', u'ꝫ'),\n    (0xA76B, 'V'),\n    (0xA76C, 'M', u'ꝭ'),\n    (0xA76D, 'V'),\n    (0xA76E, 'M', u'ꝯ'),\n    ]\n\ndef _seg_37():\n    return [\n    (0xA76F, 'V'),\n    (0xA770, 'M', u'ꝯ'),\n    (0xA771, 'V'),\n    (0xA779, 'M', u'ꝺ'),\n    (0xA77A, 'V'),\n    (0xA77B, 'M', u'ꝼ'),\n    (0xA77C, 'V'),\n    (0xA77D, 'M', u'ᵹ'),\n    (0xA77E, 'M', u'ꝿ'),\n    (0xA77F, 'V'),\n    (0xA780, 'M', u'ꞁ'),\n    (0xA781, 'V'),\n    (0xA782, 'M', u'ꞃ'),\n    (0xA783, 'V'),\n    (0xA784, 'M', u'ꞅ'),\n    (0xA785, 'V'),\n    (0xA786, 'M', u'ꞇ'),\n    (0xA787, 'V'),\n    (0xA78B, 'M', u'ꞌ'),\n    (0xA78C, 'V'),\n    (0xA78D, 'M', u'ɥ'),\n    (0xA78E, 'V'),\n    (0xA790, 'M', u'ꞑ'),\n    (0xA791, 'V'),\n    (0xA792, 'M', u'ꞓ'),\n    (0xA793, 'V'),\n    (0xA796, 'M', u'ꞗ'),\n    (0xA797, 'V'),\n    (0xA798, 'M', u'ꞙ'),\n    (0xA799, 'V'),\n    (0xA79A, 'M', u'ꞛ'),\n    (0xA79B, 'V'),\n    (0xA79C, 'M', u'ꞝ'),\n    (0xA79D, 'V'),\n    (0xA79E, 'M', u'ꞟ'),\n    (0xA79F, 'V'),\n    (0xA7A0, 'M', u'ꞡ'),\n    (0xA7A1, 'V'),\n    (0xA7A2, 'M', u'ꞣ'),\n    (0xA7A3, 'V'),\n    (0xA7A4, 'M', u'ꞥ'),\n    (0xA7A5, 'V'),\n    (0xA7A6, 'M', u'ꞧ'),\n    (0xA7A7, 'V'),\n    (0xA7A8, 'M', u'ꞩ'),\n    (0xA7A9, 'V'),\n    (0xA7AA, 'M', u'ɦ'),\n    (0xA7AB, 'M', u'ɜ'),\n    (0xA7AC, 'M', u'ɡ'),\n    (0xA7AD, 'M', u'ɬ'),\n    (0xA7AE, 'M', u'ɪ'),\n    (0xA7AF, 'V'),\n    (0xA7B0, 'M', u'ʞ'),\n    (0xA7B1, 'M', u'ʇ'),\n    (0xA7B2, 'M', u'ʝ'),\n    (0xA7B3, 'M', u'ꭓ'),\n    (0xA7B4, 'M', u'ꞵ'),\n    (0xA7B5, 'V'),\n    (0xA7B6, 'M', u'ꞷ'),\n    (0xA7B7, 'V'),\n    (0xA7B8, 'M', u'ꞹ'),\n    (0xA7B9, 'V'),\n    (0xA7BA, 'M', u'ꞻ'),\n    (0xA7BB, 'V'),\n    (0xA7BC, 'M', u'ꞽ'),\n    (0xA7BD, 'V'),\n    (0xA7BE, 'M', u'ꞿ'),\n    (0xA7BF, 'V'),\n    (0xA7C0, 'X'),\n    (0xA7C2, 'M', u'ꟃ'),\n    (0xA7C3, 'V'),\n    (0xA7C4, 'M', u'ꞔ'),\n    (0xA7C5, 'M', u'ʂ'),\n    (0xA7C6, 'M', u'ᶎ'),\n    (0xA7C7, 'M', u'ꟈ'),\n    (0xA7C8, 'V'),\n    (0xA7C9, 'M', u'ꟊ'),\n    (0xA7CA, 'V'),\n    (0xA7CB, 'X'),\n    (0xA7F5, 'M', u'ꟶ'),\n    (0xA7F6, 'V'),\n    (0xA7F8, 'M', u'ħ'),\n    (0xA7F9, 'M', u'œ'),\n    (0xA7FA, 'V'),\n    (0xA82D, 'X'),\n    (0xA830, 'V'),\n    (0xA83A, 'X'),\n    (0xA840, 'V'),\n    (0xA878, 'X'),\n    (0xA880, 'V'),\n    (0xA8C6, 'X'),\n    (0xA8CE, 'V'),\n    (0xA8DA, 'X'),\n    (0xA8E0, 'V'),\n    (0xA954, 'X'),\n    (0xA95F, 'V'),\n    (0xA97D, 'X'),\n    (0xA980, 'V'),\n    (0xA9CE, 'X'),\n    (0xA9CF, 'V'),\n    ]\n\ndef _seg_38():\n    return [\n    (0xA9DA, 'X'),\n    (0xA9DE, 'V'),\n    (0xA9FF, 'X'),\n    (0xAA00, 'V'),\n    (0xAA37, 'X'),\n    (0xAA40, 'V'),\n    (0xAA4E, 'X'),\n    (0xAA50, 'V'),\n    (0xAA5A, 'X'),\n    (0xAA5C, 'V'),\n    (0xAAC3, 'X'),\n    (0xAADB, 'V'),\n    (0xAAF7, 'X'),\n    (0xAB01, 'V'),\n    (0xAB07, 'X'),\n    (0xAB09, 'V'),\n    (0xAB0F, 'X'),\n    (0xAB11, 'V'),\n    (0xAB17, 'X'),\n    (0xAB20, 'V'),\n    (0xAB27, 'X'),\n    (0xAB28, 'V'),\n    (0xAB2F, 'X'),\n    (0xAB30, 'V'),\n    (0xAB5C, 'M', u'ꜧ'),\n    (0xAB5D, 'M', u'ꬷ'),\n    (0xAB5E, 'M', u'ɫ'),\n    (0xAB5F, 'M', u'ꭒ'),\n    (0xAB60, 'V'),\n    (0xAB69, 'M', u'ʍ'),\n    (0xAB6A, 'V'),\n    (0xAB6C, 'X'),\n    (0xAB70, 'M', u'Ꭰ'),\n    (0xAB71, 'M', u'Ꭱ'),\n    (0xAB72, 'M', u'Ꭲ'),\n    (0xAB73, 'M', u'Ꭳ'),\n    (0xAB74, 'M', u'Ꭴ'),\n    (0xAB75, 'M', u'Ꭵ'),\n    (0xAB76, 'M', u'Ꭶ'),\n    (0xAB77, 'M', u'Ꭷ'),\n    (0xAB78, 'M', u'Ꭸ'),\n    (0xAB79, 'M', u'Ꭹ'),\n    (0xAB7A, 'M', u'Ꭺ'),\n    (0xAB7B, 'M', u'Ꭻ'),\n    (0xAB7C, 'M', u'Ꭼ'),\n    (0xAB7D, 'M', u'Ꭽ'),\n    (0xAB7E, 'M', u'Ꭾ'),\n    (0xAB7F, 'M', u'Ꭿ'),\n    (0xAB80, 'M', u'Ꮀ'),\n    (0xAB81, 'M', u'Ꮁ'),\n    (0xAB82, 'M', u'Ꮂ'),\n    (0xAB83, 'M', u'Ꮃ'),\n    (0xAB84, 'M', u'Ꮄ'),\n    (0xAB85, 'M', u'Ꮅ'),\n    (0xAB86, 'M', u'Ꮆ'),\n    (0xAB87, 'M', u'Ꮇ'),\n    (0xAB88, 'M', u'Ꮈ'),\n    (0xAB89, 'M', u'Ꮉ'),\n    (0xAB8A, 'M', u'Ꮊ'),\n    (0xAB8B, 'M', u'Ꮋ'),\n    (0xAB8C, 'M', u'Ꮌ'),\n    (0xAB8D, 'M', u'Ꮍ'),\n    (0xAB8E, 'M', u'Ꮎ'),\n    (0xAB8F, 'M', u'Ꮏ'),\n    (0xAB90, 'M', u'Ꮐ'),\n    (0xAB91, 'M', u'Ꮑ'),\n    (0xAB92, 'M', u'Ꮒ'),\n    (0xAB93, 'M', u'Ꮓ'),\n    (0xAB94, 'M', u'Ꮔ'),\n    (0xAB95, 'M', u'Ꮕ'),\n    (0xAB96, 'M', u'Ꮖ'),\n    (0xAB97, 'M', u'Ꮗ'),\n    (0xAB98, 'M', u'Ꮘ'),\n    (0xAB99, 'M', u'Ꮙ'),\n    (0xAB9A, 'M', u'Ꮚ'),\n    (0xAB9B, 'M', u'Ꮛ'),\n    (0xAB9C, 'M', u'Ꮜ'),\n    (0xAB9D, 'M', u'Ꮝ'),\n    (0xAB9E, 'M', u'Ꮞ'),\n    (0xAB9F, 'M', u'Ꮟ'),\n    (0xABA0, 'M', u'Ꮠ'),\n    (0xABA1, 'M', u'Ꮡ'),\n    (0xABA2, 'M', u'Ꮢ'),\n    (0xABA3, 'M', u'Ꮣ'),\n    (0xABA4, 'M', u'Ꮤ'),\n    (0xABA5, 'M', u'Ꮥ'),\n    (0xABA6, 'M', u'Ꮦ'),\n    (0xABA7, 'M', u'Ꮧ'),\n    (0xABA8, 'M', u'Ꮨ'),\n    (0xABA9, 'M', u'Ꮩ'),\n    (0xABAA, 'M', u'Ꮪ'),\n    (0xABAB, 'M', u'Ꮫ'),\n    (0xABAC, 'M', u'Ꮬ'),\n    (0xABAD, 'M', u'Ꮭ'),\n    (0xABAE, 'M', u'Ꮮ'),\n    (0xABAF, 'M', u'Ꮯ'),\n    (0xABB0, 'M', u'Ꮰ'),\n    (0xABB1, 'M', u'Ꮱ'),\n    (0xABB2, 'M', u'Ꮲ'),\n    (0xABB3, 'M', u'Ꮳ'),\n    ]\n\ndef _seg_39():\n    return [\n    (0xABB4, 'M', u'Ꮴ'),\n    (0xABB5, 'M', u'Ꮵ'),\n    (0xABB6, 'M', u'Ꮶ'),\n    (0xABB7, 'M', u'Ꮷ'),\n    (0xABB8, 'M', u'Ꮸ'),\n    (0xABB9, 'M', u'Ꮹ'),\n    (0xABBA, 'M', u'Ꮺ'),\n    (0xABBB, 'M', u'Ꮻ'),\n    (0xABBC, 'M', u'Ꮼ'),\n    (0xABBD, 'M', u'Ꮽ'),\n    (0xABBE, 'M', u'Ꮾ'),\n    (0xABBF, 'M', u'Ꮿ'),\n    (0xABC0, 'V'),\n    (0xABEE, 'X'),\n    (0xABF0, 'V'),\n    (0xABFA, 'X'),\n    (0xAC00, 'V'),\n    (0xD7A4, 'X'),\n    (0xD7B0, 'V'),\n    (0xD7C7, 'X'),\n    (0xD7CB, 'V'),\n    (0xD7FC, 'X'),\n    (0xF900, 'M', u'豈'),\n    (0xF901, 'M', u'更'),\n    (0xF902, 'M', u'車'),\n    (0xF903, 'M', u'賈'),\n    (0xF904, 'M', u'滑'),\n    (0xF905, 'M', u'串'),\n    (0xF906, 'M', u'句'),\n    (0xF907, 'M', u'龜'),\n    (0xF909, 'M', u'契'),\n    (0xF90A, 'M', u'金'),\n    (0xF90B, 'M', u'喇'),\n    (0xF90C, 'M', u'奈'),\n    (0xF90D, 'M', u'懶'),\n    (0xF90E, 'M', u'癩'),\n    (0xF90F, 'M', u'羅'),\n    (0xF910, 'M', u'蘿'),\n    (0xF911, 'M', u'螺'),\n    (0xF912, 'M', u'裸'),\n    (0xF913, 'M', u'邏'),\n    (0xF914, 'M', u'樂'),\n    (0xF915, 'M', u'洛'),\n    (0xF916, 'M', u'烙'),\n    (0xF917, 'M', u'珞'),\n    (0xF918, 'M', u'落'),\n    (0xF919, 'M', u'酪'),\n    (0xF91A, 'M', u'駱'),\n    (0xF91B, 'M', u'亂'),\n    (0xF91C, 'M', u'卵'),\n    (0xF91D, 'M', u'欄'),\n    (0xF91E, 'M', u'爛'),\n    (0xF91F, 'M', u'蘭'),\n    (0xF920, 'M', u'鸞'),\n    (0xF921, 'M', u'嵐'),\n    (0xF922, 'M', u'濫'),\n    (0xF923, 'M', u'藍'),\n    (0xF924, 'M', u'襤'),\n    (0xF925, 'M', u'拉'),\n    (0xF926, 'M', u'臘'),\n    (0xF927, 'M', u'蠟'),\n    (0xF928, 'M', u'廊'),\n    (0xF929, 'M', u'朗'),\n    (0xF92A, 'M', u'浪'),\n    (0xF92B, 'M', u'狼'),\n    (0xF92C, 'M', u'郎'),\n    (0xF92D, 'M', u'來'),\n    (0xF92E, 'M', u'冷'),\n    (0xF92F, 'M', u'勞'),\n    (0xF930, 'M', u'擄'),\n    (0xF931, 'M', u'櫓'),\n    (0xF932, 'M', u'爐'),\n    (0xF933, 'M', u'盧'),\n    (0xF934, 'M', u'老'),\n    (0xF935, 'M', u'蘆'),\n    (0xF936, 'M', u'虜'),\n    (0xF937, 'M', u'路'),\n    (0xF938, 'M', u'露'),\n    (0xF939, 'M', u'魯'),\n    (0xF93A, 'M', u'鷺'),\n    (0xF93B, 'M', u'碌'),\n    (0xF93C, 'M', u'祿'),\n    (0xF93D, 'M', u'綠'),\n    (0xF93E, 'M', u'菉'),\n    (0xF93F, 'M', u'錄'),\n    (0xF940, 'M', u'鹿'),\n    (0xF941, 'M', u'論'),\n    (0xF942, 'M', u'壟'),\n    (0xF943, 'M', u'弄'),\n    (0xF944, 'M', u'籠'),\n    (0xF945, 'M', u'聾'),\n    (0xF946, 'M', u'牢'),\n    (0xF947, 'M', u'磊'),\n    (0xF948, 'M', u'賂'),\n    (0xF949, 'M', u'雷'),\n    (0xF94A, 'M', u'壘'),\n    (0xF94B, 'M', u'屢'),\n    (0xF94C, 'M', u'樓'),\n    (0xF94D, 'M', u'淚'),\n    (0xF94E, 'M', u'漏'),\n    ]\n\ndef _seg_40():\n    return [\n    (0xF94F, 'M', u'累'),\n    (0xF950, 'M', u'縷'),\n    (0xF951, 'M', u'陋'),\n    (0xF952, 'M', u'勒'),\n    (0xF953, 'M', u'肋'),\n    (0xF954, 'M', u'凜'),\n    (0xF955, 'M', u'凌'),\n    (0xF956, 'M', u'稜'),\n    (0xF957, 'M', u'綾'),\n    (0xF958, 'M', u'菱'),\n    (0xF959, 'M', u'陵'),\n    (0xF95A, 'M', u'讀'),\n    (0xF95B, 'M', u'拏'),\n    (0xF95C, 'M', u'樂'),\n    (0xF95D, 'M', u'諾'),\n    (0xF95E, 'M', u'丹'),\n    (0xF95F, 'M', u'寧'),\n    (0xF960, 'M', u'怒'),\n    (0xF961, 'M', u'率'),\n    (0xF962, 'M', u'異'),\n    (0xF963, 'M', u'北'),\n    (0xF964, 'M', u'磻'),\n    (0xF965, 'M', u'便'),\n    (0xF966, 'M', u'復'),\n    (0xF967, 'M', u'不'),\n    (0xF968, 'M', u'泌'),\n    (0xF969, 'M', u'數'),\n    (0xF96A, 'M', u'索'),\n    (0xF96B, 'M', u'參'),\n    (0xF96C, 'M', u'塞'),\n    (0xF96D, 'M', u'省'),\n    (0xF96E, 'M', u'葉'),\n    (0xF96F, 'M', u'說'),\n    (0xF970, 'M', u'殺'),\n    (0xF971, 'M', u'辰'),\n    (0xF972, 'M', u'沈'),\n    (0xF973, 'M', u'拾'),\n    (0xF974, 'M', u'若'),\n    (0xF975, 'M', u'掠'),\n    (0xF976, 'M', u'略'),\n    (0xF977, 'M', u'亮'),\n    (0xF978, 'M', u'兩'),\n    (0xF979, 'M', u'凉'),\n    (0xF97A, 'M', u'梁'),\n    (0xF97B, 'M', u'糧'),\n    (0xF97C, 'M', u'良'),\n    (0xF97D, 'M', u'諒'),\n    (0xF97E, 'M', u'量'),\n    (0xF97F, 'M', u'勵'),\n    (0xF980, 'M', u'呂'),\n    (0xF981, 'M', u'女'),\n    (0xF982, 'M', u'廬'),\n    (0xF983, 'M', u'旅'),\n    (0xF984, 'M', u'濾'),\n    (0xF985, 'M', u'礪'),\n    (0xF986, 'M', u'閭'),\n    (0xF987, 'M', u'驪'),\n    (0xF988, 'M', u'麗'),\n    (0xF989, 'M', u'黎'),\n    (0xF98A, 'M', u'力'),\n    (0xF98B, 'M', u'曆'),\n    (0xF98C, 'M', u'歷'),\n    (0xF98D, 'M', u'轢'),\n    (0xF98E, 'M', u'年'),\n    (0xF98F, 'M', u'憐'),\n    (0xF990, 'M', u'戀'),\n    (0xF991, 'M', u'撚'),\n    (0xF992, 'M', u'漣'),\n    (0xF993, 'M', u'煉'),\n    (0xF994, 'M', u'璉'),\n    (0xF995, 'M', u'秊'),\n    (0xF996, 'M', u'練'),\n    (0xF997, 'M', u'聯'),\n    (0xF998, 'M', u'輦'),\n    (0xF999, 'M', u'蓮'),\n    (0xF99A, 'M', u'連'),\n    (0xF99B, 'M', u'鍊'),\n    (0xF99C, 'M', u'列'),\n    (0xF99D, 'M', u'劣'),\n    (0xF99E, 'M', u'咽'),\n    (0xF99F, 'M', u'烈'),\n    (0xF9A0, 'M', u'裂'),\n    (0xF9A1, 'M', u'說'),\n    (0xF9A2, 'M', u'廉'),\n    (0xF9A3, 'M', u'念'),\n    (0xF9A4, 'M', u'捻'),\n    (0xF9A5, 'M', u'殮'),\n    (0xF9A6, 'M', u'簾'),\n    (0xF9A7, 'M', u'獵'),\n    (0xF9A8, 'M', u'令'),\n    (0xF9A9, 'M', u'囹'),\n    (0xF9AA, 'M', u'寧'),\n    (0xF9AB, 'M', u'嶺'),\n    (0xF9AC, 'M', u'怜'),\n    (0xF9AD, 'M', u'玲'),\n    (0xF9AE, 'M', u'瑩'),\n    (0xF9AF, 'M', u'羚'),\n    (0xF9B0, 'M', u'聆'),\n    (0xF9B1, 'M', u'鈴'),\n    (0xF9B2, 'M', u'零'),\n    ]\n\ndef _seg_41():\n    return [\n    (0xF9B3, 'M', u'靈'),\n    (0xF9B4, 'M', u'領'),\n    (0xF9B5, 'M', u'例'),\n    (0xF9B6, 'M', u'禮'),\n    (0xF9B7, 'M', u'醴'),\n    (0xF9B8, 'M', u'隸'),\n    (0xF9B9, 'M', u'惡'),\n    (0xF9BA, 'M', u'了'),\n    (0xF9BB, 'M', u'僚'),\n    (0xF9BC, 'M', u'寮'),\n    (0xF9BD, 'M', u'尿'),\n    (0xF9BE, 'M', u'料'),\n    (0xF9BF, 'M', u'樂'),\n    (0xF9C0, 'M', u'燎'),\n    (0xF9C1, 'M', u'療'),\n    (0xF9C2, 'M', u'蓼'),\n    (0xF9C3, 'M', u'遼'),\n    (0xF9C4, 'M', u'龍'),\n    (0xF9C5, 'M', u'暈'),\n    (0xF9C6, 'M', u'阮'),\n    (0xF9C7, 'M', u'劉'),\n    (0xF9C8, 'M', u'杻'),\n    (0xF9C9, 'M', u'柳'),\n    (0xF9CA, 'M', u'流'),\n    (0xF9CB, 'M', u'溜'),\n    (0xF9CC, 'M', u'琉'),\n    (0xF9CD, 'M', u'留'),\n    (0xF9CE, 'M', u'硫'),\n    (0xF9CF, 'M', u'紐'),\n    (0xF9D0, 'M', u'類'),\n    (0xF9D1, 'M', u'六'),\n    (0xF9D2, 'M', u'戮'),\n    (0xF9D3, 'M', u'陸'),\n    (0xF9D4, 'M', u'倫'),\n    (0xF9D5, 'M', u'崙'),\n    (0xF9D6, 'M', u'淪'),\n    (0xF9D7, 'M', u'輪'),\n    (0xF9D8, 'M', u'律'),\n    (0xF9D9, 'M', u'慄'),\n    (0xF9DA, 'M', u'栗'),\n    (0xF9DB, 'M', u'率'),\n    (0xF9DC, 'M', u'隆'),\n    (0xF9DD, 'M', u'利'),\n    (0xF9DE, 'M', u'吏'),\n    (0xF9DF, 'M', u'履'),\n    (0xF9E0, 'M', u'易'),\n    (0xF9E1, 'M', u'李'),\n    (0xF9E2, 'M', u'梨'),\n    (0xF9E3, 'M', u'泥'),\n    (0xF9E4, 'M', u'理'),\n    (0xF9E5, 'M', u'痢'),\n    (0xF9E6, 'M', u'罹'),\n    (0xF9E7, 'M', u'裏'),\n    (0xF9E8, 'M', u'裡'),\n    (0xF9E9, 'M', u'里'),\n    (0xF9EA, 'M', u'離'),\n    (0xF9EB, 'M', u'匿'),\n    (0xF9EC, 'M', u'溺'),\n    (0xF9ED, 'M', u'吝'),\n    (0xF9EE, 'M', u'燐'),\n    (0xF9EF, 'M', u'璘'),\n    (0xF9F0, 'M', u'藺'),\n    (0xF9F1, 'M', u'隣'),\n    (0xF9F2, 'M', u'鱗'),\n    (0xF9F3, 'M', u'麟'),\n    (0xF9F4, 'M', u'林'),\n    (0xF9F5, 'M', u'淋'),\n    (0xF9F6, 'M', u'臨'),\n    (0xF9F7, 'M', u'立'),\n    (0xF9F8, 'M', u'笠'),\n    (0xF9F9, 'M', u'粒'),\n    (0xF9FA, 'M', u'狀'),\n    (0xF9FB, 'M', u'炙'),\n    (0xF9FC, 'M', u'識'),\n    (0xF9FD, 'M', u'什'),\n    (0xF9FE, 'M', u'茶'),\n    (0xF9FF, 'M', u'刺'),\n    (0xFA00, 'M', u'切'),\n    (0xFA01, 'M', u'度'),\n    (0xFA02, 'M', u'拓'),\n    (0xFA03, 'M', u'糖'),\n    (0xFA04, 'M', u'宅'),\n    (0xFA05, 'M', u'洞'),\n    (0xFA06, 'M', u'暴'),\n    (0xFA07, 'M', u'輻'),\n    (0xFA08, 'M', u'行'),\n    (0xFA09, 'M', u'降'),\n    (0xFA0A, 'M', u'見'),\n    (0xFA0B, 'M', u'廓'),\n    (0xFA0C, 'M', u'兀'),\n    (0xFA0D, 'M', u'嗀'),\n    (0xFA0E, 'V'),\n    (0xFA10, 'M', u'塚'),\n    (0xFA11, 'V'),\n    (0xFA12, 'M', u'晴'),\n    (0xFA13, 'V'),\n    (0xFA15, 'M', u'凞'),\n    (0xFA16, 'M', u'猪'),\n    (0xFA17, 'M', u'益'),\n    (0xFA18, 'M', u'礼'),\n    ]\n\ndef _seg_42():\n    return [\n    (0xFA19, 'M', u'神'),\n    (0xFA1A, 'M', u'祥'),\n    (0xFA1B, 'M', u'福'),\n    (0xFA1C, 'M', u'靖'),\n    (0xFA1D, 'M', u'精'),\n    (0xFA1E, 'M', u'羽'),\n    (0xFA1F, 'V'),\n    (0xFA20, 'M', u'蘒'),\n    (0xFA21, 'V'),\n    (0xFA22, 'M', u'諸'),\n    (0xFA23, 'V'),\n    (0xFA25, 'M', u'逸'),\n    (0xFA26, 'M', u'都'),\n    (0xFA27, 'V'),\n    (0xFA2A, 'M', u'飯'),\n    (0xFA2B, 'M', u'飼'),\n    (0xFA2C, 'M', u'館'),\n    (0xFA2D, 'M', u'鶴'),\n    (0xFA2E, 'M', u'郞'),\n    (0xFA2F, 'M', u'隷'),\n    (0xFA30, 'M', u'侮'),\n    (0xFA31, 'M', u'僧'),\n    (0xFA32, 'M', u'免'),\n    (0xFA33, 'M', u'勉'),\n    (0xFA34, 'M', u'勤'),\n    (0xFA35, 'M', u'卑'),\n    (0xFA36, 'M', u'喝'),\n    (0xFA37, 'M', u'嘆'),\n    (0xFA38, 'M', u'器'),\n    (0xFA39, 'M', u'塀'),\n    (0xFA3A, 'M', u'墨'),\n    (0xFA3B, 'M', u'層'),\n    (0xFA3C, 'M', u'屮'),\n    (0xFA3D, 'M', u'悔'),\n    (0xFA3E, 'M', u'慨'),\n    (0xFA3F, 'M', u'憎'),\n    (0xFA40, 'M', u'懲'),\n    (0xFA41, 'M', u'敏'),\n    (0xFA42, 'M', u'既'),\n    (0xFA43, 'M', u'暑'),\n    (0xFA44, 'M', u'梅'),\n    (0xFA45, 'M', u'海'),\n    (0xFA46, 'M', u'渚'),\n    (0xFA47, 'M', u'漢'),\n    (0xFA48, 'M', u'煮'),\n    (0xFA49, 'M', u'爫'),\n    (0xFA4A, 'M', u'琢'),\n    (0xFA4B, 'M', u'碑'),\n    (0xFA4C, 'M', u'社'),\n    (0xFA4D, 'M', u'祉'),\n    (0xFA4E, 'M', u'祈'),\n    (0xFA4F, 'M', u'祐'),\n    (0xFA50, 'M', u'祖'),\n    (0xFA51, 'M', u'祝'),\n    (0xFA52, 'M', u'禍'),\n    (0xFA53, 'M', u'禎'),\n    (0xFA54, 'M', u'穀'),\n    (0xFA55, 'M', u'突'),\n    (0xFA56, 'M', u'節'),\n    (0xFA57, 'M', u'練'),\n    (0xFA58, 'M', u'縉'),\n    (0xFA59, 'M', u'繁'),\n    (0xFA5A, 'M', u'署'),\n    (0xFA5B, 'M', u'者'),\n    (0xFA5C, 'M', u'臭'),\n    (0xFA5D, 'M', u'艹'),\n    (0xFA5F, 'M', u'著'),\n    (0xFA60, 'M', u'褐'),\n    (0xFA61, 'M', u'視'),\n    (0xFA62, 'M', u'謁'),\n    (0xFA63, 'M', u'謹'),\n    (0xFA64, 'M', u'賓'),\n    (0xFA65, 'M', u'贈'),\n    (0xFA66, 'M', u'辶'),\n    (0xFA67, 'M', u'逸'),\n    (0xFA68, 'M', u'難'),\n    (0xFA69, 'M', u'響'),\n    (0xFA6A, 'M', u'頻'),\n    (0xFA6B, 'M', u'恵'),\n    (0xFA6C, 'M', u'𤋮'),\n    (0xFA6D, 'M', u'舘'),\n    (0xFA6E, 'X'),\n    (0xFA70, 'M', u'並'),\n    (0xFA71, 'M', u'况'),\n    (0xFA72, 'M', u'全'),\n    (0xFA73, 'M', u'侀'),\n    (0xFA74, 'M', u'充'),\n    (0xFA75, 'M', u'冀'),\n    (0xFA76, 'M', u'勇'),\n    (0xFA77, 'M', u'勺'),\n    (0xFA78, 'M', u'喝'),\n    (0xFA79, 'M', u'啕'),\n    (0xFA7A, 'M', u'喙'),\n    (0xFA7B, 'M', u'嗢'),\n    (0xFA7C, 'M', u'塚'),\n    (0xFA7D, 'M', u'墳'),\n    (0xFA7E, 'M', u'奄'),\n    (0xFA7F, 'M', u'奔'),\n    (0xFA80, 'M', u'婢'),\n    (0xFA81, 'M', u'嬨'),\n    ]\n\ndef _seg_43():\n    return [\n    (0xFA82, 'M', u'廒'),\n    (0xFA83, 'M', u'廙'),\n    (0xFA84, 'M', u'彩'),\n    (0xFA85, 'M', u'徭'),\n    (0xFA86, 'M', u'惘'),\n    (0xFA87, 'M', u'慎'),\n    (0xFA88, 'M', u'愈'),\n    (0xFA89, 'M', u'憎'),\n    (0xFA8A, 'M', u'慠'),\n    (0xFA8B, 'M', u'懲'),\n    (0xFA8C, 'M', u'戴'),\n    (0xFA8D, 'M', u'揄'),\n    (0xFA8E, 'M', u'搜'),\n    (0xFA8F, 'M', u'摒'),\n    (0xFA90, 'M', u'敖'),\n    (0xFA91, 'M', u'晴'),\n    (0xFA92, 'M', u'朗'),\n    (0xFA93, 'M', u'望'),\n    (0xFA94, 'M', u'杖'),\n    (0xFA95, 'M', u'歹'),\n    (0xFA96, 'M', u'殺'),\n    (0xFA97, 'M', u'流'),\n    (0xFA98, 'M', u'滛'),\n    (0xFA99, 'M', u'滋'),\n    (0xFA9A, 'M', u'漢'),\n    (0xFA9B, 'M', u'瀞'),\n    (0xFA9C, 'M', u'煮'),\n    (0xFA9D, 'M', u'瞧'),\n    (0xFA9E, 'M', u'爵'),\n    (0xFA9F, 'M', u'犯'),\n    (0xFAA0, 'M', u'猪'),\n    (0xFAA1, 'M', u'瑱'),\n    (0xFAA2, 'M', u'甆'),\n    (0xFAA3, 'M', u'画'),\n    (0xFAA4, 'M', u'瘝'),\n    (0xFAA5, 'M', u'瘟'),\n    (0xFAA6, 'M', u'益'),\n    (0xFAA7, 'M', u'盛'),\n    (0xFAA8, 'M', u'直'),\n    (0xFAA9, 'M', u'睊'),\n    (0xFAAA, 'M', u'着'),\n    (0xFAAB, 'M', u'磌'),\n    (0xFAAC, 'M', u'窱'),\n    (0xFAAD, 'M', u'節'),\n    (0xFAAE, 'M', u'类'),\n    (0xFAAF, 'M', u'絛'),\n    (0xFAB0, 'M', u'練'),\n    (0xFAB1, 'M', u'缾'),\n    (0xFAB2, 'M', u'者'),\n    (0xFAB3, 'M', u'荒'),\n    (0xFAB4, 'M', u'華'),\n    (0xFAB5, 'M', u'蝹'),\n    (0xFAB6, 'M', u'襁'),\n    (0xFAB7, 'M', u'覆'),\n    (0xFAB8, 'M', u'視'),\n    (0xFAB9, 'M', u'調'),\n    (0xFABA, 'M', u'諸'),\n    (0xFABB, 'M', u'請'),\n    (0xFABC, 'M', u'謁'),\n    (0xFABD, 'M', u'諾'),\n    (0xFABE, 'M', u'諭'),\n    (0xFABF, 'M', u'謹'),\n    (0xFAC0, 'M', u'變'),\n    (0xFAC1, 'M', u'贈'),\n    (0xFAC2, 'M', u'輸'),\n    (0xFAC3, 'M', u'遲'),\n    (0xFAC4, 'M', u'醙'),\n    (0xFAC5, 'M', u'鉶'),\n    (0xFAC6, 'M', u'陼'),\n    (0xFAC7, 'M', u'難'),\n    (0xFAC8, 'M', u'靖'),\n    (0xFAC9, 'M', u'韛'),\n    (0xFACA, 'M', u'響'),\n    (0xFACB, 'M', u'頋'),\n    (0xFACC, 'M', u'頻'),\n    (0xFACD, 'M', u'鬒'),\n    (0xFACE, 'M', u'龜'),\n    (0xFACF, 'M', u'𢡊'),\n    (0xFAD0, 'M', u'𢡄'),\n    (0xFAD1, 'M', u'𣏕'),\n    (0xFAD2, 'M', u'㮝'),\n    (0xFAD3, 'M', u'䀘'),\n    (0xFAD4, 'M', u'䀹'),\n    (0xFAD5, 'M', u'𥉉'),\n    (0xFAD6, 'M', u'𥳐'),\n    (0xFAD7, 'M', u'𧻓'),\n    (0xFAD8, 'M', u'齃'),\n    (0xFAD9, 'M', u'龎'),\n    (0xFADA, 'X'),\n    (0xFB00, 'M', u'ff'),\n    (0xFB01, 'M', u'fi'),\n    (0xFB02, 'M', u'fl'),\n    (0xFB03, 'M', u'ffi'),\n    (0xFB04, 'M', u'ffl'),\n    (0xFB05, 'M', u'st'),\n    (0xFB07, 'X'),\n    (0xFB13, 'M', u'մն'),\n    (0xFB14, 'M', u'մե'),\n    (0xFB15, 'M', u'մի'),\n    (0xFB16, 'M', u'վն'),\n    ]\n\ndef _seg_44():\n    return [\n    (0xFB17, 'M', u'մխ'),\n    (0xFB18, 'X'),\n    (0xFB1D, 'M', u'יִ'),\n    (0xFB1E, 'V'),\n    (0xFB1F, 'M', u'ײַ'),\n    (0xFB20, 'M', u'ע'),\n    (0xFB21, 'M', u'א'),\n    (0xFB22, 'M', u'ד'),\n    (0xFB23, 'M', u'ה'),\n    (0xFB24, 'M', u'כ'),\n    (0xFB25, 'M', u'ל'),\n    (0xFB26, 'M', u'ם'),\n    (0xFB27, 'M', u'ר'),\n    (0xFB28, 'M', u'ת'),\n    (0xFB29, '3', u'+'),\n    (0xFB2A, 'M', u'שׁ'),\n    (0xFB2B, 'M', u'שׂ'),\n    (0xFB2C, 'M', u'שּׁ'),\n    (0xFB2D, 'M', u'שּׂ'),\n    (0xFB2E, 'M', u'אַ'),\n    (0xFB2F, 'M', u'אָ'),\n    (0xFB30, 'M', u'אּ'),\n    (0xFB31, 'M', u'בּ'),\n    (0xFB32, 'M', u'גּ'),\n    (0xFB33, 'M', u'דּ'),\n    (0xFB34, 'M', u'הּ'),\n    (0xFB35, 'M', u'וּ'),\n    (0xFB36, 'M', u'זּ'),\n    (0xFB37, 'X'),\n    (0xFB38, 'M', u'טּ'),\n    (0xFB39, 'M', u'יּ'),\n    (0xFB3A, 'M', u'ךּ'),\n    (0xFB3B, 'M', u'כּ'),\n    (0xFB3C, 'M', u'לּ'),\n    (0xFB3D, 'X'),\n    (0xFB3E, 'M', u'מּ'),\n    (0xFB3F, 'X'),\n    (0xFB40, 'M', u'נּ'),\n    (0xFB41, 'M', u'סּ'),\n    (0xFB42, 'X'),\n    (0xFB43, 'M', u'ףּ'),\n    (0xFB44, 'M', u'פּ'),\n    (0xFB45, 'X'),\n    (0xFB46, 'M', u'צּ'),\n    (0xFB47, 'M', u'קּ'),\n    (0xFB48, 'M', u'רּ'),\n    (0xFB49, 'M', u'שּ'),\n    (0xFB4A, 'M', u'תּ'),\n    (0xFB4B, 'M', u'וֹ'),\n    (0xFB4C, 'M', u'בֿ'),\n    (0xFB4D, 'M', u'כֿ'),\n    (0xFB4E, 'M', u'פֿ'),\n    (0xFB4F, 'M', u'אל'),\n    (0xFB50, 'M', u'ٱ'),\n    (0xFB52, 'M', u'ٻ'),\n    (0xFB56, 'M', u'پ'),\n    (0xFB5A, 'M', u'ڀ'),\n    (0xFB5E, 'M', u'ٺ'),\n    (0xFB62, 'M', u'ٿ'),\n    (0xFB66, 'M', u'ٹ'),\n    (0xFB6A, 'M', u'ڤ'),\n    (0xFB6E, 'M', u'ڦ'),\n    (0xFB72, 'M', u'ڄ'),\n    (0xFB76, 'M', u'ڃ'),\n    (0xFB7A, 'M', u'چ'),\n    (0xFB7E, 'M', u'ڇ'),\n    (0xFB82, 'M', u'ڍ'),\n    (0xFB84, 'M', u'ڌ'),\n    (0xFB86, 'M', u'ڎ'),\n    (0xFB88, 'M', u'ڈ'),\n    (0xFB8A, 'M', u'ژ'),\n    (0xFB8C, 'M', u'ڑ'),\n    (0xFB8E, 'M', u'ک'),\n    (0xFB92, 'M', u'گ'),\n    (0xFB96, 'M', u'ڳ'),\n    (0xFB9A, 'M', u'ڱ'),\n    (0xFB9E, 'M', u'ں'),\n    (0xFBA0, 'M', u'ڻ'),\n    (0xFBA4, 'M', u'ۀ'),\n    (0xFBA6, 'M', u'ہ'),\n    (0xFBAA, 'M', u'ھ'),\n    (0xFBAE, 'M', u'ے'),\n    (0xFBB0, 'M', u'ۓ'),\n    (0xFBB2, 'V'),\n    (0xFBC2, 'X'),\n    (0xFBD3, 'M', u'ڭ'),\n    (0xFBD7, 'M', u'ۇ'),\n    (0xFBD9, 'M', u'ۆ'),\n    (0xFBDB, 'M', u'ۈ'),\n    (0xFBDD, 'M', u'ۇٴ'),\n    (0xFBDE, 'M', u'ۋ'),\n    (0xFBE0, 'M', u'ۅ'),\n    (0xFBE2, 'M', u'ۉ'),\n    (0xFBE4, 'M', u'ې'),\n    (0xFBE8, 'M', u'ى'),\n    (0xFBEA, 'M', u'ئا'),\n    (0xFBEC, 'M', u'ئە'),\n    (0xFBEE, 'M', u'ئو'),\n    (0xFBF0, 'M', u'ئۇ'),\n    (0xFBF2, 'M', u'ئۆ'),\n    ]\n\ndef _seg_45():\n    return [\n    (0xFBF4, 'M', u'ئۈ'),\n    (0xFBF6, 'M', u'ئې'),\n    (0xFBF9, 'M', u'ئى'),\n    (0xFBFC, 'M', u'ی'),\n    (0xFC00, 'M', u'ئج'),\n    (0xFC01, 'M', u'ئح'),\n    (0xFC02, 'M', u'ئم'),\n    (0xFC03, 'M', u'ئى'),\n    (0xFC04, 'M', u'ئي'),\n    (0xFC05, 'M', u'بج'),\n    (0xFC06, 'M', u'بح'),\n    (0xFC07, 'M', u'بخ'),\n    (0xFC08, 'M', u'بم'),\n    (0xFC09, 'M', u'بى'),\n    (0xFC0A, 'M', u'بي'),\n    (0xFC0B, 'M', u'تج'),\n    (0xFC0C, 'M', u'تح'),\n    (0xFC0D, 'M', u'تخ'),\n    (0xFC0E, 'M', u'تم'),\n    (0xFC0F, 'M', u'تى'),\n    (0xFC10, 'M', u'تي'),\n    (0xFC11, 'M', u'ثج'),\n    (0xFC12, 'M', u'ثم'),\n    (0xFC13, 'M', u'ثى'),\n    (0xFC14, 'M', u'ثي'),\n    (0xFC15, 'M', u'جح'),\n    (0xFC16, 'M', u'جم'),\n    (0xFC17, 'M', u'حج'),\n    (0xFC18, 'M', u'حم'),\n    (0xFC19, 'M', u'خج'),\n    (0xFC1A, 'M', u'خح'),\n    (0xFC1B, 'M', u'خم'),\n    (0xFC1C, 'M', u'سج'),\n    (0xFC1D, 'M', u'سح'),\n    (0xFC1E, 'M', u'سخ'),\n    (0xFC1F, 'M', u'سم'),\n    (0xFC20, 'M', u'صح'),\n    (0xFC21, 'M', u'صم'),\n    (0xFC22, 'M', u'ضج'),\n    (0xFC23, 'M', u'ضح'),\n    (0xFC24, 'M', u'ضخ'),\n    (0xFC25, 'M', u'ضم'),\n    (0xFC26, 'M', u'طح'),\n    (0xFC27, 'M', u'طم'),\n    (0xFC28, 'M', u'ظم'),\n    (0xFC29, 'M', u'عج'),\n    (0xFC2A, 'M', u'عم'),\n    (0xFC2B, 'M', u'غج'),\n    (0xFC2C, 'M', u'غم'),\n    (0xFC2D, 'M', u'فج'),\n    (0xFC2E, 'M', u'فح'),\n    (0xFC2F, 'M', u'فخ'),\n    (0xFC30, 'M', u'فم'),\n    (0xFC31, 'M', u'فى'),\n    (0xFC32, 'M', u'في'),\n    (0xFC33, 'M', u'قح'),\n    (0xFC34, 'M', u'قم'),\n    (0xFC35, 'M', u'قى'),\n    (0xFC36, 'M', u'قي'),\n    (0xFC37, 'M', u'كا'),\n    (0xFC38, 'M', u'كج'),\n    (0xFC39, 'M', u'كح'),\n    (0xFC3A, 'M', u'كخ'),\n    (0xFC3B, 'M', u'كل'),\n    (0xFC3C, 'M', u'كم'),\n    (0xFC3D, 'M', u'كى'),\n    (0xFC3E, 'M', u'كي'),\n    (0xFC3F, 'M', u'لج'),\n    (0xFC40, 'M', u'لح'),\n    (0xFC41, 'M', u'لخ'),\n    (0xFC42, 'M', u'لم'),\n    (0xFC43, 'M', u'لى'),\n    (0xFC44, 'M', u'لي'),\n    (0xFC45, 'M', u'مج'),\n    (0xFC46, 'M', u'مح'),\n    (0xFC47, 'M', u'مخ'),\n    (0xFC48, 'M', u'مم'),\n    (0xFC49, 'M', u'مى'),\n    (0xFC4A, 'M', u'مي'),\n    (0xFC4B, 'M', u'نج'),\n    (0xFC4C, 'M', u'نح'),\n    (0xFC4D, 'M', u'نخ'),\n    (0xFC4E, 'M', u'نم'),\n    (0xFC4F, 'M', u'نى'),\n    (0xFC50, 'M', u'ني'),\n    (0xFC51, 'M', u'هج'),\n    (0xFC52, 'M', u'هم'),\n    (0xFC53, 'M', u'هى'),\n    (0xFC54, 'M', u'هي'),\n    (0xFC55, 'M', u'يج'),\n    (0xFC56, 'M', u'يح'),\n    (0xFC57, 'M', u'يخ'),\n    (0xFC58, 'M', u'يم'),\n    (0xFC59, 'M', u'يى'),\n    (0xFC5A, 'M', u'يي'),\n    (0xFC5B, 'M', u'ذٰ'),\n    (0xFC5C, 'M', u'رٰ'),\n    (0xFC5D, 'M', u'ىٰ'),\n    (0xFC5E, '3', u' ٌّ'),\n    (0xFC5F, '3', u' ٍّ'),\n    ]\n\ndef _seg_46():\n    return [\n    (0xFC60, '3', u' َّ'),\n    (0xFC61, '3', u' ُّ'),\n    (0xFC62, '3', u' ِّ'),\n    (0xFC63, '3', u' ّٰ'),\n    (0xFC64, 'M', u'ئر'),\n    (0xFC65, 'M', u'ئز'),\n    (0xFC66, 'M', u'ئم'),\n    (0xFC67, 'M', u'ئن'),\n    (0xFC68, 'M', u'ئى'),\n    (0xFC69, 'M', u'ئي'),\n    (0xFC6A, 'M', u'بر'),\n    (0xFC6B, 'M', u'بز'),\n    (0xFC6C, 'M', u'بم'),\n    (0xFC6D, 'M', u'بن'),\n    (0xFC6E, 'M', u'بى'),\n    (0xFC6F, 'M', u'بي'),\n    (0xFC70, 'M', u'تر'),\n    (0xFC71, 'M', u'تز'),\n    (0xFC72, 'M', u'تم'),\n    (0xFC73, 'M', u'تن'),\n    (0xFC74, 'M', u'تى'),\n    (0xFC75, 'M', u'تي'),\n    (0xFC76, 'M', u'ثر'),\n    (0xFC77, 'M', u'ثز'),\n    (0xFC78, 'M', u'ثم'),\n    (0xFC79, 'M', u'ثن'),\n    (0xFC7A, 'M', u'ثى'),\n    (0xFC7B, 'M', u'ثي'),\n    (0xFC7C, 'M', u'فى'),\n    (0xFC7D, 'M', u'في'),\n    (0xFC7E, 'M', u'قى'),\n    (0xFC7F, 'M', u'قي'),\n    (0xFC80, 'M', u'كا'),\n    (0xFC81, 'M', u'كل'),\n    (0xFC82, 'M', u'كم'),\n    (0xFC83, 'M', u'كى'),\n    (0xFC84, 'M', u'كي'),\n    (0xFC85, 'M', u'لم'),\n    (0xFC86, 'M', u'لى'),\n    (0xFC87, 'M', u'لي'),\n    (0xFC88, 'M', u'ما'),\n    (0xFC89, 'M', u'مم'),\n    (0xFC8A, 'M', u'نر'),\n    (0xFC8B, 'M', u'نز'),\n    (0xFC8C, 'M', u'نم'),\n    (0xFC8D, 'M', u'نن'),\n    (0xFC8E, 'M', u'نى'),\n    (0xFC8F, 'M', u'ني'),\n    (0xFC90, 'M', u'ىٰ'),\n    (0xFC91, 'M', u'ير'),\n    (0xFC92, 'M', u'يز'),\n    (0xFC93, 'M', u'يم'),\n    (0xFC94, 'M', u'ين'),\n    (0xFC95, 'M', u'يى'),\n    (0xFC96, 'M', u'يي'),\n    (0xFC97, 'M', u'ئج'),\n    (0xFC98, 'M', u'ئح'),\n    (0xFC99, 'M', u'ئخ'),\n    (0xFC9A, 'M', u'ئم'),\n    (0xFC9B, 'M', u'ئه'),\n    (0xFC9C, 'M', u'بج'),\n    (0xFC9D, 'M', u'بح'),\n    (0xFC9E, 'M', u'بخ'),\n    (0xFC9F, 'M', u'بم'),\n    (0xFCA0, 'M', u'به'),\n    (0xFCA1, 'M', u'تج'),\n    (0xFCA2, 'M', u'تح'),\n    (0xFCA3, 'M', u'تخ'),\n    (0xFCA4, 'M', u'تم'),\n    (0xFCA5, 'M', u'ته'),\n    (0xFCA6, 'M', u'ثم'),\n    (0xFCA7, 'M', u'جح'),\n    (0xFCA8, 'M', u'جم'),\n    (0xFCA9, 'M', u'حج'),\n    (0xFCAA, 'M', u'حم'),\n    (0xFCAB, 'M', u'خج'),\n    (0xFCAC, 'M', u'خم'),\n    (0xFCAD, 'M', u'سج'),\n    (0xFCAE, 'M', u'سح'),\n    (0xFCAF, 'M', u'سخ'),\n    (0xFCB0, 'M', u'سم'),\n    (0xFCB1, 'M', u'صح'),\n    (0xFCB2, 'M', u'صخ'),\n    (0xFCB3, 'M', u'صم'),\n    (0xFCB4, 'M', u'ضج'),\n    (0xFCB5, 'M', u'ضح'),\n    (0xFCB6, 'M', u'ضخ'),\n    (0xFCB7, 'M', u'ضم'),\n    (0xFCB8, 'M', u'طح'),\n    (0xFCB9, 'M', u'ظم'),\n    (0xFCBA, 'M', u'عج'),\n    (0xFCBB, 'M', u'عم'),\n    (0xFCBC, 'M', u'غج'),\n    (0xFCBD, 'M', u'غم'),\n    (0xFCBE, 'M', u'فج'),\n    (0xFCBF, 'M', u'فح'),\n    (0xFCC0, 'M', u'فخ'),\n    (0xFCC1, 'M', u'فم'),\n    (0xFCC2, 'M', u'قح'),\n    (0xFCC3, 'M', u'قم'),\n    ]\n\ndef _seg_47():\n    return [\n    (0xFCC4, 'M', u'كج'),\n    (0xFCC5, 'M', u'كح'),\n    (0xFCC6, 'M', u'كخ'),\n    (0xFCC7, 'M', u'كل'),\n    (0xFCC8, 'M', u'كم'),\n    (0xFCC9, 'M', u'لج'),\n    (0xFCCA, 'M', u'لح'),\n    (0xFCCB, 'M', u'لخ'),\n    (0xFCCC, 'M', u'لم'),\n    (0xFCCD, 'M', u'له'),\n    (0xFCCE, 'M', u'مج'),\n    (0xFCCF, 'M', u'مح'),\n    (0xFCD0, 'M', u'مخ'),\n    (0xFCD1, 'M', u'مم'),\n    (0xFCD2, 'M', u'نج'),\n    (0xFCD3, 'M', u'نح'),\n    (0xFCD4, 'M', u'نخ'),\n    (0xFCD5, 'M', u'نم'),\n    (0xFCD6, 'M', u'نه'),\n    (0xFCD7, 'M', u'هج'),\n    (0xFCD8, 'M', u'هم'),\n    (0xFCD9, 'M', u'هٰ'),\n    (0xFCDA, 'M', u'يج'),\n    (0xFCDB, 'M', u'يح'),\n    (0xFCDC, 'M', u'يخ'),\n    (0xFCDD, 'M', u'يم'),\n    (0xFCDE, 'M', u'يه'),\n    (0xFCDF, 'M', u'ئم'),\n    (0xFCE0, 'M', u'ئه'),\n    (0xFCE1, 'M', u'بم'),\n    (0xFCE2, 'M', u'به'),\n    (0xFCE3, 'M', u'تم'),\n    (0xFCE4, 'M', u'ته'),\n    (0xFCE5, 'M', u'ثم'),\n    (0xFCE6, 'M', u'ثه'),\n    (0xFCE7, 'M', u'سم'),\n    (0xFCE8, 'M', u'سه'),\n    (0xFCE9, 'M', u'شم'),\n    (0xFCEA, 'M', u'شه'),\n    (0xFCEB, 'M', u'كل'),\n    (0xFCEC, 'M', u'كم'),\n    (0xFCED, 'M', u'لم'),\n    (0xFCEE, 'M', u'نم'),\n    (0xFCEF, 'M', u'نه'),\n    (0xFCF0, 'M', u'يم'),\n    (0xFCF1, 'M', u'يه'),\n    (0xFCF2, 'M', u'ـَّ'),\n    (0xFCF3, 'M', u'ـُّ'),\n    (0xFCF4, 'M', u'ـِّ'),\n    (0xFCF5, 'M', u'طى'),\n    (0xFCF6, 'M', u'طي'),\n    (0xFCF7, 'M', u'عى'),\n    (0xFCF8, 'M', u'عي'),\n    (0xFCF9, 'M', u'غى'),\n    (0xFCFA, 'M', u'غي'),\n    (0xFCFB, 'M', u'سى'),\n    (0xFCFC, 'M', u'سي'),\n    (0xFCFD, 'M', u'شى'),\n    (0xFCFE, 'M', u'شي'),\n    (0xFCFF, 'M', u'حى'),\n    (0xFD00, 'M', u'حي'),\n    (0xFD01, 'M', u'جى'),\n    (0xFD02, 'M', u'جي'),\n    (0xFD03, 'M', u'خى'),\n    (0xFD04, 'M', u'خي'),\n    (0xFD05, 'M', u'صى'),\n    (0xFD06, 'M', u'صي'),\n    (0xFD07, 'M', u'ضى'),\n    (0xFD08, 'M', u'ضي'),\n    (0xFD09, 'M', u'شج'),\n    (0xFD0A, 'M', u'شح'),\n    (0xFD0B, 'M', u'شخ'),\n    (0xFD0C, 'M', u'شم'),\n    (0xFD0D, 'M', u'شر'),\n    (0xFD0E, 'M', u'سر'),\n    (0xFD0F, 'M', u'صر'),\n    (0xFD10, 'M', u'ضر'),\n    (0xFD11, 'M', u'طى'),\n    (0xFD12, 'M', u'طي'),\n    (0xFD13, 'M', u'عى'),\n    (0xFD14, 'M', u'عي'),\n    (0xFD15, 'M', u'غى'),\n    (0xFD16, 'M', u'غي'),\n    (0xFD17, 'M', u'سى'),\n    (0xFD18, 'M', u'سي'),\n    (0xFD19, 'M', u'شى'),\n    (0xFD1A, 'M', u'شي'),\n    (0xFD1B, 'M', u'حى'),\n    (0xFD1C, 'M', u'حي'),\n    (0xFD1D, 'M', u'جى'),\n    (0xFD1E, 'M', u'جي'),\n    (0xFD1F, 'M', u'خى'),\n    (0xFD20, 'M', u'خي'),\n    (0xFD21, 'M', u'صى'),\n    (0xFD22, 'M', u'صي'),\n    (0xFD23, 'M', u'ضى'),\n    (0xFD24, 'M', u'ضي'),\n    (0xFD25, 'M', u'شج'),\n    (0xFD26, 'M', u'شح'),\n    (0xFD27, 'M', u'شخ'),\n    ]\n\ndef _seg_48():\n    return [\n    (0xFD28, 'M', u'شم'),\n    (0xFD29, 'M', u'شر'),\n    (0xFD2A, 'M', u'سر'),\n    (0xFD2B, 'M', u'صر'),\n    (0xFD2C, 'M', u'ضر'),\n    (0xFD2D, 'M', u'شج'),\n    (0xFD2E, 'M', u'شح'),\n    (0xFD2F, 'M', u'شخ'),\n    (0xFD30, 'M', u'شم'),\n    (0xFD31, 'M', u'سه'),\n    (0xFD32, 'M', u'شه'),\n    (0xFD33, 'M', u'طم'),\n    (0xFD34, 'M', u'سج'),\n    (0xFD35, 'M', u'سح'),\n    (0xFD36, 'M', u'سخ'),\n    (0xFD37, 'M', u'شج'),\n    (0xFD38, 'M', u'شح'),\n    (0xFD39, 'M', u'شخ'),\n    (0xFD3A, 'M', u'طم'),\n    (0xFD3B, 'M', u'ظم'),\n    (0xFD3C, 'M', u'اً'),\n    (0xFD3E, 'V'),\n    (0xFD40, 'X'),\n    (0xFD50, 'M', u'تجم'),\n    (0xFD51, 'M', u'تحج'),\n    (0xFD53, 'M', u'تحم'),\n    (0xFD54, 'M', u'تخم'),\n    (0xFD55, 'M', u'تمج'),\n    (0xFD56, 'M', u'تمح'),\n    (0xFD57, 'M', u'تمخ'),\n    (0xFD58, 'M', u'جمح'),\n    (0xFD5A, 'M', u'حمي'),\n    (0xFD5B, 'M', u'حمى'),\n    (0xFD5C, 'M', u'سحج'),\n    (0xFD5D, 'M', u'سجح'),\n    (0xFD5E, 'M', u'سجى'),\n    (0xFD5F, 'M', u'سمح'),\n    (0xFD61, 'M', u'سمج'),\n    (0xFD62, 'M', u'سمم'),\n    (0xFD64, 'M', u'صحح'),\n    (0xFD66, 'M', u'صمم'),\n    (0xFD67, 'M', u'شحم'),\n    (0xFD69, 'M', u'شجي'),\n    (0xFD6A, 'M', u'شمخ'),\n    (0xFD6C, 'M', u'شمم'),\n    (0xFD6E, 'M', u'ضحى'),\n    (0xFD6F, 'M', u'ضخم'),\n    (0xFD71, 'M', u'طمح'),\n    (0xFD73, 'M', u'طمم'),\n    (0xFD74, 'M', u'طمي'),\n    (0xFD75, 'M', u'عجم'),\n    (0xFD76, 'M', u'عمم'),\n    (0xFD78, 'M', u'عمى'),\n    (0xFD79, 'M', u'غمم'),\n    (0xFD7A, 'M', u'غمي'),\n    (0xFD7B, 'M', u'غمى'),\n    (0xFD7C, 'M', u'فخم'),\n    (0xFD7E, 'M', u'قمح'),\n    (0xFD7F, 'M', u'قمم'),\n    (0xFD80, 'M', u'لحم'),\n    (0xFD81, 'M', u'لحي'),\n    (0xFD82, 'M', u'لحى'),\n    (0xFD83, 'M', u'لجج'),\n    (0xFD85, 'M', u'لخم'),\n    (0xFD87, 'M', u'لمح'),\n    (0xFD89, 'M', u'محج'),\n    (0xFD8A, 'M', u'محم'),\n    (0xFD8B, 'M', u'محي'),\n    (0xFD8C, 'M', u'مجح'),\n    (0xFD8D, 'M', u'مجم'),\n    (0xFD8E, 'M', u'مخج'),\n    (0xFD8F, 'M', u'مخم'),\n    (0xFD90, 'X'),\n    (0xFD92, 'M', u'مجخ'),\n    (0xFD93, 'M', u'همج'),\n    (0xFD94, 'M', u'همم'),\n    (0xFD95, 'M', u'نحم'),\n    (0xFD96, 'M', u'نحى'),\n    (0xFD97, 'M', u'نجم'),\n    (0xFD99, 'M', u'نجى'),\n    (0xFD9A, 'M', u'نمي'),\n    (0xFD9B, 'M', u'نمى'),\n    (0xFD9C, 'M', u'يمم'),\n    (0xFD9E, 'M', u'بخي'),\n    (0xFD9F, 'M', u'تجي'),\n    (0xFDA0, 'M', u'تجى'),\n    (0xFDA1, 'M', u'تخي'),\n    (0xFDA2, 'M', u'تخى'),\n    (0xFDA3, 'M', u'تمي'),\n    (0xFDA4, 'M', u'تمى'),\n    (0xFDA5, 'M', u'جمي'),\n    (0xFDA6, 'M', u'جحى'),\n    (0xFDA7, 'M', u'جمى'),\n    (0xFDA8, 'M', u'سخى'),\n    (0xFDA9, 'M', u'صحي'),\n    (0xFDAA, 'M', u'شحي'),\n    (0xFDAB, 'M', u'ضحي'),\n    (0xFDAC, 'M', u'لجي'),\n    (0xFDAD, 'M', u'لمي'),\n    (0xFDAE, 'M', u'يحي'),\n    ]\n\ndef _seg_49():\n    return [\n    (0xFDAF, 'M', u'يجي'),\n    (0xFDB0, 'M', u'يمي'),\n    (0xFDB1, 'M', u'ممي'),\n    (0xFDB2, 'M', u'قمي'),\n    (0xFDB3, 'M', u'نحي'),\n    (0xFDB4, 'M', u'قمح'),\n    (0xFDB5, 'M', u'لحم'),\n    (0xFDB6, 'M', u'عمي'),\n    (0xFDB7, 'M', u'كمي'),\n    (0xFDB8, 'M', u'نجح'),\n    (0xFDB9, 'M', u'مخي'),\n    (0xFDBA, 'M', u'لجم'),\n    (0xFDBB, 'M', u'كمم'),\n    (0xFDBC, 'M', u'لجم'),\n    (0xFDBD, 'M', u'نجح'),\n    (0xFDBE, 'M', u'جحي'),\n    (0xFDBF, 'M', u'حجي'),\n    (0xFDC0, 'M', u'مجي'),\n    (0xFDC1, 'M', u'فمي'),\n    (0xFDC2, 'M', u'بحي'),\n    (0xFDC3, 'M', u'كمم'),\n    (0xFDC4, 'M', u'عجم'),\n    (0xFDC5, 'M', u'صمم'),\n    (0xFDC6, 'M', u'سخي'),\n    (0xFDC7, 'M', u'نجي'),\n    (0xFDC8, 'X'),\n    (0xFDF0, 'M', u'صلے'),\n    (0xFDF1, 'M', u'قلے'),\n    (0xFDF2, 'M', u'الله'),\n    (0xFDF3, 'M', u'اكبر'),\n    (0xFDF4, 'M', u'محمد'),\n    (0xFDF5, 'M', u'صلعم'),\n    (0xFDF6, 'M', u'رسول'),\n    (0xFDF7, 'M', u'عليه'),\n    (0xFDF8, 'M', u'وسلم'),\n    (0xFDF9, 'M', u'صلى'),\n    (0xFDFA, '3', u'صلى الله عليه وسلم'),\n    (0xFDFB, '3', u'جل جلاله'),\n    (0xFDFC, 'M', u'ریال'),\n    (0xFDFD, 'V'),\n    (0xFDFE, 'X'),\n    (0xFE00, 'I'),\n    (0xFE10, '3', u','),\n    (0xFE11, 'M', u'、'),\n    (0xFE12, 'X'),\n    (0xFE13, '3', u':'),\n    (0xFE14, '3', u';'),\n    (0xFE15, '3', u'!'),\n    (0xFE16, '3', u'?'),\n    (0xFE17, 'M', u'〖'),\n    (0xFE18, 'M', u'〗'),\n    (0xFE19, 'X'),\n    (0xFE20, 'V'),\n    (0xFE30, 'X'),\n    (0xFE31, 'M', u'—'),\n    (0xFE32, 'M', u'–'),\n    (0xFE33, '3', u'_'),\n    (0xFE35, '3', u'('),\n    (0xFE36, '3', u')'),\n    (0xFE37, '3', u'{'),\n    (0xFE38, '3', u'}'),\n    (0xFE39, 'M', u'〔'),\n    (0xFE3A, 'M', u'〕'),\n    (0xFE3B, 'M', u'【'),\n    (0xFE3C, 'M', u'】'),\n    (0xFE3D, 'M', u'《'),\n    (0xFE3E, 'M', u'》'),\n    (0xFE3F, 'M', u'〈'),\n    (0xFE40, 'M', u'〉'),\n    (0xFE41, 'M', u'「'),\n    (0xFE42, 'M', u'」'),\n    (0xFE43, 'M', u'『'),\n    (0xFE44, 'M', u'』'),\n    (0xFE45, 'V'),\n    (0xFE47, '3', u'['),\n    (0xFE48, '3', u']'),\n    (0xFE49, '3', u' ̅'),\n    (0xFE4D, '3', u'_'),\n    (0xFE50, '3', u','),\n    (0xFE51, 'M', u'、'),\n    (0xFE52, 'X'),\n    (0xFE54, '3', u';'),\n    (0xFE55, '3', u':'),\n    (0xFE56, '3', u'?'),\n    (0xFE57, '3', u'!'),\n    (0xFE58, 'M', u'—'),\n    (0xFE59, '3', u'('),\n    (0xFE5A, '3', u')'),\n    (0xFE5B, '3', u'{'),\n    (0xFE5C, '3', u'}'),\n    (0xFE5D, 'M', u'〔'),\n    (0xFE5E, 'M', u'〕'),\n    (0xFE5F, '3', u'#'),\n    (0xFE60, '3', u'&'),\n    (0xFE61, '3', u'*'),\n    (0xFE62, '3', u'+'),\n    (0xFE63, 'M', u'-'),\n    (0xFE64, '3', u'<'),\n    (0xFE65, '3', u'>'),\n    (0xFE66, '3', u'='),\n    ]\n\ndef _seg_50():\n    return [\n    (0xFE67, 'X'),\n    (0xFE68, '3', u'\\\\'),\n    (0xFE69, '3', u'$'),\n    (0xFE6A, '3', u'%'),\n    (0xFE6B, '3', u'@'),\n    (0xFE6C, 'X'),\n    (0xFE70, '3', u' ً'),\n    (0xFE71, 'M', u'ـً'),\n    (0xFE72, '3', u' ٌ'),\n    (0xFE73, 'V'),\n    (0xFE74, '3', u' ٍ'),\n    (0xFE75, 'X'),\n    (0xFE76, '3', u' َ'),\n    (0xFE77, 'M', u'ـَ'),\n    (0xFE78, '3', u' ُ'),\n    (0xFE79, 'M', u'ـُ'),\n    (0xFE7A, '3', u' ِ'),\n    (0xFE7B, 'M', u'ـِ'),\n    (0xFE7C, '3', u' ّ'),\n    (0xFE7D, 'M', u'ـّ'),\n    (0xFE7E, '3', u' ْ'),\n    (0xFE7F, 'M', u'ـْ'),\n    (0xFE80, 'M', u'ء'),\n    (0xFE81, 'M', u'آ'),\n    (0xFE83, 'M', u'أ'),\n    (0xFE85, 'M', u'ؤ'),\n    (0xFE87, 'M', u'إ'),\n    (0xFE89, 'M', u'ئ'),\n    (0xFE8D, 'M', u'ا'),\n    (0xFE8F, 'M', u'ب'),\n    (0xFE93, 'M', u'ة'),\n    (0xFE95, 'M', u'ت'),\n    (0xFE99, 'M', u'ث'),\n    (0xFE9D, 'M', u'ج'),\n    (0xFEA1, 'M', u'ح'),\n    (0xFEA5, 'M', u'خ'),\n    (0xFEA9, 'M', u'د'),\n    (0xFEAB, 'M', u'ذ'),\n    (0xFEAD, 'M', u'ر'),\n    (0xFEAF, 'M', u'ز'),\n    (0xFEB1, 'M', u'س'),\n    (0xFEB5, 'M', u'ش'),\n    (0xFEB9, 'M', u'ص'),\n    (0xFEBD, 'M', u'ض'),\n    (0xFEC1, 'M', u'ط'),\n    (0xFEC5, 'M', u'ظ'),\n    (0xFEC9, 'M', u'ع'),\n    (0xFECD, 'M', u'غ'),\n    (0xFED1, 'M', u'ف'),\n    (0xFED5, 'M', u'ق'),\n    (0xFED9, 'M', u'ك'),\n    (0xFEDD, 'M', u'ل'),\n    (0xFEE1, 'M', u'م'),\n    (0xFEE5, 'M', u'ن'),\n    (0xFEE9, 'M', u'ه'),\n    (0xFEED, 'M', u'و'),\n    (0xFEEF, 'M', u'ى'),\n    (0xFEF1, 'M', u'ي'),\n    (0xFEF5, 'M', u'لآ'),\n    (0xFEF7, 'M', u'لأ'),\n    (0xFEF9, 'M', u'لإ'),\n    (0xFEFB, 'M', u'لا'),\n    (0xFEFD, 'X'),\n    (0xFEFF, 'I'),\n    (0xFF00, 'X'),\n    (0xFF01, '3', u'!'),\n    (0xFF02, '3', u'\"'),\n    (0xFF03, '3', u'#'),\n    (0xFF04, '3', u'$'),\n    (0xFF05, '3', u'%'),\n    (0xFF06, '3', u'&'),\n    (0xFF07, '3', u'\\''),\n    (0xFF08, '3', u'('),\n    (0xFF09, '3', u')'),\n    (0xFF0A, '3', u'*'),\n    (0xFF0B, '3', u'+'),\n    (0xFF0C, '3', u','),\n    (0xFF0D, 'M', u'-'),\n    (0xFF0E, 'M', u'.'),\n    (0xFF0F, '3', u'/'),\n    (0xFF10, 'M', u'0'),\n    (0xFF11, 'M', u'1'),\n    (0xFF12, 'M', u'2'),\n    (0xFF13, 'M', u'3'),\n    (0xFF14, 'M', u'4'),\n    (0xFF15, 'M', u'5'),\n    (0xFF16, 'M', u'6'),\n    (0xFF17, 'M', u'7'),\n    (0xFF18, 'M', u'8'),\n    (0xFF19, 'M', u'9'),\n    (0xFF1A, '3', u':'),\n    (0xFF1B, '3', u';'),\n    (0xFF1C, '3', u'<'),\n    (0xFF1D, '3', u'='),\n    (0xFF1E, '3', u'>'),\n    (0xFF1F, '3', u'?'),\n    (0xFF20, '3', u'@'),\n    (0xFF21, 'M', u'a'),\n    (0xFF22, 'M', u'b'),\n    (0xFF23, 'M', u'c'),\n    ]\n\ndef _seg_51():\n    return [\n    (0xFF24, 'M', u'd'),\n    (0xFF25, 'M', u'e'),\n    (0xFF26, 'M', u'f'),\n    (0xFF27, 'M', u'g'),\n    (0xFF28, 'M', u'h'),\n    (0xFF29, 'M', u'i'),\n    (0xFF2A, 'M', u'j'),\n    (0xFF2B, 'M', u'k'),\n    (0xFF2C, 'M', u'l'),\n    (0xFF2D, 'M', u'm'),\n    (0xFF2E, 'M', u'n'),\n    (0xFF2F, 'M', u'o'),\n    (0xFF30, 'M', u'p'),\n    (0xFF31, 'M', u'q'),\n    (0xFF32, 'M', u'r'),\n    (0xFF33, 'M', u's'),\n    (0xFF34, 'M', u't'),\n    (0xFF35, 'M', u'u'),\n    (0xFF36, 'M', u'v'),\n    (0xFF37, 'M', u'w'),\n    (0xFF38, 'M', u'x'),\n    (0xFF39, 'M', u'y'),\n    (0xFF3A, 'M', u'z'),\n    (0xFF3B, '3', u'['),\n    (0xFF3C, '3', u'\\\\'),\n    (0xFF3D, '3', u']'),\n    (0xFF3E, '3', u'^'),\n    (0xFF3F, '3', u'_'),\n    (0xFF40, '3', u'`'),\n    (0xFF41, 'M', u'a'),\n    (0xFF42, 'M', u'b'),\n    (0xFF43, 'M', u'c'),\n    (0xFF44, 'M', u'd'),\n    (0xFF45, 'M', u'e'),\n    (0xFF46, 'M', u'f'),\n    (0xFF47, 'M', u'g'),\n    (0xFF48, 'M', u'h'),\n    (0xFF49, 'M', u'i'),\n    (0xFF4A, 'M', u'j'),\n    (0xFF4B, 'M', u'k'),\n    (0xFF4C, 'M', u'l'),\n    (0xFF4D, 'M', u'm'),\n    (0xFF4E, 'M', u'n'),\n    (0xFF4F, 'M', u'o'),\n    (0xFF50, 'M', u'p'),\n    (0xFF51, 'M', u'q'),\n    (0xFF52, 'M', u'r'),\n    (0xFF53, 'M', u's'),\n    (0xFF54, 'M', u't'),\n    (0xFF55, 'M', u'u'),\n    (0xFF56, 'M', u'v'),\n    (0xFF57, 'M', u'w'),\n    (0xFF58, 'M', u'x'),\n    (0xFF59, 'M', u'y'),\n    (0xFF5A, 'M', u'z'),\n    (0xFF5B, '3', u'{'),\n    (0xFF5C, '3', u'|'),\n    (0xFF5D, '3', u'}'),\n    (0xFF5E, '3', u'~'),\n    (0xFF5F, 'M', u'⦅'),\n    (0xFF60, 'M', u'⦆'),\n    (0xFF61, 'M', u'.'),\n    (0xFF62, 'M', u'「'),\n    (0xFF63, 'M', u'」'),\n    (0xFF64, 'M', u'、'),\n    (0xFF65, 'M', u'・'),\n    (0xFF66, 'M', u'ヲ'),\n    (0xFF67, 'M', u'ァ'),\n    (0xFF68, 'M', u'ィ'),\n    (0xFF69, 'M', u'ゥ'),\n    (0xFF6A, 'M', u'ェ'),\n    (0xFF6B, 'M', u'ォ'),\n    (0xFF6C, 'M', u'ャ'),\n    (0xFF6D, 'M', u'ュ'),\n    (0xFF6E, 'M', u'ョ'),\n    (0xFF6F, 'M', u'ッ'),\n    (0xFF70, 'M', u'ー'),\n    (0xFF71, 'M', u'ア'),\n    (0xFF72, 'M', u'イ'),\n    (0xFF73, 'M', u'ウ'),\n    (0xFF74, 'M', u'エ'),\n    (0xFF75, 'M', u'オ'),\n    (0xFF76, 'M', u'カ'),\n    (0xFF77, 'M', u'キ'),\n    (0xFF78, 'M', u'ク'),\n    (0xFF79, 'M', u'ケ'),\n    (0xFF7A, 'M', u'コ'),\n    (0xFF7B, 'M', u'サ'),\n    (0xFF7C, 'M', u'シ'),\n    (0xFF7D, 'M', u'ス'),\n    (0xFF7E, 'M', u'セ'),\n    (0xFF7F, 'M', u'ソ'),\n    (0xFF80, 'M', u'タ'),\n    (0xFF81, 'M', u'チ'),\n    (0xFF82, 'M', u'ツ'),\n    (0xFF83, 'M', u'テ'),\n    (0xFF84, 'M', u'ト'),\n    (0xFF85, 'M', u'ナ'),\n    (0xFF86, 'M', u'ニ'),\n    (0xFF87, 'M', u'ヌ'),\n    ]\n\ndef _seg_52():\n    return [\n    (0xFF88, 'M', u'ネ'),\n    (0xFF89, 'M', u'ノ'),\n    (0xFF8A, 'M', u'ハ'),\n    (0xFF8B, 'M', u'ヒ'),\n    (0xFF8C, 'M', u'フ'),\n    (0xFF8D, 'M', u'ヘ'),\n    (0xFF8E, 'M', u'ホ'),\n    (0xFF8F, 'M', u'マ'),\n    (0xFF90, 'M', u'ミ'),\n    (0xFF91, 'M', u'ム'),\n    (0xFF92, 'M', u'メ'),\n    (0xFF93, 'M', u'モ'),\n    (0xFF94, 'M', u'ヤ'),\n    (0xFF95, 'M', u'ユ'),\n    (0xFF96, 'M', u'ヨ'),\n    (0xFF97, 'M', u'ラ'),\n    (0xFF98, 'M', u'リ'),\n    (0xFF99, 'M', u'ル'),\n    (0xFF9A, 'M', u'レ'),\n    (0xFF9B, 'M', u'ロ'),\n    (0xFF9C, 'M', u'ワ'),\n    (0xFF9D, 'M', u'ン'),\n    (0xFF9E, 'M', u'゙'),\n    (0xFF9F, 'M', u'゚'),\n    (0xFFA0, 'X'),\n    (0xFFA1, 'M', u'ᄀ'),\n    (0xFFA2, 'M', u'ᄁ'),\n    (0xFFA3, 'M', u'ᆪ'),\n    (0xFFA4, 'M', u'ᄂ'),\n    (0xFFA5, 'M', u'ᆬ'),\n    (0xFFA6, 'M', u'ᆭ'),\n    (0xFFA7, 'M', u'ᄃ'),\n    (0xFFA8, 'M', u'ᄄ'),\n    (0xFFA9, 'M', u'ᄅ'),\n    (0xFFAA, 'M', u'ᆰ'),\n    (0xFFAB, 'M', u'ᆱ'),\n    (0xFFAC, 'M', u'ᆲ'),\n    (0xFFAD, 'M', u'ᆳ'),\n    (0xFFAE, 'M', u'ᆴ'),\n    (0xFFAF, 'M', u'ᆵ'),\n    (0xFFB0, 'M', u'ᄚ'),\n    (0xFFB1, 'M', u'ᄆ'),\n    (0xFFB2, 'M', u'ᄇ'),\n    (0xFFB3, 'M', u'ᄈ'),\n    (0xFFB4, 'M', u'ᄡ'),\n    (0xFFB5, 'M', u'ᄉ'),\n    (0xFFB6, 'M', u'ᄊ'),\n    (0xFFB7, 'M', u'ᄋ'),\n    (0xFFB8, 'M', u'ᄌ'),\n    (0xFFB9, 'M', u'ᄍ'),\n    (0xFFBA, 'M', u'ᄎ'),\n    (0xFFBB, 'M', u'ᄏ'),\n    (0xFFBC, 'M', u'ᄐ'),\n    (0xFFBD, 'M', u'ᄑ'),\n    (0xFFBE, 'M', u'ᄒ'),\n    (0xFFBF, 'X'),\n    (0xFFC2, 'M', u'ᅡ'),\n    (0xFFC3, 'M', u'ᅢ'),\n    (0xFFC4, 'M', u'ᅣ'),\n    (0xFFC5, 'M', u'ᅤ'),\n    (0xFFC6, 'M', u'ᅥ'),\n    (0xFFC7, 'M', u'ᅦ'),\n    (0xFFC8, 'X'),\n    (0xFFCA, 'M', u'ᅧ'),\n    (0xFFCB, 'M', u'ᅨ'),\n    (0xFFCC, 'M', u'ᅩ'),\n    (0xFFCD, 'M', u'ᅪ'),\n    (0xFFCE, 'M', u'ᅫ'),\n    (0xFFCF, 'M', u'ᅬ'),\n    (0xFFD0, 'X'),\n    (0xFFD2, 'M', u'ᅭ'),\n    (0xFFD3, 'M', u'ᅮ'),\n    (0xFFD4, 'M', u'ᅯ'),\n    (0xFFD5, 'M', u'ᅰ'),\n    (0xFFD6, 'M', u'ᅱ'),\n    (0xFFD7, 'M', u'ᅲ'),\n    (0xFFD8, 'X'),\n    (0xFFDA, 'M', u'ᅳ'),\n    (0xFFDB, 'M', u'ᅴ'),\n    (0xFFDC, 'M', u'ᅵ'),\n    (0xFFDD, 'X'),\n    (0xFFE0, 'M', u'¢'),\n    (0xFFE1, 'M', u'£'),\n    (0xFFE2, 'M', u'¬'),\n    (0xFFE3, '3', u' ̄'),\n    (0xFFE4, 'M', u'¦'),\n    (0xFFE5, 'M', u'¥'),\n    (0xFFE6, 'M', u'₩'),\n    (0xFFE7, 'X'),\n    (0xFFE8, 'M', u'│'),\n    (0xFFE9, 'M', u'←'),\n    (0xFFEA, 'M', u'↑'),\n    (0xFFEB, 'M', u'→'),\n    (0xFFEC, 'M', u'↓'),\n    (0xFFED, 'M', u'■'),\n    (0xFFEE, 'M', u'○'),\n    (0xFFEF, 'X'),\n    (0x10000, 'V'),\n    (0x1000C, 'X'),\n    (0x1000D, 'V'),\n    ]\n\ndef _seg_53():\n    return [\n    (0x10027, 'X'),\n    (0x10028, 'V'),\n    (0x1003B, 'X'),\n    (0x1003C, 'V'),\n    (0x1003E, 'X'),\n    (0x1003F, 'V'),\n    (0x1004E, 'X'),\n    (0x10050, 'V'),\n    (0x1005E, 'X'),\n    (0x10080, 'V'),\n    (0x100FB, 'X'),\n    (0x10100, 'V'),\n    (0x10103, 'X'),\n    (0x10107, 'V'),\n    (0x10134, 'X'),\n    (0x10137, 'V'),\n    (0x1018F, 'X'),\n    (0x10190, 'V'),\n    (0x1019D, 'X'),\n    (0x101A0, 'V'),\n    (0x101A1, 'X'),\n    (0x101D0, 'V'),\n    (0x101FE, 'X'),\n    (0x10280, 'V'),\n    (0x1029D, 'X'),\n    (0x102A0, 'V'),\n    (0x102D1, 'X'),\n    (0x102E0, 'V'),\n    (0x102FC, 'X'),\n    (0x10300, 'V'),\n    (0x10324, 'X'),\n    (0x1032D, 'V'),\n    (0x1034B, 'X'),\n    (0x10350, 'V'),\n    (0x1037B, 'X'),\n    (0x10380, 'V'),\n    (0x1039E, 'X'),\n    (0x1039F, 'V'),\n    (0x103C4, 'X'),\n    (0x103C8, 'V'),\n    (0x103D6, 'X'),\n    (0x10400, 'M', u'𐐨'),\n    (0x10401, 'M', u'𐐩'),\n    (0x10402, 'M', u'𐐪'),\n    (0x10403, 'M', u'𐐫'),\n    (0x10404, 'M', u'𐐬'),\n    (0x10405, 'M', u'𐐭'),\n    (0x10406, 'M', u'𐐮'),\n    (0x10407, 'M', u'𐐯'),\n    (0x10408, 'M', u'𐐰'),\n    (0x10409, 'M', u'𐐱'),\n    (0x1040A, 'M', u'𐐲'),\n    (0x1040B, 'M', u'𐐳'),\n    (0x1040C, 'M', u'𐐴'),\n    (0x1040D, 'M', u'𐐵'),\n    (0x1040E, 'M', u'𐐶'),\n    (0x1040F, 'M', u'𐐷'),\n    (0x10410, 'M', u'𐐸'),\n    (0x10411, 'M', u'𐐹'),\n    (0x10412, 'M', u'𐐺'),\n    (0x10413, 'M', u'𐐻'),\n    (0x10414, 'M', u'𐐼'),\n    (0x10415, 'M', u'𐐽'),\n    (0x10416, 'M', u'𐐾'),\n    (0x10417, 'M', u'𐐿'),\n    (0x10418, 'M', u'𐑀'),\n    (0x10419, 'M', u'𐑁'),\n    (0x1041A, 'M', u'𐑂'),\n    (0x1041B, 'M', u'𐑃'),\n    (0x1041C, 'M', u'𐑄'),\n    (0x1041D, 'M', u'𐑅'),\n    (0x1041E, 'M', u'𐑆'),\n    (0x1041F, 'M', u'𐑇'),\n    (0x10420, 'M', u'𐑈'),\n    (0x10421, 'M', u'𐑉'),\n    (0x10422, 'M', u'𐑊'),\n    (0x10423, 'M', u'𐑋'),\n    (0x10424, 'M', u'𐑌'),\n    (0x10425, 'M', u'𐑍'),\n    (0x10426, 'M', u'𐑎'),\n    (0x10427, 'M', u'𐑏'),\n    (0x10428, 'V'),\n    (0x1049E, 'X'),\n    (0x104A0, 'V'),\n    (0x104AA, 'X'),\n    (0x104B0, 'M', u'𐓘'),\n    (0x104B1, 'M', u'𐓙'),\n    (0x104B2, 'M', u'𐓚'),\n    (0x104B3, 'M', u'𐓛'),\n    (0x104B4, 'M', u'𐓜'),\n    (0x104B5, 'M', u'𐓝'),\n    (0x104B6, 'M', u'𐓞'),\n    (0x104B7, 'M', u'𐓟'),\n    (0x104B8, 'M', u'𐓠'),\n    (0x104B9, 'M', u'𐓡'),\n    (0x104BA, 'M', u'𐓢'),\n    (0x104BB, 'M', u'𐓣'),\n    (0x104BC, 'M', u'𐓤'),\n    (0x104BD, 'M', u'𐓥'),\n    (0x104BE, 'M', u'𐓦'),\n    ]\n\ndef _seg_54():\n    return [\n    (0x104BF, 'M', u'𐓧'),\n    (0x104C0, 'M', u'𐓨'),\n    (0x104C1, 'M', u'𐓩'),\n    (0x104C2, 'M', u'𐓪'),\n    (0x104C3, 'M', u'𐓫'),\n    (0x104C4, 'M', u'𐓬'),\n    (0x104C5, 'M', u'𐓭'),\n    (0x104C6, 'M', u'𐓮'),\n    (0x104C7, 'M', u'𐓯'),\n    (0x104C8, 'M', u'𐓰'),\n    (0x104C9, 'M', u'𐓱'),\n    (0x104CA, 'M', u'𐓲'),\n    (0x104CB, 'M', u'𐓳'),\n    (0x104CC, 'M', u'𐓴'),\n    (0x104CD, 'M', u'𐓵'),\n    (0x104CE, 'M', u'𐓶'),\n    (0x104CF, 'M', u'𐓷'),\n    (0x104D0, 'M', u'𐓸'),\n    (0x104D1, 'M', u'𐓹'),\n    (0x104D2, 'M', u'𐓺'),\n    (0x104D3, 'M', u'𐓻'),\n    (0x104D4, 'X'),\n    (0x104D8, 'V'),\n    (0x104FC, 'X'),\n    (0x10500, 'V'),\n    (0x10528, 'X'),\n    (0x10530, 'V'),\n    (0x10564, 'X'),\n    (0x1056F, 'V'),\n    (0x10570, 'X'),\n    (0x10600, 'V'),\n    (0x10737, 'X'),\n    (0x10740, 'V'),\n    (0x10756, 'X'),\n    (0x10760, 'V'),\n    (0x10768, 'X'),\n    (0x10800, 'V'),\n    (0x10806, 'X'),\n    (0x10808, 'V'),\n    (0x10809, 'X'),\n    (0x1080A, 'V'),\n    (0x10836, 'X'),\n    (0x10837, 'V'),\n    (0x10839, 'X'),\n    (0x1083C, 'V'),\n    (0x1083D, 'X'),\n    (0x1083F, 'V'),\n    (0x10856, 'X'),\n    (0x10857, 'V'),\n    (0x1089F, 'X'),\n    (0x108A7, 'V'),\n    (0x108B0, 'X'),\n    (0x108E0, 'V'),\n    (0x108F3, 'X'),\n    (0x108F4, 'V'),\n    (0x108F6, 'X'),\n    (0x108FB, 'V'),\n    (0x1091C, 'X'),\n    (0x1091F, 'V'),\n    (0x1093A, 'X'),\n    (0x1093F, 'V'),\n    (0x10940, 'X'),\n    (0x10980, 'V'),\n    (0x109B8, 'X'),\n    (0x109BC, 'V'),\n    (0x109D0, 'X'),\n    (0x109D2, 'V'),\n    (0x10A04, 'X'),\n    (0x10A05, 'V'),\n    (0x10A07, 'X'),\n    (0x10A0C, 'V'),\n    (0x10A14, 'X'),\n    (0x10A15, 'V'),\n    (0x10A18, 'X'),\n    (0x10A19, 'V'),\n    (0x10A36, 'X'),\n    (0x10A38, 'V'),\n    (0x10A3B, 'X'),\n    (0x10A3F, 'V'),\n    (0x10A49, 'X'),\n    (0x10A50, 'V'),\n    (0x10A59, 'X'),\n    (0x10A60, 'V'),\n    (0x10AA0, 'X'),\n    (0x10AC0, 'V'),\n    (0x10AE7, 'X'),\n    (0x10AEB, 'V'),\n    (0x10AF7, 'X'),\n    (0x10B00, 'V'),\n    (0x10B36, 'X'),\n    (0x10B39, 'V'),\n    (0x10B56, 'X'),\n    (0x10B58, 'V'),\n    (0x10B73, 'X'),\n    (0x10B78, 'V'),\n    (0x10B92, 'X'),\n    (0x10B99, 'V'),\n    (0x10B9D, 'X'),\n    (0x10BA9, 'V'),\n    (0x10BB0, 'X'),\n    ]\n\ndef _seg_55():\n    return [\n    (0x10C00, 'V'),\n    (0x10C49, 'X'),\n    (0x10C80, 'M', u'𐳀'),\n    (0x10C81, 'M', u'𐳁'),\n    (0x10C82, 'M', u'𐳂'),\n    (0x10C83, 'M', u'𐳃'),\n    (0x10C84, 'M', u'𐳄'),\n    (0x10C85, 'M', u'𐳅'),\n    (0x10C86, 'M', u'𐳆'),\n    (0x10C87, 'M', u'𐳇'),\n    (0x10C88, 'M', u'𐳈'),\n    (0x10C89, 'M', u'𐳉'),\n    (0x10C8A, 'M', u'𐳊'),\n    (0x10C8B, 'M', u'𐳋'),\n    (0x10C8C, 'M', u'𐳌'),\n    (0x10C8D, 'M', u'𐳍'),\n    (0x10C8E, 'M', u'𐳎'),\n    (0x10C8F, 'M', u'𐳏'),\n    (0x10C90, 'M', u'𐳐'),\n    (0x10C91, 'M', u'𐳑'),\n    (0x10C92, 'M', u'𐳒'),\n    (0x10C93, 'M', u'𐳓'),\n    (0x10C94, 'M', u'𐳔'),\n    (0x10C95, 'M', u'𐳕'),\n    (0x10C96, 'M', u'𐳖'),\n    (0x10C97, 'M', u'𐳗'),\n    (0x10C98, 'M', u'𐳘'),\n    (0x10C99, 'M', u'𐳙'),\n    (0x10C9A, 'M', u'𐳚'),\n    (0x10C9B, 'M', u'𐳛'),\n    (0x10C9C, 'M', u'𐳜'),\n    (0x10C9D, 'M', u'𐳝'),\n    (0x10C9E, 'M', u'𐳞'),\n    (0x10C9F, 'M', u'𐳟'),\n    (0x10CA0, 'M', u'𐳠'),\n    (0x10CA1, 'M', u'𐳡'),\n    (0x10CA2, 'M', u'𐳢'),\n    (0x10CA3, 'M', u'𐳣'),\n    (0x10CA4, 'M', u'𐳤'),\n    (0x10CA5, 'M', u'𐳥'),\n    (0x10CA6, 'M', u'𐳦'),\n    (0x10CA7, 'M', u'𐳧'),\n    (0x10CA8, 'M', u'𐳨'),\n    (0x10CA9, 'M', u'𐳩'),\n    (0x10CAA, 'M', u'𐳪'),\n    (0x10CAB, 'M', u'𐳫'),\n    (0x10CAC, 'M', u'𐳬'),\n    (0x10CAD, 'M', u'𐳭'),\n    (0x10CAE, 'M', u'𐳮'),\n    (0x10CAF, 'M', u'𐳯'),\n    (0x10CB0, 'M', u'𐳰'),\n    (0x10CB1, 'M', u'𐳱'),\n    (0x10CB2, 'M', u'𐳲'),\n    (0x10CB3, 'X'),\n    (0x10CC0, 'V'),\n    (0x10CF3, 'X'),\n    (0x10CFA, 'V'),\n    (0x10D28, 'X'),\n    (0x10D30, 'V'),\n    (0x10D3A, 'X'),\n    (0x10E60, 'V'),\n    (0x10E7F, 'X'),\n    (0x10E80, 'V'),\n    (0x10EAA, 'X'),\n    (0x10EAB, 'V'),\n    (0x10EAE, 'X'),\n    (0x10EB0, 'V'),\n    (0x10EB2, 'X'),\n    (0x10F00, 'V'),\n    (0x10F28, 'X'),\n    (0x10F30, 'V'),\n    (0x10F5A, 'X'),\n    (0x10FB0, 'V'),\n    (0x10FCC, 'X'),\n    (0x10FE0, 'V'),\n    (0x10FF7, 'X'),\n    (0x11000, 'V'),\n    (0x1104E, 'X'),\n    (0x11052, 'V'),\n    (0x11070, 'X'),\n    (0x1107F, 'V'),\n    (0x110BD, 'X'),\n    (0x110BE, 'V'),\n    (0x110C2, 'X'),\n    (0x110D0, 'V'),\n    (0x110E9, 'X'),\n    (0x110F0, 'V'),\n    (0x110FA, 'X'),\n    (0x11100, 'V'),\n    (0x11135, 'X'),\n    (0x11136, 'V'),\n    (0x11148, 'X'),\n    (0x11150, 'V'),\n    (0x11177, 'X'),\n    (0x11180, 'V'),\n    (0x111E0, 'X'),\n    (0x111E1, 'V'),\n    (0x111F5, 'X'),\n    (0x11200, 'V'),\n    (0x11212, 'X'),\n    ]\n\ndef _seg_56():\n    return [\n    (0x11213, 'V'),\n    (0x1123F, 'X'),\n    (0x11280, 'V'),\n    (0x11287, 'X'),\n    (0x11288, 'V'),\n    (0x11289, 'X'),\n    (0x1128A, 'V'),\n    (0x1128E, 'X'),\n    (0x1128F, 'V'),\n    (0x1129E, 'X'),\n    (0x1129F, 'V'),\n    (0x112AA, 'X'),\n    (0x112B0, 'V'),\n    (0x112EB, 'X'),\n    (0x112F0, 'V'),\n    (0x112FA, 'X'),\n    (0x11300, 'V'),\n    (0x11304, 'X'),\n    (0x11305, 'V'),\n    (0x1130D, 'X'),\n    (0x1130F, 'V'),\n    (0x11311, 'X'),\n    (0x11313, 'V'),\n    (0x11329, 'X'),\n    (0x1132A, 'V'),\n    (0x11331, 'X'),\n    (0x11332, 'V'),\n    (0x11334, 'X'),\n    (0x11335, 'V'),\n    (0x1133A, 'X'),\n    (0x1133B, 'V'),\n    (0x11345, 'X'),\n    (0x11347, 'V'),\n    (0x11349, 'X'),\n    (0x1134B, 'V'),\n    (0x1134E, 'X'),\n    (0x11350, 'V'),\n    (0x11351, 'X'),\n    (0x11357, 'V'),\n    (0x11358, 'X'),\n    (0x1135D, 'V'),\n    (0x11364, 'X'),\n    (0x11366, 'V'),\n    (0x1136D, 'X'),\n    (0x11370, 'V'),\n    (0x11375, 'X'),\n    (0x11400, 'V'),\n    (0x1145C, 'X'),\n    (0x1145D, 'V'),\n    (0x11462, 'X'),\n    (0x11480, 'V'),\n    (0x114C8, 'X'),\n    (0x114D0, 'V'),\n    (0x114DA, 'X'),\n    (0x11580, 'V'),\n    (0x115B6, 'X'),\n    (0x115B8, 'V'),\n    (0x115DE, 'X'),\n    (0x11600, 'V'),\n    (0x11645, 'X'),\n    (0x11650, 'V'),\n    (0x1165A, 'X'),\n    (0x11660, 'V'),\n    (0x1166D, 'X'),\n    (0x11680, 'V'),\n    (0x116B9, 'X'),\n    (0x116C0, 'V'),\n    (0x116CA, 'X'),\n    (0x11700, 'V'),\n    (0x1171B, 'X'),\n    (0x1171D, 'V'),\n    (0x1172C, 'X'),\n    (0x11730, 'V'),\n    (0x11740, 'X'),\n    (0x11800, 'V'),\n    (0x1183C, 'X'),\n    (0x118A0, 'M', u'𑣀'),\n    (0x118A1, 'M', u'𑣁'),\n    (0x118A2, 'M', u'𑣂'),\n    (0x118A3, 'M', u'𑣃'),\n    (0x118A4, 'M', u'𑣄'),\n    (0x118A5, 'M', u'𑣅'),\n    (0x118A6, 'M', u'𑣆'),\n    (0x118A7, 'M', u'𑣇'),\n    (0x118A8, 'M', u'𑣈'),\n    (0x118A9, 'M', u'𑣉'),\n    (0x118AA, 'M', u'𑣊'),\n    (0x118AB, 'M', u'𑣋'),\n    (0x118AC, 'M', u'𑣌'),\n    (0x118AD, 'M', u'𑣍'),\n    (0x118AE, 'M', u'𑣎'),\n    (0x118AF, 'M', u'𑣏'),\n    (0x118B0, 'M', u'𑣐'),\n    (0x118B1, 'M', u'𑣑'),\n    (0x118B2, 'M', u'𑣒'),\n    (0x118B3, 'M', u'𑣓'),\n    (0x118B4, 'M', u'𑣔'),\n    (0x118B5, 'M', u'𑣕'),\n    (0x118B6, 'M', u'𑣖'),\n    (0x118B7, 'M', u'𑣗'),\n    ]\n\ndef _seg_57():\n    return [\n    (0x118B8, 'M', u'𑣘'),\n    (0x118B9, 'M', u'𑣙'),\n    (0x118BA, 'M', u'𑣚'),\n    (0x118BB, 'M', u'𑣛'),\n    (0x118BC, 'M', u'𑣜'),\n    (0x118BD, 'M', u'𑣝'),\n    (0x118BE, 'M', u'𑣞'),\n    (0x118BF, 'M', u'𑣟'),\n    (0x118C0, 'V'),\n    (0x118F3, 'X'),\n    (0x118FF, 'V'),\n    (0x11907, 'X'),\n    (0x11909, 'V'),\n    (0x1190A, 'X'),\n    (0x1190C, 'V'),\n    (0x11914, 'X'),\n    (0x11915, 'V'),\n    (0x11917, 'X'),\n    (0x11918, 'V'),\n    (0x11936, 'X'),\n    (0x11937, 'V'),\n    (0x11939, 'X'),\n    (0x1193B, 'V'),\n    (0x11947, 'X'),\n    (0x11950, 'V'),\n    (0x1195A, 'X'),\n    (0x119A0, 'V'),\n    (0x119A8, 'X'),\n    (0x119AA, 'V'),\n    (0x119D8, 'X'),\n    (0x119DA, 'V'),\n    (0x119E5, 'X'),\n    (0x11A00, 'V'),\n    (0x11A48, 'X'),\n    (0x11A50, 'V'),\n    (0x11AA3, 'X'),\n    (0x11AC0, 'V'),\n    (0x11AF9, 'X'),\n    (0x11C00, 'V'),\n    (0x11C09, 'X'),\n    (0x11C0A, 'V'),\n    (0x11C37, 'X'),\n    (0x11C38, 'V'),\n    (0x11C46, 'X'),\n    (0x11C50, 'V'),\n    (0x11C6D, 'X'),\n    (0x11C70, 'V'),\n    (0x11C90, 'X'),\n    (0x11C92, 'V'),\n    (0x11CA8, 'X'),\n    (0x11CA9, 'V'),\n    (0x11CB7, 'X'),\n    (0x11D00, 'V'),\n    (0x11D07, 'X'),\n    (0x11D08, 'V'),\n    (0x11D0A, 'X'),\n    (0x11D0B, 'V'),\n    (0x11D37, 'X'),\n    (0x11D3A, 'V'),\n    (0x11D3B, 'X'),\n    (0x11D3C, 'V'),\n    (0x11D3E, 'X'),\n    (0x11D3F, 'V'),\n    (0x11D48, 'X'),\n    (0x11D50, 'V'),\n    (0x11D5A, 'X'),\n    (0x11D60, 'V'),\n    (0x11D66, 'X'),\n    (0x11D67, 'V'),\n    (0x11D69, 'X'),\n    (0x11D6A, 'V'),\n    (0x11D8F, 'X'),\n    (0x11D90, 'V'),\n    (0x11D92, 'X'),\n    (0x11D93, 'V'),\n    (0x11D99, 'X'),\n    (0x11DA0, 'V'),\n    (0x11DAA, 'X'),\n    (0x11EE0, 'V'),\n    (0x11EF9, 'X'),\n    (0x11FB0, 'V'),\n    (0x11FB1, 'X'),\n    (0x11FC0, 'V'),\n    (0x11FF2, 'X'),\n    (0x11FFF, 'V'),\n    (0x1239A, 'X'),\n    (0x12400, 'V'),\n    (0x1246F, 'X'),\n    (0x12470, 'V'),\n    (0x12475, 'X'),\n    (0x12480, 'V'),\n    (0x12544, 'X'),\n    (0x13000, 'V'),\n    (0x1342F, 'X'),\n    (0x14400, 'V'),\n    (0x14647, 'X'),\n    (0x16800, 'V'),\n    (0x16A39, 'X'),\n    (0x16A40, 'V'),\n    (0x16A5F, 'X'),\n    ]\n\ndef _seg_58():\n    return [\n    (0x16A60, 'V'),\n    (0x16A6A, 'X'),\n    (0x16A6E, 'V'),\n    (0x16A70, 'X'),\n    (0x16AD0, 'V'),\n    (0x16AEE, 'X'),\n    (0x16AF0, 'V'),\n    (0x16AF6, 'X'),\n    (0x16B00, 'V'),\n    (0x16B46, 'X'),\n    (0x16B50, 'V'),\n    (0x16B5A, 'X'),\n    (0x16B5B, 'V'),\n    (0x16B62, 'X'),\n    (0x16B63, 'V'),\n    (0x16B78, 'X'),\n    (0x16B7D, 'V'),\n    (0x16B90, 'X'),\n    (0x16E40, 'M', u'𖹠'),\n    (0x16E41, 'M', u'𖹡'),\n    (0x16E42, 'M', u'𖹢'),\n    (0x16E43, 'M', u'𖹣'),\n    (0x16E44, 'M', u'𖹤'),\n    (0x16E45, 'M', u'𖹥'),\n    (0x16E46, 'M', u'𖹦'),\n    (0x16E47, 'M', u'𖹧'),\n    (0x16E48, 'M', u'𖹨'),\n    (0x16E49, 'M', u'𖹩'),\n    (0x16E4A, 'M', u'𖹪'),\n    (0x16E4B, 'M', u'𖹫'),\n    (0x16E4C, 'M', u'𖹬'),\n    (0x16E4D, 'M', u'𖹭'),\n    (0x16E4E, 'M', u'𖹮'),\n    (0x16E4F, 'M', u'𖹯'),\n    (0x16E50, 'M', u'𖹰'),\n    (0x16E51, 'M', u'𖹱'),\n    (0x16E52, 'M', u'𖹲'),\n    (0x16E53, 'M', u'𖹳'),\n    (0x16E54, 'M', u'𖹴'),\n    (0x16E55, 'M', u'𖹵'),\n    (0x16E56, 'M', u'𖹶'),\n    (0x16E57, 'M', u'𖹷'),\n    (0x16E58, 'M', u'𖹸'),\n    (0x16E59, 'M', u'𖹹'),\n    (0x16E5A, 'M', u'𖹺'),\n    (0x16E5B, 'M', u'𖹻'),\n    (0x16E5C, 'M', u'𖹼'),\n    (0x16E5D, 'M', u'𖹽'),\n    (0x16E5E, 'M', u'𖹾'),\n    (0x16E5F, 'M', u'𖹿'),\n    (0x16E60, 'V'),\n    (0x16E9B, 'X'),\n    (0x16F00, 'V'),\n    (0x16F4B, 'X'),\n    (0x16F4F, 'V'),\n    (0x16F88, 'X'),\n    (0x16F8F, 'V'),\n    (0x16FA0, 'X'),\n    (0x16FE0, 'V'),\n    (0x16FE5, 'X'),\n    (0x16FF0, 'V'),\n    (0x16FF2, 'X'),\n    (0x17000, 'V'),\n    (0x187F8, 'X'),\n    (0x18800, 'V'),\n    (0x18CD6, 'X'),\n    (0x18D00, 'V'),\n    (0x18D09, 'X'),\n    (0x1B000, 'V'),\n    (0x1B11F, 'X'),\n    (0x1B150, 'V'),\n    (0x1B153, 'X'),\n    (0x1B164, 'V'),\n    (0x1B168, 'X'),\n    (0x1B170, 'V'),\n    (0x1B2FC, 'X'),\n    (0x1BC00, 'V'),\n    (0x1BC6B, 'X'),\n    (0x1BC70, 'V'),\n    (0x1BC7D, 'X'),\n    (0x1BC80, 'V'),\n    (0x1BC89, 'X'),\n    (0x1BC90, 'V'),\n    (0x1BC9A, 'X'),\n    (0x1BC9C, 'V'),\n    (0x1BCA0, 'I'),\n    (0x1BCA4, 'X'),\n    (0x1D000, 'V'),\n    (0x1D0F6, 'X'),\n    (0x1D100, 'V'),\n    (0x1D127, 'X'),\n    (0x1D129, 'V'),\n    (0x1D15E, 'M', u'𝅗𝅥'),\n    (0x1D15F, 'M', u'𝅘𝅥'),\n    (0x1D160, 'M', u'𝅘𝅥𝅮'),\n    (0x1D161, 'M', u'𝅘𝅥𝅯'),\n    (0x1D162, 'M', u'𝅘𝅥𝅰'),\n    (0x1D163, 'M', u'𝅘𝅥𝅱'),\n    (0x1D164, 'M', u'𝅘𝅥𝅲'),\n    (0x1D165, 'V'),\n    ]\n\ndef _seg_59():\n    return [\n    (0x1D173, 'X'),\n    (0x1D17B, 'V'),\n    (0x1D1BB, 'M', u'𝆹𝅥'),\n    (0x1D1BC, 'M', u'𝆺𝅥'),\n    (0x1D1BD, 'M', u'𝆹𝅥𝅮'),\n    (0x1D1BE, 'M', u'𝆺𝅥𝅮'),\n    (0x1D1BF, 'M', u'𝆹𝅥𝅯'),\n    (0x1D1C0, 'M', u'𝆺𝅥𝅯'),\n    (0x1D1C1, 'V'),\n    (0x1D1E9, 'X'),\n    (0x1D200, 'V'),\n    (0x1D246, 'X'),\n    (0x1D2E0, 'V'),\n    (0x1D2F4, 'X'),\n    (0x1D300, 'V'),\n    (0x1D357, 'X'),\n    (0x1D360, 'V'),\n    (0x1D379, 'X'),\n    (0x1D400, 'M', u'a'),\n    (0x1D401, 'M', u'b'),\n    (0x1D402, 'M', u'c'),\n    (0x1D403, 'M', u'd'),\n    (0x1D404, 'M', u'e'),\n    (0x1D405, 'M', u'f'),\n    (0x1D406, 'M', u'g'),\n    (0x1D407, 'M', u'h'),\n    (0x1D408, 'M', u'i'),\n    (0x1D409, 'M', u'j'),\n    (0x1D40A, 'M', u'k'),\n    (0x1D40B, 'M', u'l'),\n    (0x1D40C, 'M', u'm'),\n    (0x1D40D, 'M', u'n'),\n    (0x1D40E, 'M', u'o'),\n    (0x1D40F, 'M', u'p'),\n    (0x1D410, 'M', u'q'),\n    (0x1D411, 'M', u'r'),\n    (0x1D412, 'M', u's'),\n    (0x1D413, 'M', u't'),\n    (0x1D414, 'M', u'u'),\n    (0x1D415, 'M', u'v'),\n    (0x1D416, 'M', u'w'),\n    (0x1D417, 'M', u'x'),\n    (0x1D418, 'M', u'y'),\n    (0x1D419, 'M', u'z'),\n    (0x1D41A, 'M', u'a'),\n    (0x1D41B, 'M', u'b'),\n    (0x1D41C, 'M', u'c'),\n    (0x1D41D, 'M', u'd'),\n    (0x1D41E, 'M', u'e'),\n    (0x1D41F, 'M', u'f'),\n    (0x1D420, 'M', u'g'),\n    (0x1D421, 'M', u'h'),\n    (0x1D422, 'M', u'i'),\n    (0x1D423, 'M', u'j'),\n    (0x1D424, 'M', u'k'),\n    (0x1D425, 'M', u'l'),\n    (0x1D426, 'M', u'm'),\n    (0x1D427, 'M', u'n'),\n    (0x1D428, 'M', u'o'),\n    (0x1D429, 'M', u'p'),\n    (0x1D42A, 'M', u'q'),\n    (0x1D42B, 'M', u'r'),\n    (0x1D42C, 'M', u's'),\n    (0x1D42D, 'M', u't'),\n    (0x1D42E, 'M', u'u'),\n    (0x1D42F, 'M', u'v'),\n    (0x1D430, 'M', u'w'),\n    (0x1D431, 'M', u'x'),\n    (0x1D432, 'M', u'y'),\n    (0x1D433, 'M', u'z'),\n    (0x1D434, 'M', u'a'),\n    (0x1D435, 'M', u'b'),\n    (0x1D436, 'M', u'c'),\n    (0x1D437, 'M', u'd'),\n    (0x1D438, 'M', u'e'),\n    (0x1D439, 'M', u'f'),\n    (0x1D43A, 'M', u'g'),\n    (0x1D43B, 'M', u'h'),\n    (0x1D43C, 'M', u'i'),\n    (0x1D43D, 'M', u'j'),\n    (0x1D43E, 'M', u'k'),\n    (0x1D43F, 'M', u'l'),\n    (0x1D440, 'M', u'm'),\n    (0x1D441, 'M', u'n'),\n    (0x1D442, 'M', u'o'),\n    (0x1D443, 'M', u'p'),\n    (0x1D444, 'M', u'q'),\n    (0x1D445, 'M', u'r'),\n    (0x1D446, 'M', u's'),\n    (0x1D447, 'M', u't'),\n    (0x1D448, 'M', u'u'),\n    (0x1D449, 'M', u'v'),\n    (0x1D44A, 'M', u'w'),\n    (0x1D44B, 'M', u'x'),\n    (0x1D44C, 'M', u'y'),\n    (0x1D44D, 'M', u'z'),\n    (0x1D44E, 'M', u'a'),\n    (0x1D44F, 'M', u'b'),\n    (0x1D450, 'M', u'c'),\n    (0x1D451, 'M', u'd'),\n    ]\n\ndef _seg_60():\n    return [\n    (0x1D452, 'M', u'e'),\n    (0x1D453, 'M', u'f'),\n    (0x1D454, 'M', u'g'),\n    (0x1D455, 'X'),\n    (0x1D456, 'M', u'i'),\n    (0x1D457, 'M', u'j'),\n    (0x1D458, 'M', u'k'),\n    (0x1D459, 'M', u'l'),\n    (0x1D45A, 'M', u'm'),\n    (0x1D45B, 'M', u'n'),\n    (0x1D45C, 'M', u'o'),\n    (0x1D45D, 'M', u'p'),\n    (0x1D45E, 'M', u'q'),\n    (0x1D45F, 'M', u'r'),\n    (0x1D460, 'M', u's'),\n    (0x1D461, 'M', u't'),\n    (0x1D462, 'M', u'u'),\n    (0x1D463, 'M', u'v'),\n    (0x1D464, 'M', u'w'),\n    (0x1D465, 'M', u'x'),\n    (0x1D466, 'M', u'y'),\n    (0x1D467, 'M', u'z'),\n    (0x1D468, 'M', u'a'),\n    (0x1D469, 'M', u'b'),\n    (0x1D46A, 'M', u'c'),\n    (0x1D46B, 'M', u'd'),\n    (0x1D46C, 'M', u'e'),\n    (0x1D46D, 'M', u'f'),\n    (0x1D46E, 'M', u'g'),\n    (0x1D46F, 'M', u'h'),\n    (0x1D470, 'M', u'i'),\n    (0x1D471, 'M', u'j'),\n    (0x1D472, 'M', u'k'),\n    (0x1D473, 'M', u'l'),\n    (0x1D474, 'M', u'm'),\n    (0x1D475, 'M', u'n'),\n    (0x1D476, 'M', u'o'),\n    (0x1D477, 'M', u'p'),\n    (0x1D478, 'M', u'q'),\n    (0x1D479, 'M', u'r'),\n    (0x1D47A, 'M', u's'),\n    (0x1D47B, 'M', u't'),\n    (0x1D47C, 'M', u'u'),\n    (0x1D47D, 'M', u'v'),\n    (0x1D47E, 'M', u'w'),\n    (0x1D47F, 'M', u'x'),\n    (0x1D480, 'M', u'y'),\n    (0x1D481, 'M', u'z'),\n    (0x1D482, 'M', u'a'),\n    (0x1D483, 'M', u'b'),\n    (0x1D484, 'M', u'c'),\n    (0x1D485, 'M', u'd'),\n    (0x1D486, 'M', u'e'),\n    (0x1D487, 'M', u'f'),\n    (0x1D488, 'M', u'g'),\n    (0x1D489, 'M', u'h'),\n    (0x1D48A, 'M', u'i'),\n    (0x1D48B, 'M', u'j'),\n    (0x1D48C, 'M', u'k'),\n    (0x1D48D, 'M', u'l'),\n    (0x1D48E, 'M', u'm'),\n    (0x1D48F, 'M', u'n'),\n    (0x1D490, 'M', u'o'),\n    (0x1D491, 'M', u'p'),\n    (0x1D492, 'M', u'q'),\n    (0x1D493, 'M', u'r'),\n    (0x1D494, 'M', u's'),\n    (0x1D495, 'M', u't'),\n    (0x1D496, 'M', u'u'),\n    (0x1D497, 'M', u'v'),\n    (0x1D498, 'M', u'w'),\n    (0x1D499, 'M', u'x'),\n    (0x1D49A, 'M', u'y'),\n    (0x1D49B, 'M', u'z'),\n    (0x1D49C, 'M', u'a'),\n    (0x1D49D, 'X'),\n    (0x1D49E, 'M', u'c'),\n    (0x1D49F, 'M', u'd'),\n    (0x1D4A0, 'X'),\n    (0x1D4A2, 'M', u'g'),\n    (0x1D4A3, 'X'),\n    (0x1D4A5, 'M', u'j'),\n    (0x1D4A6, 'M', u'k'),\n    (0x1D4A7, 'X'),\n    (0x1D4A9, 'M', u'n'),\n    (0x1D4AA, 'M', u'o'),\n    (0x1D4AB, 'M', u'p'),\n    (0x1D4AC, 'M', u'q'),\n    (0x1D4AD, 'X'),\n    (0x1D4AE, 'M', u's'),\n    (0x1D4AF, 'M', u't'),\n    (0x1D4B0, 'M', u'u'),\n    (0x1D4B1, 'M', u'v'),\n    (0x1D4B2, 'M', u'w'),\n    (0x1D4B3, 'M', u'x'),\n    (0x1D4B4, 'M', u'y'),\n    (0x1D4B5, 'M', u'z'),\n    (0x1D4B6, 'M', u'a'),\n    (0x1D4B7, 'M', u'b'),\n    (0x1D4B8, 'M', u'c'),\n    ]\n\ndef _seg_61():\n    return [\n    (0x1D4B9, 'M', u'd'),\n    (0x1D4BA, 'X'),\n    (0x1D4BB, 'M', u'f'),\n    (0x1D4BC, 'X'),\n    (0x1D4BD, 'M', u'h'),\n    (0x1D4BE, 'M', u'i'),\n    (0x1D4BF, 'M', u'j'),\n    (0x1D4C0, 'M', u'k'),\n    (0x1D4C1, 'M', u'l'),\n    (0x1D4C2, 'M', u'm'),\n    (0x1D4C3, 'M', u'n'),\n    (0x1D4C4, 'X'),\n    (0x1D4C5, 'M', u'p'),\n    (0x1D4C6, 'M', u'q'),\n    (0x1D4C7, 'M', u'r'),\n    (0x1D4C8, 'M', u's'),\n    (0x1D4C9, 'M', u't'),\n    (0x1D4CA, 'M', u'u'),\n    (0x1D4CB, 'M', u'v'),\n    (0x1D4CC, 'M', u'w'),\n    (0x1D4CD, 'M', u'x'),\n    (0x1D4CE, 'M', u'y'),\n    (0x1D4CF, 'M', u'z'),\n    (0x1D4D0, 'M', u'a'),\n    (0x1D4D1, 'M', u'b'),\n    (0x1D4D2, 'M', u'c'),\n    (0x1D4D3, 'M', u'd'),\n    (0x1D4D4, 'M', u'e'),\n    (0x1D4D5, 'M', u'f'),\n    (0x1D4D6, 'M', u'g'),\n    (0x1D4D7, 'M', u'h'),\n    (0x1D4D8, 'M', u'i'),\n    (0x1D4D9, 'M', u'j'),\n    (0x1D4DA, 'M', u'k'),\n    (0x1D4DB, 'M', u'l'),\n    (0x1D4DC, 'M', u'm'),\n    (0x1D4DD, 'M', u'n'),\n    (0x1D4DE, 'M', u'o'),\n    (0x1D4DF, 'M', u'p'),\n    (0x1D4E0, 'M', u'q'),\n    (0x1D4E1, 'M', u'r'),\n    (0x1D4E2, 'M', u's'),\n    (0x1D4E3, 'M', u't'),\n    (0x1D4E4, 'M', u'u'),\n    (0x1D4E5, 'M', u'v'),\n    (0x1D4E6, 'M', u'w'),\n    (0x1D4E7, 'M', u'x'),\n    (0x1D4E8, 'M', u'y'),\n    (0x1D4E9, 'M', u'z'),\n    (0x1D4EA, 'M', u'a'),\n    (0x1D4EB, 'M', u'b'),\n    (0x1D4EC, 'M', u'c'),\n    (0x1D4ED, 'M', u'd'),\n    (0x1D4EE, 'M', u'e'),\n    (0x1D4EF, 'M', u'f'),\n    (0x1D4F0, 'M', u'g'),\n    (0x1D4F1, 'M', u'h'),\n    (0x1D4F2, 'M', u'i'),\n    (0x1D4F3, 'M', u'j'),\n    (0x1D4F4, 'M', u'k'),\n    (0x1D4F5, 'M', u'l'),\n    (0x1D4F6, 'M', u'm'),\n    (0x1D4F7, 'M', u'n'),\n    (0x1D4F8, 'M', u'o'),\n    (0x1D4F9, 'M', u'p'),\n    (0x1D4FA, 'M', u'q'),\n    (0x1D4FB, 'M', u'r'),\n    (0x1D4FC, 'M', u's'),\n    (0x1D4FD, 'M', u't'),\n    (0x1D4FE, 'M', u'u'),\n    (0x1D4FF, 'M', u'v'),\n    (0x1D500, 'M', u'w'),\n    (0x1D501, 'M', u'x'),\n    (0x1D502, 'M', u'y'),\n    (0x1D503, 'M', u'z'),\n    (0x1D504, 'M', u'a'),\n    (0x1D505, 'M', u'b'),\n    (0x1D506, 'X'),\n    (0x1D507, 'M', u'd'),\n    (0x1D508, 'M', u'e'),\n    (0x1D509, 'M', u'f'),\n    (0x1D50A, 'M', u'g'),\n    (0x1D50B, 'X'),\n    (0x1D50D, 'M', u'j'),\n    (0x1D50E, 'M', u'k'),\n    (0x1D50F, 'M', u'l'),\n    (0x1D510, 'M', u'm'),\n    (0x1D511, 'M', u'n'),\n    (0x1D512, 'M', u'o'),\n    (0x1D513, 'M', u'p'),\n    (0x1D514, 'M', u'q'),\n    (0x1D515, 'X'),\n    (0x1D516, 'M', u's'),\n    (0x1D517, 'M', u't'),\n    (0x1D518, 'M', u'u'),\n    (0x1D519, 'M', u'v'),\n    (0x1D51A, 'M', u'w'),\n    (0x1D51B, 'M', u'x'),\n    (0x1D51C, 'M', u'y'),\n    (0x1D51D, 'X'),\n    ]\n\ndef _seg_62():\n    return [\n    (0x1D51E, 'M', u'a'),\n    (0x1D51F, 'M', u'b'),\n    (0x1D520, 'M', u'c'),\n    (0x1D521, 'M', u'd'),\n    (0x1D522, 'M', u'e'),\n    (0x1D523, 'M', u'f'),\n    (0x1D524, 'M', u'g'),\n    (0x1D525, 'M', u'h'),\n    (0x1D526, 'M', u'i'),\n    (0x1D527, 'M', u'j'),\n    (0x1D528, 'M', u'k'),\n    (0x1D529, 'M', u'l'),\n    (0x1D52A, 'M', u'm'),\n    (0x1D52B, 'M', u'n'),\n    (0x1D52C, 'M', u'o'),\n    (0x1D52D, 'M', u'p'),\n    (0x1D52E, 'M', u'q'),\n    (0x1D52F, 'M', u'r'),\n    (0x1D530, 'M', u's'),\n    (0x1D531, 'M', u't'),\n    (0x1D532, 'M', u'u'),\n    (0x1D533, 'M', u'v'),\n    (0x1D534, 'M', u'w'),\n    (0x1D535, 'M', u'x'),\n    (0x1D536, 'M', u'y'),\n    (0x1D537, 'M', u'z'),\n    (0x1D538, 'M', u'a'),\n    (0x1D539, 'M', u'b'),\n    (0x1D53A, 'X'),\n    (0x1D53B, 'M', u'd'),\n    (0x1D53C, 'M', u'e'),\n    (0x1D53D, 'M', u'f'),\n    (0x1D53E, 'M', u'g'),\n    (0x1D53F, 'X'),\n    (0x1D540, 'M', u'i'),\n    (0x1D541, 'M', u'j'),\n    (0x1D542, 'M', u'k'),\n    (0x1D543, 'M', u'l'),\n    (0x1D544, 'M', u'm'),\n    (0x1D545, 'X'),\n    (0x1D546, 'M', u'o'),\n    (0x1D547, 'X'),\n    (0x1D54A, 'M', u's'),\n    (0x1D54B, 'M', u't'),\n    (0x1D54C, 'M', u'u'),\n    (0x1D54D, 'M', u'v'),\n    (0x1D54E, 'M', u'w'),\n    (0x1D54F, 'M', u'x'),\n    (0x1D550, 'M', u'y'),\n    (0x1D551, 'X'),\n    (0x1D552, 'M', u'a'),\n    (0x1D553, 'M', u'b'),\n    (0x1D554, 'M', u'c'),\n    (0x1D555, 'M', u'd'),\n    (0x1D556, 'M', u'e'),\n    (0x1D557, 'M', u'f'),\n    (0x1D558, 'M', u'g'),\n    (0x1D559, 'M', u'h'),\n    (0x1D55A, 'M', u'i'),\n    (0x1D55B, 'M', u'j'),\n    (0x1D55C, 'M', u'k'),\n    (0x1D55D, 'M', u'l'),\n    (0x1D55E, 'M', u'm'),\n    (0x1D55F, 'M', u'n'),\n    (0x1D560, 'M', u'o'),\n    (0x1D561, 'M', u'p'),\n    (0x1D562, 'M', u'q'),\n    (0x1D563, 'M', u'r'),\n    (0x1D564, 'M', u's'),\n    (0x1D565, 'M', u't'),\n    (0x1D566, 'M', u'u'),\n    (0x1D567, 'M', u'v'),\n    (0x1D568, 'M', u'w'),\n    (0x1D569, 'M', u'x'),\n    (0x1D56A, 'M', u'y'),\n    (0x1D56B, 'M', u'z'),\n    (0x1D56C, 'M', u'a'),\n    (0x1D56D, 'M', u'b'),\n    (0x1D56E, 'M', u'c'),\n    (0x1D56F, 'M', u'd'),\n    (0x1D570, 'M', u'e'),\n    (0x1D571, 'M', u'f'),\n    (0x1D572, 'M', u'g'),\n    (0x1D573, 'M', u'h'),\n    (0x1D574, 'M', u'i'),\n    (0x1D575, 'M', u'j'),\n    (0x1D576, 'M', u'k'),\n    (0x1D577, 'M', u'l'),\n    (0x1D578, 'M', u'm'),\n    (0x1D579, 'M', u'n'),\n    (0x1D57A, 'M', u'o'),\n    (0x1D57B, 'M', u'p'),\n    (0x1D57C, 'M', u'q'),\n    (0x1D57D, 'M', u'r'),\n    (0x1D57E, 'M', u's'),\n    (0x1D57F, 'M', u't'),\n    (0x1D580, 'M', u'u'),\n    (0x1D581, 'M', u'v'),\n    (0x1D582, 'M', u'w'),\n    (0x1D583, 'M', u'x'),\n    ]\n\ndef _seg_63():\n    return [\n    (0x1D584, 'M', u'y'),\n    (0x1D585, 'M', u'z'),\n    (0x1D586, 'M', u'a'),\n    (0x1D587, 'M', u'b'),\n    (0x1D588, 'M', u'c'),\n    (0x1D589, 'M', u'd'),\n    (0x1D58A, 'M', u'e'),\n    (0x1D58B, 'M', u'f'),\n    (0x1D58C, 'M', u'g'),\n    (0x1D58D, 'M', u'h'),\n    (0x1D58E, 'M', u'i'),\n    (0x1D58F, 'M', u'j'),\n    (0x1D590, 'M', u'k'),\n    (0x1D591, 'M', u'l'),\n    (0x1D592, 'M', u'm'),\n    (0x1D593, 'M', u'n'),\n    (0x1D594, 'M', u'o'),\n    (0x1D595, 'M', u'p'),\n    (0x1D596, 'M', u'q'),\n    (0x1D597, 'M', u'r'),\n    (0x1D598, 'M', u's'),\n    (0x1D599, 'M', u't'),\n    (0x1D59A, 'M', u'u'),\n    (0x1D59B, 'M', u'v'),\n    (0x1D59C, 'M', u'w'),\n    (0x1D59D, 'M', u'x'),\n    (0x1D59E, 'M', u'y'),\n    (0x1D59F, 'M', u'z'),\n    (0x1D5A0, 'M', u'a'),\n    (0x1D5A1, 'M', u'b'),\n    (0x1D5A2, 'M', u'c'),\n    (0x1D5A3, 'M', u'd'),\n    (0x1D5A4, 'M', u'e'),\n    (0x1D5A5, 'M', u'f'),\n    (0x1D5A6, 'M', u'g'),\n    (0x1D5A7, 'M', u'h'),\n    (0x1D5A8, 'M', u'i'),\n    (0x1D5A9, 'M', u'j'),\n    (0x1D5AA, 'M', u'k'),\n    (0x1D5AB, 'M', u'l'),\n    (0x1D5AC, 'M', u'm'),\n    (0x1D5AD, 'M', u'n'),\n    (0x1D5AE, 'M', u'o'),\n    (0x1D5AF, 'M', u'p'),\n    (0x1D5B0, 'M', u'q'),\n    (0x1D5B1, 'M', u'r'),\n    (0x1D5B2, 'M', u's'),\n    (0x1D5B3, 'M', u't'),\n    (0x1D5B4, 'M', u'u'),\n    (0x1D5B5, 'M', u'v'),\n    (0x1D5B6, 'M', u'w'),\n    (0x1D5B7, 'M', u'x'),\n    (0x1D5B8, 'M', u'y'),\n    (0x1D5B9, 'M', u'z'),\n    (0x1D5BA, 'M', u'a'),\n    (0x1D5BB, 'M', u'b'),\n    (0x1D5BC, 'M', u'c'),\n    (0x1D5BD, 'M', u'd'),\n    (0x1D5BE, 'M', u'e'),\n    (0x1D5BF, 'M', u'f'),\n    (0x1D5C0, 'M', u'g'),\n    (0x1D5C1, 'M', u'h'),\n    (0x1D5C2, 'M', u'i'),\n    (0x1D5C3, 'M', u'j'),\n    (0x1D5C4, 'M', u'k'),\n    (0x1D5C5, 'M', u'l'),\n    (0x1D5C6, 'M', u'm'),\n    (0x1D5C7, 'M', u'n'),\n    (0x1D5C8, 'M', u'o'),\n    (0x1D5C9, 'M', u'p'),\n    (0x1D5CA, 'M', u'q'),\n    (0x1D5CB, 'M', u'r'),\n    (0x1D5CC, 'M', u's'),\n    (0x1D5CD, 'M', u't'),\n    (0x1D5CE, 'M', u'u'),\n    (0x1D5CF, 'M', u'v'),\n    (0x1D5D0, 'M', u'w'),\n    (0x1D5D1, 'M', u'x'),\n    (0x1D5D2, 'M', u'y'),\n    (0x1D5D3, 'M', u'z'),\n    (0x1D5D4, 'M', u'a'),\n    (0x1D5D5, 'M', u'b'),\n    (0x1D5D6, 'M', u'c'),\n    (0x1D5D7, 'M', u'd'),\n    (0x1D5D8, 'M', u'e'),\n    (0x1D5D9, 'M', u'f'),\n    (0x1D5DA, 'M', u'g'),\n    (0x1D5DB, 'M', u'h'),\n    (0x1D5DC, 'M', u'i'),\n    (0x1D5DD, 'M', u'j'),\n    (0x1D5DE, 'M', u'k'),\n    (0x1D5DF, 'M', u'l'),\n    (0x1D5E0, 'M', u'm'),\n    (0x1D5E1, 'M', u'n'),\n    (0x1D5E2, 'M', u'o'),\n    (0x1D5E3, 'M', u'p'),\n    (0x1D5E4, 'M', u'q'),\n    (0x1D5E5, 'M', u'r'),\n    (0x1D5E6, 'M', u's'),\n    (0x1D5E7, 'M', u't'),\n    ]\n\ndef _seg_64():\n    return [\n    (0x1D5E8, 'M', u'u'),\n    (0x1D5E9, 'M', u'v'),\n    (0x1D5EA, 'M', u'w'),\n    (0x1D5EB, 'M', u'x'),\n    (0x1D5EC, 'M', u'y'),\n    (0x1D5ED, 'M', u'z'),\n    (0x1D5EE, 'M', u'a'),\n    (0x1D5EF, 'M', u'b'),\n    (0x1D5F0, 'M', u'c'),\n    (0x1D5F1, 'M', u'd'),\n    (0x1D5F2, 'M', u'e'),\n    (0x1D5F3, 'M', u'f'),\n    (0x1D5F4, 'M', u'g'),\n    (0x1D5F5, 'M', u'h'),\n    (0x1D5F6, 'M', u'i'),\n    (0x1D5F7, 'M', u'j'),\n    (0x1D5F8, 'M', u'k'),\n    (0x1D5F9, 'M', u'l'),\n    (0x1D5FA, 'M', u'm'),\n    (0x1D5FB, 'M', u'n'),\n    (0x1D5FC, 'M', u'o'),\n    (0x1D5FD, 'M', u'p'),\n    (0x1D5FE, 'M', u'q'),\n    (0x1D5FF, 'M', u'r'),\n    (0x1D600, 'M', u's'),\n    (0x1D601, 'M', u't'),\n    (0x1D602, 'M', u'u'),\n    (0x1D603, 'M', u'v'),\n    (0x1D604, 'M', u'w'),\n    (0x1D605, 'M', u'x'),\n    (0x1D606, 'M', u'y'),\n    (0x1D607, 'M', u'z'),\n    (0x1D608, 'M', u'a'),\n    (0x1D609, 'M', u'b'),\n    (0x1D60A, 'M', u'c'),\n    (0x1D60B, 'M', u'd'),\n    (0x1D60C, 'M', u'e'),\n    (0x1D60D, 'M', u'f'),\n    (0x1D60E, 'M', u'g'),\n    (0x1D60F, 'M', u'h'),\n    (0x1D610, 'M', u'i'),\n    (0x1D611, 'M', u'j'),\n    (0x1D612, 'M', u'k'),\n    (0x1D613, 'M', u'l'),\n    (0x1D614, 'M', u'm'),\n    (0x1D615, 'M', u'n'),\n    (0x1D616, 'M', u'o'),\n    (0x1D617, 'M', u'p'),\n    (0x1D618, 'M', u'q'),\n    (0x1D619, 'M', u'r'),\n    (0x1D61A, 'M', u's'),\n    (0x1D61B, 'M', u't'),\n    (0x1D61C, 'M', u'u'),\n    (0x1D61D, 'M', u'v'),\n    (0x1D61E, 'M', u'w'),\n    (0x1D61F, 'M', u'x'),\n    (0x1D620, 'M', u'y'),\n    (0x1D621, 'M', u'z'),\n    (0x1D622, 'M', u'a'),\n    (0x1D623, 'M', u'b'),\n    (0x1D624, 'M', u'c'),\n    (0x1D625, 'M', u'd'),\n    (0x1D626, 'M', u'e'),\n    (0x1D627, 'M', u'f'),\n    (0x1D628, 'M', u'g'),\n    (0x1D629, 'M', u'h'),\n    (0x1D62A, 'M', u'i'),\n    (0x1D62B, 'M', u'j'),\n    (0x1D62C, 'M', u'k'),\n    (0x1D62D, 'M', u'l'),\n    (0x1D62E, 'M', u'm'),\n    (0x1D62F, 'M', u'n'),\n    (0x1D630, 'M', u'o'),\n    (0x1D631, 'M', u'p'),\n    (0x1D632, 'M', u'q'),\n    (0x1D633, 'M', u'r'),\n    (0x1D634, 'M', u's'),\n    (0x1D635, 'M', u't'),\n    (0x1D636, 'M', u'u'),\n    (0x1D637, 'M', u'v'),\n    (0x1D638, 'M', u'w'),\n    (0x1D639, 'M', u'x'),\n    (0x1D63A, 'M', u'y'),\n    (0x1D63B, 'M', u'z'),\n    (0x1D63C, 'M', u'a'),\n    (0x1D63D, 'M', u'b'),\n    (0x1D63E, 'M', u'c'),\n    (0x1D63F, 'M', u'd'),\n    (0x1D640, 'M', u'e'),\n    (0x1D641, 'M', u'f'),\n    (0x1D642, 'M', u'g'),\n    (0x1D643, 'M', u'h'),\n    (0x1D644, 'M', u'i'),\n    (0x1D645, 'M', u'j'),\n    (0x1D646, 'M', u'k'),\n    (0x1D647, 'M', u'l'),\n    (0x1D648, 'M', u'm'),\n    (0x1D649, 'M', u'n'),\n    (0x1D64A, 'M', u'o'),\n    (0x1D64B, 'M', u'p'),\n    ]\n\ndef _seg_65():\n    return [\n    (0x1D64C, 'M', u'q'),\n    (0x1D64D, 'M', u'r'),\n    (0x1D64E, 'M', u's'),\n    (0x1D64F, 'M', u't'),\n    (0x1D650, 'M', u'u'),\n    (0x1D651, 'M', u'v'),\n    (0x1D652, 'M', u'w'),\n    (0x1D653, 'M', u'x'),\n    (0x1D654, 'M', u'y'),\n    (0x1D655, 'M', u'z'),\n    (0x1D656, 'M', u'a'),\n    (0x1D657, 'M', u'b'),\n    (0x1D658, 'M', u'c'),\n    (0x1D659, 'M', u'd'),\n    (0x1D65A, 'M', u'e'),\n    (0x1D65B, 'M', u'f'),\n    (0x1D65C, 'M', u'g'),\n    (0x1D65D, 'M', u'h'),\n    (0x1D65E, 'M', u'i'),\n    (0x1D65F, 'M', u'j'),\n    (0x1D660, 'M', u'k'),\n    (0x1D661, 'M', u'l'),\n    (0x1D662, 'M', u'm'),\n    (0x1D663, 'M', u'n'),\n    (0x1D664, 'M', u'o'),\n    (0x1D665, 'M', u'p'),\n    (0x1D666, 'M', u'q'),\n    (0x1D667, 'M', u'r'),\n    (0x1D668, 'M', u's'),\n    (0x1D669, 'M', u't'),\n    (0x1D66A, 'M', u'u'),\n    (0x1D66B, 'M', u'v'),\n    (0x1D66C, 'M', u'w'),\n    (0x1D66D, 'M', u'x'),\n    (0x1D66E, 'M', u'y'),\n    (0x1D66F, 'M', u'z'),\n    (0x1D670, 'M', u'a'),\n    (0x1D671, 'M', u'b'),\n    (0x1D672, 'M', u'c'),\n    (0x1D673, 'M', u'd'),\n    (0x1D674, 'M', u'e'),\n    (0x1D675, 'M', u'f'),\n    (0x1D676, 'M', u'g'),\n    (0x1D677, 'M', u'h'),\n    (0x1D678, 'M', u'i'),\n    (0x1D679, 'M', u'j'),\n    (0x1D67A, 'M', u'k'),\n    (0x1D67B, 'M', u'l'),\n    (0x1D67C, 'M', u'm'),\n    (0x1D67D, 'M', u'n'),\n    (0x1D67E, 'M', u'o'),\n    (0x1D67F, 'M', u'p'),\n    (0x1D680, 'M', u'q'),\n    (0x1D681, 'M', u'r'),\n    (0x1D682, 'M', u's'),\n    (0x1D683, 'M', u't'),\n    (0x1D684, 'M', u'u'),\n    (0x1D685, 'M', u'v'),\n    (0x1D686, 'M', u'w'),\n    (0x1D687, 'M', u'x'),\n    (0x1D688, 'M', u'y'),\n    (0x1D689, 'M', u'z'),\n    (0x1D68A, 'M', u'a'),\n    (0x1D68B, 'M', u'b'),\n    (0x1D68C, 'M', u'c'),\n    (0x1D68D, 'M', u'd'),\n    (0x1D68E, 'M', u'e'),\n    (0x1D68F, 'M', u'f'),\n    (0x1D690, 'M', u'g'),\n    (0x1D691, 'M', u'h'),\n    (0x1D692, 'M', u'i'),\n    (0x1D693, 'M', u'j'),\n    (0x1D694, 'M', u'k'),\n    (0x1D695, 'M', u'l'),\n    (0x1D696, 'M', u'm'),\n    (0x1D697, 'M', u'n'),\n    (0x1D698, 'M', u'o'),\n    (0x1D699, 'M', u'p'),\n    (0x1D69A, 'M', u'q'),\n    (0x1D69B, 'M', u'r'),\n    (0x1D69C, 'M', u's'),\n    (0x1D69D, 'M', u't'),\n    (0x1D69E, 'M', u'u'),\n    (0x1D69F, 'M', u'v'),\n    (0x1D6A0, 'M', u'w'),\n    (0x1D6A1, 'M', u'x'),\n    (0x1D6A2, 'M', u'y'),\n    (0x1D6A3, 'M', u'z'),\n    (0x1D6A4, 'M', u'ı'),\n    (0x1D6A5, 'M', u'ȷ'),\n    (0x1D6A6, 'X'),\n    (0x1D6A8, 'M', u'α'),\n    (0x1D6A9, 'M', u'β'),\n    (0x1D6AA, 'M', u'γ'),\n    (0x1D6AB, 'M', u'δ'),\n    (0x1D6AC, 'M', u'ε'),\n    (0x1D6AD, 'M', u'ζ'),\n    (0x1D6AE, 'M', u'η'),\n    (0x1D6AF, 'M', u'θ'),\n    (0x1D6B0, 'M', u'ι'),\n    ]\n\ndef _seg_66():\n    return [\n    (0x1D6B1, 'M', u'κ'),\n    (0x1D6B2, 'M', u'λ'),\n    (0x1D6B3, 'M', u'μ'),\n    (0x1D6B4, 'M', u'ν'),\n    (0x1D6B5, 'M', u'ξ'),\n    (0x1D6B6, 'M', u'ο'),\n    (0x1D6B7, 'M', u'π'),\n    (0x1D6B8, 'M', u'ρ'),\n    (0x1D6B9, 'M', u'θ'),\n    (0x1D6BA, 'M', u'σ'),\n    (0x1D6BB, 'M', u'τ'),\n    (0x1D6BC, 'M', u'υ'),\n    (0x1D6BD, 'M', u'φ'),\n    (0x1D6BE, 'M', u'χ'),\n    (0x1D6BF, 'M', u'ψ'),\n    (0x1D6C0, 'M', u'ω'),\n    (0x1D6C1, 'M', u'∇'),\n    (0x1D6C2, 'M', u'α'),\n    (0x1D6C3, 'M', u'β'),\n    (0x1D6C4, 'M', u'γ'),\n    (0x1D6C5, 'M', u'δ'),\n    (0x1D6C6, 'M', u'ε'),\n    (0x1D6C7, 'M', u'ζ'),\n    (0x1D6C8, 'M', u'η'),\n    (0x1D6C9, 'M', u'θ'),\n    (0x1D6CA, 'M', u'ι'),\n    (0x1D6CB, 'M', u'κ'),\n    (0x1D6CC, 'M', u'λ'),\n    (0x1D6CD, 'M', u'μ'),\n    (0x1D6CE, 'M', u'ν'),\n    (0x1D6CF, 'M', u'ξ'),\n    (0x1D6D0, 'M', u'ο'),\n    (0x1D6D1, 'M', u'π'),\n    (0x1D6D2, 'M', u'ρ'),\n    (0x1D6D3, 'M', u'σ'),\n    (0x1D6D5, 'M', u'τ'),\n    (0x1D6D6, 'M', u'υ'),\n    (0x1D6D7, 'M', u'φ'),\n    (0x1D6D8, 'M', u'χ'),\n    (0x1D6D9, 'M', u'ψ'),\n    (0x1D6DA, 'M', u'ω'),\n    (0x1D6DB, 'M', u'∂'),\n    (0x1D6DC, 'M', u'ε'),\n    (0x1D6DD, 'M', u'θ'),\n    (0x1D6DE, 'M', u'κ'),\n    (0x1D6DF, 'M', u'φ'),\n    (0x1D6E0, 'M', u'ρ'),\n    (0x1D6E1, 'M', u'π'),\n    (0x1D6E2, 'M', u'α'),\n    (0x1D6E3, 'M', u'β'),\n    (0x1D6E4, 'M', u'γ'),\n    (0x1D6E5, 'M', u'δ'),\n    (0x1D6E6, 'M', u'ε'),\n    (0x1D6E7, 'M', u'ζ'),\n    (0x1D6E8, 'M', u'η'),\n    (0x1D6E9, 'M', u'θ'),\n    (0x1D6EA, 'M', u'ι'),\n    (0x1D6EB, 'M', u'κ'),\n    (0x1D6EC, 'M', u'λ'),\n    (0x1D6ED, 'M', u'μ'),\n    (0x1D6EE, 'M', u'ν'),\n    (0x1D6EF, 'M', u'ξ'),\n    (0x1D6F0, 'M', u'ο'),\n    (0x1D6F1, 'M', u'π'),\n    (0x1D6F2, 'M', u'ρ'),\n    (0x1D6F3, 'M', u'θ'),\n    (0x1D6F4, 'M', u'σ'),\n    (0x1D6F5, 'M', u'τ'),\n    (0x1D6F6, 'M', u'υ'),\n    (0x1D6F7, 'M', u'φ'),\n    (0x1D6F8, 'M', u'χ'),\n    (0x1D6F9, 'M', u'ψ'),\n    (0x1D6FA, 'M', u'ω'),\n    (0x1D6FB, 'M', u'∇'),\n    (0x1D6FC, 'M', u'α'),\n    (0x1D6FD, 'M', u'β'),\n    (0x1D6FE, 'M', u'γ'),\n    (0x1D6FF, 'M', u'δ'),\n    (0x1D700, 'M', u'ε'),\n    (0x1D701, 'M', u'ζ'),\n    (0x1D702, 'M', u'η'),\n    (0x1D703, 'M', u'θ'),\n    (0x1D704, 'M', u'ι'),\n    (0x1D705, 'M', u'κ'),\n    (0x1D706, 'M', u'λ'),\n    (0x1D707, 'M', u'μ'),\n    (0x1D708, 'M', u'ν'),\n    (0x1D709, 'M', u'ξ'),\n    (0x1D70A, 'M', u'ο'),\n    (0x1D70B, 'M', u'π'),\n    (0x1D70C, 'M', u'ρ'),\n    (0x1D70D, 'M', u'σ'),\n    (0x1D70F, 'M', u'τ'),\n    (0x1D710, 'M', u'υ'),\n    (0x1D711, 'M', u'φ'),\n    (0x1D712, 'M', u'χ'),\n    (0x1D713, 'M', u'ψ'),\n    (0x1D714, 'M', u'ω'),\n    (0x1D715, 'M', u'∂'),\n    (0x1D716, 'M', u'ε'),\n    ]\n\ndef _seg_67():\n    return [\n    (0x1D717, 'M', u'θ'),\n    (0x1D718, 'M', u'κ'),\n    (0x1D719, 'M', u'φ'),\n    (0x1D71A, 'M', u'ρ'),\n    (0x1D71B, 'M', u'π'),\n    (0x1D71C, 'M', u'α'),\n    (0x1D71D, 'M', u'β'),\n    (0x1D71E, 'M', u'γ'),\n    (0x1D71F, 'M', u'δ'),\n    (0x1D720, 'M', u'ε'),\n    (0x1D721, 'M', u'ζ'),\n    (0x1D722, 'M', u'η'),\n    (0x1D723, 'M', u'θ'),\n    (0x1D724, 'M', u'ι'),\n    (0x1D725, 'M', u'κ'),\n    (0x1D726, 'M', u'λ'),\n    (0x1D727, 'M', u'μ'),\n    (0x1D728, 'M', u'ν'),\n    (0x1D729, 'M', u'ξ'),\n    (0x1D72A, 'M', u'ο'),\n    (0x1D72B, 'M', u'π'),\n    (0x1D72C, 'M', u'ρ'),\n    (0x1D72D, 'M', u'θ'),\n    (0x1D72E, 'M', u'σ'),\n    (0x1D72F, 'M', u'τ'),\n    (0x1D730, 'M', u'υ'),\n    (0x1D731, 'M', u'φ'),\n    (0x1D732, 'M', u'χ'),\n    (0x1D733, 'M', u'ψ'),\n    (0x1D734, 'M', u'ω'),\n    (0x1D735, 'M', u'∇'),\n    (0x1D736, 'M', u'α'),\n    (0x1D737, 'M', u'β'),\n    (0x1D738, 'M', u'γ'),\n    (0x1D739, 'M', u'δ'),\n    (0x1D73A, 'M', u'ε'),\n    (0x1D73B, 'M', u'ζ'),\n    (0x1D73C, 'M', u'η'),\n    (0x1D73D, 'M', u'θ'),\n    (0x1D73E, 'M', u'ι'),\n    (0x1D73F, 'M', u'κ'),\n    (0x1D740, 'M', u'λ'),\n    (0x1D741, 'M', u'μ'),\n    (0x1D742, 'M', u'ν'),\n    (0x1D743, 'M', u'ξ'),\n    (0x1D744, 'M', u'ο'),\n    (0x1D745, 'M', u'π'),\n    (0x1D746, 'M', u'ρ'),\n    (0x1D747, 'M', u'σ'),\n    (0x1D749, 'M', u'τ'),\n    (0x1D74A, 'M', u'υ'),\n    (0x1D74B, 'M', u'φ'),\n    (0x1D74C, 'M', u'χ'),\n    (0x1D74D, 'M', u'ψ'),\n    (0x1D74E, 'M', u'ω'),\n    (0x1D74F, 'M', u'∂'),\n    (0x1D750, 'M', u'ε'),\n    (0x1D751, 'M', u'θ'),\n    (0x1D752, 'M', u'κ'),\n    (0x1D753, 'M', u'φ'),\n    (0x1D754, 'M', u'ρ'),\n    (0x1D755, 'M', u'π'),\n    (0x1D756, 'M', u'α'),\n    (0x1D757, 'M', u'β'),\n    (0x1D758, 'M', u'γ'),\n    (0x1D759, 'M', u'δ'),\n    (0x1D75A, 'M', u'ε'),\n    (0x1D75B, 'M', u'ζ'),\n    (0x1D75C, 'M', u'η'),\n    (0x1D75D, 'M', u'θ'),\n    (0x1D75E, 'M', u'ι'),\n    (0x1D75F, 'M', u'κ'),\n    (0x1D760, 'M', u'λ'),\n    (0x1D761, 'M', u'μ'),\n    (0x1D762, 'M', u'ν'),\n    (0x1D763, 'M', u'ξ'),\n    (0x1D764, 'M', u'ο'),\n    (0x1D765, 'M', u'π'),\n    (0x1D766, 'M', u'ρ'),\n    (0x1D767, 'M', u'θ'),\n    (0x1D768, 'M', u'σ'),\n    (0x1D769, 'M', u'τ'),\n    (0x1D76A, 'M', u'υ'),\n    (0x1D76B, 'M', u'φ'),\n    (0x1D76C, 'M', u'χ'),\n    (0x1D76D, 'M', u'ψ'),\n    (0x1D76E, 'M', u'ω'),\n    (0x1D76F, 'M', u'∇'),\n    (0x1D770, 'M', u'α'),\n    (0x1D771, 'M', u'β'),\n    (0x1D772, 'M', u'γ'),\n    (0x1D773, 'M', u'δ'),\n    (0x1D774, 'M', u'ε'),\n    (0x1D775, 'M', u'ζ'),\n    (0x1D776, 'M', u'η'),\n    (0x1D777, 'M', u'θ'),\n    (0x1D778, 'M', u'ι'),\n    (0x1D779, 'M', u'κ'),\n    (0x1D77A, 'M', u'λ'),\n    (0x1D77B, 'M', u'μ'),\n    ]\n\ndef _seg_68():\n    return [\n    (0x1D77C, 'M', u'ν'),\n    (0x1D77D, 'M', u'ξ'),\n    (0x1D77E, 'M', u'ο'),\n    (0x1D77F, 'M', u'π'),\n    (0x1D780, 'M', u'ρ'),\n    (0x1D781, 'M', u'σ'),\n    (0x1D783, 'M', u'τ'),\n    (0x1D784, 'M', u'υ'),\n    (0x1D785, 'M', u'φ'),\n    (0x1D786, 'M', u'χ'),\n    (0x1D787, 'M', u'ψ'),\n    (0x1D788, 'M', u'ω'),\n    (0x1D789, 'M', u'∂'),\n    (0x1D78A, 'M', u'ε'),\n    (0x1D78B, 'M', u'θ'),\n    (0x1D78C, 'M', u'κ'),\n    (0x1D78D, 'M', u'φ'),\n    (0x1D78E, 'M', u'ρ'),\n    (0x1D78F, 'M', u'π'),\n    (0x1D790, 'M', u'α'),\n    (0x1D791, 'M', u'β'),\n    (0x1D792, 'M', u'γ'),\n    (0x1D793, 'M', u'δ'),\n    (0x1D794, 'M', u'ε'),\n    (0x1D795, 'M', u'ζ'),\n    (0x1D796, 'M', u'η'),\n    (0x1D797, 'M', u'θ'),\n    (0x1D798, 'M', u'ι'),\n    (0x1D799, 'M', u'κ'),\n    (0x1D79A, 'M', u'λ'),\n    (0x1D79B, 'M', u'μ'),\n    (0x1D79C, 'M', u'ν'),\n    (0x1D79D, 'M', u'ξ'),\n    (0x1D79E, 'M', u'ο'),\n    (0x1D79F, 'M', u'π'),\n    (0x1D7A0, 'M', u'ρ'),\n    (0x1D7A1, 'M', u'θ'),\n    (0x1D7A2, 'M', u'σ'),\n    (0x1D7A3, 'M', u'τ'),\n    (0x1D7A4, 'M', u'υ'),\n    (0x1D7A5, 'M', u'φ'),\n    (0x1D7A6, 'M', u'χ'),\n    (0x1D7A7, 'M', u'ψ'),\n    (0x1D7A8, 'M', u'ω'),\n    (0x1D7A9, 'M', u'∇'),\n    (0x1D7AA, 'M', u'α'),\n    (0x1D7AB, 'M', u'β'),\n    (0x1D7AC, 'M', u'γ'),\n    (0x1D7AD, 'M', u'δ'),\n    (0x1D7AE, 'M', u'ε'),\n    (0x1D7AF, 'M', u'ζ'),\n    (0x1D7B0, 'M', u'η'),\n    (0x1D7B1, 'M', u'θ'),\n    (0x1D7B2, 'M', u'ι'),\n    (0x1D7B3, 'M', u'κ'),\n    (0x1D7B4, 'M', u'λ'),\n    (0x1D7B5, 'M', u'μ'),\n    (0x1D7B6, 'M', u'ν'),\n    (0x1D7B7, 'M', u'ξ'),\n    (0x1D7B8, 'M', u'ο'),\n    (0x1D7B9, 'M', u'π'),\n    (0x1D7BA, 'M', u'ρ'),\n    (0x1D7BB, 'M', u'σ'),\n    (0x1D7BD, 'M', u'τ'),\n    (0x1D7BE, 'M', u'υ'),\n    (0x1D7BF, 'M', u'φ'),\n    (0x1D7C0, 'M', u'χ'),\n    (0x1D7C1, 'M', u'ψ'),\n    (0x1D7C2, 'M', u'ω'),\n    (0x1D7C3, 'M', u'∂'),\n    (0x1D7C4, 'M', u'ε'),\n    (0x1D7C5, 'M', u'θ'),\n    (0x1D7C6, 'M', u'κ'),\n    (0x1D7C7, 'M', u'φ'),\n    (0x1D7C8, 'M', u'ρ'),\n    (0x1D7C9, 'M', u'π'),\n    (0x1D7CA, 'M', u'ϝ'),\n    (0x1D7CC, 'X'),\n    (0x1D7CE, 'M', u'0'),\n    (0x1D7CF, 'M', u'1'),\n    (0x1D7D0, 'M', u'2'),\n    (0x1D7D1, 'M', u'3'),\n    (0x1D7D2, 'M', u'4'),\n    (0x1D7D3, 'M', u'5'),\n    (0x1D7D4, 'M', u'6'),\n    (0x1D7D5, 'M', u'7'),\n    (0x1D7D6, 'M', u'8'),\n    (0x1D7D7, 'M', u'9'),\n    (0x1D7D8, 'M', u'0'),\n    (0x1D7D9, 'M', u'1'),\n    (0x1D7DA, 'M', u'2'),\n    (0x1D7DB, 'M', u'3'),\n    (0x1D7DC, 'M', u'4'),\n    (0x1D7DD, 'M', u'5'),\n    (0x1D7DE, 'M', u'6'),\n    (0x1D7DF, 'M', u'7'),\n    (0x1D7E0, 'M', u'8'),\n    (0x1D7E1, 'M', u'9'),\n    (0x1D7E2, 'M', u'0'),\n    (0x1D7E3, 'M', u'1'),\n    ]\n\ndef _seg_69():\n    return [\n    (0x1D7E4, 'M', u'2'),\n    (0x1D7E5, 'M', u'3'),\n    (0x1D7E6, 'M', u'4'),\n    (0x1D7E7, 'M', u'5'),\n    (0x1D7E8, 'M', u'6'),\n    (0x1D7E9, 'M', u'7'),\n    (0x1D7EA, 'M', u'8'),\n    (0x1D7EB, 'M', u'9'),\n    (0x1D7EC, 'M', u'0'),\n    (0x1D7ED, 'M', u'1'),\n    (0x1D7EE, 'M', u'2'),\n    (0x1D7EF, 'M', u'3'),\n    (0x1D7F0, 'M', u'4'),\n    (0x1D7F1, 'M', u'5'),\n    (0x1D7F2, 'M', u'6'),\n    (0x1D7F3, 'M', u'7'),\n    (0x1D7F4, 'M', u'8'),\n    (0x1D7F5, 'M', u'9'),\n    (0x1D7F6, 'M', u'0'),\n    (0x1D7F7, 'M', u'1'),\n    (0x1D7F8, 'M', u'2'),\n    (0x1D7F9, 'M', u'3'),\n    (0x1D7FA, 'M', u'4'),\n    (0x1D7FB, 'M', u'5'),\n    (0x1D7FC, 'M', u'6'),\n    (0x1D7FD, 'M', u'7'),\n    (0x1D7FE, 'M', u'8'),\n    (0x1D7FF, 'M', u'9'),\n    (0x1D800, 'V'),\n    (0x1DA8C, 'X'),\n    (0x1DA9B, 'V'),\n    (0x1DAA0, 'X'),\n    (0x1DAA1, 'V'),\n    (0x1DAB0, 'X'),\n    (0x1E000, 'V'),\n    (0x1E007, 'X'),\n    (0x1E008, 'V'),\n    (0x1E019, 'X'),\n    (0x1E01B, 'V'),\n    (0x1E022, 'X'),\n    (0x1E023, 'V'),\n    (0x1E025, 'X'),\n    (0x1E026, 'V'),\n    (0x1E02B, 'X'),\n    (0x1E100, 'V'),\n    (0x1E12D, 'X'),\n    (0x1E130, 'V'),\n    (0x1E13E, 'X'),\n    (0x1E140, 'V'),\n    (0x1E14A, 'X'),\n    (0x1E14E, 'V'),\n    (0x1E150, 'X'),\n    (0x1E2C0, 'V'),\n    (0x1E2FA, 'X'),\n    (0x1E2FF, 'V'),\n    (0x1E300, 'X'),\n    (0x1E800, 'V'),\n    (0x1E8C5, 'X'),\n    (0x1E8C7, 'V'),\n    (0x1E8D7, 'X'),\n    (0x1E900, 'M', u'𞤢'),\n    (0x1E901, 'M', u'𞤣'),\n    (0x1E902, 'M', u'𞤤'),\n    (0x1E903, 'M', u'𞤥'),\n    (0x1E904, 'M', u'𞤦'),\n    (0x1E905, 'M', u'𞤧'),\n    (0x1E906, 'M', u'𞤨'),\n    (0x1E907, 'M', u'𞤩'),\n    (0x1E908, 'M', u'𞤪'),\n    (0x1E909, 'M', u'𞤫'),\n    (0x1E90A, 'M', u'𞤬'),\n    (0x1E90B, 'M', u'𞤭'),\n    (0x1E90C, 'M', u'𞤮'),\n    (0x1E90D, 'M', u'𞤯'),\n    (0x1E90E, 'M', u'𞤰'),\n    (0x1E90F, 'M', u'𞤱'),\n    (0x1E910, 'M', u'𞤲'),\n    (0x1E911, 'M', u'𞤳'),\n    (0x1E912, 'M', u'𞤴'),\n    (0x1E913, 'M', u'𞤵'),\n    (0x1E914, 'M', u'𞤶'),\n    (0x1E915, 'M', u'𞤷'),\n    (0x1E916, 'M', u'𞤸'),\n    (0x1E917, 'M', u'𞤹'),\n    (0x1E918, 'M', u'𞤺'),\n    (0x1E919, 'M', u'𞤻'),\n    (0x1E91A, 'M', u'𞤼'),\n    (0x1E91B, 'M', u'𞤽'),\n    (0x1E91C, 'M', u'𞤾'),\n    (0x1E91D, 'M', u'𞤿'),\n    (0x1E91E, 'M', u'𞥀'),\n    (0x1E91F, 'M', u'𞥁'),\n    (0x1E920, 'M', u'𞥂'),\n    (0x1E921, 'M', u'𞥃'),\n    (0x1E922, 'V'),\n    (0x1E94C, 'X'),\n    (0x1E950, 'V'),\n    (0x1E95A, 'X'),\n    (0x1E95E, 'V'),\n    (0x1E960, 'X'),\n    ]\n\ndef _seg_70():\n    return [\n    (0x1EC71, 'V'),\n    (0x1ECB5, 'X'),\n    (0x1ED01, 'V'),\n    (0x1ED3E, 'X'),\n    (0x1EE00, 'M', u'ا'),\n    (0x1EE01, 'M', u'ب'),\n    (0x1EE02, 'M', u'ج'),\n    (0x1EE03, 'M', u'د'),\n    (0x1EE04, 'X'),\n    (0x1EE05, 'M', u'و'),\n    (0x1EE06, 'M', u'ز'),\n    (0x1EE07, 'M', u'ح'),\n    (0x1EE08, 'M', u'ط'),\n    (0x1EE09, 'M', u'ي'),\n    (0x1EE0A, 'M', u'ك'),\n    (0x1EE0B, 'M', u'ل'),\n    (0x1EE0C, 'M', u'م'),\n    (0x1EE0D, 'M', u'ن'),\n    (0x1EE0E, 'M', u'س'),\n    (0x1EE0F, 'M', u'ع'),\n    (0x1EE10, 'M', u'ف'),\n    (0x1EE11, 'M', u'ص'),\n    (0x1EE12, 'M', u'ق'),\n    (0x1EE13, 'M', u'ر'),\n    (0x1EE14, 'M', u'ش'),\n    (0x1EE15, 'M', u'ت'),\n    (0x1EE16, 'M', u'ث'),\n    (0x1EE17, 'M', u'خ'),\n    (0x1EE18, 'M', u'ذ'),\n    (0x1EE19, 'M', u'ض'),\n    (0x1EE1A, 'M', u'ظ'),\n    (0x1EE1B, 'M', u'غ'),\n    (0x1EE1C, 'M', u'ٮ'),\n    (0x1EE1D, 'M', u'ں'),\n    (0x1EE1E, 'M', u'ڡ'),\n    (0x1EE1F, 'M', u'ٯ'),\n    (0x1EE20, 'X'),\n    (0x1EE21, 'M', u'ب'),\n    (0x1EE22, 'M', u'ج'),\n    (0x1EE23, 'X'),\n    (0x1EE24, 'M', u'ه'),\n    (0x1EE25, 'X'),\n    (0x1EE27, 'M', u'ح'),\n    (0x1EE28, 'X'),\n    (0x1EE29, 'M', u'ي'),\n    (0x1EE2A, 'M', u'ك'),\n    (0x1EE2B, 'M', u'ل'),\n    (0x1EE2C, 'M', u'م'),\n    (0x1EE2D, 'M', u'ن'),\n    (0x1EE2E, 'M', u'س'),\n    (0x1EE2F, 'M', u'ع'),\n    (0x1EE30, 'M', u'ف'),\n    (0x1EE31, 'M', u'ص'),\n    (0x1EE32, 'M', u'ق'),\n    (0x1EE33, 'X'),\n    (0x1EE34, 'M', u'ش'),\n    (0x1EE35, 'M', u'ت'),\n    (0x1EE36, 'M', u'ث'),\n    (0x1EE37, 'M', u'خ'),\n    (0x1EE38, 'X'),\n    (0x1EE39, 'M', u'ض'),\n    (0x1EE3A, 'X'),\n    (0x1EE3B, 'M', u'غ'),\n    (0x1EE3C, 'X'),\n    (0x1EE42, 'M', u'ج'),\n    (0x1EE43, 'X'),\n    (0x1EE47, 'M', u'ح'),\n    (0x1EE48, 'X'),\n    (0x1EE49, 'M', u'ي'),\n    (0x1EE4A, 'X'),\n    (0x1EE4B, 'M', u'ل'),\n    (0x1EE4C, 'X'),\n    (0x1EE4D, 'M', u'ن'),\n    (0x1EE4E, 'M', u'س'),\n    (0x1EE4F, 'M', u'ع'),\n    (0x1EE50, 'X'),\n    (0x1EE51, 'M', u'ص'),\n    (0x1EE52, 'M', u'ق'),\n    (0x1EE53, 'X'),\n    (0x1EE54, 'M', u'ش'),\n    (0x1EE55, 'X'),\n    (0x1EE57, 'M', u'خ'),\n    (0x1EE58, 'X'),\n    (0x1EE59, 'M', u'ض'),\n    (0x1EE5A, 'X'),\n    (0x1EE5B, 'M', u'غ'),\n    (0x1EE5C, 'X'),\n    (0x1EE5D, 'M', u'ں'),\n    (0x1EE5E, 'X'),\n    (0x1EE5F, 'M', u'ٯ'),\n    (0x1EE60, 'X'),\n    (0x1EE61, 'M', u'ب'),\n    (0x1EE62, 'M', u'ج'),\n    (0x1EE63, 'X'),\n    (0x1EE64, 'M', u'ه'),\n    (0x1EE65, 'X'),\n    (0x1EE67, 'M', u'ح'),\n    (0x1EE68, 'M', u'ط'),\n    (0x1EE69, 'M', u'ي'),\n    (0x1EE6A, 'M', u'ك'),\n    ]\n\ndef _seg_71():\n    return [\n    (0x1EE6B, 'X'),\n    (0x1EE6C, 'M', u'م'),\n    (0x1EE6D, 'M', u'ن'),\n    (0x1EE6E, 'M', u'س'),\n    (0x1EE6F, 'M', u'ع'),\n    (0x1EE70, 'M', u'ف'),\n    (0x1EE71, 'M', u'ص'),\n    (0x1EE72, 'M', u'ق'),\n    (0x1EE73, 'X'),\n    (0x1EE74, 'M', u'ش'),\n    (0x1EE75, 'M', u'ت'),\n    (0x1EE76, 'M', u'ث'),\n    (0x1EE77, 'M', u'خ'),\n    (0x1EE78, 'X'),\n    (0x1EE79, 'M', u'ض'),\n    (0x1EE7A, 'M', u'ظ'),\n    (0x1EE7B, 'M', u'غ'),\n    (0x1EE7C, 'M', u'ٮ'),\n    (0x1EE7D, 'X'),\n    (0x1EE7E, 'M', u'ڡ'),\n    (0x1EE7F, 'X'),\n    (0x1EE80, 'M', u'ا'),\n    (0x1EE81, 'M', u'ب'),\n    (0x1EE82, 'M', u'ج'),\n    (0x1EE83, 'M', u'د'),\n    (0x1EE84, 'M', u'ه'),\n    (0x1EE85, 'M', u'و'),\n    (0x1EE86, 'M', u'ز'),\n    (0x1EE87, 'M', u'ح'),\n    (0x1EE88, 'M', u'ط'),\n    (0x1EE89, 'M', u'ي'),\n    (0x1EE8A, 'X'),\n    (0x1EE8B, 'M', u'ل'),\n    (0x1EE8C, 'M', u'م'),\n    (0x1EE8D, 'M', u'ن'),\n    (0x1EE8E, 'M', u'س'),\n    (0x1EE8F, 'M', u'ع'),\n    (0x1EE90, 'M', u'ف'),\n    (0x1EE91, 'M', u'ص'),\n    (0x1EE92, 'M', u'ق'),\n    (0x1EE93, 'M', u'ر'),\n    (0x1EE94, 'M', u'ش'),\n    (0x1EE95, 'M', u'ت'),\n    (0x1EE96, 'M', u'ث'),\n    (0x1EE97, 'M', u'خ'),\n    (0x1EE98, 'M', u'ذ'),\n    (0x1EE99, 'M', u'ض'),\n    (0x1EE9A, 'M', u'ظ'),\n    (0x1EE9B, 'M', u'غ'),\n    (0x1EE9C, 'X'),\n    (0x1EEA1, 'M', u'ب'),\n    (0x1EEA2, 'M', u'ج'),\n    (0x1EEA3, 'M', u'د'),\n    (0x1EEA4, 'X'),\n    (0x1EEA5, 'M', u'و'),\n    (0x1EEA6, 'M', u'ز'),\n    (0x1EEA7, 'M', u'ح'),\n    (0x1EEA8, 'M', u'ط'),\n    (0x1EEA9, 'M', u'ي'),\n    (0x1EEAA, 'X'),\n    (0x1EEAB, 'M', u'ل'),\n    (0x1EEAC, 'M', u'م'),\n    (0x1EEAD, 'M', u'ن'),\n    (0x1EEAE, 'M', u'س'),\n    (0x1EEAF, 'M', u'ع'),\n    (0x1EEB0, 'M', u'ف'),\n    (0x1EEB1, 'M', u'ص'),\n    (0x1EEB2, 'M', u'ق'),\n    (0x1EEB3, 'M', u'ر'),\n    (0x1EEB4, 'M', u'ش'),\n    (0x1EEB5, 'M', u'ت'),\n    (0x1EEB6, 'M', u'ث'),\n    (0x1EEB7, 'M', u'خ'),\n    (0x1EEB8, 'M', u'ذ'),\n    (0x1EEB9, 'M', u'ض'),\n    (0x1EEBA, 'M', u'ظ'),\n    (0x1EEBB, 'M', u'غ'),\n    (0x1EEBC, 'X'),\n    (0x1EEF0, 'V'),\n    (0x1EEF2, 'X'),\n    (0x1F000, 'V'),\n    (0x1F02C, 'X'),\n    (0x1F030, 'V'),\n    (0x1F094, 'X'),\n    (0x1F0A0, 'V'),\n    (0x1F0AF, 'X'),\n    (0x1F0B1, 'V'),\n    (0x1F0C0, 'X'),\n    (0x1F0C1, 'V'),\n    (0x1F0D0, 'X'),\n    (0x1F0D1, 'V'),\n    (0x1F0F6, 'X'),\n    (0x1F101, '3', u'0,'),\n    (0x1F102, '3', u'1,'),\n    (0x1F103, '3', u'2,'),\n    (0x1F104, '3', u'3,'),\n    (0x1F105, '3', u'4,'),\n    (0x1F106, '3', u'5,'),\n    (0x1F107, '3', u'6,'),\n    (0x1F108, '3', u'7,'),\n    ]\n\ndef _seg_72():\n    return [\n    (0x1F109, '3', u'8,'),\n    (0x1F10A, '3', u'9,'),\n    (0x1F10B, 'V'),\n    (0x1F110, '3', u'(a)'),\n    (0x1F111, '3', u'(b)'),\n    (0x1F112, '3', u'(c)'),\n    (0x1F113, '3', u'(d)'),\n    (0x1F114, '3', u'(e)'),\n    (0x1F115, '3', u'(f)'),\n    (0x1F116, '3', u'(g)'),\n    (0x1F117, '3', u'(h)'),\n    (0x1F118, '3', u'(i)'),\n    (0x1F119, '3', u'(j)'),\n    (0x1F11A, '3', u'(k)'),\n    (0x1F11B, '3', u'(l)'),\n    (0x1F11C, '3', u'(m)'),\n    (0x1F11D, '3', u'(n)'),\n    (0x1F11E, '3', u'(o)'),\n    (0x1F11F, '3', u'(p)'),\n    (0x1F120, '3', u'(q)'),\n    (0x1F121, '3', u'(r)'),\n    (0x1F122, '3', u'(s)'),\n    (0x1F123, '3', u'(t)'),\n    (0x1F124, '3', u'(u)'),\n    (0x1F125, '3', u'(v)'),\n    (0x1F126, '3', u'(w)'),\n    (0x1F127, '3', u'(x)'),\n    (0x1F128, '3', u'(y)'),\n    (0x1F129, '3', u'(z)'),\n    (0x1F12A, 'M', u'〔s〕'),\n    (0x1F12B, 'M', u'c'),\n    (0x1F12C, 'M', u'r'),\n    (0x1F12D, 'M', u'cd'),\n    (0x1F12E, 'M', u'wz'),\n    (0x1F12F, 'V'),\n    (0x1F130, 'M', u'a'),\n    (0x1F131, 'M', u'b'),\n    (0x1F132, 'M', u'c'),\n    (0x1F133, 'M', u'd'),\n    (0x1F134, 'M', u'e'),\n    (0x1F135, 'M', u'f'),\n    (0x1F136, 'M', u'g'),\n    (0x1F137, 'M', u'h'),\n    (0x1F138, 'M', u'i'),\n    (0x1F139, 'M', u'j'),\n    (0x1F13A, 'M', u'k'),\n    (0x1F13B, 'M', u'l'),\n    (0x1F13C, 'M', u'm'),\n    (0x1F13D, 'M', u'n'),\n    (0x1F13E, 'M', u'o'),\n    (0x1F13F, 'M', u'p'),\n    (0x1F140, 'M', u'q'),\n    (0x1F141, 'M', u'r'),\n    (0x1F142, 'M', u's'),\n    (0x1F143, 'M', u't'),\n    (0x1F144, 'M', u'u'),\n    (0x1F145, 'M', u'v'),\n    (0x1F146, 'M', u'w'),\n    (0x1F147, 'M', u'x'),\n    (0x1F148, 'M', u'y'),\n    (0x1F149, 'M', u'z'),\n    (0x1F14A, 'M', u'hv'),\n    (0x1F14B, 'M', u'mv'),\n    (0x1F14C, 'M', u'sd'),\n    (0x1F14D, 'M', u'ss'),\n    (0x1F14E, 'M', u'ppv'),\n    (0x1F14F, 'M', u'wc'),\n    (0x1F150, 'V'),\n    (0x1F16A, 'M', u'mc'),\n    (0x1F16B, 'M', u'md'),\n    (0x1F16C, 'M', u'mr'),\n    (0x1F16D, 'V'),\n    (0x1F190, 'M', u'dj'),\n    (0x1F191, 'V'),\n    (0x1F1AE, 'X'),\n    (0x1F1E6, 'V'),\n    (0x1F200, 'M', u'ほか'),\n    (0x1F201, 'M', u'ココ'),\n    (0x1F202, 'M', u'サ'),\n    (0x1F203, 'X'),\n    (0x1F210, 'M', u'手'),\n    (0x1F211, 'M', u'字'),\n    (0x1F212, 'M', u'双'),\n    (0x1F213, 'M', u'デ'),\n    (0x1F214, 'M', u'二'),\n    (0x1F215, 'M', u'多'),\n    (0x1F216, 'M', u'解'),\n    (0x1F217, 'M', u'天'),\n    (0x1F218, 'M', u'交'),\n    (0x1F219, 'M', u'映'),\n    (0x1F21A, 'M', u'無'),\n    (0x1F21B, 'M', u'料'),\n    (0x1F21C, 'M', u'前'),\n    (0x1F21D, 'M', u'後'),\n    (0x1F21E, 'M', u'再'),\n    (0x1F21F, 'M', u'新'),\n    (0x1F220, 'M', u'初'),\n    (0x1F221, 'M', u'終'),\n    (0x1F222, 'M', u'生'),\n    (0x1F223, 'M', u'販'),\n    ]\n\ndef _seg_73():\n    return [\n    (0x1F224, 'M', u'声'),\n    (0x1F225, 'M', u'吹'),\n    (0x1F226, 'M', u'演'),\n    (0x1F227, 'M', u'投'),\n    (0x1F228, 'M', u'捕'),\n    (0x1F229, 'M', u'一'),\n    (0x1F22A, 'M', u'三'),\n    (0x1F22B, 'M', u'遊'),\n    (0x1F22C, 'M', u'左'),\n    (0x1F22D, 'M', u'中'),\n    (0x1F22E, 'M', u'右'),\n    (0x1F22F, 'M', u'指'),\n    (0x1F230, 'M', u'走'),\n    (0x1F231, 'M', u'打'),\n    (0x1F232, 'M', u'禁'),\n    (0x1F233, 'M', u'空'),\n    (0x1F234, 'M', u'合'),\n    (0x1F235, 'M', u'満'),\n    (0x1F236, 'M', u'有'),\n    (0x1F237, 'M', u'月'),\n    (0x1F238, 'M', u'申'),\n    (0x1F239, 'M', u'割'),\n    (0x1F23A, 'M', u'営'),\n    (0x1F23B, 'M', u'配'),\n    (0x1F23C, 'X'),\n    (0x1F240, 'M', u'〔本〕'),\n    (0x1F241, 'M', u'〔三〕'),\n    (0x1F242, 'M', u'〔二〕'),\n    (0x1F243, 'M', u'〔安〕'),\n    (0x1F244, 'M', u'〔点〕'),\n    (0x1F245, 'M', u'〔打〕'),\n    (0x1F246, 'M', u'〔盗〕'),\n    (0x1F247, 'M', u'〔勝〕'),\n    (0x1F248, 'M', u'〔敗〕'),\n    (0x1F249, 'X'),\n    (0x1F250, 'M', u'得'),\n    (0x1F251, 'M', u'可'),\n    (0x1F252, 'X'),\n    (0x1F260, 'V'),\n    (0x1F266, 'X'),\n    (0x1F300, 'V'),\n    (0x1F6D8, 'X'),\n    (0x1F6E0, 'V'),\n    (0x1F6ED, 'X'),\n    (0x1F6F0, 'V'),\n    (0x1F6FD, 'X'),\n    (0x1F700, 'V'),\n    (0x1F774, 'X'),\n    (0x1F780, 'V'),\n    (0x1F7D9, 'X'),\n    (0x1F7E0, 'V'),\n    (0x1F7EC, 'X'),\n    (0x1F800, 'V'),\n    (0x1F80C, 'X'),\n    (0x1F810, 'V'),\n    (0x1F848, 'X'),\n    (0x1F850, 'V'),\n    (0x1F85A, 'X'),\n    (0x1F860, 'V'),\n    (0x1F888, 'X'),\n    (0x1F890, 'V'),\n    (0x1F8AE, 'X'),\n    (0x1F8B0, 'V'),\n    (0x1F8B2, 'X'),\n    (0x1F900, 'V'),\n    (0x1F979, 'X'),\n    (0x1F97A, 'V'),\n    (0x1F9CC, 'X'),\n    (0x1F9CD, 'V'),\n    (0x1FA54, 'X'),\n    (0x1FA60, 'V'),\n    (0x1FA6E, 'X'),\n    (0x1FA70, 'V'),\n    (0x1FA75, 'X'),\n    (0x1FA78, 'V'),\n    (0x1FA7B, 'X'),\n    (0x1FA80, 'V'),\n    (0x1FA87, 'X'),\n    (0x1FA90, 'V'),\n    (0x1FAA9, 'X'),\n    (0x1FAB0, 'V'),\n    (0x1FAB7, 'X'),\n    (0x1FAC0, 'V'),\n    (0x1FAC3, 'X'),\n    (0x1FAD0, 'V'),\n    (0x1FAD7, 'X'),\n    (0x1FB00, 'V'),\n    (0x1FB93, 'X'),\n    (0x1FB94, 'V'),\n    (0x1FBCB, 'X'),\n    (0x1FBF0, 'M', u'0'),\n    (0x1FBF1, 'M', u'1'),\n    (0x1FBF2, 'M', u'2'),\n    (0x1FBF3, 'M', u'3'),\n    (0x1FBF4, 'M', u'4'),\n    (0x1FBF5, 'M', u'5'),\n    (0x1FBF6, 'M', u'6'),\n    (0x1FBF7, 'M', u'7'),\n    (0x1FBF8, 'M', u'8'),\n    (0x1FBF9, 'M', u'9'),\n    ]\n\ndef _seg_74():\n    return [\n    (0x1FBFA, 'X'),\n    (0x20000, 'V'),\n    (0x2A6DE, 'X'),\n    (0x2A700, 'V'),\n    (0x2B735, 'X'),\n    (0x2B740, 'V'),\n    (0x2B81E, 'X'),\n    (0x2B820, 'V'),\n    (0x2CEA2, 'X'),\n    (0x2CEB0, 'V'),\n    (0x2EBE1, 'X'),\n    (0x2F800, 'M', u'丽'),\n    (0x2F801, 'M', u'丸'),\n    (0x2F802, 'M', u'乁'),\n    (0x2F803, 'M', u'𠄢'),\n    (0x2F804, 'M', u'你'),\n    (0x2F805, 'M', u'侮'),\n    (0x2F806, 'M', u'侻'),\n    (0x2F807, 'M', u'倂'),\n    (0x2F808, 'M', u'偺'),\n    (0x2F809, 'M', u'備'),\n    (0x2F80A, 'M', u'僧'),\n    (0x2F80B, 'M', u'像'),\n    (0x2F80C, 'M', u'㒞'),\n    (0x2F80D, 'M', u'𠘺'),\n    (0x2F80E, 'M', u'免'),\n    (0x2F80F, 'M', u'兔'),\n    (0x2F810, 'M', u'兤'),\n    (0x2F811, 'M', u'具'),\n    (0x2F812, 'M', u'𠔜'),\n    (0x2F813, 'M', u'㒹'),\n    (0x2F814, 'M', u'內'),\n    (0x2F815, 'M', u'再'),\n    (0x2F816, 'M', u'𠕋'),\n    (0x2F817, 'M', u'冗'),\n    (0x2F818, 'M', u'冤'),\n    (0x2F819, 'M', u'仌'),\n    (0x2F81A, 'M', u'冬'),\n    (0x2F81B, 'M', u'况'),\n    (0x2F81C, 'M', u'𩇟'),\n    (0x2F81D, 'M', u'凵'),\n    (0x2F81E, 'M', u'刃'),\n    (0x2F81F, 'M', u'㓟'),\n    (0x2F820, 'M', u'刻'),\n    (0x2F821, 'M', u'剆'),\n    (0x2F822, 'M', u'割'),\n    (0x2F823, 'M', u'剷'),\n    (0x2F824, 'M', u'㔕'),\n    (0x2F825, 'M', u'勇'),\n    (0x2F826, 'M', u'勉'),\n    (0x2F827, 'M', u'勤'),\n    (0x2F828, 'M', u'勺'),\n    (0x2F829, 'M', u'包'),\n    (0x2F82A, 'M', u'匆'),\n    (0x2F82B, 'M', u'北'),\n    (0x2F82C, 'M', u'卉'),\n    (0x2F82D, 'M', u'卑'),\n    (0x2F82E, 'M', u'博'),\n    (0x2F82F, 'M', u'即'),\n    (0x2F830, 'M', u'卽'),\n    (0x2F831, 'M', u'卿'),\n    (0x2F834, 'M', u'𠨬'),\n    (0x2F835, 'M', u'灰'),\n    (0x2F836, 'M', u'及'),\n    (0x2F837, 'M', u'叟'),\n    (0x2F838, 'M', u'𠭣'),\n    (0x2F839, 'M', u'叫'),\n    (0x2F83A, 'M', u'叱'),\n    (0x2F83B, 'M', u'吆'),\n    (0x2F83C, 'M', u'咞'),\n    (0x2F83D, 'M', u'吸'),\n    (0x2F83E, 'M', u'呈'),\n    (0x2F83F, 'M', u'周'),\n    (0x2F840, 'M', u'咢'),\n    (0x2F841, 'M', u'哶'),\n    (0x2F842, 'M', u'唐'),\n    (0x2F843, 'M', u'啓'),\n    (0x2F844, 'M', u'啣'),\n    (0x2F845, 'M', u'善'),\n    (0x2F847, 'M', u'喙'),\n    (0x2F848, 'M', u'喫'),\n    (0x2F849, 'M', u'喳'),\n    (0x2F84A, 'M', u'嗂'),\n    (0x2F84B, 'M', u'圖'),\n    (0x2F84C, 'M', u'嘆'),\n    (0x2F84D, 'M', u'圗'),\n    (0x2F84E, 'M', u'噑'),\n    (0x2F84F, 'M', u'噴'),\n    (0x2F850, 'M', u'切'),\n    (0x2F851, 'M', u'壮'),\n    (0x2F852, 'M', u'城'),\n    (0x2F853, 'M', u'埴'),\n    (0x2F854, 'M', u'堍'),\n    (0x2F855, 'M', u'型'),\n    (0x2F856, 'M', u'堲'),\n    (0x2F857, 'M', u'報'),\n    (0x2F858, 'M', u'墬'),\n    (0x2F859, 'M', u'𡓤'),\n    (0x2F85A, 'M', u'売'),\n    (0x2F85B, 'M', u'壷'),\n    ]\n\ndef _seg_75():\n    return [\n    (0x2F85C, 'M', u'夆'),\n    (0x2F85D, 'M', u'多'),\n    (0x2F85E, 'M', u'夢'),\n    (0x2F85F, 'M', u'奢'),\n    (0x2F860, 'M', u'𡚨'),\n    (0x2F861, 'M', u'𡛪'),\n    (0x2F862, 'M', u'姬'),\n    (0x2F863, 'M', u'娛'),\n    (0x2F864, 'M', u'娧'),\n    (0x2F865, 'M', u'姘'),\n    (0x2F866, 'M', u'婦'),\n    (0x2F867, 'M', u'㛮'),\n    (0x2F868, 'X'),\n    (0x2F869, 'M', u'嬈'),\n    (0x2F86A, 'M', u'嬾'),\n    (0x2F86C, 'M', u'𡧈'),\n    (0x2F86D, 'M', u'寃'),\n    (0x2F86E, 'M', u'寘'),\n    (0x2F86F, 'M', u'寧'),\n    (0x2F870, 'M', u'寳'),\n    (0x2F871, 'M', u'𡬘'),\n    (0x2F872, 'M', u'寿'),\n    (0x2F873, 'M', u'将'),\n    (0x2F874, 'X'),\n    (0x2F875, 'M', u'尢'),\n    (0x2F876, 'M', u'㞁'),\n    (0x2F877, 'M', u'屠'),\n    (0x2F878, 'M', u'屮'),\n    (0x2F879, 'M', u'峀'),\n    (0x2F87A, 'M', u'岍'),\n    (0x2F87B, 'M', u'𡷤'),\n    (0x2F87C, 'M', u'嵃'),\n    (0x2F87D, 'M', u'𡷦'),\n    (0x2F87E, 'M', u'嵮'),\n    (0x2F87F, 'M', u'嵫'),\n    (0x2F880, 'M', u'嵼'),\n    (0x2F881, 'M', u'巡'),\n    (0x2F882, 'M', u'巢'),\n    (0x2F883, 'M', u'㠯'),\n    (0x2F884, 'M', u'巽'),\n    (0x2F885, 'M', u'帨'),\n    (0x2F886, 'M', u'帽'),\n    (0x2F887, 'M', u'幩'),\n    (0x2F888, 'M', u'㡢'),\n    (0x2F889, 'M', u'𢆃'),\n    (0x2F88A, 'M', u'㡼'),\n    (0x2F88B, 'M', u'庰'),\n    (0x2F88C, 'M', u'庳'),\n    (0x2F88D, 'M', u'庶'),\n    (0x2F88E, 'M', u'廊'),\n    (0x2F88F, 'M', u'𪎒'),\n    (0x2F890, 'M', u'廾'),\n    (0x2F891, 'M', u'𢌱'),\n    (0x2F893, 'M', u'舁'),\n    (0x2F894, 'M', u'弢'),\n    (0x2F896, 'M', u'㣇'),\n    (0x2F897, 'M', u'𣊸'),\n    (0x2F898, 'M', u'𦇚'),\n    (0x2F899, 'M', u'形'),\n    (0x2F89A, 'M', u'彫'),\n    (0x2F89B, 'M', u'㣣'),\n    (0x2F89C, 'M', u'徚'),\n    (0x2F89D, 'M', u'忍'),\n    (0x2F89E, 'M', u'志'),\n    (0x2F89F, 'M', u'忹'),\n    (0x2F8A0, 'M', u'悁'),\n    (0x2F8A1, 'M', u'㤺'),\n    (0x2F8A2, 'M', u'㤜'),\n    (0x2F8A3, 'M', u'悔'),\n    (0x2F8A4, 'M', u'𢛔'),\n    (0x2F8A5, 'M', u'惇'),\n    (0x2F8A6, 'M', u'慈'),\n    (0x2F8A7, 'M', u'慌'),\n    (0x2F8A8, 'M', u'慎'),\n    (0x2F8A9, 'M', u'慌'),\n    (0x2F8AA, 'M', u'慺'),\n    (0x2F8AB, 'M', u'憎'),\n    (0x2F8AC, 'M', u'憲'),\n    (0x2F8AD, 'M', u'憤'),\n    (0x2F8AE, 'M', u'憯'),\n    (0x2F8AF, 'M', u'懞'),\n    (0x2F8B0, 'M', u'懲'),\n    (0x2F8B1, 'M', u'懶'),\n    (0x2F8B2, 'M', u'成'),\n    (0x2F8B3, 'M', u'戛'),\n    (0x2F8B4, 'M', u'扝'),\n    (0x2F8B5, 'M', u'抱'),\n    (0x2F8B6, 'M', u'拔'),\n    (0x2F8B7, 'M', u'捐'),\n    (0x2F8B8, 'M', u'𢬌'),\n    (0x2F8B9, 'M', u'挽'),\n    (0x2F8BA, 'M', u'拼'),\n    (0x2F8BB, 'M', u'捨'),\n    (0x2F8BC, 'M', u'掃'),\n    (0x2F8BD, 'M', u'揤'),\n    (0x2F8BE, 'M', u'𢯱'),\n    (0x2F8BF, 'M', u'搢'),\n    (0x2F8C0, 'M', u'揅'),\n    (0x2F8C1, 'M', u'掩'),\n    (0x2F8C2, 'M', u'㨮'),\n    ]\n\ndef _seg_76():\n    return [\n    (0x2F8C3, 'M', u'摩'),\n    (0x2F8C4, 'M', u'摾'),\n    (0x2F8C5, 'M', u'撝'),\n    (0x2F8C6, 'M', u'摷'),\n    (0x2F8C7, 'M', u'㩬'),\n    (0x2F8C8, 'M', u'敏'),\n    (0x2F8C9, 'M', u'敬'),\n    (0x2F8CA, 'M', u'𣀊'),\n    (0x2F8CB, 'M', u'旣'),\n    (0x2F8CC, 'M', u'書'),\n    (0x2F8CD, 'M', u'晉'),\n    (0x2F8CE, 'M', u'㬙'),\n    (0x2F8CF, 'M', u'暑'),\n    (0x2F8D0, 'M', u'㬈'),\n    (0x2F8D1, 'M', u'㫤'),\n    (0x2F8D2, 'M', u'冒'),\n    (0x2F8D3, 'M', u'冕'),\n    (0x2F8D4, 'M', u'最'),\n    (0x2F8D5, 'M', u'暜'),\n    (0x2F8D6, 'M', u'肭'),\n    (0x2F8D7, 'M', u'䏙'),\n    (0x2F8D8, 'M', u'朗'),\n    (0x2F8D9, 'M', u'望'),\n    (0x2F8DA, 'M', u'朡'),\n    (0x2F8DB, 'M', u'杞'),\n    (0x2F8DC, 'M', u'杓'),\n    (0x2F8DD, 'M', u'𣏃'),\n    (0x2F8DE, 'M', u'㭉'),\n    (0x2F8DF, 'M', u'柺'),\n    (0x2F8E0, 'M', u'枅'),\n    (0x2F8E1, 'M', u'桒'),\n    (0x2F8E2, 'M', u'梅'),\n    (0x2F8E3, 'M', u'𣑭'),\n    (0x2F8E4, 'M', u'梎'),\n    (0x2F8E5, 'M', u'栟'),\n    (0x2F8E6, 'M', u'椔'),\n    (0x2F8E7, 'M', u'㮝'),\n    (0x2F8E8, 'M', u'楂'),\n    (0x2F8E9, 'M', u'榣'),\n    (0x2F8EA, 'M', u'槪'),\n    (0x2F8EB, 'M', u'檨'),\n    (0x2F8EC, 'M', u'𣚣'),\n    (0x2F8ED, 'M', u'櫛'),\n    (0x2F8EE, 'M', u'㰘'),\n    (0x2F8EF, 'M', u'次'),\n    (0x2F8F0, 'M', u'𣢧'),\n    (0x2F8F1, 'M', u'歔'),\n    (0x2F8F2, 'M', u'㱎'),\n    (0x2F8F3, 'M', u'歲'),\n    (0x2F8F4, 'M', u'殟'),\n    (0x2F8F5, 'M', u'殺'),\n    (0x2F8F6, 'M', u'殻'),\n    (0x2F8F7, 'M', u'𣪍'),\n    (0x2F8F8, 'M', u'𡴋'),\n    (0x2F8F9, 'M', u'𣫺'),\n    (0x2F8FA, 'M', u'汎'),\n    (0x2F8FB, 'M', u'𣲼'),\n    (0x2F8FC, 'M', u'沿'),\n    (0x2F8FD, 'M', u'泍'),\n    (0x2F8FE, 'M', u'汧'),\n    (0x2F8FF, 'M', u'洖'),\n    (0x2F900, 'M', u'派'),\n    (0x2F901, 'M', u'海'),\n    (0x2F902, 'M', u'流'),\n    (0x2F903, 'M', u'浩'),\n    (0x2F904, 'M', u'浸'),\n    (0x2F905, 'M', u'涅'),\n    (0x2F906, 'M', u'𣴞'),\n    (0x2F907, 'M', u'洴'),\n    (0x2F908, 'M', u'港'),\n    (0x2F909, 'M', u'湮'),\n    (0x2F90A, 'M', u'㴳'),\n    (0x2F90B, 'M', u'滋'),\n    (0x2F90C, 'M', u'滇'),\n    (0x2F90D, 'M', u'𣻑'),\n    (0x2F90E, 'M', u'淹'),\n    (0x2F90F, 'M', u'潮'),\n    (0x2F910, 'M', u'𣽞'),\n    (0x2F911, 'M', u'𣾎'),\n    (0x2F912, 'M', u'濆'),\n    (0x2F913, 'M', u'瀹'),\n    (0x2F914, 'M', u'瀞'),\n    (0x2F915, 'M', u'瀛'),\n    (0x2F916, 'M', u'㶖'),\n    (0x2F917, 'M', u'灊'),\n    (0x2F918, 'M', u'災'),\n    (0x2F919, 'M', u'灷'),\n    (0x2F91A, 'M', u'炭'),\n    (0x2F91B, 'M', u'𠔥'),\n    (0x2F91C, 'M', u'煅'),\n    (0x2F91D, 'M', u'𤉣'),\n    (0x2F91E, 'M', u'熜'),\n    (0x2F91F, 'X'),\n    (0x2F920, 'M', u'爨'),\n    (0x2F921, 'M', u'爵'),\n    (0x2F922, 'M', u'牐'),\n    (0x2F923, 'M', u'𤘈'),\n    (0x2F924, 'M', u'犀'),\n    (0x2F925, 'M', u'犕'),\n    (0x2F926, 'M', u'𤜵'),\n    ]\n\ndef _seg_77():\n    return [\n    (0x2F927, 'M', u'𤠔'),\n    (0x2F928, 'M', u'獺'),\n    (0x2F929, 'M', u'王'),\n    (0x2F92A, 'M', u'㺬'),\n    (0x2F92B, 'M', u'玥'),\n    (0x2F92C, 'M', u'㺸'),\n    (0x2F92E, 'M', u'瑇'),\n    (0x2F92F, 'M', u'瑜'),\n    (0x2F930, 'M', u'瑱'),\n    (0x2F931, 'M', u'璅'),\n    (0x2F932, 'M', u'瓊'),\n    (0x2F933, 'M', u'㼛'),\n    (0x2F934, 'M', u'甤'),\n    (0x2F935, 'M', u'𤰶'),\n    (0x2F936, 'M', u'甾'),\n    (0x2F937, 'M', u'𤲒'),\n    (0x2F938, 'M', u'異'),\n    (0x2F939, 'M', u'𢆟'),\n    (0x2F93A, 'M', u'瘐'),\n    (0x2F93B, 'M', u'𤾡'),\n    (0x2F93C, 'M', u'𤾸'),\n    (0x2F93D, 'M', u'𥁄'),\n    (0x2F93E, 'M', u'㿼'),\n    (0x2F93F, 'M', u'䀈'),\n    (0x2F940, 'M', u'直'),\n    (0x2F941, 'M', u'𥃳'),\n    (0x2F942, 'M', u'𥃲'),\n    (0x2F943, 'M', u'𥄙'),\n    (0x2F944, 'M', u'𥄳'),\n    (0x2F945, 'M', u'眞'),\n    (0x2F946, 'M', u'真'),\n    (0x2F948, 'M', u'睊'),\n    (0x2F949, 'M', u'䀹'),\n    (0x2F94A, 'M', u'瞋'),\n    (0x2F94B, 'M', u'䁆'),\n    (0x2F94C, 'M', u'䂖'),\n    (0x2F94D, 'M', u'𥐝'),\n    (0x2F94E, 'M', u'硎'),\n    (0x2F94F, 'M', u'碌'),\n    (0x2F950, 'M', u'磌'),\n    (0x2F951, 'M', u'䃣'),\n    (0x2F952, 'M', u'𥘦'),\n    (0x2F953, 'M', u'祖'),\n    (0x2F954, 'M', u'𥚚'),\n    (0x2F955, 'M', u'𥛅'),\n    (0x2F956, 'M', u'福'),\n    (0x2F957, 'M', u'秫'),\n    (0x2F958, 'M', u'䄯'),\n    (0x2F959, 'M', u'穀'),\n    (0x2F95A, 'M', u'穊'),\n    (0x2F95B, 'M', u'穏'),\n    (0x2F95C, 'M', u'𥥼'),\n    (0x2F95D, 'M', u'𥪧'),\n    (0x2F95F, 'X'),\n    (0x2F960, 'M', u'䈂'),\n    (0x2F961, 'M', u'𥮫'),\n    (0x2F962, 'M', u'篆'),\n    (0x2F963, 'M', u'築'),\n    (0x2F964, 'M', u'䈧'),\n    (0x2F965, 'M', u'𥲀'),\n    (0x2F966, 'M', u'糒'),\n    (0x2F967, 'M', u'䊠'),\n    (0x2F968, 'M', u'糨'),\n    (0x2F969, 'M', u'糣'),\n    (0x2F96A, 'M', u'紀'),\n    (0x2F96B, 'M', u'𥾆'),\n    (0x2F96C, 'M', u'絣'),\n    (0x2F96D, 'M', u'䌁'),\n    (0x2F96E, 'M', u'緇'),\n    (0x2F96F, 'M', u'縂'),\n    (0x2F970, 'M', u'繅'),\n    (0x2F971, 'M', u'䌴'),\n    (0x2F972, 'M', u'𦈨'),\n    (0x2F973, 'M', u'𦉇'),\n    (0x2F974, 'M', u'䍙'),\n    (0x2F975, 'M', u'𦋙'),\n    (0x2F976, 'M', u'罺'),\n    (0x2F977, 'M', u'𦌾'),\n    (0x2F978, 'M', u'羕'),\n    (0x2F979, 'M', u'翺'),\n    (0x2F97A, 'M', u'者'),\n    (0x2F97B, 'M', u'𦓚'),\n    (0x2F97C, 'M', u'𦔣'),\n    (0x2F97D, 'M', u'聠'),\n    (0x2F97E, 'M', u'𦖨'),\n    (0x2F97F, 'M', u'聰'),\n    (0x2F980, 'M', u'𣍟'),\n    (0x2F981, 'M', u'䏕'),\n    (0x2F982, 'M', u'育'),\n    (0x2F983, 'M', u'脃'),\n    (0x2F984, 'M', u'䐋'),\n    (0x2F985, 'M', u'脾'),\n    (0x2F986, 'M', u'媵'),\n    (0x2F987, 'M', u'𦞧'),\n    (0x2F988, 'M', u'𦞵'),\n    (0x2F989, 'M', u'𣎓'),\n    (0x2F98A, 'M', u'𣎜'),\n    (0x2F98B, 'M', u'舁'),\n    (0x2F98C, 'M', u'舄'),\n    (0x2F98D, 'M', u'辞'),\n    ]\n\ndef _seg_78():\n    return [\n    (0x2F98E, 'M', u'䑫'),\n    (0x2F98F, 'M', u'芑'),\n    (0x2F990, 'M', u'芋'),\n    (0x2F991, 'M', u'芝'),\n    (0x2F992, 'M', u'劳'),\n    (0x2F993, 'M', u'花'),\n    (0x2F994, 'M', u'芳'),\n    (0x2F995, 'M', u'芽'),\n    (0x2F996, 'M', u'苦'),\n    (0x2F997, 'M', u'𦬼'),\n    (0x2F998, 'M', u'若'),\n    (0x2F999, 'M', u'茝'),\n    (0x2F99A, 'M', u'荣'),\n    (0x2F99B, 'M', u'莭'),\n    (0x2F99C, 'M', u'茣'),\n    (0x2F99D, 'M', u'莽'),\n    (0x2F99E, 'M', u'菧'),\n    (0x2F99F, 'M', u'著'),\n    (0x2F9A0, 'M', u'荓'),\n    (0x2F9A1, 'M', u'菊'),\n    (0x2F9A2, 'M', u'菌'),\n    (0x2F9A3, 'M', u'菜'),\n    (0x2F9A4, 'M', u'𦰶'),\n    (0x2F9A5, 'M', u'𦵫'),\n    (0x2F9A6, 'M', u'𦳕'),\n    (0x2F9A7, 'M', u'䔫'),\n    (0x2F9A8, 'M', u'蓱'),\n    (0x2F9A9, 'M', u'蓳'),\n    (0x2F9AA, 'M', u'蔖'),\n    (0x2F9AB, 'M', u'𧏊'),\n    (0x2F9AC, 'M', u'蕤'),\n    (0x2F9AD, 'M', u'𦼬'),\n    (0x2F9AE, 'M', u'䕝'),\n    (0x2F9AF, 'M', u'䕡'),\n    (0x2F9B0, 'M', u'𦾱'),\n    (0x2F9B1, 'M', u'𧃒'),\n    (0x2F9B2, 'M', u'䕫'),\n    (0x2F9B3, 'M', u'虐'),\n    (0x2F9B4, 'M', u'虜'),\n    (0x2F9B5, 'M', u'虧'),\n    (0x2F9B6, 'M', u'虩'),\n    (0x2F9B7, 'M', u'蚩'),\n    (0x2F9B8, 'M', u'蚈'),\n    (0x2F9B9, 'M', u'蜎'),\n    (0x2F9BA, 'M', u'蛢'),\n    (0x2F9BB, 'M', u'蝹'),\n    (0x2F9BC, 'M', u'蜨'),\n    (0x2F9BD, 'M', u'蝫'),\n    (0x2F9BE, 'M', u'螆'),\n    (0x2F9BF, 'X'),\n    (0x2F9C0, 'M', u'蟡'),\n    (0x2F9C1, 'M', u'蠁'),\n    (0x2F9C2, 'M', u'䗹'),\n    (0x2F9C3, 'M', u'衠'),\n    (0x2F9C4, 'M', u'衣'),\n    (0x2F9C5, 'M', u'𧙧'),\n    (0x2F9C6, 'M', u'裗'),\n    (0x2F9C7, 'M', u'裞'),\n    (0x2F9C8, 'M', u'䘵'),\n    (0x2F9C9, 'M', u'裺'),\n    (0x2F9CA, 'M', u'㒻'),\n    (0x2F9CB, 'M', u'𧢮'),\n    (0x2F9CC, 'M', u'𧥦'),\n    (0x2F9CD, 'M', u'䚾'),\n    (0x2F9CE, 'M', u'䛇'),\n    (0x2F9CF, 'M', u'誠'),\n    (0x2F9D0, 'M', u'諭'),\n    (0x2F9D1, 'M', u'變'),\n    (0x2F9D2, 'M', u'豕'),\n    (0x2F9D3, 'M', u'𧲨'),\n    (0x2F9D4, 'M', u'貫'),\n    (0x2F9D5, 'M', u'賁'),\n    (0x2F9D6, 'M', u'贛'),\n    (0x2F9D7, 'M', u'起'),\n    (0x2F9D8, 'M', u'𧼯'),\n    (0x2F9D9, 'M', u'𠠄'),\n    (0x2F9DA, 'M', u'跋'),\n    (0x2F9DB, 'M', u'趼'),\n    (0x2F9DC, 'M', u'跰'),\n    (0x2F9DD, 'M', u'𠣞'),\n    (0x2F9DE, 'M', u'軔'),\n    (0x2F9DF, 'M', u'輸'),\n    (0x2F9E0, 'M', u'𨗒'),\n    (0x2F9E1, 'M', u'𨗭'),\n    (0x2F9E2, 'M', u'邔'),\n    (0x2F9E3, 'M', u'郱'),\n    (0x2F9E4, 'M', u'鄑'),\n    (0x2F9E5, 'M', u'𨜮'),\n    (0x2F9E6, 'M', u'鄛'),\n    (0x2F9E7, 'M', u'鈸'),\n    (0x2F9E8, 'M', u'鋗'),\n    (0x2F9E9, 'M', u'鋘'),\n    (0x2F9EA, 'M', u'鉼'),\n    (0x2F9EB, 'M', u'鏹'),\n    (0x2F9EC, 'M', u'鐕'),\n    (0x2F9ED, 'M', u'𨯺'),\n    (0x2F9EE, 'M', u'開'),\n    (0x2F9EF, 'M', u'䦕'),\n    (0x2F9F0, 'M', u'閷'),\n    (0x2F9F1, 'M', u'𨵷'),\n    ]\n\ndef _seg_79():\n    return [\n    (0x2F9F2, 'M', u'䧦'),\n    (0x2F9F3, 'M', u'雃'),\n    (0x2F9F4, 'M', u'嶲'),\n    (0x2F9F5, 'M', u'霣'),\n    (0x2F9F6, 'M', u'𩅅'),\n    (0x2F9F7, 'M', u'𩈚'),\n    (0x2F9F8, 'M', u'䩮'),\n    (0x2F9F9, 'M', u'䩶'),\n    (0x2F9FA, 'M', u'韠'),\n    (0x2F9FB, 'M', u'𩐊'),\n    (0x2F9FC, 'M', u'䪲'),\n    (0x2F9FD, 'M', u'𩒖'),\n    (0x2F9FE, 'M', u'頋'),\n    (0x2FA00, 'M', u'頩'),\n    (0x2FA01, 'M', u'𩖶'),\n    (0x2FA02, 'M', u'飢'),\n    (0x2FA03, 'M', u'䬳'),\n    (0x2FA04, 'M', u'餩'),\n    (0x2FA05, 'M', u'馧'),\n    (0x2FA06, 'M', u'駂'),\n    (0x2FA07, 'M', u'駾'),\n    (0x2FA08, 'M', u'䯎'),\n    (0x2FA09, 'M', u'𩬰'),\n    (0x2FA0A, 'M', u'鬒'),\n    (0x2FA0B, 'M', u'鱀'),\n    (0x2FA0C, 'M', u'鳽'),\n    (0x2FA0D, 'M', u'䳎'),\n    (0x2FA0E, 'M', u'䳭'),\n    (0x2FA0F, 'M', u'鵧'),\n    (0x2FA10, 'M', u'𪃎'),\n    (0x2FA11, 'M', u'䳸'),\n    (0x2FA12, 'M', u'𪄅'),\n    (0x2FA13, 'M', u'𪈎'),\n    (0x2FA14, 'M', u'𪊑'),\n    (0x2FA15, 'M', u'麻'),\n    (0x2FA16, 'M', u'䵖'),\n    (0x2FA17, 'M', u'黹'),\n    (0x2FA18, 'M', u'黾'),\n    (0x2FA19, 'M', u'鼅'),\n    (0x2FA1A, 'M', u'鼏'),\n    (0x2FA1B, 'M', u'鼖'),\n    (0x2FA1C, 'M', u'鼻'),\n    (0x2FA1D, 'M', u'𪘀'),\n    (0x2FA1E, 'X'),\n    (0x30000, 'V'),\n    (0x3134B, 'X'),\n    (0xE0100, 'I'),\n    (0xE01F0, 'X'),\n    ]\n\nuts46data = tuple(\n    _seg_0()\n    + _seg_1()\n    + _seg_2()\n    + _seg_3()\n    + _seg_4()\n    + _seg_5()\n    + _seg_6()\n    + _seg_7()\n    + _seg_8()\n    + _seg_9()\n    + _seg_10()\n    + _seg_11()\n    + _seg_12()\n    + _seg_13()\n    + _seg_14()\n    + _seg_15()\n    + _seg_16()\n    + _seg_17()\n    + _seg_18()\n    + _seg_19()\n    + _seg_20()\n    + _seg_21()\n    + _seg_22()\n    + _seg_23()\n    + _seg_24()\n    + _seg_25()\n    + _seg_26()\n    + _seg_27()\n    + _seg_28()\n    + _seg_29()\n    + _seg_30()\n    + _seg_31()\n    + _seg_32()\n    + _seg_33()\n    + _seg_34()\n    + _seg_35()\n    + _seg_36()\n    + _seg_37()\n    + _seg_38()\n    + _seg_39()\n    + _seg_40()\n    + _seg_41()\n    + _seg_42()\n    + _seg_43()\n    + _seg_44()\n    + _seg_45()\n    + _seg_46()\n    + _seg_47()\n    + _seg_48()\n    + _seg_49()\n    + _seg_50()\n    + _seg_51()\n    + _seg_52()\n    + _seg_53()\n    + _seg_54()\n    + _seg_55()\n    + _seg_56()\n    + _seg_57()\n    + _seg_58()\n    + _seg_59()\n    + _seg_60()\n    + _seg_61()\n    + _seg_62()\n    + _seg_63()\n    + _seg_64()\n    + _seg_65()\n    + _seg_66()\n    + _seg_67()\n    + _seg_68()\n    + _seg_69()\n    + _seg_70()\n    + _seg_71()\n    + _seg_72()\n    + _seg_73()\n    + _seg_74()\n    + _seg_75()\n    + _seg_76()\n    + _seg_77()\n    + _seg_78()\n    + _seg_79()\n)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/__init__.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"An editorial interchange format and library.\n\nsee: http://opentimeline.io\n\n.. moduleauthor:: Pixar Animation Studios <opentimelineio@pixar.com>\n\"\"\"\n\n# flake8: noqa\n\n# in dependency hierarchy\nfrom . import (\n    opentime,\n    exceptions,\n    core,\n    schema,\n    schemadef,\n    plugins,\n    media_linker,\n    adapters,\n    hooks,\n    algorithms,\n)\n\n__version__ = \"0.11.0\"\n__author__ = \"Pixar Animation Studios\"\n__author_email__ = \"opentimelineio@pixar.com\"\n__license__ = \"Modified Apache 2.0 License\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/adapters/__init__.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Expose the adapter interface to developers.\n\nTo read from an existing representation, use the read_from_string and\nread_from_file functions.  To query the list of adapters, use the\navailable_adapter_names function.\n\nThe otio_json adapter is provided as a the canonical, lossless, serialization\nof the in-memory otio schema.  Other adapters are to varying degrees lossy.\nFor more information, consult the documentation in the individual adapter\nmodules.\n\"\"\"\n\nimport os\nimport itertools\n\nfrom .. import (\n    exceptions,\n    plugins,\n    media_linker\n)\n\nfrom .adapter import Adapter  # noqa\n\n# OTIO Json adapter is always available\nfrom . import otio_json # noqa\n\n\ndef suffixes_with_defined_adapters(read=False, write=False):\n    \"\"\"Return a set of all the suffixes that have adapters defined for them.\"\"\"\n\n    if not read and not write:\n        read = True\n        write = True\n\n    positive_adapters = []\n    for adp in plugins.ActiveManifest().adapters:\n        if read and adp.has_feature(\"read\"):\n            positive_adapters.append(adp)\n            continue\n\n        if write and adp.has_feature(\"write\"):\n            positive_adapters.append(adp)\n\n    return set(\n        itertools.chain.from_iterable(\n            adp.suffixes for adp in positive_adapters\n        )\n    )\n\n\ndef available_adapter_names():\n    \"\"\"Return a string list of the available adapters.\"\"\"\n\n    return [str(adp.name) for adp in plugins.ActiveManifest().adapters]\n\n\ndef _from_filepath_or_name(filepath, adapter_name):\n    if adapter_name is not None:\n        return plugins.ActiveManifest().from_name(adapter_name)\n    else:\n        return from_filepath(filepath)\n\n\ndef from_filepath(filepath):\n    \"\"\"Guess the adapter object to use for a given filepath.\n\n    example:\n        \"foo.otio\" returns the \"otio_json\" adapter.\n    \"\"\"\n\n    outext = os.path.splitext(filepath)[1][1:]\n\n    try:\n        return plugins.ActiveManifest().from_filepath(outext)\n    except exceptions.NoKnownAdapterForExtensionError:\n        raise exceptions.NoKnownAdapterForExtensionError(\n            \"No adapter for suffix '{}' on file '{}'\".format(\n                outext,\n                filepath\n            )\n        )\n\n\ndef from_name(name):\n    \"\"\"Fetch the adapter object by the name of the adapter directly.\"\"\"\n\n    try:\n        return plugins.ActiveManifest().from_name(name)\n    except exceptions.NotSupportedError:\n        raise exceptions.NotSupportedError(\n            \"adapter not supported: {}, available: {}\".format(\n                name,\n                available_adapter_names()\n            )\n        )\n\n\ndef read_from_file(\n    filepath,\n    adapter_name=None,\n    media_linker_name=media_linker.MediaLinkingPolicy.ForceDefaultLinker,\n    media_linker_argument_map=None,\n    **adapter_argument_map\n):\n    \"\"\"Read filepath using adapter_name.\n\n    If adapter_name is None, try and infer the adapter name from the filepath.\n\n    For example:\n        timeline = read_from_file(\"example_trailer.otio\")\n        timeline = read_from_file(\"file_with_no_extension\", \"cmx_3600\")\n    \"\"\"\n\n    adapter = _from_filepath_or_name(filepath, adapter_name)\n\n    return adapter.read_from_file(\n        filepath=filepath,\n        media_linker_name=media_linker_name,\n        media_linker_argument_map=media_linker_argument_map,\n        **adapter_argument_map\n    )\n\n\ndef read_from_string(\n    input_str,\n    adapter_name='otio_json',\n    media_linker_name=media_linker.MediaLinkingPolicy.ForceDefaultLinker,\n    media_linker_argument_map=None,\n    **adapter_argument_map\n):\n    \"\"\"Read a timeline from input_str using adapter_name.\n\n    This is useful if you obtain a timeline from someplace other than the\n    filesystem.\n\n    Example:\n        raw_text = urlopen(my_url).read()\n        timeline = read_from_string(raw_text, \"otio_json\")\n    \"\"\"\n\n    adapter = plugins.ActiveManifest().from_name(adapter_name)\n    return adapter.read_from_string(\n        input_str=input_str,\n        media_linker_name=media_linker_name,\n        media_linker_argument_map=media_linker_argument_map,\n        **adapter_argument_map\n    )\n\n\ndef write_to_file(\n    input_otio,\n    filepath,\n    adapter_name=None,\n    **adapter_argument_map\n):\n    \"\"\"Write input_otio to filepath using adapter_name.\n\n    If adapter_name is None, infer the adapter_name to use based on the\n    filepath.\n\n    Example:\n        otio.adapters.write_to_file(my_timeline, \"output.otio\")\n    \"\"\"\n\n    adapter = _from_filepath_or_name(filepath, adapter_name)\n\n    return adapter.write_to_file(\n        input_otio=input_otio,\n        filepath=filepath,\n        **adapter_argument_map\n    )\n\n\ndef write_to_string(\n    input_otio,\n    adapter_name='otio_json',\n    **adapter_argument_map\n):\n    \"\"\"Return input_otio written to a string using adapter_name.\n\n    Example:\n        raw_text = otio.adapters.write_to_string(my_timeline, \"otio_json\")\n    \"\"\"\n\n    adapter = plugins.ActiveManifest().from_name(adapter_name)\n    return adapter.write_to_string(\n        input_otio=input_otio,\n        **adapter_argument_map\n    )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/adapters/adapter.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Implementation of the OTIO internal `Adapter` system.\n\nFor information on writing adapters, please consult:\n    https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html# # noqa\n\"\"\"\n\nfrom .. import (\n    core,\n    plugins,\n    media_linker,\n    hooks,\n)\n\n\n@core.register_type\nclass Adapter(plugins.PythonPlugin):\n    \"\"\"Adapters convert between OTIO and other formats.\n\n    Note that this class is not subclassed by adapters.  Rather, an adapter is\n    a python module that implements at least one of the following functions:\n\n        write_to_string(input_otio)\n        write_to_file(input_otio, filepath) (optionally inferred)\n        read_from_string(input_str)\n        read_from_file(filepath) (optionally inferred)\n\n    ...as well as a small json file that advertises the features of the adapter\n    to OTIO.  This class serves as the wrapper around these modules internal\n    to OTIO.  You should not need to extend this class to create new adapters\n    for OTIO.\n\n    For more information:\n    https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html# # noqa\n    \"\"\"\n    _serializable_label = \"Adapter.1\"\n\n    def __init__(\n        self,\n        name=None,\n        execution_scope=None,\n        filepath=None,\n        suffixes=None\n    ):\n        plugins.PythonPlugin.__init__(\n            self,\n            name,\n            execution_scope,\n            filepath\n        )\n\n        self.suffixes = suffixes or []\n\n    suffixes = core.serializable_field(\n        \"suffixes\",\n        type([]),\n        doc=\"File suffixes associated with this adapter.\"\n    )\n\n    def has_feature(self, feature_string):\n        \"\"\"\n        return true if adapter supports feature_string, which must be a key\n        of the _FEATURE_MAP dictionary.\n\n        Will trigger a call to self.module(), which imports the plugin.\n        \"\"\"\n\n        if feature_string.lower() not in _FEATURE_MAP:\n            return False\n\n        search_strs = _FEATURE_MAP[feature_string]\n\n        try:\n            return any(hasattr(self.module(), s) for s in search_strs)\n        except ImportError:\n            # @TODO: should issue a warning that the plugin was not importable?\n            return False\n\n    def read_from_file(\n        self,\n        filepath,\n        media_linker_name=media_linker.MediaLinkingPolicy.ForceDefaultLinker,\n        media_linker_argument_map=None,\n        hook_function_argument_map={},\n        **adapter_argument_map\n    ):\n        \"\"\"Execute the read_from_file function on this adapter.\n\n        If read_from_string exists, but not read_from_file, execute that with\n        a trivial file object wrapper.\n        \"\"\"\n\n        if media_linker_argument_map is None:\n            media_linker_argument_map = {}\n\n        result = None\n\n        if (\n            not self.has_feature(\"read_from_file\") and\n            self.has_feature(\"read_from_string\")\n        ):\n            with open(filepath, 'r') as fo:\n                contents = fo.read()\n            result = self._execute_function(\n                \"read_from_string\",\n                input_str=contents,\n                **adapter_argument_map\n            )\n        else:\n            result = self._execute_function(\n                \"read_from_file\",\n                filepath=filepath,\n                **adapter_argument_map\n            )\n\n        hook_function_argument_map['adapter_arguments'] = adapter_argument_map\n        hook_function_argument_map['media_linker_argument_map'] = \\\n            media_linker_argument_map\n        result = hooks.run(\"post_adapter_read\", result,\n                           extra_args=hook_function_argument_map)\n\n        if media_linker_name and (\n            media_linker_name != media_linker.MediaLinkingPolicy.DoNotLinkMedia\n        ):\n            _with_linked_media_references(\n                result,\n                media_linker_name,\n                media_linker_argument_map\n            )\n\n        result = hooks.run(\"post_media_linker\", result,\n                           extra_args=media_linker_argument_map)\n\n        return result\n\n    def write_to_file(\n        self,\n        input_otio,\n        filepath,\n        hook_function_argument_map={},\n        **adapter_argument_map\n    ):\n        \"\"\"Execute the write_to_file function on this adapter.\n\n        If write_to_string exists, but not write_to_file, execute that with\n        a trivial file object wrapper.\n        \"\"\"\n        hook_function_argument_map['adapter_arguments'] = adapter_argument_map\n        input_otio = hooks.run(\"pre_adapter_write\", input_otio,\n                               extra_args=hook_function_argument_map)\n\n        if (\n            not self.has_feature(\"write_to_file\") and\n            self.has_feature(\"write_to_string\")\n        ):\n            result = self.write_to_string(input_otio, **adapter_argument_map)\n            with open(filepath, 'w') as fo:\n                fo.write(result)\n            return filepath\n\n        return self._execute_function(\n            \"write_to_file\",\n            input_otio=input_otio,\n            filepath=filepath,\n            **adapter_argument_map\n        )\n\n    def read_from_string(\n        self,\n        input_str,\n        media_linker_name=media_linker.MediaLinkingPolicy.ForceDefaultLinker,\n        media_linker_argument_map=None,\n        hook_function_argument_map={},\n        **adapter_argument_map\n    ):\n        \"\"\"Call the read_from_string function on this adapter.\"\"\"\n\n        result = self._execute_function(\n            \"read_from_string\",\n            input_str=input_str,\n            **adapter_argument_map\n        )\n        hook_function_argument_map['adapter_arguments'] = adapter_argument_map\n        hook_function_argument_map['media_linker_argument_map'] = \\\n            media_linker_argument_map\n\n        result = hooks.run(\"post_adapter_read\", result,\n                           extra_args=hook_function_argument_map)\n\n        if media_linker_name and (\n            media_linker_name != media_linker.MediaLinkingPolicy.DoNotLinkMedia\n        ):\n            _with_linked_media_references(\n                result,\n                media_linker_name,\n                media_linker_argument_map\n            )\n\n        # @TODO: Should this run *ONLY* if the media linker ran?\n        result = hooks.run(\"post_media_linker\", result,\n                           extra_args=hook_function_argument_map)\n\n        return result\n\n    def write_to_string(\n        self,\n        input_otio,\n        hook_function_argument_map={},\n        **adapter_argument_map\n    ):\n        \"\"\"Call the write_to_string function on this adapter.\"\"\"\n\n        hook_function_argument_map['adapter_arguments'] = adapter_argument_map\n        input_otio = hooks.run(\"pre_adapter_write\", input_otio,\n                               extra_args=hook_function_argument_map)\n\n        return self._execute_function(\n            \"write_to_string\",\n            input_otio=input_otio,\n            **adapter_argument_map\n        )\n\n    def __str__(self):\n        return (\n            \"Adapter(\"\n            \"{}, \"\n            \"{}, \"\n            \"{}, \"\n            \"{}\"\n            \")\".format(\n                repr(self.name),\n                repr(self.execution_scope),\n                repr(self.filepath),\n                repr(self.suffixes),\n            )\n        )\n\n    def __repr__(self):\n        return (\n            \"otio.adapter.Adapter(\"\n            \"name={}, \"\n            \"execution_scope={}, \"\n            \"filepath={}, \"\n            \"suffixes={}\"\n            \")\".format(\n                repr(self.name),\n                repr(self.execution_scope),\n                repr(self.filepath),\n                repr(self.suffixes),\n            )\n        )\n\n\ndef _with_linked_media_references(\n    read_otio,\n    media_linker_name,\n    media_linker_argument_map\n):\n    \"\"\"Link media references in the read_otio if possible.\n\n    Makes changes in place and returns the read_otio structure back.\n    \"\"\"\n\n    if not read_otio or not media_linker.from_name(media_linker_name):\n        return read_otio\n\n    # not every object the adapter reads has an \"each_clip\" method, so this\n    # skips objects without one.\n    clpfn = getattr(read_otio, \"each_clip\", None)\n    if clpfn is None:\n        return read_otio\n\n    for cl in read_otio.each_clip():\n        new_mr = media_linker.linked_media_reference(\n            cl,\n            media_linker_name,\n            # @TODO: should any context get wired in at this point?\n            media_linker_argument_map\n        )\n        if new_mr is not None:\n            cl.media_reference = new_mr\n\n    return read_otio\n\n\n# map of attr to look for vs feature name in the adapter plugin\n_FEATURE_MAP = {\n    'read_from_file': ['read_from_file'],\n    'read_from_string': ['read_from_string'],\n    'read': ['read_from_file', 'read_from_string'],\n    'write_to_file': ['write_to_file'],\n    'write_to_string': ['write_to_string'],\n    'write': ['write_to_file', 'write_to_string']\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/adapters/builtin_adapters.plugin_manifest.json",
    "content": "{\n    \"OTIO_SCHEMA\" : \"PluginManifest.1\",\n    \"adapters\": [\n        {\n            \"OTIO_SCHEMA\": \"Adapter.1\",\n            \"name\": \"fcp_xml\",\n            \"execution_scope\": \"in process\",\n            \"filepath\": \"fcp_xml.py\",\n            \"suffixes\": [\"xml\"]\n        },\n        {\n            \"OTIO_SCHEMA\" : \"Adapter.1\",\n            \"name\" : \"otio_json\",\n            \"execution_scope\" : \"in process\",\n            \"filepath\" : \"otio_json.py\",\n            \"suffixes\" : [\"otio\"]\n        },\n        {\n            \"OTIO_SCHEMA\" : \"Adapter.1\",\n            \"name\" : \"cmx_3600\",\n            \"execution_scope\" : \"in process\",\n            \"filepath\" : \"cmx_3600.py\",\n            \"suffixes\" : [\"edl\"]\n        }\n    ],\n    \"hooks\": {\n        \"post_adapter_read\" : [],\n        \"post_media_linker\" : [],\n        \"pre_adapter_write\" : []\n    }\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/adapters/cmx_3600.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"OpenTimelineIO CMX 3600 EDL Adapter\"\"\"\n\n# Note: this adapter is not an ideal model for new adapters, but it works.\n# If you want to write your own adapter, please see:\n# https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html#\n\n# TODO: Flesh out Attribute Handler\n# TODO: Add line numbers to errors and warnings\n# TODO: currently tracks with linked audio/video will lose their linkage when\n#       read into OTIO.\n\nimport os\nimport re\nimport math\nimport collections\n\nfrom .. import (\n    exceptions,\n    schema,\n    opentime,\n)\n\n\nclass EDLParseError(exceptions.OTIOError):\n    pass\n\n\n# regex for parsing the playback speed of an M2 event\nSPEED_EFFECT_RE = re.compile(\n    r\"(?P<name>.*?)\\s*(?P<speed>[0-9\\.]*)\\s*(?P<tc>[0-9:]{11})$\"\n)\n\n\n# these are all CMX_3600 transition codes\n# the wipe is written in regex format because it is W### where the ### is\n# a 'wipe code'\n# @TODO: not currently read by the transition code\ntransition_regex_map = {\n    'C': 'cut',\n    'D': 'dissolve',\n    r'W\\d{3}': 'wipe',\n    'KB': 'key_background',\n    'K': 'key_foreground',\n    'KO': 'key_overlay'\n}\n\n# CMX_3600 supports some shorthand for channel assignments\n# We name the actual tracks V and A1,A2,A3,etc.\n# This channel_map tells you which track to use for each channel shorthand.\n# Channels not listed here are used as track names verbatim.\nchannel_map = {\n    'A': ['A1'],\n    'AA': ['A1', 'A2'],\n    'B': ['V', 'A1'],\n    'A2/V': ['V', 'A2'],\n    'AA/V': ['V', 'A1', 'A2']\n}\n\n\n# Currently, the 'style' argument determines\n# the comment string for the media reference:\n#   'avid': '* FROM CLIP:' (default)\n#   'nucoda': '* FROM FILE:'\n# When adding a new style, please be sure to add sufficient tests\n# to verify both the new and existing styles.\nVALID_EDL_STYLES = ['avid', 'nucoda']\n\n\nclass EDLParser(object):\n    def __init__(self, edl_string, rate=24, ignore_timecode_mismatch=False):\n        self.timeline = schema.Timeline()\n\n        # Start with no tracks. They will be added as we encounter them.\n        # This dict maps a track name (e.g \"A2\" or \"V\") to an OTIO Track.\n        self.tracks_by_name = {}\n\n        self.ignore_timecode_mismatch = ignore_timecode_mismatch\n\n        self.parse_edl(edl_string, rate=rate)\n\n        # TODO: Sort the tracks V, then A1,A2,etc.\n\n    def add_clip(self, line, comments, rate=24):\n        comment_handler = CommentHandler(comments)\n        clip_handler = ClipHandler(line, comment_handler.handled, rate=rate)\n        clip = clip_handler.clip\n        if comment_handler.unhandled:\n            clip.metadata.setdefault(\"cmx_3600\", {})\n            clip.metadata['cmx_3600'].setdefault(\"comments\", [])\n            clip.metadata['cmx_3600']['comments'] += (\n                comment_handler.unhandled\n            )\n\n        # Add reel name to metadata\n        # A reel name of `AX` represents an unknown or auxilary source\n        # We don't currently track these sources outside of this adapter\n        # So lets skip adding AX reels as metadata for now,\n        # as that would dirty json outputs with non-relevant information\n        if clip_handler.reel and clip_handler.reel != 'AX':\n            clip.metadata.setdefault(\"cmx_3600\", {})\n            clip.metadata['cmx_3600']['reel'] = clip_handler.reel\n\n        # each edit point between two clips is a transition. the default is a\n        # cut in the edl format the transition codes are for the transition\n        # into the clip\n        self.add_transition(\n            clip_handler,\n            clip_handler.transition_type,\n            clip_handler.transition_data\n        )\n\n        tracks = self.tracks_for_channel(clip_handler.channel_code)\n        for track in tracks:\n\n            edl_rate = clip_handler.edl_rate\n            record_in = opentime.from_timecode(\n                clip_handler.record_tc_in,\n                edl_rate\n            )\n            record_out = opentime.from_timecode(\n                clip_handler.record_tc_out,\n                edl_rate\n            )\n\n            src_duration = clip.duration()\n            rec_duration = record_out - record_in\n            if rec_duration != src_duration:\n                motion = comment_handler.handled.get('motion_effect')\n                freeze = comment_handler.handled.get('freeze_frame')\n                if motion is not None or freeze is not None:\n                    # Adjust the clip to match the record duration\n                    clip.source_range = opentime.TimeRange(\n                        start_time=clip.source_range.start_time,\n                        duration=rec_duration\n                    )\n\n                    if freeze is not None:\n                        clip.effects.append(schema.FreezeFrame())\n                        # XXX remove 'FF' suffix (writing edl will add it back)\n                        if clip.name.endswith(' FF'):\n                            clip.name = clip.name[:-3]\n                    elif motion is not None:\n                        fps = float(\n                            SPEED_EFFECT_RE.match(motion).group(\"speed\")\n                        )\n                        time_scalar = fps / rate\n                        clip.effects.append(\n                            schema.LinearTimeWarp(time_scalar=time_scalar)\n                        )\n\n                elif self.ignore_timecode_mismatch:\n                    # Pretend there was no problem by adjusting the record_out.\n                    # Note that we don't actually use record_out after this\n                    # point in the code, since all of the subsequent math uses\n                    # the clip's source_range. Adjusting the record_out is\n                    # just to document what the implications of ignoring the\n                    # mismatch here entails.\n                    record_out = record_in + src_duration\n\n                else:\n                    raise EDLParseError(\n                        \"Source and record duration don't match: {} != {}\"\n                        \" for clip {}\".format(\n                            src_duration,\n                            rec_duration,\n                            clip.name\n                        ))\n\n            if track.source_range is None:\n                zero = opentime.RationalTime(0, edl_rate)\n                track.source_range = opentime.TimeRange(\n                    start_time=zero - record_in,\n                    duration=zero\n                )\n\n            track_end = track.duration() - track.source_range.start_time\n            if record_in < track_end:\n                if self.ignore_timecode_mismatch:\n                    # shift it over\n                    record_in = track_end\n                    record_out = record_in + rec_duration\n                else:\n                    raise EDLParseError(\n                        \"Overlapping record in value: {} for clip {}\".format(\n                            clip_handler.record_tc_in,\n                            clip.name\n                        ))\n\n            # If the next clip is supposed to start beyond the end of the\n            # clips we've accumulated so far, then we need to add a Gap\n            # to fill that space. This can happen when an EDL has record\n            # timecodes that are sparse (e.g. from a single track of a\n            # multi-track composition).\n            if record_in > track_end and len(track) > 0:\n                gap = schema.Gap()\n                gap.source_range = opentime.TimeRange(\n                    start_time=opentime.RationalTime(0, edl_rate),\n                    duration=record_in - track_end\n                )\n                track.append(gap)\n                track.source_range = opentime.TimeRange(\n                    start_time=track.source_range.start_time,\n                    duration=track.source_range.duration + gap.duration()\n                )\n\n            track.append(clip)\n            track.source_range = opentime.TimeRange(\n                start_time=track.source_range.start_time,\n                duration=track.source_range.duration + clip.duration()\n            )\n\n    def guess_kind_for_track_name(self, name):\n        if name.startswith(\"V\"):\n            return schema.TrackKind.Video\n        if name.startswith(\"A\"):\n            return schema.TrackKind.Audio\n        return schema.TrackKind.Video\n\n    def tracks_for_channel(self, channel_code):\n        # Expand channel shorthand into a list of track names.\n        if channel_code in channel_map:\n            track_names = channel_map[channel_code]\n        else:\n            track_names = [channel_code]\n\n        # Create any channels we don't already have\n        for track_name in track_names:\n            if track_name not in self.tracks_by_name:\n                track = schema.Track(\n                    name=track_name,\n                    kind=self.guess_kind_for_track_name(track_name)\n                )\n                self.tracks_by_name[track_name] = track\n                self.timeline.tracks.append(track)\n\n        # Return a list of actual tracks\n        return [self.tracks_by_name[c] for c in track_names]\n\n    def add_transition(self, clip_handler, transition, data):\n        if transition not in ['C']:\n            md = clip_handler.clip.metadata.setdefault(\"cmx_3600\", {})\n            md[\"transition\"] = transition\n\n    def parse_edl(self, edl_string, rate=24):\n        # edl 'events' can be comprised of an indeterminate amount of lines\n        # we are to translating 'events' to a single clip and transition\n        # then we add the transition and the clip to all channels the 'event'\n        # channel code is mapped to the transition given in the 'event'\n        # precedes the clip\n\n        # remove all blank lines from the edl\n        edl_lines = [\n            l for l in (l.strip() for l in edl_string.splitlines()) if l\n        ]\n\n        while edl_lines:\n            # a basic for loop wont work cleanly since we need to look ahead at\n            # array elements to determine what type of 'event' we are looking\n            # at\n            line = edl_lines.pop(0)\n\n            if line.startswith('TITLE:'):\n                # this is the first line of interest in an edl\n                # it is required to be in the header\n                self.timeline.name = line.replace('TITLE:', '').strip()\n\n            elif line.startswith('FCM'):\n                # this can occur either in the header or before any 'event'\n                # in both cases we can ignore it since it is meant for tape\n                # timecode\n                pass\n\n            elif line.startswith('SPLIT'):\n                # this is the only comment preceding an 'event' that we care\n                # about in our context it simply means the next two clips will\n                # have the same comment data it is for reading purposes only\n                audio_delay = None\n                video_delay = None\n\n                if 'AUDIO DELAY' in line:\n                    audio_delay = line.split()[-1].strip()\n                if 'VIDEO DELAY' in line:\n                    video_delay = line.split()[-1].strip()\n                if audio_delay and video_delay:\n                    raise EDLParseError(\n                        'both audio and video delay declared after SPLIT.'\n                    )\n                if not (audio_delay or video_delay):\n                    raise EDLParseError(\n                        'either audio or video delay declared after SPLIT.'\n                    )\n\n                line_1 = edl_lines.pop(0)\n                line_2 = edl_lines.pop(0)\n\n                comments = []\n                while edl_lines:\n                    if re.match(r'^\\D', edl_lines[0]):\n                        comments.append(edl_lines.pop(0))\n                    else:\n                        break\n                self.add_clip(line_1, comments, rate=rate)\n                self.add_clip(line_2, comments, rate=rate)\n\n            elif line[0].isdigit():\n                # all 'events' start_time with an edit decision. this is\n                # denoted by the line beginning with a padded integer 000-999\n                comments = []\n                while edl_lines:\n                    # any non-numbered lines after an edit decision should be\n                    # treated as 'comments'\n                    # comments are string tags used by the reader to get extra\n                    # information not able to be found in the restricted edl\n                    # format\n                    if re.match(r'^\\D', edl_lines[0]):\n                        comments.append(edl_lines.pop(0))\n                    else:\n                        break\n\n                self.add_clip(line, comments, rate=rate)\n\n            else:\n                raise EDLParseError('Unknown event type')\n\n        for track in self.timeline.tracks:\n            # if the source_range is the same as the available_range\n            # then we don't need to set it at all.\n            if track.source_range == track.available_range():\n                track.source_range = None\n\n\nclass ClipHandler(object):\n\n    def __init__(self, line, comment_data, rate=24):\n        self.clip_num = None\n        self.reel = None\n        self.channel_code = None\n        self.edl_rate = rate\n        self.transition_id = None\n        self.transition_data = None\n        self.source_tc_in = None\n        self.source_tc_out = None\n        self.record_tc_in = None\n        self.record_tc_out = None\n\n        self.parse(line)\n        self.clip = self.make_clip(comment_data)\n\n    def make_clip(self, comment_data):\n        clip = schema.Clip()\n        clip.name = str(self.clip_num)\n\n        # BLACK/BL and BARS are called out as \"Special Source Identifiers\" in\n        # the documents referenced here:\n        # https://github.com/PixarAnimationStudios/OpenTimelineIO#cmx3600-edl\n        if self.reel in ['BL', 'BLACK']:\n            clip.media_reference = schema.GeneratorReference()\n            # TODO: Replace with enum, once one exists\n            clip.media_reference.generator_kind = 'black'\n        elif self.reel == 'BARS':\n            clip.media_reference = schema.GeneratorReference()\n            # TODO: Replace with enum, once one exists\n            clip.media_reference.generator_kind = 'SMPTEBars'\n        elif 'media_reference' in comment_data:\n            clip.media_reference = schema.ExternalReference()\n            clip.media_reference.target_url = comment_data[\n                'media_reference'\n            ]\n        else:\n            clip.media_reference = schema.MissingReference()\n\n        # this could currently break without a 'FROM CLIP' comment.\n        # Without that there is no 'media_reference' Do we have a default\n        # clip name?\n        if 'clip_name' in comment_data:\n            clip.name = comment_data[\"clip_name\"]\n        elif (\n            clip.media_reference and\n            hasattr(clip.media_reference, 'target_url') and\n            clip.media_reference.target_url is not None\n        ):\n            clip.name = os.path.splitext(\n                os.path.basename(clip.media_reference.target_url)\n            )[0]\n\n        asc_sop = comment_data.get('asc_sop', None)\n        asc_sat = comment_data.get('asc_sat', None)\n        if asc_sop or asc_sat:\n            slope = (1, 1, 1)\n            offset = (0, 0, 0)\n            power = (1, 1, 1)\n            sat = 1.0\n\n            if asc_sop:\n                triple = r'([-+]?[\\d.]+) ([-+]?[\\d.]+) ([-+]?[\\d.]+)'\n                m = re.match(\n                    r'\\('\n                    + triple\n                    + r'\\)\\s*\\('\n                    + triple + r'\\)\\s*\\('\n                    + triple + r'\\)',\n                    asc_sop\n                )\n                if m:\n                    floats = [float(g) for g in m.groups()]\n                    slope = [floats[0], floats[1], floats[2]]\n                    offset = [floats[3], floats[4], floats[5]]\n                    power = [floats[6], floats[7], floats[8]]\n                else:\n                    raise EDLParseError(\n                        'Invalid ASC_SOP found: {}'.format(asc_sop))\n\n            if asc_sat:\n                sat = float(asc_sat)\n\n            clip.metadata['cdl'] = {\n                'asc_sat': sat,\n                'asc_sop': {\n                    'slope': slope,\n                    'offset': offset,\n                    'power': power\n                }\n            }\n\n        if 'locator' in comment_data:\n            # An example EDL locator line looks like this:\n            # * LOC: 01:00:01:14 RED     ANIM FIX NEEDED\n            # We get the part after \"LOC: \" as the comment_data entry\n            # Given the fixed-width nature of these, we could be more\n            # strict about the field widths, but there are many\n            # variations of EDL, so if we are lenient then maybe we\n            # can handle more of them? Only real-world testing will\n            # determine this for sure...\n            m = re.match(\n                r'(\\d\\d:\\d\\d:\\d\\d:\\d\\d)\\s+(\\w*)\\s+(.*)',\n                comment_data[\"locator\"]\n            )\n            if m:\n                marker = schema.Marker()\n                marker.marked_range = opentime.TimeRange(\n                    start_time=opentime.from_timecode(\n                        m.group(1),\n                        self.edl_rate\n                    ),\n                    duration=opentime.RationalTime()\n                )\n\n                # always write the source value into metadata, in case it\n                # is not a valid enum somehow.\n                color_parsed_from_file = m.group(2)\n\n                marker.metadata = {\n                    \"cmx_3600\": {\n                        \"color\": color_parsed_from_file\n                    }\n                }\n\n                # @TODO: if it is a valid\n                if hasattr(\n                    schema.MarkerColor,\n                    color_parsed_from_file.upper()\n                ):\n                    marker.color = color_parsed_from_file.upper()\n                else:\n                    marker.color = schema.MarkerColor.RED\n\n                marker.name = m.group(3)\n                clip.markers.append(marker)\n            else:\n                # TODO: Should we report this as a warning somehow?\n                pass\n\n        clip.source_range = opentime.range_from_start_end_time(\n            opentime.from_timecode(self.source_tc_in, self.edl_rate),\n            opentime.from_timecode(self.source_tc_out, self.edl_rate)\n        )\n\n        return clip\n\n    def parse(self, line):\n        fields = tuple(e.strip() for e in line.split() if e.strip())\n        field_count = len(fields)\n\n        if field_count == 9:\n            # has transition data\n            # this is for edits with timing or other needed info\n            # transition data for D and W*** transitions is a n integer that\n            # denotes frame count\n            # i haven't figured out how the key transitions (K, KB, KO) work\n            (\n                self.clip_num,\n                self.reel,\n                self.channel_code,\n                self.transition_type,\n                self.transition_data,\n                self.source_tc_in,\n                self.source_tc_out,\n                self.record_tc_in,\n                self.record_tc_out\n            ) = fields\n\n        elif field_count == 8:\n            # no transition data\n            # this is for basic cuts\n            (\n                self.clip_num,\n                self.reel,\n                self.channel_code,\n                self.transition_type,\n                self.source_tc_in,\n                self.source_tc_out,\n                self.record_tc_in,\n                self.record_tc_out\n            ) = fields\n\n        else:\n            raise EDLParseError(\n                'incorrect number of fields [{0}] in form statement: {1}'\n                ''.format(field_count, line))\n\n        # Frame numbers (not just timecode) are ok\n        for prop in [\n            'source_tc_in',\n            'source_tc_out',\n            'record_tc_in',\n            'record_tc_out'\n        ]:\n            if ':' not in getattr(self, prop):\n                setattr(\n                    self,\n                    prop,\n                    opentime.to_timecode(\n                        opentime.from_frames(\n                            int(getattr(self, prop)),\n                            self.edl_rate\n                        ),\n                        self.edl_rate\n                    )\n                )\n\n\nclass CommentHandler(object):\n    # this is the for that all comment 'id' tags take\n    regex_template = r'\\*?\\s*{id}:?\\s*(?P<comment_body>.*)'\n\n    # this should be a map of all known comments that we can read\n    # 'FROM CLIP' or 'FROM FILE' is a required comment to link media\n    # An exception is raised if both 'FROM CLIP' and 'FROM FILE' are found\n    # needs to be ordered so that FROM CLIP NAME gets matched before FROM CLIP\n    comment_id_map = collections.OrderedDict([\n        ('FROM CLIP NAME', 'clip_name'),\n        ('FROM CLIP', 'media_reference'),\n        ('FROM FILE', 'media_reference'),\n        ('LOC', 'locator'),\n        ('ASC_SOP', 'asc_sop'),\n        ('ASC_SAT', 'asc_sat'),\n        ('M2', 'motion_effect'),\n        ('\\\\* FREEZE FRAME', 'freeze_frame'),\n    ])\n\n    def __init__(self, comments):\n        self.handled = {}\n        self.unhandled = []\n        for comment in comments:\n            self.parse(comment)\n\n    def parse(self, comment):\n        for comment_id, comment_type in self.comment_id_map.items():\n            regex = self.regex_template.format(id=comment_id)\n            match = re.match(regex, comment)\n            if match:\n                self.handled[comment_type] = match.group(\n                    'comment_body'\n                ).strip()\n                break\n        else:\n            stripped = comment.lstrip('*').strip()\n            if stripped:\n                self.unhandled.append(stripped)\n\n\ndef _expand_transitions(timeline):\n    \"\"\"Convert clips with metadata/transition == 'D' into OTIO transitions.\"\"\"\n\n    tracks = timeline.tracks\n    remove_list = []\n    replace_list = []\n    append_list = []\n    for track in tracks:\n        track_iter = iter(track)\n        # avid inserts an extra clip for the source\n        prev_prev = None\n        prev = None\n        clip = next(track_iter, None)\n        next_clip = next(track_iter, None)\n        while clip is not None:\n            transition_type = clip.metadata.get('cmx_3600', {}).get(\n                'transition',\n                'C'\n            )\n\n            if transition_type == 'C':\n                # nothing to do, continue to the next iteration of the loop\n                prev_prev = prev\n                prev = clip\n                clip = next_clip\n                next_clip = next(track_iter, None)\n                continue\n            if transition_type not in ['D']:\n                raise EDLParseError(\n                    \"Transition type '{}' not supported by the CMX EDL reader \"\n                    \"currently.\".format(transition_type)\n                )\n\n            transition_duration = clip.duration()\n\n            # EDL doesn't have enough data to know where the cut point was, so\n            # this arbitrarily puts it in the middle of the transition\n            pre_cut = math.floor(transition_duration.value / 2)\n            post_cut = transition_duration.value - pre_cut\n            mid_tran_cut_pre_duration = opentime.RationalTime(\n                pre_cut,\n                transition_duration.rate\n            )\n            mid_tran_cut_post_duration = opentime.RationalTime(\n                post_cut,\n                transition_duration.rate\n            )\n\n            # expand the previous\n            expansion_clip = None\n            if prev and not prev_prev:\n                expansion_clip = prev\n            elif prev_prev:\n                expansion_clip = prev_prev\n                if prev:\n                    remove_list.append((track, prev))\n\n            sr = expansion_clip.source_range\n            expansion_clip.source_range = opentime.TimeRange(\n                start_time=sr.start_time,\n                duration=sr.duration + mid_tran_cut_pre_duration\n            )\n\n            # rebuild the clip as a transition\n            new_trx = schema.Transition(\n                name=clip.name,\n                # only supported type at the moment\n                transition_type=schema.TransitionTypes.SMPTE_Dissolve,\n                metadata=clip.metadata\n            )\n            new_trx.in_offset = mid_tran_cut_pre_duration\n            new_trx.out_offset = mid_tran_cut_post_duration\n\n            #                   in     from  to\n            replace_list.append((track, clip, new_trx))\n\n            # expand the next_clip\n            if next_clip:\n                next_clip.source_range = opentime.TimeRange(\n                    next_clip.source_range.start_time - mid_tran_cut_post_duration,\n                    next_clip.source_range.duration + mid_tran_cut_post_duration\n                )\n            else:\n                fill = schema.Gap(\n                    source_range=opentime.TimeRange(\n                        duration=mid_tran_cut_post_duration,\n                        start_time=opentime.RationalTime(\n                            0,\n                            transition_duration.rate\n                        )\n                    )\n                )\n                append_list.append((track, fill))\n\n            prev = clip\n            clip = next_clip\n            next_clip = next(track_iter, None)\n\n    for (track, from_clip, to_transition) in replace_list:\n        track[track.index(from_clip)] = to_transition\n\n    for (track, clip_to_remove) in list(set(remove_list)):\n        # if clip_to_remove in track:\n        track.remove(clip_to_remove)\n\n    for (track, clip) in append_list:\n        track.append(clip)\n\n    return timeline\n\n\ndef read_from_string(input_str, rate=24, ignore_timecode_mismatch=False):\n    \"\"\"Reads a CMX Edit Decision List (EDL) from a string.\n    Since EDLs don't contain metadata specifying the rate they are meant\n    for, you may need to specify the rate parameter (default is 24).\n    By default, read_from_string will throw an exception if it discovers\n    invalid timecode in the EDL. For example, if a clip's record timecode\n    overlaps with the previous cut. Since this is a common mistake in\n    many EDLs, you can specify ignore_timecode_mismatch=True, which will\n    supress these errors and attempt to guess at the correct record\n    timecode based on the source timecode and adjacent cuts.\n    For best results, you may wish to do something like this:\n\n    Example:\n        >>> try:\n        ...     timeline = otio.adapters.read_from_string(\"mymovie.edl\", rate=30)\n        ... except EDLParseError:\n        ...    print('Log a warning here')\n        ...    try:\n        ...        timeline = otio.adapters.read_from_string(\n        ...            \"mymovie.edl\",\n        ...            rate=30,\n        ...            ignore_timecode_mismatch=True)\n        ...    except EDLParseError:\n        ...        print('Log an error here')\n    \"\"\"\n    parser = EDLParser(\n        input_str,\n        rate=float(rate),\n        ignore_timecode_mismatch=ignore_timecode_mismatch\n    )\n    result = parser.timeline\n    result = _expand_transitions(result)\n    return result\n\n\ndef write_to_string(input_otio, rate=None, style='avid', reelname_len=8):\n    # TODO: We should have convenience functions in Timeline for this?\n    # also only works for a single video track at the moment\n\n    video_tracks = [t for t in input_otio.tracks\n                    if t.kind == schema.TrackKind.Video]\n    audio_tracks = [t for t in input_otio.tracks\n                    if t.kind == schema.TrackKind.Audio]\n\n    if len(video_tracks) != 1:\n        raise exceptions.NotSupportedError(\n            \"Only a single video track is supported, got: {}\".format(\n                len(video_tracks)\n            )\n        )\n\n    if len(audio_tracks) > 2:\n        raise exceptions.NotSupportedError(\n            \"No more than 2 audio tracks are supported.\"\n        )\n    # if audio_tracks:\n    #     raise exceptions.NotSupportedError(\n    #         \"No audio tracks are currently supported.\"\n    #     )\n\n    # TODO: We should try to detect the frame rate and output an\n    # appropriate \"FCM: NON-DROP FRAME\" etc here.\n\n    writer = EDLWriter(\n        tracks=input_otio.tracks,\n        # Assume all rates are the same as the 1st track's\n        rate=rate or input_otio.tracks[0].duration().rate,\n        style=style,\n        reelname_len=reelname_len\n    )\n\n    return writer.get_content_for_track_at_index(0, title=input_otio.name)\n\n\nclass EDLWriter(object):\n    def __init__(self, tracks, rate, style, reelname_len=8):\n        self._tracks = tracks\n        self._rate = rate\n        self._style = style\n        self._reelname_len = reelname_len\n\n        if style not in VALID_EDL_STYLES:\n            raise exceptions.NotSupportedError(\n                \"The EDL style '{}' is not supported.\".format(\n                    style\n                )\n            )\n\n    def get_content_for_track_at_index(self, idx, title):\n        track = self._tracks[idx]\n\n        # Add a gap if the last child is a transition.\n        if isinstance(track[-1], schema.Transition):\n            gap = schema.Gap(\n                source_range=opentime.TimeRange(\n                    start_time=track[-1].duration(),\n                    duration=opentime.RationalTime(0.0, self._rate)\n                )\n            )\n            track.append(gap)\n\n        # Note: Transitions in EDLs are unconventionally represented.\n        #\n        # Where a transition might normally be visualized like:\n        #            |---57.0 Trans 43.0----|\n        # |------Clip1 102.0------|----------Clip2 143.0----------|Clip3 24.0|\n        #\n        # In an EDL it can be thought of more like this:\n        #            |---0.0 Trans 100.0----|\n        # |Clip1 45.0|----------------Clip2 200.0-----------------|Clip3 24.0|\n\n        # Adjust cut points to match EDL event representation.\n        for idx, child in enumerate(track):\n            if isinstance(child, schema.Transition):\n                if idx != 0:\n                    # Shorten the a-side\n                    sr = track[idx - 1].source_range\n                    track[idx - 1].source_range = opentime.TimeRange(\n                        start_time=sr.start_time,\n                        duration=sr.duration - child.in_offset\n                    )\n\n                # Lengthen the b-side\n                sr = track[idx + 1].source_range\n                track[idx + 1].source_range = opentime.TimeRange(\n                    start_time=sr.start_time - child.in_offset,\n                    duration=sr.duration + child.in_offset\n                )\n\n                # Just clean up the transition for goodness sake\n                in_offset = child.in_offset\n                child.in_offset = opentime.RationalTime(0.0, self._rate)\n                child.out_offset += in_offset\n\n        # Group events into either simple clip/a-side or transition and b-side\n        # to match EDL edit/event representation and edit numbers.\n        events = []\n        for idx, child in enumerate(track):\n            if isinstance(child, schema.Transition):\n                # Transition will be captured in subsequent iteration.\n                continue\n\n            prv = track[idx - 1] if idx > 0 else None\n\n            if isinstance(prv, schema.Transition):\n                events.append(\n                    DissolveEvent(\n                        events[-1] if len(events) else None,\n                        prv,\n                        child,\n                        self._tracks,\n                        track.kind,\n                        self._rate,\n                        self._style,\n                        self._reelname_len\n                    )\n                )\n            elif isinstance(child, schema.Clip):\n                events.append(\n                    Event(\n                        child,\n                        self._tracks,\n                        track.kind,\n                        self._rate,\n                        self._style,\n                        self._reelname_len\n                    )\n                )\n            elif isinstance(child, schema.Gap):\n                # Gaps are represented as missing record timecode, no event\n                # needed.\n                pass\n\n        content = \"TITLE: {}\\n\\n\".format(title) if title else ''\n\n        # Convert each event/dissolve-event into plain text.\n        for idx, event in enumerate(events):\n            event.edit_number = idx + 1\n            content += event.to_edl_format() + '\\n'\n\n        return content\n\n\ndef _supported_timing_effects(clip):\n    return [\n        fx for fx in clip.effects\n        if isinstance(fx, schema.LinearTimeWarp)\n    ]\n\n\ndef _relevant_timing_effect(clip):\n    # check to see if there is more than one timing effect\n    effects = _supported_timing_effects(clip)\n\n    if effects != clip.effects:\n        for thing in clip.effects:\n            if thing not in effects and isinstance(thing, schema.TimeEffect):\n                raise exceptions.NotSupportedError(\n                    \"Clip contains timing effects not supported by the EDL\"\n                    \" adapter.\\nClip: {}\".format(str(clip)))\n\n    timing_effect = None\n    if effects:\n        timing_effect = effects[0]\n    if len(effects) > 1:\n        raise exceptions.NotSupportedError(\n            \"EDL Adapter only allows one timing effect / clip.\"\n        )\n\n    return timing_effect\n\n\nclass Event(object):\n    def __init__(\n        self,\n        clip,\n        tracks,\n        kind,\n        rate,\n        style,\n        reelname_len\n    ):\n\n        line = EventLine(kind, rate, reel=_reel_from_clip(clip, reelname_len))\n        line.source_in = clip.source_range.start_time\n        line.source_out = clip.source_range.end_time_exclusive()\n\n        timing_effect = _relevant_timing_effect(clip)\n\n        if timing_effect:\n            if timing_effect.effect_name == \"FreezeFrame\":\n                line.source_out = line.source_in + opentime.RationalTime(\n                    1,\n                    line.source_in.rate\n                )\n            elif timing_effect.effect_name == \"LinearTimeWarp\":\n                value = clip.trimmed_range().duration.value / timing_effect.time_scalar\n                line.source_out = (\n                    line.source_in + opentime.RationalTime(value, rate))\n\n        range_in_timeline = clip.transformed_time_range(\n            clip.trimmed_range(),\n            tracks\n        )\n        line.record_in = range_in_timeline.start_time\n        line.record_out = range_in_timeline.end_time_exclusive()\n        self.line = line\n\n        self.comments = _generate_comment_lines(\n            clip=clip,\n            style=style,\n            edl_rate=rate,\n            reelname_len=reelname_len,\n            from_or_to='FROM'\n        )\n\n        self.clip = clip\n        self.source_out = line.source_out\n        self.record_out = line.record_out\n        self.reel = line.reel\n\n    def __str__(self):\n        return '{type}({name})'.format(\n            type=self.clip.schema_name(),\n            name=self.clip.name\n        )\n\n    def to_edl_format(self):\n        \"\"\"\n        Example output:\n            002 AX V C        00:00:00:00 00:00:00:05 00:00:00:05 00:00:00:10\n            * FROM CLIP NAME:  test clip2\n            * FROM FILE: S:\\\\var\\\\tmp\\\\test.exr\n\n        \"\"\"\n        lines = [self.line.to_edl_format(self.edit_number)]\n        lines += self.comments if len(self.comments) else []\n\n        return \"\\n\".join(lines)\n\n\nclass DissolveEvent(object):\n\n    def __init__(\n        self,\n        a_side_event,\n        transition,\n        b_side_clip,\n        tracks,\n        kind,\n        rate,\n        style,\n        reelname_len\n    ):\n        # Note: We don't make the A-Side event line here as it is represented\n        # by its own event (edit number).\n\n        cut_line = EventLine(kind, rate)\n\n        if a_side_event:\n            cut_line.reel = a_side_event.reel\n            cut_line.source_in = a_side_event.source_out\n            cut_line.source_out = a_side_event.source_out\n            cut_line.record_in = a_side_event.record_out\n            cut_line.record_out = a_side_event.record_out\n\n            self.from_comments = _generate_comment_lines(\n                clip=a_side_event.clip,\n                style=style,\n                edl_rate=rate,\n                reelname_len=reelname_len,\n                from_or_to='FROM'\n            )\n        else:\n            cut_line.reel = 'BL'\n            cut_line.source_in = opentime.RationalTime(0.0, rate)\n            cut_line.source_out = opentime.RationalTime(0.0, rate)\n            cut_line.record_in = opentime.RationalTime(0.0, rate)\n            cut_line.record_out = opentime.RationalTime(0.0, rate)\n\n        self.cut_line = cut_line\n\n        dslve_line = EventLine(\n            kind,\n            rate,\n            reel=_reel_from_clip(b_side_clip, reelname_len)\n        )\n        dslve_line.source_in = b_side_clip.source_range.start_time\n        dslve_line.source_out = b_side_clip.source_range.end_time_exclusive()\n        range_in_timeline = b_side_clip.transformed_time_range(\n            b_side_clip.trimmed_range(),\n            tracks\n        )\n        dslve_line.record_in = range_in_timeline.start_time\n        dslve_line.record_out = range_in_timeline.end_time_exclusive()\n        dslve_line.dissolve_length = transition.out_offset\n        self.dissolve_line = dslve_line\n\n        self.to_comments = _generate_comment_lines(\n            clip=b_side_clip,\n            style=style,\n            edl_rate=rate,\n            reelname_len=reelname_len,\n            from_or_to='TO'\n        )\n\n        self.a_side_event = a_side_event\n        self.transition = transition\n        self.b_side_clip = b_side_clip\n\n        # Expose so that any subsequent dissolves can borrow their values.\n        self.clip = b_side_clip\n        self.source_out = dslve_line.source_out\n        self.record_out = dslve_line.record_out\n        self.reel = dslve_line.reel\n\n    def __str__(self):\n        a_side = self.a_side_event\n        return '{a_type}({a_name}) -> {b_type}({b_name})'.format(\n            a_type=a_side.clip.schema_name() if a_side else '',\n            a_name=a_side.clip.name if a_side else '',\n            b_type=self.b_side_clip.schema_name(),\n            b_name=self.b_side_clip.name\n        )\n\n    def to_edl_format(self):\n        \"\"\"\n        Example output:\n\n        Cross dissolve...\n        002 Clip1 V C     00:00:07:08 00:00:07:08 00:00:01:21 00:00:01:21\n        002 Clip2 V D 100 00:00:09:07 00:00:17:15 00:00:01:21 00:00:10:05\n        * FROM CLIP NAME:  Clip1\n        * FROM CLIP: /var/tmp/clip1.001.exr\n        * TO CLIP NAME:  Clip2\n        * TO CLIP: /var/tmp/clip2.001.exr\n\n        Fade in...\n        001 BL      V C     00:00:00:00 00:00:00:00 00:00:00:00 00:00:00:00\n        001 My_Clip V D 012 00:00:02:02 00:00:03:04 00:00:00:00 00:00:01:02\n        * TO CLIP NAME:  My Clip\n        * TO FILE: /var/tmp/clip.001.exr\n\n        Fade out...\n        002 My_Clip V C     00:00:01:12 00:00:01:12 00:00:00:12 00:00:00:12\n        002 BL      V D 012 00:00:00:00 00:00:00:12 00:00:00:12 00:00:01:00\n        * FROM CLIP NAME:  My Clip\n        * FROM FILE: /var/tmp/clip.001.exr\n        \"\"\"\n\n        lines = [\n            self.cut_line.to_edl_format(self.edit_number),\n            self.dissolve_line.to_edl_format(self.edit_number)\n        ]\n        lines += self.from_comments if hasattr(self, 'from_comments') else []\n        lines += self.to_comments if len(self.to_comments) else []\n\n        return \"\\n\".join(lines)\n\n\nclass EventLine(object):\n    def __init__(self, kind, rate, reel='AX'):\n        self.reel = reel\n        self._kind = 'V' if kind == schema.TrackKind.Video else 'A'\n        self._rate = rate\n\n        self.source_in = opentime.RationalTime(0.0, rate=rate)\n        self.source_out = opentime.RationalTime(0.0, rate=rate)\n        self.record_in = opentime.RationalTime(0.0, rate=rate)\n        self.record_out = opentime.RationalTime(0.0, rate=rate)\n\n        self.dissolve_length = opentime.RationalTime(0.0, rate)\n\n    def to_edl_format(self, edit_number):\n        ser = {\n            'edit': edit_number,\n            'reel': self.reel,\n            'kind': self._kind,\n            'src_in': opentime.to_timecode(self.source_in, self._rate),\n            'src_out': opentime.to_timecode(self.source_out, self._rate),\n            'rec_in': opentime.to_timecode(self.record_in, self._rate),\n            'rec_out': opentime.to_timecode(self.record_out, self._rate),\n            'diss': int(\n                opentime.to_frames(self.dissolve_length, self._rate)\n            ),\n        }\n\n        if self.is_dissolve():\n            return \"{edit:03d}  {reel:8} {kind:5} D {diss:03d}    \" \\\n                \"{src_in} {src_out} {rec_in} {rec_out}\".format(**ser)\n        else:\n            return \"{edit:03d}  {reel:8} {kind:5} C        \" \\\n                \"{src_in} {src_out} {rec_in} {rec_out}\".format(**ser)\n\n    def is_dissolve(self):\n        return self.dissolve_length.value > 0\n\n\ndef _generate_comment_lines(\n    clip,\n    style,\n    edl_rate,\n    reelname_len,\n    from_or_to='FROM'\n):\n    lines = []\n    url = None\n\n    if not clip or isinstance(clip, schema.Gap):\n        return []\n\n    suffix = ''\n    timing_effect = _relevant_timing_effect(clip)\n    if timing_effect and timing_effect.effect_name == 'FreezeFrame':\n        suffix = ' FF'\n\n    if clip.media_reference:\n        if hasattr(clip.media_reference, 'target_url'):\n            url = clip.media_reference.target_url\n\n    else:\n        url = clip.name\n\n    if from_or_to not in ['FROM', 'TO']:\n        raise exceptions.NotSupportedError(\n            \"The clip FROM or TO value '{}' is not supported.\".format(\n                from_or_to\n            )\n        )\n\n    if timing_effect and isinstance(timing_effect, schema.LinearTimeWarp):\n        lines.append(\n            'M2   {}\\t\\t{}\\t\\t\\t{}'.format(\n                clip.name,\n                timing_effect.time_scalar * edl_rate,\n                opentime.to_timecode(\n                    clip.trimmed_range().start_time,\n                    edl_rate\n                )\n            )\n        )\n\n    if clip.name:\n        # Avid Media Composer outputs two spaces before the\n        # clip name so we match that.\n        lines.append(\n            \"* {from_or_to} CLIP NAME:  {name}{suffix}\".format(\n                from_or_to=from_or_to,\n                name=clip.name,\n                suffix=suffix\n            )\n        )\n    if timing_effect and timing_effect.effect_name == \"FreezeFrame\":\n        lines.append('* * FREEZE FRAME')\n    if url and style == 'avid':\n        lines.append(\"* {from_or_to} CLIP: {url}\".format(\n            from_or_to=from_or_to,\n            url=url\n        ))\n    if url and style == 'nucoda':\n        lines.append(\"* {from_or_to} FILE: {url}\".format(\n            from_or_to=from_or_to,\n            url=url\n        ))\n\n    if reelname_len and not clip.metadata.get('cmx_3600', {}).get('reel'):\n        lines.append(\"* OTIO TRUNCATED REEL NAME FROM: {url}\".format(\n            url=os.path.basename(_flip_windows_slashes(url or clip.name))\n        ))\n\n    cdl = clip.metadata.get('cdl')\n    if cdl:\n        asc_sop = cdl.get('asc_sop')\n        asc_sat = cdl.get('asc_sat')\n        if asc_sop:\n            lines.append(\n                \"*ASC_SOP ({} {} {}) ({} {} {}) ({} {} {})\".format(\n                    asc_sop['slope'][0],\n                    asc_sop['slope'][1],\n                    asc_sop['slope'][2],\n                    asc_sop['offset'][0],\n                    asc_sop['offset'][1],\n                    asc_sop['offset'][2],\n                    asc_sop['power'][0],\n                    asc_sop['power'][1],\n                    asc_sop['power'][2]\n                ))\n        if asc_sat:\n            lines.append(\"*ASC_SAT {}\".format(\n                asc_sat\n            ))\n\n    # Output any markers on this clip\n    for marker in clip.markers:\n        timecode = opentime.to_timecode(\n            marker.marked_range.start_time,\n            edl_rate\n        )\n\n        color = marker.color\n        meta = marker.metadata.get(\"cmx_3600\")\n        if not color and meta and meta.get(\"color\"):\n            color = meta.get(\"color\").upper()\n        comment = (marker.name or '').upper()\n        lines.append(\"* LOC: {} {:7} {}\".format(timecode, color, comment))\n\n    # If we are carrying any unhandled CMX 3600 comments on this clip\n    # then output them blindly.\n    extra_comments = clip.metadata.get('cmx_3600', {}).get('comments', [])\n    for comment in extra_comments:\n        lines.append(\"* {}\".format(comment))\n\n    return lines\n\n\ndef _flip_windows_slashes(path):\n    return re.sub(r'\\\\', '/', path)\n\n\ndef _reel_from_clip(clip, reelname_len):\n    if isinstance(clip, schema.Gap):\n        return 'BL'\n\n    elif clip.metadata.get('cmx_3600', {}).get('reel'):\n        return clip.metadata.get('cmx_3600').get('reel')\n\n    _reel = clip.name or 'AX'\n\n    if isinstance(clip.media_reference, schema.ExternalReference):\n        _reel = clip.media_reference.name or os.path.basename(\n            clip.media_reference.target_url\n        )\n\n    # Flip Windows slashes\n    _reel = os.path.basename(_flip_windows_slashes(_reel))\n\n    # Strip extension\n    reel = re.sub(r'([.][a-zA-Z]+)$', '', _reel)\n\n    if reelname_len:\n        # Remove non valid characters\n        reel = re.sub(r'[^ a-zA-Z0-9]+', '', reel)\n\n        if len(reel) > reelname_len:\n            reel = reel[:reelname_len]\n\n        elif len(reel) < reelname_len:\n            reel += ' ' * (reelname_len - len(reel))\n\n    return reel\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/adapters/fcp_xml.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"OpenTimelineIO Final Cut Pro 7 XML Adapter.\"\"\"\n\nimport collections\nimport functools\nimport itertools\nimport math\nimport os\nimport re\nfrom xml.etree import cElementTree\nfrom xml.dom import minidom\n\n# urlparse's name changes in Python 3\ntry:\n    # Python 2.7\n    import urlparse as urllib_parse\nexcept ImportError:\n    # Python 3\n    import urllib.parse as urllib_parse\n\n# Same with the ABC classes from collections\ntry:\n    # Python 3\n    from collections.abc import Mapping\nexcept ImportError:\n    # Python 2.7\n    from collections import Mapping\n\nfrom opentimelineio import (\n    core,\n    opentime,\n    schema,\n)\n\n# namespace to use for metadata\nMETA_NAMESPACE = 'fcp_xml'\n\n# Regex to match identifiers like clipitem-22\nID_RE = re.compile(r\"^(?P<tag>[a-zA-Z]*)-(?P<id>\\d*)$\")\n\n\n# ---------\n# utilities\n# ---------\n\n\nclass _Context(Mapping):\n    \"\"\"\n    An inherited value context.\n\n    In FCP XML there is a concept of inheritance down the element heirarchy.\n    For instance, a ``clip`` element may not specify the ``rate`` locally, but\n    instead inherit it from the parent ``track`` element.\n\n    This object models that as a stack of elements. When a value needs to be\n    queried from the context, it will be gathered by walking from the top of\n    the stack until the value is found.\n\n    For example, to find the ``rate`` element as an immediate child most\n    appropriate to the current context, you would do something like::\n        ``my_current_context[\"./rate\"]``\n\n    This object can be thought of as immutable. You get a new context when you\n    push an element. This prevents inadvertant tampering with parent contexts\n    that may be used at levels above.\n\n    This DOES NOT support ``id`` attribute dereferencing, please make sure to\n    do that prior to using this structure.\n\n    .. seealso:: https://developer.apple.com/library/archive/documentation\\\n            /AppleApplications/Reference/FinalCutPro_XML/Basics/Basics.html#\\\n            //apple_ref/doc/uid/TP30001154-TPXREF102\n    \"\"\"\n\n    def __init__(self, element=None, parent_elements=None):\n        if parent_elements is not None:\n            self.elements = parent_elements[:]\n        else:\n            self.elements = []\n\n        if element is not None:\n            self.elements.append(element)\n\n    def _all_keys(self):\n        \"\"\"\n        Returns a set of all the keys available in the context stack.\n        \"\"\"\n        return set(\n            itertools.chain.fromiterable(e.keys() for e in self.elements)\n        )\n\n    def __getitem__(self, key):\n        # Walk down the contexts until the item is found\n        for element in reversed(self.elements):\n            found_element = element.find(key)\n            if found_element is not None:\n                return found_element\n\n        raise KeyError(key)\n\n    def __iter__(self):\n        # This is unlikely to be used, so we'll do it the expensive way\n        return iter(self._all_keys)\n\n    def __len__(self):\n        # This is unlikely to be used, so we'll do it the expensive way\n        return len(self._all_keys)\n\n    def context_pushing_element(self, element):\n        \"\"\"\n        Pushes an element to the top of the stack.\n\n        :param element: Element to push to the stack.\n        :return: The new context with the provided element pushed to the top\n            of the stack.\n        :raises: :class:`ValueError` if the element is already in the stack.\n        \"\"\"\n        for context_element in self.elements:\n            if context_element == element:\n                raise ValueError(\n                    \"element {} already in context\".format(element)\n                )\n\n        return _Context(element, self.elements)\n\n\ndef _url_to_path(url):\n    parsed = urllib_parse.urlparse(url)\n    return parsed.path\n\n\ndef _bool_value(element):\n    \"\"\"\n    Given an xml element, returns the tag text converted to a bool.\n\n    :param element: The element to fetch the value from.\n\n    :return: A boolean.\n    \"\"\"\n    return (element.text.lower() == \"true\")\n\n\ndef _element_identification_string(element):\n    \"\"\"\n    Gets a string that will hopefully help in identifing an element when there\n    is an error.\n    \"\"\"\n    info_string = \"tag: {}\".format(element.tag)\n    try:\n        elem_id = element.attrib[\"id\"]\n        info_string += \" id: {}\".format(elem_id)\n    except KeyError:\n        pass\n\n    return info_string\n\n\ndef _name_from_element(element):\n    \"\"\"\n    Fetches the name from the ``name`` element child of the provided element.\n    If no element exists, returns ``None``.\n\n    :param element: The element to find the name for.\n\n    :return: The name string or ``None``\n    \"\"\"\n    name_elem = element.find(\"./name\")\n    if name_elem is not None:\n        return name_elem.text\n\n    return None\n\n\ndef _rate_for_element(element):\n    \"\"\"\n    Takes an FCP rate element and returns a rate to use with otio.\n\n    :param element: An FCP rate element.\n\n    :return: The float rate.\n    \"\"\"\n    # rate is encoded as a timebase (int) which can be drop-frame\n    base = float(element.find(\"./timebase\").text)\n    if _bool_value(element.find(\"./ntsc\")):\n        base *= 1000.0 / 1001\n\n    return base\n\n\ndef _rate_from_context(context):\n    \"\"\"\n    Given the context object, gets the appropriate rate.\n\n    :param context: The :class:`_Context` instance to find the rate in.\n\n    :return: The rate value or ``None`` if no rate is available in the context.\n    \"\"\"\n    try:\n        rate_element = context[\"./rate\"]\n    except KeyError:\n        return None\n\n    return _rate_for_element(rate_element)\n\n\ndef _time_from_timecode_element(tc_element, context=None):\n    \"\"\"\n    Given a timecode xml element, returns the time that represents.\n\n    .. todo:: Non Drop-Frame timecode is not yet supported by OTIO.\n\n    :param tc_element: The ``timecode`` element.\n    :param context: The context dict under which this timecode is being gotten.\n\n    :return: The :class:`opentime.RationalTime` representation of the\n        timecode.\n    \"\"\"\n    if context is not None:\n        local_context = context.context_pushing_element(tc_element)\n    else:\n        local_context = _Context(tc_element)\n\n    # Resolve the rate\n    rate = _rate_from_context(local_context)\n\n    # Try using the display format and frame number\n    frame = tc_element.find(\"./frame\")\n\n    # Use frame number, if available\n    if frame is not None:\n        frame_num = int(frame.text)\n        return opentime.RationalTime(frame_num, rate)\n\n    # If a TC string is provided, parse rate from it\n    tc_string_element = tc_element.find(\"./string\")\n    if tc_string_element is None:\n        raise ValueError(\"Timecode element missing required elements\")\n\n    tc_string = tc_string_element.text\n\n    return opentime.from_timecode(tc_string, rate)\n\n\ndef _track_kind_from_element(media_element):\n    \"\"\"\n    Given an FCP XML media sub-element, returns an appropriate\n    :class:`schema.TrackKind` value corresponding to that media type.\n\n    :param media_element: An XML element that is a child of the ``media`` tag.\n\n    :return: The corresponding :class`schema.TrackKind` value.\n    :raises: :class:`ValueError` When the media type is unsupported.\n    \"\"\"\n    element_tag = media_element.tag.lower()\n    if element_tag == \"audio\":\n        return schema.TrackKind.Audio\n    elif element_tag == \"video\":\n        return schema.TrackKind.Video\n\n    raise ValueError(\"Unsupported media kind: {}\".format(media_element.tag))\n\n\ndef _is_primary_audio_channel(track):\n    \"\"\"\n    Determines whether or not this is the \"primary\" audio track.\n\n    audio may be structured in stereo where each channel occupies a separate\n    track. This importer keeps stereo pairs ganged together as a single track.\n\n    :param track: An XML track element.\n\n    :return: A boolean ``True`` if this is the first track.\n    \"\"\"\n    exploded_index = track.attrib.get('currentExplodedTrackIndex', '0')\n    exploded_count = track.attrib.get('totalExplodedTrackCount', '1')\n\n    return (exploded_index == '0' or exploded_count == '1')\n\n\ndef _transition_cut_point(transition_item, context):\n    \"\"\"\n    Returns the end time at which the transition progresses from one clip to\n    the next.\n\n    :param transition_item: The XML element for the transition.\n    :param context: The context dictionary applying to this transition.\n\n    :return: The :class:`opentime.RationalTime` the transition cuts at.\n    \"\"\"\n    alignment = transition_item.find('./alignment').text\n    start = int(transition_item.find('./start').text)\n    end = int(transition_item.find('./end').text)\n\n    # start/end time is in the parent context's rate\n    local_context = context.context_pushing_element(transition_item)\n    rate = _rate_from_context(local_context)\n\n    if alignment in ('end', 'end-black'):\n        value = end\n    elif alignment in ('start', 'start-black'):\n        value = start\n    elif alignment in ('center',):\n        value = int((start + end) / 2)\n    else:\n        value = int((start + end) / 2)\n\n    return opentime.RationalTime(value, rate)\n\n\ndef _xml_tree_to_dict(node, ignore_tags=None, omit_timing=True):\n    \"\"\"\n    Translates the tree under a provided node mapping to a dictionary/list\n    representation. XML tag attributes are placed in the dictionary with an\n    ``@`` prefix.\n\n    .. note:: In addition to the provided ignore tags, this filters a subset of\n    timing metadata such as ``frame`` and ``string`` elements within timecode\n    elements.\n\n    .. warning:: This scheme does not allow for leaf elements to have\n    attributes.  for the moment this doesn't seem to be an issue.\n\n    :param node: The root xml element to express childeren of in the\n        dictionary.\n    :param ignore_tags: A collection of tagnames to skip when converting.\n    :param omit_timing: If ``True``, omits timing-specific tags.\n\n    :return: The dictionary representation.\n    \"\"\"\n    if node.tag == \"timecode\":\n        additional_ignore_tags = {\"frame\", \"string\"}\n    else:\n        additional_ignore_tags = tuple()\n\n    out_dict = collections.OrderedDict()\n\n    # Handle the attributes\n    out_dict.update(\n        collections.OrderedDict(\n            (\"@{}\".format(k), v) for k, v in node.attrib.items()\n        )\n    )\n\n    # Now traverse the child tags\n    encountered_tags = set()\n    list_tags = set()\n    for info_node in node:\n        # Skip tags we were asked to omit\n        node_tag = info_node.tag\n        if ignore_tags and node_tag in ignore_tags:\n            continue\n\n        # Skip some special case tags related to timing information\n        if node_tag in additional_ignore_tags:\n            continue\n\n        # If there are children, make this a sub-dictionary by recursing\n        if len(info_node):\n            node_value = _xml_tree_to_dict(info_node)\n        else:\n            node_value = info_node.text\n\n        # If we've seen this node before, then treat it as a list\n        if node_tag in list_tags:\n            # We've established that this tag is a list, append to that\n            out_dict[node_tag].append(node_value)\n        elif node_tag in encountered_tags:\n            # This appears to be a list we didn't know about, convert\n            out_dict[node_tag] = [\n                out_dict[node_tag], node_value\n            ]\n            list_tags.add(node_tag)\n        else:\n            # Store the value\n            out_dict[node_tag] = node_value\n            encountered_tags.add(node_tag)\n\n    return out_dict\n\n\ndef _dict_to_xml_tree(data_dict, tag):\n    \"\"\"\n    Given a dictionary, returns an element tree storing the data. This is the\n    inverse of :func:`_xml_tree_to_dict`.\n\n    Any key/value pairs in the dictionary heirarchy where the key is prefixed\n    with ``@`` will be treated as attributes on the containing element.\n\n    .. note:: This will automatically omit some kinds of metadata it should\n    be up to the xml building functions to manage (such as timecode and id).\n\n    :param data_dict: The dictionary to turn into an XML tree.\n    :param tag: The tag name to use for the top-level element.\n\n    :return: The top element for the dictionary\n    \"\"\"\n    top_attributes = collections.OrderedDict(\n        (k[1:], v) for k, v in data_dict.items()\n        if k != \"@id\" and k.startswith(\"@\")\n    )\n    top_element = cElementTree.Element(tag, **top_attributes)\n\n    def elements_for_value(python_value, element_tag):\n        \"\"\" Creates a list of appropriate XML elements given a value. \"\"\"\n        if isinstance(python_value, dict):\n            element = _dict_to_xml_tree(python_value, element_tag)\n            return [element]\n        elif isinstance(python_value, list):\n            return itertools.chain.from_iterable(\n                elements_for_value(item, element_tag) for item in python_value\n            )\n        else:\n            element = cElementTree.Element(element_tag)\n            if python_value is not None:\n                element.text = str(python_value)\n            return [element]\n\n    # Drop timecode, rate, and link elements from roundtripping because they\n    # may become stale with timeline updates.\n    default_ignore_keys = {\"timecode\", \"rate\", \"link\"}\n    specific_ignore_keys = {\"samplecharacteristics\": {\"timecode\"}}\n    ignore_keys = specific_ignore_keys.get(tag, default_ignore_keys)\n\n    # push the elements into the tree\n    for key, value in data_dict.items():\n        if key in ignore_keys:\n            continue\n\n        # We already handled the attributes\n        if key.startswith(\"@\"):\n            continue\n\n        elements = elements_for_value(value, key)\n        top_element.extend(elements)\n\n    return top_element\n\n\ndef _element_with_item_metadata(tag, item):\n    \"\"\"\n    Given a tag name, gets the FCP XML metadata dict and creates a tree of XML\n    with that metadata under a top element with the provided tag.\n\n    :param tag: The XML tag for the root element.\n    :param item: An otio object with a metadata dict.\n    \"\"\"\n    item_meta = item.metadata.get(META_NAMESPACE)\n    if item_meta:\n        return _dict_to_xml_tree(item_meta, tag)\n\n    return cElementTree.Element(tag)\n\n\ndef _get_or_create_subelement(parent_element, tag):\n    \"\"\"\n    Given an element and tag name, either gets the direct child of parent with\n    that tag name or creates a new subelement with that tag and returns it.\n\n    :param parent_element: The element to get or create the subelement from.\n    :param tag: The tag for the subelement.\n    \"\"\"\n    sub_element = parent_element.find(tag)\n    if sub_element is None:\n        sub_element = cElementTree.SubElement(parent_element, tag)\n\n    return sub_element\n\n\ndef _make_pretty_string(tree_e):\n    # most of the parsing in this adapter is done with cElementTree because it\n    # is simpler and faster. However, the string representation it returns is\n    # far from elegant. Therefor we feed it through minidom to provide an xml\n    # with indentations.\n    string = cElementTree.tostring(tree_e, encoding=\"UTF-8\", method=\"xml\")\n    dom = minidom.parseString(string)\n    return dom.toprettyxml(indent='    ')\n\n\ndef marker_for_element(marker_element, rate):\n    \"\"\"\n    Creates an :class:`schema.Marker` for the provided element.\n\n    :param marker_element: The XML element for the marker.\n    :param rate: The rate for the object the marker is attached to.\n\n    :return: The :class:`schema.Marker` instance.\n    \"\"\"\n    # TODO: The spec doc indicates that in and out are required, but doesn't\n    #       say they have to be locally specified, so is it possible they\n    #       could be inherited?\n    marker_in = opentime.RationalTime(\n        float(marker_element.find(\"./in\").text), rate\n    )\n    marker_out_value = float(marker_element.find(\"./out\").text)\n    if marker_out_value > 0:\n        marker_out = opentime.RationalTime(\n            marker_out_value, rate\n        )\n        marker_duration = (marker_out - marker_in)\n    else:\n        marker_duration = opentime.RationalTime(rate=rate)\n\n    marker_range = opentime.TimeRange(marker_in, marker_duration)\n\n    md_dict = _xml_tree_to_dict(marker_element, {\"in\", \"out\", \"name\"})\n    metadata = {META_NAMESPACE: md_dict} if md_dict else None\n\n    return schema.Marker(\n        name=_name_from_element(marker_element),\n        marked_range=marker_range,\n        metadata=metadata\n    )\n\n\ndef markers_from_element(element, context=None):\n    \"\"\"\n    Given an element, returns the list of markers attached to it.\n\n    :param element: An element with one or more ``marker`` child elements.\n    :param context: The context for this element.\n\n    :return: A :class:`list` of :class:`schema.Marker` instances attached\n        to the provided element.\n    \"\"\"\n    if context is not None:\n        local_context = context.context_pushing_element(element)\n    else:\n        local_context = _Context(element)\n    rate = _rate_from_context(local_context)\n\n    return [marker_for_element(e, rate) for e in element.iterfind(\"./marker\")]\n\n\nclass FCP7XMLParser:\n    \"\"\"\n    Implements parsing of an FCP XML file into an OTIO timeline.\n\n    Parsing FCP XML elements include two concepts that require carrying state:\n        1. Inheritance\n        2. The id Attribute\n\n    .. seealso:: https://developer.apple.com/library/archive/documentation/\\\n            AppleApplications/Reference/FinalCutPro_XML/Basics/Basics.html\\\n            #//apple_ref/doc/uid/TP30001154-TPXREF102\n\n    Inheritance is implemented using a _Context object that is pushed down\n    through layers of parsing. A given parsing method is passed the element to\n    parse into an otio object along with the context that element exists under\n    (e.x. a track element parsing method is given the track element and the\n    sequence context for that track).\n\n    The id attribute dereferencing is handled through a lookup table stored on\n    parser instances and using the ``_derefed_`` methods to take an element and\n    find dereference elements.\n    \"\"\"\n\n    _etree = None\n    \"\"\" The root etree for the FCP XML. \"\"\"\n\n    _id_map = None\n    \"\"\" A mapping of id to the first element encountered with that id. \"\"\"\n\n    def __init__(self, element_tree):\n        \"\"\"\n        Constructor, must be init with an xml etree.\n        \"\"\"\n        self._etree = element_tree\n\n        self._id_map = {}\n\n    def _derefed_element(self, element):\n        \"\"\"\n        Given an element, dereferences it by it's id attribute if needed. If\n        the element has an id attribute and it's our first time encountering\n        it, store the id.\n        \"\"\"\n        if element is None:\n            return element\n\n        try:\n            elem_id = element.attrib[\"id\"]\n        except KeyError:\n            return element\n\n        return self._id_map.setdefault(elem_id, element)\n\n    def _derefed_iterfind(self, element, path):\n        \"\"\"\n        Given an elemnt, finds elements with the provided path below and\n        returns an iterator of the dereferenced versions of those.\n\n        :param element: The XML etree element.\n        :param path: The path to find subelements.\n\n        :return: iterator of subelements dereferenced by id.\n        \"\"\"\n        return (\n            self._derefed_element(e) for e in element.iterfind(path)\n        )\n\n    def top_level_sequences(self):\n        \"\"\"\"\n        Returns a list of timelines for the top-level sequences in the file.\n        \"\"\"\n        context = _Context()\n\n        # If the tree has just sequences at the top level, this will catch them\n        top_iter = self._derefed_iterfind(self._etree, \"./sequence\")\n\n        # If there is a project or bin at the top level, this should cath them\n        project_and_bin_iter = self._derefed_iterfind(\n            self._etree, \".//children/sequence\"\n        )\n\n        # Make an iterator that will exhaust both the above\n        sequence_iter = itertools.chain(top_iter, project_and_bin_iter)\n\n        return [self.timeline_for_sequence(s, context) for s in sequence_iter]\n\n    def timeline_for_sequence(self, sequence_element, context):\n        \"\"\"\n        Returns either an :class`schema.Timeline` parsed from a sequence\n        element.\n\n        :param sequence_element: The sequence element.\n        :param context: The context dictionary.\n\n        :return: The appropriate OTIO object for the element.\n        \"\"\"\n        local_context = context.context_pushing_element(sequence_element)\n\n        name = _name_from_element(sequence_element)\n        parsed_tags = {\"name\", \"media\", \"marker\", \"duration\"}\n        md_dict = _xml_tree_to_dict(sequence_element, parsed_tags)\n\n        sequence_timecode = self._derefed_element(\n            sequence_element.find(\"./timecode\")\n        )\n        if sequence_timecode is not None:\n            seq_start_time = _time_from_timecode_element(\n                sequence_timecode, local_context\n            )\n        else:\n            seq_start_time = None\n\n        media_element = self._derefed_element(sequence_element.find(\"./media\"))\n        if media_element is None:\n            tracks = None\n        else:\n            # Reach down into the media block and escalate metadata to the\n            # sequence\n            for media_type in media_element:\n                media_info_dict = _xml_tree_to_dict(media_type, {\"track\"})\n                if media_info_dict:\n                    media_dict = md_dict.setdefault(\n                        \"media\", collections.OrderedDict()\n                    )\n                    media_dict[media_type.tag] = media_info_dict\n\n            tracks = self.stack_for_element(media_element, local_context)\n            tracks.name = name\n\n        # TODO: Should we be parsing the duration tag and pad out a track with\n        #       gap to match?\n\n        timeline = schema.Timeline(\n            name=name,\n            global_start_time=seq_start_time,\n            metadata={META_NAMESPACE: md_dict} if md_dict else {},\n        )\n        timeline.tracks = tracks\n\n        # Push the sequence markers onto the top stack\n        markers = markers_from_element(sequence_element, context)\n        timeline.tracks.markers.extend(markers)\n\n        return timeline\n\n    def stack_for_element(self, element, context):\n        \"\"\"\n        Given an element, parses out track information as a stack.\n\n        :param element: The element under which to find the tracks (typically\n            a ``media`` element.\n        :param context: The current parser context.\n\n        :return: A :class:`schema.Stack` of the tracks.\n        \"\"\"\n        # Determine the context\n        local_context = context.context_pushing_element(element)\n\n        tracks = []\n        media_type_elements = self._derefed_iterfind(element, \"./\")\n        for media_type_element in media_type_elements:\n            try:\n                track_kind = _track_kind_from_element(media_type_element)\n            except ValueError:\n                # Unexpected element\n                continue\n\n            is_audio = (track_kind == schema.TrackKind.Audio)\n            track_elements = self._derefed_iterfind(\n                media_type_element, \"./track\"\n            )\n            for track_element in track_elements:\n                if is_audio and not _is_primary_audio_channel(track_element):\n                    continue\n\n                tracks.append(\n                    self.track_for_element(\n                        track_element, track_kind, local_context\n                    )\n                )\n\n        markers = markers_from_element(element, context)\n\n        stack = schema.Stack(\n            children=tracks,\n            markers=markers,\n            name=_name_from_element(element),\n        )\n\n        return stack\n\n    def track_for_element(self, track_element, track_kind, context):\n        \"\"\"\n        Given a track element, constructs the OTIO track.\n\n        :param track_element: The track XML element.\n        :param track_kind: The :class:`schema.TrackKind` for the track.\n        :param context: The context dict for this track.\n        \"\"\"\n        local_context = context.context_pushing_element(track_element)\n        name_element = track_element.find(\"./name\")\n        track_name = (name_element.text if name_element is not None else None)\n\n        timeline_item_tags = {\"clipitem\", \"generatoritem\", \"transitionitem\"}\n\n        md_dict = _xml_tree_to_dict(track_element, timeline_item_tags)\n        track_metadata = {META_NAMESPACE: md_dict} if md_dict else None\n\n        track = schema.Track(\n            name=track_name,\n            kind=track_kind,\n            metadata=track_metadata,\n        )\n\n        # Iterate through and parse track items\n        track_rate = _rate_from_context(local_context)\n        current_timeline_time = opentime.RationalTime(0, track_rate)\n        head_transition_element = None\n        for i, item_element in enumerate(track_element):\n            if item_element.tag not in timeline_item_tags:\n                continue\n\n            item_element = self._derefed_element(item_element)\n\n            # Do a lookahead to try and find the tail transition item\n            try:\n                tail_transition_element = track_element[i + 1]\n                if tail_transition_element.tag != \"transitionitem\":\n                    tail_transition_element = None\n                else:\n                    tail_transition_element = self._derefed_element(\n                        tail_transition_element\n                    )\n            except IndexError:\n                tail_transition_element = None\n\n            track_item, item_range = self.item_and_timing_for_element(\n                item_element,\n                head_transition_element,\n                tail_transition_element,\n                local_context,\n            )\n\n            # Insert gap between timeline cursor and the new item if needed.\n            if current_timeline_time < item_range.start_time:\n                gap_duration = (item_range.start_time - current_timeline_time)\n                gap_range = opentime.TimeRange(\n                    duration=gap_duration.rescaled_to(track_rate)\n                )\n                track.append(schema.Gap(source_range=gap_range))\n\n            # Add the item and advance the timeline cursor\n            track.append(track_item)\n            current_timeline_time = item_range.end_time_exclusive()\n\n            # Stash the element for the next iteration if it's a transition\n            if item_element.tag == \"transitionitem\":\n                head_transition_element = item_element\n\n        return track\n\n    def media_reference_for_file_element(self, file_element, context):\n        \"\"\"\n        Given a file XML element, returns the\n        :class`schema.ExternalReference`.\n\n        :param file_element: The file xml element.\n        :param context: The parent context dictionary.\n\n        :return: An :class:`schema.ExternalReference`.\n        \"\"\"\n        local_context = context.context_pushing_element(file_element)\n        media_ref_rate = _rate_from_context(local_context)\n\n        name = _name_from_element(file_element)\n\n        # Get the full metadata\n        metadata_ignore_keys = {\"duration\", \"name\", \"pathurl\"}\n        md_dict = _xml_tree_to_dict(file_element, metadata_ignore_keys)\n        metadata_dict = {META_NAMESPACE: md_dict} if md_dict else None\n\n        # Determine the file path\n        path_element = file_element.find(\"./pathurl\")\n        if path_element is not None:\n            path = path_element.text\n        else:\n            path = None\n\n        # Find the timing\n        timecode_element = file_element.find(\"./timecode\")\n        if timecode_element is not None:\n            start_time = _time_from_timecode_element(timecode_element)\n            start_time = start_time.rescaled_to(media_ref_rate)\n        else:\n            start_time = opentime.RationalTime(0, media_ref_rate)\n\n        duration_element = file_element.find(\"./duration\")\n        if duration_element is not None:\n            duration = opentime.RationalTime(\n                float(duration_element.text), media_ref_rate\n            )\n            available_range = opentime.TimeRange(start_time, duration)\n        elif timecode_element is not None:\n            available_range = opentime.TimeRange(\n                start_time,\n                opentime.RationalTime(0, media_ref_rate),\n            )\n        else:\n            available_range = None\n\n        if path is None:\n            media_reference = schema.MissingReference(\n                name=name,\n                available_range=available_range,\n                metadata=metadata_dict,\n            )\n        else:\n            media_reference = schema.ExternalReference(\n                target_url=path,\n                available_range=available_range,\n                metadata=metadata_dict,\n            )\n            media_reference.name = name\n\n        return media_reference\n\n    def media_reference_for_effect_element(self, effect_element):\n        \"\"\"\n        Given an effect element, returns a generator reference.\n\n        :param effect_element: The effect for the generator.\n\n        :return: An :class:`schema.GeneratorReference` instance.\n        \"\"\"\n        name = _name_from_element(effect_element)\n        md_dict = _xml_tree_to_dict(effect_element, {\"name\"})\n\n        return schema.GeneratorReference(\n            name=name,\n            metadata=({META_NAMESPACE: md_dict} if md_dict else None)\n        )\n\n    def item_and_timing_for_element(\n        self, item_element, head_transition, tail_transition, context\n    ):\n        \"\"\"\n        Given a track item, returns a tuple with the appropriate OpenTimelineIO\n        schema item as the first element and an\n        :class:`opentime.TimeRange`of theresolved timeline range the clip\n        occupies.\n\n        :param item_element: The track item XML node.\n        :param head_transition: The xml element for the transition immediately\n            before or ``None``.\n        :param tail_transition: The xml element for the transition immediately\n            after or ``None``.\n        :param context: The context dictionary.\n\n        :return: An :class:`core.Item` subclass instance and\n            :class:`opentime.TimeRange` for the item.\n        \"\"\"\n        parent_rate = _rate_from_context(context)\n\n        # Establish the start/end time in the timeline\n        start_value = int(item_element.find(\"./start\").text)\n        end_value = int(item_element.find(\"./end\").text)\n\n        if start_value == -1:\n            # determine based on the cut point of the head transition\n            start = _transition_cut_point(head_transition, context)\n\n            # This offset is needed to determing how much to advance from the\n            # clip media's in time. Duration accounts for this offset for the\n            # out time.\n            transition_rate = _rate_from_context(\n                context.context_pushing_element(head_transition)\n            )\n            start_offset = start - opentime.RationalTime(\n                int(head_transition.find('./start').text), transition_rate\n            )\n        else:\n            start = opentime.RationalTime(start_value, parent_rate)\n            start_offset = opentime.RationalTime()\n\n        if end_value == -1:\n            # determine based on the cut point of the tail transition\n            end = _transition_cut_point(tail_transition, context)\n        else:\n            end = opentime.RationalTime(end_value, parent_rate)\n\n        item_range = opentime.TimeRange(start, (end - start))\n\n        # Get the metadata dictionary for the item\n        item_metadata_ignore_keys = {\n            \"name\",\n            \"start\",\n            \"end\",\n            \"in\",\n            \"out\",\n            \"duration\",\n            \"file\",\n            \"marker\",\n            \"effect\",\n            \"rate\",\n            \"sequence\",\n        }\n        metadata_dict = _xml_tree_to_dict(\n            item_element, item_metadata_ignore_keys\n        )\n\n        # deserialize the item\n        if item_element.tag in {\"clipitem\", \"generatoritem\"}:\n            item = self.clip_for_element(\n                item_element, item_range, start_offset, context\n            )\n        elif item_element.tag == \"transitionitem\":\n            item = self.transition_for_element(item_element, context)\n        else:\n            name = \"unknown-{}\".format(item_element.tag)\n            item = core.Item(name=name, source_range=item_range)\n\n        if metadata_dict:\n            item.metadata.setdefault(META_NAMESPACE, {}).update(metadata_dict)\n\n        return (item, item_range)\n\n    def clip_for_element(\n        self, clipitem_element, item_range, start_offset, context\n    ):\n        \"\"\"\n        Given a clipitem xml element, returns an :class:`schema.Clip`.\n\n        :param clipitem_element: The element to create a clip for.\n        :param item_range: The time range in the timeline the clip occupies.\n        :param start_offset: The amount by which the ``in`` time of the clip\n            source should be advanced (usually due to a transition).\n        :param context: The parent context for the clip.\n\n        :return: The :class:`schema.Clip` instance.\n        \"\"\"\n        local_context = context.context_pushing_element(clipitem_element)\n\n        name = _name_from_element(clipitem_element)\n\n        file_element = self._derefed_element(clipitem_element.find(\"./file\"))\n        sequence_element = self._derefed_element(\n            clipitem_element.find(\"./sequence\")\n        )\n        if clipitem_element.tag == \"generatoritem\":\n            generator_effect_element = clipitem_element.find(\n                \"./effect[effecttype='generator']\"\n            )\n        else:\n            generator_effect_element = None\n\n        media_start_time = opentime.RationalTime()\n        if sequence_element is not None:\n            item = self.stack_for_element(sequence_element, local_context)\n            # TODO: is there an applicable media start time we should be\n            #       using from nested sequences?\n        elif file_element is not None or generator_effect_element is not None:\n            if file_element is not None:\n                media_reference = self.media_reference_for_file_element(\n                    file_element, local_context\n                )\n                # See if there is a start offset\n                timecode_element = file_element.find(\"./timecode\")\n                if timecode_element is not None:\n                    media_start_time = _time_from_timecode_element(\n                        timecode_element\n                    )\n            elif generator_effect_element is not None:\n                media_reference = self.media_reference_for_effect_element(\n                    generator_effect_element\n                )\n\n            item = schema.Clip(\n                name=name,\n                media_reference=media_reference,\n            )\n        else:\n            raise TypeError(\n                'Type of clip item is not supported {}'.format(\n                    _element_identification_string(clipitem_element)\n                )\n            )\n\n        # Add the markers\n        markers = markers_from_element(clipitem_element, context)\n        item.markers.extend(markers)\n\n        # Find the in time (source time relative to media start)\n        clip_rate = _rate_from_context(local_context)\n        in_value = float(clipitem_element.find('./in').text)\n        in_time = opentime.RationalTime(in_value, clip_rate)\n\n        # Offset the \"in\" time by the start offset of the media\n        soure_start_time = in_time + media_start_time + start_offset\n        duration = item_range.duration\n\n        # Source Range is the item range expressed in the clip's rate (for now)\n        source_range = opentime.TimeRange(\n            soure_start_time.rescaled_to(clip_rate),\n            duration.rescaled_to(clip_rate),\n        )\n\n        item.source_range = source_range\n\n        # Parse the filters\n        filter_iter = self._derefed_iterfind(clipitem_element, \"./filter\")\n        for filter_element in filter_iter:\n            item.effects.append(\n                self.effect_from_filter_element(filter_element)\n            )\n\n        return item\n\n    def effect_from_filter_element(self, filter_element):\n        \"\"\"\n        Given a filter element, creates an :class:`schema.Effect`.\n\n        :param filter_element: The ``filter`` element containing the effect.\n\n        :return: The effect instance.\n        \"\"\"\n        effect_element = filter_element.find(\"./effect\")\n\n        if effect_element is None:\n            raise ValueError(\n                \"could not find effect in filter: {}\".format(filter_element)\n            )\n\n        name = effect_element.find(\"./name\").text\n\n        effect_metadata = _xml_tree_to_dict(effect_element, {\"name\"})\n\n        return schema.Effect(\n            name,\n            metadata={META_NAMESPACE: effect_metadata},\n        )\n\n    def transition_for_element(self, item_element, context):\n        \"\"\"\n        Creates an OTIO transition for the provided transition element.\n\n        :param item_element: The element to create a transition for.\n        :param context: The parent context for the element.\n\n        :return: The :class:`schema.Transition` instance.\n        \"\"\"\n        # start and end times are in the parent's rate\n        rate = _rate_from_context(context)\n        start = opentime.RationalTime(\n            int(item_element.find('./start').text),\n            rate\n        )\n        end = opentime.RationalTime(\n            int(item_element.find('./end').text),\n            rate\n        )\n        cut_point = _transition_cut_point(item_element, context)\n\n        transition = schema.Transition(\n            name=item_element.find('./effect/name').text,\n            transition_type=schema.TransitionTypes.SMPTE_Dissolve,\n            in_offset=cut_point - start,\n            out_offset=end - cut_point,\n        )\n\n        return transition\n\n\n# ------------------------\n# building single track\n# ------------------------\n\n\ndef _backreference_for_item(item, tag, br_map):\n    \"\"\"\n    Given an item, determines what the id in the backreference map should be.\n    If the item is already tracked in the map, it will be returned, otherwise\n    a new id will be minted.\n\n    .. note:: ``br_map`` may be mutated by this function. ``br_map`` is\n    intended to be an opaque data structure and only accessed through this\n    function, the structure of data in br_map may change.\n\n    :param item: The :class:`core.SerializableObject` to create an id for.\n    :param tag: The tag name that will be used for object in xml.\n    :param br_map: The dictionary containing backreference information\n        generated so far.\n\n    :return: A 2-tuple of (id_string, is_new_id) where the ``id_string`` is\n        the value for the xml id attribute and ``is_new_id`` is ``True`` when\n        this is the first time that id was encountered.\n    \"\"\"\n    # br_map is structured as a dictionary with tags as keys, and dictionaries\n    # of hash to id int as values.\n\n    def id_string(id_int):\n        return \"{}-{}\".format(tag, id_int)\n\n    # Determine how to uniquely identify the referenced item\n    if isinstance(item, schema.ExternalReference):\n        item_hash = hash(str(item.target_url))\n    else:\n        # TODO: This may become a performance issue. It means that every\n        #       non-ref object is serialized to json and hashed each time it's\n        #       encountered.\n        item_hash = hash(\n            core.json_serializer.serialize_json_to_string(item)\n        )\n\n    is_new_id = False\n    item_id = br_map.get(tag, {}).get(item_hash)\n    if item_id is not None:\n        return (id_string(item_id), is_new_id)\n\n    # This is a new id, figure out what it should be.\n    is_new_id = True\n\n    # Attempt to preserve the ID from the input metadata.\n    preferred_id = None\n    orig_id_string = item.metadata.get(META_NAMESPACE, {}).get(\"@id\")\n    if orig_id_string is not None:\n        orig_id_match = ID_RE.match(orig_id_string)\n        if orig_id_match is not None:\n            match_groups = orig_id_match.groupdict()\n            orig_tagname = match_groups[\"tag\"]\n            if orig_tagname == tag:\n                preferred_id = int(match_groups[\"id\"])\n\n    # Generate an id by finding the lowest value in a contiguous range not\n    # colliding with an existing value\n    tag_id_map = br_map.setdefault(tag, {})\n    existing_ids = set(tag_id_map.values())\n    if preferred_id is not None and preferred_id not in existing_ids:\n        item_id = preferred_id\n    else:\n        # Make a range from 1 including the ID after the largest assigned\n        # (hence the +2 since range is non-inclusive on the upper bound)\n        max_assigned_id = max(existing_ids) if existing_ids else 0\n        max_possible_id = (max_assigned_id + 2)\n        possible_ids = set(range(1, max_possible_id))\n\n        # Select the lowest unassigned ID\n        item_id = min(possible_ids.difference(existing_ids))\n\n    # Store the created id\n    tag_id_map[item_hash] = item_id\n\n    return (id_string(item_id), is_new_id)\n\n\ndef _backreference_build(tag):\n    \"\"\"\n    A decorator for functions creating XML elements to implement the id system\n    described in FCP XML.\n\n    This wrapper determines if the otio item is equivalent to one encountered\n    before with the provided tag name. If the item hasn't been encountered then\n    the wrapped function will be invoked and the XML element from that function\n    will have the ``id`` attribute set and be stored in br_map.\n    If the item is equivalent to a previously provided item, the wrapped\n    function won't be invoked and a simple tag with the previous instance's id\n    will be returned instead.\n\n    The wrapped function must:\n        - Have the otio item as the first positional argument.\n        - Have br_map (backreference map, a dictionary) as the last positional\n        arg. br_map stores the state for encountered items.\n\n    :param tag: The xml tag of the element the wrapped function generates.\n    \"\"\"\n    # We can also encode these back-references if an item is accessed multiple\n    # times. To do this we store an id attribute on the element. For back-\n    # references we then only need to return an empty element of that type with\n    # the id we logged before\n\n    def singleton_decorator(func):\n        @functools.wraps(func)\n        def wrapper(item, *args, **kwargs):\n            br_map = args[-1]\n\n            item_id, id_is_new = _backreference_for_item(item, tag, br_map)\n\n            # if the item exists in the map already, we should use the\n            # abbreviated XML element referring to the original\n            if not id_is_new:\n                return cElementTree.Element(tag, id=item_id)\n\n            # This is the first time for this unique item, it needs it's full\n            # XML. Get the element generated by the wrapped function and add\n            # the id attribute.\n            elem = func(item, *args, **kwargs)\n            elem.attrib[\"id\"] = item_id\n\n            return elem\n\n        return wrapper\n\n    return singleton_decorator\n\n\ndef _append_new_sub_element(parent, tag, attrib=None, text=None):\n    \"\"\"\n    Creates a sub-element with the provided tag, attributes, and text.\n\n    This is a convenience because the :class:`SubElement` constructor does not\n    provide the ability to set ``text``.\n\n    :param parent: The parent element.\n    :param tag: The tag string for the element.\n    :param attrib: An optional dictionary of attributes for the element.\n    :param text: Optional text value for the element.\n\n    :return: The new XML element.\n    \"\"\"\n    elem = cElementTree.SubElement(parent, tag, **attrib or {})\n    if text is not None:\n        elem.text = text\n\n    return elem\n\n\ndef _build_rate(fps):\n    \"\"\"\n    Given a framerate, makes a ``rate`` xml tree.\n\n    :param fps: The framerate.\n    :return: The fcp xml ``rate`` tree.\n    \"\"\"\n    rate = math.ceil(fps)\n\n    rate_e = cElementTree.Element('rate')\n    _append_new_sub_element(rate_e, 'timebase', text=str(int(rate)))\n    _append_new_sub_element(\n        rate_e,\n        'ntsc',\n        text='FALSE' if rate == fps else 'TRUE'\n    )\n    return rate_e\n\n\ndef _build_timecode(time, fps, drop_frame=False, additional_metadata=None):\n    \"\"\"\n    Makes a timecode xml element tree.\n\n    .. warning:: The drop_frame parameter is currently ignored and\n        auto-determined by rate. This is because the underlying otio timecode\n        conversion assumes DFTC based on rate.\n\n    :param time: The :class:`opentime.RationalTime` for the timecode.\n    :param fps: The framerate for the timecode.\n    :param drop_frame: If True, generates drop-frame timecode.\n    :param additional_metadata: A dictionary with other metadata items like\n        ``field``, ``reel``, ``source``, and ``format``. It is assumed this\n        dictionary is of the form generated by :func:`_xml_tree_to_dict` when\n        the file was read originally.\n\n    :return: The ``timecode`` element.\n    \"\"\"\n    if additional_metadata:\n        # Only allow legal child items for the timecode element\n        filtered = {\n            k: v for k, v in additional_metadata.items()\n            if k in {\"field\", \"reel\", \"source\", \"format\"}\n        }\n        tc_element = _dict_to_xml_tree(filtered, \"timecode\")\n    else:\n        tc_element = cElementTree.Element(\"timecode\")\n\n    tc_element.append(_build_rate(fps))\n    rate_is_not_ntsc = (tc_element.find('./rate/ntsc').text == \"FALSE\")\n    if drop_frame and rate_is_not_ntsc:\n        tc_fps = fps * (1000 / 1001.0)\n    else:\n        tc_fps = fps\n\n    # Get the time values\n    tc_time = opentime.RationalTime(time.value_rescaled_to(fps), tc_fps)\n    tc_string = opentime.to_timecode(tc_time, tc_fps, drop_frame)\n\n    _append_new_sub_element(tc_element, \"string\", text=tc_string)\n\n    frame_number = int(round(time.value))\n    _append_new_sub_element(\n        tc_element, \"frame\", text=\"{:.0f}\".format(frame_number)\n    )\n\n    drop_frame = (\";\" in tc_string)\n    display_format = \"DF\" if drop_frame else \"NDF\"\n    _append_new_sub_element(tc_element, \"displayformat\", text=display_format)\n\n    return tc_element\n\n\ndef _build_item_timings(\n    item_e,\n    item,\n    timeline_range,\n    transition_offsets,\n    timecode\n):\n    # source_start is absolute time taking into account the timecode of the\n    # media. But xml regards the source in point from the start of the media.\n    # So we subtract the media timecode.\n    item_rate = item.source_range.start_time.rate\n    source_start = (item.source_range.start_time - timecode)\n    source_start = source_start.rescaled_to(item_rate)\n\n    source_end = (item.source_range.end_time_exclusive() - timecode)\n    source_end = source_end.rescaled_to(item_rate)\n\n    start = '{:.0f}'.format(timeline_range.start_time.value)\n    end = '{:.0f}'.format(timeline_range.end_time_exclusive().value)\n\n    item_e.append(_build_rate(item_rate))\n\n    if transition_offsets[0] is not None:\n        start = '-1'\n        source_start -= transition_offsets[0]\n    if transition_offsets[1] is not None:\n        end = '-1'\n        source_end += transition_offsets[1]\n\n    _append_new_sub_element(\n        item_e, 'duration',\n        text='{:.0f}'.format(item.source_range.duration.value)\n    )\n    _append_new_sub_element(item_e, 'start', text=start)\n    _append_new_sub_element(item_e, 'end', text=end)\n    _append_new_sub_element(\n        item_e,\n        'in',\n        text='{:.0f}'.format(source_start.value)\n    )\n    _append_new_sub_element(\n        item_e,\n        'out',\n        text='{:.0f}'.format(source_end.value)\n    )\n\n\n@_backreference_build('file')\ndef _build_empty_file(media_ref, parent_range, br_map):\n    file_e = _element_with_item_metadata(\"file\", media_ref)\n    _append_new_sub_element(file_e, \"name\", text=media_ref.name)\n\n    if media_ref.available_range is not None:\n        available_range = media_ref.available_range\n    else:\n        available_range = opentime.TimeRange(\n            opentime.RationalTime(0, parent_range.start_time.rate),\n            parent_range.duration,\n        )\n\n    ref_rate = available_range.start_time.rate\n    file_e.append(_build_rate(ref_rate))\n\n    # Only provide a duration if one came from the media, don't invent one.\n    # For example, Slugs have no duration specified.\n    if media_ref.available_range:\n        duration = available_range.duration.rescaled_to(ref_rate)\n        _append_new_sub_element(\n            file_e,\n            'duration',\n            text='{:.0f}'.format(duration.value),\n        )\n\n    # timecode\n    ref_tc_metadata = media_ref.metadata.get(META_NAMESPACE, {}).get(\n        \"timecode\"\n    )\n    tc_element = _build_timecode_from_metadata(\n        available_range.start_time, ref_tc_metadata\n    )\n    file_e.append(tc_element)\n\n    file_media_e = _get_or_create_subelement(file_e, \"media\")\n    if file_media_e.find(\"video\") is None:\n        _append_new_sub_element(file_media_e, \"video\")\n\n    return file_e\n\n\n@_backreference_build('file')\ndef _build_file(media_reference, br_map):\n    file_e = _element_with_item_metadata(\"file\", media_reference)\n\n    available_range = media_reference.available_range\n    url_path = _url_to_path(media_reference.target_url)\n\n    file_name = (\n        media_reference.name if media_reference.name\n        else os.path.basename(url_path)\n    )\n    _append_new_sub_element(file_e, 'name', text=file_name)\n    _append_new_sub_element(file_e, 'pathurl', text=media_reference.target_url)\n\n    # timing info\n    file_e.append(_build_rate(available_range.start_time.rate))\n    _append_new_sub_element(\n        file_e, 'duration',\n        text='{:.0f}'.format(available_range.duration.value)\n    )\n\n    # timecode\n    ref_tc_metadata = media_reference.metadata.get(META_NAMESPACE, {}).get(\n        \"timecode\"\n    )\n    tc_element = _build_timecode_from_metadata(\n        available_range.start_time, ref_tc_metadata\n    )\n    file_e.append(tc_element)\n\n    # we need to flag the file reference with the content types, otherwise it\n    # will not get recognized\n    # TODO: We should use a better method for this. Perhaps pre-walk the\n    #       timeline and find all the track kinds this media is present in?\n    if not file_e.find(\"media\"):\n        file_media_e = _get_or_create_subelement(file_e, \"media\")\n\n        audio_exts = {'.wav', '.aac', '.mp3', '.aif', '.aiff', '.m4a'}\n        has_video = (os.path.splitext(url_path)[1].lower() not in audio_exts)\n        if has_video and file_media_e.find(\"video\") is None:\n            _append_new_sub_element(file_media_e, \"video\")\n\n        # TODO: This is assuming all files have an audio track. Not sure what\n        # the implications of that are.\n        if file_media_e.find(\"audio\") is None:\n            _append_new_sub_element(file_media_e, \"audio\")\n\n    return file_e\n\n\ndef _build_transition_item(\n    transition_item,\n    timeline_range,\n    transition_offsets,\n    br_map,\n):\n    transition_e = _element_with_item_metadata(\n        \"transitionitem\", transition_item\n    )\n    _append_new_sub_element(\n        transition_e,\n        'start',\n        text='{:.0f}'.format(timeline_range.start_time.value)\n    )\n    _append_new_sub_element(\n        transition_e,\n        'end',\n        text='{:.0f}'.format(timeline_range.end_time_exclusive().value)\n    )\n\n    # Only add an alignment if it didn't already come in from the metadata dict\n    if transition_e.find(\"alignment\") is None:\n        # default center aligned\n        alignment = \"center\"\n        if not transition_item.in_offset.value:\n            alignment = 'start-black'\n        elif not transition_item.out_offset.value:\n            alignment = 'end-black'\n\n        _append_new_sub_element(transition_e, 'alignment', text=alignment)\n        # todo support 'start' and 'end' alignment\n\n    transition_e.append(_build_rate(timeline_range.start_time.rate))\n\n    # Only add an effect if it didn't already come in from the metadata dict\n    if not transition_e.find(\"./effect\"):\n        try:\n            effectid = transition_item.metadata[META_NAMESPACE][\"effectid\"]\n        except KeyError:\n            effectid = \"Cross Dissolve\"\n\n        effect_e = _append_new_sub_element(transition_e, 'effect')\n        _append_new_sub_element(effect_e, 'name', text=transition_item.name)\n        _append_new_sub_element(effect_e, 'effectid', text=effectid)\n        _append_new_sub_element(effect_e, 'effecttype', text='transition')\n        _append_new_sub_element(effect_e, 'mediatype', text='video')\n\n    return transition_e\n\n\n@_backreference_build(\"clipitem\")\ndef _build_clip_item_without_media(\n    clip_item,\n    timeline_range,\n    transition_offsets,\n    br_map,\n):\n    # TODO: Does this need to be a separate function or could it be unified\n    #       with _build_clip_item?\n    clip_item_e = _element_with_item_metadata(\"clipitem\", clip_item)\n    if \"frameBlend\" not in clip_item_e.attrib:\n        clip_item_e.attrib[\"frameBlend\"] = \"FALSE\"\n\n    if clip_item.media_reference.available_range:\n        media_start_time = clip_item.media_reference.available_range.start_time\n    else:\n        media_start_time = opentime.RationalTime(\n            0, timeline_range.start_time.rate\n        )\n\n    _append_new_sub_element(clip_item_e, 'name', text=clip_item.name)\n    clip_item_e.append(\n        _build_empty_file(\n            clip_item.media_reference, timeline_range, br_map\n        )\n    )\n    clip_item_e.extend([_build_marker(m) for m in clip_item.markers])\n\n    _build_item_timings(\n        clip_item_e,\n        clip_item,\n        timeline_range,\n        transition_offsets,\n        media_start_time,\n    )\n\n    return clip_item_e\n\n\n@_backreference_build(\"clipitem\")\ndef _build_clip_item(clip_item, timeline_range, transition_offsets, br_map):\n    is_generator = isinstance(\n        clip_item.media_reference, schema.GeneratorReference\n    )\n\n    tagname = \"generatoritem\" if is_generator else \"clipitem\"\n    clip_item_e = _element_with_item_metadata(tagname, clip_item)\n    if \"frameBlend\" not in clip_item_e.attrib:\n        clip_item_e.attrib[\"frameBlend\"] = \"FALSE\"\n\n    if is_generator:\n        clip_item_e.append(_build_generator_effect(clip_item, br_map))\n    else:\n        clip_item_e.append(_build_file(clip_item.media_reference, br_map))\n\n    # set the clip name from the media reference if not defined on the clip\n    if clip_item.name is not None:\n        name = clip_item.name\n    elif is_generator:\n        name = clip_item.media_reference.name\n    else:\n        url_path = _url_to_path(clip_item.media_reference.target_url)\n        name = os.path.basename(url_path)\n\n    _append_new_sub_element(clip_item_e, 'name', text=name)\n\n    if clip_item.media_reference.available_range:\n        clip_item_e.append(\n            _build_rate(clip_item.source_range.start_time.rate)\n        )\n    clip_item_e.extend(_build_marker(m) for m in clip_item.markers)\n\n    if clip_item.media_reference.available_range:\n        timecode = clip_item.media_reference.available_range.start_time\n    else:\n        timecode = opentime.RationalTime(\n            0, clip_item.source_range.start_time.rate\n        )\n\n    _build_item_timings(\n        clip_item_e,\n        clip_item,\n        timeline_range,\n        transition_offsets,\n        timecode\n    )\n\n    return clip_item_e\n\n\ndef _build_generator_effect(clip_item, br_map):\n    \"\"\"\n    Builds an effect element for the generator ref on the provided clip item.\n\n    :param clip_item: a clip with a :class:`schema.GeneratorReference` as\n        its ``media_reference``.\n    :param br_map: The backreference map.\n    \"\"\"\n    # Since we don't support effects in a standard way, just try and build\n    # based on the metadata provided at deserialization so we can roundtrip\n    generator_ref = clip_item.media_reference\n    try:\n        fcp_xml_effect_info = generator_ref.metadata[META_NAMESPACE]\n    except KeyError:\n        return _build_empty_file(\n            generator_ref,\n            clip_item.source_range,\n            br_map,\n        )\n\n    # Get the XML Tree built from the metadata\n    effect_element = _dict_to_xml_tree(fcp_xml_effect_info, \"effect\")\n\n    # Validate the metadata and make sure it contains the required elements\n    for required in (\"effectid\", \"effecttype\", \"mediatype\", \"effectcategory\"):\n        if effect_element.find(required) is None:\n            return _build_empty_file(\n                generator_ref,\n                clip_item.source_range,\n                br_map,\n            )\n\n    # Add the name\n    _append_new_sub_element(effect_element, \"name\", text=generator_ref.name)\n\n    return effect_element\n\n\n@_backreference_build(\"clipitem\")\ndef _build_track_item(track, timeline_range, transition_offsets, br_map):\n    clip_item_e = _element_with_item_metadata(\"clipitem\", track)\n    if \"frameBlend\" not in clip_item_e.attrib:\n        clip_item_e.attrib[\"frameBlend\"] = \"FALSE\"\n\n    _append_new_sub_element(\n        clip_item_e,\n        'name',\n        text=os.path.basename(track.name)\n    )\n\n    track_e = _build_sequence_for_stack(track, timeline_range, br_map)\n\n    clip_item_e.append(_build_rate(track.source_range.start_time.rate))\n    clip_item_e.extend([_build_marker(m) for m in track.markers])\n    clip_item_e.append(track_e)\n    timecode = opentime.RationalTime(0, timeline_range.start_time.rate)\n\n    _build_item_timings(\n        clip_item_e,\n        track,\n        timeline_range,\n        transition_offsets,\n        timecode\n    )\n\n    return clip_item_e\n\n\ndef _build_item(item, timeline_range, transition_offsets, br_map):\n    if isinstance(item, schema.Transition):\n        return _build_transition_item(\n            item,\n            timeline_range,\n            transition_offsets,\n            br_map\n        )\n    elif isinstance(item, schema.Clip):\n        if isinstance(\n            item.media_reference,\n            schema.MissingReference\n        ):\n            return _build_clip_item_without_media(\n                item,\n                timeline_range,\n                transition_offsets,\n                br_map\n            )\n        else:\n            return _build_clip_item(\n                item,\n                timeline_range,\n                transition_offsets,\n                br_map\n            )\n    elif isinstance(item, schema.Stack):\n        return _build_track_item(\n            item,\n            timeline_range,\n            transition_offsets,\n            br_map\n        )\n    else:\n        raise ValueError('Unsupported item: ' + str(item))\n\n\ndef _build_top_level_track(track, track_rate, br_map):\n    track_e = _element_with_item_metadata(\"track\", track)\n\n    for n, item in enumerate(track):\n        if isinstance(item, schema.Gap):\n            continue\n\n        transition_offsets = [None, None]\n        previous_item = track[n - 1] if n > 0 else None\n        next_item = track[n + 1] if n + 1 < len(track) else None\n        if not isinstance(item, schema.Transition):\n            # find out if this item has any neighboring transition\n            if isinstance(previous_item, schema.Transition):\n                if previous_item.out_offset.value:\n                    transition_offsets[0] = previous_item.in_offset\n                else:\n                    transition_offsets[0] = None\n            if isinstance(next_item, schema.Transition):\n                if next_item.in_offset.value:\n                    transition_offsets[1] = next_item.out_offset\n                else:\n                    transition_offsets[1] = None\n\n        timeline_range = track.range_of_child_at_index(n)\n        timeline_range = opentime.TimeRange(\n            timeline_range.start_time.rescaled_to(track_rate),\n            timeline_range.duration.rescaled_to(track_rate)\n        )\n        track_e.append(\n            _build_item(item, timeline_range, transition_offsets, br_map)\n        )\n\n    return track_e\n\n\ndef _build_marker(marker):\n    marker_e = _element_with_item_metadata(\"marker\", marker)\n\n    marked_range = marker.marked_range\n\n    _append_new_sub_element(marker_e, 'name', text=marker.name)\n    _append_new_sub_element(\n        marker_e, 'in',\n        text='{:.0f}'.format(marked_range.start_time.value)\n    )\n    _append_new_sub_element(marker_e, 'out', text='-1')\n\n    return marker_e\n\n\ndef _build_timecode_from_metadata(time, tc_metadata=None):\n    \"\"\"\n    Makes a timecode element with the given time and (if available)\n    ```timecode`` metadata stashed on input.\n\n    :param time: The :class:`opentime.RationalTime` to encode.\n    :param tc_metadata: The xml dict for the ``timecode`` element populated\n        on read.\n\n    :return: A timecode element.\n    \"\"\"\n    if tc_metadata is None:\n        tc_metadata = {}\n\n    try:\n        # Parse the rate in the preserved metadata, if available\n        tc_rate = _rate_for_element(\n            _dict_to_xml_tree(tc_metadata[\"rate\"], \"rate\")\n        )\n    except KeyError:\n        # Default to the rate in the start time\n        tc_rate = time.rate\n\n    drop_frame = (tc_metadata.get(\"displayformat\", \"NDF\") == \"DF\")\n\n    return _build_timecode(\n        time,\n        tc_rate,\n        drop_frame,\n        additional_metadata=tc_metadata,\n    )\n\n\n@_backreference_build('sequence')\ndef _build_sequence_for_timeline(timeline, timeline_range, br_map):\n    sequence_e = _element_with_item_metadata(\"sequence\", timeline)\n\n    _add_stack_elements_to_sequence(\n        timeline.tracks, sequence_e, timeline_range, br_map\n    )\n\n    # In the case of timelines, use the timeline name rather than the stack\n    # name.\n    if timeline.name:\n        sequence_e.find('./name').text = timeline.name\n\n    # Add the sequence global start\n    if timeline.global_start_time is not None:\n        seq_tc_metadata = timeline.metadata.get(META_NAMESPACE, {}).get(\n            \"timecode\"\n        )\n        tc_element = _build_timecode_from_metadata(\n            timeline.global_start_time, seq_tc_metadata\n        )\n        sequence_e.append(tc_element)\n\n    return sequence_e\n\n\n@_backreference_build('sequence')\ndef _build_sequence_for_stack(stack, timeline_range, br_map):\n    sequence_e = _element_with_item_metadata(\"sequence\", stack)\n\n    _add_stack_elements_to_sequence(stack, sequence_e, timeline_range, br_map)\n\n    return sequence_e\n\n\ndef _add_stack_elements_to_sequence(stack, sequence_e, timeline_range, br_map):\n    _append_new_sub_element(sequence_e, 'name', text=stack.name)\n    _append_new_sub_element(\n        sequence_e, 'duration',\n        text='{:.0f}'.format(timeline_range.duration.value)\n    )\n    sequence_e.append(_build_rate(timeline_range.start_time.rate))\n    track_rate = timeline_range.start_time.rate\n\n    media_e = _get_or_create_subelement(sequence_e, \"media\")\n    video_e = _get_or_create_subelement(media_e, 'video')\n    audio_e = _get_or_create_subelement(media_e, 'audio')\n\n    for track in stack:\n        track_elements = _build_top_level_track(track, track_rate, br_map)\n        if track.kind == schema.TrackKind.Video:\n            video_e.append(track_elements)\n        elif track.kind == schema.TrackKind.Audio:\n            audio_e.append(track_elements)\n\n    for marker in stack.markers:\n        sequence_e.append(_build_marker(marker))\n\n\ndef _build_collection(collection, br_map):\n    tracks = []\n    for item in collection:\n        if not isinstance(item, schema.Timeline):\n            continue\n\n        timeline_range = opentime.TimeRange(\n            start_time=item.global_start_time,\n            duration=item.duration()\n        )\n        tracks.append(\n            _build_sequence_for_timeline(item, timeline_range, br_map)\n        )\n\n    return tracks\n\n\n# --------------------\n# adapter requirements\n# --------------------\n\ndef read_from_string(input_str):\n    tree = cElementTree.fromstring(input_str)\n\n    parser = FCP7XMLParser(tree)\n    sequences = parser.top_level_sequences()\n\n    if len(sequences) == 1:\n        return sequences[0]\n    elif len(sequences) > 1:\n        return schema.SerializableCollection(\n            name=\"Sequences\",\n            children=sequences,\n        )\n    else:\n        raise ValueError('No top-level sequences found')\n\n\ndef write_to_string(input_otio):\n    tree_e = cElementTree.Element('xmeml', version=\"4\")\n    project_e = _append_new_sub_element(tree_e, 'project')\n    _append_new_sub_element(project_e, 'name', text=input_otio.name)\n    children_e = _append_new_sub_element(project_e, 'children')\n\n    br_map = collections.defaultdict(dict)\n\n    if isinstance(input_otio, schema.Timeline):\n        timeline_range = opentime.TimeRange(\n            start_time=input_otio.global_start_time,\n            duration=input_otio.duration()\n        )\n        children_e.append(\n            _build_sequence_for_timeline(\n                input_otio, timeline_range, br_map\n            )\n        )\n    elif isinstance(input_otio, schema.SerializableCollection):\n        children_e.extend(\n            _build_collection(input_otio, br_map)\n        )\n\n    return _make_pretty_string(tree_e)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/adapters/otio_json.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"This adapter lets you read and write native .otio files\"\"\"\n\nfrom .. import (\n    core\n)\n\n\n# @TODO: Implement out of process plugins that hand around JSON\n\n\ndef read_from_file(filepath):\n    return core.deserialize_json_from_file(filepath)\n\n\ndef read_from_string(input_str):\n    return core.deserialize_json_from_string(input_str)\n\n\ndef write_to_string(input_otio):\n    return core.serialize_json_to_string(input_otio)\n\n\ndef write_to_file(input_otio, filepath):\n    return core.serialize_json_to_file(input_otio, filepath)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/algorithms/__init__.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Algorithms for OTIO objects.\"\"\"\n\n# flake8: noqa\nfrom .track_algo import (\n    track_trimmed_to_range,\n    track_with_expanded_transitions\n)\n\nfrom .stack_algo import (\n    flatten_stack,\n    top_clip_at_time,\n)\n\nfrom .filter import (\n    filtered_composition,\n    filtered_with_sequence_context\n)\nfrom .timeline_algo import (\n    timeline_trimmed_to_range\n)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/algorithms/filter.py",
    "content": "#!/usr/bin/env python\n#\n# Copyright 2018 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Algorithms for filtering OTIO files.  \"\"\"\n\nimport copy\n\nfrom .. import (\n    schema\n)\n\n\ndef _is_in(thing, container):\n    return any(thing is item for item in container)\n\n\ndef _isinstance_in(child, typelist):\n    return any(isinstance(child, t) for t in typelist)\n\n\ndef filtered_composition(\n    root,\n    unary_filter_fn,\n    types_to_prune=None,\n):\n    \"\"\"Filter a deep copy of root (and children) with unary_filter_fn.\n\n    types_to_prune:: tuple of types, example: (otio.schema.Gap,...)\n\n    1. Make a deep copy of root\n    2. Starting with root, perform a depth first traversal\n    3. For each item (including root):\n        a. if types_to_prune is not None and item is an instance of a type\n            in types_to_prune, prune it from the copy, continue.\n        b. Otherwise, pass the copy to unary_filter_fn.  If unary_filter_fn:\n            I.   returns an object: add it to the copy, replacing original\n            II.  returns a tuple: insert it into the list, replacing original\n            III. returns None: prune it\n    4. If an item is pruned, do not traverse its children\n    5. Return the new deep copy.\n\n    EXAMPLE 1 (filter):\n        If your unary function is:\n            def fn(thing):\n                if thing.name == B:\n                    return thing' # some transformation of B\n                else:\n                    return thing\n\n        If you have a track: [A,B,C]\n\n        filtered_composition(track, fn) => [A,B',C]\n\n    EXAMPLE 2 (prune):\n        If your unary function is:\n            def fn(thing):\n                if thing.name == B:\n                    return None\n                else:\n                    return thing\n\n        filtered_composition(track, fn) => [A,C]\n\n    EXAMPLE 3 (expand):\n        If your unary function is:\n            def fn(thing):\n                if thing.name == B:\n                    return tuple(B_1,B_2,B_3)\n                else:\n                    return thing\n\n        filtered_composition(track, fn) => [A,B_1,B_2,B_3,C]\n\n    EXAMPLE 4 (prune gaps):\n        track :: [Gap, A, Gap]\n            filtered_composition(\n                track, lambda _:_, types_to_prune=(otio.schema.Gap,)) => [A]\n    \"\"\"\n\n    # deep copy everything\n    mutable_object = copy.deepcopy(root)\n\n    prune_list = set()\n\n    header_list = [mutable_object]\n\n    if isinstance(mutable_object, schema.Timeline):\n        header_list.append(mutable_object.tracks)\n\n    iter_list = header_list + list(mutable_object.each_child())\n\n    for child in iter_list:\n        if _safe_parent(child) is not None and _is_in(child.parent(), prune_list):\n            prune_list.add(child)\n            continue\n\n        parent = None\n        child_index = None\n        if _safe_parent(child) is not None:\n            child_index = child.parent().index(child)\n            parent = child.parent()\n            del child.parent()[child_index]\n\n        # first try to prune\n        if (types_to_prune and _isinstance_in(child, types_to_prune)):\n            result = None\n        # finally call the user function\n        else:\n            result = unary_filter_fn(child)\n\n        if child is mutable_object:\n            mutable_object = result\n\n        if result is None:\n            prune_list.add(child)\n            continue\n\n        if type(result) is not tuple:\n            result = [result]\n\n        if parent is not None:\n            parent[child_index:child_index] = result\n\n    return mutable_object\n\n\ndef _safe_parent(child):\n    if hasattr(child, 'parent'):\n        return child.parent()\n    return None\n\n\ndef filtered_with_sequence_context(\n    root,\n    reduce_fn,\n    types_to_prune=None,\n):\n    \"\"\"Filter a deep copy of root (and children) with reduce_fn.\n\n    reduce_fn::function(previous_item, current, next_item) (see below)\n    types_to_prune:: tuple of types, example: (otio.schema.Gap,...)\n\n    1. Make a deep copy of root\n    2. Starting with root, perform a depth first traversal\n    3. For each item (including root):\n        a. if types_to_prune is not None and item is an instance of a type\n            in types_to_prune, prune it from the copy, continue.\n        b. Otherwise, pass (prev, copy, and next) to reduce_fn. If reduce_fn:\n            I.   returns an object: add it to the copy, replacing original\n            II.  returns a tuple: insert it into the list, replacing original\n            III. returns None: prune it\n\n            ** note that reduce_fn is always passed objects from the original\n                deep copy, not what prior calls return.  See below for examples\n    4. If an item is pruned, do not traverse its children\n    5. Return the new deep copy.\n\n    EXAMPLE 1 (filter):\n        >>> track = [A,B,C]\n        >>> def fn(prev_item, thing, next_item):\n        ...     if prev_item.name == A:\n        ...         return D # some new clip\n        ...     else:\n        ...         return thing\n        >>> filtered_with_sequence_context(track, fn) => [A,D,C]\n\n        order of calls to fn:\n            fn(None, A, B) => A\n            fn(A, B, C) => D\n            fn(B, C, D) => C # !! note that it was passed B instead of D.\n\n    EXAMPLE 2 (prune):\n        >>> track = [A,B,C]\n        >>> def fn(prev_item, thing, next_item):\n        ...    if prev_item.name == A:\n        ...        return None # prune the clip\n        ...   else:\n        ...        return thing\n        >>> filtered_with_sequence_context(track, fn) => [A,C]\n\n        order of calls to fn:\n            fn(None, A, B) => A\n            fn(A, B, C) => None\n            fn(B, C, D) => C # !! note that it was passed B instead of D.\n\n    EXAMPLE 3 (expand):\n        >>> def fn(prev_item, thing, next_item):\n        ...     if prev_item.name == A:\n        ...         return (D, E) # tuple of new clips\n        ...     else:\n        ...         return thing\n        >>> filtered_with_sequence_context(track, fn) => [A, D, E, C]\n\n         the order of calls to fn will be:\n            fn(None, A, B) => A\n            fn(A, B, C) => (D, E)\n            fn(B, C, D) => C # !! note that it was passed B instead of D.\n    \"\"\"\n\n    # deep copy everything\n    mutable_object = copy.deepcopy(root)\n\n    prune_list = set()\n\n    header_list = [mutable_object]\n\n    if isinstance(mutable_object, schema.Timeline):\n        header_list.append(mutable_object.tracks)\n\n    iter_list = header_list + list(mutable_object.each_child())\n\n    # expand to include prev, next when appropriate\n    expanded_iter_list = []\n    for child in iter_list:\n        if _safe_parent(child) and isinstance(child.parent(), schema.Track):\n            prev_item, next_item = child.parent().neighbors_of(child)\n            expanded_iter_list.append((prev_item, child, next_item))\n        else:\n            expanded_iter_list.append((None, child, None))\n\n    for prev_item, child, next_item in expanded_iter_list:\n        if _safe_parent(child) is not None and _is_in(child.parent(), prune_list):\n            prune_list.add(child)\n            continue\n\n        parent = None\n        child_index = None\n        if _safe_parent(child) is not None:\n            child_index = child.parent().index(child)\n            parent = child.parent()\n            del child.parent()[child_index]\n\n        # first try to prune\n        if types_to_prune and _isinstance_in(child, types_to_prune):\n            result = None\n        # finally call the user function\n        else:\n            result = reduce_fn(prev_item, child, next_item)\n\n        if child is mutable_object:\n            mutable_object = result\n\n        if result is None:\n            prune_list.add(child)\n            continue\n\n        if type(result) is not tuple:\n            result = [result]\n\n        if parent is not None:\n            parent[child_index:child_index] = result\n\n    return mutable_object\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/algorithms/stack_algo.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n__doc__ = \"\"\" Algorithms for stack objects. \"\"\"\n\nimport copy\n\nfrom .. import (\n    schema,\n    opentime,\n)\nfrom . import (\n    track_algo\n)\n\n\ndef top_clip_at_time(in_stack, t):\n    \"\"\"Return the topmost visible child that overlaps with time t.\n\n    Example:\n    tr1: G1, A, G2\n    tr2: [B------]\n    G1, and G2 are gaps, A and B are clips.\n\n    If t is within A, a will be returned.  If t is within G1 or G2, B will be\n    returned.\n    \"\"\"\n\n    # ensure that it only runs on stacks\n    if not isinstance(in_stack, schema.Stack):\n        raise ValueError(\n            \"Argument in_stack must be of type otio.schema.Stack, \"\n            \"not: '{}'\".format(\n                type(in_stack)\n            )\n        )\n\n    # build a range to use the `each_child`method.\n    search_range = opentime.TimeRange(\n        start_time=t,\n        # 0 duration so we are just sampling a point in time.\n        # XXX Should this duration be equal to the length of one sample?\n        #     opentime.RationalTime(1, rate)?\n        duration=opentime.RationalTime(0, t.rate)\n    )\n\n    # walk through the children of the stack in reverse order.\n    for track in reversed(in_stack):\n        valid_results = []\n        if hasattr(track, \"each_child\"):\n            valid_results = list(\n                c for c in track.each_clip(search_range, shallow_search=True)\n                if c.visible()\n            )\n\n        # XXX doesn't handle nested tracks/stacks at the moment\n\n        for result in valid_results:\n            return result\n\n    return None\n\n\ndef flatten_stack(in_stack):\n    \"\"\"Flatten a Stack, or a list of Tracks, into a single Track.\n    Note that the 1st Track is the bottom one, and the last is the top.\n    \"\"\"\n\n    flat_track = schema.Track()\n    flat_track.name = \"Flattened\"\n\n    # map of track to track.range_of_all_children\n    range_track_map = {}\n\n    def _get_next_item(\n            in_stack,\n            track_index=None,\n            trim_range=None\n    ):\n        if track_index is None:\n            # start with the top-most track\n            track_index = len(in_stack) - 1\n        if track_index < 0:\n            # if you get to the bottom, you're done\n            return\n\n        track = in_stack[track_index]\n        if trim_range is not None:\n            track = track_algo.track_trimmed_to_range(track, trim_range)\n\n        track_map = range_track_map.get(track)\n        if track_map is None:\n            track_map = track.range_of_all_children()\n            range_track_map[track] = track_map\n\n        for item in track:\n            if (\n                    item.visible()\n                    or track_index == 0\n                    or isinstance(item, schema.Transition)\n            ):\n                yield item\n            else:\n                trim = track_map[item]\n                if trim_range is not None:\n                    trim = opentime.TimeRange(\n                        start_time=trim.start_time + trim_range.start_time,\n                        duration=trim.duration\n                    )\n                    track_map[item] = trim\n                for more in _get_next_item(in_stack, track_index - 1, trim):\n                    yield more\n\n    for item in _get_next_item(in_stack):\n        flat_track.append(copy.deepcopy(item))\n\n    return flat_track\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/algorithms/timeline_algo.py",
    "content": "#\n# Copyright 2019 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Algorithms for timeline objects.\"\"\"\n\nimport copy\n\nfrom . import (\n    track_algo\n)\n\n\ndef timeline_trimmed_to_range(in_timeline, trim_range):\n    \"\"\"Returns a new timeline that is a copy of the in_timeline, but with items\n    outside the trim_range removed and items on the ends trimmed to the\n    trim_range. Note that the timeline is never expanded, only shortened.\n    Please note that you could do nearly the same thing non-destructively by\n    just setting the Track's source_range but sometimes you want to really cut\n    away the stuff outside and that's what this function is meant for.\"\"\"\n    new_timeline = copy.deepcopy(in_timeline)\n\n    for track_num, child_track in enumerate(in_timeline.tracks):\n        # @TODO: put the trim_range into the space of the tracks\n        # new_range = new_timeline.tracks.transformed_time_range(\n        #     trim_range,\n        #     child_track\n        # )\n\n        # trim the track and assign it to the new stack.\n        new_timeline.tracks[track_num] = track_algo.track_trimmed_to_range(\n            child_track,\n            trim_range\n        )\n\n    return new_timeline\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/algorithms/track_algo.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Algorithms for track objects.\"\"\"\n\nimport copy\n\nfrom .. import (\n    schema,\n    exceptions,\n    opentime,\n)\n\n\ndef track_trimmed_to_range(in_track, trim_range):\n    \"\"\"Returns a new track that is a copy of the in_track, but with items\n    outside the trim_range removed and items on the ends trimmed to the\n    trim_range. Note that the track is never expanded, only shortened.\n    Please note that you could do nearly the same thing non-destructively by\n    just setting the Track's source_range but sometimes you want to really cut\n    away the stuff outside and that's what this function is meant for.\"\"\"\n    new_track = copy.deepcopy(in_track)\n\n    track_map = new_track.range_of_all_children()\n\n    # iterate backwards so we can delete items\n    for c, child in reversed(list(enumerate(new_track))):\n        child_range = track_map[child]\n        if not trim_range.overlaps(child_range):\n            # completely outside the trim range, so we discard it\n            del new_track[c]\n        elif trim_range.contains(child_range):\n            # completely contained, keep the whole thing\n            pass\n        else:\n            if isinstance(child, schema.Transition):\n                raise exceptions.CannotTrimTransitionsError(\n                    \"Cannot trim in the middle of a Transition.\"\n                )\n\n            # we need to clip the end(s)\n            child_source_range = child.trimmed_range()\n\n            # should we trim the start?\n            if trim_range.start_time > child_range.start_time:\n                trim_amount = trim_range.start_time - child_range.start_time\n                child_source_range = opentime.TimeRange(\n                    start_time=child_source_range.start_time + trim_amount,\n                    duration=child_source_range.duration - trim_amount\n\n                )\n\n            # should we trim the end?\n            trim_end = trim_range.end_time_exclusive()\n            child_end = child_range.end_time_exclusive()\n            if trim_end < child_end:\n                trim_amount = child_end - trim_end\n                child_source_range = opentime.TimeRange(\n                    start_time=child_source_range.start_time,\n                    duration=child_source_range.duration - trim_amount\n\n                )\n\n            # set the new child's trims\n            child.source_range = child_source_range\n\n    return new_track\n\n\ndef track_with_expanded_transitions(in_track):\n    \"\"\"Expands transitions such that neighboring clips are trimmed into\n    regions of overlap.\n\n    For example, if your track is:\n        Clip1, T, Clip2\n\n    will return:\n        Clip1', Clip1_t, T, Clip2_t, Clip2'\n\n    Where Clip1' is the part of Clip1 not in the transition, Clip1_t is the\n    part inside the transition and so on.\n    \"\"\"\n\n    result_track = []\n\n    seq_iter = iter(in_track)\n    prev_thing = None\n    thing = next(seq_iter, None)\n    next_thing = next(seq_iter, None)\n\n    while thing is not None:\n        if isinstance(thing, schema.Transition):\n            result_track.append(_expand_transition(thing, in_track))\n        else:\n            # not a transition, but might be trimmed by one before or after\n            # in the track\n            pre_transition = None\n            next_transition = None\n\n            if isinstance(prev_thing, schema.Transition):\n                pre_transition = prev_thing\n\n            if isinstance(next_thing, schema.Transition):\n                next_transition = next_thing\n\n            result_track.append(\n                _trim_from_transitions(\n                    thing,\n                    pre=pre_transition,\n                    post=next_transition\n                )\n            )\n\n        # loop\n        prev_thing = thing\n        thing = next_thing\n        next_thing = next(seq_iter, None)\n\n    return result_track\n\n\ndef _expand_transition(target_transition, from_track):\n    \"\"\" Expand transitions into the portions of pre-and-post clips that\n    overlap with the transition.\n    \"\"\"\n\n    result = from_track.neighbors_of(\n        target_transition,\n        schema.NeighborGapPolicy.around_transitions\n    )\n\n    trx_duration = target_transition.in_offset + target_transition.out_offset\n\n    # make copies of the before and after, and modify their in/out points\n    pre = copy.deepcopy(result.previous)\n\n    if isinstance(pre, schema.Transition):\n        raise exceptions.TransitionFollowingATransitionError(\n            \"cannot put two transitions next to each other in a  track: \"\n            \"{}, {}\".format(\n                pre,\n                target_transition\n            )\n        )\n    if target_transition.in_offset is None:\n        raise RuntimeError(\n            \"in_offset is None on: {}\".format(target_transition)\n        )\n\n    if target_transition.out_offset is None:\n        raise RuntimeError(\n            \"out_offset is None on: {}\".format(target_transition)\n        )\n\n    pre.name = (pre.name or \"\") + \"_transition_pre\"\n\n    # ensure that pre.source_range is set, because it will get manipulated\n    tr = pre.trimmed_range()\n\n    pre.source_range = opentime.TimeRange(\n        start_time=(\n            tr.end_time_exclusive() - target_transition.in_offset\n        ),\n        duration=trx_duration.rescaled_to(\n            tr.start_time\n        )\n    )\n\n    post = copy.deepcopy(result.next)\n    if isinstance(post, schema.Transition):\n        raise exceptions.TransitionFollowingATransitionError(\n            \"cannot put two transitions next to each other in a  track: \"\n            \"{}, {}\".format(\n                target_transition,\n                post\n            )\n        )\n\n    post.name = (post.name or \"\") + \"_transition_post\"\n\n    # ensure that post.source_range is set, because it will get manipulated\n    tr = post.trimmed_range()\n\n    post.source_range = opentime.TimeRange(\n        start_time=(\n            tr.start_time - target_transition.in_offset\n        ).rescaled_to(tr.start_time),\n        duration=trx_duration.rescaled_to(tr.start_time)\n    )\n\n    return pre, target_transition, post\n\n\ndef _trim_from_transitions(thing, pre=None, post=None):\n    \"\"\" Trim clips next to transitions. \"\"\"\n\n    result = copy.deepcopy(thing)\n\n    # We might not have a source_range yet,\n    # We can trim to the computed trimmed_range to\n    # ensure we have something.\n    new_range = result.trimmed_range()\n    start_time = new_range.start_time\n    duration = new_range.duration\n\n    if pre:\n        start_time += pre.out_offset\n        duration -= pre.out_offset\n\n    if post:\n        duration -= post.in_offset\n\n    result.source_range = opentime.TimeRange(start_time, duration)\n\n    return result\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/console/__init__.py",
    "content": "#\n# Copyright 2018 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Console scripts for OpenTimelineIO\n\n.. moduleauthor:: Pixar Animation Studios <opentimelineio@pixar.com>\n\"\"\"\n\n# flake8: noqa\n\n# in dependency hierarchy\nfrom . import (\n    otioconvert,\n    otiocat,\n    otiostat,\n    console_utils,\n    autogen_serialized_datamodel,\n)\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/console/autogen_serialized_datamodel.py",
    "content": "#!/usr/bin/env python\n#\n# Copyright 2019 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\n\"\"\"Generates documentation of the serialized data model for OpenTimelineIO.\"\"\"\n\nimport argparse\nimport inspect\nimport json\nimport tempfile\nimport sys\n\ntry:\n    # python2\n    import StringIO as io\nexcept ImportError:\n    # python3\n    import io\n\nimport opentimelineio as otio\n\n\nDOCUMENT_HEADER = \"\"\"# OpenTimelineIO Serialized Data Documentation\n\nThis document is a list of all the OpenTimelineIO classes that serialize to and\nfrom JSON, omitting SchemaDef plugins.\n\nThis document is automatically generated by running\n docs/autogen_serialized_datamodel.py, or by running `make doc-model`.  It is\n part of the unit tests suite and should be updated whenever the schema changes.\n If it needs to be updated, run: `make doc-model-update` and this file should be\n regenerated.\n\n# Classes\n\n\"\"\"\n\nFIELDS_ONLY_HEADER = \"\"\"# OpenTimelineIO Serialized Data Documentation\n\nThis document is a list of all the OpenTimelineIO classes that serialize to and\nfrom JSON, omitting plugins classes and docstrings.\n\nThis document is automatically generated by running\n docs/autogen_serialized_datamodel.py, or by running `make doc-model`.  It is\n part of the unit tests suite and should be updated whenever the schema changes.\n If it needs to be updated, run: `make doc-model-update` and this file should be\n regenerated.\n\n# Classes\n\n\"\"\"\n\nCLASS_HEADER_WITH_DOCS = \"\"\"\n### {classname}\n\n*full module path*: `{modpath}`\n\n*documentation*:\n\n```\n{docstring}\n```\n\nparameters:\n\"\"\"\n\nCLASS_HEADER_ONLY_FIELDS = \"\"\"\n### {classname}\n\nparameters:\n\"\"\"\n\nMODULE_HEADER = \"\"\"\n## Module: {modname}\n\"\"\"\n\nPROP_HEADER = \"\"\"- *{propkey}*: {prophelp}\n\"\"\"\n\n# @TODO: having type information here would be awesome\nPROP_HEADER_NO_HELP = \"\"\"- *{propkey}*\n\"\"\"\n\n# three ways to try and get the property + docstring\nPROP_FETCHERS = (\n    lambda cl, k: inspect.getdoc(getattr(cl, k)),\n    lambda cl, k: inspect.getdoc(getattr(cl, \"_\" + k)),\n    lambda cl, k: inspect.getdoc(getattr(cl(), k)) and \"\" or \"\",\n)\n\n\ndef _parsed_args():\n    \"\"\" parse commandline arguments with argparse \"\"\"\n\n    parser = argparse.ArgumentParser(\n        description=__doc__,\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter\n    )\n    group = parser.add_mutually_exclusive_group()\n    group.add_argument(\n        \"-d\",\n        \"--dryrun\",\n        action=\"store_true\",\n        default=False,\n        help=\"Dryrun mode - print out instead of perform actions\"\n    )\n    group.add_argument(\n        \"-o\",\n        \"--output\",\n        type=str,\n        default=None,\n        help=\"Update the baseline with the current version\"\n    )\n\n    return parser.parse_args()\n\n\n# things to skip\nSKIP_CLASSES = [otio.core.SerializableObject, otio.core.UnknownSchema]\nSKIP_KEYS = [\"OTIO_SCHEMA\"]  # not data, just for the backing format\nSKIP_MODULES = [\"opentimelineio.schemadef\"]  # because these are plugins\n\n\ndef _generate_model_for_module(mod, classes, modules):\n    modules.add(mod)\n\n    # fetch the classes from this module\n    serializeable_classes = [\n        thing for thing in mod.__dict__.values()\n        if (\n            inspect.isclass(thing)\n            and thing not in classes\n            and issubclass(thing, otio.core.SerializableObject)\n            or thing in (\n                otio.opentime.RationalTime,\n                otio.opentime.TimeRange,\n                otio.opentime.TimeTransform,\n            )\n        )\n    ]\n\n    # serialize/deserialize the classes to capture their serialized parameters\n    model = {}\n    for cl in serializeable_classes:\n        if cl in SKIP_CLASSES:\n            continue\n\n        model[cl] = {}\n        field_dict = json.loads(otio.adapters.otio_json.write_to_string(cl()))\n        for k in field_dict.keys():\n            if k in SKIP_KEYS:\n                continue\n\n            for fetcher in PROP_FETCHERS:\n                try:\n                    model[cl][k] = fetcher(cl, k)\n                    break\n                except AttributeError:\n                    pass\n            else:\n                sys.stderr.write(\"ERROR: could not fetch property: {}\".format(k))\n\n        # Stashing the OTIO_SCHEMA back into the dictionary since the\n        # documentation uses this information in its header.\n        model[cl][\"OTIO_SCHEMA\"] = field_dict[\"OTIO_SCHEMA\"]\n\n    classes.update(model)\n\n    # find new modules to recurse into\n    new_mods = sorted(\n        (\n            thing for thing in mod.__dict__.values()\n            if (\n                inspect.ismodule(thing)\n                and thing not in modules\n                and all(not thing.__name__.startswith(t) for t in SKIP_MODULES)\n            )\n        ),\n        key=lambda mod: str(mod)\n    )\n\n    # recurse into the new modules and update the classes and modules values\n    [_generate_model_for_module(m, classes, modules) for m in new_mods]\n\n\ndef _generate_model():\n    classes = {}\n    modules = set()\n    _generate_model_for_module(otio, classes, modules)\n    return classes\n\n\ndef _write_documentation(model):\n    md_with_helpstrings = io.StringIO()\n    md_only_fields = io.StringIO()\n\n    md_with_helpstrings.write(DOCUMENT_HEADER)\n    md_only_fields.write(FIELDS_ONLY_HEADER)\n\n    modules = {}\n    for cl in model:\n        modules.setdefault(cl.__module__, []).append(cl)\n\n    CURRENT_MODULE = None\n    for module_list in sorted(modules):\n        this_mod = \".\".join(module_list.split('.')[:2])\n        if this_mod != CURRENT_MODULE:\n            CURRENT_MODULE = this_mod\n            md_with_helpstrings.write(MODULE_HEADER.format(modname=this_mod))\n            md_only_fields.write(MODULE_HEADER.format(modname=this_mod))\n\n        # because these are classes, they need to sort on their stringified\n        # names\n        for cl in sorted(modules[module_list], key=lambda cl: str(cl)):\n            modname = inspect.getmodule(cl).__name__\n            label = model[cl][\"OTIO_SCHEMA\"]\n            md_with_helpstrings.write(\n                CLASS_HEADER_WITH_DOCS.format(\n                    classname=label,\n                    modpath=modname + \".\" + cl.__name__,\n                    docstring=cl.__doc__\n                )\n            )\n            md_only_fields.write(\n                CLASS_HEADER_ONLY_FIELDS.format(\n                    classname=label,\n                )\n            )\n\n            for key, helpstr in sorted(model[cl].items()):\n                if key in SKIP_KEYS:\n                    continue\n                md_with_helpstrings.write(\n                    PROP_HEADER.format(propkey=key, prophelp=helpstr)\n                )\n                md_only_fields.write(\n                    PROP_HEADER_NO_HELP.format(propkey=key)\n                )\n\n    return md_with_helpstrings.getvalue(), md_only_fields.getvalue()\n\n\ndef main():\n    \"\"\"  main entry point  \"\"\"\n    args = _parsed_args()\n    with_docs, without_docs = generate_and_write_documentation()\n\n    # print it out somewhere\n    if args.dryrun:\n        print(with_docs)\n        return\n\n    output = args.output\n    if not output:\n        output = tempfile.NamedTemporaryFile(\n            'w',\n            suffix=\"otio_serialized_schema.md\",\n            delete=False\n        ).name\n\n    with open(output, 'w') as fo:\n        fo.write(with_docs)\n\n    # write version without docstrings\n    prefix, suffix = output.rsplit('.', 1)\n    output_only_fields = prefix + \"-only-fields.\" + suffix\n\n    with open(output_only_fields, 'w') as fo:\n        fo.write(without_docs)\n\n    print(\"wrote documentation to {} and {}\".format(output, output_only_fields))\n\n\ndef generate_and_write_documentation():\n    model = _generate_model()\n    return _write_documentation(model)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/console/console_utils.py",
    "content": "#\n# Copyright 2019 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\nimport ast\n\nfrom .. import (\n    media_linker,\n)\n\n\"\"\"Utilities for OpenTimelineIO commandline modules.\"\"\"\n\n\ndef arg_list_to_map(arg_list, label):\n    \"\"\"\n    Convert an argument of the form -A foo=bar from the parsed result to a map.\n    \"\"\"\n\n    argument_map = {}\n    for pair in arg_list:\n        if '=' not in pair:\n            raise ValueError(\n                \"error: {} arguments must be in the form key=value\"\n                \" got: {}\".format(label, pair)\n            )\n\n        key, val = pair.split('=', 1)  # only split on the 1st '='\n        try:\n            # Sometimes we need to pass a bool, int, list, etc.\n            parsed_value = ast.literal_eval(val)\n        except (ValueError, SyntaxError):\n            # Fall back to a simple string\n            parsed_value = val\n        argument_map[key] = parsed_value\n\n    return argument_map\n\n\ndef media_linker_name(ml_name_arg):\n    \"\"\"\n    Parse commandline arguments for the media linker, which can be not set\n    (fall back to default), \"\" or \"none\" (don't link media) or the name of a\n    media linker to use.\n    \"\"\"\n    if ml_name_arg.lower() == 'default':\n        media_linker_name = media_linker.MediaLinkingPolicy.ForceDefaultLinker\n    elif ml_name_arg.lower() in ['none', '']:\n        media_linker_name = media_linker.MediaLinkingPolicy.DoNotLinkMedia\n    else:\n        media_linker_name = ml_name_arg\n\n    return media_linker_name\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/console/otiocat.py",
    "content": "#!/usr/bin/env python\n#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Print the contents of an OTIO file to stdout.\"\"\"\n\nimport argparse\nimport sys\n\nimport opentimelineio as otio\n\n\ndef _parsed_args():\n    \"\"\" parse commandline arguments with argparse \"\"\"\n\n    parser = argparse.ArgumentParser(\n        description=__doc__,\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter\n    )\n    parser.add_argument(\n        'filepath',\n        type=str,\n        nargs='+',\n        help='files to print the contents of'\n    )\n    parser.add_argument(\n        '-a',\n        '--adapter-arg',\n        type=str,\n        default=[],\n        action='append',\n        help='Extra arguments to be passed to input adapter in the form of '\n        'key=value. Values are strings, numbers or Python literals: True, '\n        'False, etc. Can be used multiple times: -a burrito=\"bar\" -a taco=12.'\n    )\n    parser.add_argument(\n        '-m',\n        '--media-linker',\n        type=str,\n        default=\"Default\",\n        help=(\n            \"Specify a media linker.  'Default' means use the \"\n            \"$OTIO_DEFAULT_MEDIA_LINKER if set, 'None' or '' means explicitly \"\n            \"disable the linker, and anything else is interpreted as the name\"\n            \" of the media linker to use.\"\n        )\n    )\n    parser.add_argument(\n        '-M',\n        '--media-linker-arg',\n        type=str,\n        default=[],\n        action='append',\n        help='Extra arguments to be passed to the media linker in the form of '\n        'key=value. Values are strings, numbers or Python literals: True, '\n        'False, etc. Can be used multiple times: -M burrito=\"bar\" -M taco=12.'\n    )\n\n    return parser.parse_args()\n\n\ndef _otio_compatible_file_to_json_string(\n        fpath,\n        media_linker_name,\n        media_linker_argument_map,\n        adapter_argument_map\n):\n    \"\"\"Read the file at fpath with the default otio adapter and return the json\n    as a string.\n    \"\"\"\n\n    adapter = otio.adapters.from_name(\"otio_json\")\n    return adapter.write_to_string(\n        otio.adapters.read_from_file(\n            fpath,\n            media_linker_name=media_linker_name,\n            media_linker_argument_map=media_linker_argument_map,\n            **adapter_argument_map\n        )\n    )\n\n\ndef main():\n    \"\"\"Parse arguments and call _otio_compatible_file_to_json_string.\"\"\"\n\n    args = _parsed_args()\n\n    media_linker_name = otio.console.console_utils.media_linker_name(\n        args.media_linker\n    )\n\n    try:\n        read_adapter_arg_map = otio.console.console_utils.arg_list_to_map(\n            args.adapter_arg,\n            \"adapter\"\n        )\n        media_linker_argument_map = otio.console.console_utils.arg_list_to_map(\n            args.media_linker_arg,\n            \"media linker\"\n        )\n    except ValueError as exc:\n        sys.stderr.write(\"\\n\" + str(exc) + \"\\n\")\n        sys.exit(1)\n\n    for fpath in args.filepath:\n        print(\n            _otio_compatible_file_to_json_string(\n                fpath,\n                media_linker_name,\n                media_linker_argument_map,\n                read_adapter_arg_map\n            )\n        )\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/console/otioconvert.py",
    "content": "#!/usr/bin/env python\n#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\nimport argparse\nimport sys\nimport copy\n\nimport opentimelineio as otio\n\n__doc__ = \"\"\" Python wrapper around OTIO to convert timeline files between \\\nformats.\n\nAvailable adapters: {}\n\"\"\".format(otio.adapters.available_adapter_names())\n\n\ndef _parsed_args():\n    \"\"\" parse commandline arguments with argparse \"\"\"\n\n    parser = argparse.ArgumentParser(\n        description=__doc__,\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter\n    )\n    parser.add_argument(\n        '-i',\n        '--input',\n        type=str,\n        required=True,\n        help='path to input file',\n    )\n    parser.add_argument(\n        '-o',\n        '--output',\n        type=str,\n        required=True,\n        help='path to output file',\n    )\n    parser.add_argument(\n        '-I',\n        '--input-adapter',\n        type=str,\n        default=None,\n        help=\"Explicitly use this adapter for reading the input file\",\n    )\n    parser.add_argument(\n        '-O',\n        '--output-adapter',\n        type=str,\n        default=None,\n        help=\"Explicitly use this adapter for writing the output file\",\n    )\n    parser.add_argument(\n        '-T',\n        '--tracks',\n        type=str,\n        default=None,\n        help=\"Pick one or more tracks, by 0-based index, separated by commas.\",\n    )\n    parser.add_argument(\n        '-m',\n        '--media-linker',\n        type=str,\n        default=\"Default\",\n        help=(\n            \"Specify a media linker.  'Default' means use the \"\n            \"$OTIO_DEFAULT_MEDIA_LINKER if set, 'None' or '' means explicitly \"\n            \"disable the linker, and anything else is interpreted as the name\"\n            \" of the media linker to use.\"\n        )\n    )\n    parser.add_argument(\n        '-M',\n        '--media-linker-arg',\n        type=str,\n        default=[],\n        action='append',\n        help='Extra arguments to be passed to the media linker in the form of '\n        'key=value. Values are strings, numbers or Python literals: True, '\n        'False, etc. Can be used multiple times: -M burrito=\"bar\" -M taco=12.'\n    )\n    parser.add_argument(\n        '-a',\n        '--adapter-arg',\n        type=str,\n        default=[],\n        action='append',\n        help='Extra arguments to be passed to input adapter in the form of '\n        'key=value. Values are strings, numbers or Python literals: True, '\n        'False, etc. Can be used multiple times: -a burrito=\"bar\" -a taco=12.'\n    )\n    parser.add_argument(\n        '-A',\n        '--output-adapter-arg',\n        type=str,\n        default=[],\n        action='append',\n        help='Extra arguments to be passed to output adapter in the form of '\n        'key=value. Values are strings, numbers or Python literals: True, '\n        'False, etc. Can be used multiple times: -A burrito=\"bar\" -A taco=12.'\n    )\n    trim_args = parser.add_argument_group(\n        title=\"Trim Arguments\",\n        description=\"Arguments that allow you to trim the OTIO file.\"\n    )\n    trim_args.add_argument(\n        '--begin',\n        type=str,\n        default=None,\n        help=(\n            \"Trim out everything in the timeline before this time, in the \"\n            \"global time frame of the timeline.  Argument should be in the form\"\n            ' \"VALUE,RATE\", eg: --begin \"10,24\".  Requires --end argument.'\n        ),\n    )\n    trim_args.add_argument(\n        '--end',\n        type=str,\n        default=None,\n        help=(\n            \"Trim out everything in the timeline after this time, in the \"\n            \"global time frame of the timeline.  Argument should be in the form\"\n            ' \"VALUE,RATE\", eg: --begin \"10,24\".  Requires --begin argument.'\n        ),\n    )\n\n    result = parser.parse_args()\n\n    if result.begin is not None and result.end is None:\n        parser.error(\"--begin requires --end.\")\n    if result.end is not None and result.begin is None:\n        parser.error(\"--end requires --begin.\")\n\n    if result.begin is not None:\n        try:\n            value, rate = result.begin.split(\",\")\n            result.begin = otio.opentime.RationalTime(float(value), float(rate))\n        except ValueError:\n            parser.error(\n                \"--begin argument needs to be of the form: VALUE,RATE where \"\n                \"VALUE is the (float) time value of the resulting RationalTime \"\n                \"and RATE is the (float) time rate of the resulting RationalTime,\"\n                \" not '{}'\".format(result.begin)\n            )\n\n    if result.end is not None:\n        try:\n            value, rate = result.end.split(\",\")\n            result.end = otio.opentime.RationalTime(float(value), float(rate))\n        except ValueError:\n            parser.error(\n                \"--end argument needs to be of the form: VALUE,RATE where \"\n                \"VALUE is the (float) time value of the resulting RationalTime \"\n                \"and RATE is the (float) time rate of the resulting RationalTime,\"\n                \" not '{}'\".format(result.begin)\n            )\n\n    return result\n\n\ndef main():\n    \"\"\"Parse arguments and convert the files.\"\"\"\n\n    args = _parsed_args()\n\n    in_adapter = args.input_adapter\n    if in_adapter is None:\n        in_adapter = otio.adapters.from_filepath(args.input).name\n\n    out_adapter = args.output_adapter\n    if out_adapter is None:\n        out_adapter = otio.adapters.from_filepath(args.output).name\n\n    media_linker_name = otio.console.console_utils.media_linker_name(\n        args.media_linker\n    )\n\n    try:\n        read_adapter_arg_map = otio.console.console_utils.arg_list_to_map(\n            args.adapter_arg,\n            \"input adapter\"\n        )\n        ml_args = otio.console.console_utils.arg_list_to_map(\n            args.media_linker_arg,\n            \"media linker\"\n        )\n    except ValueError as exc:\n        sys.stderr.write(\"\\n\" + str(exc) + \"\\n\")\n        sys.exit(1)\n\n    result_tl = otio.adapters.read_from_file(\n        args.input,\n        in_adapter,\n        media_linker_name=media_linker_name,\n        media_linker_argument_map=ml_args,\n        **read_adapter_arg_map\n    )\n\n    if args.tracks:\n        result_tracks = copy.deepcopy(otio.schema.Stack())\n        del result_tracks[:]\n        for track in args.tracks.split(\",\"):\n            tr = result_tl.tracks[int(track)]\n            del result_tl.tracks[int(track)]\n            print(\"track {0} is of kind: '{1}'\".format(track, tr.kind))\n            result_tracks.append(tr)\n        result_tl.tracks = result_tracks\n\n    # handle trim arguments\n    if args.begin is not None and args.end is not None:\n        result_tl = otio.algorithms.timeline_trimmed_to_range(\n            result_tl,\n            otio.opentime.range_from_start_end_time(args.begin, args.end)\n        )\n\n    try:\n        write_adapter_arg_map = otio.console.console_utils.arg_list_to_map(\n            args.output_adapter_arg,\n            \"output adapter\"\n        )\n    except ValueError as exc:\n        sys.stderr.write(\"\\n\" + str(exc) + \"\\n\")\n        sys.exit(1)\n\n    otio.adapters.write_to_file(\n        result_tl,\n        args.output,\n        out_adapter,\n        **write_adapter_arg_map\n    )\n\n\nif __name__ == '__main__':\n    try:\n        main()\n    except otio.exceptions.OTIOError as err:\n        sys.stderr.write(\"ERROR: \" + str(err) + \"\\n\")\n        sys.exit(1)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/console/otiostat.py",
    "content": "#!/usr/bin/env python\n#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Print statistics about the otio file, including validation information.\"\"\"\n\nimport argparse\nimport sys\n\nimport opentimelineio as otio\n\n\ndef _parsed_args():\n    \"\"\" parse commandline arguments with argparse \"\"\"\n\n    parser = argparse.ArgumentParser(\n        description=__doc__,\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter\n    )\n    parser.add_argument(\n        'filepath',\n        type=str,\n        nargs='+',\n        help='files to operate on'\n    )\n\n    return parser.parse_args()\n\n\nTESTS = []\n\n\ndef stat_check(name):\n    def real_stat_check(fn):\n        TESTS.append((name, fn))\n        return fn\n    return real_stat_check\n\n\n@stat_check(\"parsed\")\ndef _did_parse(input):\n    return input and True or False\n\n\n@stat_check(\"top level object\")\ndef _top_level_object(input):\n    return input._serializable_label\n\n\n@stat_check(\"number of tracks\")\ndef _num_tracks(input):\n    try:\n        return len(input.tracks)\n    except AttributeError:\n        return 0\n\n\n@stat_check(\"Tracks are the same length\")\ndef _equal_length_tracks(tl):\n    if not tl.tracks:\n        return True\n    for i, track in enumerate(tl.tracks):\n        if track.duration() != tl.tracks[0].duration():\n            raise RuntimeError(\n                \"track {} is not the same duration as the other tracks.\"\n                \" Track {} duration, vs: {}\".format(\n                    i,\n                    track.duration(),\n                    tl.tracks[0].duration()\n                )\n            )\n    return True\n\n\n@stat_check(\"deepest nesting\")\ndef _deepest_nesting(input):\n    def depth(parent):\n        if not isinstance(parent, otio.core.Composition):\n            return 1\n        d = 0\n        for child in parent:\n            d = max(d, depth(child) + 1)\n        return d\n    if isinstance(input, otio.schema.Timeline):\n        return depth(input.tracks) + 1\n    else:\n        return depth(input)\n\n\n@stat_check(\"number of clips\")\ndef _num_clips(input):\n    return len(list(input.each_clip()))\n\n\n@stat_check(\"total duration\")\ndef _total_duration(input):\n    try:\n        return input.tracks.duration()\n    except AttributeError:\n        return \"n/a\"\n\n\n@stat_check(\"total duration in timecode\")\ndef _total_duration_timecode(input):\n    try:\n        d = input.tracks.duration()\n        return otio.opentime.to_timecode(d, d.rate)\n    except AttributeError:\n        return \"n/a\"\n\n\n@stat_check(\"top level rate\")\ndef _top_level_rate(input):\n    try:\n        return input.tracks.duration().rate\n    except AttributeError:\n        return \"n/a\"\n\n\n@stat_check(\"clips with cdl data\")\ndef _clips_with_cdl_data(input):\n    return len(list(c for c in input.each_clip() if 'cdl' in c.metadata))\n\n\n@stat_check(\"Tracks with non standard types\")\ndef _sequences_with_non_standard_types(input):\n    return len(\n        list(\n            c\n            for c in input.each_child(descended_from_type=otio.schema.Track)\n            if c.kind not in (otio.schema.TrackKind.__dict__)\n        )\n    )\n\n\ndef _stat_otio(input_otio):\n    for (test, testfunc) in TESTS:\n        try:\n            print(\"{}: {}\".format(test, testfunc(input_otio)))\n        except (otio.exceptions.OTIOError) as e:\n            sys.stderr.write(\n                \"There was an OTIO Error: \"\n                \" {}\\n\".format(e),\n            )\n            continue\n        except (Exception) as e:\n            sys.stderr.write(\"There was a system error: {}\\n\".format(e))\n            continue\n\n\ndef main():\n    \"\"\"  main entry point  \"\"\"\n    args = _parsed_args()\n\n    for fp in args.filepath:\n        try:\n            parsed_otio = otio.adapters.read_from_file(fp)\n        except (otio.exceptions.OTIOError) as e:\n            sys.stderr.write(\n                \"The file did not successfully parse, with error:\"\n                \" {}\\n\".format(e),\n            )\n            continue\n        except (Exception) as e:\n            sys.stderr.write(\"There was a system error: {}\\n\".format(e))\n            continue\n\n        _stat_otio(parsed_otio)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/core/__init__.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Internal implementation details of OpenTimelineIO.\"\"\"\n\n# flake8: noqa\n\nfrom . import (\n    serializable_object\n)\nfrom .serializable_object import (\n    SerializableObject,\n    serializable_field,\n    deprecated_field,\n)\nfrom .composable import (\n    Composable\n)\nfrom .item import (\n    Item\n)\nfrom . import composition\nfrom .composition import (\n    Composition,\n)\nfrom . import type_registry\nfrom .type_registry import (\n    register_type,\n    upgrade_function_for,\n    schema_name_from_label,\n    schema_version_from_label,\n    instance_from_schema,\n)\nfrom .json_serializer import (\n    serialize_json_to_string,\n    serialize_json_to_file,\n    deserialize_json_from_string,\n    deserialize_json_from_file,\n)\nfrom .media_reference import (\n    MediaReference,\n)\nfrom . import unknown_schema\nfrom .unknown_schema import (\n    UnknownSchema\n)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/core/composable.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Composable class definition.\n\nAn object that can be composed by tracks.\n\"\"\"\n\nimport weakref\n\nfrom . import serializable_object\nfrom . import type_registry\n\nimport copy\n\n\n@type_registry.register_type\nclass Composable(serializable_object.SerializableObject):\n    \"\"\"An object that can be composed by tracks.\n\n    Base class of:\n        Item\n        Transition\n    \"\"\"\n\n    name = serializable_object.serializable_field(\n        \"name\",\n        doc=\"Composable name.\"\n    )\n    metadata = serializable_object.serializable_field(\n        \"metadata\",\n        doc=\"Metadata dictionary for this Composable.\"\n    )\n\n    _serializable_label = \"Composable.1\"\n    _class_path = \"core.Composable\"\n\n    def __init__(self, name=None, metadata=None):\n        super(Composable, self).__init__()\n        self._parent = None\n\n        # initialize the serializable fields\n        self.name = name\n        self.metadata = copy.deepcopy(metadata) if metadata else {}\n\n    @staticmethod\n    def visible():\n        \"\"\"Return the visibility of the Composable. By default True.\"\"\"\n\n        return False\n\n    @staticmethod\n    def overlapping():\n        \"\"\"Return whether an Item is overlapping. By default False.\"\"\"\n\n        return False\n\n    # @{ functions to express the composable hierarchy\n    def _root_parent(self):\n        return ([self] + self._ancestors())[-1]\n\n    def _ancestors(self):\n        ancestors = []\n        seqi = self\n        while seqi.parent() is not None:\n            seqi = seqi.parent()\n            ancestors.append(seqi)\n        return ancestors\n\n    def parent(self):\n        \"\"\"Return the parent Composable, or None if self has no parent.\"\"\"\n\n        return self._parent() if self._parent is not None else None\n\n    def _set_parent(self, new_parent):\n        if new_parent is not None and self.parent() is not None:\n            raise ValueError(\n                \"Composable named '{}' is already in a composition named '{}',\"\n                \" remove from previous parent before adding to new one.\"\n                \" Composable: {}, Composition: {}\".format(\n                    self.name,\n                    self.parent() is not None and self.parent().name or None,\n                    self,\n                    self.parent()\n                )\n            )\n        self._parent = weakref.ref(new_parent) if new_parent is not None else None\n\n    def is_parent_of(self, other):\n        \"\"\"Returns true if self is a parent or ancestor of other.\"\"\"\n\n        visited = set([])\n        while other.parent() is not None and other.parent() not in visited:\n            if other.parent() is self:\n                return True\n            visited.add(other)\n            other = other.parent()\n\n        return False\n\n    # @}\n\n    def __repr__(self):\n        return (\n            \"otio.{}(\"\n            \"name={}, \"\n            \"metadata={}\"\n            \")\".format(\n                self._class_path,\n                repr(self.name),\n                repr(self.metadata)\n            )\n        )\n\n    def __str__(self):\n        return \"{}({}, {})\".format(\n            self._class_path.split('.')[-1],\n            self.name,\n            str(self.metadata)\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/core/composition.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Composition base class.  An object that contains `Items`.\"\"\"\n\nimport collections\n\nfrom . import (\n    serializable_object,\n    type_registry,\n    item,\n    composable,\n)\n\nfrom .. import (\n    opentime,\n    exceptions\n)\n\n\ndef _bisect_right(\n        seq,\n        tgt,\n        key_func,\n        lower_search_bound=0,\n        upper_search_bound=None\n):\n    \"\"\"Return the index of the last item in seq such that all e in seq[:index]\n    have key_func(e) <= tgt, and all e in seq[index:] have key_func(e) > tgt.\n\n    Thus, seq.insert(index, value) will insert value after the rightmost item\n    such that meets the above condition.\n\n    lower_search_bound and upper_search_bound bound the slice to be searched.\n\n    Assumes that seq is already sorted.\n    \"\"\"\n\n    if lower_search_bound < 0:\n        raise ValueError('lower_search_bound must be non-negative')\n\n    if upper_search_bound is None:\n        upper_search_bound = len(seq)\n\n    while lower_search_bound < upper_search_bound:\n        midpoint_index = (lower_search_bound + upper_search_bound) // 2\n\n        if tgt < key_func(seq[midpoint_index]):\n            upper_search_bound = midpoint_index\n        else:\n            lower_search_bound = midpoint_index + 1\n\n    return lower_search_bound\n\n\ndef _bisect_left(\n        seq,\n        tgt,\n        key_func,\n        lower_search_bound=0,\n        upper_search_bound=None\n):\n    \"\"\"Return the index of the last item in seq such that all e in seq[:index]\n    have key_func(e) < tgt, and all e in seq[index:] have key_func(e) >= tgt.\n\n    Thus, seq.insert(index, value) will insert value before the leftmost item\n    such that meets the above condition.\n\n    lower_search_bound and upper_search_bound bound the slice to be searched.\n\n    Assumes that seq is already sorted.\n    \"\"\"\n\n    if lower_search_bound < 0:\n        raise ValueError('lower_search_bound must be non-negative')\n\n    if upper_search_bound is None:\n        upper_search_bound = len(seq)\n\n    while lower_search_bound < upper_search_bound:\n        midpoint_index = (lower_search_bound + upper_search_bound) // 2\n\n        if key_func(seq[midpoint_index]) < tgt:\n            lower_search_bound = midpoint_index + 1\n        else:\n            upper_search_bound = midpoint_index\n\n    return lower_search_bound\n\n\n@type_registry.register_type\nclass Composition(item.Item, collections.MutableSequence):\n    \"\"\"Base class for an OTIO Item that contains other Items.\n\n    Should be subclassed (for example by Track and Stack), not used\n    directly.\n    \"\"\"\n\n    _serializable_label = \"Composition.1\"\n    _composition_kind = \"Composition\"\n    _modname = \"core\"\n    _composable_base_class = composable.Composable\n\n    def __init__(\n        self,\n        name=None,\n        children=None,\n        source_range=None,\n        markers=None,\n        effects=None,\n        metadata=None\n    ):\n        item.Item.__init__(\n            self,\n            name=name,\n            source_range=source_range,\n            markers=markers,\n            effects=effects,\n            metadata=metadata\n        )\n        collections.MutableSequence.__init__(self)\n\n        # Because we know that all children are unique, we store a set\n        # of all the children as well to speed up __contain__ checks.\n        self._child_lookup = set()\n\n        self._children = []\n        if children:\n            # cannot simply set ._children to children since __setitem__ runs\n            # extra logic (assigning ._parent pointers) and populates the\n            # internal membership set _child_lookup.\n            self.extend(children)\n\n    _children = serializable_object.serializable_field(\n        \"children\",\n        list,\n        \"Items contained by this composition.\"\n    )\n\n    @property\n    def composition_kind(self):\n        \"\"\"Returns a label specifying the kind of composition.\"\"\"\n\n        return self._composition_kind\n\n    def __str__(self):\n        return \"{}({}, {}, {}, {})\".format(\n            self._composition_kind,\n            str(self.name),\n            str(self._children),\n            str(self.source_range),\n            str(self.metadata)\n        )\n\n    def __repr__(self):\n        return (\n            \"otio.{}.{}(\"\n            \"name={}, \"\n            \"children={}, \"\n            \"source_range={}, \"\n            \"metadata={}\"\n            \")\".format(\n                self._modname,\n                self._composition_kind,\n                repr(self.name),\n                repr(self._children),\n                repr(self.source_range),\n                repr(self.metadata)\n            )\n        )\n\n    transform = serializable_object.deprecated_field()\n\n    def child_at_time(\n            self,\n            search_time,\n            shallow_search=False,\n    ):\n        \"\"\"Return the child that overlaps with time search_time.\n\n        search_time is in the space of self.\n\n        If shallow_search is false, will recurse into compositions.\n        \"\"\"\n\n        range_map = self.range_of_all_children()\n\n        # find the first item whose end_time_exclusive is after the\n        first_inside_range = _bisect_left(\n            seq=self._children,\n            tgt=search_time,\n            key_func=lambda child: range_map[child].end_time_exclusive(),\n        )\n\n        # find the last item whose start_time is before the\n        last_in_range = _bisect_right(\n            seq=self._children,\n            tgt=search_time,\n            key_func=lambda child: range_map[child].start_time,\n            lower_search_bound=first_inside_range,\n        )\n\n        # limit the search to children who are in the search_range\n        possible_matches = self._children[first_inside_range:last_in_range]\n\n        result = None\n        for thing in possible_matches:\n            if range_map[thing].overlaps(search_time):\n                result = thing\n                break\n\n        # if the search cannot or should not continue\n        if (\n                result is None\n                or shallow_search\n                or not hasattr(result, \"child_at_time\")\n        ):\n            return result\n\n        # before you recurse, you have to transform the time into the\n        # space of the child\n        child_search_time = self.transformed_time(search_time, result)\n\n        return result.child_at_time(child_search_time, shallow_search)\n\n    def each_child(\n            self,\n            search_range=None,\n            descended_from_type=composable.Composable,\n            shallow_search=False,\n    ):\n        \"\"\" Generator that returns each child contained in the composition in\n        the order in which it is found.\n\n        Arguments:\n            search_range: if specified, only children whose range overlaps with\n                          the search range will be yielded.\n            descended_from_type: if specified, only children who are a\n                          descendent of the descended_from_type will be yielded.\n            shallow_search: if True, will only search children of self, not\n                            and not recurse into children of children.\n        \"\"\"\n        if search_range:\n            range_map = self.range_of_all_children()\n\n            # find the first item whose end_time_inclusive is after the\n            # start_time of the search range\n            first_inside_range = _bisect_left(\n                seq=self._children,\n                tgt=search_range.start_time,\n                key_func=lambda child: range_map[child].end_time_inclusive(),\n            )\n\n            # find the last item whose start_time is before the\n            # end_time_inclusive of the search_range\n            last_in_range = _bisect_right(\n                seq=self._children,\n                tgt=search_range.end_time_inclusive(),\n                key_func=lambda child: range_map[child].start_time,\n                lower_search_bound=first_inside_range,\n            )\n\n            # limit the search to children who are in the search_range\n            children = self._children[first_inside_range:last_in_range]\n        else:\n            # otherwise search all the children\n            children = self._children\n\n        for child in children:\n            # filter out children who are not descended from the specified type\n            # shortcut the isinstance if descended_from_type is composable\n            # (since all objects in compositions are already composables)\n            is_descendant = descended_from_type == composable.Composable\n            if is_descendant or isinstance(child, descended_from_type):\n                yield child\n\n            # if not a shallow_search, for children that are compositions,\n            # recurse into their children\n            if not shallow_search and hasattr(child, \"each_child\"):\n\n                if search_range is not None:\n                    search_range = self.transformed_time_range(search_range, child)\n\n                for valid_child in child.each_child(\n                        search_range,\n                        descended_from_type,\n                        shallow_search\n                ):\n                    yield valid_child\n\n    def range_of_child_at_index(self, index):\n        \"\"\"Return the range of a child item in the time range of this\n        composition.\n\n        For example, with a track:\n            [ClipA][ClipB][ClipC]\n\n        The self.range_of_child_at_index(2) will return:\n            TimeRange(ClipA.duration + ClipB.duration, ClipC.duration)\n\n        To be implemented by subclass of Composition.\n        \"\"\"\n\n        raise NotImplementedError\n\n    def trimmed_range_of_child_at_index(self, index):\n        \"\"\"Return the trimmed range of the child item at index in the time\n        range of this composition.\n\n        For example, with a track:\n\n                       [     ]\n\n            [ClipA][ClipB][ClipC]\n\n        The range of index 2 (ClipC) will be just like\n        range_of_child_at_index() but trimmed based on this Composition's\n        source_range.\n\n        To be implemented by child.\n        \"\"\"\n\n        raise NotImplementedError\n\n    def range_of_all_children(self):\n        \"\"\"Return a dict mapping children to their range in this object.\"\"\"\n\n        raise NotImplementedError\n\n    def __copy__(self):\n        result = super(Composition, self).__copy__()\n\n        # Children are *not* copied with a shallow copy since the meaning is\n        # ambiguous - they have a parent pointer which would need to be flipped\n        # or they would need to be copied, which implies a deepcopy().\n        #\n        # This follows from the python documentation on copy/deepcopy:\n        # https://docs.python.org/2/library/copy.html\n        #\n        # \"\"\"\n        # - A shallow copy constructs a new compound object and then (to the\n        #   extent possible) inserts references into it to the objects found in\n        #   the original.\n        # - A deep copy constructs a new compound object and then, recursively,\n        #   inserts copies into it of the objects found in the original.\n        # \"\"\"\n        result._children = []\n\n        return result\n\n    def __deepcopy__(self, md):\n        result = super(Composition, self).__deepcopy__(md)\n\n        # deepcopy should have already copied the children, so only parent\n        # pointers need to be updated.\n        [c._set_parent(result) for c in result._children]\n\n        # we also need to reconstruct the membership set of _child_lookup.\n        result._child_lookup.update(result._children)\n\n        return result\n\n    def _path_to_child(self, child):\n        if not isinstance(child, composable.Composable):\n            raise TypeError(\n                \"An object child of 'Composable' is required,\"\n                \" not type '{}'\".format(\n                    type(child)\n                )\n            )\n\n        current = child\n        parents = []\n\n        while(current is not self):\n            try:\n                current = current.parent()\n            except AttributeError:\n                raise exceptions.NotAChildError(\n                    \"Item '{}' is not a child of '{}'.\".format(child, self)\n                )\n\n            parents.append(current)\n\n        return parents\n\n    def range_of_child(self, child, reference_space=None):\n        \"\"\"The range of the child in relation to another item\n        (reference_space), not trimmed based on this\n        composition's source_range.\n\n        Note that reference_space must be in the same timeline as self.\n\n        For example:\n\n            |     [-----]     | seq\n\n            [-----------------] Clip A\n\n        If ClipA has duration 17, and seq has source_range: 5, duration 15,\n        seq.range_of_child(Clip A) will return (0, 17)\n        ignoring the source range of seq.\n\n        To get the range of the child with the source_range applied, use the\n        trimmed_range_of_child() method.\n        \"\"\"\n\n        if not reference_space:\n            reference_space = self\n\n        parents = self._path_to_child(child)\n\n        current = child\n        result_range = None\n\n        for parent in parents:\n            index = parent.index(current)\n            parent_range = parent.range_of_child_at_index(index)\n\n            if not result_range:\n                result_range = parent_range\n                current = parent\n                continue\n\n            result_range = opentime.TimeRange(\n                start_time=result_range.start_time + parent_range.start_time,\n                duration=result_range.duration\n            )\n            current = parent\n\n        if reference_space is not self:\n            result_range = self.transformed_time_range(\n                result_range,\n                reference_space\n            )\n\n        return result_range\n\n    def handles_of_child(self, child):\n        \"\"\"If media beyond the ends of this child are visible due to adjacent\n        Transitions (only applicable in a Track) then this will return the\n        head and tail offsets as a tuple of RationalTime objects. If no handles\n        are present on either side, then None is returned instead of a\n        RationalTime.\n\n        Example usage:\n        >>> head, tail = track.handles_of_child(clip)\n        >>> if head:\n        ...     print('Do something')\n        >>> if tail:\n        ...     print('Do something else')\n        \"\"\"\n        return (None, None)\n\n    def trimmed_range_of_child(self, child, reference_space=None):\n        \"\"\"Get range of the child in reference_space coordinates, after the\n        self.source_range is applied.\n\n        Example\n        |     [-----]     | seq\n        [-----------------] Clip A\n\n        If ClipA has duration 17, and seq has source_range: 5, duration 10,\n        seq.trimmed_range_of_child(Clip A) will return (5, 10)\n        Which is trimming the range according to the source_range of seq.\n\n        To get the range of the child without the source_range applied, use the\n        range_of_child() method.\n\n        Another example\n        |  [-----]   | seq source range starts on frame 4 and goes to frame 8\n        [ClipA][ClipB] (each 6 frames long)\n\n        >>> seq.range_of_child(CLipA)\n        0, duration 6\n        >>> seq.trimmed_range_of_child(ClipA):\n        4, duration 2\n        \"\"\"\n\n        if not reference_space:\n            reference_space = self\n\n        if not reference_space == self:\n            raise NotImplementedError\n\n        parents = self._path_to_child(child)\n\n        current = child\n        result_range = None\n\n        for parent in parents:\n            index = parent.index(current)\n            parent_range = parent.trimmed_range_of_child_at_index(index)\n\n            if not result_range:\n                result_range = parent_range\n                current = parent\n                continue\n\n            result_range.start_time += parent_range.start_time\n            current = parent\n\n        if not self.source_range or not result_range:\n            return result_range\n\n        new_start_time = max(\n            self.source_range.start_time,\n            result_range.start_time\n        )\n\n        # trimmed out\n        if new_start_time >= result_range.end_time_exclusive():\n            return None\n\n        # compute duration\n        new_duration = min(\n            result_range.end_time_exclusive(),\n            self.source_range.end_time_exclusive()\n        ) - new_start_time\n\n        if new_duration.value < 0:\n            return None\n\n        return opentime.TimeRange(new_start_time, new_duration)\n\n    def trim_child_range(self, child_range):\n        if not self.source_range:\n            return child_range\n\n        # cropped out entirely\n        past_end_time = self.source_range.start_time >= child_range.end_time_exclusive()\n        before_start_time = \\\n            self.source_range.end_time_exclusive() <= child_range.start_time\n\n        if past_end_time or before_start_time:\n            return None\n\n        if child_range.start_time < self.source_range.start_time:\n            child_range = opentime.range_from_start_end_time(\n                self.source_range.start_time,\n                child_range.end_time_exclusive()\n            )\n\n        if (\n            child_range.end_time_exclusive() >\n            self.source_range.end_time_exclusive()\n        ):\n            child_range = opentime.range_from_start_end_time(\n                child_range.start_time,\n                self.source_range.end_time_exclusive()\n            )\n\n        return child_range\n\n    # @{ SerializableObject override.\n    def _update(self, d):\n        \"\"\"Like the dictionary .update() method.\n\n        Update the data dictionary of this SerializableObject with the .data\n        of d if d is a SerializableObject or if d is a dictionary, d itself.\n        \"\"\"\n\n        # use the parent update function\n        super(Composition, self)._update(d)\n\n        # ...except for the 'children' field, which needs to run through the\n        # insert method so that _parent pointers are correctly set on children.\n        self._children = []\n        self.extend(d.get('children', []))\n    # @}\n\n    # @{ collections.MutableSequence implementation\n    def __getitem__(self, item):\n        return self._children[item]\n\n    def _setitem_slice(self, key, value):\n        set_value = set(value)\n\n        # check if any members in the new slice are repeated\n        if len(set_value) != len(value):\n            raise ValueError(\n                \"Instancing not allowed in Compositions, {} contains repeated\"\n                \" items.\".format(value)\n            )\n\n        old = self._children[key]\n        if old:\n            set_old = set(old)\n            set_outside_old = set(self._children).difference(set_old)\n\n            isect = set_outside_old.intersection(set_value)\n            if isect:\n                raise ValueError(\n                    \"Attempting to insert duplicates of items {} already \"\n                    \"present in container, instancing not allowed in \"\n                    \"Compositions\".format(isect)\n                )\n\n            # update old parent\n            for val in old:\n                val._set_parent(None)\n                self._child_lookup.remove(val)\n\n        # insert into _children\n        self._children[key] = value\n\n        # update new parent\n        if value:\n            for val in value:\n                val._set_parent(self)\n                self._child_lookup.add(val)\n\n    def __setitem__(self, key, value):\n        # fetch the current thing at that index/slice\n        old = self._children[key]\n\n        # in the case of key being a slice, old and value are both sequences\n        if old is value:\n            return\n\n        if isinstance(key, slice):\n            return self._setitem_slice(key, value)\n\n        if value in self:\n            raise ValueError(\n                \"Composable {} already present in this container, instancing\"\n                \" not allowed in otio compositions.\".format(value)\n            )\n\n        # unset the old child's parent and delete the membership entry.\n        if old is not None:\n            old._set_parent(None)\n            self._child_lookup.remove(old)\n\n        # put it into our list of children\n        self._children[key] = value\n\n        # set the new parent\n        if value is not None:\n            value._set_parent(self)\n\n            # put it into our membership tracking set\n            self._child_lookup.add(value)\n\n    def insert(self, index, item):\n        \"\"\"Insert an item into the composition at location `index`.\"\"\"\n\n        if not isinstance(item, self._composable_base_class):\n            raise TypeError(\n                \"Not allowed to insert an object of type {0} into a {1}, only\"\n                \" objects descending from {2}. Tried to insert: {3}\".format(\n                    type(item),\n                    type(self),\n                    self._composable_base_class,\n                    str(item)\n                )\n            )\n\n        if item in self:\n            raise ValueError(\n                \"Composable {} already present in this container, instancing\"\n                \" not allowed in otio compositions.\".format(item)\n            )\n\n        # set the item's parent and add it to our membership tracking and list\n        # of children\n        item._set_parent(self)\n        self._child_lookup.add(item)\n        self._children.insert(index, item)\n\n    def __contains__(self, item):\n        \"\"\"Use our internal membership tracking set to speed up searches.\"\"\"\n        return item in self._child_lookup\n\n    def __len__(self):\n        \"\"\"The len() of a Composition is the # of children in it.\n        Note that this also means that a Composition with no children\n        is considered False, so take care to test for \"if foo is not None\"\n        versus just \"if foo\" when the difference matters.\"\"\"\n        return len(self._children)\n\n    def __delitem__(self, key):\n        # grab the old value\n        old = self._children[key]\n\n        # remove it from the membership tracking set and clear parent\n        if old is not None:\n            if isinstance(key, slice):\n                for val in old:\n                    self._child_lookup.remove(val)\n                    val._set_parent(None)\n            else:\n                self._child_lookup.remove(old)\n                old._set_parent(None)\n\n        # remove it from our list of children\n        del self._children[key]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/core/item.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Implementation of the Item base class.  OTIO Objects that contain media.\"\"\"\n\nimport copy\n\nfrom .. import (\n    opentime,\n    exceptions,\n)\n\nfrom . import (\n    serializable_object,\n    composable,\n)\n\n\nclass Item(composable.Composable):\n    \"\"\"An Item is a Composable that can be part of a Composition or Timeline.\n\n    More specifically, it is a Composable that has meaningful duration.\n\n    Can also hold effects and markers.\n\n    Base class of:\n        - Composition (and children)\n        - Clip\n        - Gap\n    \"\"\"\n\n    _serializable_label = \"Item.1\"\n    _class_path = \"core.Item\"\n\n    def __init__(\n        self,\n        name=None,\n        source_range=None,\n        effects=None,\n        markers=None,\n        metadata=None,\n    ):\n        super(Item, self).__init__(name=name, metadata=metadata)\n\n        self.source_range = copy.deepcopy(source_range)\n        self.effects = copy.deepcopy(effects) if effects else []\n        self.markers = copy.deepcopy(markers) if markers else []\n\n    name = serializable_object.serializable_field(\"name\", doc=\"Item name.\")\n    source_range = serializable_object.serializable_field(\n        \"source_range\",\n        opentime.TimeRange,\n        doc=\"Range of source to trim to.  Can be None or a TimeRange.\"\n    )\n\n    @staticmethod\n    def visible():\n        \"\"\"Return the visibility of the Item. By default True.\"\"\"\n\n        return True\n\n    def duration(self):\n        \"\"\"Convience wrapper for the trimmed_range.duration of the item.\"\"\"\n\n        return self.trimmed_range().duration\n\n    def available_range(self):\n        \"\"\"Implemented by child classes, available range of media.\"\"\"\n\n        raise NotImplementedError\n\n    def trimmed_range(self):\n        \"\"\"The range after applying the source range.\"\"\"\n        if self.source_range is not None:\n            return copy.copy(self.source_range)\n\n        return self.available_range()\n\n    def visible_range(self):\n        \"\"\"The range of this item's media visible to its parent.\n        Includes handles revealed by adjacent transitions (if any).\n        This will always be larger or equal to trimmed_range().\"\"\"\n        result = self.trimmed_range()\n        if self.parent():\n            head, tail = self.parent().handles_of_child(self)\n            if head:\n                result = opentime.TimeRange(\n                    start_time=result.start_time - head,\n                    duration=result.duration + head\n                )\n            if tail:\n                result = opentime.TimeRange(\n                    start_time=result.start_time,\n                    duration=result.duration + tail\n                )\n        return result\n\n    def trimmed_range_in_parent(self):\n        \"\"\"Find and return the trimmed range of this item in the parent.\"\"\"\n        if not self.parent():\n            raise exceptions.NotAChildError(\n                \"No parent of {}, cannot compute range in parent.\".format(self)\n            )\n\n        return self.parent().trimmed_range_of_child(self)\n\n    def range_in_parent(self):\n        \"\"\"Find and return the untrimmed range of this item in the parent.\"\"\"\n        if not self.parent():\n            raise exceptions.NotAChildError(\n                \"No parent of {}, cannot compute range in parent.\".format(self)\n            )\n\n        return self.parent().range_of_child(self)\n\n    def transformed_time(self, t, to_item):\n        \"\"\"Converts time t in the coordinate system of self to coordinate\n        system of to_item.\n\n        Note that self and to_item must be part of the same timeline (they must\n        have a common ancestor).\n\n        Example:\n\n            0                      20\n            [------t----D----------]\n            [--A-][t----B---][--C--]\n            100    101    110\n            101 in B = 6 in D\n\n        t = t argument\n        \"\"\"\n\n        if not isinstance(t, opentime.RationalTime):\n            raise ValueError(\n                \"transformed_time only operates on RationalTime, not {}\".format(\n                    type(t)\n                )\n            )\n\n        # does not operate in place\n        result = copy.copy(t)\n\n        if to_item is None:\n            return result\n\n        root = self._root_parent()\n\n        # transform t to root  parent's coordinate system\n        item = self\n        while item != root and item != to_item:\n\n            parent = item.parent()\n            result -= item.trimmed_range().start_time\n            result += parent.range_of_child(item).start_time\n\n            item = parent\n\n        ancestor = item\n\n        # transform from root parent's coordinate system to to_item\n        item = to_item\n        while item != root and item != ancestor:\n\n            parent = item.parent()\n            result += item.trimmed_range().start_time\n            result -= parent.range_of_child(item).start_time\n\n            item = parent\n\n        assert(item is ancestor)\n\n        return result\n\n    def transformed_time_range(self, tr, to_item):\n        \"\"\"Transforms the timerange tr to the range of child or self to_item.\"\"\"\n\n        return opentime.TimeRange(\n            self.transformed_time(tr.start_time, to_item),\n            tr.duration\n        )\n\n    markers = serializable_object.serializable_field(\n        \"markers\",\n        doc=\"List of markers on this item.\"\n    )\n    effects = serializable_object.serializable_field(\n        \"effects\",\n        doc=\"List of effects on this item.\"\n    )\n    metadata = serializable_object.serializable_field(\n        \"metadata\",\n        doc=\"Metadata dictionary for this item.\"\n    )\n\n    def __repr__(self):\n        return (\n            \"otio.{}(\"\n            \"name={}, \"\n            \"source_range={}, \"\n            \"effects={}, \"\n            \"markers={}, \"\n            \"metadata={}\"\n            \")\".format(\n                self._class_path,\n                repr(self.name),\n                repr(self.source_range),\n                repr(self.effects),\n                repr(self.markers),\n                repr(self.metadata)\n            )\n        )\n\n    def __str__(self):\n        return \"{}({}, {}, {}, {}, {})\".format(\n            self._class_path.split('.')[-1],\n            self.name,\n            str(self.source_range),\n            str(self.effects),\n            str(self.markers),\n            str(self.metadata)\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/core/json_serializer.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Serializer for SerializableObjects to JSON\n\nUsed for the otio_json adapter as well as for plugins and manifests.\n\"\"\"\n\nimport json\n\nfrom . import (\n    SerializableObject,\n    type_registry,\n)\n\nfrom .unknown_schema import UnknownSchema\n\nfrom .. import (\n    exceptions,\n    opentime,\n)\n\n\n# @TODO: Handle file version drifting\n\n\nclass _SerializableObjectEncoder(json.JSONEncoder):\n\n    \"\"\" Encoder for the SerializableObject OTIO Class and its descendents. \"\"\"\n\n    def default(self, obj):\n        for typename, encfn in _ENCODER_LIST:\n            if isinstance(obj, typename):\n                return encfn(obj)\n\n        return json.JSONEncoder.default(self, obj)\n\n\ndef serialize_json_to_string(root, indent=4):\n    \"\"\"Serialize a tree of SerializableObject to JSON.\n\n    Returns a JSON string.\n    \"\"\"\n\n    return _SerializableObjectEncoder(\n        sort_keys=True,\n        indent=indent\n    ).encode(root)\n\n\ndef serialize_json_to_file(root, to_file):\n    \"\"\"\n    Serialize a tree of SerializableObject to JSON.\n\n    Writes the result to the given file path.\n    \"\"\"\n\n    content = serialize_json_to_string(root)\n\n    with open(to_file, 'w') as file_contents:\n        file_contents.write(content)\n\n# @{ Encoders\n\n\ndef _encoded_serializable_object(input_otio):\n    if not input_otio._serializable_label:\n        raise exceptions.InvalidSerializableLabelError(\n            input_otio._serializable_label\n        )\n    result = {\n        \"OTIO_SCHEMA\": input_otio._serializable_label,\n    }\n    result.update(input_otio._data)\n    return result\n\n\ndef _encoded_unknown_schema_object(input_otio):\n    orig_label = input_otio.data.get(UnknownSchema._original_label)\n    if not orig_label:\n        raise exceptions.InvalidSerializableLabelError(\n            orig_label\n        )\n    # result is just a dict, not a SerializableObject\n    result = {}\n    result.update(input_otio.data)\n    result[\"OTIO_SCHEMA\"] = orig_label  # override the UnknownSchema label\n    del result[UnknownSchema._original_label]\n    return result\n\n\ndef _encoded_time(input_otio):\n    return {\n        \"OTIO_SCHEMA\": \"RationalTime.1\",\n        'value': input_otio.value,\n        'rate': input_otio.rate\n    }\n\n\ndef _encoded_time_range(input_otio):\n    return {\n        \"OTIO_SCHEMA\": \"TimeRange.1\",\n        'start_time': _encoded_time(input_otio.start_time),\n        'duration': _encoded_time(input_otio.duration)\n    }\n\n\ndef _encoded_transform(input_otio):\n    return {\n        \"OTIO_SCHEMA\": \"TimeTransform.1\",\n        'offset': _encoded_time(input_otio.offset),\n        'scale': input_otio.scale,\n        'rate': input_otio.rate\n    }\n# @}\n\n\n# Ordered list of functions for encoding OTIO objects to JSON.\n# More particular cases should precede more general cases.\n_ENCODER_LIST = [\n    (opentime.RationalTime, _encoded_time),\n    (opentime.TimeRange, _encoded_time_range),\n    (opentime.TimeTransform, _encoded_transform),\n    (UnknownSchema, _encoded_unknown_schema_object),\n    (SerializableObject, _encoded_serializable_object)\n]\n\n# @{ Decoders\n\n\ndef _decoded_time(input_otio):\n    return opentime.RationalTime(\n        input_otio['value'],\n        input_otio['rate']\n    )\n\n\ndef _decoded_time_range(input_otio):\n    return opentime.TimeRange(\n        input_otio['start_time'],\n        input_otio['duration']\n    )\n\n\ndef _decoded_transform(input_otio):\n    return opentime.TimeTransform(\n        input_otio['offset'],\n        input_otio['scale']\n    )\n# @}\n\n\n# Map of explicit decoder functions to schema labels (for opentime)\n# because opentime is implemented with no knowledge of OTIO, it doesn't use the\n# same pattern as SerializableObject.\n_DECODER_FUNCTION_MAP = {\n    'RationalTime.1': _decoded_time,\n    'TimeRange.1': _decoded_time_range,\n    'TimeTransform.1': _decoded_transform,\n}\n\n\ndef _as_otio(dct):\n    \"\"\" Specialized JSON decoder for OTIO base Objects.  \"\"\"\n\n    if \"OTIO_SCHEMA\" in dct:\n        schema_label = dct[\"OTIO_SCHEMA\"]\n\n        if schema_label in _DECODER_FUNCTION_MAP:\n            return _DECODER_FUNCTION_MAP[schema_label](dct)\n\n        schema_name = type_registry.schema_name_from_label(schema_label)\n        schema_version = type_registry.schema_version_from_label(schema_label)\n        del dct[\"OTIO_SCHEMA\"]\n\n        return type_registry.instance_from_schema(\n            schema_name,\n            schema_version,\n            dct\n        )\n\n    return dct\n\n\ndef deserialize_json_from_string(otio_string):\n    \"\"\" Deserialize a string containing JSON to OTIO objects. \"\"\"\n\n    return json.loads(otio_string, object_hook=_as_otio)\n\n\ndef deserialize_json_from_file(otio_filepath):\n    \"\"\" Deserialize the file at otio_filepath containing JSON to OTIO.  \"\"\"\n\n    with open(otio_filepath, 'r') as file_contents:\n        result = deserialize_json_from_string(file_contents.read())\n        result._json_path = otio_filepath\n        return result\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/core/media_reference.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Media Reference Classes and Functions.\"\"\"\n\nfrom .. import (\n    opentime,\n)\nfrom . import (\n    type_registry,\n    serializable_object,\n)\n\nimport copy\n\n\n@type_registry.register_type\nclass MediaReference(serializable_object.SerializableObject):\n    \"\"\"Base Media Reference Class.\n\n    Currently handles string printing the child classes, which expose interface\n    into its data dictionary.\n\n    The requirement is that the schema is named so that external systems can\n    fetch the required information correctly.\n    \"\"\"\n    _serializable_label = \"MediaReference.1\"\n    _name = \"MediaReference\"\n\n    def __init__(\n        self,\n        name=None,\n        available_range=None,\n        metadata=None\n    ):\n        super(MediaReference, self).__init__()\n\n        self.name = name\n        self.available_range = copy.deepcopy(available_range)\n        self.metadata = copy.deepcopy(metadata) or {}\n\n    name = serializable_object.serializable_field(\n        \"name\",\n        doc=\"Name of this media reference.\"\n    )\n    available_range = serializable_object.serializable_field(\n        \"available_range\",\n        opentime.TimeRange,\n        doc=\"Available range of media in this media reference.\"\n    )\n    metadata = serializable_object.serializable_field(\n        \"metadata\",\n        dict,\n        doc=\"Metadata dictionary.\"\n    )\n\n    @property\n    def is_missing_reference(self):\n        return False\n\n    def __str__(self):\n        return \"{}({}, {}, {})\".format(\n            self._name,\n            repr(self.name),\n            repr(self.available_range),\n            repr(self.metadata)\n        )\n\n    def __repr__(self):\n        return (\n            \"otio.schema.{}(\"\n            \"name={},\"\n            \" available_range={},\"\n            \" metadata={}\"\n            \")\"\n        ).format(\n            self._name,\n            repr(self.name),\n            repr(self.available_range),\n            repr(self.metadata)\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/core/serializable_object.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Implements the otio.core.SerializableObject\"\"\"\n\nimport copy\n\nfrom . import (\n    type_registry,\n)\n\n\nclass SerializableObject(object):\n    \"\"\"Base object for things that can be [de]serialized to/from .otio files.\n\n    To define a new child class of this, you inherit from it and also use the\n    register_type decorator.  Then you use the serializable_field function\n    above to create attributes that can be serialized/deserialized.\n\n    You can use the upgrade_function_for decorator to upgrade older schemas\n    to newer ones.\n\n    Finally, if you're in the process of upgrading schemas and you want to\n    catch code that refers to old attribute names, you can use the\n    deprecated_field function. This raises an exception if code attempts to\n    read or write to that attribute.  After testing and before pushing, please\n    remove references to deprecated_field.\n\n    For example\n\n    >>>    import opentimelineio as otio\n\n    >>>    @otio.core.register_type\n    ...    class ExampleChild(otio.core.SerializableObject):\n    ...        _serializable_label = \"ExampleChild.7\"\n    ...        child_data = otio.core.serializable_field(\"child_data\", int)\n\n    # @TODO: delete once testing shows nothing is referencing this.\n    >>>         old_child_data_name = otio.core.deprecated_field()\n\n    >>>    @otio.core.upgrade_function_for(ExampleChild, 3)\n    ...    def upgrade_child_to_three(_data):\n    ...        return {\"child_data\" : _data[\"old_child_data_name\"]}\n    \"\"\"\n\n    # Every child must define a _serializable_label attribute.\n    # This attribute is a string in the form of: \"SchemaName.VersionNumber\"\n    # Where VersionNumber is an integer.\n    # You can use the classmethods .schema_name() and .schema_version() to\n    # query these fields.\n    _serializable_label = None\n    _class_path = \"core.SerializableObject\"\n\n    def __init__(self):\n        self._data = {}\n\n    # @{ \"Reference Type\" semantics for SerializableObject\n    # We think of the SerializableObject as a reference type - by default\n    # comparison is pointer comparison, but you can use 'is_equivalent_to' to\n    # check if the contents of the SerializableObject are the same as some\n    # other SerializableObject's contents.\n    #\n    # Implicitly:\n    # def __eq__(self, other):\n    #     return self is other\n\n    def is_equivalent_to(self, other):\n        \"\"\"Returns true if the contents of self and other match.\"\"\"\n\n        try:\n            if self._data == other._data:\n                return True\n\n            # XXX: Gross hack takes OTIO->JSON String->Python Dictionaries\n            #\n            # using the serializer ensures that we only compare fields that are\n            # serializable, which is how we define equivalence.\n            #\n            # we use json.loads() to turn the string back into dictionaries\n            # so we can use python's equivalence for things like floating\n            # point numbers (ie 5.0 == 5) without having to do string\n            # processing.\n\n            from . import json_serializer\n            import json\n\n            lhs_str = json_serializer.serialize_json_to_string(self)\n            lhs = json.loads(lhs_str)\n\n            rhs_str = json_serializer.serialize_json_to_string(other)\n            rhs = json.loads(rhs_str)\n\n            return (lhs == rhs)\n        except AttributeError:\n            return False\n    # @}\n\n    def _update(self, d):\n        \"\"\"Like the dictionary .update() method.\n\n        Update the _data dictionary of this SerializableObject with the ._data\n        of d if d is a SerializableObject or if d is a dictionary, d itself.\n        \"\"\"\n\n        if isinstance(d, SerializableObject):\n            self._data.update(d._data)\n        else:\n            self._data.update(d)\n\n    @classmethod\n    def schema_name(cls):\n        return type_registry.schema_name_from_label(\n            cls._serializable_label\n        )\n\n    @classmethod\n    def schema_version(cls):\n        return type_registry.schema_version_from_label(\n            cls._serializable_label\n        )\n\n    @property\n    def is_unknown_schema(self):\n        # in general, SerializableObject will have a known schema\n        # but UnknownSchema subclass will redefine this property to be True\n        return False\n\n    def __copy__(self):\n        raise NotImplementedError(\n            \"Shallow copying is not permitted.  Use a deep copy.\"\n        )\n\n    def __deepcopy__(self, md):\n        result = type(self)()\n        result._data = copy.deepcopy(self._data, md)\n\n        return result\n\n    def deepcopy(self):\n        return self.__deepcopy__({})\n\n\ndef serializable_field(name, required_type=None, doc=None):\n    \"\"\"Create a serializable_field for child classes of SerializableObject.\n\n    Convienence function for adding attributes to child classes of\n    SerializableObject in such a way that they will be serialized/deserialized\n    automatically.\n\n    Use it like this:\n        class foo(SerializableObject):\n            bar = serializable_field(\"bar\", required_type=int, doc=\"example\")\n\n    This would indicate that class \"foo\" has a serializable field \"bar\".  So:\n        f = foo()\n        f.bar = \"stuff\"\n\n        # serialize & deserialize\n        otio_json = otio.adapters.from_name(\"otio\")\n        f2 = otio_json.read_from_string(otio_json.write_to_string(f))\n\n        # fields should be equal\n        f.bar == f2.bar\n\n    Additionally, the \"doc\" field will become the documentation for the\n    property.\n    \"\"\"\n\n    def getter(self):\n        return self._data[name]\n\n    def setter(self, val):\n        # always allow None values regardless of value of required_type\n        if required_type is not None and val is not None:\n            if not isinstance(val, required_type):\n                raise TypeError(\n                    \"attribute '{}' must be an instance of '{}', not: {}\".format(\n                        name,\n                        required_type,\n                        type(val)\n                    )\n                )\n\n        self._data[name] = val\n\n    return property(getter, setter, doc=doc)\n\n\ndef deprecated_field():\n    \"\"\" For marking attributes on a SerializableObject deprecated.  \"\"\"\n\n    def getter(self):\n        raise DeprecationWarning\n\n    def setter(self, val):\n        raise DeprecationWarning\n\n    return property(getter, setter, doc=\"Deprecated field, do not use.\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/core/type_registry.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Core type registry system for registering OTIO types for serialization.\"\"\"\n\nfrom .. import (\n    exceptions\n)\n\n\n# Types decorate use register_type() to insert themselves into this map\n_OTIO_TYPES = {}\n\n# maps types to a map of versions to upgrade functions\n_UPGRADE_FUNCTIONS = {}\n\n\ndef schema_name_from_label(label):\n    \"\"\"Return the schema name from the label name.\"\"\"\n\n    return label.split(\".\")[0]\n\n\ndef schema_version_from_label(label):\n    \"\"\"Return the schema version from the label name.\"\"\"\n\n    return int(label.split(\".\")[1])\n\n\ndef schema_label_from_name_version(schema_name, schema_version):\n    \"\"\"Return the serializeable object schema label given the name and version.\"\"\"\n\n    return \"{}.{}\".format(schema_name, schema_version)\n\n\ndef register_type(classobj, schemaname=None):\n    \"\"\" Register a class to a Schema Label.\n\n    Normally this is used as a decorator.  However, in special cases where a\n    type has been renamed, you might need to register the new type to multiple\n    schema names.  To do this:\n\n    >>>    @core.register_type\n    ...    class MyNewClass(...):\n    ...        pass\n\n    >>>    core.register_type(MyNewClass, \"MyOldName\")\n\n    This will parse the old schema name into the new class type.  You may also\n    need to write an upgrade function if the schema itself has changed.\n    \"\"\"\n\n    if schemaname is None:\n        schemaname = schema_name_from_label(classobj._serializable_label)\n\n    _OTIO_TYPES[schemaname] = classobj\n\n    return classobj\n\n\ndef upgrade_function_for(cls, version_to_upgrade_to):\n    \"\"\"Decorator for identifying schema class upgrade functions.\n\n    Example\n    >>>    @upgrade_function_for(MyClass, 5)\n    ...    def upgrade_to_version_five(data):\n    ...        pass\n\n    This will get called to upgrade a schema of MyClass to version 5.  My class\n    must be a class deriving from otio.core.SerializableObject.\n\n    The upgrade function should take a single argument - the dictionary to\n    upgrade, and return a dictionary with the fields upgraded.\n\n    Remember that you don't need to provide an upgrade function for upgrades\n    that add or remove fields, only for schema versions that change the field\n    names.\n    \"\"\"\n\n    def decorator_func(func):\n        \"\"\" Decorator for marking upgrade functions \"\"\"\n\n        _UPGRADE_FUNCTIONS.setdefault(cls, {})[version_to_upgrade_to] = func\n\n        return func\n\n    return decorator_func\n\n\ndef instance_from_schema(schema_name, schema_version, data_dict):\n    \"\"\"Return an instance, of the schema from data in the data_dict.\"\"\"\n\n    if schema_name not in _OTIO_TYPES:\n        from .unknown_schema import UnknownSchema\n\n        # create an object of UnknownSchema type to represent the data\n        schema_label = schema_label_from_name_version(schema_name, schema_version)\n        data_dict[UnknownSchema._original_label] = schema_label\n        unknown_label = UnknownSchema._serializable_label\n        schema_name = schema_name_from_label(unknown_label)\n        schema_version = schema_version_from_label(unknown_label)\n\n    cls = _OTIO_TYPES[schema_name]\n\n    schema_version = int(schema_version)\n    if cls.schema_version() < schema_version:\n        raise exceptions.UnsupportedSchemaError(\n            \"Schema '{}' has highest version available '{}', which is lower \"\n            \"than requested schema version '{}'\".format(\n                schema_name,\n                cls.schema_version(),\n                schema_version\n            )\n        )\n\n    if cls.schema_version() != schema_version:\n        # since the keys are the versions to upgrade to, sorting the keys\n        # before iterating through them should ensure that upgrade functions\n        # are called in order.\n        for version, upgrade_func in sorted(\n            _UPGRADE_FUNCTIONS[cls].items()\n        ):\n            if version < schema_version:\n                continue\n\n            data_dict = upgrade_func(data_dict)\n\n    obj = cls()\n    obj._update(data_dict)\n\n    return obj\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/core/unknown_schema.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"\nImplementation of the UnknownSchema schema.\n\"\"\"\n\nfrom .serializable_object import SerializableObject\nfrom .type_registry import register_type\n\n\n@register_type\nclass UnknownSchema(SerializableObject):\n    \"\"\"Represents an object whose schema is unknown to us.\"\"\"\n\n    _serializable_label = \"UnknownSchema.1\"\n    _name = \"UnknownSchema\"\n    _original_label = \"UnknownSchemaOriginalLabel\"\n\n    @property\n    def is_unknown_schema(self):\n        return True\n\n    @property\n    def data(self):\n        \"\"\"Exposes the data dictionary of the underlying SerializableObject\n        directly.\n        \"\"\"\n        return self._data\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/exceptions.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Exception classes for OpenTimelineIO\"\"\"\n\n\nclass OTIOError(Exception):\n    pass\n\n\nclass CouldNotReadFileError(OTIOError):\n    pass\n\n\nclass NoKnownAdapterForExtensionError(OTIOError):\n    pass\n\n\nclass ReadingNotSupportedError(OTIOError):\n    pass\n\n\nclass WritingNotSupportedError(OTIOError):\n    pass\n\n\nclass NotSupportedError(OTIOError):\n    pass\n\n\nclass InvalidSerializableLabelError(OTIOError):\n    pass\n\n\nclass CannotComputeAvailableRangeError(OTIOError):\n    pass\n\n\nclass AdapterDoesntSupportFunctionError(OTIOError):\n    pass\n\n\nclass UnsupportedSchemaError(OTIOError):\n    pass\n\n\nclass NotAChildError(OTIOError):\n    pass\n\n\nclass InstancingNotAllowedError(OTIOError):\n    pass\n\n\nclass TransitionFollowingATransitionError(OTIOError):\n    pass\n\n\nclass MisconfiguredPluginError(OTIOError):\n    pass\n\n\nclass CannotTrimTransitionsError(OTIOError):\n    pass\n\n\nclass NoDefaultMediaLinkerError(OTIOError):\n    pass\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/hooks.py",
    "content": "#\n# Copyright 2018 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\nfrom . import (\n    plugins,\n    core,\n)\n\n__doc__ = \"\"\"\nHookScripts are plugins that run at defined points (\"Hooks\").\n\nThey expose a hook_function with signature:\nhook_function :: otio.schema.Timeline, Dict -> otio.schema.Timeline\n\nBoth hook scripts and the hooks they attach to are defined in the plugin\nmanifest.\n\nYou can attach multiple hook scripts to a hook.  They will be executed in list\norder, first to last.\n\nThey are defined by the manifests HookScripts and hooks areas.\n\n>>>\n{\n    \"OTIO_SCHEMA\" : \"PluginManifest.1\",\n    \"hook_scripts\" : [\n        {\n            \"OTIO_SCHEMA\" : \"HookScript.1\",\n            \"name\" : \"example hook\",\n            \"execution_scope\" : \"in process\",\n            \"filepath\" : \"example.py\"\n        }\n    ],\n    \"hooks\" : {\n        \"pre_adapter_write\" : [\"example hook\"],\n        \"post_adapter_read\" : []\n    }\n}\n\nThe 'hook_scripts' area loads the python modules with the 'hook_function's to\ncall in them.  The 'hooks' area defines the hooks (and any associated\nscripts).  You can further query and modify these from python.\n\n>>> import opentimelineio as otio\n... hook_list = otio.hooks.scripts_attached_to(\"some_hook\") # -> ['a','b','c']\n...\n... # to run the hook scripts:\n... otio.hooks.run(\"some_hook\", some_timeline, optional_argument_dict)\n\nThis will pass (some_timeline, optional_argument_dict) to 'a', which will\na new timeline that will get passed into 'b' with optional_argument_dict,\netc.\n\nTo Edit the order, change the order in the list:\n\n>>> hook_list[0], hook_list[2] = hook_list[2], hook_list[0]\n... print hook_list # ['c','b','a']\n\nNow c will run, then b, then a.\n\nTo delete a function the list:\n\n>>> del hook_list[1]\n\"\"\"\n\n\n@core.register_type\nclass HookScript(plugins.PythonPlugin):\n    _serializable_label = \"HookScript.1\"\n\n    def __init__(\n        self,\n        name=None,\n        execution_scope=None,\n        filepath=None,\n    ):\n        \"\"\"HookScript plugin constructor.\"\"\"\n\n        super(HookScript, self).__init__(name, execution_scope, filepath)\n\n    def run(self, in_timeline, argument_map={}):\n        \"\"\"Run the hook_function associated with this plugin.\"\"\"\n\n        # @TODO: should in_timeline be passed in place?  or should a copy be\n        #        made?\n        return self._execute_function(\n            \"hook_function\",\n            in_timeline=in_timeline,\n            argument_map=argument_map\n        )\n\n    def __str__(self):\n        return \"HookScript({}, {}, {})\".format(\n            repr(self.name),\n            repr(self.execution_scope),\n            repr(self.filepath)\n        )\n\n    def __repr__(self):\n        return (\n            \"otio.hooks.HookScript(\"\n            \"name={}, \"\n            \"execution_scope={}, \"\n            \"filepath={}\"\n            \")\".format(\n                repr(self.name),\n                repr(self.execution_scope),\n                repr(self.filepath)\n            )\n        )\n\n\ndef names():\n    \"\"\"Return a list of all the registered hooks.\"\"\"\n\n    return plugins.ActiveManifest().hooks.keys()\n\n\ndef available_hookscript_names():\n    \"\"\"Return the names of HookScripts that have been registered.\"\"\"\n\n    return [hs.name for hs in plugins.ActiveManifest().hook_scripts]\n\n\ndef available_hookscripts():\n    \"\"\"Return the HookScripts objects that have been registered.\"\"\"\n    return plugins.ActiveManifest().hook_scripts\n\n\ndef scripts_attached_to(hook):\n    \"\"\"Return an editable list of all the hook scriptss that are attached to\n    the specified hook, in execution order.  Changing this list will change the\n    order that scripts run in, and deleting a script will remove it from\n    executing\n    \"\"\"\n\n    # @TODO: Should this return a copy?\n    return plugins.ActiveManifest().hooks[hook]\n\n\ndef run(hook, tl, extra_args=None):\n    \"\"\"Run all the scripts associated with hook, passing in tl and extra_args.\n\n    Will return the return value of the last hook script.\n\n    If no hookscripts are defined, returns tl.\n    \"\"\"\n\n    hook_scripts = plugins.ActiveManifest().hooks[hook]\n    for name in hook_scripts:\n        hs = plugins.ActiveManifest().from_name(name, \"hook_scripts\")\n        tl = hs.run(tl, extra_args)\n    return tl\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/media_linker.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\" MediaLinker plugins fire after an adapter has read a file in order to\nproduce MediaReferences that point at valid, site specific media.\n\nThey expose a \"link_media_reference\" function with the signature:\nlink_media_reference :: otio.schema.Clip -> otio.core.MediaReference\n\nor:\n    def linked_media_reference(from_clip):\n        result = otio.core.MediaReference() # whichever subclass\n        # do stuff\n        return result\n\nTo get context information, they can inspect the metadata on the clip and on\nthe media reference.  The .parent() method can be used to find the containing\ntrack if metadata is stored there.\n\nPlease raise an instance (or child instance) of\notio.exceptions.CannotLinkMediaError() if there is a problem linking the media.\n\nFor example:\n    for clip in timeline.each_clip():\n        try:\n            new_mr = otio.media_linker.linked_media_reference(clip)\n            clip.media_reference = new_mr\n        except otio.exceptions.CannotLinkMediaError:\n            # or report the error\n            pass\n\"\"\"\n\nimport os\n\nfrom . import (\n    exceptions,\n    plugins,\n    core,\n)\n\n\n# Enum describing different media linker policies\nclass MediaLinkingPolicy:\n    DoNotLinkMedia = \"__do_not_link_media\"\n    ForceDefaultLinker = \"__default\"\n\n\n# @TODO: wrap this up in the plugin system somehow?  automatically generate?\ndef available_media_linker_names():\n    \"\"\"Return a string list of the available media linker plugins.\"\"\"\n\n    return [str(adp.name) for adp in plugins.ActiveManifest().media_linkers]\n\n\ndef from_name(name):\n    \"\"\"Fetch the media linker object by the name of the adapter directly.\"\"\"\n\n    if name == MediaLinkingPolicy.ForceDefaultLinker or not name:\n        name = os.environ.get(\"OTIO_DEFAULT_MEDIA_LINKER\", None)\n\n    if not name:\n        return None\n\n    # @TODO: make this handle the enums\n    try:\n        return plugins.ActiveManifest().from_name(\n            name,\n            kind_list=\"media_linkers\"\n        )\n    except exceptions.NotSupportedError:\n        raise exceptions.NotSupportedError(\n            \"media linker not supported: {}, available: {}\".format(\n                name,\n                available_media_linker_names()\n            )\n        )\n\n\ndef default_media_linker():\n    try:\n        return os.environ['OTIO_DEFAULT_MEDIA_LINKER']\n    except KeyError:\n        raise exceptions.NoDefaultMediaLinkerError(\n            \"No default Media Linker set in $OTIO_DEFAULT_MEDIA_LINKER\"\n        )\n\n\ndef linked_media_reference(\n    target_clip,\n    media_linker_name=MediaLinkingPolicy.ForceDefaultLinker,\n    media_linker_argument_map=None\n):\n    media_linker = from_name(media_linker_name)\n\n    if not media_linker:\n        return target_clip\n\n    # @TODO: connect this argument map up to the function call through to the\n    #        real linker\n    if not media_linker_argument_map:\n        media_linker_argument_map = {}\n\n    return media_linker.link_media_reference(\n        target_clip,\n        media_linker_argument_map\n    )\n\n\n@core.register_type\nclass MediaLinker(plugins.PythonPlugin):\n    _serializable_label = \"MediaLinker.1\"\n\n    def __init__(\n        self,\n        name=None,\n        execution_scope=None,\n        filepath=None,\n    ):\n        super(MediaLinker, self).__init__(name, execution_scope, filepath)\n\n    def link_media_reference(self, in_clip, media_linker_argument_map=None):\n        media_linker_argument_map = media_linker_argument_map or {}\n\n        return self._execute_function(\n            \"link_media_reference\",\n            in_clip=in_clip,\n            media_linker_argument_map=media_linker_argument_map\n        )\n\n    def __str__(self):\n        return \"MediaLinker({}, {}, {})\".format(\n            repr(self.name),\n            repr(self.execution_scope),\n            repr(self.filepath)\n        )\n\n    def __repr__(self):\n        return (\n            \"otio.media_linker.MediaLinker(\"\n            \"name={}, \"\n            \"execution_scope={}, \"\n            \"filepath={}\"\n            \")\".format(\n                repr(self.name),\n                repr(self.execution_scope),\n                repr(self.filepath)\n            )\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/opentime.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Library for expressing and transforming time.\n\nNOTE: This module is written specifically with a future port to C in mind.\nWhen ported to C, Time will be a struct and these functions should be very\nsimple.\n\"\"\"\n\nimport math\nimport copy\n\n\nVALID_NON_DROPFRAME_TIMECODE_RATES = (\n    1,\n    12,\n    23.976,\n    23.98,\n    (24000 / 1001.0),\n    24,\n    25,\n    30,\n    29.97,\n    (30000 / 1001.0),\n    48,\n    50,\n    59.94,\n    (60000 / 1001.0),\n    60,\n)\n\nVALID_DROPFRAME_TIMECODE_RATES = (\n    29.97,\n    (30000 / 1001.0),\n    59.94,\n    (60000 / 1001.0),\n)\n\nVALID_TIMECODE_RATES = (\n    VALID_NON_DROPFRAME_TIMECODE_RATES + VALID_DROPFRAME_TIMECODE_RATES)\n\n_fn_cache = object.__setattr__\n\n\nclass RationalTime(object):\n    \"\"\" Represents an instantaneous point in time, value * (1/rate) seconds\n    from time 0seconds.\n    \"\"\"\n\n    # Locks RationalTime instances to only these attributes\n    __slots__ = ['value', 'rate']\n\n    def __init__(self, value=0.0, rate=1.0):\n        _fn_cache(self, \"value\", value)\n        _fn_cache(self, \"rate\", rate)\n\n    def __setattr__(self, key, val):\n        \"\"\"Enforces immutability \"\"\"\n        raise AttributeError(\"RationalTime is Immutable.\")\n\n    def __copy__(self, memodict=None):\n        return RationalTime(self.value, self.rate)\n\n    # Always deepcopy, since we want this class to behave like a value type\n    __deepcopy__ = __copy__\n\n    def rescaled_to(self, new_rate):\n        \"\"\"Returns the time for this time converted to new_rate\"\"\"\n\n        try:\n            new_rate = new_rate.rate\n        except AttributeError:\n            pass\n\n        if self.rate == new_rate:\n            return copy.copy(self)\n\n        return RationalTime(\n            self.value_rescaled_to(new_rate),\n            new_rate\n        )\n\n    def value_rescaled_to(self, new_rate):\n        \"\"\"Returns the time value for self converted to new_rate\"\"\"\n\n        try:\n            new_rate = new_rate.rate\n        except AttributeError:\n            pass\n\n        if new_rate == self.rate:\n            return self.value\n\n        # TODO: This math probably needs some overrun protection\n        try:\n            return float(self.value) * float(new_rate) / float(self.rate)\n        except (AttributeError, TypeError, ValueError):\n            raise TypeError(\n                \"Sorry, RationalTime cannot be rescaled to a value of type \"\n                \"'{}', only RationalTime and numbers are supported.\".format(\n                    type(new_rate)\n                )\n            )\n\n    def almost_equal(self, other, delta=0.0):\n        try:\n            rescaled_value = self.value_rescaled_to(other.rate)\n            return abs(rescaled_value - other.value) <= delta\n\n        except AttributeError:\n            return False\n\n    def __add__(self, other):\n        \"\"\"Returns a RationalTime object that is the sum of self and other.\n\n        If self and other have differing time rates, the result will have the\n        have the rate of the faster time.\n        \"\"\"\n\n        try:\n            if self.rate == other.rate:\n                return RationalTime(self.value + other.value, self.rate)\n        except AttributeError:\n            if not isinstance(other, RationalTime):\n                raise TypeError(\n                    \"RationalTime may only be added to other objects of type \"\n                    \"RationalTime, not {}.\".format(type(other))\n                )\n            raise\n\n        if self.rate > other.rate:\n            scale = self.rate\n            value = self.value + other.value_rescaled_to(scale)\n        else:\n            scale = other.rate\n            value = self.value_rescaled_to(scale) + other.value\n\n        return RationalTime(value, scale)\n\n    # because RationalTime is immutable, += is sugar around +\n    __iadd__ = __add__\n\n    def __sub__(self, other):\n        \"\"\"Returns a RationalTime object that is self - other.\n\n        If self and other have differing time rates, the result will have the\n        have the rate of the faster time.\n        \"\"\"\n\n        try:\n            if self.rate == other.rate:\n                return RationalTime(self.value - other.value, self.rate)\n        except AttributeError:\n            if not isinstance(other, RationalTime):\n                raise TypeError(\n                    \"RationalTime may only be added to other objects of type \"\n                    \"RationalTime, not {}.\".format(type(other))\n                )\n            raise\n\n        if self.rate > other.rate:\n            scale = self.rate\n            value = self.value - other.value_rescaled_to(scale)\n        else:\n            scale = other.rate\n            value = self.value_rescaled_to(scale) - other.value\n\n        return RationalTime(value=value, rate=scale)\n\n    def _comparable_floats(self, other):\n        \"\"\"Returns a tuple of two floats, (self, other), which are suitable\n        for comparison.\n\n        If other is not of a type that can be compared, TypeError is raised\n        \"\"\"\n        try:\n            return (\n                float(self.value) / self.rate,\n                float(other.value) / other.rate\n            )\n        except AttributeError:\n            if not isinstance(other, RationalTime):\n                raise TypeError(\n                    \"RationalTime can only be compared to other objects of type \"\n                    \"RationalTime, not {}\".format(type(other))\n                )\n            raise\n\n    def __gt__(self, other):\n        f_self, f_other = self._comparable_floats(other)\n        return f_self > f_other\n\n    def __lt__(self, other):\n        f_self, f_other = self._comparable_floats(other)\n        return f_self < f_other\n\n    def __le__(self, other):\n        f_self, f_other = self._comparable_floats(other)\n        return f_self <= f_other\n\n    def __ge__(self, other):\n        f_self, f_other = self._comparable_floats(other)\n        return f_self >= f_other\n\n    def __repr__(self):\n        return (\n            \"otio.opentime.RationalTime(value={value},\"\n            \" rate={rate})\".format(\n                value=repr(self.value),\n                rate=repr(self.rate),\n            )\n        )\n\n    def __str__(self):\n        return \"RationalTime({}, {})\".format(\n            str(self.value),\n            str(self.rate)\n        )\n\n    def __eq__(self, other):\n        try:\n            return self.value_rescaled_to(other.rate) == other.value\n        except AttributeError:\n            return False\n\n    def __ne__(self, other):\n        return not (self == other)\n\n    def __hash__(self):\n        return hash((self.value, self.rate))\n\n\nclass TimeTransform(object):\n    \"\"\"1D Transform for RationalTime.  Has offset and scale.\"\"\"\n\n    def __init__(self, offset=RationalTime(), scale=1.0, rate=None):\n        self.offset = copy.copy(offset)\n        self.scale = float(scale)\n        self.rate = float(rate) if rate else None\n\n    def applied_to(self, other):\n        if isinstance(other, TimeRange):\n            return range_from_start_end_time(\n                start_time=self.applied_to(other.start_time),\n                end_time_exclusive=self.applied_to(other.end_time_exclusive())\n            )\n\n        target_rate = self.rate if self.rate is not None else other.rate\n        if isinstance(other, TimeTransform):\n            return TimeTransform(\n                offset=self.offset + other.offset,\n                scale=self.scale * other.scale,\n                rate=target_rate\n            )\n        elif isinstance(other, RationalTime):\n            value = other.value * self.scale\n            result = RationalTime(value, other.rate) + self.offset\n            if target_rate is not None:\n                result = result.rescaled_to(target_rate)\n\n            return result\n        else:\n            raise TypeError(\n                \"TimeTransform can only be applied to a TimeTransform or \"\n                \"RationalTime, not a {}\".format(type(other))\n            )\n\n    def __repr__(self):\n        return (\n            \"otio.opentime.TimeTransform(offset={}, scale={}, rate={})\".format(\n                repr(self.offset),\n                repr(self.scale),\n                repr(self.rate)\n            )\n        )\n\n    def __str__(self):\n        return (\n            \"TimeTransform({}, {}, {})\".format(\n                str(self.offset),\n                str(self.scale),\n                str(self.rate)\n            )\n        )\n\n    def __eq__(self, other):\n        try:\n            return (\n                (self.offset, self.scale, self.rate) ==\n                (other.offset, other.scale, self.rate)\n            )\n        except AttributeError:\n            return False\n\n    def __ne__(self, other):\n        return not (self == other)\n\n    def __hash__(self):\n        return hash((self.offset, self.scale, self.rate))\n\n\nclass BoundStrategy(object):\n    \"\"\"Different bounding strategies for TimeRange \"\"\"\n\n    Free = 1\n    Clamp = 2\n\n\nclass TimeRange(object):\n    \"\"\"Contains a range of time, starting (and including) start_time and\n    lasting duration.value * (1/duration.rate) seconds.\n\n    A 0 duration TimeRange is the same as a RationalTime, and contains only the\n    start_time of the TimeRange.\n    \"\"\"\n\n    __slots__ = ['start_time', 'duration']\n\n    def __init__(self, start_time=None, duration=None):\n        if not isinstance(start_time, RationalTime) and start_time is not None:\n            raise TypeError(\n                \"start_time must be a RationalTime, not \"\n                \"'{}'\".format(start_time)\n            )\n        if (\n                duration is not None and (\n                    not isinstance(duration, RationalTime)\n                    or duration.value < 0.0\n                )\n        ):\n            raise TypeError(\n                \"duration must be a RationalTime with value >= 0, not \"\n                \"'{}'\".format(duration)\n            )\n\n        # if the start time has not been passed in\n        if not start_time:\n            if duration:\n                # ...get the rate from the duration\n                start_time = RationalTime(rate=duration.rate)\n            else:\n                # otherwise use the default\n                start_time = RationalTime()\n        _fn_cache(self, \"start_time\", copy.copy(start_time))\n\n        if not duration:\n            # ...get the rate from the start_time\n            duration = RationalTime(rate=start_time.rate)\n        _fn_cache(self, \"duration\", copy.copy(duration))\n\n    def __setattr__(self, key, val):\n        raise AttributeError(\"TimeRange is Immutable.\")\n\n    def __copy__(self, memodict=None):\n        # Construct a new one directly to avoid the overhead of deepcopy\n        return TimeRange(\n            copy.copy(self.start_time),\n            copy.copy(self.duration)\n        )\n\n    # Always deepcopy, since we want this class to behave like a value type\n    __deepcopy__ = __copy__\n\n    def end_time_inclusive(self):\n        \"\"\"The time of the last sample that contains data in the TimeRange.\n\n        If the TimeRange goes from (0, 24) w/ duration (10, 24), this will be\n        (9, 24)\n\n        If the TimeRange goes from (0, 24) w/ duration (10.5, 24):\n        (10, 24)\n\n        In other words, the last frame with data (however fractional).\n        \"\"\"\n\n        if (\n            self.end_time_exclusive() - self.start_time.rescaled_to(self.duration)\n        ).value > 1:\n\n            result = (\n                self.end_time_exclusive() - RationalTime(1, self.start_time.rate)\n            )\n\n            # if the duration's value has a fractional component\n            if self.duration.value != math.floor(self.duration.value):\n                result = RationalTime(\n                    math.floor(self.end_time_exclusive().value),\n                    result.rate\n                )\n\n            return result\n        else:\n            return copy.deepcopy(self.start_time)\n\n    def end_time_exclusive(self):\n        \"\"\"\"Time of the first sample outside the time range.\n\n        If Start Frame is 10 and duration is 5, then end_time_exclusive is 15,\n        even though the last time with data in this range is 14.\n\n        If Start Frame is 10 and duration is 5.5, then end_time_exclusive is\n        15.5, even though the last time with data in this range is 15.\n        \"\"\"\n\n        return self.duration + self.start_time.rescaled_to(self.duration)\n\n    def extended_by(self, other):\n        \"\"\"Construct a new TimeRange that is this one extended by another.\"\"\"\n\n        if not isinstance(other, TimeRange):\n            raise TypeError(\n                \"extended_by requires rtime be a TimeRange, not a '{}'\".format(\n                    type(other)\n                )\n            )\n\n        start_time = min(self.start_time, other.start_time)\n        new_end_time = max(\n            self.end_time_exclusive(),\n            other.end_time_exclusive()\n        )\n        duration = duration_from_start_end_time(start_time, new_end_time)\n        return TimeRange(start_time, duration)\n\n    # @TODO: remove?\n    def clamped(\n        self,\n        other,\n        start_bound=BoundStrategy.Free,\n        end_bound=BoundStrategy.Free\n    ):\n        \"\"\"Clamp 'other' (either a RationalTime or a TimeRange), according to\n        self.start_time/end_time_exclusive and the bound arguments.\n        \"\"\"\n\n        if isinstance(other, RationalTime):\n            if start_bound == BoundStrategy.Clamp:\n                other = max(other, self.start_time)\n            if end_bound == BoundStrategy.Clamp:\n                # @TODO: this should probably be the end_time_inclusive,\n                # not exclusive\n                other = min(other, self.end_time_exclusive())\n            return other\n        elif isinstance(other, TimeRange):\n            start_time = other.start_time\n            end = other.end_time_exclusive()\n            if start_bound == BoundStrategy.Clamp:\n                start_time = max(other.start_time, self.start_time)\n            if end_bound == BoundStrategy.Clamp:\n                end = min(self.end_time_exclusive(), end)\n            duration = duration_from_start_end_time(start_time, end)\n            return TimeRange(start_time, duration)\n        else:\n            raise TypeError(\n                \"TimeRange can only be applied to RationalTime objects, not \"\n                \"{}\".format(type(other))\n            )\n        return self\n\n    def contains(self, other):\n        \"\"\"Return true if self completely contains other.\n\n        (RationalTime or TimeRange)\n        \"\"\"\n\n        if isinstance(other, RationalTime):\n            return (\n                self.start_time <= other and other < self.end_time_exclusive())\n        elif isinstance(other, TimeRange):\n            return (\n                self.start_time <= other.start_time and\n                self.end_time_exclusive() >= other.end_time_exclusive()\n            )\n        raise TypeError(\n            \"contains only accepts on otio.opentime.RationalTime or \"\n            \"otio.opentime.TimeRange, not {}\".format(type(other))\n        )\n\n    def overlaps(self, other):\n        \"\"\"Return true if self overlaps any part of other.\n\n        (RationalTime or TimeRange)\n        \"\"\"\n\n        if isinstance(other, RationalTime):\n            return self.contains(other)\n        elif isinstance(other, TimeRange):\n            return (\n                (\n                    self.start_time < other.end_time_exclusive() and\n                    other.start_time < self.end_time_exclusive()\n                )\n            )\n        raise TypeError(\n            \"overlaps only accepts on otio.opentime.RationalTime or \"\n            \"otio.opentime.TimeRange, not {}\".format(type(other))\n        )\n\n    def __hash__(self):\n        return hash((self.start_time, self.duration))\n\n    def __eq__(self, rhs):\n        try:\n            return (\n                (self.start_time, self.duration) ==\n                (rhs.start_time, rhs.duration)\n            )\n        except AttributeError:\n            return False\n\n    def __ne__(self, rhs):\n        return not (self == rhs)\n\n    def __repr__(self):\n        return (\n            \"otio.opentime.TimeRange(start_time={}, duration={})\".format(\n                repr(self.start_time),\n                repr(self.duration),\n            )\n        )\n\n    def __str__(self):\n        return (\n            \"TimeRange({}, {})\".format(\n                str(self.start_time),\n                str(self.duration),\n            )\n        )\n\n\ndef from_frames(frame, fps):\n    \"\"\"Turn a frame number and fps into a time object.\n    :param frame: (:class:`int`) Frame number.\n    :param fps: (:class:`float`) Frame-rate for the (:class:`RationalTime`) instance.\n\n    :return: (:class:`RationalTime`) Instance for the frame and fps provided.\n    \"\"\"\n\n    return RationalTime(int(frame), fps)\n\n\ndef to_frames(time_obj, fps=None):\n    \"\"\"Turn a RationalTime into a frame number.\"\"\"\n\n    if not fps or time_obj.rate == fps:\n        return int(time_obj.value)\n\n    return int(time_obj.value_rescaled_to(fps))\n\n\ndef validate_timecode_rate(rate):\n    \"\"\"Check if rate is of valid type and value.\n    Raises (:class:`TypeError` for wrong type of rate.\n    Raises (:class:`VaueError`) for invalid rate value.\n\n    :param rate: (:class:`int`) or (:class:`float`) The frame rate in question\n    \"\"\"\n    if not isinstance(rate, (int, float)):\n        raise TypeError(\n            \"rate must be <float> or <int> not {t}\".format(t=type(rate)))\n\n    if rate not in VALID_TIMECODE_RATES:\n        raise ValueError(\n            '{rate} is not a valid frame rate, '\n            'Please use one of these: {valid}'.format(\n                rate=rate,\n                valid=VALID_TIMECODE_RATES))\n\n\ndef from_timecode(timecode_str, rate):\n    \"\"\"Convert a timecode string into a RationalTime.\n\n    :param timecode_str: (:class:`str`) A colon-delimited timecode.\n    :param rate: (:class:`float`) The frame-rate to calculate timecode in\n        terms of.\n\n    :return: (:class:`RationalTime`) Instance for the timecode provided.\n    \"\"\"\n    # Validate rate\n    validate_timecode_rate(rate)\n\n    # Check if rate is drop frame\n    rate_is_dropframe = rate in VALID_DROPFRAME_TIMECODE_RATES\n\n    # Make sure only DF timecodes are treated as such\n    treat_as_df = rate_is_dropframe and ';' in timecode_str\n\n    # Check if timecode indicates drop frame\n    if ';' in timecode_str:\n        if not rate_is_dropframe:\n            raise ValueError(\n                'Timecode \"{}\" indicates drop-frame rate '\n                'due to the \";\" frame divider. '\n                'Passed rate ({}) is of non-drop-frame rate. '\n                'Valid drop-frame rates are: {}'.format(\n                    timecode_str,\n                    rate,\n                    VALID_DROPFRAME_TIMECODE_RATES))\n        else:\n            timecode_str = timecode_str.replace(';', ':')\n\n    hours, minutes, seconds, frames = timecode_str.split(\":\")\n\n    # Timecode is declared in terms of nominal fps\n    nominal_fps = int(math.ceil(rate))\n\n    if int(frames) >= nominal_fps:\n        raise ValueError(\n            'Frame rate mismatch. Timecode \"{}\" has frames beyond {}.'.format(\n                timecode_str, nominal_fps - 1))\n\n    dropframes = 0\n    if treat_as_df:\n        if rate == 29.97:\n            dropframes = 2\n\n        elif rate == 59.94:\n            dropframes = 4\n\n    # To use for drop frame compensation\n    total_minutes = int(hours) * 60 + int(minutes)\n\n    # convert to frames\n    value = (\n        ((total_minutes * 60) + int(seconds)) * nominal_fps + int(frames)) - \\\n        (dropframes * (total_minutes - (total_minutes // 10)))\n\n    return RationalTime(value, rate)\n\n\ndef to_timecode(time_obj, rate=None, drop_frame=None):\n    \"\"\"Convert a RationalTime into a timecode string.\n\n    :param time_obj: (:class:`RationalTime`) instance to express as timecode.\n    :param rate: (:class:`float`) The frame-rate to calculate timecode in\n        terms of. (Default time_obj.rate)\n    :param drop_frame: (:class:`bool`) ``True`` to make drop-frame timecode,\n        ``False`` for non-drop. If left ``None``, a format will be guessed\n        based on rate.\n\n    :return: (:class:`str`) The timecode.\n    \"\"\"\n    if time_obj is None:\n        return None\n\n    rate = rate or time_obj.rate\n\n    # Validate rate\n    validate_timecode_rate(rate)\n\n    # Check if rate is drop frame\n    rate_is_dropframe = rate in VALID_DROPFRAME_TIMECODE_RATES\n    if drop_frame and not rate_is_dropframe:\n        raise ValueError(\n            \"Invalid rate for drop-frame timecode {}\".format(time_obj.rate)\n        )\n\n    # if in auto-detect for DFTC, use the rate to decide\n    if drop_frame is None:\n        drop_frame = rate_is_dropframe\n\n    dropframes = 0\n    if drop_frame:\n        if rate in (29.97, (30000 / 1001.0)):\n            dropframes = 2\n\n        elif rate == 59.94:\n            dropframes = 4\n\n    # For non-dftc, use the integral frame rate\n    if not drop_frame:\n        rate = round(rate)\n\n    # Number of frames in an hour\n    frames_per_hour = int(round(rate * 60 * 60))\n    # Number of frames in a day - timecode rolls over after 24 hours\n    frames_per_24_hours = frames_per_hour * 24\n    # Number of frames per ten minutes\n    frames_per_10_minutes = int(round(rate * 60 * 10))\n    # Number of frames per minute is the round of the framerate * 60 minus\n    # the number of dropped frames\n    frames_per_minute = int(round(rate) * 60) - dropframes\n\n    value = time_obj.value\n\n    if value < 0:\n        raise ValueError(\n            \"Negative values are not supported for converting to timecode.\")\n\n    # If frame_number is greater than 24 hrs, next operation will rollover\n    # clock\n    value %= frames_per_24_hours\n\n    if drop_frame:\n        d = value // frames_per_10_minutes\n        m = value % frames_per_10_minutes\n        if m > dropframes:\n            value += (dropframes * 9 * d) + \\\n                dropframes * ((m - dropframes) // frames_per_minute)\n        else:\n            value += dropframes * 9 * d\n\n    nominal_fps = int(math.ceil(rate))\n\n    frames = value % nominal_fps\n    seconds = (value // nominal_fps) % 60\n    minutes = ((value // nominal_fps) // 60) % 60\n    hours = (((value // nominal_fps) // 60) // 60)\n\n    tc = \"{HH:02d}:{MM:02d}:{SS:02d}{div}{FF:02d}\"\n\n    return tc.format(\n        HH=int(hours),\n        MM=int(minutes),\n        SS=int(seconds),\n        div=drop_frame and \";\" or \":\",\n        FF=int(frames))\n\n\ndef from_time_string(time_str, rate):\n    \"\"\"Convert a time with microseconds string into a RationalTime.\n\n    :param time_str: (:class:`str`) A HH:MM:ss.ms time.\n    :param rate: (:class:`float`) The frame-rate to calculate timecode in\n        terms of.\n\n    :return: (:class:`RationalTime`) Instance for the timecode provided.\n    \"\"\"\n\n    if ';' in time_str:\n        raise ValueError('Drop-Frame timecodes not supported.')\n\n    hours, minutes, seconds = time_str.split(\":\")\n    microseconds = \"0\"\n    if '.' in seconds:\n        seconds, microseconds = str(seconds).split('.')\n    microseconds = microseconds[0:6]\n    seconds = '.'.join([seconds, microseconds])\n    time_obj = from_seconds(\n        float(seconds) +\n        (int(minutes) * 60) +\n        (int(hours) * 60 * 60)\n    )\n    return time_obj.rescaled_to(rate)\n\n\ndef to_time_string(time_obj):\n    \"\"\"\n    Convert this timecode to time with microsecond, as formated in FFMPEG\n\n    :return: Number formated string of time\n    \"\"\"\n    if time_obj is None:\n        return None\n    # convert time object to seconds\n    seconds = to_seconds(time_obj)\n\n    # reformat in time string\n    time_units_per_minute = 60\n    time_units_per_hour = time_units_per_minute * 60\n    time_units_per_day = time_units_per_hour * 24\n\n    days, hour_units = divmod(seconds, time_units_per_day)\n    hours, minute_units = divmod(hour_units, time_units_per_hour)\n    minutes, seconds = divmod(minute_units, time_units_per_minute)\n    microseconds = \"0\"\n    seconds = str(seconds)\n    if '.' in seconds:\n        seconds, microseconds = str(seconds).split('.')\n\n    # TODO: There are some rollover policy issues for days and hours,\n    #       We need to research these\n\n    return \"{hours}:{minutes}:{seconds}.{microseconds}\".format(\n        hours=\"{n:0{width}d}\".format(n=int(hours), width=2),\n        minutes=\"{n:0{width}d}\".format(n=int(minutes), width=2),\n        seconds=\"{n:0{width}d}\".format(n=int(seconds), width=2),\n        microseconds=microseconds[0:6]\n    )\n\n\ndef from_seconds(seconds):\n    \"\"\"Convert a number of seconds into RationalTime\"\"\"\n\n    # Note: in the future we may consider adding a preferred rate arg\n    time_obj = RationalTime(value=seconds, rate=1)\n\n    return time_obj\n\n\ndef to_seconds(time_obj):\n    \"\"\" Convert a RationalTime into float seconds \"\"\"\n    return time_obj.value_rescaled_to(1)\n\n\ndef from_footage(footage):\n    raise NotImplementedError\n\n\ndef to_footage(time_obj):\n    raise NotImplementedError\n\n\ndef duration_from_start_end_time(start_time, end_time_exclusive):\n    \"\"\"Compute duration of samples from first to last. This is not the same as\n    distance.  For example, the duration of a clip from frame 10 to frame 15\n    is 6 frames.  Result in the rate of start_time.\n    \"\"\"\n\n    # @TODO: what to do when start_time > end_time_exclusive?\n\n    if start_time.rate == end_time_exclusive.rate:\n        return RationalTime(\n            end_time_exclusive.value - start_time.value,\n            start_time.rate\n        )\n    else:\n        return RationalTime(\n            (\n                end_time_exclusive.value_rescaled_to(start_time)\n                - start_time.value\n            ),\n            start_time.rate\n        )\n\n\n# @TODO: create range from start/end [in,ex]clusive\ndef range_from_start_end_time(start_time, end_time_exclusive):\n    \"\"\"Create a TimeRange from start and end RationalTimes.\"\"\"\n\n    return TimeRange(\n        start_time,\n        duration=duration_from_start_end_time(start_time, end_time_exclusive)\n    )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/plugins/__init__.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Plugin system for OTIO\"\"\"\n\n# flake8: noqa\n\nfrom .python_plugin import PythonPlugin\nfrom .manifest import (\n    manifest_from_file,\n    ActiveManifest,\n)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/plugins/manifest.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Implementation of an adapter registry system for OTIO.\"\"\"\n\nimport inspect\nimport logging\nimport os\n\n# on some python interpreters, pkg_resources is not available\ntry:\n    import pkg_resources\nexcept ImportError:\n    pkg_resources = None\n\nfrom .. import (\n    core,\n    exceptions,\n)\n\n\ndef manifest_from_file(filepath):\n    \"\"\"Read the .json file at filepath into a Manifest object.\"\"\"\n\n    result = core.deserialize_json_from_file(filepath)\n    result.source_files.append(filepath)\n    result._update_plugin_source(filepath)\n    return result\n\n\ndef manifest_from_string(input_string):\n    \"\"\"Deserialize the json string into a manifest object.\"\"\"\n\n    result = core.deserialize_json_from_string(input_string)\n\n    # try and get the caller's name\n    name = \"unknown\"\n    stack = inspect.stack()\n    if len(stack) > 1 and len(stack[1]) > 3:\n        #                     filename     function name\n        name = \"{}:{}\".format(stack[1][1], stack[1][3])\n\n    # set the value in the manifest\n    src_string = \"call to manifest_from_string() in \" + name\n    result.source_files.append(src_string)\n    result._update_plugin_source(src_string)\n\n    return result\n\n\n@core.register_type\nclass Manifest(core.SerializableObject):\n    \"\"\"Defines an OTIO plugin Manifest.\n\n    This is an internal OTIO implementation detail.  A manifest tracks a\n    collection of adapters and allows finding specific adapters by suffix\n\n    For writing your own adapters, consult:\n        https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html#\n    \"\"\"\n    _serializable_label = \"PluginManifest.1\"\n\n    def __init__(self):\n        super(Manifest, self).__init__()\n        self.adapters = []\n        self.schemadefs = []\n        self.media_linkers = []\n        self.source_files = []\n\n        # hook system stuff\n        self.hooks = {}\n        self.hook_scripts = []\n\n    adapters = core.serializable_field(\n        \"adapters\",\n        type([]),\n        \"Adapters this manifest describes.\"\n    )\n    schemadefs = core.serializable_field(\n        \"schemadefs\",\n        type([]),\n        \"Schemadefs this manifest describes.\"\n    )\n    media_linkers = core.serializable_field(\n        \"media_linkers\",\n        type([]),\n        \"Media Linkers this manifest describes.\"\n    )\n    hooks = core.serializable_field(\n        \"hooks\",\n        type({}),\n        \"Hooks that hooks scripts can be attached to.\"\n    )\n    hook_scripts = core.serializable_field(\n        \"hook_scripts\",\n        type([]),\n        \"Scripts that can be attached to hooks.\"\n    )\n\n    def extend(self, another_manifest):\n        \"\"\"\n        Extend the adapters, schemadefs, and media_linkers lists of this manifest\n        by appending the contents of the corresponding lists of another_manifest.\n        \"\"\"\n        if another_manifest:\n            self.adapters.extend(another_manifest.adapters)\n            self.schemadefs.extend(another_manifest.schemadefs)\n            self.media_linkers.extend(another_manifest.media_linkers)\n            self.hook_scripts.extend(another_manifest.hook_scripts)\n\n            for trigger_name, hooks in another_manifest.hooks.items():\n                if trigger_name in self.hooks:\n                    self.hooks[trigger_name].extend(hooks)\n\n    def _update_plugin_source(self, path):\n        \"\"\"Track the source .json for a given adapter.\"\"\"\n\n        for thing in (self.adapters + self.schemadefs\n                      + self.media_linkers + self.hook_scripts):\n            thing._json_path = path\n\n    def from_filepath(self, suffix):\n        \"\"\"Return the adapter object associated with a given file suffix.\"\"\"\n\n        for adapter in self.adapters:\n            if suffix.lower() in adapter.suffixes:\n                return adapter\n        raise exceptions.NoKnownAdapterForExtensionError(suffix)\n\n    def adapter_module_from_suffix(self, suffix):\n        \"\"\"Return the adapter module associated with a given file suffix.\"\"\"\n\n        adp = self.from_filepath(suffix)\n        return adp.module()\n\n    def from_name(self, name, kind_list=\"adapters\"):\n        \"\"\"Return the adapter object associated with a given adapter name.\"\"\"\n\n        for thing in getattr(self, kind_list):\n            if name == thing.name:\n                return thing\n\n        raise exceptions.NotSupportedError(\n            \"Could not find plugin: '{}' in kind_list: '{}'.\"\n            \" options: {}\".format(\n                name,\n                kind_list,\n                getattr(self, kind_list)\n            )\n        )\n\n    def adapter_module_from_name(self, name):\n        \"\"\"Return the adapter module associated with a given adapter name.\"\"\"\n\n        adp = self.from_name(name)\n        return adp.module()\n\n    def schemadef_module_from_name(self, name):\n        \"\"\"Return the schemadef module associated with a given schemadef name.\"\"\"\n\n        adp = self.from_name(name, kind_list=\"schemadefs\")\n        return adp.module()\n\n\n_MANIFEST = None\n\n\ndef load_manifest():\n    # build the manifest of adapters, starting with builtin adapters\n    result = manifest_from_file(\n        os.path.join(\n            os.path.dirname(os.path.dirname(inspect.getsourcefile(core))),\n            \"adapters\",\n            \"builtin_adapters.plugin_manifest.json\"\n        )\n    )\n\n    # layer contrib plugins after built in ones\n    try:\n        import opentimelineio_contrib as otio_c\n\n        contrib_manifest = manifest_from_file(\n            os.path.join(\n                os.path.dirname(inspect.getsourcefile(otio_c)),\n                \"adapters\",\n                \"contrib_adapters.plugin_manifest.json\"\n            )\n        )\n        result.extend(contrib_manifest)\n    except ImportError:\n        pass\n\n    # Discover setuptools-based plugins\n    if pkg_resources:\n        for plugin in pkg_resources.iter_entry_points(\n                \"opentimelineio.plugins\"\n        ):\n            plugin_name = plugin.name\n            try:\n                plugin_entry_point = plugin.load()\n                try:\n                    plugin_manifest = plugin_entry_point.plugin_manifest()\n                except AttributeError:\n                    if not pkg_resources.resource_exists(\n                            plugin.module_name,\n                            'plugin_manifest.json'\n                    ):\n                        raise\n                    manifest_stream = pkg_resources.resource_stream(\n                        plugin.module_name,\n                        'plugin_manifest.json'\n                    )\n                    plugin_manifest = core.deserialize_json_from_string(\n                        manifest_stream.read().decode('utf-8')\n                    )\n                    manifest_stream.close()\n                    filepath = pkg_resources.resource_filename(\n                        plugin.module_name,\n                        'plugin_manifest.json'\n                    )\n                    plugin_manifest._update_plugin_source(filepath)\n\n            except Exception:\n                logging.exception(\n                    \"could not load plugin: {}\".format(plugin_name)\n                )\n                continue\n\n            result.extend(plugin_manifest)\n    else:\n        # XXX: Should we print some kind of warning that pkg_resources isn't\n        #        available?\n        pass\n\n    # read local adapter manifests, if they exist\n    _local_manifest_path = os.environ.get(\"OTIO_PLUGIN_MANIFEST_PATH\", None)\n    if _local_manifest_path is not None:\n        for json_path in _local_manifest_path.split(\":\"):\n            if not os.path.exists(json_path):\n                # XXX: In case error reporting is requested\n                # print(\n                #     \"Warning: OpenTimelineIO cannot access path '{}' from \"\n                #     \"$OTIO_PLUGIN_MANIFEST_PATH\".format(json_path)\n                # )\n                continue\n\n            LOCAL_MANIFEST = manifest_from_file(json_path)\n            result.extend(LOCAL_MANIFEST)\n\n    # force the schemadefs to load and add to schemadef module namespace\n    for s in result.schemadefs:\n        s.module()\n    return result\n\n\ndef ActiveManifest(force_reload=False):\n    global _MANIFEST\n    if not _MANIFEST or force_reload:\n        _MANIFEST = load_manifest()\n\n    return _MANIFEST\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/plugins/python_plugin.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Base class for OTIO plugins that are exposed by manifests.\"\"\"\n\nimport os\nimport imp\n\nfrom .. import (\n    core,\n    exceptions,\n)\n\n\nclass PythonPlugin(core.SerializableObject):\n    \"\"\"A class of plugin that is encoded in a python module, exposed via a\n    manifest.\n    \"\"\"\n\n    _serializable_label = \"PythonPlugin.1\"\n\n    def __init__(\n        self,\n        name=None,\n        execution_scope=None,\n        filepath=None,\n    ):\n        super(PythonPlugin, self).__init__()\n        self.name = name\n        self.execution_scope = execution_scope\n        self.filepath = filepath\n        self._json_path = None\n        self._module = None\n\n    name = core.serializable_field(\"name\", doc=\"Adapter name.\")\n    execution_scope = core.serializable_field(\n        \"execution_scope\",\n        str,\n        doc=(\n            \"Describes whether this adapter is executed in the current python\"\n            \" process or in a subshell.  Options are: \"\n            \"['in process', 'out of process'].\"\n        )\n    )\n    filepath = core.serializable_field(\n        \"filepath\",\n        str,\n        doc=(\n            \"Absolute path or relative path to adapter module from location of\"\n            \" json.\"\n        )\n    )\n\n    def module_abs_path(self):\n        \"\"\"Return an absolute path to the module implementing this adapter.\"\"\"\n\n        filepath = self.filepath\n        if not os.path.isabs(filepath):\n            if not self._json_path:\n                raise exceptions.MisconfiguredPluginError(\n                    \"{} plugin is misconfigured, missing json path. \"\n                    \"plugin: {}\".format(\n                        self.name,\n                        repr(self)\n                    )\n                )\n\n            filepath = os.path.join(os.path.dirname(self._json_path), filepath)\n\n        return filepath\n\n    def _imported_module(self, namespace):\n        \"\"\"Load the module this plugin points at.\"\"\"\n\n        pyname = os.path.splitext(os.path.basename(self.module_abs_path()))[0]\n        pydir = os.path.dirname(self.module_abs_path())\n\n        (file_obj, pathname, description) = imp.find_module(pyname, [pydir])\n\n        with file_obj:\n            # this will reload the module if it has already been loaded.\n            mod = imp.load_module(\n                \"opentimelineio.{}.{}\".format(namespace, self.name),\n                file_obj,\n                pathname,\n                description\n            )\n\n            return mod\n\n    def module(self):\n        \"\"\"Return the module object for this adapter. \"\"\"\n\n        if not self._module:\n            self._module = self._imported_module(\"adapters\")\n\n        return self._module\n\n    def _execute_function(self, func_name, **kwargs):\n        \"\"\"Execute func_name on this adapter with error checking.\"\"\"\n\n        # collects the error handling into a common place.\n        if not hasattr(self.module(), func_name):\n            raise exceptions.AdapterDoesntSupportFunctionError(\n                \"Sorry, {} doesn't support {}.\".format(self.name, func_name)\n            )\n        return (getattr(self.module(), func_name)(**kwargs))\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/__init__.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n# flake8: noqa\n\n\"\"\"User facing classes.\"\"\"\n\nfrom .missing_reference import (\n    MissingReference\n)\nfrom .external_reference import (\n    ExternalReference\n)\nfrom .clip import (\n    Clip,\n)\nfrom .track import (\n    Track,\n    TrackKind,\n    NeighborGapPolicy,\n)\nfrom .stack import (\n    Stack,\n)\nfrom .timeline import (\n    Timeline,\n    timeline_from_clips,\n)\nfrom .marker import (\n    Marker,\n    MarkerColor,\n)\nfrom .gap import (\n    Gap,\n)\nfrom .effect import (\n    Effect,\n    TimeEffect,\n    LinearTimeWarp,\n    FreezeFrame,\n)\nfrom .transition import (\n    Transition,\n    TransitionTypes,\n)\nfrom .serializable_collection import (\n    SerializableCollection\n)\nfrom .generator_reference import (\n    GeneratorReference\n)\nfrom .schemadef import (\n    SchemaDef\n)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/clip.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Implementation of the Clip class, for pointing at media.\"\"\"\n\nimport copy\n\nfrom .. import (\n    core,\n    exceptions,\n)\nfrom . import (\n    missing_reference\n)\n\n\n@core.register_type\nclass Clip(core.Item):\n    \"\"\"The base editable object in OTIO.\n\n    Contains a media reference and a trim on that media reference.\n    \"\"\"\n\n    _serializable_label = \"Clip.1\"\n\n    def __init__(\n        self,\n        name=None,\n        media_reference=None,\n        source_range=None,\n        markers=[],\n        effects=[],\n        metadata=None,\n    ):\n        core.Item.__init__(\n            self,\n            name=name,\n            source_range=source_range,\n            markers=markers,\n            effects=effects,\n            metadata=metadata\n        )\n\n        if not media_reference:\n            media_reference = missing_reference.MissingReference()\n        self._media_reference = copy.deepcopy(media_reference)\n\n    name = core.serializable_field(\"name\", doc=\"Name of this clip.\")\n    transform = core.deprecated_field()\n    _media_reference = core.serializable_field(\n        \"media_reference\",\n        core.MediaReference,\n        \"Media reference to the media this clip represents.\"\n    )\n\n    @property\n    def media_reference(self):\n        if self._media_reference is None:\n            self._media_reference = missing_reference.MissingReference()\n        return self._media_reference\n\n    @media_reference.setter\n    def media_reference(self, val):\n        if val is None:\n            val = missing_reference.MissingReference()\n        self._media_reference = val\n\n    def available_range(self):\n        if not self.media_reference:\n            raise exceptions.CannotComputeAvailableRangeError(\n                \"No media reference set on clip: {}\".format(self)\n            )\n\n        if not self.media_reference.available_range:\n            raise exceptions.CannotComputeAvailableRangeError(\n                \"No available_range set on media reference on clip: {}\".format(\n                    self\n                )\n            )\n\n        return copy.copy(self.media_reference.available_range)\n\n    def __str__(self):\n        return 'Clip(\"{}\", {}, {}, {})'.format(\n            self.name,\n            self.media_reference,\n            self.source_range,\n            self.metadata\n        )\n\n    def __repr__(self):\n        return (\n            'otio.schema.Clip('\n            'name={}, '\n            'media_reference={}, '\n            'source_range={}, '\n            'metadata={}'\n            ')'.format(\n                repr(self.name),\n                repr(self.media_reference),\n                repr(self.source_range),\n                repr(self.metadata),\n            )\n        )\n\n    def each_clip(self, search_range=None):\n        \"\"\"Yields self.\"\"\"\n\n        yield self\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/effect.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Implementation of Effect OTIO class.\"\"\"\n\nfrom .. import (\n    core\n)\n\nimport copy\n\n\n@core.register_type\nclass Effect(core.SerializableObject):\n    _serializable_label = \"Effect.1\"\n\n    def __init__(\n        self,\n        name=None,\n        effect_name=None,\n        metadata=None\n    ):\n        super(Effect, self).__init__()\n        self.name = name\n        self.effect_name = effect_name\n        self.metadata = copy.deepcopy(metadata) if metadata else {}\n\n    name = core.serializable_field(\n        \"name\",\n        doc=\"Name of this effect object. Example: 'BlurByHalfEffect'.\"\n    )\n    effect_name = core.serializable_field(\n        \"effect_name\",\n        doc=\"Name of the kind of effect (example: 'Blur', 'Crop', 'Flip').\"\n    )\n    metadata = core.serializable_field(\n        \"metadata\",\n        dict,\n        doc=\"Metadata dictionary.\"\n    )\n\n    def __str__(self):\n        return (\n            \"Effect(\"\n            \"{}, \"\n            \"{}, \"\n            \"{}\"\n            \")\".format(\n                str(self.name),\n                str(self.effect_name),\n                str(self.metadata),\n            )\n        )\n\n    def __repr__(self):\n        return (\n            \"otio.schema.Effect(\"\n            \"name={}, \"\n            \"effect_name={}, \"\n            \"metadata={}\"\n            \")\".format(\n                repr(self.name),\n                repr(self.effect_name),\n                repr(self.metadata),\n            )\n        )\n\n\n@core.register_type\nclass TimeEffect(Effect):\n    \"Base Time Effect Class\"\n    _serializable_label = \"TimeEffect.1\"\n    pass\n\n\n@core.register_type\nclass LinearTimeWarp(TimeEffect):\n    \"A time warp that applies a linear scale across the entire clip\"\n    _serializable_label = \"LinearTimeWarp.1\"\n\n    def __init__(self, name=None, time_scalar=1, metadata=None):\n        Effect.__init__(\n            self,\n            name=name,\n            effect_name=\"LinearTimeWarp\",\n            metadata=metadata\n        )\n        self.time_scalar = time_scalar\n\n    time_scalar = core.serializable_field(\n        \"time_scalar\",\n        doc=\"Linear time scalar applied to clip.  \"\n        \"2.0 = double speed, 0.5 = half speed.\"\n    )\n\n\n@core.register_type\nclass FreezeFrame(LinearTimeWarp):\n    \"Hold the first frame of the clip for the duration of the clip.\"\n    _serializable_label = \"FreezeFrame.1\"\n\n    def __init__(self, name=None, metadata=None):\n        LinearTimeWarp.__init__(\n            self,\n            name=name,\n            time_scalar=0,\n            metadata=metadata\n        )\n        self.effect_name = \"FreezeFrame\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/external_reference.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"\nImplementation of the ExternalReference media reference schema.\n\"\"\"\n\nfrom .. import (\n    core,\n)\n\n\n@core.register_type\nclass ExternalReference(core.MediaReference):\n    \"\"\"Reference to media via a url, for example \"file:///var/tmp/foo.mov\" \"\"\"\n\n    _serializable_label = \"ExternalReference.1\"\n    _name = \"ExternalReference\"\n\n    def __init__(\n        self,\n        target_url=None,\n        available_range=None,\n        metadata=None,\n    ):\n        core.MediaReference.__init__(\n            self,\n            available_range=available_range,\n            metadata=metadata\n        )\n\n        self.target_url = target_url\n\n    target_url = core.serializable_field(\n        \"target_url\",\n        doc=(\n            \"URL at which this media lives.  For local references, use the \"\n            \"'file://' format.\"\n        )\n    )\n\n    def __str__(self):\n        return 'ExternalReference(\"{}\")'.format(self.target_url)\n\n    def __repr__(self):\n        return 'otio.schema.ExternalReference(target_url={})'.format(\n            repr(self.target_url)\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/gap.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\nfrom .. import (\n    core,\n    opentime,\n)\n\n\"\"\"Gap Item - represents a transparent gap in content.\"\"\"\n\n\n@core.register_type\nclass Gap(core.Item):\n    _serializable_label = \"Gap.1\"\n    _class_path = \"schema.Gap\"\n\n    def __init__(\n        self,\n        name=None,\n        # note - only one of the following two arguments is accepted\n        # if neither is provided, source_range will be set to an empty\n        # TimeRange\n        # Duration is provided as a convienence for creating a gap of a certain\n        # length.  IE: Gap(duration=otio.opentime.RationalTime(300, 24))\n        duration=None,\n        source_range=None,\n        effects=None,\n        markers=None,\n        metadata=None,\n    ):\n        if duration and source_range:\n            raise RuntimeError(\n                \"Cannot instantiate with both a source range and a duration.\"\n            )\n\n        if duration:\n            source_range = opentime.TimeRange(\n                opentime.RationalTime(0, duration.rate),\n                duration\n            )\n        elif source_range is None:\n            # if neither is provided, seed TimeRange as an empty Source Range.\n            source_range = opentime.TimeRange()\n\n        core.Item.__init__(\n            self,\n            name=name,\n            source_range=source_range,\n            effects=effects,\n            markers=markers,\n            metadata=metadata\n        )\n\n    @staticmethod\n    def visible():\n        return False\n\n\n# the original name for \"gap\" was \"filler\" - this will turn \"Filler\" found in\n# OTIO files into Gap automatically.\ncore.register_type(Gap, \"Filler\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/generator_reference.py",
    "content": "\"\"\"\nGenerators are media references that _produce_ media rather than refer to it.\n\"\"\"\n\nfrom .. import (\n    core,\n)\n\n\n@core.register_type\nclass GeneratorReference(core.MediaReference):\n    \"\"\"\n    Base class for Generators.\n\n    Generators are media references that become \"generators\" in editorial\n    systems.  For example, color bars or a solid color.\n    \"\"\"\n\n    _serializable_label = \"GeneratorReference.1\"\n    _name = \"GeneratorReference\"\n\n    def __init__(\n        self,\n        name=None,\n        generator_kind=None,\n        available_range=None,\n        parameters=None,\n        metadata=None\n    ):\n        super(GeneratorReference, self).__init__(\n            name,\n            available_range,\n            metadata\n        )\n\n        if parameters is None:\n            parameters = {}\n        self.parameters = parameters\n        self.generator_kind = generator_kind\n\n    parameters = core.serializable_field(\n        \"parameters\",\n        dict,\n        doc=\"Dictionary of parameters for generator.\"\n    )\n    generator_kind = core.serializable_field(\n        \"generator_kind\",\n        required_type=type(\"\"),\n        # @TODO: need to clarify if this also has an enum of supported types\n        # / generic\n        doc=\"Kind of generator reference, as defined by the \"\n        \"schema.generator_reference.GeneratorReferenceTypes enum.\"\n    )\n\n    def __str__(self):\n        return 'GeneratorReference(\"{}\", \"{}\", {}, {})'.format(\n            self.name,\n            self.generator_kind,\n            self.parameters,\n            self.metadata\n        )\n\n    def __repr__(self):\n        return (\n            'otio.schema.GeneratorReference('\n            'name={}, '\n            'generator_kind={}, '\n            'parameters={}, '\n            'metadata={}'\n            ')'.format(\n                repr(self.name),\n                repr(self.generator_kind),\n                repr(self.parameters),\n                repr(self.metadata),\n            )\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/marker.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Marker class.  Holds metadata over regions of time.\"\"\"\n\nfrom .. import (\n    core,\n    opentime,\n)\n\n\nclass MarkerColor:\n    \"\"\" Enum encoding colors of markers as strings.  \"\"\"\n\n    PINK = \"PINK\"\n    RED = \"RED\"\n    ORANGE = \"ORANGE\"\n    YELLOW = \"YELLOW\"\n    GREEN = \"GREEN\"\n    CYAN = \"CYAN\"\n    BLUE = \"BLUE\"\n    PURPLE = \"PURPLE\"\n    MAGENTA = \"MAGENTA\"\n    BLACK = \"BLACK\"\n    WHITE = \"WHITE\"\n\n\n@core.register_type\nclass Marker(core.SerializableObject):\n\n    \"\"\" Holds metadata over time on a timeline \"\"\"\n\n    _serializable_label = \"Marker.2\"\n    _class_path = \"marker.Marker\"\n\n    def __init__(\n        self,\n        name=None,\n        marked_range=None,\n        color=MarkerColor.RED,\n        metadata=None,\n    ):\n        core.SerializableObject.__init__(\n            self,\n        )\n        self.name = name\n        self.marked_range = marked_range\n        self.color = color\n        self.metadata = metadata or {}\n\n    name = core.serializable_field(\"name\", doc=\"Name of this marker.\")\n\n    marked_range = core.serializable_field(\n        \"marked_range\",\n        opentime.TimeRange,\n        \"Range this marker applies to, relative to the Item this marker is \"\n        \"attached to (e.g. the Clip or Track that owns this marker).\"\n    )\n\n    color = core.serializable_field(\n        \"color\",\n        required_type=type(MarkerColor.RED),\n        doc=\"Color string for this marker (for example: 'RED'), based on the \"\n        \"otio.schema.marker.MarkerColor enum.\"\n    )\n\n    # old name\n    range = core.deprecated_field()\n\n    metadata = core.serializable_field(\n        \"metadata\",\n        dict,\n        \"Metadata dictionary.\"\n    )\n\n    def __repr__(self):\n        return (\n            \"otio.schema.Marker(\"\n            \"name={}, \"\n            \"marked_range={}, \"\n            \"metadata={}\"\n            \")\".format(\n                repr(self.name),\n                repr(self.marked_range),\n                repr(self.metadata),\n            )\n        )\n\n    def __str__(self):\n        return (\n            \"Marker(\"\n            \"{}, \"\n            \"{}, \"\n            \"{}\"\n            \")\".format(\n                str(self.name),\n                str(self.marked_range),\n                str(self.metadata),\n            )\n        )\n\n\n@core.upgrade_function_for(Marker, 2)\ndef _version_one_to_two(data):\n    data[\"marked_range\"] = data[\"range\"]\n    del data[\"range\"]\n    return data\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/missing_reference.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"\nImplementation of the MissingReference media reference schema.\n\"\"\"\n\nfrom .. import (\n    core,\n)\n\n\n@core.register_type\nclass MissingReference(core.MediaReference):\n    \"\"\"Represents media for which a concrete reference is missing.\"\"\"\n\n    _serializable_label = \"MissingReference.1\"\n    _name = \"MissingReference\"\n\n    @property\n    def is_missing_reference(self):\n        return True\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/schemadef.py",
    "content": "\nfrom .. import (\n    core,\n    exceptions,\n    plugins,\n    schemadef\n)\n\n\n@core.register_type\nclass SchemaDef(plugins.PythonPlugin):\n    _serializable_label = \"SchemaDef.1\"\n\n    def __init__(\n        self,\n        name=None,\n        execution_scope=None,\n        filepath=None,\n    ):\n        super(SchemaDef, self).__init__(name, execution_scope, filepath)\n\n    def module(self):\n        \"\"\"\n        Return the module object for this schemadef plugin.\n        If the module hasn't already been imported, it is imported and\n        injected into the otio.schemadefs namespace as a side-effect.\n        (redefines PythonPlugin.module())\n        \"\"\"\n\n        if not self._module:\n            self._module = self._imported_module(\"schemadef\")\n            if self.name:\n                schemadef._add_schemadef_module(self.name, self._module)\n\n        return self._module\n\n\ndef available_schemadef_names():\n    \"\"\"Return a string list of the available schemadefs.\"\"\"\n\n    return [str(sd.name) for sd in plugins.ActiveManifest().schemadefs]\n\n\ndef from_name(name):\n    \"\"\"Fetch the schemadef plugin object by the name of the schema directly.\"\"\"\n\n    try:\n        return plugins.ActiveManifest().from_name(name, kind_list=\"schemadefs\")\n    except exceptions.NotSupportedError:\n        raise exceptions.NotSupportedError(\n            \"schemadef not supported: {}, available: {}\".format(\n                name,\n                available_schemadef_names()\n            )\n        )\n\n\ndef module_from_name(name):\n    \"\"\"Fetch the plugin's module by the name of the schemadef.\n\n    Will load the plugin if it has not already been loaded.  Reading a file that\n    contains the schemadef will also trigger a load of the plugin.\n    \"\"\"\n    plugin = from_name(name)\n    return plugin.module()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/serializable_collection.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"A serializable collection of SerializableObjects.\"\"\"\n\nimport collections\nimport copy\n\nfrom .. import (\n    core\n)\n\nfrom . import (\n    clip\n)\n\n\n@core.register_type\nclass SerializableCollection(\n    core.SerializableObject,\n    collections.MutableSequence\n):\n    \"\"\"A kind of composition which can hold any serializable object.\n\n    This composition approximates the concept of a `bin` - a collection of\n    SerializableObjects that do not have any compositing meaning, but can\n    serialize to/from OTIO correctly, with metadata and a named collection.\n    \"\"\"\n\n    _serializable_label = \"SerializableCollection.1\"\n    _class_path = \"schema.SerializableCollection\"\n\n    def __init__(\n        self,\n        name=None,\n        children=None,\n        metadata=None,\n    ):\n        super(SerializableCollection, self).__init__()\n\n        self.name = name\n        self._children = children or []\n        self.metadata = copy.deepcopy(metadata) if metadata else {}\n\n    name = core.serializable_field(\n        \"name\",\n        doc=\"SerializableCollection name.\"\n    )\n    _children = core.serializable_field(\n        \"children\",\n        list,\n        \"SerializableObject contained by this container.\"\n    )\n    metadata = core.serializable_field(\n        \"metadata\",\n        dict,\n        doc=\"Metadata dictionary for this SerializableCollection.\"\n    )\n\n    # @{ Stringification\n    def __str__(self):\n        return \"SerializableCollection({}, {}, {})\".format(\n            str(self.name),\n            str(self._children),\n            str(self.metadata)\n        )\n\n    def __repr__(self):\n        return (\n            \"otio.{}(\"\n            \"name={}, \"\n            \"children={}, \"\n            \"metadata={}\"\n            \")\".format(\n                self._class_path,\n                repr(self.name),\n                repr(self._children),\n                repr(self.metadata)\n            )\n        )\n    # @}\n\n    # @{ collections.MutableSequence implementation\n    def __getitem__(self, item):\n        return self._children[item]\n\n    def __setitem__(self, key, value):\n        self._children[key] = value\n\n    def insert(self, index, item):\n        self._children.insert(index, item)\n\n    def __len__(self):\n        return len(self._children)\n\n    def __delitem__(self, item):\n        del self._children[item]\n    # @}\n\n    def each_child(\n        self,\n        search_range=None,\n        descended_from_type=core.composable.Composable\n    ):\n        for i, child in enumerate(self._children):\n            # filter out children who are not descended from the specified type\n            is_descendant = descended_from_type == core.composable.Composable\n            if is_descendant or isinstance(child, descended_from_type):\n                yield child\n\n            # for children that are compositions, recurse into their children\n            if hasattr(child, \"each_child\"):\n                for valid_child in (\n                    c for c in child.each_child(\n                        search_range,\n                        descended_from_type\n                    )\n                ):\n                    yield valid_child\n\n    def each_clip(self, search_range=None):\n        return self.each_child(search_range, clip.Clip)\n\n\n# the original name for \"SerializableCollection\" was \"SerializeableCollection\"\n# this will turn this misspelling found in OTIO files into the correct instance\n# automatically.\ncore.register_type(SerializableCollection, 'SerializeableCollection')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/stack.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"A stack represents a series of composable.Composables that are arranged such\nthat their start times are at the same point.\n\nMost commonly, this would be a series of schema.Track objects that then\ncontain clips.  The 0 time of those tracks would be coincide with the 0-time of\nthe stack.\n\nStacks are in compositing order, with later children obscuring earlier\nchildren. In other words, from bottom to top.  If a stack has three children,\n[A, B, C], C is above B which is above A.\n\nA stack is the length of its longest child.  If a child ends before the other\nchildren, then an earlier index child would be visible before it.\n\"\"\"\n\nfrom .. import (\n    core,\n    opentime,\n    exceptions\n)\n\nfrom . import (\n    clip\n)\n\n\n@core.register_type\nclass Stack(core.Composition):\n    _serializable_label = \"Stack.1\"\n    _composition_kind = \"Stack\"\n    _modname = \"schema\"\n\n    def __init__(\n        self,\n        name=None,\n        children=None,\n        source_range=None,\n        markers=None,\n        effects=None,\n        metadata=None\n    ):\n        core.Composition.__init__(\n            self,\n            name=name,\n            children=children,\n            source_range=source_range,\n            markers=markers,\n            effects=effects,\n            metadata=metadata\n        )\n\n    def range_of_child_at_index(self, index):\n        try:\n            child = self[index]\n        except IndexError:\n            raise exceptions.NoSuchChildAtIndex(index)\n\n        dur = child.duration()\n\n        return opentime.TimeRange(\n            start_time=opentime.RationalTime(0, dur.rate),\n            duration=dur\n        )\n\n    def each_clip(self, search_range=None):\n        return self.each_child(search_range, clip.Clip)\n\n    def available_range(self):\n        if len(self) == 0:\n            return opentime.TimeRange()\n\n        duration = max(child.duration() for child in self)\n\n        return opentime.TimeRange(\n            opentime.RationalTime(0, duration.rate),\n            duration=duration\n        )\n\n    def range_of_all_children(self):\n        child_map = {}\n        for i, c in enumerate(self._children):\n            child_map[c] = self.range_of_child_at_index(i)\n        return child_map\n\n    def trimmed_range_of_child_at_index(self, index, reference_space=None):\n        range = self.range_of_child_at_index(index)\n\n        if not self.source_range:\n            return range\n\n        range = opentime.TimeRange(\n            start_time=self.source_range.start_time,\n            duration=min(range.duration, self.source_range.duration)\n        )\n\n        return range\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/timeline.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Implementation of the OTIO built in schema, Timeline object.\"\"\"\n\nimport copy\n\nfrom .. import (\n    core,\n    opentime,\n)\n\nfrom . import stack, track\n\n\n@core.register_type\nclass Timeline(core.SerializableObject):\n    _serializable_label = \"Timeline.1\"\n\n    def __init__(\n        self,\n        name=None,\n        tracks=None,\n        global_start_time=None,\n        metadata=None,\n    ):\n        super(Timeline, self).__init__()\n        self.name = name\n        self.global_start_time = copy.deepcopy(global_start_time)\n\n        if tracks is None:\n            tracks = []\n        self.tracks = stack.Stack(name=\"tracks\", children=tracks)\n\n        self.metadata = copy.deepcopy(metadata) if metadata else {}\n\n    name = core.serializable_field(\"name\", doc=\"Name of this timeline.\")\n    tracks = core.serializable_field(\n        \"tracks\",\n        core.Composition,\n        doc=\"Stack of tracks containing items.\"\n    )\n    metadata = core.serializable_field(\n        \"metadata\",\n        dict,\n        \"Metadata dictionary.\"\n    )\n    global_start_time = core.serializable_field(\n        \"global_start_time\",\n        opentime.RationalTime,\n        doc=\"Global starting time value and rate of the timeline.\"\n    )\n\n    def __str__(self):\n        return 'Timeline(\"{}\", {})'.format(str(self.name), str(self.tracks))\n\n    def __repr__(self):\n        return (\n            \"otio.schema.Timeline(name={}, tracks={})\".format(\n                repr(self.name),\n                repr(self.tracks)\n            )\n        )\n\n    def each_child(self, search_range=None, descended_from_type=core.Composable):\n        return self.tracks.each_child(search_range, descended_from_type)\n\n    def each_clip(self, search_range=None):\n        \"\"\"Return a flat list of each clip, limited to the search_range.\"\"\"\n\n        return self.tracks.each_clip(search_range)\n\n    def duration(self):\n        \"\"\"Duration of this timeline.\"\"\"\n\n        return self.tracks.duration()\n\n    def range_of_child(self, child):\n        \"\"\"Range of the child object contained in this timeline.\"\"\"\n\n        return self.tracks.range_of_child(child)\n\n    def video_tracks(self):\n        \"\"\"\n        This convenience method returns a list of the top-level video tracks in\n        this timeline.\n        \"\"\"\n        return [\n            trck for trck\n            in self.tracks\n            if (isinstance(trck, track.Track) and\n                trck.kind == track.TrackKind.Video)\n        ]\n\n    def audio_tracks(self):\n        \"\"\"\n        This convenience method returns a list of the top-level audio tracks in\n        this timeline.\n        \"\"\"\n        return [\n            trck for trck\n            in self.tracks\n            if (isinstance(trck, track.Track) and\n                trck.kind == track.TrackKind.Audio)\n        ]\n\n\ndef timeline_from_clips(clips):\n    \"\"\"Convenience for making a single track timeline from a list of clips.\"\"\"\n\n    trck = track.Track(children=clips)\n    return Timeline(tracks=[trck])\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/track.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Implement Track sublcass of composition.\"\"\"\n\nimport collections\n\nfrom .. import (\n    core,\n    opentime,\n)\n\nfrom . import (\n    gap,\n    transition,\n    clip,\n)\n\n\nclass TrackKind:\n    Video = \"Video\"\n    Audio = \"Audio\"\n\n\nclass NeighborGapPolicy:\n    \"\"\" enum for deciding how to add gap when asking for neighbors \"\"\"\n    never = 0\n    around_transitions = 1\n\n\n@core.register_type\nclass Track(core.Composition):\n    _serializable_label = \"Track.1\"\n    _composition_kind = \"Track\"\n    _modname = \"schema\"\n\n    def __init__(\n        self,\n        name=None,\n        children=None,\n        kind=TrackKind.Video,\n        source_range=None,\n        markers=None,\n        effects=None,\n        metadata=None,\n    ):\n        core.Composition.__init__(\n            self,\n            name=name,\n            children=children,\n            source_range=source_range,\n            markers=markers,\n            effects=effects,\n            metadata=metadata\n        )\n        self.kind = kind\n\n    kind = core.serializable_field(\n        \"kind\",\n        doc=\"Composition kind (Stack, Track)\"\n    )\n\n    def range_of_child_at_index(self, index):\n        child = self[index]\n\n        # sum the durations of all the children leading up to the chosen one\n        start_time = sum(\n            (\n                o_c.duration()\n                for o_c in (c for c in self[:index] if not c.overlapping())\n            ),\n            opentime.RationalTime(value=0, rate=child.duration().rate)\n        )\n        if isinstance(child, transition.Transition):\n            start_time -= child.in_offset\n\n        return opentime.TimeRange(start_time, child.duration())\n\n    def trimmed_range_of_child_at_index(self, index, reference_space=None):\n        child_range = self.range_of_child_at_index(index)\n\n        return self.trim_child_range(child_range)\n\n    def handles_of_child(self, child):\n        \"\"\"If media beyond the ends of this child are visible due to adjacent\n        Transitions (only applicable in a Track) then this will return the\n        head and tail offsets as a tuple of RationalTime objects. If no handles\n        are present on either side, then None is returned instead of a\n        RationalTime.\n\n        Example usage\n\n        >>> head, tail = track.handles_of_child(clip)\n        >>> if head:\n        ...     print('do something')\n        >>> if tail:\n        ...     print('do something else')\n        \"\"\"\n        head, tail = None, None\n        before, after = self.neighbors_of(child)\n        if isinstance(before, transition.Transition):\n            head = before.in_offset\n        if isinstance(after, transition.Transition):\n            tail = after.out_offset\n\n        return head, tail\n\n    def available_range(self):\n        # Sum up our child items' durations\n        duration = sum(\n            (c.duration() for c in self if isinstance(c, core.Item)),\n            opentime.RationalTime()\n        )\n\n        # Add the implicit gap when a Transition is at the start/end\n        if self and isinstance(self[0], transition.Transition):\n            duration += self[0].in_offset\n        if self and isinstance(self[-1], transition.Transition):\n            duration += self[-1].out_offset\n\n        result = opentime.TimeRange(\n            start_time=opentime.RationalTime(0, duration.rate),\n            duration=duration\n        )\n\n        return result\n\n    def each_clip(self, search_range=None, shallow_search=False):\n        return self.each_child(search_range, clip.Clip, shallow_search)\n\n    def neighbors_of(self, item, insert_gap=NeighborGapPolicy.never):\n        \"\"\"Returns the neighbors of the item as a namedtuple, (previous, next).\n\n        Can optionally fill in gaps when transitions have no gaps next to them.\n\n        with insert_gap == NeighborGapPolicy.never:\n        [A, B, C] :: neighbors_of(B) -> (A, C)\n        [A, B, C] :: neighbors_of(A) -> (None, B)\n        [A, B, C] :: neighbors_of(C) -> (B, None)\n        [A] :: neighbors_of(A) -> (None, None)\n\n        with insert_gap == NeighborGapPolicy.around_transitions:\n        (assuming A and C are transitions)\n        [A, B, C] :: neighbors_of(B) -> (A, C)\n        [A, B, C] :: neighbors_of(A) -> (Gap, B)\n        [A, B, C] :: neighbors_of(C) -> (B, Gap)\n        [A] :: neighbors_of(A) -> (Gap, Gap)\n        \"\"\"\n\n        try:\n            index = self.index(item)\n        except ValueError:\n            raise ValueError(\n                \"item: {} is not in composition: {}\".format(\n                    item,\n                    self\n                )\n            )\n\n        previous, next_item = None, None\n\n        # look before index\n        if index == 0:\n            if insert_gap == NeighborGapPolicy.around_transitions:\n                if isinstance(item, transition.Transition):\n                    previous = gap.Gap(\n                        source_range=opentime.TimeRange(duration=item.in_offset))\n        elif index > 0:\n            previous = self[index - 1]\n\n        if index == len(self) - 1:\n            if insert_gap == NeighborGapPolicy.around_transitions:\n                if isinstance(item, transition.Transition):\n                    next_item = gap.Gap(\n                        source_range=opentime.TimeRange(duration=item.out_offset))\n        elif index < len(self) - 1:\n            next_item = self[index + 1]\n\n        return collections.namedtuple('neighbors', ('previous', 'next'))(\n            previous,\n            next_item\n        )\n\n    def range_of_all_children(self):\n        \"\"\"Return a dict mapping children to their range in this track.\"\"\"\n\n        if not self._children:\n            return {}\n\n        result_map = {}\n\n        # Heuristic to guess what the rate should be set to based on the first\n        # thing in the track.\n        first_thing = self._children[0]\n        if isinstance(first_thing, transition.Transition):\n            rate = first_thing.in_offset.rate\n        else:\n            rate = first_thing.trimmed_range().duration.rate\n\n        last_end_time = opentime.RationalTime(0, rate)\n\n        for thing in self._children:\n            if isinstance(thing, transition.Transition):\n                result_map[thing] = opentime.TimeRange(\n                    last_end_time - thing.in_offset,\n                    thing.out_offset + thing.in_offset,\n                )\n            else:\n                last_range = opentime.TimeRange(\n                    last_end_time,\n                    thing.trimmed_range().duration\n                )\n                result_map[thing] = last_range\n                last_end_time = last_range.end_time_exclusive()\n\n        return result_map\n\n\n# the original name for \"track\" was \"sequence\" - this will turn \"Sequence\"\n# found in OTIO files into Track automatically.\ncore.register_type(Track, \"Sequence\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schema/transition.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Transition base class\"\"\"\n\nfrom .. import (\n    opentime,\n    core,\n    exceptions,\n)\n\nimport copy\n\n\nclass TransitionTypes:\n    \"\"\"Enum encoding types of transitions.\n\n    This is for representing \"Dissolves\" and \"Wipes\" defined by the\n    multi-source effect as defined by SMPTE 258M-2004 7.6.3.2\n\n    Other effects are handled by the `schema.Effect` class.\n    \"\"\"\n\n    # @{ SMPTE transitions.\n    SMPTE_Dissolve = \"SMPTE_Dissolve\"\n    # SMPTE_Wipe = \"SMPTE_Wipe\" -- @TODO\n    # @}\n\n    # Non SMPTE transitions.\n    Custom = \"Custom_Transition\"\n\n\n@core.register_type\nclass Transition(core.Composable):\n    \"\"\"Represents a transition between two items.\"\"\"\n\n    _serializable_label = \"Transition.1\"\n\n    def __init__(\n        self,\n        name=None,\n        transition_type=None,\n        # @TODO: parameters will be added later as needed (SMPTE_Wipe will\n        #        probably require it)\n        # parameters=None,\n        in_offset=None,\n        out_offset=None,\n        metadata=None\n    ):\n        core.Composable.__init__(\n            self,\n            name=name,\n            metadata=metadata\n        )\n\n        # init everything as None first, so that we will catch uninitialized\n        # values via exceptions\n        # if parameters is None:\n        #     parameters = {}\n        # self.parameters = parameters\n        self.transition_type = transition_type\n        self.in_offset = copy.deepcopy(in_offset)\n        self.out_offset = copy.deepcopy(out_offset)\n\n    transition_type = core.serializable_field(\n        \"transition_type\",\n        required_type=type(TransitionTypes.SMPTE_Dissolve),\n        doc=\"Kind of transition, as defined by the \"\n        \"schema.transition.TransitionTypes enum.\"\n    )\n    # parameters = core.serializable_field(\n    #     \"parameters\",\n    #     doc=\"Parameters of the transition.\"\n    # )\n    in_offset = core.serializable_field(\n        \"in_offset\",\n        required_type=opentime.RationalTime,\n        doc=\"Amount of the previous clip this transition overlaps, exclusive.\"\n    )\n    out_offset = core.serializable_field(\n        \"out_offset\",\n        required_type=opentime.RationalTime,\n        doc=\"Amount of the next clip this transition overlaps, exclusive.\"\n    )\n\n    def __str__(self):\n        return 'Transition(\"{}\", \"{}\", {}, {}, {})'.format(\n            self.name,\n            self.transition_type,\n            self.in_offset,\n            self.out_offset,\n            # self.parameters,\n            self.metadata\n        )\n\n    def __repr__(self):\n        return (\n            'otio.schema.Transition('\n            'name={}, '\n            'transition_type={}, '\n            'in_offset={}, '\n            'out_offset={}, '\n            # 'parameters={}, '\n            'metadata={}'\n            ')'.format(\n                repr(self.name),\n                repr(self.transition_type),\n                repr(self.in_offset),\n                repr(self.out_offset),\n                # repr(self.parameters),\n                repr(self.metadata),\n            )\n        )\n\n    @staticmethod\n    def overlapping():\n        return True\n\n    def duration(self):\n        return self.in_offset + self.out_offset\n\n    def range_in_parent(self):\n        \"\"\"Find and return the range of this item in the parent.\"\"\"\n        if not self.parent():\n            raise exceptions.NotAChildError(\n                \"No parent of {}, cannot compute range in parent.\".format(self)\n            )\n\n        return self.parent().range_of_child(self)\n\n    def trimmed_range_in_parent(self):\n        \"\"\"Find and return the timmed range of this item in the parent.\"\"\"\n        if not self.parent():\n            raise exceptions.NotAChildError(\n                \"No parent of {}, cannot compute range in parent.\".format(self)\n            )\n\n        return self.parent().trimmed_range_of_child(self)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/schemadef/__init__.py",
    "content": "\ndef _add_schemadef_module(name, mod):\n    \"\"\"Insert a new module name and module object into schemadef namespace.\"\"\"\n    ns = globals()  # the namespace dict of the schemadef package\n    ns[name] = mod\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio/test_utils.py",
    "content": "#!/usr/bin/env python\n#\n# Copyright 2018 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Utility assertions for OTIO Unit tests.\"\"\"\n\nimport re\n\nfrom . import (\n    adapters\n)\n\n\nclass OTIOAssertions(object):\n    def assertJsonEqual(self, known, test_result):\n        \"\"\"Convert to json and compare that (more readable).\"\"\"\n        self.maxDiff = None\n\n        known_str = adapters.write_to_string(known, 'otio_json')\n        test_str = adapters.write_to_string(test_result, 'otio_json')\n\n        def strip_trailing_decimal_zero(s):\n            return re.sub(r'\"(value|rate)\": (\\d+)\\.0', r'\"\\1\": \\2', s)\n\n        self.assertMultiLineEqual(\n            strip_trailing_decimal_zero(known_str),\n            strip_trailing_decimal_zero(test_str)\n        )\n\n    def assertIsOTIOEquivalentTo(self, known, test_result):\n        \"\"\"Test using the 'is equivalent to' method on SerializableObject\"\"\"\n\n        self.assertTrue(known.is_equivalent_to(test_result))\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/__init__.py",
    "content": "#!/usr/bin/env python\n#\n# Copyright 2018 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Unsupported contrib code for OpenTimelineIO.\"\"\"\n\n# flake8: noqa\n\nfrom . import (\n    adapters\n)\n\n__version__ = \"0.11.0\"\n__author__ = \"Pixar Animation Studios\"\n__author_email__ = \"opentimelineio@pixar.com\"\n__license__ = \"Modified Apache 2.0 License\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/aaf_adapter/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/aaf_adapter/aaf_writer.py",
    "content": "#\n# Copyright 2019 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"AAF Adapter Transcriber\n\nSpecifies how to transcribe an OpenTimelineIO file into an AAF file.\n\"\"\"\n\nimport aaf2\nimport abc\nimport uuid\nimport opentimelineio as otio\nimport os\nimport copy\nimport re\n\n\nAAF_PARAMETERDEF_PAN = aaf2.auid.AUID(\"e4962322-2267-11d3-8a4c-0050040ef7d2\")\nAAF_OPERATIONDEF_MONOAUDIOPAN = aaf2.auid.AUID(\"9d2ea893-0968-11d3-8a38-0050040ef7d2\")\nAAF_PARAMETERDEF_AVIDPARAMETERBYTEORDER = uuid.UUID(\n    \"c0038672-a8cf-11d3-a05b-006094eb75cb\")\nAAF_PARAMETERDEF_AVIDEFFECTID = uuid.UUID(\n    \"93994bd6-a81d-11d3-a05b-006094eb75cb\")\nAAF_PARAMETERDEF_AFX_FG_KEY_OPACITY_U = uuid.UUID(\n    \"8d56813d-847e-11d5-935a-50f857c10000\")\nAAF_PARAMETERDEF_LEVEL = uuid.UUID(\"e4962320-2267-11d3-8a4c-0050040ef7d2\")\nAAF_VVAL_EXTRAPOLATION_ID = uuid.UUID(\"0e24dd54-66cd-4f1a-b0a0-670ac3a7a0b3\")\nAAF_OPERATIONDEF_SUBMASTER = uuid.UUID(\"f1db0f3d-8d64-11d3-80df-006008143e6f\")\n\n\nclass AAFAdapterError(otio.exceptions.OTIOError):\n    pass\n\n\nclass AAFValidationError(AAFAdapterError):\n    pass\n\n\nclass AAFFileTranscriber(object):\n    \"\"\"\n    AAFFileTranscriber\n\n    AAFFileTranscriber manages the file-level knowledge during a conversion from\n    otio to aaf. This includes keeping track of unique tapemobs and mastermobs.\n    \"\"\"\n\n    def __init__(self, input_otio, aaf_file, **kwargs):\n        \"\"\"\n        AAFFileTranscriber requires an input timeline and an output pyaaf2 file handle.\n\n        Args:\n            input_otio: an input OpenTimelineIO timeline\n            aaf_file: a pyaaf2 file handle to an output file\n        \"\"\"\n        self.aaf_file = aaf_file\n        self.compositionmob = self.aaf_file.create.CompositionMob()\n        self.compositionmob.name = input_otio.name\n        self.compositionmob.usage = \"Usage_TopLevel\"\n        self.aaf_file.content.mobs.append(self.compositionmob)\n        self._unique_mastermobs = {}\n        self._unique_tapemobs = {}\n        self._clip_mob_ids_map = _gather_clip_mob_ids(input_otio, **kwargs)\n\n    def _unique_mastermob(self, otio_clip):\n        \"\"\"Get a unique mastermob, identified by clip metadata mob id.\"\"\"\n        mob_id = self._clip_mob_ids_map.get(otio_clip)\n        mastermob = self._unique_mastermobs.get(mob_id)\n        if not mastermob:\n            mastermob = self.aaf_file.create.MasterMob()\n            mastermob.name = otio_clip.name\n            mastermob.mob_id = aaf2.mobid.MobID(mob_id)\n            self.aaf_file.content.mobs.append(mastermob)\n            self._unique_mastermobs[mob_id] = mastermob\n        return mastermob\n\n    def _unique_tapemob(self, otio_clip):\n        \"\"\"Get a unique tapemob, identified by clip metadata mob id.\"\"\"\n        mob_id = self._clip_mob_ids_map.get(otio_clip)\n        tapemob = self._unique_tapemobs.get(mob_id)\n        if not tapemob:\n            tapemob = self.aaf_file.create.SourceMob()\n            tapemob.name = otio_clip.name\n            tapemob.descriptor = self.aaf_file.create.ImportDescriptor()\n            # If the edit_rate is not an integer, we need\n            # to use drop frame with a nominal integer fps.\n            edit_rate = otio_clip.visible_range().duration.rate\n            timecode_fps = round(edit_rate)\n            tape_timecode_slot = tapemob.create_timecode_slot(\n                edit_rate=edit_rate,\n                timecode_fps=timecode_fps,\n                drop_frame=(edit_rate != timecode_fps)\n            )\n            timecode_start = (\n                otio_clip.media_reference.available_range.start_time.value)\n            timecode_length = (\n                otio_clip.media_reference.available_range.duration.value)\n\n            tape_timecode_slot.segment.start = timecode_start\n            tape_timecode_slot.segment.length = timecode_length\n            self.aaf_file.content.mobs.append(tapemob)\n            self._unique_tapemobs[mob_id] = tapemob\n        return tapemob\n\n    def track_transcriber(self, otio_track):\n        \"\"\"Return an appropriate _TrackTranscriber given an otio track.\"\"\"\n        if otio_track.kind == otio.schema.TrackKind.Video:\n            transcriber = VideoTrackTranscriber(self, otio_track)\n        elif otio_track.kind == otio.schema.TrackKind.Audio:\n            transcriber = AudioTrackTranscriber(self, otio_track)\n        else:\n            raise otio.exceptions.NotSupportedError(\n                \"Unsupported track kind: {}\".format(otio_track.kind))\n        return transcriber\n\n\ndef validate_metadata(timeline):\n    \"\"\"Print a check of necessary metadata requirements for an otio timeline.\"\"\"\n\n    all_checks = [__check(timeline, \"duration().rate\")]\n    edit_rate = __check(timeline, \"duration().rate\").value\n\n    for child in timeline.each_child():\n        checks = []\n        if isinstance(child, otio.schema.Gap):\n            checks = [\n                __check(child, \"duration().rate\").equals(edit_rate)\n            ]\n        if isinstance(child, otio.schema.Clip):\n            checks = [\n                __check(child, \"duration().rate\").equals(edit_rate),\n                __check(child, \"media_reference.available_range.duration.rate\"\n                        ).equals(edit_rate),\n                __check(child, \"media_reference.available_range.start_time.rate\"\n                        ).equals(edit_rate)\n            ]\n        if isinstance(child, otio.schema.Transition):\n            checks = [\n                __check(child, \"duration().rate\").equals(edit_rate),\n                __check(child, \"metadata['AAF']['PointList']\"),\n                __check(child, \"metadata['AAF']['OperationGroup']['Operation']\"\n                        \"['DataDefinition']['Name']\"),\n                __check(child, \"metadata['AAF']['OperationGroup']['Operation']\"\n                        \"['Description']\"),\n                __check(child, \"metadata['AAF']['OperationGroup']['Operation']\"\n                        \"['Name']\"),\n                __check(child, \"metadata['AAF']['CutPoint']\")\n            ]\n        all_checks.extend(checks)\n\n    if any(check.errors for check in all_checks):\n        raise AAFValidationError(\"\\n\" + \"\\n\".join(\n            sum([check.errors for check in all_checks], [])))\n\n\ndef _gather_clip_mob_ids(input_otio,\n                         prefer_file_mob_id=False,\n                         use_empty_mob_ids=False,\n                         **kwargs):\n    \"\"\"\n    Create dictionary of otio clips with their corresponding mob ids.\n    \"\"\"\n\n    def _from_clip_metadata(clip):\n        \"\"\"Get the MobID from the clip.metadata.\"\"\"\n        return clip.metadata.get(\"AAF\", {}).get(\"SourceID\")\n\n    def _from_media_reference_metadata(clip):\n        \"\"\"Get the MobID from the media_reference.metadata.\"\"\"\n        return (clip.media_reference.metadata.get(\"AAF\", {}).get(\"MobID\") or\n                clip.media_reference.metadata.get(\"AAF\", {}).get(\"SourceID\"))\n\n    def _from_aaf_file(clip):\n        \"\"\" Get the MobID from the AAF file itself.\"\"\"\n        mob_id = None\n        target_url = clip.media_reference.target_url\n        if os.path.isfile(target_url) and target_url.endswith(\"aaf\"):\n            with aaf2.open(clip.media_reference.target_url) as aaf_file:\n                mastermobs = list(aaf_file.content.mastermobs())\n                if len(mastermobs) == 1:\n                    mob_id = mastermobs[0].mob_id\n        return mob_id\n\n    def _generate_empty_mobid(clip):\n        \"\"\"Generate a meaningless MobID.\"\"\"\n        return aaf2.mobid.MobID.new()\n\n    strategies = [\n        _from_clip_metadata,\n        _from_media_reference_metadata,\n        _from_aaf_file\n    ]\n\n    if prefer_file_mob_id:\n        strategies.remove(_from_aaf_file)\n        strategies.insert(0, _from_aaf_file)\n\n    if use_empty_mob_ids:\n        strategies.append(_generate_empty_mobid)\n\n    clip_mob_ids = {}\n\n    for otio_clip in input_otio.each_clip():\n        for strategy in strategies:\n            mob_id = strategy(otio_clip)\n            if mob_id:\n                clip_mob_ids[otio_clip] = mob_id\n                break\n        else:\n            raise AAFAdapterError(\"Cannot find mob ID for clip {}\".format(otio_clip))\n\n    return clip_mob_ids\n\n\ndef _stackify_nested_groups(timeline):\n    \"\"\"\n    Ensure that all nesting in a given timeline is in a stack container.\n    This conforms with how AAF thinks about nesting, there needs\n    to be an outer container, even if it's just one object.\n    \"\"\"\n    copied = copy.deepcopy(timeline)\n    for track in copied.tracks:\n        for i, child in enumerate(track.each_child()):\n            is_nested = isinstance(child, otio.schema.Track)\n            is_parent_in_stack = isinstance(child.parent(), otio.schema.Stack)\n            if is_nested and not is_parent_in_stack:\n                stack = otio.schema.Stack()\n                track.remove(child)\n                stack.append(child)\n                track.insert(i, stack)\n    return copied\n\n\nclass _TrackTranscriber(object):\n    \"\"\"\n    _TrackTranscriber is the base class for the conversion of a given otio track.\n\n    _TrackTranscriber is not meant to be used by itself. It provides the common\n    functionality to inherit from. We need an abstract base class because Audio and\n    Video are handled differently.\n    \"\"\"\n    __metaclass__ = abc.ABCMeta\n\n    def __init__(self, root_file_transcriber, otio_track):\n        \"\"\"\n        _TrackTranscriber\n\n        Args:\n            root_file_transcriber: the corresponding 'parent' AAFFileTranscriber object\n            otio_track: the given otio_track to convert\n        \"\"\"\n        self.root_file_transcriber = root_file_transcriber\n        self.compositionmob = root_file_transcriber.compositionmob\n        self.aaf_file = root_file_transcriber.aaf_file\n        self.otio_track = otio_track\n        self.edit_rate = next(self.otio_track.each_child()).duration().rate\n        self.timeline_mobslot, self.sequence = self._create_timeline_mobslot()\n        self.timeline_mobslot.name = self.otio_track.name\n\n    def transcribe(self, otio_child):\n        \"\"\"Transcribe otio child to corresponding AAF object\"\"\"\n        if isinstance(otio_child, otio.schema.Gap):\n            filler = self.aaf_filler(otio_child)\n            return filler\n        elif isinstance(otio_child, otio.schema.Transition):\n            transition = self.aaf_transition(otio_child)\n            return transition\n        elif isinstance(otio_child, otio.schema.Clip):\n            source_clip = self.aaf_sourceclip(otio_child)\n            return source_clip\n        elif isinstance(otio_child, otio.schema.Track):\n            sequence = self.aaf_sequence(otio_child)\n            return sequence\n        elif isinstance(otio_child, otio.schema.Stack):\n            operation_group = self.aaf_operation_group(otio_child)\n            return operation_group\n        else:\n            raise otio.exceptions.NotSupportedError(\n                \"Unsupported otio child type: {}\".format(type(otio_child)))\n\n    @property\n    @abc.abstractmethod\n    def media_kind(self):\n        \"\"\"Return the string for what kind of track this is.\"\"\"\n        pass\n\n    @property\n    @abc.abstractmethod\n    def _master_mob_slot_id(self):\n        \"\"\"\n        Return the MasterMob Slot ID for the corresponding track media kind\n        \"\"\"\n        # MasterMob's and MasterMob slots have to be unique. We handle unique\n        # MasterMob's with _unique_mastermob(). We also need to protect against\n        # duplicate MasterMob slots. As of now, we mandate all picture clips to\n        # be created in MasterMob slot 1 and all sound clips to be created in\n        # MasterMob slot 2. While this is a little inadequate, it works for now\n        pass\n\n    @abc.abstractmethod\n    def _create_timeline_mobslot(self):\n        \"\"\"\n        Return a timeline_mobslot and sequence for this track.\n\n        In AAF, a TimelineMobSlot is a container for the Sequence. A Sequence is\n        analogous to an otio track.\n\n        Returns:\n            Returns a tuple of (TimelineMobSlot, Sequence)\n        \"\"\"\n        pass\n\n    @abc.abstractmethod\n    def default_descriptor(self, otio_clip):\n        pass\n\n    @abc.abstractmethod\n    def _transition_parameters(self):\n        pass\n\n    def aaf_filler(self, otio_gap):\n        \"\"\"Convert an otio Gap into an aaf Filler\"\"\"\n        length = otio_gap.visible_range().duration.value\n        filler = self.aaf_file.create.Filler(self.media_kind, length)\n        return filler\n\n    def aaf_sourceclip(self, otio_clip):\n        \"\"\"Convert an otio Clip into an aaf SourceClip\"\"\"\n        tapemob, tapemob_slot = self._create_tapemob(otio_clip)\n        filemob, filemob_slot = self._create_filemob(otio_clip, tapemob, tapemob_slot)\n        mastermob, mastermob_slot = self._create_mastermob(otio_clip,\n                                                           filemob,\n                                                           filemob_slot)\n\n        # We need both `start_time` and `duration`\n        # Here `start` is the offset between `first` and `in` values.\n\n        offset = (otio_clip.visible_range().start_time -\n                  otio_clip.available_range().start_time)\n        start = offset.value\n        length = otio_clip.visible_range().duration.value\n\n        compmob_clip = self.compositionmob.create_source_clip(\n            slot_id=self.timeline_mobslot.slot_id,\n            start=start,\n            length=length,\n            media_kind=self.media_kind)\n        compmob_clip.mob = mastermob\n        compmob_clip.slot = mastermob_slot\n        compmob_clip.slot_id = mastermob_slot.slot_id\n        return compmob_clip\n\n    def aaf_transition(self, otio_transition):\n        \"\"\"Convert an otio Transition into an aaf Transition\"\"\"\n        if (otio_transition.transition_type !=\n                otio.schema.transition.TransitionTypes.SMPTE_Dissolve):\n            print(\n                \"Unsupported transition type: {}\".format(\n                    otio_transition.transition_type))\n            return None\n\n        transition_params, varying_value = self._transition_parameters()\n\n        interpolation_def = self.aaf_file.create.InterpolationDef(\n            aaf2.misc.LinearInterp, \"LinearInterp\", \"Linear keyframe interpolation\")\n        self.aaf_file.dictionary.register_def(interpolation_def)\n        varying_value[\"Interpolation\"].value = (\n            self.aaf_file.dictionary.lookup_interperlationdef(\"LinearInterp\"))\n\n        pointlist = otio_transition.metadata[\"AAF\"][\"PointList\"]\n\n        c1 = self.aaf_file.create.ControlPoint()\n        c1[\"EditHint\"].value = \"Proportional\"\n        c1.value = pointlist[0][\"Value\"]\n        c1.time = pointlist[0][\"Time\"]\n\n        c2 = self.aaf_file.create.ControlPoint()\n        c2[\"EditHint\"].value = \"Proportional\"\n        c2.value = pointlist[1][\"Value\"]\n        c2.time = pointlist[1][\"Time\"]\n\n        varying_value[\"PointList\"].extend([c1, c2])\n\n        op_group_metadata = otio_transition.metadata[\"AAF\"][\"OperationGroup\"]\n        effect_id = op_group_metadata[\"Operation\"].get(\"Identification\")\n        is_time_warp = op_group_metadata[\"Operation\"].get(\"IsTimeWarp\")\n        by_pass = op_group_metadata[\"Operation\"].get(\"Bypass\")\n        number_inputs = op_group_metadata[\"Operation\"].get(\"NumberInputs\")\n        operation_category = op_group_metadata[\"Operation\"].get(\"OperationCategory\")\n        data_def_name = op_group_metadata[\"Operation\"][\"DataDefinition\"][\"Name\"]\n        data_def = self.aaf_file.dictionary.lookup_datadef(str(data_def_name))\n        description = op_group_metadata[\"Operation\"][\"Description\"]\n        op_def_name = otio_transition.metadata[\"AAF\"][\n            \"OperationGroup\"\n        ][\"Operation\"][\"Name\"]\n\n        # Create OperationDefinition\n        op_def = self.aaf_file.create.OperationDef(uuid.UUID(effect_id), op_def_name)\n        self.aaf_file.dictionary.register_def(op_def)\n        op_def.media_kind = self.media_kind\n        datadef = self.aaf_file.dictionary.lookup_datadef(self.media_kind)\n        op_def[\"IsTimeWarp\"].value = is_time_warp\n        op_def[\"Bypass\"].value = by_pass\n        op_def[\"NumberInputs\"].value = number_inputs\n        op_def[\"OperationCategory\"].value = str(operation_category)\n        op_def[\"ParametersDefined\"].extend(transition_params)\n        op_def[\"DataDefinition\"].value = data_def\n        op_def[\"Description\"].value = str(description)\n\n        # Create OperationGroup\n        length = otio_transition.duration().value\n        operation_group = self.aaf_file.create.OperationGroup(op_def, length)\n        operation_group[\"DataDefinition\"].value = datadef\n        operation_group[\"Parameters\"].append(varying_value)\n\n        # Create Transition\n        transition = self.aaf_file.create.Transition(self.media_kind, length)\n        transition[\"OperationGroup\"].value = operation_group\n        transition[\"CutPoint\"].value = otio_transition.metadata[\"AAF\"][\"CutPoint\"]\n        transition[\"DataDefinition\"].value = datadef\n        return transition\n\n    def aaf_sequence(self, otio_track):\n        \"\"\"Convert an otio Track into an aaf Sequence\"\"\"\n        sequence = self.aaf_file.create.Sequence(media_kind=self.media_kind)\n        length = 0\n        for nested_otio_child in otio_track:\n            result = self.transcribe(nested_otio_child)\n            length += result.length\n            sequence.components.append(result)\n        sequence.length = length\n        return sequence\n\n    def aaf_operation_group(self, otio_stack):\n        \"\"\"\n        Create and return an OperationGroup which will contain other AAF objects\n        to support OTIO nesting\n        \"\"\"\n        # Create OperationDefinition\n        op_def = self.aaf_file.create.OperationDef(AAF_OPERATIONDEF_SUBMASTER,\n                                                   \"Submaster\")\n        self.aaf_file.dictionary.register_def(op_def)\n        op_def.media_kind = self.media_kind\n        datadef = self.aaf_file.dictionary.lookup_datadef(self.media_kind)\n\n        # These values are necessary for pyaaf2 OperationDefinitions\n        op_def[\"IsTimeWarp\"].value = False\n        op_def[\"Bypass\"].value = 0\n        op_def[\"NumberInputs\"].value = -1\n        op_def[\"OperationCategory\"].value = \"OperationCategory_Effect\"\n        op_def[\"DataDefinition\"].value = datadef\n\n        # Create OperationGroup\n        operation_group = self.aaf_file.create.OperationGroup(op_def)\n        operation_group.media_kind = self.media_kind\n        operation_group[\"DataDefinition\"].value = datadef\n\n        length = 0\n        for nested_otio_child in otio_stack:\n            result = self.transcribe(nested_otio_child)\n            length += result.length\n            operation_group.segments.append(result)\n        operation_group.length = length\n        return operation_group\n\n    def _create_tapemob(self, otio_clip):\n        \"\"\"\n        Return a physical sourcemob for an otio Clip based on the MobID.\n\n        Returns:\n            Returns a tuple of (TapeMob, TapeMobSlot)\n        \"\"\"\n        tapemob = self.root_file_transcriber._unique_tapemob(otio_clip)\n        tapemob_slot = tapemob.create_empty_slot(self.edit_rate, self.media_kind)\n        tapemob_slot.segment.length = (\n            otio_clip.media_reference.available_range.duration.value)\n        return tapemob, tapemob_slot\n\n    def _create_filemob(self, otio_clip, tapemob, tapemob_slot):\n        \"\"\"\n        Return a file sourcemob for an otio Clip. Needs a tapemob and tapemob slot.\n\n        Returns:\n            Returns a tuple of (FileMob, FileMobSlot)\n        \"\"\"\n        filemob = self.aaf_file.create.SourceMob()\n        self.aaf_file.content.mobs.append(filemob)\n\n        filemob.descriptor = self.default_descriptor(otio_clip)\n        filemob_slot = filemob.create_timeline_slot(self.edit_rate)\n        filemob_clip = filemob.create_source_clip(\n            slot_id=filemob_slot.slot_id,\n            length=tapemob_slot.segment.length,\n            media_kind=tapemob_slot.segment.media_kind)\n        filemob_clip.mob = tapemob\n        filemob_clip.slot = tapemob_slot\n        filemob_clip.slot_id = tapemob_slot.slot_id\n        filemob_slot.segment = filemob_clip\n        return filemob, filemob_slot\n\n    def _create_mastermob(self, otio_clip, filemob, filemob_slot):\n        \"\"\"\n        Return a mastermob for an otio Clip. Needs a filemob and filemob slot.\n\n        Returns:\n            Returns a tuple of (MasterMob, MasterMobSlot)\n        \"\"\"\n        mastermob = self.root_file_transcriber._unique_mastermob(otio_clip)\n        timecode_length = otio_clip.media_reference.available_range.duration.value\n\n        try:\n            mastermob_slot = mastermob.slot_at(self._master_mob_slot_id)\n        except IndexError:\n            mastermob_slot = (\n                mastermob.create_timeline_slot(edit_rate=self.edit_rate,\n                                               slot_id=self._master_mob_slot_id))\n        mastermob_clip = mastermob.create_source_clip(\n            slot_id=mastermob_slot.slot_id,\n            length=timecode_length,\n            media_kind=self.media_kind)\n        mastermob_clip.mob = filemob\n        mastermob_clip.slot = filemob_slot\n        mastermob_clip.slot_id = filemob_slot.slot_id\n        mastermob_slot.segment = mastermob_clip\n        return mastermob, mastermob_slot\n\n\nclass VideoTrackTranscriber(_TrackTranscriber):\n    \"\"\"Video track kind specialization of TrackTranscriber.\"\"\"\n\n    @property\n    def media_kind(self):\n        return \"picture\"\n\n    @property\n    def _master_mob_slot_id(self):\n        return 1\n\n    def _create_timeline_mobslot(self):\n        \"\"\"\n        Create a Sequence container (TimelineMobSlot) and Sequence.\n\n        TimelineMobSlot --> Sequence\n        \"\"\"\n        timeline_mobslot = self.compositionmob.create_timeline_slot(\n            edit_rate=self.edit_rate)\n        sequence = self.aaf_file.create.Sequence(media_kind=self.media_kind)\n        timeline_mobslot.segment = sequence\n        return timeline_mobslot, sequence\n\n    def default_descriptor(self, otio_clip):\n        # TODO: Determine if these values are the correct, and if so,\n        # maybe they should be in the AAF metadata\n        descriptor = self.aaf_file.create.CDCIDescriptor()\n        descriptor[\"ComponentWidth\"].value = 8\n        descriptor[\"HorizontalSubsampling\"].value = 2\n        descriptor[\"ImageAspectRatio\"].value = \"16/9\"\n        descriptor[\"StoredWidth\"].value = 1920\n        descriptor[\"StoredHeight\"].value = 1080\n        descriptor[\"FrameLayout\"].value = \"FullFrame\"\n        descriptor[\"VideoLineMap\"].value = [42, 0]\n        descriptor[\"SampleRate\"].value = 24\n        descriptor[\"Length\"].value = 1\n        return descriptor\n\n    def _transition_parameters(self):\n        \"\"\"\n        Return video transition parameters\n        \"\"\"\n        # Create ParameterDef for AvidParameterByteOrder\n        byteorder_typedef = self.aaf_file.dictionary.lookup_typedef(\"aafUInt16\")\n        param_byteorder = self.aaf_file.create.ParameterDef(\n            AAF_PARAMETERDEF_AVIDPARAMETERBYTEORDER,\n            \"AvidParameterByteOrder\",\n            \"\",\n            byteorder_typedef)\n        self.aaf_file.dictionary.register_def(param_byteorder)\n\n        # Create ParameterDef for AvidEffectID\n        avid_effect_typdef = self.aaf_file.dictionary.lookup_typedef(\"AvidBagOfBits\")\n        param_effect_id = self.aaf_file.create.ParameterDef(\n            AAF_PARAMETERDEF_AVIDEFFECTID,\n            \"AvidEffectID\",\n            \"\",\n            avid_effect_typdef)\n        self.aaf_file.dictionary.register_def(param_effect_id)\n\n        # Create ParameterDef for AFX_FG_KEY_OPACITY_U\n        opacity_param_def = self.aaf_file.dictionary.lookup_typedef(\"Rational\")\n        opacity_param = self.aaf_file.create.ParameterDef(\n            AAF_PARAMETERDEF_AFX_FG_KEY_OPACITY_U,\n            \"AFX_FG_KEY_OPACITY_U\",\n            \"\",\n            opacity_param_def)\n        self.aaf_file.dictionary.register_def(opacity_param)\n\n        # Create VaryingValue\n        opacity_u = self.aaf_file.create.VaryingValue()\n        opacity_u.parameterdef = self.aaf_file.dictionary.lookup_parameterdef(\n            \"AFX_FG_KEY_OPACITY_U\")\n        opacity_u[\"VVal_Extrapolation\"].value = AAF_VVAL_EXTRAPOLATION_ID\n        opacity_u[\"VVal_FieldCount\"].value = 1\n\n        return [param_byteorder, param_effect_id], opacity_u\n\n\nclass AudioTrackTranscriber(_TrackTranscriber):\n    \"\"\"Audio track kind specialization of TrackTranscriber.\"\"\"\n\n    @property\n    def media_kind(self):\n        return \"sound\"\n\n    @property\n    def _master_mob_slot_id(self):\n        return 2\n\n    def aaf_sourceclip(self, otio_clip):\n        # Parameter Definition\n        typedef = self.aaf_file.dictionary.lookup_typedef(\"Rational\")\n        param_def = self.aaf_file.create.ParameterDef(AAF_PARAMETERDEF_PAN,\n                                                      \"Pan\",\n                                                      \"Pan\",\n                                                      typedef)\n        self.aaf_file.dictionary.register_def(param_def)\n        interp_def = self.aaf_file.create.InterpolationDef(aaf2.misc.LinearInterp,\n                                                           \"LinearInterp\",\n                                                           \"LinearInterp\")\n        self.aaf_file.dictionary.register_def(interp_def)\n        # PointList\n        length = otio_clip.duration().value\n        c1 = self.aaf_file.create.ControlPoint()\n        c1[\"ControlPointSource\"].value = 2\n        c1[\"Time\"].value = aaf2.rational.AAFRational(\"0/{}\".format(length))\n        c1[\"Value\"].value = 0\n        c2 = self.aaf_file.create.ControlPoint()\n        c2[\"ControlPointSource\"].value = 2\n        c2[\"Time\"].value = aaf2.rational.AAFRational(\"{}/{}\".format(length - 1, length))\n        c2[\"Value\"].value = 0\n        varying_value = self.aaf_file.create.VaryingValue()\n        varying_value.parameterdef = param_def\n        varying_value[\"Interpolation\"].value = interp_def\n        varying_value[\"PointList\"].extend([c1, c2])\n        opgroup = self.timeline_mobslot.segment\n        opgroup.parameters.append(varying_value)\n\n        return super(AudioTrackTranscriber, self).aaf_sourceclip(otio_clip)\n\n    def _create_timeline_mobslot(self):\n        \"\"\"\n        Create a Sequence container (TimelineMobSlot) and Sequence.\n        Sequence needs to be in an OperationGroup.\n\n        TimelineMobSlot --> OperationGroup --> Sequence\n        \"\"\"\n        # TimelineMobSlot\n        timeline_mobslot = self.compositionmob.create_sound_slot(\n            edit_rate=self.edit_rate)\n        # OperationDefinition\n        opdef = self.aaf_file.create.OperationDef(AAF_OPERATIONDEF_MONOAUDIOPAN,\n                                                  \"Audio Pan\")\n        opdef.media_kind = self.media_kind\n        opdef[\"NumberInputs\"].value = 1\n        self.aaf_file.dictionary.register_def(opdef)\n        # OperationGroup\n        total_length = sum([t.duration().value for t in self.otio_track])\n        opgroup = self.aaf_file.create.OperationGroup(opdef)\n        opgroup.media_kind = self.media_kind\n        opgroup.length = total_length\n        timeline_mobslot.segment = opgroup\n        # Sequence\n        sequence = self.aaf_file.create.Sequence(media_kind=self.media_kind)\n        sequence.length = total_length\n        opgroup.segments.append(sequence)\n        return timeline_mobslot, sequence\n\n    def default_descriptor(self, otio_clip):\n        descriptor = self.aaf_file.create.PCMDescriptor()\n        descriptor[\"AverageBPS\"].value = 96000\n        descriptor[\"BlockAlign\"].value = 2\n        descriptor[\"QuantizationBits\"].value = 16\n        descriptor[\"AudioSamplingRate\"].value = 48000\n        descriptor[\"Channels\"].value = 1\n        descriptor[\"SampleRate\"].value = 48000\n        descriptor[\"Length\"].value = (\n            otio_clip.media_reference.available_range.duration.value)\n        return descriptor\n\n    def _transition_parameters(self):\n        \"\"\"\n        Return audio transition parameters\n        \"\"\"\n        # Create ParameterDef for ParameterDef_Level\n        def_level_typedef = self.aaf_file.dictionary.lookup_typedef(\"Rational\")\n        param_def_level = self.aaf_file.create.ParameterDef(AAF_PARAMETERDEF_LEVEL,\n                                                            \"ParameterDef_Level\",\n                                                            \"\",\n                                                            def_level_typedef)\n        self.aaf_file.dictionary.register_def(param_def_level)\n\n        # Create VaryingValue\n        level = self.aaf_file.create.VaryingValue()\n        level.parameterdef = (\n            self.aaf_file.dictionary.lookup_parameterdef(\"ParameterDef_Level\"))\n\n        return [param_def_level], level\n\n\nclass __check(object):\n    \"\"\"\n    __check is a private helper class that safely gets values given to check\n    for existence and equality\n    \"\"\"\n\n    def __init__(self, obj, tokenpath):\n        self.orig = obj\n        self.value = obj\n        self.errors = []\n        self.tokenpath = tokenpath\n        try:\n            for token in re.split(r\"[\\.\\[]\", tokenpath):\n                if token.endswith(\"()\"):\n                    self.value = getattr(self.value, token.replace(\"()\", \"\"))()\n                elif \"]\" in token:\n                    self.value = self.value[token.strip(\"[]'\\\"\")]\n                else:\n                    self.value = getattr(self.value, token)\n        except Exception as e:\n            self.value = None\n            self.errors.append(\"{}{} {}.{} does not exist, {}\".format(\n                self.orig.name if hasattr(self.orig, \"name\") else \"\",\n                type(self.orig),\n                type(self.orig).__name__,\n                self.tokenpath, e))\n\n    def equals(self, val):\n        \"\"\"Check if the retrieved value is equal to a given value.\"\"\"\n        if self.value is not None and self.value != val:\n            self.errors.append(\n                \"{}{} {}.{} not equal to {} (expected) != {} (actual)\".format(\n                    self.orig.name if hasattr(self.orig, \"name\") else \"\",\n                    type(self.orig),\n                    type(self.orig).__name__, self.tokenpath, val, self.value))\n        return self\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/advanced_authoring_format.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"OpenTimelineIO Advanced Authoring Format (AAF) Adapter\n\nDepending on if/where PyAAF is installed, you may need to set this env var:\n    OTIO_AAF_PYTHON_LIB - should point at the PyAAF module.\n\"\"\"\n\nimport os\nimport sys\nimport numbers\nimport copy\nfrom collections import Iterable\nimport opentimelineio as otio\n\nlib_path = os.environ.get(\"OTIO_AAF_PYTHON_LIB\")\nif lib_path and lib_path not in sys.path:\n    sys.path.insert(0, lib_path)\n\nimport aaf2  # noqa: E402\nimport aaf2.content  # noqa: E402\nimport aaf2.mobs  # noqa: E402\nimport aaf2.components  # noqa: E402\nimport aaf2.core  # noqa: E402\nfrom opentimelineio_contrib.adapters.aaf_adapter import aaf_writer  # noqa: E402\n\n\ndebug = False\n__names = set()\n\n\ndef _get_parameter(item, parameter_name):\n    values = dict((value.name, value) for value in item.parameters.value)\n    return values.get(parameter_name)\n\n\ndef _get_name(item):\n    if isinstance(item, aaf2.components.SourceClip):\n        try:\n            return item.mob.name or \"Untitled SourceClip\"\n        except AttributeError:\n            # Some AAFs produce this error:\n            # RuntimeError: failed with [-2146303738]: mob not found\n            return \"SourceClip Missing Mob?\"\n    if hasattr(item, 'name'):\n        name = item.name\n        if name:\n            return name\n    return _get_class_name(item)\n\n\ndef _get_class_name(item):\n    if hasattr(item, \"class_name\"):\n        return item.class_name\n    else:\n        return item.__class__.__name__\n\n\ndef _transcribe_property(prop):\n    # XXX: The unicode type doesn't exist in Python 3 (all strings are unicode)\n    # so we have to use type(u\"\") which works in both Python 2 and 3.\n    if isinstance(prop, (str, type(u\"\"), numbers.Integral, float)):\n        return prop\n\n    elif isinstance(prop, list):\n        result = {}\n        for child in prop:\n            if hasattr(child, \"name\") and hasattr(child, \"value\"):\n                result[child.name] = _transcribe_property(child.value)\n            else:\n                # @TODO: There may be more properties that we might want also.\n                # If you want to see what is being skipped, turn on debug.\n                if debug:\n                    debug_message = \\\n                        \"Skipping unrecognized property: {} of parent {}\"\n                    print(debug_message.format(child, prop))\n        return result\n    elif hasattr(prop, \"properties\"):\n        result = {}\n        for child in prop.properties():\n            result[child.name] = _transcribe_property(child.value)\n        return result\n    else:\n        return str(prop)\n\n\ndef _find_timecode_mobs(item):\n    mobs = [item.mob]\n\n    for c in item.walk():\n        if isinstance(c, aaf2.components.SourceClip):\n            mob = c.mob\n            if mob:\n                mobs.append(mob)\n            else:\n                continue\n        else:\n            # This could be 'EssenceGroup', 'Pulldown' or other segment\n            # subclasses\n            # See also: https://jira.pixar.com/browse/SE-3457\n            # For example:\n            # An EssenceGroup is a Segment that has one or more\n            # alternate choices, each of which represent different variations\n            # of one actual piece of content.\n            # According to the AAF Object Specification and Edit Protocol\n            # documents:\n            # \"Typically the different representations vary in essence format,\n            # compression, or frame size. The application is responsible for\n            # choosing the appropriate implementation of the essence.\"\n            # It also says they should all have the same length, but\n            # there might be nested Sequences inside which we're not attempting\n            # to handle here (yet). We'll need a concrete example to ensure\n            # we're doing the right thing.\n            # TODO: Is the Timecode for an EssenceGroup correct?\n            # TODO: Try CountChoices() and ChoiceAt(i)\n            # For now, lets just skip it.\n            continue\n\n    return mobs\n\n\ndef _extract_timecode_info(mob):\n    \"\"\"Given a mob with a single timecode slot, return the timecode and length\n    in that slot as a tuple\n    \"\"\"\n    timecodes = [slot.segment for slot in mob.slots\n                 if isinstance(slot.segment, aaf2.components.Timecode)]\n\n    if len(timecodes) == 1:\n        timecode = timecodes[0]\n        timecode_start = timecode.getvalue('Start')\n        timecode_length = timecode.getvalue('Length')\n\n        if timecode_start is None or timecode_length is None:\n            raise otio.exceptions.NotSupportedError(\n                \"Unexpected timecode value(s) in mob named: `{}`.\"\n                \" `Start`: {}, `Length`: {}\".format(mob.name,\n                                                    timecode_start,\n                                                    timecode_length)\n            )\n\n        return timecode_start, timecode_length\n    elif len(timecodes) > 1:\n        raise otio.exceptions.NotSupportedError(\n            \"Error: mob has more than one timecode slots, this is not\"\n            \" currently supported by the AAF adapter. found: {} slots, \"\n            \" mob name is: '{}'\".format(len(timecodes), mob.name)\n        )\n    else:\n        return None\n\n\ndef _add_child(parent, child, source):\n    if child is None:\n        if debug:\n            print(\"Adding null child? {}\".format(source))\n    elif isinstance(child, otio.schema.Marker):\n        parent.markers.append(child)\n    else:\n        parent.append(child)\n\n\ndef _transcribe(item, parents, editRate, masterMobs):\n    result = None\n    metadata = {}\n\n    # First lets grab some standard properties that are present on\n    # many types of AAF objects...\n    metadata[\"Name\"] = _get_name(item)\n    metadata[\"ClassName\"] = _get_class_name(item)\n\n    # Some AAF objects (like TimelineMobSlot) have an edit rate\n    # which should be used for all of the object's children.\n    # We will pass it on to any recursive calls to _transcribe()\n    if hasattr(item, \"edit_rate\"):\n        editRate = float(item.edit_rate)\n\n    if isinstance(item, aaf2.components.Component):\n        metadata[\"Length\"] = item.length\n\n    if isinstance(item, aaf2.core.AAFObject):\n        for prop in item.properties():\n            if hasattr(prop, 'name') and hasattr(prop, 'value'):\n                key = str(prop.name)\n                value = prop.value\n                metadata[key] = _transcribe_property(value)\n\n    # Now we will use the item's class to determine which OTIO type\n    # to transcribe into. Note that the order of this if/elif/... chain\n    # is important, because the class hierarchy of AAF objects is more\n    # complex than OTIO.\n\n    if isinstance(item, aaf2.content.ContentStorage):\n        result = otio.schema.SerializableCollection()\n\n        # Gather all the Master Mobs, so we can find them later by MobID\n        # when we parse the SourceClips in the composition\n        if masterMobs is None:\n            masterMobs = {}\n        for mob in item.mastermobs():\n            child = _transcribe(mob, parents + [item], editRate, masterMobs)\n            if child is not None:\n                mobID = child.metadata.get(\"AAF\", {}).get(\"MobID\")\n                masterMobs[mobID] = child\n\n        for mob in item.compositionmobs():\n            child = _transcribe(mob, parents + [item], editRate, masterMobs)\n            _add_child(result, child, mob)\n\n    elif isinstance(item, aaf2.mobs.Mob):\n        result = otio.schema.Timeline()\n\n        for slot in item.slots:\n            track = _transcribe(slot, parents + [item], editRate, masterMobs)\n            _add_child(result.tracks, track, slot)\n\n            # Use a heuristic to find the starting timecode from\n            # this track and use it for the Timeline's global_start_time\n            start_time = _find_timecode_track_start(track)\n            if start_time:\n                result.global_start_time = start_time\n\n    elif isinstance(item, aaf2.components.SourceClip):\n        result = otio.schema.Clip()\n\n        # Evidently the last mob is the one with the timecode\n        mobs = _find_timecode_mobs(item)\n        # Get the Timecode start and length values\n        last_mob = mobs[-1] if mobs else None\n        timecode_info = _extract_timecode_info(last_mob) if last_mob else None\n\n        source_start = int(metadata.get(\"StartTime\", \"0\"))\n        source_length = item.length\n        media_start = source_start\n        media_length = item.length\n\n        if timecode_info:\n            media_start, media_length = timecode_info\n            source_start += media_start\n\n        # The goal here is to find a source range. Actual editorial opinions are found on SourceClips in the\n        # CompositionMobs. To figure out whether this clip is directly in the CompositionMob, we detect if our\n        # parent mobs are only CompositionMobs. If they were anything else - a MasterMob, a SourceMob, we would\n        # know that this is in some indirect relationship.\n        parent_mobs = filter(lambda parent: isinstance(parent, aaf2.mobs.Mob), parents)\n        is_directly_in_composition = all(isinstance(mob, aaf2.mobs.CompositionMob) for mob in parent_mobs)\n        if is_directly_in_composition:\n            result.source_range = otio.opentime.TimeRange(\n                otio.opentime.RationalTime(source_start, editRate),\n                otio.opentime.RationalTime(source_length, editRate)\n            )\n\n        # The goal here is to find an available range. Media ranges are stored in the related MasterMob, and there\n        # should only be one - hence the name \"Master\" mob. Somewhere down our chain (either a child or our parents)\n        # is a MasterMob. For SourceClips in the CompositionMob, it is our child. For everything else, it is a\n        # previously encountered parent. Find the MasterMob in our chain, and then extract the information from that.\n        child_mastermob = item.mob if isinstance(item.mob, aaf2.mobs.MasterMob) else None\n        parent_mastermobs = [parent for parent in parents if isinstance(parent, aaf2.mobs.MasterMob)]\n        parent_mastermob = parent_mastermobs[0] if len(parent_mastermobs) > 1 else None\n        mastermob = child_mastermob or parent_mastermob or None\n\n        if mastermob:\n            media = otio.schema.MissingReference()\n            media.available_range = otio.opentime.TimeRange(\n                otio.opentime.RationalTime(media_start, editRate),\n                otio.opentime.RationalTime(media_length, editRate)\n            )\n            # copy the metadata from the master into the media_reference\n            mastermob_child = masterMobs.get(str(mastermob.mob_id))\n            media.metadata[\"AAF\"] = mastermob_child.metadata.get(\"AAF\", {})\n            result.media_reference = media\n\n    elif isinstance(item, aaf2.components.Transition):\n        result = otio.schema.Transition()\n\n        # Does AAF support anything else?\n        result.transition_type = otio.schema.TransitionTypes.SMPTE_Dissolve\n\n        # Extract value and time attributes of both ControlPoints used for\n        # creating AAF Transition objects\n        varying_value = None\n        for param in item.getvalue('OperationGroup').parameters:\n            if isinstance(param, aaf2.misc.VaryingValue):\n                varying_value = param\n                break\n\n        if varying_value is not None:\n            for control_point in varying_value.getvalue('PointList'):\n                value = control_point.value\n                time = control_point.time\n                metadata.setdefault('PointList', []).append({'Value': value,\n                                                             'Time': time})\n\n        in_offset = int(metadata.get(\"CutPoint\", \"0\"))\n        out_offset = item.length - in_offset\n        result.in_offset = otio.opentime.RationalTime(in_offset, editRate)\n        result.out_offset = otio.opentime.RationalTime(out_offset, editRate)\n\n    elif isinstance(item, aaf2.components.Filler):\n        result = otio.schema.Gap()\n\n        length = item.length\n        result.source_range = otio.opentime.TimeRange(\n            otio.opentime.RationalTime(0, editRate),\n            otio.opentime.RationalTime(length, editRate)\n        )\n\n    elif isinstance(item, aaf2.components.NestedScope):\n        # TODO: Is this the right class?\n        result = otio.schema.Stack()\n\n        for slot in item.slots:\n            child = _transcribe(slot, parents + [item], editRate, masterMobs)\n            _add_child(result, child, slot)\n\n    elif isinstance(item, aaf2.components.Sequence):\n        result = otio.schema.Track()\n\n        for component in item.components:\n            child = _transcribe(component, parents + [item], editRate, masterMobs)\n            _add_child(result, child, component)\n\n    elif isinstance(item, aaf2.components.OperationGroup):\n        result = _transcribe_operation_group(\n            item, parents, metadata, editRate, masterMobs\n        )\n\n    elif isinstance(item, aaf2.mobslots.TimelineMobSlot):\n        result = otio.schema.Track()\n\n        child = _transcribe(item.segment, parents + [item], editRate, masterMobs)\n        _add_child(result, child, item.segment)\n\n    elif isinstance(item, aaf2.mobslots.MobSlot):\n        result = otio.schema.Track()\n\n        child = _transcribe(item.segment, parents + [item], editRate, masterMobs)\n        _add_child(result, child, item.segment)\n\n    elif isinstance(item, aaf2.components.Timecode):\n        pass\n\n    elif isinstance(item, aaf2.components.Pulldown):\n        pass\n\n    elif isinstance(item, aaf2.components.EdgeCode):\n        pass\n\n    elif isinstance(item, aaf2.components.ScopeReference):\n        # TODO: is this like FILLER?\n\n        result = otio.schema.Gap()\n\n        length = item.length\n        result.source_range = otio.opentime.TimeRange(\n            otio.opentime.RationalTime(0, editRate),\n            otio.opentime.RationalTime(length, editRate)\n        )\n\n    elif isinstance(item, aaf2.components.DescriptiveMarker):\n\n        # Markers come in on their own separate Track.\n        # TODO: We should consolidate them onto the same track(s) as the clips\n        # result = otio.schema.Marker()\n        pass\n\n    elif isinstance(item, aaf2.components.Selector):\n        # If you mute a clip in media composer, it becomes one of these in the\n        # AAF.\n        result = _transcribe(\n            item.getvalue(\"Selected\"),\n            parents + [item],\n            editRate,\n            masterMobs\n        )\n\n        alternates = [\n            _transcribe(alt, parents + [item], editRate, masterMobs)\n            for alt in item.getvalue(\"Alternates\")\n        ]\n\n        # muted case -- if there is only one item its muted, otherwise its\n        # a multi cam thing\n        if alternates and len(alternates) == 1:\n            metadata['muted_clip'] = True\n            result.name = str(alternates[0].name) + \"_MUTED\"\n\n        metadata['alternates'] = alternates\n\n    # @TODO: There are a bunch of other AAF object types that we will\n    # likely need to add support for. I'm leaving this code here to help\n    # future efforts to extract the useful information out of these.\n\n    # elif isinstance(item, aaf.storage.File):\n    #     self.extendChildItems([item.header])\n\n    # elif isinstance(item, aaf.storage.Header):\n    #     self.extendChildItems([item.storage()])\n    #     self.extendChildItems([item.dictionary()])\n\n    # elif isinstance(item, aaf.dictionary.Dictionary):\n    #     l = []\n    #     l.append(DummyItem(list(item.class_defs()), 'ClassDefs'))\n    #     l.append(DummyItem(list(item.codec_defs()), 'CodecDefs'))\n    #     l.append(DummyItem(list(item.container_defs()), 'ContainerDefs'))\n    #     l.append(DummyItem(list(item.data_defs()), 'DataDefs'))\n    #     l.append(DummyItem(list(item.interpolation_defs()),\n    #        'InterpolationDefs'))\n    #     l.append(DummyItem(list(item.klvdata_defs()), 'KLVDataDefs'))\n    #     l.append(DummyItem(list(item.operation_defs()), 'OperationDefs'))\n    #     l.append(DummyItem(list(item.parameter_defs()), 'ParameterDefs'))\n    #     l.append(DummyItem(list(item.plugin_defs()), 'PluginDefs'))\n    #     l.append(DummyItem(list(item.taggedvalue_defs()), 'TaggedValueDefs'))\n    #     l.append(DummyItem(list(item.type_defs()), 'TypeDefs'))\n    #     self.extendChildItems(l)\n    #\n    #     elif isinstance(item, pyaaf.AxSelector):\n    #         self.extendChildItems(list(item.EnumAlternateSegments()))\n    #\n    #     elif isinstance(item, pyaaf.AxScopeReference):\n    #         #print item, item.GetRelativeScope(),item.GetRelativeSlot()\n    #         pass\n    #\n    #     elif isinstance(item, pyaaf.AxEssenceGroup):\n    #         segments = []\n    #\n    #         for i in xrange(item.CountChoices()):\n    #             choice = item.GetChoiceAt(i)\n    #             segments.append(choice)\n    #         self.extendChildItems(segments)\n    #\n    #     elif isinstance(item, pyaaf.AxProperty):\n    #         self.properties['Value'] = str(item.GetValue())\n\n    elif isinstance(item, Iterable):\n        result = otio.schema.SerializableCollection()\n        for child in item:\n            result.append(\n                _transcribe(\n                    child,\n                    parents + [item],\n                    editRate,\n                    masterMobs\n                )\n            )\n    else:\n        # For everything else, we just ignore it.\n        # To see what is being ignored, turn on the debug flag\n        if debug:\n            print(\"SKIPPING: {}: {} -- {}\".format(type(item), item, result))\n\n    # Did we get anything? If not, we're done\n    if result is None:\n        return None\n\n    # Okay, now we've turned the AAF thing into an OTIO result\n    # There's a bit more we can do before we're ready to return the result.\n\n    # If we didn't get a name yet, use the one we have in metadata\n    if result.name is None:\n        result.name = metadata[\"Name\"]\n\n    # Attach the AAF metadata\n    if not result.metadata:\n        result.metadata = {}\n    result.metadata[\"AAF\"] = metadata\n\n    # Double check that we got the length we expected\n    if isinstance(result, otio.core.Item):\n        length = metadata.get(\"Length\")\n        if (\n                length\n                and result.source_range is not None\n                and result.source_range.duration.value != length\n        ):\n            raise otio.exceptions.OTIOError(\n                \"Wrong duration? {} should be {} in {}\".format(\n                    result.source_range.duration.value,\n                    length,\n                    result\n                )\n            )\n\n    # Did we find a Track?\n    if isinstance(result, otio.schema.Track):\n        # Try to figure out the kind of Track it is\n        if hasattr(item, 'media_kind'):\n            media_kind = str(item.media_kind)\n            result.metadata[\"AAF\"][\"MediaKind\"] = media_kind\n            if media_kind == \"Picture\":\n                result.kind = otio.schema.TrackKind.Video\n            elif media_kind in (\"SoundMasterTrack\", \"Sound\"):\n                result.kind = otio.schema.TrackKind.Audio\n            else:\n                # Timecode, Edgecode, others?\n                result.kind = None\n\n    # Done!\n    return result\n\n\ndef _find_timecode_track_start(track):\n    # See if we can find a starting timecode in here...\n    aaf_metadata = track.metadata.get(\"AAF\", {})\n\n    # Is this a Timecode track?\n    if aaf_metadata.get(\"MediaKind\") == \"Timecode\":\n        edit_rate = aaf_metadata.get(\"EditRate\", \"0\")\n        fps = aaf_metadata.get(\"Segment\", {}).get(\"FPS\", 0)\n        start = aaf_metadata.get(\"Segment\", {}).get(\"Start\", \"0\")\n\n        # Often times there are several timecode tracks, so\n        # we use a heuristic to only pay attention to Timecode\n        # tracks with a FPS that matches the edit rate.\n        if edit_rate == str(fps):\n            return otio.opentime.RationalTime(\n                value=int(start),\n                rate=float(edit_rate)\n            )\n\n    # We didn't find anything useful\n    return None\n\n\ndef _transcribe_linear_timewarp(item, parameters):\n    # this is a linear time warp\n    effect = otio.schema.LinearTimeWarp()\n\n    offset_map = _get_parameter(item, 'PARAM_SPEED_OFFSET_MAP_U')\n\n    # If we have a LinearInterp with just 2 control points, then\n    # we can compute the time_scalar. Note that the SpeedRatio is\n    # NOT correct in many AAFs - we aren't sure why, but luckily we\n    # can compute the correct value this way.\n    points = offset_map.get(\"PointList\")\n    if len(points) > 2:\n        # This is something complicated... try the fancy version\n        return _transcribe_fancy_timewarp(item, parameters)\n    elif (\n        len(points) == 2\n        and float(points[0].time) == 0\n        and float(points[0].value) == 0\n    ):\n        # With just two points, we can compute the slope\n        effect.time_scalar = float(points[1].value) / float(points[1].time)\n    else:\n        # Fall back to the SpeedRatio if we didn't understand the points\n        ratio = parameters.get(\"SpeedRatio\")\n        if ratio == str(item.length):\n            # If the SpeedRatio == the length, this is a freeze frame\n            effect.time_scalar = 0\n        elif '/' in ratio:\n            numerator, denominator = map(float, ratio.split('/'))\n            # OTIO time_scalar is 1/x from AAF's SpeedRatio\n            effect.time_scalar = denominator / numerator\n        else:\n            effect.time_scalar = 1.0 / float(ratio)\n\n    # Is this is a freeze frame?\n    if effect.time_scalar == 0:\n        # Note: we might end up here if any of the code paths above\n        # produced a 0 time_scalar.\n        # Use the FreezeFrame class instead of LinearTimeWarp\n        effect = otio.schema.FreezeFrame()\n\n    return effect\n\n\ndef _transcribe_fancy_timewarp(item, parameters):\n\n    # For now, this is an unsupported time effect...\n    effect = otio.schema.TimeEffect()\n    effect.effect_name = None  # Unsupported\n    effect.name = item.get(\"Name\")\n\n    return effect\n\n    # TODO: Here is some sample code that pulls out the full\n    # details of a non-linear speed map.\n\n    # speed_map = item.parameter['PARAM_SPEED_MAP_U']\n    # offset_map = item.parameter['PARAM_SPEED_OFFSET_MAP_U']\n    # Also? PARAM_OFFSET_MAP_U (without the word \"SPEED\" in it?)\n    # print(speed_map['PointList'].value)\n    # print(speed_map.count())\n    # print(speed_map.interpolation_def().name)\n    #\n    # for p in speed_map.points():\n    #     print(\"  \", float(p.time), float(p.value), p.edit_hint)\n    #     for prop in p.point_properties():\n    #         print(\"    \", prop.name, prop.value, float(prop.value))\n    #\n    # print(offset_map.interpolation_def().name)\n    # for p in offset_map.points():\n    #     edit_hint = p.edit_hint\n    #     time = p.time\n    #     value = p.value\n    #\n    #     pass\n    #     # print \"  \", float(p.time), float(p.value)\n    #\n    # for i in range(100):\n    #     float(offset_map.value_at(\"%i/100\" % i))\n    #\n    # # Test file PARAM_SPEED_MAP_U is AvidBezierInterpolator\n    # # currently no implement for value_at\n    # try:\n    #     speed_map.value_at(.25)\n    # except NotImplementedError:\n    #     pass\n    # else:\n    #     raise\n\n\ndef _transcribe_operation_group(item, parents, metadata, editRate, masterMobs):\n    result = otio.schema.Stack()\n\n    operation = metadata.get(\"Operation\", {})\n    parameters = metadata.get(\"Parameters\", {})\n    result.name = operation.get(\"Name\")\n\n    # Trust the length that is specified in the AAF\n    length = metadata.get(\"Length\")\n    result.source_range = otio.opentime.TimeRange(\n        otio.opentime.RationalTime(0, editRate),\n        otio.opentime.RationalTime(length, editRate)\n    )\n\n    # Look for speed effects...\n    effect = None\n    if operation.get(\"IsTimeWarp\"):\n        if operation.get(\"Name\") == \"Motion Control\":\n\n            offset_map = _get_parameter(item, 'PARAM_SPEED_OFFSET_MAP_U')\n            # TODO: We should also check the PARAM_OFFSET_MAP_U which has\n            # an interpolation_def().name as well.\n            if offset_map is not None:\n                interpolation = offset_map.interpolation.name\n            else:\n                interpolation = None\n\n            if interpolation == \"LinearInterp\":\n                effect = _transcribe_linear_timewarp(item, parameters)\n            else:\n                effect = _transcribe_fancy_timewarp(item, parameters)\n\n        else:\n            # Unsupported time effect\n            effect = otio.schema.TimeEffect()\n            effect.effect_name = None  # Unsupported\n            effect.name = operation.get(\"Name\")\n    else:\n        # Unsupported effect\n        effect = otio.schema.Effect()\n        effect.effect_name = None  # Unsupported\n        effect.name = operation.get(\"Name\")\n\n    if effect is not None:\n        result.effects.append(effect)\n        effect.metadata = {\n            \"AAF\": {\n                \"Operation\": operation,\n                \"Parameters\": parameters\n            }\n        }\n\n    for segment in item.getvalue(\"InputSegments\"):\n        child = _transcribe(segment, parents + [item], editRate, masterMobs)\n        if child:\n            _add_child(result, child, segment)\n\n    return result\n\n\ndef _fix_transitions(thing):\n    if isinstance(thing, otio.schema.Timeline):\n        _fix_transitions(thing.tracks)\n    elif (\n        isinstance(thing, otio.core.Composition)\n        or isinstance(thing, otio.schema.SerializableCollection)\n    ):\n        if isinstance(thing, otio.schema.Track):\n            for c, child in enumerate(thing):\n\n                # Don't touch the Transitions themselves,\n                # only the Clips & Gaps next to them.\n                if not isinstance(child, otio.core.Item):\n                    continue\n\n                # Was the item before us a Transition?\n                if c > 0 and isinstance(\n                    thing[c - 1],\n                    otio.schema.Transition\n                ):\n                    pre_trans = thing[c - 1]\n\n                    if child.source_range is None:\n                        child.source_range = child.trimmed_range()\n                    csr = child.source_range\n                    child.source_range = otio.opentime.TimeRange(\n                        start_time=csr.start_time + pre_trans.in_offset,\n                        duration=csr.duration - pre_trans.in_offset\n                    )\n\n                # Is the item after us a Transition?\n                if c < len(thing) - 1 and isinstance(\n                    thing[c + 1],\n                    otio.schema.Transition\n                ):\n                    post_trans = thing[c + 1]\n\n                    if child.source_range is None:\n                        child.source_range = child.trimmed_range()\n                    csr = child.source_range\n                    child.source_range = otio.opentime.TimeRange(\n                        start_time=csr.start_time,\n                        duration=csr.duration - post_trans.out_offset\n                    )\n\n        for child in thing:\n            _fix_transitions(child)\n\n\ndef _simplify(thing):\n    if isinstance(thing, otio.schema.SerializableCollection):\n        if len(thing) == 1:\n            return _simplify(thing[0])\n        else:\n            for c, child in enumerate(thing):\n                thing[c] = _simplify(child)\n            return thing\n\n    elif isinstance(thing, otio.schema.Timeline):\n        result = _simplify(thing.tracks)\n\n        # Only replace the Timeline's stack if the simplified result\n        # was also a Stack. Otherwise leave it (the contents will have\n        # been simplified in place).\n        if isinstance(result, otio.schema.Stack):\n            thing.tracks = result\n\n        return thing\n\n    elif isinstance(thing, otio.core.Composition):\n        # simplify our children\n        for c, child in enumerate(thing):\n            thing[c] = _simplify(child)\n\n        # remove empty children of Stacks\n        if isinstance(thing, otio.schema.Stack):\n            for c in reversed(range(len(thing))):\n                child = thing[c]\n                if not _contains_something_valuable(child):\n                    # TODO: We're discarding metadata... should we retain it?\n                    del thing[c]\n\n            # Look for Stacks within Stacks\n            c = len(thing) - 1\n            while c >= 0:\n                child = thing[c]\n                # Is my child a Stack also? (with no effects)\n                if (\n                    not _has_effects(child)\n                    and\n                    (\n                        isinstance(child, otio.schema.Stack)\n                        or (\n                            isinstance(child, otio.schema.Track)\n                            and len(child) == 1\n                            and isinstance(child[0], otio.schema.Stack)\n                            and child[0]\n                            and isinstance(child[0][0], otio.schema.Track)\n                        )\n                    )\n                ):\n                    if isinstance(child, otio.schema.Track):\n                        child = child[0]\n\n                    # Pull the child's children into the parent\n                    num = len(child)\n                    children_of_child = child[:]\n                    # clear out the ownership of 'child'\n                    del child[:]\n                    thing[c:c + 1] = children_of_child\n\n                    # TODO: We may be discarding metadata, should we merge it?\n                    # TODO: Do we need to offset the markers in time?\n                    thing.markers.extend(child.markers)\n                    # Note: we don't merge effects, because we already made\n                    # sure the child had no effects in the if statement above.\n\n                    c = c + num\n                c = c - 1\n\n        # skip redundant containers\n        if _is_redundant_container(thing):\n            # TODO: We may be discarding metadata here, should we merge it?\n            result = thing[0].deepcopy()\n            # TODO: Do we need to offset the markers in time?\n            result.markers.extend(thing.markers)\n            # TODO: The order of the effects is probably important...\n            # should they be added to the end or the front?\n            # Intuitively it seems like the child's effects should come before\n            # the parent's effects. This will need to be solidified when we\n            # add more effects support.\n            result.effects.extend(thing.effects)\n            # Keep the parent's length, if it has one\n            if thing.source_range:\n                # make sure it has a source_range first\n                if not result.source_range:\n                    try:\n                        result.source_range = result.trimmed_range()\n                    except otio.exceptions.CannotComputeAvailableRangeError:\n                        result.source_range = copy.copy(thing.source_range)\n                # modify the duration, but leave the start_time as is\n                result.source_range = otio.opentime.TimeRange(\n                    result.source_range.start_time,\n                    thing.source_range.duration\n                )\n            return result\n\n    # if thing is the top level stack, all of its children must be in tracks\n    if isinstance(thing, otio.schema.Stack) and thing.parent() is None:\n        children_needing_tracks = []\n        for child in thing:\n            if isinstance(child, otio.schema.Track):\n                continue\n            children_needing_tracks.append(child)\n\n        for child in children_needing_tracks:\n            orig_index = thing.index(child)\n            del thing[orig_index]\n            new_track = otio.schema.Track()\n            new_track.append(child)\n            thing.insert(orig_index, new_track)\n\n    return thing\n\n\ndef _has_effects(thing):\n    if isinstance(thing, otio.core.Item):\n        if len(thing.effects) > 0:\n            return True\n\n\ndef _is_redundant_container(thing):\n\n    is_composition = isinstance(thing, otio.core.Composition)\n    if not is_composition:\n        return False\n\n    has_one_child = len(thing) == 1\n    if not has_one_child:\n        return False\n\n    am_top_level_track = (\n        type(thing) is otio.schema.Track\n        and type(thing.parent()) is otio.schema.Stack\n        and thing.parent().parent() is None\n    )\n\n    return (\n        not am_top_level_track\n        # am a top level track but my only child is a track\n        or (\n            type(thing) is otio.schema.Track\n            and type(thing[0]) is otio.schema.Track\n        )\n    )\n\n\ndef _contains_something_valuable(thing):\n    if isinstance(thing, otio.core.Item):\n        if len(thing.effects) > 0 or len(thing.markers) > 0:\n            return True\n\n    if isinstance(thing, otio.core.Composition):\n\n        if len(thing) == 0:\n            # NOT valuable because it is empty\n            return False\n\n        for child in thing:\n            if _contains_something_valuable(child):\n                # valuable because this child is valuable\n                return True\n\n        # none of the children were valuable, so thing is NOT valuable\n        return False\n\n    if isinstance(thing, otio.schema.Gap):\n        # TODO: Are there other valuable things we should look for on a Gap?\n        return False\n\n    # anything else is presumed to be valuable\n    return True\n\n\ndef read_from_file(filepath, simplify=True):\n\n    with aaf2.open(filepath) as aaf_file:\n\n        storage = aaf_file.content\n\n        # Note: We're skipping: f.header\n        # Is there something valuable in there?\n\n        __names.clear()\n        masterMobs = {}\n\n        result = _transcribe(storage, parents=list(), editRate=None, masterMobs=masterMobs)\n        top = storage.toplevel()\n        if top:\n            # re-transcribe just the top-level mobs\n            # but use all the master mobs we found in the 1st pass\n            __names.clear()  # reset the names back to 0\n        result = _transcribe(top, parents=list(), editRate=None, masterMobs=masterMobs)\n\n    # AAF is typically more deeply nested than OTIO.\n    # Lets try to simplify the structure by collapsing or removing\n    # unnecessary stuff.\n    if simplify:\n        result = _simplify(result)\n\n    # OTIO represents transitions a bit different than AAF, so\n    # we need to iterate over them and modify the items on either side.\n    # Note that we do this *after* simplifying, since the structure\n    # may change during simplification.\n    _fix_transitions(result)\n\n    return result\n\n\ndef write_to_file(input_otio, filepath, **kwargs):\n    with aaf2.open(filepath, \"w\") as f:\n\n        timeline = aaf_writer._stackify_nested_groups(input_otio)\n\n        aaf_writer.validate_metadata(timeline)\n\n        otio2aaf = aaf_writer.AAFFileTranscriber(timeline, f, **kwargs)\n\n        if not isinstance(timeline, otio.schema.Timeline):\n            raise otio.exceptions.NotSupportedError(\n                \"Currently only supporting top level Timeline\")\n\n        for otio_track in timeline.tracks:\n            # Ensure track must have clip to get the edit_rate\n            if len(otio_track) == 0:\n                continue\n\n            transcriber = otio2aaf.track_transcriber(otio_track)\n\n            for otio_child in otio_track:\n                result = transcriber.transcribe(otio_child)\n                if result:\n                    transcriber.sequence.components.append(result)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/ale.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"OpenTimelineIO Avid Log Exchange (ALE) Adapter\"\"\"\nimport re\nimport opentimelineio as otio\n\nDEFAULT_VIDEO_FORMAT = '1080'\n\n\ndef AVID_VIDEO_FORMAT_FROM_WIDTH_HEIGHT(width, height):\n    \"\"\"Utility function to map a width and height to an Avid Project Format\"\"\"\n\n    format_map = {\n        '1080': \"1080\",\n        '720': \"720\",\n        '576': \"PAL\",\n        '486': \"NTSC\",\n    }\n    mapped = format_map.get(str(height), \"CUSTOM\")\n    # check for the 2K DCI 1080 format\n    if mapped == '1080' and width > 1920:\n        mapped = \"CUSTOM\"\n    return mapped\n\n\nclass ALEParseError(otio.exceptions.OTIOError):\n    pass\n\n\ndef _parse_data_line(line, columns, fps):\n    row = line.split(\"\\t\")\n\n    if len(row) < len(columns):\n        # Fill in blanks for any missing fields in this row\n        row.extend([\"\"] * (len(columns) - len(row)))\n\n    if len(row) > len(columns):\n        raise ALEParseError(\"Too many values on row: \" + line)\n\n    try:\n\n        # Gather all the columns into a dictionary\n        # For expected columns, like Name, Start, etc. we will pop (remove)\n        # those from metadata, leaving the rest alone.\n        metadata = dict(zip(columns, row))\n\n        clip = otio.schema.Clip()\n        clip.name = metadata.pop(\"Name\", None)\n\n        # When looking for Start, Duration and End, they might be missing\n        # or blank. Treat None and \"\" as the same via: get(k,\"\")!=\"\"\n        # To have a valid source range, you need Start and either Duration\n        # or End. If all three are provided, we check to make sure they match.\n        if metadata.get(\"Start\", \"\") != \"\":\n            value = metadata.pop(\"Start\")\n            try:\n                start = otio.opentime.from_timecode(value, fps)\n            except (ValueError, TypeError):\n                raise ALEParseError(\"Invalid Start timecode: {}\".format(value))\n            duration = None\n            end = None\n            if metadata.get(\"Duration\", \"\") != \"\":\n                value = metadata.pop(\"Duration\")\n                try:\n                    duration = otio.opentime.from_timecode(value, fps)\n                except (ValueError, TypeError):\n                    raise ALEParseError(\"Invalid Duration timecode: {}\".format(\n                        value\n                    ))\n            if metadata.get(\"End\", \"\") != \"\":\n                value = metadata.pop(\"End\")\n                try:\n                    end = otio.opentime.from_timecode(value, fps)\n                except (ValueError, TypeError):\n                    raise ALEParseError(\"Invalid End timecode: {}\".format(\n                        value\n                    ))\n            if duration is None:\n                duration = end - start\n            if end is None:\n                end = start + duration\n            if end != start + duration:\n                raise ALEParseError(\n                    \"Inconsistent Start, End, Duration: \" + line\n                )\n            clip.source_range = otio.opentime.TimeRange(\n                start,\n                duration\n            )\n\n        if metadata.get(\"Source File\"):\n            source = metadata.pop(\"Source File\")\n            clip.media_reference = otio.schema.ExternalReference(\n                target_url=source\n            )\n\n        # We've pulled out the key/value pairs that we treat specially.\n        # Put the remaining key/values into clip.metadata[\"ALE\"]\n        clip.metadata[\"ALE\"] = metadata\n\n        return clip\n    except Exception as ex:\n        raise ALEParseError(\"Error parsing line: {}\\n{}\".format(\n            line, repr(ex)\n        ))\n\n\ndef _video_format_from_metadata(clips):\n    # Look for clips with Image Size metadata set\n    max_height = 0\n    max_width = 0\n    for clip in clips:\n        fields = clip.metadata.get(\"ALE\", {})\n        res = fields.get(\"Image Size\", \"\")\n        m = re.search(r'([0-9]{1,})\\s*[xX]\\s*([0-9]{1,})', res)\n        if m and len(m.groups()) >= 2:\n            width = int(m.group(1))\n            height = int(m.group(2))\n            if height > max_height:\n                max_height = height\n            if width > max_width:\n                max_width = width\n\n    # We don't have any image size information, use the defaut\n    if max_height == 0:\n        return DEFAULT_VIDEO_FORMAT\n    else:\n        return AVID_VIDEO_FORMAT_FROM_WIDTH_HEIGHT(max_width, max_height)\n\n\ndef read_from_string(input_str, fps=24):\n\n    collection = otio.schema.SerializableCollection()\n    header = {}\n    columns = []\n\n    def nextline(lines):\n        return lines.pop(0)\n\n    lines = input_str.splitlines()\n    while len(lines):\n        line = nextline(lines)\n\n        # skip blank lines\n        if line.strip() == \"\":\n            continue\n\n        if line.strip() == \"Heading\":\n            while len(lines):\n                line = nextline(lines)\n\n                if line.strip() == \"\":\n                    break\n\n                if \"\\t\" not in line:\n                    raise ALEParseError(\"Invalid Heading line: \" + line)\n\n                segments = line.split(\"\\t\")\n                while len(segments) >= 2:\n                    key, val = segments.pop(0), segments.pop(0)\n                    header[key] = val\n                if len(segments) != 0:\n                    raise ALEParseError(\"Invalid Heading line: \" + line)\n\n        if \"FPS\" in header:\n            fps = float(header[\"FPS\"])\n\n        if line.strip() == \"Column\":\n            if len(lines) == 0:\n                raise ALEParseError(\"Unexpected end of file after: \" + line)\n\n            line = nextline(lines)\n            columns = line.split(\"\\t\")\n\n        if line.strip() == \"Data\":\n            while len(lines):\n                line = nextline(lines)\n\n                if line.strip() == \"\":\n                    continue\n\n                clip = _parse_data_line(line, columns, fps)\n\n                collection.append(clip)\n\n    collection.metadata[\"ALE\"] = {\n        \"header\": header,\n        \"columns\": columns\n    }\n\n    return collection\n\n\ndef write_to_string(input_otio, columns=None, fps=None, video_format=None):\n\n    # Get all the clips we're going to export\n    clips = list(input_otio.each_clip())\n\n    result = \"\"\n\n    result += \"Heading\\n\"\n    header = dict(input_otio.metadata.get(\"ALE\", {}).get(\"header\", {}))\n\n    # Force this, since we've hard coded tab delimiters\n    header[\"FIELD_DELIM\"] = \"TABS\"\n\n    if fps is None:\n        # If we weren't given a FPS, is there one in the header metadata?\n        if \"FPS\" in header:\n            fps = float(header[\"FPS\"])\n        else:\n            # Would it be better to infer this by inspecting the input clips?\n            fps = 24\n            header[\"FPS\"] = str(fps)\n    else:\n        # Put the value we were given into the header\n        header[\"FPS\"] = str(fps)\n\n    # Check if we have been supplied a VIDEO_FORMAT, if not lets set one\n    if video_format is None:\n        # Do we already have it in the header?  If so, lets leave that as is\n        if \"VIDEO_FORMAT\" not in header:\n            header[\"VIDEO_FORMAT\"] = _video_format_from_metadata(clips)\n    else:\n        header[\"VIDEO_FORMAT\"] = str(video_format)\n\n    headers = list(header.items())\n    headers.sort()  # make the output predictable\n    for key, val in headers:\n        result += \"{}\\t{}\\n\".format(key, val)\n\n    # If the caller passed in a list of columns, use that, otherwise\n    # we need to discover the columns that should be output.\n    if columns is None:\n        # Is there a hint about the columns we want (and column ordering)\n        # at the top level?\n        columns = input_otio.metadata.get(\"ALE\", {}).get(\"columns\", [])\n\n        # Scan all the clips for any extra columns\n        for clip in clips:\n            fields = clip.metadata.get(\"ALE\", {})\n            for key in fields.keys():\n                if key not in columns:\n                    columns.append(key)\n\n    # Always output these\n    for c in [\"Duration\", \"End\", \"Start\", \"Name\", \"Source File\"]:\n        if c not in columns:\n            columns.insert(0, c)\n\n    result += \"\\nColumn\\n{}\\n\".format(\"\\t\".join(columns))\n\n    result += \"\\nData\\n\"\n\n    def val_for_column(column, clip):\n        if column == \"Name\":\n            return clip.name\n        elif column == \"Source File\":\n            if (\n                clip.media_reference and\n                hasattr(clip.media_reference, 'target_url') and\n                clip.media_reference.target_url\n            ):\n                return clip.media_reference.target_url\n            else:\n                return \"\"\n        elif column == \"Start\":\n            if not clip.source_range:\n                return \"\"\n            return otio.opentime.to_timecode(\n                clip.source_range.start_time, fps\n            )\n        elif column == \"Duration\":\n            if not clip.source_range:\n                return \"\"\n            return otio.opentime.to_timecode(\n                clip.source_range.duration, fps\n            )\n        elif column == \"End\":\n            if not clip.source_range:\n                return \"\"\n            return otio.opentime.to_timecode(\n                clip.source_range.end_time_exclusive(), fps\n            )\n        else:\n            return clip.metadata.get(\"ALE\", {}).get(column)\n\n    for clip in clips:\n        row = []\n        for column in columns:\n            val = str(val_for_column(column, clip) or \"\")\n            val.replace(\"\\t\", \" \")  # don't allow tabs inside a value\n            row.append(val)\n        result += \"\\t\".join(row) + \"\\n\"\n\n    return result\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/burnins.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\"\"\"FFMPEG Burnins Adapter\"\"\"\nimport os\nimport sys\n\n\ndef build_burnins(input_otio):\n    \"\"\"\n    Generates the burnin objects for each clip within the otio container\n\n    :param input_otio: OTIO container\n    :rtype: [ffmpeg_burnins.Burnins(), ...]\n    \"\"\"\n\n    if os.path.dirname(__file__) not in sys.path:\n        sys.path.append(os.path.dirname(__file__))\n\n    import ffmpeg_burnins\n    key = 'burnins'\n\n    burnins = []\n    for clip in input_otio.each_clip():\n\n        # per clip burnin data\n        burnin_data = clip.media_reference.metadata.get(key)\n        if not burnin_data:\n            # otherwise default to global burnin\n            burnin_data = input_otio.metadata.get(key)\n\n        if not burnin_data:\n            continue\n\n        media = clip.media_reference.target_url\n        if media.startswith('file://'):\n            media = media[7:]\n        streams = burnin_data.get('streams')\n        burnins.append(ffmpeg_burnins.Burnins(media,\n                                              streams=streams))\n        burnins[-1].otio_media = media\n        burnins[-1].otio_overwrite = burnin_data.get('overwrite')\n        burnins[-1].otio_args = burnin_data.get('args')\n\n        for burnin in burnin_data.get('burnins', []):\n            align = burnin.pop('align')\n            function = burnin.pop('function')\n            if function == 'text':\n                text = burnin.pop('text')\n                options = ffmpeg_burnins.TextOptions()\n                options.update(burnin)\n                burnins[-1].add_text(text, align, options=options)\n            elif function == 'frame_number':\n                options = ffmpeg_burnins.FrameNumberOptions()\n                options.update(burnin)\n                burnins[-1].add_frame_numbers(align, options=options)\n            elif function == 'timecode':\n                options = ffmpeg_burnins.TimeCodeOptions()\n                options.update(burnin)\n                burnins[-1].add_timecode(align, options=options)\n            else:\n                raise RuntimeError(\"Unknown function '%s'\" % function)\n\n    return burnins\n\n\ndef write_to_file(input_otio, filepath):\n    \"\"\"required OTIO function hook\"\"\"\n\n    for burnin in build_burnins(input_otio):\n        burnin.render(os.path.join(filepath, burnin.otio_media),\n                      args=burnin.otio_args,\n                      overwrite=burnin.otio_overwrite)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json",
    "content": "{\n    \"OTIO_SCHEMA\" : \"PluginManifest.1\",\n    \"adapters\": [\n        {\n            \"OTIO_SCHEMA\": \"Adapter.1\",\n            \"name\": \"fcpx_xml\",\n            \"execution_scope\": \"in process\",\n            \"filepath\": \"fcpx_xml.py\",\n            \"suffixes\": [\"fcpxml\"]\n        },\n        {\n            \"OTIO_SCHEMA\": \"Adapter.1\",\n            \"name\": \"hls_playlist\",\n            \"execution_scope\": \"in process\",\n            \"filepath\": \"hls_playlist.py\",\n            \"suffixes\": [\"m3u8\"]\n        },\n        {\n            \"OTIO_SCHEMA\" : \"Adapter.1\",\n            \"name\" : \"rv_session\",\n            \"execution_scope\" : \"in process\",\n            \"filepath\" : \"rv.py\",\n            \"suffixes\" : [\"rv\"]\n        },\n        {\n            \"OTIO_SCHEMA\" : \"Adapter.1\",\n            \"name\" : \"maya_sequencer\",\n            \"execution_scope\" : \"in process\",\n            \"filepath\" : \"maya_sequencer.py\",\n            \"suffixes\" : [\"ma\",\"mb\"]\n        },\n        {\n            \"OTIO_SCHEMA\" : \"Adapter.1\",\n            \"name\" : \"ale\",\n            \"execution_scope\" : \"in process\",\n            \"filepath\" : \"ale.py\",\n            \"suffixes\" : [\"ale\"]\n        },\n        {\n            \"OTIO_SCHEMA\" : \"Adapter.1\",\n            \"name\" : \"burnins\",\n            \"execution_scope\" : \"in process\",\n            \"filepath\" : \"burnins.py\",\n            \"suffixes\" : []\n        },\n        {\n            \"OTIO_SCHEMA\" : \"Adapter.1\",\n            \"name\" : \"AAF\",\n            \"execution_scope\" : \"in process\",\n            \"filepath\" : \"advanced_authoring_format.py\",\n            \"suffixes\" : [\"aaf\"]\n        },\n        {\n            \"OTIO_SCHEMA\": \"Adapter.1\",\n            \"name\": \"xges\",\n            \"execution_scope\": \"in process\",\n            \"filepath\": \"xges.py\",\n            \"suffixes\": [\"xges\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/extern_maya_sequencer.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\nimport os\nimport sys\n\n# deal with renaming of default library from python 2 / 3\ntry:\n    import urlparse as urllib_parse\nexcept ImportError:\n    import urllib.parse as urllib_parse\n\n# import maya and handle standalone mode\nfrom maya import cmds\n\ntry:\n    cmds.ls\nexcept AttributeError:\n    from maya import standalone\n    standalone.initialize(name='python')\n\nimport opentimelineio as otio\n\n# Mapping of Maya FPS Enum to rate.\nFPS = {\n    'game': 15,\n    'film': 24,\n    'pal': 25,\n    'ntsc': 30,\n    'show': 48,\n    'palf': 50,\n    'ntscf': 60\n}\n\n\ndef _url_to_path(url):\n    if url is None:\n        return None\n\n    return urllib_parse.urlparse(url).path\n\n\ndef _video_url_for_shot(shot):\n    current_file = os.path.normpath(cmds.file(q=True, sn=True))\n    return os.path.join(\n        os.path.dirname(current_file),\n        'playblasts',\n        '{base_name}_{shot_name}.mov'.format(\n            base_name=os.path.basename(os.path.splitext(current_file)[0]),\n            shot_name=cmds.shot(shot, q=True, shotName=True)\n        )\n    )\n\n\ndef _match_existing_shot(item, existing_shots):\n    if existing_shots is None:\n        return None\n\n    if item.media_reference.is_missing_reference:\n        return None\n\n    url_path = _url_to_path(item.media_reference.target_url)\n    return next(\n        (\n            shot for shot in existing_shots\n            if _video_url_for_shot(shot) == url_path\n        ),\n        None\n    )\n\n\n# ------------------------\n# building single track\n# ------------------------\n\ndef _build_shot(item, track_no, track_range, existing_shot=None):\n    camera = None\n    if existing_shot is None:\n        camera = cmds.camera(name=item.name.split('.')[0] + '_cam')[0]\n    cmds.shot(\n        existing_shot or item.name.split('.')[0],\n        e=existing_shot is not None,\n        shotName=item.name,\n        track=track_no,\n        currentCamera=camera,\n        startTime=item.trimmed_range().start_time.value,\n        endTime=item.trimmed_range().end_time_inclusive().value,\n        sequenceStartTime=track_range.start_time.value,\n        sequenceEndTime=track_range.end_time_inclusive().value\n    )\n\n\ndef _build_track(track, track_no, existing_shots=None):\n    for n, item in enumerate(track):\n        if not isinstance(item, otio.schema.Clip):\n            continue\n\n        track_range = track.range_of_child_at_index(n)\n        if existing_shots is not None:\n            existing_shot = _match_existing_shot(item, existing_shots)\n        else:\n            existing_shot = None\n\n        _build_shot(item, track_no, track_range, existing_shot)\n\n\ndef build_sequence(timeline, clean=False):\n    existing_shots = cmds.ls(type='shot') or []\n    if clean:\n        cmds.delete(existing_shots)\n        existing_shots = []\n\n    tracks = [\n        track for track in timeline.tracks\n        if track.kind == otio.schema.TrackKind.Video\n    ]\n\n    for track_no, track in enumerate(reversed(tracks)):\n        _build_track(track, track_no, existing_shots=existing_shots)\n\n\ndef read_from_file(path, clean=True):\n    timeline = otio.adapters.read_from_file(path)\n    build_sequence(timeline, clean=clean)\n\n\n# -----------------------\n# parsing single track\n# -----------------------\n\ndef _get_gap(duration):\n    rate = FPS.get(cmds.currentUnit(q=True, time=True), 25)\n    gap_range = otio.opentime.TimeRange(\n        duration=otio.opentime.RationalTime(duration, rate)\n    )\n    return otio.schema.Gap(source_range=gap_range)\n\n\ndef _read_shot(shot):\n    rate = FPS.get(cmds.currentUnit(q=True, time=True), 25)\n    start = int(cmds.shot(shot, q=True, startTime=True))\n    end = int(cmds.shot(shot, q=True, endTime=True)) + 1\n\n    video_reference = otio.schema.ExternalReference(\n        target_url=_video_url_for_shot(shot),\n        available_range=otio.opentime.TimeRange(\n            otio.opentime.RationalTime(value=start, rate=rate),\n            otio.opentime.RationalTime(value=end - start, rate=rate)\n        )\n    )\n\n    return otio.schema.Clip(\n        name=cmds.shot(shot, q=True, shotName=True),\n        media_reference=video_reference,\n        source_range=otio.opentime.TimeRange(\n            otio.opentime.RationalTime(value=start, rate=rate),\n            otio.opentime.RationalTime(value=end - start, rate=rate)\n        )\n    )\n\n\ndef _read_track(shots):\n    v = otio.schema.Track(kind=otio.schema.track.TrackKind.Video)\n\n    last_clip_end = 0\n    for shot in shots:\n        seq_start = int(cmds.shot(shot, q=True, sequenceStartTime=True))\n        seq_end = int(cmds.shot(shot, q=True, sequenceEndTime=True))\n\n        # add gap if necessary\n        fill_time = seq_start - last_clip_end\n        last_clip_end = seq_end + 1\n        if fill_time:\n            v.append(_get_gap(fill_time))\n\n        # add clip\n        v.append(_read_shot(shot))\n\n    return v\n\n\ndef read_sequence():\n    rate = FPS.get(cmds.currentUnit(q=True, time=True), 25)\n    shots = cmds.ls(type='shot') or []\n    per_track = {}\n\n    for shot in shots:\n        track_no = cmds.shot(shot, q=True, track=True)\n        if track_no not in per_track:\n            per_track[track_no] = []\n        per_track[track_no].append(shot)\n\n    timeline = otio.schema.Timeline()\n    timeline.global_start_time = otio.opentime.RationalTime(0, rate)\n\n    for track_no in reversed(sorted(per_track.keys())):\n        track_shots = per_track[track_no]\n        timeline.tracks.append(_read_track(track_shots))\n\n    return timeline\n\n\ndef write_to_file(path):\n    timeline = read_sequence()\n    otio.adapters.write_to_file(timeline, path)\n\n\ndef main():\n    read_write_arg = sys.argv[1]\n    filepath = sys.argv[2]\n\n    write = False\n    if read_write_arg == \"write\":\n        write = True\n\n    if write:\n        # read the input OTIO off stdin\n        input_otio = otio.adapters.read_from_string(\n            sys.stdin.read(),\n            'otio_json'\n        )\n        build_sequence(input_otio, clean=True)\n        cmds.file(rename=filepath)\n        cmds.file(save=True, type=\"mayaAscii\")\n    else:\n        cmds.file(filepath, o=True)\n        sys.stdout.write(\n            \"\\nOTIO_JSON_BEGIN\\n\" +\n            otio.adapters.write_to_string(\n                read_sequence(),\n                \"otio_json\"\n            ) +\n            \"\\nOTIO_JSON_END\\n\"\n        )\n\n    cmds.quit(force=True)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/extern_rv.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"RV External Adapter component.\n\nBecause the rv adapter requires being run from within the RV py-interp to take\nadvantage of modules inside of RV, this script gets shelled out to from the\nRV OTIO adapter.\n\nRequires that you set the environment variables:\n    OTIO_RV_PYTHON_LIB - should point at the parent directory of rvSession\n    OTIO_RV_PYTHON_BIN - should point at py-interp from within rv\n\"\"\"\n\n# python\nimport sys\nimport os\n\n# otio\nimport opentimelineio as otio\n\n# rv import\nsys.path += [os.path.join(os.environ[\"OTIO_RV_PYTHON_LIB\"], \"rvSession\")]\nimport rvSession  # noqa\n\n\ndef main():\n    \"\"\" entry point, should be called from the rv adapter in otio \"\"\"\n\n    session_file = rvSession.Session()\n\n    output_fname = sys.argv[1]\n\n    # read the input OTIO off stdin\n    input_otio = otio.adapters.read_from_string(sys.stdin.read(), 'otio_json')\n\n    result = write_otio(input_otio, session_file)\n    session_file.setViewNode(result)\n    session_file.write(output_fname)\n\n\n# exception class @{\nclass NoMappingForOtioTypeError(otio.exceptions.OTIOError):\n    pass\n# @}\n\n\ndef write_otio(otio_obj, to_session, track_kind=None):\n    WRITE_TYPE_MAP = {\n        otio.schema.Timeline: _write_timeline,\n        otio.schema.Stack: _write_stack,\n        otio.schema.Track: _write_track,\n        otio.schema.Clip: _write_item,\n        otio.schema.Gap: _write_item,\n        otio.schema.Transition: _write_transition,\n        otio.schema.SerializableCollection: _write_collection,\n    }\n\n    if type(otio_obj) in WRITE_TYPE_MAP:\n        return WRITE_TYPE_MAP[type(otio_obj)](otio_obj, to_session, track_kind)\n\n    raise NoMappingForOtioTypeError(\n        str(type(otio_obj)) + \" on object: {}\".format(otio_obj)\n    )\n\n\ndef _write_dissolve(pre_item, in_dissolve, post_item, to_session, track_kind=None):\n    rv_trx = to_session.newNode(\"CrossDissolve\", str(in_dissolve.name))\n\n    rate = pre_item.trimmed_range().duration.rate\n    rv_trx.setProperty(\n        \"CrossDissolve\",\n        \"\",\n        \"parameters\",\n        \"startFrame\",\n        rvSession.gto.FLOAT,\n        1.0\n    )\n    rv_trx.setProperty(\n        \"CrossDissolve\",\n        \"\",\n        \"parameters\",\n        \"numFrames\",\n        rvSession.gto.FLOAT,\n        int(\n            (\n                in_dissolve.in_offset\n                + in_dissolve.out_offset\n            ).rescaled_to(rate).value\n        )\n    )\n\n    rv_trx.setProperty(\n        \"CrossDissolve\",\n        \"\",\n        \"output\",\n        \"fps\",\n        rvSession.gto.FLOAT,\n        rate\n    )\n\n    pre_item_rv = write_otio(pre_item, to_session, track_kind)\n    rv_trx.addInput(pre_item_rv)\n\n    post_item_rv = write_otio(post_item, to_session, track_kind)\n\n    node_to_insert = post_item_rv\n\n    if (\n            hasattr(pre_item, \"media_reference\")\n            and pre_item.media_reference\n            and pre_item.media_reference.available_range\n            and hasattr(post_item, \"media_reference\")\n            and post_item.media_reference\n            and post_item.media_reference.available_range\n            and (\n                post_item.media_reference.available_range.start_time.rate !=\n                pre_item.media_reference.available_range.start_time.rate\n            )\n    ):\n        # write a retime to make sure post_item is in the timebase of pre_item\n        rt_node = to_session.newNode(\"Retime\", \"transition_retime\")\n        rt_node.setTargetFps(\n            pre_item.media_reference.available_range.start_time.rate\n        )\n\n        post_item_rv = write_otio(post_item, to_session, track_kind)\n\n        rt_node.addInput(post_item_rv)\n        node_to_insert = rt_node\n\n    rv_trx.addInput(node_to_insert)\n\n    return rv_trx\n\n\ndef _write_transition(\n        pre_item,\n        in_trx,\n        post_item,\n        to_session,\n        track_kind=None\n):\n    trx_map = {\n        otio.schema.TransitionTypes.SMPTE_Dissolve: _write_dissolve,\n    }\n\n    if in_trx.transition_type not in trx_map:\n        return\n\n    return trx_map[in_trx.transition_type](\n        pre_item,\n        in_trx,\n        post_item,\n        to_session,\n        track_kind\n    )\n\n\ndef _write_stack(in_stack, to_session, track_kind=None):\n    new_stack = to_session.newNode(\"Stack\", str(in_stack.name) or \"tracks\")\n\n    for seq in in_stack:\n        result = write_otio(seq, to_session, track_kind)\n        if result:\n            new_stack.addInput(result)\n\n    return new_stack\n\n\ndef _write_track(in_seq, to_session, _=None):\n    new_seq = to_session.newNode(\"Sequence\", str(in_seq.name) or \"track\")\n\n    items_to_serialize = otio.algorithms.track_with_expanded_transitions(\n        in_seq\n    )\n\n    track_kind = in_seq.kind\n\n    for thing in items_to_serialize:\n        if isinstance(thing, tuple):\n            result = _write_transition(*thing, to_session=to_session,\n                                       track_kind=track_kind)\n        elif thing.duration().value == 0:\n            continue\n        else:\n            result = write_otio(thing, to_session, track_kind)\n\n        if result:\n            new_seq.addInput(result)\n\n    return new_seq\n\n\ndef _write_timeline(tl, to_session, _=None):\n    result = write_otio(tl.tracks, to_session)\n    return result\n\n\ndef _write_collection(collection, to_session, track_kind=None):\n    results = []\n    for item in collection:\n        result = write_otio(item, to_session, track_kind)\n        if result:\n            results.append(result)\n\n    if results:\n        return results[0]\n\n\ndef _create_media_reference(item, src, track_kind=None):\n    if hasattr(item, \"media_reference\") and item.media_reference:\n        if isinstance(item.media_reference, otio.schema.ExternalReference):\n            media = [str(item.media_reference.target_url)]\n\n            if track_kind == otio.schema.TrackKind.Audio:\n                # Create blank video media to accompany audio for valid source\n                blank = \"{},start={},end={},fps={}.movieproc\".format(\n                    \"blank\",\n                    item.available_range().start_time.value,\n                    item.available_range().end_time_inclusive().value,\n                    item.available_range().duration.rate\n                )\n                # Inserting blank media here forces all content to only\n                # produce audio. We do it twice in case we look at this in\n                # stereo\n                media = [blank, blank] + media\n\n            src.setMedia(media)\n            return True\n\n        elif isinstance(item.media_reference, otio.schema.GeneratorReference):\n            if item.media_reference.generator_kind == \"SMPTEBars\":\n                kind = \"smptebars\"\n                src.setMedia(\n                    [\n                        \"{},start={},end={},fps={}.movieproc\".format(\n                            kind,\n                            item.available_range().start_time.value,\n                            item.available_range().end_time_inclusive().value,\n                            item.available_range().duration.rate\n                        )\n                    ]\n                )\n                return True\n\n    return False\n\n\ndef _write_item(it, to_session, track_kind=None):\n    src = to_session.newNode(\"Source\", str(it.name) or \"clip\")\n\n    src.setProperty(\n        \"RVSourceGroup\",\n        \"source\",\n        \"attributes\",\n        \"otio_metadata\",\n        rvSession.gto.STRING, str(it.metadata)\n    )\n\n    range_to_read = it.trimmed_range()\n\n    if not range_to_read:\n        raise otio.exceptions.OTIOError(\n            \"No valid range on clip: {0}.\".format(\n                str(it)\n            )\n        )\n\n    # because OTIO has no global concept of FPS, the rate of the duration is\n    # used as the rate for the range of the source.\n    # RationalTime.value_rescaled_to returns the time value of the object in\n    # time rate of the argument.\n    src.setCutIn(\n        range_to_read.start_time.value_rescaled_to(\n            range_to_read.duration\n        )\n    )\n    src.setCutOut(\n        range_to_read.end_time_inclusive().value_rescaled_to(\n            range_to_read.duration\n        )\n    )\n    src.setFPS(range_to_read.duration.rate)\n\n    # if the media reference is missing\n    if not _create_media_reference(it, src, track_kind):\n        kind = \"smptebars\"\n        if isinstance(it, otio.schema.Gap):\n            kind = \"blank\"\n        src.setMedia(\n            [\n                \"{},start={},end={},fps={}.movieproc\".format(\n                    kind,\n                    range_to_read.start_time.value,\n                    range_to_read.end_time_inclusive().value,\n                    range_to_read.duration.rate\n                )\n            ]\n        )\n\n    return src\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/fcpx_xml.py",
    "content": "#\n# Copyright 2018 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"OpenTimelineIO Final Cut Pro X XML Adapter. \"\"\"\nimport os\nimport subprocess\nfrom xml.etree import cElementTree\nfrom xml.dom import minidom\nfrom fractions import Fraction\nfrom datetime import date\n\ntry:\n    from urllib import unquote\nexcept ImportError:\n    from urllib.parse import unquote\n\nimport opentimelineio as otio\n\nMETA_NAMESPACE = \"fcpx_xml\"\n\nCOMPOSABLE_ELEMENTS = (\"video\", \"audio\", \"ref-clip\", \"asset-clip\")\n\nFRAMERATE_FRAMEDURATION = {23.98: \"1001/24000s\",\n                           24: \"25/600s\",\n                           25: \"1/25s\",\n                           29.97: \"1001/30000s\",\n                           30: \"100/3000s\",\n                           50: \"1/50s\",\n                           59.94: \"1001/60000s\",\n                           60: \"1/60s\"}\n\n\ndef format_name(frame_rate, path):\n    \"\"\"\n    Helper to get the formatName used in FCP X XML format elements. This\n    uses ffprobe to get the frame size of the the clip at the provided path.\n\n    Args:\n        frame_rate (int): The frame rate of the clip at the provided path\n        path (str): The path to the clip to probe\n\n    Returns:\n        str: The format name. If empty, then ffprobe couldn't find the item\n    \"\"\"\n\n    path = path.replace(\"file://\", \"\")\n    path = unquote(path)\n    if not os.path.exists(path):\n        return \"\"\n\n    try:\n        frame_size = subprocess.check_output(\n            [\n                \"ffprobe\",\n                \"-v\",\n                \"error\",\n                \"-select_streams\",\n                \"v:0\",\n                \"-show_entries\",\n                \"stream=height,width\",\n                \"-of\",\n                \"csv=s=x:p=0\",\n                path\n            ]\n        )\n    except (subprocess.CalledProcessError, OSError):\n        frame_size = \"\"\n\n    if not frame_size:\n        return \"\"\n\n    frame_size = frame_size.rstrip()\n\n    if \"1920\" in frame_size:\n        frame_size = \"1080\"\n\n    if frame_size.endswith(\"1280\"):\n        frame_size = \"720\"\n\n    return \"FFVideoFormat{}p{}\".format(frame_size, frame_rate)\n\n\ndef to_rational_time(rational_number, fps):\n    \"\"\"\n    This converts a rational number value to an otio RationalTime object\n\n    Args:\n        rational_number (str): This is a rational number from an FCP X XML\n        fps (int): The frame rate to use for calculating the rational time\n\n    Returns:\n        RationalTime: A RationalTime object\n    \"\"\"\n\n    if rational_number == \"0s\" or rational_number is None:\n        frames = 0\n    else:\n        parts = rational_number.split(\"/\")\n        if len(parts) > 1:\n            frames = int(\n                float(parts[0]) / float(parts[1].replace(\"s\", \"\")) * float(fps)\n            )\n        else:\n            frames = int(float(parts[0].replace(\"s\", \"\")) * float(fps))\n\n    return otio.opentime.RationalTime(frames, int(fps))\n\n\ndef from_rational_time(rational_time):\n    \"\"\"\n    This converts a RationalTime object to a rational number as a string\n\n    Args:\n        rational_time (RationalTime): a rational time object\n\n    Returns:\n        str: A rational number as a string\n    \"\"\"\n\n    if int(rational_time.value) == 0:\n        return \"0s\"\n    result = Fraction(\n        float(rational_time.value) / float(rational_time.rate)\n    ).limit_denominator()\n    if str(result.denominator) == \"1\":\n        return \"{}s\".format(result.numerator)\n    return \"{}/{}s\".format(result.numerator, result.denominator)\n\n\nclass FcpxOtio(object):\n    \"\"\"\n    This object is responsible for knowing how to convert an otio into an\n    FCP X XML\n    \"\"\"\n\n    def __init__(self, otio_timeline):\n        self.otio_timeline = otio_timeline\n        self.fcpx_xml = cElementTree.Element(\"fcpxml\", version=\"1.8\")\n        self.resource_element = cElementTree.SubElement(\n            self.fcpx_xml,\n            \"resources\"\n        )\n        if self.otio_timeline.schema_name() == \"Timeline\":\n            self.timelines = [self.otio_timeline]\n        else:\n            self.timelines = list(\n                self.otio_timeline.each_child(\n                    descended_from_type=otio.schema.Timeline\n                )\n            )\n\n        if len(self.timelines) > 1:\n            self.event_resource = cElementTree.SubElement(\n                self.fcpx_xml,\n                \"event\",\n                {\"name\": self._event_name()}\n            )\n        else:\n            self.event_resource = self.fcpx_xml\n\n        self.resource_count = 0\n\n    def to_xml(self):\n        \"\"\"\n        Convert an otio to an FCP X XML\n\n        Returns:\n            str: FCPX XML content\n        \"\"\"\n\n        for project in self.timelines:\n            top_sequence = self._stack_to_sequence(project.tracks)\n\n            project_element = cElementTree.Element(\n                \"project\",\n                {\n                    \"name\": project.name,\n                    \"uid\": project.metadata.get(\"fcpx\", {}).get(\"uid\", \"\")\n                }\n            )\n            project_element.append(top_sequence)\n            self.event_resource.append(project_element)\n\n        if not self.timelines:\n            for clip in self._clips():\n                if not clip.parent():\n                    self._add_asset(clip)\n\n            for stack in self._stacks():\n                ref_element = self._element_for_item(\n                    stack,\n                    None,\n                    ref_only=True,\n                    compound=True\n                )\n                self.event_resource.append(ref_element)\n        child_parent_map = {c: p for p in self.fcpx_xml.iter() for c in p}\n\n        for marker in [marker for marker in self.fcpx_xml.iter(\"marker\")]:\n            parent = child_parent_map.get(marker)\n            marker_attribs = marker.attrib.copy()\n            parent.remove(marker)\n            cElementTree.SubElement(\n                parent,\n                \"marker\",\n                marker_attribs\n            )\n\n        xml = cElementTree.tostring(\n            self.fcpx_xml,\n            encoding=\"UTF-8\",\n            method=\"xml\"\n        )\n        dom = minidom.parseString(xml)\n        pretty = dom.toprettyxml(indent=\"    \")\n        return pretty.replace(\n            '<?xml version=\"1.0\" ?>',\n            '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<!DOCTYPE fcpxml>\\n'\n        )\n\n    def _stack_to_sequence(self, stack, compound_clip=False):\n        format_element = self._find_or_create_format_from(stack)\n        sequence_element = cElementTree.Element(\n            \"sequence\",\n            {\n                \"duration\": self._calculate_rational_number(\n                    stack.duration().value,\n                    stack.duration().rate\n                ),\n                \"format\": str(format_element.get(\"id\"))\n            }\n        )\n        spine = cElementTree.SubElement(sequence_element, \"spine\")\n        video_tracks = [\n            t for t in stack\n            if t.kind == otio.schema.TrackKind.Video\n        ]\n        audio_tracks = [\n            t for t in stack\n            if t.kind == otio.schema.TrackKind.Audio\n        ]\n\n        for idx, track in enumerate(video_tracks):\n            self._track_for_spine(track, idx, spine, compound_clip)\n\n        for idx, track in enumerate(audio_tracks):\n            lane_id = -(idx + 1)\n            self._track_for_spine(track, lane_id, spine, compound_clip)\n        return sequence_element\n\n    def _track_for_spine(self, track, lane_id, spine, compound):\n        for child in self._lanable_items(track.each_child()):\n            if self._item_in_compound_clip(child) and not compound:\n                continue\n            child_element = self._element_for_item(\n                child,\n                lane_id,\n                compound=compound\n            )\n            if not lane_id:\n                spine.append(child_element)\n                continue\n            if child.schema_name() == \"Gap\":\n                continue\n\n            parent_element = self._find_parent_element(\n                spine,\n                track.trimmed_range_of_child(child).start_time,\n                self._find_or_create_format_from(track).get(\"id\")\n            )\n            offset = self._offset_based_on_parent(\n                child_element,\n                parent_element,\n                self._find_or_create_format_from(track).get(\"id\")\n            )\n            child_element.set(\n                \"offset\",\n                from_rational_time(offset)\n            )\n\n            parent_element.append(child_element)\n        return []\n\n    def _find_parent_element(self, spine, trimmed_range, format_id):\n        for item in spine.iter():\n            if item.tag not in (\"clip\", \"asset-clip\", \"gap\", \"ref-clip\"):\n                continue\n            if item.get(\"lane\") is not None:\n                continue\n            if item.tag == \"gap\" and item.find(\"./audio\") is not None:\n                continue\n            offset = to_rational_time(\n                item.get(\"offset\"),\n                self._frame_rate_from_element(item, format_id)\n            )\n            duration = to_rational_time(\n                item.get(\"duration\"),\n                self._frame_rate_from_element(item, format_id)\n            )\n            total_time = offset + duration\n            if offset > trimmed_range:\n                continue\n            if total_time > trimmed_range:\n                return item\n        return None\n\n    def _offset_based_on_parent(self, child, parent, default_format_id):\n        parent_offset = to_rational_time(\n            parent.get(\"offset\"),\n            self._frame_rate_from_element(parent, default_format_id)\n        )\n        child_offset = to_rational_time(\n            child.get(\"offset\"),\n            self._frame_rate_from_element(child, default_format_id)\n        )\n\n        parent_start = to_rational_time(\n            parent.get(\"start\"),\n            self._frame_rate_from_element(parent, default_format_id)\n        )\n        return (child_offset - parent_offset) + parent_start\n\n    def _frame_rate_from_element(self, element, default_format_id):\n        if element.tag == \"gap\":\n            format_id = default_format_id\n\n        if element.tag == \"ref-clip\":\n            media_element = self._media_by_id(element.get(\"ref\"))\n            asset = media_element.find(\"./sequence\")\n            format_id = asset.get(\"format\")\n\n        if element.tag == \"clip\":\n            if element.find(\"./gap\") is not None:\n                asset_id = element.find(\"./gap\").find(\"./audio\").get(\"ref\")\n            else:\n                asset_id = element.find(\"./video\").get(\"ref\")\n            asset = self._asset_by_id(asset_id)\n            format_id = asset.get(\"format\")\n\n        if element.tag == \"asset-clip\":\n            asset = self._asset_by_id(element.get(\"ref\"))\n            format_id = asset.get(\"format\")\n\n        format_element = self.resource_element.find(\n            \"./format[@id='{}']\".format(format_id)\n        )\n        total, rate = format_element.get(\"frameDuration\").split(\"/\")\n        rate = rate.replace(\"s\", \"\")\n        return int(float(rate) / float(total))\n\n    def _element_for_item(self, item, lane, ref_only=False, compound=False):\n        element = None\n        duration = self._calculate_rational_number(\n            item.duration().value,\n            item.duration().rate\n        )\n        if item.schema_name() == \"Clip\":\n            asset_id = self._add_asset(item, compound_only=compound)\n            element = self._element_for_clip(item, asset_id, duration, lane)\n\n        if item.schema_name() == \"Gap\":\n            element = self._element_for_gap(item, duration)\n\n        if item.schema_name() == \"Stack\":\n            element = self._element_for_stack(item, duration, ref_only)\n\n        if element is None:\n            return None\n        if lane:\n            element.set(\"lane\", str(lane))\n        for marker in item.markers:\n            marker_attribs = {\n                \"start\": from_rational_time(marker.marked_range.start_time),\n                \"duration\": from_rational_time(marker.marked_range.duration),\n                \"value\": marker.name\n            }\n            marker_element = cElementTree.Element(\n                \"marker\",\n                marker_attribs\n            )\n            if marker.color == otio.schema.MarkerColor.RED:\n                marker_element.set(\"completed\", \"0\")\n            if marker.color == otio.schema.MarkerColor.GREEN:\n                marker_element.set(\"completed\", \"1\")\n            element.append(marker_element)\n        return element\n\n    def _lanable_items(self, items):\n        return [\n            item for item in items\n            if item.schema_name() in [\"Gap\", \"Stack\", \"Clip\"]\n        ]\n\n    def _element_for_clip(self, item, asset_id, duration, lane):\n        element = cElementTree.Element(\n            \"clip\",\n            {\n                \"name\": item.name,\n                \"offset\": from_rational_time(\n                    item.trimmed_range_in_parent().start_time\n                ),\n                \"duration\": duration\n            }\n        )\n        start = from_rational_time(item.source_range.start_time)\n        if start != \"0s\":\n            element.set(\"start\", str(start))\n        if item.parent().kind == otio.schema.TrackKind.Video:\n            cElementTree.SubElement(\n                element,\n                \"video\",\n                {\n                    \"offset\": \"0s\",\n                    \"ref\": asset_id,\n                    \"duration\": self._find_asset_duration(item)\n                }\n            )\n        else:\n            gap_element = cElementTree.SubElement(\n                element,\n                \"gap\",\n                {\n                    \"name\": \"Gap\",\n                    \"offset\": \"0s\",\n                    \"duration\": self._find_asset_duration(item)\n                }\n            )\n            audio = cElementTree.SubElement(\n                gap_element,\n                \"audio\",\n                {\n                    \"offset\": \"0s\",\n                    \"ref\": asset_id,\n                    \"duration\": self._find_asset_duration(item)\n                }\n            )\n            if lane:\n                audio.set(\"lane\", str(lane))\n        return element\n\n    def _element_for_gap(self, item, duration):\n        element = cElementTree.Element(\n            \"gap\",\n            {\n                \"name\": \"Gap\",\n                \"duration\": duration,\n                \"offset\": from_rational_time(\n                    item.trimmed_range_in_parent().start_time\n                ),\n                \"start\": \"3600s\"\n            }\n        )\n        return element\n\n    def _element_for_stack(self, item, duration, ref_only):\n        media_element = self._add_compound_clip(item)\n        asset_id = media_element.get(\"id\")\n        element = cElementTree.Element(\n            \"ref-clip\",\n            {\n                \"name\": item.name,\n                \"duration\": duration,\n                \"ref\": str(asset_id)\n            }\n        )\n        if not ref_only:\n            element.set(\n                \"offset\",\n                from_rational_time(\n                    item.trimmed_range_in_parent().start_time\n                )\n            )\n            element.set(\n                \"start\",\n                from_rational_time(item.source_range.start_time)\n            )\n        if item.parent() and item.parent().kind == otio.schema.TrackKind.Audio:\n            element.set(\"srcEnable\", \"audio\")\n        return element\n\n    def _find_asset_duration(self, item):\n        if (item.media_reference and\n                not item.media_reference.is_missing_reference):\n            return self._calculate_rational_number(\n                item.media_reference.available_range.duration.value,\n                item.media_reference.available_range.duration.rate\n            )\n        return self._calculate_rational_number(\n            item.duration().value,\n            item.duration().rate\n        )\n\n    def _find_asset_start(self, item):\n        if (item.media_reference and\n                not item.media_reference.is_missing_reference):\n            return self._calculate_rational_number(\n                item.media_reference.available_range.start_time.value,\n                item.media_reference.available_range.start_time.rate\n            )\n        return self._calculate_rational_number(\n            item.source_range.start_time.value,\n            item.source_range.start_time.rate\n        )\n\n    def _clip_format_name(self, clip):\n        if clip.schema_name() in (\"Stack\", \"Track\"):\n            return \"\"\n        if not clip.media_reference:\n            return \"\"\n\n        if clip.media_reference.is_missing_reference:\n            return \"\"\n\n        return format_name(\n            clip.duration().rate,\n            clip.media_reference.target_url\n        )\n\n    def _find_or_create_format_from(self, clip):\n        frame_duration = self._framerate_to_frame_duration(\n            clip.duration().rate\n        )\n        format_element = self._format_by_frame_rate(clip.duration().rate)\n        if format_element is None:\n            format_element = cElementTree.SubElement(\n                self.resource_element,\n                \"format\",\n                {\n                    \"id\": self._resource_id_generator(),\n                    \"frameDuration\": frame_duration,\n                    \"name\": self._clip_format_name(clip)\n                }\n            )\n        if format_element.get(\"name\", \"\") == \"\":\n            format_element.set(\"name\", self._clip_format_name(clip))\n        return format_element\n\n    def _add_asset(self, clip, compound_only=False):\n        format_element = self._find_or_create_format_from(clip)\n        asset = self._create_asset_element(clip, format_element)\n\n        if not compound_only and not self._asset_clip_by_name(clip.name):\n            self._create_asset_clip_element(\n                clip,\n                format_element,\n                asset.get(\"id\")\n            )\n\n        if not clip.parent():\n            asset.set(\"hasAudio\", \"1\")\n            asset.set(\"hasVideo\", \"1\")\n            return asset.get(\"id\")\n        if clip.parent().kind == otio.schema.TrackKind.Audio:\n            asset.set(\"hasAudio\", \"1\")\n        if clip.parent().kind == otio.schema.TrackKind.Video:\n            asset.set(\"hasVideo\", \"1\")\n        return asset.get(\"id\")\n\n    def _create_asset_clip_element(self, clip, format_element, resource_id):\n        duration = self._find_asset_duration(clip)\n        a_clip = cElementTree.SubElement(\n            self.event_resource,\n            \"asset-clip\",\n            {\n                \"name\": clip.name,\n                \"format\": format_element.get(\"id\"),\n                \"ref\": resource_id,\n                \"duration\": duration\n            }\n        )\n        if clip.media_reference and not clip.media_reference.is_missing_reference:\n            fcpx_metadata = clip.media_reference.metadata.get(\"fcpx\", {})\n            note_element = self._create_note_element(\n                fcpx_metadata.get(\"note\", None)\n            )\n            keyword_elements = self._create_keyword_elements(\n                fcpx_metadata.get(\"keywords\", [])\n            )\n            metadata_element = self._create_metadata_elements(\n                fcpx_metadata.get(\"metadata\", None)\n            )\n\n            if note_element is not None:\n                a_clip.append(note_element)\n            if keyword_elements:\n                for keyword_element in keyword_elements:\n                    a_clip.append(keyword_element)\n            if metadata_element is not None:\n                a_clip.append(metadata_element)\n\n    def _create_asset_element(self, clip, format_element):\n        target_url = self._target_url_from_clip(clip)\n        asset = self._asset_by_path(target_url)\n        if asset is not None:\n            return asset\n\n        asset = cElementTree.SubElement(\n            self.resource_element,\n            \"asset\",\n            {\n                \"name\": clip.name,\n                \"src\": target_url,\n                \"format\": format_element.get(\"id\"),\n                \"id\": self._resource_id_generator(),\n                \"duration\": self._find_asset_duration(clip),\n                \"start\": self._find_asset_start(clip),\n                \"hasAudio\": \"0\",\n                \"hasVideo\": \"0\"\n            }\n        )\n        return asset\n\n    def _add_compound_clip(self, item):\n        media_element = self._media_by_name(item.name)\n        if media_element is not None:\n            return media_element\n        resource_id = self._resource_id_generator()\n        media_element = cElementTree.SubElement(\n            self.resource_element,\n            \"media\",\n            {\n                \"name\": self._compound_clip_name(item, resource_id),\n                \"id\": resource_id\n            }\n        )\n        if item.metadata.get(\"fcpx\", {}).get(\"uid\", False):\n            media_element.set(\"uid\", item.metadata.get(\"fcpx\", {}).get(\"uid\"))\n        media_element.append(self._stack_to_sequence(item, compound_clip=True))\n        return media_element\n\n    def _stacks(self):\n        return self.otio_timeline.each_child(\n            descended_from_type=otio.schema.Stack\n        )\n\n    def _clips(self):\n        return self.otio_timeline.each_child(\n            descended_from_type=otio.schema.Clip\n        )\n\n    def _resource_id_generator(self):\n        self.resource_count += 1\n        return \"r{}\".format(self.resource_count)\n\n    def _event_name(self):\n        if self.otio_timeline.name:\n            return self.otio_timeline.name\n        return date.strftime(date.today(), \"%m-%e-%y\")\n\n    def _asset_by_path(self, path):\n        return self.resource_element.find(\"./asset[@src='{}']\".format(path))\n\n    def _asset_by_id(self, asset_id):\n        return self.resource_element.find(\"./asset[@id='{}']\".format(asset_id))\n\n    def _media_by_name(self, name):\n        return self.resource_element.find(\"./media[@name='{}']\".format(name))\n\n    def _media_by_id(self, media_id):\n        return self.resource_element.find(\"./media[@id='{}']\".format(media_id))\n\n    def _format_by_frame_rate(self, frame_rate):\n        frame_duration = self._framerate_to_frame_duration(frame_rate)\n        return self.resource_element.find(\n            \"./format[@frameDuration='{}']\".format(frame_duration)\n        )\n\n    def _asset_clip_by_name(self, name):\n        return self.event_resource.find(\n            \"./asset-clip[@name='{}']\".format(name)\n        )\n\n    # --------------------\n    # static methods\n    # --------------------\n\n    @staticmethod\n    def _framerate_to_frame_duration(framerate):\n        frame_duration = FRAMERATE_FRAMEDURATION.get(int(framerate), \"\")\n        if not frame_duration:\n            frame_duration = FRAMERATE_FRAMEDURATION.get(float(framerate), \"\")\n        return frame_duration\n\n    @staticmethod\n    def _target_url_from_clip(clip):\n        if (clip.media_reference and\n                not clip.media_reference.is_missing_reference):\n            return clip.media_reference.target_url\n        return \"file:///tmp/{}\".format(clip.name)\n\n    @staticmethod\n    def _calculate_rational_number(duration, rate):\n        if int(duration) == 0:\n            return \"0s\"\n        result = Fraction(float(duration) / float(rate)).limit_denominator()\n        return \"{}/{}s\".format(result.numerator, result.denominator)\n\n    @staticmethod\n    def _compound_clip_name(compound_clip, resource_id):\n        if compound_clip.name:\n            return compound_clip.name\n        return \"compound_clip_{}\".format(resource_id)\n\n    @staticmethod\n    def _item_in_compound_clip(item):\n        stack_count = 0\n        parent = item.parent()\n        while parent is not None:\n            if parent.schema_name() == \"Stack\":\n                stack_count += 1\n            parent = parent.parent()\n        return stack_count > 1\n\n    @staticmethod\n    def _create_metadata_elements(metadata):\n        if metadata is None:\n            return None\n        metadata_element = cElementTree.Element(\n            \"metadata\"\n        )\n        for metadata_dict in metadata:\n            cElementTree.SubElement(\n                metadata_element,\n                \"md\",\n                {\n                    \"key\": list(metadata_dict.keys())[0],\n                    \"value\": list(metadata_dict.values())[0]\n                }\n            )\n        return metadata_element\n\n    @staticmethod\n    def _create_keyword_elements(keywords):\n        keyword_elements = []\n        for keyword_dict in keywords:\n            keyword_elements.append(\n                cElementTree.Element(\n                    \"keyword\",\n                    keyword_dict\n                )\n            )\n        return keyword_elements\n\n    @staticmethod\n    def _create_note_element(note):\n        if not note:\n            return None\n        note_element = cElementTree.Element(\n            \"note\"\n        )\n        note_element.text = note\n        return note_element\n\n\nclass FcpxXml(object):\n    \"\"\"\n    This object is responsible for knowing how to convert an FCP X XML\n    otio into an otio timeline\n    \"\"\"\n\n    def __init__(self, xml_string):\n        self.fcpx_xml = cElementTree.fromstring(xml_string)\n        self.child_parent_map = {c: p for p in self.fcpx_xml.iter() for c in p}\n\n    def to_otio(self):\n        \"\"\"\n        Convert an FCP X XML to an otio\n\n        Returns:\n            OpenTimeline: An OpenTimeline Timeline object\n        \"\"\"\n\n        if self.fcpx_xml.find(\"./library\") is not None:\n            return self._from_library()\n        if self.fcpx_xml.find(\"./event\") is not None:\n            return self._from_event(self.fcpx_xml.find(\"./event\"))\n        if self.fcpx_xml.find(\"./project\") is not None:\n            return self._from_project(self.fcpx_xml.find(\"./project\"))\n        if ((self.fcpx_xml.find(\"./asset-clip\") is not None) or\n                (self.fcpx_xml.find(\"./ref-clip\") is not None)):\n            return self._from_clips()\n\n    def _from_library(self):\n        # We are just grabbing the first even in the project for now\n        return self._from_event(self.fcpx_xml.find(\"./library/event\"))\n\n    def _from_event(self, event_element):\n        container = otio.schema.SerializableCollection(\n            name=event_element.get(\"name\")\n        )\n        for project in event_element.findall(\"./project\"):\n            container.append(self._from_project(project))\n        return container\n\n    def _from_project(self, project_element):\n        timeline = otio.schema.Timeline(name=project_element.get(\"name\", \"\"))\n        timeline.tracks = self._squence_to_stack(\n            project_element.find(\"./sequence\", {})\n        )\n        return timeline\n\n    def _from_clips(self):\n        container = otio.schema.SerializableCollection()\n        if self.fcpx_xml.find(\"./asset-clip\") is not None:\n            for asset_clip in self.fcpx_xml.findall(\"./asset-clip\"):\n                container.append(\n                    self._build_composable(\n                        asset_clip,\n                        asset_clip.get(\"format\")\n                    )\n                )\n\n        if self.fcpx_xml.find(\"./ref-clip\") is not None:\n            for ref_clip in self.fcpx_xml.findall(\"./ref-clip\"):\n                container.append(\n                    self._build_composable(\n                        ref_clip,\n                        \"r1\"\n                    )\n                )\n        return container\n\n    def _squence_to_stack(self, sequence_element, name=\"\", source_range=None):\n        timeline_items = []\n        lanes = []\n        stack = otio.schema.Stack(name=name, source_range=source_range)\n        for element in sequence_element.iter():\n            if element.tag not in COMPOSABLE_ELEMENTS:\n                continue\n            composable = self._build_composable(\n                element,\n                sequence_element.get(\"format\")\n            )\n\n            offset, lane = self._offset_and_lane(\n                element,\n                sequence_element.get(\"format\")\n            )\n\n            timeline_items.append(\n                {\n                    \"track\": lane,\n                    \"offset\": offset,\n                    \"composable\": composable,\n                    \"audio_only\": self._audio_only(element)\n                }\n            )\n\n            lanes.append(lane)\n        sorted_lanes = list(set(lanes))\n        sorted_lanes.sort()\n        for lane in sorted_lanes:\n            sorted_items = self._sorted_items(lane, timeline_items)\n            track = otio.schema.Track(\n                name=lane,\n                kind=self._track_type(sorted_items)\n            )\n\n            for item in sorted_items:\n                frame_diff = (\n                    int(item[\"offset\"].value) - track.duration().value\n                )\n                if frame_diff > 0:\n                    track.append(\n                        self._create_gap(\n                            0,\n                            frame_diff,\n                            sequence_element.get(\"format\")\n                        )\n                    )\n                track.append(item[\"composable\"])\n            stack.append(track)\n        return stack\n\n    def _build_composable(self, element, default_format):\n        timing_clip = self._timing_clip(element)\n        source_range = self._time_range(\n            timing_clip,\n            self._format_id_for_clip(element, default_format)\n        )\n\n        if element.tag != \"ref-clip\":\n            otio_composable = otio.schema.Clip(\n                name=timing_clip.get(\"name\"),\n                media_reference=self._reference_from_id(\n                    element.get(\"ref\"),\n                    default_format\n                ),\n                source_range=source_range\n            )\n        else:\n            media_element = self._compound_clip_by_id(element.get(\"ref\"))\n            otio_composable = self._squence_to_stack(\n                media_element.find(\"./sequence\"),\n                name=media_element.get(\"name\"),\n                source_range=source_range\n            )\n\n        for marker in timing_clip.findall(\".//marker\"):\n            otio_composable.markers.append(\n                self._marker(marker, default_format)\n            )\n\n        return otio_composable\n\n    def _marker(self, element, default_format):\n        if element.get(\"completed\", None) and element.get(\"completed\") == \"1\":\n            color = otio.schema.MarkerColor.GREEN\n        if element.get(\"completed\", None) and element.get(\"completed\") == \"0\":\n            color = otio.schema.MarkerColor.RED\n        if not element.get(\"completed\", None):\n            color = otio.schema.MarkerColor.PURPLE\n\n        otio_marker = otio.schema.Marker(\n            name=element.get(\"value\", \"\"),\n            marked_range=self._time_range(element, default_format),\n            color=color\n        )\n        return otio_marker\n\n    def _audio_only(self, element):\n        if element.tag == \"audio\":\n            return True\n        if element.tag == \"asset-clip\":\n            asset = self._asset_by_id(element.get(\"ref\", None))\n            if asset and asset.get(\"hasVideo\", \"0\") == \"0\":\n                return True\n        if element.tag == \"ref-clip\":\n            if element.get(\"srcEnable\", \"video\") == \"audio\":\n                return True\n        return False\n\n    def _create_gap(self, start_frame, number_of_frames, defualt_format):\n        fps = self._format_frame_rate(defualt_format)\n        source_range = otio.opentime.TimeRange(\n            start_time=otio.opentime.RationalTime(start_frame, fps),\n            duration=otio.opentime.RationalTime(number_of_frames, fps)\n        )\n        return otio.schema.Gap(source_range=source_range)\n\n    def _timing_clip(self, clip):\n        while clip.tag not in (\"clip\", \"asset-clip\", \"ref-clip\"):\n            clip = self.child_parent_map.get(clip)\n        return clip\n\n    def _offset_and_lane(self, clip, default_format):\n        clip_format_id = self._format_id_for_clip(clip, default_format)\n        clip = self._timing_clip(clip)\n        parent = self.child_parent_map.get(clip)\n\n        parent_format_id = self._format_id_for_clip(parent, default_format)\n\n        if parent.tag == \"spine\" and parent.get(\"lane\", None):\n            lane = parent.get(\"lane\")\n            parent = self.child_parent_map.get(parent)\n            spine = True\n        else:\n            lane = clip.get(\"lane\", \"0\")\n            spine = False\n\n        clip_offset_frames = self._number_of_frames(\n            clip.get(\"offset\"),\n            clip_format_id\n        )\n\n        if spine:\n            parent_start_frames = 0\n        else:\n            parent_start_frames = self._number_of_frames(\n                parent.get(\"start\", None),\n                parent_format_id\n            )\n\n        parent_offset_frames = self._number_of_frames(\n            parent.get(\"offset\", None),\n            parent_format_id\n        )\n\n        clip_offset_frames = (\n            (int(clip_offset_frames) - int(parent_start_frames)) +\n            int(parent_offset_frames)\n        )\n\n        offset = otio.opentime.RationalTime(\n            clip_offset_frames,\n            self._format_frame_rate(clip_format_id)\n        )\n\n        return offset, lane\n\n    def _format_id_for_clip(self, clip, default_format):\n        if not clip.get(\"ref\", None) or clip.tag == \"gap\":\n            return default_format\n\n        resource = self._asset_by_id(clip.get(\"ref\"))\n\n        if resource is None:\n            resource = self._compound_clip_by_id(\n                clip.get(\"ref\")\n            ).find(\"sequence\")\n\n        return resource.get(\"format\", default_format)\n\n    def _reference_from_id(self, asset_id, default_format):\n        asset = self._asset_by_id(asset_id)\n        if not asset.get(\"src\", \"\"):\n            return otio.schema.MissingReference()\n\n        available_range = otio.opentime.TimeRange(\n            start_time=to_rational_time(\n                asset.get(\"start\"),\n                self._format_frame_rate(\n                    asset.get(\"format\", default_format)\n                )\n            ),\n            duration=to_rational_time(\n                asset.get(\"duration\"),\n                self._format_frame_rate(\n                    asset.get(\"format\", default_format)\n                )\n            )\n        )\n        asset_clip = self._assetclip_by_ref(asset_id)\n        metadata = {}\n        if asset_clip:\n            metadata = self._create_metadta(asset_clip)\n        return otio.schema.ExternalReference(\n            target_url=asset.get(\"src\"),\n            available_range=available_range,\n            metadata={\"fcpx\": metadata}\n        )\n\n    def _create_metadta(self, item):\n        metadata = {}\n        for element in item.iter():\n            if element.tag == \"md\":\n                metadata.setdefault(\"metadata\", []).append(\n                    {element.attrib.get(\"key\"): element.attrib.get(\"value\")}\n                )\n                # metadata.update(\n                #     {element.attrib.get(\"key\"): element.attrib.get(\"value\")}\n                # )\n            if element.tag == \"note\":\n                metadata.update({\"note\": element.text})\n            if element.tag == \"keyword\":\n                metadata.setdefault(\"keywords\", []).append(element.attrib)\n        return metadata\n\n    # --------------------\n    # time helpers\n    # --------------------\n    def _format_frame_duration(self, format_id):\n        media_format = self._format_by_id(format_id)\n        total, rate = media_format.get(\"frameDuration\").split(\"/\")\n        rate = rate.replace(\"s\", \"\")\n        return total, rate\n\n    def _format_frame_rate(self, format_id):\n        fd_total, fd_rate = self._format_frame_duration(format_id)\n        return int(float(fd_rate) / float(fd_total))\n\n    def _number_of_frames(self, time_value, format_id):\n        if time_value == \"0s\" or time_value is None:\n            return 0\n        fd_total, fd_rate = self._format_frame_duration(format_id)\n        time_value = time_value.split(\"/\")\n\n        if len(time_value) > 1:\n            time_value_a, time_value_b = time_value\n            return int(\n                (float(time_value_a) / float(time_value_b.replace(\"s\", \"\"))) *\n                (float(fd_rate) / float(fd_total))\n            )\n\n        return int(\n            int(time_value[0].replace(\"s\", \"\")) *\n            (float(fd_rate) / float(fd_total))\n        )\n\n    def _time_range(self, element, format_id):\n        return otio.opentime.TimeRange(\n            start_time=to_rational_time(\n                element.get(\"start\", \"0s\"),\n                self._format_frame_rate(format_id)\n            ),\n            duration=to_rational_time(\n                element.get(\"duration\"),\n                self._format_frame_rate(format_id)\n            )\n        )\n    # --------------------\n    # search helpers\n    # --------------------\n\n    def _asset_by_id(self, asset_id):\n        return self.fcpx_xml.find(\n            \"./resources/asset[@id='{}']\".format(asset_id)\n        )\n\n    def _assetclip_by_ref(self, asset_id):\n        event = self.fcpx_xml.find(\"./event\")\n        if event is None:\n            return self.fcpx_xml.find(\"./asset-clip[@ref='{}']\".format(asset_id))\n        else:\n            return event.find(\"./asset-clip[@ref='{}']\".format(asset_id))\n\n    def _format_by_id(self, format_id):\n        return self.fcpx_xml.find(\n            \"./resources/format[@id='{}']\".format(format_id)\n        )\n\n    def _compound_clip_by_id(self, compound_id):\n        return self.fcpx_xml.find(\n            \"./resources/media[@id='{}']\".format(compound_id)\n        )\n\n    # --------------------\n    # static methods\n    # --------------------\n    @staticmethod\n    def _track_type(lane_items):\n        audio_only_items = [l for l in lane_items if l[\"audio_only\"]]\n        if len(audio_only_items) == len(lane_items):\n            return otio.schema.TrackKind.Audio\n        return otio.schema.TrackKind.Video\n\n    @staticmethod\n    def _sorted_items(lane, otio_objects):\n        lane_items = [item for item in otio_objects if item[\"track\"] == lane]\n        return sorted(lane_items, key=lambda k: k[\"offset\"])\n\n\n# --------------------\n# adapter requirements\n# --------------------\ndef read_from_string(input_str):\n    \"\"\"\n    Necessary read method for otio adapter\n\n    Args:\n        input_str (str): An FCP X XML string\n\n    Returns:\n        OpenTimeline: An OpenTimeline object\n    \"\"\"\n\n    return FcpxXml(input_str).to_otio()\n\n\ndef write_to_string(input_otio):\n    \"\"\"\n    Necessary write method for otio adapter\n\n    Args:\n        input_otio (OpenTimeline): An OpenTimeline object\n\n    Returns:\n        str: The string contents of an FCP X XML\n    \"\"\"\n\n    return FcpxOtio(input_otio).to_xml()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/ffmpeg_burnins.py",
    "content": "# MIT License\n#\n# Copyright (c) 2017 Ed Caspersen\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# allcopies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\"\"\"\nThis module provides an interface to allow users to easily\nbuild out an FFMPEG command with all the correct filters\nfor applying text (with a background) to the rendered media.\n\"\"\"\nimport os\nimport sys\nimport json\nfrom subprocess import Popen, PIPE\nfrom PIL import ImageFont\n\n\ndef _is_windows():\n    \"\"\"\n    queries if the current operating system is Windows\n\n    :rtype: bool\n    \"\"\"\n    return sys.platform.startswith('win') or \\\n        sys.platform.startswith('cygwin')\n\n\ndef _system_font():\n    \"\"\"\n    attempts to determine a default system font\n\n    :rtype: str\n    \"\"\"\n    if _is_windows():\n        font_path = os.path.join(os.environ['WINDIR'], 'Fonts')\n        fonts = ('arial.ttf', 'calibri.ttf', 'times.ttf')\n    elif sys.platform.startswith('darwin'):\n        font_path = '/System/Library/Fonts'\n        fonts = ('Menlo.ttc',)\n    else:\n        # assuming linux\n        font_path = 'usr/share/fonts/msttcorefonts'\n        fonts = ('arial.ttf', 'times.ttf', 'couri.ttf')\n\n    system_font = None\n    backup = None\n    for font in fonts:\n        font = os.path.join(font_path, font)\n        if os.path.exists(font):\n            system_font = font\n            break\n    else:\n        if os.path.exists(font_path):\n            for each in os.listdir(font_path):\n                ext = os.path.splitext(each)[-1]\n                if ext[1:].startswith('tt'):\n                    system_font = os.path.join(font_path, each)\n    return system_font or backup\n\n\n# Default valuues\nFONT = _system_font()\nFONT_SIZE = 16\nFONT_COLOR = 'white'\nBG_COLOR = 'black'\nBG_PADDING = 5\n\n# FFMPEG command strings\nFFMPEG = ('ffmpeg -loglevel panic -i %(input)s '\n          '%(filters)s %(args)s%(output)s')\nFFPROBE = ('ffprobe -v quiet -print_format json -show_format '\n           '-show_streams %(source)s')\nBOX = 'box=1:boxborderw=%(border)d:boxcolor=%(color)s@%(opacity).1f'\nDRAWTEXT = (\"drawtext=text='%(text)s':x=%(x)s:y=%(y)s:fontcolor=\"\n            \"%(color)s@%(opacity).1f:fontsize=%(size)d:fontfile='%(font)s'\")\nTIMECODE = (\"drawtext=timecode='%(text)s':timecode_rate=%(fps).2f\"\n            \":x=%(x)s:y=%(y)s:fontcolor=\"\n            \"%(color)s@%(opacity).1f:fontsize=%(size)d:fontfile='%(font)s'\")\n\n\n# Valid aligment parameters.\nTOP_CENTERED = 'top_centered'\nBOTTOM_CENTERED = 'bottom_centered'\nTOP_LEFT = 'top_left'\nBOTTOM_LEFT = 'bottom_left'\nTOP_RIGHT = 'top_right'\nBOTTOM_RIGHT = 'bottom_right'\n\n\nclass Options(dict):\n    \"\"\"\n    Base options class.\n    \"\"\"\n    _params = {\n        'opacity': 1,\n        'x_offset': 0,\n        'y_offset': 0,\n        'font': FONT,\n        'font_size': FONT_SIZE,\n        'bg_color': BG_COLOR,\n        'bg_padding': BG_PADDING,\n        'font_color': FONT_COLOR\n    }\n\n    def __init__(self, **kwargs):\n        super(Options, self).__init__()\n        params = self._params.copy()\n        params.update(kwargs)\n        super(Options, self).update(**params)\n\n    def __setitem__(self, key, value):\n        if key not in self._params:\n            raise KeyError(\"Not a valid option key '%s'\" % key)\n        super(Options, self).update({key: value})\n\n\nclass FrameNumberOptions(Options):\n    \"\"\"\n    :key int frame_offset: offset the frame numbers\n    :key float opacity: opacity value (0-1)\n    :key str expression: expression that would be used instead of text\n    :key bool x_offset: X position offset\n                         (does not apply to centered alignments)\n    :key bool y_offset: Y position offset\n    :key str font: path to the font file\n    :key int font_size: size to render the font in\n    :key str bg_color: background color of the box\n    :key int bg_padding: padding between the font and box\n    :key str font_color: color to render\n    \"\"\"\n\n    def __init__(self, **kwargs):\n        self._params.update({\n            'frame_offset': 0,\n            'expression': None\n        })\n        super(FrameNumberOptions, self).__init__(**kwargs)\n\n\nclass TextOptions(Options):\n    \"\"\"\n    :key float opacity: opacity value (0-1)\n    :key str expression: expression that would be used instead of text\n    :key bool x_offset: X position offset\n                        (does not apply to centered alignments)\n    :key bool y_offset: Y position offset\n    :key str font: path to the font file\n    :key int font_size: size to render the font in\n    :key str bg_color: background color of the box\n    :key int bg_padding: padding between the font and box\n    :key str font_color: color to render\n    \"\"\"\n\n\nclass TimeCodeOptions(Options):\n    \"\"\"\n    :key int frame_offset: offset the frame numbers\n    :key float fps: frame rate to calculate the timecode by\n    :key float opacity: opacity value (0-1)\n    :key bool x_offset: X position offset\n                         (does not apply to centered alignments)\n    :key bool y_offset: Y position offset\n    :key str font: path to the font file\n    :key int font_size: size to render the font in\n    :key str bg_color: background color of the box\n    :key int bg_padding: padding between the font and box\n    :key str font_color: color to render\n    \"\"\"\n\n    def __init__(self, **kwargs):\n        self._params.update({\n            'frame_offset': 0,\n            'fps': 24\n        })\n        super(TimeCodeOptions, self).__init__(**kwargs)\n\n\nclass Burnins(object):\n    \"\"\"\n    Class that provides convenience API for building filter\n    flags for the FFMPEG command.\n    \"\"\"\n\n    def __init__(self, source, streams=None):\n        \"\"\"\n        :param str source: source media file\n        :param [] streams: ffprobe stream data if parsed as a pre-process\n        \"\"\"\n        self.source = source\n        self.filters = {\n            'drawtext': []\n        }\n        self._streams = streams or _streams(self.source)\n\n    def __repr__(self):\n        return '<Overlayout - %s>' % os.path.basename(self.source)\n\n    @property\n    def start_frame(self):\n        \"\"\"\n        :rtype: int\n        \"\"\"\n        start_time = float(self._video_stream['start_time'])\n        return round(start_time * self.frame_rate)\n\n    @property\n    def end_frame(self):\n        \"\"\"\n        :rtype: int\n        \"\"\"\n        end_time = float(self._video_stream['duration'])\n        return round(end_time * self.frame_rate)\n\n    @property\n    def frame_rate(self):\n        \"\"\"\n        :rtype: int\n        \"\"\"\n        data = self._video_stream\n        tokens = data['r_frame_rate'].split('/')\n        return int(tokens[0]) / int(tokens[1])\n\n    @property\n    def _video_stream(self):\n        video_stream = None\n        for each in self._streams:\n            if each.get('codec_type') == 'video':\n                video_stream = each\n                break\n        else:\n            raise RuntimeError(\"Failed to locate video stream \"\n                               \"from '%s'\" % self.source)\n        return video_stream\n\n    @property\n    def resolution(self):\n        \"\"\"\n        :rtype: (int, int)\n        \"\"\"\n        data = self._video_stream\n        return data['width'], data['height']\n\n    @property\n    def filter_string(self):\n        \"\"\"\n        Generates the filter string that would be applied\n        to the `-vf` argument\n\n        :rtype: str\n        \"\"\"\n        return ','.join(self.filters['drawtext'])\n\n    def add_timecode(self, align, options=None):\n        \"\"\"\n        Convenience method to create the frame number expression.\n\n        :param enum align: alignment, must use provided enum flags\n        :param dict options: recommended to use TimeCodeOptions\n        \"\"\"\n        options = options or TimeCodeOptions()\n        timecode = _frames_to_timecode(options['frame_offset'],\n                                       self.frame_rate)\n        options = options.copy()\n        if not options.get('fps'):\n            options['fps'] = self.frame_rate\n        self._add_burnin(timecode.replace(':', r'\\:'),\n                         align,\n                         options,\n                         TIMECODE)\n\n    def add_frame_numbers(self, align, options=None):\n        \"\"\"\n        Convenience method to create the frame number expression.\n\n        :param enum align: alignment, must use provided enum flags\n        :param dict options: recommended to use FrameNumberOptions\n        \"\"\"\n        options = options or FrameNumberOptions()\n        options['expression'] = r'%%{eif\\:n+%d\\:d}' % options['frame_offset']\n        text = str(int(self.end_frame + options['frame_offset']))\n        self._add_burnin(text, align, options, DRAWTEXT)\n\n    def add_text(self, text, align, options=None):\n        \"\"\"\n        Adding static text to a filter.\n\n        :param str text: text to apply to the drawtext\n        :param enum align: alignment, must use provided enum flags\n        :param dict options: recommended to use TextOptions\n        \"\"\"\n        options = options or TextOptions()\n        self._add_burnin(text, align, options, DRAWTEXT)\n\n    def _add_burnin(self, text, align, options, draw):\n        \"\"\"\n        Generic method for building the filter flags.\n\n        :param str text: text to apply to the drawtext\n        :param enum align: alignment, must use provided enum flags\n        :param dict options:\n        \"\"\"\n        resolution = self.resolution\n        data = {\n            'text': options.get('expression') or text,\n            'color': options['font_color'],\n            'size': options['font_size']\n        }\n        data.update(options)\n        data.update(_drawtext(align, resolution, text, options))\n        if 'font' in data and _is_windows():\n            data['font'] = data['font'].replace(os.sep, r'\\\\' + os.sep)\n            data['font'] = data['font'].replace(':', r'\\:')\n        self.filters['drawtext'].append(draw % data)\n\n        if options.get('bg_color') is not None:\n            box = BOX % {\n                'border': options['bg_padding'],\n                'color': options['bg_color'],\n                'opacity': options['opacity']\n            }\n            self.filters['drawtext'][-1] += ':%s' % box\n\n    def command(self, output=None, args=None, overwrite=False):\n        \"\"\"\n        Generate the entire FFMPEG command.\n\n        :param str output: output file\n        :param str args: additional FFMPEG arguments\n        :param bool overwrite: overwrite the output if it exists\n        :returns: completed command\n        :rtype: str\n        \"\"\"\n        output = output or ''\n        if overwrite:\n            output = '-y %s' % output\n        return (FFMPEG % {\n            'input': self.source,\n            'output': output,\n            'args': '%s ' % args if args else '',\n            'filters': '-vf \"%s\"' % self.filter_string\n        }).strip()\n\n    def render(self, output, args=None, overwrite=False):\n        \"\"\"\n        Render the media to a specified destination.\n\n        :param str output: output file\n        :param str args: additional FFMPEG arguments\n        :param bool overwrite: overwrite the output if it exists\n        \"\"\"\n        if not overwrite and os.path.exists(output):\n            raise RuntimeError(\"Destination '%s' exists, please \"\n                               \"use overwrite\" % output)\n        command = self.command(output=output,\n                               args=args,\n                               overwrite=overwrite)\n        proc = Popen(command, shell=True)\n        proc.communicate()\n        if proc.returncode != 0:\n            raise RuntimeError(\"Failed to render '%s': %s'\"\n                               % (output, command))\n        if not os.path.exists(output):\n            raise RuntimeError(\"Failed to generate '%s'\" % output)\n\n\ndef _streams(source):\n    \"\"\"\n    :param str source: source media file\n    :rtype: [{}, ...]\n    \"\"\"\n    command = FFPROBE % {'source': source}\n    proc = Popen(command, shell=True, stdout=PIPE)\n    out = proc.communicate()[0]\n    if proc.returncode != 0:\n        raise RuntimeError(\"Failed to run: %s\" % command)\n    return json.loads(out)['streams']\n\n\ndef _drawtext(align, resolution, text, options):\n    \"\"\"\n    :rtype: {'x': int, 'y': int}\n    \"\"\"\n    x_pos = '0'\n    if align in (TOP_CENTERED, BOTTOM_CENTERED):\n        x_pos = 'w/2-tw/2'\n    elif align in (TOP_RIGHT, BOTTOM_RIGHT):\n        ifont = ImageFont.truetype(options['font'],\n                                   options['font_size'])\n        box_size = ifont.getsize(text)\n        x_pos = resolution[0] - (box_size[0] + options['x_offset'])\n    elif align in (TOP_LEFT, BOTTOM_LEFT):\n        x_pos = options['x_offset']\n\n    if align in (TOP_CENTERED,\n                 TOP_RIGHT,\n                 TOP_LEFT):\n        y_pos = '%d' % options['y_offset']\n    else:\n        y_pos = 'h-text_h-%d' % (options['y_offset'])\n    return {'x': x_pos, 'y': y_pos}\n\n\ndef _frames_to_timecode(frames, framerate):\n    return '{0:02d}:{1:02d}:{2:02d}:{3:02d}'.format(\n        int(frames / (3600 * framerate)),\n        int(frames / (60 * framerate) % 60),\n        int(frames / framerate % 60),\n        int(frames % framerate))\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/hls_playlist.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"HLS Playlist OpenTimelineIO adapter\n\nThis adapter supports authoring of HLS playlists within OpenTimelineIO by using\nclips to represent media fragments.\n\nStatus:\n    - Export of Media Playlists well supported\n    - Export of Master Playlists supported\n    - Import of Media Playlists well supported\n    - Import of Master Playlists unsupported\n    - Explicit Variant Stream controls in Master Playlists unsupported\n\nIn general, you can author otio as follows:\n    t = otio.schema.Timeline()\n    track = otio.schema.Track(\"v1\")\n    track.metadata['HLS'] = {\n        \"EXT-X-INDEPENDENT-SEGMENTS\": None,\n        \"EXT-X-PLAYLIST-TYPE\": \"VOD\"\n    }\n    t.tracks.append(track)\n\n    # Make a prototype media ref with the fragment's initialization metadata\n    fragmented_media_ref = otio.schema.ExternalReference(\n        target_url='video1.mp4',\n        metadata={\n            \"streaming\": {\n                \"init_byterange\": {\n                    \"byte_count\": 729,\n                    \"byte_offset\": 0\n                },\n                \"init_uri\": \"media-video-1.mp4\"\n            }\n        }\n    )\n\n    # Make a copy of the media ref specifying the byte range for the fragment\n    media_ref1 = fragmented_media_ref.deepcopy()\n    media_ref1.available_range=otio.opentime.TimeRange(\n        otio.opentime.RationalTime(0, 1),\n        otio.opentime.RationalTime(2.002, 1)\n    )\n    media_ref1.metadata['streaming'].update(\n        {\n            \"byte_count\": 534220,\n            \"byte_offset\": 1361\n        }\n    )\n\n    # make the fragment and append it\n    fragment1 = otio.schema.Clip(media_reference=media_ref1)\n    track.append(fragment1)\n\n    # (repeat to define each fragment)\n\nThe code above would yield an HLS playlist like:\n    #EXTM3U\n    #EXT-X-VERSION:7\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-PLAYLIST-TYPE:VOD\n    #EXT-X-INDEPENDENT-SEGMENTS\n    #EXT-X-MEDIA-SEQUENCE:1\n    #EXT-X-MAP:BYTERANGE=\"729@0\",URI=\"media-video-1.mp4\"\n    #EXTINF:2.00200,\n    #EXT-X-BYTERANGE:534220@1361\n    video1.mp4\n    #EXT-X-ENDLIST\n\nIf you add min_segment_duration and max_segment_duration to the timeline's\nmetadata dictionary as RationalTime objects, you can control the rule set\ndeciding how many fragments to accumulate into a single segment. When nothing\nis specified for these metadata keys, the adapter will create one segment per\nfragment.\n\nIn general, any metadata added to the track metadata dict under the HLS\nnamespace will be included at the top level of the exported playlist (see\n``EXT-X-INDEPENDENT-SEGMENTS`` and ``EXT-X-PLAYLIST-TYPE`` in the example\nabove). Each segment will pass through any metadata in the HLS namespace from\nthe media_reference.\n\nIf you write a Timeline with more than one track specified, then the adapter\nwill create an HLS master playlist.\n\nThe following track metadata keys will be used to inform exported master\nplaylist metadata per variant stream:\n    bandwidth\n    codec\n    language\n    mimeType\n    group_id (audio)\n    autoselect (audio)\n    default (audio)\nThese values are translated to EXT-X-STREAM-INF and EXT-X-MEDIA\nattributes as defined in sections 4.3.4.2 and 4.3.4.1 of\ndraft-pantos-http-live-streaming, respectively.\n\"\"\"\n\nimport re\nimport copy\n\nimport opentimelineio as otio\n\n# TODO: determine output version based on features used\nOUTPUT_PLAYLIST_VERSION = \"7\"\n\n# TODO: make sure all strings get sanitized through encoding and decoding\nPLAYLIST_STRING_ENCODING = \"utf-8\"\n\n# Enable isinstance(my_instance, basestring) tests in Python 3\n# This can be phased out when Python 2 support is dropped. Replace tests with:\n# isinstance(my_instance, str)\n\ntry:\n    basestring\nexcept NameError:\n    basestring = str\n\n\"\"\"\nMatches a single key/value pair from an HLS Attribute List.\nSee section 4.2 of draft-pantos-http-live-streaming for more detail.\n\"\"\"\nATTRIBUTE_RE = re.compile(\n    r'(?P<AttributeName>[A-Z0-9-]+)' + r'\\=' +\n    r'(?P<AttributeValue>(?:\\\"[^\\r\\n\"]*\\\")|[^,]+)' + r',?'\n)\n\n\"\"\"\nMatches AttributeValue of the above regex into appropriate data types.\nNote that these are meant to be joined using regex \"or\" in this order.\n\"\"\"\n_ATTRIBUTE_RE_VALUE_STR_LIST = [\n    r'(?P<resolution>(?P<width>[0-9]+)x(?P<height>[0-9]+))\\Z',\n    r'(?P<hexcidecimal>0[xX](?P<hex_value>[0-9A-F]+))\\Z',\n    r'(?P<floating_point>-?[0-9]+\\.[0-9]+)\\Z',\n    r'(?P<decimal>[0-9]+)\\Z',\n    r'(?P<string>\\\"(?P<string_value>[^\\r\\n\"]*)\\\")\\Z',\n    r'(?P<enumerated>[^\",\\s]+)\\Z'\n]\nATTRIBUTE_VALUE_RE = re.compile(\"|\".join(_ATTRIBUTE_RE_VALUE_STR_LIST))\n\n\"\"\"\nMatches a byterange as used in various contexts.\nSee section 4.3.2.2 of draft-pantos-http-live-streaming for an example use of\nthis byterange form.\n\"\"\"\nBYTERANGE_RE = re.compile(r'(?P<n>\\d+)(?:@(?P<o>\\d+))?')\n\n\"\"\"\nMatches HLS Playlist tags or comments, respective.\nSee section 4.1 of draft-pantos-http-live-streaming for more detail.\n\"\"\"\nTAG_RE = re.compile(\n    r'#(?P<tagname>EXT[^:\\s]+)(?P<hasvalue>:?)(?P<tagvalue>.*)'\n)\nCOMMENT_RE = re.compile(r'#(?!EXT)(?P<comment>.*)')\n\n\nclass AttributeListEnum(str):\n    \"\"\" A subclass allowing us to differentiate enums in HLS attribute lists\n    \"\"\"\n\n\ndef _value_from_raw_attribute_value(raw_attribute_value):\n    \"\"\"\n    Takes in a raw AttributeValue and returns an appopritate Python type.\n    If there is a problem decoding the value, None is returned.\n    \"\"\"\n    value_match = ATTRIBUTE_VALUE_RE.match(raw_attribute_value)\n    if not value_match:\n        return None\n\n    group_dict = value_match.groupdict()\n    # suss out the match\n    for k, v in group_dict.items():\n        # not a successful group match\n        if v is None:\n            continue\n\n        # decode the string\n        if k == 'resolution':\n            return v\n        elif k == 'enumerated':\n            return AttributeListEnum(v)\n        elif k == 'hexcidecimal':\n            return int(group_dict['hex_value'], base=16)\n        elif k == 'floating_point':\n            return float(v)\n        elif k == 'decimal':\n            return int(v)\n        elif k == 'string':\n            # grab only the data within the quotes, excluding the quotes\n            string_value = group_dict['string_value']\n            return string_value\n\n    return None\n\n\nclass AttributeList(dict):\n    \"\"\"\n    Dictionary-like object representing an HLS AttributeList.\n    See section 4.2 of draft-pantos-http-live-streaming for more detail.\n    \"\"\"\n\n    def __init__(self, other=None):\n        \"\"\"\n        contstructs an :class:`AttributeList`.\n\n        ``Other`` can be either another dictionary-like object or a list of\n        key/value pairs\n        \"\"\"\n        if not other:\n            return\n\n        try:\n            items = other.items()\n        except AttributeError:\n            items = other\n\n        for k, v in items:\n            self[k] = v\n\n    def __str__(self):\n        \"\"\"\n        Construct attribute list string as it would exist in an HLS playlist.\n        \"\"\"\n        attr_list_entries = []\n        # Use a sorted version of the dictionary to ensure consistency\n        for k, v in sorted(self.items(), key=lambda i: i[0]):\n            out_value = ''\n            if isinstance(v, AttributeListEnum):\n                out_value = v\n            elif isinstance(v, basestring):\n                out_value = '\"{}\"'.format(v)\n            else:\n                out_value = str(v)\n\n            attr_list_entries.append('{}={}'.format(k, out_value))\n\n        return ','.join(attr_list_entries)\n\n    @classmethod\n    def from_string(cls, attrlist_string):\n        \"\"\"\n        Accepts an attribute list string and returns an :class:`AttributeList`.\n\n        The values will be transformed to Python types.\n        \"\"\"\n        attr_list = cls()\n        match = ATTRIBUTE_RE.search(attrlist_string)\n        while match:\n            # unpack the values from the match\n            group_dict = match.groupdict()\n            name = group_dict['AttributeName']\n            raw_value = group_dict['AttributeValue']\n\n            # parse the raw value\n            value = _value_from_raw_attribute_value(raw_value)\n            attr_list[name] = value\n\n            # search for the next attribute in the string\n            match_end = match.span()[1]\n            match = ATTRIBUTE_RE.search(attrlist_string, match_end)\n\n        return attr_list\n\n\n# some special top-levle keys that HLS metadata will be decoded into\nFORMAT_METADATA_KEY = 'HLS'\n\"\"\"\nSome concepts are translatable between HLS and other streaming formats (DASH).\nThese metadata keys are used on OTIO objects outside the HLS namespace because\nthey are higher level concepts.\n\"\"\"\nSTREAMING_METADATA_KEY = 'streaming'\nINIT_BYTERANGE_KEY = 'init_byterange'\nINIT_URI_KEY = 'init_uri'\nSEQUENCE_NUM_KEY = 'sequence_num'\nBYTE_OFFSET_KEY = 'byte_offset'\nBYTE_COUNT_KEY = 'byte_count'\n\n\nclass Byterange(object):\n    \"\"\"Offers interpretation of HLS byte ranges in various forms.\"\"\"\n\n    count = None\n    \"\"\"(:class:`int`) Number of bytes included in the range.\"\"\"\n\n    offset = None\n    \"\"\"(:class:`int`) Byte offset at which the range starts.\"\"\"\n\n    def __init__(self, count=None, offset=None):\n        \"\"\"Constructs a :class:`Byterange` object.\n\n        :param count: (:class:`int`) Number of bytes included in the range.\n        :param offset: (:class:`int`) Byte offset at which the range starts.\n        \"\"\"\n        self.count = (count if count is not None else 0)\n        self.offset = offset\n\n    def __eq__(self, other):\n        if not isinstance(other, Byterange):\n            # fall back on identity, this should always be False\n            return (self is other)\n        return (self.count == other.count and self.offset == other.offset)\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    def __repr__(self):\n        return '{}(offset = {}, count = {})'.format(\n            type(self),\n            str(self.offset),\n            str(self.count)\n        )\n\n    def __str__(self):\n        \"\"\"returns a string in HLS format\"\"\"\n\n        out_str = str(self.count)\n        if self.offset is not None:\n            out_str += '@{}'.format(str(self.offset))\n\n        return out_str\n\n    def to_dict(self):\n        \"\"\"Returns a dict suitable for storing in otio metadata.\n\n        :return: (:class:`dict`) serializable version of byterange.\n        \"\"\"\n        range_dict = {BYTE_COUNT_KEY: self.count}\n        if self.offset is not None:\n            range_dict[BYTE_OFFSET_KEY] = self.offset\n\n        return range_dict\n\n    @classmethod\n    def from_string(cls, byterange_string):\n        \"\"\"Construct a :class:`Byterange` given a string in HLS format.\n\n        :param byterange_string: (:class:`str`) a byterange string.\n        :return: (:class:`Byterange`) The instance for the provided string.\n        \"\"\"\n        m = BYTERANGE_RE.match(byterange_string)\n\n        return cls.from_match_dict(m.groupdict())\n\n    @classmethod\n    def from_match_dict(cls, match_dict):\n        \"\"\"\n        Construct a :class:`Byterange` given a groupdict from ``BYTERANGE_RE``\n\n        :param match_dict: (:class:`dict`) the ``match_dict``.\n        :return: (:class:`Byterange`) The instance for the provided string.\n        \"\"\"\n        byterange = cls(count=int(match_dict['n']))\n\n        try:\n            byterange.offset = int(match_dict['o'])\n        except KeyError:\n            pass\n\n        return byterange\n\n    @classmethod\n    def from_dict(cls, info_dict):\n        \"\"\" Creates a :class:`Byterange` given a dictionary containing keys\n        like generated from the :meth:`to_dict method`.\n\n        :param info_dict: (:class:`dict`) Dictionary byterange.\n        :return: (:class:`Byterange`) an equivalent instance.\n        \"\"\"\n        byterange = cls(\n            count=info_dict.get(BYTE_COUNT_KEY),\n            offset=info_dict.get(BYTE_OFFSET_KEY)\n        )\n\n        return byterange\n\n\n\"\"\"\nFor a given collection of media, HLS has two playlist types:\n    - Media Playlist\n    - Master Playlist\n\nThe media playlist refers directly to the individual segments that make up an\naudio or video track of a given program. The master playlist refers to a\ncollection of media playlists and provides ways to use them together\n(rendition groups).\n\nSee section 2 of draft-pantos-http-live-streaming for more detail.\n\nThe constants below define which tags belong to which schema.\n\"\"\"\n\n\"\"\"\nBasic tags appear in both media and master playlists.\nSee section 4.3.1 of draft-pantos-http-live-streaming for more detail.\n\"\"\"\nBASIC_TAGS = set([\n    \"EXTM3U\",\n    \"EXT-X-VERSION\"\n])\n\n\"\"\"\nMedia segment tags apply to either the following media or all subsequent\nsegments. They MUST NOT appear in master playlists.\nSee section 4.3.2 of draft-pantos-http-live-streaming for more detail.\n\"\"\"\nMEDIA_SEGMENT_TAGS = set([\n    'EXTINF',\n    'EXT-X-BYTERANGE',\n    'EXT-X-DISCONTINUITY',\n    'EXT-X-KEY',\n    'EXT-X-MAP',\n    'EXT-X-PROGRAM-DATE-TIME',\n    'EXT-X-DATERANGE'\n])\n\n\"\"\" The subset of above tags that apply to every segment following them \"\"\"\nMEDIA_SEGMENT_SUBSEQUENT_TAGS = set([\n    'EXT-X-KEY',\n    'EXT-X-MAP',\n])\n\n\"\"\"\nMedia Playlist tags must only occur once per playlist, and must not appear in\nMaster Playlists.\nSee section 4.3.3 of draft-pantos-http-live-streaming for more detail.\n\"\"\"\nMEDIA_PLAYLIST_TAGS = set([\n    'EXT-X-TARGETDURATION',\n    'EXT-X-MEDIA-SEQUENCE',\n    'EXT-X-DISCONTINUITY-SEQUENCE',\n    'EXT-X-ENDLIST',\n    'EXT-X-PLAYLIST-TYPE',\n    'EXT-X-I-FRAMES-ONLY'\n])\n\n\"\"\"\nMaster playlist tags declare global parameters for the presentation.\nThey must not appear in media playlists.\nSee section 4.3.4 of draft-pantos-http-live-streaming for more detail.\n\"\"\"\nMASTER_PLAYLIST_TAGS = set([\n    'EXT-X-MEDIA',\n    'EXT-X-STREAM-INF',\n    'EXT-X-I-FRAME-STREAM-INF',\n    'EXT-X-SESSION-DATA',\n    'EXT-X-SESSION-KEY',\n])\n\n\"\"\"\nMedia or Master Playlist tags can appear in either media or master playlists.\nSee section 4.3.5 of draft-pantos-http-live-streaming for more detail.\nThese tags SHOULD appear in either the media or master playlist. If they occur\nin both, their values MUST agree.\nThese values MUST NOT appear more than once in a playlist.\n\"\"\"\nMEDIA_OR_MASTER_TAGS = set([\n    \"EXT-X-INDEPENDENT-SEGMENTS\",\n    \"EXT-X-START\"\n])\n\n\"\"\"\nSome special tags used by the parser.\n\"\"\"\nPLAYLIST_START_TAG = \"EXTM3U\"\nPLAYLIST_END_TAG = \"EXT-X-ENDLIST\"\nPLAYLIST_VERSION_TAG = \"EXT-X-VERSION\"\nPLAYLIST_SEGMENT_INF_TAG = \"EXTINF\"\n\n\"\"\"\nattribute list entries to omit from EXT-I-FRAME-STREAM-INF tags\nSee section 4.3.4.3 of draft-pantos-http-live-streaming for more detail.\n\"\"\"\nI_FRAME_OMIT_ATTRS = set([\n    'FRAME-RATE',\n    'AUDIO',\n    'SUBTITLES',\n    'CLOSED-CAPTIONS'\n])\n\n\"\"\" enum for kinds of playlist entries \"\"\"\nEntryType = type('EntryType', (), {\n    'tag': 'tag',\n    'comment': 'comment',\n    'URI': 'URI'\n})\n\n\"\"\" enum for types of playlists \"\"\"\nPlaylistType = type('PlaylistType', (), {\n    'media': 'media',\n    'master': 'master'\n})\n\n\"\"\" mapping from HLS track type to otio ``TrackKind`` \"\"\"\nHLS_TRACK_TYPE_TO_OTIO_KIND = {\n    AttributeListEnum('AUDIO'): otio.schema.TrackKind.Audio,\n    AttributeListEnum('VIDEO'): otio.schema.TrackKind.Video,\n    # TODO: determine how to handle SUBTITLES and CLOSED-CAPTIONS\n}\n\n\"\"\" mapping from otio ``TrackKind`` to HLS track type \"\"\"\nOTIO_TRACK_KIND_TO_HLS_TYPE = dict((\n    (v, k) for k, v in HLS_TRACK_TYPE_TO_OTIO_KIND.items()\n))\n\n\nclass HLSPlaylistEntry(object):\n    \"\"\"An entry in an HLS playlist.\n\n    Entries can be a tag, a comment, or a URI. All HLS playlists are parsed\n    into lists of :class:`HLSPlaylistEntry` instances that can then be\n    interpreted against the HLS schema.\n    \"\"\"\n\n    # TODO: rename this to entry_type to fix builtin masking\n    # type = None\n    \"\"\" (``EntryType``) the type of entry \"\"\"\n\n    comment_string = None\n    \"\"\"\n    (:class:`str`) value of comment (if the ``entry_type`` is\n    ``EntryType.comment``).\n    \"\"\"\n\n    tag_name = None\n    \"\"\"\n    (:class:`str`) Name of tag (if the ``entry_type`` is ``EntryType.tag``).\n    \"\"\"\n\n    tag_value = None\n    \"\"\"\n    (:class:`str`) Value of tag (if the ``entry_type`` is ``EntryType.tag``).\n    \"\"\"\n\n    uri = None\n    \"\"\"\n    (:class:`str`) Value of the URI (if the ``entry_type is ``EntryType.uri``).\n    \"\"\"\n\n    def __init__(self, type):\n        \"\"\"\n        Constructs an :class:`HLSPlaylistEntry`.\n\n        :param type: (``EntryType``) Type of entry.\n        \"\"\"\n        self.type = type\n\n    def __repr__(self):\n        base_str = 'otio.adapter.HLSPlaylistEntry(type={}'.format(\n            self.type)\n        if self.type == EntryType.tag:\n            base_str += ', tag_name={}, tag_value={}'.format(\n                repr(self.tag_name),\n                repr(self.tag_value)\n            )\n        elif self.type == EntryType.comment:\n            base_str += ', comment={}'.format(repr(self.comment_string))\n        elif self.type == EntryType.URI:\n            base_str += ', URI={}'.format(repr(self.uri))\n\n        return base_str + ')'\n\n    def __str__(self):\n        \"\"\"\n        Returns a string as it would appear in an HLS playlist.\n\n        :return: (:class:`str`) HLS playlist entry string.\n        \"\"\"\n        if self.type == EntryType.comment and self.comment_string:\n            return \"# {}\".format(self.comment_string)\n        elif self.type == EntryType.comment:\n            # empty comments are blank lines\n            return \"\"\n        elif self.type == EntryType.URI:\n            return self.uri\n        elif self.type == EntryType.tag:\n            out_tag_name = self.tag_name\n            if self.tag_value is not None:\n                return '#{}:{}'.format(out_tag_name, self.tag_value)\n            else:\n                return '#{}'.format(out_tag_name)\n\n    @classmethod\n    def tag_entry(cls, name, value=None):\n        \"\"\"\n        Creates an ``EntryType.tag`` :class:`HLSPlaylistEntry`.\n\n        :param name: (:class:`str`) tag name.\n        :param value: (:class:`str`) tag value.\n        :return: (:class:`HLSPlaylistEntry`) Entry instance.\n        \"\"\"\n        entry = cls(EntryType.tag)\n        entry.tag_name = name\n        entry.tag_value = value\n\n        return entry\n\n    @classmethod\n    def comment_entry(cls, comment):\n        \"\"\"Creates an ``EntryType.comment`` :class:`HLSPlaylistEntry`.\n\n        :param comment: (:class:`str`) the comment.\n        :return: (:class:`HLSPlaylistEntry`) Entry instance.\n        \"\"\"\n        entry = cls(EntryType.comment)\n        entry.comment_string = comment\n\n        return entry\n\n    @classmethod\n    def uri_entry(cls, uri):\n        \"\"\"Creates an ``EntryType.uri`` :class:`HLSPlaylistEntry`.\n\n        :param uri: (:class:`str`) A URI string.\n        :return: (:class:`HLSPlaylistEntry`) Entry instance.\n        \"\"\"\n        entry = cls(EntryType.URI)\n        entry.uri = uri\n\n        return entry\n\n    @classmethod\n    def from_string(cls, entry_string):\n        \"\"\"Creates an `:class:`HLSPlaylistEntry` given a string as it appears\n        in an HLS playlist.\n\n        :param entry_string: (:class:`str`) String from an HLS playlist.\n        :return: (:class:`HLSPlaylistEntry`) Entry instance.\n        \"\"\"\n        # Empty lines are skipped\n        if not entry_string.strip():\n            return None\n\n        # Attempt to parse as a tag\n        m = TAG_RE.match(entry_string)\n        if m:\n            group_dict = m.groupdict()\n            tag_value = (\n                group_dict['tagvalue']\n                if group_dict['hasvalue'] else None\n            )\n            entry = cls.tag_entry(group_dict['tagname'], tag_value)\n            return entry\n\n        # Attempt to parse as a comment\n        m = COMMENT_RE.match(entry_string)\n        if m:\n            entry = cls.comment_entry(m.groupdict()['comment'])\n            return entry\n\n        # If it's not the others, treat as a URI\n        entry = cls.uri_entry(entry_string)\n\n        return entry\n\n    \"\"\"A dispatch dictionary for grabbing the right Regex to parse tags.\"\"\"\n    TAG_VALUE_RE_MAP = {\n        \"EXTINF\": re.compile(r'(?P<duration>\\d+(\\.\\d*)?),(?P<title>.*$)'),\n        \"EXT-X-BYTERANGE\": BYTERANGE_RE,\n        \"EXT-X-KEY\": re.compile(r'(?P<attribute_list>.*$)'),\n        \"EXT-X-MAP\": re.compile(r'(?P<attribute_list>.*$)'),\n        \"EXT-X-MEDIA-SEQUENCE\": re.compile(r'(?P<number>\\d+)'),\n        \"EXT-X-PLAYLIST-TYPE\": re.compile(r'(?P<type>EVENT|VOD)'),\n        PLAYLIST_VERSION_TAG: re.compile(r'(?P<n>\\d+)')\n    }\n\n    def parsed_tag_value(self, playlist_version=None):\n        \"\"\"Parses and returns ``self.tag_value`` based on the HLS schema.\n\n        The value will be a dictionary where the keys are the names used in the\n        draft Pantos HTTP Live Streaming doc. When \"attribute-list\" is\n        specified, an entry \"attribute_list\" will be present containing\n        an :class:`AttributeList` instance.\n\n        :param playlist_version: (:class:`int`) version number of the playlist.\n            If none is provided, a best guess will be made.\n        :return: The parsed value.\n        \"\"\"\n        if self.type != EntryType.tag:\n            return None\n\n        try:\n            tag_re = self.TAG_VALUE_RE_MAP[self.tag_name]\n        except KeyError:\n            return None\n\n        # parse the tag\n        m = tag_re.match(self.tag_value)\n        group_dict = m.groupdict()\n\n        if not m:\n            return None\n\n        # If the tag value has an attribute list, parse it and add it\n        try:\n            attribute_list = group_dict['attribute_list']\n            attr_list = AttributeList.from_string(attribute_list)\n            group_dict['attributes'] = attr_list\n        except KeyError:\n            pass\n\n        return group_dict\n\n\nclass HLSPlaylistParser(object):\n    \"\"\"Bootstraps HLS parsing and hands the playlist string off to the\n    appropriate parser for the type\n    \"\"\"\n\n    def __init__(self, edl_string):\n        self.timeline = otio.schema.Timeline()\n        self.playlist_type = None\n\n        self._parse_playlist(edl_string)\n\n    def _parse_playlist(self, edl_string):\n        \"\"\"Parses the HLS Playlist string line-by-line.\"\"\"\n        # parse lines until we encounter one that identifies the playlist type\n        # then hand off\n        start_encountered = False\n        end_encountered = False\n        playlist_entries = []\n        playlist_version = 1\n        for line in edl_string.splitlines():\n            # attempt to parse the entry\n            entry = HLSPlaylistEntry.from_string(line)\n            if entry is None:\n                continue\n\n            entry_is_tag = (entry.type == EntryType.tag)\n\n            # identify if the playlist start/end is encountered\n            if (entry_is_tag and not (start_encountered and end_encountered)):\n                if entry.tag_name == PLAYLIST_START_TAG:\n                    start_encountered = True\n                elif entry.tag_name == PLAYLIST_END_TAG:\n                    end_encountered = True\n\n            # if the playlist starting tag hasn't been encountered, ignore\n            if not start_encountered:\n                continue\n\n            # Store the parsed entry\n            playlist_entries.append(entry)\n\n            # Determine if this tells us the playlist type\n            if not self.playlist_type and entry_is_tag:\n                if entry.tag_name in MASTER_PLAYLIST_TAGS:\n                    self.playlist_type = PlaylistType.master\n                elif entry.tag_name in MEDIA_PLAYLIST_TAGS:\n                    self.playlist_type = PlaylistType.media\n\n            if end_encountered:\n                break\n\n            # try to grab the version from the playlist\n            if entry_is_tag and entry.tag_name == PLAYLIST_VERSION_TAG:\n                playlist_version = int(entry.parsed_tag_value()['n'])\n\n        # dispatch to the appropriate schema interpreter\n        if self.playlist_type is None:\n            self.timeline = None\n            raise otio.exceptions.ReadingNotSupportedError(\n                \"could not determine playlist type\"\n            )\n        elif self.playlist_type == PlaylistType.master:\n            self.timeline = None\n            raise otio.exceptions.AdapterDoesntSupportFunction(\n                \"HLS master playlists are not yet supported\"\n            )\n        elif self.playlist_type == PlaylistType.media:\n            parser = MediaPlaylistParser(playlist_entries, playlist_version)\n            if len(parser.track):\n                self.timeline.tracks.append(parser.track)\n\n\nclass MediaPlaylistParser(object):\n    \"\"\"Parses an HLS Media playlist returning a SEQUENCE\"\"\"\n\n    def __init__(self, playlist_entries, playlist_version=None):\n        self.track = otio.schema.Track(\n            metadata={FORMAT_METADATA_KEY: {}}\n        )\n\n        self._parse_entries(playlist_entries, playlist_version)\n\n    def _handle_track_metadata(self, entry, playlist_version, clip):\n        \"\"\"Stashes the tag value in the track metadata\"\"\"\n        value = entry.tag_value\n        self.track.metadata[FORMAT_METADATA_KEY][entry.tag_name] = value\n\n    def _handle_discarded_metadata(self, entry, playlist_version, clip):\n        \"\"\"Handler for tags that are discarded. This is done when a tag's\n        information is represented by the native OTIO concepts.\n\n        For instance, the EXT-X-TARGETDURATION tag simply gives a rounded\n        value for the maximum segment size in the playlist. This can easily\n        be found in OTIO by examining the clips.\n        \"\"\"\n        # Do nothing\n\n    def _metadata_dict_for_MAP(self, entry, playlist_version):\n        entry_data = entry.parsed_tag_value()\n        attributes = entry_data['attributes']\n        map_dict = {}\n        for attr, value in attributes.items():\n            if attr == 'BYTERANGE':\n                byterange = Byterange.from_string(value)\n                map_dict[INIT_BYTERANGE_KEY] = byterange.to_dict()\n            elif attr == 'URI':\n                map_dict[INIT_URI_KEY] = value\n\n        return map_dict\n\n    def _handle_INF(self, entry, playlist_version, clip):\n        # This specifies segment duration and optional title\n        info_dict = entry.parsed_tag_value(playlist_version)\n        segment_duration = float(info_dict['duration'])\n        segment_title = info_dict['title']\n        available_range = otio.opentime.TimeRange(\n            otio.opentime.RationalTime(0, 1),\n            otio.opentime.RationalTime(segment_duration, 1)\n        )\n\n        # Push the info to the clip\n        clip.media_reference.available_range = available_range\n        clip.source_range = available_range\n        clip.name = segment_title\n\n    def _handle_BYTERANGE(self, entry, playlist_version, clip):\n        reference_metadata = clip.media_reference.metadata\n        ref_streaming_metadata = reference_metadata.setdefault(\n            STREAMING_METADATA_KEY,\n            {}\n        )\n\n        # Pull out the byte count and offset\n        byterange = Byterange.from_match_dict(\n            entry.parsed_tag_value(playlist_version)\n        )\n        ref_streaming_metadata.update(byterange.to_dict())\n\n    \"\"\"\n    Specifies handlers for specific HLS tags.\n    \"\"\"\n    TAG_HANDLERS = {\n        \"EXTINF\": _handle_INF,\n        PLAYLIST_VERSION_TAG: _handle_track_metadata,\n        \"EXT-X-TARGETDURATION\": _handle_discarded_metadata,\n        \"EXT-X-MEDIA-SEQUENCE\": _handle_discarded_metadata,\n        \"EXT-X-PLAYLIST-TYPE\": _handle_track_metadata,\n        \"EXT-X-INDEPENDENT-SEGMENTS\": _handle_track_metadata,\n        \"EXT-X-BYTERANGE\": _handle_BYTERANGE\n    }\n\n    def _parse_entries(self, playlist_entries, playlist_version):\n        \"\"\"Interpret the entries through the lens of the schema\"\"\"\n        current_clip = otio.schema.Clip(\n            media_reference=otio.schema.ExternalReference(\n                metadata={\n                    FORMAT_METADATA_KEY: {},\n                    STREAMING_METADATA_KEY: {}\n                }\n            )\n        )\n        current_media_ref = current_clip.media_reference\n        segment_metadata = {}\n        current_map_data = {}\n        # per section 4.3.3.2 of Pantos HLS, 0 is default start track\n        current_track = 0\n        for entry in playlist_entries:\n            if entry.type == EntryType.URI:\n                # the URI ends the segment definition\n                current_media_ref.target_url = entry.uri\n                current_media_ref.metadata[FORMAT_METADATA_KEY].update(\n                    segment_metadata\n                )\n                current_media_ref.metadata[STREAMING_METADATA_KEY].update(\n                    current_map_data\n                )\n                current_clip.metadata.setdefault(\n                    STREAMING_METADATA_KEY,\n                    {}\n                )[SEQUENCE_NUM_KEY] = current_track\n                self.track.append(current_clip)\n                current_track += 1\n\n                # Set up the next segment definition\n                current_clip = otio.schema.Clip(\n                    media_reference=otio.schema.ExternalReference(\n                        metadata={\n                            FORMAT_METADATA_KEY: {},\n                            STREAMING_METADATA_KEY: {}\n                        }\n                    )\n                )\n                current_media_ref = current_clip.media_reference\n                continue\n            elif entry.type != EntryType.tag:\n                # the rest of the code deals only with tags\n                continue\n\n            # Explode the EXT-X-MAP info out\n            if entry.tag_name == \"EXT-X-MAP\":\n                map_data = self._metadata_dict_for_MAP(entry, playlist_version)\n                current_map_data.update(map_data)\n                continue\n\n            # Grab the track when it comes around\n            if entry.tag_name == \"EXT-X-MEDIA-SEQUENCE\":\n                entry_data = entry.parsed_tag_value()\n                current_track = int(entry_data['number'])\n\n            # If the segment tag is one that applies to all that follow\n            # store the value to be applied to each segment\n            if entry.tag_name in MEDIA_SEGMENT_SUBSEQUENT_TAGS:\n                segment_metadata[entry.tag_name] = entry.tag_value\n                continue\n\n            # use a handler if available\n            try:\n                handler = self.TAG_HANDLERS[entry.tag_name]\n                handler(self, entry, playlist_version, current_clip)\n                continue\n            except KeyError:\n                pass\n\n            # add the tag to the reference metadata at the correct level\n            if entry.tag_name in [PLAYLIST_START_TAG, PLAYLIST_END_TAG]:\n                continue\n            elif entry.tag_name in MEDIA_SEGMENT_TAGS:\n                # Media segments translate into media refs\n                hls_metadata = current_media_ref.metadata[FORMAT_METADATA_KEY]\n                hls_metadata[entry.tag_name] = entry.tag_value\n            elif entry.tag_name in MEDIA_PLAYLIST_TAGS:\n                # Media playlists translate into tracks\n                hls_metadata = self.track.metadata[FORMAT_METADATA_KEY]\n                hls_metadata[entry.tag_name] = entry.tag_value\n\n\n\"\"\"\nCompatibility version list:\n    EXT-X-BYTERANGE >= 4\n    EXT-X-I-FRAMES-ONLY >= 4\n    EXT-X-MAP in media playlist with EXT-X-I-FRAMES-ONLY >= 5\n    EXT-X-MAP in media playlist without I-FRAMES-ONLY >= 6\n    EXT-X-KEY constrants are by attributes specified:\n        - IV >= 2\n        - KEYFORMAT >= 5\n        - KEYFORMATVERSIONS >= 5\n    EXTINF with floating point vaules >= 3\n\n    master playlist:\n    EXT-X-MEDIA with INSTREAM-ID=\"SERVICE\"\n\"\"\"\n\n\ndef entries_for_segment(\n    uri,\n    segment_duration,\n    segment_name=None,\n    segment_byterange=None,\n    segment_tags=None\n):\n    \"\"\"Creates a set of :class:`HLSPlaylistEntries` with the given parameters.\n\n    :param uri: (:class:`str`) The uri for the segment media.\n    :param segment_duration: (:class:`opentimelineio.opentime.RationalTime`)\n        playback duration of the segment.\n    :param segment_byterange: (:class:`ByteRange`) The data range for the\n        segment in the media (if required)\n    :param segment_tags: (:class:`dict`) key/value pairs of to become\n        additional tags for the segment\n\n    :return: (:class:`list`) a group of :class:`HLSPlaylistEntry` instances for\n        the segment\n    \"\"\"\n    # Create the tags dict to build\n    if segment_tags:\n        tags = copy.deepcopy(segment_tags)\n    else:\n        tags = {}\n\n    # Start building the entries list\n    segment_entries = []\n\n    # add the EXTINF\n    name = segment_name if segment_name is not None else ''\n    tag_value = '{0:.5f},{1}'.format(\n        otio.opentime.to_seconds(segment_duration),\n        name\n    )\n    extinf_entry = HLSPlaylistEntry.tag_entry('EXTINF', tag_value)\n    segment_entries.append(extinf_entry)\n\n    # add the additional tags\n    tag_entries = [\n        HLSPlaylistEntry.tag_entry(k, v) for k, v in\n        tags.items()\n    ]\n    segment_entries.extend(tag_entries)\n\n    # Now add the byterange for the entry\n    if segment_byterange:\n        byterange_entry = HLSPlaylistEntry.tag_entry(\n            'EXT-X-BYTERANGE',\n            str(segment_byterange)\n        )\n        segment_entries.append(byterange_entry)\n\n    # Add the URI\n    # this method expects all fragments come from the same source file\n    uri_entry = HLSPlaylistEntry.uri_entry(uri)\n    segment_entries.append(uri_entry)\n\n    return segment_entries\n\n\ndef stream_inf_attr_list_for_track(track):\n    \"\"\" Builds an :class:`AttributeList` instance for use in ``STREAM-INF``\n    tags for the provided track.\n\n    :param track: (:class:`otio.schema.Track`) A track representing a\n        variant stream\n    :return: (:class:`AttributeList`) The instance from the metadata\n    \"\"\"\n    streaming_metadata = track.metadata.get(STREAMING_METADATA_KEY, {})\n\n    attributes = []\n    bandwidth = streaming_metadata.get('bandwidth')\n    if bandwidth is not None:\n        attributes.append(('BANDWIDTH', bandwidth))\n\n    codec = streaming_metadata.get('codec')\n    if codec is not None:\n        attributes.append(('CODECS', codec))\n\n    frame_rate = streaming_metadata.get('frame_rate')\n    if frame_rate is not None:\n        attributes.append(('FRAME-RATE', frame_rate))\n\n    if 'width' in streaming_metadata and 'height' in streaming_metadata:\n        resolution = \"{}x{}\".format(\n            streaming_metadata['width'],\n            streaming_metadata['height']\n        )\n        attributes.append(('RESOLUTION', AttributeListEnum(resolution)))\n\n    al = AttributeList(attributes)\n\n    return al\n\n\ndef master_playlist_to_string(master_timeline):\n    \"\"\"Writes a master playlist describing the tracks\"\"\"\n\n    # start with a version number of 1, as features are encountered, we will\n    # update the version accordingly\n    version_requirements = set([1])\n\n    # TODO: detect rather than forcing version 6\n    version_requirements.add(6)\n\n    header_tags = copy.copy(\n        master_timeline.metadata.get(FORMAT_METADATA_KEY, {})\n    )\n\n    # Filter out any values from the HLS metadata that aren't meant to become\n    # tags, such as the directive to force an HLS master playlist\n    hls_md_blacklist = ['master_playlist']\n    for key in hls_md_blacklist:\n        try:\n            del(header_tags[key])\n        except KeyError:\n            pass\n\n    playlist_entries = []\n\n    # First declare the non-visual media\n    hls_type_count = {}\n    video_tracks = []\n    audio_tracks = [\n        t for t in master_timeline.tracks if\n        t.kind == otio.schema.TrackKind.Audio\n    ]\n    for track in master_timeline.tracks:\n        if track.kind == otio.schema.TrackKind.Video:\n            # video is done later, skip\n            video_tracks.append(track)\n            continue\n\n        # Determine the HLS type\n        hls_type = OTIO_TRACK_KIND_TO_HLS_TYPE[track.kind]\n\n        streaming_metadata = track.metadata.get(STREAMING_METADATA_KEY, {})\n\n        # Find the group name\n        try:\n            group_id = streaming_metadata['group_id']\n        except KeyError:\n            sub_id = hls_type_count.setdefault(hls_type, 1)\n            group_id = '{}{}'.format(hls_type, sub_id)\n            hls_type_count[hls_type] += 1\n\n        media_playlist_default_uri = \"{}.m3u8\".format(track.name)\n        try:\n            track_uri = track.metadata[FORMAT_METADATA_KEY].get(\n                'uri',\n                media_playlist_default_uri\n            )\n        except KeyError:\n            track_uri = media_playlist_default_uri\n\n        # Build the attribute list\n        attributes = AttributeList(\n            [\n                ('TYPE', hls_type),\n                ('GROUP-ID', group_id),\n                ('URI', track_uri),\n                ('NAME', track.name),\n            ]\n        )\n\n        if streaming_metadata.get('autoselect'):\n            attributes['AUTOSELECT'] = AttributeListEnum('YES')\n\n        if streaming_metadata.get('default'):\n            attributes['DEFAULT'] = AttributeListEnum('YES')\n\n        # Finally, create the tag\n        entry = HLSPlaylistEntry.tag_entry(\n            'EXT-X-MEDIA',\n            str(attributes)\n        )\n\n        playlist_entries.append(entry)\n\n    # Add a blank line in the playlist to separate sections\n    if playlist_entries:\n        playlist_entries.append(HLSPlaylistEntry.comment_entry(''))\n\n    # First write any i-frame playlist entires\n    iframe_list_entries = []\n    for track in video_tracks:\n        try:\n            iframe_uri = track.metadata[FORMAT_METADATA_KEY]['iframe_uri']\n        except KeyError:\n            # don't include iframe playlist\n            continue\n\n        # Create the attribute list\n        attribute_list = stream_inf_attr_list_for_track(track)\n\n        # Remove entries to not be included for I-Frame streams\n        for attr in I_FRAME_OMIT_ATTRS:\n            try:\n                del(attribute_list[attr])\n            except KeyError:\n                pass\n\n        # Add the URI\n        attribute_list['URI'] = iframe_uri\n\n        iframe_list_entries.append(\n            HLSPlaylistEntry.tag_entry(\n                'EXT-X-I-FRAME-STREAM-INF',\n                str(attribute_list)\n            )\n        )\n\n    if iframe_list_entries:\n        iframe_list_entries.append(HLSPlaylistEntry.comment_entry(''))\n\n    playlist_entries.extend(iframe_list_entries)\n\n    # Write an EXT-STREAM-INF for each rendition set\n    for track in video_tracks:\n        # create the base attribute list for the video track\n        al = stream_inf_attr_list_for_track(track)\n\n        # Create the uri\n        media_playlist_default_uri = \"{}.m3u8\".format(track.name)\n        try:\n            track_uri = track.metadata[FORMAT_METADATA_KEY].get(\n                'uri', media_playlist_default_uri\n            )\n        except KeyError:\n            track_uri = media_playlist_default_uri\n        uri_entry = HLSPlaylistEntry.uri_entry(track_uri)\n\n        # TODO: this will break when we have subtitle and CC tracks\n        added_entry = False\n        for audio_track in audio_tracks:\n            if track.name not in audio_track.metadata['linked_tracks']:\n                continue\n\n            # Write an entry for using these together\n            try:\n                audio_track_streaming_metadata = audio_track.metadata[\n                    STREAMING_METADATA_KEY\n                ]\n                aud_group = audio_track_streaming_metadata['group_id']\n                aud_codec = audio_track_streaming_metadata['codec']\n                aud_bandwidth = audio_track_streaming_metadata['bandwidth']\n            except KeyError:\n                raise TypeError(\n                    \"HLS audio tracks must have 'codec', 'group_id', and\"\n                    \" 'bandwidth' specified in metadata\"\n                )\n\n            combo_al = copy.copy(al)\n            combo_al['CODECS'] += ',{}'.format(aud_codec)\n            combo_al['AUDIO'] = aud_group\n            combo_al['BANDWIDTH'] += aud_bandwidth\n\n            entry = HLSPlaylistEntry.tag_entry(\n                'EXT-X-STREAM-INF',\n                str(combo_al)\n            )\n            playlist_entries.append(entry)\n            playlist_entries.append(uri_entry)\n\n            added_entry = True\n\n        if not added_entry:\n            # write out one simple entry\n            entry = HLSPlaylistEntry.tag_entry(\n                'EXT-X-STREAM-INF',\n                str(al)\n            )\n            playlist_entries.append(entry)\n            playlist_entries.append(uri_entry)\n\n        # add a break before the next grouping of entries\n        playlist_entries.append(HLSPlaylistEntry.comment_entry(''))\n\n    out_entries = [HLSPlaylistEntry.tag_entry(PLAYLIST_START_TAG, None)]\n\n    playlist_version = max(version_requirements)\n    playlist_version_entry = HLSPlaylistEntry.tag_entry(\n        PLAYLIST_VERSION_TAG,\n        str(playlist_version)\n    )\n\n    out_entries.append(playlist_version_entry)\n\n    out_entries += (\n        HLSPlaylistEntry.tag_entry(k, v) for k, v in header_tags.items()\n    )\n\n    # separate the header entries from the rest of the entries\n    out_entries.append(HLSPlaylistEntry.comment_entry(''))\n\n    out_entries += playlist_entries\n\n    playlist_string = '\\n'.join(\n        (str(entry) for entry in out_entries)\n    )\n\n    return playlist_string\n\n\nclass MediaPlaylistWriter():\n\n    def __init__(\n            self,\n            media_track,\n            min_seg_duration=None,\n            max_seg_duration=None\n    ):\n        # Default to one segment per fragment\n        if min_seg_duration is None:\n            min_seg_duration = otio.opentime.RationalTime(0, 1)\n        if max_seg_duration is None:\n            max_seg_duration = otio.opentime.RationalTime(0, 1)\n\n        self._min_seg_duration = min_seg_duration\n        self._max_seg_duration = max_seg_duration\n\n        self._playlist_entries = []\n        self._playlist_tags = {}\n\n        # Whenever an entry is added that has a minimum version requirement,\n        # we add that version to this set. The max value from this set is the\n        # playlist's version requirement\n        self._versions_used = set([1])\n\n        # TODO: detect rather than forcing version 7\n        self._versions_used.add(7)\n\n        # Start the build\n        self._build_playlist_with_track(media_track)\n\n    def _build_playlist_with_track(self, media_track):\n        \"\"\"\n        Executes methods to result in a fully populated _playlist_entries list\n        \"\"\"\n        self._copy_HLS_metadata(media_track)\n        self._setup_track_info(media_track)\n        self._add_segment_entries(media_track)\n        self._finalize_entries(media_track)\n\n    def _copy_HLS_metadata(self, media_track):\n        \"\"\"\n        Copies any metadata in the \"HLS\" namespace from the track to the\n        playlist-global tags\n        \"\"\"\n        # Grab any metadata provided on the otio\n        try:\n            track_metadata = media_track.metadata[FORMAT_METADATA_KEY]\n            self._playlist_tags.update(track_metadata)\n\n            # Remove the version tag from the track metadata, we'll compute\n            # based on what we write out\n            del(self._playlist_tags[PLAYLIST_VERSION_TAG])\n\n        except KeyError:\n            pass\n\n        # additionally remove metadata keys added for providing master\n        # playlist URIs\n        for key in ('uri', 'iframe_uri'):\n            try:\n                del(self._playlist_tags[key])\n            except KeyError:\n                pass\n\n    def _setup_track_info(self, media_track):\n        \"\"\"sets up playlist global metadata\"\"\"\n\n        # Setup the track start\n        if 'EXT-X-I-FRAMES-ONLY' in media_track.metadata.get(\n            FORMAT_METADATA_KEY,\n            {}\n        ):\n            # I-Frame playlists start at zero no matter what\n            track_start = 0\n        else:\n            # Pull the track num from the first clip, if provided\n            first_segment_streaming_metadata = media_track[0].metadata.get(\n                STREAMING_METADATA_KEY,\n                {}\n            )\n            track_start = first_segment_streaming_metadata.get(\n                SEQUENCE_NUM_KEY\n            )\n\n        # If we found a track start or one isn't already set in the\n        # metadata, create the tag for it.\n        if (\n            track_start is not None or\n            'EXT-X-MEDIA-SEQUENCE' not in self._playlist_tags\n        ):\n            # Choose a reasonable track start default\n            if track_start is None:\n                track_start = 1\n            self._playlist_tags['EXT-X-MEDIA-SEQUENCE'] = str(track_start)\n\n    def _add_map_entry(self, fragment):\n        \"\"\"adds an EXT-X-MAP entry from the given fragment\n\n        returns the added entry\n        \"\"\"\n\n        media_ref = fragment.media_reference\n\n        # Extract useful tag data\n        media_ref_streaming_metadata = media_ref.metadata[\n            STREAMING_METADATA_KEY\n        ]\n        uri = media_ref_streaming_metadata[INIT_URI_KEY]\n        seg_map_byterange_dict = media_ref_streaming_metadata.get(\n            INIT_BYTERANGE_KEY\n        )\n\n        # Create the attrlist\n        map_attr_list = AttributeList([\n            ('URI', uri),\n        ])\n\n        # Add the byterange if provided\n        if seg_map_byterange_dict is not None:\n            seg_map_byterange = Byterange.from_dict(seg_map_byterange_dict)\n            map_attr_list['BYTERANGE'] = str(seg_map_byterange)\n\n        # Construct the entry with the attrlist as the value\n        map_tag_str = str(map_attr_list)\n        entry = HLSPlaylistEntry.tag_entry(\"EXT-X-MAP\", map_tag_str)\n\n        self._playlist_entries.append(entry)\n\n        return entry\n\n    def _add_entries_for_segment_from_fragments(\n            self,\n            fragments,\n            omit_hls_keys=None,\n            is_iframe_playlist=False\n    ):\n        \"\"\"\n        For the given list of otio clips representing fragments in the mp4,\n        add playlist entries for single HLS segment.\n\n        :param fragments: (:clas:`list`) :class:`opentimelineio.schema.Clip`\n            objects to write as a contiguous segment.\n        :param omit_hls_keys: (:class:`list`) metadata keys from the original\n            \"HLS\" metadata namespeaces will not be passed through.\n        :param is_iframe_playlist: (:class:`bool`) If true, writes one segment\n            per fragment, otherwise writes all fragments as a single segment\n\n        :return: (:class:`list` the :class:`HLSPlaylistEntry` instances added\n            to the playlist\n        \"\"\"\n        if is_iframe_playlist:\n            entries = []\n            for fragment in fragments:\n                name = ''\n                fragment_range = Byterange.from_dict(\n                    fragment.media_reference.metadata[STREAMING_METADATA_KEY]\n                )\n\n                segment_tags = {}\n                frag_tags = fragment.media_reference.metadata.get(\n                    FORMAT_METADATA_KEY,\n                    {}\n                )\n                segment_tags.update(copy.deepcopy(frag_tags))\n\n                # scrub any metadata marked for omission\n                omit_hls_keys = omit_hls_keys or []\n                for key in omit_hls_keys:\n                    try:\n                        del(segment_tags[key])\n                    except KeyError:\n                        pass\n\n                segment_entries = entries_for_segment(\n                    fragment.media_reference.target_url,\n                    fragment.duration(),\n                    name,\n                    fragment_range,\n                    segment_tags\n                )\n                entries.extend(segment_entries)\n\n            self._playlist_entries.extend(entries)\n            return entries\n\n        segment_tags = {}\n        for fragment in fragments:\n            frag_tags = fragment.media_reference.metadata.get(\n                FORMAT_METADATA_KEY,\n                {}\n            )\n            segment_tags.update(copy.deepcopy(frag_tags))\n\n        # scrub any metadata marked for omission\n        omit_hls_keys = omit_hls_keys or []\n        for key in omit_hls_keys:\n            try:\n                del(segment_tags[key])\n            except KeyError:\n                pass\n\n        # Calculate the byterange for the segment (if byteranges are specified)\n        first_ref = fragments[0].media_reference\n        first_ref_streaming_md = first_ref.metadata[STREAMING_METADATA_KEY]\n        if 'byte_offset' in first_ref_streaming_md and len(fragments) == 1:\n            segment_range = Byterange.from_dict(first_ref_streaming_md)\n        elif 'byte_offset' in first_ref_streaming_md:\n            # Find the byterange encapsulating everything\n            last_ref = fragments[-1].media_reference\n            last_ref_streaming_md = last_ref.metadata[STREAMING_METADATA_KEY]\n            first_range = Byterange.from_dict(first_ref_streaming_md)\n            last_range = Byterange.from_dict(last_ref_streaming_md)\n\n            segment_offset = first_range.offset\n            segment_end = (last_range.offset + last_range.count)\n            segment_count = segment_end - segment_offset\n            segment_range = Byterange(segment_count, segment_offset)\n        else:\n            segment_range = None\n\n        uri = fragments[0].media_reference.target_url\n\n        # calculate the combined duration\n        segment_duration = fragments[0].duration()\n        for frag in fragments[1:]:\n            segment_duration += frag.duration()\n\n        # TODO: Determine how to pass a segment name in\n        segment_name = ''\n        segment_entries = entries_for_segment(\n            uri,\n            segment_duration,\n            segment_name,\n            segment_range,\n            segment_tags\n        )\n\n        self._playlist_entries.extend(segment_entries)\n        return segment_entries\n\n    def _fragments_have_same_map(self, fragment, following_fragment):\n        \"\"\"\n        Given fragment and following_fragment, returns whether or not their\n        initialization data is the same (what becomes EXT-X-MAP)\n        \"\"\"\n        media_ref = fragment.media_reference\n        media_ref_streaming_md = media_ref.metadata.get(\n            STREAMING_METADATA_KEY,\n            {}\n        )\n        following_ref = following_fragment.media_reference\n        following_ref_streaming_md = following_ref.metadata.get(\n            STREAMING_METADATA_KEY,\n            {}\n        )\n        # Check the init file\n        init_uri = media_ref_streaming_md.get(INIT_URI_KEY)\n        following_init_uri = media_ref_streaming_md.get(INIT_URI_KEY)\n        if init_uri != following_init_uri:\n            return False\n\n        # Check the init byterange\n        init_dict = media_ref_streaming_md.get(INIT_BYTERANGE_KEY)\n        following_init_dict = following_ref_streaming_md.get(\n            INIT_BYTERANGE_KEY\n        )\n\n        dummy_range = Byterange(0, 0)\n        init_range = (\n            Byterange.from_dict(init_dict) if init_dict else dummy_range\n        )\n        following_range = (\n            Byterange.from_dict(following_init_dict)\n            if following_init_dict else dummy_range\n        )\n\n        if init_range != following_range:\n            return False\n\n        return True\n\n    def _fragments_are_contiguous(self, fragment, following_fragment):\n        \"\"\" Given fragment and following_fragment (otio clips) returns whether\n        or not they are contiguous.\n\n        To be contiguous the fragments must:\n        1. have the same file URL\n        2. have the same initialization data (what becomes EXT-X-MAP)\n        3. be adjacent in the file (follwoing_fragment's first byte directly\n           follows fragment's last byte)\n\n        Returns True if following_fragment is contiguous from fragment\n        \"\"\"\n        # Fragments are contiguous if:\n        # 1. They have the file url\n        # 2. They have the same map info\n        # 3. Their byte ranges are contiguous\n        media_ref = fragment.media_reference\n        media_ref_streaming_md = media_ref.metadata.get(\n            STREAMING_METADATA_KEY,\n            {}\n        )\n        following_ref = following_fragment.media_reference\n        following_ref_streaming_md = following_ref.metadata.get(\n            STREAMING_METADATA_KEY,\n            {}\n        )\n        if media_ref.target_url != following_ref.target_url:\n            return False\n\n        if (\n            media_ref_streaming_md.get(INIT_URI_KEY) !=\n            following_ref_streaming_md.get(INIT_URI_KEY)\n        ):\n            return False\n\n        if not self._fragments_have_same_map(fragment, following_fragment):\n            return False\n\n        # Check if fragments are contiguous in file\n        try:\n            frag_end = (\n                media_ref_streaming_md['byte_offset'] +\n                media_ref_streaming_md['byte_count']\n            )\n            if frag_end != following_ref_streaming_md['byte_offset']:\n                return False\n        except KeyError:\n            return False\n\n        # since we haven't returned yet, all checks must have passed!\n        return True\n\n    def _add_segment_entries(self, media_track):\n        \"\"\"given a media track, generates the segment entries\"\"\"\n\n        # Determine whether or not this is an I-Frame playlist\n        track_hls_metadata = media_track.metadata.get('HLS')\n        is_iframe_playlist = 'EXT-X-I-FRAMES-ONLY' in track_hls_metadata\n\n        # Make a list copy of the fragments\n        fragments = [clip for clip in media_track]\n\n        segment_durations = []\n        previous_fragment = None\n        map_changed = True\n        while fragments:\n            # There should be at least one fragment per segment\n            frag_it = iter(fragments)\n            first_frag = next(frag_it)\n            gathered_fragments = [first_frag]\n            gathered_duration = first_frag.duration()\n\n            # Determine this segment will need a new EXT-X-MAP entry\n            map_changed = (\n                True if previous_fragment is None else\n                not self._fragments_have_same_map(\n                    previous_fragment,\n                    first_frag\n                )\n            )\n\n            # Iterate through the remaining fragments until a discontinuity\n            # is found, our time limit is met, or we add all the fragments to\n            # the segment\n            for fragment in frag_it:\n                # Determine whther or not the fragments are contiguous\n                previous_fragment = gathered_fragments[-1]\n                contiguous = self._fragments_are_contiguous(\n                    previous_fragment,\n                    fragment\n                )\n\n                # Determine if we've hit our segment time conditions\n                new_duration = gathered_duration + fragment.duration()\n                segment_full = (\n                    gathered_duration >= self._min_seg_duration or\n                    new_duration > self._max_seg_duration\n                )\n\n                # End condition met, cut the segment\n                if not contiguous or segment_full:\n                    break\n\n                # Include the fragment\n                gathered_duration = new_duration\n                gathered_fragments.append(fragment)\n\n            # Write out the segment and start the next\n            start_fragment = gathered_fragments[0]\n\n            # If the map for this segment was a change, write it\n            if map_changed:\n                self._add_map_entry(start_fragment)\n\n            # add the entries for the segment. Omit any EXT-X-MAP metadata\n            # that may have come in from reading a file (we're updating)\n            self._add_entries_for_segment_from_fragments(\n                gathered_fragments,\n                omit_hls_keys=('EXT-X-MAP'),\n                is_iframe_playlist=is_iframe_playlist\n            )\n\n            duration_seconds = otio.opentime.to_seconds(gathered_duration)\n            segment_durations.append(duration_seconds)\n\n            # in the next iteration, start where we left off\n            fragments = fragments[len(gathered_fragments):]\n\n        # Set the max segment duration\n        max_duration = round(max(segment_durations))\n        self._playlist_tags['EXT-X-TARGETDURATION'] = str(int(max_duration))\n\n    def _finalize_entries(self, media_track):\n        \"\"\"Does final wrap-up of playlist entries\"\"\"\n\n        self._playlist_tags['EXT-X-PLAYLIST-TYPE'] = 'VOD'\n\n        # add the end\n        end_entry = HLSPlaylistEntry.tag_entry(PLAYLIST_END_TAG)\n        self._playlist_entries.append(end_entry)\n\n        # find the maximum HLS feature version we've used\n        playlist_version = max(self._versions_used)\n        playlist_version_entry = HLSPlaylistEntry.tag_entry(\n            PLAYLIST_VERSION_TAG,\n            str(playlist_version)\n        )\n\n        # now that we know what was used, let's prepend the header\n        playlist_header_entries = [\n            HLSPlaylistEntry.tag_entry(PLAYLIST_START_TAG),\n            playlist_version_entry\n        ]\n\n        # add in the rest of the header entries in a deterministic order\n        playlist_header_entries += (\n            HLSPlaylistEntry.tag_entry(k, v)\n            for k, v in sorted(self._playlist_tags.items(), key=lambda i: i[0])\n        )\n\n        # Prepend the entries with the header entries\n        self._playlist_entries = (\n            playlist_header_entries + self._playlist_entries\n        )\n\n    def playlist_string(self):\n        \"\"\"Returns the string representation of the playlist entries\"\"\"\n\n        return '\\n'.join(\n            (str(entry) for entry in self._playlist_entries)\n        )\n\n# Public interface\n\n\ndef read_from_string(input_str):\n    \"\"\"Adapter entry point for reading.\"\"\"\n\n    parser = HLSPlaylistParser(input_str)\n    return parser.timeline\n\n\ndef write_to_string(input_otio):\n    \"\"\"Adapter entry point for writing.\"\"\"\n\n    if len(input_otio.tracks) == 0:\n        return None\n\n    # Determine whether we should write a media or master playlist\n    try:\n        write_master = input_otio.metadata['HLS']['master_playlist']\n    except KeyError:\n        # If no explicit directive, infer\n        write_master = (len(input_otio.tracks) > 1)\n\n    if write_master:\n        return master_playlist_to_string(input_otio)\n    else:\n        media_track = input_otio.tracks[0]\n        track_streaming_md = input_otio.metadata.get(\n            STREAMING_METADATA_KEY,\n            {}\n        )\n        min_seg_duration = track_streaming_md.get('min_segment_duration')\n        max_seg_duration = track_streaming_md.get('max_segment_duration')\n\n        writer = MediaPlaylistWriter(\n            media_track,\n            min_seg_duration,\n            max_seg_duration\n        )\n        return writer.playlist_string()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/maya_sequencer.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"Maya Sequencer Adapter Harness\"\"\"\n\nimport os\nimport subprocess\n\nfrom .. import adapters\n\n\ndef write_to_file(input_otio, filepath):\n    if \"OTIO_MAYA_PYTHON_BIN\" not in os.environ:\n        raise RuntimeError(\n            \"'OTIO_MAYA_PYTHON_BIN' not set, please set this to path to \"\n            \"mayapy within the Maya installation.\"\n        )\n    maya_python_path = os.environ[\"OTIO_MAYA_PYTHON_BIN\"]\n    if not os.path.exists(maya_python_path):\n        raise RuntimeError(\n            'Cannot access file at OTIO_MAYA_PYTHON_BIN: \"{}\"'.format(\n                maya_python_path\n            )\n        )\n    if os.path.isdir(maya_python_path):\n        raise RuntimeError(\n            \"OTIO_MAYA_PYTHON_BIN contains a path to a directory, not to an \"\n            \"executable file: {}\".format(maya_python_path)\n        )\n\n    input_data = adapters.write_to_string(input_otio, \"otio_json\")\n\n    os.environ['PYTHONPATH'] = (\n        os.pathsep.join(\n            [\n                os.environ.setdefault('PYTHONPATH', ''),\n                os.path.dirname(__file__)\n            ]\n        )\n    )\n\n    proc = subprocess.Popen(\n        [\n            os.environ[\"OTIO_MAYA_PYTHON_BIN\"],\n            '-m',\n            'extern_maya_sequencer',\n            'write',\n            filepath\n        ],\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        stdin=subprocess.PIPE,\n        env=os.environ\n    )\n    proc.stdin.write(input_data)\n    out, err = proc.communicate()\n\n    if proc.returncode:\n        raise RuntimeError(\n            \"ERROR: extern_maya_sequencer (called through the maya sequencer \"\n            \"file adapter) failed. stderr output: \" + err\n        )\n\n\ndef read_from_file(filepath):\n    if \"OTIO_MAYA_PYTHON_BIN\" not in os.environ:\n        raise RuntimeError(\n            \"'OTIO_MAYA_PYTHON_BIN' not set, please set this to path to \"\n            \"mayapy within the Maya installation.\"\n        )\n\n    os.environ['PYTHONPATH'] = (\n        os.pathsep.join(\n            [\n                os.environ.setdefault('PYTHONPATH', ''),\n                os.path.dirname(__file__)\n            ]\n        )\n    )\n\n    proc = subprocess.Popen(\n        [\n            os.environ[\"OTIO_MAYA_PYTHON_BIN\"],\n            '-m',\n            'extern_maya_sequencer',\n            'read',\n            filepath\n        ],\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        stdin=subprocess.PIPE,\n        env=os.environ\n    )\n    out, err = proc.communicate()\n\n    # maya probably puts a bunch of crap on the stdout\n    sentinel_str = \"OTIO_JSON_BEGIN\\n\"\n    end_sentinel_str = \"\\nOTIO_JSON_END\\n\"\n    start = out.find(sentinel_str)\n    end = out.find(end_sentinel_str)\n    result = adapters.read_from_string(\n        out[start + len(sentinel_str):end],\n        \"otio_json\"\n    )\n\n    if proc.returncode:\n        raise RuntimeError(\n            \"ERROR: extern_maya_sequencer (called through the maya sequencer \"\n            \"file adapter) failed. stderr output: \" + err\n        )\n    return result\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/rv.py",
    "content": "#\n# Copyright 2017 Pixar Animation Studios\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"RvSession Adapter harness\"\"\"\n\nimport subprocess\nimport os\nimport copy\n\nfrom .. import adapters\n\n\ndef write_to_file(input_otio, filepath):\n    if \"OTIO_RV_PYTHON_BIN\" not in os.environ:\n        raise RuntimeError(\n            \"'OTIO_RV_PYTHON_BIN' not set, please set this to path to \"\n            \"py-interp within the RV installation.\"\n        )\n\n    if \"OTIO_RV_PYTHON_LIB\" not in os.environ:\n        raise RuntimeError(\n            \"'OTIO_RV_PYTHON_LIB' not set, please set this to path to python \"\n            \"directory within the RV installation.\"\n        )\n\n    input_data = adapters.write_to_string(input_otio, \"otio_json\")\n\n    base_environment = copy.deepcopy(os.environ)\n\n    base_environment['PYTHONPATH'] = (\n        os.pathsep.join(\n            [\n                base_environment.setdefault('PYTHONPATH', ''),\n                os.path.dirname(__file__)\n            ]\n        )\n    )\n\n    proc = subprocess.Popen(\n        [\n            base_environment[\"OTIO_RV_PYTHON_BIN\"],\n            '-m',\n            'extern_rv',\n            filepath\n        ],\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        stdin=subprocess.PIPE,\n        env=base_environment\n    )\n    proc.stdin.write(input_data)\n    out, err = proc.communicate()\n\n    if out.strip():\n        print(\"stdout: {}\".format(out))\n    if err.strip():\n        print(\"stderr: {}\".format(err))\n\n    if proc.returncode:\n        raise RuntimeError(\n            \"ERROR: extern_rv (called through the rv session file adapter) \"\n            \"failed. stderr output: \" + err\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/opentimelineio_contrib/adapters/xges.py",
    "content": "#\n# Copyright (C) 2019 Igalia S.L\n#\n# Licensed under the Apache License, Version 2.0 (the \"Apache License\")\n# with the following modification; you may not use this file except in\n# compliance with the Apache License and the following modification to it:\n# Section 6. Trademarks. is deleted and replaced with:\n#\n# 6. Trademarks. This License does not grant permission to use the trade\n#    names, trademarks, service marks, or product names of the Licensor\n#    and its affiliates, except as required to comply with Section 4(c) of\n#    the License and to reproduce the content of the NOTICE file.\n#\n# You may obtain a copy of the Apache License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the Apache License with the above modification is\n# distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied. See the Apache License for the specific\n# language governing permissions and limitations under the Apache License.\n#\n\n\"\"\"OpenTimelineIO GStreamer Editing Services XML Adapter. \"\"\"\nimport re\nimport unittest\n\nfrom decimal import Decimal\nfrom fractions import Fraction\nfrom xml.etree import cElementTree\nfrom xml.dom import minidom\nimport opentimelineio as otio\n\nMETA_NAMESPACE = \"XGES\"\n\n\nFRAMERATE_FRAMEDURATION = {23.98: \"24000/1001\",\n                           24: \"600/25\",\n                           25: \"25/1\",\n                           29.97: \"30000/1001\",\n                           30: \"30/1\",\n                           50: \"50/1\",\n                           59.94: \"60000/1001\",\n                           60: \"60/1\"}\n\n\nTRANSITION_MAP = {\n    \"crossfade\": otio.schema.TransitionTypes.SMPTE_Dissolve\n}\n# Two way map\nTRANSITION_MAP.update(dict([(v, k) for k, v in TRANSITION_MAP.items()]))\n\n\nclass GstParseError(otio.exceptions.OTIOError):\n    pass\n\n\nclass GstStructure(object):\n    \"\"\"\n    GstStructure parser with a \"dictionary\" like API.\n    \"\"\"\n    UNESCAPE = re.compile(r'(?<!\\\\)\\\\(.)')\n    INT_TYPES = \"\".join(\n        (\"int\", \"uint\", \"int8\", \"uint8\", \"int16\",\n         \"uint16\", \"int32\", \"uint32\", \"int64\", \"uint64\")\n    )\n\n    def __init__(self, text):\n        self.text = text\n        self.modified = False\n        self.name, self.types, self.values = GstStructure._parse(text + \";\")\n\n    def __repr__(self):\n        if not self.modified:\n            return self.text\n\n        res = self.name\n        for key, value in self.values.items():\n            value_type = self.types[key]\n            res += ', %s=(%s)\"%s\"' % (key, value_type, self.escape(value))\n        res += ';'\n\n        return res\n\n    def __getitem__(self, key):\n        return self.values[key]\n\n    def set(self, key, value_type, value):\n        if self.types.get(key) == value_type and self.values.get(key) == value:\n            return\n\n        self.modified = True\n        self.types[key] = value_type\n        self.values[key] = value\n\n    def get(self, key, default=None):\n        return self.values.get(key, default)\n\n    @staticmethod\n    def _find_eos(s):\n        # find next '\"' without preceeding '\\'\n        line = 0\n        while 1:  # faster than regexp for '[^\\\\]\\\"'\n            p = s.index('\"')\n            line += p + 1\n            if s[p - 1] != '\\\\':\n                return line\n            s = s[(p + 1):]\n        return -1\n\n    @staticmethod\n    def escape(s):\n        # XXX: The unicode type doesn't exist in Python 3 (all strings are unicode)\n        # so we have to use type(u\"\") which works in both Python 2 and 3.\n        if type(s) not in (str, type(u\"\")):\n            return s\n        return s.replace(\" \", \"\\\\ \")\n\n    @staticmethod\n    def _parse(s):\n        in_string = s\n        types = {}\n        values = {}\n        scan = True\n        # parse id\n        p = s.find(',')\n        if p == -1:\n            try:\n                p = s.index(';')\n            except ValueError:\n                p = len(s)\n            scan = False\n        name = s[:p]\n        # parse fields\n        while scan:\n            comma_space_it = p\n            # skip 'name, ' / 'value, '\n            while s[comma_space_it] in [' ', ',']:\n                comma_space_it += 1\n            s = s[comma_space_it:]\n            p = s.index('=')\n            k = s[:p]\n            if not s[p + 1] == '(':\n                raise ValueError(\"In %s position: %d\" % (in_string, p))\n            s = s[(p + 2):]  # skip 'key=('\n            p = s.index(')')\n            t = s[:p]\n            s = s[(p + 1):]  # skip 'type)'\n\n            if s[0] == '\"':\n                s = s[1:]  # skip '\"'\n                p = GstStructure._find_eos(s)\n                if p == -1:\n                    raise ValueError\n                v = s[:(p - 1)]\n                if s[p] == ';':\n                    scan = False\n                # unescape \\., but not \\\\. (using a backref)\n                # need a reverse for re.escape()\n                v = v.replace('\\\\\\\\', '\\\\')\n                v = GstStructure.UNESCAPE.sub(r'\\1', v)\n            else:\n                p = s.find(',')\n                if p == -1:\n                    p = s.index(';')\n                    scan = False\n                v = s[:p]\n\n            if t == 'structure':\n                v = GstStructure(v)\n            elif t == 'string' and len(v) and v[0] == '\"':\n                v = v[1:-1]\n            elif t == 'boolean':\n                v = (v == '1')\n            elif t in GstStructure.INT_TYPES:\n                v = int(v)\n            types[k] = t\n            values[k] = v\n\n        return (name, types, values)\n\n\nclass GESTrackType:\n    UNKNOWN = 1 << 0\n    AUDIO = 1 << 1\n    VIDEO = 1 << 2\n    TEXT = 1 << 3\n    CUSTOM = 1 << 4\n\n    @staticmethod\n    def to_otio_type(_type):\n        if _type == GESTrackType.AUDIO:\n            return otio.schema.TrackKind.Audio\n        elif _type == GESTrackType.VIDEO:\n            return otio.schema.TrackKind.Video\n\n        raise GstParseError(\"Can't translate track type %s\" % _type)\n\n\nGST_CLOCK_TIME_NONE = 18446744073709551615\nGST_SECOND = 1000000000\n\n\ndef to_gstclocktime(rational_time):\n    \"\"\"\n    This converts a RationalTime object to a GstClockTime\n\n    Args:\n        rational_time (RationalTime): This is a RationalTime object\n\n    Returns:\n        int: A time in nanosecond\n    \"\"\"\n\n    return int(rational_time.value_rescaled_to(1) * GST_SECOND)\n\n\ndef get_from_structure(xmlelement, fieldname, default=None, attribute=\"properties\"):\n    structure = GstStructure(xmlelement.get(attribute, attribute))\n    return structure.get(fieldname, default)\n\n\nclass XGES:\n    \"\"\"\n    This object is responsible for knowing how to convert an xGES\n    project into an otio timeline\n    \"\"\"\n\n    def __init__(self, xml_string):\n        self.xges_xml = cElementTree.fromstring(xml_string)\n        self.rate = 25\n\n    def _set_rate_from_timeline(self, timeline):\n        metas = GstStructure(timeline.attrib.get(\"metadatas\", \"metadatas\"))\n        framerate = metas.get(\"framerate\")\n        if framerate:\n            rate = Fraction(framerate)\n        else:\n            video_track = timeline.find(\"./track[@track-type='4']\")\n            rate = None\n            if video_track is not None:\n                properties = GstStructure(\n                    video_track.get(\"properties\", \"properties;\"))\n                restriction_caps = GstStructure(properties.get(\n                    \"restriction-caps\", \"restriction-caps\"))\n                rate = restriction_caps.get(\"framerate\")\n\n        if rate is None:\n            return\n\n        self.rate = float(Fraction(rate))\n        if self.rate == int(self.rate):\n            self.rate = int(self.rate)\n        else:\n            self.rate = float(round(Decimal(self.rate), 2))\n\n    def to_rational_time(self, ns_timestamp):\n        \"\"\"\n        This converts a GstClockTime value to an otio RationalTime object\n\n        Args:\n            ns_timestamp (int): This is a GstClockTime value (nanosecond absolute value)\n\n        Returns:\n            RationalTime: A RationalTime object\n        \"\"\"\n        return otio.opentime.RationalTime(round(int(ns_timestamp) /\n                                          (GST_SECOND / self.rate)), self.rate)\n\n    def to_otio(self):\n        \"\"\"\n        Convert an xges to an otio\n\n        Returns:\n            OpenTimeline: An OpenTimeline Timeline object\n        \"\"\"\n\n        project = self.xges_xml.find(\"./project\")\n        metas = GstStructure(project.attrib.get(\"metadatas\", \"metadatas\"))\n        otio_project = otio.schema.SerializableCollection(\n            name=metas.get('name'),\n            metadata={\n                META_NAMESPACE: {\"metadatas\": project.attrib.get(\n                    \"metadatas\", \"metadatas\")}\n            }\n        )\n        timeline = project.find(\"./timeline\")\n        self._set_rate_from_timeline(timeline)\n\n        otio_timeline = otio.schema.Timeline(\n            name=metas.get('name', \"unnamed\"),\n            metadata={\n                META_NAMESPACE: {\n                    \"metadatas\": timeline.attrib.get(\"metadatas\", \"metadatas\"),\n                    \"properties\": timeline.attrib.get(\"properties\", \"properties\")\n                }\n            }\n        )\n\n        all_names = set()\n        self._add_layers(timeline, otio_timeline, all_names)\n        otio_project.append(otio_timeline)\n\n        return otio_project\n\n    def _add_layers(self, timeline, otio_timeline, all_names):\n        for layer in timeline.findall(\"./layer\"):\n            tracks = self._build_tracks_from_layer_clips(layer, all_names)\n            otio_timeline.tracks.extend(tracks)\n\n    def _get_clips_for_type(self, clips, track_type):\n        if not clips:\n            return False\n\n        clips_for_type = []\n        for clip in clips:\n            if int(clip.attrib['track-types']) & track_type:\n                clips_for_type.append(clip)\n\n        return clips_for_type\n\n    def _build_tracks_from_layer_clips(self, layer, all_names):\n        all_clips = layer.findall('./clip')\n\n        tracks = []\n        for track_type in [GESTrackType.VIDEO, GESTrackType.AUDIO]:\n            clips = self._get_clips_for_type(all_clips, track_type)\n            if not clips:\n                continue\n\n            track = otio.schema.Track()\n            track.kind = GESTrackType.to_otio_type(track_type)\n            self._add_clips_in_track(clips, track, all_names)\n\n            tracks.append(track)\n\n        return tracks\n\n    def _add_clips_in_track(self, clips, track, all_names):\n        for clip in clips:\n            otio_clip = self._create_otio_clip(clip, all_names)\n            if otio_clip is None:\n                continue\n\n            clip_offset = self.to_rational_time(int(clip.attrib['start']))\n            if clip_offset > track.duration():\n                track.append(\n                    self._create_otio_gap(\n                        0,\n                        (clip_offset - track.duration())\n                    )\n                )\n\n            track.append(otio_clip)\n\n        return track\n\n    def _get_clip_name(self, clip, all_names):\n        i = 0\n        tmpname = name = clip.get(\"name\", GstStructure(\n                                  clip.get(\"properties\", \"properties;\")).get(\"name\"))\n        while True:\n            if tmpname not in all_names:\n                all_names.add(tmpname)\n                return tmpname\n\n            i += 1\n            tmpname = name + '_%d' % i\n\n    def _create_otio_transition(self, clip, all_names):\n        start = self.to_rational_time(clip.attrib[\"start\"])\n        end = start + self.to_rational_time(clip.attrib[\"duration\"])\n        cut_point = otio.opentime.RationalTime((end.value - start.value) /\n                                               2, start.rate)\n\n        return otio.schema.Transition(\n            name=self._get_clip_name(clip, all_names),\n            transition_type=TRANSITION_MAP.get(\n                clip.attrib[\"asset-id\"], otio.schema.TransitionTypes.Custom\n            ),\n            in_offset=cut_point,\n            out_offset=cut_point,\n        )\n\n    def _create_otio_uri_clip(self, clip, all_names):\n        source_range = otio.opentime.TimeRange(\n            start_time=self.to_rational_time(clip.attrib[\"inpoint\"]),\n            duration=self.to_rational_time(clip.attrib[\"duration\"]),\n        )\n\n        otio_clip = otio.schema.Clip(\n            name=self._get_clip_name(clip, all_names),\n            source_range=source_range,\n            media_reference=self._reference_from_id(\n                clip.get(\"asset-id\"), clip.get(\"type-name\")),\n        )\n\n        return otio_clip\n\n    def _create_otio_clip(self, clip, all_names):\n        otio_clip = None\n\n        if clip.get(\"type-name\") == \"GESTransitionClip\":\n            otio_clip = self._create_otio_transition(clip, all_names)\n        elif clip.get(\"type-name\") == \"GESUriClip\":\n            otio_clip = self._create_otio_uri_clip(clip, all_names)\n\n        if otio_clip is None:\n            print(\"Could not represent: %s\" % clip.attrib)\n            return None\n\n        otio_clip.metadata[META_NAMESPACE] = {\n            \"properties\": clip.get(\"properties\", \"properties;\"),\n            \"metadatas\": clip.get(\"metadatas\", \"metadatas;\"),\n        }\n\n        return otio_clip\n\n    def _create_otio_gap(self, start, duration):\n        source_range = otio.opentime.TimeRange(\n            start_time=otio.opentime.RationalTime(start),\n            duration=duration\n        )\n        return otio.schema.Gap(source_range=source_range)\n\n    def _reference_from_id(self, asset_id, asset_type=\"GESUriClip\"):\n        asset = self._asset_by_id(asset_id, asset_type)\n        if asset is None:\n            return None\n        if not asset.get(\"id\", \"\"):\n            return otio.schema.MissingReference()\n\n        duration = GST_CLOCK_TIME_NONE\n        if asset_type == \"GESUriClip\":\n            duration = get_from_structure(asset, \"duration\", duration)\n\n        available_range = otio.opentime.TimeRange(\n            start_time=self.to_rational_time(0),\n            duration=self.to_rational_time(duration)\n        )\n        ref = otio.schema.ExternalReference(\n            target_url=asset.get(\"id\"),\n            available_range=available_range\n        )\n\n        ref.metadata[META_NAMESPACE] = {\n            \"properties\": asset.get(\"properties\"),\n            \"metadatas\": asset.get(\"metadatas\"),\n        }\n\n        return ref\n\n    # --------------------\n    # search helpers\n    # --------------------\n    def _asset_by_id(self, asset_id, asset_type):\n        return self.xges_xml.find(\n            \"./project/ressources/asset[@id='{}'][@extractable-type-name='{}']\".format(\n                asset_id, asset_type)\n        )\n\n    def _timeline_element_by_name(self, timeline, name):\n        for clip in timeline.findall(\"./layer/clip\"):\n            if get_from_structure(clip, 'name') == name:\n                return clip\n\n        return None\n\n\nclass XGESOtio:\n\n    def __init__(self, input_otio):\n        self.container = input_otio\n        self.rate = 25\n\n    def _insert_new_sub_element(self, into_parent, tag, attrib=None, text=''):\n        elem = cElementTree.SubElement(into_parent, tag, **attrib or {})\n        elem.text = text\n        return elem\n\n    def _get_element_properties(self, element):\n        return element.metadata.get(META_NAMESPACE, {}).get(\"properties\", \"properties;\")\n\n    def _get_element_metadatas(self, element):\n        return element.metadata.get(META_NAMESPACE,\n                                    {\"GES\": {}}).get(\"metadatas\", \"metadatas;\")\n\n    def _serialize_ressource(self, ressources, ressource, asset_type):\n        if isinstance(ressource, otio.schema.MissingReference):\n            return\n\n        if ressources.find(\"./asset[@id='%s'][@extractable-type-name='%s']\" % (\n                ressource.target_url, asset_type)) is not None:\n            return\n\n        properties = GstStructure(self._get_element_properties(ressource))\n        if properties.get('duration') is None:\n            properties.set('duration', 'guin64',\n                           to_gstclocktime(ressource.available_range.duration))\n\n        self._insert_new_sub_element(\n            ressources, 'asset',\n            attrib={\n                \"id\": ressource.target_url,\n                \"extractable-type-name\": 'GESUriClip',\n                \"properties\": str(properties),\n                \"metadatas\": self._get_element_metadatas(ressource),\n            }\n        )\n\n    def _get_transition_times(self, offset, otio_transition):\n        rational_offset = otio.opentime.RationalTime(\n            round(int(offset) / (GST_SECOND / self.rate)),\n            self.rate\n        )\n        start = rational_offset - otio_transition.in_offset\n        end = rational_offset + otio_transition.out_offset\n\n        return 0, to_gstclocktime(start), to_gstclocktime(end - start)\n\n    def _serialize_clip(\n            self,\n            otio_track,\n            layer,\n            layer_priority,\n            ressources,\n            otio_clip,\n            clip_id,\n            offset\n    ):\n\n        # FIXME - Figure out a proper way to determine clip type!\n        asset_id = \"GESTitleClip\"\n        asset_type = \"GESTitleClip\"\n\n        if isinstance(otio_clip, otio.schema.Transition):\n            asset_type = \"GESTransitionClip\"\n            asset_id = TRANSITION_MAP.get(otio_clip.transition_type, \"crossfade\")\n            inpoint, offset, duration = self._get_transition_times(offset, otio_clip)\n        else:\n            inpoint = to_gstclocktime(otio_clip.source_range.start_time)\n            duration = to_gstclocktime(otio_clip.source_range.duration)\n\n            if not isinstance(otio_clip.media_reference, otio.schema.MissingReference):\n                asset_id = otio_clip.media_reference.target_url\n                asset_type = \"GESUriClip\"\n\n            self._serialize_ressource(ressources, otio_clip.media_reference,\n                                      asset_type)\n\n        if otio_track.kind == otio.schema.TrackKind.Audio:\n            track_types = GESTrackType.AUDIO\n        elif otio_track.kind == otio.schema.TrackKind.Video:\n            track_types = GESTrackType.VIDEO\n        else:\n            raise ValueError(\"Unhandled track type: %s\" % otio_track.kind)\n\n        properties = otio_clip.metadata.get(\n            META_NAMESPACE,\n            {\n                \"properties\": 'properties, name=(string)\"%s\"' % (\n                    GstStructure.escape(otio_clip.name)\n                )\n            }).get(\"properties\")\n        return self._insert_new_sub_element(\n            layer, 'clip',\n            attrib={\n                \"id\": str(clip_id),\n                \"properties\": properties,\n                \"asset-id\": str(asset_id),\n                \"type-name\": str(asset_type),\n                \"track-types\": str(track_types),\n                \"layer-priority\": str(layer_priority),\n                \"start\": str(offset),\n                \"rate\": '0',\n                \"inpoint\": str(inpoint),\n                \"duration\": str(duration),\n                \"metadatas\": self._get_element_metadatas(otio_clip),\n            }\n        )\n\n    def _serialize_tracks(self, timeline, otio_timeline):\n        audio_vals = (\n            'properties',\n            'restriction-caps=(string)audio/x-raw(ANY)',\n            'framerate=(GstFraction)1',\n            otio_timeline.duration().rate\n        )\n\n        properties = '%s, %s,%s/%s' % audio_vals\n        self._insert_new_sub_element(\n            timeline, 'track',\n            attrib={\n                \"caps\": \"audio/x-raw(ANY)\",\n                \"track-type\": '2',\n                'track-id': '0',\n                'properties': properties\n            }\n        )\n\n        video_vals = (\n            'properties',\n            'restriction-caps=(string)video/x-raw(ANY)',\n            'framerate=(GstFraction)1',\n            otio_timeline.duration().rate\n        )\n\n        properties = '%s, %s,%s/%s' % video_vals\n        for otio_track in otio_timeline.tracks:\n            if otio_track.kind == otio.schema.TrackKind.Video:\n                self._insert_new_sub_element(\n                    timeline, 'track',\n                    attrib={\n                        \"caps\": \"video/x-raw(ANY)\",\n                        \"track-type\": '4',\n                        'track-id': '1',\n                        'properties': properties,\n                    }\n                )\n\n                return\n\n    def _serialize_layer(self, timeline, layers, layer_priority):\n        if layer_priority not in layers:\n            layers[layer_priority] = self._insert_new_sub_element(\n                timeline, 'layer',\n                attrib={\n                    \"priority\": str(layer_priority),\n                }\n            )\n\n    def _serialize_timeline_element(self, timeline, layers, layer_priority,\n                                    offset, otio_track, otio_element,\n                                    ressources, all_clips):\n        self._serialize_layer(timeline, layers, layer_priority)\n        layer = layers[layer_priority]\n        if isinstance(otio_element, (otio.schema.Clip, otio.schema.Transition)):\n            element = self._serialize_clip(otio_track, layer, layer_priority,\n                                           ressources, otio_element,\n                                           str(len(all_clips)), offset)\n            all_clips.add(element)\n            if isinstance(otio_element, otio.schema.Transition):\n                # Make next clip overlap\n                return int(element.get(\"start\")) - offset\n        elif not isinstance(otio_element, otio.schema.Gap):\n            print(\"FIXME: Add support for %s\" % type(otio_element))\n            return 0\n\n        return to_gstclocktime(otio_element.source_range.duration)\n\n    def _make_element_names_unique(self, all_names, otio_element):\n        if isinstance(otio_element, otio.schema.Gap):\n            return\n\n        if not isinstance(otio_element, otio.schema.Track):\n            i = 0\n            name = otio_element.name\n            while True:\n                if name not in all_names:\n                    otio_element.name = name\n                    break\n\n                i += 1\n                name = otio_element.name + '_%d' % i\n            all_names.add(otio_element.name)\n\n        if isinstance(otio_element, (otio.schema.Stack, otio.schema.Track)):\n            for sub_element in otio_element:\n                self._make_element_names_unique(all_names, sub_element)\n\n    def _make_timeline_elements_names_unique(self, otio_timeline):\n        element_names = set()\n        for track in otio_timeline.tracks:\n            for element in track:\n                self._make_element_names_unique(element_names, element)\n\n    def _serialize_timeline(self, project, ressources, otio_timeline):\n        metadatas = GstStructure(self._get_element_metadatas(otio_timeline))\n        metadatas.set(\n            \"framerate\", \"fraction\", self._framerate_to_frame_duration(\n                otio_timeline.duration().rate\n            )\n        )\n        timeline = self._insert_new_sub_element(\n            project, 'timeline',\n            attrib={\n                \"properties\": self._get_element_properties(otio_timeline),\n                \"metadatas\": str(metadatas),\n            }\n        )\n        self._serialize_tracks(timeline, otio_timeline)\n\n        self._make_timeline_elements_names_unique(otio_timeline)\n\n        all_clips = set()\n        layers = {}\n        for layer_priority, otio_track in enumerate(otio_timeline.tracks):\n            self._serialize_layer(timeline, layers, layer_priority)\n            offset = 0\n            for otio_element in otio_track:\n                offset += self._serialize_timeline_element(\n                    timeline, layers, layer_priority, offset,\n                    otio_track, otio_element, ressources, all_clips,\n                )\n\n        for layer in layers.values():\n            layer[:] = sorted(layer, key=lambda child: int(child.get(\"start\")))\n\n    # --------------------\n    # static methods\n    # --------------------\n    @staticmethod\n    def _framerate_to_frame_duration(framerate):\n        frame_duration = FRAMERATE_FRAMEDURATION.get(int(framerate), \"\")\n        if not frame_duration:\n            frame_duration = FRAMERATE_FRAMEDURATION.get(float(framerate), \"\")\n        return frame_duration\n\n    def to_xges(self):\n        xges = cElementTree.Element('ges', version=\"0.4\")\n\n        metadatas = GstStructure(self._get_element_metadatas(self.container))\n        if self.container.name is not None:\n            metadatas.set(\"name\", \"string\", self.container.name)\n        if not isinstance(self.container, otio.schema.Timeline):\n            project = self._insert_new_sub_element(\n                xges, 'project',\n                attrib={\n                    \"properties\": self._get_element_properties(self.container),\n                    \"metadatas\": str(metadatas),\n                }\n            )\n\n            if len(self.container) > 1:\n                print(\n                    \"WARNING: Only one timeline supported, using *only* the first one.\")\n\n            otio_timeline = self.container[0]\n\n        else:\n            project = self._insert_new_sub_element(\n                xges, 'project',\n                attrib={\n                    \"metadatas\": str(metadatas),\n                }\n            )\n            otio_timeline = self.container\n\n        ressources = self._insert_new_sub_element(project, 'ressources')\n        self.rate = otio_timeline.duration().rate\n        self._serialize_timeline(project, ressources, otio_timeline)\n\n        # with indentations.\n        string = cElementTree.tostring(xges, encoding=\"UTF-8\")\n        dom = minidom.parseString(string)\n        return dom.toprettyxml(indent='    ')\n\n\n# --------------------\n# adapter requirements\n# --------------------\ndef read_from_string(input_str):\n    \"\"\"\n    Necessary read method for otio adapter\n\n    Args:\n        input_str (str): A GStreamer Editing Services formated project\n\n    Returns:\n        OpenTimeline: An OpenTimeline object\n    \"\"\"\n\n    return XGES(input_str).to_otio()\n\n\ndef write_to_string(input_otio):\n    \"\"\"\n    Necessary write method for otio adapter\n\n    Args:\n        input_otio (OpenTimeline): An OpenTimeline object\n\n    Returns:\n        str: The string contents of an FCP X XML\n    \"\"\"\n\n    return XGESOtio(input_otio).to_xges()\n\n\n# --------------------\n# Some unit check for internal types\n# --------------------\n\nclass XGESTests(unittest.TestCase):\n\n    def test_gst_structure_parsing(self):\n        struct = GstStructure('properties, name=(string)\"%s\";' % (\n            GstStructure.escape(\"sc01 sh010_anim.mov\"))\n        )\n        self.assertEqual(struct[\"name\"], \"sc01 sh010_anim.mov\")\n\n    def test_gst_structure_editing(self):\n        struct = GstStructure('properties, name=(string)\"%s\";' % (\n            GstStructure.escape(\"sc01 sh010_anim.mov\"))\n        )\n        self.assertEqual(struct[\"name\"], \"sc01 sh010_anim.mov\")\n\n        struct.set(\"name\", \"string\", \"test\")\n        self.assertEqual(struct[\"name\"], \"test\")\n        self.assertEqual(str(struct), 'properties, name=(string)\"test\";')\n\n    def test_empty_string(self):\n        struct = GstStructure('properties, name=(string)\"\";')\n        self.assertEqual(struct[\"name\"], \"\")\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/__init__.py",
    "content": "# coding: utf-8\n\"\"\"\nPackage resource API\n--------------------\n\nA resource is a logical file contained within a package, or a logical\nsubdirectory thereof.  The package resource API expects resource names\nto have their path parts separated with ``/``, *not* whatever the local\npath separator is.  Do not use os.path operations to manipulate resource\nnames being passed into the API.\n\nThe package resource API is designed to work with normal filesystem packages,\n.egg files, and unpacked .egg files.  It can also work in a limited way with\n.zip files and with custom PEP 302 loaders that support the ``get_data()``\nmethod.\n\"\"\"\n\nfrom __future__ import absolute_import\n\nimport sys\nimport os\nimport io\nimport time\nimport re\nimport types\nimport zipfile\nimport zipimport\nimport warnings\nimport stat\nimport functools\nimport pkgutil\nimport operator\nimport platform\nimport collections\nimport plistlib\nimport email.parser\nimport errno\nimport tempfile\nimport textwrap\nimport itertools\nimport inspect\nimport ntpath\nimport posixpath\nfrom pkgutil import get_importer\n\ntry:\n    import _imp\nexcept ImportError:\n    # Python 3.2 compatibility\n    import imp as _imp\n\ntry:\n    FileExistsError\nexcept NameError:\n    FileExistsError = OSError\n\nfrom pkg_resources.extern import six\nfrom pkg_resources.extern.six.moves import urllib, map, filter\n\n# capture these to bypass sandboxing\nfrom os import utime\ntry:\n    from os import mkdir, rename, unlink\n    WRITE_SUPPORT = True\nexcept ImportError:\n    # no write support, probably under GAE\n    WRITE_SUPPORT = False\n\nfrom os import open as os_open\nfrom os.path import isdir, split\n\ntry:\n    import importlib.machinery as importlib_machinery\n    # access attribute to force import under delayed import mechanisms.\n    importlib_machinery.__name__\nexcept ImportError:\n    importlib_machinery = None\n\nfrom . import py31compat\nfrom pkg_resources.extern import appdirs\nfrom pkg_resources.extern import packaging\n__import__('pkg_resources.extern.packaging.version')\n__import__('pkg_resources.extern.packaging.specifiers')\n__import__('pkg_resources.extern.packaging.requirements')\n__import__('pkg_resources.extern.packaging.markers')\n__import__('pkg_resources.py2_warn')\n\n\n__metaclass__ = type\n\n\nif (3, 0) < sys.version_info < (3, 5):\n    raise RuntimeError(\"Python 3.5 or later is required\")\n\nif six.PY2:\n    # Those builtin exceptions are only defined in Python 3\n    PermissionError = None\n    NotADirectoryError = None\n\n# declare some globals that will be defined later to\n# satisfy the linters.\nrequire = None\nworking_set = None\nadd_activation_listener = None\nresources_stream = None\ncleanup_resources = None\nresource_dir = None\nresource_stream = None\nset_extraction_path = None\nresource_isdir = None\nresource_string = None\niter_entry_points = None\nresource_listdir = None\nresource_filename = None\nresource_exists = None\n_distribution_finders = None\n_namespace_handlers = None\n_namespace_packages = None\n\n\nclass PEP440Warning(RuntimeWarning):\n    \"\"\"\n    Used when there is an issue with a version or specifier not complying with\n    PEP 440.\n    \"\"\"\n\n\ndef parse_version(v):\n    try:\n        return packaging.version.Version(v)\n    except packaging.version.InvalidVersion:\n        return packaging.version.LegacyVersion(v)\n\n\n_state_vars = {}\n\n\ndef _declare_state(vartype, **kw):\n    globals().update(kw)\n    _state_vars.update(dict.fromkeys(kw, vartype))\n\n\ndef __getstate__():\n    state = {}\n    g = globals()\n    for k, v in _state_vars.items():\n        state[k] = g['_sget_' + v](g[k])\n    return state\n\n\ndef __setstate__(state):\n    g = globals()\n    for k, v in state.items():\n        g['_sset_' + _state_vars[k]](k, g[k], v)\n    return state\n\n\ndef _sget_dict(val):\n    return val.copy()\n\n\ndef _sset_dict(key, ob, state):\n    ob.clear()\n    ob.update(state)\n\n\ndef _sget_object(val):\n    return val.__getstate__()\n\n\ndef _sset_object(key, ob, state):\n    ob.__setstate__(state)\n\n\n_sget_none = _sset_none = lambda *args: None\n\n\ndef get_supported_platform():\n    \"\"\"Return this platform's maximum compatible version.\n\n    distutils.util.get_platform() normally reports the minimum version\n    of Mac OS X that would be required to *use* extensions produced by\n    distutils.  But what we want when checking compatibility is to know the\n    version of Mac OS X that we are *running*.  To allow usage of packages that\n    explicitly require a newer version of Mac OS X, we must also know the\n    current version of the OS.\n\n    If this condition occurs for any other platform with a version in its\n    platform strings, this function should be extended accordingly.\n    \"\"\"\n    plat = get_build_platform()\n    m = macosVersionString.match(plat)\n    if m is not None and sys.platform == \"darwin\":\n        try:\n            plat = 'macosx-%s-%s' % ('.'.join(_macosx_vers()[:2]), m.group(3))\n        except ValueError:\n            # not Mac OS X\n            pass\n    return plat\n\n\n__all__ = [\n    # Basic resource access and distribution/entry point discovery\n    'require', 'run_script', 'get_provider', 'get_distribution',\n    'load_entry_point', 'get_entry_map', 'get_entry_info',\n    'iter_entry_points',\n    'resource_string', 'resource_stream', 'resource_filename',\n    'resource_listdir', 'resource_exists', 'resource_isdir',\n\n    # Environmental control\n    'declare_namespace', 'working_set', 'add_activation_listener',\n    'find_distributions', 'set_extraction_path', 'cleanup_resources',\n    'get_default_cache',\n\n    # Primary implementation classes\n    'Environment', 'WorkingSet', 'ResourceManager',\n    'Distribution', 'Requirement', 'EntryPoint',\n\n    # Exceptions\n    'ResolutionError', 'VersionConflict', 'DistributionNotFound',\n    'UnknownExtra', 'ExtractionError',\n\n    # Warnings\n    'PEP440Warning',\n\n    # Parsing functions and string utilities\n    'parse_requirements', 'parse_version', 'safe_name', 'safe_version',\n    'get_platform', 'compatible_platforms', 'yield_lines', 'split_sections',\n    'safe_extra', 'to_filename', 'invalid_marker', 'evaluate_marker',\n\n    # filesystem utilities\n    'ensure_directory', 'normalize_path',\n\n    # Distribution \"precedence\" constants\n    'EGG_DIST', 'BINARY_DIST', 'SOURCE_DIST', 'CHECKOUT_DIST', 'DEVELOP_DIST',\n\n    # \"Provider\" interfaces, implementations, and registration/lookup APIs\n    'IMetadataProvider', 'IResourceProvider', 'FileMetadata',\n    'PathMetadata', 'EggMetadata', 'EmptyProvider', 'empty_provider',\n    'NullProvider', 'EggProvider', 'DefaultProvider', 'ZipProvider',\n    'register_finder', 'register_namespace_handler', 'register_loader_type',\n    'fixup_namespace_packages', 'get_importer',\n\n    # Warnings\n    'PkgResourcesDeprecationWarning',\n\n    # Deprecated/backward compatibility only\n    'run_main', 'AvailableDistributions',\n]\n\n\nclass ResolutionError(Exception):\n    \"\"\"Abstract base for dependency resolution errors\"\"\"\n\n    def __repr__(self):\n        return self.__class__.__name__ + repr(self.args)\n\n\nclass VersionConflict(ResolutionError):\n    \"\"\"\n    An already-installed version conflicts with the requested version.\n\n    Should be initialized with the installed Distribution and the requested\n    Requirement.\n    \"\"\"\n\n    _template = \"{self.dist} is installed but {self.req} is required\"\n\n    @property\n    def dist(self):\n        return self.args[0]\n\n    @property\n    def req(self):\n        return self.args[1]\n\n    def report(self):\n        return self._template.format(**locals())\n\n    def with_context(self, required_by):\n        \"\"\"\n        If required_by is non-empty, return a version of self that is a\n        ContextualVersionConflict.\n        \"\"\"\n        if not required_by:\n            return self\n        args = self.args + (required_by,)\n        return ContextualVersionConflict(*args)\n\n\nclass ContextualVersionConflict(VersionConflict):\n    \"\"\"\n    A VersionConflict that accepts a third parameter, the set of the\n    requirements that required the installed Distribution.\n    \"\"\"\n\n    _template = VersionConflict._template + ' by {self.required_by}'\n\n    @property\n    def required_by(self):\n        return self.args[2]\n\n\nclass DistributionNotFound(ResolutionError):\n    \"\"\"A requested distribution was not found\"\"\"\n\n    _template = (\"The '{self.req}' distribution was not found \"\n                 \"and is required by {self.requirers_str}\")\n\n    @property\n    def req(self):\n        return self.args[0]\n\n    @property\n    def requirers(self):\n        return self.args[1]\n\n    @property\n    def requirers_str(self):\n        if not self.requirers:\n            return 'the application'\n        return ', '.join(self.requirers)\n\n    def report(self):\n        return self._template.format(**locals())\n\n    def __str__(self):\n        return self.report()\n\n\nclass UnknownExtra(ResolutionError):\n    \"\"\"Distribution doesn't have an \"extra feature\" of the given name\"\"\"\n\n\n_provider_factories = {}\n\nPY_MAJOR = '{}.{}'.format(*sys.version_info)\nEGG_DIST = 3\nBINARY_DIST = 2\nSOURCE_DIST = 1\nCHECKOUT_DIST = 0\nDEVELOP_DIST = -1\n\n\ndef register_loader_type(loader_type, provider_factory):\n    \"\"\"Register `provider_factory` to make providers for `loader_type`\n\n    `loader_type` is the type or class of a PEP 302 ``module.__loader__``,\n    and `provider_factory` is a function that, passed a *module* object,\n    returns an ``IResourceProvider`` for that module.\n    \"\"\"\n    _provider_factories[loader_type] = provider_factory\n\n\ndef get_provider(moduleOrReq):\n    \"\"\"Return an IResourceProvider for the named module or requirement\"\"\"\n    if isinstance(moduleOrReq, Requirement):\n        return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]\n    try:\n        module = sys.modules[moduleOrReq]\n    except KeyError:\n        __import__(moduleOrReq)\n        module = sys.modules[moduleOrReq]\n    loader = getattr(module, '__loader__', None)\n    return _find_adapter(_provider_factories, loader)(module)\n\n\ndef _macosx_vers(_cache=[]):\n    if not _cache:\n        version = platform.mac_ver()[0]\n        # fallback for MacPorts\n        if version == '':\n            plist = '/System/Library/CoreServices/SystemVersion.plist'\n            if os.path.exists(plist):\n                if hasattr(plistlib, 'readPlist'):\n                    plist_content = plistlib.readPlist(plist)\n                    if 'ProductVersion' in plist_content:\n                        version = plist_content['ProductVersion']\n\n        _cache.append(version.split('.'))\n    return _cache[0]\n\n\ndef _macosx_arch(machine):\n    return {'PowerPC': 'ppc', 'Power_Macintosh': 'ppc'}.get(machine, machine)\n\n\ndef get_build_platform():\n    \"\"\"Return this platform's string for platform-specific distributions\n\n    XXX Currently this is the same as ``distutils.util.get_platform()``, but it\n    needs some hacks for Linux and Mac OS X.\n    \"\"\"\n    from sysconfig import get_platform\n\n    plat = get_platform()\n    if sys.platform == \"darwin\" and not plat.startswith('macosx-'):\n        try:\n            version = _macosx_vers()\n            machine = os.uname()[4].replace(\" \", \"_\")\n            return \"macosx-%d.%d-%s\" % (\n                int(version[0]), int(version[1]),\n                _macosx_arch(machine),\n            )\n        except ValueError:\n            # if someone is running a non-Mac darwin system, this will fall\n            # through to the default implementation\n            pass\n    return plat\n\n\nmacosVersionString = re.compile(r\"macosx-(\\d+)\\.(\\d+)-(.*)\")\ndarwinVersionString = re.compile(r\"darwin-(\\d+)\\.(\\d+)\\.(\\d+)-(.*)\")\n# XXX backward compat\nget_platform = get_build_platform\n\n\ndef compatible_platforms(provided, required):\n    \"\"\"Can code for the `provided` platform run on the `required` platform?\n\n    Returns true if either platform is ``None``, or the platforms are equal.\n\n    XXX Needs compatibility checks for Linux and other unixy OSes.\n    \"\"\"\n    if provided is None or required is None or provided == required:\n        # easy case\n        return True\n\n    # Mac OS X special cases\n    reqMac = macosVersionString.match(required)\n    if reqMac:\n        provMac = macosVersionString.match(provided)\n\n        # is this a Mac package?\n        if not provMac:\n            # this is backwards compatibility for packages built before\n            # setuptools 0.6. All packages built after this point will\n            # use the new macosx designation.\n            provDarwin = darwinVersionString.match(provided)\n            if provDarwin:\n                dversion = int(provDarwin.group(1))\n                macosversion = \"%s.%s\" % (reqMac.group(1), reqMac.group(2))\n                if dversion == 7 and macosversion >= \"10.3\" or \\\n                        dversion == 8 and macosversion >= \"10.4\":\n                    return True\n            # egg isn't macosx or legacy darwin\n            return False\n\n        # are they the same major version and machine type?\n        if provMac.group(1) != reqMac.group(1) or \\\n                provMac.group(3) != reqMac.group(3):\n            return False\n\n        # is the required OS major update >= the provided one?\n        if int(provMac.group(2)) > int(reqMac.group(2)):\n            return False\n\n        return True\n\n    # XXX Linux and other platforms' special cases should go here\n    return False\n\n\ndef run_script(dist_spec, script_name):\n    \"\"\"Locate distribution `dist_spec` and run its `script_name` script\"\"\"\n    ns = sys._getframe(1).f_globals\n    name = ns['__name__']\n    ns.clear()\n    ns['__name__'] = name\n    require(dist_spec)[0].run_script(script_name, ns)\n\n\n# backward compatibility\nrun_main = run_script\n\n\ndef get_distribution(dist):\n    \"\"\"Return a current distribution object for a Requirement or string\"\"\"\n    if isinstance(dist, six.string_types):\n        dist = Requirement.parse(dist)\n    if isinstance(dist, Requirement):\n        dist = get_provider(dist)\n    if not isinstance(dist, Distribution):\n        raise TypeError(\"Expected string, Requirement, or Distribution\", dist)\n    return dist\n\n\ndef load_entry_point(dist, group, name):\n    \"\"\"Return `name` entry point of `group` for `dist` or raise ImportError\"\"\"\n    return get_distribution(dist).load_entry_point(group, name)\n\n\ndef get_entry_map(dist, group=None):\n    \"\"\"Return the entry point map for `group`, or the full entry map\"\"\"\n    return get_distribution(dist).get_entry_map(group)\n\n\ndef get_entry_info(dist, group, name):\n    \"\"\"Return the EntryPoint object for `group`+`name`, or ``None``\"\"\"\n    return get_distribution(dist).get_entry_info(group, name)\n\n\nclass IMetadataProvider:\n    def has_metadata(name):\n        \"\"\"Does the package's distribution contain the named metadata?\"\"\"\n\n    def get_metadata(name):\n        \"\"\"The named metadata resource as a string\"\"\"\n\n    def get_metadata_lines(name):\n        \"\"\"Yield named metadata resource as list of non-blank non-comment lines\n\n       Leading and trailing whitespace is stripped from each line, and lines\n       with ``#`` as the first non-blank character are omitted.\"\"\"\n\n    def metadata_isdir(name):\n        \"\"\"Is the named metadata a directory?  (like ``os.path.isdir()``)\"\"\"\n\n    def metadata_listdir(name):\n        \"\"\"List of metadata names in the directory (like ``os.listdir()``)\"\"\"\n\n    def run_script(script_name, namespace):\n        \"\"\"Execute the named script in the supplied namespace dictionary\"\"\"\n\n\nclass IResourceProvider(IMetadataProvider):\n    \"\"\"An object that provides access to package resources\"\"\"\n\n    def get_resource_filename(manager, resource_name):\n        \"\"\"Return a true filesystem path for `resource_name`\n\n        `manager` must be an ``IResourceManager``\"\"\"\n\n    def get_resource_stream(manager, resource_name):\n        \"\"\"Return a readable file-like object for `resource_name`\n\n        `manager` must be an ``IResourceManager``\"\"\"\n\n    def get_resource_string(manager, resource_name):\n        \"\"\"Return a string containing the contents of `resource_name`\n\n        `manager` must be an ``IResourceManager``\"\"\"\n\n    def has_resource(resource_name):\n        \"\"\"Does the package contain the named resource?\"\"\"\n\n    def resource_isdir(resource_name):\n        \"\"\"Is the named resource a directory?  (like ``os.path.isdir()``)\"\"\"\n\n    def resource_listdir(resource_name):\n        \"\"\"List of resource names in the directory (like ``os.listdir()``)\"\"\"\n\n\nclass WorkingSet:\n    \"\"\"A collection of active distributions on sys.path (or a similar list)\"\"\"\n\n    def __init__(self, entries=None):\n        \"\"\"Create working set from list of path entries (default=sys.path)\"\"\"\n        self.entries = []\n        self.entry_keys = {}\n        self.by_key = {}\n        self.callbacks = []\n\n        if entries is None:\n            entries = sys.path\n\n        for entry in entries:\n            self.add_entry(entry)\n\n    @classmethod\n    def _build_master(cls):\n        \"\"\"\n        Prepare the master working set.\n        \"\"\"\n        ws = cls()\n        try:\n            from __main__ import __requires__\n        except ImportError:\n            # The main program does not list any requirements\n            return ws\n\n        # ensure the requirements are met\n        try:\n            ws.require(__requires__)\n        except VersionConflict:\n            return cls._build_from_requirements(__requires__)\n\n        return ws\n\n    @classmethod\n    def _build_from_requirements(cls, req_spec):\n        \"\"\"\n        Build a working set from a requirement spec. Rewrites sys.path.\n        \"\"\"\n        # try it without defaults already on sys.path\n        # by starting with an empty path\n        ws = cls([])\n        reqs = parse_requirements(req_spec)\n        dists = ws.resolve(reqs, Environment())\n        for dist in dists:\n            ws.add(dist)\n\n        # add any missing entries from sys.path\n        for entry in sys.path:\n            if entry not in ws.entries:\n                ws.add_entry(entry)\n\n        # then copy back to sys.path\n        sys.path[:] = ws.entries\n        return ws\n\n    def add_entry(self, entry):\n        \"\"\"Add a path item to ``.entries``, finding any distributions on it\n\n        ``find_distributions(entry, True)`` is used to find distributions\n        corresponding to the path entry, and they are added.  `entry` is\n        always appended to ``.entries``, even if it is already present.\n        (This is because ``sys.path`` can contain the same value more than\n        once, and the ``.entries`` of the ``sys.path`` WorkingSet should always\n        equal ``sys.path``.)\n        \"\"\"\n        self.entry_keys.setdefault(entry, [])\n        self.entries.append(entry)\n        for dist in find_distributions(entry, True):\n            self.add(dist, entry, False)\n\n    def __contains__(self, dist):\n        \"\"\"True if `dist` is the active distribution for its project\"\"\"\n        return self.by_key.get(dist.key) == dist\n\n    def find(self, req):\n        \"\"\"Find a distribution matching requirement `req`\n\n        If there is an active distribution for the requested project, this\n        returns it as long as it meets the version requirement specified by\n        `req`.  But, if there is an active distribution for the project and it\n        does *not* meet the `req` requirement, ``VersionConflict`` is raised.\n        If there is no active distribution for the requested project, ``None``\n        is returned.\n        \"\"\"\n        dist = self.by_key.get(req.key)\n        if dist is not None and dist not in req:\n            # XXX add more info\n            raise VersionConflict(dist, req)\n        return dist\n\n    def iter_entry_points(self, group, name=None):\n        \"\"\"Yield entry point objects from `group` matching `name`\n\n        If `name` is None, yields all entry points in `group` from all\n        distributions in the working set, otherwise only ones matching\n        both `group` and `name` are yielded (in distribution order).\n        \"\"\"\n        return (\n            entry\n            for dist in self\n            for entry in dist.get_entry_map(group).values()\n            if name is None or name == entry.name\n        )\n\n    def run_script(self, requires, script_name):\n        \"\"\"Locate distribution for `requires` and run `script_name` script\"\"\"\n        ns = sys._getframe(1).f_globals\n        name = ns['__name__']\n        ns.clear()\n        ns['__name__'] = name\n        self.require(requires)[0].run_script(script_name, ns)\n\n    def __iter__(self):\n        \"\"\"Yield distributions for non-duplicate projects in the working set\n\n        The yield order is the order in which the items' path entries were\n        added to the working set.\n        \"\"\"\n        seen = {}\n        for item in self.entries:\n            if item not in self.entry_keys:\n                # workaround a cache issue\n                continue\n\n            for key in self.entry_keys[item]:\n                if key not in seen:\n                    seen[key] = 1\n                    yield self.by_key[key]\n\n    def add(self, dist, entry=None, insert=True, replace=False):\n        \"\"\"Add `dist` to working set, associated with `entry`\n\n        If `entry` is unspecified, it defaults to the ``.location`` of `dist`.\n        On exit from this routine, `entry` is added to the end of the working\n        set's ``.entries`` (if it wasn't already present).\n\n        `dist` is only added to the working set if it's for a project that\n        doesn't already have a distribution in the set, unless `replace=True`.\n        If it's added, any callbacks registered with the ``subscribe()`` method\n        will be called.\n        \"\"\"\n        if insert:\n            dist.insert_on(self.entries, entry, replace=replace)\n\n        if entry is None:\n            entry = dist.location\n        keys = self.entry_keys.setdefault(entry, [])\n        keys2 = self.entry_keys.setdefault(dist.location, [])\n        if not replace and dist.key in self.by_key:\n            # ignore hidden distros\n            return\n\n        self.by_key[dist.key] = dist\n        if dist.key not in keys:\n            keys.append(dist.key)\n        if dist.key not in keys2:\n            keys2.append(dist.key)\n        self._added_new(dist)\n\n    def resolve(self, requirements, env=None, installer=None,\n                replace_conflicting=False, extras=None):\n        \"\"\"List all distributions needed to (recursively) meet `requirements`\n\n        `requirements` must be a sequence of ``Requirement`` objects.  `env`,\n        if supplied, should be an ``Environment`` instance.  If\n        not supplied, it defaults to all distributions available within any\n        entry or distribution in the working set.  `installer`, if supplied,\n        will be invoked with each requirement that cannot be met by an\n        already-installed distribution; it should return a ``Distribution`` or\n        ``None``.\n\n        Unless `replace_conflicting=True`, raises a VersionConflict exception\n        if\n        any requirements are found on the path that have the correct name but\n        the wrong version.  Otherwise, if an `installer` is supplied it will be\n        invoked to obtain the correct version of the requirement and activate\n        it.\n\n        `extras` is a list of the extras to be used with these requirements.\n        This is important because extra requirements may look like `my_req;\n        extra = \"my_extra\"`, which would otherwise be interpreted as a purely\n        optional requirement.  Instead, we want to be able to assert that these\n        requirements are truly required.\n        \"\"\"\n\n        # set up the stack\n        requirements = list(requirements)[::-1]\n        # set of processed requirements\n        processed = {}\n        # key -> dist\n        best = {}\n        to_activate = []\n\n        req_extras = _ReqExtras()\n\n        # Mapping of requirement to set of distributions that required it;\n        # useful for reporting info about conflicts.\n        required_by = collections.defaultdict(set)\n\n        while requirements:\n            # process dependencies breadth-first\n            req = requirements.pop(0)\n            if req in processed:\n                # Ignore cyclic or redundant dependencies\n                continue\n\n            if not req_extras.markers_pass(req, extras):\n                continue\n\n            dist = best.get(req.key)\n            if dist is None:\n                # Find the best distribution and add it to the map\n                dist = self.by_key.get(req.key)\n                if dist is None or (dist not in req and replace_conflicting):\n                    ws = self\n                    if env is None:\n                        if dist is None:\n                            env = Environment(self.entries)\n                        else:\n                            # Use an empty environment and workingset to avoid\n                            # any further conflicts with the conflicting\n                            # distribution\n                            env = Environment([])\n                            ws = WorkingSet([])\n                    dist = best[req.key] = env.best_match(\n                        req, ws, installer,\n                        replace_conflicting=replace_conflicting\n                    )\n                    if dist is None:\n                        requirers = required_by.get(req, None)\n                        raise DistributionNotFound(req, requirers)\n                to_activate.append(dist)\n            if dist not in req:\n                # Oops, the \"best\" so far conflicts with a dependency\n                dependent_req = required_by[req]\n                raise VersionConflict(dist, req).with_context(dependent_req)\n\n            # push the new requirements onto the stack\n            new_requirements = dist.requires(req.extras)[::-1]\n            requirements.extend(new_requirements)\n\n            # Register the new requirements needed by req\n            for new_requirement in new_requirements:\n                required_by[new_requirement].add(req.project_name)\n                req_extras[new_requirement] = req.extras\n\n            processed[req] = True\n\n        # return list of distros to activate\n        return to_activate\n\n    def find_plugins(\n            self, plugin_env, full_env=None, installer=None, fallback=True):\n        \"\"\"Find all activatable distributions in `plugin_env`\n\n        Example usage::\n\n            distributions, errors = working_set.find_plugins(\n                Environment(plugin_dirlist)\n            )\n            # add plugins+libs to sys.path\n            map(working_set.add, distributions)\n            # display errors\n            print('Could not load', errors)\n\n        The `plugin_env` should be an ``Environment`` instance that contains\n        only distributions that are in the project's \"plugin directory\" or\n        directories. The `full_env`, if supplied, should be an ``Environment``\n        contains all currently-available distributions.  If `full_env` is not\n        supplied, one is created automatically from the ``WorkingSet`` this\n        method is called on, which will typically mean that every directory on\n        ``sys.path`` will be scanned for distributions.\n\n        `installer` is a standard installer callback as used by the\n        ``resolve()`` method. The `fallback` flag indicates whether we should\n        attempt to resolve older versions of a plugin if the newest version\n        cannot be resolved.\n\n        This method returns a 2-tuple: (`distributions`, `error_info`), where\n        `distributions` is a list of the distributions found in `plugin_env`\n        that were loadable, along with any other distributions that are needed\n        to resolve their dependencies.  `error_info` is a dictionary mapping\n        unloadable plugin distributions to an exception instance describing the\n        error that occurred. Usually this will be a ``DistributionNotFound`` or\n        ``VersionConflict`` instance.\n        \"\"\"\n\n        plugin_projects = list(plugin_env)\n        # scan project names in alphabetic order\n        plugin_projects.sort()\n\n        error_info = {}\n        distributions = {}\n\n        if full_env is None:\n            env = Environment(self.entries)\n            env += plugin_env\n        else:\n            env = full_env + plugin_env\n\n        shadow_set = self.__class__([])\n        # put all our entries in shadow_set\n        list(map(shadow_set.add, self))\n\n        for project_name in plugin_projects:\n\n            for dist in plugin_env[project_name]:\n\n                req = [dist.as_requirement()]\n\n                try:\n                    resolvees = shadow_set.resolve(req, env, installer)\n\n                except ResolutionError as v:\n                    # save error info\n                    error_info[dist] = v\n                    if fallback:\n                        # try the next older version of project\n                        continue\n                    else:\n                        # give up on this project, keep going\n                        break\n\n                else:\n                    list(map(shadow_set.add, resolvees))\n                    distributions.update(dict.fromkeys(resolvees))\n\n                    # success, no need to try any more versions of this project\n                    break\n\n        distributions = list(distributions)\n        distributions.sort()\n\n        return distributions, error_info\n\n    def require(self, *requirements):\n        \"\"\"Ensure that distributions matching `requirements` are activated\n\n        `requirements` must be a string or a (possibly-nested) sequence\n        thereof, specifying the distributions and versions required.  The\n        return value is a sequence of the distributions that needed to be\n        activated to fulfill the requirements; all relevant distributions are\n        included, even if they were already activated in this working set.\n        \"\"\"\n        needed = self.resolve(parse_requirements(requirements))\n\n        for dist in needed:\n            self.add(dist)\n\n        return needed\n\n    def subscribe(self, callback, existing=True):\n        \"\"\"Invoke `callback` for all distributions\n\n        If `existing=True` (default),\n        call on all existing ones, as well.\n        \"\"\"\n        if callback in self.callbacks:\n            return\n        self.callbacks.append(callback)\n        if not existing:\n            return\n        for dist in self:\n            callback(dist)\n\n    def _added_new(self, dist):\n        for callback in self.callbacks:\n            callback(dist)\n\n    def __getstate__(self):\n        return (\n            self.entries[:], self.entry_keys.copy(), self.by_key.copy(),\n            self.callbacks[:]\n        )\n\n    def __setstate__(self, e_k_b_c):\n        entries, keys, by_key, callbacks = e_k_b_c\n        self.entries = entries[:]\n        self.entry_keys = keys.copy()\n        self.by_key = by_key.copy()\n        self.callbacks = callbacks[:]\n\n\nclass _ReqExtras(dict):\n    \"\"\"\n    Map each requirement to the extras that demanded it.\n    \"\"\"\n\n    def markers_pass(self, req, extras=None):\n        \"\"\"\n        Evaluate markers for req against each extra that\n        demanded it.\n\n        Return False if the req has a marker and fails\n        evaluation. Otherwise, return True.\n        \"\"\"\n        extra_evals = (\n            req.marker.evaluate({'extra': extra})\n            for extra in self.get(req, ()) + (extras or (None,))\n        )\n        return not req.marker or any(extra_evals)\n\n\nclass Environment:\n    \"\"\"Searchable snapshot of distributions on a search path\"\"\"\n\n    def __init__(\n            self, search_path=None, platform=get_supported_platform(),\n            python=PY_MAJOR):\n        \"\"\"Snapshot distributions available on a search path\n\n        Any distributions found on `search_path` are added to the environment.\n        `search_path` should be a sequence of ``sys.path`` items.  If not\n        supplied, ``sys.path`` is used.\n\n        `platform` is an optional string specifying the name of the platform\n        that platform-specific distributions must be compatible with.  If\n        unspecified, it defaults to the current platform.  `python` is an\n        optional string naming the desired version of Python (e.g. ``'3.6'``);\n        it defaults to the current version.\n\n        You may explicitly set `platform` (and/or `python`) to ``None`` if you\n        wish to map *all* distributions, not just those compatible with the\n        running platform or Python version.\n        \"\"\"\n        self._distmap = {}\n        self.platform = platform\n        self.python = python\n        self.scan(search_path)\n\n    def can_add(self, dist):\n        \"\"\"Is distribution `dist` acceptable for this environment?\n\n        The distribution must match the platform and python version\n        requirements specified when this environment was created, or False\n        is returned.\n        \"\"\"\n        py_compat = (\n            self.python is None\n            or dist.py_version is None\n            or dist.py_version == self.python\n        )\n        return py_compat and compatible_platforms(dist.platform, self.platform)\n\n    def remove(self, dist):\n        \"\"\"Remove `dist` from the environment\"\"\"\n        self._distmap[dist.key].remove(dist)\n\n    def scan(self, search_path=None):\n        \"\"\"Scan `search_path` for distributions usable in this environment\n\n        Any distributions found are added to the environment.\n        `search_path` should be a sequence of ``sys.path`` items.  If not\n        supplied, ``sys.path`` is used.  Only distributions conforming to\n        the platform/python version defined at initialization are added.\n        \"\"\"\n        if search_path is None:\n            search_path = sys.path\n\n        for item in search_path:\n            for dist in find_distributions(item):\n                self.add(dist)\n\n    def __getitem__(self, project_name):\n        \"\"\"Return a newest-to-oldest list of distributions for `project_name`\n\n        Uses case-insensitive `project_name` comparison, assuming all the\n        project's distributions use their project's name converted to all\n        lowercase as their key.\n\n        \"\"\"\n        distribution_key = project_name.lower()\n        return self._distmap.get(distribution_key, [])\n\n    def add(self, dist):\n        \"\"\"Add `dist` if we ``can_add()`` it and it has not already been added\n        \"\"\"\n        if self.can_add(dist) and dist.has_version():\n            dists = self._distmap.setdefault(dist.key, [])\n            if dist not in dists:\n                dists.append(dist)\n                dists.sort(key=operator.attrgetter('hashcmp'), reverse=True)\n\n    def best_match(\n            self, req, working_set, installer=None, replace_conflicting=False):\n        \"\"\"Find distribution best matching `req` and usable on `working_set`\n\n        This calls the ``find(req)`` method of the `working_set` to see if a\n        suitable distribution is already active.  (This may raise\n        ``VersionConflict`` if an unsuitable version of the project is already\n        active in the specified `working_set`.)  If a suitable distribution\n        isn't active, this method returns the newest distribution in the\n        environment that meets the ``Requirement`` in `req`.  If no suitable\n        distribution is found, and `installer` is supplied, then the result of\n        calling the environment's ``obtain(req, installer)`` method will be\n        returned.\n        \"\"\"\n        try:\n            dist = working_set.find(req)\n        except VersionConflict:\n            if not replace_conflicting:\n                raise\n            dist = None\n        if dist is not None:\n            return dist\n        for dist in self[req.key]:\n            if dist in req:\n                return dist\n        # try to download/install\n        return self.obtain(req, installer)\n\n    def obtain(self, requirement, installer=None):\n        \"\"\"Obtain a distribution matching `requirement` (e.g. via download)\n\n        Obtain a distro that matches requirement (e.g. via download).  In the\n        base ``Environment`` class, this routine just returns\n        ``installer(requirement)``, unless `installer` is None, in which case\n        None is returned instead.  This method is a hook that allows subclasses\n        to attempt other ways of obtaining a distribution before falling back\n        to the `installer` argument.\"\"\"\n        if installer is not None:\n            return installer(requirement)\n\n    def __iter__(self):\n        \"\"\"Yield the unique project names of the available distributions\"\"\"\n        for key in self._distmap.keys():\n            if self[key]:\n                yield key\n\n    def __iadd__(self, other):\n        \"\"\"In-place addition of a distribution or environment\"\"\"\n        if isinstance(other, Distribution):\n            self.add(other)\n        elif isinstance(other, Environment):\n            for project in other:\n                for dist in other[project]:\n                    self.add(dist)\n        else:\n            raise TypeError(\"Can't add %r to environment\" % (other,))\n        return self\n\n    def __add__(self, other):\n        \"\"\"Add an environment or distribution to an environment\"\"\"\n        new = self.__class__([], platform=None, python=None)\n        for env in self, other:\n            new += env\n        return new\n\n\n# XXX backward compatibility\nAvailableDistributions = Environment\n\n\nclass ExtractionError(RuntimeError):\n    \"\"\"An error occurred extracting a resource\n\n    The following attributes are available from instances of this exception:\n\n    manager\n        The resource manager that raised this exception\n\n    cache_path\n        The base directory for resource extraction\n\n    original_error\n        The exception instance that caused extraction to fail\n    \"\"\"\n\n\nclass ResourceManager:\n    \"\"\"Manage resource extraction and packages\"\"\"\n    extraction_path = None\n\n    def __init__(self):\n        self.cached_files = {}\n\n    def resource_exists(self, package_or_requirement, resource_name):\n        \"\"\"Does the named resource exist?\"\"\"\n        return get_provider(package_or_requirement).has_resource(resource_name)\n\n    def resource_isdir(self, package_or_requirement, resource_name):\n        \"\"\"Is the named resource an existing directory?\"\"\"\n        return get_provider(package_or_requirement).resource_isdir(\n            resource_name\n        )\n\n    def resource_filename(self, package_or_requirement, resource_name):\n        \"\"\"Return a true filesystem path for specified resource\"\"\"\n        return get_provider(package_or_requirement).get_resource_filename(\n            self, resource_name\n        )\n\n    def resource_stream(self, package_or_requirement, resource_name):\n        \"\"\"Return a readable file-like object for specified resource\"\"\"\n        return get_provider(package_or_requirement).get_resource_stream(\n            self, resource_name\n        )\n\n    def resource_string(self, package_or_requirement, resource_name):\n        \"\"\"Return specified resource as a string\"\"\"\n        return get_provider(package_or_requirement).get_resource_string(\n            self, resource_name\n        )\n\n    def resource_listdir(self, package_or_requirement, resource_name):\n        \"\"\"List the contents of the named resource directory\"\"\"\n        return get_provider(package_or_requirement).resource_listdir(\n            resource_name\n        )\n\n    def extraction_error(self):\n        \"\"\"Give an error message for problems extracting file(s)\"\"\"\n\n        old_exc = sys.exc_info()[1]\n        cache_path = self.extraction_path or get_default_cache()\n\n        tmpl = textwrap.dedent(\"\"\"\n            Can't extract file(s) to egg cache\n\n            The following error occurred while trying to extract file(s)\n            to the Python egg cache:\n\n              {old_exc}\n\n            The Python egg cache directory is currently set to:\n\n              {cache_path}\n\n            Perhaps your account does not have write access to this directory?\n            You can change the cache directory by setting the PYTHON_EGG_CACHE\n            environment variable to point to an accessible directory.\n            \"\"\").lstrip()\n        err = ExtractionError(tmpl.format(**locals()))\n        err.manager = self\n        err.cache_path = cache_path\n        err.original_error = old_exc\n        raise err\n\n    def get_cache_path(self, archive_name, names=()):\n        \"\"\"Return absolute location in cache for `archive_name` and `names`\n\n        The parent directory of the resulting path will be created if it does\n        not already exist.  `archive_name` should be the base filename of the\n        enclosing egg (which may not be the name of the enclosing zipfile!),\n        including its \".egg\" extension.  `names`, if provided, should be a\n        sequence of path name parts \"under\" the egg's extraction location.\n\n        This method should only be called by resource providers that need to\n        obtain an extraction location, and only for names they intend to\n        extract, as it tracks the generated names for possible cleanup later.\n        \"\"\"\n        extract_path = self.extraction_path or get_default_cache()\n        target_path = os.path.join(extract_path, archive_name + '-tmp', *names)\n        try:\n            _bypass_ensure_directory(target_path)\n        except Exception:\n            self.extraction_error()\n\n        self._warn_unsafe_extraction_path(extract_path)\n\n        self.cached_files[target_path] = 1\n        return target_path\n\n    @staticmethod\n    def _warn_unsafe_extraction_path(path):\n        \"\"\"\n        If the default extraction path is overridden and set to an insecure\n        location, such as /tmp, it opens up an opportunity for an attacker to\n        replace an extracted file with an unauthorized payload. Warn the user\n        if a known insecure location is used.\n\n        See Distribute #375 for more details.\n        \"\"\"\n        if os.name == 'nt' and not path.startswith(os.environ['windir']):\n            # On Windows, permissions are generally restrictive by default\n            #  and temp directories are not writable by other users, so\n            #  bypass the warning.\n            return\n        mode = os.stat(path).st_mode\n        if mode & stat.S_IWOTH or mode & stat.S_IWGRP:\n            msg = (\n                \"%s is writable by group/others and vulnerable to attack \"\n                \"when \"\n                \"used with get_resource_filename. Consider a more secure \"\n                \"location (set with .set_extraction_path or the \"\n                \"PYTHON_EGG_CACHE environment variable).\" % path\n            )\n            warnings.warn(msg, UserWarning)\n\n    def postprocess(self, tempname, filename):\n        \"\"\"Perform any platform-specific postprocessing of `tempname`\n\n        This is where Mac header rewrites should be done; other platforms don't\n        have anything special they should do.\n\n        Resource providers should call this method ONLY after successfully\n        extracting a compressed resource.  They must NOT call it on resources\n        that are already in the filesystem.\n\n        `tempname` is the current (temporary) name of the file, and `filename`\n        is the name it will be renamed to by the caller after this routine\n        returns.\n        \"\"\"\n\n        if os.name == 'posix':\n            # Make the resource executable\n            mode = ((os.stat(tempname).st_mode) | 0o555) & 0o7777\n            os.chmod(tempname, mode)\n\n    def set_extraction_path(self, path):\n        \"\"\"Set the base path where resources will be extracted to, if needed.\n\n        If you do not call this routine before any extractions take place, the\n        path defaults to the return value of ``get_default_cache()``.  (Which\n        is based on the ``PYTHON_EGG_CACHE`` environment variable, with various\n        platform-specific fallbacks.  See that routine's documentation for more\n        details.)\n\n        Resources are extracted to subdirectories of this path based upon\n        information given by the ``IResourceProvider``.  You may set this to a\n        temporary directory, but then you must call ``cleanup_resources()`` to\n        delete the extracted files when done.  There is no guarantee that\n        ``cleanup_resources()`` will be able to remove all extracted files.\n\n        (Note: you may not change the extraction path for a given resource\n        manager once resources have been extracted, unless you first call\n        ``cleanup_resources()``.)\n        \"\"\"\n        if self.cached_files:\n            raise ValueError(\n                \"Can't change extraction path, files already extracted\"\n            )\n\n        self.extraction_path = path\n\n    def cleanup_resources(self, force=False):\n        \"\"\"\n        Delete all extracted resource files and directories, returning a list\n        of the file and directory names that could not be successfully removed.\n        This function does not have any concurrency protection, so it should\n        generally only be called when the extraction path is a temporary\n        directory exclusive to a single process.  This method is not\n        automatically called; you must call it explicitly or register it as an\n        ``atexit`` function if you wish to ensure cleanup of a temporary\n        directory used for extractions.\n        \"\"\"\n        # XXX\n\n\ndef get_default_cache():\n    \"\"\"\n    Return the ``PYTHON_EGG_CACHE`` environment variable\n    or a platform-relevant user cache dir for an app\n    named \"Python-Eggs\".\n    \"\"\"\n    return (\n        os.environ.get('PYTHON_EGG_CACHE')\n        or appdirs.user_cache_dir(appname='Python-Eggs')\n    )\n\n\ndef safe_name(name):\n    \"\"\"Convert an arbitrary string to a standard distribution name\n\n    Any runs of non-alphanumeric/. characters are replaced with a single '-'.\n    \"\"\"\n    return re.sub('[^A-Za-z0-9.]+', '-', name)\n\n\ndef safe_version(version):\n    \"\"\"\n    Convert an arbitrary string to a standard version string\n    \"\"\"\n    try:\n        # normalize the version\n        return str(packaging.version.Version(version))\n    except packaging.version.InvalidVersion:\n        version = version.replace(' ', '.')\n        return re.sub('[^A-Za-z0-9.]+', '-', version)\n\n\ndef safe_extra(extra):\n    \"\"\"Convert an arbitrary string to a standard 'extra' name\n\n    Any runs of non-alphanumeric characters are replaced with a single '_',\n    and the result is always lowercased.\n    \"\"\"\n    return re.sub('[^A-Za-z0-9.-]+', '_', extra).lower()\n\n\ndef to_filename(name):\n    \"\"\"Convert a project or version name to its filename-escaped form\n\n    Any '-' characters are currently replaced with '_'.\n    \"\"\"\n    return name.replace('-', '_')\n\n\ndef invalid_marker(text):\n    \"\"\"\n    Validate text as a PEP 508 environment marker; return an exception\n    if invalid or False otherwise.\n    \"\"\"\n    try:\n        evaluate_marker(text)\n    except SyntaxError as e:\n        e.filename = None\n        e.lineno = None\n        return e\n    return False\n\n\ndef evaluate_marker(text, extra=None):\n    \"\"\"\n    Evaluate a PEP 508 environment marker.\n    Return a boolean indicating the marker result in this environment.\n    Raise SyntaxError if marker is invalid.\n\n    This implementation uses the 'pyparsing' module.\n    \"\"\"\n    try:\n        marker = packaging.markers.Marker(text)\n        return marker.evaluate()\n    except packaging.markers.InvalidMarker as e:\n        raise SyntaxError(e)\n\n\nclass NullProvider:\n    \"\"\"Try to implement resources and metadata for arbitrary PEP 302 loaders\"\"\"\n\n    egg_name = None\n    egg_info = None\n    loader = None\n\n    def __init__(self, module):\n        self.loader = getattr(module, '__loader__', None)\n        self.module_path = os.path.dirname(getattr(module, '__file__', ''))\n\n    def get_resource_filename(self, manager, resource_name):\n        return self._fn(self.module_path, resource_name)\n\n    def get_resource_stream(self, manager, resource_name):\n        return io.BytesIO(self.get_resource_string(manager, resource_name))\n\n    def get_resource_string(self, manager, resource_name):\n        return self._get(self._fn(self.module_path, resource_name))\n\n    def has_resource(self, resource_name):\n        return self._has(self._fn(self.module_path, resource_name))\n\n    def _get_metadata_path(self, name):\n        return self._fn(self.egg_info, name)\n\n    def has_metadata(self, name):\n        if not self.egg_info:\n            return self.egg_info\n\n        path = self._get_metadata_path(name)\n        return self._has(path)\n\n    def get_metadata(self, name):\n        if not self.egg_info:\n            return \"\"\n        path = self._get_metadata_path(name)\n        value = self._get(path)\n        if six.PY2:\n            return value\n        try:\n            return value.decode('utf-8')\n        except UnicodeDecodeError as exc:\n            # Include the path in the error message to simplify\n            # troubleshooting, and without changing the exception type.\n            exc.reason += ' in {} file at path: {}'.format(name, path)\n            raise\n\n    def get_metadata_lines(self, name):\n        return yield_lines(self.get_metadata(name))\n\n    def resource_isdir(self, resource_name):\n        return self._isdir(self._fn(self.module_path, resource_name))\n\n    def metadata_isdir(self, name):\n        return self.egg_info and self._isdir(self._fn(self.egg_info, name))\n\n    def resource_listdir(self, resource_name):\n        return self._listdir(self._fn(self.module_path, resource_name))\n\n    def metadata_listdir(self, name):\n        if self.egg_info:\n            return self._listdir(self._fn(self.egg_info, name))\n        return []\n\n    def run_script(self, script_name, namespace):\n        script = 'scripts/' + script_name\n        if not self.has_metadata(script):\n            raise ResolutionError(\n                \"Script {script!r} not found in metadata at {self.egg_info!r}\"\n                .format(**locals()),\n            )\n        script_text = self.get_metadata(script).replace('\\r\\n', '\\n')\n        script_text = script_text.replace('\\r', '\\n')\n        script_filename = self._fn(self.egg_info, script)\n        namespace['__file__'] = script_filename\n        if os.path.exists(script_filename):\n            source = open(script_filename).read()\n            code = compile(source, script_filename, 'exec')\n            exec(code, namespace, namespace)\n        else:\n            from linecache import cache\n            cache[script_filename] = (\n                len(script_text), 0, script_text.split('\\n'), script_filename\n            )\n            script_code = compile(script_text, script_filename, 'exec')\n            exec(script_code, namespace, namespace)\n\n    def _has(self, path):\n        raise NotImplementedError(\n            \"Can't perform this operation for unregistered loader type\"\n        )\n\n    def _isdir(self, path):\n        raise NotImplementedError(\n            \"Can't perform this operation for unregistered loader type\"\n        )\n\n    def _listdir(self, path):\n        raise NotImplementedError(\n            \"Can't perform this operation for unregistered loader type\"\n        )\n\n    def _fn(self, base, resource_name):\n        self._validate_resource_path(resource_name)\n        if resource_name:\n            return os.path.join(base, *resource_name.split('/'))\n        return base\n\n    @staticmethod\n    def _validate_resource_path(path):\n        \"\"\"\n        Validate the resource paths according to the docs.\n        https://setuptools.readthedocs.io/en/latest/pkg_resources.html#basic-resource-access\n\n        >>> warned = getfixture('recwarn')\n        >>> warnings.simplefilter('always')\n        >>> vrp = NullProvider._validate_resource_path\n        >>> vrp('foo/bar.txt')\n        >>> bool(warned)\n        False\n        >>> vrp('../foo/bar.txt')\n        >>> bool(warned)\n        True\n        >>> warned.clear()\n        >>> vrp('/foo/bar.txt')\n        >>> bool(warned)\n        True\n        >>> vrp('foo/../../bar.txt')\n        >>> bool(warned)\n        True\n        >>> warned.clear()\n        >>> vrp('foo/f../bar.txt')\n        >>> bool(warned)\n        False\n\n        Windows path separators are straight-up disallowed.\n        >>> vrp(r'\\\\foo/bar.txt')\n        Traceback (most recent call last):\n        ...\n        ValueError: Use of .. or absolute path in a resource path \\\nis not allowed.\n\n        >>> vrp(r'C:\\\\foo/bar.txt')\n        Traceback (most recent call last):\n        ...\n        ValueError: Use of .. or absolute path in a resource path \\\nis not allowed.\n\n        Blank values are allowed\n\n        >>> vrp('')\n        >>> bool(warned)\n        False\n\n        Non-string values are not.\n\n        >>> vrp(None)\n        Traceback (most recent call last):\n        ...\n        AttributeError: ...\n        \"\"\"\n        invalid = (\n            os.path.pardir in path.split(posixpath.sep) or\n            posixpath.isabs(path) or\n            ntpath.isabs(path)\n        )\n        if not invalid:\n            return\n\n        msg = \"Use of .. or absolute path in a resource path is not allowed.\"\n\n        # Aggressively disallow Windows absolute paths\n        if ntpath.isabs(path) and not posixpath.isabs(path):\n            raise ValueError(msg)\n\n        # for compatibility, warn; in future\n        # raise ValueError(msg)\n        warnings.warn(\n            msg[:-1] + \" and will raise exceptions in a future release.\",\n            DeprecationWarning,\n            stacklevel=4,\n        )\n\n    def _get(self, path):\n        if hasattr(self.loader, 'get_data'):\n            return self.loader.get_data(path)\n        raise NotImplementedError(\n            \"Can't perform this operation for loaders without 'get_data()'\"\n        )\n\n\nregister_loader_type(object, NullProvider)\n\n\nclass EggProvider(NullProvider):\n    \"\"\"Provider based on a virtual filesystem\"\"\"\n\n    def __init__(self, module):\n        NullProvider.__init__(self, module)\n        self._setup_prefix()\n\n    def _setup_prefix(self):\n        # we assume here that our metadata may be nested inside a \"basket\"\n        # of multiple eggs; that's why we use module_path instead of .archive\n        path = self.module_path\n        old = None\n        while path != old:\n            if _is_egg_path(path):\n                self.egg_name = os.path.basename(path)\n                self.egg_info = os.path.join(path, 'EGG-INFO')\n                self.egg_root = path\n                break\n            old = path\n            path, base = os.path.split(path)\n\n\nclass DefaultProvider(EggProvider):\n    \"\"\"Provides access to package resources in the filesystem\"\"\"\n\n    def _has(self, path):\n        return os.path.exists(path)\n\n    def _isdir(self, path):\n        return os.path.isdir(path)\n\n    def _listdir(self, path):\n        return os.listdir(path)\n\n    def get_resource_stream(self, manager, resource_name):\n        return open(self._fn(self.module_path, resource_name), 'rb')\n\n    def _get(self, path):\n        with open(path, 'rb') as stream:\n            return stream.read()\n\n    @classmethod\n    def _register(cls):\n        loader_names = 'SourceFileLoader', 'SourcelessFileLoader',\n        for name in loader_names:\n            loader_cls = getattr(importlib_machinery, name, type(None))\n            register_loader_type(loader_cls, cls)\n\n\nDefaultProvider._register()\n\n\nclass EmptyProvider(NullProvider):\n    \"\"\"Provider that returns nothing for all requests\"\"\"\n\n    module_path = None\n\n    _isdir = _has = lambda self, path: False\n\n    def _get(self, path):\n        return ''\n\n    def _listdir(self, path):\n        return []\n\n    def __init__(self):\n        pass\n\n\nempty_provider = EmptyProvider()\n\n\nclass ZipManifests(dict):\n    \"\"\"\n    zip manifest builder\n    \"\"\"\n\n    @classmethod\n    def build(cls, path):\n        \"\"\"\n        Build a dictionary similar to the zipimport directory\n        caches, except instead of tuples, store ZipInfo objects.\n\n        Use a platform-specific path separator (os.sep) for the path keys\n        for compatibility with pypy on Windows.\n        \"\"\"\n        with zipfile.ZipFile(path) as zfile:\n            items = (\n                (\n                    name.replace('/', os.sep),\n                    zfile.getinfo(name),\n                )\n                for name in zfile.namelist()\n            )\n            return dict(items)\n\n    load = build\n\n\nclass MemoizedZipManifests(ZipManifests):\n    \"\"\"\n    Memoized zipfile manifests.\n    \"\"\"\n    manifest_mod = collections.namedtuple('manifest_mod', 'manifest mtime')\n\n    def load(self, path):\n        \"\"\"\n        Load a manifest at path or return a suitable manifest already loaded.\n        \"\"\"\n        path = os.path.normpath(path)\n        mtime = os.stat(path).st_mtime\n\n        if path not in self or self[path].mtime != mtime:\n            manifest = self.build(path)\n            self[path] = self.manifest_mod(manifest, mtime)\n\n        return self[path].manifest\n\n\nclass ZipProvider(EggProvider):\n    \"\"\"Resource support for zips and eggs\"\"\"\n\n    eagers = None\n    _zip_manifests = MemoizedZipManifests()\n\n    def __init__(self, module):\n        EggProvider.__init__(self, module)\n        self.zip_pre = self.loader.archive + os.sep\n\n    def _zipinfo_name(self, fspath):\n        # Convert a virtual filename (full path to file) into a zipfile subpath\n        # usable with the zipimport directory cache for our target archive\n        fspath = fspath.rstrip(os.sep)\n        if fspath == self.loader.archive:\n            return ''\n        if fspath.startswith(self.zip_pre):\n            return fspath[len(self.zip_pre):]\n        raise AssertionError(\n            \"%s is not a subpath of %s\" % (fspath, self.zip_pre)\n        )\n\n    def _parts(self, zip_path):\n        # Convert a zipfile subpath into an egg-relative path part list.\n        # pseudo-fs path\n        fspath = self.zip_pre + zip_path\n        if fspath.startswith(self.egg_root + os.sep):\n            return fspath[len(self.egg_root) + 1:].split(os.sep)\n        raise AssertionError(\n            \"%s is not a subpath of %s\" % (fspath, self.egg_root)\n        )\n\n    @property\n    def zipinfo(self):\n        return self._zip_manifests.load(self.loader.archive)\n\n    def get_resource_filename(self, manager, resource_name):\n        if not self.egg_name:\n            raise NotImplementedError(\n                \"resource_filename() only supported for .egg, not .zip\"\n            )\n        # no need to lock for extraction, since we use temp names\n        zip_path = self._resource_to_zip(resource_name)\n        eagers = self._get_eager_resources()\n        if '/'.join(self._parts(zip_path)) in eagers:\n            for name in eagers:\n                self._extract_resource(manager, self._eager_to_zip(name))\n        return self._extract_resource(manager, zip_path)\n\n    @staticmethod\n    def _get_date_and_size(zip_stat):\n        size = zip_stat.file_size\n        # ymdhms+wday, yday, dst\n        date_time = zip_stat.date_time + (0, 0, -1)\n        # 1980 offset already done\n        timestamp = time.mktime(date_time)\n        return timestamp, size\n\n    def _extract_resource(self, manager, zip_path):\n\n        if zip_path in self._index():\n            for name in self._index()[zip_path]:\n                last = self._extract_resource(\n                    manager, os.path.join(zip_path, name)\n                )\n            # return the extracted directory name\n            return os.path.dirname(last)\n\n        timestamp, size = self._get_date_and_size(self.zipinfo[zip_path])\n\n        if not WRITE_SUPPORT:\n            raise IOError('\"os.rename\" and \"os.unlink\" are not supported '\n                          'on this platform')\n        try:\n\n            real_path = manager.get_cache_path(\n                self.egg_name, self._parts(zip_path)\n            )\n\n            if self._is_current(real_path, zip_path):\n                return real_path\n\n            outf, tmpnam = _mkstemp(\n                \".$extract\",\n                dir=os.path.dirname(real_path),\n            )\n            os.write(outf, self.loader.get_data(zip_path))\n            os.close(outf)\n            utime(tmpnam, (timestamp, timestamp))\n            manager.postprocess(tmpnam, real_path)\n\n            try:\n                rename(tmpnam, real_path)\n\n            except os.error:\n                if os.path.isfile(real_path):\n                    if self._is_current(real_path, zip_path):\n                        # the file became current since it was checked above,\n                        #  so proceed.\n                        return real_path\n                    # Windows, del old file and retry\n                    elif os.name == 'nt':\n                        unlink(real_path)\n                        rename(tmpnam, real_path)\n                        return real_path\n                raise\n\n        except os.error:\n            # report a user-friendly error\n            manager.extraction_error()\n\n        return real_path\n\n    def _is_current(self, file_path, zip_path):\n        \"\"\"\n        Return True if the file_path is current for this zip_path\n        \"\"\"\n        timestamp, size = self._get_date_and_size(self.zipinfo[zip_path])\n        if not os.path.isfile(file_path):\n            return False\n        stat = os.stat(file_path)\n        if stat.st_size != size or stat.st_mtime != timestamp:\n            return False\n        # check that the contents match\n        zip_contents = self.loader.get_data(zip_path)\n        with open(file_path, 'rb') as f:\n            file_contents = f.read()\n        return zip_contents == file_contents\n\n    def _get_eager_resources(self):\n        if self.eagers is None:\n            eagers = []\n            for name in ('native_libs.txt', 'eager_resources.txt'):\n                if self.has_metadata(name):\n                    eagers.extend(self.get_metadata_lines(name))\n            self.eagers = eagers\n        return self.eagers\n\n    def _index(self):\n        try:\n            return self._dirindex\n        except AttributeError:\n            ind = {}\n            for path in self.zipinfo:\n                parts = path.split(os.sep)\n                while parts:\n                    parent = os.sep.join(parts[:-1])\n                    if parent in ind:\n                        ind[parent].append(parts[-1])\n                        break\n                    else:\n                        ind[parent] = [parts.pop()]\n            self._dirindex = ind\n            return ind\n\n    def _has(self, fspath):\n        zip_path = self._zipinfo_name(fspath)\n        return zip_path in self.zipinfo or zip_path in self._index()\n\n    def _isdir(self, fspath):\n        return self._zipinfo_name(fspath) in self._index()\n\n    def _listdir(self, fspath):\n        return list(self._index().get(self._zipinfo_name(fspath), ()))\n\n    def _eager_to_zip(self, resource_name):\n        return self._zipinfo_name(self._fn(self.egg_root, resource_name))\n\n    def _resource_to_zip(self, resource_name):\n        return self._zipinfo_name(self._fn(self.module_path, resource_name))\n\n\nregister_loader_type(zipimport.zipimporter, ZipProvider)\n\n\nclass FileMetadata(EmptyProvider):\n    \"\"\"Metadata handler for standalone PKG-INFO files\n\n    Usage::\n\n        metadata = FileMetadata(\"/path/to/PKG-INFO\")\n\n    This provider rejects all data and metadata requests except for PKG-INFO,\n    which is treated as existing, and will be the contents of the file at\n    the provided location.\n    \"\"\"\n\n    def __init__(self, path):\n        self.path = path\n\n    def _get_metadata_path(self, name):\n        return self.path\n\n    def has_metadata(self, name):\n        return name == 'PKG-INFO' and os.path.isfile(self.path)\n\n    def get_metadata(self, name):\n        if name != 'PKG-INFO':\n            raise KeyError(\"No metadata except PKG-INFO is available\")\n\n        with io.open(self.path, encoding='utf-8', errors=\"replace\") as f:\n            metadata = f.read()\n        self._warn_on_replacement(metadata)\n        return metadata\n\n    def _warn_on_replacement(self, metadata):\n        # Python 2.7 compat for: replacement_char = '�'\n        replacement_char = b'\\xef\\xbf\\xbd'.decode('utf-8')\n        if replacement_char in metadata:\n            tmpl = \"{self.path} could not be properly decoded in UTF-8\"\n            msg = tmpl.format(**locals())\n            warnings.warn(msg)\n\n    def get_metadata_lines(self, name):\n        return yield_lines(self.get_metadata(name))\n\n\nclass PathMetadata(DefaultProvider):\n    \"\"\"Metadata provider for egg directories\n\n    Usage::\n\n        # Development eggs:\n\n        egg_info = \"/path/to/PackageName.egg-info\"\n        base_dir = os.path.dirname(egg_info)\n        metadata = PathMetadata(base_dir, egg_info)\n        dist_name = os.path.splitext(os.path.basename(egg_info))[0]\n        dist = Distribution(basedir, project_name=dist_name, metadata=metadata)\n\n        # Unpacked egg directories:\n\n        egg_path = \"/path/to/PackageName-ver-pyver-etc.egg\"\n        metadata = PathMetadata(egg_path, os.path.join(egg_path,'EGG-INFO'))\n        dist = Distribution.from_filename(egg_path, metadata=metadata)\n    \"\"\"\n\n    def __init__(self, path, egg_info):\n        self.module_path = path\n        self.egg_info = egg_info\n\n\nclass EggMetadata(ZipProvider):\n    \"\"\"Metadata provider for .egg files\"\"\"\n\n    def __init__(self, importer):\n        \"\"\"Create a metadata provider from a zipimporter\"\"\"\n\n        self.zip_pre = importer.archive + os.sep\n        self.loader = importer\n        if importer.prefix:\n            self.module_path = os.path.join(importer.archive, importer.prefix)\n        else:\n            self.module_path = importer.archive\n        self._setup_prefix()\n\n\n_declare_state('dict', _distribution_finders={})\n\n\ndef register_finder(importer_type, distribution_finder):\n    \"\"\"Register `distribution_finder` to find distributions in sys.path items\n\n    `importer_type` is the type or class of a PEP 302 \"Importer\" (sys.path item\n    handler), and `distribution_finder` is a callable that, passed a path\n    item and the importer instance, yields ``Distribution`` instances found on\n    that path item.  See ``pkg_resources.find_on_path`` for an example.\"\"\"\n    _distribution_finders[importer_type] = distribution_finder\n\n\ndef find_distributions(path_item, only=False):\n    \"\"\"Yield distributions accessible via `path_item`\"\"\"\n    importer = get_importer(path_item)\n    finder = _find_adapter(_distribution_finders, importer)\n    return finder(importer, path_item, only)\n\n\ndef find_eggs_in_zip(importer, path_item, only=False):\n    \"\"\"\n    Find eggs in zip files; possibly multiple nested eggs.\n    \"\"\"\n    if importer.archive.endswith('.whl'):\n        # wheels are not supported with this finder\n        # they don't have PKG-INFO metadata, and won't ever contain eggs\n        return\n    metadata = EggMetadata(importer)\n    if metadata.has_metadata('PKG-INFO'):\n        yield Distribution.from_filename(path_item, metadata=metadata)\n    if only:\n        # don't yield nested distros\n        return\n    for subitem in metadata.resource_listdir(''):\n        if _is_egg_path(subitem):\n            subpath = os.path.join(path_item, subitem)\n            dists = find_eggs_in_zip(zipimport.zipimporter(subpath), subpath)\n            for dist in dists:\n                yield dist\n        elif subitem.lower().endswith('.dist-info'):\n            subpath = os.path.join(path_item, subitem)\n            submeta = EggMetadata(zipimport.zipimporter(subpath))\n            submeta.egg_info = subpath\n            yield Distribution.from_location(path_item, subitem, submeta)\n\n\nregister_finder(zipimport.zipimporter, find_eggs_in_zip)\n\n\ndef find_nothing(importer, path_item, only=False):\n    return ()\n\n\nregister_finder(object, find_nothing)\n\n\ndef _by_version_descending(names):\n    \"\"\"\n    Given a list of filenames, return them in descending order\n    by version number.\n\n    >>> names = 'bar', 'foo', 'Python-2.7.10.egg', 'Python-2.7.2.egg'\n    >>> _by_version_descending(names)\n    ['Python-2.7.10.egg', 'Python-2.7.2.egg', 'foo', 'bar']\n    >>> names = 'Setuptools-1.2.3b1.egg', 'Setuptools-1.2.3.egg'\n    >>> _by_version_descending(names)\n    ['Setuptools-1.2.3.egg', 'Setuptools-1.2.3b1.egg']\n    >>> names = 'Setuptools-1.2.3b1.egg', 'Setuptools-1.2.3.post1.egg'\n    >>> _by_version_descending(names)\n    ['Setuptools-1.2.3.post1.egg', 'Setuptools-1.2.3b1.egg']\n    \"\"\"\n    def _by_version(name):\n        \"\"\"\n        Parse each component of the filename\n        \"\"\"\n        name, ext = os.path.splitext(name)\n        parts = itertools.chain(name.split('-'), [ext])\n        return [packaging.version.parse(part) for part in parts]\n\n    return sorted(names, key=_by_version, reverse=True)\n\n\ndef find_on_path(importer, path_item, only=False):\n    \"\"\"Yield distributions accessible on a sys.path directory\"\"\"\n    path_item = _normalize_cached(path_item)\n\n    if _is_unpacked_egg(path_item):\n        yield Distribution.from_filename(\n            path_item, metadata=PathMetadata(\n                path_item, os.path.join(path_item, 'EGG-INFO')\n            )\n        )\n        return\n\n    entries = safe_listdir(path_item)\n\n    # for performance, before sorting by version,\n    # screen entries for only those that will yield\n    # distributions\n    filtered = (\n        entry\n        for entry in entries\n        if dist_factory(path_item, entry, only)\n    )\n\n    # scan for .egg and .egg-info in directory\n    path_item_entries = _by_version_descending(filtered)\n    for entry in path_item_entries:\n        fullpath = os.path.join(path_item, entry)\n        factory = dist_factory(path_item, entry, only)\n        for dist in factory(fullpath):\n            yield dist\n\n\ndef dist_factory(path_item, entry, only):\n    \"\"\"\n    Return a dist_factory for a path_item and entry\n    \"\"\"\n    lower = entry.lower()\n    is_meta = any(map(lower.endswith, ('.egg-info', '.dist-info')))\n    return (\n        distributions_from_metadata\n        if is_meta else\n        find_distributions\n        if not only and _is_egg_path(entry) else\n        resolve_egg_link\n        if not only and lower.endswith('.egg-link') else\n        NoDists()\n    )\n\n\nclass NoDists:\n    \"\"\"\n    >>> bool(NoDists())\n    False\n\n    >>> list(NoDists()('anything'))\n    []\n    \"\"\"\n    def __bool__(self):\n        return False\n    if six.PY2:\n        __nonzero__ = __bool__\n\n    def __call__(self, fullpath):\n        return iter(())\n\n\ndef safe_listdir(path):\n    \"\"\"\n    Attempt to list contents of path, but suppress some exceptions.\n    \"\"\"\n    try:\n        return os.listdir(path)\n    except (PermissionError, NotADirectoryError):\n        pass\n    except OSError as e:\n        # Ignore the directory if does not exist, not a directory or\n        # permission denied\n        ignorable = (\n            e.errno in (errno.ENOTDIR, errno.EACCES, errno.ENOENT)\n            # Python 2 on Windows needs to be handled this way :(\n            or getattr(e, \"winerror\", None) == 267\n        )\n        if not ignorable:\n            raise\n    return ()\n\n\ndef distributions_from_metadata(path):\n    root = os.path.dirname(path)\n    if os.path.isdir(path):\n        if len(os.listdir(path)) == 0:\n            # empty metadata dir; skip\n            return\n        metadata = PathMetadata(root, path)\n    else:\n        metadata = FileMetadata(path)\n    entry = os.path.basename(path)\n    yield Distribution.from_location(\n        root, entry, metadata, precedence=DEVELOP_DIST,\n    )\n\n\ndef non_empty_lines(path):\n    \"\"\"\n    Yield non-empty lines from file at path\n    \"\"\"\n    with open(path) as f:\n        for line in f:\n            line = line.strip()\n            if line:\n                yield line\n\n\ndef resolve_egg_link(path):\n    \"\"\"\n    Given a path to an .egg-link, resolve distributions\n    present in the referenced path.\n    \"\"\"\n    referenced_paths = non_empty_lines(path)\n    resolved_paths = (\n        os.path.join(os.path.dirname(path), ref)\n        for ref in referenced_paths\n    )\n    dist_groups = map(find_distributions, resolved_paths)\n    return next(dist_groups, ())\n\n\nregister_finder(pkgutil.ImpImporter, find_on_path)\n\nif hasattr(importlib_machinery, 'FileFinder'):\n    register_finder(importlib_machinery.FileFinder, find_on_path)\n\n_declare_state('dict', _namespace_handlers={})\n_declare_state('dict', _namespace_packages={})\n\n\ndef register_namespace_handler(importer_type, namespace_handler):\n    \"\"\"Register `namespace_handler` to declare namespace packages\n\n    `importer_type` is the type or class of a PEP 302 \"Importer\" (sys.path item\n    handler), and `namespace_handler` is a callable like this::\n\n        def namespace_handler(importer, path_entry, moduleName, module):\n            # return a path_entry to use for child packages\n\n    Namespace handlers are only called if the importer object has already\n    agreed that it can handle the relevant path item, and they should only\n    return a subpath if the module __path__ does not already contain an\n    equivalent subpath.  For an example namespace handler, see\n    ``pkg_resources.file_ns_handler``.\n    \"\"\"\n    _namespace_handlers[importer_type] = namespace_handler\n\n\ndef _handle_ns(packageName, path_item):\n    \"\"\"Ensure that named package includes a subpath of path_item (if needed)\"\"\"\n\n    importer = get_importer(path_item)\n    if importer is None:\n        return None\n\n    # capture warnings due to #1111\n    with warnings.catch_warnings():\n        warnings.simplefilter(\"ignore\")\n        loader = importer.find_module(packageName)\n\n    if loader is None:\n        return None\n    module = sys.modules.get(packageName)\n    if module is None:\n        module = sys.modules[packageName] = types.ModuleType(packageName)\n        module.__path__ = []\n        _set_parent_ns(packageName)\n    elif not hasattr(module, '__path__'):\n        raise TypeError(\"Not a package:\", packageName)\n    handler = _find_adapter(_namespace_handlers, importer)\n    subpath = handler(importer, path_item, packageName, module)\n    if subpath is not None:\n        path = module.__path__\n        path.append(subpath)\n        loader.load_module(packageName)\n        _rebuild_mod_path(path, packageName, module)\n    return subpath\n\n\ndef _rebuild_mod_path(orig_path, package_name, module):\n    \"\"\"\n    Rebuild module.__path__ ensuring that all entries are ordered\n    corresponding to their sys.path order\n    \"\"\"\n    sys_path = [_normalize_cached(p) for p in sys.path]\n\n    def safe_sys_path_index(entry):\n        \"\"\"\n        Workaround for #520 and #513.\n        \"\"\"\n        try:\n            return sys_path.index(entry)\n        except ValueError:\n            return float('inf')\n\n    def position_in_sys_path(path):\n        \"\"\"\n        Return the ordinal of the path based on its position in sys.path\n        \"\"\"\n        path_parts = path.split(os.sep)\n        module_parts = package_name.count('.') + 1\n        parts = path_parts[:-module_parts]\n        return safe_sys_path_index(_normalize_cached(os.sep.join(parts)))\n\n    new_path = sorted(orig_path, key=position_in_sys_path)\n    new_path = [_normalize_cached(p) for p in new_path]\n\n    if isinstance(module.__path__, list):\n        module.__path__[:] = new_path\n    else:\n        module.__path__ = new_path\n\n\ndef declare_namespace(packageName):\n    \"\"\"Declare that package 'packageName' is a namespace package\"\"\"\n\n    _imp.acquire_lock()\n    try:\n        if packageName in _namespace_packages:\n            return\n\n        path = sys.path\n        parent, _, _ = packageName.rpartition('.')\n\n        if parent:\n            declare_namespace(parent)\n            if parent not in _namespace_packages:\n                __import__(parent)\n            try:\n                path = sys.modules[parent].__path__\n            except AttributeError:\n                raise TypeError(\"Not a package:\", parent)\n\n        # Track what packages are namespaces, so when new path items are added,\n        # they can be updated\n        _namespace_packages.setdefault(parent or None, []).append(packageName)\n        _namespace_packages.setdefault(packageName, [])\n\n        for path_item in path:\n            # Ensure all the parent's path items are reflected in the child,\n            # if they apply\n            _handle_ns(packageName, path_item)\n\n    finally:\n        _imp.release_lock()\n\n\ndef fixup_namespace_packages(path_item, parent=None):\n    \"\"\"Ensure that previously-declared namespace packages include path_item\"\"\"\n    _imp.acquire_lock()\n    try:\n        for package in _namespace_packages.get(parent, ()):\n            subpath = _handle_ns(package, path_item)\n            if subpath:\n                fixup_namespace_packages(subpath, package)\n    finally:\n        _imp.release_lock()\n\n\ndef file_ns_handler(importer, path_item, packageName, module):\n    \"\"\"Compute an ns-package subpath for a filesystem or zipfile importer\"\"\"\n\n    subpath = os.path.join(path_item, packageName.split('.')[-1])\n    normalized = _normalize_cached(subpath)\n    for item in module.__path__:\n        if _normalize_cached(item) == normalized:\n            break\n    else:\n        # Only return the path if it's not already there\n        return subpath\n\n\nregister_namespace_handler(pkgutil.ImpImporter, file_ns_handler)\nregister_namespace_handler(zipimport.zipimporter, file_ns_handler)\n\nif hasattr(importlib_machinery, 'FileFinder'):\n    register_namespace_handler(importlib_machinery.FileFinder, file_ns_handler)\n\n\ndef null_ns_handler(importer, path_item, packageName, module):\n    return None\n\n\nregister_namespace_handler(object, null_ns_handler)\n\n\ndef normalize_path(filename):\n    \"\"\"Normalize a file/dir name for comparison purposes\"\"\"\n    return os.path.normcase(os.path.realpath(os.path.normpath(_cygwin_patch(filename))))\n\n\ndef _cygwin_patch(filename):  # pragma: nocover\n    \"\"\"\n    Contrary to POSIX 2008, on Cygwin, getcwd (3) contains\n    symlink components. Using\n    os.path.abspath() works around this limitation. A fix in os.getcwd()\n    would probably better, in Cygwin even more so, except\n    that this seems to be by design...\n    \"\"\"\n    return os.path.abspath(filename) if sys.platform == 'cygwin' else filename\n\n\ndef _normalize_cached(filename, _cache={}):\n    try:\n        return _cache[filename]\n    except KeyError:\n        _cache[filename] = result = normalize_path(filename)\n        return result\n\n\ndef _is_egg_path(path):\n    \"\"\"\n    Determine if given path appears to be an egg.\n    \"\"\"\n    return path.lower().endswith('.egg')\n\n\ndef _is_unpacked_egg(path):\n    \"\"\"\n    Determine if given path appears to be an unpacked egg.\n    \"\"\"\n    return (\n        _is_egg_path(path) and\n        os.path.isfile(os.path.join(path, 'EGG-INFO', 'PKG-INFO'))\n    )\n\n\ndef _set_parent_ns(packageName):\n    parts = packageName.split('.')\n    name = parts.pop()\n    if parts:\n        parent = '.'.join(parts)\n        setattr(sys.modules[parent], name, sys.modules[packageName])\n\n\ndef yield_lines(strs):\n    \"\"\"Yield non-empty/non-comment lines of a string or sequence\"\"\"\n    if isinstance(strs, six.string_types):\n        for s in strs.splitlines():\n            s = s.strip()\n            # skip blank lines/comments\n            if s and not s.startswith('#'):\n                yield s\n    else:\n        for ss in strs:\n            for s in yield_lines(ss):\n                yield s\n\n\nMODULE = re.compile(r\"\\w+(\\.\\w+)*$\").match\nEGG_NAME = re.compile(\n    r\"\"\"\n    (?P<name>[^-]+) (\n        -(?P<ver>[^-]+) (\n            -py(?P<pyver>[^-]+) (\n                -(?P<plat>.+)\n            )?\n        )?\n    )?\n    \"\"\",\n    re.VERBOSE | re.IGNORECASE,\n).match\n\n\nclass EntryPoint:\n    \"\"\"Object representing an advertised importable object\"\"\"\n\n    def __init__(self, name, module_name, attrs=(), extras=(), dist=None):\n        if not MODULE(module_name):\n            raise ValueError(\"Invalid module name\", module_name)\n        self.name = name\n        self.module_name = module_name\n        self.attrs = tuple(attrs)\n        self.extras = tuple(extras)\n        self.dist = dist\n\n    def __str__(self):\n        s = \"%s = %s\" % (self.name, self.module_name)\n        if self.attrs:\n            s += ':' + '.'.join(self.attrs)\n        if self.extras:\n            s += ' [%s]' % ','.join(self.extras)\n        return s\n\n    def __repr__(self):\n        return \"EntryPoint.parse(%r)\" % str(self)\n\n    def load(self, require=True, *args, **kwargs):\n        \"\"\"\n        Require packages for this EntryPoint, then resolve it.\n        \"\"\"\n        if not require or args or kwargs:\n            warnings.warn(\n                \"Parameters to load are deprecated.  Call .resolve and \"\n                \".require separately.\",\n                PkgResourcesDeprecationWarning,\n                stacklevel=2,\n            )\n        if require:\n            self.require(*args, **kwargs)\n        return self.resolve()\n\n    def resolve(self):\n        \"\"\"\n        Resolve the entry point from its module and attrs.\n        \"\"\"\n        module = __import__(self.module_name, fromlist=['__name__'], level=0)\n        try:\n            return functools.reduce(getattr, self.attrs, module)\n        except AttributeError as exc:\n            raise ImportError(str(exc))\n\n    def require(self, env=None, installer=None):\n        if self.extras and not self.dist:\n            raise UnknownExtra(\"Can't require() without a distribution\", self)\n\n        # Get the requirements for this entry point with all its extras and\n        # then resolve them. We have to pass `extras` along when resolving so\n        # that the working set knows what extras we want. Otherwise, for\n        # dist-info distributions, the working set will assume that the\n        # requirements for that extra are purely optional and skip over them.\n        reqs = self.dist.requires(self.extras)\n        items = working_set.resolve(reqs, env, installer, extras=self.extras)\n        list(map(working_set.add, items))\n\n    pattern = re.compile(\n        r'\\s*'\n        r'(?P<name>.+?)\\s*'\n        r'=\\s*'\n        r'(?P<module>[\\w.]+)\\s*'\n        r'(:\\s*(?P<attr>[\\w.]+))?\\s*'\n        r'(?P<extras>\\[.*\\])?\\s*$'\n    )\n\n    @classmethod\n    def parse(cls, src, dist=None):\n        \"\"\"Parse a single entry point from string `src`\n\n        Entry point syntax follows the form::\n\n            name = some.module:some.attr [extra1, extra2]\n\n        The entry name and module name are required, but the ``:attrs`` and\n        ``[extras]`` parts are optional\n        \"\"\"\n        m = cls.pattern.match(src)\n        if not m:\n            msg = \"EntryPoint must be in 'name=module:attrs [extras]' format\"\n            raise ValueError(msg, src)\n        res = m.groupdict()\n        extras = cls._parse_extras(res['extras'])\n        attrs = res['attr'].split('.') if res['attr'] else ()\n        return cls(res['name'], res['module'], attrs, extras, dist)\n\n    @classmethod\n    def _parse_extras(cls, extras_spec):\n        if not extras_spec:\n            return ()\n        req = Requirement.parse('x' + extras_spec)\n        if req.specs:\n            raise ValueError()\n        return req.extras\n\n    @classmethod\n    def parse_group(cls, group, lines, dist=None):\n        \"\"\"Parse an entry point group\"\"\"\n        if not MODULE(group):\n            raise ValueError(\"Invalid group name\", group)\n        this = {}\n        for line in yield_lines(lines):\n            ep = cls.parse(line, dist)\n            if ep.name in this:\n                raise ValueError(\"Duplicate entry point\", group, ep.name)\n            this[ep.name] = ep\n        return this\n\n    @classmethod\n    def parse_map(cls, data, dist=None):\n        \"\"\"Parse a map of entry point groups\"\"\"\n        if isinstance(data, dict):\n            data = data.items()\n        else:\n            data = split_sections(data)\n        maps = {}\n        for group, lines in data:\n            if group is None:\n                if not lines:\n                    continue\n                raise ValueError(\"Entry points must be listed in groups\")\n            group = group.strip()\n            if group in maps:\n                raise ValueError(\"Duplicate group name\", group)\n            maps[group] = cls.parse_group(group, lines, dist)\n        return maps\n\n\ndef _remove_md5_fragment(location):\n    if not location:\n        return ''\n    parsed = urllib.parse.urlparse(location)\n    if parsed[-1].startswith('md5='):\n        return urllib.parse.urlunparse(parsed[:-1] + ('',))\n    return location\n\n\ndef _version_from_file(lines):\n    \"\"\"\n    Given an iterable of lines from a Metadata file, return\n    the value of the Version field, if present, or None otherwise.\n    \"\"\"\n    def is_version_line(line):\n        return line.lower().startswith('version:')\n    version_lines = filter(is_version_line, lines)\n    line = next(iter(version_lines), '')\n    _, _, value = line.partition(':')\n    return safe_version(value.strip()) or None\n\n\nclass Distribution:\n    \"\"\"Wrap an actual or potential sys.path entry w/metadata\"\"\"\n    PKG_INFO = 'PKG-INFO'\n\n    def __init__(\n            self, location=None, metadata=None, project_name=None,\n            version=None, py_version=PY_MAJOR, platform=None,\n            precedence=EGG_DIST):\n        self.project_name = safe_name(project_name or 'Unknown')\n        if version is not None:\n            self._version = safe_version(version)\n        self.py_version = py_version\n        self.platform = platform\n        self.location = location\n        self.precedence = precedence\n        self._provider = metadata or empty_provider\n\n    @classmethod\n    def from_location(cls, location, basename, metadata=None, **kw):\n        project_name, version, py_version, platform = [None] * 4\n        basename, ext = os.path.splitext(basename)\n        if ext.lower() in _distributionImpl:\n            cls = _distributionImpl[ext.lower()]\n\n            match = EGG_NAME(basename)\n            if match:\n                project_name, version, py_version, platform = match.group(\n                    'name', 'ver', 'pyver', 'plat'\n                )\n        return cls(\n            location, metadata, project_name=project_name, version=version,\n            py_version=py_version, platform=platform, **kw\n        )._reload_version()\n\n    def _reload_version(self):\n        return self\n\n    @property\n    def hashcmp(self):\n        return (\n            self.parsed_version,\n            self.precedence,\n            self.key,\n            _remove_md5_fragment(self.location),\n            self.py_version or '',\n            self.platform or '',\n        )\n\n    def __hash__(self):\n        return hash(self.hashcmp)\n\n    def __lt__(self, other):\n        return self.hashcmp < other.hashcmp\n\n    def __le__(self, other):\n        return self.hashcmp <= other.hashcmp\n\n    def __gt__(self, other):\n        return self.hashcmp > other.hashcmp\n\n    def __ge__(self, other):\n        return self.hashcmp >= other.hashcmp\n\n    def __eq__(self, other):\n        if not isinstance(other, self.__class__):\n            # It's not a Distribution, so they are not equal\n            return False\n        return self.hashcmp == other.hashcmp\n\n    def __ne__(self, other):\n        return not self == other\n\n    # These properties have to be lazy so that we don't have to load any\n    # metadata until/unless it's actually needed.  (i.e., some distributions\n    # may not know their name or version without loading PKG-INFO)\n\n    @property\n    def key(self):\n        try:\n            return self._key\n        except AttributeError:\n            self._key = key = self.project_name.lower()\n            return key\n\n    @property\n    def parsed_version(self):\n        if not hasattr(self, \"_parsed_version\"):\n            self._parsed_version = parse_version(self.version)\n\n        return self._parsed_version\n\n    def _warn_legacy_version(self):\n        LV = packaging.version.LegacyVersion\n        is_legacy = isinstance(self._parsed_version, LV)\n        if not is_legacy:\n            return\n\n        # While an empty version is technically a legacy version and\n        # is not a valid PEP 440 version, it's also unlikely to\n        # actually come from someone and instead it is more likely that\n        # it comes from setuptools attempting to parse a filename and\n        # including it in the list. So for that we'll gate this warning\n        # on if the version is anything at all or not.\n        if not self.version:\n            return\n\n        tmpl = textwrap.dedent(\"\"\"\n            '{project_name} ({version})' is being parsed as a legacy,\n            non PEP 440,\n            version. You may find odd behavior and sort order.\n            In particular it will be sorted as less than 0.0. It\n            is recommended to migrate to PEP 440 compatible\n            versions.\n            \"\"\").strip().replace('\\n', ' ')\n\n        warnings.warn(tmpl.format(**vars(self)), PEP440Warning)\n\n    @property\n    def version(self):\n        try:\n            return self._version\n        except AttributeError:\n            version = self._get_version()\n            if version is None:\n                path = self._get_metadata_path_for_display(self.PKG_INFO)\n                msg = (\n                    \"Missing 'Version:' header and/or {} file at path: {}\"\n                ).format(self.PKG_INFO, path)\n                raise ValueError(msg, self)\n\n            return version\n\n    @property\n    def _dep_map(self):\n        \"\"\"\n        A map of extra to its list of (direct) requirements\n        for this distribution, including the null extra.\n        \"\"\"\n        try:\n            return self.__dep_map\n        except AttributeError:\n            self.__dep_map = self._filter_extras(self._build_dep_map())\n        return self.__dep_map\n\n    @staticmethod\n    def _filter_extras(dm):\n        \"\"\"\n        Given a mapping of extras to dependencies, strip off\n        environment markers and filter out any dependencies\n        not matching the markers.\n        \"\"\"\n        for extra in list(filter(None, dm)):\n            new_extra = extra\n            reqs = dm.pop(extra)\n            new_extra, _, marker = extra.partition(':')\n            fails_marker = marker and (\n                invalid_marker(marker)\n                or not evaluate_marker(marker)\n            )\n            if fails_marker:\n                reqs = []\n            new_extra = safe_extra(new_extra) or None\n\n            dm.setdefault(new_extra, []).extend(reqs)\n        return dm\n\n    def _build_dep_map(self):\n        dm = {}\n        for name in 'requires.txt', 'depends.txt':\n            for extra, reqs in split_sections(self._get_metadata(name)):\n                dm.setdefault(extra, []).extend(parse_requirements(reqs))\n        return dm\n\n    def requires(self, extras=()):\n        \"\"\"List of Requirements needed for this distro if `extras` are used\"\"\"\n        dm = self._dep_map\n        deps = []\n        deps.extend(dm.get(None, ()))\n        for ext in extras:\n            try:\n                deps.extend(dm[safe_extra(ext)])\n            except KeyError:\n                raise UnknownExtra(\n                    \"%s has no such extra feature %r\" % (self, ext)\n                )\n        return deps\n\n    def _get_metadata_path_for_display(self, name):\n        \"\"\"\n        Return the path to the given metadata file, if available.\n        \"\"\"\n        try:\n            # We need to access _get_metadata_path() on the provider object\n            # directly rather than through this class's __getattr__()\n            # since _get_metadata_path() is marked private.\n            path = self._provider._get_metadata_path(name)\n\n        # Handle exceptions e.g. in case the distribution's metadata\n        # provider doesn't support _get_metadata_path().\n        except Exception:\n            return '[could not detect]'\n\n        return path\n\n    def _get_metadata(self, name):\n        if self.has_metadata(name):\n            for line in self.get_metadata_lines(name):\n                yield line\n\n    def _get_version(self):\n        lines = self._get_metadata(self.PKG_INFO)\n        version = _version_from_file(lines)\n\n        return version\n\n    def activate(self, path=None, replace=False):\n        \"\"\"Ensure distribution is importable on `path` (default=sys.path)\"\"\"\n        if path is None:\n            path = sys.path\n        self.insert_on(path, replace=replace)\n        if path is sys.path:\n            fixup_namespace_packages(self.location)\n            for pkg in self._get_metadata('namespace_packages.txt'):\n                if pkg in sys.modules:\n                    declare_namespace(pkg)\n\n    def egg_name(self):\n        \"\"\"Return what this distribution's standard .egg filename should be\"\"\"\n        filename = \"%s-%s-py%s\" % (\n            to_filename(self.project_name), to_filename(self.version),\n            self.py_version or PY_MAJOR\n        )\n\n        if self.platform:\n            filename += '-' + self.platform\n        return filename\n\n    def __repr__(self):\n        if self.location:\n            return \"%s (%s)\" % (self, self.location)\n        else:\n            return str(self)\n\n    def __str__(self):\n        try:\n            version = getattr(self, 'version', None)\n        except ValueError:\n            version = None\n        version = version or \"[unknown version]\"\n        return \"%s %s\" % (self.project_name, version)\n\n    def __getattr__(self, attr):\n        \"\"\"Delegate all unrecognized public attributes to .metadata provider\"\"\"\n        if attr.startswith('_'):\n            raise AttributeError(attr)\n        return getattr(self._provider, attr)\n\n    def __dir__(self):\n        return list(\n            set(super(Distribution, self).__dir__())\n            | set(\n                attr for attr in self._provider.__dir__()\n                if not attr.startswith('_')\n            )\n        )\n\n    if not hasattr(object, '__dir__'):\n        # python 2.7 not supported\n        del __dir__\n\n    @classmethod\n    def from_filename(cls, filename, metadata=None, **kw):\n        return cls.from_location(\n            _normalize_cached(filename), os.path.basename(filename), metadata,\n            **kw\n        )\n\n    def as_requirement(self):\n        \"\"\"Return a ``Requirement`` that matches this distribution exactly\"\"\"\n        if isinstance(self.parsed_version, packaging.version.Version):\n            spec = \"%s==%s\" % (self.project_name, self.parsed_version)\n        else:\n            spec = \"%s===%s\" % (self.project_name, self.parsed_version)\n\n        return Requirement.parse(spec)\n\n    def load_entry_point(self, group, name):\n        \"\"\"Return the `name` entry point of `group` or raise ImportError\"\"\"\n        ep = self.get_entry_info(group, name)\n        if ep is None:\n            raise ImportError(\"Entry point %r not found\" % ((group, name),))\n        return ep.load()\n\n    def get_entry_map(self, group=None):\n        \"\"\"Return the entry point map for `group`, or the full entry map\"\"\"\n        try:\n            ep_map = self._ep_map\n        except AttributeError:\n            ep_map = self._ep_map = EntryPoint.parse_map(\n                self._get_metadata('entry_points.txt'), self\n            )\n        if group is not None:\n            return ep_map.get(group, {})\n        return ep_map\n\n    def get_entry_info(self, group, name):\n        \"\"\"Return the EntryPoint object for `group`+`name`, or ``None``\"\"\"\n        return self.get_entry_map(group).get(name)\n\n    def insert_on(self, path, loc=None, replace=False):\n        \"\"\"Ensure self.location is on path\n\n        If replace=False (default):\n            - If location is already in path anywhere, do nothing.\n            - Else:\n              - If it's an egg and its parent directory is on path,\n                insert just ahead of the parent.\n              - Else: add to the end of path.\n        If replace=True:\n            - If location is already on path anywhere (not eggs)\n              or higher priority than its parent (eggs)\n              do nothing.\n            - Else:\n              - If it's an egg and its parent directory is on path,\n                insert just ahead of the parent,\n                removing any lower-priority entries.\n              - Else: add it to the front of path.\n        \"\"\"\n\n        loc = loc or self.location\n        if not loc:\n            return\n\n        nloc = _normalize_cached(loc)\n        bdir = os.path.dirname(nloc)\n        npath = [(p and _normalize_cached(p) or p) for p in path]\n\n        for p, item in enumerate(npath):\n            if item == nloc:\n                if replace:\n                    break\n                else:\n                    # don't modify path (even removing duplicates) if\n                    # found and not replace\n                    return\n            elif item == bdir and self.precedence == EGG_DIST:\n                # if it's an .egg, give it precedence over its directory\n                # UNLESS it's already been added to sys.path and replace=False\n                if (not replace) and nloc in npath[p:]:\n                    return\n                if path is sys.path:\n                    self.check_version_conflict()\n                path.insert(p, loc)\n                npath.insert(p, nloc)\n                break\n        else:\n            if path is sys.path:\n                self.check_version_conflict()\n            if replace:\n                path.insert(0, loc)\n            else:\n                path.append(loc)\n            return\n\n        # p is the spot where we found or inserted loc; now remove duplicates\n        while True:\n            try:\n                np = npath.index(nloc, p + 1)\n            except ValueError:\n                break\n            else:\n                del npath[np], path[np]\n                # ha!\n                p = np\n\n        return\n\n    def check_version_conflict(self):\n        if self.key == 'setuptools':\n            # ignore the inevitable setuptools self-conflicts  :(\n            return\n\n        nsp = dict.fromkeys(self._get_metadata('namespace_packages.txt'))\n        loc = normalize_path(self.location)\n        for modname in self._get_metadata('top_level.txt'):\n            if (modname not in sys.modules or modname in nsp\n                    or modname in _namespace_packages):\n                continue\n            if modname in ('pkg_resources', 'setuptools', 'site'):\n                continue\n            fn = getattr(sys.modules[modname], '__file__', None)\n            if fn and (normalize_path(fn).startswith(loc) or\n                       fn.startswith(self.location)):\n                continue\n            issue_warning(\n                \"Module %s was already imported from %s, but %s is being added\"\n                \" to sys.path\" % (modname, fn, self.location),\n            )\n\n    def has_version(self):\n        try:\n            self.version\n        except ValueError:\n            issue_warning(\"Unbuilt egg for \" + repr(self))\n            return False\n        return True\n\n    def clone(self, **kw):\n        \"\"\"Copy this distribution, substituting in any changed keyword args\"\"\"\n        names = 'project_name version py_version platform location precedence'\n        for attr in names.split():\n            kw.setdefault(attr, getattr(self, attr, None))\n        kw.setdefault('metadata', self._provider)\n        return self.__class__(**kw)\n\n    @property\n    def extras(self):\n        return [dep for dep in self._dep_map if dep]\n\n\nclass EggInfoDistribution(Distribution):\n    def _reload_version(self):\n        \"\"\"\n        Packages installed by distutils (e.g. numpy or scipy),\n        which uses an old safe_version, and so\n        their version numbers can get mangled when\n        converted to filenames (e.g., 1.11.0.dev0+2329eae to\n        1.11.0.dev0_2329eae). These distributions will not be\n        parsed properly\n        downstream by Distribution and safe_version, so\n        take an extra step and try to get the version number from\n        the metadata file itself instead of the filename.\n        \"\"\"\n        md_version = self._get_version()\n        if md_version:\n            self._version = md_version\n        return self\n\n\nclass DistInfoDistribution(Distribution):\n    \"\"\"\n    Wrap an actual or potential sys.path entry\n    w/metadata, .dist-info style.\n    \"\"\"\n    PKG_INFO = 'METADATA'\n    EQEQ = re.compile(r\"([\\(,])\\s*(\\d.*?)\\s*([,\\)])\")\n\n    @property\n    def _parsed_pkg_info(self):\n        \"\"\"Parse and cache metadata\"\"\"\n        try:\n            return self._pkg_info\n        except AttributeError:\n            metadata = self.get_metadata(self.PKG_INFO)\n            self._pkg_info = email.parser.Parser().parsestr(metadata)\n            return self._pkg_info\n\n    @property\n    def _dep_map(self):\n        try:\n            return self.__dep_map\n        except AttributeError:\n            self.__dep_map = self._compute_dependencies()\n            return self.__dep_map\n\n    def _compute_dependencies(self):\n        \"\"\"Recompute this distribution's dependencies.\"\"\"\n        dm = self.__dep_map = {None: []}\n\n        reqs = []\n        # Including any condition expressions\n        for req in self._parsed_pkg_info.get_all('Requires-Dist') or []:\n            reqs.extend(parse_requirements(req))\n\n        def reqs_for_extra(extra):\n            for req in reqs:\n                if not req.marker or req.marker.evaluate({'extra': extra}):\n                    yield req\n\n        common = frozenset(reqs_for_extra(None))\n        dm[None].extend(common)\n\n        for extra in self._parsed_pkg_info.get_all('Provides-Extra') or []:\n            s_extra = safe_extra(extra.strip())\n            dm[s_extra] = list(frozenset(reqs_for_extra(extra)) - common)\n\n        return dm\n\n\n_distributionImpl = {\n    '.egg': Distribution,\n    '.egg-info': EggInfoDistribution,\n    '.dist-info': DistInfoDistribution,\n}\n\n\ndef issue_warning(*args, **kw):\n    level = 1\n    g = globals()\n    try:\n        # find the first stack frame that is *not* code in\n        # the pkg_resources module, to use for the warning\n        while sys._getframe(level).f_globals is g:\n            level += 1\n    except ValueError:\n        pass\n    warnings.warn(stacklevel=level + 1, *args, **kw)\n\n\nclass RequirementParseError(ValueError):\n    def __str__(self):\n        return ' '.join(self.args)\n\n\ndef parse_requirements(strs):\n    \"\"\"Yield ``Requirement`` objects for each specification in `strs`\n\n    `strs` must be a string, or a (possibly-nested) iterable thereof.\n    \"\"\"\n    # create a steppable iterator, so we can handle \\-continuations\n    lines = iter(yield_lines(strs))\n\n    for line in lines:\n        # Drop comments -- a hash without a space may be in a URL.\n        if ' #' in line:\n            line = line[:line.find(' #')]\n        # If there is a line continuation, drop it, and append the next line.\n        if line.endswith('\\\\'):\n            line = line[:-2].strip()\n            try:\n                line += next(lines)\n            except StopIteration:\n                return\n        yield Requirement(line)\n\n\nclass Requirement(packaging.requirements.Requirement):\n    def __init__(self, requirement_string):\n        \"\"\"DO NOT CALL THIS UNDOCUMENTED METHOD; use Requirement.parse()!\"\"\"\n        try:\n            super(Requirement, self).__init__(requirement_string)\n        except packaging.requirements.InvalidRequirement as e:\n            raise RequirementParseError(str(e))\n        self.unsafe_name = self.name\n        project_name = safe_name(self.name)\n        self.project_name, self.key = project_name, project_name.lower()\n        self.specs = [\n            (spec.operator, spec.version) for spec in self.specifier]\n        self.extras = tuple(map(safe_extra, self.extras))\n        self.hashCmp = (\n            self.key,\n            self.url,\n            self.specifier,\n            frozenset(self.extras),\n            str(self.marker) if self.marker else None,\n        )\n        self.__hash = hash(self.hashCmp)\n\n    def __eq__(self, other):\n        return (\n            isinstance(other, Requirement) and\n            self.hashCmp == other.hashCmp\n        )\n\n    def __ne__(self, other):\n        return not self == other\n\n    def __contains__(self, item):\n        if isinstance(item, Distribution):\n            if item.key != self.key:\n                return False\n\n            item = item.version\n\n        # Allow prereleases always in order to match the previous behavior of\n        # this method. In the future this should be smarter and follow PEP 440\n        # more accurately.\n        return self.specifier.contains(item, prereleases=True)\n\n    def __hash__(self):\n        return self.__hash\n\n    def __repr__(self):\n        return \"Requirement.parse(%r)\" % str(self)\n\n    @staticmethod\n    def parse(s):\n        req, = parse_requirements(s)\n        return req\n\n\ndef _always_object(classes):\n    \"\"\"\n    Ensure object appears in the mro even\n    for old-style classes.\n    \"\"\"\n    if object not in classes:\n        return classes + (object,)\n    return classes\n\n\ndef _find_adapter(registry, ob):\n    \"\"\"Return an adapter factory for `ob` from `registry`\"\"\"\n    types = _always_object(inspect.getmro(getattr(ob, '__class__', type(ob))))\n    for t in types:\n        if t in registry:\n            return registry[t]\n\n\ndef ensure_directory(path):\n    \"\"\"Ensure that the parent directory of `path` exists\"\"\"\n    dirname = os.path.dirname(path)\n    py31compat.makedirs(dirname, exist_ok=True)\n\n\ndef _bypass_ensure_directory(path):\n    \"\"\"Sandbox-bypassing version of ensure_directory()\"\"\"\n    if not WRITE_SUPPORT:\n        raise IOError('\"os.mkdir\" not supported on this platform.')\n    dirname, filename = split(path)\n    if dirname and filename and not isdir(dirname):\n        _bypass_ensure_directory(dirname)\n        try:\n            mkdir(dirname, 0o755)\n        except FileExistsError:\n            pass\n\n\ndef split_sections(s):\n    \"\"\"Split a string or iterable thereof into (section, content) pairs\n\n    Each ``section`` is a stripped version of the section header (\"[section]\")\n    and each ``content`` is a list of stripped lines excluding blank lines and\n    comment-only lines.  If there are any such lines before the first section\n    header, they're returned in a first ``section`` of ``None``.\n    \"\"\"\n    section = None\n    content = []\n    for line in yield_lines(s):\n        if line.startswith(\"[\"):\n            if line.endswith(\"]\"):\n                if section or content:\n                    yield section, content\n                section = line[1:-1].strip()\n                content = []\n            else:\n                raise ValueError(\"Invalid section heading\", line)\n        else:\n            content.append(line)\n\n    # wrap up last segment\n    yield section, content\n\n\ndef _mkstemp(*args, **kw):\n    old_open = os.open\n    try:\n        # temporarily bypass sandboxing\n        os.open = os_open\n        return tempfile.mkstemp(*args, **kw)\n    finally:\n        # and then put it back\n        os.open = old_open\n\n\n# Silence the PEP440Warning by default, so that end users don't get hit by it\n# randomly just because they use pkg_resources. We want to append the rule\n# because we want earlier uses of filterwarnings to take precedence over this\n# one.\nwarnings.filterwarnings(\"ignore\", category=PEP440Warning, append=True)\n\n\n# from jaraco.functools 1.3\ndef _call_aside(f, *args, **kwargs):\n    f(*args, **kwargs)\n    return f\n\n\n@_call_aside\ndef _initialize(g=globals()):\n    \"Set up global resource manager (deliberately not state-saved)\"\n    manager = ResourceManager()\n    g['_manager'] = manager\n    g.update(\n        (name, getattr(manager, name))\n        for name in dir(manager)\n        if not name.startswith('_')\n    )\n\n\n@_call_aside\ndef _initialize_master_working_set():\n    \"\"\"\n    Prepare the master working set and make the ``require()``\n    API available.\n\n    This function has explicit effects on the global state\n    of pkg_resources. It is intended to be invoked once at\n    the initialization of this module.\n\n    Invocation by other packages is unsupported and done\n    at their own risk.\n    \"\"\"\n    working_set = WorkingSet._build_master()\n    _declare_state('object', working_set=working_set)\n\n    require = working_set.require\n    iter_entry_points = working_set.iter_entry_points\n    add_activation_listener = working_set.subscribe\n    run_script = working_set.run_script\n    # backward compatibility\n    run_main = run_script\n    # Activate all distributions already on sys.path with replace=False and\n    # ensure that all distributions added to the working set in the future\n    # (e.g. by calling ``require()``) will get activated as well,\n    # with higher priority (replace=True).\n    tuple(\n        dist.activate(replace=False)\n        for dist in working_set\n    )\n    add_activation_listener(\n        lambda dist: dist.activate(replace=True),\n        existing=False,\n    )\n    working_set.entries = []\n    # match order\n    list(map(working_set.add_entry, sys.path))\n    globals().update(locals())\n\nclass PkgResourcesDeprecationWarning(Warning):\n    \"\"\"\n    Base class for warning about deprecations in ``pkg_resources``\n\n    This class is not derived from ``DeprecationWarning``, and as such is\n    visible by default.\n    \"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/appdirs.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# Copyright (c) 2005-2010 ActiveState Software Inc.\n# Copyright (c) 2013 Eddy Petrișor\n\n\"\"\"Utilities for determining application-specific dirs.\n\nSee <http://github.com/ActiveState/appdirs> for details and usage.\n\"\"\"\n# Dev Notes:\n# - MSDN on where to store app data files:\n#   http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120\n# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html\n# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html\n\n__version_info__ = (1, 4, 3)\n__version__ = '.'.join(map(str, __version_info__))\n\n\nimport sys\nimport os\n\nPY3 = sys.version_info[0] == 3\n\nif PY3:\n    unicode = str\n\nif sys.platform.startswith('java'):\n    import platform\n    os_name = platform.java_ver()[3][0]\n    if os_name.startswith('Windows'): # \"Windows XP\", \"Windows 7\", etc.\n        system = 'win32'\n    elif os_name.startswith('Mac'): # \"Mac OS X\", etc.\n        system = 'darwin'\n    else: # \"Linux\", \"SunOS\", \"FreeBSD\", etc.\n        # Setting this to \"linux2\" is not ideal, but only Windows or Mac\n        # are actually checked for and the rest of the module expects\n        # *sys.platform* style strings.\n        system = 'linux2'\nelse:\n    system = sys.platform\n\n\n\ndef user_data_dir(appname=None, appauthor=None, version=None, roaming=False):\n    r\"\"\"Return full path to the user-specific data dir for this application.\n\n        \"appname\" is the name of application.\n            If None, just the system directory is returned.\n        \"appauthor\" (only used on Windows) is the name of the\n            appauthor or distributing body for this application. Typically\n            it is the owning company name. This falls back to appname. You may\n            pass False to disable it.\n        \"version\" is an optional version path element to append to the\n            path. You might want to use this if you want multiple versions\n            of your app to be able to run independently. If used, this\n            would typically be \"<major>.<minor>\".\n            Only applied when appname is present.\n        \"roaming\" (boolean, default False) can be set True to use the Windows\n            roaming appdata directory. That means that for users on a Windows\n            network setup for roaming profiles, this user data will be\n            sync'd on login. See\n            <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>\n            for a discussion of issues.\n\n    Typical user data directories are:\n        Mac OS X:               ~/Library/Application Support/<AppName>\n        Unix:                   ~/.local/share/<AppName>    # or in $XDG_DATA_HOME, if defined\n        Win XP (not roaming):   C:\\Documents and Settings\\<username>\\Application Data\\<AppAuthor>\\<AppName>\n        Win XP (roaming):       C:\\Documents and Settings\\<username>\\Local Settings\\Application Data\\<AppAuthor>\\<AppName>\n        Win 7  (not roaming):   C:\\Users\\<username>\\AppData\\Local\\<AppAuthor>\\<AppName>\n        Win 7  (roaming):       C:\\Users\\<username>\\AppData\\Roaming\\<AppAuthor>\\<AppName>\n\n    For Unix, we follow the XDG spec and support $XDG_DATA_HOME.\n    That means, by default \"~/.local/share/<AppName>\".\n    \"\"\"\n    if system == \"win32\":\n        if appauthor is None:\n            appauthor = appname\n        const = roaming and \"CSIDL_APPDATA\" or \"CSIDL_LOCAL_APPDATA\"\n        path = os.path.normpath(_get_win_folder(const))\n        if appname:\n            if appauthor is not False:\n                path = os.path.join(path, appauthor, appname)\n            else:\n                path = os.path.join(path, appname)\n    elif system == 'darwin':\n        path = os.path.expanduser('~/Library/Application Support/')\n        if appname:\n            path = os.path.join(path, appname)\n    else:\n        path = os.getenv('XDG_DATA_HOME', os.path.expanduser(\"~/.local/share\"))\n        if appname:\n            path = os.path.join(path, appname)\n    if appname and version:\n        path = os.path.join(path, version)\n    return path\n\n\ndef site_data_dir(appname=None, appauthor=None, version=None, multipath=False):\n    r\"\"\"Return full path to the user-shared data dir for this application.\n\n        \"appname\" is the name of application.\n            If None, just the system directory is returned.\n        \"appauthor\" (only used on Windows) is the name of the\n            appauthor or distributing body for this application. Typically\n            it is the owning company name. This falls back to appname. You may\n            pass False to disable it.\n        \"version\" is an optional version path element to append to the\n            path. You might want to use this if you want multiple versions\n            of your app to be able to run independently. If used, this\n            would typically be \"<major>.<minor>\".\n            Only applied when appname is present.\n        \"multipath\" is an optional parameter only applicable to *nix\n            which indicates that the entire list of data dirs should be\n            returned. By default, the first item from XDG_DATA_DIRS is\n            returned, or '/usr/local/share/<AppName>',\n            if XDG_DATA_DIRS is not set\n\n    Typical site data directories are:\n        Mac OS X:   /Library/Application Support/<AppName>\n        Unix:       /usr/local/share/<AppName> or /usr/share/<AppName>\n        Win XP:     C:\\Documents and Settings\\All Users\\Application Data\\<AppAuthor>\\<AppName>\n        Vista:      (Fail! \"C:\\ProgramData\" is a hidden *system* directory on Vista.)\n        Win 7:      C:\\ProgramData\\<AppAuthor>\\<AppName>   # Hidden, but writeable on Win 7.\n\n    For Unix, this is using the $XDG_DATA_DIRS[0] default.\n\n    WARNING: Do not use this on Windows. See the Vista-Fail note above for why.\n    \"\"\"\n    if system == \"win32\":\n        if appauthor is None:\n            appauthor = appname\n        path = os.path.normpath(_get_win_folder(\"CSIDL_COMMON_APPDATA\"))\n        if appname:\n            if appauthor is not False:\n                path = os.path.join(path, appauthor, appname)\n            else:\n                path = os.path.join(path, appname)\n    elif system == 'darwin':\n        path = os.path.expanduser('/Library/Application Support')\n        if appname:\n            path = os.path.join(path, appname)\n    else:\n        # XDG default for $XDG_DATA_DIRS\n        # only first, if multipath is False\n        path = os.getenv('XDG_DATA_DIRS',\n                         os.pathsep.join(['/usr/local/share', '/usr/share']))\n        pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]\n        if appname:\n            if version:\n                appname = os.path.join(appname, version)\n            pathlist = [os.sep.join([x, appname]) for x in pathlist]\n\n        if multipath:\n            path = os.pathsep.join(pathlist)\n        else:\n            path = pathlist[0]\n        return path\n\n    if appname and version:\n        path = os.path.join(path, version)\n    return path\n\n\ndef user_config_dir(appname=None, appauthor=None, version=None, roaming=False):\n    r\"\"\"Return full path to the user-specific config dir for this application.\n\n        \"appname\" is the name of application.\n            If None, just the system directory is returned.\n        \"appauthor\" (only used on Windows) is the name of the\n            appauthor or distributing body for this application. Typically\n            it is the owning company name. This falls back to appname. You may\n            pass False to disable it.\n        \"version\" is an optional version path element to append to the\n            path. You might want to use this if you want multiple versions\n            of your app to be able to run independently. If used, this\n            would typically be \"<major>.<minor>\".\n            Only applied when appname is present.\n        \"roaming\" (boolean, default False) can be set True to use the Windows\n            roaming appdata directory. That means that for users on a Windows\n            network setup for roaming profiles, this user data will be\n            sync'd on login. See\n            <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>\n            for a discussion of issues.\n\n    Typical user config directories are:\n        Mac OS X:               same as user_data_dir\n        Unix:                   ~/.config/<AppName>     # or in $XDG_CONFIG_HOME, if defined\n        Win *:                  same as user_data_dir\n\n    For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.\n    That means, by default \"~/.config/<AppName>\".\n    \"\"\"\n    if system in [\"win32\", \"darwin\"]:\n        path = user_data_dir(appname, appauthor, None, roaming)\n    else:\n        path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser(\"~/.config\"))\n        if appname:\n            path = os.path.join(path, appname)\n    if appname and version:\n        path = os.path.join(path, version)\n    return path\n\n\ndef site_config_dir(appname=None, appauthor=None, version=None, multipath=False):\n    r\"\"\"Return full path to the user-shared data dir for this application.\n\n        \"appname\" is the name of application.\n            If None, just the system directory is returned.\n        \"appauthor\" (only used on Windows) is the name of the\n            appauthor or distributing body for this application. Typically\n            it is the owning company name. This falls back to appname. You may\n            pass False to disable it.\n        \"version\" is an optional version path element to append to the\n            path. You might want to use this if you want multiple versions\n            of your app to be able to run independently. If used, this\n            would typically be \"<major>.<minor>\".\n            Only applied when appname is present.\n        \"multipath\" is an optional parameter only applicable to *nix\n            which indicates that the entire list of config dirs should be\n            returned. By default, the first item from XDG_CONFIG_DIRS is\n            returned, or '/etc/xdg/<AppName>', if XDG_CONFIG_DIRS is not set\n\n    Typical site config directories are:\n        Mac OS X:   same as site_data_dir\n        Unix:       /etc/xdg/<AppName> or $XDG_CONFIG_DIRS[i]/<AppName> for each value in\n                    $XDG_CONFIG_DIRS\n        Win *:      same as site_data_dir\n        Vista:      (Fail! \"C:\\ProgramData\" is a hidden *system* directory on Vista.)\n\n    For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False\n\n    WARNING: Do not use this on Windows. See the Vista-Fail note above for why.\n    \"\"\"\n    if system in [\"win32\", \"darwin\"]:\n        path = site_data_dir(appname, appauthor)\n        if appname and version:\n            path = os.path.join(path, version)\n    else:\n        # XDG default for $XDG_CONFIG_DIRS\n        # only first, if multipath is False\n        path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')\n        pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]\n        if appname:\n            if version:\n                appname = os.path.join(appname, version)\n            pathlist = [os.sep.join([x, appname]) for x in pathlist]\n\n        if multipath:\n            path = os.pathsep.join(pathlist)\n        else:\n            path = pathlist[0]\n    return path\n\n\ndef user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):\n    r\"\"\"Return full path to the user-specific cache dir for this application.\n\n        \"appname\" is the name of application.\n            If None, just the system directory is returned.\n        \"appauthor\" (only used on Windows) is the name of the\n            appauthor or distributing body for this application. Typically\n            it is the owning company name. This falls back to appname. You may\n            pass False to disable it.\n        \"version\" is an optional version path element to append to the\n            path. You might want to use this if you want multiple versions\n            of your app to be able to run independently. If used, this\n            would typically be \"<major>.<minor>\".\n            Only applied when appname is present.\n        \"opinion\" (boolean) can be False to disable the appending of\n            \"Cache\" to the base app data dir for Windows. See\n            discussion below.\n\n    Typical user cache directories are:\n        Mac OS X:   ~/Library/Caches/<AppName>\n        Unix:       ~/.cache/<AppName> (XDG default)\n        Win XP:     C:\\Documents and Settings\\<username>\\Local Settings\\Application Data\\<AppAuthor>\\<AppName>\\Cache\n        Vista:      C:\\Users\\<username>\\AppData\\Local\\<AppAuthor>\\<AppName>\\Cache\n\n    On Windows the only suggestion in the MSDN docs is that local settings go in\n    the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming\n    app data dir (the default returned by `user_data_dir` above). Apps typically\n    put cache data somewhere *under* the given dir here. Some examples:\n        ...\\Mozilla\\Firefox\\Profiles\\<ProfileName>\\Cache\n        ...\\Acme\\SuperApp\\Cache\\1.0\n    OPINION: This function appends \"Cache\" to the `CSIDL_LOCAL_APPDATA` value.\n    This can be disabled with the `opinion=False` option.\n    \"\"\"\n    if system == \"win32\":\n        if appauthor is None:\n            appauthor = appname\n        path = os.path.normpath(_get_win_folder(\"CSIDL_LOCAL_APPDATA\"))\n        if appname:\n            if appauthor is not False:\n                path = os.path.join(path, appauthor, appname)\n            else:\n                path = os.path.join(path, appname)\n            if opinion:\n                path = os.path.join(path, \"Cache\")\n    elif system == 'darwin':\n        path = os.path.expanduser('~/Library/Caches')\n        if appname:\n            path = os.path.join(path, appname)\n    else:\n        path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))\n        if appname:\n            path = os.path.join(path, appname)\n    if appname and version:\n        path = os.path.join(path, version)\n    return path\n\n\ndef user_state_dir(appname=None, appauthor=None, version=None, roaming=False):\n    r\"\"\"Return full path to the user-specific state dir for this application.\n\n        \"appname\" is the name of application.\n            If None, just the system directory is returned.\n        \"appauthor\" (only used on Windows) is the name of the\n            appauthor or distributing body for this application. Typically\n            it is the owning company name. This falls back to appname. You may\n            pass False to disable it.\n        \"version\" is an optional version path element to append to the\n            path. You might want to use this if you want multiple versions\n            of your app to be able to run independently. If used, this\n            would typically be \"<major>.<minor>\".\n            Only applied when appname is present.\n        \"roaming\" (boolean, default False) can be set True to use the Windows\n            roaming appdata directory. That means that for users on a Windows\n            network setup for roaming profiles, this user data will be\n            sync'd on login. See\n            <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>\n            for a discussion of issues.\n\n    Typical user state directories are:\n        Mac OS X:  same as user_data_dir\n        Unix:      ~/.local/state/<AppName>   # or in $XDG_STATE_HOME, if defined\n        Win *:     same as user_data_dir\n\n    For Unix, we follow this Debian proposal <https://wiki.debian.org/XDGBaseDirectorySpecification#state>\n    to extend the XDG spec and support $XDG_STATE_HOME.\n\n    That means, by default \"~/.local/state/<AppName>\".\n    \"\"\"\n    if system in [\"win32\", \"darwin\"]:\n        path = user_data_dir(appname, appauthor, None, roaming)\n    else:\n        path = os.getenv('XDG_STATE_HOME', os.path.expanduser(\"~/.local/state\"))\n        if appname:\n            path = os.path.join(path, appname)\n    if appname and version:\n        path = os.path.join(path, version)\n    return path\n\n\ndef user_log_dir(appname=None, appauthor=None, version=None, opinion=True):\n    r\"\"\"Return full path to the user-specific log dir for this application.\n\n        \"appname\" is the name of application.\n            If None, just the system directory is returned.\n        \"appauthor\" (only used on Windows) is the name of the\n            appauthor or distributing body for this application. Typically\n            it is the owning company name. This falls back to appname. You may\n            pass False to disable it.\n        \"version\" is an optional version path element to append to the\n            path. You might want to use this if you want multiple versions\n            of your app to be able to run independently. If used, this\n            would typically be \"<major>.<minor>\".\n            Only applied when appname is present.\n        \"opinion\" (boolean) can be False to disable the appending of\n            \"Logs\" to the base app data dir for Windows, and \"log\" to the\n            base cache dir for Unix. See discussion below.\n\n    Typical user log directories are:\n        Mac OS X:   ~/Library/Logs/<AppName>\n        Unix:       ~/.cache/<AppName>/log  # or under $XDG_CACHE_HOME if defined\n        Win XP:     C:\\Documents and Settings\\<username>\\Local Settings\\Application Data\\<AppAuthor>\\<AppName>\\Logs\n        Vista:      C:\\Users\\<username>\\AppData\\Local\\<AppAuthor>\\<AppName>\\Logs\n\n    On Windows the only suggestion in the MSDN docs is that local settings\n    go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in\n    examples of what some windows apps use for a logs dir.)\n\n    OPINION: This function appends \"Logs\" to the `CSIDL_LOCAL_APPDATA`\n    value for Windows and appends \"log\" to the user cache dir for Unix.\n    This can be disabled with the `opinion=False` option.\n    \"\"\"\n    if system == \"darwin\":\n        path = os.path.join(\n            os.path.expanduser('~/Library/Logs'),\n            appname)\n    elif system == \"win32\":\n        path = user_data_dir(appname, appauthor, version)\n        version = False\n        if opinion:\n            path = os.path.join(path, \"Logs\")\n    else:\n        path = user_cache_dir(appname, appauthor, version)\n        version = False\n        if opinion:\n            path = os.path.join(path, \"log\")\n    if appname and version:\n        path = os.path.join(path, version)\n    return path\n\n\nclass AppDirs(object):\n    \"\"\"Convenience wrapper for getting application dirs.\"\"\"\n    def __init__(self, appname=None, appauthor=None, version=None,\n            roaming=False, multipath=False):\n        self.appname = appname\n        self.appauthor = appauthor\n        self.version = version\n        self.roaming = roaming\n        self.multipath = multipath\n\n    @property\n    def user_data_dir(self):\n        return user_data_dir(self.appname, self.appauthor,\n                             version=self.version, roaming=self.roaming)\n\n    @property\n    def site_data_dir(self):\n        return site_data_dir(self.appname, self.appauthor,\n                             version=self.version, multipath=self.multipath)\n\n    @property\n    def user_config_dir(self):\n        return user_config_dir(self.appname, self.appauthor,\n                               version=self.version, roaming=self.roaming)\n\n    @property\n    def site_config_dir(self):\n        return site_config_dir(self.appname, self.appauthor,\n                             version=self.version, multipath=self.multipath)\n\n    @property\n    def user_cache_dir(self):\n        return user_cache_dir(self.appname, self.appauthor,\n                              version=self.version)\n\n    @property\n    def user_state_dir(self):\n        return user_state_dir(self.appname, self.appauthor,\n                              version=self.version)\n\n    @property\n    def user_log_dir(self):\n        return user_log_dir(self.appname, self.appauthor,\n                            version=self.version)\n\n\n#---- internal support stuff\n\ndef _get_win_folder_from_registry(csidl_name):\n    \"\"\"This is a fallback technique at best. I'm not sure if using the\n    registry for this guarantees us the correct answer for all CSIDL_*\n    names.\n    \"\"\"\n    if PY3:\n      import winreg as _winreg\n    else:\n      import _winreg\n\n    shell_folder_name = {\n        \"CSIDL_APPDATA\": \"AppData\",\n        \"CSIDL_COMMON_APPDATA\": \"Common AppData\",\n        \"CSIDL_LOCAL_APPDATA\": \"Local AppData\",\n    }[csidl_name]\n\n    key = _winreg.OpenKey(\n        _winreg.HKEY_CURRENT_USER,\n        r\"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\"\n    )\n    dir, type = _winreg.QueryValueEx(key, shell_folder_name)\n    return dir\n\n\ndef _get_win_folder_with_pywin32(csidl_name):\n    from win32com.shell import shellcon, shell\n    dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0)\n    # Try to make this a unicode path because SHGetFolderPath does\n    # not return unicode strings when there is unicode data in the\n    # path.\n    try:\n        dir = unicode(dir)\n\n        # Downgrade to short path name if have highbit chars. See\n        # <http://bugs.activestate.com/show_bug.cgi?id=85099>.\n        has_high_char = False\n        for c in dir:\n            if ord(c) > 255:\n                has_high_char = True\n                break\n        if has_high_char:\n            try:\n                import win32api\n                dir = win32api.GetShortPathName(dir)\n            except ImportError:\n                pass\n    except UnicodeError:\n        pass\n    return dir\n\n\ndef _get_win_folder_with_ctypes(csidl_name):\n    import ctypes\n\n    csidl_const = {\n        \"CSIDL_APPDATA\": 26,\n        \"CSIDL_COMMON_APPDATA\": 35,\n        \"CSIDL_LOCAL_APPDATA\": 28,\n    }[csidl_name]\n\n    buf = ctypes.create_unicode_buffer(1024)\n    ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)\n\n    # Downgrade to short path name if have highbit chars. See\n    # <http://bugs.activestate.com/show_bug.cgi?id=85099>.\n    has_high_char = False\n    for c in buf:\n        if ord(c) > 255:\n            has_high_char = True\n            break\n    if has_high_char:\n        buf2 = ctypes.create_unicode_buffer(1024)\n        if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):\n            buf = buf2\n\n    return buf.value\n\ndef _get_win_folder_with_jna(csidl_name):\n    import array\n    from com.sun import jna\n    from com.sun.jna.platform import win32\n\n    buf_size = win32.WinDef.MAX_PATH * 2\n    buf = array.zeros('c', buf_size)\n    shell = win32.Shell32.INSTANCE\n    shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf)\n    dir = jna.Native.toString(buf.tostring()).rstrip(\"\\0\")\n\n    # Downgrade to short path name if have highbit chars. See\n    # <http://bugs.activestate.com/show_bug.cgi?id=85099>.\n    has_high_char = False\n    for c in dir:\n        if ord(c) > 255:\n            has_high_char = True\n            break\n    if has_high_char:\n        buf = array.zeros('c', buf_size)\n        kernel = win32.Kernel32.INSTANCE\n        if kernel.GetShortPathName(dir, buf, buf_size):\n            dir = jna.Native.toString(buf.tostring()).rstrip(\"\\0\")\n\n    return dir\n\nif system == \"win32\":\n    try:\n        import win32com.shell\n        _get_win_folder = _get_win_folder_with_pywin32\n    except ImportError:\n        try:\n            from ctypes import windll\n            _get_win_folder = _get_win_folder_with_ctypes\n        except ImportError:\n            try:\n                import com.sun.jna\n                _get_win_folder = _get_win_folder_with_jna\n            except ImportError:\n                _get_win_folder = _get_win_folder_from_registry\n\n\n#---- self test code\n\nif __name__ == \"__main__\":\n    appname = \"MyApp\"\n    appauthor = \"MyCompany\"\n\n    props = (\"user_data_dir\",\n             \"user_config_dir\",\n             \"user_cache_dir\",\n             \"user_state_dir\",\n             \"user_log_dir\",\n             \"site_data_dir\",\n             \"site_config_dir\")\n\n    print(\"-- app dirs %s --\" % __version__)\n\n    print(\"-- app dirs (with optional 'version')\")\n    dirs = AppDirs(appname, appauthor, version=\"1.0\")\n    for prop in props:\n        print(\"%s: %s\" % (prop, getattr(dirs, prop)))\n\n    print(\"\\n-- app dirs (without optional 'version')\")\n    dirs = AppDirs(appname, appauthor)\n    for prop in props:\n        print(\"%s: %s\" % (prop, getattr(dirs, prop)))\n\n    print(\"\\n-- app dirs (without optional 'appauthor')\")\n    dirs = AppDirs(appname)\n    for prop in props:\n        print(\"%s: %s\" % (prop, getattr(dirs, prop)))\n\n    print(\"\\n-- app dirs (with disabled 'appauthor')\")\n    dirs = AppDirs(appname, appauthor=False)\n    for prop in props:\n        print(\"%s: %s\" % (prop, getattr(dirs, prop)))\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/packaging/__about__.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\n__all__ = [\n    \"__title__\", \"__summary__\", \"__uri__\", \"__version__\", \"__author__\",\n    \"__email__\", \"__license__\", \"__copyright__\",\n]\n\n__title__ = \"packaging\"\n__summary__ = \"Core utilities for Python packages\"\n__uri__ = \"https://github.com/pypa/packaging\"\n\n__version__ = \"16.8\"\n\n__author__ = \"Donald Stufft and individual contributors\"\n__email__ = \"donald@stufft.io\"\n\n__license__ = \"BSD or Apache License, Version 2.0\"\n__copyright__ = \"Copyright 2014-2016 %s\" % __author__\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/packaging/__init__.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nfrom .__about__ import (\n    __author__, __copyright__, __email__, __license__, __summary__, __title__,\n    __uri__, __version__\n)\n\n__all__ = [\n    \"__title__\", \"__summary__\", \"__uri__\", \"__version__\", \"__author__\",\n    \"__email__\", \"__license__\", \"__copyright__\",\n]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/packaging/_compat.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nimport sys\n\n\nPY2 = sys.version_info[0] == 2\nPY3 = sys.version_info[0] == 3\n\n# flake8: noqa\n\nif PY3:\n    string_types = str,\nelse:\n    string_types = basestring,\n\n\ndef with_metaclass(meta, *bases):\n    \"\"\"\n    Create a base class with a metaclass.\n    \"\"\"\n    # This requires a bit of explanation: the basic idea is to make a dummy\n    # metaclass for one level of class instantiation that replaces itself with\n    # the actual metaclass.\n    class metaclass(meta):\n        def __new__(cls, name, this_bases, d):\n            return meta(name, bases, d)\n    return type.__new__(metaclass, 'temporary_class', (), {})\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/packaging/_structures.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\n\nclass Infinity(object):\n\n    def __repr__(self):\n        return \"Infinity\"\n\n    def __hash__(self):\n        return hash(repr(self))\n\n    def __lt__(self, other):\n        return False\n\n    def __le__(self, other):\n        return False\n\n    def __eq__(self, other):\n        return isinstance(other, self.__class__)\n\n    def __ne__(self, other):\n        return not isinstance(other, self.__class__)\n\n    def __gt__(self, other):\n        return True\n\n    def __ge__(self, other):\n        return True\n\n    def __neg__(self):\n        return NegativeInfinity\n\nInfinity = Infinity()\n\n\nclass NegativeInfinity(object):\n\n    def __repr__(self):\n        return \"-Infinity\"\n\n    def __hash__(self):\n        return hash(repr(self))\n\n    def __lt__(self, other):\n        return True\n\n    def __le__(self, other):\n        return True\n\n    def __eq__(self, other):\n        return isinstance(other, self.__class__)\n\n    def __ne__(self, other):\n        return not isinstance(other, self.__class__)\n\n    def __gt__(self, other):\n        return False\n\n    def __ge__(self, other):\n        return False\n\n    def __neg__(self):\n        return Infinity\n\nNegativeInfinity = NegativeInfinity()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/packaging/markers.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nimport operator\nimport os\nimport platform\nimport sys\n\nfrom pkg_resources.extern.pyparsing import ParseException, ParseResults, stringStart, stringEnd\nfrom pkg_resources.extern.pyparsing import ZeroOrMore, Group, Forward, QuotedString\nfrom pkg_resources.extern.pyparsing import Literal as L  # noqa\n\nfrom ._compat import string_types\nfrom .specifiers import Specifier, InvalidSpecifier\n\n\n__all__ = [\n    \"InvalidMarker\", \"UndefinedComparison\", \"UndefinedEnvironmentName\",\n    \"Marker\", \"default_environment\",\n]\n\n\nclass InvalidMarker(ValueError):\n    \"\"\"\n    An invalid marker was found, users should refer to PEP 508.\n    \"\"\"\n\n\nclass UndefinedComparison(ValueError):\n    \"\"\"\n    An invalid operation was attempted on a value that doesn't support it.\n    \"\"\"\n\n\nclass UndefinedEnvironmentName(ValueError):\n    \"\"\"\n    A name was attempted to be used that does not exist inside of the\n    environment.\n    \"\"\"\n\n\nclass Node(object):\n\n    def __init__(self, value):\n        self.value = value\n\n    def __str__(self):\n        return str(self.value)\n\n    def __repr__(self):\n        return \"<{0}({1!r})>\".format(self.__class__.__name__, str(self))\n\n    def serialize(self):\n        raise NotImplementedError\n\n\nclass Variable(Node):\n\n    def serialize(self):\n        return str(self)\n\n\nclass Value(Node):\n\n    def serialize(self):\n        return '\"{0}\"'.format(self)\n\n\nclass Op(Node):\n\n    def serialize(self):\n        return str(self)\n\n\nVARIABLE = (\n    L(\"implementation_version\") |\n    L(\"platform_python_implementation\") |\n    L(\"implementation_name\") |\n    L(\"python_full_version\") |\n    L(\"platform_release\") |\n    L(\"platform_version\") |\n    L(\"platform_machine\") |\n    L(\"platform_system\") |\n    L(\"python_version\") |\n    L(\"sys_platform\") |\n    L(\"os_name\") |\n    L(\"os.name\") |  # PEP-345\n    L(\"sys.platform\") |  # PEP-345\n    L(\"platform.version\") |  # PEP-345\n    L(\"platform.machine\") |  # PEP-345\n    L(\"platform.python_implementation\") |  # PEP-345\n    L(\"python_implementation\") |  # undocumented setuptools legacy\n    L(\"extra\")\n)\nALIASES = {\n    'os.name': 'os_name',\n    'sys.platform': 'sys_platform',\n    'platform.version': 'platform_version',\n    'platform.machine': 'platform_machine',\n    'platform.python_implementation': 'platform_python_implementation',\n    'python_implementation': 'platform_python_implementation'\n}\nVARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0])))\n\nVERSION_CMP = (\n    L(\"===\") |\n    L(\"==\") |\n    L(\">=\") |\n    L(\"<=\") |\n    L(\"!=\") |\n    L(\"~=\") |\n    L(\">\") |\n    L(\"<\")\n)\n\nMARKER_OP = VERSION_CMP | L(\"not in\") | L(\"in\")\nMARKER_OP.setParseAction(lambda s, l, t: Op(t[0]))\n\nMARKER_VALUE = QuotedString(\"'\") | QuotedString('\"')\nMARKER_VALUE.setParseAction(lambda s, l, t: Value(t[0]))\n\nBOOLOP = L(\"and\") | L(\"or\")\n\nMARKER_VAR = VARIABLE | MARKER_VALUE\n\nMARKER_ITEM = Group(MARKER_VAR + MARKER_OP + MARKER_VAR)\nMARKER_ITEM.setParseAction(lambda s, l, t: tuple(t[0]))\n\nLPAREN = L(\"(\").suppress()\nRPAREN = L(\")\").suppress()\n\nMARKER_EXPR = Forward()\nMARKER_ATOM = MARKER_ITEM | Group(LPAREN + MARKER_EXPR + RPAREN)\nMARKER_EXPR << MARKER_ATOM + ZeroOrMore(BOOLOP + MARKER_EXPR)\n\nMARKER = stringStart + MARKER_EXPR + stringEnd\n\n\ndef _coerce_parse_result(results):\n    if isinstance(results, ParseResults):\n        return [_coerce_parse_result(i) for i in results]\n    else:\n        return results\n\n\ndef _format_marker(marker, first=True):\n    assert isinstance(marker, (list, tuple, string_types))\n\n    # Sometimes we have a structure like [[...]] which is a single item list\n    # where the single item is itself it's own list. In that case we want skip\n    # the rest of this function so that we don't get extraneous () on the\n    # outside.\n    if (isinstance(marker, list) and len(marker) == 1 and\n            isinstance(marker[0], (list, tuple))):\n        return _format_marker(marker[0])\n\n    if isinstance(marker, list):\n        inner = (_format_marker(m, first=False) for m in marker)\n        if first:\n            return \" \".join(inner)\n        else:\n            return \"(\" + \" \".join(inner) + \")\"\n    elif isinstance(marker, tuple):\n        return \" \".join([m.serialize() for m in marker])\n    else:\n        return marker\n\n\n_operators = {\n    \"in\": lambda lhs, rhs: lhs in rhs,\n    \"not in\": lambda lhs, rhs: lhs not in rhs,\n    \"<\": operator.lt,\n    \"<=\": operator.le,\n    \"==\": operator.eq,\n    \"!=\": operator.ne,\n    \">=\": operator.ge,\n    \">\": operator.gt,\n}\n\n\ndef _eval_op(lhs, op, rhs):\n    try:\n        spec = Specifier(\"\".join([op.serialize(), rhs]))\n    except InvalidSpecifier:\n        pass\n    else:\n        return spec.contains(lhs)\n\n    oper = _operators.get(op.serialize())\n    if oper is None:\n        raise UndefinedComparison(\n            \"Undefined {0!r} on {1!r} and {2!r}.\".format(op, lhs, rhs)\n        )\n\n    return oper(lhs, rhs)\n\n\n_undefined = object()\n\n\ndef _get_env(environment, name):\n    value = environment.get(name, _undefined)\n\n    if value is _undefined:\n        raise UndefinedEnvironmentName(\n            \"{0!r} does not exist in evaluation environment.\".format(name)\n        )\n\n    return value\n\n\ndef _evaluate_markers(markers, environment):\n    groups = [[]]\n\n    for marker in markers:\n        assert isinstance(marker, (list, tuple, string_types))\n\n        if isinstance(marker, list):\n            groups[-1].append(_evaluate_markers(marker, environment))\n        elif isinstance(marker, tuple):\n            lhs, op, rhs = marker\n\n            if isinstance(lhs, Variable):\n                lhs_value = _get_env(environment, lhs.value)\n                rhs_value = rhs.value\n            else:\n                lhs_value = lhs.value\n                rhs_value = _get_env(environment, rhs.value)\n\n            groups[-1].append(_eval_op(lhs_value, op, rhs_value))\n        else:\n            assert marker in [\"and\", \"or\"]\n            if marker == \"or\":\n                groups.append([])\n\n    return any(all(item) for item in groups)\n\n\ndef format_full_version(info):\n    version = '{0.major}.{0.minor}.{0.micro}'.format(info)\n    kind = info.releaselevel\n    if kind != 'final':\n        version += kind[0] + str(info.serial)\n    return version\n\n\ndef default_environment():\n    if hasattr(sys, 'implementation'):\n        iver = format_full_version(sys.implementation.version)\n        implementation_name = sys.implementation.name\n    else:\n        iver = '0'\n        implementation_name = ''\n\n    return {\n        \"implementation_name\": implementation_name,\n        \"implementation_version\": iver,\n        \"os_name\": os.name,\n        \"platform_machine\": platform.machine(),\n        \"platform_release\": platform.release(),\n        \"platform_system\": platform.system(),\n        \"platform_version\": platform.version(),\n        \"python_full_version\": platform.python_version(),\n        \"platform_python_implementation\": platform.python_implementation(),\n        \"python_version\": platform.python_version()[:3],\n        \"sys_platform\": sys.platform,\n    }\n\n\nclass Marker(object):\n\n    def __init__(self, marker):\n        try:\n            self._markers = _coerce_parse_result(MARKER.parseString(marker))\n        except ParseException as e:\n            err_str = \"Invalid marker: {0!r}, parse error at {1!r}\".format(\n                marker, marker[e.loc:e.loc + 8])\n            raise InvalidMarker(err_str)\n\n    def __str__(self):\n        return _format_marker(self._markers)\n\n    def __repr__(self):\n        return \"<Marker({0!r})>\".format(str(self))\n\n    def evaluate(self, environment=None):\n        \"\"\"Evaluate a marker.\n\n        Return the boolean from evaluating the given marker against the\n        environment. environment is an optional argument to override all or\n        part of the determined environment.\n\n        The environment is determined from the current Python process.\n        \"\"\"\n        current_environment = default_environment()\n        if environment is not None:\n            current_environment.update(environment)\n\n        return _evaluate_markers(self._markers, current_environment)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/packaging/requirements.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nimport string\nimport re\n\nfrom pkg_resources.extern.pyparsing import stringStart, stringEnd, originalTextFor, ParseException\nfrom pkg_resources.extern.pyparsing import ZeroOrMore, Word, Optional, Regex, Combine\nfrom pkg_resources.extern.pyparsing import Literal as L  # noqa\nfrom pkg_resources.extern.six.moves.urllib import parse as urlparse\n\nfrom .markers import MARKER_EXPR, Marker\nfrom .specifiers import LegacySpecifier, Specifier, SpecifierSet\n\n\nclass InvalidRequirement(ValueError):\n    \"\"\"\n    An invalid requirement was found, users should refer to PEP 508.\n    \"\"\"\n\n\nALPHANUM = Word(string.ascii_letters + string.digits)\n\nLBRACKET = L(\"[\").suppress()\nRBRACKET = L(\"]\").suppress()\nLPAREN = L(\"(\").suppress()\nRPAREN = L(\")\").suppress()\nCOMMA = L(\",\").suppress()\nSEMICOLON = L(\";\").suppress()\nAT = L(\"@\").suppress()\n\nPUNCTUATION = Word(\"-_.\")\nIDENTIFIER_END = ALPHANUM | (ZeroOrMore(PUNCTUATION) + ALPHANUM)\nIDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END))\n\nNAME = IDENTIFIER(\"name\")\nEXTRA = IDENTIFIER\n\nURI = Regex(r'[^ ]+')(\"url\")\nURL = (AT + URI)\n\nEXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA)\nEXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)(\"extras\")\n\nVERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE)\nVERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE)\n\nVERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY\nVERSION_MANY = Combine(VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE),\n                       joinString=\",\", adjacent=False)(\"_raw_spec\")\n_VERSION_SPEC = Optional(((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY))\n_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or '')\n\nVERSION_SPEC = originalTextFor(_VERSION_SPEC)(\"specifier\")\nVERSION_SPEC.setParseAction(lambda s, l, t: t[1])\n\nMARKER_EXPR = originalTextFor(MARKER_EXPR())(\"marker\")\nMARKER_EXPR.setParseAction(\n    lambda s, l, t: Marker(s[t._original_start:t._original_end])\n)\nMARKER_SEPERATOR = SEMICOLON\nMARKER = MARKER_SEPERATOR + MARKER_EXPR\n\nVERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER)\nURL_AND_MARKER = URL + Optional(MARKER)\n\nNAMED_REQUIREMENT = \\\n    NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER)\n\nREQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd\n\n\nclass Requirement(object):\n    \"\"\"Parse a requirement.\n\n    Parse a given requirement string into its parts, such as name, specifier,\n    URL, and extras. Raises InvalidRequirement on a badly-formed requirement\n    string.\n    \"\"\"\n\n    # TODO: Can we test whether something is contained within a requirement?\n    #       If so how do we do that? Do we need to test against the _name_ of\n    #       the thing as well as the version? What about the markers?\n    # TODO: Can we normalize the name and extra name?\n\n    def __init__(self, requirement_string):\n        try:\n            req = REQUIREMENT.parseString(requirement_string)\n        except ParseException as e:\n            raise InvalidRequirement(\n                \"Invalid requirement, parse error at \\\"{0!r}\\\"\".format(\n                    requirement_string[e.loc:e.loc + 8]))\n\n        self.name = req.name\n        if req.url:\n            parsed_url = urlparse.urlparse(req.url)\n            if not (parsed_url.scheme and parsed_url.netloc) or (\n                    not parsed_url.scheme and not parsed_url.netloc):\n                raise InvalidRequirement(\"Invalid URL given\")\n            self.url = req.url\n        else:\n            self.url = None\n        self.extras = set(req.extras.asList() if req.extras else [])\n        self.specifier = SpecifierSet(req.specifier)\n        self.marker = req.marker if req.marker else None\n\n    def __str__(self):\n        parts = [self.name]\n\n        if self.extras:\n            parts.append(\"[{0}]\".format(\",\".join(sorted(self.extras))))\n\n        if self.specifier:\n            parts.append(str(self.specifier))\n\n        if self.url:\n            parts.append(\"@ {0}\".format(self.url))\n\n        if self.marker:\n            parts.append(\"; {0}\".format(self.marker))\n\n        return \"\".join(parts)\n\n    def __repr__(self):\n        return \"<Requirement({0!r})>\".format(str(self))\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/packaging/specifiers.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nimport abc\nimport functools\nimport itertools\nimport re\n\nfrom ._compat import string_types, with_metaclass\nfrom .version import Version, LegacyVersion, parse\n\n\nclass InvalidSpecifier(ValueError):\n    \"\"\"\n    An invalid specifier was found, users should refer to PEP 440.\n    \"\"\"\n\n\nclass BaseSpecifier(with_metaclass(abc.ABCMeta, object)):\n\n    @abc.abstractmethod\n    def __str__(self):\n        \"\"\"\n        Returns the str representation of this Specifier like object. This\n        should be representative of the Specifier itself.\n        \"\"\"\n\n    @abc.abstractmethod\n    def __hash__(self):\n        \"\"\"\n        Returns a hash value for this Specifier like object.\n        \"\"\"\n\n    @abc.abstractmethod\n    def __eq__(self, other):\n        \"\"\"\n        Returns a boolean representing whether or not the two Specifier like\n        objects are equal.\n        \"\"\"\n\n    @abc.abstractmethod\n    def __ne__(self, other):\n        \"\"\"\n        Returns a boolean representing whether or not the two Specifier like\n        objects are not equal.\n        \"\"\"\n\n    @abc.abstractproperty\n    def prereleases(self):\n        \"\"\"\n        Returns whether or not pre-releases as a whole are allowed by this\n        specifier.\n        \"\"\"\n\n    @prereleases.setter\n    def prereleases(self, value):\n        \"\"\"\n        Sets whether or not pre-releases as a whole are allowed by this\n        specifier.\n        \"\"\"\n\n    @abc.abstractmethod\n    def contains(self, item, prereleases=None):\n        \"\"\"\n        Determines if the given item is contained within this specifier.\n        \"\"\"\n\n    @abc.abstractmethod\n    def filter(self, iterable, prereleases=None):\n        \"\"\"\n        Takes an iterable of items and filters them so that only items which\n        are contained within this specifier are allowed in it.\n        \"\"\"\n\n\nclass _IndividualSpecifier(BaseSpecifier):\n\n    _operators = {}\n\n    def __init__(self, spec=\"\", prereleases=None):\n        match = self._regex.search(spec)\n        if not match:\n            raise InvalidSpecifier(\"Invalid specifier: '{0}'\".format(spec))\n\n        self._spec = (\n            match.group(\"operator\").strip(),\n            match.group(\"version\").strip(),\n        )\n\n        # Store whether or not this Specifier should accept prereleases\n        self._prereleases = prereleases\n\n    def __repr__(self):\n        pre = (\n            \", prereleases={0!r}\".format(self.prereleases)\n            if self._prereleases is not None\n            else \"\"\n        )\n\n        return \"<{0}({1!r}{2})>\".format(\n            self.__class__.__name__,\n            str(self),\n            pre,\n        )\n\n    def __str__(self):\n        return \"{0}{1}\".format(*self._spec)\n\n    def __hash__(self):\n        return hash(self._spec)\n\n    def __eq__(self, other):\n        if isinstance(other, string_types):\n            try:\n                other = self.__class__(other)\n            except InvalidSpecifier:\n                return NotImplemented\n        elif not isinstance(other, self.__class__):\n            return NotImplemented\n\n        return self._spec == other._spec\n\n    def __ne__(self, other):\n        if isinstance(other, string_types):\n            try:\n                other = self.__class__(other)\n            except InvalidSpecifier:\n                return NotImplemented\n        elif not isinstance(other, self.__class__):\n            return NotImplemented\n\n        return self._spec != other._spec\n\n    def _get_operator(self, op):\n        return getattr(self, \"_compare_{0}\".format(self._operators[op]))\n\n    def _coerce_version(self, version):\n        if not isinstance(version, (LegacyVersion, Version)):\n            version = parse(version)\n        return version\n\n    @property\n    def operator(self):\n        return self._spec[0]\n\n    @property\n    def version(self):\n        return self._spec[1]\n\n    @property\n    def prereleases(self):\n        return self._prereleases\n\n    @prereleases.setter\n    def prereleases(self, value):\n        self._prereleases = value\n\n    def __contains__(self, item):\n        return self.contains(item)\n\n    def contains(self, item, prereleases=None):\n        # Determine if prereleases are to be allowed or not.\n        if prereleases is None:\n            prereleases = self.prereleases\n\n        # Normalize item to a Version or LegacyVersion, this allows us to have\n        # a shortcut for ``\"2.0\" in Specifier(\">=2\")\n        item = self._coerce_version(item)\n\n        # Determine if we should be supporting prereleases in this specifier\n        # or not, if we do not support prereleases than we can short circuit\n        # logic if this version is a prereleases.\n        if item.is_prerelease and not prereleases:\n            return False\n\n        # Actually do the comparison to determine if this item is contained\n        # within this Specifier or not.\n        return self._get_operator(self.operator)(item, self.version)\n\n    def filter(self, iterable, prereleases=None):\n        yielded = False\n        found_prereleases = []\n\n        kw = {\"prereleases\": prereleases if prereleases is not None else True}\n\n        # Attempt to iterate over all the values in the iterable and if any of\n        # them match, yield them.\n        for version in iterable:\n            parsed_version = self._coerce_version(version)\n\n            if self.contains(parsed_version, **kw):\n                # If our version is a prerelease, and we were not set to allow\n                # prereleases, then we'll store it for later incase nothing\n                # else matches this specifier.\n                if (parsed_version.is_prerelease and not\n                        (prereleases or self.prereleases)):\n                    found_prereleases.append(version)\n                # Either this is not a prerelease, or we should have been\n                # accepting prereleases from the begining.\n                else:\n                    yielded = True\n                    yield version\n\n        # Now that we've iterated over everything, determine if we've yielded\n        # any values, and if we have not and we have any prereleases stored up\n        # then we will go ahead and yield the prereleases.\n        if not yielded and found_prereleases:\n            for version in found_prereleases:\n                yield version\n\n\nclass LegacySpecifier(_IndividualSpecifier):\n\n    _regex_str = (\n        r\"\"\"\n        (?P<operator>(==|!=|<=|>=|<|>))\n        \\s*\n        (?P<version>\n            [^,;\\s)]* # Since this is a \"legacy\" specifier, and the version\n                      # string can be just about anything, we match everything\n                      # except for whitespace, a semi-colon for marker support,\n                      # a closing paren since versions can be enclosed in\n                      # them, and a comma since it's a version separator.\n        )\n        \"\"\"\n    )\n\n    _regex = re.compile(\n        r\"^\\s*\" + _regex_str + r\"\\s*$\", re.VERBOSE | re.IGNORECASE)\n\n    _operators = {\n        \"==\": \"equal\",\n        \"!=\": \"not_equal\",\n        \"<=\": \"less_than_equal\",\n        \">=\": \"greater_than_equal\",\n        \"<\": \"less_than\",\n        \">\": \"greater_than\",\n    }\n\n    def _coerce_version(self, version):\n        if not isinstance(version, LegacyVersion):\n            version = LegacyVersion(str(version))\n        return version\n\n    def _compare_equal(self, prospective, spec):\n        return prospective == self._coerce_version(spec)\n\n    def _compare_not_equal(self, prospective, spec):\n        return prospective != self._coerce_version(spec)\n\n    def _compare_less_than_equal(self, prospective, spec):\n        return prospective <= self._coerce_version(spec)\n\n    def _compare_greater_than_equal(self, prospective, spec):\n        return prospective >= self._coerce_version(spec)\n\n    def _compare_less_than(self, prospective, spec):\n        return prospective < self._coerce_version(spec)\n\n    def _compare_greater_than(self, prospective, spec):\n        return prospective > self._coerce_version(spec)\n\n\ndef _require_version_compare(fn):\n    @functools.wraps(fn)\n    def wrapped(self, prospective, spec):\n        if not isinstance(prospective, Version):\n            return False\n        return fn(self, prospective, spec)\n    return wrapped\n\n\nclass Specifier(_IndividualSpecifier):\n\n    _regex_str = (\n        r\"\"\"\n        (?P<operator>(~=|==|!=|<=|>=|<|>|===))\n        (?P<version>\n            (?:\n                # The identity operators allow for an escape hatch that will\n                # do an exact string match of the version you wish to install.\n                # This will not be parsed by PEP 440 and we cannot determine\n                # any semantic meaning from it. This operator is discouraged\n                # but included entirely as an escape hatch.\n                (?<====)  # Only match for the identity operator\n                \\s*\n                [^\\s]*    # We just match everything, except for whitespace\n                          # since we are only testing for strict identity.\n            )\n            |\n            (?:\n                # The (non)equality operators allow for wild card and local\n                # versions to be specified so we have to define these two\n                # operators separately to enable that.\n                (?<===|!=)            # Only match for equals and not equals\n\n                \\s*\n                v?\n                (?:[0-9]+!)?          # epoch\n                [0-9]+(?:\\.[0-9]+)*   # release\n                (?:                   # pre release\n                    [-_\\.]?\n                    (a|b|c|rc|alpha|beta|pre|preview)\n                    [-_\\.]?\n                    [0-9]*\n                )?\n                (?:                   # post release\n                    (?:-[0-9]+)|(?:[-_\\.]?(post|rev|r)[-_\\.]?[0-9]*)\n                )?\n\n                # You cannot use a wild card and a dev or local version\n                # together so group them with a | and make them optional.\n                (?:\n                    (?:[-_\\.]?dev[-_\\.]?[0-9]*)?         # dev release\n                    (?:\\+[a-z0-9]+(?:[-_\\.][a-z0-9]+)*)? # local\n                    |\n                    \\.\\*  # Wild card syntax of .*\n                )?\n            )\n            |\n            (?:\n                # The compatible operator requires at least two digits in the\n                # release segment.\n                (?<=~=)               # Only match for the compatible operator\n\n                \\s*\n                v?\n                (?:[0-9]+!)?          # epoch\n                [0-9]+(?:\\.[0-9]+)+   # release  (We have a + instead of a *)\n                (?:                   # pre release\n                    [-_\\.]?\n                    (a|b|c|rc|alpha|beta|pre|preview)\n                    [-_\\.]?\n                    [0-9]*\n                )?\n                (?:                                   # post release\n                    (?:-[0-9]+)|(?:[-_\\.]?(post|rev|r)[-_\\.]?[0-9]*)\n                )?\n                (?:[-_\\.]?dev[-_\\.]?[0-9]*)?          # dev release\n            )\n            |\n            (?:\n                # All other operators only allow a sub set of what the\n                # (non)equality operators do. Specifically they do not allow\n                # local versions to be specified nor do they allow the prefix\n                # matching wild cards.\n                (?<!==|!=|~=)         # We have special cases for these\n                                      # operators so we want to make sure they\n                                      # don't match here.\n\n                \\s*\n                v?\n                (?:[0-9]+!)?          # epoch\n                [0-9]+(?:\\.[0-9]+)*   # release\n                (?:                   # pre release\n                    [-_\\.]?\n                    (a|b|c|rc|alpha|beta|pre|preview)\n                    [-_\\.]?\n                    [0-9]*\n                )?\n                (?:                                   # post release\n                    (?:-[0-9]+)|(?:[-_\\.]?(post|rev|r)[-_\\.]?[0-9]*)\n                )?\n                (?:[-_\\.]?dev[-_\\.]?[0-9]*)?          # dev release\n            )\n        )\n        \"\"\"\n    )\n\n    _regex = re.compile(\n        r\"^\\s*\" + _regex_str + r\"\\s*$\", re.VERBOSE | re.IGNORECASE)\n\n    _operators = {\n        \"~=\": \"compatible\",\n        \"==\": \"equal\",\n        \"!=\": \"not_equal\",\n        \"<=\": \"less_than_equal\",\n        \">=\": \"greater_than_equal\",\n        \"<\": \"less_than\",\n        \">\": \"greater_than\",\n        \"===\": \"arbitrary\",\n    }\n\n    @_require_version_compare\n    def _compare_compatible(self, prospective, spec):\n        # Compatible releases have an equivalent combination of >= and ==. That\n        # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to\n        # implement this in terms of the other specifiers instead of\n        # implementing it ourselves. The only thing we need to do is construct\n        # the other specifiers.\n\n        # We want everything but the last item in the version, but we want to\n        # ignore post and dev releases and we want to treat the pre-release as\n        # it's own separate segment.\n        prefix = \".\".join(\n            list(\n                itertools.takewhile(\n                    lambda x: (not x.startswith(\"post\") and not\n                               x.startswith(\"dev\")),\n                    _version_split(spec),\n                )\n            )[:-1]\n        )\n\n        # Add the prefix notation to the end of our string\n        prefix += \".*\"\n\n        return (self._get_operator(\">=\")(prospective, spec) and\n                self._get_operator(\"==\")(prospective, prefix))\n\n    @_require_version_compare\n    def _compare_equal(self, prospective, spec):\n        # We need special logic to handle prefix matching\n        if spec.endswith(\".*\"):\n            # In the case of prefix matching we want to ignore local segment.\n            prospective = Version(prospective.public)\n            # Split the spec out by dots, and pretend that there is an implicit\n            # dot in between a release segment and a pre-release segment.\n            spec = _version_split(spec[:-2])  # Remove the trailing .*\n\n            # Split the prospective version out by dots, and pretend that there\n            # is an implicit dot in between a release segment and a pre-release\n            # segment.\n            prospective = _version_split(str(prospective))\n\n            # Shorten the prospective version to be the same length as the spec\n            # so that we can determine if the specifier is a prefix of the\n            # prospective version or not.\n            prospective = prospective[:len(spec)]\n\n            # Pad out our two sides with zeros so that they both equal the same\n            # length.\n            spec, prospective = _pad_version(spec, prospective)\n        else:\n            # Convert our spec string into a Version\n            spec = Version(spec)\n\n            # If the specifier does not have a local segment, then we want to\n            # act as if the prospective version also does not have a local\n            # segment.\n            if not spec.local:\n                prospective = Version(prospective.public)\n\n        return prospective == spec\n\n    @_require_version_compare\n    def _compare_not_equal(self, prospective, spec):\n        return not self._compare_equal(prospective, spec)\n\n    @_require_version_compare\n    def _compare_less_than_equal(self, prospective, spec):\n        return prospective <= Version(spec)\n\n    @_require_version_compare\n    def _compare_greater_than_equal(self, prospective, spec):\n        return prospective >= Version(spec)\n\n    @_require_version_compare\n    def _compare_less_than(self, prospective, spec):\n        # Convert our spec to a Version instance, since we'll want to work with\n        # it as a version.\n        spec = Version(spec)\n\n        # Check to see if the prospective version is less than the spec\n        # version. If it's not we can short circuit and just return False now\n        # instead of doing extra unneeded work.\n        if not prospective < spec:\n            return False\n\n        # This special case is here so that, unless the specifier itself\n        # includes is a pre-release version, that we do not accept pre-release\n        # versions for the version mentioned in the specifier (e.g. <3.1 should\n        # not match 3.1.dev0, but should match 3.0.dev0).\n        if not spec.is_prerelease and prospective.is_prerelease:\n            if Version(prospective.base_version) == Version(spec.base_version):\n                return False\n\n        # If we've gotten to here, it means that prospective version is both\n        # less than the spec version *and* it's not a pre-release of the same\n        # version in the spec.\n        return True\n\n    @_require_version_compare\n    def _compare_greater_than(self, prospective, spec):\n        # Convert our spec to a Version instance, since we'll want to work with\n        # it as a version.\n        spec = Version(spec)\n\n        # Check to see if the prospective version is greater than the spec\n        # version. If it's not we can short circuit and just return False now\n        # instead of doing extra unneeded work.\n        if not prospective > spec:\n            return False\n\n        # This special case is here so that, unless the specifier itself\n        # includes is a post-release version, that we do not accept\n        # post-release versions for the version mentioned in the specifier\n        # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0).\n        if not spec.is_postrelease and prospective.is_postrelease:\n            if Version(prospective.base_version) == Version(spec.base_version):\n                return False\n\n        # Ensure that we do not allow a local version of the version mentioned\n        # in the specifier, which is techincally greater than, to match.\n        if prospective.local is not None:\n            if Version(prospective.base_version) == Version(spec.base_version):\n                return False\n\n        # If we've gotten to here, it means that prospective version is both\n        # greater than the spec version *and* it's not a pre-release of the\n        # same version in the spec.\n        return True\n\n    def _compare_arbitrary(self, prospective, spec):\n        return str(prospective).lower() == str(spec).lower()\n\n    @property\n    def prereleases(self):\n        # If there is an explicit prereleases set for this, then we'll just\n        # blindly use that.\n        if self._prereleases is not None:\n            return self._prereleases\n\n        # Look at all of our specifiers and determine if they are inclusive\n        # operators, and if they are if they are including an explicit\n        # prerelease.\n        operator, version = self._spec\n        if operator in [\"==\", \">=\", \"<=\", \"~=\", \"===\"]:\n            # The == specifier can include a trailing .*, if it does we\n            # want to remove before parsing.\n            if operator == \"==\" and version.endswith(\".*\"):\n                version = version[:-2]\n\n            # Parse the version, and if it is a pre-release than this\n            # specifier allows pre-releases.\n            if parse(version).is_prerelease:\n                return True\n\n        return False\n\n    @prereleases.setter\n    def prereleases(self, value):\n        self._prereleases = value\n\n\n_prefix_regex = re.compile(r\"^([0-9]+)((?:a|b|c|rc)[0-9]+)$\")\n\n\ndef _version_split(version):\n    result = []\n    for item in version.split(\".\"):\n        match = _prefix_regex.search(item)\n        if match:\n            result.extend(match.groups())\n        else:\n            result.append(item)\n    return result\n\n\ndef _pad_version(left, right):\n    left_split, right_split = [], []\n\n    # Get the release segment of our versions\n    left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left)))\n    right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))\n\n    # Get the rest of our versions\n    left_split.append(left[len(left_split[0]):])\n    right_split.append(right[len(right_split[0]):])\n\n    # Insert our padding\n    left_split.insert(\n        1,\n        [\"0\"] * max(0, len(right_split[0]) - len(left_split[0])),\n    )\n    right_split.insert(\n        1,\n        [\"0\"] * max(0, len(left_split[0]) - len(right_split[0])),\n    )\n\n    return (\n        list(itertools.chain(*left_split)),\n        list(itertools.chain(*right_split)),\n    )\n\n\nclass SpecifierSet(BaseSpecifier):\n\n    def __init__(self, specifiers=\"\", prereleases=None):\n        # Split on , to break each indidivual specifier into it's own item, and\n        # strip each item to remove leading/trailing whitespace.\n        specifiers = [s.strip() for s in specifiers.split(\",\") if s.strip()]\n\n        # Parsed each individual specifier, attempting first to make it a\n        # Specifier and falling back to a LegacySpecifier.\n        parsed = set()\n        for specifier in specifiers:\n            try:\n                parsed.add(Specifier(specifier))\n            except InvalidSpecifier:\n                parsed.add(LegacySpecifier(specifier))\n\n        # Turn our parsed specifiers into a frozen set and save them for later.\n        self._specs = frozenset(parsed)\n\n        # Store our prereleases value so we can use it later to determine if\n        # we accept prereleases or not.\n        self._prereleases = prereleases\n\n    def __repr__(self):\n        pre = (\n            \", prereleases={0!r}\".format(self.prereleases)\n            if self._prereleases is not None\n            else \"\"\n        )\n\n        return \"<SpecifierSet({0!r}{1})>\".format(str(self), pre)\n\n    def __str__(self):\n        return \",\".join(sorted(str(s) for s in self._specs))\n\n    def __hash__(self):\n        return hash(self._specs)\n\n    def __and__(self, other):\n        if isinstance(other, string_types):\n            other = SpecifierSet(other)\n        elif not isinstance(other, SpecifierSet):\n            return NotImplemented\n\n        specifier = SpecifierSet()\n        specifier._specs = frozenset(self._specs | other._specs)\n\n        if self._prereleases is None and other._prereleases is not None:\n            specifier._prereleases = other._prereleases\n        elif self._prereleases is not None and other._prereleases is None:\n            specifier._prereleases = self._prereleases\n        elif self._prereleases == other._prereleases:\n            specifier._prereleases = self._prereleases\n        else:\n            raise ValueError(\n                \"Cannot combine SpecifierSets with True and False prerelease \"\n                \"overrides.\"\n            )\n\n        return specifier\n\n    def __eq__(self, other):\n        if isinstance(other, string_types):\n            other = SpecifierSet(other)\n        elif isinstance(other, _IndividualSpecifier):\n            other = SpecifierSet(str(other))\n        elif not isinstance(other, SpecifierSet):\n            return NotImplemented\n\n        return self._specs == other._specs\n\n    def __ne__(self, other):\n        if isinstance(other, string_types):\n            other = SpecifierSet(other)\n        elif isinstance(other, _IndividualSpecifier):\n            other = SpecifierSet(str(other))\n        elif not isinstance(other, SpecifierSet):\n            return NotImplemented\n\n        return self._specs != other._specs\n\n    def __len__(self):\n        return len(self._specs)\n\n    def __iter__(self):\n        return iter(self._specs)\n\n    @property\n    def prereleases(self):\n        # If we have been given an explicit prerelease modifier, then we'll\n        # pass that through here.\n        if self._prereleases is not None:\n            return self._prereleases\n\n        # If we don't have any specifiers, and we don't have a forced value,\n        # then we'll just return None since we don't know if this should have\n        # pre-releases or not.\n        if not self._specs:\n            return None\n\n        # Otherwise we'll see if any of the given specifiers accept\n        # prereleases, if any of them do we'll return True, otherwise False.\n        return any(s.prereleases for s in self._specs)\n\n    @prereleases.setter\n    def prereleases(self, value):\n        self._prereleases = value\n\n    def __contains__(self, item):\n        return self.contains(item)\n\n    def contains(self, item, prereleases=None):\n        # Ensure that our item is a Version or LegacyVersion instance.\n        if not isinstance(item, (LegacyVersion, Version)):\n            item = parse(item)\n\n        # Determine if we're forcing a prerelease or not, if we're not forcing\n        # one for this particular filter call, then we'll use whatever the\n        # SpecifierSet thinks for whether or not we should support prereleases.\n        if prereleases is None:\n            prereleases = self.prereleases\n\n        # We can determine if we're going to allow pre-releases by looking to\n        # see if any of the underlying items supports them. If none of them do\n        # and this item is a pre-release then we do not allow it and we can\n        # short circuit that here.\n        # Note: This means that 1.0.dev1 would not be contained in something\n        #       like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0\n        if not prereleases and item.is_prerelease:\n            return False\n\n        # We simply dispatch to the underlying specs here to make sure that the\n        # given version is contained within all of them.\n        # Note: This use of all() here means that an empty set of specifiers\n        #       will always return True, this is an explicit design decision.\n        return all(\n            s.contains(item, prereleases=prereleases)\n            for s in self._specs\n        )\n\n    def filter(self, iterable, prereleases=None):\n        # Determine if we're forcing a prerelease or not, if we're not forcing\n        # one for this particular filter call, then we'll use whatever the\n        # SpecifierSet thinks for whether or not we should support prereleases.\n        if prereleases is None:\n            prereleases = self.prereleases\n\n        # If we have any specifiers, then we want to wrap our iterable in the\n        # filter method for each one, this will act as a logical AND amongst\n        # each specifier.\n        if self._specs:\n            for spec in self._specs:\n                iterable = spec.filter(iterable, prereleases=bool(prereleases))\n            return iterable\n        # If we do not have any specifiers, then we need to have a rough filter\n        # which will filter out any pre-releases, unless there are no final\n        # releases, and which will filter out LegacyVersion in general.\n        else:\n            filtered = []\n            found_prereleases = []\n\n            for item in iterable:\n                # Ensure that we some kind of Version class for this item.\n                if not isinstance(item, (LegacyVersion, Version)):\n                    parsed_version = parse(item)\n                else:\n                    parsed_version = item\n\n                # Filter out any item which is parsed as a LegacyVersion\n                if isinstance(parsed_version, LegacyVersion):\n                    continue\n\n                # Store any item which is a pre-release for later unless we've\n                # already found a final version or we are accepting prereleases\n                if parsed_version.is_prerelease and not prereleases:\n                    if not filtered:\n                        found_prereleases.append(item)\n                else:\n                    filtered.append(item)\n\n            # If we've found no items except for pre-releases, then we'll go\n            # ahead and use the pre-releases\n            if not filtered and found_prereleases and prereleases is None:\n                return found_prereleases\n\n            return filtered\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/packaging/utils.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nimport re\n\n\n_canonicalize_regex = re.compile(r\"[-_.]+\")\n\n\ndef canonicalize_name(name):\n    # This is taken from PEP 503.\n    return _canonicalize_regex.sub(\"-\", name).lower()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/packaging/version.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nimport collections\nimport itertools\nimport re\n\nfrom ._structures import Infinity\n\n\n__all__ = [\n    \"parse\", \"Version\", \"LegacyVersion\", \"InvalidVersion\", \"VERSION_PATTERN\"\n]\n\n\n_Version = collections.namedtuple(\n    \"_Version\",\n    [\"epoch\", \"release\", \"dev\", \"pre\", \"post\", \"local\"],\n)\n\n\ndef parse(version):\n    \"\"\"\n    Parse the given version string and return either a :class:`Version` object\n    or a :class:`LegacyVersion` object depending on if the given version is\n    a valid PEP 440 version or a legacy version.\n    \"\"\"\n    try:\n        return Version(version)\n    except InvalidVersion:\n        return LegacyVersion(version)\n\n\nclass InvalidVersion(ValueError):\n    \"\"\"\n    An invalid version was found, users should refer to PEP 440.\n    \"\"\"\n\n\nclass _BaseVersion(object):\n\n    def __hash__(self):\n        return hash(self._key)\n\n    def __lt__(self, other):\n        return self._compare(other, lambda s, o: s < o)\n\n    def __le__(self, other):\n        return self._compare(other, lambda s, o: s <= o)\n\n    def __eq__(self, other):\n        return self._compare(other, lambda s, o: s == o)\n\n    def __ge__(self, other):\n        return self._compare(other, lambda s, o: s >= o)\n\n    def __gt__(self, other):\n        return self._compare(other, lambda s, o: s > o)\n\n    def __ne__(self, other):\n        return self._compare(other, lambda s, o: s != o)\n\n    def _compare(self, other, method):\n        if not isinstance(other, _BaseVersion):\n            return NotImplemented\n\n        return method(self._key, other._key)\n\n\nclass LegacyVersion(_BaseVersion):\n\n    def __init__(self, version):\n        self._version = str(version)\n        self._key = _legacy_cmpkey(self._version)\n\n    def __str__(self):\n        return self._version\n\n    def __repr__(self):\n        return \"<LegacyVersion({0})>\".format(repr(str(self)))\n\n    @property\n    def public(self):\n        return self._version\n\n    @property\n    def base_version(self):\n        return self._version\n\n    @property\n    def local(self):\n        return None\n\n    @property\n    def is_prerelease(self):\n        return False\n\n    @property\n    def is_postrelease(self):\n        return False\n\n\n_legacy_version_component_re = re.compile(\n    r\"(\\d+ | [a-z]+ | \\.| -)\", re.VERBOSE,\n)\n\n_legacy_version_replacement_map = {\n    \"pre\": \"c\", \"preview\": \"c\", \"-\": \"final-\", \"rc\": \"c\", \"dev\": \"@\",\n}\n\n\ndef _parse_version_parts(s):\n    for part in _legacy_version_component_re.split(s):\n        part = _legacy_version_replacement_map.get(part, part)\n\n        if not part or part == \".\":\n            continue\n\n        if part[:1] in \"0123456789\":\n            # pad for numeric comparison\n            yield part.zfill(8)\n        else:\n            yield \"*\" + part\n\n    # ensure that alpha/beta/candidate are before final\n    yield \"*final\"\n\n\ndef _legacy_cmpkey(version):\n    # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch\n    # greater than or equal to 0. This will effectively put the LegacyVersion,\n    # which uses the defacto standard originally implemented by setuptools,\n    # as before all PEP 440 versions.\n    epoch = -1\n\n    # This scheme is taken from pkg_resources.parse_version setuptools prior to\n    # it's adoption of the packaging library.\n    parts = []\n    for part in _parse_version_parts(version.lower()):\n        if part.startswith(\"*\"):\n            # remove \"-\" before a prerelease tag\n            if part < \"*final\":\n                while parts and parts[-1] == \"*final-\":\n                    parts.pop()\n\n            # remove trailing zeros from each series of numeric parts\n            while parts and parts[-1] == \"00000000\":\n                parts.pop()\n\n        parts.append(part)\n    parts = tuple(parts)\n\n    return epoch, parts\n\n# Deliberately not anchored to the start and end of the string, to make it\n# easier for 3rd party code to reuse\nVERSION_PATTERN = r\"\"\"\n    v?\n    (?:\n        (?:(?P<epoch>[0-9]+)!)?                           # epoch\n        (?P<release>[0-9]+(?:\\.[0-9]+)*)                  # release segment\n        (?P<pre>                                          # pre-release\n            [-_\\.]?\n            (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))\n            [-_\\.]?\n            (?P<pre_n>[0-9]+)?\n        )?\n        (?P<post>                                         # post release\n            (?:-(?P<post_n1>[0-9]+))\n            |\n            (?:\n                [-_\\.]?\n                (?P<post_l>post|rev|r)\n                [-_\\.]?\n                (?P<post_n2>[0-9]+)?\n            )\n        )?\n        (?P<dev>                                          # dev release\n            [-_\\.]?\n            (?P<dev_l>dev)\n            [-_\\.]?\n            (?P<dev_n>[0-9]+)?\n        )?\n    )\n    (?:\\+(?P<local>[a-z0-9]+(?:[-_\\.][a-z0-9]+)*))?       # local version\n\"\"\"\n\n\nclass Version(_BaseVersion):\n\n    _regex = re.compile(\n        r\"^\\s*\" + VERSION_PATTERN + r\"\\s*$\",\n        re.VERBOSE | re.IGNORECASE,\n    )\n\n    def __init__(self, version):\n        # Validate the version and parse it into pieces\n        match = self._regex.search(version)\n        if not match:\n            raise InvalidVersion(\"Invalid version: '{0}'\".format(version))\n\n        # Store the parsed out pieces of the version\n        self._version = _Version(\n            epoch=int(match.group(\"epoch\")) if match.group(\"epoch\") else 0,\n            release=tuple(int(i) for i in match.group(\"release\").split(\".\")),\n            pre=_parse_letter_version(\n                match.group(\"pre_l\"),\n                match.group(\"pre_n\"),\n            ),\n            post=_parse_letter_version(\n                match.group(\"post_l\"),\n                match.group(\"post_n1\") or match.group(\"post_n2\"),\n            ),\n            dev=_parse_letter_version(\n                match.group(\"dev_l\"),\n                match.group(\"dev_n\"),\n            ),\n            local=_parse_local_version(match.group(\"local\")),\n        )\n\n        # Generate a key which will be used for sorting\n        self._key = _cmpkey(\n            self._version.epoch,\n            self._version.release,\n            self._version.pre,\n            self._version.post,\n            self._version.dev,\n            self._version.local,\n        )\n\n    def __repr__(self):\n        return \"<Version({0})>\".format(repr(str(self)))\n\n    def __str__(self):\n        parts = []\n\n        # Epoch\n        if self._version.epoch != 0:\n            parts.append(\"{0}!\".format(self._version.epoch))\n\n        # Release segment\n        parts.append(\".\".join(str(x) for x in self._version.release))\n\n        # Pre-release\n        if self._version.pre is not None:\n            parts.append(\"\".join(str(x) for x in self._version.pre))\n\n        # Post-release\n        if self._version.post is not None:\n            parts.append(\".post{0}\".format(self._version.post[1]))\n\n        # Development release\n        if self._version.dev is not None:\n            parts.append(\".dev{0}\".format(self._version.dev[1]))\n\n        # Local version segment\n        if self._version.local is not None:\n            parts.append(\n                \"+{0}\".format(\".\".join(str(x) for x in self._version.local))\n            )\n\n        return \"\".join(parts)\n\n    @property\n    def public(self):\n        return str(self).split(\"+\", 1)[0]\n\n    @property\n    def base_version(self):\n        parts = []\n\n        # Epoch\n        if self._version.epoch != 0:\n            parts.append(\"{0}!\".format(self._version.epoch))\n\n        # Release segment\n        parts.append(\".\".join(str(x) for x in self._version.release))\n\n        return \"\".join(parts)\n\n    @property\n    def local(self):\n        version_string = str(self)\n        if \"+\" in version_string:\n            return version_string.split(\"+\", 1)[1]\n\n    @property\n    def is_prerelease(self):\n        return bool(self._version.dev or self._version.pre)\n\n    @property\n    def is_postrelease(self):\n        return bool(self._version.post)\n\n\ndef _parse_letter_version(letter, number):\n    if letter:\n        # We consider there to be an implicit 0 in a pre-release if there is\n        # not a numeral associated with it.\n        if number is None:\n            number = 0\n\n        # We normalize any letters to their lower case form\n        letter = letter.lower()\n\n        # We consider some words to be alternate spellings of other words and\n        # in those cases we want to normalize the spellings to our preferred\n        # spelling.\n        if letter == \"alpha\":\n            letter = \"a\"\n        elif letter == \"beta\":\n            letter = \"b\"\n        elif letter in [\"c\", \"pre\", \"preview\"]:\n            letter = \"rc\"\n        elif letter in [\"rev\", \"r\"]:\n            letter = \"post\"\n\n        return letter, int(number)\n    if not letter and number:\n        # We assume if we are given a number, but we are not given a letter\n        # then this is using the implicit post release syntax (e.g. 1.0-1)\n        letter = \"post\"\n\n        return letter, int(number)\n\n\n_local_version_seperators = re.compile(r\"[\\._-]\")\n\n\ndef _parse_local_version(local):\n    \"\"\"\n    Takes a string like abc.1.twelve and turns it into (\"abc\", 1, \"twelve\").\n    \"\"\"\n    if local is not None:\n        return tuple(\n            part.lower() if not part.isdigit() else int(part)\n            for part in _local_version_seperators.split(local)\n        )\n\n\ndef _cmpkey(epoch, release, pre, post, dev, local):\n    # When we compare a release version, we want to compare it with all of the\n    # trailing zeros removed. So we'll use a reverse the list, drop all the now\n    # leading zeros until we come to something non zero, then take the rest\n    # re-reverse it back into the correct order and make it a tuple and use\n    # that for our sorting key.\n    release = tuple(\n        reversed(list(\n            itertools.dropwhile(\n                lambda x: x == 0,\n                reversed(release),\n            )\n        ))\n    )\n\n    # We need to \"trick\" the sorting algorithm to put 1.0.dev0 before 1.0a0.\n    # We'll do this by abusing the pre segment, but we _only_ want to do this\n    # if there is not a pre or a post segment. If we have one of those then\n    # the normal sorting rules will handle this case correctly.\n    if pre is None and post is None and dev is not None:\n        pre = -Infinity\n    # Versions without a pre-release (except as noted above) should sort after\n    # those with one.\n    elif pre is None:\n        pre = Infinity\n\n    # Versions without a post segment should sort before those with one.\n    if post is None:\n        post = -Infinity\n\n    # Versions without a development segment should sort after those with one.\n    if dev is None:\n        dev = Infinity\n\n    if local is None:\n        # Versions without a local segment should sort before those with one.\n        local = -Infinity\n    else:\n        # Versions with a local segment need that segment parsed to implement\n        # the sorting rules in PEP440.\n        # - Alpha numeric segments sort before numeric segments\n        # - Alpha numeric segments sort lexicographically\n        # - Numeric segments sort numerically\n        # - Shorter versions sort before longer versions when the prefixes\n        #   match exactly\n        local = tuple(\n            (i, \"\") if isinstance(i, int) else (-Infinity, i)\n            for i in local\n        )\n\n    return epoch, release, pre, post, dev, local\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/pyparsing.py",
    "content": "# module pyparsing.py\n#\n# Copyright (c) 2003-2018  Paul T. McGuire\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\n# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\n# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n#\n\n__doc__ = \\\n\"\"\"\npyparsing module - Classes and methods to define and execute parsing grammars\n=============================================================================\n\nThe pyparsing module is an alternative approach to creating and executing simple grammars,\nvs. the traditional lex/yacc approach, or the use of regular expressions.  With pyparsing, you\ndon't need to learn a new syntax for defining grammars or matching expressions - the parsing module\nprovides a library of classes that you use to construct the grammar directly in Python.\n\nHere is a program to parse \"Hello, World!\" (or any greeting of the form \nC{\"<salutation>, <addressee>!\"}), built up using L{Word}, L{Literal}, and L{And} elements \n(L{'+'<ParserElement.__add__>} operator gives L{And} expressions, strings are auto-converted to\nL{Literal} expressions)::\n\n    from pyparsing import Word, alphas\n\n    # define grammar of a greeting\n    greet = Word(alphas) + \",\" + Word(alphas) + \"!\"\n\n    hello = \"Hello, World!\"\n    print (hello, \"->\", greet.parseString(hello))\n\nThe program outputs the following::\n\n    Hello, World! -> ['Hello', ',', 'World', '!']\n\nThe Python representation of the grammar is quite readable, owing to the self-explanatory\nclass names, and the use of '+', '|' and '^' operators.\n\nThe L{ParseResults} object returned from L{ParserElement.parseString<ParserElement.parseString>} can be accessed as a nested list, a dictionary, or an\nobject with named attributes.\n\nThe pyparsing module handles some of the problems that are typically vexing when writing text parsers:\n - extra or missing whitespace (the above program will also handle \"Hello,World!\", \"Hello  ,  World  !\", etc.)\n - quoted strings\n - embedded comments\n\n\nGetting Started -\n-----------------\nVisit the classes L{ParserElement} and L{ParseResults} to see the base classes that most other pyparsing\nclasses inherit from. Use the docstrings for examples of how to:\n - construct literal match expressions from L{Literal} and L{CaselessLiteral} classes\n - construct character word-group expressions using the L{Word} class\n - see how to create repetitive expressions using L{ZeroOrMore} and L{OneOrMore} classes\n - use L{'+'<And>}, L{'|'<MatchFirst>}, L{'^'<Or>}, and L{'&'<Each>} operators to combine simple expressions into more complex ones\n - associate names with your parsed results using L{ParserElement.setResultsName}\n - find some helpful expression short-cuts like L{delimitedList} and L{oneOf}\n - find more useful common expressions in the L{pyparsing_common} namespace class\n\"\"\"\n\n__version__ = \"2.2.1\"\n__versionTime__ = \"18 Sep 2018 00:49 UTC\"\n__author__ = \"Paul McGuire <ptmcg@users.sourceforge.net>\"\n\nimport string\nfrom weakref import ref as wkref\nimport copy\nimport sys\nimport warnings\nimport re\nimport sre_constants\nimport collections\nimport pprint\nimport traceback\nimport types\nfrom datetime import datetime\n\ntry:\n    from _thread import RLock\nexcept ImportError:\n    from threading import RLock\n\ntry:\n    # Python 3\n    from collections.abc import Iterable\n    from collections.abc import MutableMapping\nexcept ImportError:\n    # Python 2.7\n    from collections import Iterable\n    from collections import MutableMapping\n\ntry:\n    from collections import OrderedDict as _OrderedDict\nexcept ImportError:\n    try:\n        from ordereddict import OrderedDict as _OrderedDict\n    except ImportError:\n        _OrderedDict = None\n\n#~ sys.stderr.write( \"testing pyparsing module, version %s, %s\\n\" % (__version__,__versionTime__ ) )\n\n__all__ = [\n'And', 'CaselessKeyword', 'CaselessLiteral', 'CharsNotIn', 'Combine', 'Dict', 'Each', 'Empty',\n'FollowedBy', 'Forward', 'GoToColumn', 'Group', 'Keyword', 'LineEnd', 'LineStart', 'Literal',\n'MatchFirst', 'NoMatch', 'NotAny', 'OneOrMore', 'OnlyOnce', 'Optional', 'Or',\n'ParseBaseException', 'ParseElementEnhance', 'ParseException', 'ParseExpression', 'ParseFatalException',\n'ParseResults', 'ParseSyntaxException', 'ParserElement', 'QuotedString', 'RecursiveGrammarException',\n'Regex', 'SkipTo', 'StringEnd', 'StringStart', 'Suppress', 'Token', 'TokenConverter', \n'White', 'Word', 'WordEnd', 'WordStart', 'ZeroOrMore',\n'alphanums', 'alphas', 'alphas8bit', 'anyCloseTag', 'anyOpenTag', 'cStyleComment', 'col',\n'commaSeparatedList', 'commonHTMLEntity', 'countedArray', 'cppStyleComment', 'dblQuotedString',\n'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'hexnums',\n'htmlComment', 'javaStyleComment', 'line', 'lineEnd', 'lineStart', 'lineno',\n'makeHTMLTags', 'makeXMLTags', 'matchOnlyAtCol', 'matchPreviousExpr', 'matchPreviousLiteral',\n'nestedExpr', 'nullDebugAction', 'nums', 'oneOf', 'opAssoc', 'operatorPrecedence', 'printables',\n'punc8bit', 'pythonStyleComment', 'quotedString', 'removeQuotes', 'replaceHTMLEntity', \n'replaceWith', 'restOfLine', 'sglQuotedString', 'srange', 'stringEnd',\n'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute',\n'indentedBlock', 'originalTextFor', 'ungroup', 'infixNotation','locatedExpr', 'withClass',\n'CloseMatch', 'tokenMap', 'pyparsing_common',\n]\n\nsystem_version = tuple(sys.version_info)[:3]\nPY_3 = system_version[0] == 3\nif PY_3:\n    _MAX_INT = sys.maxsize\n    basestring = str\n    unichr = chr\n    _ustr = str\n\n    # build list of single arg builtins, that can be used as parse actions\n    singleArgBuiltins = [sum, len, sorted, reversed, list, tuple, set, any, all, min, max]\n\nelse:\n    _MAX_INT = sys.maxint\n    range = xrange\n\n    def _ustr(obj):\n        \"\"\"Drop-in replacement for str(obj) that tries to be Unicode friendly. It first tries\n           str(obj). If that fails with a UnicodeEncodeError, then it tries unicode(obj). It\n           then < returns the unicode object | encodes it with the default encoding | ... >.\n        \"\"\"\n        if isinstance(obj,unicode):\n            return obj\n\n        try:\n            # If this works, then _ustr(obj) has the same behaviour as str(obj), so\n            # it won't break any existing code.\n            return str(obj)\n\n        except UnicodeEncodeError:\n            # Else encode it\n            ret = unicode(obj).encode(sys.getdefaultencoding(), 'xmlcharrefreplace')\n            xmlcharref = Regex(r'&#\\d+;')\n            xmlcharref.setParseAction(lambda t: '\\\\u' + hex(int(t[0][2:-1]))[2:])\n            return xmlcharref.transformString(ret)\n\n    # build list of single arg builtins, tolerant of Python version, that can be used as parse actions\n    singleArgBuiltins = []\n    import __builtin__\n    for fname in \"sum len sorted reversed list tuple set any all min max\".split():\n        try:\n            singleArgBuiltins.append(getattr(__builtin__,fname))\n        except AttributeError:\n            continue\n            \n_generatorType = type((y for y in range(1)))\n \ndef _xml_escape(data):\n    \"\"\"Escape &, <, >, \", ', etc. in a string of data.\"\"\"\n\n    # ampersand must be replaced first\n    from_symbols = '&><\"\\''\n    to_symbols = ('&'+s+';' for s in \"amp gt lt quot apos\".split())\n    for from_,to_ in zip(from_symbols, to_symbols):\n        data = data.replace(from_, to_)\n    return data\n\nclass _Constants(object):\n    pass\n\nalphas     = string.ascii_uppercase + string.ascii_lowercase\nnums       = \"0123456789\"\nhexnums    = nums + \"ABCDEFabcdef\"\nalphanums  = alphas + nums\n_bslash    = chr(92)\nprintables = \"\".join(c for c in string.printable if c not in string.whitespace)\n\nclass ParseBaseException(Exception):\n    \"\"\"base exception class for all parsing runtime exceptions\"\"\"\n    # Performance tuning: we construct a *lot* of these, so keep this\n    # constructor as small and fast as possible\n    def __init__( self, pstr, loc=0, msg=None, elem=None ):\n        self.loc = loc\n        if msg is None:\n            self.msg = pstr\n            self.pstr = \"\"\n        else:\n            self.msg = msg\n            self.pstr = pstr\n        self.parserElement = elem\n        self.args = (pstr, loc, msg)\n\n    @classmethod\n    def _from_exception(cls, pe):\n        \"\"\"\n        internal factory method to simplify creating one type of ParseException \n        from another - avoids having __init__ signature conflicts among subclasses\n        \"\"\"\n        return cls(pe.pstr, pe.loc, pe.msg, pe.parserElement)\n\n    def __getattr__( self, aname ):\n        \"\"\"supported attributes by name are:\n            - lineno - returns the line number of the exception text\n            - col - returns the column number of the exception text\n            - line - returns the line containing the exception text\n        \"\"\"\n        if( aname == \"lineno\" ):\n            return lineno( self.loc, self.pstr )\n        elif( aname in (\"col\", \"column\") ):\n            return col( self.loc, self.pstr )\n        elif( aname == \"line\" ):\n            return line( self.loc, self.pstr )\n        else:\n            raise AttributeError(aname)\n\n    def __str__( self ):\n        return \"%s (at char %d), (line:%d, col:%d)\" % \\\n                ( self.msg, self.loc, self.lineno, self.column )\n    def __repr__( self ):\n        return _ustr(self)\n    def markInputline( self, markerString = \">!<\" ):\n        \"\"\"Extracts the exception line from the input string, and marks\n           the location of the exception with a special symbol.\n        \"\"\"\n        line_str = self.line\n        line_column = self.column - 1\n        if markerString:\n            line_str = \"\".join((line_str[:line_column],\n                                markerString, line_str[line_column:]))\n        return line_str.strip()\n    def __dir__(self):\n        return \"lineno col line\".split() + dir(type(self))\n\nclass ParseException(ParseBaseException):\n    \"\"\"\n    Exception thrown when parse expressions don't match class;\n    supported attributes by name are:\n     - lineno - returns the line number of the exception text\n     - col - returns the column number of the exception text\n     - line - returns the line containing the exception text\n        \n    Example::\n        try:\n            Word(nums).setName(\"integer\").parseString(\"ABC\")\n        except ParseException as pe:\n            print(pe)\n            print(\"column: {}\".format(pe.col))\n            \n    prints::\n       Expected integer (at char 0), (line:1, col:1)\n        column: 1\n    \"\"\"\n    pass\n\nclass ParseFatalException(ParseBaseException):\n    \"\"\"user-throwable exception thrown when inconsistent parse content\n       is found; stops all parsing immediately\"\"\"\n    pass\n\nclass ParseSyntaxException(ParseFatalException):\n    \"\"\"just like L{ParseFatalException}, but thrown internally when an\n       L{ErrorStop<And._ErrorStop>} ('-' operator) indicates that parsing is to stop \n       immediately because an unbacktrackable syntax error has been found\"\"\"\n    pass\n\n#~ class ReparseException(ParseBaseException):\n    #~ \"\"\"Experimental class - parse actions can raise this exception to cause\n       #~ pyparsing to reparse the input string:\n        #~ - with a modified input string, and/or\n        #~ - with a modified start location\n       #~ Set the values of the ReparseException in the constructor, and raise the\n       #~ exception in a parse action to cause pyparsing to use the new string/location.\n       #~ Setting the values as None causes no change to be made.\n       #~ \"\"\"\n    #~ def __init_( self, newstring, restartLoc ):\n        #~ self.newParseText = newstring\n        #~ self.reparseLoc = restartLoc\n\nclass RecursiveGrammarException(Exception):\n    \"\"\"exception thrown by L{ParserElement.validate} if the grammar could be improperly recursive\"\"\"\n    def __init__( self, parseElementList ):\n        self.parseElementTrace = parseElementList\n\n    def __str__( self ):\n        return \"RecursiveGrammarException: %s\" % self.parseElementTrace\n\nclass _ParseResultsWithOffset(object):\n    def __init__(self,p1,p2):\n        self.tup = (p1,p2)\n    def __getitem__(self,i):\n        return self.tup[i]\n    def __repr__(self):\n        return repr(self.tup[0])\n    def setOffset(self,i):\n        self.tup = (self.tup[0],i)\n\nclass ParseResults(object):\n    \"\"\"\n    Structured parse results, to provide multiple means of access to the parsed data:\n       - as a list (C{len(results)})\n       - by list index (C{results[0], results[1]}, etc.)\n       - by attribute (C{results.<resultsName>} - see L{ParserElement.setResultsName})\n\n    Example::\n        integer = Word(nums)\n        date_str = (integer.setResultsName(\"year\") + '/' \n                        + integer.setResultsName(\"month\") + '/' \n                        + integer.setResultsName(\"day\"))\n        # equivalent form:\n        # date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")\n\n        # parseString returns a ParseResults object\n        result = date_str.parseString(\"1999/12/31\")\n\n        def test(s, fn=repr):\n            print(\"%s -> %s\" % (s, fn(eval(s))))\n        test(\"list(result)\")\n        test(\"result[0]\")\n        test(\"result['month']\")\n        test(\"result.day\")\n        test(\"'month' in result\")\n        test(\"'minutes' in result\")\n        test(\"result.dump()\", str)\n    prints::\n        list(result) -> ['1999', '/', '12', '/', '31']\n        result[0] -> '1999'\n        result['month'] -> '12'\n        result.day -> '31'\n        'month' in result -> True\n        'minutes' in result -> False\n        result.dump() -> ['1999', '/', '12', '/', '31']\n        - day: 31\n        - month: 12\n        - year: 1999\n    \"\"\"\n    def __new__(cls, toklist=None, name=None, asList=True, modal=True ):\n        if isinstance(toklist, cls):\n            return toklist\n        retobj = object.__new__(cls)\n        retobj.__doinit = True\n        return retobj\n\n    # Performance tuning: we construct a *lot* of these, so keep this\n    # constructor as small and fast as possible\n    def __init__( self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance ):\n        if self.__doinit:\n            self.__doinit = False\n            self.__name = None\n            self.__parent = None\n            self.__accumNames = {}\n            self.__asList = asList\n            self.__modal = modal\n            if toklist is None:\n                toklist = []\n            if isinstance(toklist, list):\n                self.__toklist = toklist[:]\n            elif isinstance(toklist, _generatorType):\n                self.__toklist = list(toklist)\n            else:\n                self.__toklist = [toklist]\n            self.__tokdict = dict()\n\n        if name is not None and name:\n            if not modal:\n                self.__accumNames[name] = 0\n            if isinstance(name,int):\n                name = _ustr(name) # will always return a str, but use _ustr for consistency\n            self.__name = name\n            if not (isinstance(toklist, (type(None), basestring, list)) and toklist in (None,'',[])):\n                if isinstance(toklist,basestring):\n                    toklist = [ toklist ]\n                if asList:\n                    if isinstance(toklist,ParseResults):\n                        self[name] = _ParseResultsWithOffset(toklist.copy(),0)\n                    else:\n                        self[name] = _ParseResultsWithOffset(ParseResults(toklist[0]),0)\n                    self[name].__name = name\n                else:\n                    try:\n                        self[name] = toklist[0]\n                    except (KeyError,TypeError,IndexError):\n                        self[name] = toklist\n\n    def __getitem__( self, i ):\n        if isinstance( i, (int,slice) ):\n            return self.__toklist[i]\n        else:\n            if i not in self.__accumNames:\n                return self.__tokdict[i][-1][0]\n            else:\n                return ParseResults([ v[0] for v in self.__tokdict[i] ])\n\n    def __setitem__( self, k, v, isinstance=isinstance ):\n        if isinstance(v,_ParseResultsWithOffset):\n            self.__tokdict[k] = self.__tokdict.get(k,list()) + [v]\n            sub = v[0]\n        elif isinstance(k,(int,slice)):\n            self.__toklist[k] = v\n            sub = v\n        else:\n            self.__tokdict[k] = self.__tokdict.get(k,list()) + [_ParseResultsWithOffset(v,0)]\n            sub = v\n        if isinstance(sub,ParseResults):\n            sub.__parent = wkref(self)\n\n    def __delitem__( self, i ):\n        if isinstance(i,(int,slice)):\n            mylen = len( self.__toklist )\n            del self.__toklist[i]\n\n            # convert int to slice\n            if isinstance(i, int):\n                if i < 0:\n                    i += mylen\n                i = slice(i, i+1)\n            # get removed indices\n            removed = list(range(*i.indices(mylen)))\n            removed.reverse()\n            # fixup indices in token dictionary\n            for name,occurrences in self.__tokdict.items():\n                for j in removed:\n                    for k, (value, position) in enumerate(occurrences):\n                        occurrences[k] = _ParseResultsWithOffset(value, position - (position > j))\n        else:\n            del self.__tokdict[i]\n\n    def __contains__( self, k ):\n        return k in self.__tokdict\n\n    def __len__( self ): return len( self.__toklist )\n    def __bool__(self): return ( not not self.__toklist )\n    __nonzero__ = __bool__\n    def __iter__( self ): return iter( self.__toklist )\n    def __reversed__( self ): return iter( self.__toklist[::-1] )\n    def _iterkeys( self ):\n        if hasattr(self.__tokdict, \"iterkeys\"):\n            return self.__tokdict.iterkeys()\n        else:\n            return iter(self.__tokdict)\n\n    def _itervalues( self ):\n        return (self[k] for k in self._iterkeys())\n            \n    def _iteritems( self ):\n        return ((k, self[k]) for k in self._iterkeys())\n\n    if PY_3:\n        keys = _iterkeys       \n        \"\"\"Returns an iterator of all named result keys (Python 3.x only).\"\"\"\n\n        values = _itervalues\n        \"\"\"Returns an iterator of all named result values (Python 3.x only).\"\"\"\n\n        items = _iteritems\n        \"\"\"Returns an iterator of all named result key-value tuples (Python 3.x only).\"\"\"\n\n    else:\n        iterkeys = _iterkeys\n        \"\"\"Returns an iterator of all named result keys (Python 2.x only).\"\"\"\n\n        itervalues = _itervalues\n        \"\"\"Returns an iterator of all named result values (Python 2.x only).\"\"\"\n\n        iteritems = _iteritems\n        \"\"\"Returns an iterator of all named result key-value tuples (Python 2.x only).\"\"\"\n\n        def keys( self ):\n            \"\"\"Returns all named result keys (as a list in Python 2.x, as an iterator in Python 3.x).\"\"\"\n            return list(self.iterkeys())\n\n        def values( self ):\n            \"\"\"Returns all named result values (as a list in Python 2.x, as an iterator in Python 3.x).\"\"\"\n            return list(self.itervalues())\n                \n        def items( self ):\n            \"\"\"Returns all named result key-values (as a list of tuples in Python 2.x, as an iterator in Python 3.x).\"\"\"\n            return list(self.iteritems())\n\n    def haskeys( self ):\n        \"\"\"Since keys() returns an iterator, this method is helpful in bypassing\n           code that looks for the existence of any defined results names.\"\"\"\n        return bool(self.__tokdict)\n        \n    def pop( self, *args, **kwargs):\n        \"\"\"\n        Removes and returns item at specified index (default=C{last}).\n        Supports both C{list} and C{dict} semantics for C{pop()}. If passed no\n        argument or an integer argument, it will use C{list} semantics\n        and pop tokens from the list of parsed tokens. If passed a \n        non-integer argument (most likely a string), it will use C{dict}\n        semantics and pop the corresponding value from any defined \n        results names. A second default return value argument is \n        supported, just as in C{dict.pop()}.\n\n        Example::\n            def remove_first(tokens):\n                tokens.pop(0)\n            print(OneOrMore(Word(nums)).parseString(\"0 123 321\")) # -> ['0', '123', '321']\n            print(OneOrMore(Word(nums)).addParseAction(remove_first).parseString(\"0 123 321\")) # -> ['123', '321']\n\n            label = Word(alphas)\n            patt = label(\"LABEL\") + OneOrMore(Word(nums))\n            print(patt.parseString(\"AAB 123 321\").dump())\n\n            # Use pop() in a parse action to remove named result (note that corresponding value is not\n            # removed from list form of results)\n            def remove_LABEL(tokens):\n                tokens.pop(\"LABEL\")\n                return tokens\n            patt.addParseAction(remove_LABEL)\n            print(patt.parseString(\"AAB 123 321\").dump())\n        prints::\n            ['AAB', '123', '321']\n            - LABEL: AAB\n\n            ['AAB', '123', '321']\n        \"\"\"\n        if not args:\n            args = [-1]\n        for k,v in kwargs.items():\n            if k == 'default':\n                args = (args[0], v)\n            else:\n                raise TypeError(\"pop() got an unexpected keyword argument '%s'\" % k)\n        if (isinstance(args[0], int) or \n                        len(args) == 1 or \n                        args[0] in self):\n            index = args[0]\n            ret = self[index]\n            del self[index]\n            return ret\n        else:\n            defaultvalue = args[1]\n            return defaultvalue\n\n    def get(self, key, defaultValue=None):\n        \"\"\"\n        Returns named result matching the given key, or if there is no\n        such name, then returns the given C{defaultValue} or C{None} if no\n        C{defaultValue} is specified.\n\n        Similar to C{dict.get()}.\n        \n        Example::\n            integer = Word(nums)\n            date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")           \n\n            result = date_str.parseString(\"1999/12/31\")\n            print(result.get(\"year\")) # -> '1999'\n            print(result.get(\"hour\", \"not specified\")) # -> 'not specified'\n            print(result.get(\"hour\")) # -> None\n        \"\"\"\n        if key in self:\n            return self[key]\n        else:\n            return defaultValue\n\n    def insert( self, index, insStr ):\n        \"\"\"\n        Inserts new element at location index in the list of parsed tokens.\n        \n        Similar to C{list.insert()}.\n\n        Example::\n            print(OneOrMore(Word(nums)).parseString(\"0 123 321\")) # -> ['0', '123', '321']\n\n            # use a parse action to insert the parse location in the front of the parsed results\n            def insert_locn(locn, tokens):\n                tokens.insert(0, locn)\n            print(OneOrMore(Word(nums)).addParseAction(insert_locn).parseString(\"0 123 321\")) # -> [0, '0', '123', '321']\n        \"\"\"\n        self.__toklist.insert(index, insStr)\n        # fixup indices in token dictionary\n        for name,occurrences in self.__tokdict.items():\n            for k, (value, position) in enumerate(occurrences):\n                occurrences[k] = _ParseResultsWithOffset(value, position + (position > index))\n\n    def append( self, item ):\n        \"\"\"\n        Add single element to end of ParseResults list of elements.\n\n        Example::\n            print(OneOrMore(Word(nums)).parseString(\"0 123 321\")) # -> ['0', '123', '321']\n            \n            # use a parse action to compute the sum of the parsed integers, and add it to the end\n            def append_sum(tokens):\n                tokens.append(sum(map(int, tokens)))\n            print(OneOrMore(Word(nums)).addParseAction(append_sum).parseString(\"0 123 321\")) # -> ['0', '123', '321', 444]\n        \"\"\"\n        self.__toklist.append(item)\n\n    def extend( self, itemseq ):\n        \"\"\"\n        Add sequence of elements to end of ParseResults list of elements.\n\n        Example::\n            patt = OneOrMore(Word(alphas))\n            \n            # use a parse action to append the reverse of the matched strings, to make a palindrome\n            def make_palindrome(tokens):\n                tokens.extend(reversed([t[::-1] for t in tokens]))\n                return ''.join(tokens)\n            print(patt.addParseAction(make_palindrome).parseString(\"lskdj sdlkjf lksd\")) # -> 'lskdjsdlkjflksddsklfjkldsjdksl'\n        \"\"\"\n        if isinstance(itemseq, ParseResults):\n            self += itemseq\n        else:\n            self.__toklist.extend(itemseq)\n\n    def clear( self ):\n        \"\"\"\n        Clear all elements and results names.\n        \"\"\"\n        del self.__toklist[:]\n        self.__tokdict.clear()\n\n    def __getattr__( self, name ):\n        try:\n            return self[name]\n        except KeyError:\n            return \"\"\n            \n        if name in self.__tokdict:\n            if name not in self.__accumNames:\n                return self.__tokdict[name][-1][0]\n            else:\n                return ParseResults([ v[0] for v in self.__tokdict[name] ])\n        else:\n            return \"\"\n\n    def __add__( self, other ):\n        ret = self.copy()\n        ret += other\n        return ret\n\n    def __iadd__( self, other ):\n        if other.__tokdict:\n            offset = len(self.__toklist)\n            addoffset = lambda a: offset if a<0 else a+offset\n            otheritems = other.__tokdict.items()\n            otherdictitems = [(k, _ParseResultsWithOffset(v[0],addoffset(v[1])) )\n                                for (k,vlist) in otheritems for v in vlist]\n            for k,v in otherdictitems:\n                self[k] = v\n                if isinstance(v[0],ParseResults):\n                    v[0].__parent = wkref(self)\n            \n        self.__toklist += other.__toklist\n        self.__accumNames.update( other.__accumNames )\n        return self\n\n    def __radd__(self, other):\n        if isinstance(other,int) and other == 0:\n            # useful for merging many ParseResults using sum() builtin\n            return self.copy()\n        else:\n            # this may raise a TypeError - so be it\n            return other + self\n        \n    def __repr__( self ):\n        return \"(%s, %s)\" % ( repr( self.__toklist ), repr( self.__tokdict ) )\n\n    def __str__( self ):\n        return '[' + ', '.join(_ustr(i) if isinstance(i, ParseResults) else repr(i) for i in self.__toklist) + ']'\n\n    def _asStringList( self, sep='' ):\n        out = []\n        for item in self.__toklist:\n            if out and sep:\n                out.append(sep)\n            if isinstance( item, ParseResults ):\n                out += item._asStringList()\n            else:\n                out.append( _ustr(item) )\n        return out\n\n    def asList( self ):\n        \"\"\"\n        Returns the parse results as a nested list of matching tokens, all converted to strings.\n\n        Example::\n            patt = OneOrMore(Word(alphas))\n            result = patt.parseString(\"sldkj lsdkj sldkj\")\n            # even though the result prints in string-like form, it is actually a pyparsing ParseResults\n            print(type(result), result) # -> <class 'pyparsing.ParseResults'> ['sldkj', 'lsdkj', 'sldkj']\n            \n            # Use asList() to create an actual list\n            result_list = result.asList()\n            print(type(result_list), result_list) # -> <class 'list'> ['sldkj', 'lsdkj', 'sldkj']\n        \"\"\"\n        return [res.asList() if isinstance(res,ParseResults) else res for res in self.__toklist]\n\n    def asDict( self ):\n        \"\"\"\n        Returns the named parse results as a nested dictionary.\n\n        Example::\n            integer = Word(nums)\n            date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")\n            \n            result = date_str.parseString('12/31/1999')\n            print(type(result), repr(result)) # -> <class 'pyparsing.ParseResults'> (['12', '/', '31', '/', '1999'], {'day': [('1999', 4)], 'year': [('12', 0)], 'month': [('31', 2)]})\n            \n            result_dict = result.asDict()\n            print(type(result_dict), repr(result_dict)) # -> <class 'dict'> {'day': '1999', 'year': '12', 'month': '31'}\n\n            # even though a ParseResults supports dict-like access, sometime you just need to have a dict\n            import json\n            print(json.dumps(result)) # -> Exception: TypeError: ... is not JSON serializable\n            print(json.dumps(result.asDict())) # -> {\"month\": \"31\", \"day\": \"1999\", \"year\": \"12\"}\n        \"\"\"\n        if PY_3:\n            item_fn = self.items\n        else:\n            item_fn = self.iteritems\n            \n        def toItem(obj):\n            if isinstance(obj, ParseResults):\n                if obj.haskeys():\n                    return obj.asDict()\n                else:\n                    return [toItem(v) for v in obj]\n            else:\n                return obj\n                \n        return dict((k,toItem(v)) for k,v in item_fn())\n\n    def copy( self ):\n        \"\"\"\n        Returns a new copy of a C{ParseResults} object.\n        \"\"\"\n        ret = ParseResults( self.__toklist )\n        ret.__tokdict = self.__tokdict.copy()\n        ret.__parent = self.__parent\n        ret.__accumNames.update( self.__accumNames )\n        ret.__name = self.__name\n        return ret\n\n    def asXML( self, doctag=None, namedItemsOnly=False, indent=\"\", formatted=True ):\n        \"\"\"\n        (Deprecated) Returns the parse results as XML. Tags are created for tokens and lists that have defined results names.\n        \"\"\"\n        nl = \"\\n\"\n        out = []\n        namedItems = dict((v[1],k) for (k,vlist) in self.__tokdict.items()\n                                                            for v in vlist)\n        nextLevelIndent = indent + \"  \"\n\n        # collapse out indents if formatting is not desired\n        if not formatted:\n            indent = \"\"\n            nextLevelIndent = \"\"\n            nl = \"\"\n\n        selfTag = None\n        if doctag is not None:\n            selfTag = doctag\n        else:\n            if self.__name:\n                selfTag = self.__name\n\n        if not selfTag:\n            if namedItemsOnly:\n                return \"\"\n            else:\n                selfTag = \"ITEM\"\n\n        out += [ nl, indent, \"<\", selfTag, \">\" ]\n\n        for i,res in enumerate(self.__toklist):\n            if isinstance(res,ParseResults):\n                if i in namedItems:\n                    out += [ res.asXML(namedItems[i],\n                                        namedItemsOnly and doctag is None,\n                                        nextLevelIndent,\n                                        formatted)]\n                else:\n                    out += [ res.asXML(None,\n                                        namedItemsOnly and doctag is None,\n                                        nextLevelIndent,\n                                        formatted)]\n            else:\n                # individual token, see if there is a name for it\n                resTag = None\n                if i in namedItems:\n                    resTag = namedItems[i]\n                if not resTag:\n                    if namedItemsOnly:\n                        continue\n                    else:\n                        resTag = \"ITEM\"\n                xmlBodyText = _xml_escape(_ustr(res))\n                out += [ nl, nextLevelIndent, \"<\", resTag, \">\",\n                                                xmlBodyText,\n                                                \"</\", resTag, \">\" ]\n\n        out += [ nl, indent, \"</\", selfTag, \">\" ]\n        return \"\".join(out)\n\n    def __lookup(self,sub):\n        for k,vlist in self.__tokdict.items():\n            for v,loc in vlist:\n                if sub is v:\n                    return k\n        return None\n\n    def getName(self):\n        r\"\"\"\n        Returns the results name for this token expression. Useful when several \n        different expressions might match at a particular location.\n\n        Example::\n            integer = Word(nums)\n            ssn_expr = Regex(r\"\\d\\d\\d-\\d\\d-\\d\\d\\d\\d\")\n            house_number_expr = Suppress('#') + Word(nums, alphanums)\n            user_data = (Group(house_number_expr)(\"house_number\") \n                        | Group(ssn_expr)(\"ssn\")\n                        | Group(integer)(\"age\"))\n            user_info = OneOrMore(user_data)\n            \n            result = user_info.parseString(\"22 111-22-3333 #221B\")\n            for item in result:\n                print(item.getName(), ':', item[0])\n        prints::\n            age : 22\n            ssn : 111-22-3333\n            house_number : 221B\n        \"\"\"\n        if self.__name:\n            return self.__name\n        elif self.__parent:\n            par = self.__parent()\n            if par:\n                return par.__lookup(self)\n            else:\n                return None\n        elif (len(self) == 1 and\n               len(self.__tokdict) == 1 and\n               next(iter(self.__tokdict.values()))[0][1] in (0,-1)):\n            return next(iter(self.__tokdict.keys()))\n        else:\n            return None\n\n    def dump(self, indent='', depth=0, full=True):\n        \"\"\"\n        Diagnostic method for listing out the contents of a C{ParseResults}.\n        Accepts an optional C{indent} argument so that this string can be embedded\n        in a nested display of other data.\n\n        Example::\n            integer = Word(nums)\n            date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")\n            \n            result = date_str.parseString('12/31/1999')\n            print(result.dump())\n        prints::\n            ['12', '/', '31', '/', '1999']\n            - day: 1999\n            - month: 31\n            - year: 12\n        \"\"\"\n        out = []\n        NL = '\\n'\n        out.append( indent+_ustr(self.asList()) )\n        if full:\n            if self.haskeys():\n                items = sorted((str(k), v) for k,v in self.items())\n                for k,v in items:\n                    if out:\n                        out.append(NL)\n                    out.append( \"%s%s- %s: \" % (indent,('  '*depth), k) )\n                    if isinstance(v,ParseResults):\n                        if v:\n                            out.append( v.dump(indent,depth+1) )\n                        else:\n                            out.append(_ustr(v))\n                    else:\n                        out.append(repr(v))\n            elif any(isinstance(vv,ParseResults) for vv in self):\n                v = self\n                for i,vv in enumerate(v):\n                    if isinstance(vv,ParseResults):\n                        out.append(\"\\n%s%s[%d]:\\n%s%s%s\" % (indent,('  '*(depth)),i,indent,('  '*(depth+1)),vv.dump(indent,depth+1) ))\n                    else:\n                        out.append(\"\\n%s%s[%d]:\\n%s%s%s\" % (indent,('  '*(depth)),i,indent,('  '*(depth+1)),_ustr(vv)))\n            \n        return \"\".join(out)\n\n    def pprint(self, *args, **kwargs):\n        \"\"\"\n        Pretty-printer for parsed results as a list, using the C{pprint} module.\n        Accepts additional positional or keyword args as defined for the \n        C{pprint.pprint} method. (U{http://docs.python.org/3/library/pprint.html#pprint.pprint})\n\n        Example::\n            ident = Word(alphas, alphanums)\n            num = Word(nums)\n            func = Forward()\n            term = ident | num | Group('(' + func + ')')\n            func <<= ident + Group(Optional(delimitedList(term)))\n            result = func.parseString(\"fna a,b,(fnb c,d,200),100\")\n            result.pprint(width=40)\n        prints::\n            ['fna',\n             ['a',\n              'b',\n              ['(', 'fnb', ['c', 'd', '200'], ')'],\n              '100']]\n        \"\"\"\n        pprint.pprint(self.asList(), *args, **kwargs)\n\n    # add support for pickle protocol\n    def __getstate__(self):\n        return ( self.__toklist,\n                 ( self.__tokdict.copy(),\n                   self.__parent is not None and self.__parent() or None,\n                   self.__accumNames,\n                   self.__name ) )\n\n    def __setstate__(self,state):\n        self.__toklist = state[0]\n        (self.__tokdict,\n         par,\n         inAccumNames,\n         self.__name) = state[1]\n        self.__accumNames = {}\n        self.__accumNames.update(inAccumNames)\n        if par is not None:\n            self.__parent = wkref(par)\n        else:\n            self.__parent = None\n\n    def __getnewargs__(self):\n        return self.__toklist, self.__name, self.__asList, self.__modal\n\n    def __dir__(self):\n        return (dir(type(self)) + list(self.keys()))\n\nMutableMapping.register(ParseResults)\n\ndef col (loc,strg):\n    \"\"\"Returns current column within a string, counting newlines as line separators.\n   The first column is number 1.\n\n   Note: the default parsing behavior is to expand tabs in the input string\n   before starting the parsing process.  See L{I{ParserElement.parseString}<ParserElement.parseString>} for more information\n   on parsing strings containing C{<TAB>}s, and suggested methods to maintain a\n   consistent view of the parsed string, the parse location, and line and column\n   positions within the parsed string.\n   \"\"\"\n    s = strg\n    return 1 if 0<loc<len(s) and s[loc-1] == '\\n' else loc - s.rfind(\"\\n\", 0, loc)\n\ndef lineno(loc,strg):\n    \"\"\"Returns current line number within a string, counting newlines as line separators.\n   The first line is number 1.\n\n   Note: the default parsing behavior is to expand tabs in the input string\n   before starting the parsing process.  See L{I{ParserElement.parseString}<ParserElement.parseString>} for more information\n   on parsing strings containing C{<TAB>}s, and suggested methods to maintain a\n   consistent view of the parsed string, the parse location, and line and column\n   positions within the parsed string.\n   \"\"\"\n    return strg.count(\"\\n\",0,loc) + 1\n\ndef line( loc, strg ):\n    \"\"\"Returns the line of text containing loc within a string, counting newlines as line separators.\n       \"\"\"\n    lastCR = strg.rfind(\"\\n\", 0, loc)\n    nextCR = strg.find(\"\\n\", loc)\n    if nextCR >= 0:\n        return strg[lastCR+1:nextCR]\n    else:\n        return strg[lastCR+1:]\n\ndef _defaultStartDebugAction( instring, loc, expr ):\n    print ((\"Match \" + _ustr(expr) + \" at loc \" + _ustr(loc) + \"(%d,%d)\" % ( lineno(loc,instring), col(loc,instring) )))\n\ndef _defaultSuccessDebugAction( instring, startloc, endloc, expr, toks ):\n    print (\"Matched \" + _ustr(expr) + \" -> \" + str(toks.asList()))\n\ndef _defaultExceptionDebugAction( instring, loc, expr, exc ):\n    print (\"Exception raised:\" + _ustr(exc))\n\ndef nullDebugAction(*args):\n    \"\"\"'Do-nothing' debug action, to suppress debugging output during parsing.\"\"\"\n    pass\n\n# Only works on Python 3.x - nonlocal is toxic to Python 2 installs\n#~ 'decorator to trim function calls to match the arity of the target'\n#~ def _trim_arity(func, maxargs=3):\n    #~ if func in singleArgBuiltins:\n        #~ return lambda s,l,t: func(t)\n    #~ limit = 0\n    #~ foundArity = False\n    #~ def wrapper(*args):\n        #~ nonlocal limit,foundArity\n        #~ while 1:\n            #~ try:\n                #~ ret = func(*args[limit:])\n                #~ foundArity = True\n                #~ return ret\n            #~ except TypeError:\n                #~ if limit == maxargs or foundArity:\n                    #~ raise\n                #~ limit += 1\n                #~ continue\n    #~ return wrapper\n\n# this version is Python 2.x-3.x cross-compatible\n'decorator to trim function calls to match the arity of the target'\ndef _trim_arity(func, maxargs=2):\n    if func in singleArgBuiltins:\n        return lambda s,l,t: func(t)\n    limit = [0]\n    foundArity = [False]\n    \n    # traceback return data structure changed in Py3.5 - normalize back to plain tuples\n    if system_version[:2] >= (3,5):\n        def extract_stack(limit=0):\n            # special handling for Python 3.5.0 - extra deep call stack by 1\n            offset = -3 if system_version == (3,5,0) else -2\n            frame_summary = traceback.extract_stack(limit=-offset+limit-1)[offset]\n            return [frame_summary[:2]]\n        def extract_tb(tb, limit=0):\n            frames = traceback.extract_tb(tb, limit=limit)\n            frame_summary = frames[-1]\n            return [frame_summary[:2]]\n    else:\n        extract_stack = traceback.extract_stack\n        extract_tb = traceback.extract_tb\n    \n    # synthesize what would be returned by traceback.extract_stack at the call to \n    # user's parse action 'func', so that we don't incur call penalty at parse time\n    \n    LINE_DIFF = 6\n    # IF ANY CODE CHANGES, EVEN JUST COMMENTS OR BLANK LINES, BETWEEN THE NEXT LINE AND \n    # THE CALL TO FUNC INSIDE WRAPPER, LINE_DIFF MUST BE MODIFIED!!!!\n    this_line = extract_stack(limit=2)[-1]\n    pa_call_line_synth = (this_line[0], this_line[1]+LINE_DIFF)\n\n    def wrapper(*args):\n        while 1:\n            try:\n                ret = func(*args[limit[0]:])\n                foundArity[0] = True\n                return ret\n            except TypeError:\n                # re-raise TypeErrors if they did not come from our arity testing\n                if foundArity[0]:\n                    raise\n                else:\n                    try:\n                        tb = sys.exc_info()[-1]\n                        if not extract_tb(tb, limit=2)[-1][:2] == pa_call_line_synth:\n                            raise\n                    finally:\n                        del tb\n\n                if limit[0] <= maxargs:\n                    limit[0] += 1\n                    continue\n                raise\n\n    # copy func name to wrapper for sensible debug output\n    func_name = \"<parse action>\"\n    try:\n        func_name = getattr(func, '__name__', \n                            getattr(func, '__class__').__name__)\n    except Exception:\n        func_name = str(func)\n    wrapper.__name__ = func_name\n\n    return wrapper\n\nclass ParserElement(object):\n    \"\"\"Abstract base level parser element class.\"\"\"\n    DEFAULT_WHITE_CHARS = \" \\n\\t\\r\"\n    verbose_stacktrace = False\n\n    @staticmethod\n    def setDefaultWhitespaceChars( chars ):\n        r\"\"\"\n        Overrides the default whitespace chars\n\n        Example::\n            # default whitespace chars are space, <TAB> and newline\n            OneOrMore(Word(alphas)).parseString(\"abc def\\nghi jkl\")  # -> ['abc', 'def', 'ghi', 'jkl']\n            \n            # change to just treat newline as significant\n            ParserElement.setDefaultWhitespaceChars(\" \\t\")\n            OneOrMore(Word(alphas)).parseString(\"abc def\\nghi jkl\")  # -> ['abc', 'def']\n        \"\"\"\n        ParserElement.DEFAULT_WHITE_CHARS = chars\n\n    @staticmethod\n    def inlineLiteralsUsing(cls):\n        \"\"\"\n        Set class to be used for inclusion of string literals into a parser.\n        \n        Example::\n            # default literal class used is Literal\n            integer = Word(nums)\n            date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")           \n\n            date_str.parseString(\"1999/12/31\")  # -> ['1999', '/', '12', '/', '31']\n\n\n            # change to Suppress\n            ParserElement.inlineLiteralsUsing(Suppress)\n            date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")           \n\n            date_str.parseString(\"1999/12/31\")  # -> ['1999', '12', '31']\n        \"\"\"\n        ParserElement._literalStringClass = cls\n\n    def __init__( self, savelist=False ):\n        self.parseAction = list()\n        self.failAction = None\n        #~ self.name = \"<unknown>\"  # don't define self.name, let subclasses try/except upcall\n        self.strRepr = None\n        self.resultsName = None\n        self.saveAsList = savelist\n        self.skipWhitespace = True\n        self.whiteChars = ParserElement.DEFAULT_WHITE_CHARS\n        self.copyDefaultWhiteChars = True\n        self.mayReturnEmpty = False # used when checking for left-recursion\n        self.keepTabs = False\n        self.ignoreExprs = list()\n        self.debug = False\n        self.streamlined = False\n        self.mayIndexError = True # used to optimize exception handling for subclasses that don't advance parse index\n        self.errmsg = \"\"\n        self.modalResults = True # used to mark results names as modal (report only last) or cumulative (list all)\n        self.debugActions = ( None, None, None ) #custom debug actions\n        self.re = None\n        self.callPreparse = True # used to avoid redundant calls to preParse\n        self.callDuringTry = False\n\n    def copy( self ):\n        \"\"\"\n        Make a copy of this C{ParserElement}.  Useful for defining different parse actions\n        for the same parsing pattern, using copies of the original parse element.\n        \n        Example::\n            integer = Word(nums).setParseAction(lambda toks: int(toks[0]))\n            integerK = integer.copy().addParseAction(lambda toks: toks[0]*1024) + Suppress(\"K\")\n            integerM = integer.copy().addParseAction(lambda toks: toks[0]*1024*1024) + Suppress(\"M\")\n            \n            print(OneOrMore(integerK | integerM | integer).parseString(\"5K 100 640K 256M\"))\n        prints::\n            [5120, 100, 655360, 268435456]\n        Equivalent form of C{expr.copy()} is just C{expr()}::\n            integerM = integer().addParseAction(lambda toks: toks[0]*1024*1024) + Suppress(\"M\")\n        \"\"\"\n        cpy = copy.copy( self )\n        cpy.parseAction = self.parseAction[:]\n        cpy.ignoreExprs = self.ignoreExprs[:]\n        if self.copyDefaultWhiteChars:\n            cpy.whiteChars = ParserElement.DEFAULT_WHITE_CHARS\n        return cpy\n\n    def setName( self, name ):\n        \"\"\"\n        Define name for this expression, makes debugging and exception messages clearer.\n        \n        Example::\n            Word(nums).parseString(\"ABC\")  # -> Exception: Expected W:(0123...) (at char 0), (line:1, col:1)\n            Word(nums).setName(\"integer\").parseString(\"ABC\")  # -> Exception: Expected integer (at char 0), (line:1, col:1)\n        \"\"\"\n        self.name = name\n        self.errmsg = \"Expected \" + self.name\n        if hasattr(self,\"exception\"):\n            self.exception.msg = self.errmsg\n        return self\n\n    def setResultsName( self, name, listAllMatches=False ):\n        \"\"\"\n        Define name for referencing matching tokens as a nested attribute\n        of the returned parse results.\n        NOTE: this returns a *copy* of the original C{ParserElement} object;\n        this is so that the client can define a basic element, such as an\n        integer, and reference it in multiple places with different names.\n\n        You can also set results names using the abbreviated syntax,\n        C{expr(\"name\")} in place of C{expr.setResultsName(\"name\")} - \n        see L{I{__call__}<__call__>}.\n\n        Example::\n            date_str = (integer.setResultsName(\"year\") + '/' \n                        + integer.setResultsName(\"month\") + '/' \n                        + integer.setResultsName(\"day\"))\n\n            # equivalent form:\n            date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")\n        \"\"\"\n        newself = self.copy()\n        if name.endswith(\"*\"):\n            name = name[:-1]\n            listAllMatches=True\n        newself.resultsName = name\n        newself.modalResults = not listAllMatches\n        return newself\n\n    def setBreak(self,breakFlag = True):\n        \"\"\"Method to invoke the Python pdb debugger when this element is\n           about to be parsed. Set C{breakFlag} to True to enable, False to\n           disable.\n        \"\"\"\n        if breakFlag:\n            _parseMethod = self._parse\n            def breaker(instring, loc, doActions=True, callPreParse=True):\n                import pdb\n                pdb.set_trace()\n                return _parseMethod( instring, loc, doActions, callPreParse )\n            breaker._originalParseMethod = _parseMethod\n            self._parse = breaker\n        else:\n            if hasattr(self._parse,\"_originalParseMethod\"):\n                self._parse = self._parse._originalParseMethod\n        return self\n\n    def setParseAction( self, *fns, **kwargs ):\n        \"\"\"\n        Define one or more actions to perform when successfully matching parse element definition.\n        Parse action fn is a callable method with 0-3 arguments, called as C{fn(s,loc,toks)},\n        C{fn(loc,toks)}, C{fn(toks)}, or just C{fn()}, where:\n         - s   = the original string being parsed (see note below)\n         - loc = the location of the matching substring\n         - toks = a list of the matched tokens, packaged as a C{L{ParseResults}} object\n        If the functions in fns modify the tokens, they can return them as the return\n        value from fn, and the modified list of tokens will replace the original.\n        Otherwise, fn does not need to return any value.\n\n        Optional keyword arguments:\n         - callDuringTry = (default=C{False}) indicate if parse action should be run during lookaheads and alternate testing\n\n        Note: the default parsing behavior is to expand tabs in the input string\n        before starting the parsing process.  See L{I{parseString}<parseString>} for more information\n        on parsing strings containing C{<TAB>}s, and suggested methods to maintain a\n        consistent view of the parsed string, the parse location, and line and column\n        positions within the parsed string.\n        \n        Example::\n            integer = Word(nums)\n            date_str = integer + '/' + integer + '/' + integer\n\n            date_str.parseString(\"1999/12/31\")  # -> ['1999', '/', '12', '/', '31']\n\n            # use parse action to convert to ints at parse time\n            integer = Word(nums).setParseAction(lambda toks: int(toks[0]))\n            date_str = integer + '/' + integer + '/' + integer\n\n            # note that integer fields are now ints, not strings\n            date_str.parseString(\"1999/12/31\")  # -> [1999, '/', 12, '/', 31]\n        \"\"\"\n        self.parseAction = list(map(_trim_arity, list(fns)))\n        self.callDuringTry = kwargs.get(\"callDuringTry\", False)\n        return self\n\n    def addParseAction( self, *fns, **kwargs ):\n        \"\"\"\n        Add one or more parse actions to expression's list of parse actions. See L{I{setParseAction}<setParseAction>}.\n        \n        See examples in L{I{copy}<copy>}.\n        \"\"\"\n        self.parseAction += list(map(_trim_arity, list(fns)))\n        self.callDuringTry = self.callDuringTry or kwargs.get(\"callDuringTry\", False)\n        return self\n\n    def addCondition(self, *fns, **kwargs):\n        \"\"\"Add a boolean predicate function to expression's list of parse actions. See \n        L{I{setParseAction}<setParseAction>} for function call signatures. Unlike C{setParseAction}, \n        functions passed to C{addCondition} need to return boolean success/fail of the condition.\n\n        Optional keyword arguments:\n         - message = define a custom message to be used in the raised exception\n         - fatal   = if True, will raise ParseFatalException to stop parsing immediately; otherwise will raise ParseException\n         \n        Example::\n            integer = Word(nums).setParseAction(lambda toks: int(toks[0]))\n            year_int = integer.copy()\n            year_int.addCondition(lambda toks: toks[0] >= 2000, message=\"Only support years 2000 and later\")\n            date_str = year_int + '/' + integer + '/' + integer\n\n            result = date_str.parseString(\"1999/12/31\")  # -> Exception: Only support years 2000 and later (at char 0), (line:1, col:1)\n        \"\"\"\n        msg = kwargs.get(\"message\", \"failed user-defined condition\")\n        exc_type = ParseFatalException if kwargs.get(\"fatal\", False) else ParseException\n        for fn in fns:\n            def pa(s,l,t):\n                if not bool(_trim_arity(fn)(s,l,t)):\n                    raise exc_type(s,l,msg)\n            self.parseAction.append(pa)\n        self.callDuringTry = self.callDuringTry or kwargs.get(\"callDuringTry\", False)\n        return self\n\n    def setFailAction( self, fn ):\n        \"\"\"Define action to perform if parsing fails at this expression.\n           Fail acton fn is a callable function that takes the arguments\n           C{fn(s,loc,expr,err)} where:\n            - s = string being parsed\n            - loc = location where expression match was attempted and failed\n            - expr = the parse expression that failed\n            - err = the exception thrown\n           The function returns no value.  It may throw C{L{ParseFatalException}}\n           if it is desired to stop parsing immediately.\"\"\"\n        self.failAction = fn\n        return self\n\n    def _skipIgnorables( self, instring, loc ):\n        exprsFound = True\n        while exprsFound:\n            exprsFound = False\n            for e in self.ignoreExprs:\n                try:\n                    while 1:\n                        loc,dummy = e._parse( instring, loc )\n                        exprsFound = True\n                except ParseException:\n                    pass\n        return loc\n\n    def preParse( self, instring, loc ):\n        if self.ignoreExprs:\n            loc = self._skipIgnorables( instring, loc )\n\n        if self.skipWhitespace:\n            wt = self.whiteChars\n            instrlen = len(instring)\n            while loc < instrlen and instring[loc] in wt:\n                loc += 1\n\n        return loc\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        return loc, []\n\n    def postParse( self, instring, loc, tokenlist ):\n        return tokenlist\n\n    #~ @profile\n    def _parseNoCache( self, instring, loc, doActions=True, callPreParse=True ):\n        debugging = ( self.debug ) #and doActions )\n\n        if debugging or self.failAction:\n            #~ print (\"Match\",self,\"at loc\",loc,\"(%d,%d)\" % ( lineno(loc,instring), col(loc,instring) ))\n            if (self.debugActions[0] ):\n                self.debugActions[0]( instring, loc, self )\n            if callPreParse and self.callPreparse:\n                preloc = self.preParse( instring, loc )\n            else:\n                preloc = loc\n            tokensStart = preloc\n            try:\n                try:\n                    loc,tokens = self.parseImpl( instring, preloc, doActions )\n                except IndexError:\n                    raise ParseException( instring, len(instring), self.errmsg, self )\n            except ParseBaseException as err:\n                #~ print (\"Exception raised:\", err)\n                if self.debugActions[2]:\n                    self.debugActions[2]( instring, tokensStart, self, err )\n                if self.failAction:\n                    self.failAction( instring, tokensStart, self, err )\n                raise\n        else:\n            if callPreParse and self.callPreparse:\n                preloc = self.preParse( instring, loc )\n            else:\n                preloc = loc\n            tokensStart = preloc\n            if self.mayIndexError or preloc >= len(instring):\n                try:\n                    loc,tokens = self.parseImpl( instring, preloc, doActions )\n                except IndexError:\n                    raise ParseException( instring, len(instring), self.errmsg, self )\n            else:\n                loc,tokens = self.parseImpl( instring, preloc, doActions )\n\n        tokens = self.postParse( instring, loc, tokens )\n\n        retTokens = ParseResults( tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults )\n        if self.parseAction and (doActions or self.callDuringTry):\n            if debugging:\n                try:\n                    for fn in self.parseAction:\n                        tokens = fn( instring, tokensStart, retTokens )\n                        if tokens is not None:\n                            retTokens = ParseResults( tokens,\n                                                      self.resultsName,\n                                                      asList=self.saveAsList and isinstance(tokens,(ParseResults,list)),\n                                                      modal=self.modalResults )\n                except ParseBaseException as err:\n                    #~ print \"Exception raised in user parse action:\", err\n                    if (self.debugActions[2] ):\n                        self.debugActions[2]( instring, tokensStart, self, err )\n                    raise\n            else:\n                for fn in self.parseAction:\n                    tokens = fn( instring, tokensStart, retTokens )\n                    if tokens is not None:\n                        retTokens = ParseResults( tokens,\n                                                  self.resultsName,\n                                                  asList=self.saveAsList and isinstance(tokens,(ParseResults,list)),\n                                                  modal=self.modalResults )\n        if debugging:\n            #~ print (\"Matched\",self,\"->\",retTokens.asList())\n            if (self.debugActions[1] ):\n                self.debugActions[1]( instring, tokensStart, loc, self, retTokens )\n\n        return loc, retTokens\n\n    def tryParse( self, instring, loc ):\n        try:\n            return self._parse( instring, loc, doActions=False )[0]\n        except ParseFatalException:\n            raise ParseException( instring, loc, self.errmsg, self)\n    \n    def canParseNext(self, instring, loc):\n        try:\n            self.tryParse(instring, loc)\n        except (ParseException, IndexError):\n            return False\n        else:\n            return True\n\n    class _UnboundedCache(object):\n        def __init__(self):\n            cache = {}\n            self.not_in_cache = not_in_cache = object()\n\n            def get(self, key):\n                return cache.get(key, not_in_cache)\n\n            def set(self, key, value):\n                cache[key] = value\n\n            def clear(self):\n                cache.clear()\n                \n            def cache_len(self):\n                return len(cache)\n\n            self.get = types.MethodType(get, self)\n            self.set = types.MethodType(set, self)\n            self.clear = types.MethodType(clear, self)\n            self.__len__ = types.MethodType(cache_len, self)\n\n    if _OrderedDict is not None:\n        class _FifoCache(object):\n            def __init__(self, size):\n                self.not_in_cache = not_in_cache = object()\n\n                cache = _OrderedDict()\n\n                def get(self, key):\n                    return cache.get(key, not_in_cache)\n\n                def set(self, key, value):\n                    cache[key] = value\n                    while len(cache) > size:\n                        try:\n                            cache.popitem(False)\n                        except KeyError:\n                            pass\n\n                def clear(self):\n                    cache.clear()\n\n                def cache_len(self):\n                    return len(cache)\n\n                self.get = types.MethodType(get, self)\n                self.set = types.MethodType(set, self)\n                self.clear = types.MethodType(clear, self)\n                self.__len__ = types.MethodType(cache_len, self)\n\n    else:\n        class _FifoCache(object):\n            def __init__(self, size):\n                self.not_in_cache = not_in_cache = object()\n\n                cache = {}\n                key_fifo = collections.deque([], size)\n\n                def get(self, key):\n                    return cache.get(key, not_in_cache)\n\n                def set(self, key, value):\n                    cache[key] = value\n                    while len(key_fifo) > size:\n                        cache.pop(key_fifo.popleft(), None)\n                    key_fifo.append(key)\n\n                def clear(self):\n                    cache.clear()\n                    key_fifo.clear()\n\n                def cache_len(self):\n                    return len(cache)\n\n                self.get = types.MethodType(get, self)\n                self.set = types.MethodType(set, self)\n                self.clear = types.MethodType(clear, self)\n                self.__len__ = types.MethodType(cache_len, self)\n\n    # argument cache for optimizing repeated calls when backtracking through recursive expressions\n    packrat_cache = {} # this is set later by enabledPackrat(); this is here so that resetCache() doesn't fail\n    packrat_cache_lock = RLock()\n    packrat_cache_stats = [0, 0]\n\n    # this method gets repeatedly called during backtracking with the same arguments -\n    # we can cache these arguments and save ourselves the trouble of re-parsing the contained expression\n    def _parseCache( self, instring, loc, doActions=True, callPreParse=True ):\n        HIT, MISS = 0, 1\n        lookup = (self, instring, loc, callPreParse, doActions)\n        with ParserElement.packrat_cache_lock:\n            cache = ParserElement.packrat_cache\n            value = cache.get(lookup)\n            if value is cache.not_in_cache:\n                ParserElement.packrat_cache_stats[MISS] += 1\n                try:\n                    value = self._parseNoCache(instring, loc, doActions, callPreParse)\n                except ParseBaseException as pe:\n                    # cache a copy of the exception, without the traceback\n                    cache.set(lookup, pe.__class__(*pe.args))\n                    raise\n                else:\n                    cache.set(lookup, (value[0], value[1].copy()))\n                    return value\n            else:\n                ParserElement.packrat_cache_stats[HIT] += 1\n                if isinstance(value, Exception):\n                    raise value\n                return (value[0], value[1].copy())\n\n    _parse = _parseNoCache\n\n    @staticmethod\n    def resetCache():\n        ParserElement.packrat_cache.clear()\n        ParserElement.packrat_cache_stats[:] = [0] * len(ParserElement.packrat_cache_stats)\n\n    _packratEnabled = False\n    @staticmethod\n    def enablePackrat(cache_size_limit=128):\n        \"\"\"Enables \"packrat\" parsing, which adds memoizing to the parsing logic.\n           Repeated parse attempts at the same string location (which happens\n           often in many complex grammars) can immediately return a cached value,\n           instead of re-executing parsing/validating code.  Memoizing is done of\n           both valid results and parsing exceptions.\n           \n           Parameters:\n            - cache_size_limit - (default=C{128}) - if an integer value is provided\n              will limit the size of the packrat cache; if None is passed, then\n              the cache size will be unbounded; if 0 is passed, the cache will\n              be effectively disabled.\n            \n           This speedup may break existing programs that use parse actions that\n           have side-effects.  For this reason, packrat parsing is disabled when\n           you first import pyparsing.  To activate the packrat feature, your\n           program must call the class method C{ParserElement.enablePackrat()}.  If\n           your program uses C{psyco} to \"compile as you go\", you must call\n           C{enablePackrat} before calling C{psyco.full()}.  If you do not do this,\n           Python will crash.  For best results, call C{enablePackrat()} immediately\n           after importing pyparsing.\n           \n           Example::\n               import pyparsing\n               pyparsing.ParserElement.enablePackrat()\n        \"\"\"\n        if not ParserElement._packratEnabled:\n            ParserElement._packratEnabled = True\n            if cache_size_limit is None:\n                ParserElement.packrat_cache = ParserElement._UnboundedCache()\n            else:\n                ParserElement.packrat_cache = ParserElement._FifoCache(cache_size_limit)\n            ParserElement._parse = ParserElement._parseCache\n\n    def parseString( self, instring, parseAll=False ):\n        \"\"\"\n        Execute the parse expression with the given string.\n        This is the main interface to the client code, once the complete\n        expression has been built.\n\n        If you want the grammar to require that the entire input string be\n        successfully parsed, then set C{parseAll} to True (equivalent to ending\n        the grammar with C{L{StringEnd()}}).\n\n        Note: C{parseString} implicitly calls C{expandtabs()} on the input string,\n        in order to report proper column numbers in parse actions.\n        If the input string contains tabs and\n        the grammar uses parse actions that use the C{loc} argument to index into the\n        string being parsed, you can ensure you have a consistent view of the input\n        string by:\n         - calling C{parseWithTabs} on your grammar before calling C{parseString}\n           (see L{I{parseWithTabs}<parseWithTabs>})\n         - define your parse action using the full C{(s,loc,toks)} signature, and\n           reference the input string using the parse action's C{s} argument\n         - explictly expand the tabs in your input string before calling\n           C{parseString}\n        \n        Example::\n            Word('a').parseString('aaaaabaaa')  # -> ['aaaaa']\n            Word('a').parseString('aaaaabaaa', parseAll=True)  # -> Exception: Expected end of text\n        \"\"\"\n        ParserElement.resetCache()\n        if not self.streamlined:\n            self.streamline()\n            #~ self.saveAsList = True\n        for e in self.ignoreExprs:\n            e.streamline()\n        if not self.keepTabs:\n            instring = instring.expandtabs()\n        try:\n            loc, tokens = self._parse( instring, 0 )\n            if parseAll:\n                loc = self.preParse( instring, loc )\n                se = Empty() + StringEnd()\n                se._parse( instring, loc )\n        except ParseBaseException as exc:\n            if ParserElement.verbose_stacktrace:\n                raise\n            else:\n                # catch and re-raise exception from here, clears out pyparsing internal stack trace\n                raise exc\n        else:\n            return tokens\n\n    def scanString( self, instring, maxMatches=_MAX_INT, overlap=False ):\n        \"\"\"\n        Scan the input string for expression matches.  Each match will return the\n        matching tokens, start location, and end location.  May be called with optional\n        C{maxMatches} argument, to clip scanning after 'n' matches are found.  If\n        C{overlap} is specified, then overlapping matches will be reported.\n\n        Note that the start and end locations are reported relative to the string\n        being parsed.  See L{I{parseString}<parseString>} for more information on parsing\n        strings with embedded tabs.\n\n        Example::\n            source = \"sldjf123lsdjjkf345sldkjf879lkjsfd987\"\n            print(source)\n            for tokens,start,end in Word(alphas).scanString(source):\n                print(' '*start + '^'*(end-start))\n                print(' '*start + tokens[0])\n        \n        prints::\n        \n            sldjf123lsdjjkf345sldkjf879lkjsfd987\n            ^^^^^\n            sldjf\n                    ^^^^^^^\n                    lsdjjkf\n                              ^^^^^^\n                              sldkjf\n                                       ^^^^^^\n                                       lkjsfd\n        \"\"\"\n        if not self.streamlined:\n            self.streamline()\n        for e in self.ignoreExprs:\n            e.streamline()\n\n        if not self.keepTabs:\n            instring = _ustr(instring).expandtabs()\n        instrlen = len(instring)\n        loc = 0\n        preparseFn = self.preParse\n        parseFn = self._parse\n        ParserElement.resetCache()\n        matches = 0\n        try:\n            while loc <= instrlen and matches < maxMatches:\n                try:\n                    preloc = preparseFn( instring, loc )\n                    nextLoc,tokens = parseFn( instring, preloc, callPreParse=False )\n                except ParseException:\n                    loc = preloc+1\n                else:\n                    if nextLoc > loc:\n                        matches += 1\n                        yield tokens, preloc, nextLoc\n                        if overlap:\n                            nextloc = preparseFn( instring, loc )\n                            if nextloc > loc:\n                                loc = nextLoc\n                            else:\n                                loc += 1\n                        else:\n                            loc = nextLoc\n                    else:\n                        loc = preloc+1\n        except ParseBaseException as exc:\n            if ParserElement.verbose_stacktrace:\n                raise\n            else:\n                # catch and re-raise exception from here, clears out pyparsing internal stack trace\n                raise exc\n\n    def transformString( self, instring ):\n        \"\"\"\n        Extension to C{L{scanString}}, to modify matching text with modified tokens that may\n        be returned from a parse action.  To use C{transformString}, define a grammar and\n        attach a parse action to it that modifies the returned token list.\n        Invoking C{transformString()} on a target string will then scan for matches,\n        and replace the matched text patterns according to the logic in the parse\n        action.  C{transformString()} returns the resulting transformed string.\n        \n        Example::\n            wd = Word(alphas)\n            wd.setParseAction(lambda toks: toks[0].title())\n            \n            print(wd.transformString(\"now is the winter of our discontent made glorious summer by this sun of york.\"))\n        Prints::\n            Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York.\n        \"\"\"\n        out = []\n        lastE = 0\n        # force preservation of <TAB>s, to minimize unwanted transformation of string, and to\n        # keep string locs straight between transformString and scanString\n        self.keepTabs = True\n        try:\n            for t,s,e in self.scanString( instring ):\n                out.append( instring[lastE:s] )\n                if t:\n                    if isinstance(t,ParseResults):\n                        out += t.asList()\n                    elif isinstance(t,list):\n                        out += t\n                    else:\n                        out.append(t)\n                lastE = e\n            out.append(instring[lastE:])\n            out = [o for o in out if o]\n            return \"\".join(map(_ustr,_flatten(out)))\n        except ParseBaseException as exc:\n            if ParserElement.verbose_stacktrace:\n                raise\n            else:\n                # catch and re-raise exception from here, clears out pyparsing internal stack trace\n                raise exc\n\n    def searchString( self, instring, maxMatches=_MAX_INT ):\n        \"\"\"\n        Another extension to C{L{scanString}}, simplifying the access to the tokens found\n        to match the given parse expression.  May be called with optional\n        C{maxMatches} argument, to clip searching after 'n' matches are found.\n        \n        Example::\n            # a capitalized word starts with an uppercase letter, followed by zero or more lowercase letters\n            cap_word = Word(alphas.upper(), alphas.lower())\n            \n            print(cap_word.searchString(\"More than Iron, more than Lead, more than Gold I need Electricity\"))\n\n            # the sum() builtin can be used to merge results into a single ParseResults object\n            print(sum(cap_word.searchString(\"More than Iron, more than Lead, more than Gold I need Electricity\")))\n        prints::\n            [['More'], ['Iron'], ['Lead'], ['Gold'], ['I'], ['Electricity']]\n            ['More', 'Iron', 'Lead', 'Gold', 'I', 'Electricity']\n        \"\"\"\n        try:\n            return ParseResults([ t for t,s,e in self.scanString( instring, maxMatches ) ])\n        except ParseBaseException as exc:\n            if ParserElement.verbose_stacktrace:\n                raise\n            else:\n                # catch and re-raise exception from here, clears out pyparsing internal stack trace\n                raise exc\n\n    def split(self, instring, maxsplit=_MAX_INT, includeSeparators=False):\n        \"\"\"\n        Generator method to split a string using the given expression as a separator.\n        May be called with optional C{maxsplit} argument, to limit the number of splits;\n        and the optional C{includeSeparators} argument (default=C{False}), if the separating\n        matching text should be included in the split results.\n        \n        Example::        \n            punc = oneOf(list(\".,;:/-!?\"))\n            print(list(punc.split(\"This, this?, this sentence, is badly punctuated!\")))\n        prints::\n            ['This', ' this', '', ' this sentence', ' is badly punctuated', '']\n        \"\"\"\n        splits = 0\n        last = 0\n        for t,s,e in self.scanString(instring, maxMatches=maxsplit):\n            yield instring[last:s]\n            if includeSeparators:\n                yield t[0]\n            last = e\n        yield instring[last:]\n\n    def __add__(self, other ):\n        \"\"\"\n        Implementation of + operator - returns C{L{And}}. Adding strings to a ParserElement\n        converts them to L{Literal}s by default.\n        \n        Example::\n            greet = Word(alphas) + \",\" + Word(alphas) + \"!\"\n            hello = \"Hello, World!\"\n            print (hello, \"->\", greet.parseString(hello))\n        Prints::\n            Hello, World! -> ['Hello', ',', 'World', '!']\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return And( [ self, other ] )\n\n    def __radd__(self, other ):\n        \"\"\"\n        Implementation of + operator when left operand is not a C{L{ParserElement}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return other + self\n\n    def __sub__(self, other):\n        \"\"\"\n        Implementation of - operator, returns C{L{And}} with error stop\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return self + And._ErrorStop() + other\n\n    def __rsub__(self, other ):\n        \"\"\"\n        Implementation of - operator when left operand is not a C{L{ParserElement}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return other - self\n\n    def __mul__(self,other):\n        \"\"\"\n        Implementation of * operator, allows use of C{expr * 3} in place of\n        C{expr + expr + expr}.  Expressions may also me multiplied by a 2-integer\n        tuple, similar to C{{min,max}} multipliers in regular expressions.  Tuples\n        may also include C{None} as in:\n         - C{expr*(n,None)} or C{expr*(n,)} is equivalent\n              to C{expr*n + L{ZeroOrMore}(expr)}\n              (read as \"at least n instances of C{expr}\")\n         - C{expr*(None,n)} is equivalent to C{expr*(0,n)}\n              (read as \"0 to n instances of C{expr}\")\n         - C{expr*(None,None)} is equivalent to C{L{ZeroOrMore}(expr)}\n         - C{expr*(1,None)} is equivalent to C{L{OneOrMore}(expr)}\n\n        Note that C{expr*(None,n)} does not raise an exception if\n        more than n exprs exist in the input stream; that is,\n        C{expr*(None,n)} does not enforce a maximum number of expr\n        occurrences.  If this behavior is desired, then write\n        C{expr*(None,n) + ~expr}\n        \"\"\"\n        if isinstance(other,int):\n            minElements, optElements = other,0\n        elif isinstance(other,tuple):\n            other = (other + (None, None))[:2]\n            if other[0] is None:\n                other = (0, other[1])\n            if isinstance(other[0],int) and other[1] is None:\n                if other[0] == 0:\n                    return ZeroOrMore(self)\n                if other[0] == 1:\n                    return OneOrMore(self)\n                else:\n                    return self*other[0] + ZeroOrMore(self)\n            elif isinstance(other[0],int) and isinstance(other[1],int):\n                minElements, optElements = other\n                optElements -= minElements\n            else:\n                raise TypeError(\"cannot multiply 'ParserElement' and ('%s','%s') objects\", type(other[0]),type(other[1]))\n        else:\n            raise TypeError(\"cannot multiply 'ParserElement' and '%s' objects\", type(other))\n\n        if minElements < 0:\n            raise ValueError(\"cannot multiply ParserElement by negative value\")\n        if optElements < 0:\n            raise ValueError(\"second tuple value must be greater or equal to first tuple value\")\n        if minElements == optElements == 0:\n            raise ValueError(\"cannot multiply ParserElement by 0 or (0,0)\")\n\n        if (optElements):\n            def makeOptionalList(n):\n                if n>1:\n                    return Optional(self + makeOptionalList(n-1))\n                else:\n                    return Optional(self)\n            if minElements:\n                if minElements == 1:\n                    ret = self + makeOptionalList(optElements)\n                else:\n                    ret = And([self]*minElements) + makeOptionalList(optElements)\n            else:\n                ret = makeOptionalList(optElements)\n        else:\n            if minElements == 1:\n                ret = self\n            else:\n                ret = And([self]*minElements)\n        return ret\n\n    def __rmul__(self, other):\n        return self.__mul__(other)\n\n    def __or__(self, other ):\n        \"\"\"\n        Implementation of | operator - returns C{L{MatchFirst}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return MatchFirst( [ self, other ] )\n\n    def __ror__(self, other ):\n        \"\"\"\n        Implementation of | operator when left operand is not a C{L{ParserElement}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return other | self\n\n    def __xor__(self, other ):\n        \"\"\"\n        Implementation of ^ operator - returns C{L{Or}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return Or( [ self, other ] )\n\n    def __rxor__(self, other ):\n        \"\"\"\n        Implementation of ^ operator when left operand is not a C{L{ParserElement}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return other ^ self\n\n    def __and__(self, other ):\n        \"\"\"\n        Implementation of & operator - returns C{L{Each}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return Each( [ self, other ] )\n\n    def __rand__(self, other ):\n        \"\"\"\n        Implementation of & operator when left operand is not a C{L{ParserElement}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return other & self\n\n    def __invert__( self ):\n        \"\"\"\n        Implementation of ~ operator - returns C{L{NotAny}}\n        \"\"\"\n        return NotAny( self )\n\n    def __call__(self, name=None):\n        \"\"\"\n        Shortcut for C{L{setResultsName}}, with C{listAllMatches=False}.\n        \n        If C{name} is given with a trailing C{'*'} character, then C{listAllMatches} will be\n        passed as C{True}.\n           \n        If C{name} is omitted, same as calling C{L{copy}}.\n\n        Example::\n            # these are equivalent\n            userdata = Word(alphas).setResultsName(\"name\") + Word(nums+\"-\").setResultsName(\"socsecno\")\n            userdata = Word(alphas)(\"name\") + Word(nums+\"-\")(\"socsecno\")             \n        \"\"\"\n        if name is not None:\n            return self.setResultsName(name)\n        else:\n            return self.copy()\n\n    def suppress( self ):\n        \"\"\"\n        Suppresses the output of this C{ParserElement}; useful to keep punctuation from\n        cluttering up returned output.\n        \"\"\"\n        return Suppress( self )\n\n    def leaveWhitespace( self ):\n        \"\"\"\n        Disables the skipping of whitespace before matching the characters in the\n        C{ParserElement}'s defined pattern.  This is normally only used internally by\n        the pyparsing module, but may be needed in some whitespace-sensitive grammars.\n        \"\"\"\n        self.skipWhitespace = False\n        return self\n\n    def setWhitespaceChars( self, chars ):\n        \"\"\"\n        Overrides the default whitespace chars\n        \"\"\"\n        self.skipWhitespace = True\n        self.whiteChars = chars\n        self.copyDefaultWhiteChars = False\n        return self\n\n    def parseWithTabs( self ):\n        \"\"\"\n        Overrides default behavior to expand C{<TAB>}s to spaces before parsing the input string.\n        Must be called before C{parseString} when the input grammar contains elements that\n        match C{<TAB>} characters.\n        \"\"\"\n        self.keepTabs = True\n        return self\n\n    def ignore( self, other ):\n        \"\"\"\n        Define expression to be ignored (e.g., comments) while doing pattern\n        matching; may be called repeatedly, to define multiple comment or other\n        ignorable patterns.\n        \n        Example::\n            patt = OneOrMore(Word(alphas))\n            patt.parseString('ablaj /* comment */ lskjd') # -> ['ablaj']\n            \n            patt.ignore(cStyleComment)\n            patt.parseString('ablaj /* comment */ lskjd') # -> ['ablaj', 'lskjd']\n        \"\"\"\n        if isinstance(other, basestring):\n            other = Suppress(other)\n\n        if isinstance( other, Suppress ):\n            if other not in self.ignoreExprs:\n                self.ignoreExprs.append(other)\n        else:\n            self.ignoreExprs.append( Suppress( other.copy() ) )\n        return self\n\n    def setDebugActions( self, startAction, successAction, exceptionAction ):\n        \"\"\"\n        Enable display of debugging messages while doing pattern matching.\n        \"\"\"\n        self.debugActions = (startAction or _defaultStartDebugAction,\n                             successAction or _defaultSuccessDebugAction,\n                             exceptionAction or _defaultExceptionDebugAction)\n        self.debug = True\n        return self\n\n    def setDebug( self, flag=True ):\n        \"\"\"\n        Enable display of debugging messages while doing pattern matching.\n        Set C{flag} to True to enable, False to disable.\n\n        Example::\n            wd = Word(alphas).setName(\"alphaword\")\n            integer = Word(nums).setName(\"numword\")\n            term = wd | integer\n            \n            # turn on debugging for wd\n            wd.setDebug()\n\n            OneOrMore(term).parseString(\"abc 123 xyz 890\")\n        \n        prints::\n            Match alphaword at loc 0(1,1)\n            Matched alphaword -> ['abc']\n            Match alphaword at loc 3(1,4)\n            Exception raised:Expected alphaword (at char 4), (line:1, col:5)\n            Match alphaword at loc 7(1,8)\n            Matched alphaword -> ['xyz']\n            Match alphaword at loc 11(1,12)\n            Exception raised:Expected alphaword (at char 12), (line:1, col:13)\n            Match alphaword at loc 15(1,16)\n            Exception raised:Expected alphaword (at char 15), (line:1, col:16)\n\n        The output shown is that produced by the default debug actions - custom debug actions can be\n        specified using L{setDebugActions}. Prior to attempting\n        to match the C{wd} expression, the debugging message C{\"Match <exprname> at loc <n>(<line>,<col>)\"}\n        is shown. Then if the parse succeeds, a C{\"Matched\"} message is shown, or an C{\"Exception raised\"}\n        message is shown. Also note the use of L{setName} to assign a human-readable name to the expression,\n        which makes debugging and exception messages easier to understand - for instance, the default\n        name created for the C{Word} expression without calling C{setName} is C{\"W:(ABCD...)\"}.\n        \"\"\"\n        if flag:\n            self.setDebugActions( _defaultStartDebugAction, _defaultSuccessDebugAction, _defaultExceptionDebugAction )\n        else:\n            self.debug = False\n        return self\n\n    def __str__( self ):\n        return self.name\n\n    def __repr__( self ):\n        return _ustr(self)\n\n    def streamline( self ):\n        self.streamlined = True\n        self.strRepr = None\n        return self\n\n    def checkRecursion( self, parseElementList ):\n        pass\n\n    def validate( self, validateTrace=[] ):\n        \"\"\"\n        Check defined expressions for valid structure, check for infinite recursive definitions.\n        \"\"\"\n        self.checkRecursion( [] )\n\n    def parseFile( self, file_or_filename, parseAll=False ):\n        \"\"\"\n        Execute the parse expression on the given file or filename.\n        If a filename is specified (instead of a file object),\n        the entire file is opened, read, and closed before parsing.\n        \"\"\"\n        try:\n            file_contents = file_or_filename.read()\n        except AttributeError:\n            with open(file_or_filename, \"r\") as f:\n                file_contents = f.read()\n        try:\n            return self.parseString(file_contents, parseAll)\n        except ParseBaseException as exc:\n            if ParserElement.verbose_stacktrace:\n                raise\n            else:\n                # catch and re-raise exception from here, clears out pyparsing internal stack trace\n                raise exc\n\n    def __eq__(self,other):\n        if isinstance(other, ParserElement):\n            return self is other or vars(self) == vars(other)\n        elif isinstance(other, basestring):\n            return self.matches(other)\n        else:\n            return super(ParserElement,self)==other\n\n    def __ne__(self,other):\n        return not (self == other)\n\n    def __hash__(self):\n        return hash(id(self))\n\n    def __req__(self,other):\n        return self == other\n\n    def __rne__(self,other):\n        return not (self == other)\n\n    def matches(self, testString, parseAll=True):\n        \"\"\"\n        Method for quick testing of a parser against a test string. Good for simple \n        inline microtests of sub expressions while building up larger parser.\n           \n        Parameters:\n         - testString - to test against this expression for a match\n         - parseAll - (default=C{True}) - flag to pass to C{L{parseString}} when running tests\n            \n        Example::\n            expr = Word(nums)\n            assert expr.matches(\"100\")\n        \"\"\"\n        try:\n            self.parseString(_ustr(testString), parseAll=parseAll)\n            return True\n        except ParseBaseException:\n            return False\n                \n    def runTests(self, tests, parseAll=True, comment='#', fullDump=True, printResults=True, failureTests=False):\n        \"\"\"\n        Execute the parse expression on a series of test strings, showing each\n        test, the parsed results or where the parse failed. Quick and easy way to\n        run a parse expression against a list of sample strings.\n           \n        Parameters:\n         - tests - a list of separate test strings, or a multiline string of test strings\n         - parseAll - (default=C{True}) - flag to pass to C{L{parseString}} when running tests           \n         - comment - (default=C{'#'}) - expression for indicating embedded comments in the test \n              string; pass None to disable comment filtering\n         - fullDump - (default=C{True}) - dump results as list followed by results names in nested outline;\n              if False, only dump nested list\n         - printResults - (default=C{True}) prints test output to stdout\n         - failureTests - (default=C{False}) indicates if these tests are expected to fail parsing\n\n        Returns: a (success, results) tuple, where success indicates that all tests succeeded\n        (or failed if C{failureTests} is True), and the results contain a list of lines of each \n        test's output\n        \n        Example::\n            number_expr = pyparsing_common.number.copy()\n\n            result = number_expr.runTests('''\n                # unsigned integer\n                100\n                # negative integer\n                -100\n                # float with scientific notation\n                6.02e23\n                # integer with scientific notation\n                1e-12\n                ''')\n            print(\"Success\" if result[0] else \"Failed!\")\n\n            result = number_expr.runTests('''\n                # stray character\n                100Z\n                # missing leading digit before '.'\n                -.100\n                # too many '.'\n                3.14.159\n                ''', failureTests=True)\n            print(\"Success\" if result[0] else \"Failed!\")\n        prints::\n            # unsigned integer\n            100\n            [100]\n\n            # negative integer\n            -100\n            [-100]\n\n            # float with scientific notation\n            6.02e23\n            [6.02e+23]\n\n            # integer with scientific notation\n            1e-12\n            [1e-12]\n\n            Success\n            \n            # stray character\n            100Z\n               ^\n            FAIL: Expected end of text (at char 3), (line:1, col:4)\n\n            # missing leading digit before '.'\n            -.100\n            ^\n            FAIL: Expected {real number with scientific notation | real number | signed integer} (at char 0), (line:1, col:1)\n\n            # too many '.'\n            3.14.159\n                ^\n            FAIL: Expected end of text (at char 4), (line:1, col:5)\n\n            Success\n\n        Each test string must be on a single line. If you want to test a string that spans multiple\n        lines, create a test like this::\n\n            expr.runTest(r\"this is a test\\\\n of strings that spans \\\\n 3 lines\")\n        \n        (Note that this is a raw string literal, you must include the leading 'r'.)\n        \"\"\"\n        if isinstance(tests, basestring):\n            tests = list(map(str.strip, tests.rstrip().splitlines()))\n        if isinstance(comment, basestring):\n            comment = Literal(comment)\n        allResults = []\n        comments = []\n        success = True\n        for t in tests:\n            if comment is not None and comment.matches(t, False) or comments and not t:\n                comments.append(t)\n                continue\n            if not t:\n                continue\n            out = ['\\n'.join(comments), t]\n            comments = []\n            try:\n                t = t.replace(r'\\n','\\n')\n                result = self.parseString(t, parseAll=parseAll)\n                out.append(result.dump(full=fullDump))\n                success = success and not failureTests\n            except ParseBaseException as pe:\n                fatal = \"(FATAL)\" if isinstance(pe, ParseFatalException) else \"\"\n                if '\\n' in t:\n                    out.append(line(pe.loc, t))\n                    out.append(' '*(col(pe.loc,t)-1) + '^' + fatal)\n                else:\n                    out.append(' '*pe.loc + '^' + fatal)\n                out.append(\"FAIL: \" + str(pe))\n                success = success and failureTests\n                result = pe\n            except Exception as exc:\n                out.append(\"FAIL-EXCEPTION: \" + str(exc))\n                success = success and failureTests\n                result = exc\n\n            if printResults:\n                if fullDump:\n                    out.append('')\n                print('\\n'.join(out))\n\n            allResults.append((t, result))\n        \n        return success, allResults\n\n        \nclass Token(ParserElement):\n    \"\"\"\n    Abstract C{ParserElement} subclass, for defining atomic matching patterns.\n    \"\"\"\n    def __init__( self ):\n        super(Token,self).__init__( savelist=False )\n\n\nclass Empty(Token):\n    \"\"\"\n    An empty token, will always match.\n    \"\"\"\n    def __init__( self ):\n        super(Empty,self).__init__()\n        self.name = \"Empty\"\n        self.mayReturnEmpty = True\n        self.mayIndexError = False\n\n\nclass NoMatch(Token):\n    \"\"\"\n    A token that will never match.\n    \"\"\"\n    def __init__( self ):\n        super(NoMatch,self).__init__()\n        self.name = \"NoMatch\"\n        self.mayReturnEmpty = True\n        self.mayIndexError = False\n        self.errmsg = \"Unmatchable token\"\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        raise ParseException(instring, loc, self.errmsg, self)\n\n\nclass Literal(Token):\n    \"\"\"\n    Token to exactly match a specified string.\n    \n    Example::\n        Literal('blah').parseString('blah')  # -> ['blah']\n        Literal('blah').parseString('blahfooblah')  # -> ['blah']\n        Literal('blah').parseString('bla')  # -> Exception: Expected \"blah\"\n    \n    For case-insensitive matching, use L{CaselessLiteral}.\n    \n    For keyword matching (force word break before and after the matched string),\n    use L{Keyword} or L{CaselessKeyword}.\n    \"\"\"\n    def __init__( self, matchString ):\n        super(Literal,self).__init__()\n        self.match = matchString\n        self.matchLen = len(matchString)\n        try:\n            self.firstMatchChar = matchString[0]\n        except IndexError:\n            warnings.warn(\"null string passed to Literal; use Empty() instead\",\n                            SyntaxWarning, stacklevel=2)\n            self.__class__ = Empty\n        self.name = '\"%s\"' % _ustr(self.match)\n        self.errmsg = \"Expected \" + self.name\n        self.mayReturnEmpty = False\n        self.mayIndexError = False\n\n    # Performance tuning: this routine gets called a *lot*\n    # if this is a single character match string  and the first character matches,\n    # short-circuit as quickly as possible, and avoid calling startswith\n    #~ @profile\n    def parseImpl( self, instring, loc, doActions=True ):\n        if (instring[loc] == self.firstMatchChar and\n            (self.matchLen==1 or instring.startswith(self.match,loc)) ):\n            return loc+self.matchLen, self.match\n        raise ParseException(instring, loc, self.errmsg, self)\n_L = Literal\nParserElement._literalStringClass = Literal\n\nclass Keyword(Token):\n    \"\"\"\n    Token to exactly match a specified string as a keyword, that is, it must be\n    immediately followed by a non-keyword character.  Compare with C{L{Literal}}:\n     - C{Literal(\"if\")} will match the leading C{'if'} in C{'ifAndOnlyIf'}.\n     - C{Keyword(\"if\")} will not; it will only match the leading C{'if'} in C{'if x=1'}, or C{'if(y==2)'}\n    Accepts two optional constructor arguments in addition to the keyword string:\n     - C{identChars} is a string of characters that would be valid identifier characters,\n          defaulting to all alphanumerics + \"_\" and \"$\"\n     - C{caseless} allows case-insensitive matching, default is C{False}.\n       \n    Example::\n        Keyword(\"start\").parseString(\"start\")  # -> ['start']\n        Keyword(\"start\").parseString(\"starting\")  # -> Exception\n\n    For case-insensitive matching, use L{CaselessKeyword}.\n    \"\"\"\n    DEFAULT_KEYWORD_CHARS = alphanums+\"_$\"\n\n    def __init__( self, matchString, identChars=None, caseless=False ):\n        super(Keyword,self).__init__()\n        if identChars is None:\n            identChars = Keyword.DEFAULT_KEYWORD_CHARS\n        self.match = matchString\n        self.matchLen = len(matchString)\n        try:\n            self.firstMatchChar = matchString[0]\n        except IndexError:\n            warnings.warn(\"null string passed to Keyword; use Empty() instead\",\n                            SyntaxWarning, stacklevel=2)\n        self.name = '\"%s\"' % self.match\n        self.errmsg = \"Expected \" + self.name\n        self.mayReturnEmpty = False\n        self.mayIndexError = False\n        self.caseless = caseless\n        if caseless:\n            self.caselessmatch = matchString.upper()\n            identChars = identChars.upper()\n        self.identChars = set(identChars)\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if self.caseless:\n            if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and\n                 (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) and\n                 (loc == 0 or instring[loc-1].upper() not in self.identChars) ):\n                return loc+self.matchLen, self.match\n        else:\n            if (instring[loc] == self.firstMatchChar and\n                (self.matchLen==1 or instring.startswith(self.match,loc)) and\n                (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen] not in self.identChars) and\n                (loc == 0 or instring[loc-1] not in self.identChars) ):\n                return loc+self.matchLen, self.match\n        raise ParseException(instring, loc, self.errmsg, self)\n\n    def copy(self):\n        c = super(Keyword,self).copy()\n        c.identChars = Keyword.DEFAULT_KEYWORD_CHARS\n        return c\n\n    @staticmethod\n    def setDefaultKeywordChars( chars ):\n        \"\"\"Overrides the default Keyword chars\n        \"\"\"\n        Keyword.DEFAULT_KEYWORD_CHARS = chars\n\nclass CaselessLiteral(Literal):\n    \"\"\"\n    Token to match a specified string, ignoring case of letters.\n    Note: the matched results will always be in the case of the given\n    match string, NOT the case of the input text.\n\n    Example::\n        OneOrMore(CaselessLiteral(\"CMD\")).parseString(\"cmd CMD Cmd10\") # -> ['CMD', 'CMD', 'CMD']\n        \n    (Contrast with example for L{CaselessKeyword}.)\n    \"\"\"\n    def __init__( self, matchString ):\n        super(CaselessLiteral,self).__init__( matchString.upper() )\n        # Preserve the defining literal.\n        self.returnString = matchString\n        self.name = \"'%s'\" % self.returnString\n        self.errmsg = \"Expected \" + self.name\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if instring[ loc:loc+self.matchLen ].upper() == self.match:\n            return loc+self.matchLen, self.returnString\n        raise ParseException(instring, loc, self.errmsg, self)\n\nclass CaselessKeyword(Keyword):\n    \"\"\"\n    Caseless version of L{Keyword}.\n\n    Example::\n        OneOrMore(CaselessKeyword(\"CMD\")).parseString(\"cmd CMD Cmd10\") # -> ['CMD', 'CMD']\n        \n    (Contrast with example for L{CaselessLiteral}.)\n    \"\"\"\n    def __init__( self, matchString, identChars=None ):\n        super(CaselessKeyword,self).__init__( matchString, identChars, caseless=True )\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and\n             (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) ):\n            return loc+self.matchLen, self.match\n        raise ParseException(instring, loc, self.errmsg, self)\n\nclass CloseMatch(Token):\n    \"\"\"\n    A variation on L{Literal} which matches \"close\" matches, that is, \n    strings with at most 'n' mismatching characters. C{CloseMatch} takes parameters:\n     - C{match_string} - string to be matched\n     - C{maxMismatches} - (C{default=1}) maximum number of mismatches allowed to count as a match\n    \n    The results from a successful parse will contain the matched text from the input string and the following named results:\n     - C{mismatches} - a list of the positions within the match_string where mismatches were found\n     - C{original} - the original match_string used to compare against the input string\n    \n    If C{mismatches} is an empty list, then the match was an exact match.\n    \n    Example::\n        patt = CloseMatch(\"ATCATCGAATGGA\")\n        patt.parseString(\"ATCATCGAAXGGA\") # -> (['ATCATCGAAXGGA'], {'mismatches': [[9]], 'original': ['ATCATCGAATGGA']})\n        patt.parseString(\"ATCAXCGAAXGGA\") # -> Exception: Expected 'ATCATCGAATGGA' (with up to 1 mismatches) (at char 0), (line:1, col:1)\n\n        # exact match\n        patt.parseString(\"ATCATCGAATGGA\") # -> (['ATCATCGAATGGA'], {'mismatches': [[]], 'original': ['ATCATCGAATGGA']})\n\n        # close match allowing up to 2 mismatches\n        patt = CloseMatch(\"ATCATCGAATGGA\", maxMismatches=2)\n        patt.parseString(\"ATCAXCGAAXGGA\") # -> (['ATCAXCGAAXGGA'], {'mismatches': [[4, 9]], 'original': ['ATCATCGAATGGA']})\n    \"\"\"\n    def __init__(self, match_string, maxMismatches=1):\n        super(CloseMatch,self).__init__()\n        self.name = match_string\n        self.match_string = match_string\n        self.maxMismatches = maxMismatches\n        self.errmsg = \"Expected %r (with up to %d mismatches)\" % (self.match_string, self.maxMismatches)\n        self.mayIndexError = False\n        self.mayReturnEmpty = False\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        start = loc\n        instrlen = len(instring)\n        maxloc = start + len(self.match_string)\n\n        if maxloc <= instrlen:\n            match_string = self.match_string\n            match_stringloc = 0\n            mismatches = []\n            maxMismatches = self.maxMismatches\n\n            for match_stringloc,s_m in enumerate(zip(instring[loc:maxloc], self.match_string)):\n                src,mat = s_m\n                if src != mat:\n                    mismatches.append(match_stringloc)\n                    if len(mismatches) > maxMismatches:\n                        break\n            else:\n                loc = match_stringloc + 1\n                results = ParseResults([instring[start:loc]])\n                results['original'] = self.match_string\n                results['mismatches'] = mismatches\n                return loc, results\n\n        raise ParseException(instring, loc, self.errmsg, self)\n\n\nclass Word(Token):\n    \"\"\"\n    Token for matching words composed of allowed character sets.\n    Defined with string containing all allowed initial characters,\n    an optional string containing allowed body characters (if omitted,\n    defaults to the initial character set), and an optional minimum,\n    maximum, and/or exact length.  The default value for C{min} is 1 (a\n    minimum value < 1 is not valid); the default values for C{max} and C{exact}\n    are 0, meaning no maximum or exact length restriction. An optional\n    C{excludeChars} parameter can list characters that might be found in \n    the input C{bodyChars} string; useful to define a word of all printables\n    except for one or two characters, for instance.\n    \n    L{srange} is useful for defining custom character set strings for defining \n    C{Word} expressions, using range notation from regular expression character sets.\n    \n    A common mistake is to use C{Word} to match a specific literal string, as in \n    C{Word(\"Address\")}. Remember that C{Word} uses the string argument to define\n    I{sets} of matchable characters. This expression would match \"Add\", \"AAA\",\n    \"dAred\", or any other word made up of the characters 'A', 'd', 'r', 'e', and 's'.\n    To match an exact literal string, use L{Literal} or L{Keyword}.\n\n    pyparsing includes helper strings for building Words:\n     - L{alphas}\n     - L{nums}\n     - L{alphanums}\n     - L{hexnums}\n     - L{alphas8bit} (alphabetic characters in ASCII range 128-255 - accented, tilded, umlauted, etc.)\n     - L{punc8bit} (non-alphabetic characters in ASCII range 128-255 - currency, symbols, superscripts, diacriticals, etc.)\n     - L{printables} (any non-whitespace character)\n\n    Example::\n        # a word composed of digits\n        integer = Word(nums) # equivalent to Word(\"0123456789\") or Word(srange(\"0-9\"))\n        \n        # a word with a leading capital, and zero or more lowercase\n        capital_word = Word(alphas.upper(), alphas.lower())\n\n        # hostnames are alphanumeric, with leading alpha, and '-'\n        hostname = Word(alphas, alphanums+'-')\n        \n        # roman numeral (not a strict parser, accepts invalid mix of characters)\n        roman = Word(\"IVXLCDM\")\n        \n        # any string of non-whitespace characters, except for ','\n        csv_value = Word(printables, excludeChars=\",\")\n    \"\"\"\n    def __init__( self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword=False, excludeChars=None ):\n        super(Word,self).__init__()\n        if excludeChars:\n            initChars = ''.join(c for c in initChars if c not in excludeChars)\n            if bodyChars:\n                bodyChars = ''.join(c for c in bodyChars if c not in excludeChars)\n        self.initCharsOrig = initChars\n        self.initChars = set(initChars)\n        if bodyChars :\n            self.bodyCharsOrig = bodyChars\n            self.bodyChars = set(bodyChars)\n        else:\n            self.bodyCharsOrig = initChars\n            self.bodyChars = set(initChars)\n\n        self.maxSpecified = max > 0\n\n        if min < 1:\n            raise ValueError(\"cannot specify a minimum length < 1; use Optional(Word()) if zero-length word is permitted\")\n\n        self.minLen = min\n\n        if max > 0:\n            self.maxLen = max\n        else:\n            self.maxLen = _MAX_INT\n\n        if exact > 0:\n            self.maxLen = exact\n            self.minLen = exact\n\n        self.name = _ustr(self)\n        self.errmsg = \"Expected \" + self.name\n        self.mayIndexError = False\n        self.asKeyword = asKeyword\n\n        if ' ' not in self.initCharsOrig+self.bodyCharsOrig and (min==1 and max==0 and exact==0):\n            if self.bodyCharsOrig == self.initCharsOrig:\n                self.reString = \"[%s]+\" % _escapeRegexRangeChars(self.initCharsOrig)\n            elif len(self.initCharsOrig) == 1:\n                self.reString = \"%s[%s]*\" % \\\n                                      (re.escape(self.initCharsOrig),\n                                      _escapeRegexRangeChars(self.bodyCharsOrig),)\n            else:\n                self.reString = \"[%s][%s]*\" % \\\n                                      (_escapeRegexRangeChars(self.initCharsOrig),\n                                      _escapeRegexRangeChars(self.bodyCharsOrig),)\n            if self.asKeyword:\n                self.reString = r\"\\b\"+self.reString+r\"\\b\"\n            try:\n                self.re = re.compile( self.reString )\n            except Exception:\n                self.re = None\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if self.re:\n            result = self.re.match(instring,loc)\n            if not result:\n                raise ParseException(instring, loc, self.errmsg, self)\n\n            loc = result.end()\n            return loc, result.group()\n\n        if not(instring[ loc ] in self.initChars):\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        start = loc\n        loc += 1\n        instrlen = len(instring)\n        bodychars = self.bodyChars\n        maxloc = start + self.maxLen\n        maxloc = min( maxloc, instrlen )\n        while loc < maxloc and instring[loc] in bodychars:\n            loc += 1\n\n        throwException = False\n        if loc - start < self.minLen:\n            throwException = True\n        if self.maxSpecified and loc < instrlen and instring[loc] in bodychars:\n            throwException = True\n        if self.asKeyword:\n            if (start>0 and instring[start-1] in bodychars) or (loc<instrlen and instring[loc] in bodychars):\n                throwException = True\n\n        if throwException:\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        return loc, instring[start:loc]\n\n    def __str__( self ):\n        try:\n            return super(Word,self).__str__()\n        except Exception:\n            pass\n\n\n        if self.strRepr is None:\n\n            def charsAsStr(s):\n                if len(s)>4:\n                    return s[:4]+\"...\"\n                else:\n                    return s\n\n            if ( self.initCharsOrig != self.bodyCharsOrig ):\n                self.strRepr = \"W:(%s,%s)\" % ( charsAsStr(self.initCharsOrig), charsAsStr(self.bodyCharsOrig) )\n            else:\n                self.strRepr = \"W:(%s)\" % charsAsStr(self.initCharsOrig)\n\n        return self.strRepr\n\n\nclass Regex(Token):\n    r\"\"\"\n    Token for matching strings that match a given regular expression.\n    Defined with string specifying the regular expression in a form recognized by the inbuilt Python re module.\n    If the given regex contains named groups (defined using C{(?P<name>...)}), these will be preserved as \n    named parse results.\n\n    Example::\n        realnum = Regex(r\"[+-]?\\d+\\.\\d*\")\n        date = Regex(r'(?P<year>\\d{4})-(?P<month>\\d\\d?)-(?P<day>\\d\\d?)')\n        # ref: http://stackoverflow.com/questions/267399/how-do-you-match-only-valid-roman-numerals-with-a-regular-expression\n        roman = Regex(r\"M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})\")\n    \"\"\"\n    compiledREtype = type(re.compile(\"[A-Z]\"))\n    def __init__( self, pattern, flags=0):\n        \"\"\"The parameters C{pattern} and C{flags} are passed to the C{re.compile()} function as-is. See the Python C{re} module for an explanation of the acceptable patterns and flags.\"\"\"\n        super(Regex,self).__init__()\n\n        if isinstance(pattern, basestring):\n            if not pattern:\n                warnings.warn(\"null string passed to Regex; use Empty() instead\",\n                        SyntaxWarning, stacklevel=2)\n\n            self.pattern = pattern\n            self.flags = flags\n\n            try:\n                self.re = re.compile(self.pattern, self.flags)\n                self.reString = self.pattern\n            except sre_constants.error:\n                warnings.warn(\"invalid pattern (%s) passed to Regex\" % pattern,\n                    SyntaxWarning, stacklevel=2)\n                raise\n\n        elif isinstance(pattern, Regex.compiledREtype):\n            self.re = pattern\n            self.pattern = \\\n            self.reString = str(pattern)\n            self.flags = flags\n            \n        else:\n            raise ValueError(\"Regex may only be constructed with a string or a compiled RE object\")\n\n        self.name = _ustr(self)\n        self.errmsg = \"Expected \" + self.name\n        self.mayIndexError = False\n        self.mayReturnEmpty = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        result = self.re.match(instring,loc)\n        if not result:\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        loc = result.end()\n        d = result.groupdict()\n        ret = ParseResults(result.group())\n        if d:\n            for k in d:\n                ret[k] = d[k]\n        return loc,ret\n\n    def __str__( self ):\n        try:\n            return super(Regex,self).__str__()\n        except Exception:\n            pass\n\n        if self.strRepr is None:\n            self.strRepr = \"Re:(%s)\" % repr(self.pattern)\n\n        return self.strRepr\n\n\nclass QuotedString(Token):\n    r\"\"\"\n    Token for matching strings that are delimited by quoting characters.\n    \n    Defined with the following parameters:\n        - quoteChar - string of one or more characters defining the quote delimiting string\n        - escChar - character to escape quotes, typically backslash (default=C{None})\n        - escQuote - special quote sequence to escape an embedded quote string (such as SQL's \"\" to escape an embedded \") (default=C{None})\n        - multiline - boolean indicating whether quotes can span multiple lines (default=C{False})\n        - unquoteResults - boolean indicating whether the matched text should be unquoted (default=C{True})\n        - endQuoteChar - string of one or more characters defining the end of the quote delimited string (default=C{None} => same as quoteChar)\n        - convertWhitespaceEscapes - convert escaped whitespace (C{'\\t'}, C{'\\n'}, etc.) to actual whitespace (default=C{True})\n\n    Example::\n        qs = QuotedString('\"')\n        print(qs.searchString('lsjdf \"This is the quote\" sldjf'))\n        complex_qs = QuotedString('{{', endQuoteChar='}}')\n        print(complex_qs.searchString('lsjdf {{This is the \"quote\"}} sldjf'))\n        sql_qs = QuotedString('\"', escQuote='\"\"')\n        print(sql_qs.searchString('lsjdf \"This is the quote with \"\"embedded\"\" quotes\" sldjf'))\n    prints::\n        [['This is the quote']]\n        [['This is the \"quote\"']]\n        [['This is the quote with \"embedded\" quotes']]\n    \"\"\"\n    def __init__( self, quoteChar, escChar=None, escQuote=None, multiline=False, unquoteResults=True, endQuoteChar=None, convertWhitespaceEscapes=True):\n        super(QuotedString,self).__init__()\n\n        # remove white space from quote chars - wont work anyway\n        quoteChar = quoteChar.strip()\n        if not quoteChar:\n            warnings.warn(\"quoteChar cannot be the empty string\",SyntaxWarning,stacklevel=2)\n            raise SyntaxError()\n\n        if endQuoteChar is None:\n            endQuoteChar = quoteChar\n        else:\n            endQuoteChar = endQuoteChar.strip()\n            if not endQuoteChar:\n                warnings.warn(\"endQuoteChar cannot be the empty string\",SyntaxWarning,stacklevel=2)\n                raise SyntaxError()\n\n        self.quoteChar = quoteChar\n        self.quoteCharLen = len(quoteChar)\n        self.firstQuoteChar = quoteChar[0]\n        self.endQuoteChar = endQuoteChar\n        self.endQuoteCharLen = len(endQuoteChar)\n        self.escChar = escChar\n        self.escQuote = escQuote\n        self.unquoteResults = unquoteResults\n        self.convertWhitespaceEscapes = convertWhitespaceEscapes\n\n        if multiline:\n            self.flags = re.MULTILINE | re.DOTALL\n            self.pattern = r'%s(?:[^%s%s]' % \\\n                ( re.escape(self.quoteChar),\n                  _escapeRegexRangeChars(self.endQuoteChar[0]),\n                  (escChar is not None and _escapeRegexRangeChars(escChar) or '') )\n        else:\n            self.flags = 0\n            self.pattern = r'%s(?:[^%s\\n\\r%s]' % \\\n                ( re.escape(self.quoteChar),\n                  _escapeRegexRangeChars(self.endQuoteChar[0]),\n                  (escChar is not None and _escapeRegexRangeChars(escChar) or '') )\n        if len(self.endQuoteChar) > 1:\n            self.pattern += (\n                '|(?:' + ')|(?:'.join(\"%s[^%s]\" % (re.escape(self.endQuoteChar[:i]),\n                                               _escapeRegexRangeChars(self.endQuoteChar[i]))\n                                    for i in range(len(self.endQuoteChar)-1,0,-1)) + ')'\n                )\n        if escQuote:\n            self.pattern += (r'|(?:%s)' % re.escape(escQuote))\n        if escChar:\n            self.pattern += (r'|(?:%s.)' % re.escape(escChar))\n            self.escCharReplacePattern = re.escape(self.escChar)+\"(.)\"\n        self.pattern += (r')*%s' % re.escape(self.endQuoteChar))\n\n        try:\n            self.re = re.compile(self.pattern, self.flags)\n            self.reString = self.pattern\n        except sre_constants.error:\n            warnings.warn(\"invalid pattern (%s) passed to Regex\" % self.pattern,\n                SyntaxWarning, stacklevel=2)\n            raise\n\n        self.name = _ustr(self)\n        self.errmsg = \"Expected \" + self.name\n        self.mayIndexError = False\n        self.mayReturnEmpty = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        result = instring[loc] == self.firstQuoteChar and self.re.match(instring,loc) or None\n        if not result:\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        loc = result.end()\n        ret = result.group()\n\n        if self.unquoteResults:\n\n            # strip off quotes\n            ret = ret[self.quoteCharLen:-self.endQuoteCharLen]\n\n            if isinstance(ret,basestring):\n                # replace escaped whitespace\n                if '\\\\' in ret and self.convertWhitespaceEscapes:\n                    ws_map = {\n                        r'\\t' : '\\t',\n                        r'\\n' : '\\n',\n                        r'\\f' : '\\f',\n                        r'\\r' : '\\r',\n                    }\n                    for wslit,wschar in ws_map.items():\n                        ret = ret.replace(wslit, wschar)\n\n                # replace escaped characters\n                if self.escChar:\n                    ret = re.sub(self.escCharReplacePattern, r\"\\g<1>\", ret)\n\n                # replace escaped quotes\n                if self.escQuote:\n                    ret = ret.replace(self.escQuote, self.endQuoteChar)\n\n        return loc, ret\n\n    def __str__( self ):\n        try:\n            return super(QuotedString,self).__str__()\n        except Exception:\n            pass\n\n        if self.strRepr is None:\n            self.strRepr = \"quoted string, starting with %s ending with %s\" % (self.quoteChar, self.endQuoteChar)\n\n        return self.strRepr\n\n\nclass CharsNotIn(Token):\n    \"\"\"\n    Token for matching words composed of characters I{not} in a given set (will\n    include whitespace in matched characters if not listed in the provided exclusion set - see example).\n    Defined with string containing all disallowed characters, and an optional\n    minimum, maximum, and/or exact length.  The default value for C{min} is 1 (a\n    minimum value < 1 is not valid); the default values for C{max} and C{exact}\n    are 0, meaning no maximum or exact length restriction.\n\n    Example::\n        # define a comma-separated-value as anything that is not a ','\n        csv_value = CharsNotIn(',')\n        print(delimitedList(csv_value).parseString(\"dkls,lsdkjf,s12 34,@!#,213\"))\n    prints::\n        ['dkls', 'lsdkjf', 's12 34', '@!#', '213']\n    \"\"\"\n    def __init__( self, notChars, min=1, max=0, exact=0 ):\n        super(CharsNotIn,self).__init__()\n        self.skipWhitespace = False\n        self.notChars = notChars\n\n        if min < 1:\n            raise ValueError(\"cannot specify a minimum length < 1; use Optional(CharsNotIn()) if zero-length char group is permitted\")\n\n        self.minLen = min\n\n        if max > 0:\n            self.maxLen = max\n        else:\n            self.maxLen = _MAX_INT\n\n        if exact > 0:\n            self.maxLen = exact\n            self.minLen = exact\n\n        self.name = _ustr(self)\n        self.errmsg = \"Expected \" + self.name\n        self.mayReturnEmpty = ( self.minLen == 0 )\n        self.mayIndexError = False\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if instring[loc] in self.notChars:\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        start = loc\n        loc += 1\n        notchars = self.notChars\n        maxlen = min( start+self.maxLen, len(instring) )\n        while loc < maxlen and \\\n              (instring[loc] not in notchars):\n            loc += 1\n\n        if loc - start < self.minLen:\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        return loc, instring[start:loc]\n\n    def __str__( self ):\n        try:\n            return super(CharsNotIn, self).__str__()\n        except Exception:\n            pass\n\n        if self.strRepr is None:\n            if len(self.notChars) > 4:\n                self.strRepr = \"!W:(%s...)\" % self.notChars[:4]\n            else:\n                self.strRepr = \"!W:(%s)\" % self.notChars\n\n        return self.strRepr\n\nclass White(Token):\n    \"\"\"\n    Special matching class for matching whitespace.  Normally, whitespace is ignored\n    by pyparsing grammars.  This class is included when some whitespace structures\n    are significant.  Define with a string containing the whitespace characters to be\n    matched; default is C{\" \\\\t\\\\r\\\\n\"}.  Also takes optional C{min}, C{max}, and C{exact} arguments,\n    as defined for the C{L{Word}} class.\n    \"\"\"\n    whiteStrs = {\n        \" \" : \"<SPC>\",\n        \"\\t\": \"<TAB>\",\n        \"\\n\": \"<LF>\",\n        \"\\r\": \"<CR>\",\n        \"\\f\": \"<FF>\",\n        }\n    def __init__(self, ws=\" \\t\\r\\n\", min=1, max=0, exact=0):\n        super(White,self).__init__()\n        self.matchWhite = ws\n        self.setWhitespaceChars( \"\".join(c for c in self.whiteChars if c not in self.matchWhite) )\n        #~ self.leaveWhitespace()\n        self.name = (\"\".join(White.whiteStrs[c] for c in self.matchWhite))\n        self.mayReturnEmpty = True\n        self.errmsg = \"Expected \" + self.name\n\n        self.minLen = min\n\n        if max > 0:\n            self.maxLen = max\n        else:\n            self.maxLen = _MAX_INT\n\n        if exact > 0:\n            self.maxLen = exact\n            self.minLen = exact\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if not(instring[ loc ] in self.matchWhite):\n            raise ParseException(instring, loc, self.errmsg, self)\n        start = loc\n        loc += 1\n        maxloc = start + self.maxLen\n        maxloc = min( maxloc, len(instring) )\n        while loc < maxloc and instring[loc] in self.matchWhite:\n            loc += 1\n\n        if loc - start < self.minLen:\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        return loc, instring[start:loc]\n\n\nclass _PositionToken(Token):\n    def __init__( self ):\n        super(_PositionToken,self).__init__()\n        self.name=self.__class__.__name__\n        self.mayReturnEmpty = True\n        self.mayIndexError = False\n\nclass GoToColumn(_PositionToken):\n    \"\"\"\n    Token to advance to a specific column of input text; useful for tabular report scraping.\n    \"\"\"\n    def __init__( self, colno ):\n        super(GoToColumn,self).__init__()\n        self.col = colno\n\n    def preParse( self, instring, loc ):\n        if col(loc,instring) != self.col:\n            instrlen = len(instring)\n            if self.ignoreExprs:\n                loc = self._skipIgnorables( instring, loc )\n            while loc < instrlen and instring[loc].isspace() and col( loc, instring ) != self.col :\n                loc += 1\n        return loc\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        thiscol = col( loc, instring )\n        if thiscol > self.col:\n            raise ParseException( instring, loc, \"Text not in expected column\", self )\n        newloc = loc + self.col - thiscol\n        ret = instring[ loc: newloc ]\n        return newloc, ret\n\n\nclass LineStart(_PositionToken):\n    \"\"\"\n    Matches if current position is at the beginning of a line within the parse string\n    \n    Example::\n    \n        test = '''\\\n        AAA this line\n        AAA and this line\n          AAA but not this one\n        B AAA and definitely not this one\n        '''\n\n        for t in (LineStart() + 'AAA' + restOfLine).searchString(test):\n            print(t)\n    \n    Prints::\n        ['AAA', ' this line']\n        ['AAA', ' and this line']    \n\n    \"\"\"\n    def __init__( self ):\n        super(LineStart,self).__init__()\n        self.errmsg = \"Expected start of line\"\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if col(loc, instring) == 1:\n            return loc, []\n        raise ParseException(instring, loc, self.errmsg, self)\n\nclass LineEnd(_PositionToken):\n    \"\"\"\n    Matches if current position is at the end of a line within the parse string\n    \"\"\"\n    def __init__( self ):\n        super(LineEnd,self).__init__()\n        self.setWhitespaceChars( ParserElement.DEFAULT_WHITE_CHARS.replace(\"\\n\",\"\") )\n        self.errmsg = \"Expected end of line\"\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if loc<len(instring):\n            if instring[loc] == \"\\n\":\n                return loc+1, \"\\n\"\n            else:\n                raise ParseException(instring, loc, self.errmsg, self)\n        elif loc == len(instring):\n            return loc+1, []\n        else:\n            raise ParseException(instring, loc, self.errmsg, self)\n\nclass StringStart(_PositionToken):\n    \"\"\"\n    Matches if current position is at the beginning of the parse string\n    \"\"\"\n    def __init__( self ):\n        super(StringStart,self).__init__()\n        self.errmsg = \"Expected start of text\"\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if loc != 0:\n            # see if entire string up to here is just whitespace and ignoreables\n            if loc != self.preParse( instring, 0 ):\n                raise ParseException(instring, loc, self.errmsg, self)\n        return loc, []\n\nclass StringEnd(_PositionToken):\n    \"\"\"\n    Matches if current position is at the end of the parse string\n    \"\"\"\n    def __init__( self ):\n        super(StringEnd,self).__init__()\n        self.errmsg = \"Expected end of text\"\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if loc < len(instring):\n            raise ParseException(instring, loc, self.errmsg, self)\n        elif loc == len(instring):\n            return loc+1, []\n        elif loc > len(instring):\n            return loc, []\n        else:\n            raise ParseException(instring, loc, self.errmsg, self)\n\nclass WordStart(_PositionToken):\n    \"\"\"\n    Matches if the current position is at the beginning of a Word, and\n    is not preceded by any character in a given set of C{wordChars}\n    (default=C{printables}). To emulate the C{\\b} behavior of regular expressions,\n    use C{WordStart(alphanums)}. C{WordStart} will also match at the beginning of\n    the string being parsed, or at the beginning of a line.\n    \"\"\"\n    def __init__(self, wordChars = printables):\n        super(WordStart,self).__init__()\n        self.wordChars = set(wordChars)\n        self.errmsg = \"Not at the start of a word\"\n\n    def parseImpl(self, instring, loc, doActions=True ):\n        if loc != 0:\n            if (instring[loc-1] in self.wordChars or\n                instring[loc] not in self.wordChars):\n                raise ParseException(instring, loc, self.errmsg, self)\n        return loc, []\n\nclass WordEnd(_PositionToken):\n    \"\"\"\n    Matches if the current position is at the end of a Word, and\n    is not followed by any character in a given set of C{wordChars}\n    (default=C{printables}). To emulate the C{\\b} behavior of regular expressions,\n    use C{WordEnd(alphanums)}. C{WordEnd} will also match at the end of\n    the string being parsed, or at the end of a line.\n    \"\"\"\n    def __init__(self, wordChars = printables):\n        super(WordEnd,self).__init__()\n        self.wordChars = set(wordChars)\n        self.skipWhitespace = False\n        self.errmsg = \"Not at the end of a word\"\n\n    def parseImpl(self, instring, loc, doActions=True ):\n        instrlen = len(instring)\n        if instrlen>0 and loc<instrlen:\n            if (instring[loc] in self.wordChars or\n                instring[loc-1] not in self.wordChars):\n                raise ParseException(instring, loc, self.errmsg, self)\n        return loc, []\n\n\nclass ParseExpression(ParserElement):\n    \"\"\"\n    Abstract subclass of ParserElement, for combining and post-processing parsed tokens.\n    \"\"\"\n    def __init__( self, exprs, savelist = False ):\n        super(ParseExpression,self).__init__(savelist)\n        if isinstance( exprs, _generatorType ):\n            exprs = list(exprs)\n\n        if isinstance( exprs, basestring ):\n            self.exprs = [ ParserElement._literalStringClass( exprs ) ]\n        elif isinstance( exprs, Iterable ):\n            exprs = list(exprs)\n            # if sequence of strings provided, wrap with Literal\n            if all(isinstance(expr, basestring) for expr in exprs):\n                exprs = map(ParserElement._literalStringClass, exprs)\n            self.exprs = list(exprs)\n        else:\n            try:\n                self.exprs = list( exprs )\n            except TypeError:\n                self.exprs = [ exprs ]\n        self.callPreparse = False\n\n    def __getitem__( self, i ):\n        return self.exprs[i]\n\n    def append( self, other ):\n        self.exprs.append( other )\n        self.strRepr = None\n        return self\n\n    def leaveWhitespace( self ):\n        \"\"\"Extends C{leaveWhitespace} defined in base class, and also invokes C{leaveWhitespace} on\n           all contained expressions.\"\"\"\n        self.skipWhitespace = False\n        self.exprs = [ e.copy() for e in self.exprs ]\n        for e in self.exprs:\n            e.leaveWhitespace()\n        return self\n\n    def ignore( self, other ):\n        if isinstance( other, Suppress ):\n            if other not in self.ignoreExprs:\n                super( ParseExpression, self).ignore( other )\n                for e in self.exprs:\n                    e.ignore( self.ignoreExprs[-1] )\n        else:\n            super( ParseExpression, self).ignore( other )\n            for e in self.exprs:\n                e.ignore( self.ignoreExprs[-1] )\n        return self\n\n    def __str__( self ):\n        try:\n            return super(ParseExpression,self).__str__()\n        except Exception:\n            pass\n\n        if self.strRepr is None:\n            self.strRepr = \"%s:(%s)\" % ( self.__class__.__name__, _ustr(self.exprs) )\n        return self.strRepr\n\n    def streamline( self ):\n        super(ParseExpression,self).streamline()\n\n        for e in self.exprs:\n            e.streamline()\n\n        # collapse nested And's of the form And( And( And( a,b), c), d) to And( a,b,c,d )\n        # but only if there are no parse actions or resultsNames on the nested And's\n        # (likewise for Or's and MatchFirst's)\n        if ( len(self.exprs) == 2 ):\n            other = self.exprs[0]\n            if ( isinstance( other, self.__class__ ) and\n                  not(other.parseAction) and\n                  other.resultsName is None and\n                  not other.debug ):\n                self.exprs = other.exprs[:] + [ self.exprs[1] ]\n                self.strRepr = None\n                self.mayReturnEmpty |= other.mayReturnEmpty\n                self.mayIndexError  |= other.mayIndexError\n\n            other = self.exprs[-1]\n            if ( isinstance( other, self.__class__ ) and\n                  not(other.parseAction) and\n                  other.resultsName is None and\n                  not other.debug ):\n                self.exprs = self.exprs[:-1] + other.exprs[:]\n                self.strRepr = None\n                self.mayReturnEmpty |= other.mayReturnEmpty\n                self.mayIndexError  |= other.mayIndexError\n\n        self.errmsg = \"Expected \" + _ustr(self)\n        \n        return self\n\n    def setResultsName( self, name, listAllMatches=False ):\n        ret = super(ParseExpression,self).setResultsName(name,listAllMatches)\n        return ret\n\n    def validate( self, validateTrace=[] ):\n        tmp = validateTrace[:]+[self]\n        for e in self.exprs:\n            e.validate(tmp)\n        self.checkRecursion( [] )\n        \n    def copy(self):\n        ret = super(ParseExpression,self).copy()\n        ret.exprs = [e.copy() for e in self.exprs]\n        return ret\n\nclass And(ParseExpression):\n    \"\"\"\n    Requires all given C{ParseExpression}s to be found in the given order.\n    Expressions may be separated by whitespace.\n    May be constructed using the C{'+'} operator.\n    May also be constructed using the C{'-'} operator, which will suppress backtracking.\n\n    Example::\n        integer = Word(nums)\n        name_expr = OneOrMore(Word(alphas))\n\n        expr = And([integer(\"id\"),name_expr(\"name\"),integer(\"age\")])\n        # more easily written as:\n        expr = integer(\"id\") + name_expr(\"name\") + integer(\"age\")\n    \"\"\"\n\n    class _ErrorStop(Empty):\n        def __init__(self, *args, **kwargs):\n            super(And._ErrorStop,self).__init__(*args, **kwargs)\n            self.name = '-'\n            self.leaveWhitespace()\n\n    def __init__( self, exprs, savelist = True ):\n        super(And,self).__init__(exprs, savelist)\n        self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)\n        self.setWhitespaceChars( self.exprs[0].whiteChars )\n        self.skipWhitespace = self.exprs[0].skipWhitespace\n        self.callPreparse = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        # pass False as last arg to _parse for first element, since we already\n        # pre-parsed the string as part of our And pre-parsing\n        loc, resultlist = self.exprs[0]._parse( instring, loc, doActions, callPreParse=False )\n        errorStop = False\n        for e in self.exprs[1:]:\n            if isinstance(e, And._ErrorStop):\n                errorStop = True\n                continue\n            if errorStop:\n                try:\n                    loc, exprtokens = e._parse( instring, loc, doActions )\n                except ParseSyntaxException:\n                    raise\n                except ParseBaseException as pe:\n                    pe.__traceback__ = None\n                    raise ParseSyntaxException._from_exception(pe)\n                except IndexError:\n                    raise ParseSyntaxException(instring, len(instring), self.errmsg, self)\n            else:\n                loc, exprtokens = e._parse( instring, loc, doActions )\n            if exprtokens or exprtokens.haskeys():\n                resultlist += exprtokens\n        return loc, resultlist\n\n    def __iadd__(self, other ):\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        return self.append( other ) #And( [ self, other ] )\n\n    def checkRecursion( self, parseElementList ):\n        subRecCheckList = parseElementList[:] + [ self ]\n        for e in self.exprs:\n            e.checkRecursion( subRecCheckList )\n            if not e.mayReturnEmpty:\n                break\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"{\" + \" \".join(_ustr(e) for e in self.exprs) + \"}\"\n\n        return self.strRepr\n\n\nclass Or(ParseExpression):\n    \"\"\"\n    Requires that at least one C{ParseExpression} is found.\n    If two expressions match, the expression that matches the longest string will be used.\n    May be constructed using the C{'^'} operator.\n\n    Example::\n        # construct Or using '^' operator\n        \n        number = Word(nums) ^ Combine(Word(nums) + '.' + Word(nums))\n        print(number.searchString(\"123 3.1416 789\"))\n    prints::\n        [['123'], ['3.1416'], ['789']]\n    \"\"\"\n    def __init__( self, exprs, savelist = False ):\n        super(Or,self).__init__(exprs, savelist)\n        if self.exprs:\n            self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)\n        else:\n            self.mayReturnEmpty = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        maxExcLoc = -1\n        maxException = None\n        matches = []\n        for e in self.exprs:\n            try:\n                loc2 = e.tryParse( instring, loc )\n            except ParseException as err:\n                err.__traceback__ = None\n                if err.loc > maxExcLoc:\n                    maxException = err\n                    maxExcLoc = err.loc\n            except IndexError:\n                if len(instring) > maxExcLoc:\n                    maxException = ParseException(instring,len(instring),e.errmsg,self)\n                    maxExcLoc = len(instring)\n            else:\n                # save match among all matches, to retry longest to shortest\n                matches.append((loc2, e))\n\n        if matches:\n            matches.sort(key=lambda x: -x[0])\n            for _,e in matches:\n                try:\n                    return e._parse( instring, loc, doActions )\n                except ParseException as err:\n                    err.__traceback__ = None\n                    if err.loc > maxExcLoc:\n                        maxException = err\n                        maxExcLoc = err.loc\n\n        if maxException is not None:\n            maxException.msg = self.errmsg\n            raise maxException\n        else:\n            raise ParseException(instring, loc, \"no defined alternatives to match\", self)\n\n\n    def __ixor__(self, other ):\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        return self.append( other ) #Or( [ self, other ] )\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"{\" + \" ^ \".join(_ustr(e) for e in self.exprs) + \"}\"\n\n        return self.strRepr\n\n    def checkRecursion( self, parseElementList ):\n        subRecCheckList = parseElementList[:] + [ self ]\n        for e in self.exprs:\n            e.checkRecursion( subRecCheckList )\n\n\nclass MatchFirst(ParseExpression):\n    \"\"\"\n    Requires that at least one C{ParseExpression} is found.\n    If two expressions match, the first one listed is the one that will match.\n    May be constructed using the C{'|'} operator.\n\n    Example::\n        # construct MatchFirst using '|' operator\n        \n        # watch the order of expressions to match\n        number = Word(nums) | Combine(Word(nums) + '.' + Word(nums))\n        print(number.searchString(\"123 3.1416 789\")) #  Fail! -> [['123'], ['3'], ['1416'], ['789']]\n\n        # put more selective expression first\n        number = Combine(Word(nums) + '.' + Word(nums)) | Word(nums)\n        print(number.searchString(\"123 3.1416 789\")) #  Better -> [['123'], ['3.1416'], ['789']]\n    \"\"\"\n    def __init__( self, exprs, savelist = False ):\n        super(MatchFirst,self).__init__(exprs, savelist)\n        if self.exprs:\n            self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)\n        else:\n            self.mayReturnEmpty = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        maxExcLoc = -1\n        maxException = None\n        for e in self.exprs:\n            try:\n                ret = e._parse( instring, loc, doActions )\n                return ret\n            except ParseException as err:\n                if err.loc > maxExcLoc:\n                    maxException = err\n                    maxExcLoc = err.loc\n            except IndexError:\n                if len(instring) > maxExcLoc:\n                    maxException = ParseException(instring,len(instring),e.errmsg,self)\n                    maxExcLoc = len(instring)\n\n        # only got here if no expression matched, raise exception for match that made it the furthest\n        else:\n            if maxException is not None:\n                maxException.msg = self.errmsg\n                raise maxException\n            else:\n                raise ParseException(instring, loc, \"no defined alternatives to match\", self)\n\n    def __ior__(self, other ):\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        return self.append( other ) #MatchFirst( [ self, other ] )\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"{\" + \" | \".join(_ustr(e) for e in self.exprs) + \"}\"\n\n        return self.strRepr\n\n    def checkRecursion( self, parseElementList ):\n        subRecCheckList = parseElementList[:] + [ self ]\n        for e in self.exprs:\n            e.checkRecursion( subRecCheckList )\n\n\nclass Each(ParseExpression):\n    \"\"\"\n    Requires all given C{ParseExpression}s to be found, but in any order.\n    Expressions may be separated by whitespace.\n    May be constructed using the C{'&'} operator.\n\n    Example::\n        color = oneOf(\"RED ORANGE YELLOW GREEN BLUE PURPLE BLACK WHITE BROWN\")\n        shape_type = oneOf(\"SQUARE CIRCLE TRIANGLE STAR HEXAGON OCTAGON\")\n        integer = Word(nums)\n        shape_attr = \"shape:\" + shape_type(\"shape\")\n        posn_attr = \"posn:\" + Group(integer(\"x\") + ',' + integer(\"y\"))(\"posn\")\n        color_attr = \"color:\" + color(\"color\")\n        size_attr = \"size:\" + integer(\"size\")\n\n        # use Each (using operator '&') to accept attributes in any order \n        # (shape and posn are required, color and size are optional)\n        shape_spec = shape_attr & posn_attr & Optional(color_attr) & Optional(size_attr)\n\n        shape_spec.runTests('''\n            shape: SQUARE color: BLACK posn: 100, 120\n            shape: CIRCLE size: 50 color: BLUE posn: 50,80\n            color:GREEN size:20 shape:TRIANGLE posn:20,40\n            '''\n            )\n    prints::\n        shape: SQUARE color: BLACK posn: 100, 120\n        ['shape:', 'SQUARE', 'color:', 'BLACK', 'posn:', ['100', ',', '120']]\n        - color: BLACK\n        - posn: ['100', ',', '120']\n          - x: 100\n          - y: 120\n        - shape: SQUARE\n\n\n        shape: CIRCLE size: 50 color: BLUE posn: 50,80\n        ['shape:', 'CIRCLE', 'size:', '50', 'color:', 'BLUE', 'posn:', ['50', ',', '80']]\n        - color: BLUE\n        - posn: ['50', ',', '80']\n          - x: 50\n          - y: 80\n        - shape: CIRCLE\n        - size: 50\n\n\n        color: GREEN size: 20 shape: TRIANGLE posn: 20,40\n        ['color:', 'GREEN', 'size:', '20', 'shape:', 'TRIANGLE', 'posn:', ['20', ',', '40']]\n        - color: GREEN\n        - posn: ['20', ',', '40']\n          - x: 20\n          - y: 40\n        - shape: TRIANGLE\n        - size: 20\n    \"\"\"\n    def __init__( self, exprs, savelist = True ):\n        super(Each,self).__init__(exprs, savelist)\n        self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)\n        self.skipWhitespace = True\n        self.initExprGroups = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if self.initExprGroups:\n            self.opt1map = dict((id(e.expr),e) for e in self.exprs if isinstance(e,Optional))\n            opt1 = [ e.expr for e in self.exprs if isinstance(e,Optional) ]\n            opt2 = [ e for e in self.exprs if e.mayReturnEmpty and not isinstance(e,Optional)]\n            self.optionals = opt1 + opt2\n            self.multioptionals = [ e.expr for e in self.exprs if isinstance(e,ZeroOrMore) ]\n            self.multirequired = [ e.expr for e in self.exprs if isinstance(e,OneOrMore) ]\n            self.required = [ e for e in self.exprs if not isinstance(e,(Optional,ZeroOrMore,OneOrMore)) ]\n            self.required += self.multirequired\n            self.initExprGroups = False\n        tmpLoc = loc\n        tmpReqd = self.required[:]\n        tmpOpt  = self.optionals[:]\n        matchOrder = []\n\n        keepMatching = True\n        while keepMatching:\n            tmpExprs = tmpReqd + tmpOpt + self.multioptionals + self.multirequired\n            failed = []\n            for e in tmpExprs:\n                try:\n                    tmpLoc = e.tryParse( instring, tmpLoc )\n                except ParseException:\n                    failed.append(e)\n                else:\n                    matchOrder.append(self.opt1map.get(id(e),e))\n                    if e in tmpReqd:\n                        tmpReqd.remove(e)\n                    elif e in tmpOpt:\n                        tmpOpt.remove(e)\n            if len(failed) == len(tmpExprs):\n                keepMatching = False\n\n        if tmpReqd:\n            missing = \", \".join(_ustr(e) for e in tmpReqd)\n            raise ParseException(instring,loc,\"Missing one or more required elements (%s)\" % missing )\n\n        # add any unmatched Optionals, in case they have default values defined\n        matchOrder += [e for e in self.exprs if isinstance(e,Optional) and e.expr in tmpOpt]\n\n        resultlist = []\n        for e in matchOrder:\n            loc,results = e._parse(instring,loc,doActions)\n            resultlist.append(results)\n\n        finalResults = sum(resultlist, ParseResults([]))\n        return loc, finalResults\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"{\" + \" & \".join(_ustr(e) for e in self.exprs) + \"}\"\n\n        return self.strRepr\n\n    def checkRecursion( self, parseElementList ):\n        subRecCheckList = parseElementList[:] + [ self ]\n        for e in self.exprs:\n            e.checkRecursion( subRecCheckList )\n\n\nclass ParseElementEnhance(ParserElement):\n    \"\"\"\n    Abstract subclass of C{ParserElement}, for combining and post-processing parsed tokens.\n    \"\"\"\n    def __init__( self, expr, savelist=False ):\n        super(ParseElementEnhance,self).__init__(savelist)\n        if isinstance( expr, basestring ):\n            if issubclass(ParserElement._literalStringClass, Token):\n                expr = ParserElement._literalStringClass(expr)\n            else:\n                expr = ParserElement._literalStringClass(Literal(expr))\n        self.expr = expr\n        self.strRepr = None\n        if expr is not None:\n            self.mayIndexError = expr.mayIndexError\n            self.mayReturnEmpty = expr.mayReturnEmpty\n            self.setWhitespaceChars( expr.whiteChars )\n            self.skipWhitespace = expr.skipWhitespace\n            self.saveAsList = expr.saveAsList\n            self.callPreparse = expr.callPreparse\n            self.ignoreExprs.extend(expr.ignoreExprs)\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if self.expr is not None:\n            return self.expr._parse( instring, loc, doActions, callPreParse=False )\n        else:\n            raise ParseException(\"\",loc,self.errmsg,self)\n\n    def leaveWhitespace( self ):\n        self.skipWhitespace = False\n        self.expr = self.expr.copy()\n        if self.expr is not None:\n            self.expr.leaveWhitespace()\n        return self\n\n    def ignore( self, other ):\n        if isinstance( other, Suppress ):\n            if other not in self.ignoreExprs:\n                super( ParseElementEnhance, self).ignore( other )\n                if self.expr is not None:\n                    self.expr.ignore( self.ignoreExprs[-1] )\n        else:\n            super( ParseElementEnhance, self).ignore( other )\n            if self.expr is not None:\n                self.expr.ignore( self.ignoreExprs[-1] )\n        return self\n\n    def streamline( self ):\n        super(ParseElementEnhance,self).streamline()\n        if self.expr is not None:\n            self.expr.streamline()\n        return self\n\n    def checkRecursion( self, parseElementList ):\n        if self in parseElementList:\n            raise RecursiveGrammarException( parseElementList+[self] )\n        subRecCheckList = parseElementList[:] + [ self ]\n        if self.expr is not None:\n            self.expr.checkRecursion( subRecCheckList )\n\n    def validate( self, validateTrace=[] ):\n        tmp = validateTrace[:]+[self]\n        if self.expr is not None:\n            self.expr.validate(tmp)\n        self.checkRecursion( [] )\n\n    def __str__( self ):\n        try:\n            return super(ParseElementEnhance,self).__str__()\n        except Exception:\n            pass\n\n        if self.strRepr is None and self.expr is not None:\n            self.strRepr = \"%s:(%s)\" % ( self.__class__.__name__, _ustr(self.expr) )\n        return self.strRepr\n\n\nclass FollowedBy(ParseElementEnhance):\n    \"\"\"\n    Lookahead matching of the given parse expression.  C{FollowedBy}\n    does I{not} advance the parsing position within the input string, it only\n    verifies that the specified parse expression matches at the current\n    position.  C{FollowedBy} always returns a null token list.\n\n    Example::\n        # use FollowedBy to match a label only if it is followed by a ':'\n        data_word = Word(alphas)\n        label = data_word + FollowedBy(':')\n        attr_expr = Group(label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))\n        \n        OneOrMore(attr_expr).parseString(\"shape: SQUARE color: BLACK posn: upper left\").pprint()\n    prints::\n        [['shape', 'SQUARE'], ['color', 'BLACK'], ['posn', 'upper left']]\n    \"\"\"\n    def __init__( self, expr ):\n        super(FollowedBy,self).__init__(expr)\n        self.mayReturnEmpty = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        self.expr.tryParse( instring, loc )\n        return loc, []\n\n\nclass NotAny(ParseElementEnhance):\n    \"\"\"\n    Lookahead to disallow matching with the given parse expression.  C{NotAny}\n    does I{not} advance the parsing position within the input string, it only\n    verifies that the specified parse expression does I{not} match at the current\n    position.  Also, C{NotAny} does I{not} skip over leading whitespace. C{NotAny}\n    always returns a null token list.  May be constructed using the '~' operator.\n\n    Example::\n        \n    \"\"\"\n    def __init__( self, expr ):\n        super(NotAny,self).__init__(expr)\n        #~ self.leaveWhitespace()\n        self.skipWhitespace = False  # do NOT use self.leaveWhitespace(), don't want to propagate to exprs\n        self.mayReturnEmpty = True\n        self.errmsg = \"Found unwanted token, \"+_ustr(self.expr)\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if self.expr.canParseNext(instring, loc):\n            raise ParseException(instring, loc, self.errmsg, self)\n        return loc, []\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"~{\" + _ustr(self.expr) + \"}\"\n\n        return self.strRepr\n\nclass _MultipleMatch(ParseElementEnhance):\n    def __init__( self, expr, stopOn=None):\n        super(_MultipleMatch, self).__init__(expr)\n        self.saveAsList = True\n        ender = stopOn\n        if isinstance(ender, basestring):\n            ender = ParserElement._literalStringClass(ender)\n        self.not_ender = ~ender if ender is not None else None\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        self_expr_parse = self.expr._parse\n        self_skip_ignorables = self._skipIgnorables\n        check_ender = self.not_ender is not None\n        if check_ender:\n            try_not_ender = self.not_ender.tryParse\n        \n        # must be at least one (but first see if we are the stopOn sentinel;\n        # if so, fail)\n        if check_ender:\n            try_not_ender(instring, loc)\n        loc, tokens = self_expr_parse( instring, loc, doActions, callPreParse=False )\n        try:\n            hasIgnoreExprs = (not not self.ignoreExprs)\n            while 1:\n                if check_ender:\n                    try_not_ender(instring, loc)\n                if hasIgnoreExprs:\n                    preloc = self_skip_ignorables( instring, loc )\n                else:\n                    preloc = loc\n                loc, tmptokens = self_expr_parse( instring, preloc, doActions )\n                if tmptokens or tmptokens.haskeys():\n                    tokens += tmptokens\n        except (ParseException,IndexError):\n            pass\n\n        return loc, tokens\n        \nclass OneOrMore(_MultipleMatch):\n    \"\"\"\n    Repetition of one or more of the given expression.\n    \n    Parameters:\n     - expr - expression that must match one or more times\n     - stopOn - (default=C{None}) - expression for a terminating sentinel\n          (only required if the sentinel would ordinarily match the repetition \n          expression)          \n\n    Example::\n        data_word = Word(alphas)\n        label = data_word + FollowedBy(':')\n        attr_expr = Group(label + Suppress(':') + OneOrMore(data_word).setParseAction(' '.join))\n\n        text = \"shape: SQUARE posn: upper left color: BLACK\"\n        OneOrMore(attr_expr).parseString(text).pprint()  # Fail! read 'color' as data instead of next label -> [['shape', 'SQUARE color']]\n\n        # use stopOn attribute for OneOrMore to avoid reading label string as part of the data\n        attr_expr = Group(label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))\n        OneOrMore(attr_expr).parseString(text).pprint() # Better -> [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'BLACK']]\n        \n        # could also be written as\n        (attr_expr * (1,)).parseString(text).pprint()\n    \"\"\"\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"{\" + _ustr(self.expr) + \"}...\"\n\n        return self.strRepr\n\nclass ZeroOrMore(_MultipleMatch):\n    \"\"\"\n    Optional repetition of zero or more of the given expression.\n    \n    Parameters:\n     - expr - expression that must match zero or more times\n     - stopOn - (default=C{None}) - expression for a terminating sentinel\n          (only required if the sentinel would ordinarily match the repetition \n          expression)          \n\n    Example: similar to L{OneOrMore}\n    \"\"\"\n    def __init__( self, expr, stopOn=None):\n        super(ZeroOrMore,self).__init__(expr, stopOn=stopOn)\n        self.mayReturnEmpty = True\n        \n    def parseImpl( self, instring, loc, doActions=True ):\n        try:\n            return super(ZeroOrMore, self).parseImpl(instring, loc, doActions)\n        except (ParseException,IndexError):\n            return loc, []\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"[\" + _ustr(self.expr) + \"]...\"\n\n        return self.strRepr\n\nclass _NullToken(object):\n    def __bool__(self):\n        return False\n    __nonzero__ = __bool__\n    def __str__(self):\n        return \"\"\n\n_optionalNotMatched = _NullToken()\nclass Optional(ParseElementEnhance):\n    \"\"\"\n    Optional matching of the given expression.\n\n    Parameters:\n     - expr - expression that must match zero or more times\n     - default (optional) - value to be returned if the optional expression is not found.\n\n    Example::\n        # US postal code can be a 5-digit zip, plus optional 4-digit qualifier\n        zip = Combine(Word(nums, exact=5) + Optional('-' + Word(nums, exact=4)))\n        zip.runTests('''\n            # traditional ZIP code\n            12345\n            \n            # ZIP+4 form\n            12101-0001\n            \n            # invalid ZIP\n            98765-\n            ''')\n    prints::\n        # traditional ZIP code\n        12345\n        ['12345']\n\n        # ZIP+4 form\n        12101-0001\n        ['12101-0001']\n\n        # invalid ZIP\n        98765-\n             ^\n        FAIL: Expected end of text (at char 5), (line:1, col:6)\n    \"\"\"\n    def __init__( self, expr, default=_optionalNotMatched ):\n        super(Optional,self).__init__( expr, savelist=False )\n        self.saveAsList = self.expr.saveAsList\n        self.defaultValue = default\n        self.mayReturnEmpty = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        try:\n            loc, tokens = self.expr._parse( instring, loc, doActions, callPreParse=False )\n        except (ParseException,IndexError):\n            if self.defaultValue is not _optionalNotMatched:\n                if self.expr.resultsName:\n                    tokens = ParseResults([ self.defaultValue ])\n                    tokens[self.expr.resultsName] = self.defaultValue\n                else:\n                    tokens = [ self.defaultValue ]\n            else:\n                tokens = []\n        return loc, tokens\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"[\" + _ustr(self.expr) + \"]\"\n\n        return self.strRepr\n\nclass SkipTo(ParseElementEnhance):\n    \"\"\"\n    Token for skipping over all undefined text until the matched expression is found.\n\n    Parameters:\n     - expr - target expression marking the end of the data to be skipped\n     - include - (default=C{False}) if True, the target expression is also parsed \n          (the skipped text and target expression are returned as a 2-element list).\n     - ignore - (default=C{None}) used to define grammars (typically quoted strings and \n          comments) that might contain false matches to the target expression\n     - failOn - (default=C{None}) define expressions that are not allowed to be \n          included in the skipped test; if found before the target expression is found, \n          the SkipTo is not a match\n\n    Example::\n        report = '''\n            Outstanding Issues Report - 1 Jan 2000\n\n               # | Severity | Description                               |  Days Open\n            -----+----------+-------------------------------------------+-----------\n             101 | Critical | Intermittent system crash                 |          6\n              94 | Cosmetic | Spelling error on Login ('log|n')         |         14\n              79 | Minor    | System slow when running too many reports |         47\n            '''\n        integer = Word(nums)\n        SEP = Suppress('|')\n        # use SkipTo to simply match everything up until the next SEP\n        # - ignore quoted strings, so that a '|' character inside a quoted string does not match\n        # - parse action will call token.strip() for each matched token, i.e., the description body\n        string_data = SkipTo(SEP, ignore=quotedString)\n        string_data.setParseAction(tokenMap(str.strip))\n        ticket_expr = (integer(\"issue_num\") + SEP \n                      + string_data(\"sev\") + SEP \n                      + string_data(\"desc\") + SEP \n                      + integer(\"days_open\"))\n        \n        for tkt in ticket_expr.searchString(report):\n            print tkt.dump()\n    prints::\n        ['101', 'Critical', 'Intermittent system crash', '6']\n        - days_open: 6\n        - desc: Intermittent system crash\n        - issue_num: 101\n        - sev: Critical\n        ['94', 'Cosmetic', \"Spelling error on Login ('log|n')\", '14']\n        - days_open: 14\n        - desc: Spelling error on Login ('log|n')\n        - issue_num: 94\n        - sev: Cosmetic\n        ['79', 'Minor', 'System slow when running too many reports', '47']\n        - days_open: 47\n        - desc: System slow when running too many reports\n        - issue_num: 79\n        - sev: Minor\n    \"\"\"\n    def __init__( self, other, include=False, ignore=None, failOn=None ):\n        super( SkipTo, self ).__init__( other )\n        self.ignoreExpr = ignore\n        self.mayReturnEmpty = True\n        self.mayIndexError = False\n        self.includeMatch = include\n        self.asList = False\n        if isinstance(failOn, basestring):\n            self.failOn = ParserElement._literalStringClass(failOn)\n        else:\n            self.failOn = failOn\n        self.errmsg = \"No match found for \"+_ustr(self.expr)\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        startloc = loc\n        instrlen = len(instring)\n        expr = self.expr\n        expr_parse = self.expr._parse\n        self_failOn_canParseNext = self.failOn.canParseNext if self.failOn is not None else None\n        self_ignoreExpr_tryParse = self.ignoreExpr.tryParse if self.ignoreExpr is not None else None\n        \n        tmploc = loc\n        while tmploc <= instrlen:\n            if self_failOn_canParseNext is not None:\n                # break if failOn expression matches\n                if self_failOn_canParseNext(instring, tmploc):\n                    break\n                    \n            if self_ignoreExpr_tryParse is not None:\n                # advance past ignore expressions\n                while 1:\n                    try:\n                        tmploc = self_ignoreExpr_tryParse(instring, tmploc)\n                    except ParseBaseException:\n                        break\n            \n            try:\n                expr_parse(instring, tmploc, doActions=False, callPreParse=False)\n            except (ParseException, IndexError):\n                # no match, advance loc in string\n                tmploc += 1\n            else:\n                # matched skipto expr, done\n                break\n\n        else:\n            # ran off the end of the input string without matching skipto expr, fail\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        # build up return values\n        loc = tmploc\n        skiptext = instring[startloc:loc]\n        skipresult = ParseResults(skiptext)\n        \n        if self.includeMatch:\n            loc, mat = expr_parse(instring,loc,doActions,callPreParse=False)\n            skipresult += mat\n\n        return loc, skipresult\n\nclass Forward(ParseElementEnhance):\n    \"\"\"\n    Forward declaration of an expression to be defined later -\n    used for recursive grammars, such as algebraic infix notation.\n    When the expression is known, it is assigned to the C{Forward} variable using the '<<' operator.\n\n    Note: take care when assigning to C{Forward} not to overlook precedence of operators.\n    Specifically, '|' has a lower precedence than '<<', so that::\n        fwdExpr << a | b | c\n    will actually be evaluated as::\n        (fwdExpr << a) | b | c\n    thereby leaving b and c out as parseable alternatives.  It is recommended that you\n    explicitly group the values inserted into the C{Forward}::\n        fwdExpr << (a | b | c)\n    Converting to use the '<<=' operator instead will avoid this problem.\n\n    See L{ParseResults.pprint} for an example of a recursive parser created using\n    C{Forward}.\n    \"\"\"\n    def __init__( self, other=None ):\n        super(Forward,self).__init__( other, savelist=False )\n\n    def __lshift__( self, other ):\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass(other)\n        self.expr = other\n        self.strRepr = None\n        self.mayIndexError = self.expr.mayIndexError\n        self.mayReturnEmpty = self.expr.mayReturnEmpty\n        self.setWhitespaceChars( self.expr.whiteChars )\n        self.skipWhitespace = self.expr.skipWhitespace\n        self.saveAsList = self.expr.saveAsList\n        self.ignoreExprs.extend(self.expr.ignoreExprs)\n        return self\n        \n    def __ilshift__(self, other):\n        return self << other\n    \n    def leaveWhitespace( self ):\n        self.skipWhitespace = False\n        return self\n\n    def streamline( self ):\n        if not self.streamlined:\n            self.streamlined = True\n            if self.expr is not None:\n                self.expr.streamline()\n        return self\n\n    def validate( self, validateTrace=[] ):\n        if self not in validateTrace:\n            tmp = validateTrace[:]+[self]\n            if self.expr is not None:\n                self.expr.validate(tmp)\n        self.checkRecursion([])\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n        return self.__class__.__name__ + \": ...\"\n\n        # stubbed out for now - creates awful memory and perf issues\n        self._revertClass = self.__class__\n        self.__class__ = _ForwardNoRecurse\n        try:\n            if self.expr is not None:\n                retString = _ustr(self.expr)\n            else:\n                retString = \"None\"\n        finally:\n            self.__class__ = self._revertClass\n        return self.__class__.__name__ + \": \" + retString\n\n    def copy(self):\n        if self.expr is not None:\n            return super(Forward,self).copy()\n        else:\n            ret = Forward()\n            ret <<= self\n            return ret\n\nclass _ForwardNoRecurse(Forward):\n    def __str__( self ):\n        return \"...\"\n\nclass TokenConverter(ParseElementEnhance):\n    \"\"\"\n    Abstract subclass of C{ParseExpression}, for converting parsed results.\n    \"\"\"\n    def __init__( self, expr, savelist=False ):\n        super(TokenConverter,self).__init__( expr )#, savelist )\n        self.saveAsList = False\n\nclass Combine(TokenConverter):\n    \"\"\"\n    Converter to concatenate all matching tokens to a single string.\n    By default, the matching patterns must also be contiguous in the input string;\n    this can be disabled by specifying C{'adjacent=False'} in the constructor.\n\n    Example::\n        real = Word(nums) + '.' + Word(nums)\n        print(real.parseString('3.1416')) # -> ['3', '.', '1416']\n        # will also erroneously match the following\n        print(real.parseString('3. 1416')) # -> ['3', '.', '1416']\n\n        real = Combine(Word(nums) + '.' + Word(nums))\n        print(real.parseString('3.1416')) # -> ['3.1416']\n        # no match when there are internal spaces\n        print(real.parseString('3. 1416')) # -> Exception: Expected W:(0123...)\n    \"\"\"\n    def __init__( self, expr, joinString=\"\", adjacent=True ):\n        super(Combine,self).__init__( expr )\n        # suppress whitespace-stripping in contained parse expressions, but re-enable it on the Combine itself\n        if adjacent:\n            self.leaveWhitespace()\n        self.adjacent = adjacent\n        self.skipWhitespace = True\n        self.joinString = joinString\n        self.callPreparse = True\n\n    def ignore( self, other ):\n        if self.adjacent:\n            ParserElement.ignore(self, other)\n        else:\n            super( Combine, self).ignore( other )\n        return self\n\n    def postParse( self, instring, loc, tokenlist ):\n        retToks = tokenlist.copy()\n        del retToks[:]\n        retToks += ParseResults([ \"\".join(tokenlist._asStringList(self.joinString)) ], modal=self.modalResults)\n\n        if self.resultsName and retToks.haskeys():\n            return [ retToks ]\n        else:\n            return retToks\n\nclass Group(TokenConverter):\n    \"\"\"\n    Converter to return the matched tokens as a list - useful for returning tokens of C{L{ZeroOrMore}} and C{L{OneOrMore}} expressions.\n\n    Example::\n        ident = Word(alphas)\n        num = Word(nums)\n        term = ident | num\n        func = ident + Optional(delimitedList(term))\n        print(func.parseString(\"fn a,b,100\"))  # -> ['fn', 'a', 'b', '100']\n\n        func = ident + Group(Optional(delimitedList(term)))\n        print(func.parseString(\"fn a,b,100\"))  # -> ['fn', ['a', 'b', '100']]\n    \"\"\"\n    def __init__( self, expr ):\n        super(Group,self).__init__( expr )\n        self.saveAsList = True\n\n    def postParse( self, instring, loc, tokenlist ):\n        return [ tokenlist ]\n\nclass Dict(TokenConverter):\n    \"\"\"\n    Converter to return a repetitive expression as a list, but also as a dictionary.\n    Each element can also be referenced using the first token in the expression as its key.\n    Useful for tabular report scraping when the first column can be used as a item key.\n\n    Example::\n        data_word = Word(alphas)\n        label = data_word + FollowedBy(':')\n        attr_expr = Group(label + Suppress(':') + OneOrMore(data_word).setParseAction(' '.join))\n\n        text = \"shape: SQUARE posn: upper left color: light blue texture: burlap\"\n        attr_expr = (label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))\n        \n        # print attributes as plain groups\n        print(OneOrMore(attr_expr).parseString(text).dump())\n        \n        # instead of OneOrMore(expr), parse using Dict(OneOrMore(Group(expr))) - Dict will auto-assign names\n        result = Dict(OneOrMore(Group(attr_expr))).parseString(text)\n        print(result.dump())\n        \n        # access named fields as dict entries, or output as dict\n        print(result['shape'])        \n        print(result.asDict())\n    prints::\n        ['shape', 'SQUARE', 'posn', 'upper left', 'color', 'light blue', 'texture', 'burlap']\n\n        [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]\n        - color: light blue\n        - posn: upper left\n        - shape: SQUARE\n        - texture: burlap\n        SQUARE\n        {'color': 'light blue', 'posn': 'upper left', 'texture': 'burlap', 'shape': 'SQUARE'}\n    See more examples at L{ParseResults} of accessing fields by results name.\n    \"\"\"\n    def __init__( self, expr ):\n        super(Dict,self).__init__( expr )\n        self.saveAsList = True\n\n    def postParse( self, instring, loc, tokenlist ):\n        for i,tok in enumerate(tokenlist):\n            if len(tok) == 0:\n                continue\n            ikey = tok[0]\n            if isinstance(ikey,int):\n                ikey = _ustr(tok[0]).strip()\n            if len(tok)==1:\n                tokenlist[ikey] = _ParseResultsWithOffset(\"\",i)\n            elif len(tok)==2 and not isinstance(tok[1],ParseResults):\n                tokenlist[ikey] = _ParseResultsWithOffset(tok[1],i)\n            else:\n                dictvalue = tok.copy() #ParseResults(i)\n                del dictvalue[0]\n                if len(dictvalue)!= 1 or (isinstance(dictvalue,ParseResults) and dictvalue.haskeys()):\n                    tokenlist[ikey] = _ParseResultsWithOffset(dictvalue,i)\n                else:\n                    tokenlist[ikey] = _ParseResultsWithOffset(dictvalue[0],i)\n\n        if self.resultsName:\n            return [ tokenlist ]\n        else:\n            return tokenlist\n\n\nclass Suppress(TokenConverter):\n    \"\"\"\n    Converter for ignoring the results of a parsed expression.\n\n    Example::\n        source = \"a, b, c,d\"\n        wd = Word(alphas)\n        wd_list1 = wd + ZeroOrMore(',' + wd)\n        print(wd_list1.parseString(source))\n\n        # often, delimiters that are useful during parsing are just in the\n        # way afterward - use Suppress to keep them out of the parsed output\n        wd_list2 = wd + ZeroOrMore(Suppress(',') + wd)\n        print(wd_list2.parseString(source))\n    prints::\n        ['a', ',', 'b', ',', 'c', ',', 'd']\n        ['a', 'b', 'c', 'd']\n    (See also L{delimitedList}.)\n    \"\"\"\n    def postParse( self, instring, loc, tokenlist ):\n        return []\n\n    def suppress( self ):\n        return self\n\n\nclass OnlyOnce(object):\n    \"\"\"\n    Wrapper for parse actions, to ensure they are only called once.\n    \"\"\"\n    def __init__(self, methodCall):\n        self.callable = _trim_arity(methodCall)\n        self.called = False\n    def __call__(self,s,l,t):\n        if not self.called:\n            results = self.callable(s,l,t)\n            self.called = True\n            return results\n        raise ParseException(s,l,\"\")\n    def reset(self):\n        self.called = False\n\ndef traceParseAction(f):\n    \"\"\"\n    Decorator for debugging parse actions. \n    \n    When the parse action is called, this decorator will print C{\">> entering I{method-name}(line:I{current_source_line}, I{parse_location}, I{matched_tokens})\".}\n    When the parse action completes, the decorator will print C{\"<<\"} followed by the returned value, or any exception that the parse action raised.\n\n    Example::\n        wd = Word(alphas)\n\n        @traceParseAction\n        def remove_duplicate_chars(tokens):\n            return ''.join(sorted(set(''.join(tokens))))\n\n        wds = OneOrMore(wd).setParseAction(remove_duplicate_chars)\n        print(wds.parseString(\"slkdjs sld sldd sdlf sdljf\"))\n    prints::\n        >>entering remove_duplicate_chars(line: 'slkdjs sld sldd sdlf sdljf', 0, (['slkdjs', 'sld', 'sldd', 'sdlf', 'sdljf'], {}))\n        <<leaving remove_duplicate_chars (ret: 'dfjkls')\n        ['dfjkls']\n    \"\"\"\n    f = _trim_arity(f)\n    def z(*paArgs):\n        thisFunc = f.__name__\n        s,l,t = paArgs[-3:]\n        if len(paArgs)>3:\n            thisFunc = paArgs[0].__class__.__name__ + '.' + thisFunc\n        sys.stderr.write( \">>entering %s(line: '%s', %d, %r)\\n\" % (thisFunc,line(l,s),l,t) )\n        try:\n            ret = f(*paArgs)\n        except Exception as exc:\n            sys.stderr.write( \"<<leaving %s (exception: %s)\\n\" % (thisFunc,exc) )\n            raise\n        sys.stderr.write( \"<<leaving %s (ret: %r)\\n\" % (thisFunc,ret) )\n        return ret\n    try:\n        z.__name__ = f.__name__\n    except AttributeError:\n        pass\n    return z\n\n#\n# global helpers\n#\ndef delimitedList( expr, delim=\",\", combine=False ):\n    \"\"\"\n    Helper to define a delimited list of expressions - the delimiter defaults to ','.\n    By default, the list elements and delimiters can have intervening whitespace, and\n    comments, but this can be overridden by passing C{combine=True} in the constructor.\n    If C{combine} is set to C{True}, the matching tokens are returned as a single token\n    string, with the delimiters included; otherwise, the matching tokens are returned\n    as a list of tokens, with the delimiters suppressed.\n\n    Example::\n        delimitedList(Word(alphas)).parseString(\"aa,bb,cc\") # -> ['aa', 'bb', 'cc']\n        delimitedList(Word(hexnums), delim=':', combine=True).parseString(\"AA:BB:CC:DD:EE\") # -> ['AA:BB:CC:DD:EE']\n    \"\"\"\n    dlName = _ustr(expr)+\" [\"+_ustr(delim)+\" \"+_ustr(expr)+\"]...\"\n    if combine:\n        return Combine( expr + ZeroOrMore( delim + expr ) ).setName(dlName)\n    else:\n        return ( expr + ZeroOrMore( Suppress( delim ) + expr ) ).setName(dlName)\n\ndef countedArray( expr, intExpr=None ):\n    \"\"\"\n    Helper to define a counted list of expressions.\n    This helper defines a pattern of the form::\n        integer expr expr expr...\n    where the leading integer tells how many expr expressions follow.\n    The matched tokens returns the array of expr tokens as a list - the leading count token is suppressed.\n    \n    If C{intExpr} is specified, it should be a pyparsing expression that produces an integer value.\n\n    Example::\n        countedArray(Word(alphas)).parseString('2 ab cd ef')  # -> ['ab', 'cd']\n\n        # in this parser, the leading integer value is given in binary,\n        # '10' indicating that 2 values are in the array\n        binaryConstant = Word('01').setParseAction(lambda t: int(t[0], 2))\n        countedArray(Word(alphas), intExpr=binaryConstant).parseString('10 ab cd ef')  # -> ['ab', 'cd']\n    \"\"\"\n    arrayExpr = Forward()\n    def countFieldParseAction(s,l,t):\n        n = t[0]\n        arrayExpr << (n and Group(And([expr]*n)) or Group(empty))\n        return []\n    if intExpr is None:\n        intExpr = Word(nums).setParseAction(lambda t:int(t[0]))\n    else:\n        intExpr = intExpr.copy()\n    intExpr.setName(\"arrayLen\")\n    intExpr.addParseAction(countFieldParseAction, callDuringTry=True)\n    return ( intExpr + arrayExpr ).setName('(len) ' + _ustr(expr) + '...')\n\ndef _flatten(L):\n    ret = []\n    for i in L:\n        if isinstance(i,list):\n            ret.extend(_flatten(i))\n        else:\n            ret.append(i)\n    return ret\n\ndef matchPreviousLiteral(expr):\n    \"\"\"\n    Helper to define an expression that is indirectly defined from\n    the tokens matched in a previous expression, that is, it looks\n    for a 'repeat' of a previous expression.  For example::\n        first = Word(nums)\n        second = matchPreviousLiteral(first)\n        matchExpr = first + \":\" + second\n    will match C{\"1:1\"}, but not C{\"1:2\"}.  Because this matches a\n    previous literal, will also match the leading C{\"1:1\"} in C{\"1:10\"}.\n    If this is not desired, use C{matchPreviousExpr}.\n    Do I{not} use with packrat parsing enabled.\n    \"\"\"\n    rep = Forward()\n    def copyTokenToRepeater(s,l,t):\n        if t:\n            if len(t) == 1:\n                rep << t[0]\n            else:\n                # flatten t tokens\n                tflat = _flatten(t.asList())\n                rep << And(Literal(tt) for tt in tflat)\n        else:\n            rep << Empty()\n    expr.addParseAction(copyTokenToRepeater, callDuringTry=True)\n    rep.setName('(prev) ' + _ustr(expr))\n    return rep\n\ndef matchPreviousExpr(expr):\n    \"\"\"\n    Helper to define an expression that is indirectly defined from\n    the tokens matched in a previous expression, that is, it looks\n    for a 'repeat' of a previous expression.  For example::\n        first = Word(nums)\n        second = matchPreviousExpr(first)\n        matchExpr = first + \":\" + second\n    will match C{\"1:1\"}, but not C{\"1:2\"}.  Because this matches by\n    expressions, will I{not} match the leading C{\"1:1\"} in C{\"1:10\"};\n    the expressions are evaluated first, and then compared, so\n    C{\"1\"} is compared with C{\"10\"}.\n    Do I{not} use with packrat parsing enabled.\n    \"\"\"\n    rep = Forward()\n    e2 = expr.copy()\n    rep <<= e2\n    def copyTokenToRepeater(s,l,t):\n        matchTokens = _flatten(t.asList())\n        def mustMatchTheseTokens(s,l,t):\n            theseTokens = _flatten(t.asList())\n            if  theseTokens != matchTokens:\n                raise ParseException(\"\",0,\"\")\n        rep.setParseAction( mustMatchTheseTokens, callDuringTry=True )\n    expr.addParseAction(copyTokenToRepeater, callDuringTry=True)\n    rep.setName('(prev) ' + _ustr(expr))\n    return rep\n\ndef _escapeRegexRangeChars(s):\n    #~  escape these chars: ^-]\n    for c in r\"\\^-]\":\n        s = s.replace(c,_bslash+c)\n    s = s.replace(\"\\n\",r\"\\n\")\n    s = s.replace(\"\\t\",r\"\\t\")\n    return _ustr(s)\n\ndef oneOf( strs, caseless=False, useRegex=True ):\n    \"\"\"\n    Helper to quickly define a set of alternative Literals, and makes sure to do\n    longest-first testing when there is a conflict, regardless of the input order,\n    but returns a C{L{MatchFirst}} for best performance.\n\n    Parameters:\n     - strs - a string of space-delimited literals, or a collection of string literals\n     - caseless - (default=C{False}) - treat all literals as caseless\n     - useRegex - (default=C{True}) - as an optimization, will generate a Regex\n          object; otherwise, will generate a C{MatchFirst} object (if C{caseless=True}, or\n          if creating a C{Regex} raises an exception)\n\n    Example::\n        comp_oper = oneOf(\"< = > <= >= !=\")\n        var = Word(alphas)\n        number = Word(nums)\n        term = var | number\n        comparison_expr = term + comp_oper + term\n        print(comparison_expr.searchString(\"B = 12  AA=23 B<=AA AA>12\"))\n    prints::\n        [['B', '=', '12'], ['AA', '=', '23'], ['B', '<=', 'AA'], ['AA', '>', '12']]\n    \"\"\"\n    if caseless:\n        isequal = ( lambda a,b: a.upper() == b.upper() )\n        masks = ( lambda a,b: b.upper().startswith(a.upper()) )\n        parseElementClass = CaselessLiteral\n    else:\n        isequal = ( lambda a,b: a == b )\n        masks = ( lambda a,b: b.startswith(a) )\n        parseElementClass = Literal\n\n    symbols = []\n    if isinstance(strs,basestring):\n        symbols = strs.split()\n    elif isinstance(strs, Iterable):\n        symbols = list(strs)\n    else:\n        warnings.warn(\"Invalid argument to oneOf, expected string or iterable\",\n                SyntaxWarning, stacklevel=2)\n    if not symbols:\n        return NoMatch()\n\n    i = 0\n    while i < len(symbols)-1:\n        cur = symbols[i]\n        for j,other in enumerate(symbols[i+1:]):\n            if ( isequal(other, cur) ):\n                del symbols[i+j+1]\n                break\n            elif ( masks(cur, other) ):\n                del symbols[i+j+1]\n                symbols.insert(i,other)\n                cur = other\n                break\n        else:\n            i += 1\n\n    if not caseless and useRegex:\n        #~ print (strs,\"->\", \"|\".join( [ _escapeRegexChars(sym) for sym in symbols] ))\n        try:\n            if len(symbols)==len(\"\".join(symbols)):\n                return Regex( \"[%s]\" % \"\".join(_escapeRegexRangeChars(sym) for sym in symbols) ).setName(' | '.join(symbols))\n            else:\n                return Regex( \"|\".join(re.escape(sym) for sym in symbols) ).setName(' | '.join(symbols))\n        except Exception:\n            warnings.warn(\"Exception creating Regex for oneOf, building MatchFirst\",\n                    SyntaxWarning, stacklevel=2)\n\n\n    # last resort, just use MatchFirst\n    return MatchFirst(parseElementClass(sym) for sym in symbols).setName(' | '.join(symbols))\n\ndef dictOf( key, value ):\n    \"\"\"\n    Helper to easily and clearly define a dictionary by specifying the respective patterns\n    for the key and value.  Takes care of defining the C{L{Dict}}, C{L{ZeroOrMore}}, and C{L{Group}} tokens\n    in the proper order.  The key pattern can include delimiting markers or punctuation,\n    as long as they are suppressed, thereby leaving the significant key text.  The value\n    pattern can include named results, so that the C{Dict} results can include named token\n    fields.\n\n    Example::\n        text = \"shape: SQUARE posn: upper left color: light blue texture: burlap\"\n        attr_expr = (label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))\n        print(OneOrMore(attr_expr).parseString(text).dump())\n        \n        attr_label = label\n        attr_value = Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join)\n\n        # similar to Dict, but simpler call format\n        result = dictOf(attr_label, attr_value).parseString(text)\n        print(result.dump())\n        print(result['shape'])\n        print(result.shape)  # object attribute access works too\n        print(result.asDict())\n    prints::\n        [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]\n        - color: light blue\n        - posn: upper left\n        - shape: SQUARE\n        - texture: burlap\n        SQUARE\n        SQUARE\n        {'color': 'light blue', 'shape': 'SQUARE', 'posn': 'upper left', 'texture': 'burlap'}\n    \"\"\"\n    return Dict( ZeroOrMore( Group ( key + value ) ) )\n\ndef originalTextFor(expr, asString=True):\n    \"\"\"\n    Helper to return the original, untokenized text for a given expression.  Useful to\n    restore the parsed fields of an HTML start tag into the raw tag text itself, or to\n    revert separate tokens with intervening whitespace back to the original matching\n    input text. By default, returns astring containing the original parsed text.  \n       \n    If the optional C{asString} argument is passed as C{False}, then the return value is a \n    C{L{ParseResults}} containing any results names that were originally matched, and a \n    single token containing the original matched text from the input string.  So if \n    the expression passed to C{L{originalTextFor}} contains expressions with defined\n    results names, you must set C{asString} to C{False} if you want to preserve those\n    results name values.\n\n    Example::\n        src = \"this is test <b> bold <i>text</i> </b> normal text \"\n        for tag in (\"b\",\"i\"):\n            opener,closer = makeHTMLTags(tag)\n            patt = originalTextFor(opener + SkipTo(closer) + closer)\n            print(patt.searchString(src)[0])\n    prints::\n        ['<b> bold <i>text</i> </b>']\n        ['<i>text</i>']\n    \"\"\"\n    locMarker = Empty().setParseAction(lambda s,loc,t: loc)\n    endlocMarker = locMarker.copy()\n    endlocMarker.callPreparse = False\n    matchExpr = locMarker(\"_original_start\") + expr + endlocMarker(\"_original_end\")\n    if asString:\n        extractText = lambda s,l,t: s[t._original_start:t._original_end]\n    else:\n        def extractText(s,l,t):\n            t[:] = [s[t.pop('_original_start'):t.pop('_original_end')]]\n    matchExpr.setParseAction(extractText)\n    matchExpr.ignoreExprs = expr.ignoreExprs\n    return matchExpr\n\ndef ungroup(expr): \n    \"\"\"\n    Helper to undo pyparsing's default grouping of And expressions, even\n    if all but one are non-empty.\n    \"\"\"\n    return TokenConverter(expr).setParseAction(lambda t:t[0])\n\ndef locatedExpr(expr):\n    \"\"\"\n    Helper to decorate a returned token with its starting and ending locations in the input string.\n    This helper adds the following results names:\n     - locn_start = location where matched expression begins\n     - locn_end = location where matched expression ends\n     - value = the actual parsed results\n\n    Be careful if the input text contains C{<TAB>} characters, you may want to call\n    C{L{ParserElement.parseWithTabs}}\n\n    Example::\n        wd = Word(alphas)\n        for match in locatedExpr(wd).searchString(\"ljsdf123lksdjjf123lkkjj1222\"):\n            print(match)\n    prints::\n        [[0, 'ljsdf', 5]]\n        [[8, 'lksdjjf', 15]]\n        [[18, 'lkkjj', 23]]\n    \"\"\"\n    locator = Empty().setParseAction(lambda s,l,t: l)\n    return Group(locator(\"locn_start\") + expr(\"value\") + locator.copy().leaveWhitespace()(\"locn_end\"))\n\n\n# convenience constants for positional expressions\nempty       = Empty().setName(\"empty\")\nlineStart   = LineStart().setName(\"lineStart\")\nlineEnd     = LineEnd().setName(\"lineEnd\")\nstringStart = StringStart().setName(\"stringStart\")\nstringEnd   = StringEnd().setName(\"stringEnd\")\n\n_escapedPunc = Word( _bslash, r\"\\[]-*.$+^?()~ \", exact=2 ).setParseAction(lambda s,l,t:t[0][1])\n_escapedHexChar = Regex(r\"\\\\0?[xX][0-9a-fA-F]+\").setParseAction(lambda s,l,t:unichr(int(t[0].lstrip(r'\\0x'),16)))\n_escapedOctChar = Regex(r\"\\\\0[0-7]+\").setParseAction(lambda s,l,t:unichr(int(t[0][1:],8)))\n_singleChar = _escapedPunc | _escapedHexChar | _escapedOctChar | CharsNotIn(r'\\]', exact=1)\n_charRange = Group(_singleChar + Suppress(\"-\") + _singleChar)\n_reBracketExpr = Literal(\"[\") + Optional(\"^\").setResultsName(\"negate\") + Group( OneOrMore( _charRange | _singleChar ) ).setResultsName(\"body\") + \"]\"\n\ndef srange(s):\n    r\"\"\"\n    Helper to easily define string ranges for use in Word construction.  Borrows\n    syntax from regexp '[]' string range definitions::\n        srange(\"[0-9]\")   -> \"0123456789\"\n        srange(\"[a-z]\")   -> \"abcdefghijklmnopqrstuvwxyz\"\n        srange(\"[a-z$_]\") -> \"abcdefghijklmnopqrstuvwxyz$_\"\n    The input string must be enclosed in []'s, and the returned string is the expanded\n    character set joined into a single string.\n    The values enclosed in the []'s may be:\n     - a single character\n     - an escaped character with a leading backslash (such as C{\\-} or C{\\]})\n     - an escaped hex character with a leading C{'\\x'} (C{\\x21}, which is a C{'!'} character) \n         (C{\\0x##} is also supported for backwards compatibility) \n     - an escaped octal character with a leading C{'\\0'} (C{\\041}, which is a C{'!'} character)\n     - a range of any of the above, separated by a dash (C{'a-z'}, etc.)\n     - any combination of the above (C{'aeiouy'}, C{'a-zA-Z0-9_$'}, etc.)\n    \"\"\"\n    _expanded = lambda p: p if not isinstance(p,ParseResults) else ''.join(unichr(c) for c in range(ord(p[0]),ord(p[1])+1))\n    try:\n        return \"\".join(_expanded(part) for part in _reBracketExpr.parseString(s).body)\n    except Exception:\n        return \"\"\n\ndef matchOnlyAtCol(n):\n    \"\"\"\n    Helper method for defining parse actions that require matching at a specific\n    column in the input text.\n    \"\"\"\n    def verifyCol(strg,locn,toks):\n        if col(locn,strg) != n:\n            raise ParseException(strg,locn,\"matched token not at column %d\" % n)\n    return verifyCol\n\ndef replaceWith(replStr):\n    \"\"\"\n    Helper method for common parse actions that simply return a literal value.  Especially\n    useful when used with C{L{transformString<ParserElement.transformString>}()}.\n\n    Example::\n        num = Word(nums).setParseAction(lambda toks: int(toks[0]))\n        na = oneOf(\"N/A NA\").setParseAction(replaceWith(math.nan))\n        term = na | num\n        \n        OneOrMore(term).parseString(\"324 234 N/A 234\") # -> [324, 234, nan, 234]\n    \"\"\"\n    return lambda s,l,t: [replStr]\n\ndef removeQuotes(s,l,t):\n    \"\"\"\n    Helper parse action for removing quotation marks from parsed quoted strings.\n\n    Example::\n        # by default, quotation marks are included in parsed results\n        quotedString.parseString(\"'Now is the Winter of our Discontent'\") # -> [\"'Now is the Winter of our Discontent'\"]\n\n        # use removeQuotes to strip quotation marks from parsed results\n        quotedString.setParseAction(removeQuotes)\n        quotedString.parseString(\"'Now is the Winter of our Discontent'\") # -> [\"Now is the Winter of our Discontent\"]\n    \"\"\"\n    return t[0][1:-1]\n\ndef tokenMap(func, *args):\n    \"\"\"\n    Helper to define a parse action by mapping a function to all elements of a ParseResults list.If any additional \n    args are passed, they are forwarded to the given function as additional arguments after\n    the token, as in C{hex_integer = Word(hexnums).setParseAction(tokenMap(int, 16))}, which will convert the\n    parsed data to an integer using base 16.\n\n    Example (compare the last to example in L{ParserElement.transformString}::\n        hex_ints = OneOrMore(Word(hexnums)).setParseAction(tokenMap(int, 16))\n        hex_ints.runTests('''\n            00 11 22 aa FF 0a 0d 1a\n            ''')\n        \n        upperword = Word(alphas).setParseAction(tokenMap(str.upper))\n        OneOrMore(upperword).runTests('''\n            my kingdom for a horse\n            ''')\n\n        wd = Word(alphas).setParseAction(tokenMap(str.title))\n        OneOrMore(wd).setParseAction(' '.join).runTests('''\n            now is the winter of our discontent made glorious summer by this sun of york\n            ''')\n    prints::\n        00 11 22 aa FF 0a 0d 1a\n        [0, 17, 34, 170, 255, 10, 13, 26]\n\n        my kingdom for a horse\n        ['MY', 'KINGDOM', 'FOR', 'A', 'HORSE']\n\n        now is the winter of our discontent made glorious summer by this sun of york\n        ['Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York']\n    \"\"\"\n    def pa(s,l,t):\n        return [func(tokn, *args) for tokn in t]\n\n    try:\n        func_name = getattr(func, '__name__', \n                            getattr(func, '__class__').__name__)\n    except Exception:\n        func_name = str(func)\n    pa.__name__ = func_name\n\n    return pa\n\nupcaseTokens = tokenMap(lambda t: _ustr(t).upper())\n\"\"\"(Deprecated) Helper parse action to convert tokens to upper case. Deprecated in favor of L{pyparsing_common.upcaseTokens}\"\"\"\n\ndowncaseTokens = tokenMap(lambda t: _ustr(t).lower())\n\"\"\"(Deprecated) Helper parse action to convert tokens to lower case. Deprecated in favor of L{pyparsing_common.downcaseTokens}\"\"\"\n    \ndef _makeTags(tagStr, xml):\n    \"\"\"Internal helper to construct opening and closing tag expressions, given a tag name\"\"\"\n    if isinstance(tagStr,basestring):\n        resname = tagStr\n        tagStr = Keyword(tagStr, caseless=not xml)\n    else:\n        resname = tagStr.name\n\n    tagAttrName = Word(alphas,alphanums+\"_-:\")\n    if (xml):\n        tagAttrValue = dblQuotedString.copy().setParseAction( removeQuotes )\n        openTag = Suppress(\"<\") + tagStr(\"tag\") + \\\n                Dict(ZeroOrMore(Group( tagAttrName + Suppress(\"=\") + tagAttrValue ))) + \\\n                Optional(\"/\",default=[False]).setResultsName(\"empty\").setParseAction(lambda s,l,t:t[0]=='/') + Suppress(\">\")\n    else:\n        printablesLessRAbrack = \"\".join(c for c in printables if c not in \">\")\n        tagAttrValue = quotedString.copy().setParseAction( removeQuotes ) | Word(printablesLessRAbrack)\n        openTag = Suppress(\"<\") + tagStr(\"tag\") + \\\n                Dict(ZeroOrMore(Group( tagAttrName.setParseAction(downcaseTokens) + \\\n                Optional( Suppress(\"=\") + tagAttrValue ) ))) + \\\n                Optional(\"/\",default=[False]).setResultsName(\"empty\").setParseAction(lambda s,l,t:t[0]=='/') + Suppress(\">\")\n    closeTag = Combine(_L(\"</\") + tagStr + \">\")\n\n    openTag = openTag.setResultsName(\"start\"+\"\".join(resname.replace(\":\",\" \").title().split())).setName(\"<%s>\" % resname)\n    closeTag = closeTag.setResultsName(\"end\"+\"\".join(resname.replace(\":\",\" \").title().split())).setName(\"</%s>\" % resname)\n    openTag.tag = resname\n    closeTag.tag = resname\n    return openTag, closeTag\n\ndef makeHTMLTags(tagStr):\n    \"\"\"\n    Helper to construct opening and closing tag expressions for HTML, given a tag name. Matches\n    tags in either upper or lower case, attributes with namespaces and with quoted or unquoted values.\n\n    Example::\n        text = '<td>More info at the <a href=\"http://pyparsing.wikispaces.com\">pyparsing</a> wiki page</td>'\n        # makeHTMLTags returns pyparsing expressions for the opening and closing tags as a 2-tuple\n        a,a_end = makeHTMLTags(\"A\")\n        link_expr = a + SkipTo(a_end)(\"link_text\") + a_end\n        \n        for link in link_expr.searchString(text):\n            # attributes in the <A> tag (like \"href\" shown here) are also accessible as named results\n            print(link.link_text, '->', link.href)\n    prints::\n        pyparsing -> http://pyparsing.wikispaces.com\n    \"\"\"\n    return _makeTags( tagStr, False )\n\ndef makeXMLTags(tagStr):\n    \"\"\"\n    Helper to construct opening and closing tag expressions for XML, given a tag name. Matches\n    tags only in the given upper/lower case.\n\n    Example: similar to L{makeHTMLTags}\n    \"\"\"\n    return _makeTags( tagStr, True )\n\ndef withAttribute(*args,**attrDict):\n    \"\"\"\n    Helper to create a validating parse action to be used with start tags created\n    with C{L{makeXMLTags}} or C{L{makeHTMLTags}}. Use C{withAttribute} to qualify a starting tag\n    with a required attribute value, to avoid false matches on common tags such as\n    C{<TD>} or C{<DIV>}.\n\n    Call C{withAttribute} with a series of attribute names and values. Specify the list\n    of filter attributes names and values as:\n     - keyword arguments, as in C{(align=\"right\")}, or\n     - as an explicit dict with C{**} operator, when an attribute name is also a Python\n          reserved word, as in C{**{\"class\":\"Customer\", \"align\":\"right\"}}\n     - a list of name-value tuples, as in ( (\"ns1:class\", \"Customer\"), (\"ns2:align\",\"right\") )\n    For attribute names with a namespace prefix, you must use the second form.  Attribute\n    names are matched insensitive to upper/lower case.\n       \n    If just testing for C{class} (with or without a namespace), use C{L{withClass}}.\n\n    To verify that the attribute exists, but without specifying a value, pass\n    C{withAttribute.ANY_VALUE} as the value.\n\n    Example::\n        html = '''\n            <div>\n            Some text\n            <div type=\"grid\">1 4 0 1 0</div>\n            <div type=\"graph\">1,3 2,3 1,1</div>\n            <div>this has no type</div>\n            </div>\n                \n        '''\n        div,div_end = makeHTMLTags(\"div\")\n\n        # only match div tag having a type attribute with value \"grid\"\n        div_grid = div().setParseAction(withAttribute(type=\"grid\"))\n        grid_expr = div_grid + SkipTo(div | div_end)(\"body\")\n        for grid_header in grid_expr.searchString(html):\n            print(grid_header.body)\n        \n        # construct a match with any div tag having a type attribute, regardless of the value\n        div_any_type = div().setParseAction(withAttribute(type=withAttribute.ANY_VALUE))\n        div_expr = div_any_type + SkipTo(div | div_end)(\"body\")\n        for div_header in div_expr.searchString(html):\n            print(div_header.body)\n    prints::\n        1 4 0 1 0\n\n        1 4 0 1 0\n        1,3 2,3 1,1\n    \"\"\"\n    if args:\n        attrs = args[:]\n    else:\n        attrs = attrDict.items()\n    attrs = [(k,v) for k,v in attrs]\n    def pa(s,l,tokens):\n        for attrName,attrValue in attrs:\n            if attrName not in tokens:\n                raise ParseException(s,l,\"no matching attribute \" + attrName)\n            if attrValue != withAttribute.ANY_VALUE and tokens[attrName] != attrValue:\n                raise ParseException(s,l,\"attribute '%s' has value '%s', must be '%s'\" %\n                                            (attrName, tokens[attrName], attrValue))\n    return pa\nwithAttribute.ANY_VALUE = object()\n\ndef withClass(classname, namespace=''):\n    \"\"\"\n    Simplified version of C{L{withAttribute}} when matching on a div class - made\n    difficult because C{class} is a reserved word in Python.\n\n    Example::\n        html = '''\n            <div>\n            Some text\n            <div class=\"grid\">1 4 0 1 0</div>\n            <div class=\"graph\">1,3 2,3 1,1</div>\n            <div>this &lt;div&gt; has no class</div>\n            </div>\n                \n        '''\n        div,div_end = makeHTMLTags(\"div\")\n        div_grid = div().setParseAction(withClass(\"grid\"))\n        \n        grid_expr = div_grid + SkipTo(div | div_end)(\"body\")\n        for grid_header in grid_expr.searchString(html):\n            print(grid_header.body)\n        \n        div_any_type = div().setParseAction(withClass(withAttribute.ANY_VALUE))\n        div_expr = div_any_type + SkipTo(div | div_end)(\"body\")\n        for div_header in div_expr.searchString(html):\n            print(div_header.body)\n    prints::\n        1 4 0 1 0\n\n        1 4 0 1 0\n        1,3 2,3 1,1\n    \"\"\"\n    classattr = \"%s:class\" % namespace if namespace else \"class\"\n    return withAttribute(**{classattr : classname})        \n\nopAssoc = _Constants()\nopAssoc.LEFT = object()\nopAssoc.RIGHT = object()\n\ndef infixNotation( baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')') ):\n    \"\"\"\n    Helper method for constructing grammars of expressions made up of\n    operators working in a precedence hierarchy.  Operators may be unary or\n    binary, left- or right-associative.  Parse actions can also be attached\n    to operator expressions. The generated parser will also recognize the use \n    of parentheses to override operator precedences (see example below).\n    \n    Note: if you define a deep operator list, you may see performance issues\n    when using infixNotation. See L{ParserElement.enablePackrat} for a\n    mechanism to potentially improve your parser performance.\n\n    Parameters:\n     - baseExpr - expression representing the most basic element for the nested\n     - opList - list of tuples, one for each operator precedence level in the\n      expression grammar; each tuple is of the form\n      (opExpr, numTerms, rightLeftAssoc, parseAction), where:\n       - opExpr is the pyparsing expression for the operator;\n          may also be a string, which will be converted to a Literal;\n          if numTerms is 3, opExpr is a tuple of two expressions, for the\n          two operators separating the 3 terms\n       - numTerms is the number of terms for this operator (must\n          be 1, 2, or 3)\n       - rightLeftAssoc is the indicator whether the operator is\n          right or left associative, using the pyparsing-defined\n          constants C{opAssoc.RIGHT} and C{opAssoc.LEFT}.\n       - parseAction is the parse action to be associated with\n          expressions matching this operator expression (the\n          parse action tuple member may be omitted); if the parse action\n          is passed a tuple or list of functions, this is equivalent to\n          calling C{setParseAction(*fn)} (L{ParserElement.setParseAction})\n     - lpar - expression for matching left-parentheses (default=C{Suppress('(')})\n     - rpar - expression for matching right-parentheses (default=C{Suppress(')')})\n\n    Example::\n        # simple example of four-function arithmetic with ints and variable names\n        integer = pyparsing_common.signed_integer\n        varname = pyparsing_common.identifier \n        \n        arith_expr = infixNotation(integer | varname,\n            [\n            ('-', 1, opAssoc.RIGHT),\n            (oneOf('* /'), 2, opAssoc.LEFT),\n            (oneOf('+ -'), 2, opAssoc.LEFT),\n            ])\n        \n        arith_expr.runTests('''\n            5+3*6\n            (5+3)*6\n            -2--11\n            ''', fullDump=False)\n    prints::\n        5+3*6\n        [[5, '+', [3, '*', 6]]]\n\n        (5+3)*6\n        [[[5, '+', 3], '*', 6]]\n\n        -2--11\n        [[['-', 2], '-', ['-', 11]]]\n    \"\"\"\n    ret = Forward()\n    lastExpr = baseExpr | ( lpar + ret + rpar )\n    for i,operDef in enumerate(opList):\n        opExpr,arity,rightLeftAssoc,pa = (operDef + (None,))[:4]\n        termName = \"%s term\" % opExpr if arity < 3 else \"%s%s term\" % opExpr\n        if arity == 3:\n            if opExpr is None or len(opExpr) != 2:\n                raise ValueError(\"if numterms=3, opExpr must be a tuple or list of two expressions\")\n            opExpr1, opExpr2 = opExpr\n        thisExpr = Forward().setName(termName)\n        if rightLeftAssoc == opAssoc.LEFT:\n            if arity == 1:\n                matchExpr = FollowedBy(lastExpr + opExpr) + Group( lastExpr + OneOrMore( opExpr ) )\n            elif arity == 2:\n                if opExpr is not None:\n                    matchExpr = FollowedBy(lastExpr + opExpr + lastExpr) + Group( lastExpr + OneOrMore( opExpr + lastExpr ) )\n                else:\n                    matchExpr = FollowedBy(lastExpr+lastExpr) + Group( lastExpr + OneOrMore(lastExpr) )\n            elif arity == 3:\n                matchExpr = FollowedBy(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr) + \\\n                            Group( lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr )\n            else:\n                raise ValueError(\"operator must be unary (1), binary (2), or ternary (3)\")\n        elif rightLeftAssoc == opAssoc.RIGHT:\n            if arity == 1:\n                # try to avoid LR with this extra test\n                if not isinstance(opExpr, Optional):\n                    opExpr = Optional(opExpr)\n                matchExpr = FollowedBy(opExpr.expr + thisExpr) + Group( opExpr + thisExpr )\n            elif arity == 2:\n                if opExpr is not None:\n                    matchExpr = FollowedBy(lastExpr + opExpr + thisExpr) + Group( lastExpr + OneOrMore( opExpr + thisExpr ) )\n                else:\n                    matchExpr = FollowedBy(lastExpr + thisExpr) + Group( lastExpr + OneOrMore( thisExpr ) )\n            elif arity == 3:\n                matchExpr = FollowedBy(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr) + \\\n                            Group( lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr )\n            else:\n                raise ValueError(\"operator must be unary (1), binary (2), or ternary (3)\")\n        else:\n            raise ValueError(\"operator must indicate right or left associativity\")\n        if pa:\n            if isinstance(pa, (tuple, list)):\n                matchExpr.setParseAction(*pa)\n            else:\n                matchExpr.setParseAction(pa)\n        thisExpr <<= ( matchExpr.setName(termName) | lastExpr )\n        lastExpr = thisExpr\n    ret <<= lastExpr\n    return ret\n\noperatorPrecedence = infixNotation\n\"\"\"(Deprecated) Former name of C{L{infixNotation}}, will be dropped in a future release.\"\"\"\n\ndblQuotedString = Combine(Regex(r'\"(?:[^\"\\n\\r\\\\]|(?:\"\")|(?:\\\\(?:[^x]|x[0-9a-fA-F]+)))*')+'\"').setName(\"string enclosed in double quotes\")\nsglQuotedString = Combine(Regex(r\"'(?:[^'\\n\\r\\\\]|(?:'')|(?:\\\\(?:[^x]|x[0-9a-fA-F]+)))*\")+\"'\").setName(\"string enclosed in single quotes\")\nquotedString = Combine(Regex(r'\"(?:[^\"\\n\\r\\\\]|(?:\"\")|(?:\\\\(?:[^x]|x[0-9a-fA-F]+)))*')+'\"'|\n                       Regex(r\"'(?:[^'\\n\\r\\\\]|(?:'')|(?:\\\\(?:[^x]|x[0-9a-fA-F]+)))*\")+\"'\").setName(\"quotedString using single or double quotes\")\nunicodeString = Combine(_L('u') + quotedString.copy()).setName(\"unicode string literal\")\n\ndef nestedExpr(opener=\"(\", closer=\")\", content=None, ignoreExpr=quotedString.copy()):\n    \"\"\"\n    Helper method for defining nested lists enclosed in opening and closing\n    delimiters (\"(\" and \")\" are the default).\n\n    Parameters:\n     - opener - opening character for a nested list (default=C{\"(\"}); can also be a pyparsing expression\n     - closer - closing character for a nested list (default=C{\")\"}); can also be a pyparsing expression\n     - content - expression for items within the nested lists (default=C{None})\n     - ignoreExpr - expression for ignoring opening and closing delimiters (default=C{quotedString})\n\n    If an expression is not provided for the content argument, the nested\n    expression will capture all whitespace-delimited content between delimiters\n    as a list of separate values.\n\n    Use the C{ignoreExpr} argument to define expressions that may contain\n    opening or closing characters that should not be treated as opening\n    or closing characters for nesting, such as quotedString or a comment\n    expression.  Specify multiple expressions using an C{L{Or}} or C{L{MatchFirst}}.\n    The default is L{quotedString}, but if no expressions are to be ignored,\n    then pass C{None} for this argument.\n\n    Example::\n        data_type = oneOf(\"void int short long char float double\")\n        decl_data_type = Combine(data_type + Optional(Word('*')))\n        ident = Word(alphas+'_', alphanums+'_')\n        number = pyparsing_common.number\n        arg = Group(decl_data_type + ident)\n        LPAR,RPAR = map(Suppress, \"()\")\n\n        code_body = nestedExpr('{', '}', ignoreExpr=(quotedString | cStyleComment))\n\n        c_function = (decl_data_type(\"type\") \n                      + ident(\"name\")\n                      + LPAR + Optional(delimitedList(arg), [])(\"args\") + RPAR \n                      + code_body(\"body\"))\n        c_function.ignore(cStyleComment)\n        \n        source_code = '''\n            int is_odd(int x) { \n                return (x%2); \n            }\n                \n            int dec_to_hex(char hchar) { \n                if (hchar >= '0' && hchar <= '9') { \n                    return (ord(hchar)-ord('0')); \n                } else { \n                    return (10+ord(hchar)-ord('A'));\n                } \n            }\n        '''\n        for func in c_function.searchString(source_code):\n            print(\"%(name)s (%(type)s) args: %(args)s\" % func)\n\n    prints::\n        is_odd (int) args: [['int', 'x']]\n        dec_to_hex (int) args: [['char', 'hchar']]\n    \"\"\"\n    if opener == closer:\n        raise ValueError(\"opening and closing strings cannot be the same\")\n    if content is None:\n        if isinstance(opener,basestring) and isinstance(closer,basestring):\n            if len(opener) == 1 and len(closer)==1:\n                if ignoreExpr is not None:\n                    content = (Combine(OneOrMore(~ignoreExpr +\n                                    CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS,exact=1))\n                                ).setParseAction(lambda t:t[0].strip()))\n                else:\n                    content = (empty.copy()+CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS\n                                ).setParseAction(lambda t:t[0].strip()))\n            else:\n                if ignoreExpr is not None:\n                    content = (Combine(OneOrMore(~ignoreExpr + \n                                    ~Literal(opener) + ~Literal(closer) +\n                                    CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1))\n                                ).setParseAction(lambda t:t[0].strip()))\n                else:\n                    content = (Combine(OneOrMore(~Literal(opener) + ~Literal(closer) +\n                                    CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1))\n                                ).setParseAction(lambda t:t[0].strip()))\n        else:\n            raise ValueError(\"opening and closing arguments must be strings if no content expression is given\")\n    ret = Forward()\n    if ignoreExpr is not None:\n        ret <<= Group( Suppress(opener) + ZeroOrMore( ignoreExpr | ret | content ) + Suppress(closer) )\n    else:\n        ret <<= Group( Suppress(opener) + ZeroOrMore( ret | content )  + Suppress(closer) )\n    ret.setName('nested %s%s expression' % (opener,closer))\n    return ret\n\ndef indentedBlock(blockStatementExpr, indentStack, indent=True):\n    \"\"\"\n    Helper method for defining space-delimited indentation blocks, such as\n    those used to define block statements in Python source code.\n\n    Parameters:\n     - blockStatementExpr - expression defining syntax of statement that\n            is repeated within the indented block\n     - indentStack - list created by caller to manage indentation stack\n            (multiple statementWithIndentedBlock expressions within a single grammar\n            should share a common indentStack)\n     - indent - boolean indicating whether block must be indented beyond the\n            the current level; set to False for block of left-most statements\n            (default=C{True})\n\n    A valid block must contain at least one C{blockStatement}.\n\n    Example::\n        data = '''\n        def A(z):\n          A1\n          B = 100\n          G = A2\n          A2\n          A3\n        B\n        def BB(a,b,c):\n          BB1\n          def BBA():\n            bba1\n            bba2\n            bba3\n        C\n        D\n        def spam(x,y):\n             def eggs(z):\n                 pass\n        '''\n\n\n        indentStack = [1]\n        stmt = Forward()\n\n        identifier = Word(alphas, alphanums)\n        funcDecl = (\"def\" + identifier + Group( \"(\" + Optional( delimitedList(identifier) ) + \")\" ) + \":\")\n        func_body = indentedBlock(stmt, indentStack)\n        funcDef = Group( funcDecl + func_body )\n\n        rvalue = Forward()\n        funcCall = Group(identifier + \"(\" + Optional(delimitedList(rvalue)) + \")\")\n        rvalue << (funcCall | identifier | Word(nums))\n        assignment = Group(identifier + \"=\" + rvalue)\n        stmt << ( funcDef | assignment | identifier )\n\n        module_body = OneOrMore(stmt)\n\n        parseTree = module_body.parseString(data)\n        parseTree.pprint()\n    prints::\n        [['def',\n          'A',\n          ['(', 'z', ')'],\n          ':',\n          [['A1'], [['B', '=', '100']], [['G', '=', 'A2']], ['A2'], ['A3']]],\n         'B',\n         ['def',\n          'BB',\n          ['(', 'a', 'b', 'c', ')'],\n          ':',\n          [['BB1'], [['def', 'BBA', ['(', ')'], ':', [['bba1'], ['bba2'], ['bba3']]]]]],\n         'C',\n         'D',\n         ['def',\n          'spam',\n          ['(', 'x', 'y', ')'],\n          ':',\n          [[['def', 'eggs', ['(', 'z', ')'], ':', [['pass']]]]]]] \n    \"\"\"\n    def checkPeerIndent(s,l,t):\n        if l >= len(s): return\n        curCol = col(l,s)\n        if curCol != indentStack[-1]:\n            if curCol > indentStack[-1]:\n                raise ParseFatalException(s,l,\"illegal nesting\")\n            raise ParseException(s,l,\"not a peer entry\")\n\n    def checkSubIndent(s,l,t):\n        curCol = col(l,s)\n        if curCol > indentStack[-1]:\n            indentStack.append( curCol )\n        else:\n            raise ParseException(s,l,\"not a subentry\")\n\n    def checkUnindent(s,l,t):\n        if l >= len(s): return\n        curCol = col(l,s)\n        if not(indentStack and curCol < indentStack[-1] and curCol <= indentStack[-2]):\n            raise ParseException(s,l,\"not an unindent\")\n        indentStack.pop()\n\n    NL = OneOrMore(LineEnd().setWhitespaceChars(\"\\t \").suppress())\n    INDENT = (Empty() + Empty().setParseAction(checkSubIndent)).setName('INDENT')\n    PEER   = Empty().setParseAction(checkPeerIndent).setName('')\n    UNDENT = Empty().setParseAction(checkUnindent).setName('UNINDENT')\n    if indent:\n        smExpr = Group( Optional(NL) +\n            #~ FollowedBy(blockStatementExpr) +\n            INDENT + (OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) + UNDENT)\n    else:\n        smExpr = Group( Optional(NL) +\n            (OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) )\n    blockStatementExpr.ignore(_bslash + LineEnd())\n    return smExpr.setName('indented block')\n\nalphas8bit = srange(r\"[\\0xc0-\\0xd6\\0xd8-\\0xf6\\0xf8-\\0xff]\")\npunc8bit = srange(r\"[\\0xa1-\\0xbf\\0xd7\\0xf7]\")\n\nanyOpenTag,anyCloseTag = makeHTMLTags(Word(alphas,alphanums+\"_:\").setName('any tag'))\n_htmlEntityMap = dict(zip(\"gt lt amp nbsp quot apos\".split(),'><& \"\\''))\ncommonHTMLEntity = Regex('&(?P<entity>' + '|'.join(_htmlEntityMap.keys()) +\");\").setName(\"common HTML entity\")\ndef replaceHTMLEntity(t):\n    \"\"\"Helper parser action to replace common HTML entities with their special characters\"\"\"\n    return _htmlEntityMap.get(t.entity)\n\n# it's easy to get these comment structures wrong - they're very common, so may as well make them available\ncStyleComment = Combine(Regex(r\"/\\*(?:[^*]|\\*(?!/))*\") + '*/').setName(\"C style comment\")\n\"Comment of the form C{/* ... */}\"\n\nhtmlComment = Regex(r\"<!--[\\s\\S]*?-->\").setName(\"HTML comment\")\n\"Comment of the form C{<!-- ... -->}\"\n\nrestOfLine = Regex(r\".*\").leaveWhitespace().setName(\"rest of line\")\ndblSlashComment = Regex(r\"//(?:\\\\\\n|[^\\n])*\").setName(\"// comment\")\n\"Comment of the form C{// ... (to end of line)}\"\n\ncppStyleComment = Combine(Regex(r\"/\\*(?:[^*]|\\*(?!/))*\") + '*/'| dblSlashComment).setName(\"C++ style comment\")\n\"Comment of either form C{L{cStyleComment}} or C{L{dblSlashComment}}\"\n\njavaStyleComment = cppStyleComment\n\"Same as C{L{cppStyleComment}}\"\n\npythonStyleComment = Regex(r\"#.*\").setName(\"Python style comment\")\n\"Comment of the form C{# ... (to end of line)}\"\n\n_commasepitem = Combine(OneOrMore(Word(printables, excludeChars=',') +\n                                  Optional( Word(\" \\t\") +\n                                            ~Literal(\",\") + ~LineEnd() ) ) ).streamline().setName(\"commaItem\")\ncommaSeparatedList = delimitedList( Optional( quotedString.copy() | _commasepitem, default=\"\") ).setName(\"commaSeparatedList\")\n\"\"\"(Deprecated) Predefined expression of 1 or more printable words or quoted strings, separated by commas.\n   This expression is deprecated in favor of L{pyparsing_common.comma_separated_list}.\"\"\"\n\n# some other useful expressions - using lower-case class name since we are really using this as a namespace\nclass pyparsing_common:\n    \"\"\"\n    Here are some common low-level expressions that may be useful in jump-starting parser development:\n     - numeric forms (L{integers<integer>}, L{reals<real>}, L{scientific notation<sci_real>})\n     - common L{programming identifiers<identifier>}\n     - network addresses (L{MAC<mac_address>}, L{IPv4<ipv4_address>}, L{IPv6<ipv6_address>})\n     - ISO8601 L{dates<iso8601_date>} and L{datetime<iso8601_datetime>}\n     - L{UUID<uuid>}\n     - L{comma-separated list<comma_separated_list>}\n    Parse actions:\n     - C{L{convertToInteger}}\n     - C{L{convertToFloat}}\n     - C{L{convertToDate}}\n     - C{L{convertToDatetime}}\n     - C{L{stripHTMLTags}}\n     - C{L{upcaseTokens}}\n     - C{L{downcaseTokens}}\n\n    Example::\n        pyparsing_common.number.runTests('''\n            # any int or real number, returned as the appropriate type\n            100\n            -100\n            +100\n            3.14159\n            6.02e23\n            1e-12\n            ''')\n\n        pyparsing_common.fnumber.runTests('''\n            # any int or real number, returned as float\n            100\n            -100\n            +100\n            3.14159\n            6.02e23\n            1e-12\n            ''')\n\n        pyparsing_common.hex_integer.runTests('''\n            # hex numbers\n            100\n            FF\n            ''')\n\n        pyparsing_common.fraction.runTests('''\n            # fractions\n            1/2\n            -3/4\n            ''')\n\n        pyparsing_common.mixed_integer.runTests('''\n            # mixed fractions\n            1\n            1/2\n            -3/4\n            1-3/4\n            ''')\n\n        import uuid\n        pyparsing_common.uuid.setParseAction(tokenMap(uuid.UUID))\n        pyparsing_common.uuid.runTests('''\n            # uuid\n            12345678-1234-5678-1234-567812345678\n            ''')\n    prints::\n        # any int or real number, returned as the appropriate type\n        100\n        [100]\n\n        -100\n        [-100]\n\n        +100\n        [100]\n\n        3.14159\n        [3.14159]\n\n        6.02e23\n        [6.02e+23]\n\n        1e-12\n        [1e-12]\n\n        # any int or real number, returned as float\n        100\n        [100.0]\n\n        -100\n        [-100.0]\n\n        +100\n        [100.0]\n\n        3.14159\n        [3.14159]\n\n        6.02e23\n        [6.02e+23]\n\n        1e-12\n        [1e-12]\n\n        # hex numbers\n        100\n        [256]\n\n        FF\n        [255]\n\n        # fractions\n        1/2\n        [0.5]\n\n        -3/4\n        [-0.75]\n\n        # mixed fractions\n        1\n        [1]\n\n        1/2\n        [0.5]\n\n        -3/4\n        [-0.75]\n\n        1-3/4\n        [1.75]\n\n        # uuid\n        12345678-1234-5678-1234-567812345678\n        [UUID('12345678-1234-5678-1234-567812345678')]\n    \"\"\"\n\n    convertToInteger = tokenMap(int)\n    \"\"\"\n    Parse action for converting parsed integers to Python int\n    \"\"\"\n\n    convertToFloat = tokenMap(float)\n    \"\"\"\n    Parse action for converting parsed numbers to Python float\n    \"\"\"\n\n    integer = Word(nums).setName(\"integer\").setParseAction(convertToInteger)\n    \"\"\"expression that parses an unsigned integer, returns an int\"\"\"\n\n    hex_integer = Word(hexnums).setName(\"hex integer\").setParseAction(tokenMap(int,16))\n    \"\"\"expression that parses a hexadecimal integer, returns an int\"\"\"\n\n    signed_integer = Regex(r'[+-]?\\d+').setName(\"signed integer\").setParseAction(convertToInteger)\n    \"\"\"expression that parses an integer with optional leading sign, returns an int\"\"\"\n\n    fraction = (signed_integer().setParseAction(convertToFloat) + '/' + signed_integer().setParseAction(convertToFloat)).setName(\"fraction\")\n    \"\"\"fractional expression of an integer divided by an integer, returns a float\"\"\"\n    fraction.addParseAction(lambda t: t[0]/t[-1])\n\n    mixed_integer = (fraction | signed_integer + Optional(Optional('-').suppress() + fraction)).setName(\"fraction or mixed integer-fraction\")\n    \"\"\"mixed integer of the form 'integer - fraction', with optional leading integer, returns float\"\"\"\n    mixed_integer.addParseAction(sum)\n\n    real = Regex(r'[+-]?\\d+\\.\\d*').setName(\"real number\").setParseAction(convertToFloat)\n    \"\"\"expression that parses a floating point number and returns a float\"\"\"\n\n    sci_real = Regex(r'[+-]?\\d+([eE][+-]?\\d+|\\.\\d*([eE][+-]?\\d+)?)').setName(\"real number with scientific notation\").setParseAction(convertToFloat)\n    \"\"\"expression that parses a floating point number with optional scientific notation and returns a float\"\"\"\n\n    # streamlining this expression makes the docs nicer-looking\n    number = (sci_real | real | signed_integer).streamline()\n    \"\"\"any numeric expression, returns the corresponding Python type\"\"\"\n\n    fnumber = Regex(r'[+-]?\\d+\\.?\\d*([eE][+-]?\\d+)?').setName(\"fnumber\").setParseAction(convertToFloat)\n    \"\"\"any int or real number, returned as float\"\"\"\n    \n    identifier = Word(alphas+'_', alphanums+'_').setName(\"identifier\")\n    \"\"\"typical code identifier (leading alpha or '_', followed by 0 or more alphas, nums, or '_')\"\"\"\n    \n    ipv4_address = Regex(r'(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})(\\.(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})){3}').setName(\"IPv4 address\")\n    \"IPv4 address (C{0.0.0.0 - 255.255.255.255})\"\n\n    _ipv6_part = Regex(r'[0-9a-fA-F]{1,4}').setName(\"hex_integer\")\n    _full_ipv6_address = (_ipv6_part + (':' + _ipv6_part)*7).setName(\"full IPv6 address\")\n    _short_ipv6_address = (Optional(_ipv6_part + (':' + _ipv6_part)*(0,6)) + \"::\" + Optional(_ipv6_part + (':' + _ipv6_part)*(0,6))).setName(\"short IPv6 address\")\n    _short_ipv6_address.addCondition(lambda t: sum(1 for tt in t if pyparsing_common._ipv6_part.matches(tt)) < 8)\n    _mixed_ipv6_address = (\"::ffff:\" + ipv4_address).setName(\"mixed IPv6 address\")\n    ipv6_address = Combine((_full_ipv6_address | _mixed_ipv6_address | _short_ipv6_address).setName(\"IPv6 address\")).setName(\"IPv6 address\")\n    \"IPv6 address (long, short, or mixed form)\"\n    \n    mac_address = Regex(r'[0-9a-fA-F]{2}([:.-])[0-9a-fA-F]{2}(?:\\1[0-9a-fA-F]{2}){4}').setName(\"MAC address\")\n    \"MAC address xx:xx:xx:xx:xx (may also have '-' or '.' delimiters)\"\n\n    @staticmethod\n    def convertToDate(fmt=\"%Y-%m-%d\"):\n        \"\"\"\n        Helper to create a parse action for converting parsed date string to Python datetime.date\n\n        Params -\n         - fmt - format to be passed to datetime.strptime (default=C{\"%Y-%m-%d\"})\n\n        Example::\n            date_expr = pyparsing_common.iso8601_date.copy()\n            date_expr.setParseAction(pyparsing_common.convertToDate())\n            print(date_expr.parseString(\"1999-12-31\"))\n        prints::\n            [datetime.date(1999, 12, 31)]\n        \"\"\"\n        def cvt_fn(s,l,t):\n            try:\n                return datetime.strptime(t[0], fmt).date()\n            except ValueError as ve:\n                raise ParseException(s, l, str(ve))\n        return cvt_fn\n\n    @staticmethod\n    def convertToDatetime(fmt=\"%Y-%m-%dT%H:%M:%S.%f\"):\n        \"\"\"\n        Helper to create a parse action for converting parsed datetime string to Python datetime.datetime\n\n        Params -\n         - fmt - format to be passed to datetime.strptime (default=C{\"%Y-%m-%dT%H:%M:%S.%f\"})\n\n        Example::\n            dt_expr = pyparsing_common.iso8601_datetime.copy()\n            dt_expr.setParseAction(pyparsing_common.convertToDatetime())\n            print(dt_expr.parseString(\"1999-12-31T23:59:59.999\"))\n        prints::\n            [datetime.datetime(1999, 12, 31, 23, 59, 59, 999000)]\n        \"\"\"\n        def cvt_fn(s,l,t):\n            try:\n                return datetime.strptime(t[0], fmt)\n            except ValueError as ve:\n                raise ParseException(s, l, str(ve))\n        return cvt_fn\n\n    iso8601_date = Regex(r'(?P<year>\\d{4})(?:-(?P<month>\\d\\d)(?:-(?P<day>\\d\\d))?)?').setName(\"ISO8601 date\")\n    \"ISO8601 date (C{yyyy-mm-dd})\"\n\n    iso8601_datetime = Regex(r'(?P<year>\\d{4})-(?P<month>\\d\\d)-(?P<day>\\d\\d)[T ](?P<hour>\\d\\d):(?P<minute>\\d\\d)(:(?P<second>\\d\\d(\\.\\d*)?)?)?(?P<tz>Z|[+-]\\d\\d:?\\d\\d)?').setName(\"ISO8601 datetime\")\n    \"ISO8601 datetime (C{yyyy-mm-ddThh:mm:ss.s(Z|+-00:00)}) - trailing seconds, milliseconds, and timezone optional; accepts separating C{'T'} or C{' '}\"\n\n    uuid = Regex(r'[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}').setName(\"UUID\")\n    \"UUID (C{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx})\"\n\n    _html_stripper = anyOpenTag.suppress() | anyCloseTag.suppress()\n    @staticmethod\n    def stripHTMLTags(s, l, tokens):\n        \"\"\"\n        Parse action to remove HTML tags from web page HTML source\n\n        Example::\n            # strip HTML links from normal text \n            text = '<td>More info at the <a href=\"http://pyparsing.wikispaces.com\">pyparsing</a> wiki page</td>'\n            td,td_end = makeHTMLTags(\"TD\")\n            table_text = td + SkipTo(td_end).setParseAction(pyparsing_common.stripHTMLTags)(\"body\") + td_end\n            \n            print(table_text.parseString(text).body) # -> 'More info at the pyparsing wiki page'\n        \"\"\"\n        return pyparsing_common._html_stripper.transformString(tokens[0])\n\n    _commasepitem = Combine(OneOrMore(~Literal(\",\") + ~LineEnd() + Word(printables, excludeChars=',') \n                                        + Optional( White(\" \\t\") ) ) ).streamline().setName(\"commaItem\")\n    comma_separated_list = delimitedList( Optional( quotedString.copy() | _commasepitem, default=\"\") ).setName(\"comma separated list\")\n    \"\"\"Predefined expression of 1 or more printable words or quoted strings, separated by commas.\"\"\"\n\n    upcaseTokens = staticmethod(tokenMap(lambda t: _ustr(t).upper()))\n    \"\"\"Parse action to convert tokens to upper case.\"\"\"\n\n    downcaseTokens = staticmethod(tokenMap(lambda t: _ustr(t).lower()))\n    \"\"\"Parse action to convert tokens to lower case.\"\"\"\n\n\nif __name__ == \"__main__\":\n\n    selectToken    = CaselessLiteral(\"select\")\n    fromToken      = CaselessLiteral(\"from\")\n\n    ident          = Word(alphas, alphanums + \"_$\")\n\n    columnName     = delimitedList(ident, \".\", combine=True).setParseAction(upcaseTokens)\n    columnNameList = Group(delimitedList(columnName)).setName(\"columns\")\n    columnSpec     = ('*' | columnNameList)\n\n    tableName      = delimitedList(ident, \".\", combine=True).setParseAction(upcaseTokens)\n    tableNameList  = Group(delimitedList(tableName)).setName(\"tables\")\n    \n    simpleSQL      = selectToken(\"command\") + columnSpec(\"columns\") + fromToken + tableNameList(\"tables\")\n\n    # demo runTests method, including embedded comments in test string\n    simpleSQL.runTests(\"\"\"\n        # '*' as column list and dotted table name\n        select * from SYS.XYZZY\n\n        # caseless match on \"SELECT\", and casts back to \"select\"\n        SELECT * from XYZZY, ABC\n\n        # list of column names, and mixed case SELECT keyword\n        Select AA,BB,CC from Sys.dual\n\n        # multiple tables\n        Select A, B, C from Sys.dual, Table2\n\n        # invalid SELECT keyword - should fail\n        Xelect A, B, C from Sys.dual\n\n        # incomplete command - should fail\n        Select\n\n        # invalid column name - should fail\n        Select ^^^ frox Sys.dual\n\n        \"\"\")\n\n    pyparsing_common.number.runTests(\"\"\"\n        100\n        -100\n        +100\n        3.14159\n        6.02e23\n        1e-12\n        \"\"\")\n\n    # any int or real number, returned as float\n    pyparsing_common.fnumber.runTests(\"\"\"\n        100\n        -100\n        +100\n        3.14159\n        6.02e23\n        1e-12\n        \"\"\")\n\n    pyparsing_common.hex_integer.runTests(\"\"\"\n        100\n        FF\n        \"\"\")\n\n    import uuid\n    pyparsing_common.uuid.setParseAction(tokenMap(uuid.UUID))\n    pyparsing_common.uuid.runTests(\"\"\"\n        12345678-1234-5678-1234-567812345678\n        \"\"\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/_vendor/six.py",
    "content": "\"\"\"Utilities for writing code that runs on Python 2 and 3\"\"\"\n\n# Copyright (c) 2010-2015 Benjamin Peterson\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom __future__ import absolute_import\n\nimport functools\nimport itertools\nimport operator\nimport sys\nimport types\n\n__author__ = \"Benjamin Peterson <benjamin@python.org>\"\n__version__ = \"1.10.0\"\n\n\n# Useful for very coarse version differentiation.\nPY2 = sys.version_info[0] == 2\nPY3 = sys.version_info[0] == 3\nPY34 = sys.version_info[0:2] >= (3, 4)\n\nif PY3:\n    string_types = str,\n    integer_types = int,\n    class_types = type,\n    text_type = str\n    binary_type = bytes\n\n    MAXSIZE = sys.maxsize\nelse:\n    string_types = basestring,\n    integer_types = (int, long)\n    class_types = (type, types.ClassType)\n    text_type = unicode\n    binary_type = str\n\n    if sys.platform.startswith(\"java\"):\n        # Jython always uses 32 bits.\n        MAXSIZE = int((1 << 31) - 1)\n    else:\n        # It's possible to have sizeof(long) != sizeof(Py_ssize_t).\n        class X(object):\n\n            def __len__(self):\n                return 1 << 31\n        try:\n            len(X())\n        except OverflowError:\n            # 32-bit\n            MAXSIZE = int((1 << 31) - 1)\n        else:\n            # 64-bit\n            MAXSIZE = int((1 << 63) - 1)\n        del X\n\n\ndef _add_doc(func, doc):\n    \"\"\"Add documentation to a function.\"\"\"\n    func.__doc__ = doc\n\n\ndef _import_module(name):\n    \"\"\"Import module, returning the module after the last dot.\"\"\"\n    __import__(name)\n    return sys.modules[name]\n\n\nclass _LazyDescr(object):\n\n    def __init__(self, name):\n        self.name = name\n\n    def __get__(self, obj, tp):\n        result = self._resolve()\n        setattr(obj, self.name, result)  # Invokes __set__.\n        try:\n            # This is a bit ugly, but it avoids running this again by\n            # removing this descriptor.\n            delattr(obj.__class__, self.name)\n        except AttributeError:\n            pass\n        return result\n\n\nclass MovedModule(_LazyDescr):\n\n    def __init__(self, name, old, new=None):\n        super(MovedModule, self).__init__(name)\n        if PY3:\n            if new is None:\n                new = name\n            self.mod = new\n        else:\n            self.mod = old\n\n    def _resolve(self):\n        return _import_module(self.mod)\n\n    def __getattr__(self, attr):\n        _module = self._resolve()\n        value = getattr(_module, attr)\n        setattr(self, attr, value)\n        return value\n\n\nclass _LazyModule(types.ModuleType):\n\n    def __init__(self, name):\n        super(_LazyModule, self).__init__(name)\n        self.__doc__ = self.__class__.__doc__\n\n    def __dir__(self):\n        attrs = [\"__doc__\", \"__name__\"]\n        attrs += [attr.name for attr in self._moved_attributes]\n        return attrs\n\n    # Subclasses should override this\n    _moved_attributes = []\n\n\nclass MovedAttribute(_LazyDescr):\n\n    def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):\n        super(MovedAttribute, self).__init__(name)\n        if PY3:\n            if new_mod is None:\n                new_mod = name\n            self.mod = new_mod\n            if new_attr is None:\n                if old_attr is None:\n                    new_attr = name\n                else:\n                    new_attr = old_attr\n            self.attr = new_attr\n        else:\n            self.mod = old_mod\n            if old_attr is None:\n                old_attr = name\n            self.attr = old_attr\n\n    def _resolve(self):\n        module = _import_module(self.mod)\n        return getattr(module, self.attr)\n\n\nclass _SixMetaPathImporter(object):\n\n    \"\"\"\n    A meta path importer to import six.moves and its submodules.\n\n    This class implements a PEP302 finder and loader. It should be compatible\n    with Python 2.5 and all existing versions of Python3\n    \"\"\"\n\n    def __init__(self, six_module_name):\n        self.name = six_module_name\n        self.known_modules = {}\n\n    def _add_module(self, mod, *fullnames):\n        for fullname in fullnames:\n            self.known_modules[self.name + \".\" + fullname] = mod\n\n    def _get_module(self, fullname):\n        return self.known_modules[self.name + \".\" + fullname]\n\n    def find_module(self, fullname, path=None):\n        if fullname in self.known_modules:\n            return self\n        return None\n\n    def __get_module(self, fullname):\n        try:\n            return self.known_modules[fullname]\n        except KeyError:\n            raise ImportError(\"This loader does not know module \" + fullname)\n\n    def load_module(self, fullname):\n        try:\n            # in case of a reload\n            return sys.modules[fullname]\n        except KeyError:\n            pass\n        mod = self.__get_module(fullname)\n        if isinstance(mod, MovedModule):\n            mod = mod._resolve()\n        else:\n            mod.__loader__ = self\n        sys.modules[fullname] = mod\n        return mod\n\n    def is_package(self, fullname):\n        \"\"\"\n        Return true, if the named module is a package.\n\n        We need this method to get correct spec objects with\n        Python 3.4 (see PEP451)\n        \"\"\"\n        return hasattr(self.__get_module(fullname), \"__path__\")\n\n    def get_code(self, fullname):\n        \"\"\"Return None\n\n        Required, if is_package is implemented\"\"\"\n        self.__get_module(fullname)  # eventually raises ImportError\n        return None\n    get_source = get_code  # same as get_code\n\n_importer = _SixMetaPathImporter(__name__)\n\n\nclass _MovedItems(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects\"\"\"\n    __path__ = []  # mark as package\n\n\n_moved_attributes = [\n    MovedAttribute(\"cStringIO\", \"cStringIO\", \"io\", \"StringIO\"),\n    MovedAttribute(\"filter\", \"itertools\", \"builtins\", \"ifilter\", \"filter\"),\n    MovedAttribute(\"filterfalse\", \"itertools\", \"itertools\", \"ifilterfalse\", \"filterfalse\"),\n    MovedAttribute(\"input\", \"__builtin__\", \"builtins\", \"raw_input\", \"input\"),\n    MovedAttribute(\"intern\", \"__builtin__\", \"sys\"),\n    MovedAttribute(\"map\", \"itertools\", \"builtins\", \"imap\", \"map\"),\n    MovedAttribute(\"getcwd\", \"os\", \"os\", \"getcwdu\", \"getcwd\"),\n    MovedAttribute(\"getcwdb\", \"os\", \"os\", \"getcwd\", \"getcwdb\"),\n    MovedAttribute(\"range\", \"__builtin__\", \"builtins\", \"xrange\", \"range\"),\n    MovedAttribute(\"reload_module\", \"__builtin__\", \"importlib\" if PY34 else \"imp\", \"reload\"),\n    MovedAttribute(\"reduce\", \"__builtin__\", \"functools\"),\n    MovedAttribute(\"shlex_quote\", \"pipes\", \"shlex\", \"quote\"),\n    MovedAttribute(\"StringIO\", \"StringIO\", \"io\"),\n    MovedAttribute(\"UserDict\", \"UserDict\", \"collections\"),\n    MovedAttribute(\"UserList\", \"UserList\", \"collections\"),\n    MovedAttribute(\"UserString\", \"UserString\", \"collections\"),\n    MovedAttribute(\"xrange\", \"__builtin__\", \"builtins\", \"xrange\", \"range\"),\n    MovedAttribute(\"zip\", \"itertools\", \"builtins\", \"izip\", \"zip\"),\n    MovedAttribute(\"zip_longest\", \"itertools\", \"itertools\", \"izip_longest\", \"zip_longest\"),\n    MovedModule(\"builtins\", \"__builtin__\"),\n    MovedModule(\"configparser\", \"ConfigParser\"),\n    MovedModule(\"copyreg\", \"copy_reg\"),\n    MovedModule(\"dbm_gnu\", \"gdbm\", \"dbm.gnu\"),\n    MovedModule(\"_dummy_thread\", \"dummy_thread\", \"_dummy_thread\"),\n    MovedModule(\"http_cookiejar\", \"cookielib\", \"http.cookiejar\"),\n    MovedModule(\"http_cookies\", \"Cookie\", \"http.cookies\"),\n    MovedModule(\"html_entities\", \"htmlentitydefs\", \"html.entities\"),\n    MovedModule(\"html_parser\", \"HTMLParser\", \"html.parser\"),\n    MovedModule(\"http_client\", \"httplib\", \"http.client\"),\n    MovedModule(\"email_mime_multipart\", \"email.MIMEMultipart\", \"email.mime.multipart\"),\n    MovedModule(\"email_mime_nonmultipart\", \"email.MIMENonMultipart\", \"email.mime.nonmultipart\"),\n    MovedModule(\"email_mime_text\", \"email.MIMEText\", \"email.mime.text\"),\n    MovedModule(\"email_mime_base\", \"email.MIMEBase\", \"email.mime.base\"),\n    MovedModule(\"BaseHTTPServer\", \"BaseHTTPServer\", \"http.server\"),\n    MovedModule(\"CGIHTTPServer\", \"CGIHTTPServer\", \"http.server\"),\n    MovedModule(\"SimpleHTTPServer\", \"SimpleHTTPServer\", \"http.server\"),\n    MovedModule(\"cPickle\", \"cPickle\", \"pickle\"),\n    MovedModule(\"queue\", \"Queue\"),\n    MovedModule(\"reprlib\", \"repr\"),\n    MovedModule(\"socketserver\", \"SocketServer\"),\n    MovedModule(\"_thread\", \"thread\", \"_thread\"),\n    MovedModule(\"tkinter\", \"Tkinter\"),\n    MovedModule(\"tkinter_dialog\", \"Dialog\", \"tkinter.dialog\"),\n    MovedModule(\"tkinter_filedialog\", \"FileDialog\", \"tkinter.filedialog\"),\n    MovedModule(\"tkinter_scrolledtext\", \"ScrolledText\", \"tkinter.scrolledtext\"),\n    MovedModule(\"tkinter_simpledialog\", \"SimpleDialog\", \"tkinter.simpledialog\"),\n    MovedModule(\"tkinter_tix\", \"Tix\", \"tkinter.tix\"),\n    MovedModule(\"tkinter_ttk\", \"ttk\", \"tkinter.ttk\"),\n    MovedModule(\"tkinter_constants\", \"Tkconstants\", \"tkinter.constants\"),\n    MovedModule(\"tkinter_dnd\", \"Tkdnd\", \"tkinter.dnd\"),\n    MovedModule(\"tkinter_colorchooser\", \"tkColorChooser\",\n                \"tkinter.colorchooser\"),\n    MovedModule(\"tkinter_commondialog\", \"tkCommonDialog\",\n                \"tkinter.commondialog\"),\n    MovedModule(\"tkinter_tkfiledialog\", \"tkFileDialog\", \"tkinter.filedialog\"),\n    MovedModule(\"tkinter_font\", \"tkFont\", \"tkinter.font\"),\n    MovedModule(\"tkinter_messagebox\", \"tkMessageBox\", \"tkinter.messagebox\"),\n    MovedModule(\"tkinter_tksimpledialog\", \"tkSimpleDialog\",\n                \"tkinter.simpledialog\"),\n    MovedModule(\"urllib_parse\", __name__ + \".moves.urllib_parse\", \"urllib.parse\"),\n    MovedModule(\"urllib_error\", __name__ + \".moves.urllib_error\", \"urllib.error\"),\n    MovedModule(\"urllib\", __name__ + \".moves.urllib\", __name__ + \".moves.urllib\"),\n    MovedModule(\"urllib_robotparser\", \"robotparser\", \"urllib.robotparser\"),\n    MovedModule(\"xmlrpc_client\", \"xmlrpclib\", \"xmlrpc.client\"),\n    MovedModule(\"xmlrpc_server\", \"SimpleXMLRPCServer\", \"xmlrpc.server\"),\n]\n# Add windows specific modules.\nif sys.platform == \"win32\":\n    _moved_attributes += [\n        MovedModule(\"winreg\", \"_winreg\"),\n    ]\n\nfor attr in _moved_attributes:\n    setattr(_MovedItems, attr.name, attr)\n    if isinstance(attr, MovedModule):\n        _importer._add_module(attr, \"moves.\" + attr.name)\ndel attr\n\n_MovedItems._moved_attributes = _moved_attributes\n\nmoves = _MovedItems(__name__ + \".moves\")\n_importer._add_module(moves, \"moves\")\n\n\nclass Module_six_moves_urllib_parse(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_parse\"\"\"\n\n\n_urllib_parse_moved_attributes = [\n    MovedAttribute(\"ParseResult\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"SplitResult\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"parse_qs\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"parse_qsl\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urldefrag\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urljoin\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlparse\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlsplit\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlunparse\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlunsplit\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"quote\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"quote_plus\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"unquote\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"unquote_plus\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"urlencode\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splitquery\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splittag\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splituser\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"uses_fragment\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_netloc\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_params\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_query\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_relative\", \"urlparse\", \"urllib.parse\"),\n]\nfor attr in _urllib_parse_moved_attributes:\n    setattr(Module_six_moves_urllib_parse, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes\n\n_importer._add_module(Module_six_moves_urllib_parse(__name__ + \".moves.urllib_parse\"),\n                      \"moves.urllib_parse\", \"moves.urllib.parse\")\n\n\nclass Module_six_moves_urllib_error(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_error\"\"\"\n\n\n_urllib_error_moved_attributes = [\n    MovedAttribute(\"URLError\", \"urllib2\", \"urllib.error\"),\n    MovedAttribute(\"HTTPError\", \"urllib2\", \"urllib.error\"),\n    MovedAttribute(\"ContentTooShortError\", \"urllib\", \"urllib.error\"),\n]\nfor attr in _urllib_error_moved_attributes:\n    setattr(Module_six_moves_urllib_error, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes\n\n_importer._add_module(Module_six_moves_urllib_error(__name__ + \".moves.urllib.error\"),\n                      \"moves.urllib_error\", \"moves.urllib.error\")\n\n\nclass Module_six_moves_urllib_request(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_request\"\"\"\n\n\n_urllib_request_moved_attributes = [\n    MovedAttribute(\"urlopen\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"install_opener\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"build_opener\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"pathname2url\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"url2pathname\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"getproxies\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"Request\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"OpenerDirector\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPDefaultErrorHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPRedirectHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPCookieProcessor\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"ProxyHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"BaseHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPPasswordMgr\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPPasswordMgrWithDefaultRealm\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"AbstractBasicAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPBasicAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"ProxyBasicAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"AbstractDigestAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPDigestAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"ProxyDigestAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPSHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"FileHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"FTPHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"CacheFTPHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"UnknownHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPErrorProcessor\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"urlretrieve\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"urlcleanup\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"URLopener\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"FancyURLopener\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"proxy_bypass\", \"urllib\", \"urllib.request\"),\n]\nfor attr in _urllib_request_moved_attributes:\n    setattr(Module_six_moves_urllib_request, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes\n\n_importer._add_module(Module_six_moves_urllib_request(__name__ + \".moves.urllib.request\"),\n                      \"moves.urllib_request\", \"moves.urllib.request\")\n\n\nclass Module_six_moves_urllib_response(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_response\"\"\"\n\n\n_urllib_response_moved_attributes = [\n    MovedAttribute(\"addbase\", \"urllib\", \"urllib.response\"),\n    MovedAttribute(\"addclosehook\", \"urllib\", \"urllib.response\"),\n    MovedAttribute(\"addinfo\", \"urllib\", \"urllib.response\"),\n    MovedAttribute(\"addinfourl\", \"urllib\", \"urllib.response\"),\n]\nfor attr in _urllib_response_moved_attributes:\n    setattr(Module_six_moves_urllib_response, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes\n\n_importer._add_module(Module_six_moves_urllib_response(__name__ + \".moves.urllib.response\"),\n                      \"moves.urllib_response\", \"moves.urllib.response\")\n\n\nclass Module_six_moves_urllib_robotparser(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_robotparser\"\"\"\n\n\n_urllib_robotparser_moved_attributes = [\n    MovedAttribute(\"RobotFileParser\", \"robotparser\", \"urllib.robotparser\"),\n]\nfor attr in _urllib_robotparser_moved_attributes:\n    setattr(Module_six_moves_urllib_robotparser, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes\n\n_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + \".moves.urllib.robotparser\"),\n                      \"moves.urllib_robotparser\", \"moves.urllib.robotparser\")\n\n\nclass Module_six_moves_urllib(types.ModuleType):\n\n    \"\"\"Create a six.moves.urllib namespace that resembles the Python 3 namespace\"\"\"\n    __path__ = []  # mark as package\n    parse = _importer._get_module(\"moves.urllib_parse\")\n    error = _importer._get_module(\"moves.urllib_error\")\n    request = _importer._get_module(\"moves.urllib_request\")\n    response = _importer._get_module(\"moves.urllib_response\")\n    robotparser = _importer._get_module(\"moves.urllib_robotparser\")\n\n    def __dir__(self):\n        return ['parse', 'error', 'request', 'response', 'robotparser']\n\n_importer._add_module(Module_six_moves_urllib(__name__ + \".moves.urllib\"),\n                      \"moves.urllib\")\n\n\ndef add_move(move):\n    \"\"\"Add an item to six.moves.\"\"\"\n    setattr(_MovedItems, move.name, move)\n\n\ndef remove_move(name):\n    \"\"\"Remove item from six.moves.\"\"\"\n    try:\n        delattr(_MovedItems, name)\n    except AttributeError:\n        try:\n            del moves.__dict__[name]\n        except KeyError:\n            raise AttributeError(\"no such move, %r\" % (name,))\n\n\nif PY3:\n    _meth_func = \"__func__\"\n    _meth_self = \"__self__\"\n\n    _func_closure = \"__closure__\"\n    _func_code = \"__code__\"\n    _func_defaults = \"__defaults__\"\n    _func_globals = \"__globals__\"\nelse:\n    _meth_func = \"im_func\"\n    _meth_self = \"im_self\"\n\n    _func_closure = \"func_closure\"\n    _func_code = \"func_code\"\n    _func_defaults = \"func_defaults\"\n    _func_globals = \"func_globals\"\n\n\ntry:\n    advance_iterator = next\nexcept NameError:\n    def advance_iterator(it):\n        return it.next()\nnext = advance_iterator\n\n\ntry:\n    callable = callable\nexcept NameError:\n    def callable(obj):\n        return any(\"__call__\" in klass.__dict__ for klass in type(obj).__mro__)\n\n\nif PY3:\n    def get_unbound_function(unbound):\n        return unbound\n\n    create_bound_method = types.MethodType\n\n    def create_unbound_method(func, cls):\n        return func\n\n    Iterator = object\nelse:\n    def get_unbound_function(unbound):\n        return unbound.im_func\n\n    def create_bound_method(func, obj):\n        return types.MethodType(func, obj, obj.__class__)\n\n    def create_unbound_method(func, cls):\n        return types.MethodType(func, None, cls)\n\n    class Iterator(object):\n\n        def next(self):\n            return type(self).__next__(self)\n\n    callable = callable\n_add_doc(get_unbound_function,\n         \"\"\"Get the function out of a possibly unbound function\"\"\")\n\n\nget_method_function = operator.attrgetter(_meth_func)\nget_method_self = operator.attrgetter(_meth_self)\nget_function_closure = operator.attrgetter(_func_closure)\nget_function_code = operator.attrgetter(_func_code)\nget_function_defaults = operator.attrgetter(_func_defaults)\nget_function_globals = operator.attrgetter(_func_globals)\n\n\nif PY3:\n    def iterkeys(d, **kw):\n        return iter(d.keys(**kw))\n\n    def itervalues(d, **kw):\n        return iter(d.values(**kw))\n\n    def iteritems(d, **kw):\n        return iter(d.items(**kw))\n\n    def iterlists(d, **kw):\n        return iter(d.lists(**kw))\n\n    viewkeys = operator.methodcaller(\"keys\")\n\n    viewvalues = operator.methodcaller(\"values\")\n\n    viewitems = operator.methodcaller(\"items\")\nelse:\n    def iterkeys(d, **kw):\n        return d.iterkeys(**kw)\n\n    def itervalues(d, **kw):\n        return d.itervalues(**kw)\n\n    def iteritems(d, **kw):\n        return d.iteritems(**kw)\n\n    def iterlists(d, **kw):\n        return d.iterlists(**kw)\n\n    viewkeys = operator.methodcaller(\"viewkeys\")\n\n    viewvalues = operator.methodcaller(\"viewvalues\")\n\n    viewitems = operator.methodcaller(\"viewitems\")\n\n_add_doc(iterkeys, \"Return an iterator over the keys of a dictionary.\")\n_add_doc(itervalues, \"Return an iterator over the values of a dictionary.\")\n_add_doc(iteritems,\n         \"Return an iterator over the (key, value) pairs of a dictionary.\")\n_add_doc(iterlists,\n         \"Return an iterator over the (key, [values]) pairs of a dictionary.\")\n\n\nif PY3:\n    def b(s):\n        return s.encode(\"latin-1\")\n\n    def u(s):\n        return s\n    unichr = chr\n    import struct\n    int2byte = struct.Struct(\">B\").pack\n    del struct\n    byte2int = operator.itemgetter(0)\n    indexbytes = operator.getitem\n    iterbytes = iter\n    import io\n    StringIO = io.StringIO\n    BytesIO = io.BytesIO\n    _assertCountEqual = \"assertCountEqual\"\n    if sys.version_info[1] <= 1:\n        _assertRaisesRegex = \"assertRaisesRegexp\"\n        _assertRegex = \"assertRegexpMatches\"\n    else:\n        _assertRaisesRegex = \"assertRaisesRegex\"\n        _assertRegex = \"assertRegex\"\nelse:\n    def b(s):\n        return s\n    # Workaround for standalone backslash\n\n    def u(s):\n        return unicode(s.replace(r'\\\\', r'\\\\\\\\'), \"unicode_escape\")\n    unichr = unichr\n    int2byte = chr\n\n    def byte2int(bs):\n        return ord(bs[0])\n\n    def indexbytes(buf, i):\n        return ord(buf[i])\n    iterbytes = functools.partial(itertools.imap, ord)\n    import StringIO\n    StringIO = BytesIO = StringIO.StringIO\n    _assertCountEqual = \"assertItemsEqual\"\n    _assertRaisesRegex = \"assertRaisesRegexp\"\n    _assertRegex = \"assertRegexpMatches\"\n_add_doc(b, \"\"\"Byte literal\"\"\")\n_add_doc(u, \"\"\"Text literal\"\"\")\n\n\ndef assertCountEqual(self, *args, **kwargs):\n    return getattr(self, _assertCountEqual)(*args, **kwargs)\n\n\ndef assertRaisesRegex(self, *args, **kwargs):\n    return getattr(self, _assertRaisesRegex)(*args, **kwargs)\n\n\ndef assertRegex(self, *args, **kwargs):\n    return getattr(self, _assertRegex)(*args, **kwargs)\n\n\nif PY3:\n    exec_ = getattr(moves.builtins, \"exec\")\n\n    def reraise(tp, value, tb=None):\n        if value is None:\n            value = tp()\n        if value.__traceback__ is not tb:\n            raise value.with_traceback(tb)\n        raise value\n\nelse:\n    def exec_(_code_, _globs_=None, _locs_=None):\n        \"\"\"Execute code in a namespace.\"\"\"\n        if _globs_ is None:\n            frame = sys._getframe(1)\n            _globs_ = frame.f_globals\n            if _locs_ is None:\n                _locs_ = frame.f_locals\n            del frame\n        elif _locs_ is None:\n            _locs_ = _globs_\n        exec(\"\"\"exec _code_ in _globs_, _locs_\"\"\")\n\n    exec_(\"\"\"def reraise(tp, value, tb=None):\n    raise tp, value, tb\n\"\"\")\n\n\nif sys.version_info[:2] == (3, 2):\n    exec_(\"\"\"def raise_from(value, from_value):\n    if from_value is None:\n        raise value\n    raise value from from_value\n\"\"\")\nelif sys.version_info[:2] > (3, 2):\n    exec_(\"\"\"def raise_from(value, from_value):\n    raise value from from_value\n\"\"\")\nelse:\n    def raise_from(value, from_value):\n        raise value\n\n\nprint_ = getattr(moves.builtins, \"print\", None)\nif print_ is None:\n    def print_(*args, **kwargs):\n        \"\"\"The new-style print function for Python 2.4 and 2.5.\"\"\"\n        fp = kwargs.pop(\"file\", sys.stdout)\n        if fp is None:\n            return\n\n        def write(data):\n            if not isinstance(data, basestring):\n                data = str(data)\n            # If the file has an encoding, encode unicode with it.\n            if (isinstance(fp, file) and\n                    isinstance(data, unicode) and\n                    fp.encoding is not None):\n                errors = getattr(fp, \"errors\", None)\n                if errors is None:\n                    errors = \"strict\"\n                data = data.encode(fp.encoding, errors)\n            fp.write(data)\n        want_unicode = False\n        sep = kwargs.pop(\"sep\", None)\n        if sep is not None:\n            if isinstance(sep, unicode):\n                want_unicode = True\n            elif not isinstance(sep, str):\n                raise TypeError(\"sep must be None or a string\")\n        end = kwargs.pop(\"end\", None)\n        if end is not None:\n            if isinstance(end, unicode):\n                want_unicode = True\n            elif not isinstance(end, str):\n                raise TypeError(\"end must be None or a string\")\n        if kwargs:\n            raise TypeError(\"invalid keyword arguments to print()\")\n        if not want_unicode:\n            for arg in args:\n                if isinstance(arg, unicode):\n                    want_unicode = True\n                    break\n        if want_unicode:\n            newline = unicode(\"\\n\")\n            space = unicode(\" \")\n        else:\n            newline = \"\\n\"\n            space = \" \"\n        if sep is None:\n            sep = space\n        if end is None:\n            end = newline\n        for i, arg in enumerate(args):\n            if i:\n                write(sep)\n            write(arg)\n        write(end)\nif sys.version_info[:2] < (3, 3):\n    _print = print_\n\n    def print_(*args, **kwargs):\n        fp = kwargs.get(\"file\", sys.stdout)\n        flush = kwargs.pop(\"flush\", False)\n        _print(*args, **kwargs)\n        if flush and fp is not None:\n            fp.flush()\n\n_add_doc(reraise, \"\"\"Reraise an exception.\"\"\")\n\nif sys.version_info[0:2] < (3, 4):\n    def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,\n              updated=functools.WRAPPER_UPDATES):\n        def wrapper(f):\n            f = functools.wraps(wrapped, assigned, updated)(f)\n            f.__wrapped__ = wrapped\n            return f\n        return wrapper\nelse:\n    wraps = functools.wraps\n\n\ndef with_metaclass(meta, *bases):\n    \"\"\"Create a base class with a metaclass.\"\"\"\n    # This requires a bit of explanation: the basic idea is to make a dummy\n    # metaclass for one level of class instantiation that replaces itself with\n    # the actual metaclass.\n    class metaclass(meta):\n\n        def __new__(cls, name, this_bases, d):\n            return meta(name, bases, d)\n    return type.__new__(metaclass, 'temporary_class', (), {})\n\n\ndef add_metaclass(metaclass):\n    \"\"\"Class decorator for creating a class with a metaclass.\"\"\"\n    def wrapper(cls):\n        orig_vars = cls.__dict__.copy()\n        slots = orig_vars.get('__slots__')\n        if slots is not None:\n            if isinstance(slots, str):\n                slots = [slots]\n            for slots_var in slots:\n                orig_vars.pop(slots_var)\n        orig_vars.pop('__dict__', None)\n        orig_vars.pop('__weakref__', None)\n        return metaclass(cls.__name__, cls.__bases__, orig_vars)\n    return wrapper\n\n\ndef python_2_unicode_compatible(klass):\n    \"\"\"\n    A decorator that defines __unicode__ and __str__ methods under Python 2.\n    Under Python 3 it does nothing.\n\n    To support Python 2 and 3 with a single code base, define a __str__ method\n    returning text and apply this decorator to the class.\n    \"\"\"\n    if PY2:\n        if '__str__' not in klass.__dict__:\n            raise ValueError(\"@python_2_unicode_compatible cannot be applied \"\n                             \"to %s because it doesn't define __str__().\" %\n                             klass.__name__)\n        klass.__unicode__ = klass.__str__\n        klass.__str__ = lambda self: self.__unicode__().encode('utf-8')\n    return klass\n\n\n# Complete the moves implementation.\n# This code is at the end of this module to speed up module loading.\n# Turn this module into a package.\n__path__ = []  # required for PEP 302 and PEP 451\n__package__ = __name__  # see PEP 366 @ReservedAssignment\nif globals().get(\"__spec__\") is not None:\n    __spec__.submodule_search_locations = []  # PEP 451 @UndefinedVariable\n# Remove other six meta path importers, since they cause problems. This can\n# happen if six is removed from sys.modules and then reloaded. (Setuptools does\n# this for some reason.)\nif sys.meta_path:\n    for i, importer in enumerate(sys.meta_path):\n        # Here's some real nastiness: Another \"instance\" of the six module might\n        # be floating around. Therefore, we can't use isinstance() to check for\n        # the six meta path importer, since the other six instance will have\n        # inserted an importer with different class.\n        if (type(importer).__name__ == \"_SixMetaPathImporter\" and\n                importer.name == __name__):\n            del sys.meta_path[i]\n            break\n    del i, importer\n# Finally, add the importer to the meta path import hook.\nsys.meta_path.append(_importer)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/extern/__init__.py",
    "content": "import sys\n\n\nclass VendorImporter:\n    \"\"\"\n    A PEP 302 meta path importer for finding optionally-vendored\n    or otherwise naturally-installed packages from root_name.\n    \"\"\"\n\n    def __init__(self, root_name, vendored_names=(), vendor_pkg=None):\n        self.root_name = root_name\n        self.vendored_names = set(vendored_names)\n        self.vendor_pkg = vendor_pkg or root_name.replace('extern', '_vendor')\n\n    @property\n    def search_path(self):\n        \"\"\"\n        Search first the vendor package then as a natural package.\n        \"\"\"\n        yield self.vendor_pkg + '.'\n        yield ''\n\n    def find_module(self, fullname, path=None):\n        \"\"\"\n        Return self when fullname starts with root_name and the\n        target module is one vendored through this importer.\n        \"\"\"\n        root, base, target = fullname.partition(self.root_name + '.')\n        if root:\n            return\n        if not any(map(target.startswith, self.vendored_names)):\n            return\n        return self\n\n    def load_module(self, fullname):\n        \"\"\"\n        Iterate over the search path to locate and load fullname.\n        \"\"\"\n        root, base, target = fullname.partition(self.root_name + '.')\n        for prefix in self.search_path:\n            try:\n                extant = prefix + target\n                __import__(extant)\n                mod = sys.modules[extant]\n                sys.modules[fullname] = mod\n                # mysterious hack:\n                # Remove the reference to the extant package/module\n                # on later Python versions to cause relative imports\n                # in the vendor package to resolve the same modules\n                # as those going through this importer.\n                if prefix and sys.version_info > (3, 3):\n                    del sys.modules[extant]\n                return mod\n            except ImportError:\n                pass\n        else:\n            raise ImportError(\n                \"The '{target}' package is required; \"\n                \"normally this is bundled with this package so if you get \"\n                \"this warning, consult the packager of your \"\n                \"distribution.\".format(**locals())\n            )\n\n    def install(self):\n        \"\"\"\n        Install this importer into sys.meta_path if not already present.\n        \"\"\"\n        if self not in sys.meta_path:\n            sys.meta_path.append(self)\n\n\nnames = 'packaging', 'pyparsing', 'six', 'appdirs'\nVendorImporter(__name__, names).install()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/py2_warn.py",
    "content": "import sys\nimport warnings\nimport textwrap\n\n\nmsg = textwrap.dedent(\"\"\"\n    You are running Setuptools on Python 2, which is no longer\n    supported and\n    >>> SETUPTOOLS WILL STOP WORKING <<<\n    in a subsequent release. Please ensure you are installing\n    Setuptools using pip 9.x or later or pin to `setuptools<45`\n    in your environment.\n    If you have done those things and are still encountering\n    this message, please comment in\n    https://github.com/pypa/setuptools/issues/1458\n    about the steps that led to this unsupported combination.\n    \"\"\")\n\nsys.version_info < (3,) and warnings.warn(\"*\" * 60 + msg + \"*\" * 60)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/pkg_resources/py31compat.py",
    "content": "import os\nimport errno\nimport sys\n\nfrom .extern import six\n\n\ndef _makedirs_31(path, exist_ok=False):\n    try:\n        os.makedirs(path)\n    except OSError as exc:\n        if not exist_ok or exc.errno != errno.EEXIST:\n            raise\n\n\n# rely on compatibility behavior until mode considerations\n#  and exists_ok considerations are disentangled.\n# See https://github.com/pypa/setuptools/pull/1083#issuecomment-315168663\nneeds_makedirs = (\n    six.PY2 or\n    (3, 4) <= sys.version_info < (3, 4, 1)\n)\nmakedirs = _makedirs_31 if needs_makedirs else os.makedirs\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/Qt3DAnimation.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides Qt3DAnimation classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError, PYSIDE_VERSION\nfrom .py3compat import PY2\n\nif PYQT5:\n    from PyQt5.Qt3DAnimation import *\nelif PYSIDE2:\n    if not PY2 or (PY2 and PYSIDE_VERSION < '5.12.4'):\n        # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026\n        import PySide2.Qt3DAnimation as __temp\n        import inspect\n        for __name in inspect.getmembers(__temp.Qt3DAnimation):\n            globals()[__name[0]] = __name[1]\n    else:\n        raise PythonQtError('A bug in Shiboken prevents this')\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/Qt3DCore.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides Qt3DCore classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError, PYSIDE_VERSION\nfrom .py3compat import PY2\n\nif PYQT5:\n    from PyQt5.Qt3DCore import *\nelif PYSIDE2:\n    if not PY2 or (PY2 and PYSIDE_VERSION < '5.12.4'):\n        # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026\n        import PySide2.Qt3DCore as __temp\n        import inspect\n        for __name in inspect.getmembers(__temp.Qt3DCore):\n            globals()[__name[0]] = __name[1]\n    else:\n        raise PythonQtError('A bug in Shiboken prevents this')\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/Qt3DExtras.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides Qt3DExtras classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError, PYSIDE_VERSION\nfrom .py3compat import PY2\n\nif PYQT5:\n    from PyQt5.Qt3DExtras import *\nelif PYSIDE2:\n    if not PY2 or (PY2 and PYSIDE_VERSION < '5.12.4'):\n        # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026\n        import PySide2.Qt3DExtras as __temp\n        import inspect\n        for __name in inspect.getmembers(__temp.Qt3DExtras):\n            globals()[__name[0]] = __name[1]\n    else:\n        raise PythonQtError('A bug in Shiboken prevents this')\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/Qt3DInput.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides Qt3DInput classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError, PYSIDE_VERSION\nfrom .py3compat import PY2\n\nif PYQT5:\n    from PyQt5.Qt3DInput import *\nelif PYSIDE2:\n    if not PY2 or (PY2 and PYSIDE_VERSION < '5.12.4'):\n        # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026\n        import PySide2.Qt3DInput as __temp\n        import inspect\n        for __name in inspect.getmembers(__temp.Qt3DInput):\n            globals()[__name[0]] = __name[1]\n    else:\n        raise PythonQtError('A bug in Shiboken prevents this')\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/Qt3DLogic.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides Qt3DLogic classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError, PYSIDE_VERSION\nfrom .py3compat import PY2\n\nif PYQT5:\n    from PyQt5.Qt3DLogic import *\nelif PYSIDE2:\n    if not PY2 or (PY2 and PYSIDE_VERSION < '5.12.4'):\n        # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026\n        import PySide2.Qt3DLogic as __temp\n        import inspect\n        for __name in inspect.getmembers(__temp.Qt3DLogic):\n            globals()[__name[0]] = __name[1]\n    else:\n        raise PythonQtError('A bug in Shiboken prevents this')\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/Qt3DRender.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides Qt3DRender classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError, PYSIDE_VERSION\nfrom .py3compat import PY2\n\nif PYQT5:\n    from PyQt5.Qt3DRender import *\nelif PYSIDE2:\n    if not PY2 or (PY2 and PYSIDE_VERSION < '5.12.4'):\n        # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026\n        import PySide2.Qt3DRender as __temp\n        import inspect\n        for __name in inspect.getmembers(__temp.Qt3DRender):\n            globals()[__name[0]] = __name[1]\n    else:\n        raise PythonQtError('A bug in Shiboken prevents this')\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtCharts.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2019- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtChart classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError\n\nif PYQT5:\n    try:\n        from PyQt5 import QtChart as QtCharts\n    except ImportError:\n        raise PythonQtError('The QtChart module was not found. '\n                            'It needs to be installed separately for PyQt5.')\nelif PYSIDE2:\n    from PySide2.QtCharts import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtCore.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © 2014-2015 Colin Duquesnoy\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n\n\"\"\"\nProvides QtCore classes and functions.\n\"\"\"\n\nfrom . import PYQT5, PYSIDE2, PYQT4, PYSIDE, PythonQtError\n\n\nif PYQT5:\n    from PyQt5.QtCore import *\n    from PyQt5.QtCore import pyqtSignal as Signal\n    from PyQt5.QtCore import pyqtBoundSignal as SignalInstance\n    from PyQt5.QtCore import pyqtSlot as Slot\n    from PyQt5.QtCore import pyqtProperty as Property\n    from PyQt5.QtCore import QT_VERSION_STR as __version__\n\n    # For issue #153\n    from PyQt5.QtCore import QDateTime\n    QDateTime.toPython = QDateTime.toPyDateTime\n\n    # Those are imported from `import *`\n    del pyqtSignal, pyqtBoundSignal, pyqtSlot, pyqtProperty, QT_VERSION_STR\nelif PYSIDE2:\n    from PySide2.QtCore import *\n\n    try:  # may be limited to PySide-5.11a1 only \n        from PySide2.QtGui import QStringListModel\n    except:\n        pass\n\n    import PySide2.QtCore\n    __version__ = PySide2.QtCore.__version__\nelif PYQT4:\n    from PyQt4.QtCore import *\n    # Those are things we inherited from Spyder that fix crazy crashes under\n    # some specific situations. (See #34)\n    from PyQt4.QtCore import QCoreApplication\n    from PyQt4.QtCore import Qt\n    from PyQt4.QtCore import pyqtSignal as Signal\n    from PyQt4.QtCore import pyqtBoundSignal as SignalInstance\n    from PyQt4.QtCore import pyqtSlot as Slot\n    from PyQt4.QtCore import pyqtProperty as Property\n    from PyQt4.QtGui import (QItemSelection, QItemSelectionModel,\n                             QItemSelectionRange, QSortFilterProxyModel,\n                             QStringListModel)\n    from PyQt4.QtCore import QT_VERSION_STR as __version__\n    from PyQt4.QtCore import qInstallMsgHandler as qInstallMessageHandler\n\n    # QDesktopServices has has been split into (QDesktopServices and\n    # QStandardPaths) in Qt5\n    # This creates a dummy class that emulates QStandardPaths\n    from PyQt4.QtGui import QDesktopServices as _QDesktopServices\n\n    class QStandardPaths():\n        StandardLocation = _QDesktopServices.StandardLocation\n        displayName = _QDesktopServices.displayName\n        DesktopLocation = _QDesktopServices.DesktopLocation\n        DocumentsLocation = _QDesktopServices.DocumentsLocation\n        FontsLocation = _QDesktopServices.FontsLocation\n        ApplicationsLocation = _QDesktopServices.ApplicationsLocation\n        MusicLocation = _QDesktopServices.MusicLocation\n        MoviesLocation = _QDesktopServices.MoviesLocation\n        PicturesLocation = _QDesktopServices.PicturesLocation\n        TempLocation = _QDesktopServices.TempLocation\n        HomeLocation = _QDesktopServices.HomeLocation\n        DataLocation = _QDesktopServices.DataLocation\n        CacheLocation = _QDesktopServices.CacheLocation\n        writableLocation = _QDesktopServices.storageLocation\n\n    # Those are imported from `import *`\n    del pyqtSignal, pyqtBoundSignal, pyqtSlot, pyqtProperty, QT_VERSION_STR, qInstallMsgHandler\nelif PYSIDE:\n    from PySide.QtCore import *\n    from PySide.QtGui import (QItemSelection, QItemSelectionModel,\n                              QItemSelectionRange, QSortFilterProxyModel,\n                              QStringListModel)\n    from PySide.QtCore import qInstallMsgHandler as qInstallMessageHandler\n    del qInstallMsgHandler\n\n    # QDesktopServices has has been split into (QDesktopServices and\n    # QStandardPaths) in Qt5\n    # This creates a dummy class that emulates QStandardPaths\n    from PySide.QtGui import QDesktopServices as _QDesktopServices\n\n    class QStandardPaths():\n        StandardLocation = _QDesktopServices.StandardLocation\n        displayName = _QDesktopServices.displayName\n        DesktopLocation = _QDesktopServices.DesktopLocation\n        DocumentsLocation = _QDesktopServices.DocumentsLocation\n        FontsLocation = _QDesktopServices.FontsLocation\n        ApplicationsLocation = _QDesktopServices.ApplicationsLocation\n        MusicLocation = _QDesktopServices.MusicLocation\n        MoviesLocation = _QDesktopServices.MoviesLocation\n        PicturesLocation = _QDesktopServices.PicturesLocation\n        TempLocation = _QDesktopServices.TempLocation\n        HomeLocation = _QDesktopServices.HomeLocation\n        DataLocation = _QDesktopServices.DataLocation\n        CacheLocation = _QDesktopServices.CacheLocation\n        writableLocation = _QDesktopServices.storageLocation\n\n    import PySide.QtCore\n    __version__ = PySide.QtCore.__version__\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtDataVisualization.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtDataVisualization classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtDataVisualization import *\nelif PYSIDE2:\n    # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026\n    import PySide2.QtDataVisualization as __temp\n    import inspect\n    for __name in inspect.getmembers(__temp.QtDataVisualization):\n        globals()[__name[0]] = __name[1]\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtDesigner.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © 2014-2015 Colin Duquesnoy\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n\n\"\"\"\nProvides QtDesigner classes and functions.\n\"\"\"\n\nfrom . import PYQT5, PYQT4, PythonQtError\n\n\nif PYQT5:\n    from PyQt5.QtDesigner import *\nelif PYQT4:\n    from PyQt4.QtDesigner import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtGui.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © 2014-2015 Colin Duquesnoy\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n\n\"\"\"\nProvides QtGui classes and functions.\n.. warning:: Only PyQt4/PySide QtGui classes compatible with PyQt5.QtGui are\n    exposed here. Therefore, you need to treat/use this package as if it were\n    the ``PyQt5.QtGui`` module.\n\"\"\"\nimport warnings\n\nfrom . import PYQT5, PYQT4, PYSIDE, PYSIDE2, PythonQtError\n\n\nif PYQT5:\n    from PyQt5.QtGui import *\nelif PYSIDE2:\n    from PySide2.QtGui import *\nelif PYQT4:\n    try:\n        # Older versions of PyQt4 do not provide these\n        from PyQt4.QtGui import (QGlyphRun, QMatrix2x2, QMatrix2x3,\n                                 QMatrix2x4, QMatrix3x2, QMatrix3x3,\n                                 QMatrix3x4, QMatrix4x2, QMatrix4x3,\n                                 QMatrix4x4, QTouchEvent, QQuaternion,\n                                 QRadialGradient, QRawFont, QStaticText,\n                                 QVector2D, QVector3D, QVector4D,\n                                 qFuzzyCompare)\n    except ImportError:\n        pass\n    try:\n        from PyQt4.Qt import QKeySequence, QTextCursor\n    except ImportError:\n        # In PyQt4-sip 4.19.13 QKeySequence and QTextCursor are in PyQt4.QtGui\n        from PyQt4.QtGui import QKeySequence, QTextCursor\n    from PyQt4.QtGui import (QAbstractTextDocumentLayout, QActionEvent, QBitmap,\n                             QBrush, QClipboard, QCloseEvent, QColor,\n                             QConicalGradient, QContextMenuEvent, QCursor,\n                             QDoubleValidator, QDrag,\n                             QDragEnterEvent, QDragLeaveEvent, QDragMoveEvent,\n                             QDropEvent, QFileOpenEvent, QFocusEvent, QFont,\n                             QFontDatabase, QFontInfo, QFontMetrics,\n                             QFontMetricsF, QGradient, QHelpEvent,\n                             QHideEvent, QHoverEvent, QIcon, QIconDragEvent,\n                             QIconEngine, QImage, QImageIOHandler, QImageReader,\n                             QImageWriter, QInputEvent, QInputMethodEvent,\n                             QKeyEvent, QLinearGradient,\n                             QMouseEvent, QMoveEvent, QMovie,\n                             QPaintDevice, QPaintEngine, QPaintEngineState,\n                             QPaintEvent, QPainter, QPainterPath,\n                             QPainterPathStroker, QPalette, QPen, QPicture,\n                             QPictureIO, QPixmap, QPixmapCache, QPolygon,\n                             QPolygonF, QRegExpValidator, QRegion, QResizeEvent,\n                             QSessionManager, QShortcutEvent, QShowEvent,\n                             QStandardItem, QStandardItemModel,\n                             QStatusTipEvent, QSyntaxHighlighter, QTabletEvent,\n                             QTextBlock, QTextBlockFormat, QTextBlockGroup,\n                             QTextBlockUserData, QTextCharFormat,\n                             QTextDocument, QTextDocumentFragment,\n                             QTextDocumentWriter, QTextFormat, QTextFragment,\n                             QTextFrame, QTextFrameFormat, QTextImageFormat,\n                             QTextInlineObject, QTextItem, QTextLayout,\n                             QTextLength, QTextLine, QTextList, QTextListFormat,\n                             QTextObject, QTextObjectInterface, QTextOption,\n                             QTextTable, QTextTableCell, QTextTableCellFormat,\n                             QTextTableFormat, QTransform,\n                             QValidator, QWhatsThisClickedEvent, QWheelEvent,\n                             QWindowStateChangeEvent, qAlpha, qBlue,\n                             qGray, qGreen, qIsGray, qRed, qRgb,\n                             qRgba, QIntValidator)\n\n    # QDesktopServices has has been split into (QDesktopServices and\n    # QStandardPaths) in Qt5\n    # It only exposes QDesktopServices that are still in pyqt5\n    from PyQt4.QtGui import QDesktopServices as _QDesktopServices\n\n    class QDesktopServices():\n        openUrl = _QDesktopServices.openUrl\n        setUrlHandler = _QDesktopServices.setUrlHandler\n        unsetUrlHandler = _QDesktopServices.unsetUrlHandler\n\n        def __getattr__(self, name):\n            attr = getattr(_QDesktopServices, name)\n\n            new_name = name\n            if name == 'storageLocation':\n                new_name = 'writableLocation'\n            warnings.warn((\"Warning QDesktopServices.{} is deprecated in Qt5\"\n                            \"we recommend you use QDesktopServices.{} instead\").format(name, new_name),\n                           DeprecationWarning)\n            return attr\n    QDesktopServices = QDesktopServices()\n\nelif PYSIDE:\n    from PySide.QtGui import (QAbstractTextDocumentLayout, QActionEvent, QBitmap,\n                              QBrush, QClipboard, QCloseEvent, QColor,\n                              QConicalGradient, QContextMenuEvent, QCursor,\n                              QDoubleValidator, QDrag,\n                              QDragEnterEvent, QDragLeaveEvent, QDragMoveEvent,\n                              QDropEvent, QFileOpenEvent, QFocusEvent, QFont,\n                              QFontDatabase, QFontInfo, QFontMetrics,\n                              QFontMetricsF, QGradient, QHelpEvent,\n                              QHideEvent, QHoverEvent, QIcon, QIconDragEvent,\n                              QIconEngine, QImage, QImageIOHandler, QImageReader,\n                              QImageWriter, QInputEvent, QInputMethodEvent,\n                              QKeyEvent, QKeySequence, QLinearGradient,\n                              QMatrix2x2, QMatrix2x3, QMatrix2x4, QMatrix3x2,\n                              QMatrix3x3, QMatrix3x4, QMatrix4x2, QMatrix4x3,\n                              QMatrix4x4, QMouseEvent, QMoveEvent, QMovie,\n                              QPaintDevice, QPaintEngine, QPaintEngineState,\n                              QPaintEvent, QPainter, QPainterPath,\n                              QPainterPathStroker, QPalette, QPen, QPicture,\n                              QPictureIO, QPixmap, QPixmapCache, QPolygon,\n                              QPolygonF, QQuaternion, QRadialGradient,\n                              QRegExpValidator, QRegion, QResizeEvent,\n                              QSessionManager, QShortcutEvent, QShowEvent,\n                              QStandardItem, QStandardItemModel,\n                              QStatusTipEvent, QSyntaxHighlighter, QTabletEvent,\n                              QTextBlock, QTextBlockFormat, QTextBlockGroup,\n                              QTextBlockUserData, QTextCharFormat, QTextCursor,\n                              QTextDocument, QTextDocumentFragment,\n                              QTextFormat, QTextFragment,\n                              QTextFrame, QTextFrameFormat, QTextImageFormat,\n                              QTextInlineObject, QTextItem, QTextLayout,\n                              QTextLength, QTextLine, QTextList, QTextListFormat,\n                              QTextObject, QTextObjectInterface, QTextOption,\n                              QTextTable, QTextTableCell, QTextTableCellFormat,\n                              QTextTableFormat, QTouchEvent, QTransform,\n                              QValidator, QVector2D, QVector3D, QVector4D,\n                              QWhatsThisClickedEvent, QWheelEvent,\n                              QWindowStateChangeEvent, qAlpha, qBlue,\n                              qGray, qGreen, qIsGray, qRed, qRgb, qRgba,\n                              QIntValidator)\n    # QDesktopServices has has been split into (QDesktopServices and\n    # QStandardPaths) in Qt5\n    # It only exposes QDesktopServices that are still in pyqt5\n    from PySide.QtGui import QDesktopServices as _QDesktopServices\n\n    class QDesktopServices():\n        openUrl = _QDesktopServices.openUrl\n        setUrlHandler = _QDesktopServices.setUrlHandler\n        unsetUrlHandler = _QDesktopServices.unsetUrlHandler\n\n        def __getattr__(self, name):\n            attr = getattr(_QDesktopServices, name)\n\n            new_name = name\n            if name == 'storageLocation':\n                new_name = 'writableLocation'\n            warnings.warn((\"Warning QDesktopServices.{} is deprecated in Qt5\"\n                            \"we recommend you use QDesktopServices.{} instead\").format(name, new_name),\n                           DeprecationWarning)\n            return attr\n    QDesktopServices = QDesktopServices()\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtHelp.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n\n\"\"\"QtHelp Wrapper.\"\"\"\n\nimport warnings\n\nfrom . import PYQT5\nfrom . import PYQT4\nfrom . import PYSIDE\nfrom . import PYSIDE2\n\nif PYQT5:\n    from PyQt5.QtHelp import *\nelif PYSIDE2:\n    from PySide2.QtHelp import *\nelif PYQT4:\n    from PyQt4.QtHelp import *\nelif PYSIDE:\n    from PySide.QtHelp import *\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtLocation.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtLocation classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtLocation import *\nelif PYSIDE2:\n    from PySide2.QtLocation import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtMultimedia.py",
    "content": "import warnings\n\nfrom . import PYQT5\nfrom . import PYQT4\nfrom . import PYSIDE\nfrom . import PYSIDE2\n\nif PYQT5:\n    from PyQt5.QtMultimedia import *\nelif PYSIDE2:\n    from PySide2.QtMultimedia import *\nelif PYQT4:\n    from PyQt4.QtMultimedia import *\n    from PyQt4.QtGui import QSound\nelif PYSIDE:\n    from PySide.QtMultimedia import *\n    from PySide.QtGui import QSound\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtMultimediaWidgets.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtMultimediaWidgets classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYSIDE2, PYQT5, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtMultimediaWidgets import *\nelif PYSIDE2:\n    from PySide2.QtMultimediaWidgets import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtNetwork.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © 2014-2015 Colin Duquesnoy\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n\n\"\"\"\nProvides QtNetwork classes and functions.\n\"\"\"\n\nfrom . import PYQT5, PYSIDE2, PYQT4, PYSIDE, PythonQtError\n\n\nif PYQT5:\n    from PyQt5.QtNetwork import *\nelif PYSIDE2:\n    from PySide2.QtNetwork import *\nelif PYQT4:\n    from PyQt4.QtNetwork import *\nelif PYSIDE:\n    from PySide.QtNetwork import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtOpenGL.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtOpenGL classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT4, PYQT5, PYSIDE, PYSIDE2, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtOpenGL import *\nelif PYSIDE2:\n    from PySide2.QtOpenGL import *\nelif PYQT4:\n    from PyQt4.QtOpenGL import *\nelif PYSIDE:\n    from PySide.QtOpenGL import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n\ndel PYQT4, PYQT5, PYSIDE, PYSIDE2\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtPositioning.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright 2020 Antonio Valentino\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtPositioning classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtPositioning import *\nelif PYSIDE2:\n    from PySide2.QtPositioning import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtPrintSupport.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n\n\"\"\"\nProvides QtPrintSupport classes and functions.\n\"\"\"\n\nfrom . import PYQT5, PYQT4,PYSIDE2, PYSIDE, PythonQtError\n\n\nif PYQT5:\n    from PyQt5.QtPrintSupport import *\nelif PYSIDE2:\n    from PySide2.QtPrintSupport import *\nelif PYQT4:\n    from PyQt4.QtGui import (QAbstractPrintDialog, QPageSetupDialog,\n                             QPrintDialog, QPrintEngine, QPrintPreviewDialog,\n                             QPrintPreviewWidget, QPrinter, QPrinterInfo)\nelif PYSIDE:\n    from PySide.QtGui import (QAbstractPrintDialog, QPageSetupDialog,\n                              QPrintDialog, QPrintEngine, QPrintPreviewDialog,\n                              QPrintPreviewWidget, QPrinter, QPrinterInfo)\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtQml.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtQml classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtQml import *\nelif PYSIDE2:\n    from PySide2.QtQml import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtQuick.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtQuick classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtQuick import *\nelif PYSIDE2:\n    from PySide2.QtQuick import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtQuickWidgets.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtQuickWidgets classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtQuickWidgets import *\nelif PYSIDE2:\n    from PySide2.QtQuickWidgets import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtSerialPort.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2020 Marcin Stano\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtSerialPort classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtSerialPort import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtSql.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtSql classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT5, PYSIDE2, PYQT4, PYSIDE, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtSql import *\nelif PYSIDE2:\n    from PySide2.QtSql import *\nelif PYQT4:\n    from PyQt4.QtSql import *\nelif PYSIDE:\n    from PySide.QtSql import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n\ndel PYQT4, PYQT5, PYSIDE, PYSIDE2\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtSvg.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtSvg classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT4, PYSIDE2, PYQT5, PYSIDE, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtSvg import *\nelif PYSIDE2:\n    from PySide2.QtSvg import *\nelif PYQT4:\n    from PyQt4.QtSvg import *\nelif PYSIDE:\n    from PySide.QtSvg import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n\ndel PYQT4, PYQT5, PYSIDE, PYSIDE2\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtTest.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © 2014-2015 Colin Duquesnoy\n# Copyright © 2009- The Spyder Developmet Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n\n\"\"\"\nProvides QtTest and functions\n\"\"\"\n\nfrom . import PYQT5,PYSIDE2, PYQT4, PYSIDE, PythonQtError\n\n\nif PYQT5:\n    from PyQt5.QtTest import QTest\nelif PYSIDE2:\n    from PySide2.QtTest import QTest\nelif PYQT4:\n    from PyQt4.QtTest import QTest as OldQTest\n\n    class QTest(OldQTest):\n        @staticmethod\n        def qWaitForWindowActive(QWidget):\n            OldQTest.qWaitForWindowShown(QWidget)\nelif PYSIDE:\n    from PySide.QtTest import QTest\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtWebChannel.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtWebChannel classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYSIDE2, PYQT5, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtWebChannel import *\nelif PYSIDE2:\n    from PySide2.QtWebChannel import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtWebEngineWidgets.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © 2014-2015 Colin Duquesnoy\n# Copyright © 2009- The Spyder development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n\n\"\"\"\nProvides QtWebEngineWidgets classes and functions.\n\"\"\"\n\nfrom . import PYQT5,PYSIDE2, PYQT4, PYSIDE, PythonQtError\n\n\n# To test if we are using WebEngine or WebKit\nWEBENGINE = True\n\n\nif PYQT5:\n    try:\n        from PyQt5.QtWebEngineWidgets import QWebEnginePage\n        from PyQt5.QtWebEngineWidgets import QWebEngineView\n        from PyQt5.QtWebEngineWidgets import QWebEngineSettings\n        # Based on the work at https://github.com/spyder-ide/qtpy/pull/203\n        from PyQt5.QtWebEngineWidgets import QWebEngineProfile\n    except ImportError:\n        from PyQt5.QtWebKitWidgets import QWebPage as QWebEnginePage\n        from PyQt5.QtWebKitWidgets import QWebView as QWebEngineView\n        from PyQt5.QtWebKit import QWebSettings as QWebEngineSettings\n        WEBENGINE = False\nelif PYSIDE2:\n    from PySide2.QtWebEngineWidgets import QWebEnginePage\n    from PySide2.QtWebEngineWidgets import QWebEngineView\n    from PySide2.QtWebEngineWidgets import QWebEngineSettings\n    # Based on the work at https://github.com/spyder-ide/qtpy/pull/203\n    from PySide2.QtWebEngineWidgets import QWebEngineProfile\nelif PYQT4:\n    from PyQt4.QtWebKit import QWebPage as QWebEnginePage\n    from PyQt4.QtWebKit import QWebView as QWebEngineView\n    from PyQt4.QtWebKit import QWebSettings as QWebEngineSettings\n    WEBENGINE = False\nelif PYSIDE:\n    from PySide.QtWebKit import QWebPage as QWebEnginePage\n    from PySide.QtWebKit import QWebView as QWebEngineView\n    from PySide.QtWebKit import QWebSettings as QWebEngineSettings\n    WEBENGINE = False\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtWebSockets.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtWebSockets classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYSIDE2, PYQT5, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtWebSockets import *\nelif PYSIDE2:\n    from PySide2.QtWebSockets import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtWidgets.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © 2014-2015 Colin Duquesnoy\n# Copyright © 2009- The Spyder Developmet Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n\n\"\"\"\nProvides widget classes and functions.\n.. warning:: Only PyQt4/PySide QtGui classes compatible with PyQt5.QtWidgets\n    are exposed here. Therefore, you need to treat/use this package as if it\n    were the ``PyQt5.QtWidgets`` module.\n\"\"\"\n\nfrom . import PYQT5, PYSIDE2, PYQT4, PYSIDE, PythonQtError\nfrom ._patch.qcombobox import patch_qcombobox\nfrom ._patch.qheaderview import introduce_renamed_methods_qheaderview\n\n\nif PYQT5:\n    from PyQt5.QtWidgets import *\nelif PYSIDE2:\n    from PySide2.QtWidgets import *\nelif PYQT4:\n    from PyQt4.QtGui import *\n    QStyleOptionViewItem = QStyleOptionViewItemV4\n    del QStyleOptionViewItemV4\n    QStyleOptionFrame = QStyleOptionFrameV3\n    del QStyleOptionFrameV3\n\n    # These objects belong to QtGui\n    try:\n        # Older versions of PyQt4 do not provide these\n        del (QGlyphRun,\n             QMatrix2x2, QMatrix2x3, QMatrix2x4, QMatrix3x2, QMatrix3x3,\n             QMatrix3x4, QMatrix4x2, QMatrix4x3, QMatrix4x4,\n             QQuaternion, QRadialGradient, QRawFont, QRegExpValidator,\n             QStaticText, QTouchEvent, QVector2D, QVector3D, QVector4D,\n             qFuzzyCompare)\n    except NameError:\n        pass\n    del (QAbstractTextDocumentLayout, QActionEvent, QBitmap, QBrush, QClipboard,\n         QCloseEvent, QColor, QConicalGradient, QContextMenuEvent, QCursor,\n         QDesktopServices, QDoubleValidator, QDrag, QDragEnterEvent,\n         QDragLeaveEvent, QDragMoveEvent, QDropEvent, QFileOpenEvent,\n         QFocusEvent, QFont, QFontDatabase, QFontInfo, QFontMetrics,\n         QFontMetricsF, QGradient, QHelpEvent, QHideEvent,\n         QHoverEvent, QIcon, QIconDragEvent, QIconEngine, QImage,\n         QImageIOHandler, QImageReader, QImageWriter, QInputEvent,\n         QInputMethodEvent, QKeyEvent, QKeySequence, QLinearGradient,\n         QMouseEvent, QMoveEvent, QMovie, QPaintDevice, QPaintEngine,\n         QPaintEngineState, QPaintEvent, QPainter, QPainterPath,\n         QPainterPathStroker, QPalette, QPen, QPicture, QPictureIO, QPixmap,\n         QPixmapCache, QPolygon, QPolygonF,\n         QRegion, QResizeEvent, QSessionManager, QShortcutEvent, QShowEvent,\n         QStandardItem, QStandardItemModel, QStatusTipEvent,\n         QSyntaxHighlighter, QTabletEvent, QTextBlock, QTextBlockFormat,\n         QTextBlockGroup, QTextBlockUserData, QTextCharFormat, QTextCursor,\n         QTextDocument, QTextDocumentFragment, QTextDocumentWriter,\n         QTextFormat, QTextFragment, QTextFrame, QTextFrameFormat,\n         QTextImageFormat, QTextInlineObject, QTextItem, QTextLayout,\n         QTextLength, QTextLine, QTextList, QTextListFormat, QTextObject,\n         QTextObjectInterface, QTextOption, QTextTable, QTextTableCell,\n         QTextTableCellFormat, QTextTableFormat, QTransform,\n         QValidator, QWhatsThisClickedEvent,\n         QWheelEvent, QWindowStateChangeEvent, qAlpha, qBlue,\n         qGray, qGreen, qIsGray, qRed, qRgb, qRgba, QIntValidator,\n         QStringListModel)\n\n    # These objects belong to QtPrintSupport\n    del (QAbstractPrintDialog, QPageSetupDialog, QPrintDialog, QPrintEngine,\n         QPrintPreviewDialog, QPrintPreviewWidget, QPrinter, QPrinterInfo)\n\n    # These objects belong to QtCore\n    del (QItemSelection, QItemSelectionModel, QItemSelectionRange,\n         QSortFilterProxyModel)\n\n    # Patch QComboBox to allow Python objects to be passed to userData\n    patch_qcombobox(QComboBox)\n\n    # QHeaderView: renamed methods\n    introduce_renamed_methods_qheaderview(QHeaderView)\n\nelif PYSIDE:\n    from PySide.QtGui import *\n    QStyleOptionViewItem = QStyleOptionViewItemV4\n    del QStyleOptionViewItemV4\n\n    # These objects belong to QtGui\n    del (QAbstractTextDocumentLayout, QActionEvent, QBitmap, QBrush, QClipboard,\n         QCloseEvent, QColor, QConicalGradient, QContextMenuEvent, QCursor,\n         QDesktopServices, QDoubleValidator, QDrag, QDragEnterEvent,\n         QDragLeaveEvent, QDragMoveEvent, QDropEvent, QFileOpenEvent,\n         QFocusEvent, QFont, QFontDatabase, QFontInfo, QFontMetrics,\n         QFontMetricsF, QGradient, QHelpEvent, QHideEvent,\n         QHoverEvent, QIcon, QIconDragEvent, QIconEngine, QImage,\n         QImageIOHandler, QImageReader, QImageWriter, QInputEvent,\n         QInputMethodEvent, QKeyEvent, QKeySequence, QLinearGradient,\n         QMatrix2x2, QMatrix2x3, QMatrix2x4, QMatrix3x2, QMatrix3x3,\n         QMatrix3x4, QMatrix4x2, QMatrix4x3, QMatrix4x4, QMouseEvent,\n         QMoveEvent, QMovie, QPaintDevice, QPaintEngine, QPaintEngineState,\n         QPaintEvent, QPainter, QPainterPath, QPainterPathStroker, QPalette,\n         QPen, QPicture, QPictureIO, QPixmap, QPixmapCache, QPolygon,\n         QPolygonF, QQuaternion, QRadialGradient, QRegExpValidator,\n         QRegion, QResizeEvent, QSessionManager, QShortcutEvent, QShowEvent,\n         QStandardItem, QStandardItemModel, QStatusTipEvent,\n         QSyntaxHighlighter, QTabletEvent, QTextBlock, QTextBlockFormat,\n         QTextBlockGroup, QTextBlockUserData, QTextCharFormat, QTextCursor,\n         QTextDocument, QTextDocumentFragment,\n         QTextFormat, QTextFragment, QTextFrame, QTextFrameFormat,\n         QTextImageFormat, QTextInlineObject, QTextItem, QTextLayout,\n         QTextLength, QTextLine, QTextList, QTextListFormat, QTextObject,\n         QTextObjectInterface, QTextOption, QTextTable, QTextTableCell,\n         QTextTableCellFormat, QTextTableFormat, QTouchEvent, QTransform,\n         QValidator, QVector2D, QVector3D, QVector4D, QWhatsThisClickedEvent,\n         QWheelEvent, QWindowStateChangeEvent, qAlpha, qBlue, qGray, qGreen,\n         qIsGray, qRed, qRgb, qRgba, QIntValidator, QStringListModel)\n\n    # These objects belong to QtPrintSupport\n    del (QAbstractPrintDialog, QPageSetupDialog, QPrintDialog, QPrintEngine,\n         QPrintPreviewDialog, QPrintPreviewWidget, QPrinter, QPrinterInfo)\n\n    # These objects belong to QtCore\n    del (QItemSelection, QItemSelectionModel, QItemSelectionRange,\n         QSortFilterProxyModel)\n\n    # Patch QComboBox to allow Python objects to be passed to userData\n    patch_qcombobox(QComboBox)\n\n    # QHeaderView: renamed methods\n    introduce_renamed_methods_qheaderview(QHeaderView)\n\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtWinExtras.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n\nfrom . import PYQT5, PYSIDE2, PythonQtError\n\n\nif PYQT5:\n    from PyQt5.QtWinExtras import *\nelif PYSIDE2:\n    from PySide2.QtWinExtras import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/QtXmlPatterns.py",
    "content": "# -*- coding: utf-8 -*-\n# -----------------------------------------------------------------------------\n# Copyright © 2009- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n# -----------------------------------------------------------------------------\n\"\"\"Provides QtXmlPatterns classes and functions.\"\"\"\n\n# Local imports\nfrom . import PYQT4, PYSIDE2, PYQT5, PYSIDE, PythonQtError\n\nif PYQT5:\n    from PyQt5.QtXmlPatterns import *\nelif PYSIDE2:\n    from PySide2.QtXmlPatterns import *\nelif PYQT4:\n    from PyQt4.QtXmlPatterns import *\nelif PYSIDE:\n    from PySide.QtXmlPatterns import *\nelse:\n    raise PythonQtError('No Qt bindings could be found')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © 2009- The Spyder Development Team\n# Copyright © 2014-2015 Colin Duquesnoy\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\n\n\"\"\"\n**QtPy** is a shim over the various Python Qt bindings. It is used to write\nQt binding independent libraries or applications.\n\nIf one of the APIs has already been imported, then it will be used.\n\nOtherwise, the shim will automatically select the first available API (PyQt5,\nPySide2, PyQt4 and finally PySide); in that case, you can force the use of one\nspecific bindings (e.g. if your application is using one specific bindings and\nyou need to use library that use QtPy) by setting up the ``QT_API`` environment\nvariable.\n\nPyQt5\n=====\n\nFor PyQt5, you don't have to set anything as it will be used automatically::\n\n    >>> from qtpy import QtGui, QtWidgets, QtCore\n    >>> print(QtWidgets.QWidget)\n\n\nPySide2\n======\n\nSet the QT_API environment variable to 'pyside2' before importing other\npackages::\n\n    >>> import os\n    >>> os.environ['QT_API'] = 'pyside2'\n    >>> from qtpy import QtGui, QtWidgets, QtCore\n    >>> print(QtWidgets.QWidget)\n\nPyQt4\n=====\n\nSet the ``QT_API`` environment variable to 'pyqt' before importing any python\npackage::\n\n    >>> import os\n    >>> os.environ['QT_API'] = 'pyqt'\n    >>> from qtpy import QtGui, QtWidgets, QtCore\n    >>> print(QtWidgets.QWidget)\n\nPySide\n======\n\nSet the QT_API environment variable to 'pyside' before importing other\npackages::\n\n    >>> import os\n    >>> os.environ['QT_API'] = 'pyside'\n    >>> from qtpy import QtGui, QtWidgets, QtCore\n    >>> print(QtWidgets.QWidget)\n\n\"\"\"\n\nfrom distutils.version import LooseVersion\nimport os\nimport platform\nimport sys\nimport warnings\n\n# Version of QtPy\nfrom ._version import __version__\nfrom .py3compat import PY2\n\n\nclass PythonQtError(RuntimeError):\n    \"\"\"Error raise if no bindings could be selected.\"\"\"\n    pass\n\n\nclass PythonQtWarning(Warning):\n    \"\"\"Warning if some features are not implemented in a binding.\"\"\"\n    pass\n\n\n# Qt API environment variable name\nQT_API = 'QT_API'\n\n# Names of the expected PyQt5 api\nPYQT5_API = ['pyqt5']\n\n# Names of the expected PyQt4 api\nPYQT4_API = [\n    'pyqt',  # name used in IPython.qt\n    'pyqt4'  # pyqode.qt original name\n]\n\n# Names of the expected PySide api\nPYSIDE_API = ['pyside']\n\n# Names of the expected PySide2 api\nPYSIDE2_API = ['pyside2']\n\n# Names of the legacy APIs that we should warn users about\nLEGACY_APIS = PYQT4_API + PYSIDE_API\n\n# Minimum fully supported versions of Qt and the bindings\nPYQT_VERSION_MIN = '5.9.0'\nPYSIDE_VERSION_MIN = '5.12.0'\nQT_VERSION_MIN = '5.9.0'\n\n# Detecting if a binding was specified by the user\nbinding_specified = QT_API in os.environ\n\n# Setting a default value for QT_API\nos.environ.setdefault(QT_API, 'pyqt5')\n\nAPI = os.environ[QT_API].lower()\ninitial_api = API\nassert API in (PYQT5_API + PYQT4_API + PYSIDE_API + PYSIDE2_API)\n\nis_old_pyqt = is_pyqt46 = False\nPYQT5 = True\nPYQT4 = PYSIDE = PYSIDE2 = False\n\n# When `FORCE_QT_API` is set, we disregard\n# any previously imported python bindings.\nif not os.environ.get('FORCE_QT_API'):\n    if 'PyQt5' in sys.modules:\n        API = initial_api if initial_api in PYQT5_API else 'pyqt5'\n    elif 'PySide2' in sys.modules:\n        API = initial_api if initial_api in PYSIDE2_API else 'pyside2'\n    elif 'PyQt4' in sys.modules:\n        API = initial_api if initial_api in PYQT4_API else 'pyqt4'\n    elif 'PySide' in sys.modules:\n        API = initial_api if initial_api in PYSIDE_API else 'pyside'\n\n\nif API in PYQT5_API:\n    try:\n        from PyQt5.QtCore import PYQT_VERSION_STR as PYQT_VERSION  # analysis:ignore\n        from PyQt5.QtCore import QT_VERSION_STR as QT_VERSION  # analysis:ignore\n        PYSIDE_VERSION = None\n\n        if sys.platform == 'darwin':\n            macos_version = LooseVersion(platform.mac_ver()[0])\n            if macos_version < LooseVersion('10.10'):\n                if LooseVersion(QT_VERSION) >= LooseVersion('5.9'):\n                    raise PythonQtError(\"Qt 5.9 or higher only works in \"\n                                        \"macOS 10.10 or higher. Your \"\n                                        \"program will fail in this \"\n                                        \"system.\")\n            elif macos_version < LooseVersion('10.11'):\n                if LooseVersion(QT_VERSION) >= LooseVersion('5.11'):\n                    raise PythonQtError(\"Qt 5.11 or higher only works in \"\n                                        \"macOS 10.11 or higher. Your \"\n                                        \"program will fail in this \"\n                                        \"system.\")\n\n            del macos_version\n    except ImportError:\n        API = os.environ['QT_API'] = 'pyside2'\n\nif API in PYSIDE2_API:\n    try:\n        from PySide2 import __version__ as PYSIDE_VERSION  # analysis:ignore\n        from PySide2.QtCore import __version__ as QT_VERSION  # analysis:ignore\n\n        PYQT_VERSION = None\n        PYQT5 = False\n        PYSIDE2 = True\n\n        if sys.platform == 'darwin':\n            macos_version = LooseVersion(platform.mac_ver()[0])\n            if macos_version < LooseVersion('10.11'):\n                if LooseVersion(QT_VERSION) >= LooseVersion('5.11'):\n                    raise PythonQtError(\"Qt 5.11 or higher only works in \"\n                                        \"macOS 10.11 or higher. Your \"\n                                        \"program will fail in this \"\n                                        \"system.\")\n\n            del macos_version\n    except ImportError:\n        API = os.environ['QT_API'] = 'pyqt'\n\nif API in PYQT4_API:\n    try:\n        import sip\n        try:\n            sip.setapi('QString', 2)\n            sip.setapi('QVariant', 2)\n            sip.setapi('QDate', 2)\n            sip.setapi('QDateTime', 2)\n            sip.setapi('QTextStream', 2)\n            sip.setapi('QTime', 2)\n            sip.setapi('QUrl', 2)\n        except (AttributeError, ValueError):\n            # PyQt < v4.6\n            pass\n        try:\n            from PyQt4.Qt import PYQT_VERSION_STR as PYQT_VERSION  # analysis:ignore\n            from PyQt4.Qt import QT_VERSION_STR as QT_VERSION  # analysis:ignore\n        except ImportError:\n            # In PyQt4-sip 4.19.13 PYQT_VERSION_STR and QT_VERSION_STR are in PyQt4.QtCore\n            from PyQt4.QtCore import PYQT_VERSION_STR as PYQT_VERSION  # analysis:ignore\n            from PyQt4.QtCore import QT_VERSION_STR as QT_VERSION  # analysis:ignore\n        PYSIDE_VERSION = None\n        PYQT5 = False\n        PYQT4 = True\n    except ImportError:\n        API = os.environ['QT_API'] = 'pyside'\n    else:\n        is_old_pyqt = PYQT_VERSION.startswith(('4.4', '4.5', '4.6', '4.7'))\n        is_pyqt46 = PYQT_VERSION.startswith('4.6')\n\nif API in PYSIDE_API:\n    try:\n        from PySide import __version__ as PYSIDE_VERSION  # analysis:ignore\n        from PySide.QtCore import __version__ as QT_VERSION  # analysis:ignore\n        PYQT_VERSION = None\n        PYQT5 = PYSIDE2 = False\n        PYSIDE = True\n    except ImportError:\n        raise PythonQtError('No Qt bindings could be found')\n\n# If a correct API name is passed to QT_API and it could not be found,\n# switches to another and informs through the warning\nif API != initial_api and binding_specified:\n    warnings.warn('Selected binding \"{}\" could not be found, '\n                  'using \"{}\"'.format(initial_api, API), RuntimeWarning)\n\nAPI_NAME = {'pyqt5': 'PyQt5', 'pyqt': 'PyQt4', 'pyqt4': 'PyQt4',\n            'pyside': 'PySide', 'pyside2':'PySide2'}[API]\n\nif PYQT4:\n    import sip\n    try:\n        API_NAME += (\" (API v{0})\".format(sip.getapi('QString')))\n    except AttributeError:\n        pass\n\ntry:\n    # QtDataVisualization backward compatibility (QtDataVisualization vs. QtDatavisualization)\n    # Only available for Qt5 bindings > 5.9 on Windows\n    from . import QtDataVisualization as QtDatavisualization\nexcept (ImportError, PythonQtError):\n    pass\n\n\ndef _warn_old_minor_version(name, old_version, min_version):\n    warning_message = (\n        \"{name} version {old_version} is unsupported upstream and \"\n        \"deprecated by QtPy. To ensure your application is still supported \"\n        \"in QtPy 2.0, please make sure it doesn't depend upon {name} versions \"\n        \"older than {min_version}.\".format(\n            name=name, old_version=old_version, min_version=min_version))\n    warnings.warn(warning_message, DeprecationWarning)\n\n\n# Warn if using a legacy, soon to be unsupported Qt API/binding\nif API in LEGACY_APIS or initial_api in LEGACY_APIS:\n    warnings.warn(\n        \"A deprecated Qt4-based binding (PyQt4/PySide) was installed, \"\n        \"imported or set via the 'QT_API' environment variable. \"\n        \"To ensure your application is still supported in QtPy 2.0, \"\n        \"please make sure it doesn't depend upon, import or \"\n        \"set the 'QT_API' env var to 'pyqt', 'pyqt4' or 'pyside'.\",\n        DeprecationWarning,\n    )\nelse:\n    if LooseVersion(QT_VERSION) < LooseVersion(QT_VERSION_MIN):\n        _warn_old_minor_version('Qt', QT_VERSION, QT_VERSION_MIN)\n    if PYQT_VERSION and (LooseVersion(PYQT_VERSION)\n                         < LooseVersion(PYQT_VERSION_MIN)):\n        _warn_old_minor_version('PyQt', PYQT_VERSION, PYQT_VERSION_MIN)\n    elif PYSIDE_VERSION and (LooseVersion(PYSIDE_VERSION)\n                             < LooseVersion(PYSIDE_VERSION_MIN)):\n        _warn_old_minor_version('PySide', PYSIDE_VERSION, PYSIDE_VERSION_MIN)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/_patch/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/_patch/qcombobox.py",
    "content": "# The code below, as well as the associated test were adapted from\n# qt-helpers, which was released under a 3-Clause BSD license:\n#\n# Copyright (c) 2015, Chris Beaumont and Thomas Robitaille\n#\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n#  * Redistributions of source code must retain the above copyright\n#    notice, this list of conditions and the following disclaimer.\n#  * Redistributions in binary form must reproduce the above copyright\n#    notice, this list of conditions and the following disclaimer in the\n#    documentation and/or other materials provided with the\n#    distribution.\n#  * Neither the name of the Glue project nor the names of its\n#    contributors may be used to endorse or promote products derived\n#    from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\n# IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\n# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR\n# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\ndef patch_qcombobox(QComboBox):\n    \"\"\"\n    In PySide, using Python objects as userData in QComboBox causes\n    Segmentation faults under certain conditions. Even in cases where it\n    doesn't, findData does not work correctly. Likewise, findData also does not\n    work correctly with Python objects when using PyQt4. On the other hand,\n    PyQt5 deals with this case correctly. We therefore patch QComboBox when\n    using PyQt4 and PySide to avoid issues.\n    \"\"\"\n\n    from ..QtGui import QIcon\n    from ..QtCore import Qt, QObject\n\n    class userDataWrapper():\n        \"\"\"\n        This class is used to wrap any userData object. If we don't do this,\n        then certain types of objects can cause segmentation faults or issues\n        depending on whether/how __getitem__ is defined.\n        \"\"\"\n        def __init__(self, data):\n            self.data = data\n\n    _addItem = QComboBox.addItem\n\n    def addItem(self, *args, **kwargs):\n        if len(args) == 3 or (not isinstance(args[0], QIcon)\n                              and len(args) == 2):\n            args, kwargs['userData'] = args[:-1], args[-1]\n        if 'userData' in kwargs:\n            kwargs['userData'] = userDataWrapper(kwargs['userData'])\n        _addItem(self, *args, **kwargs)\n\n    _insertItem = QComboBox.insertItem\n\n    def insertItem(self, *args, **kwargs):\n        if len(args) == 4 or (not isinstance(args[1], QIcon)\n                              and len(args) == 3):\n            args, kwargs['userData'] = args[:-1], args[-1]\n        if 'userData' in kwargs:\n            kwargs['userData'] = userDataWrapper(kwargs['userData'])\n        _insertItem(self, *args, **kwargs)\n\n    _setItemData = QComboBox.setItemData\n\n    def setItemData(self, index, value, role=Qt.UserRole):\n        value = userDataWrapper(value)\n        _setItemData(self, index, value, role=role)\n\n    _itemData = QComboBox.itemData\n\n    def itemData(self, index, role=Qt.UserRole):\n        userData = _itemData(self, index, role=role)\n        if isinstance(userData, userDataWrapper):\n            userData = userData.data\n        return userData\n\n    def findData(self, value):\n        for i in range(self.count()):\n            if self.itemData(i) == value:\n                return i\n        return -1\n\n    QComboBox.addItem = addItem\n    QComboBox.insertItem = insertItem\n    QComboBox.setItemData = setItemData\n    QComboBox.itemData = itemData\n    QComboBox.findData = findData"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/_patch/qheaderview.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# (see LICENSE.txt for details)\nimport warnings\n\ndef introduce_renamed_methods_qheaderview(QHeaderView):\n\n    _isClickable = QHeaderView.isClickable\n    def sectionsClickable(self):\n        \"\"\"\n        QHeaderView.sectionsClickable() -> bool\n        \"\"\"\n        return _isClickable(self)\n    QHeaderView.sectionsClickable = sectionsClickable\n    def isClickable(self):\n        warnings.warn('isClickable is only available in Qt4. Use '\n                        'sectionsClickable instead.', stacklevel=2)\n        return _isClickable(self)\n    QHeaderView.isClickable = isClickable\n\n\n    _isMovable = QHeaderView.isMovable\n    def sectionsMovable(self):\n        \"\"\"\n        QHeaderView.sectionsMovable() -> bool\n        \"\"\"\n        return _isMovable(self)\n    QHeaderView.sectionsMovable = sectionsMovable\n    def isMovable(self):\n        warnings.warn('isMovable is only available in Qt4. Use '\n                        'sectionsMovable instead.', stacklevel=2)\n        return _isMovable(self)\n    QHeaderView.isMovable = isMovable\n\n\n    _resizeMode = QHeaderView.resizeMode\n    def sectionResizeMode(self, logicalIndex):\n        \"\"\"\n        QHeaderView.sectionResizeMode(int) -> QHeaderView.ResizeMode\n        \"\"\"\n        return _resizeMode(self, logicalIndex)\n    QHeaderView.sectionResizeMode = sectionResizeMode\n    def resizeMode(self, logicalIndex):\n        warnings.warn('resizeMode is only available in Qt4. Use '\n                        'sectionResizeMode instead.', stacklevel=2)\n        return _resizeMode(self, logicalIndex)\n    QHeaderView.resizeMode = resizeMode\n\n    _setClickable = QHeaderView.setClickable\n    def setSectionsClickable(self, clickable):\n        \"\"\"\n        QHeaderView.setSectionsClickable(bool)\n        \"\"\"\n        return _setClickable(self, clickable)\n    QHeaderView.setSectionsClickable = setSectionsClickable\n    def setClickable(self, clickable):\n        warnings.warn('setClickable is only available in Qt4. Use '\n                        'setSectionsClickable instead.', stacklevel=2)\n        return _setClickable(self, clickable)\n    QHeaderView.setClickable = setClickable\n\n\n    _setMovable = QHeaderView.setMovable\n    def setSectionsMovable(self, movable):\n        \"\"\"\n        QHeaderView.setSectionsMovable(bool)\n        \"\"\"\n        return _setMovable(self, movable)\n    QHeaderView.setSectionsMovable = setSectionsMovable\n    def setMovable(self, movable):\n        warnings.warn('setMovable is only available in Qt4. Use '\n                        'setSectionsMovable instead.', stacklevel=2)\n        return _setMovable(self, movable)\n    QHeaderView.setMovable = setMovable\n\n\n    _setResizeMode = QHeaderView.setResizeMode\n    def setSectionResizeMode(self, *args):\n        \"\"\"\n        QHeaderView.setSectionResizeMode(QHeaderView.ResizeMode)\n        QHeaderView.setSectionResizeMode(int, QHeaderView.ResizeMode)\n        \"\"\"\n        _setResizeMode(self, *args)\n    QHeaderView.setSectionResizeMode = setSectionResizeMode\n    def setResizeMode(self, *args):\n        warnings.warn('setResizeMode is only available in Qt4. Use '\n                        'setSectionResizeMode instead.', stacklevel=2)\n        _setResizeMode(self, *args)\n    QHeaderView.setResizeMode = setResizeMode\n\n\n\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/_version.py",
    "content": "version_info = (1, 11, 3)\n__version__ = '.'.join(map(str, version_info))\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/compat.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © 2009- The Spyder Development Team\n# Licensed under the terms of the MIT License\n\n\"\"\"\nCompatibility functions\n\"\"\"\n\nfrom __future__ import print_function\nimport sys\n\nfrom . import PYQT4\nfrom .QtWidgets import QFileDialog\nfrom .py3compat import Callable, is_text_string, to_text_string, TEXT_TYPES\n\n\n# =============================================================================\n# QVariant conversion utilities\n# =============================================================================\nPYQT_API_1 = False\nif PYQT4:\n    import sip\n    try:\n        PYQT_API_1 = sip.getapi('QVariant') == 1  # PyQt API #1\n    except AttributeError:\n        # PyQt <v4.6\n        PYQT_API_1 = True\n\n    def to_qvariant(pyobj=None):\n        \"\"\"Convert Python object to QVariant\n        This is a transitional function from PyQt API #1 (QVariant exist)\n        to PyQt API #2 and Pyside (QVariant does not exist)\"\"\"\n        if PYQT_API_1:\n            # PyQt API #1\n            from PyQt4.QtCore import QVariant\n            return QVariant(pyobj)\n        else:\n            # PyQt API #2\n            return pyobj\n\n    def from_qvariant(qobj=None, convfunc=None):\n        \"\"\"Convert QVariant object to Python object\n        This is a transitional function from PyQt API #1 (QVariant exist)\n        to PyQt API #2 and Pyside (QVariant does not exist)\"\"\"\n        if PYQT_API_1:\n            # PyQt API #1\n            assert isinstance(convfunc, Callable)\n            if convfunc in TEXT_TYPES or convfunc is to_text_string:\n                return convfunc(qobj.toString())\n            elif convfunc is bool:\n                return qobj.toBool()\n            elif convfunc is int:\n                return qobj.toInt()[0]\n            elif convfunc is float:\n                return qobj.toDouble()[0]\n            else:\n                return convfunc(qobj)\n        else:\n            # PyQt API #2\n            return qobj\nelse:\n    def to_qvariant(obj=None):  # analysis:ignore\n        \"\"\"Convert Python object to QVariant\n        This is a transitional function from PyQt API#1 (QVariant exist)\n        to PyQt API#2 and Pyside (QVariant does not exist)\"\"\"\n        return obj\n\n    def from_qvariant(qobj=None, pytype=None):  # analysis:ignore\n        \"\"\"Convert QVariant object to Python object\n        This is a transitional function from PyQt API #1 (QVariant exist)\n        to PyQt API #2 and Pyside (QVariant does not exist)\"\"\"\n        return qobj\n\n\n# =============================================================================\n# Wrappers around QFileDialog static methods\n# =============================================================================\ndef getexistingdirectory(parent=None, caption='', basedir='',\n                         options=QFileDialog.ShowDirsOnly):\n    \"\"\"Wrapper around QtGui.QFileDialog.getExistingDirectory static method\n    Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0\"\"\"\n    # Calling QFileDialog static method\n    if sys.platform == \"win32\":\n        # On Windows platforms: redirect standard outputs\n        _temp1, _temp2 = sys.stdout, sys.stderr\n        sys.stdout, sys.stderr = None, None\n    try:\n        result = QFileDialog.getExistingDirectory(parent, caption, basedir,\n                                                  options)\n    finally:\n        if sys.platform == \"win32\":\n            # On Windows platforms: restore standard outputs\n            sys.stdout, sys.stderr = _temp1, _temp2\n    if not is_text_string(result):\n        # PyQt API #1\n        result = to_text_string(result)\n    return result\n\n\ndef _qfiledialog_wrapper(attr, parent=None, caption='', basedir='',\n                         filters='', selectedfilter='', options=None):\n    if options is None:\n        options = QFileDialog.Options(0)\n    try:\n        # PyQt <v4.6 (API #1)\n        from .QtCore import QString\n    except ImportError:\n        # PySide or PyQt >=v4.6\n        QString = None  # analysis:ignore\n    tuple_returned = True\n    try:\n        # PyQt >=v4.6\n        func = getattr(QFileDialog, attr+'AndFilter')\n    except AttributeError:\n        # PySide or PyQt <v4.6\n        func = getattr(QFileDialog, attr)\n        if QString is not None:\n            selectedfilter = QString()\n            tuple_returned = False\n\n    # Calling QFileDialog static method\n    if sys.platform == \"win32\":\n        # On Windows platforms: redirect standard outputs\n        _temp1, _temp2 = sys.stdout, sys.stderr\n        sys.stdout, sys.stderr = None, None\n    try:\n        result = func(parent, caption, basedir,\n                      filters, selectedfilter, options)\n    except TypeError:\n        # The selectedfilter option (`initialFilter` in Qt) has only been\n        # introduced in Jan. 2010 for PyQt v4.7, that's why we handle here\n        # the TypeError exception which will be raised with PyQt v4.6\n        # (see Issue 960 for more details)\n        result = func(parent, caption, basedir, filters, options)\n    finally:\n        if sys.platform == \"win32\":\n            # On Windows platforms: restore standard outputs\n            sys.stdout, sys.stderr = _temp1, _temp2\n\n    # Processing output\n    if tuple_returned:\n        # PySide or PyQt >=v4.6\n        output, selectedfilter = result\n    else:\n        # PyQt <v4.6 (API #1)\n        output = result\n    if QString is not None:\n        # PyQt API #1: conversions needed from QString/QStringList\n        selectedfilter = to_text_string(selectedfilter)\n        if isinstance(output, QString):\n            # Single filename\n            output = to_text_string(output)\n        else:\n            # List of filenames\n            output = [to_text_string(fname) for fname in output]\n\n    # Always returns the tuple (output, selectedfilter)\n    return output, selectedfilter\n\n\ndef getopenfilename(parent=None, caption='', basedir='', filters='',\n                    selectedfilter='', options=None):\n    \"\"\"Wrapper around QtGui.QFileDialog.getOpenFileName static method\n    Returns a tuple (filename, selectedfilter) -- when dialog box is canceled,\n    returns a tuple of empty strings\n    Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0\"\"\"\n    return _qfiledialog_wrapper('getOpenFileName', parent=parent,\n                                caption=caption, basedir=basedir,\n                                filters=filters, selectedfilter=selectedfilter,\n                                options=options)\n\n\ndef getopenfilenames(parent=None, caption='', basedir='', filters='',\n                     selectedfilter='', options=None):\n    \"\"\"Wrapper around QtGui.QFileDialog.getOpenFileNames static method\n    Returns a tuple (filenames, selectedfilter) -- when dialog box is canceled,\n    returns a tuple (empty list, empty string)\n    Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0\"\"\"\n    return _qfiledialog_wrapper('getOpenFileNames', parent=parent,\n                                caption=caption, basedir=basedir,\n                                filters=filters, selectedfilter=selectedfilter,\n                                options=options)\n\n\ndef getsavefilename(parent=None, caption='', basedir='', filters='',\n                    selectedfilter='', options=None):\n    \"\"\"Wrapper around QtGui.QFileDialog.getSaveFileName static method\n    Returns a tuple (filename, selectedfilter) -- when dialog box is canceled,\n    returns a tuple of empty strings\n    Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0\"\"\"\n    return _qfiledialog_wrapper('getSaveFileName', parent=parent,\n                                caption=caption, basedir=basedir,\n                                filters=filters, selectedfilter=selectedfilter,\n                                options=options)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/py3compat.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright © 2012-2013 Pierre Raybaut\n# Licensed under the terms of the MIT License\n# (see spyderlib/__init__.py for details)\n\n\"\"\"\nspyderlib.py3compat\n-------------------\n\nTransitional module providing compatibility functions intended to help\nmigrating from Python 2 to Python 3.\n\nThis module should be fully compatible with:\n    * Python >=v2.6\n    * Python 3\n\"\"\"\n\nfrom __future__ import print_function\n\nimport sys\nimport os\n\nPY2 = sys.version_info[0] == 2\nPY3 = sys.version_info[0] == 3\nPY33 = PY3 and sys.version_info[1] >= 3\n\n\n# =============================================================================\n# Data types\n# =============================================================================\nif PY2:\n    # Python 2\n    TEXT_TYPES = (str, unicode)\n    INT_TYPES = (int, long)\nelse:\n    # Python 3\n    TEXT_TYPES = (str,)\n    INT_TYPES = (int,)\nNUMERIC_TYPES = tuple(list(INT_TYPES) + [float, complex])\n\n\n# =============================================================================\n# Renamed/Reorganized modules\n# =============================================================================\nif PY2:\n    # Python 2\n    import __builtin__ as builtins\n    from collections import Callable, MutableMapping\n    import ConfigParser as configparser\n    try:\n        import _winreg as winreg\n    except ImportError:\n        pass\n    from sys import maxint as maxsize\n    try:\n        import CStringIO as io\n    except ImportError:\n        import StringIO as io\n    try:\n        import cPickle as pickle\n    except ImportError:\n        import pickle\n    from UserDict import DictMixin as MutableMapping\n    import thread as _thread\n    import repr as reprlib\nelse:\n    # Python 3\n    import builtins\n    import configparser\n    try:\n        import winreg\n    except ImportError:\n        pass\n    from sys import maxsize\n    import io\n    import pickle\n    if PY33:\n        from collections.abc import Callable, MutableMapping\n    else:\n        from collections import Callable, MutableMapping\n    import _thread\n    import reprlib\n\n\n# =============================================================================\n# Strings\n# =============================================================================\nif PY2:\n    # Python 2\n    import codecs\n\n    def u(obj):\n        \"\"\"Make unicode object\"\"\"\n        return codecs.unicode_escape_decode(obj)[0]\nelse:\n    # Python 3\n    def u(obj):\n        \"\"\"Return string as it is\"\"\"\n        return obj\n\n\ndef is_text_string(obj):\n    \"\"\"Return True if `obj` is a text string, False if it is anything else,\n    like binary data (Python 3) or QString (Python 2, PyQt API #1)\"\"\"\n    if PY2:\n        # Python 2\n        return isinstance(obj, basestring)\n    else:\n        # Python 3\n        return isinstance(obj, str)\n\n\ndef is_binary_string(obj):\n    \"\"\"Return True if `obj` is a binary string, False if it is anything else\"\"\"\n    if PY2:\n        # Python 2\n        return isinstance(obj, str)\n    else:\n        # Python 3\n        return isinstance(obj, bytes)\n\n\ndef is_string(obj):\n    \"\"\"Return True if `obj` is a text or binary Python string object,\n    False if it is anything else, like a QString (Python 2, PyQt API #1)\"\"\"\n    return is_text_string(obj) or is_binary_string(obj)\n\n\ndef is_unicode(obj):\n    \"\"\"Return True if `obj` is unicode\"\"\"\n    if PY2:\n        # Python 2\n        return isinstance(obj, unicode)\n    else:\n        # Python 3\n        return isinstance(obj, str)\n\n\ndef to_text_string(obj, encoding=None):\n    \"\"\"Convert `obj` to (unicode) text string\"\"\"\n    if PY2:\n        # Python 2\n        if encoding is None:\n            return unicode(obj)\n        else:\n            return unicode(obj, encoding)\n    else:\n        # Python 3\n        if encoding is None:\n            return str(obj)\n        elif isinstance(obj, str):\n            # In case this function is not used properly, this could happen\n            return obj\n        else:\n            return str(obj, encoding)\n\n\ndef to_binary_string(obj, encoding=None):\n    \"\"\"Convert `obj` to binary string (bytes in Python 3, str in Python 2)\"\"\"\n    if PY2:\n        # Python 2\n        if encoding is None:\n            return str(obj)\n        else:\n            return obj.encode(encoding)\n    else:\n        # Python 3\n        return bytes(obj, 'utf-8' if encoding is None else encoding)\n\n\n# =============================================================================\n# Function attributes\n# =============================================================================\ndef get_func_code(func):\n    \"\"\"Return function code object\"\"\"\n    if PY2:\n        # Python 2\n        return func.func_code\n    else:\n        # Python 3\n        return func.__code__\n\n\ndef get_func_name(func):\n    \"\"\"Return function name\"\"\"\n    if PY2:\n        # Python 2\n        return func.func_name\n    else:\n        # Python 3\n        return func.__name__\n\n\ndef get_func_defaults(func):\n    \"\"\"Return function default argument values\"\"\"\n    if PY2:\n        # Python 2\n        return func.func_defaults\n    else:\n        # Python 3\n        return func.__defaults__\n\n\n# =============================================================================\n# Special method attributes\n# =============================================================================\ndef get_meth_func(obj):\n    \"\"\"Return method function object\"\"\"\n    if PY2:\n        # Python 2\n        return obj.im_func\n    else:\n        # Python 3\n        return obj.__func__\n\n\ndef get_meth_class_inst(obj):\n    \"\"\"Return method class instance\"\"\"\n    if PY2:\n        # Python 2\n        return obj.im_self\n    else:\n        # Python 3\n        return obj.__self__\n\n\ndef get_meth_class(obj):\n    \"\"\"Return method class\"\"\"\n    if PY2:\n        # Python 2\n        return obj.im_class\n    else:\n        # Python 3\n        return obj.__self__.__class__\n\n\n# =============================================================================\n# Misc.\n# =============================================================================\nif PY2:\n    # Python 2\n    input = raw_input\n    getcwd = os.getcwdu\n    cmp = cmp\n    import string\n    str_lower = string.lower\n    from itertools import izip_longest as zip_longest\nelse:\n    # Python 3\n    input = input\n    getcwd = os.getcwd\n\n    def cmp(a, b):\n        return (a > b) - (a < b)\n    str_lower = str.lower\n    from itertools import zip_longest\n\n\ndef qbytearray_to_str(qba):\n    \"\"\"Convert QByteArray object to str in a way compatible with Python 2/3\"\"\"\n    return str(bytes(qba.toHex().data()).decode())\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/conftest.py",
    "content": "import os\n\n\ndef pytest_configure(config):\n    \"\"\"\n    This function gets run by py.test at the very start\n    \"\"\"\n\n    if 'USE_QT_API' in os.environ:\n        os.environ['QT_API'] = os.environ['USE_QT_API'].lower()\n\n    # We need to import qtpy here to make sure that the API versions get set\n    # straight away.\n    import qtpy\n\n\ndef pytest_report_header(config):\n    \"\"\"\n    This function is used by py.test to insert a customized header into the\n    test report.\n    \"\"\"\n\n    versions = os.linesep\n    versions += 'PyQt4: '\n\n    try:\n        from PyQt4 import Qt\n        versions += \"PyQt: {0} - Qt: {1}\".format(Qt.PYQT_VERSION_STR, Qt.QT_VERSION_STR)\n    except ImportError:\n        versions += 'not installed'\n    except AttributeError:\n        versions += 'unknown version'\n\n    versions += os.linesep\n    versions += 'PyQt5: '\n\n    try:\n        from PyQt5 import Qt\n        versions += \"PyQt: {0} - Qt: {1}\".format(Qt.PYQT_VERSION_STR, Qt.QT_VERSION_STR)\n    except ImportError:\n        versions += 'not installed'\n    except AttributeError:\n        versions += 'unknown version'\n\n    versions += os.linesep\n    versions += 'PySide: '\n\n    try:\n        import PySide\n        from PySide import QtCore\n        versions += \"PySide: {0} - Qt: {1}\".format(PySide.__version__, QtCore.__version__)\n    except ImportError:\n        versions += 'not installed'\n    except AttributeError:\n        versions += 'unknown version'\n\n    versions += os.linesep\n    versions += 'PySide2: '\n\n    try:\n        import PySide2\n        from PySide2 import QtCore\n        versions += \"PySide: {0} - Qt: {1}\".format(PySide2.__version__, QtCore.__version__)\n    except ImportError:\n        versions += 'not installed'\n    except AttributeError:\n        versions += 'unknown version'\n\n    versions += os.linesep\n    \n    return versions\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/runtests.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# ----------------------------------------------------------------------------\n# Copyright © 2015- The Spyder Development Team\n#\n# Licensed under the terms of the MIT License\n# ----------------------------------------------------------------------------\n\n\"\"\"File for running tests programmatically.\"\"\"\n\n# Standard library imports\nimport sys\n\n# Third party imports\nimport qtpy  # to ensure that Qt4 uses API v2\nimport pytest\n\n\ndef main():\n    \"\"\"Run pytest tests.\"\"\"\n    errno = pytest.main(['-x', 'qtpy',  '-v', '-rw', '--durations=10',\n                         '--cov=qtpy', '--cov-report=term-missing'])\n    sys.exit(errno)\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_macos_checks.py",
    "content": "from __future__ import absolute_import\n\nimport mock\nimport platform\nimport sys\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n\n@pytest.mark.skipif(not PYQT5, reason=\"Targeted to PyQt5\")\n@mock.patch.object(platform, 'mac_ver')\ndef test_qt59_exception(mac_ver, monkeypatch):\n    # Remove qtpy to reimport it again\n    try:\n        del sys.modules[\"qtpy\"]\n    except KeyError:\n        pass\n\n    # Patch stdlib to emulate a macOS system\n    monkeypatch.setattr(\"sys.platform\", 'darwin')\n    mac_ver.return_value = ('10.9.2',)\n\n    # Patch Qt version\n    monkeypatch.setattr(\"PyQt5.QtCore.QT_VERSION_STR\", '5.9.1')\n\n    # This should raise an Exception\n    with pytest.raises(Exception) as e:\n        import qtpy\n\n    assert '10.10' in str(e.value)\n    assert '5.9' in str(e.value)\n\n\n@pytest.mark.skipif(not PYQT5, reason=\"Targeted to PyQt5\")\n@mock.patch.object(platform, 'mac_ver')\ndef test_qt59_no_exception(mac_ver, monkeypatch):\n    # Remove qtpy to reimport it again\n    try:\n        del sys.modules[\"qtpy\"]\n    except KeyError:\n        pass\n\n    # Patch stdlib to emulate a macOS system\n    monkeypatch.setattr(\"sys.platform\", 'darwin')\n    mac_ver.return_value = ('10.10.1',)\n\n    # Patch Qt version\n    monkeypatch.setattr(\"PyQt5.QtCore.QT_VERSION_STR\", '5.9.5')\n\n    # This should not raise an Exception\n    try:\n        import qtpy\n    except Exception:\n        pytest.fail(\"Error!\")\n\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2),\n                    reason=\"Targeted to PyQt5 or PySide2\")\n@mock.patch.object(platform, 'mac_ver')\ndef test_qt511_exception(mac_ver, monkeypatch):\n    # Remove qtpy to reimport it again\n    try:\n        del sys.modules[\"qtpy\"]\n    except KeyError:\n        pass\n\n    # Patch stdlib to emulate a macOS system\n    monkeypatch.setattr(\"sys.platform\", 'darwin')\n    mac_ver.return_value = ('10.10.3',)\n\n    # Patch Qt version\n    if PYQT5:\n        monkeypatch.setattr(\"PyQt5.QtCore.QT_VERSION_STR\", '5.11.1')\n    else:\n        monkeypatch.setattr(\"PySide2.QtCore.__version__\", '5.11.1')\n\n    # This should raise an Exception\n    with pytest.raises(Exception) as e:\n        import qtpy\n\n    assert '10.11' in str(e.value)\n    assert '5.11' in str(e.value)\n\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2),\n                    reason=\"Targeted to PyQt5 or PySide2\")\n@mock.patch.object(platform, 'mac_ver')\ndef test_qt511_no_exception(mac_ver, monkeypatch):\n    # Remove qtpy to reimport it again\n    try:\n        del sys.modules[\"qtpy\"]\n    except KeyError:\n        pass\n\n    # Patch stdlib to emulate a macOS system\n    monkeypatch.setattr(\"sys.platform\", 'darwin')\n    mac_ver.return_value = ('10.13.2',)\n\n    # Patch Qt version\n    if PYQT5:\n        monkeypatch.setattr(\"PyQt5.QtCore.QT_VERSION_STR\", '5.11.1')\n    else:\n        monkeypatch.setattr(\"PySide2.QtCore.__version__\", '5.11.1')\n\n   # This should not raise an Exception\n    try:\n        import qtpy\n    except Exception:\n        pytest.fail(\"Error!\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_main.py",
    "content": "import os\n\nfrom qtpy import QtCore, QtGui, QtWidgets, QtWebEngineWidgets\n\n\ndef assert_pyside():\n    \"\"\"\n    Make sure that we are using PySide\n    \"\"\"\n    import PySide\n    assert QtCore.QEvent is PySide.QtCore.QEvent\n    assert QtGui.QPainter is PySide.QtGui.QPainter\n    assert QtWidgets.QWidget is PySide.QtGui.QWidget\n    assert QtWebEngineWidgets.QWebEnginePage is PySide.QtWebKit.QWebPage\n\ndef assert_pyside2():\n    \"\"\"\n    Make sure that we are using PySide\n    \"\"\"\n    import PySide2\n    assert QtCore.QEvent is PySide2.QtCore.QEvent\n    assert QtGui.QPainter is PySide2.QtGui.QPainter\n    assert QtWidgets.QWidget is PySide2.QtWidgets.QWidget\n    assert QtWebEngineWidgets.QWebEnginePage is PySide2.QtWebEngineWidgets.QWebEnginePage\n\ndef assert_pyqt4():\n    \"\"\"\n    Make sure that we are using PyQt4\n    \"\"\"\n    import PyQt4\n    assert QtCore.QEvent is PyQt4.QtCore.QEvent\n    assert QtGui.QPainter is PyQt4.QtGui.QPainter\n    assert QtWidgets.QWidget is PyQt4.QtGui.QWidget\n    assert QtWebEngineWidgets.QWebEnginePage is PyQt4.QtWebKit.QWebPage\n\n\ndef assert_pyqt5():\n    \"\"\"\n    Make sure that we are using PyQt5\n    \"\"\"\n    import PyQt5\n    assert QtCore.QEvent is PyQt5.QtCore.QEvent\n    assert QtGui.QPainter is PyQt5.QtGui.QPainter\n    assert QtWidgets.QWidget is PyQt5.QtWidgets.QWidget\n    if QtWebEngineWidgets.WEBENGINE:\n        assert QtWebEngineWidgets.QWebEnginePage is PyQt5.QtWebEngineWidgets.QWebEnginePage\n    else:\n        assert QtWebEngineWidgets.QWebEnginePage is PyQt5.QtWebKitWidgets.QWebPage\n\n\ndef test_qt_api():\n    \"\"\"\n    If QT_API is specified, we check that the correct Qt wrapper was used\n    \"\"\"\n\n    QT_API = os.environ.get('QT_API', '').lower()\n\n    if QT_API == 'pyside':\n        assert_pyside()\n    elif QT_API in ('pyqt', 'pyqt4'):\n        assert_pyqt4()\n    elif QT_API == 'pyqt5':\n        assert_pyqt5()\n    elif QT_API == 'pyside2':\n        assert_pyside2()\n    else:\n        # If the tests are run locally, USE_QT_API and QT_API may not be\n        # defined, but we still want to make sure qtpy is behaving sensibly.\n        # We should then be loading, in order of decreasing preference, PyQt5,\n        # PyQt4, and PySide.\n        try:\n            import PyQt5\n        except ImportError:\n            try:\n                import PyQt4\n            except ImportError:\n                import PySide\n                assert_pyside()\n            else:\n                assert_pyqt4()\n        else:\n            assert_pyqt5()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_patch_qcombobox.py",
    "content": "from __future__ import absolute_import\n\nimport os\nimport sys\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2, QtGui, QtWidgets\n\n\nPY3 = sys.version[0] == \"3\"\n\n\ndef get_qapp(icon_path=None):\n    qapp = QtWidgets.QApplication.instance()\n    if qapp is None:\n        qapp = QtWidgets.QApplication([''])\n    return qapp\n\n\nclass Data(object):\n    \"\"\"\n    Test class to store in userData. The __getitem__ is needed in order to\n    reproduce the segmentation fault.\n    \"\"\"\n    def __getitem__(self, item):\n        raise ValueError(\"Failing\")\n\n\n@pytest.mark.skipif(PY3 or (PYSIDE2 and os.environ.get('CI', None) is not None),\n                    reason=\"It segfaults in Python 3 and in our CIs with PySide2\")\ndef test_patched_qcombobox():\n    \"\"\"\n    In PySide, using Python objects as userData in QComboBox causes\n    Segmentation faults under certain conditions. Even in cases where it\n    doesn't, findData does not work correctly. Likewise, findData also\n    does not work correctly with Python objects when using PyQt4. On the\n    other hand, PyQt5 deals with this case correctly. We therefore patch\n    QComboBox when using PyQt4 and PySide to avoid issues.\n    \"\"\"\n\n    app = get_qapp()\n\n    data1 = Data()\n    data2 = Data()\n    data3 = Data()\n    data4 = Data()\n    data5 = Data()\n    data6 = Data()\n\n    icon1 = QtGui.QIcon()\n    icon2 = QtGui.QIcon()\n\n    widget = QtWidgets.QComboBox()\n    widget.addItem('a', data1)\n    widget.insertItem(0, 'b', data2)\n    widget.addItem('c', data1)\n    widget.setItemData(2, data3)\n    widget.addItem(icon1, 'd', data4)\n    widget.insertItem(3, icon2, 'e', data5)\n    widget.addItem(icon1, 'f')\n    widget.insertItem(5, icon2, 'g')\n\n    widget.show()\n\n    assert widget.findData(data1) == 1\n    assert widget.findData(data2) == 0\n    assert widget.findData(data3) == 2\n    assert widget.findData(data4) == 4\n    assert widget.findData(data5) == 3\n    assert widget.findData(data6) == -1\n\n    assert widget.itemData(0) == data2\n    assert widget.itemData(1) == data1\n    assert widget.itemData(2) == data3\n    assert widget.itemData(3) == data5\n    assert widget.itemData(4) == data4\n    assert widget.itemData(5) is None\n    assert widget.itemData(6) is None\n\n    assert widget.itemText(0) == 'b'\n    assert widget.itemText(1) == 'a'\n    assert widget.itemText(2) == 'c'\n    assert widget.itemText(3) == 'e'\n    assert widget.itemText(4) == 'd'\n    assert widget.itemText(5) == 'g'\n    assert widget.itemText(6) == 'f'\n\n\n@pytest.mark.skipif(((PYSIDE2 or PYQT5)\n                     and os.environ.get('CI', None) is not None),\n                    reason=\"It segfaults in our CIs with PYSIDE2 or PYQT5\")\ndef test_model_item():\n    \"\"\"\n    This is a regression test for an issue that caused the call to item(0)\n    below to trigger segmentation faults in PySide. The issue is\n    non-deterministic when running the call once, so we include a loop to make\n    sure that we trigger the fault.\n    \"\"\"\n    app = get_qapp()\n    combo = QtWidgets.QComboBox()\n    label_data = [('a', None)]\n    for iter in range(10000):\n        combo.clear()\n        for i, (label, data) in enumerate(label_data):\n            combo.addItem(label, userData=data)\n        model = combo.model()\n        model.item(0)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_patch_qheaderview.py",
    "content": "from __future__ import absolute_import\n\nimport sys\n\nimport pytest\nfrom qtpy import PYSIDE, PYSIDE2, PYQT4\nfrom qtpy.QtWidgets import QApplication\nfrom qtpy.QtWidgets import QHeaderView\nfrom qtpy.QtCore import Qt\nfrom qtpy.QtCore import QAbstractListModel\n\n\nPY3 = sys.version[0] == \"3\"\n\n\ndef get_qapp(icon_path=None):\n    qapp = QApplication.instance()\n    if qapp is None:\n        qapp = QApplication([''])\n    return qapp\n\n\n@pytest.mark.skipif(PY3 or PYSIDE2, reason=\"It fails on Python 3 and PySide2\")\ndef test_patched_qheaderview():\n    \"\"\"\n    This will test whether QHeaderView has the new methods introduced in Qt5.\n    It will then create an instance of QHeaderView and test that no exceptions\n    are raised and that some basic behaviour works.\n    \"\"\"\n    assert QHeaderView.sectionsClickable is not None\n    assert QHeaderView.sectionsMovable is not None\n    assert QHeaderView.sectionResizeMode is not None\n    assert QHeaderView.setSectionsClickable is not None\n    assert QHeaderView.setSectionsMovable is not None\n    assert QHeaderView.setSectionResizeMode is not None\n\n    # setup a model and add it to a headerview\n    qapp = get_qapp()\n    headerview = QHeaderView(Qt.Horizontal)\n    class Model(QAbstractListModel):\n        pass\n    model = Model()\n    headerview.setModel(model)\n    assert headerview.count() == 1\n\n    # test it\n    assert isinstance(headerview.sectionsClickable(), bool)\n    assert isinstance(headerview.sectionsMovable(), bool)\n    if PYSIDE:\n        assert isinstance(headerview.sectionResizeMode(0),\n                          QHeaderView.ResizeMode)\n    else:\n        assert isinstance(headerview.sectionResizeMode(0), int)\n\n    headerview.setSectionsClickable(True)\n    assert headerview.sectionsClickable() == True\n    headerview.setSectionsClickable(False)\n    assert headerview.sectionsClickable() == False\n\n    headerview.setSectionsMovable(True)\n    assert headerview.sectionsMovable() == True\n    headerview.setSectionsMovable(False)\n    assert headerview.sectionsMovable() == False\n\n    headerview.setSectionResizeMode(QHeaderView.Interactive)\n    assert headerview.sectionResizeMode(0) == QHeaderView.Interactive\n    headerview.setSectionResizeMode(QHeaderView.Fixed)\n    assert headerview.sectionResizeMode(0) == QHeaderView.Fixed\n    headerview.setSectionResizeMode(QHeaderView.Stretch)\n    assert headerview.sectionResizeMode(0) == QHeaderView.Stretch\n    headerview.setSectionResizeMode(QHeaderView.ResizeToContents)\n    assert headerview.sectionResizeMode(0) == QHeaderView.ResizeToContents\n\n    headerview.setSectionResizeMode(0, QHeaderView.Interactive)\n    assert headerview.sectionResizeMode(0) == QHeaderView.Interactive\n    headerview.setSectionResizeMode(0, QHeaderView.Fixed)\n    assert headerview.sectionResizeMode(0) == QHeaderView.Fixed\n    headerview.setSectionResizeMode(0, QHeaderView.Stretch)\n    assert headerview.sectionResizeMode(0) == QHeaderView.Stretch\n    headerview.setSectionResizeMode(0, QHeaderView.ResizeToContents)\n    assert headerview.sectionResizeMode(0) == QHeaderView.ResizeToContents\n\n    # test that the old methods in Qt4 raise exceptions\n    if PYQT4 or PYSIDE:\n        with pytest.warns(UserWarning):\n            headerview.isClickable()\n        with pytest.warns(UserWarning):\n            headerview.isMovable()\n        with pytest.warns(UserWarning):\n            headerview.resizeMode(0)\n        with pytest.warns(UserWarning):\n            headerview.setClickable(True)\n        with pytest.warns(UserWarning):\n            headerview.setMovable(True)\n        with pytest.warns(UserWarning):\n            headerview.setResizeMode(0, QHeaderView.Interactive)\n\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qdesktopservice_split.py",
    "content": "\"\"\"Test QDesktopServices split in Qt5.\"\"\"\n\nfrom __future__ import absolute_import\n\nimport pytest\nimport warnings\nfrom qtpy import PYQT4, PYSIDE\n\n\ndef test_qstandarpath():\n    \"\"\"Test the qtpy.QStandardPaths namespace\"\"\"\n    from qtpy.QtCore import QStandardPaths\n\n    assert QStandardPaths.StandardLocation is not None\n\n    # Attributes from QDesktopServices shouldn't be in QStandardPaths\n    with pytest.raises(AttributeError) as excinfo:\n        QStandardPaths.setUrlHandler\n\n\ndef test_qdesktopservice():\n    \"\"\"Test the qtpy.QDesktopServices namespace\"\"\"\n    from qtpy.QtGui import QDesktopServices\n\n    assert QDesktopServices.setUrlHandler is not None\n\n\n@pytest.mark.skipif(not (PYQT4 or PYSIDE), reason=\"Warning is only raised in old bindings\")\ndef test_qdesktopservice_qt4_pyside():\n    from qtpy.QtGui import QDesktopServices\n    # Attributes from QStandardPaths should raise a warning when imported\n    # from QDesktopServices\n    with warnings.catch_warnings(record=True) as w:\n        # Cause all warnings to always be triggered.\n        warnings.simplefilter(\"always\")\n        # Try to  import QtHelp.\n        QDesktopServices.StandardLocation\n\n        assert len(w) == 1\n        assert issubclass(w[-1].category, DeprecationWarning)\n        assert \"deprecated\" in str(w[-1].message)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qt3danimation.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qt3danimation():\n    \"\"\"Test the qtpy.Qt3DAnimation namespace\"\"\"\n    Qt3DAnimation = pytest.importorskip(\"qtpy.Qt3DAnimation\")\n\n    assert Qt3DAnimation.QAnimationController is not None\n    assert Qt3DAnimation.QAdditiveClipBlend is not None\n    assert Qt3DAnimation.QAbstractClipBlendNode is not None\n    assert Qt3DAnimation.QAbstractAnimation is not None\n    assert Qt3DAnimation.QKeyframeAnimation is not None\n    assert Qt3DAnimation.QAbstractAnimationClip is not None\n    assert Qt3DAnimation.QAbstractClipAnimator is not None\n    assert Qt3DAnimation.QClipAnimator is not None\n    assert Qt3DAnimation.QAnimationGroup is not None\n    assert Qt3DAnimation.QLerpClipBlend is not None\n    assert Qt3DAnimation.QMorphingAnimation is not None\n    assert Qt3DAnimation.QAnimationAspect is not None\n    assert Qt3DAnimation.QVertexBlendAnimation is not None\n    assert Qt3DAnimation.QBlendedClipAnimator is not None\n    assert Qt3DAnimation.QMorphTarget is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qt3dcore.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qt3dcore():\n    \"\"\"Test the qtpy.Qt3DCore namespace\"\"\"\n    Qt3DCore = pytest.importorskip(\"qtpy.Qt3DCore\")\n\n    assert Qt3DCore.QPropertyValueAddedChange is not None\n    assert Qt3DCore.QSkeletonLoader is not None\n    assert Qt3DCore.QPropertyNodeRemovedChange is not None\n    assert Qt3DCore.QPropertyUpdatedChange is not None\n    assert Qt3DCore.QAspectEngine is not None\n    assert Qt3DCore.QPropertyValueAddedChangeBase is not None\n    assert Qt3DCore.QStaticPropertyValueRemovedChangeBase is not None\n    assert Qt3DCore.QPropertyNodeAddedChange is not None\n    assert Qt3DCore.QDynamicPropertyUpdatedChange is not None\n    assert Qt3DCore.QStaticPropertyUpdatedChangeBase is not None\n    assert Qt3DCore.ChangeFlags is not None\n    assert Qt3DCore.QAbstractAspect is not None\n    assert Qt3DCore.QBackendNode is not None\n    assert Qt3DCore.QTransform is not None\n    assert Qt3DCore.QPropertyUpdatedChangeBase is not None\n    assert Qt3DCore.QNodeId is not None\n    assert Qt3DCore.QJoint is not None\n    assert Qt3DCore.QSceneChange is not None\n    assert Qt3DCore.QNodeIdTypePair is not None\n    assert Qt3DCore.QAbstractSkeleton is not None\n    assert Qt3DCore.QComponentRemovedChange is not None\n    assert Qt3DCore.QComponent is not None\n    assert Qt3DCore.QEntity is not None\n    assert Qt3DCore.QNodeCommand is not None\n    assert Qt3DCore.QNode is not None\n    assert Qt3DCore.QPropertyValueRemovedChange is not None\n    assert Qt3DCore.QPropertyValueRemovedChangeBase is not None\n    assert Qt3DCore.QComponentAddedChange is not None\n    assert Qt3DCore.QNodeCreatedChangeBase is not None\n    assert Qt3DCore.QNodeDestroyedChange is not None\n    assert Qt3DCore.QArmature is not None\n    assert Qt3DCore.QStaticPropertyValueAddedChangeBase is not None\n    assert Qt3DCore.ChangeFlag is not None\n    assert Qt3DCore.QSkeleton is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qt3dextras.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qt3dextras():\n    \"\"\"Test the qtpy.Qt3DExtras namespace\"\"\"\n    Qt3DExtras = pytest.importorskip(\"qtpy.Qt3DExtras\")\n\n    assert Qt3DExtras.QTextureMaterial is not None\n    assert Qt3DExtras.QPhongAlphaMaterial is not None\n    assert Qt3DExtras.QOrbitCameraController is not None\n    assert Qt3DExtras.QAbstractSpriteSheet is not None\n    assert Qt3DExtras.QNormalDiffuseMapMaterial is not None\n    assert Qt3DExtras.QDiffuseSpecularMaterial is not None\n    assert Qt3DExtras.QSphereGeometry is not None\n    assert Qt3DExtras.QCuboidGeometry is not None\n    assert Qt3DExtras.QForwardRenderer is not None\n    assert Qt3DExtras.QPhongMaterial is not None\n    assert Qt3DExtras.QSpriteGrid is not None\n    assert Qt3DExtras.QDiffuseMapMaterial is not None\n    assert Qt3DExtras.QConeGeometry is not None\n    assert Qt3DExtras.QSpriteSheetItem is not None\n    assert Qt3DExtras.QPlaneGeometry is not None\n    assert Qt3DExtras.QSphereMesh is not None\n    assert Qt3DExtras.QNormalDiffuseSpecularMapMaterial is not None\n    assert Qt3DExtras.QCuboidMesh is not None\n    assert Qt3DExtras.QGoochMaterial is not None\n    assert Qt3DExtras.QText2DEntity is not None\n    assert Qt3DExtras.QTorusMesh is not None\n    assert Qt3DExtras.Qt3DWindow is not None\n    assert Qt3DExtras.QPerVertexColorMaterial is not None\n    assert Qt3DExtras.QExtrudedTextGeometry is not None\n    assert Qt3DExtras.QSkyboxEntity is not None\n    assert Qt3DExtras.QAbstractCameraController is not None\n    assert Qt3DExtras.QExtrudedTextMesh is not None\n    assert Qt3DExtras.QCylinderGeometry is not None\n    assert Qt3DExtras.QTorusGeometry is not None\n    assert Qt3DExtras.QMorphPhongMaterial is not None\n    assert Qt3DExtras.QPlaneMesh is not None\n    assert Qt3DExtras.QDiffuseSpecularMapMaterial is not None\n    assert Qt3DExtras.QSpriteSheet is not None\n    assert Qt3DExtras.QConeMesh is not None\n    assert Qt3DExtras.QFirstPersonCameraController is not None\n    assert Qt3DExtras.QMetalRoughMaterial is not None\n    assert Qt3DExtras.QCylinderMesh is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qt3dinput.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qt3dinput():\n    \"\"\"Test the qtpy.Qt3DInput namespace\"\"\"\n    Qt3DInput = pytest.importorskip(\"qtpy.Qt3DInput\")\n\n    assert Qt3DInput.QAxisAccumulator is not None\n    assert Qt3DInput.QInputSettings is not None\n    assert Qt3DInput.QAnalogAxisInput is not None\n    assert Qt3DInput.QAbstractAxisInput is not None\n    assert Qt3DInput.QMouseHandler is not None\n    assert Qt3DInput.QButtonAxisInput is not None\n    assert Qt3DInput.QInputSequence is not None\n    assert Qt3DInput.QWheelEvent is not None\n    assert Qt3DInput.QActionInput is not None\n    assert Qt3DInput.QKeyboardDevice is not None\n    assert Qt3DInput.QMouseDevice is not None\n    assert Qt3DInput.QAxis is not None\n    assert Qt3DInput.QInputChord is not None\n    assert Qt3DInput.QMouseEvent is not None\n    assert Qt3DInput.QKeyboardHandler is not None\n    assert Qt3DInput.QKeyEvent is not None\n    assert Qt3DInput.QAbstractActionInput is not None\n    assert Qt3DInput.QInputAspect is not None\n    assert Qt3DInput.QLogicalDevice is not None\n    assert Qt3DInput.QAction is not None\n    assert Qt3DInput.QAbstractPhysicalDevice is not None\n    assert Qt3DInput.QAxisSetting is not None\n    \n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qt3dlogic.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qt3dlogic():\n    \"\"\"Test the qtpy.Qt3DLogic namespace\"\"\"\n    Qt3DLogic = pytest.importorskip(\"qtpy.Qt3DLogic\")\n  \n    assert Qt3DLogic.QLogicAspect is not None\n    assert Qt3DLogic.QFrameAction is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qt3drender.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qt3drender():\n    \"\"\"Test the qtpy.Qt3DRender namespace\"\"\"\n    Qt3DRender = pytest.importorskip(\"qtpy.Qt3DRender\")\n\n    assert Qt3DRender.QPointSize is not None\n    assert Qt3DRender.QFrustumCulling is not None\n    assert Qt3DRender.QPickPointEvent is not None\n    assert Qt3DRender.QRenderPassFilter is not None\n    assert Qt3DRender.QMesh is not None\n    assert Qt3DRender.QRayCaster is not None\n    assert Qt3DRender.QStencilMask is not None\n    assert Qt3DRender.QPickLineEvent is not None\n    assert Qt3DRender.QPickTriangleEvent is not None\n    assert Qt3DRender.QRenderState is not None\n    assert Qt3DRender.QTextureWrapMode is not None\n    assert Qt3DRender.QRenderPass is not None\n    assert Qt3DRender.QGeometryRenderer is not None\n    assert Qt3DRender.QAttribute is not None\n    assert Qt3DRender.QStencilOperation is not None\n    assert Qt3DRender.QScissorTest is not None\n    assert Qt3DRender.QTextureCubeMapArray is not None\n    assert Qt3DRender.QRenderTarget is not None\n    assert Qt3DRender.QStencilTest is not None\n    assert Qt3DRender.QTextureData is not None\n    assert Qt3DRender.QBuffer is not None\n    assert Qt3DRender.QLineWidth is not None\n    assert Qt3DRender.QLayer is not None\n    assert Qt3DRender.QTextureRectangle is not None\n    assert Qt3DRender.QRenderTargetSelector is not None\n    assert Qt3DRender.QPickingSettings is not None\n    assert Qt3DRender.QCullFace is not None\n    assert Qt3DRender.QAbstractFunctor is not None\n    assert Qt3DRender.PropertyReaderInterface is not None\n    assert Qt3DRender.QMaterial is not None\n    assert Qt3DRender.QAlphaCoverage is not None\n    assert Qt3DRender.QClearBuffers is not None\n    assert Qt3DRender.QAlphaTest is not None\n    assert Qt3DRender.QStencilOperationArguments is not None\n    assert Qt3DRender.QTexture2DMultisample is not None\n    assert Qt3DRender.QLevelOfDetailSwitch is not None\n    assert Qt3DRender.QRenderStateSet is not None\n    assert Qt3DRender.QViewport is not None\n    assert Qt3DRender.QObjectPicker is not None\n    assert Qt3DRender.QPolygonOffset is not None\n    assert Qt3DRender.QRenderSettings is not None\n    assert Qt3DRender.QFrontFace is not None\n    assert Qt3DRender.QTexture3D is not None\n    assert Qt3DRender.QTextureBuffer is not None\n    assert Qt3DRender.QTechniqueFilter is not None\n    assert Qt3DRender.QLayerFilter is not None\n    assert Qt3DRender.QFilterKey is not None\n    assert Qt3DRender.QRenderSurfaceSelector is not None\n    assert Qt3DRender.QEnvironmentLight is not None\n    assert Qt3DRender.QMemoryBarrier is not None\n    assert Qt3DRender.QNoDepthMask is not None\n    assert Qt3DRender.QBlitFramebuffer is not None\n    assert Qt3DRender.QGraphicsApiFilter is not None\n    assert Qt3DRender.QAbstractTexture is not None\n    assert Qt3DRender.QRenderCaptureReply is not None\n    assert Qt3DRender.QAbstractLight is not None\n    assert Qt3DRender.QAbstractRayCaster is not None\n    assert Qt3DRender.QDirectionalLight is not None\n    assert Qt3DRender.QDispatchCompute is not None\n    assert Qt3DRender.QBufferDataGenerator is not None\n    assert Qt3DRender.QPointLight is not None\n    assert Qt3DRender.QStencilTestArguments is not None\n    assert Qt3DRender.QTexture1D is not None\n    assert Qt3DRender.QCameraSelector is not None\n    assert Qt3DRender.QProximityFilter is not None\n    assert Qt3DRender.QTexture1DArray is not None\n    assert Qt3DRender.QBlendEquation is not None\n    assert Qt3DRender.QTextureImageDataGenerator is not None\n    assert Qt3DRender.QSpotLight is not None\n    assert Qt3DRender.QEffect is not None\n    assert Qt3DRender.QSeamlessCubemap is not None\n    assert Qt3DRender.QTexture2DMultisampleArray is not None\n    assert Qt3DRender.QComputeCommand is not None\n    assert Qt3DRender.QFrameGraphNode is not None\n    assert Qt3DRender.QSortPolicy is not None\n    assert Qt3DRender.QTextureImageData is not None\n    assert Qt3DRender.QCamera is not None\n    assert Qt3DRender.QGeometry is not None\n    assert Qt3DRender.QScreenRayCaster is not None\n    assert Qt3DRender.QClipPlane is not None\n    assert Qt3DRender.QMultiSampleAntiAliasing is not None\n    assert Qt3DRender.QRayCasterHit is not None\n    assert Qt3DRender.QAbstractTextureImage is not None\n    assert Qt3DRender.QNoDraw is not None\n    assert Qt3DRender.QPickEvent is not None\n    assert Qt3DRender.QRenderCapture is not None\n    assert Qt3DRender.QDepthTest is not None\n    assert Qt3DRender.QParameter is not None\n    assert Qt3DRender.QLevelOfDetail is not None\n    assert Qt3DRender.QGeometryFactory is not None\n    assert Qt3DRender.QTexture2D is not None\n    assert Qt3DRender.QRenderAspect is not None\n    assert Qt3DRender.QPaintedTextureImage is not None\n    assert Qt3DRender.QDithering is not None\n    assert Qt3DRender.QTextureGenerator is not None\n    assert Qt3DRender.QBlendEquationArguments is not None\n    assert Qt3DRender.QLevelOfDetailBoundingSphere is not None\n    assert Qt3DRender.QColorMask is not None\n    assert Qt3DRender.QSceneLoader is not None\n    assert Qt3DRender.QTextureLoader is not None\n    assert Qt3DRender.QShaderProgram is not None\n    assert Qt3DRender.QTextureCubeMap is not None\n    assert Qt3DRender.QTexture2DArray is not None\n    assert Qt3DRender.QTextureImage is not None\n    assert Qt3DRender.QCameraLens is not None\n    assert Qt3DRender.QRenderTargetOutput is not None\n    assert Qt3DRender.QShaderProgramBuilder is not None\n    assert Qt3DRender.QTechnique is not None\n    assert Qt3DRender.QShaderData is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtcharts.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYSIDE2\n\n\n@pytest.mark.skipif(not PYSIDE2, reason=\"Only available by default in PySide2\")\ndef test_qtcharts():\n    \"\"\"Test the qtpy.QtCharts namespace\"\"\"\n    from qtpy import QtCharts\n    assert QtCharts.QtCharts.QChart is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtcore.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2, QtCore\n\n\"\"\"Test QtCore.\"\"\"\n\n\ndef test_qtmsghandler():\n    \"\"\"Test qtpy.QtMsgHandler\"\"\"\n    assert QtCore.qInstallMessageHandler is not None\n\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2),\n                    reason=\"Targeted to PyQt5 or PySide2\")\ndef test_DateTime_toPython():\n    \"\"\"Test QDateTime.toPython\"\"\"\n    assert QtCore.QDateTime.toPython is not None\n\n\n@pytest.mark.skipif(PYSIDE2,\n                    reason=\"Doesn't seem to be present on PySide2\")\ndef test_QtCore_SignalInstance():\n    class ClassWithSignal(QtCore.QObject):\n        signal = QtCore.Signal()\n\n    instance = ClassWithSignal()\n\n    assert isinstance(instance.signal, QtCore.SignalInstance)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtdatavisualization.py",
    "content": "from __future__ import absolute_import\n\nimport sys\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\nfrom qtpy.py3compat import PY3\n\n@pytest.mark.skipif(\n    sys.platform != \"win32\" or not (PYQT5 or PYSIDE2) or PY3,\n    reason=\"Only available in Qt5 bindings and Python 2 on Windows\")\ndef test_qtdatavisualization():\n    \"\"\"Test the qtpy.QtDataVisualization namespace\"\"\"\n    # QtDataVisualization\n    assert qtpy.QtDataVisualization.QScatter3DSeries is not None\n    assert qtpy.QtDataVisualization.QSurfaceDataItem is not None\n    assert qtpy.QtDataVisualization.QSurface3DSeries is not None\n    assert qtpy.QtDataVisualization.QAbstract3DInputHandler is not None\n    assert qtpy.QtDataVisualization.QHeightMapSurfaceDataProxy is not None\n    assert qtpy.QtDataVisualization.QAbstractDataProxy is not None\n    assert qtpy.QtDataVisualization.Q3DCamera is not None\n    assert qtpy.QtDataVisualization.QAbstract3DGraph is not None\n    assert qtpy.QtDataVisualization.QCustom3DVolume is not None\n    assert qtpy.QtDataVisualization.Q3DInputHandler is not None\n    assert qtpy.QtDataVisualization.QBarDataProxy is not None\n    assert qtpy.QtDataVisualization.QSurfaceDataProxy is not None\n    assert qtpy.QtDataVisualization.QScatterDataItem is not None\n    assert qtpy.QtDataVisualization.Q3DLight is not None\n    assert qtpy.QtDataVisualization.QScatterDataProxy is not None\n    assert qtpy.QtDataVisualization.QValue3DAxis is not None\n    assert qtpy.QtDataVisualization.Q3DBars is not None\n    assert qtpy.QtDataVisualization.QBarDataItem is not None\n    assert qtpy.QtDataVisualization.QItemModelBarDataProxy is not None\n    assert qtpy.QtDataVisualization.Q3DTheme is not None\n    assert qtpy.QtDataVisualization.QCustom3DItem is not None\n    assert qtpy.QtDataVisualization.QItemModelScatterDataProxy is not None\n    assert qtpy.QtDataVisualization.QValue3DAxisFormatter is not None\n    assert qtpy.QtDataVisualization.QItemModelSurfaceDataProxy is not None\n    assert qtpy.QtDataVisualization.Q3DScatter is not None\n    assert qtpy.QtDataVisualization.QTouch3DInputHandler is not None\n    assert qtpy.QtDataVisualization.QBar3DSeries is not None\n    assert qtpy.QtDataVisualization.QAbstract3DAxis is not None\n    assert qtpy.QtDataVisualization.Q3DScene is not None\n    assert qtpy.QtDataVisualization.QCategory3DAxis is not None\n    assert qtpy.QtDataVisualization.QAbstract3DSeries is not None\n    assert qtpy.QtDataVisualization.Q3DObject is not None\n    assert qtpy.QtDataVisualization.QCustom3DLabel is not None\n    assert qtpy.QtDataVisualization.Q3DSurface is not None\n    assert qtpy.QtDataVisualization.QLogValue3DAxisFormatter is not None\n\n    # QtDatavisualization\n    assert qtpy.QtDatavisualization.QScatter3DSeries is not None\n    assert qtpy.QtDatavisualization.QSurfaceDataItem is not None\n    assert qtpy.QtDatavisualization.QSurface3DSeries is not None\n    assert qtpy.QtDatavisualization.QAbstract3DInputHandler is not None\n    assert qtpy.QtDatavisualization.QHeightMapSurfaceDataProxy is not None\n    assert qtpy.QtDatavisualization.QAbstractDataProxy is not None\n    assert qtpy.QtDatavisualization.Q3DCamera is not None\n    assert qtpy.QtDatavisualization.QAbstract3DGraph is not None\n    assert qtpy.QtDatavisualization.QCustom3DVolume is not None\n    assert qtpy.QtDatavisualization.Q3DInputHandler is not None\n    assert qtpy.QtDatavisualization.QBarDataProxy is not None\n    assert qtpy.QtDatavisualization.QSurfaceDataProxy is not None\n    assert qtpy.QtDatavisualization.QScatterDataItem is not None\n    assert qtpy.QtDatavisualization.Q3DLight is not None\n    assert qtpy.QtDatavisualization.QScatterDataProxy is not None\n    assert qtpy.QtDatavisualization.QValue3DAxis is not None\n    assert qtpy.QtDatavisualization.Q3DBars is not None\n    assert qtpy.QtDatavisualization.QBarDataItem is not None\n    assert qtpy.QtDatavisualization.QItemModelBarDataProxy is not None\n    assert qtpy.QtDatavisualization.Q3DTheme is not None\n    assert qtpy.QtDatavisualization.QCustom3DItem is not None\n    assert qtpy.QtDatavisualization.QItemModelScatterDataProxy is not None\n    assert qtpy.QtDatavisualization.QValue3DAxisFormatter is not None\n    assert qtpy.QtDatavisualization.QItemModelSurfaceDataProxy is not None\n    assert qtpy.QtDatavisualization.Q3DScatter is not None\n    assert qtpy.QtDatavisualization.QTouch3DInputHandler is not None\n    assert qtpy.QtDatavisualization.QBar3DSeries is not None\n    assert qtpy.QtDatavisualization.QAbstract3DAxis is not None\n    assert qtpy.QtDatavisualization.Q3DScene is not None\n    assert qtpy.QtDatavisualization.QCategory3DAxis is not None\n    assert qtpy.QtDatavisualization.QAbstract3DSeries is not None\n    assert qtpy.QtDatavisualization.Q3DObject is not None\n    assert qtpy.QtDatavisualization.QCustom3DLabel is not None\n    assert qtpy.QtDatavisualization.Q3DSurface is not None\n    assert qtpy.QtDatavisualization.QLogValue3DAxisFormatter is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtdesigner.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYSIDE2, PYSIDE\n\n@pytest.mark.skipif(PYSIDE2 or PYSIDE, reason=\"QtDesigner is not avalaible in PySide/PySide2\")\ndef test_qtdesigner():\n    from qtpy import QtDesigner\n    \"\"\"Test the qtpy.QtDesigner namespace\"\"\"\n    assert QtDesigner.QAbstractExtensionFactory is not None\n    assert QtDesigner.QAbstractExtensionManager is not None\n    assert QtDesigner.QDesignerActionEditorInterface is not None\n    assert QtDesigner.QDesignerContainerExtension is not None\n    assert QtDesigner.QDesignerCustomWidgetCollectionInterface is not None\n    assert QtDesigner.QDesignerCustomWidgetInterface is not None\n    assert QtDesigner.QDesignerFormEditorInterface is not None\n    assert QtDesigner.QDesignerFormWindowCursorInterface is not None\n    assert QtDesigner.QDesignerFormWindowInterface is not None\n    assert QtDesigner.QDesignerFormWindowManagerInterface is not None\n    assert QtDesigner.QDesignerMemberSheetExtension is not None\n    assert QtDesigner.QDesignerObjectInspectorInterface is not None\n    assert QtDesigner.QDesignerPropertyEditorInterface is not None\n    assert QtDesigner.QDesignerPropertySheetExtension is not None\n    assert QtDesigner.QDesignerTaskMenuExtension is not None\n    assert QtDesigner.QDesignerWidgetBoxInterface is not None\n    assert QtDesigner.QExtensionFactory is not None\n    assert QtDesigner.QExtensionManager is not None\n    assert QtDesigner.QFormBuilder is not None"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qthelp.py",
    "content": "\"\"\"Test for QtHelp namespace.\"\"\"\n\nfrom __future__ import absolute_import\n\nimport pytest\n\n\ndef test_qthelp():\n    \"\"\"Test the qtpy.QtHelp namespace.\"\"\"\n    from qtpy import QtHelp\n\n    assert QtHelp.QHelpContentItem is not None\n    assert QtHelp.QHelpContentModel is not None\n    assert QtHelp.QHelpContentWidget is not None\n    assert QtHelp.QHelpEngine is not None\n    assert QtHelp.QHelpEngineCore is not None\n    assert QtHelp.QHelpIndexModel is not None\n    assert QtHelp.QHelpIndexWidget is not None\n    assert QtHelp.QHelpSearchEngine is not None\n    assert QtHelp.QHelpSearchQuery is not None\n    assert QtHelp.QHelpSearchQueryWidget is not None\n    assert QtHelp.QHelpSearchResultWidget is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtlocation.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qtlocation():\n    \"\"\"Test the qtpy.QtLocation namespace\"\"\"\n    from qtpy import QtLocation\n    assert QtLocation.QGeoCodeReply is not None\n    assert QtLocation.QGeoCodingManager is not None\n    assert QtLocation.QGeoCodingManagerEngine is not None\n    assert QtLocation.QGeoManeuver is not None\n    assert QtLocation.QGeoRoute is not None\n    assert QtLocation.QGeoRouteReply is not None\n    assert QtLocation.QGeoRouteRequest is not None\n    assert QtLocation.QGeoRouteSegment is not None\n    assert QtLocation.QGeoRoutingManager is not None\n    assert QtLocation.QGeoRoutingManagerEngine is not None\n    assert QtLocation.QGeoServiceProvider is not None\n    #assert QtLocation.QGeoServiceProviderFactory is not None\n    assert QtLocation.QPlace is not None\n    assert QtLocation.QPlaceAttribute is not None\n    assert QtLocation.QPlaceCategory is not None\n    assert QtLocation.QPlaceContactDetail is not None\n    assert QtLocation.QPlaceContent is not None\n    assert QtLocation.QPlaceContentReply is not None\n    assert QtLocation.QPlaceContentRequest is not None\n    assert QtLocation.QPlaceDetailsReply is not None\n    assert QtLocation.QPlaceEditorial is not None\n    assert QtLocation.QPlaceIcon is not None\n    assert QtLocation.QPlaceIdReply is not None\n    assert QtLocation.QPlaceImage is not None\n    assert QtLocation.QPlaceManager is not None\n    assert QtLocation.QPlaceManagerEngine is not None\n    assert QtLocation.QPlaceMatchReply is not None\n    assert QtLocation.QPlaceMatchRequest is not None\n    assert QtLocation.QPlaceProposedSearchResult is not None\n    assert QtLocation.QPlaceRatings is not None\n    assert QtLocation.QPlaceReply is not None\n    assert QtLocation.QPlaceResult is not None\n    assert QtLocation.QPlaceReview is not None\n    assert QtLocation.QPlaceSearchReply is not None\n    assert QtLocation.QPlaceSearchRequest is not None\n    assert QtLocation.QPlaceSearchResult is not None\n    assert QtLocation.QPlaceSearchSuggestionReply is not None\n    assert QtLocation.QPlaceSupplier is not None\n    assert QtLocation.QPlaceUser is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtmultimedia.py",
    "content": "from __future__ import absolute_import\nimport os\nimport sys\n\nimport pytest\n\n\n@pytest.mark.skipif(sys.version_info[0] == 3,\n                    reason=\"Conda packages don't seem to include QtMultimedia\")\ndef test_qtmultimedia():\n    \"\"\"Test the qtpy.QtMultimedia namespace\"\"\"\n    from qtpy import QtMultimedia\n\n    assert QtMultimedia.QAbstractVideoBuffer is not None\n    assert QtMultimedia.QAudio is not None\n    assert QtMultimedia.QAudioDeviceInfo is not None\n    assert QtMultimedia.QAudioInput is not None\n    assert QtMultimedia.QSound is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtmultimediawidgets.py",
    "content": "from __future__ import absolute_import\nimport os\nimport sys\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\n@pytest.mark.skipif(sys.version_info[0] == 3,\n                    reason=\"Conda packages don't seem to include QtMultimedia\")\ndef test_qtmultimediawidgets():\n    \"\"\"Test the qtpy.QtMultimediaWidgets namespace\"\"\"\n    from qtpy import QtMultimediaWidgets\n\n    assert QtMultimediaWidgets.QCameraViewfinder is not None\n    assert QtMultimediaWidgets.QGraphicsVideoItem is not None\n    assert QtMultimediaWidgets.QVideoWidget is not None\n    #assert QtMultimediaWidgets.QVideoWidgetControl is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtnetwork.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYSIDE, PYSIDE2, QtNetwork\n\n\ndef test_qtnetwork():\n    \"\"\"Test the qtpy.QtNetwork namespace\"\"\"\n    assert QtNetwork.QAbstractNetworkCache is not None\n    assert QtNetwork.QNetworkCacheMetaData is not None\n    if not PYSIDE and not PYSIDE2:\n        assert QtNetwork.QHttpMultiPart is not None\n        assert QtNetwork.QHttpPart is not None\n    assert QtNetwork.QNetworkAccessManager is not None\n    assert QtNetwork.QNetworkCookie is not None\n    assert QtNetwork.QNetworkCookieJar is not None\n    assert QtNetwork.QNetworkDiskCache is not None\n    assert QtNetwork.QNetworkReply is not None\n    assert QtNetwork.QNetworkRequest is not None\n    assert QtNetwork.QNetworkConfigurationManager is not None\n    assert QtNetwork.QNetworkConfiguration is not None\n    assert QtNetwork.QNetworkSession is not None\n    assert QtNetwork.QAuthenticator is not None\n    assert QtNetwork.QHostAddress is not None\n    assert QtNetwork.QHostInfo is not None\n    assert QtNetwork.QNetworkAddressEntry is not None\n    assert QtNetwork.QNetworkInterface is not None\n    assert QtNetwork.QNetworkProxy is not None\n    assert QtNetwork.QNetworkProxyFactory is not None\n    assert QtNetwork.QNetworkProxyQuery is not None\n    assert QtNetwork.QAbstractSocket is not None\n    assert QtNetwork.QLocalServer is not None\n    assert QtNetwork.QLocalSocket is not None\n    assert QtNetwork.QTcpServer is not None\n    assert QtNetwork.QTcpSocket is not None\n    assert QtNetwork.QUdpSocket is not None\n    if not PYSIDE:\n        assert QtNetwork.QSslCertificate is not None\n        assert QtNetwork.QSslCipher is not None\n        assert QtNetwork.QSslConfiguration is not None\n        assert QtNetwork.QSslError is not None\n        assert QtNetwork.QSslKey is not None\n        assert QtNetwork.QSslSocket is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtpositioning.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qtpositioning():\n    \"\"\"Test the qtpy.QtPositioning namespace\"\"\"\n    from qtpy import QtPositioning\n    assert QtPositioning.QGeoAddress is not None\n    assert QtPositioning.QGeoAreaMonitorInfo is not None\n    assert QtPositioning.QGeoAreaMonitorSource is not None\n    assert QtPositioning.QGeoCircle is not None\n    assert QtPositioning.QGeoCoordinate is not None\n    assert QtPositioning.QGeoLocation is not None\n    assert QtPositioning.QGeoPath is not None\n    # CI for Python 2.7 and 3.6 uses Qt 5.9\n    # assert QtPositioning.QGeoPolygon is not None  # New in Qt 5.10\n    assert QtPositioning.QGeoPositionInfo is not None\n    assert QtPositioning.QGeoPositionInfoSource is not None\n    # QGeoPositionInfoSourceFactory is not available in PyQt\n    # assert QtPositioning.QGeoPositionInfoSourceFactory is not None  # New in Qt 5.2\n    # assert QtPositioning.QGeoPositionInfoSourceFactoryV2 is not None  # New in Qt 5.14\n    assert QtPositioning.QGeoRectangle is not None\n    assert QtPositioning.QGeoSatelliteInfo is not None\n    assert QtPositioning.QGeoSatelliteInfoSource is not None\n    assert QtPositioning.QGeoShape is not None\n    assert QtPositioning.QNmeaPositionInfoSource is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtprintsupport.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import QtPrintSupport\n\n\ndef test_qtprintsupport():\n    \"\"\"Test the qtpy.QtPrintSupport namespace\"\"\"\n    assert QtPrintSupport.QAbstractPrintDialog is not None\n    assert QtPrintSupport.QPageSetupDialog is not None\n    assert QtPrintSupport.QPrintDialog is not None\n    assert QtPrintSupport.QPrintPreviewDialog is not None\n    assert QtPrintSupport.QPrintEngine is not None\n    assert QtPrintSupport.QPrinter is not None\n    assert QtPrintSupport.QPrinterInfo is not None\n    assert QtPrintSupport.QPrintPreviewWidget is not None\n\t\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtqml.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qtqml():\n    \"\"\"Test the qtpy.QtQml namespace\"\"\"\n    from qtpy import QtQml\n    assert QtQml.QJSEngine is not None\n    assert QtQml.QJSValue is not None\n    assert QtQml.QJSValueIterator is not None\n    assert QtQml.QQmlAbstractUrlInterceptor is not None\n    assert QtQml.QQmlApplicationEngine is not None\n    assert QtQml.QQmlComponent is not None\n    assert QtQml.QQmlContext is not None\n    assert QtQml.QQmlEngine is not None\n    assert QtQml.QQmlImageProviderBase is not None\n    assert QtQml.QQmlError is not None\n    assert QtQml.QQmlExpression is not None\n    assert QtQml.QQmlExtensionPlugin is not None\n    assert QtQml.QQmlFileSelector is not None\n    assert QtQml.QQmlIncubationController is not None\n    assert QtQml.QQmlIncubator is not None\n    if not PYSIDE2:\n        # https://wiki.qt.io/Qt_for_Python_Missing_Bindings#QtQml\n        assert QtQml.QQmlListProperty is not None\n    assert QtQml.QQmlListReference is not None\n    assert QtQml.QQmlNetworkAccessManagerFactory is not None\n    assert QtQml.QQmlParserStatus is not None\n    assert QtQml.QQmlProperty is not None\n    assert QtQml.QQmlPropertyValueSource is not None\n    assert QtQml.QQmlScriptString is not None\n    assert QtQml.QQmlPropertyMap is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtquick.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qtquick():\n    \"\"\"Test the qtpy.QtQuick namespace\"\"\"\n    from qtpy import QtQuick\n    assert QtQuick.QQuickAsyncImageProvider is not None\n    if not PYSIDE2:\n        assert QtQuick.QQuickCloseEvent is not None\n    assert QtQuick.QQuickFramebufferObject is not None\n    assert QtQuick.QQuickImageProvider is not None\n    assert QtQuick.QQuickImageResponse is not None\n    assert QtQuick.QQuickItem is not None\n    assert QtQuick.QQuickItemGrabResult is not None\n    assert QtQuick.QQuickPaintedItem is not None\n    assert QtQuick.QQuickRenderControl is not None\n    assert QtQuick.QQuickTextDocument is not None\n    assert QtQuick.QQuickTextureFactory is not None\n    assert QtQuick.QQuickView is not None\n    assert QtQuick.QQuickWindow is not None\n    assert QtQuick.QSGAbstractRenderer is not None\n    assert QtQuick.QSGBasicGeometryNode is not None\n    assert QtQuick.QSGClipNode is not None\n    assert QtQuick.QSGDynamicTexture is not None\n    assert QtQuick.QSGEngine is not None\n    if not PYSIDE2:\n        assert QtQuick.QSGFlatColorMaterial is not None\n    assert QtQuick.QSGGeometry is not None\n    assert QtQuick.QSGGeometryNode is not None\n    #assert QtQuick.QSGImageNode is not None\n    if not PYSIDE2:\n        assert QtQuick.QSGMaterial is not None\n        assert QtQuick.QSGMaterialShader is not None\n    assert QtQuick.QSGMaterialType is not None\n    assert QtQuick.QSGNode is not None\n    assert QtQuick.QSGOpacityNode is not None\n    if not PYSIDE2:\n        assert QtQuick.QSGOpaqueTextureMaterial is not None\n    #assert QtQuick.QSGRectangleNode is not None\n    #assert QtQuick.QSGRenderNode is not None\n    #assert QtQuick.QSGRendererInterface is not None\n    assert QtQuick.QSGSimpleRectNode is not None\n    assert QtQuick.QSGSimpleTextureNode is not None\n    assert QtQuick.QSGTexture is not None\n    if not PYSIDE2:\n        assert QtQuick.QSGTextureMaterial is not None\n    assert QtQuick.QSGTextureProvider is not None\n    assert QtQuick.QSGTransformNode is not None\n    if not PYSIDE2:\n        assert QtQuick.QSGVertexColorMaterial is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtquickwidgets.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qtquickwidgets():\n    \"\"\"Test the qtpy.QtQuickWidgets namespace\"\"\"\n    from qtpy import QtQuickWidgets\n    assert QtQuickWidgets.QQuickWidget is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtserialport.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5\n\n@pytest.mark.skipif(not PYQT5, reason=\"Only available in Qt5 bindings, but still not in PySide2\")\ndef test_qtserialport():\n    \"\"\"Test the qtpy.QtSerialPort namespace\"\"\"\n    from qtpy import QtSerialPort\n\n    assert QtSerialPort.QSerialPort is not None\n    assert QtSerialPort.QSerialPortInfo is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtsql.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import QtSql\n\ndef test_qtsql():\n    \"\"\"Test the qtpy.QtSql namespace\"\"\"\n    assert QtSql.QSqlDatabase is not None\n    assert QtSql.QSqlDriverCreatorBase is not None\n    assert QtSql.QSqlDriver is not None\n    assert QtSql.QSqlError is not None\n    assert QtSql.QSqlField is not None\n    assert QtSql.QSqlIndex is not None\n    assert QtSql.QSqlQuery is not None\n    assert QtSql.QSqlRecord is not None\n    assert QtSql.QSqlResult is not None\n    assert QtSql.QSqlQueryModel is not None\n    assert QtSql.QSqlRelationalDelegate is not None\n    assert QtSql.QSqlRelation is not None\n    assert QtSql.QSqlRelationalTableModel is not None\n    assert QtSql.QSqlTableModel is not None\n\n    # Following modules are not (yet) part of any wrapper:\n    # QSqlDriverCreator, QSqlDriverPlugin\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtsvg.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\n\n\ndef test_qtsvg():\n    \"\"\"Test the qtpy.QtSvg namespace\"\"\"\n    from qtpy import QtSvg\n\n    assert QtSvg.QGraphicsSvgItem is not None\n    assert QtSvg.QSvgGenerator is not None\n    assert QtSvg.QSvgRenderer is not None\n    assert QtSvg.QSvgWidget is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qttest.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import QtTest\n\n\ndef test_qttest():\n    \"\"\"Test the qtpy.QtTest namespace\"\"\"\n    assert QtTest.QTest is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtwebchannel.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qtwebchannel():\n    \"\"\"Test the qtpy.QtWebChannel namespace\"\"\"\n    from qtpy import QtWebChannel\n\n    assert QtWebChannel.QWebChannel is not None\n    assert QtWebChannel.QWebChannelAbstractTransport is not None\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtwebenginewidgets.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import QtWebEngineWidgets\n\n\ndef test_qtwebenginewidgets():\n    \"\"\"Test the qtpy.QtWebSockets namespace\"\"\"\n\n    assert QtWebEngineWidgets.QWebEnginePage is not None\n    assert QtWebEngineWidgets.QWebEngineView is not None\n    assert QtWebEngineWidgets.QWebEngineSettings is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtwebsockets.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2\n\n@pytest.mark.skipif(not (PYQT5 or PYSIDE2), reason=\"Only available in Qt5 bindings\")\ndef test_qtwebsockets():\n    \"\"\"Test the qtpy.QtWebSockets namespace\"\"\"\n    from qtpy import QtWebSockets\n\n    assert QtWebSockets.QMaskGenerator is not None\n    assert QtWebSockets.QWebSocket is not None\n    assert QtWebSockets.QWebSocketCorsAuthenticator is not None\n    assert QtWebSockets.QWebSocketProtocol is not None\n    assert QtWebSockets.QWebSocketServer is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtwinextras.py",
    "content": "from __future__ import absolute_import\n\nimport os\nimport sys\n\nimport pytest\nfrom qtpy import PYSIDE2\n\n@pytest.mark.skipif(\n    sys.platform != \"win32\" or os.environ['USE_CONDA'] == 'Yes',\n    reason=\"Only available in Qt5 bindings > 5.9 (only available with pip in the current CI setup) and Windows platform\")\ndef test_qtwinextras():\n    \"\"\"Test the qtpy.QtWinExtras namespace\"\"\"\n    from qtpy import QtWinExtras\n    assert QtWinExtras.QWinJumpList is not None\n    assert QtWinExtras.QWinJumpListCategory is not None\n    assert QtWinExtras.QWinJumpListItem is not None\n    assert QtWinExtras.QWinTaskbarButton is not None\n    assert QtWinExtras.QWinTaskbarProgress is not None\n    assert QtWinExtras.QWinThumbnailToolBar is not None\n    assert QtWinExtras.QWinThumbnailToolButton is not None\n    if not PYSIDE2:  # See https://bugreports.qt.io/browse/PYSIDE-1047\n        assert QtWinExtras.QtWin is not None\n\n    if PYSIDE2:\n        assert QtWinExtras.QWinColorizationChangeEvent is not None\n        assert QtWinExtras.QWinCompositionChangeEvent is not None\n        assert QtWinExtras.QWinEvent is not None\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_qtxmlpatterns.py",
    "content": "from __future__ import absolute_import\n\nimport pytest\nfrom qtpy import PYSIDE2, PYSIDE\n\ndef test_qtxmlpatterns():\n    \"\"\"Test the qtpy.QtXmlPatterns namespace\"\"\"\n    from qtpy import QtXmlPatterns\n    assert QtXmlPatterns.QAbstractMessageHandler is not None\n    assert QtXmlPatterns.QAbstractUriResolver is not None\n    assert QtXmlPatterns.QAbstractXmlNodeModel is not None\n    assert QtXmlPatterns.QAbstractXmlReceiver is not None\n    if not PYSIDE2 and not PYSIDE:\n        assert QtXmlPatterns.QSimpleXmlNodeModel is not None\n    assert QtXmlPatterns.QSourceLocation is not None\n    assert QtXmlPatterns.QXmlFormatter is not None\n    assert QtXmlPatterns.QXmlItem is not None\n    assert QtXmlPatterns.QXmlName is not None\n    assert QtXmlPatterns.QXmlNamePool is not None\n    assert QtXmlPatterns.QXmlNodeModelIndex is not None\n    assert QtXmlPatterns.QXmlQuery is not None\n    assert QtXmlPatterns.QXmlResultItems is not None\n    assert QtXmlPatterns.QXmlSchema is not None\n    assert QtXmlPatterns.QXmlSchemaValidator is not None\n    assert QtXmlPatterns.QXmlSerializer is not None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/tests/test_uic.py",
    "content": "import os\nimport sys\nimport contextlib\n\nimport pytest\nfrom qtpy import PYQT5, PYSIDE2, PYSIDE, QtWidgets\nfrom qtpy.QtWidgets import QComboBox\n\nif PYSIDE2 or PYSIDE:\n    pytest.importorskip(\"pyside2uic\", reason=\"pyside2uic not installed\")\n\nfrom qtpy import uic\nfrom qtpy.uic import loadUi, loadUiType\n\n\nQCOMBOBOX_SUBCLASS = \"\"\"\nfrom qtpy.QtWidgets import QComboBox\nclass _QComboBoxSubclass(QComboBox):\n    pass\n\"\"\"\n\n@contextlib.contextmanager\ndef enabled_qcombobox_subclass(tmpdir):\n    \"\"\"\n    Context manager that sets up a temporary module with a QComboBox subclass\n    and then removes it once we are done.\n    \"\"\"\n\n    with open(tmpdir.join('qcombobox_subclass.py').strpath, 'w') as f:\n        f.write(QCOMBOBOX_SUBCLASS)\n\n    sys.path.insert(0, tmpdir.strpath)\n\n    yield\n\n    sys.path.pop(0)\n\n\ndef get_qapp(icon_path=None):\n    \"\"\"\n    Helper function to return a QApplication instance\n    \"\"\"\n    qapp = QtWidgets.QApplication.instance()\n    if qapp is None:\n        qapp = QtWidgets.QApplication([''])\n    return qapp\n\n\n@pytest.mark.skipif(((PYSIDE2 or PYQT5)\n                     and os.environ.get('CI', None) is not None),\n                    reason=\"It segfaults in our CIs with PYSIDE2 or PYQT5\")\ndef test_load_ui():\n    \"\"\"\n    Make sure that the patched loadUi function behaves as expected with a\n    simple .ui file.\n    \"\"\"\n    app = get_qapp()\n    ui = loadUi(os.path.join(os.path.dirname(__file__), 'test.ui'))\n    assert isinstance(ui.pushButton, QtWidgets.QPushButton)\n    assert isinstance(ui.comboBox, QComboBox)\n\n\n@pytest.mark.skipif(((PYSIDE2 or PYQT5)\n                     and os.environ.get('CI', None) is not None),\n                    reason=\"It segfaults in our CIs with PYSIDE2 or PYQT5\")\ndef test_load_ui_type():\n    \"\"\"\n    Make sure that the patched loadUiType function behaves as expected with a\n    simple .ui file.\n    \"\"\"\n    app = get_qapp()\n    ui_type, ui_base_type = loadUiType(\n        os.path.join(os.path.dirname(__file__), 'test.ui'))\n    assert ui_type.__name__ == 'Ui_Form'\n\n    class Widget(ui_base_type, ui_type):\n        def __init__(self):\n            super(Widget, self).__init__()\n            self.setupUi(self)\n\n    ui = Widget()\n    assert isinstance(ui, QtWidgets.QWidget)\n    assert isinstance(ui.pushButton, QtWidgets.QPushButton)\n    assert isinstance(ui.comboBox, QComboBox)\n\n\n@pytest.mark.skipif(((PYSIDE2 or PYQT5)\n                     and os.environ.get('CI', None) is not None),\n                    reason=\"It segfaults in our CIs with PYSIDE2 or PYQT5\")\ndef test_load_ui_custom_auto(tmpdir):\n    \"\"\"\n    Test that we can load a .ui file with custom widgets without having to\n    explicitly specify a dictionary of custom widgets, even in the case of\n    PySide.\n    \"\"\"\n\n    app = get_qapp()\n\n    with enabled_qcombobox_subclass(tmpdir):\n        from qcombobox_subclass import _QComboBoxSubclass\n        ui = loadUi(os.path.join(os.path.dirname(__file__), 'test_custom.ui'))\n\n    assert isinstance(ui.pushButton, QtWidgets.QPushButton)\n    assert isinstance(ui.comboBox, _QComboBoxSubclass)\n\n\ndef test_load_full_uic():\n    \"\"\"Test that we load the full uic objects for PyQt5 and PyQt4.\"\"\"\n    QT_API = os.environ.get('QT_API', '').lower()\n    if QT_API.startswith('pyside'):\n        assert hasattr(uic, 'loadUi')\n        assert hasattr(uic, 'loadUiType')\n    else:\n        objects = ['compileUi', 'compileUiDir', 'loadUi', 'loadUiType',\n                   'widgetPluginPath']\n        assert all([hasattr(uic, o) for o in objects])\n"
  },
  {
    "path": "openpype/vendor/python/python_2/qtpy/uic.py",
    "content": "import os\n\nfrom . import PYSIDE, PYSIDE2, PYQT4, PYQT5\nfrom .QtWidgets import QComboBox\n\n\nif PYQT5:\n\n    from PyQt5.uic import *\n\nelif PYQT4:\n\n    from PyQt4.uic import *\n\nelse:\n\n    __all__ = ['loadUi', 'loadUiType']\n\n    # In PySide, loadUi does not exist, so we define it using QUiLoader, and\n    # then make sure we expose that function. This is adapted from qt-helpers\n    # which was released under a 3-clause BSD license:\n    # qt-helpers - a common front-end to various Qt modules\n    #\n    # Copyright (c) 2015, Chris Beaumont and Thomas Robitaille\n    #\n    # All rights reserved.\n    #\n    # Redistribution and use in source and binary forms, with or without\n    # modification, are permitted provided that the following conditions are\n    # met:\n    #\n    #  * Redistributions of source code must retain the above copyright\n    #    notice, this list of conditions and the following disclaimer.\n    #  * Redistributions in binary form must reproduce the above copyright\n    #    notice, this list of conditions and the following disclaimer in the\n    #    documentation and/or other materials provided with the\n    #    distribution.\n    #  * Neither the name of the Glue project nor the names of its contributors\n    #    may be used to endorse or promote products derived from this software\n    #    without specific prior written permission.\n    #\n    # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\n    # IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\n    # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n    # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR\n    # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n    # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n    # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n    # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n    # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n    # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n    # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n    #\n    # Which itself was based on the solution at\n    #\n    # https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8\n    #\n    # which was released under the MIT license:\n    #\n    # Copyright (c) 2011 Sebastian Wiesner <lunaryorn@gmail.com>\n    # Modifications by Charl Botha <cpbotha@vxlabs.com>\n    #\n    # Permission is hereby granted, free of charge, to any person obtaining a\n    # copy of this software and associated documentation files (the \"Software\"),\n    # to deal in the Software without restriction, including without limitation\n    # the rights to use, copy, modify, merge, publish, distribute, sublicense,\n    # and/or sell copies of the Software, and to permit persons to whom the\n    # Software is furnished to do so, subject to the following conditions:\n    #\n    # The above copyright notice and this permission notice shall be included in\n    # all copies or substantial portions of the Software.\n    #\n    # THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL\n    # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n    # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n    # DEALINGS IN THE SOFTWARE.\n\n    if PYSIDE:\n        from PySide.QtCore import QMetaObject\n        from PySide.QtUiTools import QUiLoader\n        try:\n            from pysideuic import compileUi\n        except ImportError:\n            pass\n    elif PYSIDE2:\n        from PySide2.QtCore import QMetaObject\n        from PySide2.QtUiTools import QUiLoader\n        try:\n            from pyside2uic import compileUi\n        except ImportError:\n            pass\n\n    class UiLoader(QUiLoader):\n        \"\"\"\n        Subclass of :class:`~PySide.QtUiTools.QUiLoader` to create the user\n        interface in a base instance.\n\n        Unlike :class:`~PySide.QtUiTools.QUiLoader` itself this class does not\n        create a new instance of the top-level widget, but creates the user\n        interface in an existing instance of the top-level class if needed.\n\n        This mimics the behaviour of :func:`PyQt4.uic.loadUi`.\n        \"\"\"\n\n        def __init__(self, baseinstance, customWidgets=None):\n            \"\"\"\n            Create a loader for the given ``baseinstance``.\n\n            The user interface is created in ``baseinstance``, which must be an\n            instance of the top-level class in the user interface to load, or a\n            subclass thereof.\n\n            ``customWidgets`` is a dictionary mapping from class name to class\n            object for custom widgets. Usually, this should be done by calling\n            registerCustomWidget on the QUiLoader, but with PySide 1.1.2 on\n            Ubuntu 12.04 x86_64 this causes a segfault.\n\n            ``parent`` is the parent object of this loader.\n            \"\"\"\n\n            QUiLoader.__init__(self, baseinstance)\n\n            self.baseinstance = baseinstance\n\n            if customWidgets is None:\n                self.customWidgets = {}\n            else:\n                self.customWidgets = customWidgets\n\n        def createWidget(self, class_name, parent=None, name=''):\n            \"\"\"\n            Function that is called for each widget defined in ui file,\n            overridden here to populate baseinstance instead.\n            \"\"\"\n\n            if parent is None and self.baseinstance:\n                # supposed to create the top-level widget, return the base\n                # instance instead\n                return self.baseinstance\n\n            else:\n\n                # For some reason, Line is not in the list of available\n                # widgets, but works fine, so we have to special case it here.\n                if class_name in self.availableWidgets() or class_name == 'Line':\n                    # create a new widget for child widgets\n                    widget = QUiLoader.createWidget(self, class_name, parent, name)\n\n                else:\n                    # If not in the list of availableWidgets, must be a custom\n                    # widget. This will raise KeyError if the user has not\n                    # supplied the relevant class_name in the dictionary or if\n                    # customWidgets is empty.\n                    try:\n                        widget = self.customWidgets[class_name](parent)\n                    except KeyError:\n                        raise Exception('No custom widget ' + class_name + ' '\n                                        'found in customWidgets')\n\n                if self.baseinstance:\n                    # set an attribute for the new child widget on the base\n                    # instance, just like PyQt4.uic.loadUi does.\n                    setattr(self.baseinstance, name, widget)\n\n                return widget\n\n    def _get_custom_widgets(ui_file):\n        \"\"\"\n        This function is used to parse a ui file and look for the <customwidgets>\n        section, then automatically load all the custom widget classes.\n        \"\"\"\n\n        import sys\n        import importlib\n        from xml.etree.ElementTree import ElementTree\n\n        # Parse the UI file\n        etree = ElementTree()\n        ui = etree.parse(ui_file)\n\n        # Get the customwidgets section\n        custom_widgets = ui.find('customwidgets')\n\n        if custom_widgets is None:\n            return {}\n\n        custom_widget_classes = {}\n\n        for custom_widget in list(custom_widgets):\n\n            cw_class = custom_widget.find('class').text\n            cw_header = custom_widget.find('header').text\n\n            module = importlib.import_module(cw_header)\n\n            custom_widget_classes[cw_class] = getattr(module, cw_class)\n\n        return custom_widget_classes\n\n    def loadUi(uifile, baseinstance=None, workingDirectory=None):\n        \"\"\"\n        Dynamically load a user interface from the given ``uifile``.\n\n        ``uifile`` is a string containing a file name of the UI file to load.\n\n        If ``baseinstance`` is ``None``, the a new instance of the top-level\n        widget will be created. Otherwise, the user interface is created within\n        the given ``baseinstance``. In this case ``baseinstance`` must be an\n        instance of the top-level widget class in the UI file to load, or a\n        subclass thereof. In other words, if you've created a ``QMainWindow``\n        interface in the designer, ``baseinstance`` must be a ``QMainWindow``\n        or a subclass thereof, too. You cannot load a ``QMainWindow`` UI file\n        with a plain :class:`~PySide.QtGui.QWidget` as ``baseinstance``.\n\n        :method:`~PySide.QtCore.QMetaObject.connectSlotsByName()` is called on\n        the created user interface, so you can implemented your slots according\n        to its conventions in your widget class.\n\n        Return ``baseinstance``, if ``baseinstance`` is not ``None``. Otherwise\n        return the newly created instance of the user interface.\n        \"\"\"\n\n        # We parse the UI file and import any required custom widgets\n        customWidgets = _get_custom_widgets(uifile)\n\n        loader = UiLoader(baseinstance, customWidgets)\n\n        if workingDirectory is not None:\n            loader.setWorkingDirectory(workingDirectory)\n\n        widget = loader.load(uifile)\n        QMetaObject.connectSlotsByName(widget)\n        return widget\n\n    def loadUiType(uifile, from_imports=False):\n        \"\"\"Load a .ui file and return the generated form class and\n        the Qt base class.\n\n        The \"loadUiType\" command convert the ui file to py code\n        in-memory first and then execute it in a special frame to\n        retrieve the form_class.\n\n        Credit: https://stackoverflow.com/a/14195313/15954282\n        \"\"\"\n\n        import sys\n        if sys.version_info >= (3, 0):\n            from io import StringIO\n        else:\n            from io import BytesIO as StringIO\n        from xml.etree.ElementTree import ElementTree\n        from . import QtWidgets\n\n        # Parse the UI file\n        etree = ElementTree()\n        ui = etree.parse(uifile)\n\n        widget_class = ui.find('widget').get('class')\n        form_class = ui.find('class').text\n\n        with open(uifile, 'r') as fd:\n            code_stream = StringIO()\n            frame = {}\n\n            compileUi(fd, code_stream, indent=0, from_imports=from_imports)\n            pyc = compile(code_stream.getvalue(), '<string>', 'exec')\n            exec(pyc, frame)\n\n            # Fetch the base_class and form class based on their type in the\n            # xml from designer\n            form_class = frame['Ui_%s' % form_class]\n            base_class = getattr(QtWidgets, widget_class)\n\n        return form_class, base_class\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\n#   __\n#  /__)  _  _     _   _ _/   _\n# / (   (- (/ (/ (- _)  /  _)\n#          /\n\n\"\"\"\nRequests HTTP Library\n~~~~~~~~~~~~~~~~~~~~~\n\nRequests is an HTTP library, written in Python, for human beings.\nBasic GET usage:\n\n   >>> import requests\n   >>> r = requests.get('https://www.python.org')\n   >>> r.status_code\n   200\n   >>> b'Python is a programming language' in r.content\n   True\n\n... or POST:\n\n   >>> payload = dict(key1='value1', key2='value2')\n   >>> r = requests.post('https://httpbin.org/post', data=payload)\n   >>> print(r.text)\n   {\n     ...\n     \"form\": {\n       \"key1\": \"value1\",\n       \"key2\": \"value2\"\n     },\n     ...\n   }\n\nThe other HTTP methods are supported - see `requests.api`. Full documentation\nis at <https://requests.readthedocs.io>.\n\n:copyright: (c) 2017 by Kenneth Reitz.\n:license: Apache 2.0, see LICENSE for more details.\n\"\"\"\n\nimport urllib3\nimport warnings\nfrom .exceptions import RequestsDependencyWarning\n\ntry:\n    from charset_normalizer import __version__ as charset_normalizer_version\nexcept ImportError:\n    charset_normalizer_version = None\n\ntry:\n    from chardet import __version__ as chardet_version\nexcept ImportError:\n    chardet_version = None\n\ndef check_compatibility(urllib3_version, chardet_version, charset_normalizer_version):\n    urllib3_version = urllib3_version.split('.')\n    assert urllib3_version != ['dev']  # Verify urllib3 isn't installed from git.\n\n    # Sometimes, urllib3 only reports its version as 16.1.\n    if len(urllib3_version) == 2:\n        urllib3_version.append('0')\n\n    # Check urllib3 for compatibility.\n    major, minor, patch = urllib3_version  # noqa: F811\n    major, minor, patch = int(major), int(minor), int(patch)\n    # urllib3 >= 1.21.1, <= 1.26\n    assert major == 1\n    assert minor >= 21\n    assert minor <= 26\n\n    # Check charset_normalizer for compatibility.\n    if chardet_version:\n        major, minor, patch = chardet_version.split('.')[:3]\n        major, minor, patch = int(major), int(minor), int(patch)\n        # chardet_version >= 3.0.2, < 5.0.0\n        assert (3, 0, 2) <= (major, minor, patch) < (5, 0, 0)\n    elif charset_normalizer_version:\n        major, minor, patch = charset_normalizer_version.split('.')[:3]\n        major, minor, patch = int(major), int(minor), int(patch)\n        # charset_normalizer >= 2.0.0 < 3.0.0\n        assert (2, 0, 0) <= (major, minor, patch) < (3, 0, 0)\n    else:\n        raise Exception(\"You need either charset_normalizer or chardet installed\")\n\ndef _check_cryptography(cryptography_version):\n    # cryptography < 1.3.4\n    try:\n        cryptography_version = list(map(int, cryptography_version.split('.')))\n    except ValueError:\n        return\n\n    if cryptography_version < [1, 3, 4]:\n        warning = 'Old version of cryptography ({}) may cause slowdown.'.format(cryptography_version)\n        warnings.warn(warning, RequestsDependencyWarning)\n\n# Check imported dependencies for compatibility.\ntry:\n    check_compatibility(urllib3.__version__, chardet_version, charset_normalizer_version)\nexcept (AssertionError, ValueError):\n    warnings.warn(\"urllib3 ({}) or chardet ({})/charset_normalizer ({}) doesn't match a supported \"\n                  \"version!\".format(urllib3.__version__, chardet_version, charset_normalizer_version),\n                  RequestsDependencyWarning)\n\n# Attempt to enable urllib3's fallback for SNI support\n# if the standard library doesn't support SNI or the\n# 'ssl' library isn't available.\ntry:\n    try:\n        import ssl\n    except ImportError:\n        ssl = None\n\n    if not getattr(ssl, \"HAS_SNI\", False):\n        from urllib3.contrib import pyopenssl\n        pyopenssl.inject_into_urllib3()\n\n        # Check cryptography version\n        from cryptography import __version__ as cryptography_version\n        _check_cryptography(cryptography_version)\nexcept ImportError:\n    pass\n\n# urllib3's DependencyWarnings should be silenced.\nfrom urllib3.exceptions import DependencyWarning\nwarnings.simplefilter('ignore', DependencyWarning)\n\nfrom .__version__ import __title__, __description__, __url__, __version__\nfrom .__version__ import __build__, __author__, __author_email__, __license__\nfrom .__version__ import __copyright__, __cake__\n\nfrom . import utils\nfrom . import packages\nfrom .models import Request, Response, PreparedRequest\nfrom .api import request, get, head, post, patch, put, delete, options\nfrom .sessions import session, Session\nfrom .status_codes import codes\nfrom .exceptions import (\n    RequestException, Timeout, URLRequired,\n    TooManyRedirects, HTTPError, ConnectionError,\n    FileModeWarning, ConnectTimeout, ReadTimeout, JSONDecodeError\n)\n\n# Set default logging handler to avoid \"No handler found\" warnings.\nimport logging\nfrom logging import NullHandler\n\nlogging.getLogger(__name__).addHandler(NullHandler())\n\n# FileModeWarnings go off per the default.\nwarnings.simplefilter('default', FileModeWarning, append=True)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/__version__.py",
    "content": "# .-. .-. .-. . . .-. .-. .-. .-.\n# |(  |-  |.| | | |-  `-.  |  `-.\n# ' ' `-' `-`.`-' `-' `-'  '  `-'\n\n__title__ = 'requests'\n__description__ = 'Python HTTP for Humans.'\n__url__ = 'https://requests.readthedocs.io'\n__version__ = '2.27.1'\n__build__ = 0x022701\n__author__ = 'Kenneth Reitz'\n__author_email__ = 'me@kennethreitz.org'\n__license__ = 'Apache 2.0'\n__copyright__ = 'Copyright 2022 Kenneth Reitz'\n__cake__ = u'\\u2728 \\U0001f370 \\u2728'\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/_internal_utils.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests._internal_utils\n~~~~~~~~~~~~~~\n\nProvides utility functions that are consumed internally by Requests\nwhich depend on extremely few external helpers (such as compat)\n\"\"\"\n\nfrom .compat import is_py2, builtin_str, str\n\n\ndef to_native_string(string, encoding='ascii'):\n    \"\"\"Given a string object, regardless of type, returns a representation of\n    that string in the native string type, encoding and decoding where\n    necessary. This assumes ASCII unless told otherwise.\n    \"\"\"\n    if isinstance(string, builtin_str):\n        out = string\n    else:\n        if is_py2:\n            out = string.encode(encoding)\n        else:\n            out = string.decode(encoding)\n\n    return out\n\n\ndef unicode_is_ascii(u_string):\n    \"\"\"Determine if unicode string only contains ASCII characters.\n\n    :param str u_string: unicode string to check. Must be unicode\n        and not Python 2 `str`.\n    :rtype: bool\n    \"\"\"\n    assert isinstance(u_string, str)\n    try:\n        u_string.encode('ascii')\n        return True\n    except UnicodeEncodeError:\n        return False\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/adapters.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests.adapters\n~~~~~~~~~~~~~~~~~\n\nThis module contains the transport adapters that Requests uses to define\nand maintain connections.\n\"\"\"\n\nimport os.path\nimport socket\n\nfrom urllib3.poolmanager import PoolManager, proxy_from_url\nfrom urllib3.response import HTTPResponse\nfrom urllib3.util import parse_url\nfrom urllib3.util import Timeout as TimeoutSauce\nfrom urllib3.util.retry import Retry\nfrom urllib3.exceptions import ClosedPoolError\nfrom urllib3.exceptions import ConnectTimeoutError\nfrom urllib3.exceptions import HTTPError as _HTTPError\nfrom urllib3.exceptions import InvalidHeader as _InvalidHeader\nfrom urllib3.exceptions import MaxRetryError\nfrom urllib3.exceptions import NewConnectionError\nfrom urllib3.exceptions import ProxyError as _ProxyError\nfrom urllib3.exceptions import ProtocolError\nfrom urllib3.exceptions import ReadTimeoutError\nfrom urllib3.exceptions import SSLError as _SSLError\nfrom urllib3.exceptions import ResponseError\nfrom urllib3.exceptions import LocationValueError\n\nfrom .models import Response\nfrom .compat import urlparse, basestring\nfrom .utils import (DEFAULT_CA_BUNDLE_PATH, extract_zipped_paths,\n                    get_encoding_from_headers, prepend_scheme_if_needed,\n                    get_auth_from_url, urldefragauth, select_proxy)\nfrom .structures import CaseInsensitiveDict\nfrom .cookies import extract_cookies_to_jar\nfrom .exceptions import (ConnectionError, ConnectTimeout, ReadTimeout, SSLError,\n                         ProxyError, RetryError, InvalidSchema, InvalidProxyURL,\n                         InvalidURL, InvalidHeader)\nfrom .auth import _basic_auth_str\n\ntry:\n    from urllib3.contrib.socks import SOCKSProxyManager\nexcept ImportError:\n    def SOCKSProxyManager(*args, **kwargs):\n        raise InvalidSchema(\"Missing dependencies for SOCKS support.\")\n\nDEFAULT_POOLBLOCK = False\nDEFAULT_POOLSIZE = 10\nDEFAULT_RETRIES = 0\nDEFAULT_POOL_TIMEOUT = None\n\n\nclass BaseAdapter(object):\n    \"\"\"The Base Transport Adapter\"\"\"\n\n    def __init__(self):\n        super(BaseAdapter, self).__init__()\n\n    def send(self, request, stream=False, timeout=None, verify=True,\n             cert=None, proxies=None):\n        \"\"\"Sends PreparedRequest object. Returns Response object.\n\n        :param request: The :class:`PreparedRequest <PreparedRequest>` being sent.\n        :param stream: (optional) Whether to stream the request content.\n        :param timeout: (optional) How long to wait for the server to send\n            data before giving up, as a float, or a :ref:`(connect timeout,\n            read timeout) <timeouts>` tuple.\n        :type timeout: float or tuple\n        :param verify: (optional) Either a boolean, in which case it controls whether we verify\n            the server's TLS certificate, or a string, in which case it must be a path\n            to a CA bundle to use\n        :param cert: (optional) Any user-provided SSL certificate to be trusted.\n        :param proxies: (optional) The proxies dictionary to apply to the request.\n        \"\"\"\n        raise NotImplementedError\n\n    def close(self):\n        \"\"\"Cleans up adapter specific items.\"\"\"\n        raise NotImplementedError\n\n\nclass HTTPAdapter(BaseAdapter):\n    \"\"\"The built-in HTTP Adapter for urllib3.\n\n    Provides a general-case interface for Requests sessions to contact HTTP and\n    HTTPS urls by implementing the Transport Adapter interface. This class will\n    usually be created by the :class:`Session <Session>` class under the\n    covers.\n\n    :param pool_connections: The number of urllib3 connection pools to cache.\n    :param pool_maxsize: The maximum number of connections to save in the pool.\n    :param max_retries: The maximum number of retries each connection\n        should attempt. Note, this applies only to failed DNS lookups, socket\n        connections and connection timeouts, never to requests where data has\n        made it to the server. By default, Requests does not retry failed\n        connections. If you need granular control over the conditions under\n        which we retry a request, import urllib3's ``Retry`` class and pass\n        that instead.\n    :param pool_block: Whether the connection pool should block for connections.\n\n    Usage::\n\n      >>> import requests\n      >>> s = requests.Session()\n      >>> a = requests.adapters.HTTPAdapter(max_retries=3)\n      >>> s.mount('http://', a)\n    \"\"\"\n    __attrs__ = ['max_retries', 'config', '_pool_connections', '_pool_maxsize',\n                 '_pool_block']\n\n    def __init__(self, pool_connections=DEFAULT_POOLSIZE,\n                 pool_maxsize=DEFAULT_POOLSIZE, max_retries=DEFAULT_RETRIES,\n                 pool_block=DEFAULT_POOLBLOCK):\n        if max_retries == DEFAULT_RETRIES:\n            self.max_retries = Retry(0, read=False)\n        else:\n            self.max_retries = Retry.from_int(max_retries)\n        self.config = {}\n        self.proxy_manager = {}\n\n        super(HTTPAdapter, self).__init__()\n\n        self._pool_connections = pool_connections\n        self._pool_maxsize = pool_maxsize\n        self._pool_block = pool_block\n\n        self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block)\n\n    def __getstate__(self):\n        return {attr: getattr(self, attr, None) for attr in self.__attrs__}\n\n    def __setstate__(self, state):\n        # Can't handle by adding 'proxy_manager' to self.__attrs__ because\n        # self.poolmanager uses a lambda function, which isn't pickleable.\n        self.proxy_manager = {}\n        self.config = {}\n\n        for attr, value in state.items():\n            setattr(self, attr, value)\n\n        self.init_poolmanager(self._pool_connections, self._pool_maxsize,\n                              block=self._pool_block)\n\n    def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs):\n        \"\"\"Initializes a urllib3 PoolManager.\n\n        This method should not be called from user code, and is only\n        exposed for use when subclassing the\n        :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.\n\n        :param connections: The number of urllib3 connection pools to cache.\n        :param maxsize: The maximum number of connections to save in the pool.\n        :param block: Block when no free connections are available.\n        :param pool_kwargs: Extra keyword arguments used to initialize the Pool Manager.\n        \"\"\"\n        # save these values for pickling\n        self._pool_connections = connections\n        self._pool_maxsize = maxsize\n        self._pool_block = block\n\n        self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize,\n                                       block=block, strict=True, **pool_kwargs)\n\n    def proxy_manager_for(self, proxy, **proxy_kwargs):\n        \"\"\"Return urllib3 ProxyManager for the given proxy.\n\n        This method should not be called from user code, and is only\n        exposed for use when subclassing the\n        :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.\n\n        :param proxy: The proxy to return a urllib3 ProxyManager for.\n        :param proxy_kwargs: Extra keyword arguments used to configure the Proxy Manager.\n        :returns: ProxyManager\n        :rtype: urllib3.ProxyManager\n        \"\"\"\n        if proxy in self.proxy_manager:\n            manager = self.proxy_manager[proxy]\n        elif proxy.lower().startswith('socks'):\n            username, password = get_auth_from_url(proxy)\n            manager = self.proxy_manager[proxy] = SOCKSProxyManager(\n                proxy,\n                username=username,\n                password=password,\n                num_pools=self._pool_connections,\n                maxsize=self._pool_maxsize,\n                block=self._pool_block,\n                **proxy_kwargs\n            )\n        else:\n            proxy_headers = self.proxy_headers(proxy)\n            manager = self.proxy_manager[proxy] = proxy_from_url(\n                proxy,\n                proxy_headers=proxy_headers,\n                num_pools=self._pool_connections,\n                maxsize=self._pool_maxsize,\n                block=self._pool_block,\n                **proxy_kwargs)\n\n        return manager\n\n    def cert_verify(self, conn, url, verify, cert):\n        \"\"\"Verify a SSL certificate. This method should not be called from user\n        code, and is only exposed for use when subclassing the\n        :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.\n\n        :param conn: The urllib3 connection object associated with the cert.\n        :param url: The requested URL.\n        :param verify: Either a boolean, in which case it controls whether we verify\n            the server's TLS certificate, or a string, in which case it must be a path\n            to a CA bundle to use\n        :param cert: The SSL certificate to verify.\n        \"\"\"\n        if url.lower().startswith('https') and verify:\n\n            cert_loc = None\n\n            # Allow self-specified cert location.\n            if verify is not True:\n                cert_loc = verify\n\n            if not cert_loc:\n                cert_loc = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)\n\n            if not cert_loc or not os.path.exists(cert_loc):\n                raise IOError(\"Could not find a suitable TLS CA certificate bundle, \"\n                              \"invalid path: {}\".format(cert_loc))\n\n            conn.cert_reqs = 'CERT_REQUIRED'\n\n            if not os.path.isdir(cert_loc):\n                conn.ca_certs = cert_loc\n            else:\n                conn.ca_cert_dir = cert_loc\n        else:\n            conn.cert_reqs = 'CERT_NONE'\n            conn.ca_certs = None\n            conn.ca_cert_dir = None\n\n        if cert:\n            if not isinstance(cert, basestring):\n                conn.cert_file = cert[0]\n                conn.key_file = cert[1]\n            else:\n                conn.cert_file = cert\n                conn.key_file = None\n            if conn.cert_file and not os.path.exists(conn.cert_file):\n                raise IOError(\"Could not find the TLS certificate file, \"\n                              \"invalid path: {}\".format(conn.cert_file))\n            if conn.key_file and not os.path.exists(conn.key_file):\n                raise IOError(\"Could not find the TLS key file, \"\n                              \"invalid path: {}\".format(conn.key_file))\n\n    def build_response(self, req, resp):\n        \"\"\"Builds a :class:`Response <requests.Response>` object from a urllib3\n        response. This should not be called from user code, and is only exposed\n        for use when subclassing the\n        :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`\n\n        :param req: The :class:`PreparedRequest <PreparedRequest>` used to generate the response.\n        :param resp: The urllib3 response object.\n        :rtype: requests.Response\n        \"\"\"\n        response = Response()\n\n        # Fallback to None if there's no status_code, for whatever reason.\n        response.status_code = getattr(resp, 'status', None)\n\n        # Make headers case-insensitive.\n        response.headers = CaseInsensitiveDict(getattr(resp, 'headers', {}))\n\n        # Set encoding.\n        response.encoding = get_encoding_from_headers(response.headers)\n        response.raw = resp\n        response.reason = response.raw.reason\n\n        if isinstance(req.url, bytes):\n            response.url = req.url.decode('utf-8')\n        else:\n            response.url = req.url\n\n        # Add new cookies from the server.\n        extract_cookies_to_jar(response.cookies, req, resp)\n\n        # Give the Response some context.\n        response.request = req\n        response.connection = self\n\n        return response\n\n    def get_connection(self, url, proxies=None):\n        \"\"\"Returns a urllib3 connection for the given URL. This should not be\n        called from user code, and is only exposed for use when subclassing the\n        :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.\n\n        :param url: The URL to connect to.\n        :param proxies: (optional) A Requests-style dictionary of proxies used on this request.\n        :rtype: urllib3.ConnectionPool\n        \"\"\"\n        proxy = select_proxy(url, proxies)\n\n        if proxy:\n            proxy = prepend_scheme_if_needed(proxy, 'http')\n            proxy_url = parse_url(proxy)\n            if not proxy_url.host:\n                raise InvalidProxyURL(\"Please check proxy URL. It is malformed\"\n                                      \" and could be missing the host.\")\n            proxy_manager = self.proxy_manager_for(proxy)\n            conn = proxy_manager.connection_from_url(url)\n        else:\n            # Only scheme should be lower case\n            parsed = urlparse(url)\n            url = parsed.geturl()\n            conn = self.poolmanager.connection_from_url(url)\n\n        return conn\n\n    def close(self):\n        \"\"\"Disposes of any internal state.\n\n        Currently, this closes the PoolManager and any active ProxyManager,\n        which closes any pooled connections.\n        \"\"\"\n        self.poolmanager.clear()\n        for proxy in self.proxy_manager.values():\n            proxy.clear()\n\n    def request_url(self, request, proxies):\n        \"\"\"Obtain the url to use when making the final request.\n\n        If the message is being sent through a HTTP proxy, the full URL has to\n        be used. Otherwise, we should only use the path portion of the URL.\n\n        This should not be called from user code, and is only exposed for use\n        when subclassing the\n        :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.\n\n        :param request: The :class:`PreparedRequest <PreparedRequest>` being sent.\n        :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs.\n        :rtype: str\n        \"\"\"\n        proxy = select_proxy(request.url, proxies)\n        scheme = urlparse(request.url).scheme\n\n        is_proxied_http_request = (proxy and scheme != 'https')\n        using_socks_proxy = False\n        if proxy:\n            proxy_scheme = urlparse(proxy).scheme.lower()\n            using_socks_proxy = proxy_scheme.startswith('socks')\n\n        url = request.path_url\n        if is_proxied_http_request and not using_socks_proxy:\n            url = urldefragauth(request.url)\n\n        return url\n\n    def add_headers(self, request, **kwargs):\n        \"\"\"Add any headers needed by the connection. As of v2.0 this does\n        nothing by default, but is left for overriding by users that subclass\n        the :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.\n\n        This should not be called from user code, and is only exposed for use\n        when subclassing the\n        :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.\n\n        :param request: The :class:`PreparedRequest <PreparedRequest>` to add headers to.\n        :param kwargs: The keyword arguments from the call to send().\n        \"\"\"\n        pass\n\n    def proxy_headers(self, proxy):\n        \"\"\"Returns a dictionary of the headers to add to any request sent\n        through a proxy. This works with urllib3 magic to ensure that they are\n        correctly sent to the proxy, rather than in a tunnelled request if\n        CONNECT is being used.\n\n        This should not be called from user code, and is only exposed for use\n        when subclassing the\n        :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.\n\n        :param proxy: The url of the proxy being used for this request.\n        :rtype: dict\n        \"\"\"\n        headers = {}\n        username, password = get_auth_from_url(proxy)\n\n        if username:\n            headers['Proxy-Authorization'] = _basic_auth_str(username,\n                                                             password)\n\n        return headers\n\n    def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):\n        \"\"\"Sends PreparedRequest object. Returns Response object.\n\n        :param request: The :class:`PreparedRequest <PreparedRequest>` being sent.\n        :param stream: (optional) Whether to stream the request content.\n        :param timeout: (optional) How long to wait for the server to send\n            data before giving up, as a float, or a :ref:`(connect timeout,\n            read timeout) <timeouts>` tuple.\n        :type timeout: float or tuple or urllib3 Timeout object\n        :param verify: (optional) Either a boolean, in which case it controls whether\n            we verify the server's TLS certificate, or a string, in which case it\n            must be a path to a CA bundle to use\n        :param cert: (optional) Any user-provided SSL certificate to be trusted.\n        :param proxies: (optional) The proxies dictionary to apply to the request.\n        :rtype: requests.Response\n        \"\"\"\n\n        try:\n            conn = self.get_connection(request.url, proxies)\n        except LocationValueError as e:\n            raise InvalidURL(e, request=request)\n\n        self.cert_verify(conn, request.url, verify, cert)\n        url = self.request_url(request, proxies)\n        self.add_headers(request, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies)\n\n        chunked = not (request.body is None or 'Content-Length' in request.headers)\n\n        if isinstance(timeout, tuple):\n            try:\n                connect, read = timeout\n                timeout = TimeoutSauce(connect=connect, read=read)\n            except ValueError as e:\n                # this may raise a string formatting error.\n                err = (\"Invalid timeout {}. Pass a (connect, read) \"\n                       \"timeout tuple, or a single float to set \"\n                       \"both timeouts to the same value\".format(timeout))\n                raise ValueError(err)\n        elif isinstance(timeout, TimeoutSauce):\n            pass\n        else:\n            timeout = TimeoutSauce(connect=timeout, read=timeout)\n\n        try:\n            if not chunked:\n                resp = conn.urlopen(\n                    method=request.method,\n                    url=url,\n                    body=request.body,\n                    headers=request.headers,\n                    redirect=False,\n                    assert_same_host=False,\n                    preload_content=False,\n                    decode_content=False,\n                    retries=self.max_retries,\n                    timeout=timeout\n                )\n\n            # Send the request.\n            else:\n                if hasattr(conn, 'proxy_pool'):\n                    conn = conn.proxy_pool\n\n                low_conn = conn._get_conn(timeout=DEFAULT_POOL_TIMEOUT)\n\n                try:\n                    skip_host = 'Host' in request.headers\n                    low_conn.putrequest(request.method,\n                                        url,\n                                        skip_accept_encoding=True,\n                                        skip_host=skip_host)\n\n                    for header, value in request.headers.items():\n                        low_conn.putheader(header, value)\n\n                    low_conn.endheaders()\n\n                    for i in request.body:\n                        low_conn.send(hex(len(i))[2:].encode('utf-8'))\n                        low_conn.send(b'\\r\\n')\n                        low_conn.send(i)\n                        low_conn.send(b'\\r\\n')\n                    low_conn.send(b'0\\r\\n\\r\\n')\n\n                    # Receive the response from the server\n                    try:\n                        # For Python 2.7, use buffering of HTTP responses\n                        r = low_conn.getresponse(buffering=True)\n                    except TypeError:\n                        # For compatibility with Python 3.3+\n                        r = low_conn.getresponse()\n\n                    resp = HTTPResponse.from_httplib(\n                        r,\n                        pool=conn,\n                        connection=low_conn,\n                        preload_content=False,\n                        decode_content=False\n                    )\n                except:\n                    # If we hit any problems here, clean up the connection.\n                    # Then, reraise so that we can handle the actual exception.\n                    low_conn.close()\n                    raise\n\n        except (ProtocolError, socket.error) as err:\n            raise ConnectionError(err, request=request)\n\n        except MaxRetryError as e:\n            if isinstance(e.reason, ConnectTimeoutError):\n                # TODO: Remove this in 3.0.0: see #2811\n                if not isinstance(e.reason, NewConnectionError):\n                    raise ConnectTimeout(e, request=request)\n\n            if isinstance(e.reason, ResponseError):\n                raise RetryError(e, request=request)\n\n            if isinstance(e.reason, _ProxyError):\n                raise ProxyError(e, request=request)\n\n            if isinstance(e.reason, _SSLError):\n                # This branch is for urllib3 v1.22 and later.\n                raise SSLError(e, request=request)\n\n            raise ConnectionError(e, request=request)\n\n        except ClosedPoolError as e:\n            raise ConnectionError(e, request=request)\n\n        except _ProxyError as e:\n            raise ProxyError(e)\n\n        except (_SSLError, _HTTPError) as e:\n            if isinstance(e, _SSLError):\n                # This branch is for urllib3 versions earlier than v1.22\n                raise SSLError(e, request=request)\n            elif isinstance(e, ReadTimeoutError):\n                raise ReadTimeout(e, request=request)\n            elif isinstance(e, _InvalidHeader):\n                raise InvalidHeader(e, request=request)\n            else:\n                raise\n\n        return self.build_response(request, resp)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/api.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests.api\n~~~~~~~~~~~~\n\nThis module implements the Requests API.\n\n:copyright: (c) 2012 by Kenneth Reitz.\n:license: Apache2, see LICENSE for more details.\n\"\"\"\n\nfrom . import sessions\n\n\ndef request(method, url, **kwargs):\n    \"\"\"Constructs and sends a :class:`Request <Request>`.\n\n    :param method: method for the new :class:`Request` object: ``GET``, ``OPTIONS``, ``HEAD``, ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.\n    :param url: URL for the new :class:`Request` object.\n    :param params: (optional) Dictionary, list of tuples or bytes to send\n        in the query string for the :class:`Request`.\n    :param data: (optional) Dictionary, list of tuples, bytes, or file-like\n        object to send in the body of the :class:`Request`.\n    :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.\n    :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.\n    :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.\n    :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload.\n        ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')``\n        or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string\n        defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers\n        to add for the file.\n    :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.\n    :param timeout: (optional) How many seconds to wait for the server to send data\n        before giving up, as a float, or a :ref:`(connect timeout, read\n        timeout) <timeouts>` tuple.\n    :type timeout: float or tuple\n    :param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``True``.\n    :type allow_redirects: bool\n    :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.\n    :param verify: (optional) Either a boolean, in which case it controls whether we verify\n            the server's TLS certificate, or a string, in which case it must be a path\n            to a CA bundle to use. Defaults to ``True``.\n    :param stream: (optional) if ``False``, the response content will be immediately downloaded.\n    :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.\n    :return: :class:`Response <Response>` object\n    :rtype: requests.Response\n\n    Usage::\n\n      >>> import requests\n      >>> req = requests.request('GET', 'https://httpbin.org/get')\n      >>> req\n      <Response [200]>\n    \"\"\"\n\n    # By using the 'with' statement we are sure the session is closed, thus we\n    # avoid leaving sockets open which can trigger a ResourceWarning in some\n    # cases, and look like a memory leak in others.\n    with sessions.Session() as session:\n        return session.request(method=method, url=url, **kwargs)\n\n\ndef get(url, params=None, **kwargs):\n    r\"\"\"Sends a GET request.\n\n    :param url: URL for the new :class:`Request` object.\n    :param params: (optional) Dictionary, list of tuples or bytes to send\n        in the query string for the :class:`Request`.\n    :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n    :return: :class:`Response <Response>` object\n    :rtype: requests.Response\n    \"\"\"\n\n    return request('get', url, params=params, **kwargs)\n\n\ndef options(url, **kwargs):\n    r\"\"\"Sends an OPTIONS request.\n\n    :param url: URL for the new :class:`Request` object.\n    :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n    :return: :class:`Response <Response>` object\n    :rtype: requests.Response\n    \"\"\"\n\n    return request('options', url, **kwargs)\n\n\ndef head(url, **kwargs):\n    r\"\"\"Sends a HEAD request.\n\n    :param url: URL for the new :class:`Request` object.\n    :param \\*\\*kwargs: Optional arguments that ``request`` takes. If\n        `allow_redirects` is not provided, it will be set to `False` (as\n        opposed to the default :meth:`request` behavior).\n    :return: :class:`Response <Response>` object\n    :rtype: requests.Response\n    \"\"\"\n\n    kwargs.setdefault('allow_redirects', False)\n    return request('head', url, **kwargs)\n\n\ndef post(url, data=None, json=None, **kwargs):\n    r\"\"\"Sends a POST request.\n\n    :param url: URL for the new :class:`Request` object.\n    :param data: (optional) Dictionary, list of tuples, bytes, or file-like\n        object to send in the body of the :class:`Request`.\n    :param json: (optional) json data to send in the body of the :class:`Request`.\n    :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n    :return: :class:`Response <Response>` object\n    :rtype: requests.Response\n    \"\"\"\n\n    return request('post', url, data=data, json=json, **kwargs)\n\n\ndef put(url, data=None, **kwargs):\n    r\"\"\"Sends a PUT request.\n\n    :param url: URL for the new :class:`Request` object.\n    :param data: (optional) Dictionary, list of tuples, bytes, or file-like\n        object to send in the body of the :class:`Request`.\n    :param json: (optional) json data to send in the body of the :class:`Request`.\n    :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n    :return: :class:`Response <Response>` object\n    :rtype: requests.Response\n    \"\"\"\n\n    return request('put', url, data=data, **kwargs)\n\n\ndef patch(url, data=None, **kwargs):\n    r\"\"\"Sends a PATCH request.\n\n    :param url: URL for the new :class:`Request` object.\n    :param data: (optional) Dictionary, list of tuples, bytes, or file-like\n        object to send in the body of the :class:`Request`.\n    :param json: (optional) json data to send in the body of the :class:`Request`.\n    :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n    :return: :class:`Response <Response>` object\n    :rtype: requests.Response\n    \"\"\"\n\n    return request('patch', url, data=data, **kwargs)\n\n\ndef delete(url, **kwargs):\n    r\"\"\"Sends a DELETE request.\n\n    :param url: URL for the new :class:`Request` object.\n    :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n    :return: :class:`Response <Response>` object\n    :rtype: requests.Response\n    \"\"\"\n\n    return request('delete', url, **kwargs)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/auth.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests.auth\n~~~~~~~~~~~~~\n\nThis module contains the authentication handlers for Requests.\n\"\"\"\n\nimport os\nimport re\nimport time\nimport hashlib\nimport threading\nimport warnings\n\nfrom base64 import b64encode\n\nfrom .compat import urlparse, str, basestring\nfrom .cookies import extract_cookies_to_jar\nfrom ._internal_utils import to_native_string\nfrom .utils import parse_dict_header\n\nCONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'\nCONTENT_TYPE_MULTI_PART = 'multipart/form-data'\n\n\ndef _basic_auth_str(username, password):\n    \"\"\"Returns a Basic Auth string.\"\"\"\n\n    # \"I want us to put a big-ol' comment on top of it that\n    # says that this behaviour is dumb but we need to preserve\n    # it because people are relying on it.\"\n    #    - Lukasa\n    #\n    # These are here solely to maintain backwards compatibility\n    # for things like ints. This will be removed in 3.0.0.\n    if not isinstance(username, basestring):\n        warnings.warn(\n            \"Non-string usernames will no longer be supported in Requests \"\n            \"3.0.0. Please convert the object you've passed in ({!r}) to \"\n            \"a string or bytes object in the near future to avoid \"\n            \"problems.\".format(username),\n            category=DeprecationWarning,\n        )\n        username = str(username)\n\n    if not isinstance(password, basestring):\n        warnings.warn(\n            \"Non-string passwords will no longer be supported in Requests \"\n            \"3.0.0. Please convert the object you've passed in ({!r}) to \"\n            \"a string or bytes object in the near future to avoid \"\n            \"problems.\".format(type(password)),\n            category=DeprecationWarning,\n        )\n        password = str(password)\n    # -- End Removal --\n\n    if isinstance(username, str):\n        username = username.encode('latin1')\n\n    if isinstance(password, str):\n        password = password.encode('latin1')\n\n    authstr = 'Basic ' + to_native_string(\n        b64encode(b':'.join((username, password))).strip()\n    )\n\n    return authstr\n\n\nclass AuthBase(object):\n    \"\"\"Base class that all auth implementations derive from\"\"\"\n\n    def __call__(self, r):\n        raise NotImplementedError('Auth hooks must be callable.')\n\n\nclass HTTPBasicAuth(AuthBase):\n    \"\"\"Attaches HTTP Basic Authentication to the given Request object.\"\"\"\n\n    def __init__(self, username, password):\n        self.username = username\n        self.password = password\n\n    def __eq__(self, other):\n        return all([\n            self.username == getattr(other, 'username', None),\n            self.password == getattr(other, 'password', None)\n        ])\n\n    def __ne__(self, other):\n        return not self == other\n\n    def __call__(self, r):\n        r.headers['Authorization'] = _basic_auth_str(self.username, self.password)\n        return r\n\n\nclass HTTPProxyAuth(HTTPBasicAuth):\n    \"\"\"Attaches HTTP Proxy Authentication to a given Request object.\"\"\"\n\n    def __call__(self, r):\n        r.headers['Proxy-Authorization'] = _basic_auth_str(self.username, self.password)\n        return r\n\n\nclass HTTPDigestAuth(AuthBase):\n    \"\"\"Attaches HTTP Digest Authentication to the given Request object.\"\"\"\n\n    def __init__(self, username, password):\n        self.username = username\n        self.password = password\n        # Keep state in per-thread local storage\n        self._thread_local = threading.local()\n\n    def init_per_thread_state(self):\n        # Ensure state is initialized just once per-thread\n        if not hasattr(self._thread_local, 'init'):\n            self._thread_local.init = True\n            self._thread_local.last_nonce = ''\n            self._thread_local.nonce_count = 0\n            self._thread_local.chal = {}\n            self._thread_local.pos = None\n            self._thread_local.num_401_calls = None\n\n    def build_digest_header(self, method, url):\n        \"\"\"\n        :rtype: str\n        \"\"\"\n\n        realm = self._thread_local.chal['realm']\n        nonce = self._thread_local.chal['nonce']\n        qop = self._thread_local.chal.get('qop')\n        algorithm = self._thread_local.chal.get('algorithm')\n        opaque = self._thread_local.chal.get('opaque')\n        hash_utf8 = None\n\n        if algorithm is None:\n            _algorithm = 'MD5'\n        else:\n            _algorithm = algorithm.upper()\n        # lambdas assume digest modules are imported at the top level\n        if _algorithm == 'MD5' or _algorithm == 'MD5-SESS':\n            def md5_utf8(x):\n                if isinstance(x, str):\n                    x = x.encode('utf-8')\n                return hashlib.md5(x).hexdigest()\n            hash_utf8 = md5_utf8\n        elif _algorithm == 'SHA':\n            def sha_utf8(x):\n                if isinstance(x, str):\n                    x = x.encode('utf-8')\n                return hashlib.sha1(x).hexdigest()\n            hash_utf8 = sha_utf8\n        elif _algorithm == 'SHA-256':\n            def sha256_utf8(x):\n                if isinstance(x, str):\n                    x = x.encode('utf-8')\n                return hashlib.sha256(x).hexdigest()\n            hash_utf8 = sha256_utf8\n        elif _algorithm == 'SHA-512':\n            def sha512_utf8(x):\n                if isinstance(x, str):\n                    x = x.encode('utf-8')\n                return hashlib.sha512(x).hexdigest()\n            hash_utf8 = sha512_utf8\n\n        KD = lambda s, d: hash_utf8(\"%s:%s\" % (s, d))\n\n        if hash_utf8 is None:\n            return None\n\n        # XXX not implemented yet\n        entdig = None\n        p_parsed = urlparse(url)\n        #: path is request-uri defined in RFC 2616 which should not be empty\n        path = p_parsed.path or \"/\"\n        if p_parsed.query:\n            path += '?' + p_parsed.query\n\n        A1 = '%s:%s:%s' % (self.username, realm, self.password)\n        A2 = '%s:%s' % (method, path)\n\n        HA1 = hash_utf8(A1)\n        HA2 = hash_utf8(A2)\n\n        if nonce == self._thread_local.last_nonce:\n            self._thread_local.nonce_count += 1\n        else:\n            self._thread_local.nonce_count = 1\n        ncvalue = '%08x' % self._thread_local.nonce_count\n        s = str(self._thread_local.nonce_count).encode('utf-8')\n        s += nonce.encode('utf-8')\n        s += time.ctime().encode('utf-8')\n        s += os.urandom(8)\n\n        cnonce = (hashlib.sha1(s).hexdigest()[:16])\n        if _algorithm == 'MD5-SESS':\n            HA1 = hash_utf8('%s:%s:%s' % (HA1, nonce, cnonce))\n\n        if not qop:\n            respdig = KD(HA1, \"%s:%s\" % (nonce, HA2))\n        elif qop == 'auth' or 'auth' in qop.split(','):\n            noncebit = \"%s:%s:%s:%s:%s\" % (\n                nonce, ncvalue, cnonce, 'auth', HA2\n            )\n            respdig = KD(HA1, noncebit)\n        else:\n            # XXX handle auth-int.\n            return None\n\n        self._thread_local.last_nonce = nonce\n\n        # XXX should the partial digests be encoded too?\n        base = 'username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", ' \\\n               'response=\"%s\"' % (self.username, realm, nonce, path, respdig)\n        if opaque:\n            base += ', opaque=\"%s\"' % opaque\n        if algorithm:\n            base += ', algorithm=\"%s\"' % algorithm\n        if entdig:\n            base += ', digest=\"%s\"' % entdig\n        if qop:\n            base += ', qop=\"auth\", nc=%s, cnonce=\"%s\"' % (ncvalue, cnonce)\n\n        return 'Digest %s' % (base)\n\n    def handle_redirect(self, r, **kwargs):\n        \"\"\"Reset num_401_calls counter on redirects.\"\"\"\n        if r.is_redirect:\n            self._thread_local.num_401_calls = 1\n\n    def handle_401(self, r, **kwargs):\n        \"\"\"\n        Takes the given response and tries digest-auth, if needed.\n\n        :rtype: requests.Response\n        \"\"\"\n\n        # If response is not 4xx, do not auth\n        # See https://github.com/psf/requests/issues/3772\n        if not 400 <= r.status_code < 500:\n            self._thread_local.num_401_calls = 1\n            return r\n\n        if self._thread_local.pos is not None:\n            # Rewind the file position indicator of the body to where\n            # it was to resend the request.\n            r.request.body.seek(self._thread_local.pos)\n        s_auth = r.headers.get('www-authenticate', '')\n\n        if 'digest' in s_auth.lower() and self._thread_local.num_401_calls < 2:\n\n            self._thread_local.num_401_calls += 1\n            pat = re.compile(r'digest ', flags=re.IGNORECASE)\n            self._thread_local.chal = parse_dict_header(pat.sub('', s_auth, count=1))\n\n            # Consume content and release the original connection\n            # to allow our new request to reuse the same one.\n            r.content\n            r.close()\n            prep = r.request.copy()\n            extract_cookies_to_jar(prep._cookies, r.request, r.raw)\n            prep.prepare_cookies(prep._cookies)\n\n            prep.headers['Authorization'] = self.build_digest_header(\n                prep.method, prep.url)\n            _r = r.connection.send(prep, **kwargs)\n            _r.history.append(r)\n            _r.request = prep\n\n            return _r\n\n        self._thread_local.num_401_calls = 1\n        return r\n\n    def __call__(self, r):\n        # Initialize per-thread state, if needed\n        self.init_per_thread_state()\n        # If we have a saved nonce, skip the 401\n        if self._thread_local.last_nonce:\n            r.headers['Authorization'] = self.build_digest_header(r.method, r.url)\n        try:\n            self._thread_local.pos = r.body.tell()\n        except AttributeError:\n            # In the case of HTTPDigestAuth being reused and the body of\n            # the previous request was a file-like object, pos has the\n            # file position of the previous body. Ensure it's set to\n            # None.\n            self._thread_local.pos = None\n        r.register_hook('response', self.handle_401)\n        r.register_hook('response', self.handle_redirect)\n        self._thread_local.num_401_calls = 1\n\n        return r\n\n    def __eq__(self, other):\n        return all([\n            self.username == getattr(other, 'username', None),\n            self.password == getattr(other, 'password', None)\n        ])\n\n    def __ne__(self, other):\n        return not self == other\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/certs.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests.certs\n~~~~~~~~~~~~~~\n\nThis module returns the preferred default CA certificate bundle. There is\nonly one — the one from the certifi package.\n\nIf you are packaging Requests, e.g., for a Linux distribution or a managed\nenvironment, you can change the definition of where() to return a separately\npackaged CA bundle.\n\"\"\"\nfrom certifi import where\n\nif __name__ == '__main__':\n    print(where())\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/compat.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests.compat\n~~~~~~~~~~~~~~~\n\nThis module handles import compatibility issues between Python 2 and\nPython 3.\n\"\"\"\n\ntry:\n    import chardet\nexcept ImportError:\n    import charset_normalizer as chardet\n\nimport sys\n\n# -------\n# Pythons\n# -------\n\n# Syntax sugar.\n_ver = sys.version_info\n\n#: Python 2.x?\nis_py2 = (_ver[0] == 2)\n\n#: Python 3.x?\nis_py3 = (_ver[0] == 3)\n\nhas_simplejson = False\ntry:\n    import simplejson as json\n    has_simplejson = True\nexcept ImportError:\n    import json\n\n# ---------\n# Specifics\n# ---------\n\nif is_py2:\n    from urllib import (\n        quote, unquote, quote_plus, unquote_plus, urlencode, getproxies,\n        proxy_bypass, proxy_bypass_environment, getproxies_environment)\n    from urlparse import urlparse, urlunparse, urljoin, urlsplit, urldefrag\n    from urllib2 import parse_http_list\n    import cookielib\n    from Cookie import Morsel\n    from StringIO import StringIO\n    # Keep OrderedDict for backwards compatibility.\n    from collections import Callable, Mapping, MutableMapping, OrderedDict\n\n    builtin_str = str\n    bytes = str\n    str = unicode\n    basestring = basestring\n    numeric_types = (int, long, float)\n    integer_types = (int, long)\n    JSONDecodeError = ValueError\n\nelif is_py3:\n    from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote, quote_plus, unquote_plus, urldefrag\n    from urllib.request import parse_http_list, getproxies, proxy_bypass, proxy_bypass_environment, getproxies_environment\n    from http import cookiejar as cookielib\n    from http.cookies import Morsel\n    from io import StringIO\n    # Keep OrderedDict for backwards compatibility.\n    from collections import OrderedDict\n    from collections.abc import Callable, Mapping, MutableMapping\n    if has_simplejson:\n        from simplejson import JSONDecodeError\n    else:\n        from json import JSONDecodeError\n\n    builtin_str = str\n    str = str\n    bytes = bytes\n    basestring = (str, bytes)\n    numeric_types = (int, float)\n    integer_types = (int,)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/cookies.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests.cookies\n~~~~~~~~~~~~~~~~\n\nCompatibility code to be able to use `cookielib.CookieJar` with requests.\n\nrequests.utils imports from here, so be careful with imports.\n\"\"\"\n\nimport copy\nimport time\nimport calendar\n\nfrom ._internal_utils import to_native_string\nfrom .compat import cookielib, urlparse, urlunparse, Morsel, MutableMapping\n\ntry:\n    import threading\nexcept ImportError:\n    import dummy_threading as threading\n\n\nclass MockRequest(object):\n    \"\"\"Wraps a `requests.Request` to mimic a `urllib2.Request`.\n\n    The code in `cookielib.CookieJar` expects this interface in order to correctly\n    manage cookie policies, i.e., determine whether a cookie can be set, given the\n    domains of the request and the cookie.\n\n    The original request object is read-only. The client is responsible for collecting\n    the new headers via `get_new_headers()` and interpreting them appropriately. You\n    probably want `get_cookie_header`, defined below.\n    \"\"\"\n\n    def __init__(self, request):\n        self._r = request\n        self._new_headers = {}\n        self.type = urlparse(self._r.url).scheme\n\n    def get_type(self):\n        return self.type\n\n    def get_host(self):\n        return urlparse(self._r.url).netloc\n\n    def get_origin_req_host(self):\n        return self.get_host()\n\n    def get_full_url(self):\n        # Only return the response's URL if the user hadn't set the Host\n        # header\n        if not self._r.headers.get('Host'):\n            return self._r.url\n        # If they did set it, retrieve it and reconstruct the expected domain\n        host = to_native_string(self._r.headers['Host'], encoding='utf-8')\n        parsed = urlparse(self._r.url)\n        # Reconstruct the URL as we expect it\n        return urlunparse([\n            parsed.scheme, host, parsed.path, parsed.params, parsed.query,\n            parsed.fragment\n        ])\n\n    def is_unverifiable(self):\n        return True\n\n    def has_header(self, name):\n        return name in self._r.headers or name in self._new_headers\n\n    def get_header(self, name, default=None):\n        return self._r.headers.get(name, self._new_headers.get(name, default))\n\n    def add_header(self, key, val):\n        \"\"\"cookielib has no legitimate use for this method; add it back if you find one.\"\"\"\n        raise NotImplementedError(\"Cookie headers should be added with add_unredirected_header()\")\n\n    def add_unredirected_header(self, name, value):\n        self._new_headers[name] = value\n\n    def get_new_headers(self):\n        return self._new_headers\n\n    @property\n    def unverifiable(self):\n        return self.is_unverifiable()\n\n    @property\n    def origin_req_host(self):\n        return self.get_origin_req_host()\n\n    @property\n    def host(self):\n        return self.get_host()\n\n\nclass MockResponse(object):\n    \"\"\"Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`.\n\n    ...what? Basically, expose the parsed HTTP headers from the server response\n    the way `cookielib` expects to see them.\n    \"\"\"\n\n    def __init__(self, headers):\n        \"\"\"Make a MockResponse for `cookielib` to read.\n\n        :param headers: a httplib.HTTPMessage or analogous carrying the headers\n        \"\"\"\n        self._headers = headers\n\n    def info(self):\n        return self._headers\n\n    def getheaders(self, name):\n        self._headers.getheaders(name)\n\n\ndef extract_cookies_to_jar(jar, request, response):\n    \"\"\"Extract the cookies from the response into a CookieJar.\n\n    :param jar: cookielib.CookieJar (not necessarily a RequestsCookieJar)\n    :param request: our own requests.Request object\n    :param response: urllib3.HTTPResponse object\n    \"\"\"\n    if not (hasattr(response, '_original_response') and\n            response._original_response):\n        return\n    # the _original_response field is the wrapped httplib.HTTPResponse object,\n    req = MockRequest(request)\n    # pull out the HTTPMessage with the headers and put it in the mock:\n    res = MockResponse(response._original_response.msg)\n    jar.extract_cookies(res, req)\n\n\ndef get_cookie_header(jar, request):\n    \"\"\"\n    Produce an appropriate Cookie header string to be sent with `request`, or None.\n\n    :rtype: str\n    \"\"\"\n    r = MockRequest(request)\n    jar.add_cookie_header(r)\n    return r.get_new_headers().get('Cookie')\n\n\ndef remove_cookie_by_name(cookiejar, name, domain=None, path=None):\n    \"\"\"Unsets a cookie by name, by default over all domains and paths.\n\n    Wraps CookieJar.clear(), is O(n).\n    \"\"\"\n    clearables = []\n    for cookie in cookiejar:\n        if cookie.name != name:\n            continue\n        if domain is not None and domain != cookie.domain:\n            continue\n        if path is not None and path != cookie.path:\n            continue\n        clearables.append((cookie.domain, cookie.path, cookie.name))\n\n    for domain, path, name in clearables:\n        cookiejar.clear(domain, path, name)\n\n\nclass CookieConflictError(RuntimeError):\n    \"\"\"There are two cookies that meet the criteria specified in the cookie jar.\n    Use .get and .set and include domain and path args in order to be more specific.\n    \"\"\"\n\n\nclass RequestsCookieJar(cookielib.CookieJar, MutableMapping):\n    \"\"\"Compatibility class; is a cookielib.CookieJar, but exposes a dict\n    interface.\n\n    This is the CookieJar we create by default for requests and sessions that\n    don't specify one, since some clients may expect response.cookies and\n    session.cookies to support dict operations.\n\n    Requests does not use the dict interface internally; it's just for\n    compatibility with external client code. All requests code should work\n    out of the box with externally provided instances of ``CookieJar``, e.g.\n    ``LWPCookieJar`` and ``FileCookieJar``.\n\n    Unlike a regular CookieJar, this class is pickleable.\n\n    .. warning:: dictionary operations that are normally O(1) may be O(n).\n    \"\"\"\n\n    def get(self, name, default=None, domain=None, path=None):\n        \"\"\"Dict-like get() that also supports optional domain and path args in\n        order to resolve naming collisions from using one cookie jar over\n        multiple domains.\n\n        .. warning:: operation is O(n), not O(1).\n        \"\"\"\n        try:\n            return self._find_no_duplicates(name, domain, path)\n        except KeyError:\n            return default\n\n    def set(self, name, value, **kwargs):\n        \"\"\"Dict-like set() that also supports optional domain and path args in\n        order to resolve naming collisions from using one cookie jar over\n        multiple domains.\n        \"\"\"\n        # support client code that unsets cookies by assignment of a None value:\n        if value is None:\n            remove_cookie_by_name(self, name, domain=kwargs.get('domain'), path=kwargs.get('path'))\n            return\n\n        if isinstance(value, Morsel):\n            c = morsel_to_cookie(value)\n        else:\n            c = create_cookie(name, value, **kwargs)\n        self.set_cookie(c)\n        return c\n\n    def iterkeys(self):\n        \"\"\"Dict-like iterkeys() that returns an iterator of names of cookies\n        from the jar.\n\n        .. seealso:: itervalues() and iteritems().\n        \"\"\"\n        for cookie in iter(self):\n            yield cookie.name\n\n    def keys(self):\n        \"\"\"Dict-like keys() that returns a list of names of cookies from the\n        jar.\n\n        .. seealso:: values() and items().\n        \"\"\"\n        return list(self.iterkeys())\n\n    def itervalues(self):\n        \"\"\"Dict-like itervalues() that returns an iterator of values of cookies\n        from the jar.\n\n        .. seealso:: iterkeys() and iteritems().\n        \"\"\"\n        for cookie in iter(self):\n            yield cookie.value\n\n    def values(self):\n        \"\"\"Dict-like values() that returns a list of values of cookies from the\n        jar.\n\n        .. seealso:: keys() and items().\n        \"\"\"\n        return list(self.itervalues())\n\n    def iteritems(self):\n        \"\"\"Dict-like iteritems() that returns an iterator of name-value tuples\n        from the jar.\n\n        .. seealso:: iterkeys() and itervalues().\n        \"\"\"\n        for cookie in iter(self):\n            yield cookie.name, cookie.value\n\n    def items(self):\n        \"\"\"Dict-like items() that returns a list of name-value tuples from the\n        jar. Allows client-code to call ``dict(RequestsCookieJar)`` and get a\n        vanilla python dict of key value pairs.\n\n        .. seealso:: keys() and values().\n        \"\"\"\n        return list(self.iteritems())\n\n    def list_domains(self):\n        \"\"\"Utility method to list all the domains in the jar.\"\"\"\n        domains = []\n        for cookie in iter(self):\n            if cookie.domain not in domains:\n                domains.append(cookie.domain)\n        return domains\n\n    def list_paths(self):\n        \"\"\"Utility method to list all the paths in the jar.\"\"\"\n        paths = []\n        for cookie in iter(self):\n            if cookie.path not in paths:\n                paths.append(cookie.path)\n        return paths\n\n    def multiple_domains(self):\n        \"\"\"Returns True if there are multiple domains in the jar.\n        Returns False otherwise.\n\n        :rtype: bool\n        \"\"\"\n        domains = []\n        for cookie in iter(self):\n            if cookie.domain is not None and cookie.domain in domains:\n                return True\n            domains.append(cookie.domain)\n        return False  # there is only one domain in jar\n\n    def get_dict(self, domain=None, path=None):\n        \"\"\"Takes as an argument an optional domain and path and returns a plain\n        old Python dict of name-value pairs of cookies that meet the\n        requirements.\n\n        :rtype: dict\n        \"\"\"\n        dictionary = {}\n        for cookie in iter(self):\n            if (\n                (domain is None or cookie.domain == domain) and\n                (path is None or cookie.path == path)\n            ):\n                dictionary[cookie.name] = cookie.value\n        return dictionary\n\n    def __contains__(self, name):\n        try:\n            return super(RequestsCookieJar, self).__contains__(name)\n        except CookieConflictError:\n            return True\n\n    def __getitem__(self, name):\n        \"\"\"Dict-like __getitem__() for compatibility with client code. Throws\n        exception if there are more than one cookie with name. In that case,\n        use the more explicit get() method instead.\n\n        .. warning:: operation is O(n), not O(1).\n        \"\"\"\n        return self._find_no_duplicates(name)\n\n    def __setitem__(self, name, value):\n        \"\"\"Dict-like __setitem__ for compatibility with client code. Throws\n        exception if there is already a cookie of that name in the jar. In that\n        case, use the more explicit set() method instead.\n        \"\"\"\n        self.set(name, value)\n\n    def __delitem__(self, name):\n        \"\"\"Deletes a cookie given a name. Wraps ``cookielib.CookieJar``'s\n        ``remove_cookie_by_name()``.\n        \"\"\"\n        remove_cookie_by_name(self, name)\n\n    def set_cookie(self, cookie, *args, **kwargs):\n        if hasattr(cookie.value, 'startswith') and cookie.value.startswith('\"') and cookie.value.endswith('\"'):\n            cookie.value = cookie.value.replace('\\\\\"', '')\n        return super(RequestsCookieJar, self).set_cookie(cookie, *args, **kwargs)\n\n    def update(self, other):\n        \"\"\"Updates this jar with cookies from another CookieJar or dict-like\"\"\"\n        if isinstance(other, cookielib.CookieJar):\n            for cookie in other:\n                self.set_cookie(copy.copy(cookie))\n        else:\n            super(RequestsCookieJar, self).update(other)\n\n    def _find(self, name, domain=None, path=None):\n        \"\"\"Requests uses this method internally to get cookie values.\n\n        If there are conflicting cookies, _find arbitrarily chooses one.\n        See _find_no_duplicates if you want an exception thrown if there are\n        conflicting cookies.\n\n        :param name: a string containing name of cookie\n        :param domain: (optional) string containing domain of cookie\n        :param path: (optional) string containing path of cookie\n        :return: cookie.value\n        \"\"\"\n        for cookie in iter(self):\n            if cookie.name == name:\n                if domain is None or cookie.domain == domain:\n                    if path is None or cookie.path == path:\n                        return cookie.value\n\n        raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))\n\n    def _find_no_duplicates(self, name, domain=None, path=None):\n        \"\"\"Both ``__get_item__`` and ``get`` call this function: it's never\n        used elsewhere in Requests.\n\n        :param name: a string containing name of cookie\n        :param domain: (optional) string containing domain of cookie\n        :param path: (optional) string containing path of cookie\n        :raises KeyError: if cookie is not found\n        :raises CookieConflictError: if there are multiple cookies\n            that match name and optionally domain and path\n        :return: cookie.value\n        \"\"\"\n        toReturn = None\n        for cookie in iter(self):\n            if cookie.name == name:\n                if domain is None or cookie.domain == domain:\n                    if path is None or cookie.path == path:\n                        if toReturn is not None:  # if there are multiple cookies that meet passed in criteria\n                            raise CookieConflictError('There are multiple cookies with name, %r' % (name))\n                        toReturn = cookie.value  # we will eventually return this as long as no cookie conflict\n\n        if toReturn:\n            return toReturn\n        raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))\n\n    def __getstate__(self):\n        \"\"\"Unlike a normal CookieJar, this class is pickleable.\"\"\"\n        state = self.__dict__.copy()\n        # remove the unpickleable RLock object\n        state.pop('_cookies_lock')\n        return state\n\n    def __setstate__(self, state):\n        \"\"\"Unlike a normal CookieJar, this class is pickleable.\"\"\"\n        self.__dict__.update(state)\n        if '_cookies_lock' not in self.__dict__:\n            self._cookies_lock = threading.RLock()\n\n    def copy(self):\n        \"\"\"Return a copy of this RequestsCookieJar.\"\"\"\n        new_cj = RequestsCookieJar()\n        new_cj.set_policy(self.get_policy())\n        new_cj.update(self)\n        return new_cj\n\n    def get_policy(self):\n        \"\"\"Return the CookiePolicy instance used.\"\"\"\n        return self._policy\n\n\ndef _copy_cookie_jar(jar):\n    if jar is None:\n        return None\n\n    if hasattr(jar, 'copy'):\n        # We're dealing with an instance of RequestsCookieJar\n        return jar.copy()\n    # We're dealing with a generic CookieJar instance\n    new_jar = copy.copy(jar)\n    new_jar.clear()\n    for cookie in jar:\n        new_jar.set_cookie(copy.copy(cookie))\n    return new_jar\n\n\ndef create_cookie(name, value, **kwargs):\n    \"\"\"Make a cookie from underspecified parameters.\n\n    By default, the pair of `name` and `value` will be set for the domain ''\n    and sent on every request (this is sometimes called a \"supercookie\").\n    \"\"\"\n    result = {\n        'version': 0,\n        'name': name,\n        'value': value,\n        'port': None,\n        'domain': '',\n        'path': '/',\n        'secure': False,\n        'expires': None,\n        'discard': True,\n        'comment': None,\n        'comment_url': None,\n        'rest': {'HttpOnly': None},\n        'rfc2109': False,\n    }\n\n    badargs = set(kwargs) - set(result)\n    if badargs:\n        err = 'create_cookie() got unexpected keyword arguments: %s'\n        raise TypeError(err % list(badargs))\n\n    result.update(kwargs)\n    result['port_specified'] = bool(result['port'])\n    result['domain_specified'] = bool(result['domain'])\n    result['domain_initial_dot'] = result['domain'].startswith('.')\n    result['path_specified'] = bool(result['path'])\n\n    return cookielib.Cookie(**result)\n\n\ndef morsel_to_cookie(morsel):\n    \"\"\"Convert a Morsel object into a Cookie containing the one k/v pair.\"\"\"\n\n    expires = None\n    if morsel['max-age']:\n        try:\n            expires = int(time.time() + int(morsel['max-age']))\n        except ValueError:\n            raise TypeError('max-age: %s must be integer' % morsel['max-age'])\n    elif morsel['expires']:\n        time_template = '%a, %d-%b-%Y %H:%M:%S GMT'\n        expires = calendar.timegm(\n            time.strptime(morsel['expires'], time_template)\n        )\n    return create_cookie(\n        comment=morsel['comment'],\n        comment_url=bool(morsel['comment']),\n        discard=False,\n        domain=morsel['domain'],\n        expires=expires,\n        name=morsel.key,\n        path=morsel['path'],\n        port=None,\n        rest={'HttpOnly': morsel['httponly']},\n        rfc2109=False,\n        secure=bool(morsel['secure']),\n        value=morsel.value,\n        version=morsel['version'] or 0,\n    )\n\n\ndef cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True):\n    \"\"\"Returns a CookieJar from a key/value dictionary.\n\n    :param cookie_dict: Dict of key/values to insert into CookieJar.\n    :param cookiejar: (optional) A cookiejar to add the cookies to.\n    :param overwrite: (optional) If False, will not replace cookies\n        already in the jar with new ones.\n    :rtype: CookieJar\n    \"\"\"\n    if cookiejar is None:\n        cookiejar = RequestsCookieJar()\n\n    if cookie_dict is not None:\n        names_from_jar = [cookie.name for cookie in cookiejar]\n        for name in cookie_dict:\n            if overwrite or (name not in names_from_jar):\n                cookiejar.set_cookie(create_cookie(name, cookie_dict[name]))\n\n    return cookiejar\n\n\ndef merge_cookies(cookiejar, cookies):\n    \"\"\"Add cookies to cookiejar and returns a merged CookieJar.\n\n    :param cookiejar: CookieJar object to add the cookies to.\n    :param cookies: Dictionary or CookieJar object to be added.\n    :rtype: CookieJar\n    \"\"\"\n    if not isinstance(cookiejar, cookielib.CookieJar):\n        raise ValueError('You can only merge into CookieJar')\n\n    if isinstance(cookies, dict):\n        cookiejar = cookiejar_from_dict(\n            cookies, cookiejar=cookiejar, overwrite=False)\n    elif isinstance(cookies, cookielib.CookieJar):\n        try:\n            cookiejar.update(cookies)\n        except AttributeError:\n            for cookie_in_jar in cookies:\n                cookiejar.set_cookie(cookie_in_jar)\n\n    return cookiejar\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/exceptions.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests.exceptions\n~~~~~~~~~~~~~~~~~~~\n\nThis module contains the set of Requests' exceptions.\n\"\"\"\nfrom urllib3.exceptions import HTTPError as BaseHTTPError\n\nfrom .compat import JSONDecodeError as CompatJSONDecodeError\n\n\nclass RequestException(IOError):\n    \"\"\"There was an ambiguous exception that occurred while handling your\n    request.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"Initialize RequestException with `request` and `response` objects.\"\"\"\n        response = kwargs.pop('response', None)\n        self.response = response\n        self.request = kwargs.pop('request', None)\n        if (response is not None and not self.request and\n                hasattr(response, 'request')):\n            self.request = self.response.request\n        super(RequestException, self).__init__(*args, **kwargs)\n\n\nclass InvalidJSONError(RequestException):\n    \"\"\"A JSON error occurred.\"\"\"\n\n\nclass JSONDecodeError(InvalidJSONError, CompatJSONDecodeError):\n    \"\"\"Couldn't decode the text into json\"\"\"\n\n\nclass HTTPError(RequestException):\n    \"\"\"An HTTP error occurred.\"\"\"\n\n\nclass ConnectionError(RequestException):\n    \"\"\"A Connection error occurred.\"\"\"\n\n\nclass ProxyError(ConnectionError):\n    \"\"\"A proxy error occurred.\"\"\"\n\n\nclass SSLError(ConnectionError):\n    \"\"\"An SSL error occurred.\"\"\"\n\n\nclass Timeout(RequestException):\n    \"\"\"The request timed out.\n\n    Catching this error will catch both\n    :exc:`~requests.exceptions.ConnectTimeout` and\n    :exc:`~requests.exceptions.ReadTimeout` errors.\n    \"\"\"\n\n\nclass ConnectTimeout(ConnectionError, Timeout):\n    \"\"\"The request timed out while trying to connect to the remote server.\n\n    Requests that produced this error are safe to retry.\n    \"\"\"\n\n\nclass ReadTimeout(Timeout):\n    \"\"\"The server did not send any data in the allotted amount of time.\"\"\"\n\n\nclass URLRequired(RequestException):\n    \"\"\"A valid URL is required to make a request.\"\"\"\n\n\nclass TooManyRedirects(RequestException):\n    \"\"\"Too many redirects.\"\"\"\n\n\nclass MissingSchema(RequestException, ValueError):\n    \"\"\"The URL scheme (e.g. http or https) is missing.\"\"\"\n\n\nclass InvalidSchema(RequestException, ValueError):\n    \"\"\"The URL scheme provided is either invalid or unsupported.\"\"\"\n\n\nclass InvalidURL(RequestException, ValueError):\n    \"\"\"The URL provided was somehow invalid.\"\"\"\n\n\nclass InvalidHeader(RequestException, ValueError):\n    \"\"\"The header value provided was somehow invalid.\"\"\"\n\n\nclass InvalidProxyURL(InvalidURL):\n    \"\"\"The proxy URL provided is invalid.\"\"\"\n\n\nclass ChunkedEncodingError(RequestException):\n    \"\"\"The server declared chunked encoding but sent an invalid chunk.\"\"\"\n\n\nclass ContentDecodingError(RequestException, BaseHTTPError):\n    \"\"\"Failed to decode response content.\"\"\"\n\n\nclass StreamConsumedError(RequestException, TypeError):\n    \"\"\"The content for this response was already consumed.\"\"\"\n\n\nclass RetryError(RequestException):\n    \"\"\"Custom retries logic failed\"\"\"\n\n\nclass UnrewindableBodyError(RequestException):\n    \"\"\"Requests encountered an error when trying to rewind a body.\"\"\"\n\n# Warnings\n\n\nclass RequestsWarning(Warning):\n    \"\"\"Base warning for Requests.\"\"\"\n\n\nclass FileModeWarning(RequestsWarning, DeprecationWarning):\n    \"\"\"A file was opened in text mode, but Requests determined its binary length.\"\"\"\n\n\nclass RequestsDependencyWarning(RequestsWarning):\n    \"\"\"An imported dependency doesn't match the expected version range.\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/help.py",
    "content": "\"\"\"Module containing bug report helper(s).\"\"\"\nfrom __future__ import print_function\n\nimport json\nimport platform\nimport sys\nimport ssl\n\nimport idna\nimport urllib3\n\nfrom . import __version__ as requests_version\n\ntry:\n    import charset_normalizer\nexcept ImportError:\n    charset_normalizer = None\n\ntry:\n    import chardet\nexcept ImportError:\n    chardet = None\n\ntry:\n    from urllib3.contrib import pyopenssl\nexcept ImportError:\n    pyopenssl = None\n    OpenSSL = None\n    cryptography = None\nelse:\n    import OpenSSL\n    import cryptography\n\n\ndef _implementation():\n    \"\"\"Return a dict with the Python implementation and version.\n\n    Provide both the name and the version of the Python implementation\n    currently running. For example, on CPython 2.7.5 it will return\n    {'name': 'CPython', 'version': '2.7.5'}.\n\n    This function works best on CPython and PyPy: in particular, it probably\n    doesn't work for Jython or IronPython. Future investigation should be done\n    to work out the correct shape of the code for those platforms.\n    \"\"\"\n    implementation = platform.python_implementation()\n\n    if implementation == 'CPython':\n        implementation_version = platform.python_version()\n    elif implementation == 'PyPy':\n        implementation_version = '%s.%s.%s' % (sys.pypy_version_info.major,\n                                               sys.pypy_version_info.minor,\n                                               sys.pypy_version_info.micro)\n        if sys.pypy_version_info.releaselevel != 'final':\n            implementation_version = ''.join([\n                implementation_version, sys.pypy_version_info.releaselevel\n            ])\n    elif implementation == 'Jython':\n        implementation_version = platform.python_version()  # Complete Guess\n    elif implementation == 'IronPython':\n        implementation_version = platform.python_version()  # Complete Guess\n    else:\n        implementation_version = 'Unknown'\n\n    return {'name': implementation, 'version': implementation_version}\n\n\ndef info():\n    \"\"\"Generate information for a bug report.\"\"\"\n    try:\n        platform_info = {\n            'system': platform.system(),\n            'release': platform.release(),\n        }\n    except IOError:\n        platform_info = {\n            'system': 'Unknown',\n            'release': 'Unknown',\n        }\n\n    implementation_info = _implementation()\n    urllib3_info = {'version': urllib3.__version__}\n    charset_normalizer_info = {'version': None}\n    chardet_info = {'version': None}\n    if charset_normalizer:\n        charset_normalizer_info = {'version': charset_normalizer.__version__}\n    if chardet:\n        chardet_info = {'version': chardet.__version__}\n\n    pyopenssl_info = {\n        'version': None,\n        'openssl_version': '',\n    }\n    if OpenSSL:\n        pyopenssl_info = {\n            'version': OpenSSL.__version__,\n            'openssl_version': '%x' % OpenSSL.SSL.OPENSSL_VERSION_NUMBER,\n        }\n    cryptography_info = {\n        'version': getattr(cryptography, '__version__', ''),\n    }\n    idna_info = {\n        'version': getattr(idna, '__version__', ''),\n    }\n\n    system_ssl = ssl.OPENSSL_VERSION_NUMBER\n    system_ssl_info = {\n        'version': '%x' % system_ssl if system_ssl is not None else ''\n    }\n\n    return {\n        'platform': platform_info,\n        'implementation': implementation_info,\n        'system_ssl': system_ssl_info,\n        'using_pyopenssl': pyopenssl is not None,\n        'using_charset_normalizer': chardet is None,\n        'pyOpenSSL': pyopenssl_info,\n        'urllib3': urllib3_info,\n        'chardet': chardet_info,\n        'charset_normalizer': charset_normalizer_info,\n        'cryptography': cryptography_info,\n        'idna': idna_info,\n        'requests': {\n            'version': requests_version,\n        },\n    }\n\n\ndef main():\n    \"\"\"Pretty-print the bug information as JSON.\"\"\"\n    print(json.dumps(info(), sort_keys=True, indent=2))\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/hooks.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests.hooks\n~~~~~~~~~~~~~~\n\nThis module provides the capabilities for the Requests hooks system.\n\nAvailable hooks:\n\n``response``:\n    The response generated from a Request.\n\"\"\"\nHOOKS = ['response']\n\n\ndef default_hooks():\n    return {event: [] for event in HOOKS}\n\n# TODO: response is the only one\n\n\ndef dispatch_hook(key, hooks, hook_data, **kwargs):\n    \"\"\"Dispatches a hook dictionary on a given piece of data.\"\"\"\n    hooks = hooks or {}\n    hooks = hooks.get(key)\n    if hooks:\n        if hasattr(hooks, '__call__'):\n            hooks = [hooks]\n        for hook in hooks:\n            _hook_data = hook(hook_data, **kwargs)\n            if _hook_data is not None:\n                hook_data = _hook_data\n    return hook_data\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/models.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests.models\n~~~~~~~~~~~~~~~\n\nThis module contains the primary objects that power Requests.\n\"\"\"\n\nimport datetime\nimport sys\n\n# Import encoding now, to avoid implicit import later.\n# Implicit import within threads may cause LookupError when standard library is in a ZIP,\n# such as in Embedded Python. See https://github.com/psf/requests/issues/3578.\nimport encodings.idna\n\nfrom urllib3.fields import RequestField\nfrom urllib3.filepost import encode_multipart_formdata\nfrom urllib3.util import parse_url\nfrom urllib3.exceptions import (\n    DecodeError, ReadTimeoutError, ProtocolError, LocationParseError)\n\nfrom io import UnsupportedOperation\nfrom .hooks import default_hooks\nfrom .structures import CaseInsensitiveDict\n\nfrom .auth import HTTPBasicAuth\nfrom .cookies import cookiejar_from_dict, get_cookie_header, _copy_cookie_jar\nfrom .exceptions import (\n    HTTPError, MissingSchema, InvalidURL, ChunkedEncodingError,\n    ContentDecodingError, ConnectionError, StreamConsumedError,\n    InvalidJSONError)\nfrom .exceptions import JSONDecodeError as RequestsJSONDecodeError\nfrom ._internal_utils import to_native_string, unicode_is_ascii\nfrom .utils import (\n    guess_filename, get_auth_from_url, requote_uri,\n    stream_decode_response_unicode, to_key_val_list, parse_header_links,\n    iter_slices, guess_json_utf, super_len, check_header_validity)\nfrom .compat import (\n    Callable, Mapping,\n    cookielib, urlunparse, urlsplit, urlencode, str, bytes,\n    is_py2, chardet, builtin_str, basestring, JSONDecodeError)\nfrom .compat import json as complexjson\nfrom .status_codes import codes\n\n#: The set of HTTP status codes that indicate an automatically\n#: processable redirect.\nREDIRECT_STATI = (\n    codes.moved,               # 301\n    codes.found,               # 302\n    codes.other,               # 303\n    codes.temporary_redirect,  # 307\n    codes.permanent_redirect,  # 308\n)\n\nDEFAULT_REDIRECT_LIMIT = 30\nCONTENT_CHUNK_SIZE = 10 * 1024\nITER_CHUNK_SIZE = 512\n\n\nclass RequestEncodingMixin(object):\n    @property\n    def path_url(self):\n        \"\"\"Build the path URL to use.\"\"\"\n\n        url = []\n\n        p = urlsplit(self.url)\n\n        path = p.path\n        if not path:\n            path = '/'\n\n        url.append(path)\n\n        query = p.query\n        if query:\n            url.append('?')\n            url.append(query)\n\n        return ''.join(url)\n\n    @staticmethod\n    def _encode_params(data):\n        \"\"\"Encode parameters in a piece of data.\n\n        Will successfully encode parameters when passed as a dict or a list of\n        2-tuples. Order is retained if data is a list of 2-tuples but arbitrary\n        if parameters are supplied as a dict.\n        \"\"\"\n\n        if isinstance(data, (str, bytes)):\n            return data\n        elif hasattr(data, 'read'):\n            return data\n        elif hasattr(data, '__iter__'):\n            result = []\n            for k, vs in to_key_val_list(data):\n                if isinstance(vs, basestring) or not hasattr(vs, '__iter__'):\n                    vs = [vs]\n                for v in vs:\n                    if v is not None:\n                        result.append(\n                            (k.encode('utf-8') if isinstance(k, str) else k,\n                             v.encode('utf-8') if isinstance(v, str) else v))\n            return urlencode(result, doseq=True)\n        else:\n            return data\n\n    @staticmethod\n    def _encode_files(files, data):\n        \"\"\"Build the body for a multipart/form-data request.\n\n        Will successfully encode files when passed as a dict or a list of\n        tuples. Order is retained if data is a list of tuples but arbitrary\n        if parameters are supplied as a dict.\n        The tuples may be 2-tuples (filename, fileobj), 3-tuples (filename, fileobj, contentype)\n        or 4-tuples (filename, fileobj, contentype, custom_headers).\n        \"\"\"\n        if (not files):\n            raise ValueError(\"Files must be provided.\")\n        elif isinstance(data, basestring):\n            raise ValueError(\"Data must not be a string.\")\n\n        new_fields = []\n        fields = to_key_val_list(data or {})\n        files = to_key_val_list(files or {})\n\n        for field, val in fields:\n            if isinstance(val, basestring) or not hasattr(val, '__iter__'):\n                val = [val]\n            for v in val:\n                if v is not None:\n                    # Don't call str() on bytestrings: in Py3 it all goes wrong.\n                    if not isinstance(v, bytes):\n                        v = str(v)\n\n                    new_fields.append(\n                        (field.decode('utf-8') if isinstance(field, bytes) else field,\n                         v.encode('utf-8') if isinstance(v, str) else v))\n\n        for (k, v) in files:\n            # support for explicit filename\n            ft = None\n            fh = None\n            if isinstance(v, (tuple, list)):\n                if len(v) == 2:\n                    fn, fp = v\n                elif len(v) == 3:\n                    fn, fp, ft = v\n                else:\n                    fn, fp, ft, fh = v\n            else:\n                fn = guess_filename(v) or k\n                fp = v\n\n            if isinstance(fp, (str, bytes, bytearray)):\n                fdata = fp\n            elif hasattr(fp, 'read'):\n                fdata = fp.read()\n            elif fp is None:\n                continue\n            else:\n                fdata = fp\n\n            rf = RequestField(name=k, data=fdata, filename=fn, headers=fh)\n            rf.make_multipart(content_type=ft)\n            new_fields.append(rf)\n\n        body, content_type = encode_multipart_formdata(new_fields)\n\n        return body, content_type\n\n\nclass RequestHooksMixin(object):\n    def register_hook(self, event, hook):\n        \"\"\"Properly register a hook.\"\"\"\n\n        if event not in self.hooks:\n            raise ValueError('Unsupported event specified, with event name \"%s\"' % (event))\n\n        if isinstance(hook, Callable):\n            self.hooks[event].append(hook)\n        elif hasattr(hook, '__iter__'):\n            self.hooks[event].extend(h for h in hook if isinstance(h, Callable))\n\n    def deregister_hook(self, event, hook):\n        \"\"\"Deregister a previously registered hook.\n        Returns True if the hook existed, False if not.\n        \"\"\"\n\n        try:\n            self.hooks[event].remove(hook)\n            return True\n        except ValueError:\n            return False\n\n\nclass Request(RequestHooksMixin):\n    \"\"\"A user-created :class:`Request <Request>` object.\n\n    Used to prepare a :class:`PreparedRequest <PreparedRequest>`, which is sent to the server.\n\n    :param method: HTTP method to use.\n    :param url: URL to send.\n    :param headers: dictionary of headers to send.\n    :param files: dictionary of {filename: fileobject} files to multipart upload.\n    :param data: the body to attach to the request. If a dictionary or\n        list of tuples ``[(key, value)]`` is provided, form-encoding will\n        take place.\n    :param json: json for the body to attach to the request (if files or data is not specified).\n    :param params: URL parameters to append to the URL. If a dictionary or\n        list of tuples ``[(key, value)]`` is provided, form-encoding will\n        take place.\n    :param auth: Auth handler or (user, pass) tuple.\n    :param cookies: dictionary or CookieJar of cookies to attach to this request.\n    :param hooks: dictionary of callback hooks, for internal usage.\n\n    Usage::\n\n      >>> import requests\n      >>> req = requests.Request('GET', 'https://httpbin.org/get')\n      >>> req.prepare()\n      <PreparedRequest [GET]>\n    \"\"\"\n\n    def __init__(self,\n            method=None, url=None, headers=None, files=None, data=None,\n            params=None, auth=None, cookies=None, hooks=None, json=None):\n\n        # Default empty dicts for dict params.\n        data = [] if data is None else data\n        files = [] if files is None else files\n        headers = {} if headers is None else headers\n        params = {} if params is None else params\n        hooks = {} if hooks is None else hooks\n\n        self.hooks = default_hooks()\n        for (k, v) in list(hooks.items()):\n            self.register_hook(event=k, hook=v)\n\n        self.method = method\n        self.url = url\n        self.headers = headers\n        self.files = files\n        self.data = data\n        self.json = json\n        self.params = params\n        self.auth = auth\n        self.cookies = cookies\n\n    def __repr__(self):\n        return '<Request [%s]>' % (self.method)\n\n    def prepare(self):\n        \"\"\"Constructs a :class:`PreparedRequest <PreparedRequest>` for transmission and returns it.\"\"\"\n        p = PreparedRequest()\n        p.prepare(\n            method=self.method,\n            url=self.url,\n            headers=self.headers,\n            files=self.files,\n            data=self.data,\n            json=self.json,\n            params=self.params,\n            auth=self.auth,\n            cookies=self.cookies,\n            hooks=self.hooks,\n        )\n        return p\n\n\nclass PreparedRequest(RequestEncodingMixin, RequestHooksMixin):\n    \"\"\"The fully mutable :class:`PreparedRequest <PreparedRequest>` object,\n    containing the exact bytes that will be sent to the server.\n\n    Instances are generated from a :class:`Request <Request>` object, and\n    should not be instantiated manually; doing so may produce undesirable\n    effects.\n\n    Usage::\n\n      >>> import requests\n      >>> req = requests.Request('GET', 'https://httpbin.org/get')\n      >>> r = req.prepare()\n      >>> r\n      <PreparedRequest [GET]>\n\n      >>> s = requests.Session()\n      >>> s.send(r)\n      <Response [200]>\n    \"\"\"\n\n    def __init__(self):\n        #: HTTP verb to send to the server.\n        self.method = None\n        #: HTTP URL to send the request to.\n        self.url = None\n        #: dictionary of HTTP headers.\n        self.headers = None\n        # The `CookieJar` used to create the Cookie header will be stored here\n        # after prepare_cookies is called\n        self._cookies = None\n        #: request body to send to the server.\n        self.body = None\n        #: dictionary of callback hooks, for internal usage.\n        self.hooks = default_hooks()\n        #: integer denoting starting position of a readable file-like body.\n        self._body_position = None\n\n    def prepare(self,\n            method=None, url=None, headers=None, files=None, data=None,\n            params=None, auth=None, cookies=None, hooks=None, json=None):\n        \"\"\"Prepares the entire request with the given parameters.\"\"\"\n\n        self.prepare_method(method)\n        self.prepare_url(url, params)\n        self.prepare_headers(headers)\n        self.prepare_cookies(cookies)\n        self.prepare_body(data, files, json)\n        self.prepare_auth(auth, url)\n\n        # Note that prepare_auth must be last to enable authentication schemes\n        # such as OAuth to work on a fully prepared request.\n\n        # This MUST go after prepare_auth. Authenticators could add a hook\n        self.prepare_hooks(hooks)\n\n    def __repr__(self):\n        return '<PreparedRequest [%s]>' % (self.method)\n\n    def copy(self):\n        p = PreparedRequest()\n        p.method = self.method\n        p.url = self.url\n        p.headers = self.headers.copy() if self.headers is not None else None\n        p._cookies = _copy_cookie_jar(self._cookies)\n        p.body = self.body\n        p.hooks = self.hooks\n        p._body_position = self._body_position\n        return p\n\n    def prepare_method(self, method):\n        \"\"\"Prepares the given HTTP method.\"\"\"\n        self.method = method\n        if self.method is not None:\n            self.method = to_native_string(self.method.upper())\n\n    @staticmethod\n    def _get_idna_encoded_host(host):\n        import idna\n\n        try:\n            host = idna.encode(host, uts46=True).decode('utf-8')\n        except idna.IDNAError:\n            raise UnicodeError\n        return host\n\n    def prepare_url(self, url, params):\n        \"\"\"Prepares the given HTTP URL.\"\"\"\n        #: Accept objects that have string representations.\n        #: We're unable to blindly call unicode/str functions\n        #: as this will include the bytestring indicator (b'')\n        #: on python 3.x.\n        #: https://github.com/psf/requests/pull/2238\n        if isinstance(url, bytes):\n            url = url.decode('utf8')\n        else:\n            url = unicode(url) if is_py2 else str(url)\n\n        # Remove leading whitespaces from url\n        url = url.lstrip()\n\n        # Don't do any URL preparation for non-HTTP schemes like `mailto`,\n        # `data` etc to work around exceptions from `url_parse`, which\n        # handles RFC 3986 only.\n        if ':' in url and not url.lower().startswith('http'):\n            self.url = url\n            return\n\n        # Support for unicode domain names and paths.\n        try:\n            scheme, auth, host, port, path, query, fragment = parse_url(url)\n        except LocationParseError as e:\n            raise InvalidURL(*e.args)\n\n        if not scheme:\n            error = (\"Invalid URL {0!r}: No scheme supplied. Perhaps you meant http://{0}?\")\n            error = error.format(to_native_string(url, 'utf8'))\n\n            raise MissingSchema(error)\n\n        if not host:\n            raise InvalidURL(\"Invalid URL %r: No host supplied\" % url)\n\n        # In general, we want to try IDNA encoding the hostname if the string contains\n        # non-ASCII characters. This allows users to automatically get the correct IDNA\n        # behaviour. For strings containing only ASCII characters, we need to also verify\n        # it doesn't start with a wildcard (*), before allowing the unencoded hostname.\n        if not unicode_is_ascii(host):\n            try:\n                host = self._get_idna_encoded_host(host)\n            except UnicodeError:\n                raise InvalidURL('URL has an invalid label.')\n        elif host.startswith((u'*', u'.')):\n            raise InvalidURL('URL has an invalid label.')\n\n        # Carefully reconstruct the network location\n        netloc = auth or ''\n        if netloc:\n            netloc += '@'\n        netloc += host\n        if port:\n            netloc += ':' + str(port)\n\n        # Bare domains aren't valid URLs.\n        if not path:\n            path = '/'\n\n        if is_py2:\n            if isinstance(scheme, str):\n                scheme = scheme.encode('utf-8')\n            if isinstance(netloc, str):\n                netloc = netloc.encode('utf-8')\n            if isinstance(path, str):\n                path = path.encode('utf-8')\n            if isinstance(query, str):\n                query = query.encode('utf-8')\n            if isinstance(fragment, str):\n                fragment = fragment.encode('utf-8')\n\n        if isinstance(params, (str, bytes)):\n            params = to_native_string(params)\n\n        enc_params = self._encode_params(params)\n        if enc_params:\n            if query:\n                query = '%s&%s' % (query, enc_params)\n            else:\n                query = enc_params\n\n        url = requote_uri(urlunparse([scheme, netloc, path, None, query, fragment]))\n        self.url = url\n\n    def prepare_headers(self, headers):\n        \"\"\"Prepares the given HTTP headers.\"\"\"\n\n        self.headers = CaseInsensitiveDict()\n        if headers:\n            for header in headers.items():\n                # Raise exception on invalid header value.\n                check_header_validity(header)\n                name, value = header\n                self.headers[to_native_string(name)] = value\n\n    def prepare_body(self, data, files, json=None):\n        \"\"\"Prepares the given HTTP body data.\"\"\"\n\n        # Check if file, fo, generator, iterator.\n        # If not, run through normal process.\n\n        # Nottin' on you.\n        body = None\n        content_type = None\n\n        if not data and json is not None:\n            # urllib3 requires a bytes-like body. Python 2's json.dumps\n            # provides this natively, but Python 3 gives a Unicode string.\n            content_type = 'application/json'\n\n            try:\n                body = complexjson.dumps(json, allow_nan=False)\n            except ValueError as ve:\n                raise InvalidJSONError(ve, request=self)\n\n            if not isinstance(body, bytes):\n                body = body.encode('utf-8')\n\n        is_stream = all([\n            hasattr(data, '__iter__'),\n            not isinstance(data, (basestring, list, tuple, Mapping))\n        ])\n\n        if is_stream:\n            try:\n                length = super_len(data)\n            except (TypeError, AttributeError, UnsupportedOperation):\n                length = None\n\n            body = data\n\n            if getattr(body, 'tell', None) is not None:\n                # Record the current file position before reading.\n                # This will allow us to rewind a file in the event\n                # of a redirect.\n                try:\n                    self._body_position = body.tell()\n                except (IOError, OSError):\n                    # This differentiates from None, allowing us to catch\n                    # a failed `tell()` later when trying to rewind the body\n                    self._body_position = object()\n\n            if files:\n                raise NotImplementedError('Streamed bodies and files are mutually exclusive.')\n\n            if length:\n                self.headers['Content-Length'] = builtin_str(length)\n            else:\n                self.headers['Transfer-Encoding'] = 'chunked'\n        else:\n            # Multi-part file uploads.\n            if files:\n                (body, content_type) = self._encode_files(files, data)\n            else:\n                if data:\n                    body = self._encode_params(data)\n                    if isinstance(data, basestring) or hasattr(data, 'read'):\n                        content_type = None\n                    else:\n                        content_type = 'application/x-www-form-urlencoded'\n\n            self.prepare_content_length(body)\n\n            # Add content-type if it wasn't explicitly provided.\n            if content_type and ('content-type' not in self.headers):\n                self.headers['Content-Type'] = content_type\n\n        self.body = body\n\n    def prepare_content_length(self, body):\n        \"\"\"Prepare Content-Length header based on request method and body\"\"\"\n        if body is not None:\n            length = super_len(body)\n            if length:\n                # If length exists, set it. Otherwise, we fallback\n                # to Transfer-Encoding: chunked.\n                self.headers['Content-Length'] = builtin_str(length)\n        elif self.method not in ('GET', 'HEAD') and self.headers.get('Content-Length') is None:\n            # Set Content-Length to 0 for methods that can have a body\n            # but don't provide one. (i.e. not GET or HEAD)\n            self.headers['Content-Length'] = '0'\n\n    def prepare_auth(self, auth, url=''):\n        \"\"\"Prepares the given HTTP auth data.\"\"\"\n\n        # If no Auth is explicitly provided, extract it from the URL first.\n        if auth is None:\n            url_auth = get_auth_from_url(self.url)\n            auth = url_auth if any(url_auth) else None\n\n        if auth:\n            if isinstance(auth, tuple) and len(auth) == 2:\n                # special-case basic HTTP auth\n                auth = HTTPBasicAuth(*auth)\n\n            # Allow auth to make its changes.\n            r = auth(self)\n\n            # Update self to reflect the auth changes.\n            self.__dict__.update(r.__dict__)\n\n            # Recompute Content-Length\n            self.prepare_content_length(self.body)\n\n    def prepare_cookies(self, cookies):\n        \"\"\"Prepares the given HTTP cookie data.\n\n        This function eventually generates a ``Cookie`` header from the\n        given cookies using cookielib. Due to cookielib's design, the header\n        will not be regenerated if it already exists, meaning this function\n        can only be called once for the life of the\n        :class:`PreparedRequest <PreparedRequest>` object. Any subsequent calls\n        to ``prepare_cookies`` will have no actual effect, unless the \"Cookie\"\n        header is removed beforehand.\n        \"\"\"\n        if isinstance(cookies, cookielib.CookieJar):\n            self._cookies = cookies\n        else:\n            self._cookies = cookiejar_from_dict(cookies)\n\n        cookie_header = get_cookie_header(self._cookies, self)\n        if cookie_header is not None:\n            self.headers['Cookie'] = cookie_header\n\n    def prepare_hooks(self, hooks):\n        \"\"\"Prepares the given hooks.\"\"\"\n        # hooks can be passed as None to the prepare method and to this\n        # method. To prevent iterating over None, simply use an empty list\n        # if hooks is False-y\n        hooks = hooks or []\n        for event in hooks:\n            self.register_hook(event, hooks[event])\n\n\nclass Response(object):\n    \"\"\"The :class:`Response <Response>` object, which contains a\n    server's response to an HTTP request.\n    \"\"\"\n\n    __attrs__ = [\n        '_content', 'status_code', 'headers', 'url', 'history',\n        'encoding', 'reason', 'cookies', 'elapsed', 'request'\n    ]\n\n    def __init__(self):\n        self._content = False\n        self._content_consumed = False\n        self._next = None\n\n        #: Integer Code of responded HTTP Status, e.g. 404 or 200.\n        self.status_code = None\n\n        #: Case-insensitive Dictionary of Response Headers.\n        #: For example, ``headers['content-encoding']`` will return the\n        #: value of a ``'Content-Encoding'`` response header.\n        self.headers = CaseInsensitiveDict()\n\n        #: File-like object representation of response (for advanced usage).\n        #: Use of ``raw`` requires that ``stream=True`` be set on the request.\n        #: This requirement does not apply for use internally to Requests.\n        self.raw = None\n\n        #: Final URL location of Response.\n        self.url = None\n\n        #: Encoding to decode with when accessing r.text.\n        self.encoding = None\n\n        #: A list of :class:`Response <Response>` objects from\n        #: the history of the Request. Any redirect responses will end\n        #: up here. The list is sorted from the oldest to the most recent request.\n        self.history = []\n\n        #: Textual reason of responded HTTP Status, e.g. \"Not Found\" or \"OK\".\n        self.reason = None\n\n        #: A CookieJar of Cookies the server sent back.\n        self.cookies = cookiejar_from_dict({})\n\n        #: The amount of time elapsed between sending the request\n        #: and the arrival of the response (as a timedelta).\n        #: This property specifically measures the time taken between sending\n        #: the first byte of the request and finishing parsing the headers. It\n        #: is therefore unaffected by consuming the response content or the\n        #: value of the ``stream`` keyword argument.\n        self.elapsed = datetime.timedelta(0)\n\n        #: The :class:`PreparedRequest <PreparedRequest>` object to which this\n        #: is a response.\n        self.request = None\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, *args):\n        self.close()\n\n    def __getstate__(self):\n        # Consume everything; accessing the content attribute makes\n        # sure the content has been fully read.\n        if not self._content_consumed:\n            self.content\n\n        return {attr: getattr(self, attr, None) for attr in self.__attrs__}\n\n    def __setstate__(self, state):\n        for name, value in state.items():\n            setattr(self, name, value)\n\n        # pickled objects do not have .raw\n        setattr(self, '_content_consumed', True)\n        setattr(self, 'raw', None)\n\n    def __repr__(self):\n        return '<Response [%s]>' % (self.status_code)\n\n    def __bool__(self):\n        \"\"\"Returns True if :attr:`status_code` is less than 400.\n\n        This attribute checks if the status code of the response is between\n        400 and 600 to see if there was a client error or a server error. If\n        the status code, is between 200 and 400, this will return True. This\n        is **not** a check to see if the response code is ``200 OK``.\n        \"\"\"\n        return self.ok\n\n    def __nonzero__(self):\n        \"\"\"Returns True if :attr:`status_code` is less than 400.\n\n        This attribute checks if the status code of the response is between\n        400 and 600 to see if there was a client error or a server error. If\n        the status code, is between 200 and 400, this will return True. This\n        is **not** a check to see if the response code is ``200 OK``.\n        \"\"\"\n        return self.ok\n\n    def __iter__(self):\n        \"\"\"Allows you to use a response as an iterator.\"\"\"\n        return self.iter_content(128)\n\n    @property\n    def ok(self):\n        \"\"\"Returns True if :attr:`status_code` is less than 400, False if not.\n\n        This attribute checks if the status code of the response is between\n        400 and 600 to see if there was a client error or a server error. If\n        the status code is between 200 and 400, this will return True. This\n        is **not** a check to see if the response code is ``200 OK``.\n        \"\"\"\n        try:\n            self.raise_for_status()\n        except HTTPError:\n            return False\n        return True\n\n    @property\n    def is_redirect(self):\n        \"\"\"True if this Response is a well-formed HTTP redirect that could have\n        been processed automatically (by :meth:`Session.resolve_redirects`).\n        \"\"\"\n        return ('location' in self.headers and self.status_code in REDIRECT_STATI)\n\n    @property\n    def is_permanent_redirect(self):\n        \"\"\"True if this Response one of the permanent versions of redirect.\"\"\"\n        return ('location' in self.headers and self.status_code in (codes.moved_permanently, codes.permanent_redirect))\n\n    @property\n    def next(self):\n        \"\"\"Returns a PreparedRequest for the next request in a redirect chain, if there is one.\"\"\"\n        return self._next\n\n    @property\n    def apparent_encoding(self):\n        \"\"\"The apparent encoding, provided by the charset_normalizer or chardet libraries.\"\"\"\n        return chardet.detect(self.content)['encoding']\n\n    def iter_content(self, chunk_size=1, decode_unicode=False):\n        \"\"\"Iterates over the response data.  When stream=True is set on the\n        request, this avoids reading the content at once into memory for\n        large responses.  The chunk size is the number of bytes it should\n        read into memory.  This is not necessarily the length of each item\n        returned as decoding can take place.\n\n        chunk_size must be of type int or None. A value of None will\n        function differently depending on the value of `stream`.\n        stream=True will read data as it arrives in whatever size the\n        chunks are received. If stream=False, data is returned as\n        a single chunk.\n\n        If decode_unicode is True, content will be decoded using the best\n        available encoding based on the response.\n        \"\"\"\n\n        def generate():\n            # Special case for urllib3.\n            if hasattr(self.raw, 'stream'):\n                try:\n                    for chunk in self.raw.stream(chunk_size, decode_content=True):\n                        yield chunk\n                except ProtocolError as e:\n                    raise ChunkedEncodingError(e)\n                except DecodeError as e:\n                    raise ContentDecodingError(e)\n                except ReadTimeoutError as e:\n                    raise ConnectionError(e)\n            else:\n                # Standard file-like object.\n                while True:\n                    chunk = self.raw.read(chunk_size)\n                    if not chunk:\n                        break\n                    yield chunk\n\n            self._content_consumed = True\n\n        if self._content_consumed and isinstance(self._content, bool):\n            raise StreamConsumedError()\n        elif chunk_size is not None and not isinstance(chunk_size, int):\n            raise TypeError(\"chunk_size must be an int, it is instead a %s.\" % type(chunk_size))\n        # simulate reading small chunks of the content\n        reused_chunks = iter_slices(self._content, chunk_size)\n\n        stream_chunks = generate()\n\n        chunks = reused_chunks if self._content_consumed else stream_chunks\n\n        if decode_unicode:\n            chunks = stream_decode_response_unicode(chunks, self)\n\n        return chunks\n\n    def iter_lines(self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=False, delimiter=None):\n        \"\"\"Iterates over the response data, one line at a time.  When\n        stream=True is set on the request, this avoids reading the\n        content at once into memory for large responses.\n\n        .. note:: This method is not reentrant safe.\n        \"\"\"\n\n        pending = None\n\n        for chunk in self.iter_content(chunk_size=chunk_size, decode_unicode=decode_unicode):\n\n            if pending is not None:\n                chunk = pending + chunk\n\n            if delimiter:\n                lines = chunk.split(delimiter)\n            else:\n                lines = chunk.splitlines()\n\n            if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]:\n                pending = lines.pop()\n            else:\n                pending = None\n\n            for line in lines:\n                yield line\n\n        if pending is not None:\n            yield pending\n\n    @property\n    def content(self):\n        \"\"\"Content of the response, in bytes.\"\"\"\n\n        if self._content is False:\n            # Read the contents.\n            if self._content_consumed:\n                raise RuntimeError(\n                    'The content for this response was already consumed')\n\n            if self.status_code == 0 or self.raw is None:\n                self._content = None\n            else:\n                self._content = b''.join(self.iter_content(CONTENT_CHUNK_SIZE)) or b''\n\n        self._content_consumed = True\n        # don't need to release the connection; that's been handled by urllib3\n        # since we exhausted the data.\n        return self._content\n\n    @property\n    def text(self):\n        \"\"\"Content of the response, in unicode.\n\n        If Response.encoding is None, encoding will be guessed using\n        ``charset_normalizer`` or ``chardet``.\n\n        The encoding of the response content is determined based solely on HTTP\n        headers, following RFC 2616 to the letter. If you can take advantage of\n        non-HTTP knowledge to make a better guess at the encoding, you should\n        set ``r.encoding`` appropriately before accessing this property.\n        \"\"\"\n\n        # Try charset from content-type\n        content = None\n        encoding = self.encoding\n\n        if not self.content:\n            return str('')\n\n        # Fallback to auto-detected encoding.\n        if self.encoding is None:\n            encoding = self.apparent_encoding\n\n        # Decode unicode from given encoding.\n        try:\n            content = str(self.content, encoding, errors='replace')\n        except (LookupError, TypeError):\n            # A LookupError is raised if the encoding was not found which could\n            # indicate a misspelling or similar mistake.\n            #\n            # A TypeError can be raised if encoding is None\n            #\n            # So we try blindly encoding.\n            content = str(self.content, errors='replace')\n\n        return content\n\n    def json(self, **kwargs):\n        r\"\"\"Returns the json-encoded content of a response, if any.\n\n        :param \\*\\*kwargs: Optional arguments that ``json.loads`` takes.\n        :raises requests.exceptions.JSONDecodeError: If the response body does not\n            contain valid json.\n        \"\"\"\n\n        if not self.encoding and self.content and len(self.content) > 3:\n            # No encoding set. JSON RFC 4627 section 3 states we should expect\n            # UTF-8, -16 or -32. Detect which one to use; If the detection or\n            # decoding fails, fall back to `self.text` (using charset_normalizer to make\n            # a best guess).\n            encoding = guess_json_utf(self.content)\n            if encoding is not None:\n                try:\n                    return complexjson.loads(\n                        self.content.decode(encoding), **kwargs\n                    )\n                except UnicodeDecodeError:\n                    # Wrong UTF codec detected; usually because it's not UTF-8\n                    # but some other 8-bit codec.  This is an RFC violation,\n                    # and the server didn't bother to tell us what codec *was*\n                    # used.\n                    pass\n\n        try:\n            return complexjson.loads(self.text, **kwargs)\n        except JSONDecodeError as e:\n            # Catch JSON-related errors and raise as requests.JSONDecodeError\n            # This aliases json.JSONDecodeError and simplejson.JSONDecodeError\n            if is_py2: # e is a ValueError\n                raise RequestsJSONDecodeError(e.message)\n            else:\n                raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)\n\n    @property\n    def links(self):\n        \"\"\"Returns the parsed header links of the response, if any.\"\"\"\n\n        header = self.headers.get('link')\n\n        # l = MultiDict()\n        l = {}\n\n        if header:\n            links = parse_header_links(header)\n\n            for link in links:\n                key = link.get('rel') or link.get('url')\n                l[key] = link\n\n        return l\n\n    def raise_for_status(self):\n        \"\"\"Raises :class:`HTTPError`, if one occurred.\"\"\"\n\n        http_error_msg = ''\n        if isinstance(self.reason, bytes):\n            # We attempt to decode utf-8 first because some servers\n            # choose to localize their reason strings. If the string\n            # isn't utf-8, we fall back to iso-8859-1 for all other\n            # encodings. (See PR #3538)\n            try:\n                reason = self.reason.decode('utf-8')\n            except UnicodeDecodeError:\n                reason = self.reason.decode('iso-8859-1')\n        else:\n            reason = self.reason\n\n        if 400 <= self.status_code < 500:\n            http_error_msg = u'%s Client Error: %s for url: %s' % (self.status_code, reason, self.url)\n\n        elif 500 <= self.status_code < 600:\n            http_error_msg = u'%s Server Error: %s for url: %s' % (self.status_code, reason, self.url)\n\n        if http_error_msg:\n            raise HTTPError(http_error_msg, response=self)\n\n    def close(self):\n        \"\"\"Releases the connection back to the pool. Once this method has been\n        called the underlying ``raw`` object must not be accessed again.\n\n        *Note: Should not normally need to be called explicitly.*\n        \"\"\"\n        if not self._content_consumed:\n            self.raw.close()\n\n        release_conn = getattr(self.raw, 'release_conn', None)\n        if release_conn is not None:\n            release_conn()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/packages.py",
    "content": "import sys\n\ntry:\n    import chardet\nexcept ImportError:\n    import charset_normalizer as chardet\n    import warnings\n\n    warnings.filterwarnings('ignore', 'Trying to detect', module='charset_normalizer')\n\n# This code exists for backwards compatibility reasons.\n# I don't like it either. Just look the other way. :)\n\nfor package in ('urllib3', 'idna'):\n    locals()[package] = __import__(package)\n    # This traversal is apparently necessary such that the identities are\n    # preserved (requests.packages.urllib3.* is urllib3.*)\n    for mod in list(sys.modules):\n        if mod == package or mod.startswith(package + '.'):\n            sys.modules['requests.packages.' + mod] = sys.modules[mod]\n\ntarget = chardet.__name__\nfor mod in list(sys.modules):\n    if mod == target or mod.startswith(target + '.'):\n        sys.modules['requests.packages.' + target.replace(target, 'chardet')] = sys.modules[mod]\n# Kinda cool, though, right?\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/sessions.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests.sessions\n~~~~~~~~~~~~~~~~~\n\nThis module provides a Session object to manage and persist settings across\nrequests (cookies, auth, proxies).\n\"\"\"\nimport os\nimport sys\nimport time\nfrom datetime import timedelta\nfrom collections import OrderedDict\n\nfrom .auth import _basic_auth_str\nfrom .compat import cookielib, is_py3, urljoin, urlparse, Mapping\nfrom .cookies import (\n    cookiejar_from_dict, extract_cookies_to_jar, RequestsCookieJar, merge_cookies)\nfrom .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT\nfrom .hooks import default_hooks, dispatch_hook\nfrom ._internal_utils import to_native_string\nfrom .utils import to_key_val_list, default_headers, DEFAULT_PORTS\nfrom .exceptions import (\n    TooManyRedirects, InvalidSchema, ChunkedEncodingError, ContentDecodingError)\n\nfrom .structures import CaseInsensitiveDict\nfrom .adapters import HTTPAdapter\n\nfrom .utils import (\n    requote_uri, get_environ_proxies, get_netrc_auth, should_bypass_proxies,\n    get_auth_from_url, rewind_body, resolve_proxies\n)\n\nfrom .status_codes import codes\n\n# formerly defined here, reexposed here for backward compatibility\nfrom .models import REDIRECT_STATI\n\n# Preferred clock, based on which one is more accurate on a given system.\nif sys.platform == 'win32':\n    try:  # Python 3.4+\n        preferred_clock = time.perf_counter\n    except AttributeError:  # Earlier than Python 3.\n        preferred_clock = time.clock\nelse:\n    preferred_clock = time.time\n\n\ndef merge_setting(request_setting, session_setting, dict_class=OrderedDict):\n    \"\"\"Determines appropriate setting for a given request, taking into account\n    the explicit setting on that request, and the setting in the session. If a\n    setting is a dictionary, they will be merged together using `dict_class`\n    \"\"\"\n\n    if session_setting is None:\n        return request_setting\n\n    if request_setting is None:\n        return session_setting\n\n    # Bypass if not a dictionary (e.g. verify)\n    if not (\n            isinstance(session_setting, Mapping) and\n            isinstance(request_setting, Mapping)\n    ):\n        return request_setting\n\n    merged_setting = dict_class(to_key_val_list(session_setting))\n    merged_setting.update(to_key_val_list(request_setting))\n\n    # Remove keys that are set to None. Extract keys first to avoid altering\n    # the dictionary during iteration.\n    none_keys = [k for (k, v) in merged_setting.items() if v is None]\n    for key in none_keys:\n        del merged_setting[key]\n\n    return merged_setting\n\n\ndef merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict):\n    \"\"\"Properly merges both requests and session hooks.\n\n    This is necessary because when request_hooks == {'response': []}, the\n    merge breaks Session hooks entirely.\n    \"\"\"\n    if session_hooks is None or session_hooks.get('response') == []:\n        return request_hooks\n\n    if request_hooks is None or request_hooks.get('response') == []:\n        return session_hooks\n\n    return merge_setting(request_hooks, session_hooks, dict_class)\n\n\nclass SessionRedirectMixin(object):\n\n    def get_redirect_target(self, resp):\n        \"\"\"Receives a Response. Returns a redirect URI or ``None``\"\"\"\n        # Due to the nature of how requests processes redirects this method will\n        # be called at least once upon the original response and at least twice\n        # on each subsequent redirect response (if any).\n        # If a custom mixin is used to handle this logic, it may be advantageous\n        # to cache the redirect location onto the response object as a private\n        # attribute.\n        if resp.is_redirect:\n            location = resp.headers['location']\n            # Currently the underlying http module on py3 decode headers\n            # in latin1, but empirical evidence suggests that latin1 is very\n            # rarely used with non-ASCII characters in HTTP headers.\n            # It is more likely to get UTF8 header rather than latin1.\n            # This causes incorrect handling of UTF8 encoded location headers.\n            # To solve this, we re-encode the location in latin1.\n            if is_py3:\n                location = location.encode('latin1')\n            return to_native_string(location, 'utf8')\n        return None\n\n    def should_strip_auth(self, old_url, new_url):\n        \"\"\"Decide whether Authorization header should be removed when redirecting\"\"\"\n        old_parsed = urlparse(old_url)\n        new_parsed = urlparse(new_url)\n        if old_parsed.hostname != new_parsed.hostname:\n            return True\n        # Special case: allow http -> https redirect when using the standard\n        # ports. This isn't specified by RFC 7235, but is kept to avoid\n        # breaking backwards compatibility with older versions of requests\n        # that allowed any redirects on the same host.\n        if (old_parsed.scheme == 'http' and old_parsed.port in (80, None)\n                and new_parsed.scheme == 'https' and new_parsed.port in (443, None)):\n            return False\n\n        # Handle default port usage corresponding to scheme.\n        changed_port = old_parsed.port != new_parsed.port\n        changed_scheme = old_parsed.scheme != new_parsed.scheme\n        default_port = (DEFAULT_PORTS.get(old_parsed.scheme, None), None)\n        if (not changed_scheme and old_parsed.port in default_port\n                and new_parsed.port in default_port):\n            return False\n\n        # Standard case: root URI must match\n        return changed_port or changed_scheme\n\n    def resolve_redirects(self, resp, req, stream=False, timeout=None,\n                          verify=True, cert=None, proxies=None, yield_requests=False, **adapter_kwargs):\n        \"\"\"Receives a Response. Returns a generator of Responses or Requests.\"\"\"\n\n        hist = []  # keep track of history\n\n        url = self.get_redirect_target(resp)\n        previous_fragment = urlparse(req.url).fragment\n        while url:\n            prepared_request = req.copy()\n\n            # Update history and keep track of redirects.\n            # resp.history must ignore the original request in this loop\n            hist.append(resp)\n            resp.history = hist[1:]\n\n            try:\n                resp.content  # Consume socket so it can be released\n            except (ChunkedEncodingError, ContentDecodingError, RuntimeError):\n                resp.raw.read(decode_content=False)\n\n            if len(resp.history) >= self.max_redirects:\n                raise TooManyRedirects('Exceeded {} redirects.'.format(self.max_redirects), response=resp)\n\n            # Release the connection back into the pool.\n            resp.close()\n\n            # Handle redirection without scheme (see: RFC 1808 Section 4)\n            if url.startswith('//'):\n                parsed_rurl = urlparse(resp.url)\n                url = ':'.join([to_native_string(parsed_rurl.scheme), url])\n\n            # Normalize url case and attach previous fragment if needed (RFC 7231 7.1.2)\n            parsed = urlparse(url)\n            if parsed.fragment == '' and previous_fragment:\n                parsed = parsed._replace(fragment=previous_fragment)\n            elif parsed.fragment:\n                previous_fragment = parsed.fragment\n            url = parsed.geturl()\n\n            # Facilitate relative 'location' headers, as allowed by RFC 7231.\n            # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')\n            # Compliant with RFC3986, we percent encode the url.\n            if not parsed.netloc:\n                url = urljoin(resp.url, requote_uri(url))\n            else:\n                url = requote_uri(url)\n\n            prepared_request.url = to_native_string(url)\n\n            self.rebuild_method(prepared_request, resp)\n\n            # https://github.com/psf/requests/issues/1084\n            if resp.status_code not in (codes.temporary_redirect, codes.permanent_redirect):\n                # https://github.com/psf/requests/issues/3490\n                purged_headers = ('Content-Length', 'Content-Type', 'Transfer-Encoding')\n                for header in purged_headers:\n                    prepared_request.headers.pop(header, None)\n                prepared_request.body = None\n\n            headers = prepared_request.headers\n            headers.pop('Cookie', None)\n\n            # Extract any cookies sent on the response to the cookiejar\n            # in the new request. Because we've mutated our copied prepared\n            # request, use the old one that we haven't yet touched.\n            extract_cookies_to_jar(prepared_request._cookies, req, resp.raw)\n            merge_cookies(prepared_request._cookies, self.cookies)\n            prepared_request.prepare_cookies(prepared_request._cookies)\n\n            # Rebuild auth and proxy information.\n            proxies = self.rebuild_proxies(prepared_request, proxies)\n            self.rebuild_auth(prepared_request, resp)\n\n            # A failed tell() sets `_body_position` to `object()`. This non-None\n            # value ensures `rewindable` will be True, allowing us to raise an\n            # UnrewindableBodyError, instead of hanging the connection.\n            rewindable = (\n                prepared_request._body_position is not None and\n                ('Content-Length' in headers or 'Transfer-Encoding' in headers)\n            )\n\n            # Attempt to rewind consumed file-like object.\n            if rewindable:\n                rewind_body(prepared_request)\n\n            # Override the original request.\n            req = prepared_request\n\n            if yield_requests:\n                yield req\n            else:\n\n                resp = self.send(\n                    req,\n                    stream=stream,\n                    timeout=timeout,\n                    verify=verify,\n                    cert=cert,\n                    proxies=proxies,\n                    allow_redirects=False,\n                    **adapter_kwargs\n                )\n\n                extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)\n\n                # extract redirect url, if any, for the next loop\n                url = self.get_redirect_target(resp)\n                yield resp\n\n    def rebuild_auth(self, prepared_request, response):\n        \"\"\"When being redirected we may want to strip authentication from the\n        request to avoid leaking credentials. This method intelligently removes\n        and reapplies authentication where possible to avoid credential loss.\n        \"\"\"\n        headers = prepared_request.headers\n        url = prepared_request.url\n\n        if 'Authorization' in headers and self.should_strip_auth(response.request.url, url):\n            # If we get redirected to a new host, we should strip out any\n            # authentication headers.\n            del headers['Authorization']\n\n        # .netrc might have more auth for us on our new host.\n        new_auth = get_netrc_auth(url) if self.trust_env else None\n        if new_auth is not None:\n            prepared_request.prepare_auth(new_auth)\n\n    def rebuild_proxies(self, prepared_request, proxies):\n        \"\"\"This method re-evaluates the proxy configuration by considering the\n        environment variables. If we are redirected to a URL covered by\n        NO_PROXY, we strip the proxy configuration. Otherwise, we set missing\n        proxy keys for this URL (in case they were stripped by a previous\n        redirect).\n\n        This method also replaces the Proxy-Authorization header where\n        necessary.\n\n        :rtype: dict\n        \"\"\"\n        headers = prepared_request.headers\n        scheme = urlparse(prepared_request.url).scheme\n        new_proxies = resolve_proxies(prepared_request, proxies, self.trust_env)\n\n        if 'Proxy-Authorization' in headers:\n            del headers['Proxy-Authorization']\n\n        try:\n            username, password = get_auth_from_url(new_proxies[scheme])\n        except KeyError:\n            username, password = None, None\n\n        if username and password:\n            headers['Proxy-Authorization'] = _basic_auth_str(username, password)\n\n        return new_proxies\n\n    def rebuild_method(self, prepared_request, response):\n        \"\"\"When being redirected we may want to change the method of the request\n        based on certain specs or browser behavior.\n        \"\"\"\n        method = prepared_request.method\n\n        # https://tools.ietf.org/html/rfc7231#section-6.4.4\n        if response.status_code == codes.see_other and method != 'HEAD':\n            method = 'GET'\n\n        # Do what the browsers do, despite standards...\n        # First, turn 302s into GETs.\n        if response.status_code == codes.found and method != 'HEAD':\n            method = 'GET'\n\n        # Second, if a POST is responded to with a 301, turn it into a GET.\n        # This bizarre behaviour is explained in Issue 1704.\n        if response.status_code == codes.moved and method == 'POST':\n            method = 'GET'\n\n        prepared_request.method = method\n\n\nclass Session(SessionRedirectMixin):\n    \"\"\"A Requests session.\n\n    Provides cookie persistence, connection-pooling, and configuration.\n\n    Basic Usage::\n\n      >>> import requests\n      >>> s = requests.Session()\n      >>> s.get('https://httpbin.org/get')\n      <Response [200]>\n\n    Or as a context manager::\n\n      >>> with requests.Session() as s:\n      ...     s.get('https://httpbin.org/get')\n      <Response [200]>\n    \"\"\"\n\n    __attrs__ = [\n        'headers', 'cookies', 'auth', 'proxies', 'hooks', 'params', 'verify',\n        'cert', 'adapters', 'stream', 'trust_env',\n        'max_redirects',\n    ]\n\n    def __init__(self):\n\n        #: A case-insensitive dictionary of headers to be sent on each\n        #: :class:`Request <Request>` sent from this\n        #: :class:`Session <Session>`.\n        self.headers = default_headers()\n\n        #: Default Authentication tuple or object to attach to\n        #: :class:`Request <Request>`.\n        self.auth = None\n\n        #: Dictionary mapping protocol or protocol and host to the URL of the proxy\n        #: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to\n        #: be used on each :class:`Request <Request>`.\n        self.proxies = {}\n\n        #: Event-handling hooks.\n        self.hooks = default_hooks()\n\n        #: Dictionary of querystring data to attach to each\n        #: :class:`Request <Request>`. The dictionary values may be lists for\n        #: representing multivalued query parameters.\n        self.params = {}\n\n        #: Stream response content default.\n        self.stream = False\n\n        #: SSL Verification default.\n        #: Defaults to `True`, requiring requests to verify the TLS certificate at the\n        #: remote end.\n        #: If verify is set to `False`, requests will accept any TLS certificate\n        #: presented by the server, and will ignore hostname mismatches and/or\n        #: expired certificates, which will make your application vulnerable to\n        #: man-in-the-middle (MitM) attacks.\n        #: Only set this to `False` for testing.\n        self.verify = True\n\n        #: SSL client certificate default, if String, path to ssl client\n        #: cert file (.pem). If Tuple, ('cert', 'key') pair.\n        self.cert = None\n\n        #: Maximum number of redirects allowed. If the request exceeds this\n        #: limit, a :class:`TooManyRedirects` exception is raised.\n        #: This defaults to requests.models.DEFAULT_REDIRECT_LIMIT, which is\n        #: 30.\n        self.max_redirects = DEFAULT_REDIRECT_LIMIT\n\n        #: Trust environment settings for proxy configuration, default\n        #: authentication and similar.\n        self.trust_env = True\n\n        #: A CookieJar containing all currently outstanding cookies set on this\n        #: session. By default it is a\n        #: :class:`RequestsCookieJar <requests.cookies.RequestsCookieJar>`, but\n        #: may be any other ``cookielib.CookieJar`` compatible object.\n        self.cookies = cookiejar_from_dict({})\n\n        # Default connection adapters.\n        self.adapters = OrderedDict()\n        self.mount('https://', HTTPAdapter())\n        self.mount('http://', HTTPAdapter())\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, *args):\n        self.close()\n\n    def prepare_request(self, request):\n        \"\"\"Constructs a :class:`PreparedRequest <PreparedRequest>` for\n        transmission and returns it. The :class:`PreparedRequest` has settings\n        merged from the :class:`Request <Request>` instance and those of the\n        :class:`Session`.\n\n        :param request: :class:`Request` instance to prepare with this\n            session's settings.\n        :rtype: requests.PreparedRequest\n        \"\"\"\n        cookies = request.cookies or {}\n\n        # Bootstrap CookieJar.\n        if not isinstance(cookies, cookielib.CookieJar):\n            cookies = cookiejar_from_dict(cookies)\n\n        # Merge with session cookies\n        merged_cookies = merge_cookies(\n            merge_cookies(RequestsCookieJar(), self.cookies), cookies)\n\n        # Set environment's basic authentication if not explicitly set.\n        auth = request.auth\n        if self.trust_env and not auth and not self.auth:\n            auth = get_netrc_auth(request.url)\n\n        p = PreparedRequest()\n        p.prepare(\n            method=request.method.upper(),\n            url=request.url,\n            files=request.files,\n            data=request.data,\n            json=request.json,\n            headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict),\n            params=merge_setting(request.params, self.params),\n            auth=merge_setting(auth, self.auth),\n            cookies=merged_cookies,\n            hooks=merge_hooks(request.hooks, self.hooks),\n        )\n        return p\n\n    def request(self, method, url,\n            params=None, data=None, headers=None, cookies=None, files=None,\n            auth=None, timeout=None, allow_redirects=True, proxies=None,\n            hooks=None, stream=None, verify=None, cert=None, json=None):\n        \"\"\"Constructs a :class:`Request <Request>`, prepares it and sends it.\n        Returns :class:`Response <Response>` object.\n\n        :param method: method for the new :class:`Request` object.\n        :param url: URL for the new :class:`Request` object.\n        :param params: (optional) Dictionary or bytes to be sent in the query\n            string for the :class:`Request`.\n        :param data: (optional) Dictionary, list of tuples, bytes, or file-like\n            object to send in the body of the :class:`Request`.\n        :param json: (optional) json to send in the body of the\n            :class:`Request`.\n        :param headers: (optional) Dictionary of HTTP Headers to send with the\n            :class:`Request`.\n        :param cookies: (optional) Dict or CookieJar object to send with the\n            :class:`Request`.\n        :param files: (optional) Dictionary of ``'filename': file-like-objects``\n            for multipart encoding upload.\n        :param auth: (optional) Auth tuple or callable to enable\n            Basic/Digest/Custom HTTP Auth.\n        :param timeout: (optional) How long to wait for the server to send\n            data before giving up, as a float, or a :ref:`(connect timeout,\n            read timeout) <timeouts>` tuple.\n        :type timeout: float or tuple\n        :param allow_redirects: (optional) Set to True by default.\n        :type allow_redirects: bool\n        :param proxies: (optional) Dictionary mapping protocol or protocol and\n            hostname to the URL of the proxy.\n        :param stream: (optional) whether to immediately download the response\n            content. Defaults to ``False``.\n        :param verify: (optional) Either a boolean, in which case it controls whether we verify\n            the server's TLS certificate, or a string, in which case it must be a path\n            to a CA bundle to use. Defaults to ``True``. When set to\n            ``False``, requests will accept any TLS certificate presented by\n            the server, and will ignore hostname mismatches and/or expired\n            certificates, which will make your application vulnerable to\n            man-in-the-middle (MitM) attacks. Setting verify to ``False`` \n            may be useful during local development or testing.\n        :param cert: (optional) if String, path to ssl client cert file (.pem).\n            If Tuple, ('cert', 'key') pair.\n        :rtype: requests.Response\n        \"\"\"\n        # Create the Request.\n        req = Request(\n            method=method.upper(),\n            url=url,\n            headers=headers,\n            files=files,\n            data=data or {},\n            json=json,\n            params=params or {},\n            auth=auth,\n            cookies=cookies,\n            hooks=hooks,\n        )\n        prep = self.prepare_request(req)\n\n        proxies = proxies or {}\n\n        settings = self.merge_environment_settings(\n            prep.url, proxies, stream, verify, cert\n        )\n\n        # Send the request.\n        send_kwargs = {\n            'timeout': timeout,\n            'allow_redirects': allow_redirects,\n        }\n        send_kwargs.update(settings)\n        resp = self.send(prep, **send_kwargs)\n\n        return resp\n\n    def get(self, url, **kwargs):\n        r\"\"\"Sends a GET request. Returns :class:`Response` object.\n\n        :param url: URL for the new :class:`Request` object.\n        :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n        :rtype: requests.Response\n        \"\"\"\n\n        kwargs.setdefault('allow_redirects', True)\n        return self.request('GET', url, **kwargs)\n\n    def options(self, url, **kwargs):\n        r\"\"\"Sends a OPTIONS request. Returns :class:`Response` object.\n\n        :param url: URL for the new :class:`Request` object.\n        :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n        :rtype: requests.Response\n        \"\"\"\n\n        kwargs.setdefault('allow_redirects', True)\n        return self.request('OPTIONS', url, **kwargs)\n\n    def head(self, url, **kwargs):\n        r\"\"\"Sends a HEAD request. Returns :class:`Response` object.\n\n        :param url: URL for the new :class:`Request` object.\n        :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n        :rtype: requests.Response\n        \"\"\"\n\n        kwargs.setdefault('allow_redirects', False)\n        return self.request('HEAD', url, **kwargs)\n\n    def post(self, url, data=None, json=None, **kwargs):\n        r\"\"\"Sends a POST request. Returns :class:`Response` object.\n\n        :param url: URL for the new :class:`Request` object.\n        :param data: (optional) Dictionary, list of tuples, bytes, or file-like\n            object to send in the body of the :class:`Request`.\n        :param json: (optional) json to send in the body of the :class:`Request`.\n        :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n        :rtype: requests.Response\n        \"\"\"\n\n        return self.request('POST', url, data=data, json=json, **kwargs)\n\n    def put(self, url, data=None, **kwargs):\n        r\"\"\"Sends a PUT request. Returns :class:`Response` object.\n\n        :param url: URL for the new :class:`Request` object.\n        :param data: (optional) Dictionary, list of tuples, bytes, or file-like\n            object to send in the body of the :class:`Request`.\n        :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n        :rtype: requests.Response\n        \"\"\"\n\n        return self.request('PUT', url, data=data, **kwargs)\n\n    def patch(self, url, data=None, **kwargs):\n        r\"\"\"Sends a PATCH request. Returns :class:`Response` object.\n\n        :param url: URL for the new :class:`Request` object.\n        :param data: (optional) Dictionary, list of tuples, bytes, or file-like\n            object to send in the body of the :class:`Request`.\n        :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n        :rtype: requests.Response\n        \"\"\"\n\n        return self.request('PATCH', url, data=data, **kwargs)\n\n    def delete(self, url, **kwargs):\n        r\"\"\"Sends a DELETE request. Returns :class:`Response` object.\n\n        :param url: URL for the new :class:`Request` object.\n        :param \\*\\*kwargs: Optional arguments that ``request`` takes.\n        :rtype: requests.Response\n        \"\"\"\n\n        return self.request('DELETE', url, **kwargs)\n\n    def send(self, request, **kwargs):\n        \"\"\"Send a given PreparedRequest.\n\n        :rtype: requests.Response\n        \"\"\"\n        # Set defaults that the hooks can utilize to ensure they always have\n        # the correct parameters to reproduce the previous request.\n        kwargs.setdefault('stream', self.stream)\n        kwargs.setdefault('verify', self.verify)\n        kwargs.setdefault('cert', self.cert)\n        if 'proxies' not in kwargs:\n            kwargs['proxies'] = resolve_proxies(\n                request, self.proxies, self.trust_env\n            )\n\n        # It's possible that users might accidentally send a Request object.\n        # Guard against that specific failure case.\n        if isinstance(request, Request):\n            raise ValueError('You can only send PreparedRequests.')\n\n        # Set up variables needed for resolve_redirects and dispatching of hooks\n        allow_redirects = kwargs.pop('allow_redirects', True)\n        stream = kwargs.get('stream')\n        hooks = request.hooks\n\n        # Get the appropriate adapter to use\n        adapter = self.get_adapter(url=request.url)\n\n        # Start time (approximately) of the request\n        start = preferred_clock()\n\n        # Send the request\n        r = adapter.send(request, **kwargs)\n\n        # Total elapsed time of the request (approximately)\n        elapsed = preferred_clock() - start\n        r.elapsed = timedelta(seconds=elapsed)\n\n        # Response manipulation hooks\n        r = dispatch_hook('response', hooks, r, **kwargs)\n\n        # Persist cookies\n        if r.history:\n\n            # If the hooks create history then we want those cookies too\n            for resp in r.history:\n                extract_cookies_to_jar(self.cookies, resp.request, resp.raw)\n\n        extract_cookies_to_jar(self.cookies, request, r.raw)\n\n        # Resolve redirects if allowed.\n        if allow_redirects:\n            # Redirect resolving generator.\n            gen = self.resolve_redirects(r, request, **kwargs)\n            history = [resp for resp in gen]\n        else:\n            history = []\n\n        # Shuffle things around if there's history.\n        if history:\n            # Insert the first (original) request at the start\n            history.insert(0, r)\n            # Get the last request made\n            r = history.pop()\n            r.history = history\n\n        # If redirects aren't being followed, store the response on the Request for Response.next().\n        if not allow_redirects:\n            try:\n                r._next = next(self.resolve_redirects(r, request, yield_requests=True, **kwargs))\n            except StopIteration:\n                pass\n\n        if not stream:\n            r.content\n\n        return r\n\n    def merge_environment_settings(self, url, proxies, stream, verify, cert):\n        \"\"\"\n        Check the environment and merge it with some settings.\n\n        :rtype: dict\n        \"\"\"\n        # Gather clues from the surrounding environment.\n        if self.trust_env:\n            # Set environment's proxies.\n            no_proxy = proxies.get('no_proxy') if proxies is not None else None\n            env_proxies = get_environ_proxies(url, no_proxy=no_proxy)\n            for (k, v) in env_proxies.items():\n                proxies.setdefault(k, v)\n\n            # Look for requests environment configuration and be compatible\n            # with cURL.\n            if verify is True or verify is None:\n                verify = (os.environ.get('REQUESTS_CA_BUNDLE') or\n                          os.environ.get('CURL_CA_BUNDLE'))\n\n        # Merge all the kwargs.\n        proxies = merge_setting(proxies, self.proxies)\n        stream = merge_setting(stream, self.stream)\n        verify = merge_setting(verify, self.verify)\n        cert = merge_setting(cert, self.cert)\n\n        return {'verify': verify, 'proxies': proxies, 'stream': stream,\n                'cert': cert}\n\n    def get_adapter(self, url):\n        \"\"\"\n        Returns the appropriate connection adapter for the given URL.\n\n        :rtype: requests.adapters.BaseAdapter\n        \"\"\"\n        for (prefix, adapter) in self.adapters.items():\n\n            if url.lower().startswith(prefix.lower()):\n                return adapter\n\n        # Nothing matches :-/\n        raise InvalidSchema(\"No connection adapters were found for {!r}\".format(url))\n\n    def close(self):\n        \"\"\"Closes all adapters and as such the session\"\"\"\n        for v in self.adapters.values():\n            v.close()\n\n    def mount(self, prefix, adapter):\n        \"\"\"Registers a connection adapter to a prefix.\n\n        Adapters are sorted in descending order by prefix length.\n        \"\"\"\n        self.adapters[prefix] = adapter\n        keys_to_move = [k for k in self.adapters if len(k) < len(prefix)]\n\n        for key in keys_to_move:\n            self.adapters[key] = self.adapters.pop(key)\n\n    def __getstate__(self):\n        state = {attr: getattr(self, attr, None) for attr in self.__attrs__}\n        return state\n\n    def __setstate__(self, state):\n        for attr, value in state.items():\n            setattr(self, attr, value)\n\n\ndef session():\n    \"\"\"\n    Returns a :class:`Session` for context-management.\n\n    .. deprecated:: 1.0.0\n\n        This method has been deprecated since version 1.0.0 and is only kept for\n        backwards compatibility. New code should use :class:`~requests.sessions.Session`\n        to create a session. This may be removed at a future date.\n\n    :rtype: Session\n    \"\"\"\n    return Session()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/status_codes.py",
    "content": "# -*- coding: utf-8 -*-\n\nr\"\"\"\nThe ``codes`` object defines a mapping from common names for HTTP statuses\nto their numerical codes, accessible either as attributes or as dictionary\nitems.\n\nExample::\n\n    >>> import requests\n    >>> requests.codes['temporary_redirect']\n    307\n    >>> requests.codes.teapot\n    418\n    >>> requests.codes['\\o/']\n    200\n\nSome codes have multiple names, and both upper- and lower-case versions of\nthe names are allowed. For example, ``codes.ok``, ``codes.OK``, and\n``codes.okay`` all correspond to the HTTP status code 200.\n\"\"\"\n\nfrom .structures import LookupDict\n\n_codes = {\n\n    # Informational.\n    100: ('continue',),\n    101: ('switching_protocols',),\n    102: ('processing',),\n    103: ('checkpoint',),\n    122: ('uri_too_long', 'request_uri_too_long'),\n    200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', '\\\\o/', '✓'),\n    201: ('created',),\n    202: ('accepted',),\n    203: ('non_authoritative_info', 'non_authoritative_information'),\n    204: ('no_content',),\n    205: ('reset_content', 'reset'),\n    206: ('partial_content', 'partial'),\n    207: ('multi_status', 'multiple_status', 'multi_stati', 'multiple_stati'),\n    208: ('already_reported',),\n    226: ('im_used',),\n\n    # Redirection.\n    300: ('multiple_choices',),\n    301: ('moved_permanently', 'moved', '\\\\o-'),\n    302: ('found',),\n    303: ('see_other', 'other'),\n    304: ('not_modified',),\n    305: ('use_proxy',),\n    306: ('switch_proxy',),\n    307: ('temporary_redirect', 'temporary_moved', 'temporary'),\n    308: ('permanent_redirect',\n          'resume_incomplete', 'resume',),  # These 2 to be removed in 3.0\n\n    # Client Error.\n    400: ('bad_request', 'bad'),\n    401: ('unauthorized',),\n    402: ('payment_required', 'payment'),\n    403: ('forbidden',),\n    404: ('not_found', '-o-'),\n    405: ('method_not_allowed', 'not_allowed'),\n    406: ('not_acceptable',),\n    407: ('proxy_authentication_required', 'proxy_auth', 'proxy_authentication'),\n    408: ('request_timeout', 'timeout'),\n    409: ('conflict',),\n    410: ('gone',),\n    411: ('length_required',),\n    412: ('precondition_failed', 'precondition'),\n    413: ('request_entity_too_large',),\n    414: ('request_uri_too_large',),\n    415: ('unsupported_media_type', 'unsupported_media', 'media_type'),\n    416: ('requested_range_not_satisfiable', 'requested_range', 'range_not_satisfiable'),\n    417: ('expectation_failed',),\n    418: ('im_a_teapot', 'teapot', 'i_am_a_teapot'),\n    421: ('misdirected_request',),\n    422: ('unprocessable_entity', 'unprocessable'),\n    423: ('locked',),\n    424: ('failed_dependency', 'dependency'),\n    425: ('unordered_collection', 'unordered'),\n    426: ('upgrade_required', 'upgrade'),\n    428: ('precondition_required', 'precondition'),\n    429: ('too_many_requests', 'too_many'),\n    431: ('header_fields_too_large', 'fields_too_large'),\n    444: ('no_response', 'none'),\n    449: ('retry_with', 'retry'),\n    450: ('blocked_by_windows_parental_controls', 'parental_controls'),\n    451: ('unavailable_for_legal_reasons', 'legal_reasons'),\n    499: ('client_closed_request',),\n\n    # Server Error.\n    500: ('internal_server_error', 'server_error', '/o\\\\', '✗'),\n    501: ('not_implemented',),\n    502: ('bad_gateway',),\n    503: ('service_unavailable', 'unavailable'),\n    504: ('gateway_timeout',),\n    505: ('http_version_not_supported', 'http_version'),\n    506: ('variant_also_negotiates',),\n    507: ('insufficient_storage',),\n    509: ('bandwidth_limit_exceeded', 'bandwidth'),\n    510: ('not_extended',),\n    511: ('network_authentication_required', 'network_auth', 'network_authentication'),\n}\n\ncodes = LookupDict(name='status_codes')\n\ndef _init():\n    for code, titles in _codes.items():\n        for title in titles:\n            setattr(codes, title, code)\n            if not title.startswith(('\\\\', '/')):\n                setattr(codes, title.upper(), code)\n\n    def doc(code):\n        names = ', '.join('``%s``' % n for n in _codes[code])\n        return '* %d: %s' % (code, names)\n\n    global __doc__\n    __doc__ = (__doc__ + '\\n' +\n               '\\n'.join(doc(code) for code in sorted(_codes))\n               if __doc__ is not None else None)\n\n_init()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/structures.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests.structures\n~~~~~~~~~~~~~~~~~~~\n\nData structures that power Requests.\n\"\"\"\n\nfrom collections import OrderedDict\n\nfrom .compat import Mapping, MutableMapping\n\n\nclass CaseInsensitiveDict(MutableMapping):\n    \"\"\"A case-insensitive ``dict``-like object.\n\n    Implements all methods and operations of\n    ``MutableMapping`` as well as dict's ``copy``. Also\n    provides ``lower_items``.\n\n    All keys are expected to be strings. The structure remembers the\n    case of the last key to be set, and ``iter(instance)``,\n    ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``\n    will contain case-sensitive keys. However, querying and contains\n    testing is case insensitive::\n\n        cid = CaseInsensitiveDict()\n        cid['Accept'] = 'application/json'\n        cid['aCCEPT'] == 'application/json'  # True\n        list(cid) == ['Accept']  # True\n\n    For example, ``headers['content-encoding']`` will return the\n    value of a ``'Content-Encoding'`` response header, regardless\n    of how the header name was originally stored.\n\n    If the constructor, ``.update``, or equality comparison\n    operations are given keys that have equal ``.lower()``s, the\n    behavior is undefined.\n    \"\"\"\n\n    def __init__(self, data=None, **kwargs):\n        self._store = OrderedDict()\n        if data is None:\n            data = {}\n        self.update(data, **kwargs)\n\n    def __setitem__(self, key, value):\n        # Use the lowercased key for lookups, but store the actual\n        # key alongside the value.\n        self._store[key.lower()] = (key, value)\n\n    def __getitem__(self, key):\n        return self._store[key.lower()][1]\n\n    def __delitem__(self, key):\n        del self._store[key.lower()]\n\n    def __iter__(self):\n        return (casedkey for casedkey, mappedvalue in self._store.values())\n\n    def __len__(self):\n        return len(self._store)\n\n    def lower_items(self):\n        \"\"\"Like iteritems(), but with all lowercase keys.\"\"\"\n        return (\n            (lowerkey, keyval[1])\n            for (lowerkey, keyval)\n            in self._store.items()\n        )\n\n    def __eq__(self, other):\n        if isinstance(other, Mapping):\n            other = CaseInsensitiveDict(other)\n        else:\n            return NotImplemented\n        # Compare insensitively\n        return dict(self.lower_items()) == dict(other.lower_items())\n\n    # Copy is required\n    def copy(self):\n        return CaseInsensitiveDict(self._store.values())\n\n    def __repr__(self):\n        return str(dict(self.items()))\n\n\nclass LookupDict(dict):\n    \"\"\"Dictionary lookup object.\"\"\"\n\n    def __init__(self, name=None):\n        self.name = name\n        super(LookupDict, self).__init__()\n\n    def __repr__(self):\n        return '<lookup \\'%s\\'>' % (self.name)\n\n    def __getitem__(self, key):\n        # We allow fall-through here, so values default to None\n\n        return self.__dict__.get(key, None)\n\n    def get(self, key, default=None):\n        return self.__dict__.get(key, default)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/requests/utils.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nrequests.utils\n~~~~~~~~~~~~~~\n\nThis module provides utility functions that are used within Requests\nthat are also useful for external consumption.\n\"\"\"\n\nimport codecs\nimport contextlib\nimport io\nimport os\nimport re\nimport socket\nimport struct\nimport sys\nimport tempfile\nimport warnings\nimport zipfile\nfrom collections import OrderedDict\nfrom urllib3.util import make_headers\nfrom urllib3.util import parse_url\n\nfrom .__version__ import __version__\nfrom . import certs\n# to_native_string is unused here, but imported here for backwards compatibility\nfrom ._internal_utils import to_native_string\nfrom .compat import parse_http_list as _parse_list_header\nfrom .compat import (\n    quote, urlparse, bytes, str, unquote, getproxies,\n    proxy_bypass, urlunparse, basestring, integer_types, is_py3,\n    proxy_bypass_environment, getproxies_environment, Mapping)\nfrom .cookies import cookiejar_from_dict\nfrom .structures import CaseInsensitiveDict\nfrom .exceptions import (\n    InvalidURL, InvalidHeader, FileModeWarning, UnrewindableBodyError)\n\nNETRC_FILES = ('.netrc', '_netrc')\n\nDEFAULT_CA_BUNDLE_PATH = certs.where()\n\nDEFAULT_PORTS = {'http': 80, 'https': 443}\n\n# Ensure that ', ' is used to preserve previous delimiter behavior.\nDEFAULT_ACCEPT_ENCODING = \", \".join(\n    re.split(r\",\\s*\", make_headers(accept_encoding=True)[\"accept-encoding\"])\n)\n\n\nif sys.platform == 'win32':\n    # provide a proxy_bypass version on Windows without DNS lookups\n\n    def proxy_bypass_registry(host):\n        try:\n            if is_py3:\n                import winreg\n            else:\n                import _winreg as winreg\n        except ImportError:\n            return False\n\n        try:\n            internetSettings = winreg.OpenKey(winreg.HKEY_CURRENT_USER,\n                r'Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings')\n            # ProxyEnable could be REG_SZ or REG_DWORD, normalizing it\n            proxyEnable = int(winreg.QueryValueEx(internetSettings,\n                                              'ProxyEnable')[0])\n            # ProxyOverride is almost always a string\n            proxyOverride = winreg.QueryValueEx(internetSettings,\n                                                'ProxyOverride')[0]\n        except OSError:\n            return False\n        if not proxyEnable or not proxyOverride:\n            return False\n\n        # make a check value list from the registry entry: replace the\n        # '<local>' string by the localhost entry and the corresponding\n        # canonical entry.\n        proxyOverride = proxyOverride.split(';')\n        # now check if we match one of the registry values.\n        for test in proxyOverride:\n            if test == '<local>':\n                if '.' not in host:\n                    return True\n            test = test.replace(\".\", r\"\\.\")     # mask dots\n            test = test.replace(\"*\", r\".*\")     # change glob sequence\n            test = test.replace(\"?\", r\".\")      # change glob char\n            if re.match(test, host, re.I):\n                return True\n        return False\n\n    def proxy_bypass(host):  # noqa\n        \"\"\"Return True, if the host should be bypassed.\n\n        Checks proxy settings gathered from the environment, if specified,\n        or the registry.\n        \"\"\"\n        if getproxies_environment():\n            return proxy_bypass_environment(host)\n        else:\n            return proxy_bypass_registry(host)\n\n\ndef dict_to_sequence(d):\n    \"\"\"Returns an internal sequence dictionary update.\"\"\"\n\n    if hasattr(d, 'items'):\n        d = d.items()\n\n    return d\n\n\ndef super_len(o):\n    total_length = None\n    current_position = 0\n\n    if hasattr(o, '__len__'):\n        total_length = len(o)\n\n    elif hasattr(o, 'len'):\n        total_length = o.len\n\n    elif hasattr(o, 'fileno'):\n        try:\n            fileno = o.fileno()\n        except (io.UnsupportedOperation, AttributeError):\n            # AttributeError is a surprising exception, seeing as how we've just checked\n            # that `hasattr(o, 'fileno')`.  It happens for objects obtained via\n            # `Tarfile.extractfile()`, per issue 5229.\n            pass\n        else:\n            total_length = os.fstat(fileno).st_size\n\n            # Having used fstat to determine the file length, we need to\n            # confirm that this file was opened up in binary mode.\n            if 'b' not in o.mode:\n                warnings.warn((\n                    \"Requests has determined the content-length for this \"\n                    \"request using the binary size of the file: however, the \"\n                    \"file has been opened in text mode (i.e. without the 'b' \"\n                    \"flag in the mode). This may lead to an incorrect \"\n                    \"content-length. In Requests 3.0, support will be removed \"\n                    \"for files in text mode.\"),\n                    FileModeWarning\n                )\n\n    if hasattr(o, 'tell'):\n        try:\n            current_position = o.tell()\n        except (OSError, IOError):\n            # This can happen in some weird situations, such as when the file\n            # is actually a special file descriptor like stdin. In this\n            # instance, we don't know what the length is, so set it to zero and\n            # let requests chunk it instead.\n            if total_length is not None:\n                current_position = total_length\n        else:\n            if hasattr(o, 'seek') and total_length is None:\n                # StringIO and BytesIO have seek but no usable fileno\n                try:\n                    # seek to end of file\n                    o.seek(0, 2)\n                    total_length = o.tell()\n\n                    # seek back to current position to support\n                    # partially read file-like objects\n                    o.seek(current_position or 0)\n                except (OSError, IOError):\n                    total_length = 0\n\n    if total_length is None:\n        total_length = 0\n\n    return max(0, total_length - current_position)\n\n\ndef get_netrc_auth(url, raise_errors=False):\n    \"\"\"Returns the Requests tuple auth for a given url from netrc.\"\"\"\n\n    netrc_file = os.environ.get('NETRC')\n    if netrc_file is not None:\n        netrc_locations = (netrc_file,)\n    else:\n        netrc_locations = ('~/{}'.format(f) for f in NETRC_FILES)\n\n    try:\n        from netrc import netrc, NetrcParseError\n\n        netrc_path = None\n\n        for f in netrc_locations:\n            try:\n                loc = os.path.expanduser(f)\n            except KeyError:\n                # os.path.expanduser can fail when $HOME is undefined and\n                # getpwuid fails. See https://bugs.python.org/issue20164 &\n                # https://github.com/psf/requests/issues/1846\n                return\n\n            if os.path.exists(loc):\n                netrc_path = loc\n                break\n\n        # Abort early if there isn't one.\n        if netrc_path is None:\n            return\n\n        ri = urlparse(url)\n\n        # Strip port numbers from netloc. This weird `if...encode`` dance is\n        # used for Python 3.2, which doesn't support unicode literals.\n        splitstr = b':'\n        if isinstance(url, str):\n            splitstr = splitstr.decode('ascii')\n        host = ri.netloc.split(splitstr)[0]\n\n        try:\n            _netrc = netrc(netrc_path).authenticators(host)\n            if _netrc:\n                # Return with login / password\n                login_i = (0 if _netrc[0] else 1)\n                return (_netrc[login_i], _netrc[2])\n        except (NetrcParseError, IOError):\n            # If there was a parsing error or a permissions issue reading the file,\n            # we'll just skip netrc auth unless explicitly asked to raise errors.\n            if raise_errors:\n                raise\n\n    # App Engine hackiness.\n    except (ImportError, AttributeError):\n        pass\n\n\ndef guess_filename(obj):\n    \"\"\"Tries to guess the filename of the given object.\"\"\"\n    name = getattr(obj, 'name', None)\n    if (name and isinstance(name, basestring) and name[0] != '<' and\n            name[-1] != '>'):\n        return os.path.basename(name)\n\n\ndef extract_zipped_paths(path):\n    \"\"\"Replace nonexistent paths that look like they refer to a member of a zip\n    archive with the location of an extracted copy of the target, or else\n    just return the provided path unchanged.\n    \"\"\"\n    if os.path.exists(path):\n        # this is already a valid path, no need to do anything further\n        return path\n\n    # find the first valid part of the provided path and treat that as a zip archive\n    # assume the rest of the path is the name of a member in the archive\n    archive, member = os.path.split(path)\n    while archive and not os.path.exists(archive):\n        archive, prefix = os.path.split(archive)\n        if not prefix:\n            # If we don't check for an empty prefix after the split (in other words, archive remains unchanged after the split),\n            # we _can_ end up in an infinite loop on a rare corner case affecting a small number of users\n            break\n        member = '/'.join([prefix, member])\n\n    if not zipfile.is_zipfile(archive):\n        return path\n\n    zip_file = zipfile.ZipFile(archive)\n    if member not in zip_file.namelist():\n        return path\n\n    # we have a valid zip archive and a valid member of that archive\n    tmp = tempfile.gettempdir()\n    extracted_path = os.path.join(tmp, member.split('/')[-1])\n    if not os.path.exists(extracted_path):\n        # use read + write to avoid the creating nested folders, we only want the file, avoids mkdir racing condition\n        with atomic_open(extracted_path) as file_handler:\n            file_handler.write(zip_file.read(member))\n    return extracted_path\n\n\n@contextlib.contextmanager\ndef atomic_open(filename):\n    \"\"\"Write a file to the disk in an atomic fashion\"\"\"\n    replacer = os.rename if sys.version_info[0] == 2 else os.replace\n    tmp_descriptor, tmp_name = tempfile.mkstemp(dir=os.path.dirname(filename))\n    try:\n        with os.fdopen(tmp_descriptor, 'wb') as tmp_handler:\n            yield tmp_handler\n        replacer(tmp_name, filename)\n    except BaseException:\n        os.remove(tmp_name)\n        raise\n\n\ndef from_key_val_list(value):\n    \"\"\"Take an object and test to see if it can be represented as a\n    dictionary. Unless it can not be represented as such, return an\n    OrderedDict, e.g.,\n\n    ::\n\n        >>> from_key_val_list([('key', 'val')])\n        OrderedDict([('key', 'val')])\n        >>> from_key_val_list('string')\n        Traceback (most recent call last):\n        ...\n        ValueError: cannot encode objects that are not 2-tuples\n        >>> from_key_val_list({'key': 'val'})\n        OrderedDict([('key', 'val')])\n\n    :rtype: OrderedDict\n    \"\"\"\n    if value is None:\n        return None\n\n    if isinstance(value, (str, bytes, bool, int)):\n        raise ValueError('cannot encode objects that are not 2-tuples')\n\n    return OrderedDict(value)\n\n\ndef to_key_val_list(value):\n    \"\"\"Take an object and test to see if it can be represented as a\n    dictionary. If it can be, return a list of tuples, e.g.,\n\n    ::\n\n        >>> to_key_val_list([('key', 'val')])\n        [('key', 'val')]\n        >>> to_key_val_list({'key': 'val'})\n        [('key', 'val')]\n        >>> to_key_val_list('string')\n        Traceback (most recent call last):\n        ...\n        ValueError: cannot encode objects that are not 2-tuples\n\n    :rtype: list\n    \"\"\"\n    if value is None:\n        return None\n\n    if isinstance(value, (str, bytes, bool, int)):\n        raise ValueError('cannot encode objects that are not 2-tuples')\n\n    if isinstance(value, Mapping):\n        value = value.items()\n\n    return list(value)\n\n\n# From mitsuhiko/werkzeug (used with permission).\ndef parse_list_header(value):\n    \"\"\"Parse lists as described by RFC 2068 Section 2.\n\n    In particular, parse comma-separated lists where the elements of\n    the list may include quoted-strings.  A quoted-string could\n    contain a comma.  A non-quoted string could have quotes in the\n    middle.  Quotes are removed automatically after parsing.\n\n    It basically works like :func:`parse_set_header` just that items\n    may appear multiple times and case sensitivity is preserved.\n\n    The return value is a standard :class:`list`:\n\n    >>> parse_list_header('token, \"quoted value\"')\n    ['token', 'quoted value']\n\n    To create a header from the :class:`list` again, use the\n    :func:`dump_header` function.\n\n    :param value: a string with a list header.\n    :return: :class:`list`\n    :rtype: list\n    \"\"\"\n    result = []\n    for item in _parse_list_header(value):\n        if item[:1] == item[-1:] == '\"':\n            item = unquote_header_value(item[1:-1])\n        result.append(item)\n    return result\n\n\n# From mitsuhiko/werkzeug (used with permission).\ndef parse_dict_header(value):\n    \"\"\"Parse lists of key, value pairs as described by RFC 2068 Section 2 and\n    convert them into a python dict:\n\n    >>> d = parse_dict_header('foo=\"is a fish\", bar=\"as well\"')\n    >>> type(d) is dict\n    True\n    >>> sorted(d.items())\n    [('bar', 'as well'), ('foo', 'is a fish')]\n\n    If there is no value for a key it will be `None`:\n\n    >>> parse_dict_header('key_without_value')\n    {'key_without_value': None}\n\n    To create a header from the :class:`dict` again, use the\n    :func:`dump_header` function.\n\n    :param value: a string with a dict header.\n    :return: :class:`dict`\n    :rtype: dict\n    \"\"\"\n    result = {}\n    for item in _parse_list_header(value):\n        if '=' not in item:\n            result[item] = None\n            continue\n        name, value = item.split('=', 1)\n        if value[:1] == value[-1:] == '\"':\n            value = unquote_header_value(value[1:-1])\n        result[name] = value\n    return result\n\n\n# From mitsuhiko/werkzeug (used with permission).\ndef unquote_header_value(value, is_filename=False):\n    r\"\"\"Unquotes a header value.  (Reversal of :func:`quote_header_value`).\n    This does not use the real unquoting but what browsers are actually\n    using for quoting.\n\n    :param value: the header value to unquote.\n    :rtype: str\n    \"\"\"\n    if value and value[0] == value[-1] == '\"':\n        # this is not the real unquoting, but fixing this so that the\n        # RFC is met will result in bugs with internet explorer and\n        # probably some other browsers as well.  IE for example is\n        # uploading files with \"C:\\foo\\bar.txt\" as filename\n        value = value[1:-1]\n\n        # if this is a filename and the starting characters look like\n        # a UNC path, then just return the value without quotes.  Using the\n        # replace sequence below on a UNC path has the effect of turning\n        # the leading double slash into a single slash and then\n        # _fix_ie_filename() doesn't work correctly.  See #458.\n        if not is_filename or value[:2] != '\\\\\\\\':\n            return value.replace('\\\\\\\\', '\\\\').replace('\\\\\"', '\"')\n    return value\n\n\ndef dict_from_cookiejar(cj):\n    \"\"\"Returns a key/value dictionary from a CookieJar.\n\n    :param cj: CookieJar object to extract cookies from.\n    :rtype: dict\n    \"\"\"\n\n    cookie_dict = {}\n\n    for cookie in cj:\n        cookie_dict[cookie.name] = cookie.value\n\n    return cookie_dict\n\n\ndef add_dict_to_cookiejar(cj, cookie_dict):\n    \"\"\"Returns a CookieJar from a key/value dictionary.\n\n    :param cj: CookieJar to insert cookies into.\n    :param cookie_dict: Dict of key/values to insert into CookieJar.\n    :rtype: CookieJar\n    \"\"\"\n\n    return cookiejar_from_dict(cookie_dict, cj)\n\n\ndef get_encodings_from_content(content):\n    \"\"\"Returns encodings from given content string.\n\n    :param content: bytestring to extract encodings from.\n    \"\"\"\n    warnings.warn((\n        'In requests 3.0, get_encodings_from_content will be removed. For '\n        'more information, please see the discussion on issue #2266. (This'\n        ' warning should only appear once.)'),\n        DeprecationWarning)\n\n    charset_re = re.compile(r'<meta.*?charset=[\"\\']*(.+?)[\"\\'>]', flags=re.I)\n    pragma_re = re.compile(r'<meta.*?content=[\"\\']*;?charset=(.+?)[\"\\'>]', flags=re.I)\n    xml_re = re.compile(r'^<\\?xml.*?encoding=[\"\\']*(.+?)[\"\\'>]')\n\n    return (charset_re.findall(content) +\n            pragma_re.findall(content) +\n            xml_re.findall(content))\n\n\ndef _parse_content_type_header(header):\n    \"\"\"Returns content type and parameters from given header\n\n    :param header: string\n    :return: tuple containing content type and dictionary of\n         parameters\n    \"\"\"\n\n    tokens = header.split(';')\n    content_type, params = tokens[0].strip(), tokens[1:]\n    params_dict = {}\n    items_to_strip = \"\\\"' \"\n\n    for param in params:\n        param = param.strip()\n        if param:\n            key, value = param, True\n            index_of_equals = param.find(\"=\")\n            if index_of_equals != -1:\n                key = param[:index_of_equals].strip(items_to_strip)\n                value = param[index_of_equals + 1:].strip(items_to_strip)\n            params_dict[key.lower()] = value\n    return content_type, params_dict\n\n\ndef get_encoding_from_headers(headers):\n    \"\"\"Returns encodings from given HTTP Header Dict.\n\n    :param headers: dictionary to extract encoding from.\n    :rtype: str\n    \"\"\"\n\n    content_type = headers.get('content-type')\n\n    if not content_type:\n        return None\n\n    content_type, params = _parse_content_type_header(content_type)\n\n    if 'charset' in params:\n        return params['charset'].strip(\"'\\\"\")\n\n    if 'text' in content_type:\n        return 'ISO-8859-1'\n\n    if 'application/json' in content_type:\n        # Assume UTF-8 based on RFC 4627: https://www.ietf.org/rfc/rfc4627.txt since the charset was unset\n        return 'utf-8'\n\n\ndef stream_decode_response_unicode(iterator, r):\n    \"\"\"Stream decodes a iterator.\"\"\"\n\n    if r.encoding is None:\n        for item in iterator:\n            yield item\n        return\n\n    decoder = codecs.getincrementaldecoder(r.encoding)(errors='replace')\n    for chunk in iterator:\n        rv = decoder.decode(chunk)\n        if rv:\n            yield rv\n    rv = decoder.decode(b'', final=True)\n    if rv:\n        yield rv\n\n\ndef iter_slices(string, slice_length):\n    \"\"\"Iterate over slices of a string.\"\"\"\n    pos = 0\n    if slice_length is None or slice_length <= 0:\n        slice_length = len(string)\n    while pos < len(string):\n        yield string[pos:pos + slice_length]\n        pos += slice_length\n\n\ndef get_unicode_from_response(r):\n    \"\"\"Returns the requested content back in unicode.\n\n    :param r: Response object to get unicode content from.\n\n    Tried:\n\n    1. charset from content-type\n    2. fall back and replace all unicode characters\n\n    :rtype: str\n    \"\"\"\n    warnings.warn((\n        'In requests 3.0, get_unicode_from_response will be removed. For '\n        'more information, please see the discussion on issue #2266. (This'\n        ' warning should only appear once.)'),\n        DeprecationWarning)\n\n    tried_encodings = []\n\n    # Try charset from content-type\n    encoding = get_encoding_from_headers(r.headers)\n\n    if encoding:\n        try:\n            return str(r.content, encoding)\n        except UnicodeError:\n            tried_encodings.append(encoding)\n\n    # Fall back:\n    try:\n        return str(r.content, encoding, errors='replace')\n    except TypeError:\n        return r.content\n\n\n# The unreserved URI characters (RFC 3986)\nUNRESERVED_SET = frozenset(\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\" + \"0123456789-._~\")\n\n\ndef unquote_unreserved(uri):\n    \"\"\"Un-escape any percent-escape sequences in a URI that are unreserved\n    characters. This leaves all reserved, illegal and non-ASCII bytes encoded.\n\n    :rtype: str\n    \"\"\"\n    parts = uri.split('%')\n    for i in range(1, len(parts)):\n        h = parts[i][0:2]\n        if len(h) == 2 and h.isalnum():\n            try:\n                c = chr(int(h, 16))\n            except ValueError:\n                raise InvalidURL(\"Invalid percent-escape sequence: '%s'\" % h)\n\n            if c in UNRESERVED_SET:\n                parts[i] = c + parts[i][2:]\n            else:\n                parts[i] = '%' + parts[i]\n        else:\n            parts[i] = '%' + parts[i]\n    return ''.join(parts)\n\n\ndef requote_uri(uri):\n    \"\"\"Re-quote the given URI.\n\n    This function passes the given URI through an unquote/quote cycle to\n    ensure that it is fully and consistently quoted.\n\n    :rtype: str\n    \"\"\"\n    safe_with_percent = \"!#$%&'()*+,/:;=?@[]~\"\n    safe_without_percent = \"!#$&'()*+,/:;=?@[]~\"\n    try:\n        # Unquote only the unreserved characters\n        # Then quote only illegal characters (do not quote reserved,\n        # unreserved, or '%')\n        return quote(unquote_unreserved(uri), safe=safe_with_percent)\n    except InvalidURL:\n        # We couldn't unquote the given URI, so let's try quoting it, but\n        # there may be unquoted '%'s in the URI. We need to make sure they're\n        # properly quoted so they do not cause issues elsewhere.\n        return quote(uri, safe=safe_without_percent)\n\n\ndef address_in_network(ip, net):\n    \"\"\"This function allows you to check if an IP belongs to a network subnet\n\n    Example: returns True if ip = 192.168.1.1 and net = 192.168.1.0/24\n             returns False if ip = 192.168.1.1 and net = 192.168.100.0/24\n\n    :rtype: bool\n    \"\"\"\n    ipaddr = struct.unpack('=L', socket.inet_aton(ip))[0]\n    netaddr, bits = net.split('/')\n    netmask = struct.unpack('=L', socket.inet_aton(dotted_netmask(int(bits))))[0]\n    network = struct.unpack('=L', socket.inet_aton(netaddr))[0] & netmask\n    return (ipaddr & netmask) == (network & netmask)\n\n\ndef dotted_netmask(mask):\n    \"\"\"Converts mask from /xx format to xxx.xxx.xxx.xxx\n\n    Example: if mask is 24 function returns 255.255.255.0\n\n    :rtype: str\n    \"\"\"\n    bits = 0xffffffff ^ (1 << 32 - mask) - 1\n    return socket.inet_ntoa(struct.pack('>I', bits))\n\n\ndef is_ipv4_address(string_ip):\n    \"\"\"\n    :rtype: bool\n    \"\"\"\n    try:\n        socket.inet_aton(string_ip)\n    except socket.error:\n        return False\n    return True\n\n\ndef is_valid_cidr(string_network):\n    \"\"\"\n    Very simple check of the cidr format in no_proxy variable.\n\n    :rtype: bool\n    \"\"\"\n    if string_network.count('/') == 1:\n        try:\n            mask = int(string_network.split('/')[1])\n        except ValueError:\n            return False\n\n        if mask < 1 or mask > 32:\n            return False\n\n        try:\n            socket.inet_aton(string_network.split('/')[0])\n        except socket.error:\n            return False\n    else:\n        return False\n    return True\n\n\n@contextlib.contextmanager\ndef set_environ(env_name, value):\n    \"\"\"Set the environment variable 'env_name' to 'value'\n\n    Save previous value, yield, and then restore the previous value stored in\n    the environment variable 'env_name'.\n\n    If 'value' is None, do nothing\"\"\"\n    value_changed = value is not None\n    if value_changed:\n        old_value = os.environ.get(env_name)\n        os.environ[env_name] = value\n    try:\n        yield\n    finally:\n        if value_changed:\n            if old_value is None:\n                del os.environ[env_name]\n            else:\n                os.environ[env_name] = old_value\n\n\ndef should_bypass_proxies(url, no_proxy):\n    \"\"\"\n    Returns whether we should bypass proxies or not.\n\n    :rtype: bool\n    \"\"\"\n    # Prioritize lowercase environment variables over uppercase\n    # to keep a consistent behaviour with other http projects (curl, wget).\n    get_proxy = lambda k: os.environ.get(k) or os.environ.get(k.upper())\n\n    # First check whether no_proxy is defined. If it is, check that the URL\n    # we're getting isn't in the no_proxy list.\n    no_proxy_arg = no_proxy\n    if no_proxy is None:\n        no_proxy = get_proxy('no_proxy')\n    parsed = urlparse(url)\n\n    if parsed.hostname is None:\n        # URLs don't always have hostnames, e.g. file:/// urls.\n        return True\n\n    if no_proxy:\n        # We need to check whether we match here. We need to see if we match\n        # the end of the hostname, both with and without the port.\n        no_proxy = (\n            host for host in no_proxy.replace(' ', '').split(',') if host\n        )\n\n        if is_ipv4_address(parsed.hostname):\n            for proxy_ip in no_proxy:\n                if is_valid_cidr(proxy_ip):\n                    if address_in_network(parsed.hostname, proxy_ip):\n                        return True\n                elif parsed.hostname == proxy_ip:\n                    # If no_proxy ip was defined in plain IP notation instead of cidr notation &\n                    # matches the IP of the index\n                    return True\n        else:\n            host_with_port = parsed.hostname\n            if parsed.port:\n                host_with_port += ':{}'.format(parsed.port)\n\n            for host in no_proxy:\n                if parsed.hostname.endswith(host) or host_with_port.endswith(host):\n                    # The URL does match something in no_proxy, so we don't want\n                    # to apply the proxies on this URL.\n                    return True\n\n    with set_environ('no_proxy', no_proxy_arg):\n        # parsed.hostname can be `None` in cases such as a file URI.\n        try:\n            bypass = proxy_bypass(parsed.hostname)\n        except (TypeError, socket.gaierror):\n            bypass = False\n\n    if bypass:\n        return True\n\n    return False\n\n\ndef get_environ_proxies(url, no_proxy=None):\n    \"\"\"\n    Return a dict of environment proxies.\n\n    :rtype: dict\n    \"\"\"\n    if should_bypass_proxies(url, no_proxy=no_proxy):\n        return {}\n    else:\n        return getproxies()\n\n\ndef select_proxy(url, proxies):\n    \"\"\"Select a proxy for the url, if applicable.\n\n    :param url: The url being for the request\n    :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs\n    \"\"\"\n    proxies = proxies or {}\n    urlparts = urlparse(url)\n    if urlparts.hostname is None:\n        return proxies.get(urlparts.scheme, proxies.get('all'))\n\n    proxy_keys = [\n        urlparts.scheme + '://' + urlparts.hostname,\n        urlparts.scheme,\n        'all://' + urlparts.hostname,\n        'all',\n    ]\n    proxy = None\n    for proxy_key in proxy_keys:\n        if proxy_key in proxies:\n            proxy = proxies[proxy_key]\n            break\n\n    return proxy\n\n\ndef resolve_proxies(request, proxies, trust_env=True):\n    \"\"\"This method takes proxy information from a request and configuration\n    input to resolve a mapping of target proxies. This will consider settings\n    such a NO_PROXY to strip proxy configurations.\n\n    :param request: Request or PreparedRequest\n    :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs\n    :param trust_env: Boolean declaring whether to trust environment configs\n\n    :rtype: dict\n    \"\"\"\n    proxies = proxies if proxies is not None else {}\n    url = request.url\n    scheme = urlparse(url).scheme\n    no_proxy = proxies.get('no_proxy')\n    new_proxies = proxies.copy()\n\n    if trust_env and not should_bypass_proxies(url, no_proxy=no_proxy):\n        environ_proxies = get_environ_proxies(url, no_proxy=no_proxy)\n\n        proxy = environ_proxies.get(scheme, environ_proxies.get('all'))\n\n        if proxy:\n            new_proxies.setdefault(scheme, proxy)\n    return new_proxies\n\n\ndef default_user_agent(name=\"python-requests\"):\n    \"\"\"\n    Return a string representing the default user agent.\n\n    :rtype: str\n    \"\"\"\n    return '%s/%s' % (name, __version__)\n\n\ndef default_headers():\n    \"\"\"\n    :rtype: requests.structures.CaseInsensitiveDict\n    \"\"\"\n    return CaseInsensitiveDict({\n        'User-Agent': default_user_agent(),\n        'Accept-Encoding': DEFAULT_ACCEPT_ENCODING,\n        'Accept': '*/*',\n        'Connection': 'keep-alive',\n    })\n\n\ndef parse_header_links(value):\n    \"\"\"Return a list of parsed link headers proxies.\n\n    i.e. Link: <http:/.../front.jpeg>; rel=front; type=\"image/jpeg\",<http://.../back.jpeg>; rel=back;type=\"image/jpeg\"\n\n    :rtype: list\n    \"\"\"\n\n    links = []\n\n    replace_chars = ' \\'\"'\n\n    value = value.strip(replace_chars)\n    if not value:\n        return links\n\n    for val in re.split(', *<', value):\n        try:\n            url, params = val.split(';', 1)\n        except ValueError:\n            url, params = val, ''\n\n        link = {'url': url.strip('<> \\'\"')}\n\n        for param in params.split(';'):\n            try:\n                key, value = param.split('=')\n            except ValueError:\n                break\n\n            link[key.strip(replace_chars)] = value.strip(replace_chars)\n\n        links.append(link)\n\n    return links\n\n\n# Null bytes; no need to recreate these on each call to guess_json_utf\n_null = '\\x00'.encode('ascii')  # encoding to ASCII for Python 3\n_null2 = _null * 2\n_null3 = _null * 3\n\n\ndef guess_json_utf(data):\n    \"\"\"\n    :rtype: str\n    \"\"\"\n    # JSON always starts with two ASCII characters, so detection is as\n    # easy as counting the nulls and from their location and count\n    # determine the encoding. Also detect a BOM, if present.\n    sample = data[:4]\n    if sample in (codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE):\n        return 'utf-32'     # BOM included\n    if sample[:3] == codecs.BOM_UTF8:\n        return 'utf-8-sig'  # BOM included, MS style (discouraged)\n    if sample[:2] in (codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE):\n        return 'utf-16'     # BOM included\n    nullcount = sample.count(_null)\n    if nullcount == 0:\n        return 'utf-8'\n    if nullcount == 2:\n        if sample[::2] == _null2:   # 1st and 3rd are null\n            return 'utf-16-be'\n        if sample[1::2] == _null2:  # 2nd and 4th are null\n            return 'utf-16-le'\n        # Did not detect 2 valid UTF-16 ascii-range characters\n    if nullcount == 3:\n        if sample[:3] == _null3:\n            return 'utf-32-be'\n        if sample[1:] == _null3:\n            return 'utf-32-le'\n        # Did not detect a valid UTF-32 ascii-range character\n    return None\n\n\ndef prepend_scheme_if_needed(url, new_scheme):\n    \"\"\"Given a URL that may or may not have a scheme, prepend the given scheme.\n    Does not replace a present scheme with the one provided as an argument.\n\n    :rtype: str\n    \"\"\"\n    parsed = parse_url(url)\n    scheme, auth, host, port, path, query, fragment = parsed\n\n    # A defect in urlparse determines that there isn't a netloc present in some\n    # urls. We previously assumed parsing was overly cautious, and swapped the\n    # netloc and path. Due to a lack of tests on the original defect, this is\n    # maintained with parse_url for backwards compatibility.\n    netloc = parsed.netloc\n    if not netloc:\n        netloc, path = path, netloc\n\n    if auth:\n        # parse_url doesn't provide the netloc with auth\n        # so we'll add it ourselves.\n        netloc = '@'.join([auth, netloc])\n    if scheme is None:\n        scheme = new_scheme\n    if path is None:\n        path = ''\n\n    return urlunparse((scheme, netloc, path, '', query, fragment))\n\n\ndef get_auth_from_url(url):\n    \"\"\"Given a url with authentication components, extract them into a tuple of\n    username,password.\n\n    :rtype: (str,str)\n    \"\"\"\n    parsed = urlparse(url)\n\n    try:\n        auth = (unquote(parsed.username), unquote(parsed.password))\n    except (AttributeError, TypeError):\n        auth = ('', '')\n\n    return auth\n\n\n# Moved outside of function to avoid recompile every call\n_CLEAN_HEADER_REGEX_BYTE = re.compile(b'^\\\\S[^\\\\r\\\\n]*$|^$')\n_CLEAN_HEADER_REGEX_STR = re.compile(r'^\\S[^\\r\\n]*$|^$')\n\n\ndef check_header_validity(header):\n    \"\"\"Verifies that header value is a string which doesn't contain\n    leading whitespace or return characters. This prevents unintended\n    header injection.\n\n    :param header: tuple, in the format (name, value).\n    \"\"\"\n    name, value = header\n\n    if isinstance(value, bytes):\n        pat = _CLEAN_HEADER_REGEX_BYTE\n    else:\n        pat = _CLEAN_HEADER_REGEX_STR\n    try:\n        if not pat.match(value):\n            raise InvalidHeader(\"Invalid return character or leading space in header: %s\" % name)\n    except TypeError:\n        raise InvalidHeader(\"Value for header {%s: %s} must be of type str or \"\n                            \"bytes, not %s\" % (name, value, type(value)))\n\n\ndef urldefragauth(url):\n    \"\"\"\n    Given a url remove the fragment and the authentication part.\n\n    :rtype: str\n    \"\"\"\n    scheme, netloc, path, params, query, fragment = urlparse(url)\n\n    # see func:`prepend_scheme_if_needed`\n    if not netloc:\n        netloc, path = path, netloc\n\n    netloc = netloc.rsplit('@', 1)[-1]\n\n    return urlunparse((scheme, netloc, path, params, query, ''))\n\n\ndef rewind_body(prepared_request):\n    \"\"\"Move file pointer back to its recorded starting position\n    so it can be read again on redirect.\n    \"\"\"\n    body_seek = getattr(prepared_request.body, 'seek', None)\n    if body_seek is not None and isinstance(prepared_request._body_position, integer_types):\n        try:\n            body_seek(prepared_request._body_position)\n        except (IOError, OSError):\n            raise UnrewindableBodyError(\"An error occurred when rewinding request \"\n                                        \"body for redirect.\")\n    else:\n        raise UnrewindableBodyError(\"Unable to rewind request body for redirect.\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/secrets/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2019 Scaleway\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "openpype/vendor/python/python_2/secrets/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\n\n__version__ = \"1.0.6\"\n\n# Emulates __all__ for Python2\nfrom .secrets import (\n    choice,\n    randbelow,\n    randbits,\n    SystemRandom,\n    token_bytes,\n    token_hex,\n    token_urlsafe,\n    compare_digest\n)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/secrets/secrets.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Generate cryptographically strong pseudo-random numbers suitable for\n\nmanaging secrets such as account authentication, tokens, and similar.\n\n\nSee PEP 506 for more information.\n\nhttps://www.python.org/dev/peps/pep-0506/\n\n\n\"\"\"\n\n\n__all__ = ['choice', 'randbelow', 'randbits', 'SystemRandom',\n\n           'token_bytes', 'token_hex', 'token_urlsafe',\n\n           'compare_digest',\n\n           ]\n\nimport os\nimport sys\nfrom random import SystemRandom\n\nimport base64\n\nimport binascii\n\n\n# hmac.compare_digest did appear in python 2.7.7\nif sys.version_info >= (2, 7, 7):\n    from hmac import compare_digest\nelse:\n    # If we use an older python version, we will define an equivalent method\n    def compare_digest(a, b):\n        \"\"\"Compatibility compare_digest method for python < 2.7.\n        This method is NOT cryptographically secure and may be subject to\n        timing attacks, see https://docs.python.org/2/library/hmac.html\n        \"\"\"\n        return a == b\n\n\n_sysrand = SystemRandom()\n\n\nrandbits = _sysrand.getrandbits\n\nchoice = _sysrand.choice\n\n\ndef randbelow(exclusive_upper_bound):\n\n    \"\"\"Return a random int in the range [0, n).\"\"\"\n\n    if exclusive_upper_bound <= 0:\n\n        raise ValueError(\"Upper bound must be positive.\")\n\n    return _sysrand._randbelow(exclusive_upper_bound)\n\n\nDEFAULT_ENTROPY = 32  # number of bytes to return by default\n\n\ndef token_bytes(nbytes=None):\n\n    \"\"\"Return a random byte string containing *nbytes* bytes.\n\n\n    If *nbytes* is ``None`` or not supplied, a reasonable\n\n    default is used.\n\n\n    >>> token_bytes(16)  #doctest:+SKIP\n\n    b'\\\\xebr\\\\x17D*t\\\\xae\\\\xd4\\\\xe3S\\\\xb6\\\\xe2\\\\xebP1\\\\x8b'\n\n\n    \"\"\"\n\n    if nbytes is None:\n\n        nbytes = DEFAULT_ENTROPY\n\n    return os.urandom(nbytes)\n\n\ndef token_hex(nbytes=None):\n\n    \"\"\"Return a random text string, in hexadecimal.\n\n\n    The string has *nbytes* random bytes, each byte converted to two\n\n    hex digits.  If *nbytes* is ``None`` or not supplied, a reasonable\n\n    default is used.\n\n\n    >>> token_hex(16)  #doctest:+SKIP\n\n    'f9bf78b9a18ce6d46a0cd2b0b86df9da'\n\n\n    \"\"\"\n\n    return binascii.hexlify(token_bytes(nbytes)).decode('ascii')\n\n\ndef token_urlsafe(nbytes=None):\n\n    \"\"\"Return a random URL-safe text string, in Base64 encoding.\n\n\n    The string has *nbytes* random bytes.  If *nbytes* is ``None``\n\n    or not supplied, a reasonable default is used.\n\n\n    >>> token_urlsafe(16)  #doctest:+SKIP\n\n    'Drmhze6EPcv0fN_81Bj-nA'\n\n\n    \"\"\"\n\n    tok = token_bytes(nbytes)\n\n    return base64.urlsafe_b64encode(tok).rstrip(b'=').decode('ascii')\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/__init__.py",
    "content": "\"\"\"Extensions to the 'distutils' for large or complex distributions\"\"\"\n\nimport os\nimport sys\nimport functools\nimport distutils.core\nimport distutils.filelist\nimport re\nfrom distutils.errors import DistutilsOptionError\nfrom distutils.util import convert_path\nfrom fnmatch import fnmatchcase\n\nfrom ._deprecation_warning import SetuptoolsDeprecationWarning\n\nfrom setuptools.extern.six import PY3, string_types\nfrom setuptools.extern.six.moves import filter, map\n\nimport setuptools.version\nfrom setuptools.extension import Extension\nfrom setuptools.dist import Distribution, Feature\nfrom setuptools.depends import Require\nfrom . import monkey\n\n__metaclass__ = type\n\n\n__all__ = [\n    'setup', 'Distribution', 'Feature', 'Command', 'Extension', 'Require',\n    'SetuptoolsDeprecationWarning',\n    'find_packages'\n]\n\nif PY3:\n  __all__.append('find_namespace_packages')\n\n__version__ = setuptools.version.__version__\n\nbootstrap_install_from = None\n\n# If we run 2to3 on .py files, should we also convert docstrings?\n# Default: yes; assume that we can detect doctests reliably\nrun_2to3_on_doctests = True\n# Standard package names for fixer packages\nlib2to3_fixer_packages = ['lib2to3.fixes']\n\n\nclass PackageFinder:\n    \"\"\"\n    Generate a list of all Python packages found within a directory\n    \"\"\"\n\n    @classmethod\n    def find(cls, where='.', exclude=(), include=('*',)):\n        \"\"\"Return a list all Python packages found within directory 'where'\n\n        'where' is the root directory which will be searched for packages.  It\n        should be supplied as a \"cross-platform\" (i.e. URL-style) path; it will\n        be converted to the appropriate local path syntax.\n\n        'exclude' is a sequence of package names to exclude; '*' can be used\n        as a wildcard in the names, such that 'foo.*' will exclude all\n        subpackages of 'foo' (but not 'foo' itself).\n\n        'include' is a sequence of package names to include.  If it's\n        specified, only the named packages will be included.  If it's not\n        specified, all found packages will be included.  'include' can contain\n        shell style wildcard patterns just like 'exclude'.\n        \"\"\"\n\n        return list(cls._find_packages_iter(\n            convert_path(where),\n            cls._build_filter('ez_setup', '*__pycache__', *exclude),\n            cls._build_filter(*include)))\n\n    @classmethod\n    def _find_packages_iter(cls, where, exclude, include):\n        \"\"\"\n        All the packages found in 'where' that pass the 'include' filter, but\n        not the 'exclude' filter.\n        \"\"\"\n        for root, dirs, files in os.walk(where, followlinks=True):\n            # Copy dirs to iterate over it, then empty dirs.\n            all_dirs = dirs[:]\n            dirs[:] = []\n\n            for dir in all_dirs:\n                full_path = os.path.join(root, dir)\n                rel_path = os.path.relpath(full_path, where)\n                package = rel_path.replace(os.path.sep, '.')\n\n                # Skip directory trees that are not valid packages\n                if ('.' in dir or not cls._looks_like_package(full_path)):\n                    continue\n\n                # Should this package be included?\n                if include(package) and not exclude(package):\n                    yield package\n\n                # Keep searching subdirectories, as there may be more packages\n                # down there, even if the parent was excluded.\n                dirs.append(dir)\n\n    @staticmethod\n    def _looks_like_package(path):\n        \"\"\"Does a directory look like a package?\"\"\"\n        return os.path.isfile(os.path.join(path, '__init__.py'))\n\n    @staticmethod\n    def _build_filter(*patterns):\n        \"\"\"\n        Given a list of patterns, return a callable that will be true only if\n        the input matches at least one of the patterns.\n        \"\"\"\n        return lambda name: any(fnmatchcase(name, pat=pat) for pat in patterns)\n\n\nclass PEP420PackageFinder(PackageFinder):\n    @staticmethod\n    def _looks_like_package(path):\n        return True\n\n\nfind_packages = PackageFinder.find\n\nif PY3:\n  find_namespace_packages = PEP420PackageFinder.find\n\n\ndef _install_setup_requires(attrs):\n    # Note: do not use `setuptools.Distribution` directly, as\n    # our PEP 517 backend patch `distutils.core.Distribution`.\n    dist = distutils.core.Distribution(dict(\n        (k, v) for k, v in attrs.items()\n        if k in ('dependency_links', 'setup_requires')\n    ))\n    # Honor setup.cfg's options.\n    dist.parse_config_files(ignore_option_errors=True)\n    if dist.setup_requires:\n        dist.fetch_build_eggs(dist.setup_requires)\n\n\ndef setup(**attrs):\n    # Make sure we have any requirements needed to interpret 'attrs'.\n    _install_setup_requires(attrs)\n    return distutils.core.setup(**attrs)\n\nsetup.__doc__ = distutils.core.setup.__doc__\n\n\n_Command = monkey.get_unpatched(distutils.core.Command)\n\n\nclass Command(_Command):\n    __doc__ = _Command.__doc__\n\n    command_consumes_arguments = False\n\n    def __init__(self, dist, **kw):\n        \"\"\"\n        Construct the command for dist, updating\n        vars(self) with any keyword parameters.\n        \"\"\"\n        _Command.__init__(self, dist)\n        vars(self).update(kw)\n\n    def _ensure_stringlike(self, option, what, default=None):\n        val = getattr(self, option)\n        if val is None:\n            setattr(self, option, default)\n            return default\n        elif not isinstance(val, string_types):\n            raise DistutilsOptionError(\"'%s' must be a %s (got `%s`)\"\n                                       % (option, what, val))\n        return val\n\n    def ensure_string_list(self, option):\n        r\"\"\"Ensure that 'option' is a list of strings.  If 'option' is\n        currently a string, we split it either on /,\\s*/ or /\\s+/, so\n        \"foo bar baz\", \"foo,bar,baz\", and \"foo,   bar baz\" all become\n        [\"foo\", \"bar\", \"baz\"].\n        \"\"\"\n        val = getattr(self, option)\n        if val is None:\n            return\n        elif isinstance(val, string_types):\n            setattr(self, option, re.split(r',\\s*|\\s+', val))\n        else:\n            if isinstance(val, list):\n                ok = all(isinstance(v, string_types) for v in val)\n            else:\n                ok = False\n            if not ok:\n                raise DistutilsOptionError(\n                      \"'%s' must be a list of strings (got %r)\"\n                      % (option, val))\n\n    def reinitialize_command(self, command, reinit_subcommands=0, **kw):\n        cmd = _Command.reinitialize_command(self, command, reinit_subcommands)\n        vars(cmd).update(kw)\n        return cmd\n\n\ndef _find_all_simple(path):\n    \"\"\"\n    Find all files under 'path'\n    \"\"\"\n    results = (\n        os.path.join(base, file)\n        for base, dirs, files in os.walk(path, followlinks=True)\n        for file in files\n    )\n    return filter(os.path.isfile, results)\n\n\ndef findall(dir=os.curdir):\n    \"\"\"\n    Find all files under 'dir' and return the list of full filenames.\n    Unless dir is '.', return full filenames with dir prepended.\n    \"\"\"\n    files = _find_all_simple(dir)\n    if dir == os.curdir:\n        make_rel = functools.partial(os.path.relpath, start=dir)\n        files = map(make_rel, files)\n    return list(files)\n\n\n# Apply monkey patches\nmonkey.patch_all()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_deprecation_warning.py",
    "content": "class SetuptoolsDeprecationWarning(Warning):\n    \"\"\"\n    Base class for warning deprecations in ``setuptools``\n\n    This class is not derived from ``DeprecationWarning``, and as such is\n    visible by default.\n    \"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_imp.py",
    "content": "\"\"\"\nRe-implementation of find_module and get_frozen_object\nfrom the deprecated imp module.\n\"\"\"\n\nimport os\nimport importlib.util\nimport importlib.machinery\n\nfrom .py34compat import module_from_spec\n\n\nPY_SOURCE = 1\nPY_COMPILED = 2\nC_EXTENSION = 3\nC_BUILTIN = 6\nPY_FROZEN = 7\n\n\ndef find_module(module, paths=None):\n    \"\"\"Just like 'imp.find_module()', but with package support\"\"\"\n    spec = importlib.util.find_spec(module, paths)\n    if spec is None:\n        raise ImportError(\"Can't find %s\" % module)\n    if not spec.has_location and hasattr(spec, 'submodule_search_locations'):\n        spec = importlib.util.spec_from_loader('__init__.py', spec.loader)\n\n    kind = -1\n    file = None\n    static = isinstance(spec.loader, type)\n    if spec.origin == 'frozen' or static and issubclass(\n            spec.loader, importlib.machinery.FrozenImporter):\n        kind = PY_FROZEN\n        path = None  # imp compabilty\n        suffix = mode = ''  # imp compability\n    elif spec.origin == 'built-in' or static and issubclass(\n            spec.loader, importlib.machinery.BuiltinImporter):\n        kind = C_BUILTIN\n        path = None  # imp compabilty\n        suffix = mode = ''  # imp compability\n    elif spec.has_location:\n        path = spec.origin\n        suffix = os.path.splitext(path)[1]\n        mode = 'r' if suffix in importlib.machinery.SOURCE_SUFFIXES else 'rb'\n\n        if suffix in importlib.machinery.SOURCE_SUFFIXES:\n            kind = PY_SOURCE\n        elif suffix in importlib.machinery.BYTECODE_SUFFIXES:\n            kind = PY_COMPILED\n        elif suffix in importlib.machinery.EXTENSION_SUFFIXES:\n            kind = C_EXTENSION\n\n        if kind in {PY_SOURCE, PY_COMPILED}:\n            file = open(path, mode)\n    else:\n        path = None\n        suffix = mode = ''\n\n    return file, path, (suffix, mode, kind)\n\n\ndef get_frozen_object(module, paths=None):\n    spec = importlib.util.find_spec(module, paths)\n    if not spec:\n        raise ImportError(\"Can't find %s\" % module)\n    return spec.loader.get_code(module)\n\n\ndef get_module(module, paths, info):\n    spec = importlib.util.find_spec(module, paths)\n    if not spec:\n        raise ImportError(\"Can't find %s\" % module)\n    return module_from_spec(spec)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/ordered_set.py",
    "content": "\"\"\"\nAn OrderedSet is a custom MutableSet that remembers its order, so that every\nentry has an index that can be looked up.\n\nBased on a recipe originally posted to ActiveState Recipes by Raymond Hettiger,\nand released under the MIT license.\n\"\"\"\nimport itertools as it\nfrom collections import deque\n\ntry:\n    # Python 3\n    from collections.abc import MutableSet, Sequence\nexcept ImportError:\n    # Python 2.7\n    from collections import MutableSet, Sequence\n\nSLICE_ALL = slice(None)\n__version__ = \"3.1\"\n\n\ndef is_iterable(obj):\n    \"\"\"\n    Are we being asked to look up a list of things, instead of a single thing?\n    We check for the `__iter__` attribute so that this can cover types that\n    don't have to be known by this module, such as NumPy arrays.\n\n    Strings, however, should be considered as atomic values to look up, not\n    iterables. The same goes for tuples, since they are immutable and therefore\n    valid entries.\n\n    We don't need to check for the Python 2 `unicode` type, because it doesn't\n    have an `__iter__` attribute anyway.\n    \"\"\"\n    return (\n        hasattr(obj, \"__iter__\")\n        and not isinstance(obj, str)\n        and not isinstance(obj, tuple)\n    )\n\n\nclass OrderedSet(MutableSet, Sequence):\n    \"\"\"\n    An OrderedSet is a custom MutableSet that remembers its order, so that\n    every entry has an index that can be looked up.\n\n    Example:\n        >>> OrderedSet([1, 1, 2, 3, 2])\n        OrderedSet([1, 2, 3])\n    \"\"\"\n\n    def __init__(self, iterable=None):\n        self.items = []\n        self.map = {}\n        if iterable is not None:\n            self |= iterable\n\n    def __len__(self):\n        \"\"\"\n        Returns the number of unique elements in the ordered set\n\n        Example:\n            >>> len(OrderedSet([]))\n            0\n            >>> len(OrderedSet([1, 2]))\n            2\n        \"\"\"\n        return len(self.items)\n\n    def __getitem__(self, index):\n        \"\"\"\n        Get the item at a given index.\n\n        If `index` is a slice, you will get back that slice of items, as a\n        new OrderedSet.\n\n        If `index` is a list or a similar iterable, you'll get a list of\n        items corresponding to those indices. This is similar to NumPy's\n        \"fancy indexing\". The result is not an OrderedSet because you may ask\n        for duplicate indices, and the number of elements returned should be\n        the number of elements asked for.\n\n        Example:\n            >>> oset = OrderedSet([1, 2, 3])\n            >>> oset[1]\n            2\n        \"\"\"\n        if isinstance(index, slice) and index == SLICE_ALL:\n            return self.copy()\n        elif is_iterable(index):\n            return [self.items[i] for i in index]\n        elif hasattr(index, \"__index__\") or isinstance(index, slice):\n            result = self.items[index]\n            if isinstance(result, list):\n                return self.__class__(result)\n            else:\n                return result\n        else:\n            raise TypeError(\"Don't know how to index an OrderedSet by %r\" % index)\n\n    def copy(self):\n        \"\"\"\n        Return a shallow copy of this object.\n\n        Example:\n            >>> this = OrderedSet([1, 2, 3])\n            >>> other = this.copy()\n            >>> this == other\n            True\n            >>> this is other\n            False\n        \"\"\"\n        return self.__class__(self)\n\n    def __getstate__(self):\n        if len(self) == 0:\n            # The state can't be an empty list.\n            # We need to return a truthy value, or else __setstate__ won't be run.\n            #\n            # This could have been done more gracefully by always putting the state\n            # in a tuple, but this way is backwards- and forwards- compatible with\n            # previous versions of OrderedSet.\n            return (None,)\n        else:\n            return list(self)\n\n    def __setstate__(self, state):\n        if state == (None,):\n            self.__init__([])\n        else:\n            self.__init__(state)\n\n    def __contains__(self, key):\n        \"\"\"\n        Test if the item is in this ordered set\n\n        Example:\n            >>> 1 in OrderedSet([1, 3, 2])\n            True\n            >>> 5 in OrderedSet([1, 3, 2])\n            False\n        \"\"\"\n        return key in self.map\n\n    def add(self, key):\n        \"\"\"\n        Add `key` as an item to this OrderedSet, then return its index.\n\n        If `key` is already in the OrderedSet, return the index it already\n        had.\n\n        Example:\n            >>> oset = OrderedSet()\n            >>> oset.append(3)\n            0\n            >>> print(oset)\n            OrderedSet([3])\n        \"\"\"\n        if key not in self.map:\n            self.map[key] = len(self.items)\n            self.items.append(key)\n        return self.map[key]\n\n    append = add\n\n    def update(self, sequence):\n        \"\"\"\n        Update the set with the given iterable sequence, then return the index\n        of the last element inserted.\n\n        Example:\n            >>> oset = OrderedSet([1, 2, 3])\n            >>> oset.update([3, 1, 5, 1, 4])\n            4\n            >>> print(oset)\n            OrderedSet([1, 2, 3, 5, 4])\n        \"\"\"\n        item_index = None\n        try:\n            for item in sequence:\n                item_index = self.add(item)\n        except TypeError:\n            raise ValueError(\n                \"Argument needs to be an iterable, got %s\" % type(sequence)\n            )\n        return item_index\n\n    def index(self, key):\n        \"\"\"\n        Get the index of a given entry, raising an IndexError if it's not\n        present.\n\n        `key` can be an iterable of entries that is not a string, in which case\n        this returns a list of indices.\n\n        Example:\n            >>> oset = OrderedSet([1, 2, 3])\n            >>> oset.index(2)\n            1\n        \"\"\"\n        if is_iterable(key):\n            return [self.index(subkey) for subkey in key]\n        return self.map[key]\n\n    # Provide some compatibility with pd.Index\n    get_loc = index\n    get_indexer = index\n\n    def pop(self):\n        \"\"\"\n        Remove and return the last element from the set.\n\n        Raises KeyError if the set is empty.\n\n        Example:\n            >>> oset = OrderedSet([1, 2, 3])\n            >>> oset.pop()\n            3\n        \"\"\"\n        if not self.items:\n            raise KeyError(\"Set is empty\")\n\n        elem = self.items[-1]\n        del self.items[-1]\n        del self.map[elem]\n        return elem\n\n    def discard(self, key):\n        \"\"\"\n        Remove an element.  Do not raise an exception if absent.\n\n        The MutableSet mixin uses this to implement the .remove() method, which\n        *does* raise an error when asked to remove a non-existent item.\n\n        Example:\n            >>> oset = OrderedSet([1, 2, 3])\n            >>> oset.discard(2)\n            >>> print(oset)\n            OrderedSet([1, 3])\n            >>> oset.discard(2)\n            >>> print(oset)\n            OrderedSet([1, 3])\n        \"\"\"\n        if key in self:\n            i = self.map[key]\n            del self.items[i]\n            del self.map[key]\n            for k, v in self.map.items():\n                if v >= i:\n                    self.map[k] = v - 1\n\n    def clear(self):\n        \"\"\"\n        Remove all items from this OrderedSet.\n        \"\"\"\n        del self.items[:]\n        self.map.clear()\n\n    def __iter__(self):\n        \"\"\"\n        Example:\n            >>> list(iter(OrderedSet([1, 2, 3])))\n            [1, 2, 3]\n        \"\"\"\n        return iter(self.items)\n\n    def __reversed__(self):\n        \"\"\"\n        Example:\n            >>> list(reversed(OrderedSet([1, 2, 3])))\n            [3, 2, 1]\n        \"\"\"\n        return reversed(self.items)\n\n    def __repr__(self):\n        if not self:\n            return \"%s()\" % (self.__class__.__name__,)\n        return \"%s(%r)\" % (self.__class__.__name__, list(self))\n\n    def __eq__(self, other):\n        \"\"\"\n        Returns true if the containers have the same items. If `other` is a\n        Sequence, then order is checked, otherwise it is ignored.\n\n        Example:\n            >>> oset = OrderedSet([1, 3, 2])\n            >>> oset == [1, 3, 2]\n            True\n            >>> oset == [1, 2, 3]\n            False\n            >>> oset == [2, 3]\n            False\n            >>> oset == OrderedSet([3, 2, 1])\n            False\n        \"\"\"\n        # In Python 2 deque is not a Sequence, so treat it as one for\n        # consistent behavior with Python 3.\n        if isinstance(other, (Sequence, deque)):\n            # Check that this OrderedSet contains the same elements, in the\n            # same order, as the other object.\n            return list(self) == list(other)\n        try:\n            other_as_set = set(other)\n        except TypeError:\n            # If `other` can't be converted into a set, it's not equal.\n            return False\n        else:\n            return set(self) == other_as_set\n\n    def union(self, *sets):\n        \"\"\"\n        Combines all unique items.\n        Each items order is defined by its first appearance.\n\n        Example:\n            >>> oset = OrderedSet.union(OrderedSet([3, 1, 4, 1, 5]), [1, 3], [2, 0])\n            >>> print(oset)\n            OrderedSet([3, 1, 4, 5, 2, 0])\n            >>> oset.union([8, 9])\n            OrderedSet([3, 1, 4, 5, 2, 0, 8, 9])\n            >>> oset | {10}\n            OrderedSet([3, 1, 4, 5, 2, 0, 10])\n        \"\"\"\n        cls = self.__class__ if isinstance(self, OrderedSet) else OrderedSet\n        containers = map(list, it.chain([self], sets))\n        items = it.chain.from_iterable(containers)\n        return cls(items)\n\n    def __and__(self, other):\n        # the parent implementation of this is backwards\n        return self.intersection(other)\n\n    def intersection(self, *sets):\n        \"\"\"\n        Returns elements in common between all sets. Order is defined only\n        by the first set.\n\n        Example:\n            >>> oset = OrderedSet.intersection(OrderedSet([0, 1, 2, 3]), [1, 2, 3])\n            >>> print(oset)\n            OrderedSet([1, 2, 3])\n            >>> oset.intersection([2, 4, 5], [1, 2, 3, 4])\n            OrderedSet([2])\n            >>> oset.intersection()\n            OrderedSet([1, 2, 3])\n        \"\"\"\n        cls = self.__class__ if isinstance(self, OrderedSet) else OrderedSet\n        if sets:\n            common = set.intersection(*map(set, sets))\n            items = (item for item in self if item in common)\n        else:\n            items = self\n        return cls(items)\n\n    def difference(self, *sets):\n        \"\"\"\n        Returns all elements that are in this set but not the others.\n\n        Example:\n            >>> OrderedSet([1, 2, 3]).difference(OrderedSet([2]))\n            OrderedSet([1, 3])\n            >>> OrderedSet([1, 2, 3]).difference(OrderedSet([2]), OrderedSet([3]))\n            OrderedSet([1])\n            >>> OrderedSet([1, 2, 3]) - OrderedSet([2])\n            OrderedSet([1, 3])\n            >>> OrderedSet([1, 2, 3]).difference()\n            OrderedSet([1, 2, 3])\n        \"\"\"\n        cls = self.__class__\n        if sets:\n            other = set.union(*map(set, sets))\n            items = (item for item in self if item not in other)\n        else:\n            items = self\n        return cls(items)\n\n    def issubset(self, other):\n        \"\"\"\n        Report whether another set contains this set.\n\n        Example:\n            >>> OrderedSet([1, 2, 3]).issubset({1, 2})\n            False\n            >>> OrderedSet([1, 2, 3]).issubset({1, 2, 3, 4})\n            True\n            >>> OrderedSet([1, 2, 3]).issubset({1, 4, 3, 5})\n            False\n        \"\"\"\n        if len(self) > len(other):  # Fast check for obvious cases\n            return False\n        return all(item in other for item in self)\n\n    def issuperset(self, other):\n        \"\"\"\n        Report whether this set contains another set.\n\n        Example:\n            >>> OrderedSet([1, 2]).issuperset([1, 2, 3])\n            False\n            >>> OrderedSet([1, 2, 3, 4]).issuperset({1, 2, 3})\n            True\n            >>> OrderedSet([1, 4, 3, 5]).issuperset({1, 2, 3})\n            False\n        \"\"\"\n        if len(self) < len(other):  # Fast check for obvious cases\n            return False\n        return all(item in self for item in other)\n\n    def symmetric_difference(self, other):\n        \"\"\"\n        Return the symmetric difference of two OrderedSets as a new set.\n        That is, the new set will contain all elements that are in exactly\n        one of the sets.\n\n        Their order will be preserved, with elements from `self` preceding\n        elements from `other`.\n\n        Example:\n            >>> this = OrderedSet([1, 4, 3, 5, 7])\n            >>> other = OrderedSet([9, 7, 1, 3, 2])\n            >>> this.symmetric_difference(other)\n            OrderedSet([4, 5, 9, 2])\n        \"\"\"\n        cls = self.__class__ if isinstance(self, OrderedSet) else OrderedSet\n        diff1 = cls(self).difference(other)\n        diff2 = cls(other).difference(self)\n        return diff1.union(diff2)\n\n    def _update_items(self, items):\n        \"\"\"\n        Replace the 'items' list of this OrderedSet with a new one, updating\n        self.map accordingly.\n        \"\"\"\n        self.items = items\n        self.map = {item: idx for (idx, item) in enumerate(items)}\n\n    def difference_update(self, *sets):\n        \"\"\"\n        Update this OrderedSet to remove items from one or more other sets.\n\n        Example:\n            >>> this = OrderedSet([1, 2, 3])\n            >>> this.difference_update(OrderedSet([2, 4]))\n            >>> print(this)\n            OrderedSet([1, 3])\n\n            >>> this = OrderedSet([1, 2, 3, 4, 5])\n            >>> this.difference_update(OrderedSet([2, 4]), OrderedSet([1, 4, 6]))\n            >>> print(this)\n            OrderedSet([3, 5])\n        \"\"\"\n        items_to_remove = set()\n        for other in sets:\n            items_to_remove |= set(other)\n        self._update_items([item for item in self.items if item not in items_to_remove])\n\n    def intersection_update(self, other):\n        \"\"\"\n        Update this OrderedSet to keep only items in another set, preserving\n        their order in this set.\n\n        Example:\n            >>> this = OrderedSet([1, 4, 3, 5, 7])\n            >>> other = OrderedSet([9, 7, 1, 3, 2])\n            >>> this.intersection_update(other)\n            >>> print(this)\n            OrderedSet([1, 3, 7])\n        \"\"\"\n        other = set(other)\n        self._update_items([item for item in self.items if item in other])\n\n    def symmetric_difference_update(self, other):\n        \"\"\"\n        Update this OrderedSet to remove items from another set, then\n        add items from the other set that were not present in this set.\n\n        Example:\n            >>> this = OrderedSet([1, 4, 3, 5, 7])\n            >>> other = OrderedSet([9, 7, 1, 3, 2])\n            >>> this.symmetric_difference_update(other)\n            >>> print(this)\n            OrderedSet([4, 5, 9, 2])\n        \"\"\"\n        items_to_add = [item for item in other if item not in self]\n        items_to_remove = set(other)\n        self._update_items(\n            [item for item in self.items if item not in items_to_remove] + items_to_add\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/packaging/__about__.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\n__all__ = [\n    \"__title__\",\n    \"__summary__\",\n    \"__uri__\",\n    \"__version__\",\n    \"__author__\",\n    \"__email__\",\n    \"__license__\",\n    \"__copyright__\",\n]\n\n__title__ = \"packaging\"\n__summary__ = \"Core utilities for Python packages\"\n__uri__ = \"https://github.com/pypa/packaging\"\n\n__version__ = \"19.2\"\n\n__author__ = \"Donald Stufft and individual contributors\"\n__email__ = \"donald@stufft.io\"\n\n__license__ = \"BSD or Apache License, Version 2.0\"\n__copyright__ = \"Copyright 2014-2019 %s\" % __author__\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/packaging/__init__.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nfrom .__about__ import (\n    __author__,\n    __copyright__,\n    __email__,\n    __license__,\n    __summary__,\n    __title__,\n    __uri__,\n    __version__,\n)\n\n__all__ = [\n    \"__title__\",\n    \"__summary__\",\n    \"__uri__\",\n    \"__version__\",\n    \"__author__\",\n    \"__email__\",\n    \"__license__\",\n    \"__copyright__\",\n]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/packaging/_compat.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nimport sys\n\n\nPY2 = sys.version_info[0] == 2\nPY3 = sys.version_info[0] == 3\n\n# flake8: noqa\n\nif PY3:\n    string_types = (str,)\nelse:\n    string_types = (basestring,)\n\n\ndef with_metaclass(meta, *bases):\n    \"\"\"\n    Create a base class with a metaclass.\n    \"\"\"\n    # This requires a bit of explanation: the basic idea is to make a dummy\n    # metaclass for one level of class instantiation that replaces itself with\n    # the actual metaclass.\n    class metaclass(meta):\n        def __new__(cls, name, this_bases, d):\n            return meta(name, bases, d)\n\n    return type.__new__(metaclass, \"temporary_class\", (), {})\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/packaging/_structures.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\n\nclass Infinity(object):\n    def __repr__(self):\n        return \"Infinity\"\n\n    def __hash__(self):\n        return hash(repr(self))\n\n    def __lt__(self, other):\n        return False\n\n    def __le__(self, other):\n        return False\n\n    def __eq__(self, other):\n        return isinstance(other, self.__class__)\n\n    def __ne__(self, other):\n        return not isinstance(other, self.__class__)\n\n    def __gt__(self, other):\n        return True\n\n    def __ge__(self, other):\n        return True\n\n    def __neg__(self):\n        return NegativeInfinity\n\n\nInfinity = Infinity()\n\n\nclass NegativeInfinity(object):\n    def __repr__(self):\n        return \"-Infinity\"\n\n    def __hash__(self):\n        return hash(repr(self))\n\n    def __lt__(self, other):\n        return True\n\n    def __le__(self, other):\n        return True\n\n    def __eq__(self, other):\n        return isinstance(other, self.__class__)\n\n    def __ne__(self, other):\n        return not isinstance(other, self.__class__)\n\n    def __gt__(self, other):\n        return False\n\n    def __ge__(self, other):\n        return False\n\n    def __neg__(self):\n        return Infinity\n\n\nNegativeInfinity = NegativeInfinity()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/packaging/markers.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nimport operator\nimport os\nimport platform\nimport sys\n\nfrom setuptools.extern.pyparsing import ParseException, ParseResults, stringStart, stringEnd\nfrom setuptools.extern.pyparsing import ZeroOrMore, Group, Forward, QuotedString\nfrom setuptools.extern.pyparsing import Literal as L  # noqa\n\nfrom ._compat import string_types\nfrom .specifiers import Specifier, InvalidSpecifier\n\n\n__all__ = [\n    \"InvalidMarker\",\n    \"UndefinedComparison\",\n    \"UndefinedEnvironmentName\",\n    \"Marker\",\n    \"default_environment\",\n]\n\n\nclass InvalidMarker(ValueError):\n    \"\"\"\n    An invalid marker was found, users should refer to PEP 508.\n    \"\"\"\n\n\nclass UndefinedComparison(ValueError):\n    \"\"\"\n    An invalid operation was attempted on a value that doesn't support it.\n    \"\"\"\n\n\nclass UndefinedEnvironmentName(ValueError):\n    \"\"\"\n    A name was attempted to be used that does not exist inside of the\n    environment.\n    \"\"\"\n\n\nclass Node(object):\n    def __init__(self, value):\n        self.value = value\n\n    def __str__(self):\n        return str(self.value)\n\n    def __repr__(self):\n        return \"<{0}({1!r})>\".format(self.__class__.__name__, str(self))\n\n    def serialize(self):\n        raise NotImplementedError\n\n\nclass Variable(Node):\n    def serialize(self):\n        return str(self)\n\n\nclass Value(Node):\n    def serialize(self):\n        return '\"{0}\"'.format(self)\n\n\nclass Op(Node):\n    def serialize(self):\n        return str(self)\n\n\nVARIABLE = (\n    L(\"implementation_version\")\n    | L(\"platform_python_implementation\")\n    | L(\"implementation_name\")\n    | L(\"python_full_version\")\n    | L(\"platform_release\")\n    | L(\"platform_version\")\n    | L(\"platform_machine\")\n    | L(\"platform_system\")\n    | L(\"python_version\")\n    | L(\"sys_platform\")\n    | L(\"os_name\")\n    | L(\"os.name\")\n    | L(\"sys.platform\")  # PEP-345\n    | L(\"platform.version\")  # PEP-345\n    | L(\"platform.machine\")  # PEP-345\n    | L(\"platform.python_implementation\")  # PEP-345\n    | L(\"python_implementation\")  # PEP-345\n    | L(\"extra\")  # undocumented setuptools legacy\n)\nALIASES = {\n    \"os.name\": \"os_name\",\n    \"sys.platform\": \"sys_platform\",\n    \"platform.version\": \"platform_version\",\n    \"platform.machine\": \"platform_machine\",\n    \"platform.python_implementation\": \"platform_python_implementation\",\n    \"python_implementation\": \"platform_python_implementation\",\n}\nVARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0])))\n\nVERSION_CMP = (\n    L(\"===\") | L(\"==\") | L(\">=\") | L(\"<=\") | L(\"!=\") | L(\"~=\") | L(\">\") | L(\"<\")\n)\n\nMARKER_OP = VERSION_CMP | L(\"not in\") | L(\"in\")\nMARKER_OP.setParseAction(lambda s, l, t: Op(t[0]))\n\nMARKER_VALUE = QuotedString(\"'\") | QuotedString('\"')\nMARKER_VALUE.setParseAction(lambda s, l, t: Value(t[0]))\n\nBOOLOP = L(\"and\") | L(\"or\")\n\nMARKER_VAR = VARIABLE | MARKER_VALUE\n\nMARKER_ITEM = Group(MARKER_VAR + MARKER_OP + MARKER_VAR)\nMARKER_ITEM.setParseAction(lambda s, l, t: tuple(t[0]))\n\nLPAREN = L(\"(\").suppress()\nRPAREN = L(\")\").suppress()\n\nMARKER_EXPR = Forward()\nMARKER_ATOM = MARKER_ITEM | Group(LPAREN + MARKER_EXPR + RPAREN)\nMARKER_EXPR << MARKER_ATOM + ZeroOrMore(BOOLOP + MARKER_EXPR)\n\nMARKER = stringStart + MARKER_EXPR + stringEnd\n\n\ndef _coerce_parse_result(results):\n    if isinstance(results, ParseResults):\n        return [_coerce_parse_result(i) for i in results]\n    else:\n        return results\n\n\ndef _format_marker(marker, first=True):\n    assert isinstance(marker, (list, tuple, string_types))\n\n    # Sometimes we have a structure like [[...]] which is a single item list\n    # where the single item is itself it's own list. In that case we want skip\n    # the rest of this function so that we don't get extraneous () on the\n    # outside.\n    if (\n        isinstance(marker, list)\n        and len(marker) == 1\n        and isinstance(marker[0], (list, tuple))\n    ):\n        return _format_marker(marker[0])\n\n    if isinstance(marker, list):\n        inner = (_format_marker(m, first=False) for m in marker)\n        if first:\n            return \" \".join(inner)\n        else:\n            return \"(\" + \" \".join(inner) + \")\"\n    elif isinstance(marker, tuple):\n        return \" \".join([m.serialize() for m in marker])\n    else:\n        return marker\n\n\n_operators = {\n    \"in\": lambda lhs, rhs: lhs in rhs,\n    \"not in\": lambda lhs, rhs: lhs not in rhs,\n    \"<\": operator.lt,\n    \"<=\": operator.le,\n    \"==\": operator.eq,\n    \"!=\": operator.ne,\n    \">=\": operator.ge,\n    \">\": operator.gt,\n}\n\n\ndef _eval_op(lhs, op, rhs):\n    try:\n        spec = Specifier(\"\".join([op.serialize(), rhs]))\n    except InvalidSpecifier:\n        pass\n    else:\n        return spec.contains(lhs)\n\n    oper = _operators.get(op.serialize())\n    if oper is None:\n        raise UndefinedComparison(\n            \"Undefined {0!r} on {1!r} and {2!r}.\".format(op, lhs, rhs)\n        )\n\n    return oper(lhs, rhs)\n\n\n_undefined = object()\n\n\ndef _get_env(environment, name):\n    value = environment.get(name, _undefined)\n\n    if value is _undefined:\n        raise UndefinedEnvironmentName(\n            \"{0!r} does not exist in evaluation environment.\".format(name)\n        )\n\n    return value\n\n\ndef _evaluate_markers(markers, environment):\n    groups = [[]]\n\n    for marker in markers:\n        assert isinstance(marker, (list, tuple, string_types))\n\n        if isinstance(marker, list):\n            groups[-1].append(_evaluate_markers(marker, environment))\n        elif isinstance(marker, tuple):\n            lhs, op, rhs = marker\n\n            if isinstance(lhs, Variable):\n                lhs_value = _get_env(environment, lhs.value)\n                rhs_value = rhs.value\n            else:\n                lhs_value = lhs.value\n                rhs_value = _get_env(environment, rhs.value)\n\n            groups[-1].append(_eval_op(lhs_value, op, rhs_value))\n        else:\n            assert marker in [\"and\", \"or\"]\n            if marker == \"or\":\n                groups.append([])\n\n    return any(all(item) for item in groups)\n\n\ndef format_full_version(info):\n    version = \"{0.major}.{0.minor}.{0.micro}\".format(info)\n    kind = info.releaselevel\n    if kind != \"final\":\n        version += kind[0] + str(info.serial)\n    return version\n\n\ndef default_environment():\n    if hasattr(sys, \"implementation\"):\n        iver = format_full_version(sys.implementation.version)\n        implementation_name = sys.implementation.name\n    else:\n        iver = \"0\"\n        implementation_name = \"\"\n\n    return {\n        \"implementation_name\": implementation_name,\n        \"implementation_version\": iver,\n        \"os_name\": os.name,\n        \"platform_machine\": platform.machine(),\n        \"platform_release\": platform.release(),\n        \"platform_system\": platform.system(),\n        \"platform_version\": platform.version(),\n        \"python_full_version\": platform.python_version(),\n        \"platform_python_implementation\": platform.python_implementation(),\n        \"python_version\": \".\".join(platform.python_version_tuple()[:2]),\n        \"sys_platform\": sys.platform,\n    }\n\n\nclass Marker(object):\n    def __init__(self, marker):\n        try:\n            self._markers = _coerce_parse_result(MARKER.parseString(marker))\n        except ParseException as e:\n            err_str = \"Invalid marker: {0!r}, parse error at {1!r}\".format(\n                marker, marker[e.loc : e.loc + 8]\n            )\n            raise InvalidMarker(err_str)\n\n    def __str__(self):\n        return _format_marker(self._markers)\n\n    def __repr__(self):\n        return \"<Marker({0!r})>\".format(str(self))\n\n    def evaluate(self, environment=None):\n        \"\"\"Evaluate a marker.\n\n        Return the boolean from evaluating the given marker against the\n        environment. environment is an optional argument to override all or\n        part of the determined environment.\n\n        The environment is determined from the current Python process.\n        \"\"\"\n        current_environment = default_environment()\n        if environment is not None:\n            current_environment.update(environment)\n\n        return _evaluate_markers(self._markers, current_environment)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/packaging/requirements.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nimport string\nimport re\n\nfrom setuptools.extern.pyparsing import stringStart, stringEnd, originalTextFor, ParseException\nfrom setuptools.extern.pyparsing import ZeroOrMore, Word, Optional, Regex, Combine\nfrom setuptools.extern.pyparsing import Literal as L  # noqa\nfrom setuptools.extern.six.moves.urllib import parse as urlparse\n\nfrom .markers import MARKER_EXPR, Marker\nfrom .specifiers import LegacySpecifier, Specifier, SpecifierSet\n\n\nclass InvalidRequirement(ValueError):\n    \"\"\"\n    An invalid requirement was found, users should refer to PEP 508.\n    \"\"\"\n\n\nALPHANUM = Word(string.ascii_letters + string.digits)\n\nLBRACKET = L(\"[\").suppress()\nRBRACKET = L(\"]\").suppress()\nLPAREN = L(\"(\").suppress()\nRPAREN = L(\")\").suppress()\nCOMMA = L(\",\").suppress()\nSEMICOLON = L(\";\").suppress()\nAT = L(\"@\").suppress()\n\nPUNCTUATION = Word(\"-_.\")\nIDENTIFIER_END = ALPHANUM | (ZeroOrMore(PUNCTUATION) + ALPHANUM)\nIDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END))\n\nNAME = IDENTIFIER(\"name\")\nEXTRA = IDENTIFIER\n\nURI = Regex(r\"[^ ]+\")(\"url\")\nURL = AT + URI\n\nEXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA)\nEXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)(\"extras\")\n\nVERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE)\nVERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE)\n\nVERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY\nVERSION_MANY = Combine(\n    VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), joinString=\",\", adjacent=False\n)(\"_raw_spec\")\n_VERSION_SPEC = Optional(((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY))\n_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or \"\")\n\nVERSION_SPEC = originalTextFor(_VERSION_SPEC)(\"specifier\")\nVERSION_SPEC.setParseAction(lambda s, l, t: t[1])\n\nMARKER_EXPR = originalTextFor(MARKER_EXPR())(\"marker\")\nMARKER_EXPR.setParseAction(\n    lambda s, l, t: Marker(s[t._original_start : t._original_end])\n)\nMARKER_SEPARATOR = SEMICOLON\nMARKER = MARKER_SEPARATOR + MARKER_EXPR\n\nVERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER)\nURL_AND_MARKER = URL + Optional(MARKER)\n\nNAMED_REQUIREMENT = NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER)\n\nREQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd\n# setuptools.extern.pyparsing isn't thread safe during initialization, so we do it eagerly, see\n# issue #104\nREQUIREMENT.parseString(\"x[]\")\n\n\nclass Requirement(object):\n    \"\"\"Parse a requirement.\n\n    Parse a given requirement string into its parts, such as name, specifier,\n    URL, and extras. Raises InvalidRequirement on a badly-formed requirement\n    string.\n    \"\"\"\n\n    # TODO: Can we test whether something is contained within a requirement?\n    #       If so how do we do that? Do we need to test against the _name_ of\n    #       the thing as well as the version? What about the markers?\n    # TODO: Can we normalize the name and extra name?\n\n    def __init__(self, requirement_string):\n        try:\n            req = REQUIREMENT.parseString(requirement_string)\n        except ParseException as e:\n            raise InvalidRequirement(\n                'Parse error at \"{0!r}\": {1}'.format(\n                    requirement_string[e.loc : e.loc + 8], e.msg\n                )\n            )\n\n        self.name = req.name\n        if req.url:\n            parsed_url = urlparse.urlparse(req.url)\n            if parsed_url.scheme == \"file\":\n                if urlparse.urlunparse(parsed_url) != req.url:\n                    raise InvalidRequirement(\"Invalid URL given\")\n            elif not (parsed_url.scheme and parsed_url.netloc) or (\n                not parsed_url.scheme and not parsed_url.netloc\n            ):\n                raise InvalidRequirement(\"Invalid URL: {0}\".format(req.url))\n            self.url = req.url\n        else:\n            self.url = None\n        self.extras = set(req.extras.asList() if req.extras else [])\n        self.specifier = SpecifierSet(req.specifier)\n        self.marker = req.marker if req.marker else None\n\n    def __str__(self):\n        parts = [self.name]\n\n        if self.extras:\n            parts.append(\"[{0}]\".format(\",\".join(sorted(self.extras))))\n\n        if self.specifier:\n            parts.append(str(self.specifier))\n\n        if self.url:\n            parts.append(\"@ {0}\".format(self.url))\n            if self.marker:\n                parts.append(\" \")\n\n        if self.marker:\n            parts.append(\"; {0}\".format(self.marker))\n\n        return \"\".join(parts)\n\n    def __repr__(self):\n        return \"<Requirement({0!r})>\".format(str(self))\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/packaging/specifiers.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nimport abc\nimport functools\nimport itertools\nimport re\n\nfrom ._compat import string_types, with_metaclass\nfrom .version import Version, LegacyVersion, parse\n\n\nclass InvalidSpecifier(ValueError):\n    \"\"\"\n    An invalid specifier was found, users should refer to PEP 440.\n    \"\"\"\n\n\nclass BaseSpecifier(with_metaclass(abc.ABCMeta, object)):\n    @abc.abstractmethod\n    def __str__(self):\n        \"\"\"\n        Returns the str representation of this Specifier like object. This\n        should be representative of the Specifier itself.\n        \"\"\"\n\n    @abc.abstractmethod\n    def __hash__(self):\n        \"\"\"\n        Returns a hash value for this Specifier like object.\n        \"\"\"\n\n    @abc.abstractmethod\n    def __eq__(self, other):\n        \"\"\"\n        Returns a boolean representing whether or not the two Specifier like\n        objects are equal.\n        \"\"\"\n\n    @abc.abstractmethod\n    def __ne__(self, other):\n        \"\"\"\n        Returns a boolean representing whether or not the two Specifier like\n        objects are not equal.\n        \"\"\"\n\n    @abc.abstractproperty\n    def prereleases(self):\n        \"\"\"\n        Returns whether or not pre-releases as a whole are allowed by this\n        specifier.\n        \"\"\"\n\n    @prereleases.setter\n    def prereleases(self, value):\n        \"\"\"\n        Sets whether or not pre-releases as a whole are allowed by this\n        specifier.\n        \"\"\"\n\n    @abc.abstractmethod\n    def contains(self, item, prereleases=None):\n        \"\"\"\n        Determines if the given item is contained within this specifier.\n        \"\"\"\n\n    @abc.abstractmethod\n    def filter(self, iterable, prereleases=None):\n        \"\"\"\n        Takes an iterable of items and filters them so that only items which\n        are contained within this specifier are allowed in it.\n        \"\"\"\n\n\nclass _IndividualSpecifier(BaseSpecifier):\n\n    _operators = {}\n\n    def __init__(self, spec=\"\", prereleases=None):\n        match = self._regex.search(spec)\n        if not match:\n            raise InvalidSpecifier(\"Invalid specifier: '{0}'\".format(spec))\n\n        self._spec = (match.group(\"operator\").strip(), match.group(\"version\").strip())\n\n        # Store whether or not this Specifier should accept prereleases\n        self._prereleases = prereleases\n\n    def __repr__(self):\n        pre = (\n            \", prereleases={0!r}\".format(self.prereleases)\n            if self._prereleases is not None\n            else \"\"\n        )\n\n        return \"<{0}({1!r}{2})>\".format(self.__class__.__name__, str(self), pre)\n\n    def __str__(self):\n        return \"{0}{1}\".format(*self._spec)\n\n    def __hash__(self):\n        return hash(self._spec)\n\n    def __eq__(self, other):\n        if isinstance(other, string_types):\n            try:\n                other = self.__class__(other)\n            except InvalidSpecifier:\n                return NotImplemented\n        elif not isinstance(other, self.__class__):\n            return NotImplemented\n\n        return self._spec == other._spec\n\n    def __ne__(self, other):\n        if isinstance(other, string_types):\n            try:\n                other = self.__class__(other)\n            except InvalidSpecifier:\n                return NotImplemented\n        elif not isinstance(other, self.__class__):\n            return NotImplemented\n\n        return self._spec != other._spec\n\n    def _get_operator(self, op):\n        return getattr(self, \"_compare_{0}\".format(self._operators[op]))\n\n    def _coerce_version(self, version):\n        if not isinstance(version, (LegacyVersion, Version)):\n            version = parse(version)\n        return version\n\n    @property\n    def operator(self):\n        return self._spec[0]\n\n    @property\n    def version(self):\n        return self._spec[1]\n\n    @property\n    def prereleases(self):\n        return self._prereleases\n\n    @prereleases.setter\n    def prereleases(self, value):\n        self._prereleases = value\n\n    def __contains__(self, item):\n        return self.contains(item)\n\n    def contains(self, item, prereleases=None):\n        # Determine if prereleases are to be allowed or not.\n        if prereleases is None:\n            prereleases = self.prereleases\n\n        # Normalize item to a Version or LegacyVersion, this allows us to have\n        # a shortcut for ``\"2.0\" in Specifier(\">=2\")\n        item = self._coerce_version(item)\n\n        # Determine if we should be supporting prereleases in this specifier\n        # or not, if we do not support prereleases than we can short circuit\n        # logic if this version is a prereleases.\n        if item.is_prerelease and not prereleases:\n            return False\n\n        # Actually do the comparison to determine if this item is contained\n        # within this Specifier or not.\n        return self._get_operator(self.operator)(item, self.version)\n\n    def filter(self, iterable, prereleases=None):\n        yielded = False\n        found_prereleases = []\n\n        kw = {\"prereleases\": prereleases if prereleases is not None else True}\n\n        # Attempt to iterate over all the values in the iterable and if any of\n        # them match, yield them.\n        for version in iterable:\n            parsed_version = self._coerce_version(version)\n\n            if self.contains(parsed_version, **kw):\n                # If our version is a prerelease, and we were not set to allow\n                # prereleases, then we'll store it for later incase nothing\n                # else matches this specifier.\n                if parsed_version.is_prerelease and not (\n                    prereleases or self.prereleases\n                ):\n                    found_prereleases.append(version)\n                # Either this is not a prerelease, or we should have been\n                # accepting prereleases from the beginning.\n                else:\n                    yielded = True\n                    yield version\n\n        # Now that we've iterated over everything, determine if we've yielded\n        # any values, and if we have not and we have any prereleases stored up\n        # then we will go ahead and yield the prereleases.\n        if not yielded and found_prereleases:\n            for version in found_prereleases:\n                yield version\n\n\nclass LegacySpecifier(_IndividualSpecifier):\n\n    _regex_str = r\"\"\"\n        (?P<operator>(==|!=|<=|>=|<|>))\n        \\s*\n        (?P<version>\n            [^,;\\s)]* # Since this is a \"legacy\" specifier, and the version\n                      # string can be just about anything, we match everything\n                      # except for whitespace, a semi-colon for marker support,\n                      # a closing paren since versions can be enclosed in\n                      # them, and a comma since it's a version separator.\n        )\n        \"\"\"\n\n    _regex = re.compile(r\"^\\s*\" + _regex_str + r\"\\s*$\", re.VERBOSE | re.IGNORECASE)\n\n    _operators = {\n        \"==\": \"equal\",\n        \"!=\": \"not_equal\",\n        \"<=\": \"less_than_equal\",\n        \">=\": \"greater_than_equal\",\n        \"<\": \"less_than\",\n        \">\": \"greater_than\",\n    }\n\n    def _coerce_version(self, version):\n        if not isinstance(version, LegacyVersion):\n            version = LegacyVersion(str(version))\n        return version\n\n    def _compare_equal(self, prospective, spec):\n        return prospective == self._coerce_version(spec)\n\n    def _compare_not_equal(self, prospective, spec):\n        return prospective != self._coerce_version(spec)\n\n    def _compare_less_than_equal(self, prospective, spec):\n        return prospective <= self._coerce_version(spec)\n\n    def _compare_greater_than_equal(self, prospective, spec):\n        return prospective >= self._coerce_version(spec)\n\n    def _compare_less_than(self, prospective, spec):\n        return prospective < self._coerce_version(spec)\n\n    def _compare_greater_than(self, prospective, spec):\n        return prospective > self._coerce_version(spec)\n\n\ndef _require_version_compare(fn):\n    @functools.wraps(fn)\n    def wrapped(self, prospective, spec):\n        if not isinstance(prospective, Version):\n            return False\n        return fn(self, prospective, spec)\n\n    return wrapped\n\n\nclass Specifier(_IndividualSpecifier):\n\n    _regex_str = r\"\"\"\n        (?P<operator>(~=|==|!=|<=|>=|<|>|===))\n        (?P<version>\n            (?:\n                # The identity operators allow for an escape hatch that will\n                # do an exact string match of the version you wish to install.\n                # This will not be parsed by PEP 440 and we cannot determine\n                # any semantic meaning from it. This operator is discouraged\n                # but included entirely as an escape hatch.\n                (?<====)  # Only match for the identity operator\n                \\s*\n                [^\\s]*    # We just match everything, except for whitespace\n                          # since we are only testing for strict identity.\n            )\n            |\n            (?:\n                # The (non)equality operators allow for wild card and local\n                # versions to be specified so we have to define these two\n                # operators separately to enable that.\n                (?<===|!=)            # Only match for equals and not equals\n\n                \\s*\n                v?\n                (?:[0-9]+!)?          # epoch\n                [0-9]+(?:\\.[0-9]+)*   # release\n                (?:                   # pre release\n                    [-_\\.]?\n                    (a|b|c|rc|alpha|beta|pre|preview)\n                    [-_\\.]?\n                    [0-9]*\n                )?\n                (?:                   # post release\n                    (?:-[0-9]+)|(?:[-_\\.]?(post|rev|r)[-_\\.]?[0-9]*)\n                )?\n\n                # You cannot use a wild card and a dev or local version\n                # together so group them with a | and make them optional.\n                (?:\n                    (?:[-_\\.]?dev[-_\\.]?[0-9]*)?         # dev release\n                    (?:\\+[a-z0-9]+(?:[-_\\.][a-z0-9]+)*)? # local\n                    |\n                    \\.\\*  # Wild card syntax of .*\n                )?\n            )\n            |\n            (?:\n                # The compatible operator requires at least two digits in the\n                # release segment.\n                (?<=~=)               # Only match for the compatible operator\n\n                \\s*\n                v?\n                (?:[0-9]+!)?          # epoch\n                [0-9]+(?:\\.[0-9]+)+   # release  (We have a + instead of a *)\n                (?:                   # pre release\n                    [-_\\.]?\n                    (a|b|c|rc|alpha|beta|pre|preview)\n                    [-_\\.]?\n                    [0-9]*\n                )?\n                (?:                                   # post release\n                    (?:-[0-9]+)|(?:[-_\\.]?(post|rev|r)[-_\\.]?[0-9]*)\n                )?\n                (?:[-_\\.]?dev[-_\\.]?[0-9]*)?          # dev release\n            )\n            |\n            (?:\n                # All other operators only allow a sub set of what the\n                # (non)equality operators do. Specifically they do not allow\n                # local versions to be specified nor do they allow the prefix\n                # matching wild cards.\n                (?<!==|!=|~=)         # We have special cases for these\n                                      # operators so we want to make sure they\n                                      # don't match here.\n\n                \\s*\n                v?\n                (?:[0-9]+!)?          # epoch\n                [0-9]+(?:\\.[0-9]+)*   # release\n                (?:                   # pre release\n                    [-_\\.]?\n                    (a|b|c|rc|alpha|beta|pre|preview)\n                    [-_\\.]?\n                    [0-9]*\n                )?\n                (?:                                   # post release\n                    (?:-[0-9]+)|(?:[-_\\.]?(post|rev|r)[-_\\.]?[0-9]*)\n                )?\n                (?:[-_\\.]?dev[-_\\.]?[0-9]*)?          # dev release\n            )\n        )\n        \"\"\"\n\n    _regex = re.compile(r\"^\\s*\" + _regex_str + r\"\\s*$\", re.VERBOSE | re.IGNORECASE)\n\n    _operators = {\n        \"~=\": \"compatible\",\n        \"==\": \"equal\",\n        \"!=\": \"not_equal\",\n        \"<=\": \"less_than_equal\",\n        \">=\": \"greater_than_equal\",\n        \"<\": \"less_than\",\n        \">\": \"greater_than\",\n        \"===\": \"arbitrary\",\n    }\n\n    @_require_version_compare\n    def _compare_compatible(self, prospective, spec):\n        # Compatible releases have an equivalent combination of >= and ==. That\n        # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to\n        # implement this in terms of the other specifiers instead of\n        # implementing it ourselves. The only thing we need to do is construct\n        # the other specifiers.\n\n        # We want everything but the last item in the version, but we want to\n        # ignore post and dev releases and we want to treat the pre-release as\n        # it's own separate segment.\n        prefix = \".\".join(\n            list(\n                itertools.takewhile(\n                    lambda x: (not x.startswith(\"post\") and not x.startswith(\"dev\")),\n                    _version_split(spec),\n                )\n            )[:-1]\n        )\n\n        # Add the prefix notation to the end of our string\n        prefix += \".*\"\n\n        return self._get_operator(\">=\")(prospective, spec) and self._get_operator(\"==\")(\n            prospective, prefix\n        )\n\n    @_require_version_compare\n    def _compare_equal(self, prospective, spec):\n        # We need special logic to handle prefix matching\n        if spec.endswith(\".*\"):\n            # In the case of prefix matching we want to ignore local segment.\n            prospective = Version(prospective.public)\n            # Split the spec out by dots, and pretend that there is an implicit\n            # dot in between a release segment and a pre-release segment.\n            spec = _version_split(spec[:-2])  # Remove the trailing .*\n\n            # Split the prospective version out by dots, and pretend that there\n            # is an implicit dot in between a release segment and a pre-release\n            # segment.\n            prospective = _version_split(str(prospective))\n\n            # Shorten the prospective version to be the same length as the spec\n            # so that we can determine if the specifier is a prefix of the\n            # prospective version or not.\n            prospective = prospective[: len(spec)]\n\n            # Pad out our two sides with zeros so that they both equal the same\n            # length.\n            spec, prospective = _pad_version(spec, prospective)\n        else:\n            # Convert our spec string into a Version\n            spec = Version(spec)\n\n            # If the specifier does not have a local segment, then we want to\n            # act as if the prospective version also does not have a local\n            # segment.\n            if not spec.local:\n                prospective = Version(prospective.public)\n\n        return prospective == spec\n\n    @_require_version_compare\n    def _compare_not_equal(self, prospective, spec):\n        return not self._compare_equal(prospective, spec)\n\n    @_require_version_compare\n    def _compare_less_than_equal(self, prospective, spec):\n        return prospective <= Version(spec)\n\n    @_require_version_compare\n    def _compare_greater_than_equal(self, prospective, spec):\n        return prospective >= Version(spec)\n\n    @_require_version_compare\n    def _compare_less_than(self, prospective, spec):\n        # Convert our spec to a Version instance, since we'll want to work with\n        # it as a version.\n        spec = Version(spec)\n\n        # Check to see if the prospective version is less than the spec\n        # version. If it's not we can short circuit and just return False now\n        # instead of doing extra unneeded work.\n        if not prospective < spec:\n            return False\n\n        # This special case is here so that, unless the specifier itself\n        # includes is a pre-release version, that we do not accept pre-release\n        # versions for the version mentioned in the specifier (e.g. <3.1 should\n        # not match 3.1.dev0, but should match 3.0.dev0).\n        if not spec.is_prerelease and prospective.is_prerelease:\n            if Version(prospective.base_version) == Version(spec.base_version):\n                return False\n\n        # If we've gotten to here, it means that prospective version is both\n        # less than the spec version *and* it's not a pre-release of the same\n        # version in the spec.\n        return True\n\n    @_require_version_compare\n    def _compare_greater_than(self, prospective, spec):\n        # Convert our spec to a Version instance, since we'll want to work with\n        # it as a version.\n        spec = Version(spec)\n\n        # Check to see if the prospective version is greater than the spec\n        # version. If it's not we can short circuit and just return False now\n        # instead of doing extra unneeded work.\n        if not prospective > spec:\n            return False\n\n        # This special case is here so that, unless the specifier itself\n        # includes is a post-release version, that we do not accept\n        # post-release versions for the version mentioned in the specifier\n        # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0).\n        if not spec.is_postrelease and prospective.is_postrelease:\n            if Version(prospective.base_version) == Version(spec.base_version):\n                return False\n\n        # Ensure that we do not allow a local version of the version mentioned\n        # in the specifier, which is technically greater than, to match.\n        if prospective.local is not None:\n            if Version(prospective.base_version) == Version(spec.base_version):\n                return False\n\n        # If we've gotten to here, it means that prospective version is both\n        # greater than the spec version *and* it's not a pre-release of the\n        # same version in the spec.\n        return True\n\n    def _compare_arbitrary(self, prospective, spec):\n        return str(prospective).lower() == str(spec).lower()\n\n    @property\n    def prereleases(self):\n        # If there is an explicit prereleases set for this, then we'll just\n        # blindly use that.\n        if self._prereleases is not None:\n            return self._prereleases\n\n        # Look at all of our specifiers and determine if they are inclusive\n        # operators, and if they are if they are including an explicit\n        # prerelease.\n        operator, version = self._spec\n        if operator in [\"==\", \">=\", \"<=\", \"~=\", \"===\"]:\n            # The == specifier can include a trailing .*, if it does we\n            # want to remove before parsing.\n            if operator == \"==\" and version.endswith(\".*\"):\n                version = version[:-2]\n\n            # Parse the version, and if it is a pre-release than this\n            # specifier allows pre-releases.\n            if parse(version).is_prerelease:\n                return True\n\n        return False\n\n    @prereleases.setter\n    def prereleases(self, value):\n        self._prereleases = value\n\n\n_prefix_regex = re.compile(r\"^([0-9]+)((?:a|b|c|rc)[0-9]+)$\")\n\n\ndef _version_split(version):\n    result = []\n    for item in version.split(\".\"):\n        match = _prefix_regex.search(item)\n        if match:\n            result.extend(match.groups())\n        else:\n            result.append(item)\n    return result\n\n\ndef _pad_version(left, right):\n    left_split, right_split = [], []\n\n    # Get the release segment of our versions\n    left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left)))\n    right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))\n\n    # Get the rest of our versions\n    left_split.append(left[len(left_split[0]) :])\n    right_split.append(right[len(right_split[0]) :])\n\n    # Insert our padding\n    left_split.insert(1, [\"0\"] * max(0, len(right_split[0]) - len(left_split[0])))\n    right_split.insert(1, [\"0\"] * max(0, len(left_split[0]) - len(right_split[0])))\n\n    return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split)))\n\n\nclass SpecifierSet(BaseSpecifier):\n    def __init__(self, specifiers=\"\", prereleases=None):\n        # Split on , to break each indidivual specifier into it's own item, and\n        # strip each item to remove leading/trailing whitespace.\n        specifiers = [s.strip() for s in specifiers.split(\",\") if s.strip()]\n\n        # Parsed each individual specifier, attempting first to make it a\n        # Specifier and falling back to a LegacySpecifier.\n        parsed = set()\n        for specifier in specifiers:\n            try:\n                parsed.add(Specifier(specifier))\n            except InvalidSpecifier:\n                parsed.add(LegacySpecifier(specifier))\n\n        # Turn our parsed specifiers into a frozen set and save them for later.\n        self._specs = frozenset(parsed)\n\n        # Store our prereleases value so we can use it later to determine if\n        # we accept prereleases or not.\n        self._prereleases = prereleases\n\n    def __repr__(self):\n        pre = (\n            \", prereleases={0!r}\".format(self.prereleases)\n            if self._prereleases is not None\n            else \"\"\n        )\n\n        return \"<SpecifierSet({0!r}{1})>\".format(str(self), pre)\n\n    def __str__(self):\n        return \",\".join(sorted(str(s) for s in self._specs))\n\n    def __hash__(self):\n        return hash(self._specs)\n\n    def __and__(self, other):\n        if isinstance(other, string_types):\n            other = SpecifierSet(other)\n        elif not isinstance(other, SpecifierSet):\n            return NotImplemented\n\n        specifier = SpecifierSet()\n        specifier._specs = frozenset(self._specs | other._specs)\n\n        if self._prereleases is None and other._prereleases is not None:\n            specifier._prereleases = other._prereleases\n        elif self._prereleases is not None and other._prereleases is None:\n            specifier._prereleases = self._prereleases\n        elif self._prereleases == other._prereleases:\n            specifier._prereleases = self._prereleases\n        else:\n            raise ValueError(\n                \"Cannot combine SpecifierSets with True and False prerelease \"\n                \"overrides.\"\n            )\n\n        return specifier\n\n    def __eq__(self, other):\n        if isinstance(other, string_types):\n            other = SpecifierSet(other)\n        elif isinstance(other, _IndividualSpecifier):\n            other = SpecifierSet(str(other))\n        elif not isinstance(other, SpecifierSet):\n            return NotImplemented\n\n        return self._specs == other._specs\n\n    def __ne__(self, other):\n        if isinstance(other, string_types):\n            other = SpecifierSet(other)\n        elif isinstance(other, _IndividualSpecifier):\n            other = SpecifierSet(str(other))\n        elif not isinstance(other, SpecifierSet):\n            return NotImplemented\n\n        return self._specs != other._specs\n\n    def __len__(self):\n        return len(self._specs)\n\n    def __iter__(self):\n        return iter(self._specs)\n\n    @property\n    def prereleases(self):\n        # If we have been given an explicit prerelease modifier, then we'll\n        # pass that through here.\n        if self._prereleases is not None:\n            return self._prereleases\n\n        # If we don't have any specifiers, and we don't have a forced value,\n        # then we'll just return None since we don't know if this should have\n        # pre-releases or not.\n        if not self._specs:\n            return None\n\n        # Otherwise we'll see if any of the given specifiers accept\n        # prereleases, if any of them do we'll return True, otherwise False.\n        return any(s.prereleases for s in self._specs)\n\n    @prereleases.setter\n    def prereleases(self, value):\n        self._prereleases = value\n\n    def __contains__(self, item):\n        return self.contains(item)\n\n    def contains(self, item, prereleases=None):\n        # Ensure that our item is a Version or LegacyVersion instance.\n        if not isinstance(item, (LegacyVersion, Version)):\n            item = parse(item)\n\n        # Determine if we're forcing a prerelease or not, if we're not forcing\n        # one for this particular filter call, then we'll use whatever the\n        # SpecifierSet thinks for whether or not we should support prereleases.\n        if prereleases is None:\n            prereleases = self.prereleases\n\n        # We can determine if we're going to allow pre-releases by looking to\n        # see if any of the underlying items supports them. If none of them do\n        # and this item is a pre-release then we do not allow it and we can\n        # short circuit that here.\n        # Note: This means that 1.0.dev1 would not be contained in something\n        #       like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0\n        if not prereleases and item.is_prerelease:\n            return False\n\n        # We simply dispatch to the underlying specs here to make sure that the\n        # given version is contained within all of them.\n        # Note: This use of all() here means that an empty set of specifiers\n        #       will always return True, this is an explicit design decision.\n        return all(s.contains(item, prereleases=prereleases) for s in self._specs)\n\n    def filter(self, iterable, prereleases=None):\n        # Determine if we're forcing a prerelease or not, if we're not forcing\n        # one for this particular filter call, then we'll use whatever the\n        # SpecifierSet thinks for whether or not we should support prereleases.\n        if prereleases is None:\n            prereleases = self.prereleases\n\n        # If we have any specifiers, then we want to wrap our iterable in the\n        # filter method for each one, this will act as a logical AND amongst\n        # each specifier.\n        if self._specs:\n            for spec in self._specs:\n                iterable = spec.filter(iterable, prereleases=bool(prereleases))\n            return iterable\n        # If we do not have any specifiers, then we need to have a rough filter\n        # which will filter out any pre-releases, unless there are no final\n        # releases, and which will filter out LegacyVersion in general.\n        else:\n            filtered = []\n            found_prereleases = []\n\n            for item in iterable:\n                # Ensure that we some kind of Version class for this item.\n                if not isinstance(item, (LegacyVersion, Version)):\n                    parsed_version = parse(item)\n                else:\n                    parsed_version = item\n\n                # Filter out any item which is parsed as a LegacyVersion\n                if isinstance(parsed_version, LegacyVersion):\n                    continue\n\n                # Store any item which is a pre-release for later unless we've\n                # already found a final version or we are accepting prereleases\n                if parsed_version.is_prerelease and not prereleases:\n                    if not filtered:\n                        found_prereleases.append(item)\n                else:\n                    filtered.append(item)\n\n            # If we've found no items except for pre-releases, then we'll go\n            # ahead and use the pre-releases\n            if not filtered and found_prereleases and prereleases is None:\n                return found_prereleases\n\n            return filtered\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/packaging/tags.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\n\nfrom __future__ import absolute_import\n\nimport distutils.util\n\ntry:\n    from importlib.machinery import EXTENSION_SUFFIXES\nexcept ImportError:  # pragma: no cover\n    import imp\n\n    EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()]\n    del imp\nimport platform\nimport re\nimport sys\nimport sysconfig\nimport warnings\n\n\nINTERPRETER_SHORT_NAMES = {\n    \"python\": \"py\",  # Generic.\n    \"cpython\": \"cp\",\n    \"pypy\": \"pp\",\n    \"ironpython\": \"ip\",\n    \"jython\": \"jy\",\n}\n\n\n_32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32\n\n\nclass Tag(object):\n\n    __slots__ = [\"_interpreter\", \"_abi\", \"_platform\"]\n\n    def __init__(self, interpreter, abi, platform):\n        self._interpreter = interpreter.lower()\n        self._abi = abi.lower()\n        self._platform = platform.lower()\n\n    @property\n    def interpreter(self):\n        return self._interpreter\n\n    @property\n    def abi(self):\n        return self._abi\n\n    @property\n    def platform(self):\n        return self._platform\n\n    def __eq__(self, other):\n        return (\n            (self.platform == other.platform)\n            and (self.abi == other.abi)\n            and (self.interpreter == other.interpreter)\n        )\n\n    def __hash__(self):\n        return hash((self._interpreter, self._abi, self._platform))\n\n    def __str__(self):\n        return \"{}-{}-{}\".format(self._interpreter, self._abi, self._platform)\n\n    def __repr__(self):\n        return \"<{self} @ {self_id}>\".format(self=self, self_id=id(self))\n\n\ndef parse_tag(tag):\n    tags = set()\n    interpreters, abis, platforms = tag.split(\"-\")\n    for interpreter in interpreters.split(\".\"):\n        for abi in abis.split(\".\"):\n            for platform_ in platforms.split(\".\"):\n                tags.add(Tag(interpreter, abi, platform_))\n    return frozenset(tags)\n\n\ndef _normalize_string(string):\n    return string.replace(\".\", \"_\").replace(\"-\", \"_\")\n\n\ndef _cpython_interpreter(py_version):\n    # TODO: Is using py_version_nodot for interpreter version critical?\n    return \"cp{major}{minor}\".format(major=py_version[0], minor=py_version[1])\n\n\ndef _cpython_abis(py_version):\n    abis = []\n    version = \"{}{}\".format(*py_version[:2])\n    debug = pymalloc = ucs4 = \"\"\n    with_debug = sysconfig.get_config_var(\"Py_DEBUG\")\n    has_refcount = hasattr(sys, \"gettotalrefcount\")\n    # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled\n    # extension modules is the best option.\n    # https://github.com/pypa/pip/issues/3383#issuecomment-173267692\n    has_ext = \"_d.pyd\" in EXTENSION_SUFFIXES\n    if with_debug or (with_debug is None and (has_refcount or has_ext)):\n        debug = \"d\"\n    if py_version < (3, 8):\n        with_pymalloc = sysconfig.get_config_var(\"WITH_PYMALLOC\")\n        if with_pymalloc or with_pymalloc is None:\n            pymalloc = \"m\"\n        if py_version < (3, 3):\n            unicode_size = sysconfig.get_config_var(\"Py_UNICODE_SIZE\")\n            if unicode_size == 4 or (\n                unicode_size is None and sys.maxunicode == 0x10FFFF\n            ):\n                ucs4 = \"u\"\n    elif debug:\n        # Debug builds can also load \"normal\" extension modules.\n        # We can also assume no UCS-4 or pymalloc requirement.\n        abis.append(\"cp{version}\".format(version=version))\n    abis.insert(\n        0,\n        \"cp{version}{debug}{pymalloc}{ucs4}\".format(\n            version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4\n        ),\n    )\n    return abis\n\n\ndef _cpython_tags(py_version, interpreter, abis, platforms):\n    for abi in abis:\n        for platform_ in platforms:\n            yield Tag(interpreter, abi, platform_)\n    for tag in (Tag(interpreter, \"abi3\", platform_) for platform_ in platforms):\n        yield tag\n    for tag in (Tag(interpreter, \"none\", platform_) for platform_ in platforms):\n        yield tag\n    # PEP 384 was first implemented in Python 3.2.\n    for minor_version in range(py_version[1] - 1, 1, -1):\n        for platform_ in platforms:\n            interpreter = \"cp{major}{minor}\".format(\n                major=py_version[0], minor=minor_version\n            )\n            yield Tag(interpreter, \"abi3\", platform_)\n\n\ndef _pypy_interpreter():\n    return \"pp{py_major}{pypy_major}{pypy_minor}\".format(\n        py_major=sys.version_info[0],\n        pypy_major=sys.pypy_version_info.major,\n        pypy_minor=sys.pypy_version_info.minor,\n    )\n\n\ndef _generic_abi():\n    abi = sysconfig.get_config_var(\"SOABI\")\n    if abi:\n        return _normalize_string(abi)\n    else:\n        return \"none\"\n\n\ndef _pypy_tags(py_version, interpreter, abi, platforms):\n    for tag in (Tag(interpreter, abi, platform) for platform in platforms):\n        yield tag\n    for tag in (Tag(interpreter, \"none\", platform) for platform in platforms):\n        yield tag\n\n\ndef _generic_tags(interpreter, py_version, abi, platforms):\n    for tag in (Tag(interpreter, abi, platform) for platform in platforms):\n        yield tag\n    if abi != \"none\":\n        tags = (Tag(interpreter, \"none\", platform_) for platform_ in platforms)\n        for tag in tags:\n            yield tag\n\n\ndef _py_interpreter_range(py_version):\n    \"\"\"\n    Yield Python versions in descending order.\n\n    After the latest version, the major-only version will be yielded, and then\n    all following versions up to 'end'.\n    \"\"\"\n    yield \"py{major}{minor}\".format(major=py_version[0], minor=py_version[1])\n    yield \"py{major}\".format(major=py_version[0])\n    for minor in range(py_version[1] - 1, -1, -1):\n        yield \"py{major}{minor}\".format(major=py_version[0], minor=minor)\n\n\ndef _independent_tags(interpreter, py_version, platforms):\n    \"\"\"\n    Return the sequence of tags that are consistent across implementations.\n\n    The tags consist of:\n    - py*-none-<platform>\n    - <interpreter>-none-any\n    - py*-none-any\n    \"\"\"\n    for version in _py_interpreter_range(py_version):\n        for platform_ in platforms:\n            yield Tag(version, \"none\", platform_)\n    yield Tag(interpreter, \"none\", \"any\")\n    for version in _py_interpreter_range(py_version):\n        yield Tag(version, \"none\", \"any\")\n\n\ndef _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER):\n    if not is_32bit:\n        return arch\n\n    if arch.startswith(\"ppc\"):\n        return \"ppc\"\n\n    return \"i386\"\n\n\ndef _mac_binary_formats(version, cpu_arch):\n    formats = [cpu_arch]\n    if cpu_arch == \"x86_64\":\n        if version < (10, 4):\n            return []\n        formats.extend([\"intel\", \"fat64\", \"fat32\"])\n\n    elif cpu_arch == \"i386\":\n        if version < (10, 4):\n            return []\n        formats.extend([\"intel\", \"fat32\", \"fat\"])\n\n    elif cpu_arch == \"ppc64\":\n        # TODO: Need to care about 32-bit PPC for ppc64 through 10.2?\n        if version > (10, 5) or version < (10, 4):\n            return []\n        formats.append(\"fat64\")\n\n    elif cpu_arch == \"ppc\":\n        if version > (10, 6):\n            return []\n        formats.extend([\"fat32\", \"fat\"])\n\n    formats.append(\"universal\")\n    return formats\n\n\ndef _mac_platforms(version=None, arch=None):\n    version_str, _, cpu_arch = platform.mac_ver()\n    if version is None:\n        version = tuple(map(int, version_str.split(\".\")[:2]))\n    if arch is None:\n        arch = _mac_arch(cpu_arch)\n    platforms = []\n    for minor_version in range(version[1], -1, -1):\n        compat_version = version[0], minor_version\n        binary_formats = _mac_binary_formats(compat_version, arch)\n        for binary_format in binary_formats:\n            platforms.append(\n                \"macosx_{major}_{minor}_{binary_format}\".format(\n                    major=compat_version[0],\n                    minor=compat_version[1],\n                    binary_format=binary_format,\n                )\n            )\n    return platforms\n\n\n# From PEP 513.\ndef _is_manylinux_compatible(name, glibc_version):\n    # Check for presence of _manylinux module.\n    try:\n        import _manylinux\n\n        return bool(getattr(_manylinux, name + \"_compatible\"))\n    except (ImportError, AttributeError):\n        # Fall through to heuristic check below.\n        pass\n\n    return _have_compatible_glibc(*glibc_version)\n\n\ndef _glibc_version_string():\n    # Returns glibc version string, or None if not using glibc.\n    import ctypes\n\n    # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen\n    # manpage says, \"If filename is NULL, then the returned handle is for the\n    # main program\". This way we can let the linker do the work to figure out\n    # which libc our process is actually using.\n    process_namespace = ctypes.CDLL(None)\n    try:\n        gnu_get_libc_version = process_namespace.gnu_get_libc_version\n    except AttributeError:\n        # Symbol doesn't exist -> therefore, we are not linked to\n        # glibc.\n        return None\n\n    # Call gnu_get_libc_version, which returns a string like \"2.5\"\n    gnu_get_libc_version.restype = ctypes.c_char_p\n    version_str = gnu_get_libc_version()\n    # py2 / py3 compatibility:\n    if not isinstance(version_str, str):\n        version_str = version_str.decode(\"ascii\")\n\n    return version_str\n\n\n# Separated out from have_compatible_glibc for easier unit testing.\ndef _check_glibc_version(version_str, required_major, minimum_minor):\n    # Parse string and check against requested version.\n    #\n    # We use a regexp instead of str.split because we want to discard any\n    # random junk that might come after the minor version -- this might happen\n    # in patched/forked versions of glibc (e.g. Linaro's version of glibc\n    # uses version strings like \"2.20-2014.11\"). See gh-3588.\n    m = re.match(r\"(?P<major>[0-9]+)\\.(?P<minor>[0-9]+)\", version_str)\n    if not m:\n        warnings.warn(\n            \"Expected glibc version with 2 components major.minor,\"\n            \" got: %s\" % version_str,\n            RuntimeWarning,\n        )\n        return False\n    return (\n        int(m.group(\"major\")) == required_major\n        and int(m.group(\"minor\")) >= minimum_minor\n    )\n\n\ndef _have_compatible_glibc(required_major, minimum_minor):\n    version_str = _glibc_version_string()\n    if version_str is None:\n        return False\n    return _check_glibc_version(version_str, required_major, minimum_minor)\n\n\ndef _linux_platforms(is_32bit=_32_BIT_INTERPRETER):\n    linux = _normalize_string(distutils.util.get_platform())\n    if linux == \"linux_x86_64\" and is_32bit:\n        linux = \"linux_i686\"\n    manylinux_support = (\n        (\"manylinux2014\", (2, 17)),  # CentOS 7 w/ glibc 2.17 (PEP 599)\n        (\"manylinux2010\", (2, 12)),  # CentOS 6 w/ glibc 2.12 (PEP 571)\n        (\"manylinux1\", (2, 5)),  # CentOS 5 w/ glibc 2.5 (PEP 513)\n    )\n    manylinux_support_iter = iter(manylinux_support)\n    for name, glibc_version in manylinux_support_iter:\n        if _is_manylinux_compatible(name, glibc_version):\n            platforms = [linux.replace(\"linux\", name)]\n            break\n    else:\n        platforms = []\n    # Support for a later manylinux implies support for an earlier version.\n    platforms += [linux.replace(\"linux\", name) for name, _ in manylinux_support_iter]\n    platforms.append(linux)\n    return platforms\n\n\ndef _generic_platforms():\n    platform = _normalize_string(distutils.util.get_platform())\n    return [platform]\n\n\ndef _interpreter_name():\n    name = platform.python_implementation().lower()\n    return INTERPRETER_SHORT_NAMES.get(name) or name\n\n\ndef _generic_interpreter(name, py_version):\n    version = sysconfig.get_config_var(\"py_version_nodot\")\n    if not version:\n        version = \"\".join(map(str, py_version[:2]))\n    return \"{name}{version}\".format(name=name, version=version)\n\n\ndef sys_tags():\n    \"\"\"\n    Returns the sequence of tag triples for the running interpreter.\n\n    The order of the sequence corresponds to priority order for the\n    interpreter, from most to least important.\n    \"\"\"\n    py_version = sys.version_info[:2]\n    interpreter_name = _interpreter_name()\n    if platform.system() == \"Darwin\":\n        platforms = _mac_platforms()\n    elif platform.system() == \"Linux\":\n        platforms = _linux_platforms()\n    else:\n        platforms = _generic_platforms()\n\n    if interpreter_name == \"cp\":\n        interpreter = _cpython_interpreter(py_version)\n        abis = _cpython_abis(py_version)\n        for tag in _cpython_tags(py_version, interpreter, abis, platforms):\n            yield tag\n    elif interpreter_name == \"pp\":\n        interpreter = _pypy_interpreter()\n        abi = _generic_abi()\n        for tag in _pypy_tags(py_version, interpreter, abi, platforms):\n            yield tag\n    else:\n        interpreter = _generic_interpreter(interpreter_name, py_version)\n        abi = _generic_abi()\n        for tag in _generic_tags(interpreter, py_version, abi, platforms):\n            yield tag\n    for tag in _independent_tags(interpreter, py_version, platforms):\n        yield tag\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/packaging/utils.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nimport re\n\nfrom .version import InvalidVersion, Version\n\n\n_canonicalize_regex = re.compile(r\"[-_.]+\")\n\n\ndef canonicalize_name(name):\n    # This is taken from PEP 503.\n    return _canonicalize_regex.sub(\"-\", name).lower()\n\n\ndef canonicalize_version(version):\n    \"\"\"\n    This is very similar to Version.__str__, but has one subtle differences\n    with the way it handles the release segment.\n    \"\"\"\n\n    try:\n        version = Version(version)\n    except InvalidVersion:\n        # Legacy versions cannot be normalized\n        return version\n\n    parts = []\n\n    # Epoch\n    if version.epoch != 0:\n        parts.append(\"{0}!\".format(version.epoch))\n\n    # Release segment\n    # NB: This strips trailing '.0's to normalize\n    parts.append(re.sub(r\"(\\.0)+$\", \"\", \".\".join(str(x) for x in version.release)))\n\n    # Pre-release\n    if version.pre is not None:\n        parts.append(\"\".join(str(x) for x in version.pre))\n\n    # Post-release\n    if version.post is not None:\n        parts.append(\".post{0}\".format(version.post))\n\n    # Development release\n    if version.dev is not None:\n        parts.append(\".dev{0}\".format(version.dev))\n\n    # Local version segment\n    if version.local is not None:\n        parts.append(\"+{0}\".format(version.local))\n\n    return \"\".join(parts)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/packaging/version.py",
    "content": "# This file is dual licensed under the terms of the Apache License, Version\n# 2.0, and the BSD License. See the LICENSE file in the root of this repository\n# for complete details.\nfrom __future__ import absolute_import, division, print_function\n\nimport collections\nimport itertools\nimport re\n\nfrom ._structures import Infinity\n\n\n__all__ = [\"parse\", \"Version\", \"LegacyVersion\", \"InvalidVersion\", \"VERSION_PATTERN\"]\n\n\n_Version = collections.namedtuple(\n    \"_Version\", [\"epoch\", \"release\", \"dev\", \"pre\", \"post\", \"local\"]\n)\n\n\ndef parse(version):\n    \"\"\"\n    Parse the given version string and return either a :class:`Version` object\n    or a :class:`LegacyVersion` object depending on if the given version is\n    a valid PEP 440 version or a legacy version.\n    \"\"\"\n    try:\n        return Version(version)\n    except InvalidVersion:\n        return LegacyVersion(version)\n\n\nclass InvalidVersion(ValueError):\n    \"\"\"\n    An invalid version was found, users should refer to PEP 440.\n    \"\"\"\n\n\nclass _BaseVersion(object):\n    def __hash__(self):\n        return hash(self._key)\n\n    def __lt__(self, other):\n        return self._compare(other, lambda s, o: s < o)\n\n    def __le__(self, other):\n        return self._compare(other, lambda s, o: s <= o)\n\n    def __eq__(self, other):\n        return self._compare(other, lambda s, o: s == o)\n\n    def __ge__(self, other):\n        return self._compare(other, lambda s, o: s >= o)\n\n    def __gt__(self, other):\n        return self._compare(other, lambda s, o: s > o)\n\n    def __ne__(self, other):\n        return self._compare(other, lambda s, o: s != o)\n\n    def _compare(self, other, method):\n        if not isinstance(other, _BaseVersion):\n            return NotImplemented\n\n        return method(self._key, other._key)\n\n\nclass LegacyVersion(_BaseVersion):\n    def __init__(self, version):\n        self._version = str(version)\n        self._key = _legacy_cmpkey(self._version)\n\n    def __str__(self):\n        return self._version\n\n    def __repr__(self):\n        return \"<LegacyVersion({0})>\".format(repr(str(self)))\n\n    @property\n    def public(self):\n        return self._version\n\n    @property\n    def base_version(self):\n        return self._version\n\n    @property\n    def epoch(self):\n        return -1\n\n    @property\n    def release(self):\n        return None\n\n    @property\n    def pre(self):\n        return None\n\n    @property\n    def post(self):\n        return None\n\n    @property\n    def dev(self):\n        return None\n\n    @property\n    def local(self):\n        return None\n\n    @property\n    def is_prerelease(self):\n        return False\n\n    @property\n    def is_postrelease(self):\n        return False\n\n    @property\n    def is_devrelease(self):\n        return False\n\n\n_legacy_version_component_re = re.compile(r\"(\\d+ | [a-z]+ | \\.| -)\", re.VERBOSE)\n\n_legacy_version_replacement_map = {\n    \"pre\": \"c\",\n    \"preview\": \"c\",\n    \"-\": \"final-\",\n    \"rc\": \"c\",\n    \"dev\": \"@\",\n}\n\n\ndef _parse_version_parts(s):\n    for part in _legacy_version_component_re.split(s):\n        part = _legacy_version_replacement_map.get(part, part)\n\n        if not part or part == \".\":\n            continue\n\n        if part[:1] in \"0123456789\":\n            # pad for numeric comparison\n            yield part.zfill(8)\n        else:\n            yield \"*\" + part\n\n    # ensure that alpha/beta/candidate are before final\n    yield \"*final\"\n\n\ndef _legacy_cmpkey(version):\n    # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch\n    # greater than or equal to 0. This will effectively put the LegacyVersion,\n    # which uses the defacto standard originally implemented by setuptools,\n    # as before all PEP 440 versions.\n    epoch = -1\n\n    # This scheme is taken from pkg_resources.parse_version setuptools prior to\n    # it's adoption of the packaging library.\n    parts = []\n    for part in _parse_version_parts(version.lower()):\n        if part.startswith(\"*\"):\n            # remove \"-\" before a prerelease tag\n            if part < \"*final\":\n                while parts and parts[-1] == \"*final-\":\n                    parts.pop()\n\n            # remove trailing zeros from each series of numeric parts\n            while parts and parts[-1] == \"00000000\":\n                parts.pop()\n\n        parts.append(part)\n    parts = tuple(parts)\n\n    return epoch, parts\n\n\n# Deliberately not anchored to the start and end of the string, to make it\n# easier for 3rd party code to reuse\nVERSION_PATTERN = r\"\"\"\n    v?\n    (?:\n        (?:(?P<epoch>[0-9]+)!)?                           # epoch\n        (?P<release>[0-9]+(?:\\.[0-9]+)*)                  # release segment\n        (?P<pre>                                          # pre-release\n            [-_\\.]?\n            (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))\n            [-_\\.]?\n            (?P<pre_n>[0-9]+)?\n        )?\n        (?P<post>                                         # post release\n            (?:-(?P<post_n1>[0-9]+))\n            |\n            (?:\n                [-_\\.]?\n                (?P<post_l>post|rev|r)\n                [-_\\.]?\n                (?P<post_n2>[0-9]+)?\n            )\n        )?\n        (?P<dev>                                          # dev release\n            [-_\\.]?\n            (?P<dev_l>dev)\n            [-_\\.]?\n            (?P<dev_n>[0-9]+)?\n        )?\n    )\n    (?:\\+(?P<local>[a-z0-9]+(?:[-_\\.][a-z0-9]+)*))?       # local version\n\"\"\"\n\n\nclass Version(_BaseVersion):\n\n    _regex = re.compile(r\"^\\s*\" + VERSION_PATTERN + r\"\\s*$\", re.VERBOSE | re.IGNORECASE)\n\n    def __init__(self, version):\n        # Validate the version and parse it into pieces\n        match = self._regex.search(version)\n        if not match:\n            raise InvalidVersion(\"Invalid version: '{0}'\".format(version))\n\n        # Store the parsed out pieces of the version\n        self._version = _Version(\n            epoch=int(match.group(\"epoch\")) if match.group(\"epoch\") else 0,\n            release=tuple(int(i) for i in match.group(\"release\").split(\".\")),\n            pre=_parse_letter_version(match.group(\"pre_l\"), match.group(\"pre_n\")),\n            post=_parse_letter_version(\n                match.group(\"post_l\"), match.group(\"post_n1\") or match.group(\"post_n2\")\n            ),\n            dev=_parse_letter_version(match.group(\"dev_l\"), match.group(\"dev_n\")),\n            local=_parse_local_version(match.group(\"local\")),\n        )\n\n        # Generate a key which will be used for sorting\n        self._key = _cmpkey(\n            self._version.epoch,\n            self._version.release,\n            self._version.pre,\n            self._version.post,\n            self._version.dev,\n            self._version.local,\n        )\n\n    def __repr__(self):\n        return \"<Version({0})>\".format(repr(str(self)))\n\n    def __str__(self):\n        parts = []\n\n        # Epoch\n        if self.epoch != 0:\n            parts.append(\"{0}!\".format(self.epoch))\n\n        # Release segment\n        parts.append(\".\".join(str(x) for x in self.release))\n\n        # Pre-release\n        if self.pre is not None:\n            parts.append(\"\".join(str(x) for x in self.pre))\n\n        # Post-release\n        if self.post is not None:\n            parts.append(\".post{0}\".format(self.post))\n\n        # Development release\n        if self.dev is not None:\n            parts.append(\".dev{0}\".format(self.dev))\n\n        # Local version segment\n        if self.local is not None:\n            parts.append(\"+{0}\".format(self.local))\n\n        return \"\".join(parts)\n\n    @property\n    def epoch(self):\n        return self._version.epoch\n\n    @property\n    def release(self):\n        return self._version.release\n\n    @property\n    def pre(self):\n        return self._version.pre\n\n    @property\n    def post(self):\n        return self._version.post[1] if self._version.post else None\n\n    @property\n    def dev(self):\n        return self._version.dev[1] if self._version.dev else None\n\n    @property\n    def local(self):\n        if self._version.local:\n            return \".\".join(str(x) for x in self._version.local)\n        else:\n            return None\n\n    @property\n    def public(self):\n        return str(self).split(\"+\", 1)[0]\n\n    @property\n    def base_version(self):\n        parts = []\n\n        # Epoch\n        if self.epoch != 0:\n            parts.append(\"{0}!\".format(self.epoch))\n\n        # Release segment\n        parts.append(\".\".join(str(x) for x in self.release))\n\n        return \"\".join(parts)\n\n    @property\n    def is_prerelease(self):\n        return self.dev is not None or self.pre is not None\n\n    @property\n    def is_postrelease(self):\n        return self.post is not None\n\n    @property\n    def is_devrelease(self):\n        return self.dev is not None\n\n\ndef _parse_letter_version(letter, number):\n    if letter:\n        # We consider there to be an implicit 0 in a pre-release if there is\n        # not a numeral associated with it.\n        if number is None:\n            number = 0\n\n        # We normalize any letters to their lower case form\n        letter = letter.lower()\n\n        # We consider some words to be alternate spellings of other words and\n        # in those cases we want to normalize the spellings to our preferred\n        # spelling.\n        if letter == \"alpha\":\n            letter = \"a\"\n        elif letter == \"beta\":\n            letter = \"b\"\n        elif letter in [\"c\", \"pre\", \"preview\"]:\n            letter = \"rc\"\n        elif letter in [\"rev\", \"r\"]:\n            letter = \"post\"\n\n        return letter, int(number)\n    if not letter and number:\n        # We assume if we are given a number, but we are not given a letter\n        # then this is using the implicit post release syntax (e.g. 1.0-1)\n        letter = \"post\"\n\n        return letter, int(number)\n\n\n_local_version_separators = re.compile(r\"[\\._-]\")\n\n\ndef _parse_local_version(local):\n    \"\"\"\n    Takes a string like abc.1.twelve and turns it into (\"abc\", 1, \"twelve\").\n    \"\"\"\n    if local is not None:\n        return tuple(\n            part.lower() if not part.isdigit() else int(part)\n            for part in _local_version_separators.split(local)\n        )\n\n\ndef _cmpkey(epoch, release, pre, post, dev, local):\n    # When we compare a release version, we want to compare it with all of the\n    # trailing zeros removed. So we'll use a reverse the list, drop all the now\n    # leading zeros until we come to something non zero, then take the rest\n    # re-reverse it back into the correct order and make it a tuple and use\n    # that for our sorting key.\n    release = tuple(\n        reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))\n    )\n\n    # We need to \"trick\" the sorting algorithm to put 1.0.dev0 before 1.0a0.\n    # We'll do this by abusing the pre segment, but we _only_ want to do this\n    # if there is not a pre or a post segment. If we have one of those then\n    # the normal sorting rules will handle this case correctly.\n    if pre is None and post is None and dev is not None:\n        pre = -Infinity\n    # Versions without a pre-release (except as noted above) should sort after\n    # those with one.\n    elif pre is None:\n        pre = Infinity\n\n    # Versions without a post segment should sort before those with one.\n    if post is None:\n        post = -Infinity\n\n    # Versions without a development segment should sort after those with one.\n    if dev is None:\n        dev = Infinity\n\n    if local is None:\n        # Versions without a local segment should sort before those with one.\n        local = -Infinity\n    else:\n        # Versions with a local segment need that segment parsed to implement\n        # the sorting rules in PEP440.\n        # - Alpha numeric segments sort before numeric segments\n        # - Alpha numeric segments sort lexicographically\n        # - Numeric segments sort numerically\n        # - Shorter versions sort before longer versions when the prefixes\n        #   match exactly\n        local = tuple((i, \"\") if isinstance(i, int) else (-Infinity, i) for i in local)\n\n    return epoch, release, pre, post, dev, local\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/pyparsing.py",
    "content": "# module pyparsing.py\n#\n# Copyright (c) 2003-2018  Paul T. McGuire\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\n# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\n# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n#\n\n__doc__ = \\\n\"\"\"\npyparsing module - Classes and methods to define and execute parsing grammars\n=============================================================================\n\nThe pyparsing module is an alternative approach to creating and executing simple grammars,\nvs. the traditional lex/yacc approach, or the use of regular expressions.  With pyparsing, you\ndon't need to learn a new syntax for defining grammars or matching expressions - the parsing module\nprovides a library of classes that you use to construct the grammar directly in Python.\n\nHere is a program to parse \"Hello, World!\" (or any greeting of the form \nC{\"<salutation>, <addressee>!\"}), built up using L{Word}, L{Literal}, and L{And} elements \n(L{'+'<ParserElement.__add__>} operator gives L{And} expressions, strings are auto-converted to\nL{Literal} expressions)::\n\n    from pyparsing import Word, alphas\n\n    # define grammar of a greeting\n    greet = Word(alphas) + \",\" + Word(alphas) + \"!\"\n\n    hello = \"Hello, World!\"\n    print (hello, \"->\", greet.parseString(hello))\n\nThe program outputs the following::\n\n    Hello, World! -> ['Hello', ',', 'World', '!']\n\nThe Python representation of the grammar is quite readable, owing to the self-explanatory\nclass names, and the use of '+', '|' and '^' operators.\n\nThe L{ParseResults} object returned from L{ParserElement.parseString<ParserElement.parseString>} can be accessed as a nested list, a dictionary, or an\nobject with named attributes.\n\nThe pyparsing module handles some of the problems that are typically vexing when writing text parsers:\n - extra or missing whitespace (the above program will also handle \"Hello,World!\", \"Hello  ,  World  !\", etc.)\n - quoted strings\n - embedded comments\n\n\nGetting Started -\n-----------------\nVisit the classes L{ParserElement} and L{ParseResults} to see the base classes that most other pyparsing\nclasses inherit from. Use the docstrings for examples of how to:\n - construct literal match expressions from L{Literal} and L{CaselessLiteral} classes\n - construct character word-group expressions using the L{Word} class\n - see how to create repetitive expressions using L{ZeroOrMore} and L{OneOrMore} classes\n - use L{'+'<And>}, L{'|'<MatchFirst>}, L{'^'<Or>}, and L{'&'<Each>} operators to combine simple expressions into more complex ones\n - associate names with your parsed results using L{ParserElement.setResultsName}\n - find some helpful expression short-cuts like L{delimitedList} and L{oneOf}\n - find more useful common expressions in the L{pyparsing_common} namespace class\n\"\"\"\n\n__version__ = \"2.2.1\"\n__versionTime__ = \"18 Sep 2018 00:49 UTC\"\n__author__ = \"Paul McGuire <ptmcg@users.sourceforge.net>\"\n\nimport string\nfrom weakref import ref as wkref\nimport copy\nimport sys\nimport warnings\nimport re\nimport sre_constants\nimport collections\nimport pprint\nimport traceback\nimport types\nfrom datetime import datetime\n\ntry:\n    from _thread import RLock\nexcept ImportError:\n    from threading import RLock\n\ntry:\n    # Python 3\n    from collections.abc import Iterable\n    from collections.abc import MutableMapping\nexcept ImportError:\n    # Python 2.7\n    from collections import Iterable\n    from collections import MutableMapping\n\ntry:\n    from collections import OrderedDict as _OrderedDict\nexcept ImportError:\n    try:\n        from ordereddict import OrderedDict as _OrderedDict\n    except ImportError:\n        _OrderedDict = None\n\n#~ sys.stderr.write( \"testing pyparsing module, version %s, %s\\n\" % (__version__,__versionTime__ ) )\n\n__all__ = [\n'And', 'CaselessKeyword', 'CaselessLiteral', 'CharsNotIn', 'Combine', 'Dict', 'Each', 'Empty',\n'FollowedBy', 'Forward', 'GoToColumn', 'Group', 'Keyword', 'LineEnd', 'LineStart', 'Literal',\n'MatchFirst', 'NoMatch', 'NotAny', 'OneOrMore', 'OnlyOnce', 'Optional', 'Or',\n'ParseBaseException', 'ParseElementEnhance', 'ParseException', 'ParseExpression', 'ParseFatalException',\n'ParseResults', 'ParseSyntaxException', 'ParserElement', 'QuotedString', 'RecursiveGrammarException',\n'Regex', 'SkipTo', 'StringEnd', 'StringStart', 'Suppress', 'Token', 'TokenConverter', \n'White', 'Word', 'WordEnd', 'WordStart', 'ZeroOrMore',\n'alphanums', 'alphas', 'alphas8bit', 'anyCloseTag', 'anyOpenTag', 'cStyleComment', 'col',\n'commaSeparatedList', 'commonHTMLEntity', 'countedArray', 'cppStyleComment', 'dblQuotedString',\n'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'hexnums',\n'htmlComment', 'javaStyleComment', 'line', 'lineEnd', 'lineStart', 'lineno',\n'makeHTMLTags', 'makeXMLTags', 'matchOnlyAtCol', 'matchPreviousExpr', 'matchPreviousLiteral',\n'nestedExpr', 'nullDebugAction', 'nums', 'oneOf', 'opAssoc', 'operatorPrecedence', 'printables',\n'punc8bit', 'pythonStyleComment', 'quotedString', 'removeQuotes', 'replaceHTMLEntity', \n'replaceWith', 'restOfLine', 'sglQuotedString', 'srange', 'stringEnd',\n'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute',\n'indentedBlock', 'originalTextFor', 'ungroup', 'infixNotation','locatedExpr', 'withClass',\n'CloseMatch', 'tokenMap', 'pyparsing_common',\n]\n\nsystem_version = tuple(sys.version_info)[:3]\nPY_3 = system_version[0] == 3\nif PY_3:\n    _MAX_INT = sys.maxsize\n    basestring = str\n    unichr = chr\n    _ustr = str\n\n    # build list of single arg builtins, that can be used as parse actions\n    singleArgBuiltins = [sum, len, sorted, reversed, list, tuple, set, any, all, min, max]\n\nelse:\n    _MAX_INT = sys.maxint\n    range = xrange\n\n    def _ustr(obj):\n        \"\"\"Drop-in replacement for str(obj) that tries to be Unicode friendly. It first tries\n           str(obj). If that fails with a UnicodeEncodeError, then it tries unicode(obj). It\n           then < returns the unicode object | encodes it with the default encoding | ... >.\n        \"\"\"\n        if isinstance(obj,unicode):\n            return obj\n\n        try:\n            # If this works, then _ustr(obj) has the same behaviour as str(obj), so\n            # it won't break any existing code.\n            return str(obj)\n\n        except UnicodeEncodeError:\n            # Else encode it\n            ret = unicode(obj).encode(sys.getdefaultencoding(), 'xmlcharrefreplace')\n            xmlcharref = Regex(r'&#\\d+;')\n            xmlcharref.setParseAction(lambda t: '\\\\u' + hex(int(t[0][2:-1]))[2:])\n            return xmlcharref.transformString(ret)\n\n    # build list of single arg builtins, tolerant of Python version, that can be used as parse actions\n    singleArgBuiltins = []\n    import __builtin__\n    for fname in \"sum len sorted reversed list tuple set any all min max\".split():\n        try:\n            singleArgBuiltins.append(getattr(__builtin__,fname))\n        except AttributeError:\n            continue\n            \n_generatorType = type((y for y in range(1)))\n \ndef _xml_escape(data):\n    \"\"\"Escape &, <, >, \", ', etc. in a string of data.\"\"\"\n\n    # ampersand must be replaced first\n    from_symbols = '&><\"\\''\n    to_symbols = ('&'+s+';' for s in \"amp gt lt quot apos\".split())\n    for from_,to_ in zip(from_symbols, to_symbols):\n        data = data.replace(from_, to_)\n    return data\n\nclass _Constants(object):\n    pass\n\nalphas     = string.ascii_uppercase + string.ascii_lowercase\nnums       = \"0123456789\"\nhexnums    = nums + \"ABCDEFabcdef\"\nalphanums  = alphas + nums\n_bslash    = chr(92)\nprintables = \"\".join(c for c in string.printable if c not in string.whitespace)\n\nclass ParseBaseException(Exception):\n    \"\"\"base exception class for all parsing runtime exceptions\"\"\"\n    # Performance tuning: we construct a *lot* of these, so keep this\n    # constructor as small and fast as possible\n    def __init__( self, pstr, loc=0, msg=None, elem=None ):\n        self.loc = loc\n        if msg is None:\n            self.msg = pstr\n            self.pstr = \"\"\n        else:\n            self.msg = msg\n            self.pstr = pstr\n        self.parserElement = elem\n        self.args = (pstr, loc, msg)\n\n    @classmethod\n    def _from_exception(cls, pe):\n        \"\"\"\n        internal factory method to simplify creating one type of ParseException \n        from another - avoids having __init__ signature conflicts among subclasses\n        \"\"\"\n        return cls(pe.pstr, pe.loc, pe.msg, pe.parserElement)\n\n    def __getattr__( self, aname ):\n        \"\"\"supported attributes by name are:\n            - lineno - returns the line number of the exception text\n            - col - returns the column number of the exception text\n            - line - returns the line containing the exception text\n        \"\"\"\n        if( aname == \"lineno\" ):\n            return lineno( self.loc, self.pstr )\n        elif( aname in (\"col\", \"column\") ):\n            return col( self.loc, self.pstr )\n        elif( aname == \"line\" ):\n            return line( self.loc, self.pstr )\n        else:\n            raise AttributeError(aname)\n\n    def __str__( self ):\n        return \"%s (at char %d), (line:%d, col:%d)\" % \\\n                ( self.msg, self.loc, self.lineno, self.column )\n    def __repr__( self ):\n        return _ustr(self)\n    def markInputline( self, markerString = \">!<\" ):\n        \"\"\"Extracts the exception line from the input string, and marks\n           the location of the exception with a special symbol.\n        \"\"\"\n        line_str = self.line\n        line_column = self.column - 1\n        if markerString:\n            line_str = \"\".join((line_str[:line_column],\n                                markerString, line_str[line_column:]))\n        return line_str.strip()\n    def __dir__(self):\n        return \"lineno col line\".split() + dir(type(self))\n\nclass ParseException(ParseBaseException):\n    \"\"\"\n    Exception thrown when parse expressions don't match class;\n    supported attributes by name are:\n     - lineno - returns the line number of the exception text\n     - col - returns the column number of the exception text\n     - line - returns the line containing the exception text\n        \n    Example::\n        try:\n            Word(nums).setName(\"integer\").parseString(\"ABC\")\n        except ParseException as pe:\n            print(pe)\n            print(\"column: {}\".format(pe.col))\n            \n    prints::\n       Expected integer (at char 0), (line:1, col:1)\n        column: 1\n    \"\"\"\n    pass\n\nclass ParseFatalException(ParseBaseException):\n    \"\"\"user-throwable exception thrown when inconsistent parse content\n       is found; stops all parsing immediately\"\"\"\n    pass\n\nclass ParseSyntaxException(ParseFatalException):\n    \"\"\"just like L{ParseFatalException}, but thrown internally when an\n       L{ErrorStop<And._ErrorStop>} ('-' operator) indicates that parsing is to stop \n       immediately because an unbacktrackable syntax error has been found\"\"\"\n    pass\n\n#~ class ReparseException(ParseBaseException):\n    #~ \"\"\"Experimental class - parse actions can raise this exception to cause\n       #~ pyparsing to reparse the input string:\n        #~ - with a modified input string, and/or\n        #~ - with a modified start location\n       #~ Set the values of the ReparseException in the constructor, and raise the\n       #~ exception in a parse action to cause pyparsing to use the new string/location.\n       #~ Setting the values as None causes no change to be made.\n       #~ \"\"\"\n    #~ def __init_( self, newstring, restartLoc ):\n        #~ self.newParseText = newstring\n        #~ self.reparseLoc = restartLoc\n\nclass RecursiveGrammarException(Exception):\n    \"\"\"exception thrown by L{ParserElement.validate} if the grammar could be improperly recursive\"\"\"\n    def __init__( self, parseElementList ):\n        self.parseElementTrace = parseElementList\n\n    def __str__( self ):\n        return \"RecursiveGrammarException: %s\" % self.parseElementTrace\n\nclass _ParseResultsWithOffset(object):\n    def __init__(self,p1,p2):\n        self.tup = (p1,p2)\n    def __getitem__(self,i):\n        return self.tup[i]\n    def __repr__(self):\n        return repr(self.tup[0])\n    def setOffset(self,i):\n        self.tup = (self.tup[0],i)\n\nclass ParseResults(object):\n    \"\"\"\n    Structured parse results, to provide multiple means of access to the parsed data:\n       - as a list (C{len(results)})\n       - by list index (C{results[0], results[1]}, etc.)\n       - by attribute (C{results.<resultsName>} - see L{ParserElement.setResultsName})\n\n    Example::\n        integer = Word(nums)\n        date_str = (integer.setResultsName(\"year\") + '/' \n                        + integer.setResultsName(\"month\") + '/' \n                        + integer.setResultsName(\"day\"))\n        # equivalent form:\n        # date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")\n\n        # parseString returns a ParseResults object\n        result = date_str.parseString(\"1999/12/31\")\n\n        def test(s, fn=repr):\n            print(\"%s -> %s\" % (s, fn(eval(s))))\n        test(\"list(result)\")\n        test(\"result[0]\")\n        test(\"result['month']\")\n        test(\"result.day\")\n        test(\"'month' in result\")\n        test(\"'minutes' in result\")\n        test(\"result.dump()\", str)\n    prints::\n        list(result) -> ['1999', '/', '12', '/', '31']\n        result[0] -> '1999'\n        result['month'] -> '12'\n        result.day -> '31'\n        'month' in result -> True\n        'minutes' in result -> False\n        result.dump() -> ['1999', '/', '12', '/', '31']\n        - day: 31\n        - month: 12\n        - year: 1999\n    \"\"\"\n    def __new__(cls, toklist=None, name=None, asList=True, modal=True ):\n        if isinstance(toklist, cls):\n            return toklist\n        retobj = object.__new__(cls)\n        retobj.__doinit = True\n        return retobj\n\n    # Performance tuning: we construct a *lot* of these, so keep this\n    # constructor as small and fast as possible\n    def __init__( self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance ):\n        if self.__doinit:\n            self.__doinit = False\n            self.__name = None\n            self.__parent = None\n            self.__accumNames = {}\n            self.__asList = asList\n            self.__modal = modal\n            if toklist is None:\n                toklist = []\n            if isinstance(toklist, list):\n                self.__toklist = toklist[:]\n            elif isinstance(toklist, _generatorType):\n                self.__toklist = list(toklist)\n            else:\n                self.__toklist = [toklist]\n            self.__tokdict = dict()\n\n        if name is not None and name:\n            if not modal:\n                self.__accumNames[name] = 0\n            if isinstance(name,int):\n                name = _ustr(name) # will always return a str, but use _ustr for consistency\n            self.__name = name\n            if not (isinstance(toklist, (type(None), basestring, list)) and toklist in (None,'',[])):\n                if isinstance(toklist,basestring):\n                    toklist = [ toklist ]\n                if asList:\n                    if isinstance(toklist,ParseResults):\n                        self[name] = _ParseResultsWithOffset(toklist.copy(),0)\n                    else:\n                        self[name] = _ParseResultsWithOffset(ParseResults(toklist[0]),0)\n                    self[name].__name = name\n                else:\n                    try:\n                        self[name] = toklist[0]\n                    except (KeyError,TypeError,IndexError):\n                        self[name] = toklist\n\n    def __getitem__( self, i ):\n        if isinstance( i, (int,slice) ):\n            return self.__toklist[i]\n        else:\n            if i not in self.__accumNames:\n                return self.__tokdict[i][-1][0]\n            else:\n                return ParseResults([ v[0] for v in self.__tokdict[i] ])\n\n    def __setitem__( self, k, v, isinstance=isinstance ):\n        if isinstance(v,_ParseResultsWithOffset):\n            self.__tokdict[k] = self.__tokdict.get(k,list()) + [v]\n            sub = v[0]\n        elif isinstance(k,(int,slice)):\n            self.__toklist[k] = v\n            sub = v\n        else:\n            self.__tokdict[k] = self.__tokdict.get(k,list()) + [_ParseResultsWithOffset(v,0)]\n            sub = v\n        if isinstance(sub,ParseResults):\n            sub.__parent = wkref(self)\n\n    def __delitem__( self, i ):\n        if isinstance(i,(int,slice)):\n            mylen = len( self.__toklist )\n            del self.__toklist[i]\n\n            # convert int to slice\n            if isinstance(i, int):\n                if i < 0:\n                    i += mylen\n                i = slice(i, i+1)\n            # get removed indices\n            removed = list(range(*i.indices(mylen)))\n            removed.reverse()\n            # fixup indices in token dictionary\n            for name,occurrences in self.__tokdict.items():\n                for j in removed:\n                    for k, (value, position) in enumerate(occurrences):\n                        occurrences[k] = _ParseResultsWithOffset(value, position - (position > j))\n        else:\n            del self.__tokdict[i]\n\n    def __contains__( self, k ):\n        return k in self.__tokdict\n\n    def __len__( self ): return len( self.__toklist )\n    def __bool__(self): return ( not not self.__toklist )\n    __nonzero__ = __bool__\n    def __iter__( self ): return iter( self.__toklist )\n    def __reversed__( self ): return iter( self.__toklist[::-1] )\n    def _iterkeys( self ):\n        if hasattr(self.__tokdict, \"iterkeys\"):\n            return self.__tokdict.iterkeys()\n        else:\n            return iter(self.__tokdict)\n\n    def _itervalues( self ):\n        return (self[k] for k in self._iterkeys())\n            \n    def _iteritems( self ):\n        return ((k, self[k]) for k in self._iterkeys())\n\n    if PY_3:\n        keys = _iterkeys       \n        \"\"\"Returns an iterator of all named result keys (Python 3.x only).\"\"\"\n\n        values = _itervalues\n        \"\"\"Returns an iterator of all named result values (Python 3.x only).\"\"\"\n\n        items = _iteritems\n        \"\"\"Returns an iterator of all named result key-value tuples (Python 3.x only).\"\"\"\n\n    else:\n        iterkeys = _iterkeys\n        \"\"\"Returns an iterator of all named result keys (Python 2.x only).\"\"\"\n\n        itervalues = _itervalues\n        \"\"\"Returns an iterator of all named result values (Python 2.x only).\"\"\"\n\n        iteritems = _iteritems\n        \"\"\"Returns an iterator of all named result key-value tuples (Python 2.x only).\"\"\"\n\n        def keys( self ):\n            \"\"\"Returns all named result keys (as a list in Python 2.x, as an iterator in Python 3.x).\"\"\"\n            return list(self.iterkeys())\n\n        def values( self ):\n            \"\"\"Returns all named result values (as a list in Python 2.x, as an iterator in Python 3.x).\"\"\"\n            return list(self.itervalues())\n                \n        def items( self ):\n            \"\"\"Returns all named result key-values (as a list of tuples in Python 2.x, as an iterator in Python 3.x).\"\"\"\n            return list(self.iteritems())\n\n    def haskeys( self ):\n        \"\"\"Since keys() returns an iterator, this method is helpful in bypassing\n           code that looks for the existence of any defined results names.\"\"\"\n        return bool(self.__tokdict)\n        \n    def pop( self, *args, **kwargs):\n        \"\"\"\n        Removes and returns item at specified index (default=C{last}).\n        Supports both C{list} and C{dict} semantics for C{pop()}. If passed no\n        argument or an integer argument, it will use C{list} semantics\n        and pop tokens from the list of parsed tokens. If passed a \n        non-integer argument (most likely a string), it will use C{dict}\n        semantics and pop the corresponding value from any defined \n        results names. A second default return value argument is \n        supported, just as in C{dict.pop()}.\n\n        Example::\n            def remove_first(tokens):\n                tokens.pop(0)\n            print(OneOrMore(Word(nums)).parseString(\"0 123 321\")) # -> ['0', '123', '321']\n            print(OneOrMore(Word(nums)).addParseAction(remove_first).parseString(\"0 123 321\")) # -> ['123', '321']\n\n            label = Word(alphas)\n            patt = label(\"LABEL\") + OneOrMore(Word(nums))\n            print(patt.parseString(\"AAB 123 321\").dump())\n\n            # Use pop() in a parse action to remove named result (note that corresponding value is not\n            # removed from list form of results)\n            def remove_LABEL(tokens):\n                tokens.pop(\"LABEL\")\n                return tokens\n            patt.addParseAction(remove_LABEL)\n            print(patt.parseString(\"AAB 123 321\").dump())\n        prints::\n            ['AAB', '123', '321']\n            - LABEL: AAB\n\n            ['AAB', '123', '321']\n        \"\"\"\n        if not args:\n            args = [-1]\n        for k,v in kwargs.items():\n            if k == 'default':\n                args = (args[0], v)\n            else:\n                raise TypeError(\"pop() got an unexpected keyword argument '%s'\" % k)\n        if (isinstance(args[0], int) or \n                        len(args) == 1 or \n                        args[0] in self):\n            index = args[0]\n            ret = self[index]\n            del self[index]\n            return ret\n        else:\n            defaultvalue = args[1]\n            return defaultvalue\n\n    def get(self, key, defaultValue=None):\n        \"\"\"\n        Returns named result matching the given key, or if there is no\n        such name, then returns the given C{defaultValue} or C{None} if no\n        C{defaultValue} is specified.\n\n        Similar to C{dict.get()}.\n        \n        Example::\n            integer = Word(nums)\n            date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")           \n\n            result = date_str.parseString(\"1999/12/31\")\n            print(result.get(\"year\")) # -> '1999'\n            print(result.get(\"hour\", \"not specified\")) # -> 'not specified'\n            print(result.get(\"hour\")) # -> None\n        \"\"\"\n        if key in self:\n            return self[key]\n        else:\n            return defaultValue\n\n    def insert( self, index, insStr ):\n        \"\"\"\n        Inserts new element at location index in the list of parsed tokens.\n        \n        Similar to C{list.insert()}.\n\n        Example::\n            print(OneOrMore(Word(nums)).parseString(\"0 123 321\")) # -> ['0', '123', '321']\n\n            # use a parse action to insert the parse location in the front of the parsed results\n            def insert_locn(locn, tokens):\n                tokens.insert(0, locn)\n            print(OneOrMore(Word(nums)).addParseAction(insert_locn).parseString(\"0 123 321\")) # -> [0, '0', '123', '321']\n        \"\"\"\n        self.__toklist.insert(index, insStr)\n        # fixup indices in token dictionary\n        for name,occurrences in self.__tokdict.items():\n            for k, (value, position) in enumerate(occurrences):\n                occurrences[k] = _ParseResultsWithOffset(value, position + (position > index))\n\n    def append( self, item ):\n        \"\"\"\n        Add single element to end of ParseResults list of elements.\n\n        Example::\n            print(OneOrMore(Word(nums)).parseString(\"0 123 321\")) # -> ['0', '123', '321']\n            \n            # use a parse action to compute the sum of the parsed integers, and add it to the end\n            def append_sum(tokens):\n                tokens.append(sum(map(int, tokens)))\n            print(OneOrMore(Word(nums)).addParseAction(append_sum).parseString(\"0 123 321\")) # -> ['0', '123', '321', 444]\n        \"\"\"\n        self.__toklist.append(item)\n\n    def extend( self, itemseq ):\n        \"\"\"\n        Add sequence of elements to end of ParseResults list of elements.\n\n        Example::\n            patt = OneOrMore(Word(alphas))\n            \n            # use a parse action to append the reverse of the matched strings, to make a palindrome\n            def make_palindrome(tokens):\n                tokens.extend(reversed([t[::-1] for t in tokens]))\n                return ''.join(tokens)\n            print(patt.addParseAction(make_palindrome).parseString(\"lskdj sdlkjf lksd\")) # -> 'lskdjsdlkjflksddsklfjkldsjdksl'\n        \"\"\"\n        if isinstance(itemseq, ParseResults):\n            self += itemseq\n        else:\n            self.__toklist.extend(itemseq)\n\n    def clear( self ):\n        \"\"\"\n        Clear all elements and results names.\n        \"\"\"\n        del self.__toklist[:]\n        self.__tokdict.clear()\n\n    def __getattr__( self, name ):\n        try:\n            return self[name]\n        except KeyError:\n            return \"\"\n            \n        if name in self.__tokdict:\n            if name not in self.__accumNames:\n                return self.__tokdict[name][-1][0]\n            else:\n                return ParseResults([ v[0] for v in self.__tokdict[name] ])\n        else:\n            return \"\"\n\n    def __add__( self, other ):\n        ret = self.copy()\n        ret += other\n        return ret\n\n    def __iadd__( self, other ):\n        if other.__tokdict:\n            offset = len(self.__toklist)\n            addoffset = lambda a: offset if a<0 else a+offset\n            otheritems = other.__tokdict.items()\n            otherdictitems = [(k, _ParseResultsWithOffset(v[0],addoffset(v[1])) )\n                                for (k,vlist) in otheritems for v in vlist]\n            for k,v in otherdictitems:\n                self[k] = v\n                if isinstance(v[0],ParseResults):\n                    v[0].__parent = wkref(self)\n            \n        self.__toklist += other.__toklist\n        self.__accumNames.update( other.__accumNames )\n        return self\n\n    def __radd__(self, other):\n        if isinstance(other,int) and other == 0:\n            # useful for merging many ParseResults using sum() builtin\n            return self.copy()\n        else:\n            # this may raise a TypeError - so be it\n            return other + self\n        \n    def __repr__( self ):\n        return \"(%s, %s)\" % ( repr( self.__toklist ), repr( self.__tokdict ) )\n\n    def __str__( self ):\n        return '[' + ', '.join(_ustr(i) if isinstance(i, ParseResults) else repr(i) for i in self.__toklist) + ']'\n\n    def _asStringList( self, sep='' ):\n        out = []\n        for item in self.__toklist:\n            if out and sep:\n                out.append(sep)\n            if isinstance( item, ParseResults ):\n                out += item._asStringList()\n            else:\n                out.append( _ustr(item) )\n        return out\n\n    def asList( self ):\n        \"\"\"\n        Returns the parse results as a nested list of matching tokens, all converted to strings.\n\n        Example::\n            patt = OneOrMore(Word(alphas))\n            result = patt.parseString(\"sldkj lsdkj sldkj\")\n            # even though the result prints in string-like form, it is actually a pyparsing ParseResults\n            print(type(result), result) # -> <class 'pyparsing.ParseResults'> ['sldkj', 'lsdkj', 'sldkj']\n            \n            # Use asList() to create an actual list\n            result_list = result.asList()\n            print(type(result_list), result_list) # -> <class 'list'> ['sldkj', 'lsdkj', 'sldkj']\n        \"\"\"\n        return [res.asList() if isinstance(res,ParseResults) else res for res in self.__toklist]\n\n    def asDict( self ):\n        \"\"\"\n        Returns the named parse results as a nested dictionary.\n\n        Example::\n            integer = Word(nums)\n            date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")\n            \n            result = date_str.parseString('12/31/1999')\n            print(type(result), repr(result)) # -> <class 'pyparsing.ParseResults'> (['12', '/', '31', '/', '1999'], {'day': [('1999', 4)], 'year': [('12', 0)], 'month': [('31', 2)]})\n            \n            result_dict = result.asDict()\n            print(type(result_dict), repr(result_dict)) # -> <class 'dict'> {'day': '1999', 'year': '12', 'month': '31'}\n\n            # even though a ParseResults supports dict-like access, sometime you just need to have a dict\n            import json\n            print(json.dumps(result)) # -> Exception: TypeError: ... is not JSON serializable\n            print(json.dumps(result.asDict())) # -> {\"month\": \"31\", \"day\": \"1999\", \"year\": \"12\"}\n        \"\"\"\n        if PY_3:\n            item_fn = self.items\n        else:\n            item_fn = self.iteritems\n            \n        def toItem(obj):\n            if isinstance(obj, ParseResults):\n                if obj.haskeys():\n                    return obj.asDict()\n                else:\n                    return [toItem(v) for v in obj]\n            else:\n                return obj\n                \n        return dict((k,toItem(v)) for k,v in item_fn())\n\n    def copy( self ):\n        \"\"\"\n        Returns a new copy of a C{ParseResults} object.\n        \"\"\"\n        ret = ParseResults( self.__toklist )\n        ret.__tokdict = self.__tokdict.copy()\n        ret.__parent = self.__parent\n        ret.__accumNames.update( self.__accumNames )\n        ret.__name = self.__name\n        return ret\n\n    def asXML( self, doctag=None, namedItemsOnly=False, indent=\"\", formatted=True ):\n        \"\"\"\n        (Deprecated) Returns the parse results as XML. Tags are created for tokens and lists that have defined results names.\n        \"\"\"\n        nl = \"\\n\"\n        out = []\n        namedItems = dict((v[1],k) for (k,vlist) in self.__tokdict.items()\n                                                            for v in vlist)\n        nextLevelIndent = indent + \"  \"\n\n        # collapse out indents if formatting is not desired\n        if not formatted:\n            indent = \"\"\n            nextLevelIndent = \"\"\n            nl = \"\"\n\n        selfTag = None\n        if doctag is not None:\n            selfTag = doctag\n        else:\n            if self.__name:\n                selfTag = self.__name\n\n        if not selfTag:\n            if namedItemsOnly:\n                return \"\"\n            else:\n                selfTag = \"ITEM\"\n\n        out += [ nl, indent, \"<\", selfTag, \">\" ]\n\n        for i,res in enumerate(self.__toklist):\n            if isinstance(res,ParseResults):\n                if i in namedItems:\n                    out += [ res.asXML(namedItems[i],\n                                        namedItemsOnly and doctag is None,\n                                        nextLevelIndent,\n                                        formatted)]\n                else:\n                    out += [ res.asXML(None,\n                                        namedItemsOnly and doctag is None,\n                                        nextLevelIndent,\n                                        formatted)]\n            else:\n                # individual token, see if there is a name for it\n                resTag = None\n                if i in namedItems:\n                    resTag = namedItems[i]\n                if not resTag:\n                    if namedItemsOnly:\n                        continue\n                    else:\n                        resTag = \"ITEM\"\n                xmlBodyText = _xml_escape(_ustr(res))\n                out += [ nl, nextLevelIndent, \"<\", resTag, \">\",\n                                                xmlBodyText,\n                                                \"</\", resTag, \">\" ]\n\n        out += [ nl, indent, \"</\", selfTag, \">\" ]\n        return \"\".join(out)\n\n    def __lookup(self,sub):\n        for k,vlist in self.__tokdict.items():\n            for v,loc in vlist:\n                if sub is v:\n                    return k\n        return None\n\n    def getName(self):\n        r\"\"\"\n        Returns the results name for this token expression. Useful when several \n        different expressions might match at a particular location.\n\n        Example::\n            integer = Word(nums)\n            ssn_expr = Regex(r\"\\d\\d\\d-\\d\\d-\\d\\d\\d\\d\")\n            house_number_expr = Suppress('#') + Word(nums, alphanums)\n            user_data = (Group(house_number_expr)(\"house_number\") \n                        | Group(ssn_expr)(\"ssn\")\n                        | Group(integer)(\"age\"))\n            user_info = OneOrMore(user_data)\n            \n            result = user_info.parseString(\"22 111-22-3333 #221B\")\n            for item in result:\n                print(item.getName(), ':', item[0])\n        prints::\n            age : 22\n            ssn : 111-22-3333\n            house_number : 221B\n        \"\"\"\n        if self.__name:\n            return self.__name\n        elif self.__parent:\n            par = self.__parent()\n            if par:\n                return par.__lookup(self)\n            else:\n                return None\n        elif (len(self) == 1 and\n               len(self.__tokdict) == 1 and\n               next(iter(self.__tokdict.values()))[0][1] in (0,-1)):\n            return next(iter(self.__tokdict.keys()))\n        else:\n            return None\n\n    def dump(self, indent='', depth=0, full=True):\n        \"\"\"\n        Diagnostic method for listing out the contents of a C{ParseResults}.\n        Accepts an optional C{indent} argument so that this string can be embedded\n        in a nested display of other data.\n\n        Example::\n            integer = Word(nums)\n            date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")\n            \n            result = date_str.parseString('12/31/1999')\n            print(result.dump())\n        prints::\n            ['12', '/', '31', '/', '1999']\n            - day: 1999\n            - month: 31\n            - year: 12\n        \"\"\"\n        out = []\n        NL = '\\n'\n        out.append( indent+_ustr(self.asList()) )\n        if full:\n            if self.haskeys():\n                items = sorted((str(k), v) for k,v in self.items())\n                for k,v in items:\n                    if out:\n                        out.append(NL)\n                    out.append( \"%s%s- %s: \" % (indent,('  '*depth), k) )\n                    if isinstance(v,ParseResults):\n                        if v:\n                            out.append( v.dump(indent,depth+1) )\n                        else:\n                            out.append(_ustr(v))\n                    else:\n                        out.append(repr(v))\n            elif any(isinstance(vv,ParseResults) for vv in self):\n                v = self\n                for i,vv in enumerate(v):\n                    if isinstance(vv,ParseResults):\n                        out.append(\"\\n%s%s[%d]:\\n%s%s%s\" % (indent,('  '*(depth)),i,indent,('  '*(depth+1)),vv.dump(indent,depth+1) ))\n                    else:\n                        out.append(\"\\n%s%s[%d]:\\n%s%s%s\" % (indent,('  '*(depth)),i,indent,('  '*(depth+1)),_ustr(vv)))\n            \n        return \"\".join(out)\n\n    def pprint(self, *args, **kwargs):\n        \"\"\"\n        Pretty-printer for parsed results as a list, using the C{pprint} module.\n        Accepts additional positional or keyword args as defined for the \n        C{pprint.pprint} method. (U{http://docs.python.org/3/library/pprint.html#pprint.pprint})\n\n        Example::\n            ident = Word(alphas, alphanums)\n            num = Word(nums)\n            func = Forward()\n            term = ident | num | Group('(' + func + ')')\n            func <<= ident + Group(Optional(delimitedList(term)))\n            result = func.parseString(\"fna a,b,(fnb c,d,200),100\")\n            result.pprint(width=40)\n        prints::\n            ['fna',\n             ['a',\n              'b',\n              ['(', 'fnb', ['c', 'd', '200'], ')'],\n              '100']]\n        \"\"\"\n        pprint.pprint(self.asList(), *args, **kwargs)\n\n    # add support for pickle protocol\n    def __getstate__(self):\n        return ( self.__toklist,\n                 ( self.__tokdict.copy(),\n                   self.__parent is not None and self.__parent() or None,\n                   self.__accumNames,\n                   self.__name ) )\n\n    def __setstate__(self,state):\n        self.__toklist = state[0]\n        (self.__tokdict,\n         par,\n         inAccumNames,\n         self.__name) = state[1]\n        self.__accumNames = {}\n        self.__accumNames.update(inAccumNames)\n        if par is not None:\n            self.__parent = wkref(par)\n        else:\n            self.__parent = None\n\n    def __getnewargs__(self):\n        return self.__toklist, self.__name, self.__asList, self.__modal\n\n    def __dir__(self):\n        return (dir(type(self)) + list(self.keys()))\n\nMutableMapping.register(ParseResults)\n\ndef col (loc,strg):\n    \"\"\"Returns current column within a string, counting newlines as line separators.\n   The first column is number 1.\n\n   Note: the default parsing behavior is to expand tabs in the input string\n   before starting the parsing process.  See L{I{ParserElement.parseString}<ParserElement.parseString>} for more information\n   on parsing strings containing C{<TAB>}s, and suggested methods to maintain a\n   consistent view of the parsed string, the parse location, and line and column\n   positions within the parsed string.\n   \"\"\"\n    s = strg\n    return 1 if 0<loc<len(s) and s[loc-1] == '\\n' else loc - s.rfind(\"\\n\", 0, loc)\n\ndef lineno(loc,strg):\n    \"\"\"Returns current line number within a string, counting newlines as line separators.\n   The first line is number 1.\n\n   Note: the default parsing behavior is to expand tabs in the input string\n   before starting the parsing process.  See L{I{ParserElement.parseString}<ParserElement.parseString>} for more information\n   on parsing strings containing C{<TAB>}s, and suggested methods to maintain a\n   consistent view of the parsed string, the parse location, and line and column\n   positions within the parsed string.\n   \"\"\"\n    return strg.count(\"\\n\",0,loc) + 1\n\ndef line( loc, strg ):\n    \"\"\"Returns the line of text containing loc within a string, counting newlines as line separators.\n       \"\"\"\n    lastCR = strg.rfind(\"\\n\", 0, loc)\n    nextCR = strg.find(\"\\n\", loc)\n    if nextCR >= 0:\n        return strg[lastCR+1:nextCR]\n    else:\n        return strg[lastCR+1:]\n\ndef _defaultStartDebugAction( instring, loc, expr ):\n    print ((\"Match \" + _ustr(expr) + \" at loc \" + _ustr(loc) + \"(%d,%d)\" % ( lineno(loc,instring), col(loc,instring) )))\n\ndef _defaultSuccessDebugAction( instring, startloc, endloc, expr, toks ):\n    print (\"Matched \" + _ustr(expr) + \" -> \" + str(toks.asList()))\n\ndef _defaultExceptionDebugAction( instring, loc, expr, exc ):\n    print (\"Exception raised:\" + _ustr(exc))\n\ndef nullDebugAction(*args):\n    \"\"\"'Do-nothing' debug action, to suppress debugging output during parsing.\"\"\"\n    pass\n\n# Only works on Python 3.x - nonlocal is toxic to Python 2 installs\n#~ 'decorator to trim function calls to match the arity of the target'\n#~ def _trim_arity(func, maxargs=3):\n    #~ if func in singleArgBuiltins:\n        #~ return lambda s,l,t: func(t)\n    #~ limit = 0\n    #~ foundArity = False\n    #~ def wrapper(*args):\n        #~ nonlocal limit,foundArity\n        #~ while 1:\n            #~ try:\n                #~ ret = func(*args[limit:])\n                #~ foundArity = True\n                #~ return ret\n            #~ except TypeError:\n                #~ if limit == maxargs or foundArity:\n                    #~ raise\n                #~ limit += 1\n                #~ continue\n    #~ return wrapper\n\n# this version is Python 2.x-3.x cross-compatible\n'decorator to trim function calls to match the arity of the target'\ndef _trim_arity(func, maxargs=2):\n    if func in singleArgBuiltins:\n        return lambda s,l,t: func(t)\n    limit = [0]\n    foundArity = [False]\n    \n    # traceback return data structure changed in Py3.5 - normalize back to plain tuples\n    if system_version[:2] >= (3,5):\n        def extract_stack(limit=0):\n            # special handling for Python 3.5.0 - extra deep call stack by 1\n            offset = -3 if system_version == (3,5,0) else -2\n            frame_summary = traceback.extract_stack(limit=-offset+limit-1)[offset]\n            return [frame_summary[:2]]\n        def extract_tb(tb, limit=0):\n            frames = traceback.extract_tb(tb, limit=limit)\n            frame_summary = frames[-1]\n            return [frame_summary[:2]]\n    else:\n        extract_stack = traceback.extract_stack\n        extract_tb = traceback.extract_tb\n    \n    # synthesize what would be returned by traceback.extract_stack at the call to \n    # user's parse action 'func', so that we don't incur call penalty at parse time\n    \n    LINE_DIFF = 6\n    # IF ANY CODE CHANGES, EVEN JUST COMMENTS OR BLANK LINES, BETWEEN THE NEXT LINE AND \n    # THE CALL TO FUNC INSIDE WRAPPER, LINE_DIFF MUST BE MODIFIED!!!!\n    this_line = extract_stack(limit=2)[-1]\n    pa_call_line_synth = (this_line[0], this_line[1]+LINE_DIFF)\n\n    def wrapper(*args):\n        while 1:\n            try:\n                ret = func(*args[limit[0]:])\n                foundArity[0] = True\n                return ret\n            except TypeError:\n                # re-raise TypeErrors if they did not come from our arity testing\n                if foundArity[0]:\n                    raise\n                else:\n                    try:\n                        tb = sys.exc_info()[-1]\n                        if not extract_tb(tb, limit=2)[-1][:2] == pa_call_line_synth:\n                            raise\n                    finally:\n                        del tb\n\n                if limit[0] <= maxargs:\n                    limit[0] += 1\n                    continue\n                raise\n\n    # copy func name to wrapper for sensible debug output\n    func_name = \"<parse action>\"\n    try:\n        func_name = getattr(func, '__name__', \n                            getattr(func, '__class__').__name__)\n    except Exception:\n        func_name = str(func)\n    wrapper.__name__ = func_name\n\n    return wrapper\n\nclass ParserElement(object):\n    \"\"\"Abstract base level parser element class.\"\"\"\n    DEFAULT_WHITE_CHARS = \" \\n\\t\\r\"\n    verbose_stacktrace = False\n\n    @staticmethod\n    def setDefaultWhitespaceChars( chars ):\n        r\"\"\"\n        Overrides the default whitespace chars\n\n        Example::\n            # default whitespace chars are space, <TAB> and newline\n            OneOrMore(Word(alphas)).parseString(\"abc def\\nghi jkl\")  # -> ['abc', 'def', 'ghi', 'jkl']\n            \n            # change to just treat newline as significant\n            ParserElement.setDefaultWhitespaceChars(\" \\t\")\n            OneOrMore(Word(alphas)).parseString(\"abc def\\nghi jkl\")  # -> ['abc', 'def']\n        \"\"\"\n        ParserElement.DEFAULT_WHITE_CHARS = chars\n\n    @staticmethod\n    def inlineLiteralsUsing(cls):\n        \"\"\"\n        Set class to be used for inclusion of string literals into a parser.\n        \n        Example::\n            # default literal class used is Literal\n            integer = Word(nums)\n            date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")           \n\n            date_str.parseString(\"1999/12/31\")  # -> ['1999', '/', '12', '/', '31']\n\n\n            # change to Suppress\n            ParserElement.inlineLiteralsUsing(Suppress)\n            date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")           \n\n            date_str.parseString(\"1999/12/31\")  # -> ['1999', '12', '31']\n        \"\"\"\n        ParserElement._literalStringClass = cls\n\n    def __init__( self, savelist=False ):\n        self.parseAction = list()\n        self.failAction = None\n        #~ self.name = \"<unknown>\"  # don't define self.name, let subclasses try/except upcall\n        self.strRepr = None\n        self.resultsName = None\n        self.saveAsList = savelist\n        self.skipWhitespace = True\n        self.whiteChars = ParserElement.DEFAULT_WHITE_CHARS\n        self.copyDefaultWhiteChars = True\n        self.mayReturnEmpty = False # used when checking for left-recursion\n        self.keepTabs = False\n        self.ignoreExprs = list()\n        self.debug = False\n        self.streamlined = False\n        self.mayIndexError = True # used to optimize exception handling for subclasses that don't advance parse index\n        self.errmsg = \"\"\n        self.modalResults = True # used to mark results names as modal (report only last) or cumulative (list all)\n        self.debugActions = ( None, None, None ) #custom debug actions\n        self.re = None\n        self.callPreparse = True # used to avoid redundant calls to preParse\n        self.callDuringTry = False\n\n    def copy( self ):\n        \"\"\"\n        Make a copy of this C{ParserElement}.  Useful for defining different parse actions\n        for the same parsing pattern, using copies of the original parse element.\n        \n        Example::\n            integer = Word(nums).setParseAction(lambda toks: int(toks[0]))\n            integerK = integer.copy().addParseAction(lambda toks: toks[0]*1024) + Suppress(\"K\")\n            integerM = integer.copy().addParseAction(lambda toks: toks[0]*1024*1024) + Suppress(\"M\")\n            \n            print(OneOrMore(integerK | integerM | integer).parseString(\"5K 100 640K 256M\"))\n        prints::\n            [5120, 100, 655360, 268435456]\n        Equivalent form of C{expr.copy()} is just C{expr()}::\n            integerM = integer().addParseAction(lambda toks: toks[0]*1024*1024) + Suppress(\"M\")\n        \"\"\"\n        cpy = copy.copy( self )\n        cpy.parseAction = self.parseAction[:]\n        cpy.ignoreExprs = self.ignoreExprs[:]\n        if self.copyDefaultWhiteChars:\n            cpy.whiteChars = ParserElement.DEFAULT_WHITE_CHARS\n        return cpy\n\n    def setName( self, name ):\n        \"\"\"\n        Define name for this expression, makes debugging and exception messages clearer.\n        \n        Example::\n            Word(nums).parseString(\"ABC\")  # -> Exception: Expected W:(0123...) (at char 0), (line:1, col:1)\n            Word(nums).setName(\"integer\").parseString(\"ABC\")  # -> Exception: Expected integer (at char 0), (line:1, col:1)\n        \"\"\"\n        self.name = name\n        self.errmsg = \"Expected \" + self.name\n        if hasattr(self,\"exception\"):\n            self.exception.msg = self.errmsg\n        return self\n\n    def setResultsName( self, name, listAllMatches=False ):\n        \"\"\"\n        Define name for referencing matching tokens as a nested attribute\n        of the returned parse results.\n        NOTE: this returns a *copy* of the original C{ParserElement} object;\n        this is so that the client can define a basic element, such as an\n        integer, and reference it in multiple places with different names.\n\n        You can also set results names using the abbreviated syntax,\n        C{expr(\"name\")} in place of C{expr.setResultsName(\"name\")} - \n        see L{I{__call__}<__call__>}.\n\n        Example::\n            date_str = (integer.setResultsName(\"year\") + '/' \n                        + integer.setResultsName(\"month\") + '/' \n                        + integer.setResultsName(\"day\"))\n\n            # equivalent form:\n            date_str = integer(\"year\") + '/' + integer(\"month\") + '/' + integer(\"day\")\n        \"\"\"\n        newself = self.copy()\n        if name.endswith(\"*\"):\n            name = name[:-1]\n            listAllMatches=True\n        newself.resultsName = name\n        newself.modalResults = not listAllMatches\n        return newself\n\n    def setBreak(self,breakFlag = True):\n        \"\"\"Method to invoke the Python pdb debugger when this element is\n           about to be parsed. Set C{breakFlag} to True to enable, False to\n           disable.\n        \"\"\"\n        if breakFlag:\n            _parseMethod = self._parse\n            def breaker(instring, loc, doActions=True, callPreParse=True):\n                import pdb\n                pdb.set_trace()\n                return _parseMethod( instring, loc, doActions, callPreParse )\n            breaker._originalParseMethod = _parseMethod\n            self._parse = breaker\n        else:\n            if hasattr(self._parse,\"_originalParseMethod\"):\n                self._parse = self._parse._originalParseMethod\n        return self\n\n    def setParseAction( self, *fns, **kwargs ):\n        \"\"\"\n        Define one or more actions to perform when successfully matching parse element definition.\n        Parse action fn is a callable method with 0-3 arguments, called as C{fn(s,loc,toks)},\n        C{fn(loc,toks)}, C{fn(toks)}, or just C{fn()}, where:\n         - s   = the original string being parsed (see note below)\n         - loc = the location of the matching substring\n         - toks = a list of the matched tokens, packaged as a C{L{ParseResults}} object\n        If the functions in fns modify the tokens, they can return them as the return\n        value from fn, and the modified list of tokens will replace the original.\n        Otherwise, fn does not need to return any value.\n\n        Optional keyword arguments:\n         - callDuringTry = (default=C{False}) indicate if parse action should be run during lookaheads and alternate testing\n\n        Note: the default parsing behavior is to expand tabs in the input string\n        before starting the parsing process.  See L{I{parseString}<parseString>} for more information\n        on parsing strings containing C{<TAB>}s, and suggested methods to maintain a\n        consistent view of the parsed string, the parse location, and line and column\n        positions within the parsed string.\n        \n        Example::\n            integer = Word(nums)\n            date_str = integer + '/' + integer + '/' + integer\n\n            date_str.parseString(\"1999/12/31\")  # -> ['1999', '/', '12', '/', '31']\n\n            # use parse action to convert to ints at parse time\n            integer = Word(nums).setParseAction(lambda toks: int(toks[0]))\n            date_str = integer + '/' + integer + '/' + integer\n\n            # note that integer fields are now ints, not strings\n            date_str.parseString(\"1999/12/31\")  # -> [1999, '/', 12, '/', 31]\n        \"\"\"\n        self.parseAction = list(map(_trim_arity, list(fns)))\n        self.callDuringTry = kwargs.get(\"callDuringTry\", False)\n        return self\n\n    def addParseAction( self, *fns, **kwargs ):\n        \"\"\"\n        Add one or more parse actions to expression's list of parse actions. See L{I{setParseAction}<setParseAction>}.\n        \n        See examples in L{I{copy}<copy>}.\n        \"\"\"\n        self.parseAction += list(map(_trim_arity, list(fns)))\n        self.callDuringTry = self.callDuringTry or kwargs.get(\"callDuringTry\", False)\n        return self\n\n    def addCondition(self, *fns, **kwargs):\n        \"\"\"Add a boolean predicate function to expression's list of parse actions. See \n        L{I{setParseAction}<setParseAction>} for function call signatures. Unlike C{setParseAction}, \n        functions passed to C{addCondition} need to return boolean success/fail of the condition.\n\n        Optional keyword arguments:\n         - message = define a custom message to be used in the raised exception\n         - fatal   = if True, will raise ParseFatalException to stop parsing immediately; otherwise will raise ParseException\n         \n        Example::\n            integer = Word(nums).setParseAction(lambda toks: int(toks[0]))\n            year_int = integer.copy()\n            year_int.addCondition(lambda toks: toks[0] >= 2000, message=\"Only support years 2000 and later\")\n            date_str = year_int + '/' + integer + '/' + integer\n\n            result = date_str.parseString(\"1999/12/31\")  # -> Exception: Only support years 2000 and later (at char 0), (line:1, col:1)\n        \"\"\"\n        msg = kwargs.get(\"message\", \"failed user-defined condition\")\n        exc_type = ParseFatalException if kwargs.get(\"fatal\", False) else ParseException\n        for fn in fns:\n            def pa(s,l,t):\n                if not bool(_trim_arity(fn)(s,l,t)):\n                    raise exc_type(s,l,msg)\n            self.parseAction.append(pa)\n        self.callDuringTry = self.callDuringTry or kwargs.get(\"callDuringTry\", False)\n        return self\n\n    def setFailAction( self, fn ):\n        \"\"\"Define action to perform if parsing fails at this expression.\n           Fail acton fn is a callable function that takes the arguments\n           C{fn(s,loc,expr,err)} where:\n            - s = string being parsed\n            - loc = location where expression match was attempted and failed\n            - expr = the parse expression that failed\n            - err = the exception thrown\n           The function returns no value.  It may throw C{L{ParseFatalException}}\n           if it is desired to stop parsing immediately.\"\"\"\n        self.failAction = fn\n        return self\n\n    def _skipIgnorables( self, instring, loc ):\n        exprsFound = True\n        while exprsFound:\n            exprsFound = False\n            for e in self.ignoreExprs:\n                try:\n                    while 1:\n                        loc,dummy = e._parse( instring, loc )\n                        exprsFound = True\n                except ParseException:\n                    pass\n        return loc\n\n    def preParse( self, instring, loc ):\n        if self.ignoreExprs:\n            loc = self._skipIgnorables( instring, loc )\n\n        if self.skipWhitespace:\n            wt = self.whiteChars\n            instrlen = len(instring)\n            while loc < instrlen and instring[loc] in wt:\n                loc += 1\n\n        return loc\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        return loc, []\n\n    def postParse( self, instring, loc, tokenlist ):\n        return tokenlist\n\n    #~ @profile\n    def _parseNoCache( self, instring, loc, doActions=True, callPreParse=True ):\n        debugging = ( self.debug ) #and doActions )\n\n        if debugging or self.failAction:\n            #~ print (\"Match\",self,\"at loc\",loc,\"(%d,%d)\" % ( lineno(loc,instring), col(loc,instring) ))\n            if (self.debugActions[0] ):\n                self.debugActions[0]( instring, loc, self )\n            if callPreParse and self.callPreparse:\n                preloc = self.preParse( instring, loc )\n            else:\n                preloc = loc\n            tokensStart = preloc\n            try:\n                try:\n                    loc,tokens = self.parseImpl( instring, preloc, doActions )\n                except IndexError:\n                    raise ParseException( instring, len(instring), self.errmsg, self )\n            except ParseBaseException as err:\n                #~ print (\"Exception raised:\", err)\n                if self.debugActions[2]:\n                    self.debugActions[2]( instring, tokensStart, self, err )\n                if self.failAction:\n                    self.failAction( instring, tokensStart, self, err )\n                raise\n        else:\n            if callPreParse and self.callPreparse:\n                preloc = self.preParse( instring, loc )\n            else:\n                preloc = loc\n            tokensStart = preloc\n            if self.mayIndexError or preloc >= len(instring):\n                try:\n                    loc,tokens = self.parseImpl( instring, preloc, doActions )\n                except IndexError:\n                    raise ParseException( instring, len(instring), self.errmsg, self )\n            else:\n                loc,tokens = self.parseImpl( instring, preloc, doActions )\n\n        tokens = self.postParse( instring, loc, tokens )\n\n        retTokens = ParseResults( tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults )\n        if self.parseAction and (doActions or self.callDuringTry):\n            if debugging:\n                try:\n                    for fn in self.parseAction:\n                        tokens = fn( instring, tokensStart, retTokens )\n                        if tokens is not None:\n                            retTokens = ParseResults( tokens,\n                                                      self.resultsName,\n                                                      asList=self.saveAsList and isinstance(tokens,(ParseResults,list)),\n                                                      modal=self.modalResults )\n                except ParseBaseException as err:\n                    #~ print \"Exception raised in user parse action:\", err\n                    if (self.debugActions[2] ):\n                        self.debugActions[2]( instring, tokensStart, self, err )\n                    raise\n            else:\n                for fn in self.parseAction:\n                    tokens = fn( instring, tokensStart, retTokens )\n                    if tokens is not None:\n                        retTokens = ParseResults( tokens,\n                                                  self.resultsName,\n                                                  asList=self.saveAsList and isinstance(tokens,(ParseResults,list)),\n                                                  modal=self.modalResults )\n        if debugging:\n            #~ print (\"Matched\",self,\"->\",retTokens.asList())\n            if (self.debugActions[1] ):\n                self.debugActions[1]( instring, tokensStart, loc, self, retTokens )\n\n        return loc, retTokens\n\n    def tryParse( self, instring, loc ):\n        try:\n            return self._parse( instring, loc, doActions=False )[0]\n        except ParseFatalException:\n            raise ParseException( instring, loc, self.errmsg, self)\n    \n    def canParseNext(self, instring, loc):\n        try:\n            self.tryParse(instring, loc)\n        except (ParseException, IndexError):\n            return False\n        else:\n            return True\n\n    class _UnboundedCache(object):\n        def __init__(self):\n            cache = {}\n            self.not_in_cache = not_in_cache = object()\n\n            def get(self, key):\n                return cache.get(key, not_in_cache)\n\n            def set(self, key, value):\n                cache[key] = value\n\n            def clear(self):\n                cache.clear()\n                \n            def cache_len(self):\n                return len(cache)\n\n            self.get = types.MethodType(get, self)\n            self.set = types.MethodType(set, self)\n            self.clear = types.MethodType(clear, self)\n            self.__len__ = types.MethodType(cache_len, self)\n\n    if _OrderedDict is not None:\n        class _FifoCache(object):\n            def __init__(self, size):\n                self.not_in_cache = not_in_cache = object()\n\n                cache = _OrderedDict()\n\n                def get(self, key):\n                    return cache.get(key, not_in_cache)\n\n                def set(self, key, value):\n                    cache[key] = value\n                    while len(cache) > size:\n                        try:\n                            cache.popitem(False)\n                        except KeyError:\n                            pass\n\n                def clear(self):\n                    cache.clear()\n\n                def cache_len(self):\n                    return len(cache)\n\n                self.get = types.MethodType(get, self)\n                self.set = types.MethodType(set, self)\n                self.clear = types.MethodType(clear, self)\n                self.__len__ = types.MethodType(cache_len, self)\n\n    else:\n        class _FifoCache(object):\n            def __init__(self, size):\n                self.not_in_cache = not_in_cache = object()\n\n                cache = {}\n                key_fifo = collections.deque([], size)\n\n                def get(self, key):\n                    return cache.get(key, not_in_cache)\n\n                def set(self, key, value):\n                    cache[key] = value\n                    while len(key_fifo) > size:\n                        cache.pop(key_fifo.popleft(), None)\n                    key_fifo.append(key)\n\n                def clear(self):\n                    cache.clear()\n                    key_fifo.clear()\n\n                def cache_len(self):\n                    return len(cache)\n\n                self.get = types.MethodType(get, self)\n                self.set = types.MethodType(set, self)\n                self.clear = types.MethodType(clear, self)\n                self.__len__ = types.MethodType(cache_len, self)\n\n    # argument cache for optimizing repeated calls when backtracking through recursive expressions\n    packrat_cache = {} # this is set later by enabledPackrat(); this is here so that resetCache() doesn't fail\n    packrat_cache_lock = RLock()\n    packrat_cache_stats = [0, 0]\n\n    # this method gets repeatedly called during backtracking with the same arguments -\n    # we can cache these arguments and save ourselves the trouble of re-parsing the contained expression\n    def _parseCache( self, instring, loc, doActions=True, callPreParse=True ):\n        HIT, MISS = 0, 1\n        lookup = (self, instring, loc, callPreParse, doActions)\n        with ParserElement.packrat_cache_lock:\n            cache = ParserElement.packrat_cache\n            value = cache.get(lookup)\n            if value is cache.not_in_cache:\n                ParserElement.packrat_cache_stats[MISS] += 1\n                try:\n                    value = self._parseNoCache(instring, loc, doActions, callPreParse)\n                except ParseBaseException as pe:\n                    # cache a copy of the exception, without the traceback\n                    cache.set(lookup, pe.__class__(*pe.args))\n                    raise\n                else:\n                    cache.set(lookup, (value[0], value[1].copy()))\n                    return value\n            else:\n                ParserElement.packrat_cache_stats[HIT] += 1\n                if isinstance(value, Exception):\n                    raise value\n                return (value[0], value[1].copy())\n\n    _parse = _parseNoCache\n\n    @staticmethod\n    def resetCache():\n        ParserElement.packrat_cache.clear()\n        ParserElement.packrat_cache_stats[:] = [0] * len(ParserElement.packrat_cache_stats)\n\n    _packratEnabled = False\n    @staticmethod\n    def enablePackrat(cache_size_limit=128):\n        \"\"\"Enables \"packrat\" parsing, which adds memoizing to the parsing logic.\n           Repeated parse attempts at the same string location (which happens\n           often in many complex grammars) can immediately return a cached value,\n           instead of re-executing parsing/validating code.  Memoizing is done of\n           both valid results and parsing exceptions.\n           \n           Parameters:\n            - cache_size_limit - (default=C{128}) - if an integer value is provided\n              will limit the size of the packrat cache; if None is passed, then\n              the cache size will be unbounded; if 0 is passed, the cache will\n              be effectively disabled.\n            \n           This speedup may break existing programs that use parse actions that\n           have side-effects.  For this reason, packrat parsing is disabled when\n           you first import pyparsing.  To activate the packrat feature, your\n           program must call the class method C{ParserElement.enablePackrat()}.  If\n           your program uses C{psyco} to \"compile as you go\", you must call\n           C{enablePackrat} before calling C{psyco.full()}.  If you do not do this,\n           Python will crash.  For best results, call C{enablePackrat()} immediately\n           after importing pyparsing.\n           \n           Example::\n               import pyparsing\n               pyparsing.ParserElement.enablePackrat()\n        \"\"\"\n        if not ParserElement._packratEnabled:\n            ParserElement._packratEnabled = True\n            if cache_size_limit is None:\n                ParserElement.packrat_cache = ParserElement._UnboundedCache()\n            else:\n                ParserElement.packrat_cache = ParserElement._FifoCache(cache_size_limit)\n            ParserElement._parse = ParserElement._parseCache\n\n    def parseString( self, instring, parseAll=False ):\n        \"\"\"\n        Execute the parse expression with the given string.\n        This is the main interface to the client code, once the complete\n        expression has been built.\n\n        If you want the grammar to require that the entire input string be\n        successfully parsed, then set C{parseAll} to True (equivalent to ending\n        the grammar with C{L{StringEnd()}}).\n\n        Note: C{parseString} implicitly calls C{expandtabs()} on the input string,\n        in order to report proper column numbers in parse actions.\n        If the input string contains tabs and\n        the grammar uses parse actions that use the C{loc} argument to index into the\n        string being parsed, you can ensure you have a consistent view of the input\n        string by:\n         - calling C{parseWithTabs} on your grammar before calling C{parseString}\n           (see L{I{parseWithTabs}<parseWithTabs>})\n         - define your parse action using the full C{(s,loc,toks)} signature, and\n           reference the input string using the parse action's C{s} argument\n         - explictly expand the tabs in your input string before calling\n           C{parseString}\n        \n        Example::\n            Word('a').parseString('aaaaabaaa')  # -> ['aaaaa']\n            Word('a').parseString('aaaaabaaa', parseAll=True)  # -> Exception: Expected end of text\n        \"\"\"\n        ParserElement.resetCache()\n        if not self.streamlined:\n            self.streamline()\n            #~ self.saveAsList = True\n        for e in self.ignoreExprs:\n            e.streamline()\n        if not self.keepTabs:\n            instring = instring.expandtabs()\n        try:\n            loc, tokens = self._parse( instring, 0 )\n            if parseAll:\n                loc = self.preParse( instring, loc )\n                se = Empty() + StringEnd()\n                se._parse( instring, loc )\n        except ParseBaseException as exc:\n            if ParserElement.verbose_stacktrace:\n                raise\n            else:\n                # catch and re-raise exception from here, clears out pyparsing internal stack trace\n                raise exc\n        else:\n            return tokens\n\n    def scanString( self, instring, maxMatches=_MAX_INT, overlap=False ):\n        \"\"\"\n        Scan the input string for expression matches.  Each match will return the\n        matching tokens, start location, and end location.  May be called with optional\n        C{maxMatches} argument, to clip scanning after 'n' matches are found.  If\n        C{overlap} is specified, then overlapping matches will be reported.\n\n        Note that the start and end locations are reported relative to the string\n        being parsed.  See L{I{parseString}<parseString>} for more information on parsing\n        strings with embedded tabs.\n\n        Example::\n            source = \"sldjf123lsdjjkf345sldkjf879lkjsfd987\"\n            print(source)\n            for tokens,start,end in Word(alphas).scanString(source):\n                print(' '*start + '^'*(end-start))\n                print(' '*start + tokens[0])\n        \n        prints::\n        \n            sldjf123lsdjjkf345sldkjf879lkjsfd987\n            ^^^^^\n            sldjf\n                    ^^^^^^^\n                    lsdjjkf\n                              ^^^^^^\n                              sldkjf\n                                       ^^^^^^\n                                       lkjsfd\n        \"\"\"\n        if not self.streamlined:\n            self.streamline()\n        for e in self.ignoreExprs:\n            e.streamline()\n\n        if not self.keepTabs:\n            instring = _ustr(instring).expandtabs()\n        instrlen = len(instring)\n        loc = 0\n        preparseFn = self.preParse\n        parseFn = self._parse\n        ParserElement.resetCache()\n        matches = 0\n        try:\n            while loc <= instrlen and matches < maxMatches:\n                try:\n                    preloc = preparseFn( instring, loc )\n                    nextLoc,tokens = parseFn( instring, preloc, callPreParse=False )\n                except ParseException:\n                    loc = preloc+1\n                else:\n                    if nextLoc > loc:\n                        matches += 1\n                        yield tokens, preloc, nextLoc\n                        if overlap:\n                            nextloc = preparseFn( instring, loc )\n                            if nextloc > loc:\n                                loc = nextLoc\n                            else:\n                                loc += 1\n                        else:\n                            loc = nextLoc\n                    else:\n                        loc = preloc+1\n        except ParseBaseException as exc:\n            if ParserElement.verbose_stacktrace:\n                raise\n            else:\n                # catch and re-raise exception from here, clears out pyparsing internal stack trace\n                raise exc\n\n    def transformString( self, instring ):\n        \"\"\"\n        Extension to C{L{scanString}}, to modify matching text with modified tokens that may\n        be returned from a parse action.  To use C{transformString}, define a grammar and\n        attach a parse action to it that modifies the returned token list.\n        Invoking C{transformString()} on a target string will then scan for matches,\n        and replace the matched text patterns according to the logic in the parse\n        action.  C{transformString()} returns the resulting transformed string.\n        \n        Example::\n            wd = Word(alphas)\n            wd.setParseAction(lambda toks: toks[0].title())\n            \n            print(wd.transformString(\"now is the winter of our discontent made glorious summer by this sun of york.\"))\n        Prints::\n            Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York.\n        \"\"\"\n        out = []\n        lastE = 0\n        # force preservation of <TAB>s, to minimize unwanted transformation of string, and to\n        # keep string locs straight between transformString and scanString\n        self.keepTabs = True\n        try:\n            for t,s,e in self.scanString( instring ):\n                out.append( instring[lastE:s] )\n                if t:\n                    if isinstance(t,ParseResults):\n                        out += t.asList()\n                    elif isinstance(t,list):\n                        out += t\n                    else:\n                        out.append(t)\n                lastE = e\n            out.append(instring[lastE:])\n            out = [o for o in out if o]\n            return \"\".join(map(_ustr,_flatten(out)))\n        except ParseBaseException as exc:\n            if ParserElement.verbose_stacktrace:\n                raise\n            else:\n                # catch and re-raise exception from here, clears out pyparsing internal stack trace\n                raise exc\n\n    def searchString( self, instring, maxMatches=_MAX_INT ):\n        \"\"\"\n        Another extension to C{L{scanString}}, simplifying the access to the tokens found\n        to match the given parse expression.  May be called with optional\n        C{maxMatches} argument, to clip searching after 'n' matches are found.\n        \n        Example::\n            # a capitalized word starts with an uppercase letter, followed by zero or more lowercase letters\n            cap_word = Word(alphas.upper(), alphas.lower())\n            \n            print(cap_word.searchString(\"More than Iron, more than Lead, more than Gold I need Electricity\"))\n\n            # the sum() builtin can be used to merge results into a single ParseResults object\n            print(sum(cap_word.searchString(\"More than Iron, more than Lead, more than Gold I need Electricity\")))\n        prints::\n            [['More'], ['Iron'], ['Lead'], ['Gold'], ['I'], ['Electricity']]\n            ['More', 'Iron', 'Lead', 'Gold', 'I', 'Electricity']\n        \"\"\"\n        try:\n            return ParseResults([ t for t,s,e in self.scanString( instring, maxMatches ) ])\n        except ParseBaseException as exc:\n            if ParserElement.verbose_stacktrace:\n                raise\n            else:\n                # catch and re-raise exception from here, clears out pyparsing internal stack trace\n                raise exc\n\n    def split(self, instring, maxsplit=_MAX_INT, includeSeparators=False):\n        \"\"\"\n        Generator method to split a string using the given expression as a separator.\n        May be called with optional C{maxsplit} argument, to limit the number of splits;\n        and the optional C{includeSeparators} argument (default=C{False}), if the separating\n        matching text should be included in the split results.\n        \n        Example::        \n            punc = oneOf(list(\".,;:/-!?\"))\n            print(list(punc.split(\"This, this?, this sentence, is badly punctuated!\")))\n        prints::\n            ['This', ' this', '', ' this sentence', ' is badly punctuated', '']\n        \"\"\"\n        splits = 0\n        last = 0\n        for t,s,e in self.scanString(instring, maxMatches=maxsplit):\n            yield instring[last:s]\n            if includeSeparators:\n                yield t[0]\n            last = e\n        yield instring[last:]\n\n    def __add__(self, other ):\n        \"\"\"\n        Implementation of + operator - returns C{L{And}}. Adding strings to a ParserElement\n        converts them to L{Literal}s by default.\n        \n        Example::\n            greet = Word(alphas) + \",\" + Word(alphas) + \"!\"\n            hello = \"Hello, World!\"\n            print (hello, \"->\", greet.parseString(hello))\n        Prints::\n            Hello, World! -> ['Hello', ',', 'World', '!']\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return And( [ self, other ] )\n\n    def __radd__(self, other ):\n        \"\"\"\n        Implementation of + operator when left operand is not a C{L{ParserElement}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return other + self\n\n    def __sub__(self, other):\n        \"\"\"\n        Implementation of - operator, returns C{L{And}} with error stop\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return self + And._ErrorStop() + other\n\n    def __rsub__(self, other ):\n        \"\"\"\n        Implementation of - operator when left operand is not a C{L{ParserElement}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return other - self\n\n    def __mul__(self,other):\n        \"\"\"\n        Implementation of * operator, allows use of C{expr * 3} in place of\n        C{expr + expr + expr}.  Expressions may also me multiplied by a 2-integer\n        tuple, similar to C{{min,max}} multipliers in regular expressions.  Tuples\n        may also include C{None} as in:\n         - C{expr*(n,None)} or C{expr*(n,)} is equivalent\n              to C{expr*n + L{ZeroOrMore}(expr)}\n              (read as \"at least n instances of C{expr}\")\n         - C{expr*(None,n)} is equivalent to C{expr*(0,n)}\n              (read as \"0 to n instances of C{expr}\")\n         - C{expr*(None,None)} is equivalent to C{L{ZeroOrMore}(expr)}\n         - C{expr*(1,None)} is equivalent to C{L{OneOrMore}(expr)}\n\n        Note that C{expr*(None,n)} does not raise an exception if\n        more than n exprs exist in the input stream; that is,\n        C{expr*(None,n)} does not enforce a maximum number of expr\n        occurrences.  If this behavior is desired, then write\n        C{expr*(None,n) + ~expr}\n        \"\"\"\n        if isinstance(other,int):\n            minElements, optElements = other,0\n        elif isinstance(other,tuple):\n            other = (other + (None, None))[:2]\n            if other[0] is None:\n                other = (0, other[1])\n            if isinstance(other[0],int) and other[1] is None:\n                if other[0] == 0:\n                    return ZeroOrMore(self)\n                if other[0] == 1:\n                    return OneOrMore(self)\n                else:\n                    return self*other[0] + ZeroOrMore(self)\n            elif isinstance(other[0],int) and isinstance(other[1],int):\n                minElements, optElements = other\n                optElements -= minElements\n            else:\n                raise TypeError(\"cannot multiply 'ParserElement' and ('%s','%s') objects\", type(other[0]),type(other[1]))\n        else:\n            raise TypeError(\"cannot multiply 'ParserElement' and '%s' objects\", type(other))\n\n        if minElements < 0:\n            raise ValueError(\"cannot multiply ParserElement by negative value\")\n        if optElements < 0:\n            raise ValueError(\"second tuple value must be greater or equal to first tuple value\")\n        if minElements == optElements == 0:\n            raise ValueError(\"cannot multiply ParserElement by 0 or (0,0)\")\n\n        if (optElements):\n            def makeOptionalList(n):\n                if n>1:\n                    return Optional(self + makeOptionalList(n-1))\n                else:\n                    return Optional(self)\n            if minElements:\n                if minElements == 1:\n                    ret = self + makeOptionalList(optElements)\n                else:\n                    ret = And([self]*minElements) + makeOptionalList(optElements)\n            else:\n                ret = makeOptionalList(optElements)\n        else:\n            if minElements == 1:\n                ret = self\n            else:\n                ret = And([self]*minElements)\n        return ret\n\n    def __rmul__(self, other):\n        return self.__mul__(other)\n\n    def __or__(self, other ):\n        \"\"\"\n        Implementation of | operator - returns C{L{MatchFirst}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return MatchFirst( [ self, other ] )\n\n    def __ror__(self, other ):\n        \"\"\"\n        Implementation of | operator when left operand is not a C{L{ParserElement}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return other | self\n\n    def __xor__(self, other ):\n        \"\"\"\n        Implementation of ^ operator - returns C{L{Or}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return Or( [ self, other ] )\n\n    def __rxor__(self, other ):\n        \"\"\"\n        Implementation of ^ operator when left operand is not a C{L{ParserElement}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return other ^ self\n\n    def __and__(self, other ):\n        \"\"\"\n        Implementation of & operator - returns C{L{Each}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return Each( [ self, other ] )\n\n    def __rand__(self, other ):\n        \"\"\"\n        Implementation of & operator when left operand is not a C{L{ParserElement}}\n        \"\"\"\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        if not isinstance( other, ParserElement ):\n            warnings.warn(\"Cannot combine element of type %s with ParserElement\" % type(other),\n                    SyntaxWarning, stacklevel=2)\n            return None\n        return other & self\n\n    def __invert__( self ):\n        \"\"\"\n        Implementation of ~ operator - returns C{L{NotAny}}\n        \"\"\"\n        return NotAny( self )\n\n    def __call__(self, name=None):\n        \"\"\"\n        Shortcut for C{L{setResultsName}}, with C{listAllMatches=False}.\n        \n        If C{name} is given with a trailing C{'*'} character, then C{listAllMatches} will be\n        passed as C{True}.\n           \n        If C{name} is omitted, same as calling C{L{copy}}.\n\n        Example::\n            # these are equivalent\n            userdata = Word(alphas).setResultsName(\"name\") + Word(nums+\"-\").setResultsName(\"socsecno\")\n            userdata = Word(alphas)(\"name\") + Word(nums+\"-\")(\"socsecno\")             \n        \"\"\"\n        if name is not None:\n            return self.setResultsName(name)\n        else:\n            return self.copy()\n\n    def suppress( self ):\n        \"\"\"\n        Suppresses the output of this C{ParserElement}; useful to keep punctuation from\n        cluttering up returned output.\n        \"\"\"\n        return Suppress( self )\n\n    def leaveWhitespace( self ):\n        \"\"\"\n        Disables the skipping of whitespace before matching the characters in the\n        C{ParserElement}'s defined pattern.  This is normally only used internally by\n        the pyparsing module, but may be needed in some whitespace-sensitive grammars.\n        \"\"\"\n        self.skipWhitespace = False\n        return self\n\n    def setWhitespaceChars( self, chars ):\n        \"\"\"\n        Overrides the default whitespace chars\n        \"\"\"\n        self.skipWhitespace = True\n        self.whiteChars = chars\n        self.copyDefaultWhiteChars = False\n        return self\n\n    def parseWithTabs( self ):\n        \"\"\"\n        Overrides default behavior to expand C{<TAB>}s to spaces before parsing the input string.\n        Must be called before C{parseString} when the input grammar contains elements that\n        match C{<TAB>} characters.\n        \"\"\"\n        self.keepTabs = True\n        return self\n\n    def ignore( self, other ):\n        \"\"\"\n        Define expression to be ignored (e.g., comments) while doing pattern\n        matching; may be called repeatedly, to define multiple comment or other\n        ignorable patterns.\n        \n        Example::\n            patt = OneOrMore(Word(alphas))\n            patt.parseString('ablaj /* comment */ lskjd') # -> ['ablaj']\n            \n            patt.ignore(cStyleComment)\n            patt.parseString('ablaj /* comment */ lskjd') # -> ['ablaj', 'lskjd']\n        \"\"\"\n        if isinstance(other, basestring):\n            other = Suppress(other)\n\n        if isinstance( other, Suppress ):\n            if other not in self.ignoreExprs:\n                self.ignoreExprs.append(other)\n        else:\n            self.ignoreExprs.append( Suppress( other.copy() ) )\n        return self\n\n    def setDebugActions( self, startAction, successAction, exceptionAction ):\n        \"\"\"\n        Enable display of debugging messages while doing pattern matching.\n        \"\"\"\n        self.debugActions = (startAction or _defaultStartDebugAction,\n                             successAction or _defaultSuccessDebugAction,\n                             exceptionAction or _defaultExceptionDebugAction)\n        self.debug = True\n        return self\n\n    def setDebug( self, flag=True ):\n        \"\"\"\n        Enable display of debugging messages while doing pattern matching.\n        Set C{flag} to True to enable, False to disable.\n\n        Example::\n            wd = Word(alphas).setName(\"alphaword\")\n            integer = Word(nums).setName(\"numword\")\n            term = wd | integer\n            \n            # turn on debugging for wd\n            wd.setDebug()\n\n            OneOrMore(term).parseString(\"abc 123 xyz 890\")\n        \n        prints::\n            Match alphaword at loc 0(1,1)\n            Matched alphaword -> ['abc']\n            Match alphaword at loc 3(1,4)\n            Exception raised:Expected alphaword (at char 4), (line:1, col:5)\n            Match alphaword at loc 7(1,8)\n            Matched alphaword -> ['xyz']\n            Match alphaword at loc 11(1,12)\n            Exception raised:Expected alphaword (at char 12), (line:1, col:13)\n            Match alphaword at loc 15(1,16)\n            Exception raised:Expected alphaword (at char 15), (line:1, col:16)\n\n        The output shown is that produced by the default debug actions - custom debug actions can be\n        specified using L{setDebugActions}. Prior to attempting\n        to match the C{wd} expression, the debugging message C{\"Match <exprname> at loc <n>(<line>,<col>)\"}\n        is shown. Then if the parse succeeds, a C{\"Matched\"} message is shown, or an C{\"Exception raised\"}\n        message is shown. Also note the use of L{setName} to assign a human-readable name to the expression,\n        which makes debugging and exception messages easier to understand - for instance, the default\n        name created for the C{Word} expression without calling C{setName} is C{\"W:(ABCD...)\"}.\n        \"\"\"\n        if flag:\n            self.setDebugActions( _defaultStartDebugAction, _defaultSuccessDebugAction, _defaultExceptionDebugAction )\n        else:\n            self.debug = False\n        return self\n\n    def __str__( self ):\n        return self.name\n\n    def __repr__( self ):\n        return _ustr(self)\n\n    def streamline( self ):\n        self.streamlined = True\n        self.strRepr = None\n        return self\n\n    def checkRecursion( self, parseElementList ):\n        pass\n\n    def validate( self, validateTrace=[] ):\n        \"\"\"\n        Check defined expressions for valid structure, check for infinite recursive definitions.\n        \"\"\"\n        self.checkRecursion( [] )\n\n    def parseFile( self, file_or_filename, parseAll=False ):\n        \"\"\"\n        Execute the parse expression on the given file or filename.\n        If a filename is specified (instead of a file object),\n        the entire file is opened, read, and closed before parsing.\n        \"\"\"\n        try:\n            file_contents = file_or_filename.read()\n        except AttributeError:\n            with open(file_or_filename, \"r\") as f:\n                file_contents = f.read()\n        try:\n            return self.parseString(file_contents, parseAll)\n        except ParseBaseException as exc:\n            if ParserElement.verbose_stacktrace:\n                raise\n            else:\n                # catch and re-raise exception from here, clears out pyparsing internal stack trace\n                raise exc\n\n    def __eq__(self,other):\n        if isinstance(other, ParserElement):\n            return self is other or vars(self) == vars(other)\n        elif isinstance(other, basestring):\n            return self.matches(other)\n        else:\n            return super(ParserElement,self)==other\n\n    def __ne__(self,other):\n        return not (self == other)\n\n    def __hash__(self):\n        return hash(id(self))\n\n    def __req__(self,other):\n        return self == other\n\n    def __rne__(self,other):\n        return not (self == other)\n\n    def matches(self, testString, parseAll=True):\n        \"\"\"\n        Method for quick testing of a parser against a test string. Good for simple \n        inline microtests of sub expressions while building up larger parser.\n           \n        Parameters:\n         - testString - to test against this expression for a match\n         - parseAll - (default=C{True}) - flag to pass to C{L{parseString}} when running tests\n            \n        Example::\n            expr = Word(nums)\n            assert expr.matches(\"100\")\n        \"\"\"\n        try:\n            self.parseString(_ustr(testString), parseAll=parseAll)\n            return True\n        except ParseBaseException:\n            return False\n                \n    def runTests(self, tests, parseAll=True, comment='#', fullDump=True, printResults=True, failureTests=False):\n        \"\"\"\n        Execute the parse expression on a series of test strings, showing each\n        test, the parsed results or where the parse failed. Quick and easy way to\n        run a parse expression against a list of sample strings.\n           \n        Parameters:\n         - tests - a list of separate test strings, or a multiline string of test strings\n         - parseAll - (default=C{True}) - flag to pass to C{L{parseString}} when running tests           \n         - comment - (default=C{'#'}) - expression for indicating embedded comments in the test \n              string; pass None to disable comment filtering\n         - fullDump - (default=C{True}) - dump results as list followed by results names in nested outline;\n              if False, only dump nested list\n         - printResults - (default=C{True}) prints test output to stdout\n         - failureTests - (default=C{False}) indicates if these tests are expected to fail parsing\n\n        Returns: a (success, results) tuple, where success indicates that all tests succeeded\n        (or failed if C{failureTests} is True), and the results contain a list of lines of each \n        test's output\n        \n        Example::\n            number_expr = pyparsing_common.number.copy()\n\n            result = number_expr.runTests('''\n                # unsigned integer\n                100\n                # negative integer\n                -100\n                # float with scientific notation\n                6.02e23\n                # integer with scientific notation\n                1e-12\n                ''')\n            print(\"Success\" if result[0] else \"Failed!\")\n\n            result = number_expr.runTests('''\n                # stray character\n                100Z\n                # missing leading digit before '.'\n                -.100\n                # too many '.'\n                3.14.159\n                ''', failureTests=True)\n            print(\"Success\" if result[0] else \"Failed!\")\n        prints::\n            # unsigned integer\n            100\n            [100]\n\n            # negative integer\n            -100\n            [-100]\n\n            # float with scientific notation\n            6.02e23\n            [6.02e+23]\n\n            # integer with scientific notation\n            1e-12\n            [1e-12]\n\n            Success\n            \n            # stray character\n            100Z\n               ^\n            FAIL: Expected end of text (at char 3), (line:1, col:4)\n\n            # missing leading digit before '.'\n            -.100\n            ^\n            FAIL: Expected {real number with scientific notation | real number | signed integer} (at char 0), (line:1, col:1)\n\n            # too many '.'\n            3.14.159\n                ^\n            FAIL: Expected end of text (at char 4), (line:1, col:5)\n\n            Success\n\n        Each test string must be on a single line. If you want to test a string that spans multiple\n        lines, create a test like this::\n\n            expr.runTest(r\"this is a test\\\\n of strings that spans \\\\n 3 lines\")\n        \n        (Note that this is a raw string literal, you must include the leading 'r'.)\n        \"\"\"\n        if isinstance(tests, basestring):\n            tests = list(map(str.strip, tests.rstrip().splitlines()))\n        if isinstance(comment, basestring):\n            comment = Literal(comment)\n        allResults = []\n        comments = []\n        success = True\n        for t in tests:\n            if comment is not None and comment.matches(t, False) or comments and not t:\n                comments.append(t)\n                continue\n            if not t:\n                continue\n            out = ['\\n'.join(comments), t]\n            comments = []\n            try:\n                t = t.replace(r'\\n','\\n')\n                result = self.parseString(t, parseAll=parseAll)\n                out.append(result.dump(full=fullDump))\n                success = success and not failureTests\n            except ParseBaseException as pe:\n                fatal = \"(FATAL)\" if isinstance(pe, ParseFatalException) else \"\"\n                if '\\n' in t:\n                    out.append(line(pe.loc, t))\n                    out.append(' '*(col(pe.loc,t)-1) + '^' + fatal)\n                else:\n                    out.append(' '*pe.loc + '^' + fatal)\n                out.append(\"FAIL: \" + str(pe))\n                success = success and failureTests\n                result = pe\n            except Exception as exc:\n                out.append(\"FAIL-EXCEPTION: \" + str(exc))\n                success = success and failureTests\n                result = exc\n\n            if printResults:\n                if fullDump:\n                    out.append('')\n                print('\\n'.join(out))\n\n            allResults.append((t, result))\n        \n        return success, allResults\n\n        \nclass Token(ParserElement):\n    \"\"\"\n    Abstract C{ParserElement} subclass, for defining atomic matching patterns.\n    \"\"\"\n    def __init__( self ):\n        super(Token,self).__init__( savelist=False )\n\n\nclass Empty(Token):\n    \"\"\"\n    An empty token, will always match.\n    \"\"\"\n    def __init__( self ):\n        super(Empty,self).__init__()\n        self.name = \"Empty\"\n        self.mayReturnEmpty = True\n        self.mayIndexError = False\n\n\nclass NoMatch(Token):\n    \"\"\"\n    A token that will never match.\n    \"\"\"\n    def __init__( self ):\n        super(NoMatch,self).__init__()\n        self.name = \"NoMatch\"\n        self.mayReturnEmpty = True\n        self.mayIndexError = False\n        self.errmsg = \"Unmatchable token\"\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        raise ParseException(instring, loc, self.errmsg, self)\n\n\nclass Literal(Token):\n    \"\"\"\n    Token to exactly match a specified string.\n    \n    Example::\n        Literal('blah').parseString('blah')  # -> ['blah']\n        Literal('blah').parseString('blahfooblah')  # -> ['blah']\n        Literal('blah').parseString('bla')  # -> Exception: Expected \"blah\"\n    \n    For case-insensitive matching, use L{CaselessLiteral}.\n    \n    For keyword matching (force word break before and after the matched string),\n    use L{Keyword} or L{CaselessKeyword}.\n    \"\"\"\n    def __init__( self, matchString ):\n        super(Literal,self).__init__()\n        self.match = matchString\n        self.matchLen = len(matchString)\n        try:\n            self.firstMatchChar = matchString[0]\n        except IndexError:\n            warnings.warn(\"null string passed to Literal; use Empty() instead\",\n                            SyntaxWarning, stacklevel=2)\n            self.__class__ = Empty\n        self.name = '\"%s\"' % _ustr(self.match)\n        self.errmsg = \"Expected \" + self.name\n        self.mayReturnEmpty = False\n        self.mayIndexError = False\n\n    # Performance tuning: this routine gets called a *lot*\n    # if this is a single character match string  and the first character matches,\n    # short-circuit as quickly as possible, and avoid calling startswith\n    #~ @profile\n    def parseImpl( self, instring, loc, doActions=True ):\n        if (instring[loc] == self.firstMatchChar and\n            (self.matchLen==1 or instring.startswith(self.match,loc)) ):\n            return loc+self.matchLen, self.match\n        raise ParseException(instring, loc, self.errmsg, self)\n_L = Literal\nParserElement._literalStringClass = Literal\n\nclass Keyword(Token):\n    \"\"\"\n    Token to exactly match a specified string as a keyword, that is, it must be\n    immediately followed by a non-keyword character.  Compare with C{L{Literal}}:\n     - C{Literal(\"if\")} will match the leading C{'if'} in C{'ifAndOnlyIf'}.\n     - C{Keyword(\"if\")} will not; it will only match the leading C{'if'} in C{'if x=1'}, or C{'if(y==2)'}\n    Accepts two optional constructor arguments in addition to the keyword string:\n     - C{identChars} is a string of characters that would be valid identifier characters,\n          defaulting to all alphanumerics + \"_\" and \"$\"\n     - C{caseless} allows case-insensitive matching, default is C{False}.\n       \n    Example::\n        Keyword(\"start\").parseString(\"start\")  # -> ['start']\n        Keyword(\"start\").parseString(\"starting\")  # -> Exception\n\n    For case-insensitive matching, use L{CaselessKeyword}.\n    \"\"\"\n    DEFAULT_KEYWORD_CHARS = alphanums+\"_$\"\n\n    def __init__( self, matchString, identChars=None, caseless=False ):\n        super(Keyword,self).__init__()\n        if identChars is None:\n            identChars = Keyword.DEFAULT_KEYWORD_CHARS\n        self.match = matchString\n        self.matchLen = len(matchString)\n        try:\n            self.firstMatchChar = matchString[0]\n        except IndexError:\n            warnings.warn(\"null string passed to Keyword; use Empty() instead\",\n                            SyntaxWarning, stacklevel=2)\n        self.name = '\"%s\"' % self.match\n        self.errmsg = \"Expected \" + self.name\n        self.mayReturnEmpty = False\n        self.mayIndexError = False\n        self.caseless = caseless\n        if caseless:\n            self.caselessmatch = matchString.upper()\n            identChars = identChars.upper()\n        self.identChars = set(identChars)\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if self.caseless:\n            if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and\n                 (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) and\n                 (loc == 0 or instring[loc-1].upper() not in self.identChars) ):\n                return loc+self.matchLen, self.match\n        else:\n            if (instring[loc] == self.firstMatchChar and\n                (self.matchLen==1 or instring.startswith(self.match,loc)) and\n                (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen] not in self.identChars) and\n                (loc == 0 or instring[loc-1] not in self.identChars) ):\n                return loc+self.matchLen, self.match\n        raise ParseException(instring, loc, self.errmsg, self)\n\n    def copy(self):\n        c = super(Keyword,self).copy()\n        c.identChars = Keyword.DEFAULT_KEYWORD_CHARS\n        return c\n\n    @staticmethod\n    def setDefaultKeywordChars( chars ):\n        \"\"\"Overrides the default Keyword chars\n        \"\"\"\n        Keyword.DEFAULT_KEYWORD_CHARS = chars\n\nclass CaselessLiteral(Literal):\n    \"\"\"\n    Token to match a specified string, ignoring case of letters.\n    Note: the matched results will always be in the case of the given\n    match string, NOT the case of the input text.\n\n    Example::\n        OneOrMore(CaselessLiteral(\"CMD\")).parseString(\"cmd CMD Cmd10\") # -> ['CMD', 'CMD', 'CMD']\n        \n    (Contrast with example for L{CaselessKeyword}.)\n    \"\"\"\n    def __init__( self, matchString ):\n        super(CaselessLiteral,self).__init__( matchString.upper() )\n        # Preserve the defining literal.\n        self.returnString = matchString\n        self.name = \"'%s'\" % self.returnString\n        self.errmsg = \"Expected \" + self.name\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if instring[ loc:loc+self.matchLen ].upper() == self.match:\n            return loc+self.matchLen, self.returnString\n        raise ParseException(instring, loc, self.errmsg, self)\n\nclass CaselessKeyword(Keyword):\n    \"\"\"\n    Caseless version of L{Keyword}.\n\n    Example::\n        OneOrMore(CaselessKeyword(\"CMD\")).parseString(\"cmd CMD Cmd10\") # -> ['CMD', 'CMD']\n        \n    (Contrast with example for L{CaselessLiteral}.)\n    \"\"\"\n    def __init__( self, matchString, identChars=None ):\n        super(CaselessKeyword,self).__init__( matchString, identChars, caseless=True )\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and\n             (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) ):\n            return loc+self.matchLen, self.match\n        raise ParseException(instring, loc, self.errmsg, self)\n\nclass CloseMatch(Token):\n    \"\"\"\n    A variation on L{Literal} which matches \"close\" matches, that is, \n    strings with at most 'n' mismatching characters. C{CloseMatch} takes parameters:\n     - C{match_string} - string to be matched\n     - C{maxMismatches} - (C{default=1}) maximum number of mismatches allowed to count as a match\n    \n    The results from a successful parse will contain the matched text from the input string and the following named results:\n     - C{mismatches} - a list of the positions within the match_string where mismatches were found\n     - C{original} - the original match_string used to compare against the input string\n    \n    If C{mismatches} is an empty list, then the match was an exact match.\n    \n    Example::\n        patt = CloseMatch(\"ATCATCGAATGGA\")\n        patt.parseString(\"ATCATCGAAXGGA\") # -> (['ATCATCGAAXGGA'], {'mismatches': [[9]], 'original': ['ATCATCGAATGGA']})\n        patt.parseString(\"ATCAXCGAAXGGA\") # -> Exception: Expected 'ATCATCGAATGGA' (with up to 1 mismatches) (at char 0), (line:1, col:1)\n\n        # exact match\n        patt.parseString(\"ATCATCGAATGGA\") # -> (['ATCATCGAATGGA'], {'mismatches': [[]], 'original': ['ATCATCGAATGGA']})\n\n        # close match allowing up to 2 mismatches\n        patt = CloseMatch(\"ATCATCGAATGGA\", maxMismatches=2)\n        patt.parseString(\"ATCAXCGAAXGGA\") # -> (['ATCAXCGAAXGGA'], {'mismatches': [[4, 9]], 'original': ['ATCATCGAATGGA']})\n    \"\"\"\n    def __init__(self, match_string, maxMismatches=1):\n        super(CloseMatch,self).__init__()\n        self.name = match_string\n        self.match_string = match_string\n        self.maxMismatches = maxMismatches\n        self.errmsg = \"Expected %r (with up to %d mismatches)\" % (self.match_string, self.maxMismatches)\n        self.mayIndexError = False\n        self.mayReturnEmpty = False\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        start = loc\n        instrlen = len(instring)\n        maxloc = start + len(self.match_string)\n\n        if maxloc <= instrlen:\n            match_string = self.match_string\n            match_stringloc = 0\n            mismatches = []\n            maxMismatches = self.maxMismatches\n\n            for match_stringloc,s_m in enumerate(zip(instring[loc:maxloc], self.match_string)):\n                src,mat = s_m\n                if src != mat:\n                    mismatches.append(match_stringloc)\n                    if len(mismatches) > maxMismatches:\n                        break\n            else:\n                loc = match_stringloc + 1\n                results = ParseResults([instring[start:loc]])\n                results['original'] = self.match_string\n                results['mismatches'] = mismatches\n                return loc, results\n\n        raise ParseException(instring, loc, self.errmsg, self)\n\n\nclass Word(Token):\n    \"\"\"\n    Token for matching words composed of allowed character sets.\n    Defined with string containing all allowed initial characters,\n    an optional string containing allowed body characters (if omitted,\n    defaults to the initial character set), and an optional minimum,\n    maximum, and/or exact length.  The default value for C{min} is 1 (a\n    minimum value < 1 is not valid); the default values for C{max} and C{exact}\n    are 0, meaning no maximum or exact length restriction. An optional\n    C{excludeChars} parameter can list characters that might be found in \n    the input C{bodyChars} string; useful to define a word of all printables\n    except for one or two characters, for instance.\n    \n    L{srange} is useful for defining custom character set strings for defining \n    C{Word} expressions, using range notation from regular expression character sets.\n    \n    A common mistake is to use C{Word} to match a specific literal string, as in \n    C{Word(\"Address\")}. Remember that C{Word} uses the string argument to define\n    I{sets} of matchable characters. This expression would match \"Add\", \"AAA\",\n    \"dAred\", or any other word made up of the characters 'A', 'd', 'r', 'e', and 's'.\n    To match an exact literal string, use L{Literal} or L{Keyword}.\n\n    pyparsing includes helper strings for building Words:\n     - L{alphas}\n     - L{nums}\n     - L{alphanums}\n     - L{hexnums}\n     - L{alphas8bit} (alphabetic characters in ASCII range 128-255 - accented, tilded, umlauted, etc.)\n     - L{punc8bit} (non-alphabetic characters in ASCII range 128-255 - currency, symbols, superscripts, diacriticals, etc.)\n     - L{printables} (any non-whitespace character)\n\n    Example::\n        # a word composed of digits\n        integer = Word(nums) # equivalent to Word(\"0123456789\") or Word(srange(\"0-9\"))\n        \n        # a word with a leading capital, and zero or more lowercase\n        capital_word = Word(alphas.upper(), alphas.lower())\n\n        # hostnames are alphanumeric, with leading alpha, and '-'\n        hostname = Word(alphas, alphanums+'-')\n        \n        # roman numeral (not a strict parser, accepts invalid mix of characters)\n        roman = Word(\"IVXLCDM\")\n        \n        # any string of non-whitespace characters, except for ','\n        csv_value = Word(printables, excludeChars=\",\")\n    \"\"\"\n    def __init__( self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword=False, excludeChars=None ):\n        super(Word,self).__init__()\n        if excludeChars:\n            initChars = ''.join(c for c in initChars if c not in excludeChars)\n            if bodyChars:\n                bodyChars = ''.join(c for c in bodyChars if c not in excludeChars)\n        self.initCharsOrig = initChars\n        self.initChars = set(initChars)\n        if bodyChars :\n            self.bodyCharsOrig = bodyChars\n            self.bodyChars = set(bodyChars)\n        else:\n            self.bodyCharsOrig = initChars\n            self.bodyChars = set(initChars)\n\n        self.maxSpecified = max > 0\n\n        if min < 1:\n            raise ValueError(\"cannot specify a minimum length < 1; use Optional(Word()) if zero-length word is permitted\")\n\n        self.minLen = min\n\n        if max > 0:\n            self.maxLen = max\n        else:\n            self.maxLen = _MAX_INT\n\n        if exact > 0:\n            self.maxLen = exact\n            self.minLen = exact\n\n        self.name = _ustr(self)\n        self.errmsg = \"Expected \" + self.name\n        self.mayIndexError = False\n        self.asKeyword = asKeyword\n\n        if ' ' not in self.initCharsOrig+self.bodyCharsOrig and (min==1 and max==0 and exact==0):\n            if self.bodyCharsOrig == self.initCharsOrig:\n                self.reString = \"[%s]+\" % _escapeRegexRangeChars(self.initCharsOrig)\n            elif len(self.initCharsOrig) == 1:\n                self.reString = \"%s[%s]*\" % \\\n                                      (re.escape(self.initCharsOrig),\n                                      _escapeRegexRangeChars(self.bodyCharsOrig),)\n            else:\n                self.reString = \"[%s][%s]*\" % \\\n                                      (_escapeRegexRangeChars(self.initCharsOrig),\n                                      _escapeRegexRangeChars(self.bodyCharsOrig),)\n            if self.asKeyword:\n                self.reString = r\"\\b\"+self.reString+r\"\\b\"\n            try:\n                self.re = re.compile( self.reString )\n            except Exception:\n                self.re = None\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if self.re:\n            result = self.re.match(instring,loc)\n            if not result:\n                raise ParseException(instring, loc, self.errmsg, self)\n\n            loc = result.end()\n            return loc, result.group()\n\n        if not(instring[ loc ] in self.initChars):\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        start = loc\n        loc += 1\n        instrlen = len(instring)\n        bodychars = self.bodyChars\n        maxloc = start + self.maxLen\n        maxloc = min( maxloc, instrlen )\n        while loc < maxloc and instring[loc] in bodychars:\n            loc += 1\n\n        throwException = False\n        if loc - start < self.minLen:\n            throwException = True\n        if self.maxSpecified and loc < instrlen and instring[loc] in bodychars:\n            throwException = True\n        if self.asKeyword:\n            if (start>0 and instring[start-1] in bodychars) or (loc<instrlen and instring[loc] in bodychars):\n                throwException = True\n\n        if throwException:\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        return loc, instring[start:loc]\n\n    def __str__( self ):\n        try:\n            return super(Word,self).__str__()\n        except Exception:\n            pass\n\n\n        if self.strRepr is None:\n\n            def charsAsStr(s):\n                if len(s)>4:\n                    return s[:4]+\"...\"\n                else:\n                    return s\n\n            if ( self.initCharsOrig != self.bodyCharsOrig ):\n                self.strRepr = \"W:(%s,%s)\" % ( charsAsStr(self.initCharsOrig), charsAsStr(self.bodyCharsOrig) )\n            else:\n                self.strRepr = \"W:(%s)\" % charsAsStr(self.initCharsOrig)\n\n        return self.strRepr\n\n\nclass Regex(Token):\n    r\"\"\"\n    Token for matching strings that match a given regular expression.\n    Defined with string specifying the regular expression in a form recognized by the inbuilt Python re module.\n    If the given regex contains named groups (defined using C{(?P<name>...)}), these will be preserved as \n    named parse results.\n\n    Example::\n        realnum = Regex(r\"[+-]?\\d+\\.\\d*\")\n        date = Regex(r'(?P<year>\\d{4})-(?P<month>\\d\\d?)-(?P<day>\\d\\d?)')\n        # ref: http://stackoverflow.com/questions/267399/how-do-you-match-only-valid-roman-numerals-with-a-regular-expression\n        roman = Regex(r\"M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})\")\n    \"\"\"\n    compiledREtype = type(re.compile(\"[A-Z]\"))\n    def __init__( self, pattern, flags=0):\n        \"\"\"The parameters C{pattern} and C{flags} are passed to the C{re.compile()} function as-is. See the Python C{re} module for an explanation of the acceptable patterns and flags.\"\"\"\n        super(Regex,self).__init__()\n\n        if isinstance(pattern, basestring):\n            if not pattern:\n                warnings.warn(\"null string passed to Regex; use Empty() instead\",\n                        SyntaxWarning, stacklevel=2)\n\n            self.pattern = pattern\n            self.flags = flags\n\n            try:\n                self.re = re.compile(self.pattern, self.flags)\n                self.reString = self.pattern\n            except sre_constants.error:\n                warnings.warn(\"invalid pattern (%s) passed to Regex\" % pattern,\n                    SyntaxWarning, stacklevel=2)\n                raise\n\n        elif isinstance(pattern, Regex.compiledREtype):\n            self.re = pattern\n            self.pattern = \\\n            self.reString = str(pattern)\n            self.flags = flags\n            \n        else:\n            raise ValueError(\"Regex may only be constructed with a string or a compiled RE object\")\n\n        self.name = _ustr(self)\n        self.errmsg = \"Expected \" + self.name\n        self.mayIndexError = False\n        self.mayReturnEmpty = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        result = self.re.match(instring,loc)\n        if not result:\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        loc = result.end()\n        d = result.groupdict()\n        ret = ParseResults(result.group())\n        if d:\n            for k in d:\n                ret[k] = d[k]\n        return loc,ret\n\n    def __str__( self ):\n        try:\n            return super(Regex,self).__str__()\n        except Exception:\n            pass\n\n        if self.strRepr is None:\n            self.strRepr = \"Re:(%s)\" % repr(self.pattern)\n\n        return self.strRepr\n\n\nclass QuotedString(Token):\n    r\"\"\"\n    Token for matching strings that are delimited by quoting characters.\n    \n    Defined with the following parameters:\n        - quoteChar - string of one or more characters defining the quote delimiting string\n        - escChar - character to escape quotes, typically backslash (default=C{None})\n        - escQuote - special quote sequence to escape an embedded quote string (such as SQL's \"\" to escape an embedded \") (default=C{None})\n        - multiline - boolean indicating whether quotes can span multiple lines (default=C{False})\n        - unquoteResults - boolean indicating whether the matched text should be unquoted (default=C{True})\n        - endQuoteChar - string of one or more characters defining the end of the quote delimited string (default=C{None} => same as quoteChar)\n        - convertWhitespaceEscapes - convert escaped whitespace (C{'\\t'}, C{'\\n'}, etc.) to actual whitespace (default=C{True})\n\n    Example::\n        qs = QuotedString('\"')\n        print(qs.searchString('lsjdf \"This is the quote\" sldjf'))\n        complex_qs = QuotedString('{{', endQuoteChar='}}')\n        print(complex_qs.searchString('lsjdf {{This is the \"quote\"}} sldjf'))\n        sql_qs = QuotedString('\"', escQuote='\"\"')\n        print(sql_qs.searchString('lsjdf \"This is the quote with \"\"embedded\"\" quotes\" sldjf'))\n    prints::\n        [['This is the quote']]\n        [['This is the \"quote\"']]\n        [['This is the quote with \"embedded\" quotes']]\n    \"\"\"\n    def __init__( self, quoteChar, escChar=None, escQuote=None, multiline=False, unquoteResults=True, endQuoteChar=None, convertWhitespaceEscapes=True):\n        super(QuotedString,self).__init__()\n\n        # remove white space from quote chars - wont work anyway\n        quoteChar = quoteChar.strip()\n        if not quoteChar:\n            warnings.warn(\"quoteChar cannot be the empty string\",SyntaxWarning,stacklevel=2)\n            raise SyntaxError()\n\n        if endQuoteChar is None:\n            endQuoteChar = quoteChar\n        else:\n            endQuoteChar = endQuoteChar.strip()\n            if not endQuoteChar:\n                warnings.warn(\"endQuoteChar cannot be the empty string\",SyntaxWarning,stacklevel=2)\n                raise SyntaxError()\n\n        self.quoteChar = quoteChar\n        self.quoteCharLen = len(quoteChar)\n        self.firstQuoteChar = quoteChar[0]\n        self.endQuoteChar = endQuoteChar\n        self.endQuoteCharLen = len(endQuoteChar)\n        self.escChar = escChar\n        self.escQuote = escQuote\n        self.unquoteResults = unquoteResults\n        self.convertWhitespaceEscapes = convertWhitespaceEscapes\n\n        if multiline:\n            self.flags = re.MULTILINE | re.DOTALL\n            self.pattern = r'%s(?:[^%s%s]' % \\\n                ( re.escape(self.quoteChar),\n                  _escapeRegexRangeChars(self.endQuoteChar[0]),\n                  (escChar is not None and _escapeRegexRangeChars(escChar) or '') )\n        else:\n            self.flags = 0\n            self.pattern = r'%s(?:[^%s\\n\\r%s]' % \\\n                ( re.escape(self.quoteChar),\n                  _escapeRegexRangeChars(self.endQuoteChar[0]),\n                  (escChar is not None and _escapeRegexRangeChars(escChar) or '') )\n        if len(self.endQuoteChar) > 1:\n            self.pattern += (\n                '|(?:' + ')|(?:'.join(\"%s[^%s]\" % (re.escape(self.endQuoteChar[:i]),\n                                               _escapeRegexRangeChars(self.endQuoteChar[i]))\n                                    for i in range(len(self.endQuoteChar)-1,0,-1)) + ')'\n                )\n        if escQuote:\n            self.pattern += (r'|(?:%s)' % re.escape(escQuote))\n        if escChar:\n            self.pattern += (r'|(?:%s.)' % re.escape(escChar))\n            self.escCharReplacePattern = re.escape(self.escChar)+\"(.)\"\n        self.pattern += (r')*%s' % re.escape(self.endQuoteChar))\n\n        try:\n            self.re = re.compile(self.pattern, self.flags)\n            self.reString = self.pattern\n        except sre_constants.error:\n            warnings.warn(\"invalid pattern (%s) passed to Regex\" % self.pattern,\n                SyntaxWarning, stacklevel=2)\n            raise\n\n        self.name = _ustr(self)\n        self.errmsg = \"Expected \" + self.name\n        self.mayIndexError = False\n        self.mayReturnEmpty = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        result = instring[loc] == self.firstQuoteChar and self.re.match(instring,loc) or None\n        if not result:\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        loc = result.end()\n        ret = result.group()\n\n        if self.unquoteResults:\n\n            # strip off quotes\n            ret = ret[self.quoteCharLen:-self.endQuoteCharLen]\n\n            if isinstance(ret,basestring):\n                # replace escaped whitespace\n                if '\\\\' in ret and self.convertWhitespaceEscapes:\n                    ws_map = {\n                        r'\\t' : '\\t',\n                        r'\\n' : '\\n',\n                        r'\\f' : '\\f',\n                        r'\\r' : '\\r',\n                    }\n                    for wslit,wschar in ws_map.items():\n                        ret = ret.replace(wslit, wschar)\n\n                # replace escaped characters\n                if self.escChar:\n                    ret = re.sub(self.escCharReplacePattern, r\"\\g<1>\", ret)\n\n                # replace escaped quotes\n                if self.escQuote:\n                    ret = ret.replace(self.escQuote, self.endQuoteChar)\n\n        return loc, ret\n\n    def __str__( self ):\n        try:\n            return super(QuotedString,self).__str__()\n        except Exception:\n            pass\n\n        if self.strRepr is None:\n            self.strRepr = \"quoted string, starting with %s ending with %s\" % (self.quoteChar, self.endQuoteChar)\n\n        return self.strRepr\n\n\nclass CharsNotIn(Token):\n    \"\"\"\n    Token for matching words composed of characters I{not} in a given set (will\n    include whitespace in matched characters if not listed in the provided exclusion set - see example).\n    Defined with string containing all disallowed characters, and an optional\n    minimum, maximum, and/or exact length.  The default value for C{min} is 1 (a\n    minimum value < 1 is not valid); the default values for C{max} and C{exact}\n    are 0, meaning no maximum or exact length restriction.\n\n    Example::\n        # define a comma-separated-value as anything that is not a ','\n        csv_value = CharsNotIn(',')\n        print(delimitedList(csv_value).parseString(\"dkls,lsdkjf,s12 34,@!#,213\"))\n    prints::\n        ['dkls', 'lsdkjf', 's12 34', '@!#', '213']\n    \"\"\"\n    def __init__( self, notChars, min=1, max=0, exact=0 ):\n        super(CharsNotIn,self).__init__()\n        self.skipWhitespace = False\n        self.notChars = notChars\n\n        if min < 1:\n            raise ValueError(\"cannot specify a minimum length < 1; use Optional(CharsNotIn()) if zero-length char group is permitted\")\n\n        self.minLen = min\n\n        if max > 0:\n            self.maxLen = max\n        else:\n            self.maxLen = _MAX_INT\n\n        if exact > 0:\n            self.maxLen = exact\n            self.minLen = exact\n\n        self.name = _ustr(self)\n        self.errmsg = \"Expected \" + self.name\n        self.mayReturnEmpty = ( self.minLen == 0 )\n        self.mayIndexError = False\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if instring[loc] in self.notChars:\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        start = loc\n        loc += 1\n        notchars = self.notChars\n        maxlen = min( start+self.maxLen, len(instring) )\n        while loc < maxlen and \\\n              (instring[loc] not in notchars):\n            loc += 1\n\n        if loc - start < self.minLen:\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        return loc, instring[start:loc]\n\n    def __str__( self ):\n        try:\n            return super(CharsNotIn, self).__str__()\n        except Exception:\n            pass\n\n        if self.strRepr is None:\n            if len(self.notChars) > 4:\n                self.strRepr = \"!W:(%s...)\" % self.notChars[:4]\n            else:\n                self.strRepr = \"!W:(%s)\" % self.notChars\n\n        return self.strRepr\n\nclass White(Token):\n    \"\"\"\n    Special matching class for matching whitespace.  Normally, whitespace is ignored\n    by pyparsing grammars.  This class is included when some whitespace structures\n    are significant.  Define with a string containing the whitespace characters to be\n    matched; default is C{\" \\\\t\\\\r\\\\n\"}.  Also takes optional C{min}, C{max}, and C{exact} arguments,\n    as defined for the C{L{Word}} class.\n    \"\"\"\n    whiteStrs = {\n        \" \" : \"<SPC>\",\n        \"\\t\": \"<TAB>\",\n        \"\\n\": \"<LF>\",\n        \"\\r\": \"<CR>\",\n        \"\\f\": \"<FF>\",\n        }\n    def __init__(self, ws=\" \\t\\r\\n\", min=1, max=0, exact=0):\n        super(White,self).__init__()\n        self.matchWhite = ws\n        self.setWhitespaceChars( \"\".join(c for c in self.whiteChars if c not in self.matchWhite) )\n        #~ self.leaveWhitespace()\n        self.name = (\"\".join(White.whiteStrs[c] for c in self.matchWhite))\n        self.mayReturnEmpty = True\n        self.errmsg = \"Expected \" + self.name\n\n        self.minLen = min\n\n        if max > 0:\n            self.maxLen = max\n        else:\n            self.maxLen = _MAX_INT\n\n        if exact > 0:\n            self.maxLen = exact\n            self.minLen = exact\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if not(instring[ loc ] in self.matchWhite):\n            raise ParseException(instring, loc, self.errmsg, self)\n        start = loc\n        loc += 1\n        maxloc = start + self.maxLen\n        maxloc = min( maxloc, len(instring) )\n        while loc < maxloc and instring[loc] in self.matchWhite:\n            loc += 1\n\n        if loc - start < self.minLen:\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        return loc, instring[start:loc]\n\n\nclass _PositionToken(Token):\n    def __init__( self ):\n        super(_PositionToken,self).__init__()\n        self.name=self.__class__.__name__\n        self.mayReturnEmpty = True\n        self.mayIndexError = False\n\nclass GoToColumn(_PositionToken):\n    \"\"\"\n    Token to advance to a specific column of input text; useful for tabular report scraping.\n    \"\"\"\n    def __init__( self, colno ):\n        super(GoToColumn,self).__init__()\n        self.col = colno\n\n    def preParse( self, instring, loc ):\n        if col(loc,instring) != self.col:\n            instrlen = len(instring)\n            if self.ignoreExprs:\n                loc = self._skipIgnorables( instring, loc )\n            while loc < instrlen and instring[loc].isspace() and col( loc, instring ) != self.col :\n                loc += 1\n        return loc\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        thiscol = col( loc, instring )\n        if thiscol > self.col:\n            raise ParseException( instring, loc, \"Text not in expected column\", self )\n        newloc = loc + self.col - thiscol\n        ret = instring[ loc: newloc ]\n        return newloc, ret\n\n\nclass LineStart(_PositionToken):\n    \"\"\"\n    Matches if current position is at the beginning of a line within the parse string\n    \n    Example::\n    \n        test = '''\\\n        AAA this line\n        AAA and this line\n          AAA but not this one\n        B AAA and definitely not this one\n        '''\n\n        for t in (LineStart() + 'AAA' + restOfLine).searchString(test):\n            print(t)\n    \n    Prints::\n        ['AAA', ' this line']\n        ['AAA', ' and this line']    \n\n    \"\"\"\n    def __init__( self ):\n        super(LineStart,self).__init__()\n        self.errmsg = \"Expected start of line\"\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if col(loc, instring) == 1:\n            return loc, []\n        raise ParseException(instring, loc, self.errmsg, self)\n\nclass LineEnd(_PositionToken):\n    \"\"\"\n    Matches if current position is at the end of a line within the parse string\n    \"\"\"\n    def __init__( self ):\n        super(LineEnd,self).__init__()\n        self.setWhitespaceChars( ParserElement.DEFAULT_WHITE_CHARS.replace(\"\\n\",\"\") )\n        self.errmsg = \"Expected end of line\"\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if loc<len(instring):\n            if instring[loc] == \"\\n\":\n                return loc+1, \"\\n\"\n            else:\n                raise ParseException(instring, loc, self.errmsg, self)\n        elif loc == len(instring):\n            return loc+1, []\n        else:\n            raise ParseException(instring, loc, self.errmsg, self)\n\nclass StringStart(_PositionToken):\n    \"\"\"\n    Matches if current position is at the beginning of the parse string\n    \"\"\"\n    def __init__( self ):\n        super(StringStart,self).__init__()\n        self.errmsg = \"Expected start of text\"\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if loc != 0:\n            # see if entire string up to here is just whitespace and ignoreables\n            if loc != self.preParse( instring, 0 ):\n                raise ParseException(instring, loc, self.errmsg, self)\n        return loc, []\n\nclass StringEnd(_PositionToken):\n    \"\"\"\n    Matches if current position is at the end of the parse string\n    \"\"\"\n    def __init__( self ):\n        super(StringEnd,self).__init__()\n        self.errmsg = \"Expected end of text\"\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if loc < len(instring):\n            raise ParseException(instring, loc, self.errmsg, self)\n        elif loc == len(instring):\n            return loc+1, []\n        elif loc > len(instring):\n            return loc, []\n        else:\n            raise ParseException(instring, loc, self.errmsg, self)\n\nclass WordStart(_PositionToken):\n    \"\"\"\n    Matches if the current position is at the beginning of a Word, and\n    is not preceded by any character in a given set of C{wordChars}\n    (default=C{printables}). To emulate the C{\\b} behavior of regular expressions,\n    use C{WordStart(alphanums)}. C{WordStart} will also match at the beginning of\n    the string being parsed, or at the beginning of a line.\n    \"\"\"\n    def __init__(self, wordChars = printables):\n        super(WordStart,self).__init__()\n        self.wordChars = set(wordChars)\n        self.errmsg = \"Not at the start of a word\"\n\n    def parseImpl(self, instring, loc, doActions=True ):\n        if loc != 0:\n            if (instring[loc-1] in self.wordChars or\n                instring[loc] not in self.wordChars):\n                raise ParseException(instring, loc, self.errmsg, self)\n        return loc, []\n\nclass WordEnd(_PositionToken):\n    \"\"\"\n    Matches if the current position is at the end of a Word, and\n    is not followed by any character in a given set of C{wordChars}\n    (default=C{printables}). To emulate the C{\\b} behavior of regular expressions,\n    use C{WordEnd(alphanums)}. C{WordEnd} will also match at the end of\n    the string being parsed, or at the end of a line.\n    \"\"\"\n    def __init__(self, wordChars = printables):\n        super(WordEnd,self).__init__()\n        self.wordChars = set(wordChars)\n        self.skipWhitespace = False\n        self.errmsg = \"Not at the end of a word\"\n\n    def parseImpl(self, instring, loc, doActions=True ):\n        instrlen = len(instring)\n        if instrlen>0 and loc<instrlen:\n            if (instring[loc] in self.wordChars or\n                instring[loc-1] not in self.wordChars):\n                raise ParseException(instring, loc, self.errmsg, self)\n        return loc, []\n\n\nclass ParseExpression(ParserElement):\n    \"\"\"\n    Abstract subclass of ParserElement, for combining and post-processing parsed tokens.\n    \"\"\"\n    def __init__( self, exprs, savelist = False ):\n        super(ParseExpression,self).__init__(savelist)\n        if isinstance( exprs, _generatorType ):\n            exprs = list(exprs)\n\n        if isinstance( exprs, basestring ):\n            self.exprs = [ ParserElement._literalStringClass( exprs ) ]\n        elif isinstance( exprs, Iterable ):\n            exprs = list(exprs)\n            # if sequence of strings provided, wrap with Literal\n            if all(isinstance(expr, basestring) for expr in exprs):\n                exprs = map(ParserElement._literalStringClass, exprs)\n            self.exprs = list(exprs)\n        else:\n            try:\n                self.exprs = list( exprs )\n            except TypeError:\n                self.exprs = [ exprs ]\n        self.callPreparse = False\n\n    def __getitem__( self, i ):\n        return self.exprs[i]\n\n    def append( self, other ):\n        self.exprs.append( other )\n        self.strRepr = None\n        return self\n\n    def leaveWhitespace( self ):\n        \"\"\"Extends C{leaveWhitespace} defined in base class, and also invokes C{leaveWhitespace} on\n           all contained expressions.\"\"\"\n        self.skipWhitespace = False\n        self.exprs = [ e.copy() for e in self.exprs ]\n        for e in self.exprs:\n            e.leaveWhitespace()\n        return self\n\n    def ignore( self, other ):\n        if isinstance( other, Suppress ):\n            if other not in self.ignoreExprs:\n                super( ParseExpression, self).ignore( other )\n                for e in self.exprs:\n                    e.ignore( self.ignoreExprs[-1] )\n        else:\n            super( ParseExpression, self).ignore( other )\n            for e in self.exprs:\n                e.ignore( self.ignoreExprs[-1] )\n        return self\n\n    def __str__( self ):\n        try:\n            return super(ParseExpression,self).__str__()\n        except Exception:\n            pass\n\n        if self.strRepr is None:\n            self.strRepr = \"%s:(%s)\" % ( self.__class__.__name__, _ustr(self.exprs) )\n        return self.strRepr\n\n    def streamline( self ):\n        super(ParseExpression,self).streamline()\n\n        for e in self.exprs:\n            e.streamline()\n\n        # collapse nested And's of the form And( And( And( a,b), c), d) to And( a,b,c,d )\n        # but only if there are no parse actions or resultsNames on the nested And's\n        # (likewise for Or's and MatchFirst's)\n        if ( len(self.exprs) == 2 ):\n            other = self.exprs[0]\n            if ( isinstance( other, self.__class__ ) and\n                  not(other.parseAction) and\n                  other.resultsName is None and\n                  not other.debug ):\n                self.exprs = other.exprs[:] + [ self.exprs[1] ]\n                self.strRepr = None\n                self.mayReturnEmpty |= other.mayReturnEmpty\n                self.mayIndexError  |= other.mayIndexError\n\n            other = self.exprs[-1]\n            if ( isinstance( other, self.__class__ ) and\n                  not(other.parseAction) and\n                  other.resultsName is None and\n                  not other.debug ):\n                self.exprs = self.exprs[:-1] + other.exprs[:]\n                self.strRepr = None\n                self.mayReturnEmpty |= other.mayReturnEmpty\n                self.mayIndexError  |= other.mayIndexError\n\n        self.errmsg = \"Expected \" + _ustr(self)\n        \n        return self\n\n    def setResultsName( self, name, listAllMatches=False ):\n        ret = super(ParseExpression,self).setResultsName(name,listAllMatches)\n        return ret\n\n    def validate( self, validateTrace=[] ):\n        tmp = validateTrace[:]+[self]\n        for e in self.exprs:\n            e.validate(tmp)\n        self.checkRecursion( [] )\n        \n    def copy(self):\n        ret = super(ParseExpression,self).copy()\n        ret.exprs = [e.copy() for e in self.exprs]\n        return ret\n\nclass And(ParseExpression):\n    \"\"\"\n    Requires all given C{ParseExpression}s to be found in the given order.\n    Expressions may be separated by whitespace.\n    May be constructed using the C{'+'} operator.\n    May also be constructed using the C{'-'} operator, which will suppress backtracking.\n\n    Example::\n        integer = Word(nums)\n        name_expr = OneOrMore(Word(alphas))\n\n        expr = And([integer(\"id\"),name_expr(\"name\"),integer(\"age\")])\n        # more easily written as:\n        expr = integer(\"id\") + name_expr(\"name\") + integer(\"age\")\n    \"\"\"\n\n    class _ErrorStop(Empty):\n        def __init__(self, *args, **kwargs):\n            super(And._ErrorStop,self).__init__(*args, **kwargs)\n            self.name = '-'\n            self.leaveWhitespace()\n\n    def __init__( self, exprs, savelist = True ):\n        super(And,self).__init__(exprs, savelist)\n        self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)\n        self.setWhitespaceChars( self.exprs[0].whiteChars )\n        self.skipWhitespace = self.exprs[0].skipWhitespace\n        self.callPreparse = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        # pass False as last arg to _parse for first element, since we already\n        # pre-parsed the string as part of our And pre-parsing\n        loc, resultlist = self.exprs[0]._parse( instring, loc, doActions, callPreParse=False )\n        errorStop = False\n        for e in self.exprs[1:]:\n            if isinstance(e, And._ErrorStop):\n                errorStop = True\n                continue\n            if errorStop:\n                try:\n                    loc, exprtokens = e._parse( instring, loc, doActions )\n                except ParseSyntaxException:\n                    raise\n                except ParseBaseException as pe:\n                    pe.__traceback__ = None\n                    raise ParseSyntaxException._from_exception(pe)\n                except IndexError:\n                    raise ParseSyntaxException(instring, len(instring), self.errmsg, self)\n            else:\n                loc, exprtokens = e._parse( instring, loc, doActions )\n            if exprtokens or exprtokens.haskeys():\n                resultlist += exprtokens\n        return loc, resultlist\n\n    def __iadd__(self, other ):\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        return self.append( other ) #And( [ self, other ] )\n\n    def checkRecursion( self, parseElementList ):\n        subRecCheckList = parseElementList[:] + [ self ]\n        for e in self.exprs:\n            e.checkRecursion( subRecCheckList )\n            if not e.mayReturnEmpty:\n                break\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"{\" + \" \".join(_ustr(e) for e in self.exprs) + \"}\"\n\n        return self.strRepr\n\n\nclass Or(ParseExpression):\n    \"\"\"\n    Requires that at least one C{ParseExpression} is found.\n    If two expressions match, the expression that matches the longest string will be used.\n    May be constructed using the C{'^'} operator.\n\n    Example::\n        # construct Or using '^' operator\n        \n        number = Word(nums) ^ Combine(Word(nums) + '.' + Word(nums))\n        print(number.searchString(\"123 3.1416 789\"))\n    prints::\n        [['123'], ['3.1416'], ['789']]\n    \"\"\"\n    def __init__( self, exprs, savelist = False ):\n        super(Or,self).__init__(exprs, savelist)\n        if self.exprs:\n            self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)\n        else:\n            self.mayReturnEmpty = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        maxExcLoc = -1\n        maxException = None\n        matches = []\n        for e in self.exprs:\n            try:\n                loc2 = e.tryParse( instring, loc )\n            except ParseException as err:\n                err.__traceback__ = None\n                if err.loc > maxExcLoc:\n                    maxException = err\n                    maxExcLoc = err.loc\n            except IndexError:\n                if len(instring) > maxExcLoc:\n                    maxException = ParseException(instring,len(instring),e.errmsg,self)\n                    maxExcLoc = len(instring)\n            else:\n                # save match among all matches, to retry longest to shortest\n                matches.append((loc2, e))\n\n        if matches:\n            matches.sort(key=lambda x: -x[0])\n            for _,e in matches:\n                try:\n                    return e._parse( instring, loc, doActions )\n                except ParseException as err:\n                    err.__traceback__ = None\n                    if err.loc > maxExcLoc:\n                        maxException = err\n                        maxExcLoc = err.loc\n\n        if maxException is not None:\n            maxException.msg = self.errmsg\n            raise maxException\n        else:\n            raise ParseException(instring, loc, \"no defined alternatives to match\", self)\n\n\n    def __ixor__(self, other ):\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        return self.append( other ) #Or( [ self, other ] )\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"{\" + \" ^ \".join(_ustr(e) for e in self.exprs) + \"}\"\n\n        return self.strRepr\n\n    def checkRecursion( self, parseElementList ):\n        subRecCheckList = parseElementList[:] + [ self ]\n        for e in self.exprs:\n            e.checkRecursion( subRecCheckList )\n\n\nclass MatchFirst(ParseExpression):\n    \"\"\"\n    Requires that at least one C{ParseExpression} is found.\n    If two expressions match, the first one listed is the one that will match.\n    May be constructed using the C{'|'} operator.\n\n    Example::\n        # construct MatchFirst using '|' operator\n        \n        # watch the order of expressions to match\n        number = Word(nums) | Combine(Word(nums) + '.' + Word(nums))\n        print(number.searchString(\"123 3.1416 789\")) #  Fail! -> [['123'], ['3'], ['1416'], ['789']]\n\n        # put more selective expression first\n        number = Combine(Word(nums) + '.' + Word(nums)) | Word(nums)\n        print(number.searchString(\"123 3.1416 789\")) #  Better -> [['123'], ['3.1416'], ['789']]\n    \"\"\"\n    def __init__( self, exprs, savelist = False ):\n        super(MatchFirst,self).__init__(exprs, savelist)\n        if self.exprs:\n            self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)\n        else:\n            self.mayReturnEmpty = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        maxExcLoc = -1\n        maxException = None\n        for e in self.exprs:\n            try:\n                ret = e._parse( instring, loc, doActions )\n                return ret\n            except ParseException as err:\n                if err.loc > maxExcLoc:\n                    maxException = err\n                    maxExcLoc = err.loc\n            except IndexError:\n                if len(instring) > maxExcLoc:\n                    maxException = ParseException(instring,len(instring),e.errmsg,self)\n                    maxExcLoc = len(instring)\n\n        # only got here if no expression matched, raise exception for match that made it the furthest\n        else:\n            if maxException is not None:\n                maxException.msg = self.errmsg\n                raise maxException\n            else:\n                raise ParseException(instring, loc, \"no defined alternatives to match\", self)\n\n    def __ior__(self, other ):\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass( other )\n        return self.append( other ) #MatchFirst( [ self, other ] )\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"{\" + \" | \".join(_ustr(e) for e in self.exprs) + \"}\"\n\n        return self.strRepr\n\n    def checkRecursion( self, parseElementList ):\n        subRecCheckList = parseElementList[:] + [ self ]\n        for e in self.exprs:\n            e.checkRecursion( subRecCheckList )\n\n\nclass Each(ParseExpression):\n    \"\"\"\n    Requires all given C{ParseExpression}s to be found, but in any order.\n    Expressions may be separated by whitespace.\n    May be constructed using the C{'&'} operator.\n\n    Example::\n        color = oneOf(\"RED ORANGE YELLOW GREEN BLUE PURPLE BLACK WHITE BROWN\")\n        shape_type = oneOf(\"SQUARE CIRCLE TRIANGLE STAR HEXAGON OCTAGON\")\n        integer = Word(nums)\n        shape_attr = \"shape:\" + shape_type(\"shape\")\n        posn_attr = \"posn:\" + Group(integer(\"x\") + ',' + integer(\"y\"))(\"posn\")\n        color_attr = \"color:\" + color(\"color\")\n        size_attr = \"size:\" + integer(\"size\")\n\n        # use Each (using operator '&') to accept attributes in any order \n        # (shape and posn are required, color and size are optional)\n        shape_spec = shape_attr & posn_attr & Optional(color_attr) & Optional(size_attr)\n\n        shape_spec.runTests('''\n            shape: SQUARE color: BLACK posn: 100, 120\n            shape: CIRCLE size: 50 color: BLUE posn: 50,80\n            color:GREEN size:20 shape:TRIANGLE posn:20,40\n            '''\n            )\n    prints::\n        shape: SQUARE color: BLACK posn: 100, 120\n        ['shape:', 'SQUARE', 'color:', 'BLACK', 'posn:', ['100', ',', '120']]\n        - color: BLACK\n        - posn: ['100', ',', '120']\n          - x: 100\n          - y: 120\n        - shape: SQUARE\n\n\n        shape: CIRCLE size: 50 color: BLUE posn: 50,80\n        ['shape:', 'CIRCLE', 'size:', '50', 'color:', 'BLUE', 'posn:', ['50', ',', '80']]\n        - color: BLUE\n        - posn: ['50', ',', '80']\n          - x: 50\n          - y: 80\n        - shape: CIRCLE\n        - size: 50\n\n\n        color: GREEN size: 20 shape: TRIANGLE posn: 20,40\n        ['color:', 'GREEN', 'size:', '20', 'shape:', 'TRIANGLE', 'posn:', ['20', ',', '40']]\n        - color: GREEN\n        - posn: ['20', ',', '40']\n          - x: 20\n          - y: 40\n        - shape: TRIANGLE\n        - size: 20\n    \"\"\"\n    def __init__( self, exprs, savelist = True ):\n        super(Each,self).__init__(exprs, savelist)\n        self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)\n        self.skipWhitespace = True\n        self.initExprGroups = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if self.initExprGroups:\n            self.opt1map = dict((id(e.expr),e) for e in self.exprs if isinstance(e,Optional))\n            opt1 = [ e.expr for e in self.exprs if isinstance(e,Optional) ]\n            opt2 = [ e for e in self.exprs if e.mayReturnEmpty and not isinstance(e,Optional)]\n            self.optionals = opt1 + opt2\n            self.multioptionals = [ e.expr for e in self.exprs if isinstance(e,ZeroOrMore) ]\n            self.multirequired = [ e.expr for e in self.exprs if isinstance(e,OneOrMore) ]\n            self.required = [ e for e in self.exprs if not isinstance(e,(Optional,ZeroOrMore,OneOrMore)) ]\n            self.required += self.multirequired\n            self.initExprGroups = False\n        tmpLoc = loc\n        tmpReqd = self.required[:]\n        tmpOpt  = self.optionals[:]\n        matchOrder = []\n\n        keepMatching = True\n        while keepMatching:\n            tmpExprs = tmpReqd + tmpOpt + self.multioptionals + self.multirequired\n            failed = []\n            for e in tmpExprs:\n                try:\n                    tmpLoc = e.tryParse( instring, tmpLoc )\n                except ParseException:\n                    failed.append(e)\n                else:\n                    matchOrder.append(self.opt1map.get(id(e),e))\n                    if e in tmpReqd:\n                        tmpReqd.remove(e)\n                    elif e in tmpOpt:\n                        tmpOpt.remove(e)\n            if len(failed) == len(tmpExprs):\n                keepMatching = False\n\n        if tmpReqd:\n            missing = \", \".join(_ustr(e) for e in tmpReqd)\n            raise ParseException(instring,loc,\"Missing one or more required elements (%s)\" % missing )\n\n        # add any unmatched Optionals, in case they have default values defined\n        matchOrder += [e for e in self.exprs if isinstance(e,Optional) and e.expr in tmpOpt]\n\n        resultlist = []\n        for e in matchOrder:\n            loc,results = e._parse(instring,loc,doActions)\n            resultlist.append(results)\n\n        finalResults = sum(resultlist, ParseResults([]))\n        return loc, finalResults\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"{\" + \" & \".join(_ustr(e) for e in self.exprs) + \"}\"\n\n        return self.strRepr\n\n    def checkRecursion( self, parseElementList ):\n        subRecCheckList = parseElementList[:] + [ self ]\n        for e in self.exprs:\n            e.checkRecursion( subRecCheckList )\n\n\nclass ParseElementEnhance(ParserElement):\n    \"\"\"\n    Abstract subclass of C{ParserElement}, for combining and post-processing parsed tokens.\n    \"\"\"\n    def __init__( self, expr, savelist=False ):\n        super(ParseElementEnhance,self).__init__(savelist)\n        if isinstance( expr, basestring ):\n            if issubclass(ParserElement._literalStringClass, Token):\n                expr = ParserElement._literalStringClass(expr)\n            else:\n                expr = ParserElement._literalStringClass(Literal(expr))\n        self.expr = expr\n        self.strRepr = None\n        if expr is not None:\n            self.mayIndexError = expr.mayIndexError\n            self.mayReturnEmpty = expr.mayReturnEmpty\n            self.setWhitespaceChars( expr.whiteChars )\n            self.skipWhitespace = expr.skipWhitespace\n            self.saveAsList = expr.saveAsList\n            self.callPreparse = expr.callPreparse\n            self.ignoreExprs.extend(expr.ignoreExprs)\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if self.expr is not None:\n            return self.expr._parse( instring, loc, doActions, callPreParse=False )\n        else:\n            raise ParseException(\"\",loc,self.errmsg,self)\n\n    def leaveWhitespace( self ):\n        self.skipWhitespace = False\n        self.expr = self.expr.copy()\n        if self.expr is not None:\n            self.expr.leaveWhitespace()\n        return self\n\n    def ignore( self, other ):\n        if isinstance( other, Suppress ):\n            if other not in self.ignoreExprs:\n                super( ParseElementEnhance, self).ignore( other )\n                if self.expr is not None:\n                    self.expr.ignore( self.ignoreExprs[-1] )\n        else:\n            super( ParseElementEnhance, self).ignore( other )\n            if self.expr is not None:\n                self.expr.ignore( self.ignoreExprs[-1] )\n        return self\n\n    def streamline( self ):\n        super(ParseElementEnhance,self).streamline()\n        if self.expr is not None:\n            self.expr.streamline()\n        return self\n\n    def checkRecursion( self, parseElementList ):\n        if self in parseElementList:\n            raise RecursiveGrammarException( parseElementList+[self] )\n        subRecCheckList = parseElementList[:] + [ self ]\n        if self.expr is not None:\n            self.expr.checkRecursion( subRecCheckList )\n\n    def validate( self, validateTrace=[] ):\n        tmp = validateTrace[:]+[self]\n        if self.expr is not None:\n            self.expr.validate(tmp)\n        self.checkRecursion( [] )\n\n    def __str__( self ):\n        try:\n            return super(ParseElementEnhance,self).__str__()\n        except Exception:\n            pass\n\n        if self.strRepr is None and self.expr is not None:\n            self.strRepr = \"%s:(%s)\" % ( self.__class__.__name__, _ustr(self.expr) )\n        return self.strRepr\n\n\nclass FollowedBy(ParseElementEnhance):\n    \"\"\"\n    Lookahead matching of the given parse expression.  C{FollowedBy}\n    does I{not} advance the parsing position within the input string, it only\n    verifies that the specified parse expression matches at the current\n    position.  C{FollowedBy} always returns a null token list.\n\n    Example::\n        # use FollowedBy to match a label only if it is followed by a ':'\n        data_word = Word(alphas)\n        label = data_word + FollowedBy(':')\n        attr_expr = Group(label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))\n        \n        OneOrMore(attr_expr).parseString(\"shape: SQUARE color: BLACK posn: upper left\").pprint()\n    prints::\n        [['shape', 'SQUARE'], ['color', 'BLACK'], ['posn', 'upper left']]\n    \"\"\"\n    def __init__( self, expr ):\n        super(FollowedBy,self).__init__(expr)\n        self.mayReturnEmpty = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        self.expr.tryParse( instring, loc )\n        return loc, []\n\n\nclass NotAny(ParseElementEnhance):\n    \"\"\"\n    Lookahead to disallow matching with the given parse expression.  C{NotAny}\n    does I{not} advance the parsing position within the input string, it only\n    verifies that the specified parse expression does I{not} match at the current\n    position.  Also, C{NotAny} does I{not} skip over leading whitespace. C{NotAny}\n    always returns a null token list.  May be constructed using the '~' operator.\n\n    Example::\n        \n    \"\"\"\n    def __init__( self, expr ):\n        super(NotAny,self).__init__(expr)\n        #~ self.leaveWhitespace()\n        self.skipWhitespace = False  # do NOT use self.leaveWhitespace(), don't want to propagate to exprs\n        self.mayReturnEmpty = True\n        self.errmsg = \"Found unwanted token, \"+_ustr(self.expr)\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        if self.expr.canParseNext(instring, loc):\n            raise ParseException(instring, loc, self.errmsg, self)\n        return loc, []\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"~{\" + _ustr(self.expr) + \"}\"\n\n        return self.strRepr\n\nclass _MultipleMatch(ParseElementEnhance):\n    def __init__( self, expr, stopOn=None):\n        super(_MultipleMatch, self).__init__(expr)\n        self.saveAsList = True\n        ender = stopOn\n        if isinstance(ender, basestring):\n            ender = ParserElement._literalStringClass(ender)\n        self.not_ender = ~ender if ender is not None else None\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        self_expr_parse = self.expr._parse\n        self_skip_ignorables = self._skipIgnorables\n        check_ender = self.not_ender is not None\n        if check_ender:\n            try_not_ender = self.not_ender.tryParse\n        \n        # must be at least one (but first see if we are the stopOn sentinel;\n        # if so, fail)\n        if check_ender:\n            try_not_ender(instring, loc)\n        loc, tokens = self_expr_parse( instring, loc, doActions, callPreParse=False )\n        try:\n            hasIgnoreExprs = (not not self.ignoreExprs)\n            while 1:\n                if check_ender:\n                    try_not_ender(instring, loc)\n                if hasIgnoreExprs:\n                    preloc = self_skip_ignorables( instring, loc )\n                else:\n                    preloc = loc\n                loc, tmptokens = self_expr_parse( instring, preloc, doActions )\n                if tmptokens or tmptokens.haskeys():\n                    tokens += tmptokens\n        except (ParseException,IndexError):\n            pass\n\n        return loc, tokens\n        \nclass OneOrMore(_MultipleMatch):\n    \"\"\"\n    Repetition of one or more of the given expression.\n    \n    Parameters:\n     - expr - expression that must match one or more times\n     - stopOn - (default=C{None}) - expression for a terminating sentinel\n          (only required if the sentinel would ordinarily match the repetition \n          expression)          \n\n    Example::\n        data_word = Word(alphas)\n        label = data_word + FollowedBy(':')\n        attr_expr = Group(label + Suppress(':') + OneOrMore(data_word).setParseAction(' '.join))\n\n        text = \"shape: SQUARE posn: upper left color: BLACK\"\n        OneOrMore(attr_expr).parseString(text).pprint()  # Fail! read 'color' as data instead of next label -> [['shape', 'SQUARE color']]\n\n        # use stopOn attribute for OneOrMore to avoid reading label string as part of the data\n        attr_expr = Group(label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))\n        OneOrMore(attr_expr).parseString(text).pprint() # Better -> [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'BLACK']]\n        \n        # could also be written as\n        (attr_expr * (1,)).parseString(text).pprint()\n    \"\"\"\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"{\" + _ustr(self.expr) + \"}...\"\n\n        return self.strRepr\n\nclass ZeroOrMore(_MultipleMatch):\n    \"\"\"\n    Optional repetition of zero or more of the given expression.\n    \n    Parameters:\n     - expr - expression that must match zero or more times\n     - stopOn - (default=C{None}) - expression for a terminating sentinel\n          (only required if the sentinel would ordinarily match the repetition \n          expression)          \n\n    Example: similar to L{OneOrMore}\n    \"\"\"\n    def __init__( self, expr, stopOn=None):\n        super(ZeroOrMore,self).__init__(expr, stopOn=stopOn)\n        self.mayReturnEmpty = True\n        \n    def parseImpl( self, instring, loc, doActions=True ):\n        try:\n            return super(ZeroOrMore, self).parseImpl(instring, loc, doActions)\n        except (ParseException,IndexError):\n            return loc, []\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"[\" + _ustr(self.expr) + \"]...\"\n\n        return self.strRepr\n\nclass _NullToken(object):\n    def __bool__(self):\n        return False\n    __nonzero__ = __bool__\n    def __str__(self):\n        return \"\"\n\n_optionalNotMatched = _NullToken()\nclass Optional(ParseElementEnhance):\n    \"\"\"\n    Optional matching of the given expression.\n\n    Parameters:\n     - expr - expression that must match zero or more times\n     - default (optional) - value to be returned if the optional expression is not found.\n\n    Example::\n        # US postal code can be a 5-digit zip, plus optional 4-digit qualifier\n        zip = Combine(Word(nums, exact=5) + Optional('-' + Word(nums, exact=4)))\n        zip.runTests('''\n            # traditional ZIP code\n            12345\n            \n            # ZIP+4 form\n            12101-0001\n            \n            # invalid ZIP\n            98765-\n            ''')\n    prints::\n        # traditional ZIP code\n        12345\n        ['12345']\n\n        # ZIP+4 form\n        12101-0001\n        ['12101-0001']\n\n        # invalid ZIP\n        98765-\n             ^\n        FAIL: Expected end of text (at char 5), (line:1, col:6)\n    \"\"\"\n    def __init__( self, expr, default=_optionalNotMatched ):\n        super(Optional,self).__init__( expr, savelist=False )\n        self.saveAsList = self.expr.saveAsList\n        self.defaultValue = default\n        self.mayReturnEmpty = True\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        try:\n            loc, tokens = self.expr._parse( instring, loc, doActions, callPreParse=False )\n        except (ParseException,IndexError):\n            if self.defaultValue is not _optionalNotMatched:\n                if self.expr.resultsName:\n                    tokens = ParseResults([ self.defaultValue ])\n                    tokens[self.expr.resultsName] = self.defaultValue\n                else:\n                    tokens = [ self.defaultValue ]\n            else:\n                tokens = []\n        return loc, tokens\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n\n        if self.strRepr is None:\n            self.strRepr = \"[\" + _ustr(self.expr) + \"]\"\n\n        return self.strRepr\n\nclass SkipTo(ParseElementEnhance):\n    \"\"\"\n    Token for skipping over all undefined text until the matched expression is found.\n\n    Parameters:\n     - expr - target expression marking the end of the data to be skipped\n     - include - (default=C{False}) if True, the target expression is also parsed \n          (the skipped text and target expression are returned as a 2-element list).\n     - ignore - (default=C{None}) used to define grammars (typically quoted strings and \n          comments) that might contain false matches to the target expression\n     - failOn - (default=C{None}) define expressions that are not allowed to be \n          included in the skipped test; if found before the target expression is found, \n          the SkipTo is not a match\n\n    Example::\n        report = '''\n            Outstanding Issues Report - 1 Jan 2000\n\n               # | Severity | Description                               |  Days Open\n            -----+----------+-------------------------------------------+-----------\n             101 | Critical | Intermittent system crash                 |          6\n              94 | Cosmetic | Spelling error on Login ('log|n')         |         14\n              79 | Minor    | System slow when running too many reports |         47\n            '''\n        integer = Word(nums)\n        SEP = Suppress('|')\n        # use SkipTo to simply match everything up until the next SEP\n        # - ignore quoted strings, so that a '|' character inside a quoted string does not match\n        # - parse action will call token.strip() for each matched token, i.e., the description body\n        string_data = SkipTo(SEP, ignore=quotedString)\n        string_data.setParseAction(tokenMap(str.strip))\n        ticket_expr = (integer(\"issue_num\") + SEP \n                      + string_data(\"sev\") + SEP \n                      + string_data(\"desc\") + SEP \n                      + integer(\"days_open\"))\n        \n        for tkt in ticket_expr.searchString(report):\n            print tkt.dump()\n    prints::\n        ['101', 'Critical', 'Intermittent system crash', '6']\n        - days_open: 6\n        - desc: Intermittent system crash\n        - issue_num: 101\n        - sev: Critical\n        ['94', 'Cosmetic', \"Spelling error on Login ('log|n')\", '14']\n        - days_open: 14\n        - desc: Spelling error on Login ('log|n')\n        - issue_num: 94\n        - sev: Cosmetic\n        ['79', 'Minor', 'System slow when running too many reports', '47']\n        - days_open: 47\n        - desc: System slow when running too many reports\n        - issue_num: 79\n        - sev: Minor\n    \"\"\"\n    def __init__( self, other, include=False, ignore=None, failOn=None ):\n        super( SkipTo, self ).__init__( other )\n        self.ignoreExpr = ignore\n        self.mayReturnEmpty = True\n        self.mayIndexError = False\n        self.includeMatch = include\n        self.asList = False\n        if isinstance(failOn, basestring):\n            self.failOn = ParserElement._literalStringClass(failOn)\n        else:\n            self.failOn = failOn\n        self.errmsg = \"No match found for \"+_ustr(self.expr)\n\n    def parseImpl( self, instring, loc, doActions=True ):\n        startloc = loc\n        instrlen = len(instring)\n        expr = self.expr\n        expr_parse = self.expr._parse\n        self_failOn_canParseNext = self.failOn.canParseNext if self.failOn is not None else None\n        self_ignoreExpr_tryParse = self.ignoreExpr.tryParse if self.ignoreExpr is not None else None\n        \n        tmploc = loc\n        while tmploc <= instrlen:\n            if self_failOn_canParseNext is not None:\n                # break if failOn expression matches\n                if self_failOn_canParseNext(instring, tmploc):\n                    break\n                    \n            if self_ignoreExpr_tryParse is not None:\n                # advance past ignore expressions\n                while 1:\n                    try:\n                        tmploc = self_ignoreExpr_tryParse(instring, tmploc)\n                    except ParseBaseException:\n                        break\n            \n            try:\n                expr_parse(instring, tmploc, doActions=False, callPreParse=False)\n            except (ParseException, IndexError):\n                # no match, advance loc in string\n                tmploc += 1\n            else:\n                # matched skipto expr, done\n                break\n\n        else:\n            # ran off the end of the input string without matching skipto expr, fail\n            raise ParseException(instring, loc, self.errmsg, self)\n\n        # build up return values\n        loc = tmploc\n        skiptext = instring[startloc:loc]\n        skipresult = ParseResults(skiptext)\n        \n        if self.includeMatch:\n            loc, mat = expr_parse(instring,loc,doActions,callPreParse=False)\n            skipresult += mat\n\n        return loc, skipresult\n\nclass Forward(ParseElementEnhance):\n    \"\"\"\n    Forward declaration of an expression to be defined later -\n    used for recursive grammars, such as algebraic infix notation.\n    When the expression is known, it is assigned to the C{Forward} variable using the '<<' operator.\n\n    Note: take care when assigning to C{Forward} not to overlook precedence of operators.\n    Specifically, '|' has a lower precedence than '<<', so that::\n        fwdExpr << a | b | c\n    will actually be evaluated as::\n        (fwdExpr << a) | b | c\n    thereby leaving b and c out as parseable alternatives.  It is recommended that you\n    explicitly group the values inserted into the C{Forward}::\n        fwdExpr << (a | b | c)\n    Converting to use the '<<=' operator instead will avoid this problem.\n\n    See L{ParseResults.pprint} for an example of a recursive parser created using\n    C{Forward}.\n    \"\"\"\n    def __init__( self, other=None ):\n        super(Forward,self).__init__( other, savelist=False )\n\n    def __lshift__( self, other ):\n        if isinstance( other, basestring ):\n            other = ParserElement._literalStringClass(other)\n        self.expr = other\n        self.strRepr = None\n        self.mayIndexError = self.expr.mayIndexError\n        self.mayReturnEmpty = self.expr.mayReturnEmpty\n        self.setWhitespaceChars( self.expr.whiteChars )\n        self.skipWhitespace = self.expr.skipWhitespace\n        self.saveAsList = self.expr.saveAsList\n        self.ignoreExprs.extend(self.expr.ignoreExprs)\n        return self\n        \n    def __ilshift__(self, other):\n        return self << other\n    \n    def leaveWhitespace( self ):\n        self.skipWhitespace = False\n        return self\n\n    def streamline( self ):\n        if not self.streamlined:\n            self.streamlined = True\n            if self.expr is not None:\n                self.expr.streamline()\n        return self\n\n    def validate( self, validateTrace=[] ):\n        if self not in validateTrace:\n            tmp = validateTrace[:]+[self]\n            if self.expr is not None:\n                self.expr.validate(tmp)\n        self.checkRecursion([])\n\n    def __str__( self ):\n        if hasattr(self,\"name\"):\n            return self.name\n        return self.__class__.__name__ + \": ...\"\n\n        # stubbed out for now - creates awful memory and perf issues\n        self._revertClass = self.__class__\n        self.__class__ = _ForwardNoRecurse\n        try:\n            if self.expr is not None:\n                retString = _ustr(self.expr)\n            else:\n                retString = \"None\"\n        finally:\n            self.__class__ = self._revertClass\n        return self.__class__.__name__ + \": \" + retString\n\n    def copy(self):\n        if self.expr is not None:\n            return super(Forward,self).copy()\n        else:\n            ret = Forward()\n            ret <<= self\n            return ret\n\nclass _ForwardNoRecurse(Forward):\n    def __str__( self ):\n        return \"...\"\n\nclass TokenConverter(ParseElementEnhance):\n    \"\"\"\n    Abstract subclass of C{ParseExpression}, for converting parsed results.\n    \"\"\"\n    def __init__( self, expr, savelist=False ):\n        super(TokenConverter,self).__init__( expr )#, savelist )\n        self.saveAsList = False\n\nclass Combine(TokenConverter):\n    \"\"\"\n    Converter to concatenate all matching tokens to a single string.\n    By default, the matching patterns must also be contiguous in the input string;\n    this can be disabled by specifying C{'adjacent=False'} in the constructor.\n\n    Example::\n        real = Word(nums) + '.' + Word(nums)\n        print(real.parseString('3.1416')) # -> ['3', '.', '1416']\n        # will also erroneously match the following\n        print(real.parseString('3. 1416')) # -> ['3', '.', '1416']\n\n        real = Combine(Word(nums) + '.' + Word(nums))\n        print(real.parseString('3.1416')) # -> ['3.1416']\n        # no match when there are internal spaces\n        print(real.parseString('3. 1416')) # -> Exception: Expected W:(0123...)\n    \"\"\"\n    def __init__( self, expr, joinString=\"\", adjacent=True ):\n        super(Combine,self).__init__( expr )\n        # suppress whitespace-stripping in contained parse expressions, but re-enable it on the Combine itself\n        if adjacent:\n            self.leaveWhitespace()\n        self.adjacent = adjacent\n        self.skipWhitespace = True\n        self.joinString = joinString\n        self.callPreparse = True\n\n    def ignore( self, other ):\n        if self.adjacent:\n            ParserElement.ignore(self, other)\n        else:\n            super( Combine, self).ignore( other )\n        return self\n\n    def postParse( self, instring, loc, tokenlist ):\n        retToks = tokenlist.copy()\n        del retToks[:]\n        retToks += ParseResults([ \"\".join(tokenlist._asStringList(self.joinString)) ], modal=self.modalResults)\n\n        if self.resultsName and retToks.haskeys():\n            return [ retToks ]\n        else:\n            return retToks\n\nclass Group(TokenConverter):\n    \"\"\"\n    Converter to return the matched tokens as a list - useful for returning tokens of C{L{ZeroOrMore}} and C{L{OneOrMore}} expressions.\n\n    Example::\n        ident = Word(alphas)\n        num = Word(nums)\n        term = ident | num\n        func = ident + Optional(delimitedList(term))\n        print(func.parseString(\"fn a,b,100\"))  # -> ['fn', 'a', 'b', '100']\n\n        func = ident + Group(Optional(delimitedList(term)))\n        print(func.parseString(\"fn a,b,100\"))  # -> ['fn', ['a', 'b', '100']]\n    \"\"\"\n    def __init__( self, expr ):\n        super(Group,self).__init__( expr )\n        self.saveAsList = True\n\n    def postParse( self, instring, loc, tokenlist ):\n        return [ tokenlist ]\n\nclass Dict(TokenConverter):\n    \"\"\"\n    Converter to return a repetitive expression as a list, but also as a dictionary.\n    Each element can also be referenced using the first token in the expression as its key.\n    Useful for tabular report scraping when the first column can be used as a item key.\n\n    Example::\n        data_word = Word(alphas)\n        label = data_word + FollowedBy(':')\n        attr_expr = Group(label + Suppress(':') + OneOrMore(data_word).setParseAction(' '.join))\n\n        text = \"shape: SQUARE posn: upper left color: light blue texture: burlap\"\n        attr_expr = (label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))\n        \n        # print attributes as plain groups\n        print(OneOrMore(attr_expr).parseString(text).dump())\n        \n        # instead of OneOrMore(expr), parse using Dict(OneOrMore(Group(expr))) - Dict will auto-assign names\n        result = Dict(OneOrMore(Group(attr_expr))).parseString(text)\n        print(result.dump())\n        \n        # access named fields as dict entries, or output as dict\n        print(result['shape'])        \n        print(result.asDict())\n    prints::\n        ['shape', 'SQUARE', 'posn', 'upper left', 'color', 'light blue', 'texture', 'burlap']\n\n        [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]\n        - color: light blue\n        - posn: upper left\n        - shape: SQUARE\n        - texture: burlap\n        SQUARE\n        {'color': 'light blue', 'posn': 'upper left', 'texture': 'burlap', 'shape': 'SQUARE'}\n    See more examples at L{ParseResults} of accessing fields by results name.\n    \"\"\"\n    def __init__( self, expr ):\n        super(Dict,self).__init__( expr )\n        self.saveAsList = True\n\n    def postParse( self, instring, loc, tokenlist ):\n        for i,tok in enumerate(tokenlist):\n            if len(tok) == 0:\n                continue\n            ikey = tok[0]\n            if isinstance(ikey,int):\n                ikey = _ustr(tok[0]).strip()\n            if len(tok)==1:\n                tokenlist[ikey] = _ParseResultsWithOffset(\"\",i)\n            elif len(tok)==2 and not isinstance(tok[1],ParseResults):\n                tokenlist[ikey] = _ParseResultsWithOffset(tok[1],i)\n            else:\n                dictvalue = tok.copy() #ParseResults(i)\n                del dictvalue[0]\n                if len(dictvalue)!= 1 or (isinstance(dictvalue,ParseResults) and dictvalue.haskeys()):\n                    tokenlist[ikey] = _ParseResultsWithOffset(dictvalue,i)\n                else:\n                    tokenlist[ikey] = _ParseResultsWithOffset(dictvalue[0],i)\n\n        if self.resultsName:\n            return [ tokenlist ]\n        else:\n            return tokenlist\n\n\nclass Suppress(TokenConverter):\n    \"\"\"\n    Converter for ignoring the results of a parsed expression.\n\n    Example::\n        source = \"a, b, c,d\"\n        wd = Word(alphas)\n        wd_list1 = wd + ZeroOrMore(',' + wd)\n        print(wd_list1.parseString(source))\n\n        # often, delimiters that are useful during parsing are just in the\n        # way afterward - use Suppress to keep them out of the parsed output\n        wd_list2 = wd + ZeroOrMore(Suppress(',') + wd)\n        print(wd_list2.parseString(source))\n    prints::\n        ['a', ',', 'b', ',', 'c', ',', 'd']\n        ['a', 'b', 'c', 'd']\n    (See also L{delimitedList}.)\n    \"\"\"\n    def postParse( self, instring, loc, tokenlist ):\n        return []\n\n    def suppress( self ):\n        return self\n\n\nclass OnlyOnce(object):\n    \"\"\"\n    Wrapper for parse actions, to ensure they are only called once.\n    \"\"\"\n    def __init__(self, methodCall):\n        self.callable = _trim_arity(methodCall)\n        self.called = False\n    def __call__(self,s,l,t):\n        if not self.called:\n            results = self.callable(s,l,t)\n            self.called = True\n            return results\n        raise ParseException(s,l,\"\")\n    def reset(self):\n        self.called = False\n\ndef traceParseAction(f):\n    \"\"\"\n    Decorator for debugging parse actions. \n    \n    When the parse action is called, this decorator will print C{\">> entering I{method-name}(line:I{current_source_line}, I{parse_location}, I{matched_tokens})\".}\n    When the parse action completes, the decorator will print C{\"<<\"} followed by the returned value, or any exception that the parse action raised.\n\n    Example::\n        wd = Word(alphas)\n\n        @traceParseAction\n        def remove_duplicate_chars(tokens):\n            return ''.join(sorted(set(''.join(tokens))))\n\n        wds = OneOrMore(wd).setParseAction(remove_duplicate_chars)\n        print(wds.parseString(\"slkdjs sld sldd sdlf sdljf\"))\n    prints::\n        >>entering remove_duplicate_chars(line: 'slkdjs sld sldd sdlf sdljf', 0, (['slkdjs', 'sld', 'sldd', 'sdlf', 'sdljf'], {}))\n        <<leaving remove_duplicate_chars (ret: 'dfjkls')\n        ['dfjkls']\n    \"\"\"\n    f = _trim_arity(f)\n    def z(*paArgs):\n        thisFunc = f.__name__\n        s,l,t = paArgs[-3:]\n        if len(paArgs)>3:\n            thisFunc = paArgs[0].__class__.__name__ + '.' + thisFunc\n        sys.stderr.write( \">>entering %s(line: '%s', %d, %r)\\n\" % (thisFunc,line(l,s),l,t) )\n        try:\n            ret = f(*paArgs)\n        except Exception as exc:\n            sys.stderr.write( \"<<leaving %s (exception: %s)\\n\" % (thisFunc,exc) )\n            raise\n        sys.stderr.write( \"<<leaving %s (ret: %r)\\n\" % (thisFunc,ret) )\n        return ret\n    try:\n        z.__name__ = f.__name__\n    except AttributeError:\n        pass\n    return z\n\n#\n# global helpers\n#\ndef delimitedList( expr, delim=\",\", combine=False ):\n    \"\"\"\n    Helper to define a delimited list of expressions - the delimiter defaults to ','.\n    By default, the list elements and delimiters can have intervening whitespace, and\n    comments, but this can be overridden by passing C{combine=True} in the constructor.\n    If C{combine} is set to C{True}, the matching tokens are returned as a single token\n    string, with the delimiters included; otherwise, the matching tokens are returned\n    as a list of tokens, with the delimiters suppressed.\n\n    Example::\n        delimitedList(Word(alphas)).parseString(\"aa,bb,cc\") # -> ['aa', 'bb', 'cc']\n        delimitedList(Word(hexnums), delim=':', combine=True).parseString(\"AA:BB:CC:DD:EE\") # -> ['AA:BB:CC:DD:EE']\n    \"\"\"\n    dlName = _ustr(expr)+\" [\"+_ustr(delim)+\" \"+_ustr(expr)+\"]...\"\n    if combine:\n        return Combine( expr + ZeroOrMore( delim + expr ) ).setName(dlName)\n    else:\n        return ( expr + ZeroOrMore( Suppress( delim ) + expr ) ).setName(dlName)\n\ndef countedArray( expr, intExpr=None ):\n    \"\"\"\n    Helper to define a counted list of expressions.\n    This helper defines a pattern of the form::\n        integer expr expr expr...\n    where the leading integer tells how many expr expressions follow.\n    The matched tokens returns the array of expr tokens as a list - the leading count token is suppressed.\n    \n    If C{intExpr} is specified, it should be a pyparsing expression that produces an integer value.\n\n    Example::\n        countedArray(Word(alphas)).parseString('2 ab cd ef')  # -> ['ab', 'cd']\n\n        # in this parser, the leading integer value is given in binary,\n        # '10' indicating that 2 values are in the array\n        binaryConstant = Word('01').setParseAction(lambda t: int(t[0], 2))\n        countedArray(Word(alphas), intExpr=binaryConstant).parseString('10 ab cd ef')  # -> ['ab', 'cd']\n    \"\"\"\n    arrayExpr = Forward()\n    def countFieldParseAction(s,l,t):\n        n = t[0]\n        arrayExpr << (n and Group(And([expr]*n)) or Group(empty))\n        return []\n    if intExpr is None:\n        intExpr = Word(nums).setParseAction(lambda t:int(t[0]))\n    else:\n        intExpr = intExpr.copy()\n    intExpr.setName(\"arrayLen\")\n    intExpr.addParseAction(countFieldParseAction, callDuringTry=True)\n    return ( intExpr + arrayExpr ).setName('(len) ' + _ustr(expr) + '...')\n\ndef _flatten(L):\n    ret = []\n    for i in L:\n        if isinstance(i,list):\n            ret.extend(_flatten(i))\n        else:\n            ret.append(i)\n    return ret\n\ndef matchPreviousLiteral(expr):\n    \"\"\"\n    Helper to define an expression that is indirectly defined from\n    the tokens matched in a previous expression, that is, it looks\n    for a 'repeat' of a previous expression.  For example::\n        first = Word(nums)\n        second = matchPreviousLiteral(first)\n        matchExpr = first + \":\" + second\n    will match C{\"1:1\"}, but not C{\"1:2\"}.  Because this matches a\n    previous literal, will also match the leading C{\"1:1\"} in C{\"1:10\"}.\n    If this is not desired, use C{matchPreviousExpr}.\n    Do I{not} use with packrat parsing enabled.\n    \"\"\"\n    rep = Forward()\n    def copyTokenToRepeater(s,l,t):\n        if t:\n            if len(t) == 1:\n                rep << t[0]\n            else:\n                # flatten t tokens\n                tflat = _flatten(t.asList())\n                rep << And(Literal(tt) for tt in tflat)\n        else:\n            rep << Empty()\n    expr.addParseAction(copyTokenToRepeater, callDuringTry=True)\n    rep.setName('(prev) ' + _ustr(expr))\n    return rep\n\ndef matchPreviousExpr(expr):\n    \"\"\"\n    Helper to define an expression that is indirectly defined from\n    the tokens matched in a previous expression, that is, it looks\n    for a 'repeat' of a previous expression.  For example::\n        first = Word(nums)\n        second = matchPreviousExpr(first)\n        matchExpr = first + \":\" + second\n    will match C{\"1:1\"}, but not C{\"1:2\"}.  Because this matches by\n    expressions, will I{not} match the leading C{\"1:1\"} in C{\"1:10\"};\n    the expressions are evaluated first, and then compared, so\n    C{\"1\"} is compared with C{\"10\"}.\n    Do I{not} use with packrat parsing enabled.\n    \"\"\"\n    rep = Forward()\n    e2 = expr.copy()\n    rep <<= e2\n    def copyTokenToRepeater(s,l,t):\n        matchTokens = _flatten(t.asList())\n        def mustMatchTheseTokens(s,l,t):\n            theseTokens = _flatten(t.asList())\n            if  theseTokens != matchTokens:\n                raise ParseException(\"\",0,\"\")\n        rep.setParseAction( mustMatchTheseTokens, callDuringTry=True )\n    expr.addParseAction(copyTokenToRepeater, callDuringTry=True)\n    rep.setName('(prev) ' + _ustr(expr))\n    return rep\n\ndef _escapeRegexRangeChars(s):\n    #~  escape these chars: ^-]\n    for c in r\"\\^-]\":\n        s = s.replace(c,_bslash+c)\n    s = s.replace(\"\\n\",r\"\\n\")\n    s = s.replace(\"\\t\",r\"\\t\")\n    return _ustr(s)\n\ndef oneOf( strs, caseless=False, useRegex=True ):\n    \"\"\"\n    Helper to quickly define a set of alternative Literals, and makes sure to do\n    longest-first testing when there is a conflict, regardless of the input order,\n    but returns a C{L{MatchFirst}} for best performance.\n\n    Parameters:\n     - strs - a string of space-delimited literals, or a collection of string literals\n     - caseless - (default=C{False}) - treat all literals as caseless\n     - useRegex - (default=C{True}) - as an optimization, will generate a Regex\n          object; otherwise, will generate a C{MatchFirst} object (if C{caseless=True}, or\n          if creating a C{Regex} raises an exception)\n\n    Example::\n        comp_oper = oneOf(\"< = > <= >= !=\")\n        var = Word(alphas)\n        number = Word(nums)\n        term = var | number\n        comparison_expr = term + comp_oper + term\n        print(comparison_expr.searchString(\"B = 12  AA=23 B<=AA AA>12\"))\n    prints::\n        [['B', '=', '12'], ['AA', '=', '23'], ['B', '<=', 'AA'], ['AA', '>', '12']]\n    \"\"\"\n    if caseless:\n        isequal = ( lambda a,b: a.upper() == b.upper() )\n        masks = ( lambda a,b: b.upper().startswith(a.upper()) )\n        parseElementClass = CaselessLiteral\n    else:\n        isequal = ( lambda a,b: a == b )\n        masks = ( lambda a,b: b.startswith(a) )\n        parseElementClass = Literal\n\n    symbols = []\n    if isinstance(strs,basestring):\n        symbols = strs.split()\n    elif isinstance(strs, Iterable):\n        symbols = list(strs)\n    else:\n        warnings.warn(\"Invalid argument to oneOf, expected string or iterable\",\n                SyntaxWarning, stacklevel=2)\n    if not symbols:\n        return NoMatch()\n\n    i = 0\n    while i < len(symbols)-1:\n        cur = symbols[i]\n        for j,other in enumerate(symbols[i+1:]):\n            if ( isequal(other, cur) ):\n                del symbols[i+j+1]\n                break\n            elif ( masks(cur, other) ):\n                del symbols[i+j+1]\n                symbols.insert(i,other)\n                cur = other\n                break\n        else:\n            i += 1\n\n    if not caseless and useRegex:\n        #~ print (strs,\"->\", \"|\".join( [ _escapeRegexChars(sym) for sym in symbols] ))\n        try:\n            if len(symbols)==len(\"\".join(symbols)):\n                return Regex( \"[%s]\" % \"\".join(_escapeRegexRangeChars(sym) for sym in symbols) ).setName(' | '.join(symbols))\n            else:\n                return Regex( \"|\".join(re.escape(sym) for sym in symbols) ).setName(' | '.join(symbols))\n        except Exception:\n            warnings.warn(\"Exception creating Regex for oneOf, building MatchFirst\",\n                    SyntaxWarning, stacklevel=2)\n\n\n    # last resort, just use MatchFirst\n    return MatchFirst(parseElementClass(sym) for sym in symbols).setName(' | '.join(symbols))\n\ndef dictOf( key, value ):\n    \"\"\"\n    Helper to easily and clearly define a dictionary by specifying the respective patterns\n    for the key and value.  Takes care of defining the C{L{Dict}}, C{L{ZeroOrMore}}, and C{L{Group}} tokens\n    in the proper order.  The key pattern can include delimiting markers or punctuation,\n    as long as they are suppressed, thereby leaving the significant key text.  The value\n    pattern can include named results, so that the C{Dict} results can include named token\n    fields.\n\n    Example::\n        text = \"shape: SQUARE posn: upper left color: light blue texture: burlap\"\n        attr_expr = (label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))\n        print(OneOrMore(attr_expr).parseString(text).dump())\n        \n        attr_label = label\n        attr_value = Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join)\n\n        # similar to Dict, but simpler call format\n        result = dictOf(attr_label, attr_value).parseString(text)\n        print(result.dump())\n        print(result['shape'])\n        print(result.shape)  # object attribute access works too\n        print(result.asDict())\n    prints::\n        [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]\n        - color: light blue\n        - posn: upper left\n        - shape: SQUARE\n        - texture: burlap\n        SQUARE\n        SQUARE\n        {'color': 'light blue', 'shape': 'SQUARE', 'posn': 'upper left', 'texture': 'burlap'}\n    \"\"\"\n    return Dict( ZeroOrMore( Group ( key + value ) ) )\n\ndef originalTextFor(expr, asString=True):\n    \"\"\"\n    Helper to return the original, untokenized text for a given expression.  Useful to\n    restore the parsed fields of an HTML start tag into the raw tag text itself, or to\n    revert separate tokens with intervening whitespace back to the original matching\n    input text. By default, returns astring containing the original parsed text.  \n       \n    If the optional C{asString} argument is passed as C{False}, then the return value is a \n    C{L{ParseResults}} containing any results names that were originally matched, and a \n    single token containing the original matched text from the input string.  So if \n    the expression passed to C{L{originalTextFor}} contains expressions with defined\n    results names, you must set C{asString} to C{False} if you want to preserve those\n    results name values.\n\n    Example::\n        src = \"this is test <b> bold <i>text</i> </b> normal text \"\n        for tag in (\"b\",\"i\"):\n            opener,closer = makeHTMLTags(tag)\n            patt = originalTextFor(opener + SkipTo(closer) + closer)\n            print(patt.searchString(src)[0])\n    prints::\n        ['<b> bold <i>text</i> </b>']\n        ['<i>text</i>']\n    \"\"\"\n    locMarker = Empty().setParseAction(lambda s,loc,t: loc)\n    endlocMarker = locMarker.copy()\n    endlocMarker.callPreparse = False\n    matchExpr = locMarker(\"_original_start\") + expr + endlocMarker(\"_original_end\")\n    if asString:\n        extractText = lambda s,l,t: s[t._original_start:t._original_end]\n    else:\n        def extractText(s,l,t):\n            t[:] = [s[t.pop('_original_start'):t.pop('_original_end')]]\n    matchExpr.setParseAction(extractText)\n    matchExpr.ignoreExprs = expr.ignoreExprs\n    return matchExpr\n\ndef ungroup(expr): \n    \"\"\"\n    Helper to undo pyparsing's default grouping of And expressions, even\n    if all but one are non-empty.\n    \"\"\"\n    return TokenConverter(expr).setParseAction(lambda t:t[0])\n\ndef locatedExpr(expr):\n    \"\"\"\n    Helper to decorate a returned token with its starting and ending locations in the input string.\n    This helper adds the following results names:\n     - locn_start = location where matched expression begins\n     - locn_end = location where matched expression ends\n     - value = the actual parsed results\n\n    Be careful if the input text contains C{<TAB>} characters, you may want to call\n    C{L{ParserElement.parseWithTabs}}\n\n    Example::\n        wd = Word(alphas)\n        for match in locatedExpr(wd).searchString(\"ljsdf123lksdjjf123lkkjj1222\"):\n            print(match)\n    prints::\n        [[0, 'ljsdf', 5]]\n        [[8, 'lksdjjf', 15]]\n        [[18, 'lkkjj', 23]]\n    \"\"\"\n    locator = Empty().setParseAction(lambda s,l,t: l)\n    return Group(locator(\"locn_start\") + expr(\"value\") + locator.copy().leaveWhitespace()(\"locn_end\"))\n\n\n# convenience constants for positional expressions\nempty       = Empty().setName(\"empty\")\nlineStart   = LineStart().setName(\"lineStart\")\nlineEnd     = LineEnd().setName(\"lineEnd\")\nstringStart = StringStart().setName(\"stringStart\")\nstringEnd   = StringEnd().setName(\"stringEnd\")\n\n_escapedPunc = Word( _bslash, r\"\\[]-*.$+^?()~ \", exact=2 ).setParseAction(lambda s,l,t:t[0][1])\n_escapedHexChar = Regex(r\"\\\\0?[xX][0-9a-fA-F]+\").setParseAction(lambda s,l,t:unichr(int(t[0].lstrip(r'\\0x'),16)))\n_escapedOctChar = Regex(r\"\\\\0[0-7]+\").setParseAction(lambda s,l,t:unichr(int(t[0][1:],8)))\n_singleChar = _escapedPunc | _escapedHexChar | _escapedOctChar | CharsNotIn(r'\\]', exact=1)\n_charRange = Group(_singleChar + Suppress(\"-\") + _singleChar)\n_reBracketExpr = Literal(\"[\") + Optional(\"^\").setResultsName(\"negate\") + Group( OneOrMore( _charRange | _singleChar ) ).setResultsName(\"body\") + \"]\"\n\ndef srange(s):\n    r\"\"\"\n    Helper to easily define string ranges for use in Word construction.  Borrows\n    syntax from regexp '[]' string range definitions::\n        srange(\"[0-9]\")   -> \"0123456789\"\n        srange(\"[a-z]\")   -> \"abcdefghijklmnopqrstuvwxyz\"\n        srange(\"[a-z$_]\") -> \"abcdefghijklmnopqrstuvwxyz$_\"\n    The input string must be enclosed in []'s, and the returned string is the expanded\n    character set joined into a single string.\n    The values enclosed in the []'s may be:\n     - a single character\n     - an escaped character with a leading backslash (such as C{\\-} or C{\\]})\n     - an escaped hex character with a leading C{'\\x'} (C{\\x21}, which is a C{'!'} character) \n         (C{\\0x##} is also supported for backwards compatibility) \n     - an escaped octal character with a leading C{'\\0'} (C{\\041}, which is a C{'!'} character)\n     - a range of any of the above, separated by a dash (C{'a-z'}, etc.)\n     - any combination of the above (C{'aeiouy'}, C{'a-zA-Z0-9_$'}, etc.)\n    \"\"\"\n    _expanded = lambda p: p if not isinstance(p,ParseResults) else ''.join(unichr(c) for c in range(ord(p[0]),ord(p[1])+1))\n    try:\n        return \"\".join(_expanded(part) for part in _reBracketExpr.parseString(s).body)\n    except Exception:\n        return \"\"\n\ndef matchOnlyAtCol(n):\n    \"\"\"\n    Helper method for defining parse actions that require matching at a specific\n    column in the input text.\n    \"\"\"\n    def verifyCol(strg,locn,toks):\n        if col(locn,strg) != n:\n            raise ParseException(strg,locn,\"matched token not at column %d\" % n)\n    return verifyCol\n\ndef replaceWith(replStr):\n    \"\"\"\n    Helper method for common parse actions that simply return a literal value.  Especially\n    useful when used with C{L{transformString<ParserElement.transformString>}()}.\n\n    Example::\n        num = Word(nums).setParseAction(lambda toks: int(toks[0]))\n        na = oneOf(\"N/A NA\").setParseAction(replaceWith(math.nan))\n        term = na | num\n        \n        OneOrMore(term).parseString(\"324 234 N/A 234\") # -> [324, 234, nan, 234]\n    \"\"\"\n    return lambda s,l,t: [replStr]\n\ndef removeQuotes(s,l,t):\n    \"\"\"\n    Helper parse action for removing quotation marks from parsed quoted strings.\n\n    Example::\n        # by default, quotation marks are included in parsed results\n        quotedString.parseString(\"'Now is the Winter of our Discontent'\") # -> [\"'Now is the Winter of our Discontent'\"]\n\n        # use removeQuotes to strip quotation marks from parsed results\n        quotedString.setParseAction(removeQuotes)\n        quotedString.parseString(\"'Now is the Winter of our Discontent'\") # -> [\"Now is the Winter of our Discontent\"]\n    \"\"\"\n    return t[0][1:-1]\n\ndef tokenMap(func, *args):\n    \"\"\"\n    Helper to define a parse action by mapping a function to all elements of a ParseResults list.If any additional \n    args are passed, they are forwarded to the given function as additional arguments after\n    the token, as in C{hex_integer = Word(hexnums).setParseAction(tokenMap(int, 16))}, which will convert the\n    parsed data to an integer using base 16.\n\n    Example (compare the last to example in L{ParserElement.transformString}::\n        hex_ints = OneOrMore(Word(hexnums)).setParseAction(tokenMap(int, 16))\n        hex_ints.runTests('''\n            00 11 22 aa FF 0a 0d 1a\n            ''')\n        \n        upperword = Word(alphas).setParseAction(tokenMap(str.upper))\n        OneOrMore(upperword).runTests('''\n            my kingdom for a horse\n            ''')\n\n        wd = Word(alphas).setParseAction(tokenMap(str.title))\n        OneOrMore(wd).setParseAction(' '.join).runTests('''\n            now is the winter of our discontent made glorious summer by this sun of york\n            ''')\n    prints::\n        00 11 22 aa FF 0a 0d 1a\n        [0, 17, 34, 170, 255, 10, 13, 26]\n\n        my kingdom for a horse\n        ['MY', 'KINGDOM', 'FOR', 'A', 'HORSE']\n\n        now is the winter of our discontent made glorious summer by this sun of york\n        ['Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York']\n    \"\"\"\n    def pa(s,l,t):\n        return [func(tokn, *args) for tokn in t]\n\n    try:\n        func_name = getattr(func, '__name__', \n                            getattr(func, '__class__').__name__)\n    except Exception:\n        func_name = str(func)\n    pa.__name__ = func_name\n\n    return pa\n\nupcaseTokens = tokenMap(lambda t: _ustr(t).upper())\n\"\"\"(Deprecated) Helper parse action to convert tokens to upper case. Deprecated in favor of L{pyparsing_common.upcaseTokens}\"\"\"\n\ndowncaseTokens = tokenMap(lambda t: _ustr(t).lower())\n\"\"\"(Deprecated) Helper parse action to convert tokens to lower case. Deprecated in favor of L{pyparsing_common.downcaseTokens}\"\"\"\n    \ndef _makeTags(tagStr, xml):\n    \"\"\"Internal helper to construct opening and closing tag expressions, given a tag name\"\"\"\n    if isinstance(tagStr,basestring):\n        resname = tagStr\n        tagStr = Keyword(tagStr, caseless=not xml)\n    else:\n        resname = tagStr.name\n\n    tagAttrName = Word(alphas,alphanums+\"_-:\")\n    if (xml):\n        tagAttrValue = dblQuotedString.copy().setParseAction( removeQuotes )\n        openTag = Suppress(\"<\") + tagStr(\"tag\") + \\\n                Dict(ZeroOrMore(Group( tagAttrName + Suppress(\"=\") + tagAttrValue ))) + \\\n                Optional(\"/\",default=[False]).setResultsName(\"empty\").setParseAction(lambda s,l,t:t[0]=='/') + Suppress(\">\")\n    else:\n        printablesLessRAbrack = \"\".join(c for c in printables if c not in \">\")\n        tagAttrValue = quotedString.copy().setParseAction( removeQuotes ) | Word(printablesLessRAbrack)\n        openTag = Suppress(\"<\") + tagStr(\"tag\") + \\\n                Dict(ZeroOrMore(Group( tagAttrName.setParseAction(downcaseTokens) + \\\n                Optional( Suppress(\"=\") + tagAttrValue ) ))) + \\\n                Optional(\"/\",default=[False]).setResultsName(\"empty\").setParseAction(lambda s,l,t:t[0]=='/') + Suppress(\">\")\n    closeTag = Combine(_L(\"</\") + tagStr + \">\")\n\n    openTag = openTag.setResultsName(\"start\"+\"\".join(resname.replace(\":\",\" \").title().split())).setName(\"<%s>\" % resname)\n    closeTag = closeTag.setResultsName(\"end\"+\"\".join(resname.replace(\":\",\" \").title().split())).setName(\"</%s>\" % resname)\n    openTag.tag = resname\n    closeTag.tag = resname\n    return openTag, closeTag\n\ndef makeHTMLTags(tagStr):\n    \"\"\"\n    Helper to construct opening and closing tag expressions for HTML, given a tag name. Matches\n    tags in either upper or lower case, attributes with namespaces and with quoted or unquoted values.\n\n    Example::\n        text = '<td>More info at the <a href=\"http://pyparsing.wikispaces.com\">pyparsing</a> wiki page</td>'\n        # makeHTMLTags returns pyparsing expressions for the opening and closing tags as a 2-tuple\n        a,a_end = makeHTMLTags(\"A\")\n        link_expr = a + SkipTo(a_end)(\"link_text\") + a_end\n        \n        for link in link_expr.searchString(text):\n            # attributes in the <A> tag (like \"href\" shown here) are also accessible as named results\n            print(link.link_text, '->', link.href)\n    prints::\n        pyparsing -> http://pyparsing.wikispaces.com\n    \"\"\"\n    return _makeTags( tagStr, False )\n\ndef makeXMLTags(tagStr):\n    \"\"\"\n    Helper to construct opening and closing tag expressions for XML, given a tag name. Matches\n    tags only in the given upper/lower case.\n\n    Example: similar to L{makeHTMLTags}\n    \"\"\"\n    return _makeTags( tagStr, True )\n\ndef withAttribute(*args,**attrDict):\n    \"\"\"\n    Helper to create a validating parse action to be used with start tags created\n    with C{L{makeXMLTags}} or C{L{makeHTMLTags}}. Use C{withAttribute} to qualify a starting tag\n    with a required attribute value, to avoid false matches on common tags such as\n    C{<TD>} or C{<DIV>}.\n\n    Call C{withAttribute} with a series of attribute names and values. Specify the list\n    of filter attributes names and values as:\n     - keyword arguments, as in C{(align=\"right\")}, or\n     - as an explicit dict with C{**} operator, when an attribute name is also a Python\n          reserved word, as in C{**{\"class\":\"Customer\", \"align\":\"right\"}}\n     - a list of name-value tuples, as in ( (\"ns1:class\", \"Customer\"), (\"ns2:align\",\"right\") )\n    For attribute names with a namespace prefix, you must use the second form.  Attribute\n    names are matched insensitive to upper/lower case.\n       \n    If just testing for C{class} (with or without a namespace), use C{L{withClass}}.\n\n    To verify that the attribute exists, but without specifying a value, pass\n    C{withAttribute.ANY_VALUE} as the value.\n\n    Example::\n        html = '''\n            <div>\n            Some text\n            <div type=\"grid\">1 4 0 1 0</div>\n            <div type=\"graph\">1,3 2,3 1,1</div>\n            <div>this has no type</div>\n            </div>\n                \n        '''\n        div,div_end = makeHTMLTags(\"div\")\n\n        # only match div tag having a type attribute with value \"grid\"\n        div_grid = div().setParseAction(withAttribute(type=\"grid\"))\n        grid_expr = div_grid + SkipTo(div | div_end)(\"body\")\n        for grid_header in grid_expr.searchString(html):\n            print(grid_header.body)\n        \n        # construct a match with any div tag having a type attribute, regardless of the value\n        div_any_type = div().setParseAction(withAttribute(type=withAttribute.ANY_VALUE))\n        div_expr = div_any_type + SkipTo(div | div_end)(\"body\")\n        for div_header in div_expr.searchString(html):\n            print(div_header.body)\n    prints::\n        1 4 0 1 0\n\n        1 4 0 1 0\n        1,3 2,3 1,1\n    \"\"\"\n    if args:\n        attrs = args[:]\n    else:\n        attrs = attrDict.items()\n    attrs = [(k,v) for k,v in attrs]\n    def pa(s,l,tokens):\n        for attrName,attrValue in attrs:\n            if attrName not in tokens:\n                raise ParseException(s,l,\"no matching attribute \" + attrName)\n            if attrValue != withAttribute.ANY_VALUE and tokens[attrName] != attrValue:\n                raise ParseException(s,l,\"attribute '%s' has value '%s', must be '%s'\" %\n                                            (attrName, tokens[attrName], attrValue))\n    return pa\nwithAttribute.ANY_VALUE = object()\n\ndef withClass(classname, namespace=''):\n    \"\"\"\n    Simplified version of C{L{withAttribute}} when matching on a div class - made\n    difficult because C{class} is a reserved word in Python.\n\n    Example::\n        html = '''\n            <div>\n            Some text\n            <div class=\"grid\">1 4 0 1 0</div>\n            <div class=\"graph\">1,3 2,3 1,1</div>\n            <div>this &lt;div&gt; has no class</div>\n            </div>\n                \n        '''\n        div,div_end = makeHTMLTags(\"div\")\n        div_grid = div().setParseAction(withClass(\"grid\"))\n        \n        grid_expr = div_grid + SkipTo(div | div_end)(\"body\")\n        for grid_header in grid_expr.searchString(html):\n            print(grid_header.body)\n        \n        div_any_type = div().setParseAction(withClass(withAttribute.ANY_VALUE))\n        div_expr = div_any_type + SkipTo(div | div_end)(\"body\")\n        for div_header in div_expr.searchString(html):\n            print(div_header.body)\n    prints::\n        1 4 0 1 0\n\n        1 4 0 1 0\n        1,3 2,3 1,1\n    \"\"\"\n    classattr = \"%s:class\" % namespace if namespace else \"class\"\n    return withAttribute(**{classattr : classname})        \n\nopAssoc = _Constants()\nopAssoc.LEFT = object()\nopAssoc.RIGHT = object()\n\ndef infixNotation( baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')') ):\n    \"\"\"\n    Helper method for constructing grammars of expressions made up of\n    operators working in a precedence hierarchy.  Operators may be unary or\n    binary, left- or right-associative.  Parse actions can also be attached\n    to operator expressions. The generated parser will also recognize the use \n    of parentheses to override operator precedences (see example below).\n    \n    Note: if you define a deep operator list, you may see performance issues\n    when using infixNotation. See L{ParserElement.enablePackrat} for a\n    mechanism to potentially improve your parser performance.\n\n    Parameters:\n     - baseExpr - expression representing the most basic element for the nested\n     - opList - list of tuples, one for each operator precedence level in the\n      expression grammar; each tuple is of the form\n      (opExpr, numTerms, rightLeftAssoc, parseAction), where:\n       - opExpr is the pyparsing expression for the operator;\n          may also be a string, which will be converted to a Literal;\n          if numTerms is 3, opExpr is a tuple of two expressions, for the\n          two operators separating the 3 terms\n       - numTerms is the number of terms for this operator (must\n          be 1, 2, or 3)\n       - rightLeftAssoc is the indicator whether the operator is\n          right or left associative, using the pyparsing-defined\n          constants C{opAssoc.RIGHT} and C{opAssoc.LEFT}.\n       - parseAction is the parse action to be associated with\n          expressions matching this operator expression (the\n          parse action tuple member may be omitted); if the parse action\n          is passed a tuple or list of functions, this is equivalent to\n          calling C{setParseAction(*fn)} (L{ParserElement.setParseAction})\n     - lpar - expression for matching left-parentheses (default=C{Suppress('(')})\n     - rpar - expression for matching right-parentheses (default=C{Suppress(')')})\n\n    Example::\n        # simple example of four-function arithmetic with ints and variable names\n        integer = pyparsing_common.signed_integer\n        varname = pyparsing_common.identifier \n        \n        arith_expr = infixNotation(integer | varname,\n            [\n            ('-', 1, opAssoc.RIGHT),\n            (oneOf('* /'), 2, opAssoc.LEFT),\n            (oneOf('+ -'), 2, opAssoc.LEFT),\n            ])\n        \n        arith_expr.runTests('''\n            5+3*6\n            (5+3)*6\n            -2--11\n            ''', fullDump=False)\n    prints::\n        5+3*6\n        [[5, '+', [3, '*', 6]]]\n\n        (5+3)*6\n        [[[5, '+', 3], '*', 6]]\n\n        -2--11\n        [[['-', 2], '-', ['-', 11]]]\n    \"\"\"\n    ret = Forward()\n    lastExpr = baseExpr | ( lpar + ret + rpar )\n    for i,operDef in enumerate(opList):\n        opExpr,arity,rightLeftAssoc,pa = (operDef + (None,))[:4]\n        termName = \"%s term\" % opExpr if arity < 3 else \"%s%s term\" % opExpr\n        if arity == 3:\n            if opExpr is None or len(opExpr) != 2:\n                raise ValueError(\"if numterms=3, opExpr must be a tuple or list of two expressions\")\n            opExpr1, opExpr2 = opExpr\n        thisExpr = Forward().setName(termName)\n        if rightLeftAssoc == opAssoc.LEFT:\n            if arity == 1:\n                matchExpr = FollowedBy(lastExpr + opExpr) + Group( lastExpr + OneOrMore( opExpr ) )\n            elif arity == 2:\n                if opExpr is not None:\n                    matchExpr = FollowedBy(lastExpr + opExpr + lastExpr) + Group( lastExpr + OneOrMore( opExpr + lastExpr ) )\n                else:\n                    matchExpr = FollowedBy(lastExpr+lastExpr) + Group( lastExpr + OneOrMore(lastExpr) )\n            elif arity == 3:\n                matchExpr = FollowedBy(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr) + \\\n                            Group( lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr )\n            else:\n                raise ValueError(\"operator must be unary (1), binary (2), or ternary (3)\")\n        elif rightLeftAssoc == opAssoc.RIGHT:\n            if arity == 1:\n                # try to avoid LR with this extra test\n                if not isinstance(opExpr, Optional):\n                    opExpr = Optional(opExpr)\n                matchExpr = FollowedBy(opExpr.expr + thisExpr) + Group( opExpr + thisExpr )\n            elif arity == 2:\n                if opExpr is not None:\n                    matchExpr = FollowedBy(lastExpr + opExpr + thisExpr) + Group( lastExpr + OneOrMore( opExpr + thisExpr ) )\n                else:\n                    matchExpr = FollowedBy(lastExpr + thisExpr) + Group( lastExpr + OneOrMore( thisExpr ) )\n            elif arity == 3:\n                matchExpr = FollowedBy(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr) + \\\n                            Group( lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr )\n            else:\n                raise ValueError(\"operator must be unary (1), binary (2), or ternary (3)\")\n        else:\n            raise ValueError(\"operator must indicate right or left associativity\")\n        if pa:\n            if isinstance(pa, (tuple, list)):\n                matchExpr.setParseAction(*pa)\n            else:\n                matchExpr.setParseAction(pa)\n        thisExpr <<= ( matchExpr.setName(termName) | lastExpr )\n        lastExpr = thisExpr\n    ret <<= lastExpr\n    return ret\n\noperatorPrecedence = infixNotation\n\"\"\"(Deprecated) Former name of C{L{infixNotation}}, will be dropped in a future release.\"\"\"\n\ndblQuotedString = Combine(Regex(r'\"(?:[^\"\\n\\r\\\\]|(?:\"\")|(?:\\\\(?:[^x]|x[0-9a-fA-F]+)))*')+'\"').setName(\"string enclosed in double quotes\")\nsglQuotedString = Combine(Regex(r\"'(?:[^'\\n\\r\\\\]|(?:'')|(?:\\\\(?:[^x]|x[0-9a-fA-F]+)))*\")+\"'\").setName(\"string enclosed in single quotes\")\nquotedString = Combine(Regex(r'\"(?:[^\"\\n\\r\\\\]|(?:\"\")|(?:\\\\(?:[^x]|x[0-9a-fA-F]+)))*')+'\"'|\n                       Regex(r\"'(?:[^'\\n\\r\\\\]|(?:'')|(?:\\\\(?:[^x]|x[0-9a-fA-F]+)))*\")+\"'\").setName(\"quotedString using single or double quotes\")\nunicodeString = Combine(_L('u') + quotedString.copy()).setName(\"unicode string literal\")\n\ndef nestedExpr(opener=\"(\", closer=\")\", content=None, ignoreExpr=quotedString.copy()):\n    \"\"\"\n    Helper method for defining nested lists enclosed in opening and closing\n    delimiters (\"(\" and \")\" are the default).\n\n    Parameters:\n     - opener - opening character for a nested list (default=C{\"(\"}); can also be a pyparsing expression\n     - closer - closing character for a nested list (default=C{\")\"}); can also be a pyparsing expression\n     - content - expression for items within the nested lists (default=C{None})\n     - ignoreExpr - expression for ignoring opening and closing delimiters (default=C{quotedString})\n\n    If an expression is not provided for the content argument, the nested\n    expression will capture all whitespace-delimited content between delimiters\n    as a list of separate values.\n\n    Use the C{ignoreExpr} argument to define expressions that may contain\n    opening or closing characters that should not be treated as opening\n    or closing characters for nesting, such as quotedString or a comment\n    expression.  Specify multiple expressions using an C{L{Or}} or C{L{MatchFirst}}.\n    The default is L{quotedString}, but if no expressions are to be ignored,\n    then pass C{None} for this argument.\n\n    Example::\n        data_type = oneOf(\"void int short long char float double\")\n        decl_data_type = Combine(data_type + Optional(Word('*')))\n        ident = Word(alphas+'_', alphanums+'_')\n        number = pyparsing_common.number\n        arg = Group(decl_data_type + ident)\n        LPAR,RPAR = map(Suppress, \"()\")\n\n        code_body = nestedExpr('{', '}', ignoreExpr=(quotedString | cStyleComment))\n\n        c_function = (decl_data_type(\"type\") \n                      + ident(\"name\")\n                      + LPAR + Optional(delimitedList(arg), [])(\"args\") + RPAR \n                      + code_body(\"body\"))\n        c_function.ignore(cStyleComment)\n        \n        source_code = '''\n            int is_odd(int x) { \n                return (x%2); \n            }\n                \n            int dec_to_hex(char hchar) { \n                if (hchar >= '0' && hchar <= '9') { \n                    return (ord(hchar)-ord('0')); \n                } else { \n                    return (10+ord(hchar)-ord('A'));\n                } \n            }\n        '''\n        for func in c_function.searchString(source_code):\n            print(\"%(name)s (%(type)s) args: %(args)s\" % func)\n\n    prints::\n        is_odd (int) args: [['int', 'x']]\n        dec_to_hex (int) args: [['char', 'hchar']]\n    \"\"\"\n    if opener == closer:\n        raise ValueError(\"opening and closing strings cannot be the same\")\n    if content is None:\n        if isinstance(opener,basestring) and isinstance(closer,basestring):\n            if len(opener) == 1 and len(closer)==1:\n                if ignoreExpr is not None:\n                    content = (Combine(OneOrMore(~ignoreExpr +\n                                    CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS,exact=1))\n                                ).setParseAction(lambda t:t[0].strip()))\n                else:\n                    content = (empty.copy()+CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS\n                                ).setParseAction(lambda t:t[0].strip()))\n            else:\n                if ignoreExpr is not None:\n                    content = (Combine(OneOrMore(~ignoreExpr + \n                                    ~Literal(opener) + ~Literal(closer) +\n                                    CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1))\n                                ).setParseAction(lambda t:t[0].strip()))\n                else:\n                    content = (Combine(OneOrMore(~Literal(opener) + ~Literal(closer) +\n                                    CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1))\n                                ).setParseAction(lambda t:t[0].strip()))\n        else:\n            raise ValueError(\"opening and closing arguments must be strings if no content expression is given\")\n    ret = Forward()\n    if ignoreExpr is not None:\n        ret <<= Group( Suppress(opener) + ZeroOrMore( ignoreExpr | ret | content ) + Suppress(closer) )\n    else:\n        ret <<= Group( Suppress(opener) + ZeroOrMore( ret | content )  + Suppress(closer) )\n    ret.setName('nested %s%s expression' % (opener,closer))\n    return ret\n\ndef indentedBlock(blockStatementExpr, indentStack, indent=True):\n    \"\"\"\n    Helper method for defining space-delimited indentation blocks, such as\n    those used to define block statements in Python source code.\n\n    Parameters:\n     - blockStatementExpr - expression defining syntax of statement that\n            is repeated within the indented block\n     - indentStack - list created by caller to manage indentation stack\n            (multiple statementWithIndentedBlock expressions within a single grammar\n            should share a common indentStack)\n     - indent - boolean indicating whether block must be indented beyond the\n            the current level; set to False for block of left-most statements\n            (default=C{True})\n\n    A valid block must contain at least one C{blockStatement}.\n\n    Example::\n        data = '''\n        def A(z):\n          A1\n          B = 100\n          G = A2\n          A2\n          A3\n        B\n        def BB(a,b,c):\n          BB1\n          def BBA():\n            bba1\n            bba2\n            bba3\n        C\n        D\n        def spam(x,y):\n             def eggs(z):\n                 pass\n        '''\n\n\n        indentStack = [1]\n        stmt = Forward()\n\n        identifier = Word(alphas, alphanums)\n        funcDecl = (\"def\" + identifier + Group( \"(\" + Optional( delimitedList(identifier) ) + \")\" ) + \":\")\n        func_body = indentedBlock(stmt, indentStack)\n        funcDef = Group( funcDecl + func_body )\n\n        rvalue = Forward()\n        funcCall = Group(identifier + \"(\" + Optional(delimitedList(rvalue)) + \")\")\n        rvalue << (funcCall | identifier | Word(nums))\n        assignment = Group(identifier + \"=\" + rvalue)\n        stmt << ( funcDef | assignment | identifier )\n\n        module_body = OneOrMore(stmt)\n\n        parseTree = module_body.parseString(data)\n        parseTree.pprint()\n    prints::\n        [['def',\n          'A',\n          ['(', 'z', ')'],\n          ':',\n          [['A1'], [['B', '=', '100']], [['G', '=', 'A2']], ['A2'], ['A3']]],\n         'B',\n         ['def',\n          'BB',\n          ['(', 'a', 'b', 'c', ')'],\n          ':',\n          [['BB1'], [['def', 'BBA', ['(', ')'], ':', [['bba1'], ['bba2'], ['bba3']]]]]],\n         'C',\n         'D',\n         ['def',\n          'spam',\n          ['(', 'x', 'y', ')'],\n          ':',\n          [[['def', 'eggs', ['(', 'z', ')'], ':', [['pass']]]]]]] \n    \"\"\"\n    def checkPeerIndent(s,l,t):\n        if l >= len(s): return\n        curCol = col(l,s)\n        if curCol != indentStack[-1]:\n            if curCol > indentStack[-1]:\n                raise ParseFatalException(s,l,\"illegal nesting\")\n            raise ParseException(s,l,\"not a peer entry\")\n\n    def checkSubIndent(s,l,t):\n        curCol = col(l,s)\n        if curCol > indentStack[-1]:\n            indentStack.append( curCol )\n        else:\n            raise ParseException(s,l,\"not a subentry\")\n\n    def checkUnindent(s,l,t):\n        if l >= len(s): return\n        curCol = col(l,s)\n        if not(indentStack and curCol < indentStack[-1] and curCol <= indentStack[-2]):\n            raise ParseException(s,l,\"not an unindent\")\n        indentStack.pop()\n\n    NL = OneOrMore(LineEnd().setWhitespaceChars(\"\\t \").suppress())\n    INDENT = (Empty() + Empty().setParseAction(checkSubIndent)).setName('INDENT')\n    PEER   = Empty().setParseAction(checkPeerIndent).setName('')\n    UNDENT = Empty().setParseAction(checkUnindent).setName('UNINDENT')\n    if indent:\n        smExpr = Group( Optional(NL) +\n            #~ FollowedBy(blockStatementExpr) +\n            INDENT + (OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) + UNDENT)\n    else:\n        smExpr = Group( Optional(NL) +\n            (OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) )\n    blockStatementExpr.ignore(_bslash + LineEnd())\n    return smExpr.setName('indented block')\n\nalphas8bit = srange(r\"[\\0xc0-\\0xd6\\0xd8-\\0xf6\\0xf8-\\0xff]\")\npunc8bit = srange(r\"[\\0xa1-\\0xbf\\0xd7\\0xf7]\")\n\nanyOpenTag,anyCloseTag = makeHTMLTags(Word(alphas,alphanums+\"_:\").setName('any tag'))\n_htmlEntityMap = dict(zip(\"gt lt amp nbsp quot apos\".split(),'><& \"\\''))\ncommonHTMLEntity = Regex('&(?P<entity>' + '|'.join(_htmlEntityMap.keys()) +\");\").setName(\"common HTML entity\")\ndef replaceHTMLEntity(t):\n    \"\"\"Helper parser action to replace common HTML entities with their special characters\"\"\"\n    return _htmlEntityMap.get(t.entity)\n\n# it's easy to get these comment structures wrong - they're very common, so may as well make them available\ncStyleComment = Combine(Regex(r\"/\\*(?:[^*]|\\*(?!/))*\") + '*/').setName(\"C style comment\")\n\"Comment of the form C{/* ... */}\"\n\nhtmlComment = Regex(r\"<!--[\\s\\S]*?-->\").setName(\"HTML comment\")\n\"Comment of the form C{<!-- ... -->}\"\n\nrestOfLine = Regex(r\".*\").leaveWhitespace().setName(\"rest of line\")\ndblSlashComment = Regex(r\"//(?:\\\\\\n|[^\\n])*\").setName(\"// comment\")\n\"Comment of the form C{// ... (to end of line)}\"\n\ncppStyleComment = Combine(Regex(r\"/\\*(?:[^*]|\\*(?!/))*\") + '*/'| dblSlashComment).setName(\"C++ style comment\")\n\"Comment of either form C{L{cStyleComment}} or C{L{dblSlashComment}}\"\n\njavaStyleComment = cppStyleComment\n\"Same as C{L{cppStyleComment}}\"\n\npythonStyleComment = Regex(r\"#.*\").setName(\"Python style comment\")\n\"Comment of the form C{# ... (to end of line)}\"\n\n_commasepitem = Combine(OneOrMore(Word(printables, excludeChars=',') +\n                                  Optional( Word(\" \\t\") +\n                                            ~Literal(\",\") + ~LineEnd() ) ) ).streamline().setName(\"commaItem\")\ncommaSeparatedList = delimitedList( Optional( quotedString.copy() | _commasepitem, default=\"\") ).setName(\"commaSeparatedList\")\n\"\"\"(Deprecated) Predefined expression of 1 or more printable words or quoted strings, separated by commas.\n   This expression is deprecated in favor of L{pyparsing_common.comma_separated_list}.\"\"\"\n\n# some other useful expressions - using lower-case class name since we are really using this as a namespace\nclass pyparsing_common:\n    \"\"\"\n    Here are some common low-level expressions that may be useful in jump-starting parser development:\n     - numeric forms (L{integers<integer>}, L{reals<real>}, L{scientific notation<sci_real>})\n     - common L{programming identifiers<identifier>}\n     - network addresses (L{MAC<mac_address>}, L{IPv4<ipv4_address>}, L{IPv6<ipv6_address>})\n     - ISO8601 L{dates<iso8601_date>} and L{datetime<iso8601_datetime>}\n     - L{UUID<uuid>}\n     - L{comma-separated list<comma_separated_list>}\n    Parse actions:\n     - C{L{convertToInteger}}\n     - C{L{convertToFloat}}\n     - C{L{convertToDate}}\n     - C{L{convertToDatetime}}\n     - C{L{stripHTMLTags}}\n     - C{L{upcaseTokens}}\n     - C{L{downcaseTokens}}\n\n    Example::\n        pyparsing_common.number.runTests('''\n            # any int or real number, returned as the appropriate type\n            100\n            -100\n            +100\n            3.14159\n            6.02e23\n            1e-12\n            ''')\n\n        pyparsing_common.fnumber.runTests('''\n            # any int or real number, returned as float\n            100\n            -100\n            +100\n            3.14159\n            6.02e23\n            1e-12\n            ''')\n\n        pyparsing_common.hex_integer.runTests('''\n            # hex numbers\n            100\n            FF\n            ''')\n\n        pyparsing_common.fraction.runTests('''\n            # fractions\n            1/2\n            -3/4\n            ''')\n\n        pyparsing_common.mixed_integer.runTests('''\n            # mixed fractions\n            1\n            1/2\n            -3/4\n            1-3/4\n            ''')\n\n        import uuid\n        pyparsing_common.uuid.setParseAction(tokenMap(uuid.UUID))\n        pyparsing_common.uuid.runTests('''\n            # uuid\n            12345678-1234-5678-1234-567812345678\n            ''')\n    prints::\n        # any int or real number, returned as the appropriate type\n        100\n        [100]\n\n        -100\n        [-100]\n\n        +100\n        [100]\n\n        3.14159\n        [3.14159]\n\n        6.02e23\n        [6.02e+23]\n\n        1e-12\n        [1e-12]\n\n        # any int or real number, returned as float\n        100\n        [100.0]\n\n        -100\n        [-100.0]\n\n        +100\n        [100.0]\n\n        3.14159\n        [3.14159]\n\n        6.02e23\n        [6.02e+23]\n\n        1e-12\n        [1e-12]\n\n        # hex numbers\n        100\n        [256]\n\n        FF\n        [255]\n\n        # fractions\n        1/2\n        [0.5]\n\n        -3/4\n        [-0.75]\n\n        # mixed fractions\n        1\n        [1]\n\n        1/2\n        [0.5]\n\n        -3/4\n        [-0.75]\n\n        1-3/4\n        [1.75]\n\n        # uuid\n        12345678-1234-5678-1234-567812345678\n        [UUID('12345678-1234-5678-1234-567812345678')]\n    \"\"\"\n\n    convertToInteger = tokenMap(int)\n    \"\"\"\n    Parse action for converting parsed integers to Python int\n    \"\"\"\n\n    convertToFloat = tokenMap(float)\n    \"\"\"\n    Parse action for converting parsed numbers to Python float\n    \"\"\"\n\n    integer = Word(nums).setName(\"integer\").setParseAction(convertToInteger)\n    \"\"\"expression that parses an unsigned integer, returns an int\"\"\"\n\n    hex_integer = Word(hexnums).setName(\"hex integer\").setParseAction(tokenMap(int,16))\n    \"\"\"expression that parses a hexadecimal integer, returns an int\"\"\"\n\n    signed_integer = Regex(r'[+-]?\\d+').setName(\"signed integer\").setParseAction(convertToInteger)\n    \"\"\"expression that parses an integer with optional leading sign, returns an int\"\"\"\n\n    fraction = (signed_integer().setParseAction(convertToFloat) + '/' + signed_integer().setParseAction(convertToFloat)).setName(\"fraction\")\n    \"\"\"fractional expression of an integer divided by an integer, returns a float\"\"\"\n    fraction.addParseAction(lambda t: t[0]/t[-1])\n\n    mixed_integer = (fraction | signed_integer + Optional(Optional('-').suppress() + fraction)).setName(\"fraction or mixed integer-fraction\")\n    \"\"\"mixed integer of the form 'integer - fraction', with optional leading integer, returns float\"\"\"\n    mixed_integer.addParseAction(sum)\n\n    real = Regex(r'[+-]?\\d+\\.\\d*').setName(\"real number\").setParseAction(convertToFloat)\n    \"\"\"expression that parses a floating point number and returns a float\"\"\"\n\n    sci_real = Regex(r'[+-]?\\d+([eE][+-]?\\d+|\\.\\d*([eE][+-]?\\d+)?)').setName(\"real number with scientific notation\").setParseAction(convertToFloat)\n    \"\"\"expression that parses a floating point number with optional scientific notation and returns a float\"\"\"\n\n    # streamlining this expression makes the docs nicer-looking\n    number = (sci_real | real | signed_integer).streamline()\n    \"\"\"any numeric expression, returns the corresponding Python type\"\"\"\n\n    fnumber = Regex(r'[+-]?\\d+\\.?\\d*([eE][+-]?\\d+)?').setName(\"fnumber\").setParseAction(convertToFloat)\n    \"\"\"any int or real number, returned as float\"\"\"\n    \n    identifier = Word(alphas+'_', alphanums+'_').setName(\"identifier\")\n    \"\"\"typical code identifier (leading alpha or '_', followed by 0 or more alphas, nums, or '_')\"\"\"\n    \n    ipv4_address = Regex(r'(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})(\\.(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})){3}').setName(\"IPv4 address\")\n    \"IPv4 address (C{0.0.0.0 - 255.255.255.255})\"\n\n    _ipv6_part = Regex(r'[0-9a-fA-F]{1,4}').setName(\"hex_integer\")\n    _full_ipv6_address = (_ipv6_part + (':' + _ipv6_part)*7).setName(\"full IPv6 address\")\n    _short_ipv6_address = (Optional(_ipv6_part + (':' + _ipv6_part)*(0,6)) + \"::\" + Optional(_ipv6_part + (':' + _ipv6_part)*(0,6))).setName(\"short IPv6 address\")\n    _short_ipv6_address.addCondition(lambda t: sum(1 for tt in t if pyparsing_common._ipv6_part.matches(tt)) < 8)\n    _mixed_ipv6_address = (\"::ffff:\" + ipv4_address).setName(\"mixed IPv6 address\")\n    ipv6_address = Combine((_full_ipv6_address | _mixed_ipv6_address | _short_ipv6_address).setName(\"IPv6 address\")).setName(\"IPv6 address\")\n    \"IPv6 address (long, short, or mixed form)\"\n    \n    mac_address = Regex(r'[0-9a-fA-F]{2}([:.-])[0-9a-fA-F]{2}(?:\\1[0-9a-fA-F]{2}){4}').setName(\"MAC address\")\n    \"MAC address xx:xx:xx:xx:xx (may also have '-' or '.' delimiters)\"\n\n    @staticmethod\n    def convertToDate(fmt=\"%Y-%m-%d\"):\n        \"\"\"\n        Helper to create a parse action for converting parsed date string to Python datetime.date\n\n        Params -\n         - fmt - format to be passed to datetime.strptime (default=C{\"%Y-%m-%d\"})\n\n        Example::\n            date_expr = pyparsing_common.iso8601_date.copy()\n            date_expr.setParseAction(pyparsing_common.convertToDate())\n            print(date_expr.parseString(\"1999-12-31\"))\n        prints::\n            [datetime.date(1999, 12, 31)]\n        \"\"\"\n        def cvt_fn(s,l,t):\n            try:\n                return datetime.strptime(t[0], fmt).date()\n            except ValueError as ve:\n                raise ParseException(s, l, str(ve))\n        return cvt_fn\n\n    @staticmethod\n    def convertToDatetime(fmt=\"%Y-%m-%dT%H:%M:%S.%f\"):\n        \"\"\"\n        Helper to create a parse action for converting parsed datetime string to Python datetime.datetime\n\n        Params -\n         - fmt - format to be passed to datetime.strptime (default=C{\"%Y-%m-%dT%H:%M:%S.%f\"})\n\n        Example::\n            dt_expr = pyparsing_common.iso8601_datetime.copy()\n            dt_expr.setParseAction(pyparsing_common.convertToDatetime())\n            print(dt_expr.parseString(\"1999-12-31T23:59:59.999\"))\n        prints::\n            [datetime.datetime(1999, 12, 31, 23, 59, 59, 999000)]\n        \"\"\"\n        def cvt_fn(s,l,t):\n            try:\n                return datetime.strptime(t[0], fmt)\n            except ValueError as ve:\n                raise ParseException(s, l, str(ve))\n        return cvt_fn\n\n    iso8601_date = Regex(r'(?P<year>\\d{4})(?:-(?P<month>\\d\\d)(?:-(?P<day>\\d\\d))?)?').setName(\"ISO8601 date\")\n    \"ISO8601 date (C{yyyy-mm-dd})\"\n\n    iso8601_datetime = Regex(r'(?P<year>\\d{4})-(?P<month>\\d\\d)-(?P<day>\\d\\d)[T ](?P<hour>\\d\\d):(?P<minute>\\d\\d)(:(?P<second>\\d\\d(\\.\\d*)?)?)?(?P<tz>Z|[+-]\\d\\d:?\\d\\d)?').setName(\"ISO8601 datetime\")\n    \"ISO8601 datetime (C{yyyy-mm-ddThh:mm:ss.s(Z|+-00:00)}) - trailing seconds, milliseconds, and timezone optional; accepts separating C{'T'} or C{' '}\"\n\n    uuid = Regex(r'[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}').setName(\"UUID\")\n    \"UUID (C{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx})\"\n\n    _html_stripper = anyOpenTag.suppress() | anyCloseTag.suppress()\n    @staticmethod\n    def stripHTMLTags(s, l, tokens):\n        \"\"\"\n        Parse action to remove HTML tags from web page HTML source\n\n        Example::\n            # strip HTML links from normal text \n            text = '<td>More info at the <a href=\"http://pyparsing.wikispaces.com\">pyparsing</a> wiki page</td>'\n            td,td_end = makeHTMLTags(\"TD\")\n            table_text = td + SkipTo(td_end).setParseAction(pyparsing_common.stripHTMLTags)(\"body\") + td_end\n            \n            print(table_text.parseString(text).body) # -> 'More info at the pyparsing wiki page'\n        \"\"\"\n        return pyparsing_common._html_stripper.transformString(tokens[0])\n\n    _commasepitem = Combine(OneOrMore(~Literal(\",\") + ~LineEnd() + Word(printables, excludeChars=',') \n                                        + Optional( White(\" \\t\") ) ) ).streamline().setName(\"commaItem\")\n    comma_separated_list = delimitedList( Optional( quotedString.copy() | _commasepitem, default=\"\") ).setName(\"comma separated list\")\n    \"\"\"Predefined expression of 1 or more printable words or quoted strings, separated by commas.\"\"\"\n\n    upcaseTokens = staticmethod(tokenMap(lambda t: _ustr(t).upper()))\n    \"\"\"Parse action to convert tokens to upper case.\"\"\"\n\n    downcaseTokens = staticmethod(tokenMap(lambda t: _ustr(t).lower()))\n    \"\"\"Parse action to convert tokens to lower case.\"\"\"\n\n\nif __name__ == \"__main__\":\n\n    selectToken    = CaselessLiteral(\"select\")\n    fromToken      = CaselessLiteral(\"from\")\n\n    ident          = Word(alphas, alphanums + \"_$\")\n\n    columnName     = delimitedList(ident, \".\", combine=True).setParseAction(upcaseTokens)\n    columnNameList = Group(delimitedList(columnName)).setName(\"columns\")\n    columnSpec     = ('*' | columnNameList)\n\n    tableName      = delimitedList(ident, \".\", combine=True).setParseAction(upcaseTokens)\n    tableNameList  = Group(delimitedList(tableName)).setName(\"tables\")\n    \n    simpleSQL      = selectToken(\"command\") + columnSpec(\"columns\") + fromToken + tableNameList(\"tables\")\n\n    # demo runTests method, including embedded comments in test string\n    simpleSQL.runTests(\"\"\"\n        # '*' as column list and dotted table name\n        select * from SYS.XYZZY\n\n        # caseless match on \"SELECT\", and casts back to \"select\"\n        SELECT * from XYZZY, ABC\n\n        # list of column names, and mixed case SELECT keyword\n        Select AA,BB,CC from Sys.dual\n\n        # multiple tables\n        Select A, B, C from Sys.dual, Table2\n\n        # invalid SELECT keyword - should fail\n        Xelect A, B, C from Sys.dual\n\n        # incomplete command - should fail\n        Select\n\n        # invalid column name - should fail\n        Select ^^^ frox Sys.dual\n\n        \"\"\")\n\n    pyparsing_common.number.runTests(\"\"\"\n        100\n        -100\n        +100\n        3.14159\n        6.02e23\n        1e-12\n        \"\"\")\n\n    # any int or real number, returned as float\n    pyparsing_common.fnumber.runTests(\"\"\"\n        100\n        -100\n        +100\n        3.14159\n        6.02e23\n        1e-12\n        \"\"\")\n\n    pyparsing_common.hex_integer.runTests(\"\"\"\n        100\n        FF\n        \"\"\")\n\n    import uuid\n    pyparsing_common.uuid.setParseAction(tokenMap(uuid.UUID))\n    pyparsing_common.uuid.runTests(\"\"\"\n        12345678-1234-5678-1234-567812345678\n        \"\"\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/_vendor/six.py",
    "content": "\"\"\"Utilities for writing code that runs on Python 2 and 3\"\"\"\n\n# Copyright (c) 2010-2015 Benjamin Peterson\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom __future__ import absolute_import\n\nimport functools\nimport itertools\nimport operator\nimport sys\nimport types\n\n__author__ = \"Benjamin Peterson <benjamin@python.org>\"\n__version__ = \"1.10.0\"\n\n\n# Useful for very coarse version differentiation.\nPY2 = sys.version_info[0] == 2\nPY3 = sys.version_info[0] == 3\nPY34 = sys.version_info[0:2] >= (3, 4)\n\nif PY3:\n    string_types = str,\n    integer_types = int,\n    class_types = type,\n    text_type = str\n    binary_type = bytes\n\n    MAXSIZE = sys.maxsize\nelse:\n    string_types = basestring,\n    integer_types = (int, long)\n    class_types = (type, types.ClassType)\n    text_type = unicode\n    binary_type = str\n\n    if sys.platform.startswith(\"java\"):\n        # Jython always uses 32 bits.\n        MAXSIZE = int((1 << 31) - 1)\n    else:\n        # It's possible to have sizeof(long) != sizeof(Py_ssize_t).\n        class X(object):\n\n            def __len__(self):\n                return 1 << 31\n        try:\n            len(X())\n        except OverflowError:\n            # 32-bit\n            MAXSIZE = int((1 << 31) - 1)\n        else:\n            # 64-bit\n            MAXSIZE = int((1 << 63) - 1)\n        del X\n\n\ndef _add_doc(func, doc):\n    \"\"\"Add documentation to a function.\"\"\"\n    func.__doc__ = doc\n\n\ndef _import_module(name):\n    \"\"\"Import module, returning the module after the last dot.\"\"\"\n    __import__(name)\n    return sys.modules[name]\n\n\nclass _LazyDescr(object):\n\n    def __init__(self, name):\n        self.name = name\n\n    def __get__(self, obj, tp):\n        result = self._resolve()\n        setattr(obj, self.name, result)  # Invokes __set__.\n        try:\n            # This is a bit ugly, but it avoids running this again by\n            # removing this descriptor.\n            delattr(obj.__class__, self.name)\n        except AttributeError:\n            pass\n        return result\n\n\nclass MovedModule(_LazyDescr):\n\n    def __init__(self, name, old, new=None):\n        super(MovedModule, self).__init__(name)\n        if PY3:\n            if new is None:\n                new = name\n            self.mod = new\n        else:\n            self.mod = old\n\n    def _resolve(self):\n        return _import_module(self.mod)\n\n    def __getattr__(self, attr):\n        _module = self._resolve()\n        value = getattr(_module, attr)\n        setattr(self, attr, value)\n        return value\n\n\nclass _LazyModule(types.ModuleType):\n\n    def __init__(self, name):\n        super(_LazyModule, self).__init__(name)\n        self.__doc__ = self.__class__.__doc__\n\n    def __dir__(self):\n        attrs = [\"__doc__\", \"__name__\"]\n        attrs += [attr.name for attr in self._moved_attributes]\n        return attrs\n\n    # Subclasses should override this\n    _moved_attributes = []\n\n\nclass MovedAttribute(_LazyDescr):\n\n    def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):\n        super(MovedAttribute, self).__init__(name)\n        if PY3:\n            if new_mod is None:\n                new_mod = name\n            self.mod = new_mod\n            if new_attr is None:\n                if old_attr is None:\n                    new_attr = name\n                else:\n                    new_attr = old_attr\n            self.attr = new_attr\n        else:\n            self.mod = old_mod\n            if old_attr is None:\n                old_attr = name\n            self.attr = old_attr\n\n    def _resolve(self):\n        module = _import_module(self.mod)\n        return getattr(module, self.attr)\n\n\nclass _SixMetaPathImporter(object):\n\n    \"\"\"\n    A meta path importer to import six.moves and its submodules.\n\n    This class implements a PEP302 finder and loader. It should be compatible\n    with Python 2.5 and all existing versions of Python3\n    \"\"\"\n\n    def __init__(self, six_module_name):\n        self.name = six_module_name\n        self.known_modules = {}\n\n    def _add_module(self, mod, *fullnames):\n        for fullname in fullnames:\n            self.known_modules[self.name + \".\" + fullname] = mod\n\n    def _get_module(self, fullname):\n        return self.known_modules[self.name + \".\" + fullname]\n\n    def find_module(self, fullname, path=None):\n        if fullname in self.known_modules:\n            return self\n        return None\n\n    def __get_module(self, fullname):\n        try:\n            return self.known_modules[fullname]\n        except KeyError:\n            raise ImportError(\"This loader does not know module \" + fullname)\n\n    def load_module(self, fullname):\n        try:\n            # in case of a reload\n            return sys.modules[fullname]\n        except KeyError:\n            pass\n        mod = self.__get_module(fullname)\n        if isinstance(mod, MovedModule):\n            mod = mod._resolve()\n        else:\n            mod.__loader__ = self\n        sys.modules[fullname] = mod\n        return mod\n\n    def is_package(self, fullname):\n        \"\"\"\n        Return true, if the named module is a package.\n\n        We need this method to get correct spec objects with\n        Python 3.4 (see PEP451)\n        \"\"\"\n        return hasattr(self.__get_module(fullname), \"__path__\")\n\n    def get_code(self, fullname):\n        \"\"\"Return None\n\n        Required, if is_package is implemented\"\"\"\n        self.__get_module(fullname)  # eventually raises ImportError\n        return None\n    get_source = get_code  # same as get_code\n\n_importer = _SixMetaPathImporter(__name__)\n\n\nclass _MovedItems(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects\"\"\"\n    __path__ = []  # mark as package\n\n\n_moved_attributes = [\n    MovedAttribute(\"cStringIO\", \"cStringIO\", \"io\", \"StringIO\"),\n    MovedAttribute(\"filter\", \"itertools\", \"builtins\", \"ifilter\", \"filter\"),\n    MovedAttribute(\"filterfalse\", \"itertools\", \"itertools\", \"ifilterfalse\", \"filterfalse\"),\n    MovedAttribute(\"input\", \"__builtin__\", \"builtins\", \"raw_input\", \"input\"),\n    MovedAttribute(\"intern\", \"__builtin__\", \"sys\"),\n    MovedAttribute(\"map\", \"itertools\", \"builtins\", \"imap\", \"map\"),\n    MovedAttribute(\"getcwd\", \"os\", \"os\", \"getcwdu\", \"getcwd\"),\n    MovedAttribute(\"getcwdb\", \"os\", \"os\", \"getcwd\", \"getcwdb\"),\n    MovedAttribute(\"range\", \"__builtin__\", \"builtins\", \"xrange\", \"range\"),\n    MovedAttribute(\"reload_module\", \"__builtin__\", \"importlib\" if PY34 else \"imp\", \"reload\"),\n    MovedAttribute(\"reduce\", \"__builtin__\", \"functools\"),\n    MovedAttribute(\"shlex_quote\", \"pipes\", \"shlex\", \"quote\"),\n    MovedAttribute(\"StringIO\", \"StringIO\", \"io\"),\n    MovedAttribute(\"UserDict\", \"UserDict\", \"collections\"),\n    MovedAttribute(\"UserList\", \"UserList\", \"collections\"),\n    MovedAttribute(\"UserString\", \"UserString\", \"collections\"),\n    MovedAttribute(\"xrange\", \"__builtin__\", \"builtins\", \"xrange\", \"range\"),\n    MovedAttribute(\"zip\", \"itertools\", \"builtins\", \"izip\", \"zip\"),\n    MovedAttribute(\"zip_longest\", \"itertools\", \"itertools\", \"izip_longest\", \"zip_longest\"),\n    MovedModule(\"builtins\", \"__builtin__\"),\n    MovedModule(\"configparser\", \"ConfigParser\"),\n    MovedModule(\"copyreg\", \"copy_reg\"),\n    MovedModule(\"dbm_gnu\", \"gdbm\", \"dbm.gnu\"),\n    MovedModule(\"_dummy_thread\", \"dummy_thread\", \"_dummy_thread\"),\n    MovedModule(\"http_cookiejar\", \"cookielib\", \"http.cookiejar\"),\n    MovedModule(\"http_cookies\", \"Cookie\", \"http.cookies\"),\n    MovedModule(\"html_entities\", \"htmlentitydefs\", \"html.entities\"),\n    MovedModule(\"html_parser\", \"HTMLParser\", \"html.parser\"),\n    MovedModule(\"http_client\", \"httplib\", \"http.client\"),\n    MovedModule(\"email_mime_multipart\", \"email.MIMEMultipart\", \"email.mime.multipart\"),\n    MovedModule(\"email_mime_nonmultipart\", \"email.MIMENonMultipart\", \"email.mime.nonmultipart\"),\n    MovedModule(\"email_mime_text\", \"email.MIMEText\", \"email.mime.text\"),\n    MovedModule(\"email_mime_base\", \"email.MIMEBase\", \"email.mime.base\"),\n    MovedModule(\"BaseHTTPServer\", \"BaseHTTPServer\", \"http.server\"),\n    MovedModule(\"CGIHTTPServer\", \"CGIHTTPServer\", \"http.server\"),\n    MovedModule(\"SimpleHTTPServer\", \"SimpleHTTPServer\", \"http.server\"),\n    MovedModule(\"cPickle\", \"cPickle\", \"pickle\"),\n    MovedModule(\"queue\", \"Queue\"),\n    MovedModule(\"reprlib\", \"repr\"),\n    MovedModule(\"socketserver\", \"SocketServer\"),\n    MovedModule(\"_thread\", \"thread\", \"_thread\"),\n    MovedModule(\"tkinter\", \"Tkinter\"),\n    MovedModule(\"tkinter_dialog\", \"Dialog\", \"tkinter.dialog\"),\n    MovedModule(\"tkinter_filedialog\", \"FileDialog\", \"tkinter.filedialog\"),\n    MovedModule(\"tkinter_scrolledtext\", \"ScrolledText\", \"tkinter.scrolledtext\"),\n    MovedModule(\"tkinter_simpledialog\", \"SimpleDialog\", \"tkinter.simpledialog\"),\n    MovedModule(\"tkinter_tix\", \"Tix\", \"tkinter.tix\"),\n    MovedModule(\"tkinter_ttk\", \"ttk\", \"tkinter.ttk\"),\n    MovedModule(\"tkinter_constants\", \"Tkconstants\", \"tkinter.constants\"),\n    MovedModule(\"tkinter_dnd\", \"Tkdnd\", \"tkinter.dnd\"),\n    MovedModule(\"tkinter_colorchooser\", \"tkColorChooser\",\n                \"tkinter.colorchooser\"),\n    MovedModule(\"tkinter_commondialog\", \"tkCommonDialog\",\n                \"tkinter.commondialog\"),\n    MovedModule(\"tkinter_tkfiledialog\", \"tkFileDialog\", \"tkinter.filedialog\"),\n    MovedModule(\"tkinter_font\", \"tkFont\", \"tkinter.font\"),\n    MovedModule(\"tkinter_messagebox\", \"tkMessageBox\", \"tkinter.messagebox\"),\n    MovedModule(\"tkinter_tksimpledialog\", \"tkSimpleDialog\",\n                \"tkinter.simpledialog\"),\n    MovedModule(\"urllib_parse\", __name__ + \".moves.urllib_parse\", \"urllib.parse\"),\n    MovedModule(\"urllib_error\", __name__ + \".moves.urllib_error\", \"urllib.error\"),\n    MovedModule(\"urllib\", __name__ + \".moves.urllib\", __name__ + \".moves.urllib\"),\n    MovedModule(\"urllib_robotparser\", \"robotparser\", \"urllib.robotparser\"),\n    MovedModule(\"xmlrpc_client\", \"xmlrpclib\", \"xmlrpc.client\"),\n    MovedModule(\"xmlrpc_server\", \"SimpleXMLRPCServer\", \"xmlrpc.server\"),\n]\n# Add windows specific modules.\nif sys.platform == \"win32\":\n    _moved_attributes += [\n        MovedModule(\"winreg\", \"_winreg\"),\n    ]\n\nfor attr in _moved_attributes:\n    setattr(_MovedItems, attr.name, attr)\n    if isinstance(attr, MovedModule):\n        _importer._add_module(attr, \"moves.\" + attr.name)\ndel attr\n\n_MovedItems._moved_attributes = _moved_attributes\n\nmoves = _MovedItems(__name__ + \".moves\")\n_importer._add_module(moves, \"moves\")\n\n\nclass Module_six_moves_urllib_parse(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_parse\"\"\"\n\n\n_urllib_parse_moved_attributes = [\n    MovedAttribute(\"ParseResult\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"SplitResult\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"parse_qs\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"parse_qsl\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urldefrag\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urljoin\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlparse\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlsplit\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlunparse\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlunsplit\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"quote\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"quote_plus\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"unquote\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"unquote_plus\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"urlencode\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splitquery\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splittag\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splituser\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"uses_fragment\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_netloc\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_params\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_query\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_relative\", \"urlparse\", \"urllib.parse\"),\n]\nfor attr in _urllib_parse_moved_attributes:\n    setattr(Module_six_moves_urllib_parse, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes\n\n_importer._add_module(Module_six_moves_urllib_parse(__name__ + \".moves.urllib_parse\"),\n                      \"moves.urllib_parse\", \"moves.urllib.parse\")\n\n\nclass Module_six_moves_urllib_error(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_error\"\"\"\n\n\n_urllib_error_moved_attributes = [\n    MovedAttribute(\"URLError\", \"urllib2\", \"urllib.error\"),\n    MovedAttribute(\"HTTPError\", \"urllib2\", \"urllib.error\"),\n    MovedAttribute(\"ContentTooShortError\", \"urllib\", \"urllib.error\"),\n]\nfor attr in _urllib_error_moved_attributes:\n    setattr(Module_six_moves_urllib_error, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes\n\n_importer._add_module(Module_six_moves_urllib_error(__name__ + \".moves.urllib.error\"),\n                      \"moves.urllib_error\", \"moves.urllib.error\")\n\n\nclass Module_six_moves_urllib_request(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_request\"\"\"\n\n\n_urllib_request_moved_attributes = [\n    MovedAttribute(\"urlopen\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"install_opener\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"build_opener\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"pathname2url\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"url2pathname\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"getproxies\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"Request\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"OpenerDirector\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPDefaultErrorHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPRedirectHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPCookieProcessor\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"ProxyHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"BaseHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPPasswordMgr\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPPasswordMgrWithDefaultRealm\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"AbstractBasicAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPBasicAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"ProxyBasicAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"AbstractDigestAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPDigestAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"ProxyDigestAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPSHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"FileHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"FTPHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"CacheFTPHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"UnknownHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPErrorProcessor\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"urlretrieve\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"urlcleanup\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"URLopener\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"FancyURLopener\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"proxy_bypass\", \"urllib\", \"urllib.request\"),\n]\nfor attr in _urllib_request_moved_attributes:\n    setattr(Module_six_moves_urllib_request, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes\n\n_importer._add_module(Module_six_moves_urllib_request(__name__ + \".moves.urllib.request\"),\n                      \"moves.urllib_request\", \"moves.urllib.request\")\n\n\nclass Module_six_moves_urllib_response(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_response\"\"\"\n\n\n_urllib_response_moved_attributes = [\n    MovedAttribute(\"addbase\", \"urllib\", \"urllib.response\"),\n    MovedAttribute(\"addclosehook\", \"urllib\", \"urllib.response\"),\n    MovedAttribute(\"addinfo\", \"urllib\", \"urllib.response\"),\n    MovedAttribute(\"addinfourl\", \"urllib\", \"urllib.response\"),\n]\nfor attr in _urllib_response_moved_attributes:\n    setattr(Module_six_moves_urllib_response, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes\n\n_importer._add_module(Module_six_moves_urllib_response(__name__ + \".moves.urllib.response\"),\n                      \"moves.urllib_response\", \"moves.urllib.response\")\n\n\nclass Module_six_moves_urllib_robotparser(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_robotparser\"\"\"\n\n\n_urllib_robotparser_moved_attributes = [\n    MovedAttribute(\"RobotFileParser\", \"robotparser\", \"urllib.robotparser\"),\n]\nfor attr in _urllib_robotparser_moved_attributes:\n    setattr(Module_six_moves_urllib_robotparser, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes\n\n_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + \".moves.urllib.robotparser\"),\n                      \"moves.urllib_robotparser\", \"moves.urllib.robotparser\")\n\n\nclass Module_six_moves_urllib(types.ModuleType):\n\n    \"\"\"Create a six.moves.urllib namespace that resembles the Python 3 namespace\"\"\"\n    __path__ = []  # mark as package\n    parse = _importer._get_module(\"moves.urllib_parse\")\n    error = _importer._get_module(\"moves.urllib_error\")\n    request = _importer._get_module(\"moves.urllib_request\")\n    response = _importer._get_module(\"moves.urllib_response\")\n    robotparser = _importer._get_module(\"moves.urllib_robotparser\")\n\n    def __dir__(self):\n        return ['parse', 'error', 'request', 'response', 'robotparser']\n\n_importer._add_module(Module_six_moves_urllib(__name__ + \".moves.urllib\"),\n                      \"moves.urllib\")\n\n\ndef add_move(move):\n    \"\"\"Add an item to six.moves.\"\"\"\n    setattr(_MovedItems, move.name, move)\n\n\ndef remove_move(name):\n    \"\"\"Remove item from six.moves.\"\"\"\n    try:\n        delattr(_MovedItems, name)\n    except AttributeError:\n        try:\n            del moves.__dict__[name]\n        except KeyError:\n            raise AttributeError(\"no such move, %r\" % (name,))\n\n\nif PY3:\n    _meth_func = \"__func__\"\n    _meth_self = \"__self__\"\n\n    _func_closure = \"__closure__\"\n    _func_code = \"__code__\"\n    _func_defaults = \"__defaults__\"\n    _func_globals = \"__globals__\"\nelse:\n    _meth_func = \"im_func\"\n    _meth_self = \"im_self\"\n\n    _func_closure = \"func_closure\"\n    _func_code = \"func_code\"\n    _func_defaults = \"func_defaults\"\n    _func_globals = \"func_globals\"\n\n\ntry:\n    advance_iterator = next\nexcept NameError:\n    def advance_iterator(it):\n        return it.next()\nnext = advance_iterator\n\n\ntry:\n    callable = callable\nexcept NameError:\n    def callable(obj):\n        return any(\"__call__\" in klass.__dict__ for klass in type(obj).__mro__)\n\n\nif PY3:\n    def get_unbound_function(unbound):\n        return unbound\n\n    create_bound_method = types.MethodType\n\n    def create_unbound_method(func, cls):\n        return func\n\n    Iterator = object\nelse:\n    def get_unbound_function(unbound):\n        return unbound.im_func\n\n    def create_bound_method(func, obj):\n        return types.MethodType(func, obj, obj.__class__)\n\n    def create_unbound_method(func, cls):\n        return types.MethodType(func, None, cls)\n\n    class Iterator(object):\n\n        def next(self):\n            return type(self).__next__(self)\n\n    callable = callable\n_add_doc(get_unbound_function,\n         \"\"\"Get the function out of a possibly unbound function\"\"\")\n\n\nget_method_function = operator.attrgetter(_meth_func)\nget_method_self = operator.attrgetter(_meth_self)\nget_function_closure = operator.attrgetter(_func_closure)\nget_function_code = operator.attrgetter(_func_code)\nget_function_defaults = operator.attrgetter(_func_defaults)\nget_function_globals = operator.attrgetter(_func_globals)\n\n\nif PY3:\n    def iterkeys(d, **kw):\n        return iter(d.keys(**kw))\n\n    def itervalues(d, **kw):\n        return iter(d.values(**kw))\n\n    def iteritems(d, **kw):\n        return iter(d.items(**kw))\n\n    def iterlists(d, **kw):\n        return iter(d.lists(**kw))\n\n    viewkeys = operator.methodcaller(\"keys\")\n\n    viewvalues = operator.methodcaller(\"values\")\n\n    viewitems = operator.methodcaller(\"items\")\nelse:\n    def iterkeys(d, **kw):\n        return d.iterkeys(**kw)\n\n    def itervalues(d, **kw):\n        return d.itervalues(**kw)\n\n    def iteritems(d, **kw):\n        return d.iteritems(**kw)\n\n    def iterlists(d, **kw):\n        return d.iterlists(**kw)\n\n    viewkeys = operator.methodcaller(\"viewkeys\")\n\n    viewvalues = operator.methodcaller(\"viewvalues\")\n\n    viewitems = operator.methodcaller(\"viewitems\")\n\n_add_doc(iterkeys, \"Return an iterator over the keys of a dictionary.\")\n_add_doc(itervalues, \"Return an iterator over the values of a dictionary.\")\n_add_doc(iteritems,\n         \"Return an iterator over the (key, value) pairs of a dictionary.\")\n_add_doc(iterlists,\n         \"Return an iterator over the (key, [values]) pairs of a dictionary.\")\n\n\nif PY3:\n    def b(s):\n        return s.encode(\"latin-1\")\n\n    def u(s):\n        return s\n    unichr = chr\n    import struct\n    int2byte = struct.Struct(\">B\").pack\n    del struct\n    byte2int = operator.itemgetter(0)\n    indexbytes = operator.getitem\n    iterbytes = iter\n    import io\n    StringIO = io.StringIO\n    BytesIO = io.BytesIO\n    _assertCountEqual = \"assertCountEqual\"\n    if sys.version_info[1] <= 1:\n        _assertRaisesRegex = \"assertRaisesRegexp\"\n        _assertRegex = \"assertRegexpMatches\"\n    else:\n        _assertRaisesRegex = \"assertRaisesRegex\"\n        _assertRegex = \"assertRegex\"\nelse:\n    def b(s):\n        return s\n    # Workaround for standalone backslash\n\n    def u(s):\n        return unicode(s.replace(r'\\\\', r'\\\\\\\\'), \"unicode_escape\")\n    unichr = unichr\n    int2byte = chr\n\n    def byte2int(bs):\n        return ord(bs[0])\n\n    def indexbytes(buf, i):\n        return ord(buf[i])\n    iterbytes = functools.partial(itertools.imap, ord)\n    import StringIO\n    StringIO = BytesIO = StringIO.StringIO\n    _assertCountEqual = \"assertItemsEqual\"\n    _assertRaisesRegex = \"assertRaisesRegexp\"\n    _assertRegex = \"assertRegexpMatches\"\n_add_doc(b, \"\"\"Byte literal\"\"\")\n_add_doc(u, \"\"\"Text literal\"\"\")\n\n\ndef assertCountEqual(self, *args, **kwargs):\n    return getattr(self, _assertCountEqual)(*args, **kwargs)\n\n\ndef assertRaisesRegex(self, *args, **kwargs):\n    return getattr(self, _assertRaisesRegex)(*args, **kwargs)\n\n\ndef assertRegex(self, *args, **kwargs):\n    return getattr(self, _assertRegex)(*args, **kwargs)\n\n\nif PY3:\n    exec_ = getattr(moves.builtins, \"exec\")\n\n    def reraise(tp, value, tb=None):\n        if value is None:\n            value = tp()\n        if value.__traceback__ is not tb:\n            raise value.with_traceback(tb)\n        raise value\n\nelse:\n    def exec_(_code_, _globs_=None, _locs_=None):\n        \"\"\"Execute code in a namespace.\"\"\"\n        if _globs_ is None:\n            frame = sys._getframe(1)\n            _globs_ = frame.f_globals\n            if _locs_ is None:\n                _locs_ = frame.f_locals\n            del frame\n        elif _locs_ is None:\n            _locs_ = _globs_\n        exec(\"\"\"exec _code_ in _globs_, _locs_\"\"\")\n\n    exec_(\"\"\"def reraise(tp, value, tb=None):\n    raise tp, value, tb\n\"\"\")\n\n\nif sys.version_info[:2] == (3, 2):\n    exec_(\"\"\"def raise_from(value, from_value):\n    if from_value is None:\n        raise value\n    raise value from from_value\n\"\"\")\nelif sys.version_info[:2] > (3, 2):\n    exec_(\"\"\"def raise_from(value, from_value):\n    raise value from from_value\n\"\"\")\nelse:\n    def raise_from(value, from_value):\n        raise value\n\n\nprint_ = getattr(moves.builtins, \"print\", None)\nif print_ is None:\n    def print_(*args, **kwargs):\n        \"\"\"The new-style print function for Python 2.4 and 2.5.\"\"\"\n        fp = kwargs.pop(\"file\", sys.stdout)\n        if fp is None:\n            return\n\n        def write(data):\n            if not isinstance(data, basestring):\n                data = str(data)\n            # If the file has an encoding, encode unicode with it.\n            if (isinstance(fp, file) and\n                    isinstance(data, unicode) and\n                    fp.encoding is not None):\n                errors = getattr(fp, \"errors\", None)\n                if errors is None:\n                    errors = \"strict\"\n                data = data.encode(fp.encoding, errors)\n            fp.write(data)\n        want_unicode = False\n        sep = kwargs.pop(\"sep\", None)\n        if sep is not None:\n            if isinstance(sep, unicode):\n                want_unicode = True\n            elif not isinstance(sep, str):\n                raise TypeError(\"sep must be None or a string\")\n        end = kwargs.pop(\"end\", None)\n        if end is not None:\n            if isinstance(end, unicode):\n                want_unicode = True\n            elif not isinstance(end, str):\n                raise TypeError(\"end must be None or a string\")\n        if kwargs:\n            raise TypeError(\"invalid keyword arguments to print()\")\n        if not want_unicode:\n            for arg in args:\n                if isinstance(arg, unicode):\n                    want_unicode = True\n                    break\n        if want_unicode:\n            newline = unicode(\"\\n\")\n            space = unicode(\" \")\n        else:\n            newline = \"\\n\"\n            space = \" \"\n        if sep is None:\n            sep = space\n        if end is None:\n            end = newline\n        for i, arg in enumerate(args):\n            if i:\n                write(sep)\n            write(arg)\n        write(end)\nif sys.version_info[:2] < (3, 3):\n    _print = print_\n\n    def print_(*args, **kwargs):\n        fp = kwargs.get(\"file\", sys.stdout)\n        flush = kwargs.pop(\"flush\", False)\n        _print(*args, **kwargs)\n        if flush and fp is not None:\n            fp.flush()\n\n_add_doc(reraise, \"\"\"Reraise an exception.\"\"\")\n\nif sys.version_info[0:2] < (3, 4):\n    def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,\n              updated=functools.WRAPPER_UPDATES):\n        def wrapper(f):\n            f = functools.wraps(wrapped, assigned, updated)(f)\n            f.__wrapped__ = wrapped\n            return f\n        return wrapper\nelse:\n    wraps = functools.wraps\n\n\ndef with_metaclass(meta, *bases):\n    \"\"\"Create a base class with a metaclass.\"\"\"\n    # This requires a bit of explanation: the basic idea is to make a dummy\n    # metaclass for one level of class instantiation that replaces itself with\n    # the actual metaclass.\n    class metaclass(meta):\n\n        def __new__(cls, name, this_bases, d):\n            return meta(name, bases, d)\n    return type.__new__(metaclass, 'temporary_class', (), {})\n\n\ndef add_metaclass(metaclass):\n    \"\"\"Class decorator for creating a class with a metaclass.\"\"\"\n    def wrapper(cls):\n        orig_vars = cls.__dict__.copy()\n        slots = orig_vars.get('__slots__')\n        if slots is not None:\n            if isinstance(slots, str):\n                slots = [slots]\n            for slots_var in slots:\n                orig_vars.pop(slots_var)\n        orig_vars.pop('__dict__', None)\n        orig_vars.pop('__weakref__', None)\n        return metaclass(cls.__name__, cls.__bases__, orig_vars)\n    return wrapper\n\n\ndef python_2_unicode_compatible(klass):\n    \"\"\"\n    A decorator that defines __unicode__ and __str__ methods under Python 2.\n    Under Python 3 it does nothing.\n\n    To support Python 2 and 3 with a single code base, define a __str__ method\n    returning text and apply this decorator to the class.\n    \"\"\"\n    if PY2:\n        if '__str__' not in klass.__dict__:\n            raise ValueError(\"@python_2_unicode_compatible cannot be applied \"\n                             \"to %s because it doesn't define __str__().\" %\n                             klass.__name__)\n        klass.__unicode__ = klass.__str__\n        klass.__str__ = lambda self: self.__unicode__().encode('utf-8')\n    return klass\n\n\n# Complete the moves implementation.\n# This code is at the end of this module to speed up module loading.\n# Turn this module into a package.\n__path__ = []  # required for PEP 302 and PEP 451\n__package__ = __name__  # see PEP 366 @ReservedAssignment\nif globals().get(\"__spec__\") is not None:\n    __spec__.submodule_search_locations = []  # PEP 451 @UndefinedVariable\n# Remove other six meta path importers, since they cause problems. This can\n# happen if six is removed from sys.modules and then reloaded. (Setuptools does\n# this for some reason.)\nif sys.meta_path:\n    for i, importer in enumerate(sys.meta_path):\n        # Here's some real nastiness: Another \"instance\" of the six module might\n        # be floating around. Therefore, we can't use isinstance() to check for\n        # the six meta path importer, since the other six instance will have\n        # inserted an importer with different class.\n        if (type(importer).__name__ == \"_SixMetaPathImporter\" and\n                importer.name == __name__):\n            del sys.meta_path[i]\n            break\n    del i, importer\n# Finally, add the importer to the meta path import hook.\nsys.meta_path.append(_importer)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/archive_util.py",
    "content": "\"\"\"Utilities for extracting common archive formats\"\"\"\n\nimport zipfile\nimport tarfile\nimport os\nimport shutil\nimport posixpath\nimport contextlib\nfrom distutils.errors import DistutilsError\n\nfrom pkg_resources import ensure_directory\n\n__all__ = [\n    \"unpack_archive\", \"unpack_zipfile\", \"unpack_tarfile\", \"default_filter\",\n    \"UnrecognizedFormat\", \"extraction_drivers\", \"unpack_directory\",\n]\n\n\nclass UnrecognizedFormat(DistutilsError):\n    \"\"\"Couldn't recognize the archive type\"\"\"\n\n\ndef default_filter(src, dst):\n    \"\"\"The default progress/filter callback; returns True for all files\"\"\"\n    return dst\n\n\ndef unpack_archive(filename, extract_dir, progress_filter=default_filter,\n        drivers=None):\n    \"\"\"Unpack `filename` to `extract_dir`, or raise ``UnrecognizedFormat``\n\n    `progress_filter` is a function taking two arguments: a source path\n    internal to the archive ('/'-separated), and a filesystem path where it\n    will be extracted.  The callback must return the desired extract path\n    (which may be the same as the one passed in), or else ``None`` to skip\n    that file or directory.  The callback can thus be used to report on the\n    progress of the extraction, as well as to filter the items extracted or\n    alter their extraction paths.\n\n    `drivers`, if supplied, must be a non-empty sequence of functions with the\n    same signature as this function (minus the `drivers` argument), that raise\n    ``UnrecognizedFormat`` if they do not support extracting the designated\n    archive type.  The `drivers` are tried in sequence until one is found that\n    does not raise an error, or until all are exhausted (in which case\n    ``UnrecognizedFormat`` is raised).  If you do not supply a sequence of\n    drivers, the module's ``extraction_drivers`` constant will be used, which\n    means that ``unpack_zipfile`` and ``unpack_tarfile`` will be tried, in that\n    order.\n    \"\"\"\n    for driver in drivers or extraction_drivers:\n        try:\n            driver(filename, extract_dir, progress_filter)\n        except UnrecognizedFormat:\n            continue\n        else:\n            return\n    else:\n        raise UnrecognizedFormat(\n            \"Not a recognized archive type: %s\" % filename\n        )\n\n\ndef unpack_directory(filename, extract_dir, progress_filter=default_filter):\n    \"\"\"\"Unpack\" a directory, using the same interface as for archives\n\n    Raises ``UnrecognizedFormat`` if `filename` is not a directory\n    \"\"\"\n    if not os.path.isdir(filename):\n        raise UnrecognizedFormat(\"%s is not a directory\" % filename)\n\n    paths = {\n        filename: ('', extract_dir),\n    }\n    for base, dirs, files in os.walk(filename):\n        src, dst = paths[base]\n        for d in dirs:\n            paths[os.path.join(base, d)] = src + d + '/', os.path.join(dst, d)\n        for f in files:\n            target = os.path.join(dst, f)\n            target = progress_filter(src + f, target)\n            if not target:\n                # skip non-files\n                continue\n            ensure_directory(target)\n            f = os.path.join(base, f)\n            shutil.copyfile(f, target)\n            shutil.copystat(f, target)\n\n\ndef unpack_zipfile(filename, extract_dir, progress_filter=default_filter):\n    \"\"\"Unpack zip `filename` to `extract_dir`\n\n    Raises ``UnrecognizedFormat`` if `filename` is not a zipfile (as determined\n    by ``zipfile.is_zipfile()``).  See ``unpack_archive()`` for an explanation\n    of the `progress_filter` argument.\n    \"\"\"\n\n    if not zipfile.is_zipfile(filename):\n        raise UnrecognizedFormat(\"%s is not a zip file\" % (filename,))\n\n    with zipfile.ZipFile(filename) as z:\n        for info in z.infolist():\n            name = info.filename\n\n            # don't extract absolute paths or ones with .. in them\n            if name.startswith('/') or '..' in name.split('/'):\n                continue\n\n            target = os.path.join(extract_dir, *name.split('/'))\n            target = progress_filter(name, target)\n            if not target:\n                continue\n            if name.endswith('/'):\n                # directory\n                ensure_directory(target)\n            else:\n                # file\n                ensure_directory(target)\n                data = z.read(info.filename)\n                with open(target, 'wb') as f:\n                    f.write(data)\n            unix_attributes = info.external_attr >> 16\n            if unix_attributes:\n                os.chmod(target, unix_attributes)\n\n\ndef unpack_tarfile(filename, extract_dir, progress_filter=default_filter):\n    \"\"\"Unpack tar/tar.gz/tar.bz2 `filename` to `extract_dir`\n\n    Raises ``UnrecognizedFormat`` if `filename` is not a tarfile (as determined\n    by ``tarfile.open()``).  See ``unpack_archive()`` for an explanation\n    of the `progress_filter` argument.\n    \"\"\"\n    try:\n        tarobj = tarfile.open(filename)\n    except tarfile.TarError:\n        raise UnrecognizedFormat(\n            \"%s is not a compressed or uncompressed tar file\" % (filename,)\n        )\n    with contextlib.closing(tarobj):\n        # don't do any chowning!\n        tarobj.chown = lambda *args: None\n        for member in tarobj:\n            name = member.name\n            # don't extract absolute paths or ones with .. in them\n            if not name.startswith('/') and '..' not in name.split('/'):\n                prelim_dst = os.path.join(extract_dir, *name.split('/'))\n\n                # resolve any links and to extract the link targets as normal\n                # files\n                while member is not None and (member.islnk() or member.issym()):\n                    linkpath = member.linkname\n                    if member.issym():\n                        base = posixpath.dirname(member.name)\n                        linkpath = posixpath.join(base, linkpath)\n                        linkpath = posixpath.normpath(linkpath)\n                    member = tarobj._getmember(linkpath)\n\n                if member is not None and (member.isfile() or member.isdir()):\n                    final_dst = progress_filter(name, prelim_dst)\n                    if final_dst:\n                        if final_dst.endswith(os.sep):\n                            final_dst = final_dst[:-1]\n                        try:\n                            # XXX Ugh\n                            tarobj._extract_member(member, final_dst)\n                        except tarfile.ExtractError:\n                            # chown/chmod/mkfifo/mknode/makedev failed\n                            pass\n        return True\n\n\nextraction_drivers = unpack_directory, unpack_zipfile, unpack_tarfile\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/build_meta.py",
    "content": "\"\"\"A PEP 517 interface to setuptools\n\nPreviously, when a user or a command line tool (let's call it a \"frontend\")\nneeded to make a request of setuptools to take a certain action, for\nexample, generating a list of installation requirements, the frontend would\nwould call \"setup.py egg_info\" or \"setup.py bdist_wheel\" on the command line.\n\nPEP 517 defines a different method of interfacing with setuptools. Rather\nthan calling \"setup.py\" directly, the frontend should:\n\n  1. Set the current directory to the directory with a setup.py file\n  2. Import this module into a safe python interpreter (one in which\n     setuptools can potentially set global variables or crash hard).\n  3. Call one of the functions defined in PEP 517.\n\nWhat each function does is defined in PEP 517. However, here is a \"casual\"\ndefinition of the functions (this definition should not be relied on for\nbug reports or API stability):\n\n  - `build_wheel`: build a wheel in the folder and return the basename\n  - `get_requires_for_build_wheel`: get the `setup_requires` to build\n  - `prepare_metadata_for_build_wheel`: get the `install_requires`\n  - `build_sdist`: build an sdist in the folder and return the basename\n  - `get_requires_for_build_sdist`: get the `setup_requires` to build\n\nAgain, this is not a formal definition! Just a \"taste\" of the module.\n\"\"\"\n\nimport io\nimport os\nimport sys\nimport tokenize\nimport shutil\nimport contextlib\n\nimport setuptools\nimport distutils\nfrom setuptools.py31compat import TemporaryDirectory\n\nfrom pkg_resources import parse_requirements\nfrom pkg_resources.py31compat import makedirs\n\n__all__ = ['get_requires_for_build_sdist',\n           'get_requires_for_build_wheel',\n           'prepare_metadata_for_build_wheel',\n           'build_wheel',\n           'build_sdist',\n           '__legacy__',\n           'SetupRequirementsError']\n\nclass SetupRequirementsError(BaseException):\n    def __init__(self, specifiers):\n        self.specifiers = specifiers\n\n\nclass Distribution(setuptools.dist.Distribution):\n    def fetch_build_eggs(self, specifiers):\n        specifier_list = list(map(str, parse_requirements(specifiers)))\n\n        raise SetupRequirementsError(specifier_list)\n\n    @classmethod\n    @contextlib.contextmanager\n    def patch(cls):\n        \"\"\"\n        Replace\n        distutils.dist.Distribution with this class\n        for the duration of this context.\n        \"\"\"\n        orig = distutils.core.Distribution\n        distutils.core.Distribution = cls\n        try:\n            yield\n        finally:\n            distutils.core.Distribution = orig\n\n\ndef _to_str(s):\n    \"\"\"\n    Convert a filename to a string (on Python 2, explicitly\n    a byte string, not Unicode) as distutils checks for the\n    exact type str.\n    \"\"\"\n    if sys.version_info[0] == 2 and not isinstance(s, str):\n        # Assume it's Unicode, as that's what the PEP says\n        # should be provided.\n        return s.encode(sys.getfilesystemencoding())\n    return s\n\n\ndef _get_immediate_subdirectories(a_dir):\n    return [name for name in os.listdir(a_dir)\n            if os.path.isdir(os.path.join(a_dir, name))]\n\n\ndef _file_with_extension(directory, extension):\n    matching = (\n        f for f in os.listdir(directory)\n        if f.endswith(extension)\n    )\n    file, = matching\n    return file\n\n\ndef _open_setup_script(setup_script):\n    if not os.path.exists(setup_script):\n        # Supply a default setup.py\n        return io.StringIO(u\"from setuptools import setup; setup()\")\n\n    return getattr(tokenize, 'open', open)(setup_script)\n\n\nclass _BuildMetaBackend(object):\n\n    def _fix_config(self, config_settings):\n        config_settings = config_settings or {}\n        config_settings.setdefault('--global-option', [])\n        return config_settings\n\n    def _get_build_requires(self, config_settings, requirements):\n        config_settings = self._fix_config(config_settings)\n\n        sys.argv = sys.argv[:1] + ['egg_info'] + \\\n            config_settings[\"--global-option\"]\n        try:\n            with Distribution.patch():\n                self.run_setup()\n        except SetupRequirementsError as e:\n            requirements += e.specifiers\n\n        return requirements\n\n    def run_setup(self, setup_script='setup.py'):\n        # Note that we can reuse our build directory between calls\n        # Correctness comes first, then optimization later\n        __file__ = setup_script\n        __name__ = '__main__'\n\n        with _open_setup_script(__file__) as f:\n            code = f.read().replace(r'\\r\\n', r'\\n')\n\n        exec(compile(code, __file__, 'exec'), locals())\n\n    def get_requires_for_build_wheel(self, config_settings=None):\n        config_settings = self._fix_config(config_settings)\n        return self._get_build_requires(config_settings, requirements=['wheel'])\n\n    def get_requires_for_build_sdist(self, config_settings=None):\n        config_settings = self._fix_config(config_settings)\n        return self._get_build_requires(config_settings, requirements=[])\n\n    def prepare_metadata_for_build_wheel(self, metadata_directory,\n                                         config_settings=None):\n        sys.argv = sys.argv[:1] + ['dist_info', '--egg-base',\n                                   _to_str(metadata_directory)]\n        self.run_setup()\n\n        dist_info_directory = metadata_directory\n        while True:\n            dist_infos = [f for f in os.listdir(dist_info_directory)\n                          if f.endswith('.dist-info')]\n\n            if (len(dist_infos) == 0 and\n                len(_get_immediate_subdirectories(dist_info_directory)) == 1):\n\n                dist_info_directory = os.path.join(\n                    dist_info_directory, os.listdir(dist_info_directory)[0])\n                continue\n\n            assert len(dist_infos) == 1\n            break\n\n        # PEP 517 requires that the .dist-info directory be placed in the\n        # metadata_directory. To comply, we MUST copy the directory to the root\n        if dist_info_directory != metadata_directory:\n            shutil.move(\n                os.path.join(dist_info_directory, dist_infos[0]),\n                metadata_directory)\n            shutil.rmtree(dist_info_directory, ignore_errors=True)\n\n        return dist_infos[0]\n\n    def _build_with_temp_dir(self, setup_command, result_extension,\n                             result_directory, config_settings):\n        config_settings = self._fix_config(config_settings)\n        result_directory = os.path.abspath(result_directory)\n\n        # Build in a temporary directory, then copy to the target.\n        makedirs(result_directory, exist_ok=True)\n        with TemporaryDirectory(dir=result_directory) as tmp_dist_dir:\n            sys.argv = (sys.argv[:1] + setup_command +\n                        ['--dist-dir', tmp_dist_dir] +\n                        config_settings[\"--global-option\"])\n            self.run_setup()\n\n            result_basename = _file_with_extension(tmp_dist_dir, result_extension)\n            result_path = os.path.join(result_directory, result_basename)\n            if os.path.exists(result_path):\n                # os.rename will fail overwriting on non-Unix.\n                os.remove(result_path)\n            os.rename(os.path.join(tmp_dist_dir, result_basename), result_path)\n\n        return result_basename\n\n\n    def build_wheel(self, wheel_directory, config_settings=None,\n                    metadata_directory=None):\n        return self._build_with_temp_dir(['bdist_wheel'], '.whl',\n                                         wheel_directory, config_settings)\n\n    def build_sdist(self, sdist_directory, config_settings=None):\n        return self._build_with_temp_dir(['sdist', '--formats', 'gztar'],\n                                         '.tar.gz', sdist_directory,\n                                         config_settings)\n\n\nclass _BuildMetaLegacyBackend(_BuildMetaBackend):\n    \"\"\"Compatibility backend for setuptools\n\n    This is a version of setuptools.build_meta that endeavors to maintain backwards\n    compatibility with pre-PEP 517 modes of invocation. It exists as a temporary\n    bridge between the old packaging mechanism and the new packaging mechanism,\n    and will eventually be removed.\n    \"\"\"\n    def run_setup(self, setup_script='setup.py'):\n        # In order to maintain compatibility with scripts assuming that\n        # the setup.py script is in a directory on the PYTHONPATH, inject\n        # '' into sys.path. (pypa/setuptools#1642)\n        sys_path = list(sys.path)           # Save the original path\n\n        script_dir = os.path.dirname(os.path.abspath(setup_script))\n        if script_dir not in sys.path:\n            sys.path.insert(0, script_dir)\n\n        try:\n            super(_BuildMetaLegacyBackend,\n                  self).run_setup(setup_script=setup_script)\n        finally:\n            # While PEP 517 frontends should be calling each hook in a fresh\n            # subprocess according to the standard (and thus it should not be\n            # strictly necessary to restore the old sys.path), we'll restore\n            # the original path so that the path manipulation does not persist\n            # within the hook after run_setup is called.\n            sys.path[:] = sys_path\n\n# The primary backend\n_BACKEND = _BuildMetaBackend()\n\nget_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel\nget_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist\nprepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel\nbuild_wheel = _BACKEND.build_wheel\nbuild_sdist = _BACKEND.build_sdist\n\n\n# The legacy backend\n__legacy__ = _BuildMetaLegacyBackend()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/__init__.py",
    "content": "__all__ = [\n    'alias', 'bdist_egg', 'bdist_rpm', 'build_ext', 'build_py', 'develop',\n    'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts',\n    'sdist', 'setopt', 'test', 'install_egg_info', 'install_scripts',\n    'bdist_wininst', 'upload_docs', 'build_clib', 'dist_info',\n]\n\nfrom distutils.command.bdist import bdist\nimport sys\n\nfrom setuptools.command import install_scripts\n\nif 'egg' not in bdist.format_commands:\n    bdist.format_command['egg'] = ('bdist_egg', \"Python .egg file\")\n    bdist.format_commands.append('egg')\n\ndel bdist, sys\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/alias.py",
    "content": "from distutils.errors import DistutilsOptionError\n\nfrom setuptools.extern.six.moves import map\n\nfrom setuptools.command.setopt import edit_config, option_base, config_file\n\n\ndef shquote(arg):\n    \"\"\"Quote an argument for later parsing by shlex.split()\"\"\"\n    for c in '\"', \"'\", \"\\\\\", \"#\":\n        if c in arg:\n            return repr(arg)\n    if arg.split() != [arg]:\n        return repr(arg)\n    return arg\n\n\nclass alias(option_base):\n    \"\"\"Define a shortcut that invokes one or more commands\"\"\"\n\n    description = \"define a shortcut to invoke one or more commands\"\n    command_consumes_arguments = True\n\n    user_options = [\n        ('remove', 'r', 'remove (unset) the alias'),\n    ] + option_base.user_options\n\n    boolean_options = option_base.boolean_options + ['remove']\n\n    def initialize_options(self):\n        option_base.initialize_options(self)\n        self.args = None\n        self.remove = None\n\n    def finalize_options(self):\n        option_base.finalize_options(self)\n        if self.remove and len(self.args) != 1:\n            raise DistutilsOptionError(\n                \"Must specify exactly one argument (the alias name) when \"\n                \"using --remove\"\n            )\n\n    def run(self):\n        aliases = self.distribution.get_option_dict('aliases')\n\n        if not self.args:\n            print(\"Command Aliases\")\n            print(\"---------------\")\n            for alias in aliases:\n                print(\"setup.py alias\", format_alias(alias, aliases))\n            return\n\n        elif len(self.args) == 1:\n            alias, = self.args\n            if self.remove:\n                command = None\n            elif alias in aliases:\n                print(\"setup.py alias\", format_alias(alias, aliases))\n                return\n            else:\n                print(\"No alias definition found for %r\" % alias)\n                return\n        else:\n            alias = self.args[0]\n            command = ' '.join(map(shquote, self.args[1:]))\n\n        edit_config(self.filename, {'aliases': {alias: command}}, self.dry_run)\n\n\ndef format_alias(name, aliases):\n    source, command = aliases[name]\n    if source == config_file('global'):\n        source = '--global-config '\n    elif source == config_file('user'):\n        source = '--user-config '\n    elif source == config_file('local'):\n        source = ''\n    else:\n        source = '--filename=%r' % source\n    return source + name + ' ' + command\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/bdist_egg.py",
    "content": "\"\"\"setuptools.command.bdist_egg\n\nBuild .egg distributions\"\"\"\n\nfrom distutils.errors import DistutilsSetupError\nfrom distutils.dir_util import remove_tree, mkpath\nfrom distutils import log\nfrom types import CodeType\nimport sys\nimport os\nimport re\nimport textwrap\nimport marshal\n\nfrom setuptools.extern import six\n\nfrom pkg_resources import get_build_platform, Distribution, ensure_directory\nfrom pkg_resources import EntryPoint\nfrom setuptools.extension import Library\nfrom setuptools import Command\n\ntry:\n    # Python 2.7 or >=3.2\n    from sysconfig import get_path, get_python_version\n\n    def _get_purelib():\n        return get_path(\"purelib\")\nexcept ImportError:\n    from distutils.sysconfig import get_python_lib, get_python_version\n\n    def _get_purelib():\n        return get_python_lib(False)\n\n\ndef strip_module(filename):\n    if '.' in filename:\n        filename = os.path.splitext(filename)[0]\n    if filename.endswith('module'):\n        filename = filename[:-6]\n    return filename\n\n\ndef sorted_walk(dir):\n    \"\"\"Do os.walk in a reproducible way,\n    independent of indeterministic filesystem readdir order\n    \"\"\"\n    for base, dirs, files in os.walk(dir):\n        dirs.sort()\n        files.sort()\n        yield base, dirs, files\n\n\ndef write_stub(resource, pyfile):\n    _stub_template = textwrap.dedent(\"\"\"\n        def __bootstrap__():\n            global __bootstrap__, __loader__, __file__\n            import sys, pkg_resources, imp\n            __file__ = pkg_resources.resource_filename(__name__, %r)\n            __loader__ = None; del __bootstrap__, __loader__\n            imp.load_dynamic(__name__,__file__)\n        __bootstrap__()\n        \"\"\").lstrip()\n    with open(pyfile, 'w') as f:\n        f.write(_stub_template % resource)\n\n\nclass bdist_egg(Command):\n    description = \"create an \\\"egg\\\" distribution\"\n\n    user_options = [\n        ('bdist-dir=', 'b',\n         \"temporary directory for creating the distribution\"),\n        ('plat-name=', 'p', \"platform name to embed in generated filenames \"\n                            \"(default: %s)\" % get_build_platform()),\n        ('exclude-source-files', None,\n         \"remove all .py files from the generated egg\"),\n        ('keep-temp', 'k',\n         \"keep the pseudo-installation tree around after \" +\n         \"creating the distribution archive\"),\n        ('dist-dir=', 'd',\n         \"directory to put final built distributions in\"),\n        ('skip-build', None,\n         \"skip rebuilding everything (for testing/debugging)\"),\n    ]\n\n    boolean_options = [\n        'keep-temp', 'skip-build', 'exclude-source-files'\n    ]\n\n    def initialize_options(self):\n        self.bdist_dir = None\n        self.plat_name = None\n        self.keep_temp = 0\n        self.dist_dir = None\n        self.skip_build = 0\n        self.egg_output = None\n        self.exclude_source_files = None\n\n    def finalize_options(self):\n        ei_cmd = self.ei_cmd = self.get_finalized_command(\"egg_info\")\n        self.egg_info = ei_cmd.egg_info\n\n        if self.bdist_dir is None:\n            bdist_base = self.get_finalized_command('bdist').bdist_base\n            self.bdist_dir = os.path.join(bdist_base, 'egg')\n\n        if self.plat_name is None:\n            self.plat_name = get_build_platform()\n\n        self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))\n\n        if self.egg_output is None:\n\n            # Compute filename of the output egg\n            basename = Distribution(\n                None, None, ei_cmd.egg_name, ei_cmd.egg_version,\n                get_python_version(),\n                self.distribution.has_ext_modules() and self.plat_name\n            ).egg_name()\n\n            self.egg_output = os.path.join(self.dist_dir, basename + '.egg')\n\n    def do_install_data(self):\n        # Hack for packages that install data to install's --install-lib\n        self.get_finalized_command('install').install_lib = self.bdist_dir\n\n        site_packages = os.path.normcase(os.path.realpath(_get_purelib()))\n        old, self.distribution.data_files = self.distribution.data_files, []\n\n        for item in old:\n            if isinstance(item, tuple) and len(item) == 2:\n                if os.path.isabs(item[0]):\n                    realpath = os.path.realpath(item[0])\n                    normalized = os.path.normcase(realpath)\n                    if normalized == site_packages or normalized.startswith(\n                        site_packages + os.sep\n                    ):\n                        item = realpath[len(site_packages) + 1:], item[1]\n                        # XXX else: raise ???\n            self.distribution.data_files.append(item)\n\n        try:\n            log.info(\"installing package data to %s\", self.bdist_dir)\n            self.call_command('install_data', force=0, root=None)\n        finally:\n            self.distribution.data_files = old\n\n    def get_outputs(self):\n        return [self.egg_output]\n\n    def call_command(self, cmdname, **kw):\n        \"\"\"Invoke reinitialized command `cmdname` with keyword args\"\"\"\n        for dirname in INSTALL_DIRECTORY_ATTRS:\n            kw.setdefault(dirname, self.bdist_dir)\n        kw.setdefault('skip_build', self.skip_build)\n        kw.setdefault('dry_run', self.dry_run)\n        cmd = self.reinitialize_command(cmdname, **kw)\n        self.run_command(cmdname)\n        return cmd\n\n    def run(self):\n        # Generate metadata first\n        self.run_command(\"egg_info\")\n        # We run install_lib before install_data, because some data hacks\n        # pull their data path from the install_lib command.\n        log.info(\"installing library code to %s\", self.bdist_dir)\n        instcmd = self.get_finalized_command('install')\n        old_root = instcmd.root\n        instcmd.root = None\n        if self.distribution.has_c_libraries() and not self.skip_build:\n            self.run_command('build_clib')\n        cmd = self.call_command('install_lib', warn_dir=0)\n        instcmd.root = old_root\n\n        all_outputs, ext_outputs = self.get_ext_outputs()\n        self.stubs = []\n        to_compile = []\n        for (p, ext_name) in enumerate(ext_outputs):\n            filename, ext = os.path.splitext(ext_name)\n            pyfile = os.path.join(self.bdist_dir, strip_module(filename) +\n                                  '.py')\n            self.stubs.append(pyfile)\n            log.info(\"creating stub loader for %s\", ext_name)\n            if not self.dry_run:\n                write_stub(os.path.basename(ext_name), pyfile)\n            to_compile.append(pyfile)\n            ext_outputs[p] = ext_name.replace(os.sep, '/')\n\n        if to_compile:\n            cmd.byte_compile(to_compile)\n        if self.distribution.data_files:\n            self.do_install_data()\n\n        # Make the EGG-INFO directory\n        archive_root = self.bdist_dir\n        egg_info = os.path.join(archive_root, 'EGG-INFO')\n        self.mkpath(egg_info)\n        if self.distribution.scripts:\n            script_dir = os.path.join(egg_info, 'scripts')\n            log.info(\"installing scripts to %s\", script_dir)\n            self.call_command('install_scripts', install_dir=script_dir,\n                              no_ep=1)\n\n        self.copy_metadata_to(egg_info)\n        native_libs = os.path.join(egg_info, \"native_libs.txt\")\n        if all_outputs:\n            log.info(\"writing %s\", native_libs)\n            if not self.dry_run:\n                ensure_directory(native_libs)\n                libs_file = open(native_libs, 'wt')\n                libs_file.write('\\n'.join(all_outputs))\n                libs_file.write('\\n')\n                libs_file.close()\n        elif os.path.isfile(native_libs):\n            log.info(\"removing %s\", native_libs)\n            if not self.dry_run:\n                os.unlink(native_libs)\n\n        write_safety_flag(\n            os.path.join(archive_root, 'EGG-INFO'), self.zip_safe()\n        )\n\n        if os.path.exists(os.path.join(self.egg_info, 'depends.txt')):\n            log.warn(\n                \"WARNING: 'depends.txt' will not be used by setuptools 0.6!\\n\"\n                \"Use the install_requires/extras_require setup() args instead.\"\n            )\n\n        if self.exclude_source_files:\n            self.zap_pyfiles()\n\n        # Make the archive\n        make_zipfile(self.egg_output, archive_root, verbose=self.verbose,\n                     dry_run=self.dry_run, mode=self.gen_header())\n        if not self.keep_temp:\n            remove_tree(self.bdist_dir, dry_run=self.dry_run)\n\n        # Add to 'Distribution.dist_files' so that the \"upload\" command works\n        getattr(self.distribution, 'dist_files', []).append(\n            ('bdist_egg', get_python_version(), self.egg_output))\n\n    def zap_pyfiles(self):\n        log.info(\"Removing .py files from temporary directory\")\n        for base, dirs, files in walk_egg(self.bdist_dir):\n            for name in files:\n                path = os.path.join(base, name)\n\n                if name.endswith('.py'):\n                    log.debug(\"Deleting %s\", path)\n                    os.unlink(path)\n\n                if base.endswith('__pycache__'):\n                    path_old = path\n\n                    pattern = r'(?P<name>.+)\\.(?P<magic>[^.]+)\\.pyc'\n                    m = re.match(pattern, name)\n                    path_new = os.path.join(\n                        base, os.pardir, m.group('name') + '.pyc')\n                    log.info(\n                        \"Renaming file from [%s] to [%s]\"\n                        % (path_old, path_new))\n                    try:\n                        os.remove(path_new)\n                    except OSError:\n                        pass\n                    os.rename(path_old, path_new)\n\n    def zip_safe(self):\n        safe = getattr(self.distribution, 'zip_safe', None)\n        if safe is not None:\n            return safe\n        log.warn(\"zip_safe flag not set; analyzing archive contents...\")\n        return analyze_egg(self.bdist_dir, self.stubs)\n\n    def gen_header(self):\n        epm = EntryPoint.parse_map(self.distribution.entry_points or '')\n        ep = epm.get('setuptools.installation', {}).get('eggsecutable')\n        if ep is None:\n            return 'w'  # not an eggsecutable, do it the usual way.\n\n        if not ep.attrs or ep.extras:\n            raise DistutilsSetupError(\n                \"eggsecutable entry point (%r) cannot have 'extras' \"\n                \"or refer to a module\" % (ep,)\n            )\n\n        pyver = '{}.{}'.format(*sys.version_info)\n        pkg = ep.module_name\n        full = '.'.join(ep.attrs)\n        base = ep.attrs[0]\n        basename = os.path.basename(self.egg_output)\n\n        header = (\n            \"#!/bin/sh\\n\"\n            'if [ `basename $0` = \"%(basename)s\" ]\\n'\n            'then exec python%(pyver)s -c \"'\n            \"import sys, os; sys.path.insert(0, os.path.abspath('$0')); \"\n            \"from %(pkg)s import %(base)s; sys.exit(%(full)s())\"\n            '\" \"$@\"\\n'\n            'else\\n'\n            '  echo $0 is not the correct name for this egg file.\\n'\n            '  echo Please rename it back to %(basename)s and try again.\\n'\n            '  exec false\\n'\n            'fi\\n'\n        ) % locals()\n\n        if not self.dry_run:\n            mkpath(os.path.dirname(self.egg_output), dry_run=self.dry_run)\n            f = open(self.egg_output, 'w')\n            f.write(header)\n            f.close()\n        return 'a'\n\n    def copy_metadata_to(self, target_dir):\n        \"Copy metadata (egg info) to the target_dir\"\n        # normalize the path (so that a forward-slash in egg_info will\n        # match using startswith below)\n        norm_egg_info = os.path.normpath(self.egg_info)\n        prefix = os.path.join(norm_egg_info, '')\n        for path in self.ei_cmd.filelist.files:\n            if path.startswith(prefix):\n                target = os.path.join(target_dir, path[len(prefix):])\n                ensure_directory(target)\n                self.copy_file(path, target)\n\n    def get_ext_outputs(self):\n        \"\"\"Get a list of relative paths to C extensions in the output distro\"\"\"\n\n        all_outputs = []\n        ext_outputs = []\n\n        paths = {self.bdist_dir: ''}\n        for base, dirs, files in sorted_walk(self.bdist_dir):\n            for filename in files:\n                if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS:\n                    all_outputs.append(paths[base] + filename)\n            for filename in dirs:\n                paths[os.path.join(base, filename)] = (paths[base] +\n                                                       filename + '/')\n\n        if self.distribution.has_ext_modules():\n            build_cmd = self.get_finalized_command('build_ext')\n            for ext in build_cmd.extensions:\n                if isinstance(ext, Library):\n                    continue\n                fullname = build_cmd.get_ext_fullname(ext.name)\n                filename = build_cmd.get_ext_filename(fullname)\n                if not os.path.basename(filename).startswith('dl-'):\n                    if os.path.exists(os.path.join(self.bdist_dir, filename)):\n                        ext_outputs.append(filename)\n\n        return all_outputs, ext_outputs\n\n\nNATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split())\n\n\ndef walk_egg(egg_dir):\n    \"\"\"Walk an unpacked egg's contents, skipping the metadata directory\"\"\"\n    walker = sorted_walk(egg_dir)\n    base, dirs, files = next(walker)\n    if 'EGG-INFO' in dirs:\n        dirs.remove('EGG-INFO')\n    yield base, dirs, files\n    for bdf in walker:\n        yield bdf\n\n\ndef analyze_egg(egg_dir, stubs):\n    # check for existing flag in EGG-INFO\n    for flag, fn in safety_flags.items():\n        if os.path.exists(os.path.join(egg_dir, 'EGG-INFO', fn)):\n            return flag\n    if not can_scan():\n        return False\n    safe = True\n    for base, dirs, files in walk_egg(egg_dir):\n        for name in files:\n            if name.endswith('.py') or name.endswith('.pyw'):\n                continue\n            elif name.endswith('.pyc') or name.endswith('.pyo'):\n                # always scan, even if we already know we're not safe\n                safe = scan_module(egg_dir, base, name, stubs) and safe\n    return safe\n\n\ndef write_safety_flag(egg_dir, safe):\n    # Write or remove zip safety flag file(s)\n    for flag, fn in safety_flags.items():\n        fn = os.path.join(egg_dir, fn)\n        if os.path.exists(fn):\n            if safe is None or bool(safe) != flag:\n                os.unlink(fn)\n        elif safe is not None and bool(safe) == flag:\n            f = open(fn, 'wt')\n            f.write('\\n')\n            f.close()\n\n\nsafety_flags = {\n    True: 'zip-safe',\n    False: 'not-zip-safe',\n}\n\n\ndef scan_module(egg_dir, base, name, stubs):\n    \"\"\"Check whether module possibly uses unsafe-for-zipfile stuff\"\"\"\n\n    filename = os.path.join(base, name)\n    if filename[:-1] in stubs:\n        return True  # Extension module\n    pkg = base[len(egg_dir) + 1:].replace(os.sep, '.')\n    module = pkg + (pkg and '.' or '') + os.path.splitext(name)[0]\n    if six.PY2:\n        skip = 8  # skip magic & date\n    elif sys.version_info < (3, 7):\n        skip = 12  # skip magic & date & file size\n    else:\n        skip = 16  # skip magic & reserved? & date & file size\n    f = open(filename, 'rb')\n    f.read(skip)\n    code = marshal.load(f)\n    f.close()\n    safe = True\n    symbols = dict.fromkeys(iter_symbols(code))\n    for bad in ['__file__', '__path__']:\n        if bad in symbols:\n            log.warn(\"%s: module references %s\", module, bad)\n            safe = False\n    if 'inspect' in symbols:\n        for bad in [\n            'getsource', 'getabsfile', 'getsourcefile', 'getfile'\n            'getsourcelines', 'findsource', 'getcomments', 'getframeinfo',\n            'getinnerframes', 'getouterframes', 'stack', 'trace'\n        ]:\n            if bad in symbols:\n                log.warn(\"%s: module MAY be using inspect.%s\", module, bad)\n                safe = False\n    return safe\n\n\ndef iter_symbols(code):\n    \"\"\"Yield names and strings used by `code` and its nested code objects\"\"\"\n    for name in code.co_names:\n        yield name\n    for const in code.co_consts:\n        if isinstance(const, six.string_types):\n            yield const\n        elif isinstance(const, CodeType):\n            for name in iter_symbols(const):\n                yield name\n\n\ndef can_scan():\n    if not sys.platform.startswith('java') and sys.platform != 'cli':\n        # CPython, PyPy, etc.\n        return True\n    log.warn(\"Unable to analyze compiled code on this platform.\")\n    log.warn(\"Please ask the author to include a 'zip_safe'\"\n             \" setting (either True or False) in the package's setup.py\")\n\n\n# Attribute names of options for commands that might need to be convinced to\n# install to the egg build directory\n\nINSTALL_DIRECTORY_ATTRS = [\n    'install_lib', 'install_dir', 'install_data', 'install_base'\n]\n\n\ndef make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True,\n                 mode='w'):\n    \"\"\"Create a zip file from all the files under 'base_dir'.  The output\n    zip file will be named 'base_dir' + \".zip\".  Uses either the \"zipfile\"\n    Python module (if available) or the InfoZIP \"zip\" utility (if installed\n    and found on the default search path).  If neither tool is available,\n    raises DistutilsExecError.  Returns the name of the output zip file.\n    \"\"\"\n    import zipfile\n\n    mkpath(os.path.dirname(zip_filename), dry_run=dry_run)\n    log.info(\"creating '%s' and adding '%s' to it\", zip_filename, base_dir)\n\n    def visit(z, dirname, names):\n        for name in names:\n            path = os.path.normpath(os.path.join(dirname, name))\n            if os.path.isfile(path):\n                p = path[len(base_dir) + 1:]\n                if not dry_run:\n                    z.write(path, p)\n                log.debug(\"adding '%s'\", p)\n\n    compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED\n    if not dry_run:\n        z = zipfile.ZipFile(zip_filename, mode, compression=compression)\n        for dirname, dirs, files in sorted_walk(base_dir):\n            visit(z, dirname, files)\n        z.close()\n    else:\n        for dirname, dirs, files in sorted_walk(base_dir):\n            visit(None, dirname, files)\n    return zip_filename\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/bdist_rpm.py",
    "content": "import distutils.command.bdist_rpm as orig\n\n\nclass bdist_rpm(orig.bdist_rpm):\n    \"\"\"\n    Override the default bdist_rpm behavior to do the following:\n\n    1. Run egg_info to ensure the name and version are properly calculated.\n    2. Always run 'install' using --single-version-externally-managed to\n       disable eggs in RPM distributions.\n    3. Replace dash with underscore in the version numbers for better RPM\n       compatibility.\n    \"\"\"\n\n    def run(self):\n        # ensure distro name is up-to-date\n        self.run_command('egg_info')\n\n        orig.bdist_rpm.run(self)\n\n    def _make_spec_file(self):\n        version = self.distribution.get_version()\n        rpmversion = version.replace('-', '_')\n        spec = orig.bdist_rpm._make_spec_file(self)\n        line23 = '%define version ' + version\n        line24 = '%define version ' + rpmversion\n        spec = [\n            line.replace(\n                \"Source0: %{name}-%{version}.tar\",\n                \"Source0: %{name}-%{unmangled_version}.tar\"\n            ).replace(\n                \"setup.py install \",\n                \"setup.py install --single-version-externally-managed \"\n            ).replace(\n                \"%setup\",\n                \"%setup -n %{name}-%{unmangled_version}\"\n            ).replace(line23, line24)\n            for line in spec\n        ]\n        insert_loc = spec.index(line24) + 1\n        unmangled_version = \"%define unmangled_version \" + version\n        spec.insert(insert_loc, unmangled_version)\n        return spec\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/bdist_wininst.py",
    "content": "import distutils.command.bdist_wininst as orig\n\n\nclass bdist_wininst(orig.bdist_wininst):\n    def reinitialize_command(self, command, reinit_subcommands=0):\n        \"\"\"\n        Supplement reinitialize_command to work around\n        http://bugs.python.org/issue20819\n        \"\"\"\n        cmd = self.distribution.reinitialize_command(\n            command, reinit_subcommands)\n        if command in ('install', 'install_lib'):\n            cmd.install_lib = None\n        return cmd\n\n    def run(self):\n        self._is_running = True\n        try:\n            orig.bdist_wininst.run(self)\n        finally:\n            self._is_running = False\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/build_clib.py",
    "content": "import distutils.command.build_clib as orig\nfrom distutils.errors import DistutilsSetupError\nfrom distutils import log\nfrom setuptools.dep_util import newer_pairwise_group\n\n\nclass build_clib(orig.build_clib):\n    \"\"\"\n    Override the default build_clib behaviour to do the following:\n\n    1. Implement a rudimentary timestamp-based dependency system\n       so 'compile()' doesn't run every time.\n    2. Add more keys to the 'build_info' dictionary:\n        * obj_deps - specify dependencies for each object compiled.\n                     this should be a dictionary mapping a key\n                     with the source filename to a list of\n                     dependencies. Use an empty string for global\n                     dependencies.\n        * cflags   - specify a list of additional flags to pass to\n                     the compiler.\n    \"\"\"\n\n    def build_libraries(self, libraries):\n        for (lib_name, build_info) in libraries:\n            sources = build_info.get('sources')\n            if sources is None or not isinstance(sources, (list, tuple)):\n                raise DistutilsSetupError(\n                       \"in 'libraries' option (library '%s'), \"\n                       \"'sources' must be present and must be \"\n                       \"a list of source filenames\" % lib_name)\n            sources = list(sources)\n\n            log.info(\"building '%s' library\", lib_name)\n\n            # Make sure everything is the correct type.\n            # obj_deps should be a dictionary of keys as sources\n            # and a list/tuple of files that are its dependencies.\n            obj_deps = build_info.get('obj_deps', dict())\n            if not isinstance(obj_deps, dict):\n                raise DistutilsSetupError(\n                       \"in 'libraries' option (library '%s'), \"\n                       \"'obj_deps' must be a dictionary of \"\n                       \"type 'source: list'\" % lib_name)\n            dependencies = []\n\n            # Get the global dependencies that are specified by the '' key.\n            # These will go into every source's dependency list.\n            global_deps = obj_deps.get('', list())\n            if not isinstance(global_deps, (list, tuple)):\n                raise DistutilsSetupError(\n                       \"in 'libraries' option (library '%s'), \"\n                       \"'obj_deps' must be a dictionary of \"\n                       \"type 'source: list'\" % lib_name)\n\n            # Build the list to be used by newer_pairwise_group\n            # each source will be auto-added to its dependencies.\n            for source in sources:\n                src_deps = [source]\n                src_deps.extend(global_deps)\n                extra_deps = obj_deps.get(source, list())\n                if not isinstance(extra_deps, (list, tuple)):\n                    raise DistutilsSetupError(\n                           \"in 'libraries' option (library '%s'), \"\n                           \"'obj_deps' must be a dictionary of \"\n                           \"type 'source: list'\" % lib_name)\n                src_deps.extend(extra_deps)\n                dependencies.append(src_deps)\n\n            expected_objects = self.compiler.object_filenames(\n                    sources,\n                    output_dir=self.build_temp\n                    )\n\n            if newer_pairwise_group(dependencies, expected_objects) != ([], []):\n                # First, compile the source code to object files in the library\n                # directory.  (This should probably change to putting object\n                # files in a temporary build directory.)\n                macros = build_info.get('macros')\n                include_dirs = build_info.get('include_dirs')\n                cflags = build_info.get('cflags')\n                objects = self.compiler.compile(\n                        sources,\n                        output_dir=self.build_temp,\n                        macros=macros,\n                        include_dirs=include_dirs,\n                        extra_postargs=cflags,\n                        debug=self.debug\n                        )\n\n            # Now \"link\" the object files together into a static library.\n            # (On Unix at least, this isn't really linking -- it just\n            # builds an archive.  Whatever.)\n            self.compiler.create_static_lib(\n                    expected_objects,\n                    lib_name,\n                    output_dir=self.build_clib,\n                    debug=self.debug\n                    )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/build_ext.py",
    "content": "import os\nimport sys\nimport itertools\nfrom distutils.command.build_ext import build_ext as _du_build_ext\nfrom distutils.file_util import copy_file\nfrom distutils.ccompiler import new_compiler\nfrom distutils.sysconfig import customize_compiler, get_config_var\nfrom distutils.errors import DistutilsError\nfrom distutils import log\n\nfrom setuptools.extension import Library\nfrom setuptools.extern import six\n\nif six.PY2:\n    import imp\n\n    EXTENSION_SUFFIXES = [s for s, _, tp in imp.get_suffixes() if tp == imp.C_EXTENSION]\nelse:\n    from importlib.machinery import EXTENSION_SUFFIXES\n\ntry:\n    # Attempt to use Cython for building extensions, if available\n    from Cython.Distutils.build_ext import build_ext as _build_ext\n    # Additionally, assert that the compiler module will load\n    # also. Ref #1229.\n    __import__('Cython.Compiler.Main')\nexcept ImportError:\n    _build_ext = _du_build_ext\n\n# make sure _config_vars is initialized\nget_config_var(\"LDSHARED\")\nfrom distutils.sysconfig import _config_vars as _CONFIG_VARS\n\n\ndef _customize_compiler_for_shlib(compiler):\n    if sys.platform == \"darwin\":\n        # building .dylib requires additional compiler flags on OSX; here we\n        # temporarily substitute the pyconfig.h variables so that distutils'\n        # 'customize_compiler' uses them before we build the shared libraries.\n        tmp = _CONFIG_VARS.copy()\n        try:\n            # XXX Help!  I don't have any idea whether these are right...\n            _CONFIG_VARS['LDSHARED'] = (\n                \"gcc -Wl,-x -dynamiclib -undefined dynamic_lookup\")\n            _CONFIG_VARS['CCSHARED'] = \" -dynamiclib\"\n            _CONFIG_VARS['SO'] = \".dylib\"\n            customize_compiler(compiler)\n        finally:\n            _CONFIG_VARS.clear()\n            _CONFIG_VARS.update(tmp)\n    else:\n        customize_compiler(compiler)\n\n\nhave_rtld = False\nuse_stubs = False\nlibtype = 'shared'\n\nif sys.platform == \"darwin\":\n    use_stubs = True\nelif os.name != 'nt':\n    try:\n        import dl\n        use_stubs = have_rtld = hasattr(dl, 'RTLD_NOW')\n    except ImportError:\n        pass\n\nif_dl = lambda s: s if have_rtld else ''\n\n\ndef get_abi3_suffix():\n    \"\"\"Return the file extension for an abi3-compliant Extension()\"\"\"\n    for suffix in EXTENSION_SUFFIXES:\n        if '.abi3' in suffix:  # Unix\n            return suffix\n        elif suffix == '.pyd':  # Windows\n            return suffix\n\n\nclass build_ext(_build_ext):\n    def run(self):\n        \"\"\"Build extensions in build directory, then copy if --inplace\"\"\"\n        old_inplace, self.inplace = self.inplace, 0\n        _build_ext.run(self)\n        self.inplace = old_inplace\n        if old_inplace:\n            self.copy_extensions_to_source()\n\n    def copy_extensions_to_source(self):\n        build_py = self.get_finalized_command('build_py')\n        for ext in self.extensions:\n            fullname = self.get_ext_fullname(ext.name)\n            filename = self.get_ext_filename(fullname)\n            modpath = fullname.split('.')\n            package = '.'.join(modpath[:-1])\n            package_dir = build_py.get_package_dir(package)\n            dest_filename = os.path.join(package_dir,\n                                         os.path.basename(filename))\n            src_filename = os.path.join(self.build_lib, filename)\n\n            # Always copy, even if source is older than destination, to ensure\n            # that the right extensions for the current Python/platform are\n            # used.\n            copy_file(\n                src_filename, dest_filename, verbose=self.verbose,\n                dry_run=self.dry_run\n            )\n            if ext._needs_stub:\n                self.write_stub(package_dir or os.curdir, ext, True)\n\n    def get_ext_filename(self, fullname):\n        filename = _build_ext.get_ext_filename(self, fullname)\n        if fullname in self.ext_map:\n            ext = self.ext_map[fullname]\n            use_abi3 = (\n                not six.PY2\n                and getattr(ext, 'py_limited_api')\n                and get_abi3_suffix()\n            )\n            if use_abi3:\n                so_ext = get_config_var('EXT_SUFFIX')\n                filename = filename[:-len(so_ext)]\n                filename = filename + get_abi3_suffix()\n            if isinstance(ext, Library):\n                fn, ext = os.path.splitext(filename)\n                return self.shlib_compiler.library_filename(fn, libtype)\n            elif use_stubs and ext._links_to_dynamic:\n                d, fn = os.path.split(filename)\n                return os.path.join(d, 'dl-' + fn)\n        return filename\n\n    def initialize_options(self):\n        _build_ext.initialize_options(self)\n        self.shlib_compiler = None\n        self.shlibs = []\n        self.ext_map = {}\n\n    def finalize_options(self):\n        _build_ext.finalize_options(self)\n        self.extensions = self.extensions or []\n        self.check_extensions_list(self.extensions)\n        self.shlibs = [ext for ext in self.extensions\n                       if isinstance(ext, Library)]\n        if self.shlibs:\n            self.setup_shlib_compiler()\n        for ext in self.extensions:\n            ext._full_name = self.get_ext_fullname(ext.name)\n        for ext in self.extensions:\n            fullname = ext._full_name\n            self.ext_map[fullname] = ext\n\n            # distutils 3.1 will also ask for module names\n            # XXX what to do with conflicts?\n            self.ext_map[fullname.split('.')[-1]] = ext\n\n            ltd = self.shlibs and self.links_to_dynamic(ext) or False\n            ns = ltd and use_stubs and not isinstance(ext, Library)\n            ext._links_to_dynamic = ltd\n            ext._needs_stub = ns\n            filename = ext._file_name = self.get_ext_filename(fullname)\n            libdir = os.path.dirname(os.path.join(self.build_lib, filename))\n            if ltd and libdir not in ext.library_dirs:\n                ext.library_dirs.append(libdir)\n            if ltd and use_stubs and os.curdir not in ext.runtime_library_dirs:\n                ext.runtime_library_dirs.append(os.curdir)\n\n    def setup_shlib_compiler(self):\n        compiler = self.shlib_compiler = new_compiler(\n            compiler=self.compiler, dry_run=self.dry_run, force=self.force\n        )\n        _customize_compiler_for_shlib(compiler)\n\n        if self.include_dirs is not None:\n            compiler.set_include_dirs(self.include_dirs)\n        if self.define is not None:\n            # 'define' option is a list of (name,value) tuples\n            for (name, value) in self.define:\n                compiler.define_macro(name, value)\n        if self.undef is not None:\n            for macro in self.undef:\n                compiler.undefine_macro(macro)\n        if self.libraries is not None:\n            compiler.set_libraries(self.libraries)\n        if self.library_dirs is not None:\n            compiler.set_library_dirs(self.library_dirs)\n        if self.rpath is not None:\n            compiler.set_runtime_library_dirs(self.rpath)\n        if self.link_objects is not None:\n            compiler.set_link_objects(self.link_objects)\n\n        # hack so distutils' build_extension() builds a library instead\n        compiler.link_shared_object = link_shared_object.__get__(compiler)\n\n    def get_export_symbols(self, ext):\n        if isinstance(ext, Library):\n            return ext.export_symbols\n        return _build_ext.get_export_symbols(self, ext)\n\n    def build_extension(self, ext):\n        ext._convert_pyx_sources_to_lang()\n        _compiler = self.compiler\n        try:\n            if isinstance(ext, Library):\n                self.compiler = self.shlib_compiler\n            _build_ext.build_extension(self, ext)\n            if ext._needs_stub:\n                cmd = self.get_finalized_command('build_py').build_lib\n                self.write_stub(cmd, ext)\n        finally:\n            self.compiler = _compiler\n\n    def links_to_dynamic(self, ext):\n        \"\"\"Return true if 'ext' links to a dynamic lib in the same package\"\"\"\n        # XXX this should check to ensure the lib is actually being built\n        # XXX as dynamic, and not just using a locally-found version or a\n        # XXX static-compiled version\n        libnames = dict.fromkeys([lib._full_name for lib in self.shlibs])\n        pkg = '.'.join(ext._full_name.split('.')[:-1] + [''])\n        return any(pkg + libname in libnames for libname in ext.libraries)\n\n    def get_outputs(self):\n        return _build_ext.get_outputs(self) + self.__get_stubs_outputs()\n\n    def __get_stubs_outputs(self):\n        # assemble the base name for each extension that needs a stub\n        ns_ext_bases = (\n            os.path.join(self.build_lib, *ext._full_name.split('.'))\n            for ext in self.extensions\n            if ext._needs_stub\n        )\n        # pair each base with the extension\n        pairs = itertools.product(ns_ext_bases, self.__get_output_extensions())\n        return list(base + fnext for base, fnext in pairs)\n\n    def __get_output_extensions(self):\n        yield '.py'\n        yield '.pyc'\n        if self.get_finalized_command('build_py').optimize:\n            yield '.pyo'\n\n    def write_stub(self, output_dir, ext, compile=False):\n        log.info(\"writing stub loader for %s to %s\", ext._full_name,\n                 output_dir)\n        stub_file = (os.path.join(output_dir, *ext._full_name.split('.')) +\n                     '.py')\n        if compile and os.path.exists(stub_file):\n            raise DistutilsError(stub_file + \" already exists! Please delete.\")\n        if not self.dry_run:\n            f = open(stub_file, 'w')\n            f.write(\n                '\\n'.join([\n                    \"def __bootstrap__():\",\n                    \"   global __bootstrap__, __file__, __loader__\",\n                    \"   import sys, os, pkg_resources, imp\" + if_dl(\", dl\"),\n                    \"   __file__ = pkg_resources.resource_filename\"\n                    \"(__name__,%r)\"\n                    % os.path.basename(ext._file_name),\n                    \"   del __bootstrap__\",\n                    \"   if '__loader__' in globals():\",\n                    \"       del __loader__\",\n                    if_dl(\"   old_flags = sys.getdlopenflags()\"),\n                    \"   old_dir = os.getcwd()\",\n                    \"   try:\",\n                    \"     os.chdir(os.path.dirname(__file__))\",\n                    if_dl(\"     sys.setdlopenflags(dl.RTLD_NOW)\"),\n                    \"     imp.load_dynamic(__name__,__file__)\",\n                    \"   finally:\",\n                    if_dl(\"     sys.setdlopenflags(old_flags)\"),\n                    \"     os.chdir(old_dir)\",\n                    \"__bootstrap__()\",\n                    \"\"  # terminal \\n\n                ])\n            )\n            f.close()\n        if compile:\n            from distutils.util import byte_compile\n\n            byte_compile([stub_file], optimize=0,\n                         force=True, dry_run=self.dry_run)\n            optimize = self.get_finalized_command('install_lib').optimize\n            if optimize > 0:\n                byte_compile([stub_file], optimize=optimize,\n                             force=True, dry_run=self.dry_run)\n            if os.path.exists(stub_file) and not self.dry_run:\n                os.unlink(stub_file)\n\n\nif use_stubs or os.name == 'nt':\n    # Build shared libraries\n    #\n    def link_shared_object(\n            self, objects, output_libname, output_dir=None, libraries=None,\n            library_dirs=None, runtime_library_dirs=None, export_symbols=None,\n            debug=0, extra_preargs=None, extra_postargs=None, build_temp=None,\n            target_lang=None):\n        self.link(\n            self.SHARED_LIBRARY, objects, output_libname,\n            output_dir, libraries, library_dirs, runtime_library_dirs,\n            export_symbols, debug, extra_preargs, extra_postargs,\n            build_temp, target_lang\n        )\nelse:\n    # Build static libraries everywhere else\n    libtype = 'static'\n\n    def link_shared_object(\n            self, objects, output_libname, output_dir=None, libraries=None,\n            library_dirs=None, runtime_library_dirs=None, export_symbols=None,\n            debug=0, extra_preargs=None, extra_postargs=None, build_temp=None,\n            target_lang=None):\n        # XXX we need to either disallow these attrs on Library instances,\n        # or warn/abort here if set, or something...\n        # libraries=None, library_dirs=None, runtime_library_dirs=None,\n        # export_symbols=None, extra_preargs=None, extra_postargs=None,\n        # build_temp=None\n\n        assert output_dir is None  # distutils build_ext doesn't pass this\n        output_dir, filename = os.path.split(output_libname)\n        basename, ext = os.path.splitext(filename)\n        if self.library_filename(\"x\").startswith('lib'):\n            # strip 'lib' prefix; this is kludgy if some platform uses\n            # a different prefix\n            basename = basename[3:]\n\n        self.create_static_lib(\n            objects, basename, output_dir, debug, target_lang\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/build_py.py",
    "content": "from glob import glob\nfrom distutils.util import convert_path\nimport distutils.command.build_py as orig\nimport os\nimport fnmatch\nimport textwrap\nimport io\nimport distutils.errors\nimport itertools\n\nfrom setuptools.extern import six\nfrom setuptools.extern.six.moves import map, filter, filterfalse\n\ntry:\n    from setuptools.lib2to3_ex import Mixin2to3\nexcept ImportError:\n\n    class Mixin2to3:\n        def run_2to3(self, files, doctests=True):\n            \"do nothing\"\n\n\nclass build_py(orig.build_py, Mixin2to3):\n    \"\"\"Enhanced 'build_py' command that includes data files with packages\n\n    The data files are specified via a 'package_data' argument to 'setup()'.\n    See 'setuptools.dist.Distribution' for more details.\n\n    Also, this version of the 'build_py' command allows you to specify both\n    'py_modules' and 'packages' in the same setup operation.\n    \"\"\"\n\n    def finalize_options(self):\n        orig.build_py.finalize_options(self)\n        self.package_data = self.distribution.package_data\n        self.exclude_package_data = (self.distribution.exclude_package_data or\n                                     {})\n        if 'data_files' in self.__dict__:\n            del self.__dict__['data_files']\n        self.__updated_files = []\n        self.__doctests_2to3 = []\n\n    def run(self):\n        \"\"\"Build modules, packages, and copy data files to build directory\"\"\"\n        if not self.py_modules and not self.packages:\n            return\n\n        if self.py_modules:\n            self.build_modules()\n\n        if self.packages:\n            self.build_packages()\n            self.build_package_data()\n\n        self.run_2to3(self.__updated_files, False)\n        self.run_2to3(self.__updated_files, True)\n        self.run_2to3(self.__doctests_2to3, True)\n\n        # Only compile actual .py files, using our base class' idea of what our\n        # output files are.\n        self.byte_compile(orig.build_py.get_outputs(self, include_bytecode=0))\n\n    def __getattr__(self, attr):\n        \"lazily compute data files\"\n        if attr == 'data_files':\n            self.data_files = self._get_data_files()\n            return self.data_files\n        return orig.build_py.__getattr__(self, attr)\n\n    def build_module(self, module, module_file, package):\n        if six.PY2 and isinstance(package, six.string_types):\n            # avoid errors on Python 2 when unicode is passed (#190)\n            package = package.split('.')\n        outfile, copied = orig.build_py.build_module(self, module, module_file,\n                                                     package)\n        if copied:\n            self.__updated_files.append(outfile)\n        return outfile, copied\n\n    def _get_data_files(self):\n        \"\"\"Generate list of '(package,src_dir,build_dir,filenames)' tuples\"\"\"\n        self.analyze_manifest()\n        return list(map(self._get_pkg_data_files, self.packages or ()))\n\n    def _get_pkg_data_files(self, package):\n        # Locate package source directory\n        src_dir = self.get_package_dir(package)\n\n        # Compute package build directory\n        build_dir = os.path.join(*([self.build_lib] + package.split('.')))\n\n        # Strip directory from globbed filenames\n        filenames = [\n            os.path.relpath(file, src_dir)\n            for file in self.find_data_files(package, src_dir)\n        ]\n        return package, src_dir, build_dir, filenames\n\n    def find_data_files(self, package, src_dir):\n        \"\"\"Return filenames for package's data files in 'src_dir'\"\"\"\n        patterns = self._get_platform_patterns(\n            self.package_data,\n            package,\n            src_dir,\n        )\n        globs_expanded = map(glob, patterns)\n        # flatten the expanded globs into an iterable of matches\n        globs_matches = itertools.chain.from_iterable(globs_expanded)\n        glob_files = filter(os.path.isfile, globs_matches)\n        files = itertools.chain(\n            self.manifest_files.get(package, []),\n            glob_files,\n        )\n        return self.exclude_data_files(package, src_dir, files)\n\n    def build_package_data(self):\n        \"\"\"Copy data files into build directory\"\"\"\n        for package, src_dir, build_dir, filenames in self.data_files:\n            for filename in filenames:\n                target = os.path.join(build_dir, filename)\n                self.mkpath(os.path.dirname(target))\n                srcfile = os.path.join(src_dir, filename)\n                outf, copied = self.copy_file(srcfile, target)\n                srcfile = os.path.abspath(srcfile)\n                if (copied and\n                        srcfile in self.distribution.convert_2to3_doctests):\n                    self.__doctests_2to3.append(outf)\n\n    def analyze_manifest(self):\n        self.manifest_files = mf = {}\n        if not self.distribution.include_package_data:\n            return\n        src_dirs = {}\n        for package in self.packages or ():\n            # Locate package source directory\n            src_dirs[assert_relative(self.get_package_dir(package))] = package\n\n        self.run_command('egg_info')\n        ei_cmd = self.get_finalized_command('egg_info')\n        for path in ei_cmd.filelist.files:\n            d, f = os.path.split(assert_relative(path))\n            prev = None\n            oldf = f\n            while d and d != prev and d not in src_dirs:\n                prev = d\n                d, df = os.path.split(d)\n                f = os.path.join(df, f)\n            if d in src_dirs:\n                if path.endswith('.py') and f == oldf:\n                    continue  # it's a module, not data\n                mf.setdefault(src_dirs[d], []).append(path)\n\n    def get_data_files(self):\n        pass  # Lazily compute data files in _get_data_files() function.\n\n    def check_package(self, package, package_dir):\n        \"\"\"Check namespace packages' __init__ for declare_namespace\"\"\"\n        try:\n            return self.packages_checked[package]\n        except KeyError:\n            pass\n\n        init_py = orig.build_py.check_package(self, package, package_dir)\n        self.packages_checked[package] = init_py\n\n        if not init_py or not self.distribution.namespace_packages:\n            return init_py\n\n        for pkg in self.distribution.namespace_packages:\n            if pkg == package or pkg.startswith(package + '.'):\n                break\n        else:\n            return init_py\n\n        with io.open(init_py, 'rb') as f:\n            contents = f.read()\n        if b'declare_namespace' not in contents:\n            raise distutils.errors.DistutilsError(\n                \"Namespace package problem: %s is a namespace package, but \"\n                \"its\\n__init__.py does not call declare_namespace()! Please \"\n                'fix it.\\n(See the setuptools manual under '\n                '\"Namespace Packages\" for details.)\\n\"' % (package,)\n            )\n        return init_py\n\n    def initialize_options(self):\n        self.packages_checked = {}\n        orig.build_py.initialize_options(self)\n\n    def get_package_dir(self, package):\n        res = orig.build_py.get_package_dir(self, package)\n        if self.distribution.src_root is not None:\n            return os.path.join(self.distribution.src_root, res)\n        return res\n\n    def exclude_data_files(self, package, src_dir, files):\n        \"\"\"Filter filenames for package's data files in 'src_dir'\"\"\"\n        files = list(files)\n        patterns = self._get_platform_patterns(\n            self.exclude_package_data,\n            package,\n            src_dir,\n        )\n        match_groups = (\n            fnmatch.filter(files, pattern)\n            for pattern in patterns\n        )\n        # flatten the groups of matches into an iterable of matches\n        matches = itertools.chain.from_iterable(match_groups)\n        bad = set(matches)\n        keepers = (\n            fn\n            for fn in files\n            if fn not in bad\n        )\n        # ditch dupes\n        return list(_unique_everseen(keepers))\n\n    @staticmethod\n    def _get_platform_patterns(spec, package, src_dir):\n        \"\"\"\n        yield platform-specific path patterns (suitable for glob\n        or fn_match) from a glob-based spec (such as\n        self.package_data or self.exclude_package_data)\n        matching package in src_dir.\n        \"\"\"\n        raw_patterns = itertools.chain(\n            spec.get('', []),\n            spec.get(package, []),\n        )\n        return (\n            # Each pattern has to be converted to a platform-specific path\n            os.path.join(src_dir, convert_path(pattern))\n            for pattern in raw_patterns\n        )\n\n\n# from Python docs\ndef _unique_everseen(iterable, key=None):\n    \"List unique elements, preserving order. Remember all elements ever seen.\"\n    # unique_everseen('AAAABBBCCDAABBB') --> A B C D\n    # unique_everseen('ABBCcAD', str.lower) --> A B C D\n    seen = set()\n    seen_add = seen.add\n    if key is None:\n        for element in filterfalse(seen.__contains__, iterable):\n            seen_add(element)\n            yield element\n    else:\n        for element in iterable:\n            k = key(element)\n            if k not in seen:\n                seen_add(k)\n                yield element\n\n\ndef assert_relative(path):\n    if not os.path.isabs(path):\n        return path\n    from distutils.errors import DistutilsSetupError\n\n    msg = textwrap.dedent(\"\"\"\n        Error: setup script specifies an absolute path:\n\n            %s\n\n        setup() arguments must *always* be /-separated paths relative to the\n        setup.py directory, *never* absolute paths.\n        \"\"\").lstrip() % path\n    raise DistutilsSetupError(msg)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/develop.py",
    "content": "from distutils.util import convert_path\nfrom distutils import log\nfrom distutils.errors import DistutilsError, DistutilsOptionError\nimport os\nimport glob\nimport io\n\nfrom setuptools.extern import six\n\nimport pkg_resources\nfrom setuptools.command.easy_install import easy_install\nfrom setuptools import namespaces\nimport setuptools\n\n__metaclass__ = type\n\n\nclass develop(namespaces.DevelopInstaller, easy_install):\n    \"\"\"Set up package for development\"\"\"\n\n    description = \"install package in 'development mode'\"\n\n    user_options = easy_install.user_options + [\n        (\"uninstall\", \"u\", \"Uninstall this source package\"),\n        (\"egg-path=\", None, \"Set the path to be used in the .egg-link file\"),\n    ]\n\n    boolean_options = easy_install.boolean_options + ['uninstall']\n\n    command_consumes_arguments = False  # override base\n\n    def run(self):\n        if self.uninstall:\n            self.multi_version = True\n            self.uninstall_link()\n            self.uninstall_namespaces()\n        else:\n            self.install_for_development()\n        self.warn_deprecated_options()\n\n    def initialize_options(self):\n        self.uninstall = None\n        self.egg_path = None\n        easy_install.initialize_options(self)\n        self.setup_path = None\n        self.always_copy_from = '.'  # always copy eggs installed in curdir\n\n    def finalize_options(self):\n        ei = self.get_finalized_command(\"egg_info\")\n        if ei.broken_egg_info:\n            template = \"Please rename %r to %r before using 'develop'\"\n            args = ei.egg_info, ei.broken_egg_info\n            raise DistutilsError(template % args)\n        self.args = [ei.egg_name]\n\n        easy_install.finalize_options(self)\n        self.expand_basedirs()\n        self.expand_dirs()\n        # pick up setup-dir .egg files only: no .egg-info\n        self.package_index.scan(glob.glob('*.egg'))\n\n        egg_link_fn = ei.egg_name + '.egg-link'\n        self.egg_link = os.path.join(self.install_dir, egg_link_fn)\n        self.egg_base = ei.egg_base\n        if self.egg_path is None:\n            self.egg_path = os.path.abspath(ei.egg_base)\n\n        target = pkg_resources.normalize_path(self.egg_base)\n        egg_path = pkg_resources.normalize_path(\n            os.path.join(self.install_dir, self.egg_path))\n        if egg_path != target:\n            raise DistutilsOptionError(\n                \"--egg-path must be a relative path from the install\"\n                \" directory to \" + target\n            )\n\n        # Make a distribution for the package's source\n        self.dist = pkg_resources.Distribution(\n            target,\n            pkg_resources.PathMetadata(target, os.path.abspath(ei.egg_info)),\n            project_name=ei.egg_name\n        )\n\n        self.setup_path = self._resolve_setup_path(\n            self.egg_base,\n            self.install_dir,\n            self.egg_path,\n        )\n\n    @staticmethod\n    def _resolve_setup_path(egg_base, install_dir, egg_path):\n        \"\"\"\n        Generate a path from egg_base back to '.' where the\n        setup script resides and ensure that path points to the\n        setup path from $install_dir/$egg_path.\n        \"\"\"\n        path_to_setup = egg_base.replace(os.sep, '/').rstrip('/')\n        if path_to_setup != os.curdir:\n            path_to_setup = '../' * (path_to_setup.count('/') + 1)\n        resolved = pkg_resources.normalize_path(\n            os.path.join(install_dir, egg_path, path_to_setup)\n        )\n        if resolved != pkg_resources.normalize_path(os.curdir):\n            raise DistutilsOptionError(\n                \"Can't get a consistent path to setup script from\"\n                \" installation directory\", resolved,\n                pkg_resources.normalize_path(os.curdir))\n        return path_to_setup\n\n    def install_for_development(self):\n        if not six.PY2 and getattr(self.distribution, 'use_2to3', False):\n            # If we run 2to3 we can not do this inplace:\n\n            # Ensure metadata is up-to-date\n            self.reinitialize_command('build_py', inplace=0)\n            self.run_command('build_py')\n            bpy_cmd = self.get_finalized_command(\"build_py\")\n            build_path = pkg_resources.normalize_path(bpy_cmd.build_lib)\n\n            # Build extensions\n            self.reinitialize_command('egg_info', egg_base=build_path)\n            self.run_command('egg_info')\n\n            self.reinitialize_command('build_ext', inplace=0)\n            self.run_command('build_ext')\n\n            # Fixup egg-link and easy-install.pth\n            ei_cmd = self.get_finalized_command(\"egg_info\")\n            self.egg_path = build_path\n            self.dist.location = build_path\n            # XXX\n            self.dist._provider = pkg_resources.PathMetadata(\n                build_path, ei_cmd.egg_info)\n        else:\n            # Without 2to3 inplace works fine:\n            self.run_command('egg_info')\n\n            # Build extensions in-place\n            self.reinitialize_command('build_ext', inplace=1)\n            self.run_command('build_ext')\n\n        self.install_site_py()  # ensure that target dir is site-safe\n        if setuptools.bootstrap_install_from:\n            self.easy_install(setuptools.bootstrap_install_from)\n            setuptools.bootstrap_install_from = None\n\n        self.install_namespaces()\n\n        # create an .egg-link in the installation dir, pointing to our egg\n        log.info(\"Creating %s (link to %s)\", self.egg_link, self.egg_base)\n        if not self.dry_run:\n            with open(self.egg_link, \"w\") as f:\n                f.write(self.egg_path + \"\\n\" + self.setup_path)\n        # postprocess the installed distro, fixing up .pth, installing scripts,\n        # and handling requirements\n        self.process_distribution(None, self.dist, not self.no_deps)\n\n    def uninstall_link(self):\n        if os.path.exists(self.egg_link):\n            log.info(\"Removing %s (link to %s)\", self.egg_link, self.egg_base)\n            egg_link_file = open(self.egg_link)\n            contents = [line.rstrip() for line in egg_link_file]\n            egg_link_file.close()\n            if contents not in ([self.egg_path],\n                                [self.egg_path, self.setup_path]):\n                log.warn(\"Link points to %s: uninstall aborted\", contents)\n                return\n            if not self.dry_run:\n                os.unlink(self.egg_link)\n        if not self.dry_run:\n            self.update_pth(self.dist)  # remove any .pth link to us\n        if self.distribution.scripts:\n            # XXX should also check for entry point scripts!\n            log.warn(\"Note: you must uninstall or replace scripts manually!\")\n\n    def install_egg_scripts(self, dist):\n        if dist is not self.dist:\n            # Installing a dependency, so fall back to normal behavior\n            return easy_install.install_egg_scripts(self, dist)\n\n        # create wrapper scripts in the script dir, pointing to dist.scripts\n\n        # new-style...\n        self.install_wrapper_scripts(dist)\n\n        # ...and old-style\n        for script_name in self.distribution.scripts or []:\n            script_path = os.path.abspath(convert_path(script_name))\n            script_name = os.path.basename(script_path)\n            with io.open(script_path) as strm:\n                script_text = strm.read()\n            self.install_script(dist, script_name, script_text, script_path)\n\n    def install_wrapper_scripts(self, dist):\n        dist = VersionlessRequirement(dist)\n        return easy_install.install_wrapper_scripts(self, dist)\n\n\nclass VersionlessRequirement:\n    \"\"\"\n    Adapt a pkg_resources.Distribution to simply return the project\n    name as the 'requirement' so that scripts will work across\n    multiple versions.\n\n    >>> from pkg_resources import Distribution\n    >>> dist = Distribution(project_name='foo', version='1.0')\n    >>> str(dist.as_requirement())\n    'foo==1.0'\n    >>> adapted_dist = VersionlessRequirement(dist)\n    >>> str(adapted_dist.as_requirement())\n    'foo'\n    \"\"\"\n\n    def __init__(self, dist):\n        self.__dist = dist\n\n    def __getattr__(self, name):\n        return getattr(self.__dist, name)\n\n    def as_requirement(self):\n        return self.project_name\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/dist_info.py",
    "content": "\"\"\"\nCreate a dist_info directory\nAs defined in the wheel specification\n\"\"\"\n\nimport os\n\nfrom distutils.core import Command\nfrom distutils import log\n\n\nclass dist_info(Command):\n\n    description = 'create a .dist-info directory'\n\n    user_options = [\n        ('egg-base=', 'e', \"directory containing .egg-info directories\"\n                           \" (default: top of the source tree)\"),\n    ]\n\n    def initialize_options(self):\n        self.egg_base = None\n\n    def finalize_options(self):\n        pass\n\n    def run(self):\n        egg_info = self.get_finalized_command('egg_info')\n        egg_info.egg_base = self.egg_base\n        egg_info.finalize_options()\n        egg_info.run()\n        dist_info_dir = egg_info.egg_info[:-len('.egg-info')] + '.dist-info'\n        log.info(\"creating '{}'\".format(os.path.abspath(dist_info_dir)))\n\n        bdist_wheel = self.get_finalized_command('bdist_wheel')\n        bdist_wheel.egg2dist(egg_info.egg_info, dist_info_dir)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/easy_install.py",
    "content": "#!/usr/bin/env python\n\"\"\"\nEasy Install\n------------\n\nA tool for doing automatic download/extract/build of distutils-based Python\npackages.  For detailed documentation, see the accompanying EasyInstall.txt\nfile, or visit the `EasyInstall home page`__.\n\n__ https://setuptools.readthedocs.io/en/latest/easy_install.html\n\n\"\"\"\n\nfrom glob import glob\nfrom distutils.util import get_platform\nfrom distutils.util import convert_path, subst_vars\nfrom distutils.errors import (\n    DistutilsArgError, DistutilsOptionError,\n    DistutilsError, DistutilsPlatformError,\n)\nfrom distutils.command.install import INSTALL_SCHEMES, SCHEME_KEYS\nfrom distutils import log, dir_util\nfrom distutils.command.build_scripts import first_line_re\nfrom distutils.spawn import find_executable\nimport sys\nimport os\nimport zipimport\nimport shutil\nimport tempfile\nimport zipfile\nimport re\nimport stat\nimport random\nimport textwrap\nimport warnings\nimport site\nimport struct\nimport contextlib\nimport subprocess\nimport shlex\nimport io\n\n\nfrom sysconfig import get_config_vars, get_path\n\nfrom setuptools import SetuptoolsDeprecationWarning\n\nfrom setuptools.extern import six\nfrom setuptools.extern.six.moves import configparser, map\n\nfrom setuptools import Command\nfrom setuptools.sandbox import run_setup\nfrom setuptools.py27compat import rmtree_safe\nfrom setuptools.command import setopt\nfrom setuptools.archive_util import unpack_archive\nfrom setuptools.package_index import (\n    PackageIndex, parse_requirement_arg, URL_SCHEME,\n)\nfrom setuptools.command import bdist_egg, egg_info\nfrom setuptools.wheel import Wheel\nfrom pkg_resources import (\n    yield_lines, normalize_path, resource_string, ensure_directory,\n    get_distribution, find_distributions, Environment, Requirement,\n    Distribution, PathMetadata, EggMetadata, WorkingSet, DistributionNotFound,\n    VersionConflict, DEVELOP_DIST,\n)\nimport pkg_resources.py31compat\n\n__metaclass__ = type\n\n# Turn on PEP440Warnings\nwarnings.filterwarnings(\"default\", category=pkg_resources.PEP440Warning)\n\n__all__ = [\n    'samefile', 'easy_install', 'PthDistributions', 'extract_wininst_cfg',\n    'main', 'get_exe_prefixes',\n]\n\n\ndef is_64bit():\n    return struct.calcsize(\"P\") == 8\n\n\ndef samefile(p1, p2):\n    \"\"\"\n    Determine if two paths reference the same file.\n\n    Augments os.path.samefile to work on Windows and\n    suppresses errors if the path doesn't exist.\n    \"\"\"\n    both_exist = os.path.exists(p1) and os.path.exists(p2)\n    use_samefile = hasattr(os.path, 'samefile') and both_exist\n    if use_samefile:\n        return os.path.samefile(p1, p2)\n    norm_p1 = os.path.normpath(os.path.normcase(p1))\n    norm_p2 = os.path.normpath(os.path.normcase(p2))\n    return norm_p1 == norm_p2\n\n\nif six.PY2:\n\n    def _to_bytes(s):\n        return s\n\n    def isascii(s):\n        try:\n            six.text_type(s, 'ascii')\n            return True\n        except UnicodeError:\n            return False\nelse:\n\n    def _to_bytes(s):\n        return s.encode('utf8')\n\n    def isascii(s):\n        try:\n            s.encode('ascii')\n            return True\n        except UnicodeError:\n            return False\n\n\n_one_liner = lambda text: textwrap.dedent(text).strip().replace('\\n', '; ')\n\n\nclass easy_install(Command):\n    \"\"\"Manage a download/build/install process\"\"\"\n    description = \"Find/get/install Python packages\"\n    command_consumes_arguments = True\n\n    user_options = [\n        ('prefix=', None, \"installation prefix\"),\n        (\"zip-ok\", \"z\", \"install package as a zipfile\"),\n        (\"multi-version\", \"m\", \"make apps have to require() a version\"),\n        (\"upgrade\", \"U\", \"force upgrade (searches PyPI for latest versions)\"),\n        (\"install-dir=\", \"d\", \"install package to DIR\"),\n        (\"script-dir=\", \"s\", \"install scripts to DIR\"),\n        (\"exclude-scripts\", \"x\", \"Don't install scripts\"),\n        (\"always-copy\", \"a\", \"Copy all needed packages to install dir\"),\n        (\"index-url=\", \"i\", \"base URL of Python Package Index\"),\n        (\"find-links=\", \"f\", \"additional URL(s) to search for packages\"),\n        (\"build-directory=\", \"b\",\n         \"download/extract/build in DIR; keep the results\"),\n        ('optimize=', 'O',\n         \"also compile with optimization: -O1 for \\\"python -O\\\", \"\n         \"-O2 for \\\"python -OO\\\", and -O0 to disable [default: -O0]\"),\n        ('record=', None,\n         \"filename in which to record list of installed files\"),\n        ('always-unzip', 'Z', \"don't install as a zipfile, no matter what\"),\n        ('site-dirs=', 'S', \"list of directories where .pth files work\"),\n        ('editable', 'e', \"Install specified packages in editable form\"),\n        ('no-deps', 'N', \"don't install dependencies\"),\n        ('allow-hosts=', 'H', \"pattern(s) that hostnames must match\"),\n        ('local-snapshots-ok', 'l',\n         \"allow building eggs from local checkouts\"),\n        ('version', None, \"print version information and exit\"),\n        ('no-find-links', None,\n         \"Don't load find-links defined in packages being installed\")\n    ]\n    boolean_options = [\n        'zip-ok', 'multi-version', 'exclude-scripts', 'upgrade', 'always-copy',\n        'editable',\n        'no-deps', 'local-snapshots-ok', 'version'\n    ]\n\n    if site.ENABLE_USER_SITE:\n        help_msg = \"install in user site-package '%s'\" % site.USER_SITE\n        user_options.append(('user', None, help_msg))\n        boolean_options.append('user')\n\n    negative_opt = {'always-unzip': 'zip-ok'}\n    create_index = PackageIndex\n\n    def initialize_options(self):\n        # the --user option seems to be an opt-in one,\n        # so the default should be False.\n        self.user = 0\n        self.zip_ok = self.local_snapshots_ok = None\n        self.install_dir = self.script_dir = self.exclude_scripts = None\n        self.index_url = None\n        self.find_links = None\n        self.build_directory = None\n        self.args = None\n        self.optimize = self.record = None\n        self.upgrade = self.always_copy = self.multi_version = None\n        self.editable = self.no_deps = self.allow_hosts = None\n        self.root = self.prefix = self.no_report = None\n        self.version = None\n        self.install_purelib = None  # for pure module distributions\n        self.install_platlib = None  # non-pure (dists w/ extensions)\n        self.install_headers = None  # for C/C++ headers\n        self.install_lib = None  # set to either purelib or platlib\n        self.install_scripts = None\n        self.install_data = None\n        self.install_base = None\n        self.install_platbase = None\n        if site.ENABLE_USER_SITE:\n            self.install_userbase = site.USER_BASE\n            self.install_usersite = site.USER_SITE\n        else:\n            self.install_userbase = None\n            self.install_usersite = None\n        self.no_find_links = None\n\n        # Options not specifiable via command line\n        self.package_index = None\n        self.pth_file = self.always_copy_from = None\n        self.site_dirs = None\n        self.installed_projects = {}\n        self.sitepy_installed = False\n        # Always read easy_install options, even if we are subclassed, or have\n        # an independent instance created.  This ensures that defaults will\n        # always come from the standard configuration file(s)' \"easy_install\"\n        # section, even if this is a \"develop\" or \"install\" command, or some\n        # other embedding.\n        self._dry_run = None\n        self.verbose = self.distribution.verbose\n        self.distribution._set_command_options(\n            self, self.distribution.get_option_dict('easy_install')\n        )\n\n    def delete_blockers(self, blockers):\n        extant_blockers = (\n            filename for filename in blockers\n            if os.path.exists(filename) or os.path.islink(filename)\n        )\n        list(map(self._delete_path, extant_blockers))\n\n    def _delete_path(self, path):\n        log.info(\"Deleting %s\", path)\n        if self.dry_run:\n            return\n\n        is_tree = os.path.isdir(path) and not os.path.islink(path)\n        remover = rmtree if is_tree else os.unlink\n        remover(path)\n\n    @staticmethod\n    def _render_version():\n        \"\"\"\n        Render the Setuptools version and installation details, then exit.\n        \"\"\"\n        ver = '{}.{}'.format(*sys.version_info)\n        dist = get_distribution('setuptools')\n        tmpl = 'setuptools {dist.version} from {dist.location} (Python {ver})'\n        print(tmpl.format(**locals()))\n        raise SystemExit()\n\n    def finalize_options(self):\n        self.version and self._render_version()\n\n        py_version = sys.version.split()[0]\n        prefix, exec_prefix = get_config_vars('prefix', 'exec_prefix')\n\n        self.config_vars = {\n            'dist_name': self.distribution.get_name(),\n            'dist_version': self.distribution.get_version(),\n            'dist_fullname': self.distribution.get_fullname(),\n            'py_version': py_version,\n            'py_version_short': py_version[0:3],\n            'py_version_nodot': py_version[0] + py_version[2],\n            'sys_prefix': prefix,\n            'prefix': prefix,\n            'sys_exec_prefix': exec_prefix,\n            'exec_prefix': exec_prefix,\n            # Only python 3.2+ has abiflags\n            'abiflags': getattr(sys, 'abiflags', ''),\n        }\n\n        if site.ENABLE_USER_SITE:\n            self.config_vars['userbase'] = self.install_userbase\n            self.config_vars['usersite'] = self.install_usersite\n\n        self._fix_install_dir_for_user_site()\n\n        self.expand_basedirs()\n        self.expand_dirs()\n\n        self._expand(\n            'install_dir', 'script_dir', 'build_directory',\n            'site_dirs',\n        )\n        # If a non-default installation directory was specified, default the\n        # script directory to match it.\n        if self.script_dir is None:\n            self.script_dir = self.install_dir\n\n        if self.no_find_links is None:\n            self.no_find_links = False\n\n        # Let install_dir get set by install_lib command, which in turn\n        # gets its info from the install command, and takes into account\n        # --prefix and --home and all that other crud.\n        self.set_undefined_options(\n            'install_lib', ('install_dir', 'install_dir')\n        )\n        # Likewise, set default script_dir from 'install_scripts.install_dir'\n        self.set_undefined_options(\n            'install_scripts', ('install_dir', 'script_dir')\n        )\n\n        if self.user and self.install_purelib:\n            self.install_dir = self.install_purelib\n            self.script_dir = self.install_scripts\n        # default --record from the install command\n        self.set_undefined_options('install', ('record', 'record'))\n        # Should this be moved to the if statement below? It's not used\n        # elsewhere\n        normpath = map(normalize_path, sys.path)\n        self.all_site_dirs = get_site_dirs()\n        if self.site_dirs is not None:\n            site_dirs = [\n                os.path.expanduser(s.strip()) for s in\n                self.site_dirs.split(',')\n            ]\n            for d in site_dirs:\n                if not os.path.isdir(d):\n                    log.warn(\"%s (in --site-dirs) does not exist\", d)\n                elif normalize_path(d) not in normpath:\n                    raise DistutilsOptionError(\n                        d + \" (in --site-dirs) is not on sys.path\"\n                    )\n                else:\n                    self.all_site_dirs.append(normalize_path(d))\n        if not self.editable:\n            self.check_site_dir()\n        self.index_url = self.index_url or \"https://pypi.org/simple/\"\n        self.shadow_path = self.all_site_dirs[:]\n        for path_item in self.install_dir, normalize_path(self.script_dir):\n            if path_item not in self.shadow_path:\n                self.shadow_path.insert(0, path_item)\n\n        if self.allow_hosts is not None:\n            hosts = [s.strip() for s in self.allow_hosts.split(',')]\n        else:\n            hosts = ['*']\n        if self.package_index is None:\n            self.package_index = self.create_index(\n                self.index_url, search_path=self.shadow_path, hosts=hosts,\n            )\n        self.local_index = Environment(self.shadow_path + sys.path)\n\n        if self.find_links is not None:\n            if isinstance(self.find_links, six.string_types):\n                self.find_links = self.find_links.split()\n        else:\n            self.find_links = []\n        if self.local_snapshots_ok:\n            self.package_index.scan_egg_links(self.shadow_path + sys.path)\n        if not self.no_find_links:\n            self.package_index.add_find_links(self.find_links)\n        self.set_undefined_options('install_lib', ('optimize', 'optimize'))\n        if not isinstance(self.optimize, int):\n            try:\n                self.optimize = int(self.optimize)\n                if not (0 <= self.optimize <= 2):\n                    raise ValueError\n            except ValueError:\n                raise DistutilsOptionError(\"--optimize must be 0, 1, or 2\")\n\n        if self.editable and not self.build_directory:\n            raise DistutilsArgError(\n                \"Must specify a build directory (-b) when using --editable\"\n            )\n        if not self.args:\n            raise DistutilsArgError(\n                \"No urls, filenames, or requirements specified (see --help)\")\n\n        self.outputs = []\n\n    def _fix_install_dir_for_user_site(self):\n        \"\"\"\n        Fix the install_dir if \"--user\" was used.\n        \"\"\"\n        if not self.user or not site.ENABLE_USER_SITE:\n            return\n\n        self.create_home_path()\n        if self.install_userbase is None:\n            msg = \"User base directory is not specified\"\n            raise DistutilsPlatformError(msg)\n        self.install_base = self.install_platbase = self.install_userbase\n        scheme_name = os.name.replace('posix', 'unix') + '_user'\n        self.select_scheme(scheme_name)\n\n    def _expand_attrs(self, attrs):\n        for attr in attrs:\n            val = getattr(self, attr)\n            if val is not None:\n                if os.name == 'posix' or os.name == 'nt':\n                    val = os.path.expanduser(val)\n                val = subst_vars(val, self.config_vars)\n                setattr(self, attr, val)\n\n    def expand_basedirs(self):\n        \"\"\"Calls `os.path.expanduser` on install_base, install_platbase and\n        root.\"\"\"\n        self._expand_attrs(['install_base', 'install_platbase', 'root'])\n\n    def expand_dirs(self):\n        \"\"\"Calls `os.path.expanduser` on install dirs.\"\"\"\n        dirs = [\n            'install_purelib',\n            'install_platlib',\n            'install_lib',\n            'install_headers',\n            'install_scripts',\n            'install_data',\n        ]\n        self._expand_attrs(dirs)\n\n    def run(self, show_deprecation=True):\n        if show_deprecation:\n            self.announce(\n                \"WARNING: The easy_install command is deprecated \"\n                \"and will be removed in a future version.\"\n                , log.WARN,\n            )\n        if self.verbose != self.distribution.verbose:\n            log.set_verbosity(self.verbose)\n        try:\n            for spec in self.args:\n                self.easy_install(spec, not self.no_deps)\n            if self.record:\n                outputs = self.outputs\n                if self.root:  # strip any package prefix\n                    root_len = len(self.root)\n                    for counter in range(len(outputs)):\n                        outputs[counter] = outputs[counter][root_len:]\n                from distutils import file_util\n\n                self.execute(\n                    file_util.write_file, (self.record, outputs),\n                    \"writing list of installed files to '%s'\" %\n                    self.record\n                )\n            self.warn_deprecated_options()\n        finally:\n            log.set_verbosity(self.distribution.verbose)\n\n    def pseudo_tempname(self):\n        \"\"\"Return a pseudo-tempname base in the install directory.\n        This code is intentionally naive; if a malicious party can write to\n        the target directory you're already in deep doodoo.\n        \"\"\"\n        try:\n            pid = os.getpid()\n        except Exception:\n            pid = random.randint(0, sys.maxsize)\n        return os.path.join(self.install_dir, \"test-easy-install-%s\" % pid)\n\n    def warn_deprecated_options(self):\n        pass\n\n    def check_site_dir(self):\n        \"\"\"Verify that self.install_dir is .pth-capable dir, if needed\"\"\"\n\n        instdir = normalize_path(self.install_dir)\n        pth_file = os.path.join(instdir, 'easy-install.pth')\n\n        # Is it a configured, PYTHONPATH, implicit, or explicit site dir?\n        is_site_dir = instdir in self.all_site_dirs\n\n        if not is_site_dir and not self.multi_version:\n            # No?  Then directly test whether it does .pth file processing\n            is_site_dir = self.check_pth_processing()\n        else:\n            # make sure we can write to target dir\n            testfile = self.pseudo_tempname() + '.write-test'\n            test_exists = os.path.exists(testfile)\n            try:\n                if test_exists:\n                    os.unlink(testfile)\n                open(testfile, 'w').close()\n                os.unlink(testfile)\n            except (OSError, IOError):\n                self.cant_write_to_target()\n\n        if not is_site_dir and not self.multi_version:\n            # Can't install non-multi to non-site dir\n            raise DistutilsError(self.no_default_version_msg())\n\n        if is_site_dir:\n            if self.pth_file is None:\n                self.pth_file = PthDistributions(pth_file, self.all_site_dirs)\n        else:\n            self.pth_file = None\n\n        if instdir not in map(normalize_path, _pythonpath()):\n            # only PYTHONPATH dirs need a site.py, so pretend it's there\n            self.sitepy_installed = True\n        elif self.multi_version and not os.path.exists(pth_file):\n            self.sitepy_installed = True  # don't need site.py in this case\n            self.pth_file = None  # and don't create a .pth file\n        self.install_dir = instdir\n\n    __cant_write_msg = textwrap.dedent(\"\"\"\n        can't create or remove files in install directory\n\n        The following error occurred while trying to add or remove files in the\n        installation directory:\n\n            %s\n\n        The installation directory you specified (via --install-dir, --prefix, or\n        the distutils default setting) was:\n\n            %s\n        \"\"\").lstrip()\n\n    __not_exists_id = textwrap.dedent(\"\"\"\n        This directory does not currently exist.  Please create it and try again, or\n        choose a different installation directory (using the -d or --install-dir\n        option).\n        \"\"\").lstrip()\n\n    __access_msg = textwrap.dedent(\"\"\"\n        Perhaps your account does not have write access to this directory?  If the\n        installation directory is a system-owned directory, you may need to sign in\n        as the administrator or \"root\" account.  If you do not have administrative\n        access to this machine, you may wish to choose a different installation\n        directory, preferably one that is listed in your PYTHONPATH environment\n        variable.\n\n        For information on other options, you may wish to consult the\n        documentation at:\n\n          https://setuptools.readthedocs.io/en/latest/easy_install.html\n\n        Please make the appropriate changes for your system and try again.\n        \"\"\").lstrip()\n\n    def cant_write_to_target(self):\n        msg = self.__cant_write_msg % (sys.exc_info()[1], self.install_dir,)\n\n        if not os.path.exists(self.install_dir):\n            msg += '\\n' + self.__not_exists_id\n        else:\n            msg += '\\n' + self.__access_msg\n        raise DistutilsError(msg)\n\n    def check_pth_processing(self):\n        \"\"\"Empirically verify whether .pth files are supported in inst. dir\"\"\"\n        instdir = self.install_dir\n        log.info(\"Checking .pth file support in %s\", instdir)\n        pth_file = self.pseudo_tempname() + \".pth\"\n        ok_file = pth_file + '.ok'\n        ok_exists = os.path.exists(ok_file)\n        tmpl = _one_liner(\"\"\"\n            import os\n            f = open({ok_file!r}, 'w')\n            f.write('OK')\n            f.close()\n            \"\"\") + '\\n'\n        try:\n            if ok_exists:\n                os.unlink(ok_file)\n            dirname = os.path.dirname(ok_file)\n            pkg_resources.py31compat.makedirs(dirname, exist_ok=True)\n            f = open(pth_file, 'w')\n        except (OSError, IOError):\n            self.cant_write_to_target()\n        else:\n            try:\n                f.write(tmpl.format(**locals()))\n                f.close()\n                f = None\n                executable = sys.executable\n                if os.name == 'nt':\n                    dirname, basename = os.path.split(executable)\n                    alt = os.path.join(dirname, 'pythonw.exe')\n                    use_alt = (\n                        basename.lower() == 'python.exe' and\n                        os.path.exists(alt)\n                    )\n                    if use_alt:\n                        # use pythonw.exe to avoid opening a console window\n                        executable = alt\n\n                from distutils.spawn import spawn\n\n                spawn([executable, '-E', '-c', 'pass'], 0)\n\n                if os.path.exists(ok_file):\n                    log.info(\n                        \"TEST PASSED: %s appears to support .pth files\",\n                        instdir\n                    )\n                    return True\n            finally:\n                if f:\n                    f.close()\n                if os.path.exists(ok_file):\n                    os.unlink(ok_file)\n                if os.path.exists(pth_file):\n                    os.unlink(pth_file)\n        if not self.multi_version:\n            log.warn(\"TEST FAILED: %s does NOT support .pth files\", instdir)\n        return False\n\n    def install_egg_scripts(self, dist):\n        \"\"\"Write all the scripts for `dist`, unless scripts are excluded\"\"\"\n        if not self.exclude_scripts and dist.metadata_isdir('scripts'):\n            for script_name in dist.metadata_listdir('scripts'):\n                if dist.metadata_isdir('scripts/' + script_name):\n                    # The \"script\" is a directory, likely a Python 3\n                    # __pycache__ directory, so skip it.\n                    continue\n                self.install_script(\n                    dist, script_name,\n                    dist.get_metadata('scripts/' + script_name)\n                )\n        self.install_wrapper_scripts(dist)\n\n    def add_output(self, path):\n        if os.path.isdir(path):\n            for base, dirs, files in os.walk(path):\n                for filename in files:\n                    self.outputs.append(os.path.join(base, filename))\n        else:\n            self.outputs.append(path)\n\n    def not_editable(self, spec):\n        if self.editable:\n            raise DistutilsArgError(\n                \"Invalid argument %r: you can't use filenames or URLs \"\n                \"with --editable (except via the --find-links option).\"\n                % (spec,)\n            )\n\n    def check_editable(self, spec):\n        if not self.editable:\n            return\n\n        if os.path.exists(os.path.join(self.build_directory, spec.key)):\n            raise DistutilsArgError(\n                \"%r already exists in %s; can't do a checkout there\" %\n                (spec.key, self.build_directory)\n            )\n\n    @contextlib.contextmanager\n    def _tmpdir(self):\n        tmpdir = tempfile.mkdtemp(prefix=u\"easy_install-\")\n        try:\n            # cast to str as workaround for #709 and #710 and #712\n            yield str(tmpdir)\n        finally:\n            os.path.exists(tmpdir) and rmtree(rmtree_safe(tmpdir))\n\n    def easy_install(self, spec, deps=False):\n        if not self.editable:\n            self.install_site_py()\n\n        with self._tmpdir() as tmpdir:\n            if not isinstance(spec, Requirement):\n                if URL_SCHEME(spec):\n                    # It's a url, download it to tmpdir and process\n                    self.not_editable(spec)\n                    dl = self.package_index.download(spec, tmpdir)\n                    return self.install_item(None, dl, tmpdir, deps, True)\n\n                elif os.path.exists(spec):\n                    # Existing file or directory, just process it directly\n                    self.not_editable(spec)\n                    return self.install_item(None, spec, tmpdir, deps, True)\n                else:\n                    spec = parse_requirement_arg(spec)\n\n            self.check_editable(spec)\n            dist = self.package_index.fetch_distribution(\n                spec, tmpdir, self.upgrade, self.editable,\n                not self.always_copy, self.local_index\n            )\n            if dist is None:\n                msg = \"Could not find suitable distribution for %r\" % spec\n                if self.always_copy:\n                    msg += \" (--always-copy skips system and development eggs)\"\n                raise DistutilsError(msg)\n            elif dist.precedence == DEVELOP_DIST:\n                # .egg-info dists don't need installing, just process deps\n                self.process_distribution(spec, dist, deps, \"Using\")\n                return dist\n            else:\n                return self.install_item(spec, dist.location, tmpdir, deps)\n\n    def install_item(self, spec, download, tmpdir, deps, install_needed=False):\n\n        # Installation is also needed if file in tmpdir or is not an egg\n        install_needed = install_needed or self.always_copy\n        install_needed = install_needed or os.path.dirname(download) == tmpdir\n        install_needed = install_needed or not download.endswith('.egg')\n        install_needed = install_needed or (\n            self.always_copy_from is not None and\n            os.path.dirname(normalize_path(download)) ==\n            normalize_path(self.always_copy_from)\n        )\n\n        if spec and not install_needed:\n            # at this point, we know it's a local .egg, we just don't know if\n            # it's already installed.\n            for dist in self.local_index[spec.project_name]:\n                if dist.location == download:\n                    break\n            else:\n                install_needed = True  # it's not in the local index\n\n        log.info(\"Processing %s\", os.path.basename(download))\n\n        if install_needed:\n            dists = self.install_eggs(spec, download, tmpdir)\n            for dist in dists:\n                self.process_distribution(spec, dist, deps)\n        else:\n            dists = [self.egg_distribution(download)]\n            self.process_distribution(spec, dists[0], deps, \"Using\")\n\n        if spec is not None:\n            for dist in dists:\n                if dist in spec:\n                    return dist\n\n    def select_scheme(self, name):\n        \"\"\"Sets the install directories by applying the install schemes.\"\"\"\n        # it's the caller's problem if they supply a bad name!\n        scheme = INSTALL_SCHEMES[name]\n        for key in SCHEME_KEYS:\n            attrname = 'install_' + key\n            if getattr(self, attrname) is None:\n                setattr(self, attrname, scheme[key])\n\n    def process_distribution(self, requirement, dist, deps=True, *info):\n        self.update_pth(dist)\n        self.package_index.add(dist)\n        if dist in self.local_index[dist.key]:\n            self.local_index.remove(dist)\n        self.local_index.add(dist)\n        self.install_egg_scripts(dist)\n        self.installed_projects[dist.key] = dist\n        log.info(self.installation_report(requirement, dist, *info))\n        if (dist.has_metadata('dependency_links.txt') and\n                not self.no_find_links):\n            self.package_index.add_find_links(\n                dist.get_metadata_lines('dependency_links.txt')\n            )\n        if not deps and not self.always_copy:\n            return\n        elif requirement is not None and dist.key != requirement.key:\n            log.warn(\"Skipping dependencies for %s\", dist)\n            return  # XXX this is not the distribution we were looking for\n        elif requirement is None or dist not in requirement:\n            # if we wound up with a different version, resolve what we've got\n            distreq = dist.as_requirement()\n            requirement = Requirement(str(distreq))\n        log.info(\"Processing dependencies for %s\", requirement)\n        try:\n            distros = WorkingSet([]).resolve(\n                [requirement], self.local_index, self.easy_install\n            )\n        except DistributionNotFound as e:\n            raise DistutilsError(str(e))\n        except VersionConflict as e:\n            raise DistutilsError(e.report())\n        if self.always_copy or self.always_copy_from:\n            # Force all the relevant distros to be copied or activated\n            for dist in distros:\n                if dist.key not in self.installed_projects:\n                    self.easy_install(dist.as_requirement())\n        log.info(\"Finished processing dependencies for %s\", requirement)\n\n    def should_unzip(self, dist):\n        if self.zip_ok is not None:\n            return not self.zip_ok\n        if dist.has_metadata('not-zip-safe'):\n            return True\n        if not dist.has_metadata('zip-safe'):\n            return True\n        return False\n\n    def maybe_move(self, spec, dist_filename, setup_base):\n        dst = os.path.join(self.build_directory, spec.key)\n        if os.path.exists(dst):\n            msg = (\n                \"%r already exists in %s; build directory %s will not be kept\"\n            )\n            log.warn(msg, spec.key, self.build_directory, setup_base)\n            return setup_base\n        if os.path.isdir(dist_filename):\n            setup_base = dist_filename\n        else:\n            if os.path.dirname(dist_filename) == setup_base:\n                os.unlink(dist_filename)  # get it out of the tmp dir\n            contents = os.listdir(setup_base)\n            if len(contents) == 1:\n                dist_filename = os.path.join(setup_base, contents[0])\n                if os.path.isdir(dist_filename):\n                    # if the only thing there is a directory, move it instead\n                    setup_base = dist_filename\n        ensure_directory(dst)\n        shutil.move(setup_base, dst)\n        return dst\n\n    def install_wrapper_scripts(self, dist):\n        if self.exclude_scripts:\n            return\n        for args in ScriptWriter.best().get_args(dist):\n            self.write_script(*args)\n\n    def install_script(self, dist, script_name, script_text, dev_path=None):\n        \"\"\"Generate a legacy script wrapper and install it\"\"\"\n        spec = str(dist.as_requirement())\n        is_script = is_python_script(script_text, script_name)\n\n        if is_script:\n            body = self._load_template(dev_path) % locals()\n            script_text = ScriptWriter.get_header(script_text) + body\n        self.write_script(script_name, _to_bytes(script_text), 'b')\n\n    @staticmethod\n    def _load_template(dev_path):\n        \"\"\"\n        There are a couple of template scripts in the package. This\n        function loads one of them and prepares it for use.\n        \"\"\"\n        # See https://github.com/pypa/setuptools/issues/134 for info\n        # on script file naming and downstream issues with SVR4\n        name = 'script.tmpl'\n        if dev_path:\n            name = name.replace('.tmpl', ' (dev).tmpl')\n\n        raw_bytes = resource_string('setuptools', name)\n        return raw_bytes.decode('utf-8')\n\n    def write_script(self, script_name, contents, mode=\"t\", blockers=()):\n        \"\"\"Write an executable file to the scripts directory\"\"\"\n        self.delete_blockers(  # clean up old .py/.pyw w/o a script\n            [os.path.join(self.script_dir, x) for x in blockers]\n        )\n        log.info(\"Installing %s script to %s\", script_name, self.script_dir)\n        target = os.path.join(self.script_dir, script_name)\n        self.add_output(target)\n\n        if self.dry_run:\n            return\n\n        mask = current_umask()\n        ensure_directory(target)\n        if os.path.exists(target):\n            os.unlink(target)\n        with open(target, \"w\" + mode) as f:\n            f.write(contents)\n        chmod(target, 0o777 - mask)\n\n    def install_eggs(self, spec, dist_filename, tmpdir):\n        # .egg dirs or files are already built, so just return them\n        if dist_filename.lower().endswith('.egg'):\n            return [self.install_egg(dist_filename, tmpdir)]\n        elif dist_filename.lower().endswith('.exe'):\n            return [self.install_exe(dist_filename, tmpdir)]\n        elif dist_filename.lower().endswith('.whl'):\n            return [self.install_wheel(dist_filename, tmpdir)]\n\n        # Anything else, try to extract and build\n        setup_base = tmpdir\n        if os.path.isfile(dist_filename) and not dist_filename.endswith('.py'):\n            unpack_archive(dist_filename, tmpdir, self.unpack_progress)\n        elif os.path.isdir(dist_filename):\n            setup_base = os.path.abspath(dist_filename)\n\n        if (setup_base.startswith(tmpdir)  # something we downloaded\n                and self.build_directory and spec is not None):\n            setup_base = self.maybe_move(spec, dist_filename, setup_base)\n\n        # Find the setup.py file\n        setup_script = os.path.join(setup_base, 'setup.py')\n\n        if not os.path.exists(setup_script):\n            setups = glob(os.path.join(setup_base, '*', 'setup.py'))\n            if not setups:\n                raise DistutilsError(\n                    \"Couldn't find a setup script in %s\" %\n                    os.path.abspath(dist_filename)\n                )\n            if len(setups) > 1:\n                raise DistutilsError(\n                    \"Multiple setup scripts in %s\" %\n                    os.path.abspath(dist_filename)\n                )\n            setup_script = setups[0]\n\n        # Now run it, and return the result\n        if self.editable:\n            log.info(self.report_editable(spec, setup_script))\n            return []\n        else:\n            return self.build_and_install(setup_script, setup_base)\n\n    def egg_distribution(self, egg_path):\n        if os.path.isdir(egg_path):\n            metadata = PathMetadata(egg_path, os.path.join(egg_path,\n                                                           'EGG-INFO'))\n        else:\n            metadata = EggMetadata(zipimport.zipimporter(egg_path))\n        return Distribution.from_filename(egg_path, metadata=metadata)\n\n    def install_egg(self, egg_path, tmpdir):\n        destination = os.path.join(\n            self.install_dir,\n            os.path.basename(egg_path),\n        )\n        destination = os.path.abspath(destination)\n        if not self.dry_run:\n            ensure_directory(destination)\n\n        dist = self.egg_distribution(egg_path)\n        if not samefile(egg_path, destination):\n            if os.path.isdir(destination) and not os.path.islink(destination):\n                dir_util.remove_tree(destination, dry_run=self.dry_run)\n            elif os.path.exists(destination):\n                self.execute(\n                    os.unlink,\n                    (destination,),\n                    \"Removing \" + destination,\n                )\n            try:\n                new_dist_is_zipped = False\n                if os.path.isdir(egg_path):\n                    if egg_path.startswith(tmpdir):\n                        f, m = shutil.move, \"Moving\"\n                    else:\n                        f, m = shutil.copytree, \"Copying\"\n                elif self.should_unzip(dist):\n                    self.mkpath(destination)\n                    f, m = self.unpack_and_compile, \"Extracting\"\n                else:\n                    new_dist_is_zipped = True\n                    if egg_path.startswith(tmpdir):\n                        f, m = shutil.move, \"Moving\"\n                    else:\n                        f, m = shutil.copy2, \"Copying\"\n                self.execute(\n                    f,\n                    (egg_path, destination),\n                    (m + \" %s to %s\") % (\n                        os.path.basename(egg_path),\n                        os.path.dirname(destination)\n                    ),\n                )\n                update_dist_caches(\n                    destination,\n                    fix_zipimporter_caches=new_dist_is_zipped,\n                )\n            except Exception:\n                update_dist_caches(destination, fix_zipimporter_caches=False)\n                raise\n\n        self.add_output(destination)\n        return self.egg_distribution(destination)\n\n    def install_exe(self, dist_filename, tmpdir):\n        # See if it's valid, get data\n        cfg = extract_wininst_cfg(dist_filename)\n        if cfg is None:\n            raise DistutilsError(\n                \"%s is not a valid distutils Windows .exe\" % dist_filename\n            )\n        # Create a dummy distribution object until we build the real distro\n        dist = Distribution(\n            None,\n            project_name=cfg.get('metadata', 'name'),\n            version=cfg.get('metadata', 'version'), platform=get_platform(),\n        )\n\n        # Convert the .exe to an unpacked egg\n        egg_path = os.path.join(tmpdir, dist.egg_name() + '.egg')\n        dist.location = egg_path\n        egg_tmp = egg_path + '.tmp'\n        _egg_info = os.path.join(egg_tmp, 'EGG-INFO')\n        pkg_inf = os.path.join(_egg_info, 'PKG-INFO')\n        ensure_directory(pkg_inf)  # make sure EGG-INFO dir exists\n        dist._provider = PathMetadata(egg_tmp, _egg_info)  # XXX\n        self.exe_to_egg(dist_filename, egg_tmp)\n\n        # Write EGG-INFO/PKG-INFO\n        if not os.path.exists(pkg_inf):\n            f = open(pkg_inf, 'w')\n            f.write('Metadata-Version: 1.0\\n')\n            for k, v in cfg.items('metadata'):\n                if k != 'target_version':\n                    f.write('%s: %s\\n' % (k.replace('_', '-').title(), v))\n            f.close()\n        script_dir = os.path.join(_egg_info, 'scripts')\n        # delete entry-point scripts to avoid duping\n        self.delete_blockers([\n            os.path.join(script_dir, args[0])\n            for args in ScriptWriter.get_args(dist)\n        ])\n        # Build .egg file from tmpdir\n        bdist_egg.make_zipfile(\n            egg_path, egg_tmp, verbose=self.verbose, dry_run=self.dry_run,\n        )\n        # install the .egg\n        return self.install_egg(egg_path, tmpdir)\n\n    def exe_to_egg(self, dist_filename, egg_tmp):\n        \"\"\"Extract a bdist_wininst to the directories an egg would use\"\"\"\n        # Check for .pth file and set up prefix translations\n        prefixes = get_exe_prefixes(dist_filename)\n        to_compile = []\n        native_libs = []\n        top_level = {}\n\n        def process(src, dst):\n            s = src.lower()\n            for old, new in prefixes:\n                if s.startswith(old):\n                    src = new + src[len(old):]\n                    parts = src.split('/')\n                    dst = os.path.join(egg_tmp, *parts)\n                    dl = dst.lower()\n                    if dl.endswith('.pyd') or dl.endswith('.dll'):\n                        parts[-1] = bdist_egg.strip_module(parts[-1])\n                        top_level[os.path.splitext(parts[0])[0]] = 1\n                        native_libs.append(src)\n                    elif dl.endswith('.py') and old != 'SCRIPTS/':\n                        top_level[os.path.splitext(parts[0])[0]] = 1\n                        to_compile.append(dst)\n                    return dst\n            if not src.endswith('.pth'):\n                log.warn(\"WARNING: can't process %s\", src)\n            return None\n\n        # extract, tracking .pyd/.dll->native_libs and .py -> to_compile\n        unpack_archive(dist_filename, egg_tmp, process)\n        stubs = []\n        for res in native_libs:\n            if res.lower().endswith('.pyd'):  # create stubs for .pyd's\n                parts = res.split('/')\n                resource = parts[-1]\n                parts[-1] = bdist_egg.strip_module(parts[-1]) + '.py'\n                pyfile = os.path.join(egg_tmp, *parts)\n                to_compile.append(pyfile)\n                stubs.append(pyfile)\n                bdist_egg.write_stub(resource, pyfile)\n        self.byte_compile(to_compile)  # compile .py's\n        bdist_egg.write_safety_flag(\n            os.path.join(egg_tmp, 'EGG-INFO'),\n            bdist_egg.analyze_egg(egg_tmp, stubs))  # write zip-safety flag\n\n        for name in 'top_level', 'native_libs':\n            if locals()[name]:\n                txt = os.path.join(egg_tmp, 'EGG-INFO', name + '.txt')\n                if not os.path.exists(txt):\n                    f = open(txt, 'w')\n                    f.write('\\n'.join(locals()[name]) + '\\n')\n                    f.close()\n\n    def install_wheel(self, wheel_path, tmpdir):\n        wheel = Wheel(wheel_path)\n        assert wheel.is_compatible()\n        destination = os.path.join(self.install_dir, wheel.egg_name())\n        destination = os.path.abspath(destination)\n        if not self.dry_run:\n            ensure_directory(destination)\n        if os.path.isdir(destination) and not os.path.islink(destination):\n            dir_util.remove_tree(destination, dry_run=self.dry_run)\n        elif os.path.exists(destination):\n            self.execute(\n                os.unlink,\n                (destination,),\n                \"Removing \" + destination,\n            )\n        try:\n            self.execute(\n                wheel.install_as_egg,\n                (destination,),\n                (\"Installing %s to %s\") % (\n                    os.path.basename(wheel_path),\n                    os.path.dirname(destination)\n                ),\n            )\n        finally:\n            update_dist_caches(destination, fix_zipimporter_caches=False)\n        self.add_output(destination)\n        return self.egg_distribution(destination)\n\n    __mv_warning = textwrap.dedent(\"\"\"\n        Because this distribution was installed --multi-version, before you can\n        import modules from this package in an application, you will need to\n        'import pkg_resources' and then use a 'require()' call similar to one of\n        these examples, in order to select the desired version:\n\n            pkg_resources.require(\"%(name)s\")  # latest installed version\n            pkg_resources.require(\"%(name)s==%(version)s\")  # this exact version\n            pkg_resources.require(\"%(name)s>=%(version)s\")  # this version or higher\n        \"\"\").lstrip()\n\n    __id_warning = textwrap.dedent(\"\"\"\n        Note also that the installation directory must be on sys.path at runtime for\n        this to work.  (e.g. by being the application's script directory, by being on\n        PYTHONPATH, or by being added to sys.path by your code.)\n        \"\"\")\n\n    def installation_report(self, req, dist, what=\"Installed\"):\n        \"\"\"Helpful installation message for display to package users\"\"\"\n        msg = \"\\n%(what)s %(eggloc)s%(extras)s\"\n        if self.multi_version and not self.no_report:\n            msg += '\\n' + self.__mv_warning\n            if self.install_dir not in map(normalize_path, sys.path):\n                msg += '\\n' + self.__id_warning\n\n        eggloc = dist.location\n        name = dist.project_name\n        version = dist.version\n        extras = ''  # TODO: self.report_extras(req, dist)\n        return msg % locals()\n\n    __editable_msg = textwrap.dedent(\"\"\"\n        Extracted editable version of %(spec)s to %(dirname)s\n\n        If it uses setuptools in its setup script, you can activate it in\n        \"development\" mode by going to that directory and running::\n\n            %(python)s setup.py develop\n\n        See the setuptools documentation for the \"develop\" command for more info.\n        \"\"\").lstrip()\n\n    def report_editable(self, spec, setup_script):\n        dirname = os.path.dirname(setup_script)\n        python = sys.executable\n        return '\\n' + self.__editable_msg % locals()\n\n    def run_setup(self, setup_script, setup_base, args):\n        sys.modules.setdefault('distutils.command.bdist_egg', bdist_egg)\n        sys.modules.setdefault('distutils.command.egg_info', egg_info)\n\n        args = list(args)\n        if self.verbose > 2:\n            v = 'v' * (self.verbose - 1)\n            args.insert(0, '-' + v)\n        elif self.verbose < 2:\n            args.insert(0, '-q')\n        if self.dry_run:\n            args.insert(0, '-n')\n        log.info(\n            \"Running %s %s\", setup_script[len(setup_base) + 1:], ' '.join(args)\n        )\n        try:\n            run_setup(setup_script, args)\n        except SystemExit as v:\n            raise DistutilsError(\"Setup script exited with %s\" % (v.args[0],))\n\n    def build_and_install(self, setup_script, setup_base):\n        args = ['bdist_egg', '--dist-dir']\n\n        dist_dir = tempfile.mkdtemp(\n            prefix='egg-dist-tmp-', dir=os.path.dirname(setup_script)\n        )\n        try:\n            self._set_fetcher_options(os.path.dirname(setup_script))\n            args.append(dist_dir)\n\n            self.run_setup(setup_script, setup_base, args)\n            all_eggs = Environment([dist_dir])\n            eggs = []\n            for key in all_eggs:\n                for dist in all_eggs[key]:\n                    eggs.append(self.install_egg(dist.location, setup_base))\n            if not eggs and not self.dry_run:\n                log.warn(\"No eggs found in %s (setup script problem?)\",\n                         dist_dir)\n            return eggs\n        finally:\n            rmtree(dist_dir)\n            log.set_verbosity(self.verbose)  # restore our log verbosity\n\n    def _set_fetcher_options(self, base):\n        \"\"\"\n        When easy_install is about to run bdist_egg on a source dist, that\n        source dist might have 'setup_requires' directives, requiring\n        additional fetching. Ensure the fetcher options given to easy_install\n        are available to that command as well.\n        \"\"\"\n        # find the fetch options from easy_install and write them out\n        # to the setup.cfg file.\n        ei_opts = self.distribution.get_option_dict('easy_install').copy()\n        fetch_directives = (\n            'find_links', 'site_dirs', 'index_url', 'optimize', 'allow_hosts',\n        )\n        fetch_options = {}\n        for key, val in ei_opts.items():\n            if key not in fetch_directives:\n                continue\n            fetch_options[key.replace('_', '-')] = val[1]\n        # create a settings dictionary suitable for `edit_config`\n        settings = dict(easy_install=fetch_options)\n        cfg_filename = os.path.join(base, 'setup.cfg')\n        setopt.edit_config(cfg_filename, settings)\n\n    def update_pth(self, dist):\n        if self.pth_file is None:\n            return\n\n        for d in self.pth_file[dist.key]:  # drop old entries\n            if self.multi_version or d.location != dist.location:\n                log.info(\"Removing %s from easy-install.pth file\", d)\n                self.pth_file.remove(d)\n                if d.location in self.shadow_path:\n                    self.shadow_path.remove(d.location)\n\n        if not self.multi_version:\n            if dist.location in self.pth_file.paths:\n                log.info(\n                    \"%s is already the active version in easy-install.pth\",\n                    dist,\n                )\n            else:\n                log.info(\"Adding %s to easy-install.pth file\", dist)\n                self.pth_file.add(dist)  # add new entry\n                if dist.location not in self.shadow_path:\n                    self.shadow_path.append(dist.location)\n\n        if not self.dry_run:\n\n            self.pth_file.save()\n\n            if dist.key == 'setuptools':\n                # Ensure that setuptools itself never becomes unavailable!\n                # XXX should this check for latest version?\n                filename = os.path.join(self.install_dir, 'setuptools.pth')\n                if os.path.islink(filename):\n                    os.unlink(filename)\n                f = open(filename, 'wt')\n                f.write(self.pth_file.make_relative(dist.location) + '\\n')\n                f.close()\n\n    def unpack_progress(self, src, dst):\n        # Progress filter for unpacking\n        log.debug(\"Unpacking %s to %s\", src, dst)\n        return dst  # only unpack-and-compile skips files for dry run\n\n    def unpack_and_compile(self, egg_path, destination):\n        to_compile = []\n        to_chmod = []\n\n        def pf(src, dst):\n            if dst.endswith('.py') and not src.startswith('EGG-INFO/'):\n                to_compile.append(dst)\n            elif dst.endswith('.dll') or dst.endswith('.so'):\n                to_chmod.append(dst)\n            self.unpack_progress(src, dst)\n            return not self.dry_run and dst or None\n\n        unpack_archive(egg_path, destination, pf)\n        self.byte_compile(to_compile)\n        if not self.dry_run:\n            for f in to_chmod:\n                mode = ((os.stat(f)[stat.ST_MODE]) | 0o555) & 0o7755\n                chmod(f, mode)\n\n    def byte_compile(self, to_compile):\n        if sys.dont_write_bytecode:\n            return\n\n        from distutils.util import byte_compile\n\n        try:\n            # try to make the byte compile messages quieter\n            log.set_verbosity(self.verbose - 1)\n\n            byte_compile(to_compile, optimize=0, force=1, dry_run=self.dry_run)\n            if self.optimize:\n                byte_compile(\n                    to_compile, optimize=self.optimize, force=1,\n                    dry_run=self.dry_run,\n                )\n        finally:\n            log.set_verbosity(self.verbose)  # restore original verbosity\n\n    __no_default_msg = textwrap.dedent(\"\"\"\n        bad install directory or PYTHONPATH\n\n        You are attempting to install a package to a directory that is not\n        on PYTHONPATH and which Python does not read \".pth\" files from.  The\n        installation directory you specified (via --install-dir, --prefix, or\n        the distutils default setting) was:\n\n            %s\n\n        and your PYTHONPATH environment variable currently contains:\n\n            %r\n\n        Here are some of your options for correcting the problem:\n\n        * You can choose a different installation directory, i.e., one that is\n          on PYTHONPATH or supports .pth files\n\n        * You can add the installation directory to the PYTHONPATH environment\n          variable.  (It must then also be on PYTHONPATH whenever you run\n          Python and want to use the package(s) you are installing.)\n\n        * You can set up the installation directory to support \".pth\" files by\n          using one of the approaches described here:\n\n          https://setuptools.readthedocs.io/en/latest/easy_install.html#custom-installation-locations\n\n\n        Please make the appropriate changes for your system and try again.\"\"\").lstrip()\n\n    def no_default_version_msg(self):\n        template = self.__no_default_msg\n        return template % (self.install_dir, os.environ.get('PYTHONPATH', ''))\n\n    def install_site_py(self):\n        \"\"\"Make sure there's a site.py in the target dir, if needed\"\"\"\n\n        if self.sitepy_installed:\n            return  # already did it, or don't need to\n\n        sitepy = os.path.join(self.install_dir, \"site.py\")\n        source = resource_string(\"setuptools\", \"site-patch.py\")\n        source = source.decode('utf-8')\n        current = \"\"\n\n        if os.path.exists(sitepy):\n            log.debug(\"Checking existing site.py in %s\", self.install_dir)\n            with io.open(sitepy) as strm:\n                current = strm.read()\n\n            if not current.startswith('def __boot():'):\n                raise DistutilsError(\n                    \"%s is not a setuptools-generated site.py; please\"\n                    \" remove it.\" % sitepy\n                )\n\n        if current != source:\n            log.info(\"Creating %s\", sitepy)\n            if not self.dry_run:\n                ensure_directory(sitepy)\n                with io.open(sitepy, 'w', encoding='utf-8') as strm:\n                    strm.write(source)\n            self.byte_compile([sitepy])\n\n        self.sitepy_installed = True\n\n    def create_home_path(self):\n        \"\"\"Create directories under ~.\"\"\"\n        if not self.user:\n            return\n        home = convert_path(os.path.expanduser(\"~\"))\n        for name, path in six.iteritems(self.config_vars):\n            if path.startswith(home) and not os.path.isdir(path):\n                self.debug_print(\"os.makedirs('%s', 0o700)\" % path)\n                os.makedirs(path, 0o700)\n\n    INSTALL_SCHEMES = dict(\n        posix=dict(\n            install_dir='$base/lib/python$py_version_short/site-packages',\n            script_dir='$base/bin',\n        ),\n    )\n\n    DEFAULT_SCHEME = dict(\n        install_dir='$base/Lib/site-packages',\n        script_dir='$base/Scripts',\n    )\n\n    def _expand(self, *attrs):\n        config_vars = self.get_finalized_command('install').config_vars\n\n        if self.prefix:\n            # Set default install_dir/scripts from --prefix\n            config_vars = config_vars.copy()\n            config_vars['base'] = self.prefix\n            scheme = self.INSTALL_SCHEMES.get(os.name, self.DEFAULT_SCHEME)\n            for attr, val in scheme.items():\n                if getattr(self, attr, None) is None:\n                    setattr(self, attr, val)\n\n        from distutils.util import subst_vars\n\n        for attr in attrs:\n            val = getattr(self, attr)\n            if val is not None:\n                val = subst_vars(val, config_vars)\n                if os.name == 'posix':\n                    val = os.path.expanduser(val)\n                setattr(self, attr, val)\n\n\ndef _pythonpath():\n    items = os.environ.get('PYTHONPATH', '').split(os.pathsep)\n    return filter(None, items)\n\n\ndef get_site_dirs():\n    \"\"\"\n    Return a list of 'site' dirs\n    \"\"\"\n\n    sitedirs = []\n\n    # start with PYTHONPATH\n    sitedirs.extend(_pythonpath())\n\n    prefixes = [sys.prefix]\n    if sys.exec_prefix != sys.prefix:\n        prefixes.append(sys.exec_prefix)\n    for prefix in prefixes:\n        if prefix:\n            if sys.platform in ('os2emx', 'riscos'):\n                sitedirs.append(os.path.join(prefix, \"Lib\", \"site-packages\"))\n            elif os.sep == '/':\n                sitedirs.extend([\n                    os.path.join(\n                        prefix,\n                        \"lib\",\n                        \"python{}.{}\".format(*sys.version_info),\n                        \"site-packages\",\n                    ),\n                    os.path.join(prefix, \"lib\", \"site-python\"),\n                ])\n            else:\n                sitedirs.extend([\n                    prefix,\n                    os.path.join(prefix, \"lib\", \"site-packages\"),\n                ])\n            if sys.platform == 'darwin':\n                # for framework builds *only* we add the standard Apple\n                # locations. Currently only per-user, but /Library and\n                # /Network/Library could be added too\n                if 'Python.framework' in prefix:\n                    home = os.environ.get('HOME')\n                    if home:\n                        home_sp = os.path.join(\n                            home,\n                            'Library',\n                            'Python',\n                            '{}.{}'.format(*sys.version_info),\n                            'site-packages',\n                        )\n                        sitedirs.append(home_sp)\n    lib_paths = get_path('purelib'), get_path('platlib')\n    for site_lib in lib_paths:\n        if site_lib not in sitedirs:\n            sitedirs.append(site_lib)\n\n    if site.ENABLE_USER_SITE:\n        sitedirs.append(site.USER_SITE)\n\n    try:\n        sitedirs.extend(site.getsitepackages())\n    except AttributeError:\n        pass\n\n    sitedirs = list(map(normalize_path, sitedirs))\n\n    return sitedirs\n\n\ndef expand_paths(inputs):\n    \"\"\"Yield sys.path directories that might contain \"old-style\" packages\"\"\"\n\n    seen = {}\n\n    for dirname in inputs:\n        dirname = normalize_path(dirname)\n        if dirname in seen:\n            continue\n\n        seen[dirname] = 1\n        if not os.path.isdir(dirname):\n            continue\n\n        files = os.listdir(dirname)\n        yield dirname, files\n\n        for name in files:\n            if not name.endswith('.pth'):\n                # We only care about the .pth files\n                continue\n            if name in ('easy-install.pth', 'setuptools.pth'):\n                # Ignore .pth files that we control\n                continue\n\n            # Read the .pth file\n            f = open(os.path.join(dirname, name))\n            lines = list(yield_lines(f))\n            f.close()\n\n            # Yield existing non-dupe, non-import directory lines from it\n            for line in lines:\n                if not line.startswith(\"import\"):\n                    line = normalize_path(line.rstrip())\n                    if line not in seen:\n                        seen[line] = 1\n                        if not os.path.isdir(line):\n                            continue\n                        yield line, os.listdir(line)\n\n\ndef extract_wininst_cfg(dist_filename):\n    \"\"\"Extract configuration data from a bdist_wininst .exe\n\n    Returns a configparser.RawConfigParser, or None\n    \"\"\"\n    f = open(dist_filename, 'rb')\n    try:\n        endrec = zipfile._EndRecData(f)\n        if endrec is None:\n            return None\n\n        prepended = (endrec[9] - endrec[5]) - endrec[6]\n        if prepended < 12:  # no wininst data here\n            return None\n        f.seek(prepended - 12)\n\n        tag, cfglen, bmlen = struct.unpack(\"<iii\", f.read(12))\n        if tag not in (0x1234567A, 0x1234567B):\n            return None  # not a valid tag\n\n        f.seek(prepended - (12 + cfglen))\n        init = {'version': '', 'target_version': ''}\n        cfg = configparser.RawConfigParser(init)\n        try:\n            part = f.read(cfglen)\n            # Read up to the first null byte.\n            config = part.split(b'\\0', 1)[0]\n            # Now the config is in bytes, but for RawConfigParser, it should\n            #  be text, so decode it.\n            config = config.decode(sys.getfilesystemencoding())\n            cfg.readfp(six.StringIO(config))\n        except configparser.Error:\n            return None\n        if not cfg.has_section('metadata') or not cfg.has_section('Setup'):\n            return None\n        return cfg\n\n    finally:\n        f.close()\n\n\ndef get_exe_prefixes(exe_filename):\n    \"\"\"Get exe->egg path translations for a given .exe file\"\"\"\n\n    prefixes = [\n        ('PURELIB/', ''),\n        ('PLATLIB/pywin32_system32', ''),\n        ('PLATLIB/', ''),\n        ('SCRIPTS/', 'EGG-INFO/scripts/'),\n        ('DATA/lib/site-packages', ''),\n    ]\n    z = zipfile.ZipFile(exe_filename)\n    try:\n        for info in z.infolist():\n            name = info.filename\n            parts = name.split('/')\n            if len(parts) == 3 and parts[2] == 'PKG-INFO':\n                if parts[1].endswith('.egg-info'):\n                    prefixes.insert(0, ('/'.join(parts[:2]), 'EGG-INFO/'))\n                    break\n            if len(parts) != 2 or not name.endswith('.pth'):\n                continue\n            if name.endswith('-nspkg.pth'):\n                continue\n            if parts[0].upper() in ('PURELIB', 'PLATLIB'):\n                contents = z.read(name)\n                if not six.PY2:\n                    contents = contents.decode()\n                for pth in yield_lines(contents):\n                    pth = pth.strip().replace('\\\\', '/')\n                    if not pth.startswith('import'):\n                        prefixes.append((('%s/%s/' % (parts[0], pth)), ''))\n    finally:\n        z.close()\n    prefixes = [(x.lower(), y) for x, y in prefixes]\n    prefixes.sort()\n    prefixes.reverse()\n    return prefixes\n\n\nclass PthDistributions(Environment):\n    \"\"\"A .pth file with Distribution paths in it\"\"\"\n\n    dirty = False\n\n    def __init__(self, filename, sitedirs=()):\n        self.filename = filename\n        self.sitedirs = list(map(normalize_path, sitedirs))\n        self.basedir = normalize_path(os.path.dirname(self.filename))\n        self._load()\n        Environment.__init__(self, [], None, None)\n        for path in yield_lines(self.paths):\n            list(map(self.add, find_distributions(path, True)))\n\n    def _load(self):\n        self.paths = []\n        saw_import = False\n        seen = dict.fromkeys(self.sitedirs)\n        if os.path.isfile(self.filename):\n            f = open(self.filename, 'rt')\n            for line in f:\n                if line.startswith('import'):\n                    saw_import = True\n                    continue\n                path = line.rstrip()\n                self.paths.append(path)\n                if not path.strip() or path.strip().startswith('#'):\n                    continue\n                # skip non-existent paths, in case somebody deleted a package\n                # manually, and duplicate paths as well\n                path = self.paths[-1] = normalize_path(\n                    os.path.join(self.basedir, path)\n                )\n                if not os.path.exists(path) or path in seen:\n                    self.paths.pop()  # skip it\n                    self.dirty = True  # we cleaned up, so we're dirty now :)\n                    continue\n                seen[path] = 1\n            f.close()\n\n        if self.paths and not saw_import:\n            self.dirty = True  # ensure anything we touch has import wrappers\n        while self.paths and not self.paths[-1].strip():\n            self.paths.pop()\n\n    def save(self):\n        \"\"\"Write changed .pth file back to disk\"\"\"\n        if not self.dirty:\n            return\n\n        rel_paths = list(map(self.make_relative, self.paths))\n        if rel_paths:\n            log.debug(\"Saving %s\", self.filename)\n            lines = self._wrap_lines(rel_paths)\n            data = '\\n'.join(lines) + '\\n'\n\n            if os.path.islink(self.filename):\n                os.unlink(self.filename)\n            with open(self.filename, 'wt') as f:\n                f.write(data)\n\n        elif os.path.exists(self.filename):\n            log.debug(\"Deleting empty %s\", self.filename)\n            os.unlink(self.filename)\n\n        self.dirty = False\n\n    @staticmethod\n    def _wrap_lines(lines):\n        return lines\n\n    def add(self, dist):\n        \"\"\"Add `dist` to the distribution map\"\"\"\n        new_path = (\n            dist.location not in self.paths and (\n                dist.location not in self.sitedirs or\n                # account for '.' being in PYTHONPATH\n                dist.location == os.getcwd()\n            )\n        )\n        if new_path:\n            self.paths.append(dist.location)\n            self.dirty = True\n        Environment.add(self, dist)\n\n    def remove(self, dist):\n        \"\"\"Remove `dist` from the distribution map\"\"\"\n        while dist.location in self.paths:\n            self.paths.remove(dist.location)\n            self.dirty = True\n        Environment.remove(self, dist)\n\n    def make_relative(self, path):\n        npath, last = os.path.split(normalize_path(path))\n        baselen = len(self.basedir)\n        parts = [last]\n        sep = os.altsep == '/' and '/' or os.sep\n        while len(npath) >= baselen:\n            if npath == self.basedir:\n                parts.append(os.curdir)\n                parts.reverse()\n                return sep.join(parts)\n            npath, last = os.path.split(npath)\n            parts.append(last)\n        else:\n            return path\n\n\nclass RewritePthDistributions(PthDistributions):\n    @classmethod\n    def _wrap_lines(cls, lines):\n        yield cls.prelude\n        for line in lines:\n            yield line\n        yield cls.postlude\n\n    prelude = _one_liner(\"\"\"\n        import sys\n        sys.__plen = len(sys.path)\n        \"\"\")\n    postlude = _one_liner(\"\"\"\n        import sys\n        new = sys.path[sys.__plen:]\n        del sys.path[sys.__plen:]\n        p = getattr(sys, '__egginsert', 0)\n        sys.path[p:p] = new\n        sys.__egginsert = p + len(new)\n        \"\"\")\n\n\nif os.environ.get('SETUPTOOLS_SYS_PATH_TECHNIQUE', 'raw') == 'rewrite':\n    PthDistributions = RewritePthDistributions\n\n\ndef _first_line_re():\n    \"\"\"\n    Return a regular expression based on first_line_re suitable for matching\n    strings.\n    \"\"\"\n    if isinstance(first_line_re.pattern, str):\n        return first_line_re\n\n    # first_line_re in Python >=3.1.4 and >=3.2.1 is a bytes pattern.\n    return re.compile(first_line_re.pattern.decode())\n\n\ndef auto_chmod(func, arg, exc):\n    if func in [os.unlink, os.remove] and os.name == 'nt':\n        chmod(arg, stat.S_IWRITE)\n        return func(arg)\n    et, ev, _ = sys.exc_info()\n    six.reraise(et, (ev[0], ev[1] + (\" %s %s\" % (func, arg))))\n\n\ndef update_dist_caches(dist_path, fix_zipimporter_caches):\n    \"\"\"\n    Fix any globally cached `dist_path` related data\n\n    `dist_path` should be a path of a newly installed egg distribution (zipped\n    or unzipped).\n\n    sys.path_importer_cache contains finder objects that have been cached when\n    importing data from the original distribution. Any such finders need to be\n    cleared since the replacement distribution might be packaged differently,\n    e.g. a zipped egg distribution might get replaced with an unzipped egg\n    folder or vice versa. Having the old finders cached may then cause Python\n    to attempt loading modules from the replacement distribution using an\n    incorrect loader.\n\n    zipimport.zipimporter objects are Python loaders charged with importing\n    data packaged inside zip archives. If stale loaders referencing the\n    original distribution, are left behind, they can fail to load modules from\n    the replacement distribution. E.g. if an old zipimport.zipimporter instance\n    is used to load data from a new zipped egg archive, it may cause the\n    operation to attempt to locate the requested data in the wrong location -\n    one indicated by the original distribution's zip archive directory\n    information. Such an operation may then fail outright, e.g. report having\n    read a 'bad local file header', or even worse, it may fail silently &\n    return invalid data.\n\n    zipimport._zip_directory_cache contains cached zip archive directory\n    information for all existing zipimport.zipimporter instances and all such\n    instances connected to the same archive share the same cached directory\n    information.\n\n    If asked, and the underlying Python implementation allows it, we can fix\n    all existing zipimport.zipimporter instances instead of having to track\n    them down and remove them one by one, by updating their shared cached zip\n    archive directory information. This, of course, assumes that the\n    replacement distribution is packaged as a zipped egg.\n\n    If not asked to fix existing zipimport.zipimporter instances, we still do\n    our best to clear any remaining zipimport.zipimporter related cached data\n    that might somehow later get used when attempting to load data from the new\n    distribution and thus cause such load operations to fail. Note that when\n    tracking down such remaining stale data, we can not catch every conceivable\n    usage from here, and we clear only those that we know of and have found to\n    cause problems if left alive. Any remaining caches should be updated by\n    whomever is in charge of maintaining them, i.e. they should be ready to\n    handle us replacing their zip archives with new distributions at runtime.\n\n    \"\"\"\n    # There are several other known sources of stale zipimport.zipimporter\n    # instances that we do not clear here, but might if ever given a reason to\n    # do so:\n    # * Global setuptools pkg_resources.working_set (a.k.a. 'master working\n    # set') may contain distributions which may in turn contain their\n    #   zipimport.zipimporter loaders.\n    # * Several zipimport.zipimporter loaders held by local variables further\n    #   up the function call stack when running the setuptools installation.\n    # * Already loaded modules may have their __loader__ attribute set to the\n    #   exact loader instance used when importing them. Python 3.4 docs state\n    #   that this information is intended mostly for introspection and so is\n    #   not expected to cause us problems.\n    normalized_path = normalize_path(dist_path)\n    _uncache(normalized_path, sys.path_importer_cache)\n    if fix_zipimporter_caches:\n        _replace_zip_directory_cache_data(normalized_path)\n    else:\n        # Here, even though we do not want to fix existing and now stale\n        # zipimporter cache information, we still want to remove it. Related to\n        # Python's zip archive directory information cache, we clear each of\n        # its stale entries in two phases:\n        #   1. Clear the entry so attempting to access zip archive information\n        #      via any existing stale zipimport.zipimporter instances fails.\n        #   2. Remove the entry from the cache so any newly constructed\n        #      zipimport.zipimporter instances do not end up using old stale\n        #      zip archive directory information.\n        # This whole stale data removal step does not seem strictly necessary,\n        # but has been left in because it was done before we started replacing\n        # the zip archive directory information cache content if possible, and\n        # there are no relevant unit tests that we can depend on to tell us if\n        # this is really needed.\n        _remove_and_clear_zip_directory_cache_data(normalized_path)\n\n\ndef _collect_zipimporter_cache_entries(normalized_path, cache):\n    \"\"\"\n    Return zipimporter cache entry keys related to a given normalized path.\n\n    Alternative path spellings (e.g. those using different character case or\n    those using alternative path separators) related to the same path are\n    included. Any sub-path entries are included as well, i.e. those\n    corresponding to zip archives embedded in other zip archives.\n\n    \"\"\"\n    result = []\n    prefix_len = len(normalized_path)\n    for p in cache:\n        np = normalize_path(p)\n        if (np.startswith(normalized_path) and\n                np[prefix_len:prefix_len + 1] in (os.sep, '')):\n            result.append(p)\n    return result\n\n\ndef _update_zipimporter_cache(normalized_path, cache, updater=None):\n    \"\"\"\n    Update zipimporter cache data for a given normalized path.\n\n    Any sub-path entries are processed as well, i.e. those corresponding to zip\n    archives embedded in other zip archives.\n\n    Given updater is a callable taking a cache entry key and the original entry\n    (after already removing the entry from the cache), and expected to update\n    the entry and possibly return a new one to be inserted in its place.\n    Returning None indicates that the entry should not be replaced with a new\n    one. If no updater is given, the cache entries are simply removed without\n    any additional processing, the same as if the updater simply returned None.\n\n    \"\"\"\n    for p in _collect_zipimporter_cache_entries(normalized_path, cache):\n        # N.B. pypy's custom zipimport._zip_directory_cache implementation does\n        # not support the complete dict interface:\n        # * Does not support item assignment, thus not allowing this function\n        #    to be used only for removing existing cache entries.\n        #  * Does not support the dict.pop() method, forcing us to use the\n        #    get/del patterns instead. For more detailed information see the\n        #    following links:\n        #      https://github.com/pypa/setuptools/issues/202#issuecomment-202913420\n        #      http://bit.ly/2h9itJX\n        old_entry = cache[p]\n        del cache[p]\n        new_entry = updater and updater(p, old_entry)\n        if new_entry is not None:\n            cache[p] = new_entry\n\n\ndef _uncache(normalized_path, cache):\n    _update_zipimporter_cache(normalized_path, cache)\n\n\ndef _remove_and_clear_zip_directory_cache_data(normalized_path):\n    def clear_and_remove_cached_zip_archive_directory_data(path, old_entry):\n        old_entry.clear()\n\n    _update_zipimporter_cache(\n        normalized_path, zipimport._zip_directory_cache,\n        updater=clear_and_remove_cached_zip_archive_directory_data)\n\n\n# PyPy Python implementation does not allow directly writing to the\n# zipimport._zip_directory_cache and so prevents us from attempting to correct\n# its content. The best we can do there is clear the problematic cache content\n# and have PyPy repopulate it as needed. The downside is that if there are any\n# stale zipimport.zipimporter instances laying around, attempting to use them\n# will fail due to not having its zip archive directory information available\n# instead of being automatically corrected to use the new correct zip archive\n# directory information.\nif '__pypy__' in sys.builtin_module_names:\n    _replace_zip_directory_cache_data = \\\n        _remove_and_clear_zip_directory_cache_data\nelse:\n\n    def _replace_zip_directory_cache_data(normalized_path):\n        def replace_cached_zip_archive_directory_data(path, old_entry):\n            # N.B. In theory, we could load the zip directory information just\n            # once for all updated path spellings, and then copy it locally and\n            # update its contained path strings to contain the correct\n            # spelling, but that seems like a way too invasive move (this cache\n            # structure is not officially documented anywhere and could in\n            # theory change with new Python releases) for no significant\n            # benefit.\n            old_entry.clear()\n            zipimport.zipimporter(path)\n            old_entry.update(zipimport._zip_directory_cache[path])\n            return old_entry\n\n        _update_zipimporter_cache(\n            normalized_path, zipimport._zip_directory_cache,\n            updater=replace_cached_zip_archive_directory_data)\n\n\ndef is_python(text, filename='<string>'):\n    \"Is this string a valid Python script?\"\n    try:\n        compile(text, filename, 'exec')\n    except (SyntaxError, TypeError):\n        return False\n    else:\n        return True\n\n\ndef is_sh(executable):\n    \"\"\"Determine if the specified executable is a .sh (contains a #! line)\"\"\"\n    try:\n        with io.open(executable, encoding='latin-1') as fp:\n            magic = fp.read(2)\n    except (OSError, IOError):\n        return executable\n    return magic == '#!'\n\n\ndef nt_quote_arg(arg):\n    \"\"\"Quote a command line argument according to Windows parsing rules\"\"\"\n    return subprocess.list2cmdline([arg])\n\n\ndef is_python_script(script_text, filename):\n    \"\"\"Is this text, as a whole, a Python script? (as opposed to shell/bat/etc.\n    \"\"\"\n    if filename.endswith('.py') or filename.endswith('.pyw'):\n        return True  # extension says it's Python\n    if is_python(script_text, filename):\n        return True  # it's syntactically valid Python\n    if script_text.startswith('#!'):\n        # It begins with a '#!' line, so check if 'python' is in it somewhere\n        return 'python' in script_text.splitlines()[0].lower()\n\n    return False  # Not any Python I can recognize\n\n\ntry:\n    from os import chmod as _chmod\nexcept ImportError:\n    # Jython compatibility\n    def _chmod(*args):\n        pass\n\n\ndef chmod(path, mode):\n    log.debug(\"changing mode of %s to %o\", path, mode)\n    try:\n        _chmod(path, mode)\n    except os.error as e:\n        log.debug(\"chmod failed: %s\", e)\n\n\nclass CommandSpec(list):\n    \"\"\"\n    A command spec for a #! header, specified as a list of arguments akin to\n    those passed to Popen.\n    \"\"\"\n\n    options = []\n    split_args = dict()\n\n    @classmethod\n    def best(cls):\n        \"\"\"\n        Choose the best CommandSpec class based on environmental conditions.\n        \"\"\"\n        return cls\n\n    @classmethod\n    def _sys_executable(cls):\n        _default = os.path.normpath(sys.executable)\n        return os.environ.get('__PYVENV_LAUNCHER__', _default)\n\n    @classmethod\n    def from_param(cls, param):\n        \"\"\"\n        Construct a CommandSpec from a parameter to build_scripts, which may\n        be None.\n        \"\"\"\n        if isinstance(param, cls):\n            return param\n        if isinstance(param, list):\n            return cls(param)\n        if param is None:\n            return cls.from_environment()\n        # otherwise, assume it's a string.\n        return cls.from_string(param)\n\n    @classmethod\n    def from_environment(cls):\n        return cls([cls._sys_executable()])\n\n    @classmethod\n    def from_string(cls, string):\n        \"\"\"\n        Construct a command spec from a simple string representing a command\n        line parseable by shlex.split.\n        \"\"\"\n        items = shlex.split(string, **cls.split_args)\n        return cls(items)\n\n    def install_options(self, script_text):\n        self.options = shlex.split(self._extract_options(script_text))\n        cmdline = subprocess.list2cmdline(self)\n        if not isascii(cmdline):\n            self.options[:0] = ['-x']\n\n    @staticmethod\n    def _extract_options(orig_script):\n        \"\"\"\n        Extract any options from the first line of the script.\n        \"\"\"\n        first = (orig_script + '\\n').splitlines()[0]\n        match = _first_line_re().match(first)\n        options = match.group(1) or '' if match else ''\n        return options.strip()\n\n    def as_header(self):\n        return self._render(self + list(self.options))\n\n    @staticmethod\n    def _strip_quotes(item):\n        _QUOTES = '\"\\''\n        for q in _QUOTES:\n            if item.startswith(q) and item.endswith(q):\n                return item[1:-1]\n        return item\n\n    @staticmethod\n    def _render(items):\n        cmdline = subprocess.list2cmdline(\n            CommandSpec._strip_quotes(item.strip()) for item in items)\n        return '#!' + cmdline + '\\n'\n\n\n# For pbr compat; will be removed in a future version.\nsys_executable = CommandSpec._sys_executable()\n\n\nclass WindowsCommandSpec(CommandSpec):\n    split_args = dict(posix=False)\n\n\nclass ScriptWriter:\n    \"\"\"\n    Encapsulates behavior around writing entry point scripts for console and\n    gui apps.\n    \"\"\"\n\n    template = textwrap.dedent(r\"\"\"\n        # EASY-INSTALL-ENTRY-SCRIPT: %(spec)r,%(group)r,%(name)r\n        __requires__ = %(spec)r\n        import re\n        import sys\n        from pkg_resources import load_entry_point\n\n        if __name__ == '__main__':\n            sys.argv[0] = re.sub(r'(-script\\.pyw?|\\.exe)?$', '', sys.argv[0])\n            sys.exit(\n                load_entry_point(%(spec)r, %(group)r, %(name)r)()\n            )\n    \"\"\").lstrip()\n\n    command_spec_class = CommandSpec\n\n    @classmethod\n    def get_script_args(cls, dist, executable=None, wininst=False):\n        # for backward compatibility\n        warnings.warn(\"Use get_args\", EasyInstallDeprecationWarning)\n        writer = (WindowsScriptWriter if wininst else ScriptWriter).best()\n        header = cls.get_script_header(\"\", executable, wininst)\n        return writer.get_args(dist, header)\n\n    @classmethod\n    def get_script_header(cls, script_text, executable=None, wininst=False):\n        # for backward compatibility\n        warnings.warn(\"Use get_header\", EasyInstallDeprecationWarning, stacklevel=2)\n        if wininst:\n            executable = \"python.exe\"\n        return cls.get_header(script_text, executable)\n\n    @classmethod\n    def get_args(cls, dist, header=None):\n        \"\"\"\n        Yield write_script() argument tuples for a distribution's\n        console_scripts and gui_scripts entry points.\n        \"\"\"\n        if header is None:\n            header = cls.get_header()\n        spec = str(dist.as_requirement())\n        for type_ in 'console', 'gui':\n            group = type_ + '_scripts'\n            for name, ep in dist.get_entry_map(group).items():\n                cls._ensure_safe_name(name)\n                script_text = cls.template % locals()\n                args = cls._get_script_args(type_, name, header, script_text)\n                for res in args:\n                    yield res\n\n    @staticmethod\n    def _ensure_safe_name(name):\n        \"\"\"\n        Prevent paths in *_scripts entry point names.\n        \"\"\"\n        has_path_sep = re.search(r'[\\\\/]', name)\n        if has_path_sep:\n            raise ValueError(\"Path separators not allowed in script names\")\n\n    @classmethod\n    def get_writer(cls, force_windows):\n        # for backward compatibility\n        warnings.warn(\"Use best\", EasyInstallDeprecationWarning)\n        return WindowsScriptWriter.best() if force_windows else cls.best()\n\n    @classmethod\n    def best(cls):\n        \"\"\"\n        Select the best ScriptWriter for this environment.\n        \"\"\"\n        if sys.platform == 'win32' or (os.name == 'java' and os._name == 'nt'):\n            return WindowsScriptWriter.best()\n        else:\n            return cls\n\n    @classmethod\n    def _get_script_args(cls, type_, name, header, script_text):\n        # Simply write the stub with no extension.\n        yield (name, header + script_text)\n\n    @classmethod\n    def get_header(cls, script_text=\"\", executable=None):\n        \"\"\"Create a #! line, getting options (if any) from script_text\"\"\"\n        cmd = cls.command_spec_class.best().from_param(executable)\n        cmd.install_options(script_text)\n        return cmd.as_header()\n\n\nclass WindowsScriptWriter(ScriptWriter):\n    command_spec_class = WindowsCommandSpec\n\n    @classmethod\n    def get_writer(cls):\n        # for backward compatibility\n        warnings.warn(\"Use best\", EasyInstallDeprecationWarning)\n        return cls.best()\n\n    @classmethod\n    def best(cls):\n        \"\"\"\n        Select the best ScriptWriter suitable for Windows\n        \"\"\"\n        writer_lookup = dict(\n            executable=WindowsExecutableLauncherWriter,\n            natural=cls,\n        )\n        # for compatibility, use the executable launcher by default\n        launcher = os.environ.get('SETUPTOOLS_LAUNCHER', 'executable')\n        return writer_lookup[launcher]\n\n    @classmethod\n    def _get_script_args(cls, type_, name, header, script_text):\n        \"For Windows, add a .py extension\"\n        ext = dict(console='.pya', gui='.pyw')[type_]\n        if ext not in os.environ['PATHEXT'].lower().split(';'):\n            msg = (\n                \"{ext} not listed in PATHEXT; scripts will not be \"\n                \"recognized as executables.\"\n            ).format(**locals())\n            warnings.warn(msg, UserWarning)\n        old = ['.pya', '.py', '-script.py', '.pyc', '.pyo', '.pyw', '.exe']\n        old.remove(ext)\n        header = cls._adjust_header(type_, header)\n        blockers = [name + x for x in old]\n        yield name + ext, header + script_text, 't', blockers\n\n    @classmethod\n    def _adjust_header(cls, type_, orig_header):\n        \"\"\"\n        Make sure 'pythonw' is used for gui and and 'python' is used for\n        console (regardless of what sys.executable is).\n        \"\"\"\n        pattern = 'pythonw.exe'\n        repl = 'python.exe'\n        if type_ == 'gui':\n            pattern, repl = repl, pattern\n        pattern_ob = re.compile(re.escape(pattern), re.IGNORECASE)\n        new_header = pattern_ob.sub(string=orig_header, repl=repl)\n        return new_header if cls._use_header(new_header) else orig_header\n\n    @staticmethod\n    def _use_header(new_header):\n        \"\"\"\n        Should _adjust_header use the replaced header?\n\n        On non-windows systems, always use. On\n        Windows systems, only use the replaced header if it resolves\n        to an executable on the system.\n        \"\"\"\n        clean_header = new_header[2:-1].strip('\"')\n        return sys.platform != 'win32' or find_executable(clean_header)\n\n\nclass WindowsExecutableLauncherWriter(WindowsScriptWriter):\n    @classmethod\n    def _get_script_args(cls, type_, name, header, script_text):\n        \"\"\"\n        For Windows, add a .py extension and an .exe launcher\n        \"\"\"\n        if type_ == 'gui':\n            launcher_type = 'gui'\n            ext = '-script.pyw'\n            old = ['.pyw']\n        else:\n            launcher_type = 'cli'\n            ext = '-script.py'\n            old = ['.py', '.pyc', '.pyo']\n        hdr = cls._adjust_header(type_, header)\n        blockers = [name + x for x in old]\n        yield (name + ext, hdr + script_text, 't', blockers)\n        yield (\n            name + '.exe', get_win_launcher(launcher_type),\n            'b'  # write in binary mode\n        )\n        if not is_64bit():\n            # install a manifest for the launcher to prevent Windows\n            # from detecting it as an installer (which it will for\n            #  launchers like easy_install.exe). Consider only\n            #  adding a manifest for launchers detected as installers.\n            #  See Distribute #143 for details.\n            m_name = name + '.exe.manifest'\n            yield (m_name, load_launcher_manifest(name), 't')\n\n\n# for backward-compatibility\nget_script_args = ScriptWriter.get_script_args\nget_script_header = ScriptWriter.get_script_header\n\n\ndef get_win_launcher(type):\n    \"\"\"\n    Load the Windows launcher (executable) suitable for launching a script.\n\n    `type` should be either 'cli' or 'gui'\n\n    Returns the executable as a byte string.\n    \"\"\"\n    launcher_fn = '%s.exe' % type\n    if is_64bit():\n        launcher_fn = launcher_fn.replace(\".\", \"-64.\")\n    else:\n        launcher_fn = launcher_fn.replace(\".\", \"-32.\")\n    return resource_string('setuptools', launcher_fn)\n\n\ndef load_launcher_manifest(name):\n    manifest = pkg_resources.resource_string(__name__, 'launcher manifest.xml')\n    if six.PY2:\n        return manifest % vars()\n    else:\n        return manifest.decode('utf-8') % vars()\n\n\ndef rmtree(path, ignore_errors=False, onerror=auto_chmod):\n    return shutil.rmtree(path, ignore_errors, onerror)\n\n\ndef current_umask():\n    tmp = os.umask(0o022)\n    os.umask(tmp)\n    return tmp\n\n\ndef bootstrap():\n    # This function is called when setuptools*.egg is run using /bin/sh\n    import setuptools\n\n    argv0 = os.path.dirname(setuptools.__path__[0])\n    sys.argv[0] = argv0\n    sys.argv.append(argv0)\n    main()\n\n\ndef main(argv=None, **kw):\n    from setuptools import setup\n    from setuptools.dist import Distribution\n\n    class DistributionWithoutHelpCommands(Distribution):\n        common_usage = \"\"\n\n        def _show_help(self, *args, **kw):\n            with _patch_usage():\n                Distribution._show_help(self, *args, **kw)\n\n    if argv is None:\n        argv = sys.argv[1:]\n\n    with _patch_usage():\n        setup(\n            script_args=['-q', 'easy_install', '-v'] + argv,\n            script_name=sys.argv[0] or 'easy_install',\n            distclass=DistributionWithoutHelpCommands,\n            **kw\n        )\n\n\n@contextlib.contextmanager\ndef _patch_usage():\n    import distutils.core\n    USAGE = textwrap.dedent(\"\"\"\n        usage: %(script)s [options] requirement_or_url ...\n           or: %(script)s --help\n        \"\"\").lstrip()\n\n    def gen_usage(script_name):\n        return USAGE % dict(\n            script=os.path.basename(script_name),\n        )\n\n    saved = distutils.core.gen_usage\n    distutils.core.gen_usage = gen_usage\n    try:\n        yield\n    finally:\n        distutils.core.gen_usage = saved\n\nclass EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning):\n    \"\"\"Class for warning about deprecations in EasyInstall in SetupTools. Not ignored by default, unlike DeprecationWarning.\"\"\"\n    \n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/egg_info.py",
    "content": "\"\"\"setuptools.command.egg_info\n\nCreate a distribution's .egg-info directory and contents\"\"\"\n\nfrom distutils.filelist import FileList as _FileList\nfrom distutils.errors import DistutilsInternalError\nfrom distutils.util import convert_path\nfrom distutils import log\nimport distutils.errors\nimport distutils.filelist\nimport os\nimport re\nimport sys\nimport io\nimport warnings\nimport time\nimport collections\n\nfrom setuptools.extern import six\nfrom setuptools.extern.six.moves import map\n\nfrom setuptools import Command\nfrom setuptools.command.sdist import sdist\nfrom setuptools.command.sdist import walk_revctrl\nfrom setuptools.command.setopt import edit_config\nfrom setuptools.command import bdist_egg\nfrom pkg_resources import (\n    parse_requirements, safe_name, parse_version,\n    safe_version, yield_lines, EntryPoint, iter_entry_points, to_filename)\nimport setuptools.unicode_utils as unicode_utils\nfrom setuptools.glob import glob\n\nfrom setuptools.extern import packaging\nfrom setuptools import SetuptoolsDeprecationWarning\n\ndef translate_pattern(glob):\n    \"\"\"\n    Translate a file path glob like '*.txt' in to a regular expression.\n    This differs from fnmatch.translate which allows wildcards to match\n    directory separators. It also knows about '**/' which matches any number of\n    directories.\n    \"\"\"\n    pat = ''\n\n    # This will split on '/' within [character classes]. This is deliberate.\n    chunks = glob.split(os.path.sep)\n\n    sep = re.escape(os.sep)\n    valid_char = '[^%s]' % (sep,)\n\n    for c, chunk in enumerate(chunks):\n        last_chunk = c == len(chunks) - 1\n\n        # Chunks that are a literal ** are globstars. They match anything.\n        if chunk == '**':\n            if last_chunk:\n                # Match anything if this is the last component\n                pat += '.*'\n            else:\n                # Match '(name/)*'\n                pat += '(?:%s+%s)*' % (valid_char, sep)\n            continue  # Break here as the whole path component has been handled\n\n        # Find any special characters in the remainder\n        i = 0\n        chunk_len = len(chunk)\n        while i < chunk_len:\n            char = chunk[i]\n            if char == '*':\n                # Match any number of name characters\n                pat += valid_char + '*'\n            elif char == '?':\n                # Match a name character\n                pat += valid_char\n            elif char == '[':\n                # Character class\n                inner_i = i + 1\n                # Skip initial !/] chars\n                if inner_i < chunk_len and chunk[inner_i] == '!':\n                    inner_i = inner_i + 1\n                if inner_i < chunk_len and chunk[inner_i] == ']':\n                    inner_i = inner_i + 1\n\n                # Loop till the closing ] is found\n                while inner_i < chunk_len and chunk[inner_i] != ']':\n                    inner_i = inner_i + 1\n\n                if inner_i >= chunk_len:\n                    # Got to the end of the string without finding a closing ]\n                    # Do not treat this as a matching group, but as a literal [\n                    pat += re.escape(char)\n                else:\n                    # Grab the insides of the [brackets]\n                    inner = chunk[i + 1:inner_i]\n                    char_class = ''\n\n                    # Class negation\n                    if inner[0] == '!':\n                        char_class = '^'\n                        inner = inner[1:]\n\n                    char_class += re.escape(inner)\n                    pat += '[%s]' % (char_class,)\n\n                    # Skip to the end ]\n                    i = inner_i\n            else:\n                pat += re.escape(char)\n            i += 1\n\n        # Join each chunk with the dir separator\n        if not last_chunk:\n            pat += sep\n\n    pat += r'\\Z'\n    return re.compile(pat, flags=re.MULTILINE|re.DOTALL)\n\n\nclass InfoCommon:\n    tag_build = None\n    tag_date = None\n\n    @property\n    def name(self):\n        return safe_name(self.distribution.get_name())\n\n    def tagged_version(self):\n        version = self.distribution.get_version()\n        # egg_info may be called more than once for a distribution,\n        # in which case the version string already contains all tags.\n        if self.vtags and version.endswith(self.vtags):\n            return safe_version(version)\n        return safe_version(version + self.vtags)\n\n    def tags(self):\n        version = ''\n        if self.tag_build:\n            version += self.tag_build\n        if self.tag_date:\n            version += time.strftime(\"-%Y%m%d\")\n        return version\n    vtags = property(tags)\n\n\nclass egg_info(InfoCommon, Command):\n    description = \"create a distribution's .egg-info directory\"\n\n    user_options = [\n        ('egg-base=', 'e', \"directory containing .egg-info directories\"\n                           \" (default: top of the source tree)\"),\n        ('tag-date', 'd', \"Add date stamp (e.g. 20050528) to version number\"),\n        ('tag-build=', 'b', \"Specify explicit tag to add to version number\"),\n        ('no-date', 'D', \"Don't include date stamp [default]\"),\n    ]\n\n    boolean_options = ['tag-date']\n    negative_opt = {\n        'no-date': 'tag-date',\n    }\n\n    def initialize_options(self):\n        self.egg_base = None\n        self.egg_name = None\n        self.egg_info = None\n        self.egg_version = None\n        self.broken_egg_info = False\n\n    ####################################\n    # allow the 'tag_svn_revision' to be detected and\n    # set, supporting sdists built on older Setuptools.\n    @property\n    def tag_svn_revision(self):\n        pass\n\n    @tag_svn_revision.setter\n    def tag_svn_revision(self, value):\n        pass\n    ####################################\n\n    def save_version_info(self, filename):\n        \"\"\"\n        Materialize the value of date into the\n        build tag. Install build keys in a deterministic order\n        to avoid arbitrary reordering on subsequent builds.\n        \"\"\"\n        egg_info = collections.OrderedDict()\n        # follow the order these keys would have been added\n        # when PYTHONHASHSEED=0\n        egg_info['tag_build'] = self.tags()\n        egg_info['tag_date'] = 0\n        edit_config(filename, dict(egg_info=egg_info))\n\n    def finalize_options(self):\n        # Note: we need to capture the current value returned\n        # by `self.tagged_version()`, so we can later update\n        # `self.distribution.metadata.version` without\n        # repercussions.\n        self.egg_name = self.name\n        self.egg_version = self.tagged_version()\n        parsed_version = parse_version(self.egg_version)\n\n        try:\n            is_version = isinstance(parsed_version, packaging.version.Version)\n            spec = (\n                \"%s==%s\" if is_version else \"%s===%s\"\n            )\n            list(\n                parse_requirements(spec % (self.egg_name, self.egg_version))\n            )\n        except ValueError:\n            raise distutils.errors.DistutilsOptionError(\n                \"Invalid distribution name or version syntax: %s-%s\" %\n                (self.egg_name, self.egg_version)\n            )\n\n        if self.egg_base is None:\n            dirs = self.distribution.package_dir\n            self.egg_base = (dirs or {}).get('', os.curdir)\n\n        self.ensure_dirname('egg_base')\n        self.egg_info = to_filename(self.egg_name) + '.egg-info'\n        if self.egg_base != os.curdir:\n            self.egg_info = os.path.join(self.egg_base, self.egg_info)\n        if '-' in self.egg_name:\n            self.check_broken_egg_info()\n\n        # Set package version for the benefit of dumber commands\n        # (e.g. sdist, bdist_wininst, etc.)\n        #\n        self.distribution.metadata.version = self.egg_version\n\n        # If we bootstrapped around the lack of a PKG-INFO, as might be the\n        # case in a fresh checkout, make sure that any special tags get added\n        # to the version info\n        #\n        pd = self.distribution._patched_dist\n        if pd is not None and pd.key == self.egg_name.lower():\n            pd._version = self.egg_version\n            pd._parsed_version = parse_version(self.egg_version)\n            self.distribution._patched_dist = None\n\n    def write_or_delete_file(self, what, filename, data, force=False):\n        \"\"\"Write `data` to `filename` or delete if empty\n\n        If `data` is non-empty, this routine is the same as ``write_file()``.\n        If `data` is empty but not ``None``, this is the same as calling\n        ``delete_file(filename)`.  If `data` is ``None``, then this is a no-op\n        unless `filename` exists, in which case a warning is issued about the\n        orphaned file (if `force` is false), or deleted (if `force` is true).\n        \"\"\"\n        if data:\n            self.write_file(what, filename, data)\n        elif os.path.exists(filename):\n            if data is None and not force:\n                log.warn(\n                    \"%s not set in setup(), but %s exists\", what, filename\n                )\n                return\n            else:\n                self.delete_file(filename)\n\n    def write_file(self, what, filename, data):\n        \"\"\"Write `data` to `filename` (if not a dry run) after announcing it\n\n        `what` is used in a log message to identify what is being written\n        to the file.\n        \"\"\"\n        log.info(\"writing %s to %s\", what, filename)\n        if not six.PY2:\n            data = data.encode(\"utf-8\")\n        if not self.dry_run:\n            f = open(filename, 'wb')\n            f.write(data)\n            f.close()\n\n    def delete_file(self, filename):\n        \"\"\"Delete `filename` (if not a dry run) after announcing it\"\"\"\n        log.info(\"deleting %s\", filename)\n        if not self.dry_run:\n            os.unlink(filename)\n\n    def run(self):\n        self.mkpath(self.egg_info)\n        os.utime(self.egg_info, None)\n        installer = self.distribution.fetch_build_egg\n        for ep in iter_entry_points('egg_info.writers'):\n            ep.require(installer=installer)\n            writer = ep.resolve()\n            writer(self, ep.name, os.path.join(self.egg_info, ep.name))\n\n        # Get rid of native_libs.txt if it was put there by older bdist_egg\n        nl = os.path.join(self.egg_info, \"native_libs.txt\")\n        if os.path.exists(nl):\n            self.delete_file(nl)\n\n        self.find_sources()\n\n    def find_sources(self):\n        \"\"\"Generate SOURCES.txt manifest file\"\"\"\n        manifest_filename = os.path.join(self.egg_info, \"SOURCES.txt\")\n        mm = manifest_maker(self.distribution)\n        mm.manifest = manifest_filename\n        mm.run()\n        self.filelist = mm.filelist\n\n    def check_broken_egg_info(self):\n        bei = self.egg_name + '.egg-info'\n        if self.egg_base != os.curdir:\n            bei = os.path.join(self.egg_base, bei)\n        if os.path.exists(bei):\n            log.warn(\n                \"-\" * 78 + '\\n'\n                \"Note: Your current .egg-info directory has a '-' in its name;\"\n                '\\nthis will not work correctly with \"setup.py develop\".\\n\\n'\n                'Please rename %s to %s to correct this problem.\\n' + '-' * 78,\n                bei, self.egg_info\n            )\n            self.broken_egg_info = self.egg_info\n            self.egg_info = bei  # make it work for now\n\n\nclass FileList(_FileList):\n    # Implementations of the various MANIFEST.in commands\n\n    def process_template_line(self, line):\n        # Parse the line: split it up, make sure the right number of words\n        # is there, and return the relevant words.  'action' is always\n        # defined: it's the first word of the line.  Which of the other\n        # three are defined depends on the action; it'll be either\n        # patterns, (dir and patterns), or (dir_pattern).\n        (action, patterns, dir, dir_pattern) = self._parse_template_line(line)\n\n        # OK, now we know that the action is valid and we have the\n        # right number of words on the line for that action -- so we\n        # can proceed with minimal error-checking.\n        if action == 'include':\n            self.debug_print(\"include \" + ' '.join(patterns))\n            for pattern in patterns:\n                if not self.include(pattern):\n                    log.warn(\"warning: no files found matching '%s'\", pattern)\n\n        elif action == 'exclude':\n            self.debug_print(\"exclude \" + ' '.join(patterns))\n            for pattern in patterns:\n                if not self.exclude(pattern):\n                    log.warn((\"warning: no previously-included files \"\n                              \"found matching '%s'\"), pattern)\n\n        elif action == 'global-include':\n            self.debug_print(\"global-include \" + ' '.join(patterns))\n            for pattern in patterns:\n                if not self.global_include(pattern):\n                    log.warn((\"warning: no files found matching '%s' \"\n                              \"anywhere in distribution\"), pattern)\n\n        elif action == 'global-exclude':\n            self.debug_print(\"global-exclude \" + ' '.join(patterns))\n            for pattern in patterns:\n                if not self.global_exclude(pattern):\n                    log.warn((\"warning: no previously-included files matching \"\n                              \"'%s' found anywhere in distribution\"),\n                             pattern)\n\n        elif action == 'recursive-include':\n            self.debug_print(\"recursive-include %s %s\" %\n                             (dir, ' '.join(patterns)))\n            for pattern in patterns:\n                if not self.recursive_include(dir, pattern):\n                    log.warn((\"warning: no files found matching '%s' \"\n                              \"under directory '%s'\"),\n                             pattern, dir)\n\n        elif action == 'recursive-exclude':\n            self.debug_print(\"recursive-exclude %s %s\" %\n                             (dir, ' '.join(patterns)))\n            for pattern in patterns:\n                if not self.recursive_exclude(dir, pattern):\n                    log.warn((\"warning: no previously-included files matching \"\n                              \"'%s' found under directory '%s'\"),\n                             pattern, dir)\n\n        elif action == 'graft':\n            self.debug_print(\"graft \" + dir_pattern)\n            if not self.graft(dir_pattern):\n                log.warn(\"warning: no directories found matching '%s'\",\n                         dir_pattern)\n\n        elif action == 'prune':\n            self.debug_print(\"prune \" + dir_pattern)\n            if not self.prune(dir_pattern):\n                log.warn((\"no previously-included directories found \"\n                          \"matching '%s'\"), dir_pattern)\n\n        else:\n            raise DistutilsInternalError(\n                \"this cannot happen: invalid action '%s'\" % action)\n\n    def _remove_files(self, predicate):\n        \"\"\"\n        Remove all files from the file list that match the predicate.\n        Return True if any matching files were removed\n        \"\"\"\n        found = False\n        for i in range(len(self.files) - 1, -1, -1):\n            if predicate(self.files[i]):\n                self.debug_print(\" removing \" + self.files[i])\n                del self.files[i]\n                found = True\n        return found\n\n    def include(self, pattern):\n        \"\"\"Include files that match 'pattern'.\"\"\"\n        found = [f for f in glob(pattern) if not os.path.isdir(f)]\n        self.extend(found)\n        return bool(found)\n\n    def exclude(self, pattern):\n        \"\"\"Exclude files that match 'pattern'.\"\"\"\n        match = translate_pattern(pattern)\n        return self._remove_files(match.match)\n\n    def recursive_include(self, dir, pattern):\n        \"\"\"\n        Include all files anywhere in 'dir/' that match the pattern.\n        \"\"\"\n        full_pattern = os.path.join(dir, '**', pattern)\n        found = [f for f in glob(full_pattern, recursive=True)\n                 if not os.path.isdir(f)]\n        self.extend(found)\n        return bool(found)\n\n    def recursive_exclude(self, dir, pattern):\n        \"\"\"\n        Exclude any file anywhere in 'dir/' that match the pattern.\n        \"\"\"\n        match = translate_pattern(os.path.join(dir, '**', pattern))\n        return self._remove_files(match.match)\n\n    def graft(self, dir):\n        \"\"\"Include all files from 'dir/'.\"\"\"\n        found = [\n            item\n            for match_dir in glob(dir)\n            for item in distutils.filelist.findall(match_dir)\n        ]\n        self.extend(found)\n        return bool(found)\n\n    def prune(self, dir):\n        \"\"\"Filter out files from 'dir/'.\"\"\"\n        match = translate_pattern(os.path.join(dir, '**'))\n        return self._remove_files(match.match)\n\n    def global_include(self, pattern):\n        \"\"\"\n        Include all files anywhere in the current directory that match the\n        pattern. This is very inefficient on large file trees.\n        \"\"\"\n        if self.allfiles is None:\n            self.findall()\n        match = translate_pattern(os.path.join('**', pattern))\n        found = [f for f in self.allfiles if match.match(f)]\n        self.extend(found)\n        return bool(found)\n\n    def global_exclude(self, pattern):\n        \"\"\"\n        Exclude all files anywhere that match the pattern.\n        \"\"\"\n        match = translate_pattern(os.path.join('**', pattern))\n        return self._remove_files(match.match)\n\n    def append(self, item):\n        if item.endswith('\\r'):  # Fix older sdists built on Windows\n            item = item[:-1]\n        path = convert_path(item)\n\n        if self._safe_path(path):\n            self.files.append(path)\n\n    def extend(self, paths):\n        self.files.extend(filter(self._safe_path, paths))\n\n    def _repair(self):\n        \"\"\"\n        Replace self.files with only safe paths\n\n        Because some owners of FileList manipulate the underlying\n        ``files`` attribute directly, this method must be called to\n        repair those paths.\n        \"\"\"\n        self.files = list(filter(self._safe_path, self.files))\n\n    def _safe_path(self, path):\n        enc_warn = \"'%s' not %s encodable -- skipping\"\n\n        # To avoid accidental trans-codings errors, first to unicode\n        u_path = unicode_utils.filesys_decode(path)\n        if u_path is None:\n            log.warn(\"'%s' in unexpected encoding -- skipping\" % path)\n            return False\n\n        # Must ensure utf-8 encodability\n        utf8_path = unicode_utils.try_encode(u_path, \"utf-8\")\n        if utf8_path is None:\n            log.warn(enc_warn, path, 'utf-8')\n            return False\n\n        try:\n            # accept is either way checks out\n            if os.path.exists(u_path) or os.path.exists(utf8_path):\n                return True\n        # this will catch any encode errors decoding u_path\n        except UnicodeEncodeError:\n            log.warn(enc_warn, path, sys.getfilesystemencoding())\n\n\nclass manifest_maker(sdist):\n    template = \"MANIFEST.in\"\n\n    def initialize_options(self):\n        self.use_defaults = 1\n        self.prune = 1\n        self.manifest_only = 1\n        self.force_manifest = 1\n\n    def finalize_options(self):\n        pass\n\n    def run(self):\n        self.filelist = FileList()\n        if not os.path.exists(self.manifest):\n            self.write_manifest()  # it must exist so it'll get in the list\n        self.add_defaults()\n        if os.path.exists(self.template):\n            self.read_template()\n        self.prune_file_list()\n        self.filelist.sort()\n        self.filelist.remove_duplicates()\n        self.write_manifest()\n\n    def _manifest_normalize(self, path):\n        path = unicode_utils.filesys_decode(path)\n        return path.replace(os.sep, '/')\n\n    def write_manifest(self):\n        \"\"\"\n        Write the file list in 'self.filelist' to the manifest file\n        named by 'self.manifest'.\n        \"\"\"\n        self.filelist._repair()\n\n        # Now _repairs should encodability, but not unicode\n        files = [self._manifest_normalize(f) for f in self.filelist.files]\n        msg = \"writing manifest file '%s'\" % self.manifest\n        self.execute(write_file, (self.manifest, files), msg)\n\n    def warn(self, msg):\n        if not self._should_suppress_warning(msg):\n            sdist.warn(self, msg)\n\n    @staticmethod\n    def _should_suppress_warning(msg):\n        \"\"\"\n        suppress missing-file warnings from sdist\n        \"\"\"\n        return re.match(r\"standard file .*not found\", msg)\n\n    def add_defaults(self):\n        sdist.add_defaults(self)\n        self.check_license()\n        self.filelist.append(self.template)\n        self.filelist.append(self.manifest)\n        rcfiles = list(walk_revctrl())\n        if rcfiles:\n            self.filelist.extend(rcfiles)\n        elif os.path.exists(self.manifest):\n            self.read_manifest()\n\n        if os.path.exists(\"setup.py\"):\n            # setup.py should be included by default, even if it's not\n            # the script called to create the sdist\n            self.filelist.append(\"setup.py\")\n\n        ei_cmd = self.get_finalized_command('egg_info')\n        self.filelist.graft(ei_cmd.egg_info)\n\n    def prune_file_list(self):\n        build = self.get_finalized_command('build')\n        base_dir = self.distribution.get_fullname()\n        self.filelist.prune(build.build_base)\n        self.filelist.prune(base_dir)\n        sep = re.escape(os.sep)\n        self.filelist.exclude_pattern(r'(^|' + sep + r')(RCS|CVS|\\.svn)' + sep,\n                                      is_regex=1)\n\n\ndef write_file(filename, contents):\n    \"\"\"Create a file with the specified name and write 'contents' (a\n    sequence of strings without line terminators) to it.\n    \"\"\"\n    contents = \"\\n\".join(contents)\n\n    # assuming the contents has been vetted for utf-8 encoding\n    contents = contents.encode(\"utf-8\")\n\n    with open(filename, \"wb\") as f:  # always write POSIX-style manifest\n        f.write(contents)\n\n\ndef write_pkg_info(cmd, basename, filename):\n    log.info(\"writing %s\", filename)\n    if not cmd.dry_run:\n        metadata = cmd.distribution.metadata\n        metadata.version, oldver = cmd.egg_version, metadata.version\n        metadata.name, oldname = cmd.egg_name, metadata.name\n\n        try:\n            # write unescaped data to PKG-INFO, so older pkg_resources\n            # can still parse it\n            metadata.write_pkg_info(cmd.egg_info)\n        finally:\n            metadata.name, metadata.version = oldname, oldver\n\n        safe = getattr(cmd.distribution, 'zip_safe', None)\n\n        bdist_egg.write_safety_flag(cmd.egg_info, safe)\n\n\ndef warn_depends_obsolete(cmd, basename, filename):\n    if os.path.exists(filename):\n        log.warn(\n            \"WARNING: 'depends.txt' is not used by setuptools 0.6!\\n\"\n            \"Use the install_requires/extras_require setup() args instead.\"\n        )\n\n\ndef _write_requirements(stream, reqs):\n    lines = yield_lines(reqs or ())\n    append_cr = lambda line: line + '\\n'\n    lines = map(append_cr, lines)\n    stream.writelines(lines)\n\n\ndef write_requirements(cmd, basename, filename):\n    dist = cmd.distribution\n    data = six.StringIO()\n    _write_requirements(data, dist.install_requires)\n    extras_require = dist.extras_require or {}\n    for extra in sorted(extras_require):\n        data.write('\\n[{extra}]\\n'.format(**vars()))\n        _write_requirements(data, extras_require[extra])\n    cmd.write_or_delete_file(\"requirements\", filename, data.getvalue())\n\n\ndef write_setup_requirements(cmd, basename, filename):\n    data = io.StringIO()\n    _write_requirements(data, cmd.distribution.setup_requires)\n    cmd.write_or_delete_file(\"setup-requirements\", filename, data.getvalue())\n\n\ndef write_toplevel_names(cmd, basename, filename):\n    pkgs = dict.fromkeys(\n        [\n            k.split('.', 1)[0]\n            for k in cmd.distribution.iter_distribution_names()\n        ]\n    )\n    cmd.write_file(\"top-level names\", filename, '\\n'.join(sorted(pkgs)) + '\\n')\n\n\ndef overwrite_arg(cmd, basename, filename):\n    write_arg(cmd, basename, filename, True)\n\n\ndef write_arg(cmd, basename, filename, force=False):\n    argname = os.path.splitext(basename)[0]\n    value = getattr(cmd.distribution, argname, None)\n    if value is not None:\n        value = '\\n'.join(value) + '\\n'\n    cmd.write_or_delete_file(argname, filename, value, force)\n\n\ndef write_entries(cmd, basename, filename):\n    ep = cmd.distribution.entry_points\n\n    if isinstance(ep, six.string_types) or ep is None:\n        data = ep\n    elif ep is not None:\n        data = []\n        for section, contents in sorted(ep.items()):\n            if not isinstance(contents, six.string_types):\n                contents = EntryPoint.parse_group(section, contents)\n                contents = '\\n'.join(sorted(map(str, contents.values())))\n            data.append('[%s]\\n%s\\n\\n' % (section, contents))\n        data = ''.join(data)\n\n    cmd.write_or_delete_file('entry points', filename, data, True)\n\n\ndef get_pkg_info_revision():\n    \"\"\"\n    Get a -r### off of PKG-INFO Version in case this is an sdist of\n    a subversion revision.\n    \"\"\"\n    warnings.warn(\"get_pkg_info_revision is deprecated.\", EggInfoDeprecationWarning)\n    if os.path.exists('PKG-INFO'):\n        with io.open('PKG-INFO') as f:\n            for line in f:\n                match = re.match(r\"Version:.*-r(\\d+)\\s*$\", line)\n                if match:\n                    return int(match.group(1))\n    return 0\n\n\nclass EggInfoDeprecationWarning(SetuptoolsDeprecationWarning):\n    \"\"\"Class for warning about deprecations in eggInfo in setupTools. Not ignored by default, unlike DeprecationWarning.\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/install.py",
    "content": "from distutils.errors import DistutilsArgError\nimport inspect\nimport glob\nimport warnings\nimport platform\nimport distutils.command.install as orig\n\nimport setuptools\n\n# Prior to numpy 1.9, NumPy relies on the '_install' name, so provide it for\n# now. See https://github.com/pypa/setuptools/issues/199/\n_install = orig.install\n\n\nclass install(orig.install):\n    \"\"\"Use easy_install to install the package, w/dependencies\"\"\"\n\n    user_options = orig.install.user_options + [\n        ('old-and-unmanageable', None, \"Try not to use this!\"),\n        ('single-version-externally-managed', None,\n         \"used by system package builders to create 'flat' eggs\"),\n    ]\n    boolean_options = orig.install.boolean_options + [\n        'old-and-unmanageable', 'single-version-externally-managed',\n    ]\n    new_commands = [\n        ('install_egg_info', lambda self: True),\n        ('install_scripts', lambda self: True),\n    ]\n    _nc = dict(new_commands)\n\n    def initialize_options(self):\n        orig.install.initialize_options(self)\n        self.old_and_unmanageable = None\n        self.single_version_externally_managed = None\n\n    def finalize_options(self):\n        orig.install.finalize_options(self)\n        if self.root:\n            self.single_version_externally_managed = True\n        elif self.single_version_externally_managed:\n            if not self.root and not self.record:\n                raise DistutilsArgError(\n                    \"You must specify --record or --root when building system\"\n                    \" packages\"\n                )\n\n    def handle_extra_path(self):\n        if self.root or self.single_version_externally_managed:\n            # explicit backward-compatibility mode, allow extra_path to work\n            return orig.install.handle_extra_path(self)\n\n        # Ignore extra_path when installing an egg (or being run by another\n        # command without --root or --single-version-externally-managed\n        self.path_file = None\n        self.extra_dirs = ''\n\n    def run(self):\n        # Explicit request for old-style install?  Just do it\n        if self.old_and_unmanageable or self.single_version_externally_managed:\n            return orig.install.run(self)\n\n        if not self._called_from_setup(inspect.currentframe()):\n            # Run in backward-compatibility mode to support bdist_* commands.\n            orig.install.run(self)\n        else:\n            self.do_egg_install()\n\n    @staticmethod\n    def _called_from_setup(run_frame):\n        \"\"\"\n        Attempt to detect whether run() was called from setup() or by another\n        command.  If called by setup(), the parent caller will be the\n        'run_command' method in 'distutils.dist', and *its* caller will be\n        the 'run_commands' method.  If called any other way, the\n        immediate caller *might* be 'run_command', but it won't have been\n        called by 'run_commands'. Return True in that case or if a call stack\n        is unavailable. Return False otherwise.\n        \"\"\"\n        if run_frame is None:\n            msg = \"Call stack not available. bdist_* commands may fail.\"\n            warnings.warn(msg)\n            if platform.python_implementation() == 'IronPython':\n                msg = \"For best results, pass -X:Frames to enable call stack.\"\n                warnings.warn(msg)\n            return True\n        res = inspect.getouterframes(run_frame)[2]\n        caller, = res[:1]\n        info = inspect.getframeinfo(caller)\n        caller_module = caller.f_globals.get('__name__', '')\n        return (\n            caller_module == 'distutils.dist'\n            and info.function == 'run_commands'\n        )\n\n    def do_egg_install(self):\n\n        easy_install = self.distribution.get_command_class('easy_install')\n\n        cmd = easy_install(\n            self.distribution, args=\"x\", root=self.root, record=self.record,\n        )\n        cmd.ensure_finalized()  # finalize before bdist_egg munges install cmd\n        cmd.always_copy_from = '.'  # make sure local-dir eggs get installed\n\n        # pick up setup-dir .egg files only: no .egg-info\n        cmd.package_index.scan(glob.glob('*.egg'))\n\n        self.run_command('bdist_egg')\n        args = [self.distribution.get_command_obj('bdist_egg').egg_output]\n\n        if setuptools.bootstrap_install_from:\n            # Bootstrap self-installation of setuptools\n            args.insert(0, setuptools.bootstrap_install_from)\n\n        cmd.args = args\n        cmd.run(show_deprecation=False)\n        setuptools.bootstrap_install_from = None\n\n\n# XXX Python 3.1 doesn't see _nc if this is inside the class\ninstall.sub_commands = (\n    [cmd for cmd in orig.install.sub_commands if cmd[0] not in install._nc] +\n    install.new_commands\n)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/install_egg_info.py",
    "content": "from distutils import log, dir_util\nimport os\n\nfrom setuptools import Command\nfrom setuptools import namespaces\nfrom setuptools.archive_util import unpack_archive\nimport pkg_resources\n\n\nclass install_egg_info(namespaces.Installer, Command):\n    \"\"\"Install an .egg-info directory for the package\"\"\"\n\n    description = \"Install an .egg-info directory for the package\"\n\n    user_options = [\n        ('install-dir=', 'd', \"directory to install to\"),\n    ]\n\n    def initialize_options(self):\n        self.install_dir = None\n\n    def finalize_options(self):\n        self.set_undefined_options('install_lib',\n                                   ('install_dir', 'install_dir'))\n        ei_cmd = self.get_finalized_command(\"egg_info\")\n        basename = pkg_resources.Distribution(\n            None, None, ei_cmd.egg_name, ei_cmd.egg_version\n        ).egg_name() + '.egg-info'\n        self.source = ei_cmd.egg_info\n        self.target = os.path.join(self.install_dir, basename)\n        self.outputs = []\n\n    def run(self):\n        self.run_command('egg_info')\n        if os.path.isdir(self.target) and not os.path.islink(self.target):\n            dir_util.remove_tree(self.target, dry_run=self.dry_run)\n        elif os.path.exists(self.target):\n            self.execute(os.unlink, (self.target,), \"Removing \" + self.target)\n        if not self.dry_run:\n            pkg_resources.ensure_directory(self.target)\n        self.execute(\n            self.copytree, (), \"Copying %s to %s\" % (self.source, self.target)\n        )\n        self.install_namespaces()\n\n    def get_outputs(self):\n        return self.outputs\n\n    def copytree(self):\n        # Copy the .egg-info tree to site-packages\n        def skimmer(src, dst):\n            # filter out source-control directories; note that 'src' is always\n            # a '/'-separated path, regardless of platform.  'dst' is a\n            # platform-specific path.\n            for skip in '.svn/', 'CVS/':\n                if src.startswith(skip) or '/' + skip in src:\n                    return None\n            self.outputs.append(dst)\n            log.debug(\"Copying %s to %s\", src, dst)\n            return dst\n\n        unpack_archive(self.source, self.target, skimmer)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/install_lib.py",
    "content": "import os\nimport sys\nfrom itertools import product, starmap\nimport distutils.command.install_lib as orig\n\n\nclass install_lib(orig.install_lib):\n    \"\"\"Don't add compiled flags to filenames of non-Python files\"\"\"\n\n    def run(self):\n        self.build()\n        outfiles = self.install()\n        if outfiles is not None:\n            # always compile, in case we have any extension stubs to deal with\n            self.byte_compile(outfiles)\n\n    def get_exclusions(self):\n        \"\"\"\n        Return a collections.Sized collections.Container of paths to be\n        excluded for single_version_externally_managed installations.\n        \"\"\"\n        all_packages = (\n            pkg\n            for ns_pkg in self._get_SVEM_NSPs()\n            for pkg in self._all_packages(ns_pkg)\n        )\n\n        excl_specs = product(all_packages, self._gen_exclusion_paths())\n        return set(starmap(self._exclude_pkg_path, excl_specs))\n\n    def _exclude_pkg_path(self, pkg, exclusion_path):\n        \"\"\"\n        Given a package name and exclusion path within that package,\n        compute the full exclusion path.\n        \"\"\"\n        parts = pkg.split('.') + [exclusion_path]\n        return os.path.join(self.install_dir, *parts)\n\n    @staticmethod\n    def _all_packages(pkg_name):\n        \"\"\"\n        >>> list(install_lib._all_packages('foo.bar.baz'))\n        ['foo.bar.baz', 'foo.bar', 'foo']\n        \"\"\"\n        while pkg_name:\n            yield pkg_name\n            pkg_name, sep, child = pkg_name.rpartition('.')\n\n    def _get_SVEM_NSPs(self):\n        \"\"\"\n        Get namespace packages (list) but only for\n        single_version_externally_managed installations and empty otherwise.\n        \"\"\"\n        # TODO: is it necessary to short-circuit here? i.e. what's the cost\n        # if get_finalized_command is called even when namespace_packages is\n        # False?\n        if not self.distribution.namespace_packages:\n            return []\n\n        install_cmd = self.get_finalized_command('install')\n        svem = install_cmd.single_version_externally_managed\n\n        return self.distribution.namespace_packages if svem else []\n\n    @staticmethod\n    def _gen_exclusion_paths():\n        \"\"\"\n        Generate file paths to be excluded for namespace packages (bytecode\n        cache files).\n        \"\"\"\n        # always exclude the package module itself\n        yield '__init__.py'\n\n        yield '__init__.pyc'\n        yield '__init__.pyo'\n\n        if not hasattr(sys, 'implementation'):\n            return\n\n        base = os.path.join('__pycache__', '__init__.' + sys.implementation.cache_tag)\n        yield base + '.pyc'\n        yield base + '.pyo'\n        yield base + '.opt-1.pyc'\n        yield base + '.opt-2.pyc'\n\n    def copy_tree(\n            self, infile, outfile,\n            preserve_mode=1, preserve_times=1, preserve_symlinks=0, level=1\n    ):\n        assert preserve_mode and preserve_times and not preserve_symlinks\n        exclude = self.get_exclusions()\n\n        if not exclude:\n            return orig.install_lib.copy_tree(self, infile, outfile)\n\n        # Exclude namespace package __init__.py* files from the output\n\n        from setuptools.archive_util import unpack_directory\n        from distutils import log\n\n        outfiles = []\n\n        def pf(src, dst):\n            if dst in exclude:\n                log.warn(\"Skipping installation of %s (namespace package)\",\n                         dst)\n                return False\n\n            log.info(\"copying %s -> %s\", src, os.path.dirname(dst))\n            outfiles.append(dst)\n            return dst\n\n        unpack_directory(infile, outfile, pf)\n        return outfiles\n\n    def get_outputs(self):\n        outputs = orig.install_lib.get_outputs(self)\n        exclude = self.get_exclusions()\n        if exclude:\n            return [f for f in outputs if f not in exclude]\n        return outputs\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/install_scripts.py",
    "content": "from distutils import log\nimport distutils.command.install_scripts as orig\nimport os\nimport sys\n\nfrom pkg_resources import Distribution, PathMetadata, ensure_directory\n\n\nclass install_scripts(orig.install_scripts):\n    \"\"\"Do normal script install, plus any egg_info wrapper scripts\"\"\"\n\n    def initialize_options(self):\n        orig.install_scripts.initialize_options(self)\n        self.no_ep = False\n\n    def run(self):\n        import setuptools.command.easy_install as ei\n\n        self.run_command(\"egg_info\")\n        if self.distribution.scripts:\n            orig.install_scripts.run(self)  # run first to set up self.outfiles\n        else:\n            self.outfiles = []\n        if self.no_ep:\n            # don't install entry point scripts into .egg file!\n            return\n\n        ei_cmd = self.get_finalized_command(\"egg_info\")\n        dist = Distribution(\n            ei_cmd.egg_base, PathMetadata(ei_cmd.egg_base, ei_cmd.egg_info),\n            ei_cmd.egg_name, ei_cmd.egg_version,\n        )\n        bs_cmd = self.get_finalized_command('build_scripts')\n        exec_param = getattr(bs_cmd, 'executable', None)\n        bw_cmd = self.get_finalized_command(\"bdist_wininst\")\n        is_wininst = getattr(bw_cmd, '_is_running', False)\n        writer = ei.ScriptWriter\n        if is_wininst:\n            exec_param = \"python.exe\"\n            writer = ei.WindowsScriptWriter\n        if exec_param == sys.executable:\n            # In case the path to the Python executable contains a space, wrap\n            # it so it's not split up.\n            exec_param = [exec_param]\n        # resolve the writer to the environment\n        writer = writer.best()\n        cmd = writer.command_spec_class.best().from_param(exec_param)\n        for args in writer.get_args(dist, cmd.as_header()):\n            self.write_script(*args)\n\n    def write_script(self, script_name, contents, mode=\"t\", *ignored):\n        \"\"\"Write an executable file to the scripts directory\"\"\"\n        from setuptools.command.easy_install import chmod, current_umask\n\n        log.info(\"Installing %s script to %s\", script_name, self.install_dir)\n        target = os.path.join(self.install_dir, script_name)\n        self.outfiles.append(target)\n\n        mask = current_umask()\n        if not self.dry_run:\n            ensure_directory(target)\n            f = open(target, \"w\" + mode)\n            f.write(contents)\n            f.close()\n            chmod(target, 0o777 - mask)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/launcher manifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n    <assemblyIdentity version=\"1.0.0.0\"\n                      processorArchitecture=\"X86\"\n                      name=\"%(name)s\"\n                      type=\"win32\"/>\n    <!-- Identify the application security requirements. -->\n    <trustInfo xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n        <security>\n            <requestedPrivileges>\n                <requestedExecutionLevel level=\"asInvoker\" uiAccess=\"false\"/>\n            </requestedPrivileges>\n        </security>\n    </trustInfo>\n</assembly>\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/py36compat.py",
    "content": "import os\nfrom glob import glob\nfrom distutils.util import convert_path\nfrom distutils.command import sdist\n\nfrom setuptools.extern.six.moves import filter\n\n\nclass sdist_add_defaults:\n    \"\"\"\n    Mix-in providing forward-compatibility for functionality as found in\n    distutils on Python 3.7.\n\n    Do not edit the code in this class except to update functionality\n    as implemented in distutils. Instead, override in the subclass.\n    \"\"\"\n\n    def add_defaults(self):\n        \"\"\"Add all the default files to self.filelist:\n          - README or README.txt\n          - setup.py\n          - test/test*.py\n          - all pure Python modules mentioned in setup script\n          - all files pointed by package_data (build_py)\n          - all files defined in data_files.\n          - all files defined as scripts.\n          - all C sources listed as part of extensions or C libraries\n            in the setup script (doesn't catch C headers!)\n        Warns if (README or README.txt) or setup.py are missing; everything\n        else is optional.\n        \"\"\"\n        self._add_defaults_standards()\n        self._add_defaults_optional()\n        self._add_defaults_python()\n        self._add_defaults_data_files()\n        self._add_defaults_ext()\n        self._add_defaults_c_libs()\n        self._add_defaults_scripts()\n\n    @staticmethod\n    def _cs_path_exists(fspath):\n        \"\"\"\n        Case-sensitive path existence check\n\n        >>> sdist_add_defaults._cs_path_exists(__file__)\n        True\n        >>> sdist_add_defaults._cs_path_exists(__file__.upper())\n        False\n        \"\"\"\n        if not os.path.exists(fspath):\n            return False\n        # make absolute so we always have a directory\n        abspath = os.path.abspath(fspath)\n        directory, filename = os.path.split(abspath)\n        return filename in os.listdir(directory)\n\n    def _add_defaults_standards(self):\n        standards = [self.READMES, self.distribution.script_name]\n        for fn in standards:\n            if isinstance(fn, tuple):\n                alts = fn\n                got_it = False\n                for fn in alts:\n                    if self._cs_path_exists(fn):\n                        got_it = True\n                        self.filelist.append(fn)\n                        break\n\n                if not got_it:\n                    self.warn(\"standard file not found: should have one of \" +\n                              ', '.join(alts))\n            else:\n                if self._cs_path_exists(fn):\n                    self.filelist.append(fn)\n                else:\n                    self.warn(\"standard file '%s' not found\" % fn)\n\n    def _add_defaults_optional(self):\n        optional = ['test/test*.py', 'setup.cfg']\n        for pattern in optional:\n            files = filter(os.path.isfile, glob(pattern))\n            self.filelist.extend(files)\n\n    def _add_defaults_python(self):\n        # build_py is used to get:\n        #  - python modules\n        #  - files defined in package_data\n        build_py = self.get_finalized_command('build_py')\n\n        # getting python files\n        if self.distribution.has_pure_modules():\n            self.filelist.extend(build_py.get_source_files())\n\n        # getting package_data files\n        # (computed in build_py.data_files by build_py.finalize_options)\n        for pkg, src_dir, build_dir, filenames in build_py.data_files:\n            for filename in filenames:\n                self.filelist.append(os.path.join(src_dir, filename))\n\n    def _add_defaults_data_files(self):\n        # getting distribution.data_files\n        if self.distribution.has_data_files():\n            for item in self.distribution.data_files:\n                if isinstance(item, str):\n                    # plain file\n                    item = convert_path(item)\n                    if os.path.isfile(item):\n                        self.filelist.append(item)\n                else:\n                    # a (dirname, filenames) tuple\n                    dirname, filenames = item\n                    for f in filenames:\n                        f = convert_path(f)\n                        if os.path.isfile(f):\n                            self.filelist.append(f)\n\n    def _add_defaults_ext(self):\n        if self.distribution.has_ext_modules():\n            build_ext = self.get_finalized_command('build_ext')\n            self.filelist.extend(build_ext.get_source_files())\n\n    def _add_defaults_c_libs(self):\n        if self.distribution.has_c_libraries():\n            build_clib = self.get_finalized_command('build_clib')\n            self.filelist.extend(build_clib.get_source_files())\n\n    def _add_defaults_scripts(self):\n        if self.distribution.has_scripts():\n            build_scripts = self.get_finalized_command('build_scripts')\n            self.filelist.extend(build_scripts.get_source_files())\n\n\nif hasattr(sdist.sdist, '_add_defaults_standards'):\n    # disable the functionality already available upstream\n    class sdist_add_defaults:\n        pass\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/register.py",
    "content": "from distutils import log\nimport distutils.command.register as orig\n\nfrom setuptools.errors import RemovedCommandError\n\n\nclass register(orig.register):\n    \"\"\"Formerly used to register packages on PyPI.\"\"\"\n\n    def run(self):\n        msg = (\n            \"The register command has been removed, use twine to upload \"\n            + \"instead (https://pypi.org/p/twine)\"\n        )\n\n        self.announce(\"ERROR: \" + msg, log.ERROR)\n\n        raise RemovedCommandError(msg)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/rotate.py",
    "content": "from distutils.util import convert_path\nfrom distutils import log\nfrom distutils.errors import DistutilsOptionError\nimport os\nimport shutil\n\nfrom setuptools.extern import six\n\nfrom setuptools import Command\n\n\nclass rotate(Command):\n    \"\"\"Delete older distributions\"\"\"\n\n    description = \"delete older distributions, keeping N newest files\"\n    user_options = [\n        ('match=', 'm', \"patterns to match (required)\"),\n        ('dist-dir=', 'd', \"directory where the distributions are\"),\n        ('keep=', 'k', \"number of matching distributions to keep\"),\n    ]\n\n    boolean_options = []\n\n    def initialize_options(self):\n        self.match = None\n        self.dist_dir = None\n        self.keep = None\n\n    def finalize_options(self):\n        if self.match is None:\n            raise DistutilsOptionError(\n                \"Must specify one or more (comma-separated) match patterns \"\n                \"(e.g. '.zip' or '.egg')\"\n            )\n        if self.keep is None:\n            raise DistutilsOptionError(\"Must specify number of files to keep\")\n        try:\n            self.keep = int(self.keep)\n        except ValueError:\n            raise DistutilsOptionError(\"--keep must be an integer\")\n        if isinstance(self.match, six.string_types):\n            self.match = [\n                convert_path(p.strip()) for p in self.match.split(',')\n            ]\n        self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))\n\n    def run(self):\n        self.run_command(\"egg_info\")\n        from glob import glob\n\n        for pattern in self.match:\n            pattern = self.distribution.get_name() + '*' + pattern\n            files = glob(os.path.join(self.dist_dir, pattern))\n            files = [(os.path.getmtime(f), f) for f in files]\n            files.sort()\n            files.reverse()\n\n            log.info(\"%d file(s) matching %s\", len(files), pattern)\n            files = files[self.keep:]\n            for (t, f) in files:\n                log.info(\"Deleting %s\", f)\n                if not self.dry_run:\n                    if os.path.isdir(f):\n                        shutil.rmtree(f)\n                    else:\n                        os.unlink(f)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/saveopts.py",
    "content": "from setuptools.command.setopt import edit_config, option_base\n\n\nclass saveopts(option_base):\n    \"\"\"Save command-line options to a file\"\"\"\n\n    description = \"save supplied options to setup.cfg or other config file\"\n\n    def run(self):\n        dist = self.distribution\n        settings = {}\n\n        for cmd in dist.command_options:\n\n            if cmd == 'saveopts':\n                continue  # don't save our own options!\n\n            for opt, (src, val) in dist.get_option_dict(cmd).items():\n                if src == \"command line\":\n                    settings.setdefault(cmd, {})[opt] = val\n\n        edit_config(self.filename, settings, self.dry_run)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/sdist.py",
    "content": "from distutils import log\nimport distutils.command.sdist as orig\nimport os\nimport sys\nimport io\nimport contextlib\n\nfrom setuptools.extern import six, ordered_set\n\nfrom .py36compat import sdist_add_defaults\n\nimport pkg_resources\n\n_default_revctrl = list\n\n\ndef walk_revctrl(dirname=''):\n    \"\"\"Find all files under revision control\"\"\"\n    for ep in pkg_resources.iter_entry_points('setuptools.file_finders'):\n        for item in ep.load()(dirname):\n            yield item\n\n\nclass sdist(sdist_add_defaults, orig.sdist):\n    \"\"\"Smart sdist that finds anything supported by revision control\"\"\"\n\n    user_options = [\n        ('formats=', None,\n         \"formats for source distribution (comma-separated list)\"),\n        ('keep-temp', 'k',\n         \"keep the distribution tree around after creating \" +\n         \"archive file(s)\"),\n        ('dist-dir=', 'd',\n         \"directory to put the source distribution archive(s) in \"\n         \"[default: dist]\"),\n    ]\n\n    negative_opt = {}\n\n    README_EXTENSIONS = ['', '.rst', '.txt', '.md']\n    READMES = tuple('README{0}'.format(ext) for ext in README_EXTENSIONS)\n\n    def run(self):\n        self.run_command('egg_info')\n        ei_cmd = self.get_finalized_command('egg_info')\n        self.filelist = ei_cmd.filelist\n        self.filelist.append(os.path.join(ei_cmd.egg_info, 'SOURCES.txt'))\n        self.check_readme()\n\n        # Run sub commands\n        for cmd_name in self.get_sub_commands():\n            self.run_command(cmd_name)\n\n        self.make_distribution()\n\n        dist_files = getattr(self.distribution, 'dist_files', [])\n        for file in self.archive_files:\n            data = ('sdist', '', file)\n            if data not in dist_files:\n                dist_files.append(data)\n\n    def initialize_options(self):\n        orig.sdist.initialize_options(self)\n\n        self._default_to_gztar()\n\n    def _default_to_gztar(self):\n        # only needed on Python prior to 3.6.\n        if sys.version_info >= (3, 6, 0, 'beta', 1):\n            return\n        self.formats = ['gztar']\n\n    def make_distribution(self):\n        \"\"\"\n        Workaround for #516\n        \"\"\"\n        with self._remove_os_link():\n            orig.sdist.make_distribution(self)\n\n    @staticmethod\n    @contextlib.contextmanager\n    def _remove_os_link():\n        \"\"\"\n        In a context, remove and restore os.link if it exists\n        \"\"\"\n\n        class NoValue:\n            pass\n\n        orig_val = getattr(os, 'link', NoValue)\n        try:\n            del os.link\n        except Exception:\n            pass\n        try:\n            yield\n        finally:\n            if orig_val is not NoValue:\n                setattr(os, 'link', orig_val)\n\n    def __read_template_hack(self):\n        # This grody hack closes the template file (MANIFEST.in) if an\n        #  exception occurs during read_template.\n        # Doing so prevents an error when easy_install attempts to delete the\n        #  file.\n        try:\n            orig.sdist.read_template(self)\n        except Exception:\n            _, _, tb = sys.exc_info()\n            tb.tb_next.tb_frame.f_locals['template'].close()\n            raise\n\n    # Beginning with Python 2.7.2, 3.1.4, and 3.2.1, this leaky file handle\n    #  has been fixed, so only override the method if we're using an earlier\n    #  Python.\n    has_leaky_handle = (\n        sys.version_info < (2, 7, 2)\n        or (3, 0) <= sys.version_info < (3, 1, 4)\n        or (3, 2) <= sys.version_info < (3, 2, 1)\n    )\n    if has_leaky_handle:\n        read_template = __read_template_hack\n\n    def _add_defaults_optional(self):\n        if six.PY2:\n            sdist_add_defaults._add_defaults_optional(self)\n        else:\n            super()._add_defaults_optional()\n        if os.path.isfile('pyproject.toml'):\n            self.filelist.append('pyproject.toml')\n\n    def _add_defaults_python(self):\n        \"\"\"getting python files\"\"\"\n        if self.distribution.has_pure_modules():\n            build_py = self.get_finalized_command('build_py')\n            self.filelist.extend(build_py.get_source_files())\n            self._add_data_files(self._safe_data_files(build_py))\n\n    def _safe_data_files(self, build_py):\n        \"\"\"\n        Extracting data_files from build_py is known to cause\n        infinite recursion errors when `include_package_data`\n        is enabled, so suppress it in that case.\n        \"\"\"\n        if self.distribution.include_package_data:\n            return ()\n        return build_py.data_files\n\n    def _add_data_files(self, data_files):\n        \"\"\"\n        Add data files as found in build_py.data_files.\n        \"\"\"\n        self.filelist.extend(\n            os.path.join(src_dir, name)\n            for _, src_dir, _, filenames in data_files\n            for name in filenames\n        )\n\n    def _add_defaults_data_files(self):\n        try:\n            if six.PY2:\n                sdist_add_defaults._add_defaults_data_files(self)\n            else:\n                super()._add_defaults_data_files()\n        except TypeError:\n            log.warn(\"data_files contains unexpected objects\")\n\n    def check_readme(self):\n        for f in self.READMES:\n            if os.path.exists(f):\n                return\n        else:\n            self.warn(\n                \"standard file not found: should have one of \" +\n                ', '.join(self.READMES)\n            )\n\n    def make_release_tree(self, base_dir, files):\n        orig.sdist.make_release_tree(self, base_dir, files)\n\n        # Save any egg_info command line options used to create this sdist\n        dest = os.path.join(base_dir, 'setup.cfg')\n        if hasattr(os, 'link') and os.path.exists(dest):\n            # unlink and re-copy, since it might be hard-linked, and\n            # we don't want to change the source version\n            os.unlink(dest)\n            self.copy_file('setup.cfg', dest)\n\n        self.get_finalized_command('egg_info').save_version_info(dest)\n\n    def _manifest_is_not_generated(self):\n        # check for special comment used in 2.7.1 and higher\n        if not os.path.isfile(self.manifest):\n            return False\n\n        with io.open(self.manifest, 'rb') as fp:\n            first_line = fp.readline()\n        return (first_line !=\n                '# file GENERATED by distutils, do NOT edit\\n'.encode())\n\n    def read_manifest(self):\n        \"\"\"Read the manifest file (named by 'self.manifest') and use it to\n        fill in 'self.filelist', the list of files to include in the source\n        distribution.\n        \"\"\"\n        log.info(\"reading manifest file '%s'\", self.manifest)\n        manifest = open(self.manifest, 'rb')\n        for line in manifest:\n            # The manifest must contain UTF-8. See #303.\n            if not six.PY2:\n                try:\n                    line = line.decode('UTF-8')\n                except UnicodeDecodeError:\n                    log.warn(\"%r not UTF-8 decodable -- skipping\" % line)\n                    continue\n            # ignore comments and blank lines\n            line = line.strip()\n            if line.startswith('#') or not line:\n                continue\n            self.filelist.append(line)\n        manifest.close()\n\n    def check_license(self):\n        \"\"\"Checks if license_file' or 'license_files' is configured and adds any\n        valid paths to 'self.filelist'.\n        \"\"\"\n\n        files = ordered_set.OrderedSet()\n\n        opts = self.distribution.get_option_dict('metadata')\n\n        # ignore the source of the value\n        _, license_file = opts.get('license_file', (None, None))\n\n        if license_file is None:\n            log.debug(\"'license_file' option was not specified\")\n        else:\n            files.add(license_file)\n\n        try:\n            files.update(self.distribution.metadata.license_files)\n        except TypeError:\n            log.warn(\"warning: 'license_files' option is malformed\")\n\n        for f in files:\n            if not os.path.exists(f):\n                log.warn(\n                    \"warning: Failed to find the configured license file '%s'\",\n                    f)\n                files.remove(f)\n\n        self.filelist.extend(files)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/setopt.py",
    "content": "from distutils.util import convert_path\nfrom distutils import log\nfrom distutils.errors import DistutilsOptionError\nimport distutils\nimport os\n\nfrom setuptools.extern.six.moves import configparser\n\nfrom setuptools import Command\n\n__all__ = ['config_file', 'edit_config', 'option_base', 'setopt']\n\n\ndef config_file(kind=\"local\"):\n    \"\"\"Get the filename of the distutils, local, global, or per-user config\n\n    `kind` must be one of \"local\", \"global\", or \"user\"\n    \"\"\"\n    if kind == 'local':\n        return 'setup.cfg'\n    if kind == 'global':\n        return os.path.join(\n            os.path.dirname(distutils.__file__), 'distutils.cfg'\n        )\n    if kind == 'user':\n        dot = os.name == 'posix' and '.' or ''\n        return os.path.expanduser(convert_path(\"~/%spydistutils.cfg\" % dot))\n    raise ValueError(\n        \"config_file() type must be 'local', 'global', or 'user'\", kind\n    )\n\n\ndef edit_config(filename, settings, dry_run=False):\n    \"\"\"Edit a configuration file to include `settings`\n\n    `settings` is a dictionary of dictionaries or ``None`` values, keyed by\n    command/section name.  A ``None`` value means to delete the entire section,\n    while a dictionary lists settings to be changed or deleted in that section.\n    A setting of ``None`` means to delete that setting.\n    \"\"\"\n    log.debug(\"Reading configuration from %s\", filename)\n    opts = configparser.RawConfigParser()\n    opts.read([filename])\n    for section, options in settings.items():\n        if options is None:\n            log.info(\"Deleting section [%s] from %s\", section, filename)\n            opts.remove_section(section)\n        else:\n            if not opts.has_section(section):\n                log.debug(\"Adding new section [%s] to %s\", section, filename)\n                opts.add_section(section)\n            for option, value in options.items():\n                if value is None:\n                    log.debug(\n                        \"Deleting %s.%s from %s\",\n                        section, option, filename\n                    )\n                    opts.remove_option(section, option)\n                    if not opts.options(section):\n                        log.info(\"Deleting empty [%s] section from %s\",\n                                 section, filename)\n                        opts.remove_section(section)\n                else:\n                    log.debug(\n                        \"Setting %s.%s to %r in %s\",\n                        section, option, value, filename\n                    )\n                    opts.set(section, option, value)\n\n    log.info(\"Writing %s\", filename)\n    if not dry_run:\n        with open(filename, 'w') as f:\n            opts.write(f)\n\n\nclass option_base(Command):\n    \"\"\"Abstract base class for commands that mess with config files\"\"\"\n\n    user_options = [\n        ('global-config', 'g',\n         \"save options to the site-wide distutils.cfg file\"),\n        ('user-config', 'u',\n         \"save options to the current user's pydistutils.cfg file\"),\n        ('filename=', 'f',\n         \"configuration file to use (default=setup.cfg)\"),\n    ]\n\n    boolean_options = [\n        'global-config', 'user-config',\n    ]\n\n    def initialize_options(self):\n        self.global_config = None\n        self.user_config = None\n        self.filename = None\n\n    def finalize_options(self):\n        filenames = []\n        if self.global_config:\n            filenames.append(config_file('global'))\n        if self.user_config:\n            filenames.append(config_file('user'))\n        if self.filename is not None:\n            filenames.append(self.filename)\n        if not filenames:\n            filenames.append(config_file('local'))\n        if len(filenames) > 1:\n            raise DistutilsOptionError(\n                \"Must specify only one configuration file option\",\n                filenames\n            )\n        self.filename, = filenames\n\n\nclass setopt(option_base):\n    \"\"\"Save command-line options to a file\"\"\"\n\n    description = \"set an option in setup.cfg or another config file\"\n\n    user_options = [\n        ('command=', 'c', 'command to set an option for'),\n        ('option=', 'o', 'option to set'),\n        ('set-value=', 's', 'value of the option'),\n        ('remove', 'r', 'remove (unset) the value'),\n    ] + option_base.user_options\n\n    boolean_options = option_base.boolean_options + ['remove']\n\n    def initialize_options(self):\n        option_base.initialize_options(self)\n        self.command = None\n        self.option = None\n        self.set_value = None\n        self.remove = None\n\n    def finalize_options(self):\n        option_base.finalize_options(self)\n        if self.command is None or self.option is None:\n            raise DistutilsOptionError(\"Must specify --command *and* --option\")\n        if self.set_value is None and not self.remove:\n            raise DistutilsOptionError(\"Must specify --set-value or --remove\")\n\n    def run(self):\n        edit_config(\n            self.filename, {\n                self.command: {self.option.replace('-', '_'): self.set_value}\n            },\n            self.dry_run\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/test.py",
    "content": "import os\nimport operator\nimport sys\nimport contextlib\nimport itertools\nimport unittest\nfrom distutils.errors import DistutilsError, DistutilsOptionError\nfrom distutils import log\nfrom unittest import TestLoader\n\nfrom setuptools.extern import six\nfrom setuptools.extern.six.moves import map, filter\n\nfrom pkg_resources import (resource_listdir, resource_exists, normalize_path,\n                           working_set, _namespace_packages, evaluate_marker,\n                           add_activation_listener, require, EntryPoint)\nfrom setuptools import Command\nfrom .build_py import _unique_everseen\n\n__metaclass__ = type\n\n\nclass ScanningLoader(TestLoader):\n\n    def __init__(self):\n        TestLoader.__init__(self)\n        self._visited = set()\n\n    def loadTestsFromModule(self, module, pattern=None):\n        \"\"\"Return a suite of all tests cases contained in the given module\n\n        If the module is a package, load tests from all the modules in it.\n        If the module has an ``additional_tests`` function, call it and add\n        the return value to the tests.\n        \"\"\"\n        if module in self._visited:\n            return None\n        self._visited.add(module)\n\n        tests = []\n        tests.append(TestLoader.loadTestsFromModule(self, module))\n\n        if hasattr(module, \"additional_tests\"):\n            tests.append(module.additional_tests())\n\n        if hasattr(module, '__path__'):\n            for file in resource_listdir(module.__name__, ''):\n                if file.endswith('.py') and file != '__init__.py':\n                    submodule = module.__name__ + '.' + file[:-3]\n                else:\n                    if resource_exists(module.__name__, file + '/__init__.py'):\n                        submodule = module.__name__ + '.' + file\n                    else:\n                        continue\n                tests.append(self.loadTestsFromName(submodule))\n\n        if len(tests) != 1:\n            return self.suiteClass(tests)\n        else:\n            return tests[0]  # don't create a nested suite for only one return\n\n\n# adapted from jaraco.classes.properties:NonDataProperty\nclass NonDataProperty:\n    def __init__(self, fget):\n        self.fget = fget\n\n    def __get__(self, obj, objtype=None):\n        if obj is None:\n            return self\n        return self.fget(obj)\n\n\nclass test(Command):\n    \"\"\"Command to run unit tests after in-place build\"\"\"\n\n    description = \"run unit tests after in-place build (deprecated)\"\n\n    user_options = [\n        ('test-module=', 'm', \"Run 'test_suite' in specified module\"),\n        ('test-suite=', 's',\n         \"Run single test, case or suite (e.g. 'module.test_suite')\"),\n        ('test-runner=', 'r', \"Test runner to use\"),\n    ]\n\n    def initialize_options(self):\n        self.test_suite = None\n        self.test_module = None\n        self.test_loader = None\n        self.test_runner = None\n\n    def finalize_options(self):\n\n        if self.test_suite and self.test_module:\n            msg = \"You may specify a module or a suite, but not both\"\n            raise DistutilsOptionError(msg)\n\n        if self.test_suite is None:\n            if self.test_module is None:\n                self.test_suite = self.distribution.test_suite\n            else:\n                self.test_suite = self.test_module + \".test_suite\"\n\n        if self.test_loader is None:\n            self.test_loader = getattr(self.distribution, 'test_loader', None)\n        if self.test_loader is None:\n            self.test_loader = \"setuptools.command.test:ScanningLoader\"\n        if self.test_runner is None:\n            self.test_runner = getattr(self.distribution, 'test_runner', None)\n\n    @NonDataProperty\n    def test_args(self):\n        return list(self._test_args())\n\n    def _test_args(self):\n        if not self.test_suite and sys.version_info >= (2, 7):\n            yield 'discover'\n        if self.verbose:\n            yield '--verbose'\n        if self.test_suite:\n            yield self.test_suite\n\n    def with_project_on_sys_path(self, func):\n        \"\"\"\n        Backward compatibility for project_on_sys_path context.\n        \"\"\"\n        with self.project_on_sys_path():\n            func()\n\n    @contextlib.contextmanager\n    def project_on_sys_path(self, include_dists=[]):\n        with_2to3 = not six.PY2 and getattr(self.distribution, 'use_2to3', False)\n\n        if with_2to3:\n            # If we run 2to3 we can not do this inplace:\n\n            # Ensure metadata is up-to-date\n            self.reinitialize_command('build_py', inplace=0)\n            self.run_command('build_py')\n            bpy_cmd = self.get_finalized_command(\"build_py\")\n            build_path = normalize_path(bpy_cmd.build_lib)\n\n            # Build extensions\n            self.reinitialize_command('egg_info', egg_base=build_path)\n            self.run_command('egg_info')\n\n            self.reinitialize_command('build_ext', inplace=0)\n            self.run_command('build_ext')\n        else:\n            # Without 2to3 inplace works fine:\n            self.run_command('egg_info')\n\n            # Build extensions in-place\n            self.reinitialize_command('build_ext', inplace=1)\n            self.run_command('build_ext')\n\n        ei_cmd = self.get_finalized_command(\"egg_info\")\n\n        old_path = sys.path[:]\n        old_modules = sys.modules.copy()\n\n        try:\n            project_path = normalize_path(ei_cmd.egg_base)\n            sys.path.insert(0, project_path)\n            working_set.__init__()\n            add_activation_listener(lambda dist: dist.activate())\n            require('%s==%s' % (ei_cmd.egg_name, ei_cmd.egg_version))\n            with self.paths_on_pythonpath([project_path]):\n                yield\n        finally:\n            sys.path[:] = old_path\n            sys.modules.clear()\n            sys.modules.update(old_modules)\n            working_set.__init__()\n\n    @staticmethod\n    @contextlib.contextmanager\n    def paths_on_pythonpath(paths):\n        \"\"\"\n        Add the indicated paths to the head of the PYTHONPATH environment\n        variable so that subprocesses will also see the packages at\n        these paths.\n\n        Do this in a context that restores the value on exit.\n        \"\"\"\n        nothing = object()\n        orig_pythonpath = os.environ.get('PYTHONPATH', nothing)\n        current_pythonpath = os.environ.get('PYTHONPATH', '')\n        try:\n            prefix = os.pathsep.join(_unique_everseen(paths))\n            to_join = filter(None, [prefix, current_pythonpath])\n            new_path = os.pathsep.join(to_join)\n            if new_path:\n                os.environ['PYTHONPATH'] = new_path\n            yield\n        finally:\n            if orig_pythonpath is nothing:\n                os.environ.pop('PYTHONPATH', None)\n            else:\n                os.environ['PYTHONPATH'] = orig_pythonpath\n\n    @staticmethod\n    def install_dists(dist):\n        \"\"\"\n        Install the requirements indicated by self.distribution and\n        return an iterable of the dists that were built.\n        \"\"\"\n        ir_d = dist.fetch_build_eggs(dist.install_requires)\n        tr_d = dist.fetch_build_eggs(dist.tests_require or [])\n        er_d = dist.fetch_build_eggs(\n            v for k, v in dist.extras_require.items()\n            if k.startswith(':') and evaluate_marker(k[1:])\n        )\n        return itertools.chain(ir_d, tr_d, er_d)\n\n    def run(self):\n        self.announce(\n            \"WARNING: Testing via this command is deprecated and will be \"\n            \"removed in a future version. Users looking for a generic test \"\n            \"entry point independent of test runner are encouraged to use \"\n            \"tox.\",\n            log.WARN,\n        )\n\n        installed_dists = self.install_dists(self.distribution)\n\n        cmd = ' '.join(self._argv)\n        if self.dry_run:\n            self.announce('skipping \"%s\" (dry run)' % cmd)\n            return\n\n        self.announce('running \"%s\"' % cmd)\n\n        paths = map(operator.attrgetter('location'), installed_dists)\n        with self.paths_on_pythonpath(paths):\n            with self.project_on_sys_path():\n                self.run_tests()\n\n    def run_tests(self):\n        # Purge modules under test from sys.modules. The test loader will\n        # re-import them from the build location. Required when 2to3 is used\n        # with namespace packages.\n        if not six.PY2 and getattr(self.distribution, 'use_2to3', False):\n            module = self.test_suite.split('.')[0]\n            if module in _namespace_packages:\n                del_modules = []\n                if module in sys.modules:\n                    del_modules.append(module)\n                module += '.'\n                for name in sys.modules:\n                    if name.startswith(module):\n                        del_modules.append(name)\n                list(map(sys.modules.__delitem__, del_modules))\n\n        test = unittest.main(\n            None, None, self._argv,\n            testLoader=self._resolve_as_ep(self.test_loader),\n            testRunner=self._resolve_as_ep(self.test_runner),\n            exit=False,\n        )\n        if not test.result.wasSuccessful():\n            msg = 'Test failed: %s' % test.result\n            self.announce(msg, log.ERROR)\n            raise DistutilsError(msg)\n\n    @property\n    def _argv(self):\n        return ['unittest'] + self.test_args\n\n    @staticmethod\n    def _resolve_as_ep(val):\n        \"\"\"\n        Load the indicated attribute value, called, as a as if it were\n        specified as an entry point.\n        \"\"\"\n        if val is None:\n            return\n        parsed = EntryPoint.parse(\"x=\" + val)\n        return parsed.resolve()()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/upload.py",
    "content": "from distutils import log\nfrom distutils.command import upload as orig\n\nfrom setuptools.errors import RemovedCommandError\n\n\nclass upload(orig.upload):\n    \"\"\"Formerly used to upload packages to PyPI.\"\"\"\n\n    def run(self):\n        msg = (\n            \"The upload command has been removed, use twine to upload \"\n            + \"instead (https://pypi.org/p/twine)\"\n        )\n\n        self.announce(\"ERROR: \" + msg, log.ERROR)\n        raise RemovedCommandError(msg)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/command/upload_docs.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"upload_docs\n\nImplements a Distutils 'upload_docs' subcommand (upload documentation to\nPyPI's pythonhosted.org).\n\"\"\"\n\nfrom base64 import standard_b64encode\nfrom distutils import log\nfrom distutils.errors import DistutilsOptionError\nimport os\nimport socket\nimport zipfile\nimport tempfile\nimport shutil\nimport itertools\nimport functools\n\nfrom setuptools.extern import six\nfrom setuptools.extern.six.moves import http_client, urllib\n\nfrom pkg_resources import iter_entry_points\nfrom .upload import upload\n\n\ndef _encode(s):\n    errors = 'strict' if six.PY2 else 'surrogateescape'\n    return s.encode('utf-8', errors)\n\n\nclass upload_docs(upload):\n    # override the default repository as upload_docs isn't\n    # supported by Warehouse (and won't be).\n    DEFAULT_REPOSITORY = 'https://pypi.python.org/pypi/'\n\n    description = 'Upload documentation to PyPI'\n\n    user_options = [\n        ('repository=', 'r',\n         \"url of repository [default: %s]\" % upload.DEFAULT_REPOSITORY),\n        ('show-response', None,\n         'display full response text from server'),\n        ('upload-dir=', None, 'directory to upload'),\n    ]\n    boolean_options = upload.boolean_options\n\n    def has_sphinx(self):\n        if self.upload_dir is None:\n            for ep in iter_entry_points('distutils.commands', 'build_sphinx'):\n                return True\n\n    sub_commands = [('build_sphinx', has_sphinx)]\n\n    def initialize_options(self):\n        upload.initialize_options(self)\n        self.upload_dir = None\n        self.target_dir = None\n\n    def finalize_options(self):\n        upload.finalize_options(self)\n        if self.upload_dir is None:\n            if self.has_sphinx():\n                build_sphinx = self.get_finalized_command('build_sphinx')\n                self.target_dir = build_sphinx.builder_target_dir\n            else:\n                build = self.get_finalized_command('build')\n                self.target_dir = os.path.join(build.build_base, 'docs')\n        else:\n            self.ensure_dirname('upload_dir')\n            self.target_dir = self.upload_dir\n        if 'pypi.python.org' in self.repository:\n            log.warn(\"Upload_docs command is deprecated. Use RTD instead.\")\n        self.announce('Using upload directory %s' % self.target_dir)\n\n    def create_zipfile(self, filename):\n        zip_file = zipfile.ZipFile(filename, \"w\")\n        try:\n            self.mkpath(self.target_dir)  # just in case\n            for root, dirs, files in os.walk(self.target_dir):\n                if root == self.target_dir and not files:\n                    tmpl = \"no files found in upload directory '%s'\"\n                    raise DistutilsOptionError(tmpl % self.target_dir)\n                for name in files:\n                    full = os.path.join(root, name)\n                    relative = root[len(self.target_dir):].lstrip(os.path.sep)\n                    dest = os.path.join(relative, name)\n                    zip_file.write(full, dest)\n        finally:\n            zip_file.close()\n\n    def run(self):\n        # Run sub commands\n        for cmd_name in self.get_sub_commands():\n            self.run_command(cmd_name)\n\n        tmp_dir = tempfile.mkdtemp()\n        name = self.distribution.metadata.get_name()\n        zip_file = os.path.join(tmp_dir, \"%s.zip\" % name)\n        try:\n            self.create_zipfile(zip_file)\n            self.upload_file(zip_file)\n        finally:\n            shutil.rmtree(tmp_dir)\n\n    @staticmethod\n    def _build_part(item, sep_boundary):\n        key, values = item\n        title = '\\nContent-Disposition: form-data; name=\"%s\"' % key\n        # handle multiple entries for the same name\n        if not isinstance(values, list):\n            values = [values]\n        for value in values:\n            if isinstance(value, tuple):\n                title += '; filename=\"%s\"' % value[0]\n                value = value[1]\n            else:\n                value = _encode(value)\n            yield sep_boundary\n            yield _encode(title)\n            yield b\"\\n\\n\"\n            yield value\n            if value and value[-1:] == b'\\r':\n                yield b'\\n'  # write an extra newline (lurve Macs)\n\n    @classmethod\n    def _build_multipart(cls, data):\n        \"\"\"\n        Build up the MIME payload for the POST data\n        \"\"\"\n        boundary = b'--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'\n        sep_boundary = b'\\n--' + boundary\n        end_boundary = sep_boundary + b'--'\n        end_items = end_boundary, b\"\\n\",\n        builder = functools.partial(\n            cls._build_part,\n            sep_boundary=sep_boundary,\n        )\n        part_groups = map(builder, data.items())\n        parts = itertools.chain.from_iterable(part_groups)\n        body_items = itertools.chain(parts, end_items)\n        content_type = 'multipart/form-data; boundary=%s' % boundary.decode('ascii')\n        return b''.join(body_items), content_type\n\n    def upload_file(self, filename):\n        with open(filename, 'rb') as f:\n            content = f.read()\n        meta = self.distribution.metadata\n        data = {\n            ':action': 'doc_upload',\n            'name': meta.get_name(),\n            'content': (os.path.basename(filename), content),\n        }\n        # set up the authentication\n        credentials = _encode(self.username + ':' + self.password)\n        credentials = standard_b64encode(credentials)\n        if not six.PY2:\n            credentials = credentials.decode('ascii')\n        auth = \"Basic \" + credentials\n\n        body, ct = self._build_multipart(data)\n\n        msg = \"Submitting documentation to %s\" % (self.repository)\n        self.announce(msg, log.INFO)\n\n        # build the Request\n        # We can't use urllib2 since we need to send the Basic\n        # auth right with the first request\n        schema, netloc, url, params, query, fragments = \\\n            urllib.parse.urlparse(self.repository)\n        assert not params and not query and not fragments\n        if schema == 'http':\n            conn = http_client.HTTPConnection(netloc)\n        elif schema == 'https':\n            conn = http_client.HTTPSConnection(netloc)\n        else:\n            raise AssertionError(\"unsupported schema \" + schema)\n\n        data = ''\n        try:\n            conn.connect()\n            conn.putrequest(\"POST\", url)\n            content_type = ct\n            conn.putheader('Content-type', content_type)\n            conn.putheader('Content-length', str(len(body)))\n            conn.putheader('Authorization', auth)\n            conn.endheaders()\n            conn.send(body)\n        except socket.error as e:\n            self.announce(str(e), log.ERROR)\n            return\n\n        r = conn.getresponse()\n        if r.status == 200:\n            msg = 'Server response (%s): %s' % (r.status, r.reason)\n            self.announce(msg, log.INFO)\n        elif r.status == 301:\n            location = r.getheader('Location')\n            if location is None:\n                location = 'https://pythonhosted.org/%s/' % meta.get_name()\n            msg = 'Upload successful. Visit %s' % location\n            self.announce(msg, log.INFO)\n        else:\n            msg = 'Upload failed (%s): %s' % (r.status, r.reason)\n            self.announce(msg, log.ERROR)\n        if self.show_response:\n            print('-' * 75, r.read(), '-' * 75)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/config.py",
    "content": "from __future__ import absolute_import, unicode_literals\nimport io\nimport os\nimport sys\n\nimport warnings\nimport functools\nfrom collections import defaultdict\nfrom functools import partial\nfrom functools import wraps\nfrom importlib import import_module\n\nfrom distutils.errors import DistutilsOptionError, DistutilsFileError\nfrom setuptools.extern.packaging.version import LegacyVersion, parse\nfrom setuptools.extern.packaging.specifiers import SpecifierSet\nfrom setuptools.extern.six import string_types, PY3\n\n\n__metaclass__ = type\n\n\ndef read_configuration(\n        filepath, find_others=False, ignore_option_errors=False):\n    \"\"\"Read given configuration file and returns options from it as a dict.\n\n    :param str|unicode filepath: Path to configuration file\n        to get options from.\n\n    :param bool find_others: Whether to search for other configuration files\n        which could be on in various places.\n\n    :param bool ignore_option_errors: Whether to silently ignore\n        options, values of which could not be resolved (e.g. due to exceptions\n        in directives such as file:, attr:, etc.).\n        If False exceptions are propagated as expected.\n\n    :rtype: dict\n    \"\"\"\n    from setuptools.dist import Distribution, _Distribution\n\n    filepath = os.path.abspath(filepath)\n\n    if not os.path.isfile(filepath):\n        raise DistutilsFileError(\n            'Configuration file %s does not exist.' % filepath)\n\n    current_directory = os.getcwd()\n    os.chdir(os.path.dirname(filepath))\n\n    try:\n        dist = Distribution()\n\n        filenames = dist.find_config_files() if find_others else []\n        if filepath not in filenames:\n            filenames.append(filepath)\n\n        _Distribution.parse_config_files(dist, filenames=filenames)\n\n        handlers = parse_configuration(\n            dist, dist.command_options,\n            ignore_option_errors=ignore_option_errors)\n\n    finally:\n        os.chdir(current_directory)\n\n    return configuration_to_dict(handlers)\n\n\ndef _get_option(target_obj, key):\n    \"\"\"\n    Given a target object and option key, get that option from\n    the target object, either through a get_{key} method or\n    from an attribute directly.\n    \"\"\"\n    getter_name = 'get_{key}'.format(**locals())\n    by_attribute = functools.partial(getattr, target_obj, key)\n    getter = getattr(target_obj, getter_name, by_attribute)\n    return getter()\n\n\ndef configuration_to_dict(handlers):\n    \"\"\"Returns configuration data gathered by given handlers as a dict.\n\n    :param list[ConfigHandler] handlers: Handlers list,\n        usually from parse_configuration()\n\n    :rtype: dict\n    \"\"\"\n    config_dict = defaultdict(dict)\n\n    for handler in handlers:\n        for option in handler.set_options:\n            value = _get_option(handler.target_obj, option)\n            config_dict[handler.section_prefix][option] = value\n\n    return config_dict\n\n\ndef parse_configuration(\n        distribution, command_options, ignore_option_errors=False):\n    \"\"\"Performs additional parsing of configuration options\n    for a distribution.\n\n    Returns a list of used option handlers.\n\n    :param Distribution distribution:\n    :param dict command_options:\n    :param bool ignore_option_errors: Whether to silently ignore\n        options, values of which could not be resolved (e.g. due to exceptions\n        in directives such as file:, attr:, etc.).\n        If False exceptions are propagated as expected.\n    :rtype: list\n    \"\"\"\n    options = ConfigOptionsHandler(\n        distribution, command_options, ignore_option_errors)\n    options.parse()\n\n    meta = ConfigMetadataHandler(\n        distribution.metadata, command_options, ignore_option_errors,\n        distribution.package_dir)\n    meta.parse()\n\n    return meta, options\n\n\nclass ConfigHandler:\n    \"\"\"Handles metadata supplied in configuration files.\"\"\"\n\n    section_prefix = None\n    \"\"\"Prefix for config sections handled by this handler.\n    Must be provided by class heirs.\n\n    \"\"\"\n\n    aliases = {}\n    \"\"\"Options aliases.\n    For compatibility with various packages. E.g.: d2to1 and pbr.\n    Note: `-` in keys is replaced with `_` by config parser.\n\n    \"\"\"\n\n    def __init__(self, target_obj, options, ignore_option_errors=False):\n        sections = {}\n\n        section_prefix = self.section_prefix\n        for section_name, section_options in options.items():\n            if not section_name.startswith(section_prefix):\n                continue\n\n            section_name = section_name.replace(section_prefix, '').strip('.')\n            sections[section_name] = section_options\n\n        self.ignore_option_errors = ignore_option_errors\n        self.target_obj = target_obj\n        self.sections = sections\n        self.set_options = []\n\n    @property\n    def parsers(self):\n        \"\"\"Metadata item name to parser function mapping.\"\"\"\n        raise NotImplementedError(\n            '%s must provide .parsers property' % self.__class__.__name__)\n\n    def __setitem__(self, option_name, value):\n        unknown = tuple()\n        target_obj = self.target_obj\n\n        # Translate alias into real name.\n        option_name = self.aliases.get(option_name, option_name)\n\n        current_value = getattr(target_obj, option_name, unknown)\n\n        if current_value is unknown:\n            raise KeyError(option_name)\n\n        if current_value:\n            # Already inhabited. Skipping.\n            return\n\n        skip_option = False\n        parser = self.parsers.get(option_name)\n        if parser:\n            try:\n                value = parser(value)\n\n            except Exception:\n                skip_option = True\n                if not self.ignore_option_errors:\n                    raise\n\n        if skip_option:\n            return\n\n        setter = getattr(target_obj, 'set_%s' % option_name, None)\n        if setter is None:\n            setattr(target_obj, option_name, value)\n        else:\n            setter(value)\n\n        self.set_options.append(option_name)\n\n    @classmethod\n    def _parse_list(cls, value, separator=','):\n        \"\"\"Represents value as a list.\n\n        Value is split either by separator (defaults to comma) or by lines.\n\n        :param value:\n        :param separator: List items separator character.\n        :rtype: list\n        \"\"\"\n        if isinstance(value, list):  # _get_parser_compound case\n            return value\n\n        if '\\n' in value:\n            value = value.splitlines()\n        else:\n            value = value.split(separator)\n\n        return [chunk.strip() for chunk in value if chunk.strip()]\n\n    @classmethod\n    def _parse_dict(cls, value):\n        \"\"\"Represents value as a dict.\n\n        :param value:\n        :rtype: dict\n        \"\"\"\n        separator = '='\n        result = {}\n        for line in cls._parse_list(value):\n            key, sep, val = line.partition(separator)\n            if sep != separator:\n                raise DistutilsOptionError(\n                    'Unable to parse option value to dict: %s' % value)\n            result[key.strip()] = val.strip()\n\n        return result\n\n    @classmethod\n    def _parse_bool(cls, value):\n        \"\"\"Represents value as boolean.\n\n        :param value:\n        :rtype: bool\n        \"\"\"\n        value = value.lower()\n        return value in ('1', 'true', 'yes')\n\n    @classmethod\n    def _exclude_files_parser(cls, key):\n        \"\"\"Returns a parser function to make sure field inputs\n        are not files.\n\n        Parses a value after getting the key so error messages are\n        more informative.\n\n        :param key:\n        :rtype: callable\n        \"\"\"\n        def parser(value):\n            exclude_directive = 'file:'\n            if value.startswith(exclude_directive):\n                raise ValueError(\n                    'Only strings are accepted for the {0} field, '\n                    'files are not accepted'.format(key))\n            return value\n        return parser\n\n    @classmethod\n    def _parse_file(cls, value):\n        \"\"\"Represents value as a string, allowing including text\n        from nearest files using `file:` directive.\n\n        Directive is sandboxed and won't reach anything outside\n        directory with setup.py.\n\n        Examples:\n            file: README.rst, CHANGELOG.md, src/file.txt\n\n        :param str value:\n        :rtype: str\n        \"\"\"\n        include_directive = 'file:'\n\n        if not isinstance(value, string_types):\n            return value\n\n        if not value.startswith(include_directive):\n            return value\n\n        spec = value[len(include_directive):]\n        filepaths = (os.path.abspath(path.strip()) for path in spec.split(','))\n        return '\\n'.join(\n            cls._read_file(path)\n            for path in filepaths\n            if (cls._assert_local(path) or True)\n            and os.path.isfile(path)\n        )\n\n    @staticmethod\n    def _assert_local(filepath):\n        if not filepath.startswith(os.getcwd()):\n            raise DistutilsOptionError(\n                '`file:` directive can not access %s' % filepath)\n\n    @staticmethod\n    def _read_file(filepath):\n        with io.open(filepath, encoding='utf-8') as f:\n            return f.read()\n\n    @classmethod\n    def _parse_attr(cls, value, package_dir=None):\n        \"\"\"Represents value as a module attribute.\n\n        Examples:\n            attr: package.attr\n            attr: package.module.attr\n\n        :param str value:\n        :rtype: str\n        \"\"\"\n        attr_directive = 'attr:'\n        if not value.startswith(attr_directive):\n            return value\n\n        attrs_path = value.replace(attr_directive, '').strip().split('.')\n        attr_name = attrs_path.pop()\n\n        module_name = '.'.join(attrs_path)\n        module_name = module_name or '__init__'\n\n        parent_path = os.getcwd()\n        if package_dir:\n            if attrs_path[0] in package_dir:\n                # A custom path was specified for the module we want to import\n                custom_path = package_dir[attrs_path[0]]\n                parts = custom_path.rsplit('/', 1)\n                if len(parts) > 1:\n                    parent_path = os.path.join(os.getcwd(), parts[0])\n                    module_name = parts[1]\n                else:\n                    module_name = custom_path\n            elif '' in package_dir:\n                # A custom parent directory was specified for all root modules\n                parent_path = os.path.join(os.getcwd(), package_dir[''])\n        sys.path.insert(0, parent_path)\n        try:\n            module = import_module(module_name)\n            value = getattr(module, attr_name)\n\n        finally:\n            sys.path = sys.path[1:]\n\n        return value\n\n    @classmethod\n    def _get_parser_compound(cls, *parse_methods):\n        \"\"\"Returns parser function to represents value as a list.\n\n        Parses a value applying given methods one after another.\n\n        :param parse_methods:\n        :rtype: callable\n        \"\"\"\n        def parse(value):\n            parsed = value\n\n            for method in parse_methods:\n                parsed = method(parsed)\n\n            return parsed\n\n        return parse\n\n    @classmethod\n    def _parse_section_to_dict(cls, section_options, values_parser=None):\n        \"\"\"Parses section options into a dictionary.\n\n        Optionally applies a given parser to values.\n\n        :param dict section_options:\n        :param callable values_parser:\n        :rtype: dict\n        \"\"\"\n        value = {}\n        values_parser = values_parser or (lambda val: val)\n        for key, (_, val) in section_options.items():\n            value[key] = values_parser(val)\n        return value\n\n    def parse_section(self, section_options):\n        \"\"\"Parses configuration file section.\n\n        :param dict section_options:\n        \"\"\"\n        for (name, (_, value)) in section_options.items():\n            try:\n                self[name] = value\n\n            except KeyError:\n                pass  # Keep silent for a new option may appear anytime.\n\n    def parse(self):\n        \"\"\"Parses configuration file items from one\n        or more related sections.\n\n        \"\"\"\n        for section_name, section_options in self.sections.items():\n\n            method_postfix = ''\n            if section_name:  # [section.option] variant\n                method_postfix = '_%s' % section_name\n\n            section_parser_method = getattr(\n                self,\n                # Dots in section names are translated into dunderscores.\n                ('parse_section%s' % method_postfix).replace('.', '__'),\n                None)\n\n            if section_parser_method is None:\n                raise DistutilsOptionError(\n                    'Unsupported distribution option section: [%s.%s]' % (\n                        self.section_prefix, section_name))\n\n            section_parser_method(section_options)\n\n    def _deprecated_config_handler(self, func, msg, warning_class):\n        \"\"\" this function will wrap around parameters that are deprecated\n\n        :param msg: deprecation message\n        :param warning_class: class of warning exception to be raised\n        :param func: function to be wrapped around\n        \"\"\"\n        @wraps(func)\n        def config_handler(*args, **kwargs):\n            warnings.warn(msg, warning_class)\n            return func(*args, **kwargs)\n\n        return config_handler\n\n\nclass ConfigMetadataHandler(ConfigHandler):\n\n    section_prefix = 'metadata'\n\n    aliases = {\n        'home_page': 'url',\n        'summary': 'description',\n        'classifier': 'classifiers',\n        'platform': 'platforms',\n    }\n\n    strict_mode = False\n    \"\"\"We need to keep it loose, to be partially compatible with\n    `pbr` and `d2to1` packages which also uses `metadata` section.\n\n    \"\"\"\n\n    def __init__(self, target_obj, options, ignore_option_errors=False,\n                 package_dir=None):\n        super(ConfigMetadataHandler, self).__init__(target_obj, options,\n                                                    ignore_option_errors)\n        self.package_dir = package_dir\n\n    @property\n    def parsers(self):\n        \"\"\"Metadata item name to parser function mapping.\"\"\"\n        parse_list = self._parse_list\n        parse_file = self._parse_file\n        parse_dict = self._parse_dict\n        exclude_files_parser = self._exclude_files_parser\n\n        return {\n            'platforms': parse_list,\n            'keywords': parse_list,\n            'provides': parse_list,\n            'requires': self._deprecated_config_handler(\n                parse_list,\n                \"The requires parameter is deprecated, please use \"\n                \"install_requires for runtime dependencies.\",\n                DeprecationWarning),\n            'obsoletes': parse_list,\n            'classifiers': self._get_parser_compound(parse_file, parse_list),\n            'license': exclude_files_parser('license'),\n            'license_files': parse_list,\n            'description': parse_file,\n            'long_description': parse_file,\n            'version': self._parse_version,\n            'project_urls': parse_dict,\n        }\n\n    def _parse_version(self, value):\n        \"\"\"Parses `version` option value.\n\n        :param value:\n        :rtype: str\n\n        \"\"\"\n        version = self._parse_file(value)\n\n        if version != value:\n            version = version.strip()\n            # Be strict about versions loaded from file because it's easy to\n            # accidentally include newlines and other unintended content\n            if isinstance(parse(version), LegacyVersion):\n                tmpl = (\n                    'Version loaded from {value} does not '\n                    'comply with PEP 440: {version}'\n                )\n                raise DistutilsOptionError(tmpl.format(**locals()))\n\n            return version\n\n        version = self._parse_attr(value, self.package_dir)\n\n        if callable(version):\n            version = version()\n\n        if not isinstance(version, string_types):\n            if hasattr(version, '__iter__'):\n                version = '.'.join(map(str, version))\n            else:\n                version = '%s' % version\n\n        return version\n\n\nclass ConfigOptionsHandler(ConfigHandler):\n\n    section_prefix = 'options'\n\n    @property\n    def parsers(self):\n        \"\"\"Metadata item name to parser function mapping.\"\"\"\n        parse_list = self._parse_list\n        parse_list_semicolon = partial(self._parse_list, separator=';')\n        parse_bool = self._parse_bool\n        parse_dict = self._parse_dict\n\n        return {\n            'zip_safe': parse_bool,\n            'use_2to3': parse_bool,\n            'include_package_data': parse_bool,\n            'package_dir': parse_dict,\n            'use_2to3_fixers': parse_list,\n            'use_2to3_exclude_fixers': parse_list,\n            'convert_2to3_doctests': parse_list,\n            'scripts': parse_list,\n            'eager_resources': parse_list,\n            'dependency_links': parse_list,\n            'namespace_packages': parse_list,\n            'install_requires': parse_list_semicolon,\n            'setup_requires': parse_list_semicolon,\n            'tests_require': parse_list_semicolon,\n            'packages': self._parse_packages,\n            'entry_points': self._parse_file,\n            'py_modules': parse_list,\n            'python_requires': SpecifierSet,\n        }\n\n    def _parse_packages(self, value):\n        \"\"\"Parses `packages` option value.\n\n        :param value:\n        :rtype: list\n        \"\"\"\n        find_directives = ['find:', 'find_namespace:']\n        trimmed_value = value.strip()\n\n        if trimmed_value not in find_directives:\n            return self._parse_list(value)\n\n        findns = trimmed_value == find_directives[1]\n        if findns and not PY3:\n            raise DistutilsOptionError(\n                'find_namespace: directive is unsupported on Python < 3.3')\n\n        # Read function arguments from a dedicated section.\n        find_kwargs = self.parse_section_packages__find(\n            self.sections.get('packages.find', {}))\n\n        if findns:\n            from setuptools import find_namespace_packages as find_packages\n        else:\n            from setuptools import find_packages\n\n        return find_packages(**find_kwargs)\n\n    def parse_section_packages__find(self, section_options):\n        \"\"\"Parses `packages.find` configuration file section.\n\n        To be used in conjunction with _parse_packages().\n\n        :param dict section_options:\n        \"\"\"\n        section_data = self._parse_section_to_dict(\n            section_options, self._parse_list)\n\n        valid_keys = ['where', 'include', 'exclude']\n\n        find_kwargs = dict(\n            [(k, v) for k, v in section_data.items() if k in valid_keys and v])\n\n        where = find_kwargs.get('where')\n        if where is not None:\n            find_kwargs['where'] = where[0]  # cast list to single val\n\n        return find_kwargs\n\n    def parse_section_entry_points(self, section_options):\n        \"\"\"Parses `entry_points` configuration file section.\n\n        :param dict section_options:\n        \"\"\"\n        parsed = self._parse_section_to_dict(section_options, self._parse_list)\n        self['entry_points'] = parsed\n\n    def _parse_package_data(self, section_options):\n        parsed = self._parse_section_to_dict(section_options, self._parse_list)\n\n        root = parsed.get('*')\n        if root:\n            parsed[''] = root\n            del parsed['*']\n\n        return parsed\n\n    def parse_section_package_data(self, section_options):\n        \"\"\"Parses `package_data` configuration file section.\n\n        :param dict section_options:\n        \"\"\"\n        self['package_data'] = self._parse_package_data(section_options)\n\n    def parse_section_exclude_package_data(self, section_options):\n        \"\"\"Parses `exclude_package_data` configuration file section.\n\n        :param dict section_options:\n        \"\"\"\n        self['exclude_package_data'] = self._parse_package_data(\n            section_options)\n\n    def parse_section_extras_require(self, section_options):\n        \"\"\"Parses `extras_require` configuration file section.\n\n        :param dict section_options:\n        \"\"\"\n        parse_list = partial(self._parse_list, separator=';')\n        self['extras_require'] = self._parse_section_to_dict(\n            section_options, parse_list)\n\n    def parse_section_data_files(self, section_options):\n        \"\"\"Parses `data_files` configuration file section.\n\n        :param dict section_options:\n        \"\"\"\n        parsed = self._parse_section_to_dict(section_options, self._parse_list)\n        self['data_files'] = [(k, v) for k, v in parsed.items()]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/dep_util.py",
    "content": "from distutils.dep_util import newer_group\n\n# yes, this is was almost entirely copy-pasted from\n# 'newer_pairwise()', this is just another convenience\n# function.\ndef newer_pairwise_group(sources_groups, targets):\n    \"\"\"Walk both arguments in parallel, testing if each source group is newer\n    than its corresponding target. Returns a pair of lists (sources_groups,\n    targets) where sources is newer than target, according to the semantics\n    of 'newer_group()'.\n    \"\"\"\n    if len(sources_groups) != len(targets):\n        raise ValueError(\"'sources_group' and 'targets' must be the same length\")\n\n    # build a pair of lists (sources_groups, targets) where source is newer\n    n_sources = []\n    n_targets = []\n    for i in range(len(sources_groups)):\n        if newer_group(sources_groups[i], targets[i]):\n            n_sources.append(sources_groups[i])\n            n_targets.append(targets[i])\n\n    return n_sources, n_targets\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/depends.py",
    "content": "import sys\nimport marshal\nimport contextlib\nfrom distutils.version import StrictVersion\n\nfrom .py33compat import Bytecode\n\nfrom .py27compat import find_module, PY_COMPILED, PY_FROZEN, PY_SOURCE\nfrom . import py27compat\n\n\n__all__ = [\n    'Require', 'find_module', 'get_module_constant', 'extract_constant'\n]\n\n\nclass Require:\n    \"\"\"A prerequisite to building or installing a distribution\"\"\"\n\n    def __init__(\n            self, name, requested_version, module, homepage='',\n            attribute=None, format=None):\n\n        if format is None and requested_version is not None:\n            format = StrictVersion\n\n        if format is not None:\n            requested_version = format(requested_version)\n            if attribute is None:\n                attribute = '__version__'\n\n        self.__dict__.update(locals())\n        del self.self\n\n    def full_name(self):\n        \"\"\"Return full package/distribution name, w/version\"\"\"\n        if self.requested_version is not None:\n            return '%s-%s' % (self.name, self.requested_version)\n        return self.name\n\n    def version_ok(self, version):\n        \"\"\"Is 'version' sufficiently up-to-date?\"\"\"\n        return self.attribute is None or self.format is None or \\\n            str(version) != \"unknown\" and version >= self.requested_version\n\n    def get_version(self, paths=None, default=\"unknown\"):\n        \"\"\"Get version number of installed module, 'None', or 'default'\n\n        Search 'paths' for module.  If not found, return 'None'.  If found,\n        return the extracted version attribute, or 'default' if no version\n        attribute was specified, or the value cannot be determined without\n        importing the module.  The version is formatted according to the\n        requirement's version format (if any), unless it is 'None' or the\n        supplied 'default'.\n        \"\"\"\n\n        if self.attribute is None:\n            try:\n                f, p, i = find_module(self.module, paths)\n                if f:\n                    f.close()\n                return default\n            except ImportError:\n                return None\n\n        v = get_module_constant(self.module, self.attribute, default, paths)\n\n        if v is not None and v is not default and self.format is not None:\n            return self.format(v)\n\n        return v\n\n    def is_present(self, paths=None):\n        \"\"\"Return true if dependency is present on 'paths'\"\"\"\n        return self.get_version(paths) is not None\n\n    def is_current(self, paths=None):\n        \"\"\"Return true if dependency is present and up-to-date on 'paths'\"\"\"\n        version = self.get_version(paths)\n        if version is None:\n            return False\n        return self.version_ok(version)\n\n\ndef maybe_close(f):\n    @contextlib.contextmanager\n    def empty():\n        yield\n        return\n    if not f:\n        return empty()\n\n    return contextlib.closing(f)\n\n\ndef get_module_constant(module, symbol, default=-1, paths=None):\n    \"\"\"Find 'module' by searching 'paths', and extract 'symbol'\n\n    Return 'None' if 'module' does not exist on 'paths', or it does not define\n    'symbol'.  If the module defines 'symbol' as a constant, return the\n    constant.  Otherwise, return 'default'.\"\"\"\n\n    try:\n        f, path, (suffix, mode, kind) = info = find_module(module, paths)\n    except ImportError:\n        # Module doesn't exist\n        return None\n\n    with maybe_close(f):\n        if kind == PY_COMPILED:\n            f.read(8)  # skip magic & date\n            code = marshal.load(f)\n        elif kind == PY_FROZEN:\n            code = py27compat.get_frozen_object(module, paths)\n        elif kind == PY_SOURCE:\n            code = compile(f.read(), path, 'exec')\n        else:\n            # Not something we can parse; we'll have to import it.  :(\n            imported = py27compat.get_module(module, paths, info)\n            return getattr(imported, symbol, None)\n\n    return extract_constant(code, symbol, default)\n\n\ndef extract_constant(code, symbol, default=-1):\n    \"\"\"Extract the constant value of 'symbol' from 'code'\n\n    If the name 'symbol' is bound to a constant value by the Python code\n    object 'code', return that value.  If 'symbol' is bound to an expression,\n    return 'default'.  Otherwise, return 'None'.\n\n    Return value is based on the first assignment to 'symbol'.  'symbol' must\n    be a global, or at least a non-\"fast\" local in the code block.  That is,\n    only 'STORE_NAME' and 'STORE_GLOBAL' opcodes are checked, and 'symbol'\n    must be present in 'code.co_names'.\n    \"\"\"\n    if symbol not in code.co_names:\n        # name's not there, can't possibly be an assignment\n        return None\n\n    name_idx = list(code.co_names).index(symbol)\n\n    STORE_NAME = 90\n    STORE_GLOBAL = 97\n    LOAD_CONST = 100\n\n    const = default\n\n    for byte_code in Bytecode(code):\n        op = byte_code.opcode\n        arg = byte_code.arg\n\n        if op == LOAD_CONST:\n            const = code.co_consts[arg]\n        elif arg == name_idx and (op == STORE_NAME or op == STORE_GLOBAL):\n            return const\n        else:\n            const = default\n\n\ndef _update_globals():\n    \"\"\"\n    Patch the globals to remove the objects not available on some platforms.\n\n    XXX it'd be better to test assertions about bytecode instead.\n    \"\"\"\n\n    if not sys.platform.startswith('java') and sys.platform != 'cli':\n        return\n    incompatible = 'extract_constant', 'get_module_constant'\n    for name in incompatible:\n        del globals()[name]\n        __all__.remove(name)\n\n\n_update_globals()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/dist.py",
    "content": "# -*- coding: utf-8 -*-\n__all__ = ['Distribution']\n\nimport io\nimport sys\nimport re\nimport os\nimport warnings\nimport numbers\nimport distutils.log\nimport distutils.core\nimport distutils.cmd\nimport distutils.dist\nfrom distutils.util import strtobool\nfrom distutils.debug import DEBUG\nfrom distutils.fancy_getopt import translate_longopt\nimport itertools\n\nfrom collections import defaultdict\nfrom email import message_from_file\n\nfrom distutils.errors import (\n    DistutilsOptionError, DistutilsPlatformError, DistutilsSetupError,\n)\nfrom distutils.util import rfc822_escape\nfrom distutils.version import StrictVersion\n\nfrom setuptools.extern import six\nfrom setuptools.extern import packaging\nfrom setuptools.extern import ordered_set\nfrom setuptools.extern.six.moves import map, filter, filterfalse\n\nfrom . import SetuptoolsDeprecationWarning\n\nfrom setuptools.depends import Require\nfrom setuptools import windows_support\nfrom setuptools.monkey import get_unpatched\nfrom setuptools.config import parse_configuration\nimport pkg_resources\n\n__import__('setuptools.extern.packaging.specifiers')\n__import__('setuptools.extern.packaging.version')\n\n\ndef _get_unpatched(cls):\n    warnings.warn(\"Do not call this function\", DistDeprecationWarning)\n    return get_unpatched(cls)\n\n\ndef get_metadata_version(self):\n    mv = getattr(self, 'metadata_version', None)\n\n    if mv is None:\n        if self.long_description_content_type or self.provides_extras:\n            mv = StrictVersion('2.1')\n        elif (self.maintainer is not None or\n              self.maintainer_email is not None or\n              getattr(self, 'python_requires', None) is not None or\n              self.project_urls):\n            mv = StrictVersion('1.2')\n        elif (self.provides or self.requires or self.obsoletes or\n                self.classifiers or self.download_url):\n            mv = StrictVersion('1.1')\n        else:\n            mv = StrictVersion('1.0')\n\n        self.metadata_version = mv\n\n    return mv\n\n\ndef read_pkg_file(self, file):\n    \"\"\"Reads the metadata values from a file object.\"\"\"\n    msg = message_from_file(file)\n\n    def _read_field(name):\n        value = msg[name]\n        if value == 'UNKNOWN':\n            return None\n        return value\n\n    def _read_list(name):\n        values = msg.get_all(name, None)\n        if values == []:\n            return None\n        return values\n\n    self.metadata_version = StrictVersion(msg['metadata-version'])\n    self.name = _read_field('name')\n    self.version = _read_field('version')\n    self.description = _read_field('summary')\n    # we are filling author only.\n    self.author = _read_field('author')\n    self.maintainer = None\n    self.author_email = _read_field('author-email')\n    self.maintainer_email = None\n    self.url = _read_field('home-page')\n    self.license = _read_field('license')\n\n    if 'download-url' in msg:\n        self.download_url = _read_field('download-url')\n    else:\n        self.download_url = None\n\n    self.long_description = _read_field('description')\n    self.description = _read_field('summary')\n\n    if 'keywords' in msg:\n        self.keywords = _read_field('keywords').split(',')\n\n    self.platforms = _read_list('platform')\n    self.classifiers = _read_list('classifier')\n\n    # PEP 314 - these fields only exist in 1.1\n    if self.metadata_version == StrictVersion('1.1'):\n        self.requires = _read_list('requires')\n        self.provides = _read_list('provides')\n        self.obsoletes = _read_list('obsoletes')\n    else:\n        self.requires = None\n        self.provides = None\n        self.obsoletes = None\n\n\n# Based on Python 3.5 version\ndef write_pkg_file(self, file):\n    \"\"\"Write the PKG-INFO format data to a file object.\n    \"\"\"\n    version = self.get_metadata_version()\n\n    if six.PY2:\n        def write_field(key, value):\n            file.write(\"%s: %s\\n\" % (key, self._encode_field(value)))\n    else:\n        def write_field(key, value):\n            file.write(\"%s: %s\\n\" % (key, value))\n\n    write_field('Metadata-Version', str(version))\n    write_field('Name', self.get_name())\n    write_field('Version', self.get_version())\n    write_field('Summary', self.get_description())\n    write_field('Home-page', self.get_url())\n\n    if version < StrictVersion('1.2'):\n        write_field('Author', self.get_contact())\n        write_field('Author-email', self.get_contact_email())\n    else:\n        optional_fields = (\n            ('Author', 'author'),\n            ('Author-email', 'author_email'),\n            ('Maintainer', 'maintainer'),\n            ('Maintainer-email', 'maintainer_email'),\n        )\n\n        for field, attr in optional_fields:\n            attr_val = getattr(self, attr)\n\n            if attr_val is not None:\n                write_field(field, attr_val)\n\n    write_field('License', self.get_license())\n    if self.download_url:\n        write_field('Download-URL', self.download_url)\n    for project_url in self.project_urls.items():\n        write_field('Project-URL',  '%s, %s' % project_url)\n\n    long_desc = rfc822_escape(self.get_long_description())\n    write_field('Description', long_desc)\n\n    keywords = ','.join(self.get_keywords())\n    if keywords:\n        write_field('Keywords', keywords)\n\n    if version >= StrictVersion('1.2'):\n        for platform in self.get_platforms():\n            write_field('Platform', platform)\n    else:\n        self._write_list(file, 'Platform', self.get_platforms())\n\n    self._write_list(file, 'Classifier', self.get_classifiers())\n\n    # PEP 314\n    self._write_list(file, 'Requires', self.get_requires())\n    self._write_list(file, 'Provides', self.get_provides())\n    self._write_list(file, 'Obsoletes', self.get_obsoletes())\n\n    # Setuptools specific for PEP 345\n    if hasattr(self, 'python_requires'):\n        write_field('Requires-Python', self.python_requires)\n\n    # PEP 566\n    if self.long_description_content_type:\n        write_field(\n            'Description-Content-Type',\n            self.long_description_content_type\n        )\n    if self.provides_extras:\n        for extra in self.provides_extras:\n            write_field('Provides-Extra', extra)\n\n\nsequence = tuple, list\n\n\ndef check_importable(dist, attr, value):\n    try:\n        ep = pkg_resources.EntryPoint.parse('x=' + value)\n        assert not ep.extras\n    except (TypeError, ValueError, AttributeError, AssertionError):\n        raise DistutilsSetupError(\n            \"%r must be importable 'module:attrs' string (got %r)\"\n            % (attr, value)\n        )\n\n\ndef assert_string_list(dist, attr, value):\n    \"\"\"Verify that value is a string list\"\"\"\n    try:\n        # verify that value is a list or tuple to exclude unordered\n        # or single-use iterables\n        assert isinstance(value, (list, tuple))\n        # verify that elements of value are strings\n        assert ''.join(value) != value\n    except (TypeError, ValueError, AttributeError, AssertionError):\n        raise DistutilsSetupError(\n            \"%r must be a list of strings (got %r)\" % (attr, value)\n        )\n\n\ndef check_nsp(dist, attr, value):\n    \"\"\"Verify that namespace packages are valid\"\"\"\n    ns_packages = value\n    assert_string_list(dist, attr, ns_packages)\n    for nsp in ns_packages:\n        if not dist.has_contents_for(nsp):\n            raise DistutilsSetupError(\n                \"Distribution contains no modules or packages for \" +\n                \"namespace package %r\" % nsp\n            )\n        parent, sep, child = nsp.rpartition('.')\n        if parent and parent not in ns_packages:\n            distutils.log.warn(\n                \"WARNING: %r is declared as a package namespace, but %r\"\n                \" is not: please correct this in setup.py\", nsp, parent\n            )\n\n\ndef check_extras(dist, attr, value):\n    \"\"\"Verify that extras_require mapping is valid\"\"\"\n    try:\n        list(itertools.starmap(_check_extra, value.items()))\n    except (TypeError, ValueError, AttributeError):\n        raise DistutilsSetupError(\n            \"'extras_require' must be a dictionary whose values are \"\n            \"strings or lists of strings containing valid project/version \"\n            \"requirement specifiers.\"\n        )\n\n\ndef _check_extra(extra, reqs):\n    name, sep, marker = extra.partition(':')\n    if marker and pkg_resources.invalid_marker(marker):\n        raise DistutilsSetupError(\"Invalid environment marker: \" + marker)\n    list(pkg_resources.parse_requirements(reqs))\n\n\ndef assert_bool(dist, attr, value):\n    \"\"\"Verify that value is True, False, 0, or 1\"\"\"\n    if bool(value) != value:\n        tmpl = \"{attr!r} must be a boolean value (got {value!r})\"\n        raise DistutilsSetupError(tmpl.format(attr=attr, value=value))\n\n\ndef check_requirements(dist, attr, value):\n    \"\"\"Verify that install_requires is a valid requirements list\"\"\"\n    try:\n        list(pkg_resources.parse_requirements(value))\n        if isinstance(value, (dict, set)):\n            raise TypeError(\"Unordered types are not allowed\")\n    except (TypeError, ValueError) as error:\n        tmpl = (\n            \"{attr!r} must be a string or list of strings \"\n            \"containing valid project/version requirement specifiers; {error}\"\n        )\n        raise DistutilsSetupError(tmpl.format(attr=attr, error=error))\n\n\ndef check_specifier(dist, attr, value):\n    \"\"\"Verify that value is a valid version specifier\"\"\"\n    try:\n        packaging.specifiers.SpecifierSet(value)\n    except packaging.specifiers.InvalidSpecifier as error:\n        tmpl = (\n            \"{attr!r} must be a string \"\n            \"containing valid version specifiers; {error}\"\n        )\n        raise DistutilsSetupError(tmpl.format(attr=attr, error=error))\n\n\ndef check_entry_points(dist, attr, value):\n    \"\"\"Verify that entry_points map is parseable\"\"\"\n    try:\n        pkg_resources.EntryPoint.parse_map(value)\n    except ValueError as e:\n        raise DistutilsSetupError(e)\n\n\ndef check_test_suite(dist, attr, value):\n    if not isinstance(value, six.string_types):\n        raise DistutilsSetupError(\"test_suite must be a string\")\n\n\ndef check_package_data(dist, attr, value):\n    \"\"\"Verify that value is a dictionary of package names to glob lists\"\"\"\n    if not isinstance(value, dict):\n        raise DistutilsSetupError(\n            \"{!r} must be a dictionary mapping package names to lists of \"\n            \"string wildcard patterns\".format(attr))\n    for k, v in value.items():\n        if not isinstance(k, six.string_types):\n            raise DistutilsSetupError(\n                \"keys of {!r} dict must be strings (got {!r})\"\n                .format(attr, k)\n            )\n        assert_string_list(dist, 'values of {!r} dict'.format(attr), v)\n\n\ndef check_packages(dist, attr, value):\n    for pkgname in value:\n        if not re.match(r'\\w+(\\.\\w+)*', pkgname):\n            distutils.log.warn(\n                \"WARNING: %r not a valid package name; please use only \"\n                \".-separated package names in setup.py\", pkgname\n            )\n\n\n_Distribution = get_unpatched(distutils.core.Distribution)\n\n\nclass Distribution(_Distribution):\n    \"\"\"Distribution with support for features, tests, and package data\n\n    This is an enhanced version of 'distutils.dist.Distribution' that\n    effectively adds the following new optional keyword arguments to 'setup()':\n\n     'install_requires' -- a string or sequence of strings specifying project\n        versions that the distribution requires when installed, in the format\n        used by 'pkg_resources.require()'.  They will be installed\n        automatically when the package is installed.  If you wish to use\n        packages that are not available in PyPI, or want to give your users an\n        alternate download location, you can add a 'find_links' option to the\n        '[easy_install]' section of your project's 'setup.cfg' file, and then\n        setuptools will scan the listed web pages for links that satisfy the\n        requirements.\n\n     'extras_require' -- a dictionary mapping names of optional \"extras\" to the\n        additional requirement(s) that using those extras incurs. For example,\n        this::\n\n            extras_require = dict(reST = [\"docutils>=0.3\", \"reSTedit\"])\n\n        indicates that the distribution can optionally provide an extra\n        capability called \"reST\", but it can only be used if docutils and\n        reSTedit are installed.  If the user installs your package using\n        EasyInstall and requests one of your extras, the corresponding\n        additional requirements will be installed if needed.\n\n     'features' **deprecated** -- a dictionary mapping option names to\n        'setuptools.Feature'\n        objects.  Features are a portion of the distribution that can be\n        included or excluded based on user options, inter-feature dependencies,\n        and availability on the current system.  Excluded features are omitted\n        from all setup commands, including source and binary distributions, so\n        you can create multiple distributions from the same source tree.\n        Feature names should be valid Python identifiers, except that they may\n        contain the '-' (minus) sign.  Features can be included or excluded\n        via the command line options '--with-X' and '--without-X', where 'X' is\n        the name of the feature.  Whether a feature is included by default, and\n        whether you are allowed to control this from the command line, is\n        determined by the Feature object.  See the 'Feature' class for more\n        information.\n\n     'test_suite' -- the name of a test suite to run for the 'test' command.\n        If the user runs 'python setup.py test', the package will be installed,\n        and the named test suite will be run.  The format is the same as\n        would be used on a 'unittest.py' command line.  That is, it is the\n        dotted name of an object to import and call to generate a test suite.\n\n     'package_data' -- a dictionary mapping package names to lists of filenames\n        or globs to use to find data files contained in the named packages.\n        If the dictionary has filenames or globs listed under '\"\"' (the empty\n        string), those names will be searched for in every package, in addition\n        to any names for the specific package.  Data files found using these\n        names/globs will be installed along with the package, in the same\n        location as the package.  Note that globs are allowed to reference\n        the contents of non-package subdirectories, as long as you use '/' as\n        a path separator.  (Globs are automatically converted to\n        platform-specific paths at runtime.)\n\n    In addition to these new keywords, this class also has several new methods\n    for manipulating the distribution's contents.  For example, the 'include()'\n    and 'exclude()' methods can be thought of as in-place add and subtract\n    commands that add or remove packages, modules, extensions, and so on from\n    the distribution.  They are used by the feature subsystem to configure the\n    distribution for the included and excluded features.\n    \"\"\"\n\n    _DISTUTILS_UNSUPPORTED_METADATA = {\n        'long_description_content_type': None,\n        'project_urls': dict,\n        'provides_extras': ordered_set.OrderedSet,\n        'license_files': ordered_set.OrderedSet,\n    }\n\n    _patched_dist = None\n\n    def patch_missing_pkg_info(self, attrs):\n        # Fake up a replacement for the data that would normally come from\n        # PKG-INFO, but which might not yet be built if this is a fresh\n        # checkout.\n        #\n        if not attrs or 'name' not in attrs or 'version' not in attrs:\n            return\n        key = pkg_resources.safe_name(str(attrs['name'])).lower()\n        dist = pkg_resources.working_set.by_key.get(key)\n        if dist is not None and not dist.has_metadata('PKG-INFO'):\n            dist._version = pkg_resources.safe_version(str(attrs['version']))\n            self._patched_dist = dist\n\n    def __init__(self, attrs=None):\n        have_package_data = hasattr(self, \"package_data\")\n        if not have_package_data:\n            self.package_data = {}\n        attrs = attrs or {}\n        if 'features' in attrs or 'require_features' in attrs:\n            Feature.warn_deprecated()\n        self.require_features = []\n        self.features = {}\n        self.dist_files = []\n        # Filter-out setuptools' specific options.\n        self.src_root = attrs.pop(\"src_root\", None)\n        self.patch_missing_pkg_info(attrs)\n        self.dependency_links = attrs.pop('dependency_links', [])\n        self.setup_requires = attrs.pop('setup_requires', [])\n        for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'):\n            vars(self).setdefault(ep.name, None)\n        _Distribution.__init__(self, {\n            k: v for k, v in attrs.items()\n            if k not in self._DISTUTILS_UNSUPPORTED_METADATA\n        })\n\n        # Fill-in missing metadata fields not supported by distutils.\n        # Note some fields may have been set by other tools (e.g. pbr)\n        # above; they are taken preferrentially to setup() arguments\n        for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items():\n            for source in self.metadata.__dict__, attrs:\n                if option in source:\n                    value = source[option]\n                    break\n            else:\n                value = default() if default else None\n            setattr(self.metadata, option, value)\n\n        if isinstance(self.metadata.version, numbers.Number):\n            # Some people apparently take \"version number\" too literally :)\n            self.metadata.version = str(self.metadata.version)\n\n        if self.metadata.version is not None:\n            try:\n                ver = packaging.version.Version(self.metadata.version)\n                normalized_version = str(ver)\n                if self.metadata.version != normalized_version:\n                    warnings.warn(\n                        \"Normalizing '%s' to '%s'\" % (\n                            self.metadata.version,\n                            normalized_version,\n                        )\n                    )\n                    self.metadata.version = normalized_version\n            except (packaging.version.InvalidVersion, TypeError):\n                warnings.warn(\n                    \"The version specified (%r) is an invalid version, this \"\n                    \"may not work as expected with newer versions of \"\n                    \"setuptools, pip, and PyPI. Please see PEP 440 for more \"\n                    \"details.\" % self.metadata.version\n                )\n        self._finalize_requires()\n\n    def _finalize_requires(self):\n        \"\"\"\n        Set `metadata.python_requires` and fix environment markers\n        in `install_requires` and `extras_require`.\n        \"\"\"\n        if getattr(self, 'python_requires', None):\n            self.metadata.python_requires = self.python_requires\n\n        if getattr(self, 'extras_require', None):\n            for extra in self.extras_require.keys():\n                # Since this gets called multiple times at points where the\n                # keys have become 'converted' extras, ensure that we are only\n                # truly adding extras we haven't seen before here.\n                extra = extra.split(':')[0]\n                if extra:\n                    self.metadata.provides_extras.add(extra)\n\n        self._convert_extras_requirements()\n        self._move_install_requirements_markers()\n\n    def _convert_extras_requirements(self):\n        \"\"\"\n        Convert requirements in `extras_require` of the form\n        `\"extra\": [\"barbazquux; {marker}\"]` to\n        `\"extra:{marker}\": [\"barbazquux\"]`.\n        \"\"\"\n        spec_ext_reqs = getattr(self, 'extras_require', None) or {}\n        self._tmp_extras_require = defaultdict(list)\n        for section, v in spec_ext_reqs.items():\n            # Do not strip empty sections.\n            self._tmp_extras_require[section]\n            for r in pkg_resources.parse_requirements(v):\n                suffix = self._suffix_for(r)\n                self._tmp_extras_require[section + suffix].append(r)\n\n    @staticmethod\n    def _suffix_for(req):\n        \"\"\"\n        For a requirement, return the 'extras_require' suffix for\n        that requirement.\n        \"\"\"\n        return ':' + str(req.marker) if req.marker else ''\n\n    def _move_install_requirements_markers(self):\n        \"\"\"\n        Move requirements in `install_requires` that are using environment\n        markers `extras_require`.\n        \"\"\"\n\n        # divide the install_requires into two sets, simple ones still\n        # handled by install_requires and more complex ones handled\n        # by extras_require.\n\n        def is_simple_req(req):\n            return not req.marker\n\n        spec_inst_reqs = getattr(self, 'install_requires', None) or ()\n        inst_reqs = list(pkg_resources.parse_requirements(spec_inst_reqs))\n        simple_reqs = filter(is_simple_req, inst_reqs)\n        complex_reqs = filterfalse(is_simple_req, inst_reqs)\n        self.install_requires = list(map(str, simple_reqs))\n\n        for r in complex_reqs:\n            self._tmp_extras_require[':' + str(r.marker)].append(r)\n        self.extras_require = dict(\n            (k, [str(r) for r in map(self._clean_req, v)])\n            for k, v in self._tmp_extras_require.items()\n        )\n\n    def _clean_req(self, req):\n        \"\"\"\n        Given a Requirement, remove environment markers and return it.\n        \"\"\"\n        req.marker = None\n        return req\n\n    def _parse_config_files(self, filenames=None):\n        \"\"\"\n        Adapted from distutils.dist.Distribution.parse_config_files,\n        this method provides the same functionality in subtly-improved\n        ways.\n        \"\"\"\n        from setuptools.extern.six.moves.configparser import ConfigParser\n\n        # Ignore install directory options if we have a venv\n        if not six.PY2 and sys.prefix != sys.base_prefix:\n            ignore_options = [\n                'install-base', 'install-platbase', 'install-lib',\n                'install-platlib', 'install-purelib', 'install-headers',\n                'install-scripts', 'install-data', 'prefix', 'exec-prefix',\n                'home', 'user', 'root']\n        else:\n            ignore_options = []\n\n        ignore_options = frozenset(ignore_options)\n\n        if filenames is None:\n            filenames = self.find_config_files()\n\n        if DEBUG:\n            self.announce(\"Distribution.parse_config_files():\")\n\n        parser = ConfigParser()\n        for filename in filenames:\n            with io.open(filename, encoding='utf-8') as reader:\n                if DEBUG:\n                    self.announce(\"  reading {filename}\".format(**locals()))\n                (parser.readfp if six.PY2 else parser.read_file)(reader)\n            for section in parser.sections():\n                options = parser.options(section)\n                opt_dict = self.get_option_dict(section)\n\n                for opt in options:\n                    if opt != '__name__' and opt not in ignore_options:\n                        val = self._try_str(parser.get(section, opt))\n                        opt = opt.replace('-', '_')\n                        opt_dict[opt] = (filename, val)\n\n            # Make the ConfigParser forget everything (so we retain\n            # the original filenames that options come from)\n            parser.__init__()\n\n        # If there was a \"global\" section in the config file, use it\n        # to set Distribution options.\n\n        if 'global' in self.command_options:\n            for (opt, (src, val)) in self.command_options['global'].items():\n                alias = self.negative_opt.get(opt)\n                try:\n                    if alias:\n                        setattr(self, alias, not strtobool(val))\n                    elif opt in ('verbose', 'dry_run'):  # ugh!\n                        setattr(self, opt, strtobool(val))\n                    else:\n                        setattr(self, opt, val)\n                except ValueError as msg:\n                    raise DistutilsOptionError(msg)\n\n    @staticmethod\n    def _try_str(val):\n        \"\"\"\n        On Python 2, much of distutils relies on string values being of\n        type 'str' (bytes) and not unicode text. If the value can be safely\n        encoded to bytes using the default encoding, prefer that.\n\n        Why the default encoding? Because that value can be implicitly\n        decoded back to text if needed.\n\n        Ref #1653\n        \"\"\"\n        if not six.PY2:\n            return val\n        try:\n            return val.encode()\n        except UnicodeEncodeError:\n            pass\n        return val\n\n    def _set_command_options(self, command_obj, option_dict=None):\n        \"\"\"\n        Set the options for 'command_obj' from 'option_dict'.  Basically\n        this means copying elements of a dictionary ('option_dict') to\n        attributes of an instance ('command').\n\n        'command_obj' must be a Command instance.  If 'option_dict' is not\n        supplied, uses the standard option dictionary for this command\n        (from 'self.command_options').\n\n        (Adopted from distutils.dist.Distribution._set_command_options)\n        \"\"\"\n        command_name = command_obj.get_command_name()\n        if option_dict is None:\n            option_dict = self.get_option_dict(command_name)\n\n        if DEBUG:\n            self.announce(\"  setting options for '%s' command:\" % command_name)\n        for (option, (source, value)) in option_dict.items():\n            if DEBUG:\n                self.announce(\"    %s = %s (from %s)\" % (option, value,\n                                                         source))\n            try:\n                bool_opts = [translate_longopt(o)\n                             for o in command_obj.boolean_options]\n            except AttributeError:\n                bool_opts = []\n            try:\n                neg_opt = command_obj.negative_opt\n            except AttributeError:\n                neg_opt = {}\n\n            try:\n                is_string = isinstance(value, six.string_types)\n                if option in neg_opt and is_string:\n                    setattr(command_obj, neg_opt[option], not strtobool(value))\n                elif option in bool_opts and is_string:\n                    setattr(command_obj, option, strtobool(value))\n                elif hasattr(command_obj, option):\n                    setattr(command_obj, option, value)\n                else:\n                    raise DistutilsOptionError(\n                        \"error in %s: command '%s' has no such option '%s'\"\n                        % (source, command_name, option))\n            except ValueError as msg:\n                raise DistutilsOptionError(msg)\n\n    def parse_config_files(self, filenames=None, ignore_option_errors=False):\n        \"\"\"Parses configuration files from various levels\n        and loads configuration.\n\n        \"\"\"\n        self._parse_config_files(filenames=filenames)\n\n        parse_configuration(self, self.command_options,\n                            ignore_option_errors=ignore_option_errors)\n        self._finalize_requires()\n\n    def parse_command_line(self):\n        \"\"\"Process features after parsing command line options\"\"\"\n        result = _Distribution.parse_command_line(self)\n        if self.features:\n            self._finalize_features()\n        return result\n\n    def _feature_attrname(self, name):\n        \"\"\"Convert feature name to corresponding option attribute name\"\"\"\n        return 'with_' + name.replace('-', '_')\n\n    def fetch_build_eggs(self, requires):\n        \"\"\"Resolve pre-setup requirements\"\"\"\n        resolved_dists = pkg_resources.working_set.resolve(\n            pkg_resources.parse_requirements(requires),\n            installer=self.fetch_build_egg,\n            replace_conflicting=True,\n        )\n        for dist in resolved_dists:\n            pkg_resources.working_set.add(dist, replace=True)\n        return resolved_dists\n\n    def finalize_options(self):\n        \"\"\"\n        Allow plugins to apply arbitrary operations to the\n        distribution. Each hook may optionally define a 'order'\n        to influence the order of execution. Smaller numbers\n        go first and the default is 0.\n        \"\"\"\n        hook_key = 'setuptools.finalize_distribution_options'\n\n        def by_order(hook):\n            return getattr(hook, 'order', 0)\n        eps = pkg_resources.iter_entry_points(hook_key)\n        for ep in sorted(eps, key=by_order):\n            ep.load()(self)\n\n    def _finalize_setup_keywords(self):\n        for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'):\n            value = getattr(self, ep.name, None)\n            if value is not None:\n                ep.require(installer=self.fetch_build_egg)\n                ep.load()(self, ep.name, value)\n\n    def _finalize_2to3_doctests(self):\n        if getattr(self, 'convert_2to3_doctests', None):\n            # XXX may convert to set here when we can rely on set being builtin\n            self.convert_2to3_doctests = [\n                os.path.abspath(p)\n                for p in self.convert_2to3_doctests\n            ]\n        else:\n            self.convert_2to3_doctests = []\n\n    def get_egg_cache_dir(self):\n        egg_cache_dir = os.path.join(os.curdir, '.eggs')\n        if not os.path.exists(egg_cache_dir):\n            os.mkdir(egg_cache_dir)\n            windows_support.hide_file(egg_cache_dir)\n            readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt')\n            with open(readme_txt_filename, 'w') as f:\n                f.write('This directory contains eggs that were downloaded '\n                        'by setuptools to build, test, and run plug-ins.\\n\\n')\n                f.write('This directory caches those eggs to prevent '\n                        'repeated downloads.\\n\\n')\n                f.write('However, it is safe to delete this directory.\\n\\n')\n\n        return egg_cache_dir\n\n    def fetch_build_egg(self, req):\n        \"\"\"Fetch an egg needed for building\"\"\"\n        from setuptools.installer import fetch_build_egg\n        return fetch_build_egg(self, req)\n\n    def _finalize_feature_opts(self):\n        \"\"\"Add --with-X/--without-X options based on optional features\"\"\"\n\n        if not self.features:\n            return\n\n        go = []\n        no = self.negative_opt.copy()\n\n        for name, feature in self.features.items():\n            self._set_feature(name, None)\n            feature.validate(self)\n\n            if feature.optional:\n                descr = feature.description\n                incdef = ' (default)'\n                excdef = ''\n                if not feature.include_by_default():\n                    excdef, incdef = incdef, excdef\n\n                new = (\n                    ('with-' + name, None, 'include ' + descr + incdef),\n                    ('without-' + name, None, 'exclude ' + descr + excdef),\n                )\n                go.extend(new)\n                no['without-' + name] = 'with-' + name\n\n        self.global_options = self.feature_options = go + self.global_options\n        self.negative_opt = self.feature_negopt = no\n\n    def _finalize_features(self):\n        \"\"\"Add/remove features and resolve dependencies between them\"\"\"\n\n        # First, flag all the enabled items (and thus their dependencies)\n        for name, feature in self.features.items():\n            enabled = self.feature_is_included(name)\n            if enabled or (enabled is None and feature.include_by_default()):\n                feature.include_in(self)\n                self._set_feature(name, 1)\n\n        # Then disable the rest, so that off-by-default features don't\n        # get flagged as errors when they're required by an enabled feature\n        for name, feature in self.features.items():\n            if not self.feature_is_included(name):\n                feature.exclude_from(self)\n                self._set_feature(name, 0)\n\n    def get_command_class(self, command):\n        \"\"\"Pluggable version of get_command_class()\"\"\"\n        if command in self.cmdclass:\n            return self.cmdclass[command]\n\n        eps = pkg_resources.iter_entry_points('distutils.commands', command)\n        for ep in eps:\n            ep.require(installer=self.fetch_build_egg)\n            self.cmdclass[command] = cmdclass = ep.load()\n            return cmdclass\n        else:\n            return _Distribution.get_command_class(self, command)\n\n    def print_commands(self):\n        for ep in pkg_resources.iter_entry_points('distutils.commands'):\n            if ep.name not in self.cmdclass:\n                # don't require extras as the commands won't be invoked\n                cmdclass = ep.resolve()\n                self.cmdclass[ep.name] = cmdclass\n        return _Distribution.print_commands(self)\n\n    def get_command_list(self):\n        for ep in pkg_resources.iter_entry_points('distutils.commands'):\n            if ep.name not in self.cmdclass:\n                # don't require extras as the commands won't be invoked\n                cmdclass = ep.resolve()\n                self.cmdclass[ep.name] = cmdclass\n        return _Distribution.get_command_list(self)\n\n    def _set_feature(self, name, status):\n        \"\"\"Set feature's inclusion status\"\"\"\n        setattr(self, self._feature_attrname(name), status)\n\n    def feature_is_included(self, name):\n        \"\"\"Return 1 if feature is included, 0 if excluded, 'None' if unknown\"\"\"\n        return getattr(self, self._feature_attrname(name))\n\n    def include_feature(self, name):\n        \"\"\"Request inclusion of feature named 'name'\"\"\"\n\n        if self.feature_is_included(name) == 0:\n            descr = self.features[name].description\n            raise DistutilsOptionError(\n                descr + \" is required, but was excluded or is not available\"\n            )\n        self.features[name].include_in(self)\n        self._set_feature(name, 1)\n\n    def include(self, **attrs):\n        \"\"\"Add items to distribution that are named in keyword arguments\n\n        For example, 'dist.include(py_modules=[\"x\"])' would add 'x' to\n        the distribution's 'py_modules' attribute, if it was not already\n        there.\n\n        Currently, this method only supports inclusion for attributes that are\n        lists or tuples.  If you need to add support for adding to other\n        attributes in this or a subclass, you can add an '_include_X' method,\n        where 'X' is the name of the attribute.  The method will be called with\n        the value passed to 'include()'.  So, 'dist.include(foo={\"bar\":\"baz\"})'\n        will try to call 'dist._include_foo({\"bar\":\"baz\"})', which can then\n        handle whatever special inclusion logic is needed.\n        \"\"\"\n        for k, v in attrs.items():\n            include = getattr(self, '_include_' + k, None)\n            if include:\n                include(v)\n            else:\n                self._include_misc(k, v)\n\n    def exclude_package(self, package):\n        \"\"\"Remove packages, modules, and extensions in named package\"\"\"\n\n        pfx = package + '.'\n        if self.packages:\n            self.packages = [\n                p for p in self.packages\n                if p != package and not p.startswith(pfx)\n            ]\n\n        if self.py_modules:\n            self.py_modules = [\n                p for p in self.py_modules\n                if p != package and not p.startswith(pfx)\n            ]\n\n        if self.ext_modules:\n            self.ext_modules = [\n                p for p in self.ext_modules\n                if p.name != package and not p.name.startswith(pfx)\n            ]\n\n    def has_contents_for(self, package):\n        \"\"\"Return true if 'exclude_package(package)' would do something\"\"\"\n\n        pfx = package + '.'\n\n        for p in self.iter_distribution_names():\n            if p == package or p.startswith(pfx):\n                return True\n\n    def _exclude_misc(self, name, value):\n        \"\"\"Handle 'exclude()' for list/tuple attrs without a special handler\"\"\"\n        if not isinstance(value, sequence):\n            raise DistutilsSetupError(\n                \"%s: setting must be a list or tuple (%r)\" % (name, value)\n            )\n        try:\n            old = getattr(self, name)\n        except AttributeError:\n            raise DistutilsSetupError(\n                \"%s: No such distribution setting\" % name\n            )\n        if old is not None and not isinstance(old, sequence):\n            raise DistutilsSetupError(\n                name + \": this setting cannot be changed via include/exclude\"\n            )\n        elif old:\n            setattr(self, name, [item for item in old if item not in value])\n\n    def _include_misc(self, name, value):\n        \"\"\"Handle 'include()' for list/tuple attrs without a special handler\"\"\"\n\n        if not isinstance(value, sequence):\n            raise DistutilsSetupError(\n                \"%s: setting must be a list (%r)\" % (name, value)\n            )\n        try:\n            old = getattr(self, name)\n        except AttributeError:\n            raise DistutilsSetupError(\n                \"%s: No such distribution setting\" % name\n            )\n        if old is None:\n            setattr(self, name, value)\n        elif not isinstance(old, sequence):\n            raise DistutilsSetupError(\n                name + \": this setting cannot be changed via include/exclude\"\n            )\n        else:\n            new = [item for item in value if item not in old]\n            setattr(self, name, old + new)\n\n    def exclude(self, **attrs):\n        \"\"\"Remove items from distribution that are named in keyword arguments\n\n        For example, 'dist.exclude(py_modules=[\"x\"])' would remove 'x' from\n        the distribution's 'py_modules' attribute.  Excluding packages uses\n        the 'exclude_package()' method, so all of the package's contained\n        packages, modules, and extensions are also excluded.\n\n        Currently, this method only supports exclusion from attributes that are\n        lists or tuples.  If you need to add support for excluding from other\n        attributes in this or a subclass, you can add an '_exclude_X' method,\n        where 'X' is the name of the attribute.  The method will be called with\n        the value passed to 'exclude()'.  So, 'dist.exclude(foo={\"bar\":\"baz\"})'\n        will try to call 'dist._exclude_foo({\"bar\":\"baz\"})', which can then\n        handle whatever special exclusion logic is needed.\n        \"\"\"\n        for k, v in attrs.items():\n            exclude = getattr(self, '_exclude_' + k, None)\n            if exclude:\n                exclude(v)\n            else:\n                self._exclude_misc(k, v)\n\n    def _exclude_packages(self, packages):\n        if not isinstance(packages, sequence):\n            raise DistutilsSetupError(\n                \"packages: setting must be a list or tuple (%r)\" % (packages,)\n            )\n        list(map(self.exclude_package, packages))\n\n    def _parse_command_opts(self, parser, args):\n        # Remove --with-X/--without-X options when processing command args\n        self.global_options = self.__class__.global_options\n        self.negative_opt = self.__class__.negative_opt\n\n        # First, expand any aliases\n        command = args[0]\n        aliases = self.get_option_dict('aliases')\n        while command in aliases:\n            src, alias = aliases[command]\n            del aliases[command]  # ensure each alias can expand only once!\n            import shlex\n            args[:1] = shlex.split(alias, True)\n            command = args[0]\n\n        nargs = _Distribution._parse_command_opts(self, parser, args)\n\n        # Handle commands that want to consume all remaining arguments\n        cmd_class = self.get_command_class(command)\n        if getattr(cmd_class, 'command_consumes_arguments', None):\n            self.get_option_dict(command)['args'] = (\"command line\", nargs)\n            if nargs is not None:\n                return []\n\n        return nargs\n\n    def get_cmdline_options(self):\n        \"\"\"Return a '{cmd: {opt:val}}' map of all command-line options\n\n        Option names are all long, but do not include the leading '--', and\n        contain dashes rather than underscores.  If the option doesn't take\n        an argument (e.g. '--quiet'), the 'val' is 'None'.\n\n        Note that options provided by config files are intentionally excluded.\n        \"\"\"\n\n        d = {}\n\n        for cmd, opts in self.command_options.items():\n\n            for opt, (src, val) in opts.items():\n\n                if src != \"command line\":\n                    continue\n\n                opt = opt.replace('_', '-')\n\n                if val == 0:\n                    cmdobj = self.get_command_obj(cmd)\n                    neg_opt = self.negative_opt.copy()\n                    neg_opt.update(getattr(cmdobj, 'negative_opt', {}))\n                    for neg, pos in neg_opt.items():\n                        if pos == opt:\n                            opt = neg\n                            val = None\n                            break\n                    else:\n                        raise AssertionError(\"Shouldn't be able to get here\")\n\n                elif val == 1:\n                    val = None\n\n                d.setdefault(cmd, {})[opt] = val\n\n        return d\n\n    def iter_distribution_names(self):\n        \"\"\"Yield all packages, modules, and extension names in distribution\"\"\"\n\n        for pkg in self.packages or ():\n            yield pkg\n\n        for module in self.py_modules or ():\n            yield module\n\n        for ext in self.ext_modules or ():\n            if isinstance(ext, tuple):\n                name, buildinfo = ext\n            else:\n                name = ext.name\n            if name.endswith('module'):\n                name = name[:-6]\n            yield name\n\n    def handle_display_options(self, option_order):\n        \"\"\"If there were any non-global \"display-only\" options\n        (--help-commands or the metadata display options) on the command\n        line, display the requested info and return true; else return\n        false.\n        \"\"\"\n        import sys\n\n        if six.PY2 or self.help_commands:\n            return _Distribution.handle_display_options(self, option_order)\n\n        # Stdout may be StringIO (e.g. in tests)\n        if not isinstance(sys.stdout, io.TextIOWrapper):\n            return _Distribution.handle_display_options(self, option_order)\n\n        # Don't wrap stdout if utf-8 is already the encoding. Provides\n        #  workaround for #334.\n        if sys.stdout.encoding.lower() in ('utf-8', 'utf8'):\n            return _Distribution.handle_display_options(self, option_order)\n\n        # Print metadata in UTF-8 no matter the platform\n        encoding = sys.stdout.encoding\n        errors = sys.stdout.errors\n        newline = sys.platform != 'win32' and '\\n' or None\n        line_buffering = sys.stdout.line_buffering\n\n        sys.stdout = io.TextIOWrapper(\n            sys.stdout.detach(), 'utf-8', errors, newline, line_buffering)\n        try:\n            return _Distribution.handle_display_options(self, option_order)\n        finally:\n            sys.stdout = io.TextIOWrapper(\n                sys.stdout.detach(), encoding, errors, newline, line_buffering)\n\n\nclass Feature:\n    \"\"\"\n    **deprecated** -- The `Feature` facility was never completely implemented\n    or supported, `has reported issues\n    <https://github.com/pypa/setuptools/issues/58>`_ and will be removed in\n    a future version.\n\n    A subset of the distribution that can be excluded if unneeded/wanted\n\n    Features are created using these keyword arguments:\n\n      'description' -- a short, human readable description of the feature, to\n         be used in error messages, and option help messages.\n\n      'standard' -- if true, the feature is included by default if it is\n         available on the current system.  Otherwise, the feature is only\n         included if requested via a command line '--with-X' option, or if\n         another included feature requires it.  The default setting is 'False'.\n\n      'available' -- if true, the feature is available for installation on the\n         current system.  The default setting is 'True'.\n\n      'optional' -- if true, the feature's inclusion can be controlled from the\n         command line, using the '--with-X' or '--without-X' options.  If\n         false, the feature's inclusion status is determined automatically,\n         based on 'availabile', 'standard', and whether any other feature\n         requires it.  The default setting is 'True'.\n\n      'require_features' -- a string or sequence of strings naming features\n         that should also be included if this feature is included.  Defaults to\n         empty list.  May also contain 'Require' objects that should be\n         added/removed from the distribution.\n\n      'remove' -- a string or list of strings naming packages to be removed\n         from the distribution if this feature is *not* included.  If the\n         feature *is* included, this argument is ignored.  This argument exists\n         to support removing features that \"crosscut\" a distribution, such as\n         defining a 'tests' feature that removes all the 'tests' subpackages\n         provided by other features.  The default for this argument is an empty\n         list.  (Note: the named package(s) or modules must exist in the base\n         distribution when the 'setup()' function is initially called.)\n\n      other keywords -- any other keyword arguments are saved, and passed to\n         the distribution's 'include()' and 'exclude()' methods when the\n         feature is included or excluded, respectively.  So, for example, you\n         could pass 'packages=[\"a\",\"b\"]' to cause packages 'a' and 'b' to be\n         added or removed from the distribution as appropriate.\n\n    A feature must include at least one 'requires', 'remove', or other\n    keyword argument.  Otherwise, it can't affect the distribution in any way.\n    Note also that you can subclass 'Feature' to create your own specialized\n    feature types that modify the distribution in other ways when included or\n    excluded.  See the docstrings for the various methods here for more detail.\n    Aside from the methods, the only feature attributes that distributions look\n    at are 'description' and 'optional'.\n    \"\"\"\n\n    @staticmethod\n    def warn_deprecated():\n        msg = (\n            \"Features are deprecated and will be removed in a future \"\n            \"version. See https://github.com/pypa/setuptools/issues/65.\"\n        )\n        warnings.warn(msg, DistDeprecationWarning, stacklevel=3)\n\n    def __init__(\n            self, description, standard=False, available=True,\n            optional=True, require_features=(), remove=(), **extras):\n        self.warn_deprecated()\n\n        self.description = description\n        self.standard = standard\n        self.available = available\n        self.optional = optional\n        if isinstance(require_features, (str, Require)):\n            require_features = require_features,\n\n        self.require_features = [\n            r for r in require_features if isinstance(r, str)\n        ]\n        er = [r for r in require_features if not isinstance(r, str)]\n        if er:\n            extras['require_features'] = er\n\n        if isinstance(remove, str):\n            remove = remove,\n        self.remove = remove\n        self.extras = extras\n\n        if not remove and not require_features and not extras:\n            raise DistutilsSetupError(\n                \"Feature %s: must define 'require_features', 'remove', or \"\n                \"at least one of 'packages', 'py_modules', etc.\"\n            )\n\n    def include_by_default(self):\n        \"\"\"Should this feature be included by default?\"\"\"\n        return self.available and self.standard\n\n    def include_in(self, dist):\n        \"\"\"Ensure feature and its requirements are included in distribution\n\n        You may override this in a subclass to perform additional operations on\n        the distribution.  Note that this method may be called more than once\n        per feature, and so should be idempotent.\n\n        \"\"\"\n\n        if not self.available:\n            raise DistutilsPlatformError(\n                self.description + \" is required, \"\n                \"but is not available on this platform\"\n            )\n\n        dist.include(**self.extras)\n\n        for f in self.require_features:\n            dist.include_feature(f)\n\n    def exclude_from(self, dist):\n        \"\"\"Ensure feature is excluded from distribution\n\n        You may override this in a subclass to perform additional operations on\n        the distribution.  This method will be called at most once per\n        feature, and only after all included features have been asked to\n        include themselves.\n        \"\"\"\n\n        dist.exclude(**self.extras)\n\n        if self.remove:\n            for item in self.remove:\n                dist.exclude_package(item)\n\n    def validate(self, dist):\n        \"\"\"Verify that feature makes sense in context of distribution\n\n        This method is called by the distribution just before it parses its\n        command line.  It checks to ensure that the 'remove' attribute, if any,\n        contains only valid package/module names that are present in the base\n        distribution when 'setup()' is called.  You may override it in a\n        subclass to perform any other required validation of the feature\n        against a target distribution.\n        \"\"\"\n\n        for item in self.remove:\n            if not dist.has_contents_for(item):\n                raise DistutilsSetupError(\n                    \"%s wants to be able to remove %s, but the distribution\"\n                    \" doesn't contain any packages or modules under %s\"\n                    % (self.description, item, item)\n                )\n\n\nclass DistDeprecationWarning(SetuptoolsDeprecationWarning):\n    \"\"\"Class for warning about deprecations in dist in\n    setuptools. Not ignored by default, unlike DeprecationWarning.\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/errors.py",
    "content": "\"\"\"setuptools.errors\n\nProvides exceptions used by setuptools modules.\n\"\"\"\n\nfrom distutils.errors import DistutilsError\n\n\nclass RemovedCommandError(DistutilsError, RuntimeError):\n    \"\"\"Error used for commands that have been removed in setuptools.\n\n    Since ``setuptools`` is built on ``distutils``, simply removing a command\n    from ``setuptools`` will make the behavior fall back to ``distutils``; this\n    error is raised if a command exists in ``distutils`` but has been actively\n    removed in ``setuptools``.\n    \"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/extension.py",
    "content": "import re\nimport functools\nimport distutils.core\nimport distutils.errors\nimport distutils.extension\n\nfrom setuptools.extern.six.moves import map\n\nfrom .monkey import get_unpatched\n\n\ndef _have_cython():\n    \"\"\"\n    Return True if Cython can be imported.\n    \"\"\"\n    cython_impl = 'Cython.Distutils.build_ext'\n    try:\n        # from (cython_impl) import build_ext\n        __import__(cython_impl, fromlist=['build_ext']).build_ext\n        return True\n    except Exception:\n        pass\n    return False\n\n\n# for compatibility\nhave_pyrex = _have_cython\n\n_Extension = get_unpatched(distutils.core.Extension)\n\n\nclass Extension(_Extension):\n    \"\"\"Extension that uses '.c' files in place of '.pyx' files\"\"\"\n\n    def __init__(self, name, sources, *args, **kw):\n        # The *args is needed for compatibility as calls may use positional\n        # arguments. py_limited_api may be set only via keyword.\n        self.py_limited_api = kw.pop(\"py_limited_api\", False)\n        _Extension.__init__(self, name, sources, *args, **kw)\n\n    def _convert_pyx_sources_to_lang(self):\n        \"\"\"\n        Replace sources with .pyx extensions to sources with the target\n        language extension. This mechanism allows language authors to supply\n        pre-converted sources but to prefer the .pyx sources.\n        \"\"\"\n        if _have_cython():\n            # the build has Cython, so allow it to compile the .pyx files\n            return\n        lang = self.language or ''\n        target_ext = '.cpp' if lang.lower() == 'c++' else '.c'\n        sub = functools.partial(re.sub, '.pyx$', target_ext)\n        self.sources = list(map(sub, self.sources))\n\n\nclass Library(Extension):\n    \"\"\"Just like a regular Extension, but built as a library instead\"\"\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/extern/__init__.py",
    "content": "import sys\n\n\nclass VendorImporter:\n    \"\"\"\n    A PEP 302 meta path importer for finding optionally-vendored\n    or otherwise naturally-installed packages from root_name.\n    \"\"\"\n\n    def __init__(self, root_name, vendored_names=(), vendor_pkg=None):\n        self.root_name = root_name\n        self.vendored_names = set(vendored_names)\n        self.vendor_pkg = vendor_pkg or root_name.replace('extern', '_vendor')\n\n    @property\n    def search_path(self):\n        \"\"\"\n        Search first the vendor package then as a natural package.\n        \"\"\"\n        yield self.vendor_pkg + '.'\n        yield ''\n\n    def find_module(self, fullname, path=None):\n        \"\"\"\n        Return self when fullname starts with root_name and the\n        target module is one vendored through this importer.\n        \"\"\"\n        root, base, target = fullname.partition(self.root_name + '.')\n        if root:\n            return\n        if not any(map(target.startswith, self.vendored_names)):\n            return\n        return self\n\n    def load_module(self, fullname):\n        \"\"\"\n        Iterate over the search path to locate and load fullname.\n        \"\"\"\n        root, base, target = fullname.partition(self.root_name + '.')\n        for prefix in self.search_path:\n            try:\n                extant = prefix + target\n                __import__(extant)\n                mod = sys.modules[extant]\n                sys.modules[fullname] = mod\n                # mysterious hack:\n                # Remove the reference to the extant package/module\n                # on later Python versions to cause relative imports\n                # in the vendor package to resolve the same modules\n                # as those going through this importer.\n                if sys.version_info >= (3, ):\n                    del sys.modules[extant]\n                return mod\n            except ImportError:\n                pass\n        else:\n            raise ImportError(\n                \"The '{target}' package is required; \"\n                \"normally this is bundled with this package so if you get \"\n                \"this warning, consult the packager of your \"\n                \"distribution.\".format(**locals())\n            )\n\n    def install(self):\n        \"\"\"\n        Install this importer into sys.meta_path if not already present.\n        \"\"\"\n        if self not in sys.meta_path:\n            sys.meta_path.append(self)\n\n\nnames = 'six', 'packaging', 'pyparsing', 'ordered_set',\nVendorImporter(__name__, names, 'setuptools._vendor').install()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/glob.py",
    "content": "\"\"\"\nFilename globbing utility. Mostly a copy of `glob` from Python 3.5.\n\nChanges include:\n * `yield from` and PEP3102 `*` removed.\n * Hidden files are not ignored.\n\"\"\"\n\nimport os\nimport re\nimport fnmatch\n\n__all__ = [\"glob\", \"iglob\", \"escape\"]\n\n\ndef glob(pathname, recursive=False):\n    \"\"\"Return a list of paths matching a pathname pattern.\n\n    The pattern may contain simple shell-style wildcards a la\n    fnmatch. However, unlike fnmatch, filenames starting with a\n    dot are special cases that are not matched by '*' and '?'\n    patterns.\n\n    If recursive is true, the pattern '**' will match any files and\n    zero or more directories and subdirectories.\n    \"\"\"\n    return list(iglob(pathname, recursive=recursive))\n\n\ndef iglob(pathname, recursive=False):\n    \"\"\"Return an iterator which yields the paths matching a pathname pattern.\n\n    The pattern may contain simple shell-style wildcards a la\n    fnmatch. However, unlike fnmatch, filenames starting with a\n    dot are special cases that are not matched by '*' and '?'\n    patterns.\n\n    If recursive is true, the pattern '**' will match any files and\n    zero or more directories and subdirectories.\n    \"\"\"\n    it = _iglob(pathname, recursive)\n    if recursive and _isrecursive(pathname):\n        s = next(it)  # skip empty string\n        assert not s\n    return it\n\n\ndef _iglob(pathname, recursive):\n    dirname, basename = os.path.split(pathname)\n    if not has_magic(pathname):\n        if basename:\n            if os.path.lexists(pathname):\n                yield pathname\n        else:\n            # Patterns ending with a slash should match only directories\n            if os.path.isdir(dirname):\n                yield pathname\n        return\n    if not dirname:\n        if recursive and _isrecursive(basename):\n            for x in glob2(dirname, basename):\n                yield x\n        else:\n            for x in glob1(dirname, basename):\n                yield x\n        return\n    # `os.path.split()` returns the argument itself as a dirname if it is a\n    # drive or UNC path.  Prevent an infinite recursion if a drive or UNC path\n    # contains magic characters (i.e. r'\\\\?\\C:').\n    if dirname != pathname and has_magic(dirname):\n        dirs = _iglob(dirname, recursive)\n    else:\n        dirs = [dirname]\n    if has_magic(basename):\n        if recursive and _isrecursive(basename):\n            glob_in_dir = glob2\n        else:\n            glob_in_dir = glob1\n    else:\n        glob_in_dir = glob0\n    for dirname in dirs:\n        for name in glob_in_dir(dirname, basename):\n            yield os.path.join(dirname, name)\n\n\n# These 2 helper functions non-recursively glob inside a literal directory.\n# They return a list of basenames. `glob1` accepts a pattern while `glob0`\n# takes a literal basename (so it only has to check for its existence).\n\n\ndef glob1(dirname, pattern):\n    if not dirname:\n        if isinstance(pattern, bytes):\n            dirname = os.curdir.encode('ASCII')\n        else:\n            dirname = os.curdir\n    try:\n        names = os.listdir(dirname)\n    except OSError:\n        return []\n    return fnmatch.filter(names, pattern)\n\n\ndef glob0(dirname, basename):\n    if not basename:\n        # `os.path.split()` returns an empty basename for paths ending with a\n        # directory separator.  'q*x/' should match only directories.\n        if os.path.isdir(dirname):\n            return [basename]\n    else:\n        if os.path.lexists(os.path.join(dirname, basename)):\n            return [basename]\n    return []\n\n\n# This helper function recursively yields relative pathnames inside a literal\n# directory.\n\n\ndef glob2(dirname, pattern):\n    assert _isrecursive(pattern)\n    yield pattern[:0]\n    for x in _rlistdir(dirname):\n        yield x\n\n\n# Recursively yields relative pathnames inside a literal directory.\ndef _rlistdir(dirname):\n    if not dirname:\n        if isinstance(dirname, bytes):\n            dirname = os.curdir.encode('ASCII')\n        else:\n            dirname = os.curdir\n    try:\n        names = os.listdir(dirname)\n    except os.error:\n        return\n    for x in names:\n        yield x\n        path = os.path.join(dirname, x) if dirname else x\n        for y in _rlistdir(path):\n            yield os.path.join(x, y)\n\n\nmagic_check = re.compile('([*?[])')\nmagic_check_bytes = re.compile(b'([*?[])')\n\n\ndef has_magic(s):\n    if isinstance(s, bytes):\n        match = magic_check_bytes.search(s)\n    else:\n        match = magic_check.search(s)\n    return match is not None\n\n\ndef _isrecursive(pattern):\n    if isinstance(pattern, bytes):\n        return pattern == b'**'\n    else:\n        return pattern == '**'\n\n\ndef escape(pathname):\n    \"\"\"Escape all special characters.\n    \"\"\"\n    # Escaping is done by wrapping any of \"*?[\" between square brackets.\n    # Metacharacters do not work in the drive part and shouldn't be escaped.\n    drive, pathname = os.path.splitdrive(pathname)\n    if isinstance(pathname, bytes):\n        pathname = magic_check_bytes.sub(br'[\\1]', pathname)\n    else:\n        pathname = magic_check.sub(r'[\\1]', pathname)\n    return drive + pathname\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/installer.py",
    "content": "import glob\nimport os\nimport subprocess\nimport sys\nfrom distutils import log\nfrom distutils.errors import DistutilsError\n\nimport pkg_resources\nfrom setuptools.command.easy_install import easy_install\nfrom setuptools.extern import six\nfrom setuptools.wheel import Wheel\n\nfrom .py31compat import TemporaryDirectory\n\n\ndef _fixup_find_links(find_links):\n    \"\"\"Ensure find-links option end-up being a list of strings.\"\"\"\n    if isinstance(find_links, six.string_types):\n        return find_links.split()\n    assert isinstance(find_links, (tuple, list))\n    return find_links\n\n\ndef _legacy_fetch_build_egg(dist, req):\n    \"\"\"Fetch an egg needed for building.\n\n    Legacy path using EasyInstall.\n    \"\"\"\n    tmp_dist = dist.__class__({'script_args': ['easy_install']})\n    opts = tmp_dist.get_option_dict('easy_install')\n    opts.clear()\n    opts.update(\n        (k, v)\n        for k, v in dist.get_option_dict('easy_install').items()\n        if k in (\n            # don't use any other settings\n            'find_links', 'site_dirs', 'index_url',\n            'optimize', 'site_dirs', 'allow_hosts',\n        ))\n    if dist.dependency_links:\n        links = dist.dependency_links[:]\n        if 'find_links' in opts:\n            links = _fixup_find_links(opts['find_links'][1]) + links\n        opts['find_links'] = ('setup', links)\n    install_dir = dist.get_egg_cache_dir()\n    cmd = easy_install(\n        tmp_dist, args=[\"x\"], install_dir=install_dir,\n        exclude_scripts=True,\n        always_copy=False, build_directory=None, editable=False,\n        upgrade=False, multi_version=True, no_report=True, user=False\n    )\n    cmd.ensure_finalized()\n    return cmd.easy_install(req)\n\n\ndef fetch_build_egg(dist, req):\n    \"\"\"Fetch an egg needed for building.\n\n    Use pip/wheel to fetch/build a wheel.\"\"\"\n    # Check pip is available.\n    try:\n        pkg_resources.get_distribution('pip')\n    except pkg_resources.DistributionNotFound:\n        dist.announce(\n            'WARNING: The pip package is not available, falling back '\n            'to EasyInstall for handling setup_requires/test_requires; '\n            'this is deprecated and will be removed in a future version.'\n            , log.WARN\n        )\n        return _legacy_fetch_build_egg(dist, req)\n    # Warn if wheel is not.\n    try:\n        pkg_resources.get_distribution('wheel')\n    except pkg_resources.DistributionNotFound:\n        dist.announce('WARNING: The wheel package is not available.', log.WARN)\n    # Ignore environment markers; if supplied, it is required.\n    req = strip_marker(req)\n    # Take easy_install options into account, but do not override relevant\n    # pip environment variables (like PIP_INDEX_URL or PIP_QUIET); they'll\n    # take precedence.\n    opts = dist.get_option_dict('easy_install')\n    if 'allow_hosts' in opts:\n        raise DistutilsError('the `allow-hosts` option is not supported '\n                             'when using pip to install requirements.')\n    if 'PIP_QUIET' in os.environ or 'PIP_VERBOSE' in os.environ:\n        quiet = False\n    else:\n        quiet = True\n    if 'PIP_INDEX_URL' in os.environ:\n        index_url = None\n    elif 'index_url' in opts:\n        index_url = opts['index_url'][1]\n    else:\n        index_url = None\n    if 'find_links' in opts:\n        find_links = _fixup_find_links(opts['find_links'][1])[:]\n    else:\n        find_links = []\n    if dist.dependency_links:\n        find_links.extend(dist.dependency_links)\n    eggs_dir = os.path.realpath(dist.get_egg_cache_dir())\n    environment = pkg_resources.Environment()\n    for egg_dist in pkg_resources.find_distributions(eggs_dir):\n        if egg_dist in req and environment.can_add(egg_dist):\n            return egg_dist\n    with TemporaryDirectory() as tmpdir:\n        cmd = [\n            sys.executable, '-m', 'pip',\n            '--disable-pip-version-check',\n            'wheel', '--no-deps',\n            '-w', tmpdir,\n        ]\n        if quiet:\n            cmd.append('--quiet')\n        if index_url is not None:\n            cmd.extend(('--index-url', index_url))\n        if find_links is not None:\n            for link in find_links:\n                cmd.extend(('--find-links', link))\n        # If requirement is a PEP 508 direct URL, directly pass\n        # the URL to pip, as `req @ url` does not work on the\n        # command line.\n        if req.url:\n            cmd.append(req.url)\n        else:\n            cmd.append(str(req))\n        try:\n            subprocess.check_call(cmd)\n        except subprocess.CalledProcessError as e:\n            raise DistutilsError(str(e))\n        wheel = Wheel(glob.glob(os.path.join(tmpdir, '*.whl'))[0])\n        dist_location = os.path.join(eggs_dir, wheel.egg_name())\n        wheel.install_as_egg(dist_location)\n        dist_metadata = pkg_resources.PathMetadata(\n            dist_location, os.path.join(dist_location, 'EGG-INFO'))\n        dist = pkg_resources.Distribution.from_filename(\n            dist_location, metadata=dist_metadata)\n        return dist\n\n\ndef strip_marker(req):\n    \"\"\"\n    Return a new requirement without the environment marker to avoid\n    calling pip with something like `babel; extra == \"i18n\"`, which\n    would always be ignored.\n    \"\"\"\n    # create a copy to avoid mutating the input\n    req = pkg_resources.Requirement.parse(str(req))\n    req.marker = None\n    return req\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/launch.py",
    "content": "\"\"\"\nLaunch the Python script on the command line after\nsetuptools is bootstrapped via import.\n\"\"\"\n\n# Note that setuptools gets imported implicitly by the\n# invocation of this script using python -m setuptools.launch\n\nimport tokenize\nimport sys\n\n\ndef run():\n    \"\"\"\n    Run the script in sys.argv[1] as if it had\n    been invoked naturally.\n    \"\"\"\n    __builtins__\n    script_name = sys.argv[1]\n    namespace = dict(\n        __file__=script_name,\n        __name__='__main__',\n        __doc__=None,\n    )\n    sys.argv[:] = sys.argv[1:]\n\n    open_ = getattr(tokenize, 'open', open)\n    script = open_(script_name).read()\n    norm_script = script.replace('\\\\r\\\\n', '\\\\n')\n    code = compile(norm_script, script_name, 'exec')\n    exec(code, namespace)\n\n\nif __name__ == '__main__':\n    run()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/lib2to3_ex.py",
    "content": "\"\"\"\nCustomized Mixin2to3 support:\n\n - adds support for converting doctests\n\n\nThis module raises an ImportError on Python 2.\n\"\"\"\n\nfrom distutils.util import Mixin2to3 as _Mixin2to3\nfrom distutils import log\nfrom lib2to3.refactor import RefactoringTool, get_fixers_from_package\n\nimport setuptools\n\n\nclass DistutilsRefactoringTool(RefactoringTool):\n    def log_error(self, msg, *args, **kw):\n        log.error(msg, *args)\n\n    def log_message(self, msg, *args):\n        log.info(msg, *args)\n\n    def log_debug(self, msg, *args):\n        log.debug(msg, *args)\n\n\nclass Mixin2to3(_Mixin2to3):\n    def run_2to3(self, files, doctests=False):\n        # See of the distribution option has been set, otherwise check the\n        # setuptools default.\n        if self.distribution.use_2to3 is not True:\n            return\n        if not files:\n            return\n        log.info(\"Fixing \" + \" \".join(files))\n        self.__build_fixer_names()\n        self.__exclude_fixers()\n        if doctests:\n            if setuptools.run_2to3_on_doctests:\n                r = DistutilsRefactoringTool(self.fixer_names)\n                r.refactor(files, write=True, doctests_only=True)\n        else:\n            _Mixin2to3.run_2to3(self, files)\n\n    def __build_fixer_names(self):\n        if self.fixer_names:\n            return\n        self.fixer_names = []\n        for p in setuptools.lib2to3_fixer_packages:\n            self.fixer_names.extend(get_fixers_from_package(p))\n        if self.distribution.use_2to3_fixers is not None:\n            for p in self.distribution.use_2to3_fixers:\n                self.fixer_names.extend(get_fixers_from_package(p))\n\n    def __exclude_fixers(self):\n        excluded_fixers = getattr(self, 'exclude_fixers', [])\n        if self.distribution.use_2to3_exclude_fixers is not None:\n            excluded_fixers.extend(self.distribution.use_2to3_exclude_fixers)\n        for fixer_name in excluded_fixers:\n            if fixer_name in self.fixer_names:\n                self.fixer_names.remove(fixer_name)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/monkey.py",
    "content": "\"\"\"\nMonkey patching of distutils.\n\"\"\"\n\nimport sys\nimport distutils.filelist\nimport platform\nimport types\nimport functools\nfrom importlib import import_module\nimport inspect\n\nfrom setuptools.extern import six\n\nimport setuptools\n\n__all__ = []\n\"\"\"\nEverything is private. Contact the project team\nif you think you need this functionality.\n\"\"\"\n\n\ndef _get_mro(cls):\n    \"\"\"\n    Returns the bases classes for cls sorted by the MRO.\n\n    Works around an issue on Jython where inspect.getmro will not return all\n    base classes if multiple classes share the same name. Instead, this\n    function will return a tuple containing the class itself, and the contents\n    of cls.__bases__. See https://github.com/pypa/setuptools/issues/1024.\n    \"\"\"\n    if platform.python_implementation() == \"Jython\":\n        return (cls,) + cls.__bases__\n    return inspect.getmro(cls)\n\n\ndef get_unpatched(item):\n    lookup = (\n        get_unpatched_class if isinstance(item, six.class_types) else\n        get_unpatched_function if isinstance(item, types.FunctionType) else\n        lambda item: None\n    )\n    return lookup(item)\n\n\ndef get_unpatched_class(cls):\n    \"\"\"Protect against re-patching the distutils if reloaded\n\n    Also ensures that no other distutils extension monkeypatched the distutils\n    first.\n    \"\"\"\n    external_bases = (\n        cls\n        for cls in _get_mro(cls)\n        if not cls.__module__.startswith('setuptools')\n    )\n    base = next(external_bases)\n    if not base.__module__.startswith('distutils'):\n        msg = \"distutils has already been patched by %r\" % cls\n        raise AssertionError(msg)\n    return base\n\n\ndef patch_all():\n    # we can't patch distutils.cmd, alas\n    distutils.core.Command = setuptools.Command\n\n    has_issue_12885 = sys.version_info <= (3, 5, 3)\n\n    if has_issue_12885:\n        # fix findall bug in distutils (http://bugs.python.org/issue12885)\n        distutils.filelist.findall = setuptools.findall\n\n    needs_warehouse = (\n        sys.version_info < (2, 7, 13)\n        or\n        (3, 4) < sys.version_info < (3, 4, 6)\n        or\n        (3, 5) < sys.version_info <= (3, 5, 3)\n    )\n\n    if needs_warehouse:\n        warehouse = 'https://upload.pypi.org/legacy/'\n        distutils.config.PyPIRCCommand.DEFAULT_REPOSITORY = warehouse\n\n    _patch_distribution_metadata()\n\n    # Install Distribution throughout the distutils\n    for module in distutils.dist, distutils.core, distutils.cmd:\n        module.Distribution = setuptools.dist.Distribution\n\n    # Install the patched Extension\n    distutils.core.Extension = setuptools.extension.Extension\n    distutils.extension.Extension = setuptools.extension.Extension\n    if 'distutils.command.build_ext' in sys.modules:\n        sys.modules['distutils.command.build_ext'].Extension = (\n            setuptools.extension.Extension\n        )\n\n    patch_for_msvc_specialized_compiler()\n\n\ndef _patch_distribution_metadata():\n    \"\"\"Patch write_pkg_file and read_pkg_file for higher metadata standards\"\"\"\n    for attr in ('write_pkg_file', 'read_pkg_file', 'get_metadata_version'):\n        new_val = getattr(setuptools.dist, attr)\n        setattr(distutils.dist.DistributionMetadata, attr, new_val)\n\n\ndef patch_func(replacement, target_mod, func_name):\n    \"\"\"\n    Patch func_name in target_mod with replacement\n\n    Important - original must be resolved by name to avoid\n    patching an already patched function.\n    \"\"\"\n    original = getattr(target_mod, func_name)\n\n    # set the 'unpatched' attribute on the replacement to\n    # point to the original.\n    vars(replacement).setdefault('unpatched', original)\n\n    # replace the function in the original module\n    setattr(target_mod, func_name, replacement)\n\n\ndef get_unpatched_function(candidate):\n    return getattr(candidate, 'unpatched')\n\n\ndef patch_for_msvc_specialized_compiler():\n    \"\"\"\n    Patch functions in distutils to use standalone Microsoft Visual C++\n    compilers.\n    \"\"\"\n    # import late to avoid circular imports on Python < 3.5\n    msvc = import_module('setuptools.msvc')\n\n    if platform.system() != 'Windows':\n        # Compilers only availables on Microsoft Windows\n        return\n\n    def patch_params(mod_name, func_name):\n        \"\"\"\n        Prepare the parameters for patch_func to patch indicated function.\n        \"\"\"\n        repl_prefix = 'msvc9_' if 'msvc9' in mod_name else 'msvc14_'\n        repl_name = repl_prefix + func_name.lstrip('_')\n        repl = getattr(msvc, repl_name)\n        mod = import_module(mod_name)\n        if not hasattr(mod, func_name):\n            raise ImportError(func_name)\n        return repl, mod, func_name\n\n    # Python 2.7 to 3.4\n    msvc9 = functools.partial(patch_params, 'distutils.msvc9compiler')\n\n    # Python 3.5+\n    msvc14 = functools.partial(patch_params, 'distutils._msvccompiler')\n\n    try:\n        # Patch distutils.msvc9compiler\n        patch_func(*msvc9('find_vcvarsall'))\n        patch_func(*msvc9('query_vcvarsall'))\n    except ImportError:\n        pass\n\n    try:\n        # Patch distutils._msvccompiler._get_vc_env\n        patch_func(*msvc14('_get_vc_env'))\n    except ImportError:\n        pass\n\n    try:\n        # Patch distutils._msvccompiler.gen_lib_options for Numpy\n        patch_func(*msvc14('gen_lib_options'))\n    except ImportError:\n        pass\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/msvc.py",
    "content": "\"\"\"\nImproved support for Microsoft Visual C++ compilers.\n\nKnown supported compilers:\n--------------------------\nMicrosoft Visual C++ 9.0:\n    Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64)\n    Microsoft Windows SDK 6.1 (x86, x64, ia64)\n    Microsoft Windows SDK 7.0 (x86, x64, ia64)\n\nMicrosoft Visual C++ 10.0:\n    Microsoft Windows SDK 7.1 (x86, x64, ia64)\n\nMicrosoft Visual C++ 14.X:\n    Microsoft Visual C++ Build Tools 2015 (x86, x64, arm)\n    Microsoft Visual Studio Build Tools 2017 (x86, x64, arm, arm64)\n    Microsoft Visual Studio Build Tools 2019 (x86, x64, arm, arm64)\n\nThis may also support compilers shipped with compatible Visual Studio versions.\n\"\"\"\n\nimport json\nfrom io import open\nfrom os import listdir, pathsep\nfrom os.path import join, isfile, isdir, dirname\nimport sys\nimport platform\nimport itertools\nimport distutils.errors\nfrom setuptools.extern.packaging.version import LegacyVersion\n\nfrom setuptools.extern.six.moves import filterfalse\n\nfrom .monkey import get_unpatched\n\nif platform.system() == 'Windows':\n    from setuptools.extern.six.moves import winreg\n    from os import environ\nelse:\n    # Mock winreg and environ so the module can be imported on this platform.\n\n    class winreg:\n        HKEY_USERS = None\n        HKEY_CURRENT_USER = None\n        HKEY_LOCAL_MACHINE = None\n        HKEY_CLASSES_ROOT = None\n\n    environ = dict()\n\n_msvc9_suppress_errors = (\n    # msvc9compiler isn't available on some platforms\n    ImportError,\n\n    # msvc9compiler raises DistutilsPlatformError in some\n    # environments. See #1118.\n    distutils.errors.DistutilsPlatformError,\n)\n\ntry:\n    from distutils.msvc9compiler import Reg\nexcept _msvc9_suppress_errors:\n    pass\n\n\ndef msvc9_find_vcvarsall(version):\n    \"\"\"\n    Patched \"distutils.msvc9compiler.find_vcvarsall\" to use the standalone\n    compiler build for Python\n    (VCForPython / Microsoft Visual C++ Compiler for Python 2.7).\n\n    Fall back to original behavior when the standalone compiler is not\n    available.\n\n    Redirect the path of \"vcvarsall.bat\".\n\n    Parameters\n    ----------\n    version: float\n        Required Microsoft Visual C++ version.\n\n    Return\n    ------\n    str\n        vcvarsall.bat path\n    \"\"\"\n    vc_base = r'Software\\%sMicrosoft\\DevDiv\\VCForPython\\%0.1f'\n    key = vc_base % ('', version)\n    try:\n        # Per-user installs register the compiler path here\n        productdir = Reg.get_value(key, \"installdir\")\n    except KeyError:\n        try:\n            # All-user installs on a 64-bit system register here\n            key = vc_base % ('Wow6432Node\\\\', version)\n            productdir = Reg.get_value(key, \"installdir\")\n        except KeyError:\n            productdir = None\n\n    if productdir:\n        vcvarsall = join(productdir, \"vcvarsall.bat\")\n        if isfile(vcvarsall):\n            return vcvarsall\n\n    return get_unpatched(msvc9_find_vcvarsall)(version)\n\n\ndef msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs):\n    \"\"\"\n    Patched \"distutils.msvc9compiler.query_vcvarsall\" for support extra\n    Microsoft Visual C++ 9.0 and 10.0 compilers.\n\n    Set environment without use of \"vcvarsall.bat\".\n\n    Parameters\n    ----------\n    ver: float\n        Required Microsoft Visual C++ version.\n    arch: str\n        Target architecture.\n\n    Return\n    ------\n    dict\n        environment\n    \"\"\"\n    # Try to get environment from vcvarsall.bat (Classical way)\n    try:\n        orig = get_unpatched(msvc9_query_vcvarsall)\n        return orig(ver, arch, *args, **kwargs)\n    except distutils.errors.DistutilsPlatformError:\n        # Pass error if Vcvarsall.bat is missing\n        pass\n    except ValueError:\n        # Pass error if environment not set after executing vcvarsall.bat\n        pass\n\n    # If error, try to set environment directly\n    try:\n        return EnvironmentInfo(arch, ver).return_env()\n    except distutils.errors.DistutilsPlatformError as exc:\n        _augment_exception(exc, ver, arch)\n        raise\n\n\ndef msvc14_get_vc_env(plat_spec):\n    \"\"\"\n    Patched \"distutils._msvccompiler._get_vc_env\" for support extra\n    Microsoft Visual C++ 14.X compilers.\n\n    Set environment without use of \"vcvarsall.bat\".\n\n    Parameters\n    ----------\n    plat_spec: str\n        Target architecture.\n\n    Return\n    ------\n    dict\n        environment\n    \"\"\"\n    # Try to get environment from vcvarsall.bat (Classical way)\n    try:\n        return get_unpatched(msvc14_get_vc_env)(plat_spec)\n    except distutils.errors.DistutilsPlatformError:\n        # Pass error Vcvarsall.bat is missing\n        pass\n\n    # If error, try to set environment directly\n    try:\n        return EnvironmentInfo(plat_spec, vc_min_ver=14.0).return_env()\n    except distutils.errors.DistutilsPlatformError as exc:\n        _augment_exception(exc, 14.0)\n        raise\n\n\ndef msvc14_gen_lib_options(*args, **kwargs):\n    \"\"\"\n    Patched \"distutils._msvccompiler.gen_lib_options\" for fix\n    compatibility between \"numpy.distutils\" and \"distutils._msvccompiler\"\n    (for Numpy < 1.11.2)\n    \"\"\"\n    if \"numpy.distutils\" in sys.modules:\n        import numpy as np\n        if LegacyVersion(np.__version__) < LegacyVersion('1.11.2'):\n            return np.distutils.ccompiler.gen_lib_options(*args, **kwargs)\n    return get_unpatched(msvc14_gen_lib_options)(*args, **kwargs)\n\n\ndef _augment_exception(exc, version, arch=''):\n    \"\"\"\n    Add details to the exception message to help guide the user\n    as to what action will resolve it.\n    \"\"\"\n    # Error if MSVC++ directory not found or environment not set\n    message = exc.args[0]\n\n    if \"vcvarsall\" in message.lower() or \"visual c\" in message.lower():\n        # Special error message if MSVC++ not installed\n        tmpl = 'Microsoft Visual C++ {version:0.1f} is required.'\n        message = tmpl.format(**locals())\n        msdownload = 'www.microsoft.com/download/details.aspx?id=%d'\n        if version == 9.0:\n            if arch.lower().find('ia64') > -1:\n                # For VC++ 9.0, if IA64 support is needed, redirect user\n                # to Windows SDK 7.0.\n                # Note: No download link available from Microsoft.\n                message += ' Get it with \"Microsoft Windows SDK 7.0\"'\n            else:\n                # For VC++ 9.0 redirect user to Vc++ for Python 2.7 :\n                # This redirection link is maintained by Microsoft.\n                # Contact vspython@microsoft.com if it needs updating.\n                message += ' Get it from http://aka.ms/vcpython27'\n        elif version == 10.0:\n            # For VC++ 10.0 Redirect user to Windows SDK 7.1\n            message += ' Get it with \"Microsoft Windows SDK 7.1\": '\n            message += msdownload % 8279\n        elif version >= 14.0:\n            # For VC++ 14.X Redirect user to latest Visual C++ Build Tools\n            message += (' Get it with \"Build Tools for Visual Studio\": '\n                        r'https://visualstudio.microsoft.com/downloads/')\n\n    exc.args = (message, )\n\n\nclass PlatformInfo:\n    \"\"\"\n    Current and Target Architectures information.\n\n    Parameters\n    ----------\n    arch: str\n        Target architecture.\n    \"\"\"\n    current_cpu = environ.get('processor_architecture', '').lower()\n\n    def __init__(self, arch):\n        self.arch = arch.lower().replace('x64', 'amd64')\n\n    @property\n    def target_cpu(self):\n        \"\"\"\n        Return Target CPU architecture.\n\n        Return\n        ------\n        str\n            Target CPU\n        \"\"\"\n        return self.arch[self.arch.find('_') + 1:]\n\n    def target_is_x86(self):\n        \"\"\"\n        Return True if target CPU is x86 32 bits..\n\n        Return\n        ------\n        bool\n            CPU is x86 32 bits\n        \"\"\"\n        return self.target_cpu == 'x86'\n\n    def current_is_x86(self):\n        \"\"\"\n        Return True if current CPU is x86 32 bits..\n\n        Return\n        ------\n        bool\n            CPU is x86 32 bits\n        \"\"\"\n        return self.current_cpu == 'x86'\n\n    def current_dir(self, hidex86=False, x64=False):\n        \"\"\"\n        Current platform specific subfolder.\n\n        Parameters\n        ----------\n        hidex86: bool\n            return '' and not '\\x86' if architecture is x86.\n        x64: bool\n            return '\\x64' and not '\\amd64' if architecture is amd64.\n\n        Return\n        ------\n        str\n            subfolder: '\\target', or '' (see hidex86 parameter)\n        \"\"\"\n        return (\n            '' if (self.current_cpu == 'x86' and hidex86) else\n            r'\\x64' if (self.current_cpu == 'amd64' and x64) else\n            r'\\%s' % self.current_cpu\n        )\n\n    def target_dir(self, hidex86=False, x64=False):\n        r\"\"\"\n        Target platform specific subfolder.\n\n        Parameters\n        ----------\n        hidex86: bool\n            return '' and not '\\x86' if architecture is x86.\n        x64: bool\n            return '\\x64' and not '\\amd64' if architecture is amd64.\n\n        Return\n        ------\n        str\n            subfolder: '\\current', or '' (see hidex86 parameter)\n        \"\"\"\n        return (\n            '' if (self.target_cpu == 'x86' and hidex86) else\n            r'\\x64' if (self.target_cpu == 'amd64' and x64) else\n            r'\\%s' % self.target_cpu\n        )\n\n    def cross_dir(self, forcex86=False):\n        r\"\"\"\n        Cross platform specific subfolder.\n\n        Parameters\n        ----------\n        forcex86: bool\n            Use 'x86' as current architecture even if current architecture is\n            not x86.\n\n        Return\n        ------\n        str\n            subfolder: '' if target architecture is current architecture,\n            '\\current_target' if not.\n        \"\"\"\n        current = 'x86' if forcex86 else self.current_cpu\n        return (\n            '' if self.target_cpu == current else\n            self.target_dir().replace('\\\\', '\\\\%s_' % current)\n        )\n\n\nclass RegistryInfo:\n    \"\"\"\n    Microsoft Visual Studio related registry information.\n\n    Parameters\n    ----------\n    platform_info: PlatformInfo\n        \"PlatformInfo\" instance.\n    \"\"\"\n    HKEYS = (winreg.HKEY_USERS,\n             winreg.HKEY_CURRENT_USER,\n             winreg.HKEY_LOCAL_MACHINE,\n             winreg.HKEY_CLASSES_ROOT)\n\n    def __init__(self, platform_info):\n        self.pi = platform_info\n\n    @property\n    def visualstudio(self):\n        \"\"\"\n        Microsoft Visual Studio root registry key.\n\n        Return\n        ------\n        str\n            Registry key\n        \"\"\"\n        return 'VisualStudio'\n\n    @property\n    def sxs(self):\n        \"\"\"\n        Microsoft Visual Studio SxS registry key.\n\n        Return\n        ------\n        str\n            Registry key\n        \"\"\"\n        return join(self.visualstudio, 'SxS')\n\n    @property\n    def vc(self):\n        \"\"\"\n        Microsoft Visual C++ VC7 registry key.\n\n        Return\n        ------\n        str\n            Registry key\n        \"\"\"\n        return join(self.sxs, 'VC7')\n\n    @property\n    def vs(self):\n        \"\"\"\n        Microsoft Visual Studio VS7 registry key.\n\n        Return\n        ------\n        str\n            Registry key\n        \"\"\"\n        return join(self.sxs, 'VS7')\n\n    @property\n    def vc_for_python(self):\n        \"\"\"\n        Microsoft Visual C++ for Python registry key.\n\n        Return\n        ------\n        str\n            Registry key\n        \"\"\"\n        return r'DevDiv\\VCForPython'\n\n    @property\n    def microsoft_sdk(self):\n        \"\"\"\n        Microsoft SDK registry key.\n\n        Return\n        ------\n        str\n            Registry key\n        \"\"\"\n        return 'Microsoft SDKs'\n\n    @property\n    def windows_sdk(self):\n        \"\"\"\n        Microsoft Windows/Platform SDK registry key.\n\n        Return\n        ------\n        str\n            Registry key\n        \"\"\"\n        return join(self.microsoft_sdk, 'Windows')\n\n    @property\n    def netfx_sdk(self):\n        \"\"\"\n        Microsoft .NET Framework SDK registry key.\n\n        Return\n        ------\n        str\n            Registry key\n        \"\"\"\n        return join(self.microsoft_sdk, 'NETFXSDK')\n\n    @property\n    def windows_kits_roots(self):\n        \"\"\"\n        Microsoft Windows Kits Roots registry key.\n\n        Return\n        ------\n        str\n            Registry key\n        \"\"\"\n        return r'Windows Kits\\Installed Roots'\n\n    def microsoft(self, key, x86=False):\n        \"\"\"\n        Return key in Microsoft software registry.\n\n        Parameters\n        ----------\n        key: str\n            Registry key path where look.\n        x86: str\n            Force x86 software registry.\n\n        Return\n        ------\n        str\n            Registry key\n        \"\"\"\n        node64 = '' if self.pi.current_is_x86() or x86 else 'Wow6432Node'\n        return join('Software', node64, 'Microsoft', key)\n\n    def lookup(self, key, name):\n        \"\"\"\n        Look for values in registry in Microsoft software registry.\n\n        Parameters\n        ----------\n        key: str\n            Registry key path where look.\n        name: str\n            Value name to find.\n\n        Return\n        ------\n        str\n            value\n        \"\"\"\n        key_read = winreg.KEY_READ\n        openkey = winreg.OpenKey\n        ms = self.microsoft\n        for hkey in self.HKEYS:\n            try:\n                bkey = openkey(hkey, ms(key), 0, key_read)\n            except (OSError, IOError):\n                if not self.pi.current_is_x86():\n                    try:\n                        bkey = openkey(hkey, ms(key, True), 0, key_read)\n                    except (OSError, IOError):\n                        continue\n                else:\n                    continue\n            try:\n                return winreg.QueryValueEx(bkey, name)[0]\n            except (OSError, IOError):\n                pass\n\n\nclass SystemInfo:\n    \"\"\"\n    Microsoft Windows and Visual Studio related system information.\n\n    Parameters\n    ----------\n    registry_info: RegistryInfo\n        \"RegistryInfo\" instance.\n    vc_ver: float\n        Required Microsoft Visual C++ version.\n    \"\"\"\n\n    # Variables and properties in this class use originals CamelCase variables\n    # names from Microsoft source files for more easy comparison.\n    WinDir = environ.get('WinDir', '')\n    ProgramFiles = environ.get('ProgramFiles', '')\n    ProgramFilesx86 = environ.get('ProgramFiles(x86)', ProgramFiles)\n\n    def __init__(self, registry_info, vc_ver=None):\n        self.ri = registry_info\n        self.pi = self.ri.pi\n\n        self.known_vs_paths = self.find_programdata_vs_vers()\n\n        # Except for VS15+, VC version is aligned with VS version\n        self.vs_ver = self.vc_ver = (\n                vc_ver or self._find_latest_available_vs_ver())\n\n    def _find_latest_available_vs_ver(self):\n        \"\"\"\n        Find the latest VC version\n\n        Return\n        ------\n        float\n            version\n        \"\"\"\n        reg_vc_vers = self.find_reg_vs_vers()\n\n        if not (reg_vc_vers or self.known_vs_paths):\n            raise distutils.errors.DistutilsPlatformError(\n                'No Microsoft Visual C++ version found')\n\n        vc_vers = set(reg_vc_vers)\n        vc_vers.update(self.known_vs_paths)\n        return sorted(vc_vers)[-1]\n\n    def find_reg_vs_vers(self):\n        \"\"\"\n        Find Microsoft Visual Studio versions available in registry.\n\n        Return\n        ------\n        list of float\n            Versions\n        \"\"\"\n        ms = self.ri.microsoft\n        vckeys = (self.ri.vc, self.ri.vc_for_python, self.ri.vs)\n        vs_vers = []\n        for hkey in self.ri.HKEYS:\n            for key in vckeys:\n                try:\n                    bkey = winreg.OpenKey(hkey, ms(key), 0, winreg.KEY_READ)\n                except (OSError, IOError):\n                    continue\n                subkeys, values, _ = winreg.QueryInfoKey(bkey)\n                for i in range(values):\n                    try:\n                        ver = float(winreg.EnumValue(bkey, i)[0])\n                        if ver not in vs_vers:\n                            vs_vers.append(ver)\n                    except ValueError:\n                        pass\n                for i in range(subkeys):\n                    try:\n                        ver = float(winreg.EnumKey(bkey, i))\n                        if ver not in vs_vers:\n                            vs_vers.append(ver)\n                    except ValueError:\n                        pass\n        return sorted(vs_vers)\n\n    def find_programdata_vs_vers(self):\n        r\"\"\"\n        Find Visual studio 2017+ versions from information in\n        \"C:\\ProgramData\\Microsoft\\VisualStudio\\Packages\\_Instances\".\n\n        Return\n        ------\n        dict\n            float version as key, path as value.\n        \"\"\"\n        vs_versions = {}\n        instances_dir = \\\n            r'C:\\ProgramData\\Microsoft\\VisualStudio\\Packages\\_Instances'\n\n        try:\n            hashed_names = listdir(instances_dir)\n\n        except (OSError, IOError):\n            # Directory not exists with all Visual Studio versions\n            return vs_versions\n\n        for name in hashed_names:\n            try:\n                # Get VS installation path from \"state.json\" file\n                state_path = join(instances_dir, name, 'state.json')\n                with open(state_path, 'rt', encoding='utf-8') as state_file:\n                    state = json.load(state_file)\n                vs_path = state['installationPath']\n\n                # Raises OSError if this VS installation does not contain VC\n                listdir(join(vs_path, r'VC\\Tools\\MSVC'))\n\n                # Store version and path\n                vs_versions[self._as_float_version(\n                    state['installationVersion'])] = vs_path\n\n            except (OSError, IOError, KeyError):\n                # Skip if \"state.json\" file is missing or bad format\n                continue\n\n        return vs_versions\n\n    @staticmethod\n    def _as_float_version(version):\n        \"\"\"\n        Return a string version as a simplified float version (major.minor)\n\n        Parameters\n        ----------\n        version: str\n            Version.\n\n        Return\n        ------\n        float\n            version\n        \"\"\"\n        return float('.'.join(version.split('.')[:2]))\n\n    @property\n    def VSInstallDir(self):\n        \"\"\"\n        Microsoft Visual Studio directory.\n\n        Return\n        ------\n        str\n            path\n        \"\"\"\n        # Default path\n        default = join(self.ProgramFilesx86,\n                       'Microsoft Visual Studio %0.1f' % self.vs_ver)\n\n        # Try to get path from registry, if fail use default path\n        return self.ri.lookup(self.ri.vs, '%0.1f' % self.vs_ver) or default\n\n    @property\n    def VCInstallDir(self):\n        \"\"\"\n        Microsoft Visual C++ directory.\n\n        Return\n        ------\n        str\n            path\n        \"\"\"\n        path = self._guess_vc() or self._guess_vc_legacy()\n\n        if not isdir(path):\n            msg = 'Microsoft Visual C++ directory not found'\n            raise distutils.errors.DistutilsPlatformError(msg)\n\n        return path\n\n    def _guess_vc(self):\n        \"\"\"\n        Locate Visual C++ for VS2017+.\n\n        Return\n        ------\n        str\n            path\n        \"\"\"\n        if self.vs_ver <= 14.0:\n            return ''\n\n        try:\n            # First search in known VS paths\n            vs_dir = self.known_vs_paths[self.vs_ver]\n        except KeyError:\n            # Else, search with path from registry\n            vs_dir = self.VSInstallDir\n\n        guess_vc = join(vs_dir, r'VC\\Tools\\MSVC')\n\n        # Subdir with VC exact version as name\n        try:\n            # Update the VC version with real one instead of VS version\n            vc_ver = listdir(guess_vc)[-1]\n            self.vc_ver = self._as_float_version(vc_ver)\n            return join(guess_vc, vc_ver)\n        except (OSError, IOError, IndexError):\n            return ''\n\n    def _guess_vc_legacy(self):\n        \"\"\"\n        Locate Visual C++ for versions prior to 2017.\n\n        Return\n        ------\n        str\n            path\n        \"\"\"\n        default = join(self.ProgramFilesx86,\n                       r'Microsoft Visual Studio %0.1f\\VC' % self.vs_ver)\n\n        # Try to get \"VC++ for Python\" path from registry as default path\n        reg_path = join(self.ri.vc_for_python, '%0.1f' % self.vs_ver)\n        python_vc = self.ri.lookup(reg_path, 'installdir')\n        default_vc = join(python_vc, 'VC') if python_vc else default\n\n        # Try to get path from registry, if fail use default path\n        return self.ri.lookup(self.ri.vc, '%0.1f' % self.vs_ver) or default_vc\n\n    @property\n    def WindowsSdkVersion(self):\n        \"\"\"\n        Microsoft Windows SDK versions for specified MSVC++ version.\n\n        Return\n        ------\n        tuple of str\n            versions\n        \"\"\"\n        if self.vs_ver <= 9.0:\n            return '7.0', '6.1', '6.0a'\n        elif self.vs_ver == 10.0:\n            return '7.1', '7.0a'\n        elif self.vs_ver == 11.0:\n            return '8.0', '8.0a'\n        elif self.vs_ver == 12.0:\n            return '8.1', '8.1a'\n        elif self.vs_ver >= 14.0:\n            return '10.0', '8.1'\n\n    @property\n    def WindowsSdkLastVersion(self):\n        \"\"\"\n        Microsoft Windows SDK last version.\n\n        Return\n        ------\n        str\n            version\n        \"\"\"\n        return self._use_last_dir_name(join(self.WindowsSdkDir, 'lib'))\n\n    @property\n    def WindowsSdkDir(self):\n        \"\"\"\n        Microsoft Windows SDK directory.\n\n        Return\n        ------\n        str\n            path\n        \"\"\"\n        sdkdir = ''\n        for ver in self.WindowsSdkVersion:\n            # Try to get it from registry\n            loc = join(self.ri.windows_sdk, 'v%s' % ver)\n            sdkdir = self.ri.lookup(loc, 'installationfolder')\n            if sdkdir:\n                break\n        if not sdkdir or not isdir(sdkdir):\n            # Try to get \"VC++ for Python\" version from registry\n            path = join(self.ri.vc_for_python, '%0.1f' % self.vc_ver)\n            install_base = self.ri.lookup(path, 'installdir')\n            if install_base:\n                sdkdir = join(install_base, 'WinSDK')\n        if not sdkdir or not isdir(sdkdir):\n            # If fail, use default new path\n            for ver in self.WindowsSdkVersion:\n                intver = ver[:ver.rfind('.')]\n                path = r'Microsoft SDKs\\Windows Kits\\%s' % intver\n                d = join(self.ProgramFiles, path)\n                if isdir(d):\n                    sdkdir = d\n        if not sdkdir or not isdir(sdkdir):\n            # If fail, use default old path\n            for ver in self.WindowsSdkVersion:\n                path = r'Microsoft SDKs\\Windows\\v%s' % ver\n                d = join(self.ProgramFiles, path)\n                if isdir(d):\n                    sdkdir = d\n        if not sdkdir:\n            # If fail, use Platform SDK\n            sdkdir = join(self.VCInstallDir, 'PlatformSDK')\n        return sdkdir\n\n    @property\n    def WindowsSDKExecutablePath(self):\n        \"\"\"\n        Microsoft Windows SDK executable directory.\n\n        Return\n        ------\n        str\n            path\n        \"\"\"\n        # Find WinSDK NetFx Tools registry dir name\n        if self.vs_ver <= 11.0:\n            netfxver = 35\n            arch = ''\n        else:\n            netfxver = 40\n            hidex86 = True if self.vs_ver <= 12.0 else False\n            arch = self.pi.current_dir(x64=True, hidex86=hidex86)\n        fx = 'WinSDK-NetFx%dTools%s' % (netfxver, arch.replace('\\\\', '-'))\n\n        # list all possibles registry paths\n        regpaths = []\n        if self.vs_ver >= 14.0:\n            for ver in self.NetFxSdkVersion:\n                regpaths += [join(self.ri.netfx_sdk, ver, fx)]\n\n        for ver in self.WindowsSdkVersion:\n            regpaths += [join(self.ri.windows_sdk, 'v%sA' % ver, fx)]\n\n        # Return installation folder from the more recent path\n        for path in regpaths:\n            execpath = self.ri.lookup(path, 'installationfolder')\n            if execpath:\n                return execpath\n\n    @property\n    def FSharpInstallDir(self):\n        \"\"\"\n        Microsoft Visual F# directory.\n\n        Return\n        ------\n        str\n            path\n        \"\"\"\n        path = join(self.ri.visualstudio, r'%0.1f\\Setup\\F#' % self.vs_ver)\n        return self.ri.lookup(path, 'productdir') or ''\n\n    @property\n    def UniversalCRTSdkDir(self):\n        \"\"\"\n        Microsoft Universal CRT SDK directory.\n\n        Return\n        ------\n        str\n            path\n        \"\"\"\n        # Set Kit Roots versions for specified MSVC++ version\n        vers = ('10', '81') if self.vs_ver >= 14.0 else ()\n\n        # Find path of the more recent Kit\n        for ver in vers:\n            sdkdir = self.ri.lookup(self.ri.windows_kits_roots,\n                                    'kitsroot%s' % ver)\n            if sdkdir:\n                return sdkdir or ''\n\n    @property\n    def UniversalCRTSdkLastVersion(self):\n        \"\"\"\n        Microsoft Universal C Runtime SDK last version.\n\n        Return\n        ------\n        str\n            version\n        \"\"\"\n        return self._use_last_dir_name(join(self.UniversalCRTSdkDir, 'lib'))\n\n    @property\n    def NetFxSdkVersion(self):\n        \"\"\"\n        Microsoft .NET Framework SDK versions.\n\n        Return\n        ------\n        tuple of str\n            versions\n        \"\"\"\n        # Set FxSdk versions for specified VS version\n        return (('4.7.2', '4.7.1', '4.7',\n                 '4.6.2', '4.6.1', '4.6',\n                 '4.5.2', '4.5.1', '4.5')\n                if self.vs_ver >= 14.0 else ())\n\n    @property\n    def NetFxSdkDir(self):\n        \"\"\"\n        Microsoft .NET Framework SDK directory.\n\n        Return\n        ------\n        str\n            path\n        \"\"\"\n        sdkdir = ''\n        for ver in self.NetFxSdkVersion:\n            loc = join(self.ri.netfx_sdk, ver)\n            sdkdir = self.ri.lookup(loc, 'kitsinstallationfolder')\n            if sdkdir:\n                break\n        return sdkdir\n\n    @property\n    def FrameworkDir32(self):\n        \"\"\"\n        Microsoft .NET Framework 32bit directory.\n\n        Return\n        ------\n        str\n            path\n        \"\"\"\n        # Default path\n        guess_fw = join(self.WinDir, r'Microsoft.NET\\Framework')\n\n        # Try to get path from registry, if fail use default path\n        return self.ri.lookup(self.ri.vc, 'frameworkdir32') or guess_fw\n\n    @property\n    def FrameworkDir64(self):\n        \"\"\"\n        Microsoft .NET Framework 64bit directory.\n\n        Return\n        ------\n        str\n            path\n        \"\"\"\n        # Default path\n        guess_fw = join(self.WinDir, r'Microsoft.NET\\Framework64')\n\n        # Try to get path from registry, if fail use default path\n        return self.ri.lookup(self.ri.vc, 'frameworkdir64') or guess_fw\n\n    @property\n    def FrameworkVersion32(self):\n        \"\"\"\n        Microsoft .NET Framework 32bit versions.\n\n        Return\n        ------\n        tuple of str\n            versions\n        \"\"\"\n        return self._find_dot_net_versions(32)\n\n    @property\n    def FrameworkVersion64(self):\n        \"\"\"\n        Microsoft .NET Framework 64bit versions.\n\n        Return\n        ------\n        tuple of str\n            versions\n        \"\"\"\n        return self._find_dot_net_versions(64)\n\n    def _find_dot_net_versions(self, bits):\n        \"\"\"\n        Find Microsoft .NET Framework versions.\n\n        Parameters\n        ----------\n        bits: int\n            Platform number of bits: 32 or 64.\n\n        Return\n        ------\n        tuple of str\n            versions\n        \"\"\"\n        # Find actual .NET version in registry\n        reg_ver = self.ri.lookup(self.ri.vc, 'frameworkver%d' % bits)\n        dot_net_dir = getattr(self, 'FrameworkDir%d' % bits)\n        ver = reg_ver or self._use_last_dir_name(dot_net_dir, 'v') or ''\n\n        # Set .NET versions for specified MSVC++ version\n        if self.vs_ver >= 12.0:\n            return ver, 'v4.0'\n        elif self.vs_ver >= 10.0:\n            return 'v4.0.30319' if ver.lower()[:2] != 'v4' else ver, 'v3.5'\n        elif self.vs_ver == 9.0:\n            return 'v3.5', 'v2.0.50727'\n        elif self.vs_ver == 8.0:\n            return 'v3.0', 'v2.0.50727'\n\n    @staticmethod\n    def _use_last_dir_name(path, prefix=''):\n        \"\"\"\n        Return name of the last dir in path or '' if no dir found.\n\n        Parameters\n        ----------\n        path: str\n            Use dirs in this path\n        prefix: str\n            Use only dirs starting by this prefix\n\n        Return\n        ------\n        str\n            name\n        \"\"\"\n        matching_dirs = (\n            dir_name\n            for dir_name in reversed(listdir(path))\n            if isdir(join(path, dir_name)) and\n            dir_name.startswith(prefix)\n        )\n        return next(matching_dirs, None) or ''\n\n\nclass EnvironmentInfo:\n    \"\"\"\n    Return environment variables for specified Microsoft Visual C++ version\n    and platform : Lib, Include, Path and libpath.\n\n    This function is compatible with Microsoft Visual C++ 9.0 to 14.X.\n\n    Script created by analysing Microsoft environment configuration files like\n    \"vcvars[...].bat\", \"SetEnv.Cmd\", \"vcbuildtools.bat\", ...\n\n    Parameters\n    ----------\n    arch: str\n        Target architecture.\n    vc_ver: float\n        Required Microsoft Visual C++ version. If not set, autodetect the last\n        version.\n    vc_min_ver: float\n        Minimum Microsoft Visual C++ version.\n    \"\"\"\n\n    # Variables and properties in this class use originals CamelCase variables\n    # names from Microsoft source files for more easy comparison.\n\n    def __init__(self, arch, vc_ver=None, vc_min_ver=0):\n        self.pi = PlatformInfo(arch)\n        self.ri = RegistryInfo(self.pi)\n        self.si = SystemInfo(self.ri, vc_ver)\n\n        if self.vc_ver < vc_min_ver:\n            err = 'No suitable Microsoft Visual C++ version found'\n            raise distutils.errors.DistutilsPlatformError(err)\n\n    @property\n    def vs_ver(self):\n        \"\"\"\n        Microsoft Visual Studio.\n\n        Return\n        ------\n        float\n            version\n        \"\"\"\n        return self.si.vs_ver\n\n    @property\n    def vc_ver(self):\n        \"\"\"\n        Microsoft Visual C++ version.\n\n        Return\n        ------\n        float\n            version\n        \"\"\"\n        return self.si.vc_ver\n\n    @property\n    def VSTools(self):\n        \"\"\"\n        Microsoft Visual Studio Tools.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        paths = [r'Common7\\IDE', r'Common7\\Tools']\n\n        if self.vs_ver >= 14.0:\n            arch_subdir = self.pi.current_dir(hidex86=True, x64=True)\n            paths += [r'Common7\\IDE\\CommonExtensions\\Microsoft\\TestWindow']\n            paths += [r'Team Tools\\Performance Tools']\n            paths += [r'Team Tools\\Performance Tools%s' % arch_subdir]\n\n        return [join(self.si.VSInstallDir, path) for path in paths]\n\n    @property\n    def VCIncludes(self):\n        \"\"\"\n        Microsoft Visual C++ & Microsoft Foundation Class Includes.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        return [join(self.si.VCInstallDir, 'Include'),\n                join(self.si.VCInstallDir, r'ATLMFC\\Include')]\n\n    @property\n    def VCLibraries(self):\n        \"\"\"\n        Microsoft Visual C++ & Microsoft Foundation Class Libraries.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        if self.vs_ver >= 15.0:\n            arch_subdir = self.pi.target_dir(x64=True)\n        else:\n            arch_subdir = self.pi.target_dir(hidex86=True)\n        paths = ['Lib%s' % arch_subdir, r'ATLMFC\\Lib%s' % arch_subdir]\n\n        if self.vs_ver >= 14.0:\n            paths += [r'Lib\\store%s' % arch_subdir]\n\n        return [join(self.si.VCInstallDir, path) for path in paths]\n\n    @property\n    def VCStoreRefs(self):\n        \"\"\"\n        Microsoft Visual C++ store references Libraries.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        if self.vs_ver < 14.0:\n            return []\n        return [join(self.si.VCInstallDir, r'Lib\\store\\references')]\n\n    @property\n    def VCTools(self):\n        \"\"\"\n        Microsoft Visual C++ Tools.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        si = self.si\n        tools = [join(si.VCInstallDir, 'VCPackages')]\n\n        forcex86 = True if self.vs_ver <= 10.0 else False\n        arch_subdir = self.pi.cross_dir(forcex86)\n        if arch_subdir:\n            tools += [join(si.VCInstallDir, 'Bin%s' % arch_subdir)]\n\n        if self.vs_ver == 14.0:\n            path = 'Bin%s' % self.pi.current_dir(hidex86=True)\n            tools += [join(si.VCInstallDir, path)]\n\n        elif self.vs_ver >= 15.0:\n            host_dir = (r'bin\\HostX86%s' if self.pi.current_is_x86() else\n                        r'bin\\HostX64%s')\n            tools += [join(\n                si.VCInstallDir, host_dir % self.pi.target_dir(x64=True))]\n\n            if self.pi.current_cpu != self.pi.target_cpu:\n                tools += [join(\n                    si.VCInstallDir, host_dir % self.pi.current_dir(x64=True))]\n\n        else:\n            tools += [join(si.VCInstallDir, 'Bin')]\n\n        return tools\n\n    @property\n    def OSLibraries(self):\n        \"\"\"\n        Microsoft Windows SDK Libraries.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        if self.vs_ver <= 10.0:\n            arch_subdir = self.pi.target_dir(hidex86=True, x64=True)\n            return [join(self.si.WindowsSdkDir, 'Lib%s' % arch_subdir)]\n\n        else:\n            arch_subdir = self.pi.target_dir(x64=True)\n            lib = join(self.si.WindowsSdkDir, 'lib')\n            libver = self._sdk_subdir\n            return [join(lib, '%sum%s' % (libver , arch_subdir))]\n\n    @property\n    def OSIncludes(self):\n        \"\"\"\n        Microsoft Windows SDK Include.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        include = join(self.si.WindowsSdkDir, 'include')\n\n        if self.vs_ver <= 10.0:\n            return [include, join(include, 'gl')]\n\n        else:\n            if self.vs_ver >= 14.0:\n                sdkver = self._sdk_subdir\n            else:\n                sdkver = ''\n            return [join(include, '%sshared' % sdkver),\n                    join(include, '%sum' % sdkver),\n                    join(include, '%swinrt' % sdkver)]\n\n    @property\n    def OSLibpath(self):\n        \"\"\"\n        Microsoft Windows SDK Libraries Paths.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        ref = join(self.si.WindowsSdkDir, 'References')\n        libpath = []\n\n        if self.vs_ver <= 9.0:\n            libpath += self.OSLibraries\n\n        if self.vs_ver >= 11.0:\n            libpath += [join(ref, r'CommonConfiguration\\Neutral')]\n\n        if self.vs_ver >= 14.0:\n            libpath += [\n                ref,\n                join(self.si.WindowsSdkDir, 'UnionMetadata'),\n                join(ref, 'Windows.Foundation.UniversalApiContract', '1.0.0.0'),\n                join(ref, 'Windows.Foundation.FoundationContract', '1.0.0.0'),\n                join(ref,'Windows.Networking.Connectivity.WwanContract',\n                     '1.0.0.0'),\n                join(self.si.WindowsSdkDir, 'ExtensionSDKs', 'Microsoft.VCLibs',\n                     '%0.1f' % self.vs_ver, 'References', 'CommonConfiguration',\n                     'neutral'),\n            ]\n        return libpath\n\n    @property\n    def SdkTools(self):\n        \"\"\"\n        Microsoft Windows SDK Tools.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        return list(self._sdk_tools())\n\n    def _sdk_tools(self):\n        \"\"\"\n        Microsoft Windows SDK Tools paths generator.\n\n        Return\n        ------\n        generator of str\n            paths\n        \"\"\"\n        if self.vs_ver < 15.0:\n            bin_dir = 'Bin' if self.vs_ver <= 11.0 else r'Bin\\x86'\n            yield join(self.si.WindowsSdkDir, bin_dir)\n\n        if not self.pi.current_is_x86():\n            arch_subdir = self.pi.current_dir(x64=True)\n            path = 'Bin%s' % arch_subdir\n            yield join(self.si.WindowsSdkDir, path)\n\n        if self.vs_ver in (10.0, 11.0):\n            if self.pi.target_is_x86():\n                arch_subdir = ''\n            else:\n                arch_subdir = self.pi.current_dir(hidex86=True, x64=True)\n            path = r'Bin\\NETFX 4.0 Tools%s' % arch_subdir\n            yield join(self.si.WindowsSdkDir, path)\n\n        elif self.vs_ver >= 15.0:\n            path = join(self.si.WindowsSdkDir, 'Bin')\n            arch_subdir = self.pi.current_dir(x64=True)\n            sdkver = self.si.WindowsSdkLastVersion\n            yield join(path, '%s%s' % (sdkver, arch_subdir))\n\n        if self.si.WindowsSDKExecutablePath:\n            yield self.si.WindowsSDKExecutablePath\n\n    @property\n    def _sdk_subdir(self):\n        \"\"\"\n        Microsoft Windows SDK version subdir.\n\n        Return\n        ------\n        str\n            subdir\n        \"\"\"\n        ucrtver = self.si.WindowsSdkLastVersion\n        return ('%s\\\\' % ucrtver) if ucrtver else ''\n\n    @property\n    def SdkSetup(self):\n        \"\"\"\n        Microsoft Windows SDK Setup.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        if self.vs_ver > 9.0:\n            return []\n\n        return [join(self.si.WindowsSdkDir, 'Setup')]\n\n    @property\n    def FxTools(self):\n        \"\"\"\n        Microsoft .NET Framework Tools.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        pi = self.pi\n        si = self.si\n\n        if self.vs_ver <= 10.0:\n            include32 = True\n            include64 = not pi.target_is_x86() and not pi.current_is_x86()\n        else:\n            include32 = pi.target_is_x86() or pi.current_is_x86()\n            include64 = pi.current_cpu == 'amd64' or pi.target_cpu == 'amd64'\n\n        tools = []\n        if include32:\n            tools += [join(si.FrameworkDir32, ver)\n                      for ver in si.FrameworkVersion32]\n        if include64:\n            tools += [join(si.FrameworkDir64, ver)\n                      for ver in si.FrameworkVersion64]\n        return tools\n\n    @property\n    def NetFxSDKLibraries(self):\n        \"\"\"\n        Microsoft .Net Framework SDK Libraries.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        if self.vs_ver < 14.0 or not self.si.NetFxSdkDir:\n            return []\n\n        arch_subdir = self.pi.target_dir(x64=True)\n        return [join(self.si.NetFxSdkDir, r'lib\\um%s' % arch_subdir)]\n\n    @property\n    def NetFxSDKIncludes(self):\n        \"\"\"\n        Microsoft .Net Framework SDK Includes.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        if self.vs_ver < 14.0 or not self.si.NetFxSdkDir:\n            return []\n\n        return [join(self.si.NetFxSdkDir, r'include\\um')]\n\n    @property\n    def VsTDb(self):\n        \"\"\"\n        Microsoft Visual Studio Team System Database.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        return [join(self.si.VSInstallDir, r'VSTSDB\\Deploy')]\n\n    @property\n    def MSBuild(self):\n        \"\"\"\n        Microsoft Build Engine.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        if self.vs_ver < 12.0:\n            return []\n        elif self.vs_ver < 15.0:\n            base_path = self.si.ProgramFilesx86\n            arch_subdir = self.pi.current_dir(hidex86=True)\n        else:\n            base_path = self.si.VSInstallDir\n            arch_subdir = ''\n\n        path = r'MSBuild\\%0.1f\\bin%s' % (self.vs_ver, arch_subdir)\n        build = [join(base_path, path)]\n\n        if self.vs_ver >= 15.0:\n            # Add Roslyn C# & Visual Basic Compiler\n            build += [join(base_path, path, 'Roslyn')]\n\n        return build\n\n    @property\n    def HTMLHelpWorkshop(self):\n        \"\"\"\n        Microsoft HTML Help Workshop.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        if self.vs_ver < 11.0:\n            return []\n\n        return [join(self.si.ProgramFilesx86, 'HTML Help Workshop')]\n\n    @property\n    def UCRTLibraries(self):\n        \"\"\"\n        Microsoft Universal C Runtime SDK Libraries.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        if self.vs_ver < 14.0:\n            return []\n\n        arch_subdir = self.pi.target_dir(x64=True)\n        lib = join(self.si.UniversalCRTSdkDir, 'lib')\n        ucrtver = self._ucrt_subdir\n        return [join(lib, '%sucrt%s' % (ucrtver, arch_subdir))]\n\n    @property\n    def UCRTIncludes(self):\n        \"\"\"\n        Microsoft Universal C Runtime SDK Include.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        if self.vs_ver < 14.0:\n            return []\n\n        include = join(self.si.UniversalCRTSdkDir, 'include')\n        return [join(include, '%sucrt' % self._ucrt_subdir)]\n\n    @property\n    def _ucrt_subdir(self):\n        \"\"\"\n        Microsoft Universal C Runtime SDK version subdir.\n\n        Return\n        ------\n        str\n            subdir\n        \"\"\"\n        ucrtver = self.si.UniversalCRTSdkLastVersion\n        return ('%s\\\\' % ucrtver) if ucrtver else ''\n\n    @property\n    def FSharp(self):\n        \"\"\"\n        Microsoft Visual F#.\n\n        Return\n        ------\n        list of str\n            paths\n        \"\"\"\n        if 11.0 > self.vs_ver > 12.0:\n            return []\n\n        return [self.si.FSharpInstallDir]\n\n    @property\n    def VCRuntimeRedist(self):\n        \"\"\"\n        Microsoft Visual C++ runtime redistributable dll.\n\n        Return\n        ------\n        str\n            path\n        \"\"\"\n        vcruntime = 'vcruntime%d0.dll' % self.vc_ver\n        arch_subdir = self.pi.target_dir(x64=True).strip('\\\\')\n\n        # Installation prefixes candidates\n        prefixes = []\n        tools_path = self.si.VCInstallDir\n        redist_path = dirname(tools_path.replace(r'\\Tools', r'\\Redist'))\n        if isdir(redist_path):\n            # Redist version may not be exactly the same as tools\n            redist_path = join(redist_path, listdir(redist_path)[-1])\n            prefixes += [redist_path, join(redist_path, 'onecore')]\n\n        prefixes += [join(tools_path, 'redist')]  # VS14 legacy path\n\n        # CRT directory\n        crt_dirs = ('Microsoft.VC%d.CRT' % (self.vc_ver * 10),\n                    # Sometime store in directory with VS version instead of VC\n                    'Microsoft.VC%d.CRT' % (int(self.vs_ver) * 10))\n\n        # vcruntime path\n        for prefix, crt_dir in itertools.product(prefixes, crt_dirs):\n            path = join(prefix, arch_subdir, crt_dir, vcruntime)\n            if isfile(path):\n                return path\n\n    def return_env(self, exists=True):\n        \"\"\"\n        Return environment dict.\n\n        Parameters\n        ----------\n        exists: bool\n            It True, only return existing paths.\n\n        Return\n        ------\n        dict\n            environment\n        \"\"\"\n        env = dict(\n            include=self._build_paths('include',\n                                      [self.VCIncludes,\n                                       self.OSIncludes,\n                                       self.UCRTIncludes,\n                                       self.NetFxSDKIncludes],\n                                      exists),\n            lib=self._build_paths('lib',\n                                  [self.VCLibraries,\n                                   self.OSLibraries,\n                                   self.FxTools,\n                                   self.UCRTLibraries,\n                                   self.NetFxSDKLibraries],\n                                  exists),\n            libpath=self._build_paths('libpath',\n                                      [self.VCLibraries,\n                                       self.FxTools,\n                                       self.VCStoreRefs,\n                                       self.OSLibpath],\n                                      exists),\n            path=self._build_paths('path',\n                                   [self.VCTools,\n                                    self.VSTools,\n                                    self.VsTDb,\n                                    self.SdkTools,\n                                    self.SdkSetup,\n                                    self.FxTools,\n                                    self.MSBuild,\n                                    self.HTMLHelpWorkshop,\n                                    self.FSharp],\n                                   exists),\n        )\n        if self.vs_ver >= 14 and isfile(self.VCRuntimeRedist):\n            env['py_vcruntime_redist'] = self.VCRuntimeRedist\n        return env\n\n    def _build_paths(self, name, spec_path_lists, exists):\n        \"\"\"\n        Given an environment variable name and specified paths,\n        return a pathsep-separated string of paths containing\n        unique, extant, directories from those paths and from\n        the environment variable. Raise an error if no paths\n        are resolved.\n\n        Parameters\n        ----------\n        name: str\n            Environment variable name\n        spec_path_lists: list of str\n            Paths\n        exists: bool\n            It True, only return existing paths.\n\n        Return\n        ------\n        str\n            Pathsep-separated paths\n        \"\"\"\n        # flatten spec_path_lists\n        spec_paths = itertools.chain.from_iterable(spec_path_lists)\n        env_paths = environ.get(name, '').split(pathsep)\n        paths = itertools.chain(spec_paths, env_paths)\n        extant_paths = list(filter(isdir, paths)) if exists else paths\n        if not extant_paths:\n            msg = \"%s environment variable is empty\" % name.upper()\n            raise distutils.errors.DistutilsPlatformError(msg)\n        unique_paths = self._unique_everseen(extant_paths)\n        return pathsep.join(unique_paths)\n\n    # from Python docs\n    @staticmethod\n    def _unique_everseen(iterable, key=None):\n        \"\"\"\n        List unique elements, preserving order.\n        Remember all elements ever seen.\n\n        _unique_everseen('AAAABBBCCDAABBB') --> A B C D\n\n        _unique_everseen('ABBCcAD', str.lower) --> A B C D\n        \"\"\"\n        seen = set()\n        seen_add = seen.add\n        if key is None:\n            for element in filterfalse(seen.__contains__, iterable):\n                seen_add(element)\n                yield element\n        else:\n            for element in iterable:\n                k = key(element)\n                if k not in seen:\n                    seen_add(k)\n                    yield element\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/namespaces.py",
    "content": "import os\nfrom distutils import log\nimport itertools\n\nfrom setuptools.extern.six.moves import map\n\n\nflatten = itertools.chain.from_iterable\n\n\nclass Installer:\n\n    nspkg_ext = '-nspkg.pth'\n\n    def install_namespaces(self):\n        nsp = self._get_all_ns_packages()\n        if not nsp:\n            return\n        filename, ext = os.path.splitext(self._get_target())\n        filename += self.nspkg_ext\n        self.outputs.append(filename)\n        log.info(\"Installing %s\", filename)\n        lines = map(self._gen_nspkg_line, nsp)\n\n        if self.dry_run:\n            # always generate the lines, even in dry run\n            list(lines)\n            return\n\n        with open(filename, 'wt') as f:\n            f.writelines(lines)\n\n    def uninstall_namespaces(self):\n        filename, ext = os.path.splitext(self._get_target())\n        filename += self.nspkg_ext\n        if not os.path.exists(filename):\n            return\n        log.info(\"Removing %s\", filename)\n        os.remove(filename)\n\n    def _get_target(self):\n        return self.target\n\n    _nspkg_tmpl = (\n        \"import sys, types, os\",\n        \"has_mfs = sys.version_info > (3, 5)\",\n        \"p = os.path.join(%(root)s, *%(pth)r)\",\n        \"importlib = has_mfs and __import__('importlib.util')\",\n        \"has_mfs and __import__('importlib.machinery')\",\n        \"m = has_mfs and \"\n            \"sys.modules.setdefault(%(pkg)r, \"\n                \"importlib.util.module_from_spec(\"\n                    \"importlib.machinery.PathFinder.find_spec(%(pkg)r, \"\n                        \"[os.path.dirname(p)])))\",\n        \"m = m or \"\n            \"sys.modules.setdefault(%(pkg)r, types.ModuleType(%(pkg)r))\",\n        \"mp = (m or []) and m.__dict__.setdefault('__path__',[])\",\n        \"(p not in mp) and mp.append(p)\",\n    )\n    \"lines for the namespace installer\"\n\n    _nspkg_tmpl_multi = (\n        'm and setattr(sys.modules[%(parent)r], %(child)r, m)',\n    )\n    \"additional line(s) when a parent package is indicated\"\n\n    def _get_root(self):\n        return \"sys._getframe(1).f_locals['sitedir']\"\n\n    def _gen_nspkg_line(self, pkg):\n        # ensure pkg is not a unicode string under Python 2.7\n        pkg = str(pkg)\n        pth = tuple(pkg.split('.'))\n        root = self._get_root()\n        tmpl_lines = self._nspkg_tmpl\n        parent, sep, child = pkg.rpartition('.')\n        if parent:\n            tmpl_lines += self._nspkg_tmpl_multi\n        return ';'.join(tmpl_lines) % locals() + '\\n'\n\n    def _get_all_ns_packages(self):\n        \"\"\"Return sorted list of all package namespaces\"\"\"\n        pkgs = self.distribution.namespace_packages or []\n        return sorted(flatten(map(self._pkg_names, pkgs)))\n\n    @staticmethod\n    def _pkg_names(pkg):\n        \"\"\"\n        Given a namespace package, yield the components of that\n        package.\n\n        >>> names = Installer._pkg_names('a.b.c')\n        >>> set(names) == set(['a', 'a.b', 'a.b.c'])\n        True\n        \"\"\"\n        parts = pkg.split('.')\n        while parts:\n            yield '.'.join(parts)\n            parts.pop()\n\n\nclass DevelopInstaller(Installer):\n    def _get_root(self):\n        return repr(str(self.egg_path))\n\n    def _get_target(self):\n        return self.egg_link\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/package_index.py",
    "content": "\"\"\"PyPI and direct package downloading\"\"\"\nimport sys\nimport os\nimport re\nimport shutil\nimport socket\nimport base64\nimport hashlib\nimport itertools\nimport warnings\nfrom functools import wraps\n\nfrom setuptools.extern import six\nfrom setuptools.extern.six.moves import urllib, http_client, configparser, map\n\nimport setuptools\nfrom pkg_resources import (\n    CHECKOUT_DIST, Distribution, BINARY_DIST, normalize_path, SOURCE_DIST,\n    Environment, find_distributions, safe_name, safe_version,\n    to_filename, Requirement, DEVELOP_DIST, EGG_DIST,\n)\nfrom setuptools import ssl_support\nfrom distutils import log\nfrom distutils.errors import DistutilsError\nfrom fnmatch import translate\nfrom setuptools.py27compat import get_all_headers\nfrom setuptools.py33compat import unescape\nfrom setuptools.wheel import Wheel\n\n__metaclass__ = type\n\nEGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$')\nHREF = re.compile(r\"\"\"href\\s*=\\s*['\"]?([^'\"> ]+)\"\"\", re.I)\nPYPI_MD5 = re.compile(\n    r'<a href=\"([^\"#]+)\">([^<]+)</a>\\n\\s+\\(<a (?:title=\"MD5 hash\"\\n\\s+)'\n    r'href=\"[^?]+\\?:action=show_md5&amp;digest=([0-9a-f]{32})\">md5</a>\\)'\n)\nURL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match\nEXTENSIONS = \".tar.gz .tar.bz2 .tar .zip .tgz\".split()\n\n__all__ = [\n    'PackageIndex', 'distros_for_url', 'parse_bdist_wininst',\n    'interpret_distro_name',\n]\n\n_SOCKET_TIMEOUT = 15\n\n_tmpl = \"setuptools/{setuptools.__version__} Python-urllib/{py_major}\"\nuser_agent = _tmpl.format(py_major='{}.{}'.format(*sys.version_info), setuptools=setuptools)\n\n\ndef parse_requirement_arg(spec):\n    try:\n        return Requirement.parse(spec)\n    except ValueError:\n        raise DistutilsError(\n            \"Not a URL, existing file, or requirement spec: %r\" % (spec,)\n        )\n\n\ndef parse_bdist_wininst(name):\n    \"\"\"Return (base,pyversion) or (None,None) for possible .exe name\"\"\"\n\n    lower = name.lower()\n    base, py_ver, plat = None, None, None\n\n    if lower.endswith('.exe'):\n        if lower.endswith('.win32.exe'):\n            base = name[:-10]\n            plat = 'win32'\n        elif lower.startswith('.win32-py', -16):\n            py_ver = name[-7:-4]\n            base = name[:-16]\n            plat = 'win32'\n        elif lower.endswith('.win-amd64.exe'):\n            base = name[:-14]\n            plat = 'win-amd64'\n        elif lower.startswith('.win-amd64-py', -20):\n            py_ver = name[-7:-4]\n            base = name[:-20]\n            plat = 'win-amd64'\n    return base, py_ver, plat\n\n\ndef egg_info_for_url(url):\n    parts = urllib.parse.urlparse(url)\n    scheme, server, path, parameters, query, fragment = parts\n    base = urllib.parse.unquote(path.split('/')[-1])\n    if server == 'sourceforge.net' and base == 'download':  # XXX Yuck\n        base = urllib.parse.unquote(path.split('/')[-2])\n    if '#' in base:\n        base, fragment = base.split('#', 1)\n    return base, fragment\n\n\ndef distros_for_url(url, metadata=None):\n    \"\"\"Yield egg or source distribution objects that might be found at a URL\"\"\"\n    base, fragment = egg_info_for_url(url)\n    for dist in distros_for_location(url, base, metadata):\n        yield dist\n    if fragment:\n        match = EGG_FRAGMENT.match(fragment)\n        if match:\n            for dist in interpret_distro_name(\n                url, match.group(1), metadata, precedence=CHECKOUT_DIST\n            ):\n                yield dist\n\n\ndef distros_for_location(location, basename, metadata=None):\n    \"\"\"Yield egg or source distribution objects based on basename\"\"\"\n    if basename.endswith('.egg.zip'):\n        basename = basename[:-4]  # strip the .zip\n    if basename.endswith('.egg') and '-' in basename:\n        # only one, unambiguous interpretation\n        return [Distribution.from_location(location, basename, metadata)]\n    if basename.endswith('.whl') and '-' in basename:\n        wheel = Wheel(basename)\n        if not wheel.is_compatible():\n            return []\n        return [Distribution(\n            location=location,\n            project_name=wheel.project_name,\n            version=wheel.version,\n            # Increase priority over eggs.\n            precedence=EGG_DIST + 1,\n        )]\n    if basename.endswith('.exe'):\n        win_base, py_ver, platform = parse_bdist_wininst(basename)\n        if win_base is not None:\n            return interpret_distro_name(\n                location, win_base, metadata, py_ver, BINARY_DIST, platform\n            )\n    # Try source distro extensions (.zip, .tgz, etc.)\n    #\n    for ext in EXTENSIONS:\n        if basename.endswith(ext):\n            basename = basename[:-len(ext)]\n            return interpret_distro_name(location, basename, metadata)\n    return []  # no extension matched\n\n\ndef distros_for_filename(filename, metadata=None):\n    \"\"\"Yield possible egg or source distribution objects based on a filename\"\"\"\n    return distros_for_location(\n        normalize_path(filename), os.path.basename(filename), metadata\n    )\n\n\ndef interpret_distro_name(\n        location, basename, metadata, py_version=None, precedence=SOURCE_DIST,\n        platform=None\n):\n    \"\"\"Generate alternative interpretations of a source distro name\n\n    Note: if `location` is a filesystem filename, you should call\n    ``pkg_resources.normalize_path()`` on it before passing it to this\n    routine!\n    \"\"\"\n    # Generate alternative interpretations of a source distro name\n    # Because some packages are ambiguous as to name/versions split\n    # e.g. \"adns-python-1.1.0\", \"egenix-mx-commercial\", etc.\n    # So, we generate each possible interepretation (e.g. \"adns, python-1.1.0\"\n    # \"adns-python, 1.1.0\", and \"adns-python-1.1.0, no version\").  In practice,\n    # the spurious interpretations should be ignored, because in the event\n    # there's also an \"adns\" package, the spurious \"python-1.1.0\" version will\n    # compare lower than any numeric version number, and is therefore unlikely\n    # to match a request for it.  It's still a potential problem, though, and\n    # in the long run PyPI and the distutils should go for \"safe\" names and\n    # versions in distribution archive names (sdist and bdist).\n\n    parts = basename.split('-')\n    if not py_version and any(re.match(r'py\\d\\.\\d$', p) for p in parts[2:]):\n        # it is a bdist_dumb, not an sdist -- bail out\n        return\n\n    for p in range(1, len(parts) + 1):\n        yield Distribution(\n            location, metadata, '-'.join(parts[:p]), '-'.join(parts[p:]),\n            py_version=py_version, precedence=precedence,\n            platform=platform\n        )\n\n\n# From Python 2.7 docs\ndef unique_everseen(iterable, key=None):\n    \"List unique elements, preserving order. Remember all elements ever seen.\"\n    # unique_everseen('AAAABBBCCDAABBB') --> A B C D\n    # unique_everseen('ABBCcAD', str.lower) --> A B C D\n    seen = set()\n    seen_add = seen.add\n    if key is None:\n        for element in six.moves.filterfalse(seen.__contains__, iterable):\n            seen_add(element)\n            yield element\n    else:\n        for element in iterable:\n            k = key(element)\n            if k not in seen:\n                seen_add(k)\n                yield element\n\n\ndef unique_values(func):\n    \"\"\"\n    Wrap a function returning an iterable such that the resulting iterable\n    only ever yields unique items.\n    \"\"\"\n\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        return unique_everseen(func(*args, **kwargs))\n\n    return wrapper\n\n\nREL = re.compile(r\"\"\"<([^>]*\\srel\\s*=\\s*['\"]?([^'\">]+)[^>]*)>\"\"\", re.I)\n# this line is here to fix emacs' cruddy broken syntax highlighting\n\n\n@unique_values\ndef find_external_links(url, page):\n    \"\"\"Find rel=\"homepage\" and rel=\"download\" links in `page`, yielding URLs\"\"\"\n\n    for match in REL.finditer(page):\n        tag, rel = match.groups()\n        rels = set(map(str.strip, rel.lower().split(',')))\n        if 'homepage' in rels or 'download' in rels:\n            for match in HREF.finditer(tag):\n                yield urllib.parse.urljoin(url, htmldecode(match.group(1)))\n\n    for tag in (\"<th>Home Page\", \"<th>Download URL\"):\n        pos = page.find(tag)\n        if pos != -1:\n            match = HREF.search(page, pos)\n            if match:\n                yield urllib.parse.urljoin(url, htmldecode(match.group(1)))\n\n\nclass ContentChecker:\n    \"\"\"\n    A null content checker that defines the interface for checking content\n    \"\"\"\n\n    def feed(self, block):\n        \"\"\"\n        Feed a block of data to the hash.\n        \"\"\"\n        return\n\n    def is_valid(self):\n        \"\"\"\n        Check the hash. Return False if validation fails.\n        \"\"\"\n        return True\n\n    def report(self, reporter, template):\n        \"\"\"\n        Call reporter with information about the checker (hash name)\n        substituted into the template.\n        \"\"\"\n        return\n\n\nclass HashChecker(ContentChecker):\n    pattern = re.compile(\n        r'(?P<hash_name>sha1|sha224|sha384|sha256|sha512|md5)='\n        r'(?P<expected>[a-f0-9]+)'\n    )\n\n    def __init__(self, hash_name, expected):\n        self.hash_name = hash_name\n        self.hash = hashlib.new(hash_name)\n        self.expected = expected\n\n    @classmethod\n    def from_url(cls, url):\n        \"Construct a (possibly null) ContentChecker from a URL\"\n        fragment = urllib.parse.urlparse(url)[-1]\n        if not fragment:\n            return ContentChecker()\n        match = cls.pattern.search(fragment)\n        if not match:\n            return ContentChecker()\n        return cls(**match.groupdict())\n\n    def feed(self, block):\n        self.hash.update(block)\n\n    def is_valid(self):\n        return self.hash.hexdigest() == self.expected\n\n    def report(self, reporter, template):\n        msg = template % self.hash_name\n        return reporter(msg)\n\n\nclass PackageIndex(Environment):\n    \"\"\"A distribution index that scans web pages for download URLs\"\"\"\n\n    def __init__(\n            self, index_url=\"https://pypi.org/simple/\", hosts=('*',),\n            ca_bundle=None, verify_ssl=True, *args, **kw\n    ):\n        Environment.__init__(self, *args, **kw)\n        self.index_url = index_url + \"/\" [:not index_url.endswith('/')]\n        self.scanned_urls = {}\n        self.fetched_urls = {}\n        self.package_pages = {}\n        self.allows = re.compile('|'.join(map(translate, hosts))).match\n        self.to_scan = []\n        use_ssl = (\n            verify_ssl\n            and ssl_support.is_available\n            and (ca_bundle or ssl_support.find_ca_bundle())\n        )\n        if use_ssl:\n            self.opener = ssl_support.opener_for(ca_bundle)\n        else:\n            self.opener = urllib.request.urlopen\n\n    def process_url(self, url, retrieve=False):\n        \"\"\"Evaluate a URL as a possible download, and maybe retrieve it\"\"\"\n        if url in self.scanned_urls and not retrieve:\n            return\n        self.scanned_urls[url] = True\n        if not URL_SCHEME(url):\n            self.process_filename(url)\n            return\n        else:\n            dists = list(distros_for_url(url))\n            if dists:\n                if not self.url_ok(url):\n                    return\n                self.debug(\"Found link: %s\", url)\n\n        if dists or not retrieve or url in self.fetched_urls:\n            list(map(self.add, dists))\n            return  # don't need the actual page\n\n        if not self.url_ok(url):\n            self.fetched_urls[url] = True\n            return\n\n        self.info(\"Reading %s\", url)\n        self.fetched_urls[url] = True  # prevent multiple fetch attempts\n        tmpl = \"Download error on %s: %%s -- Some packages may not be found!\"\n        f = self.open_url(url, tmpl % url)\n        if f is None:\n            return\n        self.fetched_urls[f.url] = True\n        if 'html' not in f.headers.get('content-type', '').lower():\n            f.close()  # not html, we can't process it\n            return\n\n        base = f.url  # handle redirects\n        page = f.read()\n        if not isinstance(page, str):\n            # In Python 3 and got bytes but want str.\n            if isinstance(f, urllib.error.HTTPError):\n                # Errors have no charset, assume latin1:\n                charset = 'latin-1'\n            else:\n                charset = f.headers.get_param('charset') or 'latin-1'\n            page = page.decode(charset, \"ignore\")\n        f.close()\n        for match in HREF.finditer(page):\n            link = urllib.parse.urljoin(base, htmldecode(match.group(1)))\n            self.process_url(link)\n        if url.startswith(self.index_url) and getattr(f, 'code', None) != 404:\n            page = self.process_index(url, page)\n\n    def process_filename(self, fn, nested=False):\n        # process filenames or directories\n        if not os.path.exists(fn):\n            self.warn(\"Not found: %s\", fn)\n            return\n\n        if os.path.isdir(fn) and not nested:\n            path = os.path.realpath(fn)\n            for item in os.listdir(path):\n                self.process_filename(os.path.join(path, item), True)\n\n        dists = distros_for_filename(fn)\n        if dists:\n            self.debug(\"Found: %s\", fn)\n            list(map(self.add, dists))\n\n    def url_ok(self, url, fatal=False):\n        s = URL_SCHEME(url)\n        is_file = s and s.group(1).lower() == 'file'\n        if is_file or self.allows(urllib.parse.urlparse(url)[1]):\n            return True\n        msg = (\n            \"\\nNote: Bypassing %s (disallowed host; see \"\n            \"http://bit.ly/2hrImnY for details).\\n\")\n        if fatal:\n            raise DistutilsError(msg % url)\n        else:\n            self.warn(msg, url)\n\n    def scan_egg_links(self, search_path):\n        dirs = filter(os.path.isdir, search_path)\n        egg_links = (\n            (path, entry)\n            for path in dirs\n            for entry in os.listdir(path)\n            if entry.endswith('.egg-link')\n        )\n        list(itertools.starmap(self.scan_egg_link, egg_links))\n\n    def scan_egg_link(self, path, entry):\n        with open(os.path.join(path, entry)) as raw_lines:\n            # filter non-empty lines\n            lines = list(filter(None, map(str.strip, raw_lines)))\n\n        if len(lines) != 2:\n            # format is not recognized; punt\n            return\n\n        egg_path, setup_path = lines\n\n        for dist in find_distributions(os.path.join(path, egg_path)):\n            dist.location = os.path.join(path, *lines)\n            dist.precedence = SOURCE_DIST\n            self.add(dist)\n\n    def process_index(self, url, page):\n        \"\"\"Process the contents of a PyPI page\"\"\"\n\n        def scan(link):\n            # Process a URL to see if it's for a package page\n            if link.startswith(self.index_url):\n                parts = list(map(\n                    urllib.parse.unquote, link[len(self.index_url):].split('/')\n                ))\n                if len(parts) == 2 and '#' not in parts[1]:\n                    # it's a package page, sanitize and index it\n                    pkg = safe_name(parts[0])\n                    ver = safe_version(parts[1])\n                    self.package_pages.setdefault(pkg.lower(), {})[link] = True\n                    return to_filename(pkg), to_filename(ver)\n            return None, None\n\n        # process an index page into the package-page index\n        for match in HREF.finditer(page):\n            try:\n                scan(urllib.parse.urljoin(url, htmldecode(match.group(1))))\n            except ValueError:\n                pass\n\n        pkg, ver = scan(url)  # ensure this page is in the page index\n        if pkg:\n            # process individual package page\n            for new_url in find_external_links(url, page):\n                # Process the found URL\n                base, frag = egg_info_for_url(new_url)\n                if base.endswith('.py') and not frag:\n                    if ver:\n                        new_url += '#egg=%s-%s' % (pkg, ver)\n                    else:\n                        self.need_version_info(url)\n                self.scan_url(new_url)\n\n            return PYPI_MD5.sub(\n                lambda m: '<a href=\"%s#md5=%s\">%s</a>' % m.group(1, 3, 2), page\n            )\n        else:\n            return \"\"  # no sense double-scanning non-package pages\n\n    def need_version_info(self, url):\n        self.scan_all(\n            \"Page at %s links to .py file(s) without version info; an index \"\n            \"scan is required.\", url\n        )\n\n    def scan_all(self, msg=None, *args):\n        if self.index_url not in self.fetched_urls:\n            if msg:\n                self.warn(msg, *args)\n            self.info(\n                \"Scanning index of all packages (this may take a while)\"\n            )\n        self.scan_url(self.index_url)\n\n    def find_packages(self, requirement):\n        self.scan_url(self.index_url + requirement.unsafe_name + '/')\n\n        if not self.package_pages.get(requirement.key):\n            # Fall back to safe version of the name\n            self.scan_url(self.index_url + requirement.project_name + '/')\n\n        if not self.package_pages.get(requirement.key):\n            # We couldn't find the target package, so search the index page too\n            self.not_found_in_index(requirement)\n\n        for url in list(self.package_pages.get(requirement.key, ())):\n            # scan each page that might be related to the desired package\n            self.scan_url(url)\n\n    def obtain(self, requirement, installer=None):\n        self.prescan()\n        self.find_packages(requirement)\n        for dist in self[requirement.key]:\n            if dist in requirement:\n                return dist\n            self.debug(\"%s does not match %s\", requirement, dist)\n        return super(PackageIndex, self).obtain(requirement, installer)\n\n    def check_hash(self, checker, filename, tfp):\n        \"\"\"\n        checker is a ContentChecker\n        \"\"\"\n        checker.report(\n            self.debug,\n            \"Validating %%s checksum for %s\" % filename)\n        if not checker.is_valid():\n            tfp.close()\n            os.unlink(filename)\n            raise DistutilsError(\n                \"%s validation failed for %s; \"\n                \"possible download problem?\"\n                % (checker.hash.name, os.path.basename(filename))\n            )\n\n    def add_find_links(self, urls):\n        \"\"\"Add `urls` to the list that will be prescanned for searches\"\"\"\n        for url in urls:\n            if (\n                self.to_scan is None  # if we have already \"gone online\"\n                or not URL_SCHEME(url)  # or it's a local file/directory\n                or url.startswith('file:')\n                or list(distros_for_url(url))  # or a direct package link\n            ):\n                # then go ahead and process it now\n                self.scan_url(url)\n            else:\n                # otherwise, defer retrieval till later\n                self.to_scan.append(url)\n\n    def prescan(self):\n        \"\"\"Scan urls scheduled for prescanning (e.g. --find-links)\"\"\"\n        if self.to_scan:\n            list(map(self.scan_url, self.to_scan))\n        self.to_scan = None  # from now on, go ahead and process immediately\n\n    def not_found_in_index(self, requirement):\n        if self[requirement.key]:  # we've seen at least one distro\n            meth, msg = self.info, \"Couldn't retrieve index page for %r\"\n        else:  # no distros seen for this name, might be misspelled\n            meth, msg = (\n                self.warn,\n                \"Couldn't find index page for %r (maybe misspelled?)\")\n        meth(msg, requirement.unsafe_name)\n        self.scan_all()\n\n    def download(self, spec, tmpdir):\n        \"\"\"Locate and/or download `spec` to `tmpdir`, returning a local path\n\n        `spec` may be a ``Requirement`` object, or a string containing a URL,\n        an existing local filename, or a project/version requirement spec\n        (i.e. the string form of a ``Requirement`` object).  If it is the URL\n        of a .py file with an unambiguous ``#egg=name-version`` tag (i.e., one\n        that escapes ``-`` as ``_`` throughout), a trivial ``setup.py`` is\n        automatically created alongside the downloaded file.\n\n        If `spec` is a ``Requirement`` object or a string containing a\n        project/version requirement spec, this method returns the location of\n        a matching distribution (possibly after downloading it to `tmpdir`).\n        If `spec` is a locally existing file or directory name, it is simply\n        returned unchanged.  If `spec` is a URL, it is downloaded to a subpath\n        of `tmpdir`, and the local filename is returned.  Various errors may be\n        raised if a problem occurs during downloading.\n        \"\"\"\n        if not isinstance(spec, Requirement):\n            scheme = URL_SCHEME(spec)\n            if scheme:\n                # It's a url, download it to tmpdir\n                found = self._download_url(scheme.group(1), spec, tmpdir)\n                base, fragment = egg_info_for_url(spec)\n                if base.endswith('.py'):\n                    found = self.gen_setup(found, fragment, tmpdir)\n                return found\n            elif os.path.exists(spec):\n                # Existing file or directory, just return it\n                return spec\n            else:\n                spec = parse_requirement_arg(spec)\n        return getattr(self.fetch_distribution(spec, tmpdir), 'location', None)\n\n    def fetch_distribution(\n            self, requirement, tmpdir, force_scan=False, source=False,\n            develop_ok=False, local_index=None):\n        \"\"\"Obtain a distribution suitable for fulfilling `requirement`\n\n        `requirement` must be a ``pkg_resources.Requirement`` instance.\n        If necessary, or if the `force_scan` flag is set, the requirement is\n        searched for in the (online) package index as well as the locally\n        installed packages.  If a distribution matching `requirement` is found,\n        the returned distribution's ``location`` is the value you would have\n        gotten from calling the ``download()`` method with the matching\n        distribution's URL or filename.  If no matching distribution is found,\n        ``None`` is returned.\n\n        If the `source` flag is set, only source distributions and source\n        checkout links will be considered.  Unless the `develop_ok` flag is\n        set, development and system eggs (i.e., those using the ``.egg-info``\n        format) will be ignored.\n        \"\"\"\n        # process a Requirement\n        self.info(\"Searching for %s\", requirement)\n        skipped = {}\n        dist = None\n\n        def find(req, env=None):\n            if env is None:\n                env = self\n            # Find a matching distribution; may be called more than once\n\n            for dist in env[req.key]:\n\n                if dist.precedence == DEVELOP_DIST and not develop_ok:\n                    if dist not in skipped:\n                        self.warn(\n                            \"Skipping development or system egg: %s\", dist,\n                        )\n                        skipped[dist] = 1\n                    continue\n\n                test = (\n                    dist in req\n                    and (dist.precedence <= SOURCE_DIST or not source)\n                )\n                if test:\n                    loc = self.download(dist.location, tmpdir)\n                    dist.download_location = loc\n                    if os.path.exists(dist.download_location):\n                        return dist\n\n        if force_scan:\n            self.prescan()\n            self.find_packages(requirement)\n            dist = find(requirement)\n\n        if not dist and local_index is not None:\n            dist = find(requirement, local_index)\n\n        if dist is None:\n            if self.to_scan is not None:\n                self.prescan()\n            dist = find(requirement)\n\n        if dist is None and not force_scan:\n            self.find_packages(requirement)\n            dist = find(requirement)\n\n        if dist is None:\n            self.warn(\n                \"No local packages or working download links found for %s%s\",\n                (source and \"a source distribution of \" or \"\"),\n                requirement,\n            )\n        else:\n            self.info(\"Best match: %s\", dist)\n            return dist.clone(location=dist.download_location)\n\n    def fetch(self, requirement, tmpdir, force_scan=False, source=False):\n        \"\"\"Obtain a file suitable for fulfilling `requirement`\n\n        DEPRECATED; use the ``fetch_distribution()`` method now instead.  For\n        backward compatibility, this routine is identical but returns the\n        ``location`` of the downloaded distribution instead of a distribution\n        object.\n        \"\"\"\n        dist = self.fetch_distribution(requirement, tmpdir, force_scan, source)\n        if dist is not None:\n            return dist.location\n        return None\n\n    def gen_setup(self, filename, fragment, tmpdir):\n        match = EGG_FRAGMENT.match(fragment)\n        dists = match and [\n            d for d in\n            interpret_distro_name(filename, match.group(1), None) if d.version\n        ] or []\n\n        if len(dists) == 1:  # unambiguous ``#egg`` fragment\n            basename = os.path.basename(filename)\n\n            # Make sure the file has been downloaded to the temp dir.\n            if os.path.dirname(filename) != tmpdir:\n                dst = os.path.join(tmpdir, basename)\n                from setuptools.command.easy_install import samefile\n                if not samefile(filename, dst):\n                    shutil.copy2(filename, dst)\n                    filename = dst\n\n            with open(os.path.join(tmpdir, 'setup.py'), 'w') as file:\n                file.write(\n                    \"from setuptools import setup\\n\"\n                    \"setup(name=%r, version=%r, py_modules=[%r])\\n\"\n                    % (\n                        dists[0].project_name, dists[0].version,\n                        os.path.splitext(basename)[0]\n                    )\n                )\n            return filename\n\n        elif match:\n            raise DistutilsError(\n                \"Can't unambiguously interpret project/version identifier %r; \"\n                \"any dashes in the name or version should be escaped using \"\n                \"underscores. %r\" % (fragment, dists)\n            )\n        else:\n            raise DistutilsError(\n                \"Can't process plain .py files without an '#egg=name-version'\"\n                \" suffix to enable automatic setup script generation.\"\n            )\n\n    dl_blocksize = 8192\n\n    def _download_to(self, url, filename):\n        self.info(\"Downloading %s\", url)\n        # Download the file\n        fp = None\n        try:\n            checker = HashChecker.from_url(url)\n            fp = self.open_url(url)\n            if isinstance(fp, urllib.error.HTTPError):\n                raise DistutilsError(\n                    \"Can't download %s: %s %s\" % (url, fp.code, fp.msg)\n                )\n            headers = fp.info()\n            blocknum = 0\n            bs = self.dl_blocksize\n            size = -1\n            if \"content-length\" in headers:\n                # Some servers return multiple Content-Length headers :(\n                sizes = get_all_headers(headers, 'Content-Length')\n                size = max(map(int, sizes))\n                self.reporthook(url, filename, blocknum, bs, size)\n            with open(filename, 'wb') as tfp:\n                while True:\n                    block = fp.read(bs)\n                    if block:\n                        checker.feed(block)\n                        tfp.write(block)\n                        blocknum += 1\n                        self.reporthook(url, filename, blocknum, bs, size)\n                    else:\n                        break\n                self.check_hash(checker, filename, tfp)\n            return headers\n        finally:\n            if fp:\n                fp.close()\n\n    def reporthook(self, url, filename, blocknum, blksize, size):\n        pass  # no-op\n\n    def open_url(self, url, warning=None):\n        if url.startswith('file:'):\n            return local_open(url)\n        try:\n            return open_with_auth(url, self.opener)\n        except (ValueError, http_client.InvalidURL) as v:\n            msg = ' '.join([str(arg) for arg in v.args])\n            if warning:\n                self.warn(warning, msg)\n            else:\n                raise DistutilsError('%s %s' % (url, msg))\n        except urllib.error.HTTPError as v:\n            return v\n        except urllib.error.URLError as v:\n            if warning:\n                self.warn(warning, v.reason)\n            else:\n                raise DistutilsError(\"Download error for %s: %s\"\n                                     % (url, v.reason))\n        except http_client.BadStatusLine as v:\n            if warning:\n                self.warn(warning, v.line)\n            else:\n                raise DistutilsError(\n                    '%s returned a bad status line. The server might be '\n                    'down, %s' %\n                    (url, v.line)\n                )\n        except (http_client.HTTPException, socket.error) as v:\n            if warning:\n                self.warn(warning, v)\n            else:\n                raise DistutilsError(\"Download error for %s: %s\"\n                                     % (url, v))\n\n    def _download_url(self, scheme, url, tmpdir):\n        # Determine download filename\n        #\n        name, fragment = egg_info_for_url(url)\n        if name:\n            while '..' in name:\n                name = name.replace('..', '.').replace('\\\\', '_')\n        else:\n            name = \"__downloaded__\"  # default if URL has no path contents\n\n        if name.endswith('.egg.zip'):\n            name = name[:-4]  # strip the extra .zip before download\n\n        filename = os.path.join(tmpdir, name)\n\n        # Download the file\n        #\n        if scheme == 'svn' or scheme.startswith('svn+'):\n            return self._download_svn(url, filename)\n        elif scheme == 'git' or scheme.startswith('git+'):\n            return self._download_git(url, filename)\n        elif scheme.startswith('hg+'):\n            return self._download_hg(url, filename)\n        elif scheme == 'file':\n            return urllib.request.url2pathname(urllib.parse.urlparse(url)[2])\n        else:\n            self.url_ok(url, True)  # raises error if not allowed\n            return self._attempt_download(url, filename)\n\n    def scan_url(self, url):\n        self.process_url(url, True)\n\n    def _attempt_download(self, url, filename):\n        headers = self._download_to(url, filename)\n        if 'html' in headers.get('content-type', '').lower():\n            return self._download_html(url, headers, filename)\n        else:\n            return filename\n\n    def _download_html(self, url, headers, filename):\n        file = open(filename)\n        for line in file:\n            if line.strip():\n                # Check for a subversion index page\n                if re.search(r'<title>([^- ]+ - )?Revision \\d+:', line):\n                    # it's a subversion index page:\n                    file.close()\n                    os.unlink(filename)\n                    return self._download_svn(url, filename)\n                break  # not an index page\n        file.close()\n        os.unlink(filename)\n        raise DistutilsError(\"Unexpected HTML page found at \" + url)\n\n    def _download_svn(self, url, filename):\n        warnings.warn(\"SVN download support is deprecated\", UserWarning)\n        url = url.split('#', 1)[0]  # remove any fragment for svn's sake\n        creds = ''\n        if url.lower().startswith('svn:') and '@' in url:\n            scheme, netloc, path, p, q, f = urllib.parse.urlparse(url)\n            if not netloc and path.startswith('//') and '/' in path[2:]:\n                netloc, path = path[2:].split('/', 1)\n                auth, host = _splituser(netloc)\n                if auth:\n                    if ':' in auth:\n                        user, pw = auth.split(':', 1)\n                        creds = \" --username=%s --password=%s\" % (user, pw)\n                    else:\n                        creds = \" --username=\" + auth\n                    netloc = host\n                    parts = scheme, netloc, url, p, q, f\n                    url = urllib.parse.urlunparse(parts)\n        self.info(\"Doing subversion checkout from %s to %s\", url, filename)\n        os.system(\"svn checkout%s -q %s %s\" % (creds, url, filename))\n        return filename\n\n    @staticmethod\n    def _vcs_split_rev_from_url(url, pop_prefix=False):\n        scheme, netloc, path, query, frag = urllib.parse.urlsplit(url)\n\n        scheme = scheme.split('+', 1)[-1]\n\n        # Some fragment identification fails\n        path = path.split('#', 1)[0]\n\n        rev = None\n        if '@' in path:\n            path, rev = path.rsplit('@', 1)\n\n        # Also, discard fragment\n        url = urllib.parse.urlunsplit((scheme, netloc, path, query, ''))\n\n        return url, rev\n\n    def _download_git(self, url, filename):\n        filename = filename.split('#', 1)[0]\n        url, rev = self._vcs_split_rev_from_url(url, pop_prefix=True)\n\n        self.info(\"Doing git clone from %s to %s\", url, filename)\n        os.system(\"git clone --quiet %s %s\" % (url, filename))\n\n        if rev is not None:\n            self.info(\"Checking out %s\", rev)\n            os.system(\"git -C %s checkout --quiet %s\" % (\n                filename,\n                rev,\n            ))\n\n        return filename\n\n    def _download_hg(self, url, filename):\n        filename = filename.split('#', 1)[0]\n        url, rev = self._vcs_split_rev_from_url(url, pop_prefix=True)\n\n        self.info(\"Doing hg clone from %s to %s\", url, filename)\n        os.system(\"hg clone --quiet %s %s\" % (url, filename))\n\n        if rev is not None:\n            self.info(\"Updating to %s\", rev)\n            os.system(\"hg --cwd %s up -C -r %s -q\" % (\n                filename,\n                rev,\n            ))\n\n        return filename\n\n    def debug(self, msg, *args):\n        log.debug(msg, *args)\n\n    def info(self, msg, *args):\n        log.info(msg, *args)\n\n    def warn(self, msg, *args):\n        log.warn(msg, *args)\n\n\n# This pattern matches a character entity reference (a decimal numeric\n# references, a hexadecimal numeric reference, or a named reference).\nentity_sub = re.compile(r'&(#(\\d+|x[\\da-fA-F]+)|[\\w.:-]+);?').sub\n\n\ndef decode_entity(match):\n    what = match.group(0)\n    return unescape(what)\n\n\ndef htmldecode(text):\n    \"\"\"\n    Decode HTML entities in the given text.\n\n    >>> htmldecode(\n    ...     'https://../package_name-0.1.2.tar.gz'\n    ...     '?tokena=A&amp;tokenb=B\">package_name-0.1.2.tar.gz')\n    'https://../package_name-0.1.2.tar.gz?tokena=A&tokenb=B\">package_name-0.1.2.tar.gz'\n    \"\"\"\n    return entity_sub(decode_entity, text)\n\n\ndef socket_timeout(timeout=15):\n    def _socket_timeout(func):\n        def _socket_timeout(*args, **kwargs):\n            old_timeout = socket.getdefaulttimeout()\n            socket.setdefaulttimeout(timeout)\n            try:\n                return func(*args, **kwargs)\n            finally:\n                socket.setdefaulttimeout(old_timeout)\n\n        return _socket_timeout\n\n    return _socket_timeout\n\n\ndef _encode_auth(auth):\n    \"\"\"\n    A function compatible with Python 2.3-3.3 that will encode\n    auth from a URL suitable for an HTTP header.\n    >>> str(_encode_auth('username%3Apassword'))\n    'dXNlcm5hbWU6cGFzc3dvcmQ='\n\n    Long auth strings should not cause a newline to be inserted.\n    >>> long_auth = 'username:' + 'password'*10\n    >>> chr(10) in str(_encode_auth(long_auth))\n    False\n    \"\"\"\n    auth_s = urllib.parse.unquote(auth)\n    # convert to bytes\n    auth_bytes = auth_s.encode()\n    encoded_bytes = base64.b64encode(auth_bytes)\n    # convert back to a string\n    encoded = encoded_bytes.decode()\n    # strip the trailing carriage return\n    return encoded.replace('\\n', '')\n\n\nclass Credential:\n    \"\"\"\n    A username/password pair. Use like a namedtuple.\n    \"\"\"\n\n    def __init__(self, username, password):\n        self.username = username\n        self.password = password\n\n    def __iter__(self):\n        yield self.username\n        yield self.password\n\n    def __str__(self):\n        return '%(username)s:%(password)s' % vars(self)\n\n\nclass PyPIConfig(configparser.RawConfigParser):\n    def __init__(self):\n        \"\"\"\n        Load from ~/.pypirc\n        \"\"\"\n        defaults = dict.fromkeys(['username', 'password', 'repository'], '')\n        configparser.RawConfigParser.__init__(self, defaults)\n\n        rc = os.path.join(os.path.expanduser('~'), '.pypirc')\n        if os.path.exists(rc):\n            self.read(rc)\n\n    @property\n    def creds_by_repository(self):\n        sections_with_repositories = [\n            section for section in self.sections()\n            if self.get(section, 'repository').strip()\n        ]\n\n        return dict(map(self._get_repo_cred, sections_with_repositories))\n\n    def _get_repo_cred(self, section):\n        repo = self.get(section, 'repository').strip()\n        return repo, Credential(\n            self.get(section, 'username').strip(),\n            self.get(section, 'password').strip(),\n        )\n\n    def find_credential(self, url):\n        \"\"\"\n        If the URL indicated appears to be a repository defined in this\n        config, return the credential for that repository.\n        \"\"\"\n        for repository, cred in self.creds_by_repository.items():\n            if url.startswith(repository):\n                return cred\n\n\ndef open_with_auth(url, opener=urllib.request.urlopen):\n    \"\"\"Open a urllib2 request, handling HTTP authentication\"\"\"\n\n    parsed = urllib.parse.urlparse(url)\n    scheme, netloc, path, params, query, frag = parsed\n\n    # Double scheme does not raise on Mac OS X as revealed by a\n    # failing test. We would expect \"nonnumeric port\". Refs #20.\n    if netloc.endswith(':'):\n        raise http_client.InvalidURL(\"nonnumeric port: ''\")\n\n    if scheme in ('http', 'https'):\n        auth, address = _splituser(netloc)\n    else:\n        auth = None\n\n    if not auth:\n        cred = PyPIConfig().find_credential(url)\n        if cred:\n            auth = str(cred)\n            info = cred.username, url\n            log.info('Authenticating as %s for %s (from .pypirc)', *info)\n\n    if auth:\n        auth = \"Basic \" + _encode_auth(auth)\n        parts = scheme, address, path, params, query, frag\n        new_url = urllib.parse.urlunparse(parts)\n        request = urllib.request.Request(new_url)\n        request.add_header(\"Authorization\", auth)\n    else:\n        request = urllib.request.Request(url)\n\n    request.add_header('User-Agent', user_agent)\n    fp = opener(request)\n\n    if auth:\n        # Put authentication info back into request URL if same host,\n        # so that links found on the page will work\n        s2, h2, path2, param2, query2, frag2 = urllib.parse.urlparse(fp.url)\n        if s2 == scheme and h2 == address:\n            parts = s2, netloc, path2, param2, query2, frag2\n            fp.url = urllib.parse.urlunparse(parts)\n\n    return fp\n\n\n# copy of urllib.parse._splituser from Python 3.8\ndef _splituser(host):\n    \"\"\"splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'.\"\"\"\n    user, delim, host = host.rpartition('@')\n    return (user if delim else None), host\n\n\n# adding a timeout to avoid freezing package_index\nopen_with_auth = socket_timeout(_SOCKET_TIMEOUT)(open_with_auth)\n\n\ndef fix_sf_url(url):\n    return url  # backward compatibility\n\n\ndef local_open(url):\n    \"\"\"Read a local path, with special support for directories\"\"\"\n    scheme, server, path, param, query, frag = urllib.parse.urlparse(url)\n    filename = urllib.request.url2pathname(path)\n    if os.path.isfile(filename):\n        return urllib.request.urlopen(url)\n    elif path.endswith('/') and os.path.isdir(filename):\n        files = []\n        for f in os.listdir(filename):\n            filepath = os.path.join(filename, f)\n            if f == 'index.html':\n                with open(filepath, 'r') as fp:\n                    body = fp.read()\n                break\n            elif os.path.isdir(filepath):\n                f += '/'\n            files.append('<a href=\"{name}\">{name}</a>'.format(name=f))\n        else:\n            tmpl = (\n                \"<html><head><title>{url}</title>\"\n                \"</head><body>{files}</body></html>\")\n            body = tmpl.format(url=url, files='\\n'.join(files))\n        status, message = 200, \"OK\"\n    else:\n        status, message, body = 404, \"Path not found\", \"Not found\"\n\n    headers = {'content-type': 'text/html'}\n    body_stream = six.StringIO(body)\n    return urllib.error.HTTPError(url, status, message, headers, body_stream)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/py27compat.py",
    "content": "\"\"\"\nCompatibility Support for Python 2.7 and earlier\n\"\"\"\n\nimport sys\nimport platform\n\nfrom setuptools.extern import six\n\n\ndef get_all_headers(message, key):\n    \"\"\"\n    Given an HTTPMessage, return all headers matching a given key.\n    \"\"\"\n    return message.get_all(key)\n\n\nif six.PY2:\n    def get_all_headers(message, key):\n        return message.getheaders(key)\n\n\nlinux_py2_ascii = (\n    platform.system() == 'Linux' and\n    six.PY2\n)\n\nrmtree_safe = str if linux_py2_ascii else lambda x: x\n\"\"\"Workaround for http://bugs.python.org/issue24672\"\"\"\n\n\ntry:\n    from ._imp import find_module, PY_COMPILED, PY_FROZEN, PY_SOURCE\n    from ._imp import get_frozen_object, get_module\nexcept ImportError:\n    import imp\n    from imp import PY_COMPILED, PY_FROZEN, PY_SOURCE  # noqa\n\n    def find_module(module, paths=None):\n        \"\"\"Just like 'imp.find_module()', but with package support\"\"\"\n        parts = module.split('.')\n        while parts:\n            part = parts.pop(0)\n            f, path, (suffix, mode, kind) = info = imp.find_module(part, paths)\n\n            if kind == imp.PKG_DIRECTORY:\n                parts = parts or ['__init__']\n                paths = [path]\n\n            elif parts:\n                raise ImportError(\"Can't find %r in %s\" % (parts, module))\n\n        return info\n\n    def get_frozen_object(module, paths):\n        return imp.get_frozen_object(module)\n\n    def get_module(module, paths, info):\n        imp.load_module(module, *info)\n        return sys.modules[module]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/py31compat.py",
    "content": "__all__ = []\n\n__metaclass__ = type\n\n\ntry:\n    # Python >=3.2\n    from tempfile import TemporaryDirectory\nexcept ImportError:\n    import shutil\n    import tempfile\n\n    class TemporaryDirectory:\n        \"\"\"\n        Very simple temporary directory context manager.\n        Will try to delete afterward, but will also ignore OS and similar\n        errors on deletion.\n        \"\"\"\n\n        def __init__(self, **kwargs):\n            self.name = None  # Handle mkdtemp raising an exception\n            self.name = tempfile.mkdtemp(**kwargs)\n\n        def __enter__(self):\n            return self.name\n\n        def __exit__(self, exctype, excvalue, exctrace):\n            try:\n                shutil.rmtree(self.name, True)\n            except OSError:  # removal errors are not the only possible\n                pass\n            self.name = None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/py33compat.py",
    "content": "import dis\nimport array\nimport collections\n\ntry:\n    import html\nexcept ImportError:\n    html = None\n\nfrom setuptools.extern import six\nfrom setuptools.extern.six.moves import html_parser\n\n__metaclass__ = type\n\nOpArg = collections.namedtuple('OpArg', 'opcode arg')\n\n\nclass Bytecode_compat:\n    def __init__(self, code):\n        self.code = code\n\n    def __iter__(self):\n        \"\"\"Yield '(op,arg)' pair for each operation in code object 'code'\"\"\"\n\n        bytes = array.array('b', self.code.co_code)\n        eof = len(self.code.co_code)\n\n        ptr = 0\n        extended_arg = 0\n\n        while ptr < eof:\n\n            op = bytes[ptr]\n\n            if op >= dis.HAVE_ARGUMENT:\n\n                arg = bytes[ptr + 1] + bytes[ptr + 2] * 256 + extended_arg\n                ptr += 3\n\n                if op == dis.EXTENDED_ARG:\n                    long_type = six.integer_types[-1]\n                    extended_arg = arg * long_type(65536)\n                    continue\n\n            else:\n                arg = None\n                ptr += 1\n\n            yield OpArg(op, arg)\n\n\nBytecode = getattr(dis, 'Bytecode', Bytecode_compat)\n\n\nunescape = getattr(html, 'unescape', None)\nif unescape is None:\n    # HTMLParser.unescape is deprecated since Python 3.4, and will be removed\n    # from 3.9.\n    unescape = html_parser.HTMLParser().unescape\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/py34compat.py",
    "content": "import importlib\n\ntry:\n    import importlib.util\nexcept ImportError:\n    pass\n\n\ntry:\n    module_from_spec = importlib.util.module_from_spec\nexcept AttributeError:\n    def module_from_spec(spec):\n        return spec.loader.load_module(spec.name)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/sandbox.py",
    "content": "import os\nimport sys\nimport tempfile\nimport operator\nimport functools\nimport itertools\nimport re\nimport contextlib\nimport pickle\nimport textwrap\n\nfrom setuptools.extern import six\nfrom setuptools.extern.six.moves import builtins, map\n\nimport pkg_resources.py31compat\n\nif sys.platform.startswith('java'):\n    import org.python.modules.posix.PosixModule as _os\nelse:\n    _os = sys.modules[os.name]\ntry:\n    _file = file\nexcept NameError:\n    _file = None\n_open = open\nfrom distutils.errors import DistutilsError\nfrom pkg_resources import working_set\n\n\n__all__ = [\n    \"AbstractSandbox\", \"DirectorySandbox\", \"SandboxViolation\", \"run_setup\",\n]\n\n\ndef _execfile(filename, globals, locals=None):\n    \"\"\"\n    Python 3 implementation of execfile.\n    \"\"\"\n    mode = 'rb'\n    with open(filename, mode) as stream:\n        script = stream.read()\n    if locals is None:\n        locals = globals\n    code = compile(script, filename, 'exec')\n    exec(code, globals, locals)\n\n\n@contextlib.contextmanager\ndef save_argv(repl=None):\n    saved = sys.argv[:]\n    if repl is not None:\n        sys.argv[:] = repl\n    try:\n        yield saved\n    finally:\n        sys.argv[:] = saved\n\n\n@contextlib.contextmanager\ndef save_path():\n    saved = sys.path[:]\n    try:\n        yield saved\n    finally:\n        sys.path[:] = saved\n\n\n@contextlib.contextmanager\ndef override_temp(replacement):\n    \"\"\"\n    Monkey-patch tempfile.tempdir with replacement, ensuring it exists\n    \"\"\"\n    pkg_resources.py31compat.makedirs(replacement, exist_ok=True)\n\n    saved = tempfile.tempdir\n\n    tempfile.tempdir = replacement\n\n    try:\n        yield\n    finally:\n        tempfile.tempdir = saved\n\n\n@contextlib.contextmanager\ndef pushd(target):\n    saved = os.getcwd()\n    os.chdir(target)\n    try:\n        yield saved\n    finally:\n        os.chdir(saved)\n\n\nclass UnpickleableException(Exception):\n    \"\"\"\n    An exception representing another Exception that could not be pickled.\n    \"\"\"\n\n    @staticmethod\n    def dump(type, exc):\n        \"\"\"\n        Always return a dumped (pickled) type and exc. If exc can't be pickled,\n        wrap it in UnpickleableException first.\n        \"\"\"\n        try:\n            return pickle.dumps(type), pickle.dumps(exc)\n        except Exception:\n            # get UnpickleableException inside the sandbox\n            from setuptools.sandbox import UnpickleableException as cls\n            return cls.dump(cls, cls(repr(exc)))\n\n\nclass ExceptionSaver:\n    \"\"\"\n    A Context Manager that will save an exception, serialized, and restore it\n    later.\n    \"\"\"\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, type, exc, tb):\n        if not exc:\n            return\n\n        # dump the exception\n        self._saved = UnpickleableException.dump(type, exc)\n        self._tb = tb\n\n        # suppress the exception\n        return True\n\n    def resume(self):\n        \"restore and re-raise any exception\"\n\n        if '_saved' not in vars(self):\n            return\n\n        type, exc = map(pickle.loads, self._saved)\n        six.reraise(type, exc, self._tb)\n\n\n@contextlib.contextmanager\ndef save_modules():\n    \"\"\"\n    Context in which imported modules are saved.\n\n    Translates exceptions internal to the context into the equivalent exception\n    outside the context.\n    \"\"\"\n    saved = sys.modules.copy()\n    with ExceptionSaver() as saved_exc:\n        yield saved\n\n    sys.modules.update(saved)\n    # remove any modules imported since\n    del_modules = (\n        mod_name for mod_name in sys.modules\n        if mod_name not in saved\n        # exclude any encodings modules. See #285\n        and not mod_name.startswith('encodings.')\n    )\n    _clear_modules(del_modules)\n\n    saved_exc.resume()\n\n\ndef _clear_modules(module_names):\n    for mod_name in list(module_names):\n        del sys.modules[mod_name]\n\n\n@contextlib.contextmanager\ndef save_pkg_resources_state():\n    saved = pkg_resources.__getstate__()\n    try:\n        yield saved\n    finally:\n        pkg_resources.__setstate__(saved)\n\n\n@contextlib.contextmanager\ndef setup_context(setup_dir):\n    temp_dir = os.path.join(setup_dir, 'temp')\n    with save_pkg_resources_state():\n        with save_modules():\n            hide_setuptools()\n            with save_path():\n                with save_argv():\n                    with override_temp(temp_dir):\n                        with pushd(setup_dir):\n                            # ensure setuptools commands are available\n                            __import__('setuptools')\n                            yield\n\n\ndef _needs_hiding(mod_name):\n    \"\"\"\n    >>> _needs_hiding('setuptools')\n    True\n    >>> _needs_hiding('pkg_resources')\n    True\n    >>> _needs_hiding('setuptools_plugin')\n    False\n    >>> _needs_hiding('setuptools.__init__')\n    True\n    >>> _needs_hiding('distutils')\n    True\n    >>> _needs_hiding('os')\n    False\n    >>> _needs_hiding('Cython')\n    True\n    \"\"\"\n    pattern = re.compile(r'(setuptools|pkg_resources|distutils|Cython)(\\.|$)')\n    return bool(pattern.match(mod_name))\n\n\ndef hide_setuptools():\n    \"\"\"\n    Remove references to setuptools' modules from sys.modules to allow the\n    invocation to import the most appropriate setuptools. This technique is\n    necessary to avoid issues such as #315 where setuptools upgrading itself\n    would fail to find a function declared in the metadata.\n    \"\"\"\n    modules = filter(_needs_hiding, sys.modules)\n    _clear_modules(modules)\n\n\ndef run_setup(setup_script, args):\n    \"\"\"Run a distutils setup script, sandboxed in its directory\"\"\"\n    setup_dir = os.path.abspath(os.path.dirname(setup_script))\n    with setup_context(setup_dir):\n        try:\n            sys.argv[:] = [setup_script] + list(args)\n            sys.path.insert(0, setup_dir)\n            # reset to include setup dir, w/clean callback list\n            working_set.__init__()\n            working_set.callbacks.append(lambda dist: dist.activate())\n\n            # __file__ should be a byte string on Python 2 (#712)\n            dunder_file = (\n                setup_script\n                if isinstance(setup_script, str) else\n                setup_script.encode(sys.getfilesystemencoding())\n            )\n\n            with DirectorySandbox(setup_dir):\n                ns = dict(__file__=dunder_file, __name__='__main__')\n                _execfile(setup_script, ns)\n        except SystemExit as v:\n            if v.args and v.args[0]:\n                raise\n            # Normal exit, just return\n\n\nclass AbstractSandbox:\n    \"\"\"Wrap 'os' module and 'open()' builtin for virtualizing setup scripts\"\"\"\n\n    _active = False\n\n    def __init__(self):\n        self._attrs = [\n            name for name in dir(_os)\n            if not name.startswith('_') and hasattr(self, name)\n        ]\n\n    def _copy(self, source):\n        for name in self._attrs:\n            setattr(os, name, getattr(source, name))\n\n    def __enter__(self):\n        self._copy(self)\n        if _file:\n            builtins.file = self._file\n        builtins.open = self._open\n        self._active = True\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        self._active = False\n        if _file:\n            builtins.file = _file\n        builtins.open = _open\n        self._copy(_os)\n\n    def run(self, func):\n        \"\"\"Run 'func' under os sandboxing\"\"\"\n        with self:\n            return func()\n\n    def _mk_dual_path_wrapper(name):\n        original = getattr(_os, name)\n\n        def wrap(self, src, dst, *args, **kw):\n            if self._active:\n                src, dst = self._remap_pair(name, src, dst, *args, **kw)\n            return original(src, dst, *args, **kw)\n\n        return wrap\n\n    for name in [\"rename\", \"link\", \"symlink\"]:\n        if hasattr(_os, name):\n            locals()[name] = _mk_dual_path_wrapper(name)\n\n    def _mk_single_path_wrapper(name, original=None):\n        original = original or getattr(_os, name)\n\n        def wrap(self, path, *args, **kw):\n            if self._active:\n                path = self._remap_input(name, path, *args, **kw)\n            return original(path, *args, **kw)\n\n        return wrap\n\n    if _file:\n        _file = _mk_single_path_wrapper('file', _file)\n    _open = _mk_single_path_wrapper('open', _open)\n    for name in [\n        \"stat\", \"listdir\", \"chdir\", \"open\", \"chmod\", \"chown\", \"mkdir\",\n        \"remove\", \"unlink\", \"rmdir\", \"utime\", \"lchown\", \"chroot\", \"lstat\",\n        \"startfile\", \"mkfifo\", \"mknod\", \"pathconf\", \"access\"\n    ]:\n        if hasattr(_os, name):\n            locals()[name] = _mk_single_path_wrapper(name)\n\n    def _mk_single_with_return(name):\n        original = getattr(_os, name)\n\n        def wrap(self, path, *args, **kw):\n            if self._active:\n                path = self._remap_input(name, path, *args, **kw)\n                return self._remap_output(name, original(path, *args, **kw))\n            return original(path, *args, **kw)\n\n        return wrap\n\n    for name in ['readlink', 'tempnam']:\n        if hasattr(_os, name):\n            locals()[name] = _mk_single_with_return(name)\n\n    def _mk_query(name):\n        original = getattr(_os, name)\n\n        def wrap(self, *args, **kw):\n            retval = original(*args, **kw)\n            if self._active:\n                return self._remap_output(name, retval)\n            return retval\n\n        return wrap\n\n    for name in ['getcwd', 'tmpnam']:\n        if hasattr(_os, name):\n            locals()[name] = _mk_query(name)\n\n    def _validate_path(self, path):\n        \"\"\"Called to remap or validate any path, whether input or output\"\"\"\n        return path\n\n    def _remap_input(self, operation, path, *args, **kw):\n        \"\"\"Called for path inputs\"\"\"\n        return self._validate_path(path)\n\n    def _remap_output(self, operation, path):\n        \"\"\"Called for path outputs\"\"\"\n        return self._validate_path(path)\n\n    def _remap_pair(self, operation, src, dst, *args, **kw):\n        \"\"\"Called for path pairs like rename, link, and symlink operations\"\"\"\n        return (\n            self._remap_input(operation + '-from', src, *args, **kw),\n            self._remap_input(operation + '-to', dst, *args, **kw)\n        )\n\n\nif hasattr(os, 'devnull'):\n    _EXCEPTIONS = [os.devnull,]\nelse:\n    _EXCEPTIONS = []\n\n\nclass DirectorySandbox(AbstractSandbox):\n    \"\"\"Restrict operations to a single subdirectory - pseudo-chroot\"\"\"\n\n    write_ops = dict.fromkeys([\n        \"open\", \"chmod\", \"chown\", \"mkdir\", \"remove\", \"unlink\", \"rmdir\",\n        \"utime\", \"lchown\", \"chroot\", \"mkfifo\", \"mknod\", \"tempnam\",\n    ])\n\n    _exception_patterns = [\n        # Allow lib2to3 to attempt to save a pickled grammar object (#121)\n        r'.*lib2to3.*\\.pickle$',\n    ]\n    \"exempt writing to paths that match the pattern\"\n\n    def __init__(self, sandbox, exceptions=_EXCEPTIONS):\n        self._sandbox = os.path.normcase(os.path.realpath(sandbox))\n        self._prefix = os.path.join(self._sandbox, '')\n        self._exceptions = [\n            os.path.normcase(os.path.realpath(path))\n            for path in exceptions\n        ]\n        AbstractSandbox.__init__(self)\n\n    def _violation(self, operation, *args, **kw):\n        from setuptools.sandbox import SandboxViolation\n        raise SandboxViolation(operation, args, kw)\n\n    if _file:\n\n        def _file(self, path, mode='r', *args, **kw):\n            if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path):\n                self._violation(\"file\", path, mode, *args, **kw)\n            return _file(path, mode, *args, **kw)\n\n    def _open(self, path, mode='r', *args, **kw):\n        if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path):\n            self._violation(\"open\", path, mode, *args, **kw)\n        return _open(path, mode, *args, **kw)\n\n    def tmpnam(self):\n        self._violation(\"tmpnam\")\n\n    def _ok(self, path):\n        active = self._active\n        try:\n            self._active = False\n            realpath = os.path.normcase(os.path.realpath(path))\n            return (\n                self._exempted(realpath)\n                or realpath == self._sandbox\n                or realpath.startswith(self._prefix)\n            )\n        finally:\n            self._active = active\n\n    def _exempted(self, filepath):\n        start_matches = (\n            filepath.startswith(exception)\n            for exception in self._exceptions\n        )\n        pattern_matches = (\n            re.match(pattern, filepath)\n            for pattern in self._exception_patterns\n        )\n        candidates = itertools.chain(start_matches, pattern_matches)\n        return any(candidates)\n\n    def _remap_input(self, operation, path, *args, **kw):\n        \"\"\"Called for path inputs\"\"\"\n        if operation in self.write_ops and not self._ok(path):\n            self._violation(operation, os.path.realpath(path), *args, **kw)\n        return path\n\n    def _remap_pair(self, operation, src, dst, *args, **kw):\n        \"\"\"Called for path pairs like rename, link, and symlink operations\"\"\"\n        if not self._ok(src) or not self._ok(dst):\n            self._violation(operation, src, dst, *args, **kw)\n        return (src, dst)\n\n    def open(self, file, flags, mode=0o777, *args, **kw):\n        \"\"\"Called for low-level os.open()\"\"\"\n        if flags & WRITE_FLAGS and not self._ok(file):\n            self._violation(\"os.open\", file, flags, mode, *args, **kw)\n        return _os.open(file, flags, mode, *args, **kw)\n\n\nWRITE_FLAGS = functools.reduce(\n    operator.or_, [getattr(_os, a, 0) for a in\n        \"O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY\".split()]\n)\n\n\nclass SandboxViolation(DistutilsError):\n    \"\"\"A setup script attempted to modify the filesystem outside the sandbox\"\"\"\n\n    tmpl = textwrap.dedent(\"\"\"\n        SandboxViolation: {cmd}{args!r} {kwargs}\n\n        The package setup script has attempted to modify files on your system\n        that are not within the EasyInstall build area, and has been aborted.\n\n        This package cannot be safely installed by EasyInstall, and may not\n        support alternate installation locations even if you run its setup\n        script by hand.  Please inform the package's author and the EasyInstall\n        maintainers to find out if a fix or workaround is available.\n        \"\"\").lstrip()\n\n    def __str__(self):\n        cmd, args, kwargs = self.args\n        return self.tmpl.format(**locals())\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/script (dev).tmpl",
    "content": "# EASY-INSTALL-DEV-SCRIPT: %(spec)r,%(script_name)r\n__requires__ = %(spec)r\n__import__('pkg_resources').require(%(spec)r)\n__file__ = %(dev_path)r\nwith open(__file__) as f:\n    exec(compile(f.read(), __file__, 'exec'))\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/script.tmpl",
    "content": "# EASY-INSTALL-SCRIPT: %(spec)r,%(script_name)r\n__requires__ = %(spec)r\n__import__('pkg_resources').run_script(%(spec)r, %(script_name)r)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/site-patch.py",
    "content": "def __boot():\n    import sys\n    import os\n    PYTHONPATH = os.environ.get('PYTHONPATH')\n    if PYTHONPATH is None or (sys.platform == 'win32' and not PYTHONPATH):\n        PYTHONPATH = []\n    else:\n        PYTHONPATH = PYTHONPATH.split(os.pathsep)\n\n    pic = getattr(sys, 'path_importer_cache', {})\n    stdpath = sys.path[len(PYTHONPATH):]\n    mydir = os.path.dirname(__file__)\n\n    for item in stdpath:\n        if item == mydir or not item:\n            continue  # skip if current dir. on Windows, or my own directory\n        importer = pic.get(item)\n        if importer is not None:\n            loader = importer.find_module('site')\n            if loader is not None:\n                # This should actually reload the current module\n                loader.load_module('site')\n                break\n        else:\n            try:\n                import imp  # Avoid import loop in Python 3\n                stream, path, descr = imp.find_module('site', [item])\n            except ImportError:\n                continue\n            if stream is None:\n                continue\n            try:\n                # This should actually reload the current module\n                imp.load_module('site', stream, path, descr)\n            finally:\n                stream.close()\n            break\n    else:\n        raise ImportError(\"Couldn't find the real 'site' module\")\n\n    known_paths = dict([(makepath(item)[1], 1) for item in sys.path])  # 2.2 comp\n\n    oldpos = getattr(sys, '__egginsert', 0)  # save old insertion position\n    sys.__egginsert = 0  # and reset the current one\n\n    for item in PYTHONPATH:\n        addsitedir(item)\n\n    sys.__egginsert += oldpos  # restore effective old position\n\n    d, nd = makepath(stdpath[0])\n    insert_at = None\n    new_path = []\n\n    for item in sys.path:\n        p, np = makepath(item)\n\n        if np == nd and insert_at is None:\n            # We've hit the first 'system' path entry, so added entries go here\n            insert_at = len(new_path)\n\n        if np in known_paths or insert_at is None:\n            new_path.append(item)\n        else:\n            # new path after the insert point, back-insert it\n            new_path.insert(insert_at, item)\n            insert_at += 1\n\n    sys.path[:] = new_path\n\n\nif __name__ == 'site':\n    __boot()\n    del __boot\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/ssl_support.py",
    "content": "import os\nimport socket\nimport atexit\nimport re\nimport functools\n\nfrom setuptools.extern.six.moves import urllib, http_client, map, filter\n\nfrom pkg_resources import ResolutionError, ExtractionError\n\ntry:\n    import ssl\nexcept ImportError:\n    ssl = None\n\n__all__ = [\n    'VerifyingHTTPSHandler', 'find_ca_bundle', 'is_available', 'cert_paths',\n    'opener_for'\n]\n\ncert_paths = \"\"\"\n/etc/pki/tls/certs/ca-bundle.crt\n/etc/ssl/certs/ca-certificates.crt\n/usr/share/ssl/certs/ca-bundle.crt\n/usr/local/share/certs/ca-root.crt\n/etc/ssl/cert.pem\n/System/Library/OpenSSL/certs/cert.pem\n/usr/local/share/certs/ca-root-nss.crt\n/etc/ssl/ca-bundle.pem\n\"\"\".strip().split()\n\ntry:\n    HTTPSHandler = urllib.request.HTTPSHandler\n    HTTPSConnection = http_client.HTTPSConnection\nexcept AttributeError:\n    HTTPSHandler = HTTPSConnection = object\n\nis_available = ssl is not None and object not in (HTTPSHandler, HTTPSConnection)\n\n\ntry:\n    from ssl import CertificateError, match_hostname\nexcept ImportError:\n    try:\n        from backports.ssl_match_hostname import CertificateError\n        from backports.ssl_match_hostname import match_hostname\n    except ImportError:\n        CertificateError = None\n        match_hostname = None\n\nif not CertificateError:\n\n    class CertificateError(ValueError):\n        pass\n\n\nif not match_hostname:\n\n    def _dnsname_match(dn, hostname, max_wildcards=1):\n        \"\"\"Matching according to RFC 6125, section 6.4.3\n\n        https://tools.ietf.org/html/rfc6125#section-6.4.3\n        \"\"\"\n        pats = []\n        if not dn:\n            return False\n\n        # Ported from python3-syntax:\n        # leftmost, *remainder = dn.split(r'.')\n        parts = dn.split(r'.')\n        leftmost = parts[0]\n        remainder = parts[1:]\n\n        wildcards = leftmost.count('*')\n        if wildcards > max_wildcards:\n            # Issue #17980: avoid denials of service by refusing more\n            # than one wildcard per fragment.  A survey of established\n            # policy among SSL implementations showed it to be a\n            # reasonable choice.\n            raise CertificateError(\n                \"too many wildcards in certificate DNS name: \" + repr(dn))\n\n        # speed up common case w/o wildcards\n        if not wildcards:\n            return dn.lower() == hostname.lower()\n\n        # RFC 6125, section 6.4.3, subitem 1.\n        # The client SHOULD NOT attempt to match a presented identifier in which\n        # the wildcard character comprises a label other than the left-most label.\n        if leftmost == '*':\n            # When '*' is a fragment by itself, it matches a non-empty dotless\n            # fragment.\n            pats.append('[^.]+')\n        elif leftmost.startswith('xn--') or hostname.startswith('xn--'):\n            # RFC 6125, section 6.4.3, subitem 3.\n            # The client SHOULD NOT attempt to match a presented identifier\n            # where the wildcard character is embedded within an A-label or\n            # U-label of an internationalized domain name.\n            pats.append(re.escape(leftmost))\n        else:\n            # Otherwise, '*' matches any dotless string, e.g. www*\n            pats.append(re.escape(leftmost).replace(r'\\*', '[^.]*'))\n\n        # add the remaining fragments, ignore any wildcards\n        for frag in remainder:\n            pats.append(re.escape(frag))\n\n        pat = re.compile(r'\\A' + r'\\.'.join(pats) + r'\\Z', re.IGNORECASE)\n        return pat.match(hostname)\n\n    def match_hostname(cert, hostname):\n        \"\"\"Verify that *cert* (in decoded format as returned by\n        SSLSocket.getpeercert()) matches the *hostname*.  RFC 2818 and RFC 6125\n        rules are followed, but IP addresses are not accepted for *hostname*.\n\n        CertificateError is raised on failure. On success, the function\n        returns nothing.\n        \"\"\"\n        if not cert:\n            raise ValueError(\"empty or no certificate\")\n        dnsnames = []\n        san = cert.get('subjectAltName', ())\n        for key, value in san:\n            if key == 'DNS':\n                if _dnsname_match(value, hostname):\n                    return\n                dnsnames.append(value)\n        if not dnsnames:\n            # The subject is only checked when there is no dNSName entry\n            # in subjectAltName\n            for sub in cert.get('subject', ()):\n                for key, value in sub:\n                    # XXX according to RFC 2818, the most specific Common Name\n                    # must be used.\n                    if key == 'commonName':\n                        if _dnsname_match(value, hostname):\n                            return\n                        dnsnames.append(value)\n        if len(dnsnames) > 1:\n            raise CertificateError(\"hostname %r \"\n                \"doesn't match either of %s\"\n                % (hostname, ', '.join(map(repr, dnsnames))))\n        elif len(dnsnames) == 1:\n            raise CertificateError(\"hostname %r \"\n                \"doesn't match %r\"\n                % (hostname, dnsnames[0]))\n        else:\n            raise CertificateError(\"no appropriate commonName or \"\n                \"subjectAltName fields were found\")\n\n\nclass VerifyingHTTPSHandler(HTTPSHandler):\n    \"\"\"Simple verifying handler: no auth, subclasses, timeouts, etc.\"\"\"\n\n    def __init__(self, ca_bundle):\n        self.ca_bundle = ca_bundle\n        HTTPSHandler.__init__(self)\n\n    def https_open(self, req):\n        return self.do_open(\n            lambda host, **kw: VerifyingHTTPSConn(host, self.ca_bundle, **kw), req\n        )\n\n\nclass VerifyingHTTPSConn(HTTPSConnection):\n    \"\"\"Simple verifying connection: no auth, subclasses, timeouts, etc.\"\"\"\n\n    def __init__(self, host, ca_bundle, **kw):\n        HTTPSConnection.__init__(self, host, **kw)\n        self.ca_bundle = ca_bundle\n\n    def connect(self):\n        sock = socket.create_connection(\n            (self.host, self.port), getattr(self, 'source_address', None)\n        )\n\n        # Handle the socket if a (proxy) tunnel is present\n        if hasattr(self, '_tunnel') and getattr(self, '_tunnel_host', None):\n            self.sock = sock\n            self._tunnel()\n            # http://bugs.python.org/issue7776: Python>=3.4.1 and >=2.7.7\n            # change self.host to mean the proxy server host when tunneling is\n            # being used. Adapt, since we are interested in the destination\n            # host for the match_hostname() comparison.\n            actual_host = self._tunnel_host\n        else:\n            actual_host = self.host\n\n        if hasattr(ssl, 'create_default_context'):\n            ctx = ssl.create_default_context(cafile=self.ca_bundle)\n            self.sock = ctx.wrap_socket(sock, server_hostname=actual_host)\n        else:\n            # This is for python < 2.7.9 and < 3.4?\n            self.sock = ssl.wrap_socket(\n                sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.ca_bundle\n            )\n        try:\n            match_hostname(self.sock.getpeercert(), actual_host)\n        except CertificateError:\n            self.sock.shutdown(socket.SHUT_RDWR)\n            self.sock.close()\n            raise\n\n\ndef opener_for(ca_bundle=None):\n    \"\"\"Get a urlopen() replacement that uses ca_bundle for verification\"\"\"\n    return urllib.request.build_opener(\n        VerifyingHTTPSHandler(ca_bundle or find_ca_bundle())\n    ).open\n\n\n# from jaraco.functools\ndef once(func):\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs):\n        if not hasattr(func, 'always_returns'):\n            func.always_returns = func(*args, **kwargs)\n        return func.always_returns\n    return wrapper\n\n\n@once\ndef get_win_certfile():\n    try:\n        import wincertstore\n    except ImportError:\n        return None\n\n    class CertFile(wincertstore.CertFile):\n        def __init__(self):\n            super(CertFile, self).__init__()\n            atexit.register(self.close)\n\n        def close(self):\n            try:\n                super(CertFile, self).close()\n            except OSError:\n                pass\n\n    _wincerts = CertFile()\n    _wincerts.addstore('CA')\n    _wincerts.addstore('ROOT')\n    return _wincerts.name\n\n\ndef find_ca_bundle():\n    \"\"\"Return an existing CA bundle path, or None\"\"\"\n    extant_cert_paths = filter(os.path.isfile, cert_paths)\n    return (\n        get_win_certfile()\n        or next(extant_cert_paths, None)\n        or _certifi_where()\n    )\n\n\ndef _certifi_where():\n    try:\n        return __import__('certifi').where()\n    except (ImportError, ResolutionError, ExtractionError):\n        pass\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/unicode_utils.py",
    "content": "import unicodedata\nimport sys\n\nfrom setuptools.extern import six\n\n\n# HFS Plus uses decomposed UTF-8\ndef decompose(path):\n    if isinstance(path, six.text_type):\n        return unicodedata.normalize('NFD', path)\n    try:\n        path = path.decode('utf-8')\n        path = unicodedata.normalize('NFD', path)\n        path = path.encode('utf-8')\n    except UnicodeError:\n        pass  # Not UTF-8\n    return path\n\n\ndef filesys_decode(path):\n    \"\"\"\n    Ensure that the given path is decoded,\n    NONE when no expected encoding works\n    \"\"\"\n\n    if isinstance(path, six.text_type):\n        return path\n\n    fs_enc = sys.getfilesystemencoding() or 'utf-8'\n    candidates = fs_enc, 'utf-8'\n\n    for enc in candidates:\n        try:\n            return path.decode(enc)\n        except UnicodeDecodeError:\n            continue\n\n\ndef try_encode(string, enc):\n    \"turn unicode encoding into a functional routine\"\n    try:\n        return string.encode(enc)\n    except UnicodeEncodeError:\n        return None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/version.py",
    "content": "import pkg_resources\n\ntry:\n    __version__ = pkg_resources.get_distribution('setuptools').version\nexcept Exception:\n    __version__ = 'unknown'\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/wheel.py",
    "content": "\"\"\"Wheels support.\"\"\"\n\nfrom distutils.util import get_platform\nfrom distutils import log\nimport email\nimport itertools\nimport os\nimport posixpath\nimport re\nimport zipfile\n\nimport pkg_resources\nimport setuptools\nfrom pkg_resources import parse_version\nfrom setuptools.extern.packaging.tags import sys_tags\nfrom setuptools.extern.packaging.utils import canonicalize_name\nfrom setuptools.extern.six import PY3\nfrom setuptools.command.egg_info import write_requirements\n\n\n__metaclass__ = type\n\n\nWHEEL_NAME = re.compile(\n    r\"\"\"^(?P<project_name>.+?)-(?P<version>\\d.*?)\n    ((-(?P<build>\\d.*?))?-(?P<py_version>.+?)-(?P<abi>.+?)-(?P<platform>.+?)\n    )\\.whl$\"\"\",\n    re.VERBOSE).match\n\nNAMESPACE_PACKAGE_INIT = '''\\\ntry:\n    __import__('pkg_resources').declare_namespace(__name__)\nexcept ImportError:\n    __path__ = __import__('pkgutil').extend_path(__path__, __name__)\n'''\n\n\ndef unpack(src_dir, dst_dir):\n    '''Move everything under `src_dir` to `dst_dir`, and delete the former.'''\n    for dirpath, dirnames, filenames in os.walk(src_dir):\n        subdir = os.path.relpath(dirpath, src_dir)\n        for f in filenames:\n            src = os.path.join(dirpath, f)\n            dst = os.path.join(dst_dir, subdir, f)\n            os.renames(src, dst)\n        for n, d in reversed(list(enumerate(dirnames))):\n            src = os.path.join(dirpath, d)\n            dst = os.path.join(dst_dir, subdir, d)\n            if not os.path.exists(dst):\n                # Directory does not exist in destination,\n                # rename it and prune it from os.walk list.\n                os.renames(src, dst)\n                del dirnames[n]\n    # Cleanup.\n    for dirpath, dirnames, filenames in os.walk(src_dir, topdown=True):\n        assert not filenames\n        os.rmdir(dirpath)\n\n\nclass Wheel:\n\n    def __init__(self, filename):\n        match = WHEEL_NAME(os.path.basename(filename))\n        if match is None:\n            raise ValueError('invalid wheel name: %r' % filename)\n        self.filename = filename\n        for k, v in match.groupdict().items():\n            setattr(self, k, v)\n\n    def tags(self):\n        '''List tags (py_version, abi, platform) supported by this wheel.'''\n        return itertools.product(\n            self.py_version.split('.'),\n            self.abi.split('.'),\n            self.platform.split('.'),\n        )\n\n    def is_compatible(self):\n        '''Is the wheel is compatible with the current platform?'''\n        supported_tags = set((t.interpreter, t.abi, t.platform) for t in sys_tags())\n        return next((True for t in self.tags() if t in supported_tags), False)\n\n    def egg_name(self):\n        return pkg_resources.Distribution(\n            project_name=self.project_name, version=self.version,\n            platform=(None if self.platform == 'any' else get_platform()),\n        ).egg_name() + '.egg'\n\n    def get_dist_info(self, zf):\n        # find the correct name of the .dist-info dir in the wheel file\n        for member in zf.namelist():\n            dirname = posixpath.dirname(member)\n            if (dirname.endswith('.dist-info') and\n                    canonicalize_name(dirname).startswith(\n                        canonicalize_name(self.project_name))):\n                return dirname\n        raise ValueError(\"unsupported wheel format. .dist-info not found\")\n\n    def install_as_egg(self, destination_eggdir):\n        '''Install wheel as an egg directory.'''\n        with zipfile.ZipFile(self.filename) as zf:\n            self._install_as_egg(destination_eggdir, zf)\n\n    def _install_as_egg(self, destination_eggdir, zf):\n        dist_basename = '%s-%s' % (self.project_name, self.version)\n        dist_info = self.get_dist_info(zf)\n        dist_data = '%s.data' % dist_basename\n        egg_info = os.path.join(destination_eggdir, 'EGG-INFO')\n\n        self._convert_metadata(zf, destination_eggdir, dist_info, egg_info)\n        self._move_data_entries(destination_eggdir, dist_data)\n        self._fix_namespace_packages(egg_info, destination_eggdir)\n\n    @staticmethod\n    def _convert_metadata(zf, destination_eggdir, dist_info, egg_info):\n        def get_metadata(name):\n            with zf.open(posixpath.join(dist_info, name)) as fp:\n                value = fp.read().decode('utf-8') if PY3 else fp.read()\n                return email.parser.Parser().parsestr(value)\n\n        wheel_metadata = get_metadata('WHEEL')\n        # Check wheel format version is supported.\n        wheel_version = parse_version(wheel_metadata.get('Wheel-Version'))\n        wheel_v1 = (\n            parse_version('1.0') <= wheel_version < parse_version('2.0dev0')\n        )\n        if not wheel_v1:\n            raise ValueError(\n                'unsupported wheel format version: %s' % wheel_version)\n        # Extract to target directory.\n        os.mkdir(destination_eggdir)\n        zf.extractall(destination_eggdir)\n        # Convert metadata.\n        dist_info = os.path.join(destination_eggdir, dist_info)\n        dist = pkg_resources.Distribution.from_location(\n            destination_eggdir, dist_info,\n            metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info),\n        )\n\n        # Note: Evaluate and strip markers now,\n        # as it's difficult to convert back from the syntax:\n        # foobar; \"linux\" in sys_platform and extra == 'test'\n        def raw_req(req):\n            req.marker = None\n            return str(req)\n        install_requires = list(sorted(map(raw_req, dist.requires())))\n        extras_require = {\n            extra: sorted(\n                req\n                for req in map(raw_req, dist.requires((extra,)))\n                if req not in install_requires\n            )\n            for extra in dist.extras\n        }\n        os.rename(dist_info, egg_info)\n        os.rename(\n            os.path.join(egg_info, 'METADATA'),\n            os.path.join(egg_info, 'PKG-INFO'),\n        )\n        setup_dist = setuptools.Distribution(\n            attrs=dict(\n                install_requires=install_requires,\n                extras_require=extras_require,\n            ),\n        )\n        # Temporarily disable info traces.\n        log_threshold = log._global_log.threshold\n        log.set_threshold(log.WARN)\n        try:\n            write_requirements(\n                setup_dist.get_command_obj('egg_info'),\n                None,\n                os.path.join(egg_info, 'requires.txt'),\n            )\n        finally:\n            log.set_threshold(log_threshold)\n\n    @staticmethod\n    def _move_data_entries(destination_eggdir, dist_data):\n        \"\"\"Move data entries to their correct location.\"\"\"\n        dist_data = os.path.join(destination_eggdir, dist_data)\n        dist_data_scripts = os.path.join(dist_data, 'scripts')\n        if os.path.exists(dist_data_scripts):\n            egg_info_scripts = os.path.join(\n                destination_eggdir, 'EGG-INFO', 'scripts')\n            os.mkdir(egg_info_scripts)\n            for entry in os.listdir(dist_data_scripts):\n                # Remove bytecode, as it's not properly handled\n                # during easy_install scripts install phase.\n                if entry.endswith('.pyc'):\n                    os.unlink(os.path.join(dist_data_scripts, entry))\n                else:\n                    os.rename(\n                        os.path.join(dist_data_scripts, entry),\n                        os.path.join(egg_info_scripts, entry),\n                    )\n            os.rmdir(dist_data_scripts)\n        for subdir in filter(os.path.exists, (\n            os.path.join(dist_data, d)\n            for d in ('data', 'headers', 'purelib', 'platlib')\n        )):\n            unpack(subdir, destination_eggdir)\n        if os.path.exists(dist_data):\n            os.rmdir(dist_data)\n\n    @staticmethod\n    def _fix_namespace_packages(egg_info, destination_eggdir):\n        namespace_packages = os.path.join(\n            egg_info, 'namespace_packages.txt')\n        if os.path.exists(namespace_packages):\n            with open(namespace_packages) as fp:\n                namespace_packages = fp.read().split()\n            for mod in namespace_packages:\n                mod_dir = os.path.join(destination_eggdir, *mod.split('.'))\n                mod_init = os.path.join(mod_dir, '__init__.py')\n                if not os.path.exists(mod_dir):\n                    os.mkdir(mod_dir)\n                if not os.path.exists(mod_init):\n                    with open(mod_init, 'w') as fp:\n                        fp.write(NAMESPACE_PACKAGE_INIT)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools/windows_support.py",
    "content": "import platform\nimport ctypes\n\n\ndef windows_only(func):\n    if platform.system() != 'Windows':\n        return lambda *args, **kwargs: None\n    return func\n\n\n@windows_only\ndef hide_file(path):\n    \"\"\"\n    Set the hidden attribute on a file or directory.\n\n    From http://stackoverflow.com/questions/19622133/\n\n    `path` must be text.\n    \"\"\"\n    __import__('ctypes.wintypes')\n    SetFileAttributes = ctypes.windll.kernel32.SetFileAttributesW\n    SetFileAttributes.argtypes = ctypes.wintypes.LPWSTR, ctypes.wintypes.DWORD\n    SetFileAttributes.restype = ctypes.wintypes.BOOL\n\n    FILE_ATTRIBUTE_HIDDEN = 0x02\n\n    ret = SetFileAttributes(path, FILE_ATTRIBUTE_HIDDEN)\n    if not ret:\n        raise ctypes.WinError()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools-45.0.0.dist-info/INSTALLER",
    "content": "pip\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools-45.0.0.dist-info/LICENSE",
    "content": "Copyright (C) 2016 Jason R Coombs <jaraco@jaraco.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools-45.0.0.dist-info/METADATA",
    "content": "Metadata-Version: 2.1\nName: setuptools\nVersion: 45.0.0\nSummary: Easily download, build, install, upgrade, and uninstall Python packages\nHome-page: https://github.com/pypa/setuptools\nAuthor: Python Packaging Authority\nAuthor-email: distutils-sig@python.org\nLicense: UNKNOWN\nProject-URL: Documentation, https://setuptools.readthedocs.io/\nKeywords: CPAN PyPI distutils eggs package management\nPlatform: UNKNOWN\nClassifier: Development Status :: 5 - Production/Stable\nClassifier: Intended Audience :: Developers\nClassifier: License :: OSI Approved :: MIT License\nClassifier: Operating System :: OS Independent\nClassifier: Programming Language :: Python :: 3\nClassifier: Programming Language :: Python :: 3.5\nClassifier: Programming Language :: Python :: 3.6\nClassifier: Programming Language :: Python :: 3.7\nClassifier: Programming Language :: Python :: 3.8\nClassifier: Topic :: Software Development :: Libraries :: Python Modules\nClassifier: Topic :: System :: Archiving :: Packaging\nClassifier: Topic :: System :: Systems Administration\nClassifier: Topic :: Utilities\nRequires-Python: >=3.5\nDescription-Content-Type: text/x-rst; charset=UTF-8\nProvides-Extra: certs\nRequires-Dist: certifi (==2016.9.26) ; extra == 'certs'\nProvides-Extra: ssl\nRequires-Dist: wincertstore (==0.2) ; (sys_platform == \"win32\") and extra == 'ssl'\n\n.. image:: https://img.shields.io/pypi/v/setuptools.svg\n   :target: https://pypi.org/project/setuptools\n\n.. image:: https://img.shields.io/readthedocs/setuptools/latest.svg\n    :target: https://setuptools.readthedocs.io\n\n.. image:: https://img.shields.io/travis/pypa/setuptools/master.svg?label=Linux%20CI&logo=travis&logoColor=white\n   :target: https://travis-ci.org/pypa/setuptools\n\n.. image:: https://img.shields.io/appveyor/ci/pypa/setuptools/master.svg?label=Windows%20CI&logo=appveyor&logoColor=white\n   :target: https://ci.appveyor.com/project/pypa/setuptools/branch/master\n\n.. image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white\n   :target: https://codecov.io/gh/pypa/setuptools\n\n.. image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat\n   :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme\n\n.. image:: https://img.shields.io/pypi/pyversions/setuptools.svg\n\nSee the `Installation Instructions\n<https://packaging.python.org/installing/>`_ in the Python Packaging\nUser's Guide for instructions on installing, upgrading, and uninstalling\nSetuptools.\n\nQuestions and comments should be directed to the `distutils-sig\nmailing list <http://mail.python.org/pipermail/distutils-sig/>`_.\nBug reports and especially tested patches may be\nsubmitted directly to the `bug tracker\n<https://github.com/pypa/setuptools/issues>`_.\n\nTo report a security vulnerability, please use the\n`Tidelift security contact <https://tidelift.com/security>`_.\nTidelift will coordinate the fix and disclosure.\n\n\nFor Enterprise\n==============\n\nAvailable as part of the Tidelift Subscription.\n\nSetuptools and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.\n\n`Learn more <https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=referral&utm_campaign=github>`_.\n\nCode of Conduct\n===============\n\nEveryone interacting in the setuptools project's codebases, issue trackers,\nchat rooms, and mailing lists is expected to follow the\n`PyPA Code of Conduct <https://www.pypa.io/en/latest/code-of-conduct/>`_.\n\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools-45.0.0.dist-info/RECORD",
    "content": "../../Scripts/easy_install-3.7.exe,sha256=HdpGlRtRJ9ZeQSBTVyemMNUNfxNPFr1t2HpKejXwx_c,106371\n../../Scripts/easy_install.exe,sha256=HdpGlRtRJ9ZeQSBTVyemMNUNfxNPFr1t2HpKejXwx_c,106371\n__pycache__/easy_install.cpython-37.pyc,,\neasy_install.py,sha256=MDC9vt5AxDsXX5qcKlBz2TnW6Tpuv_AobnfhCJ9X3PM,126\npkg_resources/__init__.py,sha256=ZCRis5JbLA20eGj5AXGuQUYUbiWlDDjdV4cR03-UGcE,108386\npkg_resources/__pycache__/__init__.cpython-37.pyc,,\npkg_resources/__pycache__/py2_warn.cpython-37.pyc,,\npkg_resources/__pycache__/py31compat.cpython-37.pyc,,\npkg_resources/_vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0\npkg_resources/_vendor/__pycache__/__init__.cpython-37.pyc,,\npkg_resources/_vendor/__pycache__/appdirs.cpython-37.pyc,,\npkg_resources/_vendor/__pycache__/pyparsing.cpython-37.pyc,,\npkg_resources/_vendor/__pycache__/six.cpython-37.pyc,,\npkg_resources/_vendor/appdirs.py,sha256=MievUEuv3l_mQISH5SF0shDk_BNhHHzYiAPrT3ITN4I,24701\npkg_resources/_vendor/packaging/__about__.py,sha256=zkcCPTN_6TcLW0Nrlg0176-R1QQ_WVPTm8sz1R4-HjM,720\npkg_resources/_vendor/packaging/__init__.py,sha256=_vNac5TrzwsrzbOFIbF-5cHqc_Y2aPT2D7zrIR06BOo,513\npkg_resources/_vendor/packaging/__pycache__/__about__.cpython-37.pyc,,\npkg_resources/_vendor/packaging/__pycache__/__init__.cpython-37.pyc,,\npkg_resources/_vendor/packaging/__pycache__/_compat.cpython-37.pyc,,\npkg_resources/_vendor/packaging/__pycache__/_structures.cpython-37.pyc,,\npkg_resources/_vendor/packaging/__pycache__/markers.cpython-37.pyc,,\npkg_resources/_vendor/packaging/__pycache__/requirements.cpython-37.pyc,,\npkg_resources/_vendor/packaging/__pycache__/specifiers.cpython-37.pyc,,\npkg_resources/_vendor/packaging/__pycache__/utils.cpython-37.pyc,,\npkg_resources/_vendor/packaging/__pycache__/version.cpython-37.pyc,,\npkg_resources/_vendor/packaging/_compat.py,sha256=Vi_A0rAQeHbU-a9X0tt1yQm9RqkgQbDSxzRw8WlU9kA,860\npkg_resources/_vendor/packaging/_structures.py,sha256=RImECJ4c_wTlaTYYwZYLHEiebDMaAJmK1oPARhw1T5o,1416\npkg_resources/_vendor/packaging/markers.py,sha256=uEcBBtGvzqltgnArqb9c4RrcInXezDLos14zbBHhWJo,8248\npkg_resources/_vendor/packaging/requirements.py,sha256=SikL2UynbsT0qtY9ltqngndha_sfo0w6XGFhAhoSoaQ,4355\npkg_resources/_vendor/packaging/specifiers.py,sha256=SAMRerzO3fK2IkFZCaZkuwZaL_EGqHNOz4pni4vhnN0,28025\npkg_resources/_vendor/packaging/utils.py,sha256=3m6WvPm6NNxE8rkTGmn0r75B_GZSGg7ikafxHsBN1WA,421\npkg_resources/_vendor/packaging/version.py,sha256=OwGnxYfr2ghNzYx59qWIBkrK3SnB6n-Zfd1XaLpnnM0,11556\npkg_resources/_vendor/pyparsing.py,sha256=tmrp-lu-qO1i75ZzIN5A12nKRRD1Cm4Vpk-5LR9rims,232055\npkg_resources/_vendor/six.py,sha256=A6hdJZVjI3t_geebZ9BzUvwRrIXo0lfwzQlM2LcKyas,30098\npkg_resources/extern/__init__.py,sha256=cHiEfHuLmm6rs5Ve_ztBfMI7Lr31vss-D4wkqF5xzlI,2498\npkg_resources/extern/__pycache__/__init__.cpython-37.pyc,,\npkg_resources/py2_warn.py,sha256=6drj8vgzon3qUYMBKCeCY77OgXPAL4SANHmeKgwlrtE,633\npkg_resources/py31compat.py,sha256=-WQ0e4c3RG_acdhwC3gLiXhP_lg4G5q7XYkZkQg0gxU,558\nsetuptools-45.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4\nsetuptools-45.0.0.dist-info/LICENSE,sha256=wyo6w5WvYyHv0ovnPQagDw22q4h9HCHU_sRhKNIFbVo,1078\nsetuptools-45.0.0.dist-info/METADATA,sha256=EjoB5PxAV39vuFzUjRADw6yDVslcSgAy6cMXc6_hMNA,3566\nsetuptools-45.0.0.dist-info/RECORD,,\nsetuptools-45.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0\nsetuptools-45.0.0.dist-info/WHEEL,sha256=8zNYZbwQSXoB9IfXOjPfeNwvAsALAjffgk27FqvCWbo,110\nsetuptools-45.0.0.dist-info/dependency_links.txt,sha256=HlkCFkoK5TbZ5EMLbLKYhLcY_E31kBWD8TqW2EgmatQ,239\nsetuptools-45.0.0.dist-info/entry_points.txt,sha256=fwogkZeakIfxMNWog7kxcsbjwnMAMkpCpQv8x1ZjQ70,3206\nsetuptools-45.0.0.dist-info/top_level.txt,sha256=2HUXVVwA4Pff1xgTFr3GsTXXKaPaO6vlG6oNJ_4u4Tg,38\nsetuptools-45.0.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1\nsetuptools/__init__.py,sha256=WBpCcn2lvdckotabeae1TTYonPOcgCIF3raD2zRWzBc,7283\nsetuptools/__pycache__/__init__.cpython-37.pyc,,\nsetuptools/__pycache__/_deprecation_warning.cpython-37.pyc,,\nsetuptools/__pycache__/_imp.cpython-37.pyc,,\nsetuptools/__pycache__/archive_util.cpython-37.pyc,,\nsetuptools/__pycache__/build_meta.cpython-37.pyc,,\nsetuptools/__pycache__/config.cpython-37.pyc,,\nsetuptools/__pycache__/dep_util.cpython-37.pyc,,\nsetuptools/__pycache__/depends.cpython-37.pyc,,\nsetuptools/__pycache__/dist.cpython-37.pyc,,\nsetuptools/__pycache__/errors.cpython-37.pyc,,\nsetuptools/__pycache__/extension.cpython-37.pyc,,\nsetuptools/__pycache__/glob.cpython-37.pyc,,\nsetuptools/__pycache__/installer.cpython-37.pyc,,\nsetuptools/__pycache__/launch.cpython-37.pyc,,\nsetuptools/__pycache__/lib2to3_ex.cpython-37.pyc,,\nsetuptools/__pycache__/monkey.cpython-37.pyc,,\nsetuptools/__pycache__/msvc.cpython-37.pyc,,\nsetuptools/__pycache__/namespaces.cpython-37.pyc,,\nsetuptools/__pycache__/package_index.cpython-37.pyc,,\nsetuptools/__pycache__/py27compat.cpython-37.pyc,,\nsetuptools/__pycache__/py31compat.cpython-37.pyc,,\nsetuptools/__pycache__/py33compat.cpython-37.pyc,,\nsetuptools/__pycache__/py34compat.cpython-37.pyc,,\nsetuptools/__pycache__/sandbox.cpython-37.pyc,,\nsetuptools/__pycache__/site-patch.cpython-37.pyc,,\nsetuptools/__pycache__/ssl_support.cpython-37.pyc,,\nsetuptools/__pycache__/unicode_utils.cpython-37.pyc,,\nsetuptools/__pycache__/version.cpython-37.pyc,,\nsetuptools/__pycache__/wheel.cpython-37.pyc,,\nsetuptools/__pycache__/windows_support.cpython-37.pyc,,\nsetuptools/_deprecation_warning.py,sha256=jU9-dtfv6cKmtQJOXN8nP1mm7gONw5kKEtiPtbwnZyI,218\nsetuptools/_imp.py,sha256=jloslOkxrTKbobgemfP94YII0nhqiJzE1bRmCTZ1a5I,2223\nsetuptools/_vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0\nsetuptools/_vendor/__pycache__/__init__.cpython-37.pyc,,\nsetuptools/_vendor/__pycache__/ordered_set.cpython-37.pyc,,\nsetuptools/_vendor/__pycache__/pyparsing.cpython-37.pyc,,\nsetuptools/_vendor/__pycache__/six.cpython-37.pyc,,\nsetuptools/_vendor/ordered_set.py,sha256=dbaCcs27dyN9gnMWGF5nA_BrVn6Q-NrjKYJpV9_fgBs,15130\nsetuptools/_vendor/packaging/__about__.py,sha256=CpuMSyh1V7adw8QMjWKkY3LtdqRUkRX4MgJ6nF4stM0,744\nsetuptools/_vendor/packaging/__init__.py,sha256=6enbp5XgRfjBjsI9-bn00HjHf5TH21PDMOKkJW8xw-w,562\nsetuptools/_vendor/packaging/__pycache__/__about__.cpython-37.pyc,,\nsetuptools/_vendor/packaging/__pycache__/__init__.cpython-37.pyc,,\nsetuptools/_vendor/packaging/__pycache__/_compat.cpython-37.pyc,,\nsetuptools/_vendor/packaging/__pycache__/_structures.cpython-37.pyc,,\nsetuptools/_vendor/packaging/__pycache__/markers.cpython-37.pyc,,\nsetuptools/_vendor/packaging/__pycache__/requirements.cpython-37.pyc,,\nsetuptools/_vendor/packaging/__pycache__/specifiers.cpython-37.pyc,,\nsetuptools/_vendor/packaging/__pycache__/tags.cpython-37.pyc,,\nsetuptools/_vendor/packaging/__pycache__/utils.cpython-37.pyc,,\nsetuptools/_vendor/packaging/__pycache__/version.cpython-37.pyc,,\nsetuptools/_vendor/packaging/_compat.py,sha256=Ugdm-qcneSchW25JrtMIKgUxfEEBcCAz6WrEeXeqz9o,865\nsetuptools/_vendor/packaging/_structures.py,sha256=pVd90XcXRGwpZRB_qdFuVEibhCHpX_bL5zYr9-N0mc8,1416\nsetuptools/_vendor/packaging/markers.py,sha256=-meFl9Fr9V8rF5Rduzgett5EHK9wBYRUqssAV2pj0lw,8268\nsetuptools/_vendor/packaging/requirements.py,sha256=3dwIJekt8RRGCUbgxX8reeAbgmZYjb0wcCRtmH63kxI,4742\nsetuptools/_vendor/packaging/specifiers.py,sha256=0ZzQpcUnvrQ6LjR-mQRLzMr8G6hdRv-mY0VSf_amFtI,27778\nsetuptools/_vendor/packaging/tags.py,sha256=EPLXhO6GTD7_oiWEO1U0l0PkfR8R_xivpMDHXnsTlts,12933\nsetuptools/_vendor/packaging/utils.py,sha256=VaTC0Ei7zO2xl9ARiWmz2YFLFt89PuuhLbAlXMyAGms,1520\nsetuptools/_vendor/packaging/version.py,sha256=Npdwnb8OHedj_2L86yiUqscujb7w_i5gmSK1PhOAFzg,11978\nsetuptools/_vendor/pyparsing.py,sha256=tmrp-lu-qO1i75ZzIN5A12nKRRD1Cm4Vpk-5LR9rims,232055\nsetuptools/_vendor/six.py,sha256=A6hdJZVjI3t_geebZ9BzUvwRrIXo0lfwzQlM2LcKyas,30098\nsetuptools/archive_util.py,sha256=kw8Ib_lKjCcnPKNbS7h8HztRVK0d5RacU3r_KRdVnmM,6592\nsetuptools/build_meta.py,sha256=-9Nmj9YdbW4zX3TssPJZhsENrTa4fw3k86Jm1cdKMik,9597\nsetuptools/cli-32.exe,sha256=dfEuovMNnA2HLa3jRfMPVi5tk4R7alCbpTvuxtCyw0Y,65536\nsetuptools/cli-64.exe,sha256=KLABu5pyrnokJCv6skjXZ6GsXeyYHGcqOUT3oHI3Xpo,74752\nsetuptools/cli.exe,sha256=dfEuovMNnA2HLa3jRfMPVi5tk4R7alCbpTvuxtCyw0Y,65536\nsetuptools/command/__init__.py,sha256=QCAuA9whnq8Bnoc0bBaS6Lw_KAUO0DiHYZQXEMNn5hg,568\nsetuptools/command/__pycache__/__init__.cpython-37.pyc,,\nsetuptools/command/__pycache__/alias.cpython-37.pyc,,\nsetuptools/command/__pycache__/bdist_egg.cpython-37.pyc,,\nsetuptools/command/__pycache__/bdist_rpm.cpython-37.pyc,,\nsetuptools/command/__pycache__/bdist_wininst.cpython-37.pyc,,\nsetuptools/command/__pycache__/build_clib.cpython-37.pyc,,\nsetuptools/command/__pycache__/build_ext.cpython-37.pyc,,\nsetuptools/command/__pycache__/build_py.cpython-37.pyc,,\nsetuptools/command/__pycache__/develop.cpython-37.pyc,,\nsetuptools/command/__pycache__/dist_info.cpython-37.pyc,,\nsetuptools/command/__pycache__/easy_install.cpython-37.pyc,,\nsetuptools/command/__pycache__/egg_info.cpython-37.pyc,,\nsetuptools/command/__pycache__/install.cpython-37.pyc,,\nsetuptools/command/__pycache__/install_egg_info.cpython-37.pyc,,\nsetuptools/command/__pycache__/install_lib.cpython-37.pyc,,\nsetuptools/command/__pycache__/install_scripts.cpython-37.pyc,,\nsetuptools/command/__pycache__/py36compat.cpython-37.pyc,,\nsetuptools/command/__pycache__/register.cpython-37.pyc,,\nsetuptools/command/__pycache__/rotate.cpython-37.pyc,,\nsetuptools/command/__pycache__/saveopts.cpython-37.pyc,,\nsetuptools/command/__pycache__/sdist.cpython-37.pyc,,\nsetuptools/command/__pycache__/setopt.cpython-37.pyc,,\nsetuptools/command/__pycache__/test.cpython-37.pyc,,\nsetuptools/command/__pycache__/upload.cpython-37.pyc,,\nsetuptools/command/__pycache__/upload_docs.cpython-37.pyc,,\nsetuptools/command/alias.py,sha256=KjpE0sz_SDIHv3fpZcIQK-sCkJz-SrC6Gmug6b9Nkc8,2426\nsetuptools/command/bdist_egg.py,sha256=nnfV8Ah8IRC_Ifv5Loa9FdxL66MVbyDXwy-foP810zM,18185\nsetuptools/command/bdist_rpm.py,sha256=B7l0TnzCGb-0nLlm6rS00jWLkojASwVmdhW2w5Qz_Ak,1508\nsetuptools/command/bdist_wininst.py,sha256=_6dz3lpB1tY200LxKPLM7qgwTCceOMgaWFF-jW2-pm0,637\nsetuptools/command/build_clib.py,sha256=bQ9aBr-5ZSO-9fGsGsDLz0mnnFteHUZnftVLkhvHDq0,4484\nsetuptools/command/build_ext.py,sha256=8k4kJcOp_ZMxZ1sZYmle5_OAtNYLXGjrbWOj-IPjvwY,13023\nsetuptools/command/build_py.py,sha256=yWyYaaS9F3o9JbIczn064A5g1C5_UiKRDxGaTqYbtLE,9596\nsetuptools/command/develop.py,sha256=B3-ImHP30Bxnqx-s_9Dp-fxtHhE245ACE1W5hmKY9xE,8188\nsetuptools/command/dist_info.py,sha256=5t6kOfrdgALT-P3ogss6PF9k-Leyesueycuk3dUyZnI,960\nsetuptools/command/easy_install.py,sha256=lY0fTvseKfEbEeHUOypWmbk5akh7-MEgRNObDgrroOY,87548\nsetuptools/command/egg_info.py,sha256=WezqoeF0of9FlAy8s20And7ieqLhQNcUrb6ryUAUX2s,25574\nsetuptools/command/install.py,sha256=8doMxeQEDoK4Eco0mO2WlXXzzp9QnsGJQ7Z7yWkZPG8,4705\nsetuptools/command/install_egg_info.py,sha256=bMgeIeRiXzQ4DAGPV1328kcjwQjHjOWU4FngAWLV78Q,2203\nsetuptools/command/install_lib.py,sha256=r5NuasaSxvmIrjgZNj38Iq-1UJG1o1ms7CuHq6MCTbQ,3862\nsetuptools/command/install_scripts.py,sha256=UD0rEZ6861mTYhIdzcsqKnUl8PozocXWl9VBQ1VTWnc,2439\nsetuptools/command/launcher manifest.xml,sha256=xlLbjWrB01tKC0-hlVkOKkiSPbzMml2eOPtJ_ucCnbE,628\nsetuptools/command/py36compat.py,sha256=SzjZcOxF7zdFUT47Zv2n7AM3H8koDys_0OpS-n9gIfc,4986\nsetuptools/command/register.py,sha256=kk3DxXCb5lXTvqnhfwx2g6q7iwbUmgTyXUCaBooBOUk,468\nsetuptools/command/rotate.py,sha256=co5C1EkI7P0GGT6Tqz-T2SIj2LBJTZXYELpmao6d4KQ,2164\nsetuptools/command/saveopts.py,sha256=za7QCBcQimKKriWcoCcbhxPjUz30gSB74zuTL47xpP4,658\nsetuptools/command/sdist.py,sha256=14kBw_QOZ9L_RQDqgf9DAlEuoj0zC30X5mfDWeiyZwU,8092\nsetuptools/command/setopt.py,sha256=NTWDyx-gjDF-txf4dO577s7LOzHVoKR0Mq33rFxaRr8,5085\nsetuptools/command/test.py,sha256=MahF7jRBGYwgM-fdPBLyBpfCqR5ZXSGexPVR-afV3Vc,9610\nsetuptools/command/upload.py,sha256=XT3YFVfYPAmA5qhGg0euluU98ftxRUW-PzKcODMLxUs,462\nsetuptools/command/upload_docs.py,sha256=O137bN9dt_sELevf4vwuwWRkogHf4bPXc-jNRxVQaiw,7315\nsetuptools/config.py,sha256=6SB2OY3qcooOJmG_rsK_s0pKBsorBlDpfMJUyzjQIGk,20575\nsetuptools/dep_util.py,sha256=fgixvC1R7sH3r13ktyf7N0FALoqEXL1cBarmNpSEoWg,935\nsetuptools/depends.py,sha256=qt2RWllArRvhnm8lxsyRpcthEZYp4GHQgREl1q0LkFw,5517\nsetuptools/dist.py,sha256=IM2JJpMjq3CuY3wqpeTkAutTCK2DJ1-rLPPHcvw5M4k,49865\nsetuptools/errors.py,sha256=MVOcv381HNSajDgEUWzOQ4J6B5BHCBMSjHfaWcEwA1o,524\nsetuptools/extension.py,sha256=uc6nHI-MxwmNCNPbUiBnybSyqhpJqjbhvOQ-emdvt_E,1729\nsetuptools/extern/__init__.py,sha256=4q9gtShB1XFP6CisltsyPqtcfTO6ZM9Lu1QBl3l-qmo,2514\nsetuptools/extern/__pycache__/__init__.cpython-37.pyc,,\nsetuptools/glob.py,sha256=o75cHrOxYsvn854thSxE0x9k8JrKDuhP_rRXlVB00Q4,5084\nsetuptools/gui-32.exe,sha256=XBr0bHMA6Hpz2s9s9Bzjl-PwXfa9nH4ie0rFn4V2kWA,65536\nsetuptools/gui-64.exe,sha256=aYKMhX1IJLn4ULHgWX0sE0yREUt6B3TEHf_jOw6yNyE,75264\nsetuptools/gui.exe,sha256=XBr0bHMA6Hpz2s9s9Bzjl-PwXfa9nH4ie0rFn4V2kWA,65536\nsetuptools/installer.py,sha256=TCFRonRo01I79zo-ucf3Ymhj8TenPlmhMijN916aaJs,5337\nsetuptools/launch.py,sha256=sd7ejwhBocCDx_wG9rIs0OaZ8HtmmFU8ZC6IR_S0Lvg,787\nsetuptools/lib2to3_ex.py,sha256=t5e12hbR2pi9V4ezWDTB4JM-AISUnGOkmcnYHek3xjg,2013\nsetuptools/monkey.py,sha256=FGc9fffh7gAxMLFmJs2DW_OYWpBjkdbNS2n14UAK4NA,5264\nsetuptools/msvc.py,sha256=8baJ6aYgCA4TRdWQQi185qB9dnU8FaP4wgpbmd7VODs,46751\nsetuptools/namespaces.py,sha256=F0Nrbv8KCT2OrO7rwa03om4N4GZKAlnce-rr-cgDQa8,3199\nsetuptools/package_index.py,sha256=6pb-B1POtHyLycAbkDETk4fO-Qv8_sY-rjTXhUOoh6k,40605\nsetuptools/py27compat.py,sha256=tvmer0Tn-wk_JummCkoM22UIjpjL-AQ8uUiOaqTs8sI,1496\nsetuptools/py31compat.py,sha256=h2rtZghOfwoGYd8sQ0-auaKiF3TcL3qX0bX3VessqcE,838\nsetuptools/py33compat.py,sha256=SMF9Z8wnGicTOkU1uRNwZ_kz5Z_bj29PUBbqdqeeNsc,1330\nsetuptools/py34compat.py,sha256=KYOd6ybRxjBW8NJmYD8t_UyyVmysppFXqHpFLdslGXU,245\nsetuptools/sandbox.py,sha256=9UbwfEL5QY436oMI1LtFWohhoZ-UzwHvGyZjUH_qhkw,14276\nsetuptools/script (dev).tmpl,sha256=RUzQzCQUaXtwdLtYHWYbIQmOaES5Brqq1FvUA_tu-5I,218\nsetuptools/script.tmpl,sha256=WGTt5piezO27c-Dbx6l5Q4T3Ff20A5z7872hv3aAhYY,138\nsetuptools/site-patch.py,sha256=OumkIHMuoSenRSW1382kKWI1VAwxNE86E5W8iDd34FY,2302\nsetuptools/ssl_support.py,sha256=nLjPUBBw7RTTx6O4RJZ5eAMGgjJG8beiDbkFXDZpLuM,8493\nsetuptools/unicode_utils.py,sha256=NOiZ_5hD72A6w-4wVj8awHFM3n51Kmw1Ic_vx15XFqw,996\nsetuptools/version.py,sha256=og_cuZQb0QI6ukKZFfZWPlr1HgJBPPn2vO2m_bI9ZTE,144\nsetuptools/wheel.py,sha256=zct-SEj5_LoHg6XELt2cVRdulsUENenCdS1ekM7TlZA,8455\nsetuptools/windows_support.py,sha256=5GrfqSP2-dLGJoZTq2g6dCKkyQxxa2n5IQiXlJCoYEE,714\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools-45.0.0.dist-info/REQUESTED",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools-45.0.0.dist-info/WHEEL",
    "content": "Wheel-Version: 1.0\nGenerator: bdist_wheel (0.33.6)\nRoot-Is-Purelib: true\nTag: py2-none-any\nTag: py3-none-any\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools-45.0.0.dist-info/dependency_links.txt",
    "content": "https://files.pythonhosted.org/packages/source/c/certifi/certifi-2016.9.26.tar.gz#md5=baa81e951a29958563689d868ef1064d\nhttps://files.pythonhosted.org/packages/source/w/wincertstore/wincertstore-0.2.zip#md5=ae728f2f007185648d0c7a8679b361e2\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools-45.0.0.dist-info/entry_points.txt",
    "content": "[console_scripts]\neasy_install = setuptools.command.easy_install:main\neasy_install-3.8 = setuptools.command.easy_install:main\n\n[distutils.commands]\nalias = setuptools.command.alias:alias\nbdist_egg = setuptools.command.bdist_egg:bdist_egg\nbdist_rpm = setuptools.command.bdist_rpm:bdist_rpm\nbdist_wininst = setuptools.command.bdist_wininst:bdist_wininst\nbuild_clib = setuptools.command.build_clib:build_clib\nbuild_ext = setuptools.command.build_ext:build_ext\nbuild_py = setuptools.command.build_py:build_py\ndevelop = setuptools.command.develop:develop\ndist_info = setuptools.command.dist_info:dist_info\neasy_install = setuptools.command.easy_install:easy_install\negg_info = setuptools.command.egg_info:egg_info\ninstall = setuptools.command.install:install\ninstall_egg_info = setuptools.command.install_egg_info:install_egg_info\ninstall_lib = setuptools.command.install_lib:install_lib\ninstall_scripts = setuptools.command.install_scripts:install_scripts\nrotate = setuptools.command.rotate:rotate\nsaveopts = setuptools.command.saveopts:saveopts\nsdist = setuptools.command.sdist:sdist\nsetopt = setuptools.command.setopt:setopt\ntest = setuptools.command.test:test\nupload_docs = setuptools.command.upload_docs:upload_docs\n\n[distutils.setup_keywords]\nconvert_2to3_doctests = setuptools.dist:assert_string_list\ndependency_links = setuptools.dist:assert_string_list\neager_resources = setuptools.dist:assert_string_list\nentry_points = setuptools.dist:check_entry_points\nexclude_package_data = setuptools.dist:check_package_data\nextras_require = setuptools.dist:check_extras\ninclude_package_data = setuptools.dist:assert_bool\ninstall_requires = setuptools.dist:check_requirements\nnamespace_packages = setuptools.dist:check_nsp\npackage_data = setuptools.dist:check_package_data\npackages = setuptools.dist:check_packages\npython_requires = setuptools.dist:check_specifier\nsetup_requires = setuptools.dist:check_requirements\ntest_loader = setuptools.dist:check_importable\ntest_runner = setuptools.dist:check_importable\ntest_suite = setuptools.dist:check_test_suite\ntests_require = setuptools.dist:check_requirements\nuse_2to3 = setuptools.dist:assert_bool\nuse_2to3_exclude_fixers = setuptools.dist:assert_string_list\nuse_2to3_fixers = setuptools.dist:assert_string_list\nzip_safe = setuptools.dist:assert_bool\n\n[egg_info.writers]\nPKG-INFO = setuptools.command.egg_info:write_pkg_info\ndependency_links.txt = setuptools.command.egg_info:overwrite_arg\ndepends.txt = setuptools.command.egg_info:warn_depends_obsolete\neager_resources.txt = setuptools.command.egg_info:overwrite_arg\nentry_points.txt = setuptools.command.egg_info:write_entries\nnamespace_packages.txt = setuptools.command.egg_info:overwrite_arg\nrequires.txt = setuptools.command.egg_info:write_requirements\ntop_level.txt = setuptools.command.egg_info:write_toplevel_names\n\n[setuptools.finalize_distribution_options]\n2to3_doctests = setuptools.dist:Distribution._finalize_2to3_doctests\nfeatures = setuptools.dist:Distribution._finalize_feature_opts\nkeywords = setuptools.dist:Distribution._finalize_setup_keywords\nparent_finalize = setuptools.dist:_Distribution.finalize_options\n\n[setuptools.installation]\neggsecutable = setuptools.command.easy_install:bootstrap\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools-45.0.0.dist-info/top_level.txt",
    "content": "easy_install\npkg_resources\nsetuptools\n"
  },
  {
    "path": "openpype/vendor/python/python_2/setuptools-45.0.0.dist-info/zip-safe",
    "content": "\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/__init__.py",
    "content": "import sys\n\nfrom .client import Client\nfrom .base_manager import BaseManager\nfrom .pubsub_manager import PubSubManager\nfrom .kombu_manager import KombuManager\nfrom .redis_manager import RedisManager\nfrom .zmq_manager import ZmqManager\nfrom .server import Server\nfrom .namespace import Namespace, ClientNamespace\nfrom .middleware import WSGIApp, Middleware\nfrom .tornado import get_tornado_handler\nif sys.version_info >= (3, 5):  # pragma: no cover\n    from .asyncio_client import AsyncClient\n    from .asyncio_server import AsyncServer\n    from .asyncio_manager import AsyncManager\n    from .asyncio_namespace import AsyncNamespace, AsyncClientNamespace\n    from .asyncio_redis_manager import AsyncRedisManager\n    from .asgi import ASGIApp\nelse:  # pragma: no cover\n    AsyncClient = None\n    AsyncServer = None\n    AsyncManager = None\n    AsyncNamespace = None\n    AsyncRedisManager = None\n\n__version__ = '4.2.1'\n\n__all__ = ['__version__', 'Client', 'Server', 'BaseManager', 'PubSubManager',\n           'KombuManager', 'RedisManager', 'ZmqManager', 'Namespace',\n           'ClientNamespace', 'WSGIApp', 'Middleware']\nif AsyncServer is not None:  # pragma: no cover\n    __all__ += ['AsyncClient', 'AsyncServer', 'AsyncNamespace',\n                'AsyncClientNamespace', 'AsyncManager', 'AsyncRedisManager',\n                'ASGIApp', 'get_tornado_handler']\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/asgi.py",
    "content": "import engineio\n\n\nclass ASGIApp(engineio.ASGIApp):  # pragma: no cover\n    \"\"\"ASGI application middleware for Socket.IO.\n\n    This middleware dispatches traffic to an Socket.IO application. It can\n    also serve a list of static files to the client, or forward unrelated\n    HTTP traffic to another ASGI application.\n\n    :param socketio_server: The Socket.IO server. Must be an instance of the\n                            ``socketio.AsyncServer`` class.\n    :param static_files: A dictionary with static file mapping rules. See the\n                         documentation for details on this argument.\n    :param other_asgi_app: A separate ASGI app that receives all other traffic.\n    :param socketio_path: The endpoint where the Socket.IO application should\n                          be installed. The default value is appropriate for\n                          most cases.\n\n    Example usage::\n\n        import socketio\n        import uvicorn\n\n        sio = socketio.AsyncServer()\n        app = engineio.ASGIApp(sio, static_files={\n            '/': 'index.html',\n            '/static': './public',\n        })\n        uvicorn.run(app, host='127.0.0.1', port=5000)\n    \"\"\"\n    def __init__(self, socketio_server, other_asgi_app=None,\n                 static_files=None, socketio_path='socket.io'):\n        super().__init__(socketio_server, other_asgi_app,\n                         static_files=static_files,\n                         engineio_path=socketio_path)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/asyncio_client.py",
    "content": "import asyncio\nimport logging\nimport random\n\nimport engineio\nimport six\n\nfrom . import client\nfrom . import exceptions\nfrom . import packet\n\ndefault_logger = logging.getLogger('socketio.client')\n\n\nclass AsyncClient(client.Client):\n    \"\"\"A Socket.IO client for asyncio.\n\n    This class implements a fully compliant Socket.IO web client with support\n    for websocket and long-polling transports.\n\n    :param reconnection: ``True`` if the client should automatically attempt to\n                         reconnect to the server after an interruption, or\n                         ``False`` to not reconnect. The default is ``True``.\n    :param reconnection_attempts: How many reconnection attempts to issue\n                                  before giving up, or 0 for infinity attempts.\n                                  The default is 0.\n    :param reconnection_delay: How long to wait in seconds before the first\n                               reconnection attempt. Each successive attempt\n                               doubles this delay.\n    :param reconnection_delay_max: The maximum delay between reconnection\n                                   attempts.\n    :param randomization_factor: Randomization amount for each delay between\n                                 reconnection attempts. The default is 0.5,\n                                 which means that each delay is randomly\n                                 adjusted by +/- 50%.\n    :param logger: To enable logging set to ``True`` or pass a logger object to\n                   use. To disable logging set to ``False``. The default is\n                   ``False``.\n    :param binary: ``True`` to support binary payloads, ``False`` to treat all\n                   payloads as text. On Python 2, if this is set to ``True``,\n                   ``unicode`` values are treated as text, and ``str`` and\n                   ``bytes`` values are treated as binary.  This option has no\n                   effect on Python 3, where text and binary payloads are\n                   always automatically discovered.\n    :param json: An alternative json module to use for encoding and decoding\n                 packets. Custom json modules must have ``dumps`` and ``loads``\n                 functions that are compatible with the standard library\n                 versions.\n\n    The Engine.IO configuration supports the following settings:\n\n    :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass\n                            a logger object to use. To disable logging set to\n                            ``False``. The default is ``False``.\n    \"\"\"\n    def is_asyncio_based(self):\n        return True\n\n    async def connect(self, url, headers={}, transports=None,\n                      namespaces=None, socketio_path='socket.io'):\n        \"\"\"Connect to a Socket.IO server.\n\n        :param url: The URL of the Socket.IO server. It can include custom\n                    query string parameters if required by the server.\n        :param headers: A dictionary with custom headers to send with the\n                        connection request.\n        :param transports: The list of allowed transports. Valid transports\n                           are ``'polling'`` and ``'websocket'``. If not\n                           given, the polling transport is connected first,\n                           then an upgrade to websocket is attempted.\n        :param namespaces: The list of custom namespaces to connect, in\n                           addition to the default namespace. If not given,\n                           the namespace list is obtained from the registered\n                           event handlers.\n        :param socketio_path: The endpoint where the Socket.IO server is\n                              installed. The default value is appropriate for\n                              most cases.\n\n        Note: this method is a coroutine.\n\n        Example usage::\n\n            sio = socketio.Client()\n            sio.connect('http://localhost:5000')\n        \"\"\"\n        self.connection_url = url\n        self.connection_headers = headers\n        self.connection_transports = transports\n        self.connection_namespaces = namespaces\n        self.socketio_path = socketio_path\n\n        if namespaces is None:\n            namespaces = set(self.handlers.keys()).union(\n                set(self.namespace_handlers.keys()))\n        elif isinstance(namespaces, six.string_types):\n            namespaces = [namespaces]\n            self.connection_namespaces = namespaces\n        self.namespaces = [n for n in namespaces if n != '/']\n        try:\n            await self.eio.connect(url, headers=headers,\n                                   transports=transports,\n                                   engineio_path=socketio_path)\n        except engineio.exceptions.ConnectionError as exc:\n            six.raise_from(exceptions.ConnectionError(exc.args[0]), None)\n\n    async def wait(self):\n        \"\"\"Wait until the connection with the server ends.\n\n        Client applications can use this function to block the main thread\n        during the life of the connection.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        while True:\n            await self.eio.wait()\n            await self.sleep(1)  # give the reconnect task time to start up\n            if not self._reconnect_task:\n                break\n            await self._reconnect_task\n            if self.eio.state != 'connected':\n                break\n\n    async def emit(self, event, data=None, namespace=None, callback=None):\n        \"\"\"Emit a custom event to one or more connected clients.\n\n        :param event: The event name. It can be any string. The event names\n                      ``'connect'``, ``'message'`` and ``'disconnect'`` are\n                      reserved and should not be used.\n        :param data: The data to send to the client or clients. Data can be of\n                     type ``str``, ``bytes``, ``list`` or ``dict``. If a\n                     ``list`` or ``dict``, the data will be serialized as JSON.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the event is emitted to the\n                          default namespace.\n        :param callback: If given, this function will be called to acknowledge\n                         the the client has received the message. The arguments\n                         that will be passed to the function are those provided\n                         by the client. Callback functions can only be used\n                         when addressing an individual client.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        namespace = namespace or '/'\n        self.logger.info('Emitting event \"%s\" [%s]', event, namespace)\n        if callback is not None:\n            id = self._generate_ack_id(namespace, callback)\n        else:\n            id = None\n        if six.PY2 and not self.binary:\n            binary = False  # pragma: nocover\n        else:\n            binary = None\n        # tuples are expanded to multiple arguments, everything else is sent\n        # as a single argument\n        if isinstance(data, tuple):\n            data = list(data)\n        elif data is not None:\n            data = [data]\n        else:\n            data = []\n        await self._send_packet(packet.Packet(\n            packet.EVENT, namespace=namespace, data=[event] + data, id=id,\n            binary=binary))\n\n    async def send(self, data, namespace=None, callback=None):\n        \"\"\"Send a message to one or more connected clients.\n\n        This function emits an event with the name ``'message'``. Use\n        :func:`emit` to issue custom event names.\n\n        :param data: The data to send to the client or clients. Data can be of\n                     type ``str``, ``bytes``, ``list`` or ``dict``. If a\n                     ``list`` or ``dict``, the data will be serialized as JSON.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the event is emitted to the\n                          default namespace.\n        :param callback: If given, this function will be called to acknowledge\n                         the the client has received the message. The arguments\n                         that will be passed to the function are those provided\n                         by the client. Callback functions can only be used\n                         when addressing an individual client.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        await self.emit('message', data=data, namespace=namespace,\n                        callback=callback)\n\n    async def call(self, event, data=None, namespace=None, timeout=60):\n        \"\"\"Emit a custom event to a client and wait for the response.\n\n        :param event: The event name. It can be any string. The event names\n                      ``'connect'``, ``'message'`` and ``'disconnect'`` are\n                      reserved and should not be used.\n        :param data: The data to send to the client or clients. Data can be of\n                     type ``str``, ``bytes``, ``list`` or ``dict``. If a\n                     ``list`` or ``dict``, the data will be serialized as JSON.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the event is emitted to the\n                          default namespace.\n        :param timeout: The waiting timeout. If the timeout is reached before\n                        the client acknowledges the event, then a\n                        ``TimeoutError`` exception is raised.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        callback_event = self.eio.create_event()\n        callback_args = []\n\n        def event_callback(*args):\n            callback_args.append(args)\n            callback_event.set()\n\n        await self.emit(event, data=data, namespace=namespace,\n                        callback=event_callback)\n        try:\n            await asyncio.wait_for(callback_event.wait(), timeout)\n        except asyncio.TimeoutError:\n            six.raise_from(exceptions.TimeoutError(), None)\n        return callback_args[0] if len(callback_args[0]) > 1 \\\n            else callback_args[0][0] if len(callback_args[0]) == 1 \\\n            else None\n\n    async def disconnect(self):\n        \"\"\"Disconnect from the server.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        # here we just request the disconnection\n        # later in _handle_eio_disconnect we invoke the disconnect handler\n        for n in self.namespaces:\n            await self._send_packet(packet.Packet(packet.DISCONNECT,\n                                    namespace=n))\n        await self._send_packet(packet.Packet(\n            packet.DISCONNECT, namespace='/'))\n        await self.eio.disconnect(abort=True)\n\n    def start_background_task(self, target, *args, **kwargs):\n        \"\"\"Start a background task using the appropriate async model.\n\n        This is a utility function that applications can use to start a\n        background task using the method that is compatible with the\n        selected async mode.\n\n        :param target: the target function to execute.\n        :param args: arguments to pass to the function.\n        :param kwargs: keyword arguments to pass to the function.\n\n        This function returns an object compatible with the `Thread` class in\n        the Python standard library. The `start()` method on this object is\n        already called by this function.\n        \"\"\"\n        return self.eio.start_background_task(target, *args, **kwargs)\n\n    async def sleep(self, seconds=0):\n        \"\"\"Sleep for the requested amount of time using the appropriate async\n        model.\n\n        This is a utility function that applications can use to put a task to\n        sleep without having to worry about using the correct call for the\n        selected async mode.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await self.eio.sleep(seconds)\n\n    async def _send_packet(self, pkt):\n        \"\"\"Send a Socket.IO packet to the server.\"\"\"\n        encoded_packet = pkt.encode()\n        if isinstance(encoded_packet, list):\n            binary = False\n            for ep in encoded_packet:\n                await self.eio.send(ep, binary=binary)\n                binary = True\n        else:\n            await self.eio.send(encoded_packet, binary=False)\n\n    async def _handle_connect(self, namespace):\n        namespace = namespace or '/'\n        self.logger.info('Namespace {} is connected'.format(namespace))\n        await self._trigger_event('connect', namespace=namespace)\n        if namespace == '/':\n            for n in self.namespaces:\n                await self._send_packet(packet.Packet(packet.CONNECT,\n                                        namespace=n))\n        elif namespace not in self.namespaces:\n            self.namespaces.append(namespace)\n\n    async def _handle_disconnect(self, namespace):\n        namespace = namespace or '/'\n        await self._trigger_event('disconnect', namespace=namespace)\n        if namespace in self.namespaces:\n            self.namespaces.remove(namespace)\n\n    async def _handle_event(self, namespace, id, data):\n        namespace = namespace or '/'\n        self.logger.info('Received event \"%s\" [%s]', data[0], namespace)\n        r = await self._trigger_event(data[0], namespace, *data[1:])\n        if id is not None:\n            # send ACK packet with the response returned by the handler\n            # tuples are expanded as multiple arguments\n            if r is None:\n                data = []\n            elif isinstance(r, tuple):\n                data = list(r)\n            else:\n                data = [r]\n            if six.PY2 and not self.binary:\n                binary = False  # pragma: nocover\n            else:\n                binary = None\n            await self._send_packet(packet.Packet(\n                packet.ACK, namespace=namespace, id=id, data=data,\n                binary=binary))\n\n    async def _handle_ack(self, namespace, id, data):\n        namespace = namespace or '/'\n        self.logger.info('Received ack [%s]', namespace)\n        callback = None\n        try:\n            callback = self.callbacks[namespace][id]\n        except KeyError:\n            # if we get an unknown callback we just ignore it\n            self.logger.warning('Unknown callback received, ignoring.')\n        else:\n            del self.callbacks[namespace][id]\n        if callback is not None:\n            if asyncio.iscoroutinefunction(callback):\n                await callback(*data)\n            else:\n                callback(*data)\n\n    def _handle_error(self, namespace):\n        namespace = namespace or '/'\n        self.logger.info('Connection to namespace {} was rejected'.format(\n            namespace))\n        if namespace in self.namespaces:\n            self.namespaces.remove(namespace)\n\n    async def _trigger_event(self, event, namespace, *args):\n        \"\"\"Invoke an application event handler.\"\"\"\n        # first see if we have an explicit handler for the event\n        if namespace in self.handlers and event in self.handlers[namespace]:\n            if asyncio.iscoroutinefunction(self.handlers[namespace][event]):\n                try:\n                    ret = await self.handlers[namespace][event](*args)\n                except asyncio.CancelledError:  # pragma: no cover\n                    ret = None\n            else:\n                ret = self.handlers[namespace][event](*args)\n            return ret\n\n        # or else, forward the event to a namepsace handler if one exists\n        elif namespace in self.namespace_handlers:\n            return await self.namespace_handlers[namespace].trigger_event(\n                event, *args)\n\n    async def _handle_reconnect(self):\n        self._reconnect_abort.clear()\n        client.reconnecting_clients.append(self)\n        attempt_count = 0\n        current_delay = self.reconnection_delay\n        while True:\n            delay = current_delay\n            current_delay *= 2\n            if delay > self.reconnection_delay_max:\n                delay = self.reconnection_delay_max\n            delay += self.randomization_factor * (2 * random.random() - 1)\n            self.logger.info(\n                'Connection failed, new attempt in {:.02f} seconds'.format(\n                    delay))\n            try:\n                await asyncio.wait_for(self._reconnect_abort.wait(), delay)\n                self.logger.info('Reconnect task aborted')\n                break\n            except (asyncio.TimeoutError, asyncio.CancelledError):\n                pass\n            attempt_count += 1\n            try:\n                await self.connect(self.connection_url,\n                                   headers=self.connection_headers,\n                                   transports=self.connection_transports,\n                                   namespaces=self.connection_namespaces,\n                                   socketio_path=self.socketio_path)\n            except (exceptions.ConnectionError, ValueError):\n                pass\n            else:\n                self.logger.info('Reconnection successful')\n                self._reconnect_task = None\n                break\n            if self.reconnection_attempts and \\\n                    attempt_count >= self.reconnection_attempts:\n                self.logger.info(\n                    'Maximum reconnection attempts reached, giving up')\n                break\n        client.reconnecting_clients.remove(self)\n\n    def _handle_eio_connect(self):\n        \"\"\"Handle the Engine.IO connection event.\"\"\"\n        self.logger.info('Engine.IO connection established')\n        self.sid = self.eio.sid\n\n    async def _handle_eio_message(self, data):\n        \"\"\"Dispatch Engine.IO messages.\"\"\"\n        if self._binary_packet:\n            pkt = self._binary_packet\n            if pkt.add_attachment(data):\n                self._binary_packet = None\n                if pkt.packet_type == packet.BINARY_EVENT:\n                    await self._handle_event(pkt.namespace, pkt.id, pkt.data)\n                else:\n                    await self._handle_ack(pkt.namespace, pkt.id, pkt.data)\n        else:\n            pkt = packet.Packet(encoded_packet=data)\n            if pkt.packet_type == packet.CONNECT:\n                await self._handle_connect(pkt.namespace)\n            elif pkt.packet_type == packet.DISCONNECT:\n                await self._handle_disconnect(pkt.namespace)\n            elif pkt.packet_type == packet.EVENT:\n                await self._handle_event(pkt.namespace, pkt.id, pkt.data)\n            elif pkt.packet_type == packet.ACK:\n                await self._handle_ack(pkt.namespace, pkt.id, pkt.data)\n            elif pkt.packet_type == packet.BINARY_EVENT or \\\n                    pkt.packet_type == packet.BINARY_ACK:\n                self._binary_packet = pkt\n            elif pkt.packet_type == packet.ERROR:\n                self._handle_error(pkt.namespace)\n            else:\n                raise ValueError('Unknown packet type.')\n\n    async def _handle_eio_disconnect(self):\n        \"\"\"Handle the Engine.IO disconnection event.\"\"\"\n        self.logger.info('Engine.IO connection dropped')\n        self._reconnect_abort.set()\n        for n in self.namespaces:\n            await self._trigger_event('disconnect', namespace=n)\n        await self._trigger_event('disconnect', namespace='/')\n        self.callbacks = {}\n        self._binary_packet = None\n        self.sid = None\n        if self.eio.state == 'connected' and self.reconnection:\n            self._reconnect_task = self.start_background_task(\n                self._handle_reconnect)\n\n    def _engineio_client_class(self):\n        return engineio.AsyncClient\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/asyncio_manager.py",
    "content": "import asyncio\n\nfrom .base_manager import BaseManager\n\n\nclass AsyncManager(BaseManager):\n    \"\"\"Manage a client list for an asyncio server.\"\"\"\n    async def emit(self, event, data, namespace, room=None, skip_sid=None,\n                   callback=None, **kwargs):\n        \"\"\"Emit a message to a single client, a room, or all the clients\n        connected to the namespace.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        if namespace not in self.rooms or room not in self.rooms[namespace]:\n            return\n        tasks = []\n        if not isinstance(skip_sid, list):\n            skip_sid = [skip_sid]\n        for sid in self.get_participants(namespace, room):\n            if sid not in skip_sid:\n                if callback is not None:\n                    id = self._generate_ack_id(sid, namespace, callback)\n                else:\n                    id = None\n                tasks.append(self.server._emit_internal(sid, event, data,\n                                                        namespace, id))\n        if tasks == []:  # pragma: no cover\n            return\n        await asyncio.wait(tasks)\n\n    async def close_room(self, room, namespace):\n        \"\"\"Remove all participants from a room.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return super().close_room(room, namespace)\n\n    async def trigger_callback(self, sid, namespace, id, data):\n        \"\"\"Invoke an application callback.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        callback = None\n        try:\n            callback = self.callbacks[sid][namespace][id]\n        except KeyError:\n            # if we get an unknown callback we just ignore it\n            self._get_logger().warning('Unknown callback received, ignoring.')\n        else:\n            del self.callbacks[sid][namespace][id]\n        if callback is not None:\n            ret = callback(*data)\n            if asyncio.iscoroutine(ret):\n                try:\n                    await ret\n                except asyncio.CancelledError:  # pragma: no cover\n                    pass\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/asyncio_namespace.py",
    "content": "import asyncio\n\nfrom socketio import namespace\n\n\nclass AsyncNamespace(namespace.Namespace):\n    \"\"\"Base class for asyncio server-side class-based namespaces.\n\n    A class-based namespace is a class that contains all the event handlers\n    for a Socket.IO namespace. The event handlers are methods of the class\n    with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``,\n    ``on_message``, ``on_json``, and so on. These can be regular functions or\n    coroutines.\n\n    :param namespace: The Socket.IO namespace to be used with all the event\n                      handlers defined in this class. If this argument is\n                      omitted, the default namespace is used.\n    \"\"\"\n    def is_asyncio_based(self):\n        return True\n\n    async def trigger_event(self, event, *args):\n        \"\"\"Dispatch an event to the proper handler method.\n\n        In the most common usage, this method is not overloaded by subclasses,\n        as it performs the routing of events to methods. However, this\n        method can be overriden if special dispatching rules are needed, or if\n        having a single method that catches all events is desired.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        handler_name = 'on_' + event\n        if hasattr(self, handler_name):\n            handler = getattr(self, handler_name)\n            if asyncio.iscoroutinefunction(handler) is True:\n                try:\n                    ret = await handler(*args)\n                except asyncio.CancelledError:  # pragma: no cover\n                    ret = None\n            else:\n                ret = handler(*args)\n            return ret\n\n    async def emit(self, event, data=None, room=None, skip_sid=None,\n                   namespace=None, callback=None):\n        \"\"\"Emit a custom event to one or more connected clients.\n\n        The only difference with the :func:`socketio.Server.emit` method is\n        that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await self.server.emit(event, data=data, room=room,\n                                      skip_sid=skip_sid,\n                                      namespace=namespace or self.namespace,\n                                      callback=callback)\n\n    async def send(self, data, room=None, skip_sid=None, namespace=None,\n                   callback=None):\n        \"\"\"Send a message to one or more connected clients.\n\n        The only difference with the :func:`socketio.Server.send` method is\n        that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await self.server.send(data, room=room, skip_sid=skip_sid,\n                                      namespace=namespace or self.namespace,\n                                      callback=callback)\n\n    async def close_room(self, room, namespace=None):\n        \"\"\"Close a room.\n\n        The only difference with the :func:`socketio.Server.close_room` method\n        is that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await self.server.close_room(\n            room, namespace=namespace or self.namespace)\n\n    async def get_session(self, sid, namespace=None):\n        \"\"\"Return the user session for a client.\n\n        The only difference with the :func:`socketio.Server.get_session`\n        method is that when the ``namespace`` argument is not given the\n        namespace associated with the class is used.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await self.server.get_session(\n            sid, namespace=namespace or self.namespace)\n\n    async def save_session(self, sid, session, namespace=None):\n        \"\"\"Store the user session for a client.\n\n        The only difference with the :func:`socketio.Server.save_session`\n        method is that when the ``namespace`` argument is not given the\n        namespace associated with the class is used.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await self.server.save_session(\n            sid, session, namespace=namespace or self.namespace)\n\n    def session(self, sid, namespace=None):\n        \"\"\"Return the user session for a client with context manager syntax.\n\n        The only difference with the :func:`socketio.Server.session` method is\n        that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n        \"\"\"\n        return self.server.session(sid, namespace=namespace or self.namespace)\n\n    async def disconnect(self, sid, namespace=None):\n        \"\"\"Disconnect a client.\n\n        The only difference with the :func:`socketio.Server.disconnect` method\n        is that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await self.server.disconnect(\n            sid, namespace=namespace or self.namespace)\n\n\nclass AsyncClientNamespace(namespace.ClientNamespace):\n    \"\"\"Base class for asyncio client-side class-based namespaces.\n\n    A class-based namespace is a class that contains all the event handlers\n    for a Socket.IO namespace. The event handlers are methods of the class\n    with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``,\n    ``on_message``, ``on_json``, and so on. These can be regular functions or\n    coroutines.\n\n    :param namespace: The Socket.IO namespace to be used with all the event\n                      handlers defined in this class. If this argument is\n                      omitted, the default namespace is used.\n    \"\"\"\n    def is_asyncio_based(self):\n        return True\n\n    async def trigger_event(self, event, *args):\n        \"\"\"Dispatch an event to the proper handler method.\n\n        In the most common usage, this method is not overloaded by subclasses,\n        as it performs the routing of events to methods. However, this\n        method can be overriden if special dispatching rules are needed, or if\n        having a single method that catches all events is desired.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        handler_name = 'on_' + event\n        if hasattr(self, handler_name):\n            handler = getattr(self, handler_name)\n            if asyncio.iscoroutinefunction(handler) is True:\n                try:\n                    ret = await handler(*args)\n                except asyncio.CancelledError:  # pragma: no cover\n                    ret = None\n            else:\n                ret = handler(*args)\n            return ret\n\n    async def emit(self, event, data=None, namespace=None, callback=None):\n        \"\"\"Emit a custom event to the server.\n\n        The only difference with the :func:`socketio.Client.emit` method is\n        that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await self.client.emit(event, data=data,\n                                      namespace=namespace or self.namespace,\n                                      callback=callback)\n\n    async def send(self, data, namespace=None, callback=None):\n        \"\"\"Send a message to the server.\n\n        The only difference with the :func:`socketio.Client.send` method is\n        that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await self.client.send(data,\n                                      namespace=namespace or self.namespace,\n                                      callback=callback)\n\n    async def disconnect(self):\n        \"\"\"Disconnect a client.\n\n        The only difference with the :func:`socketio.Client.disconnect` method\n        is that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await self.client.disconnect()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/asyncio_pubsub_manager.py",
    "content": "from functools import partial\nimport uuid\n\nimport json\nimport pickle\nimport six\n\nfrom .asyncio_manager import AsyncManager\n\n\nclass AsyncPubSubManager(AsyncManager):\n    \"\"\"Manage a client list attached to a pub/sub backend under asyncio.\n\n    This is a base class that enables multiple servers to share the list of\n    clients, with the servers communicating events through a pub/sub backend.\n    The use of a pub/sub backend also allows any client connected to the\n    backend to emit events addressed to Socket.IO clients.\n\n    The actual backends must be implemented by subclasses, this class only\n    provides a pub/sub generic framework for asyncio applications.\n\n    :param channel: The channel name on which the server sends and receives\n                    notifications.\n    \"\"\"\n    name = 'asyncpubsub'\n\n    def __init__(self, channel='socketio', write_only=False, logger=None):\n        super().__init__()\n        self.channel = channel\n        self.write_only = write_only\n        self.host_id = uuid.uuid4().hex\n        self.logger = logger\n\n    def initialize(self):\n        super().initialize()\n        if not self.write_only:\n            self.thread = self.server.start_background_task(self._thread)\n        self._get_logger().info(self.name + ' backend initialized.')\n\n    async def emit(self, event, data, namespace=None, room=None, skip_sid=None,\n                   callback=None, **kwargs):\n        \"\"\"Emit a message to a single client, a room, or all the clients\n        connected to the namespace.\n\n        This method takes care or propagating the message to all the servers\n        that are connected through the message queue.\n\n        The parameters are the same as in :meth:`.Server.emit`.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        if kwargs.get('ignore_queue'):\n            return await super().emit(\n                event, data, namespace=namespace, room=room, skip_sid=skip_sid,\n                callback=callback)\n        namespace = namespace or '/'\n        if callback is not None:\n            if self.server is None:\n                raise RuntimeError('Callbacks can only be issued from the '\n                                   'context of a server.')\n            if room is None:\n                raise ValueError('Cannot use callback without a room set.')\n            id = self._generate_ack_id(room, namespace, callback)\n            callback = (room, namespace, id)\n        else:\n            callback = None\n        await self._publish({'method': 'emit', 'event': event, 'data': data,\n                             'namespace': namespace, 'room': room,\n                             'skip_sid': skip_sid, 'callback': callback,\n                             'host_id': self.host_id})\n\n    async def close_room(self, room, namespace=None):\n        await self._publish({'method': 'close_room', 'room': room,\n                             'namespace': namespace or '/'})\n\n    async def _publish(self, data):\n        \"\"\"Publish a message on the Socket.IO channel.\n\n        This method needs to be implemented by the different subclasses that\n        support pub/sub backends.\n        \"\"\"\n        raise NotImplementedError('This method must be implemented in a '\n                                  'subclass.')  # pragma: no cover\n\n    async def _listen(self):\n        \"\"\"Return the next message published on the Socket.IO channel,\n        blocking until a message is available.\n\n        This method needs to be implemented by the different subclasses that\n        support pub/sub backends.\n        \"\"\"\n        raise NotImplementedError('This method must be implemented in a '\n                                  'subclass.')  # pragma: no cover\n\n    async def _handle_emit(self, message):\n        # Events with callbacks are very tricky to handle across hosts\n        # Here in the receiving end we set up a local callback that preserves\n        # the callback host and id from the sender\n        remote_callback = message.get('callback')\n        remote_host_id = message.get('host_id')\n        if remote_callback is not None and len(remote_callback) == 3:\n            callback = partial(self._return_callback, remote_host_id,\n                               *remote_callback)\n        else:\n            callback = None\n        await super().emit(message['event'], message['data'],\n                           namespace=message.get('namespace'),\n                           room=message.get('room'),\n                           skip_sid=message.get('skip_sid'),\n                           callback=callback)\n\n    async def _handle_callback(self, message):\n        if self.host_id == message.get('host_id'):\n            try:\n                sid = message['sid']\n                namespace = message['namespace']\n                id = message['id']\n                args = message['args']\n            except KeyError:\n                return\n            await self.trigger_callback(sid, namespace, id, args)\n\n    async def _return_callback(self, host_id, sid, namespace, callback_id,\n                               *args):\n        # When an event callback is received, the callback is returned back\n        # the sender, which is identified by the host_id\n        await self._publish({'method': 'callback', 'host_id': host_id,\n                             'sid': sid, 'namespace': namespace,\n                             'id': callback_id, 'args': args})\n\n    async def _handle_close_room(self, message):\n        await super().close_room(\n            room=message.get('room'), namespace=message.get('namespace'))\n\n    async def _thread(self):\n        while True:\n            try:\n                message = await self._listen()\n            except:\n                import traceback\n                traceback.print_exc()\n                break\n            data = None\n            if isinstance(message, dict):\n                data = message\n            else:\n                if isinstance(message, six.binary_type):  # pragma: no cover\n                    try:\n                        data = pickle.loads(message)\n                    except:\n                        pass\n                if data is None:\n                    try:\n                        data = json.loads(message)\n                    except:\n                        pass\n            if data and 'method' in data:\n                if data['method'] == 'emit':\n                    await self._handle_emit(data)\n                elif data['method'] == 'callback':\n                    await self._handle_callback(data)\n                elif data['method'] == 'close_room':\n                    await self._handle_close_room(data)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/asyncio_redis_manager.py",
    "content": "import asyncio\nimport pickle\nfrom urllib.parse import urlparse\n\ntry:\n    import aioredis\nexcept ImportError:\n    aioredis = None\n\nfrom .asyncio_pubsub_manager import AsyncPubSubManager\n\n\ndef _parse_redis_url(url):\n    p = urlparse(url)\n    if p.scheme not in {'redis', 'rediss'}:\n        raise ValueError('Invalid redis url')\n    ssl = p.scheme == 'rediss'\n    host = p.hostname or 'localhost'\n    port = p.port or 6379\n    password = p.password\n    if p.path:\n        db = int(p.path[1:])\n    else:\n        db = 0\n    return host, port, password, db, ssl\n\n\nclass AsyncRedisManager(AsyncPubSubManager):  # pragma: no cover\n    \"\"\"Redis based client manager for asyncio servers.\n\n    This class implements a Redis backend for event sharing across multiple\n    processes. Only kept here as one more example of how to build a custom\n    backend, since the kombu backend is perfectly adequate to support a Redis\n    message queue.\n\n    To use a Redis backend, initialize the :class:`Server` instance as\n    follows::\n\n        server = socketio.Server(client_manager=socketio.AsyncRedisManager(\n            'redis://hostname:port/0'))\n\n    :param url: The connection URL for the Redis server. For a default Redis\n                store running on the same host, use ``redis://``.  To use an\n                SSL connection, use ``rediss://``.\n    :param channel: The channel name on which the server sends and receives\n                    notifications. Must be the same in all the servers.\n    :param write_only: If set ot ``True``, only initialize to emit events. The\n                       default of ``False`` initializes the class for emitting\n                       and receiving.\n    \"\"\"\n    name = 'aioredis'\n\n    def __init__(self, url='redis://localhost:6379/0', channel='socketio',\n                 write_only=False, logger=None):\n        if aioredis is None:\n            raise RuntimeError('Redis package is not installed '\n                               '(Run \"pip install aioredis\" in your '\n                               'virtualenv).')\n        (\n            self.host, self.port, self.password, self.db, self.ssl\n        ) = _parse_redis_url(url)\n        self.pub = None\n        self.sub = None\n        super().__init__(channel=channel, write_only=write_only, logger=logger)\n\n    async def _publish(self, data):\n        retry = True\n        while True:\n            try:\n                if self.pub is None:\n                    self.pub = await aioredis.create_redis(\n                        (self.host, self.port), db=self.db,\n                        password=self.password, ssl=self.ssl\n                    )\n                return await self.pub.publish(self.channel,\n                                              pickle.dumps(data))\n            except (aioredis.RedisError, OSError):\n                if retry:\n                    self._get_logger().error('Cannot publish to redis... '\n                                             'retrying')\n                    self.pub = None\n                    retry = False\n                else:\n                    self._get_logger().error('Cannot publish to redis... '\n                                             'giving up')\n                    break\n\n    async def _listen(self):\n        retry_sleep = 1\n        while True:\n            try:\n                if self.sub is None:\n                    self.sub = await aioredis.create_redis(\n                        (self.host, self.port), db=self.db,\n                        password=self.password, ssl=self.ssl\n                    )\n                self.ch = (await self.sub.subscribe(self.channel))[0]\n                return await self.ch.get()\n            except (aioredis.RedisError, OSError):\n                self._get_logger().error('Cannot receive from redis... '\n                                         'retrying in '\n                                         '{} secs'.format(retry_sleep))\n                self.sub = None\n                await asyncio.sleep(retry_sleep)\n                retry_sleep *= 2\n                if retry_sleep > 60:\n                    retry_sleep = 60\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/asyncio_server.py",
    "content": "import asyncio\n\nimport engineio\nimport six\n\nfrom . import asyncio_manager\nfrom . import exceptions\nfrom . import packet\nfrom . import server\n\n\nclass AsyncServer(server.Server):\n    \"\"\"A Socket.IO server for asyncio.\n\n    This class implements a fully compliant Socket.IO web server with support\n    for websocket and long-polling transports, compatible with the asyncio\n    framework on Python 3.5 or newer.\n\n    :param client_manager: The client manager instance that will manage the\n                           client list. When this is omitted, the client list\n                           is stored in an in-memory structure, so the use of\n                           multiple connected servers is not possible.\n    :param logger: To enable logging set to ``True`` or pass a logger object to\n                   use. To disable logging set to ``False``.\n    :param json: An alternative json module to use for encoding and decoding\n                 packets. Custom json modules must have ``dumps`` and ``loads``\n                 functions that are compatible with the standard library\n                 versions.\n    :param async_handlers: If set to ``True``, event handlers are executed in\n                           separate threads. To run handlers synchronously,\n                           set to ``False``. The default is ``True``.\n    :param kwargs: Connection parameters for the underlying Engine.IO server.\n\n    The Engine.IO configuration supports the following settings:\n\n    :param async_mode: The asynchronous model to use. See the Deployment\n                       section in the documentation for a description of the\n                       available options. Valid async modes are \"aiohttp\". If\n                       this argument is not given, an async mode is chosen\n                       based on the installed packages.\n    :param ping_timeout: The time in seconds that the client waits for the\n                         server to respond before disconnecting.\n    :param ping_interval: The interval in seconds at which the client pings\n                          the server.\n    :param max_http_buffer_size: The maximum size of a message when using the\n                                 polling transport.\n    :param allow_upgrades: Whether to allow transport upgrades or not.\n    :param http_compression: Whether to compress packages when using the\n                             polling transport.\n    :param compression_threshold: Only compress messages when their byte size\n                                  is greater than this value.\n    :param cookie: Name of the HTTP cookie that contains the client session\n                   id. If set to ``None``, a cookie is not sent to the client.\n    :param cors_allowed_origins: List of origins that are allowed to connect\n                                 to this server. All origins are allowed by\n                                 default.\n    :param cors_credentials: Whether credentials (cookies, authentication) are\n                             allowed in requests to this server.\n    :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass\n                            a logger object to use. To disable logging set to\n                            ``False``.\n    \"\"\"\n    def __init__(self, client_manager=None, logger=False, json=None,\n                 async_handlers=True, **kwargs):\n        if client_manager is None:\n            client_manager = asyncio_manager.AsyncManager()\n        super().__init__(client_manager=client_manager, logger=logger,\n                         binary=False, json=json,\n                         async_handlers=async_handlers, **kwargs)\n\n    def is_asyncio_based(self):\n        return True\n\n    def attach(self, app, socketio_path='socket.io'):\n        \"\"\"Attach the Socket.IO server to an application.\"\"\"\n        self.eio.attach(app, socketio_path)\n\n    async def emit(self, event, data=None, to=None, room=None, skip_sid=None,\n                   namespace=None, callback=None, **kwargs):\n        \"\"\"Emit a custom event to one or more connected clients.\n\n        :param event: The event name. It can be any string. The event names\n                      ``'connect'``, ``'message'`` and ``'disconnect'`` are\n                      reserved and should not be used.\n        :param data: The data to send to the client or clients. Data can be of\n                     type ``str``, ``bytes``, ``list`` or ``dict``. If a\n                     ``list`` or ``dict``, the data will be serialized as JSON.\n        :param to: The recipient of the message. This can be set to the\n                   session ID of a client to address only that client, or to\n                   to any custom room created by the application to address all\n                   the clients in that room, If this argument is omitted the\n                   event is broadcasted to all connected clients.\n        :param room: Alias for the ``to`` parameter.\n        :param skip_sid: The session ID of a client to skip when broadcasting\n                         to a room or to all clients. This can be used to\n                         prevent a message from being sent to the sender.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the event is emitted to the\n                          default namespace.\n        :param callback: If given, this function will be called to acknowledge\n                         the the client has received the message. The arguments\n                         that will be passed to the function are those provided\n                         by the client. Callback functions can only be used\n                         when addressing an individual client.\n        :param ignore_queue: Only used when a message queue is configured. If\n                             set to ``True``, the event is emitted to the\n                             clients directly, without going through the queue.\n                             This is more efficient, but only works when a\n                             single server process is used. It is recommended\n                             to always leave this parameter with its default\n                             value of ``False``.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        namespace = namespace or '/'\n        room = to or room\n        self.logger.info('emitting event \"%s\" to %s [%s]', event,\n                         room or 'all', namespace)\n        await self.manager.emit(event, data, namespace, room=room,\n                                skip_sid=skip_sid, callback=callback,\n                                **kwargs)\n\n    async def send(self, data, to=None, room=None, skip_sid=None,\n                   namespace=None, callback=None, **kwargs):\n        \"\"\"Send a message to one or more connected clients.\n\n        This function emits an event with the name ``'message'``. Use\n        :func:`emit` to issue custom event names.\n\n        :param data: The data to send to the client or clients. Data can be of\n                     type ``str``, ``bytes``, ``list`` or ``dict``. If a\n                     ``list`` or ``dict``, the data will be serialized as JSON.\n        :param to: The recipient of the message. This can be set to the\n                   session ID of a client to address only that client, or to\n                   to any custom room created by the application to address all\n                   the clients in that room, If this argument is omitted the\n                   event is broadcasted to all connected clients.\n        :param room: Alias for the ``to`` parameter.\n        :param skip_sid: The session ID of a client to skip when broadcasting\n                         to a room or to all clients. This can be used to\n                         prevent a message from being sent to the sender.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the event is emitted to the\n                          default namespace.\n        :param callback: If given, this function will be called to acknowledge\n                         the the client has received the message. The arguments\n                         that will be passed to the function are those provided\n                         by the client. Callback functions can only be used\n                         when addressing an individual client.\n        :param ignore_queue: Only used when a message queue is configured. If\n                             set to ``True``, the event is emitted to the\n                             clients directly, without going through the queue.\n                             This is more efficient, but only works when a\n                             single server process is used. It is recommended\n                             to always leave this parameter with its default\n                             value of ``False``.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        await self.emit('message', data=data, to=to, room=room,\n                        skip_sid=skip_sid, namespace=namespace,\n                        callback=callback, **kwargs)\n\n    async def call(self, event, data=None, to=None, sid=None, namespace=None,\n                   timeout=60, **kwargs):\n        \"\"\"Emit a custom event to a client and wait for the response.\n\n        :param event: The event name. It can be any string. The event names\n                      ``'connect'``, ``'message'`` and ``'disconnect'`` are\n                      reserved and should not be used.\n        :param data: The data to send to the client or clients. Data can be of\n                     type ``str``, ``bytes``, ``list`` or ``dict``. If a\n                     ``list`` or ``dict``, the data will be serialized as JSON.\n        :param to: The session ID of the recipient client.\n        :param sid: Alias for the ``to`` parameter.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the event is emitted to the\n                          default namespace.\n        :param timeout: The waiting timeout. If the timeout is reached before\n                        the client acknowledges the event, then a\n                        ``TimeoutError`` exception is raised.\n        :param ignore_queue: Only used when a message queue is configured. If\n                             set to ``True``, the event is emitted to the\n                             client directly, without going through the queue.\n                             This is more efficient, but only works when a\n                             single server process is used. It is recommended\n                             to always leave this parameter with its default\n                             value of ``False``.\n        \"\"\"\n        if not self.async_handlers:\n            raise RuntimeError(\n                'Cannot use call() when async_handlers is False.')\n        callback_event = self.eio.create_event()\n        callback_args = []\n\n        def event_callback(*args):\n            callback_args.append(args)\n            callback_event.set()\n\n        await self.emit(event, data=data, room=to or sid, namespace=namespace,\n                        callback=event_callback, **kwargs)\n        try:\n            await asyncio.wait_for(callback_event.wait(), timeout)\n        except asyncio.TimeoutError:\n            six.raise_from(exceptions.TimeoutError(), None)\n        return callback_args[0] if len(callback_args[0]) > 1 \\\n            else callback_args[0][0] if len(callback_args[0]) == 1 \\\n            else None\n\n    async def close_room(self, room, namespace=None):\n        \"\"\"Close a room.\n\n        This function removes all the clients from the given room.\n\n        :param room: Room name.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the default namespace is used.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        namespace = namespace or '/'\n        self.logger.info('room %s is closing [%s]', room, namespace)\n        await self.manager.close_room(room, namespace)\n\n    async def get_session(self, sid, namespace=None):\n        \"\"\"Return the user session for a client.\n\n        :param sid: The session id of the client.\n        :param namespace: The Socket.IO namespace. If this argument is omitted\n                          the default namespace is used.\n\n        The return value is a dictionary. Modifications made to this\n        dictionary are not guaranteed to be preserved. If you want to modify\n        the user session, use the ``session`` context manager instead.\n        \"\"\"\n        namespace = namespace or '/'\n        eio_session = await self.eio.get_session(sid)\n        return eio_session.setdefault(namespace, {})\n\n    async def save_session(self, sid, session, namespace=None):\n        \"\"\"Store the user session for a client.\n\n        :param sid: The session id of the client.\n        :param session: The session dictionary.\n        :param namespace: The Socket.IO namespace. If this argument is omitted\n                          the default namespace is used.\n        \"\"\"\n        namespace = namespace or '/'\n        eio_session = await self.eio.get_session(sid)\n        eio_session[namespace] = session\n\n    def session(self, sid, namespace=None):\n        \"\"\"Return the user session for a client with context manager syntax.\n\n        :param sid: The session id of the client.\n\n        This is a context manager that returns the user session dictionary for\n        the client. Any changes that are made to this dictionary inside the\n        context manager block are saved back to the session. Example usage::\n\n            @eio.on('connect')\n            def on_connect(sid, environ):\n                username = authenticate_user(environ)\n                if not username:\n                    return False\n                with eio.session(sid) as session:\n                    session['username'] = username\n\n            @eio.on('message')\n            def on_message(sid, msg):\n                async with eio.session(sid) as session:\n                    print('received message from ', session['username'])\n        \"\"\"\n        class _session_context_manager(object):\n            def __init__(self, server, sid, namespace):\n                self.server = server\n                self.sid = sid\n                self.namespace = namespace\n                self.session = None\n\n            async def __aenter__(self):\n                self.session = await self.server.get_session(\n                    sid, namespace=self.namespace)\n                return self.session\n\n            async def __aexit__(self, *args):\n                await self.server.save_session(sid, self.session,\n                                               namespace=self.namespace)\n\n        return _session_context_manager(self, sid, namespace)\n\n    async def disconnect(self, sid, namespace=None):\n        \"\"\"Disconnect a client.\n\n        :param sid: Session ID of the client.\n        :param namespace: The Socket.IO namespace to disconnect. If this\n                          argument is omitted the default namespace is used.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        namespace = namespace or '/'\n        if self.manager.is_connected(sid, namespace=namespace):\n            self.logger.info('Disconnecting %s [%s]', sid, namespace)\n            self.manager.pre_disconnect(sid, namespace=namespace)\n            await self._send_packet(sid, packet.Packet(packet.DISCONNECT,\n                                                       namespace=namespace))\n            await self._trigger_event('disconnect', namespace, sid)\n            self.manager.disconnect(sid, namespace=namespace)\n\n    async def handle_request(self, *args, **kwargs):\n        \"\"\"Handle an HTTP request from the client.\n\n        This is the entry point of the Socket.IO application. This function\n        returns the HTTP response body to deliver to the client.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await self.eio.handle_request(*args, **kwargs)\n\n    def start_background_task(self, target, *args, **kwargs):\n        \"\"\"Start a background task using the appropriate async model.\n\n        This is a utility function that applications can use to start a\n        background task using the method that is compatible with the\n        selected async mode.\n\n        :param target: the target function to execute. Must be a coroutine.\n        :param args: arguments to pass to the function.\n        :param kwargs: keyword arguments to pass to the function.\n\n        The return value is a ``asyncio.Task`` object.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return self.eio.start_background_task(target, *args, **kwargs)\n\n    async def sleep(self, seconds=0):\n        \"\"\"Sleep for the requested amount of time using the appropriate async\n        model.\n\n        This is a utility function that applications can use to put a task to\n        sleep without having to worry about using the correct call for the\n        selected async mode.\n\n        Note: this method is a coroutine.\n        \"\"\"\n        return await self.eio.sleep(seconds)\n\n    async def _emit_internal(self, sid, event, data, namespace=None, id=None):\n        \"\"\"Send a message to a client.\"\"\"\n        # tuples are expanded to multiple arguments, everything else is sent\n        # as a single argument\n        if isinstance(data, tuple):\n            data = list(data)\n        else:\n            data = [data]\n        await self._send_packet(sid, packet.Packet(\n            packet.EVENT, namespace=namespace, data=[event] + data, id=id,\n            binary=None))\n\n    async def _send_packet(self, sid, pkt):\n        \"\"\"Send a Socket.IO packet to a client.\"\"\"\n        encoded_packet = pkt.encode()\n        if isinstance(encoded_packet, list):\n            binary = False\n            for ep in encoded_packet:\n                await self.eio.send(sid, ep, binary=binary)\n                binary = True\n        else:\n            await self.eio.send(sid, encoded_packet, binary=False)\n\n    async def _handle_connect(self, sid, namespace):\n        \"\"\"Handle a client connection request.\"\"\"\n        namespace = namespace or '/'\n        self.manager.connect(sid, namespace)\n        if self.always_connect:\n            await self._send_packet(sid, packet.Packet(packet.CONNECT,\n                                                       namespace=namespace))\n        fail_reason = None\n        try:\n            success = await self._trigger_event('connect', namespace, sid,\n                                                self.environ[sid])\n        except exceptions.ConnectionRefusedError as exc:\n            fail_reason = exc.error_args\n            success = False\n\n        if success is False:\n            if self.always_connect:\n                self.manager.pre_disconnect(sid, namespace)\n                await self._send_packet(sid, packet.Packet(\n                    packet.DISCONNECT, data=fail_reason, namespace=namespace))\n            self.manager.disconnect(sid, namespace)\n            if not self.always_connect:\n                await self._send_packet(sid, packet.Packet(\n                    packet.ERROR, data=fail_reason, namespace=namespace))\n            if sid in self.environ:  # pragma: no cover\n                del self.environ[sid]\n            return False\n        elif not self.always_connect:\n            await self._send_packet(sid, packet.Packet(packet.CONNECT,\n                                                       namespace=namespace))\n\n    async def _handle_disconnect(self, sid, namespace):\n        \"\"\"Handle a client disconnect.\"\"\"\n        namespace = namespace or '/'\n        if namespace == '/':\n            namespace_list = list(self.manager.get_namespaces())\n        else:\n            namespace_list = [namespace]\n        for n in namespace_list:\n            if n != '/' and self.manager.is_connected(sid, n):\n                await self._trigger_event('disconnect', n, sid)\n                self.manager.disconnect(sid, n)\n        if namespace == '/' and self.manager.is_connected(sid, namespace):\n            await self._trigger_event('disconnect', '/', sid)\n            self.manager.disconnect(sid, '/')\n\n    async def _handle_event(self, sid, namespace, id, data):\n        \"\"\"Handle an incoming client event.\"\"\"\n        namespace = namespace or '/'\n        self.logger.info('received event \"%s\" from %s [%s]', data[0], sid,\n                         namespace)\n        if self.async_handlers:\n            self.start_background_task(self._handle_event_internal, self, sid,\n                                       data, namespace, id)\n        else:\n            await self._handle_event_internal(self, sid, data, namespace, id)\n\n    async def _handle_event_internal(self, server, sid, data, namespace, id):\n        r = await server._trigger_event(data[0], namespace, sid, *data[1:])\n        if id is not None:\n            # send ACK packet with the response returned by the handler\n            # tuples are expanded as multiple arguments\n            if r is None:\n                data = []\n            elif isinstance(r, tuple):\n                data = list(r)\n            else:\n                data = [r]\n            await server._send_packet(sid, packet.Packet(packet.ACK,\n                                                         namespace=namespace,\n                                                         id=id, data=data,\n                                                         binary=None))\n\n    async def _handle_ack(self, sid, namespace, id, data):\n        \"\"\"Handle ACK packets from the client.\"\"\"\n        namespace = namespace or '/'\n        self.logger.info('received ack from %s [%s]', sid, namespace)\n        await self.manager.trigger_callback(sid, namespace, id, data)\n\n    async def _trigger_event(self, event, namespace, *args):\n        \"\"\"Invoke an application event handler.\"\"\"\n        # first see if we have an explicit handler for the event\n        if namespace in self.handlers and event in self.handlers[namespace]:\n            if asyncio.iscoroutinefunction(self.handlers[namespace][event]) \\\n                    is True:\n                try:\n                    ret = await self.handlers[namespace][event](*args)\n                except asyncio.CancelledError:  # pragma: no cover\n                    ret = None\n            else:\n                ret = self.handlers[namespace][event](*args)\n            return ret\n\n        # or else, forward the event to a namepsace handler if one exists\n        elif namespace in self.namespace_handlers:\n            return await self.namespace_handlers[namespace].trigger_event(\n                event, *args)\n\n    async def _handle_eio_connect(self, sid, environ):\n        \"\"\"Handle the Engine.IO connection event.\"\"\"\n        if not self.manager_initialized:\n            self.manager_initialized = True\n            self.manager.initialize()\n        self.environ[sid] = environ\n        return await self._handle_connect(sid, '/')\n\n    async def _handle_eio_message(self, sid, data):\n        \"\"\"Dispatch Engine.IO messages.\"\"\"\n        if sid in self._binary_packet:\n            pkt = self._binary_packet[sid]\n            if pkt.add_attachment(data):\n                del self._binary_packet[sid]\n                if pkt.packet_type == packet.BINARY_EVENT:\n                    await self._handle_event(sid, pkt.namespace, pkt.id,\n                                             pkt.data)\n                else:\n                    await self._handle_ack(sid, pkt.namespace, pkt.id,\n                                           pkt.data)\n        else:\n            pkt = packet.Packet(encoded_packet=data)\n            if pkt.packet_type == packet.CONNECT:\n                await self._handle_connect(sid, pkt.namespace)\n            elif pkt.packet_type == packet.DISCONNECT:\n                await self._handle_disconnect(sid, pkt.namespace)\n            elif pkt.packet_type == packet.EVENT:\n                await self._handle_event(sid, pkt.namespace, pkt.id, pkt.data)\n            elif pkt.packet_type == packet.ACK:\n                await self._handle_ack(sid, pkt.namespace, pkt.id, pkt.data)\n            elif pkt.packet_type == packet.BINARY_EVENT or \\\n                    pkt.packet_type == packet.BINARY_ACK:\n                self._binary_packet[sid] = pkt\n            elif pkt.packet_type == packet.ERROR:\n                raise ValueError('Unexpected ERROR packet.')\n            else:\n                raise ValueError('Unknown packet type.')\n\n    async def _handle_eio_disconnect(self, sid):\n        \"\"\"Handle Engine.IO disconnect event.\"\"\"\n        await self._handle_disconnect(sid, '/')\n        if sid in self.environ:\n            del self.environ[sid]\n\n    def _engineio_server_class(self):\n        return engineio.AsyncServer\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/base_manager.py",
    "content": "import itertools\nimport logging\n\nimport six\n\ndefault_logger = logging.getLogger('socketio')\n\n\nclass BaseManager(object):\n    \"\"\"Manage client connections.\n\n    This class keeps track of all the clients and the rooms they are in, to\n    support the broadcasting of messages. The data used by this class is\n    stored in a memory structure, making it appropriate only for single process\n    services. More sophisticated storage backends can be implemented by\n    subclasses.\n    \"\"\"\n    def __init__(self):\n        self.logger = None\n        self.server = None\n        self.rooms = {}\n        self.callbacks = {}\n        self.pending_disconnect = {}\n\n    def set_server(self, server):\n        self.server = server\n\n    def initialize(self):\n        \"\"\"Invoked before the first request is received. Subclasses can add\n        their initialization code here.\n        \"\"\"\n        pass\n\n    def get_namespaces(self):\n        \"\"\"Return an iterable with the active namespace names.\"\"\"\n        return six.iterkeys(self.rooms)\n\n    def get_participants(self, namespace, room):\n        \"\"\"Return an iterable with the active participants in a room.\"\"\"\n        for sid, active in six.iteritems(self.rooms[namespace][room].copy()):\n            yield sid\n\n    def connect(self, sid, namespace):\n        \"\"\"Register a client connection to a namespace.\"\"\"\n        self.enter_room(sid, namespace, None)\n        self.enter_room(sid, namespace, sid)\n\n    def is_connected(self, sid, namespace):\n        if namespace in self.pending_disconnect and \\\n                sid in self.pending_disconnect[namespace]:\n            # the client is in the process of being disconnected\n            return False\n        try:\n            return self.rooms[namespace][None][sid]\n        except KeyError:\n            pass\n\n    def pre_disconnect(self, sid, namespace):\n        \"\"\"Put the client in the to-be-disconnected list.\n\n        This allows the client data structures to be present while the\n        disconnect handler is invoked, but still recognize the fact that the\n        client is soon going away.\n        \"\"\"\n        if namespace not in self.pending_disconnect:\n            self.pending_disconnect[namespace] = []\n        self.pending_disconnect[namespace].append(sid)\n\n    def disconnect(self, sid, namespace):\n        \"\"\"Register a client disconnect from a namespace.\"\"\"\n        if namespace not in self.rooms:\n            return\n        rooms = []\n        for room_name, room in six.iteritems(self.rooms[namespace].copy()):\n            if sid in room:\n                rooms.append(room_name)\n        for room in rooms:\n            self.leave_room(sid, namespace, room)\n        if sid in self.callbacks and namespace in self.callbacks[sid]:\n            del self.callbacks[sid][namespace]\n            if len(self.callbacks[sid]) == 0:\n                del self.callbacks[sid]\n        if namespace in self.pending_disconnect and \\\n                sid in self.pending_disconnect[namespace]:\n            self.pending_disconnect[namespace].remove(sid)\n            if len(self.pending_disconnect[namespace]) == 0:\n                del self.pending_disconnect[namespace]\n\n    def enter_room(self, sid, namespace, room):\n        \"\"\"Add a client to a room.\"\"\"\n        if namespace not in self.rooms:\n            self.rooms[namespace] = {}\n        if room not in self.rooms[namespace]:\n            self.rooms[namespace][room] = {}\n        self.rooms[namespace][room][sid] = True\n\n    def leave_room(self, sid, namespace, room):\n        \"\"\"Remove a client from a room.\"\"\"\n        try:\n            del self.rooms[namespace][room][sid]\n            if len(self.rooms[namespace][room]) == 0:\n                del self.rooms[namespace][room]\n                if len(self.rooms[namespace]) == 0:\n                    del self.rooms[namespace]\n        except KeyError:\n            pass\n\n    def close_room(self, room, namespace):\n        \"\"\"Remove all participants from a room.\"\"\"\n        try:\n            for sid in self.get_participants(namespace, room):\n                self.leave_room(sid, namespace, room)\n        except KeyError:\n            pass\n\n    def get_rooms(self, sid, namespace):\n        \"\"\"Return the rooms a client is in.\"\"\"\n        r = []\n        try:\n            for room_name, room in six.iteritems(self.rooms[namespace]):\n                if room_name is not None and sid in room and room[sid]:\n                    r.append(room_name)\n        except KeyError:\n            pass\n        return r\n\n    def emit(self, event, data, namespace, room=None, skip_sid=None,\n             callback=None, **kwargs):\n        \"\"\"Emit a message to a single client, a room, or all the clients\n        connected to the namespace.\"\"\"\n        if namespace not in self.rooms or room not in self.rooms[namespace]:\n            return\n        if not isinstance(skip_sid, list):\n            skip_sid = [skip_sid]\n        for sid in self.get_participants(namespace, room):\n            if sid not in skip_sid:\n                if callback is not None:\n                    id = self._generate_ack_id(sid, namespace, callback)\n                else:\n                    id = None\n                self.server._emit_internal(sid, event, data, namespace, id)\n\n    def trigger_callback(self, sid, namespace, id, data):\n        \"\"\"Invoke an application callback.\"\"\"\n        callback = None\n        try:\n            callback = self.callbacks[sid][namespace][id]\n        except KeyError:\n            # if we get an unknown callback we just ignore it\n            self._get_logger().warning('Unknown callback received, ignoring.')\n        else:\n            del self.callbacks[sid][namespace][id]\n        if callback is not None:\n            callback(*data)\n\n    def _generate_ack_id(self, sid, namespace, callback):\n        \"\"\"Generate a unique identifier for an ACK packet.\"\"\"\n        namespace = namespace or '/'\n        if sid not in self.callbacks:\n            self.callbacks[sid] = {}\n        if namespace not in self.callbacks[sid]:\n            self.callbacks[sid][namespace] = {0: itertools.count(1)}\n        id = six.next(self.callbacks[sid][namespace][0])\n        self.callbacks[sid][namespace][id] = callback\n        return id\n\n    def _get_logger(self):\n        \"\"\"Get the appropriate logger\n\n        Prevents uninitialized servers in write-only mode from failing.\n        \"\"\"\n\n        if self.logger:\n            return self.logger\n        elif self.server:\n            return self.server.logger\n        else:\n            return default_logger\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/client.py",
    "content": "import itertools\nimport logging\nimport random\nimport signal\n\nimport engineio\nimport six\n\nfrom . import exceptions\nfrom . import namespace\nfrom . import packet\n\ndefault_logger = logging.getLogger('socketio.client')\nreconnecting_clients = []\n\n\ndef signal_handler(sig, frame):  # pragma: no cover\n    \"\"\"SIGINT handler.\n\n    Notify any clients that are in a reconnect loop to abort. Other\n    disconnection tasks are handled at the engine.io level.\n    \"\"\"\n    for client in reconnecting_clients[:]:\n        client._reconnect_abort.set()\n    return original_signal_handler(sig, frame)\n\n\noriginal_signal_handler = signal.signal(signal.SIGINT, signal_handler)\n\n\nclass Client(object):\n    \"\"\"A Socket.IO client.\n\n    This class implements a fully compliant Socket.IO web client with support\n    for websocket and long-polling transports.\n\n    :param reconnection: ``True`` if the client should automatically attempt to\n                         reconnect to the server after an interruption, or\n                         ``False`` to not reconnect. The default is ``True``.\n    :param reconnection_attempts: How many reconnection attempts to issue\n                                  before giving up, or 0 for infinity attempts.\n                                  The default is 0.\n    :param reconnection_delay: How long to wait in seconds before the first\n                               reconnection attempt. Each successive attempt\n                               doubles this delay.\n    :param reconnection_delay_max: The maximum delay between reconnection\n                                   attempts.\n    :param randomization_factor: Randomization amount for each delay between\n                                 reconnection attempts. The default is 0.5,\n                                 which means that each delay is randomly\n                                 adjusted by +/- 50%.\n    :param logger: To enable logging set to ``True`` or pass a logger object to\n                   use. To disable logging set to ``False``. The default is\n                   ``False``.\n    :param binary: ``True`` to support binary payloads, ``False`` to treat all\n                   payloads as text. On Python 2, if this is set to ``True``,\n                   ``unicode`` values are treated as text, and ``str`` and\n                   ``bytes`` values are treated as binary.  This option has no\n                   effect on Python 3, where text and binary payloads are\n                   always automatically discovered.\n    :param json: An alternative json module to use for encoding and decoding\n                 packets. Custom json modules must have ``dumps`` and ``loads``\n                 functions that are compatible with the standard library\n                 versions.\n\n    The Engine.IO configuration supports the following settings:\n\n    :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass\n                            a logger object to use. To disable logging set to\n                            ``False``. The default is ``False``.\n    \"\"\"\n    def __init__(self, reconnection=True, reconnection_attempts=0,\n                 reconnection_delay=1, reconnection_delay_max=5,\n                 randomization_factor=0.5, logger=False, binary=False,\n                 json=None, **kwargs):\n        self.reconnection = reconnection\n        self.reconnection_attempts = reconnection_attempts\n        self.reconnection_delay = reconnection_delay\n        self.reconnection_delay_max = reconnection_delay_max\n        self.randomization_factor = randomization_factor\n        self.binary = binary\n\n        engineio_options = kwargs\n        engineio_logger = engineio_options.pop('engineio_logger', None)\n        if engineio_logger is not None:\n            engineio_options['logger'] = engineio_logger\n        if json is not None:\n            packet.Packet.json = json\n            engineio_options['json'] = json\n\n        self.eio = self._engineio_client_class()(**engineio_options)\n        self.eio.on('connect', self._handle_eio_connect)\n        self.eio.on('message', self._handle_eio_message)\n        self.eio.on('disconnect', self._handle_eio_disconnect)\n\n        if not isinstance(logger, bool):\n            self.logger = logger\n        else:\n            self.logger = default_logger\n            if not logging.root.handlers and \\\n                    self.logger.level == logging.NOTSET:\n                if logger:\n                    self.logger.setLevel(logging.INFO)\n                else:\n                    self.logger.setLevel(logging.ERROR)\n                self.logger.addHandler(logging.StreamHandler())\n\n        self.connection_url = None\n        self.connection_headers = None\n        self.connection_transports = None\n        self.connection_namespaces = None\n        self.socketio_path = None\n        self.sid = None\n\n        self.namespaces = []\n        self.handlers = {}\n        self.namespace_handlers = {}\n        self.callbacks = {}\n        self._binary_packet = None\n        self._reconnect_task = None\n        self._reconnect_abort = self.eio.create_event()\n\n    def is_asyncio_based(self):\n        return False\n\n    def on(self, event, handler=None, namespace=None):\n        \"\"\"Register an event handler.\n\n        :param event: The event name. It can be any string. The event names\n                      ``'connect'``, ``'message'`` and ``'disconnect'`` are\n                      reserved and should not be used.\n        :param handler: The function that should be invoked to handle the\n                        event. When this parameter is not given, the method\n                        acts as a decorator for the handler function.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the handler is associated with\n                          the default namespace.\n\n        Example usage::\n\n            # as a decorator:\n            @sio.on('connect')\n            def connect_handler():\n                print('Connected!')\n\n            # as a method:\n            def message_handler(msg):\n                print('Received message: ', msg)\n                sio.send( 'response')\n            sio.on('message', message_handler)\n\n        The ``'connect'`` event handler receives no arguments. The\n        ``'message'`` handler and handlers for custom event names receive the\n        message payload as only argument. Any values returned from a message\n        handler will be passed to the client's acknowledgement callback\n        function if it exists. The ``'disconnect'`` handler does not take\n        arguments.\n        \"\"\"\n        namespace = namespace or '/'\n\n        def set_handler(handler):\n            if namespace not in self.handlers:\n                self.handlers[namespace] = {}\n            self.handlers[namespace][event] = handler\n            return handler\n\n        if handler is None:\n            return set_handler\n        set_handler(handler)\n\n    def event(self, *args, **kwargs):\n        \"\"\"Decorator to register an event handler.\n\n        This is a simplified version of the ``on()`` method that takes the\n        event name from the decorated function.\n\n        Example usage::\n\n            @sio.event\n            def my_event(data):\n                print('Received data: ', data)\n\n        The above example is equivalent to::\n\n            @sio.on('my_event')\n            def my_event(data):\n                print('Received data: ', data)\n\n        A custom namespace can be given as an argument to the decorator::\n\n            @sio.event(namespace='/test')\n            def my_event(data):\n                print('Received data: ', data)\n        \"\"\"\n        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):\n            # the decorator was invoked without arguments\n            # args[0] is the decorated function\n            return self.on(args[0].__name__)(args[0])\n        else:\n            # the decorator was invoked with arguments\n            def set_handler(handler):\n                return self.on(handler.__name__, *args, **kwargs)(handler)\n\n            return set_handler\n\n    def register_namespace(self, namespace_handler):\n        \"\"\"Register a namespace handler object.\n\n        :param namespace_handler: An instance of a :class:`Namespace`\n                                  subclass that handles all the event traffic\n                                  for a namespace.\n        \"\"\"\n        if not isinstance(namespace_handler, namespace.ClientNamespace):\n            raise ValueError('Not a namespace instance')\n        if self.is_asyncio_based() != namespace_handler.is_asyncio_based():\n            raise ValueError('Not a valid namespace class for this client')\n        namespace_handler._set_client(self)\n        self.namespace_handlers[namespace_handler.namespace] = \\\n            namespace_handler\n\n    def connect(self, url, headers={}, transports=None,\n                namespaces=None, socketio_path='socket.io'):\n        \"\"\"Connect to a Socket.IO server.\n\n        :param url: The URL of the Socket.IO server. It can include custom\n                    query string parameters if required by the server.\n        :param headers: A dictionary with custom headers to send with the\n                        connection request.\n        :param transports: The list of allowed transports. Valid transports\n                           are ``'polling'`` and ``'websocket'``. If not\n                           given, the polling transport is connected first,\n                           then an upgrade to websocket is attempted.\n        :param namespaces: The list of custom namespaces to connect, in\n                           addition to the default namespace. If not given,\n                           the namespace list is obtained from the registered\n                           event handlers.\n        :param socketio_path: The endpoint where the Socket.IO server is\n                              installed. The default value is appropriate for\n                              most cases.\n\n        Example usage::\n\n            sio = socketio.Client()\n            sio.connect('http://localhost:5000')\n        \"\"\"\n        self.connection_url = url\n        self.connection_headers = headers\n        self.connection_transports = transports\n        self.connection_namespaces = namespaces\n        self.socketio_path = socketio_path\n\n        if namespaces is None:\n            namespaces = set(self.handlers.keys()).union(\n                set(self.namespace_handlers.keys()))\n        elif isinstance(namespaces, six.string_types):\n            namespaces = [namespaces]\n            self.connection_namespaces = namespaces\n        self.namespaces = [n for n in namespaces if n != '/']\n        try:\n            self.eio.connect(url, headers=headers, transports=transports,\n                             engineio_path=socketio_path)\n        except engineio.exceptions.ConnectionError as exc:\n            six.raise_from(exceptions.ConnectionError(exc.args[0]), None)\n\n    def wait(self):\n        \"\"\"Wait until the connection with the server ends.\n\n        Client applications can use this function to block the main thread\n        during the life of the connection.\n        \"\"\"\n        while True:\n            self.eio.wait()\n            self.sleep(1)  # give the reconnect task time to start up\n            if not self._reconnect_task:\n                break\n            self._reconnect_task.join()\n            if self.eio.state != 'connected':\n                break\n\n    def emit(self, event, data=None, namespace=None, callback=None):\n        \"\"\"Emit a custom event to one or more connected clients.\n\n        :param event: The event name. It can be any string. The event names\n                      ``'connect'``, ``'message'`` and ``'disconnect'`` are\n                      reserved and should not be used.\n        :param data: The data to send to the client or clients. Data can be of\n                     type ``str``, ``bytes``, ``list`` or ``dict``. If a\n                     ``list`` or ``dict``, the data will be serialized as JSON.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the event is emitted to the\n                          default namespace.\n        :param callback: If given, this function will be called to acknowledge\n                         the the client has received the message. The arguments\n                         that will be passed to the function are those provided\n                         by the client. Callback functions can only be used\n                         when addressing an individual client.\n        \"\"\"\n        namespace = namespace or '/'\n        self.logger.info('Emitting event \"%s\" [%s]', event, namespace)\n        if callback is not None:\n            id = self._generate_ack_id(namespace, callback)\n        else:\n            id = None\n        if six.PY2 and not self.binary:\n            binary = False  # pragma: nocover\n        else:\n            binary = None\n        # tuples are expanded to multiple arguments, everything else is sent\n        # as a single argument\n        if isinstance(data, tuple):\n            data = list(data)\n        elif data is not None:\n            data = [data]\n        else:\n            data = []\n        self._send_packet(packet.Packet(packet.EVENT, namespace=namespace,\n                                        data=[event] + data, id=id,\n                                        binary=binary))\n\n    def send(self, data, namespace=None, callback=None):\n        \"\"\"Send a message to one or more connected clients.\n\n        This function emits an event with the name ``'message'``. Use\n        :func:`emit` to issue custom event names.\n\n        :param data: The data to send to the client or clients. Data can be of\n                     type ``str``, ``bytes``, ``list`` or ``dict``. If a\n                     ``list`` or ``dict``, the data will be serialized as JSON.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the event is emitted to the\n                          default namespace.\n        :param callback: If given, this function will be called to acknowledge\n                         the the client has received the message. The arguments\n                         that will be passed to the function are those provided\n                         by the client. Callback functions can only be used\n                         when addressing an individual client.\n        \"\"\"\n        self.emit('message', data=data, namespace=namespace,\n                  callback=callback)\n\n    def call(self, event, data=None, namespace=None, timeout=60):\n        \"\"\"Emit a custom event to a client and wait for the response.\n\n        :param event: The event name. It can be any string. The event names\n                      ``'connect'``, ``'message'`` and ``'disconnect'`` are\n                      reserved and should not be used.\n        :param data: The data to send to the client or clients. Data can be of\n                     type ``str``, ``bytes``, ``list`` or ``dict``. If a\n                     ``list`` or ``dict``, the data will be serialized as JSON.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the event is emitted to the\n                          default namespace.\n        :param timeout: The waiting timeout. If the timeout is reached before\n                        the client acknowledges the event, then a\n                        ``TimeoutError`` exception is raised.\n        \"\"\"\n        callback_event = self.eio.create_event()\n        callback_args = []\n\n        def event_callback(*args):\n            callback_args.append(args)\n            callback_event.set()\n\n        self.emit(event, data=data, namespace=namespace,\n                  callback=event_callback)\n        if not callback_event.wait(timeout=timeout):\n            raise exceptions.TimeoutError()\n        return callback_args[0] if len(callback_args[0]) > 1 \\\n            else callback_args[0][0] if len(callback_args[0]) == 1 \\\n            else None\n\n    def disconnect(self):\n        \"\"\"Disconnect from the server.\"\"\"\n        # here we just request the disconnection\n        # later in _handle_eio_disconnect we invoke the disconnect handler\n        for n in self.namespaces:\n            self._send_packet(packet.Packet(packet.DISCONNECT, namespace=n))\n        self._send_packet(packet.Packet(\n            packet.DISCONNECT, namespace='/'))\n        self.eio.disconnect(abort=True)\n\n    def transport(self):\n        \"\"\"Return the name of the transport used by the client.\n\n        The two possible values returned by this function are ``'polling'``\n        and ``'websocket'``.\n        \"\"\"\n        return self.eio.transport()\n\n    def start_background_task(self, target, *args, **kwargs):\n        \"\"\"Start a background task using the appropriate async model.\n\n        This is a utility function that applications can use to start a\n        background task using the method that is compatible with the\n        selected async mode.\n\n        :param target: the target function to execute.\n        :param args: arguments to pass to the function.\n        :param kwargs: keyword arguments to pass to the function.\n\n        This function returns an object compatible with the `Thread` class in\n        the Python standard library. The `start()` method on this object is\n        already called by this function.\n        \"\"\"\n        return self.eio.start_background_task(target, *args, **kwargs)\n\n    def sleep(self, seconds=0):\n        \"\"\"Sleep for the requested amount of time using the appropriate async\n        model.\n\n        This is a utility function that applications can use to put a task to\n        sleep without having to worry about using the correct call for the\n        selected async mode.\n        \"\"\"\n        return self.eio.sleep(seconds)\n\n    def _send_packet(self, pkt):\n        \"\"\"Send a Socket.IO packet to the server.\"\"\"\n        encoded_packet = pkt.encode()\n        if isinstance(encoded_packet, list):\n            binary = False\n            for ep in encoded_packet:\n                self.eio.send(ep, binary=binary)\n                binary = True\n        else:\n            self.eio.send(encoded_packet, binary=False)\n\n    def _generate_ack_id(self, namespace, callback):\n        \"\"\"Generate a unique identifier for an ACK packet.\"\"\"\n        namespace = namespace or '/'\n        if namespace not in self.callbacks:\n            self.callbacks[namespace] = {0: itertools.count(1)}\n        id = six.next(self.callbacks[namespace][0])\n        self.callbacks[namespace][id] = callback\n        return id\n\n    def _handle_connect(self, namespace):\n        namespace = namespace or '/'\n        self.logger.info('Namespace {} is connected'.format(namespace))\n        self._trigger_event('connect', namespace=namespace)\n        if namespace == '/':\n            for n in self.namespaces:\n                self._send_packet(packet.Packet(packet.CONNECT, namespace=n))\n        elif namespace not in self.namespaces:\n            self.namespaces.append(namespace)\n\n    def _handle_disconnect(self, namespace):\n        namespace = namespace or '/'\n        self._trigger_event('disconnect', namespace=namespace)\n        if namespace in self.namespaces:\n            self.namespaces.remove(namespace)\n\n    def _handle_event(self, namespace, id, data):\n        namespace = namespace or '/'\n        self.logger.info('Received event \"%s\" [%s]', data[0], namespace)\n        r = self._trigger_event(data[0], namespace, *data[1:])\n        if id is not None:\n            # send ACK packet with the response returned by the handler\n            # tuples are expanded as multiple arguments\n            if r is None:\n                data = []\n            elif isinstance(r, tuple):\n                data = list(r)\n            else:\n                data = [r]\n            if six.PY2 and not self.binary:\n                binary = False  # pragma: nocover\n            else:\n                binary = None\n            self._send_packet(packet.Packet(packet.ACK, namespace=namespace,\n                              id=id, data=data, binary=binary))\n\n    def _handle_ack(self, namespace, id, data):\n        namespace = namespace or '/'\n        self.logger.info('Received ack [%s]', namespace)\n        callback = None\n        try:\n            callback = self.callbacks[namespace][id]\n        except KeyError:\n            # if we get an unknown callback we just ignore it\n            self.logger.warning('Unknown callback received, ignoring.')\n        else:\n            del self.callbacks[namespace][id]\n        if callback is not None:\n            callback(*data)\n\n    def _handle_error(self, namespace):\n        namespace = namespace or '/'\n        self.logger.info('Connection to namespace {} was rejected'.format(\n            namespace))\n        if namespace in self.namespaces:\n            self.namespaces.remove(namespace)\n\n    def _trigger_event(self, event, namespace, *args):\n        \"\"\"Invoke an application event handler.\"\"\"\n        # first see if we have an explicit handler for the event\n        if namespace in self.handlers and event in self.handlers[namespace]:\n            return self.handlers[namespace][event](*args)\n\n        # or else, forward the event to a namespace handler if one exists\n        elif namespace in self.namespace_handlers:\n            return self.namespace_handlers[namespace].trigger_event(\n                event, *args)\n\n    def _handle_reconnect(self):\n        self._reconnect_abort.clear()\n        reconnecting_clients.append(self)\n        attempt_count = 0\n        current_delay = self.reconnection_delay\n        while True:\n            delay = current_delay\n            current_delay *= 2\n            if delay > self.reconnection_delay_max:\n                delay = self.reconnection_delay_max\n            delay += self.randomization_factor * (2 * random.random() - 1)\n            self.logger.info(\n                'Connection failed, new attempt in {:.02f} seconds'.format(\n                    delay))\n            print('***', self._reconnect_abort.wait)\n            if self._reconnect_abort.wait(delay):\n                self.logger.info('Reconnect task aborted')\n                break\n            attempt_count += 1\n            try:\n                self.connect(self.connection_url,\n                             headers=self.connection_headers,\n                             transports=self.connection_transports,\n                             namespaces=self.connection_namespaces,\n                             socketio_path=self.socketio_path)\n            except (exceptions.ConnectionError, ValueError):\n                pass\n            else:\n                self.logger.info('Reconnection successful')\n                self._reconnect_task = None\n                break\n            if self.reconnection_attempts and \\\n                    attempt_count >= self.reconnection_attempts:\n                self.logger.info(\n                    'Maximum reconnection attempts reached, giving up')\n                break\n        reconnecting_clients.remove(self)\n\n    def _handle_eio_connect(self):\n        \"\"\"Handle the Engine.IO connection event.\"\"\"\n        self.logger.info('Engine.IO connection established')\n        self.sid = self.eio.sid\n\n    def _handle_eio_message(self, data):\n        \"\"\"Dispatch Engine.IO messages.\"\"\"\n        if self._binary_packet:\n            pkt = self._binary_packet\n            if pkt.add_attachment(data):\n                self._binary_packet = None\n                if pkt.packet_type == packet.BINARY_EVENT:\n                    self._handle_event(pkt.namespace, pkt.id, pkt.data)\n                else:\n                    self._handle_ack(pkt.namespace, pkt.id, pkt.data)\n        else:\n            pkt = packet.Packet(encoded_packet=data)\n            if pkt.packet_type == packet.CONNECT:\n                self._handle_connect(pkt.namespace)\n            elif pkt.packet_type == packet.DISCONNECT:\n                self._handle_disconnect(pkt.namespace)\n            elif pkt.packet_type == packet.EVENT:\n                self._handle_event(pkt.namespace, pkt.id, pkt.data)\n            elif pkt.packet_type == packet.ACK:\n                self._handle_ack(pkt.namespace, pkt.id, pkt.data)\n            elif pkt.packet_type == packet.BINARY_EVENT or \\\n                    pkt.packet_type == packet.BINARY_ACK:\n                self._binary_packet = pkt\n            elif pkt.packet_type == packet.ERROR:\n                self._handle_error(pkt.namespace)\n            else:\n                raise ValueError('Unknown packet type.')\n\n    def _handle_eio_disconnect(self):\n        \"\"\"Handle the Engine.IO disconnection event.\"\"\"\n        self.logger.info('Engine.IO connection dropped')\n        for n in self.namespaces:\n            self._trigger_event('disconnect', namespace=n)\n        self._trigger_event('disconnect', namespace='/')\n        self.callbacks = {}\n        self._binary_packet = None\n        self.sid = None\n        if self.eio.state == 'connected' and self.reconnection:\n            self._reconnect_task = self.start_background_task(\n                self._handle_reconnect)\n\n    def _engineio_client_class(self):\n        return engineio.Client\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/exceptions.py",
    "content": "class SocketIOError(Exception):\n    pass\n\n\nclass ConnectionError(SocketIOError):\n    pass\n\n\nclass ConnectionRefusedError(ConnectionError):\n    \"\"\"Connection refused exception.\n\n    This exception can be raised from a connect handler when the connection\n    is not accepted. The positional arguments provided with the exception are\n    returned with the error packet to the client.\n    \"\"\"\n    def __init__(self, *args):\n        if len(args) == 0:\n            self.error_args = None\n        elif len(args) == 1:\n            self.error_args = args[0]\n        else:\n            self.error_args = args\n\n\nclass TimeoutError(SocketIOError):\n    pass\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/kombu_manager.py",
    "content": "import pickle\nimport uuid\n\ntry:\n    import kombu\nexcept ImportError:\n    kombu = None\n\nfrom .pubsub_manager import PubSubManager\n\n\nclass KombuManager(PubSubManager):  # pragma: no cover\n    \"\"\"Client manager that uses kombu for inter-process messaging.\n\n    This class implements a client manager backend for event sharing across\n    multiple processes, using RabbitMQ, Redis or any other messaging mechanism\n    supported by `kombu <http://kombu.readthedocs.org/en/latest/>`_.\n\n    To use a kombu backend, initialize the :class:`Server` instance as\n    follows::\n\n        url = 'amqp://user:password@hostname:port//'\n        server = socketio.Server(client_manager=socketio.KombuManager(url))\n\n    :param url: The connection URL for the backend messaging queue. Example\n                connection URLs are ``'amqp://guest:guest@localhost:5672//'``\n                and ``'redis://localhost:6379/'`` for RabbitMQ and Redis\n                respectively. Consult the `kombu documentation\n                <http://kombu.readthedocs.org/en/latest/userguide\\\n                /connections.html#urls>`_ for more on how to construct\n                connection URLs.\n    :param channel: The channel name on which the server sends and receives\n                    notifications. Must be the same in all the servers.\n    :param write_only: If set ot ``True``, only initialize to emit events. The\n                       default of ``False`` initializes the class for emitting\n                       and receiving.\n    \"\"\"\n    name = 'kombu'\n\n    def __init__(self, url='amqp://guest:guest@localhost:5672//',\n                 channel='socketio', write_only=False, logger=None):\n        if kombu is None:\n            raise RuntimeError('Kombu package is not installed '\n                               '(Run \"pip install kombu\" in your '\n                               'virtualenv).')\n        super(KombuManager, self).__init__(channel=channel,\n                                           write_only=write_only,\n                                           logger=logger)\n        self.url = url\n        self.producer = self._producer()\n\n    def initialize(self):\n        super(KombuManager, self).initialize()\n\n        monkey_patched = True\n        if self.server.async_mode == 'eventlet':\n            from eventlet.patcher import is_monkey_patched\n            monkey_patched = is_monkey_patched('socket')\n        elif 'gevent' in self.server.async_mode:\n            from gevent.monkey import is_module_patched\n            monkey_patched = is_module_patched('socket')\n        if not monkey_patched:\n            raise RuntimeError(\n                'Kombu requires a monkey patched socket library to work '\n                'with ' + self.server.async_mode)\n\n    def _connection(self):\n        return kombu.Connection(self.url)\n\n    def _exchange(self):\n        return kombu.Exchange(self.channel, type='fanout', durable=False)\n\n    def _queue(self):\n        queue_name = 'flask-socketio.' + str(uuid.uuid4())\n        return kombu.Queue(queue_name, self._exchange(),\n                           durable=False,\n                           queue_arguments={'x-expires': 300000})\n\n    def _producer(self):\n        return self._connection().Producer(exchange=self._exchange())\n\n    def __error_callback(self, exception, interval):\n        self._get_logger().exception('Sleeping {}s'.format(interval))\n\n    def _publish(self, data):\n        connection = self._connection()\n        publish = connection.ensure(self.producer, self.producer.publish,\n                                    errback=self.__error_callback)\n        publish(pickle.dumps(data))\n\n    def _listen(self):\n        reader_queue = self._queue()\n\n        while True:\n            connection = self._connection().ensure_connection(\n                errback=self.__error_callback)\n            try:\n                with connection.SimpleQueue(reader_queue) as queue:\n                    while True:\n                        message = queue.get(block=True)\n                        message.ack()\n                        yield message.payload\n            except connection.connection_errors:\n                self._get_logger().exception(\"Connection error \"\n                                             \"while reading from queue\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/middleware.py",
    "content": "import engineio\n\n\nclass WSGIApp(engineio.WSGIApp):\n    \"\"\"WSGI middleware for Socket.IO.\n\n    This middleware dispatches traffic to a Socket.IO application. It can also\n    serve a list of static files to the client, or forward unrelated HTTP\n    traffic to another WSGI application.\n\n    :param socketio_app: The Socket.IO server. Must be an instance of the\n                         ``socketio.Server`` class.\n    :param wsgi_app: The WSGI app that receives all other traffic.\n    :param static_files: A dictionary with static file mapping rules. See the\n                         documentation for details on this argument.\n    :param socketio_path: The endpoint where the Socket.IO application should\n                          be installed. The default value is appropriate for\n                          most cases.\n\n    Example usage::\n\n        import socketio\n        import eventlet\n        from . import wsgi_app\n\n        sio = socketio.Server()\n        app = socketio.WSGIApp(sio, wsgi_app)\n        eventlet.wsgi.server(eventlet.listen(('', 8000)), app)\n    \"\"\"\n    def __init__(self, socketio_app, wsgi_app=None, static_files=None,\n                 socketio_path='socket.io'):\n        super(WSGIApp, self).__init__(socketio_app, wsgi_app,\n                                      static_files=static_files,\n                                      engineio_path=socketio_path)\n\n\nclass Middleware(WSGIApp):\n    \"\"\"This class has been renamed to WSGIApp and is now deprecated.\"\"\"\n    def __init__(self, socketio_app, wsgi_app=None,\n                 socketio_path='socket.io'):\n        super(Middleware, self).__init__(socketio_app, wsgi_app,\n                                         socketio_path=socketio_path)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/namespace.py",
    "content": "class BaseNamespace(object):\n    def __init__(self, namespace=None):\n        self.namespace = namespace or '/'\n\n    def is_asyncio_based(self):\n        return False\n\n    def trigger_event(self, event, *args):\n        \"\"\"Dispatch an event to the proper handler method.\n\n        In the most common usage, this method is not overloaded by subclasses,\n        as it performs the routing of events to methods. However, this\n        method can be overriden if special dispatching rules are needed, or if\n        having a single method that catches all events is desired.\n        \"\"\"\n        handler_name = 'on_' + event\n        if hasattr(self, handler_name):\n            return getattr(self, handler_name)(*args)\n\n\nclass Namespace(BaseNamespace):\n    \"\"\"Base class for server-side class-based namespaces.\n\n    A class-based namespace is a class that contains all the event handlers\n    for a Socket.IO namespace. The event handlers are methods of the class\n    with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``,\n    ``on_message``, ``on_json``, and so on.\n\n    :param namespace: The Socket.IO namespace to be used with all the event\n                      handlers defined in this class. If this argument is\n                      omitted, the default namespace is used.\n    \"\"\"\n    def __init__(self, namespace=None):\n        super(Namespace, self).__init__(namespace=namespace)\n        self.server = None\n\n    def _set_server(self, server):\n        self.server = server\n\n    def emit(self, event, data=None, room=None, skip_sid=None, namespace=None,\n             callback=None):\n        \"\"\"Emit a custom event to one or more connected clients.\n\n        The only difference with the :func:`socketio.Server.emit` method is\n        that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n        \"\"\"\n        return self.server.emit(event, data=data, room=room, skip_sid=skip_sid,\n                                namespace=namespace or self.namespace,\n                                callback=callback)\n\n    def send(self, data, room=None, skip_sid=None, namespace=None,\n             callback=None):\n        \"\"\"Send a message to one or more connected clients.\n\n        The only difference with the :func:`socketio.Server.send` method is\n        that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n        \"\"\"\n        return self.server.send(data, room=room, skip_sid=skip_sid,\n                                namespace=namespace or self.namespace,\n                                callback=callback)\n\n    def enter_room(self, sid, room, namespace=None):\n        \"\"\"Enter a room.\n\n        The only difference with the :func:`socketio.Server.enter_room` method\n        is that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n        \"\"\"\n        return self.server.enter_room(sid, room,\n                                      namespace=namespace or self.namespace)\n\n    def leave_room(self, sid, room, namespace=None):\n        \"\"\"Leave a room.\n\n        The only difference with the :func:`socketio.Server.leave_room` method\n        is that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n        \"\"\"\n        return self.server.leave_room(sid, room,\n                                      namespace=namespace or self.namespace)\n\n    def close_room(self, room, namespace=None):\n        \"\"\"Close a room.\n\n        The only difference with the :func:`socketio.Server.close_room` method\n        is that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n        \"\"\"\n        return self.server.close_room(room,\n                                      namespace=namespace or self.namespace)\n\n    def rooms(self, sid, namespace=None):\n        \"\"\"Return the rooms a client is in.\n\n        The only difference with the :func:`socketio.Server.rooms` method is\n        that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n        \"\"\"\n        return self.server.rooms(sid, namespace=namespace or self.namespace)\n\n    def get_session(self, sid, namespace=None):\n        \"\"\"Return the user session for a client.\n\n        The only difference with the :func:`socketio.Server.get_session`\n        method is that when the ``namespace`` argument is not given the\n        namespace associated with the class is used.\n        \"\"\"\n        return self.server.get_session(\n            sid, namespace=namespace or self.namespace)\n\n    def save_session(self, sid, session, namespace=None):\n        \"\"\"Store the user session for a client.\n\n        The only difference with the :func:`socketio.Server.save_session`\n        method is that when the ``namespace`` argument is not given the\n        namespace associated with the class is used.\n        \"\"\"\n        return self.server.save_session(\n            sid, session, namespace=namespace or self.namespace)\n\n    def session(self, sid, namespace=None):\n        \"\"\"Return the user session for a client with context manager syntax.\n\n        The only difference with the :func:`socketio.Server.session` method is\n        that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n        \"\"\"\n        return self.server.session(sid, namespace=namespace or self.namespace)\n\n    def disconnect(self, sid, namespace=None):\n        \"\"\"Disconnect a client.\n\n        The only difference with the :func:`socketio.Server.disconnect` method\n        is that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n        \"\"\"\n        return self.server.disconnect(sid,\n                                      namespace=namespace or self.namespace)\n\n\nclass ClientNamespace(BaseNamespace):\n    \"\"\"Base class for client-side class-based namespaces.\n\n    A class-based namespace is a class that contains all the event handlers\n    for a Socket.IO namespace. The event handlers are methods of the class\n    with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``,\n    ``on_message``, ``on_json``, and so on.\n\n    :param namespace: The Socket.IO namespace to be used with all the event\n                      handlers defined in this class. If this argument is\n                      omitted, the default namespace is used.\n    \"\"\"\n    def __init__(self, namespace=None):\n        super(ClientNamespace, self).__init__(namespace=namespace)\n        self.client = None\n\n    def _set_client(self, client):\n        self.client = client\n\n    def emit(self, event, data=None, namespace=None, callback=None):\n        \"\"\"Emit a custom event to the server.\n\n        The only difference with the :func:`socketio.Client.emit` method is\n        that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n        \"\"\"\n        return self.client.emit(event, data=data,\n                                namespace=namespace or self.namespace,\n                                callback=callback)\n\n    def send(self, data, room=None, skip_sid=None, namespace=None,\n             callback=None):\n        \"\"\"Send a message to the server.\n\n        The only difference with the :func:`socketio.Client.send` method is\n        that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n        \"\"\"\n        return self.client.send(data, namespace=namespace or self.namespace,\n                                callback=callback)\n\n    def disconnect(self):\n        \"\"\"Disconnect from the server.\n\n        The only difference with the :func:`socketio.Client.disconnect` method\n        is that when the ``namespace`` argument is not given the namespace\n        associated with the class is used.\n        \"\"\"\n        return self.client.disconnect()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/packet.py",
    "content": "import functools\nimport json as _json\n\nimport six\n\n(CONNECT, DISCONNECT, EVENT, ACK, ERROR, BINARY_EVENT, BINARY_ACK) = \\\n    (0, 1, 2, 3, 4, 5, 6)\npacket_names = ['CONNECT', 'DISCONNECT', 'EVENT', 'ACK', 'ERROR',\n                'BINARY_EVENT', 'BINARY_ACK']\n\n\nclass Packet(object):\n    \"\"\"Socket.IO packet.\"\"\"\n\n    # the format of the Socket.IO packet is as follows:\n    #\n    # packet type: 1 byte, values 0-6\n    # num_attachments: ASCII encoded, only if num_attachments != 0\n    # '-': only if num_attachments != 0\n    # namespace: only if namespace != '/'\n    # ',': only if namespace and one of id and data are defined in this packet\n    # id: ASCII encoded, only if id is not None\n    # data: JSON dump of data payload\n\n    json = _json\n\n    def __init__(self, packet_type=EVENT, data=None, namespace=None, id=None,\n                 binary=None, encoded_packet=None):\n        self.packet_type = packet_type\n        self.data = data\n        self.namespace = namespace\n        self.id = id\n        if binary or (binary is None and self._data_is_binary(self.data)):\n            if self.packet_type == EVENT:\n                self.packet_type = BINARY_EVENT\n            elif self.packet_type == ACK:\n                self.packet_type = BINARY_ACK\n            else:\n                raise ValueError('Packet does not support binary payload.')\n        self.attachment_count = 0\n        self.attachments = []\n        if encoded_packet:\n            self.attachment_count = self.decode(encoded_packet)\n\n    def encode(self):\n        \"\"\"Encode the packet for transmission.\n\n        If the packet contains binary elements, this function returns a list\n        of packets where the first is the original packet with placeholders for\n        the binary components and the remaining ones the binary attachments.\n        \"\"\"\n        encoded_packet = six.text_type(self.packet_type)\n        if self.packet_type == BINARY_EVENT or self.packet_type == BINARY_ACK:\n            data, attachments = self._deconstruct_binary(self.data)\n            encoded_packet += six.text_type(len(attachments)) + '-'\n        else:\n            data = self.data\n            attachments = None\n        needs_comma = False\n        if self.namespace is not None and self.namespace != '/':\n            encoded_packet += self.namespace\n            needs_comma = True\n        if self.id is not None:\n            if needs_comma:\n                encoded_packet += ','\n                needs_comma = False\n            encoded_packet += six.text_type(self.id)\n        if data is not None:\n            if needs_comma:\n                encoded_packet += ','\n            encoded_packet += self.json.dumps(data, separators=(',', ':'))\n        if attachments is not None:\n            encoded_packet = [encoded_packet] + attachments\n        return encoded_packet\n\n    def decode(self, encoded_packet):\n        \"\"\"Decode a transmitted package.\n\n        The return value indicates how many binary attachment packets are\n        necessary to fully decode the packet.\n        \"\"\"\n        ep = encoded_packet\n        try:\n            self.packet_type = int(ep[0:1])\n        except TypeError:\n            self.packet_type = ep\n            ep = ''\n        self.namespace = None\n        self.data = None\n        ep = ep[1:]\n        dash = ep.find('-')\n        attachment_count = 0\n        if dash > 0 and ep[0:dash].isdigit():\n            attachment_count = int(ep[0:dash])\n            ep = ep[dash + 1:]\n        if ep and ep[0:1] == '/':\n            sep = ep.find(',')\n            if sep == -1:\n                self.namespace = ep\n                ep = ''\n            else:\n                self.namespace = ep[0:sep]\n                ep = ep[sep + 1:]\n            q = self.namespace.find('?')\n            if q != -1:\n                self.namespace = self.namespace[0:q]\n        if ep and ep[0].isdigit():\n            self.id = 0\n            while ep and ep[0].isdigit():\n                self.id = self.id * 10 + int(ep[0])\n                ep = ep[1:]\n        if ep:\n            self.data = self.json.loads(ep)\n        return attachment_count\n\n    def add_attachment(self, attachment):\n        if self.attachment_count <= len(self.attachments):\n            raise ValueError('Unexpected binary attachment')\n        self.attachments.append(attachment)\n        if self.attachment_count == len(self.attachments):\n            self.reconstruct_binary(self.attachments)\n            return True\n        return False\n\n    def reconstruct_binary(self, attachments):\n        \"\"\"Reconstruct a decoded packet using the given list of binary\n        attachments.\n        \"\"\"\n        self.data = self._reconstruct_binary_internal(self.data,\n                                                      self.attachments)\n\n    def _reconstruct_binary_internal(self, data, attachments):\n        if isinstance(data, list):\n            return [self._reconstruct_binary_internal(item, attachments)\n                    for item in data]\n        elif isinstance(data, dict):\n            if data.get('_placeholder') and 'num' in data:\n                return attachments[data['num']]\n            else:\n                return {key: self._reconstruct_binary_internal(value,\n                                                               attachments)\n                        for key, value in six.iteritems(data)}\n        else:\n            return data\n\n    def _deconstruct_binary(self, data):\n        \"\"\"Extract binary components in the packet.\"\"\"\n        attachments = []\n        data = self._deconstruct_binary_internal(data, attachments)\n        return data, attachments\n\n    def _deconstruct_binary_internal(self, data, attachments):\n        if isinstance(data, six.binary_type):\n            attachments.append(data)\n            return {'_placeholder': True, 'num': len(attachments) - 1}\n        elif isinstance(data, list):\n            return [self._deconstruct_binary_internal(item, attachments)\n                    for item in data]\n        elif isinstance(data, dict):\n            return {key: self._deconstruct_binary_internal(value, attachments)\n                    for key, value in six.iteritems(data)}\n        else:\n            return data\n\n    def _data_is_binary(self, data):\n        \"\"\"Check if the data contains binary components.\"\"\"\n        if isinstance(data, six.binary_type):\n            return True\n        elif isinstance(data, list):\n            return functools.reduce(\n                lambda a, b: a or b, [self._data_is_binary(item)\n                                      for item in data], False)\n        elif isinstance(data, dict):\n            return functools.reduce(\n                lambda a, b: a or b, [self._data_is_binary(item)\n                                      for item in six.itervalues(data)],\n                False)\n        else:\n            return False\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/pubsub_manager.py",
    "content": "from functools import partial\nimport uuid\n\nimport json\nimport pickle\nimport six\n\nfrom .base_manager import BaseManager\n\n\nclass PubSubManager(BaseManager):\n    \"\"\"Manage a client list attached to a pub/sub backend.\n\n    This is a base class that enables multiple servers to share the list of\n    clients, with the servers communicating events through a pub/sub backend.\n    The use of a pub/sub backend also allows any client connected to the\n    backend to emit events addressed to Socket.IO clients.\n\n    The actual backends must be implemented by subclasses, this class only\n    provides a pub/sub generic framework.\n\n    :param channel: The channel name on which the server sends and receives\n                    notifications.\n    \"\"\"\n    name = 'pubsub'\n\n    def __init__(self, channel='socketio', write_only=False, logger=None):\n        super(PubSubManager, self).__init__()\n        self.channel = channel\n        self.write_only = write_only\n        self.host_id = uuid.uuid4().hex\n        self.logger = logger\n\n    def initialize(self):\n        super(PubSubManager, self).initialize()\n        if not self.write_only:\n            self.thread = self.server.start_background_task(self._thread)\n        self._get_logger().info(self.name + ' backend initialized.')\n\n    def emit(self, event, data, namespace=None, room=None, skip_sid=None,\n             callback=None, **kwargs):\n        \"\"\"Emit a message to a single client, a room, or all the clients\n        connected to the namespace.\n\n        This method takes care or propagating the message to all the servers\n        that are connected through the message queue.\n\n        The parameters are the same as in :meth:`.Server.emit`.\n        \"\"\"\n        if kwargs.get('ignore_queue'):\n            return super(PubSubManager, self).emit(\n                event, data, namespace=namespace, room=room, skip_sid=skip_sid,\n                callback=callback)\n        namespace = namespace or '/'\n        if callback is not None:\n            if self.server is None:\n                raise RuntimeError('Callbacks can only be issued from the '\n                                   'context of a server.')\n            if room is None:\n                raise ValueError('Cannot use callback without a room set.')\n            id = self._generate_ack_id(room, namespace, callback)\n            callback = (room, namespace, id)\n        else:\n            callback = None\n        self._publish({'method': 'emit', 'event': event, 'data': data,\n                       'namespace': namespace, 'room': room,\n                       'skip_sid': skip_sid, 'callback': callback,\n                       'host_id': self.host_id})\n\n    def close_room(self, room, namespace=None):\n        self._publish({'method': 'close_room', 'room': room,\n                       'namespace': namespace or '/'})\n\n    def _publish(self, data):\n        \"\"\"Publish a message on the Socket.IO channel.\n\n        This method needs to be implemented by the different subclasses that\n        support pub/sub backends.\n        \"\"\"\n        raise NotImplementedError('This method must be implemented in a '\n                                  'subclass.')  # pragma: no cover\n\n    def _listen(self):\n        \"\"\"Return the next message published on the Socket.IO channel,\n        blocking until a message is available.\n\n        This method needs to be implemented by the different subclasses that\n        support pub/sub backends.\n        \"\"\"\n        raise NotImplementedError('This method must be implemented in a '\n                                  'subclass.')  # pragma: no cover\n\n    def _handle_emit(self, message):\n        # Events with callbacks are very tricky to handle across hosts\n        # Here in the receiving end we set up a local callback that preserves\n        # the callback host and id from the sender\n        remote_callback = message.get('callback')\n        remote_host_id = message.get('host_id')\n        if remote_callback is not None and len(remote_callback) == 3:\n            callback = partial(self._return_callback, remote_host_id,\n                               *remote_callback)\n        else:\n            callback = None\n        super(PubSubManager, self).emit(message['event'], message['data'],\n                                        namespace=message.get('namespace'),\n                                        room=message.get('room'),\n                                        skip_sid=message.get('skip_sid'),\n                                        callback=callback)\n\n    def _handle_callback(self, message):\n        if self.host_id == message.get('host_id'):\n            try:\n                sid = message['sid']\n                namespace = message['namespace']\n                id = message['id']\n                args = message['args']\n            except KeyError:\n                return\n            self.trigger_callback(sid, namespace, id, args)\n\n    def _return_callback(self, host_id, sid, namespace, callback_id, *args):\n        # When an event callback is received, the callback is returned back\n        # the sender, which is identified by the host_id\n        self._publish({'method': 'callback', 'host_id': host_id,\n                       'sid': sid, 'namespace': namespace, 'id': callback_id,\n                       'args': args})\n\n    def _handle_close_room(self, message):\n        super(PubSubManager, self).close_room(\n            room=message.get('room'), namespace=message.get('namespace'))\n\n    def _thread(self):\n        for message in self._listen():\n            data = None\n            if isinstance(message, dict):\n                data = message\n            else:\n                if isinstance(message, six.binary_type):  # pragma: no cover\n                    try:\n                        data = pickle.loads(message)\n                    except:\n                        pass\n                if data is None:\n                    try:\n                        data = json.loads(message)\n                    except:\n                        pass\n            if data and 'method' in data:\n                if data['method'] == 'emit':\n                    self._handle_emit(data)\n                elif data['method'] == 'callback':\n                    self._handle_callback(data)\n                elif data['method'] == 'close_room':\n                    self._handle_close_room(data)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/redis_manager.py",
    "content": "import logging\nimport pickle\nimport time\n\ntry:\n    import redis\nexcept ImportError:\n    redis = None\n\nfrom .pubsub_manager import PubSubManager\n\nlogger = logging.getLogger('socketio')\n\n\nclass RedisManager(PubSubManager):  # pragma: no cover\n    \"\"\"Redis based client manager.\n\n    This class implements a Redis backend for event sharing across multiple\n    processes. Only kept here as one more example of how to build a custom\n    backend, since the kombu backend is perfectly adequate to support a Redis\n    message queue.\n\n    To use a Redis backend, initialize the :class:`Server` instance as\n    follows::\n\n        url = 'redis://hostname:port/0'\n        server = socketio.Server(client_manager=socketio.RedisManager(url))\n\n    :param url: The connection URL for the Redis server. For a default Redis\n                store running on the same host, use ``redis://``.\n    :param channel: The channel name on which the server sends and receives\n                    notifications. Must be the same in all the servers.\n    :param write_only: If set ot ``True``, only initialize to emit events. The\n                       default of ``False`` initializes the class for emitting\n                       and receiving.\n    \"\"\"\n    name = 'redis'\n\n    def __init__(self, url='redis://localhost:6379/0', channel='socketio',\n                 write_only=False, logger=None):\n        if redis is None:\n            raise RuntimeError('Redis package is not installed '\n                               '(Run \"pip install redis\" in your '\n                               'virtualenv).')\n        self.redis_url = url\n        self._redis_connect()\n        super(RedisManager, self).__init__(channel=channel,\n                                           write_only=write_only,\n                                           logger=logger)\n\n    def initialize(self):\n        super(RedisManager, self).initialize()\n\n        monkey_patched = True\n        if self.server.async_mode == 'eventlet':\n            from eventlet.patcher import is_monkey_patched\n            monkey_patched = is_monkey_patched('socket')\n        elif 'gevent' in self.server.async_mode:\n            from gevent.monkey import is_module_patched\n            monkey_patched = is_module_patched('socket')\n        if not monkey_patched:\n            raise RuntimeError(\n                'Redis requires a monkey patched socket library to work '\n                'with ' + self.server.async_mode)\n\n    def _redis_connect(self):\n        self.redis = redis.Redis.from_url(self.redis_url)\n        self.pubsub = self.redis.pubsub()\n\n    def _publish(self, data):\n        retry = True\n        while True:\n            try:\n                if not retry:\n                    self._redis_connect()\n                return self.redis.publish(self.channel, pickle.dumps(data))\n            except redis.exceptions.ConnectionError:\n                if retry:\n                    logger.error('Cannot publish to redis... retrying')\n                    retry = False\n                else:\n                    logger.error('Cannot publish to redis... giving up')\n                    break\n\n    def _redis_listen_with_retries(self):\n        retry_sleep = 1\n        connect = False\n        while True:\n            try:\n                if connect:\n                    self._redis_connect()\n                    self.pubsub.subscribe(self.channel)\n                for message in self.pubsub.listen():\n                    yield message\n            except redis.exceptions.ConnectionError:\n                logger.error('Cannot receive from redis... '\n                             'retrying in {} secs'.format(retry_sleep))\n                connect = True\n                time.sleep(retry_sleep)\n                retry_sleep *= 2\n                if retry_sleep > 60:\n                    retry_sleep = 60\n\n    def _listen(self):\n        channel = self.channel.encode('utf-8')\n        self.pubsub.subscribe(self.channel)\n        for message in self._redis_listen_with_retries():\n            if message['channel'] == channel and \\\n                    message['type'] == 'message' and 'data' in message:\n                yield message['data']\n        self.pubsub.unsubscribe(self.channel)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/server.py",
    "content": "import logging\n\nimport engineio\nimport six\n\nfrom . import base_manager\nfrom . import exceptions\nfrom . import namespace\nfrom . import packet\n\ndefault_logger = logging.getLogger('socketio.server')\n\n\nclass Server(object):\n    \"\"\"A Socket.IO server.\n\n    This class implements a fully compliant Socket.IO web server with support\n    for websocket and long-polling transports.\n\n    :param client_manager: The client manager instance that will manage the\n                           client list. When this is omitted, the client list\n                           is stored in an in-memory structure, so the use of\n                           multiple connected servers is not possible.\n    :param logger: To enable logging set to ``True`` or pass a logger object to\n                   use. To disable logging set to ``False``. The default is\n                   ``False``.\n    :param binary: ``True`` to support binary payloads, ``False`` to treat all\n                   payloads as text. On Python 2, if this is set to ``True``,\n                   ``unicode`` values are treated as text, and ``str`` and\n                   ``bytes`` values are treated as binary.  This option has no\n                   effect on Python 3, where text and binary payloads are\n                   always automatically discovered.\n    :param json: An alternative json module to use for encoding and decoding\n                 packets. Custom json modules must have ``dumps`` and ``loads``\n                 functions that are compatible with the standard library\n                 versions.\n    :param async_handlers: If set to ``True``, event handlers for a client are\n                           executed in separate threads. To run handlers for a\n                           client synchronously, set to ``False``. The default\n                           is ``True``.\n    :param always_connect: When set to ``False``, new connections are\n                           provisory until the connect handler returns\n                           something other than ``False``, at which point they\n                           are accepted. When set to ``True``, connections are\n                           immediately accepted, and then if the connect\n                           handler returns ``False`` a disconnect is issued.\n                           Set to ``True`` if you need to emit events from the\n                           connect handler and your client is confused when it\n                           receives events before the connection acceptance.\n                           In any other case use the default of ``False``.\n    :param kwargs: Connection parameters for the underlying Engine.IO server.\n\n    The Engine.IO configuration supports the following settings:\n\n    :param async_mode: The asynchronous model to use. See the Deployment\n                       section in the documentation for a description of the\n                       available options. Valid async modes are \"threading\",\n                       \"eventlet\", \"gevent\" and \"gevent_uwsgi\". If this\n                       argument is not given, \"eventlet\" is tried first, then\n                       \"gevent_uwsgi\", then \"gevent\", and finally \"threading\".\n                       The first async mode that has all its dependencies\n                       installed is then one that is chosen.\n    :param ping_timeout: The time in seconds that the client waits for the\n                         server to respond before disconnecting. The default\n                         is 60 seconds.\n    :param ping_interval: The interval in seconds at which the client pings\n                          the server. The default is 25 seconds.\n    :param max_http_buffer_size: The maximum size of a message when using the\n                                 polling transport. The default is 100,000,000\n                                 bytes.\n    :param allow_upgrades: Whether to allow transport upgrades or not. The\n                           default is ``True``.\n    :param http_compression: Whether to compress packages when using the\n                             polling transport. The default is ``True``.\n    :param compression_threshold: Only compress messages when their byte size\n                                  is greater than this value. The default is\n                                  1024 bytes.\n    :param cookie: Name of the HTTP cookie that contains the client session\n                   id. If set to ``None``, a cookie is not sent to the client.\n                   The default is ``'io'``.\n    :param cors_allowed_origins: List of origins that are allowed to connect\n                                 to this server. All origins are allowed by\n                                 default.\n    :param cors_credentials: Whether credentials (cookies, authentication) are\n                             allowed in requests to this server. The default is\n                             ``True``.\n    :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass\n                            a logger object to use. To disable logging set to\n                            ``False``. The default is ``False``.\n    \"\"\"\n    def __init__(self, client_manager=None, logger=False, binary=False,\n                 json=None, async_handlers=True, always_connect=False,\n                 **kwargs):\n        engineio_options = kwargs\n        engineio_logger = engineio_options.pop('engineio_logger', None)\n        if engineio_logger is not None:\n            engineio_options['logger'] = engineio_logger\n        if json is not None:\n            packet.Packet.json = json\n            engineio_options['json'] = json\n        engineio_options['async_handlers'] = False\n        self.eio = self._engineio_server_class()(**engineio_options)\n        self.eio.on('connect', self._handle_eio_connect)\n        self.eio.on('message', self._handle_eio_message)\n        self.eio.on('disconnect', self._handle_eio_disconnect)\n        self.binary = binary\n\n        self.environ = {}\n        self.handlers = {}\n        self.namespace_handlers = {}\n\n        self._binary_packet = {}\n\n        if not isinstance(logger, bool):\n            self.logger = logger\n        else:\n            self.logger = default_logger\n            if not logging.root.handlers and \\\n                    self.logger.level == logging.NOTSET:\n                if logger:\n                    self.logger.setLevel(logging.INFO)\n                else:\n                    self.logger.setLevel(logging.ERROR)\n                self.logger.addHandler(logging.StreamHandler())\n\n        if client_manager is None:\n            client_manager = base_manager.BaseManager()\n        self.manager = client_manager\n        self.manager.set_server(self)\n        self.manager_initialized = False\n\n        self.async_handlers = async_handlers\n        self.always_connect = always_connect\n\n        self.async_mode = self.eio.async_mode\n\n    def is_asyncio_based(self):\n        return False\n\n    def on(self, event, handler=None, namespace=None):\n        \"\"\"Register an event handler.\n\n        :param event: The event name. It can be any string. The event names\n                      ``'connect'``, ``'message'`` and ``'disconnect'`` are\n                      reserved and should not be used.\n        :param handler: The function that should be invoked to handle the\n                        event. When this parameter is not given, the method\n                        acts as a decorator for the handler function.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the handler is associated with\n                          the default namespace.\n\n        Example usage::\n\n            # as a decorator:\n            @socket_io.on('connect', namespace='/chat')\n            def connect_handler(sid, environ):\n                print('Connection request')\n                if environ['REMOTE_ADDR'] in blacklisted:\n                    return False  # reject\n\n            # as a method:\n            def message_handler(sid, msg):\n                print('Received message: ', msg)\n                eio.send(sid, 'response')\n            socket_io.on('message', namespace='/chat', message_handler)\n\n        The handler function receives the ``sid`` (session ID) for the\n        client as first argument. The ``'connect'`` event handler receives the\n        WSGI environment as a second argument, and can return ``False`` to\n        reject the connection. The ``'message'`` handler and handlers for\n        custom event names receive the message payload as a second argument.\n        Any values returned from a message handler will be passed to the\n        client's acknowledgement callback function if it exists. The\n        ``'disconnect'`` handler does not take a second argument.\n        \"\"\"\n        namespace = namespace or '/'\n\n        def set_handler(handler):\n            if namespace not in self.handlers:\n                self.handlers[namespace] = {}\n            self.handlers[namespace][event] = handler\n            return handler\n\n        if handler is None:\n            return set_handler\n        set_handler(handler)\n\n    def event(self, *args, **kwargs):\n        \"\"\"Decorator to register an event handler.\n\n        This is a simplified version of the ``on()`` method that takes the\n        event name from the decorated function.\n\n        Example usage::\n\n            @sio.event\n            def my_event(data):\n                print('Received data: ', data)\n\n        The above example is equivalent to::\n\n            @sio.on('my_event')\n            def my_event(data):\n                print('Received data: ', data)\n\n        A custom namespace can be given as an argument to the decorator::\n\n            @sio.event(namespace='/test')\n            def my_event(data):\n                print('Received data: ', data)\n        \"\"\"\n        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):\n            # the decorator was invoked without arguments\n            # args[0] is the decorated function\n            return self.on(args[0].__name__)(args[0])\n        else:\n            # the decorator was invoked with arguments\n            def set_handler(handler):\n                return self.on(handler.__name__, *args, **kwargs)(handler)\n\n            return set_handler\n\n    def register_namespace(self, namespace_handler):\n        \"\"\"Register a namespace handler object.\n\n        :param namespace_handler: An instance of a :class:`Namespace`\n                                  subclass that handles all the event traffic\n                                  for a namespace.\n        \"\"\"\n        if not isinstance(namespace_handler, namespace.Namespace):\n            raise ValueError('Not a namespace instance')\n        if self.is_asyncio_based() != namespace_handler.is_asyncio_based():\n            raise ValueError('Not a valid namespace class for this server')\n        namespace_handler._set_server(self)\n        self.namespace_handlers[namespace_handler.namespace] = \\\n            namespace_handler\n\n    def emit(self, event, data=None, to=None, room=None, skip_sid=None,\n             namespace=None, callback=None, **kwargs):\n        \"\"\"Emit a custom event to one or more connected clients.\n\n        :param event: The event name. It can be any string. The event names\n                      ``'connect'``, ``'message'`` and ``'disconnect'`` are\n                      reserved and should not be used.\n        :param data: The data to send to the client or clients. Data can be of\n                     type ``str``, ``bytes``, ``list`` or ``dict``. If a\n                     ``list`` or ``dict``, the data will be serialized as JSON.\n        :param to: The recipient of the message. This can be set to the\n                   session ID of a client to address only that client, or to\n                   to any custom room created by the application to address all\n                   the clients in that room, If this argument is omitted the\n                   event is broadcasted to all connected clients.\n        :param room: Alias for the ``to`` parameter.\n        :param skip_sid: The session ID of a client to skip when broadcasting\n                         to a room or to all clients. This can be used to\n                         prevent a message from being sent to the sender. To\n                         skip multiple sids, pass a list.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the event is emitted to the\n                          default namespace.\n        :param callback: If given, this function will be called to acknowledge\n                         the the client has received the message. The arguments\n                         that will be passed to the function are those provided\n                         by the client. Callback functions can only be used\n                         when addressing an individual client.\n        :param ignore_queue: Only used when a message queue is configured. If\n                             set to ``True``, the event is emitted to the\n                             clients directly, without going through the queue.\n                             This is more efficient, but only works when a\n                             single server process is used. It is recommended\n                             to always leave this parameter with its default\n                             value of ``False``.\n        \"\"\"\n        namespace = namespace or '/'\n        room = to or room\n        self.logger.info('emitting event \"%s\" to %s [%s]', event,\n                         room or 'all', namespace)\n        self.manager.emit(event, data, namespace, room=room,\n                          skip_sid=skip_sid, callback=callback, **kwargs)\n\n    def send(self, data, to=None, room=None, skip_sid=None, namespace=None,\n             callback=None, **kwargs):\n        \"\"\"Send a message to one or more connected clients.\n\n        This function emits an event with the name ``'message'``. Use\n        :func:`emit` to issue custom event names.\n\n        :param data: The data to send to the client or clients. Data can be of\n                     type ``str``, ``bytes``, ``list`` or ``dict``. If a\n                     ``list`` or ``dict``, the data will be serialized as JSON.\n        :param to: The recipient of the message. This can be set to the\n                   session ID of a client to address only that client, or to\n                   to any custom room created by the application to address all\n                   the clients in that room, If this argument is omitted the\n                   event is broadcasted to all connected clients.\n        :param room: Alias for the ``to`` parameter.\n        :param skip_sid: The session ID of a client to skip when broadcasting\n                         to a room or to all clients. This can be used to\n                         prevent a message from being sent to the sender. To\n                         skip multiple sids, pass a list.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the event is emitted to the\n                          default namespace.\n        :param callback: If given, this function will be called to acknowledge\n                         the the client has received the message. The arguments\n                         that will be passed to the function are those provided\n                         by the client. Callback functions can only be used\n                         when addressing an individual client.\n        :param ignore_queue: Only used when a message queue is configured. If\n                             set to ``True``, the event is emitted to the\n                             clients directly, without going through the queue.\n                             This is more efficient, but only works when a\n                             single server process is used. It is recommended\n                             to always leave this parameter with its default\n                             value of ``False``.\n        \"\"\"\n        self.emit('message', data=data, to=to, room=room, skip_sid=skip_sid,\n                  namespace=namespace, callback=callback, **kwargs)\n\n    def call(self, event, data=None, to=None, sid=None, namespace=None,\n             timeout=60, **kwargs):\n        \"\"\"Emit a custom event to a client and wait for the response.\n\n        :param event: The event name. It can be any string. The event names\n                      ``'connect'``, ``'message'`` and ``'disconnect'`` are\n                      reserved and should not be used.\n        :param data: The data to send to the client or clients. Data can be of\n                     type ``str``, ``bytes``, ``list`` or ``dict``. If a\n                     ``list`` or ``dict``, the data will be serialized as JSON.\n        :param to: The session ID of the recipient client.\n        :param sid: Alias for the ``to`` parameter.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the event is emitted to the\n                          default namespace.\n        :param timeout: The waiting timeout. If the timeout is reached before\n                        the client acknowledges the event, then a\n                        ``TimeoutError`` exception is raised.\n        :param ignore_queue: Only used when a message queue is configured. If\n                             set to ``True``, the event is emitted to the\n                             client directly, without going through the queue.\n                             This is more efficient, but only works when a\n                             single server process is used. It is recommended\n                             to always leave this parameter with its default\n                             value of ``False``.\n        \"\"\"\n        if not self.async_handlers:\n            raise RuntimeError(\n                'Cannot use call() when async_handlers is False.')\n        callback_event = self.eio.create_event()\n        callback_args = []\n\n        def event_callback(*args):\n            callback_args.append(args)\n            callback_event.set()\n\n        self.emit(event, data=data, room=to or sid, namespace=namespace,\n                  callback=event_callback, **kwargs)\n        if not callback_event.wait(timeout=timeout):\n            raise exceptions.TimeoutError()\n        return callback_args[0] if len(callback_args[0]) > 1 \\\n            else callback_args[0][0] if len(callback_args[0]) == 1 \\\n            else None\n\n    def enter_room(self, sid, room, namespace=None):\n        \"\"\"Enter a room.\n\n        This function adds the client to a room. The :func:`emit` and\n        :func:`send` functions can optionally broadcast events to all the\n        clients in a room.\n\n        :param sid: Session ID of the client.\n        :param room: Room name. If the room does not exist it is created.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the default namespace is used.\n        \"\"\"\n        namespace = namespace or '/'\n        self.logger.info('%s is entering room %s [%s]', sid, room, namespace)\n        self.manager.enter_room(sid, namespace, room)\n\n    def leave_room(self, sid, room, namespace=None):\n        \"\"\"Leave a room.\n\n        This function removes the client from a room.\n\n        :param sid: Session ID of the client.\n        :param room: Room name.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the default namespace is used.\n        \"\"\"\n        namespace = namespace or '/'\n        self.logger.info('%s is leaving room %s [%s]', sid, room, namespace)\n        self.manager.leave_room(sid, namespace, room)\n\n    def close_room(self, room, namespace=None):\n        \"\"\"Close a room.\n\n        This function removes all the clients from the given room.\n\n        :param room: Room name.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the default namespace is used.\n        \"\"\"\n        namespace = namespace or '/'\n        self.logger.info('room %s is closing [%s]', room, namespace)\n        self.manager.close_room(room, namespace)\n\n    def rooms(self, sid, namespace=None):\n        \"\"\"Return the rooms a client is in.\n\n        :param sid: Session ID of the client.\n        :param namespace: The Socket.IO namespace for the event. If this\n                          argument is omitted the default namespace is used.\n        \"\"\"\n        namespace = namespace or '/'\n        return self.manager.get_rooms(sid, namespace)\n\n    def get_session(self, sid, namespace=None):\n        \"\"\"Return the user session for a client.\n\n        :param sid: The session id of the client.\n        :param namespace: The Socket.IO namespace. If this argument is omitted\n                          the default namespace is used.\n\n        The return value is a dictionary. Modifications made to this\n        dictionary are not guaranteed to be preserved unless\n        ``save_session()`` is called, or when the ``session`` context manager\n        is used.\n        \"\"\"\n        namespace = namespace or '/'\n        eio_session = self.eio.get_session(sid)\n        return eio_session.setdefault(namespace, {})\n\n    def save_session(self, sid, session, namespace=None):\n        \"\"\"Store the user session for a client.\n\n        :param sid: The session id of the client.\n        :param session: The session dictionary.\n        :param namespace: The Socket.IO namespace. If this argument is omitted\n                          the default namespace is used.\n        \"\"\"\n        namespace = namespace or '/'\n        eio_session = self.eio.get_session(sid)\n        eio_session[namespace] = session\n\n    def session(self, sid, namespace=None):\n        \"\"\"Return the user session for a client with context manager syntax.\n\n        :param sid: The session id of the client.\n\n        This is a context manager that returns the user session dictionary for\n        the client. Any changes that are made to this dictionary inside the\n        context manager block are saved back to the session. Example usage::\n\n            @sio.on('connect')\n            def on_connect(sid, environ):\n                username = authenticate_user(environ)\n                if not username:\n                    return False\n                with sio.session(sid) as session:\n                    session['username'] = username\n\n            @sio.on('message')\n            def on_message(sid, msg):\n                with sio.session(sid) as session:\n                    print('received message from ', session['username'])\n        \"\"\"\n        class _session_context_manager(object):\n            def __init__(self, server, sid, namespace):\n                self.server = server\n                self.sid = sid\n                self.namespace = namespace\n                self.session = None\n\n            def __enter__(self):\n                self.session = self.server.get_session(sid,\n                                                       namespace=namespace)\n                return self.session\n\n            def __exit__(self, *args):\n                self.server.save_session(sid, self.session,\n                                         namespace=namespace)\n\n        return _session_context_manager(self, sid, namespace)\n\n    def disconnect(self, sid, namespace=None):\n        \"\"\"Disconnect a client.\n\n        :param sid: Session ID of the client.\n        :param namespace: The Socket.IO namespace to disconnect. If this\n                          argument is omitted the default namespace is used.\n        \"\"\"\n        namespace = namespace or '/'\n        if self.manager.is_connected(sid, namespace=namespace):\n            self.logger.info('Disconnecting %s [%s]', sid, namespace)\n            self.manager.pre_disconnect(sid, namespace=namespace)\n            self._send_packet(sid, packet.Packet(packet.DISCONNECT,\n                                                 namespace=namespace))\n            self._trigger_event('disconnect', namespace, sid)\n            self.manager.disconnect(sid, namespace=namespace)\n\n    def transport(self, sid):\n        \"\"\"Return the name of the transport used by the client.\n\n        The two possible values returned by this function are ``'polling'``\n        and ``'websocket'``.\n\n        :param sid: The session of the client.\n        \"\"\"\n        return self.eio.transport(sid)\n\n    def handle_request(self, environ, start_response):\n        \"\"\"Handle an HTTP request from the client.\n\n        This is the entry point of the Socket.IO application, using the same\n        interface as a WSGI application. For the typical usage, this function\n        is invoked by the :class:`Middleware` instance, but it can be invoked\n        directly when the middleware is not used.\n\n        :param environ: The WSGI environment.\n        :param start_response: The WSGI ``start_response`` function.\n\n        This function returns the HTTP response body to deliver to the client\n        as a byte sequence.\n        \"\"\"\n        return self.eio.handle_request(environ, start_response)\n\n    def start_background_task(self, target, *args, **kwargs):\n        \"\"\"Start a background task using the appropriate async model.\n\n        This is a utility function that applications can use to start a\n        background task using the method that is compatible with the\n        selected async mode.\n\n        :param target: the target function to execute.\n        :param args: arguments to pass to the function.\n        :param kwargs: keyword arguments to pass to the function.\n\n        This function returns an object compatible with the `Thread` class in\n        the Python standard library. The `start()` method on this object is\n        already called by this function.\n        \"\"\"\n        return self.eio.start_background_task(target, *args, **kwargs)\n\n    def sleep(self, seconds=0):\n        \"\"\"Sleep for the requested amount of time using the appropriate async\n        model.\n\n        This is a utility function that applications can use to put a task to\n        sleep without having to worry about using the correct call for the\n        selected async mode.\n        \"\"\"\n        return self.eio.sleep(seconds)\n\n    def _emit_internal(self, sid, event, data, namespace=None, id=None):\n        \"\"\"Send a message to a client.\"\"\"\n        if six.PY2 and not self.binary:\n            binary = False  # pragma: nocover\n        else:\n            binary = None\n        # tuples are expanded to multiple arguments, everything else is sent\n        # as a single argument\n        if isinstance(data, tuple):\n            data = list(data)\n        else:\n            data = [data]\n        self._send_packet(sid, packet.Packet(packet.EVENT, namespace=namespace,\n                                             data=[event] + data, id=id,\n                                             binary=binary))\n\n    def _send_packet(self, sid, pkt):\n        \"\"\"Send a Socket.IO packet to a client.\"\"\"\n        encoded_packet = pkt.encode()\n        if isinstance(encoded_packet, list):\n            binary = False\n            for ep in encoded_packet:\n                self.eio.send(sid, ep, binary=binary)\n                binary = True\n        else:\n            self.eio.send(sid, encoded_packet, binary=False)\n\n    def _handle_connect(self, sid, namespace):\n        \"\"\"Handle a client connection request.\"\"\"\n        namespace = namespace or '/'\n        self.manager.connect(sid, namespace)\n        if self.always_connect:\n            self._send_packet(sid, packet.Packet(packet.CONNECT,\n                                                 namespace=namespace))\n        fail_reason = None\n        try:\n            success = self._trigger_event('connect', namespace, sid,\n                                          self.environ[sid])\n        except exceptions.ConnectionRefusedError as exc:\n            fail_reason = exc.error_args\n            success = False\n\n        if success is False:\n            if self.always_connect:\n                self.manager.pre_disconnect(sid, namespace)\n                self._send_packet(sid, packet.Packet(\n                    packet.DISCONNECT, data=fail_reason, namespace=namespace))\n            self.manager.disconnect(sid, namespace)\n            if not self.always_connect:\n                self._send_packet(sid, packet.Packet(\n                    packet.ERROR, data=fail_reason, namespace=namespace))\n            if sid in self.environ:  # pragma: no cover\n                del self.environ[sid]\n            return False\n        elif not self.always_connect:\n            self._send_packet(sid, packet.Packet(packet.CONNECT,\n                                                 namespace=namespace))\n\n    def _handle_disconnect(self, sid, namespace):\n        \"\"\"Handle a client disconnect.\"\"\"\n        namespace = namespace or '/'\n        if namespace == '/':\n            namespace_list = list(self.manager.get_namespaces())\n        else:\n            namespace_list = [namespace]\n        for n in namespace_list:\n            if n != '/' and self.manager.is_connected(sid, n):\n                self._trigger_event('disconnect', n, sid)\n                self.manager.disconnect(sid, n)\n        if namespace == '/' and self.manager.is_connected(sid, namespace):\n            self._trigger_event('disconnect', '/', sid)\n            self.manager.disconnect(sid, '/')\n\n    def _handle_event(self, sid, namespace, id, data):\n        \"\"\"Handle an incoming client event.\"\"\"\n        namespace = namespace or '/'\n        self.logger.info('received event \"%s\" from %s [%s]', data[0], sid,\n                         namespace)\n        if self.async_handlers:\n            self.start_background_task(self._handle_event_internal, self, sid,\n                                       data, namespace, id)\n        else:\n            self._handle_event_internal(self, sid, data, namespace, id)\n\n    def _handle_event_internal(self, server, sid, data, namespace, id):\n        r = server._trigger_event(data[0], namespace, sid, *data[1:])\n        if id is not None:\n            # send ACK packet with the response returned by the handler\n            # tuples are expanded as multiple arguments\n            if r is None:\n                data = []\n            elif isinstance(r, tuple):\n                data = list(r)\n            else:\n                data = [r]\n            if six.PY2 and not self.binary:\n                binary = False  # pragma: nocover\n            else:\n                binary = None\n            server._send_packet(sid, packet.Packet(packet.ACK,\n                                                   namespace=namespace,\n                                                   id=id, data=data,\n                                                   binary=binary))\n\n    def _handle_ack(self, sid, namespace, id, data):\n        \"\"\"Handle ACK packets from the client.\"\"\"\n        namespace = namespace or '/'\n        self.logger.info('received ack from %s [%s]', sid, namespace)\n        self.manager.trigger_callback(sid, namespace, id, data)\n\n    def _trigger_event(self, event, namespace, *args):\n        \"\"\"Invoke an application event handler.\"\"\"\n        # first see if we have an explicit handler for the event\n        if namespace in self.handlers and event in self.handlers[namespace]:\n            return self.handlers[namespace][event](*args)\n\n        # or else, forward the event to a namespace handler if one exists\n        elif namespace in self.namespace_handlers:\n            return self.namespace_handlers[namespace].trigger_event(\n                event, *args)\n\n    def _handle_eio_connect(self, sid, environ):\n        \"\"\"Handle the Engine.IO connection event.\"\"\"\n        if not self.manager_initialized:\n            self.manager_initialized = True\n            self.manager.initialize()\n        self.environ[sid] = environ\n        return self._handle_connect(sid, '/')\n\n    def _handle_eio_message(self, sid, data):\n        \"\"\"Dispatch Engine.IO messages.\"\"\"\n        if sid in self._binary_packet:\n            pkt = self._binary_packet[sid]\n            if pkt.add_attachment(data):\n                del self._binary_packet[sid]\n                if pkt.packet_type == packet.BINARY_EVENT:\n                    self._handle_event(sid, pkt.namespace, pkt.id, pkt.data)\n                else:\n                    self._handle_ack(sid, pkt.namespace, pkt.id, pkt.data)\n        else:\n            pkt = packet.Packet(encoded_packet=data)\n            if pkt.packet_type == packet.CONNECT:\n                self._handle_connect(sid, pkt.namespace)\n            elif pkt.packet_type == packet.DISCONNECT:\n                self._handle_disconnect(sid, pkt.namespace)\n            elif pkt.packet_type == packet.EVENT:\n                self._handle_event(sid, pkt.namespace, pkt.id, pkt.data)\n            elif pkt.packet_type == packet.ACK:\n                self._handle_ack(sid, pkt.namespace, pkt.id, pkt.data)\n            elif pkt.packet_type == packet.BINARY_EVENT or \\\n                    pkt.packet_type == packet.BINARY_ACK:\n                self._binary_packet[sid] = pkt\n            elif pkt.packet_type == packet.ERROR:\n                raise ValueError('Unexpected ERROR packet.')\n            else:\n                raise ValueError('Unknown packet type.')\n\n    def _handle_eio_disconnect(self, sid):\n        \"\"\"Handle Engine.IO disconnect event.\"\"\"\n        self._handle_disconnect(sid, '/')\n        if sid in self.environ:\n            del self.environ[sid]\n\n    def _engineio_server_class(self):\n        return engineio.Server\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/tornado.py",
    "content": "import sys\nif sys.version_info >= (3, 5):\n    try:\n        from engineio.async_drivers.tornado import get_tornado_handler as \\\n            get_engineio_handler\n    except ImportError:  # pragma: no cover\n        get_engineio_handler = None\n\n\ndef get_tornado_handler(socketio_server):  # pragma: no cover\n    return get_engineio_handler(socketio_server.eio)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/socketio/zmq_manager.py",
    "content": "import pickle\nimport re\n\ntry:\n    import eventlet.green.zmq as zmq\nexcept ImportError:\n    zmq = None\nimport six\n\nfrom .pubsub_manager import PubSubManager\n\n\nclass ZmqManager(PubSubManager):  # pragma: no cover\n    \"\"\"zmq based client manager.\n\n    NOTE: this zmq implementation should be considered experimental at this\n    time. At this time, eventlet is required to use zmq.\n\n    This class implements a zmq backend for event sharing across multiple\n    processes. To use a zmq backend, initialize the :class:`Server` instance as\n    follows::\n\n        url = 'zmq+tcp://hostname:port1+port2'\n        server = socketio.Server(client_manager=socketio.ZmqManager(url))\n\n    :param url: The connection URL for the zmq message broker,\n                which will need to be provided and running.\n    :param channel: The channel name on which the server sends and receives\n                    notifications. Must be the same in all the servers.\n    :param write_only: If set to ``True``, only initialize to emit events. The\n                       default of ``False`` initializes the class for emitting\n                       and receiving.\n\n    A zmq message broker must be running for the zmq_manager to work.\n    you can write your own or adapt one from the following simple broker\n    below::\n\n        import zmq\n\n        receiver = zmq.Context().socket(zmq.PULL)\n        receiver.bind(\"tcp://*:5555\")\n\n        publisher = zmq.Context().socket(zmq.PUB)\n        publisher.bind(\"tcp://*:5556\")\n\n        while True:\n            publisher.send(receiver.recv())\n    \"\"\"\n    name = 'zmq'\n\n    def __init__(self, url='zmq+tcp://localhost:5555+5556',\n                 channel='socketio',\n                 write_only=False,\n                 logger=None):\n        if zmq is None:\n            raise RuntimeError('zmq package is not installed '\n                               '(Run \"pip install pyzmq\" in your '\n                               'virtualenv).')\n\n        r = re.compile(r':\\d+\\+\\d+$')\n        if not (url.startswith('zmq+tcp://') and r.search(url)):\n            raise RuntimeError('unexpected connection string: ' + url)\n\n        url = url.replace('zmq+', '')\n        (sink_url, sub_port) = url.split('+')\n        sink_port = sink_url.split(':')[-1]\n        sub_url = sink_url.replace(sink_port, sub_port)\n\n        sink = zmq.Context().socket(zmq.PUSH)\n        sink.connect(sink_url)\n\n        sub = zmq.Context().socket(zmq.SUB)\n        sub.setsockopt_string(zmq.SUBSCRIBE, u'')\n        sub.connect(sub_url)\n\n        self.sink = sink\n        self.sub = sub\n        self.channel = channel\n        super(ZmqManager, self).__init__(channel=channel,\n                                         write_only=write_only,\n                                         logger=logger)\n\n    def _publish(self, data):\n        pickled_data = pickle.dumps(\n            {\n                'type': 'message',\n                'channel': self.channel,\n                'data': data\n            }\n        )\n        return self.sink.send(pickled_data)\n\n    def zmq_listen(self):\n        while True:\n            response = self.sub.recv()\n            if response is not None:\n                yield response\n\n    def _listen(self):\n        for message in self.zmq_listen():\n            if isinstance(message, six.binary_type):\n                try:\n                    message = pickle.loads(message)\n                except Exception:\n                    pass\n            if isinstance(message, dict) and \\\n                    message['type'] == 'message' and \\\n                    message['channel'] == self.channel and \\\n                    'data' in message:\n                yield message['data']\n        return\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/__init__.py",
    "content": "\"\"\"\nPython HTTP library with thread-safe connection pooling, file post support, user friendly, and more\n\"\"\"\nfrom __future__ import absolute_import\n\n# Set default logging handler to avoid \"No handler found\" warnings.\nimport logging\nimport warnings\nfrom logging import NullHandler\n\nfrom . import exceptions\nfrom ._version import __version__\nfrom .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url\nfrom .filepost import encode_multipart_formdata\nfrom .poolmanager import PoolManager, ProxyManager, proxy_from_url\nfrom .response import HTTPResponse\nfrom .util.request import make_headers\nfrom .util.retry import Retry\nfrom .util.timeout import Timeout\nfrom .util.url import get_host\n\n__author__ = \"Andrey Petrov (andrey.petrov@shazow.net)\"\n__license__ = \"MIT\"\n__version__ = __version__\n\n__all__ = (\n    \"HTTPConnectionPool\",\n    \"HTTPSConnectionPool\",\n    \"PoolManager\",\n    \"ProxyManager\",\n    \"HTTPResponse\",\n    \"Retry\",\n    \"Timeout\",\n    \"add_stderr_logger\",\n    \"connection_from_url\",\n    \"disable_warnings\",\n    \"encode_multipart_formdata\",\n    \"get_host\",\n    \"make_headers\",\n    \"proxy_from_url\",\n)\n\nlogging.getLogger(__name__).addHandler(NullHandler())\n\n\ndef add_stderr_logger(level=logging.DEBUG):\n    \"\"\"\n    Helper for quickly adding a StreamHandler to the logger. Useful for\n    debugging.\n\n    Returns the handler after adding it.\n    \"\"\"\n    # This method needs to be in this __init__.py to get the __name__ correct\n    # even if urllib3 is vendored within another package.\n    logger = logging.getLogger(__name__)\n    handler = logging.StreamHandler()\n    handler.setFormatter(logging.Formatter(\"%(asctime)s %(levelname)s %(message)s\"))\n    logger.addHandler(handler)\n    logger.setLevel(level)\n    logger.debug(\"Added a stderr logging handler to logger: %s\", __name__)\n    return handler\n\n\n# ... Clean up.\ndel NullHandler\n\n\n# All warning filters *must* be appended unless you're really certain that they\n# shouldn't be: otherwise, it's very hard for users to use most Python\n# mechanisms to silence them.\n# SecurityWarning's always go off by default.\nwarnings.simplefilter(\"always\", exceptions.SecurityWarning, append=True)\n# SubjectAltNameWarning's should go off once per host\nwarnings.simplefilter(\"default\", exceptions.SubjectAltNameWarning, append=True)\n# InsecurePlatformWarning's don't vary between requests, so we keep it default.\nwarnings.simplefilter(\"default\", exceptions.InsecurePlatformWarning, append=True)\n# SNIMissingWarnings should go off only once.\nwarnings.simplefilter(\"default\", exceptions.SNIMissingWarning, append=True)\n\n\ndef disable_warnings(category=exceptions.HTTPWarning):\n    \"\"\"\n    Helper for quickly disabling all urllib3 warnings.\n    \"\"\"\n    warnings.simplefilter(\"ignore\", category)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/_collections.py",
    "content": "from __future__ import absolute_import\n\ntry:\n    from collections.abc import Mapping, MutableMapping\nexcept ImportError:\n    from collections import Mapping, MutableMapping\ntry:\n    from threading import RLock\nexcept ImportError:  # Platform-specific: No threads available\n\n    class RLock:\n        def __enter__(self):\n            pass\n\n        def __exit__(self, exc_type, exc_value, traceback):\n            pass\n\n\nfrom collections import OrderedDict\n\nfrom .exceptions import InvalidHeader\nfrom .packages import six\nfrom .packages.six import iterkeys, itervalues\n\n__all__ = [\"RecentlyUsedContainer\", \"HTTPHeaderDict\"]\n\n\n_Null = object()\n\n\nclass RecentlyUsedContainer(MutableMapping):\n    \"\"\"\n    Provides a thread-safe dict-like container which maintains up to\n    ``maxsize`` keys while throwing away the least-recently-used keys beyond\n    ``maxsize``.\n\n    :param maxsize:\n        Maximum number of recent elements to retain.\n\n    :param dispose_func:\n        Every time an item is evicted from the container,\n        ``dispose_func(value)`` is called.  Callback which will get called\n    \"\"\"\n\n    ContainerCls = OrderedDict\n\n    def __init__(self, maxsize=10, dispose_func=None):\n        self._maxsize = maxsize\n        self.dispose_func = dispose_func\n\n        self._container = self.ContainerCls()\n        self.lock = RLock()\n\n    def __getitem__(self, key):\n        # Re-insert the item, moving it to the end of the eviction line.\n        with self.lock:\n            item = self._container.pop(key)\n            self._container[key] = item\n            return item\n\n    def __setitem__(self, key, value):\n        evicted_value = _Null\n        with self.lock:\n            # Possibly evict the existing value of 'key'\n            evicted_value = self._container.get(key, _Null)\n            self._container[key] = value\n\n            # If we didn't evict an existing value, we might have to evict the\n            # least recently used item from the beginning of the container.\n            if len(self._container) > self._maxsize:\n                _key, evicted_value = self._container.popitem(last=False)\n\n        if self.dispose_func and evicted_value is not _Null:\n            self.dispose_func(evicted_value)\n\n    def __delitem__(self, key):\n        with self.lock:\n            value = self._container.pop(key)\n\n        if self.dispose_func:\n            self.dispose_func(value)\n\n    def __len__(self):\n        with self.lock:\n            return len(self._container)\n\n    def __iter__(self):\n        raise NotImplementedError(\n            \"Iteration over this class is unlikely to be threadsafe.\"\n        )\n\n    def clear(self):\n        with self.lock:\n            # Copy pointers to all values, then wipe the mapping\n            values = list(itervalues(self._container))\n            self._container.clear()\n\n        if self.dispose_func:\n            for value in values:\n                self.dispose_func(value)\n\n    def keys(self):\n        with self.lock:\n            return list(iterkeys(self._container))\n\n\nclass HTTPHeaderDict(MutableMapping):\n    \"\"\"\n    :param headers:\n        An iterable of field-value pairs. Must not contain multiple field names\n        when compared case-insensitively.\n\n    :param kwargs:\n        Additional field-value pairs to pass in to ``dict.update``.\n\n    A ``dict`` like container for storing HTTP Headers.\n\n    Field names are stored and compared case-insensitively in compliance with\n    RFC 7230. Iteration provides the first case-sensitive key seen for each\n    case-insensitive pair.\n\n    Using ``__setitem__`` syntax overwrites fields that compare equal\n    case-insensitively in order to maintain ``dict``'s api. For fields that\n    compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add``\n    in a loop.\n\n    If multiple fields that are equal case-insensitively are passed to the\n    constructor or ``.update``, the behavior is undefined and some will be\n    lost.\n\n    >>> headers = HTTPHeaderDict()\n    >>> headers.add('Set-Cookie', 'foo=bar')\n    >>> headers.add('set-cookie', 'baz=quxx')\n    >>> headers['content-length'] = '7'\n    >>> headers['SET-cookie']\n    'foo=bar, baz=quxx'\n    >>> headers['Content-Length']\n    '7'\n    \"\"\"\n\n    def __init__(self, headers=None, **kwargs):\n        super(HTTPHeaderDict, self).__init__()\n        self._container = OrderedDict()\n        if headers is not None:\n            if isinstance(headers, HTTPHeaderDict):\n                self._copy_from(headers)\n            else:\n                self.extend(headers)\n        if kwargs:\n            self.extend(kwargs)\n\n    def __setitem__(self, key, val):\n        self._container[key.lower()] = [key, val]\n        return self._container[key.lower()]\n\n    def __getitem__(self, key):\n        val = self._container[key.lower()]\n        return \", \".join(val[1:])\n\n    def __delitem__(self, key):\n        del self._container[key.lower()]\n\n    def __contains__(self, key):\n        return key.lower() in self._container\n\n    def __eq__(self, other):\n        if not isinstance(other, Mapping) and not hasattr(other, \"keys\"):\n            return False\n        if not isinstance(other, type(self)):\n            other = type(self)(other)\n        return dict((k.lower(), v) for k, v in self.itermerged()) == dict(\n            (k.lower(), v) for k, v in other.itermerged()\n        )\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    if six.PY2:  # Python 2\n        iterkeys = MutableMapping.iterkeys\n        itervalues = MutableMapping.itervalues\n\n    __marker = object()\n\n    def __len__(self):\n        return len(self._container)\n\n    def __iter__(self):\n        # Only provide the originally cased names\n        for vals in self._container.values():\n            yield vals[0]\n\n    def pop(self, key, default=__marker):\n        \"\"\"D.pop(k[,d]) -> v, remove specified key and return the corresponding value.\n        If key is not found, d is returned if given, otherwise KeyError is raised.\n        \"\"\"\n        # Using the MutableMapping function directly fails due to the private marker.\n        # Using ordinary dict.pop would expose the internal structures.\n        # So let's reinvent the wheel.\n        try:\n            value = self[key]\n        except KeyError:\n            if default is self.__marker:\n                raise\n            return default\n        else:\n            del self[key]\n            return value\n\n    def discard(self, key):\n        try:\n            del self[key]\n        except KeyError:\n            pass\n\n    def add(self, key, val):\n        \"\"\"Adds a (name, value) pair, doesn't overwrite the value if it already\n        exists.\n\n        >>> headers = HTTPHeaderDict(foo='bar')\n        >>> headers.add('Foo', 'baz')\n        >>> headers['foo']\n        'bar, baz'\n        \"\"\"\n        key_lower = key.lower()\n        new_vals = [key, val]\n        # Keep the common case aka no item present as fast as possible\n        vals = self._container.setdefault(key_lower, new_vals)\n        if new_vals is not vals:\n            vals.append(val)\n\n    def extend(self, *args, **kwargs):\n        \"\"\"Generic import function for any type of header-like object.\n        Adapted version of MutableMapping.update in order to insert items\n        with self.add instead of self.__setitem__\n        \"\"\"\n        if len(args) > 1:\n            raise TypeError(\n                \"extend() takes at most 1 positional \"\n                \"arguments ({0} given)\".format(len(args))\n            )\n        other = args[0] if len(args) >= 1 else ()\n\n        if isinstance(other, HTTPHeaderDict):\n            for key, val in other.iteritems():\n                self.add(key, val)\n        elif isinstance(other, Mapping):\n            for key in other:\n                self.add(key, other[key])\n        elif hasattr(other, \"keys\"):\n            for key in other.keys():\n                self.add(key, other[key])\n        else:\n            for key, value in other:\n                self.add(key, value)\n\n        for key, value in kwargs.items():\n            self.add(key, value)\n\n    def getlist(self, key, default=__marker):\n        \"\"\"Returns a list of all the values for the named field. Returns an\n        empty list if the key doesn't exist.\"\"\"\n        try:\n            vals = self._container[key.lower()]\n        except KeyError:\n            if default is self.__marker:\n                return []\n            return default\n        else:\n            return vals[1:]\n\n    # Backwards compatibility for httplib\n    getheaders = getlist\n    getallmatchingheaders = getlist\n    iget = getlist\n\n    # Backwards compatibility for http.cookiejar\n    get_all = getlist\n\n    def __repr__(self):\n        return \"%s(%s)\" % (type(self).__name__, dict(self.itermerged()))\n\n    def _copy_from(self, other):\n        for key in other:\n            val = other.getlist(key)\n            if isinstance(val, list):\n                # Don't need to convert tuples\n                val = list(val)\n            self._container[key.lower()] = [key] + val\n\n    def copy(self):\n        clone = type(self)()\n        clone._copy_from(self)\n        return clone\n\n    def iteritems(self):\n        \"\"\"Iterate over all header lines, including duplicate ones.\"\"\"\n        for key in self:\n            vals = self._container[key.lower()]\n            for val in vals[1:]:\n                yield vals[0], val\n\n    def itermerged(self):\n        \"\"\"Iterate over all headers, merging duplicate ones together.\"\"\"\n        for key in self:\n            val = self._container[key.lower()]\n            yield val[0], \", \".join(val[1:])\n\n    def items(self):\n        return list(self.iteritems())\n\n    @classmethod\n    def from_httplib(cls, message):  # Python 2\n        \"\"\"Read headers from a Python 2 httplib message object.\"\"\"\n        # python2.7 does not expose a proper API for exporting multiheaders\n        # efficiently. This function re-reads raw lines from the message\n        # object and extracts the multiheaders properly.\n        obs_fold_continued_leaders = (\" \", \"\\t\")\n        headers = []\n\n        for line in message.headers:\n            if line.startswith(obs_fold_continued_leaders):\n                if not headers:\n                    # We received a header line that starts with OWS as described\n                    # in RFC-7230 S3.2.4. This indicates a multiline header, but\n                    # there exists no previous header to which we can attach it.\n                    raise InvalidHeader(\n                        \"Header continuation with no previous header: %s\" % line\n                    )\n                else:\n                    key, value = headers[-1]\n                    headers[-1] = (key, value + \" \" + line.strip())\n                    continue\n\n            key, value = line.split(\":\", 1)\n            headers.append((key, value.strip()))\n\n        return cls(headers)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/_version.py",
    "content": "# This file is protected via CODEOWNERS\n__version__ = \"1.26.9\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/connection.py",
    "content": "from __future__ import absolute_import\n\nimport datetime\nimport logging\nimport os\nimport re\nimport socket\nimport warnings\nfrom socket import error as SocketError\nfrom socket import timeout as SocketTimeout\n\nfrom .packages import six\nfrom .packages.six.moves.http_client import HTTPConnection as _HTTPConnection\nfrom .packages.six.moves.http_client import HTTPException  # noqa: F401\nfrom .util.proxy import create_proxy_ssl_context\n\ntry:  # Compiled with SSL?\n    import ssl\n\n    BaseSSLError = ssl.SSLError\nexcept (ImportError, AttributeError):  # Platform-specific: No SSL.\n    ssl = None\n\n    class BaseSSLError(BaseException):\n        pass\n\n\ntry:\n    # Python 3: not a no-op, we're adding this to the namespace so it can be imported.\n    ConnectionError = ConnectionError\nexcept NameError:\n    # Python 2\n    class ConnectionError(Exception):\n        pass\n\n\ntry:  # Python 3:\n    # Not a no-op, we're adding this to the namespace so it can be imported.\n    BrokenPipeError = BrokenPipeError\nexcept NameError:  # Python 2:\n\n    class BrokenPipeError(Exception):\n        pass\n\n\nfrom ._collections import HTTPHeaderDict  # noqa (historical, removed in v2)\nfrom ._version import __version__\nfrom .exceptions import (\n    ConnectTimeoutError,\n    NewConnectionError,\n    SubjectAltNameWarning,\n    SystemTimeWarning,\n)\nfrom .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection\nfrom .util.ssl_ import (\n    assert_fingerprint,\n    create_urllib3_context,\n    is_ipaddress,\n    resolve_cert_reqs,\n    resolve_ssl_version,\n    ssl_wrap_socket,\n)\nfrom .util.ssl_match_hostname import CertificateError, match_hostname\n\nlog = logging.getLogger(__name__)\n\nport_by_scheme = {\"http\": 80, \"https\": 443}\n\n# When it comes time to update this value as a part of regular maintenance\n# (ie test_recent_date is failing) update it to ~6 months before the current date.\nRECENT_DATE = datetime.date(2020, 7, 1)\n\n_CONTAINS_CONTROL_CHAR_RE = re.compile(r\"[^-!#$%&'*+.^_`|~0-9a-zA-Z]\")\n\n\nclass HTTPConnection(_HTTPConnection, object):\n    \"\"\"\n    Based on :class:`http.client.HTTPConnection` but provides an extra constructor\n    backwards-compatibility layer between older and newer Pythons.\n\n    Additional keyword parameters are used to configure attributes of the connection.\n    Accepted parameters include:\n\n    - ``strict``: See the documentation on :class:`urllib3.connectionpool.HTTPConnectionPool`\n    - ``source_address``: Set the source address for the current connection.\n    - ``socket_options``: Set specific options on the underlying socket. If not specified, then\n      defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling\n      Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy.\n\n      For example, if you wish to enable TCP Keep Alive in addition to the defaults,\n      you might pass:\n\n      .. code-block:: python\n\n         HTTPConnection.default_socket_options + [\n             (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),\n         ]\n\n      Or you may want to disable the defaults by passing an empty list (e.g., ``[]``).\n    \"\"\"\n\n    default_port = port_by_scheme[\"http\"]\n\n    #: Disable Nagle's algorithm by default.\n    #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]``\n    default_socket_options = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]\n\n    #: Whether this connection verifies the host's certificate.\n    is_verified = False\n\n    #: Whether this proxy connection (if used) verifies the proxy host's\n    #: certificate.\n    proxy_is_verified = None\n\n    def __init__(self, *args, **kw):\n        if not six.PY2:\n            kw.pop(\"strict\", None)\n\n        # Pre-set source_address.\n        self.source_address = kw.get(\"source_address\")\n\n        #: The socket options provided by the user. If no options are\n        #: provided, we use the default options.\n        self.socket_options = kw.pop(\"socket_options\", self.default_socket_options)\n\n        # Proxy options provided by the user.\n        self.proxy = kw.pop(\"proxy\", None)\n        self.proxy_config = kw.pop(\"proxy_config\", None)\n\n        _HTTPConnection.__init__(self, *args, **kw)\n\n    @property\n    def host(self):\n        \"\"\"\n        Getter method to remove any trailing dots that indicate the hostname is an FQDN.\n\n        In general, SSL certificates don't include the trailing dot indicating a\n        fully-qualified domain name, and thus, they don't validate properly when\n        checked against a domain name that includes the dot. In addition, some\n        servers may not expect to receive the trailing dot when provided.\n\n        However, the hostname with trailing dot is critical to DNS resolution; doing a\n        lookup with the trailing dot will properly only resolve the appropriate FQDN,\n        whereas a lookup without a trailing dot will search the system's search domain\n        list. Thus, it's important to keep the original host around for use only in\n        those cases where it's appropriate (i.e., when doing DNS lookup to establish the\n        actual TCP connection across which we're going to send HTTP requests).\n        \"\"\"\n        return self._dns_host.rstrip(\".\")\n\n    @host.setter\n    def host(self, value):\n        \"\"\"\n        Setter for the `host` property.\n\n        We assume that only urllib3 uses the _dns_host attribute; httplib itself\n        only uses `host`, and it seems reasonable that other libraries follow suit.\n        \"\"\"\n        self._dns_host = value\n\n    def _new_conn(self):\n        \"\"\"Establish a socket connection and set nodelay settings on it.\n\n        :return: New socket connection.\n        \"\"\"\n        extra_kw = {}\n        if self.source_address:\n            extra_kw[\"source_address\"] = self.source_address\n\n        if self.socket_options:\n            extra_kw[\"socket_options\"] = self.socket_options\n\n        try:\n            conn = connection.create_connection(\n                (self._dns_host, self.port), self.timeout, **extra_kw\n            )\n\n        except SocketTimeout:\n            raise ConnectTimeoutError(\n                self,\n                \"Connection to %s timed out. (connect timeout=%s)\"\n                % (self.host, self.timeout),\n            )\n\n        except SocketError as e:\n            raise NewConnectionError(\n                self, \"Failed to establish a new connection: %s\" % e\n            )\n\n        return conn\n\n    def _is_using_tunnel(self):\n        # Google App Engine's httplib does not define _tunnel_host\n        return getattr(self, \"_tunnel_host\", None)\n\n    def _prepare_conn(self, conn):\n        self.sock = conn\n        if self._is_using_tunnel():\n            # TODO: Fix tunnel so it doesn't depend on self.sock state.\n            self._tunnel()\n            # Mark this connection as not reusable\n            self.auto_open = 0\n\n    def connect(self):\n        conn = self._new_conn()\n        self._prepare_conn(conn)\n\n    def putrequest(self, method, url, *args, **kwargs):\n        \"\"\" \"\"\"\n        # Empty docstring because the indentation of CPython's implementation\n        # is broken but we don't want this method in our documentation.\n        match = _CONTAINS_CONTROL_CHAR_RE.search(method)\n        if match:\n            raise ValueError(\n                \"Method cannot contain non-token characters %r (found at least %r)\"\n                % (method, match.group())\n            )\n\n        return _HTTPConnection.putrequest(self, method, url, *args, **kwargs)\n\n    def putheader(self, header, *values):\n        \"\"\" \"\"\"\n        if not any(isinstance(v, str) and v == SKIP_HEADER for v in values):\n            _HTTPConnection.putheader(self, header, *values)\n        elif six.ensure_str(header.lower()) not in SKIPPABLE_HEADERS:\n            raise ValueError(\n                \"urllib3.util.SKIP_HEADER only supports '%s'\"\n                % (\"', '\".join(map(str.title, sorted(SKIPPABLE_HEADERS))),)\n            )\n\n    def request(self, method, url, body=None, headers=None):\n        if headers is None:\n            headers = {}\n        else:\n            # Avoid modifying the headers passed into .request()\n            headers = headers.copy()\n        if \"user-agent\" not in (six.ensure_str(k.lower()) for k in headers):\n            headers[\"User-Agent\"] = _get_default_user_agent()\n        super(HTTPConnection, self).request(method, url, body=body, headers=headers)\n\n    def request_chunked(self, method, url, body=None, headers=None):\n        \"\"\"\n        Alternative to the common request method, which sends the\n        body with chunked encoding and not as one block\n        \"\"\"\n        headers = headers or {}\n        header_keys = set([six.ensure_str(k.lower()) for k in headers])\n        skip_accept_encoding = \"accept-encoding\" in header_keys\n        skip_host = \"host\" in header_keys\n        self.putrequest(\n            method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host\n        )\n        if \"user-agent\" not in header_keys:\n            self.putheader(\"User-Agent\", _get_default_user_agent())\n        for header, value in headers.items():\n            self.putheader(header, value)\n        if \"transfer-encoding\" not in header_keys:\n            self.putheader(\"Transfer-Encoding\", \"chunked\")\n        self.endheaders()\n\n        if body is not None:\n            stringish_types = six.string_types + (bytes,)\n            if isinstance(body, stringish_types):\n                body = (body,)\n            for chunk in body:\n                if not chunk:\n                    continue\n                if not isinstance(chunk, bytes):\n                    chunk = chunk.encode(\"utf8\")\n                len_str = hex(len(chunk))[2:]\n                to_send = bytearray(len_str.encode())\n                to_send += b\"\\r\\n\"\n                to_send += chunk\n                to_send += b\"\\r\\n\"\n                self.send(to_send)\n\n        # After the if clause, to always have a closed body\n        self.send(b\"0\\r\\n\\r\\n\")\n\n\nclass HTTPSConnection(HTTPConnection):\n    \"\"\"\n    Many of the parameters to this constructor are passed to the underlying SSL\n    socket by means of :py:func:`urllib3.util.ssl_wrap_socket`.\n    \"\"\"\n\n    default_port = port_by_scheme[\"https\"]\n\n    cert_reqs = None\n    ca_certs = None\n    ca_cert_dir = None\n    ca_cert_data = None\n    ssl_version = None\n    assert_fingerprint = None\n    tls_in_tls_required = False\n\n    def __init__(\n        self,\n        host,\n        port=None,\n        key_file=None,\n        cert_file=None,\n        key_password=None,\n        strict=None,\n        timeout=socket._GLOBAL_DEFAULT_TIMEOUT,\n        ssl_context=None,\n        server_hostname=None,\n        **kw\n    ):\n\n        HTTPConnection.__init__(self, host, port, strict=strict, timeout=timeout, **kw)\n\n        self.key_file = key_file\n        self.cert_file = cert_file\n        self.key_password = key_password\n        self.ssl_context = ssl_context\n        self.server_hostname = server_hostname\n\n        # Required property for Google AppEngine 1.9.0 which otherwise causes\n        # HTTPS requests to go out as HTTP. (See Issue #356)\n        self._protocol = \"https\"\n\n    def set_cert(\n        self,\n        key_file=None,\n        cert_file=None,\n        cert_reqs=None,\n        key_password=None,\n        ca_certs=None,\n        assert_hostname=None,\n        assert_fingerprint=None,\n        ca_cert_dir=None,\n        ca_cert_data=None,\n    ):\n        \"\"\"\n        This method should only be called once, before the connection is used.\n        \"\"\"\n        # If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also\n        # have an SSLContext object in which case we'll use its verify_mode.\n        if cert_reqs is None:\n            if self.ssl_context is not None:\n                cert_reqs = self.ssl_context.verify_mode\n            else:\n                cert_reqs = resolve_cert_reqs(None)\n\n        self.key_file = key_file\n        self.cert_file = cert_file\n        self.cert_reqs = cert_reqs\n        self.key_password = key_password\n        self.assert_hostname = assert_hostname\n        self.assert_fingerprint = assert_fingerprint\n        self.ca_certs = ca_certs and os.path.expanduser(ca_certs)\n        self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir)\n        self.ca_cert_data = ca_cert_data\n\n    def connect(self):\n        # Add certificate verification\n        self.sock = conn = self._new_conn()\n        hostname = self.host\n        tls_in_tls = False\n\n        if self._is_using_tunnel():\n            if self.tls_in_tls_required:\n                self.sock = conn = self._connect_tls_proxy(hostname, conn)\n                tls_in_tls = True\n\n            # Calls self._set_hostport(), so self.host is\n            # self._tunnel_host below.\n            self._tunnel()\n            # Mark this connection as not reusable\n            self.auto_open = 0\n\n            # Override the host with the one we're requesting data from.\n            hostname = self._tunnel_host\n\n        server_hostname = hostname\n        if self.server_hostname is not None:\n            server_hostname = self.server_hostname\n\n        is_time_off = datetime.date.today() < RECENT_DATE\n        if is_time_off:\n            warnings.warn(\n                (\n                    \"System time is way off (before {0}). This will probably \"\n                    \"lead to SSL verification errors\"\n                ).format(RECENT_DATE),\n                SystemTimeWarning,\n            )\n\n        # Wrap socket using verification with the root certs in\n        # trusted_root_certs\n        default_ssl_context = False\n        if self.ssl_context is None:\n            default_ssl_context = True\n            self.ssl_context = create_urllib3_context(\n                ssl_version=resolve_ssl_version(self.ssl_version),\n                cert_reqs=resolve_cert_reqs(self.cert_reqs),\n            )\n\n        context = self.ssl_context\n        context.verify_mode = resolve_cert_reqs(self.cert_reqs)\n\n        # Try to load OS default certs if none are given.\n        # Works well on Windows (requires Python3.4+)\n        if (\n            not self.ca_certs\n            and not self.ca_cert_dir\n            and not self.ca_cert_data\n            and default_ssl_context\n            and hasattr(context, \"load_default_certs\")\n        ):\n            context.load_default_certs()\n\n        self.sock = ssl_wrap_socket(\n            sock=conn,\n            keyfile=self.key_file,\n            certfile=self.cert_file,\n            key_password=self.key_password,\n            ca_certs=self.ca_certs,\n            ca_cert_dir=self.ca_cert_dir,\n            ca_cert_data=self.ca_cert_data,\n            server_hostname=server_hostname,\n            ssl_context=context,\n            tls_in_tls=tls_in_tls,\n        )\n\n        # If we're using all defaults and the connection\n        # is TLSv1 or TLSv1.1 we throw a DeprecationWarning\n        # for the host.\n        if (\n            default_ssl_context\n            and self.ssl_version is None\n            and hasattr(self.sock, \"version\")\n            and self.sock.version() in {\"TLSv1\", \"TLSv1.1\"}\n        ):\n            warnings.warn(\n                \"Negotiating TLSv1/TLSv1.1 by default is deprecated \"\n                \"and will be disabled in urllib3 v2.0.0. Connecting to \"\n                \"'%s' with '%s' can be enabled by explicitly opting-in \"\n                \"with 'ssl_version'\" % (self.host, self.sock.version()),\n                DeprecationWarning,\n            )\n\n        if self.assert_fingerprint:\n            assert_fingerprint(\n                self.sock.getpeercert(binary_form=True), self.assert_fingerprint\n            )\n        elif (\n            context.verify_mode != ssl.CERT_NONE\n            and not getattr(context, \"check_hostname\", False)\n            and self.assert_hostname is not False\n        ):\n            # While urllib3 attempts to always turn off hostname matching from\n            # the TLS library, this cannot always be done. So we check whether\n            # the TLS Library still thinks it's matching hostnames.\n            cert = self.sock.getpeercert()\n            if not cert.get(\"subjectAltName\", ()):\n                warnings.warn(\n                    (\n                        \"Certificate for {0} has no `subjectAltName`, falling back to check for a \"\n                        \"`commonName` for now. This feature is being removed by major browsers and \"\n                        \"deprecated by RFC 2818. (See https://github.com/urllib3/urllib3/issues/497 \"\n                        \"for details.)\".format(hostname)\n                    ),\n                    SubjectAltNameWarning,\n                )\n            _match_hostname(cert, self.assert_hostname or server_hostname)\n\n        self.is_verified = (\n            context.verify_mode == ssl.CERT_REQUIRED\n            or self.assert_fingerprint is not None\n        )\n\n    def _connect_tls_proxy(self, hostname, conn):\n        \"\"\"\n        Establish a TLS connection to the proxy using the provided SSL context.\n        \"\"\"\n        proxy_config = self.proxy_config\n        ssl_context = proxy_config.ssl_context\n        if ssl_context:\n            # If the user provided a proxy context, we assume CA and client\n            # certificates have already been set\n            return ssl_wrap_socket(\n                sock=conn,\n                server_hostname=hostname,\n                ssl_context=ssl_context,\n            )\n\n        ssl_context = create_proxy_ssl_context(\n            self.ssl_version,\n            self.cert_reqs,\n            self.ca_certs,\n            self.ca_cert_dir,\n            self.ca_cert_data,\n        )\n\n        # If no cert was provided, use only the default options for server\n        # certificate validation\n        socket = ssl_wrap_socket(\n            sock=conn,\n            ca_certs=self.ca_certs,\n            ca_cert_dir=self.ca_cert_dir,\n            ca_cert_data=self.ca_cert_data,\n            server_hostname=hostname,\n            ssl_context=ssl_context,\n        )\n\n        if ssl_context.verify_mode != ssl.CERT_NONE and not getattr(\n            ssl_context, \"check_hostname\", False\n        ):\n            # While urllib3 attempts to always turn off hostname matching from\n            # the TLS library, this cannot always be done. So we check whether\n            # the TLS Library still thinks it's matching hostnames.\n            cert = socket.getpeercert()\n            if not cert.get(\"subjectAltName\", ()):\n                warnings.warn(\n                    (\n                        \"Certificate for {0} has no `subjectAltName`, falling back to check for a \"\n                        \"`commonName` for now. This feature is being removed by major browsers and \"\n                        \"deprecated by RFC 2818. (See https://github.com/urllib3/urllib3/issues/497 \"\n                        \"for details.)\".format(hostname)\n                    ),\n                    SubjectAltNameWarning,\n                )\n            _match_hostname(cert, hostname)\n\n        self.proxy_is_verified = ssl_context.verify_mode == ssl.CERT_REQUIRED\n        return socket\n\n\ndef _match_hostname(cert, asserted_hostname):\n    # Our upstream implementation of ssl.match_hostname()\n    # only applies this normalization to IP addresses so it doesn't\n    # match DNS SANs so we do the same thing!\n    stripped_hostname = asserted_hostname.strip(\"u[]\")\n    if is_ipaddress(stripped_hostname):\n        asserted_hostname = stripped_hostname\n\n    try:\n        match_hostname(cert, asserted_hostname)\n    except CertificateError as e:\n        log.warning(\n            \"Certificate did not match expected hostname: %s. Certificate: %s\",\n            asserted_hostname,\n            cert,\n        )\n        # Add cert to exception and reraise so client code can inspect\n        # the cert when catching the exception, if they want to\n        e._peer_cert = cert\n        raise\n\n\ndef _get_default_user_agent():\n    return \"python-urllib3/%s\" % __version__\n\n\nclass DummyConnection(object):\n    \"\"\"Used to detect a failed ConnectionCls import.\"\"\"\n\n    pass\n\n\nif not ssl:\n    HTTPSConnection = DummyConnection  # noqa: F811\n\n\nVerifiedHTTPSConnection = HTTPSConnection\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/connectionpool.py",
    "content": "from __future__ import absolute_import\n\nimport errno\nimport logging\nimport re\nimport socket\nimport sys\nimport warnings\nfrom socket import error as SocketError\nfrom socket import timeout as SocketTimeout\n\nfrom .connection import (\n    BaseSSLError,\n    BrokenPipeError,\n    DummyConnection,\n    HTTPConnection,\n    HTTPException,\n    HTTPSConnection,\n    VerifiedHTTPSConnection,\n    port_by_scheme,\n)\nfrom .exceptions import (\n    ClosedPoolError,\n    EmptyPoolError,\n    HeaderParsingError,\n    HostChangedError,\n    InsecureRequestWarning,\n    LocationValueError,\n    MaxRetryError,\n    NewConnectionError,\n    ProtocolError,\n    ProxyError,\n    ReadTimeoutError,\n    SSLError,\n    TimeoutError,\n)\nfrom .packages import six\nfrom .packages.six.moves import queue\nfrom .request import RequestMethods\nfrom .response import HTTPResponse\nfrom .util.connection import is_connection_dropped\nfrom .util.proxy import connection_requires_http_tunnel\nfrom .util.queue import LifoQueue\nfrom .util.request import set_file_position\nfrom .util.response import assert_header_parsing\nfrom .util.retry import Retry\nfrom .util.ssl_match_hostname import CertificateError\nfrom .util.timeout import Timeout\nfrom .util.url import Url, _encode_target\nfrom .util.url import _normalize_host as normalize_host\nfrom .util.url import get_host, parse_url\n\nxrange = six.moves.xrange\n\nlog = logging.getLogger(__name__)\n\n_Default = object()\n\n\n# Pool objects\nclass ConnectionPool(object):\n    \"\"\"\n    Base class for all connection pools, such as\n    :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`.\n\n    .. note::\n       ConnectionPool.urlopen() does not normalize or percent-encode target URIs\n       which is useful if your target server doesn't support percent-encoded\n       target URIs.\n    \"\"\"\n\n    scheme = None\n    QueueCls = LifoQueue\n\n    def __init__(self, host, port=None):\n        if not host:\n            raise LocationValueError(\"No host specified.\")\n\n        self.host = _normalize_host(host, scheme=self.scheme)\n        self._proxy_host = host.lower()\n        self.port = port\n\n    def __str__(self):\n        return \"%s(host=%r, port=%r)\" % (type(self).__name__, self.host, self.port)\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n        # Return False to re-raise any potential exceptions\n        return False\n\n    def close(self):\n        \"\"\"\n        Close all pooled connections and disable the pool.\n        \"\"\"\n        pass\n\n\n# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252\n_blocking_errnos = {errno.EAGAIN, errno.EWOULDBLOCK}\n\n\nclass HTTPConnectionPool(ConnectionPool, RequestMethods):\n    \"\"\"\n    Thread-safe connection pool for one host.\n\n    :param host:\n        Host used for this HTTP Connection (e.g. \"localhost\"), passed into\n        :class:`http.client.HTTPConnection`.\n\n    :param port:\n        Port used for this HTTP Connection (None is equivalent to 80), passed\n        into :class:`http.client.HTTPConnection`.\n\n    :param strict:\n        Causes BadStatusLine to be raised if the status line can't be parsed\n        as a valid HTTP/1.0 or 1.1 status line, passed into\n        :class:`http.client.HTTPConnection`.\n\n        .. note::\n           Only works in Python 2. This parameter is ignored in Python 3.\n\n    :param timeout:\n        Socket timeout in seconds for each individual connection. This can\n        be a float or integer, which sets the timeout for the HTTP request,\n        or an instance of :class:`urllib3.util.Timeout` which gives you more\n        fine-grained control over request timeouts. After the constructor has\n        been parsed, this is always a `urllib3.util.Timeout` object.\n\n    :param maxsize:\n        Number of connections to save that can be reused. More than 1 is useful\n        in multithreaded situations. If ``block`` is set to False, more\n        connections will be created but they will not be saved once they've\n        been used.\n\n    :param block:\n        If set to True, no more than ``maxsize`` connections will be used at\n        a time. When no free connections are available, the call will block\n        until a connection has been released. This is a useful side effect for\n        particular multithreaded situations where one does not want to use more\n        than maxsize connections per host to prevent flooding.\n\n    :param headers:\n        Headers to include with all requests, unless other headers are given\n        explicitly.\n\n    :param retries:\n        Retry configuration to use by default with requests in this pool.\n\n    :param _proxy:\n        Parsed proxy URL, should not be used directly, instead, see\n        :class:`urllib3.ProxyManager`\n\n    :param _proxy_headers:\n        A dictionary with proxy headers, should not be used directly,\n        instead, see :class:`urllib3.ProxyManager`\n\n    :param \\\\**conn_kw:\n        Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`,\n        :class:`urllib3.connection.HTTPSConnection` instances.\n    \"\"\"\n\n    scheme = \"http\"\n    ConnectionCls = HTTPConnection\n    ResponseCls = HTTPResponse\n\n    def __init__(\n        self,\n        host,\n        port=None,\n        strict=False,\n        timeout=Timeout.DEFAULT_TIMEOUT,\n        maxsize=1,\n        block=False,\n        headers=None,\n        retries=None,\n        _proxy=None,\n        _proxy_headers=None,\n        _proxy_config=None,\n        **conn_kw\n    ):\n        ConnectionPool.__init__(self, host, port)\n        RequestMethods.__init__(self, headers)\n\n        self.strict = strict\n\n        if not isinstance(timeout, Timeout):\n            timeout = Timeout.from_float(timeout)\n\n        if retries is None:\n            retries = Retry.DEFAULT\n\n        self.timeout = timeout\n        self.retries = retries\n\n        self.pool = self.QueueCls(maxsize)\n        self.block = block\n\n        self.proxy = _proxy\n        self.proxy_headers = _proxy_headers or {}\n        self.proxy_config = _proxy_config\n\n        # Fill the queue up so that doing get() on it will block properly\n        for _ in xrange(maxsize):\n            self.pool.put(None)\n\n        # These are mostly for testing and debugging purposes.\n        self.num_connections = 0\n        self.num_requests = 0\n        self.conn_kw = conn_kw\n\n        if self.proxy:\n            # Enable Nagle's algorithm for proxies, to avoid packet fragmentation.\n            # We cannot know if the user has added default socket options, so we cannot replace the\n            # list.\n            self.conn_kw.setdefault(\"socket_options\", [])\n\n            self.conn_kw[\"proxy\"] = self.proxy\n            self.conn_kw[\"proxy_config\"] = self.proxy_config\n\n    def _new_conn(self):\n        \"\"\"\n        Return a fresh :class:`HTTPConnection`.\n        \"\"\"\n        self.num_connections += 1\n        log.debug(\n            \"Starting new HTTP connection (%d): %s:%s\",\n            self.num_connections,\n            self.host,\n            self.port or \"80\",\n        )\n\n        conn = self.ConnectionCls(\n            host=self.host,\n            port=self.port,\n            timeout=self.timeout.connect_timeout,\n            strict=self.strict,\n            **self.conn_kw\n        )\n        return conn\n\n    def _get_conn(self, timeout=None):\n        \"\"\"\n        Get a connection. Will return a pooled connection if one is available.\n\n        If no connections are available and :prop:`.block` is ``False``, then a\n        fresh connection is returned.\n\n        :param timeout:\n            Seconds to wait before giving up and raising\n            :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and\n            :prop:`.block` is ``True``.\n        \"\"\"\n        conn = None\n        try:\n            conn = self.pool.get(block=self.block, timeout=timeout)\n\n        except AttributeError:  # self.pool is None\n            raise ClosedPoolError(self, \"Pool is closed.\")\n\n        except queue.Empty:\n            if self.block:\n                raise EmptyPoolError(\n                    self,\n                    \"Pool reached maximum size and no more connections are allowed.\",\n                )\n            pass  # Oh well, we'll create a new connection then\n\n        # If this is a persistent connection, check if it got disconnected\n        if conn and is_connection_dropped(conn):\n            log.debug(\"Resetting dropped connection: %s\", self.host)\n            conn.close()\n            if getattr(conn, \"auto_open\", 1) == 0:\n                # This is a proxied connection that has been mutated by\n                # http.client._tunnel() and cannot be reused (since it would\n                # attempt to bypass the proxy)\n                conn = None\n\n        return conn or self._new_conn()\n\n    def _put_conn(self, conn):\n        \"\"\"\n        Put a connection back into the pool.\n\n        :param conn:\n            Connection object for the current host and port as returned by\n            :meth:`._new_conn` or :meth:`._get_conn`.\n\n        If the pool is already full, the connection is closed and discarded\n        because we exceeded maxsize. If connections are discarded frequently,\n        then maxsize should be increased.\n\n        If the pool is closed, then the connection will be closed and discarded.\n        \"\"\"\n        try:\n            self.pool.put(conn, block=False)\n            return  # Everything is dandy, done.\n        except AttributeError:\n            # self.pool is None.\n            pass\n        except queue.Full:\n            # This should never happen if self.block == True\n            log.warning(\n                \"Connection pool is full, discarding connection: %s. Connection pool size: %s\",\n                self.host,\n                self.pool.qsize(),\n            )\n        # Connection never got put back into the pool, close it.\n        if conn:\n            conn.close()\n\n    def _validate_conn(self, conn):\n        \"\"\"\n        Called right before a request is made, after the socket is created.\n        \"\"\"\n        pass\n\n    def _prepare_proxy(self, conn):\n        # Nothing to do for HTTP connections.\n        pass\n\n    def _get_timeout(self, timeout):\n        \"\"\"Helper that always returns a :class:`urllib3.util.Timeout`\"\"\"\n        if timeout is _Default:\n            return self.timeout.clone()\n\n        if isinstance(timeout, Timeout):\n            return timeout.clone()\n        else:\n            # User passed us an int/float. This is for backwards compatibility,\n            # can be removed later\n            return Timeout.from_float(timeout)\n\n    def _raise_timeout(self, err, url, timeout_value):\n        \"\"\"Is the error actually a timeout? Will raise a ReadTimeout or pass\"\"\"\n\n        if isinstance(err, SocketTimeout):\n            raise ReadTimeoutError(\n                self, url, \"Read timed out. (read timeout=%s)\" % timeout_value\n            )\n\n        # See the above comment about EAGAIN in Python 3. In Python 2 we have\n        # to specifically catch it and throw the timeout error\n        if hasattr(err, \"errno\") and err.errno in _blocking_errnos:\n            raise ReadTimeoutError(\n                self, url, \"Read timed out. (read timeout=%s)\" % timeout_value\n            )\n\n        # Catch possible read timeouts thrown as SSL errors. If not the\n        # case, rethrow the original. We need to do this because of:\n        # http://bugs.python.org/issue10272\n        if \"timed out\" in str(err) or \"did not complete (read)\" in str(\n            err\n        ):  # Python < 2.7.4\n            raise ReadTimeoutError(\n                self, url, \"Read timed out. (read timeout=%s)\" % timeout_value\n            )\n\n    def _make_request(\n        self, conn, method, url, timeout=_Default, chunked=False, **httplib_request_kw\n    ):\n        \"\"\"\n        Perform a request on a given urllib connection object taken from our\n        pool.\n\n        :param conn:\n            a connection from one of our connection pools\n\n        :param timeout:\n            Socket timeout in seconds for the request. This can be a\n            float or integer, which will set the same timeout value for\n            the socket connect and the socket read, or an instance of\n            :class:`urllib3.util.Timeout`, which gives you more fine-grained\n            control over your timeouts.\n        \"\"\"\n        self.num_requests += 1\n\n        timeout_obj = self._get_timeout(timeout)\n        timeout_obj.start_connect()\n        conn.timeout = timeout_obj.connect_timeout\n\n        # Trigger any extra validation we need to do.\n        try:\n            self._validate_conn(conn)\n        except (SocketTimeout, BaseSSLError) as e:\n            # Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout.\n            self._raise_timeout(err=e, url=url, timeout_value=conn.timeout)\n            raise\n\n        # conn.request() calls http.client.*.request, not the method in\n        # urllib3.request. It also calls makefile (recv) on the socket.\n        try:\n            if chunked:\n                conn.request_chunked(method, url, **httplib_request_kw)\n            else:\n                conn.request(method, url, **httplib_request_kw)\n\n        # We are swallowing BrokenPipeError (errno.EPIPE) since the server is\n        # legitimately able to close the connection after sending a valid response.\n        # With this behaviour, the received response is still readable.\n        except BrokenPipeError:\n            # Python 3\n            pass\n        except IOError as e:\n            # Python 2 and macOS/Linux\n            # EPIPE and ESHUTDOWN are BrokenPipeError on Python 2, and EPROTOTYPE is needed on macOS\n            # https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/\n            if e.errno not in {\n                errno.EPIPE,\n                errno.ESHUTDOWN,\n                errno.EPROTOTYPE,\n            }:\n                raise\n\n        # Reset the timeout for the recv() on the socket\n        read_timeout = timeout_obj.read_timeout\n\n        # App Engine doesn't have a sock attr\n        if getattr(conn, \"sock\", None):\n            # In Python 3 socket.py will catch EAGAIN and return None when you\n            # try and read into the file pointer created by http.client, which\n            # instead raises a BadStatusLine exception. Instead of catching\n            # the exception and assuming all BadStatusLine exceptions are read\n            # timeouts, check for a zero timeout before making the request.\n            if read_timeout == 0:\n                raise ReadTimeoutError(\n                    self, url, \"Read timed out. (read timeout=%s)\" % read_timeout\n                )\n            if read_timeout is Timeout.DEFAULT_TIMEOUT:\n                conn.sock.settimeout(socket.getdefaulttimeout())\n            else:  # None or a value\n                conn.sock.settimeout(read_timeout)\n\n        # Receive the response from the server\n        try:\n            try:\n                # Python 2.7, use buffering of HTTP responses\n                httplib_response = conn.getresponse(buffering=True)\n            except TypeError:\n                # Python 3\n                try:\n                    httplib_response = conn.getresponse()\n                except BaseException as e:\n                    # Remove the TypeError from the exception chain in\n                    # Python 3 (including for exceptions like SystemExit).\n                    # Otherwise it looks like a bug in the code.\n                    six.raise_from(e, None)\n        except (SocketTimeout, BaseSSLError, SocketError) as e:\n            self._raise_timeout(err=e, url=url, timeout_value=read_timeout)\n            raise\n\n        # AppEngine doesn't have a version attr.\n        http_version = getattr(conn, \"_http_vsn_str\", \"HTTP/?\")\n        log.debug(\n            '%s://%s:%s \"%s %s %s\" %s %s',\n            self.scheme,\n            self.host,\n            self.port,\n            method,\n            url,\n            http_version,\n            httplib_response.status,\n            httplib_response.length,\n        )\n\n        try:\n            assert_header_parsing(httplib_response.msg)\n        except (HeaderParsingError, TypeError) as hpe:  # Platform-specific: Python 3\n            log.warning(\n                \"Failed to parse headers (url=%s): %s\",\n                self._absolute_url(url),\n                hpe,\n                exc_info=True,\n            )\n\n        return httplib_response\n\n    def _absolute_url(self, path):\n        return Url(scheme=self.scheme, host=self.host, port=self.port, path=path).url\n\n    def close(self):\n        \"\"\"\n        Close all pooled connections and disable the pool.\n        \"\"\"\n        if self.pool is None:\n            return\n        # Disable access to the pool\n        old_pool, self.pool = self.pool, None\n\n        try:\n            while True:\n                conn = old_pool.get(block=False)\n                if conn:\n                    conn.close()\n\n        except queue.Empty:\n            pass  # Done.\n\n    def is_same_host(self, url):\n        \"\"\"\n        Check if the given ``url`` is a member of the same host as this\n        connection pool.\n        \"\"\"\n        if url.startswith(\"/\"):\n            return True\n\n        # TODO: Add optional support for socket.gethostbyname checking.\n        scheme, host, port = get_host(url)\n        if host is not None:\n            host = _normalize_host(host, scheme=scheme)\n\n        # Use explicit default port for comparison when none is given\n        if self.port and not port:\n            port = port_by_scheme.get(scheme)\n        elif not self.port and port == port_by_scheme.get(scheme):\n            port = None\n\n        return (scheme, host, port) == (self.scheme, self.host, self.port)\n\n    def urlopen(\n        self,\n        method,\n        url,\n        body=None,\n        headers=None,\n        retries=None,\n        redirect=True,\n        assert_same_host=True,\n        timeout=_Default,\n        pool_timeout=None,\n        release_conn=None,\n        chunked=False,\n        body_pos=None,\n        **response_kw\n    ):\n        \"\"\"\n        Get a connection from the pool and perform an HTTP request. This is the\n        lowest level call for making a request, so you'll need to specify all\n        the raw details.\n\n        .. note::\n\n           More commonly, it's appropriate to use a convenience method provided\n           by :class:`.RequestMethods`, such as :meth:`request`.\n\n        .. note::\n\n           `release_conn` will only behave as expected if\n           `preload_content=False` because we want to make\n           `preload_content=False` the default behaviour someday soon without\n           breaking backwards compatibility.\n\n        :param method:\n            HTTP request method (such as GET, POST, PUT, etc.)\n\n        :param url:\n            The URL to perform the request on.\n\n        :param body:\n            Data to send in the request body, either :class:`str`, :class:`bytes`,\n            an iterable of :class:`str`/:class:`bytes`, or a file-like object.\n\n        :param headers:\n            Dictionary of custom headers to send, such as User-Agent,\n            If-None-Match, etc. If None, pool headers are used. If provided,\n            these headers completely replace any pool-specific headers.\n\n        :param retries:\n            Configure the number of retries to allow before raising a\n            :class:`~urllib3.exceptions.MaxRetryError` exception.\n\n            Pass ``None`` to retry until you receive a response. Pass a\n            :class:`~urllib3.util.retry.Retry` object for fine-grained control\n            over different types of retries.\n            Pass an integer number to retry connection errors that many times,\n            but no other types of errors. Pass zero to never retry.\n\n            If ``False``, then retries are disabled and any exception is raised\n            immediately. Also, instead of raising a MaxRetryError on redirects,\n            the redirect response will be returned.\n\n        :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int.\n\n        :param redirect:\n            If True, automatically handle redirects (status codes 301, 302,\n            303, 307, 308). Each redirect counts as a retry. Disabling retries\n            will disable redirect, too.\n\n        :param assert_same_host:\n            If ``True``, will make sure that the host of the pool requests is\n            consistent else will raise HostChangedError. When ``False``, you can\n            use the pool on an HTTP proxy and request foreign hosts.\n\n        :param timeout:\n            If specified, overrides the default timeout for this one\n            request. It may be a float (in seconds) or an instance of\n            :class:`urllib3.util.Timeout`.\n\n        :param pool_timeout:\n            If set and the pool is set to block=True, then this method will\n            block for ``pool_timeout`` seconds and raise EmptyPoolError if no\n            connection is available within the time period.\n\n        :param release_conn:\n            If False, then the urlopen call will not release the connection\n            back into the pool once a response is received (but will release if\n            you read the entire contents of the response such as when\n            `preload_content=True`). This is useful if you're not preloading\n            the response's content immediately. You will need to call\n            ``r.release_conn()`` on the response ``r`` to return the connection\n            back into the pool. If None, it takes the value of\n            ``response_kw.get('preload_content', True)``.\n\n        :param chunked:\n            If True, urllib3 will send the body using chunked transfer\n            encoding. Otherwise, urllib3 will send the body using the standard\n            content-length form. Defaults to False.\n\n        :param int body_pos:\n            Position to seek to in file-like body in the event of a retry or\n            redirect. Typically this won't need to be set because urllib3 will\n            auto-populate the value when needed.\n\n        :param \\\\**response_kw:\n            Additional parameters are passed to\n            :meth:`urllib3.response.HTTPResponse.from_httplib`\n        \"\"\"\n\n        parsed_url = parse_url(url)\n        destination_scheme = parsed_url.scheme\n\n        if headers is None:\n            headers = self.headers\n\n        if not isinstance(retries, Retry):\n            retries = Retry.from_int(retries, redirect=redirect, default=self.retries)\n\n        if release_conn is None:\n            release_conn = response_kw.get(\"preload_content\", True)\n\n        # Check host\n        if assert_same_host and not self.is_same_host(url):\n            raise HostChangedError(self, url, retries)\n\n        # Ensure that the URL we're connecting to is properly encoded\n        if url.startswith(\"/\"):\n            url = six.ensure_str(_encode_target(url))\n        else:\n            url = six.ensure_str(parsed_url.url)\n\n        conn = None\n\n        # Track whether `conn` needs to be released before\n        # returning/raising/recursing. Update this variable if necessary, and\n        # leave `release_conn` constant throughout the function. That way, if\n        # the function recurses, the original value of `release_conn` will be\n        # passed down into the recursive call, and its value will be respected.\n        #\n        # See issue #651 [1] for details.\n        #\n        # [1] <https://github.com/urllib3/urllib3/issues/651>\n        release_this_conn = release_conn\n\n        http_tunnel_required = connection_requires_http_tunnel(\n            self.proxy, self.proxy_config, destination_scheme\n        )\n\n        # Merge the proxy headers. Only done when not using HTTP CONNECT. We\n        # have to copy the headers dict so we can safely change it without those\n        # changes being reflected in anyone else's copy.\n        if not http_tunnel_required:\n            headers = headers.copy()\n            headers.update(self.proxy_headers)\n\n        # Must keep the exception bound to a separate variable or else Python 3\n        # complains about UnboundLocalError.\n        err = None\n\n        # Keep track of whether we cleanly exited the except block. This\n        # ensures we do proper cleanup in finally.\n        clean_exit = False\n\n        # Rewind body position, if needed. Record current position\n        # for future rewinds in the event of a redirect/retry.\n        body_pos = set_file_position(body, body_pos)\n\n        try:\n            # Request a connection from the queue.\n            timeout_obj = self._get_timeout(timeout)\n            conn = self._get_conn(timeout=pool_timeout)\n\n            conn.timeout = timeout_obj.connect_timeout\n\n            is_new_proxy_conn = self.proxy is not None and not getattr(\n                conn, \"sock\", None\n            )\n            if is_new_proxy_conn and http_tunnel_required:\n                self._prepare_proxy(conn)\n\n            # Make the request on the httplib connection object.\n            httplib_response = self._make_request(\n                conn,\n                method,\n                url,\n                timeout=timeout_obj,\n                body=body,\n                headers=headers,\n                chunked=chunked,\n            )\n\n            # If we're going to release the connection in ``finally:``, then\n            # the response doesn't need to know about the connection. Otherwise\n            # it will also try to release it and we'll have a double-release\n            # mess.\n            response_conn = conn if not release_conn else None\n\n            # Pass method to Response for length checking\n            response_kw[\"request_method\"] = method\n\n            # Import httplib's response into our own wrapper object\n            response = self.ResponseCls.from_httplib(\n                httplib_response,\n                pool=self,\n                connection=response_conn,\n                retries=retries,\n                **response_kw\n            )\n\n            # Everything went great!\n            clean_exit = True\n\n        except EmptyPoolError:\n            # Didn't get a connection from the pool, no need to clean up\n            clean_exit = True\n            release_this_conn = False\n            raise\n\n        except (\n            TimeoutError,\n            HTTPException,\n            SocketError,\n            ProtocolError,\n            BaseSSLError,\n            SSLError,\n            CertificateError,\n        ) as e:\n            # Discard the connection for these exceptions. It will be\n            # replaced during the next _get_conn() call.\n            clean_exit = False\n\n            def _is_ssl_error_message_from_http_proxy(ssl_error):\n                # We're trying to detect the message 'WRONG_VERSION_NUMBER' but\n                # SSLErrors are kinda all over the place when it comes to the message,\n                # so we try to cover our bases here!\n                message = \" \".join(re.split(\"[^a-z]\", str(ssl_error).lower()))\n                return (\n                    \"wrong version number\" in message or \"unknown protocol\" in message\n                )\n\n            # Try to detect a common user error with proxies which is to\n            # set an HTTP proxy to be HTTPS when it should be 'http://'\n            # (ie {'http': 'http://proxy', 'https': 'https://proxy'})\n            # Instead we add a nice error message and point to a URL.\n            if (\n                isinstance(e, BaseSSLError)\n                and self.proxy\n                and _is_ssl_error_message_from_http_proxy(e)\n            ):\n                e = ProxyError(\n                    \"Your proxy appears to only use HTTP and not HTTPS, \"\n                    \"try changing your proxy URL to be HTTP. See: \"\n                    \"https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html\"\n                    \"#https-proxy-error-http-proxy\",\n                    SSLError(e),\n                )\n            elif isinstance(e, (BaseSSLError, CertificateError)):\n                e = SSLError(e)\n            elif isinstance(e, (SocketError, NewConnectionError)) and self.proxy:\n                e = ProxyError(\"Cannot connect to proxy.\", e)\n            elif isinstance(e, (SocketError, HTTPException)):\n                e = ProtocolError(\"Connection aborted.\", e)\n\n            retries = retries.increment(\n                method, url, error=e, _pool=self, _stacktrace=sys.exc_info()[2]\n            )\n            retries.sleep()\n\n            # Keep track of the error for the retry warning.\n            err = e\n\n        finally:\n            if not clean_exit:\n                # We hit some kind of exception, handled or otherwise. We need\n                # to throw the connection away unless explicitly told not to.\n                # Close the connection, set the variable to None, and make sure\n                # we put the None back in the pool to avoid leaking it.\n                conn = conn and conn.close()\n                release_this_conn = True\n\n            if release_this_conn:\n                # Put the connection back to be reused. If the connection is\n                # expired then it will be None, which will get replaced with a\n                # fresh connection during _get_conn.\n                self._put_conn(conn)\n\n        if not conn:\n            # Try again\n            log.warning(\n                \"Retrying (%r) after connection broken by '%r': %s\", retries, err, url\n            )\n            return self.urlopen(\n                method,\n                url,\n                body,\n                headers,\n                retries,\n                redirect,\n                assert_same_host,\n                timeout=timeout,\n                pool_timeout=pool_timeout,\n                release_conn=release_conn,\n                chunked=chunked,\n                body_pos=body_pos,\n                **response_kw\n            )\n\n        # Handle redirect?\n        redirect_location = redirect and response.get_redirect_location()\n        if redirect_location:\n            if response.status == 303:\n                method = \"GET\"\n\n            try:\n                retries = retries.increment(method, url, response=response, _pool=self)\n            except MaxRetryError:\n                if retries.raise_on_redirect:\n                    response.drain_conn()\n                    raise\n                return response\n\n            response.drain_conn()\n            retries.sleep_for_retry(response)\n            log.debug(\"Redirecting %s -> %s\", url, redirect_location)\n            return self.urlopen(\n                method,\n                redirect_location,\n                body,\n                headers,\n                retries=retries,\n                redirect=redirect,\n                assert_same_host=assert_same_host,\n                timeout=timeout,\n                pool_timeout=pool_timeout,\n                release_conn=release_conn,\n                chunked=chunked,\n                body_pos=body_pos,\n                **response_kw\n            )\n\n        # Check if we should retry the HTTP response.\n        has_retry_after = bool(response.getheader(\"Retry-After\"))\n        if retries.is_retry(method, response.status, has_retry_after):\n            try:\n                retries = retries.increment(method, url, response=response, _pool=self)\n            except MaxRetryError:\n                if retries.raise_on_status:\n                    response.drain_conn()\n                    raise\n                return response\n\n            response.drain_conn()\n            retries.sleep(response)\n            log.debug(\"Retry: %s\", url)\n            return self.urlopen(\n                method,\n                url,\n                body,\n                headers,\n                retries=retries,\n                redirect=redirect,\n                assert_same_host=assert_same_host,\n                timeout=timeout,\n                pool_timeout=pool_timeout,\n                release_conn=release_conn,\n                chunked=chunked,\n                body_pos=body_pos,\n                **response_kw\n            )\n\n        return response\n\n\nclass HTTPSConnectionPool(HTTPConnectionPool):\n    \"\"\"\n    Same as :class:`.HTTPConnectionPool`, but HTTPS.\n\n    :class:`.HTTPSConnection` uses one of ``assert_fingerprint``,\n    ``assert_hostname`` and ``host`` in this order to verify connections.\n    If ``assert_hostname`` is False, no verification is done.\n\n    The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``,\n    ``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl`\n    is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade\n    the connection socket into an SSL socket.\n    \"\"\"\n\n    scheme = \"https\"\n    ConnectionCls = HTTPSConnection\n\n    def __init__(\n        self,\n        host,\n        port=None,\n        strict=False,\n        timeout=Timeout.DEFAULT_TIMEOUT,\n        maxsize=1,\n        block=False,\n        headers=None,\n        retries=None,\n        _proxy=None,\n        _proxy_headers=None,\n        key_file=None,\n        cert_file=None,\n        cert_reqs=None,\n        key_password=None,\n        ca_certs=None,\n        ssl_version=None,\n        assert_hostname=None,\n        assert_fingerprint=None,\n        ca_cert_dir=None,\n        **conn_kw\n    ):\n\n        HTTPConnectionPool.__init__(\n            self,\n            host,\n            port,\n            strict,\n            timeout,\n            maxsize,\n            block,\n            headers,\n            retries,\n            _proxy,\n            _proxy_headers,\n            **conn_kw\n        )\n\n        self.key_file = key_file\n        self.cert_file = cert_file\n        self.cert_reqs = cert_reqs\n        self.key_password = key_password\n        self.ca_certs = ca_certs\n        self.ca_cert_dir = ca_cert_dir\n        self.ssl_version = ssl_version\n        self.assert_hostname = assert_hostname\n        self.assert_fingerprint = assert_fingerprint\n\n    def _prepare_conn(self, conn):\n        \"\"\"\n        Prepare the ``connection`` for :meth:`urllib3.util.ssl_wrap_socket`\n        and establish the tunnel if proxy is used.\n        \"\"\"\n\n        if isinstance(conn, VerifiedHTTPSConnection):\n            conn.set_cert(\n                key_file=self.key_file,\n                key_password=self.key_password,\n                cert_file=self.cert_file,\n                cert_reqs=self.cert_reqs,\n                ca_certs=self.ca_certs,\n                ca_cert_dir=self.ca_cert_dir,\n                assert_hostname=self.assert_hostname,\n                assert_fingerprint=self.assert_fingerprint,\n            )\n            conn.ssl_version = self.ssl_version\n        return conn\n\n    def _prepare_proxy(self, conn):\n        \"\"\"\n        Establishes a tunnel connection through HTTP CONNECT.\n\n        Tunnel connection is established early because otherwise httplib would\n        improperly set Host: header to proxy's IP:port.\n        \"\"\"\n\n        conn.set_tunnel(self._proxy_host, self.port, self.proxy_headers)\n\n        if self.proxy.scheme == \"https\":\n            conn.tls_in_tls_required = True\n\n        conn.connect()\n\n    def _new_conn(self):\n        \"\"\"\n        Return a fresh :class:`http.client.HTTPSConnection`.\n        \"\"\"\n        self.num_connections += 1\n        log.debug(\n            \"Starting new HTTPS connection (%d): %s:%s\",\n            self.num_connections,\n            self.host,\n            self.port or \"443\",\n        )\n\n        if not self.ConnectionCls or self.ConnectionCls is DummyConnection:\n            raise SSLError(\n                \"Can't connect to HTTPS URL because the SSL module is not available.\"\n            )\n\n        actual_host = self.host\n        actual_port = self.port\n        if self.proxy is not None:\n            actual_host = self.proxy.host\n            actual_port = self.proxy.port\n\n        conn = self.ConnectionCls(\n            host=actual_host,\n            port=actual_port,\n            timeout=self.timeout.connect_timeout,\n            strict=self.strict,\n            cert_file=self.cert_file,\n            key_file=self.key_file,\n            key_password=self.key_password,\n            **self.conn_kw\n        )\n\n        return self._prepare_conn(conn)\n\n    def _validate_conn(self, conn):\n        \"\"\"\n        Called right before a request is made, after the socket is created.\n        \"\"\"\n        super(HTTPSConnectionPool, self)._validate_conn(conn)\n\n        # Force connect early to allow us to validate the connection.\n        if not getattr(conn, \"sock\", None):  # AppEngine might not have  `.sock`\n            conn.connect()\n\n        if not conn.is_verified:\n            warnings.warn(\n                (\n                    \"Unverified HTTPS request is being made to host '%s'. \"\n                    \"Adding certificate verification is strongly advised. See: \"\n                    \"https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html\"\n                    \"#ssl-warnings\" % conn.host\n                ),\n                InsecureRequestWarning,\n            )\n\n        if getattr(conn, \"proxy_is_verified\", None) is False:\n            warnings.warn(\n                (\n                    \"Unverified HTTPS connection done to an HTTPS proxy. \"\n                    \"Adding certificate verification is strongly advised. See: \"\n                    \"https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html\"\n                    \"#ssl-warnings\"\n                ),\n                InsecureRequestWarning,\n            )\n\n\ndef connection_from_url(url, **kw):\n    \"\"\"\n    Given a url, return an :class:`.ConnectionPool` instance of its host.\n\n    This is a shortcut for not having to parse out the scheme, host, and port\n    of the url before creating an :class:`.ConnectionPool` instance.\n\n    :param url:\n        Absolute URL string that must include the scheme. Port is optional.\n\n    :param \\\\**kw:\n        Passes additional parameters to the constructor of the appropriate\n        :class:`.ConnectionPool`. Useful for specifying things like\n        timeout, maxsize, headers, etc.\n\n    Example::\n\n        >>> conn = connection_from_url('http://google.com/')\n        >>> r = conn.request('GET', '/')\n    \"\"\"\n    scheme, host, port = get_host(url)\n    port = port or port_by_scheme.get(scheme, 80)\n    if scheme == \"https\":\n        return HTTPSConnectionPool(host, port=port, **kw)\n    else:\n        return HTTPConnectionPool(host, port=port, **kw)\n\n\ndef _normalize_host(host, scheme):\n    \"\"\"\n    Normalize hosts for comparisons and use with sockets.\n    \"\"\"\n\n    host = normalize_host(host, scheme)\n\n    # httplib doesn't like it when we include brackets in IPv6 addresses\n    # Specifically, if we include brackets but also pass the port then\n    # httplib crazily doubles up the square brackets on the Host header.\n    # Instead, we need to make sure we never pass ``None`` as the port.\n    # However, for backward compatibility reasons we can't actually\n    # *assert* that.  See http://bugs.python.org/issue28539\n    if host.startswith(\"[\") and host.endswith(\"]\"):\n        host = host[1:-1]\n    return host\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/contrib/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/contrib/_appengine_environ.py",
    "content": "\"\"\"\nThis module provides means to detect the App Engine environment.\n\"\"\"\n\nimport os\n\n\ndef is_appengine():\n    return is_local_appengine() or is_prod_appengine()\n\n\ndef is_appengine_sandbox():\n    \"\"\"Reports if the app is running in the first generation sandbox.\n\n    The second generation runtimes are technically still in a sandbox, but it\n    is much less restrictive, so generally you shouldn't need to check for it.\n    see https://cloud.google.com/appengine/docs/standard/runtimes\n    \"\"\"\n    return is_appengine() and os.environ[\"APPENGINE_RUNTIME\"] == \"python27\"\n\n\ndef is_local_appengine():\n    return \"APPENGINE_RUNTIME\" in os.environ and os.environ.get(\n        \"SERVER_SOFTWARE\", \"\"\n    ).startswith(\"Development/\")\n\n\ndef is_prod_appengine():\n    return \"APPENGINE_RUNTIME\" in os.environ and os.environ.get(\n        \"SERVER_SOFTWARE\", \"\"\n    ).startswith(\"Google App Engine/\")\n\n\ndef is_prod_appengine_mvms():\n    \"\"\"Deprecated.\"\"\"\n    return False\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/contrib/_securetransport/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/contrib/_securetransport/bindings.py",
    "content": "\"\"\"\nThis module uses ctypes to bind a whole bunch of functions and constants from\nSecureTransport. The goal here is to provide the low-level API to\nSecureTransport. These are essentially the C-level functions and constants, and\nthey're pretty gross to work with.\n\nThis code is a bastardised version of the code found in Will Bond's oscrypto\nlibrary. An enormous debt is owed to him for blazing this trail for us. For\nthat reason, this code should be considered to be covered both by urllib3's\nlicense and by oscrypto's:\n\n    Copyright (c) 2015-2016 Will Bond <will@wbond.net>\n\n    Permission is hereby granted, free of charge, to any person obtaining a\n    copy of this software and associated documentation files (the \"Software\"),\n    to deal in the Software without restriction, including without limitation\n    the rights to use, copy, modify, merge, publish, distribute, sublicense,\n    and/or sell copies of the Software, and to permit persons to whom the\n    Software is furnished to do so, subject to the following conditions:\n\n    The above copyright notice and this permission notice shall be included in\n    all copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n    DEALINGS IN THE SOFTWARE.\n\"\"\"\nfrom __future__ import absolute_import\n\nimport platform\nfrom ctypes import (\n    CDLL,\n    CFUNCTYPE,\n    POINTER,\n    c_bool,\n    c_byte,\n    c_char_p,\n    c_int32,\n    c_long,\n    c_size_t,\n    c_uint32,\n    c_ulong,\n    c_void_p,\n)\nfrom ctypes.util import find_library\n\nfrom ...packages.six import raise_from\n\nif platform.system() != \"Darwin\":\n    raise ImportError(\"Only macOS is supported\")\n\nversion = platform.mac_ver()[0]\nversion_info = tuple(map(int, version.split(\".\")))\nif version_info < (10, 8):\n    raise OSError(\n        \"Only OS X 10.8 and newer are supported, not %s.%s\"\n        % (version_info[0], version_info[1])\n    )\n\n\ndef load_cdll(name, macos10_16_path):\n    \"\"\"Loads a CDLL by name, falling back to known path on 10.16+\"\"\"\n    try:\n        # Big Sur is technically 11 but we use 10.16 due to the Big Sur\n        # beta being labeled as 10.16.\n        if version_info >= (10, 16):\n            path = macos10_16_path\n        else:\n            path = find_library(name)\n        if not path:\n            raise OSError  # Caught and reraised as 'ImportError'\n        return CDLL(path, use_errno=True)\n    except OSError:\n        raise_from(ImportError(\"The library %s failed to load\" % name), None)\n\n\nSecurity = load_cdll(\n    \"Security\", \"/System/Library/Frameworks/Security.framework/Security\"\n)\nCoreFoundation = load_cdll(\n    \"CoreFoundation\",\n    \"/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation\",\n)\n\n\nBoolean = c_bool\nCFIndex = c_long\nCFStringEncoding = c_uint32\nCFData = c_void_p\nCFString = c_void_p\nCFArray = c_void_p\nCFMutableArray = c_void_p\nCFDictionary = c_void_p\nCFError = c_void_p\nCFType = c_void_p\nCFTypeID = c_ulong\n\nCFTypeRef = POINTER(CFType)\nCFAllocatorRef = c_void_p\n\nOSStatus = c_int32\n\nCFDataRef = POINTER(CFData)\nCFStringRef = POINTER(CFString)\nCFArrayRef = POINTER(CFArray)\nCFMutableArrayRef = POINTER(CFMutableArray)\nCFDictionaryRef = POINTER(CFDictionary)\nCFArrayCallBacks = c_void_p\nCFDictionaryKeyCallBacks = c_void_p\nCFDictionaryValueCallBacks = c_void_p\n\nSecCertificateRef = POINTER(c_void_p)\nSecExternalFormat = c_uint32\nSecExternalItemType = c_uint32\nSecIdentityRef = POINTER(c_void_p)\nSecItemImportExportFlags = c_uint32\nSecItemImportExportKeyParameters = c_void_p\nSecKeychainRef = POINTER(c_void_p)\nSSLProtocol = c_uint32\nSSLCipherSuite = c_uint32\nSSLContextRef = POINTER(c_void_p)\nSecTrustRef = POINTER(c_void_p)\nSSLConnectionRef = c_uint32\nSecTrustResultType = c_uint32\nSecTrustOptionFlags = c_uint32\nSSLProtocolSide = c_uint32\nSSLConnectionType = c_uint32\nSSLSessionOption = c_uint32\n\n\ntry:\n    Security.SecItemImport.argtypes = [\n        CFDataRef,\n        CFStringRef,\n        POINTER(SecExternalFormat),\n        POINTER(SecExternalItemType),\n        SecItemImportExportFlags,\n        POINTER(SecItemImportExportKeyParameters),\n        SecKeychainRef,\n        POINTER(CFArrayRef),\n    ]\n    Security.SecItemImport.restype = OSStatus\n\n    Security.SecCertificateGetTypeID.argtypes = []\n    Security.SecCertificateGetTypeID.restype = CFTypeID\n\n    Security.SecIdentityGetTypeID.argtypes = []\n    Security.SecIdentityGetTypeID.restype = CFTypeID\n\n    Security.SecKeyGetTypeID.argtypes = []\n    Security.SecKeyGetTypeID.restype = CFTypeID\n\n    Security.SecCertificateCreateWithData.argtypes = [CFAllocatorRef, CFDataRef]\n    Security.SecCertificateCreateWithData.restype = SecCertificateRef\n\n    Security.SecCertificateCopyData.argtypes = [SecCertificateRef]\n    Security.SecCertificateCopyData.restype = CFDataRef\n\n    Security.SecCopyErrorMessageString.argtypes = [OSStatus, c_void_p]\n    Security.SecCopyErrorMessageString.restype = CFStringRef\n\n    Security.SecIdentityCreateWithCertificate.argtypes = [\n        CFTypeRef,\n        SecCertificateRef,\n        POINTER(SecIdentityRef),\n    ]\n    Security.SecIdentityCreateWithCertificate.restype = OSStatus\n\n    Security.SecKeychainCreate.argtypes = [\n        c_char_p,\n        c_uint32,\n        c_void_p,\n        Boolean,\n        c_void_p,\n        POINTER(SecKeychainRef),\n    ]\n    Security.SecKeychainCreate.restype = OSStatus\n\n    Security.SecKeychainDelete.argtypes = [SecKeychainRef]\n    Security.SecKeychainDelete.restype = OSStatus\n\n    Security.SecPKCS12Import.argtypes = [\n        CFDataRef,\n        CFDictionaryRef,\n        POINTER(CFArrayRef),\n    ]\n    Security.SecPKCS12Import.restype = OSStatus\n\n    SSLReadFunc = CFUNCTYPE(OSStatus, SSLConnectionRef, c_void_p, POINTER(c_size_t))\n    SSLWriteFunc = CFUNCTYPE(\n        OSStatus, SSLConnectionRef, POINTER(c_byte), POINTER(c_size_t)\n    )\n\n    Security.SSLSetIOFuncs.argtypes = [SSLContextRef, SSLReadFunc, SSLWriteFunc]\n    Security.SSLSetIOFuncs.restype = OSStatus\n\n    Security.SSLSetPeerID.argtypes = [SSLContextRef, c_char_p, c_size_t]\n    Security.SSLSetPeerID.restype = OSStatus\n\n    Security.SSLSetCertificate.argtypes = [SSLContextRef, CFArrayRef]\n    Security.SSLSetCertificate.restype = OSStatus\n\n    Security.SSLSetCertificateAuthorities.argtypes = [SSLContextRef, CFTypeRef, Boolean]\n    Security.SSLSetCertificateAuthorities.restype = OSStatus\n\n    Security.SSLSetConnection.argtypes = [SSLContextRef, SSLConnectionRef]\n    Security.SSLSetConnection.restype = OSStatus\n\n    Security.SSLSetPeerDomainName.argtypes = [SSLContextRef, c_char_p, c_size_t]\n    Security.SSLSetPeerDomainName.restype = OSStatus\n\n    Security.SSLHandshake.argtypes = [SSLContextRef]\n    Security.SSLHandshake.restype = OSStatus\n\n    Security.SSLRead.argtypes = [SSLContextRef, c_char_p, c_size_t, POINTER(c_size_t)]\n    Security.SSLRead.restype = OSStatus\n\n    Security.SSLWrite.argtypes = [SSLContextRef, c_char_p, c_size_t, POINTER(c_size_t)]\n    Security.SSLWrite.restype = OSStatus\n\n    Security.SSLClose.argtypes = [SSLContextRef]\n    Security.SSLClose.restype = OSStatus\n\n    Security.SSLGetNumberSupportedCiphers.argtypes = [SSLContextRef, POINTER(c_size_t)]\n    Security.SSLGetNumberSupportedCiphers.restype = OSStatus\n\n    Security.SSLGetSupportedCiphers.argtypes = [\n        SSLContextRef,\n        POINTER(SSLCipherSuite),\n        POINTER(c_size_t),\n    ]\n    Security.SSLGetSupportedCiphers.restype = OSStatus\n\n    Security.SSLSetEnabledCiphers.argtypes = [\n        SSLContextRef,\n        POINTER(SSLCipherSuite),\n        c_size_t,\n    ]\n    Security.SSLSetEnabledCiphers.restype = OSStatus\n\n    Security.SSLGetNumberEnabledCiphers.argtype = [SSLContextRef, POINTER(c_size_t)]\n    Security.SSLGetNumberEnabledCiphers.restype = OSStatus\n\n    Security.SSLGetEnabledCiphers.argtypes = [\n        SSLContextRef,\n        POINTER(SSLCipherSuite),\n        POINTER(c_size_t),\n    ]\n    Security.SSLGetEnabledCiphers.restype = OSStatus\n\n    Security.SSLGetNegotiatedCipher.argtypes = [SSLContextRef, POINTER(SSLCipherSuite)]\n    Security.SSLGetNegotiatedCipher.restype = OSStatus\n\n    Security.SSLGetNegotiatedProtocolVersion.argtypes = [\n        SSLContextRef,\n        POINTER(SSLProtocol),\n    ]\n    Security.SSLGetNegotiatedProtocolVersion.restype = OSStatus\n\n    Security.SSLCopyPeerTrust.argtypes = [SSLContextRef, POINTER(SecTrustRef)]\n    Security.SSLCopyPeerTrust.restype = OSStatus\n\n    Security.SecTrustSetAnchorCertificates.argtypes = [SecTrustRef, CFArrayRef]\n    Security.SecTrustSetAnchorCertificates.restype = OSStatus\n\n    Security.SecTrustSetAnchorCertificatesOnly.argstypes = [SecTrustRef, Boolean]\n    Security.SecTrustSetAnchorCertificatesOnly.restype = OSStatus\n\n    Security.SecTrustEvaluate.argtypes = [SecTrustRef, POINTER(SecTrustResultType)]\n    Security.SecTrustEvaluate.restype = OSStatus\n\n    Security.SecTrustGetCertificateCount.argtypes = [SecTrustRef]\n    Security.SecTrustGetCertificateCount.restype = CFIndex\n\n    Security.SecTrustGetCertificateAtIndex.argtypes = [SecTrustRef, CFIndex]\n    Security.SecTrustGetCertificateAtIndex.restype = SecCertificateRef\n\n    Security.SSLCreateContext.argtypes = [\n        CFAllocatorRef,\n        SSLProtocolSide,\n        SSLConnectionType,\n    ]\n    Security.SSLCreateContext.restype = SSLContextRef\n\n    Security.SSLSetSessionOption.argtypes = [SSLContextRef, SSLSessionOption, Boolean]\n    Security.SSLSetSessionOption.restype = OSStatus\n\n    Security.SSLSetProtocolVersionMin.argtypes = [SSLContextRef, SSLProtocol]\n    Security.SSLSetProtocolVersionMin.restype = OSStatus\n\n    Security.SSLSetProtocolVersionMax.argtypes = [SSLContextRef, SSLProtocol]\n    Security.SSLSetProtocolVersionMax.restype = OSStatus\n\n    try:\n        Security.SSLSetALPNProtocols.argtypes = [SSLContextRef, CFArrayRef]\n        Security.SSLSetALPNProtocols.restype = OSStatus\n    except AttributeError:\n        # Supported only in 10.12+\n        pass\n\n    Security.SecCopyErrorMessageString.argtypes = [OSStatus, c_void_p]\n    Security.SecCopyErrorMessageString.restype = CFStringRef\n\n    Security.SSLReadFunc = SSLReadFunc\n    Security.SSLWriteFunc = SSLWriteFunc\n    Security.SSLContextRef = SSLContextRef\n    Security.SSLProtocol = SSLProtocol\n    Security.SSLCipherSuite = SSLCipherSuite\n    Security.SecIdentityRef = SecIdentityRef\n    Security.SecKeychainRef = SecKeychainRef\n    Security.SecTrustRef = SecTrustRef\n    Security.SecTrustResultType = SecTrustResultType\n    Security.SecExternalFormat = SecExternalFormat\n    Security.OSStatus = OSStatus\n\n    Security.kSecImportExportPassphrase = CFStringRef.in_dll(\n        Security, \"kSecImportExportPassphrase\"\n    )\n    Security.kSecImportItemIdentity = CFStringRef.in_dll(\n        Security, \"kSecImportItemIdentity\"\n    )\n\n    # CoreFoundation time!\n    CoreFoundation.CFRetain.argtypes = [CFTypeRef]\n    CoreFoundation.CFRetain.restype = CFTypeRef\n\n    CoreFoundation.CFRelease.argtypes = [CFTypeRef]\n    CoreFoundation.CFRelease.restype = None\n\n    CoreFoundation.CFGetTypeID.argtypes = [CFTypeRef]\n    CoreFoundation.CFGetTypeID.restype = CFTypeID\n\n    CoreFoundation.CFStringCreateWithCString.argtypes = [\n        CFAllocatorRef,\n        c_char_p,\n        CFStringEncoding,\n    ]\n    CoreFoundation.CFStringCreateWithCString.restype = CFStringRef\n\n    CoreFoundation.CFStringGetCStringPtr.argtypes = [CFStringRef, CFStringEncoding]\n    CoreFoundation.CFStringGetCStringPtr.restype = c_char_p\n\n    CoreFoundation.CFStringGetCString.argtypes = [\n        CFStringRef,\n        c_char_p,\n        CFIndex,\n        CFStringEncoding,\n    ]\n    CoreFoundation.CFStringGetCString.restype = c_bool\n\n    CoreFoundation.CFDataCreate.argtypes = [CFAllocatorRef, c_char_p, CFIndex]\n    CoreFoundation.CFDataCreate.restype = CFDataRef\n\n    CoreFoundation.CFDataGetLength.argtypes = [CFDataRef]\n    CoreFoundation.CFDataGetLength.restype = CFIndex\n\n    CoreFoundation.CFDataGetBytePtr.argtypes = [CFDataRef]\n    CoreFoundation.CFDataGetBytePtr.restype = c_void_p\n\n    CoreFoundation.CFDictionaryCreate.argtypes = [\n        CFAllocatorRef,\n        POINTER(CFTypeRef),\n        POINTER(CFTypeRef),\n        CFIndex,\n        CFDictionaryKeyCallBacks,\n        CFDictionaryValueCallBacks,\n    ]\n    CoreFoundation.CFDictionaryCreate.restype = CFDictionaryRef\n\n    CoreFoundation.CFDictionaryGetValue.argtypes = [CFDictionaryRef, CFTypeRef]\n    CoreFoundation.CFDictionaryGetValue.restype = CFTypeRef\n\n    CoreFoundation.CFArrayCreate.argtypes = [\n        CFAllocatorRef,\n        POINTER(CFTypeRef),\n        CFIndex,\n        CFArrayCallBacks,\n    ]\n    CoreFoundation.CFArrayCreate.restype = CFArrayRef\n\n    CoreFoundation.CFArrayCreateMutable.argtypes = [\n        CFAllocatorRef,\n        CFIndex,\n        CFArrayCallBacks,\n    ]\n    CoreFoundation.CFArrayCreateMutable.restype = CFMutableArrayRef\n\n    CoreFoundation.CFArrayAppendValue.argtypes = [CFMutableArrayRef, c_void_p]\n    CoreFoundation.CFArrayAppendValue.restype = None\n\n    CoreFoundation.CFArrayGetCount.argtypes = [CFArrayRef]\n    CoreFoundation.CFArrayGetCount.restype = CFIndex\n\n    CoreFoundation.CFArrayGetValueAtIndex.argtypes = [CFArrayRef, CFIndex]\n    CoreFoundation.CFArrayGetValueAtIndex.restype = c_void_p\n\n    CoreFoundation.kCFAllocatorDefault = CFAllocatorRef.in_dll(\n        CoreFoundation, \"kCFAllocatorDefault\"\n    )\n    CoreFoundation.kCFTypeArrayCallBacks = c_void_p.in_dll(\n        CoreFoundation, \"kCFTypeArrayCallBacks\"\n    )\n    CoreFoundation.kCFTypeDictionaryKeyCallBacks = c_void_p.in_dll(\n        CoreFoundation, \"kCFTypeDictionaryKeyCallBacks\"\n    )\n    CoreFoundation.kCFTypeDictionaryValueCallBacks = c_void_p.in_dll(\n        CoreFoundation, \"kCFTypeDictionaryValueCallBacks\"\n    )\n\n    CoreFoundation.CFTypeRef = CFTypeRef\n    CoreFoundation.CFArrayRef = CFArrayRef\n    CoreFoundation.CFStringRef = CFStringRef\n    CoreFoundation.CFDictionaryRef = CFDictionaryRef\n\nexcept (AttributeError):\n    raise ImportError(\"Error initializing ctypes\")\n\n\nclass CFConst(object):\n    \"\"\"\n    A class object that acts as essentially a namespace for CoreFoundation\n    constants.\n    \"\"\"\n\n    kCFStringEncodingUTF8 = CFStringEncoding(0x08000100)\n\n\nclass SecurityConst(object):\n    \"\"\"\n    A class object that acts as essentially a namespace for Security constants.\n    \"\"\"\n\n    kSSLSessionOptionBreakOnServerAuth = 0\n\n    kSSLProtocol2 = 1\n    kSSLProtocol3 = 2\n    kTLSProtocol1 = 4\n    kTLSProtocol11 = 7\n    kTLSProtocol12 = 8\n    # SecureTransport does not support TLS 1.3 even if there's a constant for it\n    kTLSProtocol13 = 10\n    kTLSProtocolMaxSupported = 999\n\n    kSSLClientSide = 1\n    kSSLStreamType = 0\n\n    kSecFormatPEMSequence = 10\n\n    kSecTrustResultInvalid = 0\n    kSecTrustResultProceed = 1\n    # This gap is present on purpose: this was kSecTrustResultConfirm, which\n    # is deprecated.\n    kSecTrustResultDeny = 3\n    kSecTrustResultUnspecified = 4\n    kSecTrustResultRecoverableTrustFailure = 5\n    kSecTrustResultFatalTrustFailure = 6\n    kSecTrustResultOtherError = 7\n\n    errSSLProtocol = -9800\n    errSSLWouldBlock = -9803\n    errSSLClosedGraceful = -9805\n    errSSLClosedNoNotify = -9816\n    errSSLClosedAbort = -9806\n\n    errSSLXCertChainInvalid = -9807\n    errSSLCrypto = -9809\n    errSSLInternal = -9810\n    errSSLCertExpired = -9814\n    errSSLCertNotYetValid = -9815\n    errSSLUnknownRootCert = -9812\n    errSSLNoRootCert = -9813\n    errSSLHostNameMismatch = -9843\n    errSSLPeerHandshakeFail = -9824\n    errSSLPeerUserCancelled = -9839\n    errSSLWeakPeerEphemeralDHKey = -9850\n    errSSLServerAuthCompleted = -9841\n    errSSLRecordOverflow = -9847\n\n    errSecVerifyFailed = -67808\n    errSecNoTrustSettings = -25263\n    errSecItemNotFound = -25300\n    errSecInvalidTrustSettings = -25262\n\n    # Cipher suites. We only pick the ones our default cipher string allows.\n    # Source: https://developer.apple.com/documentation/security/1550981-ssl_cipher_suite_values\n    TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xC02C\n    TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 0xC030\n    TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 0xC02B\n    TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xC02F\n    TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 0xCCA9\n    TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 0xCCA8\n    TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 = 0x009F\n    TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 = 0x009E\n    TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = 0xC024\n    TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 = 0xC028\n    TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA = 0xC00A\n    TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA = 0xC014\n    TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 = 0x006B\n    TLS_DHE_RSA_WITH_AES_256_CBC_SHA = 0x0039\n    TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 = 0xC023\n    TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 = 0xC027\n    TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA = 0xC009\n    TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA = 0xC013\n    TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 = 0x0067\n    TLS_DHE_RSA_WITH_AES_128_CBC_SHA = 0x0033\n    TLS_RSA_WITH_AES_256_GCM_SHA384 = 0x009D\n    TLS_RSA_WITH_AES_128_GCM_SHA256 = 0x009C\n    TLS_RSA_WITH_AES_256_CBC_SHA256 = 0x003D\n    TLS_RSA_WITH_AES_128_CBC_SHA256 = 0x003C\n    TLS_RSA_WITH_AES_256_CBC_SHA = 0x0035\n    TLS_RSA_WITH_AES_128_CBC_SHA = 0x002F\n    TLS_AES_128_GCM_SHA256 = 0x1301\n    TLS_AES_256_GCM_SHA384 = 0x1302\n    TLS_AES_128_CCM_8_SHA256 = 0x1305\n    TLS_AES_128_CCM_SHA256 = 0x1304\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/contrib/_securetransport/low_level.py",
    "content": "\"\"\"\nLow-level helpers for the SecureTransport bindings.\n\nThese are Python functions that are not directly related to the high-level APIs\nbut are necessary to get them to work. They include a whole bunch of low-level\nCoreFoundation messing about and memory management. The concerns in this module\nare almost entirely about trying to avoid memory leaks and providing\nappropriate and useful assistance to the higher-level code.\n\"\"\"\nimport base64\nimport ctypes\nimport itertools\nimport os\nimport re\nimport ssl\nimport struct\nimport tempfile\n\nfrom .bindings import CFConst, CoreFoundation, Security\n\n# This regular expression is used to grab PEM data out of a PEM bundle.\n_PEM_CERTS_RE = re.compile(\n    b\"-----BEGIN CERTIFICATE-----\\n(.*?)\\n-----END CERTIFICATE-----\", re.DOTALL\n)\n\n\ndef _cf_data_from_bytes(bytestring):\n    \"\"\"\n    Given a bytestring, create a CFData object from it. This CFData object must\n    be CFReleased by the caller.\n    \"\"\"\n    return CoreFoundation.CFDataCreate(\n        CoreFoundation.kCFAllocatorDefault, bytestring, len(bytestring)\n    )\n\n\ndef _cf_dictionary_from_tuples(tuples):\n    \"\"\"\n    Given a list of Python tuples, create an associated CFDictionary.\n    \"\"\"\n    dictionary_size = len(tuples)\n\n    # We need to get the dictionary keys and values out in the same order.\n    keys = (t[0] for t in tuples)\n    values = (t[1] for t in tuples)\n    cf_keys = (CoreFoundation.CFTypeRef * dictionary_size)(*keys)\n    cf_values = (CoreFoundation.CFTypeRef * dictionary_size)(*values)\n\n    return CoreFoundation.CFDictionaryCreate(\n        CoreFoundation.kCFAllocatorDefault,\n        cf_keys,\n        cf_values,\n        dictionary_size,\n        CoreFoundation.kCFTypeDictionaryKeyCallBacks,\n        CoreFoundation.kCFTypeDictionaryValueCallBacks,\n    )\n\n\ndef _cfstr(py_bstr):\n    \"\"\"\n    Given a Python binary data, create a CFString.\n    The string must be CFReleased by the caller.\n    \"\"\"\n    c_str = ctypes.c_char_p(py_bstr)\n    cf_str = CoreFoundation.CFStringCreateWithCString(\n        CoreFoundation.kCFAllocatorDefault,\n        c_str,\n        CFConst.kCFStringEncodingUTF8,\n    )\n    return cf_str\n\n\ndef _create_cfstring_array(lst):\n    \"\"\"\n    Given a list of Python binary data, create an associated CFMutableArray.\n    The array must be CFReleased by the caller.\n\n    Raises an ssl.SSLError on failure.\n    \"\"\"\n    cf_arr = None\n    try:\n        cf_arr = CoreFoundation.CFArrayCreateMutable(\n            CoreFoundation.kCFAllocatorDefault,\n            0,\n            ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks),\n        )\n        if not cf_arr:\n            raise MemoryError(\"Unable to allocate memory!\")\n        for item in lst:\n            cf_str = _cfstr(item)\n            if not cf_str:\n                raise MemoryError(\"Unable to allocate memory!\")\n            try:\n                CoreFoundation.CFArrayAppendValue(cf_arr, cf_str)\n            finally:\n                CoreFoundation.CFRelease(cf_str)\n    except BaseException as e:\n        if cf_arr:\n            CoreFoundation.CFRelease(cf_arr)\n        raise ssl.SSLError(\"Unable to allocate array: %s\" % (e,))\n    return cf_arr\n\n\ndef _cf_string_to_unicode(value):\n    \"\"\"\n    Creates a Unicode string from a CFString object. Used entirely for error\n    reporting.\n\n    Yes, it annoys me quite a lot that this function is this complex.\n    \"\"\"\n    value_as_void_p = ctypes.cast(value, ctypes.POINTER(ctypes.c_void_p))\n\n    string = CoreFoundation.CFStringGetCStringPtr(\n        value_as_void_p, CFConst.kCFStringEncodingUTF8\n    )\n    if string is None:\n        buffer = ctypes.create_string_buffer(1024)\n        result = CoreFoundation.CFStringGetCString(\n            value_as_void_p, buffer, 1024, CFConst.kCFStringEncodingUTF8\n        )\n        if not result:\n            raise OSError(\"Error copying C string from CFStringRef\")\n        string = buffer.value\n    if string is not None:\n        string = string.decode(\"utf-8\")\n    return string\n\n\ndef _assert_no_error(error, exception_class=None):\n    \"\"\"\n    Checks the return code and throws an exception if there is an error to\n    report\n    \"\"\"\n    if error == 0:\n        return\n\n    cf_error_string = Security.SecCopyErrorMessageString(error, None)\n    output = _cf_string_to_unicode(cf_error_string)\n    CoreFoundation.CFRelease(cf_error_string)\n\n    if output is None or output == u\"\":\n        output = u\"OSStatus %s\" % error\n\n    if exception_class is None:\n        exception_class = ssl.SSLError\n\n    raise exception_class(output)\n\n\ndef _cert_array_from_pem(pem_bundle):\n    \"\"\"\n    Given a bundle of certs in PEM format, turns them into a CFArray of certs\n    that can be used to validate a cert chain.\n    \"\"\"\n    # Normalize the PEM bundle's line endings.\n    pem_bundle = pem_bundle.replace(b\"\\r\\n\", b\"\\n\")\n\n    der_certs = [\n        base64.b64decode(match.group(1)) for match in _PEM_CERTS_RE.finditer(pem_bundle)\n    ]\n    if not der_certs:\n        raise ssl.SSLError(\"No root certificates specified\")\n\n    cert_array = CoreFoundation.CFArrayCreateMutable(\n        CoreFoundation.kCFAllocatorDefault,\n        0,\n        ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks),\n    )\n    if not cert_array:\n        raise ssl.SSLError(\"Unable to allocate memory!\")\n\n    try:\n        for der_bytes in der_certs:\n            certdata = _cf_data_from_bytes(der_bytes)\n            if not certdata:\n                raise ssl.SSLError(\"Unable to allocate memory!\")\n            cert = Security.SecCertificateCreateWithData(\n                CoreFoundation.kCFAllocatorDefault, certdata\n            )\n            CoreFoundation.CFRelease(certdata)\n            if not cert:\n                raise ssl.SSLError(\"Unable to build cert object!\")\n\n            CoreFoundation.CFArrayAppendValue(cert_array, cert)\n            CoreFoundation.CFRelease(cert)\n    except Exception:\n        # We need to free the array before the exception bubbles further.\n        # We only want to do that if an error occurs: otherwise, the caller\n        # should free.\n        CoreFoundation.CFRelease(cert_array)\n        raise\n\n    return cert_array\n\n\ndef _is_cert(item):\n    \"\"\"\n    Returns True if a given CFTypeRef is a certificate.\n    \"\"\"\n    expected = Security.SecCertificateGetTypeID()\n    return CoreFoundation.CFGetTypeID(item) == expected\n\n\ndef _is_identity(item):\n    \"\"\"\n    Returns True if a given CFTypeRef is an identity.\n    \"\"\"\n    expected = Security.SecIdentityGetTypeID()\n    return CoreFoundation.CFGetTypeID(item) == expected\n\n\ndef _temporary_keychain():\n    \"\"\"\n    This function creates a temporary Mac keychain that we can use to work with\n    credentials. This keychain uses a one-time password and a temporary file to\n    store the data. We expect to have one keychain per socket. The returned\n    SecKeychainRef must be freed by the caller, including calling\n    SecKeychainDelete.\n\n    Returns a tuple of the SecKeychainRef and the path to the temporary\n    directory that contains it.\n    \"\"\"\n    # Unfortunately, SecKeychainCreate requires a path to a keychain. This\n    # means we cannot use mkstemp to use a generic temporary file. Instead,\n    # we're going to create a temporary directory and a filename to use there.\n    # This filename will be 8 random bytes expanded into base64. We also need\n    # some random bytes to password-protect the keychain we're creating, so we\n    # ask for 40 random bytes.\n    random_bytes = os.urandom(40)\n    filename = base64.b16encode(random_bytes[:8]).decode(\"utf-8\")\n    password = base64.b16encode(random_bytes[8:])  # Must be valid UTF-8\n    tempdirectory = tempfile.mkdtemp()\n\n    keychain_path = os.path.join(tempdirectory, filename).encode(\"utf-8\")\n\n    # We now want to create the keychain itself.\n    keychain = Security.SecKeychainRef()\n    status = Security.SecKeychainCreate(\n        keychain_path, len(password), password, False, None, ctypes.byref(keychain)\n    )\n    _assert_no_error(status)\n\n    # Having created the keychain, we want to pass it off to the caller.\n    return keychain, tempdirectory\n\n\ndef _load_items_from_file(keychain, path):\n    \"\"\"\n    Given a single file, loads all the trust objects from it into arrays and\n    the keychain.\n    Returns a tuple of lists: the first list is a list of identities, the\n    second a list of certs.\n    \"\"\"\n    certificates = []\n    identities = []\n    result_array = None\n\n    with open(path, \"rb\") as f:\n        raw_filedata = f.read()\n\n    try:\n        filedata = CoreFoundation.CFDataCreate(\n            CoreFoundation.kCFAllocatorDefault, raw_filedata, len(raw_filedata)\n        )\n        result_array = CoreFoundation.CFArrayRef()\n        result = Security.SecItemImport(\n            filedata,  # cert data\n            None,  # Filename, leaving it out for now\n            None,  # What the type of the file is, we don't care\n            None,  # what's in the file, we don't care\n            0,  # import flags\n            None,  # key params, can include passphrase in the future\n            keychain,  # The keychain to insert into\n            ctypes.byref(result_array),  # Results\n        )\n        _assert_no_error(result)\n\n        # A CFArray is not very useful to us as an intermediary\n        # representation, so we are going to extract the objects we want\n        # and then free the array. We don't need to keep hold of keys: the\n        # keychain already has them!\n        result_count = CoreFoundation.CFArrayGetCount(result_array)\n        for index in range(result_count):\n            item = CoreFoundation.CFArrayGetValueAtIndex(result_array, index)\n            item = ctypes.cast(item, CoreFoundation.CFTypeRef)\n\n            if _is_cert(item):\n                CoreFoundation.CFRetain(item)\n                certificates.append(item)\n            elif _is_identity(item):\n                CoreFoundation.CFRetain(item)\n                identities.append(item)\n    finally:\n        if result_array:\n            CoreFoundation.CFRelease(result_array)\n\n        CoreFoundation.CFRelease(filedata)\n\n    return (identities, certificates)\n\n\ndef _load_client_cert_chain(keychain, *paths):\n    \"\"\"\n    Load certificates and maybe keys from a number of files. Has the end goal\n    of returning a CFArray containing one SecIdentityRef, and then zero or more\n    SecCertificateRef objects, suitable for use as a client certificate trust\n    chain.\n    \"\"\"\n    # Ok, the strategy.\n    #\n    # This relies on knowing that macOS will not give you a SecIdentityRef\n    # unless you have imported a key into a keychain. This is a somewhat\n    # artificial limitation of macOS (for example, it doesn't necessarily\n    # affect iOS), but there is nothing inside Security.framework that lets you\n    # get a SecIdentityRef without having a key in a keychain.\n    #\n    # So the policy here is we take all the files and iterate them in order.\n    # Each one will use SecItemImport to have one or more objects loaded from\n    # it. We will also point at a keychain that macOS can use to work with the\n    # private key.\n    #\n    # Once we have all the objects, we'll check what we actually have. If we\n    # already have a SecIdentityRef in hand, fab: we'll use that. Otherwise,\n    # we'll take the first certificate (which we assume to be our leaf) and\n    # ask the keychain to give us a SecIdentityRef with that cert's associated\n    # key.\n    #\n    # We'll then return a CFArray containing the trust chain: one\n    # SecIdentityRef and then zero-or-more SecCertificateRef objects. The\n    # responsibility for freeing this CFArray will be with the caller. This\n    # CFArray must remain alive for the entire connection, so in practice it\n    # will be stored with a single SSLSocket, along with the reference to the\n    # keychain.\n    certificates = []\n    identities = []\n\n    # Filter out bad paths.\n    paths = (path for path in paths if path)\n\n    try:\n        for file_path in paths:\n            new_identities, new_certs = _load_items_from_file(keychain, file_path)\n            identities.extend(new_identities)\n            certificates.extend(new_certs)\n\n        # Ok, we have everything. The question is: do we have an identity? If\n        # not, we want to grab one from the first cert we have.\n        if not identities:\n            new_identity = Security.SecIdentityRef()\n            status = Security.SecIdentityCreateWithCertificate(\n                keychain, certificates[0], ctypes.byref(new_identity)\n            )\n            _assert_no_error(status)\n            identities.append(new_identity)\n\n            # We now want to release the original certificate, as we no longer\n            # need it.\n            CoreFoundation.CFRelease(certificates.pop(0))\n\n        # We now need to build a new CFArray that holds the trust chain.\n        trust_chain = CoreFoundation.CFArrayCreateMutable(\n            CoreFoundation.kCFAllocatorDefault,\n            0,\n            ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks),\n        )\n        for item in itertools.chain(identities, certificates):\n            # ArrayAppendValue does a CFRetain on the item. That's fine,\n            # because the finally block will release our other refs to them.\n            CoreFoundation.CFArrayAppendValue(trust_chain, item)\n\n        return trust_chain\n    finally:\n        for obj in itertools.chain(identities, certificates):\n            CoreFoundation.CFRelease(obj)\n\n\nTLS_PROTOCOL_VERSIONS = {\n    \"SSLv2\": (0, 2),\n    \"SSLv3\": (3, 0),\n    \"TLSv1\": (3, 1),\n    \"TLSv1.1\": (3, 2),\n    \"TLSv1.2\": (3, 3),\n}\n\n\ndef _build_tls_unknown_ca_alert(version):\n    \"\"\"\n    Builds a TLS alert record for an unknown CA.\n    \"\"\"\n    ver_maj, ver_min = TLS_PROTOCOL_VERSIONS[version]\n    severity_fatal = 0x02\n    description_unknown_ca = 0x30\n    msg = struct.pack(\">BB\", severity_fatal, description_unknown_ca)\n    msg_len = len(msg)\n    record_type_alert = 0x15\n    record = struct.pack(\">BBBH\", record_type_alert, ver_maj, ver_min, msg_len) + msg\n    return record\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/contrib/appengine.py",
    "content": "\"\"\"\nThis module provides a pool manager that uses Google App Engine's\n`URLFetch Service <https://cloud.google.com/appengine/docs/python/urlfetch>`_.\n\nExample usage::\n\n    from urllib3 import PoolManager\n    from urllib3.contrib.appengine import AppEngineManager, is_appengine_sandbox\n\n    if is_appengine_sandbox():\n        # AppEngineManager uses AppEngine's URLFetch API behind the scenes\n        http = AppEngineManager()\n    else:\n        # PoolManager uses a socket-level API behind the scenes\n        http = PoolManager()\n\n    r = http.request('GET', 'https://google.com/')\n\nThere are `limitations <https://cloud.google.com/appengine/docs/python/\\\nurlfetch/#Python_Quotas_and_limits>`_ to the URLFetch service and it may not be\nthe best choice for your application. There are three options for using\nurllib3 on Google App Engine:\n\n1. You can use :class:`AppEngineManager` with URLFetch. URLFetch is\n   cost-effective in many circumstances as long as your usage is within the\n   limitations.\n2. You can use a normal :class:`~urllib3.PoolManager` by enabling sockets.\n   Sockets also have `limitations and restrictions\n   <https://cloud.google.com/appengine/docs/python/sockets/\\\n   #limitations-and-restrictions>`_ and have a lower free quota than URLFetch.\n   To use sockets, be sure to specify the following in your ``app.yaml``::\n\n        env_variables:\n            GAE_USE_SOCKETS_HTTPLIB : 'true'\n\n3. If you are using `App Engine Flexible\n<https://cloud.google.com/appengine/docs/flexible/>`_, you can use the standard\n:class:`PoolManager` without any configuration or special environment variables.\n\"\"\"\n\nfrom __future__ import absolute_import\n\nimport io\nimport logging\nimport warnings\n\nfrom ..exceptions import (\n    HTTPError,\n    HTTPWarning,\n    MaxRetryError,\n    ProtocolError,\n    SSLError,\n    TimeoutError,\n)\nfrom ..packages.six.moves.urllib.parse import urljoin\nfrom ..request import RequestMethods\nfrom ..response import HTTPResponse\nfrom ..util.retry import Retry\nfrom ..util.timeout import Timeout\nfrom . import _appengine_environ\n\ntry:\n    from google.appengine.api import urlfetch\nexcept ImportError:\n    urlfetch = None\n\n\nlog = logging.getLogger(__name__)\n\n\nclass AppEnginePlatformWarning(HTTPWarning):\n    pass\n\n\nclass AppEnginePlatformError(HTTPError):\n    pass\n\n\nclass AppEngineManager(RequestMethods):\n    \"\"\"\n    Connection manager for Google App Engine sandbox applications.\n\n    This manager uses the URLFetch service directly instead of using the\n    emulated httplib, and is subject to URLFetch limitations as described in\n    the App Engine documentation `here\n    <https://cloud.google.com/appengine/docs/python/urlfetch>`_.\n\n    Notably it will raise an :class:`AppEnginePlatformError` if:\n        * URLFetch is not available.\n        * If you attempt to use this on App Engine Flexible, as full socket\n          support is available.\n        * If a request size is more than 10 megabytes.\n        * If a response size is more than 32 megabytes.\n        * If you use an unsupported request method such as OPTIONS.\n\n    Beyond those cases, it will raise normal urllib3 errors.\n    \"\"\"\n\n    def __init__(\n        self,\n        headers=None,\n        retries=None,\n        validate_certificate=True,\n        urlfetch_retries=True,\n    ):\n        if not urlfetch:\n            raise AppEnginePlatformError(\n                \"URLFetch is not available in this environment.\"\n            )\n\n        warnings.warn(\n            \"urllib3 is using URLFetch on Google App Engine sandbox instead \"\n            \"of sockets. To use sockets directly instead of URLFetch see \"\n            \"https://urllib3.readthedocs.io/en/1.26.x/reference/urllib3.contrib.html.\",\n            AppEnginePlatformWarning,\n        )\n\n        RequestMethods.__init__(self, headers)\n        self.validate_certificate = validate_certificate\n        self.urlfetch_retries = urlfetch_retries\n\n        self.retries = retries or Retry.DEFAULT\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        # Return False to re-raise any potential exceptions\n        return False\n\n    def urlopen(\n        self,\n        method,\n        url,\n        body=None,\n        headers=None,\n        retries=None,\n        redirect=True,\n        timeout=Timeout.DEFAULT_TIMEOUT,\n        **response_kw\n    ):\n\n        retries = self._get_retries(retries, redirect)\n\n        try:\n            follow_redirects = redirect and retries.redirect != 0 and retries.total\n            response = urlfetch.fetch(\n                url,\n                payload=body,\n                method=method,\n                headers=headers or {},\n                allow_truncated=False,\n                follow_redirects=self.urlfetch_retries and follow_redirects,\n                deadline=self._get_absolute_timeout(timeout),\n                validate_certificate=self.validate_certificate,\n            )\n        except urlfetch.DeadlineExceededError as e:\n            raise TimeoutError(self, e)\n\n        except urlfetch.InvalidURLError as e:\n            if \"too large\" in str(e):\n                raise AppEnginePlatformError(\n                    \"URLFetch request too large, URLFetch only \"\n                    \"supports requests up to 10mb in size.\",\n                    e,\n                )\n            raise ProtocolError(e)\n\n        except urlfetch.DownloadError as e:\n            if \"Too many redirects\" in str(e):\n                raise MaxRetryError(self, url, reason=e)\n            raise ProtocolError(e)\n\n        except urlfetch.ResponseTooLargeError as e:\n            raise AppEnginePlatformError(\n                \"URLFetch response too large, URLFetch only supports\"\n                \"responses up to 32mb in size.\",\n                e,\n            )\n\n        except urlfetch.SSLCertificateError as e:\n            raise SSLError(e)\n\n        except urlfetch.InvalidMethodError as e:\n            raise AppEnginePlatformError(\n                \"URLFetch does not support method: %s\" % method, e\n            )\n\n        http_response = self._urlfetch_response_to_http_response(\n            response, retries=retries, **response_kw\n        )\n\n        # Handle redirect?\n        redirect_location = redirect and http_response.get_redirect_location()\n        if redirect_location:\n            # Check for redirect response\n            if self.urlfetch_retries and retries.raise_on_redirect:\n                raise MaxRetryError(self, url, \"too many redirects\")\n            else:\n                if http_response.status == 303:\n                    method = \"GET\"\n\n                try:\n                    retries = retries.increment(\n                        method, url, response=http_response, _pool=self\n                    )\n                except MaxRetryError:\n                    if retries.raise_on_redirect:\n                        raise MaxRetryError(self, url, \"too many redirects\")\n                    return http_response\n\n                retries.sleep_for_retry(http_response)\n                log.debug(\"Redirecting %s -> %s\", url, redirect_location)\n                redirect_url = urljoin(url, redirect_location)\n                return self.urlopen(\n                    method,\n                    redirect_url,\n                    body,\n                    headers,\n                    retries=retries,\n                    redirect=redirect,\n                    timeout=timeout,\n                    **response_kw\n                )\n\n        # Check if we should retry the HTTP response.\n        has_retry_after = bool(http_response.getheader(\"Retry-After\"))\n        if retries.is_retry(method, http_response.status, has_retry_after):\n            retries = retries.increment(method, url, response=http_response, _pool=self)\n            log.debug(\"Retry: %s\", url)\n            retries.sleep(http_response)\n            return self.urlopen(\n                method,\n                url,\n                body=body,\n                headers=headers,\n                retries=retries,\n                redirect=redirect,\n                timeout=timeout,\n                **response_kw\n            )\n\n        return http_response\n\n    def _urlfetch_response_to_http_response(self, urlfetch_resp, **response_kw):\n\n        if is_prod_appengine():\n            # Production GAE handles deflate encoding automatically, but does\n            # not remove the encoding header.\n            content_encoding = urlfetch_resp.headers.get(\"content-encoding\")\n\n            if content_encoding == \"deflate\":\n                del urlfetch_resp.headers[\"content-encoding\"]\n\n        transfer_encoding = urlfetch_resp.headers.get(\"transfer-encoding\")\n        # We have a full response's content,\n        # so let's make sure we don't report ourselves as chunked data.\n        if transfer_encoding == \"chunked\":\n            encodings = transfer_encoding.split(\",\")\n            encodings.remove(\"chunked\")\n            urlfetch_resp.headers[\"transfer-encoding\"] = \",\".join(encodings)\n\n        original_response = HTTPResponse(\n            # In order for decoding to work, we must present the content as\n            # a file-like object.\n            body=io.BytesIO(urlfetch_resp.content),\n            msg=urlfetch_resp.header_msg,\n            headers=urlfetch_resp.headers,\n            status=urlfetch_resp.status_code,\n            **response_kw\n        )\n\n        return HTTPResponse(\n            body=io.BytesIO(urlfetch_resp.content),\n            headers=urlfetch_resp.headers,\n            status=urlfetch_resp.status_code,\n            original_response=original_response,\n            **response_kw\n        )\n\n    def _get_absolute_timeout(self, timeout):\n        if timeout is Timeout.DEFAULT_TIMEOUT:\n            return None  # Defer to URLFetch's default.\n        if isinstance(timeout, Timeout):\n            if timeout._read is not None or timeout._connect is not None:\n                warnings.warn(\n                    \"URLFetch does not support granular timeout settings, \"\n                    \"reverting to total or default URLFetch timeout.\",\n                    AppEnginePlatformWarning,\n                )\n            return timeout.total\n        return timeout\n\n    def _get_retries(self, retries, redirect):\n        if not isinstance(retries, Retry):\n            retries = Retry.from_int(retries, redirect=redirect, default=self.retries)\n\n        if retries.connect or retries.read or retries.redirect:\n            warnings.warn(\n                \"URLFetch only supports total retries and does not \"\n                \"recognize connect, read, or redirect retry parameters.\",\n                AppEnginePlatformWarning,\n            )\n\n        return retries\n\n\n# Alias methods from _appengine_environ to maintain public API interface.\n\nis_appengine = _appengine_environ.is_appengine\nis_appengine_sandbox = _appengine_environ.is_appengine_sandbox\nis_local_appengine = _appengine_environ.is_local_appengine\nis_prod_appengine = _appengine_environ.is_prod_appengine\nis_prod_appengine_mvms = _appengine_environ.is_prod_appengine_mvms\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/contrib/ntlmpool.py",
    "content": "\"\"\"\nNTLM authenticating pool, contributed by erikcederstran\n\nIssue #10, see: http://code.google.com/p/urllib3/issues/detail?id=10\n\"\"\"\nfrom __future__ import absolute_import\n\nimport warnings\nfrom logging import getLogger\n\nfrom ntlm import ntlm\n\nfrom .. import HTTPSConnectionPool\nfrom ..packages.six.moves.http_client import HTTPSConnection\n\nwarnings.warn(\n    \"The 'urllib3.contrib.ntlmpool' module is deprecated and will be removed \"\n    \"in urllib3 v2.0 release, urllib3 is not able to support it properly due \"\n    \"to reasons listed in issue: https://github.com/urllib3/urllib3/issues/2282. \"\n    \"If you are a user of this module please comment in the mentioned issue.\",\n    DeprecationWarning,\n)\n\nlog = getLogger(__name__)\n\n\nclass NTLMConnectionPool(HTTPSConnectionPool):\n    \"\"\"\n    Implements an NTLM authentication version of an urllib3 connection pool\n    \"\"\"\n\n    scheme = \"https\"\n\n    def __init__(self, user, pw, authurl, *args, **kwargs):\n        \"\"\"\n        authurl is a random URL on the server that is protected by NTLM.\n        user is the Windows user, probably in the DOMAIN\\\\username format.\n        pw is the password for the user.\n        \"\"\"\n        super(NTLMConnectionPool, self).__init__(*args, **kwargs)\n        self.authurl = authurl\n        self.rawuser = user\n        user_parts = user.split(\"\\\\\", 1)\n        self.domain = user_parts[0].upper()\n        self.user = user_parts[1]\n        self.pw = pw\n\n    def _new_conn(self):\n        # Performs the NTLM handshake that secures the connection. The socket\n        # must be kept open while requests are performed.\n        self.num_connections += 1\n        log.debug(\n            \"Starting NTLM HTTPS connection no. %d: https://%s%s\",\n            self.num_connections,\n            self.host,\n            self.authurl,\n        )\n\n        headers = {\"Connection\": \"Keep-Alive\"}\n        req_header = \"Authorization\"\n        resp_header = \"www-authenticate\"\n\n        conn = HTTPSConnection(host=self.host, port=self.port)\n\n        # Send negotiation message\n        headers[req_header] = \"NTLM %s\" % ntlm.create_NTLM_NEGOTIATE_MESSAGE(\n            self.rawuser\n        )\n        log.debug(\"Request headers: %s\", headers)\n        conn.request(\"GET\", self.authurl, None, headers)\n        res = conn.getresponse()\n        reshdr = dict(res.getheaders())\n        log.debug(\"Response status: %s %s\", res.status, res.reason)\n        log.debug(\"Response headers: %s\", reshdr)\n        log.debug(\"Response data: %s [...]\", res.read(100))\n\n        # Remove the reference to the socket, so that it can not be closed by\n        # the response object (we want to keep the socket open)\n        res.fp = None\n\n        # Server should respond with a challenge message\n        auth_header_values = reshdr[resp_header].split(\", \")\n        auth_header_value = None\n        for s in auth_header_values:\n            if s[:5] == \"NTLM \":\n                auth_header_value = s[5:]\n        if auth_header_value is None:\n            raise Exception(\n                \"Unexpected %s response header: %s\" % (resp_header, reshdr[resp_header])\n            )\n\n        # Send authentication message\n        ServerChallenge, NegotiateFlags = ntlm.parse_NTLM_CHALLENGE_MESSAGE(\n            auth_header_value\n        )\n        auth_msg = ntlm.create_NTLM_AUTHENTICATE_MESSAGE(\n            ServerChallenge, self.user, self.domain, self.pw, NegotiateFlags\n        )\n        headers[req_header] = \"NTLM %s\" % auth_msg\n        log.debug(\"Request headers: %s\", headers)\n        conn.request(\"GET\", self.authurl, None, headers)\n        res = conn.getresponse()\n        log.debug(\"Response status: %s %s\", res.status, res.reason)\n        log.debug(\"Response headers: %s\", dict(res.getheaders()))\n        log.debug(\"Response data: %s [...]\", res.read()[:100])\n        if res.status != 200:\n            if res.status == 401:\n                raise Exception(\"Server rejected request: wrong username or password\")\n            raise Exception(\"Wrong server response: %s %s\" % (res.status, res.reason))\n\n        res.fp = None\n        log.debug(\"Connection established\")\n        return conn\n\n    def urlopen(\n        self,\n        method,\n        url,\n        body=None,\n        headers=None,\n        retries=3,\n        redirect=True,\n        assert_same_host=True,\n    ):\n        if headers is None:\n            headers = {}\n        headers[\"Connection\"] = \"Keep-Alive\"\n        return super(NTLMConnectionPool, self).urlopen(\n            method, url, body, headers, retries, redirect, assert_same_host\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/contrib/pyopenssl.py",
    "content": "\"\"\"\nTLS with SNI_-support for Python 2. Follow these instructions if you would\nlike to verify TLS certificates in Python 2. Note, the default libraries do\n*not* do certificate checking; you need to do additional work to validate\ncertificates yourself.\n\nThis needs the following packages installed:\n\n* `pyOpenSSL`_ (tested with 16.0.0)\n* `cryptography`_ (minimum 1.3.4, from pyopenssl)\n* `idna`_ (minimum 2.0, from cryptography)\n\nHowever, pyopenssl depends on cryptography, which depends on idna, so while we\nuse all three directly here we end up having relatively few packages required.\n\nYou can install them with the following command:\n\n.. code-block:: bash\n\n    $ python -m pip install pyopenssl cryptography idna\n\nTo activate certificate checking, call\n:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code\nbefore you begin making HTTP requests. This can be done in a ``sitecustomize``\nmodule, or at any other time before your application begins using ``urllib3``,\nlike this:\n\n.. code-block:: python\n\n    try:\n        import urllib3.contrib.pyopenssl\n        urllib3.contrib.pyopenssl.inject_into_urllib3()\n    except ImportError:\n        pass\n\nNow you can use :mod:`urllib3` as you normally would, and it will support SNI\nwhen the required modules are installed.\n\nActivating this module also has the positive side effect of disabling SSL/TLS\ncompression in Python 2 (see `CRIME attack`_).\n\n.. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication\n.. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit)\n.. _pyopenssl: https://www.pyopenssl.org\n.. _cryptography: https://cryptography.io\n.. _idna: https://github.com/kjd/idna\n\"\"\"\nfrom __future__ import absolute_import\n\nimport OpenSSL.SSL\nfrom cryptography import x509\nfrom cryptography.hazmat.backends.openssl import backend as openssl_backend\nfrom cryptography.hazmat.backends.openssl.x509 import _Certificate\n\ntry:\n    from cryptography.x509 import UnsupportedExtension\nexcept ImportError:\n    # UnsupportedExtension is gone in cryptography >= 2.1.0\n    class UnsupportedExtension(Exception):\n        pass\n\n\nfrom io import BytesIO\nfrom socket import error as SocketError\nfrom socket import timeout\n\ntry:  # Platform-specific: Python 2\n    from socket import _fileobject\nexcept ImportError:  # Platform-specific: Python 3\n    _fileobject = None\n    from ..packages.backports.makefile import backport_makefile\n\nimport logging\nimport ssl\nimport sys\n\nfrom .. import util\nfrom ..packages import six\nfrom ..util.ssl_ import PROTOCOL_TLS_CLIENT\n\n__all__ = [\"inject_into_urllib3\", \"extract_from_urllib3\"]\n\n# SNI always works.\nHAS_SNI = True\n\n# Map from urllib3 to PyOpenSSL compatible parameter-values.\n_openssl_versions = {\n    util.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD,\n    PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD,\n    ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,\n}\n\nif hasattr(ssl, \"PROTOCOL_SSLv3\") and hasattr(OpenSSL.SSL, \"SSLv3_METHOD\"):\n    _openssl_versions[ssl.PROTOCOL_SSLv3] = OpenSSL.SSL.SSLv3_METHOD\n\nif hasattr(ssl, \"PROTOCOL_TLSv1_1\") and hasattr(OpenSSL.SSL, \"TLSv1_1_METHOD\"):\n    _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD\n\nif hasattr(ssl, \"PROTOCOL_TLSv1_2\") and hasattr(OpenSSL.SSL, \"TLSv1_2_METHOD\"):\n    _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD\n\n\n_stdlib_to_openssl_verify = {\n    ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE,\n    ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER,\n    ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER\n    + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT,\n}\n_openssl_to_stdlib_verify = dict((v, k) for k, v in _stdlib_to_openssl_verify.items())\n\n# OpenSSL will only write 16K at a time\nSSL_WRITE_BLOCKSIZE = 16384\n\norig_util_HAS_SNI = util.HAS_SNI\norig_util_SSLContext = util.ssl_.SSLContext\n\n\nlog = logging.getLogger(__name__)\n\n\ndef inject_into_urllib3():\n    \"Monkey-patch urllib3 with PyOpenSSL-backed SSL-support.\"\n\n    _validate_dependencies_met()\n\n    util.SSLContext = PyOpenSSLContext\n    util.ssl_.SSLContext = PyOpenSSLContext\n    util.HAS_SNI = HAS_SNI\n    util.ssl_.HAS_SNI = HAS_SNI\n    util.IS_PYOPENSSL = True\n    util.ssl_.IS_PYOPENSSL = True\n\n\ndef extract_from_urllib3():\n    \"Undo monkey-patching by :func:`inject_into_urllib3`.\"\n\n    util.SSLContext = orig_util_SSLContext\n    util.ssl_.SSLContext = orig_util_SSLContext\n    util.HAS_SNI = orig_util_HAS_SNI\n    util.ssl_.HAS_SNI = orig_util_HAS_SNI\n    util.IS_PYOPENSSL = False\n    util.ssl_.IS_PYOPENSSL = False\n\n\ndef _validate_dependencies_met():\n    \"\"\"\n    Verifies that PyOpenSSL's package-level dependencies have been met.\n    Throws `ImportError` if they are not met.\n    \"\"\"\n    # Method added in `cryptography==1.1`; not available in older versions\n    from cryptography.x509.extensions import Extensions\n\n    if getattr(Extensions, \"get_extension_for_class\", None) is None:\n        raise ImportError(\n            \"'cryptography' module missing required functionality.  \"\n            \"Try upgrading to v1.3.4 or newer.\"\n        )\n\n    # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509\n    # attribute is only present on those versions.\n    from OpenSSL.crypto import X509\n\n    x509 = X509()\n    if getattr(x509, \"_x509\", None) is None:\n        raise ImportError(\n            \"'pyOpenSSL' module missing required functionality. \"\n            \"Try upgrading to v0.14 or newer.\"\n        )\n\n\ndef _dnsname_to_stdlib(name):\n    \"\"\"\n    Converts a dNSName SubjectAlternativeName field to the form used by the\n    standard library on the given Python version.\n\n    Cryptography produces a dNSName as a unicode string that was idna-decoded\n    from ASCII bytes. We need to idna-encode that string to get it back, and\n    then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib\n    uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8).\n\n    If the name cannot be idna-encoded then we return None signalling that\n    the name given should be skipped.\n    \"\"\"\n\n    def idna_encode(name):\n        \"\"\"\n        Borrowed wholesale from the Python Cryptography Project. It turns out\n        that we can't just safely call `idna.encode`: it can explode for\n        wildcard names. This avoids that problem.\n        \"\"\"\n        import idna\n\n        try:\n            for prefix in [u\"*.\", u\".\"]:\n                if name.startswith(prefix):\n                    name = name[len(prefix) :]\n                    return prefix.encode(\"ascii\") + idna.encode(name)\n            return idna.encode(name)\n        except idna.core.IDNAError:\n            return None\n\n    # Don't send IPv6 addresses through the IDNA encoder.\n    if \":\" in name:\n        return name\n\n    name = idna_encode(name)\n    if name is None:\n        return None\n    elif sys.version_info >= (3, 0):\n        name = name.decode(\"utf-8\")\n    return name\n\n\ndef get_subj_alt_name(peer_cert):\n    \"\"\"\n    Given an PyOpenSSL certificate, provides all the subject alternative names.\n    \"\"\"\n    # Pass the cert to cryptography, which has much better APIs for this.\n    if hasattr(peer_cert, \"to_cryptography\"):\n        cert = peer_cert.to_cryptography()\n    else:\n        # This is technically using private APIs, but should work across all\n        # relevant versions before PyOpenSSL got a proper API for this.\n        cert = _Certificate(openssl_backend, peer_cert._x509)\n\n    # We want to find the SAN extension. Ask Cryptography to locate it (it's\n    # faster than looping in Python)\n    try:\n        ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value\n    except x509.ExtensionNotFound:\n        # No such extension, return the empty list.\n        return []\n    except (\n        x509.DuplicateExtension,\n        UnsupportedExtension,\n        x509.UnsupportedGeneralNameType,\n        UnicodeError,\n    ) as e:\n        # A problem has been found with the quality of the certificate. Assume\n        # no SAN field is present.\n        log.warning(\n            \"A problem was encountered with the certificate that prevented \"\n            \"urllib3 from finding the SubjectAlternativeName field. This can \"\n            \"affect certificate validation. The error was %s\",\n            e,\n        )\n        return []\n\n    # We want to return dNSName and iPAddress fields. We need to cast the IPs\n    # back to strings because the match_hostname function wants them as\n    # strings.\n    # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8\n    # decoded. This is pretty frustrating, but that's what the standard library\n    # does with certificates, and so we need to attempt to do the same.\n    # We also want to skip over names which cannot be idna encoded.\n    names = [\n        (\"DNS\", name)\n        for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName))\n        if name is not None\n    ]\n    names.extend(\n        (\"IP Address\", str(name)) for name in ext.get_values_for_type(x509.IPAddress)\n    )\n\n    return names\n\n\nclass WrappedSocket(object):\n    \"\"\"API-compatibility wrapper for Python OpenSSL's Connection-class.\n\n    Note: _makefile_refs, _drop() and _reuse() are needed for the garbage\n    collector of pypy.\n    \"\"\"\n\n    def __init__(self, connection, socket, suppress_ragged_eofs=True):\n        self.connection = connection\n        self.socket = socket\n        self.suppress_ragged_eofs = suppress_ragged_eofs\n        self._makefile_refs = 0\n        self._closed = False\n\n    def fileno(self):\n        return self.socket.fileno()\n\n    # Copy-pasted from Python 3.5 source code\n    def _decref_socketios(self):\n        if self._makefile_refs > 0:\n            self._makefile_refs -= 1\n        if self._closed:\n            self.close()\n\n    def recv(self, *args, **kwargs):\n        try:\n            data = self.connection.recv(*args, **kwargs)\n        except OpenSSL.SSL.SysCallError as e:\n            if self.suppress_ragged_eofs and e.args == (-1, \"Unexpected EOF\"):\n                return b\"\"\n            else:\n                raise SocketError(str(e))\n        except OpenSSL.SSL.ZeroReturnError:\n            if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:\n                return b\"\"\n            else:\n                raise\n        except OpenSSL.SSL.WantReadError:\n            if not util.wait_for_read(self.socket, self.socket.gettimeout()):\n                raise timeout(\"The read operation timed out\")\n            else:\n                return self.recv(*args, **kwargs)\n\n        # TLS 1.3 post-handshake authentication\n        except OpenSSL.SSL.Error as e:\n            raise ssl.SSLError(\"read error: %r\" % e)\n        else:\n            return data\n\n    def recv_into(self, *args, **kwargs):\n        try:\n            return self.connection.recv_into(*args, **kwargs)\n        except OpenSSL.SSL.SysCallError as e:\n            if self.suppress_ragged_eofs and e.args == (-1, \"Unexpected EOF\"):\n                return 0\n            else:\n                raise SocketError(str(e))\n        except OpenSSL.SSL.ZeroReturnError:\n            if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:\n                return 0\n            else:\n                raise\n        except OpenSSL.SSL.WantReadError:\n            if not util.wait_for_read(self.socket, self.socket.gettimeout()):\n                raise timeout(\"The read operation timed out\")\n            else:\n                return self.recv_into(*args, **kwargs)\n\n        # TLS 1.3 post-handshake authentication\n        except OpenSSL.SSL.Error as e:\n            raise ssl.SSLError(\"read error: %r\" % e)\n\n    def settimeout(self, timeout):\n        return self.socket.settimeout(timeout)\n\n    def _send_until_done(self, data):\n        while True:\n            try:\n                return self.connection.send(data)\n            except OpenSSL.SSL.WantWriteError:\n                if not util.wait_for_write(self.socket, self.socket.gettimeout()):\n                    raise timeout()\n                continue\n            except OpenSSL.SSL.SysCallError as e:\n                raise SocketError(str(e))\n\n    def sendall(self, data):\n        total_sent = 0\n        while total_sent < len(data):\n            sent = self._send_until_done(\n                data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE]\n            )\n            total_sent += sent\n\n    def shutdown(self):\n        # FIXME rethrow compatible exceptions should we ever use this\n        self.connection.shutdown()\n\n    def close(self):\n        if self._makefile_refs < 1:\n            try:\n                self._closed = True\n                return self.connection.close()\n            except OpenSSL.SSL.Error:\n                return\n        else:\n            self._makefile_refs -= 1\n\n    def getpeercert(self, binary_form=False):\n        x509 = self.connection.get_peer_certificate()\n\n        if not x509:\n            return x509\n\n        if binary_form:\n            return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509)\n\n        return {\n            \"subject\": (((\"commonName\", x509.get_subject().CN),),),\n            \"subjectAltName\": get_subj_alt_name(x509),\n        }\n\n    def version(self):\n        return self.connection.get_protocol_version_name()\n\n    def _reuse(self):\n        self._makefile_refs += 1\n\n    def _drop(self):\n        if self._makefile_refs < 1:\n            self.close()\n        else:\n            self._makefile_refs -= 1\n\n\nif _fileobject:  # Platform-specific: Python 2\n\n    def makefile(self, mode, bufsize=-1):\n        self._makefile_refs += 1\n        return _fileobject(self, mode, bufsize, close=True)\n\n\nelse:  # Platform-specific: Python 3\n    makefile = backport_makefile\n\nWrappedSocket.makefile = makefile\n\n\nclass PyOpenSSLContext(object):\n    \"\"\"\n    I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible\n    for translating the interface of the standard library ``SSLContext`` object\n    to calls into PyOpenSSL.\n    \"\"\"\n\n    def __init__(self, protocol):\n        self.protocol = _openssl_versions[protocol]\n        self._ctx = OpenSSL.SSL.Context(self.protocol)\n        self._options = 0\n        self.check_hostname = False\n\n    @property\n    def options(self):\n        return self._options\n\n    @options.setter\n    def options(self, value):\n        self._options = value\n        self._ctx.set_options(value)\n\n    @property\n    def verify_mode(self):\n        return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()]\n\n    @verify_mode.setter\n    def verify_mode(self, value):\n        self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback)\n\n    def set_default_verify_paths(self):\n        self._ctx.set_default_verify_paths()\n\n    def set_ciphers(self, ciphers):\n        if isinstance(ciphers, six.text_type):\n            ciphers = ciphers.encode(\"utf-8\")\n        self._ctx.set_cipher_list(ciphers)\n\n    def load_verify_locations(self, cafile=None, capath=None, cadata=None):\n        if cafile is not None:\n            cafile = cafile.encode(\"utf-8\")\n        if capath is not None:\n            capath = capath.encode(\"utf-8\")\n        try:\n            self._ctx.load_verify_locations(cafile, capath)\n            if cadata is not None:\n                self._ctx.load_verify_locations(BytesIO(cadata))\n        except OpenSSL.SSL.Error as e:\n            raise ssl.SSLError(\"unable to load trusted certificates: %r\" % e)\n\n    def load_cert_chain(self, certfile, keyfile=None, password=None):\n        self._ctx.use_certificate_chain_file(certfile)\n        if password is not None:\n            if not isinstance(password, six.binary_type):\n                password = password.encode(\"utf-8\")\n            self._ctx.set_passwd_cb(lambda *_: password)\n        self._ctx.use_privatekey_file(keyfile or certfile)\n\n    def set_alpn_protocols(self, protocols):\n        protocols = [six.ensure_binary(p) for p in protocols]\n        return self._ctx.set_alpn_protos(protocols)\n\n    def wrap_socket(\n        self,\n        sock,\n        server_side=False,\n        do_handshake_on_connect=True,\n        suppress_ragged_eofs=True,\n        server_hostname=None,\n    ):\n        cnx = OpenSSL.SSL.Connection(self._ctx, sock)\n\n        if isinstance(server_hostname, six.text_type):  # Platform-specific: Python 3\n            server_hostname = server_hostname.encode(\"utf-8\")\n\n        if server_hostname is not None:\n            cnx.set_tlsext_host_name(server_hostname)\n\n        cnx.set_connect_state()\n\n        while True:\n            try:\n                cnx.do_handshake()\n            except OpenSSL.SSL.WantReadError:\n                if not util.wait_for_read(sock, sock.gettimeout()):\n                    raise timeout(\"select timed out\")\n                continue\n            except OpenSSL.SSL.Error as e:\n                raise ssl.SSLError(\"bad handshake: %r\" % e)\n            break\n\n        return WrappedSocket(cnx, sock)\n\n\ndef _verify_callback(cnx, x509, err_no, err_depth, return_code):\n    return err_no == 0\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/contrib/securetransport.py",
    "content": "\"\"\"\nSecureTranport support for urllib3 via ctypes.\n\nThis makes platform-native TLS available to urllib3 users on macOS without the\nuse of a compiler. This is an important feature because the Python Package\nIndex is moving to become a TLSv1.2-or-higher server, and the default OpenSSL\nthat ships with macOS is not capable of doing TLSv1.2. The only way to resolve\nthis is to give macOS users an alternative solution to the problem, and that\nsolution is to use SecureTransport.\n\nWe use ctypes here because this solution must not require a compiler. That's\nbecause pip is not allowed to require a compiler either.\n\nThis is not intended to be a seriously long-term solution to this problem.\nThe hope is that PEP 543 will eventually solve this issue for us, at which\npoint we can retire this contrib module. But in the short term, we need to\nsolve the impending tire fire that is Python on Mac without this kind of\ncontrib module. So...here we are.\n\nTo use this module, simply import and inject it::\n\n    import urllib3.contrib.securetransport\n    urllib3.contrib.securetransport.inject_into_urllib3()\n\nHappy TLSing!\n\nThis code is a bastardised version of the code found in Will Bond's oscrypto\nlibrary. An enormous debt is owed to him for blazing this trail for us. For\nthat reason, this code should be considered to be covered both by urllib3's\nlicense and by oscrypto's:\n\n.. code-block::\n\n    Copyright (c) 2015-2016 Will Bond <will@wbond.net>\n\n    Permission is hereby granted, free of charge, to any person obtaining a\n    copy of this software and associated documentation files (the \"Software\"),\n    to deal in the Software without restriction, including without limitation\n    the rights to use, copy, modify, merge, publish, distribute, sublicense,\n    and/or sell copies of the Software, and to permit persons to whom the\n    Software is furnished to do so, subject to the following conditions:\n\n    The above copyright notice and this permission notice shall be included in\n    all copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n    DEALINGS IN THE SOFTWARE.\n\"\"\"\nfrom __future__ import absolute_import\n\nimport contextlib\nimport ctypes\nimport errno\nimport os.path\nimport shutil\nimport socket\nimport ssl\nimport struct\nimport threading\nimport weakref\n\nimport six\n\nfrom .. import util\nfrom ..util.ssl_ import PROTOCOL_TLS_CLIENT\nfrom ._securetransport.bindings import CoreFoundation, Security, SecurityConst\nfrom ._securetransport.low_level import (\n    _assert_no_error,\n    _build_tls_unknown_ca_alert,\n    _cert_array_from_pem,\n    _create_cfstring_array,\n    _load_client_cert_chain,\n    _temporary_keychain,\n)\n\ntry:  # Platform-specific: Python 2\n    from socket import _fileobject\nexcept ImportError:  # Platform-specific: Python 3\n    _fileobject = None\n    from ..packages.backports.makefile import backport_makefile\n\n__all__ = [\"inject_into_urllib3\", \"extract_from_urllib3\"]\n\n# SNI always works\nHAS_SNI = True\n\norig_util_HAS_SNI = util.HAS_SNI\norig_util_SSLContext = util.ssl_.SSLContext\n\n# This dictionary is used by the read callback to obtain a handle to the\n# calling wrapped socket. This is a pretty silly approach, but for now it'll\n# do. I feel like I should be able to smuggle a handle to the wrapped socket\n# directly in the SSLConnectionRef, but for now this approach will work I\n# guess.\n#\n# We need to lock around this structure for inserts, but we don't do it for\n# reads/writes in the callbacks. The reasoning here goes as follows:\n#\n#    1. It is not possible to call into the callbacks before the dictionary is\n#       populated, so once in the callback the id must be in the dictionary.\n#    2. The callbacks don't mutate the dictionary, they only read from it, and\n#       so cannot conflict with any of the insertions.\n#\n# This is good: if we had to lock in the callbacks we'd drastically slow down\n# the performance of this code.\n_connection_refs = weakref.WeakValueDictionary()\n_connection_ref_lock = threading.Lock()\n\n# Limit writes to 16kB. This is OpenSSL's limit, but we'll cargo-cult it over\n# for no better reason than we need *a* limit, and this one is right there.\nSSL_WRITE_BLOCKSIZE = 16384\n\n# This is our equivalent of util.ssl_.DEFAULT_CIPHERS, but expanded out to\n# individual cipher suites. We need to do this because this is how\n# SecureTransport wants them.\nCIPHER_SUITES = [\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,\n    SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n    SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,\n    SecurityConst.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,\n    SecurityConst.TLS_DHE_RSA_WITH_AES_256_GCM_SHA384,\n    SecurityConst.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,\n    SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,\n    SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,\n    SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,\n    SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,\n    SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,\n    SecurityConst.TLS_DHE_RSA_WITH_AES_256_CBC_SHA256,\n    SecurityConst.TLS_DHE_RSA_WITH_AES_256_CBC_SHA,\n    SecurityConst.TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,\n    SecurityConst.TLS_DHE_RSA_WITH_AES_128_CBC_SHA,\n    SecurityConst.TLS_AES_256_GCM_SHA384,\n    SecurityConst.TLS_AES_128_GCM_SHA256,\n    SecurityConst.TLS_RSA_WITH_AES_256_GCM_SHA384,\n    SecurityConst.TLS_RSA_WITH_AES_128_GCM_SHA256,\n    SecurityConst.TLS_AES_128_CCM_8_SHA256,\n    SecurityConst.TLS_AES_128_CCM_SHA256,\n    SecurityConst.TLS_RSA_WITH_AES_256_CBC_SHA256,\n    SecurityConst.TLS_RSA_WITH_AES_128_CBC_SHA256,\n    SecurityConst.TLS_RSA_WITH_AES_256_CBC_SHA,\n    SecurityConst.TLS_RSA_WITH_AES_128_CBC_SHA,\n]\n\n# Basically this is simple: for PROTOCOL_SSLv23 we turn it into a low of\n# TLSv1 and a high of TLSv1.2. For everything else, we pin to that version.\n# TLSv1 to 1.2 are supported on macOS 10.8+\n_protocol_to_min_max = {\n    util.PROTOCOL_TLS: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocol12),\n    PROTOCOL_TLS_CLIENT: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocol12),\n}\n\nif hasattr(ssl, \"PROTOCOL_SSLv2\"):\n    _protocol_to_min_max[ssl.PROTOCOL_SSLv2] = (\n        SecurityConst.kSSLProtocol2,\n        SecurityConst.kSSLProtocol2,\n    )\nif hasattr(ssl, \"PROTOCOL_SSLv3\"):\n    _protocol_to_min_max[ssl.PROTOCOL_SSLv3] = (\n        SecurityConst.kSSLProtocol3,\n        SecurityConst.kSSLProtocol3,\n    )\nif hasattr(ssl, \"PROTOCOL_TLSv1\"):\n    _protocol_to_min_max[ssl.PROTOCOL_TLSv1] = (\n        SecurityConst.kTLSProtocol1,\n        SecurityConst.kTLSProtocol1,\n    )\nif hasattr(ssl, \"PROTOCOL_TLSv1_1\"):\n    _protocol_to_min_max[ssl.PROTOCOL_TLSv1_1] = (\n        SecurityConst.kTLSProtocol11,\n        SecurityConst.kTLSProtocol11,\n    )\nif hasattr(ssl, \"PROTOCOL_TLSv1_2\"):\n    _protocol_to_min_max[ssl.PROTOCOL_TLSv1_2] = (\n        SecurityConst.kTLSProtocol12,\n        SecurityConst.kTLSProtocol12,\n    )\n\n\ndef inject_into_urllib3():\n    \"\"\"\n    Monkey-patch urllib3 with SecureTransport-backed SSL-support.\n    \"\"\"\n    util.SSLContext = SecureTransportContext\n    util.ssl_.SSLContext = SecureTransportContext\n    util.HAS_SNI = HAS_SNI\n    util.ssl_.HAS_SNI = HAS_SNI\n    util.IS_SECURETRANSPORT = True\n    util.ssl_.IS_SECURETRANSPORT = True\n\n\ndef extract_from_urllib3():\n    \"\"\"\n    Undo monkey-patching by :func:`inject_into_urllib3`.\n    \"\"\"\n    util.SSLContext = orig_util_SSLContext\n    util.ssl_.SSLContext = orig_util_SSLContext\n    util.HAS_SNI = orig_util_HAS_SNI\n    util.ssl_.HAS_SNI = orig_util_HAS_SNI\n    util.IS_SECURETRANSPORT = False\n    util.ssl_.IS_SECURETRANSPORT = False\n\n\ndef _read_callback(connection_id, data_buffer, data_length_pointer):\n    \"\"\"\n    SecureTransport read callback. This is called by ST to request that data\n    be returned from the socket.\n    \"\"\"\n    wrapped_socket = None\n    try:\n        wrapped_socket = _connection_refs.get(connection_id)\n        if wrapped_socket is None:\n            return SecurityConst.errSSLInternal\n        base_socket = wrapped_socket.socket\n\n        requested_length = data_length_pointer[0]\n\n        timeout = wrapped_socket.gettimeout()\n        error = None\n        read_count = 0\n\n        try:\n            while read_count < requested_length:\n                if timeout is None or timeout >= 0:\n                    if not util.wait_for_read(base_socket, timeout):\n                        raise socket.error(errno.EAGAIN, \"timed out\")\n\n                remaining = requested_length - read_count\n                buffer = (ctypes.c_char * remaining).from_address(\n                    data_buffer + read_count\n                )\n                chunk_size = base_socket.recv_into(buffer, remaining)\n                read_count += chunk_size\n                if not chunk_size:\n                    if not read_count:\n                        return SecurityConst.errSSLClosedGraceful\n                    break\n        except (socket.error) as e:\n            error = e.errno\n\n            if error is not None and error != errno.EAGAIN:\n                data_length_pointer[0] = read_count\n                if error == errno.ECONNRESET or error == errno.EPIPE:\n                    return SecurityConst.errSSLClosedAbort\n                raise\n\n        data_length_pointer[0] = read_count\n\n        if read_count != requested_length:\n            return SecurityConst.errSSLWouldBlock\n\n        return 0\n    except Exception as e:\n        if wrapped_socket is not None:\n            wrapped_socket._exception = e\n        return SecurityConst.errSSLInternal\n\n\ndef _write_callback(connection_id, data_buffer, data_length_pointer):\n    \"\"\"\n    SecureTransport write callback. This is called by ST to request that data\n    actually be sent on the network.\n    \"\"\"\n    wrapped_socket = None\n    try:\n        wrapped_socket = _connection_refs.get(connection_id)\n        if wrapped_socket is None:\n            return SecurityConst.errSSLInternal\n        base_socket = wrapped_socket.socket\n\n        bytes_to_write = data_length_pointer[0]\n        data = ctypes.string_at(data_buffer, bytes_to_write)\n\n        timeout = wrapped_socket.gettimeout()\n        error = None\n        sent = 0\n\n        try:\n            while sent < bytes_to_write:\n                if timeout is None or timeout >= 0:\n                    if not util.wait_for_write(base_socket, timeout):\n                        raise socket.error(errno.EAGAIN, \"timed out\")\n                chunk_sent = base_socket.send(data)\n                sent += chunk_sent\n\n                # This has some needless copying here, but I'm not sure there's\n                # much value in optimising this data path.\n                data = data[chunk_sent:]\n        except (socket.error) as e:\n            error = e.errno\n\n            if error is not None and error != errno.EAGAIN:\n                data_length_pointer[0] = sent\n                if error == errno.ECONNRESET or error == errno.EPIPE:\n                    return SecurityConst.errSSLClosedAbort\n                raise\n\n        data_length_pointer[0] = sent\n\n        if sent != bytes_to_write:\n            return SecurityConst.errSSLWouldBlock\n\n        return 0\n    except Exception as e:\n        if wrapped_socket is not None:\n            wrapped_socket._exception = e\n        return SecurityConst.errSSLInternal\n\n\n# We need to keep these two objects references alive: if they get GC'd while\n# in use then SecureTransport could attempt to call a function that is in freed\n# memory. That would be...uh...bad. Yeah, that's the word. Bad.\n_read_callback_pointer = Security.SSLReadFunc(_read_callback)\n_write_callback_pointer = Security.SSLWriteFunc(_write_callback)\n\n\nclass WrappedSocket(object):\n    \"\"\"\n    API-compatibility wrapper for Python's OpenSSL wrapped socket object.\n\n    Note: _makefile_refs, _drop(), and _reuse() are needed for the garbage\n    collector of PyPy.\n    \"\"\"\n\n    def __init__(self, socket):\n        self.socket = socket\n        self.context = None\n        self._makefile_refs = 0\n        self._closed = False\n        self._exception = None\n        self._keychain = None\n        self._keychain_dir = None\n        self._client_cert_chain = None\n\n        # We save off the previously-configured timeout and then set it to\n        # zero. This is done because we use select and friends to handle the\n        # timeouts, but if we leave the timeout set on the lower socket then\n        # Python will \"kindly\" call select on that socket again for us. Avoid\n        # that by forcing the timeout to zero.\n        self._timeout = self.socket.gettimeout()\n        self.socket.settimeout(0)\n\n    @contextlib.contextmanager\n    def _raise_on_error(self):\n        \"\"\"\n        A context manager that can be used to wrap calls that do I/O from\n        SecureTransport. If any of the I/O callbacks hit an exception, this\n        context manager will correctly propagate the exception after the fact.\n        This avoids silently swallowing those exceptions.\n\n        It also correctly forces the socket closed.\n        \"\"\"\n        self._exception = None\n\n        # We explicitly don't catch around this yield because in the unlikely\n        # event that an exception was hit in the block we don't want to swallow\n        # it.\n        yield\n        if self._exception is not None:\n            exception, self._exception = self._exception, None\n            self.close()\n            raise exception\n\n    def _set_ciphers(self):\n        \"\"\"\n        Sets up the allowed ciphers. By default this matches the set in\n        util.ssl_.DEFAULT_CIPHERS, at least as supported by macOS. This is done\n        custom and doesn't allow changing at this time, mostly because parsing\n        OpenSSL cipher strings is going to be a freaking nightmare.\n        \"\"\"\n        ciphers = (Security.SSLCipherSuite * len(CIPHER_SUITES))(*CIPHER_SUITES)\n        result = Security.SSLSetEnabledCiphers(\n            self.context, ciphers, len(CIPHER_SUITES)\n        )\n        _assert_no_error(result)\n\n    def _set_alpn_protocols(self, protocols):\n        \"\"\"\n        Sets up the ALPN protocols on the context.\n        \"\"\"\n        if not protocols:\n            return\n        protocols_arr = _create_cfstring_array(protocols)\n        try:\n            result = Security.SSLSetALPNProtocols(self.context, protocols_arr)\n            _assert_no_error(result)\n        finally:\n            CoreFoundation.CFRelease(protocols_arr)\n\n    def _custom_validate(self, verify, trust_bundle):\n        \"\"\"\n        Called when we have set custom validation. We do this in two cases:\n        first, when cert validation is entirely disabled; and second, when\n        using a custom trust DB.\n        Raises an SSLError if the connection is not trusted.\n        \"\"\"\n        # If we disabled cert validation, just say: cool.\n        if not verify:\n            return\n\n        successes = (\n            SecurityConst.kSecTrustResultUnspecified,\n            SecurityConst.kSecTrustResultProceed,\n        )\n        try:\n            trust_result = self._evaluate_trust(trust_bundle)\n            if trust_result in successes:\n                return\n            reason = \"error code: %d\" % (trust_result,)\n        except Exception as e:\n            # Do not trust on error\n            reason = \"exception: %r\" % (e,)\n\n        # SecureTransport does not send an alert nor shuts down the connection.\n        rec = _build_tls_unknown_ca_alert(self.version())\n        self.socket.sendall(rec)\n        # close the connection immediately\n        # l_onoff = 1, activate linger\n        # l_linger = 0, linger for 0 seoncds\n        opts = struct.pack(\"ii\", 1, 0)\n        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, opts)\n        self.close()\n        raise ssl.SSLError(\"certificate verify failed, %s\" % reason)\n\n    def _evaluate_trust(self, trust_bundle):\n        # We want data in memory, so load it up.\n        if os.path.isfile(trust_bundle):\n            with open(trust_bundle, \"rb\") as f:\n                trust_bundle = f.read()\n\n        cert_array = None\n        trust = Security.SecTrustRef()\n\n        try:\n            # Get a CFArray that contains the certs we want.\n            cert_array = _cert_array_from_pem(trust_bundle)\n\n            # Ok, now the hard part. We want to get the SecTrustRef that ST has\n            # created for this connection, shove our CAs into it, tell ST to\n            # ignore everything else it knows, and then ask if it can build a\n            # chain. This is a buuuunch of code.\n            result = Security.SSLCopyPeerTrust(self.context, ctypes.byref(trust))\n            _assert_no_error(result)\n            if not trust:\n                raise ssl.SSLError(\"Failed to copy trust reference\")\n\n            result = Security.SecTrustSetAnchorCertificates(trust, cert_array)\n            _assert_no_error(result)\n\n            result = Security.SecTrustSetAnchorCertificatesOnly(trust, True)\n            _assert_no_error(result)\n\n            trust_result = Security.SecTrustResultType()\n            result = Security.SecTrustEvaluate(trust, ctypes.byref(trust_result))\n            _assert_no_error(result)\n        finally:\n            if trust:\n                CoreFoundation.CFRelease(trust)\n\n            if cert_array is not None:\n                CoreFoundation.CFRelease(cert_array)\n\n        return trust_result.value\n\n    def handshake(\n        self,\n        server_hostname,\n        verify,\n        trust_bundle,\n        min_version,\n        max_version,\n        client_cert,\n        client_key,\n        client_key_passphrase,\n        alpn_protocols,\n    ):\n        \"\"\"\n        Actually performs the TLS handshake. This is run automatically by\n        wrapped socket, and shouldn't be needed in user code.\n        \"\"\"\n        # First, we do the initial bits of connection setup. We need to create\n        # a context, set its I/O funcs, and set the connection reference.\n        self.context = Security.SSLCreateContext(\n            None, SecurityConst.kSSLClientSide, SecurityConst.kSSLStreamType\n        )\n        result = Security.SSLSetIOFuncs(\n            self.context, _read_callback_pointer, _write_callback_pointer\n        )\n        _assert_no_error(result)\n\n        # Here we need to compute the handle to use. We do this by taking the\n        # id of self modulo 2**31 - 1. If this is already in the dictionary, we\n        # just keep incrementing by one until we find a free space.\n        with _connection_ref_lock:\n            handle = id(self) % 2147483647\n            while handle in _connection_refs:\n                handle = (handle + 1) % 2147483647\n            _connection_refs[handle] = self\n\n        result = Security.SSLSetConnection(self.context, handle)\n        _assert_no_error(result)\n\n        # If we have a server hostname, we should set that too.\n        if server_hostname:\n            if not isinstance(server_hostname, bytes):\n                server_hostname = server_hostname.encode(\"utf-8\")\n\n            result = Security.SSLSetPeerDomainName(\n                self.context, server_hostname, len(server_hostname)\n            )\n            _assert_no_error(result)\n\n        # Setup the ciphers.\n        self._set_ciphers()\n\n        # Setup the ALPN protocols.\n        self._set_alpn_protocols(alpn_protocols)\n\n        # Set the minimum and maximum TLS versions.\n        result = Security.SSLSetProtocolVersionMin(self.context, min_version)\n        _assert_no_error(result)\n\n        result = Security.SSLSetProtocolVersionMax(self.context, max_version)\n        _assert_no_error(result)\n\n        # If there's a trust DB, we need to use it. We do that by telling\n        # SecureTransport to break on server auth. We also do that if we don't\n        # want to validate the certs at all: we just won't actually do any\n        # authing in that case.\n        if not verify or trust_bundle is not None:\n            result = Security.SSLSetSessionOption(\n                self.context, SecurityConst.kSSLSessionOptionBreakOnServerAuth, True\n            )\n            _assert_no_error(result)\n\n        # If there's a client cert, we need to use it.\n        if client_cert:\n            self._keychain, self._keychain_dir = _temporary_keychain()\n            self._client_cert_chain = _load_client_cert_chain(\n                self._keychain, client_cert, client_key\n            )\n            result = Security.SSLSetCertificate(self.context, self._client_cert_chain)\n            _assert_no_error(result)\n\n        while True:\n            with self._raise_on_error():\n                result = Security.SSLHandshake(self.context)\n\n                if result == SecurityConst.errSSLWouldBlock:\n                    raise socket.timeout(\"handshake timed out\")\n                elif result == SecurityConst.errSSLServerAuthCompleted:\n                    self._custom_validate(verify, trust_bundle)\n                    continue\n                else:\n                    _assert_no_error(result)\n                    break\n\n    def fileno(self):\n        return self.socket.fileno()\n\n    # Copy-pasted from Python 3.5 source code\n    def _decref_socketios(self):\n        if self._makefile_refs > 0:\n            self._makefile_refs -= 1\n        if self._closed:\n            self.close()\n\n    def recv(self, bufsiz):\n        buffer = ctypes.create_string_buffer(bufsiz)\n        bytes_read = self.recv_into(buffer, bufsiz)\n        data = buffer[:bytes_read]\n        return data\n\n    def recv_into(self, buffer, nbytes=None):\n        # Read short on EOF.\n        if self._closed:\n            return 0\n\n        if nbytes is None:\n            nbytes = len(buffer)\n\n        buffer = (ctypes.c_char * nbytes).from_buffer(buffer)\n        processed_bytes = ctypes.c_size_t(0)\n\n        with self._raise_on_error():\n            result = Security.SSLRead(\n                self.context, buffer, nbytes, ctypes.byref(processed_bytes)\n            )\n\n        # There are some result codes that we want to treat as \"not always\n        # errors\". Specifically, those are errSSLWouldBlock,\n        # errSSLClosedGraceful, and errSSLClosedNoNotify.\n        if result == SecurityConst.errSSLWouldBlock:\n            # If we didn't process any bytes, then this was just a time out.\n            # However, we can get errSSLWouldBlock in situations when we *did*\n            # read some data, and in those cases we should just read \"short\"\n            # and return.\n            if processed_bytes.value == 0:\n                # Timed out, no data read.\n                raise socket.timeout(\"recv timed out\")\n        elif result in (\n            SecurityConst.errSSLClosedGraceful,\n            SecurityConst.errSSLClosedNoNotify,\n        ):\n            # The remote peer has closed this connection. We should do so as\n            # well. Note that we don't actually return here because in\n            # principle this could actually be fired along with return data.\n            # It's unlikely though.\n            self.close()\n        else:\n            _assert_no_error(result)\n\n        # Ok, we read and probably succeeded. We should return whatever data\n        # was actually read.\n        return processed_bytes.value\n\n    def settimeout(self, timeout):\n        self._timeout = timeout\n\n    def gettimeout(self):\n        return self._timeout\n\n    def send(self, data):\n        processed_bytes = ctypes.c_size_t(0)\n\n        with self._raise_on_error():\n            result = Security.SSLWrite(\n                self.context, data, len(data), ctypes.byref(processed_bytes)\n            )\n\n        if result == SecurityConst.errSSLWouldBlock and processed_bytes.value == 0:\n            # Timed out\n            raise socket.timeout(\"send timed out\")\n        else:\n            _assert_no_error(result)\n\n        # We sent, and probably succeeded. Tell them how much we sent.\n        return processed_bytes.value\n\n    def sendall(self, data):\n        total_sent = 0\n        while total_sent < len(data):\n            sent = self.send(data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE])\n            total_sent += sent\n\n    def shutdown(self):\n        with self._raise_on_error():\n            Security.SSLClose(self.context)\n\n    def close(self):\n        # TODO: should I do clean shutdown here? Do I have to?\n        if self._makefile_refs < 1:\n            self._closed = True\n            if self.context:\n                CoreFoundation.CFRelease(self.context)\n                self.context = None\n            if self._client_cert_chain:\n                CoreFoundation.CFRelease(self._client_cert_chain)\n                self._client_cert_chain = None\n            if self._keychain:\n                Security.SecKeychainDelete(self._keychain)\n                CoreFoundation.CFRelease(self._keychain)\n                shutil.rmtree(self._keychain_dir)\n                self._keychain = self._keychain_dir = None\n            return self.socket.close()\n        else:\n            self._makefile_refs -= 1\n\n    def getpeercert(self, binary_form=False):\n        # Urgh, annoying.\n        #\n        # Here's how we do this:\n        #\n        # 1. Call SSLCopyPeerTrust to get hold of the trust object for this\n        #    connection.\n        # 2. Call SecTrustGetCertificateAtIndex for index 0 to get the leaf.\n        # 3. To get the CN, call SecCertificateCopyCommonName and process that\n        #    string so that it's of the appropriate type.\n        # 4. To get the SAN, we need to do something a bit more complex:\n        #    a. Call SecCertificateCopyValues to get the data, requesting\n        #       kSecOIDSubjectAltName.\n        #    b. Mess about with this dictionary to try to get the SANs out.\n        #\n        # This is gross. Really gross. It's going to be a few hundred LoC extra\n        # just to repeat something that SecureTransport can *already do*. So my\n        # operating assumption at this time is that what we want to do is\n        # instead to just flag to urllib3 that it shouldn't do its own hostname\n        # validation when using SecureTransport.\n        if not binary_form:\n            raise ValueError(\"SecureTransport only supports dumping binary certs\")\n        trust = Security.SecTrustRef()\n        certdata = None\n        der_bytes = None\n\n        try:\n            # Grab the trust store.\n            result = Security.SSLCopyPeerTrust(self.context, ctypes.byref(trust))\n            _assert_no_error(result)\n            if not trust:\n                # Probably we haven't done the handshake yet. No biggie.\n                return None\n\n            cert_count = Security.SecTrustGetCertificateCount(trust)\n            if not cert_count:\n                # Also a case that might happen if we haven't handshaked.\n                # Handshook? Handshaken?\n                return None\n\n            leaf = Security.SecTrustGetCertificateAtIndex(trust, 0)\n            assert leaf\n\n            # Ok, now we want the DER bytes.\n            certdata = Security.SecCertificateCopyData(leaf)\n            assert certdata\n\n            data_length = CoreFoundation.CFDataGetLength(certdata)\n            data_buffer = CoreFoundation.CFDataGetBytePtr(certdata)\n            der_bytes = ctypes.string_at(data_buffer, data_length)\n        finally:\n            if certdata:\n                CoreFoundation.CFRelease(certdata)\n            if trust:\n                CoreFoundation.CFRelease(trust)\n\n        return der_bytes\n\n    def version(self):\n        protocol = Security.SSLProtocol()\n        result = Security.SSLGetNegotiatedProtocolVersion(\n            self.context, ctypes.byref(protocol)\n        )\n        _assert_no_error(result)\n        if protocol.value == SecurityConst.kTLSProtocol13:\n            raise ssl.SSLError(\"SecureTransport does not support TLS 1.3\")\n        elif protocol.value == SecurityConst.kTLSProtocol12:\n            return \"TLSv1.2\"\n        elif protocol.value == SecurityConst.kTLSProtocol11:\n            return \"TLSv1.1\"\n        elif protocol.value == SecurityConst.kTLSProtocol1:\n            return \"TLSv1\"\n        elif protocol.value == SecurityConst.kSSLProtocol3:\n            return \"SSLv3\"\n        elif protocol.value == SecurityConst.kSSLProtocol2:\n            return \"SSLv2\"\n        else:\n            raise ssl.SSLError(\"Unknown TLS version: %r\" % protocol)\n\n    def _reuse(self):\n        self._makefile_refs += 1\n\n    def _drop(self):\n        if self._makefile_refs < 1:\n            self.close()\n        else:\n            self._makefile_refs -= 1\n\n\nif _fileobject:  # Platform-specific: Python 2\n\n    def makefile(self, mode, bufsize=-1):\n        self._makefile_refs += 1\n        return _fileobject(self, mode, bufsize, close=True)\n\n\nelse:  # Platform-specific: Python 3\n\n    def makefile(self, mode=\"r\", buffering=None, *args, **kwargs):\n        # We disable buffering with SecureTransport because it conflicts with\n        # the buffering that ST does internally (see issue #1153 for more).\n        buffering = 0\n        return backport_makefile(self, mode, buffering, *args, **kwargs)\n\n\nWrappedSocket.makefile = makefile\n\n\nclass SecureTransportContext(object):\n    \"\"\"\n    I am a wrapper class for the SecureTransport library, to translate the\n    interface of the standard library ``SSLContext`` object to calls into\n    SecureTransport.\n    \"\"\"\n\n    def __init__(self, protocol):\n        self._min_version, self._max_version = _protocol_to_min_max[protocol]\n        self._options = 0\n        self._verify = False\n        self._trust_bundle = None\n        self._client_cert = None\n        self._client_key = None\n        self._client_key_passphrase = None\n        self._alpn_protocols = None\n\n    @property\n    def check_hostname(self):\n        \"\"\"\n        SecureTransport cannot have its hostname checking disabled. For more,\n        see the comment on getpeercert() in this file.\n        \"\"\"\n        return True\n\n    @check_hostname.setter\n    def check_hostname(self, value):\n        \"\"\"\n        SecureTransport cannot have its hostname checking disabled. For more,\n        see the comment on getpeercert() in this file.\n        \"\"\"\n        pass\n\n    @property\n    def options(self):\n        # TODO: Well, crap.\n        #\n        # So this is the bit of the code that is the most likely to cause us\n        # trouble. Essentially we need to enumerate all of the SSL options that\n        # users might want to use and try to see if we can sensibly translate\n        # them, or whether we should just ignore them.\n        return self._options\n\n    @options.setter\n    def options(self, value):\n        # TODO: Update in line with above.\n        self._options = value\n\n    @property\n    def verify_mode(self):\n        return ssl.CERT_REQUIRED if self._verify else ssl.CERT_NONE\n\n    @verify_mode.setter\n    def verify_mode(self, value):\n        self._verify = True if value == ssl.CERT_REQUIRED else False\n\n    def set_default_verify_paths(self):\n        # So, this has to do something a bit weird. Specifically, what it does\n        # is nothing.\n        #\n        # This means that, if we had previously had load_verify_locations\n        # called, this does not undo that. We need to do that because it turns\n        # out that the rest of the urllib3 code will attempt to load the\n        # default verify paths if it hasn't been told about any paths, even if\n        # the context itself was sometime earlier. We resolve that by just\n        # ignoring it.\n        pass\n\n    def load_default_certs(self):\n        return self.set_default_verify_paths()\n\n    def set_ciphers(self, ciphers):\n        # For now, we just require the default cipher string.\n        if ciphers != util.ssl_.DEFAULT_CIPHERS:\n            raise ValueError(\"SecureTransport doesn't support custom cipher strings\")\n\n    def load_verify_locations(self, cafile=None, capath=None, cadata=None):\n        # OK, we only really support cadata and cafile.\n        if capath is not None:\n            raise ValueError(\"SecureTransport does not support cert directories\")\n\n        # Raise if cafile does not exist.\n        if cafile is not None:\n            with open(cafile):\n                pass\n\n        self._trust_bundle = cafile or cadata\n\n    def load_cert_chain(self, certfile, keyfile=None, password=None):\n        self._client_cert = certfile\n        self._client_key = keyfile\n        self._client_cert_passphrase = password\n\n    def set_alpn_protocols(self, protocols):\n        \"\"\"\n        Sets the ALPN protocols that will later be set on the context.\n\n        Raises a NotImplementedError if ALPN is not supported.\n        \"\"\"\n        if not hasattr(Security, \"SSLSetALPNProtocols\"):\n            raise NotImplementedError(\n                \"SecureTransport supports ALPN only in macOS 10.12+\"\n            )\n        self._alpn_protocols = [six.ensure_binary(p) for p in protocols]\n\n    def wrap_socket(\n        self,\n        sock,\n        server_side=False,\n        do_handshake_on_connect=True,\n        suppress_ragged_eofs=True,\n        server_hostname=None,\n    ):\n        # So, what do we do here? Firstly, we assert some properties. This is a\n        # stripped down shim, so there is some functionality we don't support.\n        # See PEP 543 for the real deal.\n        assert not server_side\n        assert do_handshake_on_connect\n        assert suppress_ragged_eofs\n\n        # Ok, we're good to go. Now we want to create the wrapped socket object\n        # and store it in the appropriate place.\n        wrapped_socket = WrappedSocket(sock)\n\n        # Now we can handshake\n        wrapped_socket.handshake(\n            server_hostname,\n            self._verify,\n            self._trust_bundle,\n            self._min_version,\n            self._max_version,\n            self._client_cert,\n            self._client_key,\n            self._client_key_passphrase,\n            self._alpn_protocols,\n        )\n        return wrapped_socket\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/contrib/socks.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nThis module contains provisional support for SOCKS proxies from within\nurllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and\nSOCKS5. To enable its functionality, either install PySocks or install this\nmodule with the ``socks`` extra.\n\nThe SOCKS implementation supports the full range of urllib3 features. It also\nsupports the following SOCKS features:\n\n- SOCKS4A (``proxy_url='socks4a://...``)\n- SOCKS4 (``proxy_url='socks4://...``)\n- SOCKS5 with remote DNS (``proxy_url='socks5h://...``)\n- SOCKS5 with local DNS (``proxy_url='socks5://...``)\n- Usernames and passwords for the SOCKS proxy\n\n.. note::\n   It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in\n   your ``proxy_url`` to ensure that DNS resolution is done from the remote\n   server instead of client-side when connecting to a domain name.\n\nSOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5\nsupports IPv4, IPv6, and domain names.\n\nWhen connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url``\nwill be sent as the ``userid`` section of the SOCKS request:\n\n.. code-block:: python\n\n    proxy_url=\"socks4a://<userid>@proxy-host\"\n\nWhen connecting to a SOCKS5 proxy the ``username`` and ``password`` portion\nof the ``proxy_url`` will be sent as the username/password to authenticate\nwith the proxy:\n\n.. code-block:: python\n\n    proxy_url=\"socks5h://<username>:<password>@proxy-host\"\n\n\"\"\"\nfrom __future__ import absolute_import\n\ntry:\n    import socks\nexcept ImportError:\n    import warnings\n\n    from ..exceptions import DependencyWarning\n\n    warnings.warn(\n        (\n            \"SOCKS support in urllib3 requires the installation of optional \"\n            \"dependencies: specifically, PySocks.  For more information, see \"\n            \"https://urllib3.readthedocs.io/en/1.26.x/contrib.html#socks-proxies\"\n        ),\n        DependencyWarning,\n    )\n    raise\n\nfrom socket import error as SocketError\nfrom socket import timeout as SocketTimeout\n\nfrom ..connection import HTTPConnection, HTTPSConnection\nfrom ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool\nfrom ..exceptions import ConnectTimeoutError, NewConnectionError\nfrom ..poolmanager import PoolManager\nfrom ..util.url import parse_url\n\ntry:\n    import ssl\nexcept ImportError:\n    ssl = None\n\n\nclass SOCKSConnection(HTTPConnection):\n    \"\"\"\n    A plain-text HTTP connection that connects via a SOCKS proxy.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        self._socks_options = kwargs.pop(\"_socks_options\")\n        super(SOCKSConnection, self).__init__(*args, **kwargs)\n\n    def _new_conn(self):\n        \"\"\"\n        Establish a new connection via the SOCKS proxy.\n        \"\"\"\n        extra_kw = {}\n        if self.source_address:\n            extra_kw[\"source_address\"] = self.source_address\n\n        if self.socket_options:\n            extra_kw[\"socket_options\"] = self.socket_options\n\n        try:\n            conn = socks.create_connection(\n                (self.host, self.port),\n                proxy_type=self._socks_options[\"socks_version\"],\n                proxy_addr=self._socks_options[\"proxy_host\"],\n                proxy_port=self._socks_options[\"proxy_port\"],\n                proxy_username=self._socks_options[\"username\"],\n                proxy_password=self._socks_options[\"password\"],\n                proxy_rdns=self._socks_options[\"rdns\"],\n                timeout=self.timeout,\n                **extra_kw\n            )\n\n        except SocketTimeout:\n            raise ConnectTimeoutError(\n                self,\n                \"Connection to %s timed out. (connect timeout=%s)\"\n                % (self.host, self.timeout),\n            )\n\n        except socks.ProxyError as e:\n            # This is fragile as hell, but it seems to be the only way to raise\n            # useful errors here.\n            if e.socket_err:\n                error = e.socket_err\n                if isinstance(error, SocketTimeout):\n                    raise ConnectTimeoutError(\n                        self,\n                        \"Connection to %s timed out. (connect timeout=%s)\"\n                        % (self.host, self.timeout),\n                    )\n                else:\n                    raise NewConnectionError(\n                        self, \"Failed to establish a new connection: %s\" % error\n                    )\n            else:\n                raise NewConnectionError(\n                    self, \"Failed to establish a new connection: %s\" % e\n                )\n\n        except SocketError as e:  # Defensive: PySocks should catch all these.\n            raise NewConnectionError(\n                self, \"Failed to establish a new connection: %s\" % e\n            )\n\n        return conn\n\n\n# We don't need to duplicate the Verified/Unverified distinction from\n# urllib3/connection.py here because the HTTPSConnection will already have been\n# correctly set to either the Verified or Unverified form by that module. This\n# means the SOCKSHTTPSConnection will automatically be the correct type.\nclass SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection):\n    pass\n\n\nclass SOCKSHTTPConnectionPool(HTTPConnectionPool):\n    ConnectionCls = SOCKSConnection\n\n\nclass SOCKSHTTPSConnectionPool(HTTPSConnectionPool):\n    ConnectionCls = SOCKSHTTPSConnection\n\n\nclass SOCKSProxyManager(PoolManager):\n    \"\"\"\n    A version of the urllib3 ProxyManager that routes connections via the\n    defined SOCKS proxy.\n    \"\"\"\n\n    pool_classes_by_scheme = {\n        \"http\": SOCKSHTTPConnectionPool,\n        \"https\": SOCKSHTTPSConnectionPool,\n    }\n\n    def __init__(\n        self,\n        proxy_url,\n        username=None,\n        password=None,\n        num_pools=10,\n        headers=None,\n        **connection_pool_kw\n    ):\n        parsed = parse_url(proxy_url)\n\n        if username is None and password is None and parsed.auth is not None:\n            split = parsed.auth.split(\":\")\n            if len(split) == 2:\n                username, password = split\n        if parsed.scheme == \"socks5\":\n            socks_version = socks.PROXY_TYPE_SOCKS5\n            rdns = False\n        elif parsed.scheme == \"socks5h\":\n            socks_version = socks.PROXY_TYPE_SOCKS5\n            rdns = True\n        elif parsed.scheme == \"socks4\":\n            socks_version = socks.PROXY_TYPE_SOCKS4\n            rdns = False\n        elif parsed.scheme == \"socks4a\":\n            socks_version = socks.PROXY_TYPE_SOCKS4\n            rdns = True\n        else:\n            raise ValueError(\"Unable to determine SOCKS version from %s\" % proxy_url)\n\n        self.proxy_url = proxy_url\n\n        socks_options = {\n            \"socks_version\": socks_version,\n            \"proxy_host\": parsed.host,\n            \"proxy_port\": parsed.port,\n            \"username\": username,\n            \"password\": password,\n            \"rdns\": rdns,\n        }\n        connection_pool_kw[\"_socks_options\"] = socks_options\n\n        super(SOCKSProxyManager, self).__init__(\n            num_pools, headers, **connection_pool_kw\n        )\n\n        self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/exceptions.py",
    "content": "from __future__ import absolute_import\n\nfrom .packages.six.moves.http_client import IncompleteRead as httplib_IncompleteRead\n\n# Base Exceptions\n\n\nclass HTTPError(Exception):\n    \"\"\"Base exception used by this module.\"\"\"\n\n    pass\n\n\nclass HTTPWarning(Warning):\n    \"\"\"Base warning used by this module.\"\"\"\n\n    pass\n\n\nclass PoolError(HTTPError):\n    \"\"\"Base exception for errors caused within a pool.\"\"\"\n\n    def __init__(self, pool, message):\n        self.pool = pool\n        HTTPError.__init__(self, \"%s: %s\" % (pool, message))\n\n    def __reduce__(self):\n        # For pickling purposes.\n        return self.__class__, (None, None)\n\n\nclass RequestError(PoolError):\n    \"\"\"Base exception for PoolErrors that have associated URLs.\"\"\"\n\n    def __init__(self, pool, url, message):\n        self.url = url\n        PoolError.__init__(self, pool, message)\n\n    def __reduce__(self):\n        # For pickling purposes.\n        return self.__class__, (None, self.url, None)\n\n\nclass SSLError(HTTPError):\n    \"\"\"Raised when SSL certificate fails in an HTTPS connection.\"\"\"\n\n    pass\n\n\nclass ProxyError(HTTPError):\n    \"\"\"Raised when the connection to a proxy fails.\"\"\"\n\n    def __init__(self, message, error, *args):\n        super(ProxyError, self).__init__(message, error, *args)\n        self.original_error = error\n\n\nclass DecodeError(HTTPError):\n    \"\"\"Raised when automatic decoding based on Content-Type fails.\"\"\"\n\n    pass\n\n\nclass ProtocolError(HTTPError):\n    \"\"\"Raised when something unexpected happens mid-request/response.\"\"\"\n\n    pass\n\n\n#: Renamed to ProtocolError but aliased for backwards compatibility.\nConnectionError = ProtocolError\n\n\n# Leaf Exceptions\n\n\nclass MaxRetryError(RequestError):\n    \"\"\"Raised when the maximum number of retries is exceeded.\n\n    :param pool: The connection pool\n    :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool`\n    :param string url: The requested Url\n    :param exceptions.Exception reason: The underlying error\n\n    \"\"\"\n\n    def __init__(self, pool, url, reason=None):\n        self.reason = reason\n\n        message = \"Max retries exceeded with url: %s (Caused by %r)\" % (url, reason)\n\n        RequestError.__init__(self, pool, url, message)\n\n\nclass HostChangedError(RequestError):\n    \"\"\"Raised when an existing pool gets a request for a foreign host.\"\"\"\n\n    def __init__(self, pool, url, retries=3):\n        message = \"Tried to open a foreign host with url: %s\" % url\n        RequestError.__init__(self, pool, url, message)\n        self.retries = retries\n\n\nclass TimeoutStateError(HTTPError):\n    \"\"\"Raised when passing an invalid state to a timeout\"\"\"\n\n    pass\n\n\nclass TimeoutError(HTTPError):\n    \"\"\"Raised when a socket timeout error occurs.\n\n    Catching this error will catch both :exc:`ReadTimeoutErrors\n    <ReadTimeoutError>` and :exc:`ConnectTimeoutErrors <ConnectTimeoutError>`.\n    \"\"\"\n\n    pass\n\n\nclass ReadTimeoutError(TimeoutError, RequestError):\n    \"\"\"Raised when a socket timeout occurs while receiving data from a server\"\"\"\n\n    pass\n\n\n# This timeout error does not have a URL attached and needs to inherit from the\n# base HTTPError\nclass ConnectTimeoutError(TimeoutError):\n    \"\"\"Raised when a socket timeout occurs while connecting to a server\"\"\"\n\n    pass\n\n\nclass NewConnectionError(ConnectTimeoutError, PoolError):\n    \"\"\"Raised when we fail to establish a new connection. Usually ECONNREFUSED.\"\"\"\n\n    pass\n\n\nclass EmptyPoolError(PoolError):\n    \"\"\"Raised when a pool runs out of connections and no more are allowed.\"\"\"\n\n    pass\n\n\nclass ClosedPoolError(PoolError):\n    \"\"\"Raised when a request enters a pool after the pool has been closed.\"\"\"\n\n    pass\n\n\nclass LocationValueError(ValueError, HTTPError):\n    \"\"\"Raised when there is something wrong with a given URL input.\"\"\"\n\n    pass\n\n\nclass LocationParseError(LocationValueError):\n    \"\"\"Raised when get_host or similar fails to parse the URL input.\"\"\"\n\n    def __init__(self, location):\n        message = \"Failed to parse: %s\" % location\n        HTTPError.__init__(self, message)\n\n        self.location = location\n\n\nclass URLSchemeUnknown(LocationValueError):\n    \"\"\"Raised when a URL input has an unsupported scheme.\"\"\"\n\n    def __init__(self, scheme):\n        message = \"Not supported URL scheme %s\" % scheme\n        super(URLSchemeUnknown, self).__init__(message)\n\n        self.scheme = scheme\n\n\nclass ResponseError(HTTPError):\n    \"\"\"Used as a container for an error reason supplied in a MaxRetryError.\"\"\"\n\n    GENERIC_ERROR = \"too many error responses\"\n    SPECIFIC_ERROR = \"too many {status_code} error responses\"\n\n\nclass SecurityWarning(HTTPWarning):\n    \"\"\"Warned when performing security reducing actions\"\"\"\n\n    pass\n\n\nclass SubjectAltNameWarning(SecurityWarning):\n    \"\"\"Warned when connecting to a host with a certificate missing a SAN.\"\"\"\n\n    pass\n\n\nclass InsecureRequestWarning(SecurityWarning):\n    \"\"\"Warned when making an unverified HTTPS request.\"\"\"\n\n    pass\n\n\nclass SystemTimeWarning(SecurityWarning):\n    \"\"\"Warned when system time is suspected to be wrong\"\"\"\n\n    pass\n\n\nclass InsecurePlatformWarning(SecurityWarning):\n    \"\"\"Warned when certain TLS/SSL configuration is not available on a platform.\"\"\"\n\n    pass\n\n\nclass SNIMissingWarning(HTTPWarning):\n    \"\"\"Warned when making a HTTPS request without SNI available.\"\"\"\n\n    pass\n\n\nclass DependencyWarning(HTTPWarning):\n    \"\"\"\n    Warned when an attempt is made to import a module with missing optional\n    dependencies.\n    \"\"\"\n\n    pass\n\n\nclass ResponseNotChunked(ProtocolError, ValueError):\n    \"\"\"Response needs to be chunked in order to read it as chunks.\"\"\"\n\n    pass\n\n\nclass BodyNotHttplibCompatible(HTTPError):\n    \"\"\"\n    Body should be :class:`http.client.HTTPResponse` like\n    (have an fp attribute which returns raw chunks) for read_chunked().\n    \"\"\"\n\n    pass\n\n\nclass IncompleteRead(HTTPError, httplib_IncompleteRead):\n    \"\"\"\n    Response length doesn't match expected Content-Length\n\n    Subclass of :class:`http.client.IncompleteRead` to allow int value\n    for ``partial`` to avoid creating large objects on streamed reads.\n    \"\"\"\n\n    def __init__(self, partial, expected):\n        super(IncompleteRead, self).__init__(partial, expected)\n\n    def __repr__(self):\n        return \"IncompleteRead(%i bytes read, %i more expected)\" % (\n            self.partial,\n            self.expected,\n        )\n\n\nclass InvalidChunkLength(HTTPError, httplib_IncompleteRead):\n    \"\"\"Invalid chunk length in a chunked response.\"\"\"\n\n    def __init__(self, response, length):\n        super(InvalidChunkLength, self).__init__(\n            response.tell(), response.length_remaining\n        )\n        self.response = response\n        self.length = length\n\n    def __repr__(self):\n        return \"InvalidChunkLength(got length %r, %i bytes read)\" % (\n            self.length,\n            self.partial,\n        )\n\n\nclass InvalidHeader(HTTPError):\n    \"\"\"The header provided was somehow invalid.\"\"\"\n\n    pass\n\n\nclass ProxySchemeUnknown(AssertionError, URLSchemeUnknown):\n    \"\"\"ProxyManager does not support the supplied scheme\"\"\"\n\n    # TODO(t-8ch): Stop inheriting from AssertionError in v2.0.\n\n    def __init__(self, scheme):\n        # 'localhost' is here because our URL parser parses\n        # localhost:8080 -> scheme=localhost, remove if we fix this.\n        if scheme == \"localhost\":\n            scheme = None\n        if scheme is None:\n            message = \"Proxy URL had no scheme, should start with http:// or https://\"\n        else:\n            message = (\n                \"Proxy URL had unsupported scheme %s, should use http:// or https://\"\n                % scheme\n            )\n        super(ProxySchemeUnknown, self).__init__(message)\n\n\nclass ProxySchemeUnsupported(ValueError):\n    \"\"\"Fetching HTTPS resources through HTTPS proxies is unsupported\"\"\"\n\n    pass\n\n\nclass HeaderParsingError(HTTPError):\n    \"\"\"Raised by assert_header_parsing, but we convert it to a log.warning statement.\"\"\"\n\n    def __init__(self, defects, unparsed_data):\n        message = \"%s, unparsed data: %r\" % (defects or \"Unknown\", unparsed_data)\n        super(HeaderParsingError, self).__init__(message)\n\n\nclass UnrewindableBodyError(HTTPError):\n    \"\"\"urllib3 encountered an error when trying to rewind a body\"\"\"\n\n    pass\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/fields.py",
    "content": "from __future__ import absolute_import\n\nimport email.utils\nimport mimetypes\nimport re\n\nfrom .packages import six\n\n\ndef guess_content_type(filename, default=\"application/octet-stream\"):\n    \"\"\"\n    Guess the \"Content-Type\" of a file.\n\n    :param filename:\n        The filename to guess the \"Content-Type\" of using :mod:`mimetypes`.\n    :param default:\n        If no \"Content-Type\" can be guessed, default to `default`.\n    \"\"\"\n    if filename:\n        return mimetypes.guess_type(filename)[0] or default\n    return default\n\n\ndef format_header_param_rfc2231(name, value):\n    \"\"\"\n    Helper function to format and quote a single header parameter using the\n    strategy defined in RFC 2231.\n\n    Particularly useful for header parameters which might contain\n    non-ASCII values, like file names. This follows\n    `RFC 2388 Section 4.4 <https://tools.ietf.org/html/rfc2388#section-4.4>`_.\n\n    :param name:\n        The name of the parameter, a string expected to be ASCII only.\n    :param value:\n        The value of the parameter, provided as ``bytes`` or `str``.\n    :ret:\n        An RFC-2231-formatted unicode string.\n    \"\"\"\n    if isinstance(value, six.binary_type):\n        value = value.decode(\"utf-8\")\n\n    if not any(ch in value for ch in '\"\\\\\\r\\n'):\n        result = u'%s=\"%s\"' % (name, value)\n        try:\n            result.encode(\"ascii\")\n        except (UnicodeEncodeError, UnicodeDecodeError):\n            pass\n        else:\n            return result\n\n    if six.PY2:  # Python 2:\n        value = value.encode(\"utf-8\")\n\n    # encode_rfc2231 accepts an encoded string and returns an ascii-encoded\n    # string in Python 2 but accepts and returns unicode strings in Python 3\n    value = email.utils.encode_rfc2231(value, \"utf-8\")\n    value = \"%s*=%s\" % (name, value)\n\n    if six.PY2:  # Python 2:\n        value = value.decode(\"utf-8\")\n\n    return value\n\n\n_HTML5_REPLACEMENTS = {\n    u\"\\u0022\": u\"%22\",\n    # Replace \"\\\" with \"\\\\\".\n    u\"\\u005C\": u\"\\u005C\\u005C\",\n}\n\n# All control characters from 0x00 to 0x1F *except* 0x1B.\n_HTML5_REPLACEMENTS.update(\n    {\n        six.unichr(cc): u\"%{:02X}\".format(cc)\n        for cc in range(0x00, 0x1F + 1)\n        if cc not in (0x1B,)\n    }\n)\n\n\ndef _replace_multiple(value, needles_and_replacements):\n    def replacer(match):\n        return needles_and_replacements[match.group(0)]\n\n    pattern = re.compile(\n        r\"|\".join([re.escape(needle) for needle in needles_and_replacements.keys()])\n    )\n\n    result = pattern.sub(replacer, value)\n\n    return result\n\n\ndef format_header_param_html5(name, value):\n    \"\"\"\n    Helper function to format and quote a single header parameter using the\n    HTML5 strategy.\n\n    Particularly useful for header parameters which might contain\n    non-ASCII values, like file names. This follows the `HTML5 Working Draft\n    Section 4.10.22.7`_ and matches the behavior of curl and modern browsers.\n\n    .. _HTML5 Working Draft Section 4.10.22.7:\n        https://w3c.github.io/html/sec-forms.html#multipart-form-data\n\n    :param name:\n        The name of the parameter, a string expected to be ASCII only.\n    :param value:\n        The value of the parameter, provided as ``bytes`` or `str``.\n    :ret:\n        A unicode string, stripped of troublesome characters.\n    \"\"\"\n    if isinstance(value, six.binary_type):\n        value = value.decode(\"utf-8\")\n\n    value = _replace_multiple(value, _HTML5_REPLACEMENTS)\n\n    return u'%s=\"%s\"' % (name, value)\n\n\n# For backwards-compatibility.\nformat_header_param = format_header_param_html5\n\n\nclass RequestField(object):\n    \"\"\"\n    A data container for request body parameters.\n\n    :param name:\n        The name of this request field. Must be unicode.\n    :param data:\n        The data/value body.\n    :param filename:\n        An optional filename of the request field. Must be unicode.\n    :param headers:\n        An optional dict-like object of headers to initially use for the field.\n    :param header_formatter:\n        An optional callable that is used to encode and format the headers. By\n        default, this is :func:`format_header_param_html5`.\n    \"\"\"\n\n    def __init__(\n        self,\n        name,\n        data,\n        filename=None,\n        headers=None,\n        header_formatter=format_header_param_html5,\n    ):\n        self._name = name\n        self._filename = filename\n        self.data = data\n        self.headers = {}\n        if headers:\n            self.headers = dict(headers)\n        self.header_formatter = header_formatter\n\n    @classmethod\n    def from_tuples(cls, fieldname, value, header_formatter=format_header_param_html5):\n        \"\"\"\n        A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters.\n\n        Supports constructing :class:`~urllib3.fields.RequestField` from\n        parameter of key/value strings AND key/filetuple. A filetuple is a\n        (filename, data, MIME type) tuple where the MIME type is optional.\n        For example::\n\n            'foo': 'bar',\n            'fakefile': ('foofile.txt', 'contents of foofile'),\n            'realfile': ('barfile.txt', open('realfile').read()),\n            'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'),\n            'nonamefile': 'contents of nonamefile field',\n\n        Field names and filenames must be unicode.\n        \"\"\"\n        if isinstance(value, tuple):\n            if len(value) == 3:\n                filename, data, content_type = value\n            else:\n                filename, data = value\n                content_type = guess_content_type(filename)\n        else:\n            filename = None\n            content_type = None\n            data = value\n\n        request_param = cls(\n            fieldname, data, filename=filename, header_formatter=header_formatter\n        )\n        request_param.make_multipart(content_type=content_type)\n\n        return request_param\n\n    def _render_part(self, name, value):\n        \"\"\"\n        Overridable helper function to format a single header parameter. By\n        default, this calls ``self.header_formatter``.\n\n        :param name:\n            The name of the parameter, a string expected to be ASCII only.\n        :param value:\n            The value of the parameter, provided as a unicode string.\n        \"\"\"\n\n        return self.header_formatter(name, value)\n\n    def _render_parts(self, header_parts):\n        \"\"\"\n        Helper function to format and quote a single header.\n\n        Useful for single headers that are composed of multiple items. E.g.,\n        'Content-Disposition' fields.\n\n        :param header_parts:\n            A sequence of (k, v) tuples or a :class:`dict` of (k, v) to format\n            as `k1=\"v1\"; k2=\"v2\"; ...`.\n        \"\"\"\n        parts = []\n        iterable = header_parts\n        if isinstance(header_parts, dict):\n            iterable = header_parts.items()\n\n        for name, value in iterable:\n            if value is not None:\n                parts.append(self._render_part(name, value))\n\n        return u\"; \".join(parts)\n\n    def render_headers(self):\n        \"\"\"\n        Renders the headers for this request field.\n        \"\"\"\n        lines = []\n\n        sort_keys = [\"Content-Disposition\", \"Content-Type\", \"Content-Location\"]\n        for sort_key in sort_keys:\n            if self.headers.get(sort_key, False):\n                lines.append(u\"%s: %s\" % (sort_key, self.headers[sort_key]))\n\n        for header_name, header_value in self.headers.items():\n            if header_name not in sort_keys:\n                if header_value:\n                    lines.append(u\"%s: %s\" % (header_name, header_value))\n\n        lines.append(u\"\\r\\n\")\n        return u\"\\r\\n\".join(lines)\n\n    def make_multipart(\n        self, content_disposition=None, content_type=None, content_location=None\n    ):\n        \"\"\"\n        Makes this request field into a multipart request field.\n\n        This method overrides \"Content-Disposition\", \"Content-Type\" and\n        \"Content-Location\" headers to the request parameter.\n\n        :param content_type:\n            The 'Content-Type' of the request body.\n        :param content_location:\n            The 'Content-Location' of the request body.\n\n        \"\"\"\n        self.headers[\"Content-Disposition\"] = content_disposition or u\"form-data\"\n        self.headers[\"Content-Disposition\"] += u\"; \".join(\n            [\n                u\"\",\n                self._render_parts(\n                    ((u\"name\", self._name), (u\"filename\", self._filename))\n                ),\n            ]\n        )\n        self.headers[\"Content-Type\"] = content_type\n        self.headers[\"Content-Location\"] = content_location\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/filepost.py",
    "content": "from __future__ import absolute_import\n\nimport binascii\nimport codecs\nimport os\nfrom io import BytesIO\n\nfrom .fields import RequestField\nfrom .packages import six\nfrom .packages.six import b\n\nwriter = codecs.lookup(\"utf-8\")[3]\n\n\ndef choose_boundary():\n    \"\"\"\n    Our embarrassingly-simple replacement for mimetools.choose_boundary.\n    \"\"\"\n    boundary = binascii.hexlify(os.urandom(16))\n    if not six.PY2:\n        boundary = boundary.decode(\"ascii\")\n    return boundary\n\n\ndef iter_field_objects(fields):\n    \"\"\"\n    Iterate over fields.\n\n    Supports list of (k, v) tuples and dicts, and lists of\n    :class:`~urllib3.fields.RequestField`.\n\n    \"\"\"\n    if isinstance(fields, dict):\n        i = six.iteritems(fields)\n    else:\n        i = iter(fields)\n\n    for field in i:\n        if isinstance(field, RequestField):\n            yield field\n        else:\n            yield RequestField.from_tuples(*field)\n\n\ndef iter_fields(fields):\n    \"\"\"\n    .. deprecated:: 1.6\n\n    Iterate over fields.\n\n    The addition of :class:`~urllib3.fields.RequestField` makes this function\n    obsolete. Instead, use :func:`iter_field_objects`, which returns\n    :class:`~urllib3.fields.RequestField` objects.\n\n    Supports list of (k, v) tuples and dicts.\n    \"\"\"\n    if isinstance(fields, dict):\n        return ((k, v) for k, v in six.iteritems(fields))\n\n    return ((k, v) for k, v in fields)\n\n\ndef encode_multipart_formdata(fields, boundary=None):\n    \"\"\"\n    Encode a dictionary of ``fields`` using the multipart/form-data MIME format.\n\n    :param fields:\n        Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`).\n\n    :param boundary:\n        If not specified, then a random boundary will be generated using\n        :func:`urllib3.filepost.choose_boundary`.\n    \"\"\"\n    body = BytesIO()\n    if boundary is None:\n        boundary = choose_boundary()\n\n    for field in iter_field_objects(fields):\n        body.write(b(\"--%s\\r\\n\" % (boundary)))\n\n        writer(body).write(field.render_headers())\n        data = field.data\n\n        if isinstance(data, int):\n            data = str(data)  # Backwards compatibility\n\n        if isinstance(data, six.text_type):\n            writer(body).write(data)\n        else:\n            body.write(data)\n\n        body.write(b\"\\r\\n\")\n\n    body.write(b(\"--%s--\\r\\n\" % (boundary)))\n\n    content_type = str(\"multipart/form-data; boundary=%s\" % boundary)\n\n    return body.getvalue(), content_type\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/packages/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/packages/backports/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/packages/backports/makefile.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nbackports.makefile\n~~~~~~~~~~~~~~~~~~\n\nBackports the Python 3 ``socket.makefile`` method for use with anything that\nwants to create a \"fake\" socket object.\n\"\"\"\nimport io\nfrom socket import SocketIO\n\n\ndef backport_makefile(\n    self, mode=\"r\", buffering=None, encoding=None, errors=None, newline=None\n):\n    \"\"\"\n    Backport of ``socket.makefile`` from Python 3.5.\n    \"\"\"\n    if not set(mode) <= {\"r\", \"w\", \"b\"}:\n        raise ValueError(\"invalid mode %r (only r, w, b allowed)\" % (mode,))\n    writing = \"w\" in mode\n    reading = \"r\" in mode or not writing\n    assert reading or writing\n    binary = \"b\" in mode\n    rawmode = \"\"\n    if reading:\n        rawmode += \"r\"\n    if writing:\n        rawmode += \"w\"\n    raw = SocketIO(self, rawmode)\n    self._makefile_refs += 1\n    if buffering is None:\n        buffering = -1\n    if buffering < 0:\n        buffering = io.DEFAULT_BUFFER_SIZE\n    if buffering == 0:\n        if not binary:\n            raise ValueError(\"unbuffered streams must be binary\")\n        return raw\n    if reading and writing:\n        buffer = io.BufferedRWPair(raw, raw, buffering)\n    elif reading:\n        buffer = io.BufferedReader(raw, buffering)\n    else:\n        assert writing\n        buffer = io.BufferedWriter(raw, buffering)\n    if binary:\n        return buffer\n    text = io.TextIOWrapper(buffer, encoding, errors, newline)\n    text.mode = mode\n    return text\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/packages/six.py",
    "content": "# Copyright (c) 2010-2020 Benjamin Peterson\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\"\"\"Utilities for writing code that runs on Python 2 and 3\"\"\"\n\nfrom __future__ import absolute_import\n\nimport functools\nimport itertools\nimport operator\nimport sys\nimport types\n\n__author__ = \"Benjamin Peterson <benjamin@python.org>\"\n__version__ = \"1.16.0\"\n\n\n# Useful for very coarse version differentiation.\nPY2 = sys.version_info[0] == 2\nPY3 = sys.version_info[0] == 3\nPY34 = sys.version_info[0:2] >= (3, 4)\n\nif PY3:\n    string_types = (str,)\n    integer_types = (int,)\n    class_types = (type,)\n    text_type = str\n    binary_type = bytes\n\n    MAXSIZE = sys.maxsize\nelse:\n    string_types = (basestring,)\n    integer_types = (int, long)\n    class_types = (type, types.ClassType)\n    text_type = unicode\n    binary_type = str\n\n    if sys.platform.startswith(\"java\"):\n        # Jython always uses 32 bits.\n        MAXSIZE = int((1 << 31) - 1)\n    else:\n        # It's possible to have sizeof(long) != sizeof(Py_ssize_t).\n        class X(object):\n            def __len__(self):\n                return 1 << 31\n\n        try:\n            len(X())\n        except OverflowError:\n            # 32-bit\n            MAXSIZE = int((1 << 31) - 1)\n        else:\n            # 64-bit\n            MAXSIZE = int((1 << 63) - 1)\n        del X\n\nif PY34:\n    from importlib.util import spec_from_loader\nelse:\n    spec_from_loader = None\n\n\ndef _add_doc(func, doc):\n    \"\"\"Add documentation to a function.\"\"\"\n    func.__doc__ = doc\n\n\ndef _import_module(name):\n    \"\"\"Import module, returning the module after the last dot.\"\"\"\n    __import__(name)\n    return sys.modules[name]\n\n\nclass _LazyDescr(object):\n    def __init__(self, name):\n        self.name = name\n\n    def __get__(self, obj, tp):\n        result = self._resolve()\n        setattr(obj, self.name, result)  # Invokes __set__.\n        try:\n            # This is a bit ugly, but it avoids running this again by\n            # removing this descriptor.\n            delattr(obj.__class__, self.name)\n        except AttributeError:\n            pass\n        return result\n\n\nclass MovedModule(_LazyDescr):\n    def __init__(self, name, old, new=None):\n        super(MovedModule, self).__init__(name)\n        if PY3:\n            if new is None:\n                new = name\n            self.mod = new\n        else:\n            self.mod = old\n\n    def _resolve(self):\n        return _import_module(self.mod)\n\n    def __getattr__(self, attr):\n        _module = self._resolve()\n        value = getattr(_module, attr)\n        setattr(self, attr, value)\n        return value\n\n\nclass _LazyModule(types.ModuleType):\n    def __init__(self, name):\n        super(_LazyModule, self).__init__(name)\n        self.__doc__ = self.__class__.__doc__\n\n    def __dir__(self):\n        attrs = [\"__doc__\", \"__name__\"]\n        attrs += [attr.name for attr in self._moved_attributes]\n        return attrs\n\n    # Subclasses should override this\n    _moved_attributes = []\n\n\nclass MovedAttribute(_LazyDescr):\n    def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):\n        super(MovedAttribute, self).__init__(name)\n        if PY3:\n            if new_mod is None:\n                new_mod = name\n            self.mod = new_mod\n            if new_attr is None:\n                if old_attr is None:\n                    new_attr = name\n                else:\n                    new_attr = old_attr\n            self.attr = new_attr\n        else:\n            self.mod = old_mod\n            if old_attr is None:\n                old_attr = name\n            self.attr = old_attr\n\n    def _resolve(self):\n        module = _import_module(self.mod)\n        return getattr(module, self.attr)\n\n\nclass _SixMetaPathImporter(object):\n\n    \"\"\"\n    A meta path importer to import six.moves and its submodules.\n\n    This class implements a PEP302 finder and loader. It should be compatible\n    with Python 2.5 and all existing versions of Python3\n    \"\"\"\n\n    def __init__(self, six_module_name):\n        self.name = six_module_name\n        self.known_modules = {}\n\n    def _add_module(self, mod, *fullnames):\n        for fullname in fullnames:\n            self.known_modules[self.name + \".\" + fullname] = mod\n\n    def _get_module(self, fullname):\n        return self.known_modules[self.name + \".\" + fullname]\n\n    def find_module(self, fullname, path=None):\n        if fullname in self.known_modules:\n            return self\n        return None\n\n    def find_spec(self, fullname, path, target=None):\n        if fullname in self.known_modules:\n            return spec_from_loader(fullname, self)\n        return None\n\n    def __get_module(self, fullname):\n        try:\n            return self.known_modules[fullname]\n        except KeyError:\n            raise ImportError(\"This loader does not know module \" + fullname)\n\n    def load_module(self, fullname):\n        try:\n            # in case of a reload\n            return sys.modules[fullname]\n        except KeyError:\n            pass\n        mod = self.__get_module(fullname)\n        if isinstance(mod, MovedModule):\n            mod = mod._resolve()\n        else:\n            mod.__loader__ = self\n        sys.modules[fullname] = mod\n        return mod\n\n    def is_package(self, fullname):\n        \"\"\"\n        Return true, if the named module is a package.\n\n        We need this method to get correct spec objects with\n        Python 3.4 (see PEP451)\n        \"\"\"\n        return hasattr(self.__get_module(fullname), \"__path__\")\n\n    def get_code(self, fullname):\n        \"\"\"Return None\n\n        Required, if is_package is implemented\"\"\"\n        self.__get_module(fullname)  # eventually raises ImportError\n        return None\n\n    get_source = get_code  # same as get_code\n\n    def create_module(self, spec):\n        return self.load_module(spec.name)\n\n    def exec_module(self, module):\n        pass\n\n\n_importer = _SixMetaPathImporter(__name__)\n\n\nclass _MovedItems(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects\"\"\"\n\n    __path__ = []  # mark as package\n\n\n_moved_attributes = [\n    MovedAttribute(\"cStringIO\", \"cStringIO\", \"io\", \"StringIO\"),\n    MovedAttribute(\"filter\", \"itertools\", \"builtins\", \"ifilter\", \"filter\"),\n    MovedAttribute(\n        \"filterfalse\", \"itertools\", \"itertools\", \"ifilterfalse\", \"filterfalse\"\n    ),\n    MovedAttribute(\"input\", \"__builtin__\", \"builtins\", \"raw_input\", \"input\"),\n    MovedAttribute(\"intern\", \"__builtin__\", \"sys\"),\n    MovedAttribute(\"map\", \"itertools\", \"builtins\", \"imap\", \"map\"),\n    MovedAttribute(\"getcwd\", \"os\", \"os\", \"getcwdu\", \"getcwd\"),\n    MovedAttribute(\"getcwdb\", \"os\", \"os\", \"getcwd\", \"getcwdb\"),\n    MovedAttribute(\"getoutput\", \"commands\", \"subprocess\"),\n    MovedAttribute(\"range\", \"__builtin__\", \"builtins\", \"xrange\", \"range\"),\n    MovedAttribute(\n        \"reload_module\", \"__builtin__\", \"importlib\" if PY34 else \"imp\", \"reload\"\n    ),\n    MovedAttribute(\"reduce\", \"__builtin__\", \"functools\"),\n    MovedAttribute(\"shlex_quote\", \"pipes\", \"shlex\", \"quote\"),\n    MovedAttribute(\"StringIO\", \"StringIO\", \"io\"),\n    MovedAttribute(\"UserDict\", \"UserDict\", \"collections\"),\n    MovedAttribute(\"UserList\", \"UserList\", \"collections\"),\n    MovedAttribute(\"UserString\", \"UserString\", \"collections\"),\n    MovedAttribute(\"xrange\", \"__builtin__\", \"builtins\", \"xrange\", \"range\"),\n    MovedAttribute(\"zip\", \"itertools\", \"builtins\", \"izip\", \"zip\"),\n    MovedAttribute(\n        \"zip_longest\", \"itertools\", \"itertools\", \"izip_longest\", \"zip_longest\"\n    ),\n    MovedModule(\"builtins\", \"__builtin__\"),\n    MovedModule(\"configparser\", \"ConfigParser\"),\n    MovedModule(\n        \"collections_abc\",\n        \"collections\",\n        \"collections.abc\" if sys.version_info >= (3, 3) else \"collections\",\n    ),\n    MovedModule(\"copyreg\", \"copy_reg\"),\n    MovedModule(\"dbm_gnu\", \"gdbm\", \"dbm.gnu\"),\n    MovedModule(\"dbm_ndbm\", \"dbm\", \"dbm.ndbm\"),\n    MovedModule(\n        \"_dummy_thread\",\n        \"dummy_thread\",\n        \"_dummy_thread\" if sys.version_info < (3, 9) else \"_thread\",\n    ),\n    MovedModule(\"http_cookiejar\", \"cookielib\", \"http.cookiejar\"),\n    MovedModule(\"http_cookies\", \"Cookie\", \"http.cookies\"),\n    MovedModule(\"html_entities\", \"htmlentitydefs\", \"html.entities\"),\n    MovedModule(\"html_parser\", \"HTMLParser\", \"html.parser\"),\n    MovedModule(\"http_client\", \"httplib\", \"http.client\"),\n    MovedModule(\"email_mime_base\", \"email.MIMEBase\", \"email.mime.base\"),\n    MovedModule(\"email_mime_image\", \"email.MIMEImage\", \"email.mime.image\"),\n    MovedModule(\"email_mime_multipart\", \"email.MIMEMultipart\", \"email.mime.multipart\"),\n    MovedModule(\n        \"email_mime_nonmultipart\", \"email.MIMENonMultipart\", \"email.mime.nonmultipart\"\n    ),\n    MovedModule(\"email_mime_text\", \"email.MIMEText\", \"email.mime.text\"),\n    MovedModule(\"BaseHTTPServer\", \"BaseHTTPServer\", \"http.server\"),\n    MovedModule(\"CGIHTTPServer\", \"CGIHTTPServer\", \"http.server\"),\n    MovedModule(\"SimpleHTTPServer\", \"SimpleHTTPServer\", \"http.server\"),\n    MovedModule(\"cPickle\", \"cPickle\", \"pickle\"),\n    MovedModule(\"queue\", \"Queue\"),\n    MovedModule(\"reprlib\", \"repr\"),\n    MovedModule(\"socketserver\", \"SocketServer\"),\n    MovedModule(\"_thread\", \"thread\", \"_thread\"),\n    MovedModule(\"tkinter\", \"Tkinter\"),\n    MovedModule(\"tkinter_dialog\", \"Dialog\", \"tkinter.dialog\"),\n    MovedModule(\"tkinter_filedialog\", \"FileDialog\", \"tkinter.filedialog\"),\n    MovedModule(\"tkinter_scrolledtext\", \"ScrolledText\", \"tkinter.scrolledtext\"),\n    MovedModule(\"tkinter_simpledialog\", \"SimpleDialog\", \"tkinter.simpledialog\"),\n    MovedModule(\"tkinter_tix\", \"Tix\", \"tkinter.tix\"),\n    MovedModule(\"tkinter_ttk\", \"ttk\", \"tkinter.ttk\"),\n    MovedModule(\"tkinter_constants\", \"Tkconstants\", \"tkinter.constants\"),\n    MovedModule(\"tkinter_dnd\", \"Tkdnd\", \"tkinter.dnd\"),\n    MovedModule(\"tkinter_colorchooser\", \"tkColorChooser\", \"tkinter.colorchooser\"),\n    MovedModule(\"tkinter_commondialog\", \"tkCommonDialog\", \"tkinter.commondialog\"),\n    MovedModule(\"tkinter_tkfiledialog\", \"tkFileDialog\", \"tkinter.filedialog\"),\n    MovedModule(\"tkinter_font\", \"tkFont\", \"tkinter.font\"),\n    MovedModule(\"tkinter_messagebox\", \"tkMessageBox\", \"tkinter.messagebox\"),\n    MovedModule(\"tkinter_tksimpledialog\", \"tkSimpleDialog\", \"tkinter.simpledialog\"),\n    MovedModule(\"urllib_parse\", __name__ + \".moves.urllib_parse\", \"urllib.parse\"),\n    MovedModule(\"urllib_error\", __name__ + \".moves.urllib_error\", \"urllib.error\"),\n    MovedModule(\"urllib\", __name__ + \".moves.urllib\", __name__ + \".moves.urllib\"),\n    MovedModule(\"urllib_robotparser\", \"robotparser\", \"urllib.robotparser\"),\n    MovedModule(\"xmlrpc_client\", \"xmlrpclib\", \"xmlrpc.client\"),\n    MovedModule(\"xmlrpc_server\", \"SimpleXMLRPCServer\", \"xmlrpc.server\"),\n]\n# Add windows specific modules.\nif sys.platform == \"win32\":\n    _moved_attributes += [\n        MovedModule(\"winreg\", \"_winreg\"),\n    ]\n\nfor attr in _moved_attributes:\n    setattr(_MovedItems, attr.name, attr)\n    if isinstance(attr, MovedModule):\n        _importer._add_module(attr, \"moves.\" + attr.name)\ndel attr\n\n_MovedItems._moved_attributes = _moved_attributes\n\nmoves = _MovedItems(__name__ + \".moves\")\n_importer._add_module(moves, \"moves\")\n\n\nclass Module_six_moves_urllib_parse(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_parse\"\"\"\n\n\n_urllib_parse_moved_attributes = [\n    MovedAttribute(\"ParseResult\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"SplitResult\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"parse_qs\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"parse_qsl\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urldefrag\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urljoin\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlparse\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlsplit\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlunparse\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"urlunsplit\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"quote\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"quote_plus\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"unquote\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"unquote_plus\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\n        \"unquote_to_bytes\", \"urllib\", \"urllib.parse\", \"unquote\", \"unquote_to_bytes\"\n    ),\n    MovedAttribute(\"urlencode\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splitquery\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splittag\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splituser\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"splitvalue\", \"urllib\", \"urllib.parse\"),\n    MovedAttribute(\"uses_fragment\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_netloc\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_params\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_query\", \"urlparse\", \"urllib.parse\"),\n    MovedAttribute(\"uses_relative\", \"urlparse\", \"urllib.parse\"),\n]\nfor attr in _urllib_parse_moved_attributes:\n    setattr(Module_six_moves_urllib_parse, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes\n\n_importer._add_module(\n    Module_six_moves_urllib_parse(__name__ + \".moves.urllib_parse\"),\n    \"moves.urllib_parse\",\n    \"moves.urllib.parse\",\n)\n\n\nclass Module_six_moves_urllib_error(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_error\"\"\"\n\n\n_urllib_error_moved_attributes = [\n    MovedAttribute(\"URLError\", \"urllib2\", \"urllib.error\"),\n    MovedAttribute(\"HTTPError\", \"urllib2\", \"urllib.error\"),\n    MovedAttribute(\"ContentTooShortError\", \"urllib\", \"urllib.error\"),\n]\nfor attr in _urllib_error_moved_attributes:\n    setattr(Module_six_moves_urllib_error, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes\n\n_importer._add_module(\n    Module_six_moves_urllib_error(__name__ + \".moves.urllib.error\"),\n    \"moves.urllib_error\",\n    \"moves.urllib.error\",\n)\n\n\nclass Module_six_moves_urllib_request(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_request\"\"\"\n\n\n_urllib_request_moved_attributes = [\n    MovedAttribute(\"urlopen\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"install_opener\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"build_opener\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"pathname2url\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"url2pathname\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"getproxies\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"Request\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"OpenerDirector\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPDefaultErrorHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPRedirectHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPCookieProcessor\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"ProxyHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"BaseHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPPasswordMgr\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPPasswordMgrWithDefaultRealm\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"AbstractBasicAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPBasicAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"ProxyBasicAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"AbstractDigestAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPDigestAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"ProxyDigestAuthHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPSHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"FileHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"FTPHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"CacheFTPHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"UnknownHandler\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"HTTPErrorProcessor\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"urlretrieve\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"urlcleanup\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"URLopener\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"FancyURLopener\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"proxy_bypass\", \"urllib\", \"urllib.request\"),\n    MovedAttribute(\"parse_http_list\", \"urllib2\", \"urllib.request\"),\n    MovedAttribute(\"parse_keqv_list\", \"urllib2\", \"urllib.request\"),\n]\nfor attr in _urllib_request_moved_attributes:\n    setattr(Module_six_moves_urllib_request, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes\n\n_importer._add_module(\n    Module_six_moves_urllib_request(__name__ + \".moves.urllib.request\"),\n    \"moves.urllib_request\",\n    \"moves.urllib.request\",\n)\n\n\nclass Module_six_moves_urllib_response(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_response\"\"\"\n\n\n_urllib_response_moved_attributes = [\n    MovedAttribute(\"addbase\", \"urllib\", \"urllib.response\"),\n    MovedAttribute(\"addclosehook\", \"urllib\", \"urllib.response\"),\n    MovedAttribute(\"addinfo\", \"urllib\", \"urllib.response\"),\n    MovedAttribute(\"addinfourl\", \"urllib\", \"urllib.response\"),\n]\nfor attr in _urllib_response_moved_attributes:\n    setattr(Module_six_moves_urllib_response, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes\n\n_importer._add_module(\n    Module_six_moves_urllib_response(__name__ + \".moves.urllib.response\"),\n    \"moves.urllib_response\",\n    \"moves.urllib.response\",\n)\n\n\nclass Module_six_moves_urllib_robotparser(_LazyModule):\n\n    \"\"\"Lazy loading of moved objects in six.moves.urllib_robotparser\"\"\"\n\n\n_urllib_robotparser_moved_attributes = [\n    MovedAttribute(\"RobotFileParser\", \"robotparser\", \"urllib.robotparser\"),\n]\nfor attr in _urllib_robotparser_moved_attributes:\n    setattr(Module_six_moves_urllib_robotparser, attr.name, attr)\ndel attr\n\nModule_six_moves_urllib_robotparser._moved_attributes = (\n    _urllib_robotparser_moved_attributes\n)\n\n_importer._add_module(\n    Module_six_moves_urllib_robotparser(__name__ + \".moves.urllib.robotparser\"),\n    \"moves.urllib_robotparser\",\n    \"moves.urllib.robotparser\",\n)\n\n\nclass Module_six_moves_urllib(types.ModuleType):\n\n    \"\"\"Create a six.moves.urllib namespace that resembles the Python 3 namespace\"\"\"\n\n    __path__ = []  # mark as package\n    parse = _importer._get_module(\"moves.urllib_parse\")\n    error = _importer._get_module(\"moves.urllib_error\")\n    request = _importer._get_module(\"moves.urllib_request\")\n    response = _importer._get_module(\"moves.urllib_response\")\n    robotparser = _importer._get_module(\"moves.urllib_robotparser\")\n\n    def __dir__(self):\n        return [\"parse\", \"error\", \"request\", \"response\", \"robotparser\"]\n\n\n_importer._add_module(\n    Module_six_moves_urllib(__name__ + \".moves.urllib\"), \"moves.urllib\"\n)\n\n\ndef add_move(move):\n    \"\"\"Add an item to six.moves.\"\"\"\n    setattr(_MovedItems, move.name, move)\n\n\ndef remove_move(name):\n    \"\"\"Remove item from six.moves.\"\"\"\n    try:\n        delattr(_MovedItems, name)\n    except AttributeError:\n        try:\n            del moves.__dict__[name]\n        except KeyError:\n            raise AttributeError(\"no such move, %r\" % (name,))\n\n\nif PY3:\n    _meth_func = \"__func__\"\n    _meth_self = \"__self__\"\n\n    _func_closure = \"__closure__\"\n    _func_code = \"__code__\"\n    _func_defaults = \"__defaults__\"\n    _func_globals = \"__globals__\"\nelse:\n    _meth_func = \"im_func\"\n    _meth_self = \"im_self\"\n\n    _func_closure = \"func_closure\"\n    _func_code = \"func_code\"\n    _func_defaults = \"func_defaults\"\n    _func_globals = \"func_globals\"\n\n\ntry:\n    advance_iterator = next\nexcept NameError:\n\n    def advance_iterator(it):\n        return it.next()\n\n\nnext = advance_iterator\n\n\ntry:\n    callable = callable\nexcept NameError:\n\n    def callable(obj):\n        return any(\"__call__\" in klass.__dict__ for klass in type(obj).__mro__)\n\n\nif PY3:\n\n    def get_unbound_function(unbound):\n        return unbound\n\n    create_bound_method = types.MethodType\n\n    def create_unbound_method(func, cls):\n        return func\n\n    Iterator = object\nelse:\n\n    def get_unbound_function(unbound):\n        return unbound.im_func\n\n    def create_bound_method(func, obj):\n        return types.MethodType(func, obj, obj.__class__)\n\n    def create_unbound_method(func, cls):\n        return types.MethodType(func, None, cls)\n\n    class Iterator(object):\n        def next(self):\n            return type(self).__next__(self)\n\n    callable = callable\n_add_doc(\n    get_unbound_function, \"\"\"Get the function out of a possibly unbound function\"\"\"\n)\n\n\nget_method_function = operator.attrgetter(_meth_func)\nget_method_self = operator.attrgetter(_meth_self)\nget_function_closure = operator.attrgetter(_func_closure)\nget_function_code = operator.attrgetter(_func_code)\nget_function_defaults = operator.attrgetter(_func_defaults)\nget_function_globals = operator.attrgetter(_func_globals)\n\n\nif PY3:\n\n    def iterkeys(d, **kw):\n        return iter(d.keys(**kw))\n\n    def itervalues(d, **kw):\n        return iter(d.values(**kw))\n\n    def iteritems(d, **kw):\n        return iter(d.items(**kw))\n\n    def iterlists(d, **kw):\n        return iter(d.lists(**kw))\n\n    viewkeys = operator.methodcaller(\"keys\")\n\n    viewvalues = operator.methodcaller(\"values\")\n\n    viewitems = operator.methodcaller(\"items\")\nelse:\n\n    def iterkeys(d, **kw):\n        return d.iterkeys(**kw)\n\n    def itervalues(d, **kw):\n        return d.itervalues(**kw)\n\n    def iteritems(d, **kw):\n        return d.iteritems(**kw)\n\n    def iterlists(d, **kw):\n        return d.iterlists(**kw)\n\n    viewkeys = operator.methodcaller(\"viewkeys\")\n\n    viewvalues = operator.methodcaller(\"viewvalues\")\n\n    viewitems = operator.methodcaller(\"viewitems\")\n\n_add_doc(iterkeys, \"Return an iterator over the keys of a dictionary.\")\n_add_doc(itervalues, \"Return an iterator over the values of a dictionary.\")\n_add_doc(iteritems, \"Return an iterator over the (key, value) pairs of a dictionary.\")\n_add_doc(\n    iterlists, \"Return an iterator over the (key, [values]) pairs of a dictionary.\"\n)\n\n\nif PY3:\n\n    def b(s):\n        return s.encode(\"latin-1\")\n\n    def u(s):\n        return s\n\n    unichr = chr\n    import struct\n\n    int2byte = struct.Struct(\">B\").pack\n    del struct\n    byte2int = operator.itemgetter(0)\n    indexbytes = operator.getitem\n    iterbytes = iter\n    import io\n\n    StringIO = io.StringIO\n    BytesIO = io.BytesIO\n    del io\n    _assertCountEqual = \"assertCountEqual\"\n    if sys.version_info[1] <= 1:\n        _assertRaisesRegex = \"assertRaisesRegexp\"\n        _assertRegex = \"assertRegexpMatches\"\n        _assertNotRegex = \"assertNotRegexpMatches\"\n    else:\n        _assertRaisesRegex = \"assertRaisesRegex\"\n        _assertRegex = \"assertRegex\"\n        _assertNotRegex = \"assertNotRegex\"\nelse:\n\n    def b(s):\n        return s\n\n    # Workaround for standalone backslash\n\n    def u(s):\n        return unicode(s.replace(r\"\\\\\", r\"\\\\\\\\\"), \"unicode_escape\")\n\n    unichr = unichr\n    int2byte = chr\n\n    def byte2int(bs):\n        return ord(bs[0])\n\n    def indexbytes(buf, i):\n        return ord(buf[i])\n\n    iterbytes = functools.partial(itertools.imap, ord)\n    import StringIO\n\n    StringIO = BytesIO = StringIO.StringIO\n    _assertCountEqual = \"assertItemsEqual\"\n    _assertRaisesRegex = \"assertRaisesRegexp\"\n    _assertRegex = \"assertRegexpMatches\"\n    _assertNotRegex = \"assertNotRegexpMatches\"\n_add_doc(b, \"\"\"Byte literal\"\"\")\n_add_doc(u, \"\"\"Text literal\"\"\")\n\n\ndef assertCountEqual(self, *args, **kwargs):\n    return getattr(self, _assertCountEqual)(*args, **kwargs)\n\n\ndef assertRaisesRegex(self, *args, **kwargs):\n    return getattr(self, _assertRaisesRegex)(*args, **kwargs)\n\n\ndef assertRegex(self, *args, **kwargs):\n    return getattr(self, _assertRegex)(*args, **kwargs)\n\n\ndef assertNotRegex(self, *args, **kwargs):\n    return getattr(self, _assertNotRegex)(*args, **kwargs)\n\n\nif PY3:\n    exec_ = getattr(moves.builtins, \"exec\")\n\n    def reraise(tp, value, tb=None):\n        try:\n            if value is None:\n                value = tp()\n            if value.__traceback__ is not tb:\n                raise value.with_traceback(tb)\n            raise value\n        finally:\n            value = None\n            tb = None\n\n\nelse:\n\n    def exec_(_code_, _globs_=None, _locs_=None):\n        \"\"\"Execute code in a namespace.\"\"\"\n        if _globs_ is None:\n            frame = sys._getframe(1)\n            _globs_ = frame.f_globals\n            if _locs_ is None:\n                _locs_ = frame.f_locals\n            del frame\n        elif _locs_ is None:\n            _locs_ = _globs_\n        exec (\"\"\"exec _code_ in _globs_, _locs_\"\"\")\n\n    exec_(\n        \"\"\"def reraise(tp, value, tb=None):\n    try:\n        raise tp, value, tb\n    finally:\n        tb = None\n\"\"\"\n    )\n\n\nif sys.version_info[:2] > (3,):\n    exec_(\n        \"\"\"def raise_from(value, from_value):\n    try:\n        raise value from from_value\n    finally:\n        value = None\n\"\"\"\n    )\nelse:\n\n    def raise_from(value, from_value):\n        raise value\n\n\nprint_ = getattr(moves.builtins, \"print\", None)\nif print_ is None:\n\n    def print_(*args, **kwargs):\n        \"\"\"The new-style print function for Python 2.4 and 2.5.\"\"\"\n        fp = kwargs.pop(\"file\", sys.stdout)\n        if fp is None:\n            return\n\n        def write(data):\n            if not isinstance(data, basestring):\n                data = str(data)\n            # If the file has an encoding, encode unicode with it.\n            if (\n                isinstance(fp, file)\n                and isinstance(data, unicode)\n                and fp.encoding is not None\n            ):\n                errors = getattr(fp, \"errors\", None)\n                if errors is None:\n                    errors = \"strict\"\n                data = data.encode(fp.encoding, errors)\n            fp.write(data)\n\n        want_unicode = False\n        sep = kwargs.pop(\"sep\", None)\n        if sep is not None:\n            if isinstance(sep, unicode):\n                want_unicode = True\n            elif not isinstance(sep, str):\n                raise TypeError(\"sep must be None or a string\")\n        end = kwargs.pop(\"end\", None)\n        if end is not None:\n            if isinstance(end, unicode):\n                want_unicode = True\n            elif not isinstance(end, str):\n                raise TypeError(\"end must be None or a string\")\n        if kwargs:\n            raise TypeError(\"invalid keyword arguments to print()\")\n        if not want_unicode:\n            for arg in args:\n                if isinstance(arg, unicode):\n                    want_unicode = True\n                    break\n        if want_unicode:\n            newline = unicode(\"\\n\")\n            space = unicode(\" \")\n        else:\n            newline = \"\\n\"\n            space = \" \"\n        if sep is None:\n            sep = space\n        if end is None:\n            end = newline\n        for i, arg in enumerate(args):\n            if i:\n                write(sep)\n            write(arg)\n        write(end)\n\n\nif sys.version_info[:2] < (3, 3):\n    _print = print_\n\n    def print_(*args, **kwargs):\n        fp = kwargs.get(\"file\", sys.stdout)\n        flush = kwargs.pop(\"flush\", False)\n        _print(*args, **kwargs)\n        if flush and fp is not None:\n            fp.flush()\n\n\n_add_doc(reraise, \"\"\"Reraise an exception.\"\"\")\n\nif sys.version_info[0:2] < (3, 4):\n    # This does exactly the same what the :func:`py3:functools.update_wrapper`\n    # function does on Python versions after 3.2. It sets the ``__wrapped__``\n    # attribute on ``wrapper`` object and it doesn't raise an error if any of\n    # the attributes mentioned in ``assigned`` and ``updated`` are missing on\n    # ``wrapped`` object.\n    def _update_wrapper(\n        wrapper,\n        wrapped,\n        assigned=functools.WRAPPER_ASSIGNMENTS,\n        updated=functools.WRAPPER_UPDATES,\n    ):\n        for attr in assigned:\n            try:\n                value = getattr(wrapped, attr)\n            except AttributeError:\n                continue\n            else:\n                setattr(wrapper, attr, value)\n        for attr in updated:\n            getattr(wrapper, attr).update(getattr(wrapped, attr, {}))\n        wrapper.__wrapped__ = wrapped\n        return wrapper\n\n    _update_wrapper.__doc__ = functools.update_wrapper.__doc__\n\n    def wraps(\n        wrapped,\n        assigned=functools.WRAPPER_ASSIGNMENTS,\n        updated=functools.WRAPPER_UPDATES,\n    ):\n        return functools.partial(\n            _update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated\n        )\n\n    wraps.__doc__ = functools.wraps.__doc__\n\nelse:\n    wraps = functools.wraps\n\n\ndef with_metaclass(meta, *bases):\n    \"\"\"Create a base class with a metaclass.\"\"\"\n    # This requires a bit of explanation: the basic idea is to make a dummy\n    # metaclass for one level of class instantiation that replaces itself with\n    # the actual metaclass.\n    class metaclass(type):\n        def __new__(cls, name, this_bases, d):\n            if sys.version_info[:2] >= (3, 7):\n                # This version introduced PEP 560 that requires a bit\n                # of extra care (we mimic what is done by __build_class__).\n                resolved_bases = types.resolve_bases(bases)\n                if resolved_bases is not bases:\n                    d[\"__orig_bases__\"] = bases\n            else:\n                resolved_bases = bases\n            return meta(name, resolved_bases, d)\n\n        @classmethod\n        def __prepare__(cls, name, this_bases):\n            return meta.__prepare__(name, bases)\n\n    return type.__new__(metaclass, \"temporary_class\", (), {})\n\n\ndef add_metaclass(metaclass):\n    \"\"\"Class decorator for creating a class with a metaclass.\"\"\"\n\n    def wrapper(cls):\n        orig_vars = cls.__dict__.copy()\n        slots = orig_vars.get(\"__slots__\")\n        if slots is not None:\n            if isinstance(slots, str):\n                slots = [slots]\n            for slots_var in slots:\n                orig_vars.pop(slots_var)\n        orig_vars.pop(\"__dict__\", None)\n        orig_vars.pop(\"__weakref__\", None)\n        if hasattr(cls, \"__qualname__\"):\n            orig_vars[\"__qualname__\"] = cls.__qualname__\n        return metaclass(cls.__name__, cls.__bases__, orig_vars)\n\n    return wrapper\n\n\ndef ensure_binary(s, encoding=\"utf-8\", errors=\"strict\"):\n    \"\"\"Coerce **s** to six.binary_type.\n\n    For Python 2:\n      - `unicode` -> encoded to `str`\n      - `str` -> `str`\n\n    For Python 3:\n      - `str` -> encoded to `bytes`\n      - `bytes` -> `bytes`\n    \"\"\"\n    if isinstance(s, binary_type):\n        return s\n    if isinstance(s, text_type):\n        return s.encode(encoding, errors)\n    raise TypeError(\"not expecting type '%s'\" % type(s))\n\n\ndef ensure_str(s, encoding=\"utf-8\", errors=\"strict\"):\n    \"\"\"Coerce *s* to `str`.\n\n    For Python 2:\n      - `unicode` -> encoded to `str`\n      - `str` -> `str`\n\n    For Python 3:\n      - `str` -> `str`\n      - `bytes` -> decoded to `str`\n    \"\"\"\n    # Optimization: Fast return for the common case.\n    if type(s) is str:\n        return s\n    if PY2 and isinstance(s, text_type):\n        return s.encode(encoding, errors)\n    elif PY3 and isinstance(s, binary_type):\n        return s.decode(encoding, errors)\n    elif not isinstance(s, (text_type, binary_type)):\n        raise TypeError(\"not expecting type '%s'\" % type(s))\n    return s\n\n\ndef ensure_text(s, encoding=\"utf-8\", errors=\"strict\"):\n    \"\"\"Coerce *s* to six.text_type.\n\n    For Python 2:\n      - `unicode` -> `unicode`\n      - `str` -> `unicode`\n\n    For Python 3:\n      - `str` -> `str`\n      - `bytes` -> decoded to `str`\n    \"\"\"\n    if isinstance(s, binary_type):\n        return s.decode(encoding, errors)\n    elif isinstance(s, text_type):\n        return s\n    else:\n        raise TypeError(\"not expecting type '%s'\" % type(s))\n\n\ndef python_2_unicode_compatible(klass):\n    \"\"\"\n    A class decorator that defines __unicode__ and __str__ methods under Python 2.\n    Under Python 3 it does nothing.\n\n    To support Python 2 and 3 with a single code base, define a __str__ method\n    returning text and apply this decorator to the class.\n    \"\"\"\n    if PY2:\n        if \"__str__\" not in klass.__dict__:\n            raise ValueError(\n                \"@python_2_unicode_compatible cannot be applied \"\n                \"to %s because it doesn't define __str__().\" % klass.__name__\n            )\n        klass.__unicode__ = klass.__str__\n        klass.__str__ = lambda self: self.__unicode__().encode(\"utf-8\")\n    return klass\n\n\n# Complete the moves implementation.\n# This code is at the end of this module to speed up module loading.\n# Turn this module into a package.\n__path__ = []  # required for PEP 302 and PEP 451\n__package__ = __name__  # see PEP 366 @ReservedAssignment\nif globals().get(\"__spec__\") is not None:\n    __spec__.submodule_search_locations = []  # PEP 451 @UndefinedVariable\n# Remove other six meta path importers, since they cause problems. This can\n# happen if six is removed from sys.modules and then reloaded. (Setuptools does\n# this for some reason.)\nif sys.meta_path:\n    for i, importer in enumerate(sys.meta_path):\n        # Here's some real nastiness: Another \"instance\" of the six module might\n        # be floating around. Therefore, we can't use isinstance() to check for\n        # the six meta path importer, since the other six instance will have\n        # inserted an importer with different class.\n        if (\n            type(importer).__name__ == \"_SixMetaPathImporter\"\n            and importer.name == __name__\n        ):\n            del sys.meta_path[i]\n            break\n    del i, importer\n# Finally, add the importer to the meta path import hook.\nsys.meta_path.append(_importer)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/poolmanager.py",
    "content": "from __future__ import absolute_import\n\nimport collections\nimport functools\nimport logging\n\nfrom ._collections import RecentlyUsedContainer\nfrom .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme\nfrom .exceptions import (\n    LocationValueError,\n    MaxRetryError,\n    ProxySchemeUnknown,\n    ProxySchemeUnsupported,\n    URLSchemeUnknown,\n)\nfrom .packages import six\nfrom .packages.six.moves.urllib.parse import urljoin\nfrom .request import RequestMethods\nfrom .util.proxy import connection_requires_http_tunnel\nfrom .util.retry import Retry\nfrom .util.url import parse_url\n\n__all__ = [\"PoolManager\", \"ProxyManager\", \"proxy_from_url\"]\n\n\nlog = logging.getLogger(__name__)\n\nSSL_KEYWORDS = (\n    \"key_file\",\n    \"cert_file\",\n    \"cert_reqs\",\n    \"ca_certs\",\n    \"ssl_version\",\n    \"ca_cert_dir\",\n    \"ssl_context\",\n    \"key_password\",\n    \"server_hostname\",\n)\n\n# All known keyword arguments that could be provided to the pool manager, its\n# pools, or the underlying connections. This is used to construct a pool key.\n_key_fields = (\n    \"key_scheme\",  # str\n    \"key_host\",  # str\n    \"key_port\",  # int\n    \"key_timeout\",  # int or float or Timeout\n    \"key_retries\",  # int or Retry\n    \"key_strict\",  # bool\n    \"key_block\",  # bool\n    \"key_source_address\",  # str\n    \"key_key_file\",  # str\n    \"key_key_password\",  # str\n    \"key_cert_file\",  # str\n    \"key_cert_reqs\",  # str\n    \"key_ca_certs\",  # str\n    \"key_ssl_version\",  # str\n    \"key_ca_cert_dir\",  # str\n    \"key_ssl_context\",  # instance of ssl.SSLContext or urllib3.util.ssl_.SSLContext\n    \"key_maxsize\",  # int\n    \"key_headers\",  # dict\n    \"key__proxy\",  # parsed proxy url\n    \"key__proxy_headers\",  # dict\n    \"key__proxy_config\",  # class\n    \"key_socket_options\",  # list of (level (int), optname (int), value (int or str)) tuples\n    \"key__socks_options\",  # dict\n    \"key_assert_hostname\",  # bool or string\n    \"key_assert_fingerprint\",  # str\n    \"key_server_hostname\",  # str\n)\n\n#: The namedtuple class used to construct keys for the connection pool.\n#: All custom key schemes should include the fields in this key at a minimum.\nPoolKey = collections.namedtuple(\"PoolKey\", _key_fields)\n\n_proxy_config_fields = (\"ssl_context\", \"use_forwarding_for_https\")\nProxyConfig = collections.namedtuple(\"ProxyConfig\", _proxy_config_fields)\n\n\ndef _default_key_normalizer(key_class, request_context):\n    \"\"\"\n    Create a pool key out of a request context dictionary.\n\n    According to RFC 3986, both the scheme and host are case-insensitive.\n    Therefore, this function normalizes both before constructing the pool\n    key for an HTTPS request. If you wish to change this behaviour, provide\n    alternate callables to ``key_fn_by_scheme``.\n\n    :param key_class:\n        The class to use when constructing the key. This should be a namedtuple\n        with the ``scheme`` and ``host`` keys at a minimum.\n    :type  key_class: namedtuple\n    :param request_context:\n        A dictionary-like object that contain the context for a request.\n    :type  request_context: dict\n\n    :return: A namedtuple that can be used as a connection pool key.\n    :rtype:  PoolKey\n    \"\"\"\n    # Since we mutate the dictionary, make a copy first\n    context = request_context.copy()\n    context[\"scheme\"] = context[\"scheme\"].lower()\n    context[\"host\"] = context[\"host\"].lower()\n\n    # These are both dictionaries and need to be transformed into frozensets\n    for key in (\"headers\", \"_proxy_headers\", \"_socks_options\"):\n        if key in context and context[key] is not None:\n            context[key] = frozenset(context[key].items())\n\n    # The socket_options key may be a list and needs to be transformed into a\n    # tuple.\n    socket_opts = context.get(\"socket_options\")\n    if socket_opts is not None:\n        context[\"socket_options\"] = tuple(socket_opts)\n\n    # Map the kwargs to the names in the namedtuple - this is necessary since\n    # namedtuples can't have fields starting with '_'.\n    for key in list(context.keys()):\n        context[\"key_\" + key] = context.pop(key)\n\n    # Default to ``None`` for keys missing from the context\n    for field in key_class._fields:\n        if field not in context:\n            context[field] = None\n\n    return key_class(**context)\n\n\n#: A dictionary that maps a scheme to a callable that creates a pool key.\n#: This can be used to alter the way pool keys are constructed, if desired.\n#: Each PoolManager makes a copy of this dictionary so they can be configured\n#: globally here, or individually on the instance.\nkey_fn_by_scheme = {\n    \"http\": functools.partial(_default_key_normalizer, PoolKey),\n    \"https\": functools.partial(_default_key_normalizer, PoolKey),\n}\n\npool_classes_by_scheme = {\"http\": HTTPConnectionPool, \"https\": HTTPSConnectionPool}\n\n\nclass PoolManager(RequestMethods):\n    \"\"\"\n    Allows for arbitrary requests while transparently keeping track of\n    necessary connection pools for you.\n\n    :param num_pools:\n        Number of connection pools to cache before discarding the least\n        recently used pool.\n\n    :param headers:\n        Headers to include with all requests, unless other headers are given\n        explicitly.\n\n    :param \\\\**connection_pool_kw:\n        Additional parameters are used to create fresh\n        :class:`urllib3.connectionpool.ConnectionPool` instances.\n\n    Example::\n\n        >>> manager = PoolManager(num_pools=2)\n        >>> r = manager.request('GET', 'http://google.com/')\n        >>> r = manager.request('GET', 'http://google.com/mail')\n        >>> r = manager.request('GET', 'http://yahoo.com/')\n        >>> len(manager.pools)\n        2\n\n    \"\"\"\n\n    proxy = None\n    proxy_config = None\n\n    def __init__(self, num_pools=10, headers=None, **connection_pool_kw):\n        RequestMethods.__init__(self, headers)\n        self.connection_pool_kw = connection_pool_kw\n        self.pools = RecentlyUsedContainer(num_pools, dispose_func=lambda p: p.close())\n\n        # Locally set the pool classes and keys so other PoolManagers can\n        # override them.\n        self.pool_classes_by_scheme = pool_classes_by_scheme\n        self.key_fn_by_scheme = key_fn_by_scheme.copy()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.clear()\n        # Return False to re-raise any potential exceptions\n        return False\n\n    def _new_pool(self, scheme, host, port, request_context=None):\n        \"\"\"\n        Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and\n        any additional pool keyword arguments.\n\n        If ``request_context`` is provided, it is provided as keyword arguments\n        to the pool class used. This method is used to actually create the\n        connection pools handed out by :meth:`connection_from_url` and\n        companion methods. It is intended to be overridden for customization.\n        \"\"\"\n        pool_cls = self.pool_classes_by_scheme[scheme]\n        if request_context is None:\n            request_context = self.connection_pool_kw.copy()\n\n        # Although the context has everything necessary to create the pool,\n        # this function has historically only used the scheme, host, and port\n        # in the positional args. When an API change is acceptable these can\n        # be removed.\n        for key in (\"scheme\", \"host\", \"port\"):\n            request_context.pop(key, None)\n\n        if scheme == \"http\":\n            for kw in SSL_KEYWORDS:\n                request_context.pop(kw, None)\n\n        return pool_cls(host, port, **request_context)\n\n    def clear(self):\n        \"\"\"\n        Empty our store of pools and direct them all to close.\n\n        This will not affect in-flight connections, but they will not be\n        re-used after completion.\n        \"\"\"\n        self.pools.clear()\n\n    def connection_from_host(self, host, port=None, scheme=\"http\", pool_kwargs=None):\n        \"\"\"\n        Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme.\n\n        If ``port`` isn't given, it will be derived from the ``scheme`` using\n        ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is\n        provided, it is merged with the instance's ``connection_pool_kw``\n        variable and used to create the new connection pool, if one is\n        needed.\n        \"\"\"\n\n        if not host:\n            raise LocationValueError(\"No host specified.\")\n\n        request_context = self._merge_pool_kwargs(pool_kwargs)\n        request_context[\"scheme\"] = scheme or \"http\"\n        if not port:\n            port = port_by_scheme.get(request_context[\"scheme\"].lower(), 80)\n        request_context[\"port\"] = port\n        request_context[\"host\"] = host\n\n        return self.connection_from_context(request_context)\n\n    def connection_from_context(self, request_context):\n        \"\"\"\n        Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context.\n\n        ``request_context`` must at least contain the ``scheme`` key and its\n        value must be a key in ``key_fn_by_scheme`` instance variable.\n        \"\"\"\n        scheme = request_context[\"scheme\"].lower()\n        pool_key_constructor = self.key_fn_by_scheme.get(scheme)\n        if not pool_key_constructor:\n            raise URLSchemeUnknown(scheme)\n        pool_key = pool_key_constructor(request_context)\n\n        return self.connection_from_pool_key(pool_key, request_context=request_context)\n\n    def connection_from_pool_key(self, pool_key, request_context=None):\n        \"\"\"\n        Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key.\n\n        ``pool_key`` should be a namedtuple that only contains immutable\n        objects. At a minimum it must have the ``scheme``, ``host``, and\n        ``port`` fields.\n        \"\"\"\n        with self.pools.lock:\n            # If the scheme, host, or port doesn't match existing open\n            # connections, open a new ConnectionPool.\n            pool = self.pools.get(pool_key)\n            if pool:\n                return pool\n\n            # Make a fresh ConnectionPool of the desired type\n            scheme = request_context[\"scheme\"]\n            host = request_context[\"host\"]\n            port = request_context[\"port\"]\n            pool = self._new_pool(scheme, host, port, request_context=request_context)\n            self.pools[pool_key] = pool\n\n        return pool\n\n    def connection_from_url(self, url, pool_kwargs=None):\n        \"\"\"\n        Similar to :func:`urllib3.connectionpool.connection_from_url`.\n\n        If ``pool_kwargs`` is not provided and a new pool needs to be\n        constructed, ``self.connection_pool_kw`` is used to initialize\n        the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs``\n        is provided, it is used instead. Note that if a new pool does not\n        need to be created for the request, the provided ``pool_kwargs`` are\n        not used.\n        \"\"\"\n        u = parse_url(url)\n        return self.connection_from_host(\n            u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs\n        )\n\n    def _merge_pool_kwargs(self, override):\n        \"\"\"\n        Merge a dictionary of override values for self.connection_pool_kw.\n\n        This does not modify self.connection_pool_kw and returns a new dict.\n        Any keys in the override dictionary with a value of ``None`` are\n        removed from the merged dictionary.\n        \"\"\"\n        base_pool_kwargs = self.connection_pool_kw.copy()\n        if override:\n            for key, value in override.items():\n                if value is None:\n                    try:\n                        del base_pool_kwargs[key]\n                    except KeyError:\n                        pass\n                else:\n                    base_pool_kwargs[key] = value\n        return base_pool_kwargs\n\n    def _proxy_requires_url_absolute_form(self, parsed_url):\n        \"\"\"\n        Indicates if the proxy requires the complete destination URL in the\n        request.  Normally this is only needed when not using an HTTP CONNECT\n        tunnel.\n        \"\"\"\n        if self.proxy is None:\n            return False\n\n        return not connection_requires_http_tunnel(\n            self.proxy, self.proxy_config, parsed_url.scheme\n        )\n\n    def _validate_proxy_scheme_url_selection(self, url_scheme):\n        \"\"\"\n        Validates that were not attempting to do TLS in TLS connections on\n        Python2 or with unsupported SSL implementations.\n        \"\"\"\n        if self.proxy is None or url_scheme != \"https\":\n            return\n\n        if self.proxy.scheme != \"https\":\n            return\n\n        if six.PY2 and not self.proxy_config.use_forwarding_for_https:\n            raise ProxySchemeUnsupported(\n                \"Contacting HTTPS destinations through HTTPS proxies \"\n                \"'via CONNECT tunnels' is not supported in Python 2\"\n            )\n\n    def urlopen(self, method, url, redirect=True, **kw):\n        \"\"\"\n        Same as :meth:`urllib3.HTTPConnectionPool.urlopen`\n        with custom cross-host redirect logic and only sends the request-uri\n        portion of the ``url``.\n\n        The given ``url`` parameter must be absolute, such that an appropriate\n        :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it.\n        \"\"\"\n        u = parse_url(url)\n        self._validate_proxy_scheme_url_selection(u.scheme)\n\n        conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)\n\n        kw[\"assert_same_host\"] = False\n        kw[\"redirect\"] = False\n\n        if \"headers\" not in kw:\n            kw[\"headers\"] = self.headers.copy()\n\n        if self._proxy_requires_url_absolute_form(u):\n            response = conn.urlopen(method, url, **kw)\n        else:\n            response = conn.urlopen(method, u.request_uri, **kw)\n\n        redirect_location = redirect and response.get_redirect_location()\n        if not redirect_location:\n            return response\n\n        # Support relative URLs for redirecting.\n        redirect_location = urljoin(url, redirect_location)\n\n        # RFC 7231, Section 6.4.4\n        if response.status == 303:\n            method = \"GET\"\n\n        retries = kw.get(\"retries\")\n        if not isinstance(retries, Retry):\n            retries = Retry.from_int(retries, redirect=redirect)\n\n        # Strip headers marked as unsafe to forward to the redirected location.\n        # Check remove_headers_on_redirect to avoid a potential network call within\n        # conn.is_same_host() which may use socket.gethostbyname() in the future.\n        if retries.remove_headers_on_redirect and not conn.is_same_host(\n            redirect_location\n        ):\n            headers = list(six.iterkeys(kw[\"headers\"]))\n            for header in headers:\n                if header.lower() in retries.remove_headers_on_redirect:\n                    kw[\"headers\"].pop(header, None)\n\n        try:\n            retries = retries.increment(method, url, response=response, _pool=conn)\n        except MaxRetryError:\n            if retries.raise_on_redirect:\n                response.drain_conn()\n                raise\n            return response\n\n        kw[\"retries\"] = retries\n        kw[\"redirect\"] = redirect\n\n        log.info(\"Redirecting %s -> %s\", url, redirect_location)\n\n        response.drain_conn()\n        return self.urlopen(method, redirect_location, **kw)\n\n\nclass ProxyManager(PoolManager):\n    \"\"\"\n    Behaves just like :class:`PoolManager`, but sends all requests through\n    the defined proxy, using the CONNECT method for HTTPS URLs.\n\n    :param proxy_url:\n        The URL of the proxy to be used.\n\n    :param proxy_headers:\n        A dictionary containing headers that will be sent to the proxy. In case\n        of HTTP they are being sent with each request, while in the\n        HTTPS/CONNECT case they are sent only once. Could be used for proxy\n        authentication.\n\n    :param proxy_ssl_context:\n        The proxy SSL context is used to establish the TLS connection to the\n        proxy when using HTTPS proxies.\n\n    :param use_forwarding_for_https:\n        (Defaults to False) If set to True will forward requests to the HTTPS\n        proxy to be made on behalf of the client instead of creating a TLS\n        tunnel via the CONNECT method. **Enabling this flag means that request\n        and response headers and content will be visible from the HTTPS proxy**\n        whereas tunneling keeps request and response headers and content\n        private.  IP address, target hostname, SNI, and port are always visible\n        to an HTTPS proxy even when this flag is disabled.\n\n    Example:\n        >>> proxy = urllib3.ProxyManager('http://localhost:3128/')\n        >>> r1 = proxy.request('GET', 'http://google.com/')\n        >>> r2 = proxy.request('GET', 'http://httpbin.org/')\n        >>> len(proxy.pools)\n        1\n        >>> r3 = proxy.request('GET', 'https://httpbin.org/')\n        >>> r4 = proxy.request('GET', 'https://twitter.com/')\n        >>> len(proxy.pools)\n        3\n\n    \"\"\"\n\n    def __init__(\n        self,\n        proxy_url,\n        num_pools=10,\n        headers=None,\n        proxy_headers=None,\n        proxy_ssl_context=None,\n        use_forwarding_for_https=False,\n        **connection_pool_kw\n    ):\n\n        if isinstance(proxy_url, HTTPConnectionPool):\n            proxy_url = \"%s://%s:%i\" % (\n                proxy_url.scheme,\n                proxy_url.host,\n                proxy_url.port,\n            )\n        proxy = parse_url(proxy_url)\n\n        if proxy.scheme not in (\"http\", \"https\"):\n            raise ProxySchemeUnknown(proxy.scheme)\n\n        if not proxy.port:\n            port = port_by_scheme.get(proxy.scheme, 80)\n            proxy = proxy._replace(port=port)\n\n        self.proxy = proxy\n        self.proxy_headers = proxy_headers or {}\n        self.proxy_ssl_context = proxy_ssl_context\n        self.proxy_config = ProxyConfig(proxy_ssl_context, use_forwarding_for_https)\n\n        connection_pool_kw[\"_proxy\"] = self.proxy\n        connection_pool_kw[\"_proxy_headers\"] = self.proxy_headers\n        connection_pool_kw[\"_proxy_config\"] = self.proxy_config\n\n        super(ProxyManager, self).__init__(num_pools, headers, **connection_pool_kw)\n\n    def connection_from_host(self, host, port=None, scheme=\"http\", pool_kwargs=None):\n        if scheme == \"https\":\n            return super(ProxyManager, self).connection_from_host(\n                host, port, scheme, pool_kwargs=pool_kwargs\n            )\n\n        return super(ProxyManager, self).connection_from_host(\n            self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs\n        )\n\n    def _set_proxy_headers(self, url, headers=None):\n        \"\"\"\n        Sets headers needed by proxies: specifically, the Accept and Host\n        headers. Only sets headers not provided by the user.\n        \"\"\"\n        headers_ = {\"Accept\": \"*/*\"}\n\n        netloc = parse_url(url).netloc\n        if netloc:\n            headers_[\"Host\"] = netloc\n\n        if headers:\n            headers_.update(headers)\n        return headers_\n\n    def urlopen(self, method, url, redirect=True, **kw):\n        \"Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute.\"\n        u = parse_url(url)\n        if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme):\n            # For connections using HTTP CONNECT, httplib sets the necessary\n            # headers on the CONNECT to the proxy. If we're not using CONNECT,\n            # we'll definitely need to set 'Host' at the very least.\n            headers = kw.get(\"headers\", self.headers)\n            kw[\"headers\"] = self._set_proxy_headers(url, headers)\n\n        return super(ProxyManager, self).urlopen(method, url, redirect=redirect, **kw)\n\n\ndef proxy_from_url(url, **kw):\n    return ProxyManager(proxy_url=url, **kw)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/request.py",
    "content": "from __future__ import absolute_import\n\nfrom .filepost import encode_multipart_formdata\nfrom .packages.six.moves.urllib.parse import urlencode\n\n__all__ = [\"RequestMethods\"]\n\n\nclass RequestMethods(object):\n    \"\"\"\n    Convenience mixin for classes who implement a :meth:`urlopen` method, such\n    as :class:`urllib3.HTTPConnectionPool` and\n    :class:`urllib3.PoolManager`.\n\n    Provides behavior for making common types of HTTP request methods and\n    decides which type of request field encoding to use.\n\n    Specifically,\n\n    :meth:`.request_encode_url` is for sending requests whose fields are\n    encoded in the URL (such as GET, HEAD, DELETE).\n\n    :meth:`.request_encode_body` is for sending requests whose fields are\n    encoded in the *body* of the request using multipart or www-form-urlencoded\n    (such as for POST, PUT, PATCH).\n\n    :meth:`.request` is for making any kind of request, it will look up the\n    appropriate encoding format and use one of the above two methods to make\n    the request.\n\n    Initializer parameters:\n\n    :param headers:\n        Headers to include with all requests, unless other headers are given\n        explicitly.\n    \"\"\"\n\n    _encode_url_methods = {\"DELETE\", \"GET\", \"HEAD\", \"OPTIONS\"}\n\n    def __init__(self, headers=None):\n        self.headers = headers or {}\n\n    def urlopen(\n        self,\n        method,\n        url,\n        body=None,\n        headers=None,\n        encode_multipart=True,\n        multipart_boundary=None,\n        **kw\n    ):  # Abstract\n        raise NotImplementedError(\n            \"Classes extending RequestMethods must implement \"\n            \"their own ``urlopen`` method.\"\n        )\n\n    def request(self, method, url, fields=None, headers=None, **urlopen_kw):\n        \"\"\"\n        Make a request using :meth:`urlopen` with the appropriate encoding of\n        ``fields`` based on the ``method`` used.\n\n        This is a convenience method that requires the least amount of manual\n        effort. It can be used in most situations, while still having the\n        option to drop down to more specific methods when necessary, such as\n        :meth:`request_encode_url`, :meth:`request_encode_body`,\n        or even the lowest level :meth:`urlopen`.\n        \"\"\"\n        method = method.upper()\n\n        urlopen_kw[\"request_url\"] = url\n\n        if method in self._encode_url_methods:\n            return self.request_encode_url(\n                method, url, fields=fields, headers=headers, **urlopen_kw\n            )\n        else:\n            return self.request_encode_body(\n                method, url, fields=fields, headers=headers, **urlopen_kw\n            )\n\n    def request_encode_url(self, method, url, fields=None, headers=None, **urlopen_kw):\n        \"\"\"\n        Make a request using :meth:`urlopen` with the ``fields`` encoded in\n        the url. This is useful for request methods like GET, HEAD, DELETE, etc.\n        \"\"\"\n        if headers is None:\n            headers = self.headers\n\n        extra_kw = {\"headers\": headers}\n        extra_kw.update(urlopen_kw)\n\n        if fields:\n            url += \"?\" + urlencode(fields)\n\n        return self.urlopen(method, url, **extra_kw)\n\n    def request_encode_body(\n        self,\n        method,\n        url,\n        fields=None,\n        headers=None,\n        encode_multipart=True,\n        multipart_boundary=None,\n        **urlopen_kw\n    ):\n        \"\"\"\n        Make a request using :meth:`urlopen` with the ``fields`` encoded in\n        the body. This is useful for request methods like POST, PUT, PATCH, etc.\n\n        When ``encode_multipart=True`` (default), then\n        :func:`urllib3.encode_multipart_formdata` is used to encode\n        the payload with the appropriate content type. Otherwise\n        :func:`urllib.parse.urlencode` is used with the\n        'application/x-www-form-urlencoded' content type.\n\n        Multipart encoding must be used when posting files, and it's reasonably\n        safe to use it in other times too. However, it may break request\n        signing, such as with OAuth.\n\n        Supports an optional ``fields`` parameter of key/value strings AND\n        key/filetuple. A filetuple is a (filename, data, MIME type) tuple where\n        the MIME type is optional. For example::\n\n            fields = {\n                'foo': 'bar',\n                'fakefile': ('foofile.txt', 'contents of foofile'),\n                'realfile': ('barfile.txt', open('realfile').read()),\n                'typedfile': ('bazfile.bin', open('bazfile').read(),\n                              'image/jpeg'),\n                'nonamefile': 'contents of nonamefile field',\n            }\n\n        When uploading a file, providing a filename (the first parameter of the\n        tuple) is optional but recommended to best mimic behavior of browsers.\n\n        Note that if ``headers`` are supplied, the 'Content-Type' header will\n        be overwritten because it depends on the dynamic random boundary string\n        which is used to compose the body of the request. The random boundary\n        string can be explicitly set with the ``multipart_boundary`` parameter.\n        \"\"\"\n        if headers is None:\n            headers = self.headers\n\n        extra_kw = {\"headers\": {}}\n\n        if fields:\n            if \"body\" in urlopen_kw:\n                raise TypeError(\n                    \"request got values for both 'fields' and 'body', can only specify one.\"\n                )\n\n            if encode_multipart:\n                body, content_type = encode_multipart_formdata(\n                    fields, boundary=multipart_boundary\n                )\n            else:\n                body, content_type = (\n                    urlencode(fields),\n                    \"application/x-www-form-urlencoded\",\n                )\n\n            extra_kw[\"body\"] = body\n            extra_kw[\"headers\"] = {\"Content-Type\": content_type}\n\n        extra_kw[\"headers\"].update(headers)\n        extra_kw.update(urlopen_kw)\n\n        return self.urlopen(method, url, **extra_kw)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/response.py",
    "content": "from __future__ import absolute_import\n\nimport io\nimport logging\nimport zlib\nfrom contextlib import contextmanager\nfrom socket import error as SocketError\nfrom socket import timeout as SocketTimeout\n\ntry:\n    try:\n        import brotlicffi as brotli\n    except ImportError:\n        import brotli\nexcept ImportError:\n    brotli = None\n\nfrom ._collections import HTTPHeaderDict\nfrom .connection import BaseSSLError, HTTPException\nfrom .exceptions import (\n    BodyNotHttplibCompatible,\n    DecodeError,\n    HTTPError,\n    IncompleteRead,\n    InvalidChunkLength,\n    InvalidHeader,\n    ProtocolError,\n    ReadTimeoutError,\n    ResponseNotChunked,\n    SSLError,\n)\nfrom .packages import six\nfrom .util.response import is_fp_closed, is_response_to_head\n\nlog = logging.getLogger(__name__)\n\n\nclass DeflateDecoder(object):\n    def __init__(self):\n        self._first_try = True\n        self._data = b\"\"\n        self._obj = zlib.decompressobj()\n\n    def __getattr__(self, name):\n        return getattr(self._obj, name)\n\n    def decompress(self, data):\n        if not data:\n            return data\n\n        if not self._first_try:\n            return self._obj.decompress(data)\n\n        self._data += data\n        try:\n            decompressed = self._obj.decompress(data)\n            if decompressed:\n                self._first_try = False\n                self._data = None\n            return decompressed\n        except zlib.error:\n            self._first_try = False\n            self._obj = zlib.decompressobj(-zlib.MAX_WBITS)\n            try:\n                return self.decompress(self._data)\n            finally:\n                self._data = None\n\n\nclass GzipDecoderState(object):\n\n    FIRST_MEMBER = 0\n    OTHER_MEMBERS = 1\n    SWALLOW_DATA = 2\n\n\nclass GzipDecoder(object):\n    def __init__(self):\n        self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)\n        self._state = GzipDecoderState.FIRST_MEMBER\n\n    def __getattr__(self, name):\n        return getattr(self._obj, name)\n\n    def decompress(self, data):\n        ret = bytearray()\n        if self._state == GzipDecoderState.SWALLOW_DATA or not data:\n            return bytes(ret)\n        while True:\n            try:\n                ret += self._obj.decompress(data)\n            except zlib.error:\n                previous_state = self._state\n                # Ignore data after the first error\n                self._state = GzipDecoderState.SWALLOW_DATA\n                if previous_state == GzipDecoderState.OTHER_MEMBERS:\n                    # Allow trailing garbage acceptable in other gzip clients\n                    return bytes(ret)\n                raise\n            data = self._obj.unused_data\n            if not data:\n                return bytes(ret)\n            self._state = GzipDecoderState.OTHER_MEMBERS\n            self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)\n\n\nif brotli is not None:\n\n    class BrotliDecoder(object):\n        # Supports both 'brotlipy' and 'Brotli' packages\n        # since they share an import name. The top branches\n        # are for 'brotlipy' and bottom branches for 'Brotli'\n        def __init__(self):\n            self._obj = brotli.Decompressor()\n            if hasattr(self._obj, \"decompress\"):\n                self.decompress = self._obj.decompress\n            else:\n                self.decompress = self._obj.process\n\n        def flush(self):\n            if hasattr(self._obj, \"flush\"):\n                return self._obj.flush()\n            return b\"\"\n\n\nclass MultiDecoder(object):\n    \"\"\"\n    From RFC7231:\n        If one or more encodings have been applied to a representation, the\n        sender that applied the encodings MUST generate a Content-Encoding\n        header field that lists the content codings in the order in which\n        they were applied.\n    \"\"\"\n\n    def __init__(self, modes):\n        self._decoders = [_get_decoder(m.strip()) for m in modes.split(\",\")]\n\n    def flush(self):\n        return self._decoders[0].flush()\n\n    def decompress(self, data):\n        for d in reversed(self._decoders):\n            data = d.decompress(data)\n        return data\n\n\ndef _get_decoder(mode):\n    if \",\" in mode:\n        return MultiDecoder(mode)\n\n    if mode == \"gzip\":\n        return GzipDecoder()\n\n    if brotli is not None and mode == \"br\":\n        return BrotliDecoder()\n\n    return DeflateDecoder()\n\n\nclass HTTPResponse(io.IOBase):\n    \"\"\"\n    HTTP Response container.\n\n    Backwards-compatible with :class:`http.client.HTTPResponse` but the response ``body`` is\n    loaded and decoded on-demand when the ``data`` property is accessed.  This\n    class is also compatible with the Python standard library's :mod:`io`\n    module, and can hence be treated as a readable object in the context of that\n    framework.\n\n    Extra parameters for behaviour not present in :class:`http.client.HTTPResponse`:\n\n    :param preload_content:\n        If True, the response's body will be preloaded during construction.\n\n    :param decode_content:\n        If True, will attempt to decode the body based on the\n        'content-encoding' header.\n\n    :param original_response:\n        When this HTTPResponse wrapper is generated from an :class:`http.client.HTTPResponse`\n        object, it's convenient to include the original for debug purposes. It's\n        otherwise unused.\n\n    :param retries:\n        The retries contains the last :class:`~urllib3.util.retry.Retry` that\n        was used during the request.\n\n    :param enforce_content_length:\n        Enforce content length checking. Body returned by server must match\n        value of Content-Length header, if present. Otherwise, raise error.\n    \"\"\"\n\n    CONTENT_DECODERS = [\"gzip\", \"deflate\"]\n    if brotli is not None:\n        CONTENT_DECODERS += [\"br\"]\n    REDIRECT_STATUSES = [301, 302, 303, 307, 308]\n\n    def __init__(\n        self,\n        body=\"\",\n        headers=None,\n        status=0,\n        version=0,\n        reason=None,\n        strict=0,\n        preload_content=True,\n        decode_content=True,\n        original_response=None,\n        pool=None,\n        connection=None,\n        msg=None,\n        retries=None,\n        enforce_content_length=False,\n        request_method=None,\n        request_url=None,\n        auto_close=True,\n    ):\n\n        if isinstance(headers, HTTPHeaderDict):\n            self.headers = headers\n        else:\n            self.headers = HTTPHeaderDict(headers)\n        self.status = status\n        self.version = version\n        self.reason = reason\n        self.strict = strict\n        self.decode_content = decode_content\n        self.retries = retries\n        self.enforce_content_length = enforce_content_length\n        self.auto_close = auto_close\n\n        self._decoder = None\n        self._body = None\n        self._fp = None\n        self._original_response = original_response\n        self._fp_bytes_read = 0\n        self.msg = msg\n        self._request_url = request_url\n\n        if body and isinstance(body, (six.string_types, bytes)):\n            self._body = body\n\n        self._pool = pool\n        self._connection = connection\n\n        if hasattr(body, \"read\"):\n            self._fp = body\n\n        # Are we using the chunked-style of transfer encoding?\n        self.chunked = False\n        self.chunk_left = None\n        tr_enc = self.headers.get(\"transfer-encoding\", \"\").lower()\n        # Don't incur the penalty of creating a list and then discarding it\n        encodings = (enc.strip() for enc in tr_enc.split(\",\"))\n        if \"chunked\" in encodings:\n            self.chunked = True\n\n        # Determine length of response\n        self.length_remaining = self._init_length(request_method)\n\n        # If requested, preload the body.\n        if preload_content and not self._body:\n            self._body = self.read(decode_content=decode_content)\n\n    def get_redirect_location(self):\n        \"\"\"\n        Should we redirect and where to?\n\n        :returns: Truthy redirect location string if we got a redirect status\n            code and valid location. ``None`` if redirect status and no\n            location. ``False`` if not a redirect status code.\n        \"\"\"\n        if self.status in self.REDIRECT_STATUSES:\n            return self.headers.get(\"location\")\n\n        return False\n\n    def release_conn(self):\n        if not self._pool or not self._connection:\n            return\n\n        self._pool._put_conn(self._connection)\n        self._connection = None\n\n    def drain_conn(self):\n        \"\"\"\n        Read and discard any remaining HTTP response data in the response connection.\n\n        Unread data in the HTTPResponse connection blocks the connection from being released back to the pool.\n        \"\"\"\n        try:\n            self.read()\n        except (HTTPError, SocketError, BaseSSLError, HTTPException):\n            pass\n\n    @property\n    def data(self):\n        # For backwards-compat with earlier urllib3 0.4 and earlier.\n        if self._body:\n            return self._body\n\n        if self._fp:\n            return self.read(cache_content=True)\n\n    @property\n    def connection(self):\n        return self._connection\n\n    def isclosed(self):\n        return is_fp_closed(self._fp)\n\n    def tell(self):\n        \"\"\"\n        Obtain the number of bytes pulled over the wire so far. May differ from\n        the amount of content returned by :meth:``urllib3.response.HTTPResponse.read``\n        if bytes are encoded on the wire (e.g, compressed).\n        \"\"\"\n        return self._fp_bytes_read\n\n    def _init_length(self, request_method):\n        \"\"\"\n        Set initial length value for Response content if available.\n        \"\"\"\n        length = self.headers.get(\"content-length\")\n\n        if length is not None:\n            if self.chunked:\n                # This Response will fail with an IncompleteRead if it can't be\n                # received as chunked. This method falls back to attempt reading\n                # the response before raising an exception.\n                log.warning(\n                    \"Received response with both Content-Length and \"\n                    \"Transfer-Encoding set. This is expressly forbidden \"\n                    \"by RFC 7230 sec 3.3.2. Ignoring Content-Length and \"\n                    \"attempting to process response as Transfer-Encoding: \"\n                    \"chunked.\"\n                )\n                return None\n\n            try:\n                # RFC 7230 section 3.3.2 specifies multiple content lengths can\n                # be sent in a single Content-Length header\n                # (e.g. Content-Length: 42, 42). This line ensures the values\n                # are all valid ints and that as long as the `set` length is 1,\n                # all values are the same. Otherwise, the header is invalid.\n                lengths = set([int(val) for val in length.split(\",\")])\n                if len(lengths) > 1:\n                    raise InvalidHeader(\n                        \"Content-Length contained multiple \"\n                        \"unmatching values (%s)\" % length\n                    )\n                length = lengths.pop()\n            except ValueError:\n                length = None\n            else:\n                if length < 0:\n                    length = None\n\n        # Convert status to int for comparison\n        # In some cases, httplib returns a status of \"_UNKNOWN\"\n        try:\n            status = int(self.status)\n        except ValueError:\n            status = 0\n\n        # Check for responses that shouldn't include a body\n        if status in (204, 304) or 100 <= status < 200 or request_method == \"HEAD\":\n            length = 0\n\n        return length\n\n    def _init_decoder(self):\n        \"\"\"\n        Set-up the _decoder attribute if necessary.\n        \"\"\"\n        # Note: content-encoding value should be case-insensitive, per RFC 7230\n        # Section 3.2\n        content_encoding = self.headers.get(\"content-encoding\", \"\").lower()\n        if self._decoder is None:\n            if content_encoding in self.CONTENT_DECODERS:\n                self._decoder = _get_decoder(content_encoding)\n            elif \",\" in content_encoding:\n                encodings = [\n                    e.strip()\n                    for e in content_encoding.split(\",\")\n                    if e.strip() in self.CONTENT_DECODERS\n                ]\n                if len(encodings):\n                    self._decoder = _get_decoder(content_encoding)\n\n    DECODER_ERROR_CLASSES = (IOError, zlib.error)\n    if brotli is not None:\n        DECODER_ERROR_CLASSES += (brotli.error,)\n\n    def _decode(self, data, decode_content, flush_decoder):\n        \"\"\"\n        Decode the data passed in and potentially flush the decoder.\n        \"\"\"\n        if not decode_content:\n            return data\n\n        try:\n            if self._decoder:\n                data = self._decoder.decompress(data)\n        except self.DECODER_ERROR_CLASSES as e:\n            content_encoding = self.headers.get(\"content-encoding\", \"\").lower()\n            raise DecodeError(\n                \"Received response with content-encoding: %s, but \"\n                \"failed to decode it.\" % content_encoding,\n                e,\n            )\n        if flush_decoder:\n            data += self._flush_decoder()\n\n        return data\n\n    def _flush_decoder(self):\n        \"\"\"\n        Flushes the decoder. Should only be called if the decoder is actually\n        being used.\n        \"\"\"\n        if self._decoder:\n            buf = self._decoder.decompress(b\"\")\n            return buf + self._decoder.flush()\n\n        return b\"\"\n\n    @contextmanager\n    def _error_catcher(self):\n        \"\"\"\n        Catch low-level python exceptions, instead re-raising urllib3\n        variants, so that low-level exceptions are not leaked in the\n        high-level api.\n\n        On exit, release the connection back to the pool.\n        \"\"\"\n        clean_exit = False\n\n        try:\n            try:\n                yield\n\n            except SocketTimeout:\n                # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but\n                # there is yet no clean way to get at it from this context.\n                raise ReadTimeoutError(self._pool, None, \"Read timed out.\")\n\n            except BaseSSLError as e:\n                # FIXME: Is there a better way to differentiate between SSLErrors?\n                if \"read operation timed out\" not in str(e):\n                    # SSL errors related to framing/MAC get wrapped and reraised here\n                    raise SSLError(e)\n\n                raise ReadTimeoutError(self._pool, None, \"Read timed out.\")\n\n            except (HTTPException, SocketError) as e:\n                # This includes IncompleteRead.\n                raise ProtocolError(\"Connection broken: %r\" % e, e)\n\n            # If no exception is thrown, we should avoid cleaning up\n            # unnecessarily.\n            clean_exit = True\n        finally:\n            # If we didn't terminate cleanly, we need to throw away our\n            # connection.\n            if not clean_exit:\n                # The response may not be closed but we're not going to use it\n                # anymore so close it now to ensure that the connection is\n                # released back to the pool.\n                if self._original_response:\n                    self._original_response.close()\n\n                # Closing the response may not actually be sufficient to close\n                # everything, so if we have a hold of the connection close that\n                # too.\n                if self._connection:\n                    self._connection.close()\n\n            # If we hold the original response but it's closed now, we should\n            # return the connection back to the pool.\n            if self._original_response and self._original_response.isclosed():\n                self.release_conn()\n\n    def read(self, amt=None, decode_content=None, cache_content=False):\n        \"\"\"\n        Similar to :meth:`http.client.HTTPResponse.read`, but with two additional\n        parameters: ``decode_content`` and ``cache_content``.\n\n        :param amt:\n            How much of the content to read. If specified, caching is skipped\n            because it doesn't make sense to cache partial content as the full\n            response.\n\n        :param decode_content:\n            If True, will attempt to decode the body based on the\n            'content-encoding' header.\n\n        :param cache_content:\n            If True, will save the returned data such that the same result is\n            returned despite of the state of the underlying file object. This\n            is useful if you want the ``.data`` property to continue working\n            after having ``.read()`` the file object. (Overridden if ``amt`` is\n            set.)\n        \"\"\"\n        self._init_decoder()\n        if decode_content is None:\n            decode_content = self.decode_content\n\n        if self._fp is None:\n            return\n\n        flush_decoder = False\n        fp_closed = getattr(self._fp, \"closed\", False)\n\n        with self._error_catcher():\n            if amt is None:\n                # cStringIO doesn't like amt=None\n                data = self._fp.read() if not fp_closed else b\"\"\n                flush_decoder = True\n            else:\n                cache_content = False\n                data = self._fp.read(amt) if not fp_closed else b\"\"\n                if (\n                    amt != 0 and not data\n                ):  # Platform-specific: Buggy versions of Python.\n                    # Close the connection when no data is returned\n                    #\n                    # This is redundant to what httplib/http.client _should_\n                    # already do.  However, versions of python released before\n                    # December 15, 2012 (http://bugs.python.org/issue16298) do\n                    # not properly close the connection in all cases. There is\n                    # no harm in redundantly calling close.\n                    self._fp.close()\n                    flush_decoder = True\n                    if self.enforce_content_length and self.length_remaining not in (\n                        0,\n                        None,\n                    ):\n                        # This is an edge case that httplib failed to cover due\n                        # to concerns of backward compatibility. We're\n                        # addressing it here to make sure IncompleteRead is\n                        # raised during streaming, so all calls with incorrect\n                        # Content-Length are caught.\n                        raise IncompleteRead(self._fp_bytes_read, self.length_remaining)\n\n        if data:\n            self._fp_bytes_read += len(data)\n            if self.length_remaining is not None:\n                self.length_remaining -= len(data)\n\n            data = self._decode(data, decode_content, flush_decoder)\n\n            if cache_content:\n                self._body = data\n\n        return data\n\n    def stream(self, amt=2 ** 16, decode_content=None):\n        \"\"\"\n        A generator wrapper for the read() method. A call will block until\n        ``amt`` bytes have been read from the connection or until the\n        connection is closed.\n\n        :param amt:\n            How much of the content to read. The generator will return up to\n            much data per iteration, but may return less. This is particularly\n            likely when using compressed data. However, the empty string will\n            never be returned.\n\n        :param decode_content:\n            If True, will attempt to decode the body based on the\n            'content-encoding' header.\n        \"\"\"\n        if self.chunked and self.supports_chunked_reads():\n            for line in self.read_chunked(amt, decode_content=decode_content):\n                yield line\n        else:\n            while not is_fp_closed(self._fp):\n                data = self.read(amt=amt, decode_content=decode_content)\n\n                if data:\n                    yield data\n\n    @classmethod\n    def from_httplib(ResponseCls, r, **response_kw):\n        \"\"\"\n        Given an :class:`http.client.HTTPResponse` instance ``r``, return a\n        corresponding :class:`urllib3.response.HTTPResponse` object.\n\n        Remaining parameters are passed to the HTTPResponse constructor, along\n        with ``original_response=r``.\n        \"\"\"\n        headers = r.msg\n\n        if not isinstance(headers, HTTPHeaderDict):\n            if six.PY2:\n                # Python 2.7\n                headers = HTTPHeaderDict.from_httplib(headers)\n            else:\n                headers = HTTPHeaderDict(headers.items())\n\n        # HTTPResponse objects in Python 3 don't have a .strict attribute\n        strict = getattr(r, \"strict\", 0)\n        resp = ResponseCls(\n            body=r,\n            headers=headers,\n            status=r.status,\n            version=r.version,\n            reason=r.reason,\n            strict=strict,\n            original_response=r,\n            **response_kw\n        )\n        return resp\n\n    # Backwards-compatibility methods for http.client.HTTPResponse\n    def getheaders(self):\n        return self.headers\n\n    def getheader(self, name, default=None):\n        return self.headers.get(name, default)\n\n    # Backwards compatibility for http.cookiejar\n    def info(self):\n        return self.headers\n\n    # Overrides from io.IOBase\n    def close(self):\n        if not self.closed:\n            self._fp.close()\n\n        if self._connection:\n            self._connection.close()\n\n        if not self.auto_close:\n            io.IOBase.close(self)\n\n    @property\n    def closed(self):\n        if not self.auto_close:\n            return io.IOBase.closed.__get__(self)\n        elif self._fp is None:\n            return True\n        elif hasattr(self._fp, \"isclosed\"):\n            return self._fp.isclosed()\n        elif hasattr(self._fp, \"closed\"):\n            return self._fp.closed\n        else:\n            return True\n\n    def fileno(self):\n        if self._fp is None:\n            raise IOError(\"HTTPResponse has no file to get a fileno from\")\n        elif hasattr(self._fp, \"fileno\"):\n            return self._fp.fileno()\n        else:\n            raise IOError(\n                \"The file-like object this HTTPResponse is wrapped \"\n                \"around has no file descriptor\"\n            )\n\n    def flush(self):\n        if (\n            self._fp is not None\n            and hasattr(self._fp, \"flush\")\n            and not getattr(self._fp, \"closed\", False)\n        ):\n            return self._fp.flush()\n\n    def readable(self):\n        # This method is required for `io` module compatibility.\n        return True\n\n    def readinto(self, b):\n        # This method is required for `io` module compatibility.\n        temp = self.read(len(b))\n        if len(temp) == 0:\n            return 0\n        else:\n            b[: len(temp)] = temp\n            return len(temp)\n\n    def supports_chunked_reads(self):\n        \"\"\"\n        Checks if the underlying file-like object looks like a\n        :class:`http.client.HTTPResponse` object. We do this by testing for\n        the fp attribute. If it is present we assume it returns raw chunks as\n        processed by read_chunked().\n        \"\"\"\n        return hasattr(self._fp, \"fp\")\n\n    def _update_chunk_length(self):\n        # First, we'll figure out length of a chunk and then\n        # we'll try to read it from socket.\n        if self.chunk_left is not None:\n            return\n        line = self._fp.fp.readline()\n        line = line.split(b\";\", 1)[0]\n        try:\n            self.chunk_left = int(line, 16)\n        except ValueError:\n            # Invalid chunked protocol response, abort.\n            self.close()\n            raise InvalidChunkLength(self, line)\n\n    def _handle_chunk(self, amt):\n        returned_chunk = None\n        if amt is None:\n            chunk = self._fp._safe_read(self.chunk_left)\n            returned_chunk = chunk\n            self._fp._safe_read(2)  # Toss the CRLF at the end of the chunk.\n            self.chunk_left = None\n        elif amt < self.chunk_left:\n            value = self._fp._safe_read(amt)\n            self.chunk_left = self.chunk_left - amt\n            returned_chunk = value\n        elif amt == self.chunk_left:\n            value = self._fp._safe_read(amt)\n            self._fp._safe_read(2)  # Toss the CRLF at the end of the chunk.\n            self.chunk_left = None\n            returned_chunk = value\n        else:  # amt > self.chunk_left\n            returned_chunk = self._fp._safe_read(self.chunk_left)\n            self._fp._safe_read(2)  # Toss the CRLF at the end of the chunk.\n            self.chunk_left = None\n        return returned_chunk\n\n    def read_chunked(self, amt=None, decode_content=None):\n        \"\"\"\n        Similar to :meth:`HTTPResponse.read`, but with an additional\n        parameter: ``decode_content``.\n\n        :param amt:\n            How much of the content to read. If specified, caching is skipped\n            because it doesn't make sense to cache partial content as the full\n            response.\n\n        :param decode_content:\n            If True, will attempt to decode the body based on the\n            'content-encoding' header.\n        \"\"\"\n        self._init_decoder()\n        # FIXME: Rewrite this method and make it a class with a better structured logic.\n        if not self.chunked:\n            raise ResponseNotChunked(\n                \"Response is not chunked. \"\n                \"Header 'transfer-encoding: chunked' is missing.\"\n            )\n        if not self.supports_chunked_reads():\n            raise BodyNotHttplibCompatible(\n                \"Body should be http.client.HTTPResponse like. \"\n                \"It should have have an fp attribute which returns raw chunks.\"\n            )\n\n        with self._error_catcher():\n            # Don't bother reading the body of a HEAD request.\n            if self._original_response and is_response_to_head(self._original_response):\n                self._original_response.close()\n                return\n\n            # If a response is already read and closed\n            # then return immediately.\n            if self._fp.fp is None:\n                return\n\n            while True:\n                self._update_chunk_length()\n                if self.chunk_left == 0:\n                    break\n                chunk = self._handle_chunk(amt)\n                decoded = self._decode(\n                    chunk, decode_content=decode_content, flush_decoder=False\n                )\n                if decoded:\n                    yield decoded\n\n            if decode_content:\n                # On CPython and PyPy, we should never need to flush the\n                # decoder. However, on Jython we *might* need to, so\n                # lets defensively do it anyway.\n                decoded = self._flush_decoder()\n                if decoded:  # Platform-specific: Jython.\n                    yield decoded\n\n            # Chunk content ends with \\r\\n: discard it.\n            while True:\n                line = self._fp.fp.readline()\n                if not line:\n                    # Some sites may not end with '\\r\\n'.\n                    break\n                if line == b\"\\r\\n\":\n                    break\n\n            # We read everything; close the \"file\".\n            if self._original_response:\n                self._original_response.close()\n\n    def geturl(self):\n        \"\"\"\n        Returns the URL that was the source of this response.\n        If the request that generated this response redirected, this method\n        will return the final redirect location.\n        \"\"\"\n        if self.retries is not None and len(self.retries.history):\n            return self.retries.history[-1].redirect_location\n        else:\n            return self._request_url\n\n    def __iter__(self):\n        buffer = []\n        for chunk in self.stream(decode_content=True):\n            if b\"\\n\" in chunk:\n                chunk = chunk.split(b\"\\n\")\n                yield b\"\".join(buffer) + chunk[0] + b\"\\n\"\n                for x in chunk[1:-1]:\n                    yield x + b\"\\n\"\n                if chunk[-1]:\n                    buffer = [chunk[-1]]\n                else:\n                    buffer = []\n            else:\n                buffer.append(chunk)\n        if buffer:\n            yield b\"\".join(buffer)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/__init__.py",
    "content": "from __future__ import absolute_import\n\n# For backwards compatibility, provide imports that used to be here.\nfrom .connection import is_connection_dropped\nfrom .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers\nfrom .response import is_fp_closed\nfrom .retry import Retry\nfrom .ssl_ import (\n    ALPN_PROTOCOLS,\n    HAS_SNI,\n    IS_PYOPENSSL,\n    IS_SECURETRANSPORT,\n    PROTOCOL_TLS,\n    SSLContext,\n    assert_fingerprint,\n    resolve_cert_reqs,\n    resolve_ssl_version,\n    ssl_wrap_socket,\n)\nfrom .timeout import Timeout, current_time\nfrom .url import Url, get_host, parse_url, split_first\nfrom .wait import wait_for_read, wait_for_write\n\n__all__ = (\n    \"HAS_SNI\",\n    \"IS_PYOPENSSL\",\n    \"IS_SECURETRANSPORT\",\n    \"SSLContext\",\n    \"PROTOCOL_TLS\",\n    \"ALPN_PROTOCOLS\",\n    \"Retry\",\n    \"Timeout\",\n    \"Url\",\n    \"assert_fingerprint\",\n    \"current_time\",\n    \"is_connection_dropped\",\n    \"is_fp_closed\",\n    \"get_host\",\n    \"parse_url\",\n    \"make_headers\",\n    \"resolve_cert_reqs\",\n    \"resolve_ssl_version\",\n    \"split_first\",\n    \"ssl_wrap_socket\",\n    \"wait_for_read\",\n    \"wait_for_write\",\n    \"SKIP_HEADER\",\n    \"SKIPPABLE_HEADERS\",\n)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/connection.py",
    "content": "from __future__ import absolute_import\n\nimport socket\n\nfrom ..contrib import _appengine_environ\nfrom ..exceptions import LocationParseError\nfrom ..packages import six\nfrom .wait import NoWayToWaitForSocketError, wait_for_read\n\n\ndef is_connection_dropped(conn):  # Platform-specific\n    \"\"\"\n    Returns True if the connection is dropped and should be closed.\n\n    :param conn:\n        :class:`http.client.HTTPConnection` object.\n\n    Note: For platforms like AppEngine, this will always return ``False`` to\n    let the platform handle connection recycling transparently for us.\n    \"\"\"\n    sock = getattr(conn, \"sock\", False)\n    if sock is False:  # Platform-specific: AppEngine\n        return False\n    if sock is None:  # Connection already closed (such as by httplib).\n        return True\n    try:\n        # Returns True if readable, which here means it's been dropped\n        return wait_for_read(sock, timeout=0.0)\n    except NoWayToWaitForSocketError:  # Platform-specific: AppEngine\n        return False\n\n\n# This function is copied from socket.py in the Python 2.7 standard\n# library test suite. Added to its signature is only `socket_options`.\n# One additional modification is that we avoid binding to IPv6 servers\n# discovered in DNS if the system doesn't have IPv6 functionality.\ndef create_connection(\n    address,\n    timeout=socket._GLOBAL_DEFAULT_TIMEOUT,\n    source_address=None,\n    socket_options=None,\n):\n    \"\"\"Connect to *address* and return the socket object.\n\n    Convenience function.  Connect to *address* (a 2-tuple ``(host,\n    port)``) and return the socket object.  Passing the optional\n    *timeout* parameter will set the timeout on the socket instance\n    before attempting to connect.  If no *timeout* is supplied, the\n    global default timeout setting returned by :func:`socket.getdefaulttimeout`\n    is used.  If *source_address* is set it must be a tuple of (host, port)\n    for the socket to bind as a source address before making the connection.\n    An host of '' or port 0 tells the OS to use the default.\n    \"\"\"\n\n    host, port = address\n    if host.startswith(\"[\"):\n        host = host.strip(\"[]\")\n    err = None\n\n    # Using the value from allowed_gai_family() in the context of getaddrinfo lets\n    # us select whether to work with IPv4 DNS records, IPv6 records, or both.\n    # The original create_connection function always returns all records.\n    family = allowed_gai_family()\n\n    try:\n        host.encode(\"idna\")\n    except UnicodeError:\n        return six.raise_from(\n            LocationParseError(u\"'%s', label empty or too long\" % host), None\n        )\n\n    for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):\n        af, socktype, proto, canonname, sa = res\n        sock = None\n        try:\n            sock = socket.socket(af, socktype, proto)\n\n            # If provided, set socket level options before connecting.\n            _set_socket_options(sock, socket_options)\n\n            if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:\n                sock.settimeout(timeout)\n            if source_address:\n                sock.bind(source_address)\n            sock.connect(sa)\n            return sock\n\n        except socket.error as e:\n            err = e\n            if sock is not None:\n                sock.close()\n                sock = None\n\n    if err is not None:\n        raise err\n\n    raise socket.error(\"getaddrinfo returns an empty list\")\n\n\ndef _set_socket_options(sock, options):\n    if options is None:\n        return\n\n    for opt in options:\n        sock.setsockopt(*opt)\n\n\ndef allowed_gai_family():\n    \"\"\"This function is designed to work in the context of\n    getaddrinfo, where family=socket.AF_UNSPEC is the default and\n    will perform a DNS search for both IPv6 and IPv4 records.\"\"\"\n\n    family = socket.AF_INET\n    if HAS_IPV6:\n        family = socket.AF_UNSPEC\n    return family\n\n\ndef _has_ipv6(host):\n    \"\"\"Returns True if the system can bind an IPv6 address.\"\"\"\n    sock = None\n    has_ipv6 = False\n\n    # App Engine doesn't support IPV6 sockets and actually has a quota on the\n    # number of sockets that can be used, so just early out here instead of\n    # creating a socket needlessly.\n    # See https://github.com/urllib3/urllib3/issues/1446\n    if _appengine_environ.is_appengine_sandbox():\n        return False\n\n    if socket.has_ipv6:\n        # has_ipv6 returns true if cPython was compiled with IPv6 support.\n        # It does not tell us if the system has IPv6 support enabled. To\n        # determine that we must bind to an IPv6 address.\n        # https://github.com/urllib3/urllib3/pull/611\n        # https://bugs.python.org/issue658327\n        try:\n            sock = socket.socket(socket.AF_INET6)\n            sock.bind((host, 0))\n            has_ipv6 = True\n        except Exception:\n            pass\n\n    if sock:\n        sock.close()\n    return has_ipv6\n\n\nHAS_IPV6 = _has_ipv6(\"::1\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/proxy.py",
    "content": "from .ssl_ import create_urllib3_context, resolve_cert_reqs, resolve_ssl_version\n\n\ndef connection_requires_http_tunnel(\n    proxy_url=None, proxy_config=None, destination_scheme=None\n):\n    \"\"\"\n    Returns True if the connection requires an HTTP CONNECT through the proxy.\n\n    :param URL proxy_url:\n        URL of the proxy.\n    :param ProxyConfig proxy_config:\n        Proxy configuration from poolmanager.py\n    :param str destination_scheme:\n        The scheme of the destination. (i.e https, http, etc)\n    \"\"\"\n    # If we're not using a proxy, no way to use a tunnel.\n    if proxy_url is None:\n        return False\n\n    # HTTP destinations never require tunneling, we always forward.\n    if destination_scheme == \"http\":\n        return False\n\n    # Support for forwarding with HTTPS proxies and HTTPS destinations.\n    if (\n        proxy_url.scheme == \"https\"\n        and proxy_config\n        and proxy_config.use_forwarding_for_https\n    ):\n        return False\n\n    # Otherwise always use a tunnel.\n    return True\n\n\ndef create_proxy_ssl_context(\n    ssl_version, cert_reqs, ca_certs=None, ca_cert_dir=None, ca_cert_data=None\n):\n    \"\"\"\n    Generates a default proxy ssl context if one hasn't been provided by the\n    user.\n    \"\"\"\n    ssl_context = create_urllib3_context(\n        ssl_version=resolve_ssl_version(ssl_version),\n        cert_reqs=resolve_cert_reqs(cert_reqs),\n    )\n\n    if (\n        not ca_certs\n        and not ca_cert_dir\n        and not ca_cert_data\n        and hasattr(ssl_context, \"load_default_certs\")\n    ):\n        ssl_context.load_default_certs()\n\n    return ssl_context\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/queue.py",
    "content": "import collections\n\nfrom ..packages import six\nfrom ..packages.six.moves import queue\n\nif six.PY2:\n    # Queue is imported for side effects on MS Windows. See issue #229.\n    import Queue as _unused_module_Queue  # noqa: F401\n\n\nclass LifoQueue(queue.Queue):\n    def _init(self, _):\n        self.queue = collections.deque()\n\n    def _qsize(self, len=len):\n        return len(self.queue)\n\n    def _put(self, item):\n        self.queue.append(item)\n\n    def _get(self):\n        return self.queue.pop()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/request.py",
    "content": "from __future__ import absolute_import\n\nfrom base64 import b64encode\n\nfrom ..exceptions import UnrewindableBodyError\nfrom ..packages.six import b, integer_types\n\n# Pass as a value within ``headers`` to skip\n# emitting some HTTP headers that are added automatically.\n# The only headers that are supported are ``Accept-Encoding``,\n# ``Host``, and ``User-Agent``.\nSKIP_HEADER = \"@@@SKIP_HEADER@@@\"\nSKIPPABLE_HEADERS = frozenset([\"accept-encoding\", \"host\", \"user-agent\"])\n\nACCEPT_ENCODING = \"gzip,deflate\"\ntry:\n    try:\n        import brotlicffi as _unused_module_brotli  # noqa: F401\n    except ImportError:\n        import brotli as _unused_module_brotli  # noqa: F401\nexcept ImportError:\n    pass\nelse:\n    ACCEPT_ENCODING += \",br\"\n\n_FAILEDTELL = object()\n\n\ndef make_headers(\n    keep_alive=None,\n    accept_encoding=None,\n    user_agent=None,\n    basic_auth=None,\n    proxy_basic_auth=None,\n    disable_cache=None,\n):\n    \"\"\"\n    Shortcuts for generating request headers.\n\n    :param keep_alive:\n        If ``True``, adds 'connection: keep-alive' header.\n\n    :param accept_encoding:\n        Can be a boolean, list, or string.\n        ``True`` translates to 'gzip,deflate'.\n        List will get joined by comma.\n        String will be used as provided.\n\n    :param user_agent:\n        String representing the user-agent you want, such as\n        \"python-urllib3/0.6\"\n\n    :param basic_auth:\n        Colon-separated username:password string for 'authorization: basic ...'\n        auth header.\n\n    :param proxy_basic_auth:\n        Colon-separated username:password string for 'proxy-authorization: basic ...'\n        auth header.\n\n    :param disable_cache:\n        If ``True``, adds 'cache-control: no-cache' header.\n\n    Example::\n\n        >>> make_headers(keep_alive=True, user_agent=\"Batman/1.0\")\n        {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'}\n        >>> make_headers(accept_encoding=True)\n        {'accept-encoding': 'gzip,deflate'}\n    \"\"\"\n    headers = {}\n    if accept_encoding:\n        if isinstance(accept_encoding, str):\n            pass\n        elif isinstance(accept_encoding, list):\n            accept_encoding = \",\".join(accept_encoding)\n        else:\n            accept_encoding = ACCEPT_ENCODING\n        headers[\"accept-encoding\"] = accept_encoding\n\n    if user_agent:\n        headers[\"user-agent\"] = user_agent\n\n    if keep_alive:\n        headers[\"connection\"] = \"keep-alive\"\n\n    if basic_auth:\n        headers[\"authorization\"] = \"Basic \" + b64encode(b(basic_auth)).decode(\"utf-8\")\n\n    if proxy_basic_auth:\n        headers[\"proxy-authorization\"] = \"Basic \" + b64encode(\n            b(proxy_basic_auth)\n        ).decode(\"utf-8\")\n\n    if disable_cache:\n        headers[\"cache-control\"] = \"no-cache\"\n\n    return headers\n\n\ndef set_file_position(body, pos):\n    \"\"\"\n    If a position is provided, move file to that point.\n    Otherwise, we'll attempt to record a position for future use.\n    \"\"\"\n    if pos is not None:\n        rewind_body(body, pos)\n    elif getattr(body, \"tell\", None) is not None:\n        try:\n            pos = body.tell()\n        except (IOError, OSError):\n            # This differentiates from None, allowing us to catch\n            # a failed `tell()` later when trying to rewind the body.\n            pos = _FAILEDTELL\n\n    return pos\n\n\ndef rewind_body(body, body_pos):\n    \"\"\"\n    Attempt to rewind body to a certain position.\n    Primarily used for request redirects and retries.\n\n    :param body:\n        File-like object that supports seek.\n\n    :param int pos:\n        Position to seek to in file.\n    \"\"\"\n    body_seek = getattr(body, \"seek\", None)\n    if body_seek is not None and isinstance(body_pos, integer_types):\n        try:\n            body_seek(body_pos)\n        except (IOError, OSError):\n            raise UnrewindableBodyError(\n                \"An error occurred when rewinding request body for redirect/retry.\"\n            )\n    elif body_pos is _FAILEDTELL:\n        raise UnrewindableBodyError(\n            \"Unable to record file position for rewinding \"\n            \"request body during a redirect/retry.\"\n        )\n    else:\n        raise ValueError(\n            \"body_pos must be of type integer, instead it was %s.\" % type(body_pos)\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/response.py",
    "content": "from __future__ import absolute_import\n\nfrom email.errors import MultipartInvariantViolationDefect, StartBoundaryNotFoundDefect\n\nfrom ..exceptions import HeaderParsingError\nfrom ..packages.six.moves import http_client as httplib\n\n\ndef is_fp_closed(obj):\n    \"\"\"\n    Checks whether a given file-like object is closed.\n\n    :param obj:\n        The file-like object to check.\n    \"\"\"\n\n    try:\n        # Check `isclosed()` first, in case Python3 doesn't set `closed`.\n        # GH Issue #928\n        return obj.isclosed()\n    except AttributeError:\n        pass\n\n    try:\n        # Check via the official file-like-object way.\n        return obj.closed\n    except AttributeError:\n        pass\n\n    try:\n        # Check if the object is a container for another file-like object that\n        # gets released on exhaustion (e.g. HTTPResponse).\n        return obj.fp is None\n    except AttributeError:\n        pass\n\n    raise ValueError(\"Unable to determine whether fp is closed.\")\n\n\ndef assert_header_parsing(headers):\n    \"\"\"\n    Asserts whether all headers have been successfully parsed.\n    Extracts encountered errors from the result of parsing headers.\n\n    Only works on Python 3.\n\n    :param http.client.HTTPMessage headers: Headers to verify.\n\n    :raises urllib3.exceptions.HeaderParsingError:\n        If parsing errors are found.\n    \"\"\"\n\n    # This will fail silently if we pass in the wrong kind of parameter.\n    # To make debugging easier add an explicit check.\n    if not isinstance(headers, httplib.HTTPMessage):\n        raise TypeError(\"expected httplib.Message, got {0}.\".format(type(headers)))\n\n    defects = getattr(headers, \"defects\", None)\n    get_payload = getattr(headers, \"get_payload\", None)\n\n    unparsed_data = None\n    if get_payload:\n        # get_payload is actually email.message.Message.get_payload;\n        # we're only interested in the result if it's not a multipart message\n        if not headers.is_multipart():\n            payload = get_payload()\n\n            if isinstance(payload, (bytes, str)):\n                unparsed_data = payload\n    if defects:\n        # httplib is assuming a response body is available\n        # when parsing headers even when httplib only sends\n        # header data to parse_headers() This results in\n        # defects on multipart responses in particular.\n        # See: https://github.com/urllib3/urllib3/issues/800\n\n        # So we ignore the following defects:\n        # - StartBoundaryNotFoundDefect:\n        #     The claimed start boundary was never found.\n        # - MultipartInvariantViolationDefect:\n        #     A message claimed to be a multipart but no subparts were found.\n        defects = [\n            defect\n            for defect in defects\n            if not isinstance(\n                defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect)\n            )\n        ]\n\n    if defects or unparsed_data:\n        raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data)\n\n\ndef is_response_to_head(response):\n    \"\"\"\n    Checks whether the request of a response has been a HEAD-request.\n    Handles the quirks of AppEngine.\n\n    :param http.client.HTTPResponse response:\n        Response to check if the originating request\n        used 'HEAD' as a method.\n    \"\"\"\n    # FIXME: Can we do this somehow without accessing private httplib _method?\n    method = response._method\n    if isinstance(method, int):  # Platform-specific: Appengine\n        return method == 3\n    return method.upper() == \"HEAD\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/retry.py",
    "content": "from __future__ import absolute_import\n\nimport email\nimport logging\nimport re\nimport time\nimport warnings\nfrom collections import namedtuple\nfrom itertools import takewhile\n\nfrom ..exceptions import (\n    ConnectTimeoutError,\n    InvalidHeader,\n    MaxRetryError,\n    ProtocolError,\n    ProxyError,\n    ReadTimeoutError,\n    ResponseError,\n)\nfrom ..packages import six\n\nlog = logging.getLogger(__name__)\n\n\n# Data structure for representing the metadata of requests that result in a retry.\nRequestHistory = namedtuple(\n    \"RequestHistory\", [\"method\", \"url\", \"error\", \"status\", \"redirect_location\"]\n)\n\n\n# TODO: In v2 we can remove this sentinel and metaclass with deprecated options.\n_Default = object()\n\n\nclass _RetryMeta(type):\n    @property\n    def DEFAULT_METHOD_WHITELIST(cls):\n        warnings.warn(\n            \"Using 'Retry.DEFAULT_METHOD_WHITELIST' is deprecated and \"\n            \"will be removed in v2.0. Use 'Retry.DEFAULT_ALLOWED_METHODS' instead\",\n            DeprecationWarning,\n        )\n        return cls.DEFAULT_ALLOWED_METHODS\n\n    @DEFAULT_METHOD_WHITELIST.setter\n    def DEFAULT_METHOD_WHITELIST(cls, value):\n        warnings.warn(\n            \"Using 'Retry.DEFAULT_METHOD_WHITELIST' is deprecated and \"\n            \"will be removed in v2.0. Use 'Retry.DEFAULT_ALLOWED_METHODS' instead\",\n            DeprecationWarning,\n        )\n        cls.DEFAULT_ALLOWED_METHODS = value\n\n    @property\n    def DEFAULT_REDIRECT_HEADERS_BLACKLIST(cls):\n        warnings.warn(\n            \"Using 'Retry.DEFAULT_REDIRECT_HEADERS_BLACKLIST' is deprecated and \"\n            \"will be removed in v2.0. Use 'Retry.DEFAULT_REMOVE_HEADERS_ON_REDIRECT' instead\",\n            DeprecationWarning,\n        )\n        return cls.DEFAULT_REMOVE_HEADERS_ON_REDIRECT\n\n    @DEFAULT_REDIRECT_HEADERS_BLACKLIST.setter\n    def DEFAULT_REDIRECT_HEADERS_BLACKLIST(cls, value):\n        warnings.warn(\n            \"Using 'Retry.DEFAULT_REDIRECT_HEADERS_BLACKLIST' is deprecated and \"\n            \"will be removed in v2.0. Use 'Retry.DEFAULT_REMOVE_HEADERS_ON_REDIRECT' instead\",\n            DeprecationWarning,\n        )\n        cls.DEFAULT_REMOVE_HEADERS_ON_REDIRECT = value\n\n    @property\n    def BACKOFF_MAX(cls):\n        warnings.warn(\n            \"Using 'Retry.BACKOFF_MAX' is deprecated and \"\n            \"will be removed in v2.0. Use 'Retry.DEFAULT_BACKOFF_MAX' instead\",\n            DeprecationWarning,\n        )\n        return cls.DEFAULT_BACKOFF_MAX\n\n    @BACKOFF_MAX.setter\n    def BACKOFF_MAX(cls, value):\n        warnings.warn(\n            \"Using 'Retry.BACKOFF_MAX' is deprecated and \"\n            \"will be removed in v2.0. Use 'Retry.DEFAULT_BACKOFF_MAX' instead\",\n            DeprecationWarning,\n        )\n        cls.DEFAULT_BACKOFF_MAX = value\n\n\n@six.add_metaclass(_RetryMeta)\nclass Retry(object):\n    \"\"\"Retry configuration.\n\n    Each retry attempt will create a new Retry object with updated values, so\n    they can be safely reused.\n\n    Retries can be defined as a default for a pool::\n\n        retries = Retry(connect=5, read=2, redirect=5)\n        http = PoolManager(retries=retries)\n        response = http.request('GET', 'http://example.com/')\n\n    Or per-request (which overrides the default for the pool)::\n\n        response = http.request('GET', 'http://example.com/', retries=Retry(10))\n\n    Retries can be disabled by passing ``False``::\n\n        response = http.request('GET', 'http://example.com/', retries=False)\n\n    Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless\n    retries are disabled, in which case the causing exception will be raised.\n\n    :param int total:\n        Total number of retries to allow. Takes precedence over other counts.\n\n        Set to ``None`` to remove this constraint and fall back on other\n        counts.\n\n        Set to ``0`` to fail on the first retry.\n\n        Set to ``False`` to disable and imply ``raise_on_redirect=False``.\n\n    :param int connect:\n        How many connection-related errors to retry on.\n\n        These are errors raised before the request is sent to the remote server,\n        which we assume has not triggered the server to process the request.\n\n        Set to ``0`` to fail on the first retry of this type.\n\n    :param int read:\n        How many times to retry on read errors.\n\n        These errors are raised after the request was sent to the server, so the\n        request may have side-effects.\n\n        Set to ``0`` to fail on the first retry of this type.\n\n    :param int redirect:\n        How many redirects to perform. Limit this to avoid infinite redirect\n        loops.\n\n        A redirect is a HTTP response with a status code 301, 302, 303, 307 or\n        308.\n\n        Set to ``0`` to fail on the first retry of this type.\n\n        Set to ``False`` to disable and imply ``raise_on_redirect=False``.\n\n    :param int status:\n        How many times to retry on bad status codes.\n\n        These are retries made on responses, where status code matches\n        ``status_forcelist``.\n\n        Set to ``0`` to fail on the first retry of this type.\n\n    :param int other:\n        How many times to retry on other errors.\n\n        Other errors are errors that are not connect, read, redirect or status errors.\n        These errors might be raised after the request was sent to the server, so the\n        request might have side-effects.\n\n        Set to ``0`` to fail on the first retry of this type.\n\n        If ``total`` is not set, it's a good idea to set this to 0 to account\n        for unexpected edge cases and avoid infinite retry loops.\n\n    :param iterable allowed_methods:\n        Set of uppercased HTTP method verbs that we should retry on.\n\n        By default, we only retry on methods which are considered to be\n        idempotent (multiple requests with the same parameters end with the\n        same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`.\n\n        Set to a ``False`` value to retry on any verb.\n\n        .. warning::\n\n            Previously this parameter was named ``method_whitelist``, that\n            usage is deprecated in v1.26.0 and will be removed in v2.0.\n\n    :param iterable status_forcelist:\n        A set of integer HTTP status codes that we should force a retry on.\n        A retry is initiated if the request method is in ``allowed_methods``\n        and the response status code is in ``status_forcelist``.\n\n        By default, this is disabled with ``None``.\n\n    :param float backoff_factor:\n        A backoff factor to apply between attempts after the second try\n        (most errors are resolved immediately by a second try without a\n        delay). urllib3 will sleep for::\n\n            {backoff factor} * (2 ** ({number of total retries} - 1))\n\n        seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep\n        for [0.0s, 0.2s, 0.4s, ...] between retries. It will never be longer\n        than :attr:`Retry.DEFAULT_BACKOFF_MAX`.\n\n        By default, backoff is disabled (set to 0).\n\n    :param bool raise_on_redirect: Whether, if the number of redirects is\n        exhausted, to raise a MaxRetryError, or to return a response with a\n        response code in the 3xx range.\n\n    :param bool raise_on_status: Similar meaning to ``raise_on_redirect``:\n        whether we should raise an exception, or return a response,\n        if status falls in ``status_forcelist`` range and retries have\n        been exhausted.\n\n    :param tuple history: The history of the request encountered during\n        each call to :meth:`~Retry.increment`. The list is in the order\n        the requests occurred. Each list item is of class :class:`RequestHistory`.\n\n    :param bool respect_retry_after_header:\n        Whether to respect Retry-After header on status codes defined as\n        :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not.\n\n    :param iterable remove_headers_on_redirect:\n        Sequence of headers to remove from the request when a response\n        indicating a redirect is returned before firing off the redirected\n        request.\n    \"\"\"\n\n    #: Default methods to be used for ``allowed_methods``\n    DEFAULT_ALLOWED_METHODS = frozenset(\n        [\"HEAD\", \"GET\", \"PUT\", \"DELETE\", \"OPTIONS\", \"TRACE\"]\n    )\n\n    #: Default status codes to be used for ``status_forcelist``\n    RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503])\n\n    #: Default headers to be used for ``remove_headers_on_redirect``\n    DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset([\"Authorization\"])\n\n    #: Maximum backoff time.\n    DEFAULT_BACKOFF_MAX = 120\n\n    def __init__(\n        self,\n        total=10,\n        connect=None,\n        read=None,\n        redirect=None,\n        status=None,\n        other=None,\n        allowed_methods=_Default,\n        status_forcelist=None,\n        backoff_factor=0,\n        raise_on_redirect=True,\n        raise_on_status=True,\n        history=None,\n        respect_retry_after_header=True,\n        remove_headers_on_redirect=_Default,\n        # TODO: Deprecated, remove in v2.0\n        method_whitelist=_Default,\n    ):\n\n        if method_whitelist is not _Default:\n            if allowed_methods is not _Default:\n                raise ValueError(\n                    \"Using both 'allowed_methods' and \"\n                    \"'method_whitelist' together is not allowed. \"\n                    \"Instead only use 'allowed_methods'\"\n                )\n            warnings.warn(\n                \"Using 'method_whitelist' with Retry is deprecated and \"\n                \"will be removed in v2.0. Use 'allowed_methods' instead\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n            allowed_methods = method_whitelist\n        if allowed_methods is _Default:\n            allowed_methods = self.DEFAULT_ALLOWED_METHODS\n        if remove_headers_on_redirect is _Default:\n            remove_headers_on_redirect = self.DEFAULT_REMOVE_HEADERS_ON_REDIRECT\n\n        self.total = total\n        self.connect = connect\n        self.read = read\n        self.status = status\n        self.other = other\n\n        if redirect is False or total is False:\n            redirect = 0\n            raise_on_redirect = False\n\n        self.redirect = redirect\n        self.status_forcelist = status_forcelist or set()\n        self.allowed_methods = allowed_methods\n        self.backoff_factor = backoff_factor\n        self.raise_on_redirect = raise_on_redirect\n        self.raise_on_status = raise_on_status\n        self.history = history or tuple()\n        self.respect_retry_after_header = respect_retry_after_header\n        self.remove_headers_on_redirect = frozenset(\n            [h.lower() for h in remove_headers_on_redirect]\n        )\n\n    def new(self, **kw):\n        params = dict(\n            total=self.total,\n            connect=self.connect,\n            read=self.read,\n            redirect=self.redirect,\n            status=self.status,\n            other=self.other,\n            status_forcelist=self.status_forcelist,\n            backoff_factor=self.backoff_factor,\n            raise_on_redirect=self.raise_on_redirect,\n            raise_on_status=self.raise_on_status,\n            history=self.history,\n            remove_headers_on_redirect=self.remove_headers_on_redirect,\n            respect_retry_after_header=self.respect_retry_after_header,\n        )\n\n        # TODO: If already given in **kw we use what's given to us\n        # If not given we need to figure out what to pass. We decide\n        # based on whether our class has the 'method_whitelist' property\n        # and if so we pass the deprecated 'method_whitelist' otherwise\n        # we use 'allowed_methods'. Remove in v2.0\n        if \"method_whitelist\" not in kw and \"allowed_methods\" not in kw:\n            if \"method_whitelist\" in self.__dict__:\n                warnings.warn(\n                    \"Using 'method_whitelist' with Retry is deprecated and \"\n                    \"will be removed in v2.0. Use 'allowed_methods' instead\",\n                    DeprecationWarning,\n                )\n                params[\"method_whitelist\"] = self.allowed_methods\n            else:\n                params[\"allowed_methods\"] = self.allowed_methods\n\n        params.update(kw)\n        return type(self)(**params)\n\n    @classmethod\n    def from_int(cls, retries, redirect=True, default=None):\n        \"\"\"Backwards-compatibility for the old retries format.\"\"\"\n        if retries is None:\n            retries = default if default is not None else cls.DEFAULT\n\n        if isinstance(retries, Retry):\n            return retries\n\n        redirect = bool(redirect) and None\n        new_retries = cls(retries, redirect=redirect)\n        log.debug(\"Converted retries value: %r -> %r\", retries, new_retries)\n        return new_retries\n\n    def get_backoff_time(self):\n        \"\"\"Formula for computing the current backoff\n\n        :rtype: float\n        \"\"\"\n        # We want to consider only the last consecutive errors sequence (Ignore redirects).\n        consecutive_errors_len = len(\n            list(\n                takewhile(lambda x: x.redirect_location is None, reversed(self.history))\n            )\n        )\n        if consecutive_errors_len <= 1:\n            return 0\n\n        backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1))\n        return min(self.DEFAULT_BACKOFF_MAX, backoff_value)\n\n    def parse_retry_after(self, retry_after):\n        # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4\n        if re.match(r\"^\\s*[0-9]+\\s*$\", retry_after):\n            seconds = int(retry_after)\n        else:\n            retry_date_tuple = email.utils.parsedate_tz(retry_after)\n            if retry_date_tuple is None:\n                raise InvalidHeader(\"Invalid Retry-After header: %s\" % retry_after)\n            if retry_date_tuple[9] is None:  # Python 2\n                # Assume UTC if no timezone was specified\n                # On Python2.7, parsedate_tz returns None for a timezone offset\n                # instead of 0 if no timezone is given, where mktime_tz treats\n                # a None timezone offset as local time.\n                retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:]\n\n            retry_date = email.utils.mktime_tz(retry_date_tuple)\n            seconds = retry_date - time.time()\n\n        if seconds < 0:\n            seconds = 0\n\n        return seconds\n\n    def get_retry_after(self, response):\n        \"\"\"Get the value of Retry-After in seconds.\"\"\"\n\n        retry_after = response.getheader(\"Retry-After\")\n\n        if retry_after is None:\n            return None\n\n        return self.parse_retry_after(retry_after)\n\n    def sleep_for_retry(self, response=None):\n        retry_after = self.get_retry_after(response)\n        if retry_after:\n            time.sleep(retry_after)\n            return True\n\n        return False\n\n    def _sleep_backoff(self):\n        backoff = self.get_backoff_time()\n        if backoff <= 0:\n            return\n        time.sleep(backoff)\n\n    def sleep(self, response=None):\n        \"\"\"Sleep between retry attempts.\n\n        This method will respect a server's ``Retry-After`` response header\n        and sleep the duration of the time requested. If that is not present, it\n        will use an exponential backoff. By default, the backoff factor is 0 and\n        this method will return immediately.\n        \"\"\"\n\n        if self.respect_retry_after_header and response:\n            slept = self.sleep_for_retry(response)\n            if slept:\n                return\n\n        self._sleep_backoff()\n\n    def _is_connection_error(self, err):\n        \"\"\"Errors when we're fairly sure that the server did not receive the\n        request, so it should be safe to retry.\n        \"\"\"\n        if isinstance(err, ProxyError):\n            err = err.original_error\n        return isinstance(err, ConnectTimeoutError)\n\n    def _is_read_error(self, err):\n        \"\"\"Errors that occur after the request has been started, so we should\n        assume that the server began processing it.\n        \"\"\"\n        return isinstance(err, (ReadTimeoutError, ProtocolError))\n\n    def _is_method_retryable(self, method):\n        \"\"\"Checks if a given HTTP method should be retried upon, depending if\n        it is included in the allowed_methods\n        \"\"\"\n        # TODO: For now favor if the Retry implementation sets its own method_whitelist\n        # property outside of our constructor to avoid breaking custom implementations.\n        if \"method_whitelist\" in self.__dict__:\n            warnings.warn(\n                \"Using 'method_whitelist' with Retry is deprecated and \"\n                \"will be removed in v2.0. Use 'allowed_methods' instead\",\n                DeprecationWarning,\n            )\n            allowed_methods = self.method_whitelist\n        else:\n            allowed_methods = self.allowed_methods\n\n        if allowed_methods and method.upper() not in allowed_methods:\n            return False\n        return True\n\n    def is_retry(self, method, status_code, has_retry_after=False):\n        \"\"\"Is this method/status code retryable? (Based on allowlists and control\n        variables such as the number of total retries to allow, whether to\n        respect the Retry-After header, whether this header is present, and\n        whether the returned status code is on the list of status codes to\n        be retried upon on the presence of the aforementioned header)\n        \"\"\"\n        if not self._is_method_retryable(method):\n            return False\n\n        if self.status_forcelist and status_code in self.status_forcelist:\n            return True\n\n        return (\n            self.total\n            and self.respect_retry_after_header\n            and has_retry_after\n            and (status_code in self.RETRY_AFTER_STATUS_CODES)\n        )\n\n    def is_exhausted(self):\n        \"\"\"Are we out of retries?\"\"\"\n        retry_counts = (\n            self.total,\n            self.connect,\n            self.read,\n            self.redirect,\n            self.status,\n            self.other,\n        )\n        retry_counts = list(filter(None, retry_counts))\n        if not retry_counts:\n            return False\n\n        return min(retry_counts) < 0\n\n    def increment(\n        self,\n        method=None,\n        url=None,\n        response=None,\n        error=None,\n        _pool=None,\n        _stacktrace=None,\n    ):\n        \"\"\"Return a new Retry object with incremented retry counters.\n\n        :param response: A response object, or None, if the server did not\n            return a response.\n        :type response: :class:`~urllib3.response.HTTPResponse`\n        :param Exception error: An error encountered during the request, or\n            None if the response was received successfully.\n\n        :return: A new ``Retry`` object.\n        \"\"\"\n        if self.total is False and error:\n            # Disabled, indicate to re-raise the error.\n            raise six.reraise(type(error), error, _stacktrace)\n\n        total = self.total\n        if total is not None:\n            total -= 1\n\n        connect = self.connect\n        read = self.read\n        redirect = self.redirect\n        status_count = self.status\n        other = self.other\n        cause = \"unknown\"\n        status = None\n        redirect_location = None\n\n        if error and self._is_connection_error(error):\n            # Connect retry?\n            if connect is False:\n                raise six.reraise(type(error), error, _stacktrace)\n            elif connect is not None:\n                connect -= 1\n\n        elif error and self._is_read_error(error):\n            # Read retry?\n            if read is False or not self._is_method_retryable(method):\n                raise six.reraise(type(error), error, _stacktrace)\n            elif read is not None:\n                read -= 1\n\n        elif error:\n            # Other retry?\n            if other is not None:\n                other -= 1\n\n        elif response and response.get_redirect_location():\n            # Redirect retry?\n            if redirect is not None:\n                redirect -= 1\n            cause = \"too many redirects\"\n            redirect_location = response.get_redirect_location()\n            status = response.status\n\n        else:\n            # Incrementing because of a server error like a 500 in\n            # status_forcelist and the given method is in the allowed_methods\n            cause = ResponseError.GENERIC_ERROR\n            if response and response.status:\n                if status_count is not None:\n                    status_count -= 1\n                cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status)\n                status = response.status\n\n        history = self.history + (\n            RequestHistory(method, url, error, status, redirect_location),\n        )\n\n        new_retry = self.new(\n            total=total,\n            connect=connect,\n            read=read,\n            redirect=redirect,\n            status=status_count,\n            other=other,\n            history=history,\n        )\n\n        if new_retry.is_exhausted():\n            raise MaxRetryError(_pool, url, error or ResponseError(cause))\n\n        log.debug(\"Incremented Retry for (url='%s'): %r\", url, new_retry)\n\n        return new_retry\n\n    def __repr__(self):\n        return (\n            \"{cls.__name__}(total={self.total}, connect={self.connect}, \"\n            \"read={self.read}, redirect={self.redirect}, status={self.status})\"\n        ).format(cls=type(self), self=self)\n\n    def __getattr__(self, item):\n        if item == \"method_whitelist\":\n            # TODO: Remove this deprecated alias in v2.0\n            warnings.warn(\n                \"Using 'method_whitelist' with Retry is deprecated and \"\n                \"will be removed in v2.0. Use 'allowed_methods' instead\",\n                DeprecationWarning,\n            )\n            return self.allowed_methods\n        try:\n            return getattr(super(Retry, self), item)\n        except AttributeError:\n            return getattr(Retry, item)\n\n\n# For backwards compatibility (equivalent to pre-v1.9):\nRetry.DEFAULT = Retry(3)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/ssl_.py",
    "content": "from __future__ import absolute_import\n\nimport hmac\nimport os\nimport sys\nimport warnings\nfrom binascii import hexlify, unhexlify\nfrom hashlib import md5, sha1, sha256\n\nfrom ..exceptions import (\n    InsecurePlatformWarning,\n    ProxySchemeUnsupported,\n    SNIMissingWarning,\n    SSLError,\n)\nfrom ..packages import six\nfrom .url import BRACELESS_IPV6_ADDRZ_RE, IPV4_RE\n\nSSLContext = None\nSSLTransport = None\nHAS_SNI = False\nIS_PYOPENSSL = False\nIS_SECURETRANSPORT = False\nALPN_PROTOCOLS = [\"http/1.1\"]\n\n# Maps the length of a digest to a possible hash function producing this digest\nHASHFUNC_MAP = {32: md5, 40: sha1, 64: sha256}\n\n\ndef _const_compare_digest_backport(a, b):\n    \"\"\"\n    Compare two digests of equal length in constant time.\n\n    The digests must be of type str/bytes.\n    Returns True if the digests match, and False otherwise.\n    \"\"\"\n    result = abs(len(a) - len(b))\n    for left, right in zip(bytearray(a), bytearray(b)):\n        result |= left ^ right\n    return result == 0\n\n\n_const_compare_digest = getattr(hmac, \"compare_digest\", _const_compare_digest_backport)\n\ntry:  # Test for SSL features\n    import ssl\n    from ssl import CERT_REQUIRED, wrap_socket\nexcept ImportError:\n    pass\n\ntry:\n    from ssl import HAS_SNI  # Has SNI?\nexcept ImportError:\n    pass\n\ntry:\n    from .ssltransport import SSLTransport\nexcept ImportError:\n    pass\n\n\ntry:  # Platform-specific: Python 3.6\n    from ssl import PROTOCOL_TLS\n\n    PROTOCOL_SSLv23 = PROTOCOL_TLS\nexcept ImportError:\n    try:\n        from ssl import PROTOCOL_SSLv23 as PROTOCOL_TLS\n\n        PROTOCOL_SSLv23 = PROTOCOL_TLS\n    except ImportError:\n        PROTOCOL_SSLv23 = PROTOCOL_TLS = 2\n\ntry:\n    from ssl import PROTOCOL_TLS_CLIENT\nexcept ImportError:\n    PROTOCOL_TLS_CLIENT = PROTOCOL_TLS\n\n\ntry:\n    from ssl import OP_NO_COMPRESSION, OP_NO_SSLv2, OP_NO_SSLv3\nexcept ImportError:\n    OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000\n    OP_NO_COMPRESSION = 0x20000\n\n\ntry:  # OP_NO_TICKET was added in Python 3.6\n    from ssl import OP_NO_TICKET\nexcept ImportError:\n    OP_NO_TICKET = 0x4000\n\n\n# A secure default.\n# Sources for more information on TLS ciphers:\n#\n# - https://wiki.mozilla.org/Security/Server_Side_TLS\n# - https://www.ssllabs.com/projects/best-practices/index.html\n# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/\n#\n# The general intent is:\n# - prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE),\n# - prefer ECDHE over DHE for better performance,\n# - prefer any AES-GCM and ChaCha20 over any AES-CBC for better performance and\n#   security,\n# - prefer AES-GCM over ChaCha20 because hardware-accelerated AES is common,\n# - disable NULL authentication, MD5 MACs, DSS, and other\n#   insecure ciphers for security reasons.\n# - NOTE: TLS 1.3 cipher suites are managed through a different interface\n#   not exposed by CPython (yet!) and are enabled by default if they're available.\nDEFAULT_CIPHERS = \":\".join(\n    [\n        \"ECDHE+AESGCM\",\n        \"ECDHE+CHACHA20\",\n        \"DHE+AESGCM\",\n        \"DHE+CHACHA20\",\n        \"ECDH+AESGCM\",\n        \"DH+AESGCM\",\n        \"ECDH+AES\",\n        \"DH+AES\",\n        \"RSA+AESGCM\",\n        \"RSA+AES\",\n        \"!aNULL\",\n        \"!eNULL\",\n        \"!MD5\",\n        \"!DSS\",\n    ]\n)\n\ntry:\n    from ssl import SSLContext  # Modern SSL?\nexcept ImportError:\n\n    class SSLContext(object):  # Platform-specific: Python 2\n        def __init__(self, protocol_version):\n            self.protocol = protocol_version\n            # Use default values from a real SSLContext\n            self.check_hostname = False\n            self.verify_mode = ssl.CERT_NONE\n            self.ca_certs = None\n            self.options = 0\n            self.certfile = None\n            self.keyfile = None\n            self.ciphers = None\n\n        def load_cert_chain(self, certfile, keyfile):\n            self.certfile = certfile\n            self.keyfile = keyfile\n\n        def load_verify_locations(self, cafile=None, capath=None, cadata=None):\n            self.ca_certs = cafile\n\n            if capath is not None:\n                raise SSLError(\"CA directories not supported in older Pythons\")\n\n            if cadata is not None:\n                raise SSLError(\"CA data not supported in older Pythons\")\n\n        def set_ciphers(self, cipher_suite):\n            self.ciphers = cipher_suite\n\n        def wrap_socket(self, socket, server_hostname=None, server_side=False):\n            warnings.warn(\n                \"A true SSLContext object is not available. This prevents \"\n                \"urllib3 from configuring SSL appropriately and may cause \"\n                \"certain SSL connections to fail. You can upgrade to a newer \"\n                \"version of Python to solve this. For more information, see \"\n                \"https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html\"\n                \"#ssl-warnings\",\n                InsecurePlatformWarning,\n            )\n            kwargs = {\n                \"keyfile\": self.keyfile,\n                \"certfile\": self.certfile,\n                \"ca_certs\": self.ca_certs,\n                \"cert_reqs\": self.verify_mode,\n                \"ssl_version\": self.protocol,\n                \"server_side\": server_side,\n            }\n            return wrap_socket(socket, ciphers=self.ciphers, **kwargs)\n\n\ndef assert_fingerprint(cert, fingerprint):\n    \"\"\"\n    Checks if given fingerprint matches the supplied certificate.\n\n    :param cert:\n        Certificate as bytes object.\n    :param fingerprint:\n        Fingerprint as string of hexdigits, can be interspersed by colons.\n    \"\"\"\n\n    fingerprint = fingerprint.replace(\":\", \"\").lower()\n    digest_length = len(fingerprint)\n    hashfunc = HASHFUNC_MAP.get(digest_length)\n    if not hashfunc:\n        raise SSLError(\"Fingerprint of invalid length: {0}\".format(fingerprint))\n\n    # We need encode() here for py32; works on py2 and p33.\n    fingerprint_bytes = unhexlify(fingerprint.encode())\n\n    cert_digest = hashfunc(cert).digest()\n\n    if not _const_compare_digest(cert_digest, fingerprint_bytes):\n        raise SSLError(\n            'Fingerprints did not match. Expected \"{0}\", got \"{1}\".'.format(\n                fingerprint, hexlify(cert_digest)\n            )\n        )\n\n\ndef resolve_cert_reqs(candidate):\n    \"\"\"\n    Resolves the argument to a numeric constant, which can be passed to\n    the wrap_socket function/method from the ssl module.\n    Defaults to :data:`ssl.CERT_REQUIRED`.\n    If given a string it is assumed to be the name of the constant in the\n    :mod:`ssl` module or its abbreviation.\n    (So you can specify `REQUIRED` instead of `CERT_REQUIRED`.\n    If it's neither `None` nor a string we assume it is already the numeric\n    constant which can directly be passed to wrap_socket.\n    \"\"\"\n    if candidate is None:\n        return CERT_REQUIRED\n\n    if isinstance(candidate, str):\n        res = getattr(ssl, candidate, None)\n        if res is None:\n            res = getattr(ssl, \"CERT_\" + candidate)\n        return res\n\n    return candidate\n\n\ndef resolve_ssl_version(candidate):\n    \"\"\"\n    like resolve_cert_reqs\n    \"\"\"\n    if candidate is None:\n        return PROTOCOL_TLS\n\n    if isinstance(candidate, str):\n        res = getattr(ssl, candidate, None)\n        if res is None:\n            res = getattr(ssl, \"PROTOCOL_\" + candidate)\n        return res\n\n    return candidate\n\n\ndef create_urllib3_context(\n    ssl_version=None, cert_reqs=None, options=None, ciphers=None\n):\n    \"\"\"All arguments have the same meaning as ``ssl_wrap_socket``.\n\n    By default, this function does a lot of the same work that\n    ``ssl.create_default_context`` does on Python 3.4+. It:\n\n    - Disables SSLv2, SSLv3, and compression\n    - Sets a restricted set of server ciphers\n\n    If you wish to enable SSLv3, you can do::\n\n        from urllib3.util import ssl_\n        context = ssl_.create_urllib3_context()\n        context.options &= ~ssl_.OP_NO_SSLv3\n\n    You can do the same to enable compression (substituting ``COMPRESSION``\n    for ``SSLv3`` in the last line above).\n\n    :param ssl_version:\n        The desired protocol version to use. This will default to\n        PROTOCOL_SSLv23 which will negotiate the highest protocol that both\n        the server and your installation of OpenSSL support.\n    :param cert_reqs:\n        Whether to require the certificate verification. This defaults to\n        ``ssl.CERT_REQUIRED``.\n    :param options:\n        Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``,\n        ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``.\n    :param ciphers:\n        Which cipher suites to allow the server to select.\n    :returns:\n        Constructed SSLContext object with specified options\n    :rtype: SSLContext\n    \"\"\"\n    # PROTOCOL_TLS is deprecated in Python 3.10\n    if not ssl_version or ssl_version == PROTOCOL_TLS:\n        ssl_version = PROTOCOL_TLS_CLIENT\n\n    context = SSLContext(ssl_version)\n\n    context.set_ciphers(ciphers or DEFAULT_CIPHERS)\n\n    # Setting the default here, as we may have no ssl module on import\n    cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs\n\n    if options is None:\n        options = 0\n        # SSLv2 is easily broken and is considered harmful and dangerous\n        options |= OP_NO_SSLv2\n        # SSLv3 has several problems and is now dangerous\n        options |= OP_NO_SSLv3\n        # Disable compression to prevent CRIME attacks for OpenSSL 1.0+\n        # (issue #309)\n        options |= OP_NO_COMPRESSION\n        # TLSv1.2 only. Unless set explicitly, do not request tickets.\n        # This may save some bandwidth on wire, and although the ticket is encrypted,\n        # there is a risk associated with it being on wire,\n        # if the server is not rotating its ticketing keys properly.\n        options |= OP_NO_TICKET\n\n    context.options |= options\n\n    # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is\n    # necessary for conditional client cert authentication with TLS 1.3.\n    # The attribute is None for OpenSSL <= 1.1.0 or does not exist in older\n    # versions of Python.  We only enable on Python 3.7.4+ or if certificate\n    # verification is enabled to work around Python issue #37428\n    # See: https://bugs.python.org/issue37428\n    if (cert_reqs == ssl.CERT_REQUIRED or sys.version_info >= (3, 7, 4)) and getattr(\n        context, \"post_handshake_auth\", None\n    ) is not None:\n        context.post_handshake_auth = True\n\n    def disable_check_hostname():\n        if (\n            getattr(context, \"check_hostname\", None) is not None\n        ):  # Platform-specific: Python 3.2\n            # We do our own verification, including fingerprints and alternative\n            # hostnames. So disable it here\n            context.check_hostname = False\n\n    # The order of the below lines setting verify_mode and check_hostname\n    # matter due to safe-guards SSLContext has to prevent an SSLContext with\n    # check_hostname=True, verify_mode=NONE/OPTIONAL. This is made even more\n    # complex because we don't know whether PROTOCOL_TLS_CLIENT will be used\n    # or not so we don't know the initial state of the freshly created SSLContext.\n    if cert_reqs == ssl.CERT_REQUIRED:\n        context.verify_mode = cert_reqs\n        disable_check_hostname()\n    else:\n        disable_check_hostname()\n        context.verify_mode = cert_reqs\n\n    # Enable logging of TLS session keys via defacto standard environment variable\n    # 'SSLKEYLOGFILE', if the feature is available (Python 3.8+). Skip empty values.\n    if hasattr(context, \"keylog_filename\"):\n        sslkeylogfile = os.environ.get(\"SSLKEYLOGFILE\")\n        if sslkeylogfile:\n            context.keylog_filename = sslkeylogfile\n\n    return context\n\n\ndef ssl_wrap_socket(\n    sock,\n    keyfile=None,\n    certfile=None,\n    cert_reqs=None,\n    ca_certs=None,\n    server_hostname=None,\n    ssl_version=None,\n    ciphers=None,\n    ssl_context=None,\n    ca_cert_dir=None,\n    key_password=None,\n    ca_cert_data=None,\n    tls_in_tls=False,\n):\n    \"\"\"\n    All arguments except for server_hostname, ssl_context, and ca_cert_dir have\n    the same meaning as they do when using :func:`ssl.wrap_socket`.\n\n    :param server_hostname:\n        When SNI is supported, the expected hostname of the certificate\n    :param ssl_context:\n        A pre-made :class:`SSLContext` object. If none is provided, one will\n        be created using :func:`create_urllib3_context`.\n    :param ciphers:\n        A string of ciphers we wish the client to support.\n    :param ca_cert_dir:\n        A directory containing CA certificates in multiple separate files, as\n        supported by OpenSSL's -CApath flag or the capath argument to\n        SSLContext.load_verify_locations().\n    :param key_password:\n        Optional password if the keyfile is encrypted.\n    :param ca_cert_data:\n        Optional string containing CA certificates in PEM format suitable for\n        passing as the cadata parameter to SSLContext.load_verify_locations()\n    :param tls_in_tls:\n        Use SSLTransport to wrap the existing socket.\n    \"\"\"\n    context = ssl_context\n    if context is None:\n        # Note: This branch of code and all the variables in it are no longer\n        # used by urllib3 itself. We should consider deprecating and removing\n        # this code.\n        context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers)\n\n    if ca_certs or ca_cert_dir or ca_cert_data:\n        try:\n            context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data)\n        except (IOError, OSError) as e:\n            raise SSLError(e)\n\n    elif ssl_context is None and hasattr(context, \"load_default_certs\"):\n        # try to load OS default certs; works well on Windows (require Python3.4+)\n        context.load_default_certs()\n\n    # Attempt to detect if we get the goofy behavior of the\n    # keyfile being encrypted and OpenSSL asking for the\n    # passphrase via the terminal and instead error out.\n    if keyfile and key_password is None and _is_key_file_encrypted(keyfile):\n        raise SSLError(\"Client private key is encrypted, password is required\")\n\n    if certfile:\n        if key_password is None:\n            context.load_cert_chain(certfile, keyfile)\n        else:\n            context.load_cert_chain(certfile, keyfile, key_password)\n\n    try:\n        if hasattr(context, \"set_alpn_protocols\"):\n            context.set_alpn_protocols(ALPN_PROTOCOLS)\n    except NotImplementedError:  # Defensive: in CI, we always have set_alpn_protocols\n        pass\n\n    # If we detect server_hostname is an IP address then the SNI\n    # extension should not be used according to RFC3546 Section 3.1\n    use_sni_hostname = server_hostname and not is_ipaddress(server_hostname)\n    # SecureTransport uses server_hostname in certificate verification.\n    send_sni = (use_sni_hostname and HAS_SNI) or (\n        IS_SECURETRANSPORT and server_hostname\n    )\n    # Do not warn the user if server_hostname is an invalid SNI hostname.\n    if not HAS_SNI and use_sni_hostname:\n        warnings.warn(\n            \"An HTTPS request has been made, but the SNI (Server Name \"\n            \"Indication) extension to TLS is not available on this platform. \"\n            \"This may cause the server to present an incorrect TLS \"\n            \"certificate, which can cause validation failures. You can upgrade to \"\n            \"a newer version of Python to solve this. For more information, see \"\n            \"https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html\"\n            \"#ssl-warnings\",\n            SNIMissingWarning,\n        )\n\n    if send_sni:\n        ssl_sock = _ssl_wrap_socket_impl(\n            sock, context, tls_in_tls, server_hostname=server_hostname\n        )\n    else:\n        ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls)\n    return ssl_sock\n\n\ndef is_ipaddress(hostname):\n    \"\"\"Detects whether the hostname given is an IPv4 or IPv6 address.\n    Also detects IPv6 addresses with Zone IDs.\n\n    :param str hostname: Hostname to examine.\n    :return: True if the hostname is an IP address, False otherwise.\n    \"\"\"\n    if not six.PY2 and isinstance(hostname, bytes):\n        # IDN A-label bytes are ASCII compatible.\n        hostname = hostname.decode(\"ascii\")\n    return bool(IPV4_RE.match(hostname) or BRACELESS_IPV6_ADDRZ_RE.match(hostname))\n\n\ndef _is_key_file_encrypted(key_file):\n    \"\"\"Detects if a key file is encrypted or not.\"\"\"\n    with open(key_file, \"r\") as f:\n        for line in f:\n            # Look for Proc-Type: 4,ENCRYPTED\n            if \"ENCRYPTED\" in line:\n                return True\n\n    return False\n\n\ndef _ssl_wrap_socket_impl(sock, ssl_context, tls_in_tls, server_hostname=None):\n    if tls_in_tls:\n        if not SSLTransport:\n            # Import error, ssl is not available.\n            raise ProxySchemeUnsupported(\n                \"TLS in TLS requires support for the 'ssl' module\"\n            )\n\n        SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context)\n        return SSLTransport(sock, ssl_context, server_hostname)\n\n    if server_hostname:\n        return ssl_context.wrap_socket(sock, server_hostname=server_hostname)\n    else:\n        return ssl_context.wrap_socket(sock)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/ssl_match_hostname.py",
    "content": "\"\"\"The match_hostname() function from Python 3.3.3, essential when using SSL.\"\"\"\n\n# Note: This file is under the PSF license as the code comes from the python\n# stdlib.   http://docs.python.org/3/license.html\n\nimport re\nimport sys\n\n# ipaddress has been backported to 2.6+ in pypi.  If it is installed on the\n# system, use it to handle IPAddress ServerAltnames (this was added in\n# python-3.5) otherwise only do DNS matching.  This allows\n# util.ssl_match_hostname to continue to be used in Python 2.7.\ntry:\n    import ipaddress\nexcept ImportError:\n    ipaddress = None\n\n__version__ = \"3.5.0.1\"\n\n\nclass CertificateError(ValueError):\n    pass\n\n\ndef _dnsname_match(dn, hostname, max_wildcards=1):\n    \"\"\"Matching according to RFC 6125, section 6.4.3\n\n    http://tools.ietf.org/html/rfc6125#section-6.4.3\n    \"\"\"\n    pats = []\n    if not dn:\n        return False\n\n    # Ported from python3-syntax:\n    # leftmost, *remainder = dn.split(r'.')\n    parts = dn.split(r\".\")\n    leftmost = parts[0]\n    remainder = parts[1:]\n\n    wildcards = leftmost.count(\"*\")\n    if wildcards > max_wildcards:\n        # Issue #17980: avoid denials of service by refusing more\n        # than one wildcard per fragment.  A survey of established\n        # policy among SSL implementations showed it to be a\n        # reasonable choice.\n        raise CertificateError(\n            \"too many wildcards in certificate DNS name: \" + repr(dn)\n        )\n\n    # speed up common case w/o wildcards\n    if not wildcards:\n        return dn.lower() == hostname.lower()\n\n    # RFC 6125, section 6.4.3, subitem 1.\n    # The client SHOULD NOT attempt to match a presented identifier in which\n    # the wildcard character comprises a label other than the left-most label.\n    if leftmost == \"*\":\n        # When '*' is a fragment by itself, it matches a non-empty dotless\n        # fragment.\n        pats.append(\"[^.]+\")\n    elif leftmost.startswith(\"xn--\") or hostname.startswith(\"xn--\"):\n        # RFC 6125, section 6.4.3, subitem 3.\n        # The client SHOULD NOT attempt to match a presented identifier\n        # where the wildcard character is embedded within an A-label or\n        # U-label of an internationalized domain name.\n        pats.append(re.escape(leftmost))\n    else:\n        # Otherwise, '*' matches any dotless string, e.g. www*\n        pats.append(re.escape(leftmost).replace(r\"\\*\", \"[^.]*\"))\n\n    # add the remaining fragments, ignore any wildcards\n    for frag in remainder:\n        pats.append(re.escape(frag))\n\n    pat = re.compile(r\"\\A\" + r\"\\.\".join(pats) + r\"\\Z\", re.IGNORECASE)\n    return pat.match(hostname)\n\n\ndef _to_unicode(obj):\n    if isinstance(obj, str) and sys.version_info < (3,):\n        # ignored flake8 # F821 to support python 2.7 function\n        obj = unicode(obj, encoding=\"ascii\", errors=\"strict\")  # noqa: F821\n    return obj\n\n\ndef _ipaddress_match(ipname, host_ip):\n    \"\"\"Exact matching of IP addresses.\n\n    RFC 6125 explicitly doesn't define an algorithm for this\n    (section 1.7.2 - \"Out of Scope\").\n    \"\"\"\n    # OpenSSL may add a trailing newline to a subjectAltName's IP address\n    # Divergence from upstream: ipaddress can't handle byte str\n    ip = ipaddress.ip_address(_to_unicode(ipname).rstrip())\n    return ip == host_ip\n\n\ndef match_hostname(cert, hostname):\n    \"\"\"Verify that *cert* (in decoded format as returned by\n    SSLSocket.getpeercert()) matches the *hostname*.  RFC 2818 and RFC 6125\n    rules are followed, but IP addresses are not accepted for *hostname*.\n\n    CertificateError is raised on failure. On success, the function\n    returns nothing.\n    \"\"\"\n    if not cert:\n        raise ValueError(\n            \"empty or no certificate, match_hostname needs a \"\n            \"SSL socket or SSL context with either \"\n            \"CERT_OPTIONAL or CERT_REQUIRED\"\n        )\n    try:\n        # Divergence from upstream: ipaddress can't handle byte str\n        host_ip = ipaddress.ip_address(_to_unicode(hostname))\n    except (UnicodeError, ValueError):\n        # ValueError: Not an IP address (common case)\n        # UnicodeError: Divergence from upstream: Have to deal with ipaddress not taking\n        # byte strings.  addresses should be all ascii, so we consider it not\n        # an ipaddress in this case\n        host_ip = None\n    except AttributeError:\n        # Divergence from upstream: Make ipaddress library optional\n        if ipaddress is None:\n            host_ip = None\n        else:  # Defensive\n            raise\n    dnsnames = []\n    san = cert.get(\"subjectAltName\", ())\n    for key, value in san:\n        if key == \"DNS\":\n            if host_ip is None and _dnsname_match(value, hostname):\n                return\n            dnsnames.append(value)\n        elif key == \"IP Address\":\n            if host_ip is not None and _ipaddress_match(value, host_ip):\n                return\n            dnsnames.append(value)\n    if not dnsnames:\n        # The subject is only checked when there is no dNSName entry\n        # in subjectAltName\n        for sub in cert.get(\"subject\", ()):\n            for key, value in sub:\n                # XXX according to RFC 2818, the most specific Common Name\n                # must be used.\n                if key == \"commonName\":\n                    if _dnsname_match(value, hostname):\n                        return\n                    dnsnames.append(value)\n    if len(dnsnames) > 1:\n        raise CertificateError(\n            \"hostname %r \"\n            \"doesn't match either of %s\" % (hostname, \", \".join(map(repr, dnsnames)))\n        )\n    elif len(dnsnames) == 1:\n        raise CertificateError(\"hostname %r doesn't match %r\" % (hostname, dnsnames[0]))\n    else:\n        raise CertificateError(\n            \"no appropriate commonName or subjectAltName fields were found\"\n        )\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/ssltransport.py",
    "content": "import io\nimport socket\nimport ssl\n\nfrom ..exceptions import ProxySchemeUnsupported\nfrom ..packages import six\n\nSSL_BLOCKSIZE = 16384\n\n\nclass SSLTransport:\n    \"\"\"\n    The SSLTransport wraps an existing socket and establishes an SSL connection.\n\n    Contrary to Python's implementation of SSLSocket, it allows you to chain\n    multiple TLS connections together. It's particularly useful if you need to\n    implement TLS within TLS.\n\n    The class supports most of the socket API operations.\n    \"\"\"\n\n    @staticmethod\n    def _validate_ssl_context_for_tls_in_tls(ssl_context):\n        \"\"\"\n        Raises a ProxySchemeUnsupported if the provided ssl_context can't be used\n        for TLS in TLS.\n\n        The only requirement is that the ssl_context provides the 'wrap_bio'\n        methods.\n        \"\"\"\n\n        if not hasattr(ssl_context, \"wrap_bio\"):\n            if six.PY2:\n                raise ProxySchemeUnsupported(\n                    \"TLS in TLS requires SSLContext.wrap_bio() which isn't \"\n                    \"supported on Python 2\"\n                )\n            else:\n                raise ProxySchemeUnsupported(\n                    \"TLS in TLS requires SSLContext.wrap_bio() which isn't \"\n                    \"available on non-native SSLContext\"\n                )\n\n    def __init__(\n        self, socket, ssl_context, server_hostname=None, suppress_ragged_eofs=True\n    ):\n        \"\"\"\n        Create an SSLTransport around socket using the provided ssl_context.\n        \"\"\"\n        self.incoming = ssl.MemoryBIO()\n        self.outgoing = ssl.MemoryBIO()\n\n        self.suppress_ragged_eofs = suppress_ragged_eofs\n        self.socket = socket\n\n        self.sslobj = ssl_context.wrap_bio(\n            self.incoming, self.outgoing, server_hostname=server_hostname\n        )\n\n        # Perform initial handshake.\n        self._ssl_io_loop(self.sslobj.do_handshake)\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, *_):\n        self.close()\n\n    def fileno(self):\n        return self.socket.fileno()\n\n    def read(self, len=1024, buffer=None):\n        return self._wrap_ssl_read(len, buffer)\n\n    def recv(self, len=1024, flags=0):\n        if flags != 0:\n            raise ValueError(\"non-zero flags not allowed in calls to recv\")\n        return self._wrap_ssl_read(len)\n\n    def recv_into(self, buffer, nbytes=None, flags=0):\n        if flags != 0:\n            raise ValueError(\"non-zero flags not allowed in calls to recv_into\")\n        if buffer and (nbytes is None):\n            nbytes = len(buffer)\n        elif nbytes is None:\n            nbytes = 1024\n        return self.read(nbytes, buffer)\n\n    def sendall(self, data, flags=0):\n        if flags != 0:\n            raise ValueError(\"non-zero flags not allowed in calls to sendall\")\n        count = 0\n        with memoryview(data) as view, view.cast(\"B\") as byte_view:\n            amount = len(byte_view)\n            while count < amount:\n                v = self.send(byte_view[count:])\n                count += v\n\n    def send(self, data, flags=0):\n        if flags != 0:\n            raise ValueError(\"non-zero flags not allowed in calls to send\")\n        response = self._ssl_io_loop(self.sslobj.write, data)\n        return response\n\n    def makefile(\n        self, mode=\"r\", buffering=None, encoding=None, errors=None, newline=None\n    ):\n        \"\"\"\n        Python's httpclient uses makefile and buffered io when reading HTTP\n        messages and we need to support it.\n\n        This is unfortunately a copy and paste of socket.py makefile with small\n        changes to point to the socket directly.\n        \"\"\"\n        if not set(mode) <= {\"r\", \"w\", \"b\"}:\n            raise ValueError(\"invalid mode %r (only r, w, b allowed)\" % (mode,))\n\n        writing = \"w\" in mode\n        reading = \"r\" in mode or not writing\n        assert reading or writing\n        binary = \"b\" in mode\n        rawmode = \"\"\n        if reading:\n            rawmode += \"r\"\n        if writing:\n            rawmode += \"w\"\n        raw = socket.SocketIO(self, rawmode)\n        self.socket._io_refs += 1\n        if buffering is None:\n            buffering = -1\n        if buffering < 0:\n            buffering = io.DEFAULT_BUFFER_SIZE\n        if buffering == 0:\n            if not binary:\n                raise ValueError(\"unbuffered streams must be binary\")\n            return raw\n        if reading and writing:\n            buffer = io.BufferedRWPair(raw, raw, buffering)\n        elif reading:\n            buffer = io.BufferedReader(raw, buffering)\n        else:\n            assert writing\n            buffer = io.BufferedWriter(raw, buffering)\n        if binary:\n            return buffer\n        text = io.TextIOWrapper(buffer, encoding, errors, newline)\n        text.mode = mode\n        return text\n\n    def unwrap(self):\n        self._ssl_io_loop(self.sslobj.unwrap)\n\n    def close(self):\n        self.socket.close()\n\n    def getpeercert(self, binary_form=False):\n        return self.sslobj.getpeercert(binary_form)\n\n    def version(self):\n        return self.sslobj.version()\n\n    def cipher(self):\n        return self.sslobj.cipher()\n\n    def selected_alpn_protocol(self):\n        return self.sslobj.selected_alpn_protocol()\n\n    def selected_npn_protocol(self):\n        return self.sslobj.selected_npn_protocol()\n\n    def shared_ciphers(self):\n        return self.sslobj.shared_ciphers()\n\n    def compression(self):\n        return self.sslobj.compression()\n\n    def settimeout(self, value):\n        self.socket.settimeout(value)\n\n    def gettimeout(self):\n        return self.socket.gettimeout()\n\n    def _decref_socketios(self):\n        self.socket._decref_socketios()\n\n    def _wrap_ssl_read(self, len, buffer=None):\n        try:\n            return self._ssl_io_loop(self.sslobj.read, len, buffer)\n        except ssl.SSLError as e:\n            if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs:\n                return 0  # eof, return 0.\n            else:\n                raise\n\n    def _ssl_io_loop(self, func, *args):\n        \"\"\"Performs an I/O loop between incoming/outgoing and the socket.\"\"\"\n        should_loop = True\n        ret = None\n\n        while should_loop:\n            errno = None\n            try:\n                ret = func(*args)\n            except ssl.SSLError as e:\n                if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE):\n                    # WANT_READ, and WANT_WRITE are expected, others are not.\n                    raise e\n                errno = e.errno\n\n            buf = self.outgoing.read()\n            self.socket.sendall(buf)\n\n            if errno is None:\n                should_loop = False\n            elif errno == ssl.SSL_ERROR_WANT_READ:\n                buf = self.socket.recv(SSL_BLOCKSIZE)\n                if buf:\n                    self.incoming.write(buf)\n                else:\n                    self.incoming.write_eof()\n        return ret\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/timeout.py",
    "content": "from __future__ import absolute_import\n\nimport time\n\n# The default socket timeout, used by httplib to indicate that no timeout was\n# specified by the user\nfrom socket import _GLOBAL_DEFAULT_TIMEOUT\n\nfrom ..exceptions import TimeoutStateError\n\n# A sentinel value to indicate that no timeout was specified by the user in\n# urllib3\n_Default = object()\n\n\n# Use time.monotonic if available.\ncurrent_time = getattr(time, \"monotonic\", time.time)\n\n\nclass Timeout(object):\n    \"\"\"Timeout configuration.\n\n    Timeouts can be defined as a default for a pool:\n\n    .. code-block:: python\n\n       timeout = Timeout(connect=2.0, read=7.0)\n       http = PoolManager(timeout=timeout)\n       response = http.request('GET', 'http://example.com/')\n\n    Or per-request (which overrides the default for the pool):\n\n    .. code-block:: python\n\n       response = http.request('GET', 'http://example.com/', timeout=Timeout(10))\n\n    Timeouts can be disabled by setting all the parameters to ``None``:\n\n    .. code-block:: python\n\n       no_timeout = Timeout(connect=None, read=None)\n       response = http.request('GET', 'http://example.com/, timeout=no_timeout)\n\n\n    :param total:\n        This combines the connect and read timeouts into one; the read timeout\n        will be set to the time leftover from the connect attempt. In the\n        event that both a connect timeout and a total are specified, or a read\n        timeout and a total are specified, the shorter timeout will be applied.\n\n        Defaults to None.\n\n    :type total: int, float, or None\n\n    :param connect:\n        The maximum amount of time (in seconds) to wait for a connection\n        attempt to a server to succeed. Omitting the parameter will default the\n        connect timeout to the system default, probably `the global default\n        timeout in socket.py\n        <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.\n        None will set an infinite timeout for connection attempts.\n\n    :type connect: int, float, or None\n\n    :param read:\n        The maximum amount of time (in seconds) to wait between consecutive\n        read operations for a response from the server. Omitting the parameter\n        will default the read timeout to the system default, probably `the\n        global default timeout in socket.py\n        <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.\n        None will set an infinite timeout.\n\n    :type read: int, float, or None\n\n    .. note::\n\n        Many factors can affect the total amount of time for urllib3 to return\n        an HTTP response.\n\n        For example, Python's DNS resolver does not obey the timeout specified\n        on the socket. Other factors that can affect total request time include\n        high CPU load, high swap, the program running at a low priority level,\n        or other behaviors.\n\n        In addition, the read and total timeouts only measure the time between\n        read operations on the socket connecting the client and the server,\n        not the total amount of time for the request to return a complete\n        response. For most requests, the timeout is raised because the server\n        has not sent the first byte in the specified time. This is not always\n        the case; if a server streams one byte every fifteen seconds, a timeout\n        of 20 seconds will not trigger, even though the request will take\n        several minutes to complete.\n\n        If your goal is to cut off any request after a set amount of wall clock\n        time, consider having a second \"watcher\" thread to cut off a slow\n        request.\n    \"\"\"\n\n    #: A sentinel object representing the default timeout value\n    DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT\n\n    def __init__(self, total=None, connect=_Default, read=_Default):\n        self._connect = self._validate_timeout(connect, \"connect\")\n        self._read = self._validate_timeout(read, \"read\")\n        self.total = self._validate_timeout(total, \"total\")\n        self._start_connect = None\n\n    def __repr__(self):\n        return \"%s(connect=%r, read=%r, total=%r)\" % (\n            type(self).__name__,\n            self._connect,\n            self._read,\n            self.total,\n        )\n\n    # __str__ provided for backwards compatibility\n    __str__ = __repr__\n\n    @classmethod\n    def _validate_timeout(cls, value, name):\n        \"\"\"Check that a timeout attribute is valid.\n\n        :param value: The timeout value to validate\n        :param name: The name of the timeout attribute to validate. This is\n            used to specify in error messages.\n        :return: The validated and casted version of the given value.\n        :raises ValueError: If it is a numeric value less than or equal to\n            zero, or the type is not an integer, float, or None.\n        \"\"\"\n        if value is _Default:\n            return cls.DEFAULT_TIMEOUT\n\n        if value is None or value is cls.DEFAULT_TIMEOUT:\n            return value\n\n        if isinstance(value, bool):\n            raise ValueError(\n                \"Timeout cannot be a boolean value. It must \"\n                \"be an int, float or None.\"\n            )\n        try:\n            float(value)\n        except (TypeError, ValueError):\n            raise ValueError(\n                \"Timeout value %s was %s, but it must be an \"\n                \"int, float or None.\" % (name, value)\n            )\n\n        try:\n            if value <= 0:\n                raise ValueError(\n                    \"Attempted to set %s timeout to %s, but the \"\n                    \"timeout cannot be set to a value less \"\n                    \"than or equal to 0.\" % (name, value)\n                )\n        except TypeError:\n            # Python 3\n            raise ValueError(\n                \"Timeout value %s was %s, but it must be an \"\n                \"int, float or None.\" % (name, value)\n            )\n\n        return value\n\n    @classmethod\n    def from_float(cls, timeout):\n        \"\"\"Create a new Timeout from a legacy timeout value.\n\n        The timeout value used by httplib.py sets the same timeout on the\n        connect(), and recv() socket requests. This creates a :class:`Timeout`\n        object that sets the individual timeouts to the ``timeout`` value\n        passed to this function.\n\n        :param timeout: The legacy timeout value.\n        :type timeout: integer, float, sentinel default object, or None\n        :return: Timeout object\n        :rtype: :class:`Timeout`\n        \"\"\"\n        return Timeout(read=timeout, connect=timeout)\n\n    def clone(self):\n        \"\"\"Create a copy of the timeout object\n\n        Timeout properties are stored per-pool but each request needs a fresh\n        Timeout object to ensure each one has its own start/stop configured.\n\n        :return: a copy of the timeout object\n        :rtype: :class:`Timeout`\n        \"\"\"\n        # We can't use copy.deepcopy because that will also create a new object\n        # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to\n        # detect the user default.\n        return Timeout(connect=self._connect, read=self._read, total=self.total)\n\n    def start_connect(self):\n        \"\"\"Start the timeout clock, used during a connect() attempt\n\n        :raises urllib3.exceptions.TimeoutStateError: if you attempt\n            to start a timer that has been started already.\n        \"\"\"\n        if self._start_connect is not None:\n            raise TimeoutStateError(\"Timeout timer has already been started.\")\n        self._start_connect = current_time()\n        return self._start_connect\n\n    def get_connect_duration(self):\n        \"\"\"Gets the time elapsed since the call to :meth:`start_connect`.\n\n        :return: Elapsed time in seconds.\n        :rtype: float\n        :raises urllib3.exceptions.TimeoutStateError: if you attempt\n            to get duration for a timer that hasn't been started.\n        \"\"\"\n        if self._start_connect is None:\n            raise TimeoutStateError(\n                \"Can't get connect duration for timer that has not started.\"\n            )\n        return current_time() - self._start_connect\n\n    @property\n    def connect_timeout(self):\n        \"\"\"Get the value to use when setting a connection timeout.\n\n        This will be a positive float or integer, the value None\n        (never timeout), or the default system timeout.\n\n        :return: Connect timeout.\n        :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None\n        \"\"\"\n        if self.total is None:\n            return self._connect\n\n        if self._connect is None or self._connect is self.DEFAULT_TIMEOUT:\n            return self.total\n\n        return min(self._connect, self.total)\n\n    @property\n    def read_timeout(self):\n        \"\"\"Get the value for the read timeout.\n\n        This assumes some time has elapsed in the connection timeout and\n        computes the read timeout appropriately.\n\n        If self.total is set, the read timeout is dependent on the amount of\n        time taken by the connect timeout. If the connection time has not been\n        established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be\n        raised.\n\n        :return: Value to use for the read timeout.\n        :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None\n        :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect`\n            has not yet been called on this object.\n        \"\"\"\n        if (\n            self.total is not None\n            and self.total is not self.DEFAULT_TIMEOUT\n            and self._read is not None\n            and self._read is not self.DEFAULT_TIMEOUT\n        ):\n            # In case the connect timeout has not yet been established.\n            if self._start_connect is None:\n                return self._read\n            return max(0, min(self.total - self.get_connect_duration(), self._read))\n        elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT:\n            return max(0, self.total - self.get_connect_duration())\n        else:\n            return self._read\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/url.py",
    "content": "from __future__ import absolute_import\n\nimport re\nfrom collections import namedtuple\n\nfrom ..exceptions import LocationParseError\nfrom ..packages import six\n\nurl_attrs = [\"scheme\", \"auth\", \"host\", \"port\", \"path\", \"query\", \"fragment\"]\n\n# We only want to normalize urls with an HTTP(S) scheme.\n# urllib3 infers URLs without a scheme (None) to be http.\nNORMALIZABLE_SCHEMES = (\"http\", \"https\", None)\n\n# Almost all of these patterns were derived from the\n# 'rfc3986' module: https://github.com/python-hyper/rfc3986\nPERCENT_RE = re.compile(r\"%[a-fA-F0-9]{2}\")\nSCHEME_RE = re.compile(r\"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)\")\nURI_RE = re.compile(\n    r\"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?\"\n    r\"(?://([^\\\\/?#]*))?\"\n    r\"([^?#]*)\"\n    r\"(?:\\?([^#]*))?\"\n    r\"(?:#(.*))?$\",\n    re.UNICODE | re.DOTALL,\n)\n\nIPV4_PAT = r\"(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\"\nHEX_PAT = \"[0-9A-Fa-f]{1,4}\"\nLS32_PAT = \"(?:{hex}:{hex}|{ipv4})\".format(hex=HEX_PAT, ipv4=IPV4_PAT)\n_subs = {\"hex\": HEX_PAT, \"ls32\": LS32_PAT}\n_variations = [\n    #                            6( h16 \":\" ) ls32\n    \"(?:%(hex)s:){6}%(ls32)s\",\n    #                       \"::\" 5( h16 \":\" ) ls32\n    \"::(?:%(hex)s:){5}%(ls32)s\",\n    # [               h16 ] \"::\" 4( h16 \":\" ) ls32\n    \"(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s\",\n    # [ *1( h16 \":\" ) h16 ] \"::\" 3( h16 \":\" ) ls32\n    \"(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s\",\n    # [ *2( h16 \":\" ) h16 ] \"::\" 2( h16 \":\" ) ls32\n    \"(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s\",\n    # [ *3( h16 \":\" ) h16 ] \"::\"    h16 \":\"   ls32\n    \"(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s\",\n    # [ *4( h16 \":\" ) h16 ] \"::\"              ls32\n    \"(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s\",\n    # [ *5( h16 \":\" ) h16 ] \"::\"              h16\n    \"(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s\",\n    # [ *6( h16 \":\" ) h16 ] \"::\"\n    \"(?:(?:%(hex)s:){0,6}%(hex)s)?::\",\n]\n\nUNRESERVED_PAT = r\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._!\\-~\"\nIPV6_PAT = \"(?:\" + \"|\".join([x % _subs for x in _variations]) + \")\"\nZONE_ID_PAT = \"(?:%25|%)(?:[\" + UNRESERVED_PAT + \"]|%[a-fA-F0-9]{2})+\"\nIPV6_ADDRZ_PAT = r\"\\[\" + IPV6_PAT + r\"(?:\" + ZONE_ID_PAT + r\")?\\]\"\nREG_NAME_PAT = r\"(?:[^\\[\\]%:/?#]|%[a-fA-F0-9]{2})*\"\nTARGET_RE = re.compile(r\"^(/[^?#]*)(?:\\?([^#]*))?(?:#.*)?$\")\n\nIPV4_RE = re.compile(\"^\" + IPV4_PAT + \"$\")\nIPV6_RE = re.compile(\"^\" + IPV6_PAT + \"$\")\nIPV6_ADDRZ_RE = re.compile(\"^\" + IPV6_ADDRZ_PAT + \"$\")\nBRACELESS_IPV6_ADDRZ_RE = re.compile(\"^\" + IPV6_ADDRZ_PAT[2:-2] + \"$\")\nZONE_ID_RE = re.compile(\"(\" + ZONE_ID_PAT + r\")\\]$\")\n\n_HOST_PORT_PAT = (\"^(%s|%s|%s)(?::([0-9]{0,5}))?$\") % (\n    REG_NAME_PAT,\n    IPV4_PAT,\n    IPV6_ADDRZ_PAT,\n)\n_HOST_PORT_RE = re.compile(_HOST_PORT_PAT, re.UNICODE | re.DOTALL)\n\nUNRESERVED_CHARS = set(\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-~\"\n)\nSUB_DELIM_CHARS = set(\"!$&'()*+,;=\")\nUSERINFO_CHARS = UNRESERVED_CHARS | SUB_DELIM_CHARS | {\":\"}\nPATH_CHARS = USERINFO_CHARS | {\"@\", \"/\"}\nQUERY_CHARS = FRAGMENT_CHARS = PATH_CHARS | {\"?\"}\n\n\nclass Url(namedtuple(\"Url\", url_attrs)):\n    \"\"\"\n    Data structure for representing an HTTP URL. Used as a return value for\n    :func:`parse_url`. Both the scheme and host are normalized as they are\n    both case-insensitive according to RFC 3986.\n    \"\"\"\n\n    __slots__ = ()\n\n    def __new__(\n        cls,\n        scheme=None,\n        auth=None,\n        host=None,\n        port=None,\n        path=None,\n        query=None,\n        fragment=None,\n    ):\n        if path and not path.startswith(\"/\"):\n            path = \"/\" + path\n        if scheme is not None:\n            scheme = scheme.lower()\n        return super(Url, cls).__new__(\n            cls, scheme, auth, host, port, path, query, fragment\n        )\n\n    @property\n    def hostname(self):\n        \"\"\"For backwards-compatibility with urlparse. We're nice like that.\"\"\"\n        return self.host\n\n    @property\n    def request_uri(self):\n        \"\"\"Absolute path including the query string.\"\"\"\n        uri = self.path or \"/\"\n\n        if self.query is not None:\n            uri += \"?\" + self.query\n\n        return uri\n\n    @property\n    def netloc(self):\n        \"\"\"Network location including host and port\"\"\"\n        if self.port:\n            return \"%s:%d\" % (self.host, self.port)\n        return self.host\n\n    @property\n    def url(self):\n        \"\"\"\n        Convert self into a url\n\n        This function should more or less round-trip with :func:`.parse_url`. The\n        returned url may not be exactly the same as the url inputted to\n        :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls\n        with a blank port will have : removed).\n\n        Example: ::\n\n            >>> U = parse_url('http://google.com/mail/')\n            >>> U.url\n            'http://google.com/mail/'\n            >>> Url('http', 'username:password', 'host.com', 80,\n            ... '/path', 'query', 'fragment').url\n            'http://username:password@host.com:80/path?query#fragment'\n        \"\"\"\n        scheme, auth, host, port, path, query, fragment = self\n        url = u\"\"\n\n        # We use \"is not None\" we want things to happen with empty strings (or 0 port)\n        if scheme is not None:\n            url += scheme + u\"://\"\n        if auth is not None:\n            url += auth + u\"@\"\n        if host is not None:\n            url += host\n        if port is not None:\n            url += u\":\" + str(port)\n        if path is not None:\n            url += path\n        if query is not None:\n            url += u\"?\" + query\n        if fragment is not None:\n            url += u\"#\" + fragment\n\n        return url\n\n    def __str__(self):\n        return self.url\n\n\ndef split_first(s, delims):\n    \"\"\"\n    .. deprecated:: 1.25\n\n    Given a string and an iterable of delimiters, split on the first found\n    delimiter. Return two split parts and the matched delimiter.\n\n    If not found, then the first part is the full input string.\n\n    Example::\n\n        >>> split_first('foo/bar?baz', '?/=')\n        ('foo', 'bar?baz', '/')\n        >>> split_first('foo/bar?baz', '123')\n        ('foo/bar?baz', '', None)\n\n    Scales linearly with number of delims. Not ideal for large number of delims.\n    \"\"\"\n    min_idx = None\n    min_delim = None\n    for d in delims:\n        idx = s.find(d)\n        if idx < 0:\n            continue\n\n        if min_idx is None or idx < min_idx:\n            min_idx = idx\n            min_delim = d\n\n    if min_idx is None or min_idx < 0:\n        return s, \"\", None\n\n    return s[:min_idx], s[min_idx + 1 :], min_delim\n\n\ndef _encode_invalid_chars(component, allowed_chars, encoding=\"utf-8\"):\n    \"\"\"Percent-encodes a URI component without reapplying\n    onto an already percent-encoded component.\n    \"\"\"\n    if component is None:\n        return component\n\n    component = six.ensure_text(component)\n\n    # Normalize existing percent-encoded bytes.\n    # Try to see if the component we're encoding is already percent-encoded\n    # so we can skip all '%' characters but still encode all others.\n    component, percent_encodings = PERCENT_RE.subn(\n        lambda match: match.group(0).upper(), component\n    )\n\n    uri_bytes = component.encode(\"utf-8\", \"surrogatepass\")\n    is_percent_encoded = percent_encodings == uri_bytes.count(b\"%\")\n    encoded_component = bytearray()\n\n    for i in range(0, len(uri_bytes)):\n        # Will return a single character bytestring on both Python 2 & 3\n        byte = uri_bytes[i : i + 1]\n        byte_ord = ord(byte)\n        if (is_percent_encoded and byte == b\"%\") or (\n            byte_ord < 128 and byte.decode() in allowed_chars\n        ):\n            encoded_component += byte\n            continue\n        encoded_component.extend(b\"%\" + (hex(byte_ord)[2:].encode().zfill(2).upper()))\n\n    return encoded_component.decode(encoding)\n\n\ndef _remove_path_dot_segments(path):\n    # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code\n    segments = path.split(\"/\")  # Turn the path into a list of segments\n    output = []  # Initialize the variable to use to store output\n\n    for segment in segments:\n        # '.' is the current directory, so ignore it, it is superfluous\n        if segment == \".\":\n            continue\n        # Anything other than '..', should be appended to the output\n        elif segment != \"..\":\n            output.append(segment)\n        # In this case segment == '..', if we can, we should pop the last\n        # element\n        elif output:\n            output.pop()\n\n    # If the path starts with '/' and the output is empty or the first string\n    # is non-empty\n    if path.startswith(\"/\") and (not output or output[0]):\n        output.insert(0, \"\")\n\n    # If the path starts with '/.' or '/..' ensure we add one more empty\n    # string to add a trailing '/'\n    if path.endswith((\"/.\", \"/..\")):\n        output.append(\"\")\n\n    return \"/\".join(output)\n\n\ndef _normalize_host(host, scheme):\n    if host:\n        if isinstance(host, six.binary_type):\n            host = six.ensure_str(host)\n\n        if scheme in NORMALIZABLE_SCHEMES:\n            is_ipv6 = IPV6_ADDRZ_RE.match(host)\n            if is_ipv6:\n                match = ZONE_ID_RE.search(host)\n                if match:\n                    start, end = match.span(1)\n                    zone_id = host[start:end]\n\n                    if zone_id.startswith(\"%25\") and zone_id != \"%25\":\n                        zone_id = zone_id[3:]\n                    else:\n                        zone_id = zone_id[1:]\n                    zone_id = \"%\" + _encode_invalid_chars(zone_id, UNRESERVED_CHARS)\n                    return host[:start].lower() + zone_id + host[end:]\n                else:\n                    return host.lower()\n            elif not IPV4_RE.match(host):\n                return six.ensure_str(\n                    b\".\".join([_idna_encode(label) for label in host.split(\".\")])\n                )\n    return host\n\n\ndef _idna_encode(name):\n    if name and any([ord(x) > 128 for x in name]):\n        try:\n            import idna\n        except ImportError:\n            six.raise_from(\n                LocationParseError(\"Unable to parse URL without the 'idna' module\"),\n                None,\n            )\n        try:\n            return idna.encode(name.lower(), strict=True, std3_rules=True)\n        except idna.IDNAError:\n            six.raise_from(\n                LocationParseError(u\"Name '%s' is not a valid IDNA label\" % name), None\n            )\n    return name.lower().encode(\"ascii\")\n\n\ndef _encode_target(target):\n    \"\"\"Percent-encodes a request target so that there are no invalid characters\"\"\"\n    path, query = TARGET_RE.match(target).groups()\n    target = _encode_invalid_chars(path, PATH_CHARS)\n    query = _encode_invalid_chars(query, QUERY_CHARS)\n    if query is not None:\n        target += \"?\" + query\n    return target\n\n\ndef parse_url(url):\n    \"\"\"\n    Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is\n    performed to parse incomplete urls. Fields not provided will be None.\n    This parser is RFC 3986 compliant.\n\n    The parser logic and helper functions are based heavily on\n    work done in the ``rfc3986`` module.\n\n    :param str url: URL to parse into a :class:`.Url` namedtuple.\n\n    Partly backwards-compatible with :mod:`urlparse`.\n\n    Example::\n\n        >>> parse_url('http://google.com/mail/')\n        Url(scheme='http', host='google.com', port=None, path='/mail/', ...)\n        >>> parse_url('google.com:80')\n        Url(scheme=None, host='google.com', port=80, path=None, ...)\n        >>> parse_url('/foo?bar')\n        Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...)\n    \"\"\"\n    if not url:\n        # Empty\n        return Url()\n\n    source_url = url\n    if not SCHEME_RE.search(url):\n        url = \"//\" + url\n\n    try:\n        scheme, authority, path, query, fragment = URI_RE.match(url).groups()\n        normalize_uri = scheme is None or scheme.lower() in NORMALIZABLE_SCHEMES\n\n        if scheme:\n            scheme = scheme.lower()\n\n        if authority:\n            auth, _, host_port = authority.rpartition(\"@\")\n            auth = auth or None\n            host, port = _HOST_PORT_RE.match(host_port).groups()\n            if auth and normalize_uri:\n                auth = _encode_invalid_chars(auth, USERINFO_CHARS)\n            if port == \"\":\n                port = None\n        else:\n            auth, host, port = None, None, None\n\n        if port is not None:\n            port = int(port)\n            if not (0 <= port <= 65535):\n                raise LocationParseError(url)\n\n        host = _normalize_host(host, scheme)\n\n        if normalize_uri and path:\n            path = _remove_path_dot_segments(path)\n            path = _encode_invalid_chars(path, PATH_CHARS)\n        if normalize_uri and query:\n            query = _encode_invalid_chars(query, QUERY_CHARS)\n        if normalize_uri and fragment:\n            fragment = _encode_invalid_chars(fragment, FRAGMENT_CHARS)\n\n    except (ValueError, AttributeError):\n        return six.raise_from(LocationParseError(source_url), None)\n\n    # For the sake of backwards compatibility we put empty\n    # string values for path if there are any defined values\n    # beyond the path in the URL.\n    # TODO: Remove this when we break backwards compatibility.\n    if not path:\n        if query is not None or fragment is not None:\n            path = \"\"\n        else:\n            path = None\n\n    # Ensure that each part of the URL is a `str` for\n    # backwards compatibility.\n    if isinstance(url, six.text_type):\n        ensure_func = six.ensure_text\n    else:\n        ensure_func = six.ensure_str\n\n    def ensure_type(x):\n        return x if x is None else ensure_func(x)\n\n    return Url(\n        scheme=ensure_type(scheme),\n        auth=ensure_type(auth),\n        host=ensure_type(host),\n        port=port,\n        path=ensure_type(path),\n        query=ensure_type(query),\n        fragment=ensure_type(fragment),\n    )\n\n\ndef get_host(url):\n    \"\"\"\n    Deprecated. Use :func:`parse_url` instead.\n    \"\"\"\n    p = parse_url(url)\n    return p.scheme or \"http\", p.hostname, p.port\n"
  },
  {
    "path": "openpype/vendor/python/python_2/urllib3/util/wait.py",
    "content": "import errno\nimport select\nimport sys\nfrom functools import partial\n\ntry:\n    from time import monotonic\nexcept ImportError:\n    from time import time as monotonic\n\n__all__ = [\"NoWayToWaitForSocketError\", \"wait_for_read\", \"wait_for_write\"]\n\n\nclass NoWayToWaitForSocketError(Exception):\n    pass\n\n\n# How should we wait on sockets?\n#\n# There are two types of APIs you can use for waiting on sockets: the fancy\n# modern stateful APIs like epoll/kqueue, and the older stateless APIs like\n# select/poll. The stateful APIs are more efficient when you have a lots of\n# sockets to keep track of, because you can set them up once and then use them\n# lots of times. But we only ever want to wait on a single socket at a time\n# and don't want to keep track of state, so the stateless APIs are actually\n# more efficient. So we want to use select() or poll().\n#\n# Now, how do we choose between select() and poll()? On traditional Unixes,\n# select() has a strange calling convention that makes it slow, or fail\n# altogether, for high-numbered file descriptors. The point of poll() is to fix\n# that, so on Unixes, we prefer poll().\n#\n# On Windows, there is no poll() (or at least Python doesn't provide a wrapper\n# for it), but that's OK, because on Windows, select() doesn't have this\n# strange calling convention; plain select() works fine.\n#\n# So: on Windows we use select(), and everywhere else we use poll(). We also\n# fall back to select() in case poll() is somehow broken or missing.\n\nif sys.version_info >= (3, 5):\n    # Modern Python, that retries syscalls by default\n    def _retry_on_intr(fn, timeout):\n        return fn(timeout)\n\n\nelse:\n    # Old and broken Pythons.\n    def _retry_on_intr(fn, timeout):\n        if timeout is None:\n            deadline = float(\"inf\")\n        else:\n            deadline = monotonic() + timeout\n\n        while True:\n            try:\n                return fn(timeout)\n            # OSError for 3 <= pyver < 3.5, select.error for pyver <= 2.7\n            except (OSError, select.error) as e:\n                # 'e.args[0]' incantation works for both OSError and select.error\n                if e.args[0] != errno.EINTR:\n                    raise\n                else:\n                    timeout = deadline - monotonic()\n                    if timeout < 0:\n                        timeout = 0\n                    if timeout == float(\"inf\"):\n                        timeout = None\n                    continue\n\n\ndef select_wait_for_socket(sock, read=False, write=False, timeout=None):\n    if not read and not write:\n        raise RuntimeError(\"must specify at least one of read=True, write=True\")\n    rcheck = []\n    wcheck = []\n    if read:\n        rcheck.append(sock)\n    if write:\n        wcheck.append(sock)\n    # When doing a non-blocking connect, most systems signal success by\n    # marking the socket writable. Windows, though, signals success by marked\n    # it as \"exceptional\". We paper over the difference by checking the write\n    # sockets for both conditions. (The stdlib selectors module does the same\n    # thing.)\n    fn = partial(select.select, rcheck, wcheck, wcheck)\n    rready, wready, xready = _retry_on_intr(fn, timeout)\n    return bool(rready or wready or xready)\n\n\ndef poll_wait_for_socket(sock, read=False, write=False, timeout=None):\n    if not read and not write:\n        raise RuntimeError(\"must specify at least one of read=True, write=True\")\n    mask = 0\n    if read:\n        mask |= select.POLLIN\n    if write:\n        mask |= select.POLLOUT\n    poll_obj = select.poll()\n    poll_obj.register(sock, mask)\n\n    # For some reason, poll() takes timeout in milliseconds\n    def do_poll(t):\n        if t is not None:\n            t *= 1000\n        return poll_obj.poll(t)\n\n    return bool(_retry_on_intr(do_poll, timeout))\n\n\ndef null_wait_for_socket(*args, **kwargs):\n    raise NoWayToWaitForSocketError(\"no select-equivalent available\")\n\n\ndef _have_working_poll():\n    # Apparently some systems have a select.poll that fails as soon as you try\n    # to use it, either due to strange configuration or broken monkeypatching\n    # from libraries like eventlet/greenlet.\n    try:\n        poll_obj = select.poll()\n        _retry_on_intr(poll_obj.poll, 0)\n    except (AttributeError, OSError):\n        return False\n    else:\n        return True\n\n\ndef wait_for_socket(*args, **kwargs):\n    # We delay choosing which implementation to use until the first time we're\n    # called. We could do it at import time, but then we might make the wrong\n    # decision if someone goes wild with monkeypatching select.poll after\n    # we're imported.\n    global wait_for_socket\n    if _have_working_poll():\n        wait_for_socket = poll_wait_for_socket\n    elif hasattr(select, \"select\"):\n        wait_for_socket = select_wait_for_socket\n    else:  # Platform-specific: Appengine.\n        wait_for_socket = null_wait_for_socket\n    return wait_for_socket(*args, **kwargs)\n\n\ndef wait_for_read(sock, timeout=None):\n    \"\"\"Waits for reading to be available on a given socket.\n    Returns True if the socket is readable, or False if the timeout expired.\n    \"\"\"\n    return wait_for_socket(sock, read=True, timeout=timeout)\n\n\ndef wait_for_write(sock, timeout=None):\n    \"\"\"Waits for writing to be available on a given socket.\n    Returns True if the socket is readable, or False if the timeout expired.\n    \"\"\"\n    return wait_for_socket(sock, write=True, timeout=timeout)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/__init__.py",
    "content": "\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\nfrom ._abnf import *\nfrom ._app import WebSocketApp\nfrom ._core import *\nfrom ._exceptions import *\nfrom ._logging import *\nfrom ._socket import *\n\n__version__ = \"0.59.0\"\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/_abnf.py",
    "content": "\"\"\"\n\n\"\"\"\n\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\nimport array\nimport os\nimport struct\n\nimport six\n\nfrom ._exceptions import *\nfrom ._utils import validate_utf8\nfrom threading import Lock\n\ntry:\n    if six.PY3:\n        import numpy\n    else:\n        numpy = None\nexcept ImportError:\n    numpy = None\n\ntry:\n    # If wsaccel is available we use compiled routines to mask data.\n    if not numpy:\n        from wsaccel.xormask import XorMaskerSimple\n\n        def _mask(_m, _d):\n            return XorMaskerSimple(_m).process(_d)\nexcept ImportError:\n    # wsaccel is not available, we rely on python implementations.\n    def _mask(_m, _d):\n        for i in range(len(_d)):\n            _d[i] ^= _m[i % 4]\n\n        if six.PY3:\n            return _d.tobytes()\n        else:\n            return _d.tostring()\n\n\n__all__ = [\n    'ABNF', 'continuous_frame', 'frame_buffer',\n    'STATUS_NORMAL',\n    'STATUS_GOING_AWAY',\n    'STATUS_PROTOCOL_ERROR',\n    'STATUS_UNSUPPORTED_DATA_TYPE',\n    'STATUS_STATUS_NOT_AVAILABLE',\n    'STATUS_ABNORMAL_CLOSED',\n    'STATUS_INVALID_PAYLOAD',\n    'STATUS_POLICY_VIOLATION',\n    'STATUS_MESSAGE_TOO_BIG',\n    'STATUS_INVALID_EXTENSION',\n    'STATUS_UNEXPECTED_CONDITION',\n    'STATUS_BAD_GATEWAY',\n    'STATUS_TLS_HANDSHAKE_ERROR',\n]\n\n# closing frame status codes.\nSTATUS_NORMAL = 1000\nSTATUS_GOING_AWAY = 1001\nSTATUS_PROTOCOL_ERROR = 1002\nSTATUS_UNSUPPORTED_DATA_TYPE = 1003\nSTATUS_STATUS_NOT_AVAILABLE = 1005\nSTATUS_ABNORMAL_CLOSED = 1006\nSTATUS_INVALID_PAYLOAD = 1007\nSTATUS_POLICY_VIOLATION = 1008\nSTATUS_MESSAGE_TOO_BIG = 1009\nSTATUS_INVALID_EXTENSION = 1010\nSTATUS_UNEXPECTED_CONDITION = 1011\nSTATUS_BAD_GATEWAY = 1014\nSTATUS_TLS_HANDSHAKE_ERROR = 1015\n\nVALID_CLOSE_STATUS = (\n    STATUS_NORMAL,\n    STATUS_GOING_AWAY,\n    STATUS_PROTOCOL_ERROR,\n    STATUS_UNSUPPORTED_DATA_TYPE,\n    STATUS_INVALID_PAYLOAD,\n    STATUS_POLICY_VIOLATION,\n    STATUS_MESSAGE_TOO_BIG,\n    STATUS_INVALID_EXTENSION,\n    STATUS_UNEXPECTED_CONDITION,\n    STATUS_BAD_GATEWAY,\n)\n\n\nclass ABNF(object):\n    \"\"\"\n    ABNF frame class.\n    See http://tools.ietf.org/html/rfc5234\n    and http://tools.ietf.org/html/rfc6455#section-5.2\n    \"\"\"\n\n    # operation code values.\n    OPCODE_CONT = 0x0\n    OPCODE_TEXT = 0x1\n    OPCODE_BINARY = 0x2\n    OPCODE_CLOSE = 0x8\n    OPCODE_PING = 0x9\n    OPCODE_PONG = 0xa\n\n    # available operation code value tuple\n    OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,\n               OPCODE_PING, OPCODE_PONG)\n\n    # opcode human readable string\n    OPCODE_MAP = {\n        OPCODE_CONT: \"cont\",\n        OPCODE_TEXT: \"text\",\n        OPCODE_BINARY: \"binary\",\n        OPCODE_CLOSE: \"close\",\n        OPCODE_PING: \"ping\",\n        OPCODE_PONG: \"pong\"\n    }\n\n    # data length threshold.\n    LENGTH_7 = 0x7e\n    LENGTH_16 = 1 << 16\n    LENGTH_63 = 1 << 63\n\n    def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0,\n                 opcode=OPCODE_TEXT, mask=1, data=\"\"):\n        \"\"\"\n        Constructor for ABNF. Please check RFC for arguments.\n        \"\"\"\n        self.fin = fin\n        self.rsv1 = rsv1\n        self.rsv2 = rsv2\n        self.rsv3 = rsv3\n        self.opcode = opcode\n        self.mask = mask\n        if data is None:\n            data = \"\"\n        self.data = data\n        self.get_mask_key = os.urandom\n\n    def validate(self, skip_utf8_validation=False):\n        \"\"\"\n        Validate the ABNF frame.\n\n        Parameters\n        ----------\n        skip_utf8_validation: skip utf8 validation.\n        \"\"\"\n        if self.rsv1 or self.rsv2 or self.rsv3:\n            raise WebSocketProtocolException(\"rsv is not implemented, yet\")\n\n        if self.opcode not in ABNF.OPCODES:\n            raise WebSocketProtocolException(\"Invalid opcode %r\", self.opcode)\n\n        if self.opcode == ABNF.OPCODE_PING and not self.fin:\n            raise WebSocketProtocolException(\"Invalid ping frame.\")\n\n        if self.opcode == ABNF.OPCODE_CLOSE:\n            l = len(self.data)\n            if not l:\n                return\n            if l == 1 or l >= 126:\n                raise WebSocketProtocolException(\"Invalid close frame.\")\n            if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]):\n                raise WebSocketProtocolException(\"Invalid close frame.\")\n\n            code = 256 * \\\n                six.byte2int(self.data[0:1]) + six.byte2int(self.data[1:2])\n            if not self._is_valid_close_status(code):\n                raise WebSocketProtocolException(\"Invalid close opcode.\")\n\n    @staticmethod\n    def _is_valid_close_status(code):\n        return code in VALID_CLOSE_STATUS or (3000 <= code < 5000)\n\n    def __str__(self):\n        return \"fin=\" + str(self.fin) \\\n            + \" opcode=\" + str(self.opcode) \\\n            + \" data=\" + str(self.data)\n\n    @staticmethod\n    def create_frame(data, opcode, fin=1):\n        \"\"\"\n        Create frame to send text, binary and other data.\n\n        Parameters\n        ----------\n        data: <type>\n            data to send. This is string value(byte array).\n            If opcode is OPCODE_TEXT and this value is unicode,\n            data value is converted into unicode string, automatically.\n        opcode: <type>\n            operation code. please see OPCODE_XXX.\n        fin: <type>\n            fin flag. if set to 0, create continue fragmentation.\n        \"\"\"\n        if opcode == ABNF.OPCODE_TEXT and isinstance(data, six.text_type):\n            data = data.encode(\"utf-8\")\n        # mask must be set if send data from client\n        return ABNF(fin, 0, 0, 0, opcode, 1, data)\n\n    def format(self):\n        \"\"\"\n        Format this object to string(byte array) to send data to server.\n        \"\"\"\n        if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):\n            raise ValueError(\"not 0 or 1\")\n        if self.opcode not in ABNF.OPCODES:\n            raise ValueError(\"Invalid OPCODE\")\n        length = len(self.data)\n        if length >= ABNF.LENGTH_63:\n            raise ValueError(\"data is too long\")\n\n        frame_header = chr(self.fin << 7 |\n                           self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 |\n                           self.opcode)\n        if length < ABNF.LENGTH_7:\n            frame_header += chr(self.mask << 7 | length)\n            frame_header = six.b(frame_header)\n        elif length < ABNF.LENGTH_16:\n            frame_header += chr(self.mask << 7 | 0x7e)\n            frame_header = six.b(frame_header)\n            frame_header += struct.pack(\"!H\", length)\n        else:\n            frame_header += chr(self.mask << 7 | 0x7f)\n            frame_header = six.b(frame_header)\n            frame_header += struct.pack(\"!Q\", length)\n\n        if not self.mask:\n            return frame_header + self.data\n        else:\n            mask_key = self.get_mask_key(4)\n            return frame_header + self._get_masked(mask_key)\n\n    def _get_masked(self, mask_key):\n        s = ABNF.mask(mask_key, self.data)\n\n        if isinstance(mask_key, six.text_type):\n            mask_key = mask_key.encode('utf-8')\n\n        return mask_key + s\n\n    @staticmethod\n    def mask(mask_key, data):\n        \"\"\"\n        Mask or unmask data. Just do xor for each byte\n\n        Parameters\n        ----------\n        mask_key: <type>\n            4 byte string(byte).\n        data: <type>\n            data to mask/unmask.\n        \"\"\"\n        if data is None:\n            data = \"\"\n\n        if isinstance(mask_key, six.text_type):\n            mask_key = six.b(mask_key)\n\n        if isinstance(data, six.text_type):\n            data = six.b(data)\n\n        if numpy:\n            origlen = len(data)\n            _mask_key = mask_key[3] << 24 | mask_key[2] << 16 | mask_key[1] << 8 | mask_key[0]\n\n            # We need data to be a multiple of four...\n            data += bytes(\" \" * (4 - (len(data) % 4)), \"us-ascii\")\n            a = numpy.frombuffer(data, dtype=\"uint32\")\n            masked = numpy.bitwise_xor(a, [_mask_key]).astype(\"uint32\")\n            if len(data) > origlen:\n                return masked.tobytes()[:origlen]\n            return masked.tobytes()\n        else:\n            _m = array.array(\"B\", mask_key)\n            _d = array.array(\"B\", data)\n            return _mask(_m, _d)\n\n\nclass frame_buffer(object):\n    _HEADER_MASK_INDEX = 5\n    _HEADER_LENGTH_INDEX = 6\n\n    def __init__(self, recv_fn, skip_utf8_validation):\n        self.recv = recv_fn\n        self.skip_utf8_validation = skip_utf8_validation\n        # Buffers over the packets from the layer beneath until desired amount\n        # bytes of bytes are received.\n        self.recv_buffer = []\n        self.clear()\n        self.lock = Lock()\n\n    def clear(self):\n        self.header = None\n        self.length = None\n        self.mask = None\n\n    def has_received_header(self):\n        return self.header is None\n\n    def recv_header(self):\n        header = self.recv_strict(2)\n        b1 = header[0]\n\n        if six.PY2:\n            b1 = ord(b1)\n\n        fin = b1 >> 7 & 1\n        rsv1 = b1 >> 6 & 1\n        rsv2 = b1 >> 5 & 1\n        rsv3 = b1 >> 4 & 1\n        opcode = b1 & 0xf\n        b2 = header[1]\n\n        if six.PY2:\n            b2 = ord(b2)\n\n        has_mask = b2 >> 7 & 1\n        length_bits = b2 & 0x7f\n\n        self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits)\n\n    def has_mask(self):\n        if not self.header:\n            return False\n        return self.header[frame_buffer._HEADER_MASK_INDEX]\n\n    def has_received_length(self):\n        return self.length is None\n\n    def recv_length(self):\n        bits = self.header[frame_buffer._HEADER_LENGTH_INDEX]\n        length_bits = bits & 0x7f\n        if length_bits == 0x7e:\n            v = self.recv_strict(2)\n            self.length = struct.unpack(\"!H\", v)[0]\n        elif length_bits == 0x7f:\n            v = self.recv_strict(8)\n            self.length = struct.unpack(\"!Q\", v)[0]\n        else:\n            self.length = length_bits\n\n    def has_received_mask(self):\n        return self.mask is None\n\n    def recv_mask(self):\n        self.mask = self.recv_strict(4) if self.has_mask() else \"\"\n\n    def recv_frame(self):\n\n        with self.lock:\n            # Header\n            if self.has_received_header():\n                self.recv_header()\n            (fin, rsv1, rsv2, rsv3, opcode, has_mask, _) = self.header\n\n            # Frame length\n            if self.has_received_length():\n                self.recv_length()\n            length = self.length\n\n            # Mask\n            if self.has_received_mask():\n                self.recv_mask()\n            mask = self.mask\n\n            # Payload\n            payload = self.recv_strict(length)\n            if has_mask:\n                payload = ABNF.mask(mask, payload)\n\n            # Reset for next frame\n            self.clear()\n\n            frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)\n            frame.validate(self.skip_utf8_validation)\n\n        return frame\n\n    def recv_strict(self, bufsize):\n        shortage = bufsize - sum(len(x) for x in self.recv_buffer)\n        while shortage > 0:\n            # Limit buffer size that we pass to socket.recv() to avoid\n            # fragmenting the heap -- the number of bytes recv() actually\n            # reads is limited by socket buffer and is relatively small,\n            # yet passing large numbers repeatedly causes lots of large\n            # buffers allocated and then shrunk, which results in\n            # fragmentation.\n            bytes_ = self.recv(min(16384, shortage))\n            self.recv_buffer.append(bytes_)\n            shortage -= len(bytes_)\n\n        unified = six.b(\"\").join(self.recv_buffer)\n\n        if shortage == 0:\n            self.recv_buffer = []\n            return unified\n        else:\n            self.recv_buffer = [unified[bufsize:]]\n            return unified[:bufsize]\n\n\nclass continuous_frame(object):\n\n    def __init__(self, fire_cont_frame, skip_utf8_validation):\n        self.fire_cont_frame = fire_cont_frame\n        self.skip_utf8_validation = skip_utf8_validation\n        self.cont_data = None\n        self.recving_frames = None\n\n    def validate(self, frame):\n        if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT:\n            raise WebSocketProtocolException(\"Illegal frame\")\n        if self.recving_frames and \\\n                frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):\n            raise WebSocketProtocolException(\"Illegal frame\")\n\n    def add(self, frame):\n        if self.cont_data:\n            self.cont_data[1] += frame.data\n        else:\n            if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):\n                self.recving_frames = frame.opcode\n            self.cont_data = [frame.opcode, frame.data]\n\n        if frame.fin:\n            self.recving_frames = None\n\n    def is_fire(self, frame):\n        return frame.fin or self.fire_cont_frame\n\n    def extract(self, frame):\n        data = self.cont_data\n        self.cont_data = None\n        frame.data = data[1]\n        if not self.fire_cont_frame and data[0] == ABNF.OPCODE_TEXT and not self.skip_utf8_validation and not validate_utf8(frame.data):\n            raise WebSocketPayloadException(\n                \"cannot decode: \" + repr(frame.data))\n\n        return [data[0], frame]\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/_app.py",
    "content": "\"\"\"\n\n\"\"\"\n\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\nimport inspect\nimport select\nimport sys\nimport threading\nimport time\nimport traceback\n\nimport six\n\nfrom ._abnf import ABNF\nfrom ._core import WebSocket, getdefaulttimeout\nfrom ._exceptions import *\nfrom . import _logging\n\n\n__all__ = [\"WebSocketApp\"]\n\n\nclass Dispatcher:\n    \"\"\"\n    Dispatcher\n    \"\"\"\n    def __init__(self, app, ping_timeout):\n        self.app = app\n        self.ping_timeout = ping_timeout\n\n    def read(self, sock, read_callback, check_callback):\n        while self.app.keep_running:\n            r, w, e = select.select(\n                (self.app.sock.sock, ), (), (), self.ping_timeout)\n            if r:\n                if not read_callback():\n                    break\n            check_callback()\n\n\nclass SSLDispatcher:\n    \"\"\"\n    SSLDispatcher\n    \"\"\"\n    def __init__(self, app, ping_timeout):\n        self.app = app\n        self.ping_timeout = ping_timeout\n\n    def read(self, sock, read_callback, check_callback):\n        while self.app.keep_running:\n            r = self.select()\n            if r:\n                if not read_callback():\n                    break\n            check_callback()\n\n    def select(self):\n        sock = self.app.sock.sock\n        if sock.pending():\n            return [sock,]\n\n        r, w, e = select.select((sock, ), (), (), self.ping_timeout)\n        return r\n\n\nclass WebSocketApp(object):\n    \"\"\"\n    Higher level of APIs are provided. The interface is like JavaScript WebSocket object.\n    \"\"\"\n\n    def __init__(self, url, header=None,\n                 on_open=None, on_message=None, on_error=None,\n                 on_close=None, on_ping=None, on_pong=None,\n                 on_cont_message=None,\n                 keep_running=True, get_mask_key=None, cookie=None,\n                 subprotocols=None,\n                 on_data=None):\n        \"\"\"\n        WebSocketApp initialization\n\n        Parameters\n        ----------\n        url: <type>\n            websocket url.\n        header: list or dict\n            custom header for websocket handshake.\n        on_open: <type>\n            callable object which is called at opening websocket.\n            this function has one argument. The argument is this class object.\n        on_message: <type>\n            callable object which is called when received data.\n            on_message has 2 arguments.\n            The 1st argument is this class object.\n            The 2nd argument is utf-8 string which we get from the server.\n        on_error: <type>\n            callable object which is called when we get error.\n            on_error has 2 arguments.\n            The 1st argument is this class object.\n            The 2nd argument is exception object.\n        on_close: <type>\n            callable object which is called when closed the connection.\n            this function has one argument. The argument is this class object.\n        on_cont_message: <type>\n            callback object which is called when receive continued\n            frame data.\n            on_cont_message has 3 arguments.\n            The 1st argument is this class object.\n            The 2nd argument is utf-8 string which we get from the server.\n            The 3rd argument is continue flag. if 0, the data continue\n            to next frame data\n        on_data: <type>\n            callback object which is called when a message received.\n            This is called before on_message or on_cont_message,\n            and then on_message or on_cont_message is called.\n            on_data has 4 argument.\n            The 1st argument is this class object.\n            The 2nd argument is utf-8 string which we get from the server.\n            The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came.\n            The 4th argument is continue flag. if 0, the data continue\n        keep_running: <type>\n            this parameter is obsolete and ignored.\n        get_mask_key: func\n            a callable to produce new mask keys,\n            see the WebSocket.set_mask_key's docstring for more information\n        cookie: str\n            cookie value.\n        subprotocols: <type>\n            array of available sub protocols. default is None.\n        \"\"\"\n        self.url = url\n        self.header = header if header is not None else []\n        self.cookie = cookie\n\n        self.on_open = on_open\n        self.on_message = on_message\n        self.on_data = on_data\n        self.on_error = on_error\n        self.on_close = on_close\n        self.on_ping = on_ping\n        self.on_pong = on_pong\n        self.on_cont_message = on_cont_message\n        self.keep_running = False\n        self.get_mask_key = get_mask_key\n        self.sock = None\n        self.last_ping_tm = 0\n        self.last_pong_tm = 0\n        self.subprotocols = subprotocols\n\n    def send(self, data, opcode=ABNF.OPCODE_TEXT):\n        \"\"\"\n        send message\n\n        Parameters\n        ----------\n        data: <type>\n            Message to send. If you set opcode to OPCODE_TEXT,\n            data must be utf-8 string or unicode.\n        opcode: <type>\n            Operation code of data. default is OPCODE_TEXT.\n        \"\"\"\n\n        if not self.sock or self.sock.send(data, opcode) == 0:\n            raise WebSocketConnectionClosedException(\n                \"Connection is already closed.\")\n\n    def close(self, **kwargs):\n        \"\"\"\n        Close websocket connection.\n        \"\"\"\n        self.keep_running = False\n        if self.sock:\n            self.sock.close(**kwargs)\n            self.sock = None\n\n    def _send_ping(self, interval, event, payload):\n        while not event.wait(interval):\n            self.last_ping_tm = time.time()\n            if self.sock:\n                try:\n                    self.sock.ping(payload)\n                except Exception as ex:\n                    _logging.warning(\"send_ping routine terminated: {}\".format(ex))\n                    break\n\n    def run_forever(self, sockopt=None, sslopt=None,\n                    ping_interval=0, ping_timeout=None,\n                    ping_payload=\"\",\n                    http_proxy_host=None, http_proxy_port=None,\n                    http_no_proxy=None, http_proxy_auth=None,\n                    skip_utf8_validation=False,\n                    host=None, origin=None, dispatcher=None,\n                    suppress_origin=False, proxy_type=None):\n        \"\"\"\n        Run event loop for WebSocket framework.\n\n        This loop is an infinite loop and is alive while websocket is available.\n\n        Parameters\n        ----------\n        sockopt: tuple\n            values for socket.setsockopt.\n            sockopt must be tuple\n            and each element is argument of sock.setsockopt.\n        sslopt: dict\n            optional dict object for ssl socket option.\n        ping_interval: int or float\n            automatically send \"ping\" command\n            every specified period (in seconds)\n            if set to 0, not send automatically.\n        ping_timeout: int or float\n            timeout (in seconds) if the pong message is not received.\n        ping_payload: str\n            payload message to send with each ping.\n        http_proxy_host: <type>\n            http proxy host name.\n        http_proxy_port: <type>\n            http proxy port. If not set, set to 80.\n        http_no_proxy: <type>\n            host names, which doesn't use proxy.\n        skip_utf8_validation: bool\n            skip utf8 validation.\n        host: str\n            update host header.\n        origin: str\n            update origin header.\n        dispatcher: <type>\n            customize reading data from socket.\n        suppress_origin: bool\n            suppress outputting origin header.\n\n        Returns\n        -------\n        teardown: bool\n            False if caught KeyboardInterrupt, True if other exception was raised during a loop\n        \"\"\"\n\n        if ping_timeout is not None and ping_timeout <= 0:\n            ping_timeout = None\n        if ping_timeout and ping_interval and ping_interval <= ping_timeout:\n            raise WebSocketException(\"Ensure ping_interval > ping_timeout\")\n        if not sockopt:\n            sockopt = []\n        if not sslopt:\n            sslopt = {}\n        if self.sock:\n            raise WebSocketException(\"socket is already opened\")\n        thread = None\n        self.keep_running = True\n        self.last_ping_tm = 0\n        self.last_pong_tm = 0\n\n        def teardown(close_frame=None):\n            \"\"\"\n            Tears down the connection.\n\n            If close_frame is set, we will invoke the on_close handler with the\n            statusCode and reason from there.\n            \"\"\"\n            if thread and thread.is_alive():\n                event.set()\n                thread.join()\n            self.keep_running = False\n            if self.sock:\n                self.sock.close()\n            close_args = self._get_close_args(\n                close_frame.data if close_frame else None)\n            self._callback(self.on_close, *close_args)\n            self.sock = None\n\n        try:\n            self.sock = WebSocket(\n                self.get_mask_key, sockopt=sockopt, sslopt=sslopt,\n                fire_cont_frame=self.on_cont_message is not None,\n                skip_utf8_validation=skip_utf8_validation,\n                enable_multithread=True if ping_interval else False)\n            self.sock.settimeout(getdefaulttimeout())\n            self.sock.connect(\n                self.url, header=self.header, cookie=self.cookie,\n                http_proxy_host=http_proxy_host,\n                http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy,\n                http_proxy_auth=http_proxy_auth, subprotocols=self.subprotocols,\n                host=host, origin=origin, suppress_origin=suppress_origin,\n                proxy_type=proxy_type)\n            if not dispatcher:\n                dispatcher = self.create_dispatcher(ping_timeout)\n\n            self._callback(self.on_open)\n\n            if ping_interval:\n                event = threading.Event()\n                thread = threading.Thread(\n                    target=self._send_ping, args=(ping_interval, event, ping_payload))\n                thread.daemon = True\n                thread.start()\n\n            def read():\n                if not self.keep_running:\n                    return teardown()\n\n                op_code, frame = self.sock.recv_data_frame(True)\n                if op_code == ABNF.OPCODE_CLOSE:\n                    return teardown(frame)\n                elif op_code == ABNF.OPCODE_PING:\n                    self._callback(self.on_ping, frame.data)\n                elif op_code == ABNF.OPCODE_PONG:\n                    self.last_pong_tm = time.time()\n                    self._callback(self.on_pong, frame.data)\n                elif op_code == ABNF.OPCODE_CONT and self.on_cont_message:\n                    self._callback(self.on_data, frame.data,\n                                   frame.opcode, frame.fin)\n                    self._callback(self.on_cont_message,\n                                   frame.data, frame.fin)\n                else:\n                    data = frame.data\n                    if six.PY3 and op_code == ABNF.OPCODE_TEXT:\n                        data = data.decode(\"utf-8\")\n                    self._callback(self.on_data, data, frame.opcode, True)\n                    self._callback(self.on_message, data)\n\n                return True\n\n            def check():\n                if (ping_timeout):\n                    has_timeout_expired = time.time() - self.last_ping_tm > ping_timeout\n                    has_pong_not_arrived_after_last_ping = self.last_pong_tm - self.last_ping_tm < 0\n                    has_pong_arrived_too_late = self.last_pong_tm - self.last_ping_tm > ping_timeout\n\n                    if (self.last_ping_tm and\n                            has_timeout_expired and\n                            (has_pong_not_arrived_after_last_ping or has_pong_arrived_too_late)):\n                        raise WebSocketTimeoutException(\"ping/pong timed out\")\n                return True\n\n            dispatcher.read(self.sock.sock, read, check)\n        except (Exception, KeyboardInterrupt, SystemExit) as e:\n            self._callback(self.on_error, e)\n            if isinstance(e, SystemExit):\n                # propagate SystemExit further\n                raise\n            teardown()\n            return not isinstance(e, KeyboardInterrupt)\n\n    def create_dispatcher(self, ping_timeout):\n        timeout = ping_timeout or 10\n        if self.sock.is_ssl():\n            return SSLDispatcher(self, timeout)\n\n        return Dispatcher(self, timeout)\n\n    def _get_close_args(self, data):\n        \"\"\"\n        _get_close_args extracts the code, reason from the close body\n        if they exists, and if the self.on_close except three arguments\n        \"\"\"\n        # if the on_close callback is \"old\", just return empty list\n        if sys.version_info < (3, 0):\n            if not self.on_close or len(inspect.getargspec(self.on_close).args) != 3:\n                return []\n        else:\n            if not self.on_close or len(inspect.getfullargspec(self.on_close).args) != 3:\n                return []\n\n        if data and len(data) >= 2:\n            code = 256 * six.byte2int(data[0:1]) + six.byte2int(data[1:2])\n            reason = data[2:].decode('utf-8')\n            return [code, reason]\n\n        return [None, None]\n\n    def _callback(self, callback, *args):\n        if callback:\n            try:\n                callback(self, *args)\n\n            except Exception as e:\n                _logging.error(\"error from callback {}: {}\".format(callback, e))\n                if _logging.isEnabledForDebug():\n                    _, _, tb = sys.exc_info()\n                    traceback.print_tb(tb)\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/_cookiejar.py",
    "content": "\"\"\"\n\n\"\"\"\n\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\ntry:\n    import Cookie\nexcept:\n    import http.cookies as Cookie\n\n\nclass SimpleCookieJar(object):\n    def __init__(self):\n        self.jar = dict()\n\n    def add(self, set_cookie):\n        if set_cookie:\n            try:\n                simpleCookie = Cookie.SimpleCookie(set_cookie)\n            except:\n                simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore'))\n\n            for k, v in simpleCookie.items():\n                domain = v.get(\"domain\")\n                if domain:\n                    if not domain.startswith(\".\"):\n                        domain = \".\" + domain\n                    cookie = self.jar.get(domain) if self.jar.get(domain) else Cookie.SimpleCookie()\n                    cookie.update(simpleCookie)\n                    self.jar[domain.lower()] = cookie\n\n    def set(self, set_cookie):\n        if set_cookie:\n            try:\n                simpleCookie = Cookie.SimpleCookie(set_cookie)\n            except:\n                simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore'))\n\n            for k, v in simpleCookie.items():\n                domain = v.get(\"domain\")\n                if domain:\n                    if not domain.startswith(\".\"):\n                        domain = \".\" + domain\n                    self.jar[domain.lower()] = simpleCookie\n\n    def get(self, host):\n        if not host:\n            return \"\"\n\n        cookies = []\n        for domain, simpleCookie in self.jar.items():\n            host = host.lower()\n            if host.endswith(domain) or host == domain[1:]:\n                cookies.append(self.jar.get(domain))\n\n        return \"; \".join(filter(\n            None, sorted(\n                [\"%s=%s\" % (k, v.value) for cookie in filter(None, cookies) for k, v in cookie.items()]\n            )))\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/_core.py",
    "content": "from __future__ import print_function\n\"\"\"\n_core.py\n====================================\nWebSocket Python client\n\"\"\"\n\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\nimport socket\nimport struct\nimport threading\nimport time\n\nimport six\n\n# websocket modules\nfrom ._abnf import *\nfrom ._exceptions import *\nfrom ._handshake import *\nfrom ._http import *\nfrom ._logging import *\nfrom ._socket import *\nfrom ._ssl_compat import *\nfrom ._utils import *\n\n__all__ = ['WebSocket', 'create_connection']\n\n\nclass WebSocket(object):\n    \"\"\"\n    Low level WebSocket interface.\n\n    This class is based on the WebSocket protocol `draft-hixie-thewebsocketprotocol-76 <http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76>`_\n\n    We can connect to the websocket server and send/receive data.\n    The following example is an echo client.\n\n    >>> import websocket\n    >>> ws = websocket.WebSocket()\n    >>> ws.connect(\"ws://echo.websocket.org\")\n    >>> ws.send(\"Hello, Server\")\n    >>> ws.recv()\n    'Hello, Server'\n    >>> ws.close()\n\n    Parameters\n    ----------\n    get_mask_key: func\n        a callable to produce new mask keys, see the set_mask_key\n        function's docstring for more details\n    sockopt: tuple\n        values for socket.setsockopt.\n        sockopt must be tuple and each element is argument of sock.setsockopt.\n    sslopt: dict\n        optional dict object for ssl socket option.\n    fire_cont_frame: bool\n        fire recv event for each cont frame. default is False\n    enable_multithread: bool\n        if set to True, lock send method.\n    skip_utf8_validation: bool\n        skip utf8 validation.\n    \"\"\"\n\n    def __init__(self, get_mask_key=None, sockopt=None, sslopt=None,\n                 fire_cont_frame=False, enable_multithread=False,\n                 skip_utf8_validation=False, **_):\n        \"\"\"\n        Initialize WebSocket object.\n\n        Parameters\n        ----------\n        sslopt: specify ssl certification verification options\n        \"\"\"\n        self.sock_opt = sock_opt(sockopt, sslopt)\n        self.handshake_response = None\n        self.sock = None\n\n        self.connected = False\n        self.get_mask_key = get_mask_key\n        # These buffer over the build-up of a single frame.\n        self.frame_buffer = frame_buffer(self._recv, skip_utf8_validation)\n        self.cont_frame = continuous_frame(\n            fire_cont_frame, skip_utf8_validation)\n\n        if enable_multithread:\n            self.lock = threading.Lock()\n            self.readlock = threading.Lock()\n        else:\n            self.lock = NoLock()\n            self.readlock = NoLock()\n\n    def __iter__(self):\n        \"\"\"\n        Allow iteration over websocket, implying sequential `recv` executions.\n        \"\"\"\n        while True:\n            yield self.recv()\n\n    def __next__(self):\n        return self.recv()\n\n    def next(self):\n        return self.__next__()\n\n    def fileno(self):\n        return self.sock.fileno()\n\n    def set_mask_key(self, func):\n        \"\"\"\n        Set function to create mask key. You can customize mask key generator.\n        Mainly, this is for testing purpose.\n\n        Parameters\n        ----------\n        func: func\n            callable object. the func takes 1 argument as integer.\n            The argument means length of mask key.\n            This func must return string(byte array),\n            which length is argument specified.\n        \"\"\"\n        self.get_mask_key = func\n\n    def gettimeout(self):\n        \"\"\"\n        Get the websocket timeout (in seconds) as an int or float\n\n        Returns\n        ----------\n        timeout: int or float\n             returns timeout value (in seconds). This value could be either float/integer.\n        \"\"\"\n        return self.sock_opt.timeout\n\n    def settimeout(self, timeout):\n        \"\"\"\n        Set the timeout to the websocket.\n\n        Parameters\n        ----------\n        timeout: int or float\n            timeout time (in seconds). This value could be either float/integer.\n        \"\"\"\n        self.sock_opt.timeout = timeout\n        if self.sock:\n            self.sock.settimeout(timeout)\n\n    timeout = property(gettimeout, settimeout)\n\n    def getsubprotocol(self):\n        \"\"\"\n        Get subprotocol\n        \"\"\"\n        if self.handshake_response:\n            return self.handshake_response.subprotocol\n        else:\n            return None\n\n    subprotocol = property(getsubprotocol)\n\n    def getstatus(self):\n        \"\"\"\n        Get handshake status\n        \"\"\"\n        if self.handshake_response:\n            return self.handshake_response.status\n        else:\n            return None\n\n    status = property(getstatus)\n\n    def getheaders(self):\n        \"\"\"\n        Get handshake response header\n        \"\"\"\n        if self.handshake_response:\n            return self.handshake_response.headers\n        else:\n            return None\n\n    def is_ssl(self):\n        return isinstance(self.sock, ssl.SSLSocket)\n\n    headers = property(getheaders)\n\n    def connect(self, url, **options):\n        \"\"\"\n        Connect to url. url is websocket url scheme.\n        ie. ws://host:port/resource\n        You can customize using 'options'.\n        If you set \"header\" list object, you can set your own custom header.\n\n        >>> ws = WebSocket()\n        >>> ws.connect(\"ws://echo.websocket.org/\",\n                ...     header=[\"User-Agent: MyProgram\",\n                ...             \"x-custom: header\"])\n\n        timeout: <type>\n            socket timeout time. This value is an integer or float.\n            if you set None for this value, it means \"use default_timeout value\"\n\n        Parameters\n        ----------\n        options:\n                 - header: list or dict\n                    custom http header list or dict.\n                 - cookie: str\n                    cookie value.\n                 - origin: str\n                    custom origin url.\n                 - suppress_origin: bool\n                    suppress outputting origin header.\n                 - host: str\n                    custom host header string.\n                 - http_proxy_host: <type>\n                    http proxy host name.\n                 - http_proxy_port: <type>\n                    http proxy port. If not set, set to 80.\n                 - http_no_proxy: <type>\n                    host names, which doesn't use proxy.\n                 - http_proxy_auth: <type>\n                    http proxy auth information. tuple of username and password. default is None\n                 - redirect_limit: <type>\n                    number of redirects to follow.\n                 - subprotocols: <type>\n                    array of available sub protocols. default is None.\n                 - socket: <type>\n                    pre-initialized stream socket.\n        \"\"\"\n        self.sock_opt.timeout = options.get('timeout', self.sock_opt.timeout)\n        self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options),\n                                   options.pop('socket', None))\n\n        try:\n            self.handshake_response = handshake(self.sock, *addrs, **options)\n            for attempt in range(options.pop('redirect_limit', 3)):\n                if self.handshake_response.status in SUPPORTED_REDIRECT_STATUSES:\n                    url = self.handshake_response.headers['location']\n                    self.sock.close()\n                    self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options),\n                                               options.pop('socket', None))\n                    self.handshake_response = handshake(self.sock, *addrs, **options)\n            self.connected = True\n        except:\n            if self.sock:\n                self.sock.close()\n                self.sock = None\n            raise\n\n    def send(self, payload, opcode=ABNF.OPCODE_TEXT):\n        \"\"\"\n        Send the data as string.\n\n        Parameters\n        ----------\n        payload:  <type>\n                  Payload must be utf-8 string or unicode,\n                  if the opcode is OPCODE_TEXT.\n                  Otherwise, it must be string(byte array)\n        opcode:   <type>\n                  operation code to send. Please see OPCODE_XXX.\n        \"\"\"\n\n        frame = ABNF.create_frame(payload, opcode)\n        return self.send_frame(frame)\n\n    def send_frame(self, frame):\n        \"\"\"\n        Send the data frame.\n\n        >>> ws = create_connection(\"ws://echo.websocket.org/\")\n        >>> frame = ABNF.create_frame(\"Hello\", ABNF.OPCODE_TEXT)\n        >>> ws.send_frame(frame)\n        >>> cont_frame = ABNF.create_frame(\"My name is \", ABNF.OPCODE_CONT, 0)\n        >>> ws.send_frame(frame)\n        >>> cont_frame = ABNF.create_frame(\"Foo Bar\", ABNF.OPCODE_CONT, 1)\n        >>> ws.send_frame(frame)\n\n        Parameters\n        ----------\n        frame: <type>\n            frame data created by ABNF.create_frame\n        \"\"\"\n        if self.get_mask_key:\n            frame.get_mask_key = self.get_mask_key\n        data = frame.format()\n        length = len(data)\n        if (isEnabledForTrace()):\n            trace(\"send: \" + repr(data))\n\n        with self.lock:\n            while data:\n                l = self._send(data)\n                data = data[l:]\n\n        return length\n\n    def send_binary(self, payload):\n        return self.send(payload, ABNF.OPCODE_BINARY)\n\n    def ping(self, payload=\"\"):\n        \"\"\"\n        Send ping data.\n\n        Parameters\n        ----------\n        payload: <type>\n            data payload to send server.\n        \"\"\"\n        if isinstance(payload, six.text_type):\n            payload = payload.encode(\"utf-8\")\n        self.send(payload, ABNF.OPCODE_PING)\n\n    def pong(self, payload=\"\"):\n        \"\"\"\n        Send pong data.\n\n        Parameters\n        ----------\n        payload: <type>\n            data payload to send server.\n        \"\"\"\n        if isinstance(payload, six.text_type):\n            payload = payload.encode(\"utf-8\")\n        self.send(payload, ABNF.OPCODE_PONG)\n\n    def recv(self):\n        \"\"\"\n        Receive string data(byte array) from the server.\n\n        Returns\n        ----------\n        data: string (byte array) value.\n        \"\"\"\n        with self.readlock:\n            opcode, data = self.recv_data()\n        if six.PY3 and opcode == ABNF.OPCODE_TEXT:\n            return data.decode(\"utf-8\")\n        elif opcode == ABNF.OPCODE_TEXT or opcode == ABNF.OPCODE_BINARY:\n            return data\n        else:\n            return ''\n\n    def recv_data(self, control_frame=False):\n        \"\"\"\n        Receive data with operation code.\n\n        Parameters\n        ----------\n        control_frame: bool\n            a boolean flag indicating whether to return control frame\n            data, defaults to False\n\n        Returns\n        -------\n        opcode, frame.data: tuple\n            tuple of operation code and string(byte array) value.\n        \"\"\"\n        opcode, frame = self.recv_data_frame(control_frame)\n        return opcode, frame.data\n\n    def recv_data_frame(self, control_frame=False):\n        \"\"\"\n        Receive data with operation code.\n\n        Parameters\n        ----------\n        control_frame: bool\n            a boolean flag indicating whether to return control frame\n            data, defaults to False\n\n        Returns\n        -------\n        frame.opcode, frame: tuple\n            tuple of operation code and string(byte array) value.\n        \"\"\"\n        while True:\n            frame = self.recv_frame()\n            if not frame:\n                # handle error:\n                # 'NoneType' object has no attribute 'opcode'\n                raise WebSocketProtocolException(\n                    \"Not a valid frame %s\" % frame)\n            elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):\n                self.cont_frame.validate(frame)\n                self.cont_frame.add(frame)\n\n                if self.cont_frame.is_fire(frame):\n                    return self.cont_frame.extract(frame)\n\n            elif frame.opcode == ABNF.OPCODE_CLOSE:\n                self.send_close()\n                return frame.opcode, frame\n            elif frame.opcode == ABNF.OPCODE_PING:\n                if len(frame.data) < 126:\n                    self.pong(frame.data)\n                else:\n                    raise WebSocketProtocolException(\n                        \"Ping message is too long\")\n                if control_frame:\n                    return frame.opcode, frame\n            elif frame.opcode == ABNF.OPCODE_PONG:\n                if control_frame:\n                    return frame.opcode, frame\n\n    def recv_frame(self):\n        \"\"\"\n        Receive data as frame from server.\n\n        Returns\n        -------\n        self.frame_buffer.recv_frame(): ABNF frame object\n        \"\"\"\n        return self.frame_buffer.recv_frame()\n\n    def send_close(self, status=STATUS_NORMAL, reason=six.b(\"\")):\n        \"\"\"\n        Send close data to the server.\n\n        Parameters\n        ----------\n        status: <type>\n            status code to send. see STATUS_XXX.\n        reason: str or bytes\n            the reason to close. This must be string or bytes.\n        \"\"\"\n        if status < 0 or status >= ABNF.LENGTH_16:\n            raise ValueError(\"code is invalid range\")\n        self.connected = False\n        self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)\n\n    def close(self, status=STATUS_NORMAL, reason=six.b(\"\"), timeout=3):\n        \"\"\"\n        Close Websocket object\n\n        Parameters\n        ----------\n        status: <type>\n            status code to send. see STATUS_XXX.\n        reason: <type>\n            the reason to close. This must be string.\n        timeout: int or float\n            timeout until receive a close frame.\n            If None, it will wait forever until receive a close frame.\n        \"\"\"\n        if self.connected:\n            if status < 0 or status >= ABNF.LENGTH_16:\n                raise ValueError(\"code is invalid range\")\n\n            try:\n                self.connected = False\n                self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)\n                sock_timeout = self.sock.gettimeout()\n                self.sock.settimeout(timeout)\n                start_time = time.time()\n                while timeout is None or time.time() - start_time < timeout:\n                    try:\n                        frame = self.recv_frame()\n                        if frame.opcode != ABNF.OPCODE_CLOSE:\n                            continue\n                        if isEnabledForError():\n                            recv_status = struct.unpack(\"!H\", frame.data[0:2])[0]\n                            if recv_status >= 3000 and recv_status <= 4999:\n                                debug(\"close status: \" + repr(recv_status))\n                            elif recv_status != STATUS_NORMAL:\n                                error(\"close status: \" + repr(recv_status))\n                        break\n                    except:\n                        break\n                self.sock.settimeout(sock_timeout)\n                self.sock.shutdown(socket.SHUT_RDWR)\n            except:\n                pass\n\n            self.shutdown()\n\n    def abort(self):\n        \"\"\"\n        Low-level asynchronous abort, wakes up other threads that are waiting in recv_*\n        \"\"\"\n        if self.connected:\n            self.sock.shutdown(socket.SHUT_RDWR)\n\n    def shutdown(self):\n        \"\"\"\n        close socket, immediately.\n        \"\"\"\n        if self.sock:\n            self.sock.close()\n            self.sock = None\n            self.connected = False\n\n    def _send(self, data):\n        return send(self.sock, data)\n\n    def _recv(self, bufsize):\n        try:\n            return recv(self.sock, bufsize)\n        except WebSocketConnectionClosedException:\n            if self.sock:\n                self.sock.close()\n            self.sock = None\n            self.connected = False\n            raise\n\n\ndef create_connection(url, timeout=None, class_=WebSocket, **options):\n    \"\"\"\n    Connect to url and return websocket object.\n\n    Connect to url and return the WebSocket object.\n    Passing optional timeout parameter will set the timeout on the socket.\n    If no timeout is supplied,\n    the global default timeout setting returned by getdefaulttimeout() is used.\n    You can customize using 'options'.\n    If you set \"header\" list object, you can set your own custom header.\n\n    >>> conn = create_connection(\"ws://echo.websocket.org/\",\n         ...     header=[\"User-Agent: MyProgram\",\n         ...             \"x-custom: header\"])\n\n    Parameters\n    ----------\n    timeout: int or float\n             socket timeout time. This value could be either float/integer.\n             if you set None for this value,\n             it means \"use default_timeout value\"\n    class_: <type>\n            class to instantiate when creating the connection. It has to implement\n            settimeout and connect. It's __init__ should be compatible with\n            WebSocket.__init__, i.e. accept all of it's kwargs.\n    options: <type>\n             - header: list or dict\n                custom http header list or dict.\n             - cookie: str\n                cookie value.\n             - origin: str\n                custom origin url.\n             - suppress_origin: bool\n                suppress outputting origin header.\n             - host: <type>\n                custom host header string.\n             - http_proxy_host: <type>\n                http proxy host name.\n             - http_proxy_port: <type>\n                http proxy port. If not set, set to 80.\n             - http_no_proxy: <type>\n                host names, which doesn't use proxy.\n             - http_proxy_auth: <type>\n                http proxy auth information. tuple of username and password. default is None\n             - enable_multithread: bool\n                enable lock for multithread.\n             - redirect_limit: <type>\n                number of redirects to follow.\n             - sockopt: <type>\n                socket options\n             - sslopt: <type>\n                ssl option\n             - subprotocols: <type>\n                array of available sub protocols. default is None.\n             - skip_utf8_validation: bool\n                skip utf8 validation.\n             - socket: <type>\n                pre-initialized stream socket.\n    \"\"\"\n    sockopt = options.pop(\"sockopt\", [])\n    sslopt = options.pop(\"sslopt\", {})\n    fire_cont_frame = options.pop(\"fire_cont_frame\", False)\n    enable_multithread = options.pop(\"enable_multithread\", False)\n    skip_utf8_validation = options.pop(\"skip_utf8_validation\", False)\n    websock = class_(sockopt=sockopt, sslopt=sslopt,\n                     fire_cont_frame=fire_cont_frame,\n                     enable_multithread=enable_multithread,\n                     skip_utf8_validation=skip_utf8_validation, **options)\n    websock.settimeout(timeout if timeout is not None else getdefaulttimeout())\n    websock.connect(url, **options)\n    return websock\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/_exceptions.py",
    "content": "\"\"\"\nDefine WebSocket exceptions\n\"\"\"\n\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\n\n\nclass WebSocketException(Exception):\n    \"\"\"\n    WebSocket exception class.\n    \"\"\"\n    pass\n\n\nclass WebSocketProtocolException(WebSocketException):\n    \"\"\"\n    If the WebSocket protocol is invalid, this exception will be raised.\n    \"\"\"\n    pass\n\n\nclass WebSocketPayloadException(WebSocketException):\n    \"\"\"\n    If the WebSocket payload is invalid, this exception will be raised.\n    \"\"\"\n    pass\n\n\nclass WebSocketConnectionClosedException(WebSocketException):\n    \"\"\"\n    If remote host closed the connection or some network error happened,\n    this exception will be raised.\n    \"\"\"\n    pass\n\n\nclass WebSocketTimeoutException(WebSocketException):\n    \"\"\"\n    WebSocketTimeoutException will be raised at socket timeout during read/write data.\n    \"\"\"\n    pass\n\n\nclass WebSocketProxyException(WebSocketException):\n    \"\"\"\n    WebSocketProxyException will be raised when proxy error occurred.\n    \"\"\"\n    pass\n\n\nclass WebSocketBadStatusException(WebSocketException):\n    \"\"\"\n    WebSocketBadStatusException will be raised when we get bad handshake status code.\n    \"\"\"\n\n    def __init__(self, message, status_code, status_message=None, resp_headers=None):\n        msg = message % (status_code, status_message)\n        super(WebSocketBadStatusException, self).__init__(msg)\n        self.status_code = status_code\n        self.resp_headers = resp_headers\n\n\nclass WebSocketAddressException(WebSocketException):\n    \"\"\"\n    If the websocket address info cannot be found, this exception will be raised.\n    \"\"\"\n    pass\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/_handshake.py",
    "content": "\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\nimport hashlib\nimport hmac\nimport os\n\nimport six\n\nfrom ._cookiejar import SimpleCookieJar\nfrom ._exceptions import *\nfrom ._http import *\nfrom ._logging import *\nfrom ._socket import *\n\nif hasattr(six, 'PY3') and six.PY3:\n    from base64 import encodebytes as base64encode\nelse:\n    from base64 import encodestring as base64encode\n\nif hasattr(six, 'PY3') and six.PY3:\n    if hasattr(six, 'PY34') and six.PY34:\n        from http import client as HTTPStatus\n    else:\n        from http import HTTPStatus\nelse:\n    import httplib as HTTPStatus\n\n__all__ = [\"handshake_response\", \"handshake\", \"SUPPORTED_REDIRECT_STATUSES\"]\n\nif hasattr(hmac, \"compare_digest\"):\n    compare_digest = hmac.compare_digest\nelse:\n    def compare_digest(s1, s2):\n        return s1 == s2\n\n# websocket supported version.\nVERSION = 13\n\nSUPPORTED_REDIRECT_STATUSES = (HTTPStatus.MOVED_PERMANENTLY, HTTPStatus.FOUND, HTTPStatus.SEE_OTHER,)\nSUCCESS_STATUSES = SUPPORTED_REDIRECT_STATUSES + (HTTPStatus.SWITCHING_PROTOCOLS,)\n\nCookieJar = SimpleCookieJar()\n\n\nclass handshake_response(object):\n\n    def __init__(self, status, headers, subprotocol):\n        self.status = status\n        self.headers = headers\n        self.subprotocol = subprotocol\n        CookieJar.add(headers.get(\"set-cookie\"))\n\n\ndef handshake(sock, hostname, port, resource, **options):\n    headers, key = _get_handshake_headers(resource, hostname, port, options)\n\n    header_str = \"\\r\\n\".join(headers)\n    send(sock, header_str)\n    dump(\"request header\", header_str)\n\n    status, resp = _get_resp_headers(sock)\n    if status in SUPPORTED_REDIRECT_STATUSES:\n        return handshake_response(status, resp, None)\n    success, subproto = _validate(resp, key, options.get(\"subprotocols\"))\n    if not success:\n        raise WebSocketException(\"Invalid WebSocket Header\")\n\n    return handshake_response(status, resp, subproto)\n\n\ndef _pack_hostname(hostname):\n    # IPv6 address\n    if ':' in hostname:\n        return '[' + hostname + ']'\n\n    return hostname\n\n\ndef _get_handshake_headers(resource, host, port, options):\n    headers = [\n        \"GET %s HTTP/1.1\" % resource,\n        \"Upgrade: websocket\"\n    ]\n    if port == 80 or port == 443:\n        hostport = _pack_hostname(host)\n    else:\n        hostport = \"%s:%d\" % (_pack_hostname(host), port)\n    if \"host\" in options and options[\"host\"] is not None:\n        headers.append(\"Host: %s\" % options[\"host\"])\n    else:\n        headers.append(\"Host: %s\" % hostport)\n\n    if \"suppress_origin\" not in options or not options[\"suppress_origin\"]:\n        if \"origin\" in options and options[\"origin\"] is not None:\n            headers.append(\"Origin: %s\" % options[\"origin\"])\n        else:\n            headers.append(\"Origin: http://%s\" % hostport)\n\n    key = _create_sec_websocket_key()\n\n    # Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified\n    if 'header' not in options or 'Sec-WebSocket-Key' not in options['header']:\n        key = _create_sec_websocket_key()\n        headers.append(\"Sec-WebSocket-Key: %s\" % key)\n    else:\n        key = options['header']['Sec-WebSocket-Key']\n\n    if 'header' not in options or 'Sec-WebSocket-Version' not in options['header']:\n        headers.append(\"Sec-WebSocket-Version: %s\" % VERSION)\n\n    if 'connection' not in options or options['connection'] is None:\n        headers.append('Connection: Upgrade')\n    else:\n        headers.append(options['connection'])\n\n    subprotocols = options.get(\"subprotocols\")\n    if subprotocols:\n        headers.append(\"Sec-WebSocket-Protocol: %s\" % \",\".join(subprotocols))\n\n    if \"header\" in options:\n        header = options[\"header\"]\n        if isinstance(header, dict):\n            header = [\n                \": \".join([k, v])\n                for k, v in header.items()\n                if v is not None\n            ]\n        headers.extend(header)\n\n    server_cookie = CookieJar.get(host)\n    client_cookie = options.get(\"cookie\", None)\n\n    cookie = \"; \".join(filter(None, [server_cookie, client_cookie]))\n\n    if cookie:\n        headers.append(\"Cookie: %s\" % cookie)\n\n    headers.append(\"\")\n    headers.append(\"\")\n\n    return headers, key\n\n\ndef _get_resp_headers(sock, success_statuses=SUCCESS_STATUSES):\n    status, resp_headers, status_message = read_headers(sock)\n    if status not in success_statuses:\n        raise WebSocketBadStatusException(\"Handshake status %d %s\", status, status_message, resp_headers)\n    return status, resp_headers\n\n\n_HEADERS_TO_CHECK = {\n    \"upgrade\": \"websocket\",\n    \"connection\": \"upgrade\",\n}\n\n\ndef _validate(headers, key, subprotocols):\n    subproto = None\n    for k, v in _HEADERS_TO_CHECK.items():\n        r = headers.get(k, None)\n        if not r:\n            return False, None\n        r = [x.strip().lower() for x in r.split(',')]\n        if v not in r:\n            return False, None\n\n    if subprotocols:\n        subproto = headers.get(\"sec-websocket-protocol\", None)\n        if not subproto or subproto.lower() not in [s.lower() for s in subprotocols]:\n            error(\"Invalid subprotocol: \" + str(subprotocols))\n            return False, None\n        subproto = subproto.lower()\n\n    result = headers.get(\"sec-websocket-accept\", None)\n    if not result:\n        return False, None\n    result = result.lower()\n\n    if isinstance(result, six.text_type):\n        result = result.encode('utf-8')\n\n    value = (key + \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\").encode('utf-8')\n    hashed = base64encode(hashlib.sha1(value).digest()).strip().lower()\n    success = compare_digest(hashed, result)\n\n    if success:\n        return True, subproto\n    else:\n        return False, None\n\n\ndef _create_sec_websocket_key():\n    randomness = os.urandom(16)\n    return base64encode(randomness).decode('utf-8').strip()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/_http.py",
    "content": "\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\nimport errno\nimport os\nimport socket\nimport sys\n\nimport six\n\nfrom ._exceptions import *\nfrom ._logging import *\nfrom ._socket import*\nfrom ._ssl_compat import *\nfrom ._url import *\n\nif six.PY3:\n    from base64 import encodebytes as base64encode\nelse:\n    from base64 import encodestring as base64encode\n\n__all__ = [\"proxy_info\", \"connect\", \"read_headers\"]\n\ntry:\n    import socks\n    ProxyConnectionError = socks.ProxyConnectionError\n    HAS_PYSOCKS = True\nexcept:\n    class ProxyConnectionError(BaseException):\n        pass\n    HAS_PYSOCKS = False\n\n\nclass proxy_info(object):\n\n    def __init__(self, **options):\n        self.type = options.get(\"proxy_type\") or \"http\"\n        if not(self.type in ['http', 'socks4', 'socks5', 'socks5h']):\n            raise ValueError(\"proxy_type must be 'http', 'socks4', 'socks5' or 'socks5h'\")\n        self.host = options.get(\"http_proxy_host\", None)\n        if self.host:\n            self.port = options.get(\"http_proxy_port\", 0)\n            self.auth = options.get(\"http_proxy_auth\", None)\n            self.no_proxy = options.get(\"http_no_proxy\", None)\n        else:\n            self.port = 0\n            self.auth = None\n            self.no_proxy = None\n\n\ndef _open_proxied_socket(url, options, proxy):\n    hostname, port, resource, is_secure = parse_url(url)\n\n    if not HAS_PYSOCKS:\n        raise WebSocketException(\"PySocks module not found.\")\n\n    ptype = socks.SOCKS5\n    rdns = False\n    if proxy.type == \"socks4\":\n        ptype = socks.SOCKS4\n    if proxy.type == \"http\":\n        ptype = socks.HTTP\n    if proxy.type[-1] == \"h\":\n        rdns = True\n\n    sock = socks.create_connection(\n        (hostname, port),\n        proxy_type=ptype,\n        proxy_addr=proxy.host,\n        proxy_port=proxy.port,\n        proxy_rdns=rdns,\n        proxy_username=proxy.auth[0] if proxy.auth else None,\n        proxy_password=proxy.auth[1] if proxy.auth else None,\n        timeout=options.timeout,\n        socket_options=DEFAULT_SOCKET_OPTION + options.sockopt\n    )\n\n    if is_secure:\n        if HAVE_SSL:\n            sock = _ssl_socket(sock, options.sslopt, hostname)\n        else:\n            raise WebSocketException(\"SSL not available.\")\n\n    return sock, (hostname, port, resource)\n\n\ndef connect(url, options, proxy, socket):\n    if proxy.host and not socket and not (proxy.type == 'http'):\n        return _open_proxied_socket(url, options, proxy)\n\n    hostname, port, resource, is_secure = parse_url(url)\n\n    if socket:\n        return socket, (hostname, port, resource)\n\n    addrinfo_list, need_tunnel, auth = _get_addrinfo_list(\n        hostname, port, is_secure, proxy)\n    if not addrinfo_list:\n        raise WebSocketException(\n            \"Host not found.: \" + hostname + \":\" + str(port))\n\n    sock = None\n    try:\n        sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)\n        if need_tunnel:\n            sock = _tunnel(sock, hostname, port, auth)\n\n        if is_secure:\n            if HAVE_SSL:\n                sock = _ssl_socket(sock, options.sslopt, hostname)\n            else:\n                raise WebSocketException(\"SSL not available.\")\n\n        return sock, (hostname, port, resource)\n    except:\n        if sock:\n            sock.close()\n        raise\n\n\ndef _get_addrinfo_list(hostname, port, is_secure, proxy):\n    phost, pport, pauth = get_proxy_info(\n        hostname, is_secure, proxy.host, proxy.port, proxy.auth, proxy.no_proxy)\n    try:\n        # when running on windows 10, getaddrinfo without socktype returns a socktype 0.\n        # This generates an error exception: `_on_error: exception Socket type must be stream or datagram, not 0`\n        # or `OSError: [Errno 22] Invalid argument` when creating socket. Force the socket type to SOCK_STREAM.\n        if not phost:\n            addrinfo_list = socket.getaddrinfo(\n                hostname, port, 0, socket.SOCK_STREAM, socket.SOL_TCP)\n            return addrinfo_list, False, None\n        else:\n            pport = pport and pport or 80\n            # when running on windows 10, the getaddrinfo used above\n            # returns a socktype 0. This generates an error exception:\n            # _on_error: exception Socket type must be stream or datagram, not 0\n            # Force the socket type to SOCK_STREAM\n            addrinfo_list = socket.getaddrinfo(phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP)\n            return addrinfo_list, True, pauth\n    except socket.gaierror as e:\n        raise WebSocketAddressException(e)\n\n\ndef _open_socket(addrinfo_list, sockopt, timeout):\n    err = None\n    for addrinfo in addrinfo_list:\n        family, socktype, proto = addrinfo[:3]\n        sock = socket.socket(family, socktype, proto)\n        sock.settimeout(timeout)\n        for opts in DEFAULT_SOCKET_OPTION:\n            sock.setsockopt(*opts)\n        for opts in sockopt:\n            sock.setsockopt(*opts)\n\n        address = addrinfo[4]\n        err = None\n        while not err:\n            try:\n                sock.connect(address)\n            except ProxyConnectionError as error:\n                err = WebSocketProxyException(str(error))\n                err.remote_ip = str(address[0])\n                continue\n            except socket.error as error:\n                error.remote_ip = str(address[0])\n                try:\n                    eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED)\n                except:\n                    eConnRefused = (errno.ECONNREFUSED, )\n                if error.errno == errno.EINTR:\n                    continue\n                elif error.errno in eConnRefused:\n                    err = error\n                    continue\n                else:\n                    raise error\n            else:\n                break\n        else:\n            continue\n        break\n    else:\n        if err:\n            raise err\n\n    return sock\n\n\ndef _can_use_sni():\n    return six.PY2 and sys.version_info >= (2, 7, 9) or sys.version_info >= (3, 2)\n\n\ndef _wrap_sni_socket(sock, sslopt, hostname, check_hostname):\n    context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23))\n\n    if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE:\n        cafile = sslopt.get('ca_certs', None)\n        capath = sslopt.get('ca_cert_path', None)\n        if cafile or capath:\n            context.load_verify_locations(cafile=cafile, capath=capath)\n        elif hasattr(context, 'load_default_certs'):\n            context.load_default_certs(ssl.Purpose.SERVER_AUTH)\n    if sslopt.get('certfile', None):\n        context.load_cert_chain(\n            sslopt['certfile'],\n            sslopt.get('keyfile', None),\n            sslopt.get('password', None),\n        )\n    # see\n    # https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153\n    context.verify_mode = sslopt['cert_reqs']\n    if HAVE_CONTEXT_CHECK_HOSTNAME:\n        context.check_hostname = check_hostname\n    if 'ciphers' in sslopt:\n        context.set_ciphers(sslopt['ciphers'])\n    if 'cert_chain' in sslopt:\n        certfile, keyfile, password = sslopt['cert_chain']\n        context.load_cert_chain(certfile, keyfile, password)\n    if 'ecdh_curve' in sslopt:\n        context.set_ecdh_curve(sslopt['ecdh_curve'])\n\n    return context.wrap_socket(\n        sock,\n        do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True),\n        suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True),\n        server_hostname=hostname,\n    )\n\n\ndef _ssl_socket(sock, user_sslopt, hostname):\n    sslopt = dict(cert_reqs=ssl.CERT_REQUIRED)\n    sslopt.update(user_sslopt)\n\n    certPath = os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE')\n    if certPath and os.path.isfile(certPath) \\\n            and user_sslopt.get('ca_certs', None) is None \\\n            and user_sslopt.get('ca_cert', None) is None:\n        sslopt['ca_certs'] = certPath\n    elif certPath and os.path.isdir(certPath) \\\n            and user_sslopt.get('ca_cert_path', None) is None:\n        sslopt['ca_cert_path'] = certPath\n\n    check_hostname = sslopt[\"cert_reqs\"] != ssl.CERT_NONE and sslopt.pop(\n        'check_hostname', True)\n\n    if _can_use_sni():\n        sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)\n    else:\n        sslopt.pop('check_hostname', True)\n        sock = ssl.wrap_socket(sock, **sslopt)\n\n    if not HAVE_CONTEXT_CHECK_HOSTNAME and check_hostname:\n        match_hostname(sock.getpeercert(), hostname)\n\n    return sock\n\n\ndef _tunnel(sock, host, port, auth):\n    debug(\"Connecting proxy...\")\n    connect_header = \"CONNECT %s:%d HTTP/1.1\\r\\n\" % (host, port)\n    connect_header += \"Host: %s:%d\\r\\n\" % (host, port)\n\n    # TODO: support digest auth.\n    if auth and auth[0]:\n        auth_str = auth[0]\n        if auth[1]:\n            auth_str += \":\" + auth[1]\n        encoded_str = base64encode(auth_str.encode()).strip().decode().replace('\\n', '')\n        connect_header += \"Proxy-Authorization: Basic %s\\r\\n\" % encoded_str\n    connect_header += \"\\r\\n\"\n    dump(\"request header\", connect_header)\n\n    send(sock, connect_header)\n\n    try:\n        status, resp_headers, status_message = read_headers(sock)\n    except Exception as e:\n        raise WebSocketProxyException(str(e))\n\n    if status != 200:\n        raise WebSocketProxyException(\n            \"failed CONNECT via proxy status: %r\" % status)\n\n    return sock\n\n\ndef read_headers(sock):\n    status = None\n    status_message = None\n    headers = {}\n    trace(\"--- response header ---\")\n\n    while True:\n        line = recv_line(sock)\n        line = line.decode('utf-8').strip()\n        if not line:\n            break\n        trace(line)\n        if not status:\n\n            status_info = line.split(\" \", 2)\n            status = int(status_info[1])\n            if len(status_info) > 2:\n                status_message = status_info[2]\n        else:\n            kv = line.split(\":\", 1)\n            if len(kv) == 2:\n                key, value = kv\n                if key.lower() == \"set-cookie\" and headers.get(\"set-cookie\"):\n                    headers[\"set-cookie\"] = headers.get(\"set-cookie\") + \"; \" + value.strip()\n                else:\n                    headers[key.lower()] = value.strip()\n            else:\n                raise WebSocketException(\"Invalid header\")\n\n    trace(\"-----------------------\")\n\n    return status, headers, status_message\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/_logging.py",
    "content": "\"\"\"\n\n\"\"\"\n\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\nimport logging\n\n_logger = logging.getLogger('websocket')\ntry:\n    from logging import NullHandler\nexcept ImportError:\n    class NullHandler(logging.Handler):\n        def emit(self, record):\n            pass\n\n_logger.addHandler(NullHandler())\n\n_traceEnabled = False\n\n__all__ = [\"enableTrace\", \"dump\", \"error\", \"warning\", \"debug\", \"trace\",\n           \"isEnabledForError\", \"isEnabledForDebug\", \"isEnabledForTrace\"]\n\n\ndef enableTrace(traceable, handler=logging.StreamHandler()):\n    \"\"\"\n    Turn on/off the traceability.\n\n    Parameters\n    ----------\n    traceable: bool\n        If set to True, traceability is enabled.\n    \"\"\"\n    global _traceEnabled\n    _traceEnabled = traceable\n    if traceable:\n        _logger.addHandler(handler)\n        _logger.setLevel(logging.DEBUG)\n\n\ndef dump(title, message):\n    if _traceEnabled:\n        _logger.debug(\"--- \" + title + \" ---\")\n        _logger.debug(message)\n        _logger.debug(\"-----------------------\")\n\n\ndef error(msg):\n    _logger.error(msg)\n\n\ndef warning(msg):\n    _logger.warning(msg)\n\n\ndef debug(msg):\n    _logger.debug(msg)\n\n\ndef trace(msg):\n    if _traceEnabled:\n        _logger.debug(msg)\n\n\ndef isEnabledForError():\n    return _logger.isEnabledFor(logging.ERROR)\n\n\ndef isEnabledForDebug():\n    return _logger.isEnabledFor(logging.DEBUG)\n\n\ndef isEnabledForTrace():\n    return _traceEnabled\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/_socket.py",
    "content": "\"\"\"\n\n\"\"\"\n\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\nimport errno\nimport select\nimport socket\n\nimport six\n\nfrom ._exceptions import *\nfrom ._ssl_compat import *\nfrom ._utils import *\n\nDEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)]\nif hasattr(socket, \"SO_KEEPALIVE\"):\n    DEFAULT_SOCKET_OPTION.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1))\nif hasattr(socket, \"TCP_KEEPIDLE\"):\n    DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPIDLE, 30))\nif hasattr(socket, \"TCP_KEEPINTVL\"):\n    DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPINTVL, 10))\nif hasattr(socket, \"TCP_KEEPCNT\"):\n    DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPCNT, 3))\n\n_default_timeout = None\n\n__all__ = [\"DEFAULT_SOCKET_OPTION\", \"sock_opt\", \"setdefaulttimeout\", \"getdefaulttimeout\",\n           \"recv\", \"recv_line\", \"send\"]\n\n\nclass sock_opt(object):\n\n    def __init__(self, sockopt, sslopt):\n        if sockopt is None:\n            sockopt = []\n        if sslopt is None:\n            sslopt = {}\n        self.sockopt = sockopt\n        self.sslopt = sslopt\n        self.timeout = None\n\n\ndef setdefaulttimeout(timeout):\n    \"\"\"\n    Set the global timeout setting to connect.\n\n    Parameters\n    ----------\n    timeout: int or float\n        default socket timeout time (in seconds)\n    \"\"\"\n    global _default_timeout\n    _default_timeout = timeout\n\n\ndef getdefaulttimeout():\n    \"\"\"\n    Get default timeout\n\n    Returns\n    ----------\n    _default_timeout: int or float\n        Return the global timeout setting (in seconds) to connect.\n    \"\"\"\n    return _default_timeout\n\n\ndef recv(sock, bufsize):\n    if not sock:\n        raise WebSocketConnectionClosedException(\"socket is already closed.\")\n\n    def _recv():\n        try:\n            return sock.recv(bufsize)\n        except SSLWantReadError:\n            pass\n        except socket.error as exc:\n            error_code = extract_error_code(exc)\n            if error_code is None:\n                raise\n            if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK:\n                raise\n\n        r, w, e = select.select((sock, ), (), (), sock.gettimeout())\n        if r:\n            return sock.recv(bufsize)\n\n    try:\n        if sock.gettimeout() == 0:\n            bytes_ = sock.recv(bufsize)\n        else:\n            bytes_ = _recv()\n    except socket.timeout as e:\n        message = extract_err_message(e)\n        raise WebSocketTimeoutException(message)\n    except SSLError as e:\n        message = extract_err_message(e)\n        if isinstance(message, str) and 'timed out' in message:\n            raise WebSocketTimeoutException(message)\n        else:\n            raise\n\n    if not bytes_:\n        raise WebSocketConnectionClosedException(\n            \"Connection is already closed.\")\n\n    return bytes_\n\n\ndef recv_line(sock):\n    line = []\n    while True:\n        c = recv(sock, 1)\n        line.append(c)\n        if c == six.b(\"\\n\"):\n            break\n    return six.b(\"\").join(line)\n\n\ndef send(sock, data):\n    if isinstance(data, six.text_type):\n        data = data.encode('utf-8')\n\n    if not sock:\n        raise WebSocketConnectionClosedException(\"socket is already closed.\")\n\n    def _send():\n        try:\n            return sock.send(data)\n        except SSLWantWriteError:\n            pass\n        except socket.error as exc:\n            error_code = extract_error_code(exc)\n            if error_code is None:\n                raise\n            if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK:\n                raise\n\n        r, w, e = select.select((), (sock, ), (), sock.gettimeout())\n        if w:\n            return sock.send(data)\n\n    try:\n        if sock.gettimeout() == 0:\n            return sock.send(data)\n        else:\n            return _send()\n    except socket.timeout as e:\n        message = extract_err_message(e)\n        raise WebSocketTimeoutException(message)\n    except Exception as e:\n        message = extract_err_message(e)\n        if isinstance(message, str) and \"timed out\" in message:\n            raise WebSocketTimeoutException(message)\n        else:\n            raise\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/_ssl_compat.py",
    "content": "\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\n__all__ = [\"HAVE_SSL\", \"ssl\", \"SSLError\", \"SSLWantReadError\", \"SSLWantWriteError\"]\n\ntry:\n    import ssl\n    from ssl import SSLError\n    from ssl import SSLWantReadError\n    from ssl import SSLWantWriteError\n    if hasattr(ssl, 'SSLContext') and hasattr(ssl.SSLContext, 'check_hostname'):\n        HAVE_CONTEXT_CHECK_HOSTNAME = True\n    else:\n        HAVE_CONTEXT_CHECK_HOSTNAME = False\n        if hasattr(ssl, \"match_hostname\"):\n            from ssl import match_hostname\n        else:\n            from backports.ssl_match_hostname import match_hostname\n        __all__.append(\"match_hostname\")\n    __all__.append(\"HAVE_CONTEXT_CHECK_HOSTNAME\")\n\n    HAVE_SSL = True\nexcept ImportError:\n    # dummy class of SSLError for ssl none-support environment.\n    class SSLError(Exception):\n        pass\n\n    class SSLWantReadError(Exception):\n        pass\n\n    class SSLWantWriteError(Exception):\n        pass\n\n    ssl = None\n\n    HAVE_SSL = False\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/_url.py",
    "content": "\"\"\"\n\n\"\"\"\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\n\nimport os\nimport socket\nimport struct\n\nfrom six.moves.urllib.parse import urlparse\n\n\n__all__ = [\"parse_url\", \"get_proxy_info\"]\n\n\ndef parse_url(url):\n    \"\"\"\n    parse url and the result is tuple of\n    (hostname, port, resource path and the flag of secure mode)\n\n    Parameters\n    ----------\n    url: str\n        url string.\n    \"\"\"\n    if \":\" not in url:\n        raise ValueError(\"url is invalid\")\n\n    scheme, url = url.split(\":\", 1)\n\n    parsed = urlparse(url, scheme=\"http\")\n    if parsed.hostname:\n        hostname = parsed.hostname\n    else:\n        raise ValueError(\"hostname is invalid\")\n    port = 0\n    if parsed.port:\n        port = parsed.port\n\n    is_secure = False\n    if scheme == \"ws\":\n        if not port:\n            port = 80\n    elif scheme == \"wss\":\n        is_secure = True\n        if not port:\n            port = 443\n    else:\n        raise ValueError(\"scheme %s is invalid\" % scheme)\n\n    if parsed.path:\n        resource = parsed.path\n    else:\n        resource = \"/\"\n\n    if parsed.query:\n        resource += \"?\" + parsed.query\n\n    return hostname, port, resource, is_secure\n\n\nDEFAULT_NO_PROXY_HOST = [\"localhost\", \"127.0.0.1\"]\n\n\ndef _is_ip_address(addr):\n    try:\n        socket.inet_aton(addr)\n    except socket.error:\n        return False\n    else:\n        return True\n\n\ndef _is_subnet_address(hostname):\n    try:\n        addr, netmask = hostname.split(\"/\")\n        return _is_ip_address(addr) and 0 <= int(netmask) < 32\n    except ValueError:\n        return False\n\n\ndef _is_address_in_network(ip, net):\n    ipaddr = struct.unpack('!I', socket.inet_aton(ip))[0]\n    netaddr, netmask = net.split('/')\n    netaddr = struct.unpack('!I', socket.inet_aton(netaddr))[0]\n\n    netmask = (0xFFFFFFFF << (32 - int(netmask))) & 0xFFFFFFFF\n    return ipaddr & netmask == netaddr\n\n\ndef _is_no_proxy_host(hostname, no_proxy):\n    if not no_proxy:\n        v = os.environ.get(\"no_proxy\", \"\").replace(\" \", \"\")\n        if v:\n            no_proxy = v.split(\",\")\n    if not no_proxy:\n        no_proxy = DEFAULT_NO_PROXY_HOST\n\n    if '*' in no_proxy:\n        return True\n    if hostname in no_proxy:\n        return True\n    if _is_ip_address(hostname):\n        return any([_is_address_in_network(hostname, subnet) for subnet in no_proxy if _is_subnet_address(subnet)])\n    for domain in [domain for domain in no_proxy if domain.startswith('.')]:\n        if hostname.endswith(domain):\n            return True\n    return False\n\n\ndef get_proxy_info(\n        hostname, is_secure, proxy_host=None, proxy_port=0, proxy_auth=None,\n        no_proxy=None, proxy_type='http'):\n    \"\"\"\n    Try to retrieve proxy host and port from environment\n    if not provided in options.\n    Result is (proxy_host, proxy_port, proxy_auth).\n    proxy_auth is tuple of username and password\n    of proxy authentication information.\n\n    Parameters\n    ----------\n    hostname: <type>\n        websocket server name.\n    is_secure: <type>\n        is the connection secure? (wss) looks for \"https_proxy\" in env\n        before falling back to \"http_proxy\"\n    options: <type>\n        - http_proxy_host: <type>\n            http proxy host name.\n        - http_proxy_port: <type>\n            http proxy port.\n        - http_no_proxy: <type>\n            host names, which doesn't use proxy.\n        - http_proxy_auth: <type>\n            http proxy auth information. tuple of username and password. default is None\n        - proxy_type: <type>\n            if set to \"socks5\" PySocks wrapper will be used in place of a http proxy. default is \"http\"\n    \"\"\"\n    if _is_no_proxy_host(hostname, no_proxy):\n        return None, 0, None\n\n    if proxy_host:\n        port = proxy_port\n        auth = proxy_auth\n        return proxy_host, port, auth\n\n    env_keys = [\"http_proxy\"]\n    if is_secure:\n        env_keys.insert(0, \"https_proxy\")\n\n    for key in env_keys:\n        value = os.environ.get(key, None)\n        if value:\n            proxy = urlparse(value)\n            auth = (proxy.username, proxy.password) if proxy.username else None\n            return proxy.hostname, proxy.port, auth\n\n    return None, 0, None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/_utils.py",
    "content": "\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\nimport six\n\n__all__ = [\"NoLock\", \"validate_utf8\", \"extract_err_message\", \"extract_error_code\"]\n\n\nclass NoLock(object):\n\n    def __enter__(self):\n        pass\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        pass\n\n\ntry:\n    # If wsaccel is available we use compiled routines to validate UTF-8\n    # strings.\n    from wsaccel.utf8validator import Utf8Validator\n\n    def _validate_utf8(utfbytes):\n        return Utf8Validator().validate(utfbytes)[0]\n\nexcept ImportError:\n    # UTF-8 validator\n    # python implementation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/\n\n    _UTF8_ACCEPT = 0\n    _UTF8_REJECT = 12\n\n    _UTF8D = [\n        # The first part of the table maps bytes to character classes that\n        # to reduce the size of the transition table and create bitmasks.\n        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\n        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\n        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\n        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\n        1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,  9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,\n        7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,  7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,\n        8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,  2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,\n        10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8,\n\n        # The second part is a transition table that maps a combination\n        # of a state of the automaton and a character class to a state.\n        0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12,\n        12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12,\n        12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12,\n        12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12,\n        12,36,12,12,12,12,12,12,12,12,12,12, ]\n\n    def _decode(state, codep, ch):\n        tp = _UTF8D[ch]\n\n        codep = (ch & 0x3f) | (codep << 6) if (\n            state != _UTF8_ACCEPT) else (0xff >> tp) & ch\n        state = _UTF8D[256 + state + tp]\n\n        return state, codep\n\n    def _validate_utf8(utfbytes):\n        state = _UTF8_ACCEPT\n        codep = 0\n        for i in utfbytes:\n            if six.PY2:\n                i = ord(i)\n            state, codep = _decode(state, codep, i)\n            if state == _UTF8_REJECT:\n                return False\n\n        return True\n\n\ndef validate_utf8(utfbytes):\n    \"\"\"\n    validate utf8 byte string.\n    utfbytes: utf byte string to check.\n    return value: if valid utf8 string, return true. Otherwise, return false.\n    \"\"\"\n    return _validate_utf8(utfbytes)\n\n\ndef extract_err_message(exception):\n    if exception.args:\n        return exception.args[0]\n    else:\n        return None\n\n\ndef extract_error_code(exception):\n    if exception.args and len(exception.args) > 1:\n        return exception.args[0] if isinstance(exception.args[0], int) else None\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/tests/__init__.py",
    "content": ""
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/tests/data/header01.txt",
    "content": "HTTP/1.1 101 WebSocket Protocol Handshake\nConnection: Upgrade \nUpgrade: WebSocket\nSec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=\nsome_header: something\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/tests/data/header02.txt",
    "content": "HTTP/1.1 101 WebSocket Protocol Handshake\nConnection: Upgrade\nUpgrade WebSocket\nSec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=\nsome_header: something\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/tests/data/header03.txt",
    "content": "HTTP/1.1 101 WebSocket Protocol Handshake\nConnection: Upgrade, Keep-Alive\nUpgrade: WebSocket\nSec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=\nsome_header: something\n\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/tests/test_abnf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\n\nimport os\nimport websocket as ws\nfrom websocket._abnf import *\nimport sys\nsys.path[0:0] = [\"\"]\n\nif sys.version_info[0] == 2 and sys.version_info[1] < 7:\n    import unittest2 as unittest\nelse:\n    import unittest\n\n\nclass ABNFTest(unittest.TestCase):\n\n    def testInit(self):\n        a = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING)\n        self.assertEqual(a.fin, 0)\n        self.assertEqual(a.rsv1, 0)\n        self.assertEqual(a.rsv2, 0)\n        self.assertEqual(a.rsv3, 0)\n        self.assertEqual(a.opcode, 9)\n        self.assertEqual(a.data, '')\n        a_bad = ABNF(0,1,0,0, opcode=77)\n        self.assertEqual(a_bad.rsv1, 1)\n        self.assertEqual(a_bad.opcode, 77)\n\n    def testValidate(self):\n        a = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING)\n        self.assertRaises(ws.WebSocketProtocolException, a.validate)\n        a_bad = ABNF(0,1,0,0, opcode=77)\n        self.assertRaises(ws.WebSocketProtocolException, a_bad.validate)\n        a_close = ABNF(0,1,0,0, opcode=ABNF.OPCODE_CLOSE, data=\"abcdefgh1234567890abcdefgh1234567890abcdefgh1234567890abcdefgh1234567890\")\n        self.assertRaises(ws.WebSocketProtocolException, a_close.validate)\n\n#    This caused an error in the Python 2.7 Github Actions build\n#    Uncomment test case when Python 2 support no longer wanted\n#    def testMask(self):\n#        ab = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING)\n#        bytes_val = bytes(\"aaaa\", 'utf-8')\n#        self.assertEqual(ab._get_masked(bytes_val), bytes_val)\n\n    def testFrameBuffer(self):\n        fb = frame_buffer(0, True)\n        self.assertEqual(fb.recv, 0)\n        self.assertEqual(fb.skip_utf8_validation, True)\n        fb.clear\n        self.assertEqual(fb.header, None)\n        self.assertEqual(fb.length, None)\n        self.assertEqual(fb.mask, None)\n        self.assertEqual(fb.has_mask(), False)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/tests/test_app.py",
    "content": "# -*- coding: utf-8 -*-\n#\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\n\nimport os\nimport os.path\nimport websocket as ws\nimport sys\nsys.path[0:0] = [\"\"]\n\ntry:\n    import ssl\nexcept ImportError:\n    HAVE_SSL = False\n\nif sys.version_info[0] == 2 and sys.version_info[1] < 7:\n    import unittest2 as unittest\nelse:\n    import unittest\n\n# Skip test to access the internet.\nTEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1'\nTRACEABLE = True\n\n\nclass WebSocketAppTest(unittest.TestCase):\n\n    class NotSetYet(object):\n        \"\"\" A marker class for signalling that a value hasn't been set yet.\n        \"\"\"\n\n    def setUp(self):\n        ws.enableTrace(TRACEABLE)\n\n        WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet()\n        WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet()\n        WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet()\n\n    def tearDown(self):\n        WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet()\n        WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet()\n        WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet()\n\n    @unittest.skipUnless(TEST_WITH_INTERNET, \"Internet-requiring tests are disabled\")\n    def testKeepRunning(self):\n        \"\"\" A WebSocketApp should keep running as long as its self.keep_running\n        is not False (in the boolean context).\n        \"\"\"\n\n        def on_open(self, *args, **kwargs):\n            \"\"\" Set the keep_running flag for later inspection and immediately\n            close the connection.\n            \"\"\"\n            WebSocketAppTest.keep_running_open = self.keep_running\n\n            self.close()\n\n        def on_close(self, *args, **kwargs):\n            \"\"\" Set the keep_running flag for the test to use.\n            \"\"\"\n            WebSocketAppTest.keep_running_close = self.keep_running\n\n        app = ws.WebSocketApp('ws://echo.websocket.org/', on_open=on_open, on_close=on_close)\n        app.run_forever()\n\n        # if numpy is installed, this assertion fail\n        # self.assertFalse(isinstance(WebSocketAppTest.keep_running_open,\n        #                             WebSocketAppTest.NotSetYet))\n\n        # self.assertFalse(isinstance(WebSocketAppTest.keep_running_close,\n        #                             WebSocketAppTest.NotSetYet))\n\n        # self.assertEqual(True, WebSocketAppTest.keep_running_open)\n        # self.assertEqual(False, WebSocketAppTest.keep_running_close)\n\n    @unittest.skipUnless(TEST_WITH_INTERNET, \"Internet-requiring tests are disabled\")\n    def testSockMaskKey(self):\n        \"\"\" A WebSocketApp should forward the received mask_key function down\n        to the actual socket.\n        \"\"\"\n\n        def my_mask_key_func():\n            pass\n\n        def on_open(self, *args, **kwargs):\n            \"\"\" Set the value so the test can use it later on and immediately\n            close the connection.\n            \"\"\"\n            WebSocketAppTest.get_mask_key_id = id(self.get_mask_key)\n            self.close()\n\n        app = ws.WebSocketApp('ws://echo.websocket.org/', on_open=on_open, get_mask_key=my_mask_key_func)\n        app.run_forever()\n\n        # if numpy is installed, this assertion fail\n        # Note: We can't use 'is' for comparing the functions directly, need to use 'id'.\n        # self.assertEqual(WebSocketAppTest.get_mask_key_id, id(my_mask_key_func))\n\n    @unittest.skipUnless(TEST_WITH_INTERNET, \"Internet-requiring tests are disabled\")\n    def testPingInterval(self):\n        \"\"\" A WebSocketApp should ping regularly\n        \"\"\"\n\n        def on_ping(app, msg):\n            print(\"Got a ping!\")\n            app.close()\n\n        def on_pong(app, msg):\n            print(\"Got a pong! No need to respond\")\n            app.close()\n\n        app = ws.WebSocketApp('wss://api-pub.bitfinex.com/ws/1', on_ping=on_ping, on_pong=on_pong)\n        app.run_forever(ping_interval=2, ping_timeout=1)  # , sslopt={\"cert_reqs\": ssl.CERT_NONE}\n        self.assertRaises(ws.WebSocketException, app.run_forever, ping_interval=2, ping_timeout=3, sslopt={\"cert_reqs\": ssl.CERT_NONE})\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/tests/test_cookiejar.py",
    "content": "\"\"\"\n\n\"\"\"\n\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\nimport unittest\n\nfrom websocket._cookiejar import SimpleCookieJar\n\n\nclass CookieJarTest(unittest.TestCase):\n    def testAdd(self):\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.add(\"\")\n        self.assertFalse(cookie_jar.jar, \"Cookie with no domain should not be added to the jar\")\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.add(\"a=b\")\n        self.assertFalse(cookie_jar.jar, \"Cookie with no domain should not be added to the jar\")\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.add(\"a=b; domain=.abc\")\n        self.assertTrue(\".abc\" in cookie_jar.jar)\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.add(\"a=b; domain=abc\")\n        self.assertTrue(\".abc\" in cookie_jar.jar)\n        self.assertTrue(\"abc\" not in cookie_jar.jar)\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.add(\"a=b; c=d; domain=abc\")\n        self.assertEqual(cookie_jar.get(\"abc\"), \"a=b; c=d\")\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.add(\"a=b; c=d; domain=abc\")\n        cookie_jar.add(\"e=f; domain=abc\")\n        self.assertEqual(cookie_jar.get(\"abc\"), \"a=b; c=d; e=f\")\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.add(\"a=b; c=d; domain=abc\")\n        cookie_jar.add(\"e=f; domain=.abc\")\n        self.assertEqual(cookie_jar.get(\"abc\"), \"a=b; c=d; e=f\")\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.add(\"a=b; c=d; domain=abc\")\n        cookie_jar.add(\"e=f; domain=xyz\")\n        self.assertEqual(cookie_jar.get(\"abc\"), \"a=b; c=d\")\n        self.assertEqual(cookie_jar.get(\"xyz\"), \"e=f\")\n        self.assertEqual(cookie_jar.get(\"something\"), \"\")\n\n    def testSet(self):\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.set(\"a=b\")\n        self.assertFalse(cookie_jar.jar, \"Cookie with no domain should not be added to the jar\")\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.set(\"a=b; domain=.abc\")\n        self.assertTrue(\".abc\" in cookie_jar.jar)\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.set(\"a=b; domain=abc\")\n        self.assertTrue(\".abc\" in cookie_jar.jar)\n        self.assertTrue(\"abc\" not in cookie_jar.jar)\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.set(\"a=b; c=d; domain=abc\")\n        self.assertEqual(cookie_jar.get(\"abc\"), \"a=b; c=d\")\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.set(\"a=b; c=d; domain=abc\")\n        cookie_jar.set(\"e=f; domain=abc\")\n        self.assertEqual(cookie_jar.get(\"abc\"), \"e=f\")\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.set(\"a=b; c=d; domain=abc\")\n        cookie_jar.set(\"e=f; domain=.abc\")\n        self.assertEqual(cookie_jar.get(\"abc\"), \"e=f\")\n\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.set(\"a=b; c=d; domain=abc\")\n        cookie_jar.set(\"e=f; domain=xyz\")\n        self.assertEqual(cookie_jar.get(\"abc\"), \"a=b; c=d\")\n        self.assertEqual(cookie_jar.get(\"xyz\"), \"e=f\")\n        self.assertEqual(cookie_jar.get(\"something\"), \"\")\n\n    def testGet(self):\n        cookie_jar = SimpleCookieJar()\n        cookie_jar.set(\"a=b; c=d; domain=abc.com\")\n        self.assertEqual(cookie_jar.get(\"abc.com\"), \"a=b; c=d\")\n        self.assertEqual(cookie_jar.get(\"x.abc.com\"), \"a=b; c=d\")\n        self.assertEqual(cookie_jar.get(\"abc.com.es\"), \"\")\n        self.assertEqual(cookie_jar.get(\"xabc.com\"), \"\")\n\n        cookie_jar.set(\"a=b; c=d; domain=.abc.com\")\n        self.assertEqual(cookie_jar.get(\"abc.com\"), \"a=b; c=d\")\n        self.assertEqual(cookie_jar.get(\"x.abc.com\"), \"a=b; c=d\")\n        self.assertEqual(cookie_jar.get(\"abc.com.es\"), \"\")\n        self.assertEqual(cookie_jar.get(\"xabc.com\"), \"\")\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/tests/test_http.py",
    "content": "# -*- coding: utf-8 -*-\n#\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\n\nimport os\nimport os.path\nimport websocket as ws\nfrom websocket._http import proxy_info, read_headers, _open_proxied_socket, _tunnel\nimport sys\nsys.path[0:0] = [\"\"]\n\nif sys.version_info[0] == 2 and sys.version_info[1] < 7:\n    import unittest2 as unittest\nelse:\n    import unittest\n\n\nclass SockMock(object):\n    def __init__(self):\n        self.data = []\n        self.sent = []\n\n    def add_packet(self, data):\n        self.data.append(data)\n\n    def gettimeout(self):\n        return None\n\n    def recv(self, bufsize):\n        if self.data:\n            e = self.data.pop(0)\n            if isinstance(e, Exception):\n                raise e\n            if len(e) > bufsize:\n                self.data.insert(0, e[bufsize:])\n            return e[:bufsize]\n\n    def send(self, data):\n        self.sent.append(data)\n        return len(data)\n\n    def close(self):\n        pass\n\n\nclass HeaderSockMock(SockMock):\n\n    def __init__(self, fname):\n        SockMock.__init__(self)\n        path = os.path.join(os.path.dirname(__file__), fname)\n        with open(path, \"rb\") as f:\n            self.add_packet(f.read())\n\n\nclass OptsList():\n\n    def __init__(self):\n        self.timeout = 0\n        self.sockopt = []\n\n\nclass HttpTest(unittest.TestCase):\n\n    def testReadHeader(self):\n        status, header, status_message = read_headers(HeaderSockMock(\"data/header01.txt\"))\n        self.assertEqual(status, 101)\n        self.assertEqual(header[\"connection\"], \"Upgrade\")\n        # header02.txt is intentionally malformed\n        self.assertRaises(ws.WebSocketException, read_headers, HeaderSockMock(\"data/header02.txt\"))\n\n    def testTunnel(self):\n        self.assertRaises(ws.WebSocketProxyException, _tunnel, HeaderSockMock(\"data/header01.txt\"), \"example.com\", 80, (\"username\", \"password\"))\n        self.assertRaises(ws.WebSocketProxyException, _tunnel, HeaderSockMock(\"data/header02.txt\"), \"example.com\", 80, (\"username\", \"password\"))\n\n    def testConnect(self):\n        # Not currently testing an actual proxy connection, so just check whether TypeError is raised\n        self.assertRaises(TypeError, _open_proxied_socket, \"wss://example.com\", OptsList(), proxy_info(http_proxy_host=\"example.com\", http_proxy_port=\"8080\", proxy_type=\"http\"))\n        self.assertRaises(TypeError, _open_proxied_socket, \"wss://example.com\", OptsList(), proxy_info(http_proxy_host=\"example.com\", http_proxy_port=\"8080\", proxy_type=\"socks4\"))\n        self.assertRaises(TypeError, _open_proxied_socket, \"wss://example.com\", OptsList(), proxy_info(http_proxy_host=\"example.com\", http_proxy_port=\"8080\", proxy_type=\"socks5h\"))\n\n    def testProxyInfo(self):\n        self.assertEqual(proxy_info(http_proxy_host=\"127.0.0.1\", http_proxy_port=\"8080\", proxy_type=\"http\").type, \"http\")\n        self.assertRaises(ValueError, proxy_info, http_proxy_host=\"127.0.0.1\", http_proxy_port=\"8080\", proxy_type=\"badval\")\n        self.assertEqual(proxy_info(http_proxy_host=\"example.com\", http_proxy_port=\"8080\", proxy_type=\"http\").host, \"example.com\")\n        self.assertEqual(proxy_info(http_proxy_host=\"127.0.0.1\", http_proxy_port=\"8080\", proxy_type=\"http\").port, \"8080\")\n        self.assertEqual(proxy_info(http_proxy_host=\"127.0.0.1\", http_proxy_port=\"8080\", proxy_type=\"http\").auth, None)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/tests/test_url.py",
    "content": "# -*- coding: utf-8 -*-\n#\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\n\nimport sys\nimport os\n\nfrom websocket._url import get_proxy_info, parse_url, _is_address_in_network, _is_no_proxy_host\n\nif sys.version_info[0] == 2 and sys.version_info[1] < 7:\n    import unittest2 as unittest\nelse:\n    import unittest\nsys.path[0:0] = [\"\"]\n\n\nclass UrlTest(unittest.TestCase):\n\n    def test_address_in_network(self):\n        self.assertTrue(_is_address_in_network('127.0.0.1', '127.0.0.0/8'))\n        self.assertTrue(_is_address_in_network('127.1.0.1', '127.0.0.0/8'))\n        self.assertFalse(_is_address_in_network('127.1.0.1', '127.0.0.0/24'))\n\n    def testParseUrl(self):\n        p = parse_url(\"ws://www.example.com/r\")\n        self.assertEqual(p[0], \"www.example.com\")\n        self.assertEqual(p[1], 80)\n        self.assertEqual(p[2], \"/r\")\n        self.assertEqual(p[3], False)\n\n        p = parse_url(\"ws://www.example.com/r/\")\n        self.assertEqual(p[0], \"www.example.com\")\n        self.assertEqual(p[1], 80)\n        self.assertEqual(p[2], \"/r/\")\n        self.assertEqual(p[3], False)\n\n        p = parse_url(\"ws://www.example.com/\")\n        self.assertEqual(p[0], \"www.example.com\")\n        self.assertEqual(p[1], 80)\n        self.assertEqual(p[2], \"/\")\n        self.assertEqual(p[3], False)\n\n        p = parse_url(\"ws://www.example.com\")\n        self.assertEqual(p[0], \"www.example.com\")\n        self.assertEqual(p[1], 80)\n        self.assertEqual(p[2], \"/\")\n        self.assertEqual(p[3], False)\n\n        p = parse_url(\"ws://www.example.com:8080/r\")\n        self.assertEqual(p[0], \"www.example.com\")\n        self.assertEqual(p[1], 8080)\n        self.assertEqual(p[2], \"/r\")\n        self.assertEqual(p[3], False)\n\n        p = parse_url(\"ws://www.example.com:8080/\")\n        self.assertEqual(p[0], \"www.example.com\")\n        self.assertEqual(p[1], 8080)\n        self.assertEqual(p[2], \"/\")\n        self.assertEqual(p[3], False)\n\n        p = parse_url(\"ws://www.example.com:8080\")\n        self.assertEqual(p[0], \"www.example.com\")\n        self.assertEqual(p[1], 8080)\n        self.assertEqual(p[2], \"/\")\n        self.assertEqual(p[3], False)\n\n        p = parse_url(\"wss://www.example.com:8080/r\")\n        self.assertEqual(p[0], \"www.example.com\")\n        self.assertEqual(p[1], 8080)\n        self.assertEqual(p[2], \"/r\")\n        self.assertEqual(p[3], True)\n\n        p = parse_url(\"wss://www.example.com:8080/r?key=value\")\n        self.assertEqual(p[0], \"www.example.com\")\n        self.assertEqual(p[1], 8080)\n        self.assertEqual(p[2], \"/r?key=value\")\n        self.assertEqual(p[3], True)\n\n        self.assertRaises(ValueError, parse_url, \"http://www.example.com/r\")\n\n        if sys.version_info[0] == 2 and sys.version_info[1] < 7:\n            return\n\n        p = parse_url(\"ws://[2a03:4000:123:83::3]/r\")\n        self.assertEqual(p[0], \"2a03:4000:123:83::3\")\n        self.assertEqual(p[1], 80)\n        self.assertEqual(p[2], \"/r\")\n        self.assertEqual(p[3], False)\n\n        p = parse_url(\"ws://[2a03:4000:123:83::3]:8080/r\")\n        self.assertEqual(p[0], \"2a03:4000:123:83::3\")\n        self.assertEqual(p[1], 8080)\n        self.assertEqual(p[2], \"/r\")\n        self.assertEqual(p[3], False)\n\n        p = parse_url(\"wss://[2a03:4000:123:83::3]/r\")\n        self.assertEqual(p[0], \"2a03:4000:123:83::3\")\n        self.assertEqual(p[1], 443)\n        self.assertEqual(p[2], \"/r\")\n        self.assertEqual(p[3], True)\n\n        p = parse_url(\"wss://[2a03:4000:123:83::3]:8080/r\")\n        self.assertEqual(p[0], \"2a03:4000:123:83::3\")\n        self.assertEqual(p[1], 8080)\n        self.assertEqual(p[2], \"/r\")\n        self.assertEqual(p[3], True)\n\n\nclass IsNoProxyHostTest(unittest.TestCase):\n    def setUp(self):\n        self.no_proxy = os.environ.get(\"no_proxy\", None)\n        if \"no_proxy\" in os.environ:\n            del os.environ[\"no_proxy\"]\n\n    def tearDown(self):\n        if self.no_proxy:\n            os.environ[\"no_proxy\"] = self.no_proxy\n        elif \"no_proxy\" in os.environ:\n            del os.environ[\"no_proxy\"]\n\n    def testMatchAll(self):\n        self.assertTrue(_is_no_proxy_host(\"any.websocket.org\", ['*']))\n        self.assertTrue(_is_no_proxy_host(\"192.168.0.1\", ['*']))\n        self.assertTrue(_is_no_proxy_host(\"any.websocket.org\", ['other.websocket.org', '*']))\n        os.environ['no_proxy'] = '*'\n        self.assertTrue(_is_no_proxy_host(\"any.websocket.org\", None))\n        self.assertTrue(_is_no_proxy_host(\"192.168.0.1\", None))\n        os.environ['no_proxy'] = 'other.websocket.org, *'\n        self.assertTrue(_is_no_proxy_host(\"any.websocket.org\", None))\n\n    def testIpAddress(self):\n        self.assertTrue(_is_no_proxy_host(\"127.0.0.1\", ['127.0.0.1']))\n        self.assertFalse(_is_no_proxy_host(\"127.0.0.2\", ['127.0.0.1']))\n        self.assertTrue(_is_no_proxy_host(\"127.0.0.1\", ['other.websocket.org', '127.0.0.1']))\n        self.assertFalse(_is_no_proxy_host(\"127.0.0.2\", ['other.websocket.org', '127.0.0.1']))\n        os.environ['no_proxy'] = '127.0.0.1'\n        self.assertTrue(_is_no_proxy_host(\"127.0.0.1\", None))\n        self.assertFalse(_is_no_proxy_host(\"127.0.0.2\", None))\n        os.environ['no_proxy'] = 'other.websocket.org, 127.0.0.1'\n        self.assertTrue(_is_no_proxy_host(\"127.0.0.1\", None))\n        self.assertFalse(_is_no_proxy_host(\"127.0.0.2\", None))\n\n    def testIpAddressInRange(self):\n        self.assertTrue(_is_no_proxy_host(\"127.0.0.1\", ['127.0.0.0/8']))\n        self.assertTrue(_is_no_proxy_host(\"127.0.0.2\", ['127.0.0.0/8']))\n        self.assertFalse(_is_no_proxy_host(\"127.1.0.1\", ['127.0.0.0/24']))\n        os.environ['no_proxy'] = '127.0.0.0/8'\n        self.assertTrue(_is_no_proxy_host(\"127.0.0.1\", None))\n        self.assertTrue(_is_no_proxy_host(\"127.0.0.2\", None))\n        os.environ['no_proxy'] = '127.0.0.0/24'\n        self.assertFalse(_is_no_proxy_host(\"127.1.0.1\", None))\n\n    def testHostnameMatch(self):\n        self.assertTrue(_is_no_proxy_host(\"my.websocket.org\", ['my.websocket.org']))\n        self.assertTrue(_is_no_proxy_host(\"my.websocket.org\", ['other.websocket.org', 'my.websocket.org']))\n        self.assertFalse(_is_no_proxy_host(\"my.websocket.org\", ['other.websocket.org']))\n        os.environ['no_proxy'] = 'my.websocket.org'\n        self.assertTrue(_is_no_proxy_host(\"my.websocket.org\", None))\n        self.assertFalse(_is_no_proxy_host(\"other.websocket.org\", None))\n        os.environ['no_proxy'] = 'other.websocket.org, my.websocket.org'\n        self.assertTrue(_is_no_proxy_host(\"my.websocket.org\", None))\n\n    def testHostnameMatchDomain(self):\n        self.assertTrue(_is_no_proxy_host(\"any.websocket.org\", ['.websocket.org']))\n        self.assertTrue(_is_no_proxy_host(\"my.other.websocket.org\", ['.websocket.org']))\n        self.assertTrue(_is_no_proxy_host(\"any.websocket.org\", ['my.websocket.org', '.websocket.org']))\n        self.assertFalse(_is_no_proxy_host(\"any.websocket.com\", ['.websocket.org']))\n        os.environ['no_proxy'] = '.websocket.org'\n        self.assertTrue(_is_no_proxy_host(\"any.websocket.org\", None))\n        self.assertTrue(_is_no_proxy_host(\"my.other.websocket.org\", None))\n        self.assertFalse(_is_no_proxy_host(\"any.websocket.com\", None))\n        os.environ['no_proxy'] = 'my.websocket.org, .websocket.org'\n        self.assertTrue(_is_no_proxy_host(\"any.websocket.org\", None))\n\n\nclass ProxyInfoTest(unittest.TestCase):\n    def setUp(self):\n        self.http_proxy = os.environ.get(\"http_proxy\", None)\n        self.https_proxy = os.environ.get(\"https_proxy\", None)\n        self.no_proxy = os.environ.get(\"no_proxy\", None)\n        if \"http_proxy\" in os.environ:\n            del os.environ[\"http_proxy\"]\n        if \"https_proxy\" in os.environ:\n            del os.environ[\"https_proxy\"]\n        if \"no_proxy\" in os.environ:\n            del os.environ[\"no_proxy\"]\n\n    def tearDown(self):\n        if self.http_proxy:\n            os.environ[\"http_proxy\"] = self.http_proxy\n        elif \"http_proxy\" in os.environ:\n            del os.environ[\"http_proxy\"]\n\n        if self.https_proxy:\n            os.environ[\"https_proxy\"] = self.https_proxy\n        elif \"https_proxy\" in os.environ:\n            del os.environ[\"https_proxy\"]\n\n        if self.no_proxy:\n            os.environ[\"no_proxy\"] = self.no_proxy\n        elif \"no_proxy\" in os.environ:\n            del os.environ[\"no_proxy\"]\n\n    def testProxyFromArgs(self):\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", False, proxy_host=\"localhost\"), (\"localhost\", 0, None))\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", False, proxy_host=\"localhost\", proxy_port=3128),\n                         (\"localhost\", 3128, None))\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", True, proxy_host=\"localhost\"), (\"localhost\", 0, None))\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", True, proxy_host=\"localhost\", proxy_port=3128),\n                         (\"localhost\", 3128, None))\n\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", False, proxy_host=\"localhost\", proxy_auth=(\"a\", \"b\")),\n                         (\"localhost\", 0, (\"a\", \"b\")))\n        self.assertEqual(\n            get_proxy_info(\"echo.websocket.org\", False, proxy_host=\"localhost\", proxy_port=3128, proxy_auth=(\"a\", \"b\")),\n            (\"localhost\", 3128, (\"a\", \"b\")))\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", True, proxy_host=\"localhost\", proxy_auth=(\"a\", \"b\")),\n                         (\"localhost\", 0, (\"a\", \"b\")))\n        self.assertEqual(\n            get_proxy_info(\"echo.websocket.org\", True, proxy_host=\"localhost\", proxy_port=3128, proxy_auth=(\"a\", \"b\")),\n            (\"localhost\", 3128, (\"a\", \"b\")))\n\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", True, proxy_host=\"localhost\", proxy_port=3128,\n                                        no_proxy=[\"example.com\"], proxy_auth=(\"a\", \"b\")),\n                         (\"localhost\", 3128, (\"a\", \"b\")))\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", True, proxy_host=\"localhost\", proxy_port=3128,\n                                        no_proxy=[\"echo.websocket.org\"], proxy_auth=(\"a\", \"b\")),\n                         (None, 0, None))\n\n    def testProxyFromEnv(self):\n        os.environ[\"http_proxy\"] = \"http://localhost/\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", False), (\"localhost\", None, None))\n        os.environ[\"http_proxy\"] = \"http://localhost:3128/\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", False), (\"localhost\", 3128, None))\n\n        os.environ[\"http_proxy\"] = \"http://localhost/\"\n        os.environ[\"https_proxy\"] = \"http://localhost2/\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", False), (\"localhost\", None, None))\n        os.environ[\"http_proxy\"] = \"http://localhost:3128/\"\n        os.environ[\"https_proxy\"] = \"http://localhost2:3128/\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", False), (\"localhost\", 3128, None))\n\n        os.environ[\"http_proxy\"] = \"http://localhost/\"\n        os.environ[\"https_proxy\"] = \"http://localhost2/\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", True), (\"localhost2\", None, None))\n        os.environ[\"http_proxy\"] = \"http://localhost:3128/\"\n        os.environ[\"https_proxy\"] = \"http://localhost2:3128/\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", True), (\"localhost2\", 3128, None))\n\n        os.environ[\"http_proxy\"] = \"http://a:b@localhost/\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", False), (\"localhost\", None, (\"a\", \"b\")))\n        os.environ[\"http_proxy\"] = \"http://a:b@localhost:3128/\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", False), (\"localhost\", 3128, (\"a\", \"b\")))\n\n        os.environ[\"http_proxy\"] = \"http://a:b@localhost/\"\n        os.environ[\"https_proxy\"] = \"http://a:b@localhost2/\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", False), (\"localhost\", None, (\"a\", \"b\")))\n        os.environ[\"http_proxy\"] = \"http://a:b@localhost:3128/\"\n        os.environ[\"https_proxy\"] = \"http://a:b@localhost2:3128/\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", False), (\"localhost\", 3128, (\"a\", \"b\")))\n\n        os.environ[\"http_proxy\"] = \"http://a:b@localhost/\"\n        os.environ[\"https_proxy\"] = \"http://a:b@localhost2/\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", True), (\"localhost2\", None, (\"a\", \"b\")))\n        os.environ[\"http_proxy\"] = \"http://a:b@localhost:3128/\"\n        os.environ[\"https_proxy\"] = \"http://a:b@localhost2:3128/\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", True), (\"localhost2\", 3128, (\"a\", \"b\")))\n\n        os.environ[\"http_proxy\"] = \"http://a:b@localhost/\"\n        os.environ[\"https_proxy\"] = \"http://a:b@localhost2/\"\n        os.environ[\"no_proxy\"] = \"example1.com,example2.com\"\n        self.assertEqual(get_proxy_info(\"example.1.com\", True), (\"localhost2\", None, (\"a\", \"b\")))\n        os.environ[\"http_proxy\"] = \"http://a:b@localhost:3128/\"\n        os.environ[\"https_proxy\"] = \"http://a:b@localhost2:3128/\"\n        os.environ[\"no_proxy\"] = \"example1.com,example2.com, echo.websocket.org\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", True), (None, 0, None))\n        os.environ[\"http_proxy\"] = \"http://a:b@localhost:3128/\"\n        os.environ[\"https_proxy\"] = \"http://a:b@localhost2:3128/\"\n        os.environ[\"no_proxy\"] = \"example1.com,example2.com, .websocket.org\"\n        self.assertEqual(get_proxy_info(\"echo.websocket.org\", True), (None, 0, None))\n\n        os.environ[\"http_proxy\"] = \"http://a:b@localhost:3128/\"\n        os.environ[\"https_proxy\"] = \"http://a:b@localhost2:3128/\"\n        os.environ[\"no_proxy\"] = \"127.0.0.0/8, 192.168.0.0/16\"\n        self.assertEqual(get_proxy_info(\"127.0.0.1\", False), (None, 0, None))\n        self.assertEqual(get_proxy_info(\"192.168.1.1\", False), (None, 0, None))\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "openpype/vendor/python/python_2/websocket/tests/test_websocket.py",
    "content": "# -*- coding: utf-8 -*-\n#\n\"\"\"\n\n\"\"\"\n\n\"\"\"\nwebsocket - WebSocket client library for Python\n\nCopyright (C) 2010 Hiroki Ohtani(liris)\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\n\"\"\"\n\nimport sys\nsys.path[0:0] = [\"\"]\n\nimport os\nimport os.path\nimport socket\n\nimport six\n\n# websocket-client\nimport websocket as ws\nfrom websocket._handshake import _create_sec_websocket_key, \\\n    _validate as _validate_header\nfrom websocket._http import read_headers\nfrom websocket._utils import validate_utf8\n\nif six.PY3:\n    from base64 import decodebytes as base64decode\nelse:\n    from base64 import decodestring as base64decode\n\nif sys.version_info[0] == 2 and sys.version_info[1] < 7:\n    import unittest2 as unittest\nelse:\n    import unittest\n\ntry:\n    from ssl import SSLError\nexcept ImportError:\n    # dummy class of SSLError for ssl none-support environment.\n    class SSLError(Exception):\n        pass\n\n# Skip test to access the internet.\nTEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1'\nTRACEABLE = True\n\n\ndef create_mask_key(_):\n    return \"abcd\"\n\n\nclass SockMock(object):\n    def __init__(self):\n        self.data = []\n        self.sent = []\n\n    def add_packet(self, data):\n        self.data.append(data)\n\n    def gettimeout(self):\n        return None\n\n    def recv(self, bufsize):\n        if self.data:\n            e = self.data.pop(0)\n            if isinstance(e, Exception):\n                raise e\n            if len(e) > bufsize:\n                self.data.insert(0, e[bufsize:])\n            return e[:bufsize]\n\n    def send(self, data):\n        self.sent.append(data)\n        return len(data)\n\n    def close(self):\n        pass\n\n\nclass HeaderSockMock(SockMock):\n\n    def __init__(self, fname):\n        SockMock.__init__(self)\n        path = os.path.join(os.path.dirname(__file__), fname)\n        with open(path, \"rb\") as f:\n            self.add_packet(f.read())\n\n\nclass WebSocketTest(unittest.TestCase):\n    def setUp(self):\n        ws.enableTrace(TRACEABLE)\n\n    def tearDown(self):\n        pass\n\n    def testDefaultTimeout(self):\n        self.assertEqual(ws.getdefaulttimeout(), None)\n        ws.setdefaulttimeout(10)\n        self.assertEqual(ws.getdefaulttimeout(), 10)\n        ws.setdefaulttimeout(None)\n\n    def testWSKey(self):\n        key = _create_sec_websocket_key()\n        self.assertTrue(key != 24)\n        self.assertTrue(six.u(\"¥n\") not in key)\n\n    def testNonce(self):\n        \"\"\" WebSocket key should be a random 16-byte nonce.\n        \"\"\"\n        key = _create_sec_websocket_key()\n        nonce = base64decode(key.encode(\"utf-8\"))\n        self.assertEqual(16, len(nonce))\n\n    def testWsUtils(self):\n        key = \"c6b8hTg4EeGb2gQMztV1/g==\"\n        required_header = {\n            \"upgrade\": \"websocket\",\n            \"connection\": \"upgrade\",\n            \"sec-websocket-accept\": \"Kxep+hNu9n51529fGidYu7a3wO0=\"}\n        self.assertEqual(_validate_header(required_header, key, None), (True, None))\n\n        header = required_header.copy()\n        header[\"upgrade\"] = \"http\"\n        self.assertEqual(_validate_header(header, key, None), (False, None))\n        del header[\"upgrade\"]\n        self.assertEqual(_validate_header(header, key, None), (False, None))\n\n        header = required_header.copy()\n        header[\"connection\"] = \"something\"\n        self.assertEqual(_validate_header(header, key, None), (False, None))\n        del header[\"connection\"]\n        self.assertEqual(_validate_header(header, key, None), (False, None))\n\n        header = required_header.copy()\n        header[\"sec-websocket-accept\"] = \"something\"\n        self.assertEqual(_validate_header(header, key, None), (False, None))\n        del header[\"sec-websocket-accept\"]\n        self.assertEqual(_validate_header(header, key, None), (False, None))\n\n        header = required_header.copy()\n        header[\"sec-websocket-protocol\"] = \"sub1\"\n        self.assertEqual(_validate_header(header, key, [\"sub1\", \"sub2\"]), (True, \"sub1\"))\n        self.assertEqual(_validate_header(header, key, [\"sub2\", \"sub3\"]), (False, None))\n\n        header = required_header.copy()\n        header[\"sec-websocket-protocol\"] = \"sUb1\"\n        self.assertEqual(_validate_header(header, key, [\"Sub1\", \"suB2\"]), (True, \"sub1\"))\n\n        header = required_header.copy()\n        self.assertEqual(_validate_header(header, key, [\"Sub1\", \"suB2\"]), (False, None))\n\n    def testReadHeader(self):\n        status, header, status_message = read_headers(HeaderSockMock(\"data/header01.txt\"))\n        self.assertEqual(status, 101)\n        self.assertEqual(header[\"connection\"], \"Upgrade\")\n\n        status, header, status_message = read_headers(HeaderSockMock(\"data/header03.txt\"))\n        self.assertEqual(status, 101)\n        self.assertEqual(header[\"connection\"], \"Upgrade, Keep-Alive\")\n\n        HeaderSockMock(\"data/header02.txt\")\n        self.assertRaises(ws.WebSocketException, read_headers, HeaderSockMock(\"data/header02.txt\"))\n\n    def testSend(self):\n        # TODO: add longer frame data\n        sock = ws.WebSocket()\n        sock.set_mask_key(create_mask_key)\n        s = sock.sock = HeaderSockMock(\"data/header01.txt\")\n        sock.send(\"Hello\")\n        self.assertEqual(s.sent[0], six.b(\"\\x81\\x85abcd)\\x07\\x0f\\x08\\x0e\"))\n\n        sock.send(\"こんにちは\")\n        self.assertEqual(s.sent[1], six.b(\"\\x81\\x8fabcd\\x82\\xe3\\xf0\\x87\\xe3\\xf1\\x80\\xe5\\xca\\x81\\xe2\\xc5\\x82\\xe3\\xcc\"))\n\n        sock.send(u\"こんにちは\")\n        self.assertEqual(s.sent[1], six.b(\"\\x81\\x8fabcd\\x82\\xe3\\xf0\\x87\\xe3\\xf1\\x80\\xe5\\xca\\x81\\xe2\\xc5\\x82\\xe3\\xcc\"))\n\n#        sock.send(\"x\" * 5000)\n#        self.assertEqual(s.sent[1], six.b(\"\\x81\\x8fabcd\\x82\\xe3\\xf0\\x87\\xe3\\xf1\\x80\\xe5\\xca\\x81\\xe2\\xc5\\x82\\xe3\\xcc\"))\n\n        self.assertEqual(sock.send_binary(b'1111111111101'), 19)\n\n    def testRecv(self):\n        # TODO: add longer frame data\n        sock = ws.WebSocket()\n        s = sock.sock = SockMock()\n        something = six.b(\"\\x81\\x8fabcd\\x82\\xe3\\xf0\\x87\\xe3\\xf1\\x80\\xe5\\xca\\x81\\xe2\\xc5\\x82\\xe3\\xcc\")\n        s.add_packet(something)\n        data = sock.recv()\n        self.assertEqual(data, \"こんにちは\")\n\n        s.add_packet(six.b(\"\\x81\\x85abcd)\\x07\\x0f\\x08\\x0e\"))\n        data = sock.recv()\n        self.assertEqual(data, \"Hello\")\n\n    @unittest.skipUnless(TEST_WITH_INTERNET, \"Internet-requiring tests are disabled\")\n    def testIter(self):\n        count = 2\n        for _ in ws.create_connection('wss://stream.meetup.com/2/rsvps'):\n            count -= 1\n            if count == 0:\n                break\n\n    @unittest.skipUnless(TEST_WITH_INTERNET, \"Internet-requiring tests are disabled\")\n    def testNext(self):\n        sock = ws.create_connection('wss://stream.meetup.com/2/rsvps')\n        self.assertEqual(str, type(next(sock)))\n\n    def testInternalRecvStrict(self):\n        sock = ws.WebSocket()\n        s = sock.sock = SockMock()\n        s.add_packet(six.b(\"foo\"))\n        s.add_packet(socket.timeout())\n        s.add_packet(six.b(\"bar\"))\n        # s.add_packet(SSLError(\"The read operation timed out\"))\n        s.add_packet(six.b(\"baz\"))\n        with self.assertRaises(ws.WebSocketTimeoutException):\n            sock.frame_buffer.recv_strict(9)\n        # if six.PY2:\n        #     with self.assertRaises(ws.WebSocketTimeoutException):\n        #         data = sock._recv_strict(9)\n        # else:\n        #     with self.assertRaises(SSLError):\n        #         data = sock._recv_strict(9)\n        data = sock.frame_buffer.recv_strict(9)\n        self.assertEqual(data, six.b(\"foobarbaz\"))\n        with self.assertRaises(ws.WebSocketConnectionClosedException):\n            sock.frame_buffer.recv_strict(1)\n\n    def testRecvTimeout(self):\n        sock = ws.WebSocket()\n        s = sock.sock = SockMock()\n        s.add_packet(six.b(\"\\x81\"))\n        s.add_packet(socket.timeout())\n        s.add_packet(six.b(\"\\x8dabcd\\x29\\x07\\x0f\\x08\\x0e\"))\n        s.add_packet(socket.timeout())\n        s.add_packet(six.b(\"\\x4e\\x43\\x33\\x0e\\x10\\x0f\\x00\\x40\"))\n        with self.assertRaises(ws.WebSocketTimeoutException):\n            sock.recv()\n        with self.assertRaises(ws.WebSocketTimeoutException):\n            sock.recv()\n        data = sock.recv()\n        self.assertEqual(data, \"Hello, World!\")\n        with self.assertRaises(ws.WebSocketConnectionClosedException):\n            sock.recv()\n\n    def testRecvWithSimpleFragmentation(self):\n        sock = ws.WebSocket()\n        s = sock.sock = SockMock()\n        # OPCODE=TEXT, FIN=0, MSG=\"Brevity is \"\n        s.add_packet(six.b(\"\\x01\\x8babcd#\\x10\\x06\\x12\\x08\\x16\\x1aD\\x08\\x11C\"))\n        # OPCODE=CONT, FIN=1, MSG=\"the soul of wit\"\n        s.add_packet(six.b(\"\\x80\\x8fabcd\\x15\\n\\x06D\\x12\\r\\x16\\x08A\\r\\x05D\\x16\\x0b\\x17\"))\n        data = sock.recv()\n        self.assertEqual(data, \"Brevity is the soul of wit\")\n        with self.assertRaises(ws.WebSocketConnectionClosedException):\n            sock.recv()\n\n    def testRecvWithFireEventOfFragmentation(self):\n        sock = ws.WebSocket(fire_cont_frame=True)\n        s = sock.sock = SockMock()\n        # OPCODE=TEXT, FIN=0, MSG=\"Brevity is \"\n        s.add_packet(six.b(\"\\x01\\x8babcd#\\x10\\x06\\x12\\x08\\x16\\x1aD\\x08\\x11C\"))\n        # OPCODE=CONT, FIN=0, MSG=\"Brevity is \"\n        s.add_packet(six.b(\"\\x00\\x8babcd#\\x10\\x06\\x12\\x08\\x16\\x1aD\\x08\\x11C\"))\n        # OPCODE=CONT, FIN=1, MSG=\"the soul of wit\"\n        s.add_packet(six.b(\"\\x80\\x8fabcd\\x15\\n\\x06D\\x12\\r\\x16\\x08A\\r\\x05D\\x16\\x0b\\x17\"))\n\n        _, data = sock.recv_data()\n        self.assertEqual(data, six.b(\"Brevity is \"))\n        _, data = sock.recv_data()\n        self.assertEqual(data, six.b(\"Brevity is \"))\n        _, data = sock.recv_data()\n        self.assertEqual(data, six.b(\"the soul of wit\"))\n\n        # OPCODE=CONT, FIN=0, MSG=\"Brevity is \"\n        s.add_packet(six.b(\"\\x80\\x8babcd#\\x10\\x06\\x12\\x08\\x16\\x1aD\\x08\\x11C\"))\n\n        with self.assertRaises(ws.WebSocketException):\n            sock.recv_data()\n\n        with self.assertRaises(ws.WebSocketConnectionClosedException):\n            sock.recv()\n\n    def testClose(self):\n        sock = ws.WebSocket()\n        sock.sock = SockMock()\n        sock.connected = True\n        sock.close()\n        self.assertEqual(sock.connected, False)\n\n        sock = ws.WebSocket()\n        s = sock.sock = SockMock()\n        sock.connected = True\n        s.add_packet(six.b('\\x88\\x80\\x17\\x98p\\x84'))\n        sock.recv()\n        self.assertEqual(sock.connected, False)\n\n    def testRecvContFragmentation(self):\n        sock = ws.WebSocket()\n        s = sock.sock = SockMock()\n        # OPCODE=CONT, FIN=1, MSG=\"the soul of wit\"\n        s.add_packet(six.b(\"\\x80\\x8fabcd\\x15\\n\\x06D\\x12\\r\\x16\\x08A\\r\\x05D\\x16\\x0b\\x17\"))\n        self.assertRaises(ws.WebSocketException, sock.recv)\n\n    def testRecvWithProlongedFragmentation(self):\n        sock = ws.WebSocket()\n        s = sock.sock = SockMock()\n        # OPCODE=TEXT, FIN=0, MSG=\"Once more unto the breach, \"\n        s.add_packet(six.b(\"\\x01\\x9babcd.\\x0c\\x00\\x01A\\x0f\\x0c\\x16\\x04B\\x16\\n\\x15\"\n                           \"\\rC\\x10\\t\\x07C\\x06\\x13\\x07\\x02\\x07\\tNC\"))\n        # OPCODE=CONT, FIN=0, MSG=\"dear friends, \"\n        s.add_packet(six.b(\"\\x00\\x8eabcd\\x05\\x07\\x02\\x16A\\x04\\x11\\r\\x04\\x0c\\x07\"\n                           \"\\x17MB\"))\n        # OPCODE=CONT, FIN=1, MSG=\"once more\"\n        s.add_packet(six.b(\"\\x80\\x89abcd\\x0e\\x0c\\x00\\x01A\\x0f\\x0c\\x16\\x04\"))\n        data = sock.recv()\n        self.assertEqual(\n            data,\n            \"Once more unto the breach, dear friends, once more\")\n        with self.assertRaises(ws.WebSocketConnectionClosedException):\n            sock.recv()\n\n    def testRecvWithFragmentationAndControlFrame(self):\n        sock = ws.WebSocket()\n        sock.set_mask_key(create_mask_key)\n        s = sock.sock = SockMock()\n        # OPCODE=TEXT, FIN=0, MSG=\"Too much \"\n        s.add_packet(six.b(\"\\x01\\x89abcd5\\r\\x0cD\\x0c\\x17\\x00\\x0cA\"))\n        # OPCODE=PING, FIN=1, MSG=\"Please PONG this\"\n        s.add_packet(six.b(\"\\x89\\x90abcd1\\x0e\\x06\\x05\\x12\\x07C4.,$D\\x15\\n\\n\\x17\"))\n        # OPCODE=CONT, FIN=1, MSG=\"of a good thing\"\n        s.add_packet(six.b(\"\\x80\\x8fabcd\\x0e\\x04C\\x05A\\x05\\x0c\\x0b\\x05B\\x17\\x0c\"\n                           \"\\x08\\x0c\\x04\"))\n        data = sock.recv()\n        self.assertEqual(data, \"Too much of a good thing\")\n        with self.assertRaises(ws.WebSocketConnectionClosedException):\n            sock.recv()\n        self.assertEqual(\n            s.sent[0],\n            six.b(\"\\x8a\\x90abcd1\\x0e\\x06\\x05\\x12\\x07C4.,$D\\x15\\n\\n\\x17\"))\n\n    @unittest.skipUnless(TEST_WITH_INTERNET, \"Internet-requiring tests are disabled\")\n    def testWebSocket(self):\n        s = ws.create_connection(\"ws://echo.websocket.org/\")\n        self.assertNotEqual(s, None)\n        s.send(\"Hello, World\")\n        result = s.recv()\n        self.assertEqual(result, \"Hello, World\")\n\n        s.send(u\"こにゃにゃちは、世界\")\n        result = s.recv()\n        self.assertEqual(result, \"こにゃにゃちは、世界\")\n        self.assertRaises(ValueError, s.send_close, -1, \"\")\n        s.close()\n\n    @unittest.skipUnless(TEST_WITH_INTERNET, \"Internet-requiring tests are disabled\")\n    def testPingPong(self):\n        s = ws.create_connection(\"ws://echo.websocket.org/\")\n        self.assertNotEqual(s, None)\n        s.ping(\"Hello\")\n        s.pong(\"Hi\")\n        s.close()\n\n    @unittest.skipUnless(TEST_WITH_INTERNET, \"Internet-requiring tests are disabled\")\n    def testSecureWebSocket(self):\n        import ssl\n        s = ws.create_connection(\"wss://api.bitfinex.com/ws/2\")\n        self.assertNotEqual(s, None)\n        self.assertTrue(isinstance(s.sock, ssl.SSLSocket))\n        self.assertEqual(s.getstatus(), 101)\n        self.assertNotEqual(s.getheaders(), None)\n        s.close()\n\n    @unittest.skipUnless(TEST_WITH_INTERNET, \"Internet-requiring tests are disabled\")\n    def testWebSocketWithCustomHeader(self):\n        s = ws.create_connection(\"ws://echo.websocket.org/\",\n                                 headers={\"User-Agent\": \"PythonWebsocketClient\"})\n        self.assertNotEqual(s, None)\n        s.send(\"Hello, World\")\n        result = s.recv()\n        self.assertEqual(result, \"Hello, World\")\n        self.assertRaises(ValueError, s.close, -1, \"\")\n        s.close()\n\n    @unittest.skipUnless(TEST_WITH_INTERNET, \"Internet-requiring tests are disabled\")\n    def testAfterClose(self):\n        s = ws.create_connection(\"ws://echo.websocket.org/\")\n        self.assertNotEqual(s, None)\n        s.close()\n        self.assertRaises(ws.WebSocketConnectionClosedException, s.send, \"Hello\")\n        self.assertRaises(ws.WebSocketConnectionClosedException, s.recv)\n\n\nclass SockOptTest(unittest.TestCase):\n    @unittest.skipUnless(TEST_WITH_INTERNET, \"Internet-requiring tests are disabled\")\n    def testSockOpt(self):\n        sockopt = ((socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),)\n        s = ws.create_connection(\"ws://echo.websocket.org\", sockopt=sockopt)\n        self.assertNotEqual(s.sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY), 0)\n        s.close()\n\n\nclass UtilsTest(unittest.TestCase):\n    def testUtf8Validator(self):\n        state = validate_utf8(six.b('\\xf0\\x90\\x80\\x80'))\n        self.assertEqual(state, True)\n        state = validate_utf8(six.b('\\xce\\xba\\xe1\\xbd\\xb9\\xcf\\x83\\xce\\xbc\\xce\\xb5\\xed\\xa0\\x80edited'))\n        self.assertEqual(state, False)\n        state = validate_utf8(six.b(''))\n        self.assertEqual(state, True)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "openpype/vendor/python/python_3/README.md",
    "content": "## Info\nOnly **Python 3** modules are here."
  },
  {
    "path": "openpype/version.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Package declaring Pype version.\"\"\"\n__version__ = \"3.18.12-nightly.26\"\n"
  },
  {
    "path": "openpype/widgets/README.md",
    "content": "# Widgets\n\n## Splash Screen\n\nThis widget is used for executing a monitoring progress of a process which has been executed on a different thread.\n\nTo properly use this widget certain preparation has to be done in order to correctly execute the process and show the\nsplash screen.\n\n### Prerequisites\n\nIn order to run a function or an operation on another thread, a `QtCore.QObject` class needs to be created with the\ndesired code. The class has to have a method as an entry point for the thread to execute the code.\n\nFor utilizing the functionalities of the splash screen, certain signals need to be declared to let it know what is\nhappening in the thread and how is it progressing. It is also recommended to have a function to set up certain variables\nwhich are needed in the worker's code\n\nFor example:\n```python\nfrom qtpy import QtCore\n\nclass ExampleWorker(QtCore.QObject):\n    \n    finished = QtCore.Signal()\n    failed = QtCore.Signal(str)\n    progress = QtCore.Signal(int)\n    log = QtCore.Signal(str)\n    stage_begin = QtCore.Signal(str)\n    \n    foo = None\n    bar = None\n    \n    def run(self):\n        # The code goes here\n        print(\"Hello world!\")\n        self.finished.emit()\n        \n    def setup(self,\n              foo: str,\n              bar: str,):\n        self.foo = foo\n        self.bar = bar\n```\n\n### Creating the splash screen\n\n```python\nimport os\nfrom qtpy import QtCore\nfrom pathlib import Path\nfrom openpype.widgets.splash_screen import SplashScreen\nfrom openpype import resources\n\n\ndef exec_plugin_install( engine_path: Path, env: dict = None):\n    env = env or os.environ\n    q_thread = QtCore.QThread()\n    example_worker = ExampleWorker()\n\n    q_thread.started.connect(example_worker.run)\n    example_worker.setup(engine_path, env)\n    example_worker.moveToThread(q_thread)\n\n    splash_screen = SplashScreen(\"Executing process ...\",\n                                 resources.get_openpype_icon_filepath())\n\n    # set up the splash screen with necessary events\n    example_worker.installing.connect(splash_screen.update_top_label_text)\n    example_worker.progress.connect(splash_screen.update_progress)\n    example_worker.log.connect(splash_screen.append_log)\n    example_worker.finished.connect(splash_screen.quit_and_close)\n    example_worker.failed.connect(splash_screen.fail)\n\n    splash_screen.start_thread(q_thread)\n    splash_screen.show_ui()\n```\n\nIn this example code, before executing the process the worker needs to be instantiated and moved onto a newly created\n`QtCore.QThread` object. After this, needed signals have to be connected to the desired slots to make full use of\nthe splash screen. Finally, the `start_thread` and `show_ui` is called.\n\n**Note that when the `show_ui` function is called the thread is blocked until the splash screen quits automatically, or \nit is closed by the user in case the process fails! The `start_thread` method in that case must be called before\nshowing the UI!**\n\nThe most important signals are\n```python\nq_thread.started.connect(example_worker.run)\n```\n and\n```python\nexample_worker.finished.connect(splash_screen.quit_and_close)\n```\n\nThese ensure that when the `start_thread` method is called (which takes as a parameter the `QtCore.QThread` object and\nsaves it as a reference), the `QThread` object starts and signals the worker to\nstart executing its own code. Once the worker is done and emits a signal that it has finished with the `quit_and_close`\nslot, the splash screen quits the `QtCore.QThread` and closes itself.\n\nIt is highly recommended to also use the `fail` slot in case an exception or other error occurs during the execution of\nthe worker's code (You would use in this case the `failed` signal in the `ExampleWorker`).\n"
  },
  {
    "path": "openpype/widgets/__init__.py",
    "content": "from .password_dialog import PasswordDialog\n\n\n__all__ = (\n    \"PasswordDialog\",\n)\n"
  },
  {
    "path": "openpype/widgets/color_widgets/__init__.py",
    "content": "from .color_picker_widget import (\n    ColorPickerWidget\n)\n\nfrom .color_view import (\n    draw_checkerboard_tile\n)\n\n\n__all__ = (\n    \"ColorPickerWidget\",\n\n    \"draw_checkerboard_tile\"\n)\n"
  },
  {
    "path": "openpype/widgets/color_widgets/color_inputs.py",
    "content": "import re\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom .color_view import draw_checkerboard_tile\n\n\nclass AlphaSlider(QtWidgets.QSlider):\n    def __init__(self, *args, **kwargs):\n        super(AlphaSlider, self).__init__(*args, **kwargs)\n        self._mouse_clicked = False\n        self._handle_size = 0\n\n        self.setSingleStep(1)\n        self.setMinimum(0)\n        self.setMaximum(255)\n        self.setValue(255)\n\n        self._handle_brush = QtGui.QBrush(QtGui.QColor(127, 127, 127))\n\n    def mousePressEvent(self, event):\n        self._mouse_clicked = True\n        if event.button() == QtCore.Qt.LeftButton:\n            self._set_value_to_pos(event.pos())\n            return event.accept()\n        return super(AlphaSlider, self).mousePressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        if self._mouse_clicked:\n            self._set_value_to_pos(event.pos())\n\n        super(AlphaSlider, self).mouseMoveEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        self._mouse_clicked = True\n        super(AlphaSlider, self).mouseReleaseEvent(event)\n\n    def _set_value_to_pos(self, pos):\n        if self.orientation() == QtCore.Qt.Horizontal:\n            self._set_value_to_pos_x(pos.x())\n        else:\n            self._set_value_to_pos_y(pos.y())\n\n    def _set_value_to_pos_x(self, pos_x):\n        _range = self.maximum() - self.minimum()\n        handle_size = self._handle_size\n        half_handle = handle_size / 2\n        pos_x -= half_handle\n        width = self.width() - handle_size\n        value = ((_range * pos_x) / width) + self.minimum()\n        self.setValue(value)\n\n    def _set_value_to_pos_y(self, pos_y):\n        _range = self.maximum() - self.minimum()\n        handle_size = self._handle_size\n        half_handle = handle_size / 2\n        pos_y = self.height() - pos_y - half_handle\n        height = self.height() - handle_size\n        value = (_range * pos_y / height) + self.minimum()\n        self.setValue(value)\n\n    def paintEvent(self, event):\n        painter = QtGui.QPainter(self)\n        opt = QtWidgets.QStyleOptionSlider()\n        self.initStyleOption(opt)\n\n        painter.fillRect(event.rect(), QtCore.Qt.transparent)\n\n        painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)\n\n        horizontal = self.orientation() == QtCore.Qt.Horizontal\n\n        rect = self.style().subControlRect(\n            QtWidgets.QStyle.CC_Slider,\n            opt,\n            QtWidgets.QStyle.SC_SliderGroove,\n            self\n        )\n\n        _range = self.maximum() - self.minimum()\n        _offset = self.value() - self.minimum()\n        if horizontal:\n            _handle_half = rect.height() / 2\n            _handle_size = _handle_half * 2\n            width = rect.width() - _handle_size\n            pos_x = ((width / _range) * _offset)\n            pos_y = rect.center().y() - _handle_half + 1\n        else:\n            _handle_half = rect.width() / 2\n            _handle_size = _handle_half * 2\n            height = rect.height() - _handle_size\n            pos_x = rect.center().x() - _handle_half + 1\n            pos_y = height - ((height / _range) * _offset)\n\n        handle_rect = QtCore.QRect(\n            pos_x, pos_y, _handle_size, _handle_size\n        )\n\n        self._handle_size = _handle_size\n        _offset = 2\n        _size = _handle_size - _offset\n        if horizontal:\n            if rect.height() > _size:\n                new_rect = QtCore.QRect(0, 0, rect.width(), _size)\n                center_point = QtCore.QPoint(\n                    rect.center().x(), handle_rect.center().y()\n                )\n                new_rect.moveCenter(center_point)\n                rect = new_rect\n\n            ratio = rect.height() / 2\n\n        else:\n            if rect.width() > _size:\n                new_rect = QtCore.QRect(0, 0, _size, rect.height())\n                center_point = QtCore.QPoint(\n                    handle_rect.center().x(), rect.center().y()\n                )\n                new_rect.moveCenter(center_point)\n                rect = new_rect\n\n            ratio = rect.width() / 2\n\n        painter.save()\n        clip_path = QtGui.QPainterPath()\n        clip_path.addRoundedRect(rect, ratio, ratio)\n        painter.setClipPath(clip_path)\n        checker_size = int(_handle_size / 3)\n        if checker_size == 0:\n            checker_size = 1\n        checkerboard = draw_checkerboard_tile(\n            checker_size, QtGui.QColor(173, 173, 173), QtGui.QColor(27, 27, 27)\n        )\n        painter.drawTiledPixmap(rect, checkerboard)\n        gradient = QtGui.QLinearGradient(rect.topLeft(), rect.bottomRight())\n        gradient.setColorAt(0, QtCore.Qt.transparent)\n        gradient.setColorAt(1, QtCore.Qt.white)\n        painter.setPen(QtCore.Qt.NoPen)\n        painter.fillRect(rect, gradient)\n        painter.restore()\n\n        painter.save()\n        painter.setPen(QtCore.Qt.NoPen)\n        painter.setBrush(self._handle_brush)\n        painter.drawEllipse(handle_rect)\n        painter.restore()\n\n\nclass AlphaInputs(QtWidgets.QWidget):\n    alpha_changed = QtCore.Signal(int)\n\n    def __init__(self, parent=None):\n        super(AlphaInputs, self).__init__(parent)\n\n        self._block_changes = False\n        self.alpha_value = None\n\n        percent_input = QtWidgets.QDoubleSpinBox(self)\n        percent_input.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n        percent_input.setMinimum(0)\n        percent_input.setMaximum(100)\n        percent_input.setDecimals(2)\n\n        int_input = QtWidgets.QSpinBox(self)\n        int_input.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n        int_input.setMinimum(0)\n        int_input.setMaximum(255)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(int_input)\n        layout.addWidget(QtWidgets.QLabel(\"0-255\"))\n        layout.addWidget(percent_input)\n        layout.addWidget(QtWidgets.QLabel(\"%\"))\n\n        percent_input.valueChanged.connect(self._on_percent_change)\n        int_input.valueChanged.connect(self._on_int_change)\n\n        self.percent_input = percent_input\n        self.int_input = int_input\n\n        self.set_alpha(255)\n\n    def set_alpha(self, alpha):\n        if alpha == self.alpha_value:\n            return\n        self.alpha_value = alpha\n\n        self.update_alpha()\n\n    def _on_percent_change(self):\n        if self._block_changes:\n            return\n        self.alpha_value = int(self.percent_input.value() * 255 / 100)\n        self.alpha_changed.emit(self.alpha_value)\n        self.update_alpha()\n\n    def _on_int_change(self):\n        if self._block_changes:\n            return\n\n        self.alpha_value = self.int_input.value()\n        self.alpha_changed.emit(self.alpha_value)\n        self.update_alpha()\n\n    def update_alpha(self):\n        self._block_changes = True\n        if self.int_input.value() != self.alpha_value:\n            self.int_input.setValue(self.alpha_value)\n\n        percent = round(100 * self.alpha_value / 255, 2)\n        if self.percent_input.value() != percent:\n            self.percent_input.setValue(percent)\n\n        self._block_changes = False\n\n\nclass RGBInputs(QtWidgets.QWidget):\n    value_changed = QtCore.Signal()\n\n    def __init__(self, color, parent=None):\n        super(RGBInputs, self).__init__(parent)\n\n        self._block_changes = False\n\n        self.color = color\n\n        input_red = QtWidgets.QSpinBox(self)\n        input_green = QtWidgets.QSpinBox(self)\n        input_blue = QtWidgets.QSpinBox(self)\n\n        input_red.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n        input_green.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n        input_blue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n\n        input_red.setMinimum(0)\n        input_green.setMinimum(0)\n        input_blue.setMinimum(0)\n\n        input_red.setMaximum(255)\n        input_green.setMaximum(255)\n        input_blue.setMaximum(255)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(input_red, 1)\n        layout.addWidget(input_green, 1)\n        layout.addWidget(input_blue, 1)\n\n        input_red.valueChanged.connect(self._on_red_change)\n        input_green.valueChanged.connect(self._on_green_change)\n        input_blue.valueChanged.connect(self._on_blue_change)\n\n        self.input_red = input_red\n        self.input_green = input_green\n        self.input_blue = input_blue\n\n    def _on_red_change(self, value):\n        if self._block_changes:\n            return\n        self.color.setRed(value)\n        self._on_change()\n\n    def _on_green_change(self, value):\n        if self._block_changes:\n            return\n        self.color.setGreen(value)\n        self._on_change()\n\n    def _on_blue_change(self, value):\n        if self._block_changes:\n            return\n        self.color.setBlue(value)\n        self._on_change()\n\n    def _on_change(self):\n        self.value_changed.emit()\n\n    def color_changed(self):\n        if (\n            self.input_red.value() == self.color.red()\n            and self.input_green.value() == self.color.green()\n            and self.input_blue.value() == self.color.blue()\n        ):\n            return\n\n        self._block_changes = True\n\n        self.input_red.setValue(self.color.red())\n        self.input_green.setValue(self.color.green())\n        self.input_blue.setValue(self.color.blue())\n\n        self._block_changes = False\n\n\nclass CMYKInputs(QtWidgets.QWidget):\n    value_changed = QtCore.Signal()\n\n    def __init__(self, color, parent=None):\n        super(CMYKInputs, self).__init__(parent)\n\n        self.color = color\n\n        self._block_changes = False\n\n        input_cyan = QtWidgets.QSpinBox(self)\n        input_magenta = QtWidgets.QSpinBox(self)\n        input_yellow = QtWidgets.QSpinBox(self)\n        input_black = QtWidgets.QSpinBox(self)\n\n        input_cyan.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n        input_magenta.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n        input_yellow.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n        input_black.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n\n        input_cyan.setMinimum(0)\n        input_magenta.setMinimum(0)\n        input_yellow.setMinimum(0)\n        input_black.setMinimum(0)\n\n        input_cyan.setMaximum(255)\n        input_magenta.setMaximum(255)\n        input_yellow.setMaximum(255)\n        input_black.setMaximum(255)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(input_cyan, 1)\n        layout.addWidget(input_magenta, 1)\n        layout.addWidget(input_yellow, 1)\n        layout.addWidget(input_black, 1)\n\n        input_cyan.valueChanged.connect(self._on_change)\n        input_magenta.valueChanged.connect(self._on_change)\n        input_yellow.valueChanged.connect(self._on_change)\n        input_black.valueChanged.connect(self._on_change)\n\n        self.input_cyan = input_cyan\n        self.input_magenta = input_magenta\n        self.input_yellow = input_yellow\n        self.input_black = input_black\n\n    def _on_change(self):\n        if self._block_changes:\n            return\n        self.color.setCmyk(\n            self.input_cyan.value(),\n            self.input_magenta.value(),\n            self.input_yellow.value(),\n            self.input_black.value()\n        )\n        self.value_changed.emit()\n\n    def color_changed(self):\n        if self._block_changes:\n            return\n        _cur_color = QtGui.QColor()\n        _cur_color.setCmyk(\n            self.input_cyan.value(),\n            self.input_magenta.value(),\n            self.input_yellow.value(),\n            self.input_black.value()\n        )\n        if (\n            _cur_color.red() == self.color.red()\n            and _cur_color.green() == self.color.green()\n            and _cur_color.blue() == self.color.blue()\n        ):\n            return\n\n        c, m, y, k, _ = self.color.getCmyk()\n        self._block_changes = True\n\n        self.input_cyan.setValue(c)\n        self.input_magenta.setValue(m)\n        self.input_yellow.setValue(y)\n        self.input_black.setValue(k)\n\n        self._block_changes = False\n\n\nclass HEXInputs(QtWidgets.QWidget):\n    hex_regex = re.compile(\"^#(([0-9a-fA-F]{2}){3}|([0-9a-fA-F]){3})$\")\n    value_changed = QtCore.Signal()\n\n    def __init__(self, color, parent=None):\n        super(HEXInputs, self).__init__(parent)\n        self.color = color\n\n        input_field = QtWidgets.QLineEdit(self)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(input_field, 1)\n\n        input_field.textChanged.connect(self._on_change)\n\n        self.input_field = input_field\n\n    def _on_change(self):\n        if self._block_changes:\n            return\n        input_value = self.input_field.text()\n        # TODO what if does not match?\n        if self.hex_regex.match(input_value):\n            self.color.setNamedColor(input_value)\n            self.value_changed.emit()\n\n    def color_changed(self):\n        input_value = self.input_field.text()\n        if self.hex_regex.match(input_value):\n            _cur_color = QtGui.QColor()\n            _cur_color.setNamedColor(input_value)\n            if (\n                _cur_color.red() == self.color.red()\n                and _cur_color.green() == self.color.green()\n                and _cur_color.blue() == self.color.blue()\n            ):\n                return\n        self._block_changes = True\n\n        self.input_field.setText(self.color.name())\n\n        self._block_changes = False\n\n\nclass HSVInputs(QtWidgets.QWidget):\n    value_changed = QtCore.Signal()\n\n    def __init__(self, color, parent=None):\n        super(HSVInputs, self).__init__(parent)\n\n        self._block_changes = False\n\n        self.color = color\n\n        input_hue = QtWidgets.QSpinBox(self)\n        input_sat = QtWidgets.QSpinBox(self)\n        input_val = QtWidgets.QSpinBox(self)\n\n        input_hue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n        input_sat.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n        input_val.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n\n        input_hue.setMinimum(0)\n        input_sat.setMinimum(0)\n        input_val.setMinimum(0)\n\n        input_hue.setMaximum(359)\n        input_sat.setMaximum(255)\n        input_val.setMaximum(255)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(input_hue, 1)\n        layout.addWidget(input_sat, 1)\n        layout.addWidget(input_val, 1)\n\n        input_hue.valueChanged.connect(self._on_change)\n        input_sat.valueChanged.connect(self._on_change)\n        input_val.valueChanged.connect(self._on_change)\n\n        self.input_hue = input_hue\n        self.input_sat = input_sat\n        self.input_val = input_val\n\n    def _on_change(self):\n        if self._block_changes:\n            return\n        self.color.setHsv(\n            self.input_hue.value(),\n            self.input_sat.value(),\n            self.input_val.value()\n        )\n        self.value_changed.emit()\n\n    def color_changed(self):\n        _cur_color = QtGui.QColor()\n        _cur_color.setHsv(\n            self.input_hue.value(),\n            self.input_sat.value(),\n            self.input_val.value()\n        )\n        if (\n            _cur_color.red() == self.color.red()\n            and _cur_color.green() == self.color.green()\n            and _cur_color.blue() == self.color.blue()\n        ):\n            return\n\n        self._block_changes = True\n        h, s, v, _ = self.color.getHsv()\n\n        self.input_hue.setValue(h)\n        self.input_sat.setValue(s)\n        self.input_val.setValue(v)\n\n        self._block_changes = False\n\n\nclass HSLInputs(QtWidgets.QWidget):\n    value_changed = QtCore.Signal()\n\n    def __init__(self, color, parent=None):\n        super(HSLInputs, self).__init__(parent)\n\n        self._block_changes = False\n\n        self.color = color\n\n        input_hue = QtWidgets.QSpinBox(self)\n        input_sat = QtWidgets.QSpinBox(self)\n        input_light = QtWidgets.QSpinBox(self)\n\n        input_hue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n        input_sat.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n        input_light.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)\n\n        input_hue.setMinimum(0)\n        input_sat.setMinimum(0)\n        input_light.setMinimum(0)\n\n        input_hue.setMaximum(359)\n        input_sat.setMaximum(255)\n        input_light.setMaximum(255)\n\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(input_hue, 1)\n        layout.addWidget(input_sat, 1)\n        layout.addWidget(input_light, 1)\n\n        input_hue.valueChanged.connect(self._on_change)\n        input_sat.valueChanged.connect(self._on_change)\n        input_light.valueChanged.connect(self._on_change)\n\n        self.input_hue = input_hue\n        self.input_sat = input_sat\n        self.input_light = input_light\n\n    def _on_change(self):\n        if self._block_changes:\n            return\n        self.color.setHsl(\n            self.input_hue.value(),\n            self.input_sat.value(),\n            self.input_light.value()\n        )\n        self.value_changed.emit()\n\n    def color_changed(self):\n        _cur_color = QtGui.QColor()\n        _cur_color.setHsl(\n            self.input_hue.value(),\n            self.input_sat.value(),\n            self.input_light.value()\n        )\n        if (\n            _cur_color.red() == self.color.red()\n            and _cur_color.green() == self.color.green()\n            and _cur_color.blue() == self.color.blue()\n        ):\n            return\n\n        self._block_changes = True\n        h, s, l, _ = self.color.getHsl()\n\n        self.input_hue.setValue(h)\n        self.input_sat.setValue(s)\n        self.input_light.setValue(l)\n\n        self._block_changes = False\n\n\nclass ColorInputsWidget(QtWidgets.QWidget):\n    color_changed = QtCore.Signal(QtGui.QColor)\n\n    def __init__(self, parent=None, **kwargs):\n        super(ColorInputsWidget, self).__init__(parent)\n\n        color = QtGui.QColor()\n\n        input_fields = []\n\n        if kwargs.get(\"use_hex\", True):\n            input_fields.append(HEXInputs(color, self))\n\n        if kwargs.get(\"use_rgb\", True):\n            input_fields.append(RGBInputs(color, self))\n\n        if kwargs.get(\"use_hsl\", True):\n            input_fields.append(HSLInputs(color, self))\n\n        if kwargs.get(\"use_hsv\", True):\n            input_fields.append(HSVInputs(color, self))\n\n        if kwargs.get(\"use_cmyk\", True):\n            input_fields.append(CMYKInputs(color, self))\n\n        inputs_widget = QtWidgets.QWidget(self)\n        inputs_layout = QtWidgets.QVBoxLayout(inputs_widget)\n\n        for input_field in input_fields:\n            inputs_layout.addWidget(input_field)\n            input_field.value_changed.connect(self._on_value_change)\n\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(inputs_widget, 0)\n        spacer = QtWidgets.QWidget(self)\n        layout.addWidget(spacer, 1)\n\n        self.input_fields = input_fields\n\n        self.color = color\n\n    def set_color(self, color):\n        if (\n            color.red() == self.color.red()\n            and color.green() == self.color.green()\n            and color.blue() == self.color.blue()\n        ):\n            return\n        self.color.setRed(color.red())\n        self.color.setGreen(color.green())\n        self.color.setBlue(color.blue())\n        self._on_value_change()\n\n    def _on_value_change(self):\n        for input_field in self.input_fields:\n            input_field.color_changed()\n        self.color_changed.emit(self.color)\n"
  },
  {
    "path": "openpype/widgets/color_widgets/color_picker_widget.py",
    "content": "import os\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom .color_triangle import QtColorTriangle\nfrom .color_view import ColorViewer\nfrom .color_screen_pick import PickScreenColorWidget\nfrom .color_inputs import (\n    AlphaSlider,\n    AlphaInputs,\n    HEXInputs,\n    RGBInputs,\n    HSLInputs,\n    HSVInputs\n)\n\n\nclass ColorPickerWidget(QtWidgets.QWidget):\n    color_changed = QtCore.Signal(QtGui.QColor)\n\n    def __init__(self, color=None, use_alpha=True, parent=None):\n        super(ColorPickerWidget, self).__init__(parent)\n\n        # Color triangle\n        color_triangle = QtColorTriangle(self)\n\n        # Eye picked widget\n        pick_widget = PickScreenColorWidget()\n        pick_widget.setMaximumHeight(50)\n\n        # Color pick button\n        btn_pick_color = QtWidgets.QPushButton(self)\n        icon_path = os.path.join(\n            os.path.dirname(os.path.abspath(__file__)),\n            \"eyedropper.png\"\n        )\n        btn_pick_color.setIcon(QtGui.QIcon(icon_path))\n        btn_pick_color.setToolTip(\"Pick a color\")\n\n        # Color preview\n        color_view = ColorViewer(self)\n        color_view.setMaximumHeight(50)\n\n        color_inputs_color = QtGui.QColor()\n        col_inputs_by_label = [\n            (\"HEX\", HEXInputs(color_inputs_color, self)),\n            (\"RGB\", RGBInputs(color_inputs_color, self)),\n            (\"HSL\", HSLInputs(color_inputs_color, self)),\n            (\"HSV\", HSVInputs(color_inputs_color, self))\n        ]\n\n        layout = QtWidgets.QGridLayout(self)\n\n        empty_col = 1\n        label_col = empty_col + 1\n        input_col = label_col + 1\n        empty_widget = QtWidgets.QWidget(self)\n        empty_widget.setFixedWidth(10)\n        layout.addWidget(empty_widget, 0, empty_col)\n\n        layout.setColumnStretch(0, 1)\n        layout.setColumnStretch(input_col, 1)\n\n        row = 0\n        layout.addWidget(btn_pick_color, row, label_col)\n        layout.addWidget(color_view, row, input_col)\n        row += 1\n\n        color_input_fields = []\n        for label, input_field in col_inputs_by_label:\n            layout.addWidget(QtWidgets.QLabel(label, self), row, label_col)\n            layout.addWidget(input_field, row, input_col)\n            input_field.value_changed.connect(\n                self._on_color_input_value_change\n            )\n            color_input_fields.append(input_field)\n            row += 1\n\n        layout.addWidget(color_triangle, 0, 0, row + 1, 1)\n        layout.setRowStretch(row, 1)\n        row += 1\n\n        alpha_label = None\n        alpha_slider_proxy = None\n        alpha_slider = None\n        alpha_inputs = None\n        if not use_alpha:\n            color.setAlpha(255)\n        else:\n            alpha_inputs = AlphaInputs(self)\n            alpha_label = QtWidgets.QLabel(\"Alpha\", self)\n            alpha_slider_proxy = QtWidgets.QWidget(self)\n            alpha_slider = AlphaSlider(\n                QtCore.Qt.Horizontal, alpha_slider_proxy\n            )\n\n            alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy)\n            alpha_slider_layout.setContentsMargins(5, 5, 5, 5)\n            alpha_slider_layout.addWidget(alpha_slider, 1)\n\n            layout.addWidget(alpha_slider_proxy, row, 0)\n\n            layout.addWidget(alpha_label, row, label_col)\n            layout.addWidget(alpha_inputs, row, input_col)\n\n            row += 1\n\n        layout.setRowStretch(row, 1)\n\n        color_view.set_color(color_triangle.cur_color)\n\n        color_triangle.color_changed.connect(self.triangle_color_changed)\n        pick_widget.color_selected.connect(self.on_color_change)\n        btn_pick_color.released.connect(self.pick_color)\n        if alpha_slider:\n            alpha_slider.valueChanged.connect(self._on_alpha_slider_change)\n            alpha_inputs.alpha_changed.connect(self._on_alpha_inputs_changed)\n\n        self.color_input_fields = color_input_fields\n        self.color_inputs_color = color_inputs_color\n\n        self.pick_widget = pick_widget\n\n        self.color_triangle = color_triangle\n        self.alpha_slider = alpha_slider\n\n        self.color_view = color_view\n        self.alpha_inputs = alpha_inputs\n        self.btn_pick_color = btn_pick_color\n\n        self._minimum_size_set = False\n\n        if color:\n            self.set_color(color)\n            self.alpha_changed(color.alpha())\n\n    def showEvent(self, event):\n        super(ColorPickerWidget, self).showEvent(event)\n        if self._minimum_size_set:\n            return\n\n        triangle_size = max(int(self.width() / 5 * 3), 180)\n        self.color_triangle.setMinimumWidth(triangle_size)\n        self.color_triangle.setMinimumHeight(triangle_size)\n        self._minimum_size_set = True\n\n    def color(self):\n        return self.color_view.color()\n\n    def set_color(self, color):\n        if self.alpha_inputs:\n            self.alpha_inputs.set_alpha(color.alpha())\n        self.on_color_change(color)\n\n    def pick_color(self):\n        self.pick_widget.pick_color()\n\n    def triangle_color_changed(self, color):\n        self.color_view.set_color(color)\n        if self.color_inputs_color != color:\n            self.color_inputs_color.setRgb(\n                color.red(), color.green(), color.blue()\n            )\n            for color_input in self.color_input_fields:\n                color_input.color_changed()\n\n    def on_color_change(self, color):\n        self.color_view.set_color(color)\n        self.color_triangle.set_color(color)\n        if self.color_inputs_color != color:\n            self.color_inputs_color.setRgb(\n                color.red(), color.green(), color.blue()\n            )\n            for color_input in self.color_input_fields:\n                color_input.color_changed()\n\n    def _on_color_input_value_change(self):\n        for input_field in self.color_input_fields:\n            input_field.color_changed()\n        self.on_color_change(QtGui.QColor(self.color_inputs_color))\n\n    def alpha_changed(self, value):\n        self.color_view.set_alpha(value)\n        if self.alpha_slider and self.alpha_slider.value() != value:\n            self.alpha_slider.setValue(value)\n\n        if self.alpha_inputs and self.alpha_inputs.alpha_value != value:\n            self.alpha_inputs.set_alpha(value)\n\n    def _on_alpha_inputs_changed(self, value):\n        self.alpha_changed(value)\n\n    def _on_alpha_slider_change(self, value):\n        self.alpha_changed(value)\n"
  },
  {
    "path": "openpype/widgets/color_widgets/color_screen_pick.py",
    "content": "import qtpy\nfrom qtpy import QtWidgets, QtCore, QtGui\n\n\nclass PickScreenColorWidget(QtWidgets.QWidget):\n    color_selected = QtCore.Signal(QtGui.QColor)\n\n    def __init__(self, parent=None):\n        super(PickScreenColorWidget, self).__init__(parent)\n        self.labels = []\n        self.magnification = 2\n\n        self._min_magnification = 1\n        self._max_magnification = 10\n\n    def add_magnification_delta(self, delta):\n        _delta = abs(delta / 1000)\n        if delta > 0:\n            self.magnification += _delta\n        else:\n            self.magnification -= _delta\n\n        if self.magnification > self._max_magnification:\n            self.magnification = self._max_magnification\n        elif self.magnification < self._min_magnification:\n            self.magnification = self._min_magnification\n\n    def pick_color(self):\n        if self.labels:\n            if self.labels[0].isVisible():\n                return\n            self.labels = []\n\n        for screen in QtWidgets.QApplication.screens():\n            label = PickLabel(self)\n            label.pick_color(screen)\n            label.color_selected.connect(self.on_color_select)\n            label.close_session.connect(self.end_pick_session)\n            self.labels.append(label)\n\n    def end_pick_session(self):\n        for label in self.labels:\n            label.close()\n        self.labels = []\n\n    def on_color_select(self, color):\n        self.color_selected.emit(color)\n        self.end_pick_session()\n\n\nclass PickLabel(QtWidgets.QLabel):\n    color_selected = QtCore.Signal(QtGui.QColor)\n    close_session = QtCore.Signal()\n\n    def __init__(self, pick_widget):\n        super(PickLabel, self).__init__()\n        self.setMouseTracking(True)\n\n        self.pick_widget = pick_widget\n\n        self.radius_pen = QtGui.QPen(QtGui.QColor(27, 27, 27), 2)\n        self.text_pen = QtGui.QPen(QtGui.QColor(127, 127, 127), 4)\n        self.text_bg = QtGui.QBrush(QtGui.QColor(27, 27, 27))\n        self._mouse_over = False\n\n        self.radius = 100\n        self.radius_ratio = 11\n\n    @property\n    def magnification(self):\n        return self.pick_widget.magnification\n\n    def pick_color(self, screen_obj):\n        self.show()\n        self.windowHandle().setScreen(screen_obj)\n        geo = screen_obj.geometry()\n        args = (\n            QtWidgets.QApplication.desktop().winId(),\n            geo.x(), geo.y(), geo.width(), geo.height()\n        )\n        if qtpy.API in (\"pyqt4\", \"pyside\"):\n            pix = QtGui.QPixmap.grabWindow(*args)\n        else:\n            pix = screen_obj.grabWindow(*args)\n\n        if pix.width() > pix.height():\n            size = pix.height()\n        else:\n            size = pix.width()\n\n        self.radius = int(size / self.radius_ratio)\n\n        self.setPixmap(pix)\n        self.showFullScreen()\n\n    def wheelEvent(self, event):\n        y_delta = event.angleDelta().y()\n        self.pick_widget.add_magnification_delta(y_delta)\n        self.update()\n\n    def enterEvent(self, event):\n        self._mouse_over = True\n        super().enterEvent(event)\n\n    def leaveEvent(self, event):\n        self._mouse_over = False\n        super().leaveEvent(event)\n        self.update()\n\n    def mouseMoveEvent(self, event):\n        self.update()\n\n    def paintEvent(self, event):\n        super().paintEvent(event)\n        if not self._mouse_over:\n            return\n\n        mouse_pos_to_widet = self.mapFromGlobal(QtGui.QCursor.pos())\n\n        magnified_half_size = self.radius / self.magnification\n        magnified_size = magnified_half_size * 2\n\n        zoom_x_1 = mouse_pos_to_widet.x() - magnified_half_size\n        zoom_x_2 = mouse_pos_to_widet.x() + magnified_half_size\n        zoom_y_1 = mouse_pos_to_widet.y() - magnified_half_size\n        zoom_y_2 = mouse_pos_to_widet.y() + magnified_half_size\n        pix_width = magnified_size\n        pix_height = magnified_size\n        draw_pos_x = 0\n        draw_pos_y = 0\n        if zoom_x_1 < 0:\n            draw_pos_x = abs(zoom_x_1)\n            pix_width -= draw_pos_x\n            zoom_x_1 = 1\n        elif zoom_x_2 > self.pixmap().width():\n            pix_width -= zoom_x_2 - self.pixmap().width()\n\n        if zoom_y_1 < 0:\n            draw_pos_y = abs(zoom_y_1)\n            pix_height -= draw_pos_y\n            zoom_y_1 = 1\n        elif zoom_y_2 > self.pixmap().height():\n            pix_height -= zoom_y_2 - self.pixmap().height()\n\n        new_pix = QtGui.QPixmap(magnified_size, magnified_size)\n        new_pix.fill(QtCore.Qt.transparent)\n        new_pix_painter = QtGui.QPainter(new_pix)\n        new_pix_painter.drawPixmap(\n            QtCore.QRect(draw_pos_x, draw_pos_y, pix_width, pix_height),\n            self.pixmap().copy(zoom_x_1, zoom_y_1, pix_width, pix_height)\n        )\n        new_pix_painter.end()\n\n        painter = QtGui.QPainter(self)\n\n        ellipse_rect = QtCore.QRect(\n            mouse_pos_to_widet.x() - self.radius,\n            mouse_pos_to_widet.y() - self.radius,\n            self.radius * 2,\n            self.radius * 2\n        )\n        ellipse_rect_f = QtCore.QRectF(ellipse_rect)\n        path = QtGui.QPainterPath()\n        path.addEllipse(ellipse_rect_f)\n        painter.setClipPath(path)\n\n        new_pix_rect = QtCore.QRect(\n            mouse_pos_to_widet.x() - self.radius + 1,\n            mouse_pos_to_widet.y() - self.radius + 1,\n            new_pix.width() * self.magnification,\n            new_pix.height() * self.magnification\n        )\n\n        painter.drawPixmap(new_pix_rect, new_pix)\n\n        painter.setClipping(False)\n\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n\n        painter.setPen(self.radius_pen)\n        painter.drawEllipse(ellipse_rect_f)\n\n        image = self.pixmap().toImage()\n        if image.valid(mouse_pos_to_widet):\n            color = QtGui.QColor(image.pixel(mouse_pos_to_widet))\n        else:\n            color = QtGui.QColor()\n\n        color_text = \"Red: {} - Green: {} - Blue: {}\".format(\n            color.red(), color.green(), color.blue()\n        )\n        font = painter.font()\n        font.setPointSize(self.radius / 10)\n        painter.setFont(font)\n\n        text_rect_height = int(painter.fontMetrics().height() + 10)\n        text_rect = QtCore.QRect(\n            ellipse_rect.x(),\n            ellipse_rect.bottom(),\n            ellipse_rect.width(),\n            text_rect_height\n        )\n        if text_rect.bottom() > self.pixmap().height():\n            text_rect.moveBottomLeft(ellipse_rect.topLeft())\n\n        rect_radius = text_rect_height / 2\n        path = QtGui.QPainterPath()\n        path.addRoundedRect(\n            QtCore.QRectF(text_rect),\n            rect_radius,\n            rect_radius\n        )\n        painter.fillPath(path, self.text_bg)\n\n        painter.setPen(self.text_pen)\n        painter.drawText(\n            text_rect,\n            QtCore.Qt.AlignLeft | QtCore.Qt.AlignCenter,\n            color_text\n        )\n\n        color_rect_x = ellipse_rect.x() - text_rect_height\n        if color_rect_x < 0:\n            color_rect_x += (text_rect_height + ellipse_rect.width())\n\n        color_rect = QtCore.QRect(\n            color_rect_x,\n            ellipse_rect.y(),\n            text_rect_height,\n            ellipse_rect.height()\n        )\n        path = QtGui.QPainterPath()\n        path.addRoundedRect(\n            QtCore.QRectF(color_rect),\n            rect_radius,\n            rect_radius\n        )\n        painter.fillPath(path, color)\n        painter.drawRoundedRect(color_rect, rect_radius, rect_radius)\n        painter.end()\n\n    def mouseReleaseEvent(self, event):\n        color = QtGui.QColor(self.pixmap().toImage().pixel(event.pos()))\n        self.color_selected.emit(color)\n\n    def keyPressEvent(self, event):\n        if event.key() == QtCore.Qt.Key_Escape:\n            self.close_session.emit()\n"
  },
  {
    "path": "openpype/widgets/color_widgets/color_triangle.py",
    "content": "from enum import Enum\nfrom math import floor, ceil, sqrt, sin, cos, acos, pi as PI\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nTWOPI = PI * 2\n\n\nclass TriangleState(Enum):\n    IdleState = object()\n    SelectingHueState = object()\n    SelectingSatValueState = object()\n\n\nclass DoubleColor:\n    def __init__(self, r, g=None, b=None):\n        if g is None:\n            g = r.g\n            b = r.b\n            r = r.r\n        self.r = float(r)\n        self.g = float(g)\n        self.b = float(b)\n\n\nclass Vertex:\n    def __init__(self, color, point):\n        # Convert GlobalColor to QColor as globals don't have red, green, blue\n        if isinstance(color, QtCore.Qt.GlobalColor):\n            color = QtGui.QColor(color)\n\n        # Convert QColor to DoubleColor\n        if isinstance(color, QtGui.QColor):\n            color = DoubleColor(color.red(), color.green(), color.blue())\n\n        self.color = color\n        self.point = point\n\n\nclass QtColorTriangle(QtWidgets.QWidget):\n    \"\"\"The QtColorTriangle class provides a triangular color selection widget.\n\n    This widget uses the HSV color model, and is therefore useful for\n    selecting colors by eye.\n\n    The triangle in the center of the widget is used for selecting\n    saturation and value, and the surrounding circle is used for\n    selecting hue.\n\n    Use set_color() and color() to set and get the current color.\n    \"\"\"\n    color_changed = QtCore.Signal(QtGui.QColor)\n\n    # Thick of color wheel ratio where 1 is fully filled circle\n    inner_radius_ratio = 5.0\n    # Ratio where hue selector on wheel is relative to `inner_radius_ratio`\n    # - middle of the wheel is twice `inner_radius_ratio`\n    selector_radius_ratio = inner_radius_ratio * 2\n    # Size ratio of selectors on wheel and in triangle\n    ellipse_size_ratio = 10.0\n    # Ration of selectors thickness\n    ellipse_thick_ratio = 50.0\n    # Hue offset on color wheel (0 - 359)\n    # - red on top if set to \"0\"\n    hue_offset = 90\n\n    def __init__(self, parent=None):\n        super(QtColorTriangle, self).__init__(parent)\n        self.setSizePolicy(\n            QtWidgets.QSizePolicy.Minimum,\n            QtWidgets.QSizePolicy.Minimum\n        )\n        self.setFocusPolicy(QtCore.Qt.StrongFocus)\n\n        self.angle_a = float()\n        self.angle_b = float()\n        self.angle_c = float()\n\n        self.bg_image = QtGui.QImage(\n            self.sizeHint(), QtGui.QImage.Format_RGB32\n        )\n        self.cur_color = QtGui.QColor()\n        self.point_a = QtCore.QPointF()\n        self.point_b = QtCore.QPointF()\n        self.point_c = QtCore.QPointF()\n        self.point_d = QtCore.QPointF()\n\n        self.cur_hue = int()\n\n        self.pen_width = int()\n        self.ellipse_size = int()\n        self.outer_radius = int()\n        self.selector_pos = QtCore.QPointF()\n\n        self.sel_mode = TriangleState.IdleState\n\n        self._triangle_outline_pen = QtGui.QPen(\n            QtGui.QColor(40, 40, 40, 128),\n            2\n        )\n        # Prepare hue numbers for color circle\n        _hue_circle_range = []\n        for idx in range(11):\n            # Some Qt versions may require:\n            # hue = int(idx * 360.0)\n            percent_idx = idx * 0.1\n            hue = int(360.0 - (percent_idx * 360.0))\n            _hue_circle_range.append((percent_idx, hue))\n        self._hue_circle_range = tuple(_hue_circle_range)\n\n        color = QtGui.QColor()\n        color.setHsv(0, 255, 255)\n        self.set_color(color)\n\n    def set_color(self, col):\n        if (\n            col.red() == self.cur_color.red()\n            and col.green() == self.cur_color.green()\n            and col.blue() == self.cur_color.blue()\n        ):\n            return\n\n        self.cur_color = col\n\n        hue, *_ = self.cur_color.getHsv()\n\n        # Never use an invalid hue to display colors\n        if hue != -1:\n            self.cur_hue = hue\n\n        angle_with_offset = (360 - self.cur_hue - self.hue_offset) % 360\n        self.angle_a = (angle_with_offset * TWOPI) / 360.0\n        self.angle_a += PI / 2.0\n        if self.angle_a > TWOPI:\n            self.angle_a -= TWOPI\n\n        self.angle_b = self.angle_a + TWOPI / 3\n        self.angle_c = self.angle_b + TWOPI / 3\n\n        if self.angle_b > TWOPI:\n            self.angle_b -= TWOPI\n        if self.angle_c > TWOPI:\n            self.angle_c -= TWOPI\n\n        cx = float(self.contentsRect().center().x())\n        cy = float(self.contentsRect().center().y())\n        inner_radius = (\n            self.outer_radius\n            - (self.outer_radius / self.inner_radius_ratio)\n        )\n        selector_radius = (\n            self.outer_radius\n            - (self.outer_radius / self.selector_radius_ratio)\n        )\n        self.point_a = QtCore.QPointF(\n            cx + (cos(self.angle_a) * inner_radius),\n            cy - (sin(self.angle_a) * inner_radius)\n        )\n        self.point_b = QtCore.QPointF(\n            cx + (cos(self.angle_b) * inner_radius),\n            cy - (sin(self.angle_b) * inner_radius)\n        )\n        self.point_c = QtCore.QPointF(\n            cx + (cos(self.angle_c) * inner_radius),\n            cy - (sin(self.angle_c) * inner_radius)\n        )\n        self.point_d = QtCore.QPointF(\n            cx + (cos(self.angle_a) * selector_radius),\n            cy - (sin(self.angle_a) * selector_radius)\n        )\n\n        self.selector_pos = self._point_from_color(self.cur_color)\n        self.update()\n\n        self.color_changed.emit(self.cur_color)\n\n    def heightForWidth(self, width):\n        return width\n\n    def polish(self):\n        size_w = self.contentsRect().width()\n        size_h = self.contentsRect().height()\n        if size_w < size_h:\n            size = size_w\n        else:\n            size = size_h\n\n        self.outer_radius = (size - 1) / 2\n\n        self.pen_width = int(\n            ceil(self.outer_radius / self.ellipse_thick_ratio)\n        )\n        self.ellipse_size = int(\n            ceil(self.outer_radius / self.ellipse_size_ratio)\n        )\n\n        cx = float(self.contentsRect().center().x())\n        cy = float(self.contentsRect().center().y())\n\n        inner_radius = (\n            self.outer_radius\n            - (self.outer_radius / self.inner_radius_ratio)\n        )\n        selector_radius = (\n            self.outer_radius\n            - (self.outer_radius / self.selector_radius_ratio)\n        )\n        self.point_a = QtCore.QPointF(\n            cx + (cos(self.angle_a) * inner_radius),\n            cy - (sin(self.angle_a) * inner_radius)\n        )\n        self.point_b = QtCore.QPointF(\n            cx + (cos(self.angle_b) * inner_radius),\n            cy - (sin(self.angle_b) * inner_radius)\n        )\n        self.point_c = QtCore.QPointF(\n            cx + (cos(self.angle_c) * inner_radius),\n            cy - (sin(self.angle_c) * inner_radius)\n        )\n        self.point_d = QtCore.QPointF(\n            cx + (cos(self.angle_a) * selector_radius),\n            cy - (sin(self.angle_a) * selector_radius)\n        )\n\n        self.selector_pos = self._point_from_color(self.cur_color)\n\n        self.update()\n\n    def paintEvent(self, event):\n        painter = QtGui.QPainter(self)\n        if event.rect().intersects(self.contentsRect()):\n            event_region = event.region()\n            if hasattr(event_region, \"intersect\"):\n                clip_region = event_region.intersect(self.contentsRect())\n            else:\n                clip_region = event_region.intersected(\n                    self.contentsRect()\n                )\n            painter.setClipRegion(clip_region)\n\n        self.paint_bg()\n\n        # Blit the static generated background with the hue gradient onto\n        # the double buffer.\n        buf = QtGui.QImage(\n            self.bg_image.width(),\n            self.bg_image.height(),\n            QtGui.QImage.Format_RGB32\n        )\n\n        # Draw the trigon\n        # Find the color with only the hue, and max value and saturation\n        hue_color = QtGui.QColor()\n        hue_color.setHsv(self.cur_hue, 255, 255)\n\n        # Draw the triangle\n        self.drawTrigon(\n            buf, self.point_a, self.point_b, self.point_c, hue_color\n        )\n\n        # Slow step: convert the image to a pixmap\n        pix = self.bg_image.copy()\n        pix_painter = QtGui.QPainter(pix)\n\n        pix_painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)\n\n        trigon_path = QtGui.QPainterPath()\n        trigon_path.moveTo(self.point_a)\n        trigon_path.lineTo(self.point_b)\n        trigon_path.lineTo(self.point_c)\n        trigon_path.closeSubpath()\n        pix_painter.setClipPath(trigon_path)\n\n        pix_painter.drawImage(0, 0, buf)\n\n        pix_painter.setClipping(False)\n\n        # Draw an outline of the triangle\n        pix_painter.setPen(self._triangle_outline_pen)\n        pix_painter.drawLine(self.point_a, self.point_b)\n        pix_painter.drawLine(self.point_b, self.point_c)\n        pix_painter.drawLine(self.point_c, self.point_a)\n\n        # Draw the color wheel selector\n        pix_painter.setPen(QtGui.QPen(QtCore.Qt.white, self.pen_width))\n        pix_painter.drawEllipse(\n            int(self.point_d.x() - self.ellipse_size / 2.0),\n            int(self.point_d.y() - self.ellipse_size / 2.0),\n            self.ellipse_size, self.ellipse_size\n        )\n\n        # Draw the triangle selector\n        pix_painter.setBrush(self.cur_color)\n        pix_painter.drawEllipse(\n            QtCore.QRectF(\n                self.selector_pos.x() - self.ellipse_size / 2.0,\n                self.selector_pos.y() - self.ellipse_size / 2.0,\n                self.ellipse_size + 0.5,\n                self.ellipse_size + 0.5\n            )\n        )\n\n        pix_painter.end()\n        # Blit\n        painter.drawPixmap(self.contentsRect().topLeft(), pix)\n        painter.end()\n\n    def mouseMoveEvent(self, event):\n        if (event.buttons() & QtCore.Qt.LeftButton) == 0:\n            return\n\n        depos = QtCore.QPointF(\n            event.pos().x(),\n            event.pos().y()\n        )\n        new_color = False\n\n        if self.sel_mode is TriangleState.SelectingHueState:\n            self.angle_a = self._angle_at(depos, self.contentsRect())\n            self.angle_b = self.angle_a + (TWOPI / 3.0)\n            self.angle_c = self.angle_b + (TWOPI / 3.0)\n            if self.angle_b > TWOPI:\n                self.angle_b -= TWOPI\n            if self.angle_c > TWOPI:\n                self.angle_c -= TWOPI\n\n            am = self.angle_a - (PI / 2)\n            if am < 0:\n                am += TWOPI\n            self.cur_hue = (\n                360 - int((am * 360.0) / TWOPI) - self.hue_offset\n            ) % 360\n            hue, sat, val, _ = self.cur_color.getHsv()\n\n            if self.cur_hue != hue:\n                new_color = True\n                self.cur_color.setHsv(self.cur_hue, sat, val)\n\n            cx = float(self.contentsRect().center().x())\n            cy = float(self.contentsRect().center().y())\n            inner_radius = (\n                self.outer_radius\n                - (self.outer_radius / self.inner_radius_ratio)\n            )\n            selector_radius = (\n                self.outer_radius\n                - (self.outer_radius / self.selector_radius_ratio)\n            )\n            self.point_a = QtCore.QPointF(\n                cx + (cos(self.angle_a) * inner_radius),\n                cy - (sin(self.angle_a) * inner_radius)\n            )\n            self.point_b = QtCore.QPointF(\n                cx + (cos(self.angle_b) * inner_radius),\n                cy - (sin(self.angle_b) * inner_radius)\n            )\n            self.point_c = QtCore.QPointF(\n                cx + (cos(self.angle_c) * inner_radius),\n                cy - (sin(self.angle_c) * inner_radius)\n            )\n            self.point_d = QtCore.QPointF(\n                cx + (cos(self.angle_a) * selector_radius),\n                cy - (sin(self.angle_a) * selector_radius)\n            )\n\n            self.selector_pos = self._point_from_color(self.cur_color)\n        else:\n            aa = Vertex(QtCore.Qt.transparent, self.point_a)\n            bb = Vertex(QtCore.Qt.transparent, self.point_b)\n            cc = Vertex(QtCore.Qt.transparent, self.point_c)\n\n            self.selector_pos = self._move_point_to_triangle(\n                depos.x(), depos.y(), aa, bb, cc\n            )\n            col = self._color_from_point(self.selector_pos)\n            if col != self.cur_color:\n                # Ensure that hue does not change when selecting\n                # saturation and value.\n                _, sat, val, _ = col.getHsv()\n                self.cur_color.setHsv(self.cur_hue, sat, val)\n                new_color = True\n\n        if new_color:\n            self.color_changed.emit(self.cur_color)\n\n        self.update()\n\n    def mousePressEvent(self, event):\n        # Only respond to the left mouse button.\n        if event.button() != QtCore.Qt.LeftButton:\n            return\n\n        depos = QtCore.QPointF(\n            event.pos().x(),\n            event.pos().y()\n        )\n        rad = self._radius_at(depos, self.contentsRect())\n        new_color = False\n\n        # As in mouseMoveEvent, either find the a, b, c angles or the\n        # radian position of the selector, then order an update.\n        inner_radius = (\n            self.outer_radius - (self.outer_radius / self.inner_radius_ratio)\n        )\n        if rad > inner_radius:\n            self.sel_mode = TriangleState.SelectingHueState\n\n            self.angle_a = self._angle_at(depos, self.contentsRect())\n            self.angle_b = self.angle_a + TWOPI / 3.0\n            self.angle_c = self.angle_b + TWOPI / 3.0\n            if self.angle_b > TWOPI:\n                self.angle_b -= TWOPI\n            if self.angle_c > TWOPI:\n                self.angle_c -= TWOPI\n\n            am = self.angle_a - PI / 2\n            if am < 0:\n                am += TWOPI\n\n            self.cur_hue = (\n                360 - int((am * 360.0) / TWOPI) - self.hue_offset\n            ) % 360\n            hue, sat, val, _ = self.cur_color.getHsv()\n\n            if hue != self.cur_hue:\n                new_color = True\n                self.cur_color.setHsv(self.cur_hue, sat, val)\n\n            cx = float(self.contentsRect().center().x())\n            cy = float(self.contentsRect().center().y())\n\n            self.point_a = QtCore.QPointF(\n                cx + (cos(self.angle_a) * inner_radius),\n                cy - (sin(self.angle_a) * inner_radius)\n            )\n            self.point_b = QtCore.QPointF(\n                cx + (cos(self.angle_b) * inner_radius),\n                cy - (sin(self.angle_b) * inner_radius)\n            )\n            self.point_c = QtCore.QPointF(\n                cx + (cos(self.angle_c) * inner_radius),\n                cy - (sin(self.angle_c) * inner_radius)\n            )\n\n            selector_radius = (\n                self.outer_radius\n                - (self.outer_radius / self.selector_radius_ratio)\n            )\n            self.point_d = QtCore.QPointF(\n                cx + (cos(self.angle_a) * selector_radius),\n                cy - (sin(self.angle_a) * selector_radius)\n            )\n\n            self.selector_pos = self._point_from_color(self.cur_color)\n            self.color_changed.emit(self.cur_color)\n        else:\n            self.sel_mode = TriangleState.SelectingSatValueState\n\n            aa = Vertex(QtCore.Qt.transparent, self.point_a)\n            bb = Vertex(QtCore.Qt.transparent, self.point_b)\n            cc = Vertex(QtCore.Qt.transparent, self.point_c)\n\n            self.selector_pos = self._move_point_to_triangle(\n                depos.x(), depos.y(), aa, bb, cc\n            )\n            col = self._color_from_point(self.selector_pos)\n            if col != self.cur_color:\n                self.cur_color = col\n                new_color = True\n\n        if new_color:\n            self.color_changed.emit(self.cur_color)\n\n        self.update()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() == QtCore.Qt.LeftButton:\n            self.sel_mode = TriangleState.IdleState\n\n    def keyPressEvent(self, event):\n        key = event.key()\n        if key == QtCore.Qt.Key_Left:\n            self.cur_hue -= 1\n            if self.cur_hue < 0:\n                self.cur_hue += 360\n            _, sat, val, _ = self.cur_color.getHsv()\n\n            tmp = QtGui.QColor()\n            tmp.setHsv(self.cur_hue, sat, val)\n            self.set_color(tmp)\n\n        elif key == QtCore.Qt.Key_Right:\n            self.cur_hue += 1\n            if (self.cur_hue > 359):\n                self.cur_hue -= 360\n            _, sat, val, _ = self.cur_color.getHsv()\n            tmp = QtGui.QColor()\n            tmp.setHsv(self.cur_hue, sat, val)\n            self.set_color(tmp)\n\n        elif key == QtCore.Qt.Key_Up:\n            _, sat, val, _ = self.cur_color.getHsv()\n            if event.modifiers() & QtCore.Qt.ShiftModifier:\n                if sat > 5:\n                    sat -= 5\n                else:\n                    sat = 0\n            else:\n                if val > 5:\n                    val -= 5\n                else:\n                    val = 0\n\n            tmp = QtGui.QColor()\n            tmp.setHsv(self.cur_hue, sat, val)\n            self.set_color(tmp)\n\n        elif key == QtCore.Qt.Key_Down:\n            _, sat, val, _ = self.cur_color.getHsv()\n            if event.modifiers() & QtCore.Qt.ShiftModifier:\n                if sat < 250:\n                    sat += 5\n                else:\n                    sat = 255\n            else:\n                if val < 250:\n                    val += 5\n                else:\n                    val = 255\n\n            tmp = QtGui.QColor()\n            tmp.setHsv(self.cur_hue, sat, val)\n            self.set_color(tmp)\n\n    def resizeEvent(self, _event):\n        size_w = self.contentsRect().width()\n        size_h = self.contentsRect().height()\n        if size_w < size_h:\n            size = size_w\n        else:\n            size = size_h\n\n        self.outer_radius = (size - 1) / 2\n\n        self.pen_width = int(\n            ceil(self.outer_radius / self.ellipse_thick_ratio)\n        )\n        self.ellipse_size = int(\n            ceil(self.outer_radius / self.ellipse_size_ratio)\n        )\n\n        cx = float(self.contentsRect().center().x())\n        cy = float(self.contentsRect().center().y())\n        inner_radius = (\n            self.outer_radius\n            - (self.outer_radius / self.inner_radius_ratio)\n        )\n        selector_radius = (\n            self.outer_radius\n            - (self.outer_radius / self.selector_radius_ratio)\n        )\n        self.point_a = QtCore.QPointF(\n            cx + (cos(self.angle_a) * inner_radius),\n            cy - (sin(self.angle_a) * inner_radius)\n        )\n        self.point_b = QtCore.QPointF(\n            cx + (cos(self.angle_b) * inner_radius),\n            cy - (sin(self.angle_b) * inner_radius)\n        )\n        self.point_c = QtCore.QPointF(\n            cx + (cos(self.angle_c) * inner_radius),\n            cy - (sin(self.angle_c) * inner_radius)\n        )\n        self.point_d = QtCore.QPointF(\n            cx + (cos(self.angle_a) * selector_radius),\n            cy - (sin(self.angle_a) * selector_radius)\n        )\n\n        # Find the current position of the selector\n        self.selector_pos = self._point_from_color(self.cur_color)\n\n        self.update()\n\n    def drawTrigon(self, buf, pa, pb, pc, color):\n        # Create three Vertex objects. A Vertex contains a double-point\n        # coordinate and a color.\n        # pa is the tip of the arrow\n        # pb is the black corner\n        # pc is the white corner\n        p1 = Vertex(color, pa)\n        p2 = Vertex(QtCore.Qt.black, pb)\n        p3 = Vertex(QtCore.Qt.white, pc)\n\n        # Sort. Make p1 above p2, which is above p3 (using y coordinate).\n        # Bubble sorting is fastest here.\n        if p1.point.y() > p2.point.y():\n            p1, p2 = p2, p1\n        if p1.point.y() > p3.point.y():\n            p1, p3 = p3, p1\n        if p2.point.y() > p3.point.y():\n            p2, p3 = p3, p2\n\n        # All the three y deltas are >= 0\n        p1p2ydist = float(p2.point.y() - p1.point.y())\n        p1p3ydist = float(p3.point.y() - p1.point.y())\n        p2p3ydist = float(p3.point.y() - p2.point.y())\n        p1p2xdist = float(p2.point.x() - p1.point.x())\n        p1p3xdist = float(p3.point.x() - p1.point.x())\n        p2p3xdist = float(p3.point.x() - p2.point.x())\n\n        # The first x delta decides wether we have a lefty or a righty\n        # trigon.\n        lefty = p1p2xdist < 0\n\n        # Left and right colors and X values. The key in this map is the\n        # y values. Our goal is to fill these structures with all the\n        # information needed to do a single pass top-to-bottom,\n        # left-to-right drawing of the trigon.\n        leftColors = {}\n        rightColors = {}\n        leftX = {}\n        rightX = {}\n\n        # Scan longy - find all left and right colors and X-values for\n        # the tallest edge (p1-p3).\n        # Initialize with known values\n        x = p1.point.x()\n        source = p1.color\n        dest = p3.color\n        r = source.r\n        g = source.g\n        b = source.b\n        y1 = int(floor(p1.point.y()))\n        y2 = int(floor(p3.point.y()))\n\n        # Find slopes (notice that if the y dists are 0, we don't care\n        # about the slopes)\n        xdelta = 0.0\n        rdelta = 0.0\n        gdelta = 0.0\n        bdelta = 0.0\n        if p1p3ydist != 0.0:\n            xdelta = p1p3xdist / p1p3ydist\n            rdelta = (dest.r - r) / p1p3ydist\n            gdelta = (dest.g - g) / p1p3ydist\n            bdelta = (dest.b - b) / p1p3ydist\n\n        # Calculate gradients using linear approximation\n        for y in range(y1, y2):\n            if lefty:\n                rightColors[y] = DoubleColor(r, g, b)\n                rightX[y] = x\n            else:\n                leftColors[y] = DoubleColor(r, g, b)\n                leftX[y] = x\n\n            r += rdelta\n            g += gdelta\n            b += bdelta\n            x += xdelta\n\n        # Scan top shorty - find all left and right colors and x-values\n        # for the topmost of the two not-tallest short edges.\n        x = p1.point.x()\n        source = p1.color\n        dest = p2.color\n        r = source.r\n        g = source.g\n        b = source.b\n        y1 = int(floor(p1.point.y()))\n        y2 = int(floor(p2.point.y()))\n\n        # Find slopes (notice that if the y dists are 0, we don't care\n        # about the slopes)\n        xdelta = 0.0\n        rdelta = 0.0\n        gdelta = 0.0\n        bdelta = 0.0\n        if p1p2ydist != 0.0:\n            xdelta = p1p2xdist / p1p2ydist\n            rdelta = (dest.r - r) / p1p2ydist\n            gdelta = (dest.g - g) / p1p2ydist\n            bdelta = (dest.b - b) / p1p2ydist\n\n        # Calculate gradients using linear approximation\n        for y in range(y1, y2):\n            if lefty:\n                leftColors[y] = DoubleColor(r, g, b)\n                leftX[y] = x\n            else:\n                rightColors[y] = DoubleColor(r, g, b)\n                rightX[y] = x\n\n            r += rdelta\n            g += gdelta\n            b += bdelta\n            x += xdelta\n\n        # Scan bottom shorty - find all left and right colors and\n        # x-values for the bottommost of the two not-tallest short edges.\n        x = p2.point.x()\n        source = p2.color\n        dest = p3.color\n        r = source.r\n        g = source.g\n        b = source.b\n        y1 = int(floor(p2.point.y()))\n        y2 = int(floor(p3.point.y()))\n\n        # Find slopes (notice that if the y dists are 0, we don't care\n        # about the slopes)\n        xdelta = 0.0\n        rdelta = 0.0\n        gdelta = 0.0\n        bdelta = 0.0\n        if p2p3ydist != 0.0:\n            xdelta = p2p3xdist / p2p3ydist\n            rdelta = (dest.r - r) / p2p3ydist\n            gdelta = (dest.g - g) / p2p3ydist\n            bdelta = (dest.b - b) / p2p3ydist\n\n        # Calculate gradients using linear approximation\n        for y in range(y1, y2):\n            if lefty:\n                leftColors[y] = DoubleColor(r, g, b)\n                leftX[y] = x\n            else:\n                rightColors[y] = DoubleColor(r, g, b)\n                rightX[y] = x\n\n            r += rdelta\n            g += gdelta\n            b += bdelta\n            x += xdelta\n\n        # Inner loop. For each y in the left map of x-values, draw one\n        # line from left to right.\n        p3yfloor = int(floor(p3.point.y()))\n        p1yfloor = int(floor(p1.point.y()))\n        for y in range(p1yfloor, p3yfloor):\n            lx = leftX[y]\n            rx = rightX[y]\n\n            # if the xdist is 0, don't draw anything.\n            xdist = rx - lx\n            if xdist == 0.0:\n                continue\n\n            lxi = int(floor(lx))\n            rxi = int(floor(rx))\n            rc = rightColors[y]\n            lc = leftColors[y]\n\n            r = lc.r\n            g = lc.g\n            b = lc.b\n            rdelta = (rc.r - r) / xdist\n            gdelta = (rc.g - g) / xdist\n            bdelta = (rc.b - b) / xdist\n\n            # Draw 2 more pixels on left side for smoothing\n            for x in range(lxi - 2, lxi):\n                buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b)))\n\n            # Inner loop 2. Draws the line from left to right.\n            for x in range(lxi, rxi):\n                buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b)))\n                r += rdelta\n                g += gdelta\n                b += bdelta\n\n            # Draw 2 more pixels on right side for smoothing\n            for x in range(rxi, rxi + 3):\n                buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b)))\n\n    def _radius_at(self, pos, rect):\n        mousexdist = pos.x() - float(rect.center().x())\n        mouseydist = pos.y() - float(rect.center().y())\n        return sqrt(mousexdist ** 2 + mouseydist ** 2)\n\n    def _angle_at(self, pos, rect):\n        mousexdist = pos.x() - float(rect.center().x())\n        mouseydist = pos.y() - float(rect.center().y())\n        mouserad = sqrt(mousexdist ** 2 + mouseydist ** 2)\n        if mouserad == 0.0:\n            return 0.0\n\n        angle = acos(mousexdist / mouserad)\n        if mouseydist >= 0:\n            angle = TWOPI - angle\n\n        return angle\n\n    def _point_from_color(self, col):\n        # Simplifications for the corner cases.\n        if col == QtCore.Qt.black:\n            return self.point_b\n        elif col == QtCore.Qt.white:\n            return self.point_c\n\n        # Find the x and y slopes\n        ab_deltax = self.point_b.x() - self.point_a.x()\n        ab_deltay = self.point_b.y() - self.point_a.y()\n        bc_deltax = self.point_c.x() - self.point_b.x()\n        bc_deltay = self.point_c.y() - self.point_b.y()\n        ac_deltax = self.point_c.x() - self.point_a.x()\n        ac_deltay = self.point_c.y() - self.point_a.y()\n\n        # Extract the h,s,v values of col.\n        _, sat, val, _ = col.getHsv()\n\n        # Find the line that passes through the triangle where the value\n        # is equal to our color's value.\n        p1 = self.point_a.x() + (ab_deltax * float(255 - val)) / 255.0\n        q1 = self.point_a.y() + (ab_deltay * float(255 - val)) / 255.0\n        p2 = self.point_b.x() + (bc_deltax * float(val)) / 255.0\n        q2 = self.point_b.y() + (bc_deltay * float(val)) / 255.0\n\n        # Find the line that passes through the triangle where the\n        # saturation is equal to our color's value.\n        p3 = self.point_a.x() + (ac_deltax * float(255 - sat)) / 255.0\n        q3 = self.point_a.y() + (ac_deltay * float(255 - sat)) / 255.0\n        p4 = self.point_b.x()\n        q4 = self.point_b.y()\n\n        # Find the intersection between these lines.\n        if p1 != p2:\n            a = (q2 - q1) / (p2 - p1)\n            c = (q4 - q3) / (p4 - p3)\n            b = q1 - a * p1\n            d = q3 - c * p3\n\n            x = (d - b) / (a - c)\n            y = a * x + b\n        else:\n            x = p1\n            p4_p3 = p4 - p3\n            if p4_p3 == 0:\n                y = 0\n            else:\n                y = q3 + (x - p3) * (q4 - q3) / p4_p3\n\n        return QtCore.QPointF(x, y)\n\n    def _color_from_point(self, p):\n        # Find the outer radius of the hue gradient.\n        size_w = self.contentsRect().width()\n        size_h = self.contentsRect().height()\n        if size_w < size_h:\n            size = size_w\n        else:\n            size = size_h\n        outer_radius = (size - 1) / 2\n\n        # Find the center coordinates\n        cx = float(self.contentsRect().center().x())\n        cy = float(self.contentsRect().center().y())\n\n        # Find the a, b and c from their angles, the center of the rect\n        # and the radius of the hue gradient donut.\n        inner_radius = outer_radius - (outer_radius / self.inner_radius_ratio)\n        pa = QtCore.QPointF(\n            cx + (cos(self.angle_a) * inner_radius),\n            cy - (sin(self.angle_a) * inner_radius)\n        )\n        pb = QtCore.QPointF(\n            cx + (cos(self.angle_b) * inner_radius),\n            cy - (sin(self.angle_b) * inner_radius)\n        )\n        pc = QtCore.QPointF(\n            cx + (cos(self.angle_c) * inner_radius),\n            cy - (sin(self.angle_c) * inner_radius)\n        )\n\n        # Find the hue value from the angle of the 'a' point.\n        angle = self.angle_a - PI / 2.0\n        if angle < 0:\n            angle += TWOPI\n        hue = (\n            360\n            - int(floor((360.0 * angle) / TWOPI))\n            - self.hue_offset\n        ) % 360\n\n        # Create the color of the 'a' corner point. We know that b is\n        # black and c is white.\n        color = QtGui.QColor()\n        color.setHsv(hue, 255, 255)\n\n        # See also drawTrigon(), which basically does exactly the same to\n        # determine all colors in the trigon.\n        p1 = Vertex(color, pa)\n        p2 = Vertex(QtCore.Qt.black, pb)\n        p3 = Vertex(QtCore.Qt.white, pc)\n\n        # Make sure p1 is above p2, which is above p3.\n        if p1.point.y() > p2.point.y():\n            p1, p2 = p2, p1\n        if p1.point.y() > p3.point.y():\n            p1, p3 = p3, p1\n        if p2.point.y() > p3.point.y():\n            p2, p3 = p3, p2\n\n        # Find the slopes of all edges in the trigon. All the three y\n        # deltas here are positive because of the above sorting.\n        p1p2ydist = p2.point.y() - p1.point.y()\n        p1p3ydist = p3.point.y() - p1.point.y()\n        p2p3ydist = p3.point.y() - p2.point.y()\n        p1p2xdist = p2.point.x() - p1.point.x()\n        p1p3xdist = p3.point.x() - p1.point.x()\n        p2p3xdist = p3.point.x() - p2.point.x()\n\n        # The first x delta decides wether we have a lefty or a righty\n        # trigon. A lefty trigon has its tallest edge on the right hand\n        # side of the trigon. The righty trigon has it on its left side.\n        # This property determines wether the left or the right set of x\n        # coordinates will be continuous.\n        lefty = p1p2xdist < 0\n\n        # Find whether the selector's y is in the first or second shorty,\n        # counting from the top and downwards. This is used to find the\n        # color at the selector point.\n        firstshorty = (p.y() >= p1.point.y() and p.y() < p2.point.y())\n\n        # From the y value of the selector's position, find the left and\n        # right x values.\n        if lefty:\n            if firstshorty:\n                if (floor(p1p2ydist) != 0.0):\n                    leftx = (\n                        p1.point.x()\n                        + ((p1p2xdist * (p.y() - p1.point.y())) / p1p2ydist)\n                    )\n                else:\n                    leftx = min(p1.point.x(), p2.point.x())\n\n            else:\n                if (floor(p2p3ydist) != 0.0):\n                    leftx = (\n                        p2.point.x()\n                        + (p2p3xdist * (p.y() - p2.point.y())) / p2p3ydist\n                    )\n                else:\n                    leftx = min(p2.point.x(), p3.point.x())\n\n            rightx = (\n                p1.point.x()\n                + ((p1p3xdist * (p.y() - p1.point.y())) / p1p3ydist)\n            )\n        else:\n            leftx = (\n                p1.point.x()\n                + ((p1p3xdist * (p.y() - p1.point.y())) / p1p3ydist)\n            )\n            if firstshorty:\n                if floor(p1p2ydist) != 0.0:\n                    rightx = (\n                        p1.point.x()\n                        + ((p1p2xdist * (p.y() - p1.point.y())) / p1p2ydist)\n                    )\n                else:\n                    rightx = max(p1.point.x(), p2.point.x())\n\n            else:\n                if floor(p2p3ydist) != 0.0:\n                    rightx = (\n                        p2.point.x()\n                        + ((p2p3xdist * (p.y() - p2.point.y())) / p2p3ydist)\n                    )\n                else:\n                    rightx = max(p2.point.x(), p3.point.x())\n\n        # Find the r,g,b values of the points on the trigon's edges that\n        # are to the left and right of the selector.\n        if firstshorty:\n            if floor(p1p2ydist) != 0.0:\n                p_p1_ratio = (p.y() - p1.point.y()) / p1p2ydist\n                p2_p_ratio = (p2.point.y() - p.y()) / p1p2ydist\n                rshort = (p2.color.r * p_p1_ratio) + (p1.color.r * p2_p_ratio)\n                gshort = (p2.color.g * p_p1_ratio) + (p1.color.g * p2_p_ratio)\n                bshort = (p2.color.b * p_p1_ratio) + (p1.color.b * p2_p_ratio)\n            elif lefty:\n                if p1.point.x() <= p2.point.x():\n                    rshort = p1.color.r\n                    gshort = p1.color.g\n                    bshort = p1.color.b\n                else:\n                    rshort = p2.color.r\n                    gshort = p2.color.g\n                    bshort = p2.color.b\n\n            else:\n                if p1.point.x() > p2.point.x():\n                    rshort = p1.color.r\n                    gshort = p1.color.g\n                    bshort = p1.color.b\n                else:\n                    rshort = p2.color.r\n                    gshort = p2.color.g\n                    bshort = p2.color.b\n\n        else:\n            if floor(p2p3ydist) != 0.0:\n                p_p2_ratio = (p.y() - p2.point.y()) / p2p3ydist\n                p3_p_ratio = (p3.point.y() - p.y()) / p2p3ydist\n                rshort = (p3.color.r * p_p2_ratio) + (p2.color.r * p3_p_ratio)\n                gshort = (p3.color.g * p_p2_ratio) + (p2.color.g * p3_p_ratio)\n                bshort = (p3.color.b * p_p2_ratio) + (p2.color.b * p3_p_ratio)\n            elif lefty:\n                if p2.point.x() <= p3.point.x():\n                    rshort = p2.color.r\n                    gshort = p2.color.g\n                    bshort = p2.color.b\n                else:\n                    rshort = p3.color.r\n                    gshort = p3.color.g\n                    bshort = p3.color.b\n\n            else:\n                if p2.point.x() > p3.point.x():\n                    rshort = p2.color.r\n                    gshort = p2.color.g\n                    bshort = p2.color.b\n                else:\n                    rshort = p3.color.r\n                    gshort = p3.color.g\n                    bshort = p3.color.b\n\n        # p1p3ydist is never 0\n        p_p1_ratio = (p.y() - p1.point.y()) / p1p3ydist\n        p3_p_ratio = (p3.point.y() - p.y()) / p1p3ydist\n        rlong = (p3.color.r * p_p1_ratio) + (p1.color.r * p3_p_ratio)\n        glong = (p3.color.g * p_p1_ratio) + (p1.color.g * p3_p_ratio)\n        blong = (p3.color.b * p_p1_ratio) + (p1.color.b * p3_p_ratio)\n\n        # rshort,gshort,bshort is the color on one of the shortys.\n        # rlong,glong,blong is the color on the longy. So depending on\n        # wether we have a lefty trigon or not, we can determine which\n        # colors are on the left and right edge.\n        if lefty:\n            rl = rshort\n            gl = gshort\n            bl = bshort\n            rr = rlong\n            gr = glong\n            br = blong\n        else:\n            rl = rlong\n            gl = glong\n            bl = blong\n            rr = rshort\n            gr = gshort\n            br = bshort\n\n        # Find the distance from the left x to the right x (xdist). Then\n        # find the distances from the selector to each of these (saxdist\n        # and saxdist2). These distances are used to find the color at\n        # the selector.\n        xdist = rightx - leftx\n        saxdist = p.x() - leftx\n        saxdist2 = xdist - saxdist\n\n        # Now determine the r,g,b values of the selector using a linear\n        # approximation.\n        if xdist != 0.0:\n            r = (saxdist2 * rl / xdist) + (saxdist * rr / xdist)\n            g = (saxdist2 * gl / xdist) + (saxdist * gr / xdist)\n            b = (saxdist2 * bl / xdist) + (saxdist * br / xdist)\n        else:\n            # In theory, the left and right color will be equal here. But\n            # because of the loss of precision, we get an error on both\n            # colors. The best approximation we can get is from adding\n            # the two errors, which in theory will eliminate the error\n            # but in practise will only minimize it.\n            r = (rl + rr) / 2\n            g = (gl + gr) / 2\n            b = (bl + br) / 2\n\n        # Now floor the color components and fit them into proper\n        # boundaries. This again is to compensate for the error caused by\n        # loss of precision.\n        ri = int(floor(r))\n        gi = int(floor(g))\n        bi = int(floor(b))\n        if ri < 0:\n            ri = 0\n        elif ri > 255:\n            ri = 255\n\n        if gi < 0:\n            gi = 0\n        elif gi > 255:\n            gi = 255\n\n        if bi < 0:\n            bi = 0\n        elif bi > 255:\n            bi = 255\n\n        # Voila, we have the color at the point of the selector.\n        return QtGui.QColor(ri, gi, bi)\n\n    def paint_bg(self):\n        bg_image = QtGui.QPixmap(self.contentsRect().size())\n        bg_image.fill(QtCore.Qt.transparent)\n        self.bg_image = bg_image\n\n        painter = QtGui.QPainter(self.bg_image)\n\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n\n        hue_gradient = QtGui.QConicalGradient(\n            bg_image.rect().center(), 90 - self.hue_offset\n        )\n        sat_val_gradient = QtGui.QConicalGradient(\n            bg_image.rect().center(), 90 - self.hue_offset\n        )\n\n        hue_color = QtGui.QColor()\n        sat_val_color = QtGui.QColor()\n        _, sat, val, _ = self.cur_color.getHsv()\n\n        for idx, hue in self._hue_circle_range:\n            hue_color.setHsv(hue, 255, 255)\n            sat_val_color.setHsv(hue, sat, val)\n\n            hue_gradient.setColorAt(idx, hue_color)\n            sat_val_gradient.setColorAt(idx, sat_val_color)\n\n        inner_radius = self.outer_radius - (\n            self.outer_radius / self.inner_radius_ratio\n        )\n        half_radius = self.outer_radius - (\n            (self.outer_radius - inner_radius) / 2\n        )\n\n        hue_inner_radius_rect = QtCore.QRectF(\n            bg_image.rect().center().x() - inner_radius,\n            bg_image.rect().center().y() - inner_radius,\n            inner_radius * 2 + 1,\n            inner_radius * 2 + 1\n        )\n        hue_outer_radius_rect = QtCore.QRectF(\n            bg_image.rect().center().x() - half_radius - 1,\n            bg_image.rect().center().y() - half_radius - 1,\n            half_radius * 2 + 3,\n            half_radius * 2 + 3\n        )\n        sat_val_inner_radius_rect = QtCore.QRectF(\n            bg_image.rect().center().x() - half_radius,\n            bg_image.rect().center().y() - half_radius,\n            half_radius * 2 + 1,\n            half_radius * 2 + 1\n        )\n        sat_val_outer_radius_rect = QtCore.QRectF(\n            bg_image.rect().center().x() - self.outer_radius,\n            bg_image.rect().center().y() - self.outer_radius,\n            self.outer_radius * 2 + 1,\n            self.outer_radius * 2 + 1\n        )\n        hue_path = QtGui.QPainterPath()\n        hue_path.addEllipse(hue_inner_radius_rect)\n        hue_path.addEllipse(hue_outer_radius_rect)\n\n        sat_val_path = QtGui.QPainterPath()\n        sat_val_path.addEllipse(sat_val_inner_radius_rect)\n        sat_val_path.addEllipse(sat_val_outer_radius_rect)\n\n        painter.save()\n        painter.setClipPath(hue_path)\n        painter.fillRect(self.bg_image.rect(), hue_gradient)\n        painter.restore()\n\n        painter.save()\n        painter.setClipPath(sat_val_path)\n        painter.fillRect(self.bg_image.rect(), sat_val_gradient)\n        painter.restore()\n\n        painter.end()\n\n    @staticmethod\n    def vlen(x, y):\n        return sqrt((x ** 2) + (y ** 2))\n\n    @staticmethod\n    def vprod(x1, y1, x2, y2):\n        return x1 * x2 + y1 * y2\n\n    @staticmethod\n    def _angle_between_angles(p, a1, a2):\n        if a1 > a2:\n            a2 += TWOPI\n            if p < PI:\n                p += TWOPI\n\n        return p >= a1 and p < a2\n\n    @staticmethod\n    def _point_above_point(x, y, px, py, ax, ay, bx, by):\n        floored_ax = floor(ax)\n        floored_bx = floor(bx)\n        floored_ay = floor(ay)\n        floored_by = floor(by)\n\n        if floored_ax == floored_bx:\n            # line is vertical\n            if floored_ay < floored_by:\n                return x < ax\n            elif floored_ay > floored_by:\n                return x > ax\n            return not (x == ax and y == ay)\n\n        if floored_ax > floored_bx:\n            if floored_ay < floored_by:\n                # line is draw upright-to-downleft\n                return (floor(x) < floor(px) or floor(y) < floor(py))\n            elif floored_ay > floored_by:\n                # line is draw downright-to-upleft\n                return (floor(x) > floor(px) or floor(y) < floor(py))\n            # line is flat horizontal\n            return y < ay\n\n        if floored_ay < floored_by:\n            # line is draw upleft-to-downright\n            return (floor(x) < floor(px) or floor(y) > floor(py))\n        elif floored_ay > floored_by:\n            # line is draw downleft-to-upright\n            return (floor(x) > floor(px) or floor(y) > floor(py))\n        # line is flat horizontal\n        return y > ay\n\n    @staticmethod\n    def _point_in_line(x, y, ax, ay, bx, by):\n        if ax > bx:\n            if ay < by:\n                # line is draw upright-to-downleft\n\n                # if (x,y) is in on or above the upper right point,\n                # return -1.\n                if y <= ay and x >= ax:\n                    return -1\n\n                # if (x,y) is in on or below the lower left point,\n                # return 1.\n                if y >= by and x <= bx:\n                    return 1\n            else:\n                # line is draw downright-to-upleft\n\n                # If the line is flat, only use the x coordinate.\n                if floor(ay) == floor(by):\n                    # if (x is to the right of the rightmost point,\n                    # return -1. otherwise if x is to the left of the\n                    # leftmost point, return 1.\n                    if x >= ax:\n                        return -1\n                    elif x <= bx:\n                        return 1\n                else:\n                    # if (x,y) is on or below the lower right point,\n                    # return -1.\n                    if y >= ay and x >= ax:\n                        return -1\n\n                    # if (x,y) is on or above the upper left point, return 1.\n                    if y <= by and x <= bx:\n                        return 1\n        else:\n            if ay < by:\n                # line is draw upleft-to-downright\n\n                # If (x,y) is on or above the upper left point, return -1.\n                if y <= ay and x <= ax:\n                    return -1\n\n                # If (x,y) is on or below the lower right point, return 1.\n                if y >= by and x >= bx:\n                    return 1\n            else:\n                # line is draw downleft-to-upright\n\n                # If the line is flat, only use the x coordinate.\n                if floor(ay) == floor(by):\n                    if x <= ax:\n                        return -1\n                    elif x >= bx:\n                        return 1\n                else:\n                    # If (x,y) is on or below the lower left point, return -1.\n                    if y >= ay and x <= ax:\n                        return -1\n\n                    # If (x,y) is on or above the upper right point, return 1.\n                    if y <= by and x >= bx:\n                        return 1\n\n        # No tests proved that (x,y) was outside [(ax,ay),(bx,by)], so we\n        # assume it's inside the line's bounds.\n        return 0\n\n    def _move_point_to_triangle(self, x, y, a, b, c):\n        # Let v1A be the vector from (x,y) to a.\n        # Let v2A be the vector from a to b.\n        # Find the angle alphaA between v1A and v2A.\n        v1xA = x - a.point.x()\n        v1yA = y - a.point.y()\n        v2xA = b.point.x() - a.point.x()\n        v2yA = b.point.y() - a.point.y()\n        vpA = self.vprod(v1xA, v1yA, v2xA, v2yA)\n        cosA = vpA / (self.vlen(v1xA, v1yA) * self.vlen(v2xA, v2yA))\n        alphaA = acos(cosA)\n\n        # Let v1B be the vector from x to b.\n        # Let v2B be the vector from b to c.\n        v1xB = x - b.point.x()\n        v1yB = y - b.point.y()\n        v2xB = c.point.x() - b.point.x()\n        v2yB = c.point.y() - b.point.y()\n        vpB = self.vprod(v1xB, v1yB, v2xB, v2yB)\n        cosB = vpB / (self.vlen(v1xB, v1yB) * self.vlen(v2xB, v2yB))\n        alphaB = acos(cosB)\n\n        # Let v1C be the vector from x to c.\n        # Let v2C be the vector from c back to a.\n        v1xC = x - c.point.x()\n        v1yC = y - c.point.y()\n        v2xC = a.point.x() - c.point.x()\n        v2yC = a.point.y() - c.point.y()\n        vpC = self.vprod(v1xC, v1yC, v2xC, v2yC)\n        cosC = vpC / (self.vlen(v1xC, v1yC) * self.vlen(v2xC, v2yC))\n        alphaC = acos(cosC)\n\n        # Find the radian angles between the (1,0) vector and the points\n        # A, B, C and (x,y). Use this information to determine which of\n        # the edges we should project (x,y) onto.\n        angleA = self._angle_at(a.point, self.contentsRect())\n        angleB = self._angle_at(b.point, self.contentsRect())\n        angleC = self._angle_at(c.point, self.contentsRect())\n        angleP = self._angle_at(QtCore.QPointF(x, y), self.contentsRect())\n\n        # If (x,y) is in the a-b area, project onto the a-b vector.\n        if self._angle_between_angles(angleP, angleA, angleB):\n            # Find the distance from (x,y) to a. Then use the slope of\n            # the a-b vector with this distance and the angle between a-b\n            # and a-(x,y) to determine the point of intersection of the\n            # perpendicular projection from (x,y) onto a-b.\n            pdist = sqrt(\n                ((x - a.point.x()) ** 2) + ((y - a.point.y()) ** 2)\n            )\n\n            # the length of all edges is always > 0\n            p0x = (\n                a.point.x()\n                + ((b.point.x() - a.point.x()) / self.vlen(v2xB, v2yB))\n                * cos(alphaA) * pdist\n            )\n            p0y = (\n                a.point.y()\n                + ((b.point.y() - a.point.y()) / self.vlen(v2xB, v2yB))\n                * cos(alphaA) * pdist\n            )\n\n            # If (x,y) is above the a-b line, which basically means it's\n            # outside the triangle, then return its projection onto a-b.\n            if self._point_above_point(\n                x, y,\n                p0x, p0y,\n                a.point.x(), a.point.y(),\n                b.point.x(), b.point.y()\n            ):\n                # If the projection is \"outside\" a, return a. If it is\n                # outside b, return b. Otherwise return the projection.\n                n = self._point_in_line(\n                    p0x, p0y,\n                    a.point.x(), a.point.y(),\n                    b.point.x(), b.point.y()\n                )\n                if n < 0:\n                    return a.point\n                elif n > 0:\n                    return b.point\n\n                return QtCore.QPointF(p0x, p0y)\n\n        elif self._angle_between_angles(angleP, angleB, angleC):\n            # If (x,y) is in the b-c area, project onto the b-c vector.\n            pdist = sqrt(\n                ((x - b.point.x()) ** 2) + ((y - b.point.y()) ** 2)\n            )\n\n            # the length of all edges is always > 0\n            p0x = (\n                b.point.x()\n                + ((c.point.x() - b.point.x()) / self.vlen(v2xC, v2yC))\n                * cos(alphaB) * pdist\n            )\n            p0y = (\n                b.point.y()\n                + ((c.point.y() - b.point.y()) / self.vlen(v2xC, v2yC))\n                * cos(alphaB)\n                * pdist\n            )\n\n            if self._point_above_point(\n                x, y,\n                p0x, p0y,\n                b.point.x(), b.point.y(),\n                c.point.x(), c.point.y()\n            ):\n                n = self._point_in_line(\n                    p0x, p0y,\n                    b.point.x(), b.point.y(),\n                    c.point.x(), c.point.y()\n                )\n                if n < 0:\n                    return b.point\n                elif n > 0:\n                    return c.point\n                return QtCore.QPointF(p0x, p0y)\n\n        elif self._angle_between_angles(angleP, angleC, angleA):\n            # If (x,y) is in the c-a area, project onto the c-a vector.\n            pdist = sqrt(\n                ((x - c.point.x()) ** 2) + ((y - c.point.y()) ** 2)\n            )\n\n            # the length of all edges is always > 0\n            p0x = (\n                c.point.x()\n                + ((a.point.x() - c.point.x()) / self.vlen(v2xA, v2yA))\n                * cos(alphaC)\n                * pdist\n            )\n            p0y = (\n                c.point.y()\n                + ((a.point.y() - c.point.y()) / self.vlen(v2xA, v2yA))\n                * cos(alphaC) * pdist\n            )\n\n            if self._point_above_point(\n                x, y,\n                p0x, p0y,\n                c.point.x(), c.point.y(),\n                a.point.x(), a.point.y()\n            ):\n                n = self._point_in_line(\n                    p0x, p0y,\n                    c.point.x(), c.point.y(),\n                    a.point.x(), a.point.y()\n                )\n                if n < 0:\n                    return c.point\n                elif n > 0:\n                    return a.point\n                return QtCore.QPointF(p0x, p0y)\n\n        # (x,y) is inside the triangle (inside a-b, b-c and a-c).\n        return QtCore.QPointF(x, y)\n"
  },
  {
    "path": "openpype/widgets/color_widgets/color_view.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\n\ndef draw_checkerboard_tile(piece_size=None, color_1=None, color_2=None):\n    if piece_size is None:\n        piece_size = 7\n\n    # Make sure piece size is not float\n    piece_size = int(piece_size)\n    if color_1 is None:\n        color_1 = QtGui.QColor(188, 188, 188)\n\n    if color_2 is None:\n        color_2 = QtGui.QColor(90, 90, 90)\n\n    pix = QtGui.QPixmap(piece_size * 2, piece_size * 2)\n    pix_painter = QtGui.QPainter(pix)\n\n    rect = QtCore.QRect(\n        0, 0, piece_size, piece_size\n    )\n    pix_painter.fillRect(rect, color_1)\n    rect.moveTo(piece_size, piece_size)\n    pix_painter.fillRect(rect, color_1)\n    rect.moveTo(piece_size, 0)\n    pix_painter.fillRect(rect, color_2)\n    rect.moveTo(0, piece_size)\n    pix_painter.fillRect(rect, color_2)\n    pix_painter.end()\n\n    return pix\n\n\nclass ColorViewer(QtWidgets.QWidget):\n    def __init__(self, parent=None):\n        super(ColorViewer, self).__init__(parent)\n\n        self.setMinimumSize(10, 10)\n\n        self.alpha = 255\n        self.actual_pen = QtGui.QPen()\n        self.actual_color = QtGui.QColor()\n        self._checkerboard = None\n\n    def checkerboard(self):\n        if not self._checkerboard:\n            self._checkerboard = draw_checkerboard_tile(4)\n        return self._checkerboard\n\n    def color(self):\n        return self.actual_color\n\n    def set_color(self, color):\n        if color == self.actual_color:\n            return\n\n        # Create copy of entered color\n        self.actual_color = QtGui.QColor(color)\n        # Set alpha by current alpha value\n        self.actual_color.setAlpha(self.alpha)\n        # Repaint\n        self.update()\n\n    def set_alpha(self, alpha):\n        if alpha == self.alpha:\n            return\n        # Change alpha of current color\n        self.actual_color.setAlpha(alpha)\n        # Store the value\n        self.alpha = alpha\n        # Repaint\n        self.update()\n\n    def paintEvent(self, event):\n        clip_rect = event.rect()\n        rect = clip_rect.adjusted(0, 0, -1, -1)\n\n        painter = QtGui.QPainter(self)\n        painter.setClipRect(clip_rect)\n        painter.drawTiledPixmap(rect, self.checkerboard())\n        painter.setBrush(self.actual_color)\n        pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 67))\n        painter.setPen(pen)\n        painter.drawRect(rect)\n        painter.end()\n"
  },
  {
    "path": "openpype/widgets/message_window.py",
    "content": "import sys\nimport logging\nfrom qtpy import QtWidgets, QtCore\n\nlog = logging.getLogger(__name__)\n\n\nclass Window(QtWidgets.QWidget):\n    def __init__(self, parent, title, message, level):\n        super(Window, self).__init__()\n        self.parent = parent\n        self.title = title\n        self.message = message\n        self.level = level\n\n        if self.level == \"info\":\n            self._info()\n        elif self.level == \"warning\":\n            self._warning()\n        elif self.level == \"critical\":\n            self._critical()\n\n    def _info(self):\n        self.setWindowTitle(self.title)\n        rc = QtWidgets.QMessageBox.information(\n            self, self.title, self.message)\n        if rc:\n            self.exit()\n\n    def _warning(self):\n        self.setWindowTitle(self.title)\n        rc = QtWidgets.QMessageBox.warning(\n            self, self.title, self.message)\n        if rc:\n            self.exit()\n\n    def _critical(self):\n        self.setWindowTitle(self.title)\n        rc = QtWidgets.QMessageBox.critical(\n            self, self.title, self.message)\n        if rc:\n            self.exit()\n\n    def exit(self):\n        self.hide()\n        # self.parent.exec_()\n        # self.parent.hide()\n        return\n\n\ndef message(title=None, message=None, level=\"info\", parent=None):\n    \"\"\"\n        Produces centered dialog with specific level denoting severity\n    Args:\n        title: (string) dialog title\n        message: (string) message\n        level: (string) info|warning|critical\n        parent: (QtWidgets.QApplication)\n\n    Returns:\n         None\n    \"\"\"\n    app = parent\n    if not app:\n        app = QtWidgets.QApplication(sys.argv)\n\n    ex = Window(app, title, message, level)\n    ex.show()\n\n    # Move widget to center of screen\n    try:\n        desktop_rect = QtWidgets.QApplication.desktop().availableGeometry(ex)\n        center = desktop_rect.center()\n        ex.move(\n            center.x() - (ex.width() * 0.5),\n            center.y() - (ex.height() * 0.5)\n        )\n    except Exception:\n        # skip all possible issues that may happen feature is not crutial\n        log.warning(\"Couldn't center message.\", exc_info=True)\n    # sys.exit(app.exec_())\n\n\nclass ScrollMessageBox(QtWidgets.QDialog):\n    \"\"\"\n        Basic version of scrollable QMessageBox. No other existing dialog\n        implementation is scrollable.\n        Args:\n            icon: <QtWidgets.QMessageBox.Icon>\n            title: <string>\n            messages: <list> of messages\n            cancelable: <boolean> - True if Cancel button should be added\n    \"\"\"\n    def __init__(self, icon, title, messages, cancelable=False):\n        super(ScrollMessageBox, self).__init__()\n        self.setWindowTitle(title)\n        self.icon = icon\n\n        self.setWindowFlags(QtCore.Qt.WindowTitleHint)\n\n        layout = QtWidgets.QVBoxLayout(self)\n\n        scroll_widget = QtWidgets.QScrollArea(self)\n        scroll_widget.setWidgetResizable(True)\n        content_widget = QtWidgets.QWidget(self)\n        scroll_widget.setWidget(content_widget)\n\n        message_len = 0\n        content_layout = QtWidgets.QVBoxLayout(content_widget)\n        for message in messages:\n            label_widget = QtWidgets.QLabel(message, content_widget)\n            content_layout.addWidget(label_widget)\n            message_len = max(message_len, len(message))\n\n        # guess size of scrollable area\n        desktop = QtWidgets.QApplication.desktop()\n        max_width = desktop.availableGeometry().width()\n        scroll_widget.setMinimumWidth(\n            min(max_width, message_len * 6)\n        )\n        layout.addWidget(scroll_widget)\n\n        if not cancelable:  # if no specific buttons OK only\n            buttons = QtWidgets.QDialogButtonBox.Ok\n        else:\n            buttons = QtWidgets.QDialogButtonBox.Ok | \\\n                      QtWidgets.QDialogButtonBox.Cancel\n\n        btn_box = QtWidgets.QDialogButtonBox(buttons)\n        btn_box.accepted.connect(self.accept)\n\n        if cancelable:\n            btn_box.reject.connect(self.reject)\n\n        btn = QtWidgets.QPushButton('Copy to clipboard')\n        btn.clicked.connect(lambda: QtWidgets.QApplication.\n                            clipboard().setText(\"\\n\".join(messages)))\n        btn_box.addButton(btn, QtWidgets.QDialogButtonBox.NoRole)\n\n        layout.addWidget(btn_box)\n        self.show()\n"
  },
  {
    "path": "openpype/widgets/nice_checkbox.py",
    "content": "from math import floor, sqrt, ceil\nfrom qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype.style import get_objected_colors\n\n\nclass NiceCheckbox(QtWidgets.QFrame):\n    stateChanged = QtCore.Signal(int)\n    clicked = QtCore.Signal()\n\n    _checked_bg_color = None\n    _unchecked_bg_color = None\n    _checker_color = None\n    _checker_hover_color = None\n\n    def __init__(self, checked=False, draw_icons=False, parent=None):\n        super(NiceCheckbox, self).__init__(parent)\n\n        self.setObjectName(\"NiceCheckbox\")\n        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)\n        self.setSizePolicy(\n            QtWidgets.QSizePolicy.Fixed,\n            QtWidgets.QSizePolicy.Fixed\n        )\n        self._checked = checked\n        if checked:\n            checkstate = QtCore.Qt.Checked\n        else:\n            checkstate = QtCore.Qt.Unchecked\n        self._checkstate = checkstate\n        self._is_tristate = False\n\n        self._draw_icons = draw_icons\n\n        self._animation_timer = QtCore.QTimer(self)\n        self._animation_timeout = 6\n\n        self._fixed_width_set = False\n        self._fixed_height_set = False\n\n        self._current_step = None\n        self._steps = 21\n        self._middle_step = 11\n        self.set_steps(self._steps)\n\n        self._checker_margins_divider = 0\n\n        self._pressed = False\n        self._under_mouse = False\n\n        self.icon_scale_factor = sqrt(2) / 2\n\n        icon_path_stroker = QtGui.QPainterPathStroker()\n        icon_path_stroker.setCapStyle(QtCore.Qt.RoundCap)\n        icon_path_stroker.setJoinStyle(QtCore.Qt.RoundJoin)\n\n        self.icon_path_stroker = icon_path_stroker\n\n        self._animation_timer.timeout.connect(self._on_animation_timeout)\n\n        self._base_size = QtCore.QSize(90, 50)\n        self._load_colors()\n\n    @classmethod\n    def _load_colors(cls):\n        if cls._checked_bg_color is not None:\n            return\n\n        colors_info = get_objected_colors(\"nice-checkbox\")\n\n        cls._checked_bg_color = colors_info[\"bg-checked\"].get_qcolor()\n        cls._unchecked_bg_color = colors_info[\"bg-unchecked\"].get_qcolor()\n\n        cls._checker_color = colors_info[\"bg-checker\"].get_qcolor()\n        cls._checker_hover_color = colors_info[\"bg-checker-hover\"].get_qcolor()\n\n    @property\n    def checked_bg_color(self):\n        return self._checked_bg_color\n\n    @property\n    def unchecked_bg_color(self):\n        return self._unchecked_bg_color\n\n    @property\n    def checker_color(self):\n        return self._checker_color\n\n    @property\n    def checker_hover_color(self):\n        return self._checker_hover_color\n\n    def setTristate(self, tristate=True):\n        if self._is_tristate != tristate:\n            self._is_tristate = tristate\n\n    def set_draw_icons(self, draw_icons=None):\n        if draw_icons is None:\n            draw_icons = not self._draw_icons\n\n        if draw_icons == self._draw_icons:\n            return\n\n        self._draw_icons = draw_icons\n        self.repaint()\n\n    def sizeHint(self):\n        height = self.fontMetrics().height()\n        width = self.get_width_hint_by_height(height)\n        return QtCore.QSize(width, height)\n\n    def get_width_hint_by_height(self, height):\n        return int((\n            float(height) / self._base_size.height()\n        ) * self._base_size.width())\n\n    def get_height_hint_by_width(self, width):\n        return int((\n            float(width) / self._base_size.width()\n        ) * self._base_size.height())\n\n    def setFixedHeight(self, *args, **kwargs):\n        self._fixed_height_set = True\n        super(NiceCheckbox, self).setFixedHeight(*args, **kwargs)\n        if not self._fixed_width_set:\n            width = self.get_width_hint_by_height(self.height())\n            self.setFixedWidth(width)\n\n    def setFixedWidth(self, *args, **kwargs):\n        self._fixed_width_set = True\n        super(NiceCheckbox, self).setFixedWidth(*args, **kwargs)\n        if not self._fixed_height_set:\n            height = self.get_height_hint_by_width(self.width())\n            self.setFixedHeight(height)\n\n    def setFixedSize(self, *args, **kwargs):\n        self._fixed_height_set = True\n        self._fixed_width_set = True\n        super(NiceCheckbox, self).setFixedSize(*args, **kwargs)\n\n    def steps(self):\n        return self._steps\n\n    def set_steps(self, steps):\n        if steps < 2:\n            steps = 2\n\n        # Make sure animation is stopped\n        if self._animation_timer.isActive():\n            self._animation_timer.stop()\n\n        # Set steps and set current step by current checkstate\n        self._steps = steps\n        diff = steps % 2\n        self._middle_step = (int(steps - diff) / 2) + diff\n        if self._checkstate == QtCore.Qt.Checked:\n            self._current_step = self._steps\n        elif self._checkstate == QtCore.Qt.Unchecked:\n            self._current_step = 0\n        else:\n            self._current_step = self._middle_step\n\n    def checkState(self):\n        return self._checkstate\n\n    def isChecked(self):\n        return self._checked\n\n    def _checkstate_int_to_enum(self, state):\n        if not isinstance(state, int):\n            return state\n\n        if state == 2:\n            return QtCore.Qt.Checked\n        if state == 1:\n            return QtCore.Qt.PartiallyChecked\n        return QtCore.Qt.Unchecked\n\n    def _checkstate_enum_to_int(self, state):\n        if isinstance(state, int):\n            return state\n        if state == QtCore.Qt.Checked:\n            return 2\n        if state == QtCore.Qt.PartiallyChecked:\n            return 1\n        return 0\n\n    def setCheckState(self, state):\n        state = self._checkstate_int_to_enum(state)\n        if self._checkstate == state:\n            return\n\n        self._checkstate = state\n        if state == QtCore.Qt.Checked:\n            self._checked = True\n        elif state == QtCore.Qt.Unchecked:\n            self._checked = False\n\n        self.stateChanged.emit(self._checkstate_enum_to_int(self.checkState()))\n\n        if self._animation_timer.isActive():\n            self._animation_timer.stop()\n\n        if self.isVisible() and self.isEnabled():\n            # Start animation\n            self._animation_timer.start(self._animation_timeout)\n        else:\n            # Do not animate change if is disabled\n            if state == QtCore.Qt.Checked:\n                self._current_step = self._steps\n            elif state == QtCore.Qt.Unchecked:\n                self._current_step = 0\n            else:\n                self._current_step = self._middle_step\n            self.repaint()\n\n    def setChecked(self, checked):\n        if checked == self._checked:\n            return\n\n        if checked:\n            checkstate = QtCore.Qt.Checked\n        else:\n            checkstate = QtCore.Qt.Unchecked\n\n        self.setCheckState(checkstate)\n\n    def nextCheckState(self):\n        if self._checkstate == QtCore.Qt.Unchecked:\n            if self._is_tristate:\n                return QtCore.Qt.PartiallyChecked\n            return QtCore.Qt.Checked\n\n        if self._checkstate == QtCore.Qt.Checked:\n            return QtCore.Qt.Unchecked\n\n        if self._checked:\n            return QtCore.Qt.Unchecked\n        return QtCore.Qt.Checked\n\n    def mousePressEvent(self, event):\n        if event.buttons() & QtCore.Qt.LeftButton:\n            self._pressed = True\n            self.repaint()\n        super(NiceCheckbox, self).mousePressEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        if self._pressed and not event.buttons() & QtCore.Qt.LeftButton:\n            self._pressed = False\n            if self.rect().contains(event.pos()):\n                self.setCheckState(self.nextCheckState())\n                self.clicked.emit()\n                event.accept()\n                return\n        super(NiceCheckbox, self).mouseReleaseEvent(event)\n\n    def mouseMoveEvent(self, event):\n        if self._pressed:\n            under_mouse = self.rect().contains(event.pos())\n            if under_mouse != self._under_mouse:\n                self._under_mouse = under_mouse\n                self.repaint()\n\n        super(NiceCheckbox, self).mouseMoveEvent(event)\n\n    def enterEvent(self, event):\n        self._under_mouse = True\n        if self.isEnabled():\n            self.repaint()\n        super(NiceCheckbox, self).enterEvent(event)\n\n    def leaveEvent(self, event):\n        self._under_mouse = False\n        if self.isEnabled():\n            self.repaint()\n        super(NiceCheckbox, self).leaveEvent(event)\n\n    def _on_animation_timeout(self):\n        if self._checkstate == QtCore.Qt.Checked:\n            if self._current_step == self._steps:\n                self._animation_timer.stop()\n                return\n            self._current_step += 1\n\n        elif self._checkstate == QtCore.Qt.Unchecked:\n            if self._current_step == 0:\n                self._animation_timer.stop()\n                return\n            self._current_step -= 1\n\n        else:\n            if self._current_step < self._middle_step:\n                self._current_step += 1\n\n            elif self._current_step > self._middle_step:\n                self._current_step -= 1\n\n            if self._current_step == self._middle_step:\n                self._animation_timer.stop()\n\n        self.repaint()\n\n    @staticmethod\n    def steped_color(color1, color2, offset_ratio):\n        red_dif = (\n            color1.red() - color2.red()\n        )\n        green_dif = (\n            color1.green() - color2.green()\n        )\n        blue_dif = (\n            color1.blue() - color2.blue()\n        )\n        red = int(color2.red() + (\n            red_dif * offset_ratio\n        ))\n        green = int(color2.green() + (\n            green_dif * offset_ratio\n        ))\n        blue = int(color2.blue() + (\n            blue_dif * offset_ratio\n        ))\n\n        return QtGui.QColor(red, green, blue)\n\n    def paintEvent(self, event):\n        frame_rect = QtCore.QRect(self.rect())\n        if frame_rect.width() < 0 or frame_rect.height() < 0:\n            return\n\n        painter = QtGui.QPainter(self)\n\n        painter.setRenderHint(QtGui.QPainter.Antialiasing)\n\n        # Draw inner background\n        if self._current_step == self._steps:\n            bg_color = self.checked_bg_color\n\n        elif self._current_step == 0:\n            bg_color = self.unchecked_bg_color\n\n        else:\n            offset_ratio = float(self._current_step) / self._steps\n            # Animation bg\n            bg_color = self.steped_color(\n                self.checked_bg_color,\n                self.unchecked_bg_color,\n                offset_ratio\n            )\n\n        margins_ratio = float(self._checker_margins_divider)\n        if margins_ratio > 0:\n            size_without_margins = int(\n                (float(frame_rect.height()) / margins_ratio)\n                * (margins_ratio - 2)\n            )\n            size_without_margins -= size_without_margins % 2\n            margin_size_c = ceil(\n                frame_rect.height() - size_without_margins\n            ) / 2\n\n        else:\n            size_without_margins = frame_rect.height()\n            margin_size_c = 0\n\n        checkbox_rect = QtCore.QRect(\n            frame_rect.x() + margin_size_c,\n            frame_rect.y() + margin_size_c,\n            frame_rect.width() - (margin_size_c * 2),\n            frame_rect.height() - (margin_size_c * 2)\n        )\n\n        if checkbox_rect.width() > checkbox_rect.height():\n            radius = floor(checkbox_rect.height() * 0.5)\n        else:\n            radius = floor(checkbox_rect.width() * 0.5)\n\n        painter.setPen(QtCore.Qt.transparent)\n        painter.setBrush(bg_color)\n        painter.drawRoundedRect(checkbox_rect, radius, radius)\n\n        # Draw checker\n        checker_size = size_without_margins - (margin_size_c * 2)\n        area_width = (\n            checkbox_rect.width()\n            - (margin_size_c * 2)\n            - checker_size\n        )\n        if self._current_step == 0:\n            x_offset = 0\n        else:\n            x_offset = (float(area_width) / self._steps) * self._current_step\n\n        pos_x = checkbox_rect.x() + x_offset + margin_size_c\n        pos_y = checkbox_rect.y() + margin_size_c\n\n        checker_rect = QtCore.QRect(pos_x, pos_y, checker_size, checker_size)\n\n        under_mouse = self.isEnabled() and self._under_mouse\n        if under_mouse:\n            checker_color = self.checker_hover_color\n        else:\n            checker_color = self.checker_color\n\n        painter.setBrush(checker_color)\n        painter.drawEllipse(checker_rect)\n\n        if self._draw_icons:\n            painter.setBrush(bg_color)\n            icon_path = self._get_icon_path(painter, checker_rect)\n            painter.drawPath(icon_path)\n\n        # Draw shadow overlay\n        if not self.isEnabled():\n            level = 33\n            alpha = 127\n            painter.setPen(QtCore.Qt.transparent)\n            painter.setBrush(QtGui.QColor(level, level, level, alpha))\n            painter.drawRoundedRect(checkbox_rect, radius, radius)\n\n        painter.end()\n\n    def _get_icon_path(self, painter, checker_rect):\n        self.icon_path_stroker.setWidth(checker_rect.height() / 5)\n\n        if self._current_step == self._steps:\n            return self._get_enabled_icon_path(painter, checker_rect)\n\n        if self._current_step == 0:\n            return self._get_disabled_icon_path(painter, checker_rect)\n\n        if self._current_step == self._middle_step:\n            return self._get_middle_circle_path(painter, checker_rect)\n\n        disabled_step = self._steps - self._current_step\n        enabled_step = self._steps - disabled_step\n        half_steps = self._steps + 1 - ((self._steps + 1) % 2)\n        if enabled_step > disabled_step:\n            return self._get_enabled_icon_path(\n                painter, checker_rect, enabled_step, half_steps\n            )\n        else:\n            return self._get_disabled_icon_path(\n                painter, checker_rect, disabled_step, half_steps\n            )\n\n    def _get_middle_circle_path(self, painter, checker_rect):\n        width = self.icon_path_stroker.width()\n        path = QtGui.QPainterPath()\n        path.addEllipse(checker_rect.center(), width, width)\n\n        return path\n\n    def _get_enabled_icon_path(\n        self, painter, checker_rect, step=None, half_steps=None\n    ):\n        fifteenth = float(checker_rect.height()) / 15\n        # Left point\n        p1 = QtCore.QPoint(\n            int(checker_rect.x() + (5 * fifteenth)),\n            int(checker_rect.y() + (9 * fifteenth))\n        )\n        # Middle bottom point\n        p2 = QtCore.QPoint(\n            checker_rect.center().x(),\n            int(checker_rect.y() + (11 * fifteenth))\n        )\n        # Top right point\n        p3 = QtCore.QPoint(\n            int(checker_rect.x() + (10 * fifteenth)),\n            int(checker_rect.y() + (5 * fifteenth))\n        )\n        if step is not None:\n            multiplier = (half_steps - step)\n\n            p1c = p1 - checker_rect.center()\n            p2c = p2 - checker_rect.center()\n            p3c = p3 - checker_rect.center()\n\n            p1o = QtCore.QPoint(\n                int((float(p1c.x()) / half_steps) * multiplier),\n                int((float(p1c.y()) / half_steps) * multiplier)\n            )\n            p2o = QtCore.QPoint(\n                int((float(p2c.x()) / half_steps) * multiplier),\n                int((float(p2c.y()) / half_steps) * multiplier)\n            )\n            p3o = QtCore.QPoint(\n                int((float(p3c.x()) / half_steps) * multiplier),\n                int((float(p3c.y()) / half_steps) * multiplier)\n            )\n\n            p1 -= p1o\n            p2 -= p2o\n            p3 -= p3o\n\n        path = QtGui.QPainterPath(p1)\n        path.lineTo(p2)\n        path.lineTo(p3)\n\n        return self.icon_path_stroker.createStroke(path)\n\n    def _get_disabled_icon_path(\n        self, painter, checker_rect, step=None, half_steps=None\n    ):\n        center_point = QtCore.QPointF(\n            float(checker_rect.width()) / 2,\n            float(checker_rect.height()) / 2\n        )\n        offset = float((\n            (center_point + QtCore.QPointF(0, 0)) / 2\n        ).x()) / 4 * 5\n        if step is not None:\n            diff = center_point.x() - offset\n            diff_offset = (diff / half_steps) * (half_steps - step)\n            offset += diff_offset\n\n        line1_p1 = QtCore.QPointF(\n            checker_rect.topLeft().x() + offset,\n            checker_rect.topLeft().y() + offset,\n        )\n        line1_p2 = QtCore.QPointF(\n            checker_rect.bottomRight().x() - offset,\n            checker_rect.bottomRight().y() - offset\n        )\n        line2_p1 = QtCore.QPointF(\n            checker_rect.bottomLeft().x() + offset,\n            checker_rect.bottomLeft().y() - offset\n        )\n        line2_p2 = QtCore.QPointF(\n            checker_rect.topRight().x() - offset,\n            checker_rect.topRight().y() + offset\n        )\n        path = QtGui.QPainterPath()\n        path.moveTo(line1_p1)\n        path.lineTo(line1_p2)\n        path.moveTo(line2_p1)\n        path.lineTo(line2_p2)\n\n        return self.icon_path_stroker.createStroke(path)\n"
  },
  {
    "path": "openpype/widgets/password_dialog.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\nfrom openpype import style\nfrom openpype.resources import get_resource\n\nfrom openpype.settings import get_system_settings\nfrom openpype.settings.lib import (\n    get_local_settings,\n    save_local_settings\n)\n\n\nclass PressHoverButton(QtWidgets.QPushButton):\n    _mouse_pressed = False\n    _mouse_hovered = False\n    change_state = QtCore.Signal(bool)\n\n    def mousePressEvent(self, event):\n        self._mouse_pressed = True\n        self._mouse_hovered = True\n        self.change_state.emit(self._mouse_hovered)\n        super(PressHoverButton, self).mousePressEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        self._mouse_pressed = False\n        self._mouse_hovered = False\n        self.change_state.emit(self._mouse_hovered)\n        super(PressHoverButton, self).mouseReleaseEvent(event)\n\n    def mouseMoveEvent(self, event):\n        mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())\n        under_mouse = self.rect().contains(mouse_pos)\n        if under_mouse != self._mouse_hovered:\n            self._mouse_hovered = under_mouse\n            self.change_state.emit(self._mouse_hovered)\n\n        super(PressHoverButton, self).mouseMoveEvent(event)\n\n\nclass PasswordDialog(QtWidgets.QDialog):\n    \"\"\"Stupidly simple dialog to compare password from general settings.\"\"\"\n    finished = QtCore.Signal(bool)\n\n    def __init__(self, parent=None, allow_remember=True):\n        super(PasswordDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"Admin Password\")\n        self.resize(300, 120)\n\n        system_settings = get_system_settings()\n\n        self._expected_result = (\n            system_settings[\"general\"].get(\"admin_password\")\n        )\n        self._final_result = None\n        self._allow_remember = allow_remember\n\n        # Password input\n        password_widget = QtWidgets.QWidget(self)\n\n        password_label = QtWidgets.QLabel(\"Password:\", password_widget)\n\n        password_input = QtWidgets.QLineEdit(password_widget)\n        password_input.setEchoMode(QtWidgets.QLineEdit.Password)\n\n        show_password_icon_path = get_resource(\"icons\", \"eye.png\")\n        show_password_icon = QtGui.QIcon(show_password_icon_path)\n        show_password_btn = PressHoverButton(password_widget)\n        show_password_btn.setObjectName(\"PasswordBtn\")\n        show_password_btn.setIcon(show_password_icon)\n        show_password_btn.setFocusPolicy(QtCore.Qt.ClickFocus)\n\n        password_layout = QtWidgets.QHBoxLayout(password_widget)\n        password_layout.setContentsMargins(0, 0, 0, 0)\n        password_layout.addWidget(password_label)\n        password_layout.addWidget(password_input)\n        password_layout.addWidget(show_password_btn)\n\n        message_label = QtWidgets.QLabel(\"\", self)\n\n        # Buttons\n        buttons_widget = QtWidgets.QWidget(self)\n\n        remember_checkbox = QtWidgets.QCheckBox(\"Remember\", buttons_widget)\n        remember_checkbox.setObjectName(\"RememberCheckbox\")\n        remember_checkbox.setVisible(allow_remember)\n\n        ok_btn = QtWidgets.QPushButton(\"Ok\", buttons_widget)\n        cancel_btn = QtWidgets.QPushButton(\"Cancel\", buttons_widget)\n\n        buttons_layout = QtWidgets.QHBoxLayout(buttons_widget)\n        buttons_layout.setContentsMargins(0, 0, 0, 0)\n        buttons_layout.addWidget(remember_checkbox)\n        buttons_layout.addStretch(1)\n        buttons_layout.addWidget(ok_btn)\n        buttons_layout.addWidget(cancel_btn)\n\n        # Main layout\n        layout = QtWidgets.QVBoxLayout(self)\n        layout.addSpacing(10)\n        layout.addWidget(password_widget, 0)\n        layout.addWidget(message_label, 0)\n        layout.addStretch(1)\n        layout.addWidget(buttons_widget, 0)\n\n        ok_btn.clicked.connect(self._on_ok_click)\n        cancel_btn.clicked.connect(self._on_cancel_click)\n        show_password_btn.change_state.connect(self._on_show_password)\n\n        self.password_input = password_input\n        self.remember_checkbox = remember_checkbox\n        self.message_label = message_label\n\n        self.setStyleSheet(style.load_stylesheet())\n\n    def remember_password(self):\n        if not self._allow_remember:\n            return False\n        return self.remember_checkbox.isChecked()\n\n    def result(self):\n        if self._final_result is None:\n            return False\n        return self._final_result == self._expected_result\n\n    def keyPressEvent(self, event):\n        if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):\n            self._on_ok_click()\n            return event.accept()\n        super(PasswordDialog, self).keyPressEvent(event)\n\n    def closeEvent(self, event):\n        super(PasswordDialog, self).closeEvent(event)\n        self.finished.emit(self.result())\n\n    def _on_ok_click(self):\n        input_value = self.password_input.text()\n        if input_value != self._expected_result:\n            self.message_label.setText(\"Invalid password. Try it again...\")\n            self.password_input.setFocus()\n            return\n\n        if self.remember_password():\n            local_settings = get_local_settings()\n            if \"general\" not in local_settings:\n                local_settings[\"general\"] = {}\n\n            local_settings[\"general\"][\"is_admin\"] = True\n\n            save_local_settings(local_settings)\n\n        self._final_result = input_value\n        self.close()\n\n    def _on_show_password(self, show_password):\n        if show_password:\n            echo_mode = QtWidgets.QLineEdit.Normal\n        else:\n            echo_mode = QtWidgets.QLineEdit.Password\n        self.password_input.setEchoMode(echo_mode)\n\n    def _on_cancel_click(self):\n        self.close()\n"
  },
  {
    "path": "openpype/widgets/popup.py",
    "content": "import sys\nimport contextlib\n\nfrom qtpy import QtCore, QtWidgets\n\n\nclass Popup(QtWidgets.QDialog):\n    \"\"\"A Popup that moves itself to bottom right of screen on show event.\n\n    The UI contains a message label and a red highlighted button to \"show\"\n    or perform another custom action from this pop-up.\n\n    \"\"\"\n\n    on_clicked = QtCore.Signal()\n\n    def __init__(self, parent=None, *args, **kwargs):\n        super(Popup, self).__init__(parent=parent, *args, **kwargs)\n        self.setContentsMargins(0, 0, 0, 0)\n\n        # Layout\n        layout = QtWidgets.QHBoxLayout(self)\n        layout.setContentsMargins(10, 5, 10, 10)\n\n        # Increase spacing slightly for readability\n        layout.setSpacing(10)\n\n        message = QtWidgets.QLabel(\"\")\n        message.setStyleSheet(\"\"\"\n        QLabel {\n            font-size: 12px;\n        }\n        \"\"\")\n        button = QtWidgets.QPushButton(\"Show\")\n        button.setSizePolicy(QtWidgets.QSizePolicy.Maximum,\n                           QtWidgets.QSizePolicy.Maximum)\n        button.setStyleSheet(\"\"\"QPushButton { background-color: #BB0000 }\"\"\")\n\n        layout.addWidget(message)\n        layout.addWidget(button)\n\n        # Default size\n        self.resize(400, 40)\n\n        self.widgets = {\n            \"message\": message,\n            \"button\": button,\n        }\n\n        # Signals\n        button.clicked.connect(self._on_clicked)\n\n        # Set default title\n        self.setWindowTitle(\"Popup\")\n\n    def setMessage(self, message):\n        self.widgets['message'].setText(message)\n\n    def setButtonText(self, text):\n        self.widgets[\"button\"].setText(text)\n\n    def _on_clicked(self):\n        \"\"\"Callback for when the 'show' button is clicked.\n\n        Raises the parent (if any)\n\n        \"\"\"\n\n        parent = self.parent()\n        self.close()\n\n        # Trigger the signal\n        self.on_clicked.emit()\n\n        if parent:\n            parent.raise_()\n\n    def showEvent(self, event):\n\n        # Position popup based on contents on show event\n        geo = self.calculate_window_geometry()\n        self.setGeometry(geo)\n\n        return super(Popup, self).showEvent(event)\n\n    def calculate_window_geometry(self):\n        \"\"\"Respond to status changes\n\n        On creation, align window with screen bottom right.\n\n        \"\"\"\n\n        window = self\n\n        width = window.width()\n        width = max(width, window.minimumWidth())\n\n        height = window.height()\n        height = max(height, window.sizeHint().height())\n\n        try:\n            screen = window.screen()\n            desktop_geometry = screen.availableGeometry()\n        except AttributeError:\n            # Backwards compatibility for older Qt versions\n            # PySide6 removed QDesktopWidget\n            desktop_geometry = QtWidgets.QDesktopWidget().availableGeometry()\n\n        window_geometry = window.geometry()\n\n        screen_width = window_geometry.width()\n        screen_height = window_geometry.height()\n\n        # Calculate width and height of system tray\n        systray_width = window_geometry.width() - desktop_geometry.width()\n        systray_height = window_geometry.height() - desktop_geometry.height()\n\n        padding = 10\n\n        x = screen_width - width\n        y = screen_height - height\n\n        x -= systray_width + padding\n        y -= systray_height + padding\n\n        return QtCore.QRect(x, y, width, height)\n\n\nclass PopupUpdateKeys(Popup):\n    \"\"\"Popup with Update Keys checkbox (intended for Maya)\"\"\"\n\n    on_clicked_state = QtCore.Signal(bool)\n\n    def __init__(self, parent=None, *args, **kwargs):\n        Popup.__init__(self, parent=parent, *args, **kwargs)\n\n        layout = self.layout()\n\n        # Insert toggle for Update keys\n        toggle = QtWidgets.QCheckBox(\"Update Keys\")\n        layout.insertWidget(1, toggle)\n        self.widgets[\"toggle\"] = toggle\n\n        self.on_clicked.connect(self.emit_click_with_state)\n\n        layout.insertStretch(1, 1)\n\n    def emit_click_with_state(self):\n        \"\"\"Emit the on_clicked_state signal with the toggled state\"\"\"\n        checked = self.widgets[\"toggle\"].isChecked()\n        self.on_clicked_state.emit(checked)\n\n\n@contextlib.contextmanager\ndef application():\n    app = QtWidgets.QApplication(sys.argv)\n    yield\n    app.exec_()\n\n\nif __name__ == \"__main__\":\n    with application():\n        dialog = Popup()\n        dialog.setMessage(\"There are outdated containers in your Maya scene.\")\n        dialog.show()\n"
  },
  {
    "path": "openpype/widgets/sliders.py",
    "content": "from qtpy import QtWidgets, QtCore, QtGui\n\n\nclass NiceSlider(QtWidgets.QSlider):\n    def __init__(self, *args, **kwargs):\n        super(NiceSlider, self).__init__(*args, **kwargs)\n        self._mouse_clicked = False\n        self._handle_size = 0\n\n        self._bg_brush = QtGui.QBrush(QtGui.QColor(\"#21252B\"))\n        self._fill_brush = QtGui.QBrush(QtGui.QColor(\"#5cadd6\"))\n\n    def mousePressEvent(self, event):\n        self._mouse_clicked = True\n        if event.button() == QtCore.Qt.LeftButton:\n            self._set_value_to_pos(event.pos())\n            return event.accept()\n        return super(NiceSlider, self).mousePressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        if self._mouse_clicked:\n            self._set_value_to_pos(event.pos())\n\n        super(NiceSlider, self).mouseMoveEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        self._mouse_clicked = True\n        super(NiceSlider, self).mouseReleaseEvent(event)\n\n    def _set_value_to_pos(self, pos):\n        if self.orientation() == QtCore.Qt.Horizontal:\n            self._set_value_to_pos_x(pos.x())\n        else:\n            self._set_value_to_pos_y(pos.y())\n\n    def _set_value_to_pos_x(self, pos_x):\n        _range = self.maximum() - self.minimum()\n        handle_size = self._handle_size\n        half_handle = handle_size / 2\n        pos_x -= half_handle\n        width = self.width() - handle_size\n        value = ((_range * pos_x) / width) + self.minimum()\n        self.setValue(value)\n\n    def _set_value_to_pos_y(self, pos_y):\n        _range = self.maximum() - self.minimum()\n        handle_size = self._handle_size\n        half_handle = handle_size / 2\n        pos_y = self.height() - pos_y - half_handle\n        height = self.height() - handle_size\n        value = (_range * pos_y / height) + self.minimum()\n        self.setValue(value)\n\n    def paintEvent(self, event):\n        painter = QtGui.QPainter(self)\n        opt = QtWidgets.QStyleOptionSlider()\n        self.initStyleOption(opt)\n\n        painter.fillRect(event.rect(), QtCore.Qt.transparent)\n\n        painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)\n\n        horizontal = self.orientation() == QtCore.Qt.Horizontal\n\n        rect = self.style().subControlRect(\n            QtWidgets.QStyle.CC_Slider,\n            opt,\n            QtWidgets.QStyle.SC_SliderGroove,\n            self\n        )\n\n        _range = self.maximum() - self.minimum()\n        _offset = self.value() - self.minimum()\n        if horizontal:\n            _handle_half = rect.height() / 2\n            _handle_size = _handle_half * 2\n            width = rect.width() - _handle_size\n            pos_x = ((width / _range) * _offset)\n            pos_y = rect.center().y() - _handle_half + 1\n        else:\n            _handle_half = rect.width() / 2\n            _handle_size = _handle_half * 2\n            height = rect.height() - _handle_size\n            pos_x = rect.center().x() - _handle_half + 1\n            pos_y = height - ((height / _range) * _offset)\n\n        handle_rect = QtCore.QRect(\n            pos_x, pos_y, _handle_size, _handle_size\n        )\n\n        self._handle_size = _handle_size\n        _offset = 2\n        _size = _handle_size - _offset\n        if horizontal:\n            if rect.height() > _size:\n                new_rect = QtCore.QRect(0, 0, rect.width(), _size)\n                center_point = QtCore.QPoint(\n                    rect.center().x(), handle_rect.center().y()\n                )\n                new_rect.moveCenter(center_point)\n                rect = new_rect\n\n            ratio = rect.height() / 2\n            fill_rect = QtCore.QRect(\n                rect.x(),\n                rect.y(),\n                handle_rect.right() - rect.x(),\n                rect.height()\n            )\n\n        else:\n            if rect.width() > _size:\n                new_rect = QtCore.QRect(0, 0, _size, rect.height())\n                center_point = QtCore.QPoint(\n                    handle_rect.center().x(), rect.center().y()\n                )\n                new_rect.moveCenter(center_point)\n                rect = new_rect\n\n            ratio = rect.width() / 2\n            fill_rect = QtCore.QRect(\n                rect.x(),\n                handle_rect.y(),\n                rect.width(),\n                rect.height() - handle_rect.y(),\n            )\n\n        painter.save()\n        painter.setPen(QtCore.Qt.NoPen)\n        painter.setBrush(self._bg_brush)\n        painter.drawRoundedRect(rect, ratio, ratio)\n\n        painter.setBrush(self._fill_brush)\n        painter.drawRoundedRect(fill_rect, ratio, ratio)\n\n        painter.setPen(QtCore.Qt.NoPen)\n        painter.setBrush(self._fill_brush)\n        painter.drawEllipse(handle_rect)\n        painter.restore()\n"
  },
  {
    "path": "poetry.toml",
    "content": "[virtualenvs]\nin-project = true\n\n[repositories.pype]\nurl = \"https://distribute.openpype.io/wheels/\"\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"OpenPype\"\nversion = \"3.18.11\" # OpenPype\ndescription = \"Open VFX and Animation pipeline with support.\"\nauthors = [\"OpenPype Team <info@openpype.io>\"]\nlicense = \"MIT License\"\nhomepage = \"https://openpype.io\"\ndocumentation = \"https://openpype.io/docs/artist_getting_started\"\nrepository = \"https://github.com/pypeclub/openpype\"\nreadme = \"README.md\"\nkeywords = [\"Pipeline\", \"Avalon\", \"VFX\", \"animation\", \"automation\", \"tracking\", \"asset management\"]\npackages = [\n    {include = \"igniter\"},\n    {include = \"repos\"},\n    {include = \"tools\"},\n    {include = \"tests\"},\n    {include = \"docs\"},\n    {include = \"openpype\"},\n    {include = \"start.py\"},\n    {include = \"LICENSE\"},\n    {include = \"README.md\"},\n    {include = \"setup.py\"},\n    {include = \"pyproject.toml\"},\n    {include = \"poetry.lock\"}\n]\n\n[tool.poetry.scripts]\nopenpype = 'start:boot'\n\n[tool.poetry.dependencies]\npython = \">=3.9.1,<3.10\"\naiohttp = \"^3.7\"\naiohttp_json_rpc = \"*\" # TVPaint server\nacre = { git = \"https://github.com/pypeclub/acre.git\" }\nappdirs = { git = \"https://github.com/ActiveState/appdirs.git\", branch = \"master\" }\nblessed = \"^1.17\" # openpype terminal formatting\ncoolname = \"*\"\nclique = \"1.6.*\"\nClick = \"7.1.2\"\ndnspython = \"^2.1.0\"\nftrack-python-api = \"^2.3.3\"\narrow = \"^0.17\"\nshotgun_api3 = {git = \"https://github.com/shotgunsoftware/python-api.git\", rev = \"v3.3.3\"}\ngazu = \"^0.9.3\"\ngoogle-api-python-client = \"^1.12.8\" # sync server google support (should be separate?)\njsonschema = \"^2.6.0\"\nkeyring = \"^22.0.1\"\nlog4mongo = \"^1.7\"\npathlib2= \"^2.3.5\" # deadline submit publish job only (single place, maybe not needed?)\nPillow = \"^9.0\" # used in TVPaint and for slates\npyblish-base = \"^1.8.11\"\npynput = \"^1.7.2\" # idle manager in tray\npymongo = \"^3.11.2\"\n\"Qt.py\" = \"^1.3.3\"\nQtPy = \"^2.3.0\"\nqtawesome = \"0.7.3\"\nspeedcopy = \"^2.1\"\nsix = \"^1.15\"\nurllib3 = \"1.26.16\"\nsemver = \"^2.13.0\" # for version resolution\nwsrpc_aiohttp = \"^3.1.1\" # websocket server\npywin32 = { version = \"301\", markers = \"sys_platform == 'win32'\" }\njinxed = [\n    { version = \"^1.0.1\", markers = \"sys_platform == 'darwin'\" },\n    { version = \"^1.0.1\", markers = \"sys_platform == 'linux'\" }\n]\npython3-xlib = { version=\"*\", markers = \"sys_platform == 'linux'\"}\nenlighten = \"^1.9.0\"\nslack-sdk = \"^3.6.0\"\nrequests = \"^2.25.1\"\npysftp = \"^0.2.9\"\ndropbox = \"^11.20.0\"\naiohttp-middlewares = \"^2.0.0\"\nUnidecode = \"1.2.0\"\ncryptography = \"39.0.0\"\n\n[tool.poetry.dev-dependencies]\nflake8 = \"^6.0\"\nautopep8 = \"^2.0\"\ncoverage = \"*\"\ncx_freeze = \"6.12.0\"\nGitPython = \"^3.1.17\"\njedi = \"^0.13\"\nJinja2 = \"^3\"\nmarkupsafe = \"2.0.1\"\npycodestyle = \"*\"\npydocstyle = \"*\"\nlinkify-it-py = \"^2.0.0\"\nmyst-parser = \"^0.18.1\"\npylint = \"^2.4.4\"\npytest = \"^6.1\"\npytest-cov = \"*\"\npytest-print = \"*\"\nSphinx = \"^5.3\"\nm2r2 = \"^0.3.3.post2\"\nsphinx-autoapi = \"^2.0.1\"\nsphinxcontrib-napoleon = \"^0.7\"\nrevitron-sphinx-theme = { git = \"https://github.com/revitron/revitron-sphinx-theme.git\", branch = \"master\" }\nrecommonmark = \"*\"\nwheel = \"*\"\nenlighten = \"*\"  # cool terminal progress bars\ntoml = \"^0.10.2\" # for parsing pyproject.toml\npre-commit = \"*\"\n\n[tool.poetry.urls]\n\"Bug Tracker\" = \"https://github.com/pypeclub/openpype/issues\"\n\"Discussions\" = \"https://github.com/pypeclub/openpype/discussions\"\n\n[[tool.poetry.source]]\nname = \"openpype\"\nurl = \"https://distribute.openpype.io/wheels/\"\nsecondary = true\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n[openpype]\n# note: in here we can use pip version specifiers as this is installed with pip until\n# Poetry will support custom location (-t flag for pip)\n# https://pip.pypa.io/en/stable/cli/pip_install/#requirement-specifiers\n[openpype.qtbinding.windows]\npackage = \"PySide2\"\nversion = \"5.15.2\"\n\n[openpype.qtbinding.darwin]\npackage = \"PySide6\"\nversion = \"6.4.3\"\n\n[openpype.qtbinding.linux]\npackage = \"PySide2\"\nversion = \"5.15.2\"\n\n# Python dependencies that will be available only in runtime of\n#   OpenPype process - do not interfere with DCCs dependencies\n[openpype.runtime-deps]\nopencolorio = \"2.2.1\"\nopentimelineio = \"0.14.1\"\n\n# TODO: we will need to handle different linux flavours here and\n#       also different macos versions too.\n[openpype.thirdparty.ffmpeg.windows]\nurl = \"https://distribute.openpype.io/thirdparty/ffmpeg-4.4-windows.zip\"\nhash = \"dd51ba29d64ee238e7c4c3c7301b19754c3f0ee2e2a729c20a0e2789e72db925\"\n\n[openpype.thirdparty.ffmpeg.linux]\nurl = \"https://distribute.openpype.io/thirdparty/ffmpeg-4.4-linux.tgz\"\nhash = \"10b9beda57cfbb69b9ed0ce896c0c8d99227b26ca8b9f611040c4752e365cbe9\"\n\n[openpype.thirdparty.ffmpeg.darwin]\nurl = \"https://distribute.openpype.io/thirdparty/ffmpeg-4.4-macos.tgz\"\nhash = \"95f43568338c275f80dc0cab1e1836a2e2270f856f0e7b204440d881dd74fbdb\"\n\n[openpype.thirdparty.oiio.windows]\nurl = \"https://distribute.openpype.io/thirdparty/oiio_tools-2.3.10-windows.zip\"\nhash = \"b9950f5d2fa3720b52b8be55bacf5f56d33f9e029d38ee86534995f3d8d253d2\"\n\n[openpype.thirdparty.oiio.linux]\nurl = \"https://distribute.openpype.io/thirdparty/oiio_tools-2.2.20-linux-centos7.tgz\"\nhash = \"3894dec7e4e521463891a869586850e8605f5fd604858b674c87323bf33e273d\"\n\n[openpype.thirdparty.ocioconfig]\nurl = \"https://distribute.openpype.io/thirdparty/OpenColorIO-Configs-1.0.2.zip\"\nhash = \"4ac17c1f7de83465e6f51dd352d7117e07e765b66d00443257916c828e35b6ce\"\n\n[tool.pyright]\ninclude = [\n    \"igniter\",\n    \"openpype\",\n    \"repos\",\n    \"vendor\"\n]\nexclude = [\n    \"**/node_modules\",\n    \"**/__pycache__\"\n]\nignore = [\"website\", \"docs\", \".git\"]\n\nreportMissingImports = true\nreportMissingTypeStubs = false\n\n[tool.poetry.extras]\ndocs = [\"Sphinx\", \"furo\", \"sphinxcontrib-napoleon\"]\n\n[tool.pydocstyle]\ninherit = false\nconvetion = \"google\"\nmatch = \"(?!test_).*\\\\.py\"\n"
  },
  {
    "path": "server_addon/README.md",
    "content": "# Addons for AYON server\nPreparation of AYON addons based on OpenPype codebase. The output is a bunch of zip files in `./packages` directory that can be uploaded to AYON server. One of the packages is `openpype` which is OpenPype code converted to AYON addon. The addon is must have requirement to be able to use `ayon-launcher`. The versioning of `openpype` addon is following versioning of OpenPype. The other addons contain only settings models.\n\n## Intro\nOpenPype is transitioning to AYON, a dedicated server with its own database, moving away from MongoDB. During this transition period, OpenPype will remain compatible with both MongoDB and AYON. However, we will gradually update the codebase to align with AYON's data structure and separate individual components into addons.\n\nCurrently, OpenPype has an AYON mode, which means it utilizes the AYON server instead of MongoDB through conversion utilities. Initially, we added the AYON executable alongside the OpenPype executables to enable AYON mode. While this approach worked, updating to new code versions would require a complete reinstallation. To address this, we have decided to create a new repository specifically for the base desktop application logic, which we currently refer to as the AYON Launcher. This Launcher will replace the executables generated by the OpenPype build and convert the OpenPype code into a server addon, resulting in smaller updates.\n\nSince the implementation of the AYON Launcher is not yet fully completed, we will maintain both methods of starting AYON mode for now. Once the AYON Launcher is finished, we will remove the AYON executables from the OpenPype codebase entirely.\n\nDuring this transitional period, the AYON Launcher addon will be a requirement as the entry point for using the AYON Launcher.\n\n## How to start\nThere is a `create_ayon_addons.py` python file which contains logic how to create server addon from OpenPype codebase. Just run the code.\n```shell\n./.poetry/bin/poetry run python ./server_addon/create_ayon_addons.py\n```\n\nIt will create directory `./packages/<addon name>.zip` files for AYON server. You can then copy upload the zip files to AYON server. Restart server to update addons information, add the addon version to server bundle and set the bundle for production or staging usage.\n\nOnce addon is on server and is enabled, you can just run AYON launcher. Content will be downloaded and used automatically.\n\n### Additional arguments\nAdditional arguments are useful for development purposes.\n\nTo skip zip creation to keep only server ready folder structure, pass `--skip-zip` argument.\n```shell\n./.poetry/bin/poetry run python ./server_addon/create_ayon_addons.py --skip-zip\n```\n\nTo create both zips and keep folder structure, pass `--keep-sources` argument.\n```shell\n./.poetry/bin/poetry run python ./server_addon/create_ayon_addons.py --keep-sources\n```\n"
  },
  {
    "path": "server_addon/aftereffects/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "server_addon/aftereffects/README.md",
    "content": "AfterEffects Addon\n===============\n\nIntegration with Adobe AfterEffects.\n"
  },
  {
    "path": "server_addon/aftereffects/server/__init__.py",
    "content": "from ayon_server.addons import BaseServerAddon\n\nfrom .settings import AfterEffectsSettings, DEFAULT_AFTEREFFECTS_SETTING\nfrom .version import __version__\n\n\nclass AfterEffects(BaseServerAddon):\n    name = \"aftereffects\"\n    title = \"AfterEffects\"\n    version = __version__\n\n    settings_model = AfterEffectsSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_AFTEREFFECTS_SETTING)\n"
  },
  {
    "path": "server_addon/aftereffects/server/settings/__init__.py",
    "content": "from .main import (\n    AfterEffectsSettings,\n    DEFAULT_AFTEREFFECTS_SETTING,\n)\n\n\n__all__ = (\n    \"AfterEffectsSettings\",\n    \"DEFAULT_AFTEREFFECTS_SETTING\",\n)\n"
  },
  {
    "path": "server_addon/aftereffects/server/settings/creator_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass CreateRenderPlugin(BaseSettingsModel):\n    mark_for_review: bool = SettingsField(True, title=\"Review\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Default Variants\"\n    )\n    force_setting_values: bool = SettingsField(\n        True, title=\"Force resolution and duration values from Asset\")\n\n\nclass AfterEffectsCreatorPlugins(BaseSettingsModel):\n    RenderCreator: CreateRenderPlugin = SettingsField(\n        title=\"Create Render\",\n        default_factory=CreateRenderPlugin,\n    )\n"
  },
  {
    "path": "server_addon/aftereffects/server/settings/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass AfterEffectsImageIOModel(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n"
  },
  {
    "path": "server_addon/aftereffects/server/settings/main.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\nfrom .imageio import AfterEffectsImageIOModel\nfrom .creator_plugins import AfterEffectsCreatorPlugins\nfrom .publish_plugins import (\n    AfterEffectsPublishPlugins,\n    AE_PUBLISH_PLUGINS_DEFAULTS,\n)\nfrom .workfile_builder import WorkfileBuilderPlugin\nfrom .templated_workfile_build import TemplatedWorkfileBuildModel\n\n\nclass AfterEffectsSettings(BaseSettingsModel):\n    \"\"\"AfterEffects Project Settings.\"\"\"\n\n    imageio: AfterEffectsImageIOModel = SettingsField(\n        default_factory=AfterEffectsImageIOModel,\n        title=\"OCIO config\"\n    )\n    create: AfterEffectsCreatorPlugins = SettingsField(\n        default_factory=AfterEffectsCreatorPlugins,\n        title=\"Creator plugins\"\n    )\n    publish: AfterEffectsPublishPlugins = SettingsField(\n        default_factory=AfterEffectsPublishPlugins,\n        title=\"Publish plugins\"\n    )\n    workfile_builder: WorkfileBuilderPlugin = SettingsField(\n        default_factory=WorkfileBuilderPlugin,\n        title=\"Workfile Builder\"\n    )\n    templated_workfile_build: TemplatedWorkfileBuildModel = SettingsField(\n        default_factory=TemplatedWorkfileBuildModel,\n        title=\"Templated Workfile Build Settings\"\n    )\n\n\nDEFAULT_AFTEREFFECTS_SETTING = {\n    \"create\": {\n        \"RenderCreator\": {\n            \"mark_for_review\": True,\n            \"default_variants\": [\n                \"Main\"\n            ]\n        }\n    },\n    \"publish\": AE_PUBLISH_PLUGINS_DEFAULTS,\n    \"workfile_builder\": {\n        \"create_first_version\": False,\n        \"custom_templates\": []\n    },\n    \"templated_workfile_build\": {\n        \"profiles\": []\n    },\n}\n"
  },
  {
    "path": "server_addon/aftereffects/server/settings/publish_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass CollectReviewPluginModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n\n\nclass ValidateSceneSettingsModel(BaseSettingsModel):\n    \"\"\"Validate naming of products and layers\"\"\"\n\n    # _isGroup = True\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    optional: bool = SettingsField(False, title=\"Optional\")\n    active: bool = SettingsField(True, title=\"Active\")\n    skip_resolution_check: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Skip Resolution Check for Tasks\",\n    )\n    skip_timelines_check: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Skip Timeline Check for Tasks\",\n    )\n\n\nclass ValidateContainersModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    optional: bool = SettingsField(True, title=\"Optional\")\n    active: bool = SettingsField(True, title=\"Active\")\n\n\nclass AfterEffectsPublishPlugins(BaseSettingsModel):\n    CollectReview: CollectReviewPluginModel = SettingsField(\n        default_factory=CollectReviewPluginModel,\n        title=\"Collect Review\",\n    )\n    ValidateSceneSettings: ValidateSceneSettingsModel = SettingsField(\n        default_factory=ValidateSceneSettingsModel,\n        title=\"Validate Scene Settings\",\n    )\n    ValidateContainers: ValidateContainersModel = SettingsField(\n        default_factory=ValidateContainersModel,\n        title=\"Validate Containers\",\n    )\n\n\nAE_PUBLISH_PLUGINS_DEFAULTS = {\n    \"CollectReview\": {\n        \"enabled\": True\n    },\n    \"ValidateSceneSettings\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True,\n        \"skip_resolution_check\": [\n            \".*\"\n        ],\n        \"skip_timelines_check\": [\n            \".*\"\n        ]\n    },\n    \"ValidateContainers\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True,\n    }\n}\n"
  },
  {
    "path": "server_addon/aftereffects/server/settings/templated_workfile_build.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    task_types_enum,\n    SettingsField,\n)\n\n\nclass TemplatedWorkfileProfileModel(BaseSettingsModel):\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    task_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task names\"\n    )\n    path: str = SettingsField(\n        title=\"Path to template\"\n    )\n    keep_placeholder: bool = SettingsField(\n        False,\n        title=\"Keep placeholders\")\n    create_first_version: bool = SettingsField(\n        True,\n        title=\"Create first version\"\n    )\n\n\nclass TemplatedWorkfileBuildModel(BaseSettingsModel):\n    profiles: list[TemplatedWorkfileProfileModel] = SettingsField(\n        default_factory=list\n    )\n"
  },
  {
    "path": "server_addon/aftereffects/server/settings/workfile_builder.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    MultiplatformPathModel,\n)\n\n\nclass CustomBuilderTemplate(BaseSettingsModel):\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n    )\n    template_path: MultiplatformPathModel = SettingsField(\n        default_factory=MultiplatformPathModel\n    )\n\n\nclass WorkfileBuilderPlugin(BaseSettingsModel):\n    _title = \"Workfile Builder\"\n    create_first_version: bool = SettingsField(\n        False,\n        title=\"Create first workfile\"\n    )\n\n    custom_templates: list[CustomBuilderTemplate] = SettingsField(\n        default_factory=list\n    )\n"
  },
  {
    "path": "server_addon/aftereffects/server/version.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Package declaring addon version.\"\"\"\n__version__ = \"0.1.3\"\n"
  },
  {
    "path": "server_addon/applications/server/__init__.py",
    "content": "import os\nimport json\nimport copy\n\nfrom ayon_server.addons import BaseServerAddon, AddonLibrary\nfrom ayon_server.lib.postgres import Postgres\n\nfrom .version import __version__\nfrom .settings import ApplicationsAddonSettings, DEFAULT_VALUES\n\ntry:\n    import semver\nexcept ImportError:\n    semver = None\n\n\ndef sort_versions(addon_versions, reverse=False):\n    if semver is None:\n        for addon_version in sorted(addon_versions, reverse=reverse):\n            yield addon_version\n        return\n\n    version_objs = []\n    invalid_versions = []\n    for addon_version in addon_versions:\n        try:\n            version_objs.append(\n                (addon_version, semver.VersionInfo.parse(addon_version))\n            )\n        except ValueError:\n            invalid_versions.append(addon_version)\n\n    valid_versions = [\n        addon_version\n        for addon_version, _ in sorted(version_objs, key=lambda x: x[1])\n    ]\n    sorted_versions = list(sorted(invalid_versions)) + valid_versions\n    if reverse:\n        sorted_versions = reversed(sorted_versions)\n    for addon_version in sorted_versions:\n        yield addon_version\n\n\ndef merge_groups(output, new_groups):\n    groups_by_name = {\n        o_group[\"name\"]: o_group\n        for o_group in output\n    }\n    extend_groups = []\n    for new_group in new_groups:\n        group_name = new_group[\"name\"]\n        if group_name not in groups_by_name:\n            extend_groups.append(new_group)\n            continue\n        existing_group = groups_by_name[group_name]\n        existing_variants = existing_group[\"variants\"]\n        existing_variants_by_name = {\n            variant[\"name\"]: variant\n            for variant in existing_variants\n        }\n        for new_variant in new_group[\"variants\"]:\n            if new_variant[\"name\"] not in existing_variants_by_name:\n                existing_variants.append(new_variant)\n\n    output.extend(extend_groups)\n\n\ndef get_enum_items_from_groups(groups):\n    label_by_name = {}\n    for group in groups:\n        group_name = group[\"name\"]\n        group_label = group[\"label\"] or group_name\n        for variant in group[\"variants\"]:\n            variant_name = variant[\"name\"]\n            if not variant_name:\n                continue\n            variant_label = variant[\"label\"] or variant_name\n            full_name = f\"{group_name}/{variant_name}\"\n            full_label = f\"{group_label} {variant_label}\"\n            label_by_name[full_name] = full_label\n\n    return [\n        {\"value\": full_name, \"label\": label_by_name[full_name]}\n        for full_name in sorted(label_by_name)\n    ]\n\n\nclass ApplicationsAddon(BaseServerAddon):\n    name = \"applications\"\n    title = \"Applications\"\n    version = __version__\n    settings_model = ApplicationsAddonSettings\n\n    async def get_default_settings(self):\n        applications_path = os.path.join(self.addon_dir, \"applications.json\")\n        tools_path = os.path.join(self.addon_dir, \"tools.json\")\n        default_values = copy.deepcopy(DEFAULT_VALUES)\n        with open(applications_path, \"r\") as stream:\n            default_values.update(json.load(stream))\n\n        with open(tools_path, \"r\") as stream:\n            default_values.update(json.load(stream))\n\n        return self.get_settings_model()(**default_values)\n\n    async def pre_setup(self):\n        \"\"\"Make sure older version of addon use the new way of attributes.\"\"\"\n\n        instance = AddonLibrary.getinstance()\n        app_defs = instance.data.get(self.name)\n        old_addon = app_defs.versions.get(\"0.1.0\")\n        if old_addon is not None:\n            # Override 'create_applications_attribute' for older versions\n            #   - avoid infinite server restart loop\n            old_addon.create_applications_attribute = (\n                self.create_applications_attribute\n            )\n\n    async def setup(self):\n        need_restart = await self.create_applications_attribute()\n        if need_restart:\n            self.request_server_restart()\n\n    async def create_applications_attribute(self) -> bool:\n        \"\"\"Make sure there are required attributes which ftrack addon needs.\n\n        Returns:\n            bool: 'True' if an attribute was created or updated.\n        \"\"\"\n\n        instance = AddonLibrary.getinstance()\n        app_defs = instance.data.get(self.name)\n        all_applications = []\n        all_tools = []\n        for addon_version in sort_versions(\n            app_defs.versions.keys(), reverse=True\n        ):\n            addon = app_defs.versions[addon_version]\n            for variant in (\"production\", \"staging\"):\n                settings_model = await addon.get_studio_settings(variant)\n                studio_settings = settings_model.dict()\n                application_settings = studio_settings[\"applications\"]\n                app_groups = application_settings.pop(\"additional_apps\")\n                for group_name, value in application_settings.items():\n                    value[\"name\"] = group_name\n                    app_groups.append(value)\n                merge_groups(all_applications, app_groups)\n                merge_groups(all_tools, studio_settings[\"tool_groups\"])\n\n        query = \"SELECT name, position, scope, data from public.attributes\"\n\n        apps_attrib_name = \"applications\"\n        tools_attrib_name = \"tools\"\n\n        apps_enum = get_enum_items_from_groups(all_applications)\n        tools_enum = get_enum_items_from_groups(all_tools)\n        apps_attribute_data = {\n            \"type\": \"list_of_strings\",\n            \"title\": \"Applications\",\n            \"enum\": apps_enum\n        }\n        tools_attribute_data = {\n            \"type\": \"list_of_strings\",\n            \"title\": \"Tools\",\n            \"enum\": tools_enum\n        }\n        apps_scope = [\"project\"]\n        tools_scope = [\"project\", \"folder\", \"task\"]\n\n        apps_match_position = None\n        apps_matches = False\n        tools_match_position = None\n        tools_matches = False\n        position = 1\n        async for row in Postgres.iterate(query):\n            position += 1\n            if row[\"name\"] == apps_attrib_name:\n                # Check if scope is matching ftrack addon requirements\n                if (\n                    set(row[\"scope\"]) == set(apps_scope)\n                    and row[\"data\"].get(\"enum\") == apps_enum\n                ):\n                    apps_matches = True\n                apps_match_position = row[\"position\"]\n\n            elif row[\"name\"] == tools_attrib_name:\n                if (\n                    set(row[\"scope\"]) == set(tools_scope)\n                    and row[\"data\"].get(\"enum\") == tools_enum\n                ):\n                    tools_matches = True\n                tools_match_position = row[\"position\"]\n\n        if apps_matches and tools_matches:\n            return False\n\n        postgre_query = \"\\n\".join((\n            \"INSERT INTO public.attributes\",\n            \"    (name, position, scope, data)\",\n            \"VALUES\",\n            \"    ($1, $2, $3, $4)\",\n            \"ON CONFLICT (name)\",\n            \"DO UPDATE SET\",\n            \"    scope = $3,\",\n            \"    data = $4\",\n        ))\n        if not apps_matches:\n            # Reuse position from found attribute\n            if apps_match_position is None:\n                apps_match_position = position\n                position += 1\n\n            await Postgres.execute(\n                postgre_query,\n                apps_attrib_name,\n                apps_match_position,\n                apps_scope,\n                apps_attribute_data,\n            )\n\n        if not tools_matches:\n            if tools_match_position is None:\n                tools_match_position = position\n                position += 1\n\n            await Postgres.execute(\n                postgre_query,\n                tools_attrib_name,\n                tools_match_position,\n                tools_scope,\n                tools_attribute_data,\n            )\n        return True\n"
  },
  {
    "path": "server_addon/applications/server/applications.json",
    "content": "{\n    \"applications\": {\n        \"maya\": {\n            \"enabled\": true,\n            \"label\": \"Maya\",\n            \"icon\": \"{}/app_icons/maya.png\",\n            \"host_name\": \"maya\",\n            \"environment\": \"{\\n  \\\"MAYA_DISABLE_CLIC_IPM\\\": \\\"Yes\\\",\\n  \\\"MAYA_DISABLE_CIP\\\": \\\"Yes\\\",\\n  \\\"MAYA_DISABLE_CER\\\": \\\"Yes\\\",\\n  \\\"PYMEL_SKIP_MEL_INIT\\\": \\\"Yes\\\",\\n  \\\"LC_ALL\\\": \\\"C\\\"\\n}\\n\",\n            \"variants\": [\n                {\n                    \"name\": \"2024\",\n                    \"label\": \"2024\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Autodesk\\\\Maya2024\\\\bin\\\\maya.exe\"\n                        ],\n                        \"darwin\": [\"/Applications/Autodesk/maya2024/Maya.app\"],\n                        \"linux\": [\n                            \"/usr/autodesk/maya2024/bin/maya\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n  \\\"MAYA_VERSION\\\": \\\"2024\\\"\\n}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"2023\",\n                    \"label\": \"2023\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Autodesk\\\\Maya2023\\\\bin\\\\maya.exe\"\n                        ],\n                        \"darwin\": [\"/Applications/Autodesk/maya2023/Maya.app\"],\n                        \"linux\": [\n                            \"/usr/autodesk/maya2023/bin/maya\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n  \\\"MAYA_VERSION\\\": \\\"2023\\\"\\n}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"2022\",\n                    \"label\": \"2022\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Autodesk\\\\Maya2022\\\\bin\\\\maya.exe\"\n                        ],\n                        \"darwin\": [\"/Applications/Autodesk/maya2022/Maya.app\"],\n                        \"linux\": [\n                            \"/usr/autodesk/maya2022/bin/maya\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n  \\\"MAYA_VERSION\\\": \\\"2022\\\"\\n}\",\n                    \"use_python_2\": true\n                }\n            ]\n        },\n        \"mayapy\": {\n            \"enabled\": true,\n            \"label\": \"Maya\",\n            \"icon\": \"{}/app_icons/maya.png\",\n            \"host_name\": \"maya\",\n            \"environment\": \"{\\n  \\\"MAYA_DISABLE_CLIC_IPM\\\": \\\"Yes\\\",\\n  \\\"MAYA_DISABLE_CIP\\\": \\\"Yes\\\",\\n  \\\"MAYA_DISABLE_CER\\\": \\\"Yes\\\",\\n  \\\"PYMEL_SKIP_MEL_INIT\\\": \\\"Yes\\\",\\n  \\\"LC_ALL\\\": \\\"C\\\"\\n}\\n\",\n            \"variants\": [\n                {\n                    \"name\": \"2024\",\n                    \"label\": \"2024\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Autodesk\\\\Maya2024\\\\bin\\\\mayapy.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": [\n                            \"/usr/autodesk/maya2024/bin/mayapy\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n  \\\"MAYA_VERSION\\\": \\\"2024\\\"\\n}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"2023\",\n                    \"label\": \"2023\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Autodesk\\\\Maya2023\\\\bin\\\\mayapy.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": [\n                            \"/usr/autodesk/maya2023/bin/mayapy\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n  \\\"MAYA_VERSION\\\": \\\"2023\\\"\\n}\",\n                    \"use_python_2\": false\n                }\n            ]\n        },\n        \"adsk_3dsmax\": {\n            \"enabled\": true,\n            \"label\": \"3ds Max\",\n            \"icon\": \"{}/app_icons/3dsmax.png\",\n            \"host_name\": \"max\",\n            \"environment\": \"{\\n    \\\"ADSK_3DSMAX_STARTUPSCRIPTS_ADDON_DIR\\\": \\\"{OPENPYPE_ROOT}/openpype/hosts/max/startup\\\"\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"2024\",\n                    \"use_python_2\": false,\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Autodesk\\\\3ds Max 2024\\\\3dsmax.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n    \\\"3DSMAX_VERSION\\\": \\\"2024\\\"\\n}\"\n                },\n                {\n                    \"name\": \"2023\",\n                    \"use_python_2\": false,\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Autodesk\\\\3ds Max 2023\\\\3dsmax.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n    \\\"3DSMAX_VERSION\\\": \\\"2023\\\"\\n}\"\n                }\n            ]\n        },\n        \"flame\": {\n            \"enabled\": true,\n            \"label\": \"Flame\",\n            \"icon\": \"{}/app_icons/flame.png\",\n            \"host_name\": \"flame\",\n            \"environment\": \"{\\n    \\\"FLAME_SCRIPT_DIRS\\\": {\\n        \\\"windows\\\": \\\"\\\",\\n        \\\"darwin\\\": \\\"\\\",\\n        \\\"linux\\\": \\\"\\\"\\n    },\\n    \\\"FLAME_WIRETAP_HOSTNAME\\\": \\\"\\\",\\n    \\\"FLAME_WIRETAP_VOLUME\\\": \\\"stonefs\\\",\\n    \\\"FLAME_WIRETAP_GROUP\\\": \\\"staff\\\"\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"2021\",\n                    \"label\": \"2021\",\n                    \"executables\": {\n                        \"windows\": [],\n                        \"darwin\": [\n                            \"/opt/Autodesk/flame_2021/bin/flame.app/Contents/MacOS/startApp\"\n                        ],\n                        \"linux\": [\n                            \"/opt/Autodesk/flame_2021/bin/startApplication\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n    \\\"OPENPYPE_FLAME_PYTHON_EXEC\\\": \\\"/opt/Autodesk/python/2021/bin/python2.7\\\",\\n    \\\"OPENPYPE_FLAME_PYTHONPATH\\\": \\\"/opt/Autodesk/flame_2021/python\\\",\\n    \\\"OPENPYPE_WIRETAP_TOOLS\\\": \\\"/opt/Autodesk/wiretap/tools/2021\\\"\\n}\",\n                    \"use_python_2\": true\n                },\n                {\n                    \"name\": \"2021_1\",\n                    \"label\": \"2021.1\",\n                    \"executables\": {\n                        \"windows\": [],\n                        \"darwin\": [\n                            \"/opt/Autodesk/flame_2021.1/bin/flame.app/Contents/MacOS/startApp\"\n                        ],\n                        \"linux\": [\n                            \"/opt/Autodesk/flame_2021.1/bin/startApplication\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n  \\\"OPENPYPE_FLAME_PYTHON_EXEC\\\": \\\"/opt/Autodesk/python/2021.1/bin/python2.7\\\",\\n  \\\"OPENPYPE_FLAME_PYTHONPATH\\\": \\\"/opt/Autodesk/flame_2021.1/python\\\",\\n  \\\"OPENPYPE_WIRETAP_TOOLS\\\": \\\"/opt/Autodesk/wiretap/tools/2021.1\\\"\\n}\",\n                    \"use_python_2\": true\n                }\n            ]\n        },\n        \"nuke\": {\n            \"enabled\": true,\n            \"label\": \"Nuke\",\n            \"icon\": \"{}/app_icons/nuke.png\",\n            \"host_name\": \"nuke\",\n            \"environment\": \"{\\n    \\\"NUKE_PATH\\\": [\\n        \\\"{NUKE_PATH}\\\",\\n        \\\"{OPENPYPE_STUDIO_PLUGINS}/nuke\\\"\\n    ]\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"15-0\",\n                    \"label\": \"15.0\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke15.0v2\\\\Nuke15.0.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke15.0v2/Nuke15.0v2.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke5.0v2/Nuke15.0\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"14-0\",\n                    \"label\": \"14.0\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke14.0v5\\\\Nuke14.0.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke14.0v5/Nuke14.0v5.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke14.0v5/Nuke14.0\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"13-2\",\n                    \"label\": \"13.2\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke13.2v5\\\\Nuke13.2.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke13.2v5/Nuke13.2v5.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke13.2v5/Nuke13.2\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                }\n            ]\n        },\n        \"nukeassist\": {\n            \"enabled\": true,\n            \"label\": \"Nuke Assist\",\n            \"icon\": \"{}/app_icons/nuke.png\",\n            \"host_name\": \"nuke\",\n            \"environment\": \"{\\n    \\\"NUKE_PATH\\\": [\\n        \\\"{NUKE_PATH}\\\",\\n        \\\"{OPENPYPE_STUDIO_PLUGINS}/nuke\\\"\\n    ]\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"15-0\",\n                    \"label\": \"15.0\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke15.0v2\\\\Nuke15.0.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke15.0v2/NukeAssist15.0v2.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke5.0v2/Nuke15.0\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [\"--nukeassist\"],\n                        \"darwin\": [],\n                        \"linux\": [\"--nukeassist\"]\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"14-0\",\n                    \"label\": \"14.0\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke14.0v5\\\\Nuke14.0.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke14.0v5/NukeAssist14.0v5.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke14.0v5/Nuke14.0\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [\"--nukeassist\"],\n                        \"darwin\": [],\n                        \"linux\": [\"--nukeassist\"]\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"13-2\",\n                    \"label\": \"13.2\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke13.2v5\\\\Nuke13.2.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke13.2v5/NukeAssist13.2v5.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke13.2v5/Nuke13.2\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [\"--nukeassist\"],\n                        \"darwin\": [],\n                        \"linux\": [\"--nukeassist\"]\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                }\n            ]\n        },\n        \"nukex\": {\n            \"enabled\": true,\n            \"label\": \"Nuke X\",\n            \"icon\": \"{}/app_icons/nukex.png\",\n            \"host_name\": \"nuke\",\n            \"environment\": \"{\\n    \\\"NUKE_PATH\\\": [\\n        \\\"{NUKE_PATH}\\\",\\n        \\\"{OPENPYPE_STUDIO_PLUGINS}/nuke\\\"\\n    ]\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"15-0\",\n                    \"label\": \"15.0\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke15.0v2\\\\Nuke15.0.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke15.0v2/NukeX15.0v2.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke5.0v2/Nuke15.0\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [\"--nukex\"],\n                        \"darwin\": [],\n                        \"linux\": [\"--nukex\"]\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"14-0\",\n                    \"label\": \"14.0\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke14.0v5\\\\Nuke14.0.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke14.0v5/NukeX14.0v5.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke14.0v5/Nuke14.0\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [\"--nukex\"],\n                        \"darwin\": [],\n                        \"linux\": [\"--nukex\"]\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"13-2\",\n                    \"label\": \"13.2\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke13.2v5\\\\Nuke13.2.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke13.2v5/NukeX13.2v5.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke13.2v5/Nuke13.2\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [\"--nukex\"],\n                        \"darwin\": [],\n                        \"linux\": [\"--nukex\"]\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                }\n            ]\n        },\n        \"nukestudio\": {\n            \"enabled\": true,\n            \"label\": \"Nuke Studio\",\n            \"icon\": \"{}/app_icons/nukestudio.png\",\n            \"host_name\": \"hiero\",\n            \"environment\": \"{\\n    \\\"WORKFILES_STARTUP\\\": \\\"0\\\",\\n    \\\"TAG_ASSETBUILD_STARTUP\\\": \\\"0\\\"\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"15-0\",\n                    \"label\": \"15.0\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke15.0v2\\\\Nuke15.0.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke15.0v2/NukeStudio15.0v2.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke5.0v2/Nuke15.0\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [\"--studio\"],\n                        \"darwin\": [],\n                        \"linux\": [\"--studio\"]\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"14-0\",\n                    \"label\": \"14.0\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke14.0v5\\\\Nuke14.0.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke14.0v5/NukeStudio14.0v5.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke14.0v5/Nuke14.0\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [\"--studio\"],\n                        \"darwin\": [],\n                        \"linux\": [\"--studio\"]\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"13-2\",\n                    \"label\": \"13.2\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke13.2v5\\\\Nuke13.2.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke13.2v5/NukeStudio13.2v5.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke13.2v5/Nuke13.2\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [\"--studio\"],\n                        \"darwin\": [],\n                        \"linux\": [\"--studio\"]\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                }\n            ]\n        },\n        \"hiero\": {\n            \"enabled\": true,\n            \"label\": \"Hiero\",\n            \"icon\": \"{}/app_icons/hiero.png\",\n            \"host_name\": \"hiero\",\n            \"environment\": \"{\\n    \\\"WORKFILES_STARTUP\\\": \\\"0\\\",\\n    \\\"TAG_ASSETBUILD_STARTUP\\\": \\\"0\\\"\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"15-0\",\n                    \"label\": \"15.0\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke15.0v2\\\\Nuke15.0.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke15.0v2/Hiero15.0v2.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke5.0v2/Nuke15.0\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [\"--hiero\"],\n                        \"darwin\": [],\n                        \"linux\": [\"--hiero\"]\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"14-0\",\n                    \"label\": \"14.0\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke14.0v5\\\\Nuke14.0.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke14.0v5/Hiero14.0v5.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke14.0v5/Nuke14.0\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [\"--hiero\"],\n                        \"darwin\": [],\n                        \"linux\": [\"--hiero\"]\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                },\n                {\n                    \"name\": \"13-2\",\n                    \"label\": \"13.2\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Nuke13.2v5\\\\Nuke13.2.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Nuke13.2v5/Hiero13.2v5.app\"\n                        ],\n                        \"linux\": [\n                            \"/usr/local/Nuke13.2v5/Nuke13.2\"\n                        ]\n                    },\n                    \"arguments\": {\n                        \"windows\": [\"--hiero\"],\n                        \"darwin\": [],\n                        \"linux\": [\"--hiero\"]\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": false\n                }\n            ]\n        },\n        \"fusion\": {\n            \"enabled\": true,\n            \"label\": \"Fusion\",\n            \"icon\": \"{}/app_icons/fusion.png\",\n            \"host_name\": \"fusion\",\n            \"environment\": \"{\\n    \\\"FUSION_PYTHON3_HOME\\\": {\\n        \\\"windows\\\": \\\"{LOCALAPPDATA}/Programs/Python/Python36\\\",\\n        \\\"darwin\\\": \\\"~/Library/Python/3.6/bin\\\",\\n        \\\"linux\\\": \\\"/opt/Python/3.6/bin\\\"\\n    }\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"18\",\n                    \"label\": \"18\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Blackmagic Design\\\\Fusion 18\\\\Fusion.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"17\",\n                    \"label\": \"17\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Blackmagic Design\\\\Fusion 17\\\\Fusion.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"16\",\n                    \"label\": \"16\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Blackmagic Design\\\\Fusion 16\\\\Fusion.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                }\n            ]\n        },\n        \"resolve\": {\n            \"enabled\": true,\n            \"label\": \"Resolve\",\n            \"icon\": \"{}/app_icons/resolve.png\",\n            \"host_name\": \"resolve\",\n            \"environment\": \"{\\n    \\\"RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR\\\": [],\\n    \\\"RESOLVE_PYTHON3_HOME\\\": {\\n        \\\"windows\\\": \\\"{LOCALAPPDATA}/Programs/Python/Python36\\\",\\n        \\\"darwin\\\": \\\"~/Library/Python/3.6/bin\\\",\\n        \\\"linux\\\": \\\"/opt/Python/3.6/bin\\\"\\n    }\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"stable\",\n                    \"label\": \"stable\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:/Program Files/Blackmagic Design/DaVinci Resolve/Resolve.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                }\n            ]\n        },\n        \"houdini\": {\n            \"enabled\": true,\n            \"label\": \"Houdini\",\n            \"icon\": \"{}/app_icons/houdini.png\",\n            \"host_name\": \"houdini\",\n            \"environment\": \"{}\",\n            \"variants\": [\n                {\n                    \"name\": \"19-5\",\n                    \"label\": \"19.5\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Side Effects Software\\\\Houdini 19.5.805\\\\bin\\\\houdini.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": true\n                },\n                {\n                    \"name\": \"19-0\",\n                    \"label\": \"19.0\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Side Effects Software\\\\Houdini 19.0.720\\\\bin\\\\houdini.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": true\n                },\n                {\n                    \"name\": \"18-5\",\n                    \"label\": \"18.5\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Side Effects Software\\\\Houdini 18.5.759\\\\bin\\\\houdini.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\",\n                    \"use_python_2\": true\n                }\n            ]\n        },\n        \"blender\": {\n            \"enabled\": true,\n            \"label\": \"Blender\",\n            \"icon\": \"{}/app_icons/blender.png\",\n            \"host_name\": \"blender\",\n            \"environment\": \"{}\",\n            \"variants\": [\n                {\n                    \"name\": \"3-6-5\",\n                    \"label\": \"3.6.5 LTS\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Blender Foundation\\\\Blender 3.6\\\\blender.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [\n                            \"--python-use-system-env\"\n                        ],\n                        \"darwin\": [\n                            \"--python-use-system-env\"\n                        ],\n                        \"linux\": [\n                            \"--python-use-system-env\"\n                        ]\n                    },\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"2-90\",\n                    \"label\": \"2.90\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Blender Foundation\\\\Blender 2.90\\\\blender.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                      },\n                    \"arguments\": {\n                        \"windows\": [\n                            \"--python-use-system-env\"\n                        ],\n                        \"darwin\": [\n                            \"--python-use-system-env\"\n                        ],\n                        \"linux\": [\n                            \"--python-use-system-env\"\n                        ]\n                    },\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"2-91\",\n                    \"label\": \"2.91\",\n                    \"executables\": {\n                      \"windows\": [\n                          \"C:\\\\Program Files\\\\Blender Foundation\\\\Blender 2.91\\\\blender.exe\"\n                      ],\n                      \"darwin\": [],\n                      \"linux\": []\n                    },\n                    \"arguments\": {\n                      \"windows\": [\n                          \"--python-use-system-env\"\n                      ],\n                      \"darwin\": [\n                          \"--python-use-system-env\"\n                      ],\n                      \"linux\": [\n                          \"--python-use-system-env\"\n                      ]\n                    },\n                    \"environment\": \"{}\"\n                }\n            ]\n        },\n        \"harmony\": {\n            \"enabled\": true,\n            \"label\": \"Harmony\",\n            \"icon\": \"{}/app_icons/harmony.png\",\n            \"host_name\": \"harmony\",\n            \"environment\": \"{\\n    \\\"AVALON_HARMONY_WORKFILES_ON_LAUNCH\\\": \\\"1\\\"\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"22\",\n                    \"label\": \"22\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"c:\\\\Program Files (x86)\\\\Toon Boom Animation\\\\Toon Boom Harmony 22 Premium\\\\win64\\\\bin\\\\HarmonyPremium.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Toon Boom Harmony 22 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium\"\n                        ],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"21\",\n                    \"label\": \"21\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"c:\\\\Program Files (x86)\\\\Toon Boom Animation\\\\Toon Boom Harmony 21 Premium\\\\win64\\\\bin\\\\HarmonyPremium.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Toon Boom Harmony 21 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium\"\n                        ],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"20\",\n                    \"label\": \"20\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"c:\\\\Program Files (x86)\\\\Toon Boom Animation\\\\Toon Boom Harmony 20 Premium\\\\win64\\\\bin\\\\HarmonyPremium.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Toon Boom Harmony 20 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium\"\n                        ],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"17\",\n                    \"label\": \"17\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"c:\\\\Program Files (x86)\\\\Toon Boom Animation\\\\Toon Boom Harmony 17 Premium\\\\win64\\\\bin\\\\HarmonyPremium.exe\"\n                        ],\n                        \"darwin\": [\n                            \"/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium\"\n                        ],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                }\n            ]\n        },\n        \"tvpaint\": {\n            \"enabled\": true,\n            \"label\": \"TVPaint\",\n            \"icon\": \"{}/app_icons/tvpaint.png\",\n            \"host_name\": \"tvpaint\",\n            \"environment\": \"{}\",\n            \"variants\": [\n                {\n                    \"name\": \"animation_11-64bits\",\n                    \"label\": \"11 (64bits)\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\TVPaint Developpement\\\\TVPaint Animation 11 (64bits)\\\\TVPaint Animation 11 (64bits).exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"animation_11-32bits\",\n                    \"label\": \"11 (32bits)\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files (x86)\\\\TVPaint Developpement\\\\TVPaint Animation 11 (32bits)\\\\TVPaint Animation 11 (32bits).exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                }\n            ]\n        },\n        \"photoshop\": {\n            \"enabled\": true,\n            \"label\": \"Photoshop\",\n            \"icon\": \"{}/app_icons/photoshop.png\",\n            \"host_name\": \"photoshop\",\n            \"environment\": \"{\\n    \\\"AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH\\\": \\\"1\\\",\\n    \\\"WORKFILES_SAVE_AS\\\": \\\"Yes\\\"\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"2022\",\n                    \"label\": \"2022\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Adobe\\\\Adobe Photoshop 2022\\\\Photoshop.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"2023\",\n                    \"label\": \"2023\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Adobe\\\\Adobe Photoshop 2023\\\\Photoshop.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"2024\",\n                    \"label\": \"2024\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Adobe\\\\Adobe Photoshop 2024\\\\Photoshop.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                }\n            ]\n        },\n        \"aftereffects\": {\n            \"enabled\": true,\n            \"label\": \"AfterEffects\",\n            \"icon\": \"{}/app_icons/aftereffects.png\",\n            \"host_name\": \"aftereffects\",\n            \"environment\": \"{\\n    \\\"AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH\\\": \\\"1\\\",\\n    \\\"WORKFILES_SAVE_AS\\\": \\\"Yes\\\"\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"2021\",\n                    \"label\": \"2021\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Adobe\\\\Adobe After Effects 2021\\\\Support Files\\\\AfterFX.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"2022\",\n                    \"label\": \"2022\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Adobe\\\\Adobe After Effects 2022\\\\Support Files\\\\AfterFX.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n  \\\"MULTIPROCESS\\\": \\\"No\\\"\\n}\"\n                },\n                {\n                    \"name\": \"2023\",\n                    \"label\": \"2023\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Adobe\\\\Adobe After Effects 2023\\\\Support Files\\\\AfterFX.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n  \\\"MULTIPROCESS\\\": \\\"No\\\"\\n}\"\n                },\n                {\n                    \"name\": \"2024\",\n                    \"label\": \"2024\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Adobe\\\\Adobe After Effects 2024\\\\Support Files\\\\AfterFX.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n  \\\"MULTIPROCESS\\\": \\\"No\\\"\\n}\"\n                }\n            ]\n        },\n        \"celaction\": {\n            \"enabled\": true,\n            \"label\": \"CelAction 2D\",\n            \"icon\": \"app_icons/celaction.png\",\n            \"host_name\": \"celaction\",\n            \"environment\": \"{\\n    \\\"CELACTION_TEMPLATE\\\": \\\"{OPENPYPE_REPOS_ROOT}/openpype/hosts/celaction/celaction_template_scene.scn\\\"\\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"local\",\n                    \"label\": \"local\",\n                    \"executables\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                }\n            ]\n        },\n        \"substancepainter\": {\n            \"enabled\": true,\n            \"label\": \"Substance Painter\",\n            \"icon\": \"{}/app_icons/substancepainter.png\",\n            \"host_name\": \"substancepainter\",\n            \"environment\": \"{}\",\n            \"variants\": [\n                {\n                    \"name\": \"stable\",\n                    \"label\": \"Stable\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Adobe\\\\Adobe Substance 3D Painter\\\\Adobe Substance 3D Painter.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                }\n            ]\n        },\n        \"unreal\": {\n            \"enabled\": false,\n            \"label\": \"Unreal Editor\",\n            \"icon\": \"{}/app_icons/ue4.png\",\n            \"host_name\": \"unreal\",\n            \"environment\": \"{}\",\n            \"variants\": [\n                {\n                    \"name\": \"5-0\",\n                    \"label\": \"5.0\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Epic Games\\\\UE_5.0\\\\Engine\\\\Binaries\\\\Win64\\\\UnrealEditor.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {},\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"5-1\",\n                    \"label\": \"5.1\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Epic Games\\\\UE_5.1\\\\Engine\\\\Binaries\\\\Win64\\\\UnrealEditor.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {},\n                    \"environment\": \"{}\"\n                },\n                {\n                    \"name\": \"5-2\",\n                    \"label\": \"5.2\",\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\Epic Games\\\\UE_5.2\\\\Engine\\\\Binaries\\\\Win64\\\\UnrealEditor.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {},\n                    \"environment\": \"{}\"\n                }\n            ]\n        },\n        \"djvview\": {\n            \"enabled\": true,\n            \"label\": \"DJV View\",\n            \"icon\": \"{}/app_icons/djvView.png\",\n            \"host_name\": \"\",\n            \"environment\": \"{}\",\n            \"variants\": [\n                {\n                    \"name\": \"1-1\",\n                    \"label\": \"1.1\",\n                    \"executables\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                }\n            ]\n        },\n        \"equalizer\": {\n            \"enabled\": true,\n            \"label\": \"3DEqualizer\",\n            \"icon\": \"{}/app_icons/3de4.png\",\n            \"host_name\": \"equalizer\",\n            \"environment\": \"{}\",\n            \"variants\": [\n                {\n                    \"name\": \"7-1v2\",\n                    \"label\": \"7.1v2\",\n                    \"use_python_2\": false,\n                    \"executables\": {\n                        \"windows\": [\n                            \"C:\\\\Program Files\\\\3DE4_win64_r7.1v2\\\\bin\\\\3DE4.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{}\"\n                }\n            ]\n        },\n        \"wrap\": {\n            \"enabled\": true,\n            \"label\": \"Wrap\",\n            \"icon\": \"{}/app_icons/wrap.png\",\n            \"host_name\": \"wrap\",\n            \"environment\": \"{\\n    \\n}\",\n            \"variants\": [\n                {\n                    \"name\": \"2023\",\n                    \"use_python_2\": false,\n                    \"executables\": {\n                        \"windows\": [\n                            \"c:\\\\Program Files\\\\Faceform\\\\Wrap 2023.10.2\\\\Wrap.exe\"\n                        ],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"arguments\": {\n                        \"windows\": [],\n                        \"darwin\": [],\n                        \"linux\": []\n                    },\n                    \"environment\": \"{\\n   \\n}\"\n                }\n            ]\n        },\n        \"additional_apps\": []\n    }\n}\n"
  },
  {
    "path": "server_addon/applications/server/settings.py",
    "content": "import json\nfrom pydantic import validator\n\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n)\nfrom ayon_server.exceptions import BadRequestException\n\n\ndef validate_json_dict(value):\n    if not value.strip():\n        return \"{}\"\n    try:\n        converted_value = json.loads(value)\n        success = isinstance(converted_value, dict)\n    except json.JSONDecodeError as exc:\n        print(exc)\n        success = False\n\n    if not success:\n        raise BadRequestException(\n            \"Environment's can't be parsed as json object\"\n        )\n    return value\n\n\nclass MultiplatformStrList(BaseSettingsModel):\n    windows: list[str] = SettingsField(default_factory=list, title=\"Windows\")\n    linux: list[str] = SettingsField(default_factory=list, title=\"Linux\")\n    darwin: list[str] = SettingsField(default_factory=list, title=\"MacOS\")\n\n\nclass AppVariant(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Name\")\n    label: str = SettingsField(\"\", title=\"Label\")\n    executables: MultiplatformStrList = SettingsField(\n        default_factory=MultiplatformStrList, title=\"Executables\"\n    )\n    arguments: MultiplatformStrList = SettingsField(\n        default_factory=MultiplatformStrList, title=\"Arguments\"\n    )\n    environment: str = SettingsField(\n        \"{}\", title=\"Environment\", widget=\"textarea\"\n    )\n\n    @validator(\"environment\")\n    def validate_json(cls, value):\n        return validate_json_dict(value)\n\n\nclass AppVariantWithPython(AppVariant):\n    use_python_2: bool = SettingsField(False, title=\"Use Python 2\")\n\n\nclass AppGroup(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    label: str = SettingsField(\"\", title=\"Label\")\n    host_name: str = SettingsField(\"\", title=\"Host name\")\n    icon: str = SettingsField(\"\", title=\"Icon\")\n    environment: str = SettingsField(\n        \"{}\", title=\"Environment\", widget=\"textarea\"\n    )\n\n    variants: list[AppVariant] = SettingsField(\n        default_factory=list,\n        title=\"Variants\",\n        description=\"Different variants of the applications\",\n        section=\"Variants\",\n    )\n\n    @validator(\"variants\")\n    def validate_unique_name(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass AppGroupWithPython(AppGroup):\n    variants: list[AppVariantWithPython] = SettingsField(\n        default_factory=list,\n        title=\"Variants\",\n        description=\"Different variants of the applications\",\n        section=\"Variants\",\n    )\n\n\nclass AdditionalAppGroup(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    name: str = SettingsField(\"\", title=\"Name\")\n    label: str = SettingsField(\"\", title=\"Label\")\n    host_name: str = SettingsField(\"\", title=\"Host name\")\n    icon: str = SettingsField(\"\", title=\"Icon\")\n    environment: str = SettingsField(\n        \"{}\", title=\"Environment\", widget=\"textarea\"\n    )\n\n    variants: list[AppVariantWithPython] = SettingsField(\n        default_factory=list,\n        title=\"Variants\",\n        description=\"Different variants of the applications\",\n        section=\"Variants\",\n    )\n\n    @validator(\"variants\")\n    def validate_unique_name(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ToolVariantModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Name\")\n    label: str = SettingsField(\"\", title=\"Label\")\n    host_names: list[str] = SettingsField(default_factory=list, title=\"Hosts\")\n    # TODO use applications enum if possible\n    app_variants: list[str] = SettingsField(\n        default_factory=list, title=\"Applications\"\n    )\n    environment: str = SettingsField(\n        \"{}\", title=\"Environments\", widget=\"textarea\"\n    )\n\n    @validator(\"environment\")\n    def validate_json(cls, value):\n        return validate_json_dict(value)\n\n\nclass ToolGroupModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Name\")\n    label: str = SettingsField(\"\", title=\"Label\")\n    environment: str = SettingsField(\n        \"{}\", title=\"Environments\", widget=\"textarea\"\n    )\n    variants: list[ToolVariantModel] = SettingsField(default_factory=list)\n\n    @validator(\"environment\")\n    def validate_json(cls, value):\n        return validate_json_dict(value)\n\n    @validator(\"variants\")\n    def validate_unique_name(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ApplicationsSettings(BaseSettingsModel):\n    \"\"\"Applications settings\"\"\"\n\n    maya: AppGroupWithPython = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Autodesk Maya\")\n    adsk_3dsmax: AppGroupWithPython = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Autodesk 3ds Max\")\n    flame: AppGroupWithPython = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Autodesk Flame\")\n    nuke: AppGroupWithPython = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Nuke\")\n    nukeassist: AppGroupWithPython = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Nuke Assist\")\n    nukex: AppGroupWithPython = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Nuke X\")\n    nukestudio: AppGroupWithPython = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Nuke Studio\")\n    hiero: AppGroupWithPython = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Hiero\")\n    fusion: AppGroup = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Fusion\")\n    resolve: AppGroupWithPython = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Resolve\")\n    houdini: AppGroupWithPython = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Houdini\")\n    blender: AppGroup = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Blender\")\n    harmony: AppGroup = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Harmony\")\n    tvpaint: AppGroup = SettingsField(\n        default_factory=AppGroupWithPython, title=\"TVPaint\")\n    photoshop: AppGroup = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Adobe Photoshop\")\n    aftereffects: AppGroup = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Adobe After Effects\")\n    celaction: AppGroup = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Celaction 2D\")\n    substancepainter: AppGroup = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Substance Painter\")\n    unreal: AppGroup = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Unreal Editor\")\n    wrap: AppGroup = SettingsField(\n        default_factory=AppGroupWithPython, title=\"Wrap\")\n    equalizer: AppGroup = SettingsField(\n        default_factory=AppGroupWithPython, title=\"3DEqualizer\")\n    additional_apps: list[AdditionalAppGroup] = SettingsField(\n        default_factory=list, title=\"Additional Applications\")\n\n    @validator(\"additional_apps\")\n    def validate_unique_name(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ApplicationsAddonSettings(BaseSettingsModel):\n    applications: ApplicationsSettings = SettingsField(\n        default_factory=ApplicationsSettings,\n        title=\"Applications\",\n        scope=[\"studio\"]\n    )\n    tool_groups: list[ToolGroupModel] = SettingsField(\n        default_factory=list,\n        scope=[\"studio\"]\n    )\n    only_available: bool = SettingsField(\n        True, title=\"Show only available applications\")\n\n    @validator(\"tool_groups\")\n    def validate_unique_name(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nDEFAULT_VALUES = {\n    \"only_available\": True\n}\n"
  },
  {
    "path": "server_addon/applications/server/tools.json",
    "content": "{\n    \"tool_groups\": [\n        {\n            \"environment\": \"{\\n    \\\"MTOA\\\": \\\"{STUDIO_SOFTWARE}/arnold/mtoa_{MAYA_VERSION}_{MTOA_VERSION}\\\",\\n    \\\"MAYA_RENDER_DESC_PATH\\\": \\\"{MTOA}\\\",\\n    \\\"MAYA_MODULE_PATH\\\": \\\"{MTOA}\\\",\\n    \\\"ARNOLD_PLUGIN_PATH\\\": \\\"{MTOA}/shaders\\\",\\n    \\\"MTOA_EXTENSIONS_PATH\\\": {\\n        \\\"darwin\\\": \\\"{MTOA}/extensions\\\",\\n        \\\"linux\\\": \\\"{MTOA}/extensions\\\",\\n        \\\"windows\\\": \\\"{MTOA}/extensions\\\"\\n    },\\n    \\\"MTOA_EXTENSIONS\\\": {\\n        \\\"darwin\\\": \\\"{MTOA}/extensions\\\",\\n        \\\"linux\\\": \\\"{MTOA}/extensions\\\",\\n        \\\"windows\\\": \\\"{MTOA}/extensions\\\"\\n    },\\n    \\\"DYLD_LIBRARY_PATH\\\": {\\n        \\\"darwin\\\": \\\"{MTOA}/bin\\\"\\n    },\\n    \\\"PATH\\\": {\\n        \\\"windows\\\": \\\"{PATH};{MTOA}/bin\\\"\\n    }\\n}\",\n            \"name\": \"mtoa\",\n            \"label\": \"Autodesk Arnold\",\n            \"variants\": [\n                {\n                    \"host_names\": [],\n                    \"app_variants\": [],\n                    \"environment\": \"{\\n    \\\"MTOA_VERSION\\\": \\\"3.2\\\"\\n}\",\n                    \"name\": \"3-2\",\n                    \"label\": \"3.2\"\n                },\n                {\n                    \"host_names\": [],\n                    \"app_variants\": [],\n                    \"environment\": \"{\\n    \\\"MTOA_VERSION\\\": \\\"3.1\\\"\\n}\",\n                    \"name\": \"3-1\",\n                    \"label\": \"3.1\"\n                }\n            ]\n        },\n        {\n            \"environment\": \"{}\",\n            \"name\": \"vray\",\n            \"label\": \"Chaos Group Vray\",\n            \"variants\": []\n        },\n        {\n            \"environment\": \"{}\",\n            \"name\": \"yeti\",\n            \"label\": \"Peregrine Labs Yeti\",\n            \"variants\": []\n        },\n        {\n            \"environment\": \"{}\",\n            \"name\": \"renderman\",\n            \"label\": \"Pixar Renderman\",\n            \"variants\": [\n                {\n                    \"host_names\": [\n                        \"maya\"\n                    ],\n                    \"app_variants\": [\n                        \"maya/2022\"\n                    ],\n                    \"environment\": \"{\\n    \\\"RFMTREE\\\": {\\n        \\\"windows\\\": \\\"C:\\\\\\\\Program Files\\\\\\\\Pixar\\\\\\\\RenderManForMaya-24.3\\\",\\n        \\\"darwin\\\": \\\"/Applications/Pixar/RenderManForMaya-24.3\\\",\\n        \\\"linux\\\": \\\"/opt/pixar/RenderManForMaya-24.3\\\"\\n    },\\n    \\\"RMANTREE\\\": {\\n        \\\"windows\\\": \\\"C:\\\\\\\\Program Files\\\\\\\\Pixar\\\\\\\\RenderManProServer-24.3\\\",\\n        \\\"darwin\\\": \\\"/Applications/Pixar/RenderManProServer-24.3\\\",\\n        \\\"linux\\\": \\\"/opt/pixar/RenderManProServer-24.3\\\"\\n    }\\n}\",\n                    \"name\": \"24-3-maya\",\n                    \"label\": \"24.3 RFM\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "server_addon/applications/server/version.py",
    "content": "__version__ = \"0.1.4\"\n"
  },
  {
    "path": "server_addon/blender/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import BlenderSettings, DEFAULT_VALUES\n\n\nclass BlenderAddon(BaseServerAddon):\n    name = \"blender\"\n    title = \"Blender\"\n    version = __version__\n    settings_model: Type[BlenderSettings] = BlenderSettings\n    frontend_scopes = {}\n    services = {}\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/blender/server/settings/__init__.py",
    "content": "from .main import (\n    BlenderSettings,\n    DEFAULT_VALUES,\n)\n\n\n__all__ = (\n    \"BlenderSettings\",\n    \"DEFAULT_VALUES\",\n)\n"
  },
  {
    "path": "server_addon/blender/server/settings/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass BlenderImageIOModel(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n"
  },
  {
    "path": "server_addon/blender/server/settings/main.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    TemplateWorkfileBaseOptions,\n)\n\nfrom .imageio import BlenderImageIOModel\nfrom .publish_plugins import (\n    PublishPuginsModel,\n    DEFAULT_BLENDER_PUBLISH_SETTINGS\n)\nfrom .render_settings import (\n    RenderSettingsModel,\n    DEFAULT_RENDER_SETTINGS\n)\n\n\nclass UnitScaleSettingsModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    apply_on_opening: bool = SettingsField(\n        False, title=\"Apply on Opening Existing Files\")\n    base_file_unit_scale: float = SettingsField(\n        1.0, title=\"Base File Unit Scale\"\n    )\n\n\nclass BlenderSettings(BaseSettingsModel):\n    unit_scale_settings: UnitScaleSettingsModel = SettingsField(\n        default_factory=UnitScaleSettingsModel,\n        title=\"Set Unit Scale\"\n    )\n    set_resolution_startup: bool = SettingsField(\n        True,\n        title=\"Set Resolution on Startup\"\n    )\n    set_frames_startup: bool = SettingsField(\n        True,\n        title=\"Set Start/End Frames and FPS on Startup\"\n    )\n    imageio: BlenderImageIOModel = SettingsField(\n        default_factory=BlenderImageIOModel,\n        title=\"Color Management (ImageIO)\"\n    )\n    RenderSettings: RenderSettingsModel = SettingsField(\n        default_factory=RenderSettingsModel, title=\"Render Settings\")\n    workfile_builder: TemplateWorkfileBaseOptions = SettingsField(\n        default_factory=TemplateWorkfileBaseOptions,\n        title=\"Workfile Builder\"\n    )\n    publish: PublishPuginsModel = SettingsField(\n        default_factory=PublishPuginsModel,\n        title=\"Publish Plugins\"\n    )\n\n\nDEFAULT_VALUES = {\n    \"unit_scale_settings\": {\n        \"enabled\": True,\n        \"apply_on_opening\": False,\n        \"base_file_unit_scale\": 1.00\n    },\n    \"set_frames_startup\": True,\n    \"set_resolution_startup\": True,\n    \"RenderSettings\": DEFAULT_RENDER_SETTINGS,\n    \"publish\": DEFAULT_BLENDER_PUBLISH_SETTINGS,\n    \"workfile_builder\": {\n        \"create_first_version\": False,\n        \"custom_templates\": []\n    }\n}\n"
  },
  {
    "path": "server_addon/blender/server/settings/publish_plugins.py",
    "content": "import json\nfrom pydantic import validator\nfrom ayon_server.exceptions import BadRequestException\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\n\n\ndef validate_json_dict(value):\n    if not value.strip():\n        return \"{}\"\n    try:\n        converted_value = json.loads(value)\n        success = isinstance(converted_value, dict)\n    except json.JSONDecodeError:\n        success = False\n\n    if not success:\n        raise BadRequestException(\n            \"Environment's can't be parsed as json object\"\n        )\n    return value\n\n\nclass ValidatePluginModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n\n\nclass ValidateFileSavedModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"ValidateFileSaved\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    exclude_families: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Exclude product types\"\n    )\n\n\nclass ExtractBlendModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    families: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Families\"\n    )\n\n\nclass ExtractPlayblastModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    presets: str = SettingsField(\"\", title=\"Presets\", widget=\"textarea\")\n\n    @validator(\"presets\")\n    def validate_json(cls, value):\n        return validate_json_dict(value)\n\n\nclass PublishPuginsModel(BaseSettingsModel):\n    ValidateCameraZeroKeyframe: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Validate Camera Zero Keyframe\",\n        section=\"General Validators\"\n    )\n    ValidateFileSaved: ValidateFileSavedModel = SettingsField(\n        default_factory=ValidateFileSavedModel,\n        title=\"Validate File Saved\",\n    )\n    ValidateInstanceEmpty: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Validate Instance is not Empty\"\n    )\n    ValidateMeshHasUvs: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Validate Mesh Has Uvs\",\n        section=\"Model Validators\"\n    )\n    ValidateMeshNoNegativeScale: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Validate Mesh No Negative Scale\"\n    )\n    ValidateTransformZero: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Validate Transform Zero\"\n    )\n    ValidateNoColonsInName: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Validate No Colons In Name\"\n    )\n    ValidateRenderCameraIsSet: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Validate Render Camera Is Set\",\n        section=\"Render Validators\"\n    )\n    ValidateDeadlinePublish: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Validate Render Output for Deadline\",\n    )\n    ExtractBlend: ExtractBlendModel = SettingsField(\n        default_factory=ExtractBlendModel,\n        title=\"Extract Blend\",\n        section=\"Extractors\"\n    )\n    ExtractFBX: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Extract FBX\"\n    )\n    ExtractModelABC: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Extract ABC\"\n    )\n    ExtractBlendAnimation: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Extract Blend Animation\"\n    )\n    ExtractAnimationFBX: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Extract Animation FBX\"\n    )\n    ExtractCamera: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Extract Camera\"\n    )\n    ExtractCameraABC: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Extract Camera as ABC\"\n    )\n    ExtractLayout: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Extract Layout (JSON)\"\n    )\n    ExtractThumbnail: ExtractPlayblastModel = SettingsField(\n        default_factory=ExtractPlayblastModel,\n        title=\"Extract Thumbnail\"\n    )\n    ExtractPlayblast: ExtractPlayblastModel = SettingsField(\n        default_factory=ExtractPlayblastModel,\n        title=\"Extract Playblast\"\n    )\n\n\nDEFAULT_BLENDER_PUBLISH_SETTINGS = {\n    \"ValidateCameraZeroKeyframe\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateFileSaved\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True,\n        \"exclude_families\": []\n    },\n    \"ValidateRenderCameraIsSet\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateDeadlinePublish\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateMeshHasUvs\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshNoNegativeScale\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateTransformZero\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateNoColonsInName\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateInstanceEmpty\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ExtractBlend\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True,\n        \"families\": [\n            \"model\",\n            \"camera\",\n            \"rig\",\n            \"action\",\n            \"layout\",\n            \"blendScene\"\n        ]\n    },\n    \"ExtractFBX\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ExtractModelABC\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ExtractBlendAnimation\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ExtractAnimationFBX\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ExtractCamera\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ExtractCameraABC\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ExtractLayout\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": False\n    },\n    \"ExtractThumbnail\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True,\n        \"presets\": json.dumps(\n            {\n                \"model\": {\n                    \"image_settings\": {\n                        \"file_format\": \"JPEG\",\n                        \"color_mode\": \"RGB\",\n                        \"quality\": 100\n                    },\n                    \"display_options\": {\n                        \"shading\": {\n                            \"light\": \"STUDIO\",\n                            \"studio_light\": \"Default\",\n                            \"type\": \"SOLID\",\n                            \"color_type\": \"OBJECT\",\n                            \"show_xray\": False,\n                            \"show_shadows\": False,\n                            \"show_cavity\": True\n                        },\n                        \"overlay\": {\n                            \"show_overlays\": False\n                        }\n                    }\n                },\n                \"rig\": {\n                    \"image_settings\": {\n                        \"file_format\": \"JPEG\",\n                        \"color_mode\": \"RGB\",\n                        \"quality\": 100\n                    },\n                    \"display_options\": {\n                        \"shading\": {\n                            \"light\": \"STUDIO\",\n                            \"studio_light\": \"Default\",\n                            \"type\": \"SOLID\",\n                            \"color_type\": \"OBJECT\",\n                            \"show_xray\": True,\n                            \"show_shadows\": False,\n                            \"show_cavity\": False\n                        },\n                        \"overlay\": {\n                            \"show_overlays\": True,\n                            \"show_ortho_grid\": False,\n                            \"show_floor\": False,\n                            \"show_axis_x\": False,\n                            \"show_axis_y\": False,\n                            \"show_axis_z\": False,\n                            \"show_text\": False,\n                            \"show_stats\": False,\n                            \"show_cursor\": False,\n                            \"show_annotation\": False,\n                            \"show_extras\": False,\n                            \"show_relationship_lines\": False,\n                            \"show_outline_selected\": False,\n                            \"show_motion_paths\": False,\n                            \"show_object_origins\": False,\n                            \"show_bones\": True\n                        }\n                    }\n                }\n            },\n            indent=4,\n        )\n    },\n    \"ExtractPlayblast\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True,\n        \"presets\": json.dumps(\n            {\n                \"default\": {\n                    \"image_settings\": {\n                        \"file_format\": \"PNG\",\n                        \"color_mode\": \"RGB\",\n                        \"color_depth\": \"8\",\n                        \"compression\": 15\n                    },\n                    \"display_options\": {\n                        \"shading\": {\n                            \"type\": \"MATERIAL\",\n                            \"render_pass\": \"COMBINED\"\n                        },\n                        \"overlay\": {\n                            \"show_overlays\": False\n                        }\n                    }\n                }\n            },\n            indent=4\n        )\n    }\n}\n"
  },
  {
    "path": "server_addon/blender/server/settings/render_settings.py",
    "content": "\"\"\"Providing models and values for Blender Render Settings.\"\"\"\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\n\n\ndef aov_separators_enum():\n    return [\n        {\"value\": \"dash\", \"label\": \"- (dash)\"},\n        {\"value\": \"underscore\", \"label\": \"_ (underscore)\"},\n        {\"value\": \"dot\", \"label\": \". (dot)\"}\n    ]\n\n\ndef image_format_enum():\n    return [\n        {\"value\": \"exr\", \"label\": \"OpenEXR\"},\n        {\"value\": \"bmp\", \"label\": \"BMP\"},\n        {\"value\": \"rgb\", \"label\": \"Iris\"},\n        {\"value\": \"png\", \"label\": \"PNG\"},\n        {\"value\": \"jpg\", \"label\": \"JPEG\"},\n        {\"value\": \"jp2\", \"label\": \"JPEG 2000\"},\n        {\"value\": \"tga\", \"label\": \"Targa\"},\n        {\"value\": \"tif\", \"label\": \"TIFF\"},\n    ]\n\n\ndef renderers_enum():\n    return [\n        {\"value\": \"CYCLES\", \"label\": \"Cycles\"},\n        {\"value\": \"BLENDER_EEVEE\", \"label\": \"Eevee\"},\n    ]\n\n\ndef aov_list_enum():\n    return [\n        {\"value\": \"empty\", \"label\": \"< none >\"},\n        {\"value\": \"combined\", \"label\": \"Combined\"},\n        {\"value\": \"z\", \"label\": \"Z\"},\n        {\"value\": \"mist\", \"label\": \"Mist\"},\n        {\"value\": \"normal\", \"label\": \"Normal\"},\n        {\"value\": \"position\", \"label\": \"Position (Cycles Only)\"},\n        {\"value\": \"vector\", \"label\": \"Vector (Cycles Only)\"},\n        {\"value\": \"uv\", \"label\": \"UV (Cycles Only)\"},\n        {\"value\": \"denoising\", \"label\": \"Denoising Data (Cycles Only)\"},\n        {\"value\": \"object_index\", \"label\": \"Object Index (Cycles Only)\"},\n        {\"value\": \"material_index\", \"label\": \"Material Index (Cycles Only)\"},\n        {\"value\": \"sample_count\", \"label\": \"Sample Count (Cycles Only)\"},\n        {\"value\": \"diffuse_light\", \"label\": \"Diffuse Light/Direct\"},\n        {\n            \"value\": \"diffuse_indirect\",\n            \"label\": \"Diffuse Indirect (Cycles Only)\"\n        },\n        {\"value\": \"diffuse_color\", \"label\": \"Diffuse Color\"},\n        {\"value\": \"specular_light\", \"label\": \"Specular (Glossy) Light/Direct\"},\n        {\n            \"value\": \"specular_indirect\",\n            \"label\": \"Specular (Glossy) Indirect (Cycles Only)\"\n        },\n        {\"value\": \"specular_color\", \"label\": \"Specular (Glossy) Color\"},\n        {\n            \"value\": \"transmission_light\",\n            \"label\": \"Transmission Light/Direct (Cycles Only)\"\n        },\n        {\n            \"value\": \"transmission_indirect\",\n            \"label\": \"Transmission Indirect (Cycles Only)\"\n        },\n        {\n            \"value\": \"transmission_color\",\n            \"label\": \"Transmission Color (Cycles Only)\"\n        },\n        {\"value\": \"volume_light\", \"label\": \"Volume Light/Direct\"},\n        {\"value\": \"volume_indirect\", \"label\": \"Volume Indirect (Cycles Only)\"},\n        {\"value\": \"emission\", \"label\": \"Emission\"},\n        {\"value\": \"environment\", \"label\": \"Environment\"},\n        {\"value\": \"shadow\", \"label\": \"Shadow/Shadow Catcher\"},\n        {\"value\": \"ao\", \"label\": \"Ambient Occlusion\"},\n        {\"value\": \"bloom\", \"label\": \"Bloom (Eevee Only)\"},\n        {\"value\": \"transparent\", \"label\": \"Transparent (Eevee Only)\"},\n        {\"value\": \"cryptomatte_object\", \"label\": \"Cryptomatte Object\"},\n        {\"value\": \"cryptomatte_material\", \"label\": \"Cryptomatte Material\"},\n        {\"value\": \"cryptomatte_asset\", \"label\": \"Cryptomatte Asset\"},\n        {\n            \"value\": \"cryptomatte_accurate\",\n            \"label\": \"Cryptomatte Accurate Mode (Eevee Only)\"\n        },\n    ]\n\n\ndef custom_passes_types_enum():\n    return [\n        {\"value\": \"COLOR\", \"label\": \"Color\"},\n        {\"value\": \"VALUE\", \"label\": \"Value\"},\n    ]\n\n\nclass CustomPassesModel(BaseSettingsModel):\n    \"\"\"Custom Passes\"\"\"\n    _layout = \"compact\"\n\n    attribute: str = SettingsField(\"\", title=\"Attribute name\")\n    value: str = SettingsField(\n        \"COLOR\",\n        title=\"Type\",\n        enum_resolver=custom_passes_types_enum\n    )\n\n\nclass RenderSettingsModel(BaseSettingsModel):\n    default_render_image_folder: str = SettingsField(\n        title=\"Default Render Image Folder\"\n    )\n    aov_separator: str = SettingsField(\n        \"underscore\",\n        title=\"AOV Separator Character\",\n        enum_resolver=aov_separators_enum\n    )\n    image_format: str = SettingsField(\n        \"exr\",\n        title=\"Image Format\",\n        enum_resolver=image_format_enum\n    )\n    multilayer_exr: bool = SettingsField(\n        title=\"Multilayer (EXR)\"\n    )\n    renderer: str = SettingsField(\n        \"CYCLES\",\n        title=\"Renderer\",\n        enum_resolver=renderers_enum\n    )\n    compositing: bool = SettingsField(\n        title=\"Enable Compositing\"\n    )\n    aov_list: list[str] = SettingsField(\n        default_factory=list,\n        enum_resolver=aov_list_enum,\n        title=\"AOVs to create\"\n    )\n    custom_passes: list[CustomPassesModel] = SettingsField(\n        default_factory=list,\n        title=\"Custom Passes\",\n        description=(\n            \"Add custom AOVs. They are added to the view layer and in the \"\n            \"Compositing Nodetree,\\nbut they need to be added manually to \"\n            \"the Shader Nodetree.\"\n        )\n    )\n\n\nDEFAULT_RENDER_SETTINGS = {\n    \"default_render_image_folder\": \"renders/blender\",\n    \"aov_separator\": \"underscore\",\n    \"image_format\": \"exr\",\n    \"multilayer_exr\": True,\n    \"renderer\": \"CYCLES\",\n    \"compositing\": True,\n    \"aov_list\": [\"combined\"],\n    \"custom_passes\": []\n}\n"
  },
  {
    "path": "server_addon/blender/server/version.py",
    "content": "__version__ = \"0.1.6\"\n"
  },
  {
    "path": "server_addon/celaction/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import CelActionSettings, DEFAULT_VALUES\n\n\nclass CelActionAddon(BaseServerAddon):\n    name = \"celaction\"\n    title = \"CelAction\"\n    version = __version__\n    settings_model: Type[CelActionSettings] = CelActionSettings\n    frontend_scopes = {}\n    services = {}\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/celaction/server/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass CelActionImageIOModel(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n"
  },
  {
    "path": "server_addon/celaction/server/settings.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\nfrom .imageio import CelActionImageIOModel\n\n\nclass CollectRenderPathModel(BaseSettingsModel):\n    output_extension: str = SettingsField(\n        \"\",\n        title=\"Output render file extension\"\n    )\n    anatomy_template_key_render_files: str = SettingsField(\n        \"\",\n        title=\"Anatomy template key: render files\"\n    )\n    anatomy_template_key_metadata: str = SettingsField(\n        \"\",\n        title=\"Anatomy template key: metadata job file\"\n    )\n\n\ndef _workfile_submit_overrides():\n    return [\n        {\n            \"value\": \"render_chunk\",\n            \"label\": \"Pass chunk size\"\n        },\n        {\n            \"value\": \"frame_range\",\n            \"label\": \"Pass frame range\"\n        },\n        {\n            \"value\": \"resolution\",\n            \"label\": \"Pass resolution\"\n        }\n    ]\n\n\nclass WorkfileModel(BaseSettingsModel):\n    submission_overrides: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Submission workfile overrides\",\n        enum_resolver=_workfile_submit_overrides\n    )\n\n\nclass PublishPuginsModel(BaseSettingsModel):\n    CollectRenderPath: CollectRenderPathModel = SettingsField(\n        default_factory=CollectRenderPathModel,\n        title=\"Collect Render Path\"\n    )\n\n\nclass CelActionSettings(BaseSettingsModel):\n    imageio: CelActionImageIOModel = SettingsField(\n        default_factory=CelActionImageIOModel,\n        title=\"Color Management (ImageIO)\"\n    )\n    workfile: WorkfileModel = SettingsField(\n        title=\"Workfile\"\n    )\n    publish: PublishPuginsModel = SettingsField(\n        default_factory=PublishPuginsModel,\n        title=\"Publish plugins\",\n    )\n\n\nDEFAULT_VALUES = {\n    \"imageio\": {\n        \"ocio_config\": {\n            \"enabled\": False,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"enabled\": False,\n            \"rules\": []\n        }\n    },\n    \"workfile\": {\n        \"submission_overrides\": [\n            \"render_chunk\",\n            \"frame_range\",\n            \"resolution\"\n        ]\n    },\n    \"publish\": {\n        \"CollectRenderPath\": {\n            \"output_extension\": \"png\",\n            \"anatomy_template_key_render_files\": \"render\",\n            \"anatomy_template_key_metadata\": \"render\"\n        }\n    }\n}\n"
  },
  {
    "path": "server_addon/celaction/server/version.py",
    "content": "__version__ = \"0.1.0\"\n"
  },
  {
    "path": "server_addon/clockify/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import ClockifySettings\n\n\nclass ClockifyAddon(BaseServerAddon):\n    name = \"clockify\"\n    title = \"Clockify\"\n    version = __version__\n    settings_model: Type[ClockifySettings] = ClockifySettings\n    frontend_scopes = {}\n    services = {}\n"
  },
  {
    "path": "server_addon/clockify/server/settings.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass ClockifySettings(BaseSettingsModel):\n    workspace_name: str = SettingsField(\n        \"\",\n        title=\"Workspace name\",\n        scope=[\"studio\"]\n    )\n"
  },
  {
    "path": "server_addon/clockify/server/version.py",
    "content": "__version__ = \"0.1.1\"\n"
  },
  {
    "path": "server_addon/core/server/__init__.py",
    "content": "from ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import CoreSettings, DEFAULT_VALUES\n\n\nclass CoreAddon(BaseServerAddon):\n    name = \"core\"\n    title = \"Core\"\n    version = __version__\n    settings_model = CoreSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/core/server/settings/__init__.py",
    "content": "from .main import CoreSettings, DEFAULT_VALUES\n\n\n__all__ = (\n    \"CoreSettings\",\n    \"DEFAULT_VALUES\",\n)\n"
  },
  {
    "path": "server_addon/core/server/settings/main.py",
    "content": "import json\nfrom pydantic import validator\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    MultiplatformPathListModel,\n    ensure_unique_names,\n    task_types_enum,\n)\nfrom ayon_server.exceptions import BadRequestException\n\nfrom .publish_plugins import PublishPuginsModel, DEFAULT_PUBLISH_VALUES\nfrom .tools import GlobalToolsModel, DEFAULT_TOOLS_VALUES\n\n\nclass DiskMappingItemModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    source: str = SettingsField(\"\", title=\"Source\")\n    destination: str = SettingsField(\"\", title=\"Destination\")\n\n\nclass DiskMappingModel(BaseSettingsModel):\n    windows: list[DiskMappingItemModel] = SettingsField(\n        title=\"Windows\",\n        default_factory=list,\n    )\n    linux: list[DiskMappingItemModel] = SettingsField(\n        title=\"Linux\",\n        default_factory=list,\n    )\n    darwin: list[DiskMappingItemModel] = SettingsField(\n        title=\"MacOS\",\n        default_factory=list,\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass CoreImageIOFileRulesModel(BaseSettingsModel):\n    activate_global_file_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass CoreImageIOConfigModel(BaseSettingsModel):\n    filepath: list[str] = SettingsField(\n        default_factory=list, title=\"Config path\"\n    )\n\n\nclass CoreImageIOBaseModel(BaseSettingsModel):\n    activate_global_color_management: bool = SettingsField(\n        False,\n        title=\"Enable Color Management\"\n    )\n    ocio_config: CoreImageIOConfigModel = SettingsField(\n        default_factory=CoreImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: CoreImageIOFileRulesModel = SettingsField(\n        default_factory=CoreImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n\n\nclass VersionStartCategoryProfileModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    host_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Host names\"\n    )\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    task_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task names\"\n    )\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product types\"\n    )\n    product_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product names\"\n    )\n    version_start: int = SettingsField(\n        1,\n        title=\"Version Start\",\n        ge=0\n    )\n\n\nclass VersionStartCategoryModel(BaseSettingsModel):\n    profiles: list[VersionStartCategoryProfileModel] = SettingsField(\n        default_factory=list,\n        title=\"Profiles\"\n    )\n\n\nclass CoreSettings(BaseSettingsModel):\n    studio_name: str = SettingsField(\"\", title=\"Studio name\", scope=[\"studio\"])\n    studio_code: str = SettingsField(\"\", title=\"Studio code\", scope=[\"studio\"])\n    environments: str = SettingsField(\n        \"{}\",\n        title=\"Global environment variables\",\n        widget=\"textarea\",\n        scope=[\"studio\"],\n    )\n    disk_mapping: DiskMappingModel = SettingsField(\n        default_factory=DiskMappingModel,\n        title=\"Disk mapping\",\n    )\n    tools: GlobalToolsModel = SettingsField(\n        default_factory=GlobalToolsModel,\n        title=\"Tools\"\n    )\n    version_start_category: VersionStartCategoryModel = SettingsField(\n        default_factory=VersionStartCategoryModel,\n        title=\"Version start\"\n    )\n    imageio: CoreImageIOBaseModel = SettingsField(\n        default_factory=CoreImageIOBaseModel,\n        title=\"Color Management (ImageIO)\"\n    )\n    publish: PublishPuginsModel = SettingsField(\n        default_factory=PublishPuginsModel,\n        title=\"Publish plugins\"\n    )\n    project_plugins: MultiplatformPathListModel = SettingsField(\n        default_factory=MultiplatformPathListModel,\n        title=\"Additional Project Plugin Paths\",\n    )\n    project_folder_structure: str = SettingsField(\n        \"{}\",\n        widget=\"textarea\",\n        title=\"Project folder structure\",\n        section=\"---\"\n    )\n    project_environments: str = SettingsField(\n        \"{}\",\n        widget=\"textarea\",\n        title=\"Project environments\",\n        section=\"---\"\n    )\n\n    @validator(\n        \"environments\",\n        \"project_folder_structure\",\n        \"project_environments\")\n    def validate_json(cls, value):\n        if not value.strip():\n            return \"{}\"\n        try:\n            converted_value = json.loads(value)\n            success = isinstance(converted_value, dict)\n        except json.JSONDecodeError:\n            success = False\n\n        if not success:\n            raise BadRequestException(\n                \"Environment's can't be parsed as json object\"\n            )\n        return value\n\n\nDEFAULT_VALUES = {\n    \"imageio\": {\n        \"activate_global_color_management\": False,\n        \"ocio_config\": {\n            \"filepath\": [\n                \"{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio\",\n                \"{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio\"\n            ]\n        },\n        \"file_rules\": {\n            \"activate_global_file_rules\": False,\n            \"rules\": [\n                {\n                    \"name\": \"example\",\n                    \"pattern\": \".*(beauty).*\",\n                    \"colorspace\": \"ACES - ACEScg\",\n                    \"ext\": \"exr\"\n                }\n            ]\n        }\n    },\n    \"studio_name\": \"\",\n    \"studio_code\": \"\",\n    \"environments\": \"{\\n\\\"STUDIO_SW\\\": {\\n        \\\"darwin\\\": \\\"/mnt/REPO_SW\\\",\\n        \\\"linux\\\": \\\"/mnt/REPO_SW\\\",\\n        \\\"windows\\\": \\\"P:/REPO_SW\\\"\\n    }\\n}\",\n    \"tools\": DEFAULT_TOOLS_VALUES,\n    \"version_start_category\": {\n        \"profiles\": []\n    },\n    \"publish\": DEFAULT_PUBLISH_VALUES,\n    \"project_folder_structure\": json.dumps({\n        \"__project_root__\": {\n            \"prod\": {},\n            \"resources\": {\n                \"footage\": {\n                    \"plates\": {},\n                    \"offline\": {}\n                },\n                \"audio\": {},\n                \"art_dept\": {}\n            },\n            \"editorial\": {},\n            \"assets\": {\n                \"characters\": {},\n                \"locations\": {}\n            },\n            \"shots\": {}\n        }\n    }, indent=4),\n    \"project_plugins\": {\n        \"windows\": [],\n        \"darwin\": [],\n        \"linux\": []\n    },\n    \"project_environments\": \"{}\"\n}\n"
  },
  {
    "path": "server_addon/core/server/settings/publish_plugins.py",
    "content": "from pydantic import validator\n\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    MultiplatformPathModel,\n    normalize_name,\n    ensure_unique_names,\n    task_types_enum,\n)\n\nfrom ayon_server.types import ColorRGBA_uint8\n\n\nclass ValidateBaseModel(BaseSettingsModel):\n    _isGroup = True\n    enabled: bool = SettingsField(True)\n    optional: bool = SettingsField(True, title=\"Optional\")\n    active: bool = SettingsField(True, title=\"Active\")\n\n\nclass CollectAnatomyInstanceDataModel(BaseSettingsModel):\n    _isGroup = True\n    follow_workfile_version: bool = SettingsField(\n        True, title=\"Follow workfile version\"\n    )\n\n\nclass CollectAudioModel(BaseSettingsModel):\n    _isGroup = True\n    enabled: bool = SettingsField(True)\n    audio_product_name: str = SettingsField(\n        \"\", title=\"Name of audio variant\"\n    )\n\n\nclass CollectSceneVersionModel(BaseSettingsModel):\n    _isGroup = True\n    hosts: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Host names\"\n    )\n    skip_hosts_headless_publish: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Skip for host if headless publish\"\n    )\n\n\nclass CollectCommentPIModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    families: list[str] = SettingsField(default_factory=list, title=\"Families\")\n\n\nclass CollectFramesFixDefModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    rewrite_version_enable: bool = SettingsField(\n        True,\n        title=\"Show 'Rewrite latest version' toggle\"\n    )\n\n\nclass ValidateIntentProfile(BaseSettingsModel):\n    _layout = \"expanded\"\n    hosts: list[str] = SettingsField(default_factory=list, title=\"Host names\")\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    tasks: list[str] = SettingsField(default_factory=list, title=\"Task names\")\n    # TODO This was 'validate' in v3\n    validate_intent: bool = SettingsField(True, title=\"Validate\")\n\n\nclass ValidateIntentModel(BaseSettingsModel):\n    \"\"\"Validate if Publishing intent was selected.\n\n    It is possible to disable validation for specific publishing context\n    with profiles.\n    \"\"\"\n\n    _isGroup = True\n    enabled: bool = SettingsField(False)\n    profiles: list[ValidateIntentProfile] = SettingsField(default_factory=list)\n\n\nclass ExtractThumbnailFFmpegModel(BaseSettingsModel):\n    input: list[str] = SettingsField(\n        default_factory=list,\n        title=\"FFmpeg input arguments\"\n    )\n    output: list[str] = SettingsField(\n        default_factory=list,\n        title=\"FFmpeg input arguments\"\n    )\n\n\nclass ResizeItemModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    width: int = SettingsField(\n        1920,\n        ge=0,\n        le=100000,\n        title=\"Width\",\n        description=\"Width and Height must be both set to higher value than 0\"\n        \" else source resolution is used.\"\n    )\n    height: int = SettingsField(\n        1080,\n        title=\"Height\",\n        ge=0,\n        le=100000,\n    )\n\n\n_resize_types_enum = [\n    {\"value\": \"source\", \"label\": \"Image source\"},\n    {\"value\": \"resize\", \"label\": \"Resize\"},\n]\n\n\nclass ResizeModel(BaseSettingsModel):\n    _layout = \"expanded\"\n\n    type: str = SettingsField(\n        title=\"Type\",\n        description=\"Type of resizing\",\n        enum_resolver=lambda: _resize_types_enum,\n        conditionalEnum=True,\n        default=\"source\"\n    )\n\n    resize: ResizeItemModel = SettingsField(\n        default_factory=ResizeItemModel,\n        title=\"Resize\"\n    )\n\n\n_thumbnail_oiio_transcoding_type = [\n    {\"value\": \"colorspace\", \"label\": \"Use Colorspace\"},\n    {\"value\": \"display_and_view\", \"label\": \"Use Display&View\"}\n]\n\n\nclass DisplayAndViewModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    display: str = SettingsField(\n        \"default\",\n        title=\"Display\"\n    )\n    view: str = SettingsField(\n        \"sRGB\",\n        title=\"View\"\n    )\n\n\nclass ExtractThumbnailOIIODefaultsModel(BaseSettingsModel):\n    type: str = SettingsField(\n        title=\"Type\",\n        description=\"Transcoding type\",\n        enum_resolver=lambda: _thumbnail_oiio_transcoding_type,\n        conditionalEnum=True,\n        default=\"colorspace\"\n    )\n\n    colorspace: str = SettingsField(\n        \"\",\n        title=\"Colorspace\"\n    )\n    display_and_view: DisplayAndViewModel = SettingsField(\n        default_factory=DisplayAndViewModel,\n        title=\"Display&View\"\n    )\n\n\nclass ExtractThumbnailModel(BaseSettingsModel):\n    _isGroup = True\n    enabled: bool = SettingsField(True)\n    product_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product names\"\n    )\n    integrate_thumbnail: bool = SettingsField(\n        True,\n        title=\"Integrate Thumbnail Representation\"\n    )\n    target_size: ResizeModel = SettingsField(\n        default_factory=ResizeModel,\n        title=\"Target size\"\n    )\n    background_color: ColorRGBA_uint8 = SettingsField(\n        (0, 0, 0, 0.0),\n        title=\"Background color\"\n    )\n    duration_split: float = SettingsField(\n        0.5,\n        title=\"Duration split\",\n        ge=0.0,\n        le=1.0\n    )\n    oiiotool_defaults: ExtractThumbnailOIIODefaultsModel = SettingsField(\n        default_factory=ExtractThumbnailOIIODefaultsModel,\n        title=\"OIIOtool defaults\"\n    )\n    ffmpeg_args: ExtractThumbnailFFmpegModel = SettingsField(\n        default_factory=ExtractThumbnailFFmpegModel\n    )\n\n\ndef _extract_oiio_transcoding_type():\n    return [\n        {\"value\": \"colorspace\", \"label\": \"Use Colorspace\"},\n        {\"value\": \"display\", \"label\": \"Use Display&View\"}\n    ]\n\n\nclass OIIOToolArgumentsModel(BaseSettingsModel):\n    additional_command_args: list[str] = SettingsField(\n        default_factory=list, title=\"Arguments\")\n\n\nclass ExtractOIIOTranscodeOutputModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField(\"\", title=\"Name\")\n    extension: str = SettingsField(\"\", title=\"Extension\")\n    transcoding_type: str = SettingsField(\n        \"colorspace\",\n        title=\"Transcoding type\",\n        enum_resolver=_extract_oiio_transcoding_type\n    )\n    colorspace: str = SettingsField(\"\", title=\"Colorspace\")\n    display: str = SettingsField(\"\", title=\"Display\")\n    view: str = SettingsField(\"\", title=\"View\")\n    oiiotool_args: OIIOToolArgumentsModel = SettingsField(\n        default_factory=OIIOToolArgumentsModel,\n        title=\"OIIOtool arguments\")\n\n    tags: list[str] = SettingsField(default_factory=list, title=\"Tags\")\n    custom_tags: list[str] = SettingsField(\n        default_factory=list, title=\"Custom Tags\"\n    )\n\n\nclass ExtractOIIOTranscodeProfileModel(BaseSettingsModel):\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product types\"\n    )\n    hosts: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Host names\"\n    )\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    task_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task names\"\n    )\n    product_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product names\"\n    )\n    delete_original: bool = SettingsField(\n        True,\n        title=\"Delete Original Representation\"\n    )\n    outputs: list[ExtractOIIOTranscodeOutputModel] = SettingsField(\n        default_factory=list,\n        title=\"Output Definitions\",\n    )\n\n    @validator(\"outputs\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ExtractOIIOTranscodeModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    profiles: list[ExtractOIIOTranscodeProfileModel] = SettingsField(\n        default_factory=list, title=\"Profiles\"\n    )\n\n\n# --- [START] Extract Review ---\nclass ExtractReviewFFmpegModel(BaseSettingsModel):\n    video_filters: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Video filters\"\n    )\n    audio_filters: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Audio filters\"\n    )\n    input: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Input arguments\"\n    )\n    output: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Output arguments\"\n    )\n\n\ndef extract_review_filter_enum():\n    return [\n        {\n            \"value\": \"everytime\",\n            \"label\": \"Always\"\n        },\n        {\n            \"value\": \"single_frame\",\n            \"label\": \"Only if input has 1 image frame\"\n        },\n        {\n            \"value\": \"multi_frame\",\n            \"label\": \"Only if input is video or sequence of frames\"\n        }\n    ]\n\n\nclass ExtractReviewFilterModel(BaseSettingsModel):\n    families: list[str] = SettingsField(default_factory=list, title=\"Families\")\n    product_names: list[str] = SettingsField(\n        default_factory=list, title=\"Product names\")\n    custom_tags: list[str] = SettingsField(\n        default_factory=list, title=\"Custom Tags\"\n    )\n    single_frame_filter: str = SettingsField(\n        \"everytime\",\n        description=(\n            \"Use output <b>always</b> / only if input <b>is 1 frame</b>\"\n            \" image / only if has <b>2+ frames</b> or <b>is video</b>\"\n        ),\n        enum_resolver=extract_review_filter_enum\n    )\n\n\nclass ExtractReviewLetterBox(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    ratio: float = SettingsField(\n        0.0,\n        title=\"Ratio\",\n        ge=0.0,\n        le=10000.0\n    )\n    fill_color: ColorRGBA_uint8 = SettingsField(\n        (0, 0, 0, 0.0),\n        title=\"Fill Color\"\n    )\n    line_thickness: int = SettingsField(\n        0,\n        title=\"Line Thickness\",\n        ge=0,\n        le=1000\n    )\n    line_color: ColorRGBA_uint8 = SettingsField(\n        (0, 0, 0, 0.0),\n        title=\"Line Color\"\n    )\n\n\nclass ExtractReviewOutputDefModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField(\"\", title=\"Name\")\n    ext: str = SettingsField(\"\", title=\"Output extension\")\n    # TODO use some different source of tags\n    tags: list[str] = SettingsField(default_factory=list, title=\"Tags\")\n    burnins: list[str] = SettingsField(\n        default_factory=list, title=\"Link to a burnin by name\"\n    )\n    ffmpeg_args: ExtractReviewFFmpegModel = SettingsField(\n        default_factory=ExtractReviewFFmpegModel,\n        title=\"FFmpeg arguments\"\n    )\n    filter: ExtractReviewFilterModel = SettingsField(\n        default_factory=ExtractReviewFilterModel,\n        title=\"Additional output filtering\"\n    )\n    overscan_crop: str = SettingsField(\n        \"\",\n        title=\"Overscan crop\",\n        description=(\n            \"Crop input overscan. See the documentation for more information.\"\n        )\n    )\n    overscan_color: ColorRGBA_uint8 = SettingsField(\n        (0, 0, 0, 0.0),\n        title=\"Overscan color\",\n        description=(\n            \"Overscan color is used when input aspect ratio is not\"\n            \" same as output aspect ratio.\"\n        )\n    )\n    width: int = SettingsField(\n        0,\n        ge=0,\n        le=100000,\n        title=\"Output width\",\n        description=(\n            \"Width and Height must be both set to higher\"\n            \" value than 0 else source resolution is used.\"\n        )\n    )\n    height: int = SettingsField(\n        0,\n        title=\"Output height\",\n        ge=0,\n        le=100000,\n    )\n    scale_pixel_aspect: bool = SettingsField(\n        True,\n        title=\"Scale pixel aspect\",\n        description=(\n            \"Rescale input when it's pixel aspect ratio is not 1.\"\n            \" Usefull for anamorph reviews.\"\n        )\n    )\n    bg_color: ColorRGBA_uint8 = SettingsField(\n        (0, 0, 0, 0.0),\n        description=(\n            \"Background color is used only when input have transparency\"\n            \" and Alpha is higher than 0.\"\n        ),\n        title=\"Background color\",\n    )\n    letter_box: ExtractReviewLetterBox = SettingsField(\n        default_factory=ExtractReviewLetterBox,\n        title=\"Letter Box\"\n    )\n\n    @validator(\"name\")\n    def validate_name(cls, value):\n        \"\"\"Ensure name does not contain weird characters\"\"\"\n        return normalize_name(value)\n\n\nclass ExtractReviewProfileModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    product_types: list[str] = SettingsField(\n        default_factory=list, title=\"Product types\"\n    )\n    # TODO use hosts enum\n    hosts: list[str] = SettingsField(\n        default_factory=list, title=\"Host names\"\n    )\n    outputs: list[ExtractReviewOutputDefModel] = SettingsField(\n        default_factory=list, title=\"Output Definitions\"\n    )\n\n    @validator(\"outputs\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ExtractReviewModel(BaseSettingsModel):\n    _isGroup = True\n    enabled: bool = SettingsField(True)\n    profiles: list[ExtractReviewProfileModel] = SettingsField(\n        default_factory=list,\n        title=\"Profiles\"\n    )\n# --- [END] Extract Review ---\n\n\n# --- [Start] Extract Burnin ---\nclass ExtractBurninOptionsModel(BaseSettingsModel):\n    font_size: int = SettingsField(0, ge=0, title=\"Font size\")\n    font_color: ColorRGBA_uint8 = SettingsField(\n        (255, 255, 255, 1.0),\n        title=\"Font color\"\n    )\n    bg_color: ColorRGBA_uint8 = SettingsField(\n        (0, 0, 0, 1.0),\n        title=\"Background color\"\n    )\n    x_offset: int = SettingsField(0, title=\"X Offset\")\n    y_offset: int = SettingsField(0, title=\"Y Offset\")\n    bg_padding: int = SettingsField(0, title=\"Padding around text\")\n    font_filepath: MultiplatformPathModel = SettingsField(\n        default_factory=MultiplatformPathModel,\n        title=\"Font file path\"\n    )\n\n\nclass ExtractBurninDefFilter(BaseSettingsModel):\n    families: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Families\"\n    )\n    tags: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Tags\"\n    )\n\n\nclass ExtractBurninDef(BaseSettingsModel):\n    _isGroup = True\n    _layout = \"expanded\"\n    name: str = SettingsField(\"\")\n    TOP_LEFT: str = SettingsField(\"\", topic=\"Top Left\")\n    TOP_CENTERED: str = SettingsField(\"\", topic=\"Top Centered\")\n    TOP_RIGHT: str = SettingsField(\"\", topic=\"Top Right\")\n    BOTTOM_LEFT: str = SettingsField(\"\", topic=\"Bottom Left\")\n    BOTTOM_CENTERED: str = SettingsField(\"\", topic=\"Bottom Centered\")\n    BOTTOM_RIGHT: str = SettingsField(\"\", topic=\"Bottom Right\")\n    filter: ExtractBurninDefFilter = SettingsField(\n        default_factory=ExtractBurninDefFilter,\n        title=\"Additional filtering\"\n    )\n\n    @validator(\"name\")\n    def validate_name(cls, value):\n        \"\"\"Ensure name does not contain weird characters\"\"\"\n        return normalize_name(value)\n\n\nclass ExtractBurninProfile(BaseSettingsModel):\n    _layout = \"expanded\"\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Produt types\"\n    )\n    hosts: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Host names\"\n    )\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    task_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task names\"\n    )\n    product_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product names\"\n    )\n    burnins: list[ExtractBurninDef] = SettingsField(\n        default_factory=list,\n        title=\"Burnins\"\n    )\n\n    @validator(\"burnins\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n\n        return value\n\n\nclass ExtractBurninModel(BaseSettingsModel):\n    _isGroup = True\n    enabled: bool = SettingsField(True)\n    options: ExtractBurninOptionsModel = SettingsField(\n        default_factory=ExtractBurninOptionsModel,\n        title=\"Burnin formatting options\"\n    )\n    profiles: list[ExtractBurninProfile] = SettingsField(\n        default_factory=list,\n        title=\"Profiles\"\n    )\n# --- [END] Extract Burnin ---\n\n\nclass PreIntegrateThumbnailsProfile(BaseSettingsModel):\n    _isGroup = True\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product types\",\n    )\n    hosts: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Hosts\",\n    )\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    product_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product names\",\n    )\n    integrate_thumbnail: bool = SettingsField(True)\n\n\nclass PreIntegrateThumbnailsModel(BaseSettingsModel):\n    \"\"\"Explicitly set if Thumbnail representation should be integrated.\n\n    If no matching profile set, existing state from Host implementation\n    is kept.\n    \"\"\"\n\n    _isGroup = True\n    enabled: bool = SettingsField(True)\n    integrate_profiles: list[PreIntegrateThumbnailsProfile] = SettingsField(\n        default_factory=list,\n        title=\"Integrate profiles\"\n    )\n\n\nclass IntegrateProductGroupProfile(BaseSettingsModel):\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product types\"\n    )\n    hosts: list[str] = SettingsField(default_factory=list, title=\"Hosts\")\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    tasks: list[str] = SettingsField(default_factory=list, title=\"Task names\")\n    template: str = SettingsField(\"\", title=\"Template\")\n\n\nclass IntegrateProductGroupModel(BaseSettingsModel):\n    \"\"\"Group published products by filtering logic.\n\n    Set all published instances as a part of specific group named according\n     to 'Template'.\n\n    Implemented all variants of placeholders '{task}', '{product[type]}',\n    '{host}', '{product[name]}', '{renderlayer}'.\n    \"\"\"\n\n    _isGroup = True\n    product_grouping_profiles: list[IntegrateProductGroupProfile] = (\n        SettingsField(\n            default_factory=list,\n            title=\"Product group profiles\"\n        )\n    )\n\n\nclass IntegrateANProductGroupProfileModel(BaseSettingsModel):\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product types\"\n    )\n    hosts: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Hosts\"\n    )\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    tasks: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task names\"\n    )\n    template: str = SettingsField(\"\", title=\"Template\")\n\n\nclass IntegrateANTemplateNameProfileModel(BaseSettingsModel):\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product types\"\n    )\n    hosts: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Hosts\"\n    )\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    tasks: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task names\"\n    )\n    template_name: str = SettingsField(\"\", title=\"Template name\")\n\n\nclass IntegrateHeroTemplateNameProfileModel(BaseSettingsModel):\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product types\"\n    )\n    hosts: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Hosts\"\n    )\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    task_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task names\"\n    )\n    template_name: str = SettingsField(\"\", title=\"Template name\")\n\n\nclass IntegrateHeroVersionModel(BaseSettingsModel):\n    _isGroup = True\n    enabled: bool = SettingsField(True)\n    optional: bool = SettingsField(False, title=\"Optional\")\n    active: bool = SettingsField(True, title=\"Active\")\n    families: list[str] = SettingsField(default_factory=list, title=\"Families\")\n\n\nclass CleanUpModel(BaseSettingsModel):\n    _isGroup = True\n    paterns: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Patterns (regex)\"\n    )\n    remove_temp_renders: bool = SettingsField(\n        False, title=\"Remove Temp renders\"\n    )\n\n\nclass CleanUpFarmModel(BaseSettingsModel):\n    _isGroup = True\n    enabled: bool = SettingsField(True)\n\n\nclass PublishPuginsModel(BaseSettingsModel):\n    CollectAnatomyInstanceData: CollectAnatomyInstanceDataModel = (\n        SettingsField(\n            default_factory=CollectAnatomyInstanceDataModel,\n            title=\"Collect Anatomy Instance Data\"\n        )\n    )\n    CollectAudio: CollectAudioModel = SettingsField(\n        default_factory=CollectAudioModel,\n        title=\"Collect Audio\"\n    )\n    CollectSceneVersion: CollectSceneVersionModel = SettingsField(\n        default_factory=CollectSceneVersionModel,\n        title=\"Collect Version from Workfile\"\n    )\n    collect_comment_per_instance: CollectCommentPIModel = SettingsField(\n        default_factory=CollectCommentPIModel,\n        title=\"Collect comment per instance\",\n    )\n    CollectFramesFixDef: CollectFramesFixDefModel = SettingsField(\n        default_factory=CollectFramesFixDefModel,\n        title=\"Collect Frames to Fix\",\n    )\n    ValidateEditorialAssetName: ValidateBaseModel = SettingsField(\n        default_factory=ValidateBaseModel,\n        title=\"Validate Editorial Asset Name\"\n    )\n    ValidateVersion: ValidateBaseModel = SettingsField(\n        default_factory=ValidateBaseModel,\n        title=\"Validate Version\"\n    )\n    ValidateIntent: ValidateIntentModel = SettingsField(\n        default_factory=ValidateIntentModel,\n        title=\"Validate Intent\"\n    )\n    ExtractThumbnail: ExtractThumbnailModel = SettingsField(\n        default_factory=ExtractThumbnailModel,\n        title=\"Extract Thumbnail\"\n    )\n    ExtractOIIOTranscode: ExtractOIIOTranscodeModel = SettingsField(\n        default_factory=ExtractOIIOTranscodeModel,\n        title=\"Extract OIIO Transcode\"\n    )\n    ExtractReview: ExtractReviewModel = SettingsField(\n        default_factory=ExtractReviewModel,\n        title=\"Extract Review\"\n    )\n    ExtractBurnin: ExtractBurninModel = SettingsField(\n        default_factory=ExtractBurninModel,\n        title=\"Extract Burnin\"\n    )\n    PreIntegrateThumbnails: PreIntegrateThumbnailsModel = SettingsField(\n        default_factory=PreIntegrateThumbnailsModel,\n        title=\"Override Integrate Thumbnail Representations\"\n    )\n    IntegrateProductGroup: IntegrateProductGroupModel = SettingsField(\n        default_factory=IntegrateProductGroupModel,\n        title=\"Integrate Product Group\"\n    )\n    IntegrateHeroVersion: IntegrateHeroVersionModel = SettingsField(\n        default_factory=IntegrateHeroVersionModel,\n        title=\"Integrate Hero Version\"\n    )\n    CleanUp: CleanUpModel = SettingsField(\n        default_factory=CleanUpModel,\n        title=\"Clean Up\"\n    )\n    CleanUpFarm: CleanUpFarmModel = SettingsField(\n        default_factory=CleanUpFarmModel,\n        title=\"Clean Up Farm\"\n    )\n\n\nDEFAULT_PUBLISH_VALUES = {\n    \"CollectAnatomyInstanceData\": {\n        \"follow_workfile_version\": False\n    },\n    \"CollectAudio\": {\n        \"enabled\": False,\n        \"audio_product_name\": \"audioMain\"\n    },\n    \"CollectSceneVersion\": {\n        \"hosts\": [\n            \"aftereffects\",\n            \"blender\",\n            \"celaction\",\n            \"fusion\",\n            \"harmony\",\n            \"hiero\",\n            \"houdini\",\n            \"maya\",\n            \"nuke\",\n            \"photoshop\",\n            \"resolve\",\n            \"tvpaint\"\n        ],\n        \"skip_hosts_headless_publish\": []\n    },\n    \"collect_comment_per_instance\": {\n        \"enabled\": False,\n        \"families\": []\n    },\n    \"CollectFramesFixDef\": {\n        \"enabled\": True,\n        \"rewrite_version_enable\": True\n    },\n    \"ValidateEditorialAssetName\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateVersion\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateIntent\": {\n        \"enabled\": False,\n        \"profiles\": []\n    },\n    \"ExtractThumbnail\": {\n        \"enabled\": True,\n        \"product_names\": [],\n        \"integrate_thumbnail\": True,\n        \"target_size\": {\n            \"type\": \"source\"\n        },\n        \"duration_split\": 0.5,\n        \"oiiotool_defaults\": {\n            \"type\": \"colorspace\",\n            \"colorspace\": \"color_picking\"\n        },\n        \"ffmpeg_args\": {\n            \"input\": [\n                \"-apply_trc gamma22\"\n            ],\n            \"output\": []\n        }\n    },\n    \"ExtractOIIOTranscode\": {\n        \"enabled\": True,\n        \"profiles\": []\n    },\n    \"ExtractReview\": {\n        \"enabled\": True,\n        \"profiles\": [\n            {\n                \"product_types\": [],\n                \"hosts\": [],\n                \"outputs\": [\n                    {\n                        \"name\": \"png\",\n                        \"ext\": \"png\",\n                        \"tags\": [\n                            \"ftrackreview\",\n                            \"kitsureview\"\n                        ],\n                        \"burnins\": [],\n                        \"ffmpeg_args\": {\n                            \"video_filters\": [],\n                            \"audio_filters\": [],\n                            \"input\": [],\n                            \"output\": []\n                        },\n                        \"filter\": {\n                            \"families\": [\n                                \"render\",\n                                \"review\",\n                                \"ftrack\"\n                            ],\n                            \"product_names\": [],\n                            \"custom_tags\": [],\n                            \"single_frame_filter\": \"single_frame\"\n                        },\n                        \"overscan_crop\": \"\",\n                        \"overscan_color\": [0, 0, 0, 1.0],\n                        \"width\": 1920,\n                        \"height\": 1080,\n                        \"scale_pixel_aspect\": True,\n                        \"bg_color\": [0, 0, 0, 0.0],\n                        \"letter_box\": {\n                            \"enabled\": False,\n                            \"ratio\": 0.0,\n                            \"fill_color\": [0, 0, 0, 1.0],\n                            \"line_thickness\": 0,\n                            \"line_color\": [255, 0, 0, 1.0]\n                        }\n                    },\n                    {\n                        \"name\": \"h264\",\n                        \"ext\": \"mp4\",\n                        \"tags\": [\n                            \"burnin\",\n                            \"ftrackreview\",\n                            \"kitsureview\"\n                        ],\n                        \"burnins\": [],\n                        \"ffmpeg_args\": {\n                            \"video_filters\": [],\n                            \"audio_filters\": [],\n                            \"input\": [\n                                \"-apply_trc gamma22\"\n                            ],\n                            \"output\": [\n                                \"-pix_fmt yuv420p\",\n                                \"-crf 18\",\n                                \"-intra\"\n                            ]\n                        },\n                        \"filter\": {\n                            \"families\": [\n                                \"render\",\n                                \"review\",\n                                \"ftrack\"\n                            ],\n                            \"product_names\": [],\n                            \"custom_tags\": [],\n                            \"single_frame_filter\": \"multi_frame\"\n                        },\n                        \"overscan_crop\": \"\",\n                        \"overscan_color\": [0, 0, 0, 1.0],\n                        \"width\": 0,\n                        \"height\": 0,\n                        \"scale_pixel_aspect\": True,\n                        \"bg_color\": [0, 0, 0, 0.0],\n                        \"letter_box\": {\n                            \"enabled\": False,\n                            \"ratio\": 0.0,\n                            \"fill_color\": [0, 0, 0, 1.0],\n                            \"line_thickness\": 0,\n                            \"line_color\": [255, 0, 0, 1.0]\n                        }\n                    }\n                ]\n            }\n        ]\n    },\n    \"ExtractBurnin\": {\n        \"enabled\": True,\n        \"options\": {\n            \"font_size\": 42,\n            \"font_color\": [255, 255, 255, 1.0],\n            \"bg_color\": [0, 0, 0, 0.5],\n            \"x_offset\": 5,\n            \"y_offset\": 5,\n            \"bg_padding\": 5,\n            \"font_filepath\": {\n                \"windows\": \"\",\n                \"darwin\": \"\",\n                \"linux\": \"\"\n            }\n        },\n        \"profiles\": [\n            {\n                \"product_types\": [],\n                \"hosts\": [],\n                \"task_types\": [],\n                \"task_names\": [],\n                \"product_names\": [],\n                \"burnins\": [\n                    {\n                        \"name\": \"burnin\",\n                        \"TOP_LEFT\": \"{yy}-{mm}-{dd}\",\n                        \"TOP_CENTERED\": \"\",\n                        \"TOP_RIGHT\": \"{anatomy[version]}\",\n                        \"BOTTOM_LEFT\": \"{username}\",\n                        \"BOTTOM_CENTERED\": \"{folder[name]}\",\n                        \"BOTTOM_RIGHT\": \"{frame_start}-{current_frame}-{frame_end}\",\n                        \"filter\": {\n                            \"families\": [],\n                            \"tags\": []\n                        }\n                    },\n                ]\n            },\n            {\n                \"product_types\": [\"review\"],\n                \"hosts\": [\n                    \"maya\",\n                    \"houdini\",\n                    \"max\"\n                ],\n                \"task_types\": [],\n                \"task_names\": [],\n                \"product_names\": [],\n                \"burnins\": [\n                    {\n                        \"name\": \"focal_length_burnin\",\n                        \"TOP_LEFT\": \"{yy}-{mm}-{dd}\",\n                        \"TOP_CENTERED\": \"{focalLength:.2f} mm\",\n                        \"TOP_RIGHT\": \"{anatomy[version]}\",\n                        \"BOTTOM_LEFT\": \"{username}\",\n                        \"BOTTOM_CENTERED\": \"{folder[name]}\",\n                        \"BOTTOM_RIGHT\": \"{frame_start}-{current_frame}-{frame_end}\",\n                        \"filter\": {\n                            \"families\": [],\n                            \"tags\": []\n                        }\n                    }\n                ]\n            }\n        ]\n    },\n    \"PreIntegrateThumbnails\": {\n        \"enabled\": True,\n        \"integrate_profiles\": []\n    },\n    \"IntegrateProductGroup\": {\n        \"product_grouping_profiles\": [\n            {\n                \"product_types\": [],\n                \"hosts\": [],\n                \"task_types\": [],\n                \"tasks\": [],\n                \"template\": \"\"\n            }\n        ]\n    },\n    \"IntegrateHeroVersion\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True,\n        \"families\": [\n            \"model\",\n            \"rig\",\n            \"look\",\n            \"pointcache\",\n            \"animation\",\n            \"setdress\",\n            \"layout\",\n            \"mayaScene\",\n            \"simpleUnrealTexture\"\n        ]\n    },\n    \"CleanUp\": {\n        \"paterns\": [],\n        \"remove_temp_renders\": False\n    },\n    \"CleanUpFarm\": {\n        \"enabled\": False\n    }\n}\n"
  },
  {
    "path": "server_addon/core/server/settings/tools.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    normalize_name,\n    ensure_unique_names,\n    task_types_enum,\n)\n\n\nclass ProductTypeSmartSelectModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField(\"\", title=\"Product type\")\n    task_names: list[str] = SettingsField(\n        default_factory=list, title=\"Task names\"\n    )\n\n    @validator(\"name\")\n    def normalize_value(cls, value):\n        return normalize_name(value)\n\n\nclass ProductNameProfile(BaseSettingsModel):\n    _layout = \"expanded\"\n    product_types: list[str] = SettingsField(\n        default_factory=list, title=\"Product types\"\n    )\n    hosts: list[str] = SettingsField(default_factory=list, title=\"Hosts\")\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    tasks: list[str] = SettingsField(default_factory=list, title=\"Task names\")\n    template: str = SettingsField(\"\", title=\"Template\")\n\n\nclass CreatorToolModel(BaseSettingsModel):\n    # TODO this was dynamic dictionary '{name: task_names}'\n    product_types_smart_select: list[ProductTypeSmartSelectModel] = (\n        SettingsField(\n            default_factory=list,\n            title=\"Create Smart Select\"\n        )\n    )\n    product_name_profiles: list[ProductNameProfile] = SettingsField(\n        default_factory=list,\n        title=\"Product name profiles\"\n    )\n\n    @validator(\"product_types_smart_select\")\n    def validate_unique_name(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass WorkfileTemplateProfile(BaseSettingsModel):\n    _layout = \"expanded\"\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    # TODO this should use hosts enum\n    hosts: list[str] = SettingsField(default_factory=list, title=\"Hosts\")\n    # TODO this was using project anatomy template name\n    workfile_template: str = SettingsField(\"\", title=\"Workfile template\")\n\n\nclass LastWorkfileOnStartupProfile(BaseSettingsModel):\n    _layout = \"expanded\"\n    # TODO this should use hosts enum\n    hosts: list[str] = SettingsField(default_factory=list, title=\"Hosts\")\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    tasks: list[str] = SettingsField(default_factory=list, title=\"Task names\")\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    use_last_published_workfile: bool = SettingsField(\n        True, title=\"Use last published workfile\"\n    )\n\n\nclass WorkfilesToolOnStartupProfile(BaseSettingsModel):\n    _layout = \"expanded\"\n    # TODO this should use hosts enum\n    hosts: list[str] = SettingsField(default_factory=list, title=\"Hosts\")\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    tasks: list[str] = SettingsField(default_factory=list, title=\"Task names\")\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n\n\nclass ExtraWorkFoldersProfile(BaseSettingsModel):\n    _layout = \"expanded\"\n    # TODO this should use hosts enum\n    hosts: list[str] = SettingsField(default_factory=list, title=\"Hosts\")\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    task_names: list[str] = SettingsField(\n        default_factory=list, title=\"Task names\"\n    )\n    folders: list[str] = SettingsField(default_factory=list, title=\"Folders\")\n\n\nclass WorkfilesLockProfile(BaseSettingsModel):\n    _layout = \"expanded\"\n    # TODO this should use hosts enum\n    host_names: list[str] = SettingsField(default_factory=list, title=\"Hosts\")\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n\n\nclass WorkfilesToolModel(BaseSettingsModel):\n    workfile_template_profiles: list[WorkfileTemplateProfile] = SettingsField(\n        default_factory=list,\n        title=\"Workfile template profiles\"\n    )\n    last_workfile_on_startup: list[LastWorkfileOnStartupProfile] = (\n        SettingsField(\n            default_factory=list,\n            title=\"Open last workfile on launch\"\n        )\n    )\n    open_workfile_tool_on_startup: list[WorkfilesToolOnStartupProfile] = (\n        SettingsField(\n            default_factory=list,\n            title=\"Open workfile tool on launch\"\n        )\n    )\n    extra_folders: list[ExtraWorkFoldersProfile] = SettingsField(\n        default_factory=list,\n        title=\"Extra work folders\"\n    )\n    workfile_lock_profiles: list[WorkfilesLockProfile] = SettingsField(\n        default_factory=list,\n        title=\"Workfile lock profiles\"\n    )\n\n\ndef _product_types_enum():\n    return [\n        \"action\",\n        \"animation\",\n        \"assembly\",\n        \"audio\",\n        \"backgroundComp\",\n        \"backgroundLayout\",\n        \"camera\",\n        \"editorial\",\n        \"gizmo\",\n        \"image\",\n        \"layout\",\n        \"look\",\n        \"matchmove\",\n        \"mayaScene\",\n        \"model\",\n        \"nukenodes\",\n        \"plate\",\n        \"pointcache\",\n        \"prerender\",\n        \"redshiftproxy\",\n        \"reference\",\n        \"render\",\n        \"review\",\n        \"rig\",\n        \"setdress\",\n        \"take\",\n        \"usdShade\",\n        \"vdbcache\",\n        \"vrayproxy\",\n        \"workfile\",\n        \"xgen\",\n        \"yetiRig\",\n        \"yeticache\"\n    ]\n\n\nclass LoaderProductTypeFilterProfile(BaseSettingsModel):\n    _layout = \"expanded\"\n    # TODO this should use hosts enum\n    hosts: list[str] = SettingsField(default_factory=list, title=\"Hosts\")\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    is_include: bool = SettingsField(True, title=\"Exclude / Include\")\n    filter_product_types: list[str] = SettingsField(\n        default_factory=list,\n        enum_resolver=_product_types_enum\n    )\n\n\nclass LoaderToolModel(BaseSettingsModel):\n    product_type_filter_profiles: list[LoaderProductTypeFilterProfile] = (\n        SettingsField(default_factory=list, title=\"Product type filtering\")\n    )\n\n\nclass PublishTemplateNameProfile(BaseSettingsModel):\n    _layout = \"expanded\"\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product types\"\n    )\n    # TODO this should use hosts enum\n    hosts: list[str] = SettingsField(default_factory=list, title=\"Hosts\")\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    task_names: list[str] = SettingsField(\n        default_factory=list, title=\"Task names\"\n    )\n    template_name: str = SettingsField(\"\", title=\"Template name\")\n\n\nclass CustomStagingDirProfileModel(BaseSettingsModel):\n    active: bool = SettingsField(True, title=\"Is active\")\n    hosts: list[str] = SettingsField(default_factory=list, title=\"Host names\")\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    task_names: list[str] = SettingsField(\n        default_factory=list, title=\"Task names\"\n    )\n    product_types: list[str] = SettingsField(\n        default_factory=list, title=\"Product types\"\n    )\n    product_names: list[str] = SettingsField(\n        default_factory=list, title=\"Product names\"\n    )\n    custom_staging_dir_persistent: bool = SettingsField(\n        False, title=\"Custom Staging Folder Persistent\"\n    )\n    template_name: str = SettingsField(\"\", title=\"Template Name\")\n\n\nclass PublishToolModel(BaseSettingsModel):\n    template_name_profiles: list[PublishTemplateNameProfile] = SettingsField(\n        default_factory=list,\n        title=\"Template name profiles\"\n    )\n    hero_template_name_profiles: list[PublishTemplateNameProfile] = (\n        SettingsField(\n            default_factory=list,\n            title=\"Hero template name profiles\"\n        )\n    )\n    custom_staging_dir_profiles: list[CustomStagingDirProfileModel] = (\n        SettingsField(\n            default_factory=list,\n            title=\"Custom Staging Dir Profiles\"\n        )\n    )\n\n\nclass GlobalToolsModel(BaseSettingsModel):\n    creator: CreatorToolModel = SettingsField(\n        default_factory=CreatorToolModel,\n        title=\"Creator\"\n    )\n    Workfiles: WorkfilesToolModel = SettingsField(\n        default_factory=WorkfilesToolModel,\n        title=\"Workfiles\"\n    )\n    loader: LoaderToolModel = SettingsField(\n        default_factory=LoaderToolModel,\n        title=\"Loader\"\n    )\n    publish: PublishToolModel = SettingsField(\n        default_factory=PublishToolModel,\n        title=\"Publish\"\n    )\n\n\nDEFAULT_TOOLS_VALUES = {\n    \"creator\": {\n        \"product_types_smart_select\": [\n            {\n                \"name\": \"Render\",\n                \"task_names\": [\n                    \"light\",\n                    \"render\"\n                ]\n            },\n            {\n                \"name\": \"Model\",\n                \"task_names\": [\n                    \"model\"\n                ]\n            },\n            {\n                \"name\": \"Layout\",\n                \"task_names\": [\n                    \"layout\"\n                ]\n            },\n            {\n                \"name\": \"Look\",\n                \"task_names\": [\n                    \"look\"\n                ]\n            },\n            {\n                \"name\": \"Rig\",\n                \"task_names\": [\n                    \"rigging\",\n                    \"rig\"\n                ]\n            }\n        ],\n        \"product_name_profiles\": [\n            {\n                \"product_types\": [],\n                \"hosts\": [],\n                \"task_types\": [],\n                \"tasks\": [],\n                \"template\": \"{product[type]}{variant}\"\n            },\n            {\n                \"product_types\": [\n                    \"workfile\"\n                ],\n                \"hosts\": [],\n                \"task_types\": [],\n                \"tasks\": [],\n                \"template\": \"{product[type]}{Task[name]}\"\n            },\n            {\n                \"product_types\": [\n                    \"render\"\n                ],\n                \"hosts\": [],\n                \"task_types\": [],\n                \"tasks\": [],\n                \"template\": \"{product[type]}{Task[name]}{Variant}\"\n            },\n            {\n                \"product_types\": [\n                    \"renderLayer\",\n                    \"renderPass\"\n                ],\n                \"hosts\": [\n                    \"tvpaint\"\n                ],\n                \"task_types\": [],\n                \"tasks\": [],\n                \"template\": \"{product[type]}{Task[name]}_{Renderlayer}_{Renderpass}\"\n            },\n            {\n                \"product_types\": [\n                    \"review\",\n                    \"workfile\"\n                ],\n                \"hosts\": [\n                    \"aftereffects\",\n                    \"tvpaint\"\n                ],\n                \"task_types\": [],\n                \"tasks\": [],\n                \"template\": \"{product[type]}{Task[name]}\"\n            },\n            {\n                \"product_types\": [\"render\"],\n                \"hosts\": [\n                    \"aftereffects\"\n                ],\n                \"task_types\": [],\n                \"tasks\": [],\n                \"template\": \"{product[type]}{Task[name]}{Composition}{Variant}\"\n            },\n            {\n                \"product_types\": [\n                    \"staticMesh\"\n                ],\n                \"hosts\": [\n                    \"maya\"\n                ],\n                \"task_types\": [],\n                \"tasks\": [],\n                \"template\": \"S_{folder[name]}{variant}\"\n            },\n            {\n                \"product_types\": [\n                    \"skeletalMesh\"\n                ],\n                \"hosts\": [\n                    \"maya\"\n                ],\n                \"task_types\": [],\n                \"tasks\": [],\n                \"template\": \"SK_{folder[name]}{variant}\"\n            }\n        ]\n    },\n    \"Workfiles\": {\n        \"workfile_template_profiles\": [\n            {\n                \"task_types\": [],\n                \"hosts\": [],\n                \"workfile_template\": \"work\"\n            },\n            {\n                \"task_types\": [],\n                \"hosts\": [\n                    \"unreal\"\n                ],\n                \"workfile_template\": \"work_unreal\"\n            }\n        ],\n        \"last_workfile_on_startup\": [\n            {\n                \"hosts\": [],\n                \"task_types\": [],\n                \"tasks\": [],\n                \"enabled\": True,\n                \"use_last_published_workfile\": False\n            }\n        ],\n        \"open_workfile_tool_on_startup\": [\n            {\n                \"hosts\": [],\n                \"task_types\": [],\n                \"tasks\": [],\n                \"enabled\": False\n            }\n        ],\n        \"extra_folders\": [],\n        \"workfile_lock_profiles\": []\n    },\n    \"loader\": {\n        \"product_type_filter_profiles\": [\n            {\n                \"hosts\": [],\n                \"task_types\": [],\n                \"is_include\": True,\n                \"filter_product_types\": []\n            }\n        ]\n    },\n    \"publish\": {\n        \"template_name_profiles\": [\n            {\n                \"product_types\": [],\n                \"hosts\": [],\n                \"task_types\": [],\n                \"task_names\": [],\n                \"template_name\": \"publish\"\n            },\n            {\n                \"product_types\": [\n                    \"review\",\n                    \"render\",\n                    \"prerender\"\n                ],\n                \"hosts\": [],\n                \"task_types\": [],\n                \"task_names\": [],\n                \"template_name\": \"publish_render\"\n            },\n            {\n                \"product_types\": [\n                    \"simpleUnrealTexture\"\n                ],\n                \"hosts\": [\n                    \"standalonepublisher\"\n                ],\n                \"task_types\": [],\n                \"task_names\": [],\n                \"template_name\": \"publish_simpleUnrealTexture\"\n            },\n            {\n                \"product_types\": [\n                    \"staticMesh\",\n                    \"skeletalMesh\"\n                ],\n                \"hosts\": [\n                    \"maya\"\n                ],\n                \"task_types\": [],\n                \"task_names\": [],\n                \"template_name\": \"publish_maya2unreal\"\n            },\n            {\n                \"product_types\": [\n                    \"online\"\n                ],\n                \"hosts\": [\n                    \"traypublisher\"\n                ],\n                \"task_types\": [],\n                \"task_names\": [],\n                \"template_name\": \"publish_online\"\n            },\n            {\n                \"product_types\": [\n                    \"tycache\"\n                ],\n                \"hosts\": [\n                    \"max\"\n                ],\n                \"task_types\": [],\n                \"task_names\": [],\n                \"template_name\": \"publish_tycache\"\n            }\n        ],\n        \"hero_template_name_profiles\": [\n            {\n                \"product_types\": [\n                    \"simpleUnrealTexture\"\n                ],\n                \"hosts\": [\n                    \"standalonepublisher\"\n                ],\n                \"task_types\": [],\n                \"task_names\": [],\n                \"template_name\": \"hero_simpleUnrealTextureHero\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "server_addon/core/server/version.py",
    "content": "__version__ = \"0.1.5\"\n"
  },
  {
    "path": "server_addon/create_ayon_addons.py",
    "content": "import os\nimport sys\nimport re\nimport json\nimport shutil\nimport argparse\nimport zipfile\nimport platform\nimport collections\nfrom pathlib import Path\nfrom typing import Any, Optional, Iterable, Pattern, List, Tuple\n\n# Patterns of directories to be skipped for server part of addon\nIGNORE_DIR_PATTERNS: List[Pattern] = [\n    re.compile(pattern)\n    for pattern in {\n        # Skip directories starting with '.'\n        r\"^\\.\",\n        # Skip any pycache folders\n        \"^__pycache__$\"\n    }\n]\n\n# Patterns of files to be skipped for server part of addon\nIGNORE_FILE_PATTERNS: List[Pattern] = [\n    re.compile(pattern)\n    for pattern in {\n        # Skip files starting with '.'\n        # NOTE this could be an issue in some cases\n        r\"^\\.\",\n        # Skip '.pyc' files\n        r\"\\.pyc$\"\n    }\n]\n\nIGNORED_HOSTS = [\n    \"flame\",\n    \"harmony\",\n]\n\nIGNORED_MODULES = [\n    \"ftrack\",\n    \"shotgrid\",\n    \"sync_server\",\n    \"example_addons\",\n    \"slack\",\n    \"kitsu\",\n]\n\n\nclass ZipFileLongPaths(zipfile.ZipFile):\n    \"\"\"Allows longer paths in zip files.\n\n    Regular DOS paths are limited to MAX_PATH (260) characters, including\n    the string's terminating NUL character.\n    That limit can be exceeded by using an extended-length path that\n    starts with the '\\\\?\\' prefix.\n    \"\"\"\n    _is_windows = platform.system().lower() == \"windows\"\n\n    def _extract_member(self, member, tpath, pwd):\n        if self._is_windows:\n            tpath = os.path.abspath(tpath)\n            if tpath.startswith(\"\\\\\\\\\"):\n                tpath = \"\\\\\\\\?\\\\UNC\\\\\" + tpath[2:]\n            else:\n                tpath = \"\\\\\\\\?\\\\\" + tpath\n\n        return super(ZipFileLongPaths, self)._extract_member(\n            member, tpath, pwd\n        )\n\n\ndef _value_match_regexes(value: str, regexes: Iterable[Pattern]) -> bool:\n    return any(\n        regex.search(value)\n        for regex in regexes\n    )\n\n\ndef find_files_in_subdir(\n    src_path: str,\n    ignore_file_patterns: Optional[List[Pattern]] = None,\n    ignore_dir_patterns: Optional[List[Pattern]] = None,\n    ignore_subdirs: Optional[Iterable[Tuple[str]]] = None\n):\n    \"\"\"Find all files to copy in subdirectories of given path.\n\n    All files that match any of the patterns in 'ignore_file_patterns' will\n        be skipped and any directories that match any of the patterns in\n        'ignore_dir_patterns' will be skipped with all subfiles.\n\n    Args:\n        src_path (str): Path to directory to search in.\n        ignore_file_patterns (Optional[List[Pattern]]): List of regexes\n            to match files to ignore.\n        ignore_dir_patterns (Optional[List[Pattern]]): List of regexes\n            to match directories to ignore.\n        ignore_subdirs (Optional[Iterable[Tuple[str]]]): List of\n            subdirectories to ignore.\n\n    Returns:\n        List[Tuple[str, str]]: List of tuples with path to file and parent\n            directories relative to 'src_path'.\n    \"\"\"\n\n    if ignore_file_patterns is None:\n        ignore_file_patterns = IGNORE_FILE_PATTERNS\n\n    if ignore_dir_patterns is None:\n        ignore_dir_patterns = IGNORE_DIR_PATTERNS\n    output: list[tuple[str, str]] = []\n\n    hierarchy_queue = collections.deque()\n    hierarchy_queue.append((src_path, []))\n    while hierarchy_queue:\n        item: tuple[str, str] = hierarchy_queue.popleft()\n        dirpath, parents = item\n        if ignore_subdirs and parents in ignore_subdirs:\n            continue\n        for name in os.listdir(dirpath):\n            path = os.path.join(dirpath, name)\n            if os.path.isfile(path):\n                if not _value_match_regexes(name, ignore_file_patterns):\n                    items = list(parents)\n                    items.append(name)\n                    output.append((path, os.path.sep.join(items)))\n                continue\n\n            if not _value_match_regexes(name, ignore_dir_patterns):\n                items = list(parents)\n                items.append(name)\n                hierarchy_queue.append((path, items))\n\n    return output\n\n\ndef read_addon_version(version_path: Path) -> str:\n    # Read version\n    version_content: dict[str, Any] = {}\n    with open(str(version_path), \"r\") as stream:\n        exec(stream.read(), version_content)\n    return version_content[\"__version__\"]\n\n\ndef get_addon_version(addon_dir: Path) -> str:\n    return read_addon_version(addon_dir / \"server\" / \"version.py\")\n\n\ndef create_addon_zip(\n    output_dir: Path,\n    addon_name: str,\n    addon_version: str,\n    keep_source: bool\n):\n    zip_filepath = output_dir / f\"{addon_name}-{addon_version}.zip\"\n    addon_output_dir = output_dir / addon_name / addon_version\n    with ZipFileLongPaths(zip_filepath, \"w\", zipfile.ZIP_DEFLATED) as zipf:\n        zipf.writestr(\n            \"manifest.json\",\n            json.dumps({\n                \"addon_name\": addon_name,\n                \"addon_version\": addon_version\n            })\n        )\n        # Add client code content to zip\n        src_root = os.path.normpath(str(addon_output_dir.absolute()))\n        src_root_offset = len(src_root) + 1\n        for root, _, filenames in os.walk(str(addon_output_dir)):\n            rel_root = \"\"\n            if root != src_root:\n                rel_root = root[src_root_offset:]\n\n            for filename in filenames:\n                src_path = os.path.join(root, filename)\n                if rel_root:\n                    dst_path = os.path.join(\"addon\", rel_root, filename)\n                else:\n                    dst_path = os.path.join(\"addon\", filename)\n                zipf.write(src_path, dst_path)\n\n    if not keep_source:\n        shutil.rmtree(str(output_dir / addon_name))\n\n\ndef create_openpype_package(\n    addon_dir: Path,\n    output_dir: Path,\n    root_dir: Path,\n    create_zip: bool,\n    keep_source: bool\n):\n    server_dir = addon_dir / \"server\"\n    pyproject_path = addon_dir / \"client\" / \"pyproject.toml\"\n\n    openpype_dir = root_dir / \"openpype\"\n    version_path = openpype_dir / \"version.py\"\n    addon_version = read_addon_version(version_path)\n\n    addon_output_dir = output_dir / \"openpype\" / addon_version\n    private_dir = addon_output_dir / \"private\"\n    if addon_output_dir.exists():\n        shutil.rmtree(str(addon_output_dir))\n\n    # Make sure dir exists\n    addon_output_dir.mkdir(parents=True, exist_ok=True)\n    private_dir.mkdir(parents=True, exist_ok=True)\n\n    # Copy version\n    shutil.copy(str(version_path), str(addon_output_dir))\n    for subitem in server_dir.iterdir():\n        shutil.copy(str(subitem), str(addon_output_dir / subitem.name))\n\n    # Copy pyproject.toml\n    shutil.copy(\n        str(pyproject_path),\n        (private_dir / pyproject_path.name)\n    )\n    # Subdirs that won't be added to output zip file\n    ignored_subpaths = [\n        [\"addons\"],\n        [\"vendor\", \"common\", \"ayon_api\"],\n    ]\n    ignored_subpaths.extend(\n        [\"hosts\", host_name]\n        for host_name in IGNORED_HOSTS\n    )\n    ignored_subpaths.extend(\n        [\"modules\", module_name]\n        for module_name in IGNORED_MODULES\n    )\n\n    # Zip client\n    zip_filepath = private_dir / \"client.zip\"\n    with ZipFileLongPaths(zip_filepath, \"w\", zipfile.ZIP_DEFLATED) as zipf:\n        # Add client code content to zip\n        for path, sub_path in find_files_in_subdir(\n            str(openpype_dir), ignore_subdirs=ignored_subpaths\n        ):\n            zipf.write(path, f\"{openpype_dir.name}/{sub_path}\")\n\n    if create_zip:\n        create_addon_zip(output_dir, \"openpype\", addon_version, keep_source)\n\n\ndef create_addon_package(\n    addon_dir: Path,\n    output_dir: Path,\n    create_zip: bool,\n    keep_source: bool\n):\n    server_dir = addon_dir / \"server\"\n    addon_version = get_addon_version(addon_dir)\n\n    addon_output_dir = output_dir / addon_dir.name / addon_version\n    if addon_output_dir.exists():\n        shutil.rmtree(str(addon_output_dir))\n    addon_output_dir.mkdir(parents=True)\n\n    # Copy server content\n    src_root = os.path.normpath(str(server_dir.absolute()))\n    src_root_offset = len(src_root) + 1\n    for root, _, filenames in os.walk(str(server_dir)):\n        dst_root = addon_output_dir\n        if root != src_root:\n            rel_root = root[src_root_offset:]\n            dst_root = dst_root / rel_root\n\n        dst_root.mkdir(parents=True, exist_ok=True)\n        for filename in filenames:\n            src_path = os.path.join(root, filename)\n            shutil.copy(src_path, str(dst_root))\n\n    if create_zip:\n        create_addon_zip(\n            output_dir, addon_dir.name, addon_version, keep_source\n        )\n\n\ndef main(\n    output_dir=None,\n    skip_zip=True,\n    keep_source=False,\n    clear_output_dir=False,\n    addons=None,\n):\n    current_dir = Path(os.path.dirname(os.path.abspath(__file__)))\n    root_dir = current_dir.parent\n    create_zip = not skip_zip\n\n    if output_dir:\n        output_dir = Path(output_dir)\n    else:\n        output_dir = current_dir / \"packages\"\n\n    if output_dir.exists() and clear_output_dir:\n        shutil.rmtree(str(output_dir))\n\n    print(\"Package creation started...\")\n    print(f\"Output directory: {output_dir}\")\n\n    # Make sure output dir is created\n    output_dir.mkdir(parents=True, exist_ok=True)\n    ignored_addons = set(IGNORED_HOSTS) | set(IGNORED_MODULES)\n    for addon_dir in current_dir.iterdir():\n        if not addon_dir.is_dir():\n            continue\n\n        if addons and addon_dir.name not in addons:\n            continue\n\n        if addon_dir.name in ignored_addons:\n            continue\n\n        server_dir = addon_dir / \"server\"\n        if not server_dir.exists():\n            continue\n\n        if addon_dir.name == \"openpype\":\n            create_openpype_package(\n                addon_dir, output_dir, root_dir, create_zip, keep_source\n            )\n\n        else:\n            create_addon_package(\n                addon_dir, output_dir, create_zip, keep_source\n            )\n\n        print(f\"- package '{addon_dir.name}' created\")\n    print(f\"Package creation finished. Output directory: {output_dir}\")\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\n        \"--skip-zip\",\n        dest=\"skip_zip\",\n        action=\"store_true\",\n        help=(\n            \"Skip zipping server package and create only\"\n            \" server folder structure.\"\n        )\n    )\n    parser.add_argument(\n        \"--keep-sources\",\n        dest=\"keep_sources\",\n        action=\"store_true\",\n        help=(\n            \"Keep folder structure when server package is created.\"\n        )\n    )\n    parser.add_argument(\n        \"-o\", \"--output\",\n        dest=\"output_dir\",\n        default=None,\n        help=(\n            \"Directory path where package will be created\"\n            \" (Will be purged if already exists!)\"\n        )\n    )\n    parser.add_argument(\n        \"-c\", \"--clear-output-dir\",\n        dest=\"clear_output_dir\",\n        action=\"store_true\",\n        help=(\n            \"Clear output directory before package creation.\"\n        )\n    )\n    parser.add_argument(\n        \"-a\",\n        \"--addon\",\n        dest=\"addons\",\n        action=\"append\",\n        help=\"Limit addon creation to given addon name\",\n    )\n\n    args = parser.parse_args(sys.argv[1:])\n    main(\n        args.output_dir,\n        args.skip_zip,\n        args.keep_sources,\n        args.clear_output_dir,\n        args.addons,\n    )\n"
  },
  {
    "path": "server_addon/deadline/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import DeadlineSettings, DEFAULT_VALUES\n\n\nclass Deadline(BaseServerAddon):\n    name = \"deadline\"\n    title = \"Deadline\"\n    version = __version__\n    settings_model: Type[DeadlineSettings] = DeadlineSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/deadline/server/settings/__init__.py",
    "content": "from .main import (\n    DeadlineSettings,\n    DEFAULT_VALUES,\n)\n\n\n__all__ = (\n    \"DeadlineSettings\",\n    \"DEFAULT_VALUES\",\n)\n"
  },
  {
    "path": "server_addon/deadline/server/settings/main.py",
    "content": "from pydantic import validator\n\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n)\n\nfrom .publish_plugins import (\n    PublishPluginsModel,\n    DEFAULT_DEADLINE_PLUGINS_SETTINGS\n)\n\n\nclass ServerListSubmodel(BaseSettingsModel):\n    _layout = \"compact\"\n    name: str = SettingsField(title=\"Name\")\n    value: str = SettingsField(title=\"Value\")\n\n\nasync def defined_deadline_ws_name_enum_resolver(\n    addon: \"BaseServerAddon\",\n    settings_variant: str = \"production\",\n    project_name: str | None = None,\n) -> list[str]:\n    \"\"\"Provides list of names of configured Deadline webservice urls.\"\"\"\n    if addon is None:\n        return []\n\n    settings = await addon.get_studio_settings(variant=settings_variant)\n\n    ws_urls = []\n    for deadline_url_item in settings.deadline_urls:\n        ws_urls.append(deadline_url_item.name)\n\n    return ws_urls\n\n\nclass DeadlineSettings(BaseSettingsModel):\n    deadline_urls: list[ServerListSubmodel] = SettingsField(\n        default_factory=list,\n        title=\"System Deadline Webservice URLs\",\n        scope=[\"studio\"],\n    )\n    deadline_server: str = SettingsField(\n        title=\"Project deadline server\",\n        section=\"---\",\n        scope=[\"project\"],\n        enum_resolver=defined_deadline_ws_name_enum_resolver\n    )\n    publish: PublishPluginsModel = SettingsField(\n        default_factory=PublishPluginsModel,\n        title=\"Publish Plugins\",\n    )\n\n    @validator(\"deadline_urls\")\n    def validate_unique_names(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nDEFAULT_VALUES = {\n    \"deadline_urls\": [\n        {\n            \"name\": \"default\",\n            \"value\": \"http://127.0.0.1:8082\"\n        }\n    ],\n    \"deadline_server\": \"default\",\n    \"publish\": DEFAULT_DEADLINE_PLUGINS_SETTINGS\n}\n"
  },
  {
    "path": "server_addon/deadline/server/settings/publish_plugins.py",
    "content": "from pydantic import validator\n\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n)\n\n\nclass CollectDeadlinePoolsModel(BaseSettingsModel):\n    \"\"\"Settings Deadline default pools.\"\"\"\n\n    primary_pool: str = SettingsField(title=\"Primary Pool\")\n\n    secondary_pool: str = SettingsField(title=\"Secondary Pool\")\n\n\nclass ValidateExpectedFilesModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    active: bool = SettingsField(True, title=\"Active\")\n    allow_user_override: bool = SettingsField(\n        True, title=\"Allow user change frame range\"\n    )\n    families: list[str] = SettingsField(\n        default_factory=list, title=\"Trigger on families\"\n    )\n    targets: list[str] = SettingsField(\n        default_factory=list, title=\"Trigger for plugins\"\n    )\n\n\ndef tile_assembler_enum():\n    \"\"\"Return a list of value/label dicts for the enumerator.\n\n    Returning a list of dicts is used to allow for a custom label to be\n    displayed in the UI.\n    \"\"\"\n    return [\n        {\n            \"value\": \"DraftTileAssembler\",\n            \"label\": \"Draft Tile Assembler\"\n        },\n        {\n            \"value\": \"OpenPypeTileAssembler\",\n            \"label\": \"Open Image IO\"\n        }\n    ]\n\n\nclass ScenePatchesSubmodel(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField(title=\"Patch name\")\n    regex: str = SettingsField(title=\"Patch regex\")\n    line: str = SettingsField(title=\"Patch line\")\n\n\nclass MayaSubmitDeadlineModel(BaseSettingsModel):\n    \"\"\"Maya deadline submitter settings.\"\"\"\n\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    use_published: bool = SettingsField(title=\"Use Published scene\")\n    import_reference: bool = SettingsField(\n        title=\"Use Scene with Imported Reference\"\n    )\n    asset_dependencies: bool = SettingsField(title=\"Use Asset dependencies\")\n    priority: int = SettingsField(title=\"Priority\")\n    tile_priority: int = SettingsField(title=\"Tile Priority\")\n    group: str = SettingsField(title=\"Group\")\n    limit: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Limit Groups\"\n    )\n    tile_assembler_plugin: str = SettingsField(\n        title=\"Tile Assembler Plugin\",\n        enum_resolver=tile_assembler_enum,\n    )\n    jobInfo: str = SettingsField(\n        title=\"Additional JobInfo data\",\n        widget=\"textarea\",\n    )\n    pluginInfo: str = SettingsField(\n        title=\"Additional PluginInfo data\",\n        widget=\"textarea\",\n    )\n\n    scene_patches: list[ScenePatchesSubmodel] = SettingsField(\n        default_factory=list,\n        title=\"Scene patches\",\n    )\n    strict_error_checking: bool = SettingsField(\n        title=\"Disable Strict Error Check profiles\"\n    )\n\n    @validator(\"scene_patches\")\n    def validate_unique_names(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass MaxSubmitDeadlineModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    use_published: bool = SettingsField(title=\"Use Published scene\")\n    priority: int = SettingsField(title=\"Priority\")\n    chunk_size: int = SettingsField(title=\"Frame per Task\")\n    group: str = SettingsField(\"\", title=\"Group Name\")\n\n\nclass EnvSearchReplaceSubmodel(BaseSettingsModel):\n    _layout = \"compact\"\n    name: str = SettingsField(title=\"Name\")\n    value: str = SettingsField(title=\"Value\")\n\n\nclass LimitGroupsSubmodel(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField(title=\"Name\")\n    value: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Limit Groups\"\n    )\n\n\ndef fusion_deadline_plugin_enum():\n    \"\"\"Return a list of value/label dicts for the enumerator.\n\n    Returning a list of dicts is used to allow for a custom label to be\n    displayed in the UI.\n    \"\"\"\n    return [\n        {\n            \"value\": \"Fusion\",\n            \"label\": \"Fusion\"\n        },\n        {\n            \"value\": \"FusionCmd\",\n            \"label\": \"FusionCmd\"\n        }\n    ]\n\n\nclass FusionSubmitDeadlineModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    optional: bool = SettingsField(False, title=\"Optional\")\n    active: bool = SettingsField(True, title=\"Active\")\n    priority: int = SettingsField(50, title=\"Priority\")\n    chunk_size: int = SettingsField(10, title=\"Frame per Task\")\n    concurrent_tasks: int = SettingsField(\n        1, title=\"Number of concurrent tasks\"\n    )\n    group: str = SettingsField(\"\", title=\"Group Name\")\n    plugin: str = SettingsField(\"Fusion\",\n                        enum_resolver=fusion_deadline_plugin_enum,\n                        title=\"Deadline Plugin\")\n\n\nclass NukeSubmitDeadlineModel(BaseSettingsModel):\n    \"\"\"Nuke deadline submitter settings.\"\"\"\n\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    priority: int = SettingsField(title=\"Priority\")\n    chunk_size: int = SettingsField(title=\"Chunk Size\")\n    concurrent_tasks: int = SettingsField(title=\"Number of concurrent tasks\")\n    group: str = SettingsField(title=\"Group\")\n    department: str = SettingsField(title=\"Department\")\n    use_gpu: bool = SettingsField(title=\"Use GPU\")\n    workfile_dependency: bool = SettingsField(title=\"Workfile Dependency\")\n    use_published_workfile: bool = SettingsField(\n        title=\"Use Published Workfile\"\n    )\n\n    env_allowed_keys: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Allowed environment keys\"\n    )\n\n    env_search_replace_values: list[EnvSearchReplaceSubmodel] = SettingsField(\n        default_factory=list,\n        title=\"Search & replace in environment values\",\n    )\n\n    limit_groups: list[LimitGroupsSubmodel] = SettingsField(\n        default_factory=list,\n        title=\"Limit Groups\",\n    )\n\n    @validator(\n        \"limit_groups\",\n        \"env_allowed_keys\",\n        \"env_search_replace_values\")\n    def validate_unique_names(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass HarmonySubmitDeadlineModel(BaseSettingsModel):\n    \"\"\"Harmony deadline submitter settings.\"\"\"\n\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    use_published: bool = SettingsField(title=\"Use Published scene\")\n    priority: int = SettingsField(title=\"Priority\")\n    chunk_size: int = SettingsField(title=\"Chunk Size\")\n    group: str = SettingsField(title=\"Group\")\n    department: str = SettingsField(title=\"Department\")\n\n\nclass HoudiniSubmitDeadlineModel(BaseSettingsModel):\n    \"\"\"Houdini deadline render submitter settings.\"\"\"\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n\n    priority: int = SettingsField(title=\"Priority\")\n    chunk_size: int = SettingsField(title=\"Chunk Size\")\n    group: str = SettingsField(title=\"Group\")\n\n    export_priority: int = SettingsField(title=\"Export Priority\")\n    export_chunk_size: int = SettingsField(title=\"Export Chunk Size\")\n    export_group: str = SettingsField(title=\"Export Group\")\n\n\nclass HoudiniCacheSubmitDeadlineModel(BaseSettingsModel):\n    \"\"\"Houdini deadline cache submitter settings.\"\"\"\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n\n    priority: int = SettingsField(title=\"Priority\")\n    chunk_size: int = SettingsField(title=\"Chunk Size\")\n    group: str = SettingsField(title=\"Group\")\n\n\nclass AfterEffectsSubmitDeadlineModel(BaseSettingsModel):\n    \"\"\"After Effects deadline submitter settings.\"\"\"\n\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    use_published: bool = SettingsField(title=\"Use Published scene\")\n    priority: int = SettingsField(title=\"Priority\")\n    chunk_size: int = SettingsField(title=\"Chunk Size\")\n    group: str = SettingsField(title=\"Group\")\n    department: str = SettingsField(title=\"Department\")\n    multiprocess: bool = SettingsField(title=\"Optional\")\n\n\nclass CelactionSubmitDeadlineModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    deadline_department: str = SettingsField(\"\", title=\"Deadline apartment\")\n    deadline_priority: int = SettingsField(50, title=\"Deadline priority\")\n    deadline_pool: str = SettingsField(\"\", title=\"Deadline pool\")\n    deadline_pool_secondary: str = SettingsField(\n        \"\", title=\"Deadline pool (secondary)\"\n    )\n    deadline_group: str = SettingsField(\"\", title=\"Deadline Group\")\n    deadline_chunk_size: int = SettingsField(10, title=\"Deadline Chunk size\")\n    deadline_job_delay: str = SettingsField(\n        \"\", title=\"Delay job (timecode dd:hh:mm:ss)\"\n    )\n\n\nclass BlenderSubmitDeadlineModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    use_published: bool = SettingsField(title=\"Use Published scene\")\n    priority: int = SettingsField(title=\"Priority\")\n    chunk_size: int = SettingsField(title=\"Frame per Task\")\n    group: str = SettingsField(\"\", title=\"Group Name\")\n    job_delay: str = SettingsField(\n        \"\", title=\"Delay job (timecode dd:hh:mm:ss)\"\n    )\n\n\nclass AOVFilterSubmodel(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField(title=\"Host\")\n    value: list[str] = SettingsField(\n        default_factory=list,\n        title=\"AOV regex\"\n    )\n\n\nclass ProcessCacheJobFarmModel(BaseSettingsModel):\n    \"\"\"Process submitted job on farm.\"\"\"\n\n    enabled: bool = SettingsField(title=\"Enabled\")\n    deadline_department: str = SettingsField(title=\"Department\")\n    deadline_pool: str = SettingsField(title=\"Pool\")\n    deadline_group: str = SettingsField(title=\"Group\")\n    deadline_chunk_size: int = SettingsField(title=\"Chunk Size\")\n    deadline_priority: int = SettingsField(title=\"Priority\")\n\n\nclass ProcessSubmittedJobOnFarmModel(BaseSettingsModel):\n    \"\"\"Process submitted job on farm.\"\"\"\n\n    enabled: bool = SettingsField(title=\"Enabled\")\n    deadline_department: str = SettingsField(title=\"Department\")\n    deadline_pool: str = SettingsField(title=\"Pool\")\n    deadline_group: str = SettingsField(title=\"Group\")\n    deadline_chunk_size: int = SettingsField(title=\"Chunk Size\")\n    deadline_priority: int = SettingsField(title=\"Priority\")\n    publishing_script: str = SettingsField(title=\"Publishing script path\")\n    skip_integration_repre_list: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Skip integration of representation with ext\"\n    )\n    aov_filter: list[AOVFilterSubmodel] = SettingsField(\n        default_factory=list,\n        title=\"Reviewable products filter\",\n    )\n\n    @validator(\"aov_filter\")\n    def validate_unique_names(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass PublishPluginsModel(BaseSettingsModel):\n    CollectDeadlinePools: CollectDeadlinePoolsModel = SettingsField(\n        default_factory=CollectDeadlinePoolsModel,\n        title=\"Default Pools\")\n    ValidateExpectedFiles: ValidateExpectedFilesModel = SettingsField(\n        default_factory=ValidateExpectedFilesModel,\n        title=\"Validate Expected Files\"\n    )\n    MayaSubmitDeadline: MayaSubmitDeadlineModel = SettingsField(\n        default_factory=MayaSubmitDeadlineModel,\n        title=\"Maya Submit to deadline\")\n    HoudiniCacheSubmitDeadline: HoudiniCacheSubmitDeadlineModel = SettingsField(  # noqa\n        default_factory=HoudiniCacheSubmitDeadlineModel,\n        title=\"Houdini Submit cache to deadline\")\n    HoudiniSubmitDeadline: HoudiniSubmitDeadlineModel = SettingsField(\n        default_factory=HoudiniSubmitDeadlineModel,\n        title=\"Houdini Submit render to deadline\")\n    MaxSubmitDeadline: MaxSubmitDeadlineModel = SettingsField(\n        default_factory=MaxSubmitDeadlineModel,\n        title=\"Max Submit to deadline\")\n    FusionSubmitDeadline: FusionSubmitDeadlineModel = SettingsField(\n        default_factory=FusionSubmitDeadlineModel,\n        title=\"Fusion submit to Deadline\")\n    NukeSubmitDeadline: NukeSubmitDeadlineModel = SettingsField(\n        default_factory=NukeSubmitDeadlineModel,\n        title=\"Nuke Submit to deadline\")\n    HarmonySubmitDeadline: HarmonySubmitDeadlineModel = SettingsField(\n        default_factory=HarmonySubmitDeadlineModel,\n        title=\"Harmony Submit to deadline\")\n    AfterEffectsSubmitDeadline: AfterEffectsSubmitDeadlineModel = (\n        SettingsField(\n            default_factory=AfterEffectsSubmitDeadlineModel,\n            title=\"After Effects to deadline\"\n        )\n    )\n    CelactionSubmitDeadline: CelactionSubmitDeadlineModel = SettingsField(\n        default_factory=CelactionSubmitDeadlineModel,\n        title=\"Celaction Submit Deadline\")\n    BlenderSubmitDeadline: BlenderSubmitDeadlineModel = SettingsField(\n        default_factory=BlenderSubmitDeadlineModel,\n        title=\"Blender Submit Deadline\")\n    ProcessSubmittedCacheJobOnFarm: ProcessCacheJobFarmModel = SettingsField(\n        default_factory=ProcessCacheJobFarmModel,\n        title=\"Process submitted cache Job on farm.\")\n    ProcessSubmittedJobOnFarm: ProcessSubmittedJobOnFarmModel = SettingsField(\n        default_factory=ProcessSubmittedJobOnFarmModel,\n        title=\"Process submitted job on farm.\")\n\n\nDEFAULT_DEADLINE_PLUGINS_SETTINGS = {\n    \"CollectDeadlinePools\": {\n        \"primary_pool\": \"\",\n        \"secondary_pool\": \"\"\n    },\n    \"ValidateExpectedFiles\": {\n        \"enabled\": True,\n        \"active\": True,\n        \"allow_user_override\": True,\n        \"families\": [\n            \"render\"\n        ],\n        \"targets\": [\n            \"deadline\"\n        ]\n    },\n    \"MayaSubmitDeadline\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True,\n        \"tile_assembler_plugin\": \"DraftTileAssembler\",\n        \"use_published\": True,\n        \"import_reference\": False,\n        \"asset_dependencies\": True,\n        \"strict_error_checking\": True,\n        \"priority\": 50,\n        \"tile_priority\": 50,\n        \"group\": \"none\",\n        \"limit\": [],\n        # this used to be empty dict\n        \"jobInfo\": \"\",\n        # this used to be empty dict\n        \"pluginInfo\": \"\",\n        \"scene_patches\": []\n    },\n    \"HoudiniCacheSubmitDeadline\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True,\n        \"priority\": 50,\n        \"chunk_size\": 999999,\n        \"group\": \"\"\n    },\n    \"HoudiniSubmitDeadline\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True,\n        \"priority\": 50,\n        \"chunk_size\": 1,\n        \"group\": \"\",\n        \"export_priority\": 50,\n        \"export_chunk_size\": 10,\n        \"export_group\": \"\"\n    },\n    \"MaxSubmitDeadline\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True,\n        \"use_published\": True,\n        \"priority\": 50,\n        \"chunk_size\": 10,\n        \"group\": \"none\"\n    },\n    \"FusionSubmitDeadline\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True,\n        \"priority\": 50,\n        \"chunk_size\": 10,\n        \"concurrent_tasks\": 1,\n        \"group\": \"\"\n    },\n    \"NukeSubmitDeadline\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True,\n        \"priority\": 50,\n        \"chunk_size\": 10,\n        \"concurrent_tasks\": 1,\n        \"group\": \"\",\n        \"department\": \"\",\n        \"use_gpu\": True,\n        \"workfile_dependency\": True,\n        \"use_published_workfile\": True,\n        \"env_allowed_keys\": [],\n        \"env_search_replace_values\": [],\n        \"limit_groups\": []\n    },\n    \"HarmonySubmitDeadline\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True,\n        \"use_published\": True,\n        \"priority\": 50,\n        \"chunk_size\": 10000,\n        \"group\": \"\",\n        \"department\": \"\"\n    },\n    \"AfterEffectsSubmitDeadline\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True,\n        \"use_published\": True,\n        \"priority\": 50,\n        \"chunk_size\": 10000,\n        \"group\": \"\",\n        \"department\": \"\",\n        \"multiprocess\": True\n    },\n    \"CelactionSubmitDeadline\": {\n        \"enabled\": True,\n        \"deadline_department\": \"\",\n        \"deadline_priority\": 50,\n        \"deadline_pool\": \"\",\n        \"deadline_pool_secondary\": \"\",\n        \"deadline_group\": \"\",\n        \"deadline_chunk_size\": 10,\n        \"deadline_job_delay\": \"00:00:00:00\"\n    },\n    \"BlenderSubmitDeadline\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True,\n        \"use_published\": True,\n        \"priority\": 50,\n        \"chunk_size\": 10,\n        \"group\": \"none\",\n        \"job_delay\": \"00:00:00:00\"\n    },\n    \"ProcessSubmittedCacheJobOnFarm\": {\n        \"enabled\": True,\n        \"deadline_department\": \"\",\n        \"deadline_pool\": \"\",\n        \"deadline_group\": \"\",\n        \"deadline_chunk_size\": 1,\n        \"deadline_priority\": 50\n    },\n    \"ProcessSubmittedJobOnFarm\": {\n        \"enabled\": True,\n        \"deadline_department\": \"\",\n        \"deadline_pool\": \"\",\n        \"deadline_group\": \"\",\n        \"deadline_chunk_size\": 1,\n        \"deadline_priority\": 50,\n        \"publishing_script\": \"\",\n        \"skip_integration_repre_list\": [],\n        \"aov_filter\": [\n            {\n                \"name\": \"maya\",\n                \"value\": [\n                    \".*([Bb]eauty).*\"\n                ]\n            },\n            {\n                \"name\": \"blender\",\n                \"value\": [\n                    \".*([Bb]eauty).*\"\n                ]\n            },\n            {\n                \"name\": \"aftereffects\",\n                \"value\": [\n                    \".*\"\n                ]\n            },\n            {\n                \"name\": \"celaction\",\n                \"value\": [\n                    \".*\"\n                ]\n            },\n            {\n                \"name\": \"harmony\",\n                \"value\": [\n                    \".*\"\n                ]\n            },\n            {\n                \"name\": \"max\",\n                \"value\": [\n                    \".*\"\n                ]\n            },\n            {\n                \"name\": \"fusion\",\n                \"value\": [\n                    \".*\"\n                ]\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "server_addon/deadline/server/version.py",
    "content": "__version__ = \"0.1.9\"\n"
  },
  {
    "path": "server_addon/equalizer/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2023 YNPUT, s.r.o.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "server_addon/equalizer/README.md",
    "content": "3DEqualizer Integration for AYON\n===============================\n\nThis is addon for AYON that allows to integrate 3DEqualizer with AYON.\n"
  },
  {
    "path": "server_addon/equalizer/server/__init__.py",
    "content": "from ayon_server.addons import BaseServerAddon\n\nfrom .settings import EqualizerSettings, DEFAULT_EQUALIZER_SETTING\nfrom .version import __version__\n\n\nclass Equalizer(BaseServerAddon):\n    name = \"equalizer\"\n    title = \"3DEqualizer\"\n    version = __version__\n\n    settings_model = EqualizerSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_EQUALIZER_SETTING)\n"
  },
  {
    "path": "server_addon/equalizer/server/settings/__init__.py",
    "content": "from .main import (\n    EqualizerSettings,\n    DEFAULT_EQUALIZER_SETTING,\n)\n\n\n__all__ = (\n    \"EqualizerSettings\",\n    \"DEFAULT_EQUALIZER_SETTING\",\n)\n"
  },
  {
    "path": "server_addon/equalizer/server/settings/creator_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel\nfrom pydantic import Field\n\n\nclass BasicCreatorModel(BaseSettingsModel):\n    enabled: bool = Field(title=\"Enabled\")\n    default_variants: list[str] = Field(\n        default_factory=list,\n        title=\"Default Variants\"\n    )\n\n\nclass EqualizerCreatorPlugins(BaseSettingsModel):\n    CreateMatchMove: BasicCreatorModel = Field(\n        default_factory=BasicCreatorModel,\n        title=\"Create Match Move data\"\n    )\n\n\nEQ_CREATORS_PLUGINS_DEFAULTS = {\n    \"CreateMatchMove\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"CameraTrack\",\n            \"ObjectTrack\",\n            \"PointTrack\",\n            \"Stabilize\",\n            \"SurveyTrack\",\n            \"UserTrack\",\n        ]\n    },\n}\n"
  },
  {
    "path": "server_addon/equalizer/server/settings/main.py",
    "content": "from pydantic import Field\nfrom ayon_server.settings import BaseSettingsModel\n\nfrom .creator_plugins import (\n    EqualizerCreatorPlugins,\n    EQ_CREATORS_PLUGINS_DEFAULTS,\n)\n# from .publish_plugins import (\n#     EqualizerPublishPlugins,\n#     EQ_PUBLISH_PLUGINS_DEFAULTS,\n# )\n\n\nclass EqualizerSettings(BaseSettingsModel):\n    \"\"\"AfterEffects Project Settings.\"\"\"\n\n    create: EqualizerCreatorPlugins = Field(\n        default_factory=EqualizerCreatorPlugins,\n        title=\"Creator plugins\"\n    )\n    # publish: EqualizerPublishPlugins = Field(\n    #     default_factory=EqualizerPublishPlugins,\n    #     title=\"Publish plugins\"\n    # )\n\n\nDEFAULT_EQUALIZER_SETTING = {\n    \"create\": EQ_CREATORS_PLUGINS_DEFAULTS,\n    # \"publish\": EQ_PUBLISH_PLUGINS_DEFAULTS,\n}\n"
  },
  {
    "path": "server_addon/equalizer/server/version.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Package declaring addon version.\"\"\"\n__version__ = \"0.0.1\"\n"
  },
  {
    "path": "server_addon/flame/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import FlameSettings, DEFAULT_VALUES\n\n\nclass FlameAddon(BaseServerAddon):\n    name = \"flame\"\n    title = \"Flame\"\n    version = __version__\n    settings_model: Type[FlameSettings] = FlameSettings\n    frontend_scopes = {}\n    services = {}\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/flame/server/settings/__init__.py",
    "content": "from .main import (\n    FlameSettings,\n    DEFAULT_VALUES,\n)\n\n\n__all__ = (\n    \"FlameSettings\",\n    \"DEFAULT_VALUES\",\n)\n"
  },
  {
    "path": "server_addon/flame/server/settings/create_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass CreateShotClipModel(BaseSettingsModel):\n    hierarchy: str = SettingsField(\n        \"shot\",\n        title=\"Shot parent hierarchy\",\n        section=\"Shot Hierarchy And Rename Settings\"\n    )\n    useShotName: bool = SettingsField(\n        True,\n        title=\"Use Shot Name\",\n    )\n    clipRename: bool = SettingsField(\n        False,\n        title=\"Rename clips\",\n    )\n    clipName: str = SettingsField(\n        \"{sequence}{shot}\",\n        title=\"Clip name template\"\n    )\n    segmentIndex: bool = SettingsField(\n        True,\n        title=\"Accept segment order\"\n    )\n    countFrom: int = SettingsField(\n        10,\n        title=\"Count sequence from\"\n    )\n    countSteps: int = SettingsField(\n        10,\n        title=\"Stepping number\"\n    )\n\n    folder: str = SettingsField(\n        \"shots\",\n        title=\"{folder}\",\n        section=\"Shot Template Keywords\"\n    )\n    episode: str = SettingsField(\n        \"ep01\",\n        title=\"{episode}\"\n    )\n    sequence: str = SettingsField(\n        \"a\",\n        title=\"{sequence}\"\n    )\n    track: str = SettingsField(\n        \"{_track_}\",\n        title=\"{track}\"\n    )\n    shot: str = SettingsField(\n        \"####\",\n        title=\"{shot}\"\n    )\n\n    vSyncOn: bool = SettingsField(\n        False,\n        title=\"Enable Vertical Sync\",\n        section=\"Vertical Synchronization Of Attributes\"\n    )\n\n    workfileFrameStart: int = SettingsField(\n        1001,\n        title=\"Workfiles Start Frame\",\n        section=\"Shot Attributes\"\n    )\n    handleStart: int = SettingsField(\n        10,\n        title=\"Handle start (head)\"\n    )\n    handleEnd: int = SettingsField(\n        10,\n        title=\"Handle end (tail)\"\n    )\n    includeHandles: bool = SettingsField(\n        False,\n        title=\"Enable handles including\"\n    )\n    retimedHandles: bool = SettingsField(\n        True,\n        title=\"Enable retimed handles\"\n    )\n    retimedFramerange: bool = SettingsField(\n        True,\n        title=\"Enable retimed shot frameranges\"\n    )\n\n\nclass CreatePuginsModel(BaseSettingsModel):\n    CreateShotClip: CreateShotClipModel = SettingsField(\n        default_factory=CreateShotClipModel,\n        title=\"Create Shot Clip\"\n    )\n\n\nDEFAULT_CREATE_SETTINGS = {\n    \"CreateShotClip\": {\n        \"hierarchy\": \"{folder}/{sequence}\",\n        \"useShotName\": True,\n        \"clipRename\": False,\n        \"clipName\": \"{sequence}{shot}\",\n        \"segmentIndex\": True,\n        \"countFrom\": 10,\n        \"countSteps\": 10,\n        \"folder\": \"shots\",\n        \"episode\": \"ep01\",\n        \"sequence\": \"a\",\n        \"track\": \"{_track_}\",\n        \"shot\": \"####\",\n        \"vSyncOn\": False,\n        \"workfileFrameStart\": 1001,\n        \"handleStart\": 5,\n        \"handleEnd\": 5,\n        \"includeHandles\": False,\n        \"retimedHandles\": True,\n        \"retimedFramerange\": True\n    }\n}\n"
  },
  {
    "path": "server_addon/flame/server/settings/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n)\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ImageIORemappingRulesModel(BaseSettingsModel):\n    host_native_name: str = SettingsField(\n        title=\"Application native colorspace name\"\n    )\n    ocio_name: str = SettingsField(title=\"OCIO colorspace name\")\n\n\nclass ImageIORemappingModel(BaseSettingsModel):\n    rules: list[ImageIORemappingRulesModel] = SettingsField(\n        default_factory=list\n    )\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ProfileNamesMappingInputsModel(BaseSettingsModel):\n    _layout = \"expanded\"\n\n    flameName: str = SettingsField(\"\", title=\"Flame name\")\n    ocioName: str = SettingsField(\"\", title=\"OCIO name\")\n\n\nclass ProfileNamesMappingModel(BaseSettingsModel):\n    _layout = \"expanded\"\n\n    inputs: list[ProfileNamesMappingInputsModel] = SettingsField(\n        default_factory=list,\n        title=\"Profile names mapping\"\n    )\n\n\nclass ImageIOProjectModel(BaseSettingsModel):\n    colourPolicy: str = SettingsField(\n        \"ACES 1.1\",\n        title=\"Colour Policy (name or path)\",\n        section=\"Project\"\n    )\n    frameDepth: str = SettingsField(\n        \"16-bit fp\",\n        title=\"Image Depth\"\n    )\n    fieldDominance: str = SettingsField(\n        \"PROGRESSIVE\",\n        title=\"Field Dominance\"\n    )\n\n\nclass FlameImageIOModel(BaseSettingsModel):\n    _isGroup = True\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    remapping: ImageIORemappingModel = SettingsField(\n        title=\"Remapping colorspace names\",\n        default_factory=ImageIORemappingModel\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n    # NOTE 'project' attribute was expanded to this model but that caused\n    #   inconsistency with v3 settings and harder conversion handling\n    #   - it can be moved back but keep in mind that it must be handled in v3\n    #       conversion script too\n    project: ImageIOProjectModel = SettingsField(\n        default_factory=ImageIOProjectModel,\n        title=\"Project\"\n    )\n    profilesMapping: ProfileNamesMappingModel = SettingsField(\n        default_factory=ProfileNamesMappingModel,\n        title=\"Profile names mapping\"\n    )\n\n\nDEFAULT_IMAGEIO_SETTINGS = {\n    \"project\": {\n        \"colourPolicy\": \"ACES 1.1\",\n        \"frameDepth\": \"16-bit fp\",\n        \"fieldDominance\": \"PROGRESSIVE\"\n    },\n    \"profilesMapping\": {\n        \"inputs\": [\n            {\n                \"flameName\": \"ACEScg\",\n                \"ocioName\": \"ACES - ACEScg\"\n            },\n            {\n                \"flameName\": \"Rec.709 video\",\n                \"ocioName\": \"Output - Rec.709\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "server_addon/flame/server/settings/loader_plugins.py",
    "content": "from ayon_server.settings import SettingsField, BaseSettingsModel\n\n\nclass LoadClipModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product types\"\n    )\n    reel_group_name: str = SettingsField(\n        \"OpenPype_Reels\",\n        title=\"Reel group name\"\n    )\n    reel_name: str = SettingsField(\n        \"Loaded\",\n        title=\"Reel name\"\n    )\n\n    clip_name_template: str = SettingsField(\n        \"{folder[name]}_{product[name]}<_{output}>\",\n        title=\"Clip name template\"\n    )\n    layer_rename_template: str = SettingsField(\n        \"\", title=\"Layer name template\"\n    )\n    layer_rename_patterns: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Layer rename patters\",\n    )\n\n\nclass LoadClipBatchModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product types\"\n    )\n    reel_name: str = SettingsField(\n        \"OP_LoadedReel\",\n        title=\"Reel name\"\n    )\n    clip_name_template: str = SettingsField(\n        \"{batch}_{folder[name]}_{product[name]}<_{output}>\",\n        title=\"Clip name template\"\n    )\n    layer_rename_template: str = SettingsField(\n        \"\", title=\"Layer name template\"\n    )\n    layer_rename_patterns: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Layer rename patters\",\n    )\n\n\nclass LoaderPluginsModel(BaseSettingsModel):\n    LoadClip: LoadClipModel = SettingsField(\n        default_factory=LoadClipModel,\n        title=\"Load Clip\"\n    )\n    LoadClipBatch: LoadClipBatchModel = SettingsField(\n        default_factory=LoadClipBatchModel,\n        title=\"Load as clip to current batch\"\n    )\n\n\nDEFAULT_LOADER_SETTINGS = {\n    \"LoadClip\": {\n        \"enabled\": True,\n        \"product_types\": [\n            \"render2d\",\n            \"source\",\n            \"plate\",\n            \"render\",\n            \"review\"\n        ],\n        \"reel_group_name\": \"OpenPype_Reels\",\n        \"reel_name\": \"Loaded\",\n        \"clip_name_template\": \"{folder[name]}_{product[name]}<_{output}>\",\n        \"layer_rename_template\": \"{folder[name]}_{product[name]}<_{output}>\",\n        \"layer_rename_patterns\": [\n            \"rgb\",\n            \"rgba\"\n        ]\n    },\n    \"LoadClipBatch\": {\n        \"enabled\": True,\n        \"product_types\": [\n            \"render2d\",\n            \"source\",\n            \"plate\",\n            \"render\",\n            \"review\"\n        ],\n        \"reel_name\": \"OP_LoadedReel\",\n        \"clip_name_template\": \"{batch}_{folder[name]}_{product[name]}<_{output}>\",\n        \"layer_rename_template\": \"{folder[name]}_{product[name]}<_{output}>\",\n        \"layer_rename_patterns\": [\n            \"rgb\",\n            \"rgba\"\n        ]\n    }\n}\n"
  },
  {
    "path": "server_addon/flame/server/settings/main.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\nfrom .imageio import FlameImageIOModel, DEFAULT_IMAGEIO_SETTINGS\nfrom .create_plugins import CreatePuginsModel, DEFAULT_CREATE_SETTINGS\nfrom .publish_plugins import PublishPuginsModel, DEFAULT_PUBLISH_SETTINGS\nfrom .loader_plugins import LoaderPluginsModel, DEFAULT_LOADER_SETTINGS\n\n\nclass FlameSettings(BaseSettingsModel):\n    imageio: FlameImageIOModel = SettingsField(\n        default_factory=FlameImageIOModel,\n        title=\"Color Management (ImageIO)\"\n    )\n    create: CreatePuginsModel = SettingsField(\n        default_factory=CreatePuginsModel,\n        title=\"Create plugins\"\n    )\n    publish: PublishPuginsModel = SettingsField(\n        default_factory=PublishPuginsModel,\n        title=\"Publish plugins\"\n    )\n    load: LoaderPluginsModel = SettingsField(\n        default_factory=LoaderPluginsModel,\n        title=\"Loader plugins\"\n    )\n\n\nDEFAULT_VALUES = {\n    \"imageio\": DEFAULT_IMAGEIO_SETTINGS,\n    \"create\": DEFAULT_CREATE_SETTINGS,\n    \"publish\": DEFAULT_PUBLISH_SETTINGS,\n    \"load\": DEFAULT_LOADER_SETTINGS\n}\n"
  },
  {
    "path": "server_addon/flame/server/settings/publish_plugins.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    task_types_enum,\n)\n\n\nclass XMLPresetAttrsFromCommentsModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField(\"\", title=\"Attribute name\")\n    type: str = SettingsField(\n        default_factory=str,\n        title=\"Attribute type\",\n        enum_resolver=lambda: [\"number\", \"float\", \"string\"]\n    )\n\n\nclass AddTasksModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField(\"\", title=\"Task name\")\n    type: str = SettingsField(\n        default_factory=str,\n        title=\"Task type\",\n        enum_resolver=task_types_enum\n    )\n    create_batch_group: bool = SettingsField(\n        True,\n        title=\"Create batch group\"\n    )\n\n\nclass CollectTimelineInstancesModel(BaseSettingsModel):\n    _isGroup = True\n\n    xml_preset_attrs_from_comments: list[XMLPresetAttrsFromCommentsModel] = (\n        SettingsField(\n            default_factory=list,\n            title=\"XML presets attributes parsable from segment comments\"\n        )\n    )\n    add_tasks: list[AddTasksModel] = SettingsField(\n        default_factory=list,\n        title=\"Add tasks\"\n    )\n\n\nclass ExportPresetsMappingModel(BaseSettingsModel):\n    _layout = \"expanded\"\n\n    name: str = SettingsField(\n        ...,\n        title=\"Name\"\n    )\n    active: bool = SettingsField(True, title=\"Is active\")\n    export_type: str = SettingsField(\n        \"File Sequence\",\n        title=\"Eport clip type\",\n        enum_resolver=lambda: [\"Movie\", \"File Sequence\", \"Sequence Publish\"]\n    )\n    ext: str = SettingsField(\"exr\", title=\"Output extension\")\n    xml_preset_file: str = SettingsField(\n        \"OpenEXR (16-bit fp DWAA).xml\",\n        title=\"XML preset file (with ext)\"\n    )\n    colorspace_out: str = SettingsField(\n        \"ACES - ACEScg\",\n        title=\"Output color (imageio)\"\n    )\n    # TODO remove when resolved or v3 is not a thing anymore\n    # NOTE next 4 attributes were grouped under 'other_parameters' but that\n    #   created inconsistency with v3 settings and harder conversion handling\n    #   - it can be moved back but keep in mind that it must be handled in v3\n    #       conversion script too\n    xml_preset_dir: str = SettingsField(\n        \"\",\n        title=\"XML preset directory\"\n    )\n    parsed_comment_attrs: bool = SettingsField(\n        True,\n        title=\"Parsed comment attributes\"\n    )\n    representation_add_range: bool = SettingsField(\n        True,\n        title=\"Add range to representation name\"\n    )\n    representation_tags: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Representation tags\"\n    )\n    load_to_batch_group: bool = SettingsField(\n        True,\n        title=\"Load to batch group reel\"\n    )\n    batch_group_loader_name: str = SettingsField(\n        \"LoadClipBatch\",\n        title=\"Use loader name\"\n    )\n    filter_path_regex: str = SettingsField(\n        \".*\",\n        title=\"Regex in clip path\"\n    )\n\n\nclass ExtractProductResourcesModel(BaseSettingsModel):\n    _isGroup = True\n\n    keep_original_representation: bool = SettingsField(\n        False,\n        title=\"Publish clip's original media\"\n    )\n    export_presets_mapping: list[ExportPresetsMappingModel] = SettingsField(\n        default_factory=list,\n        title=\"Export presets mapping\"\n    )\n\n\nclass IntegrateBatchGroupModel(BaseSettingsModel):\n    enabled: bool = SettingsField(\n        False,\n        title=\"Enabled\"\n    )\n\n\nclass PublishPuginsModel(BaseSettingsModel):\n    CollectTimelineInstances: CollectTimelineInstancesModel = SettingsField(\n        default_factory=CollectTimelineInstancesModel,\n        title=\"Collect Timeline Instances\"\n    )\n\n    ExtractProductResources: ExtractProductResourcesModel = SettingsField(\n        default_factory=ExtractProductResourcesModel,\n        title=\"Extract Product Resources\"\n    )\n\n    IntegrateBatchGroup: IntegrateBatchGroupModel = SettingsField(\n        default_factory=IntegrateBatchGroupModel,\n        title=\"IntegrateBatchGroup\"\n    )\n\n\nDEFAULT_PUBLISH_SETTINGS = {\n    \"CollectTimelineInstances\": {\n        \"xml_preset_attrs_from_comments\": [\n            {\n                \"name\": \"width\",\n                \"type\": \"number\"\n            },\n            {\n                \"name\": \"height\",\n                \"type\": \"number\"\n            },\n            {\n                \"name\": \"pixelRatio\",\n                \"type\": \"float\"\n            },\n            {\n                \"name\": \"resizeType\",\n                \"type\": \"string\"\n            },\n            {\n                \"name\": \"resizeFilter\",\n                \"type\": \"string\"\n            }\n        ],\n        \"add_tasks\": [\n            {\n                \"name\": \"compositing\",\n                \"type\": \"Compositing\",\n                \"create_batch_group\": True\n            }\n        ]\n    },\n    \"ExtractProductResources\": {\n        \"keep_original_representation\": False,\n        \"export_presets_mapping\": [\n            {\n                \"name\": \"exr16fpdwaa\",\n                \"active\": True,\n                \"export_type\": \"File Sequence\",\n                \"ext\": \"exr\",\n                \"xml_preset_file\": \"OpenEXR (16-bit fp DWAA).xml\",\n                \"colorspace_out\": \"ACES - ACEScg\",\n                \"xml_preset_dir\": \"\",\n                \"parsed_comment_attrs\": True,\n                \"representation_add_range\": True,\n                \"representation_tags\": [],\n                \"load_to_batch_group\": True,\n                \"batch_group_loader_name\": \"LoadClipBatch\",\n                \"filter_path_regex\": \".*\"\n            }\n        ]\n    },\n    \"IntegrateBatchGroup\": {\n        \"enabled\": False\n    }\n}\n"
  },
  {
    "path": "server_addon/flame/server/version.py",
    "content": "__version__ = \"0.1.0\"\n"
  },
  {
    "path": "server_addon/fusion/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import FusionSettings, DEFAULT_VALUES\n\n\nclass FusionAddon(BaseServerAddon):\n    name = \"fusion\"\n    title = \"Fusion\"\n    version = __version__\n    settings_model: Type[FusionSettings] = FusionSettings\n    frontend_scopes = {}\n    services = {}\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/fusion/server/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass FusionImageIOModel(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n"
  },
  {
    "path": "server_addon/fusion/server/settings.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n)\n\nfrom .imageio import FusionImageIOModel\n\n\nclass CopyFusionSettingsModel(BaseSettingsModel):\n    copy_path: str = SettingsField(\"\", title=\"Local Fusion profile directory\")\n    copy_status: bool = SettingsField(title=\"Copy profile on first launch\")\n    force_sync: bool = SettingsField(title=\"Resync profile on each launch\")\n\n\ndef _create_saver_instance_attributes_enum():\n    return [\n        {\n            \"value\": \"reviewable\",\n            \"label\": \"Reviewable\"\n        },\n        {\n            \"value\": \"farm_rendering\",\n            \"label\": \"Farm rendering\"\n        }\n    ]\n\n\ndef _image_format_enum():\n    return [\n        {\"value\": \"exr\", \"label\": \"exr\"},\n        {\"value\": \"tga\", \"label\": \"tga\"},\n        {\"value\": \"png\", \"label\": \"png\"},\n        {\"value\": \"tif\", \"label\": \"tif\"},\n        {\"value\": \"jpg\", \"label\": \"jpg\"},\n    ]\n\n\ndef _frame_range_options_enum():\n    return [\n        {\"value\": \"asset_db\", \"label\": \"Current asset context\"},\n        {\"value\": \"render_range\", \"label\": \"From render in/out\"},\n        {\"value\": \"comp_range\", \"label\": \"From composition timeline\"},\n    ]\n\n\nclass CreateSaverPluginModel(BaseSettingsModel):\n    _isGroup = True\n    temp_rendering_path_template: str = SettingsField(\n        \"\", title=\"Temporary rendering path template\"\n    )\n    default_variants: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Default variants\"\n    )\n    instance_attributes: list[str] = SettingsField(\n        default_factory=list,\n        enum_resolver=_create_saver_instance_attributes_enum,\n        title=\"Instance attributes\"\n    )\n    image_format: str = SettingsField(\n        enum_resolver=_image_format_enum,\n        title=\"Output Image Format\"\n    )\n\n\nclass HookOptionalModel(BaseSettingsModel):\n    enabled: bool = SettingsField(\n        True,\n        title=\"Enabled\"\n    )\n\n\nclass HooksModel(BaseSettingsModel):\n    InstallPySideToFusion: HookOptionalModel = SettingsField(\n        default_factory=HookOptionalModel,\n        title=\"Install PySide2\"\n    )\n\n\nclass CreateSaverModel(CreateSaverPluginModel):\n    default_frame_range_option: str = SettingsField(\n        default=\"asset_db\",\n        enum_resolver=_frame_range_options_enum,\n        title=\"Default frame range source\"\n    )\n\n\nclass CreateImageSaverModel(CreateSaverPluginModel):\n    default_frame: int = SettingsField(\n        0,\n        title=\"Default rendered frame\"\n    )\n\n\nclass CreatPluginsModel(BaseSettingsModel):\n    CreateSaver: CreateSaverModel = SettingsField(\n        default_factory=CreateSaverModel,\n        title=\"Create Saver\",\n        description=\"Creator for render product type (eg. sequence)\"\n    )\n    CreateImageSaver: CreateImageSaverModel = SettingsField(\n        default_factory=CreateImageSaverModel,\n        title=\"Create Image Saver\",\n        description=\"Creator for image product type (eg. single)\"\n    )\n\n\nclass FusionSettings(BaseSettingsModel):\n    imageio: FusionImageIOModel = SettingsField(\n        default_factory=FusionImageIOModel,\n        title=\"Color Management (ImageIO)\"\n    )\n    copy_fusion_settings: CopyFusionSettingsModel = SettingsField(\n        default_factory=CopyFusionSettingsModel,\n        title=\"Local Fusion profile settings\"\n    )\n    hooks: HooksModel = SettingsField(\n        default_factory=HooksModel,\n        title=\"Hooks\"\n    )\n    create: CreatPluginsModel = SettingsField(\n        default_factory=CreatPluginsModel,\n        title=\"Creator plugins\"\n    )\n\n\nDEFAULT_VALUES = {\n    \"imageio\": {\n        \"ocio_config\": {\n            \"enabled\": False,\n            \"filepath\": []\n        },\n        \"file_rules\": {\n            \"enabled\": False,\n            \"rules\": []\n        }\n    },\n    \"copy_fusion_settings\": {\n        \"copy_path\": \"~/.openpype/hosts/fusion/profiles\",\n        \"copy_status\": False,\n        \"force_sync\": False\n    },\n    \"hooks\": {\n        \"InstallPySideToFusion\": {\n            \"enabled\": True\n        }\n    },\n    \"create\": {\n        \"CreateSaver\": {\n            \"temp_rendering_path_template\": \"{workdir}/renders/fusion/{product[name]}/{product[name]}.{frame}.{ext}\",\n            \"default_variants\": [\n                \"Main\",\n                \"Mask\"\n            ],\n            \"instance_attributes\": [\n                \"reviewable\",\n                \"farm_rendering\"\n            ],\n            \"image_format\": \"exr\",\n            \"default_frame_range_option\": \"asset_db\"\n        },\n        \"CreateImageSaver\": {\n            \"temp_rendering_path_template\": \"{workdir}/renders/fusion/{product[name]}/{product[name]}.{ext}\",\n            \"default_variants\": [\n                \"Main\",\n                \"Mask\"\n            ],\n            \"instance_attributes\": [\n                \"reviewable\",\n                \"farm_rendering\"\n            ],\n            \"image_format\": \"exr\",\n            \"default_frame\": 0\n        }\n    }\n}\n"
  },
  {
    "path": "server_addon/fusion/server/version.py",
    "content": "__version__ = \"0.1.4\"\n"
  },
  {
    "path": "server_addon/harmony/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "server_addon/harmony/README.md",
    "content": "ToonBoom Harmony Addon\n===============\n\nIntegration with ToonBoom Harmony.\n"
  },
  {
    "path": "server_addon/harmony/server/__init__.py",
    "content": "from ayon_server.addons import BaseServerAddon\n\nfrom .settings import HarmonySettings, DEFAULT_HARMONY_SETTING\nfrom .version import __version__\n\n\nclass Harmony(BaseServerAddon):\n    name = \"harmony\"\n    title = \"Harmony\"\n    version = __version__\n\n    settings_model = HarmonySettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_HARMONY_SETTING)\n"
  },
  {
    "path": "server_addon/harmony/server/settings/__init__.py",
    "content": "from .main import (\n    HarmonySettings,\n    DEFAULT_HARMONY_SETTING,\n)\n\n\n__all__ = (\n    \"HarmonySettings\",\n    \"DEFAULT_HARMONY_SETTING\",\n)\n"
  },
  {
    "path": "server_addon/harmony/server/settings/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ImageIORemappingRulesModel(BaseSettingsModel):\n    host_native_name: str = SettingsField(\n        title=\"Application native colorspace name\"\n    )\n    ocio_name: str = SettingsField(title=\"OCIO colorspace name\")\n\n\nclass HarmonyImageIOModel(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n"
  },
  {
    "path": "server_addon/harmony/server/settings/main.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\nfrom .imageio import HarmonyImageIOModel\nfrom .publish_plugins import HarmonyPublishPlugins\n\n\nclass HarmonySettings(BaseSettingsModel):\n    \"\"\"Harmony Project Settings.\"\"\"\n\n    imageio: HarmonyImageIOModel = SettingsField(\n        default_factory=HarmonyImageIOModel,\n        title=\"OCIO config\"\n    )\n    publish: HarmonyPublishPlugins = SettingsField(\n        default_factory=HarmonyPublishPlugins,\n        title=\"Publish plugins\"\n    )\n\n\nDEFAULT_HARMONY_SETTING = {\n    \"load\": {\n        \"ImageSequenceLoader\": {\n            \"family\": [\n                \"shot\",\n                \"render\",\n                \"image\",\n                \"plate\",\n                \"reference\"\n            ],\n            \"representations\": [\n                \"jpeg\",\n                \"png\",\n                \"jpg\"\n            ]\n        }\n    },\n    \"publish\": {\n        \"CollectPalettes\": {\n            \"allowed_tasks\": [\n                \".*\"\n            ]\n        },\n        \"ValidateAudio\": {\n            \"enabled\": True,\n            \"optional\": True,\n            \"active\": True\n        },\n        \"ValidateContainers\": {\n            \"enabled\": True,\n            \"optional\": True,\n            \"active\": True\n        },\n        \"ValidateSceneSettings\": {\n            \"enabled\": True,\n            \"optional\": True,\n            \"active\": True,\n            \"frame_check_filter\": [],\n            \"skip_resolution_check\": [],\n            \"skip_timelines_check\": []\n        }\n    }\n}\n"
  },
  {
    "path": "server_addon/harmony/server/settings/publish_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass CollectPalettesPlugin(BaseSettingsModel):\n    \"\"\"Set regular expressions to filter triggering on specific task names. '.*' means on all.\"\"\"  # noqa\n\n    allowed_tasks: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Allowed tasks\"\n    )\n\n\nclass ValidateAudioPlugin(BaseSettingsModel):\n    \"\"\"Check if scene contains audio track.\"\"\"  #\n    _isGroup = True\n    enabled: bool = True\n    optional: bool = SettingsField(False, title=\"Optional\")\n    active: bool = SettingsField(True, title=\"Active\")\n\n\nclass ValidateContainersPlugin(BaseSettingsModel):\n    \"\"\"Check if loaded container is scene are latest versions.\"\"\"\n    _isGroup = True\n    enabled: bool = True\n    optional: bool = SettingsField(False, title=\"Optional\")\n    active: bool = SettingsField(True, title=\"Active\")\n\n\nclass ValidateSceneSettingsPlugin(BaseSettingsModel):\n    \"\"\"Validate if FrameStart, FrameEnd and Resolution match shot data in DB.\n       Use regular expressions to limit validations only on particular asset\n       or task names.\"\"\"\n    _isGroup = True\n    enabled: bool = True\n    optional: bool = SettingsField(False, title=\"Optional\")\n    active: bool = SettingsField(True, title=\"Active\")\n\n    frame_check_filter: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Skip Frame check for Assets with name containing\"\n    )\n\n    skip_resolution_check: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Skip Resolution Check for Tasks\"\n    )\n\n    skip_timelines_check: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Skip Timeline Check for Tasks\"\n    )\n\n\nclass HarmonyPublishPlugins(BaseSettingsModel):\n\n    CollectPalettes: CollectPalettesPlugin = SettingsField(\n        title=\"Collect Palettes\",\n        default_factory=CollectPalettesPlugin,\n    )\n\n    ValidateAudio: ValidateAudioPlugin = SettingsField(\n        title=\"Validate Audio\",\n        default_factory=ValidateAudioPlugin,\n    )\n\n    ValidateContainers: ValidateContainersPlugin = SettingsField(\n        title=\"Validate Containers\",\n        default_factory=ValidateContainersPlugin,\n    )\n\n    ValidateSceneSettings: ValidateSceneSettingsPlugin = SettingsField(\n        title=\"Validate Scene Settings\",\n        default_factory=ValidateSceneSettingsPlugin,\n    )\n"
  },
  {
    "path": "server_addon/harmony/server/version.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Package declaring addon version.\"\"\"\n__version__ = \"0.1.2\"\n"
  },
  {
    "path": "server_addon/hiero/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import HieroSettings, DEFAULT_VALUES\n\n\nclass HieroAddon(BaseServerAddon):\n    name = \"hiero\"\n    title = \"Hiero\"\n    version = __version__\n    settings_model: Type[HieroSettings] = HieroSettings\n    frontend_scopes = {}\n    services = {}\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/hiero/server/settings/__init__.py",
    "content": "from .main import (\n    HieroSettings,\n    DEFAULT_VALUES,\n)\n\n\n__all__ = (\n    \"HieroSettings\",\n    \"DEFAULT_VALUES\",\n)\n"
  },
  {
    "path": "server_addon/hiero/server/settings/common.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.types import (\n    ColorRGBA_float,\n    ColorRGB_uint8\n)\n\n\nclass Vector2d(BaseSettingsModel):\n    _layout = \"compact\"\n\n    x: float = SettingsField(1.0, title=\"X\")\n    y: float = SettingsField(1.0, title=\"Y\")\n\n\nclass Vector3d(BaseSettingsModel):\n    _layout = \"compact\"\n\n    x: float = SettingsField(1.0, title=\"X\")\n    y: float = SettingsField(1.0, title=\"Y\")\n    z: float = SettingsField(1.0, title=\"Z\")\n\n\ndef formatable_knob_type_enum():\n    return [\n        {\"value\": \"text\", \"label\": \"Text\"},\n        {\"value\": \"number\", \"label\": \"Number\"},\n        {\"value\": \"decimal_number\", \"label\": \"Decimal number\"},\n        {\"value\": \"2d_vector\", \"label\": \"2D vector\"},\n        # \"3D vector\"\n    ]\n\n\nclass Formatable(BaseSettingsModel):\n    _layout = \"compact\"\n\n    template: str = SettingsField(\n        \"\",\n        placeholder=\"\"\"{{key}} or {{key}};{{key}}\"\"\",\n        title=\"Template\"\n    )\n    to_type: str = SettingsField(\n        \"Text\",\n        title=\"To Knob type\",\n        enum_resolver=formatable_knob_type_enum,\n    )\n\n\nknob_types_enum = [\n    {\"value\": \"text\", \"label\": \"Text\"},\n    {\"value\": \"formatable\", \"label\": \"Formate from template\"},\n    {\"value\": \"color_gui\", \"label\": \"Color GUI\"},\n    {\"value\": \"boolean\", \"label\": \"Boolean\"},\n    {\"value\": \"number\", \"label\": \"Number\"},\n    {\"value\": \"decimal_number\", \"label\": \"Decimal number\"},\n    {\"value\": \"vector_2d\", \"label\": \"2D vector\"},\n    {\"value\": \"vector_3d\", \"label\": \"3D vector\"},\n    {\"value\": \"color\", \"label\": \"Color\"}\n]\n\n\nclass KnobModel(BaseSettingsModel):\n    _layout = \"expanded\"\n\n    type: str = SettingsField(\n        title=\"Type\",\n        description=\"Switch between different knob types\",\n        enum_resolver=lambda: knob_types_enum,\n        conditionalEnum=True\n    )\n    name: str = SettingsField(\n        title=\"Name\",\n        placeholder=\"Name\"\n    )\n    text: str = SettingsField(\"\", title=\"Value\")\n    color_gui: ColorRGB_uint8 = SettingsField(\n        (0, 0, 255),\n        title=\"RGB Uint8\",\n    )\n    boolean: bool = SettingsField(False, title=\"Value\")\n    number: int = SettingsField(0, title=\"Value\")\n    decimal_number: float = SettingsField(0.0, title=\"Value\")\n    vector_2d: Vector2d = SettingsField(\n        default_factory=Vector2d,\n        title=\"Value\"\n    )\n    vector_3d: Vector3d = SettingsField(\n        default_factory=Vector3d,\n        title=\"Value\"\n    )\n    color: ColorRGBA_float = SettingsField(\n        (0.0, 0.0, 1.0, 1.0),\n        title=\"RGBA Float\"\n    )\n    formatable: Formatable = SettingsField(\n        default_factory=Formatable,\n        title=\"Value\"\n    )\n"
  },
  {
    "path": "server_addon/hiero/server/settings/create_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass CreateShotClipModels(BaseSettingsModel):\n    hierarchy: str = SettingsField(\n        \"{folder}/{sequence}\",\n        title=\"Shot parent hierarchy\",\n        section=\"Shot Hierarchy And Rename Settings\"\n    )\n    clipRename: bool = SettingsField(\n        True,\n        title=\"Rename clips\"\n    )\n    clipName: str = SettingsField(\n        \"{track}{sequence}{shot}\",\n        title=\"Clip name template\"\n    )\n    countFrom: int = SettingsField(\n        10,\n        title=\"Count sequence from\"\n    )\n    countSteps: int = SettingsField(\n        10,\n        title=\"Stepping number\"\n    )\n\n    folder: str = SettingsField(\n        \"shots\",\n        title=\"{folder}\",\n        section=\"Shot Template Keywords\"\n    )\n    episode: str = SettingsField(\n        \"ep01\",\n        title=\"{episode}\"\n    )\n    sequence: str = SettingsField(\n        \"sq01\",\n        title=\"{sequence}\"\n    )\n    track: str = SettingsField(\n        \"{_track_}\",\n        title=\"{track}\"\n    )\n    shot: str = SettingsField(\n        \"sh###\",\n        title=\"{shot}\"\n    )\n\n    vSyncOn: bool = SettingsField(\n        False,\n        title=\"Enable Vertical Sync\",\n        section=\"Vertical Synchronization Of Attributes\"\n    )\n\n    workfileFrameStart: int = SettingsField(\n        1001,\n        title=\"Workfiles Start Frame\",\n        section=\"Shot Attributes\"\n    )\n    handleStart: int = SettingsField(\n        10,\n        title=\"Handle start (head)\"\n    )\n    handleEnd: int = SettingsField(\n        10,\n        title=\"Handle end (tail)\"\n    )\n\n\nclass CreatorPluginsSettings(BaseSettingsModel):\n    CreateShotClip: CreateShotClipModels = SettingsField(\n        default_factory=CreateShotClipModels,\n        title=\"Create Shot Clip\"\n    )\n\n\nDEFAULT_CREATE_SETTINGS = {\n    \"create\": {\n        \"CreateShotClip\": {\n            \"hierarchy\": \"{folder}/{sequence}\",\n            \"clipRename\": True,\n            \"clipName\": \"{track}{sequence}{shot}\",\n            \"countFrom\": 10,\n            \"countSteps\": 10,\n            \"folder\": \"shots\",\n            \"episode\": \"ep01\",\n            \"sequence\": \"sq01\",\n            \"track\": \"{_track_}\",\n            \"shot\": \"sh###\",\n            \"vSyncOn\": False,\n            \"workfileFrameStart\": 1001,\n            \"handleStart\": 10,\n            \"handleEnd\": 10\n        }\n    }\n}\n"
  },
  {
    "path": "server_addon/hiero/server/settings/filters.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n)\n\n\nclass PublishGUIFilterItemModel(BaseSettingsModel):\n    _layout = \"compact\"\n    name: str = SettingsField(title=\"Name\")\n    value: bool = SettingsField(True, title=\"Active\")\n\n\nclass PublishGUIFiltersModel(BaseSettingsModel):\n    _layout = \"compact\"\n    name: str = SettingsField(title=\"Name\")\n    value: list[PublishGUIFilterItemModel] = SettingsField(\n        default_factory=list\n    )\n\n    @validator(\"value\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n"
  },
  {
    "path": "server_addon/hiero/server/settings/imageio.py",
    "content": "from pydantic import validator\n\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n)\n\n\ndef ocio_configs_switcher_enum():\n    return [\n        {\"value\": \"nuke-default\", \"label\": \"nuke-default\"},\n        {\"value\": \"spi-vfx\", \"label\": \"spi-vfx\"},\n        {\"value\": \"spi-anim\", \"label\": \"spi-anim\"},\n        {\"value\": \"aces_0.1.1\", \"label\": \"aces_0.1.1\"},\n        {\"value\": \"aces_0.7.1\", \"label\": \"aces_0.7.1\"},\n        {\"value\": \"aces_1.0.1\", \"label\": \"aces_1.0.1\"},\n        {\"value\": \"aces_1.0.3\", \"label\": \"aces_1.0.3\"},\n        {\"value\": \"aces_1.1\", \"label\": \"aces_1.1\"},\n        {\"value\": \"aces_1.2\", \"label\": \"aces_1.2\"},\n        {\"value\": \"aces_1.3\", \"label\": \"aces_1.3\"},\n        {\"value\": \"custom\", \"label\": \"custom\"}\n    ]\n\n\nclass WorkfileColorspaceSettings(BaseSettingsModel):\n    \"\"\"Hiero workfile colorspace preset. \"\"\"\n    \"\"\"# TODO: enhance settings with host api:\n    we need to add mapping to resolve properly keys.\n    Hiero is excpecting camel case key names,\n    but for better code consistency we are using snake_case:\n\n    ocio_config = ocioConfigName\n    working_space_name = workingSpace\n    int_16_name = sixteenBitLut\n    int_8_name = eightBitLut\n    float_name = floatLut\n    log_name = logLut\n    viewer_name = viewerLut\n    thumbnail_name = thumbnailLut\n    \"\"\"\n\n    ocioConfigName: str = SettingsField(\n        title=\"OpenColorIO Config\",\n        description=\"Switch between OCIO configs\",\n        enum_resolver=ocio_configs_switcher_enum,\n        conditionalEnum=True\n    )\n    workingSpace: str = SettingsField(\n        title=\"Working Space\"\n    )\n    viewerLut: str = SettingsField(\n        title=\"Viewer\"\n    )\n    eightBitLut: str = SettingsField(\n        title=\"8-bit files\"\n    )\n    sixteenBitLut: str = SettingsField(\n        title=\"16-bit files\"\n    )\n    logLut: str = SettingsField(\n        title=\"Log files\"\n    )\n    floatLut: str = SettingsField(\n        title=\"Float files\"\n    )\n    thumbnailLut: str = SettingsField(\n        title=\"Thumnails\"\n    )\n    monitorOutLut: str = SettingsField(\n        title=\"Monitor\"\n    )\n\n\nclass ClipColorspaceRulesItems(BaseSettingsModel):\n    _layout = \"expanded\"\n\n    regex: str = SettingsField(\"\", title=\"Regex expression\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace\")\n\n\nclass RegexInputsModel(BaseSettingsModel):\n    inputs: list[ClipColorspaceRulesItems] = SettingsField(\n        default_factory=list,\n        title=\"Inputs\"\n    )\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ImageIOSettings(BaseSettingsModel):\n    \"\"\"Hiero color management project settings. \"\"\"\n    _isGroup: bool = True\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n    workfile: WorkfileColorspaceSettings = SettingsField(\n        default_factory=WorkfileColorspaceSettings,\n        title=\"Workfile\"\n    )\n    \"\"\"# TODO: enhance settings with host api:\n    - old settings are using `regexInputs` key but we\n      need to rename to `regex_inputs`\n    - no need for `inputs` middle part. It can stay\n      directly on `regex_inputs`\n    \"\"\"\n    regexInputs: RegexInputsModel = SettingsField(\n        default_factory=RegexInputsModel,\n        title=\"Assign colorspace to clips via rules\"\n    )\n\n\nDEFAULT_IMAGEIO_SETTINGS = {\n    \"workfile\": {\n        \"ocioConfigName\": \"nuke-default\",\n        \"workingSpace\": \"linear\",\n        \"viewerLut\": \"sRGB\",\n        \"eightBitLut\": \"sRGB\",\n        \"sixteenBitLut\": \"sRGB\",\n        \"logLut\": \"Cineon\",\n        \"floatLut\": \"linear\",\n        \"thumbnailLut\": \"sRGB\",\n        \"monitorOutLut\": \"sRGB\"\n    },\n    \"regexInputs\": {\n        \"inputs\": [\n            {\n                \"regex\": \"[^-a-zA-Z0-9](plateRef).*(?=mp4)\",\n                \"colorspace\": \"sRGB\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "server_addon/hiero/server/settings/loader_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass LoadClipModel(BaseSettingsModel):\n    enabled: bool = SettingsField(\n        True,\n        title=\"Enabled\"\n    )\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product types\"\n    )\n    clip_name_template: str = SettingsField(\n        title=\"Clip name template\"\n    )\n\n\nclass LoaderPuginsModel(BaseSettingsModel):\n    LoadClip: LoadClipModel = SettingsField(\n        default_factory=LoadClipModel,\n        title=\"Load Clip\"\n    )\n\n\nDEFAULT_LOADER_PLUGINS_SETTINGS = {\n    \"LoadClip\": {\n        \"enabled\": True,\n        \"product_types\": [\n            \"render2d\",\n            \"source\",\n            \"plate\",\n            \"render\",\n            \"review\"\n        ],\n        \"clip_name_template\": \"{folder[name]}_{product[name]}_{representation}\"\n    }\n}\n"
  },
  {
    "path": "server_addon/hiero/server/settings/main.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\nfrom .imageio import (\n    ImageIOSettings,\n    DEFAULT_IMAGEIO_SETTINGS\n)\nfrom .create_plugins import (\n    CreatorPluginsSettings,\n    DEFAULT_CREATE_SETTINGS\n)\nfrom .loader_plugins import (\n    LoaderPuginsModel,\n    DEFAULT_LOADER_PLUGINS_SETTINGS\n)\nfrom .publish_plugins import (\n    PublishPuginsModel,\n    DEFAULT_PUBLISH_PLUGIN_SETTINGS\n)\nfrom .scriptsmenu import (\n    ScriptsmenuSettings,\n    DEFAULT_SCRIPTSMENU_SETTINGS\n)\nfrom .filters import PublishGUIFilterItemModel\n\n\nclass HieroSettings(BaseSettingsModel):\n    \"\"\"Nuke addon settings.\"\"\"\n\n    imageio: ImageIOSettings = SettingsField(\n        default_factory=ImageIOSettings,\n        title=\"Color Management (imageio)\",\n    )\n\n    create: CreatorPluginsSettings = SettingsField(\n        default_factory=CreatorPluginsSettings,\n        title=\"Creator Plugins\",\n    )\n    load: LoaderPuginsModel = SettingsField(\n        default_factory=LoaderPuginsModel,\n        title=\"Loader plugins\"\n    )\n    publish: PublishPuginsModel = SettingsField(\n        default_factory=PublishPuginsModel,\n        title=\"Publish plugins\"\n    )\n    scriptsmenu: ScriptsmenuSettings = SettingsField(\n        default_factory=ScriptsmenuSettings,\n        title=\"Scripts Menu Definition\",\n    )\n    filters: list[PublishGUIFilterItemModel] = SettingsField(\n        default_factory=list\n    )\n\n\nDEFAULT_VALUES = {\n    \"imageio\": DEFAULT_IMAGEIO_SETTINGS,\n    \"create\": DEFAULT_CREATE_SETTINGS,\n    \"load\": DEFAULT_LOADER_PLUGINS_SETTINGS,\n    \"publish\": DEFAULT_PUBLISH_PLUGIN_SETTINGS,\n    \"scriptsmenu\": DEFAULT_SCRIPTSMENU_SETTINGS,\n    \"filters\": [],\n}\n"
  },
  {
    "path": "server_addon/hiero/server/settings/publish_plugins.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n    normalize_name,\n)\n\n\nclass CollectInstanceVersionModel(BaseSettingsModel):\n    enabled: bool = SettingsField(\n        True,\n        title=\"Enabled\"\n    )\n\n\nclass CollectClipEffectsDefModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField(\"\", title=\"Name\")\n    effect_classes: list[str] = SettingsField(\n        default_factory=list, title=\"Effect Classes\"\n    )\n\n    @validator(\"name\")\n    def validate_name(cls, value):\n        \"\"\"Ensure name does not contain weird characters\"\"\"\n        return normalize_name(value)\n\n\nclass CollectClipEffectsModel(BaseSettingsModel):\n    effect_categories: list[CollectClipEffectsDefModel] = SettingsField(\n        default_factory=list, title=\"Effect Categories\"\n    )\n\n    @validator(\"effect_categories\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ExtractReviewCutUpVideoModel(BaseSettingsModel):\n    enabled: bool = SettingsField(\n        True,\n        title=\"Enabled\"\n    )\n    tags_addition: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Additional tags\"\n    )\n\n\nclass PublishPuginsModel(BaseSettingsModel):\n    CollectInstanceVersion: CollectInstanceVersionModel = SettingsField(\n        default_factory=CollectInstanceVersionModel,\n        title=\"Collect Instance Version\"\n    )\n    CollectClipEffects: CollectClipEffectsModel = SettingsField(\n        default_factory=CollectClipEffectsModel,\n        title=\"Collect Clip Effects\"\n    )\n    \"\"\"# TODO: enhance settings with host api:\n    Rename class name and plugin name\n    to match title (it makes more sense)\n    \"\"\"\n    ExtractReviewCutUpVideo: ExtractReviewCutUpVideoModel = SettingsField(\n        default_factory=ExtractReviewCutUpVideoModel,\n        title=\"Exctract Review Trim\"\n    )\n\n\nDEFAULT_PUBLISH_PLUGIN_SETTINGS = {\n    \"CollectInstanceVersion\": {\n        \"enabled\": False,\n    },\n    \"ExtractReviewCutUpVideo\": {\n        \"enabled\": True,\n        \"tags_addition\": [\n            \"review\"\n        ]\n    },\n    \"CollectClipEffectsModel\": {\n        \"effect_categories\": []\n    }\n}\n"
  },
  {
    "path": "server_addon/hiero/server/settings/scriptsmenu.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass ScriptsmenuSubmodel(BaseSettingsModel):\n    \"\"\"Item Definition\"\"\"\n    _isGroup = True\n\n    type: str = SettingsField(title=\"Type\")\n    command: str = SettingsField(title=\"Command\")\n    sourcetype: str = SettingsField(title=\"Source Type\")\n    title: str = SettingsField(title=\"Title\")\n    tooltip: str = SettingsField(title=\"Tooltip\")\n\n\nclass ScriptsmenuSettings(BaseSettingsModel):\n    \"\"\"Nuke script menu project settings.\"\"\"\n    _isGroup = True\n\n    \"\"\"# TODO: enhance settings with host api:\n    - in api rename key `name` to `menu_name`\n    \"\"\"\n    name: str = SettingsField(title=\"Menu name\")\n    definition: list[ScriptsmenuSubmodel] = SettingsField(\n        default_factory=list,\n        title=\"Definition\",\n        description=\"Scriptmenu Items Definition\")\n\n\nDEFAULT_SCRIPTSMENU_SETTINGS = {\n    \"name\": \"Custom Tools\",\n    \"definition\": [\n        {\n            \"type\": \"action\",\n            \"sourcetype\": \"python\",\n            \"title\": \"Ayon Hiero Docs\",\n            \"command\": \"import webbrowser;webbrowser.open(url='https://ayon.ynput.io/docs/addon_hiero_artist')\",  # noqa\n            \"tooltip\": \"Open the Ayon Hiero user doc page\"\n        }\n    ]\n}\n"
  },
  {
    "path": "server_addon/hiero/server/version.py",
    "content": "__version__ = \"0.1.2\"\n"
  },
  {
    "path": "server_addon/houdini/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import HoudiniSettings, DEFAULT_VALUES\n\n\nclass Houdini(BaseServerAddon):\n    name = \"houdini\"\n    title = \"Houdini\"\n    version = __version__\n    settings_model: Type[HoudiniSettings] = HoudiniSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/houdini/server/settings/__init__.py",
    "content": "from .main import (\n    HoudiniSettings,\n    DEFAULT_VALUES,\n)\n\n\n__all__ = (\n    \"HoudiniSettings\",\n    \"DEFAULT_VALUES\",\n)\n"
  },
  {
    "path": "server_addon/houdini/server/settings/create.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\n# Creator Plugins\nclass CreatorModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    default_variants: list[str] = SettingsField(\n        title=\"Default Products\",\n        default_factory=list,\n    )\n\n\nclass CreateArnoldAssModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    default_variants: list[str] = SettingsField(\n        title=\"Default Products\",\n        default_factory=list,\n    )\n    ext: str = SettingsField(Title=\"Extension\")\n\n\nclass CreateStaticMeshModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Default Products\"\n    )\n    static_mesh_prefix: str = SettingsField(\"S\", title=\"Static Mesh Prefix\")\n    collision_prefixes: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Collision Prefixes\"\n    )\n\n\nclass CreatePluginsModel(BaseSettingsModel):\n    CreateAlembicCamera: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create Alembic Camera\")\n    CreateArnoldAss: CreateArnoldAssModel = SettingsField(\n        default_factory=CreateArnoldAssModel,\n        title=\"Create Arnold Ass\")\n    CreateArnoldRop: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create Arnold ROP\")\n    CreateCompositeSequence: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create Composite (Image Sequence)\")\n    CreateHDA: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create Houdini Digital Asset\")\n    CreateKarmaROP: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create Karma ROP\")\n    CreateMantraIFD: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create Mantra IFD\")\n    CreateMantraROP: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create Mantra ROP\")\n    CreatePointCache: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create PointCache (Abc)\")\n    CreateBGEO: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create PointCache (Bgeo)\")\n    CreateRedshiftProxy: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create Redshift Proxy\")\n    CreateRedshiftROP: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create Redshift ROP\")\n    CreateReview: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create Review\")\n    # \"-\" is not compatible in the new model\n    CreateStaticMesh: CreateStaticMeshModel = SettingsField(\n        default_factory=CreateStaticMeshModel,\n        title=\"Create Static Mesh\")\n    CreateUSD: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create USD (experimental)\")\n    CreateUSDRender: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create USD render (experimental)\")\n    CreateVDBCache: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create VDB Cache\")\n    CreateVrayROP: CreatorModel = SettingsField(\n        default_factory=CreatorModel,\n        title=\"Create VRay ROP\")\n\n\nDEFAULT_HOUDINI_CREATE_SETTINGS = {\n    \"CreateAlembicCamera\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateArnoldAss\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"],\n        \"ext\": \".ass\"\n    },\n    \"CreateArnoldRop\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateCompositeSequence\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateHDA\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateKarmaROP\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateMantraIFD\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateMantraROP\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreatePointCache\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateBGEO\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateRedshiftProxy\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateRedshiftROP\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateReview\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateStaticMesh\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ],\n        \"static_mesh_prefix\": \"S\",\n        \"collision_prefixes\": [\n            \"UBX\",\n            \"UCP\",\n            \"USP\",\n            \"UCX\"\n        ]\n    },\n    \"CreateUSD\": {\n        \"enabled\": False,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateUSDRender\": {\n        \"enabled\": False,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateVDBCache\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n    \"CreateVrayROP\": {\n        \"enabled\": True,\n        \"default_variants\": [\"Main\"]\n    },\n}\n"
  },
  {
    "path": "server_addon/houdini/server/settings/general.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass HoudiniVarModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    var: str = SettingsField(\"\", title=\"Var\")\n    value: str = SettingsField(\"\", title=\"Value\")\n    is_directory: bool = SettingsField(False, title=\"Treat as directory\")\n\n\nclass UpdateHoudiniVarcontextModel(BaseSettingsModel):\n    \"\"\"Sync vars with context changes.\n\n    If a value is treated as a directory on update\n    it will be ensured the folder exists.\n    \"\"\"\n\n    enabled: bool = SettingsField(title=\"Enabled\")\n    # TODO this was dynamic dictionary '{var: path}'\n    houdini_vars: list[HoudiniVarModel] = SettingsField(\n        default_factory=list,\n        title=\"Houdini Vars\"\n    )\n\n\nclass GeneralSettingsModel(BaseSettingsModel):\n    add_self_publish_button: bool = SettingsField(\n        False,\n        title=\"Add Self Publish Button\"\n    )\n    update_houdini_var_context: UpdateHoudiniVarcontextModel = SettingsField(\n        default_factory=UpdateHoudiniVarcontextModel,\n        title=\"Update Houdini Vars on context change\"\n    )\n\n\nDEFAULT_GENERAL_SETTINGS = {\n    \"add_self_publish_button\": False,\n    \"update_houdini_var_context\": {\n        \"enabled\": True,\n        \"houdini_vars\": [\n            {\n                \"var\": \"JOB\",\n                \"value\": \"{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}\",  # noqa\n                \"is_directory\": True\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "server_addon/houdini/server/settings/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass HoudiniImageIOModel(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n"
  },
  {
    "path": "server_addon/houdini/server/settings/main.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\nfrom .general import (\n    GeneralSettingsModel,\n    DEFAULT_GENERAL_SETTINGS\n)\nfrom .imageio import HoudiniImageIOModel\nfrom .shelves import ShelvesModel\nfrom .create import (\n    CreatePluginsModel,\n    DEFAULT_HOUDINI_CREATE_SETTINGS\n)\nfrom .publish import (\n    PublishPluginsModel,\n    DEFAULT_HOUDINI_PUBLISH_SETTINGS,\n)\n\n\nclass HoudiniSettings(BaseSettingsModel):\n    general: GeneralSettingsModel = SettingsField(\n        default_factory=GeneralSettingsModel,\n        title=\"General\"\n    )\n    imageio: HoudiniImageIOModel = SettingsField(\n        default_factory=HoudiniImageIOModel,\n        title=\"Color Management (ImageIO)\"\n    )\n    shelves: list[ShelvesModel] = SettingsField(\n        default_factory=list,\n        title=\"Shelves Manager\",\n    )\n    create: CreatePluginsModel = SettingsField(\n        default_factory=CreatePluginsModel,\n        title=\"Creator Plugins\",\n    )\n    publish: PublishPluginsModel = SettingsField(\n        default_factory=PublishPluginsModel,\n        title=\"Publish Plugins\",\n    )\n\n\nDEFAULT_VALUES = {\n    \"general\": DEFAULT_GENERAL_SETTINGS,\n    \"shelves\": [],\n    \"create\": DEFAULT_HOUDINI_CREATE_SETTINGS,\n    \"publish\": DEFAULT_HOUDINI_PUBLISH_SETTINGS\n}\n"
  },
  {
    "path": "server_addon/houdini/server/settings/publish.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\n# Publish Plugins\nclass CollectAssetHandlesModel(BaseSettingsModel):\n    \"\"\"Collect Frame Range\n    Disable this if you want the publisher to\n    ignore start and end handles specified in the\n    asset data for publish instances\n    \"\"\"\n    use_asset_handles: bool = SettingsField(\n        title=\"Use asset handles\")\n\n\nclass CollectChunkSizeModel(BaseSettingsModel):\n    \"\"\"Collect Chunk Size.\"\"\"\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    chunk_size: int = SettingsField(\n        title=\"Frames Per Task\")\n\n\nclass ValidateWorkfilePathsModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    node_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Node Types\"\n    )\n    prohibited_vars: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Prohibited Variables\"\n    )\n\n\nclass BasicValidateModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n\n\nclass PublishPluginsModel(BaseSettingsModel):\n    CollectAssetHandles: CollectAssetHandlesModel = SettingsField(\n        default_factory=CollectAssetHandlesModel,\n        title=\"Collect Asset Handles.\",\n        section=\"Collectors\"\n    )\n    CollectChunkSize: CollectChunkSizeModel = SettingsField(\n        default_factory=CollectChunkSizeModel,\n        title=\"Collect Chunk Size.\"\n    )\n    ValidateContainers: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Latest Containers.\",\n        section=\"Validators\")\n    ValidateMeshIsStatic: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh is Static.\")\n    ValidateReviewColorspace: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Review Colorspace.\")\n    ValidateSubsetName: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Subset Name.\")\n    ValidateUnrealStaticMeshName: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Unreal Static Mesh Name.\")\n    ValidateWorkfilePaths: ValidateWorkfilePathsModel = SettingsField(\n        default_factory=ValidateWorkfilePathsModel,\n        title=\"Validate workfile paths settings.\")\n\n\nDEFAULT_HOUDINI_PUBLISH_SETTINGS = {\n    \"CollectAssetHandles\": {\n        \"use_asset_handles\": True\n    },\n    \"CollectChunkSize\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"chunk_size\": 999999\n    },\n    \"ValidateContainers\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshIsStatic\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateReviewColorspace\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateSubsetName\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateUnrealStaticMeshName\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateWorkfilePaths\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"node_types\": [\n            \"file\",\n            \"alembic\"\n        ],\n        \"prohibited_vars\": [\n            \"$HIP\",\n            \"$JOB\"\n        ]\n    }\n}\n"
  },
  {
    "path": "server_addon/houdini/server/settings/shelves.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    MultiplatformPathModel\n)\n\n\nclass ShelfToolsModel(BaseSettingsModel):\n    \"\"\"Name and Script Path are mandatory.\"\"\"\n    label: str = SettingsField(title=\"Name\")\n    script: str = SettingsField(title=\"Script Path\")\n    icon: str = SettingsField(\"\", title=\"Icon Path\")\n    help: str = SettingsField(\"\", title=\"Help text\")\n\n\nclass ShelfDefinitionModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    shelf_name: str = SettingsField(title=\"Shelf name\")\n    tools_list: list[ShelfToolsModel] = SettingsField(\n        default_factory=list,\n        title=\"Shelf Tools\"\n    )\n\n\nclass AddShelfFileModel(BaseSettingsModel):\n    shelf_set_source_path: MultiplatformPathModel = SettingsField(\n        default_factory=MultiplatformPathModel,\n        title=\"Shelf Set Path\"\n    )\n\n\nclass AddSetAndDefinitionsModel(BaseSettingsModel):\n    shelf_set_name: str = SettingsField(\"\", title=\"Shelf Set Name\")\n    shelf_definition: list[ShelfDefinitionModel] = SettingsField(\n        default_factory=list,\n        title=\"Shelves Definitions\"\n    )\n\n\ndef shelves_enum_options():\n    return [\n        {\n            \"value\": \"add_shelf_file\",\n            \"label\": \"Add a .shelf file\"\n        },\n        {\n            \"value\": \"add_set_and_definitions\",\n            \"label\": \"Add Shelf Set Name and Shelves Definitions\"\n        }\n    ]\n\n\nclass ShelvesModel(BaseSettingsModel):\n    options: str = SettingsField(\n        title=\"Options\",\n        description=\"Switch between shelves manager options\",\n        enum_resolver=shelves_enum_options,\n        conditionalEnum=True\n    )\n    add_shelf_file: AddShelfFileModel = SettingsField(\n        title=\"Add a .shelf file\",\n        default_factory=AddShelfFileModel\n    )\n    add_set_and_definitions: AddSetAndDefinitionsModel = SettingsField(\n        title=\"Add Shelf Set Name and Shelves Definitions\",\n        default_factory=AddSetAndDefinitionsModel\n    )\n"
  },
  {
    "path": "server_addon/houdini/server/version.py",
    "content": "__version__ = \"0.2.11\"\n"
  },
  {
    "path": "server_addon/max/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import MaxSettings, DEFAULT_VALUES\n\n\nclass MaxAddon(BaseServerAddon):\n    name = \"max\"\n    title = \"Max\"\n    version = __version__\n    settings_model: Type[MaxSettings] = MaxSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/max/server/settings/__init__.py",
    "content": "from .main import (\n    MaxSettings,\n    DEFAULT_VALUES,\n)\n\n\n__all__ = (\n    \"MaxSettings\",\n    \"DEFAULT_VALUES\",\n)\n"
  },
  {
    "path": "server_addon/max/server/settings/create_review_settings.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\ndef image_format_enum():\n    \"\"\"Return enumerator for image output formats.\"\"\"\n    return [\n        {\"label\": \"exr\", \"value\": \"exr\"},\n        {\"label\": \"jpg\", \"value\": \"jpg\"},\n        {\"label\": \"png\", \"value\": \"png\"},\n        {\"label\": \"tga\", \"value\": \"tga\"}\n    ]\n\n\ndef visual_style_enum():\n    \"\"\"Return enumerator for viewport visual style.\"\"\"\n    return [\n        {\"label\": \"Realistic\", \"value\": \"Realistic\"},\n        {\"label\": \"Shaded\", \"value\": \"Shaded\"},\n        {\"label\": \"Facets\", \"value\": \"Facets\"},\n        {\"label\": \"ConsistentColors\",\n         \"value\": \"ConsistentColors\"},\n        {\"label\": \"Wireframe\", \"value\": \"Wireframe\"},\n        {\"label\": \"BoundingBox\", \"value\": \"BoundingBox\"},\n        {\"label\": \"Ink\", \"value\": \"Ink\"},\n        {\"label\": \"ColorInk\", \"value\": \"ColorInk\"},\n        {\"label\": \"Acrylic\", \"value\": \"Acrylic\"},\n        {\"label\": \"Tech\", \"value\": \"Tech\"},\n        {\"label\": \"Graphite\", \"value\": \"Graphite\"},\n        {\"label\": \"ColorPencil\", \"value\": \"ColorPencil\"},\n        {\"label\": \"Pastel\", \"value\": \"Pastel\"},\n        {\"label\": \"Clay\", \"value\": \"Clay\"},\n        {\"label\": \"ModelAssist\", \"value\": \"ModelAssist\"}\n    ]\n\n\ndef preview_preset_enum():\n    \"\"\"Return enumerator for viewport visual preset.\"\"\"\n    return [\n        {\"label\": \"Quality\", \"value\": \"Quality\"},\n        {\"label\": \"Standard\", \"value\": \"Standard\"},\n        {\"label\": \"Performance\", \"value\": \"Performance\"},\n        {\"label\": \"DXMode\", \"value\": \"DXMode\"},\n        {\"label\": \"Customize\", \"value\": \"Customize\"},\n    ]\n\n\ndef anti_aliasing_enum():\n    \"\"\"Return enumerator for viewport anti-aliasing.\"\"\"\n    return [\n        {\"label\": \"None\", \"value\": \"None\"},\n        {\"label\": \"2X\", \"value\": \"2X\"},\n        {\"label\": \"4X\", \"value\": \"4X\"},\n        {\"label\": \"8X\", \"value\": \"8X\"}\n    ]\n\n\nclass CreateReviewModel(BaseSettingsModel):\n    review_width: int = SettingsField(1920, title=\"Review Width\")\n    review_height: int = SettingsField(1080, title=\"Review Height\")\n    percentSize: float = SettingsField(100.0, title=\"Percent of Output\")\n    keep_images: bool = SettingsField(False, title=\"Keep Image Sequences\")\n    image_format: str = SettingsField(\n        enum_resolver=image_format_enum,\n        title=\"Image Format Options\"\n    )\n    visual_style: str = SettingsField(\n        enum_resolver=visual_style_enum,\n        title=\"Preference\"\n    )\n    viewport_preset: str = SettingsField(\n        enum_resolver=preview_preset_enum,\n        title=\"Preview Preset\"\n    )\n    anti_aliasing: str = SettingsField(\n        enum_resolver=anti_aliasing_enum,\n        title=\"Anti-aliasing Quality\"\n    )\n    vp_texture: bool = SettingsField(True, title=\"Viewport Texture\")\n\n\nDEFAULT_CREATE_REVIEW_SETTINGS = {\n    \"review_width\": 1920,\n    \"review_height\": 1080,\n    \"percentSize\": 100.0,\n    \"keep_images\": False,\n    \"image_format\": \"png\",\n    \"visual_style\": \"Realistic\",\n    \"viewport_preset\": \"Quality\",\n    \"anti_aliasing\": \"None\",\n    \"vp_texture\": True\n}\n"
  },
  {
    "path": "server_addon/max/server/settings/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ImageIOSettings(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n"
  },
  {
    "path": "server_addon/max/server/settings/main.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\nfrom .imageio import ImageIOSettings\nfrom .render_settings import (\n    RenderSettingsModel, DEFAULT_RENDER_SETTINGS\n)\nfrom .create_review_settings import (\n    CreateReviewModel, DEFAULT_CREATE_REVIEW_SETTINGS\n)\nfrom .publishers import (\n    PublishersModel, DEFAULT_PUBLISH_SETTINGS\n)\n\n\ndef unit_scale_enum():\n    \"\"\"Return enumerator for scene unit scale.\"\"\"\n    return [\n        {\"label\": \"mm\", \"value\": \"Millimeters\"},\n        {\"label\": \"cm\", \"value\": \"Centimeters\"},\n        {\"label\": \"m\", \"value\": \"Meters\"},\n        {\"label\": \"km\", \"value\": \"Kilometers\"}\n    ]\n\n\nclass UnitScaleSettings(BaseSettingsModel):\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    scene_unit_scale: str = SettingsField(\n        \"Centimeters\",\n        title=\"Scene Unit Scale\",\n        enum_resolver=unit_scale_enum\n    )\n\n\nclass PRTAttributesModel(BaseSettingsModel):\n    _layout = \"compact\"\n    name: str = SettingsField(title=\"Name\")\n    value: str = SettingsField(title=\"Attribute\")\n\n\nclass PointCloudSettings(BaseSettingsModel):\n    attribute: list[PRTAttributesModel] = SettingsField(\n        default_factory=list, title=\"Channel Attribute\")\n\n\nclass MaxSettings(BaseSettingsModel):\n    unit_scale_settings: UnitScaleSettings = SettingsField(\n        default_factory=UnitScaleSettings,\n        title=\"Set Unit Scale\"\n    )\n    imageio: ImageIOSettings = SettingsField(\n        default_factory=ImageIOSettings,\n        title=\"Color Management (ImageIO)\"\n    )\n    RenderSettings: RenderSettingsModel = SettingsField(\n        default_factory=RenderSettingsModel,\n        title=\"Render Settings\"\n    )\n    CreateReview: CreateReviewModel = SettingsField(\n        default_factory=CreateReviewModel,\n        title=\"Create Review\"\n    )\n    PointCloud: PointCloudSettings = SettingsField(\n        default_factory=PointCloudSettings,\n        title=\"Point Cloud\"\n    )\n    publish: PublishersModel = SettingsField(\n        default_factory=PublishersModel,\n        title=\"Publish Plugins\")\n\n\nDEFAULT_VALUES = {\n    \"unit_scale_settings\": {\n        \"enabled\": True,\n        \"scene_unit_scale\": \"Centimeters\"\n    },\n    \"RenderSettings\": DEFAULT_RENDER_SETTINGS,\n    \"CreateReview\": DEFAULT_CREATE_REVIEW_SETTINGS,\n    \"PointCloud\": {\n        \"attribute\": [\n            {\"name\": \"Age\", \"value\": \"age\"},\n            {\"name\": \"Radius\", \"value\": \"radius\"},\n            {\"name\": \"Position\", \"value\": \"position\"},\n            {\"name\": \"Rotation\", \"value\": \"rotation\"},\n            {\"name\": \"Scale\", \"value\": \"scale\"},\n            {\"name\": \"Velocity\", \"value\": \"velocity\"},\n            {\"name\": \"Color\", \"value\": \"color\"},\n            {\"name\": \"TextureCoordinate\", \"value\": \"texcoord\"},\n            {\"name\": \"MaterialID\", \"value\": \"matid\"},\n            {\"name\": \"custFloats\", \"value\": \"custFloats\"},\n            {\"name\": \"custVecs\", \"value\": \"custVecs\"},\n        ]\n    },\n    \"publish\": DEFAULT_PUBLISH_SETTINGS\n\n}\n"
  },
  {
    "path": "server_addon/max/server/settings/publishers.py",
    "content": "import json\nfrom pydantic import validator\n\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.exceptions import BadRequestException\n\n\nclass ValidateAttributesModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"ValidateAttributes\")\n    attributes: str = SettingsField(\n        \"{}\", title=\"Attributes\", widget=\"textarea\")\n\n    @validator(\"attributes\")\n    def validate_json(cls, value):\n        if not value.strip():\n            return \"{}\"\n        try:\n            converted_value = json.loads(value)\n            success = isinstance(converted_value, dict)\n        except json.JSONDecodeError:\n            success = False\n\n        if not success:\n            raise BadRequestException(\n                \"The attibutes can't be parsed as json object\"\n            )\n        return value\n\n\nclass ValidateCameraAttributesModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    fov: float = SettingsField(0.0, title=\"Focal Length\")\n    nearrange: float = SettingsField(0.0, title=\"Near Range\")\n    farrange: float = SettingsField(0.0, title=\"Far Range\")\n    nearclip: float = SettingsField(0.0, title=\"Near Clip\")\n    farclip: float = SettingsField(0.0, title=\"Far Clip\")\n\n\nclass FamilyMappingItemModel(BaseSettingsModel):\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product Types\"\n    )\n    plugins: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Plugins\"\n    )\n\n\nclass ValidateLoadedPluginModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    family_plugins_mapping: list[FamilyMappingItemModel] = SettingsField(\n        default_factory=list,\n        title=\"Family Plugins Mapping\"\n    )\n\n\nclass BasicValidateModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n\n\nclass PublishersModel(BaseSettingsModel):\n    ValidateInstanceInContext: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Instance In Context\",\n        section=\"Validators\"\n    )\n    ValidateFrameRange: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Frame Range\"\n    )\n    ValidateAttributes: ValidateAttributesModel = SettingsField(\n        default_factory=ValidateAttributesModel,\n        title=\"Validate Attributes\"\n    )\n    ValidateCameraAttributes: ValidateCameraAttributesModel = SettingsField(\n        default_factory=ValidateCameraAttributesModel,\n        title=\"Validate Camera Attributes\",\n        description=(\n            \"If the value of the camera attributes set to 0, \"\n            \"the system automatically skips checking it\"\n        )\n    )\n    ValidateLoadedPlugin: ValidateLoadedPluginModel = SettingsField(\n        default_factory=ValidateLoadedPluginModel,\n        title=\"Validate Loaded Plugin\"\n    )\n    ValidateRenderPasses: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Render Passes\"\n    )\n    ExtractModelObj: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Extract OBJ\",\n        section=\"Extractors\"\n    )\n    ExtractModelFbx: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Extract FBX\"\n    )\n    ExtractModelUSD: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Extract Geometry (USD)\"\n    )\n    ExtractModel: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Extract Geometry (Alembic)\"\n    )\n    ExtractMaxSceneRaw: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Extract Max Scene (Raw)\"\n    )\n\n\nDEFAULT_PUBLISH_SETTINGS = {\n    \"ValidateInstanceInContext\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateFrameRange\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateAttributes\": {\n        \"enabled\": False,\n        \"attributes\": \"{}\"\n    },\n    \"ValidateCameraAttributes\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": False,\n        \"fov\": 45.0,\n        \"nearrange\": 0.0,\n        \"farrange\": 1000.0,\n        \"nearclip\": 1.0,\n        \"farclip\": 1000.0\n    },\n    \"ValidateLoadedPlugin\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"family_plugins_mapping\": []\n    },\n    \"ValidateRenderPasses\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ExtractModelObj\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": False\n    },\n    \"ExtractModelFbx\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": False\n    },\n    \"ExtractModelUSD\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": False\n    },\n    \"ExtractModel\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ExtractMaxSceneRaw\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    }\n}\n"
  },
  {
    "path": "server_addon/max/server/settings/render_settings.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\ndef aov_separators_enum():\n    return [\n        {\"value\": \"dash\", \"label\": \"- (dash)\"},\n        {\"value\": \"underscore\", \"label\": \"_ (underscore)\"},\n        {\"value\": \"dot\", \"label\": \". (dot)\"}\n    ]\n\n\ndef image_format_enum():\n    \"\"\"Return enumerator for image output formats.\"\"\"\n    return [\n        {\"label\": \"bmp\", \"value\": \"bmp\"},\n        {\"label\": \"exr\", \"value\": \"exr\"},\n        {\"label\": \"tif\", \"value\": \"tif\"},\n        {\"label\": \"tiff\", \"value\": \"tiff\"},\n        {\"label\": \"jpg\", \"value\": \"jpg\"},\n        {\"label\": \"png\", \"value\": \"png\"},\n        {\"label\": \"tga\", \"value\": \"tga\"},\n        {\"label\": \"dds\", \"value\": \"dds\"}\n    ]\n\n\nclass RenderSettingsModel(BaseSettingsModel):\n    default_render_image_folder: str = SettingsField(\n        title=\"Default render image folder\"\n    )\n    aov_separator: str = SettingsField(\n        \"underscore\",\n        title=\"AOV Separator character\",\n        enum_resolver=aov_separators_enum\n    )\n    image_format: str = SettingsField(\n        enum_resolver=image_format_enum,\n        title=\"Output Image Format\"\n    )\n    multipass: bool = SettingsField(title=\"multipass\")\n\n\nDEFAULT_RENDER_SETTINGS = {\n    \"default_render_image_folder\": \"renders/3dsmax\",\n    \"aov_separator\": \"underscore\",\n    \"image_format\": \"exr\",\n    \"multipass\": True\n}\n"
  },
  {
    "path": "server_addon/max/server/version.py",
    "content": "__version__ = \"0.1.5\"\n"
  },
  {
    "path": "server_addon/maya/LICENCE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "server_addon/maya/README.md",
    "content": "Maya Integration Addon\n======================\n\nWIP\n"
  },
  {
    "path": "server_addon/maya/server/__init__.py",
    "content": "\"\"\"Maya Addon Module\"\"\"\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .settings.main import MayaSettings, DEFAULT_MAYA_SETTING\nfrom .version import __version__\n\n\nclass MayaAddon(BaseServerAddon):\n    name = \"maya\"\n    title = \"Maya\"\n    version = __version__\n    settings_model = MayaSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_MAYA_SETTING)\n"
  },
  {
    "path": "server_addon/maya/server/settings/__init__.py",
    "content": ""
  },
  {
    "path": "server_addon/maya/server/settings/creators.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    task_types_enum,\n)\n\n\nclass CreateLookModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    make_tx: bool = SettingsField(title=\"Make tx files\")\n    rs_tex: bool = SettingsField(title=\"Make Redshift texture files\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list, title=\"Default Products\"\n    )\n\n\nclass BasicCreatorModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Default Products\"\n    )\n\n\nclass CreateUnrealStaticMeshModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Default Products\"\n    )\n    static_mesh_prefix: str = SettingsField(\"S\", title=\"Static Mesh Prefix\")\n    collision_prefixes: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Collision Prefixes\"\n    )\n\n\nclass CreateUnrealSkeletalMeshModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list, title=\"Default Products\")\n    joint_hints: str = SettingsField(\"jnt_org\", title=\"Joint root hint\")\n\n\nclass CreateMultiverseLookModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    publish_mip_map: bool = SettingsField(title=\"publish_mip_map\")\n\n\nclass BasicExportMeshModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    write_color_sets: bool = SettingsField(title=\"Write Color Sets\")\n    write_face_sets: bool = SettingsField(title=\"Write Face Sets\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Default Products\"\n    )\n\n\nclass CreateAnimationModel(BaseSettingsModel):\n    write_color_sets: bool = SettingsField(title=\"Write Color Sets\")\n    write_face_sets: bool = SettingsField(title=\"Write Face Sets\")\n    include_parent_hierarchy: bool = SettingsField(\n        title=\"Include Parent Hierarchy\")\n    include_user_defined_attributes: bool = SettingsField(\n        title=\"Include User Defined Attributes\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Default Products\"\n    )\n\n\nclass CreatePointCacheModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    write_color_sets: bool = SettingsField(title=\"Write Color Sets\")\n    write_face_sets: bool = SettingsField(title=\"Write Face Sets\")\n    include_user_defined_attributes: bool = SettingsField(\n        title=\"Include User Defined Attributes\"\n    )\n    default_variants: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Default Products\"\n    )\n\n\nclass CreateProxyAlembicModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    write_color_sets: bool = SettingsField(title=\"Write Color Sets\")\n    write_face_sets: bool = SettingsField(title=\"Write Face Sets\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Default Products\"\n    )\n\n\nclass CreateAssModel(BasicCreatorModel):\n    expandProcedurals: bool = SettingsField(title=\"Expand Procedurals\")\n    motionBlur: bool = SettingsField(title=\"Motion Blur\")\n    motionBlurKeys: int = SettingsField(2, title=\"Motion Blur Keys\")\n    motionBlurLength: float = SettingsField(0.5, title=\"Motion Blur Length\")\n    maskOptions: bool = SettingsField(title=\"Mask Options\")\n    maskCamera: bool = SettingsField(title=\"Mask Camera\")\n    maskLight: bool = SettingsField(title=\"Mask Light\")\n    maskShape: bool = SettingsField(title=\"Mask Shape\")\n    maskShader: bool = SettingsField(title=\"Mask Shader\")\n    maskOverride: bool = SettingsField(title=\"Mask Override\")\n    maskDriver: bool = SettingsField(title=\"Mask Driver\")\n    maskFilter: bool = SettingsField(title=\"Mask Filter\")\n    maskColor_manager: bool = SettingsField(title=\"Mask Color Manager\")\n    maskOperator: bool = SettingsField(title=\"Mask Operator\")\n\n\nclass CreateReviewModel(BasicCreatorModel):\n    useMayaTimeline: bool = SettingsField(\n        title=\"Use Maya Timeline for Frame Range.\"\n    )\n\n\nclass CreateVrayProxyModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    vrmesh: bool = SettingsField(title=\"VrMesh\")\n    alembic: bool = SettingsField(title=\"Alembic\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list, title=\"Default Products\")\n\n\nclass CreateMultishotLayout(BasicCreatorModel):\n    shotParent: str = SettingsField(title=\"Shot Parent Folder\")\n    groupLoadedAssets: bool = SettingsField(title=\"Group Loaded Assets\")\n    task_type: list[str] = SettingsField(\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    task_name: str = SettingsField(title=\"Task name (regex)\")\n\n\nclass CreatorsModel(BaseSettingsModel):\n    CreateLook: CreateLookModel = SettingsField(\n        default_factory=CreateLookModel,\n        title=\"Create Look\"\n    )\n    CreateRender: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create Render\"\n    )\n    # \"-\" is not compatible in the new model\n    CreateUnrealStaticMesh: CreateUnrealStaticMeshModel = SettingsField(\n        default_factory=CreateUnrealStaticMeshModel,\n        title=\"Create Unreal_Static Mesh\"\n    )\n    # \"-\" is not compatible in the new model\n    CreateUnrealSkeletalMesh: CreateUnrealSkeletalMeshModel = SettingsField(\n        default_factory=CreateUnrealSkeletalMeshModel,\n        title=\"Create Unreal_Skeletal Mesh\"\n    )\n    CreateMultiverseLook: CreateMultiverseLookModel = SettingsField(\n        default_factory=CreateMultiverseLookModel,\n        title=\"Create Multiverse Look\"\n    )\n    CreateAnimation: CreateAnimationModel = SettingsField(\n        default_factory=CreateAnimationModel,\n        title=\"Create Animation\"\n    )\n    CreateModel: BasicExportMeshModel = SettingsField(\n        default_factory=BasicExportMeshModel,\n        title=\"Create Model\"\n    )\n    CreatePointCache: CreatePointCacheModel = SettingsField(\n        default_factory=CreatePointCacheModel,\n        title=\"Create Point Cache\"\n    )\n    CreateProxyAlembic: CreateProxyAlembicModel = SettingsField(\n        default_factory=CreateProxyAlembicModel,\n        title=\"Create Proxy Alembic\"\n    )\n    CreateMultiverseUsd: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create Multiverse USD\"\n    )\n    CreateMultiverseUsdComp: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create Multiverse USD Composition\"\n    )\n    CreateMultiverseUsdOver: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create Multiverse USD Override\"\n    )\n    CreateAss: CreateAssModel = SettingsField(\n        default_factory=CreateAssModel,\n        title=\"Create Ass\"\n    )\n    CreateAssembly: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create Assembly\"\n    )\n    CreateCamera: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create Camera\"\n    )\n    CreateLayout: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create Layout\"\n    )\n    CreateMayaScene: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create Maya Scene\"\n    )\n    CreateRenderSetup: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create Render Setup\"\n    )\n    CreateReview: CreateReviewModel = SettingsField(\n        default_factory=CreateReviewModel,\n        title=\"Create Review\"\n    )\n    CreateRig: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create Rig\"\n    )\n    CreateSetDress: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create Set Dress\"\n    )\n    CreateVrayProxy: CreateVrayProxyModel = SettingsField(\n        default_factory=CreateVrayProxyModel,\n        title=\"Create VRay Proxy\"\n    )\n    CreateVRayScene: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create VRay Scene\"\n    )\n    CreateYetiRig: BasicCreatorModel = SettingsField(\n        default_factory=BasicCreatorModel,\n        title=\"Create Yeti Rig\"\n    )\n\n\nDEFAULT_CREATORS_SETTINGS = {\n    \"CreateLook\": {\n        \"enabled\": True,\n        \"make_tx\": True,\n        \"rs_tex\": False,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateRender\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateUnrealStaticMesh\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"\",\n            \"_Main\"\n        ],\n        \"static_mesh_prefix\": \"S\",\n        \"collision_prefixes\": [\n            \"UBX\",\n            \"UCP\",\n            \"USP\",\n            \"UCX\"\n        ]\n    },\n    \"CreateUnrealSkeletalMesh\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\",\n        ],\n        \"joint_hints\": \"jnt_org\"\n    },\n    \"CreateMultiverseLook\": {\n        \"enabled\": True,\n        \"publish_mip_map\": True\n    },\n    \"CreateAnimation\": {\n        \"write_color_sets\": False,\n        \"write_face_sets\": False,\n        \"include_parent_hierarchy\": False,\n        \"include_user_defined_attributes\": False,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateModel\": {\n        \"enabled\": True,\n        \"write_color_sets\": False,\n        \"write_face_sets\": False,\n        \"default_variants\": [\n            \"Main\",\n            \"Proxy\",\n            \"Sculpt\"\n        ]\n    },\n    \"CreatePointCache\": {\n        \"enabled\": True,\n        \"write_color_sets\": False,\n        \"write_face_sets\": False,\n        \"include_user_defined_attributes\": False,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateProxyAlembic\": {\n        \"enabled\": True,\n        \"write_color_sets\": False,\n        \"write_face_sets\": False,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateMultiverseUsd\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateMultiverseUsdComp\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateMultiverseUsdOver\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateAss\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ],\n        \"expandProcedurals\": False,\n        \"motionBlur\": True,\n        \"motionBlurKeys\": 2,\n        \"motionBlurLength\": 0.5,\n        \"maskOptions\": False,\n        \"maskCamera\": False,\n        \"maskLight\": False,\n        \"maskShape\": False,\n        \"maskShader\": False,\n        \"maskOverride\": False,\n        \"maskDriver\": False,\n        \"maskFilter\": False,\n        \"maskColor_manager\": False,\n        \"maskOperator\": False\n    },\n    \"CreateAssembly\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateCamera\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateLayout\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateMayaScene\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateRenderSetup\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateReview\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ],\n        \"useMayaTimeline\": True\n    },\n    \"CreateRig\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\",\n            \"Sim\",\n            \"Cloth\"\n        ]\n    },\n    \"CreateSetDress\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\",\n            \"Anim\"\n        ]\n    },\n    \"CreateVrayProxy\": {\n        \"enabled\": True,\n        \"vrmesh\": True,\n        \"alembic\": True,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateVRayScene\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"CreateYetiRig\": {\n        \"enabled\": True,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    }\n}\n"
  },
  {
    "path": "server_addon/maya/server/settings/explicit_plugins_loading.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass PluginsModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    enabled: bool = SettingsField(title=\"Enabled\")\n    name: str = SettingsField(\"\", title=\"Name\")\n\n\nclass ExplicitPluginsLoadingModel(BaseSettingsModel):\n    \"\"\"Maya Explicit Plugins Loading.\"\"\"\n    _isGroup: bool = True\n    enabled: bool = SettingsField(title=\"enabled\")\n    plugins_to_load: list[PluginsModel] = SettingsField(\n        default_factory=list, title=\"Plugins To Load\"\n    )\n\n\nDEFAULT_EXPLITCIT_PLUGINS_LOADING_SETTINGS = {\n    \"enabled\": False,\n    \"plugins_to_load\": [\n        {\n            \"enabled\": False,\n            \"name\": \"AbcBullet\"\n        },\n        {\n            \"enabled\": True,\n            \"name\": \"AbcExport\"\n        },\n        {\n            \"enabled\": True,\n            \"name\": \"AbcImport\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"animImportExport\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"ArubaTessellator\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"ATFPlugin\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"atomImportExport\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"AutodeskPacketFile\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"autoLoader\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"bifmeshio\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"bifrostGraph\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"bifrostshellnode\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"bifrostvisplugin\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"blast2Cmd\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"bluePencil\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"Boss\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"bullet\"\n        },\n        {\n            \"enabled\": True,\n            \"name\": \"cacheEvaluator\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"cgfxShader\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"cleanPerFaceAssignment\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"clearcoat\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"convertToComponentTags\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"curveWarp\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"ddsFloatReader\"\n        },\n        {\n            \"enabled\": True,\n            \"name\": \"deformerEvaluator\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"dgProfiler\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"drawUfe\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"dx11Shader\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"fbxmaya\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"fltTranslator\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"freeze\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"Fur\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"gameFbxExporter\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"gameInputDevice\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"GamePipeline\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"gameVertexCount\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"geometryReport\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"geometryTools\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"glslShader\"\n        },\n        {\n            \"enabled\": True,\n            \"name\": \"GPUBuiltInDeformer\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"gpuCache\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"hairPhysicalShader\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"ik2Bsolver\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"ikSpringSolver\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"invertShape\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"lges\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"lookdevKit\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"MASH\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"matrixNodes\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"mayaCharacterization\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"mayaHIK\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"MayaMuscle\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"mayaUsdPlugin\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"mayaVnnPlugin\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"melProfiler\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"meshReorder\"\n        },\n        {\n            \"enabled\": True,\n            \"name\": \"modelingToolkit\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"mtoa\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"mtoh\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"nearestPointOnMesh\"\n        },\n        {\n            \"enabled\": True,\n            \"name\": \"objExport\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"OneClick\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"OpenEXRLoader\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"pgYetiMaya\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"pgyetiVrayMaya\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"polyBoolean\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"poseInterpolator\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"quatNodes\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"randomizerDevice\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"redshift4maya\"\n        },\n        {\n            \"enabled\": True,\n            \"name\": \"renderSetup\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"retargeterNodes\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"RokokoMotionLibrary\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"rotateHelper\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"sceneAssembly\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"shaderFXPlugin\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"shotCamera\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"snapTransform\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"stage\"\n        },\n        {\n            \"enabled\": True,\n            \"name\": \"stereoCamera\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"stlTranslator\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"studioImport\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"Substance\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"substancelink\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"substancemaya\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"substanceworkflow\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"svgFileTranslator\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"sweep\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"testify\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"tiffFloatReader\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"timeSliderBookmark\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"Turtle\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"Type\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"udpDevice\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"ufeSupport\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"Unfold3D\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"VectorRender\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"vrayformaya\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"vrayvolumegrid\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"xgenToolkit\"\n        },\n        {\n            \"enabled\": False,\n            \"name\": \"xgenVray\"\n        }\n    ]\n}\n"
  },
  {
    "path": "server_addon/maya/server/settings/imageio.py",
    "content": "\"\"\"Providing models and setting values for image IO in Maya.\n\nNote: Names were changed to get rid of the versions in class names.\n\"\"\"\nfrom pydantic import validator\n\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n)\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ColorManagementPreferenceV2Model(BaseSettingsModel):\n    \"\"\"Color Management Preference v2 (Maya 2022+).\n\n    Please migrate all to 'imageio/workfile' and enable it.\n    \"\"\"\n\n    enabled: bool = SettingsField(\n        True, title=\"Use Color Management Preference v2\"\n    )\n\n    renderSpace: str = SettingsField(title=\"Rendering Space\")\n    displayName: str = SettingsField(title=\"Display\")\n    viewName: str = SettingsField(title=\"View\")\n\n\nclass ColorManagementPreferenceModel(BaseSettingsModel):\n    \"\"\"Color Management Preference (legacy).\"\"\"\n\n    renderSpace: str = SettingsField(title=\"Rendering Space\")\n    viewTransform: str = SettingsField(title=\"Viewer Transform \")\n\n\nclass WorkfileImageIOModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    renderSpace: str = SettingsField(title=\"Rendering Space\")\n    displayName: str = SettingsField(title=\"Display\")\n    viewName: str = SettingsField(title=\"View\")\n\n\nclass ImageIOSettings(BaseSettingsModel):\n    \"\"\"Maya color management project settings.\n\n    Todo: What to do with color management preferences version?\n    \"\"\"\n\n    _isGroup: bool = True\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n    workfile: WorkfileImageIOModel = SettingsField(\n        default_factory=WorkfileImageIOModel,\n        title=\"Workfile\"\n    )\n    # Deprecated\n    colorManagementPreference_v2: ColorManagementPreferenceV2Model = (\n        SettingsField(\n            default_factory=ColorManagementPreferenceV2Model,\n            title=\"DEPRECATED: Color Management Preference v2 (Maya 2022+)\"\n        )\n    )\n    colorManagementPreference: ColorManagementPreferenceModel = SettingsField(\n        default_factory=ColorManagementPreferenceModel,\n        title=\"DEPRECATED: Color Management Preference (legacy)\"\n    )\n\n\nDEFAULT_IMAGEIO_SETTINGS = {\n    \"activate_host_color_management\": True,\n    \"ocio_config\": {\n        \"override_global_config\": False,\n        \"filepath\": []\n    },\n    \"file_rules\": {\n        \"activate_host_rules\": False,\n        \"rules\": []\n    },\n    \"workfile\": {\n        \"enabled\": False,\n        \"renderSpace\": \"ACES - ACEScg\",\n        \"displayName\": \"ACES\",\n        \"viewName\": \"sRGB\"\n    },\n    \"colorManagementPreference_v2\": {\n        \"enabled\": True,\n        \"renderSpace\": \"ACEScg\",\n        \"displayName\": \"sRGB\",\n        \"viewName\": \"ACES 1.0 SDR-video\"\n    },\n    \"colorManagementPreference\": {\n        \"renderSpace\": \"scene-linear Rec 709/sRGB\",\n        \"viewTransform\": \"sRGB gamma\"\n    }\n}\n"
  },
  {
    "path": "server_addon/maya/server/settings/include_handles.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    task_types_enum,\n)\n\n\nclass IncludeByTaskTypeModel(BaseSettingsModel):\n    task_type: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    include_handles: bool = SettingsField(True, title=\"Include handles\")\n\n\nclass IncludeHandlesModel(BaseSettingsModel):\n    \"\"\"Maya dirmap settings.\"\"\"\n    # _layout = \"expanded\"\n    include_handles_default: bool = SettingsField(\n        True, title=\"Include handles by default\"\n    )\n    per_task_type: list[IncludeByTaskTypeModel] = SettingsField(\n        default_factory=list,\n        title=\"Include/exclude handles by task type\"\n    )\n\n\nDEFAULT_INCLUDE_HANDLES = {\n    \"include_handles_default\": False,\n    \"per_task_type\": []\n}\n"
  },
  {
    "path": "server_addon/maya/server/settings/loaders.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.types import ColorRGBA_uint8\n\n\nclass ColorsSetting(BaseSettingsModel):\n    model: ColorRGBA_uint8 = SettingsField(\n        (209, 132, 30, 1.0), title=\"Model:\")\n    rig: ColorRGBA_uint8 = SettingsField(\n        (59, 226, 235, 1.0), title=\"Rig:\")\n    pointcache: ColorRGBA_uint8 = SettingsField(\n        (94, 209, 30, 1.0), title=\"Pointcache:\")\n    animation: ColorRGBA_uint8 = SettingsField(\n        (94, 209, 30, 1.0), title=\"Animation:\")\n    ass: ColorRGBA_uint8 = SettingsField(\n        (249, 135, 53, 1.0), title=\"Arnold StandIn:\")\n    camera: ColorRGBA_uint8 = SettingsField(\n        (136, 114, 244, 1.0), title=\"Camera:\")\n    fbx: ColorRGBA_uint8 = SettingsField(\n        (215, 166, 255, 1.0), title=\"FBX:\")\n    mayaAscii: ColorRGBA_uint8 = SettingsField(\n        (67, 174, 255, 1.0), title=\"Maya Ascii:\")\n    mayaScene: ColorRGBA_uint8 = SettingsField(\n        (67, 174, 255, 1.0), title=\"Maya Scene:\")\n    setdress: ColorRGBA_uint8 = SettingsField(\n        (255, 250, 90, 1.0), title=\"Set Dress:\")\n    layout: ColorRGBA_uint8 = SettingsField((\n        255, 250, 90, 1.0), title=\"Layout:\")\n    vdbcache: ColorRGBA_uint8 = SettingsField(\n        (249, 54, 0, 1.0), title=\"VDB Cache:\")\n    vrayproxy: ColorRGBA_uint8 = SettingsField(\n        (255, 150, 12, 1.0), title=\"VRay Proxy:\")\n    vrayscene_layer: ColorRGBA_uint8 = SettingsField(\n        (255, 150, 12, 1.0), title=\"VRay Scene:\")\n    yeticache: ColorRGBA_uint8 = SettingsField(\n        (99, 206, 220, 1.0), title=\"Yeti Cache:\")\n    yetiRig: ColorRGBA_uint8 = SettingsField(\n        (0, 205, 125, 1.0), title=\"Yeti Rig:\")\n\n\nclass ReferenceLoaderModel(BaseSettingsModel):\n    namespace: str = SettingsField(title=\"Namespace\")\n    group_name: str = SettingsField(title=\"Group name\")\n    display_handle: bool = SettingsField(\n        title=\"Display Handle On Load References\"\n    )\n\n\nclass ImportLoaderModel(BaseSettingsModel):\n    namespace: str = SettingsField(title=\"Namespace\")\n    group_name: str = SettingsField(title=\"Group name\")\n\n\nclass LoadersModel(BaseSettingsModel):\n    colors: ColorsSetting = SettingsField(\n        default_factory=ColorsSetting,\n        title=\"Loaded Products Outliner Colors\")\n\n    reference_loader: ReferenceLoaderModel = SettingsField(\n        default_factory=ReferenceLoaderModel,\n        title=\"Reference Loader\"\n    )\n\n    import_loader: ImportLoaderModel = SettingsField(\n        default_factory=ImportLoaderModel,\n        title=\"Import Loader\"\n    )\n\nDEFAULT_LOADERS_SETTING = {\n    \"colors\": {\n        \"model\": [\n            209, 132, 30, 1.0\n        ],\n        \"rig\": [\n            59, 226, 235, 1.0\n        ],\n        \"pointcache\": [\n            94, 209, 30, 1.0\n        ],\n        \"animation\": [\n            94, 209, 30, 1.0\n        ],\n        \"ass\": [\n            249, 135, 53, 1.0\n        ],\n        \"camera\": [\n            136, 114, 244, 1.0\n        ],\n        \"fbx\": [\n            215, 166, 255, 1.0\n        ],\n        \"mayaAscii\": [\n            67, 174, 255, 1.0\n        ],\n        \"mayaScene\": [\n            67, 174, 255, 1.0\n        ],\n        \"setdress\": [\n            255, 250, 90, 1.0\n        ],\n        \"layout\": [\n            255, 250, 90, 1.0\n        ],\n        \"vdbcache\": [\n            249, 54, 0, 1.0\n        ],\n        \"vrayproxy\": [\n            255, 150, 12, 1.0\n        ],\n        \"vrayscene_layer\": [\n            255, 150, 12, 1.0\n        ],\n        \"yeticache\": [\n            99, 206, 220, 1.0\n        ],\n        \"yetiRig\": [\n            0, 205, 125, 1.0\n        ]\n    },\n    \"reference_loader\": {\n        \"namespace\": \"{folder[name]}_{product[name]}_##_\",\n        \"group_name\": \"_GRP\",\n        \"display_handle\": True\n    },\n    \"import_loader\": {\n        \"namespace\": \"{folder[name]}_{product[name]}_##_\",\n        \"group_name\": \"_GRP\",\n        \"display_handle\": True\n    }\n}\n"
  },
  {
    "path": "server_addon/maya/server/settings/main.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n)\nfrom .imageio import ImageIOSettings, DEFAULT_IMAGEIO_SETTINGS\nfrom .maya_dirmap import MayaDirmapModel, DEFAULT_MAYA_DIRMAP_SETTINGS\nfrom .include_handles import IncludeHandlesModel, DEFAULT_INCLUDE_HANDLES\nfrom .explicit_plugins_loading import (\n    ExplicitPluginsLoadingModel, DEFAULT_EXPLITCIT_PLUGINS_LOADING_SETTINGS\n)\nfrom .scriptsmenu import ScriptsmenuModel, DEFAULT_SCRIPTSMENU_SETTINGS\nfrom .render_settings import RenderSettingsModel, DEFAULT_RENDER_SETTINGS\nfrom .creators import CreatorsModel, DEFAULT_CREATORS_SETTINGS\nfrom .publishers import PublishersModel, DEFAULT_PUBLISH_SETTINGS\nfrom .loaders import LoadersModel, DEFAULT_LOADERS_SETTING\nfrom .workfile_build_settings import ProfilesModel, DEFAULT_WORKFILE_SETTING\nfrom .templated_workfile_settings import (\n    TemplatedProfilesModel, DEFAULT_TEMPLATED_WORKFILE_SETTINGS\n)\n\n\nclass ExtMappingItemModel(BaseSettingsModel):\n    _layout = \"compact\"\n    name: str = SettingsField(title=\"Product type\")\n    value: str = SettingsField(title=\"Extension\")\n\n\nclass MayaSettings(BaseSettingsModel):\n    \"\"\"Maya Project Settings.\"\"\"\n\n    open_workfile_post_initialization: bool = SettingsField(\n        True, title=\"Open Workfile Post Initialization\")\n    explicit_plugins_loading: ExplicitPluginsLoadingModel = SettingsField(\n        default_factory=ExplicitPluginsLoadingModel,\n        title=\"Explicit Plugins Loading\")\n    imageio: ImageIOSettings = SettingsField(\n        default_factory=ImageIOSettings, title=\"Color Management (imageio)\")\n    mel_workspace: str = SettingsField(\n        title=\"Maya MEL Workspace\", widget=\"textarea\"\n    )\n    ext_mapping: list[ExtMappingItemModel] = SettingsField(\n        default_factory=list, title=\"Extension Mapping\")\n    maya_dirmap: MayaDirmapModel = SettingsField(\n        default_factory=MayaDirmapModel, title=\"Maya dirmap Settings\")\n    include_handles: IncludeHandlesModel = SettingsField(\n        default_factory=IncludeHandlesModel,\n        title=\"Include/Exclude Handles in default playback & render range\"\n    )\n    scriptsmenu: ScriptsmenuModel = SettingsField(\n        default_factory=ScriptsmenuModel,\n        title=\"Scriptsmenu Settings\"\n    )\n    render_settings: RenderSettingsModel = SettingsField(\n        default_factory=RenderSettingsModel, title=\"Render Settings\")\n    create: CreatorsModel = SettingsField(\n        default_factory=CreatorsModel, title=\"Creators\")\n    publish: PublishersModel = SettingsField(\n        default_factory=PublishersModel, title=\"Publishers\")\n    load: LoadersModel = SettingsField(\n        default_factory=LoadersModel, title=\"Loaders\")\n    workfile_build: ProfilesModel = SettingsField(\n        default_factory=ProfilesModel, title=\"Workfile Build Settings\")\n    templated_workfile_build: TemplatedProfilesModel = SettingsField(\n        default_factory=TemplatedProfilesModel,\n        title=\"Templated Workfile Build Settings\")\n\n    @validator(\"ext_mapping\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nDEFAULT_MEL_WORKSPACE_SETTINGS = \"\\n\".join((\n    'workspace -fr \"shaders\" \"renderData/shaders\";',\n    'workspace -fr \"images\" \"renders/maya\";',\n    'workspace -fr \"particles\" \"particles\";',\n    'workspace -fr \"mayaAscii\" \"\";',\n    'workspace -fr \"mayaBinary\" \"\";',\n    'workspace -fr \"scene\" \"\";',\n    'workspace -fr \"alembicCache\" \"cache/alembic\";',\n    'workspace -fr \"renderData\" \"renderData\";',\n    'workspace -fr \"sourceImages\" \"sourceimages\";',\n    'workspace -fr \"fileCache\" \"cache/nCache\";',\n    'workspace -fr \"autoSave\" \"autosave\";',\n    '',\n))\n\nDEFAULT_MAYA_SETTING = {\n    \"open_workfile_post_initialization\": True,\n    \"explicit_plugins_loading\": DEFAULT_EXPLITCIT_PLUGINS_LOADING_SETTINGS,\n    \"imageio\": DEFAULT_IMAGEIO_SETTINGS,\n    \"mel_workspace\": DEFAULT_MEL_WORKSPACE_SETTINGS,\n    \"ext_mapping\": [\n        {\"name\": \"model\", \"value\": \"ma\"},\n        {\"name\": \"mayaAscii\", \"value\": \"ma\"},\n        {\"name\": \"camera\", \"value\": \"ma\"},\n        {\"name\": \"rig\", \"value\": \"ma\"},\n        {\"name\": \"workfile\", \"value\": \"ma\"},\n        {\"name\": \"yetiRig\", \"value\": \"ma\"}\n    ],\n    # `maya_dirmap` was originally with dash - `maya-dirmap`\n    \"maya_dirmap\": DEFAULT_MAYA_DIRMAP_SETTINGS,\n    \"include_handles\": DEFAULT_INCLUDE_HANDLES,\n    \"scriptsmenu\": DEFAULT_SCRIPTSMENU_SETTINGS,\n    \"render_settings\": DEFAULT_RENDER_SETTINGS,\n    \"create\": DEFAULT_CREATORS_SETTINGS,\n    \"publish\": DEFAULT_PUBLISH_SETTINGS,\n    \"load\": DEFAULT_LOADERS_SETTING,\n    \"workfile_build\": DEFAULT_WORKFILE_SETTING,\n    \"templated_workfile_build\": DEFAULT_TEMPLATED_WORKFILE_SETTINGS\n}\n"
  },
  {
    "path": "server_addon/maya/server/settings/maya_dirmap.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass MayaDirmapPathsSubmodel(BaseSettingsModel):\n    _layout = \"compact\"\n    source_path: list[str] = SettingsField(\n        default_factory=list, title=\"Source Paths\"\n    )\n    destination_path: list[str] = SettingsField(\n        default_factory=list, title=\"Destination Paths\"\n    )\n\n\nclass MayaDirmapModel(BaseSettingsModel):\n    \"\"\"Maya dirmap settings.\"\"\"\n    # _layout = \"expanded\"\n    _isGroup: bool = True\n\n    enabled: bool = SettingsField(title=\"enabled\")\n    # Use ${} placeholder instead of absolute value of a root in\n    #   referenced filepaths.\n    use_env_var_as_root: bool = SettingsField(\n        title=\"Use env var placeholder in referenced paths\"\n    )\n    paths: MayaDirmapPathsSubmodel = SettingsField(\n        default_factory=MayaDirmapPathsSubmodel,\n        title=\"Dirmap Paths\"\n    )\n\n\nDEFAULT_MAYA_DIRMAP_SETTINGS = {\n    \"use_env_var_as_root\": False,\n    \"enabled\": False,\n    \"paths\": {\n        \"source-path\": [],\n        \"destination-path\": []\n    }\n}\n"
  },
  {
    "path": "server_addon/maya/server/settings/publish_playblast.py",
    "content": "from pydantic import validator\n\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n    task_types_enum,\n)\nfrom ayon_server.types import ColorRGBA_uint8\n\n\ndef hardware_falloff_enum():\n    return [\n        {\"label\": \"Linear\", \"value\": \"0\"},\n        {\"label\": \"Exponential\", \"value\": \"1\"},\n        {\"label\": \"Exponential Squared\", \"value\": \"2\"}\n    ]\n\n\ndef renderer_enum():\n    return [\n        {\"label\": \"Viewport 2.0\", \"value\": \"vp2Renderer\"}\n    ]\n\n\ndef displayLights_enum():\n    return [\n        {\"label\": \"Default Lighting\", \"value\": \"default\"},\n        {\"label\": \"All Lights\", \"value\": \"all\"},\n        {\"label\": \"Selected Lights\", \"value\": \"selected\"},\n        {\"label\": \"Flat Lighting\", \"value\": \"flat\"},\n        {\"label\": \"No Lights\", \"value\": \"nolights\"}\n    ]\n\n\ndef plugin_objects_default():\n    return [\n        {\n            \"name\": \"gpuCacheDisplayFilter\",\n            \"value\": False\n        }\n    ]\n\n\nclass CodecSetting(BaseSettingsModel):\n    _layout = \"expanded\"\n    compression: str = SettingsField(\"png\", title=\"Encoding\")\n    format: str = SettingsField(\"image\", title=\"Format\")\n    quality: int = SettingsField(95, title=\"Quality\", ge=0, le=100)\n\n\nclass DisplayOptionsSetting(BaseSettingsModel):\n    _layout = \"expanded\"\n    override_display: bool = SettingsField(\n        True, title=\"Override display options\"\n    )\n    background: ColorRGBA_uint8 = SettingsField(\n        (125, 125, 125, 1.0), title=\"Background Color\"\n    )\n    displayGradient: bool = SettingsField(\n        True, title=\"Display background gradient\"\n    )\n    backgroundTop: ColorRGBA_uint8 = SettingsField(\n        (125, 125, 125, 1.0), title=\"Background Top\"\n    )\n    backgroundBottom: ColorRGBA_uint8 = SettingsField(\n        (125, 125, 125, 1.0), title=\"Background Bottom\"\n    )\n\n\nclass GenericSetting(BaseSettingsModel):\n    _layout = \"expanded\"\n    isolate_view: bool = SettingsField(True, title=\"Isolate View\")\n    off_screen: bool = SettingsField(True, title=\"Off Screen\")\n    pan_zoom: bool = SettingsField(False, title=\"2D Pan/Zoom\")\n\n\nclass RendererSetting(BaseSettingsModel):\n    _layout = \"expanded\"\n    rendererName: str = SettingsField(\n        \"vp2Renderer\",\n        enum_resolver=renderer_enum,\n        title=\"Renderer name\"\n    )\n\n\nclass ResolutionSetting(BaseSettingsModel):\n    _layout = \"expanded\"\n    width: int = SettingsField(0, title=\"Width\")\n    height: int = SettingsField(0, title=\"Height\")\n\n\nclass PluginObjectsModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Name\")\n    value: bool = SettingsField(True, title=\"Enabled\")\n\n\nclass ViewportOptionsSetting(BaseSettingsModel):\n    override_viewport_options: bool = SettingsField(\n        True, title=\"Override viewport options\"\n    )\n    displayLights: str = SettingsField(\n        \"default\", enum_resolver=displayLights_enum, title=\"Display Lights\"\n    )\n    displayTextures: bool = SettingsField(True, title=\"Display Textures\")\n    textureMaxResolution: int = SettingsField(\n        1024, title=\"Texture Clamp Resolution\"\n    )\n    renderDepthOfField: bool = SettingsField(\n        True, title=\"Depth of Field\", section=\"Depth of Field\"\n    )\n    shadows: bool = SettingsField(True, title=\"Display Shadows\")\n    twoSidedLighting: bool = SettingsField(True, title=\"Two Sided Lighting\")\n    lineAAEnable: bool = SettingsField(\n        True, title=\"Enable Anti-Aliasing\", section=\"Anti-Aliasing\"\n    )\n    multiSample: int = SettingsField(8, title=\"Anti Aliasing Samples\")\n    loadTextures: bool = SettingsField(False, title=\"Load Textures\")\n    useDefaultMaterial: bool = SettingsField(\n        False, title=\"Use Default Material\"\n    )\n    wireframeOnShaded: bool = SettingsField(False, title=\"Wireframe On Shaded\")\n    xray: bool = SettingsField(False, title=\"X-Ray\")\n    jointXray: bool = SettingsField(False, title=\"X-Ray Joints\")\n    backfaceCulling: bool = SettingsField(False, title=\"Backface Culling\")\n    ssaoEnable: bool = SettingsField(\n        False, title=\"Screen Space Ambient Occlusion\", section=\"SSAO\"\n    )\n    ssaoAmount: int = SettingsField(1, title=\"SSAO Amount\")\n    ssaoRadius: int = SettingsField(16, title=\"SSAO Radius\")\n    ssaoFilterRadius: int = SettingsField(16, title=\"SSAO Filter Radius\")\n    ssaoSamples: int = SettingsField(16, title=\"SSAO Samples\")\n    fogging: bool = SettingsField(\n        False, title=\"Enable Hardware Fog\", section=\"Fog\"\n    )\n    hwFogFalloff: str = SettingsField(\n        \"0\", enum_resolver=hardware_falloff_enum, title=\"Hardware Falloff\"\n    )\n    hwFogDensity: float = SettingsField(0.0, title=\"Fog Density\")\n    hwFogStart: int = SettingsField(0, title=\"Fog Start\")\n    hwFogEnd: int = SettingsField(100, title=\"Fog End\")\n    hwFogAlpha: int = SettingsField(0, title=\"Fog Alpha\")\n    hwFogColorR: float = SettingsField(1.0, title=\"Fog Color R\")\n    hwFogColorG: float = SettingsField(1.0, title=\"Fog Color G\")\n    hwFogColorB: float = SettingsField(1.0, title=\"Fog Color B\")\n    motionBlurEnable: bool = SettingsField(\n        False, title=\"Enable Motion Blur\", section=\"Motion Blur\"\n    )\n    motionBlurSampleCount: int = SettingsField(\n        8, title=\"Motion Blur Sample Count\"\n    )\n    motionBlurShutterOpenFraction: float = SettingsField(\n        0.2, title=\"Shutter Open Fraction\"\n    )\n    cameras: bool = SettingsField(False, title=\"Cameras\", section=\"Show\")\n    clipGhosts: bool = SettingsField(False, title=\"Clip Ghosts\")\n    deformers: bool = SettingsField(False, title=\"Deformers\")\n    dimensions: bool = SettingsField(False, title=\"Dimensions\")\n    dynamicConstraints: bool = SettingsField(\n        False, title=\"Dynamic Constraints\"\n    )\n    dynamics: bool = SettingsField(False, title=\"Dynamics\")\n    fluids: bool = SettingsField(False, title=\"Fluids\")\n    follicles: bool = SettingsField(False, title=\"Follicles\")\n    greasePencils: bool = SettingsField(False, title=\"Grease Pencils\")\n    grid: bool = SettingsField(False, title=\"Grid\")\n    hairSystems: bool = SettingsField(True, title=\"Hair Systems\")\n    handles: bool = SettingsField(False, title=\"Handles\")\n    headsUpDisplay: bool = SettingsField(False, title=\"HUD\")\n    ikHandles: bool = SettingsField(False, title=\"IK Handles\")\n    imagePlane: bool = SettingsField(True, title=\"Image Plane\")\n    joints: bool = SettingsField(False, title=\"Joints\")\n    lights: bool = SettingsField(False, title=\"Lights\")\n    locators: bool = SettingsField(False, title=\"Locators\")\n    manipulators: bool = SettingsField(False, title=\"Manipulators\")\n    motionTrails: bool = SettingsField(False, title=\"Motion Trails\")\n    nCloths: bool = SettingsField(False, title=\"nCloths\")\n    nParticles: bool = SettingsField(False, title=\"nParticles\")\n    nRigids: bool = SettingsField(False, title=\"nRigids\")\n    controlVertices: bool = SettingsField(False, title=\"NURBS CVs\")\n    nurbsCurves: bool = SettingsField(False, title=\"NURBS Curves\")\n    hulls: bool = SettingsField(False, title=\"NURBS Hulls\")\n    nurbsSurfaces: bool = SettingsField(False, title=\"NURBS Surfaces\")\n    particleInstancers: bool = SettingsField(\n        False, title=\"Particle Instancers\"\n    )\n    pivots: bool = SettingsField(False, title=\"Pivots\")\n    planes: bool = SettingsField(False, title=\"Planes\")\n    pluginShapes: bool = SettingsField(False, title=\"Plugin Shapes\")\n    polymeshes: bool = SettingsField(True, title=\"Polygons\")\n    strokes: bool = SettingsField(False, title=\"Strokes\")\n    subdivSurfaces: bool = SettingsField(False, title=\"Subdiv Surfaces\")\n    textures: bool = SettingsField(False, title=\"Texture Placements\")\n    pluginObjects: list[PluginObjectsModel] = SettingsField(\n        default_factory=plugin_objects_default,\n        title=\"Plugin Objects\"\n    )\n\n    @validator(\"pluginObjects\")\n    def validate_unique_plugin_objects(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass CameraOptionsSetting(BaseSettingsModel):\n    displayGateMask: bool = SettingsField(False, title=\"Display Gate Mask\")\n    displayResolution: bool = SettingsField(False, title=\"Display Resolution\")\n    displayFilmGate: bool = SettingsField(False, title=\"Display Film Gate\")\n    displayFieldChart: bool = SettingsField(False, title=\"Display Field Chart\")\n    displaySafeAction: bool = SettingsField(False, title=\"Display Safe Action\")\n    displaySafeTitle: bool = SettingsField(False, title=\"Display Safe Title\")\n    displayFilmPivot: bool = SettingsField(False, title=\"Display Film Pivot\")\n    displayFilmOrigin: bool = SettingsField(False, title=\"Display Film Origin\")\n    overscan: int = SettingsField(1.0, title=\"Overscan\")\n\n\nclass CapturePresetSetting(BaseSettingsModel):\n    Codec: CodecSetting = SettingsField(\n        default_factory=CodecSetting,\n        title=\"Codec\",\n        section=\"Codec\")\n    DisplayOptions: DisplayOptionsSetting = SettingsField(\n        default_factory=DisplayOptionsSetting,\n        title=\"Display Options\",\n        section=\"Display Options\")\n    Generic: GenericSetting = SettingsField(\n        default_factory=GenericSetting,\n        title=\"Generic\",\n        section=\"Generic\")\n    Renderer: RendererSetting = SettingsField(\n        default_factory=RendererSetting,\n        title=\"Renderer\",\n        section=\"Renderer\")\n    Resolution: ResolutionSetting = SettingsField(\n        default_factory=ResolutionSetting,\n        title=\"Resolution\",\n        section=\"Resolution\")\n    ViewportOptions: ViewportOptionsSetting = SettingsField(\n        default_factory=ViewportOptionsSetting,\n        title=\"Viewport Options\")\n    CameraOptions: CameraOptionsSetting = SettingsField(\n        default_factory=CameraOptionsSetting,\n        title=\"Camera Options\")\n\n\nclass ProfilesModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    task_names: list[str] = SettingsField(\n        default_factory=list, title=\"Task names\"\n    )\n    product_names: list[str] = SettingsField(\n        default_factory=list, title=\"Products names\"\n    )\n    capture_preset: CapturePresetSetting = SettingsField(\n        default_factory=CapturePresetSetting,\n        title=\"Capture Preset\"\n    )\n\n\nclass ExtractPlayblastSetting(BaseSettingsModel):\n    capture_preset: CapturePresetSetting = SettingsField(\n        default_factory=CapturePresetSetting,\n        title=\"DEPRECATED! Please use \\\"Profiles\\\" below. Capture Preset\"\n    )\n    profiles: list[ProfilesModel] = SettingsField(\n        default_factory=list,\n        title=\"Profiles\"\n    )\n\n\nDEFAULT_PLAYBLAST_SETTING = {\n    \"capture_preset\": {\n        \"Codec\": {\n            \"compression\": \"png\",\n            \"format\": \"image\",\n            \"quality\": 95\n        },\n        \"DisplayOptions\": {\n            \"override_display\": True,\n            \"background\": [\n                125,\n                125,\n                125,\n                1.0\n            ],\n            \"backgroundBottom\": [\n                125,\n                125,\n                125,\n                1.0\n            ],\n            \"backgroundTop\": [\n                125,\n                125,\n                125,\n                1.0\n            ],\n            \"displayGradient\": True\n        },\n        \"Generic\": {\n            \"isolate_view\": True,\n            \"off_screen\": True,\n            \"pan_zoom\": False\n        },\n        \"Renderer\": {\n            \"rendererName\": \"vp2Renderer\"\n        },\n        \"Resolution\": {\n            \"width\": 1920,\n            \"height\": 1080\n        },\n        \"ViewportOptions\": {\n            \"override_viewport_options\": True,\n            \"displayLights\": \"default\",\n            \"displayTextures\": True,\n            \"textureMaxResolution\": 1024,\n            \"renderDepthOfField\": True,\n            \"shadows\": True,\n            \"twoSidedLighting\": True,\n            \"lineAAEnable\": True,\n            \"multiSample\": 8,\n            \"loadTextures\": False,\n            \"useDefaultMaterial\": False,\n            \"wireframeOnShaded\": False,\n            \"xray\": False,\n            \"jointXray\": False,\n            \"backfaceCulling\": False,\n            \"ssaoEnable\": False,\n            \"ssaoAmount\": 1,\n            \"ssaoRadius\": 16,\n            \"ssaoFilterRadius\": 16,\n            \"ssaoSamples\": 16,\n            \"fogging\": False,\n            \"hwFogFalloff\": \"0\",\n            \"hwFogDensity\": 0.0,\n            \"hwFogStart\": 0,\n            \"hwFogEnd\": 100,\n            \"hwFogAlpha\": 0,\n            \"hwFogColorR\": 1.0,\n            \"hwFogColorG\": 1.0,\n            \"hwFogColorB\": 1.0,\n            \"motionBlurEnable\": False,\n            \"motionBlurSampleCount\": 8,\n            \"motionBlurShutterOpenFraction\": 0.2,\n            \"cameras\": False,\n            \"clipGhosts\": False,\n            \"deformers\": False,\n            \"dimensions\": False,\n            \"dynamicConstraints\": False,\n            \"dynamics\": False,\n            \"fluids\": False,\n            \"follicles\": False,\n            \"greasePencils\": False,\n            \"grid\": False,\n            \"hairSystems\": True,\n            \"handles\": False,\n            \"headsUpDisplay\": False,\n            \"ikHandles\": False,\n            \"imagePlane\": True,\n            \"joints\": False,\n            \"lights\": False,\n            \"locators\": False,\n            \"manipulators\": False,\n            \"motionTrails\": False,\n            \"nCloths\": False,\n            \"nParticles\": False,\n            \"nRigids\": False,\n            \"controlVertices\": False,\n            \"nurbsCurves\": False,\n            \"hulls\": False,\n            \"nurbsSurfaces\": False,\n            \"particleInstancers\": False,\n            \"pivots\": False,\n            \"planes\": False,\n            \"pluginShapes\": False,\n            \"polymeshes\": True,\n            \"strokes\": False,\n            \"subdivSurfaces\": False,\n            \"textures\": False,\n            \"pluginObjects\": [\n                {\n                    \"name\": \"gpuCacheDisplayFilter\",\n                    \"value\": False\n                }\n            ]\n        },\n        \"CameraOptions\": {\n            \"displayGateMask\": False,\n            \"displayResolution\": False,\n            \"displayFilmGate\": False,\n            \"displayFieldChart\": False,\n            \"displaySafeAction\": False,\n            \"displaySafeTitle\": False,\n            \"displayFilmPivot\": False,\n            \"displayFilmOrigin\": False,\n            \"overscan\": 1.0\n        }\n    },\n    \"profiles\": []\n}\n"
  },
  {
    "path": "server_addon/maya/server/settings/publishers.py",
    "content": "import json\nfrom pydantic import validator\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    MultiplatformPathModel,\n    ensure_unique_names,\n)\nfrom ayon_server.exceptions import BadRequestException\nfrom .publish_playblast import (\n    ExtractPlayblastSetting,\n    DEFAULT_PLAYBLAST_SETTING,\n)\n\n\ndef linear_unit_enum():\n    \"\"\"Get linear units enumerator.\"\"\"\n    return [\n        {\"label\": \"mm\", \"value\": \"millimeter\"},\n        {\"label\": \"cm\", \"value\": \"centimeter\"},\n        {\"label\": \"m\", \"value\": \"meter\"},\n        {\"label\": \"km\", \"value\": \"kilometer\"},\n        {\"label\": \"in\", \"value\": \"inch\"},\n        {\"label\": \"ft\", \"value\": \"foot\"},\n        {\"label\": \"yd\", \"value\": \"yard\"},\n        {\"label\": \"mi\", \"value\": \"mile\"}\n    ]\n\n\ndef angular_unit_enum():\n    \"\"\"Get angular units enumerator.\"\"\"\n    return [\n        {\"label\": \"deg\", \"value\": \"degree\"},\n        {\"label\": \"rad\", \"value\": \"radian\"},\n    ]\n\n\nclass BasicValidateModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n\n\nclass ValidateMeshUVSetMap1Model(BasicValidateModel):\n    \"\"\"Validate model's default uv set exists and is named 'map1'.\"\"\"\n    pass\n\n\nclass ValidateNoAnimationModel(BasicValidateModel):\n    \"\"\"Ensure no keyframes on nodes in the Instance.\"\"\"\n    pass\n\n\nclass ValidateRigOutSetNodeIdsModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"ValidateSkinclusterDeformerSet\")\n    optional: bool = SettingsField(title=\"Optional\")\n    allow_history_only: bool = SettingsField(title=\"Allow history only\")\n\n\nclass ValidateModelNameModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    database: bool = SettingsField(\n        title=\"Use database shader name definitions\"\n    )\n    material_file: MultiplatformPathModel = SettingsField(\n        default_factory=MultiplatformPathModel,\n        title=\"Material File\",\n        description=(\n            \"Path to material file defining list of material names to check.\"\n        )\n    )\n    regex: str = SettingsField(\n        \"(.*)_(\\\\d)*_(?P<shader>.*)_(GEO)\",\n        title=\"Validation regex\",\n        description=(\n            \"Regex for validating name of top level group name. You can use\"\n            \" named capturing groups:(?P<asset>.*) for Asset name\"\n        )\n    )\n    top_level_regex: str = SettingsField(\n        \".*_GRP\",\n        title=\"Top level group name regex\",\n        description=(\n            \"To check for asset in name so *_some_asset_name_GRP\"\n            \" is valid, use:.*?_(?P<asset>.*)_GEO\"\n        )\n    )\n\n\nclass ValidateModelContentModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    validate_top_group: bool = SettingsField(title=\"Validate one top group\")\n\n\nclass ValidateTransformNamingSuffixModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    SUFFIX_NAMING_TABLE: str = SettingsField(\n        \"{}\",\n        title=\"Suffix Naming Tables\",\n        widget=\"textarea\",\n        description=(\n            \"Validates transform suffix based on\"\n            \" the type of its children shapes.\"\n        )\n    )\n\n    @validator(\"SUFFIX_NAMING_TABLE\")\n    def validate_json(cls, value):\n        if not value.strip():\n            return \"{}\"\n        try:\n            converted_value = json.loads(value)\n            success = isinstance(converted_value, dict)\n        except json.JSONDecodeError:\n            success = False\n\n        if not success:\n            raise BadRequestException(\n                \"The text can't be parsed as json object\"\n            )\n        return value\n    ALLOW_IF_NOT_IN_SUFFIX_TABLE: bool = SettingsField(\n        title=\"Allow if suffix not in table\"\n    )\n\n\nclass CollectMayaRenderModel(BaseSettingsModel):\n    sync_workfile_version: bool = SettingsField(\n        title=\"Sync render version with workfile\"\n    )\n\n\nclass CollectFbxAnimationModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Collect Fbx Animation\")\n\n\nclass CollectFbxCameraModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"CollectFbxCamera\")\n\n\nclass CollectGLTFModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"CollectGLTF\")\n\n\nclass ValidateFrameRangeModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"ValidateFrameRange\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    exclude_product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Exclude product types\"\n    )\n\n\nclass ValidateShaderNameModel(BaseSettingsModel):\n    \"\"\"\n    Shader name regex can use named capture group asset to validate against current asset name.\n    \"\"\"\n    enabled: bool = SettingsField(title=\"ValidateShaderName\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    regex: str = SettingsField(\n        \"(?P<asset>.*)_(.*)_SHD\",\n        title=\"Validation regex\"\n    )\n\n\nclass ValidateAttributesModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"ValidateAttributes\")\n    attributes: str = SettingsField(\n        \"{}\", title=\"Attributes\", widget=\"textarea\")\n\n    @validator(\"attributes\")\n    def validate_json(cls, value):\n        if not value.strip():\n            return \"{}\"\n        try:\n            converted_value = json.loads(value)\n            success = isinstance(converted_value, dict)\n        except json.JSONDecodeError:\n            success = False\n\n        if not success:\n            raise BadRequestException(\n                \"The attibutes can't be parsed as json object\"\n            )\n        return value\n\n\nclass ValidateLoadedPluginModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"ValidateLoadedPlugin\")\n    optional: bool = SettingsField(title=\"Optional\")\n    whitelist_native_plugins: bool = SettingsField(\n        title=\"Whitelist Maya Native Plugins\"\n    )\n    authorized_plugins: list[str] = SettingsField(\n        default_factory=list, title=\"Authorized plugins\"\n    )\n\n\nclass ValidateMayaUnitsModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"ValidateMayaUnits\")\n    optional: bool = SettingsField(title=\"Optional\")\n    validate_linear_units: bool = SettingsField(title=\"Validate linear units\")\n    linear_units: str = SettingsField(\n        enum_resolver=linear_unit_enum, title=\"Linear Units\"\n    )\n    validate_angular_units: bool = SettingsField(\n        title=\"Validate angular units\"\n    )\n    angular_units: str = SettingsField(\n        enum_resolver=angular_unit_enum, title=\"Angular units\"\n    )\n    validate_fps: bool = SettingsField(title=\"Validate fps\")\n\n\nclass ValidateUnrealStaticMeshNameModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"ValidateUnrealStaticMeshName\")\n    optional: bool = SettingsField(title=\"Optional\")\n    validate_mesh: bool = SettingsField(title=\"Validate mesh names\")\n    validate_collision: bool = SettingsField(title=\"Validate collison names\")\n\n\nclass ValidateCycleErrorModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"ValidateCycleError\")\n    optional: bool = SettingsField(title=\"Optional\")\n    families: list[str] = SettingsField(\n        default_factory=list, title=\"Families\"\n    )\n\n\nclass ValidatePluginPathAttributesAttrModel(BaseSettingsModel):\n    name: str = SettingsField(title=\"Node type\")\n    value: str = SettingsField(title=\"Attribute\")\n\n\nclass ValidatePluginPathAttributesModel(BaseSettingsModel):\n    \"\"\"Fill in the node types and attributes you want to validate.\n\n    <p>e.g. <b>AlembicNode.abc_file</b>, the node type is <b>AlembicNode</b>\n    and the node attribute is <b>abc_file</b>\n    \"\"\"\n\n    enabled: bool = True\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    attribute: list[ValidatePluginPathAttributesAttrModel] = SettingsField(\n        default_factory=list,\n        title=\"File Attribute\"\n    )\n\n    @validator(\"attribute\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\n# Validate Render Setting\nclass RendererAttributesModel(BaseSettingsModel):\n    _layout = \"compact\"\n    type: str = SettingsField(title=\"Type\")\n    value: str = SettingsField(title=\"Value\")\n\n\nclass ValidateRenderSettingsModel(BaseSettingsModel):\n    arnold_render_attributes: list[RendererAttributesModel] = SettingsField(\n        default_factory=list, title=\"Arnold Render Attributes\")\n    vray_render_attributes: list[RendererAttributesModel] = SettingsField(\n        default_factory=list, title=\"VRay Render Attributes\")\n    redshift_render_attributes: list[RendererAttributesModel] = SettingsField(\n        default_factory=list, title=\"Redshift Render Attributes\")\n    renderman_render_attributes: list[RendererAttributesModel] = SettingsField(\n        default_factory=list, title=\"Renderman Render Attributes\")\n\n\nclass BasicValidateModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n\n\nclass ValidateCameraContentsModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    validate_shapes: bool = SettingsField(title=\"Validate presence of shapes\")\n\n\nclass ExtractProxyAlembicModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    families: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Families\")\n\n\nclass ExtractAlembicModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    families: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Families\")\n\n\nclass ExtractObjModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n\n\nclass ExtractMayaSceneRawModel(BaseSettingsModel):\n    \"\"\"Add loaded instances to those published families:\"\"\"\n    enabled: bool = SettingsField(title=\"ExtractMayaSceneRaw\")\n    add_for_families: list[str] = SettingsField(\n        default_factory=list, title=\"Families\"\n    )\n\n\nclass ExtractCameraAlembicModel(BaseSettingsModel):\n    \"\"\"\n    List of attributes that will be added to the baked alembic camera. Needs to be written in python list syntax.\n    \"\"\"\n    enabled: bool = SettingsField(title=\"ExtractCameraAlembic\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n    bake_attributes: str = SettingsField(\n        \"[]\", title=\"Base Attributes\", widget=\"textarea\"\n    )\n\n    @validator(\"bake_attributes\")\n    def validate_json_list(cls, value):\n        if not value.strip():\n            return \"[]\"\n        try:\n            converted_value = json.loads(value)\n            success = isinstance(converted_value, list)\n        except json.JSONDecodeError:\n            success = False\n\n        if not success:\n            raise BadRequestException(\n                \"The text can't be parsed as json object\"\n            )\n        return value\n\n\nclass ExtractGLBModel(BaseSettingsModel):\n    enabled: bool = True\n    active: bool = SettingsField(title=\"Active\")\n    ogsfx_path: str = SettingsField(title=\"GLSL Shader Directory\")\n\n\nclass ExtractLookArgsModel(BaseSettingsModel):\n    argument: str = SettingsField(title=\"Argument\")\n    parameters: list[str] = SettingsField(\n        default_factory=list, title=\"Parameters\"\n    )\n\n\nclass ExtractLookModel(BaseSettingsModel):\n    maketx_arguments: list[ExtractLookArgsModel] = SettingsField(\n        default_factory=list,\n        title=\"Extra arguments for maketx command line\"\n    )\n\n\nclass ExtractGPUCacheModel(BaseSettingsModel):\n    enabled: bool = True\n    families: list[str] = SettingsField(default_factory=list, title=\"Families\")\n    step: float = SettingsField(1.0, ge=1.0, title=\"Step\")\n    stepSave: int = SettingsField(1, ge=1, title=\"Step Save\")\n    optimize: bool = SettingsField(title=\"Optimize Hierarchy\")\n    optimizationThreshold: int = SettingsField(\n        1, ge=1, title=\"Optimization Threshold\"\n    )\n    optimizeAnimationsForMotionBlur: bool = SettingsField(\n        title=\"Optimize Animations For Motion Blur\"\n    )\n    writeMaterials: bool = SettingsField(title=\"Write Materials\")\n    useBaseTessellation: bool = SettingsField(title=\"User Base Tesselation\")\n\n\nclass PublishersModel(BaseSettingsModel):\n    CollectMayaRender: CollectMayaRenderModel = SettingsField(\n        default_factory=CollectMayaRenderModel,\n        title=\"Collect Render Layers\",\n        section=\"Collectors\"\n    )\n    CollectFbxAnimation: CollectFbxAnimationModel = SettingsField(\n        default_factory=CollectFbxAnimationModel,\n        title=\"Collect FBX Animation\",\n    )\n    CollectFbxCamera: CollectFbxCameraModel = SettingsField(\n        default_factory=CollectFbxCameraModel,\n        title=\"Collect Camera for FBX export\",\n    )\n    CollectGLTF: CollectGLTFModel = SettingsField(\n        default_factory=CollectGLTFModel,\n        title=\"Collect Assets for GLB/GLTF export\"\n    )\n    ValidateInstanceInContext: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Instance In Context\",\n        section=\"Validators\"\n    )\n    ValidateContainers: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Containers\"\n    )\n    ValidateFrameRange: ValidateFrameRangeModel = SettingsField(\n        default_factory=ValidateFrameRangeModel,\n        title=\"Validate Frame Range\"\n    )\n    ValidateShaderName: ValidateShaderNameModel = SettingsField(\n        default_factory=ValidateShaderNameModel,\n        title=\"Validate Shader Name\"\n    )\n    ValidateShadingEngine: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Look Shading Engine Naming\"\n    )\n    ValidateMayaColorSpace: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Colorspace\"\n    )\n    ValidateAttributes: ValidateAttributesModel = SettingsField(\n        default_factory=ValidateAttributesModel,\n        title=\"Validate Attributes\"\n    )\n    ValidateLoadedPlugin: ValidateLoadedPluginModel = SettingsField(\n        default_factory=ValidateLoadedPluginModel,\n        title=\"Validate Loaded Plugin\"\n    )\n    ValidateMayaUnits: ValidateMayaUnitsModel = SettingsField(\n        default_factory=ValidateMayaUnitsModel,\n        title=\"Validate Maya Units\"\n    )\n    ValidateUnrealStaticMeshName: ValidateUnrealStaticMeshNameModel = (\n        SettingsField(\n            default_factory=ValidateUnrealStaticMeshNameModel,\n            title=\"Validate Unreal Static Mesh Name\"\n        )\n    )\n    ValidateCycleError: ValidateCycleErrorModel = SettingsField(\n        default_factory=ValidateCycleErrorModel,\n        title=\"Validate Cycle Error\"\n    )\n    ValidatePluginPathAttributes: ValidatePluginPathAttributesModel = (\n        SettingsField(\n            default_factory=ValidatePluginPathAttributesModel,\n            title=\"Plug-in Path Attributes\"\n        )\n    )\n    ValidateRenderSettings: ValidateRenderSettingsModel = SettingsField(\n        default_factory=ValidateRenderSettingsModel,\n        title=\"Validate Render Settings\"\n    )\n    ValidateResolution: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Resolution Setting\"\n    )\n    ValidateCurrentRenderLayerIsRenderable: BasicValidateModel = (\n        SettingsField(\n            default_factory=BasicValidateModel,\n            title=\"Validate Current Render Layer Has Renderable Camera\"\n        )\n    )\n    ValidateGLSLMaterial: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate GLSL Material\"\n    )\n    ValidateGLSLPlugin: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate GLSL Plugin\"\n    )\n    ValidateRenderImageRule: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Render Image Rule (Workspace)\"\n    )\n    ValidateRenderNoDefaultCameras: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate No Default Cameras Renderable\"\n    )\n    ValidateRenderSingleCamera: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Render Single Camera \"\n    )\n    ValidateRenderLayerAOVs: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Render Passes/AOVs Are Registered\"\n    )\n    ValidateStepSize: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Step Size\"\n    )\n    ValidateVRayDistributedRendering: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"VRay Distributed Rendering\"\n    )\n    ValidateVrayReferencedAOVs: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"VRay Referenced AOVs\"\n    )\n    ValidateVRayTranslatorEnabled: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"VRay Translator Settings\"\n    )\n    ValidateVrayProxy: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"VRay Proxy Settings\"\n    )\n    ValidateVrayProxyMembers: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"VRay Proxy Members\"\n    )\n    ValidateYetiRenderScriptCallbacks: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Yeti Render Script Callbacks\"\n    )\n    ValidateYetiRigCacheState: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Yeti Rig Cache State\"\n    )\n    ValidateYetiRigInputShapesInInstance: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Yeti Rig Input Shapes In Instance\"\n    )\n    ValidateYetiRigSettings: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Yeti Rig Settings\"\n    )\n    # Model - START\n    ValidateModelName: ValidateModelNameModel = SettingsField(\n        default_factory=ValidateModelNameModel,\n        title=\"Validate Model Name\",\n        section=\"Model\",\n    )\n    ValidateModelContent: ValidateModelContentModel = SettingsField(\n        default_factory=ValidateModelContentModel,\n        title=\"Validate Model Content\",\n    )\n    ValidateTransformNamingSuffix: ValidateTransformNamingSuffixModel = (\n        SettingsField(\n            default_factory=ValidateTransformNamingSuffixModel,\n            title=\"Validate Transform Naming Suffix\",\n        )\n    )\n    ValidateColorSets: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Color Sets\",\n    )\n    ValidateMeshHasOverlappingUVs: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh Has Overlapping UVs\",\n    )\n    ValidateMeshArnoldAttributes: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh Arnold Attributes\",\n    )\n    ValidateMeshShaderConnections: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh Shader Connections\",\n    )\n    ValidateMeshSingleUVSet: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh Single UV Set\",\n    )\n    ValidateMeshHasUVs: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh Has UVs\",\n    )\n    ValidateMeshLaminaFaces: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh Lamina Faces\",\n    )\n    ValidateMeshNgons: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh Ngons\",\n    )\n    ValidateMeshNonManifold: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh Non-Manifold\",\n    )\n    ValidateMeshNoNegativeScale: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh No Negative Scale\",\n    )\n    ValidateMeshNonZeroEdgeLength: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh Edge Length Non Zero\",\n    )\n    ValidateMeshNormalsUnlocked: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh Normals Unlocked\",\n    )\n    ValidateMeshUVSetMap1: ValidateMeshUVSetMap1Model = SettingsField(\n        default_factory=ValidateMeshUVSetMap1Model,\n        title=\"Validate Mesh UV Set Map 1\",\n    )\n    ValidateMeshVerticesHaveEdges: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Mesh Vertices Have Edges\",\n    )\n    ValidateNoAnimation: ValidateNoAnimationModel = SettingsField(\n        default_factory=ValidateNoAnimationModel,\n        title=\"Validate No Animation\",\n    )\n    ValidateNoNamespace: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate No Namespace\",\n    )\n    ValidateNoNullTransforms: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate No Null Transforms\",\n    )\n    ValidateNoUnknownNodes: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate No Unknown Nodes\",\n    )\n    ValidateNodeNoGhosting: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Node No Ghosting\",\n    )\n    ValidateShapeDefaultNames: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Shape Default Names\",\n    )\n    ValidateShapeRenderStats: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Shape Render Stats\",\n    )\n    ValidateShapeZero: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Shape Zero\",\n    )\n    ValidateTransformZero: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Transform Zero\",\n    )\n    ValidateUniqueNames: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Unique Names\",\n    )\n    ValidateNoVRayMesh: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate No V-Ray Proxies (VRayMesh)\",\n    )\n    ValidateUnrealMeshTriangulated: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate if Mesh is Triangulated\",\n    )\n    ValidateAlembicVisibleOnly: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Alembic Visible Node\",\n    )\n    ExtractProxyAlembic: ExtractProxyAlembicModel = SettingsField(\n        default_factory=ExtractProxyAlembicModel,\n        title=\"Extract Proxy Alembic\",\n        section=\"Model Extractors\",\n    )\n    ExtractAlembic: ExtractAlembicModel = SettingsField(\n        default_factory=ExtractAlembicModel,\n        title=\"Extract Alembic\",\n    )\n    ExtractObj: ExtractObjModel = SettingsField(\n        default_factory=ExtractObjModel,\n        title=\"Extract OBJ\"\n    )\n    # Model - END\n\n    # Rig - START\n    ValidateRigContents: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Rig Contents\",\n        section=\"Rig\",\n    )\n    ValidateRigJointsHidden: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Rig Joints Hidden\",\n    )\n    ValidateRigControllers: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Rig Controllers\",\n    )\n    ValidateAnimatedReferenceRig: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Animated Reference Rig\",\n    )\n    ValidateAnimationContent: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Animation Content\",\n    )\n    ValidateOutRelatedNodeIds: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Animation Out Set Related Node Ids\",\n    )\n    ValidateRigControllersArnoldAttributes: BasicValidateModel = (\n        SettingsField(\n            default_factory=BasicValidateModel,\n            title=\"Validate Rig Controllers (Arnold Attributes)\",\n        )\n    )\n    ValidateSkeletalMeshHierarchy: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Skeletal Mesh Top Node\",\n    )\n    ValidateSkeletonRigContents: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Skeleton Rig Contents\"\n    )\n    ValidateSkeletonRigControllers: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Skeleton Rig Controllers\"\n    )\n    ValidateSkinclusterDeformerSet: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Skincluster Deformer Relationships\",\n    )\n    ValidateSkeletonRigOutputIds: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Skeleton Rig Output Ids\"\n    )\n    ValidateSkeletonTopGroupHierarchy: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Skeleton Top Group Hierarchy\",\n    )\n    ValidateRigOutSetNodeIds: ValidateRigOutSetNodeIdsModel = SettingsField(\n        default_factory=ValidateRigOutSetNodeIdsModel,\n        title=\"Validate Rig Out Set Node Ids\",\n    )\n    ValidateSkeletonRigOutSetNodeIds: ValidateRigOutSetNodeIdsModel = (\n        SettingsField(\n            default_factory=ValidateRigOutSetNodeIdsModel,\n            title=\"Validate Skeleton Rig Out Set Node Ids\",\n        )\n    )\n    # Rig - END\n    ValidateCameraAttributes: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Camera Attributes\"\n    )\n    ValidateAssemblyName: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Assembly Name\"\n    )\n    ValidateAssemblyNamespaces: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Assembly Namespaces\"\n    )\n    ValidateAssemblyModelTransforms: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Assembly Model Transforms\"\n    )\n    ValidateAssRelativePaths: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Ass Relative Paths\"\n    )\n    ValidateInstancerContent: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Instancer Content\"\n    )\n    ValidateInstancerFrameRanges: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Instancer Cache Frame Ranges\"\n    )\n    ValidateNoDefaultCameras: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate No Default Cameras\"\n    )\n    ValidateUnrealUpAxis: BasicValidateModel = SettingsField(\n        default_factory=BasicValidateModel,\n        title=\"Validate Unreal Up-Axis Check\"\n    )\n    ValidateCameraContents: ValidateCameraContentsModel = SettingsField(\n        default_factory=ValidateCameraContentsModel,\n        title=\"Validate Camera Content\"\n    )\n    ExtractPlayblast: ExtractPlayblastSetting = SettingsField(\n        default_factory=ExtractPlayblastSetting,\n        title=\"Extract Playblast Settings\",\n        section=\"Extractors\"\n    )\n    ExtractMayaSceneRaw: ExtractMayaSceneRawModel = SettingsField(\n        default_factory=ExtractMayaSceneRawModel,\n        title=\"Maya Scene(Raw)\"\n    )\n    ExtractCameraAlembic: ExtractCameraAlembicModel = SettingsField(\n        default_factory=ExtractCameraAlembicModel,\n        title=\"Extract Camera Alembic\"\n    )\n    ExtractGLB: ExtractGLBModel = SettingsField(\n        default_factory=ExtractGLBModel,\n        title=\"Extract GLB\"\n    )\n    ExtractLook: ExtractLookModel = SettingsField(\n        default_factory=ExtractLookModel,\n        title=\"Extract Look\"\n    )\n    ExtractGPUCache: ExtractGPUCacheModel = SettingsField(\n        default_factory=ExtractGPUCacheModel,\n        title=\"Extract GPU Cache\",\n    )\n\n\nDEFAULT_SUFFIX_NAMING = {\n    \"mesh\": [\"_GEO\", \"_GES\", \"_GEP\", \"_OSD\"],\n    \"nurbsCurve\": [\"_CRV\"],\n    \"nurbsSurface\": [\"_NRB\"],\n    \"locator\": [\"_LOC\"],\n    \"group\": [\"_GRP\"]\n}\n\nDEFAULT_PUBLISH_SETTINGS = {\n    \"CollectMayaRender\": {\n        \"sync_workfile_version\": False\n    },\n    \"CollectFbxAnimation\": {\n        \"enabled\": False\n    },\n    \"CollectFbxCamera\": {\n        \"enabled\": False\n    },\n    \"CollectGLTF\": {\n        \"enabled\": False\n    },\n    \"ValidateInstanceInContext\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateContainers\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateFrameRange\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True,\n        \"exclude_product_types\": [\n            \"model\",\n            \"rig\",\n            \"staticMesh\"\n        ]\n    },\n    \"ValidateShaderName\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True,\n        \"regex\": \"(?P<asset>.*)_(.*)_SHD\"\n    },\n    \"ValidateShadingEngine\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMayaColorSpace\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateAttributes\": {\n        \"enabled\": False,\n        \"attributes\": \"{}\"\n    },\n    \"ValidateLoadedPlugin\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"whitelist_native_plugins\": False,\n        \"authorized_plugins\": []\n    },\n    \"ValidateMayaUnits\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"validate_linear_units\": True,\n        \"linear_units\": \"cm\",\n        \"validate_angular_units\": True,\n        \"angular_units\": \"deg\",\n        \"validate_fps\": True\n    },\n    \"ValidateUnrealStaticMeshName\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"validate_mesh\": False,\n        \"validate_collision\": True\n    },\n    \"ValidateCycleError\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"families\": [\n            \"rig\"\n        ]\n    },\n    \"ValidatePluginPathAttributes\": {\n        \"enabled\": False,\n        \"optional\": False,\n        \"active\": True,\n        \"attribute\": [\n            {\"name\": \"AlembicNode\", \"value\": \"abc_File\"},\n            {\"name\": \"VRayProxy\", \"value\": \"fileName\"},\n            {\"name\": \"RenderManArchive\", \"value\": \"filename\"},\n            {\"name\": \"pgYetiMaya\", \"value\": \"cacheFileName\"},\n            {\"name\": \"aiStandIn\", \"value\": \"dso\"},\n            {\"name\": \"RedshiftSprite\", \"value\": \"tex0\"},\n            {\"name\": \"RedshiftBokeh\", \"value\": \"dofBokehImage\"},\n            {\"name\": \"RedshiftCameraMap\", \"value\": \"tex0\"},\n            {\"name\": \"RedshiftEnvironment\", \"value\": \"tex2\"},\n            {\"name\": \"RedshiftDomeLight\", \"value\": \"tex1\"},\n            {\"name\": \"RedshiftIESLight\", \"value\": \"profile\"},\n            {\"name\": \"RedshiftLightGobo\", \"value\": \"tex0\"},\n            {\"name\": \"RedshiftNormalMap\", \"value\": \"tex0\"},\n            {\"name\": \"RedshiftProxyMesh\", \"value\": \"fileName\"},\n            {\"name\": \"RedshiftVolumeShape\", \"value\": \"fileName\"},\n            {\"name\": \"VRayTexGLSL\", \"value\": \"fileName\"},\n            {\"name\": \"VRayMtlGLSL\", \"value\": \"fileName\"},\n            {\"name\": \"VRayVRmatMtl\", \"value\": \"fileName\"},\n            {\"name\": \"VRayPtex\", \"value\": \"ptexFile\"},\n            {\"name\": \"VRayLightIESShape\", \"value\": \"iesFile\"},\n            {\"name\": \"VRayMesh\", \"value\": \"materialAssignmentsFile\"},\n            {\"name\": \"VRayMtlOSL\", \"value\": \"fileName\"},\n            {\"name\": \"VRayTexOSL\", \"value\": \"fileName\"},\n            {\"name\": \"VRayTexOCIO\", \"value\": \"ocioConfigFile\"},\n            {\"name\": \"VRaySettingsNode\", \"value\": \"pmap_autoSaveFile2\"},\n            {\"name\": \"VRayScannedMtl\", \"value\": \"file\"},\n            {\"name\": \"VRayScene\", \"value\": \"parameterOverrideFilePath\"},\n            {\"name\": \"VRayMtlMDL\", \"value\": \"filename\"},\n            {\"name\": \"VRaySimbiont\", \"value\": \"file\"},\n            {\"name\": \"dlOpenVDBShape\", \"value\": \"filename\"},\n            {\"name\": \"pgYetiMayaShape\", \"value\": \"liveABCFilename\"},\n            {\"name\": \"gpuCache\", \"value\": \"cacheFileName\"},\n        ]\n    },\n    \"ValidateRenderSettings\": {\n        \"arnold_render_attributes\": [],\n        \"vray_render_attributes\": [],\n        \"redshift_render_attributes\": [],\n        \"renderman_render_attributes\": []\n    },\n    \"ValidateResolution\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateCurrentRenderLayerIsRenderable\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateGLSLMaterial\": {\n        \"enabled\": False,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateGLSLPlugin\": {\n        \"enabled\": False,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateRenderImageRule\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateRenderNoDefaultCameras\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateRenderSingleCamera\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateRenderLayerAOVs\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateStepSize\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateVRayDistributedRendering\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateVrayReferencedAOVs\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateVRayTranslatorEnabled\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateVrayProxy\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateVrayProxyMembers\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateYetiRenderScriptCallbacks\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateYetiRigCacheState\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateYetiRigInputShapesInInstance\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateYetiRigSettings\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateModelName\": {\n        \"enabled\": False,\n        \"database\": True,\n        \"material_file\": {\n            \"windows\": \"\",\n            \"darwin\": \"\",\n            \"linux\": \"\"\n        },\n        \"regex\": \"(.*)_(\\\\d)*_(?P<shader>.*)_(GEO)\",\n        \"top_level_regex\": \".*_GRP\"\n    },\n    \"ValidateModelContent\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"validate_top_group\": True\n    },\n    \"ValidateTransformNamingSuffix\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"SUFFIX_NAMING_TABLE\": json.dumps(DEFAULT_SUFFIX_NAMING, indent=4),\n        \"ALLOW_IF_NOT_IN_SUFFIX_TABLE\": True\n    },\n    \"ValidateColorSets\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshHasOverlappingUVs\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshArnoldAttributes\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshShaderConnections\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshSingleUVSet\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshHasUVs\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshLaminaFaces\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshNgons\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshNonManifold\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshNoNegativeScale\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateMeshNonZeroEdgeLength\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshNormalsUnlocked\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshUVSetMap1\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMeshVerticesHaveEdges\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateNoAnimation\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateNoNamespace\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateNoNullTransforms\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateNoUnknownNodes\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateNodeNoGhosting\": {\n        \"enabled\": False,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateShapeDefaultNames\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateShapeRenderStats\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateShapeZero\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateTransformZero\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateUniqueNames\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateNoVRayMesh\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateUnrealMeshTriangulated\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateAlembicVisibleOnly\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ExtractProxyAlembic\": {\n        \"enabled\": False,\n        \"families\": [\n            \"proxyAbc\"\n        ]\n    },\n    \"ExtractAlembic\": {\n        \"enabled\": True,\n        \"families\": [\n            \"pointcache\",\n            \"model\",\n            \"vrayproxy.alembic\"\n        ]\n    },\n    \"ExtractObj\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateRigContents\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateRigJointsHidden\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateRigControllers\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateAnimatedReferenceRig\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateAnimationContent\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateOutRelatedNodeIds\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateRigControllersArnoldAttributes\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateSkeletalMeshHierarchy\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateSkeletonRigContents\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateSkeletonRigControllers\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateSkinclusterDeformerSet\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateRigOutSetNodeIds\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"allow_history_only\": False\n    },\n    \"ValidateSkeletonRigOutSetNodeIds\": {\n        \"enabled\": False,\n        \"optional\": False,\n        \"allow_history_only\": False\n    },\n    \"ValidateSkeletonRigOutputIds\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateSkeletonTopGroupHierarchy\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateCameraAttributes\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateAssemblyName\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateAssemblyNamespaces\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateAssemblyModelTransforms\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateAssRelativePaths\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateInstancerContent\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateInstancerFrameRanges\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateNoDefaultCameras\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"active\": True\n    },\n    \"ValidateUnrealUpAxis\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateCameraContents\": {\n        \"enabled\": True,\n        \"optional\": False,\n        \"validate_shapes\": True\n    },\n    \"ExtractPlayblast\": DEFAULT_PLAYBLAST_SETTING,\n    \"ExtractMayaSceneRaw\": {\n        \"enabled\": True,\n        \"add_for_families\": [\n            \"layout\"\n        ]\n    },\n    \"ExtractCameraAlembic\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True,\n        \"bake_attributes\": \"[]\"\n    },\n    \"ExtractGLB\": {\n        \"enabled\": False,\n        \"active\": True,\n        \"ogsfx_path\": \"/maya2glTF/PBR/shaders/glTF_PBR.ogsfx\"\n    },\n    \"ExtractLook\": {\n        \"maketx_arguments\": []\n    },\n    \"ExtractGPUCache\": {\n        \"enabled\": False,\n        \"families\": [\n            \"model\",\n            \"animation\",\n            \"pointcache\"\n        ],\n        \"step\": 1.0,\n        \"stepSave\": 1,\n        \"optimize\": True,\n        \"optimizationThreshold\": 40000,\n        \"optimizeAnimationsForMotionBlur\": True,\n        \"writeMaterials\": True,\n        \"useBaseTessellation\": True\n    }\n}\n"
  },
  {
    "path": "server_addon/maya/server/settings/render_settings.py",
    "content": "\"\"\"Providing models and values for Maya Render Settings.\"\"\"\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\n\n\ndef aov_separators_enum():\n    return [\n        {\"value\": \"dash\", \"label\": \"- (dash)\"},\n        {\"value\": \"underscore\", \"label\": \"_ (underscore)\"},\n        {\"value\": \"dot\", \"label\": \". (dot)\"}\n    ]\n\n\ndef arnold_image_format_enum():\n    \"\"\"Return enumerator for Arnold output formats.\"\"\"\n    return [\n        {\"label\": \"jpeg\", \"value\": \"jpeg\"},\n        {\"label\": \"png\", \"value\": \"png\"},\n        {\"label\": \"deepexr\", \"value\": \"deep exr\"},\n        {\"label\": \"tif\", \"value\": \"tif\"},\n        {\"label\": \"exr\", \"value\": \"exr\"},\n        {\"label\": \"maya\", \"value\": \"maya\"},\n        {\"label\": \"mtoa_shaders\", \"value\": \"mtoa_shaders\"}\n    ]\n\n\ndef arnold_aov_list_enum():\n    \"\"\"Return enumerator for Arnold AOVs.\n\n    Note: Key is value, Value in this case is Label. This\n        was taken from v3 settings.\n    \"\"\"\n    return [\n        {\"value\": \"empty\", \"label\": \"< empty >\"},\n        {\"value\": \"ID\", \"label\": \"ID\"},\n        {\"value\": \"N\", \"label\": \"N\"},\n        {\"value\": \"P\", \"label\": \"P\"},\n        {\"value\": \"Pref\", \"label\": \"Pref\"},\n        {\"value\": \"RGBA\", \"label\": \"RGBA\"},\n        {\"value\": \"Z\", \"label\": \"Z\"},\n        {\"value\": \"albedo\", \"label\": \"albedo\"},\n        {\"value\": \"background\", \"label\": \"background\"},\n        {\"value\": \"coat\", \"label\": \"coat\"},\n        {\"value\": \"coat_albedo\", \"label\": \"coat_albedo\"},\n        {\"value\": \"coat_direct\", \"label\": \"coat_direct\"},\n        {\"value\": \"coat_indirect\", \"label\": \"coat_indirect\"},\n        {\"value\": \"cputime\", \"label\": \"cputime\"},\n        {\"value\": \"crypto_asset\", \"label\": \"crypto_asset\"},\n        {\"value\": \"crypto_material\", \"label\": \"cypto_material\"},\n        {\"value\": \"crypto_object\", \"label\": \"crypto_object\"},\n        {\"value\": \"diffuse\", \"label\": \"diffuse\"},\n        {\"value\": \"diffuse_albedo\", \"label\": \"diffuse_albedo\"},\n        {\"value\": \"diffuse_direct\", \"label\": \"diffuse_direct\"},\n        {\"value\": \"diffuse_indirect\", \"label\": \"diffuse_indirect\"},\n        {\"value\": \"direct\", \"label\": \"direct\"},\n        {\"value\": \"emission\", \"label\": \"emission\"},\n        {\"value\": \"highlight\", \"label\": \"highlight\"},\n        {\"value\": \"indirect\", \"label\": \"indirect\"},\n        {\"value\": \"motionvector\", \"label\": \"motionvector\"},\n        {\"value\": \"opacity\", \"label\": \"opacity\"},\n        {\"value\": \"raycount\", \"label\": \"raycount\"},\n        {\"value\": \"rim_light\", \"label\": \"rim_light\"},\n        {\"value\": \"shadow\", \"label\": \"shadow\"},\n        {\"value\": \"shadow_diff\", \"label\": \"shadow_diff\"},\n        {\"value\": \"shadow_mask\", \"label\": \"shadow_mask\"},\n        {\"value\": \"shadow_matte\", \"label\": \"shadow_matte\"},\n        {\"value\": \"sheen\", \"label\": \"sheen\"},\n        {\"value\": \"sheen_albedo\", \"label\": \"sheen_albedo\"},\n        {\"value\": \"sheen_direct\", \"label\": \"sheen_direct\"},\n        {\"value\": \"sheen_indirect\", \"label\": \"sheen_indirect\"},\n        {\"value\": \"specular\", \"label\": \"specular\"},\n        {\"value\": \"specular_albedo\", \"label\": \"specular_albedo\"},\n        {\"value\": \"specular_direct\", \"label\": \"specular_direct\"},\n        {\"value\": \"specular_indirect\", \"label\": \"specular_indirect\"},\n        {\"value\": \"sss\", \"label\": \"sss\"},\n        {\"value\": \"sss_albedo\", \"label\": \"sss_albedo\"},\n        {\"value\": \"sss_direct\", \"label\": \"sss_direct\"},\n        {\"value\": \"sss_indirect\", \"label\": \"sss_indirect\"},\n        {\"value\": \"transmission\", \"label\": \"transmission\"},\n        {\"value\": \"transmission_albedo\", \"label\": \"transmission_albedo\"},\n        {\"value\": \"transmission_direct\", \"label\": \"transmission_direct\"},\n        {\"value\": \"transmission_indirect\", \"label\": \"transmission_indirect\"},\n        {\"value\": \"volume\", \"label\": \"volume\"},\n        {\"value\": \"volume_Z\", \"label\": \"volume_Z\"},\n        {\"value\": \"volume_albedo\", \"label\": \"volume_albedo\"},\n        {\"value\": \"volume_direct\", \"label\": \"volume_direct\"},\n        {\"value\": \"volume_indirect\", \"label\": \"volume_indirect\"},\n        {\"value\": \"volume_opacity\", \"label\": \"volume_opacity\"},\n    ]\n\n\ndef vray_image_output_enum():\n    \"\"\"Return output format for Vray enumerator.\"\"\"\n    return [\n        {\"label\": \"png\", \"value\": \"png\"},\n        {\"label\": \"jpg\", \"value\": \"jpg\"},\n        {\"label\": \"vrimg\", \"value\": \"vrimg\"},\n        {\"label\": \"hdr\", \"value\": \"hdr\"},\n        {\"label\": \"exr\", \"value\": \"exr\"},\n        {\"label\": \"exr (multichannel)\", \"value\": \"exr (multichannel)\"},\n        {\"label\": \"exr (deep)\", \"value\": \"exr (deep)\"},\n        {\"label\": \"tga\", \"value\": \"tga\"},\n        {\"label\": \"bmp\", \"value\": \"bmp\"},\n        {\"label\": \"sgi\", \"value\": \"sgi\"}\n    ]\n\n\ndef vray_aov_list_enum():\n    \"\"\"Return enumerator for Vray AOVs.\n\n    Note: Key is value, Value in this case is Label. This\n        was taken from v3 settings.\n    \"\"\"\n\n    return [\n        {\"value\": \"empty\", \"label\": \"< empty >\"},\n        {\"value\": \"atmosphereChannel\", \"label\": \"atmosphere\"},\n        {\"value\": \"backgroundChannel\", \"label\": \"background\"},\n        {\"value\": \"bumpNormalsChannel\", \"label\": \"bumpnormals\"},\n        {\"value\": \"causticsChannel\", \"label\": \"caustics\"},\n        {\"value\": \"coatFilterChannel\", \"label\": \"coat_filter\"},\n        {\"value\": \"coatGlossinessChannel\", \"label\": \"coatGloss\"},\n        {\"value\": \"coatReflectionChannel\", \"label\": \"coat_reflection\"},\n        {\"value\": \"vrayCoatChannel\", \"label\": \"coat_specular\"},\n        {\"value\": \"CoverageChannel\", \"label\": \"coverage\"},\n        {\"value\": \"cryptomatteChannel\", \"label\": \"cryptomatte\"},\n        {\"value\": \"customColor\", \"label\": \"custom_color\"},\n        {\"value\": \"drBucketChannel\", \"label\": \"DR\"},\n        {\"value\": \"denoiserChannel\", \"label\": \"denoiser\"},\n        {\"value\": \"diffuseChannel\", \"label\": \"diffuse\"},\n        {\"value\": \"ExtraTexElement\", \"label\": \"extraTex\"},\n        {\"value\": \"giChannel\", \"label\": \"GI\"},\n        {\"value\": \"LightMixElement\", \"label\": \"None\"},\n        {\"value\": \"lightingChannel\", \"label\": \"lighting\"},\n        {\"value\": \"LightingAnalysisChannel\", \"label\": \"LightingAnalysis\"},\n        {\"value\": \"materialIDChannel\", \"label\": \"materialID\"},\n        {\"value\": \"MaterialSelectElement\", \"label\": \"materialSelect\"},\n        {\"value\": \"matteShadowChannel\", \"label\": \"matteShadow\"},\n        {\"value\": \"MultiMatteElement\", \"label\": \"multimatte\"},\n        {\"value\": \"multimatteIDChannel\", \"label\": \"multimatteID\"},\n        {\"value\": \"normalsChannel\", \"label\": \"normals\"},\n        {\"value\": \"nodeIDChannel\", \"label\": \"objectId\"},\n        {\"value\": \"objectSelectChannel\", \"label\": \"objectSelect\"},\n        {\"value\": \"rawCoatFilterChannel\", \"label\": \"raw_coat_filter\"},\n        {\"value\": \"rawCoatReflectionChannel\", \"label\": \"raw_coat_reflection\"},\n        {\"value\": \"rawDiffuseFilterChannel\", \"label\": \"rawDiffuseFilter\"},\n        {\"value\": \"rawGiChannel\", \"label\": \"rawGI\"},\n        {\"value\": \"rawLightChannel\", \"label\": \"rawLight\"},\n        {\"value\": \"rawReflectionChannel\", \"label\": \"rawReflection\"},\n        {\n            \"value\": \"rawReflectionFilterChannel\",\n            \"label\": \"rawReflectionFilter\"\n        },\n        {\"value\": \"rawRefractionChannel\", \"label\": \"rawRefraction\"},\n        {\n            \"value\": \"rawRefractionFilterChannel\",\n            \"label\": \"rawRefractionFilter\"\n        },\n        {\"value\": \"rawShadowChannel\", \"label\": \"rawShadow\"},\n        {\"value\": \"rawSheenFilterChannel\", \"label\": \"raw_sheen_filter\"},\n        {\n            \"value\": \"rawSheenReflectionChannel\",\n            \"label\": \"raw_sheen_reflection\"\n        },\n        {\"value\": \"rawTotalLightChannel\", \"label\": \"rawTotalLight\"},\n        {\"value\": \"reflectIORChannel\", \"label\": \"reflIOR\"},\n        {\"value\": \"reflectChannel\", \"label\": \"reflect\"},\n        {\"value\": \"reflectionFilterChannel\", \"label\": \"reflectionFilter\"},\n        {\"value\": \"reflectGlossinessChannel\", \"label\": \"reflGloss\"},\n        {\"value\": \"refractChannel\", \"label\": \"refract\"},\n        {\"value\": \"refractionFilterChannel\", \"label\": \"refractionFilter\"},\n        {\"value\": \"refractGlossinessChannel\", \"label\": \"refrGloss\"},\n        {\"value\": \"renderIDChannel\", \"label\": \"renderId\"},\n        {\"value\": \"FastSSS2Channel\", \"label\": \"SSS\"},\n        {\"value\": \"sampleRateChannel\", \"label\": \"sampleRate\"},\n        {\"value\": \"samplerInfo\", \"label\": \"samplerInfo\"},\n        {\"value\": \"selfIllumChannel\", \"label\": \"selfIllum\"},\n        {\"value\": \"shadowChannel\", \"label\": \"shadow\"},\n        {\"value\": \"sheenFilterChannel\", \"label\": \"sheen_filter\"},\n        {\"value\": \"sheenGlossinessChannel\", \"label\": \"sheenGloss\"},\n        {\"value\": \"sheenReflectionChannel\", \"label\": \"sheen_reflection\"},\n        {\"value\": \"vraySheenChannel\", \"label\": \"sheen_specular\"},\n        {\"value\": \"specularChannel\", \"label\": \"specular\"},\n        {\"value\": \"Toon\", \"label\": \"Toon\"},\n        {\"value\": \"toonLightingChannel\", \"label\": \"toonLighting\"},\n        {\"value\": \"toonSpecularChannel\", \"label\": \"toonSpecular\"},\n        {\"value\": \"totalLightChannel\", \"label\": \"totalLight\"},\n        {\"value\": \"unclampedColorChannel\", \"label\": \"unclampedColor\"},\n        {\"value\": \"VRScansPaintMaskChannel\", \"label\": \"VRScansPaintMask\"},\n        {\"value\": \"VRScansZoneMaskChannel\", \"label\": \"VRScansZoneMask\"},\n        {\"value\": \"velocityChannel\", \"label\": \"velocity\"},\n        {\"value\": \"zdepthChannel\", \"label\": \"zDepth\"},\n        {\"value\": \"LightSelectElement\", \"label\": \"lightselect\"},\n    ]\n\n\ndef redshift_engine_enum():\n    \"\"\"Get Redshift engine type enumerator.\"\"\"\n    return [\n        {\"value\": \"0\", \"label\": \"None\"},\n        {\"value\": \"1\", \"label\": \"Photon Map\"},\n        {\"value\": \"2\", \"label\": \"Irradiance Cache\"},\n        {\"value\": \"3\", \"label\": \"Brute Force\"}\n    ]\n\n\ndef redshift_image_output_enum():\n    \"\"\"Return output format for Redshift enumerator.\"\"\"\n    return [\n        {\"value\": \"iff\", \"label\": \"Maya IFF\"},\n        {\"value\": \"exr\", \"label\": \"OpenEXR\"},\n        {\"value\": \"tif\", \"label\": \"TIFF\"},\n        {\"value\": \"png\", \"label\": \"PNG\"},\n        {\"value\": \"tga\", \"label\": \"Targa\"},\n        {\"value\": \"jpg\", \"label\": \"JPEG\"}\n    ]\n\n\ndef redshift_aov_list_enum():\n    \"\"\"Return enumerator for Vray AOVs.\n\n        Note: Key is value, Value in this case is Label. This\n            was taken from v3 settings.\n        \"\"\"\n    return [\n        {\"value\": \"empty\", \"label\": \"< none >\"},\n        {\"value\": \"AO\", \"label\": \"Ambient Occlusion\"},\n        {\"value\": \"Background\", \"label\": \"Background\"},\n        {\"value\": \"Beauty\", \"label\": \"Beauty\"},\n        {\"value\": \"BumpNormals\", \"label\": \"Bump Normals\"},\n        {\"value\": \"Caustics\", \"label\": \"Caustics\"},\n        {\"value\": \"CausticsRaw\", \"label\": \"Caustics Raw\"},\n        {\"value\": \"Cryptomatte\", \"label\": \"Cryptomatte\"},\n        {\"value\": \"Custom\", \"label\": \"Custom\"},\n        {\"value\": \"Z\", \"label\": \"Depth\"},\n        {\"value\": \"DiffuseFilter\", \"label\": \"Diffuse Filter\"},\n        {\"value\": \"DiffuseLighting\", \"label\": \"Diffuse Lighting\"},\n        {\"value\": \"DiffuseLightingRaw\", \"label\": \"Diffuse Lighting Raw\"},\n        {\"value\": \"Emission\", \"label\": \"Emission\"},\n        {\"value\": \"GI\", \"label\": \"Global Illumination\"},\n        {\"value\": \"GIRaw\", \"label\": \"Global Illumination Raw\"},\n        {\"value\": \"Matte\", \"label\": \"Matte\"},\n        {\"value\": \"MotionVectors\", \"label\": \"Ambient Occlusion\"},\n        {\"value\": \"N\", \"label\": \"Normals\"},\n        {\"value\": \"ID\", \"label\": \"ObjectID\"},\n        {\"value\": \"ObjectBumpNormal\", \"label\": \"Object-Space Bump Normals\"},\n        {\"value\": \"ObjectPosition\", \"label\": \"Object-Space Positions\"},\n        {\"value\": \"PuzzleMatte\", \"label\": \"Puzzle Matte\"},\n        {\"value\": \"Reflections\", \"label\": \"Reflections\"},\n        {\"value\": \"ReflectionsFilter\", \"label\": \"Reflections Filter\"},\n        {\"value\": \"ReflectionsRaw\", \"label\": \"Reflections Raw\"},\n        {\"value\": \"Refractions\", \"label\": \"Refractions\"},\n        {\"value\": \"RefractionsFilter\", \"label\": \"Refractions Filter\"},\n        {\"value\": \"RefractionsRaw\", \"label\": \"Refractions Filter\"},\n        {\"value\": \"Shadows\", \"label\": \"Shadows\"},\n        {\"value\": \"SpecularLighting\", \"label\": \"Specular Lighting\"},\n        {\"value\": \"SSS\", \"label\": \"Sub Surface Scatter\"},\n        {\"value\": \"SSSRaw\", \"label\": \"Sub Surface Scatter Raw\"},\n        {\n            \"value\": \"TotalDiffuseLightingRaw\",\n            \"label\": \"Total Diffuse Lighting Raw\"\n        },\n        {\n            \"value\": \"TotalTransLightingRaw\",\n            \"label\": \"Total Translucency Filter\"\n        },\n        {\"value\": \"TransTint\", \"label\": \"Translucency Filter\"},\n        {\"value\": \"TransGIRaw\", \"label\": \"Translucency Lighting Raw\"},\n        {\"value\": \"VolumeFogEmission\", \"label\": \"Volume Fog Emission\"},\n        {\"value\": \"VolumeFogTint\", \"label\": \"Volume Fog Tint\"},\n        {\"value\": \"VolumeLighting\", \"label\": \"Volume Lighting\"},\n        {\"value\": \"P\", \"label\": \"World Position\"},\n    ]\n\n\nclass AdditionalOptionsModel(BaseSettingsModel):\n    \"\"\"Additional Option\"\"\"\n    _layout = \"compact\"\n\n    attribute: str = SettingsField(\"\", title=\"Attribute name\")\n    value: str = SettingsField(\"\", title=\"Value\")\n\n\nclass ArnoldSettingsModel(BaseSettingsModel):\n    image_prefix: str = SettingsField(title=\"Image prefix template\")\n    image_format: str = SettingsField(\n        enum_resolver=arnold_image_format_enum, title=\"Output Image Format\")\n    multilayer_exr: bool = SettingsField(title=\"Multilayer (exr)\")\n    tiled: bool = SettingsField(title=\"Tiled (tif, exr)\")\n    aov_list: list[str] = SettingsField(\n        default_factory=list,\n        enum_resolver=arnold_aov_list_enum,\n        title=\"AOVs to create\"\n    )\n    additional_options: list[AdditionalOptionsModel] = SettingsField(\n        default_factory=list,\n        title=\"Additional Arnold Options\",\n        description=(\n            \"Add additional options - put attribute and value, like AASamples\"\n            \" and 4\"\n        )\n    )\n\n\nclass VraySettingsModel(BaseSettingsModel):\n    image_prefix: str = SettingsField(title=\"Image prefix template\")\n    # engine was str because of JSON limitation (key must be string)\n    engine: str = SettingsField(\n        enum_resolver=lambda: [\n            {\"label\": \"V-Ray\", \"value\": \"1\"},\n            {\"label\": \"V-Ray GPU\", \"value\": \"2\"}\n        ],\n        title=\"Production Engine\"\n    )\n    image_format: str = SettingsField(\n        enum_resolver=vray_image_output_enum,\n        title=\"Output Image Format\"\n    )\n    aov_list: list[str] = SettingsField(\n        default_factory=list,\n        enum_resolver=vray_aov_list_enum,\n        title=\"AOVs to create\"\n    )\n    additional_options: list[AdditionalOptionsModel] = SettingsField(\n        default_factory=list,\n        title=\"Additional Vray Options\",\n        description=(\n            \"Add additional options - put attribute and value, like \"\n            \"aaFilterSize and 1.5\"\n        )\n    )\n\n\nclass RedshiftSettingsModel(BaseSettingsModel):\n    image_prefix: str = SettingsField(title=\"Image prefix template\")\n    # both engines are using the same enumerator,\n    #   both were originally str because of JSON limitation.\n    primary_gi_engine: str = SettingsField(\n        enum_resolver=redshift_engine_enum,\n        title=\"Primary GI Engine\"\n    )\n    secondary_gi_engine: str = SettingsField(\n        enum_resolver=redshift_engine_enum,\n        title=\"Secondary GI Engine\"\n    )\n    image_format: str = SettingsField(\n        enum_resolver=redshift_image_output_enum,\n        title=\"Output Image Format\"\n    )\n    multilayer_exr: bool = SettingsField(title=\"Multilayer (exr)\")\n    force_combine: bool = SettingsField(title=\"Force combine beauty and AOVs\")\n    aov_list: list[str] = SettingsField(\n        default_factory=list,\n        enum_resolver=redshift_aov_list_enum,\n        title=\"AOVs to create\"\n    )\n    additional_options: list[AdditionalOptionsModel] = SettingsField(\n        default_factory=list,\n        title=\"Additional Vray Options\",\n        description=(\n            \"Add additional options - put attribute and value, like \"\n            \"reflectionMaxTraceDepth and 3\"\n        )\n    )\n\n\ndef renderman_display_filters():\n    return [\n        \"PxrBackgroundDisplayFilter\",\n        \"PxrCopyAOVDisplayFilter\",\n        \"PxrEdgeDetect\",\n        \"PxrFilmicTonemapperDisplayFilter\",\n        \"PxrGradeDisplayFilter\",\n        \"PxrHalfBufferErrorFilter\",\n        \"PxrImageDisplayFilter\",\n        \"PxrLightSaturation\",\n        \"PxrShadowDisplayFilter\",\n        \"PxrStylizedHatching\",\n        \"PxrStylizedLines\",\n        \"PxrStylizedToon\",\n        \"PxrWhitePointDisplayFilter\"\n    ]\n\n\ndef renderman_sample_filters_enum():\n    return [\n        \"PxrBackgroundSampleFilter\",\n        \"PxrCopyAOVSampleFilter\",\n        \"PxrCryptomatte\",\n        \"PxrFilmicTonemapperSampleFilter\",\n        \"PxrGradeSampleFilter\",\n        \"PxrShadowFilter\",\n        \"PxrWatermarkFilter\",\n        \"PxrWhitePointSampleFilter\"\n    ]\n\n\nclass RendermanSettingsModel(BaseSettingsModel):\n    image_prefix: str = SettingsField(\n        \"\", title=\"Image prefix template\")\n    image_dir: str = SettingsField(\n        \"\", title=\"Image Output Directory\")\n    display_filters: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Display Filters\",\n        enum_resolver=renderman_display_filters\n    )\n    imageDisplay_dir: str = SettingsField(\n        \"\", title=\"Image Display Filter Directory\")\n    sample_filters: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Sample Filters\",\n        enum_resolver=renderman_sample_filters_enum\n    )\n    cryptomatte_dir: str = SettingsField(\n        \"\", title=\"Cryptomatte Output Directory\")\n    watermark_dir: str = SettingsField(\n        \"\", title=\"Watermark Filter Directory\")\n    additional_options: list[AdditionalOptionsModel] = SettingsField(\n        default_factory=list,\n        title=\"Additional Renderer Options\"\n    )\n\n\nclass RenderSettingsModel(BaseSettingsModel):\n    apply_render_settings: bool = SettingsField(\n        title=\"Apply Render Settings on creation\"\n    )\n    default_render_image_folder: str = SettingsField(\n        title=\"Default render image folder\"\n    )\n    enable_all_lights: bool = SettingsField(\n        title=\"Include all lights in Render Setup Layers by default\"\n    )\n    aov_separator: str = SettingsField(\n        \"underscore\",\n        title=\"AOV Separator character\",\n        enum_resolver=aov_separators_enum\n    )\n    reset_current_frame: bool = SettingsField(\n        title=\"Reset Current Frame\")\n    remove_aovs: bool = SettingsField(\n        title=\"Remove existing AOVs\")\n    arnold_renderer: ArnoldSettingsModel = SettingsField(\n        default_factory=ArnoldSettingsModel,\n        title=\"Arnold Renderer\")\n    vray_renderer: VraySettingsModel = SettingsField(\n        default_factory=VraySettingsModel,\n        title=\"Vray Renderer\")\n    redshift_renderer: RedshiftSettingsModel = SettingsField(\n        default_factory=RedshiftSettingsModel,\n        title=\"Redshift Renderer\")\n    renderman_renderer: RendermanSettingsModel = SettingsField(\n        default_factory=RendermanSettingsModel,\n        title=\"Renderman Renderer\")\n\n\nDEFAULT_RENDER_SETTINGS = {\n    \"apply_render_settings\": True,\n    \"default_render_image_folder\": \"renders/maya\",\n    \"enable_all_lights\": True,\n    \"aov_separator\": \"underscore\",\n    \"reset_current_frame\": False,\n    \"remove_aovs\": False,\n    \"arnold_renderer\": {\n        \"image_prefix\": \"<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>\",\n        \"image_format\": \"exr\",\n        \"multilayer_exr\": True,\n        \"tiled\": True,\n        \"aov_list\": [],\n        \"additional_options\": []\n    },\n    \"vray_renderer\": {\n        \"image_prefix\": \"<scene>/<Layer>/<Layer>\",\n        \"engine\": \"1\",\n        \"image_format\": \"exr\",\n        \"aov_list\": [],\n        \"additional_options\": []\n    },\n    \"redshift_renderer\": {\n        \"image_prefix\": \"<Scene>/<RenderLayer>/<RenderLayer>\",\n        \"primary_gi_engine\": \"0\",\n        \"secondary_gi_engine\": \"0\",\n        \"image_format\": \"exr\",\n        \"multilayer_exr\": True,\n        \"force_combine\": True,\n        \"aov_list\": [],\n        \"additional_options\": []\n    },\n    \"renderman_renderer\": {\n        \"image_prefix\": \"<layer>{aov_separator}<aov>.<f4>.<ext>\",\n        \"image_dir\": \"<scene>/<layer>\",\n        \"display_filters\": [],\n        \"imageDisplay_dir\": \"<imagedir>/<layer>{aov_separator}imageDisplayFilter.<f4>.<ext>\",\n        \"sample_filters\": [],\n        \"cryptomatte_dir\": \"<imagedir>/<layer>{aov_separator}cryptomatte.<f4>.<ext>\",\n        \"watermark_dir\": \"<imagedir>/<layer>{aov_separator}watermarkFilter.<f4>.<ext>\",\n        \"additional_options\": []\n    }\n}\n"
  },
  {
    "path": "server_addon/maya/server/settings/scriptsmenu.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass ScriptsmenuSubmodel(BaseSettingsModel):\n    \"\"\"Item Definition\"\"\"\n    _isGroup = True\n    type: str = SettingsField(title=\"Type\")\n    command: str = SettingsField(title=\"Command\")\n    sourcetype: str = SettingsField(title=\"Source Type\")\n    title: str = SettingsField(title=\"Title\")\n    tooltip: str = SettingsField(title=\"Tooltip\")\n    tags: list[str] = SettingsField(\n        default_factory=list, title=\"A list of tags\"\n    )\n\n\nclass ScriptsmenuModel(BaseSettingsModel):\n    _isGroup = True\n\n    name: str = SettingsField(title=\"Menu Name\")\n    definition: list[ScriptsmenuSubmodel] = SettingsField(\n        default_factory=list,\n        title=\"Menu Definition\",\n        description=\"Scriptmenu Items Definition\"\n    )\n\n\nDEFAULT_SCRIPTSMENU_SETTINGS = {\n    \"name\": \"Custom Tools\",\n    \"definition\": [\n        {\n            \"type\": \"action\",\n            \"command\": \"import openpype.hosts.maya.api.commands as op_cmds; op_cmds.edit_shader_definitions()\",\n            \"sourcetype\": \"python\",\n            \"title\": \"Edit shader name definitions\",\n            \"tooltip\": \"Edit shader name definitions used in validation and renaming.\",\n            \"tags\": [\n                \"pipeline\",\n                \"shader\"\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "server_addon/maya/server/settings/templated_workfile_settings.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    task_types_enum,\n)\n\n\nclass WorkfileBuildProfilesModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    task_names: list[str] = SettingsField(\n        default_factory=list, title=\"Task names\"\n    )\n    path: str = SettingsField(\"\", title=\"Path to template\")\n\n\nclass TemplatedProfilesModel(BaseSettingsModel):\n    profiles: list[WorkfileBuildProfilesModel] = SettingsField(\n        default_factory=list,\n        title=\"Profiles\"\n    )\n\n\nDEFAULT_TEMPLATED_WORKFILE_SETTINGS = {\n    \"profiles\": []\n}\n"
  },
  {
    "path": "server_addon/maya/server/settings/workfile_build_settings.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    task_types_enum,\n)\n\n\nclass ContextItemModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    product_name_filters: list[str] = SettingsField(\n        default_factory=list, title=\"Product name Filters\")\n    product_types: list[str] = SettingsField(\n        default_factory=list, title=\"Product types\")\n    repre_names: list[str] = SettingsField(\n        default_factory=list, title=\"Repre Names\")\n    loaders: list[str] = SettingsField(\n        default_factory=list, title=\"Loaders\")\n\n\nclass WorkfileSettingModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        enum_resolver=task_types_enum,\n        title=\"Task types\")\n    tasks: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task names\")\n    current_context: list[ContextItemModel] = SettingsField(\n        default_factory=list,\n        title=\"Current Context\")\n    linked_assets: list[ContextItemModel] = SettingsField(\n        default_factory=list,\n        title=\"Linked Assets\")\n\n\nclass ProfilesModel(BaseSettingsModel):\n    profiles: list[WorkfileSettingModel] = SettingsField(\n        default_factory=list,\n        title=\"Profiles\"\n    )\n\n\nDEFAULT_WORKFILE_SETTING = {\n    \"profiles\": [\n        {\n            \"task_types\": [],\n            \"tasks\": [\n                \"Lighting\"\n            ],\n            \"current_context\": [\n                {\n                    \"product_name_filters\": [\n                        \".+[Mm]ain\"\n                    ],\n                    \"product_types\": [\n                        \"model\"\n                    ],\n                    \"repre_names\": [\n                        \"abc\",\n                        \"ma\"\n                    ],\n                    \"loaders\": [\n                        \"ReferenceLoader\"\n                    ]\n                },\n                {\n                    \"product_name_filters\": [],\n                    \"product_types\": [\n                        \"animation\",\n                        \"pointcache\",\n                        \"proxyAbc\"\n                    ],\n                    \"repre_names\": [\n                        \"abc\"\n                    ],\n                    \"loaders\": [\n                        \"ReferenceLoader\"\n                    ]\n                },\n                {\n                    \"product_name_filters\": [],\n                    \"product_types\": [\n                        \"rendersetup\"\n                    ],\n                    \"repre_names\": [\n                        \"json\"\n                    ],\n                    \"loaders\": [\n                        \"RenderSetupLoader\"\n                    ]\n                },\n                {\n                    \"product_name_filters\": [],\n                    \"product_types\": [\n                        \"camera\"\n                    ],\n                    \"repre_names\": [\n                        \"abc\"\n                    ],\n                    \"loaders\": [\n                        \"ReferenceLoader\"\n                    ]\n                }\n            ],\n            \"linked_assets\": [\n                {\n                    \"product_name_filters\": [],\n                    \"product_types\": [\n                        \"setdress\"\n                    ],\n                    \"repre_names\": [\n                        \"ma\"\n                    ],\n                    \"loaders\": [\n                        \"ReferenceLoader\"\n                    ]\n                },\n                {\n                    \"product_name_filters\": [],\n                    \"product_types\": [\n                        \"ArnoldStandin\"\n                    ],\n                    \"repre_names\": [\n                        \"ass\"\n                    ],\n                    \"loaders\": [\n                        \"assLoader\"\n                    ]\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "server_addon/maya/server/version.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Package declaring addon version.\"\"\"\n__version__ = \"0.1.8\"\n"
  },
  {
    "path": "server_addon/nuke/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import NukeSettings, DEFAULT_VALUES\n\n\nclass NukeAddon(BaseServerAddon):\n    name = \"nuke\"\n    title = \"Nuke\"\n    version = __version__\n    settings_model: Type[NukeSettings] = NukeSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/nuke/server/settings/__init__.py",
    "content": "from .main import (\n    NukeSettings,\n    DEFAULT_VALUES,\n)\n\n\n__all__ = (\n    \"NukeSettings\",\n    \"DEFAULT_VALUES\",\n)\n"
  },
  {
    "path": "server_addon/nuke/server/settings/common.py",
    "content": "import json\nfrom ayon_server.exceptions import BadRequestException\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.types import (\n    ColorRGBA_float,\n    ColorRGB_uint8\n)\n\n\ndef validate_json_dict(value):\n    if not value.strip():\n        return \"{}\"\n    try:\n        converted_value = json.loads(value)\n        success = isinstance(converted_value, dict)\n    except json.JSONDecodeError:\n        success = False\n\n    if not success:\n        raise BadRequestException(\n            \"Environment's can't be parsed as json object\"\n        )\n    return value\n\n\nclass Vector2d(BaseSettingsModel):\n    _layout = \"compact\"\n\n    x: float = SettingsField(1.0, title=\"X\")\n    y: float = SettingsField(1.0, title=\"Y\")\n\n\nclass Vector3d(BaseSettingsModel):\n    _layout = \"compact\"\n\n    x: float = SettingsField(1.0, title=\"X\")\n    y: float = SettingsField(1.0, title=\"Y\")\n    z: float = SettingsField(1.0, title=\"Z\")\n\n\nclass Box(BaseSettingsModel):\n    _layout = \"compact\"\n\n    x: float = SettingsField(1.0, title=\"X\")\n    y: float = SettingsField(1.0, title=\"Y\")\n    r: float = SettingsField(1.0, title=\"R\")\n    t: float = SettingsField(1.0, title=\"T\")\n\n\ndef formatable_knob_type_enum():\n    return [\n        {\"value\": \"text\", \"label\": \"Text\"},\n        {\"value\": \"number\", \"label\": \"Number\"},\n        {\"value\": \"decimal_number\", \"label\": \"Decimal number\"},\n        {\"value\": \"2d_vector\", \"label\": \"2D vector\"},\n        # \"3D vector\"\n    ]\n\n\nclass Formatable(BaseSettingsModel):\n    _layout = \"compact\"\n\n    template: str = SettingsField(\n        \"\",\n        placeholder=\"\"\"{{key}} or {{key}};{{key}}\"\"\",\n        title=\"Template\"\n    )\n    to_type: str = SettingsField(\n        \"Text\",\n        title=\"To Knob type\",\n        enum_resolver=formatable_knob_type_enum,\n    )\n\n\nknob_types_enum = [\n    {\"value\": \"text\", \"label\": \"Text\"},\n    {\"value\": \"formatable\", \"label\": \"Formate from template\"},\n    {\"value\": \"color_gui\", \"label\": \"Color GUI\"},\n    {\"value\": \"boolean\", \"label\": \"Boolean\"},\n    {\"value\": \"number\", \"label\": \"Number\"},\n    {\"value\": \"decimal_number\", \"label\": \"Decimal number\"},\n    {\"value\": \"vector_2d\", \"label\": \"2D vector\"},\n    {\"value\": \"vector_3d\", \"label\": \"3D vector\"},\n    {\"value\": \"color\", \"label\": \"Color\"},\n    {\"value\": \"box\", \"label\": \"Box\"},\n    {\"value\": \"expression\", \"label\": \"Expression\"}\n]\n\n\nclass KnobModel(BaseSettingsModel):\n    _layout = \"expanded\"\n\n    type: str = SettingsField(\n        title=\"Type\",\n        description=\"Switch between different knob types\",\n        enum_resolver=lambda: knob_types_enum,\n        conditionalEnum=True\n    )\n\n    name: str = SettingsField(\n        title=\"Name\",\n        placeholder=\"Name\"\n    )\n    text: str = SettingsField(\"\", title=\"Value\")\n    color_gui: ColorRGB_uint8 = SettingsField(\n        (0, 0, 255),\n        title=\"RGB Uint8\",\n    )\n    boolean: bool = SettingsField(False, title=\"Value\")\n    number: int = SettingsField(0, title=\"Value\")\n    decimal_number: float = SettingsField(0.0, title=\"Value\")\n    vector_2d: Vector2d = SettingsField(\n        default_factory=Vector2d,\n        title=\"Value\"\n    )\n    vector_3d: Vector3d = SettingsField(\n        default_factory=Vector3d,\n        title=\"Value\"\n    )\n    color: ColorRGBA_float = SettingsField(\n        (0.0, 0.0, 1.0, 1.0),\n        title=\"RGBA Float\"\n    )\n    box: Box = SettingsField(\n        default_factory=Box,\n        title=\"Value\"\n    )\n    formatable: Formatable = SettingsField(\n        default_factory=Formatable,\n        title=\"Formatable\"\n    )\n    expression: str = SettingsField(\n        \"\",\n        title=\"Expression\"\n    )\n"
  },
  {
    "path": "server_addon/nuke/server/settings/create_plugins.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names\n)\nfrom .common import KnobModel\n\n\ndef instance_attributes_enum():\n    \"\"\"Return create write instance attributes.\"\"\"\n    return [\n        {\"value\": \"reviewable\", \"label\": \"Reviewable\"},\n        {\"value\": \"farm_rendering\", \"label\": \"Farm rendering\"},\n        {\"value\": \"use_range_limit\", \"label\": \"Use range limit\"}\n    ]\n\n\nclass PrenodeModel(BaseSettingsModel):\n    name: str = SettingsField(\n        title=\"Node name\"\n    )\n\n    nodeclass: str = SettingsField(\n        \"\",\n        title=\"Node class\"\n    )\n    dependent: str = SettingsField(\n        \"\",\n        title=\"Incoming dependency\"\n    )\n\n    knobs: list[KnobModel] = SettingsField(\n        default_factory=list,\n        title=\"Knobs\",\n    )\n\n    @validator(\"knobs\")\n    def ensure_unique_names(cls, value):\n        \"\"\"Ensure name fields within the lists have unique names.\"\"\"\n        ensure_unique_names(value)\n        return value\n\n\nclass CreateWriteRenderModel(BaseSettingsModel):\n    temp_rendering_path_template: str = SettingsField(\n        title=\"Temporary rendering path template\"\n    )\n    default_variants: list[str] = SettingsField(\n        title=\"Default variants\",\n        default_factory=list\n    )\n    instance_attributes: list[str] = SettingsField(\n        default_factory=list,\n        enum_resolver=instance_attributes_enum,\n        title=\"Instance attributes\"\n    )\n    exposed_knobs: list[str] = SettingsField(\n        title=\"Write Node Exposed Knobs\",\n        default_factory=list\n    )\n    prenodes: list[PrenodeModel] = SettingsField(\n        default_factory=list,\n        title=\"Preceding nodes\",\n    )\n\n    @validator(\"prenodes\")\n    def ensure_unique_names(cls, value):\n        \"\"\"Ensure name fields within the lists have unique names.\"\"\"\n        ensure_unique_names(value)\n        return value\n\n\nclass CreateWritePrerenderModel(BaseSettingsModel):\n    temp_rendering_path_template: str = SettingsField(\n        title=\"Temporary rendering path template\"\n    )\n    default_variants: list[str] = SettingsField(\n        title=\"Default variants\",\n        default_factory=list\n    )\n    instance_attributes: list[str] = SettingsField(\n        default_factory=list,\n        enum_resolver=instance_attributes_enum,\n        title=\"Instance attributes\"\n    )\n    exposed_knobs: list[str] = SettingsField(\n        title=\"Write Node Exposed Knobs\",\n        default_factory=list\n    )\n    prenodes: list[PrenodeModel] = SettingsField(\n        default_factory=list,\n        title=\"Preceding nodes\",\n    )\n\n    @validator(\"prenodes\")\n    def ensure_unique_names(cls, value):\n        \"\"\"Ensure name fields within the lists have unique names.\"\"\"\n        ensure_unique_names(value)\n        return value\n\n\nclass CreateWriteImageModel(BaseSettingsModel):\n    temp_rendering_path_template: str = SettingsField(\n        title=\"Temporary rendering path template\"\n    )\n    default_variants: list[str] = SettingsField(\n        title=\"Default variants\",\n        default_factory=list\n    )\n    instance_attributes: list[str] = SettingsField(\n        default_factory=list,\n        enum_resolver=instance_attributes_enum,\n        title=\"Instance attributes\"\n    )\n    exposed_knobs: list[str] = SettingsField(\n        title=\"Write Node Exposed Knobs\",\n        default_factory=list\n    )\n    prenodes: list[PrenodeModel] = SettingsField(\n        default_factory=list,\n        title=\"Preceding nodes\",\n    )\n\n    @validator(\"prenodes\")\n    def ensure_unique_names(cls, value):\n        \"\"\"Ensure name fields within the lists have unique names.\"\"\"\n        ensure_unique_names(value)\n        return value\n\n\nclass CreatorPluginsSettings(BaseSettingsModel):\n    CreateWriteRender: CreateWriteRenderModel = SettingsField(\n        default_factory=CreateWriteRenderModel,\n        title=\"Create Write Render\"\n    )\n    CreateWritePrerender: CreateWritePrerenderModel = SettingsField(\n        default_factory=CreateWritePrerenderModel,\n        title=\"Create Write Prerender\"\n    )\n    CreateWriteImage: CreateWriteImageModel = SettingsField(\n        default_factory=CreateWriteImageModel,\n        title=\"Create Write Image\"\n    )\n\n\nDEFAULT_CREATE_SETTINGS = {\n    \"CreateWriteRender\": {\n        \"temp_rendering_path_template\": \"{work}/renders/nuke/{product[name]}/{product[name]}.{frame}.{ext}\",\n        \"default_variants\": [\n            \"Main\",\n            \"Mask\"\n        ],\n        \"instance_attributes\": [\n            \"reviewable\",\n            \"farm_rendering\"\n        ],\n        \"exposed_knobs\": [],\n        \"prenodes\": [\n            {\n                \"name\": \"Reformat01\",\n                \"nodeclass\": \"Reformat\",\n                \"dependent\": \"\",\n                \"knobs\": [\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"resize\",\n                        \"text\": \"none\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"name\": \"black_outside\",\n                        \"boolean\": True\n                    }\n                ]\n            }\n        ]\n    },\n    \"CreateWritePrerender\": {\n        \"temp_rendering_path_template\": \"{work}/renders/nuke/{product[name]}/{product[name]}.{frame}.{ext}\",\n        \"default_variants\": [\n            \"Key01\",\n            \"Bg01\",\n            \"Fg01\",\n            \"Branch01\",\n            \"Part01\"\n        ],\n        \"instance_attributes\": [\n            \"farm_rendering\",\n            \"use_range_limit\"\n        ],\n        \"exposed_knobs\": [],\n        \"prenodes\": []\n    },\n    \"CreateWriteImage\": {\n        \"temp_rendering_path_template\": \"{work}/renders/nuke/{product[name]}/{product[name]}.{ext}\",\n        \"default_variants\": [\n            \"StillFrame\",\n            \"MPFrame\",\n            \"LayoutFrame\"\n        ],\n        \"instance_attributes\": [\n            \"use_range_limit\"\n        ],\n        \"exposed_knobs\": [],\n        \"prenodes\": [\n            {\n                \"name\": \"FrameHold01\",\n                \"nodeclass\": \"FrameHold\",\n                \"dependent\": \"\",\n                \"knobs\": [\n                    {\n                        \"type\": \"expression\",\n                        \"name\": \"first_frame\",\n                        \"expression\": \"parent.first\"\n                    }\n                ]\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "server_addon/nuke/server/settings/dirmap.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass DirmapPathsSubmodel(BaseSettingsModel):\n    _layout = \"compact\"\n    source_path: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Source Paths\"\n    )\n    destination_path: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Destination Paths\"\n    )\n\n\nclass DirmapSettings(BaseSettingsModel):\n    \"\"\"Nuke color management project settings.\"\"\"\n    _isGroup: bool = True\n\n    enabled: bool = SettingsField(title=\"enabled\")\n    paths: DirmapPathsSubmodel = SettingsField(\n        default_factory=DirmapPathsSubmodel,\n        title=\"Dirmap Paths\"\n    )\n\n\nDEFAULT_DIRMAP_SETTINGS = {\n    \"enabled\": False,\n    \"paths\": {\n        \"source_path\": [],\n        \"destination_path\": []\n    }\n}\n"
  },
  {
    "path": "server_addon/nuke/server/settings/general.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass MenuShortcut(BaseSettingsModel):\n    \"\"\"Nuke general project settings.\"\"\"\n\n    create: str = SettingsField(\n        title=\"Create...\"\n    )\n    publish: str = SettingsField(\n        title=\"Publish...\"\n    )\n    load: str = SettingsField(\n        title=\"Load...\"\n    )\n    manage: str = SettingsField(\n        title=\"Manage...\"\n    )\n    build_workfile: str = SettingsField(\n        title=\"Build Workfile...\"\n    )\n\n\nclass GeneralSettings(BaseSettingsModel):\n    \"\"\"Nuke general project settings.\"\"\"\n\n    menu: MenuShortcut = SettingsField(\n        default_factory=MenuShortcut,\n        title=\"Menu Shortcuts\",\n    )\n\n\nDEFAULT_GENERAL_SETTINGS = {\n    \"menu\": {\n        \"create\": \"ctrl+alt+c\",\n        \"publish\": \"ctrl+alt+p\",\n        \"load\": \"ctrl+alt+l\",\n        \"manage\": \"ctrl+alt+m\",\n        \"build_workfile\": \"ctrl+alt+b\"\n    }\n}\n"
  },
  {
    "path": "server_addon/nuke/server/settings/gizmo.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    MultiplatformPathModel,\n    MultiplatformPathListModel,\n)\n\n\nclass SubGizmoItem(BaseSettingsModel):\n    title: str = SettingsField(\n        title=\"Label\"\n    )\n    sourcetype: str = SettingsField(\n        title=\"Type of usage\"\n    )\n    command: str = SettingsField(\n        title=\"Python command\"\n    )\n    icon: str = SettingsField(\n        title=\"Icon Path\"\n    )\n    shortcut: str = SettingsField(\n        title=\"Hotkey\"\n    )\n\n\nclass GizmoDefinitionItem(BaseSettingsModel):\n    gizmo_toolbar_path: str = SettingsField(\n        title=\"Gizmo Menu\"\n    )\n    sub_gizmo_list: list[SubGizmoItem] = SettingsField(\n        default_factory=list, title=\"Sub Gizmo List\")\n\n\nclass GizmoItem(BaseSettingsModel):\n    \"\"\"Nuke gizmo item \"\"\"\n\n    toolbar_menu_name: str = SettingsField(\n        title=\"Toolbar Menu Name\"\n    )\n    gizmo_source_dir: MultiplatformPathListModel = SettingsField(\n        default_factory=MultiplatformPathListModel,\n        title=\"Gizmo Directory Path\"\n    )\n    toolbar_icon_path: MultiplatformPathModel = SettingsField(\n        default_factory=MultiplatformPathModel,\n        title=\"Toolbar Icon Path\"\n    )\n    gizmo_definition: list[GizmoDefinitionItem] = SettingsField(\n        default_factory=list, title=\"Gizmo Definition\")\n\n\nDEFAULT_GIZMO_ITEM = {\n    \"toolbar_menu_name\": \"OpenPype Gizmo\",\n    \"gizmo_source_dir\": {\n        \"windows\": [],\n        \"darwin\": [],\n        \"linux\": []\n    },\n    \"toolbar_icon_path\": {\n        \"windows\": \"\",\n        \"darwin\": \"\",\n        \"linux\": \"\"\n    },\n    \"gizmo_definition\": [\n        {\n            \"gizmo_toolbar_path\": \"/path/to/menu\",\n            \"sub_gizmo_list\": [\n                {\n                    \"sourcetype\": \"python\",\n                    \"title\": \"Gizmo Note\",\n                    \"command\": \"nuke.nodes.StickyNote(label='You can create your own toolbar menu in the Nuke GizmoMenu of OpenPype')\",\n                    \"icon\": \"\",\n                    \"shortcut\": \"\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "server_addon/nuke/server/settings/imageio.py",
    "content": "from typing import Literal\nfrom pydantic import validator\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n)\n\nfrom .common import KnobModel\n\n\nclass NodesModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    plugins: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Used in plugins\"\n    )\n    nuke_node_class: str = SettingsField(\n        title=\"Nuke Node Class\",\n    )\n\n\nclass RequiredNodesModel(NodesModel):\n    knobs: list[KnobModel] = SettingsField(\n        default_factory=list,\n        title=\"Knobs\",\n    )\n\n    @validator(\"knobs\")\n    def ensure_unique_names(cls, value):\n        \"\"\"Ensure name fields within the lists have unique names.\"\"\"\n        ensure_unique_names(value)\n        return value\n\n\nclass OverrideNodesModel(NodesModel):\n    subsets: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Subsets\"\n    )\n\n    knobs: list[KnobModel] = SettingsField(\n        default_factory=list,\n        title=\"Knobs\",\n    )\n\n    @validator(\"knobs\")\n    def ensure_unique_names(cls, value):\n        \"\"\"Ensure name fields within the lists have unique names.\"\"\"\n        ensure_unique_names(value)\n        return value\n\n\nclass NodesSetting(BaseSettingsModel):\n    required_nodes: list[RequiredNodesModel] = SettingsField(\n        title=\"Plugin required\",\n        default_factory=list\n    )\n    override_nodes: list[OverrideNodesModel] = SettingsField(\n        title=\"Plugin's node overrides\",\n        default_factory=list\n    )\n\n\ndef ocio_configs_switcher_enum():\n    return [\n        {\"value\": \"nuke-default\", \"label\": \"nuke-default\"},\n        {\"value\": \"spi-vfx\", \"label\": \"spi-vfx (11)\"},\n        {\"value\": \"spi-anim\", \"label\": \"spi-anim (11)\"},\n        {\"value\": \"aces_0.1.1\", \"label\": \"aces_0.1.1 (11)\"},\n        {\"value\": \"aces_0.7.1\", \"label\": \"aces_0.7.1 (11)\"},\n        {\"value\": \"aces_1.0.1\", \"label\": \"aces_1.0.1 (11)\"},\n        {\"value\": \"aces_1.0.3\", \"label\": \"aces_1.0.3 (11, 12)\"},\n        {\"value\": \"aces_1.1\", \"label\": \"aces_1.1 (12, 13)\"},\n        {\"value\": \"aces_1.2\", \"label\": \"aces_1.2 (13, 14)\"},\n        {\"value\": \"studio-config-v1.0.0_aces-v1.3_ocio-v2.1\",\n         \"label\": \"studio-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)\"},\n        {\"value\": \"cg-config-v1.0.0_aces-v1.3_ocio-v2.1\",\n         \"label\": \"cg-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)\"},\n    ]\n\n\nclass WorkfileColorspaceSettings(BaseSettingsModel):\n    \"\"\"Nuke workfile colorspace preset. \"\"\"\n\n    color_management: Literal[\"Nuke\", \"OCIO\"] = SettingsField(\n        title=\"Color Management Workflow\"\n    )\n\n    native_ocio_config: str = SettingsField(\n        title=\"Native OpenColorIO Config\",\n        description=\"Switch between native OCIO configs\",\n        enum_resolver=ocio_configs_switcher_enum,\n        conditionalEnum=True\n    )\n\n    working_space: str = SettingsField(\n        title=\"Working Space\"\n    )\n    thumbnail_space: str = SettingsField(\n        title=\"Thumbnail Space\"\n    )\n\n\nclass ReadColorspaceRulesItems(BaseSettingsModel):\n    _layout = \"expanded\"\n\n    regex: str = SettingsField(\"\", title=\"Regex expression\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace\")\n\n\nclass RegexInputsModel(BaseSettingsModel):\n    inputs: list[ReadColorspaceRulesItems] = SettingsField(\n        default_factory=list,\n        title=\"Inputs\"\n    )\n\n\nclass ViewProcessModel(BaseSettingsModel):\n    viewerProcess: str = SettingsField(\n        title=\"Viewer Process Name\"\n    )\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ImageIOSettings(BaseSettingsModel):\n    \"\"\"Nuke color management project settings. \"\"\"\n    _isGroup: bool = True\n\n    \"\"\"# TODO: enhance settings with host api:\n    to restructure settings for simplification.\n\n    now: nuke/imageio/viewer/viewerProcess\n    future: nuke/imageio/viewer\n    \"\"\"\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\")\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n    viewer: ViewProcessModel = SettingsField(\n        default_factory=ViewProcessModel,\n        title=\"Viewer\",\n        description=\"\"\"Viewer profile is used during\n        Creation of new viewer node at knob viewerProcess\"\"\"\n    )\n\n    \"\"\"# TODO: enhance settings with host api:\n    to restructure settings for simplification.\n\n    now: nuke/imageio/baking/viewerProcess\n    future: nuke/imageio/baking\n    \"\"\"\n    baking: ViewProcessModel = SettingsField(\n        default_factory=ViewProcessModel,\n        title=\"Baking\",\n        description=\"\"\"Baking profile is used during\n        publishing baked colorspace data at knob viewerProcess\"\"\"\n    )\n\n    workfile: WorkfileColorspaceSettings = SettingsField(\n        default_factory=WorkfileColorspaceSettings,\n        title=\"Workfile\"\n    )\n\n    nodes: NodesSetting = SettingsField(\n        default_factory=NodesSetting,\n        title=\"Nodes\"\n    )\n    \"\"\"# TODO: enhance settings with host api:\n    - [ ] no need for `inputs` middle part. It can stay\n      directly on `regex_inputs`\n    \"\"\"\n    regex_inputs: RegexInputsModel = SettingsField(\n        default_factory=RegexInputsModel,\n        title=\"Assign colorspace to read nodes via rules\"\n    )\n\n\nDEFAULT_IMAGEIO_SETTINGS = {\n    \"viewer\": {\n        \"viewerProcess\": \"sRGB (default)\"\n    },\n    \"baking\": {\n        \"viewerProcess\": \"rec709 (default)\"\n    },\n    \"workfile\": {\n        \"color_management\": \"OCIO\",\n        \"native_ocio_config\": \"nuke-default\",\n        \"working_space\": \"scene_linear\",\n        \"thumbnail_space\": \"sRGB (default)\",\n    },\n    \"nodes\": {\n        \"required_nodes\": [\n            {\n                \"plugins\": [\n                    \"CreateWriteRender\"\n                ],\n                \"nuke_node_class\": \"Write\",\n                \"knobs\": [\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"file_type\",\n                        \"text\": \"exr\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"datatype\",\n                        \"text\": \"16 bit half\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"compression\",\n                        \"text\": \"Zip (1 scanline)\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"name\": \"autocrop\",\n                        \"boolean\": True\n                    },\n                    {\n                        \"type\": \"color_gui\",\n                        \"name\": \"tile_color\",\n                        \"color_gui\": [\n                            186,\n                            35,\n                            35\n                        ]\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"channels\",\n                        \"text\": \"rgb\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"colorspace\",\n                        \"text\": \"scene_linear\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"name\": \"create_directories\",\n                        \"boolean\": True\n                    }\n                ]\n            },\n            {\n                \"plugins\": [\n                    \"CreateWritePrerender\"\n                ],\n                \"nuke_node_class\": \"Write\",\n                \"knobs\": [\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"file_type\",\n                        \"text\": \"exr\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"datatype\",\n                        \"text\": \"16 bit half\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"compression\",\n                        \"text\": \"Zip (1 scanline)\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"name\": \"autocrop\",\n                        \"boolean\": True\n                    },\n                    {\n                        \"type\": \"color_gui\",\n                        \"name\": \"tile_color\",\n                        \"color_gui\": [\n                            171,\n                            171,\n                            10\n                        ]\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"channels\",\n                        \"text\": \"rgb\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"colorspace\",\n                        \"text\": \"scene_linear\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"name\": \"create_directories\",\n                        \"boolean\": True\n                    }\n                ]\n            },\n            {\n                \"plugins\": [\n                    \"CreateWriteImage\"\n                ],\n                \"nuke_node_class\": \"Write\",\n                \"knobs\": [\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"file_type\",\n                        \"text\": \"tiff\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"datatype\",\n                        \"text\": \"16 bit\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"compression\",\n                        \"text\": \"Deflate\"\n                    },\n                    {\n                        \"type\": \"color_gui\",\n                        \"name\": \"tile_color\",\n                        \"color_gui\": [\n                            56,\n                            162,\n                            7\n                        ]\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"channels\",\n                        \"text\": \"rgb\"\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"name\": \"colorspace\",\n                        \"text\": \"texture_paint\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"name\": \"create_directories\",\n                        \"boolean\": True\n                    }\n                ]\n            }\n        ],\n        \"override_nodes\": []\n    },\n    \"regex_inputs\": {\n        \"inputs\": [\n            {\n                \"regex\": \"(beauty).*(?=.exr)\",\n                \"colorspace\": \"linear\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "server_addon/nuke/server/settings/loader_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass LoadImageModel(BaseSettingsModel):\n    enabled: bool = SettingsField(\n        title=\"Enabled\"\n    )\n    representations_include: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Include representations\"\n    )\n\n    node_name_template: str = SettingsField(\n        title=\"Read node name template\"\n    )\n\n\nclass LoadClipOptionsModel(BaseSettingsModel):\n    start_at_workfile: bool = SettingsField(\n        title=\"Start at workfile's start frame\"\n    )\n    add_retime: bool = SettingsField(\n        title=\"Add retime\"\n    )\n\n\nclass LoadClipModel(BaseSettingsModel):\n    enabled: bool = SettingsField(\n        title=\"Enabled\"\n    )\n    representations_include: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Include representations\"\n    )\n\n    node_name_template: str = SettingsField(\n        title=\"Read node name template\"\n    )\n    options_defaults: LoadClipOptionsModel = SettingsField(\n        default_factory=LoadClipOptionsModel,\n        title=\"Loader option defaults\"\n    )\n\n\nclass LoaderPuginsModel(BaseSettingsModel):\n    LoadImage: LoadImageModel = SettingsField(\n        default_factory=LoadImageModel,\n        title=\"Load Image\"\n    )\n    LoadClip: LoadClipModel = SettingsField(\n        default_factory=LoadClipModel,\n        title=\"Load Clip\"\n    )\n\n\nDEFAULT_LOADER_PLUGINS_SETTINGS = {\n    \"LoadImage\": {\n        \"enabled\": True,\n        \"representations_include\": [],\n        \"node_name_template\": \"{class_name}_{ext}\"\n    },\n    \"LoadClip\": {\n        \"enabled\": True,\n        \"representations_include\": [],\n        \"node_name_template\": \"{class_name}_{ext}\",\n        \"options_defaults\": {\n            \"start_at_workfile\": True,\n            \"add_retime\": True\n        }\n    }\n}\n"
  },
  {
    "path": "server_addon/nuke/server/settings/main.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names\n)\n\nfrom .general import (\n    GeneralSettings,\n    DEFAULT_GENERAL_SETTINGS\n)\nfrom .imageio import (\n    ImageIOSettings,\n    DEFAULT_IMAGEIO_SETTINGS\n)\nfrom .dirmap import (\n    DirmapSettings,\n    DEFAULT_DIRMAP_SETTINGS\n)\nfrom .scriptsmenu import (\n    ScriptsmenuSettings,\n    DEFAULT_SCRIPTSMENU_SETTINGS\n)\nfrom .gizmo import (\n    GizmoItem,\n    DEFAULT_GIZMO_ITEM\n)\nfrom .create_plugins import (\n    CreatorPluginsSettings,\n    DEFAULT_CREATE_SETTINGS\n)\nfrom .publish_plugins import (\n    PublishPuginsModel,\n    DEFAULT_PUBLISH_PLUGIN_SETTINGS\n)\nfrom .loader_plugins import (\n    LoaderPuginsModel,\n    DEFAULT_LOADER_PLUGINS_SETTINGS\n)\nfrom .workfile_builder import (\n    WorkfileBuilderModel,\n    DEFAULT_WORKFILE_BUILDER_SETTINGS\n)\nfrom .templated_workfile_build import (\n    TemplatedWorkfileBuildModel\n)\n\n\nclass NukeSettings(BaseSettingsModel):\n    \"\"\"Nuke addon settings.\"\"\"\n\n    general: GeneralSettings = SettingsField(\n        default_factory=GeneralSettings,\n        title=\"General\",\n    )\n\n    imageio: ImageIOSettings = SettingsField(\n        default_factory=ImageIOSettings,\n        title=\"Color Management (imageio)\",\n    )\n\n    dirmap: DirmapSettings = SettingsField(\n        default_factory=DirmapSettings,\n        title=\"Nuke Directory Mapping\",\n    )\n\n    scriptsmenu: ScriptsmenuSettings = SettingsField(\n        default_factory=ScriptsmenuSettings,\n        title=\"Scripts Menu Definition\",\n    )\n\n    gizmo: list[GizmoItem] = SettingsField(\n        default_factory=list, title=\"Gizmo Menu\")\n\n    create: CreatorPluginsSettings = SettingsField(\n        default_factory=CreatorPluginsSettings,\n        title=\"Creator Plugins\",\n    )\n\n    publish: PublishPuginsModel = SettingsField(\n        default_factory=PublishPuginsModel,\n        title=\"Publish Plugins\",\n    )\n\n    load: LoaderPuginsModel = SettingsField(\n        default_factory=LoaderPuginsModel,\n        title=\"Loader Plugins\",\n    )\n\n    workfile_builder: WorkfileBuilderModel = SettingsField(\n        default_factory=WorkfileBuilderModel,\n        title=\"Workfile Builder\",\n    )\n\n    templated_workfile_build: TemplatedWorkfileBuildModel = SettingsField(\n        title=\"Templated Workfile Build\",\n        default_factory=TemplatedWorkfileBuildModel\n    )\n\n\nDEFAULT_VALUES = {\n    \"general\": DEFAULT_GENERAL_SETTINGS,\n    \"imageio\": DEFAULT_IMAGEIO_SETTINGS,\n    \"dirmap\": DEFAULT_DIRMAP_SETTINGS,\n    \"scriptsmenu\": DEFAULT_SCRIPTSMENU_SETTINGS,\n    \"gizmo\": [DEFAULT_GIZMO_ITEM],\n    \"create\": DEFAULT_CREATE_SETTINGS,\n    \"publish\": DEFAULT_PUBLISH_PLUGIN_SETTINGS,\n    \"load\": DEFAULT_LOADER_PLUGINS_SETTINGS,\n    \"workfile_builder\": DEFAULT_WORKFILE_BUILDER_SETTINGS,\n    \"templated_workfile_build\": {\n        \"profiles\": []\n    }\n}\n"
  },
  {
    "path": "server_addon/nuke/server/settings/publish_plugins.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n    task_types_enum\n)\nfrom .common import KnobModel, validate_json_dict\n\n\ndef nuke_render_publish_types_enum():\n    \"\"\"Return all nuke render families available in creators.\"\"\"\n    return [\n        {\"value\": \"render\", \"label\": \"Render\"},\n        {\"value\": \"prerender\", \"label\": \"Prerender\"},\n        {\"value\": \"image\", \"label\": \"Image\"}\n    ]\n\n\ndef nuke_product_types_enum():\n    \"\"\"Return all nuke families available in creators.\"\"\"\n    return [\n        {\"value\": \"nukenodes\", \"label\": \"Nukenodes\"},\n        {\"value\": \"model\", \"label\": \"Model\"},\n        {\"value\": \"camera\", \"label\": \"Camera\"},\n        {\"value\": \"gizmo\", \"label\": \"Gizmo\"},\n        {\"value\": \"source\", \"label\": \"Source\"}\n    ] + nuke_render_publish_types_enum()\n\n\nclass NodeModel(BaseSettingsModel):\n    name: str = SettingsField(\n        title=\"Node name\"\n    )\n    nodeclass: str = SettingsField(\n        \"\",\n        title=\"Node class\"\n    )\n    dependent: str = SettingsField(\n        \"\",\n        title=\"Incoming dependency\"\n    )\n    knobs: list[KnobModel] = SettingsField(\n        default_factory=list,\n        title=\"Knobs\",\n    )\n\n    @validator(\"knobs\")\n    def ensure_unique_names(cls, value):\n        \"\"\"Ensure name fields within the lists have unique names.\"\"\"\n        ensure_unique_names(value)\n        return value\n\n\nclass CollectInstanceDataModel(BaseSettingsModel):\n    sync_workfile_version_on_product_types: list[str] = SettingsField(\n        default_factory=list,\n        enum_resolver=nuke_product_types_enum,\n        title=\"Sync workfile versions for familes\"\n    )\n\n\nclass OptionalPluginModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n\n\nclass ValidateKnobsModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    knobs: str = SettingsField(\n        \"{}\",\n        title=\"Knobs\",\n        widget=\"textarea\",\n    )\n\n    @validator(\"knobs\")\n    def validate_json(cls, value):\n        return validate_json_dict(value)\n\n\nclass ExtractReviewDataModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n\n\nclass ExtractReviewDataLutModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n\n\nclass BakingStreamFilterModel(BaseSettingsModel):\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        enum_resolver=nuke_render_publish_types_enum,\n        title=\"Sync workfile versions for familes\"\n    )\n    product_names: list[str] = SettingsField(\n        default_factory=list, title=\"Product names\")\n\n\nclass ReformatNodesRepositionNodes(BaseSettingsModel):\n    node_class: str = SettingsField(title=\"Node class\")\n    knobs: list[KnobModel] = SettingsField(\n        default_factory=list,\n        title=\"Node knobs\")\n\n\nclass ReformatNodesConfigModel(BaseSettingsModel):\n    \"\"\"Only reposition nodes supported.\n\n    You can add multiple reformat nodes and set their knobs.\n    Order of reformat nodes is important. First reformat node will\n    be applied first and last reformat node will be applied last.\n    \"\"\"\n    enabled: bool = SettingsField(False)\n    reposition_nodes: list[ReformatNodesRepositionNodes] = SettingsField(\n        default_factory=list,\n        title=\"Reposition knobs\"\n    )\n\n\nclass IntermediateOutputModel(BaseSettingsModel):\n    name: str = SettingsField(title=\"Output name\")\n    filter: BakingStreamFilterModel = SettingsField(\n        title=\"Filter\", default_factory=BakingStreamFilterModel)\n    read_raw: bool = SettingsField(\n        False,\n        title=\"Read raw switch\"\n    )\n    viewer_process_override: str = SettingsField(\n        \"\",\n        title=\"Viewer process override\"\n    )\n    bake_viewer_process: bool = SettingsField(\n        True,\n        title=\"Bake viewer process\"\n    )\n    bake_viewer_input_process: bool = SettingsField(\n        True,\n        title=\"Bake viewer input process node (LUT)\"\n    )\n    reformat_nodes_config: ReformatNodesConfigModel = SettingsField(\n        default_factory=ReformatNodesConfigModel,\n        title=\"Reformat Nodes\")\n    extension: str = SettingsField(\n        \"mov\",\n        title=\"File extension\"\n    )\n    add_custom_tags: list[str] = SettingsField(\n        title=\"Custom tags\", default_factory=list)\n\n\nclass ExtractReviewDataMovModel(BaseSettingsModel):\n    \"\"\"[deprecated] use Extract Review Data Baking\n    Streams instead.\n    \"\"\"\n    enabled: bool = SettingsField(title=\"Enabled\")\n    viewer_lut_raw: bool = SettingsField(title=\"Viewer lut raw\")\n    outputs: list[IntermediateOutputModel] = SettingsField(\n        default_factory=list,\n        title=\"Baking streams\"\n    )\n\n\nclass ExtractReviewIntermediatesModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    viewer_lut_raw: bool = SettingsField(title=\"Viewer lut raw\")\n    outputs: list[IntermediateOutputModel] = SettingsField(\n        default_factory=list,\n        title=\"Baking streams\"\n    )\n\n\nclass FSubmissionNoteModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"enabled\")\n    template: str = SettingsField(title=\"Template\")\n\n\nclass FSubmistingForModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"enabled\")\n    template: str = SettingsField(title=\"Template\")\n\n\nclass FVFXScopeOfWorkModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"enabled\")\n    template: str = SettingsField(title=\"Template\")\n\n\nclass ExctractSlateFrameParamModel(BaseSettingsModel):\n    f_submission_note: FSubmissionNoteModel = SettingsField(\n        title=\"f_submission_note\",\n        default_factory=FSubmissionNoteModel\n    )\n    f_submitting_for: FSubmistingForModel = SettingsField(\n        title=\"f_submitting_for\",\n        default_factory=FSubmistingForModel\n    )\n    f_vfx_scope_of_work: FVFXScopeOfWorkModel = SettingsField(\n        title=\"f_vfx_scope_of_work\",\n        default_factory=FVFXScopeOfWorkModel\n    )\n\n\nclass ExtractSlateFrameModel(BaseSettingsModel):\n    viewer_lut_raw: bool = SettingsField(title=\"Viewer lut raw\")\n    key_value_mapping: ExctractSlateFrameParamModel = SettingsField(\n        title=\"Key value mapping\",\n        default_factory=ExctractSlateFrameParamModel\n    )\n\n\nclass IncrementScriptVersionModel(BaseSettingsModel):\n    enabled: bool = SettingsField(title=\"Enabled\")\n    optional: bool = SettingsField(title=\"Optional\")\n    active: bool = SettingsField(title=\"Active\")\n\n\nclass PublishPuginsModel(BaseSettingsModel):\n    CollectInstanceData: CollectInstanceDataModel = SettingsField(\n        title=\"Collect Instance Version\",\n        default_factory=CollectInstanceDataModel,\n        section=\"Collectors\"\n    )\n    ValidateCorrectAssetContext: OptionalPluginModel = SettingsField(\n        title=\"Validate Correct Folder Name\",\n        default_factory=OptionalPluginModel,\n        section=\"Validators\"\n    )\n    ValidateContainers: OptionalPluginModel = SettingsField(\n        title=\"Validate Containers\",\n        default_factory=OptionalPluginModel\n    )\n    ValidateKnobs: ValidateKnobsModel = SettingsField(\n        title=\"Validate Knobs\",\n        default_factory=ValidateKnobsModel\n    )\n    ValidateOutputResolution: OptionalPluginModel = SettingsField(\n        title=\"Validate Output Resolution\",\n        default_factory=OptionalPluginModel\n    )\n    ValidateGizmo: OptionalPluginModel = SettingsField(\n        title=\"Validate Gizmo\",\n        default_factory=OptionalPluginModel\n    )\n    ValidateBackdrop: OptionalPluginModel = SettingsField(\n        title=\"Validate Backdrop\",\n        default_factory=OptionalPluginModel\n    )\n    ValidateScriptAttributes: OptionalPluginModel = SettingsField(\n        title=\"Validate workfile attributes\",\n        default_factory=OptionalPluginModel\n    )\n    ExtractReviewData: ExtractReviewDataModel = SettingsField(\n        title=\"Extract Review Data\",\n        default_factory=ExtractReviewDataModel\n    )\n    ExtractReviewDataLut: ExtractReviewDataLutModel = SettingsField(\n        title=\"Extract Review Data Lut\",\n        default_factory=ExtractReviewDataLutModel\n    )\n    ExtractReviewDataMov: ExtractReviewDataMovModel = SettingsField(\n        title=\"Extract Review Data Mov\",\n        default_factory=ExtractReviewDataMovModel\n    )\n    ExtractReviewIntermediates: ExtractReviewIntermediatesModel = (\n        SettingsField(\n            title=\"Extract Review Intermediates\",\n            default_factory=ExtractReviewIntermediatesModel\n        )\n    )\n    ExtractSlateFrame: ExtractSlateFrameModel = SettingsField(\n        title=\"Extract Slate Frame\",\n        default_factory=ExtractSlateFrameModel\n    )\n    IncrementScriptVersion: IncrementScriptVersionModel = SettingsField(\n        title=\"Increment Workfile Version\",\n        default_factory=IncrementScriptVersionModel,\n        section=\"Integrators\"\n    )\n\n\nDEFAULT_PUBLISH_PLUGIN_SETTINGS = {\n    \"CollectInstanceData\": {\n        \"sync_workfile_version_on_product_types\": [\n            \"nukenodes\",\n            \"camera\",\n            \"gizmo\",\n            \"source\",\n            \"render\",\n            \"write\"\n        ]\n    },\n    \"ValidateCorrectAssetContext\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateContainers\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateKnobs\": {\n        \"enabled\": False,\n        \"knobs\": \"\\n\".join([\n            '{',\n            '    \"render\": {',\n            '        \"review\": true',\n            '    }',\n            '}'\n        ])\n    },\n    \"ValidateOutputResolution\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateGizmo\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateBackdrop\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateScriptAttributes\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ExtractReviewData\": {\n        \"enabled\": False\n    },\n    \"ExtractReviewDataLut\": {\n        \"enabled\": False\n    },\n    \"ExtractReviewDataMov\": {\n        \"enabled\": False,\n        \"viewer_lut_raw\": False,\n        \"outputs\": [\n            {\n                \"name\": \"baking\",\n                \"filter\": {\n                    \"task_types\": [],\n                    \"product_types\": [],\n                    \"product_names\": []\n                },\n                \"read_raw\": False,\n                \"viewer_process_override\": \"\",\n                \"bake_viewer_process\": True,\n                \"bake_viewer_input_process\": True,\n                \"reformat_nodes_config\": {\n                    \"enabled\": False,\n                    \"reposition_nodes\": [\n                        {\n                            \"node_class\": \"Reformat\",\n                            \"knobs\": [\n                                {\n                                    \"type\": \"text\",\n                                    \"name\": \"type\",\n                                    \"text\": \"to format\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"name\": \"format\",\n                                    \"text\": \"HD_1080\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"name\": \"filter\",\n                                    \"text\": \"Lanczos6\"\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"name\": \"black_outside\",\n                                    \"boolean\": True\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"name\": \"pbb\",\n                                    \"boolean\": False\n                                }\n                            ]\n                        }\n                    ]\n                },\n                \"extension\": \"mov\",\n                \"add_custom_tags\": []\n            }\n        ]\n    },\n    \"ExtractReviewIntermediates\": {\n        \"enabled\": True,\n        \"viewer_lut_raw\": False,\n        \"outputs\": [\n            {\n                \"name\": \"baking\",\n                \"filter\": {\n                    \"task_types\": [],\n                    \"product_types\": [],\n                    \"product_names\": []\n                },\n                \"read_raw\": False,\n                \"viewer_process_override\": \"\",\n                \"bake_viewer_process\": True,\n                \"bake_viewer_input_process\": True,\n                \"reformat_nodes_config\": {\n                    \"enabled\": False,\n                    \"reposition_nodes\": [\n                        {\n                            \"node_class\": \"Reformat\",\n                            \"knobs\": [\n                                {\n                                    \"type\": \"text\",\n                                    \"name\": \"type\",\n                                    \"text\": \"to format\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"name\": \"format\",\n                                    \"text\": \"HD_1080\"\n                                },\n                                {\n                                    \"type\": \"text\",\n                                    \"name\": \"filter\",\n                                    \"text\": \"Lanczos6\"\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"name\": \"black_outside\",\n                                    \"boolean\": True\n                                },\n                                {\n                                    \"type\": \"boolean\",\n                                    \"name\": \"pbb\",\n                                    \"boolean\": False\n                                }\n                            ]\n                        }\n                    ]\n                },\n                \"extension\": \"mov\",\n                \"add_custom_tags\": []\n            }\n        ]\n    },\n    \"ExtractSlateFrame\": {\n        \"viewer_lut_raw\": False,\n        \"key_value_mapping\": {\n            \"f_submission_note\": {\n                \"enabled\": True,\n                \"template\": \"{comment}\"\n            },\n            \"f_submitting_for\": {\n                \"enabled\": True,\n                \"template\": \"{intent[value]}\"\n            },\n            \"f_vfx_scope_of_work\": {\n                \"enabled\": False,\n                \"template\": \"\"\n            }\n        }\n    },\n    \"IncrementScriptVersion\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    }\n}\n"
  },
  {
    "path": "server_addon/nuke/server/settings/scriptsmenu.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass ScriptsmenuSubmodel(BaseSettingsModel):\n    \"\"\"Item Definition\"\"\"\n    _isGroup = True\n\n    type: str = SettingsField(title=\"Type\")\n    command: str = SettingsField(title=\"Command\")\n    sourcetype: str = SettingsField(title=\"Source Type\")\n    title: str = SettingsField(title=\"Title\")\n    tooltip: str = SettingsField(title=\"Tooltip\")\n\n\nclass ScriptsmenuSettings(BaseSettingsModel):\n    \"\"\"Nuke script menu project settings.\"\"\"\n    _isGroup = True\n\n    name: str = SettingsField(title=\"Menu Name\")\n    definition: list[ScriptsmenuSubmodel] = SettingsField(\n        default_factory=list,\n        title=\"Definition\",\n        description=\"Scriptmenu Items Definition\"\n    )\n\n\nDEFAULT_SCRIPTSMENU_SETTINGS = {\n    \"name\": \"Custom Tools\",\n    \"definition\": [\n        {\n            \"type\": \"action\",\n            \"sourcetype\": \"python\",\n            \"title\": \"Ayon Nuke Docs\",\n            \"command\": \"import webbrowser;webbrowser.open(url='https://ayon.ynput.io/docs/addon_nuke_artist')\",  # noqa\n            \"tooltip\": \"Open the Ayon Nuke user doc page\"\n        },\n        {\n            \"type\": \"action\",\n            \"sourcetype\": \"python\",\n            \"title\": \"Set Frame Start (Read Node)\",\n            \"command\": \"from openpype.hosts.nuke.startup.frame_setting_for_read_nodes import main;main();\",  # noqa\n            \"tooltip\": \"Set frame start for read node(s)\"\n        },\n        {\n            \"type\": \"action\",\n            \"sourcetype\": \"python\",\n            \"title\": \"Set non publish output for Write Node\",\n            \"command\": \"from openpype.hosts.nuke.startup.custom_write_node import main;main();\",  # noqa\n            \"tooltip\": \"Open the OpenPype Nuke user doc page\"\n        }\n    ]\n}\n"
  },
  {
    "path": "server_addon/nuke/server/settings/templated_workfile_build.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    task_types_enum,\n)\n\n\nclass TemplatedWorkfileProfileModel(BaseSettingsModel):\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    task_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task names\"\n    )\n    path: str = SettingsField(\n        title=\"Path to template\"\n    )\n    keep_placeholder: bool = SettingsField(\n        False,\n        title=\"Keep placeholders\")\n    create_first_version: bool = SettingsField(\n        True,\n        title=\"Create first version\"\n    )\n\n\nclass TemplatedWorkfileBuildModel(BaseSettingsModel):\n    \"\"\"Settings for templated workfile builder.\"\"\"\n    profiles: list[TemplatedWorkfileProfileModel] = SettingsField(\n        default_factory=list\n    )\n"
  },
  {
    "path": "server_addon/nuke/server/settings/workfile_builder.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    task_types_enum,\n    MultiplatformPathModel,\n)\n\n\nclass CustomTemplateModel(BaseSettingsModel):\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    path: MultiplatformPathModel = SettingsField(\n        default_factory=MultiplatformPathModel,\n        title=\"Gizmo Directory Path\"\n    )\n\n\nclass BuilderProfileItemModel(BaseSettingsModel):\n    product_name_filters: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product name\"\n    )\n    product_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Product types\"\n    )\n    repre_names: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Representations\"\n    )\n    loaders: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Loader plugins\"\n    )\n\n\nclass BuilderProfileModel(BaseSettingsModel):\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    tasks: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task names\"\n    )\n    current_context: list[BuilderProfileItemModel] = SettingsField(\n        default_factory=list,\n        title=\"Current context\"\n    )\n    linked_assets: list[BuilderProfileItemModel] = SettingsField(\n        default_factory=list,\n        title=\"Linked assets/shots\"\n    )\n\n\nclass WorkfileBuilderModel(BaseSettingsModel):\n    \"\"\"[deprecated] use Template Workfile Build Settings instead.\n    \"\"\"\n    create_first_version: bool = SettingsField(\n        title=\"Create first workfile\")\n    custom_templates: list[CustomTemplateModel] = SettingsField(\n        default_factory=list,\n        title=\"Custom templates\"\n    )\n    builder_on_start: bool = SettingsField(\n        default=False,\n        title=\"Run Builder at first workfile\"\n    )\n    profiles: list[BuilderProfileModel] = SettingsField(\n        default_factory=list,\n        title=\"Builder profiles\"\n    )\n\n\nDEFAULT_WORKFILE_BUILDER_SETTINGS = {\n    \"create_first_version\": False,\n    \"custom_templates\": [],\n    \"builder_on_start\": False,\n    \"profiles\": []\n}\n"
  },
  {
    "path": "server_addon/nuke/server/version.py",
    "content": "__version__ = \"0.1.9\"\n"
  },
  {
    "path": "server_addon/openpype/client/pyproject.toml",
    "content": "[project]\nname=\"openpype\"\ndescription=\"OpenPype addon for AYON server.\"\n\n[tool.poetry.dependencies]\npython = \">=3.9.1,<3.10\"\naiohttp_json_rpc = \"*\" # TVPaint server\naiohttp-middlewares = \"^2.0.0\"\nwsrpc_aiohttp = \"^3.1.1\" # websocket server\nClick = \"^8\"\nclique = \"1.6.*\"\njsonschema = \"^2.6.0\"\npymongo = \"^3.11.2\"\nlog4mongo = \"^1.7\"\npyblish-base = \"^1.8.11\"\npynput = \"^1.7.2\" # Timers manager - TODO remove\nspeedcopy = \"^2.1\"\nsix = \"^1.15\"\nqtawesome = \"0.7.3\"\n\n[ayon.runtimeDependencies]\nOpenTimelineIO = \"0.14.1\"\nopencolorio = \"2.2.1\"\nPillow = \"9.5.0\"\n"
  },
  {
    "path": "server_addon/openpype/server/__init__.py",
    "content": "from ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\n\n\nclass OpenPypeAddon(BaseServerAddon):\n    name = \"openpype\"\n    title = \"OpenPype\"\n    version = __version__\n"
  },
  {
    "path": "server_addon/photoshop/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "server_addon/photoshop/README.md",
    "content": "Photoshp Addon\n===============\n\nIntegration with Adobe Photoshop.\n"
  },
  {
    "path": "server_addon/photoshop/server/__init__.py",
    "content": "from ayon_server.addons import BaseServerAddon\n\nfrom .settings import PhotoshopSettings, DEFAULT_PHOTOSHOP_SETTING\nfrom .version import __version__\n\n\nclass Photoshop(BaseServerAddon):\n    name = \"photoshop\"\n    title = \"Photoshop\"\n    version = __version__\n\n    settings_model = PhotoshopSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_PHOTOSHOP_SETTING)\n"
  },
  {
    "path": "server_addon/photoshop/server/settings/__init__.py",
    "content": "from .main import (\n    PhotoshopSettings,\n    DEFAULT_PHOTOSHOP_SETTING,\n)\n\n\n__all__ = (\n    \"PhotoshopSettings\",\n    \"DEFAULT_PHOTOSHOP_SETTING\",\n)\n"
  },
  {
    "path": "server_addon/photoshop/server/settings/creator_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass CreateImagePluginModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    active_on_create: bool = SettingsField(True, title=\"Active by default\")\n    mark_for_review: bool = SettingsField(False, title=\"Review by default\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Default Variants\"\n    )\n\n\nclass AutoImageCreatorPluginModel(BaseSettingsModel):\n    enabled: bool = SettingsField(False, title=\"Enabled\")\n    active_on_create: bool = SettingsField(True, title=\"Active by default\")\n    mark_for_review: bool = SettingsField(False, title=\"Review by default\")\n    default_variant: str = SettingsField(\"\", title=\"Default Variants\")\n\n\nclass CreateReviewPlugin(BaseSettingsModel):\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    active_on_create: bool = SettingsField(True, title=\"Active by default\")\n    default_variant: str = SettingsField(\"\", title=\"Default Variants\")\n\n\nclass CreateWorkfilelugin(BaseSettingsModel):\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    active_on_create: bool = SettingsField(True, title=\"Active by default\")\n    default_variant: str = SettingsField(\"\", title=\"Default Variants\")\n\n\nclass PhotoshopCreatorPlugins(BaseSettingsModel):\n    ImageCreator: CreateImagePluginModel = SettingsField(\n        title=\"Create Image\",\n        default_factory=CreateImagePluginModel,\n    )\n    AutoImageCreator: AutoImageCreatorPluginModel = SettingsField(\n        title=\"Create Flatten Image\",\n        default_factory=AutoImageCreatorPluginModel,\n    )\n    ReviewCreator: CreateReviewPlugin = SettingsField(\n        title=\"Create Review\",\n        default_factory=CreateReviewPlugin,\n    )\n    WorkfileCreator: CreateWorkfilelugin = SettingsField(\n        title=\"Create Workfile\",\n        default_factory=CreateWorkfilelugin,\n    )\n\n\nDEFAULT_CREATE_SETTINGS = {\n    \"ImageCreator\": {\n        \"enabled\": True,\n        \"active_on_create\": True,\n        \"mark_for_review\": False,\n        \"default_variants\": [\n            \"Main\"\n        ]\n    },\n    \"AutoImageCreator\": {\n        \"enabled\": False,\n        \"active_on_create\": True,\n        \"mark_for_review\": False,\n        \"default_variant\": \"\"\n    },\n    \"ReviewCreator\": {\n        \"enabled\": True,\n        \"active_on_create\": True,\n        \"default_variant\": \"\"\n    },\n    \"WorkfileCreator\": {\n        \"enabled\": True,\n        \"active_on_create\": True,\n        \"default_variant\": \"Main\"\n    }\n}\n"
  },
  {
    "path": "server_addon/photoshop/server/settings/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ImageIORemappingRulesModel(BaseSettingsModel):\n    host_native_name: str = SettingsField(\n        title=\"Application native colorspace name\"\n    )\n    ocio_name: str = SettingsField(title=\"OCIO colorspace name\")\n\n\nclass ImageIORemappingModel(BaseSettingsModel):\n    rules: list[ImageIORemappingRulesModel] = SettingsField(\n        default_factory=list)\n\n\nclass PhotoshopImageIOModel(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    remapping: ImageIORemappingModel = SettingsField(\n        title=\"Remapping colorspace names\",\n        default_factory=ImageIORemappingModel\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n"
  },
  {
    "path": "server_addon/photoshop/server/settings/main.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\nfrom .imageio import PhotoshopImageIOModel\nfrom .creator_plugins import PhotoshopCreatorPlugins, DEFAULT_CREATE_SETTINGS\nfrom .publish_plugins import PhotoshopPublishPlugins, DEFAULT_PUBLISH_SETTINGS\nfrom .workfile_builder import WorkfileBuilderPlugin\n\n\nclass PhotoshopSettings(BaseSettingsModel):\n    \"\"\"Photoshop Project Settings.\"\"\"\n\n    imageio: PhotoshopImageIOModel = SettingsField(\n        default_factory=PhotoshopImageIOModel,\n        title=\"OCIO config\"\n    )\n\n    create: PhotoshopCreatorPlugins = SettingsField(\n        default_factory=PhotoshopCreatorPlugins,\n        title=\"Creator plugins\"\n    )\n\n    publish: PhotoshopPublishPlugins = SettingsField(\n        default_factory=PhotoshopPublishPlugins,\n        title=\"Publish plugins\"\n    )\n\n    workfile_builder: WorkfileBuilderPlugin = SettingsField(\n        default_factory=WorkfileBuilderPlugin,\n        title=\"Workfile Builder\"\n    )\n\n\nDEFAULT_PHOTOSHOP_SETTING = {\n    \"create\": DEFAULT_CREATE_SETTINGS,\n    \"publish\": DEFAULT_PUBLISH_SETTINGS,\n    \"workfile_builder\": {\n        \"create_first_version\": False,\n        \"custom_templates\": []\n    }\n}\n"
  },
  {
    "path": "server_addon/photoshop/server/settings/publish_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\ncreate_flatten_image_enum = [\n    {\"value\": \"flatten_with_images\", \"label\": \"Flatten with images\"},\n    {\"value\": \"flatten_only\", \"label\": \"Flatten only\"},\n    {\"value\": \"no\", \"label\": \"No\"},\n]\n\n\ncolor_code_enum = [\n    {\"value\": \"red\", \"label\": \"Red\"},\n    {\"value\": \"orange\", \"label\": \"Orange\"},\n    {\"value\": \"yellowColor\", \"label\": \"Yellow\"},\n    {\"value\": \"grain\", \"label\": \"Green\"},\n    {\"value\": \"blue\", \"label\": \"Blue\"},\n    {\"value\": \"violet\", \"label\": \"Violet\"},\n    {\"value\": \"gray\", \"label\": \"Gray\"},\n]\n\n\nclass ColorCodeMappings(BaseSettingsModel):\n    color_code: list[str] = SettingsField(\n        title=\"Color codes for layers\",\n        default_factory=list,\n        enum_resolver=lambda: color_code_enum,\n    )\n\n    layer_name_regex: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Layer name regex\"\n    )\n\n    product_type: str = SettingsField(\n        \"\",\n        title=\"Resulting product type\"\n    )\n\n    product_name_template: str = SettingsField(\n        \"\",\n        title=\"Product name template\"\n    )\n\n\nclass ExtractedOptions(BaseSettingsModel):\n    tags: list[str] = SettingsField(\n        title=\"Tags\",\n        default_factory=list\n    )\n\n\nclass CollectColorCodedInstancesPlugin(BaseSettingsModel):\n    \"\"\"Set color for publishable layers, set its resulting product type\n    and template for product name. \\n Can create flatten image from published\n    instances.\n    (Applicable only for remote publishing!)\"\"\"\n\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n    create_flatten_image: str = SettingsField(\n        \"\",\n        title=\"Create flatten image\",\n        enum_resolver=lambda: create_flatten_image_enum,\n    )\n\n    flatten_product_type_template: str = SettingsField(\n        \"\",\n        title=\"Subset template for flatten image\"\n    )\n\n    color_code_mapping: list[ColorCodeMappings] = SettingsField(\n        title=\"Color code mappings\",\n        default_factory=ColorCodeMappings,\n    )\n\n\nclass CollectReviewPlugin(BaseSettingsModel):\n    \"\"\"Should review product be created\"\"\"\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n\n\nclass CollectVersionPlugin(BaseSettingsModel):\n    \"\"\"Synchronize version for image and review instances by workfile version\"\"\"  # noqa\n    enabled: bool = SettingsField(True, title=\"Enabled\")\n\n\nclass ValidateContainersPlugin(BaseSettingsModel):\n    \"\"\"Check that workfile contains latest version of loaded items\"\"\"  # noqa\n    _isGroup = True\n    enabled: bool = True\n    optional: bool = SettingsField(False, title=\"Optional\")\n    active: bool = SettingsField(True, title=\"Active\")\n\n\nclass ValidateNamingPlugin(BaseSettingsModel):\n    \"\"\"Validate naming of products and layers\"\"\"  # noqa\n    invalid_chars: str = SettingsField(\n        '',\n        title=\"Regex pattern of invalid characters\"\n    )\n\n    replace_char: str = SettingsField(\n        '',\n        title=\"Replacement character\"\n    )\n\n\nclass ExtractImagePlugin(BaseSettingsModel):\n    \"\"\"Currently only jpg and png are supported\"\"\"\n    formats: list[str] = SettingsField(\n        title=\"Extract Formats\",\n        default_factory=list,\n    )\n\n\nclass ExtractReviewPlugin(BaseSettingsModel):\n    make_image_sequence: bool = SettingsField(\n        False,\n        title=\"Make an image sequence instead of flatten image\"\n    )\n\n    max_downscale_size: int = SettingsField(\n        8192,\n        title=\"Maximum size of sources for review\",\n        description=\"FFMpeg can only handle limited resolution for creation of review and/or thumbnail\",  # noqa\n        gt=300,  # greater than\n        le=16384,  # less or equal\n    )\n\n    jpg_options: ExtractedOptions = SettingsField(\n        title=\"Extracted jpg Options\",\n        default_factory=ExtractedOptions\n    )\n\n    mov_options: ExtractedOptions = SettingsField(\n        title=\"Extracted mov Options\",\n        default_factory=ExtractedOptions\n    )\n\n\nclass PhotoshopPublishPlugins(BaseSettingsModel):\n    CollectColorCodedInstances: CollectColorCodedInstancesPlugin = (\n        SettingsField(\n            title=\"Collect Color Coded Instances\",\n            default_factory=CollectColorCodedInstancesPlugin,\n        )\n    )\n    CollectReview: CollectReviewPlugin = SettingsField(\n        title=\"Collect Review\",\n        default_factory=CollectReviewPlugin,\n    )\n\n    CollectVersion: CollectVersionPlugin = SettingsField(\n        title=\"Collect Version\",\n        default_factory=CollectVersionPlugin,\n    )\n\n    ValidateContainers: ValidateContainersPlugin = SettingsField(\n        title=\"Validate Containers\",\n        default_factory=ValidateContainersPlugin,\n    )\n\n    ValidateNaming: ValidateNamingPlugin = SettingsField(\n        title=\"Validate naming of products and layers\",\n        default_factory=ValidateNamingPlugin,\n    )\n\n    ExtractImage: ExtractImagePlugin = SettingsField(\n        title=\"Extract Image\",\n        default_factory=ExtractImagePlugin,\n    )\n\n    ExtractReview: ExtractReviewPlugin = SettingsField(\n        title=\"Extract Review\",\n        default_factory=ExtractReviewPlugin,\n    )\n\n\nDEFAULT_PUBLISH_SETTINGS = {\n    \"CollectColorCodedInstances\": {\n        \"create_flatten_image\": \"no\",\n        \"flatten_product_type_template\": \"\",\n        \"color_code_mapping\": []\n    },\n    \"CollectReview\": {\n        \"enabled\": True\n    },\n    \"CollectVersion\": {\n        \"enabled\": False\n    },\n    \"ValidateContainers\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateNaming\": {\n        \"invalid_chars\": \"[ \\\\\\\\/+\\\\*\\\\?\\\\(\\\\)\\\\[\\\\]\\\\{\\\\}:,;]\",\n        \"replace_char\": \"_\"\n    },\n    \"ExtractImage\": {\n        \"formats\": [\n            \"png\",\n            \"jpg\"\n        ]\n    },\n    \"ExtractReview\": {\n        \"make_image_sequence\": False,\n        \"max_downscale_size\": 8192,\n        \"jpg_options\": {\n            \"tags\": [\n                \"review\",\n                \"ftrackreview\"\n            ]\n        },\n        \"mov_options\": {\n            \"tags\": [\n                \"review\",\n                \"ftrackreview\"\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "server_addon/photoshop/server/settings/workfile_builder.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    MultiplatformPathModel,\n)\n\n\nclass CustomBuilderTemplate(BaseSettingsModel):\n    _layout = \"expanded\"\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n    )\n\n    path: MultiplatformPathModel = SettingsField(\n        default_factory=MultiplatformPathModel,\n        title=\"Template path\"\n    )\n\n\nclass WorkfileBuilderPlugin(BaseSettingsModel):\n    _title = \"Workfile Builder\"\n    create_first_version: bool = SettingsField(\n        False,\n        title=\"Create first workfile\"\n    )\n\n    custom_templates: list[CustomBuilderTemplate] = SettingsField(\n        default_factory=CustomBuilderTemplate,\n        title=\"Template profiles\"\n    )\n"
  },
  {
    "path": "server_addon/photoshop/server/version.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Package declaring addon version.\"\"\"\n__version__ = \"0.1.1\"\n"
  },
  {
    "path": "server_addon/resolve/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import ResolveSettings, DEFAULT_VALUES\n\n\nclass ResolveAddon(BaseServerAddon):\n    name = \"resolve\"\n    title = \"DaVinci Resolve\"\n    version = __version__\n    settings_model: Type[ResolveSettings] = ResolveSettings\n    frontend_scopes = {}\n    services = {}\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/resolve/server/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ImageIORemappingRulesModel(BaseSettingsModel):\n    host_native_name: str = SettingsField(\n        title=\"Application native colorspace name\"\n    )\n    ocio_name: str = SettingsField(title=\"OCIO colorspace name\")\n\n\nclass ImageIORemappingModel(BaseSettingsModel):\n    rules: list[ImageIORemappingRulesModel] = SettingsField(\n        default_factory=list)\n\n\nclass ResolveImageIOModel(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    remapping: ImageIORemappingModel = SettingsField(\n        title=\"Remapping colorspace names\",\n        default_factory=ImageIORemappingModel\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n"
  },
  {
    "path": "server_addon/resolve/server/settings.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\nfrom .imageio import ResolveImageIOModel\n\n\nclass CreateShotClipModels(BaseSettingsModel):\n    hierarchy: str = SettingsField(\n        \"{folder}/{sequence}\",\n        title=\"Shot parent hierarchy\",\n        section=\"Shot Hierarchy And Rename Settings\"\n    )\n    clipRename: bool = SettingsField(\n        True,\n        title=\"Rename clips\"\n    )\n    clipName: str = SettingsField(\n        \"{track}{sequence}{shot}\",\n        title=\"Clip name template\"\n    )\n    countFrom: int = SettingsField(\n        10,\n        title=\"Count sequence from\"\n    )\n    countSteps: int = SettingsField(\n        10,\n        title=\"Stepping number\"\n    )\n\n    folder: str = SettingsField(\n        \"shots\",\n        title=\"{folder}\",\n        section=\"Shot Template Keywords\"\n    )\n    episode: str = SettingsField(\n        \"ep01\",\n        title=\"{episode}\"\n    )\n    sequence: str = SettingsField(\n        \"sq01\",\n        title=\"{sequence}\"\n    )\n    track: str = SettingsField(\n        \"{_track_}\",\n        title=\"{track}\"\n    )\n    shot: str = SettingsField(\n        \"sh###\",\n        title=\"{shot}\"\n    )\n\n    vSyncOn: bool = SettingsField(\n        False,\n        title=\"Enable Vertical Sync\",\n        section=\"Vertical Synchronization Of Attributes\"\n    )\n\n    workfileFrameStart: int = SettingsField(\n        1001,\n        title=\"Workfiles Start Frame\",\n        section=\"Shot Attributes\"\n    )\n    handleStart: int = SettingsField(\n        10,\n        title=\"Handle start (head)\"\n    )\n    handleEnd: int = SettingsField(\n        10,\n        title=\"Handle end (tail)\"\n    )\n\n\nclass CreatorPuginsModel(BaseSettingsModel):\n    CreateShotClip: CreateShotClipModels = SettingsField(\n        default_factory=CreateShotClipModels,\n        title=\"Create Shot Clip\"\n    )\n\n\nclass ResolveSettings(BaseSettingsModel):\n    launch_openpype_menu_on_start: bool = SettingsField(\n        False, title=\"Launch OpenPype menu on start of Resolve\"\n    )\n    imageio: ResolveImageIOModel = SettingsField(\n        default_factory=ResolveImageIOModel,\n        title=\"Color Management (ImageIO)\"\n    )\n    create: CreatorPuginsModel = SettingsField(\n        default_factory=CreatorPuginsModel,\n        title=\"Creator plugins\",\n    )\n\n\nDEFAULT_VALUES = {\n    \"launch_openpype_menu_on_start\": False,\n    \"create\": {\n        \"CreateShotClip\": {\n            \"hierarchy\": \"{folder}/{sequence}\",\n            \"clipRename\": True,\n            \"clipName\": \"{track}{sequence}{shot}\",\n            \"countFrom\": 10,\n            \"countSteps\": 10,\n            \"folder\": \"shots\",\n            \"episode\": \"ep01\",\n            \"sequence\": \"sq01\",\n            \"track\": \"{_track_}\",\n            \"shot\": \"sh###\",\n            \"vSyncOn\": False,\n            \"workfileFrameStart\": 1001,\n            \"handleStart\": 10,\n            \"handleEnd\": 10\n        }\n    }\n}\n"
  },
  {
    "path": "server_addon/resolve/server/version.py",
    "content": "__version__ = \"0.1.0\"\n"
  },
  {
    "path": "server_addon/royal_render/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import RoyalRenderSettings, DEFAULT_VALUES\n\n\nclass RoyalRenderAddon(BaseServerAddon):\n    name = \"royalrender\"\n    version = __version__\n    title = \"Royal Render\"\n    settings_model: Type[RoyalRenderSettings] = RoyalRenderSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/royal_render/server/settings.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    MultiplatformPathModel,\n)\n\n\nclass CustomPath(MultiplatformPathModel):\n    _layout = \"expanded\"\n\n\nclass ServerListSubmodel(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField(\"\", title=\"Name\")\n    value: CustomPath = SettingsField(\n        default_factory=CustomPath\n    )\n\n\nclass CollectSequencesFromJobModel(BaseSettingsModel):\n    review: bool = SettingsField(\n        True, title=\"Generate reviews from sequences\"\n    )\n\n\nclass PublishPluginsModel(BaseSettingsModel):\n    CollectSequencesFromJob: CollectSequencesFromJobModel = SettingsField(\n        default_factory=CollectSequencesFromJobModel,\n        title=\"Collect Sequences from the Job\"\n    )\n\n\nclass RoyalRenderSettings(BaseSettingsModel):\n    enabled: bool = True\n    # WARNING/TODO this needs change\n    # - both system and project settings contained 'rr_path'\n    #   where project settings did choose one of rr_path from system settings\n    #   that is not possible in AYON\n    rr_paths: list[ServerListSubmodel] = SettingsField(\n        default_factory=list,\n        title=\"Royal Render Root Paths\",\n        scope=[\"studio\"],\n    )\n    # This was 'rr_paths' in project settings and should be enum of\n    #   'rr_paths' from system settings, but that's not possible in AYON\n    selected_rr_paths: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Selected Royal Render Paths\",\n        section=\"---\",\n    )\n    publish: PublishPluginsModel = SettingsField(\n        default_factory=PublishPluginsModel,\n        title=\"Publish plugins\",\n    )\n\n\nDEFAULT_VALUES = {\n    \"enabled\": False,\n    \"rr_paths\": [\n        {\n            \"name\": \"default\",\n            \"value\": {\n                \"windows\": \"\",\n                \"darwin\": \"\",\n                \"linux\": \"\"\n            }\n        }\n    ],\n    \"selected_rr_paths\": [\"default\"],\n    \"publish\": {\n        \"CollectSequencesFromJob\": {\n            \"review\": True\n        }\n    }\n}\n"
  },
  {
    "path": "server_addon/royal_render/server/version.py",
    "content": "__version__ = \"0.1.1\"\n"
  },
  {
    "path": "server_addon/substancepainter/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import SubstancePainterSettings, DEFAULT_SPAINTER_SETTINGS\n\n\nclass SubstancePainterAddon(BaseServerAddon):\n    name = \"substancepainter\"\n    title = \"Substance Painter\"\n    version = __version__\n    settings_model: Type[SubstancePainterSettings] = SubstancePainterSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_SPAINTER_SETTINGS)\n"
  },
  {
    "path": "server_addon/substancepainter/server/settings/__init__.py",
    "content": "from .main import (\n    SubstancePainterSettings,\n    DEFAULT_SPAINTER_SETTINGS,\n)\n\n\n__all__ = (\n    \"SubstancePainterSettings\",\n    \"DEFAULT_SPAINTER_SETTINGS\",\n)\n"
  },
  {
    "path": "server_addon/substancepainter/server/settings/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass ImageIOSettings(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n\n\nDEFAULT_IMAGEIO_SETTINGS = {\n    \"activate_host_color_management\": True,\n    \"ocio_config\": {\n        \"override_global_config\": False,\n        \"filepath\": []\n    },\n    \"file_rules\": {\n        \"activate_host_rules\": False,\n        \"rules\": []\n    }\n}\n"
  },
  {
    "path": "server_addon/substancepainter/server/settings/load_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\ndef normal_map_format_enum():\n    return [\n        {\"label\": \"DirectX\", \"value\": \"NormalMapFormat.DirectX\"},\n        {\"label\": \"OpenGL\", \"value\": \"NormalMapFormat.OpenGL\"},\n    ]\n\n\ndef tangent_space_enum():\n    return [\n        {\"label\": \"Per Fragment\", \"value\": \"TangentSpace.PerFragment\"},\n        {\"label\": \"Per Vertex\", \"value\": \"TangentSpace.PerVertex\"},\n    ]\n\n\ndef uv_workflow_enum():\n    return [\n        {\"label\": \"Default\", \"value\": \"ProjectWorkflow.Default\"},\n        {\"label\": \"UV Tile\", \"value\": \"ProjectWorkflow.UVTile\"},\n        {\"label\": \"Texture Set Per UV Tile\",\n         \"value\": \"ProjectWorkflow.TextureSetPerUVTile\"}\n    ]\n\n\ndef document_resolution_enum():\n    return [\n        {\"label\": \"128\", \"value\": 128},\n        {\"label\": \"256\", \"value\": 256},\n        {\"label\": \"512\", \"value\": 512},\n        {\"label\": \"1024\", \"value\": 1024},\n        {\"label\": \"2048\", \"value\": 2048},\n        {\"label\": \"4096\", \"value\": 4096}\n    ]\n\n\nclass ProjectTemplatesModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField(\"default\", title=\"Template Name\")\n    default_texture_resolution: int = SettingsField(\n        1024, enum_resolver=document_resolution_enum,\n        title=\"Document Resolution\",\n        description=(\"Set texture resolution when \"\n                     \"creating new project.\")\n    )\n    import_cameras: bool = SettingsField(\n        True, title=\"Import Cameras\",\n        description=\"Import cameras from the mesh file.\")\n    normal_map_format: str = SettingsField(\n        \"DirectX\", enum_resolver=normal_map_format_enum,\n        title=\"Normal Map Format\",\n        description=(\"Set normal map format when \"\n                     \"creating new project.\")\n    )\n    project_workflow: str = SettingsField(\n        \"Default\", enum_resolver=uv_workflow_enum,\n        title=\"UV Tile Settings\",\n        description=(\"Set UV workflow when \"\n                     \"creating new project.\")\n    )\n    tangent_space_mode: str = SettingsField(\n        \"PerFragment\", enum_resolver=tangent_space_enum,\n        title=\"Tangent Space\",\n        description=(\"An option to compute tangent space \"\n                     \"when creating new project.\")\n    )\n    preserve_strokes: bool = SettingsField(\n        True, title=\"Preserve Strokes\",\n        description=(\"Preserve strokes positions on mesh.\\n\"\n                     \"(only relevant when loading into \"\n                     \"existing project)\")\n    )\n\n\nclass ProjectTemplateSettingModel(BaseSettingsModel):\n    project_templates: list[ProjectTemplatesModel] = SettingsField(\n        default_factory=ProjectTemplatesModel,\n        title=\"Project Templates\"\n    )\n\n\nclass LoadersModel(BaseSettingsModel):\n    SubstanceLoadProjectMesh: ProjectTemplateSettingModel = SettingsField(\n        default_factory=ProjectTemplateSettingModel,\n        title=\"Load Mesh\"\n    )\n\n\nDEFAULT_LOADER_SETTINGS = {\n    \"SubstanceLoadProjectMesh\": {\n        \"project_templates\": [\n            {\n                \"name\": \"2K(Default)\",\n                \"default_texture_resolution\": 2048,\n                \"import_cameras\": True,\n                \"normal_map_format\": \"NormalMapFormat.DirectX\",\n                \"project_workflow\": \"ProjectWorkflow.Default\",\n                \"tangent_space_mode\": \"TangentSpace.PerFragment\",\n                \"preserve_strokes\": True\n            },\n            {\n                \"name\": \"2K(UV tile)\",\n                \"default_texture_resolution\": 2048,\n                \"import_cameras\": True,\n                \"normal_map_format\": \"NormalMapFormat.DirectX\",\n                \"project_workflow\": \"ProjectWorkflow.UVTile\",\n                \"tangent_space_mode\": \"TangentSpace.PerFragment\",\n                \"preserve_strokes\": True\n            },\n            {\n                \"name\": \"4K(Custom)\",\n                \"default_texture_resolution\": 4096,\n                \"import_cameras\": True,\n                \"normal_map_format\": \"NormalMapFormat.OpenGL\",\n                \"project_workflow\": \"ProjectWorkflow.UVTile\",\n                \"tangent_space_mode\": \"TangentSpace.PerFragment\",\n                \"preserve_strokes\": True\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "server_addon/substancepainter/server/settings/main.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\nfrom .imageio import ImageIOSettings, DEFAULT_IMAGEIO_SETTINGS\nfrom .load_plugins import LoadersModel, DEFAULT_LOADER_SETTINGS\n\n\nclass ShelvesSettingsModel(BaseSettingsModel):\n    _layout = \"compact\"\n    name: str = SettingsField(title=\"Name\")\n    value: str = SettingsField(title=\"Path\")\n\n\nclass SubstancePainterSettings(BaseSettingsModel):\n    imageio: ImageIOSettings = SettingsField(\n        default_factory=ImageIOSettings,\n        title=\"Color Management (ImageIO)\"\n    )\n    shelves: list[ShelvesSettingsModel] = SettingsField(\n        default_factory=list,\n        title=\"Shelves\"\n    )\n    load: LoadersModel = SettingsField(\n        default_factory=DEFAULT_LOADER_SETTINGS, title=\"Loaders\")\n\n\nDEFAULT_SPAINTER_SETTINGS = {\n    \"imageio\": DEFAULT_IMAGEIO_SETTINGS,\n    \"shelves\": [],\n    \"load\": DEFAULT_LOADER_SETTINGS,\n}\n"
  },
  {
    "path": "server_addon/substancepainter/server/version.py",
    "content": "__version__ = \"0.1.1\"\n"
  },
  {
    "path": "server_addon/timers_manager/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import TimersManagerSettings\n\n\nclass TimersManagerAddon(BaseServerAddon):\n    name = \"timers_manager\"\n    version = __version__\n    title = \"Timers Manager\"\n    settings_model: Type[TimersManagerSettings] = TimersManagerSettings\n"
  },
  {
    "path": "server_addon/timers_manager/server/settings.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass TimersManagerSettings(BaseSettingsModel):\n    auto_stop: bool = SettingsField(\n        True,\n        title=\"Auto stop timer\",\n        scope=[\"studio\"],\n    )\n    full_time: int = SettingsField(\n        15,\n        title=\"Max idle time\",\n        scope=[\"studio\"],\n    )\n    message_time: float = SettingsField(\n        0.5,\n        title=\"When dialog will show\",\n        scope=[\"studio\"],\n    )\n    disregard_publishing: bool = SettingsField(\n        False,\n        title=\"Disregard publishing\",\n        scope=[\"studio\"],\n    )\n"
  },
  {
    "path": "server_addon/timers_manager/server/version.py",
    "content": "__version__ = \"0.1.1\"\n"
  },
  {
    "path": "server_addon/traypublisher/server/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "server_addon/traypublisher/server/README.md",
    "content": "Photoshp Addon\n===============\n\nIntegration with Adobe Traypublisher.\n"
  },
  {
    "path": "server_addon/traypublisher/server/__init__.py",
    "content": "from ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import TraypublisherSettings, DEFAULT_TRAYPUBLISHER_SETTING\n\n\nclass Traypublisher(BaseServerAddon):\n    name = \"traypublisher\"\n    title = \"TrayPublisher\"\n    version = __version__\n\n    settings_model = TraypublisherSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_TRAYPUBLISHER_SETTING)\n"
  },
  {
    "path": "server_addon/traypublisher/server/settings/__init__.py",
    "content": "from .main import (\n    TraypublisherSettings,\n    DEFAULT_TRAYPUBLISHER_SETTING,\n)\n\n\n__all__ = (\n    \"TraypublisherSettings\",\n    \"DEFAULT_TRAYPUBLISHER_SETTING\",\n)\n"
  },
  {
    "path": "server_addon/traypublisher/server/settings/creator_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass BatchMovieCreatorPlugin(BaseSettingsModel):\n    \"\"\"Allows to publish multiple video files in one go. <br />Name of matching\n     asset is parsed from file names ('asset.mov', 'asset_v001.mov',\n     'my_asset_to_publish.mov')\"\"\"\n\n    default_variants: list[str] = SettingsField(\n        title=\"Default variants\",\n        default_factory=list\n    )\n\n    default_tasks: list[str] = SettingsField(\n        title=\"Default tasks\",\n        default_factory=list\n    )\n\n    extensions: list[str] = SettingsField(\n        title=\"Extensions\",\n        default_factory=list\n    )\n\n\nclass TrayPublisherCreatePluginsModel(BaseSettingsModel):\n    BatchMovieCreator: BatchMovieCreatorPlugin = SettingsField(\n        title=\"Batch Movie Creator\",\n        default_factory=BatchMovieCreatorPlugin\n    )\n\n\nDEFAULT_CREATORS = {\n    \"BatchMovieCreator\": {\n        \"default_variants\": [\n            \"Main\"\n        ],\n        \"default_tasks\": [\n            \"Compositing\"\n        ],\n        \"extensions\": [\n            \".mov\"\n        ]\n    },\n}\n"
  },
  {
    "path": "server_addon/traypublisher/server/settings/editorial_creators.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    task_types_enum,\n)\n\n\nclass ClipNameTokenizerItem(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField(\"\", title=\"Tokenizer name\")\n    regex: str = SettingsField(\"\", title=\"Tokenizer regex\")\n\n\nclass ShotAddTasksItem(BaseSettingsModel):\n    _layout = \"expanded\"\n    name: str = SettingsField('', title=\"Key\")\n    task_type: str = SettingsField(\n        title=\"Task type\",\n        enum_resolver=task_types_enum\n    )\n\n\nclass ShotRenameSubmodel(BaseSettingsModel):\n    enabled: bool = True\n    shot_rename_template: str = SettingsField(\n        \"\",\n        title=\"Shot rename template\"\n    )\n\n\nparent_type_enum = [\n    {\"value\": \"Project\", \"label\": \"Project\"},\n    {\"value\": \"Folder\", \"label\": \"Folder\"},\n    {\"value\": \"Episode\", \"label\": \"Episode\"},\n    {\"value\": \"Sequence\", \"label\": \"Sequence\"},\n]\n\n\nclass TokenToParentConvertorItem(BaseSettingsModel):\n    # TODO - was 'type' must be renamed in code to `parent_type`\n    parent_type: str = SettingsField(\n        \"Project\",\n        enum_resolver=lambda: parent_type_enum\n    )\n    name: str = SettingsField(\n        \"\",\n        title=\"Parent token name\",\n        description=\"Unique name used in `Parent path template`\"\n    )\n    value: str = SettingsField(\n        \"\",\n        title=\"Parent token value\",\n        description=\"Template where any text, Anatomy keys and Tokens could be used\"  # noqa\n    )\n\n\nclass ShotHierarchySubmodel(BaseSettingsModel):\n    enabled: bool = True\n    parents_path: str = SettingsField(\n        \"\",\n        title=\"Parents path template\",\n        description=\"Using keys from \\\"Token to parent convertor\\\" or tokens directly\"  # noqa\n    )\n    parents: list[TokenToParentConvertorItem] = SettingsField(\n        default_factory=TokenToParentConvertorItem,\n        title=\"Token to parent convertor\"\n    )\n\n\noutput_file_type = [\n    {\"value\": \".mp4\", \"label\": \"MP4\"},\n    {\"value\": \".mov\", \"label\": \"MOV\"},\n    {\"value\": \".wav\", \"label\": \"WAV\"}\n]\n\n\nclass ProductTypePresetItem(BaseSettingsModel):\n    product_type: str = SettingsField(\"\", title=\"Product type\")\n    # TODO add placeholder '< Inherited >'\n    variant: str = SettingsField(\"\", title=\"Variant\")\n    review: bool = SettingsField(True, title=\"Review\")\n    output_file_type: str = SettingsField(\n        \".mp4\",\n        enum_resolver=lambda: output_file_type\n    )\n\n\nclass EditorialSimpleCreatorPlugin(BaseSettingsModel):\n    default_variants: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Default Variants\"\n    )\n    clip_name_tokenizer: list[ClipNameTokenizerItem] = SettingsField(\n        default_factory=ClipNameTokenizerItem,\n        description=(\n            \"Using Regex expression to create tokens. \\nThose can be used\"\n            \" later in \\\"Shot rename\\\" creator \\nor \\\"Shot hierarchy\\\".\"\n            \"\\n\\nTokens should be decorated with \\\"_\\\" on each side\"\n        )\n    )\n    shot_rename: ShotRenameSubmodel = SettingsField(\n        title=\"Shot Rename\",\n        default_factory=ShotRenameSubmodel\n    )\n    shot_hierarchy: ShotHierarchySubmodel = SettingsField(\n        title=\"Shot Hierarchy\",\n        default_factory=ShotHierarchySubmodel\n    )\n    shot_add_tasks: list[ShotAddTasksItem] = SettingsField(\n        title=\"Add tasks to shot\",\n        default_factory=ShotAddTasksItem\n    )\n    product_type_presets: list[ProductTypePresetItem] = SettingsField(\n        default_factory=list\n    )\n\n\nclass TraypublisherEditorialCreatorPlugins(BaseSettingsModel):\n    editorial_simple: EditorialSimpleCreatorPlugin = SettingsField(\n        title=\"Editorial simple creator\",\n        default_factory=EditorialSimpleCreatorPlugin,\n    )\n\n\nDEFAULT_EDITORIAL_CREATORS = {\n    \"editorial_simple\": {\n        \"default_variants\": [\n            \"Main\"\n        ],\n        \"clip_name_tokenizer\": [\n            {\"name\": \"_sequence_\", \"regex\": \"(sc\\\\d{3})\"},\n            {\"name\": \"_shot_\", \"regex\": \"(sh\\\\d{3})\"}\n        ],\n        \"shot_rename\": {\n            \"enabled\": True,\n            \"shot_rename_template\": \"{project[code]}_{_sequence_}_{_shot_}\"\n        },\n        \"shot_hierarchy\": {\n            \"enabled\": True,\n            \"parents_path\": \"{project}/{folder}/{sequence}\",\n            \"parents\": [\n                {\n                    \"parent_type\": \"Project\",\n                    \"name\": \"project\",\n                    \"value\": \"{project[name]}\"\n                },\n                {\n                    \"parent_type\": \"Folder\",\n                    \"name\": \"folder\",\n                    \"value\": \"shots\"\n                },\n                {\n                    \"parent_type\": \"Sequence\",\n                    \"name\": \"sequence\",\n                    \"value\": \"{_sequence_}\"\n                }\n            ]\n        },\n        \"shot_add_tasks\": [],\n        \"product_type_presets\": [\n            {\n                \"product_type\": \"review\",\n                \"variant\": \"Reference\",\n                \"review\": True,\n                \"output_file_type\": \".mp4\"\n            },\n            {\n                \"product_type\": \"plate\",\n                \"variant\": \"\",\n                \"review\": False,\n                \"output_file_type\": \".mov\"\n            },\n            {\n                \"product_type\": \"audio\",\n                \"variant\": \"\",\n                \"review\": False,\n                \"output_file_type\": \".wav\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "server_addon/traypublisher/server/settings/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass TrayPublisherImageIOModel(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n"
  },
  {
    "path": "server_addon/traypublisher/server/settings/main.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\nfrom .imageio import TrayPublisherImageIOModel\nfrom .simple_creators import (\n    SimpleCreatorPlugin,\n    DEFAULT_SIMPLE_CREATORS,\n)\nfrom .editorial_creators import (\n    TraypublisherEditorialCreatorPlugins,\n    DEFAULT_EDITORIAL_CREATORS,\n)\nfrom .creator_plugins import (\n    TrayPublisherCreatePluginsModel,\n    DEFAULT_CREATORS,\n)\nfrom .publish_plugins import (\n    TrayPublisherPublishPlugins,\n    DEFAULT_PUBLISH_PLUGINS,\n)\n\n\nclass TraypublisherSettings(BaseSettingsModel):\n    \"\"\"Traypublisher Project Settings.\"\"\"\n    imageio: TrayPublisherImageIOModel = SettingsField(\n        default_factory=TrayPublisherImageIOModel,\n        title=\"Color Management (ImageIO)\"\n    )\n    simple_creators: list[SimpleCreatorPlugin] = SettingsField(\n        title=\"Simple Create Plugins\",\n        default_factory=SimpleCreatorPlugin,\n    )\n    editorial_creators: TraypublisherEditorialCreatorPlugins = SettingsField(\n        title=\"Editorial Creators\",\n        default_factory=TraypublisherEditorialCreatorPlugins,\n    )\n    create: TrayPublisherCreatePluginsModel = SettingsField(\n        title=\"Create\",\n        default_factory=TrayPublisherCreatePluginsModel\n    )\n    publish: TrayPublisherPublishPlugins = SettingsField(\n        title=\"Publish Plugins\",\n        default_factory=TrayPublisherPublishPlugins\n    )\n\n\nDEFAULT_TRAYPUBLISHER_SETTING = {\n    \"simple_creators\": DEFAULT_SIMPLE_CREATORS,\n    \"editorial_creators\": DEFAULT_EDITORIAL_CREATORS,\n    \"create\": DEFAULT_CREATORS,\n    \"publish\": DEFAULT_PUBLISH_PLUGINS,\n}\n"
  },
  {
    "path": "server_addon/traypublisher/server/settings/publish_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass ValidatePluginModel(BaseSettingsModel):\n    _isGroup = True\n    enabled: bool = True\n    optional: bool = SettingsField(True, title=\"Optional\")\n    active: bool = SettingsField(True, title=\"Active\")\n\n\nclass ValidateFrameRangeModel(ValidatePluginModel):\n    \"\"\"Allows to publish multiple video files in one go. <br />Name of matching\n     asset is parsed from file names ('asset.mov', 'asset_v001.mov',\n     'my_asset_to_publish.mov')\"\"\"\n\n\nclass TrayPublisherPublishPlugins(BaseSettingsModel):\n    CollectFrameDataFromAssetEntity: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Collect Frame Data From Folder Entity\",\n    )\n    ValidateFrameRange: ValidateFrameRangeModel = SettingsField(\n        title=\"Validate Frame Range\",\n        default_factory=ValidateFrameRangeModel,\n    )\n    ValidateExistingVersion: ValidatePluginModel = SettingsField(\n        title=\"Validate Existing Version\",\n        default_factory=ValidatePluginModel,\n    )\n\n\nDEFAULT_PUBLISH_PLUGINS = {\n    \"CollectFrameDataFromAssetEntity\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateFrameRange\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateExistingVersion\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    }\n}\n"
  },
  {
    "path": "server_addon/traypublisher/server/settings/simple_creators.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass SimpleCreatorPlugin(BaseSettingsModel):\n    _layout = \"expanded\"\n    product_type: str = SettingsField(\"\", title=\"Product type\")\n    # TODO add placeholder\n    identifier: str = SettingsField(\"\", title=\"Identifier\")\n    label: str = SettingsField(\"\", title=\"Label\")\n    icon: str = SettingsField(\"\", title=\"Icon\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Default Variants\"\n    )\n    description: str = SettingsField(\n        \"\",\n        title=\"Description\",\n        widget=\"textarea\"\n    )\n    detailed_description: str = SettingsField(\n        \"\",\n        title=\"Detailed Description\",\n        widget=\"textarea\"\n    )\n    allow_sequences: bool = SettingsField(\n        False,\n        title=\"Allow sequences\"\n    )\n    allow_multiple_items: bool = SettingsField(\n        False,\n        title=\"Allow multiple items\"\n    )\n    allow_version_control: bool = SettingsField(\n        False,\n        title=\"Allow version control\"\n    )\n    extensions: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Extensions\"\n    )\n\n\nDEFAULT_SIMPLE_CREATORS = [\n    {\n        \"product_type\": \"workfile\",\n        \"identifier\": \"\",\n        \"label\": \"Workfile\",\n        \"icon\": \"fa.file\",\n        \"default_variants\": [\n            \"Main\"\n        ],\n        \"description\": \"Backup of a working scene\",\n        \"detailed_description\": \"Workfiles are full scenes from any application that are directly edited by artists. They represent a state of work on a task at a given point and are usually not directly referenced into other scenes.\",\n        \"allow_sequences\": False,\n        \"allow_multiple_items\": False,\n        \"allow_version_control\": False,\n        \"extensions\": [\n            \".ma\",\n            \".mb\",\n            \".nk\",\n            \".hrox\",\n            \".hip\",\n            \".hiplc\",\n            \".hipnc\",\n            \".blend\",\n            \".scn\",\n            \".tvpp\",\n            \".comp\",\n            \".zip\",\n            \".prproj\",\n            \".drp\",\n            \".psd\",\n            \".psb\",\n            \".aep\"\n        ]\n    },\n    {\n        \"product_type\": \"model\",\n        \"identifier\": \"\",\n        \"label\": \"Model\",\n        \"icon\": \"fa.cubes\",\n        \"default_variants\": [\n            \"Main\",\n            \"Proxy\",\n            \"Sculpt\"\n        ],\n        \"description\": \"Clean models\",\n        \"detailed_description\": \"Models should only contain geometry data, without any extras like cameras, locators or bones.\\n\\nKeep in mind that models published from tray publisher are not validated for correctness. \",\n        \"allow_sequences\": False,\n        \"allow_multiple_items\": True,\n        \"allow_version_control\": False,\n        \"extensions\": [\n            \".ma\",\n            \".mb\",\n            \".obj\",\n            \".abc\",\n            \".fbx\",\n            \".bgeo\",\n            \".bgeogz\",\n            \".bgeosc\",\n            \".usd\",\n            \".blend\"\n        ]\n    },\n    {\n        \"product_type\": \"pointcache\",\n        \"identifier\": \"\",\n        \"label\": \"Pointcache\",\n        \"icon\": \"fa.gears\",\n        \"default_variants\": [\n            \"Main\"\n        ],\n        \"description\": \"Geometry Caches\",\n        \"detailed_description\": \"Alembic or bgeo cache of animated data\",\n        \"allow_sequences\": True,\n        \"allow_multiple_items\": True,\n        \"allow_version_control\": False,\n        \"extensions\": [\n            \".abc\",\n            \".bgeo\",\n            \".bgeogz\",\n            \".bgeosc\"\n        ]\n    },\n    {\n        \"product_type\": \"plate\",\n        \"identifier\": \"\",\n        \"label\": \"Plate\",\n        \"icon\": \"mdi.camera-image\",\n        \"default_variants\": [\n            \"Main\",\n            \"BG\",\n            \"Animatic\",\n            \"Reference\",\n            \"Offline\"\n        ],\n        \"description\": \"Footage Plates\",\n        \"detailed_description\": \"Any type of image seqeuence coming from outside of the studio. Usually camera footage, but could also be animatics used for reference.\",\n        \"allow_sequences\": True,\n        \"allow_multiple_items\": True,\n        \"allow_version_control\": False,\n        \"extensions\": [\n            \".exr\",\n            \".png\",\n            \".dpx\",\n            \".jpg\",\n            \".tiff\",\n            \".tif\",\n            \".mov\",\n            \".mp4\",\n            \".avi\"\n        ]\n    },\n    {\n        \"product_type\": \"render\",\n        \"identifier\": \"\",\n        \"label\": \"Render\",\n        \"icon\": \"mdi.folder-multiple-image\",\n        \"default_variants\": [],\n        \"description\": \"Rendered images or video\",\n        \"detailed_description\": \"Sequence or single file renders\",\n        \"allow_sequences\": True,\n        \"allow_multiple_items\": True,\n        \"allow_version_control\": False,\n        \"extensions\": [\n            \".exr\",\n            \".png\",\n            \".dpx\",\n            \".jpg\",\n            \".jpeg\",\n            \".tiff\",\n            \".tif\",\n            \".mov\",\n            \".mp4\",\n            \".avi\"\n        ]\n    },\n    {\n        \"product_type\": \"camera\",\n        \"identifier\": \"\",\n        \"label\": \"Camera\",\n        \"icon\": \"fa.video-camera\",\n        \"default_variants\": [],\n        \"description\": \"3d Camera\",\n        \"detailed_description\": \"Ideally this should be only camera itself with baked animation, however, it can technically also include helper geometry.\",\n        \"allow_sequences\": False,\n        \"allow_multiple_items\": True,\n        \"allow_version_control\": False,\n        \"extensions\": [\n            \".abc\",\n            \".ma\",\n            \".hip\",\n            \".blend\",\n            \".fbx\",\n            \".usd\"\n        ]\n    },\n    {\n        \"product_type\": \"image\",\n        \"identifier\": \"\",\n        \"label\": \"Image\",\n        \"icon\": \"fa.image\",\n        \"default_variants\": [\n            \"Reference\",\n            \"Texture\",\n            \"Concept\",\n            \"Background\"\n        ],\n        \"description\": \"Single image\",\n        \"detailed_description\": \"Any image data can be published as image product type. References, textures, concept art, matte paints. This is a fallback 2d product type for everything that doesn't fit more specific product type.\",\n        \"allow_sequences\": False,\n        \"allow_multiple_items\": True,\n        \"allow_version_control\": False,\n        \"extensions\": [\n            \".exr\",\n            \".jpg\",\n            \".jpeg\",\n            \".dpx\",\n            \".bmp\",\n            \".tif\",\n            \".tiff\",\n            \".png\",\n            \".psb\",\n            \".psd\"\n        ]\n    },\n    {\n        \"product_type\": \"vdb\",\n        \"identifier\": \"\",\n        \"label\": \"VDB Volumes\",\n        \"icon\": \"fa.cloud\",\n        \"default_variants\": [],\n        \"description\": \"Sparse volumetric data\",\n        \"detailed_description\": \"Hierarchical data structure for the efficient storage and manipulation of sparse volumetric data discretized on three-dimensional grids\",\n        \"allow_sequences\": True,\n        \"allow_multiple_items\": True,\n        \"allow_version_control\": False,\n        \"extensions\": [\n            \".vdb\"\n        ]\n    },\n    {\n        \"product_type\": \"matchmove\",\n        \"identifier\": \"\",\n        \"label\": \"Matchmove\",\n        \"icon\": \"fa.empire\",\n        \"default_variants\": [\n            \"Camera\",\n            \"Object\",\n            \"Mocap\"\n        ],\n        \"description\": \"Matchmoving script\",\n        \"detailed_description\": \"Script exported from matchmoving application to be later processed into a tracked camera with additional data\",\n        \"allow_sequences\": False,\n        \"allow_multiple_items\": True,\n        \"allow_version_control\": False,\n        \"extensions\": []\n    },\n    {\n        \"product_type\": \"rig\",\n        \"identifier\": \"\",\n        \"label\": \"Rig\",\n        \"icon\": \"fa.wheelchair\",\n        \"default_variants\": [],\n        \"description\": \"CG rig file\",\n        \"detailed_description\": \"CG rigged character or prop. Rig should be clean of any extra data and directly loadable into it's respective application\\t\",\n        \"allow_sequences\": False,\n        \"allow_multiple_items\": False,\n        \"allow_version_control\": False,\n        \"extensions\": [\n            \".ma\",\n            \".blend\",\n            \".hip\",\n            \".hda\"\n        ]\n    },\n    {\n        \"product_type\": \"simpleUnrealTexture\",\n        \"identifier\": \"\",\n        \"label\": \"Simple UE texture\",\n        \"icon\": \"fa.image\",\n        \"default_variants\": [],\n        \"description\": \"Simple Unreal Engine texture\",\n        \"detailed_description\": \"Texture files with Unreal Engine naming conventions\",\n        \"allow_sequences\": False,\n        \"allow_multiple_items\": True,\n        \"allow_version_control\": False,\n        \"extensions\": []\n    },\n    {\n        \"product_type\": \"audio\",\n        \"identifier\": \"\",\n        \"label\": \"Audio \",\n        \"icon\": \"fa5s.file-audio\",\n        \"default_variants\": [\n            \"Main\"\n        ],\n        \"description\": \"Audio product\",\n        \"detailed_description\": \"Audio files for review or final delivery\",\n        \"allow_sequences\": False,\n        \"allow_multiple_items\": False,\n        \"allow_version_control\": False,\n        \"extensions\": [\n            \".wav\"\n        ]\n    }\n]\n"
  },
  {
    "path": "server_addon/traypublisher/server/version.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Package declaring addon version.\"\"\"\n__version__ = \"0.1.3\"\n"
  },
  {
    "path": "server_addon/tvpaint/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import TvpaintSettings, DEFAULT_VALUES\n\n\nclass TvpaintAddon(BaseServerAddon):\n    name = \"tvpaint\"\n    title = \"TVPaint\"\n    version = __version__\n    settings_model: Type[TvpaintSettings] = TvpaintSettings\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/tvpaint/server/settings/__init__.py",
    "content": "from .main import (\n    TvpaintSettings,\n    DEFAULT_VALUES,\n)\n\n\n__all__ = (\n    \"TvpaintSettings\",\n    \"DEFAULT_VALUES\",\n)\n"
  },
  {
    "path": "server_addon/tvpaint/server/settings/create_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass CreateWorkfileModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    default_variant: str = SettingsField(title=\"Default variant\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list, title=\"Default variants\")\n\n\nclass CreateReviewModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    active_on_create: bool = SettingsField(True, title=\"Active by default\")\n    default_variant: str = SettingsField(title=\"Default variant\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list, title=\"Default variants\")\n\n\nclass CreateRenderSceneModel(BaseSettingsModel):\n    enabled: bool = SettingsField(True)\n    active_on_create: bool = SettingsField(True, title=\"Active by default\")\n    mark_for_review: bool = SettingsField(True, title=\"Review by default\")\n    default_pass_name: str = SettingsField(title=\"Default beauty pass\")\n    default_variant: str = SettingsField(title=\"Default variant\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list, title=\"Default variants\")\n\n\nclass CreateRenderLayerModel(BaseSettingsModel):\n    mark_for_review: bool = SettingsField(True, title=\"Review by default\")\n    default_pass_name: str = SettingsField(title=\"Default beauty pass\")\n    default_variant: str = SettingsField(title=\"Default variant\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list, title=\"Default variants\")\n\n\nclass CreateRenderPassModel(BaseSettingsModel):\n    mark_for_review: bool = SettingsField(True, title=\"Review by default\")\n    default_variant: str = SettingsField(title=\"Default variant\")\n    default_variants: list[str] = SettingsField(\n        default_factory=list, title=\"Default variants\")\n\n\nclass AutoDetectCreateRenderModel(BaseSettingsModel):\n    \"\"\"The creator tries to auto-detect Render Layers and Render Passes in scene.\n\n    For Render Layers is used group name as a variant and for Render Passes is\n    used TVPaint layer name.\n\n    Group names can be renamed by their used order in scene. The renaming\n    template where can be used '{group_index}' formatting key which is\n    filled by \"used position index of group\".\n    - Template: 'L{group_index}'\n    - Group offset: '10'\n    - Group padding: '3'\n\n    Would create group names \"L010\", \"L020\", ...\n    \"\"\"\n\n    enabled: bool = SettingsField(True)\n    allow_group_rename: bool = SettingsField(title=\"Allow group rename\")\n    group_name_template: str = SettingsField(title=\"Group name template\")\n    group_idx_offset: int = SettingsField(\n        1, title=\"Group index Offset\", ge=1\n    )\n    group_idx_padding: int = SettingsField(\n        4, title=\"Group index Padding\", ge=1\n    )\n\n\nclass CreatePluginsModel(BaseSettingsModel):\n    create_workfile: CreateWorkfileModel = SettingsField(\n        default_factory=CreateWorkfileModel,\n        title=\"Create Workfile\"\n    )\n    create_review: CreateReviewModel = SettingsField(\n        default_factory=CreateReviewModel,\n        title=\"Create Review\"\n    )\n    create_render_scene: CreateRenderSceneModel = SettingsField(\n        default_factory=CreateReviewModel,\n        title=\"Create Render Scene\"\n    )\n    create_render_layer: CreateRenderLayerModel = SettingsField(\n        default_factory=CreateRenderLayerModel,\n        title=\"Create Render Layer\"\n    )\n    create_render_pass: CreateRenderPassModel = SettingsField(\n        default_factory=CreateRenderPassModel,\n        title=\"Create Render Pass\"\n    )\n    auto_detect_render: AutoDetectCreateRenderModel = SettingsField(\n        default_factory=AutoDetectCreateRenderModel,\n        title=\"Auto-Detect Create Render\",\n    )\n\n\nDEFAULT_CREATE_SETTINGS = {\n    \"create_workfile\": {\n        \"enabled\": True,\n        \"default_variant\": \"Main\",\n        \"default_variants\": []\n    },\n    \"create_review\": {\n        \"enabled\": True,\n        \"active_on_create\": True,\n        \"default_variant\": \"Main\",\n        \"default_variants\": []\n    },\n    \"create_render_scene\": {\n        \"enabled\": True,\n        \"active_on_create\": False,\n        \"mark_for_review\": True,\n        \"default_pass_name\": \"beauty\",\n        \"default_variant\": \"Main\",\n        \"default_variants\": []\n    },\n    \"create_render_layer\": {\n        \"mark_for_review\": False,\n        \"default_pass_name\": \"beauty\",\n        \"default_variant\": \"Main\",\n        \"default_variants\": []\n    },\n    \"create_render_pass\": {\n        \"mark_for_review\": False,\n        \"default_variant\": \"Main\",\n        \"default_variants\": []\n    },\n    \"auto_detect_render\": {\n        \"enabled\": False,\n        \"allow_group_rename\": True,\n        \"group_name_template\": \"L{group_index}\",\n        \"group_idx_offset\": 10,\n        \"group_idx_padding\": 3\n    }\n}\n"
  },
  {
    "path": "server_addon/tvpaint/server/settings/filters.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\n\nclass FiltersSubmodel(BaseSettingsModel):\n    _layout = \"compact\"\n    name: str = SettingsField(title=\"Name\")\n    value: str = SettingsField(\n        \"\",\n        title=\"Textarea\",\n        widget=\"textarea\",\n    )\n\n\nclass PublishFiltersModel(BaseSettingsModel):\n    env_search_replace_values: list[FiltersSubmodel] = SettingsField(\n        default_factory=list\n    )\n"
  },
  {
    "path": "server_addon/tvpaint/server/settings/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass TVPaintImageIOModel(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n"
  },
  {
    "path": "server_addon/tvpaint/server/settings/main.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    ensure_unique_names,\n)\n\nfrom .imageio import TVPaintImageIOModel\nfrom .workfile_builder import WorkfileBuilderPlugin\nfrom .create_plugins import CreatePluginsModel, DEFAULT_CREATE_SETTINGS\nfrom .publish_plugins import (\n    PublishPluginsModel,\n    LoadPluginsModel,\n    DEFAULT_PUBLISH_SETTINGS,\n)\n\n\nclass TvpaintSettings(BaseSettingsModel):\n    imageio: TVPaintImageIOModel = SettingsField(\n        default_factory=TVPaintImageIOModel,\n        title=\"Color Management (ImageIO)\"\n    )\n    stop_timer_on_application_exit: bool = SettingsField(\n        title=\"Stop timer on application exit\")\n    create: CreatePluginsModel = SettingsField(\n        default_factory=CreatePluginsModel,\n        title=\"Create plugins\"\n    )\n    publish: PublishPluginsModel = SettingsField(\n        default_factory=PublishPluginsModel,\n        title=\"Publish plugins\")\n    load: LoadPluginsModel = SettingsField(\n        default_factory=LoadPluginsModel,\n        title=\"Load plugins\")\n    workfile_builder: WorkfileBuilderPlugin = SettingsField(\n        default_factory=WorkfileBuilderPlugin,\n        title=\"Workfile Builder\"\n    )\n\n\nDEFAULT_VALUES = {\n    \"stop_timer_on_application_exit\": False,\n    \"create\": DEFAULT_CREATE_SETTINGS,\n    \"publish\": DEFAULT_PUBLISH_SETTINGS,\n    \"load\": {\n        \"LoadImage\": {\n            \"defaults\": {\n                \"stretch\": True,\n                \"timestretch\": True,\n                \"preload\": True\n            }\n        },\n        \"ImportImage\": {\n            \"defaults\": {\n                \"stretch\": True,\n                \"timestretch\": True,\n                \"preload\": True\n            }\n        }\n    },\n    \"workfile_builder\": {\n        \"create_first_version\": False,\n        \"custom_templates\": []\n    },\n    \"filters\": []\n}\n"
  },
  {
    "path": "server_addon/tvpaint/server/settings/publish_plugins.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.types import ColorRGBA_uint8\n\n\nclass CollectRenderInstancesModel(BaseSettingsModel):\n    ignore_render_pass_transparency: bool = SettingsField(\n        title=\"Ignore Render Pass opacity\"\n    )\n\n\nclass ExtractSequenceModel(BaseSettingsModel):\n    \"\"\"Review BG color is used for whole scene review and for thumbnails.\"\"\"\n    # TODO Use alpha color\n    review_bg: ColorRGBA_uint8 = SettingsField(\n        (255, 255, 255, 1.0),\n        title=\"Review BG color\")\n\n\nclass ValidatePluginModel(BaseSettingsModel):\n    enabled: bool = True\n    optional: bool = SettingsField(True, title=\"Optional\")\n    active: bool = SettingsField(True, title=\"Active\")\n\n\ndef compression_enum():\n    return [\n        {\"value\": \"ZIP\", \"label\": \"ZIP\"},\n        {\"value\": \"ZIPS\", \"label\": \"ZIPS\"},\n        {\"value\": \"DWAA\", \"label\": \"DWAA\"},\n        {\"value\": \"DWAB\", \"label\": \"DWAB\"},\n        {\"value\": \"PIZ\", \"label\": \"PIZ\"},\n        {\"value\": \"RLE\", \"label\": \"RLE\"},\n        {\"value\": \"PXR24\", \"label\": \"PXR24\"},\n        {\"value\": \"B44\", \"label\": \"B44\"},\n        {\"value\": \"B44A\", \"label\": \"B44A\"},\n        {\"value\": \"none\", \"label\": \"None\"}\n    ]\n\n\nclass ExtractConvertToEXRModel(BaseSettingsModel):\n    \"\"\"WARNING: This plugin does not work on MacOS (using OIIO tool).\"\"\"\n    enabled: bool = False\n    replace_pngs: bool = True\n\n    exr_compression: str = SettingsField(\n        \"ZIP\",\n        enum_resolver=compression_enum,\n        title=\"EXR Compression\"\n    )\n\n\nclass LoadImageDefaultModel(BaseSettingsModel):\n    _layout = \"expanded\"\n    stretch: bool = SettingsField(title=\"Stretch\")\n    timestretch: bool = SettingsField(title=\"TimeStretch\")\n    preload: bool = SettingsField(title=\"Preload\")\n\n\nclass LoadImageModel(BaseSettingsModel):\n    defaults: LoadImageDefaultModel = SettingsField(\n        default_factory=LoadImageDefaultModel\n    )\n\n\nclass PublishPluginsModel(BaseSettingsModel):\n    CollectRenderInstances: CollectRenderInstancesModel = SettingsField(\n        default_factory=CollectRenderInstancesModel,\n        title=\"Collect Render Instances\")\n    ExtractSequence: ExtractSequenceModel = SettingsField(\n        default_factory=ExtractSequenceModel,\n        title=\"Extract Sequence\")\n    ValidateProjectSettings: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Validate Project Settings\")\n    ValidateMarks: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Validate MarkIn/Out\")\n    ValidateStartFrame: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Validate Scene Start Frame\")\n    ValidateAssetName: ValidatePluginModel = SettingsField(\n        default_factory=ValidatePluginModel,\n        title=\"Validate Folder Name\")\n    ExtractConvertToEXR: ExtractConvertToEXRModel = SettingsField(\n        default_factory=ExtractConvertToEXRModel,\n        title=\"Extract Convert To EXR\")\n\n\nclass LoadPluginsModel(BaseSettingsModel):\n    LoadImage: LoadImageModel = SettingsField(\n        default_factory=LoadImageModel,\n        title=\"Load Image\")\n    ImportImage: LoadImageModel = SettingsField(\n        default_factory=LoadImageModel,\n        title=\"Import Image\")\n\n\nDEFAULT_PUBLISH_SETTINGS = {\n    \"CollectRenderInstances\": {\n        \"ignore_render_pass_transparency\": False\n    },\n    \"ExtractSequence\": {\n        \"review_bg\": [255, 255, 255, 1.0]\n    },\n    \"ValidateProjectSettings\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateMarks\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateStartFrame\": {\n        \"enabled\": False,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ValidateAssetName\": {\n        \"enabled\": True,\n        \"optional\": True,\n        \"active\": True\n    },\n    \"ExtractConvertToEXR\": {\n        \"enabled\": False,\n        \"replace_pngs\": True,\n        \"exr_compression\": \"ZIP\"\n    }\n}\n"
  },
  {
    "path": "server_addon/tvpaint/server/settings/workfile_builder.py",
    "content": "from ayon_server.settings import (\n    BaseSettingsModel,\n    SettingsField,\n    MultiplatformPathModel,\n    task_types_enum,\n)\n\n\nclass CustomBuilderTemplate(BaseSettingsModel):\n    task_types: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Task types\",\n        enum_resolver=task_types_enum\n    )\n    template_path: MultiplatformPathModel = SettingsField(\n        default_factory=MultiplatformPathModel\n    )\n\n\nclass WorkfileBuilderPlugin(BaseSettingsModel):\n    _title = \"Workfile Builder\"\n    create_first_version: bool = SettingsField(\n        False,\n        title=\"Create first workfile\"\n    )\n\n    custom_templates: list[CustomBuilderTemplate] = SettingsField(\n        default_factory=CustomBuilderTemplate\n    )\n"
  },
  {
    "path": "server_addon/tvpaint/server/version.py",
    "content": "__version__ = \"0.1.1\"\n"
  },
  {
    "path": "server_addon/unreal/server/__init__.py",
    "content": "from typing import Type\n\nfrom ayon_server.addons import BaseServerAddon\n\nfrom .version import __version__\nfrom .settings import UnrealSettings, DEFAULT_VALUES\n\n\nclass UnrealAddon(BaseServerAddon):\n    name = \"unreal\"\n    title = \"Unreal\"\n    version = __version__\n    settings_model: Type[UnrealSettings] = UnrealSettings\n    frontend_scopes = {}\n    services = {}\n\n    async def get_default_settings(self):\n        settings_model_cls = self.get_settings_model()\n        return settings_model_cls(**DEFAULT_VALUES)\n"
  },
  {
    "path": "server_addon/unreal/server/imageio.py",
    "content": "from pydantic import validator\nfrom ayon_server.settings import BaseSettingsModel, SettingsField\nfrom ayon_server.settings.validators import ensure_unique_names\n\n\nclass ImageIOConfigModel(BaseSettingsModel):\n    override_global_config: bool = SettingsField(\n        False,\n        title=\"Override global OCIO config\"\n    )\n    filepath: list[str] = SettingsField(\n        default_factory=list,\n        title=\"Config path\"\n    )\n\n\nclass ImageIOFileRuleModel(BaseSettingsModel):\n    name: str = SettingsField(\"\", title=\"Rule name\")\n    pattern: str = SettingsField(\"\", title=\"Regex pattern\")\n    colorspace: str = SettingsField(\"\", title=\"Colorspace name\")\n    ext: str = SettingsField(\"\", title=\"File extension\")\n\n\nclass ImageIOFileRulesModel(BaseSettingsModel):\n    activate_host_rules: bool = SettingsField(False)\n    rules: list[ImageIOFileRuleModel] = SettingsField(\n        default_factory=list,\n        title=\"Rules\"\n    )\n\n    @validator(\"rules\")\n    def validate_unique_outputs(cls, value):\n        ensure_unique_names(value)\n        return value\n\n\nclass UnrealImageIOModel(BaseSettingsModel):\n    activate_host_color_management: bool = SettingsField(\n        True, title=\"Enable Color Management\"\n    )\n    ocio_config: ImageIOConfigModel = SettingsField(\n        default_factory=ImageIOConfigModel,\n        title=\"OCIO config\"\n    )\n    file_rules: ImageIOFileRulesModel = SettingsField(\n        default_factory=ImageIOFileRulesModel,\n        title=\"File Rules\"\n    )\n"
  },
  {
    "path": "server_addon/unreal/server/settings.py",
    "content": "from ayon_server.settings import BaseSettingsModel, SettingsField\n\nfrom .imageio import UnrealImageIOModel\n\n\nclass ProjectSetup(BaseSettingsModel):\n    dev_mode: bool = SettingsField(\n        False,\n        title=\"Dev mode\"\n    )\n\n\ndef _render_format_enum():\n    return [\n        {\"value\": \"png\", \"label\": \"PNG\"},\n        {\"value\": \"exr\", \"label\": \"EXR\"},\n        {\"value\": \"jpg\", \"label\": \"JPG\"},\n        {\"value\": \"bmp\", \"label\": \"BMP\"}\n    ]\n\n\nclass UnrealSettings(BaseSettingsModel):\n    imageio: UnrealImageIOModel = SettingsField(\n        default_factory=UnrealImageIOModel,\n        title=\"Color Management (ImageIO)\"\n    )\n    level_sequences_for_layouts: bool = SettingsField(\n        False,\n        title=\"Generate level sequences when loading layouts\"\n    )\n    delete_unmatched_assets: bool = SettingsField(\n        False,\n        title=\"Delete assets that are not matched\"\n    )\n    render_config_path: str = SettingsField(\n        \"\",\n        title=\"Render Config Path\"\n    )\n    preroll_frames: int = SettingsField(\n        0,\n        title=\"Pre-roll frames\"\n    )\n    render_format: str = SettingsField(\n        \"png\",\n        title=\"Render format\",\n        enum_resolver=_render_format_enum\n    )\n    project_setup: ProjectSetup = SettingsField(\n        default_factory=ProjectSetup,\n        title=\"Project Setup\",\n    )\n\n\nDEFAULT_VALUES = {\n    \"level_sequences_for_layouts\": True,\n    \"delete_unmatched_assets\": False,\n    \"render_config_path\": \"\",\n    \"preroll_frames\": 0,\n    \"render_format\": \"exr\",\n    \"project_setup\": {\n        \"dev_mode\": False\n    }\n}\n"
  },
  {
    "path": "server_addon/unreal/server/version.py",
    "content": "__version__ = \"0.1.0\"\n"
  },
  {
    "path": "setup.cfg",
    "content": "[flake8]\n# ignore = D203\nignore = BLK100, W504, W503\nmax-line-length = 79\nexclude =\n  .git,\n  __pycache__,\n  docs,\n  */vendor,\n  website,\n  openpype/vendor,\n  *deadline/repository/custom/plugins\n\nmax-complexity = 30\n\n[pylint.'MESSAGES CONTROL']\ndisable = no-member\n\n[coverage:run]\nbranch = True\nomit = /tests\n\n[coverage:html]\ndirectory = ./coverage\n\n[tool:pytest]\nnorecursedirs = openpype/modules/ftrack/*\n\n[isort]\nline_length = 79\nmulti_line_output = 3\ninclude_trailing_comma = True\nforce_grid_wrap = 0\ncombine_as_imports = True\n"
  },
  {
    "path": "setup.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Setup info for building OpenPype 3.0.\"\"\"\nimport os\nimport re\nimport platform\nimport distutils.spawn\nfrom pathlib import Path\n\nfrom cx_Freeze import setup, Executable\nfrom sphinx.setup_command import BuildDoc\n\nopenpype_root = Path(os.path.dirname(__file__))\n\n\ndef validate_thirdparty_binaries():\n    \"\"\"Check existence of thirdpart executables.\"\"\"\n    low_platform = platform.system().lower()\n    binary_vendors_dir = os.path.join(\n        openpype_root,\n        \"vendor\",\n        \"bin\"\n    )\n\n    error_msg = (\n        \"Missing binary dependency {}. Please fetch thirdparty dependencies.\"\n    )\n    # Validate existence of FFmpeg\n    ffmpeg_dir = os.path.join(binary_vendors_dir, \"ffmpeg\", low_platform)\n    if low_platform == \"windows\":\n        ffmpeg_dir = os.path.join(ffmpeg_dir, \"bin\")\n    ffmpeg_executable = os.path.join(ffmpeg_dir, \"ffmpeg\")\n    ffmpeg_result = distutils.spawn.find_executable(ffmpeg_executable)\n    if ffmpeg_result is None:\n        raise RuntimeError(error_msg.format(\"FFmpeg\"))\n\n    # Validate existence of OpenImageIO (not on MacOs)\n    oiio_tool_path = None\n    if low_platform == \"linux\":\n        oiio_tool_path = os.path.join(\n            binary_vendors_dir,\n            \"oiio\",\n            low_platform,\n            \"bin\",\n            \"oiiotool\"\n        )\n    elif low_platform == \"windows\":\n        oiio_tool_path = os.path.join(\n            binary_vendors_dir,\n            \"oiio\",\n            low_platform,\n            \"oiiotool\"\n        )\n    oiio_result = None\n    if oiio_tool_path is not None:\n        oiio_result = distutils.spawn.find_executable(oiio_tool_path)\n        if oiio_result is None:\n            raise RuntimeError(error_msg.format(\"OpenImageIO\"))\n\n\n# Give ability to skip vaidation\nif not os.getenv(\"SKIP_THIRD_PARTY_VALIDATION\"):\n    validate_thirdparty_binaries()\n\nversion = {}\n\nwith open(openpype_root / \"openpype\" / \"version.py\") as fp:\n    exec(fp.read(), version)\n\nversion_match = re.search(r\"(\\d+\\.\\d+.\\d+).*\", version[\"__version__\"])\n__version__ = version_match.group(1)\n\nlow_platform_name = platform.system().lower()\nIS_WINDOWS = low_platform_name == \"windows\"\nIS_LINUX = low_platform_name == \"linux\"\nIS_MACOS = low_platform_name == \"darwin\"\n\nbase = None\nif IS_WINDOWS:\n    base = \"Win32GUI\"\n\n# -----------------------------------------------------------------------\n# build_exe\n# Build options for cx_Freeze. Manually add/exclude packages and binaries\n\ninstall_requires = [\n    \"appdirs\",\n    \"cx_Freeze\",\n    \"keyring\",\n    \"clique\",\n    \"jsonschema\",\n    \"pathlib2\",\n    \"pkg_resources\",\n    \"PIL\",\n    \"pymongo\",\n    \"pynput\",\n    \"jinxed\",\n    \"blessed\",\n    \"Qt\",\n    \"qtpy\",\n    \"speedcopy\",\n    \"googleapiclient\",\n    \"httplib2\",\n    # Harmony implementation\n    \"filecmp\",\n    \"dns\",\n    # Python defaults (cx_Freeze skip them by default)\n    \"dbm\",\n    \"sqlite3\",\n    \"dataclasses\",\n    \"timeit\"\n]\n\nincludes = []\n# WARNING: As of cx_freeze there is a bug?\n# when this is empty, its hooks will not kick in\n# and won't clean platform irrelevant modules\n# like dbm mentioned above.\nexcludes = [\n    \"openpype\"\n]\nbin_includes = [\n    \"vendor\"\n]\ninclude_files = [\n    \"igniter\",\n    \"openpype\",\n    \"LICENSE\",\n    \"README.md\"\n]\n\nif IS_WINDOWS:\n    install_requires.extend([\n        # `pywin32` packages\n        \"win32ctypes\",\n        \"win32comext\",\n        \"pythoncom\"\n    ])\n\n\nicon_path = openpype_root / \"igniter\" / \"openpype.ico\"\nmac_icon_path = openpype_root / \"igniter\" / \"openpype.icns\"\n\nbuild_exe_options = dict(\n    packages=install_requires,\n    includes=includes,\n    excludes=excludes,\n    bin_includes=bin_includes,\n    include_files=include_files,\n    optimize=0\n)\n\nbdist_mac_options = dict(\n    bundle_name=f\"OpenPype {__version__}\",\n    iconfile=mac_icon_path\n)\n\nexecutables = [\n    Executable(\n        \"start.py\",\n        base=base,\n        target_name=\"openpype_gui\",\n        icon=icon_path.as_posix()\n    ),\n    Executable(\n        \"start.py\",\n        base=None,\n        target_name=\"openpype_console\",\n        icon=icon_path.as_posix()\n    ),\n]\n\nif IS_LINUX:\n    executables.append(\n        Executable(\n            \"app_launcher.py\",\n            base=None,\n            target_name=\"app_launcher\",\n            icon=icon_path.as_posix()\n        )\n    )\n\nsetup(\n    name=\"OpenPype\",\n    version=__version__,\n    description=\"OpenPype\",\n    cmdclass={\"build_sphinx\": BuildDoc},\n    options={\n        \"build_exe\": build_exe_options,\n        \"bdist_mac\": bdist_mac_options,\n        \"build_sphinx\": {\n            \"project\": \"OpenPype\",\n            \"version\": __version__,\n            \"release\": __version__,\n            \"source_dir\": (openpype_root / \"docs\" / \"source\").as_posix(),\n            \"build_dir\": (openpype_root / \"docs\" / \"build\").as_posix()\n        }\n    },\n    executables=executables,\n    packages=[]\n)\n"
  },
  {
    "path": "start.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Main entry point for OpenPype command.\n\nBootstrapping process of OpenPype is as follows:\n\n`OPENPYPE_PATH` is checked for existence - either one from environment or\nfrom user settings. Precedence takes the one set by environment.\n\nOn this path we try to find OpenPype in directories version string in their\nnames. For example: `openpype-v3.0.1-foo` is valid name, or\neven `foo_3.0.2` - as long as version can be determined from its name\n_AND_ file `openpype/openpype/version.py` can be found inside, it is\nconsidered OpenPype installation.\n\nIf no OpenPype repositories are found in `OPENPYPE_PATH` (user data dir)\nthen **Igniter** (OpenPype setup tool) will launch its GUI.\n\nIt can be used to specify `OPENPYPE_PATH` or if it is _not_ specified, current\n*\"live\"* repositories will be used to create zip file and copy it to\nappdata dir in user home and extract it there. Version will be determined by\nversion specified in OpenPype module.\n\nIf OpenPype repository directories are found in default install location\n(user data dir) or in `OPENPYPE_PATH`, it will get list of those dirs\nthere and use latest one or the one specified with optional `--use-version`\ncommand line argument. If the one specified doesn't exist then latest\navailable version will be used. All repositories in that dir will be added\nto `sys.path` and `PYTHONPATH`.\n\nIf OpenPype is live (not frozen) then current version of OpenPype module\nwill be used. All directories under `repos` will be added to `sys.path` and\n`PYTHONPATH`.\n\nOpenPype depends on connection to `MongoDB`_. You can specify MongoDB\nconnection string via `OPENPYPE_MONGO` set in environment or it can be set\nin user settings or via **Igniter** GUI.\n\nSo, bootstrapping OpenPype looks like this::\n\n.. code-block:: bash\n\n┌───────────────────────────────────────────────────────┐\n│ Determine MongoDB connection:                         │\n│ Use `OPENPYPE_MONGO`, system keyring `openPypeMongo`  │\n└──────────────────────────┬────────────────────────────┘\n                  ┌───- Found? -─┐\n                 YES             NO\n                  │              │\n                  │       ┌──────┴──────────────┐\n                  │       │ Fire up Igniter GUI ├<-────────┐\n                  │       │ and ask User        │          │\n                  │       └─────────────────────┘          │\n                  │                                        │\n                  │                                        │\n┌─────────────────┴─────────────────────────────────────┐  │\n│ Get location of OpenPype:                             │  │\n│   1) Test for `OPENPYPE_PATH` environment variable    │  │\n│   2) Test `openPypePath` in registry setting          │  │\n│   3) Test user data directory                         │  │\n│ ····················································· │  │\n│ If running from frozen code:                          │  │\n│   - Use latest one found in user data dir             │  │\n│ If running from live code:                            │  │\n│   - Use live code and install it to user data dir     │  │\n│ * can be overridden with `--use-version` argument     │  │\n└──────────────────────────┬────────────────────────────┘  │\n              ┌─- Is OpenPype found? -─┐                   │\n             YES                       NO                  │\n              │                        │                   │\n              │      ┌─────────────────┴─────────────┐     │\n              │      │ Look in `OPENPYPE_PATH`, find │     │\n              │      │ latest version and install it │     │\n              │      │ to user data dir.             │     │\n              │      └──────────────┬────────────────┘     │\n              │         ┌─- Is OpenPype found? -─┐         │\n              │        YES                       NO -──────┘\n              │         │\n              ├<-───────┘\n              │\n┌─────────────┴────────────┐\n│      Run OpenPype        │\n└─────═══════════════──────┘\n\n\nTodo:\n    Move or remove bootstrapping environments out of the code.\n\nAttributes:\n    silent_commands (set): list of commands for which we won't print OpenPype\n        logo and info header.\n\n.. _MongoDB:\n   https://www.mongodb.com/\n\n\"\"\"\nimport os\nimport re\nimport sys\nimport platform\nimport traceback\nimport subprocess\nimport site\nimport distutils.spawn\nfrom pathlib import Path\n\n\nsilent_mode = False\n\n# OPENPYPE_ROOT is variable pointing to build (or code) directory\n# WARNING `OPENPYPE_ROOT` must be defined before igniter import\n# - igniter changes cwd which cause that filepath of this script won't lead\n#   to right directory\nif not getattr(sys, 'frozen', False):\n    # Code root defined by `start.py` directory\n    OPENPYPE_ROOT = os.path.dirname(os.path.abspath(__file__))\nelse:\n    OPENPYPE_ROOT = os.path.dirname(sys.executable)\n\n    # add dependencies folder to sys.pat for frozen code\n    frozen_libs = os.path.normpath(\n        os.path.join(OPENPYPE_ROOT, \"dependencies\")\n    )\n    sys.path.append(frozen_libs)\n    sys.path.insert(0, OPENPYPE_ROOT)\n    # add stuff from `<frozen>/dependencies` to PYTHONPATH.\n    pythonpath = os.getenv(\"PYTHONPATH\", \"\")\n    paths = pythonpath.split(os.pathsep)\n    paths.append(frozen_libs)\n    os.environ[\"PYTHONPATH\"] = os.pathsep.join(paths)\n\n# Vendored python modules that must not be in PYTHONPATH environment but\n#   are required for OpenPype processes\nvendor_python_path = os.path.join(OPENPYPE_ROOT, \"vendor\", \"python\")\nsys.path.insert(0, vendor_python_path)\n\n# Add common package to sys path\n# - common contains common code for bootstraping and OpenPype processes\nsys.path.insert(0, os.path.join(OPENPYPE_ROOT, \"common\"))\n\nimport blessed  # noqa: E402\nimport certifi  # noqa: E402\n\n\nif sys.__stdout__:\n    term = blessed.Terminal()\n\n    def _print(message: str, force=False):\n        if silent_mode and not force:\n            return\n        if message.startswith(\"!!! \"):\n            print(f'{term.orangered2(\"!!! \")}{message[4:]}')\n            return\n        if message.startswith(\">>> \"):\n            print(f'{term.aquamarine3(\">>> \")}{message[4:]}')\n            return\n        if message.startswith(\"--- \"):\n            print(f'{term.darkolivegreen3(\"--- \")}{message[4:]}')\n            return\n        if message.startswith(\"*** \"):\n            print(f'{term.gold(\"*** \")}{message[4:]}')\n            return\n        if message.startswith(\"  - \"):\n            print(f'{term.wheat(\"  - \")}{message[4:]}')\n            return\n        if message.startswith(\"  . \"):\n            print(f'{term.tan(\"  . \")}{message[4:]}')\n            return\n        if message.startswith(\"     - \"):\n            print(f'{term.seagreen3(\"     - \")}{message[7:]}')\n            return\n        if message.startswith(\"     ! \"):\n            print(f'{term.goldenrod(\"     ! \")}{message[7:]}')\n            return\n        if message.startswith(\"     * \"):\n            print(f'{term.aquamarine1(\"     * \")}{message[7:]}')\n            return\n        if message.startswith(\"    \"):\n            print(f'{term.darkseagreen3(\"    \")}{message[4:]}')\n            return\n\n        print(message)\nelse:\n    def _print(message: str):\n        if silent_mode:\n            return\n        print(message)\n\n\n# if SSL_CERT_FILE is not set prior to OpenPype launch, we set it to point\n# to certifi bundle to make sure we have reasonably new CA certificates.\nif os.getenv(\"SSL_CERT_FILE\") and \\\n        os.getenv(\"SSL_CERT_FILE\") != certifi.where():\n    _print(\"--- your system is set to use custom CA certificate bundle.\")\nelse:\n    ssl_cert_file = certifi.where()\n    os.environ[\"SSL_CERT_FILE\"] = ssl_cert_file\n\nif \"--headless\" in sys.argv:\n    os.environ[\"OPENPYPE_HEADLESS_MODE\"] = \"1\"\n    sys.argv.remove(\"--headless\")\nelif os.getenv(\"OPENPYPE_HEADLESS_MODE\") != \"1\":\n    os.environ.pop(\"OPENPYPE_HEADLESS_MODE\", None)\n\n# Set builtin ocio root\nos.environ[\"BUILTIN_OCIO_ROOT\"] = os.path.join(\n    OPENPYPE_ROOT,\n    \"vendor\",\n    \"bin\",\n    \"ocioconfig\",\n    \"OpenColorIOConfigs\"\n)\n\n# Enabled logging debug mode when \"--debug\" is passed\nif \"--verbose\" in sys.argv:\n    expected_values = (\n        \"Expected: notset, debug, info, warning, error, critical\"\n        \" or integer [0-50].\"\n    )\n    idx = sys.argv.index(\"--verbose\")\n    sys.argv.pop(idx)\n    if idx < len(sys.argv):\n        value = sys.argv.pop(idx)\n    else:\n        raise RuntimeError((\n            f\"Expect value after \\\"--verbose\\\" argument. {expected_values}\"\n        ))\n\n    log_level = None\n    low_value = value.lower()\n    if low_value.isdigit():\n        log_level = int(low_value)\n    elif low_value == \"notset\":\n        log_level = 0\n    elif low_value == \"debug\":\n        log_level = 10\n    elif low_value == \"info\":\n        log_level = 20\n    elif low_value == \"warning\":\n        log_level = 30\n    elif low_value == \"error\":\n        log_level = 40\n    elif low_value == \"critical\":\n        log_level = 50\n\n    if log_level is None:\n        raise RuntimeError((\n            \"Unexpected value after \\\"--verbose\\\" \"\n            f\"argument \\\"{value}\\\". {expected_values}\"\n        ))\n\n    os.environ[\"OPENPYPE_LOG_LEVEL\"] = str(log_level)\n\n# Enable debug mode, may affect log level if log level is not defined\nif \"--debug\" in sys.argv:\n    sys.argv.remove(\"--debug\")\n    os.environ[\"OPENPYPE_DEBUG\"] = \"1\"\n\nif \"--automatic-tests\" in sys.argv:\n    sys.argv.remove(\"--automatic-tests\")\n    os.environ[\"IS_TEST\"] = \"1\"\n\nif \"--use-staging\" in sys.argv:\n    sys.argv.remove(\"--use-staging\")\n    os.environ[\"OPENPYPE_USE_STAGING\"] = \"1\"\n\nimport igniter  # noqa: E402\nfrom igniter import BootstrapRepos  # noqa: E402\nfrom igniter.tools import (\n    get_openpype_global_settings,\n    get_openpype_path_from_settings,\n    get_local_openpype_path_from_settings,\n    validate_mongo_connection,\n    OpenPypeVersionNotFound,\n    OpenPypeVersionIncompatible\n)  # noqa\nfrom igniter.bootstrap_repos import OpenPypeVersion  # noqa: E402\n\nbootstrap = BootstrapRepos()\nsilent_commands = {\"run\", \"igniter\", \"standalonepublisher\",\n                   \"extractenvironments\", \"version\"}\n\n\ndef list_versions(openpype_versions: list, local_version=None) -> None:\n    \"\"\"Print list of detected versions.\"\"\"\n    _print(\"  - Detected versions:\")\n    for v in sorted(openpype_versions):\n        _print(f\"     - {v}: {v.path}\")\n    if not openpype_versions:\n        _print(\"     ! none in repository detected\")\n    if local_version:\n        _print(f\"     * local version {local_version}\")\n\n\ndef set_openpype_global_environments() -> None:\n    \"\"\"Set global OpenPype's environments.\"\"\"\n    import acre\n\n    from openpype.settings import get_general_environments\n\n    general_env = get_general_environments()\n\n    # first resolve general environment because merge doesn't expect\n    # values to be list.\n    # TODO: switch to OpenPype environment functions\n    merged_env = acre.merge(\n        acre.compute(acre.parse(general_env), cleanup=False),\n        dict(os.environ)\n    )\n    env = acre.compute(\n        merged_env,\n        cleanup=False\n    )\n    os.environ.clear()\n    os.environ.update(env)\n\n    # Hardcoded default values\n    os.environ[\"PYBLISH_GUI\"] = \"pyblish_pype\"\n    # Change scale factor only if is not set\n    if \"QT_AUTO_SCREEN_SCALE_FACTOR\" not in os.environ:\n        os.environ[\"QT_AUTO_SCREEN_SCALE_FACTOR\"] = \"1\"\n\n\ndef run(arguments: list, env: dict = None) -> int:\n    \"\"\"Use correct executable to run stuff.\n\n    This passing arguments to correct OpenPype executable. If OpenPype is run\n    from live sources, executable will be `python` in virtual environment.\n    If running from frozen code, executable will be `openpype_console` or\n    `openpype_gui`. Its equivalent in live code is `python start.py`.\n\n    Args:\n        arguments (list): Argument list to pass OpenPype.\n        env (dict, optional): Dictionary containing environment.\n\n    Returns:\n        int: Process return code.\n\n    \"\"\"\n    if getattr(sys, 'frozen', False):\n        interpreter = [sys.executable]\n    else:\n        interpreter = [sys.executable, __file__]\n\n    interpreter.extend(arguments)\n\n    p = subprocess.Popen(interpreter, env=env)\n    p.wait()\n    _print(f\">>> done [{p.returncode}]\")\n    return p.returncode\n\n\ndef run_disk_mapping_commands(settings):\n    \"\"\" Run disk mapping command\n\n        Used to map shared disk for OP to pull codebase.\n    \"\"\"\n\n    low_platform = platform.system().lower()\n    disk_mapping = settings.get(\"disk_mapping\")\n    if not disk_mapping:\n        return\n\n    mappings = disk_mapping.get(low_platform) or []\n    for source, destination in mappings:\n        if low_platform == \"windows\":\n            destination = destination.replace(\"/\", \"\\\\\").rstrip(\"\\\\\")\n            source = source.replace(\"/\", \"\\\\\").rstrip(\"\\\\\")\n            # Add slash after ':' ('G:' -> 'G:\\')\n            if source.endswith(\":\"):\n                source += \"\\\\\"\n        else:\n            destination = destination.rstrip(\"/\")\n            source = source.rstrip(\"/\")\n\n        if low_platform == \"darwin\":\n            scr = f'do shell script \"ln -s {source} {destination}\" with administrator privileges'  # noqa\n\n            args = [\"osascript\", \"-e\", scr]\n        elif low_platform == \"windows\":\n            args = [\"subst\", destination, source]\n        else:\n            args = [\"sudo\", \"ln\", \"-s\", source, destination]\n\n        _print(f\"*** disk mapping arguments: {args}\")\n        try:\n            if not os.path.exists(destination):\n                output = subprocess.Popen(args)\n                if output.returncode and output.returncode != 0:\n                    exc_msg = f'Executing was not successful: \"{args}\"'\n\n                    raise RuntimeError(exc_msg)\n        except TypeError as exc:\n            _print(\n                f\"Error {str(exc)} in mapping drive {source}, {destination}\")\n            raise\n\n\ndef set_avalon_environments():\n    \"\"\"Set avalon specific environments.\n\n    These are non-modifiable environments for avalon workflow that must be set\n    before avalon module is imported because avalon works with globals set with\n    environment variables.\n    \"\"\"\n\n    avalon_db = os.environ.get(\"AVALON_DB\") or \"avalon\"  # for tests\n    os.environ.update({\n        # Mongo DB name where avalon docs are stored\n        \"AVALON_DB\": avalon_db,\n        # Name of config\n        \"AVALON_LABEL\": \"OpenPype\"\n    })\n\n\ndef set_modules_environments():\n    \"\"\"Set global environments for OpenPype modules.\n\n    This requires to have OpenPype in `sys.path`.\n    \"\"\"\n\n    from openpype.modules import ModulesManager\n    import acre\n\n    modules_manager = ModulesManager()\n\n    module_envs = modules_manager.collect_global_environments()\n\n    # Merge environments with current environments and update values\n    if module_envs:\n        parsed_envs = acre.parse(module_envs)\n        env = acre.merge(parsed_envs, dict(os.environ))\n        os.environ.clear()\n        os.environ.update(env)\n\n\ndef _startup_validations():\n    \"\"\"Validations before OpenPype starts.\"\"\"\n    try:\n        _validate_thirdparty_binaries()\n    except Exception as exc:\n        if os.environ.get(\"OPENPYPE_HEADLESS_MODE\"):\n            raise\n\n        import tkinter\n        from tkinter.messagebox import showerror\n\n        root = tkinter.Tk()\n        root.attributes(\"-alpha\", 0.0)\n        root.wm_state(\"iconic\")\n        if platform.system().lower() != \"windows\":\n            root.withdraw()\n\n        showerror(\n            \"Startup validations didn't pass\",\n            str(exc)\n        )\n        root.withdraw()\n        sys.exit(1)\n\n\ndef _validate_thirdparty_binaries():\n    \"\"\"Check existence of thirdpart executables.\"\"\"\n    low_platform = platform.system().lower()\n    binary_vendors_dir = os.path.join(\n        os.environ[\"OPENPYPE_ROOT\"],\n        \"vendor\",\n        \"bin\"\n    )\n\n    error_msg = (\n        \"Missing binary dependency {}. Please fetch thirdparty dependencies.\"\n    )\n    # Validate existence of FFmpeg\n    ffmpeg_dir = os.path.join(binary_vendors_dir, \"ffmpeg\", low_platform)\n    if low_platform == \"windows\":\n        ffmpeg_dir = os.path.join(ffmpeg_dir, \"bin\")\n    ffmpeg_executable = os.path.join(ffmpeg_dir, \"ffmpeg\")\n    ffmpeg_result = distutils.spawn.find_executable(ffmpeg_executable)\n    if ffmpeg_result is None:\n        raise RuntimeError(error_msg.format(\"FFmpeg\"))\n\n    # Validate existence of OpenImageIO (not on MacOs)\n    oiio_tool_path = None\n    if low_platform == \"linux\":\n        oiio_tool_path = os.path.join(\n            binary_vendors_dir,\n            \"oiio\",\n            low_platform,\n            \"bin\",\n            \"oiiotool\"\n        )\n    elif low_platform == \"windows\":\n        oiio_tool_path = os.path.join(\n            binary_vendors_dir,\n            \"oiio\",\n            low_platform,\n            \"oiiotool\"\n        )\n    oiio_result = None\n    if oiio_tool_path is not None:\n        oiio_result = distutils.spawn.find_executable(oiio_tool_path)\n        if oiio_result is None:\n            raise RuntimeError(error_msg.format(\"OpenImageIO\"))\n\n\ndef _process_arguments() -> tuple:\n    \"\"\"Process command line arguments.\n\n    Returns:\n        tuple: Return tuple with specific version to use (if any) and flag\n            to prioritize staging (if set)\n    \"\"\"\n    # check for `--use-version=3.0.0` argument and `--use-staging`\n    use_version = None\n    commands = []\n\n    # OpenPype version specification through arguments\n    use_version_arg = \"--use-version\"\n\n    for arg in sys.argv:\n        if arg.startswith(use_version_arg):\n            # Remove arg from sys argv\n            sys.argv.remove(arg)\n            # Extract string after use version arg\n            use_version_value = arg[len(use_version_arg):]\n\n            if (\n                not use_version_value\n                or not use_version_value.startswith(\"=\")\n            ):\n                _print(\"!!! Please use option --use-version like:\", True)\n                _print(\"    --use-version=3.0.0\", True)\n                sys.exit(1)\n\n            version_str = use_version_value[1:]\n            use_version = None\n            if version_str.lower() == \"latest\":\n                use_version = \"latest\"\n            else:\n                m = re.search(\n                    r\"(?P<version>\\d+\\.\\d+\\.\\d+(?:\\S*)?)\", version_str\n                )\n                if m and m.group('version'):\n                    use_version = m.group('version')\n                    _print(f\">>> Requested version [ {use_version} ]\")\n                    break\n\n            if use_version is None:\n                _print(\"!!! Requested version isn't in correct format.\", True)\n                _print((\"    Use --list-versions to find out\"\n                       \" proper version string.\"), True)\n                sys.exit(1)\n\n        if arg == \"--validate-version\":\n            _print(\"!!! Please use option --validate-version like:\", True)\n            _print(\"    --validate-version=3.0.0\", True)\n            sys.exit(1)\n\n        if arg.startswith(\"--validate-version=\"):\n            m = re.search(\n                r\"--validate-version=(?P<version>\\d+\\.\\d+\\.\\d+(?:\\S*)?)\", arg)\n            if m and m.group('version'):\n                use_version = m.group('version')\n                sys.argv.remove(arg)\n                commands.append(\"validate\")\n            else:\n                _print(\"!!! Requested version isn't in correct format.\", True)\n                _print((\"    Use --list-versions to find out\"\n                        \" proper version string.\"), True)\n                sys.exit(1)\n\n    if \"--list-versions\" in sys.argv:\n        commands.append(\"print_versions\")\n        sys.argv.remove(\"--list-versions\")\n\n    # handle igniter\n    # this is helper to run igniter before anything else\n    if \"igniter\" in sys.argv:\n        if os.getenv(\"OPENPYPE_HEADLESS_MODE\") == \"1\":\n            _print(\"!!! Cannot open Igniter dialog in headless mode.\", True)\n            sys.exit(1)\n\n        return_code = igniter.open_dialog()\n\n        # this is when we want to run OpenPype without installing anything.\n        # or we are ready to run.\n        if return_code not in [2, 3]:\n            sys.exit(return_code)\n\n        idx = sys.argv.index(\"igniter\")\n        sys.argv.pop(idx)\n        sys.argv.insert(idx, \"tray\")\n\n    return use_version, commands\n\n\ndef _determine_mongodb() -> str:\n    \"\"\"Determine mongodb connection string.\n\n    First use ``OPENPYPE_MONGO`` environment variable, then system keyring.\n    Then try to run **Igniter UI** to let user specify it.\n\n    Returns:\n        str: mongodb connection URL\n\n    Raises:\n        RuntimeError: if mongodb connection url cannot by determined.\n\n    \"\"\"\n\n    openpype_mongo = os.getenv(\"OPENPYPE_MONGO\", None)\n    if not openpype_mongo:\n        # try system keyring\n        try:\n            openpype_mongo = bootstrap.secure_registry.get_item(\n                \"openPypeMongo\"\n            )\n        except ValueError:\n            pass\n\n    if openpype_mongo:\n        result, msg = validate_mongo_connection(openpype_mongo)\n        if not result:\n            _print(msg)\n            openpype_mongo = None\n\n    if not openpype_mongo:\n        _print(\"*** No DB connection string specified.\")\n        if os.getenv(\"OPENPYPE_HEADLESS_MODE\") == \"1\":\n            _print(\"!!! Cannot open Igniter dialog in headless mode.\", True)\n            _print((\"!!! Please use `OPENPYPE_MONGO` to specify \"\n                    \"server address.\"), True)\n            sys.exit(1)\n        _print(\"--- launching setup UI ...\")\n\n        result = igniter.open_dialog()\n        if result == 0:\n            raise RuntimeError(\"MongoDB URL was not defined\")\n\n        openpype_mongo = os.getenv(\"OPENPYPE_MONGO\")\n        if not openpype_mongo:\n            try:\n                openpype_mongo = bootstrap.secure_registry.get_item(\n                    \"openPypeMongo\")\n            except ValueError as e:\n                raise RuntimeError(\"Missing MongoDB url\") from e\n\n    return openpype_mongo\n\n\ndef _initialize_environment(openpype_version: OpenPypeVersion) -> None:\n    version_path = openpype_version.path\n    if not version_path:\n        _print(f\"!!! Version {openpype_version} doesn't have path set.\")\n        raise ValueError(\"No path set in specified OpenPype version.\")\n    os.environ[\"OPENPYPE_VERSION\"] = str(openpype_version)\n    # set OPENPYPE_REPOS_ROOT to point to currently used OpenPype version.\n    os.environ[\"OPENPYPE_REPOS_ROOT\"] = os.path.normpath(\n        version_path.as_posix()\n    )\n    # inject version to Python environment (sys.path, ...)\n    _print(\">>> Injecting OpenPype version to running environment  ...\")\n    bootstrap.add_paths_from_directory(version_path)\n\n    # Additional sys paths related to OPENPYPE_REPOS_ROOT directory\n    # TODO move additional paths to `boot` part when OPENPYPE_REPOS_ROOT will\n    # point to same hierarchy from code and from frozen OpenPype\n    additional_paths = [\n        os.environ[\"OPENPYPE_REPOS_ROOT\"],\n        # add OpenPype tools\n        os.path.join(os.environ[\"OPENPYPE_REPOS_ROOT\"], \"openpype\", \"tools\"),\n        # add common OpenPype vendor\n        # (common for multiple Python interpreter versions)\n        os.path.join(\n            os.environ[\"OPENPYPE_REPOS_ROOT\"],\n            \"openpype\",\n            \"vendor\",\n            \"python\",\n            \"common\"\n        )\n    ]\n\n    split_paths = os.getenv(\"PYTHONPATH\", \"\").split(os.pathsep)\n    for path in additional_paths:\n        split_paths.insert(0, path)\n        sys.path.insert(0, path)\n\n    os.environ[\"PYTHONPATH\"] = os.pathsep.join(split_paths)\n\n\ndef _find_frozen_openpype(use_version: str = None,\n                          use_staging: bool = False) -> Path:\n    \"\"\"Find OpenPype to run from frozen code.\n\n    This will process and modify environment variables:\n    ``PYTHONPATH``, ``OPENPYPE_VERSION``, ``OPENPYPE_REPOS_ROOT``\n\n    Args:\n        use_version (str, optional): Try to use specified version.\n        use_staging (bool, optional): Prefer *staging* flavor over production.\n\n    Returns:\n        Path: Path to version to be used.\n\n    Raises:\n        RuntimeError: If no OpenPype version are found.\n\n    \"\"\"\n    # Collect OpenPype versions\n    installed_version = OpenPypeVersion.get_installed_version()\n    # Expected version that should be used by studio settings\n    #   - this option is used only if version is not explicitly set and if\n    #       studio has set explicit version in settings\n    studio_version = OpenPypeVersion.get_expected_studio_version(use_staging)\n\n    if use_version is not None:\n        # Specific version is defined\n        if use_version.lower() == \"latest\":\n            # Version says to use latest version\n            _print(\">>> Finding latest version defined by use version\")\n            openpype_version = bootstrap.find_latest_openpype_version()\n        else:\n            _print(f\">>> Finding specified version \\\"{use_version}\\\"\")\n            openpype_version = bootstrap.find_openpype_version(use_version)\n\n        if openpype_version is None:\n            raise OpenPypeVersionNotFound(\n                f\"Requested version \\\"{use_version}\\\" was not found.\"\n            )\n\n    elif studio_version is not None:\n        # Studio has defined a version to use\n        _print(f\">>> Finding studio version \\\"{studio_version}\\\"\")\n        openpype_version = bootstrap.find_openpype_version(studio_version)\n        if openpype_version is None:\n            raise OpenPypeVersionNotFound((\n                \"Requested OpenPype version \"\n                f\"\\\"{studio_version}\\\" defined by settings\"\n                \" was not found.\"\n            ))\n\n    else:\n        # Default behavior to use latest version\n        _print((\n            \">>> Finding latest version \"\n            f\"with [ {installed_version} ]\"))\n        openpype_version = bootstrap.find_latest_openpype_version()\n\n        if openpype_version is None:\n            raise OpenPypeVersionNotFound(\"Didn't find any versions.\")\n\n    # get local frozen version and add it to detected version so if it is\n    # newer it will be used instead.\n    if installed_version == openpype_version:\n        version_path = _bootstrap_from_code(use_version)\n        openpype_version = OpenPypeVersion(\n            version=BootstrapRepos.get_version(version_path),\n            path=version_path)\n        _initialize_environment(openpype_version)\n        return version_path\n\n    in_headless_mode = os.getenv(\"OPENPYPE_HEADLESS_MODE\") == \"1\"\n    if not installed_version.is_compatible(openpype_version):\n        message = \"Version {} is not compatible with installed version {}.\"\n        # Show UI to user\n        if not in_headless_mode:\n            igniter.show_message_dialog(\n                \"Incompatible OpenPype installation\",\n                message.format(\n                    \"<b>{}</b>\".format(openpype_version),\n                    \"<b>{}</b>\".format(installed_version)\n                )\n            )\n        # Raise incompatible error\n        raise OpenPypeVersionIncompatible(\n            message.format(openpype_version, installed_version)\n        )\n\n    # test if latest detected is installed (in user data dir)\n    is_inside = False\n    try:\n        is_inside = openpype_version.path.resolve().relative_to(\n            bootstrap.data_dir)\n    except ValueError:\n        # if relative path cannot be calculated, openpype version is not\n        # inside user data dir\n        pass\n\n    if not is_inside:\n        # install latest version to user data dir\n        if in_headless_mode:\n            version_path = bootstrap.install_version(\n                openpype_version, force=True\n            )\n        else:\n            version_path = igniter.open_update_window(openpype_version)\n\n        openpype_version.path = version_path\n        _initialize_environment(openpype_version)\n        return openpype_version.path\n\n    if openpype_version.path.is_file():\n        _print(\">>> Extracting zip file ...\")\n        try:\n            version_path = bootstrap.extract_openpype(openpype_version)\n        except OSError as e:\n            _print(\"!!! failed: {}\".format(str(e)), True)\n            sys.exit(1)\n        else:\n            # cleanup zip after extraction\n            os.unlink(openpype_version.path)\n\n        openpype_version.path = version_path\n\n    _initialize_environment(openpype_version)\n    return openpype_version.path\n\n\ndef _bootstrap_from_code(use_version):\n    \"\"\"Bootstrap live code (or the one coming with frozen OpenPype).\n\n    Args:\n        use_version: (str): specific version to use.\n\n    Returns:\n        Path: path to sourced version.\n\n    \"\"\"\n    # run through repos and add them to `sys.path` and `PYTHONPATH`\n    # set root\n    _openpype_root = OPENPYPE_ROOT\n    # Unset use version if latest should be used\n    #   - when executed from code then code is expected as latest\n    #   - when executed from build then build is already marked as latest\n    #       in '_find_frozen_openpype'\n    if use_version and use_version.lower() == \"latest\":\n        use_version = None\n\n    if getattr(sys, 'frozen', False):\n        local_version = bootstrap.get_version(Path(_openpype_root))\n        switch_str = f\" - will switch to {use_version}\" if use_version and use_version != local_version else \"\"  # noqa\n        _print(f\"  - booting version: {local_version}{switch_str}\")\n        if not local_version:\n            raise OpenPypeVersionNotFound(\n                f\"Cannot find version at {_openpype_root}\")\n    else:\n        # get current version of OpenPype\n        local_version = OpenPypeVersion.get_installed_version_str()\n\n    # All cases when should be used different version than build\n    if use_version and use_version != local_version:\n        if use_version:\n            # Explicit version should be used\n            version_to_use = bootstrap.find_openpype_version(use_version)\n            if version_to_use is None:\n                raise OpenPypeVersionIncompatible(\n                    f\"Requested version \\\"{use_version}\\\" was not found.\")\n        else:\n            version_to_use = bootstrap.find_latest_openpype_version()\n            if version_to_use is None:\n                raise OpenPypeVersionNotFound(\"Didn't find any versions.\")\n\n        # Start extraction of version if needed\n        if version_to_use.path.is_file():\n            version_to_use.path = bootstrap.extract_openpype(version_to_use)\n        bootstrap.add_paths_from_directory(version_to_use.path)\n        os.environ[\"OPENPYPE_VERSION\"] = use_version\n        version_path = version_to_use.path\n        os.environ[\"OPENPYPE_REPOS_ROOT\"] = (\n            version_path / \"openpype\"\n        ).as_posix()\n        _openpype_root = version_to_use.path.as_posix()\n\n    else:\n        os.environ[\"OPENPYPE_VERSION\"] = local_version\n        version_path = Path(_openpype_root)\n        os.environ[\"OPENPYPE_REPOS_ROOT\"] = _openpype_root\n\n    # add self to sys.path of current process\n    # NOTE: this seems to be duplicate of 'add_paths_from_directory'\n    sys.path.insert(0, _openpype_root)\n    # add venv 'site-packages' to PYTHONPATH\n    python_path = os.getenv(\"PYTHONPATH\", \"\")\n    split_paths = python_path.split(os.pathsep)\n    # add self to python paths\n    split_paths.insert(0, _openpype_root)\n\n    # last one should be venv site-packages\n    # this is slightly convoluted as we can get here from frozen code too\n    # in case when we are running without any version installed.\n    if not getattr(sys, 'frozen', False):\n        split_paths.append(site.getsitepackages()[-1])\n        # TODO move additional paths to `boot` part when OPENPYPE_ROOT will\n        # point to same hierarchy from code and from frozen OpenPype\n        additional_paths = [\n            # add OpenPype tools\n            os.path.join(_openpype_root, \"openpype\", \"tools\"),\n            # add common OpenPype vendor\n            # (common for multiple Python interpreter versions)\n            os.path.join(\n                _openpype_root,\n                \"openpype\",\n                \"vendor\",\n                \"python\",\n                \"common\"\n            )\n        ]\n        for path in additional_paths:\n            split_paths.insert(0, path)\n            sys.path.insert(0, path)\n\n    os.environ[\"PYTHONPATH\"] = os.pathsep.join(split_paths)\n\n    return version_path\n\n\ndef _boot_validate_versions(use_version, local_version):\n    _print(f\">>> Validating version [ {use_version} ]\")\n    openpype_versions = bootstrap.find_openpype(include_zips=True)\n    v: OpenPypeVersion\n    found = [v for v in openpype_versions if str(v) == use_version]\n    if not found:\n        _print(f\"!!! Version [ {use_version} ] not found.\", True)\n        list_versions(openpype_versions, local_version)\n        sys.exit(1)\n\n    # print result\n    version_path = bootstrap.get_version_path_from_list(\n        use_version, openpype_versions\n    )\n    valid, message = bootstrap.validate_openpype_version(version_path)\n    _print(f'{\">>> \" if valid else \"!!! \"}{message}', not valid)\n    return valid\n\n\ndef _boot_print_versions(openpype_root):\n    if getattr(sys, 'frozen', False):\n        local_version = bootstrap.get_version(Path(openpype_root))\n    else:\n        local_version = OpenPypeVersion.get_installed_version_str()\n\n    compatible_with = OpenPypeVersion(version=local_version)\n    if \"--all\" in sys.argv:\n        _print(\"--- Showing all version (even those not compatible).\")\n    else:\n        _print((\"--- Showing only compatible versions \"\n                f\"with [ {compatible_with.major}.{compatible_with.minor} ]\"))\n\n    openpype_versions = bootstrap.find_openpype(include_zips=True)\n    openpype_versions = [\n        version for version in openpype_versions\n        if version.is_compatible(\n            OpenPypeVersion.get_installed_version())\n    ]\n\n    list_versions(openpype_versions, local_version)\n\n\ndef _boot_handle_missing_version(local_version, message):\n    _print(message, True)\n    if os.environ.get(\"OPENPYPE_HEADLESS_MODE\") == \"1\":\n        openpype_versions = bootstrap.find_openpype(\n            include_zips=True)\n        list_versions(openpype_versions, local_version)\n    else:\n        igniter.show_message_dialog(\"Version not found\", message)\n\n\ndef boot():\n    \"\"\"Bootstrap OpenPype.\"\"\"\n    global silent_mode\n    if any(arg in silent_commands for arg in sys.argv):\n        silent_mode = True\n\n    # ------------------------------------------------------------------------\n    # Set environment to OpenPype root path\n    # ------------------------------------------------------------------------\n    os.environ[\"OPENPYPE_ROOT\"] = OPENPYPE_ROOT\n\n    # ------------------------------------------------------------------------\n    # Do necessary startup validations\n    # ------------------------------------------------------------------------\n    _startup_validations()\n\n    # ------------------------------------------------------------------------\n    # Process arguments\n    # ------------------------------------------------------------------------\n\n    use_version, commands = _process_arguments()\n    use_staging = os.environ.get(\"OPENPYPE_USE_STAGING\") == \"1\"\n\n    if os.getenv(\"OPENPYPE_VERSION\"):\n        if use_version:\n            _print((\"*** environment variable OPENPYPE_VERSION\"\n                    \"is overridden by command line argument.\"))\n        else:\n            _print(\">>> version set by environment variable\")\n            use_version = os.getenv(\"OPENPYPE_VERSION\")\n\n    # ------------------------------------------------------------------------\n    # Determine mongodb connection\n    # ------------------------------------------------------------------------\n\n    try:\n        openpype_mongo = _determine_mongodb()\n    except RuntimeError as e:\n        # without mongodb url we are done for.\n        _print(f\"!!! {e}\", True)\n        sys.exit(1)\n\n    os.environ[\"OPENPYPE_MONGO\"] = openpype_mongo\n    # name of Pype database\n    os.environ[\"OPENPYPE_DATABASE_NAME\"] = \\\n        os.environ.get(\"OPENPYPE_DATABASE_NAME\") or \"openpype\"\n\n    if os.environ.get(\"IS_TEST\") == \"1\":\n        # change source DBs to predefined ones set for automatic testing\n        if \"_tests\" not in os.environ[\"OPENPYPE_DATABASE_NAME\"]:\n            os.environ[\"OPENPYPE_DATABASE_NAME\"] += \"_tests\"\n        avalon_db = os.environ.get(\"AVALON_DB\") or \"avalon\"\n        if \"_tests\" not in avalon_db:\n            os.environ[\"AVALON_DB\"] = avalon_db + \"_tests\"\n\n    global_settings = get_openpype_global_settings(openpype_mongo)\n\n    _print(\">>> run disk mapping command ...\")\n    run_disk_mapping_commands(global_settings)\n\n    # Logging to server enabled/disabled\n    log_to_server = global_settings.get(\"log_to_server\", True)\n    if log_to_server:\n        os.environ[\"OPENPYPE_LOG_TO_SERVER\"] = \"1\"\n        log_to_server_msg = \"ON\"\n    else:\n        os.environ.pop(\"OPENPYPE_LOG_TO_SERVER\", None)\n        log_to_server_msg = \"OFF\"\n    _print(f\">>> Logging to server is turned {log_to_server_msg}\")\n\n    # Get openpype path from database and set it to environment so openpype can\n    # find its versions there and bootstrap them.\n    openpype_path = get_openpype_path_from_settings(global_settings)\n\n    # Check if local versions should be installed in custom folder and not in\n    # user app data\n    data_dir = get_local_openpype_path_from_settings(global_settings)\n    bootstrap.set_data_dir(data_dir)\n    if getattr(sys, 'frozen', False):\n        local_version = bootstrap.get_version(Path(OPENPYPE_ROOT))\n    else:\n        local_version = OpenPypeVersion.get_installed_version_str()\n\n    if \"validate\" in commands:\n        valid = _boot_validate_versions(use_version, local_version)\n        sys.exit(0 if valid else 1)\n\n    if not openpype_path:\n        _print(\"*** Cannot get OpenPype path from database.\")\n\n    if not os.getenv(\"OPENPYPE_PATH\") and openpype_path:\n        os.environ[\"OPENPYPE_PATH\"] = openpype_path\n\n    if \"print_versions\" in commands:\n        _boot_print_versions(OPENPYPE_ROOT)\n        sys.exit(0)\n\n    # ------------------------------------------------------------------------\n    # Find OpenPype versions\n    # ------------------------------------------------------------------------\n    # WARNING: Environment OPENPYPE_REPOS_ROOT may change if frozen OpenPype\n    # is executed\n    if getattr(sys, 'frozen', False):\n        # find versions of OpenPype to be used with frozen code\n        try:\n            version_path = _find_frozen_openpype(use_version, use_staging)\n        except OpenPypeVersionNotFound as exc:\n            _boot_handle_missing_version(local_version, str(exc))\n            sys.exit(1)\n\n        except RuntimeError as e:\n            # no version to run\n            _print(f\"!!! {e}\", True)\n            sys.exit(1)\n        # validate version\n        _print(f\">>> Validating version in frozen [ {str(version_path)} ]\")\n        result = bootstrap.validate_openpype_version(version_path)\n        if not result[0]:\n            _print(f\"!!! Invalid version: {result[1]}\", True)\n            sys.exit(1)\n        _print(\"--- version is valid\")\n    else:\n        try:\n            version_path = _bootstrap_from_code(use_version)\n\n        except OpenPypeVersionNotFound as exc:\n            _boot_handle_missing_version(local_version, str(exc))\n            sys.exit(1)\n\n    # set this to point either to `python` from venv in case of live code\n    # or to `openpype` or `openpype_console` in case of frozen code\n    os.environ[\"OPENPYPE_EXECUTABLE\"] = sys.executable\n\n    # delete OpenPype module and it's submodules from cache so it is used from\n    # specific version\n    modules_to_del = [\n        sys.modules.pop(module_name)\n        for module_name in tuple(sys.modules)\n        if module_name == \"openpype\" or module_name.startswith(\"openpype.\")\n    ]\n\n    try:\n        for module_name in modules_to_del:\n            del sys.modules[module_name]\n    except AttributeError:\n        pass\n    except KeyError:\n        pass\n\n    _print(\">>> loading environments ...\")\n    # Avalon environments must be set before avalon module is imported\n    _print(\"  - for Avalon ...\")\n    set_avalon_environments()\n    _print(\"  - global OpenPype ...\")\n    set_openpype_global_environments()\n    _print(\"  - for modules ...\")\n    set_modules_environments()\n\n    assert version_path, \"Version path not defined.\"\n\n    # print info when not running scripts defined in 'silent commands'\n    if all(arg not in silent_commands for arg in sys.argv):\n        from openpype.lib import terminal as t\n        from openpype.version import __version__\n\n        info = get_info(use_staging)\n        info.insert(0, f\">>> Using OpenPype from [ {version_path} ]\")\n\n        t_width = 20\n        try:\n            t_width = os.get_terminal_size().columns - 2\n        except (ValueError, OSError):\n            # running without terminal\n            pass\n\n        _header = f\"*** OpenPype [{__version__}] \"\n        info.insert(0, _header + \"-\" * (t_width - len(_header)))\n\n        for i in info:\n            t.echo(i)\n\n    from openpype import cli\n    try:\n        cli.main(obj={}, prog_name=\"openpype\")\n    except Exception:  # noqa\n        exc_info = sys.exc_info()\n        _print(\"!!! OpenPype crashed:\", True)\n        traceback.print_exception(*exc_info)\n        sys.exit(1)\n\n\ndef get_info(use_staging=None) -> list:\n    \"\"\"Print additional information to console.\"\"\"\n    from openpype.client.mongo import get_default_components\n    try:\n        from openpype.lib.log import Logger\n    except ImportError:\n        # Backwards compatibility for 'PypeLogger'\n        from openpype.lib.log import PypeLogger as Logger\n\n    components = get_default_components()\n\n    inf = []\n    if use_staging:\n        inf.append((\"OpenPype variant\", \"staging\"))\n    else:\n        inf.append((\"OpenPype variant\", \"production\"))\n    inf.extend([\n        (\"Running OpenPype from\", os.environ.get('OPENPYPE_REPOS_ROOT')),\n        (\"Using mongodb\", components[\"host\"])]\n    )\n\n    if os.environ.get(\"FTRACK_SERVER\"):\n        inf.append((\"Using FTrack at\",\n                    os.environ.get(\"FTRACK_SERVER\")))\n\n    if os.environ.get('DEADLINE_REST_URL'):\n        inf.append((\"Using Deadline webservice at\",\n                    os.environ.get(\"DEADLINE_REST_URL\")))\n\n    # Reinitialize\n    Logger.initialize()\n\n    mongo_components = get_default_components()\n    if mongo_components[\"host\"]:\n        inf.extend([\n            (\"Logging to MongoDB\", mongo_components[\"host\"]),\n            (\"  - port\", mongo_components[\"port\"] or \"<N/A>\"),\n            (\"  - database\", Logger.log_database_name),\n            (\"  - collection\", Logger.log_collection_name),\n            (\"  - user\", mongo_components[\"username\"] or \"<N/A>\")\n        ])\n        if mongo_components[\"auth_db\"]:\n            inf.append((\"  - auth source\", mongo_components[\"auth_db\"]))\n\n    maximum = max(len(i[0]) for i in inf)\n    formatted = []\n    for info in inf:\n        padding = (maximum - len(info[0])) + 1\n        formatted.append(f'... {info[0]}:{\" \" * padding}[ {info[1]} ]')\n    return formatted\n\n\nif __name__ == \"__main__\":\n    boot()\n"
  },
  {
    "path": "tests/README.md",
    "content": "Automatic tests for OpenPype\n============================\n\nRequirements:\n============\nTests are recreating fresh DB for each run, so `mongorestore`, `mongodump` and `mongoimport` command line tools must be installed and on Path.\n\nYou can find intallers here: https://www.mongodb.com/docs/database-tools/installation/installation/\n\nYou can test that `mongorestore` is available by running this in console, or cmd:\n```mongorestore --version```\n\nStructure:\n- integration - end to end tests, slow (see README.md in the integration folder for more info)\n    - openpype/modules/MODULE_NAME - structure follow directory structure in code base\n        - fixture - sample data `(MongoDB dumps, test files etc.)`\n        - `tests.py` - single or more pytest files for MODULE_NAME\n- unit - quick unit test\n    - MODULE_NAME\n        - fixture\n        - `tests.py`\n\nHow to run:\n----------\n- use Openpype command 'runtests' from command line (`.venv` in ${OPENPYPE_ROOT} must be activated to use configured Python!)\n-- `python ${OPENPYPE_ROOT}/start.py runtests`\n\nBy default, this command will run all tests in ${OPENPYPE_ROOT}/tests.\n\nSpecific location could be provided to this command as an argument, either as absolute path, or relative path to ${OPENPYPE_ROOT}.\n(eg. `python ${OPENPYPE_ROOT}/start.py start.py runtests ../tests/integration`) will trigger only tests in `integration` folder.\n\nSee `${OPENPYPE_ROOT}/cli.py:runtests` for other arguments.\n\nRun in IDE:\n-----------\nIf you prefer to run/debug single file directly in IDE of your choice, you might encounter issues with imports.\nIt would manifest like `KeyError: 'OPENPYPE_DATABASE_NAME'`. That means you are importing module that depends on OP to be running, eg. all expected variables are set.\n\nIn some cases your tests might be so localized, that you don't care about all env vars to be set properly.\nIn that case you might add this dummy configuration BEFORE any imports in your test file\n```\nimport os\nos.environ[\"OPENPYPE_DEBUG\"] = \"1\"\nos.environ[\"OPENPYPE_MONGO\"] = \"mongodb://localhost:27017\"\nos.environ[\"OPENPYPE_DATABASE_NAME\"] = \"openpype\"\nos.environ[\"AVALON_DB\"] = \"avalon\"\nos.environ[\"AVALON_TIMEOUT\"] = \"3000\"\nos.environ[\"AVALON_ASSET\"] = \"Asset\"\nos.environ[\"AVALON_PROJECT\"] = \"test_project\"\n```\n(AVALON_ASSET and AVALON_PROJECT values should exist in your environment)\n\nThis might be enough to run your test file separately. Do not commit this skeleton though.\nUse only when you know what you are doing!\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/conftest.py",
    "content": "# -*- coding: utf-8 -*-\n# adds command line arguments for 'runtests' as a fixtures\nimport pytest\n\n\ndef pytest_addoption(parser):\n    parser.addoption(\n        \"--test_data_folder\", action=\"store\", default=None,\n        help=\"Provide url of a folder of unzipped test file\"\n    )\n\n    parser.addoption(\n        \"--persist\", action=\"store\", default=None,\n        help=\"True - keep test_db, test_openpype, outputted test files\"\n    )\n\n    parser.addoption(\n        \"--app_group\", action=\"store\", default=None,\n        help=\"Keep empty to use default application or explicit\"\n    )\n\n    parser.addoption(\n        \"--app_variant\", action=\"store\", default=None,\n        help=\"Keep empty to locate latest installed variant or explicit\"\n    )\n\n    parser.addoption(\n        \"--timeout\", action=\"store\", default=None,\n        help=\"Overwrite default timeout\"\n    )\n\n    parser.addoption(\n        \"--setup_only\", action=\"store\", default=None,\n        help=\"True - only setup test, do not run any tests\"\n    )\n\n    parser.addoption(\n        \"--mongo_url\", action=\"store\", default=None,\n        help=\"Provide url of the Mongo database.\"\n    )\n\n    parser.addoption(\n        \"--dump_databases\", action=\"store\", default=None,\n        help=\"Dump databases to data folder.\"\n    )\n\n\n@pytest.fixture(scope=\"module\")\ndef test_data_folder(request):\n    return request.config.getoption(\"--test_data_folder\")\n\n\n@pytest.fixture(scope=\"module\")\ndef persist(request):\n    return request.config.getoption(\"--persist\")\n\n\n@pytest.fixture(scope=\"module\")\ndef app_group(request):\n    return request.config.getoption(\"--app_group\")\n\n\n@pytest.fixture(scope=\"module\")\ndef app_variant(request):\n    return request.config.getoption(\"--app_variant\")\n\n\n@pytest.fixture(scope=\"module\")\ndef timeout(request):\n    return request.config.getoption(\"--timeout\")\n\n\n@pytest.fixture(scope=\"module\")\ndef setup_only(request):\n    return request.config.getoption(\"--setup_only\")\n\n\n@pytest.fixture(scope=\"module\")\ndef mongo_url(request):\n    return request.config.getoption(\"--mongo_url\")\n\n\n@pytest.fixture(scope=\"module\")\ndef dump_databases(request):\n    return request.config.getoption(\"--dump_databases\")\n\n\n@pytest.hookimpl(tryfirst=True, hookwrapper=True)\ndef pytest_runtest_makereport(item, call):\n    # execute all other hooks to obtain the report object\n    outcome = yield\n    rep = outcome.get_result()\n\n    # set a report attribute for each phase of a call, which can\n    # be \"setup\", \"call\", \"teardown\"\n\n    setattr(item, \"rep_\" + rep.when, rep)\n\n    # In the event of module scoped fixtures, also mark failure in module.\n    module = item\n    while module is not None and not isinstance(module, pytest.Module):\n        module = module.parent\n    if module is not None:\n        if rep.when == 'call' and (rep.failed or rep.skipped):\n            module.module_test_failure = True\n"
  },
  {
    "path": "tests/integration/README.md",
    "content": "Integration test for OpenPype\n=============================\nContains end-to-end tests for automatic testing of OP.\n\nShould run headless publish on all hosts to check basic publish use cases automatically\nto limit regression issues.\n\nUses env var `HEADLESS_PUBLISH` (set in test data zip files) to differentiate between regular publish\nand \"automated\" one.\n\nHow to run\n----------\n- activate `{OPENPYPE_ROOT}/.venv`\n- run in cmd\n`{OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests {OPENPYPE_ROOT}/tests/integration`\n  - add `hosts/APP_NAME` after integration part to limit only on specific app (eg. `{OPENPYPE_ROOT}/tests/integration/hosts/maya`)\n\nOR can use built executables\n`openpype_console runtests {ABS_PATH}/tests/integration`\n\nCommand line arguments\n----------------------\n - \"--mark\" - \"Run tests marked by\",\n - \"--pyargs\" - \"Run tests from package\",\n - \"--test_data_folder\" - \"Unzipped directory path of test file\",\n - \"--persist\" - \"Persist test DB and published files after test end\",\n - \"--app_variant\" - \"Provide specific app variant for test, empty for latest\",\n - \"--app_group\" - \"Provide specific app group for test, empty for default\",\n - \"--timeout\" - \"Provide specific timeout value for test case\",\n - \"--setup_only\" - \"Only create dbs, do not run tests\",\n - \"--mongo_url\" - \"MongoDB for testing.\",\n - \"--dump_databases\" - (\"json\"|\"bson\") export database in expected format after successful test (to output folder in temp location - which is made persistent by this, must be cleared manually)\nRun Tray for test\n-----------------\nIn case of failed test you might want to run it manually and visually debug what happened.\nFor that:\n- run tests that is failing\n- add environment variables (to command line process or your IDE)\n  - OPENPYPE_DATABASE_NAME = openpype_tests\n  - AVALON_DB = avalon_tests\n- run tray as usual\n  - `{OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py run tray --debug`\n\nYou should see only test asset and state of databases for that particular use case.\n\nHow to check logs/errors from app\n--------------------------------\nKeep PERSIST to True in the class and check `test_openpype.logs` collection.\n\nHow to create test for publishing from host\n------------------------------------------\n- Extend PublishTest in `tests/lib/testing_classes.py`\n- Use `resources\\test_data.zip` skeleton file as a template for testing input data\n- Create subfolder `test_data` with matching name to your test file containing you test class\n  - (see `tests/integration/hosts/maya/test_publish_in_maya` and `test_publish_in_maya.py`)\n- Put this subfolder name into TEST_FILES [(HASH_ID, FILE_NAME, MD5_OPTIONAL)]\n  - at first position, all others may be \"\"\n- Put workfile into `test_data/input/workfile`\n- If you require other than base DB dumps provide them to `test_data/input/dumps`\n-- (Check commented code in `db_handler.py` how to dump specific DB. Currently all collections will be dumped.)\n- Implement `last_workfile_path`\n- `startup_scripts` - must contain pointing host to startup script saved into `test_data/input/startup`\n  -- Script must contain something like (pseudocode)\n```\nimport openpype\nfrom avalon import api, HOST\n\nfrom openpype.api import Logger\n\nlog = Logger().get_logger(__name__)\n\napi.install(HOST)\nlog_lines = []\nfor result in pyblish.util.publish_iter():\n    for record in result[\"records\"]:  # for logging to test_openpype DB\n        log_lines.append(\"{}: {}\".format(\n            result[\"plugin\"].label, record.msg))\n\n    if result[\"error\"]:\n        err_fmt = \"Failed {plugin.__name__}: {error} -- {error.traceback}\"\n        log.error(err_fmt.format(**result))\n\nEXIT_APP (command to exit host)\n```\n(Install and publish methods must be triggered only AFTER host app is fully initialized!)\n- If you would like add any command line arguments for your host app add it to `test_data/input/app_args/app_args.json` (as a json list)\n- Provide any required environment variables to `test_data/input/env_vars/env_vars.json` (as a json dictionary)\n- Implement any assert checks you need in extended class\n- Run test class manually (via Pycharm or pytest runner (TODO))\n- If you want test to visually compare expected files to published one, set PERSIST to True, run test manually\n  -- Locate temporary `publish` subfolder of temporary folder (found in debugging console log)\n  -- Copy whole folder content into .zip file into `expected` subfolder\n  -- By default tests are comparing only structure of `expected` and published format (eg. if you want to save space, replace published files with empty files, but with expected names!)\n  -- Zip and upload again, change PERSIST to False\n\n- Use `TEST_DATA_FOLDER` variable in your class to reuse existing downloaded and unzipped test data (for faster creation of tests)\n- Keep `APP_VARIANT` empty if you want to trigger test on latest version of app, or provide explicit value (as '2022' for Photoshop for example)\n\nFor storing test zip files on Google Drive:\n- Zip `test_data.zip`, named it with descriptive name, upload it to Google Drive, right click - `Get link`, copy hash id (file must be accessible to anyone with a link!)\n- Put this hash id and zip file name into TEST_FILES [(HASH_ID, FILE_NAME, MD5_OPTIONAL)]. If you want to check MD5 of downloaded\nfile, provide md5 value of zipped file.\n"
  },
  {
    "path": "tests/integration/hosts/aftereffects/lib.py",
    "content": "import os\nimport pytest\nimport shutil\n\nfrom tests.lib.testing_classes import (\n    HostFixtures,\n    PublishTest,\n    DeadlinePublishTest\n\n)\n\n\nclass AEHostFixtures(HostFixtures):\n    @pytest.fixture(scope=\"module\")\n    def last_workfile_path(self, download_test_data, output_folder_url):\n        \"\"\"Get last_workfile_path from source data.\n\n            Maya expects workfile in proper folder, so copy is done first.\n        \"\"\"\n        src_path = os.path.join(download_test_data,\n                                \"input\",\n                                \"workfile\",\n                                \"test_project_test_asset_test_task_v001.aep\")\n        dest_folder = os.path.join(output_folder_url,\n                                   self.PROJECT,\n                                   self.ASSET,\n                                   \"work\",\n                                   self.TASK)\n        os.makedirs(dest_folder)\n        dest_path = os.path.join(dest_folder,\n                                 \"test_project_test_asset_test_task_v001.aep\")\n        shutil.copy(src_path, dest_path)\n\n        yield dest_path\n\n    @pytest.fixture(scope=\"module\")\n    def startup_scripts(self, monkeypatch_session, download_test_data):\n        \"\"\"Points Maya to userSetup file from input data\"\"\"\n        pass\n\n    @pytest.fixture(scope=\"module\")\n    def skip_compare_folders(self):\n        # skip folder that contain \"Logs\", these come only from Deadline\n        return [\"Logs\", \"Auto-Save\"]\n\n\nclass AELocalPublishTestClass(AEHostFixtures, PublishTest):\n    \"\"\"Testing class for local publishes.\"\"\"\n\n\nclass AEDeadlinePublishTestClass(AEHostFixtures, DeadlinePublishTest):\n    \"\"\"Testing class for Deadline publishes.\"\"\"\n"
  },
  {
    "path": "tests/integration/hosts/aftereffects/test_deadline_publish_in_aftereffects.py",
    "content": "import logging\n\nfrom tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.aftereffects.lib import AEDeadlinePublishTestClass\n\nlog = logging.getLogger(\"test_publish_in_aftereffects\")\n\n\nclass TestDeadlinePublishInAfterEffects(AEDeadlinePublishTestClass):\n    \"\"\"Basic test case for DL publishing in AfterEffects\n\n        Uses generic TestCase to prepare fixtures for test data, testing DBs,\n        env vars.\n\n        Opens AfterEffects, run DL publish on prepared workile.\n\n        Test zip file sets 3 required env vars:\n        - HEADLESS_PUBLISH - this triggers publish immediately app is open\n        - IS_TEST - this differentiate between regular webpublish\n        - PYBLISH_TARGETS\n\n        Waits for publish job on DL is finished.\n\n        Then checks content of DB (if subset, version, representations were\n        created.\n        Checks tmp folder if all expected files were published.\n\n    \"\"\"\n    PERSIST = False\n\n    TEST_FILES = [\n        (\"1xhd2ij2ixyjCyTjZgcJEPAIiBHLU1FEY\",\n         \"test_aftereffects_publish.zip\",\n         \"\")\n    ]\n\n    APP_GROUP = \"aftereffects\"\n    APP_VARIANT = \"\"\n\n    APP_NAME = \"{}/{}\".format(APP_GROUP, APP_VARIANT)\n\n    TIMEOUT = 120  # publish timeout\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 2))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"renderTest_taskMain\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 3))\n\n        additional_args = {\"context.subset\": \"workfileTest_task\",\n                           \"context.ext\": \"aep\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"context.ext\": \"png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 2,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"thumbnail\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"png_png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestDeadlinePublishInAfterEffects()\n"
  },
  {
    "path": "tests/integration/hosts/aftereffects/test_deadline_publish_in_aftereffects_multicomposition.py",
    "content": "import logging\n\nfrom tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.aftereffects.lib import AEDeadlinePublishTestClass\n\nlog = logging.getLogger(\"test_publish_in_aftereffects\")\n\n\nclass TestDeadlinePublishInAfterEffectsMultiComposition(AEDeadlinePublishTestClass):  # noqa\n    \"\"\"est case for DL publishing in AfterEffects with multiple compositions.\n\n        Workfile contains 2 prepared `render` instances. First has review set,\n        second doesn't.\n\n        Uses generic TestCase to prepare fixtures for test data, testing DBs,\n        env vars.\n\n        Opens AfterEffects, run DL publish on prepared workile.\n\n        Test zip file sets 3 required env vars:\n        - HEADLESS_PUBLISH - this triggers publish immediately app is open\n        - IS_TEST - this differentiate between regular webpublish\n        - PYBLISH_TARGETS\n\n        As there are multiple render and publish jobs, it waits for publish job\n        of later render job. Depends on date created of metadata.json.\n\n        Then checks content of DB (if subset, version, representations were\n        created.\n        Checks tmp folder if all expected files were published.\n\n    \"\"\"\n    PERSIST = False\n\n    TEST_FILES = [\n        (\"16xIm3U5P7WQJXpa9E06jWebMK9QKUATN\",\n         \"test_aftereffects_deadline_publish_multicomposition.zip\",\n         \"\")\n    ]\n\n    APP_GROUP = \"aftereffects\"\n    APP_VARIANT = \"\"\n\n    APP_NAME = \"{}/{}\".format(APP_GROUP, APP_VARIANT)\n\n    TIMEOUT = 120  # publish timeout\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 3))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 3))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"renderTest_taskMain\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"renderTest_taskMain2\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 4))\n\n        additional_args = {\"context.subset\": \"workfileTest_task\",\n                           \"context.ext\": \"aep\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        # renderTest_taskMain\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"context.ext\": \"png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 2,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"thumbnail\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"png_png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        # renderTest_taskMain2\n        additional_args = {\"context.subset\": \"renderTest_taskMain2\",\n                           \"context.ext\": \"exr\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain2\",\n                           \"name\": \"thumbnail\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain2\",\n                           \"name\": \"png_exr\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestDeadlinePublishInAfterEffectsMultiComposition()\n"
  },
  {
    "path": "tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py",
    "content": "import logging\n\nfrom tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.aftereffects.lib import AELocalPublishTestClass\n\nlog = logging.getLogger(\"test_publish_in_aftereffects\")\n\n\nclass TestPublishInAfterEffects(AELocalPublishTestClass):\n    \"\"\"Basic test case for publishing in AfterEffects\n\n        Uses generic TestCase to prepare fixtures for test data, testing DBs,\n        env vars.\n\n        Opens AfterEffects, run publish on prepared workile.\n\n        Test zip file sets 3 required env vars:\n        - HEADLESS_PUBLISH - this triggers publish immediately app is open\n        - IS_TEST - this differentiate between regular webpublish\n        - PYBLISH_TARGETS\n\n        Then checks content of DB (if subset, version, representations were\n        created.\n        Checks tmp folder if all expected files were published.\n\n    \"\"\"\n    PERSIST = False\n\n    TEST_FILES = [\n        (\"1c8261CmHwyMgS-g7S4xL5epAp0jCBmhf\",\n         \"test_aftereffects_publish.zip\",\n         \"\")\n    ]\n\n    APP_GROUP = \"aftereffects\"\n    APP_VARIANT = \"\"\n\n    APP_NAME = \"{}/{}\".format(APP_GROUP, APP_VARIANT)\n\n    TIMEOUT = 120  # publish timeout\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 2))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"renderTest_taskMain\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 3))\n\n        additional_args = {\"context.subset\": \"workfileTest_task\",\n                           \"context.ext\": \"aep\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"context.ext\": \"png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 2,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"thumbnail\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"png_png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestPublishInAfterEffects()\n"
  },
  {
    "path": "tests/integration/hosts/aftereffects/test_publish_in_aftereffects_legacy.py",
    "content": "import logging\n\nfrom tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.aftereffects.lib import AELocalPublishTestClass\n\nlog = logging.getLogger(\"test_publish_in_aftereffects\")\n\n\nclass TestPublishInAfterEffects(AELocalPublishTestClass):\n    \"\"\"Basic test case for publishing in AfterEffects\n\n        Uses old Pyblish schema of created instances.\n\n        Uses generic TestCase to prepare fixtures for test data, testing DBs,\n        env vars.\n\n        Opens AfterEffects, run publish on prepared workile.\n\n        Test zip file sets 3 required env vars:\n        - HEADLESS_PUBLISH - this triggers publish immediately app is open\n        - IS_TEST - this differentiate between regular webpublish\n        - PYBLISH_TARGETS\n\n        Then checks content of DB (if subset, version, representations were\n        created.\n        Checks tmp folder if all expected files were published.\n\n    \"\"\"\n    PERSIST = False\n\n    TEST_FILES = [\n        (\"1jqI_uG2NusKFvZZF7C0ScHjxFJrlc9F-\",\n         \"test_aftereffects_publish_legacy.zip\",\n         \"\")\n    ]\n\n    APP_GROUP = \"aftereffects\"\n    APP_VARIANT = \"\"\n\n    APP_NAME = \"{}/{}\".format(APP_GROUP, APP_VARIANT)\n\n    TIMEOUT = 120  # publish timeout\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 2))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"renderTest_taskMain\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 3))\n\n        additional_args = {\"context.subset\": \"workfileTest_task\",\n                           \"context.ext\": \"aep\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"context.ext\": \"png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 2,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"thumbnail\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"png_png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"thumbnail\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"png_png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestPublishInAfterEffects()\n"
  },
  {
    "path": "tests/integration/hosts/aftereffects/test_publish_in_aftereffects_multiframe.py",
    "content": "import logging\n\nfrom tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.aftereffects.lib import AELocalPublishTestClass\n\nlog = logging.getLogger(\"test_publish_in_aftereffects\")\n\n\nclass TestPublishInAfterEffects(AELocalPublishTestClass):\n    \"\"\"Basic test case for publishing in AfterEffects\n\n    Should publish 10 frames\n    \"\"\"\n    PERSIST = True\n\n    TEST_FILES = [\n        (\"12aSDRjthn4X3yw83gz_0FZJcRRiVDEYT\",\n         \"test_aftereffects_publish_multiframe.zip\",\n         \"\")\n    ]\n\n    APP_GROUP = \"aftereffects\"\n    APP_VARIANT = \"\"\n\n    APP_NAME = \"{}/{}\".format(APP_GROUP, APP_VARIANT)\n\n    TIMEOUT = 120  # publish timeout\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 2))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"renderTest_taskMain\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 3))\n\n        additional_args = {\"context.subset\": \"workfileTest_task\",\n                           \"context.ext\": \"aep\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"context.ext\": \"png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"thumbnail\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"h264_png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestPublishInAfterEffects()\n"
  },
  {
    "path": "tests/integration/hosts/maya/lib.py",
    "content": "import os\nimport pytest\nimport shutil\n\nfrom tests.lib.testing_classes import (\n    HostFixtures,\n    PublishTest,\n    DeadlinePublishTest\n)\n\n\nclass MayaHostFixtures(HostFixtures):\n    @pytest.fixture(scope=\"module\")\n    def last_workfile_path(self, download_test_data, output_folder_url):\n        \"\"\"Get last_workfile_path from source data.\n\n            Maya expects workfile in proper folder, so copy is done first.\n        \"\"\"\n        src_path = os.path.join(\n            download_test_data,\n            \"input\",\n            \"workfile\",\n            \"test_project_test_asset_test_task_v001.ma\"\n        )\n        dest_folder = os.path.join(\n            output_folder_url,\n            self.PROJECT,\n            self.ASSET,\n            \"work\",\n            self.TASK\n        )\n\n        os.makedirs(dest_folder)\n\n        dest_path = os.path.join(\n            dest_folder, \"test_project_test_asset_test_task_v001.ma\"\n        )\n        shutil.copy(src_path, dest_path)\n\n        yield dest_path\n\n    @pytest.fixture(scope=\"module\")\n    def startup_scripts(self, monkeypatch_session, download_test_data):\n        \"\"\"Points Maya to userSetup file from input data\"\"\"\n        startup_path = os.path.join(\n            download_test_data, \"input\", \"startup\"\n        )\n        original_pythonpath = os.environ.get(\"PYTHONPATH\")\n        monkeypatch_session.setenv(\n            \"PYTHONPATH\",\n            \"{}{}{}\".format(startup_path, os.pathsep, original_pythonpath)\n        )\n\n        monkeypatch_session.setenv(\n            \"MAYA_CMD_FILE_OUTPUT\",\n            os.path.join(download_test_data, \"output.log\")\n        )\n\n    @pytest.fixture(scope=\"module\")\n    def skip_compare_folders(self):\n        yield []\n\n\nclass MayaLocalPublishTestClass(MayaHostFixtures, PublishTest):\n    \"\"\"Testing class for local publishes.\"\"\"\n\n\nclass MayaDeadlinePublishTestClass(MayaHostFixtures, DeadlinePublishTest):\n    \"\"\"Testing class for Deadline publishes.\"\"\"\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya/README.md",
    "content": "Test data\n---------\nEach class implementing `TestCase` can provide test file(s) by adding them to\nTEST_FILES ('GDRIVE_FILE_ID', 'ACTUAL_FILE_NAME', 'MD5HASH')\n\nGDRIVE_FILE_ID can be pulled from shareable link from Google Drive app.\n\nCurrently it is expected that test file will be zip file with structure:\n- expected - expected files (not implemented yet)\n- input\n    - data - test data (workfiles, images etc)\n    - dumps - folder for BSOn dumps from (`mongodump`)\n    - env_vars\n        env_vars.json - dictionary with environment variables {key:value}\n\n    - sql - sql files to load with `mongoimport` (human readable)\n    - startup - scripts that should run in the host on its startup\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/hero/test_project_test_asset_modelMain_hero.ma",
    "content": "//Maya ASCII 2020 scene\n//Name: modelMain.ma\n//Last modified: Mon, Oct 24, 2022 02:57:47 PM\n//Codeset: 1252\nrequires maya \"2020\";\nrequires \"mtoa\" \"4.1.1\";\ncurrentUnit -l centimeter -a degree -t pal;\nfileInfo \"application\" \"maya\";\nfileInfo \"product\" \"Maya 2020\";\nfileInfo \"version\" \"2020\";\nfileInfo \"cutIdentifier\" \"202011110415-b1e20b88e2\";\nfileInfo \"osv\" \"Microsoft Windows 10 Technical Preview  (Build 19044)\\n\";\nfileInfo \"UUID\" \"A787A358-4FE7-6E55-0C81-61BFEB0C2726\";\ncreateNode transform -n \"model_GRP\";\n\trename -uid \"445FDC20-4A9D-2C5B-C7BD-F98F6E660B5C\";\n\tsetAttr \".rlio[0]\" 1 yes 0;\ncreateNode transform -n \"pSphere1_GEO\" -p \"model_GRP\";\n\trename -uid \"7445A43F-444F-B2D3-4315-2AA013D2E0B6\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:302a4c6123a4\";\ncreateNode mesh -n \"pSphere1_GEOShape1\" -p \"pSphere1_GEO\";\n\trename -uid \"7C731260-45C6-339E-07BF-359446B08EA1\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr -s 439 \".uvst[0].uvsp\";\n\tsetAttr \".uvst[0].uvsp[0:249]\" -type \"float2\" 0 0.050000001 0.050000001 0.050000001\n\t\t 0.1 0.050000001 0.15000001 0.050000001 0.2 0.050000001 0.25 0.050000001 0.30000001\n\t\t 0.050000001 0.35000002 0.050000001 0.40000004 0.050000001 0.45000005 0.050000001\n\t\t 0.50000006 0.050000001 0.55000007 0.050000001 0.60000008 0.050000001 0.6500001 0.050000001\n\t\t 0.70000011 0.050000001 0.75000012 0.050000001 0.80000013 0.050000001 0.85000014 0.050000001\n\t\t 0.90000015 0.050000001 0.95000017 0.050000001 1.000000119209 0.050000001 0 0.1 0.050000001\n\t\t 0.1 0.1 0.1 0.15000001 0.1 0.2 0.1 0.25 0.1 0.30000001 0.1 0.35000002 0.1 0.40000004\n\t\t 0.1 0.45000005 0.1 0.50000006 0.1 0.55000007 0.1 0.60000008 0.1 0.6500001 0.1 0.70000011\n\t\t 0.1 0.75000012 0.1 0.80000013 0.1 0.85000014 0.1 0.90000015 0.1 0.95000017 0.1 1.000000119209\n\t\t 0.1 0 0.15000001 0.050000001 0.15000001 0.1 0.15000001 0.15000001 0.15000001 0.2\n\t\t 0.15000001 0.25 0.15000001 0.30000001 0.15000001 0.35000002 0.15000001 0.40000004\n\t\t 0.15000001 0.45000005 0.15000001 0.50000006 0.15000001 0.55000007 0.15000001 0.60000008\n\t\t 0.15000001 0.6500001 0.15000001 0.70000011 0.15000001 0.75000012 0.15000001 0.80000013\n\t\t 0.15000001 0.85000014 0.15000001 0.90000015 0.15000001 0.95000017 0.15000001 1.000000119209\n\t\t 0.15000001 0 0.2 0.050000001 0.2 0.1 0.2 0.15000001 0.2 0.2 0.2 0.25 0.2 0.30000001\n\t\t 0.2 0.35000002 0.2 0.40000004 0.2 0.45000005 0.2 0.50000006 0.2 0.55000007 0.2 0.60000008\n\t\t 0.2 0.6500001 0.2 0.70000011 0.2 0.75000012 0.2 0.80000013 0.2 0.85000014 0.2 0.90000015\n\t\t 0.2 0.95000017 0.2 1.000000119209 0.2 0 0.25 0.050000001 0.25 0.1 0.25 0.15000001\n\t\t 0.25 0.2 0.25 0.25 0.25 0.30000001 0.25 0.35000002 0.25 0.40000004 0.25 0.45000005\n\t\t 0.25 0.50000006 0.25 0.55000007 0.25 0.60000008 0.25 0.6500001 0.25 0.70000011 0.25\n\t\t 0.75000012 0.25 0.80000013 0.25 0.85000014 0.25 0.90000015 0.25 0.95000017 0.25 1.000000119209\n\t\t 0.25 0 0.30000001 0.050000001 0.30000001 0.1 0.30000001 0.15000001 0.30000001 0.2\n\t\t 0.30000001 0.25 0.30000001 0.30000001 0.30000001 0.35000002 0.30000001 0.40000004\n\t\t 0.30000001 0.45000005 0.30000001 0.50000006 0.30000001 0.55000007 0.30000001 0.60000008\n\t\t 0.30000001 0.6500001 0.30000001 0.70000011 0.30000001 0.75000012 0.30000001 0.80000013\n\t\t 0.30000001 0.85000014 0.30000001 0.90000015 0.30000001 0.95000017 0.30000001 1.000000119209\n\t\t 0.30000001 0 0.35000002 0.050000001 0.35000002 0.1 0.35000002 0.15000001 0.35000002\n\t\t 0.2 0.35000002 0.25 0.35000002 0.30000001 0.35000002 0.35000002 0.35000002 0.40000004\n\t\t 0.35000002 0.45000005 0.35000002 0.50000006 0.35000002 0.55000007 0.35000002 0.60000008\n\t\t 0.35000002 0.6500001 0.35000002 0.70000011 0.35000002 0.75000012 0.35000002 0.80000013\n\t\t 0.35000002 0.85000014 0.35000002 0.90000015 0.35000002 0.95000017 0.35000002 1.000000119209\n\t\t 0.35000002 0 0.40000004 0.050000001 0.40000004 0.1 0.40000004 0.15000001 0.40000004\n\t\t 0.2 0.40000004 0.25 0.40000004 0.30000001 0.40000004 0.35000002 0.40000004 0.40000004\n\t\t 0.40000004 0.45000005 0.40000004 0.50000006 0.40000004 0.55000007 0.40000004 0.60000008\n\t\t 0.40000004 0.6500001 0.40000004 0.70000011 0.40000004 0.75000012 0.40000004 0.80000013\n\t\t 0.40000004 0.85000014 0.40000004 0.90000015 0.40000004 0.95000017 0.40000004 1.000000119209\n\t\t 0.40000004 0 0.45000005 0.050000001 0.45000005 0.1 0.45000005 0.15000001 0.45000005\n\t\t 0.2 0.45000005 0.25 0.45000005 0.30000001 0.45000005 0.35000002 0.45000005 0.40000004\n\t\t 0.45000005 0.45000005 0.45000005 0.50000006 0.45000005 0.55000007 0.45000005 0.60000008\n\t\t 0.45000005 0.6500001 0.45000005 0.70000011 0.45000005 0.75000012 0.45000005 0.80000013\n\t\t 0.45000005 0.85000014 0.45000005 0.90000015 0.45000005 0.95000017 0.45000005 1.000000119209\n\t\t 0.45000005 0 0.50000006 0.050000001 0.50000006 0.1 0.50000006 0.15000001 0.50000006\n\t\t 0.2 0.50000006 0.25 0.50000006 0.30000001 0.50000006 0.35000002 0.50000006 0.40000004\n\t\t 0.50000006 0.45000005 0.50000006 0.50000006 0.50000006 0.55000007 0.50000006 0.60000008\n\t\t 0.50000006 0.6500001 0.50000006 0.70000011 0.50000006 0.75000012 0.50000006 0.80000013\n\t\t 0.50000006 0.85000014 0.50000006 0.90000015 0.50000006 0.95000017 0.50000006 1.000000119209\n\t\t 0.50000006 0 0.55000007 0.050000001 0.55000007 0.1 0.55000007 0.15000001 0.55000007\n\t\t 0.2 0.55000007 0.25 0.55000007 0.30000001 0.55000007 0.35000002 0.55000007 0.40000004\n\t\t 0.55000007 0.45000005 0.55000007 0.50000006 0.55000007 0.55000007 0.55000007 0.60000008\n\t\t 0.55000007 0.6500001 0.55000007 0.70000011 0.55000007 0.75000012 0.55000007 0.80000013\n\t\t 0.55000007 0.85000014 0.55000007 0.90000015 0.55000007 0.95000017 0.55000007 1.000000119209\n\t\t 0.55000007 0 0.60000008 0.050000001 0.60000008 0.1 0.60000008 0.15000001 0.60000008\n\t\t 0.2 0.60000008 0.25 0.60000008 0.30000001 0.60000008 0.35000002 0.60000008 0.40000004\n\t\t 0.60000008 0.45000005 0.60000008 0.50000006 0.60000008 0.55000007 0.60000008 0.60000008\n\t\t 0.60000008 0.6500001 0.60000008 0.70000011 0.60000008 0.75000012 0.60000008 0.80000013\n\t\t 0.60000008 0.85000014 0.60000008 0.90000015 0.60000008;\n\tsetAttr \".uvst[0].uvsp[250:438]\" 0.95000017 0.60000008 1.000000119209 0.60000008\n\t\t 0 0.6500001 0.050000001 0.6500001 0.1 0.6500001 0.15000001 0.6500001 0.2 0.6500001\n\t\t 0.25 0.6500001 0.30000001 0.6500001 0.35000002 0.6500001 0.40000004 0.6500001 0.45000005\n\t\t 0.6500001 0.50000006 0.6500001 0.55000007 0.6500001 0.60000008 0.6500001 0.6500001\n\t\t 0.6500001 0.70000011 0.6500001 0.75000012 0.6500001 0.80000013 0.6500001 0.85000014\n\t\t 0.6500001 0.90000015 0.6500001 0.95000017 0.6500001 1.000000119209 0.6500001 0 0.70000011\n\t\t 0.050000001 0.70000011 0.1 0.70000011 0.15000001 0.70000011 0.2 0.70000011 0.25 0.70000011\n\t\t 0.30000001 0.70000011 0.35000002 0.70000011 0.40000004 0.70000011 0.45000005 0.70000011\n\t\t 0.50000006 0.70000011 0.55000007 0.70000011 0.60000008 0.70000011 0.6500001 0.70000011\n\t\t 0.70000011 0.70000011 0.75000012 0.70000011 0.80000013 0.70000011 0.85000014 0.70000011\n\t\t 0.90000015 0.70000011 0.95000017 0.70000011 1.000000119209 0.70000011 0 0.75000012\n\t\t 0.050000001 0.75000012 0.1 0.75000012 0.15000001 0.75000012 0.2 0.75000012 0.25 0.75000012\n\t\t 0.30000001 0.75000012 0.35000002 0.75000012 0.40000004 0.75000012 0.45000005 0.75000012\n\t\t 0.50000006 0.75000012 0.55000007 0.75000012 0.60000008 0.75000012 0.6500001 0.75000012\n\t\t 0.70000011 0.75000012 0.75000012 0.75000012 0.80000013 0.75000012 0.85000014 0.75000012\n\t\t 0.90000015 0.75000012 0.95000017 0.75000012 1.000000119209 0.75000012 0 0.80000013\n\t\t 0.050000001 0.80000013 0.1 0.80000013 0.15000001 0.80000013 0.2 0.80000013 0.25 0.80000013\n\t\t 0.30000001 0.80000013 0.35000002 0.80000013 0.40000004 0.80000013 0.45000005 0.80000013\n\t\t 0.50000006 0.80000013 0.55000007 0.80000013 0.60000008 0.80000013 0.6500001 0.80000013\n\t\t 0.70000011 0.80000013 0.75000012 0.80000013 0.80000013 0.80000013 0.85000014 0.80000013\n\t\t 0.90000015 0.80000013 0.95000017 0.80000013 1.000000119209 0.80000013 0 0.85000014\n\t\t 0.050000001 0.85000014 0.1 0.85000014 0.15000001 0.85000014 0.2 0.85000014 0.25 0.85000014\n\t\t 0.30000001 0.85000014 0.35000002 0.85000014 0.40000004 0.85000014 0.45000005 0.85000014\n\t\t 0.50000006 0.85000014 0.55000007 0.85000014 0.60000008 0.85000014 0.6500001 0.85000014\n\t\t 0.70000011 0.85000014 0.75000012 0.85000014 0.80000013 0.85000014 0.85000014 0.85000014\n\t\t 0.90000015 0.85000014 0.95000017 0.85000014 1.000000119209 0.85000014 0 0.90000015\n\t\t 0.050000001 0.90000015 0.1 0.90000015 0.15000001 0.90000015 0.2 0.90000015 0.25 0.90000015\n\t\t 0.30000001 0.90000015 0.35000002 0.90000015 0.40000004 0.90000015 0.45000005 0.90000015\n\t\t 0.50000006 0.90000015 0.55000007 0.90000015 0.60000008 0.90000015 0.6500001 0.90000015\n\t\t 0.70000011 0.90000015 0.75000012 0.90000015 0.80000013 0.90000015 0.85000014 0.90000015\n\t\t 0.90000015 0.90000015 0.95000017 0.90000015 1.000000119209 0.90000015 0 0.95000017\n\t\t 0.050000001 0.95000017 0.1 0.95000017 0.15000001 0.95000017 0.2 0.95000017 0.25 0.95000017\n\t\t 0.30000001 0.95000017 0.35000002 0.95000017 0.40000004 0.95000017 0.45000005 0.95000017\n\t\t 0.50000006 0.95000017 0.55000007 0.95000017 0.60000008 0.95000017 0.6500001 0.95000017\n\t\t 0.70000011 0.95000017 0.75000012 0.95000017 0.80000013 0.95000017 0.85000014 0.95000017\n\t\t 0.90000015 0.95000017 0.95000017 0.95000017 1.000000119209 0.95000017 0.025 0 0.075000003\n\t\t 0 0.125 0 0.17500001 0 0.22500001 0 0.27500001 0 0.32500002 0 0.375 0 0.42500001\n\t\t 0 0.47500002 0 0.52499998 0 0.57499999 0 0.625 0 0.67500001 0 0.72499996 0 0.77499998\n\t\t 0 0.82499999 0 0.875 0 0.92500001 0 0.97499996 0 0.025 1 0.075000003 1 0.125 1 0.17500001\n\t\t 1 0.22500001 1 0.27500001 1 0.32500002 1 0.375 1 0.42500001 1 0.47500002 1 0.52499998\n\t\t 1 0.57499999 1 0.625 1 0.67500001 1 0.72499996 1 0.77499998 1 0.82499999 1 0.875\n\t\t 1 0.92500001 1 0.97499996 1;\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr -s 382 \".vt\";\n\tsetAttr \".vt[0:165]\"  0.14877813 -0.98768836 -0.048340943 0.12655823 -0.98768836 -0.091949932\n\t\t 0.091949932 -0.98768836 -0.12655823 0.048340935 -0.98768836 -0.14877811 0 -0.98768836 -0.15643455\n\t\t -0.048340935 -0.98768836 -0.1487781 -0.091949917 -0.98768836 -0.1265582 -0.12655818 -0.98768836 -0.091949902\n\t\t -0.14877807 -0.98768836 -0.048340924 -0.15643452 -0.98768836 0 -0.14877807 -0.98768836 0.048340924\n\t\t -0.12655818 -0.98768836 0.091949895 -0.091949895 -0.98768836 0.12655817 -0.048340924 -0.98768836 0.14877805\n\t\t -4.6621107e-09 -0.98768836 0.15643449 0.048340909 -0.98768836 0.14877804 0.09194988 -0.98768836 0.12655815\n\t\t 0.12655815 -0.98768836 0.091949888 0.14877804 -0.98768836 0.048340913 0.15643448 -0.98768836 0\n\t\t 0.29389283 -0.95105654 -0.095491566 0.25000018 -0.95105654 -0.18163574 0.18163574 -0.95105654 -0.25000015\n\t\t 0.095491551 -0.95105654 -0.2938928 0 -0.95105654 -0.30901715 -0.095491551 -0.95105654 -0.29389277\n\t\t -0.18163571 -0.95105654 -0.25000009 -0.25000009 -0.95105654 -0.18163569 -0.29389271 -0.95105654 -0.095491529\n\t\t -0.30901706 -0.95105654 0 -0.29389271 -0.95105654 0.095491529 -0.25000006 -0.95105654 0.18163568\n\t\t -0.18163568 -0.95105654 0.25000006 -0.095491529 -0.95105654 0.29389268 -9.2094243e-09 -0.95105654 0.30901703\n\t\t 0.095491499 -0.95105654 0.29389265 0.18163563 -0.95105654 0.25000003 0.25 -0.95105654 0.18163565\n\t\t 0.29389265 -0.95105654 0.095491506 0.309017 -0.95105654 0 0.43177092 -0.89100653 -0.14029087\n\t\t 0.36728629 -0.89100653 -0.2668491 0.2668491 -0.89100653 -0.36728626 0.14029086 -0.89100653 -0.43177086\n\t\t 0 -0.89100653 -0.45399073 -0.14029086 -0.89100653 -0.43177083 -0.26684904 -0.89100653 -0.36728618\n\t\t -0.36728615 -0.89100653 -0.26684901 -0.43177077 -0.89100653 -0.14029081 -0.45399064 -0.89100653 0\n\t\t -0.43177077 -0.89100653 0.14029081 -0.36728612 -0.89100653 0.26684898 -0.26684898 -0.89100653 0.36728612\n\t\t -0.14029081 -0.89100653 0.43177071 -1.3529972e-08 -0.89100653 0.45399058 0.14029078 -0.89100653 0.43177068\n\t\t 0.26684892 -0.89100653 0.36728609 0.36728606 -0.89100653 0.26684895 0.43177065 -0.89100653 0.1402908\n\t\t 0.45399052 -0.89100653 0 0.55901736 -0.809017 -0.18163574 0.47552857 -0.809017 -0.34549171\n\t\t 0.34549171 -0.809017 -0.47552854 0.18163572 -0.809017 -0.5590173 0 -0.809017 -0.58778554\n\t\t -0.18163572 -0.809017 -0.55901724 -0.34549165 -0.809017 -0.47552842 -0.47552839 -0.809017 -0.34549159\n\t\t -0.55901712 -0.809017 -0.18163566 -0.58778536 -0.809017 0 -0.55901712 -0.809017 0.18163566\n\t\t -0.47552836 -0.809017 0.34549156 -0.34549156 -0.809017 0.47552833 -0.18163566 -0.809017 0.55901706\n\t\t -1.7517365e-08 -0.809017 0.5877853 0.18163562 -0.809017 0.55901706 0.3454915 -0.809017 0.4755283\n\t\t 0.47552827 -0.809017 0.34549153 0.559017 -0.809017 0.18163563 0.58778524 -0.809017 0\n\t\t 0.67249894 -0.70710677 -0.21850814 0.57206178 -0.70710677 -0.41562718 0.41562718 -0.70710677 -0.57206172\n\t\t 0.21850812 -0.70710677 -0.67249888 0 -0.70710677 -0.70710713 -0.21850812 -0.70710677 -0.67249882\n\t\t -0.41562709 -0.70710677 -0.5720616 -0.57206154 -0.70710677 -0.41562706 -0.6724987 -0.70710677 -0.21850805\n\t\t -0.70710695 -0.70710677 0 -0.6724987 -0.70710677 0.21850805 -0.57206154 -0.70710677 0.415627\n\t\t -0.415627 -0.70710677 0.57206148 -0.21850805 -0.70710677 0.67249858 -2.1073424e-08 -0.70710677 0.70710683\n\t\t 0.21850799 -0.70710677 0.67249858 0.41562691 -0.70710677 0.57206142 0.57206142 -0.70710677 0.41562697\n\t\t 0.67249852 -0.70710677 0.21850802 0.70710677 -0.70710677 0 0.7694214 -0.58778524 -0.25000015\n\t\t 0.65450895 -0.58778524 -0.47552854 0.47552854 -0.58778524 -0.65450889 0.25000012 -0.58778524 -0.76942128\n\t\t 0 -0.58778524 -0.80901736 -0.25000012 -0.58778524 -0.76942122 -0.47552845 -0.58778524 -0.65450877\n\t\t -0.65450871 -0.58778524 -0.47552839 -0.7694211 -0.58778524 -0.25000006 -0.80901718 -0.58778524 0\n\t\t -0.7694211 -0.58778524 0.25000006 -0.65450865 -0.58778524 0.47552836 -0.47552836 -0.58778524 0.65450859\n\t\t -0.25000006 -0.58778524 0.76942098 -2.4110586e-08 -0.58778524 0.80901712 0.24999999 -0.58778524 0.76942098\n\t\t 0.47552827 -0.58778524 0.65450853 0.65450853 -0.58778524 0.4755283 0.76942092 -0.58778524 0.25\n\t\t 0.809017 -0.58778524 0 0.8473981 -0.45399052 -0.27533633 0.72083992 -0.45399052 -0.5237208\n\t\t 0.5237208 -0.45399052 -0.72083986 0.2753363 -0.45399052 -0.84739798 0 -0.45399052 -0.89100695\n\t\t -0.2753363 -0.45399052 -0.84739798 -0.52372068 -0.45399052 -0.72083968 -0.72083962 -0.45399052 -0.52372062\n\t\t -0.8473978 -0.45399052 -0.27533621 -0.89100677 -0.45399052 0 -0.8473978 -0.45399052 0.27533621\n\t\t -0.72083962 -0.45399052 0.52372062 -0.52372062 -0.45399052 0.72083956 -0.27533621 -0.45399052 0.84739769\n\t\t -2.6554064e-08 -0.45399052 0.89100665 0.27533615 -0.45399052 0.84739763 0.5237205 -0.45399052 0.7208395\n\t\t 0.72083944 -0.45399052 0.52372056 0.84739757 -0.45399052 0.27533618 0.89100653 -0.45399052 0\n\t\t 0.90450913 -0.30901697 -0.2938928 0.7694214 -0.30901697 -0.55901736 0.55901736 -0.30901697 -0.76942134\n\t\t 0.29389277 -0.30901697 -0.90450901 0 -0.30901697 -0.95105702 -0.29389277 -0.30901697 -0.90450895\n\t\t -0.55901724 -0.30901697 -0.76942122 -0.76942116 -0.30901697 -0.55901718 -0.90450877 -0.30901697 -0.29389271\n\t\t -0.95105678 -0.30901697 0 -0.90450877 -0.30901697 0.29389271 -0.7694211 -0.30901697 0.55901712\n\t\t -0.55901712 -0.30901697 0.76942104 -0.29389271 -0.30901697 0.90450865 -2.8343694e-08 -0.30901697 0.95105666\n\t\t 0.29389262 -0.30901697 0.90450859 0.559017 -0.30901697 0.76942098 0.76942092 -0.30901697 0.55901706\n\t\t 0.90450853 -0.30901697 0.29389265 0.95105654 -0.30901697 0 0.93934804 -0.15643437 -0.30521268\n\t\t 0.79905719 -0.15643437 -0.580549 0.580549 -0.15643437 -0.79905713 0.30521265 -0.15643437 -0.93934792\n\t\t 0 -0.15643437 -0.98768884 -0.30521265 -0.15643437 -0.93934786;\n\tsetAttr \".vt[166:331]\" -0.58054888 -0.15643437 -0.79905695 -0.79905689 -0.15643437 -0.58054882\n\t\t -0.93934768 -0.15643437 -0.30521256 -0.9876886 -0.15643437 0 -0.93934768 -0.15643437 0.30521256\n\t\t -0.79905683 -0.15643437 0.58054876 -0.58054876 -0.15643437 0.79905677 -0.30521256 -0.15643437 0.93934757\n\t\t -2.9435407e-08 -0.15643437 0.98768848 0.30521247 -0.15643437 0.93934757 0.58054864 -0.15643437 0.79905671\n\t\t 0.79905665 -0.15643437 0.5805487 0.93934751 -0.15643437 0.3052125 0.98768836 -0.15643437 0\n\t\t 0.95105714 0 -0.30901718 0.80901754 0 -0.5877856 0.5877856 0 -0.80901748 0.30901715 0 -0.95105702\n\t\t 0 0 -1.000000476837 -0.30901715 0 -0.95105696 -0.58778548 0 -0.8090173 -0.80901724 0 -0.58778542\n\t\t -0.95105678 0 -0.30901706 -1.000000238419 0 0 -0.95105678 0 0.30901706 -0.80901718 0 0.58778536\n\t\t -0.58778536 0 0.80901712 -0.30901706 0 0.95105666 -2.9802322e-08 0 1.000000119209\n\t\t 0.30901697 0 0.9510566 0.58778524 0 0.80901706 0.809017 0 0.5877853 0.95105654 0 0.309017\n\t\t 1 0 0 0.93934804 0.15643437 -0.30521268 0.79905719 0.15643437 -0.580549 0.580549 0.15643437 -0.79905713\n\t\t 0.30521265 0.15643437 -0.93934792 0 0.15643437 -0.98768884 -0.30521265 0.15643437 -0.93934786\n\t\t -0.58054888 0.15643437 -0.79905695 -0.79905689 0.15643437 -0.58054882 -0.93934768 0.15643437 -0.30521256\n\t\t -0.9876886 0.15643437 0 -0.93934768 0.15643437 0.30521256 -0.79905683 0.15643437 0.58054876\n\t\t -0.58054876 0.15643437 0.79905677 -0.30521256 0.15643437 0.93934757 -2.9435407e-08 0.15643437 0.98768848\n\t\t 0.30521247 0.15643437 0.93934757 0.58054864 0.15643437 0.79905671 0.79905665 0.15643437 0.5805487\n\t\t 0.93934751 0.15643437 0.3052125 0.98768836 0.15643437 0 0.90450913 0.30901697 -0.2938928\n\t\t 0.7694214 0.30901697 -0.55901736 0.55901736 0.30901697 -0.76942134 0.29389277 0.30901697 -0.90450901\n\t\t 0 0.30901697 -0.95105702 -0.29389277 0.30901697 -0.90450895 -0.55901724 0.30901697 -0.76942122\n\t\t -0.76942116 0.30901697 -0.55901718 -0.90450877 0.30901697 -0.29389271 -0.95105678 0.30901697 0\n\t\t -0.90450877 0.30901697 0.29389271 -0.7694211 0.30901697 0.55901712 -0.55901712 0.30901697 0.76942104\n\t\t -0.29389271 0.30901697 0.90450865 -2.8343694e-08 0.30901697 0.95105666 0.29389262 0.30901697 0.90450859\n\t\t 0.559017 0.30901697 0.76942098 0.76942092 0.30901697 0.55901706 0.90450853 0.30901697 0.29389265\n\t\t 0.95105654 0.30901697 0 0.8473981 0.45399052 -0.27533633 0.72083992 0.45399052 -0.5237208\n\t\t 0.5237208 0.45399052 -0.72083986 0.2753363 0.45399052 -0.84739798 0 0.45399052 -0.89100695\n\t\t -0.2753363 0.45399052 -0.84739798 -0.52372068 0.45399052 -0.72083968 -0.72083962 0.45399052 -0.52372062\n\t\t -0.8473978 0.45399052 -0.27533621 -0.89100677 0.45399052 0 -0.8473978 0.45399052 0.27533621\n\t\t -0.72083962 0.45399052 0.52372062 -0.52372062 0.45399052 0.72083956 -0.27533621 0.45399052 0.84739769\n\t\t -2.6554064e-08 0.45399052 0.89100665 0.27533615 0.45399052 0.84739763 0.5237205 0.45399052 0.7208395\n\t\t 0.72083944 0.45399052 0.52372056 0.84739757 0.45399052 0.27533618 0.89100653 0.45399052 0\n\t\t 0.7694214 0.58778524 -0.25000015 0.65450895 0.58778524 -0.47552854 0.47552854 0.58778524 -0.65450889\n\t\t 0.25000012 0.58778524 -0.76942128 0 0.58778524 -0.80901736 -0.25000012 0.58778524 -0.76942122\n\t\t -0.47552845 0.58778524 -0.65450877 -0.65450871 0.58778524 -0.47552839 -0.7694211 0.58778524 -0.25000006\n\t\t -0.80901718 0.58778524 0 -0.7694211 0.58778524 0.25000006 -0.65450865 0.58778524 0.47552836\n\t\t -0.47552836 0.58778524 0.65450859 -0.25000006 0.58778524 0.76942098 -2.4110586e-08 0.58778524 0.80901712\n\t\t 0.24999999 0.58778524 0.76942098 0.47552827 0.58778524 0.65450853 0.65450853 0.58778524 0.4755283\n\t\t 0.76942092 0.58778524 0.25 0.809017 0.58778524 0 0.67249894 0.70710677 -0.21850814\n\t\t 0.57206178 0.70710677 -0.41562718 0.41562718 0.70710677 -0.57206172 0.21850812 0.70710677 -0.67249888\n\t\t 0 0.70710677 -0.70710713 -0.21850812 0.70710677 -0.67249882 -0.41562709 0.70710677 -0.5720616\n\t\t -0.57206154 0.70710677 -0.41562706 -0.6724987 0.70710677 -0.21850805 -0.70710695 0.70710677 0\n\t\t -0.6724987 0.70710677 0.21850805 -0.57206154 0.70710677 0.415627 -0.415627 0.70710677 0.57206148\n\t\t -0.21850805 0.70710677 0.67249858 -2.1073424e-08 0.70710677 0.70710683 0.21850799 0.70710677 0.67249858\n\t\t 0.41562691 0.70710677 0.57206142 0.57206142 0.70710677 0.41562697 0.67249852 0.70710677 0.21850802\n\t\t 0.70710677 0.70710677 0 0.55901736 0.809017 -0.18163574 0.47552857 0.809017 -0.34549171\n\t\t 0.34549171 0.809017 -0.47552854 0.18163572 0.809017 -0.5590173 0 0.809017 -0.58778554\n\t\t -0.18163572 0.809017 -0.55901724 -0.34549165 0.809017 -0.47552842 -0.47552839 0.809017 -0.34549159\n\t\t -0.55901712 0.809017 -0.18163566 -0.58778536 0.809017 0 -0.55901712 0.809017 0.18163566\n\t\t -0.47552836 0.809017 0.34549156 -0.34549156 0.809017 0.47552833 -0.18163566 0.809017 0.55901706\n\t\t -1.7517365e-08 0.809017 0.5877853 0.18163562 0.809017 0.55901706 0.3454915 0.809017 0.4755283\n\t\t 0.47552827 0.809017 0.34549153 0.559017 0.809017 0.18163563 0.58778524 0.809017 0\n\t\t 0.43177092 0.89100653 -0.14029087 0.36728629 0.89100653 -0.2668491 0.2668491 0.89100653 -0.36728626\n\t\t 0.14029086 0.89100653 -0.43177086 0 0.89100653 -0.45399073 -0.14029086 0.89100653 -0.43177083\n\t\t -0.26684904 0.89100653 -0.36728618 -0.36728615 0.89100653 -0.26684901 -0.43177077 0.89100653 -0.14029081\n\t\t -0.45399064 0.89100653 0 -0.43177077 0.89100653 0.14029081 -0.36728612 0.89100653 0.26684898;\n\tsetAttr \".vt[332:381]\" -0.26684898 0.89100653 0.36728612 -0.14029081 0.89100653 0.43177071\n\t\t -1.3529972e-08 0.89100653 0.45399058 0.14029078 0.89100653 0.43177068 0.26684892 0.89100653 0.36728609\n\t\t 0.36728606 0.89100653 0.26684895 0.43177065 0.89100653 0.1402908 0.45399052 0.89100653 0\n\t\t 0.29389283 0.95105654 -0.095491566 0.25000018 0.95105654 -0.18163574 0.18163574 0.95105654 -0.25000015\n\t\t 0.095491551 0.95105654 -0.2938928 0 0.95105654 -0.30901715 -0.095491551 0.95105654 -0.29389277\n\t\t -0.18163571 0.95105654 -0.25000009 -0.25000009 0.95105654 -0.18163569 -0.29389271 0.95105654 -0.095491529\n\t\t -0.30901706 0.95105654 0 -0.29389271 0.95105654 0.095491529 -0.25000006 0.95105654 0.18163568\n\t\t -0.18163568 0.95105654 0.25000006 -0.095491529 0.95105654 0.29389268 -9.2094243e-09 0.95105654 0.30901703\n\t\t 0.095491499 0.95105654 0.29389265 0.18163563 0.95105654 0.25000003 0.25 0.95105654 0.18163565\n\t\t 0.29389265 0.95105654 0.095491506 0.309017 0.95105654 0 0.14877813 0.98768836 -0.048340943\n\t\t 0.12655823 0.98768836 -0.091949932 0.091949932 0.98768836 -0.12655823 0.048340935 0.98768836 -0.14877811\n\t\t 0 0.98768836 -0.15643455 -0.048340935 0.98768836 -0.1487781 -0.091949917 0.98768836 -0.1265582\n\t\t -0.12655818 0.98768836 -0.091949902 -0.14877807 0.98768836 -0.048340924 -0.15643452 0.98768836 0\n\t\t -0.14877807 0.98768836 0.048340924 -0.12655818 0.98768836 0.091949895 -0.091949895 0.98768836 0.12655817\n\t\t -0.048340924 0.98768836 0.14877805 -4.6621107e-09 0.98768836 0.15643449 0.048340909 0.98768836 0.14877804\n\t\t 0.09194988 0.98768836 0.12655815 0.12655815 0.98768836 0.091949888 0.14877804 0.98768836 0.048340913\n\t\t 0.15643448 0.98768836 0 0 -1 0 0 1 0;\n\tsetAttr -s 780 \".ed\";\n\tsetAttr \".ed[0:165]\"  0 1 1 1 2 1 2 3 1 3 4 1 4 5 1 5 6 1 6 7 1 7 8 1 8 9 1\n\t\t 9 10 1 10 11 1 11 12 1 12 13 1 13 14 1 14 15 1 15 16 1 16 17 1 17 18 1 18 19 1 19 0 1\n\t\t 20 21 1 21 22 1 22 23 1 23 24 1 24 25 1 25 26 1 26 27 1 27 28 1 28 29 1 29 30 1 30 31 1\n\t\t 31 32 1 32 33 1 33 34 1 34 35 1 35 36 1 36 37 1 37 38 1 38 39 1 39 20 1 40 41 1 41 42 1\n\t\t 42 43 1 43 44 1 44 45 1 45 46 1 46 47 1 47 48 1 48 49 1 49 50 1 50 51 1 51 52 1 52 53 1\n\t\t 53 54 1 54 55 1 55 56 1 56 57 1 57 58 1 58 59 1 59 40 1 60 61 1 61 62 1 62 63 1 63 64 1\n\t\t 64 65 1 65 66 1 66 67 1 67 68 1 68 69 1 69 70 1 70 71 1 71 72 1 72 73 1 73 74 1 74 75 1\n\t\t 75 76 1 76 77 1 77 78 1 78 79 1 79 60 1 80 81 1 81 82 1 82 83 1 83 84 1 84 85 1 85 86 1\n\t\t 86 87 1 87 88 1 88 89 1 89 90 1 90 91 1 91 92 1 92 93 1 93 94 1 94 95 1 95 96 1 96 97 1\n\t\t 97 98 1 98 99 1 99 80 1 100 101 1 101 102 1 102 103 1 103 104 1 104 105 1 105 106 1\n\t\t 106 107 1 107 108 1 108 109 1 109 110 1 110 111 1 111 112 1 112 113 1 113 114 1 114 115 1\n\t\t 115 116 1 116 117 1 117 118 1 118 119 1 119 100 1 120 121 1 121 122 1 122 123 1 123 124 1\n\t\t 124 125 1 125 126 1 126 127 1 127 128 1 128 129 1 129 130 1 130 131 1 131 132 1 132 133 1\n\t\t 133 134 1 134 135 1 135 136 1 136 137 1 137 138 1 138 139 1 139 120 1 140 141 1 141 142 1\n\t\t 142 143 1 143 144 1 144 145 1 145 146 1 146 147 1 147 148 1 148 149 1 149 150 1 150 151 1\n\t\t 151 152 1 152 153 1 153 154 1 154 155 1 155 156 1 156 157 1 157 158 1 158 159 1 159 140 1\n\t\t 160 161 1 161 162 1 162 163 1 163 164 1 164 165 1 165 166 1;\n\tsetAttr \".ed[166:331]\" 166 167 1 167 168 1 168 169 1 169 170 1 170 171 1 171 172 1\n\t\t 172 173 1 173 174 1 174 175 1 175 176 1 176 177 1 177 178 1 178 179 1 179 160 1 180 181 1\n\t\t 181 182 1 182 183 1 183 184 1 184 185 1 185 186 1 186 187 1 187 188 1 188 189 1 189 190 1\n\t\t 190 191 1 191 192 1 192 193 1 193 194 1 194 195 1 195 196 1 196 197 1 197 198 1 198 199 1\n\t\t 199 180 1 200 201 1 201 202 1 202 203 1 203 204 1 204 205 1 205 206 1 206 207 1 207 208 1\n\t\t 208 209 1 209 210 1 210 211 1 211 212 1 212 213 1 213 214 1 214 215 1 215 216 1 216 217 1\n\t\t 217 218 1 218 219 1 219 200 1 220 221 1 221 222 1 222 223 1 223 224 1 224 225 1 225 226 1\n\t\t 226 227 1 227 228 1 228 229 1 229 230 1 230 231 1 231 232 1 232 233 1 233 234 1 234 235 1\n\t\t 235 236 1 236 237 1 237 238 1 238 239 1 239 220 1 240 241 1 241 242 1 242 243 1 243 244 1\n\t\t 244 245 1 245 246 1 246 247 1 247 248 1 248 249 1 249 250 1 250 251 1 251 252 1 252 253 1\n\t\t 253 254 1 254 255 1 255 256 1 256 257 1 257 258 1 258 259 1 259 240 1 260 261 1 261 262 1\n\t\t 262 263 1 263 264 1 264 265 1 265 266 1 266 267 1 267 268 1 268 269 1 269 270 1 270 271 1\n\t\t 271 272 1 272 273 1 273 274 1 274 275 1 275 276 1 276 277 1 277 278 1 278 279 1 279 260 1\n\t\t 280 281 1 281 282 1 282 283 1 283 284 1 284 285 1 285 286 1 286 287 1 287 288 1 288 289 1\n\t\t 289 290 1 290 291 1 291 292 1 292 293 1 293 294 1 294 295 1 295 296 1 296 297 1 297 298 1\n\t\t 298 299 1 299 280 1 300 301 1 301 302 1 302 303 1 303 304 1 304 305 1 305 306 1 306 307 1\n\t\t 307 308 1 308 309 1 309 310 1 310 311 1 311 312 1 312 313 1 313 314 1 314 315 1 315 316 1\n\t\t 316 317 1 317 318 1 318 319 1 319 300 1 320 321 1 321 322 1 322 323 1 323 324 1 324 325 1\n\t\t 325 326 1 326 327 1 327 328 1 328 329 1 329 330 1 330 331 1 331 332 1;\n\tsetAttr \".ed[332:497]\" 332 333 1 333 334 1 334 335 1 335 336 1 336 337 1 337 338 1\n\t\t 338 339 1 339 320 1 340 341 1 341 342 1 342 343 1 343 344 1 344 345 1 345 346 1 346 347 1\n\t\t 347 348 1 348 349 1 349 350 1 350 351 1 351 352 1 352 353 1 353 354 1 354 355 1 355 356 1\n\t\t 356 357 1 357 358 1 358 359 1 359 340 1 360 361 1 361 362 1 362 363 1 363 364 1 364 365 1\n\t\t 365 366 1 366 367 1 367 368 1 368 369 1 369 370 1 370 371 1 371 372 1 372 373 1 373 374 1\n\t\t 374 375 1 375 376 1 376 377 1 377 378 1 378 379 1 379 360 1 0 20 1 1 21 1 2 22 1\n\t\t 3 23 1 4 24 1 5 25 1 6 26 1 7 27 1 8 28 1 9 29 1 10 30 1 11 31 1 12 32 1 13 33 1\n\t\t 14 34 1 15 35 1 16 36 1 17 37 1 18 38 1 19 39 1 20 40 1 21 41 1 22 42 1 23 43 1 24 44 1\n\t\t 25 45 1 26 46 1 27 47 1 28 48 1 29 49 1 30 50 1 31 51 1 32 52 1 33 53 1 34 54 1 35 55 1\n\t\t 36 56 1 37 57 1 38 58 1 39 59 1 40 60 1 41 61 1 42 62 1 43 63 1 44 64 1 45 65 1 46 66 1\n\t\t 47 67 1 48 68 1 49 69 1 50 70 1 51 71 1 52 72 1 53 73 1 54 74 1 55 75 1 56 76 1 57 77 1\n\t\t 58 78 1 59 79 1 60 80 1 61 81 1 62 82 1 63 83 1 64 84 1 65 85 1 66 86 1 67 87 1 68 88 1\n\t\t 69 89 1 70 90 1 71 91 1 72 92 1 73 93 1 74 94 1 75 95 1 76 96 1 77 97 1 78 98 1 79 99 1\n\t\t 80 100 1 81 101 1 82 102 1 83 103 1 84 104 1 85 105 1 86 106 1 87 107 1 88 108 1\n\t\t 89 109 1 90 110 1 91 111 1 92 112 1 93 113 1 94 114 1 95 115 1 96 116 1 97 117 1\n\t\t 98 118 1 99 119 1 100 120 1 101 121 1 102 122 1 103 123 1 104 124 1 105 125 1 106 126 1\n\t\t 107 127 1 108 128 1 109 129 1 110 130 1 111 131 1 112 132 1 113 133 1 114 134 1 115 135 1\n\t\t 116 136 1 117 137 1;\n\tsetAttr \".ed[498:663]\" 118 138 1 119 139 1 120 140 1 121 141 1 122 142 1 123 143 1\n\t\t 124 144 1 125 145 1 126 146 1 127 147 1 128 148 1 129 149 1 130 150 1 131 151 1 132 152 1\n\t\t 133 153 1 134 154 1 135 155 1 136 156 1 137 157 1 138 158 1 139 159 1 140 160 1 141 161 1\n\t\t 142 162 1 143 163 1 144 164 1 145 165 1 146 166 1 147 167 1 148 168 1 149 169 1 150 170 1\n\t\t 151 171 1 152 172 1 153 173 1 154 174 1 155 175 1 156 176 1 157 177 1 158 178 1 159 179 1\n\t\t 160 180 1 161 181 1 162 182 1 163 183 1 164 184 1 165 185 1 166 186 1 167 187 1 168 188 1\n\t\t 169 189 1 170 190 1 171 191 1 172 192 1 173 193 1 174 194 1 175 195 1 176 196 1 177 197 1\n\t\t 178 198 1 179 199 1 180 200 1 181 201 1 182 202 1 183 203 1 184 204 1 185 205 1 186 206 1\n\t\t 187 207 1 188 208 1 189 209 1 190 210 1 191 211 1 192 212 1 193 213 1 194 214 1 195 215 1\n\t\t 196 216 1 197 217 1 198 218 1 199 219 1 200 220 1 201 221 1 202 222 1 203 223 1 204 224 1\n\t\t 205 225 1 206 226 1 207 227 1 208 228 1 209 229 1 210 230 1 211 231 1 212 232 1 213 233 1\n\t\t 214 234 1 215 235 1 216 236 1 217 237 1 218 238 1 219 239 1 220 240 1 221 241 1 222 242 1\n\t\t 223 243 1 224 244 1 225 245 1 226 246 1 227 247 1 228 248 1 229 249 1 230 250 1 231 251 1\n\t\t 232 252 1 233 253 1 234 254 1 235 255 1 236 256 1 237 257 1 238 258 1 239 259 1 240 260 1\n\t\t 241 261 1 242 262 1 243 263 1 244 264 1 245 265 1 246 266 1 247 267 1 248 268 1 249 269 1\n\t\t 250 270 1 251 271 1 252 272 1 253 273 1 254 274 1 255 275 1 256 276 1 257 277 1 258 278 1\n\t\t 259 279 1 260 280 1 261 281 1 262 282 1 263 283 1 264 284 1 265 285 1 266 286 1 267 287 1\n\t\t 268 288 1 269 289 1 270 290 1 271 291 1 272 292 1 273 293 1 274 294 1 275 295 1 276 296 1\n\t\t 277 297 1 278 298 1 279 299 1 280 300 1 281 301 1 282 302 1 283 303 1;\n\tsetAttr \".ed[664:779]\" 284 304 1 285 305 1 286 306 1 287 307 1 288 308 1 289 309 1\n\t\t 290 310 1 291 311 1 292 312 1 293 313 1 294 314 1 295 315 1 296 316 1 297 317 1 298 318 1\n\t\t 299 319 1 300 320 1 301 321 1 302 322 1 303 323 1 304 324 1 305 325 1 306 326 1 307 327 1\n\t\t 308 328 1 309 329 1 310 330 1 311 331 1 312 332 1 313 333 1 314 334 1 315 335 1 316 336 1\n\t\t 317 337 1 318 338 1 319 339 1 320 340 1 321 341 1 322 342 1 323 343 1 324 344 1 325 345 1\n\t\t 326 346 1 327 347 1 328 348 1 329 349 1 330 350 1 331 351 1 332 352 1 333 353 1 334 354 1\n\t\t 335 355 1 336 356 1 337 357 1 338 358 1 339 359 1 340 360 1 341 361 1 342 362 1 343 363 1\n\t\t 344 364 1 345 365 1 346 366 1 347 367 1 348 368 1 349 369 1 350 370 1 351 371 1 352 372 1\n\t\t 353 373 1 354 374 1 355 375 1 356 376 1 357 377 1 358 378 1 359 379 1 380 0 1 380 1 1\n\t\t 380 2 1 380 3 1 380 4 1 380 5 1 380 6 1 380 7 1 380 8 1 380 9 1 380 10 1 380 11 1\n\t\t 380 12 1 380 13 1 380 14 1 380 15 1 380 16 1 380 17 1 380 18 1 380 19 1 360 381 1\n\t\t 361 381 1 362 381 1 363 381 1 364 381 1 365 381 1 366 381 1 367 381 1 368 381 1 369 381 1\n\t\t 370 381 1 371 381 1 372 381 1 373 381 1 374 381 1 375 381 1 376 381 1 377 381 1 378 381 1\n\t\t 379 381 1;\n\tsetAttr -s 400 -ch 1560 \".fc[0:399]\" -type \"polyFaces\"\n\t\tf 4 0 381 -21 -381\n\t\tmu 0 4 0 1 22 21\n\t\tf 4 1 382 -22 -382\n\t\tmu 0 4 1 2 23 22\n\t\tf 4 2 383 -23 -383\n\t\tmu 0 4 2 3 24 23\n\t\tf 4 3 384 -24 -384\n\t\tmu 0 4 3 4 25 24\n\t\tf 4 4 385 -25 -385\n\t\tmu 0 4 4 5 26 25\n\t\tf 4 5 386 -26 -386\n\t\tmu 0 4 5 6 27 26\n\t\tf 4 6 387 -27 -387\n\t\tmu 0 4 6 7 28 27\n\t\tf 4 7 388 -28 -388\n\t\tmu 0 4 7 8 29 28\n\t\tf 4 8 389 -29 -389\n\t\tmu 0 4 8 9 30 29\n\t\tf 4 9 390 -30 -390\n\t\tmu 0 4 9 10 31 30\n\t\tf 4 10 391 -31 -391\n\t\tmu 0 4 10 11 32 31\n\t\tf 4 11 392 -32 -392\n\t\tmu 0 4 11 12 33 32\n\t\tf 4 12 393 -33 -393\n\t\tmu 0 4 12 13 34 33\n\t\tf 4 13 394 -34 -394\n\t\tmu 0 4 13 14 35 34\n\t\tf 4 14 395 -35 -395\n\t\tmu 0 4 14 15 36 35\n\t\tf 4 15 396 -36 -396\n\t\tmu 0 4 15 16 37 36\n\t\tf 4 16 397 -37 -397\n\t\tmu 0 4 16 17 38 37\n\t\tf 4 17 398 -38 -398\n\t\tmu 0 4 17 18 39 38\n\t\tf 4 18 399 -39 -399\n\t\tmu 0 4 18 19 40 39\n\t\tf 4 19 380 -40 -400\n\t\tmu 0 4 19 20 41 40\n\t\tf 4 20 401 -41 -401\n\t\tmu 0 4 21 22 43 42\n\t\tf 4 21 402 -42 -402\n\t\tmu 0 4 22 23 44 43\n\t\tf 4 22 403 -43 -403\n\t\tmu 0 4 23 24 45 44\n\t\tf 4 23 404 -44 -404\n\t\tmu 0 4 24 25 46 45\n\t\tf 4 24 405 -45 -405\n\t\tmu 0 4 25 26 47 46\n\t\tf 4 25 406 -46 -406\n\t\tmu 0 4 26 27 48 47\n\t\tf 4 26 407 -47 -407\n\t\tmu 0 4 27 28 49 48\n\t\tf 4 27 408 -48 -408\n\t\tmu 0 4 28 29 50 49\n\t\tf 4 28 409 -49 -409\n\t\tmu 0 4 29 30 51 50\n\t\tf 4 29 410 -50 -410\n\t\tmu 0 4 30 31 52 51\n\t\tf 4 30 411 -51 -411\n\t\tmu 0 4 31 32 53 52\n\t\tf 4 31 412 -52 -412\n\t\tmu 0 4 32 33 54 53\n\t\tf 4 32 413 -53 -413\n\t\tmu 0 4 33 34 55 54\n\t\tf 4 33 414 -54 -414\n\t\tmu 0 4 34 35 56 55\n\t\tf 4 34 415 -55 -415\n\t\tmu 0 4 35 36 57 56\n\t\tf 4 35 416 -56 -416\n\t\tmu 0 4 36 37 58 57\n\t\tf 4 36 417 -57 -417\n\t\tmu 0 4 37 38 59 58\n\t\tf 4 37 418 -58 -418\n\t\tmu 0 4 38 39 60 59\n\t\tf 4 38 419 -59 -419\n\t\tmu 0 4 39 40 61 60\n\t\tf 4 39 400 -60 -420\n\t\tmu 0 4 40 41 62 61\n\t\tf 4 40 421 -61 -421\n\t\tmu 0 4 42 43 64 63\n\t\tf 4 41 422 -62 -422\n\t\tmu 0 4 43 44 65 64\n\t\tf 4 42 423 -63 -423\n\t\tmu 0 4 44 45 66 65\n\t\tf 4 43 424 -64 -424\n\t\tmu 0 4 45 46 67 66\n\t\tf 4 44 425 -65 -425\n\t\tmu 0 4 46 47 68 67\n\t\tf 4 45 426 -66 -426\n\t\tmu 0 4 47 48 69 68\n\t\tf 4 46 427 -67 -427\n\t\tmu 0 4 48 49 70 69\n\t\tf 4 47 428 -68 -428\n\t\tmu 0 4 49 50 71 70\n\t\tf 4 48 429 -69 -429\n\t\tmu 0 4 50 51 72 71\n\t\tf 4 49 430 -70 -430\n\t\tmu 0 4 51 52 73 72\n\t\tf 4 50 431 -71 -431\n\t\tmu 0 4 52 53 74 73\n\t\tf 4 51 432 -72 -432\n\t\tmu 0 4 53 54 75 74\n\t\tf 4 52 433 -73 -433\n\t\tmu 0 4 54 55 76 75\n\t\tf 4 53 434 -74 -434\n\t\tmu 0 4 55 56 77 76\n\t\tf 4 54 435 -75 -435\n\t\tmu 0 4 56 57 78 77\n\t\tf 4 55 436 -76 -436\n\t\tmu 0 4 57 58 79 78\n\t\tf 4 56 437 -77 -437\n\t\tmu 0 4 58 59 80 79\n\t\tf 4 57 438 -78 -438\n\t\tmu 0 4 59 60 81 80\n\t\tf 4 58 439 -79 -439\n\t\tmu 0 4 60 61 82 81\n\t\tf 4 59 420 -80 -440\n\t\tmu 0 4 61 62 83 82\n\t\tf 4 60 441 -81 -441\n\t\tmu 0 4 63 64 85 84\n\t\tf 4 61 442 -82 -442\n\t\tmu 0 4 64 65 86 85\n\t\tf 4 62 443 -83 -443\n\t\tmu 0 4 65 66 87 86\n\t\tf 4 63 444 -84 -444\n\t\tmu 0 4 66 67 88 87\n\t\tf 4 64 445 -85 -445\n\t\tmu 0 4 67 68 89 88\n\t\tf 4 65 446 -86 -446\n\t\tmu 0 4 68 69 90 89\n\t\tf 4 66 447 -87 -447\n\t\tmu 0 4 69 70 91 90\n\t\tf 4 67 448 -88 -448\n\t\tmu 0 4 70 71 92 91\n\t\tf 4 68 449 -89 -449\n\t\tmu 0 4 71 72 93 92\n\t\tf 4 69 450 -90 -450\n\t\tmu 0 4 72 73 94 93\n\t\tf 4 70 451 -91 -451\n\t\tmu 0 4 73 74 95 94\n\t\tf 4 71 452 -92 -452\n\t\tmu 0 4 74 75 96 95\n\t\tf 4 72 453 -93 -453\n\t\tmu 0 4 75 76 97 96\n\t\tf 4 73 454 -94 -454\n\t\tmu 0 4 76 77 98 97\n\t\tf 4 74 455 -95 -455\n\t\tmu 0 4 77 78 99 98\n\t\tf 4 75 456 -96 -456\n\t\tmu 0 4 78 79 100 99\n\t\tf 4 76 457 -97 -457\n\t\tmu 0 4 79 80 101 100\n\t\tf 4 77 458 -98 -458\n\t\tmu 0 4 80 81 102 101\n\t\tf 4 78 459 -99 -459\n\t\tmu 0 4 81 82 103 102\n\t\tf 4 79 440 -100 -460\n\t\tmu 0 4 82 83 104 103\n\t\tf 4 80 461 -101 -461\n\t\tmu 0 4 84 85 106 105\n\t\tf 4 81 462 -102 -462\n\t\tmu 0 4 85 86 107 106\n\t\tf 4 82 463 -103 -463\n\t\tmu 0 4 86 87 108 107\n\t\tf 4 83 464 -104 -464\n\t\tmu 0 4 87 88 109 108\n\t\tf 4 84 465 -105 -465\n\t\tmu 0 4 88 89 110 109\n\t\tf 4 85 466 -106 -466\n\t\tmu 0 4 89 90 111 110\n\t\tf 4 86 467 -107 -467\n\t\tmu 0 4 90 91 112 111\n\t\tf 4 87 468 -108 -468\n\t\tmu 0 4 91 92 113 112\n\t\tf 4 88 469 -109 -469\n\t\tmu 0 4 92 93 114 113\n\t\tf 4 89 470 -110 -470\n\t\tmu 0 4 93 94 115 114\n\t\tf 4 90 471 -111 -471\n\t\tmu 0 4 94 95 116 115\n\t\tf 4 91 472 -112 -472\n\t\tmu 0 4 95 96 117 116\n\t\tf 4 92 473 -113 -473\n\t\tmu 0 4 96 97 118 117\n\t\tf 4 93 474 -114 -474\n\t\tmu 0 4 97 98 119 118\n\t\tf 4 94 475 -115 -475\n\t\tmu 0 4 98 99 120 119\n\t\tf 4 95 476 -116 -476\n\t\tmu 0 4 99 100 121 120\n\t\tf 4 96 477 -117 -477\n\t\tmu 0 4 100 101 122 121\n\t\tf 4 97 478 -118 -478\n\t\tmu 0 4 101 102 123 122\n\t\tf 4 98 479 -119 -479\n\t\tmu 0 4 102 103 124 123\n\t\tf 4 99 460 -120 -480\n\t\tmu 0 4 103 104 125 124\n\t\tf 4 100 481 -121 -481\n\t\tmu 0 4 105 106 127 126\n\t\tf 4 101 482 -122 -482\n\t\tmu 0 4 106 107 128 127\n\t\tf 4 102 483 -123 -483\n\t\tmu 0 4 107 108 129 128\n\t\tf 4 103 484 -124 -484\n\t\tmu 0 4 108 109 130 129\n\t\tf 4 104 485 -125 -485\n\t\tmu 0 4 109 110 131 130\n\t\tf 4 105 486 -126 -486\n\t\tmu 0 4 110 111 132 131\n\t\tf 4 106 487 -127 -487\n\t\tmu 0 4 111 112 133 132\n\t\tf 4 107 488 -128 -488\n\t\tmu 0 4 112 113 134 133\n\t\tf 4 108 489 -129 -489\n\t\tmu 0 4 113 114 135 134\n\t\tf 4 109 490 -130 -490\n\t\tmu 0 4 114 115 136 135\n\t\tf 4 110 491 -131 -491\n\t\tmu 0 4 115 116 137 136\n\t\tf 4 111 492 -132 -492\n\t\tmu 0 4 116 117 138 137\n\t\tf 4 112 493 -133 -493\n\t\tmu 0 4 117 118 139 138\n\t\tf 4 113 494 -134 -494\n\t\tmu 0 4 118 119 140 139\n\t\tf 4 114 495 -135 -495\n\t\tmu 0 4 119 120 141 140\n\t\tf 4 115 496 -136 -496\n\t\tmu 0 4 120 121 142 141\n\t\tf 4 116 497 -137 -497\n\t\tmu 0 4 121 122 143 142\n\t\tf 4 117 498 -138 -498\n\t\tmu 0 4 122 123 144 143\n\t\tf 4 118 499 -139 -499\n\t\tmu 0 4 123 124 145 144\n\t\tf 4 119 480 -140 -500\n\t\tmu 0 4 124 125 146 145\n\t\tf 4 120 501 -141 -501\n\t\tmu 0 4 126 127 148 147\n\t\tf 4 121 502 -142 -502\n\t\tmu 0 4 127 128 149 148\n\t\tf 4 122 503 -143 -503\n\t\tmu 0 4 128 129 150 149\n\t\tf 4 123 504 -144 -504\n\t\tmu 0 4 129 130 151 150\n\t\tf 4 124 505 -145 -505\n\t\tmu 0 4 130 131 152 151\n\t\tf 4 125 506 -146 -506\n\t\tmu 0 4 131 132 153 152\n\t\tf 4 126 507 -147 -507\n\t\tmu 0 4 132 133 154 153\n\t\tf 4 127 508 -148 -508\n\t\tmu 0 4 133 134 155 154\n\t\tf 4 128 509 -149 -509\n\t\tmu 0 4 134 135 156 155\n\t\tf 4 129 510 -150 -510\n\t\tmu 0 4 135 136 157 156\n\t\tf 4 130 511 -151 -511\n\t\tmu 0 4 136 137 158 157\n\t\tf 4 131 512 -152 -512\n\t\tmu 0 4 137 138 159 158\n\t\tf 4 132 513 -153 -513\n\t\tmu 0 4 138 139 160 159\n\t\tf 4 133 514 -154 -514\n\t\tmu 0 4 139 140 161 160\n\t\tf 4 134 515 -155 -515\n\t\tmu 0 4 140 141 162 161\n\t\tf 4 135 516 -156 -516\n\t\tmu 0 4 141 142 163 162\n\t\tf 4 136 517 -157 -517\n\t\tmu 0 4 142 143 164 163\n\t\tf 4 137 518 -158 -518\n\t\tmu 0 4 143 144 165 164\n\t\tf 4 138 519 -159 -519\n\t\tmu 0 4 144 145 166 165\n\t\tf 4 139 500 -160 -520\n\t\tmu 0 4 145 146 167 166\n\t\tf 4 140 521 -161 -521\n\t\tmu 0 4 147 148 169 168\n\t\tf 4 141 522 -162 -522\n\t\tmu 0 4 148 149 170 169\n\t\tf 4 142 523 -163 -523\n\t\tmu 0 4 149 150 171 170\n\t\tf 4 143 524 -164 -524\n\t\tmu 0 4 150 151 172 171\n\t\tf 4 144 525 -165 -525\n\t\tmu 0 4 151 152 173 172\n\t\tf 4 145 526 -166 -526\n\t\tmu 0 4 152 153 174 173\n\t\tf 4 146 527 -167 -527\n\t\tmu 0 4 153 154 175 174\n\t\tf 4 147 528 -168 -528\n\t\tmu 0 4 154 155 176 175\n\t\tf 4 148 529 -169 -529\n\t\tmu 0 4 155 156 177 176\n\t\tf 4 149 530 -170 -530\n\t\tmu 0 4 156 157 178 177\n\t\tf 4 150 531 -171 -531\n\t\tmu 0 4 157 158 179 178\n\t\tf 4 151 532 -172 -532\n\t\tmu 0 4 158 159 180 179\n\t\tf 4 152 533 -173 -533\n\t\tmu 0 4 159 160 181 180\n\t\tf 4 153 534 -174 -534\n\t\tmu 0 4 160 161 182 181\n\t\tf 4 154 535 -175 -535\n\t\tmu 0 4 161 162 183 182\n\t\tf 4 155 536 -176 -536\n\t\tmu 0 4 162 163 184 183\n\t\tf 4 156 537 -177 -537\n\t\tmu 0 4 163 164 185 184\n\t\tf 4 157 538 -178 -538\n\t\tmu 0 4 164 165 186 185\n\t\tf 4 158 539 -179 -539\n\t\tmu 0 4 165 166 187 186\n\t\tf 4 159 520 -180 -540\n\t\tmu 0 4 166 167 188 187\n\t\tf 4 160 541 -181 -541\n\t\tmu 0 4 168 169 190 189\n\t\tf 4 161 542 -182 -542\n\t\tmu 0 4 169 170 191 190\n\t\tf 4 162 543 -183 -543\n\t\tmu 0 4 170 171 192 191\n\t\tf 4 163 544 -184 -544\n\t\tmu 0 4 171 172 193 192\n\t\tf 4 164 545 -185 -545\n\t\tmu 0 4 172 173 194 193\n\t\tf 4 165 546 -186 -546\n\t\tmu 0 4 173 174 195 194\n\t\tf 4 166 547 -187 -547\n\t\tmu 0 4 174 175 196 195\n\t\tf 4 167 548 -188 -548\n\t\tmu 0 4 175 176 197 196\n\t\tf 4 168 549 -189 -549\n\t\tmu 0 4 176 177 198 197\n\t\tf 4 169 550 -190 -550\n\t\tmu 0 4 177 178 199 198\n\t\tf 4 170 551 -191 -551\n\t\tmu 0 4 178 179 200 199\n\t\tf 4 171 552 -192 -552\n\t\tmu 0 4 179 180 201 200\n\t\tf 4 172 553 -193 -553\n\t\tmu 0 4 180 181 202 201\n\t\tf 4 173 554 -194 -554\n\t\tmu 0 4 181 182 203 202\n\t\tf 4 174 555 -195 -555\n\t\tmu 0 4 182 183 204 203\n\t\tf 4 175 556 -196 -556\n\t\tmu 0 4 183 184 205 204\n\t\tf 4 176 557 -197 -557\n\t\tmu 0 4 184 185 206 205\n\t\tf 4 177 558 -198 -558\n\t\tmu 0 4 185 186 207 206\n\t\tf 4 178 559 -199 -559\n\t\tmu 0 4 186 187 208 207\n\t\tf 4 179 540 -200 -560\n\t\tmu 0 4 187 188 209 208\n\t\tf 4 180 561 -201 -561\n\t\tmu 0 4 189 190 211 210\n\t\tf 4 181 562 -202 -562\n\t\tmu 0 4 190 191 212 211\n\t\tf 4 182 563 -203 -563\n\t\tmu 0 4 191 192 213 212\n\t\tf 4 183 564 -204 -564\n\t\tmu 0 4 192 193 214 213\n\t\tf 4 184 565 -205 -565\n\t\tmu 0 4 193 194 215 214\n\t\tf 4 185 566 -206 -566\n\t\tmu 0 4 194 195 216 215\n\t\tf 4 186 567 -207 -567\n\t\tmu 0 4 195 196 217 216\n\t\tf 4 187 568 -208 -568\n\t\tmu 0 4 196 197 218 217\n\t\tf 4 188 569 -209 -569\n\t\tmu 0 4 197 198 219 218\n\t\tf 4 189 570 -210 -570\n\t\tmu 0 4 198 199 220 219\n\t\tf 4 190 571 -211 -571\n\t\tmu 0 4 199 200 221 220\n\t\tf 4 191 572 -212 -572\n\t\tmu 0 4 200 201 222 221\n\t\tf 4 192 573 -213 -573\n\t\tmu 0 4 201 202 223 222\n\t\tf 4 193 574 -214 -574\n\t\tmu 0 4 202 203 224 223\n\t\tf 4 194 575 -215 -575\n\t\tmu 0 4 203 204 225 224\n\t\tf 4 195 576 -216 -576\n\t\tmu 0 4 204 205 226 225\n\t\tf 4 196 577 -217 -577\n\t\tmu 0 4 205 206 227 226\n\t\tf 4 197 578 -218 -578\n\t\tmu 0 4 206 207 228 227\n\t\tf 4 198 579 -219 -579\n\t\tmu 0 4 207 208 229 228\n\t\tf 4 199 560 -220 -580\n\t\tmu 0 4 208 209 230 229\n\t\tf 4 200 581 -221 -581\n\t\tmu 0 4 210 211 232 231\n\t\tf 4 201 582 -222 -582\n\t\tmu 0 4 211 212 233 232\n\t\tf 4 202 583 -223 -583\n\t\tmu 0 4 212 213 234 233\n\t\tf 4 203 584 -224 -584\n\t\tmu 0 4 213 214 235 234\n\t\tf 4 204 585 -225 -585\n\t\tmu 0 4 214 215 236 235\n\t\tf 4 205 586 -226 -586\n\t\tmu 0 4 215 216 237 236\n\t\tf 4 206 587 -227 -587\n\t\tmu 0 4 216 217 238 237\n\t\tf 4 207 588 -228 -588\n\t\tmu 0 4 217 218 239 238\n\t\tf 4 208 589 -229 -589\n\t\tmu 0 4 218 219 240 239\n\t\tf 4 209 590 -230 -590\n\t\tmu 0 4 219 220 241 240\n\t\tf 4 210 591 -231 -591\n\t\tmu 0 4 220 221 242 241\n\t\tf 4 211 592 -232 -592\n\t\tmu 0 4 221 222 243 242\n\t\tf 4 212 593 -233 -593\n\t\tmu 0 4 222 223 244 243\n\t\tf 4 213 594 -234 -594\n\t\tmu 0 4 223 224 245 244\n\t\tf 4 214 595 -235 -595\n\t\tmu 0 4 224 225 246 245\n\t\tf 4 215 596 -236 -596\n\t\tmu 0 4 225 226 247 246\n\t\tf 4 216 597 -237 -597\n\t\tmu 0 4 226 227 248 247\n\t\tf 4 217 598 -238 -598\n\t\tmu 0 4 227 228 249 248\n\t\tf 4 218 599 -239 -599\n\t\tmu 0 4 228 229 250 249\n\t\tf 4 219 580 -240 -600\n\t\tmu 0 4 229 230 251 250\n\t\tf 4 220 601 -241 -601\n\t\tmu 0 4 231 232 253 252\n\t\tf 4 221 602 -242 -602\n\t\tmu 0 4 232 233 254 253\n\t\tf 4 222 603 -243 -603\n\t\tmu 0 4 233 234 255 254\n\t\tf 4 223 604 -244 -604\n\t\tmu 0 4 234 235 256 255\n\t\tf 4 224 605 -245 -605\n\t\tmu 0 4 235 236 257 256\n\t\tf 4 225 606 -246 -606\n\t\tmu 0 4 236 237 258 257\n\t\tf 4 226 607 -247 -607\n\t\tmu 0 4 237 238 259 258\n\t\tf 4 227 608 -248 -608\n\t\tmu 0 4 238 239 260 259\n\t\tf 4 228 609 -249 -609\n\t\tmu 0 4 239 240 261 260\n\t\tf 4 229 610 -250 -610\n\t\tmu 0 4 240 241 262 261\n\t\tf 4 230 611 -251 -611\n\t\tmu 0 4 241 242 263 262\n\t\tf 4 231 612 -252 -612\n\t\tmu 0 4 242 243 264 263\n\t\tf 4 232 613 -253 -613\n\t\tmu 0 4 243 244 265 264\n\t\tf 4 233 614 -254 -614\n\t\tmu 0 4 244 245 266 265\n\t\tf 4 234 615 -255 -615\n\t\tmu 0 4 245 246 267 266\n\t\tf 4 235 616 -256 -616\n\t\tmu 0 4 246 247 268 267\n\t\tf 4 236 617 -257 -617\n\t\tmu 0 4 247 248 269 268\n\t\tf 4 237 618 -258 -618\n\t\tmu 0 4 248 249 270 269\n\t\tf 4 238 619 -259 -619\n\t\tmu 0 4 249 250 271 270\n\t\tf 4 239 600 -260 -620\n\t\tmu 0 4 250 251 272 271\n\t\tf 4 240 621 -261 -621\n\t\tmu 0 4 252 253 274 273\n\t\tf 4 241 622 -262 -622\n\t\tmu 0 4 253 254 275 274\n\t\tf 4 242 623 -263 -623\n\t\tmu 0 4 254 255 276 275\n\t\tf 4 243 624 -264 -624\n\t\tmu 0 4 255 256 277 276\n\t\tf 4 244 625 -265 -625\n\t\tmu 0 4 256 257 278 277\n\t\tf 4 245 626 -266 -626\n\t\tmu 0 4 257 258 279 278\n\t\tf 4 246 627 -267 -627\n\t\tmu 0 4 258 259 280 279\n\t\tf 4 247 628 -268 -628\n\t\tmu 0 4 259 260 281 280\n\t\tf 4 248 629 -269 -629\n\t\tmu 0 4 260 261 282 281\n\t\tf 4 249 630 -270 -630\n\t\tmu 0 4 261 262 283 282\n\t\tf 4 250 631 -271 -631\n\t\tmu 0 4 262 263 284 283\n\t\tf 4 251 632 -272 -632\n\t\tmu 0 4 263 264 285 284\n\t\tf 4 252 633 -273 -633\n\t\tmu 0 4 264 265 286 285\n\t\tf 4 253 634 -274 -634\n\t\tmu 0 4 265 266 287 286\n\t\tf 4 254 635 -275 -635\n\t\tmu 0 4 266 267 288 287\n\t\tf 4 255 636 -276 -636\n\t\tmu 0 4 267 268 289 288\n\t\tf 4 256 637 -277 -637\n\t\tmu 0 4 268 269 290 289\n\t\tf 4 257 638 -278 -638\n\t\tmu 0 4 269 270 291 290\n\t\tf 4 258 639 -279 -639\n\t\tmu 0 4 270 271 292 291\n\t\tf 4 259 620 -280 -640\n\t\tmu 0 4 271 272 293 292\n\t\tf 4 260 641 -281 -641\n\t\tmu 0 4 273 274 295 294\n\t\tf 4 261 642 -282 -642\n\t\tmu 0 4 274 275 296 295\n\t\tf 4 262 643 -283 -643\n\t\tmu 0 4 275 276 297 296\n\t\tf 4 263 644 -284 -644\n\t\tmu 0 4 276 277 298 297\n\t\tf 4 264 645 -285 -645\n\t\tmu 0 4 277 278 299 298\n\t\tf 4 265 646 -286 -646\n\t\tmu 0 4 278 279 300 299\n\t\tf 4 266 647 -287 -647\n\t\tmu 0 4 279 280 301 300\n\t\tf 4 267 648 -288 -648\n\t\tmu 0 4 280 281 302 301\n\t\tf 4 268 649 -289 -649\n\t\tmu 0 4 281 282 303 302\n\t\tf 4 269 650 -290 -650\n\t\tmu 0 4 282 283 304 303\n\t\tf 4 270 651 -291 -651\n\t\tmu 0 4 283 284 305 304\n\t\tf 4 271 652 -292 -652\n\t\tmu 0 4 284 285 306 305\n\t\tf 4 272 653 -293 -653\n\t\tmu 0 4 285 286 307 306\n\t\tf 4 273 654 -294 -654\n\t\tmu 0 4 286 287 308 307\n\t\tf 4 274 655 -295 -655\n\t\tmu 0 4 287 288 309 308\n\t\tf 4 275 656 -296 -656\n\t\tmu 0 4 288 289 310 309\n\t\tf 4 276 657 -297 -657\n\t\tmu 0 4 289 290 311 310\n\t\tf 4 277 658 -298 -658\n\t\tmu 0 4 290 291 312 311\n\t\tf 4 278 659 -299 -659\n\t\tmu 0 4 291 292 313 312\n\t\tf 4 279 640 -300 -660\n\t\tmu 0 4 292 293 314 313\n\t\tf 4 280 661 -301 -661\n\t\tmu 0 4 294 295 316 315\n\t\tf 4 281 662 -302 -662\n\t\tmu 0 4 295 296 317 316\n\t\tf 4 282 663 -303 -663\n\t\tmu 0 4 296 297 318 317\n\t\tf 4 283 664 -304 -664\n\t\tmu 0 4 297 298 319 318\n\t\tf 4 284 665 -305 -665\n\t\tmu 0 4 298 299 320 319\n\t\tf 4 285 666 -306 -666\n\t\tmu 0 4 299 300 321 320\n\t\tf 4 286 667 -307 -667\n\t\tmu 0 4 300 301 322 321\n\t\tf 4 287 668 -308 -668\n\t\tmu 0 4 301 302 323 322\n\t\tf 4 288 669 -309 -669\n\t\tmu 0 4 302 303 324 323\n\t\tf 4 289 670 -310 -670\n\t\tmu 0 4 303 304 325 324\n\t\tf 4 290 671 -311 -671\n\t\tmu 0 4 304 305 326 325\n\t\tf 4 291 672 -312 -672\n\t\tmu 0 4 305 306 327 326\n\t\tf 4 292 673 -313 -673\n\t\tmu 0 4 306 307 328 327\n\t\tf 4 293 674 -314 -674\n\t\tmu 0 4 307 308 329 328\n\t\tf 4 294 675 -315 -675\n\t\tmu 0 4 308 309 330 329\n\t\tf 4 295 676 -316 -676\n\t\tmu 0 4 309 310 331 330\n\t\tf 4 296 677 -317 -677\n\t\tmu 0 4 310 311 332 331\n\t\tf 4 297 678 -318 -678\n\t\tmu 0 4 311 312 333 332\n\t\tf 4 298 679 -319 -679\n\t\tmu 0 4 312 313 334 333\n\t\tf 4 299 660 -320 -680\n\t\tmu 0 4 313 314 335 334\n\t\tf 4 300 681 -321 -681\n\t\tmu 0 4 315 316 337 336\n\t\tf 4 301 682 -322 -682\n\t\tmu 0 4 316 317 338 337\n\t\tf 4 302 683 -323 -683\n\t\tmu 0 4 317 318 339 338\n\t\tf 4 303 684 -324 -684\n\t\tmu 0 4 318 319 340 339\n\t\tf 4 304 685 -325 -685\n\t\tmu 0 4 319 320 341 340\n\t\tf 4 305 686 -326 -686\n\t\tmu 0 4 320 321 342 341\n\t\tf 4 306 687 -327 -687\n\t\tmu 0 4 321 322 343 342\n\t\tf 4 307 688 -328 -688\n\t\tmu 0 4 322 323 344 343\n\t\tf 4 308 689 -329 -689\n\t\tmu 0 4 323 324 345 344\n\t\tf 4 309 690 -330 -690\n\t\tmu 0 4 324 325 346 345\n\t\tf 4 310 691 -331 -691\n\t\tmu 0 4 325 326 347 346\n\t\tf 4 311 692 -332 -692\n\t\tmu 0 4 326 327 348 347\n\t\tf 4 312 693 -333 -693\n\t\tmu 0 4 327 328 349 348\n\t\tf 4 313 694 -334 -694\n\t\tmu 0 4 328 329 350 349\n\t\tf 4 314 695 -335 -695\n\t\tmu 0 4 329 330 351 350\n\t\tf 4 315 696 -336 -696\n\t\tmu 0 4 330 331 352 351\n\t\tf 4 316 697 -337 -697\n\t\tmu 0 4 331 332 353 352\n\t\tf 4 317 698 -338 -698\n\t\tmu 0 4 332 333 354 353\n\t\tf 4 318 699 -339 -699\n\t\tmu 0 4 333 334 355 354\n\t\tf 4 319 680 -340 -700\n\t\tmu 0 4 334 335 356 355\n\t\tf 4 320 701 -341 -701\n\t\tmu 0 4 336 337 358 357\n\t\tf 4 321 702 -342 -702\n\t\tmu 0 4 337 338 359 358\n\t\tf 4 322 703 -343 -703\n\t\tmu 0 4 338 339 360 359\n\t\tf 4 323 704 -344 -704\n\t\tmu 0 4 339 340 361 360\n\t\tf 4 324 705 -345 -705\n\t\tmu 0 4 340 341 362 361\n\t\tf 4 325 706 -346 -706\n\t\tmu 0 4 341 342 363 362\n\t\tf 4 326 707 -347 -707\n\t\tmu 0 4 342 343 364 363\n\t\tf 4 327 708 -348 -708\n\t\tmu 0 4 343 344 365 364\n\t\tf 4 328 709 -349 -709\n\t\tmu 0 4 344 345 366 365\n\t\tf 4 329 710 -350 -710\n\t\tmu 0 4 345 346 367 366\n\t\tf 4 330 711 -351 -711\n\t\tmu 0 4 346 347 368 367\n\t\tf 4 331 712 -352 -712\n\t\tmu 0 4 347 348 369 368\n\t\tf 4 332 713 -353 -713\n\t\tmu 0 4 348 349 370 369\n\t\tf 4 333 714 -354 -714\n\t\tmu 0 4 349 350 371 370\n\t\tf 4 334 715 -355 -715\n\t\tmu 0 4 350 351 372 371\n\t\tf 4 335 716 -356 -716\n\t\tmu 0 4 351 352 373 372\n\t\tf 4 336 717 -357 -717\n\t\tmu 0 4 352 353 374 373\n\t\tf 4 337 718 -358 -718\n\t\tmu 0 4 353 354 375 374\n\t\tf 4 338 719 -359 -719\n\t\tmu 0 4 354 355 376 375\n\t\tf 4 339 700 -360 -720\n\t\tmu 0 4 355 356 377 376\n\t\tf 4 340 721 -361 -721\n\t\tmu 0 4 357 358 379 378\n\t\tf 4 341 722 -362 -722\n\t\tmu 0 4 358 359 380 379\n\t\tf 4 342 723 -363 -723\n\t\tmu 0 4 359 360 381 380\n\t\tf 4 343 724 -364 -724\n\t\tmu 0 4 360 361 382 381\n\t\tf 4 344 725 -365 -725\n\t\tmu 0 4 361 362 383 382\n\t\tf 4 345 726 -366 -726\n\t\tmu 0 4 362 363 384 383\n\t\tf 4 346 727 -367 -727\n\t\tmu 0 4 363 364 385 384\n\t\tf 4 347 728 -368 -728\n\t\tmu 0 4 364 365 386 385\n\t\tf 4 348 729 -369 -729\n\t\tmu 0 4 365 366 387 386\n\t\tf 4 349 730 -370 -730\n\t\tmu 0 4 366 367 388 387\n\t\tf 4 350 731 -371 -731\n\t\tmu 0 4 367 368 389 388\n\t\tf 4 351 732 -372 -732\n\t\tmu 0 4 368 369 390 389\n\t\tf 4 352 733 -373 -733\n\t\tmu 0 4 369 370 391 390\n\t\tf 4 353 734 -374 -734\n\t\tmu 0 4 370 371 392 391\n\t\tf 4 354 735 -375 -735\n\t\tmu 0 4 371 372 393 392\n\t\tf 4 355 736 -376 -736\n\t\tmu 0 4 372 373 394 393\n\t\tf 4 356 737 -377 -737\n\t\tmu 0 4 373 374 395 394\n\t\tf 4 357 738 -378 -738\n\t\tmu 0 4 374 375 396 395\n\t\tf 4 358 739 -379 -739\n\t\tmu 0 4 375 376 397 396\n\t\tf 4 359 720 -380 -740\n\t\tmu 0 4 376 377 398 397\n\t\tf 3 -1 -741 741\n\t\tmu 0 3 1 0 399\n\t\tf 3 -2 -742 742\n\t\tmu 0 3 2 1 400\n\t\tf 3 -3 -743 743\n\t\tmu 0 3 3 2 401\n\t\tf 3 -4 -744 744\n\t\tmu 0 3 4 3 402\n\t\tf 3 -5 -745 745\n\t\tmu 0 3 5 4 403\n\t\tf 3 -6 -746 746\n\t\tmu 0 3 6 5 404\n\t\tf 3 -7 -747 747\n\t\tmu 0 3 7 6 405\n\t\tf 3 -8 -748 748\n\t\tmu 0 3 8 7 406\n\t\tf 3 -9 -749 749\n\t\tmu 0 3 9 8 407\n\t\tf 3 -10 -750 750\n\t\tmu 0 3 10 9 408\n\t\tf 3 -11 -751 751\n\t\tmu 0 3 11 10 409\n\t\tf 3 -12 -752 752\n\t\tmu 0 3 12 11 410\n\t\tf 3 -13 -753 753\n\t\tmu 0 3 13 12 411\n\t\tf 3 -14 -754 754\n\t\tmu 0 3 14 13 412\n\t\tf 3 -15 -755 755\n\t\tmu 0 3 15 14 413\n\t\tf 3 -16 -756 756\n\t\tmu 0 3 16 15 414\n\t\tf 3 -17 -757 757\n\t\tmu 0 3 17 16 415\n\t\tf 3 -18 -758 758\n\t\tmu 0 3 18 17 416\n\t\tf 3 -19 -759 759\n\t\tmu 0 3 19 18 417\n\t\tf 3 -20 -760 740\n\t\tmu 0 3 20 19 418\n\t\tf 3 360 761 -761\n\t\tmu 0 3 378 379 419\n\t\tf 3 361 762 -762\n\t\tmu 0 3 379 380 420\n\t\tf 3 362 763 -763\n\t\tmu 0 3 380 381 421\n\t\tf 3 363 764 -764\n\t\tmu 0 3 381 382 422\n\t\tf 3 364 765 -765\n\t\tmu 0 3 382 383 423\n\t\tf 3 365 766 -766\n\t\tmu 0 3 383 384 424\n\t\tf 3 366 767 -767\n\t\tmu 0 3 384 385 425\n\t\tf 3 367 768 -768\n\t\tmu 0 3 385 386 426\n\t\tf 3 368 769 -769\n\t\tmu 0 3 386 387 427\n\t\tf 3 369 770 -770\n\t\tmu 0 3 387 388 428\n\t\tf 3 370 771 -771\n\t\tmu 0 3 388 389 429\n\t\tf 3 371 772 -772\n\t\tmu 0 3 389 390 430\n\t\tf 3 372 773 -773\n\t\tmu 0 3 390 391 431\n\t\tf 3 373 774 -774\n\t\tmu 0 3 391 392 432\n\t\tf 3 374 775 -775\n\t\tmu 0 3 392 393 433\n\t\tf 3 375 776 -776\n\t\tmu 0 3 393 394 434\n\t\tf 3 376 777 -777\n\t\tmu 0 3 394 395 435\n\t\tf 3 377 778 -778\n\t\tmu 0 3 395 396 436\n\t\tf 3 378 779 -779\n\t\tmu 0 3 396 397 437\n\t\tf 3 379 760 -780\n\t\tmu 0 3 397 398 438;\n\tsetAttr \".cd\" -type \"dataPolyComponent\" Index_Data Edge 0 ;\n\tsetAttr \".cvd\" -type \"dataPolyComponent\" Index_Data Vertex 0 ;\n\tsetAttr \".pd[0]\" -type \"dataPolyComponent\" Index_Data UV 0 ;\n\tsetAttr \".hfd\" -type \"dataPolyComponent\" Index_Data Face 0 ;\n\tsetAttr \".dr\" 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:6c77a15a98a9\";\nselect -ne :time1;\n\tsetAttr \".o\" 1001;\n\tsetAttr \".unw\" 1001;\nselect -ne :hardwareRenderingGlobals;\n\tsetAttr \".otfna\" -type \"stringArray\" 22 \"NURBS Curves\" \"NURBS Surfaces\" \"Polygons\" \"Subdiv Surface\" \"Particles\" \"Particle Instance\" \"Fluids\" \"Strokes\" \"Image Planes\" \"UI\" \"Lights\" \"Cameras\" \"Locators\" \"Joints\" \"IK Handles\" \"Deformers\" \"Motion Trails\" \"Components\" \"Hair Systems\" \"Follicles\" \"Misc. UI\" \"Ornaments\"  ;\n\tsetAttr \".otfva\" -type \"Int32Array\" 22 0 1 1 1 1 1\n\t\t 1 1 1 0 0 0 0 0 0 0 0 0\n\t\t 0 0 0 0 ;\n\tsetAttr \".fprt\" yes;\nselect -ne :renderPartition;\n\tsetAttr -s 2 \".st\";\nselect -ne :renderGlobalsList1;\nselect -ne :defaultShaderList1;\n\tsetAttr -s 5 \".s\";\nselect -ne :postProcessList1;\n\tsetAttr -s 2 \".p\";\nselect -ne :defaultRenderingList1;\n\tsetAttr -s 2 \".r\";\nselect -ne :lightList1;\nselect -ne :initialShadingGroup;\n\tsetAttr -s 2 \".dsm\";\n\tsetAttr \".ro\" yes;\nselect -ne :initialParticleSE;\n\tsetAttr \".ro\" yes;\nselect -ne :defaultRenderGlobals;\n\taddAttr -ci true -h true -sn \"dss\" -ln \"defaultSurfaceShader\" -dt \"string\";\n\tsetAttr \".ren\" -type \"string\" \"arnold\";\n\tsetAttr \".outf\" 51;\n\tsetAttr \".imfkey\" -type \"string\" \"exr\";\n\tsetAttr \".an\" yes;\n\tsetAttr \".fs\" 1001;\n\tsetAttr \".ef\" 1001;\n\tsetAttr \".oft\" -type \"string\" \"\";\n\tsetAttr \".pff\" yes;\n\tsetAttr \".ifp\" -type \"string\" \"<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>\";\n\tsetAttr \".rv\" -type \"string\" \"\";\n\tsetAttr \".pram\" -type \"string\" \"\";\n\tsetAttr \".poam\" -type \"string\" \"\";\n\tsetAttr \".prlm\" -type \"string\" \"\";\n\tsetAttr \".polm\" -type \"string\" \"\";\n\tsetAttr \".prm\" -type \"string\" \"\";\n\tsetAttr \".pom\" -type \"string\" \"\";\n\tsetAttr \".dss\" -type \"string\" \"lambert1\";\nselect -ne :defaultResolution;\n\tsetAttr \".w\" 1920;\n\tsetAttr \".h\" 1080;\n\tsetAttr \".pa\" 1;\n\tsetAttr \".dar\" 1.7777777910232544;\nselect -ne :defaultLightSet;\nselect -ne :hardwareRenderGlobals;\n\tsetAttr \".ctrs\" 256;\n\tsetAttr \".btrs\" 512;\nconnectAttr \"pSphere1_GEOShape1.iog\" \":initialShadingGroup.dsm\" -na;\n// End of modelMain.ma\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/v001/test_project_test_asset_modelMain_v001.ma",
    "content": "//Maya ASCII 2020 scene\n//Name: modelMain.ma\n//Last modified: Mon, Oct 24, 2022 02:57:47 PM\n//Codeset: 1252\nrequires maya \"2020\";\nrequires \"mtoa\" \"4.1.1\";\ncurrentUnit -l centimeter -a degree -t pal;\nfileInfo \"application\" \"maya\";\nfileInfo \"product\" \"Maya 2020\";\nfileInfo \"version\" \"2020\";\nfileInfo \"cutIdentifier\" \"202011110415-b1e20b88e2\";\nfileInfo \"osv\" \"Microsoft Windows 10 Technical Preview  (Build 19044)\\n\";\nfileInfo \"UUID\" \"A787A358-4FE7-6E55-0C81-61BFEB0C2726\";\ncreateNode transform -n \"model_GRP\";\n\trename -uid \"445FDC20-4A9D-2C5B-C7BD-F98F6E660B5C\";\n\tsetAttr \".rlio[0]\" 1 yes 0;\ncreateNode transform -n \"pSphere1_GEO\" -p \"model_GRP\";\n\trename -uid \"7445A43F-444F-B2D3-4315-2AA013D2E0B6\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:302a4c6123a4\";\ncreateNode mesh -n \"pSphere1_GEOShape1\" -p \"pSphere1_GEO\";\n\trename -uid \"7C731260-45C6-339E-07BF-359446B08EA1\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr -s 439 \".uvst[0].uvsp\";\n\tsetAttr \".uvst[0].uvsp[0:249]\" -type \"float2\" 0 0.050000001 0.050000001 0.050000001\n\t\t 0.1 0.050000001 0.15000001 0.050000001 0.2 0.050000001 0.25 0.050000001 0.30000001\n\t\t 0.050000001 0.35000002 0.050000001 0.40000004 0.050000001 0.45000005 0.050000001\n\t\t 0.50000006 0.050000001 0.55000007 0.050000001 0.60000008 0.050000001 0.6500001 0.050000001\n\t\t 0.70000011 0.050000001 0.75000012 0.050000001 0.80000013 0.050000001 0.85000014 0.050000001\n\t\t 0.90000015 0.050000001 0.95000017 0.050000001 1.000000119209 0.050000001 0 0.1 0.050000001\n\t\t 0.1 0.1 0.1 0.15000001 0.1 0.2 0.1 0.25 0.1 0.30000001 0.1 0.35000002 0.1 0.40000004\n\t\t 0.1 0.45000005 0.1 0.50000006 0.1 0.55000007 0.1 0.60000008 0.1 0.6500001 0.1 0.70000011\n\t\t 0.1 0.75000012 0.1 0.80000013 0.1 0.85000014 0.1 0.90000015 0.1 0.95000017 0.1 1.000000119209\n\t\t 0.1 0 0.15000001 0.050000001 0.15000001 0.1 0.15000001 0.15000001 0.15000001 0.2\n\t\t 0.15000001 0.25 0.15000001 0.30000001 0.15000001 0.35000002 0.15000001 0.40000004\n\t\t 0.15000001 0.45000005 0.15000001 0.50000006 0.15000001 0.55000007 0.15000001 0.60000008\n\t\t 0.15000001 0.6500001 0.15000001 0.70000011 0.15000001 0.75000012 0.15000001 0.80000013\n\t\t 0.15000001 0.85000014 0.15000001 0.90000015 0.15000001 0.95000017 0.15000001 1.000000119209\n\t\t 0.15000001 0 0.2 0.050000001 0.2 0.1 0.2 0.15000001 0.2 0.2 0.2 0.25 0.2 0.30000001\n\t\t 0.2 0.35000002 0.2 0.40000004 0.2 0.45000005 0.2 0.50000006 0.2 0.55000007 0.2 0.60000008\n\t\t 0.2 0.6500001 0.2 0.70000011 0.2 0.75000012 0.2 0.80000013 0.2 0.85000014 0.2 0.90000015\n\t\t 0.2 0.95000017 0.2 1.000000119209 0.2 0 0.25 0.050000001 0.25 0.1 0.25 0.15000001\n\t\t 0.25 0.2 0.25 0.25 0.25 0.30000001 0.25 0.35000002 0.25 0.40000004 0.25 0.45000005\n\t\t 0.25 0.50000006 0.25 0.55000007 0.25 0.60000008 0.25 0.6500001 0.25 0.70000011 0.25\n\t\t 0.75000012 0.25 0.80000013 0.25 0.85000014 0.25 0.90000015 0.25 0.95000017 0.25 1.000000119209\n\t\t 0.25 0 0.30000001 0.050000001 0.30000001 0.1 0.30000001 0.15000001 0.30000001 0.2\n\t\t 0.30000001 0.25 0.30000001 0.30000001 0.30000001 0.35000002 0.30000001 0.40000004\n\t\t 0.30000001 0.45000005 0.30000001 0.50000006 0.30000001 0.55000007 0.30000001 0.60000008\n\t\t 0.30000001 0.6500001 0.30000001 0.70000011 0.30000001 0.75000012 0.30000001 0.80000013\n\t\t 0.30000001 0.85000014 0.30000001 0.90000015 0.30000001 0.95000017 0.30000001 1.000000119209\n\t\t 0.30000001 0 0.35000002 0.050000001 0.35000002 0.1 0.35000002 0.15000001 0.35000002\n\t\t 0.2 0.35000002 0.25 0.35000002 0.30000001 0.35000002 0.35000002 0.35000002 0.40000004\n\t\t 0.35000002 0.45000005 0.35000002 0.50000006 0.35000002 0.55000007 0.35000002 0.60000008\n\t\t 0.35000002 0.6500001 0.35000002 0.70000011 0.35000002 0.75000012 0.35000002 0.80000013\n\t\t 0.35000002 0.85000014 0.35000002 0.90000015 0.35000002 0.95000017 0.35000002 1.000000119209\n\t\t 0.35000002 0 0.40000004 0.050000001 0.40000004 0.1 0.40000004 0.15000001 0.40000004\n\t\t 0.2 0.40000004 0.25 0.40000004 0.30000001 0.40000004 0.35000002 0.40000004 0.40000004\n\t\t 0.40000004 0.45000005 0.40000004 0.50000006 0.40000004 0.55000007 0.40000004 0.60000008\n\t\t 0.40000004 0.6500001 0.40000004 0.70000011 0.40000004 0.75000012 0.40000004 0.80000013\n\t\t 0.40000004 0.85000014 0.40000004 0.90000015 0.40000004 0.95000017 0.40000004 1.000000119209\n\t\t 0.40000004 0 0.45000005 0.050000001 0.45000005 0.1 0.45000005 0.15000001 0.45000005\n\t\t 0.2 0.45000005 0.25 0.45000005 0.30000001 0.45000005 0.35000002 0.45000005 0.40000004\n\t\t 0.45000005 0.45000005 0.45000005 0.50000006 0.45000005 0.55000007 0.45000005 0.60000008\n\t\t 0.45000005 0.6500001 0.45000005 0.70000011 0.45000005 0.75000012 0.45000005 0.80000013\n\t\t 0.45000005 0.85000014 0.45000005 0.90000015 0.45000005 0.95000017 0.45000005 1.000000119209\n\t\t 0.45000005 0 0.50000006 0.050000001 0.50000006 0.1 0.50000006 0.15000001 0.50000006\n\t\t 0.2 0.50000006 0.25 0.50000006 0.30000001 0.50000006 0.35000002 0.50000006 0.40000004\n\t\t 0.50000006 0.45000005 0.50000006 0.50000006 0.50000006 0.55000007 0.50000006 0.60000008\n\t\t 0.50000006 0.6500001 0.50000006 0.70000011 0.50000006 0.75000012 0.50000006 0.80000013\n\t\t 0.50000006 0.85000014 0.50000006 0.90000015 0.50000006 0.95000017 0.50000006 1.000000119209\n\t\t 0.50000006 0 0.55000007 0.050000001 0.55000007 0.1 0.55000007 0.15000001 0.55000007\n\t\t 0.2 0.55000007 0.25 0.55000007 0.30000001 0.55000007 0.35000002 0.55000007 0.40000004\n\t\t 0.55000007 0.45000005 0.55000007 0.50000006 0.55000007 0.55000007 0.55000007 0.60000008\n\t\t 0.55000007 0.6500001 0.55000007 0.70000011 0.55000007 0.75000012 0.55000007 0.80000013\n\t\t 0.55000007 0.85000014 0.55000007 0.90000015 0.55000007 0.95000017 0.55000007 1.000000119209\n\t\t 0.55000007 0 0.60000008 0.050000001 0.60000008 0.1 0.60000008 0.15000001 0.60000008\n\t\t 0.2 0.60000008 0.25 0.60000008 0.30000001 0.60000008 0.35000002 0.60000008 0.40000004\n\t\t 0.60000008 0.45000005 0.60000008 0.50000006 0.60000008 0.55000007 0.60000008 0.60000008\n\t\t 0.60000008 0.6500001 0.60000008 0.70000011 0.60000008 0.75000012 0.60000008 0.80000013\n\t\t 0.60000008 0.85000014 0.60000008 0.90000015 0.60000008;\n\tsetAttr \".uvst[0].uvsp[250:438]\" 0.95000017 0.60000008 1.000000119209 0.60000008\n\t\t 0 0.6500001 0.050000001 0.6500001 0.1 0.6500001 0.15000001 0.6500001 0.2 0.6500001\n\t\t 0.25 0.6500001 0.30000001 0.6500001 0.35000002 0.6500001 0.40000004 0.6500001 0.45000005\n\t\t 0.6500001 0.50000006 0.6500001 0.55000007 0.6500001 0.60000008 0.6500001 0.6500001\n\t\t 0.6500001 0.70000011 0.6500001 0.75000012 0.6500001 0.80000013 0.6500001 0.85000014\n\t\t 0.6500001 0.90000015 0.6500001 0.95000017 0.6500001 1.000000119209 0.6500001 0 0.70000011\n\t\t 0.050000001 0.70000011 0.1 0.70000011 0.15000001 0.70000011 0.2 0.70000011 0.25 0.70000011\n\t\t 0.30000001 0.70000011 0.35000002 0.70000011 0.40000004 0.70000011 0.45000005 0.70000011\n\t\t 0.50000006 0.70000011 0.55000007 0.70000011 0.60000008 0.70000011 0.6500001 0.70000011\n\t\t 0.70000011 0.70000011 0.75000012 0.70000011 0.80000013 0.70000011 0.85000014 0.70000011\n\t\t 0.90000015 0.70000011 0.95000017 0.70000011 1.000000119209 0.70000011 0 0.75000012\n\t\t 0.050000001 0.75000012 0.1 0.75000012 0.15000001 0.75000012 0.2 0.75000012 0.25 0.75000012\n\t\t 0.30000001 0.75000012 0.35000002 0.75000012 0.40000004 0.75000012 0.45000005 0.75000012\n\t\t 0.50000006 0.75000012 0.55000007 0.75000012 0.60000008 0.75000012 0.6500001 0.75000012\n\t\t 0.70000011 0.75000012 0.75000012 0.75000012 0.80000013 0.75000012 0.85000014 0.75000012\n\t\t 0.90000015 0.75000012 0.95000017 0.75000012 1.000000119209 0.75000012 0 0.80000013\n\t\t 0.050000001 0.80000013 0.1 0.80000013 0.15000001 0.80000013 0.2 0.80000013 0.25 0.80000013\n\t\t 0.30000001 0.80000013 0.35000002 0.80000013 0.40000004 0.80000013 0.45000005 0.80000013\n\t\t 0.50000006 0.80000013 0.55000007 0.80000013 0.60000008 0.80000013 0.6500001 0.80000013\n\t\t 0.70000011 0.80000013 0.75000012 0.80000013 0.80000013 0.80000013 0.85000014 0.80000013\n\t\t 0.90000015 0.80000013 0.95000017 0.80000013 1.000000119209 0.80000013 0 0.85000014\n\t\t 0.050000001 0.85000014 0.1 0.85000014 0.15000001 0.85000014 0.2 0.85000014 0.25 0.85000014\n\t\t 0.30000001 0.85000014 0.35000002 0.85000014 0.40000004 0.85000014 0.45000005 0.85000014\n\t\t 0.50000006 0.85000014 0.55000007 0.85000014 0.60000008 0.85000014 0.6500001 0.85000014\n\t\t 0.70000011 0.85000014 0.75000012 0.85000014 0.80000013 0.85000014 0.85000014 0.85000014\n\t\t 0.90000015 0.85000014 0.95000017 0.85000014 1.000000119209 0.85000014 0 0.90000015\n\t\t 0.050000001 0.90000015 0.1 0.90000015 0.15000001 0.90000015 0.2 0.90000015 0.25 0.90000015\n\t\t 0.30000001 0.90000015 0.35000002 0.90000015 0.40000004 0.90000015 0.45000005 0.90000015\n\t\t 0.50000006 0.90000015 0.55000007 0.90000015 0.60000008 0.90000015 0.6500001 0.90000015\n\t\t 0.70000011 0.90000015 0.75000012 0.90000015 0.80000013 0.90000015 0.85000014 0.90000015\n\t\t 0.90000015 0.90000015 0.95000017 0.90000015 1.000000119209 0.90000015 0 0.95000017\n\t\t 0.050000001 0.95000017 0.1 0.95000017 0.15000001 0.95000017 0.2 0.95000017 0.25 0.95000017\n\t\t 0.30000001 0.95000017 0.35000002 0.95000017 0.40000004 0.95000017 0.45000005 0.95000017\n\t\t 0.50000006 0.95000017 0.55000007 0.95000017 0.60000008 0.95000017 0.6500001 0.95000017\n\t\t 0.70000011 0.95000017 0.75000012 0.95000017 0.80000013 0.95000017 0.85000014 0.95000017\n\t\t 0.90000015 0.95000017 0.95000017 0.95000017 1.000000119209 0.95000017 0.025 0 0.075000003\n\t\t 0 0.125 0 0.17500001 0 0.22500001 0 0.27500001 0 0.32500002 0 0.375 0 0.42500001\n\t\t 0 0.47500002 0 0.52499998 0 0.57499999 0 0.625 0 0.67500001 0 0.72499996 0 0.77499998\n\t\t 0 0.82499999 0 0.875 0 0.92500001 0 0.97499996 0 0.025 1 0.075000003 1 0.125 1 0.17500001\n\t\t 1 0.22500001 1 0.27500001 1 0.32500002 1 0.375 1 0.42500001 1 0.47500002 1 0.52499998\n\t\t 1 0.57499999 1 0.625 1 0.67500001 1 0.72499996 1 0.77499998 1 0.82499999 1 0.875\n\t\t 1 0.92500001 1 0.97499996 1;\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr -s 382 \".vt\";\n\tsetAttr \".vt[0:165]\"  0.14877813 -0.98768836 -0.048340943 0.12655823 -0.98768836 -0.091949932\n\t\t 0.091949932 -0.98768836 -0.12655823 0.048340935 -0.98768836 -0.14877811 0 -0.98768836 -0.15643455\n\t\t -0.048340935 -0.98768836 -0.1487781 -0.091949917 -0.98768836 -0.1265582 -0.12655818 -0.98768836 -0.091949902\n\t\t -0.14877807 -0.98768836 -0.048340924 -0.15643452 -0.98768836 0 -0.14877807 -0.98768836 0.048340924\n\t\t -0.12655818 -0.98768836 0.091949895 -0.091949895 -0.98768836 0.12655817 -0.048340924 -0.98768836 0.14877805\n\t\t -4.6621107e-09 -0.98768836 0.15643449 0.048340909 -0.98768836 0.14877804 0.09194988 -0.98768836 0.12655815\n\t\t 0.12655815 -0.98768836 0.091949888 0.14877804 -0.98768836 0.048340913 0.15643448 -0.98768836 0\n\t\t 0.29389283 -0.95105654 -0.095491566 0.25000018 -0.95105654 -0.18163574 0.18163574 -0.95105654 -0.25000015\n\t\t 0.095491551 -0.95105654 -0.2938928 0 -0.95105654 -0.30901715 -0.095491551 -0.95105654 -0.29389277\n\t\t -0.18163571 -0.95105654 -0.25000009 -0.25000009 -0.95105654 -0.18163569 -0.29389271 -0.95105654 -0.095491529\n\t\t -0.30901706 -0.95105654 0 -0.29389271 -0.95105654 0.095491529 -0.25000006 -0.95105654 0.18163568\n\t\t -0.18163568 -0.95105654 0.25000006 -0.095491529 -0.95105654 0.29389268 -9.2094243e-09 -0.95105654 0.30901703\n\t\t 0.095491499 -0.95105654 0.29389265 0.18163563 -0.95105654 0.25000003 0.25 -0.95105654 0.18163565\n\t\t 0.29389265 -0.95105654 0.095491506 0.309017 -0.95105654 0 0.43177092 -0.89100653 -0.14029087\n\t\t 0.36728629 -0.89100653 -0.2668491 0.2668491 -0.89100653 -0.36728626 0.14029086 -0.89100653 -0.43177086\n\t\t 0 -0.89100653 -0.45399073 -0.14029086 -0.89100653 -0.43177083 -0.26684904 -0.89100653 -0.36728618\n\t\t -0.36728615 -0.89100653 -0.26684901 -0.43177077 -0.89100653 -0.14029081 -0.45399064 -0.89100653 0\n\t\t -0.43177077 -0.89100653 0.14029081 -0.36728612 -0.89100653 0.26684898 -0.26684898 -0.89100653 0.36728612\n\t\t -0.14029081 -0.89100653 0.43177071 -1.3529972e-08 -0.89100653 0.45399058 0.14029078 -0.89100653 0.43177068\n\t\t 0.26684892 -0.89100653 0.36728609 0.36728606 -0.89100653 0.26684895 0.43177065 -0.89100653 0.1402908\n\t\t 0.45399052 -0.89100653 0 0.55901736 -0.809017 -0.18163574 0.47552857 -0.809017 -0.34549171\n\t\t 0.34549171 -0.809017 -0.47552854 0.18163572 -0.809017 -0.5590173 0 -0.809017 -0.58778554\n\t\t -0.18163572 -0.809017 -0.55901724 -0.34549165 -0.809017 -0.47552842 -0.47552839 -0.809017 -0.34549159\n\t\t -0.55901712 -0.809017 -0.18163566 -0.58778536 -0.809017 0 -0.55901712 -0.809017 0.18163566\n\t\t -0.47552836 -0.809017 0.34549156 -0.34549156 -0.809017 0.47552833 -0.18163566 -0.809017 0.55901706\n\t\t -1.7517365e-08 -0.809017 0.5877853 0.18163562 -0.809017 0.55901706 0.3454915 -0.809017 0.4755283\n\t\t 0.47552827 -0.809017 0.34549153 0.559017 -0.809017 0.18163563 0.58778524 -0.809017 0\n\t\t 0.67249894 -0.70710677 -0.21850814 0.57206178 -0.70710677 -0.41562718 0.41562718 -0.70710677 -0.57206172\n\t\t 0.21850812 -0.70710677 -0.67249888 0 -0.70710677 -0.70710713 -0.21850812 -0.70710677 -0.67249882\n\t\t -0.41562709 -0.70710677 -0.5720616 -0.57206154 -0.70710677 -0.41562706 -0.6724987 -0.70710677 -0.21850805\n\t\t -0.70710695 -0.70710677 0 -0.6724987 -0.70710677 0.21850805 -0.57206154 -0.70710677 0.415627\n\t\t -0.415627 -0.70710677 0.57206148 -0.21850805 -0.70710677 0.67249858 -2.1073424e-08 -0.70710677 0.70710683\n\t\t 0.21850799 -0.70710677 0.67249858 0.41562691 -0.70710677 0.57206142 0.57206142 -0.70710677 0.41562697\n\t\t 0.67249852 -0.70710677 0.21850802 0.70710677 -0.70710677 0 0.7694214 -0.58778524 -0.25000015\n\t\t 0.65450895 -0.58778524 -0.47552854 0.47552854 -0.58778524 -0.65450889 0.25000012 -0.58778524 -0.76942128\n\t\t 0 -0.58778524 -0.80901736 -0.25000012 -0.58778524 -0.76942122 -0.47552845 -0.58778524 -0.65450877\n\t\t -0.65450871 -0.58778524 -0.47552839 -0.7694211 -0.58778524 -0.25000006 -0.80901718 -0.58778524 0\n\t\t -0.7694211 -0.58778524 0.25000006 -0.65450865 -0.58778524 0.47552836 -0.47552836 -0.58778524 0.65450859\n\t\t -0.25000006 -0.58778524 0.76942098 -2.4110586e-08 -0.58778524 0.80901712 0.24999999 -0.58778524 0.76942098\n\t\t 0.47552827 -0.58778524 0.65450853 0.65450853 -0.58778524 0.4755283 0.76942092 -0.58778524 0.25\n\t\t 0.809017 -0.58778524 0 0.8473981 -0.45399052 -0.27533633 0.72083992 -0.45399052 -0.5237208\n\t\t 0.5237208 -0.45399052 -0.72083986 0.2753363 -0.45399052 -0.84739798 0 -0.45399052 -0.89100695\n\t\t -0.2753363 -0.45399052 -0.84739798 -0.52372068 -0.45399052 -0.72083968 -0.72083962 -0.45399052 -0.52372062\n\t\t -0.8473978 -0.45399052 -0.27533621 -0.89100677 -0.45399052 0 -0.8473978 -0.45399052 0.27533621\n\t\t -0.72083962 -0.45399052 0.52372062 -0.52372062 -0.45399052 0.72083956 -0.27533621 -0.45399052 0.84739769\n\t\t -2.6554064e-08 -0.45399052 0.89100665 0.27533615 -0.45399052 0.84739763 0.5237205 -0.45399052 0.7208395\n\t\t 0.72083944 -0.45399052 0.52372056 0.84739757 -0.45399052 0.27533618 0.89100653 -0.45399052 0\n\t\t 0.90450913 -0.30901697 -0.2938928 0.7694214 -0.30901697 -0.55901736 0.55901736 -0.30901697 -0.76942134\n\t\t 0.29389277 -0.30901697 -0.90450901 0 -0.30901697 -0.95105702 -0.29389277 -0.30901697 -0.90450895\n\t\t -0.55901724 -0.30901697 -0.76942122 -0.76942116 -0.30901697 -0.55901718 -0.90450877 -0.30901697 -0.29389271\n\t\t -0.95105678 -0.30901697 0 -0.90450877 -0.30901697 0.29389271 -0.7694211 -0.30901697 0.55901712\n\t\t -0.55901712 -0.30901697 0.76942104 -0.29389271 -0.30901697 0.90450865 -2.8343694e-08 -0.30901697 0.95105666\n\t\t 0.29389262 -0.30901697 0.90450859 0.559017 -0.30901697 0.76942098 0.76942092 -0.30901697 0.55901706\n\t\t 0.90450853 -0.30901697 0.29389265 0.95105654 -0.30901697 0 0.93934804 -0.15643437 -0.30521268\n\t\t 0.79905719 -0.15643437 -0.580549 0.580549 -0.15643437 -0.79905713 0.30521265 -0.15643437 -0.93934792\n\t\t 0 -0.15643437 -0.98768884 -0.30521265 -0.15643437 -0.93934786;\n\tsetAttr \".vt[166:331]\" -0.58054888 -0.15643437 -0.79905695 -0.79905689 -0.15643437 -0.58054882\n\t\t -0.93934768 -0.15643437 -0.30521256 -0.9876886 -0.15643437 0 -0.93934768 -0.15643437 0.30521256\n\t\t -0.79905683 -0.15643437 0.58054876 -0.58054876 -0.15643437 0.79905677 -0.30521256 -0.15643437 0.93934757\n\t\t -2.9435407e-08 -0.15643437 0.98768848 0.30521247 -0.15643437 0.93934757 0.58054864 -0.15643437 0.79905671\n\t\t 0.79905665 -0.15643437 0.5805487 0.93934751 -0.15643437 0.3052125 0.98768836 -0.15643437 0\n\t\t 0.95105714 0 -0.30901718 0.80901754 0 -0.5877856 0.5877856 0 -0.80901748 0.30901715 0 -0.95105702\n\t\t 0 0 -1.000000476837 -0.30901715 0 -0.95105696 -0.58778548 0 -0.8090173 -0.80901724 0 -0.58778542\n\t\t -0.95105678 0 -0.30901706 -1.000000238419 0 0 -0.95105678 0 0.30901706 -0.80901718 0 0.58778536\n\t\t -0.58778536 0 0.80901712 -0.30901706 0 0.95105666 -2.9802322e-08 0 1.000000119209\n\t\t 0.30901697 0 0.9510566 0.58778524 0 0.80901706 0.809017 0 0.5877853 0.95105654 0 0.309017\n\t\t 1 0 0 0.93934804 0.15643437 -0.30521268 0.79905719 0.15643437 -0.580549 0.580549 0.15643437 -0.79905713\n\t\t 0.30521265 0.15643437 -0.93934792 0 0.15643437 -0.98768884 -0.30521265 0.15643437 -0.93934786\n\t\t -0.58054888 0.15643437 -0.79905695 -0.79905689 0.15643437 -0.58054882 -0.93934768 0.15643437 -0.30521256\n\t\t -0.9876886 0.15643437 0 -0.93934768 0.15643437 0.30521256 -0.79905683 0.15643437 0.58054876\n\t\t -0.58054876 0.15643437 0.79905677 -0.30521256 0.15643437 0.93934757 -2.9435407e-08 0.15643437 0.98768848\n\t\t 0.30521247 0.15643437 0.93934757 0.58054864 0.15643437 0.79905671 0.79905665 0.15643437 0.5805487\n\t\t 0.93934751 0.15643437 0.3052125 0.98768836 0.15643437 0 0.90450913 0.30901697 -0.2938928\n\t\t 0.7694214 0.30901697 -0.55901736 0.55901736 0.30901697 -0.76942134 0.29389277 0.30901697 -0.90450901\n\t\t 0 0.30901697 -0.95105702 -0.29389277 0.30901697 -0.90450895 -0.55901724 0.30901697 -0.76942122\n\t\t -0.76942116 0.30901697 -0.55901718 -0.90450877 0.30901697 -0.29389271 -0.95105678 0.30901697 0\n\t\t -0.90450877 0.30901697 0.29389271 -0.7694211 0.30901697 0.55901712 -0.55901712 0.30901697 0.76942104\n\t\t -0.29389271 0.30901697 0.90450865 -2.8343694e-08 0.30901697 0.95105666 0.29389262 0.30901697 0.90450859\n\t\t 0.559017 0.30901697 0.76942098 0.76942092 0.30901697 0.55901706 0.90450853 0.30901697 0.29389265\n\t\t 0.95105654 0.30901697 0 0.8473981 0.45399052 -0.27533633 0.72083992 0.45399052 -0.5237208\n\t\t 0.5237208 0.45399052 -0.72083986 0.2753363 0.45399052 -0.84739798 0 0.45399052 -0.89100695\n\t\t -0.2753363 0.45399052 -0.84739798 -0.52372068 0.45399052 -0.72083968 -0.72083962 0.45399052 -0.52372062\n\t\t -0.8473978 0.45399052 -0.27533621 -0.89100677 0.45399052 0 -0.8473978 0.45399052 0.27533621\n\t\t -0.72083962 0.45399052 0.52372062 -0.52372062 0.45399052 0.72083956 -0.27533621 0.45399052 0.84739769\n\t\t -2.6554064e-08 0.45399052 0.89100665 0.27533615 0.45399052 0.84739763 0.5237205 0.45399052 0.7208395\n\t\t 0.72083944 0.45399052 0.52372056 0.84739757 0.45399052 0.27533618 0.89100653 0.45399052 0\n\t\t 0.7694214 0.58778524 -0.25000015 0.65450895 0.58778524 -0.47552854 0.47552854 0.58778524 -0.65450889\n\t\t 0.25000012 0.58778524 -0.76942128 0 0.58778524 -0.80901736 -0.25000012 0.58778524 -0.76942122\n\t\t -0.47552845 0.58778524 -0.65450877 -0.65450871 0.58778524 -0.47552839 -0.7694211 0.58778524 -0.25000006\n\t\t -0.80901718 0.58778524 0 -0.7694211 0.58778524 0.25000006 -0.65450865 0.58778524 0.47552836\n\t\t -0.47552836 0.58778524 0.65450859 -0.25000006 0.58778524 0.76942098 -2.4110586e-08 0.58778524 0.80901712\n\t\t 0.24999999 0.58778524 0.76942098 0.47552827 0.58778524 0.65450853 0.65450853 0.58778524 0.4755283\n\t\t 0.76942092 0.58778524 0.25 0.809017 0.58778524 0 0.67249894 0.70710677 -0.21850814\n\t\t 0.57206178 0.70710677 -0.41562718 0.41562718 0.70710677 -0.57206172 0.21850812 0.70710677 -0.67249888\n\t\t 0 0.70710677 -0.70710713 -0.21850812 0.70710677 -0.67249882 -0.41562709 0.70710677 -0.5720616\n\t\t -0.57206154 0.70710677 -0.41562706 -0.6724987 0.70710677 -0.21850805 -0.70710695 0.70710677 0\n\t\t -0.6724987 0.70710677 0.21850805 -0.57206154 0.70710677 0.415627 -0.415627 0.70710677 0.57206148\n\t\t -0.21850805 0.70710677 0.67249858 -2.1073424e-08 0.70710677 0.70710683 0.21850799 0.70710677 0.67249858\n\t\t 0.41562691 0.70710677 0.57206142 0.57206142 0.70710677 0.41562697 0.67249852 0.70710677 0.21850802\n\t\t 0.70710677 0.70710677 0 0.55901736 0.809017 -0.18163574 0.47552857 0.809017 -0.34549171\n\t\t 0.34549171 0.809017 -0.47552854 0.18163572 0.809017 -0.5590173 0 0.809017 -0.58778554\n\t\t -0.18163572 0.809017 -0.55901724 -0.34549165 0.809017 -0.47552842 -0.47552839 0.809017 -0.34549159\n\t\t -0.55901712 0.809017 -0.18163566 -0.58778536 0.809017 0 -0.55901712 0.809017 0.18163566\n\t\t -0.47552836 0.809017 0.34549156 -0.34549156 0.809017 0.47552833 -0.18163566 0.809017 0.55901706\n\t\t -1.7517365e-08 0.809017 0.5877853 0.18163562 0.809017 0.55901706 0.3454915 0.809017 0.4755283\n\t\t 0.47552827 0.809017 0.34549153 0.559017 0.809017 0.18163563 0.58778524 0.809017 0\n\t\t 0.43177092 0.89100653 -0.14029087 0.36728629 0.89100653 -0.2668491 0.2668491 0.89100653 -0.36728626\n\t\t 0.14029086 0.89100653 -0.43177086 0 0.89100653 -0.45399073 -0.14029086 0.89100653 -0.43177083\n\t\t -0.26684904 0.89100653 -0.36728618 -0.36728615 0.89100653 -0.26684901 -0.43177077 0.89100653 -0.14029081\n\t\t -0.45399064 0.89100653 0 -0.43177077 0.89100653 0.14029081 -0.36728612 0.89100653 0.26684898;\n\tsetAttr \".vt[332:381]\" -0.26684898 0.89100653 0.36728612 -0.14029081 0.89100653 0.43177071\n\t\t -1.3529972e-08 0.89100653 0.45399058 0.14029078 0.89100653 0.43177068 0.26684892 0.89100653 0.36728609\n\t\t 0.36728606 0.89100653 0.26684895 0.43177065 0.89100653 0.1402908 0.45399052 0.89100653 0\n\t\t 0.29389283 0.95105654 -0.095491566 0.25000018 0.95105654 -0.18163574 0.18163574 0.95105654 -0.25000015\n\t\t 0.095491551 0.95105654 -0.2938928 0 0.95105654 -0.30901715 -0.095491551 0.95105654 -0.29389277\n\t\t -0.18163571 0.95105654 -0.25000009 -0.25000009 0.95105654 -0.18163569 -0.29389271 0.95105654 -0.095491529\n\t\t -0.30901706 0.95105654 0 -0.29389271 0.95105654 0.095491529 -0.25000006 0.95105654 0.18163568\n\t\t -0.18163568 0.95105654 0.25000006 -0.095491529 0.95105654 0.29389268 -9.2094243e-09 0.95105654 0.30901703\n\t\t 0.095491499 0.95105654 0.29389265 0.18163563 0.95105654 0.25000003 0.25 0.95105654 0.18163565\n\t\t 0.29389265 0.95105654 0.095491506 0.309017 0.95105654 0 0.14877813 0.98768836 -0.048340943\n\t\t 0.12655823 0.98768836 -0.091949932 0.091949932 0.98768836 -0.12655823 0.048340935 0.98768836 -0.14877811\n\t\t 0 0.98768836 -0.15643455 -0.048340935 0.98768836 -0.1487781 -0.091949917 0.98768836 -0.1265582\n\t\t -0.12655818 0.98768836 -0.091949902 -0.14877807 0.98768836 -0.048340924 -0.15643452 0.98768836 0\n\t\t -0.14877807 0.98768836 0.048340924 -0.12655818 0.98768836 0.091949895 -0.091949895 0.98768836 0.12655817\n\t\t -0.048340924 0.98768836 0.14877805 -4.6621107e-09 0.98768836 0.15643449 0.048340909 0.98768836 0.14877804\n\t\t 0.09194988 0.98768836 0.12655815 0.12655815 0.98768836 0.091949888 0.14877804 0.98768836 0.048340913\n\t\t 0.15643448 0.98768836 0 0 -1 0 0 1 0;\n\tsetAttr -s 780 \".ed\";\n\tsetAttr \".ed[0:165]\"  0 1 1 1 2 1 2 3 1 3 4 1 4 5 1 5 6 1 6 7 1 7 8 1 8 9 1\n\t\t 9 10 1 10 11 1 11 12 1 12 13 1 13 14 1 14 15 1 15 16 1 16 17 1 17 18 1 18 19 1 19 0 1\n\t\t 20 21 1 21 22 1 22 23 1 23 24 1 24 25 1 25 26 1 26 27 1 27 28 1 28 29 1 29 30 1 30 31 1\n\t\t 31 32 1 32 33 1 33 34 1 34 35 1 35 36 1 36 37 1 37 38 1 38 39 1 39 20 1 40 41 1 41 42 1\n\t\t 42 43 1 43 44 1 44 45 1 45 46 1 46 47 1 47 48 1 48 49 1 49 50 1 50 51 1 51 52 1 52 53 1\n\t\t 53 54 1 54 55 1 55 56 1 56 57 1 57 58 1 58 59 1 59 40 1 60 61 1 61 62 1 62 63 1 63 64 1\n\t\t 64 65 1 65 66 1 66 67 1 67 68 1 68 69 1 69 70 1 70 71 1 71 72 1 72 73 1 73 74 1 74 75 1\n\t\t 75 76 1 76 77 1 77 78 1 78 79 1 79 60 1 80 81 1 81 82 1 82 83 1 83 84 1 84 85 1 85 86 1\n\t\t 86 87 1 87 88 1 88 89 1 89 90 1 90 91 1 91 92 1 92 93 1 93 94 1 94 95 1 95 96 1 96 97 1\n\t\t 97 98 1 98 99 1 99 80 1 100 101 1 101 102 1 102 103 1 103 104 1 104 105 1 105 106 1\n\t\t 106 107 1 107 108 1 108 109 1 109 110 1 110 111 1 111 112 1 112 113 1 113 114 1 114 115 1\n\t\t 115 116 1 116 117 1 117 118 1 118 119 1 119 100 1 120 121 1 121 122 1 122 123 1 123 124 1\n\t\t 124 125 1 125 126 1 126 127 1 127 128 1 128 129 1 129 130 1 130 131 1 131 132 1 132 133 1\n\t\t 133 134 1 134 135 1 135 136 1 136 137 1 137 138 1 138 139 1 139 120 1 140 141 1 141 142 1\n\t\t 142 143 1 143 144 1 144 145 1 145 146 1 146 147 1 147 148 1 148 149 1 149 150 1 150 151 1\n\t\t 151 152 1 152 153 1 153 154 1 154 155 1 155 156 1 156 157 1 157 158 1 158 159 1 159 140 1\n\t\t 160 161 1 161 162 1 162 163 1 163 164 1 164 165 1 165 166 1;\n\tsetAttr \".ed[166:331]\" 166 167 1 167 168 1 168 169 1 169 170 1 170 171 1 171 172 1\n\t\t 172 173 1 173 174 1 174 175 1 175 176 1 176 177 1 177 178 1 178 179 1 179 160 1 180 181 1\n\t\t 181 182 1 182 183 1 183 184 1 184 185 1 185 186 1 186 187 1 187 188 1 188 189 1 189 190 1\n\t\t 190 191 1 191 192 1 192 193 1 193 194 1 194 195 1 195 196 1 196 197 1 197 198 1 198 199 1\n\t\t 199 180 1 200 201 1 201 202 1 202 203 1 203 204 1 204 205 1 205 206 1 206 207 1 207 208 1\n\t\t 208 209 1 209 210 1 210 211 1 211 212 1 212 213 1 213 214 1 214 215 1 215 216 1 216 217 1\n\t\t 217 218 1 218 219 1 219 200 1 220 221 1 221 222 1 222 223 1 223 224 1 224 225 1 225 226 1\n\t\t 226 227 1 227 228 1 228 229 1 229 230 1 230 231 1 231 232 1 232 233 1 233 234 1 234 235 1\n\t\t 235 236 1 236 237 1 237 238 1 238 239 1 239 220 1 240 241 1 241 242 1 242 243 1 243 244 1\n\t\t 244 245 1 245 246 1 246 247 1 247 248 1 248 249 1 249 250 1 250 251 1 251 252 1 252 253 1\n\t\t 253 254 1 254 255 1 255 256 1 256 257 1 257 258 1 258 259 1 259 240 1 260 261 1 261 262 1\n\t\t 262 263 1 263 264 1 264 265 1 265 266 1 266 267 1 267 268 1 268 269 1 269 270 1 270 271 1\n\t\t 271 272 1 272 273 1 273 274 1 274 275 1 275 276 1 276 277 1 277 278 1 278 279 1 279 260 1\n\t\t 280 281 1 281 282 1 282 283 1 283 284 1 284 285 1 285 286 1 286 287 1 287 288 1 288 289 1\n\t\t 289 290 1 290 291 1 291 292 1 292 293 1 293 294 1 294 295 1 295 296 1 296 297 1 297 298 1\n\t\t 298 299 1 299 280 1 300 301 1 301 302 1 302 303 1 303 304 1 304 305 1 305 306 1 306 307 1\n\t\t 307 308 1 308 309 1 309 310 1 310 311 1 311 312 1 312 313 1 313 314 1 314 315 1 315 316 1\n\t\t 316 317 1 317 318 1 318 319 1 319 300 1 320 321 1 321 322 1 322 323 1 323 324 1 324 325 1\n\t\t 325 326 1 326 327 1 327 328 1 328 329 1 329 330 1 330 331 1 331 332 1;\n\tsetAttr \".ed[332:497]\" 332 333 1 333 334 1 334 335 1 335 336 1 336 337 1 337 338 1\n\t\t 338 339 1 339 320 1 340 341 1 341 342 1 342 343 1 343 344 1 344 345 1 345 346 1 346 347 1\n\t\t 347 348 1 348 349 1 349 350 1 350 351 1 351 352 1 352 353 1 353 354 1 354 355 1 355 356 1\n\t\t 356 357 1 357 358 1 358 359 1 359 340 1 360 361 1 361 362 1 362 363 1 363 364 1 364 365 1\n\t\t 365 366 1 366 367 1 367 368 1 368 369 1 369 370 1 370 371 1 371 372 1 372 373 1 373 374 1\n\t\t 374 375 1 375 376 1 376 377 1 377 378 1 378 379 1 379 360 1 0 20 1 1 21 1 2 22 1\n\t\t 3 23 1 4 24 1 5 25 1 6 26 1 7 27 1 8 28 1 9 29 1 10 30 1 11 31 1 12 32 1 13 33 1\n\t\t 14 34 1 15 35 1 16 36 1 17 37 1 18 38 1 19 39 1 20 40 1 21 41 1 22 42 1 23 43 1 24 44 1\n\t\t 25 45 1 26 46 1 27 47 1 28 48 1 29 49 1 30 50 1 31 51 1 32 52 1 33 53 1 34 54 1 35 55 1\n\t\t 36 56 1 37 57 1 38 58 1 39 59 1 40 60 1 41 61 1 42 62 1 43 63 1 44 64 1 45 65 1 46 66 1\n\t\t 47 67 1 48 68 1 49 69 1 50 70 1 51 71 1 52 72 1 53 73 1 54 74 1 55 75 1 56 76 1 57 77 1\n\t\t 58 78 1 59 79 1 60 80 1 61 81 1 62 82 1 63 83 1 64 84 1 65 85 1 66 86 1 67 87 1 68 88 1\n\t\t 69 89 1 70 90 1 71 91 1 72 92 1 73 93 1 74 94 1 75 95 1 76 96 1 77 97 1 78 98 1 79 99 1\n\t\t 80 100 1 81 101 1 82 102 1 83 103 1 84 104 1 85 105 1 86 106 1 87 107 1 88 108 1\n\t\t 89 109 1 90 110 1 91 111 1 92 112 1 93 113 1 94 114 1 95 115 1 96 116 1 97 117 1\n\t\t 98 118 1 99 119 1 100 120 1 101 121 1 102 122 1 103 123 1 104 124 1 105 125 1 106 126 1\n\t\t 107 127 1 108 128 1 109 129 1 110 130 1 111 131 1 112 132 1 113 133 1 114 134 1 115 135 1\n\t\t 116 136 1 117 137 1;\n\tsetAttr \".ed[498:663]\" 118 138 1 119 139 1 120 140 1 121 141 1 122 142 1 123 143 1\n\t\t 124 144 1 125 145 1 126 146 1 127 147 1 128 148 1 129 149 1 130 150 1 131 151 1 132 152 1\n\t\t 133 153 1 134 154 1 135 155 1 136 156 1 137 157 1 138 158 1 139 159 1 140 160 1 141 161 1\n\t\t 142 162 1 143 163 1 144 164 1 145 165 1 146 166 1 147 167 1 148 168 1 149 169 1 150 170 1\n\t\t 151 171 1 152 172 1 153 173 1 154 174 1 155 175 1 156 176 1 157 177 1 158 178 1 159 179 1\n\t\t 160 180 1 161 181 1 162 182 1 163 183 1 164 184 1 165 185 1 166 186 1 167 187 1 168 188 1\n\t\t 169 189 1 170 190 1 171 191 1 172 192 1 173 193 1 174 194 1 175 195 1 176 196 1 177 197 1\n\t\t 178 198 1 179 199 1 180 200 1 181 201 1 182 202 1 183 203 1 184 204 1 185 205 1 186 206 1\n\t\t 187 207 1 188 208 1 189 209 1 190 210 1 191 211 1 192 212 1 193 213 1 194 214 1 195 215 1\n\t\t 196 216 1 197 217 1 198 218 1 199 219 1 200 220 1 201 221 1 202 222 1 203 223 1 204 224 1\n\t\t 205 225 1 206 226 1 207 227 1 208 228 1 209 229 1 210 230 1 211 231 1 212 232 1 213 233 1\n\t\t 214 234 1 215 235 1 216 236 1 217 237 1 218 238 1 219 239 1 220 240 1 221 241 1 222 242 1\n\t\t 223 243 1 224 244 1 225 245 1 226 246 1 227 247 1 228 248 1 229 249 1 230 250 1 231 251 1\n\t\t 232 252 1 233 253 1 234 254 1 235 255 1 236 256 1 237 257 1 238 258 1 239 259 1 240 260 1\n\t\t 241 261 1 242 262 1 243 263 1 244 264 1 245 265 1 246 266 1 247 267 1 248 268 1 249 269 1\n\t\t 250 270 1 251 271 1 252 272 1 253 273 1 254 274 1 255 275 1 256 276 1 257 277 1 258 278 1\n\t\t 259 279 1 260 280 1 261 281 1 262 282 1 263 283 1 264 284 1 265 285 1 266 286 1 267 287 1\n\t\t 268 288 1 269 289 1 270 290 1 271 291 1 272 292 1 273 293 1 274 294 1 275 295 1 276 296 1\n\t\t 277 297 1 278 298 1 279 299 1 280 300 1 281 301 1 282 302 1 283 303 1;\n\tsetAttr \".ed[664:779]\" 284 304 1 285 305 1 286 306 1 287 307 1 288 308 1 289 309 1\n\t\t 290 310 1 291 311 1 292 312 1 293 313 1 294 314 1 295 315 1 296 316 1 297 317 1 298 318 1\n\t\t 299 319 1 300 320 1 301 321 1 302 322 1 303 323 1 304 324 1 305 325 1 306 326 1 307 327 1\n\t\t 308 328 1 309 329 1 310 330 1 311 331 1 312 332 1 313 333 1 314 334 1 315 335 1 316 336 1\n\t\t 317 337 1 318 338 1 319 339 1 320 340 1 321 341 1 322 342 1 323 343 1 324 344 1 325 345 1\n\t\t 326 346 1 327 347 1 328 348 1 329 349 1 330 350 1 331 351 1 332 352 1 333 353 1 334 354 1\n\t\t 335 355 1 336 356 1 337 357 1 338 358 1 339 359 1 340 360 1 341 361 1 342 362 1 343 363 1\n\t\t 344 364 1 345 365 1 346 366 1 347 367 1 348 368 1 349 369 1 350 370 1 351 371 1 352 372 1\n\t\t 353 373 1 354 374 1 355 375 1 356 376 1 357 377 1 358 378 1 359 379 1 380 0 1 380 1 1\n\t\t 380 2 1 380 3 1 380 4 1 380 5 1 380 6 1 380 7 1 380 8 1 380 9 1 380 10 1 380 11 1\n\t\t 380 12 1 380 13 1 380 14 1 380 15 1 380 16 1 380 17 1 380 18 1 380 19 1 360 381 1\n\t\t 361 381 1 362 381 1 363 381 1 364 381 1 365 381 1 366 381 1 367 381 1 368 381 1 369 381 1\n\t\t 370 381 1 371 381 1 372 381 1 373 381 1 374 381 1 375 381 1 376 381 1 377 381 1 378 381 1\n\t\t 379 381 1;\n\tsetAttr -s 400 -ch 1560 \".fc[0:399]\" -type \"polyFaces\"\n\t\tf 4 0 381 -21 -381\n\t\tmu 0 4 0 1 22 21\n\t\tf 4 1 382 -22 -382\n\t\tmu 0 4 1 2 23 22\n\t\tf 4 2 383 -23 -383\n\t\tmu 0 4 2 3 24 23\n\t\tf 4 3 384 -24 -384\n\t\tmu 0 4 3 4 25 24\n\t\tf 4 4 385 -25 -385\n\t\tmu 0 4 4 5 26 25\n\t\tf 4 5 386 -26 -386\n\t\tmu 0 4 5 6 27 26\n\t\tf 4 6 387 -27 -387\n\t\tmu 0 4 6 7 28 27\n\t\tf 4 7 388 -28 -388\n\t\tmu 0 4 7 8 29 28\n\t\tf 4 8 389 -29 -389\n\t\tmu 0 4 8 9 30 29\n\t\tf 4 9 390 -30 -390\n\t\tmu 0 4 9 10 31 30\n\t\tf 4 10 391 -31 -391\n\t\tmu 0 4 10 11 32 31\n\t\tf 4 11 392 -32 -392\n\t\tmu 0 4 11 12 33 32\n\t\tf 4 12 393 -33 -393\n\t\tmu 0 4 12 13 34 33\n\t\tf 4 13 394 -34 -394\n\t\tmu 0 4 13 14 35 34\n\t\tf 4 14 395 -35 -395\n\t\tmu 0 4 14 15 36 35\n\t\tf 4 15 396 -36 -396\n\t\tmu 0 4 15 16 37 36\n\t\tf 4 16 397 -37 -397\n\t\tmu 0 4 16 17 38 37\n\t\tf 4 17 398 -38 -398\n\t\tmu 0 4 17 18 39 38\n\t\tf 4 18 399 -39 -399\n\t\tmu 0 4 18 19 40 39\n\t\tf 4 19 380 -40 -400\n\t\tmu 0 4 19 20 41 40\n\t\tf 4 20 401 -41 -401\n\t\tmu 0 4 21 22 43 42\n\t\tf 4 21 402 -42 -402\n\t\tmu 0 4 22 23 44 43\n\t\tf 4 22 403 -43 -403\n\t\tmu 0 4 23 24 45 44\n\t\tf 4 23 404 -44 -404\n\t\tmu 0 4 24 25 46 45\n\t\tf 4 24 405 -45 -405\n\t\tmu 0 4 25 26 47 46\n\t\tf 4 25 406 -46 -406\n\t\tmu 0 4 26 27 48 47\n\t\tf 4 26 407 -47 -407\n\t\tmu 0 4 27 28 49 48\n\t\tf 4 27 408 -48 -408\n\t\tmu 0 4 28 29 50 49\n\t\tf 4 28 409 -49 -409\n\t\tmu 0 4 29 30 51 50\n\t\tf 4 29 410 -50 -410\n\t\tmu 0 4 30 31 52 51\n\t\tf 4 30 411 -51 -411\n\t\tmu 0 4 31 32 53 52\n\t\tf 4 31 412 -52 -412\n\t\tmu 0 4 32 33 54 53\n\t\tf 4 32 413 -53 -413\n\t\tmu 0 4 33 34 55 54\n\t\tf 4 33 414 -54 -414\n\t\tmu 0 4 34 35 56 55\n\t\tf 4 34 415 -55 -415\n\t\tmu 0 4 35 36 57 56\n\t\tf 4 35 416 -56 -416\n\t\tmu 0 4 36 37 58 57\n\t\tf 4 36 417 -57 -417\n\t\tmu 0 4 37 38 59 58\n\t\tf 4 37 418 -58 -418\n\t\tmu 0 4 38 39 60 59\n\t\tf 4 38 419 -59 -419\n\t\tmu 0 4 39 40 61 60\n\t\tf 4 39 400 -60 -420\n\t\tmu 0 4 40 41 62 61\n\t\tf 4 40 421 -61 -421\n\t\tmu 0 4 42 43 64 63\n\t\tf 4 41 422 -62 -422\n\t\tmu 0 4 43 44 65 64\n\t\tf 4 42 423 -63 -423\n\t\tmu 0 4 44 45 66 65\n\t\tf 4 43 424 -64 -424\n\t\tmu 0 4 45 46 67 66\n\t\tf 4 44 425 -65 -425\n\t\tmu 0 4 46 47 68 67\n\t\tf 4 45 426 -66 -426\n\t\tmu 0 4 47 48 69 68\n\t\tf 4 46 427 -67 -427\n\t\tmu 0 4 48 49 70 69\n\t\tf 4 47 428 -68 -428\n\t\tmu 0 4 49 50 71 70\n\t\tf 4 48 429 -69 -429\n\t\tmu 0 4 50 51 72 71\n\t\tf 4 49 430 -70 -430\n\t\tmu 0 4 51 52 73 72\n\t\tf 4 50 431 -71 -431\n\t\tmu 0 4 52 53 74 73\n\t\tf 4 51 432 -72 -432\n\t\tmu 0 4 53 54 75 74\n\t\tf 4 52 433 -73 -433\n\t\tmu 0 4 54 55 76 75\n\t\tf 4 53 434 -74 -434\n\t\tmu 0 4 55 56 77 76\n\t\tf 4 54 435 -75 -435\n\t\tmu 0 4 56 57 78 77\n\t\tf 4 55 436 -76 -436\n\t\tmu 0 4 57 58 79 78\n\t\tf 4 56 437 -77 -437\n\t\tmu 0 4 58 59 80 79\n\t\tf 4 57 438 -78 -438\n\t\tmu 0 4 59 60 81 80\n\t\tf 4 58 439 -79 -439\n\t\tmu 0 4 60 61 82 81\n\t\tf 4 59 420 -80 -440\n\t\tmu 0 4 61 62 83 82\n\t\tf 4 60 441 -81 -441\n\t\tmu 0 4 63 64 85 84\n\t\tf 4 61 442 -82 -442\n\t\tmu 0 4 64 65 86 85\n\t\tf 4 62 443 -83 -443\n\t\tmu 0 4 65 66 87 86\n\t\tf 4 63 444 -84 -444\n\t\tmu 0 4 66 67 88 87\n\t\tf 4 64 445 -85 -445\n\t\tmu 0 4 67 68 89 88\n\t\tf 4 65 446 -86 -446\n\t\tmu 0 4 68 69 90 89\n\t\tf 4 66 447 -87 -447\n\t\tmu 0 4 69 70 91 90\n\t\tf 4 67 448 -88 -448\n\t\tmu 0 4 70 71 92 91\n\t\tf 4 68 449 -89 -449\n\t\tmu 0 4 71 72 93 92\n\t\tf 4 69 450 -90 -450\n\t\tmu 0 4 72 73 94 93\n\t\tf 4 70 451 -91 -451\n\t\tmu 0 4 73 74 95 94\n\t\tf 4 71 452 -92 -452\n\t\tmu 0 4 74 75 96 95\n\t\tf 4 72 453 -93 -453\n\t\tmu 0 4 75 76 97 96\n\t\tf 4 73 454 -94 -454\n\t\tmu 0 4 76 77 98 97\n\t\tf 4 74 455 -95 -455\n\t\tmu 0 4 77 78 99 98\n\t\tf 4 75 456 -96 -456\n\t\tmu 0 4 78 79 100 99\n\t\tf 4 76 457 -97 -457\n\t\tmu 0 4 79 80 101 100\n\t\tf 4 77 458 -98 -458\n\t\tmu 0 4 80 81 102 101\n\t\tf 4 78 459 -99 -459\n\t\tmu 0 4 81 82 103 102\n\t\tf 4 79 440 -100 -460\n\t\tmu 0 4 82 83 104 103\n\t\tf 4 80 461 -101 -461\n\t\tmu 0 4 84 85 106 105\n\t\tf 4 81 462 -102 -462\n\t\tmu 0 4 85 86 107 106\n\t\tf 4 82 463 -103 -463\n\t\tmu 0 4 86 87 108 107\n\t\tf 4 83 464 -104 -464\n\t\tmu 0 4 87 88 109 108\n\t\tf 4 84 465 -105 -465\n\t\tmu 0 4 88 89 110 109\n\t\tf 4 85 466 -106 -466\n\t\tmu 0 4 89 90 111 110\n\t\tf 4 86 467 -107 -467\n\t\tmu 0 4 90 91 112 111\n\t\tf 4 87 468 -108 -468\n\t\tmu 0 4 91 92 113 112\n\t\tf 4 88 469 -109 -469\n\t\tmu 0 4 92 93 114 113\n\t\tf 4 89 470 -110 -470\n\t\tmu 0 4 93 94 115 114\n\t\tf 4 90 471 -111 -471\n\t\tmu 0 4 94 95 116 115\n\t\tf 4 91 472 -112 -472\n\t\tmu 0 4 95 96 117 116\n\t\tf 4 92 473 -113 -473\n\t\tmu 0 4 96 97 118 117\n\t\tf 4 93 474 -114 -474\n\t\tmu 0 4 97 98 119 118\n\t\tf 4 94 475 -115 -475\n\t\tmu 0 4 98 99 120 119\n\t\tf 4 95 476 -116 -476\n\t\tmu 0 4 99 100 121 120\n\t\tf 4 96 477 -117 -477\n\t\tmu 0 4 100 101 122 121\n\t\tf 4 97 478 -118 -478\n\t\tmu 0 4 101 102 123 122\n\t\tf 4 98 479 -119 -479\n\t\tmu 0 4 102 103 124 123\n\t\tf 4 99 460 -120 -480\n\t\tmu 0 4 103 104 125 124\n\t\tf 4 100 481 -121 -481\n\t\tmu 0 4 105 106 127 126\n\t\tf 4 101 482 -122 -482\n\t\tmu 0 4 106 107 128 127\n\t\tf 4 102 483 -123 -483\n\t\tmu 0 4 107 108 129 128\n\t\tf 4 103 484 -124 -484\n\t\tmu 0 4 108 109 130 129\n\t\tf 4 104 485 -125 -485\n\t\tmu 0 4 109 110 131 130\n\t\tf 4 105 486 -126 -486\n\t\tmu 0 4 110 111 132 131\n\t\tf 4 106 487 -127 -487\n\t\tmu 0 4 111 112 133 132\n\t\tf 4 107 488 -128 -488\n\t\tmu 0 4 112 113 134 133\n\t\tf 4 108 489 -129 -489\n\t\tmu 0 4 113 114 135 134\n\t\tf 4 109 490 -130 -490\n\t\tmu 0 4 114 115 136 135\n\t\tf 4 110 491 -131 -491\n\t\tmu 0 4 115 116 137 136\n\t\tf 4 111 492 -132 -492\n\t\tmu 0 4 116 117 138 137\n\t\tf 4 112 493 -133 -493\n\t\tmu 0 4 117 118 139 138\n\t\tf 4 113 494 -134 -494\n\t\tmu 0 4 118 119 140 139\n\t\tf 4 114 495 -135 -495\n\t\tmu 0 4 119 120 141 140\n\t\tf 4 115 496 -136 -496\n\t\tmu 0 4 120 121 142 141\n\t\tf 4 116 497 -137 -497\n\t\tmu 0 4 121 122 143 142\n\t\tf 4 117 498 -138 -498\n\t\tmu 0 4 122 123 144 143\n\t\tf 4 118 499 -139 -499\n\t\tmu 0 4 123 124 145 144\n\t\tf 4 119 480 -140 -500\n\t\tmu 0 4 124 125 146 145\n\t\tf 4 120 501 -141 -501\n\t\tmu 0 4 126 127 148 147\n\t\tf 4 121 502 -142 -502\n\t\tmu 0 4 127 128 149 148\n\t\tf 4 122 503 -143 -503\n\t\tmu 0 4 128 129 150 149\n\t\tf 4 123 504 -144 -504\n\t\tmu 0 4 129 130 151 150\n\t\tf 4 124 505 -145 -505\n\t\tmu 0 4 130 131 152 151\n\t\tf 4 125 506 -146 -506\n\t\tmu 0 4 131 132 153 152\n\t\tf 4 126 507 -147 -507\n\t\tmu 0 4 132 133 154 153\n\t\tf 4 127 508 -148 -508\n\t\tmu 0 4 133 134 155 154\n\t\tf 4 128 509 -149 -509\n\t\tmu 0 4 134 135 156 155\n\t\tf 4 129 510 -150 -510\n\t\tmu 0 4 135 136 157 156\n\t\tf 4 130 511 -151 -511\n\t\tmu 0 4 136 137 158 157\n\t\tf 4 131 512 -152 -512\n\t\tmu 0 4 137 138 159 158\n\t\tf 4 132 513 -153 -513\n\t\tmu 0 4 138 139 160 159\n\t\tf 4 133 514 -154 -514\n\t\tmu 0 4 139 140 161 160\n\t\tf 4 134 515 -155 -515\n\t\tmu 0 4 140 141 162 161\n\t\tf 4 135 516 -156 -516\n\t\tmu 0 4 141 142 163 162\n\t\tf 4 136 517 -157 -517\n\t\tmu 0 4 142 143 164 163\n\t\tf 4 137 518 -158 -518\n\t\tmu 0 4 143 144 165 164\n\t\tf 4 138 519 -159 -519\n\t\tmu 0 4 144 145 166 165\n\t\tf 4 139 500 -160 -520\n\t\tmu 0 4 145 146 167 166\n\t\tf 4 140 521 -161 -521\n\t\tmu 0 4 147 148 169 168\n\t\tf 4 141 522 -162 -522\n\t\tmu 0 4 148 149 170 169\n\t\tf 4 142 523 -163 -523\n\t\tmu 0 4 149 150 171 170\n\t\tf 4 143 524 -164 -524\n\t\tmu 0 4 150 151 172 171\n\t\tf 4 144 525 -165 -525\n\t\tmu 0 4 151 152 173 172\n\t\tf 4 145 526 -166 -526\n\t\tmu 0 4 152 153 174 173\n\t\tf 4 146 527 -167 -527\n\t\tmu 0 4 153 154 175 174\n\t\tf 4 147 528 -168 -528\n\t\tmu 0 4 154 155 176 175\n\t\tf 4 148 529 -169 -529\n\t\tmu 0 4 155 156 177 176\n\t\tf 4 149 530 -170 -530\n\t\tmu 0 4 156 157 178 177\n\t\tf 4 150 531 -171 -531\n\t\tmu 0 4 157 158 179 178\n\t\tf 4 151 532 -172 -532\n\t\tmu 0 4 158 159 180 179\n\t\tf 4 152 533 -173 -533\n\t\tmu 0 4 159 160 181 180\n\t\tf 4 153 534 -174 -534\n\t\tmu 0 4 160 161 182 181\n\t\tf 4 154 535 -175 -535\n\t\tmu 0 4 161 162 183 182\n\t\tf 4 155 536 -176 -536\n\t\tmu 0 4 162 163 184 183\n\t\tf 4 156 537 -177 -537\n\t\tmu 0 4 163 164 185 184\n\t\tf 4 157 538 -178 -538\n\t\tmu 0 4 164 165 186 185\n\t\tf 4 158 539 -179 -539\n\t\tmu 0 4 165 166 187 186\n\t\tf 4 159 520 -180 -540\n\t\tmu 0 4 166 167 188 187\n\t\tf 4 160 541 -181 -541\n\t\tmu 0 4 168 169 190 189\n\t\tf 4 161 542 -182 -542\n\t\tmu 0 4 169 170 191 190\n\t\tf 4 162 543 -183 -543\n\t\tmu 0 4 170 171 192 191\n\t\tf 4 163 544 -184 -544\n\t\tmu 0 4 171 172 193 192\n\t\tf 4 164 545 -185 -545\n\t\tmu 0 4 172 173 194 193\n\t\tf 4 165 546 -186 -546\n\t\tmu 0 4 173 174 195 194\n\t\tf 4 166 547 -187 -547\n\t\tmu 0 4 174 175 196 195\n\t\tf 4 167 548 -188 -548\n\t\tmu 0 4 175 176 197 196\n\t\tf 4 168 549 -189 -549\n\t\tmu 0 4 176 177 198 197\n\t\tf 4 169 550 -190 -550\n\t\tmu 0 4 177 178 199 198\n\t\tf 4 170 551 -191 -551\n\t\tmu 0 4 178 179 200 199\n\t\tf 4 171 552 -192 -552\n\t\tmu 0 4 179 180 201 200\n\t\tf 4 172 553 -193 -553\n\t\tmu 0 4 180 181 202 201\n\t\tf 4 173 554 -194 -554\n\t\tmu 0 4 181 182 203 202\n\t\tf 4 174 555 -195 -555\n\t\tmu 0 4 182 183 204 203\n\t\tf 4 175 556 -196 -556\n\t\tmu 0 4 183 184 205 204\n\t\tf 4 176 557 -197 -557\n\t\tmu 0 4 184 185 206 205\n\t\tf 4 177 558 -198 -558\n\t\tmu 0 4 185 186 207 206\n\t\tf 4 178 559 -199 -559\n\t\tmu 0 4 186 187 208 207\n\t\tf 4 179 540 -200 -560\n\t\tmu 0 4 187 188 209 208\n\t\tf 4 180 561 -201 -561\n\t\tmu 0 4 189 190 211 210\n\t\tf 4 181 562 -202 -562\n\t\tmu 0 4 190 191 212 211\n\t\tf 4 182 563 -203 -563\n\t\tmu 0 4 191 192 213 212\n\t\tf 4 183 564 -204 -564\n\t\tmu 0 4 192 193 214 213\n\t\tf 4 184 565 -205 -565\n\t\tmu 0 4 193 194 215 214\n\t\tf 4 185 566 -206 -566\n\t\tmu 0 4 194 195 216 215\n\t\tf 4 186 567 -207 -567\n\t\tmu 0 4 195 196 217 216\n\t\tf 4 187 568 -208 -568\n\t\tmu 0 4 196 197 218 217\n\t\tf 4 188 569 -209 -569\n\t\tmu 0 4 197 198 219 218\n\t\tf 4 189 570 -210 -570\n\t\tmu 0 4 198 199 220 219\n\t\tf 4 190 571 -211 -571\n\t\tmu 0 4 199 200 221 220\n\t\tf 4 191 572 -212 -572\n\t\tmu 0 4 200 201 222 221\n\t\tf 4 192 573 -213 -573\n\t\tmu 0 4 201 202 223 222\n\t\tf 4 193 574 -214 -574\n\t\tmu 0 4 202 203 224 223\n\t\tf 4 194 575 -215 -575\n\t\tmu 0 4 203 204 225 224\n\t\tf 4 195 576 -216 -576\n\t\tmu 0 4 204 205 226 225\n\t\tf 4 196 577 -217 -577\n\t\tmu 0 4 205 206 227 226\n\t\tf 4 197 578 -218 -578\n\t\tmu 0 4 206 207 228 227\n\t\tf 4 198 579 -219 -579\n\t\tmu 0 4 207 208 229 228\n\t\tf 4 199 560 -220 -580\n\t\tmu 0 4 208 209 230 229\n\t\tf 4 200 581 -221 -581\n\t\tmu 0 4 210 211 232 231\n\t\tf 4 201 582 -222 -582\n\t\tmu 0 4 211 212 233 232\n\t\tf 4 202 583 -223 -583\n\t\tmu 0 4 212 213 234 233\n\t\tf 4 203 584 -224 -584\n\t\tmu 0 4 213 214 235 234\n\t\tf 4 204 585 -225 -585\n\t\tmu 0 4 214 215 236 235\n\t\tf 4 205 586 -226 -586\n\t\tmu 0 4 215 216 237 236\n\t\tf 4 206 587 -227 -587\n\t\tmu 0 4 216 217 238 237\n\t\tf 4 207 588 -228 -588\n\t\tmu 0 4 217 218 239 238\n\t\tf 4 208 589 -229 -589\n\t\tmu 0 4 218 219 240 239\n\t\tf 4 209 590 -230 -590\n\t\tmu 0 4 219 220 241 240\n\t\tf 4 210 591 -231 -591\n\t\tmu 0 4 220 221 242 241\n\t\tf 4 211 592 -232 -592\n\t\tmu 0 4 221 222 243 242\n\t\tf 4 212 593 -233 -593\n\t\tmu 0 4 222 223 244 243\n\t\tf 4 213 594 -234 -594\n\t\tmu 0 4 223 224 245 244\n\t\tf 4 214 595 -235 -595\n\t\tmu 0 4 224 225 246 245\n\t\tf 4 215 596 -236 -596\n\t\tmu 0 4 225 226 247 246\n\t\tf 4 216 597 -237 -597\n\t\tmu 0 4 226 227 248 247\n\t\tf 4 217 598 -238 -598\n\t\tmu 0 4 227 228 249 248\n\t\tf 4 218 599 -239 -599\n\t\tmu 0 4 228 229 250 249\n\t\tf 4 219 580 -240 -600\n\t\tmu 0 4 229 230 251 250\n\t\tf 4 220 601 -241 -601\n\t\tmu 0 4 231 232 253 252\n\t\tf 4 221 602 -242 -602\n\t\tmu 0 4 232 233 254 253\n\t\tf 4 222 603 -243 -603\n\t\tmu 0 4 233 234 255 254\n\t\tf 4 223 604 -244 -604\n\t\tmu 0 4 234 235 256 255\n\t\tf 4 224 605 -245 -605\n\t\tmu 0 4 235 236 257 256\n\t\tf 4 225 606 -246 -606\n\t\tmu 0 4 236 237 258 257\n\t\tf 4 226 607 -247 -607\n\t\tmu 0 4 237 238 259 258\n\t\tf 4 227 608 -248 -608\n\t\tmu 0 4 238 239 260 259\n\t\tf 4 228 609 -249 -609\n\t\tmu 0 4 239 240 261 260\n\t\tf 4 229 610 -250 -610\n\t\tmu 0 4 240 241 262 261\n\t\tf 4 230 611 -251 -611\n\t\tmu 0 4 241 242 263 262\n\t\tf 4 231 612 -252 -612\n\t\tmu 0 4 242 243 264 263\n\t\tf 4 232 613 -253 -613\n\t\tmu 0 4 243 244 265 264\n\t\tf 4 233 614 -254 -614\n\t\tmu 0 4 244 245 266 265\n\t\tf 4 234 615 -255 -615\n\t\tmu 0 4 245 246 267 266\n\t\tf 4 235 616 -256 -616\n\t\tmu 0 4 246 247 268 267\n\t\tf 4 236 617 -257 -617\n\t\tmu 0 4 247 248 269 268\n\t\tf 4 237 618 -258 -618\n\t\tmu 0 4 248 249 270 269\n\t\tf 4 238 619 -259 -619\n\t\tmu 0 4 249 250 271 270\n\t\tf 4 239 600 -260 -620\n\t\tmu 0 4 250 251 272 271\n\t\tf 4 240 621 -261 -621\n\t\tmu 0 4 252 253 274 273\n\t\tf 4 241 622 -262 -622\n\t\tmu 0 4 253 254 275 274\n\t\tf 4 242 623 -263 -623\n\t\tmu 0 4 254 255 276 275\n\t\tf 4 243 624 -264 -624\n\t\tmu 0 4 255 256 277 276\n\t\tf 4 244 625 -265 -625\n\t\tmu 0 4 256 257 278 277\n\t\tf 4 245 626 -266 -626\n\t\tmu 0 4 257 258 279 278\n\t\tf 4 246 627 -267 -627\n\t\tmu 0 4 258 259 280 279\n\t\tf 4 247 628 -268 -628\n\t\tmu 0 4 259 260 281 280\n\t\tf 4 248 629 -269 -629\n\t\tmu 0 4 260 261 282 281\n\t\tf 4 249 630 -270 -630\n\t\tmu 0 4 261 262 283 282\n\t\tf 4 250 631 -271 -631\n\t\tmu 0 4 262 263 284 283\n\t\tf 4 251 632 -272 -632\n\t\tmu 0 4 263 264 285 284\n\t\tf 4 252 633 -273 -633\n\t\tmu 0 4 264 265 286 285\n\t\tf 4 253 634 -274 -634\n\t\tmu 0 4 265 266 287 286\n\t\tf 4 254 635 -275 -635\n\t\tmu 0 4 266 267 288 287\n\t\tf 4 255 636 -276 -636\n\t\tmu 0 4 267 268 289 288\n\t\tf 4 256 637 -277 -637\n\t\tmu 0 4 268 269 290 289\n\t\tf 4 257 638 -278 -638\n\t\tmu 0 4 269 270 291 290\n\t\tf 4 258 639 -279 -639\n\t\tmu 0 4 270 271 292 291\n\t\tf 4 259 620 -280 -640\n\t\tmu 0 4 271 272 293 292\n\t\tf 4 260 641 -281 -641\n\t\tmu 0 4 273 274 295 294\n\t\tf 4 261 642 -282 -642\n\t\tmu 0 4 274 275 296 295\n\t\tf 4 262 643 -283 -643\n\t\tmu 0 4 275 276 297 296\n\t\tf 4 263 644 -284 -644\n\t\tmu 0 4 276 277 298 297\n\t\tf 4 264 645 -285 -645\n\t\tmu 0 4 277 278 299 298\n\t\tf 4 265 646 -286 -646\n\t\tmu 0 4 278 279 300 299\n\t\tf 4 266 647 -287 -647\n\t\tmu 0 4 279 280 301 300\n\t\tf 4 267 648 -288 -648\n\t\tmu 0 4 280 281 302 301\n\t\tf 4 268 649 -289 -649\n\t\tmu 0 4 281 282 303 302\n\t\tf 4 269 650 -290 -650\n\t\tmu 0 4 282 283 304 303\n\t\tf 4 270 651 -291 -651\n\t\tmu 0 4 283 284 305 304\n\t\tf 4 271 652 -292 -652\n\t\tmu 0 4 284 285 306 305\n\t\tf 4 272 653 -293 -653\n\t\tmu 0 4 285 286 307 306\n\t\tf 4 273 654 -294 -654\n\t\tmu 0 4 286 287 308 307\n\t\tf 4 274 655 -295 -655\n\t\tmu 0 4 287 288 309 308\n\t\tf 4 275 656 -296 -656\n\t\tmu 0 4 288 289 310 309\n\t\tf 4 276 657 -297 -657\n\t\tmu 0 4 289 290 311 310\n\t\tf 4 277 658 -298 -658\n\t\tmu 0 4 290 291 312 311\n\t\tf 4 278 659 -299 -659\n\t\tmu 0 4 291 292 313 312\n\t\tf 4 279 640 -300 -660\n\t\tmu 0 4 292 293 314 313\n\t\tf 4 280 661 -301 -661\n\t\tmu 0 4 294 295 316 315\n\t\tf 4 281 662 -302 -662\n\t\tmu 0 4 295 296 317 316\n\t\tf 4 282 663 -303 -663\n\t\tmu 0 4 296 297 318 317\n\t\tf 4 283 664 -304 -664\n\t\tmu 0 4 297 298 319 318\n\t\tf 4 284 665 -305 -665\n\t\tmu 0 4 298 299 320 319\n\t\tf 4 285 666 -306 -666\n\t\tmu 0 4 299 300 321 320\n\t\tf 4 286 667 -307 -667\n\t\tmu 0 4 300 301 322 321\n\t\tf 4 287 668 -308 -668\n\t\tmu 0 4 301 302 323 322\n\t\tf 4 288 669 -309 -669\n\t\tmu 0 4 302 303 324 323\n\t\tf 4 289 670 -310 -670\n\t\tmu 0 4 303 304 325 324\n\t\tf 4 290 671 -311 -671\n\t\tmu 0 4 304 305 326 325\n\t\tf 4 291 672 -312 -672\n\t\tmu 0 4 305 306 327 326\n\t\tf 4 292 673 -313 -673\n\t\tmu 0 4 306 307 328 327\n\t\tf 4 293 674 -314 -674\n\t\tmu 0 4 307 308 329 328\n\t\tf 4 294 675 -315 -675\n\t\tmu 0 4 308 309 330 329\n\t\tf 4 295 676 -316 -676\n\t\tmu 0 4 309 310 331 330\n\t\tf 4 296 677 -317 -677\n\t\tmu 0 4 310 311 332 331\n\t\tf 4 297 678 -318 -678\n\t\tmu 0 4 311 312 333 332\n\t\tf 4 298 679 -319 -679\n\t\tmu 0 4 312 313 334 333\n\t\tf 4 299 660 -320 -680\n\t\tmu 0 4 313 314 335 334\n\t\tf 4 300 681 -321 -681\n\t\tmu 0 4 315 316 337 336\n\t\tf 4 301 682 -322 -682\n\t\tmu 0 4 316 317 338 337\n\t\tf 4 302 683 -323 -683\n\t\tmu 0 4 317 318 339 338\n\t\tf 4 303 684 -324 -684\n\t\tmu 0 4 318 319 340 339\n\t\tf 4 304 685 -325 -685\n\t\tmu 0 4 319 320 341 340\n\t\tf 4 305 686 -326 -686\n\t\tmu 0 4 320 321 342 341\n\t\tf 4 306 687 -327 -687\n\t\tmu 0 4 321 322 343 342\n\t\tf 4 307 688 -328 -688\n\t\tmu 0 4 322 323 344 343\n\t\tf 4 308 689 -329 -689\n\t\tmu 0 4 323 324 345 344\n\t\tf 4 309 690 -330 -690\n\t\tmu 0 4 324 325 346 345\n\t\tf 4 310 691 -331 -691\n\t\tmu 0 4 325 326 347 346\n\t\tf 4 311 692 -332 -692\n\t\tmu 0 4 326 327 348 347\n\t\tf 4 312 693 -333 -693\n\t\tmu 0 4 327 328 349 348\n\t\tf 4 313 694 -334 -694\n\t\tmu 0 4 328 329 350 349\n\t\tf 4 314 695 -335 -695\n\t\tmu 0 4 329 330 351 350\n\t\tf 4 315 696 -336 -696\n\t\tmu 0 4 330 331 352 351\n\t\tf 4 316 697 -337 -697\n\t\tmu 0 4 331 332 353 352\n\t\tf 4 317 698 -338 -698\n\t\tmu 0 4 332 333 354 353\n\t\tf 4 318 699 -339 -699\n\t\tmu 0 4 333 334 355 354\n\t\tf 4 319 680 -340 -700\n\t\tmu 0 4 334 335 356 355\n\t\tf 4 320 701 -341 -701\n\t\tmu 0 4 336 337 358 357\n\t\tf 4 321 702 -342 -702\n\t\tmu 0 4 337 338 359 358\n\t\tf 4 322 703 -343 -703\n\t\tmu 0 4 338 339 360 359\n\t\tf 4 323 704 -344 -704\n\t\tmu 0 4 339 340 361 360\n\t\tf 4 324 705 -345 -705\n\t\tmu 0 4 340 341 362 361\n\t\tf 4 325 706 -346 -706\n\t\tmu 0 4 341 342 363 362\n\t\tf 4 326 707 -347 -707\n\t\tmu 0 4 342 343 364 363\n\t\tf 4 327 708 -348 -708\n\t\tmu 0 4 343 344 365 364\n\t\tf 4 328 709 -349 -709\n\t\tmu 0 4 344 345 366 365\n\t\tf 4 329 710 -350 -710\n\t\tmu 0 4 345 346 367 366\n\t\tf 4 330 711 -351 -711\n\t\tmu 0 4 346 347 368 367\n\t\tf 4 331 712 -352 -712\n\t\tmu 0 4 347 348 369 368\n\t\tf 4 332 713 -353 -713\n\t\tmu 0 4 348 349 370 369\n\t\tf 4 333 714 -354 -714\n\t\tmu 0 4 349 350 371 370\n\t\tf 4 334 715 -355 -715\n\t\tmu 0 4 350 351 372 371\n\t\tf 4 335 716 -356 -716\n\t\tmu 0 4 351 352 373 372\n\t\tf 4 336 717 -357 -717\n\t\tmu 0 4 352 353 374 373\n\t\tf 4 337 718 -358 -718\n\t\tmu 0 4 353 354 375 374\n\t\tf 4 338 719 -359 -719\n\t\tmu 0 4 354 355 376 375\n\t\tf 4 339 700 -360 -720\n\t\tmu 0 4 355 356 377 376\n\t\tf 4 340 721 -361 -721\n\t\tmu 0 4 357 358 379 378\n\t\tf 4 341 722 -362 -722\n\t\tmu 0 4 358 359 380 379\n\t\tf 4 342 723 -363 -723\n\t\tmu 0 4 359 360 381 380\n\t\tf 4 343 724 -364 -724\n\t\tmu 0 4 360 361 382 381\n\t\tf 4 344 725 -365 -725\n\t\tmu 0 4 361 362 383 382\n\t\tf 4 345 726 -366 -726\n\t\tmu 0 4 362 363 384 383\n\t\tf 4 346 727 -367 -727\n\t\tmu 0 4 363 364 385 384\n\t\tf 4 347 728 -368 -728\n\t\tmu 0 4 364 365 386 385\n\t\tf 4 348 729 -369 -729\n\t\tmu 0 4 365 366 387 386\n\t\tf 4 349 730 -370 -730\n\t\tmu 0 4 366 367 388 387\n\t\tf 4 350 731 -371 -731\n\t\tmu 0 4 367 368 389 388\n\t\tf 4 351 732 -372 -732\n\t\tmu 0 4 368 369 390 389\n\t\tf 4 352 733 -373 -733\n\t\tmu 0 4 369 370 391 390\n\t\tf 4 353 734 -374 -734\n\t\tmu 0 4 370 371 392 391\n\t\tf 4 354 735 -375 -735\n\t\tmu 0 4 371 372 393 392\n\t\tf 4 355 736 -376 -736\n\t\tmu 0 4 372 373 394 393\n\t\tf 4 356 737 -377 -737\n\t\tmu 0 4 373 374 395 394\n\t\tf 4 357 738 -378 -738\n\t\tmu 0 4 374 375 396 395\n\t\tf 4 358 739 -379 -739\n\t\tmu 0 4 375 376 397 396\n\t\tf 4 359 720 -380 -740\n\t\tmu 0 4 376 377 398 397\n\t\tf 3 -1 -741 741\n\t\tmu 0 3 1 0 399\n\t\tf 3 -2 -742 742\n\t\tmu 0 3 2 1 400\n\t\tf 3 -3 -743 743\n\t\tmu 0 3 3 2 401\n\t\tf 3 -4 -744 744\n\t\tmu 0 3 4 3 402\n\t\tf 3 -5 -745 745\n\t\tmu 0 3 5 4 403\n\t\tf 3 -6 -746 746\n\t\tmu 0 3 6 5 404\n\t\tf 3 -7 -747 747\n\t\tmu 0 3 7 6 405\n\t\tf 3 -8 -748 748\n\t\tmu 0 3 8 7 406\n\t\tf 3 -9 -749 749\n\t\tmu 0 3 9 8 407\n\t\tf 3 -10 -750 750\n\t\tmu 0 3 10 9 408\n\t\tf 3 -11 -751 751\n\t\tmu 0 3 11 10 409\n\t\tf 3 -12 -752 752\n\t\tmu 0 3 12 11 410\n\t\tf 3 -13 -753 753\n\t\tmu 0 3 13 12 411\n\t\tf 3 -14 -754 754\n\t\tmu 0 3 14 13 412\n\t\tf 3 -15 -755 755\n\t\tmu 0 3 15 14 413\n\t\tf 3 -16 -756 756\n\t\tmu 0 3 16 15 414\n\t\tf 3 -17 -757 757\n\t\tmu 0 3 17 16 415\n\t\tf 3 -18 -758 758\n\t\tmu 0 3 18 17 416\n\t\tf 3 -19 -759 759\n\t\tmu 0 3 19 18 417\n\t\tf 3 -20 -760 740\n\t\tmu 0 3 20 19 418\n\t\tf 3 360 761 -761\n\t\tmu 0 3 378 379 419\n\t\tf 3 361 762 -762\n\t\tmu 0 3 379 380 420\n\t\tf 3 362 763 -763\n\t\tmu 0 3 380 381 421\n\t\tf 3 363 764 -764\n\t\tmu 0 3 381 382 422\n\t\tf 3 364 765 -765\n\t\tmu 0 3 382 383 423\n\t\tf 3 365 766 -766\n\t\tmu 0 3 383 384 424\n\t\tf 3 366 767 -767\n\t\tmu 0 3 384 385 425\n\t\tf 3 367 768 -768\n\t\tmu 0 3 385 386 426\n\t\tf 3 368 769 -769\n\t\tmu 0 3 386 387 427\n\t\tf 3 369 770 -770\n\t\tmu 0 3 387 388 428\n\t\tf 3 370 771 -771\n\t\tmu 0 3 388 389 429\n\t\tf 3 371 772 -772\n\t\tmu 0 3 389 390 430\n\t\tf 3 372 773 -773\n\t\tmu 0 3 390 391 431\n\t\tf 3 373 774 -774\n\t\tmu 0 3 391 392 432\n\t\tf 3 374 775 -775\n\t\tmu 0 3 392 393 433\n\t\tf 3 375 776 -776\n\t\tmu 0 3 393 394 434\n\t\tf 3 376 777 -777\n\t\tmu 0 3 394 395 435\n\t\tf 3 377 778 -778\n\t\tmu 0 3 395 396 436\n\t\tf 3 378 779 -779\n\t\tmu 0 3 396 397 437\n\t\tf 3 379 760 -780\n\t\tmu 0 3 397 398 438;\n\tsetAttr \".cd\" -type \"dataPolyComponent\" Index_Data Edge 0 ;\n\tsetAttr \".cvd\" -type \"dataPolyComponent\" Index_Data Vertex 0 ;\n\tsetAttr \".pd[0]\" -type \"dataPolyComponent\" Index_Data UV 0 ;\n\tsetAttr \".hfd\" -type \"dataPolyComponent\" Index_Data Face 0 ;\n\tsetAttr \".dr\" 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:6c77a15a98a9\";\nselect -ne :time1;\n\tsetAttr \".o\" 1001;\n\tsetAttr \".unw\" 1001;\nselect -ne :hardwareRenderingGlobals;\n\tsetAttr \".otfna\" -type \"stringArray\" 22 \"NURBS Curves\" \"NURBS Surfaces\" \"Polygons\" \"Subdiv Surface\" \"Particles\" \"Particle Instance\" \"Fluids\" \"Strokes\" \"Image Planes\" \"UI\" \"Lights\" \"Cameras\" \"Locators\" \"Joints\" \"IK Handles\" \"Deformers\" \"Motion Trails\" \"Components\" \"Hair Systems\" \"Follicles\" \"Misc. UI\" \"Ornaments\"  ;\n\tsetAttr \".otfva\" -type \"Int32Array\" 22 0 1 1 1 1 1\n\t\t 1 1 1 0 0 0 0 0 0 0 0 0\n\t\t 0 0 0 0 ;\n\tsetAttr \".fprt\" yes;\nselect -ne :renderPartition;\n\tsetAttr -s 2 \".st\";\nselect -ne :renderGlobalsList1;\nselect -ne :defaultShaderList1;\n\tsetAttr -s 5 \".s\";\nselect -ne :postProcessList1;\n\tsetAttr -s 2 \".p\";\nselect -ne :defaultRenderingList1;\n\tsetAttr -s 2 \".r\";\nselect -ne :lightList1;\nselect -ne :initialShadingGroup;\n\tsetAttr -s 2 \".dsm\";\n\tsetAttr \".ro\" yes;\nselect -ne :initialParticleSE;\n\tsetAttr \".ro\" yes;\nselect -ne :defaultRenderGlobals;\n\taddAttr -ci true -h true -sn \"dss\" -ln \"defaultSurfaceShader\" -dt \"string\";\n\tsetAttr \".ren\" -type \"string\" \"arnold\";\n\tsetAttr \".outf\" 51;\n\tsetAttr \".imfkey\" -type \"string\" \"exr\";\n\tsetAttr \".an\" yes;\n\tsetAttr \".fs\" 1001;\n\tsetAttr \".ef\" 1001;\n\tsetAttr \".oft\" -type \"string\" \"\";\n\tsetAttr \".pff\" yes;\n\tsetAttr \".ifp\" -type \"string\" \"<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>\";\n\tsetAttr \".rv\" -type \"string\" \"\";\n\tsetAttr \".pram\" -type \"string\" \"\";\n\tsetAttr \".poam\" -type \"string\" \"\";\n\tsetAttr \".prlm\" -type \"string\" \"\";\n\tsetAttr \".polm\" -type \"string\" \"\";\n\tsetAttr \".prm\" -type \"string\" \"\";\n\tsetAttr \".pom\" -type \"string\" \"\";\n\tsetAttr \".dss\" -type \"string\" \"lambert1\";\nselect -ne :defaultResolution;\n\tsetAttr \".w\" 1920;\n\tsetAttr \".h\" 1080;\n\tsetAttr \".pa\" 1;\n\tsetAttr \".dar\" 1.7777777910232544;\nselect -ne :defaultLightSet;\nselect -ne :hardwareRenderGlobals;\n\tsetAttr \".ctrs\" 256;\n\tsetAttr \".btrs\" 512;\nconnectAttr \"pSphere1_GEOShape1.iog\" \":initialShadingGroup.dsm\" -na;\n// End of modelMain.ma\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/workfile/workfileTest_task/v001/test_project_test_asset_workfileTest_task_v001.ma",
    "content": "//Maya ASCII 2023 scene\n//Name: test_project_test_asset_test_task_v002.ma\n//Last modified: Thu, Nov 09, 2023 11:59:33 AM\n//Codeset: 1252\nrequires maya \"2023\";\nrequires -nodeType \"simpleSelector\" -nodeType \"renderSetupLayer\" -nodeType \"renderSetup\"\n\t\t -nodeType \"collection\" \"renderSetup.py\" \"1.0\";\nrequires -nodeType \"aiOptions\" -nodeType \"aiAOVDriver\" -nodeType \"aiAOVFilter\" -nodeType \"aiSkyDomeLight\"\n\t\t \"mtoa\" \"5.2.1.1\";\nrequires -nodeType \"polyDisc\" \"modelingToolkit\" \"0.0.0.0\";\ncurrentUnit -l centimeter -a degree -t pal;\nfileInfo \"application\" \"maya\";\nfileInfo \"product\" \"Maya 2023\";\nfileInfo \"version\" \"2023\";\nfileInfo \"cutIdentifier\" \"202211021031-847a9f9623\";\nfileInfo \"osv\" \"Windows 10 Pro v2009 (Build: 19045)\";\nfileInfo \"license\" \"education\";\nfileInfo \"UUID\" \"591BA477-4DBF-D8A9-D1CE-AEB4E5E95DCA\";\nfileInfo \"OpenPypeContext\" \"eyJwdWJsaXNoX2F0dHJpYnV0ZXMiOiB7IlZhbGlkYXRlQ29udGFpbmVycyI6IHsiYWN0aXZlIjogdHJ1ZX19fQ==\";\ncreateNode transform -s -n \"persp\";\n\trename -uid \"D52C935B-47C9-D868-A875-D799DD17B3A1\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 33.329836010894773 18.034068470832839 24.890981774804157 ;\n\tsetAttr \".r\" -type \"double3\" 79.461647270402509 -44.999999999997357 183.99999999999159 ;\n\tsetAttr \".rp\" -type \"double3\" -2.0401242849359917e-14 2.2609331405046354e-14 -44.821869662029947 ;\n\tsetAttr \".rpt\" -type \"double3\" -27.999999999999989 -21.000000000000025 16.82186966202995 ;\ncreateNode camera -s -n \"perspShape\" -p \"persp\";\n\trename -uid \"2399E6C0-490F-BA1F-485F-5AA8A01D27BC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 50.609449488607154;\n\tsetAttr \".imn\" -type \"string\" \"persp\";\n\tsetAttr \".den\" -type \"string\" \"persp_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".ai_translator\" -type \"string\" \"perspective\";\ncreateNode transform -s -n \"top\";\n\trename -uid \"415C7426-413E-0FAE-FCC3-3DAED7443A52\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 1000.1 0 ;\n\tsetAttr \".r\" -type \"double3\" 90 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" 0 -1000.1 1000.1 ;\ncreateNode camera -s -n \"topShape\" -p \"top\";\n\trename -uid \"3BD0CF60-40DB-5278-5D8B-06ACBDA32122\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"top\";\n\tsetAttr \".den\" -type \"string\" \"top_depth\";\n\tsetAttr \".man\" -type \"string\" \"top_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -t %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"front\";\n\trename -uid \"D83DD5CE-4FE0-AB1B-81B2-87A63F0B7A05\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 0 1000.1 ;\n\tsetAttr \".r\" -type \"double3\" 180 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\ncreateNode camera -s -n \"frontShape\" -p \"front\";\n\trename -uid \"23313CBA-42C2-0B3A-0FCF-EA965EAC5DEC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"front\";\n\tsetAttr \".den\" -type \"string\" \"front_depth\";\n\tsetAttr \".man\" -type \"string\" \"front_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -f %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"side\";\n\trename -uid \"F70F692C-4A0D-BE64-9EE4-A99B6FA2D56E\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 1000.1 0 0 ;\n\tsetAttr \".r\" -type \"double3\" 180 -90 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" -1000.1 0 1000.1 ;\ncreateNode camera -s -n \"sideShape\" -p \"side\";\n\trename -uid \"C05669C3-420E-CA11-E5FC-7EB64EF8B632\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"side\";\n\tsetAttr \".den\" -type \"string\" \"side_depth\";\n\tsetAttr \".man\" -type \"string\" \"side_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -s %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -n \"pDisc1\";\n\trename -uid \"DED70CCF-4C19-16E4-9E5D-66A05037BA47\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:90e762703f08\";\ncreateNode mesh -n \"pDiscShape1\" -p \"pDisc1\";\n\trename -uid \"E1FCDCCF-4DE1-D3B9-C4F8-3285F1CF5B25\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:4ee3da11a1a4\";\ncreateNode transform -n \"aiSkyDomeLight1\";\n\trename -uid \"402BF091-4305-22E3-7CF0-9BA3D7F948F7\";\ncreateNode aiSkyDomeLight -n \"aiSkyDomeLightShape1\" -p \"aiSkyDomeLight1\";\n\trename -uid \"CEF32074-4066-553D-A4FD-65B508A56ABE\";\n\taddAttr -ci true -h true -sn \"aal\" -ln \"attributeAliasList\" -dt \"attributeAlias\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".csh\" no;\n\tsetAttr \".rcsh\" no;\n\tsetAttr \".aal\" -type \"attributeAlias\" {\"exposure\",\"aiExposure\"} ;\ncreateNode transform -n \"mainCamera\";\n\trename -uid \"58651370-474E-02EE-39A9-A2AB27E0DD87\";\n\tsetAttr \".t\" -type \"double3\" 33.329836010894773 18.034068470832839 24.890981774804157 ;\n\tsetAttr \".r\" -type \"double3\" 79.461647270402509 -44.999999999997357 183.99999999999159 ;\n\tsetAttr \".rp\" -type \"double3\" -2.0401242849359917e-14 2.2609331405046354e-14 -44.821869662029947 ;\n\tsetAttr \".rpt\" -type \"double3\" -27.999999999999989 -21.000000000000025 16.82186966202995 ;\ncreateNode camera -n \"mainCameraShape\" -p \"mainCamera\";\n\trename -uid \"CCE11369-4101-EE5E-5381-3F87DA963CA2\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 50.609449488607154;\n\tsetAttr \".imn\" -type \"string\" \"persp\";\n\tsetAttr \".den\" -type \"string\" \"persp_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".ai_translator\" -type \"string\" \"perspective\";\ncreateNode transform -n \"model_GRP\";\n\trename -uid \"445FDC20-4A9D-2C5B-C7BD-F98F6E660B5C\";\ncreateNode transform -n \"pSphere1_GEO\" -p \"model_GRP\";\n\trename -uid \"7445A43F-444F-B2D3-4315-2AA013D2E0B6\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:302a4c6123a4\";\ncreateNode mesh -n \"pSphere1_GEOShape1\" -p \"pSphere1_GEO\";\n\trename -uid \"7C731260-45C6-339E-07BF-359446B08EA1\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:6c77a15a98a9\";\ncreateNode lightLinker -s -n \"lightLinker1\";\n\trename -uid \"D9ADDBD2-49DE-91E2-4166-99A362986A3A\";\n\tsetAttr -s 2 \".lnk\";\n\tsetAttr -s 2 \".slnk\";\ncreateNode shapeEditorManager -n \"shapeEditorManager\";\n\trename -uid \"A232A3B1-4B62-92E7-A7C9-9D9FC5EF010A\";\ncreateNode poseInterpolatorManager -n \"poseInterpolatorManager\";\n\trename -uid \"B30BA35D-492A-834B-3448-49A80BBBFC39\";\ncreateNode displayLayerManager -n \"layerManager\";\n\trename -uid \"4417380F-4A6A-16CC-B1DE-AA95ED9C7FB2\";\ncreateNode displayLayer -n \"defaultLayer\";\n\trename -uid \"4A776D1B-401F-7069-1C74-A7AAE84CEE03\";\n\tsetAttr \".ufem\" -type \"stringArray\" 0  ;\ncreateNode renderLayerManager -n \"renderLayerManager\";\n\trename -uid \"EE21C644-43B8-C754-0BED-709D2EEB204D\";\n\tsetAttr -s 2 \".rlmi[1]\"  1;\n\tsetAttr -s 2 \".rlmi\";\ncreateNode renderLayer -n \"defaultRenderLayer\";\n\trename -uid \"B134920D-4508-23BD-A6CA-11B43DE03F53\";\n\tsetAttr \".g\" yes;\ncreateNode renderSetup -n \"renderSetup\";\n\trename -uid \"9A8F0D15-41AB-CA70-C2D8-B78840BF9BC1\";\ncreateNode polySphere -n \"polySphere1\";\n\trename -uid \"DA319706-4ACF-B15C-53B2-48AC80D202EA\";\ncreateNode script -n \"uiConfigurationScriptNode\";\n\trename -uid \"4B7AFB53-452E-E870-63E1-CCA1DD6EAF13\";\n\tsetAttr \".b\" -type \"string\" (\n\t\t\"// Maya Mel UI Configuration File.\\n//\\n//  This script is machine generated.  Edit at your own risk.\\n//\\n//\\n\\nglobal string $gMainPane;\\nif (`paneLayout -exists $gMainPane`) {\\n\\n\\tglobal int $gUseScenePanelConfig;\\n\\tint    $useSceneConfig = $gUseScenePanelConfig;\\n\\tint    $nodeEditorPanelVisible = stringArrayContains(\\\"nodeEditorPanel1\\\", `getPanel -vis`);\\n\\tint    $nodeEditorWorkspaceControlOpen = (`workspaceControl -exists nodeEditorPanel1Window` && `workspaceControl -q -visible nodeEditorPanel1Window`);\\n\\tint    $menusOkayInPanels = `optionVar -q allowMenusInPanels`;\\n\\tint    $nVisPanes = `paneLayout -q -nvp $gMainPane`;\\n\\tint    $nPanes = 0;\\n\\tstring $editorName;\\n\\tstring $panelName;\\n\\tstring $itemFilterName;\\n\\tstring $panelConfig;\\n\\n\\t//\\n\\t//  get current state of the UI\\n\\t//\\n\\tsceneUIReplacement -update $gMainPane;\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Top View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Top View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|front\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n\"\n\t\t+ \"            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n\"\n\t\t+ \"            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Side View\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Side View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|front\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n\"\n\t\t+ \"            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n\"\n\t\t+ \"            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n\"\n\t\t+ \"        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Front View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Front View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|top\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n\"\n\t\t+ \"            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n\"\n\t\t+ \"            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n\"\n\t\t+ \"            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Persp View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Persp View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|mainCamera\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n\"\n\t\t+ \"            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n\"\n\t\t+ \"            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n\"\n\t\t+ \"            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"ToggledOutliner\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"ToggledOutliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -docTag \\\"isolOutln_fromSeln\\\" \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 1\\n            -showReferenceMembers 1\\n\"\n\t\t+ \"            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n\"\n\t\t+ \"            -isSet 0\\n            -isSetMember 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -selectCommand \\\"print(\\\\\\\"\\\\\\\")\\\" \\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n            -renderFilterIndex 0\\n            -selectionOrder \\\"chronological\\\" \\n            -expandAttribute 0\\n            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"Outliner\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"Outliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 0\\n            -showReferenceMembers 0\\n            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n\"\n\t\t+ \"            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n\"\n\t\t+ \"            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"graphEditor\\\" (localizedPanelLabel(\\\"Graph Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Graph Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 1\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n\"\n\t\t+ \"                -showPublishedAsConnected 0\\n                -showParentContainers 1\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 1\\n                -showCompounds 0\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 1\\n                -doNotSelectNewObjects 0\\n                -dropIsParent 1\\n                -transmitFilters 1\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n\"\n\t\t+ \"                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 1\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"GraphEd\\\");\\n            animCurveEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -showPlayRangeShades \\\"on\\\" \\n                -lockPlayRangeShades \\\"off\\\" \\n                -smoothness \\\"fine\\\" \\n                -resultSamples 1.041667\\n                -resultScreenSamples 0\\n                -resultUpdate \\\"delayed\\\" \\n                -showUpstreamCurves 1\\n                -keyMinScale 1\\n                -stackedCurvesMin -1\\n                -stackedCurvesMax 1\\n\"\n\t\t+ \"                -stackedCurvesSpace 0.2\\n                -preSelectionHighlight 0\\n                -constrainDrag 0\\n                -valueLinesToggle 1\\n                -highlightAffectedCurves 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dopeSheetPanel\\\" (localizedPanelLabel(\\\"Dope Sheet\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dope Sheet\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n\"\n\t\t+ \"                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 0\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n                -showPublishedAsConnected 0\\n                -showParentContainers 1\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 0\\n                -showCompounds 1\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 0\\n                -doNotSelectNewObjects 1\\n                -dropIsParent 1\\n                -transmitFilters 0\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n\"\n\t\t+ \"                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 0\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"DopeSheetEd\\\");\\n            dopeSheetEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -outliner \\\"dopeSheetPanel1OutlineEd\\\" \\n                -showSummary 1\\n                -showScene 0\\n                -hierarchyBelow 0\\n\"\n\t\t+ \"                -showTicks 1\\n                -selectionWindow 0 0 0 0 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"timeEditorPanel\\\" (localizedPanelLabel(\\\"Time Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Time Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"clipEditorPanel\\\" (localizedPanelLabel(\\\"Trax Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Trax Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = clipEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 0 \\n\"\n\t\t+ \"                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"sequenceEditorPanel\\\" (localizedPanelLabel(\\\"Camera Sequencer\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Camera Sequencer\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = sequenceEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 1 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperGraphPanel\\\" (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\n\\t\\t\\t$editorName = ($panelName+\\\"HyperGraphEd\\\");\\n            hyperGraph -e \\n                -graphLayoutStyle \\\"hierarchicalLayout\\\" \\n                -orientation \\\"horiz\\\" \\n                -mergeConnections 0\\n                -zoom 1\\n                -animateTransition 0\\n                -showRelationships 1\\n                -showShapes 0\\n                -showDeformers 0\\n                -showExpressions 0\\n                -showConstraints 0\\n                -showConnectionFromSelected 0\\n                -showConnectionToSelected 0\\n                -showConstraintLabels 0\\n                -showUnderworld 0\\n                -showInvisible 0\\n                -transitionFrames 1\\n                -opaqueContainers 0\\n                -freeform 0\\n                -imagePosition 0 0 \\n                -imageScale 1\\n                -imageEnabled 0\\n                -graphType \\\"DAG\\\" \\n                -heatMapDisplay 0\\n                -updateSelection 1\\n                -updateNodeAdded 1\\n                -useDrawOverrideColor 0\\n                -limitGraphTraversal -1\\n\"\n\t\t+ \"                -range 0 0 \\n                -iconSize \\\"smallIcons\\\" \\n                -showCachedConnections 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperShadePanel\\\" (localizedPanelLabel(\\\"Hypershade\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypershade\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"visorPanel\\\" (localizedPanelLabel(\\\"Visor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Visor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"nodeEditorPanel\\\" (localizedPanelLabel(\\\"Node Editor\\\")) `;\\n\\tif ($nodeEditorPanelVisible || $nodeEditorWorkspaceControlOpen) {\\n\"\n\t\t+ \"\\t\\tif (\\\"\\\" == $panelName) {\\n\\t\\t\\tif ($useSceneConfig) {\\n\\t\\t\\t\\t$panelName = `scriptedPanel -unParent  -type \\\"nodeEditorPanel\\\" -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels `;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n\"\n\t\t+ \"                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\t}\\n\\t\\t} else {\\n\\t\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n\"\n\t\t+ \"                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"createNodePanel\\\" (localizedPanelLabel(\\\"Create Node\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Create Node\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"polyTexturePlacementPanel\\\" (localizedPanelLabel(\\\"UV Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"UV Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"renderWindowPanel\\\" (localizedPanelLabel(\\\"Render View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Render View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"shapePanel\\\" (localizedPanelLabel(\\\"Shape Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tshapePanel -edit -l (localizedPanelLabel(\\\"Shape Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"posePanel\\\" (localizedPanelLabel(\\\"Pose Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tposePanel -edit -l (localizedPanelLabel(\\\"Pose Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynRelEdPanel\\\" (localizedPanelLabel(\\\"Dynamic Relationships\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dynamic Relationships\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"relationshipPanel\\\" (localizedPanelLabel(\\\"Relationship Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Relationship Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"referenceEditorPanel\\\" (localizedPanelLabel(\\\"Reference Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Reference Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynPaintScriptedPanelType\\\" (localizedPanelLabel(\\\"Paint Effects\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Paint Effects\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"scriptEditorPanel\\\" (localizedPanelLabel(\\\"Script Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Script Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"profilerPanel\\\" (localizedPanelLabel(\\\"Profiler Tool\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Profiler Tool\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"contentBrowserPanel\\\" (localizedPanelLabel(\\\"Content Browser\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Content Browser\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\tif ($useSceneConfig) {\\n        string $configName = `getPanel -cwl (localizedPanelLabel(\\\"Current Layout\\\"))`;\\n\"\n\t\t+ \"        if (\\\"\\\" != $configName) {\\n\\t\\t\\tpanelConfiguration -edit -label (localizedPanelLabel(\\\"Current Layout\\\")) \\n\\t\\t\\t\\t-userCreated false\\n\\t\\t\\t\\t-defaultImage \\\"vacantCell.xP:/\\\"\\n\\t\\t\\t\\t-image \\\"\\\"\\n\\t\\t\\t\\t-sc false\\n\\t\\t\\t\\t-configString \\\"global string $gMainPane; paneLayout -e -cn \\\\\\\"quad\\\\\\\" -ps 1 50 50 -ps 2 50 50 -ps 3 50 50 -ps 4 50 50 $gMainPane;\\\"\\n\\t\\t\\t\\t-removeAllPanels\\n\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Top View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera top` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera top` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Persp View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|mainCamera\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|mainCamera\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Side View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera side` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera side` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Front View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t$configName;\\n\\n            setNamedPanelLayout (localizedPanelLabel(\\\"Current Layout\\\"));\\n        }\\n\\n        panelHistory -e -clear mainPanelHistory;\\n        sceneUIReplacement -clear;\\n\\t}\\n\\n\\ngrid -spacing 5 -size 12 -divisions 5 -displayAxes yes -displayGridLines yes -displayDivisionLines yes -displayPerspectiveLabels no -displayOrthographicLabels no -displayAxesBold yes -perspectiveLabelPosition axis -orthographicLabelPosition edge;\\nviewManip -drawCompass 0 -compassAngle 0 -frontParameters \\\"\\\" -homeParameters \\\"\\\" -selectionLockParameters \\\"\\\";\\n}\\n\");\n\tsetAttr \".st\" 3;\ncreateNode script -n \"sceneConfigurationScriptNode\";\n\trename -uid \"72B19BC2-43A2-E229-0A73-2CB861A291D1\";\n\tsetAttr \".b\" -type \"string\" \"playbackOptions -min 1000 -max 1001 -ast 1000 -aet 1001 \";\n\tsetAttr \".st\" 6;\ncreateNode polyDisc -n \"polyDisc1\";\n\trename -uid \"9ED8A7BD-4FFD-6107-4322-35ACD1D3AC42\";\ncreateNode aiOptions -s -n \"defaultArnoldRenderOptions\";\n\trename -uid \"31A81965-48A6-B90D-503D-2FA162B7C982\";\ncreateNode aiAOVFilter -s -n \"defaultArnoldFilter\";\n\trename -uid \"77A2BCB1-4613-905E-080E-B997FD5E1C6F\";\n\tsetAttr \".ai_translator\" -type \"string\" \"gaussian\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDriver\";\n\trename -uid \"C05729BE-4A33-F1DA-C222-3F8AB6EE7504\";\n\tsetAttr \".ai_translator\" -type \"string\" \"exr\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDisplayDriver\";\n\trename -uid \"806C25D7-4284-C09D-A8AE-4A80DBFFFAAF\";\n\tsetAttr \".output_mode\" 0;\n\tsetAttr \".ai_translator\" -type \"string\" \"maya\";\ncreateNode renderSetupLayer -n \"Main\";\n\trename -uid \"DC3F077F-49F5-1D64-BFF3-AAAF06798636\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode renderLayer -n \"rs_Main\";\n\trename -uid \"D798EE14-43EE-D8EF-F4C9-D6B19C9BC029\";\n\tsetAttr \".do\" 1;\ncreateNode collection -n \"defaultCollection\";\n\trename -uid \"0194FCB7-43C4-DC06-C8D6-D9BA2721CCFA\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode simpleSelector -n \"defaultCollectionSelector\";\n\trename -uid \"37D69395-4785-D0BE-AEA0-EEA66D1FAEDF\";\n\tsetAttr \".pat\" -type \"string\" \"*\";\ncreateNode objectSet -n \"modelMain\";\n\trename -uid \"811E4501-4B64-3016-BE29-E18EC09D90B7\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"writeColorSets\" -ln \"writeColorSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"writeFaceSets\" -ln \"writeFaceSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"attr\" -ln \"attr\" -dt \"string\";\n\taddAttr -ci true -sn \"attrPrefix\" -ln \"attrPrefix\" -dt \"string\";\n\taddAttr -ci true -sn \"includeParentHierarchy\" -ln \"includeParentHierarchy\" -min\n\t\t0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:49991563bf50\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"model\";\n\tsetAttr \".subset\" -type \"string\" \"modelMain\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.model\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr -cb on \".writeColorSets\";\n\tsetAttr -cb on \".writeFaceSets\";\n\tsetAttr \".attr\" -type \"string\" \"\";\n\tsetAttr \".attrPrefix\" -type \"string\" \"\";\n\tsetAttr -cb on \".includeParentHierarchy\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateNodeIDsRelated\\\": {\\\"active\\\": true}, \\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ValidateTransformNamingSuffix\\\": {\\\"active\\\": true}, \\\"ValidateColorSets\\\": {\\\"active\\\": true}, \\\"ValidateMeshHasUVs\\\": {\\\"active\\\": true}, \\\"ValidateMeshNonZeroEdgeLength\\\": {\\\"active\\\": true}, \\\"ExtractModel\\\": {\\\"active\\\": true}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"writeColorSets,writeFaceSets,includeParentHierarchy,attr,attrPrefix\";\ncreateNode objectSet -n \"_renderingMain:Main\";\n\trename -uid \"4E1D2600-482D-425C-352A-74BA418DC374\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:752f6f925fe6\";\ncreateNode objectSet -n \"_renderingMain1:Main\";\n\trename -uid \"A6382090-4537-44CB-E6DC-A5A58B98D008\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:d587b60d712e\";\ncreateNode objectSet -n \"workfileMain\";\n\trename -uid \"0F32608C-4AA1-F493-205C-25BDABF95CEC\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".hio\" yes;\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"workfile\";\n\tsetAttr \".subset\" -type \"string\" \"workfileTest_task\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.workfile\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ExtractImportReference\\\": {\\\"active\\\": false}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:eee200c4dd38\";\ncreateNode objectSet -n \"renderingMain1\";\n\trename -uid \"9AC6AB5B-45EB-8439-BB6C-C197555E11E8\";\n\taddAttr -ci true -sn \"pre_creator_identifier\" -ln \"pre_creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".pre_creator_identifier\" -type \"string\" \"io.openpype.creators.maya.renderlayer\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:e72a7af3a6c5\";\ncreateNode objectSet -n \"_renderingMain:Main1\";\n\trename -uid \"7CFC031E-42E2-E431-89AB-5A991800F6F2\";\n\taddAttr -s false -ci true -sn \"renderlayer\" -ln \"renderlayer\" -at \"message\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"deadlineServers\" -ln \"deadlineServers\" -dt \"string\";\n\taddAttr -ci true -sn \"suspendPublishJob\" -ln \"suspendPublishJob\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"priority\" -ln \"priority\" -at \"long\";\n\taddAttr -ci true -sn \"tile_priority\" -ln \"tile_priority\" -at \"long\";\n\taddAttr -ci true -sn \"framesPerTask\" -ln \"framesPerTask\" -at \"long\";\n\taddAttr -ci true -sn \"whitelist\" -ln \"whitelist\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"machineList\" -ln \"machineList\" -dt \"string\";\n\taddAttr -ci true -sn \"useMayaBatch\" -ln \"useMayaBatch\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"primaryPool\" -ln \"primaryPool\" -dt \"string\";\n\taddAttr -ci true -sn \"secondaryPool\" -ln \"secondaryPool\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"review\" -ln \"review\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"extendFrames\" -ln \"extendFrames\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"overrideExistingFrame\" -ln \"overrideExistingFrame\" -min 0\n\t\t-max 1 -at \"bool\";\n\taddAttr -ci true -sn \"tileRendering\" -ln \"tileRendering\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"tilesX\" -ln \"tilesX\" -at \"long\";\n\taddAttr -ci true -sn \"tilesY\" -ln \"tilesY\" -at \"long\";\n\taddAttr -ci true -sn \"convertToScanline\" -ln \"convertToScanline\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"useReferencedAovs\" -ln \"useReferencedAovs\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"renderSetupIncludeLights\" -ln \"renderSetupIncludeLights\" -min\n\t\t0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:0850eb5268f2\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"renderlayer\";\n\tsetAttr \".subset\" -type \"string\" \"renderTest_taskMain\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.renderlayer\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".deadlineServers\" -type \"string\" \"default\";\n\tsetAttr -cb on \".suspendPublishJob\";\n\tsetAttr -cb on \".priority\" 50;\n\tsetAttr -cb on \".tile_priority\" 50;\n\tsetAttr -cb on \".framesPerTask\" 1;\n\tsetAttr -cb on \".whitelist\";\n\tsetAttr \".machineList\" -type \"string\" \"\";\n\tsetAttr -cb on \".useMayaBatch\";\n\tsetAttr \".primaryPool\" -type \"string\" \"none\";\n\tsetAttr \".secondaryPool\" -type \"string\" \"-\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr -cb on \".review\" yes;\n\tsetAttr -cb on \".extendFrames\";\n\tsetAttr -cb on \".overrideExistingFrame\" yes;\n\tsetAttr -cb on \".tileRendering\";\n\tsetAttr -cb on \".tilesX\" 2;\n\tsetAttr -cb on \".tilesY\" 2;\n\tsetAttr -cb on \".convertToScanline\";\n\tsetAttr -cb on \".useReferencedAovs\";\n\tsetAttr -cb on \".renderSetupIncludeLights\" yes;\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"CollectDeadlinePools\\\": {\\\"primaryPool\\\": \\\"\\\", \\\"secondaryPool\\\": \\\"\\\"}, \\\"ValidateResolution\\\": {\\\"active\\\": true}, \\\"ValidateDeadlinePools\\\": {\\\"active\\\": true}, \\\"ValidateFrameRange\\\": {\\\"active\\\": true}, \\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ExtractImportReference\\\": {\\\"active\\\": false}, \\\"MayaSubmitDeadline\\\": {\\\"priority\\\": 50, \\\"chunkSize\\\": 1, \\\"machineList\\\": \\\"\\\", \\\"whitelist\\\": false, \\\"tile_priority\\\": 50, \\\"strict_error_checking\\\": true}, \\\"ProcessSubmittedJobOnFarm\\\": {\\\"publishJobState\\\": \\\"Active\\\"}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"review,extendFrames,overrideExistingFrame,tileRendering,tilesX,tilesY,convertToScanline,useReferencedAovs,renderSetupIncludeLights\";\nselect -ne :time1;\n\tsetAttr \".o\" 1001;\n\tsetAttr \".unw\" 1001;\nselect -ne :hardwareRenderingGlobals;\n\tsetAttr \".otfna\" -type \"stringArray\" 22 \"NURBS Curves\" \"NURBS Surfaces\" \"Polygons\" \"Subdiv Surface\" \"Particles\" \"Particle Instance\" \"Fluids\" \"Strokes\" \"Image Planes\" \"UI\" \"Lights\" \"Cameras\" \"Locators\" \"Joints\" \"IK Handles\" \"Deformers\" \"Motion Trails\" \"Components\" \"Hair Systems\" \"Follicles\" \"Misc. UI\" \"Ornaments\"  ;\n\tsetAttr \".otfva\" -type \"Int32Array\" 22 0 1 1 1 1 1\n\t\t 1 1 1 0 0 0 0 0 0 0 0 0\n\t\t 0 0 0 0 ;\n\tsetAttr \".fprt\" yes;\nselect -ne :renderPartition;\n\tsetAttr -s 2 \".st\";\nselect -ne :renderGlobalsList1;\nselect -ne :defaultShaderList1;\n\tsetAttr -s 5 \".s\";\nselect -ne :postProcessList1;\n\tsetAttr -s 2 \".p\";\nselect -ne :defaultRenderingList1;\n\tsetAttr -s 2 \".r\";\nselect -ne :lightList1;\nselect -ne :standardSurface1;\n\tsetAttr \".b\" 0.80000001192092896;\n\tsetAttr \".bc\" -type \"float3\" 1 1 1 ;\n\tsetAttr \".s\" 0.20000000298023224;\nselect -ne :initialShadingGroup;\n\tsetAttr -s 2 \".dsm\";\n\tsetAttr \".ro\" yes;\nselect -ne :initialParticleSE;\n\tsetAttr \".ro\" yes;\nselect -ne :defaultRenderGlobals;\n\taddAttr -ci true -h true -sn \"dss\" -ln \"defaultSurfaceShader\" -dt \"string\";\n\tsetAttr \".ren\" -type \"string\" \"arnold\";\n\tsetAttr \".outf\" 51;\n\tsetAttr \".imfkey\" -type \"string\" \"exr\";\n\tsetAttr \".an\" yes;\n\tsetAttr \".fs\" 1001;\n\tsetAttr \".ef\" 1001;\n\tsetAttr \".oft\" -type \"string\" \"\";\n\tsetAttr \".pff\" yes;\n\tsetAttr \".ifp\" -type \"string\" \"<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>\";\n\tsetAttr \".rv\" -type \"string\" \"\";\n\tsetAttr \".pram\" -type \"string\" \"\";\n\tsetAttr \".poam\" -type \"string\" \"\";\n\tsetAttr \".prlm\" -type \"string\" \"\";\n\tsetAttr \".polm\" -type \"string\" \"\";\n\tsetAttr \".prm\" -type \"string\" \"\";\n\tsetAttr \".pom\" -type \"string\" \"\";\n\tsetAttr \".dss\" -type \"string\" \"lambert1\";\nselect -ne :defaultResolution;\n\tsetAttr \".w\" 1920;\n\tsetAttr \".h\" 1080;\n\tsetAttr \".pa\" 1;\n\tsetAttr \".dar\" 1.7777777910232544;\nselect -ne :defaultLightSet;\nselect -ne :defaultColorMgtGlobals;\n\tsetAttr \".cfe\" yes;\n\tsetAttr \".cfp\" -type \"string\" \"<MAYA_RESOURCES>/OCIO-configs/Maya-legacy/config.ocio\";\n\tsetAttr \".vtn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".vn\" -type \"string\" \"sRGB gamma\";\n\tsetAttr \".dn\" -type \"string\" \"legacy\";\n\tsetAttr \".wsn\" -type \"string\" \"scene-linear Rec 709/sRGB\";\n\tsetAttr \".ovt\" no;\n\tsetAttr \".povt\" no;\n\tsetAttr \".otn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".potn\" -type \"string\" \"sRGB gamma (legacy)\";\nselect -ne :hardwareRenderGlobals;\n\tsetAttr \".ctrs\" 256;\n\tsetAttr \".btrs\" 512;\nconnectAttr \"rs_Main.ri\" \":persp.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":top.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":front.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":side.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"pDisc1.rlio[0]\";\nconnectAttr \"polyDisc1.output\" \"pDiscShape1.i\";\nconnectAttr \"rs_Main.ri\" \"aiSkyDomeLight1.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"mainCamera.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"model_GRP.rlio[0]\";\nconnectAttr \"polySphere1.out\" \"pSphere1_GEOShape1.i\";\nrelationship \"link\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"link\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nconnectAttr \"layerManager.dli[0]\" \"defaultLayer.id\";\nconnectAttr \"renderLayerManager.rlmi[0]\" \"defaultRenderLayer.rlid\";\nconnectAttr \"Main.msg\" \"renderSetup.frl\";\nconnectAttr \"Main.msg\" \"renderSetup.lrl\";\nconnectAttr \":defaultArnoldDisplayDriver.msg\" \":defaultArnoldRenderOptions.drivers\"\n\t\t -na;\nconnectAttr \":defaultArnoldFilter.msg\" \":defaultArnoldRenderOptions.filt\";\nconnectAttr \":defaultArnoldDriver.msg\" \":defaultArnoldRenderOptions.drvr\";\nconnectAttr \"rs_Main.msg\" \"Main.lrl\";\nconnectAttr \"renderSetup.lit\" \"Main.pls\";\nconnectAttr \"defaultCollection.msg\" \"Main.cl\";\nconnectAttr \"defaultCollection.msg\" \"Main.ch\";\nconnectAttr \"renderLayerManager.rlmi[1]\" \"rs_Main.rlid\";\nconnectAttr \"defaultCollectionSelector.c\" \"defaultCollection.sel\";\nconnectAttr \"Main.lit\" \"defaultCollection.pls\";\nconnectAttr \"Main.nic\" \"defaultCollection.pic\";\nconnectAttr \"model_GRP.iog\" \"modelMain.dsm\" -na;\nconnectAttr \"modelMain.msg\" \"_renderingMain:Main.dnsm\" -na;\nconnectAttr \"model_GRP.iog\" \"_renderingMain1:Main.dsm\" -na;\nconnectAttr \"_renderingMain:Main1.msg\" \"renderingMain1.dnsm\" -na;\nconnectAttr \"Main.msg\" \"_renderingMain:Main1.renderlayer\";\nconnectAttr \"defaultRenderLayer.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"rs_Main.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"aiSkyDomeLightShape1.ltd\" \":lightList1.l\" -na;\nconnectAttr \"pSphere1_GEOShape1.iog\" \":initialShadingGroup.dsm\" -na;\nconnectAttr \"pDiscShape1.iog\" \":initialShadingGroup.dsm\" -na;\nconnectAttr \"aiSkyDomeLight1.iog\" \":defaultLightSet.dsm\" -na;\n// End of test_project_test_asset_test_task_v002.ma\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v001.ma",
    "content": "//Maya ASCII 2023 scene\n//Name: test_project_test_asset_test_task_v002.ma\n//Last modified: Thu, Nov 09, 2023 11:59:33 AM\n//Codeset: 1252\nrequires maya \"2023\";\nrequires -nodeType \"simpleSelector\" -nodeType \"renderSetupLayer\" -nodeType \"renderSetup\"\n\t\t -nodeType \"collection\" \"renderSetup.py\" \"1.0\";\nrequires -nodeType \"aiOptions\" -nodeType \"aiAOVDriver\" -nodeType \"aiAOVFilter\" -nodeType \"aiSkyDomeLight\"\n\t\t \"mtoa\" \"5.2.1.1\";\nrequires -nodeType \"polyDisc\" \"modelingToolkit\" \"0.0.0.0\";\ncurrentUnit -l centimeter -a degree -t pal;\nfileInfo \"application\" \"maya\";\nfileInfo \"product\" \"Maya 2023\";\nfileInfo \"version\" \"2023\";\nfileInfo \"cutIdentifier\" \"202211021031-847a9f9623\";\nfileInfo \"osv\" \"Windows 10 Pro v2009 (Build: 19045)\";\nfileInfo \"license\" \"education\";\nfileInfo \"UUID\" \"591BA477-4DBF-D8A9-D1CE-AEB4E5E95DCA\";\nfileInfo \"OpenPypeContext\" \"eyJwdWJsaXNoX2F0dHJpYnV0ZXMiOiB7IlZhbGlkYXRlQ29udGFpbmVycyI6IHsiYWN0aXZlIjogdHJ1ZX19fQ==\";\ncreateNode transform -s -n \"persp\";\n\trename -uid \"D52C935B-47C9-D868-A875-D799DD17B3A1\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 33.329836010894773 18.034068470832839 24.890981774804157 ;\n\tsetAttr \".r\" -type \"double3\" 79.461647270402509 -44.999999999997357 183.99999999999159 ;\n\tsetAttr \".rp\" -type \"double3\" -2.0401242849359917e-14 2.2609331405046354e-14 -44.821869662029947 ;\n\tsetAttr \".rpt\" -type \"double3\" -27.999999999999989 -21.000000000000025 16.82186966202995 ;\ncreateNode camera -s -n \"perspShape\" -p \"persp\";\n\trename -uid \"2399E6C0-490F-BA1F-485F-5AA8A01D27BC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 50.609449488607154;\n\tsetAttr \".imn\" -type \"string\" \"persp\";\n\tsetAttr \".den\" -type \"string\" \"persp_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".ai_translator\" -type \"string\" \"perspective\";\ncreateNode transform -s -n \"top\";\n\trename -uid \"415C7426-413E-0FAE-FCC3-3DAED7443A52\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 1000.1 0 ;\n\tsetAttr \".r\" -type \"double3\" 90 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" 0 -1000.1 1000.1 ;\ncreateNode camera -s -n \"topShape\" -p \"top\";\n\trename -uid \"3BD0CF60-40DB-5278-5D8B-06ACBDA32122\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"top\";\n\tsetAttr \".den\" -type \"string\" \"top_depth\";\n\tsetAttr \".man\" -type \"string\" \"top_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -t %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"front\";\n\trename -uid \"D83DD5CE-4FE0-AB1B-81B2-87A63F0B7A05\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 0 1000.1 ;\n\tsetAttr \".r\" -type \"double3\" 180 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\ncreateNode camera -s -n \"frontShape\" -p \"front\";\n\trename -uid \"23313CBA-42C2-0B3A-0FCF-EA965EAC5DEC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"front\";\n\tsetAttr \".den\" -type \"string\" \"front_depth\";\n\tsetAttr \".man\" -type \"string\" \"front_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -f %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"side\";\n\trename -uid \"F70F692C-4A0D-BE64-9EE4-A99B6FA2D56E\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 1000.1 0 0 ;\n\tsetAttr \".r\" -type \"double3\" 180 -90 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" -1000.1 0 1000.1 ;\ncreateNode camera -s -n \"sideShape\" -p \"side\";\n\trename -uid \"C05669C3-420E-CA11-E5FC-7EB64EF8B632\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"side\";\n\tsetAttr \".den\" -type \"string\" \"side_depth\";\n\tsetAttr \".man\" -type \"string\" \"side_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -s %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -n \"pDisc1\";\n\trename -uid \"DED70CCF-4C19-16E4-9E5D-66A05037BA47\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:90e762703f08\";\ncreateNode mesh -n \"pDiscShape1\" -p \"pDisc1\";\n\trename -uid \"E1FCDCCF-4DE1-D3B9-C4F8-3285F1CF5B25\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:4ee3da11a1a4\";\ncreateNode transform -n \"aiSkyDomeLight1\";\n\trename -uid \"402BF091-4305-22E3-7CF0-9BA3D7F948F7\";\ncreateNode aiSkyDomeLight -n \"aiSkyDomeLightShape1\" -p \"aiSkyDomeLight1\";\n\trename -uid \"CEF32074-4066-553D-A4FD-65B508A56ABE\";\n\taddAttr -ci true -h true -sn \"aal\" -ln \"attributeAliasList\" -dt \"attributeAlias\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".csh\" no;\n\tsetAttr \".rcsh\" no;\n\tsetAttr \".aal\" -type \"attributeAlias\" {\"exposure\",\"aiExposure\"} ;\ncreateNode transform -n \"mainCamera\";\n\trename -uid \"58651370-474E-02EE-39A9-A2AB27E0DD87\";\n\tsetAttr \".t\" -type \"double3\" 33.329836010894773 18.034068470832839 24.890981774804157 ;\n\tsetAttr \".r\" -type \"double3\" 79.461647270402509 -44.999999999997357 183.99999999999159 ;\n\tsetAttr \".rp\" -type \"double3\" -2.0401242849359917e-14 2.2609331405046354e-14 -44.821869662029947 ;\n\tsetAttr \".rpt\" -type \"double3\" -27.999999999999989 -21.000000000000025 16.82186966202995 ;\ncreateNode camera -n \"mainCameraShape\" -p \"mainCamera\";\n\trename -uid \"CCE11369-4101-EE5E-5381-3F87DA963CA2\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 50.609449488607154;\n\tsetAttr \".imn\" -type \"string\" \"persp\";\n\tsetAttr \".den\" -type \"string\" \"persp_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".ai_translator\" -type \"string\" \"perspective\";\ncreateNode transform -n \"model_GRP\";\n\trename -uid \"445FDC20-4A9D-2C5B-C7BD-F98F6E660B5C\";\ncreateNode transform -n \"pSphere1_GEO\" -p \"model_GRP\";\n\trename -uid \"7445A43F-444F-B2D3-4315-2AA013D2E0B6\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:302a4c6123a4\";\ncreateNode mesh -n \"pSphere1_GEOShape1\" -p \"pSphere1_GEO\";\n\trename -uid \"7C731260-45C6-339E-07BF-359446B08EA1\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:6c77a15a98a9\";\ncreateNode lightLinker -s -n \"lightLinker1\";\n\trename -uid \"D9ADDBD2-49DE-91E2-4166-99A362986A3A\";\n\tsetAttr -s 2 \".lnk\";\n\tsetAttr -s 2 \".slnk\";\ncreateNode shapeEditorManager -n \"shapeEditorManager\";\n\trename -uid \"A232A3B1-4B62-92E7-A7C9-9D9FC5EF010A\";\ncreateNode poseInterpolatorManager -n \"poseInterpolatorManager\";\n\trename -uid \"B30BA35D-492A-834B-3448-49A80BBBFC39\";\ncreateNode displayLayerManager -n \"layerManager\";\n\trename -uid \"4417380F-4A6A-16CC-B1DE-AA95ED9C7FB2\";\ncreateNode displayLayer -n \"defaultLayer\";\n\trename -uid \"4A776D1B-401F-7069-1C74-A7AAE84CEE03\";\n\tsetAttr \".ufem\" -type \"stringArray\" 0  ;\ncreateNode renderLayerManager -n \"renderLayerManager\";\n\trename -uid \"EE21C644-43B8-C754-0BED-709D2EEB204D\";\n\tsetAttr -s 2 \".rlmi[1]\"  1;\n\tsetAttr -s 2 \".rlmi\";\ncreateNode renderLayer -n \"defaultRenderLayer\";\n\trename -uid \"B134920D-4508-23BD-A6CA-11B43DE03F53\";\n\tsetAttr \".g\" yes;\ncreateNode renderSetup -n \"renderSetup\";\n\trename -uid \"9A8F0D15-41AB-CA70-C2D8-B78840BF9BC1\";\ncreateNode polySphere -n \"polySphere1\";\n\trename -uid \"DA319706-4ACF-B15C-53B2-48AC80D202EA\";\ncreateNode script -n \"uiConfigurationScriptNode\";\n\trename -uid \"4B7AFB53-452E-E870-63E1-CCA1DD6EAF13\";\n\tsetAttr \".b\" -type \"string\" (\n\t\t\"// Maya Mel UI Configuration File.\\n//\\n//  This script is machine generated.  Edit at your own risk.\\n//\\n//\\n\\nglobal string $gMainPane;\\nif (`paneLayout -exists $gMainPane`) {\\n\\n\\tglobal int $gUseScenePanelConfig;\\n\\tint    $useSceneConfig = $gUseScenePanelConfig;\\n\\tint    $nodeEditorPanelVisible = stringArrayContains(\\\"nodeEditorPanel1\\\", `getPanel -vis`);\\n\\tint    $nodeEditorWorkspaceControlOpen = (`workspaceControl -exists nodeEditorPanel1Window` && `workspaceControl -q -visible nodeEditorPanel1Window`);\\n\\tint    $menusOkayInPanels = `optionVar -q allowMenusInPanels`;\\n\\tint    $nVisPanes = `paneLayout -q -nvp $gMainPane`;\\n\\tint    $nPanes = 0;\\n\\tstring $editorName;\\n\\tstring $panelName;\\n\\tstring $itemFilterName;\\n\\tstring $panelConfig;\\n\\n\\t//\\n\\t//  get current state of the UI\\n\\t//\\n\\tsceneUIReplacement -update $gMainPane;\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Top View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Top View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|front\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n\"\n\t\t+ \"            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n\"\n\t\t+ \"            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Side View\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Side View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|front\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n\"\n\t\t+ \"            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n\"\n\t\t+ \"            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n\"\n\t\t+ \"        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Front View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Front View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|top\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n\"\n\t\t+ \"            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n\"\n\t\t+ \"            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n\"\n\t\t+ \"            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Persp View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Persp View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|mainCamera\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n\"\n\t\t+ \"            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n\"\n\t\t+ \"            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n\"\n\t\t+ \"            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"ToggledOutliner\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"ToggledOutliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -docTag \\\"isolOutln_fromSeln\\\" \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 1\\n            -showReferenceMembers 1\\n\"\n\t\t+ \"            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n\"\n\t\t+ \"            -isSet 0\\n            -isSetMember 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -selectCommand \\\"print(\\\\\\\"\\\\\\\")\\\" \\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n            -renderFilterIndex 0\\n            -selectionOrder \\\"chronological\\\" \\n            -expandAttribute 0\\n            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"Outliner\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"Outliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 0\\n            -showReferenceMembers 0\\n            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n\"\n\t\t+ \"            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n\"\n\t\t+ \"            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"graphEditor\\\" (localizedPanelLabel(\\\"Graph Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Graph Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 1\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n\"\n\t\t+ \"                -showPublishedAsConnected 0\\n                -showParentContainers 1\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 1\\n                -showCompounds 0\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 1\\n                -doNotSelectNewObjects 0\\n                -dropIsParent 1\\n                -transmitFilters 1\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n\"\n\t\t+ \"                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 1\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"GraphEd\\\");\\n            animCurveEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -showPlayRangeShades \\\"on\\\" \\n                -lockPlayRangeShades \\\"off\\\" \\n                -smoothness \\\"fine\\\" \\n                -resultSamples 1.041667\\n                -resultScreenSamples 0\\n                -resultUpdate \\\"delayed\\\" \\n                -showUpstreamCurves 1\\n                -keyMinScale 1\\n                -stackedCurvesMin -1\\n                -stackedCurvesMax 1\\n\"\n\t\t+ \"                -stackedCurvesSpace 0.2\\n                -preSelectionHighlight 0\\n                -constrainDrag 0\\n                -valueLinesToggle 1\\n                -highlightAffectedCurves 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dopeSheetPanel\\\" (localizedPanelLabel(\\\"Dope Sheet\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dope Sheet\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n\"\n\t\t+ \"                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 0\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n                -showPublishedAsConnected 0\\n                -showParentContainers 1\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 0\\n                -showCompounds 1\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 0\\n                -doNotSelectNewObjects 1\\n                -dropIsParent 1\\n                -transmitFilters 0\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n\"\n\t\t+ \"                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 0\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"DopeSheetEd\\\");\\n            dopeSheetEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -outliner \\\"dopeSheetPanel1OutlineEd\\\" \\n                -showSummary 1\\n                -showScene 0\\n                -hierarchyBelow 0\\n\"\n\t\t+ \"                -showTicks 1\\n                -selectionWindow 0 0 0 0 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"timeEditorPanel\\\" (localizedPanelLabel(\\\"Time Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Time Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"clipEditorPanel\\\" (localizedPanelLabel(\\\"Trax Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Trax Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = clipEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 0 \\n\"\n\t\t+ \"                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"sequenceEditorPanel\\\" (localizedPanelLabel(\\\"Camera Sequencer\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Camera Sequencer\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = sequenceEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 1 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperGraphPanel\\\" (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\n\\t\\t\\t$editorName = ($panelName+\\\"HyperGraphEd\\\");\\n            hyperGraph -e \\n                -graphLayoutStyle \\\"hierarchicalLayout\\\" \\n                -orientation \\\"horiz\\\" \\n                -mergeConnections 0\\n                -zoom 1\\n                -animateTransition 0\\n                -showRelationships 1\\n                -showShapes 0\\n                -showDeformers 0\\n                -showExpressions 0\\n                -showConstraints 0\\n                -showConnectionFromSelected 0\\n                -showConnectionToSelected 0\\n                -showConstraintLabels 0\\n                -showUnderworld 0\\n                -showInvisible 0\\n                -transitionFrames 1\\n                -opaqueContainers 0\\n                -freeform 0\\n                -imagePosition 0 0 \\n                -imageScale 1\\n                -imageEnabled 0\\n                -graphType \\\"DAG\\\" \\n                -heatMapDisplay 0\\n                -updateSelection 1\\n                -updateNodeAdded 1\\n                -useDrawOverrideColor 0\\n                -limitGraphTraversal -1\\n\"\n\t\t+ \"                -range 0 0 \\n                -iconSize \\\"smallIcons\\\" \\n                -showCachedConnections 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperShadePanel\\\" (localizedPanelLabel(\\\"Hypershade\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypershade\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"visorPanel\\\" (localizedPanelLabel(\\\"Visor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Visor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"nodeEditorPanel\\\" (localizedPanelLabel(\\\"Node Editor\\\")) `;\\n\\tif ($nodeEditorPanelVisible || $nodeEditorWorkspaceControlOpen) {\\n\"\n\t\t+ \"\\t\\tif (\\\"\\\" == $panelName) {\\n\\t\\t\\tif ($useSceneConfig) {\\n\\t\\t\\t\\t$panelName = `scriptedPanel -unParent  -type \\\"nodeEditorPanel\\\" -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels `;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n\"\n\t\t+ \"                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\t}\\n\\t\\t} else {\\n\\t\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n\"\n\t\t+ \"                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"createNodePanel\\\" (localizedPanelLabel(\\\"Create Node\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Create Node\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"polyTexturePlacementPanel\\\" (localizedPanelLabel(\\\"UV Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"UV Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"renderWindowPanel\\\" (localizedPanelLabel(\\\"Render View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Render View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"shapePanel\\\" (localizedPanelLabel(\\\"Shape Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tshapePanel -edit -l (localizedPanelLabel(\\\"Shape Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"posePanel\\\" (localizedPanelLabel(\\\"Pose Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tposePanel -edit -l (localizedPanelLabel(\\\"Pose Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynRelEdPanel\\\" (localizedPanelLabel(\\\"Dynamic Relationships\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dynamic Relationships\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"relationshipPanel\\\" (localizedPanelLabel(\\\"Relationship Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Relationship Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"referenceEditorPanel\\\" (localizedPanelLabel(\\\"Reference Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Reference Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynPaintScriptedPanelType\\\" (localizedPanelLabel(\\\"Paint Effects\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Paint Effects\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"scriptEditorPanel\\\" (localizedPanelLabel(\\\"Script Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Script Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"profilerPanel\\\" (localizedPanelLabel(\\\"Profiler Tool\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Profiler Tool\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"contentBrowserPanel\\\" (localizedPanelLabel(\\\"Content Browser\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Content Browser\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\tif ($useSceneConfig) {\\n        string $configName = `getPanel -cwl (localizedPanelLabel(\\\"Current Layout\\\"))`;\\n\"\n\t\t+ \"        if (\\\"\\\" != $configName) {\\n\\t\\t\\tpanelConfiguration -edit -label (localizedPanelLabel(\\\"Current Layout\\\")) \\n\\t\\t\\t\\t-userCreated false\\n\\t\\t\\t\\t-defaultImage \\\"vacantCell.xP:/\\\"\\n\\t\\t\\t\\t-image \\\"\\\"\\n\\t\\t\\t\\t-sc false\\n\\t\\t\\t\\t-configString \\\"global string $gMainPane; paneLayout -e -cn \\\\\\\"quad\\\\\\\" -ps 1 50 50 -ps 2 50 50 -ps 3 50 50 -ps 4 50 50 $gMainPane;\\\"\\n\\t\\t\\t\\t-removeAllPanels\\n\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Top View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera top` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera top` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Persp View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|mainCamera\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|mainCamera\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Side View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera side` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera side` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Front View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t$configName;\\n\\n            setNamedPanelLayout (localizedPanelLabel(\\\"Current Layout\\\"));\\n        }\\n\\n        panelHistory -e -clear mainPanelHistory;\\n        sceneUIReplacement -clear;\\n\\t}\\n\\n\\ngrid -spacing 5 -size 12 -divisions 5 -displayAxes yes -displayGridLines yes -displayDivisionLines yes -displayPerspectiveLabels no -displayOrthographicLabels no -displayAxesBold yes -perspectiveLabelPosition axis -orthographicLabelPosition edge;\\nviewManip -drawCompass 0 -compassAngle 0 -frontParameters \\\"\\\" -homeParameters \\\"\\\" -selectionLockParameters \\\"\\\";\\n}\\n\");\n\tsetAttr \".st\" 3;\ncreateNode script -n \"sceneConfigurationScriptNode\";\n\trename -uid \"72B19BC2-43A2-E229-0A73-2CB861A291D1\";\n\tsetAttr \".b\" -type \"string\" \"playbackOptions -min 1000 -max 1001 -ast 1000 -aet 1001 \";\n\tsetAttr \".st\" 6;\ncreateNode polyDisc -n \"polyDisc1\";\n\trename -uid \"9ED8A7BD-4FFD-6107-4322-35ACD1D3AC42\";\ncreateNode aiOptions -s -n \"defaultArnoldRenderOptions\";\n\trename -uid \"31A81965-48A6-B90D-503D-2FA162B7C982\";\ncreateNode aiAOVFilter -s -n \"defaultArnoldFilter\";\n\trename -uid \"77A2BCB1-4613-905E-080E-B997FD5E1C6F\";\n\tsetAttr \".ai_translator\" -type \"string\" \"gaussian\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDriver\";\n\trename -uid \"C05729BE-4A33-F1DA-C222-3F8AB6EE7504\";\n\tsetAttr \".ai_translator\" -type \"string\" \"exr\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDisplayDriver\";\n\trename -uid \"806C25D7-4284-C09D-A8AE-4A80DBFFFAAF\";\n\tsetAttr \".output_mode\" 0;\n\tsetAttr \".ai_translator\" -type \"string\" \"maya\";\ncreateNode renderSetupLayer -n \"Main\";\n\trename -uid \"DC3F077F-49F5-1D64-BFF3-AAAF06798636\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode renderLayer -n \"rs_Main\";\n\trename -uid \"D798EE14-43EE-D8EF-F4C9-D6B19C9BC029\";\n\tsetAttr \".do\" 1;\ncreateNode collection -n \"defaultCollection\";\n\trename -uid \"0194FCB7-43C4-DC06-C8D6-D9BA2721CCFA\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode simpleSelector -n \"defaultCollectionSelector\";\n\trename -uid \"37D69395-4785-D0BE-AEA0-EEA66D1FAEDF\";\n\tsetAttr \".pat\" -type \"string\" \"*\";\ncreateNode objectSet -n \"modelMain\";\n\trename -uid \"811E4501-4B64-3016-BE29-E18EC09D90B7\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"writeColorSets\" -ln \"writeColorSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"writeFaceSets\" -ln \"writeFaceSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"attr\" -ln \"attr\" -dt \"string\";\n\taddAttr -ci true -sn \"attrPrefix\" -ln \"attrPrefix\" -dt \"string\";\n\taddAttr -ci true -sn \"includeParentHierarchy\" -ln \"includeParentHierarchy\" -min\n\t\t0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:49991563bf50\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"model\";\n\tsetAttr \".subset\" -type \"string\" \"modelMain\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.model\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr -cb on \".writeColorSets\";\n\tsetAttr -cb on \".writeFaceSets\";\n\tsetAttr \".attr\" -type \"string\" \"\";\n\tsetAttr \".attrPrefix\" -type \"string\" \"\";\n\tsetAttr -cb on \".includeParentHierarchy\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateNodeIDsRelated\\\": {\\\"active\\\": true}, \\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ValidateTransformNamingSuffix\\\": {\\\"active\\\": true}, \\\"ValidateColorSets\\\": {\\\"active\\\": true}, \\\"ValidateMeshHasUVs\\\": {\\\"active\\\": true}, \\\"ValidateMeshNonZeroEdgeLength\\\": {\\\"active\\\": true}, \\\"ExtractModel\\\": {\\\"active\\\": true}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"writeColorSets,writeFaceSets,includeParentHierarchy,attr,attrPrefix\";\ncreateNode objectSet -n \"_renderingMain:Main\";\n\trename -uid \"4E1D2600-482D-425C-352A-74BA418DC374\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:752f6f925fe6\";\ncreateNode objectSet -n \"_renderingMain1:Main\";\n\trename -uid \"A6382090-4537-44CB-E6DC-A5A58B98D008\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:d587b60d712e\";\ncreateNode objectSet -n \"workfileMain\";\n\trename -uid \"0F32608C-4AA1-F493-205C-25BDABF95CEC\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".hio\" yes;\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"workfile\";\n\tsetAttr \".subset\" -type \"string\" \"workfileTest_task\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.workfile\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ExtractImportReference\\\": {\\\"active\\\": false}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:eee200c4dd38\";\ncreateNode objectSet -n \"renderingMain1\";\n\trename -uid \"9AC6AB5B-45EB-8439-BB6C-C197555E11E8\";\n\taddAttr -ci true -sn \"pre_creator_identifier\" -ln \"pre_creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".pre_creator_identifier\" -type \"string\" \"io.openpype.creators.maya.renderlayer\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:e72a7af3a6c5\";\ncreateNode objectSet -n \"_renderingMain:Main1\";\n\trename -uid \"7CFC031E-42E2-E431-89AB-5A991800F6F2\";\n\taddAttr -s false -ci true -sn \"renderlayer\" -ln \"renderlayer\" -at \"message\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"deadlineServers\" -ln \"deadlineServers\" -dt \"string\";\n\taddAttr -ci true -sn \"suspendPublishJob\" -ln \"suspendPublishJob\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"priority\" -ln \"priority\" -at \"long\";\n\taddAttr -ci true -sn \"tile_priority\" -ln \"tile_priority\" -at \"long\";\n\taddAttr -ci true -sn \"framesPerTask\" -ln \"framesPerTask\" -at \"long\";\n\taddAttr -ci true -sn \"whitelist\" -ln \"whitelist\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"machineList\" -ln \"machineList\" -dt \"string\";\n\taddAttr -ci true -sn \"useMayaBatch\" -ln \"useMayaBatch\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"primaryPool\" -ln \"primaryPool\" -dt \"string\";\n\taddAttr -ci true -sn \"secondaryPool\" -ln \"secondaryPool\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"review\" -ln \"review\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"extendFrames\" -ln \"extendFrames\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"overrideExistingFrame\" -ln \"overrideExistingFrame\" -min 0\n\t\t-max 1 -at \"bool\";\n\taddAttr -ci true -sn \"tileRendering\" -ln \"tileRendering\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"tilesX\" -ln \"tilesX\" -at \"long\";\n\taddAttr -ci true -sn \"tilesY\" -ln \"tilesY\" -at \"long\";\n\taddAttr -ci true -sn \"convertToScanline\" -ln \"convertToScanline\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"useReferencedAovs\" -ln \"useReferencedAovs\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"renderSetupIncludeLights\" -ln \"renderSetupIncludeLights\" -min\n\t\t0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:0850eb5268f2\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"renderlayer\";\n\tsetAttr \".subset\" -type \"string\" \"renderTest_taskMain\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.renderlayer\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".deadlineServers\" -type \"string\" \"default\";\n\tsetAttr -cb on \".suspendPublishJob\";\n\tsetAttr -cb on \".priority\" 50;\n\tsetAttr -cb on \".tile_priority\" 50;\n\tsetAttr -cb on \".framesPerTask\" 1;\n\tsetAttr -cb on \".whitelist\";\n\tsetAttr \".machineList\" -type \"string\" \"\";\n\tsetAttr -cb on \".useMayaBatch\";\n\tsetAttr \".primaryPool\" -type \"string\" \"none\";\n\tsetAttr \".secondaryPool\" -type \"string\" \"-\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr -cb on \".review\" yes;\n\tsetAttr -cb on \".extendFrames\";\n\tsetAttr -cb on \".overrideExistingFrame\" yes;\n\tsetAttr -cb on \".tileRendering\";\n\tsetAttr -cb on \".tilesX\" 2;\n\tsetAttr -cb on \".tilesY\" 2;\n\tsetAttr -cb on \".convertToScanline\";\n\tsetAttr -cb on \".useReferencedAovs\";\n\tsetAttr -cb on \".renderSetupIncludeLights\" yes;\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"CollectDeadlinePools\\\": {\\\"primaryPool\\\": \\\"\\\", \\\"secondaryPool\\\": \\\"\\\"}, \\\"ValidateResolution\\\": {\\\"active\\\": true}, \\\"ValidateDeadlinePools\\\": {\\\"active\\\": true}, \\\"ValidateFrameRange\\\": {\\\"active\\\": true}, \\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ExtractImportReference\\\": {\\\"active\\\": false}, \\\"MayaSubmitDeadline\\\": {\\\"priority\\\": 50, \\\"chunkSize\\\": 1, \\\"machineList\\\": \\\"\\\", \\\"whitelist\\\": false, \\\"tile_priority\\\": 50, \\\"strict_error_checking\\\": true}, \\\"ProcessSubmittedJobOnFarm\\\": {\\\"publishJobState\\\": \\\"Active\\\"}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"review,extendFrames,overrideExistingFrame,tileRendering,tilesX,tilesY,convertToScanline,useReferencedAovs,renderSetupIncludeLights\";\nselect -ne :time1;\n\tsetAttr \".o\" 1001;\n\tsetAttr \".unw\" 1001;\nselect -ne :hardwareRenderingGlobals;\n\tsetAttr \".otfna\" -type \"stringArray\" 22 \"NURBS Curves\" \"NURBS Surfaces\" \"Polygons\" \"Subdiv Surface\" \"Particles\" \"Particle Instance\" \"Fluids\" \"Strokes\" \"Image Planes\" \"UI\" \"Lights\" \"Cameras\" \"Locators\" \"Joints\" \"IK Handles\" \"Deformers\" \"Motion Trails\" \"Components\" \"Hair Systems\" \"Follicles\" \"Misc. UI\" \"Ornaments\"  ;\n\tsetAttr \".otfva\" -type \"Int32Array\" 22 0 1 1 1 1 1\n\t\t 1 1 1 0 0 0 0 0 0 0 0 0\n\t\t 0 0 0 0 ;\n\tsetAttr \".fprt\" yes;\nselect -ne :renderPartition;\n\tsetAttr -s 2 \".st\";\nselect -ne :renderGlobalsList1;\nselect -ne :defaultShaderList1;\n\tsetAttr -s 5 \".s\";\nselect -ne :postProcessList1;\n\tsetAttr -s 2 \".p\";\nselect -ne :defaultRenderingList1;\n\tsetAttr -s 2 \".r\";\nselect -ne :lightList1;\nselect -ne :standardSurface1;\n\tsetAttr \".b\" 0.80000001192092896;\n\tsetAttr \".bc\" -type \"float3\" 1 1 1 ;\n\tsetAttr \".s\" 0.20000000298023224;\nselect -ne :initialShadingGroup;\n\tsetAttr -s 2 \".dsm\";\n\tsetAttr \".ro\" yes;\nselect -ne :initialParticleSE;\n\tsetAttr \".ro\" yes;\nselect -ne :defaultRenderGlobals;\n\taddAttr -ci true -h true -sn \"dss\" -ln \"defaultSurfaceShader\" -dt \"string\";\n\tsetAttr \".ren\" -type \"string\" \"arnold\";\n\tsetAttr \".outf\" 51;\n\tsetAttr \".imfkey\" -type \"string\" \"exr\";\n\tsetAttr \".an\" yes;\n\tsetAttr \".fs\" 1001;\n\tsetAttr \".ef\" 1001;\n\tsetAttr \".oft\" -type \"string\" \"\";\n\tsetAttr \".pff\" yes;\n\tsetAttr \".ifp\" -type \"string\" \"<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>\";\n\tsetAttr \".rv\" -type \"string\" \"\";\n\tsetAttr \".pram\" -type \"string\" \"\";\n\tsetAttr \".poam\" -type \"string\" \"\";\n\tsetAttr \".prlm\" -type \"string\" \"\";\n\tsetAttr \".polm\" -type \"string\" \"\";\n\tsetAttr \".prm\" -type \"string\" \"\";\n\tsetAttr \".pom\" -type \"string\" \"\";\n\tsetAttr \".dss\" -type \"string\" \"lambert1\";\nselect -ne :defaultResolution;\n\tsetAttr \".w\" 1920;\n\tsetAttr \".h\" 1080;\n\tsetAttr \".pa\" 1;\n\tsetAttr \".dar\" 1.7777777910232544;\nselect -ne :defaultLightSet;\nselect -ne :defaultColorMgtGlobals;\n\tsetAttr \".cfe\" yes;\n\tsetAttr \".cfp\" -type \"string\" \"<MAYA_RESOURCES>/OCIO-configs/Maya-legacy/config.ocio\";\n\tsetAttr \".vtn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".vn\" -type \"string\" \"sRGB gamma\";\n\tsetAttr \".dn\" -type \"string\" \"legacy\";\n\tsetAttr \".wsn\" -type \"string\" \"scene-linear Rec 709/sRGB\";\n\tsetAttr \".ovt\" no;\n\tsetAttr \".povt\" no;\n\tsetAttr \".otn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".potn\" -type \"string\" \"sRGB gamma (legacy)\";\nselect -ne :hardwareRenderGlobals;\n\tsetAttr \".ctrs\" 256;\n\tsetAttr \".btrs\" 512;\nconnectAttr \"rs_Main.ri\" \":persp.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":top.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":front.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":side.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"pDisc1.rlio[0]\";\nconnectAttr \"polyDisc1.output\" \"pDiscShape1.i\";\nconnectAttr \"rs_Main.ri\" \"aiSkyDomeLight1.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"mainCamera.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"model_GRP.rlio[0]\";\nconnectAttr \"polySphere1.out\" \"pSphere1_GEOShape1.i\";\nrelationship \"link\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"link\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nconnectAttr \"layerManager.dli[0]\" \"defaultLayer.id\";\nconnectAttr \"renderLayerManager.rlmi[0]\" \"defaultRenderLayer.rlid\";\nconnectAttr \"Main.msg\" \"renderSetup.frl\";\nconnectAttr \"Main.msg\" \"renderSetup.lrl\";\nconnectAttr \":defaultArnoldDisplayDriver.msg\" \":defaultArnoldRenderOptions.drivers\"\n\t\t -na;\nconnectAttr \":defaultArnoldFilter.msg\" \":defaultArnoldRenderOptions.filt\";\nconnectAttr \":defaultArnoldDriver.msg\" \":defaultArnoldRenderOptions.drvr\";\nconnectAttr \"rs_Main.msg\" \"Main.lrl\";\nconnectAttr \"renderSetup.lit\" \"Main.pls\";\nconnectAttr \"defaultCollection.msg\" \"Main.cl\";\nconnectAttr \"defaultCollection.msg\" \"Main.ch\";\nconnectAttr \"renderLayerManager.rlmi[1]\" \"rs_Main.rlid\";\nconnectAttr \"defaultCollectionSelector.c\" \"defaultCollection.sel\";\nconnectAttr \"Main.lit\" \"defaultCollection.pls\";\nconnectAttr \"Main.nic\" \"defaultCollection.pic\";\nconnectAttr \"model_GRP.iog\" \"modelMain.dsm\" -na;\nconnectAttr \"modelMain.msg\" \"_renderingMain:Main.dnsm\" -na;\nconnectAttr \"model_GRP.iog\" \"_renderingMain1:Main.dsm\" -na;\nconnectAttr \"_renderingMain:Main1.msg\" \"renderingMain1.dnsm\" -na;\nconnectAttr \"Main.msg\" \"_renderingMain:Main1.renderlayer\";\nconnectAttr \"defaultRenderLayer.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"rs_Main.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"aiSkyDomeLightShape1.ltd\" \":lightList1.l\" -na;\nconnectAttr \"pSphere1_GEOShape1.iog\" \":initialShadingGroup.dsm\" -na;\nconnectAttr \"pDiscShape1.iog\" \":initialShadingGroup.dsm\" -na;\nconnectAttr \"aiSkyDomeLight1.iog\" \":defaultLightSet.dsm\" -na;\n// End of test_project_test_asset_test_task_v002.ma\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v002.ma",
    "content": "//Maya ASCII 2023 scene\n//Name: test_project_test_asset_test_task_v002.ma\n//Last modified: Thu, Nov 09, 2023 11:59:33 AM\n//Codeset: 1252\nrequires maya \"2023\";\nrequires -nodeType \"simpleSelector\" -nodeType \"renderSetupLayer\" -nodeType \"renderSetup\"\n\t\t -nodeType \"collection\" \"renderSetup.py\" \"1.0\";\nrequires -nodeType \"aiOptions\" -nodeType \"aiAOVDriver\" -nodeType \"aiAOVFilter\" -nodeType \"aiSkyDomeLight\"\n\t\t \"mtoa\" \"5.2.1.1\";\nrequires -nodeType \"polyDisc\" \"modelingToolkit\" \"0.0.0.0\";\ncurrentUnit -l centimeter -a degree -t pal;\nfileInfo \"application\" \"maya\";\nfileInfo \"product\" \"Maya 2023\";\nfileInfo \"version\" \"2023\";\nfileInfo \"cutIdentifier\" \"202211021031-847a9f9623\";\nfileInfo \"osv\" \"Windows 10 Pro v2009 (Build: 19045)\";\nfileInfo \"license\" \"education\";\nfileInfo \"UUID\" \"591BA477-4DBF-D8A9-D1CE-AEB4E5E95DCA\";\nfileInfo \"OpenPypeContext\" \"eyJwdWJsaXNoX2F0dHJpYnV0ZXMiOiB7IlZhbGlkYXRlQ29udGFpbmVycyI6IHsiYWN0aXZlIjogdHJ1ZX19fQ==\";\ncreateNode transform -s -n \"persp\";\n\trename -uid \"D52C935B-47C9-D868-A875-D799DD17B3A1\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 33.329836010894773 18.034068470832839 24.890981774804157 ;\n\tsetAttr \".r\" -type \"double3\" 79.461647270402509 -44.999999999997357 183.99999999999159 ;\n\tsetAttr \".rp\" -type \"double3\" -2.0401242849359917e-14 2.2609331405046354e-14 -44.821869662029947 ;\n\tsetAttr \".rpt\" -type \"double3\" -27.999999999999989 -21.000000000000025 16.82186966202995 ;\ncreateNode camera -s -n \"perspShape\" -p \"persp\";\n\trename -uid \"2399E6C0-490F-BA1F-485F-5AA8A01D27BC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 50.609449488607154;\n\tsetAttr \".imn\" -type \"string\" \"persp\";\n\tsetAttr \".den\" -type \"string\" \"persp_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".ai_translator\" -type \"string\" \"perspective\";\ncreateNode transform -s -n \"top\";\n\trename -uid \"415C7426-413E-0FAE-FCC3-3DAED7443A52\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 1000.1 0 ;\n\tsetAttr \".r\" -type \"double3\" 90 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" 0 -1000.1 1000.1 ;\ncreateNode camera -s -n \"topShape\" -p \"top\";\n\trename -uid \"3BD0CF60-40DB-5278-5D8B-06ACBDA32122\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"top\";\n\tsetAttr \".den\" -type \"string\" \"top_depth\";\n\tsetAttr \".man\" -type \"string\" \"top_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -t %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"front\";\n\trename -uid \"D83DD5CE-4FE0-AB1B-81B2-87A63F0B7A05\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 0 1000.1 ;\n\tsetAttr \".r\" -type \"double3\" 180 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\ncreateNode camera -s -n \"frontShape\" -p \"front\";\n\trename -uid \"23313CBA-42C2-0B3A-0FCF-EA965EAC5DEC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"front\";\n\tsetAttr \".den\" -type \"string\" \"front_depth\";\n\tsetAttr \".man\" -type \"string\" \"front_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -f %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"side\";\n\trename -uid \"F70F692C-4A0D-BE64-9EE4-A99B6FA2D56E\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 1000.1 0 0 ;\n\tsetAttr \".r\" -type \"double3\" 180 -90 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" -1000.1 0 1000.1 ;\ncreateNode camera -s -n \"sideShape\" -p \"side\";\n\trename -uid \"C05669C3-420E-CA11-E5FC-7EB64EF8B632\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"side\";\n\tsetAttr \".den\" -type \"string\" \"side_depth\";\n\tsetAttr \".man\" -type \"string\" \"side_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -s %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -n \"pDisc1\";\n\trename -uid \"DED70CCF-4C19-16E4-9E5D-66A05037BA47\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:90e762703f08\";\ncreateNode mesh -n \"pDiscShape1\" -p \"pDisc1\";\n\trename -uid \"E1FCDCCF-4DE1-D3B9-C4F8-3285F1CF5B25\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:4ee3da11a1a4\";\ncreateNode transform -n \"aiSkyDomeLight1\";\n\trename -uid \"402BF091-4305-22E3-7CF0-9BA3D7F948F7\";\ncreateNode aiSkyDomeLight -n \"aiSkyDomeLightShape1\" -p \"aiSkyDomeLight1\";\n\trename -uid \"CEF32074-4066-553D-A4FD-65B508A56ABE\";\n\taddAttr -ci true -h true -sn \"aal\" -ln \"attributeAliasList\" -dt \"attributeAlias\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".csh\" no;\n\tsetAttr \".rcsh\" no;\n\tsetAttr \".aal\" -type \"attributeAlias\" {\"exposure\",\"aiExposure\"} ;\ncreateNode transform -n \"mainCamera\";\n\trename -uid \"58651370-474E-02EE-39A9-A2AB27E0DD87\";\n\tsetAttr \".t\" -type \"double3\" 33.329836010894773 18.034068470832839 24.890981774804157 ;\n\tsetAttr \".r\" -type \"double3\" 79.461647270402509 -44.999999999997357 183.99999999999159 ;\n\tsetAttr \".rp\" -type \"double3\" -2.0401242849359917e-14 2.2609331405046354e-14 -44.821869662029947 ;\n\tsetAttr \".rpt\" -type \"double3\" -27.999999999999989 -21.000000000000025 16.82186966202995 ;\ncreateNode camera -n \"mainCameraShape\" -p \"mainCamera\";\n\trename -uid \"CCE11369-4101-EE5E-5381-3F87DA963CA2\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 50.609449488607154;\n\tsetAttr \".imn\" -type \"string\" \"persp\";\n\tsetAttr \".den\" -type \"string\" \"persp_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".ai_translator\" -type \"string\" \"perspective\";\ncreateNode transform -n \"model_GRP\";\n\trename -uid \"445FDC20-4A9D-2C5B-C7BD-F98F6E660B5C\";\ncreateNode transform -n \"pSphere1_GEO\" -p \"model_GRP\";\n\trename -uid \"7445A43F-444F-B2D3-4315-2AA013D2E0B6\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:302a4c6123a4\";\ncreateNode mesh -n \"pSphere1_GEOShape1\" -p \"pSphere1_GEO\";\n\trename -uid \"7C731260-45C6-339E-07BF-359446B08EA1\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:6c77a15a98a9\";\ncreateNode lightLinker -s -n \"lightLinker1\";\n\trename -uid \"D9ADDBD2-49DE-91E2-4166-99A362986A3A\";\n\tsetAttr -s 2 \".lnk\";\n\tsetAttr -s 2 \".slnk\";\ncreateNode shapeEditorManager -n \"shapeEditorManager\";\n\trename -uid \"A232A3B1-4B62-92E7-A7C9-9D9FC5EF010A\";\ncreateNode poseInterpolatorManager -n \"poseInterpolatorManager\";\n\trename -uid \"B30BA35D-492A-834B-3448-49A80BBBFC39\";\ncreateNode displayLayerManager -n \"layerManager\";\n\trename -uid \"4417380F-4A6A-16CC-B1DE-AA95ED9C7FB2\";\ncreateNode displayLayer -n \"defaultLayer\";\n\trename -uid \"4A776D1B-401F-7069-1C74-A7AAE84CEE03\";\n\tsetAttr \".ufem\" -type \"stringArray\" 0  ;\ncreateNode renderLayerManager -n \"renderLayerManager\";\n\trename -uid \"EE21C644-43B8-C754-0BED-709D2EEB204D\";\n\tsetAttr -s 2 \".rlmi[1]\"  1;\n\tsetAttr -s 2 \".rlmi\";\ncreateNode renderLayer -n \"defaultRenderLayer\";\n\trename -uid \"B134920D-4508-23BD-A6CA-11B43DE03F53\";\n\tsetAttr \".g\" yes;\ncreateNode renderSetup -n \"renderSetup\";\n\trename -uid \"9A8F0D15-41AB-CA70-C2D8-B78840BF9BC1\";\ncreateNode polySphere -n \"polySphere1\";\n\trename -uid \"DA319706-4ACF-B15C-53B2-48AC80D202EA\";\ncreateNode script -n \"uiConfigurationScriptNode\";\n\trename -uid \"4B7AFB53-452E-E870-63E1-CCA1DD6EAF13\";\n\tsetAttr \".b\" -type \"string\" (\n\t\t\"// Maya Mel UI Configuration File.\\n//\\n//  This script is machine generated.  Edit at your own risk.\\n//\\n//\\n\\nglobal string $gMainPane;\\nif (`paneLayout -exists $gMainPane`) {\\n\\n\\tglobal int $gUseScenePanelConfig;\\n\\tint    $useSceneConfig = $gUseScenePanelConfig;\\n\\tint    $nodeEditorPanelVisible = stringArrayContains(\\\"nodeEditorPanel1\\\", `getPanel -vis`);\\n\\tint    $nodeEditorWorkspaceControlOpen = (`workspaceControl -exists nodeEditorPanel1Window` && `workspaceControl -q -visible nodeEditorPanel1Window`);\\n\\tint    $menusOkayInPanels = `optionVar -q allowMenusInPanels`;\\n\\tint    $nVisPanes = `paneLayout -q -nvp $gMainPane`;\\n\\tint    $nPanes = 0;\\n\\tstring $editorName;\\n\\tstring $panelName;\\n\\tstring $itemFilterName;\\n\\tstring $panelConfig;\\n\\n\\t//\\n\\t//  get current state of the UI\\n\\t//\\n\\tsceneUIReplacement -update $gMainPane;\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Top View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Top View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|front\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n\"\n\t\t+ \"            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n\"\n\t\t+ \"            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Side View\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Side View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|front\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n\"\n\t\t+ \"            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n\"\n\t\t+ \"            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n\"\n\t\t+ \"        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Front View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Front View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|top\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n\"\n\t\t+ \"            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n\"\n\t\t+ \"            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n\"\n\t\t+ \"            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Persp View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Persp View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|mainCamera\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n\"\n\t\t+ \"            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n\"\n\t\t+ \"            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n\"\n\t\t+ \"            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"ToggledOutliner\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"ToggledOutliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -docTag \\\"isolOutln_fromSeln\\\" \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 1\\n            -showReferenceMembers 1\\n\"\n\t\t+ \"            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n\"\n\t\t+ \"            -isSet 0\\n            -isSetMember 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -selectCommand \\\"print(\\\\\\\"\\\\\\\")\\\" \\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n            -renderFilterIndex 0\\n            -selectionOrder \\\"chronological\\\" \\n            -expandAttribute 0\\n            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"Outliner\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"Outliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 0\\n            -showReferenceMembers 0\\n            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n\"\n\t\t+ \"            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n\"\n\t\t+ \"            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"graphEditor\\\" (localizedPanelLabel(\\\"Graph Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Graph Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 1\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n\"\n\t\t+ \"                -showPublishedAsConnected 0\\n                -showParentContainers 1\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 1\\n                -showCompounds 0\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 1\\n                -doNotSelectNewObjects 0\\n                -dropIsParent 1\\n                -transmitFilters 1\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n\"\n\t\t+ \"                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 1\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"GraphEd\\\");\\n            animCurveEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -showPlayRangeShades \\\"on\\\" \\n                -lockPlayRangeShades \\\"off\\\" \\n                -smoothness \\\"fine\\\" \\n                -resultSamples 1.041667\\n                -resultScreenSamples 0\\n                -resultUpdate \\\"delayed\\\" \\n                -showUpstreamCurves 1\\n                -keyMinScale 1\\n                -stackedCurvesMin -1\\n                -stackedCurvesMax 1\\n\"\n\t\t+ \"                -stackedCurvesSpace 0.2\\n                -preSelectionHighlight 0\\n                -constrainDrag 0\\n                -valueLinesToggle 1\\n                -highlightAffectedCurves 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dopeSheetPanel\\\" (localizedPanelLabel(\\\"Dope Sheet\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dope Sheet\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n\"\n\t\t+ \"                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 0\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n                -showPublishedAsConnected 0\\n                -showParentContainers 1\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 0\\n                -showCompounds 1\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 0\\n                -doNotSelectNewObjects 1\\n                -dropIsParent 1\\n                -transmitFilters 0\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n\"\n\t\t+ \"                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 0\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"DopeSheetEd\\\");\\n            dopeSheetEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -outliner \\\"dopeSheetPanel1OutlineEd\\\" \\n                -showSummary 1\\n                -showScene 0\\n                -hierarchyBelow 0\\n\"\n\t\t+ \"                -showTicks 1\\n                -selectionWindow 0 0 0 0 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"timeEditorPanel\\\" (localizedPanelLabel(\\\"Time Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Time Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"clipEditorPanel\\\" (localizedPanelLabel(\\\"Trax Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Trax Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = clipEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 0 \\n\"\n\t\t+ \"                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"sequenceEditorPanel\\\" (localizedPanelLabel(\\\"Camera Sequencer\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Camera Sequencer\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = sequenceEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 1 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperGraphPanel\\\" (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\n\\t\\t\\t$editorName = ($panelName+\\\"HyperGraphEd\\\");\\n            hyperGraph -e \\n                -graphLayoutStyle \\\"hierarchicalLayout\\\" \\n                -orientation \\\"horiz\\\" \\n                -mergeConnections 0\\n                -zoom 1\\n                -animateTransition 0\\n                -showRelationships 1\\n                -showShapes 0\\n                -showDeformers 0\\n                -showExpressions 0\\n                -showConstraints 0\\n                -showConnectionFromSelected 0\\n                -showConnectionToSelected 0\\n                -showConstraintLabels 0\\n                -showUnderworld 0\\n                -showInvisible 0\\n                -transitionFrames 1\\n                -opaqueContainers 0\\n                -freeform 0\\n                -imagePosition 0 0 \\n                -imageScale 1\\n                -imageEnabled 0\\n                -graphType \\\"DAG\\\" \\n                -heatMapDisplay 0\\n                -updateSelection 1\\n                -updateNodeAdded 1\\n                -useDrawOverrideColor 0\\n                -limitGraphTraversal -1\\n\"\n\t\t+ \"                -range 0 0 \\n                -iconSize \\\"smallIcons\\\" \\n                -showCachedConnections 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperShadePanel\\\" (localizedPanelLabel(\\\"Hypershade\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypershade\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"visorPanel\\\" (localizedPanelLabel(\\\"Visor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Visor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"nodeEditorPanel\\\" (localizedPanelLabel(\\\"Node Editor\\\")) `;\\n\\tif ($nodeEditorPanelVisible || $nodeEditorWorkspaceControlOpen) {\\n\"\n\t\t+ \"\\t\\tif (\\\"\\\" == $panelName) {\\n\\t\\t\\tif ($useSceneConfig) {\\n\\t\\t\\t\\t$panelName = `scriptedPanel -unParent  -type \\\"nodeEditorPanel\\\" -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels `;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n\"\n\t\t+ \"                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\t}\\n\\t\\t} else {\\n\\t\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n\"\n\t\t+ \"                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"createNodePanel\\\" (localizedPanelLabel(\\\"Create Node\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Create Node\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"polyTexturePlacementPanel\\\" (localizedPanelLabel(\\\"UV Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"UV Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"renderWindowPanel\\\" (localizedPanelLabel(\\\"Render View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Render View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"shapePanel\\\" (localizedPanelLabel(\\\"Shape Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tshapePanel -edit -l (localizedPanelLabel(\\\"Shape Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"posePanel\\\" (localizedPanelLabel(\\\"Pose Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tposePanel -edit -l (localizedPanelLabel(\\\"Pose Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynRelEdPanel\\\" (localizedPanelLabel(\\\"Dynamic Relationships\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dynamic Relationships\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"relationshipPanel\\\" (localizedPanelLabel(\\\"Relationship Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Relationship Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"referenceEditorPanel\\\" (localizedPanelLabel(\\\"Reference Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Reference Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynPaintScriptedPanelType\\\" (localizedPanelLabel(\\\"Paint Effects\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Paint Effects\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"scriptEditorPanel\\\" (localizedPanelLabel(\\\"Script Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Script Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"profilerPanel\\\" (localizedPanelLabel(\\\"Profiler Tool\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Profiler Tool\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"contentBrowserPanel\\\" (localizedPanelLabel(\\\"Content Browser\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Content Browser\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\tif ($useSceneConfig) {\\n        string $configName = `getPanel -cwl (localizedPanelLabel(\\\"Current Layout\\\"))`;\\n\"\n\t\t+ \"        if (\\\"\\\" != $configName) {\\n\\t\\t\\tpanelConfiguration -edit -label (localizedPanelLabel(\\\"Current Layout\\\")) \\n\\t\\t\\t\\t-userCreated false\\n\\t\\t\\t\\t-defaultImage \\\"vacantCell.xP:/\\\"\\n\\t\\t\\t\\t-image \\\"\\\"\\n\\t\\t\\t\\t-sc false\\n\\t\\t\\t\\t-configString \\\"global string $gMainPane; paneLayout -e -cn \\\\\\\"quad\\\\\\\" -ps 1 50 50 -ps 2 50 50 -ps 3 50 50 -ps 4 50 50 $gMainPane;\\\"\\n\\t\\t\\t\\t-removeAllPanels\\n\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Top View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera top` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera top` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Persp View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|mainCamera\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|mainCamera\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Side View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera side` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera side` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Front View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t$configName;\\n\\n            setNamedPanelLayout (localizedPanelLabel(\\\"Current Layout\\\"));\\n        }\\n\\n        panelHistory -e -clear mainPanelHistory;\\n        sceneUIReplacement -clear;\\n\\t}\\n\\n\\ngrid -spacing 5 -size 12 -divisions 5 -displayAxes yes -displayGridLines yes -displayDivisionLines yes -displayPerspectiveLabels no -displayOrthographicLabels no -displayAxesBold yes -perspectiveLabelPosition axis -orthographicLabelPosition edge;\\nviewManip -drawCompass 0 -compassAngle 0 -frontParameters \\\"\\\" -homeParameters \\\"\\\" -selectionLockParameters \\\"\\\";\\n}\\n\");\n\tsetAttr \".st\" 3;\ncreateNode script -n \"sceneConfigurationScriptNode\";\n\trename -uid \"72B19BC2-43A2-E229-0A73-2CB861A291D1\";\n\tsetAttr \".b\" -type \"string\" \"playbackOptions -min 1000 -max 1001 -ast 1000 -aet 1001 \";\n\tsetAttr \".st\" 6;\ncreateNode polyDisc -n \"polyDisc1\";\n\trename -uid \"9ED8A7BD-4FFD-6107-4322-35ACD1D3AC42\";\ncreateNode aiOptions -s -n \"defaultArnoldRenderOptions\";\n\trename -uid \"31A81965-48A6-B90D-503D-2FA162B7C982\";\ncreateNode aiAOVFilter -s -n \"defaultArnoldFilter\";\n\trename -uid \"77A2BCB1-4613-905E-080E-B997FD5E1C6F\";\n\tsetAttr \".ai_translator\" -type \"string\" \"gaussian\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDriver\";\n\trename -uid \"C05729BE-4A33-F1DA-C222-3F8AB6EE7504\";\n\tsetAttr \".ai_translator\" -type \"string\" \"exr\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDisplayDriver\";\n\trename -uid \"806C25D7-4284-C09D-A8AE-4A80DBFFFAAF\";\n\tsetAttr \".output_mode\" 0;\n\tsetAttr \".ai_translator\" -type \"string\" \"maya\";\ncreateNode renderSetupLayer -n \"Main\";\n\trename -uid \"DC3F077F-49F5-1D64-BFF3-AAAF06798636\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode renderLayer -n \"rs_Main\";\n\trename -uid \"D798EE14-43EE-D8EF-F4C9-D6B19C9BC029\";\n\tsetAttr \".do\" 1;\ncreateNode collection -n \"defaultCollection\";\n\trename -uid \"0194FCB7-43C4-DC06-C8D6-D9BA2721CCFA\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode simpleSelector -n \"defaultCollectionSelector\";\n\trename -uid \"37D69395-4785-D0BE-AEA0-EEA66D1FAEDF\";\n\tsetAttr \".pat\" -type \"string\" \"*\";\ncreateNode objectSet -n \"modelMain\";\n\trename -uid \"811E4501-4B64-3016-BE29-E18EC09D90B7\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"writeColorSets\" -ln \"writeColorSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"writeFaceSets\" -ln \"writeFaceSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"attr\" -ln \"attr\" -dt \"string\";\n\taddAttr -ci true -sn \"attrPrefix\" -ln \"attrPrefix\" -dt \"string\";\n\taddAttr -ci true -sn \"includeParentHierarchy\" -ln \"includeParentHierarchy\" -min\n\t\t0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:49991563bf50\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"model\";\n\tsetAttr \".subset\" -type \"string\" \"modelMain\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.model\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr -cb on \".writeColorSets\";\n\tsetAttr -cb on \".writeFaceSets\";\n\tsetAttr \".attr\" -type \"string\" \"\";\n\tsetAttr \".attrPrefix\" -type \"string\" \"\";\n\tsetAttr -cb on \".includeParentHierarchy\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateNodeIDsRelated\\\": {\\\"active\\\": true}, \\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ValidateTransformNamingSuffix\\\": {\\\"active\\\": true}, \\\"ValidateColorSets\\\": {\\\"active\\\": true}, \\\"ValidateMeshHasUVs\\\": {\\\"active\\\": true}, \\\"ValidateMeshNonZeroEdgeLength\\\": {\\\"active\\\": true}, \\\"ExtractModel\\\": {\\\"active\\\": true}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"writeColorSets,writeFaceSets,includeParentHierarchy,attr,attrPrefix\";\ncreateNode objectSet -n \"_renderingMain:Main\";\n\trename -uid \"4E1D2600-482D-425C-352A-74BA418DC374\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:752f6f925fe6\";\ncreateNode objectSet -n \"_renderingMain1:Main\";\n\trename -uid \"A6382090-4537-44CB-E6DC-A5A58B98D008\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:d587b60d712e\";\ncreateNode objectSet -n \"workfileMain\";\n\trename -uid \"0F32608C-4AA1-F493-205C-25BDABF95CEC\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".hio\" yes;\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"workfile\";\n\tsetAttr \".subset\" -type \"string\" \"workfileTest_task\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.workfile\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ExtractImportReference\\\": {\\\"active\\\": false}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:eee200c4dd38\";\ncreateNode objectSet -n \"renderingMain1\";\n\trename -uid \"9AC6AB5B-45EB-8439-BB6C-C197555E11E8\";\n\taddAttr -ci true -sn \"pre_creator_identifier\" -ln \"pre_creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".pre_creator_identifier\" -type \"string\" \"io.openpype.creators.maya.renderlayer\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:e72a7af3a6c5\";\ncreateNode objectSet -n \"_renderingMain:Main1\";\n\trename -uid \"7CFC031E-42E2-E431-89AB-5A991800F6F2\";\n\taddAttr -s false -ci true -sn \"renderlayer\" -ln \"renderlayer\" -at \"message\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"deadlineServers\" -ln \"deadlineServers\" -dt \"string\";\n\taddAttr -ci true -sn \"suspendPublishJob\" -ln \"suspendPublishJob\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"priority\" -ln \"priority\" -at \"long\";\n\taddAttr -ci true -sn \"tile_priority\" -ln \"tile_priority\" -at \"long\";\n\taddAttr -ci true -sn \"framesPerTask\" -ln \"framesPerTask\" -at \"long\";\n\taddAttr -ci true -sn \"whitelist\" -ln \"whitelist\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"machineList\" -ln \"machineList\" -dt \"string\";\n\taddAttr -ci true -sn \"useMayaBatch\" -ln \"useMayaBatch\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"primaryPool\" -ln \"primaryPool\" -dt \"string\";\n\taddAttr -ci true -sn \"secondaryPool\" -ln \"secondaryPool\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"review\" -ln \"review\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"extendFrames\" -ln \"extendFrames\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"overrideExistingFrame\" -ln \"overrideExistingFrame\" -min 0\n\t\t-max 1 -at \"bool\";\n\taddAttr -ci true -sn \"tileRendering\" -ln \"tileRendering\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"tilesX\" -ln \"tilesX\" -at \"long\";\n\taddAttr -ci true -sn \"tilesY\" -ln \"tilesY\" -at \"long\";\n\taddAttr -ci true -sn \"convertToScanline\" -ln \"convertToScanline\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"useReferencedAovs\" -ln \"useReferencedAovs\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"renderSetupIncludeLights\" -ln \"renderSetupIncludeLights\" -min\n\t\t0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:0850eb5268f2\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"renderlayer\";\n\tsetAttr \".subset\" -type \"string\" \"renderTest_taskMain\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.renderlayer\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".deadlineServers\" -type \"string\" \"default\";\n\tsetAttr -cb on \".suspendPublishJob\";\n\tsetAttr -cb on \".priority\" 50;\n\tsetAttr -cb on \".tile_priority\" 50;\n\tsetAttr -cb on \".framesPerTask\" 1;\n\tsetAttr -cb on \".whitelist\";\n\tsetAttr \".machineList\" -type \"string\" \"\";\n\tsetAttr -cb on \".useMayaBatch\";\n\tsetAttr \".primaryPool\" -type \"string\" \"none\";\n\tsetAttr \".secondaryPool\" -type \"string\" \"-\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr -cb on \".review\" yes;\n\tsetAttr -cb on \".extendFrames\";\n\tsetAttr -cb on \".overrideExistingFrame\" yes;\n\tsetAttr -cb on \".tileRendering\";\n\tsetAttr -cb on \".tilesX\" 2;\n\tsetAttr -cb on \".tilesY\" 2;\n\tsetAttr -cb on \".convertToScanline\";\n\tsetAttr -cb on \".useReferencedAovs\";\n\tsetAttr -cb on \".renderSetupIncludeLights\" yes;\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"CollectDeadlinePools\\\": {\\\"primaryPool\\\": \\\"\\\", \\\"secondaryPool\\\": \\\"\\\"}, \\\"ValidateResolution\\\": {\\\"active\\\": true}, \\\"ValidateDeadlinePools\\\": {\\\"active\\\": true}, \\\"ValidateFrameRange\\\": {\\\"active\\\": true}, \\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ExtractImportReference\\\": {\\\"active\\\": false}, \\\"MayaSubmitDeadline\\\": {\\\"priority\\\": 50, \\\"chunkSize\\\": 1, \\\"machineList\\\": \\\"\\\", \\\"whitelist\\\": false, \\\"tile_priority\\\": 50, \\\"strict_error_checking\\\": true}, \\\"ProcessSubmittedJobOnFarm\\\": {\\\"publishJobState\\\": \\\"Active\\\"}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"review,extendFrames,overrideExistingFrame,tileRendering,tilesX,tilesY,convertToScanline,useReferencedAovs,renderSetupIncludeLights\";\nselect -ne :time1;\n\tsetAttr \".o\" 1001;\n\tsetAttr \".unw\" 1001;\nselect -ne :hardwareRenderingGlobals;\n\tsetAttr \".otfna\" -type \"stringArray\" 22 \"NURBS Curves\" \"NURBS Surfaces\" \"Polygons\" \"Subdiv Surface\" \"Particles\" \"Particle Instance\" \"Fluids\" \"Strokes\" \"Image Planes\" \"UI\" \"Lights\" \"Cameras\" \"Locators\" \"Joints\" \"IK Handles\" \"Deformers\" \"Motion Trails\" \"Components\" \"Hair Systems\" \"Follicles\" \"Misc. UI\" \"Ornaments\"  ;\n\tsetAttr \".otfva\" -type \"Int32Array\" 22 0 1 1 1 1 1\n\t\t 1 1 1 0 0 0 0 0 0 0 0 0\n\t\t 0 0 0 0 ;\n\tsetAttr \".fprt\" yes;\nselect -ne :renderPartition;\n\tsetAttr -s 2 \".st\";\nselect -ne :renderGlobalsList1;\nselect -ne :defaultShaderList1;\n\tsetAttr -s 5 \".s\";\nselect -ne :postProcessList1;\n\tsetAttr -s 2 \".p\";\nselect -ne :defaultRenderingList1;\n\tsetAttr -s 2 \".r\";\nselect -ne :lightList1;\nselect -ne :standardSurface1;\n\tsetAttr \".b\" 0.80000001192092896;\n\tsetAttr \".bc\" -type \"float3\" 1 1 1 ;\n\tsetAttr \".s\" 0.20000000298023224;\nselect -ne :initialShadingGroup;\n\tsetAttr -s 2 \".dsm\";\n\tsetAttr \".ro\" yes;\nselect -ne :initialParticleSE;\n\tsetAttr \".ro\" yes;\nselect -ne :defaultRenderGlobals;\n\taddAttr -ci true -h true -sn \"dss\" -ln \"defaultSurfaceShader\" -dt \"string\";\n\tsetAttr \".ren\" -type \"string\" \"arnold\";\n\tsetAttr \".outf\" 51;\n\tsetAttr \".imfkey\" -type \"string\" \"exr\";\n\tsetAttr \".an\" yes;\n\tsetAttr \".fs\" 1001;\n\tsetAttr \".ef\" 1001;\n\tsetAttr \".oft\" -type \"string\" \"\";\n\tsetAttr \".pff\" yes;\n\tsetAttr \".ifp\" -type \"string\" \"<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>\";\n\tsetAttr \".rv\" -type \"string\" \"\";\n\tsetAttr \".pram\" -type \"string\" \"\";\n\tsetAttr \".poam\" -type \"string\" \"\";\n\tsetAttr \".prlm\" -type \"string\" \"\";\n\tsetAttr \".polm\" -type \"string\" \"\";\n\tsetAttr \".prm\" -type \"string\" \"\";\n\tsetAttr \".pom\" -type \"string\" \"\";\n\tsetAttr \".dss\" -type \"string\" \"lambert1\";\nselect -ne :defaultResolution;\n\tsetAttr \".w\" 1920;\n\tsetAttr \".h\" 1080;\n\tsetAttr \".pa\" 1;\n\tsetAttr \".dar\" 1.7777777910232544;\nselect -ne :defaultLightSet;\nselect -ne :defaultColorMgtGlobals;\n\tsetAttr \".cfe\" yes;\n\tsetAttr \".cfp\" -type \"string\" \"<MAYA_RESOURCES>/OCIO-configs/Maya-legacy/config.ocio\";\n\tsetAttr \".vtn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".vn\" -type \"string\" \"sRGB gamma\";\n\tsetAttr \".dn\" -type \"string\" \"legacy\";\n\tsetAttr \".wsn\" -type \"string\" \"scene-linear Rec 709/sRGB\";\n\tsetAttr \".ovt\" no;\n\tsetAttr \".povt\" no;\n\tsetAttr \".otn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".potn\" -type \"string\" \"sRGB gamma (legacy)\";\nselect -ne :hardwareRenderGlobals;\n\tsetAttr \".ctrs\" 256;\n\tsetAttr \".btrs\" 512;\nconnectAttr \"rs_Main.ri\" \":persp.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":top.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":front.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":side.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"pDisc1.rlio[0]\";\nconnectAttr \"polyDisc1.output\" \"pDiscShape1.i\";\nconnectAttr \"rs_Main.ri\" \"aiSkyDomeLight1.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"mainCamera.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"model_GRP.rlio[0]\";\nconnectAttr \"polySphere1.out\" \"pSphere1_GEOShape1.i\";\nrelationship \"link\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"link\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nconnectAttr \"layerManager.dli[0]\" \"defaultLayer.id\";\nconnectAttr \"renderLayerManager.rlmi[0]\" \"defaultRenderLayer.rlid\";\nconnectAttr \"Main.msg\" \"renderSetup.frl\";\nconnectAttr \"Main.msg\" \"renderSetup.lrl\";\nconnectAttr \":defaultArnoldDisplayDriver.msg\" \":defaultArnoldRenderOptions.drivers\"\n\t\t -na;\nconnectAttr \":defaultArnoldFilter.msg\" \":defaultArnoldRenderOptions.filt\";\nconnectAttr \":defaultArnoldDriver.msg\" \":defaultArnoldRenderOptions.drvr\";\nconnectAttr \"rs_Main.msg\" \"Main.lrl\";\nconnectAttr \"renderSetup.lit\" \"Main.pls\";\nconnectAttr \"defaultCollection.msg\" \"Main.cl\";\nconnectAttr \"defaultCollection.msg\" \"Main.ch\";\nconnectAttr \"renderLayerManager.rlmi[1]\" \"rs_Main.rlid\";\nconnectAttr \"defaultCollectionSelector.c\" \"defaultCollection.sel\";\nconnectAttr \"Main.lit\" \"defaultCollection.pls\";\nconnectAttr \"Main.nic\" \"defaultCollection.pic\";\nconnectAttr \"model_GRP.iog\" \"modelMain.dsm\" -na;\nconnectAttr \"modelMain.msg\" \"_renderingMain:Main.dnsm\" -na;\nconnectAttr \"model_GRP.iog\" \"_renderingMain1:Main.dsm\" -na;\nconnectAttr \"_renderingMain:Main1.msg\" \"renderingMain1.dnsm\" -na;\nconnectAttr \"Main.msg\" \"_renderingMain:Main1.renderlayer\";\nconnectAttr \"defaultRenderLayer.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"rs_Main.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"aiSkyDomeLightShape1.ltd\" \":lightList1.l\" -na;\nconnectAttr \"pSphere1_GEOShape1.iog\" \":initialShadingGroup.dsm\" -na;\nconnectAttr \"pDiscShape1.iog\" \":initialShadingGroup.dsm\" -na;\nconnectAttr \"aiSkyDomeLight1.iog\" \":defaultLightSet.dsm\" -na;\n// End of test_project_test_asset_test_task_v002.ma\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/work/test_task/workspace.mel",
    "content": "workspace -fr \"shaders\" \"renderData/shaders\";\nworkspace -fr \"images\" \"renders/maya\";\nworkspace -fr \"particles\" \"particles\";\nworkspace -fr \"mayaAscii\" \"\";\nworkspace -fr \"mayaBinary\" \"\";\nworkspace -fr \"scene\" \"\";\nworkspace -fr \"alembicCache\" \"cache/alembic\";\nworkspace -fr \"renderData\" \"renderData\";\nworkspace -fr \"sourceImages\" \"sourceimages\";\nworkspace -fr \"fileCache\" \"cache/nCache\";\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya/input/dumps/avalon_tests/test_project.metadata.json",
    "content": "{\"indexes\":[{\"v\":{\"$numberInt\":\"2\"},\"key\":{\"_id\":{\"$numberInt\":\"1\"}},\"name\":\"_id_\"}],\"uuid\":\"0fe6204d5115481a93ff43a1ad4b5fdd\",\"collectionName\":\"test_project\"}\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya/input/dumps/openpype_tests/settings.metadata.json",
    "content": "{\"indexes\":[{\"v\":{\"$numberInt\":\"2\"},\"key\":{\"_id\":{\"$numberInt\":\"1\"}},\"name\":\"_id_\"}],\"uuid\":\"feec494c3f8045e9a23a5092522d157c\",\"collectionName\":\"settings\"}\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya/input/env_vars/env_var.json",
    "content": "{\n    \"OPENPYPE_MONGO\": \"{TEST_OPENPYPE_MONGO}\",\n    \"AVALON_MONGO\": \"{TEST_OPENPYPE_MONGO}\",\n    \"OPENPYPE_DATABASE_NAME\": \"{TEST_OPENPYPE_NAME}\",\n    \"AVALON_TIMEOUT\": \"3000\",\n    \"AVALON_DB\": \"{TEST_DB_NAME}\",\n    \"AVALON_PROJECT\": \"{TEST_PROJECT_NAME}\",\n    \"PYPE_DEBUG\": \"3\",\n    \"AVALON_CONFIG\": \"openpype\",\n    \"IS_TEST\": \"1\"\n}\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya/input/startup/userSetup.py",
    "content": "import sys\nprint(\"\\n\".join(sys.path))\n\nfrom maya import cmds\nimport pyblish.util\nimport openpype\n\nprint(\"starting OpenPype usersetup for testing\")\ncmds.evalDeferred(\"pyblish.util.publish()\")\n\ncmds.evalDeferred(\"cmds.quit(force=True)\")\ncmds.evalDeferred(\"cmds.quit\")\nprint(\"finished OpenPype usersetup  for testing\")\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya/input/workfile/test_project_test_asset_test_task_v001.ma",
    "content": "//Maya ASCII 2023 scene\n//Name: test_project_test_asset_test_task_v002.ma\n//Last modified: Thu, Nov 09, 2023 11:59:33 AM\n//Codeset: 1252\nrequires maya \"2023\";\nrequires -nodeType \"simpleSelector\" -nodeType \"renderSetupLayer\" -nodeType \"renderSetup\"\n\t\t -nodeType \"collection\" \"renderSetup.py\" \"1.0\";\nrequires -nodeType \"aiOptions\" -nodeType \"aiAOVDriver\" -nodeType \"aiAOVFilter\" -nodeType \"aiSkyDomeLight\"\n\t\t \"mtoa\" \"5.2.1.1\";\nrequires -nodeType \"polyDisc\" \"modelingToolkit\" \"0.0.0.0\";\ncurrentUnit -l centimeter -a degree -t pal;\nfileInfo \"application\" \"maya\";\nfileInfo \"product\" \"Maya 2023\";\nfileInfo \"version\" \"2023\";\nfileInfo \"cutIdentifier\" \"202211021031-847a9f9623\";\nfileInfo \"osv\" \"Windows 10 Pro v2009 (Build: 19045)\";\nfileInfo \"license\" \"education\";\nfileInfo \"UUID\" \"591BA477-4DBF-D8A9-D1CE-AEB4E5E95DCA\";\nfileInfo \"OpenPypeContext\" \"eyJwdWJsaXNoX2F0dHJpYnV0ZXMiOiB7IlZhbGlkYXRlQ29udGFpbmVycyI6IHsiYWN0aXZlIjogdHJ1ZX19fQ==\";\ncreateNode transform -s -n \"persp\";\n\trename -uid \"D52C935B-47C9-D868-A875-D799DD17B3A1\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 33.329836010894773 18.034068470832839 24.890981774804157 ;\n\tsetAttr \".r\" -type \"double3\" 79.461647270402509 -44.999999999997357 183.99999999999159 ;\n\tsetAttr \".rp\" -type \"double3\" -2.0401242849359917e-14 2.2609331405046354e-14 -44.821869662029947 ;\n\tsetAttr \".rpt\" -type \"double3\" -27.999999999999989 -21.000000000000025 16.82186966202995 ;\ncreateNode camera -s -n \"perspShape\" -p \"persp\";\n\trename -uid \"2399E6C0-490F-BA1F-485F-5AA8A01D27BC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 50.609449488607154;\n\tsetAttr \".imn\" -type \"string\" \"persp\";\n\tsetAttr \".den\" -type \"string\" \"persp_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".ai_translator\" -type \"string\" \"perspective\";\ncreateNode transform -s -n \"top\";\n\trename -uid \"415C7426-413E-0FAE-FCC3-3DAED7443A52\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 1000.1 0 ;\n\tsetAttr \".r\" -type \"double3\" 90 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" 0 -1000.1 1000.1 ;\ncreateNode camera -s -n \"topShape\" -p \"top\";\n\trename -uid \"3BD0CF60-40DB-5278-5D8B-06ACBDA32122\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"top\";\n\tsetAttr \".den\" -type \"string\" \"top_depth\";\n\tsetAttr \".man\" -type \"string\" \"top_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -t %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"front\";\n\trename -uid \"D83DD5CE-4FE0-AB1B-81B2-87A63F0B7A05\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 0 1000.1 ;\n\tsetAttr \".r\" -type \"double3\" 180 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\ncreateNode camera -s -n \"frontShape\" -p \"front\";\n\trename -uid \"23313CBA-42C2-0B3A-0FCF-EA965EAC5DEC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"front\";\n\tsetAttr \".den\" -type \"string\" \"front_depth\";\n\tsetAttr \".man\" -type \"string\" \"front_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -f %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"side\";\n\trename -uid \"F70F692C-4A0D-BE64-9EE4-A99B6FA2D56E\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 1000.1 0 0 ;\n\tsetAttr \".r\" -type \"double3\" 180 -90 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" -1000.1 0 1000.1 ;\ncreateNode camera -s -n \"sideShape\" -p \"side\";\n\trename -uid \"C05669C3-420E-CA11-E5FC-7EB64EF8B632\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"side\";\n\tsetAttr \".den\" -type \"string\" \"side_depth\";\n\tsetAttr \".man\" -type \"string\" \"side_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -s %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -n \"pDisc1\";\n\trename -uid \"DED70CCF-4C19-16E4-9E5D-66A05037BA47\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:90e762703f08\";\ncreateNode mesh -n \"pDiscShape1\" -p \"pDisc1\";\n\trename -uid \"E1FCDCCF-4DE1-D3B9-C4F8-3285F1CF5B25\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:4ee3da11a1a4\";\ncreateNode transform -n \"aiSkyDomeLight1\";\n\trename -uid \"402BF091-4305-22E3-7CF0-9BA3D7F948F7\";\ncreateNode aiSkyDomeLight -n \"aiSkyDomeLightShape1\" -p \"aiSkyDomeLight1\";\n\trename -uid \"CEF32074-4066-553D-A4FD-65B508A56ABE\";\n\taddAttr -ci true -h true -sn \"aal\" -ln \"attributeAliasList\" -dt \"attributeAlias\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".csh\" no;\n\tsetAttr \".rcsh\" no;\n\tsetAttr \".aal\" -type \"attributeAlias\" {\"exposure\",\"aiExposure\"} ;\ncreateNode transform -n \"mainCamera\";\n\trename -uid \"58651370-474E-02EE-39A9-A2AB27E0DD87\";\n\tsetAttr \".t\" -type \"double3\" 33.329836010894773 18.034068470832839 24.890981774804157 ;\n\tsetAttr \".r\" -type \"double3\" 79.461647270402509 -44.999999999997357 183.99999999999159 ;\n\tsetAttr \".rp\" -type \"double3\" -2.0401242849359917e-14 2.2609331405046354e-14 -44.821869662029947 ;\n\tsetAttr \".rpt\" -type \"double3\" -27.999999999999989 -21.000000000000025 16.82186966202995 ;\ncreateNode camera -n \"mainCameraShape\" -p \"mainCamera\";\n\trename -uid \"CCE11369-4101-EE5E-5381-3F87DA963CA2\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 50.609449488607154;\n\tsetAttr \".imn\" -type \"string\" \"persp\";\n\tsetAttr \".den\" -type \"string\" \"persp_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".ai_translator\" -type \"string\" \"perspective\";\ncreateNode transform -n \"model_GRP\";\n\trename -uid \"445FDC20-4A9D-2C5B-C7BD-F98F6E660B5C\";\ncreateNode transform -n \"pSphere1_GEO\" -p \"model_GRP\";\n\trename -uid \"7445A43F-444F-B2D3-4315-2AA013D2E0B6\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:302a4c6123a4\";\ncreateNode mesh -n \"pSphere1_GEOShape1\" -p \"pSphere1_GEO\";\n\trename -uid \"7C731260-45C6-339E-07BF-359446B08EA1\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:6c77a15a98a9\";\ncreateNode lightLinker -s -n \"lightLinker1\";\n\trename -uid \"D9ADDBD2-49DE-91E2-4166-99A362986A3A\";\n\tsetAttr -s 2 \".lnk\";\n\tsetAttr -s 2 \".slnk\";\ncreateNode shapeEditorManager -n \"shapeEditorManager\";\n\trename -uid \"A232A3B1-4B62-92E7-A7C9-9D9FC5EF010A\";\ncreateNode poseInterpolatorManager -n \"poseInterpolatorManager\";\n\trename -uid \"B30BA35D-492A-834B-3448-49A80BBBFC39\";\ncreateNode displayLayerManager -n \"layerManager\";\n\trename -uid \"4417380F-4A6A-16CC-B1DE-AA95ED9C7FB2\";\ncreateNode displayLayer -n \"defaultLayer\";\n\trename -uid \"4A776D1B-401F-7069-1C74-A7AAE84CEE03\";\n\tsetAttr \".ufem\" -type \"stringArray\" 0  ;\ncreateNode renderLayerManager -n \"renderLayerManager\";\n\trename -uid \"EE21C644-43B8-C754-0BED-709D2EEB204D\";\n\tsetAttr -s 2 \".rlmi[1]\"  1;\n\tsetAttr -s 2 \".rlmi\";\ncreateNode renderLayer -n \"defaultRenderLayer\";\n\trename -uid \"B134920D-4508-23BD-A6CA-11B43DE03F53\";\n\tsetAttr \".g\" yes;\ncreateNode renderSetup -n \"renderSetup\";\n\trename -uid \"9A8F0D15-41AB-CA70-C2D8-B78840BF9BC1\";\ncreateNode polySphere -n \"polySphere1\";\n\trename -uid \"DA319706-4ACF-B15C-53B2-48AC80D202EA\";\ncreateNode script -n \"uiConfigurationScriptNode\";\n\trename -uid \"4B7AFB53-452E-E870-63E1-CCA1DD6EAF13\";\n\tsetAttr \".b\" -type \"string\" (\n\t\t\"// Maya Mel UI Configuration File.\\n//\\n//  This script is machine generated.  Edit at your own risk.\\n//\\n//\\n\\nglobal string $gMainPane;\\nif (`paneLayout -exists $gMainPane`) {\\n\\n\\tglobal int $gUseScenePanelConfig;\\n\\tint    $useSceneConfig = $gUseScenePanelConfig;\\n\\tint    $nodeEditorPanelVisible = stringArrayContains(\\\"nodeEditorPanel1\\\", `getPanel -vis`);\\n\\tint    $nodeEditorWorkspaceControlOpen = (`workspaceControl -exists nodeEditorPanel1Window` && `workspaceControl -q -visible nodeEditorPanel1Window`);\\n\\tint    $menusOkayInPanels = `optionVar -q allowMenusInPanels`;\\n\\tint    $nVisPanes = `paneLayout -q -nvp $gMainPane`;\\n\\tint    $nPanes = 0;\\n\\tstring $editorName;\\n\\tstring $panelName;\\n\\tstring $itemFilterName;\\n\\tstring $panelConfig;\\n\\n\\t//\\n\\t//  get current state of the UI\\n\\t//\\n\\tsceneUIReplacement -update $gMainPane;\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Top View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Top View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|front\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n\"\n\t\t+ \"            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n\"\n\t\t+ \"            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Side View\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Side View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|front\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n\"\n\t\t+ \"            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n\"\n\t\t+ \"            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n\"\n\t\t+ \"        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Front View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Front View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|top\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n\"\n\t\t+ \"            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n\"\n\t\t+ \"            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n\"\n\t\t+ \"            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Persp View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Persp View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|mainCamera\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n\"\n\t\t+ \"            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n\"\n\t\t+ \"            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n\"\n\t\t+ \"            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 345\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"ToggledOutliner\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"ToggledOutliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -docTag \\\"isolOutln_fromSeln\\\" \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 1\\n            -showReferenceMembers 1\\n\"\n\t\t+ \"            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n\"\n\t\t+ \"            -isSet 0\\n            -isSetMember 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -selectCommand \\\"print(\\\\\\\"\\\\\\\")\\\" \\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n            -renderFilterIndex 0\\n            -selectionOrder \\\"chronological\\\" \\n            -expandAttribute 0\\n            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"Outliner\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"Outliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 0\\n            -showReferenceMembers 0\\n            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n\"\n\t\t+ \"            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n\"\n\t\t+ \"            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"graphEditor\\\" (localizedPanelLabel(\\\"Graph Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Graph Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 1\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n\"\n\t\t+ \"                -showPublishedAsConnected 0\\n                -showParentContainers 1\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 1\\n                -showCompounds 0\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 1\\n                -doNotSelectNewObjects 0\\n                -dropIsParent 1\\n                -transmitFilters 1\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n\"\n\t\t+ \"                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 1\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"GraphEd\\\");\\n            animCurveEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -showPlayRangeShades \\\"on\\\" \\n                -lockPlayRangeShades \\\"off\\\" \\n                -smoothness \\\"fine\\\" \\n                -resultSamples 1.041667\\n                -resultScreenSamples 0\\n                -resultUpdate \\\"delayed\\\" \\n                -showUpstreamCurves 1\\n                -keyMinScale 1\\n                -stackedCurvesMin -1\\n                -stackedCurvesMax 1\\n\"\n\t\t+ \"                -stackedCurvesSpace 0.2\\n                -preSelectionHighlight 0\\n                -constrainDrag 0\\n                -valueLinesToggle 1\\n                -highlightAffectedCurves 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dopeSheetPanel\\\" (localizedPanelLabel(\\\"Dope Sheet\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dope Sheet\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n\"\n\t\t+ \"                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 0\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n                -showPublishedAsConnected 0\\n                -showParentContainers 1\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 0\\n                -showCompounds 1\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 0\\n                -doNotSelectNewObjects 1\\n                -dropIsParent 1\\n                -transmitFilters 0\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n\"\n\t\t+ \"                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 0\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"DopeSheetEd\\\");\\n            dopeSheetEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -outliner \\\"dopeSheetPanel1OutlineEd\\\" \\n                -showSummary 1\\n                -showScene 0\\n                -hierarchyBelow 0\\n\"\n\t\t+ \"                -showTicks 1\\n                -selectionWindow 0 0 0 0 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"timeEditorPanel\\\" (localizedPanelLabel(\\\"Time Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Time Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"clipEditorPanel\\\" (localizedPanelLabel(\\\"Trax Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Trax Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = clipEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 0 \\n\"\n\t\t+ \"                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"sequenceEditorPanel\\\" (localizedPanelLabel(\\\"Camera Sequencer\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Camera Sequencer\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = sequenceEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 1 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperGraphPanel\\\" (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\n\\t\\t\\t$editorName = ($panelName+\\\"HyperGraphEd\\\");\\n            hyperGraph -e \\n                -graphLayoutStyle \\\"hierarchicalLayout\\\" \\n                -orientation \\\"horiz\\\" \\n                -mergeConnections 0\\n                -zoom 1\\n                -animateTransition 0\\n                -showRelationships 1\\n                -showShapes 0\\n                -showDeformers 0\\n                -showExpressions 0\\n                -showConstraints 0\\n                -showConnectionFromSelected 0\\n                -showConnectionToSelected 0\\n                -showConstraintLabels 0\\n                -showUnderworld 0\\n                -showInvisible 0\\n                -transitionFrames 1\\n                -opaqueContainers 0\\n                -freeform 0\\n                -imagePosition 0 0 \\n                -imageScale 1\\n                -imageEnabled 0\\n                -graphType \\\"DAG\\\" \\n                -heatMapDisplay 0\\n                -updateSelection 1\\n                -updateNodeAdded 1\\n                -useDrawOverrideColor 0\\n                -limitGraphTraversal -1\\n\"\n\t\t+ \"                -range 0 0 \\n                -iconSize \\\"smallIcons\\\" \\n                -showCachedConnections 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperShadePanel\\\" (localizedPanelLabel(\\\"Hypershade\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypershade\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"visorPanel\\\" (localizedPanelLabel(\\\"Visor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Visor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"nodeEditorPanel\\\" (localizedPanelLabel(\\\"Node Editor\\\")) `;\\n\\tif ($nodeEditorPanelVisible || $nodeEditorWorkspaceControlOpen) {\\n\"\n\t\t+ \"\\t\\tif (\\\"\\\" == $panelName) {\\n\\t\\t\\tif ($useSceneConfig) {\\n\\t\\t\\t\\t$panelName = `scriptedPanel -unParent  -type \\\"nodeEditorPanel\\\" -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels `;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n\"\n\t\t+ \"                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\t}\\n\\t\\t} else {\\n\\t\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n\"\n\t\t+ \"                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"createNodePanel\\\" (localizedPanelLabel(\\\"Create Node\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Create Node\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"polyTexturePlacementPanel\\\" (localizedPanelLabel(\\\"UV Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"UV Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"renderWindowPanel\\\" (localizedPanelLabel(\\\"Render View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Render View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"shapePanel\\\" (localizedPanelLabel(\\\"Shape Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tshapePanel -edit -l (localizedPanelLabel(\\\"Shape Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"posePanel\\\" (localizedPanelLabel(\\\"Pose Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tposePanel -edit -l (localizedPanelLabel(\\\"Pose Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynRelEdPanel\\\" (localizedPanelLabel(\\\"Dynamic Relationships\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dynamic Relationships\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"relationshipPanel\\\" (localizedPanelLabel(\\\"Relationship Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Relationship Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"referenceEditorPanel\\\" (localizedPanelLabel(\\\"Reference Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Reference Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynPaintScriptedPanelType\\\" (localizedPanelLabel(\\\"Paint Effects\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Paint Effects\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"scriptEditorPanel\\\" (localizedPanelLabel(\\\"Script Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Script Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"profilerPanel\\\" (localizedPanelLabel(\\\"Profiler Tool\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Profiler Tool\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"contentBrowserPanel\\\" (localizedPanelLabel(\\\"Content Browser\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Content Browser\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\tif ($useSceneConfig) {\\n        string $configName = `getPanel -cwl (localizedPanelLabel(\\\"Current Layout\\\"))`;\\n\"\n\t\t+ \"        if (\\\"\\\" != $configName) {\\n\\t\\t\\tpanelConfiguration -edit -label (localizedPanelLabel(\\\"Current Layout\\\")) \\n\\t\\t\\t\\t-userCreated false\\n\\t\\t\\t\\t-defaultImage \\\"vacantCell.xP:/\\\"\\n\\t\\t\\t\\t-image \\\"\\\"\\n\\t\\t\\t\\t-sc false\\n\\t\\t\\t\\t-configString \\\"global string $gMainPane; paneLayout -e -cn \\\\\\\"quad\\\\\\\" -ps 1 50 50 -ps 2 50 50 -ps 3 50 50 -ps 4 50 50 $gMainPane;\\\"\\n\\t\\t\\t\\t-removeAllPanels\\n\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Top View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera top` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera top` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Persp View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|mainCamera\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|mainCamera\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Side View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera side` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera side` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Front View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 345\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t$configName;\\n\\n            setNamedPanelLayout (localizedPanelLabel(\\\"Current Layout\\\"));\\n        }\\n\\n        panelHistory -e -clear mainPanelHistory;\\n        sceneUIReplacement -clear;\\n\\t}\\n\\n\\ngrid -spacing 5 -size 12 -divisions 5 -displayAxes yes -displayGridLines yes -displayDivisionLines yes -displayPerspectiveLabels no -displayOrthographicLabels no -displayAxesBold yes -perspectiveLabelPosition axis -orthographicLabelPosition edge;\\nviewManip -drawCompass 0 -compassAngle 0 -frontParameters \\\"\\\" -homeParameters \\\"\\\" -selectionLockParameters \\\"\\\";\\n}\\n\");\n\tsetAttr \".st\" 3;\ncreateNode script -n \"sceneConfigurationScriptNode\";\n\trename -uid \"72B19BC2-43A2-E229-0A73-2CB861A291D1\";\n\tsetAttr \".b\" -type \"string\" \"playbackOptions -min 1000 -max 1001 -ast 1000 -aet 1001 \";\n\tsetAttr \".st\" 6;\ncreateNode polyDisc -n \"polyDisc1\";\n\trename -uid \"9ED8A7BD-4FFD-6107-4322-35ACD1D3AC42\";\ncreateNode aiOptions -s -n \"defaultArnoldRenderOptions\";\n\trename -uid \"31A81965-48A6-B90D-503D-2FA162B7C982\";\n\tsetAttr \".skip_license_check\" yes;\ncreateNode aiAOVFilter -s -n \"defaultArnoldFilter\";\n\trename -uid \"77A2BCB1-4613-905E-080E-B997FD5E1C6F\";\n\tsetAttr \".ai_translator\" -type \"string\" \"gaussian\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDriver\";\n\trename -uid \"C05729BE-4A33-F1DA-C222-3F8AB6EE7504\";\n\tsetAttr \".ai_translator\" -type \"string\" \"exr\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDisplayDriver\";\n\trename -uid \"806C25D7-4284-C09D-A8AE-4A80DBFFFAAF\";\n\tsetAttr \".output_mode\" 0;\n\tsetAttr \".ai_translator\" -type \"string\" \"maya\";\ncreateNode renderSetupLayer -n \"Main\";\n\trename -uid \"DC3F077F-49F5-1D64-BFF3-AAAF06798636\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode renderLayer -n \"rs_Main\";\n\trename -uid \"D798EE14-43EE-D8EF-F4C9-D6B19C9BC029\";\n\tsetAttr \".do\" 1;\ncreateNode collection -n \"defaultCollection\";\n\trename -uid \"0194FCB7-43C4-DC06-C8D6-D9BA2721CCFA\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode simpleSelector -n \"defaultCollectionSelector\";\n\trename -uid \"37D69395-4785-D0BE-AEA0-EEA66D1FAEDF\";\n\tsetAttr \".pat\" -type \"string\" \"*\";\ncreateNode objectSet -n \"modelMain\";\n\trename -uid \"811E4501-4B64-3016-BE29-E18EC09D90B7\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"writeColorSets\" -ln \"writeColorSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"writeFaceSets\" -ln \"writeFaceSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"attr\" -ln \"attr\" -dt \"string\";\n\taddAttr -ci true -sn \"attrPrefix\" -ln \"attrPrefix\" -dt \"string\";\n\taddAttr -ci true -sn \"includeParentHierarchy\" -ln \"includeParentHierarchy\" -min\n\t\t0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:49991563bf50\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"model\";\n\tsetAttr \".subset\" -type \"string\" \"modelMain\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.model\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr -cb on \".writeColorSets\";\n\tsetAttr -cb on \".writeFaceSets\";\n\tsetAttr \".attr\" -type \"string\" \"\";\n\tsetAttr \".attrPrefix\" -type \"string\" \"\";\n\tsetAttr -cb on \".includeParentHierarchy\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateNodeIDsRelated\\\": {\\\"active\\\": true}, \\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ValidateTransformNamingSuffix\\\": {\\\"active\\\": true}, \\\"ValidateColorSets\\\": {\\\"active\\\": true}, \\\"ValidateMeshHasUVs\\\": {\\\"active\\\": true}, \\\"ValidateMeshNonZeroEdgeLength\\\": {\\\"active\\\": true}, \\\"ExtractModel\\\": {\\\"active\\\": true}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"writeColorSets,writeFaceSets,includeParentHierarchy,attr,attrPrefix\";\ncreateNode objectSet -n \"_renderingMain:Main\";\n\trename -uid \"4E1D2600-482D-425C-352A-74BA418DC374\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:752f6f925fe6\";\ncreateNode objectSet -n \"_renderingMain1:Main\";\n\trename -uid \"A6382090-4537-44CB-E6DC-A5A58B98D008\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:d587b60d712e\";\ncreateNode objectSet -n \"workfileMain\";\n\trename -uid \"0F32608C-4AA1-F493-205C-25BDABF95CEC\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".hio\" yes;\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"workfile\";\n\tsetAttr \".subset\" -type \"string\" \"workfileTest_task\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.workfile\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ExtractImportReference\\\": {\\\"active\\\": false}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:eee200c4dd38\";\ncreateNode objectSet -n \"renderingMain1\";\n\trename -uid \"9AC6AB5B-45EB-8439-BB6C-C197555E11E8\";\n\taddAttr -ci true -sn \"pre_creator_identifier\" -ln \"pre_creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".pre_creator_identifier\" -type \"string\" \"io.openpype.creators.maya.renderlayer\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:e72a7af3a6c5\";\ncreateNode objectSet -n \"_renderingMain:Main1\";\n\trename -uid \"7CFC031E-42E2-E431-89AB-5A991800F6F2\";\n\taddAttr -s false -ci true -sn \"renderlayer\" -ln \"renderlayer\" -at \"message\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"deadlineServers\" -ln \"deadlineServers\" -dt \"string\";\n\taddAttr -ci true -sn \"suspendPublishJob\" -ln \"suspendPublishJob\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"priority\" -ln \"priority\" -at \"long\";\n\taddAttr -ci true -sn \"tile_priority\" -ln \"tile_priority\" -at \"long\";\n\taddAttr -ci true -sn \"framesPerTask\" -ln \"framesPerTask\" -at \"long\";\n\taddAttr -ci true -sn \"whitelist\" -ln \"whitelist\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"machineList\" -ln \"machineList\" -dt \"string\";\n\taddAttr -ci true -sn \"useMayaBatch\" -ln \"useMayaBatch\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"primaryPool\" -ln \"primaryPool\" -dt \"string\";\n\taddAttr -ci true -sn \"secondaryPool\" -ln \"secondaryPool\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"review\" -ln \"review\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"extendFrames\" -ln \"extendFrames\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"overrideExistingFrame\" -ln \"overrideExistingFrame\" -min 0\n\t\t-max 1 -at \"bool\";\n\taddAttr -ci true -sn \"tileRendering\" -ln \"tileRendering\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"tilesX\" -ln \"tilesX\" -at \"long\";\n\taddAttr -ci true -sn \"tilesY\" -ln \"tilesY\" -at \"long\";\n\taddAttr -ci true -sn \"convertToScanline\" -ln \"convertToScanline\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"useReferencedAovs\" -ln \"useReferencedAovs\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"renderSetupIncludeLights\" -ln \"renderSetupIncludeLights\" -min\n\t\t0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:0850eb5268f2\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"renderlayer\";\n\tsetAttr \".subset\" -type \"string\" \"renderTest_taskMain\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.renderlayer\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".deadlineServers\" -type \"string\" \"default\";\n\tsetAttr -cb on \".suspendPublishJob\";\n\tsetAttr -cb on \".priority\" 50;\n\tsetAttr -cb on \".tile_priority\" 50;\n\tsetAttr -cb on \".framesPerTask\" 1;\n\tsetAttr -cb on \".whitelist\";\n\tsetAttr \".machineList\" -type \"string\" \"\";\n\tsetAttr -cb on \".useMayaBatch\";\n\tsetAttr \".primaryPool\" -type \"string\" \"none\";\n\tsetAttr \".secondaryPool\" -type \"string\" \"-\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr -cb on \".review\" yes;\n\tsetAttr -cb on \".extendFrames\";\n\tsetAttr -cb on \".overrideExistingFrame\" yes;\n\tsetAttr -cb on \".tileRendering\";\n\tsetAttr -cb on \".tilesX\" 2;\n\tsetAttr -cb on \".tilesY\" 2;\n\tsetAttr -cb on \".convertToScanline\";\n\tsetAttr -cb on \".useReferencedAovs\";\n\tsetAttr -cb on \".renderSetupIncludeLights\" yes;\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"CollectDeadlinePools\\\": {\\\"primaryPool\\\": \\\"\\\", \\\"secondaryPool\\\": \\\"\\\"}, \\\"ValidateResolution\\\": {\\\"active\\\": true}, \\\"ValidateDeadlinePools\\\": {\\\"active\\\": true}, \\\"ValidateFrameRange\\\": {\\\"active\\\": true}, \\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ExtractImportReference\\\": {\\\"active\\\": false}, \\\"MayaSubmitDeadline\\\": {\\\"priority\\\": 50, \\\"chunkSize\\\": 1, \\\"machineList\\\": \\\"\\\", \\\"whitelist\\\": false, \\\"tile_priority\\\": 50, \\\"strict_error_checking\\\": true}, \\\"ProcessSubmittedJobOnFarm\\\": {\\\"publishJobState\\\": \\\"Active\\\"}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"review,extendFrames,overrideExistingFrame,tileRendering,tilesX,tilesY,convertToScanline,useReferencedAovs,renderSetupIncludeLights\";\nselect -ne :time1;\n\tsetAttr \".o\" 1001;\n\tsetAttr \".unw\" 1001;\nselect -ne :hardwareRenderingGlobals;\n\tsetAttr \".otfna\" -type \"stringArray\" 22 \"NURBS Curves\" \"NURBS Surfaces\" \"Polygons\" \"Subdiv Surface\" \"Particles\" \"Particle Instance\" \"Fluids\" \"Strokes\" \"Image Planes\" \"UI\" \"Lights\" \"Cameras\" \"Locators\" \"Joints\" \"IK Handles\" \"Deformers\" \"Motion Trails\" \"Components\" \"Hair Systems\" \"Follicles\" \"Misc. UI\" \"Ornaments\"  ;\n\tsetAttr \".otfva\" -type \"Int32Array\" 22 0 1 1 1 1 1\n\t\t 1 1 1 0 0 0 0 0 0 0 0 0\n\t\t 0 0 0 0 ;\n\tsetAttr \".fprt\" yes;\nselect -ne :renderPartition;\n\tsetAttr -s 2 \".st\";\nselect -ne :renderGlobalsList1;\nselect -ne :defaultShaderList1;\n\tsetAttr -s 5 \".s\";\nselect -ne :postProcessList1;\n\tsetAttr -s 2 \".p\";\nselect -ne :defaultRenderingList1;\n\tsetAttr -s 2 \".r\";\nselect -ne :lightList1;\nselect -ne :standardSurface1;\n\tsetAttr \".b\" 0.80000001192092896;\n\tsetAttr \".bc\" -type \"float3\" 1 1 1 ;\n\tsetAttr \".s\" 0.20000000298023224;\nselect -ne :initialShadingGroup;\n\tsetAttr -s 2 \".dsm\";\n\tsetAttr \".ro\" yes;\nselect -ne :initialParticleSE;\n\tsetAttr \".ro\" yes;\nselect -ne :defaultRenderGlobals;\n\taddAttr -ci true -h true -sn \"dss\" -ln \"defaultSurfaceShader\" -dt \"string\";\n\tsetAttr \".ren\" -type \"string\" \"arnold\";\n\tsetAttr \".outf\" 51;\n\tsetAttr \".imfkey\" -type \"string\" \"exr\";\n\tsetAttr \".an\" yes;\n\tsetAttr \".fs\" 1001;\n\tsetAttr \".ef\" 1001;\n\tsetAttr \".oft\" -type \"string\" \"\";\n\tsetAttr \".pff\" yes;\n\tsetAttr \".ifp\" -type \"string\" \"<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>\";\n\tsetAttr \".rv\" -type \"string\" \"\";\n\tsetAttr \".pram\" -type \"string\" \"\";\n\tsetAttr \".poam\" -type \"string\" \"\";\n\tsetAttr \".prlm\" -type \"string\" \"\";\n\tsetAttr \".polm\" -type \"string\" \"\";\n\tsetAttr \".prm\" -type \"string\" \"\";\n\tsetAttr \".pom\" -type \"string\" \"\";\n\tsetAttr \".dss\" -type \"string\" \"lambert1\";\nselect -ne :defaultResolution;\n\tsetAttr \".w\" 1920;\n\tsetAttr \".h\" 1080;\n\tsetAttr \".pa\" 1;\n\tsetAttr \".dar\" 1.7777777910232544;\nselect -ne :defaultLightSet;\nselect -ne :defaultColorMgtGlobals;\n\tsetAttr \".cfe\" yes;\n\tsetAttr \".cfp\" -type \"string\" \"<MAYA_RESOURCES>/OCIO-configs/Maya-legacy/config.ocio\";\n\tsetAttr \".vtn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".vn\" -type \"string\" \"sRGB gamma\";\n\tsetAttr \".dn\" -type \"string\" \"legacy\";\n\tsetAttr \".wsn\" -type \"string\" \"scene-linear Rec 709/sRGB\";\n\tsetAttr \".ovt\" no;\n\tsetAttr \".povt\" no;\n\tsetAttr \".otn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".potn\" -type \"string\" \"sRGB gamma (legacy)\";\nselect -ne :hardwareRenderGlobals;\n\tsetAttr \".ctrs\" 256;\n\tsetAttr \".btrs\" 512;\nconnectAttr \"rs_Main.ri\" \":persp.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":top.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":front.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":side.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"pDisc1.rlio[0]\";\nconnectAttr \"polyDisc1.output\" \"pDiscShape1.i\";\nconnectAttr \"rs_Main.ri\" \"aiSkyDomeLight1.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"mainCamera.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"model_GRP.rlio[0]\";\nconnectAttr \"polySphere1.out\" \"pSphere1_GEOShape1.i\";\nrelationship \"link\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"link\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nconnectAttr \"layerManager.dli[0]\" \"defaultLayer.id\";\nconnectAttr \"renderLayerManager.rlmi[0]\" \"defaultRenderLayer.rlid\";\nconnectAttr \"Main.msg\" \"renderSetup.frl\";\nconnectAttr \"Main.msg\" \"renderSetup.lrl\";\nconnectAttr \":defaultArnoldDisplayDriver.msg\" \":defaultArnoldRenderOptions.drivers\"\n\t\t -na;\nconnectAttr \":defaultArnoldFilter.msg\" \":defaultArnoldRenderOptions.filt\";\nconnectAttr \":defaultArnoldDriver.msg\" \":defaultArnoldRenderOptions.drvr\";\nconnectAttr \"rs_Main.msg\" \"Main.lrl\";\nconnectAttr \"renderSetup.lit\" \"Main.pls\";\nconnectAttr \"defaultCollection.msg\" \"Main.cl\";\nconnectAttr \"defaultCollection.msg\" \"Main.ch\";\nconnectAttr \"renderLayerManager.rlmi[1]\" \"rs_Main.rlid\";\nconnectAttr \"defaultCollectionSelector.c\" \"defaultCollection.sel\";\nconnectAttr \"Main.lit\" \"defaultCollection.pls\";\nconnectAttr \"Main.nic\" \"defaultCollection.pic\";\nconnectAttr \"model_GRP.iog\" \"modelMain.dsm\" -na;\nconnectAttr \"modelMain.msg\" \"_renderingMain:Main.dnsm\" -na;\nconnectAttr \"model_GRP.iog\" \"_renderingMain1:Main.dsm\" -na;\nconnectAttr \"_renderingMain:Main1.msg\" \"renderingMain1.dnsm\" -na;\nconnectAttr \"Main.msg\" \"_renderingMain:Main1.renderlayer\";\nconnectAttr \"defaultRenderLayer.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"rs_Main.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"aiSkyDomeLightShape1.ltd\" \":lightList1.l\" -na;\nconnectAttr \"pSphere1_GEOShape1.iog\" \":initialShadingGroup.dsm\" -na;\nconnectAttr \"pDiscShape1.iog\" \":initialShadingGroup.dsm\" -na;\nconnectAttr \"aiSkyDomeLight1.iog\" \":defaultLightSet.dsm\" -na;\n// End of test_project_test_asset_test_task_v002.ma\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_deadline_publish_in_maya.py",
    "content": "from tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.maya.lib import MayaDeadlinePublishTestClass\n\n\nclass TestDeadlinePublishInMaya(MayaDeadlinePublishTestClass):\n    \"\"\"Basic test case for publishing in Maya\n\n\n        Always pulls and uses test data from GDrive!\n\n        Opens Maya, runs publish on prepared workile.\n\n        Sends file to be rendered on Deadline.\n\n        Then checks content of DB (if subset, version, representations were\n        created.\n        Checks tmp folder if all expected files were published.\n\n        How to run:\n        (in cmd with activated {OPENPYPE_ROOT}/.venv)\n        {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/maya  # noqa: E501\n\n    \"\"\"\n    PERSIST = False\n\n    TEST_FILES = [\n        (\"test_deadline_publish_in_maya\", \"\", \"\")\n    ]\n\n    APP_GROUP = \"maya\"\n    # keep empty to locate latest installed variant or explicit\n    APP_VARIANT = \"\"\n\n    TIMEOUT = 180  # publish timeout\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 3))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"modelMain\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"renderTest_taskMain_beauty\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(DBAssert.count_of_types(dbcon, \"representation\", 7))\n\n        # hero included\n        additional_args = {\"context.subset\": \"modelMain\",\n                           \"context.ext\": \"abc\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 2,\n                                    additional_args=additional_args))\n\n        # hero included\n        additional_args = {\"context.subset\": \"modelMain\",\n                           \"context.ext\": \"ma\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 2,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"modelMain\",\n                           \"context.ext\": \"mb\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain_beauty\",\n                           \"context.ext\": \"exr\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain_beauty\",\n                           \"context.ext\": \"jpg\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain_beauty\",\n                           \"context.ext\": \"png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestDeadlinePublishInMaya()\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/hero/test_project_test_asset_modelMain_hero.ma",
    "content": "//Maya ASCII 2019 scene\n//Name: modelMain.ma\n//Last modified: Thu, Sep 02, 2021 03:24:19 PM\n//Codeset: 1252\nrequires maya \"2019\";\nrequires \"mtoa\" \"4.0.4.2\";\nrequires \"mtoa\" \"4.0.4.2\";\ncurrentUnit -l centimeter -a degree -t pal;\nfileInfo \"application\" \"maya\";\nfileInfo \"product\" \"Maya 2019\";\nfileInfo \"version\" \"2019\";\nfileInfo \"cutIdentifier\" \"201812112215-434d8d9c04\";\nfileInfo \"osv\" \"Microsoft Windows 10 Technical Preview  (Build 19041)\\n\";\nfileInfo \"license\" \"education\";\ncreateNode transform -n \"pSphere1_GEO\";\n\trename -uid \"7445A43F-444F-B2D3-4315-2AA013D2E0B6\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:440654b3dfe4\";\ncreateNode mesh -n \"pSphere1_GEOShape1\" -p \"pSphere1_GEO\";\n\trename -uid \"7C731260-45C6-339E-07BF-359446B08EA1\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr -s 439 \".uvst[0].uvsp\";\n\tsetAttr \".uvst[0].uvsp[0:249]\" -type \"float2\" 0 0.050000001 0.050000001 0.050000001\n\t\t 0.1 0.050000001 0.15000001 0.050000001 0.2 0.050000001 0.25 0.050000001 0.30000001\n\t\t 0.050000001 0.35000002 0.050000001 0.40000004 0.050000001 0.45000005 0.050000001\n\t\t 0.50000006 0.050000001 0.55000007 0.050000001 0.60000008 0.050000001 0.6500001 0.050000001\n\t\t 0.70000011 0.050000001 0.75000012 0.050000001 0.80000013 0.050000001 0.85000014 0.050000001\n\t\t 0.90000015 0.050000001 0.95000017 0.050000001 1.000000119209 0.050000001 0 0.1 0.050000001\n\t\t 0.1 0.1 0.1 0.15000001 0.1 0.2 0.1 0.25 0.1 0.30000001 0.1 0.35000002 0.1 0.40000004\n\t\t 0.1 0.45000005 0.1 0.50000006 0.1 0.55000007 0.1 0.60000008 0.1 0.6500001 0.1 0.70000011\n\t\t 0.1 0.75000012 0.1 0.80000013 0.1 0.85000014 0.1 0.90000015 0.1 0.95000017 0.1 1.000000119209\n\t\t 0.1 0 0.15000001 0.050000001 0.15000001 0.1 0.15000001 0.15000001 0.15000001 0.2\n\t\t 0.15000001 0.25 0.15000001 0.30000001 0.15000001 0.35000002 0.15000001 0.40000004\n\t\t 0.15000001 0.45000005 0.15000001 0.50000006 0.15000001 0.55000007 0.15000001 0.60000008\n\t\t 0.15000001 0.6500001 0.15000001 0.70000011 0.15000001 0.75000012 0.15000001 0.80000013\n\t\t 0.15000001 0.85000014 0.15000001 0.90000015 0.15000001 0.95000017 0.15000001 1.000000119209\n\t\t 0.15000001 0 0.2 0.050000001 0.2 0.1 0.2 0.15000001 0.2 0.2 0.2 0.25 0.2 0.30000001\n\t\t 0.2 0.35000002 0.2 0.40000004 0.2 0.45000005 0.2 0.50000006 0.2 0.55000007 0.2 0.60000008\n\t\t 0.2 0.6500001 0.2 0.70000011 0.2 0.75000012 0.2 0.80000013 0.2 0.85000014 0.2 0.90000015\n\t\t 0.2 0.95000017 0.2 1.000000119209 0.2 0 0.25 0.050000001 0.25 0.1 0.25 0.15000001\n\t\t 0.25 0.2 0.25 0.25 0.25 0.30000001 0.25 0.35000002 0.25 0.40000004 0.25 0.45000005\n\t\t 0.25 0.50000006 0.25 0.55000007 0.25 0.60000008 0.25 0.6500001 0.25 0.70000011 0.25\n\t\t 0.75000012 0.25 0.80000013 0.25 0.85000014 0.25 0.90000015 0.25 0.95000017 0.25 1.000000119209\n\t\t 0.25 0 0.30000001 0.050000001 0.30000001 0.1 0.30000001 0.15000001 0.30000001 0.2\n\t\t 0.30000001 0.25 0.30000001 0.30000001 0.30000001 0.35000002 0.30000001 0.40000004\n\t\t 0.30000001 0.45000005 0.30000001 0.50000006 0.30000001 0.55000007 0.30000001 0.60000008\n\t\t 0.30000001 0.6500001 0.30000001 0.70000011 0.30000001 0.75000012 0.30000001 0.80000013\n\t\t 0.30000001 0.85000014 0.30000001 0.90000015 0.30000001 0.95000017 0.30000001 1.000000119209\n\t\t 0.30000001 0 0.35000002 0.050000001 0.35000002 0.1 0.35000002 0.15000001 0.35000002\n\t\t 0.2 0.35000002 0.25 0.35000002 0.30000001 0.35000002 0.35000002 0.35000002 0.40000004\n\t\t 0.35000002 0.45000005 0.35000002 0.50000006 0.35000002 0.55000007 0.35000002 0.60000008\n\t\t 0.35000002 0.6500001 0.35000002 0.70000011 0.35000002 0.75000012 0.35000002 0.80000013\n\t\t 0.35000002 0.85000014 0.35000002 0.90000015 0.35000002 0.95000017 0.35000002 1.000000119209\n\t\t 0.35000002 0 0.40000004 0.050000001 0.40000004 0.1 0.40000004 0.15000001 0.40000004\n\t\t 0.2 0.40000004 0.25 0.40000004 0.30000001 0.40000004 0.35000002 0.40000004 0.40000004\n\t\t 0.40000004 0.45000005 0.40000004 0.50000006 0.40000004 0.55000007 0.40000004 0.60000008\n\t\t 0.40000004 0.6500001 0.40000004 0.70000011 0.40000004 0.75000012 0.40000004 0.80000013\n\t\t 0.40000004 0.85000014 0.40000004 0.90000015 0.40000004 0.95000017 0.40000004 1.000000119209\n\t\t 0.40000004 0 0.45000005 0.050000001 0.45000005 0.1 0.45000005 0.15000001 0.45000005\n\t\t 0.2 0.45000005 0.25 0.45000005 0.30000001 0.45000005 0.35000002 0.45000005 0.40000004\n\t\t 0.45000005 0.45000005 0.45000005 0.50000006 0.45000005 0.55000007 0.45000005 0.60000008\n\t\t 0.45000005 0.6500001 0.45000005 0.70000011 0.45000005 0.75000012 0.45000005 0.80000013\n\t\t 0.45000005 0.85000014 0.45000005 0.90000015 0.45000005 0.95000017 0.45000005 1.000000119209\n\t\t 0.45000005 0 0.50000006 0.050000001 0.50000006 0.1 0.50000006 0.15000001 0.50000006\n\t\t 0.2 0.50000006 0.25 0.50000006 0.30000001 0.50000006 0.35000002 0.50000006 0.40000004\n\t\t 0.50000006 0.45000005 0.50000006 0.50000006 0.50000006 0.55000007 0.50000006 0.60000008\n\t\t 0.50000006 0.6500001 0.50000006 0.70000011 0.50000006 0.75000012 0.50000006 0.80000013\n\t\t 0.50000006 0.85000014 0.50000006 0.90000015 0.50000006 0.95000017 0.50000006 1.000000119209\n\t\t 0.50000006 0 0.55000007 0.050000001 0.55000007 0.1 0.55000007 0.15000001 0.55000007\n\t\t 0.2 0.55000007 0.25 0.55000007 0.30000001 0.55000007 0.35000002 0.55000007 0.40000004\n\t\t 0.55000007 0.45000005 0.55000007 0.50000006 0.55000007 0.55000007 0.55000007 0.60000008\n\t\t 0.55000007 0.6500001 0.55000007 0.70000011 0.55000007 0.75000012 0.55000007 0.80000013\n\t\t 0.55000007 0.85000014 0.55000007 0.90000015 0.55000007 0.95000017 0.55000007 1.000000119209\n\t\t 0.55000007 0 0.60000008 0.050000001 0.60000008 0.1 0.60000008 0.15000001 0.60000008\n\t\t 0.2 0.60000008 0.25 0.60000008 0.30000001 0.60000008 0.35000002 0.60000008 0.40000004\n\t\t 0.60000008 0.45000005 0.60000008 0.50000006 0.60000008 0.55000007 0.60000008 0.60000008\n\t\t 0.60000008 0.6500001 0.60000008 0.70000011 0.60000008 0.75000012 0.60000008 0.80000013\n\t\t 0.60000008 0.85000014 0.60000008 0.90000015 0.60000008;\n\tsetAttr \".uvst[0].uvsp[250:438]\" 0.95000017 0.60000008 1.000000119209 0.60000008\n\t\t 0 0.6500001 0.050000001 0.6500001 0.1 0.6500001 0.15000001 0.6500001 0.2 0.6500001\n\t\t 0.25 0.6500001 0.30000001 0.6500001 0.35000002 0.6500001 0.40000004 0.6500001 0.45000005\n\t\t 0.6500001 0.50000006 0.6500001 0.55000007 0.6500001 0.60000008 0.6500001 0.6500001\n\t\t 0.6500001 0.70000011 0.6500001 0.75000012 0.6500001 0.80000013 0.6500001 0.85000014\n\t\t 0.6500001 0.90000015 0.6500001 0.95000017 0.6500001 1.000000119209 0.6500001 0 0.70000011\n\t\t 0.050000001 0.70000011 0.1 0.70000011 0.15000001 0.70000011 0.2 0.70000011 0.25 0.70000011\n\t\t 0.30000001 0.70000011 0.35000002 0.70000011 0.40000004 0.70000011 0.45000005 0.70000011\n\t\t 0.50000006 0.70000011 0.55000007 0.70000011 0.60000008 0.70000011 0.6500001 0.70000011\n\t\t 0.70000011 0.70000011 0.75000012 0.70000011 0.80000013 0.70000011 0.85000014 0.70000011\n\t\t 0.90000015 0.70000011 0.95000017 0.70000011 1.000000119209 0.70000011 0 0.75000012\n\t\t 0.050000001 0.75000012 0.1 0.75000012 0.15000001 0.75000012 0.2 0.75000012 0.25 0.75000012\n\t\t 0.30000001 0.75000012 0.35000002 0.75000012 0.40000004 0.75000012 0.45000005 0.75000012\n\t\t 0.50000006 0.75000012 0.55000007 0.75000012 0.60000008 0.75000012 0.6500001 0.75000012\n\t\t 0.70000011 0.75000012 0.75000012 0.75000012 0.80000013 0.75000012 0.85000014 0.75000012\n\t\t 0.90000015 0.75000012 0.95000017 0.75000012 1.000000119209 0.75000012 0 0.80000013\n\t\t 0.050000001 0.80000013 0.1 0.80000013 0.15000001 0.80000013 0.2 0.80000013 0.25 0.80000013\n\t\t 0.30000001 0.80000013 0.35000002 0.80000013 0.40000004 0.80000013 0.45000005 0.80000013\n\t\t 0.50000006 0.80000013 0.55000007 0.80000013 0.60000008 0.80000013 0.6500001 0.80000013\n\t\t 0.70000011 0.80000013 0.75000012 0.80000013 0.80000013 0.80000013 0.85000014 0.80000013\n\t\t 0.90000015 0.80000013 0.95000017 0.80000013 1.000000119209 0.80000013 0 0.85000014\n\t\t 0.050000001 0.85000014 0.1 0.85000014 0.15000001 0.85000014 0.2 0.85000014 0.25 0.85000014\n\t\t 0.30000001 0.85000014 0.35000002 0.85000014 0.40000004 0.85000014 0.45000005 0.85000014\n\t\t 0.50000006 0.85000014 0.55000007 0.85000014 0.60000008 0.85000014 0.6500001 0.85000014\n\t\t 0.70000011 0.85000014 0.75000012 0.85000014 0.80000013 0.85000014 0.85000014 0.85000014\n\t\t 0.90000015 0.85000014 0.95000017 0.85000014 1.000000119209 0.85000014 0 0.90000015\n\t\t 0.050000001 0.90000015 0.1 0.90000015 0.15000001 0.90000015 0.2 0.90000015 0.25 0.90000015\n\t\t 0.30000001 0.90000015 0.35000002 0.90000015 0.40000004 0.90000015 0.45000005 0.90000015\n\t\t 0.50000006 0.90000015 0.55000007 0.90000015 0.60000008 0.90000015 0.6500001 0.90000015\n\t\t 0.70000011 0.90000015 0.75000012 0.90000015 0.80000013 0.90000015 0.85000014 0.90000015\n\t\t 0.90000015 0.90000015 0.95000017 0.90000015 1.000000119209 0.90000015 0 0.95000017\n\t\t 0.050000001 0.95000017 0.1 0.95000017 0.15000001 0.95000017 0.2 0.95000017 0.25 0.95000017\n\t\t 0.30000001 0.95000017 0.35000002 0.95000017 0.40000004 0.95000017 0.45000005 0.95000017\n\t\t 0.50000006 0.95000017 0.55000007 0.95000017 0.60000008 0.95000017 0.6500001 0.95000017\n\t\t 0.70000011 0.95000017 0.75000012 0.95000017 0.80000013 0.95000017 0.85000014 0.95000017\n\t\t 0.90000015 0.95000017 0.95000017 0.95000017 1.000000119209 0.95000017 0.025 0 0.075000003\n\t\t 0 0.125 0 0.17500001 0 0.22500001 0 0.27500001 0 0.32500002 0 0.375 0 0.42500001\n\t\t 0 0.47500002 0 0.52499998 0 0.57499999 0 0.625 0 0.67500001 0 0.72499996 0 0.77499998\n\t\t 0 0.82499999 0 0.875 0 0.92500001 0 0.97499996 0 0.025 1 0.075000003 1 0.125 1 0.17500001\n\t\t 1 0.22500001 1 0.27500001 1 0.32500002 1 0.375 1 0.42500001 1 0.47500002 1 0.52499998\n\t\t 1 0.57499999 1 0.625 1 0.67500001 1 0.72499996 1 0.77499998 1 0.82499999 1 0.875\n\t\t 1 0.92500001 1 0.97499996 1;\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr -s 382 \".vt\";\n\tsetAttr \".vt[0:165]\"  0.14877813 -0.98768836 -0.048340943 0.12655823 -0.98768836 -0.091949932\n\t\t 0.091949932 -0.98768836 -0.12655823 0.048340935 -0.98768836 -0.14877811 0 -0.98768836 -0.15643455\n\t\t -0.048340935 -0.98768836 -0.1487781 -0.091949917 -0.98768836 -0.1265582 -0.12655818 -0.98768836 -0.091949902\n\t\t -0.14877807 -0.98768836 -0.048340924 -0.15643452 -0.98768836 0 -0.14877807 -0.98768836 0.048340924\n\t\t -0.12655818 -0.98768836 0.091949895 -0.091949895 -0.98768836 0.12655817 -0.048340924 -0.98768836 0.14877805\n\t\t -4.6621107e-09 -0.98768836 0.15643449 0.048340909 -0.98768836 0.14877804 0.09194988 -0.98768836 0.12655815\n\t\t 0.12655815 -0.98768836 0.091949888 0.14877804 -0.98768836 0.048340913 0.15643448 -0.98768836 0\n\t\t 0.29389283 -0.95105654 -0.095491566 0.25000018 -0.95105654 -0.18163574 0.18163574 -0.95105654 -0.25000015\n\t\t 0.095491551 -0.95105654 -0.2938928 0 -0.95105654 -0.30901715 -0.095491551 -0.95105654 -0.29389277\n\t\t -0.18163571 -0.95105654 -0.25000009 -0.25000009 -0.95105654 -0.18163569 -0.29389271 -0.95105654 -0.095491529\n\t\t -0.30901706 -0.95105654 0 -0.29389271 -0.95105654 0.095491529 -0.25000006 -0.95105654 0.18163568\n\t\t -0.18163568 -0.95105654 0.25000006 -0.095491529 -0.95105654 0.29389268 -9.2094243e-09 -0.95105654 0.30901703\n\t\t 0.095491499 -0.95105654 0.29389265 0.18163563 -0.95105654 0.25000003 0.25 -0.95105654 0.18163565\n\t\t 0.29389265 -0.95105654 0.095491506 0.309017 -0.95105654 0 0.43177092 -0.89100653 -0.14029087\n\t\t 0.36728629 -0.89100653 -0.2668491 0.2668491 -0.89100653 -0.36728626 0.14029086 -0.89100653 -0.43177086\n\t\t 0 -0.89100653 -0.45399073 -0.14029086 -0.89100653 -0.43177083 -0.26684904 -0.89100653 -0.36728618\n\t\t -0.36728615 -0.89100653 -0.26684901 -0.43177077 -0.89100653 -0.14029081 -0.45399064 -0.89100653 0\n\t\t -0.43177077 -0.89100653 0.14029081 -0.36728612 -0.89100653 0.26684898 -0.26684898 -0.89100653 0.36728612\n\t\t -0.14029081 -0.89100653 0.43177071 -1.3529972e-08 -0.89100653 0.45399058 0.14029078 -0.89100653 0.43177068\n\t\t 0.26684892 -0.89100653 0.36728609 0.36728606 -0.89100653 0.26684895 0.43177065 -0.89100653 0.1402908\n\t\t 0.45399052 -0.89100653 0 0.55901736 -0.809017 -0.18163574 0.47552857 -0.809017 -0.34549171\n\t\t 0.34549171 -0.809017 -0.47552854 0.18163572 -0.809017 -0.5590173 0 -0.809017 -0.58778554\n\t\t -0.18163572 -0.809017 -0.55901724 -0.34549165 -0.809017 -0.47552842 -0.47552839 -0.809017 -0.34549159\n\t\t -0.55901712 -0.809017 -0.18163566 -0.58778536 -0.809017 0 -0.55901712 -0.809017 0.18163566\n\t\t -0.47552836 -0.809017 0.34549156 -0.34549156 -0.809017 0.47552833 -0.18163566 -0.809017 0.55901706\n\t\t -1.7517365e-08 -0.809017 0.5877853 0.18163562 -0.809017 0.55901706 0.3454915 -0.809017 0.4755283\n\t\t 0.47552827 -0.809017 0.34549153 0.559017 -0.809017 0.18163563 0.58778524 -0.809017 0\n\t\t 0.67249894 -0.70710677 -0.21850814 0.57206178 -0.70710677 -0.41562718 0.41562718 -0.70710677 -0.57206172\n\t\t 0.21850812 -0.70710677 -0.67249888 0 -0.70710677 -0.70710713 -0.21850812 -0.70710677 -0.67249882\n\t\t -0.41562709 -0.70710677 -0.5720616 -0.57206154 -0.70710677 -0.41562706 -0.6724987 -0.70710677 -0.21850805\n\t\t -0.70710695 -0.70710677 0 -0.6724987 -0.70710677 0.21850805 -0.57206154 -0.70710677 0.415627\n\t\t -0.415627 -0.70710677 0.57206148 -0.21850805 -0.70710677 0.67249858 -2.1073424e-08 -0.70710677 0.70710683\n\t\t 0.21850799 -0.70710677 0.67249858 0.41562691 -0.70710677 0.57206142 0.57206142 -0.70710677 0.41562697\n\t\t 0.67249852 -0.70710677 0.21850802 0.70710677 -0.70710677 0 0.7694214 -0.58778524 -0.25000015\n\t\t 0.65450895 -0.58778524 -0.47552854 0.47552854 -0.58778524 -0.65450889 0.25000012 -0.58778524 -0.76942128\n\t\t 0 -0.58778524 -0.80901736 -0.25000012 -0.58778524 -0.76942122 -0.47552845 -0.58778524 -0.65450877\n\t\t -0.65450871 -0.58778524 -0.47552839 -0.7694211 -0.58778524 -0.25000006 -0.80901718 -0.58778524 0\n\t\t -0.7694211 -0.58778524 0.25000006 -0.65450865 -0.58778524 0.47552836 -0.47552836 -0.58778524 0.65450859\n\t\t -0.25000006 -0.58778524 0.76942098 -2.4110586e-08 -0.58778524 0.80901712 0.24999999 -0.58778524 0.76942098\n\t\t 0.47552827 -0.58778524 0.65450853 0.65450853 -0.58778524 0.4755283 0.76942092 -0.58778524 0.25\n\t\t 0.809017 -0.58778524 0 0.8473981 -0.45399052 -0.27533633 0.72083992 -0.45399052 -0.5237208\n\t\t 0.5237208 -0.45399052 -0.72083986 0.2753363 -0.45399052 -0.84739798 0 -0.45399052 -0.89100695\n\t\t -0.2753363 -0.45399052 -0.84739798 -0.52372068 -0.45399052 -0.72083968 -0.72083962 -0.45399052 -0.52372062\n\t\t -0.8473978 -0.45399052 -0.27533621 -0.89100677 -0.45399052 0 -0.8473978 -0.45399052 0.27533621\n\t\t -0.72083962 -0.45399052 0.52372062 -0.52372062 -0.45399052 0.72083956 -0.27533621 -0.45399052 0.84739769\n\t\t -2.6554064e-08 -0.45399052 0.89100665 0.27533615 -0.45399052 0.84739763 0.5237205 -0.45399052 0.7208395\n\t\t 0.72083944 -0.45399052 0.52372056 0.84739757 -0.45399052 0.27533618 0.89100653 -0.45399052 0\n\t\t 0.90450913 -0.30901697 -0.2938928 0.7694214 -0.30901697 -0.55901736 0.55901736 -0.30901697 -0.76942134\n\t\t 0.29389277 -0.30901697 -0.90450901 0 -0.30901697 -0.95105702 -0.29389277 -0.30901697 -0.90450895\n\t\t -0.55901724 -0.30901697 -0.76942122 -0.76942116 -0.30901697 -0.55901718 -0.90450877 -0.30901697 -0.29389271\n\t\t -0.95105678 -0.30901697 0 -0.90450877 -0.30901697 0.29389271 -0.7694211 -0.30901697 0.55901712\n\t\t -0.55901712 -0.30901697 0.76942104 -0.29389271 -0.30901697 0.90450865 -2.8343694e-08 -0.30901697 0.95105666\n\t\t 0.29389262 -0.30901697 0.90450859 0.559017 -0.30901697 0.76942098 0.76942092 -0.30901697 0.55901706\n\t\t 0.90450853 -0.30901697 0.29389265 0.95105654 -0.30901697 0 0.93934804 -0.15643437 -0.30521268\n\t\t 0.79905719 -0.15643437 -0.580549 0.580549 -0.15643437 -0.79905713 0.30521265 -0.15643437 -0.93934792\n\t\t 0 -0.15643437 -0.98768884 -0.30521265 -0.15643437 -0.93934786;\n\tsetAttr \".vt[166:331]\" -0.58054888 -0.15643437 -0.79905695 -0.79905689 -0.15643437 -0.58054882\n\t\t -0.93934768 -0.15643437 -0.30521256 -0.9876886 -0.15643437 0 -0.93934768 -0.15643437 0.30521256\n\t\t -0.79905683 -0.15643437 0.58054876 -0.58054876 -0.15643437 0.79905677 -0.30521256 -0.15643437 0.93934757\n\t\t -2.9435407e-08 -0.15643437 0.98768848 0.30521247 -0.15643437 0.93934757 0.58054864 -0.15643437 0.79905671\n\t\t 0.79905665 -0.15643437 0.5805487 0.93934751 -0.15643437 0.3052125 0.98768836 -0.15643437 0\n\t\t 0.95105714 0 -0.30901718 0.80901754 0 -0.5877856 0.5877856 0 -0.80901748 0.30901715 0 -0.95105702\n\t\t 0 0 -1.000000476837 -0.30901715 0 -0.95105696 -0.58778548 0 -0.8090173 -0.80901724 0 -0.58778542\n\t\t -0.95105678 0 -0.30901706 -1.000000238419 0 0 -0.95105678 0 0.30901706 -0.80901718 0 0.58778536\n\t\t -0.58778536 0 0.80901712 -0.30901706 0 0.95105666 -2.9802322e-08 0 1.000000119209\n\t\t 0.30901697 0 0.9510566 0.58778524 0 0.80901706 0.809017 0 0.5877853 0.95105654 0 0.309017\n\t\t 1 0 0 0.93934804 0.15643437 -0.30521268 0.79905719 0.15643437 -0.580549 0.580549 0.15643437 -0.79905713\n\t\t 0.30521265 0.15643437 -0.93934792 0 0.15643437 -0.98768884 -0.30521265 0.15643437 -0.93934786\n\t\t -0.58054888 0.15643437 -0.79905695 -0.79905689 0.15643437 -0.58054882 -0.93934768 0.15643437 -0.30521256\n\t\t -0.9876886 0.15643437 0 -0.93934768 0.15643437 0.30521256 -0.79905683 0.15643437 0.58054876\n\t\t -0.58054876 0.15643437 0.79905677 -0.30521256 0.15643437 0.93934757 -2.9435407e-08 0.15643437 0.98768848\n\t\t 0.30521247 0.15643437 0.93934757 0.58054864 0.15643437 0.79905671 0.79905665 0.15643437 0.5805487\n\t\t 0.93934751 0.15643437 0.3052125 0.98768836 0.15643437 0 0.90450913 0.30901697 -0.2938928\n\t\t 0.7694214 0.30901697 -0.55901736 0.55901736 0.30901697 -0.76942134 0.29389277 0.30901697 -0.90450901\n\t\t 0 0.30901697 -0.95105702 -0.29389277 0.30901697 -0.90450895 -0.55901724 0.30901697 -0.76942122\n\t\t -0.76942116 0.30901697 -0.55901718 -0.90450877 0.30901697 -0.29389271 -0.95105678 0.30901697 0\n\t\t -0.90450877 0.30901697 0.29389271 -0.7694211 0.30901697 0.55901712 -0.55901712 0.30901697 0.76942104\n\t\t -0.29389271 0.30901697 0.90450865 -2.8343694e-08 0.30901697 0.95105666 0.29389262 0.30901697 0.90450859\n\t\t 0.559017 0.30901697 0.76942098 0.76942092 0.30901697 0.55901706 0.90450853 0.30901697 0.29389265\n\t\t 0.95105654 0.30901697 0 0.8473981 0.45399052 -0.27533633 0.72083992 0.45399052 -0.5237208\n\t\t 0.5237208 0.45399052 -0.72083986 0.2753363 0.45399052 -0.84739798 0 0.45399052 -0.89100695\n\t\t -0.2753363 0.45399052 -0.84739798 -0.52372068 0.45399052 -0.72083968 -0.72083962 0.45399052 -0.52372062\n\t\t -0.8473978 0.45399052 -0.27533621 -0.89100677 0.45399052 0 -0.8473978 0.45399052 0.27533621\n\t\t -0.72083962 0.45399052 0.52372062 -0.52372062 0.45399052 0.72083956 -0.27533621 0.45399052 0.84739769\n\t\t -2.6554064e-08 0.45399052 0.89100665 0.27533615 0.45399052 0.84739763 0.5237205 0.45399052 0.7208395\n\t\t 0.72083944 0.45399052 0.52372056 0.84739757 0.45399052 0.27533618 0.89100653 0.45399052 0\n\t\t 0.7694214 0.58778524 -0.25000015 0.65450895 0.58778524 -0.47552854 0.47552854 0.58778524 -0.65450889\n\t\t 0.25000012 0.58778524 -0.76942128 0 0.58778524 -0.80901736 -0.25000012 0.58778524 -0.76942122\n\t\t -0.47552845 0.58778524 -0.65450877 -0.65450871 0.58778524 -0.47552839 -0.7694211 0.58778524 -0.25000006\n\t\t -0.80901718 0.58778524 0 -0.7694211 0.58778524 0.25000006 -0.65450865 0.58778524 0.47552836\n\t\t -0.47552836 0.58778524 0.65450859 -0.25000006 0.58778524 0.76942098 -2.4110586e-08 0.58778524 0.80901712\n\t\t 0.24999999 0.58778524 0.76942098 0.47552827 0.58778524 0.65450853 0.65450853 0.58778524 0.4755283\n\t\t 0.76942092 0.58778524 0.25 0.809017 0.58778524 0 0.67249894 0.70710677 -0.21850814\n\t\t 0.57206178 0.70710677 -0.41562718 0.41562718 0.70710677 -0.57206172 0.21850812 0.70710677 -0.67249888\n\t\t 0 0.70710677 -0.70710713 -0.21850812 0.70710677 -0.67249882 -0.41562709 0.70710677 -0.5720616\n\t\t -0.57206154 0.70710677 -0.41562706 -0.6724987 0.70710677 -0.21850805 -0.70710695 0.70710677 0\n\t\t -0.6724987 0.70710677 0.21850805 -0.57206154 0.70710677 0.415627 -0.415627 0.70710677 0.57206148\n\t\t -0.21850805 0.70710677 0.67249858 -2.1073424e-08 0.70710677 0.70710683 0.21850799 0.70710677 0.67249858\n\t\t 0.41562691 0.70710677 0.57206142 0.57206142 0.70710677 0.41562697 0.67249852 0.70710677 0.21850802\n\t\t 0.70710677 0.70710677 0 0.55901736 0.809017 -0.18163574 0.47552857 0.809017 -0.34549171\n\t\t 0.34549171 0.809017 -0.47552854 0.18163572 0.809017 -0.5590173 0 0.809017 -0.58778554\n\t\t -0.18163572 0.809017 -0.55901724 -0.34549165 0.809017 -0.47552842 -0.47552839 0.809017 -0.34549159\n\t\t -0.55901712 0.809017 -0.18163566 -0.58778536 0.809017 0 -0.55901712 0.809017 0.18163566\n\t\t -0.47552836 0.809017 0.34549156 -0.34549156 0.809017 0.47552833 -0.18163566 0.809017 0.55901706\n\t\t -1.7517365e-08 0.809017 0.5877853 0.18163562 0.809017 0.55901706 0.3454915 0.809017 0.4755283\n\t\t 0.47552827 0.809017 0.34549153 0.559017 0.809017 0.18163563 0.58778524 0.809017 0\n\t\t 0.43177092 0.89100653 -0.14029087 0.36728629 0.89100653 -0.2668491 0.2668491 0.89100653 -0.36728626\n\t\t 0.14029086 0.89100653 -0.43177086 0 0.89100653 -0.45399073 -0.14029086 0.89100653 -0.43177083\n\t\t -0.26684904 0.89100653 -0.36728618 -0.36728615 0.89100653 -0.26684901 -0.43177077 0.89100653 -0.14029081\n\t\t -0.45399064 0.89100653 0 -0.43177077 0.89100653 0.14029081 -0.36728612 0.89100653 0.26684898;\n\tsetAttr \".vt[332:381]\" -0.26684898 0.89100653 0.36728612 -0.14029081 0.89100653 0.43177071\n\t\t -1.3529972e-08 0.89100653 0.45399058 0.14029078 0.89100653 0.43177068 0.26684892 0.89100653 0.36728609\n\t\t 0.36728606 0.89100653 0.26684895 0.43177065 0.89100653 0.1402908 0.45399052 0.89100653 0\n\t\t 0.29389283 0.95105654 -0.095491566 0.25000018 0.95105654 -0.18163574 0.18163574 0.95105654 -0.25000015\n\t\t 0.095491551 0.95105654 -0.2938928 0 0.95105654 -0.30901715 -0.095491551 0.95105654 -0.29389277\n\t\t -0.18163571 0.95105654 -0.25000009 -0.25000009 0.95105654 -0.18163569 -0.29389271 0.95105654 -0.095491529\n\t\t -0.30901706 0.95105654 0 -0.29389271 0.95105654 0.095491529 -0.25000006 0.95105654 0.18163568\n\t\t -0.18163568 0.95105654 0.25000006 -0.095491529 0.95105654 0.29389268 -9.2094243e-09 0.95105654 0.30901703\n\t\t 0.095491499 0.95105654 0.29389265 0.18163563 0.95105654 0.25000003 0.25 0.95105654 0.18163565\n\t\t 0.29389265 0.95105654 0.095491506 0.309017 0.95105654 0 0.14877813 0.98768836 -0.048340943\n\t\t 0.12655823 0.98768836 -0.091949932 0.091949932 0.98768836 -0.12655823 0.048340935 0.98768836 -0.14877811\n\t\t 0 0.98768836 -0.15643455 -0.048340935 0.98768836 -0.1487781 -0.091949917 0.98768836 -0.1265582\n\t\t -0.12655818 0.98768836 -0.091949902 -0.14877807 0.98768836 -0.048340924 -0.15643452 0.98768836 0\n\t\t -0.14877807 0.98768836 0.048340924 -0.12655818 0.98768836 0.091949895 -0.091949895 0.98768836 0.12655817\n\t\t -0.048340924 0.98768836 0.14877805 -4.6621107e-09 0.98768836 0.15643449 0.048340909 0.98768836 0.14877804\n\t\t 0.09194988 0.98768836 0.12655815 0.12655815 0.98768836 0.091949888 0.14877804 0.98768836 0.048340913\n\t\t 0.15643448 0.98768836 0 0 -1 0 0 1 0;\n\tsetAttr -s 780 \".ed\";\n\tsetAttr \".ed[0:165]\"  0 1 1 1 2 1 2 3 1 3 4 1 4 5 1 5 6 1 6 7 1 7 8 1 8 9 1\n\t\t 9 10 1 10 11 1 11 12 1 12 13 1 13 14 1 14 15 1 15 16 1 16 17 1 17 18 1 18 19 1 19 0 1\n\t\t 20 21 1 21 22 1 22 23 1 23 24 1 24 25 1 25 26 1 26 27 1 27 28 1 28 29 1 29 30 1 30 31 1\n\t\t 31 32 1 32 33 1 33 34 1 34 35 1 35 36 1 36 37 1 37 38 1 38 39 1 39 20 1 40 41 1 41 42 1\n\t\t 42 43 1 43 44 1 44 45 1 45 46 1 46 47 1 47 48 1 48 49 1 49 50 1 50 51 1 51 52 1 52 53 1\n\t\t 53 54 1 54 55 1 55 56 1 56 57 1 57 58 1 58 59 1 59 40 1 60 61 1 61 62 1 62 63 1 63 64 1\n\t\t 64 65 1 65 66 1 66 67 1 67 68 1 68 69 1 69 70 1 70 71 1 71 72 1 72 73 1 73 74 1 74 75 1\n\t\t 75 76 1 76 77 1 77 78 1 78 79 1 79 60 1 80 81 1 81 82 1 82 83 1 83 84 1 84 85 1 85 86 1\n\t\t 86 87 1 87 88 1 88 89 1 89 90 1 90 91 1 91 92 1 92 93 1 93 94 1 94 95 1 95 96 1 96 97 1\n\t\t 97 98 1 98 99 1 99 80 1 100 101 1 101 102 1 102 103 1 103 104 1 104 105 1 105 106 1\n\t\t 106 107 1 107 108 1 108 109 1 109 110 1 110 111 1 111 112 1 112 113 1 113 114 1 114 115 1\n\t\t 115 116 1 116 117 1 117 118 1 118 119 1 119 100 1 120 121 1 121 122 1 122 123 1 123 124 1\n\t\t 124 125 1 125 126 1 126 127 1 127 128 1 128 129 1 129 130 1 130 131 1 131 132 1 132 133 1\n\t\t 133 134 1 134 135 1 135 136 1 136 137 1 137 138 1 138 139 1 139 120 1 140 141 1 141 142 1\n\t\t 142 143 1 143 144 1 144 145 1 145 146 1 146 147 1 147 148 1 148 149 1 149 150 1 150 151 1\n\t\t 151 152 1 152 153 1 153 154 1 154 155 1 155 156 1 156 157 1 157 158 1 158 159 1 159 140 1\n\t\t 160 161 1 161 162 1 162 163 1 163 164 1 164 165 1 165 166 1;\n\tsetAttr \".ed[166:331]\" 166 167 1 167 168 1 168 169 1 169 170 1 170 171 1 171 172 1\n\t\t 172 173 1 173 174 1 174 175 1 175 176 1 176 177 1 177 178 1 178 179 1 179 160 1 180 181 1\n\t\t 181 182 1 182 183 1 183 184 1 184 185 1 185 186 1 186 187 1 187 188 1 188 189 1 189 190 1\n\t\t 190 191 1 191 192 1 192 193 1 193 194 1 194 195 1 195 196 1 196 197 1 197 198 1 198 199 1\n\t\t 199 180 1 200 201 1 201 202 1 202 203 1 203 204 1 204 205 1 205 206 1 206 207 1 207 208 1\n\t\t 208 209 1 209 210 1 210 211 1 211 212 1 212 213 1 213 214 1 214 215 1 215 216 1 216 217 1\n\t\t 217 218 1 218 219 1 219 200 1 220 221 1 221 222 1 222 223 1 223 224 1 224 225 1 225 226 1\n\t\t 226 227 1 227 228 1 228 229 1 229 230 1 230 231 1 231 232 1 232 233 1 233 234 1 234 235 1\n\t\t 235 236 1 236 237 1 237 238 1 238 239 1 239 220 1 240 241 1 241 242 1 242 243 1 243 244 1\n\t\t 244 245 1 245 246 1 246 247 1 247 248 1 248 249 1 249 250 1 250 251 1 251 252 1 252 253 1\n\t\t 253 254 1 254 255 1 255 256 1 256 257 1 257 258 1 258 259 1 259 240 1 260 261 1 261 262 1\n\t\t 262 263 1 263 264 1 264 265 1 265 266 1 266 267 1 267 268 1 268 269 1 269 270 1 270 271 1\n\t\t 271 272 1 272 273 1 273 274 1 274 275 1 275 276 1 276 277 1 277 278 1 278 279 1 279 260 1\n\t\t 280 281 1 281 282 1 282 283 1 283 284 1 284 285 1 285 286 1 286 287 1 287 288 1 288 289 1\n\t\t 289 290 1 290 291 1 291 292 1 292 293 1 293 294 1 294 295 1 295 296 1 296 297 1 297 298 1\n\t\t 298 299 1 299 280 1 300 301 1 301 302 1 302 303 1 303 304 1 304 305 1 305 306 1 306 307 1\n\t\t 307 308 1 308 309 1 309 310 1 310 311 1 311 312 1 312 313 1 313 314 1 314 315 1 315 316 1\n\t\t 316 317 1 317 318 1 318 319 1 319 300 1 320 321 1 321 322 1 322 323 1 323 324 1 324 325 1\n\t\t 325 326 1 326 327 1 327 328 1 328 329 1 329 330 1 330 331 1 331 332 1;\n\tsetAttr \".ed[332:497]\" 332 333 1 333 334 1 334 335 1 335 336 1 336 337 1 337 338 1\n\t\t 338 339 1 339 320 1 340 341 1 341 342 1 342 343 1 343 344 1 344 345 1 345 346 1 346 347 1\n\t\t 347 348 1 348 349 1 349 350 1 350 351 1 351 352 1 352 353 1 353 354 1 354 355 1 355 356 1\n\t\t 356 357 1 357 358 1 358 359 1 359 340 1 360 361 1 361 362 1 362 363 1 363 364 1 364 365 1\n\t\t 365 366 1 366 367 1 367 368 1 368 369 1 369 370 1 370 371 1 371 372 1 372 373 1 373 374 1\n\t\t 374 375 1 375 376 1 376 377 1 377 378 1 378 379 1 379 360 1 0 20 1 1 21 1 2 22 1\n\t\t 3 23 1 4 24 1 5 25 1 6 26 1 7 27 1 8 28 1 9 29 1 10 30 1 11 31 1 12 32 1 13 33 1\n\t\t 14 34 1 15 35 1 16 36 1 17 37 1 18 38 1 19 39 1 20 40 1 21 41 1 22 42 1 23 43 1 24 44 1\n\t\t 25 45 1 26 46 1 27 47 1 28 48 1 29 49 1 30 50 1 31 51 1 32 52 1 33 53 1 34 54 1 35 55 1\n\t\t 36 56 1 37 57 1 38 58 1 39 59 1 40 60 1 41 61 1 42 62 1 43 63 1 44 64 1 45 65 1 46 66 1\n\t\t 47 67 1 48 68 1 49 69 1 50 70 1 51 71 1 52 72 1 53 73 1 54 74 1 55 75 1 56 76 1 57 77 1\n\t\t 58 78 1 59 79 1 60 80 1 61 81 1 62 82 1 63 83 1 64 84 1 65 85 1 66 86 1 67 87 1 68 88 1\n\t\t 69 89 1 70 90 1 71 91 1 72 92 1 73 93 1 74 94 1 75 95 1 76 96 1 77 97 1 78 98 1 79 99 1\n\t\t 80 100 1 81 101 1 82 102 1 83 103 1 84 104 1 85 105 1 86 106 1 87 107 1 88 108 1\n\t\t 89 109 1 90 110 1 91 111 1 92 112 1 93 113 1 94 114 1 95 115 1 96 116 1 97 117 1\n\t\t 98 118 1 99 119 1 100 120 1 101 121 1 102 122 1 103 123 1 104 124 1 105 125 1 106 126 1\n\t\t 107 127 1 108 128 1 109 129 1 110 130 1 111 131 1 112 132 1 113 133 1 114 134 1 115 135 1\n\t\t 116 136 1 117 137 1;\n\tsetAttr \".ed[498:663]\" 118 138 1 119 139 1 120 140 1 121 141 1 122 142 1 123 143 1\n\t\t 124 144 1 125 145 1 126 146 1 127 147 1 128 148 1 129 149 1 130 150 1 131 151 1 132 152 1\n\t\t 133 153 1 134 154 1 135 155 1 136 156 1 137 157 1 138 158 1 139 159 1 140 160 1 141 161 1\n\t\t 142 162 1 143 163 1 144 164 1 145 165 1 146 166 1 147 167 1 148 168 1 149 169 1 150 170 1\n\t\t 151 171 1 152 172 1 153 173 1 154 174 1 155 175 1 156 176 1 157 177 1 158 178 1 159 179 1\n\t\t 160 180 1 161 181 1 162 182 1 163 183 1 164 184 1 165 185 1 166 186 1 167 187 1 168 188 1\n\t\t 169 189 1 170 190 1 171 191 1 172 192 1 173 193 1 174 194 1 175 195 1 176 196 1 177 197 1\n\t\t 178 198 1 179 199 1 180 200 1 181 201 1 182 202 1 183 203 1 184 204 1 185 205 1 186 206 1\n\t\t 187 207 1 188 208 1 189 209 1 190 210 1 191 211 1 192 212 1 193 213 1 194 214 1 195 215 1\n\t\t 196 216 1 197 217 1 198 218 1 199 219 1 200 220 1 201 221 1 202 222 1 203 223 1 204 224 1\n\t\t 205 225 1 206 226 1 207 227 1 208 228 1 209 229 1 210 230 1 211 231 1 212 232 1 213 233 1\n\t\t 214 234 1 215 235 1 216 236 1 217 237 1 218 238 1 219 239 1 220 240 1 221 241 1 222 242 1\n\t\t 223 243 1 224 244 1 225 245 1 226 246 1 227 247 1 228 248 1 229 249 1 230 250 1 231 251 1\n\t\t 232 252 1 233 253 1 234 254 1 235 255 1 236 256 1 237 257 1 238 258 1 239 259 1 240 260 1\n\t\t 241 261 1 242 262 1 243 263 1 244 264 1 245 265 1 246 266 1 247 267 1 248 268 1 249 269 1\n\t\t 250 270 1 251 271 1 252 272 1 253 273 1 254 274 1 255 275 1 256 276 1 257 277 1 258 278 1\n\t\t 259 279 1 260 280 1 261 281 1 262 282 1 263 283 1 264 284 1 265 285 1 266 286 1 267 287 1\n\t\t 268 288 1 269 289 1 270 290 1 271 291 1 272 292 1 273 293 1 274 294 1 275 295 1 276 296 1\n\t\t 277 297 1 278 298 1 279 299 1 280 300 1 281 301 1 282 302 1 283 303 1;\n\tsetAttr \".ed[664:779]\" 284 304 1 285 305 1 286 306 1 287 307 1 288 308 1 289 309 1\n\t\t 290 310 1 291 311 1 292 312 1 293 313 1 294 314 1 295 315 1 296 316 1 297 317 1 298 318 1\n\t\t 299 319 1 300 320 1 301 321 1 302 322 1 303 323 1 304 324 1 305 325 1 306 326 1 307 327 1\n\t\t 308 328 1 309 329 1 310 330 1 311 331 1 312 332 1 313 333 1 314 334 1 315 335 1 316 336 1\n\t\t 317 337 1 318 338 1 319 339 1 320 340 1 321 341 1 322 342 1 323 343 1 324 344 1 325 345 1\n\t\t 326 346 1 327 347 1 328 348 1 329 349 1 330 350 1 331 351 1 332 352 1 333 353 1 334 354 1\n\t\t 335 355 1 336 356 1 337 357 1 338 358 1 339 359 1 340 360 1 341 361 1 342 362 1 343 363 1\n\t\t 344 364 1 345 365 1 346 366 1 347 367 1 348 368 1 349 369 1 350 370 1 351 371 1 352 372 1\n\t\t 353 373 1 354 374 1 355 375 1 356 376 1 357 377 1 358 378 1 359 379 1 380 0 1 380 1 1\n\t\t 380 2 1 380 3 1 380 4 1 380 5 1 380 6 1 380 7 1 380 8 1 380 9 1 380 10 1 380 11 1\n\t\t 380 12 1 380 13 1 380 14 1 380 15 1 380 16 1 380 17 1 380 18 1 380 19 1 360 381 1\n\t\t 361 381 1 362 381 1 363 381 1 364 381 1 365 381 1 366 381 1 367 381 1 368 381 1 369 381 1\n\t\t 370 381 1 371 381 1 372 381 1 373 381 1 374 381 1 375 381 1 376 381 1 377 381 1 378 381 1\n\t\t 379 381 1;\n\tsetAttr -s 400 -ch 1560 \".fc[0:399]\" -type \"polyFaces\" \n\t\tf 4 0 381 -21 -381\n\t\tmu 0 4 0 1 22 21\n\t\tf 4 1 382 -22 -382\n\t\tmu 0 4 1 2 23 22\n\t\tf 4 2 383 -23 -383\n\t\tmu 0 4 2 3 24 23\n\t\tf 4 3 384 -24 -384\n\t\tmu 0 4 3 4 25 24\n\t\tf 4 4 385 -25 -385\n\t\tmu 0 4 4 5 26 25\n\t\tf 4 5 386 -26 -386\n\t\tmu 0 4 5 6 27 26\n\t\tf 4 6 387 -27 -387\n\t\tmu 0 4 6 7 28 27\n\t\tf 4 7 388 -28 -388\n\t\tmu 0 4 7 8 29 28\n\t\tf 4 8 389 -29 -389\n\t\tmu 0 4 8 9 30 29\n\t\tf 4 9 390 -30 -390\n\t\tmu 0 4 9 10 31 30\n\t\tf 4 10 391 -31 -391\n\t\tmu 0 4 10 11 32 31\n\t\tf 4 11 392 -32 -392\n\t\tmu 0 4 11 12 33 32\n\t\tf 4 12 393 -33 -393\n\t\tmu 0 4 12 13 34 33\n\t\tf 4 13 394 -34 -394\n\t\tmu 0 4 13 14 35 34\n\t\tf 4 14 395 -35 -395\n\t\tmu 0 4 14 15 36 35\n\t\tf 4 15 396 -36 -396\n\t\tmu 0 4 15 16 37 36\n\t\tf 4 16 397 -37 -397\n\t\tmu 0 4 16 17 38 37\n\t\tf 4 17 398 -38 -398\n\t\tmu 0 4 17 18 39 38\n\t\tf 4 18 399 -39 -399\n\t\tmu 0 4 18 19 40 39\n\t\tf 4 19 380 -40 -400\n\t\tmu 0 4 19 20 41 40\n\t\tf 4 20 401 -41 -401\n\t\tmu 0 4 21 22 43 42\n\t\tf 4 21 402 -42 -402\n\t\tmu 0 4 22 23 44 43\n\t\tf 4 22 403 -43 -403\n\t\tmu 0 4 23 24 45 44\n\t\tf 4 23 404 -44 -404\n\t\tmu 0 4 24 25 46 45\n\t\tf 4 24 405 -45 -405\n\t\tmu 0 4 25 26 47 46\n\t\tf 4 25 406 -46 -406\n\t\tmu 0 4 26 27 48 47\n\t\tf 4 26 407 -47 -407\n\t\tmu 0 4 27 28 49 48\n\t\tf 4 27 408 -48 -408\n\t\tmu 0 4 28 29 50 49\n\t\tf 4 28 409 -49 -409\n\t\tmu 0 4 29 30 51 50\n\t\tf 4 29 410 -50 -410\n\t\tmu 0 4 30 31 52 51\n\t\tf 4 30 411 -51 -411\n\t\tmu 0 4 31 32 53 52\n\t\tf 4 31 412 -52 -412\n\t\tmu 0 4 32 33 54 53\n\t\tf 4 32 413 -53 -413\n\t\tmu 0 4 33 34 55 54\n\t\tf 4 33 414 -54 -414\n\t\tmu 0 4 34 35 56 55\n\t\tf 4 34 415 -55 -415\n\t\tmu 0 4 35 36 57 56\n\t\tf 4 35 416 -56 -416\n\t\tmu 0 4 36 37 58 57\n\t\tf 4 36 417 -57 -417\n\t\tmu 0 4 37 38 59 58\n\t\tf 4 37 418 -58 -418\n\t\tmu 0 4 38 39 60 59\n\t\tf 4 38 419 -59 -419\n\t\tmu 0 4 39 40 61 60\n\t\tf 4 39 400 -60 -420\n\t\tmu 0 4 40 41 62 61\n\t\tf 4 40 421 -61 -421\n\t\tmu 0 4 42 43 64 63\n\t\tf 4 41 422 -62 -422\n\t\tmu 0 4 43 44 65 64\n\t\tf 4 42 423 -63 -423\n\t\tmu 0 4 44 45 66 65\n\t\tf 4 43 424 -64 -424\n\t\tmu 0 4 45 46 67 66\n\t\tf 4 44 425 -65 -425\n\t\tmu 0 4 46 47 68 67\n\t\tf 4 45 426 -66 -426\n\t\tmu 0 4 47 48 69 68\n\t\tf 4 46 427 -67 -427\n\t\tmu 0 4 48 49 70 69\n\t\tf 4 47 428 -68 -428\n\t\tmu 0 4 49 50 71 70\n\t\tf 4 48 429 -69 -429\n\t\tmu 0 4 50 51 72 71\n\t\tf 4 49 430 -70 -430\n\t\tmu 0 4 51 52 73 72\n\t\tf 4 50 431 -71 -431\n\t\tmu 0 4 52 53 74 73\n\t\tf 4 51 432 -72 -432\n\t\tmu 0 4 53 54 75 74\n\t\tf 4 52 433 -73 -433\n\t\tmu 0 4 54 55 76 75\n\t\tf 4 53 434 -74 -434\n\t\tmu 0 4 55 56 77 76\n\t\tf 4 54 435 -75 -435\n\t\tmu 0 4 56 57 78 77\n\t\tf 4 55 436 -76 -436\n\t\tmu 0 4 57 58 79 78\n\t\tf 4 56 437 -77 -437\n\t\tmu 0 4 58 59 80 79\n\t\tf 4 57 438 -78 -438\n\t\tmu 0 4 59 60 81 80\n\t\tf 4 58 439 -79 -439\n\t\tmu 0 4 60 61 82 81\n\t\tf 4 59 420 -80 -440\n\t\tmu 0 4 61 62 83 82\n\t\tf 4 60 441 -81 -441\n\t\tmu 0 4 63 64 85 84\n\t\tf 4 61 442 -82 -442\n\t\tmu 0 4 64 65 86 85\n\t\tf 4 62 443 -83 -443\n\t\tmu 0 4 65 66 87 86\n\t\tf 4 63 444 -84 -444\n\t\tmu 0 4 66 67 88 87\n\t\tf 4 64 445 -85 -445\n\t\tmu 0 4 67 68 89 88\n\t\tf 4 65 446 -86 -446\n\t\tmu 0 4 68 69 90 89\n\t\tf 4 66 447 -87 -447\n\t\tmu 0 4 69 70 91 90\n\t\tf 4 67 448 -88 -448\n\t\tmu 0 4 70 71 92 91\n\t\tf 4 68 449 -89 -449\n\t\tmu 0 4 71 72 93 92\n\t\tf 4 69 450 -90 -450\n\t\tmu 0 4 72 73 94 93\n\t\tf 4 70 451 -91 -451\n\t\tmu 0 4 73 74 95 94\n\t\tf 4 71 452 -92 -452\n\t\tmu 0 4 74 75 96 95\n\t\tf 4 72 453 -93 -453\n\t\tmu 0 4 75 76 97 96\n\t\tf 4 73 454 -94 -454\n\t\tmu 0 4 76 77 98 97\n\t\tf 4 74 455 -95 -455\n\t\tmu 0 4 77 78 99 98\n\t\tf 4 75 456 -96 -456\n\t\tmu 0 4 78 79 100 99\n\t\tf 4 76 457 -97 -457\n\t\tmu 0 4 79 80 101 100\n\t\tf 4 77 458 -98 -458\n\t\tmu 0 4 80 81 102 101\n\t\tf 4 78 459 -99 -459\n\t\tmu 0 4 81 82 103 102\n\t\tf 4 79 440 -100 -460\n\t\tmu 0 4 82 83 104 103\n\t\tf 4 80 461 -101 -461\n\t\tmu 0 4 84 85 106 105\n\t\tf 4 81 462 -102 -462\n\t\tmu 0 4 85 86 107 106\n\t\tf 4 82 463 -103 -463\n\t\tmu 0 4 86 87 108 107\n\t\tf 4 83 464 -104 -464\n\t\tmu 0 4 87 88 109 108\n\t\tf 4 84 465 -105 -465\n\t\tmu 0 4 88 89 110 109\n\t\tf 4 85 466 -106 -466\n\t\tmu 0 4 89 90 111 110\n\t\tf 4 86 467 -107 -467\n\t\tmu 0 4 90 91 112 111\n\t\tf 4 87 468 -108 -468\n\t\tmu 0 4 91 92 113 112\n\t\tf 4 88 469 -109 -469\n\t\tmu 0 4 92 93 114 113\n\t\tf 4 89 470 -110 -470\n\t\tmu 0 4 93 94 115 114\n\t\tf 4 90 471 -111 -471\n\t\tmu 0 4 94 95 116 115\n\t\tf 4 91 472 -112 -472\n\t\tmu 0 4 95 96 117 116\n\t\tf 4 92 473 -113 -473\n\t\tmu 0 4 96 97 118 117\n\t\tf 4 93 474 -114 -474\n\t\tmu 0 4 97 98 119 118\n\t\tf 4 94 475 -115 -475\n\t\tmu 0 4 98 99 120 119\n\t\tf 4 95 476 -116 -476\n\t\tmu 0 4 99 100 121 120\n\t\tf 4 96 477 -117 -477\n\t\tmu 0 4 100 101 122 121\n\t\tf 4 97 478 -118 -478\n\t\tmu 0 4 101 102 123 122\n\t\tf 4 98 479 -119 -479\n\t\tmu 0 4 102 103 124 123\n\t\tf 4 99 460 -120 -480\n\t\tmu 0 4 103 104 125 124\n\t\tf 4 100 481 -121 -481\n\t\tmu 0 4 105 106 127 126\n\t\tf 4 101 482 -122 -482\n\t\tmu 0 4 106 107 128 127\n\t\tf 4 102 483 -123 -483\n\t\tmu 0 4 107 108 129 128\n\t\tf 4 103 484 -124 -484\n\t\tmu 0 4 108 109 130 129\n\t\tf 4 104 485 -125 -485\n\t\tmu 0 4 109 110 131 130\n\t\tf 4 105 486 -126 -486\n\t\tmu 0 4 110 111 132 131\n\t\tf 4 106 487 -127 -487\n\t\tmu 0 4 111 112 133 132\n\t\tf 4 107 488 -128 -488\n\t\tmu 0 4 112 113 134 133\n\t\tf 4 108 489 -129 -489\n\t\tmu 0 4 113 114 135 134\n\t\tf 4 109 490 -130 -490\n\t\tmu 0 4 114 115 136 135\n\t\tf 4 110 491 -131 -491\n\t\tmu 0 4 115 116 137 136\n\t\tf 4 111 492 -132 -492\n\t\tmu 0 4 116 117 138 137\n\t\tf 4 112 493 -133 -493\n\t\tmu 0 4 117 118 139 138\n\t\tf 4 113 494 -134 -494\n\t\tmu 0 4 118 119 140 139\n\t\tf 4 114 495 -135 -495\n\t\tmu 0 4 119 120 141 140\n\t\tf 4 115 496 -136 -496\n\t\tmu 0 4 120 121 142 141\n\t\tf 4 116 497 -137 -497\n\t\tmu 0 4 121 122 143 142\n\t\tf 4 117 498 -138 -498\n\t\tmu 0 4 122 123 144 143\n\t\tf 4 118 499 -139 -499\n\t\tmu 0 4 123 124 145 144\n\t\tf 4 119 480 -140 -500\n\t\tmu 0 4 124 125 146 145\n\t\tf 4 120 501 -141 -501\n\t\tmu 0 4 126 127 148 147\n\t\tf 4 121 502 -142 -502\n\t\tmu 0 4 127 128 149 148\n\t\tf 4 122 503 -143 -503\n\t\tmu 0 4 128 129 150 149\n\t\tf 4 123 504 -144 -504\n\t\tmu 0 4 129 130 151 150\n\t\tf 4 124 505 -145 -505\n\t\tmu 0 4 130 131 152 151\n\t\tf 4 125 506 -146 -506\n\t\tmu 0 4 131 132 153 152\n\t\tf 4 126 507 -147 -507\n\t\tmu 0 4 132 133 154 153\n\t\tf 4 127 508 -148 -508\n\t\tmu 0 4 133 134 155 154\n\t\tf 4 128 509 -149 -509\n\t\tmu 0 4 134 135 156 155\n\t\tf 4 129 510 -150 -510\n\t\tmu 0 4 135 136 157 156\n\t\tf 4 130 511 -151 -511\n\t\tmu 0 4 136 137 158 157\n\t\tf 4 131 512 -152 -512\n\t\tmu 0 4 137 138 159 158\n\t\tf 4 132 513 -153 -513\n\t\tmu 0 4 138 139 160 159\n\t\tf 4 133 514 -154 -514\n\t\tmu 0 4 139 140 161 160\n\t\tf 4 134 515 -155 -515\n\t\tmu 0 4 140 141 162 161\n\t\tf 4 135 516 -156 -516\n\t\tmu 0 4 141 142 163 162\n\t\tf 4 136 517 -157 -517\n\t\tmu 0 4 142 143 164 163\n\t\tf 4 137 518 -158 -518\n\t\tmu 0 4 143 144 165 164\n\t\tf 4 138 519 -159 -519\n\t\tmu 0 4 144 145 166 165\n\t\tf 4 139 500 -160 -520\n\t\tmu 0 4 145 146 167 166\n\t\tf 4 140 521 -161 -521\n\t\tmu 0 4 147 148 169 168\n\t\tf 4 141 522 -162 -522\n\t\tmu 0 4 148 149 170 169\n\t\tf 4 142 523 -163 -523\n\t\tmu 0 4 149 150 171 170\n\t\tf 4 143 524 -164 -524\n\t\tmu 0 4 150 151 172 171\n\t\tf 4 144 525 -165 -525\n\t\tmu 0 4 151 152 173 172\n\t\tf 4 145 526 -166 -526\n\t\tmu 0 4 152 153 174 173\n\t\tf 4 146 527 -167 -527\n\t\tmu 0 4 153 154 175 174\n\t\tf 4 147 528 -168 -528\n\t\tmu 0 4 154 155 176 175\n\t\tf 4 148 529 -169 -529\n\t\tmu 0 4 155 156 177 176\n\t\tf 4 149 530 -170 -530\n\t\tmu 0 4 156 157 178 177\n\t\tf 4 150 531 -171 -531\n\t\tmu 0 4 157 158 179 178\n\t\tf 4 151 532 -172 -532\n\t\tmu 0 4 158 159 180 179\n\t\tf 4 152 533 -173 -533\n\t\tmu 0 4 159 160 181 180\n\t\tf 4 153 534 -174 -534\n\t\tmu 0 4 160 161 182 181\n\t\tf 4 154 535 -175 -535\n\t\tmu 0 4 161 162 183 182\n\t\tf 4 155 536 -176 -536\n\t\tmu 0 4 162 163 184 183\n\t\tf 4 156 537 -177 -537\n\t\tmu 0 4 163 164 185 184\n\t\tf 4 157 538 -178 -538\n\t\tmu 0 4 164 165 186 185\n\t\tf 4 158 539 -179 -539\n\t\tmu 0 4 165 166 187 186\n\t\tf 4 159 520 -180 -540\n\t\tmu 0 4 166 167 188 187\n\t\tf 4 160 541 -181 -541\n\t\tmu 0 4 168 169 190 189\n\t\tf 4 161 542 -182 -542\n\t\tmu 0 4 169 170 191 190\n\t\tf 4 162 543 -183 -543\n\t\tmu 0 4 170 171 192 191\n\t\tf 4 163 544 -184 -544\n\t\tmu 0 4 171 172 193 192\n\t\tf 4 164 545 -185 -545\n\t\tmu 0 4 172 173 194 193\n\t\tf 4 165 546 -186 -546\n\t\tmu 0 4 173 174 195 194\n\t\tf 4 166 547 -187 -547\n\t\tmu 0 4 174 175 196 195\n\t\tf 4 167 548 -188 -548\n\t\tmu 0 4 175 176 197 196\n\t\tf 4 168 549 -189 -549\n\t\tmu 0 4 176 177 198 197\n\t\tf 4 169 550 -190 -550\n\t\tmu 0 4 177 178 199 198\n\t\tf 4 170 551 -191 -551\n\t\tmu 0 4 178 179 200 199\n\t\tf 4 171 552 -192 -552\n\t\tmu 0 4 179 180 201 200\n\t\tf 4 172 553 -193 -553\n\t\tmu 0 4 180 181 202 201\n\t\tf 4 173 554 -194 -554\n\t\tmu 0 4 181 182 203 202\n\t\tf 4 174 555 -195 -555\n\t\tmu 0 4 182 183 204 203\n\t\tf 4 175 556 -196 -556\n\t\tmu 0 4 183 184 205 204\n\t\tf 4 176 557 -197 -557\n\t\tmu 0 4 184 185 206 205\n\t\tf 4 177 558 -198 -558\n\t\tmu 0 4 185 186 207 206\n\t\tf 4 178 559 -199 -559\n\t\tmu 0 4 186 187 208 207\n\t\tf 4 179 540 -200 -560\n\t\tmu 0 4 187 188 209 208\n\t\tf 4 180 561 -201 -561\n\t\tmu 0 4 189 190 211 210\n\t\tf 4 181 562 -202 -562\n\t\tmu 0 4 190 191 212 211\n\t\tf 4 182 563 -203 -563\n\t\tmu 0 4 191 192 213 212\n\t\tf 4 183 564 -204 -564\n\t\tmu 0 4 192 193 214 213\n\t\tf 4 184 565 -205 -565\n\t\tmu 0 4 193 194 215 214\n\t\tf 4 185 566 -206 -566\n\t\tmu 0 4 194 195 216 215\n\t\tf 4 186 567 -207 -567\n\t\tmu 0 4 195 196 217 216\n\t\tf 4 187 568 -208 -568\n\t\tmu 0 4 196 197 218 217\n\t\tf 4 188 569 -209 -569\n\t\tmu 0 4 197 198 219 218\n\t\tf 4 189 570 -210 -570\n\t\tmu 0 4 198 199 220 219\n\t\tf 4 190 571 -211 -571\n\t\tmu 0 4 199 200 221 220\n\t\tf 4 191 572 -212 -572\n\t\tmu 0 4 200 201 222 221\n\t\tf 4 192 573 -213 -573\n\t\tmu 0 4 201 202 223 222\n\t\tf 4 193 574 -214 -574\n\t\tmu 0 4 202 203 224 223\n\t\tf 4 194 575 -215 -575\n\t\tmu 0 4 203 204 225 224\n\t\tf 4 195 576 -216 -576\n\t\tmu 0 4 204 205 226 225\n\t\tf 4 196 577 -217 -577\n\t\tmu 0 4 205 206 227 226\n\t\tf 4 197 578 -218 -578\n\t\tmu 0 4 206 207 228 227\n\t\tf 4 198 579 -219 -579\n\t\tmu 0 4 207 208 229 228\n\t\tf 4 199 560 -220 -580\n\t\tmu 0 4 208 209 230 229\n\t\tf 4 200 581 -221 -581\n\t\tmu 0 4 210 211 232 231\n\t\tf 4 201 582 -222 -582\n\t\tmu 0 4 211 212 233 232\n\t\tf 4 202 583 -223 -583\n\t\tmu 0 4 212 213 234 233\n\t\tf 4 203 584 -224 -584\n\t\tmu 0 4 213 214 235 234\n\t\tf 4 204 585 -225 -585\n\t\tmu 0 4 214 215 236 235\n\t\tf 4 205 586 -226 -586\n\t\tmu 0 4 215 216 237 236\n\t\tf 4 206 587 -227 -587\n\t\tmu 0 4 216 217 238 237\n\t\tf 4 207 588 -228 -588\n\t\tmu 0 4 217 218 239 238\n\t\tf 4 208 589 -229 -589\n\t\tmu 0 4 218 219 240 239\n\t\tf 4 209 590 -230 -590\n\t\tmu 0 4 219 220 241 240\n\t\tf 4 210 591 -231 -591\n\t\tmu 0 4 220 221 242 241\n\t\tf 4 211 592 -232 -592\n\t\tmu 0 4 221 222 243 242\n\t\tf 4 212 593 -233 -593\n\t\tmu 0 4 222 223 244 243\n\t\tf 4 213 594 -234 -594\n\t\tmu 0 4 223 224 245 244\n\t\tf 4 214 595 -235 -595\n\t\tmu 0 4 224 225 246 245\n\t\tf 4 215 596 -236 -596\n\t\tmu 0 4 225 226 247 246\n\t\tf 4 216 597 -237 -597\n\t\tmu 0 4 226 227 248 247\n\t\tf 4 217 598 -238 -598\n\t\tmu 0 4 227 228 249 248\n\t\tf 4 218 599 -239 -599\n\t\tmu 0 4 228 229 250 249\n\t\tf 4 219 580 -240 -600\n\t\tmu 0 4 229 230 251 250\n\t\tf 4 220 601 -241 -601\n\t\tmu 0 4 231 232 253 252\n\t\tf 4 221 602 -242 -602\n\t\tmu 0 4 232 233 254 253\n\t\tf 4 222 603 -243 -603\n\t\tmu 0 4 233 234 255 254\n\t\tf 4 223 604 -244 -604\n\t\tmu 0 4 234 235 256 255\n\t\tf 4 224 605 -245 -605\n\t\tmu 0 4 235 236 257 256\n\t\tf 4 225 606 -246 -606\n\t\tmu 0 4 236 237 258 257\n\t\tf 4 226 607 -247 -607\n\t\tmu 0 4 237 238 259 258\n\t\tf 4 227 608 -248 -608\n\t\tmu 0 4 238 239 260 259\n\t\tf 4 228 609 -249 -609\n\t\tmu 0 4 239 240 261 260\n\t\tf 4 229 610 -250 -610\n\t\tmu 0 4 240 241 262 261\n\t\tf 4 230 611 -251 -611\n\t\tmu 0 4 241 242 263 262\n\t\tf 4 231 612 -252 -612\n\t\tmu 0 4 242 243 264 263\n\t\tf 4 232 613 -253 -613\n\t\tmu 0 4 243 244 265 264\n\t\tf 4 233 614 -254 -614\n\t\tmu 0 4 244 245 266 265\n\t\tf 4 234 615 -255 -615\n\t\tmu 0 4 245 246 267 266\n\t\tf 4 235 616 -256 -616\n\t\tmu 0 4 246 247 268 267\n\t\tf 4 236 617 -257 -617\n\t\tmu 0 4 247 248 269 268\n\t\tf 4 237 618 -258 -618\n\t\tmu 0 4 248 249 270 269\n\t\tf 4 238 619 -259 -619\n\t\tmu 0 4 249 250 271 270\n\t\tf 4 239 600 -260 -620\n\t\tmu 0 4 250 251 272 271\n\t\tf 4 240 621 -261 -621\n\t\tmu 0 4 252 253 274 273\n\t\tf 4 241 622 -262 -622\n\t\tmu 0 4 253 254 275 274\n\t\tf 4 242 623 -263 -623\n\t\tmu 0 4 254 255 276 275\n\t\tf 4 243 624 -264 -624\n\t\tmu 0 4 255 256 277 276\n\t\tf 4 244 625 -265 -625\n\t\tmu 0 4 256 257 278 277\n\t\tf 4 245 626 -266 -626\n\t\tmu 0 4 257 258 279 278\n\t\tf 4 246 627 -267 -627\n\t\tmu 0 4 258 259 280 279\n\t\tf 4 247 628 -268 -628\n\t\tmu 0 4 259 260 281 280\n\t\tf 4 248 629 -269 -629\n\t\tmu 0 4 260 261 282 281\n\t\tf 4 249 630 -270 -630\n\t\tmu 0 4 261 262 283 282\n\t\tf 4 250 631 -271 -631\n\t\tmu 0 4 262 263 284 283\n\t\tf 4 251 632 -272 -632\n\t\tmu 0 4 263 264 285 284\n\t\tf 4 252 633 -273 -633\n\t\tmu 0 4 264 265 286 285\n\t\tf 4 253 634 -274 -634\n\t\tmu 0 4 265 266 287 286\n\t\tf 4 254 635 -275 -635\n\t\tmu 0 4 266 267 288 287\n\t\tf 4 255 636 -276 -636\n\t\tmu 0 4 267 268 289 288\n\t\tf 4 256 637 -277 -637\n\t\tmu 0 4 268 269 290 289\n\t\tf 4 257 638 -278 -638\n\t\tmu 0 4 269 270 291 290\n\t\tf 4 258 639 -279 -639\n\t\tmu 0 4 270 271 292 291\n\t\tf 4 259 620 -280 -640\n\t\tmu 0 4 271 272 293 292\n\t\tf 4 260 641 -281 -641\n\t\tmu 0 4 273 274 295 294\n\t\tf 4 261 642 -282 -642\n\t\tmu 0 4 274 275 296 295\n\t\tf 4 262 643 -283 -643\n\t\tmu 0 4 275 276 297 296\n\t\tf 4 263 644 -284 -644\n\t\tmu 0 4 276 277 298 297\n\t\tf 4 264 645 -285 -645\n\t\tmu 0 4 277 278 299 298\n\t\tf 4 265 646 -286 -646\n\t\tmu 0 4 278 279 300 299\n\t\tf 4 266 647 -287 -647\n\t\tmu 0 4 279 280 301 300\n\t\tf 4 267 648 -288 -648\n\t\tmu 0 4 280 281 302 301\n\t\tf 4 268 649 -289 -649\n\t\tmu 0 4 281 282 303 302\n\t\tf 4 269 650 -290 -650\n\t\tmu 0 4 282 283 304 303\n\t\tf 4 270 651 -291 -651\n\t\tmu 0 4 283 284 305 304\n\t\tf 4 271 652 -292 -652\n\t\tmu 0 4 284 285 306 305\n\t\tf 4 272 653 -293 -653\n\t\tmu 0 4 285 286 307 306\n\t\tf 4 273 654 -294 -654\n\t\tmu 0 4 286 287 308 307\n\t\tf 4 274 655 -295 -655\n\t\tmu 0 4 287 288 309 308\n\t\tf 4 275 656 -296 -656\n\t\tmu 0 4 288 289 310 309\n\t\tf 4 276 657 -297 -657\n\t\tmu 0 4 289 290 311 310\n\t\tf 4 277 658 -298 -658\n\t\tmu 0 4 290 291 312 311\n\t\tf 4 278 659 -299 -659\n\t\tmu 0 4 291 292 313 312\n\t\tf 4 279 640 -300 -660\n\t\tmu 0 4 292 293 314 313\n\t\tf 4 280 661 -301 -661\n\t\tmu 0 4 294 295 316 315\n\t\tf 4 281 662 -302 -662\n\t\tmu 0 4 295 296 317 316\n\t\tf 4 282 663 -303 -663\n\t\tmu 0 4 296 297 318 317\n\t\tf 4 283 664 -304 -664\n\t\tmu 0 4 297 298 319 318\n\t\tf 4 284 665 -305 -665\n\t\tmu 0 4 298 299 320 319\n\t\tf 4 285 666 -306 -666\n\t\tmu 0 4 299 300 321 320\n\t\tf 4 286 667 -307 -667\n\t\tmu 0 4 300 301 322 321\n\t\tf 4 287 668 -308 -668\n\t\tmu 0 4 301 302 323 322\n\t\tf 4 288 669 -309 -669\n\t\tmu 0 4 302 303 324 323\n\t\tf 4 289 670 -310 -670\n\t\tmu 0 4 303 304 325 324\n\t\tf 4 290 671 -311 -671\n\t\tmu 0 4 304 305 326 325\n\t\tf 4 291 672 -312 -672\n\t\tmu 0 4 305 306 327 326\n\t\tf 4 292 673 -313 -673\n\t\tmu 0 4 306 307 328 327\n\t\tf 4 293 674 -314 -674\n\t\tmu 0 4 307 308 329 328\n\t\tf 4 294 675 -315 -675\n\t\tmu 0 4 308 309 330 329\n\t\tf 4 295 676 -316 -676\n\t\tmu 0 4 309 310 331 330\n\t\tf 4 296 677 -317 -677\n\t\tmu 0 4 310 311 332 331\n\t\tf 4 297 678 -318 -678\n\t\tmu 0 4 311 312 333 332\n\t\tf 4 298 679 -319 -679\n\t\tmu 0 4 312 313 334 333\n\t\tf 4 299 660 -320 -680\n\t\tmu 0 4 313 314 335 334\n\t\tf 4 300 681 -321 -681\n\t\tmu 0 4 315 316 337 336\n\t\tf 4 301 682 -322 -682\n\t\tmu 0 4 316 317 338 337\n\t\tf 4 302 683 -323 -683\n\t\tmu 0 4 317 318 339 338\n\t\tf 4 303 684 -324 -684\n\t\tmu 0 4 318 319 340 339\n\t\tf 4 304 685 -325 -685\n\t\tmu 0 4 319 320 341 340\n\t\tf 4 305 686 -326 -686\n\t\tmu 0 4 320 321 342 341\n\t\tf 4 306 687 -327 -687\n\t\tmu 0 4 321 322 343 342\n\t\tf 4 307 688 -328 -688\n\t\tmu 0 4 322 323 344 343\n\t\tf 4 308 689 -329 -689\n\t\tmu 0 4 323 324 345 344\n\t\tf 4 309 690 -330 -690\n\t\tmu 0 4 324 325 346 345\n\t\tf 4 310 691 -331 -691\n\t\tmu 0 4 325 326 347 346\n\t\tf 4 311 692 -332 -692\n\t\tmu 0 4 326 327 348 347\n\t\tf 4 312 693 -333 -693\n\t\tmu 0 4 327 328 349 348\n\t\tf 4 313 694 -334 -694\n\t\tmu 0 4 328 329 350 349\n\t\tf 4 314 695 -335 -695\n\t\tmu 0 4 329 330 351 350\n\t\tf 4 315 696 -336 -696\n\t\tmu 0 4 330 331 352 351\n\t\tf 4 316 697 -337 -697\n\t\tmu 0 4 331 332 353 352\n\t\tf 4 317 698 -338 -698\n\t\tmu 0 4 332 333 354 353\n\t\tf 4 318 699 -339 -699\n\t\tmu 0 4 333 334 355 354\n\t\tf 4 319 680 -340 -700\n\t\tmu 0 4 334 335 356 355\n\t\tf 4 320 701 -341 -701\n\t\tmu 0 4 336 337 358 357\n\t\tf 4 321 702 -342 -702\n\t\tmu 0 4 337 338 359 358\n\t\tf 4 322 703 -343 -703\n\t\tmu 0 4 338 339 360 359\n\t\tf 4 323 704 -344 -704\n\t\tmu 0 4 339 340 361 360\n\t\tf 4 324 705 -345 -705\n\t\tmu 0 4 340 341 362 361\n\t\tf 4 325 706 -346 -706\n\t\tmu 0 4 341 342 363 362\n\t\tf 4 326 707 -347 -707\n\t\tmu 0 4 342 343 364 363\n\t\tf 4 327 708 -348 -708\n\t\tmu 0 4 343 344 365 364\n\t\tf 4 328 709 -349 -709\n\t\tmu 0 4 344 345 366 365\n\t\tf 4 329 710 -350 -710\n\t\tmu 0 4 345 346 367 366\n\t\tf 4 330 711 -351 -711\n\t\tmu 0 4 346 347 368 367\n\t\tf 4 331 712 -352 -712\n\t\tmu 0 4 347 348 369 368\n\t\tf 4 332 713 -353 -713\n\t\tmu 0 4 348 349 370 369\n\t\tf 4 333 714 -354 -714\n\t\tmu 0 4 349 350 371 370\n\t\tf 4 334 715 -355 -715\n\t\tmu 0 4 350 351 372 371\n\t\tf 4 335 716 -356 -716\n\t\tmu 0 4 351 352 373 372\n\t\tf 4 336 717 -357 -717\n\t\tmu 0 4 352 353 374 373\n\t\tf 4 337 718 -358 -718\n\t\tmu 0 4 353 354 375 374\n\t\tf 4 338 719 -359 -719\n\t\tmu 0 4 354 355 376 375\n\t\tf 4 339 700 -360 -720\n\t\tmu 0 4 355 356 377 376\n\t\tf 4 340 721 -361 -721\n\t\tmu 0 4 357 358 379 378\n\t\tf 4 341 722 -362 -722\n\t\tmu 0 4 358 359 380 379\n\t\tf 4 342 723 -363 -723\n\t\tmu 0 4 359 360 381 380\n\t\tf 4 343 724 -364 -724\n\t\tmu 0 4 360 361 382 381\n\t\tf 4 344 725 -365 -725\n\t\tmu 0 4 361 362 383 382\n\t\tf 4 345 726 -366 -726\n\t\tmu 0 4 362 363 384 383\n\t\tf 4 346 727 -367 -727\n\t\tmu 0 4 363 364 385 384\n\t\tf 4 347 728 -368 -728\n\t\tmu 0 4 364 365 386 385\n\t\tf 4 348 729 -369 -729\n\t\tmu 0 4 365 366 387 386\n\t\tf 4 349 730 -370 -730\n\t\tmu 0 4 366 367 388 387\n\t\tf 4 350 731 -371 -731\n\t\tmu 0 4 367 368 389 388\n\t\tf 4 351 732 -372 -732\n\t\tmu 0 4 368 369 390 389\n\t\tf 4 352 733 -373 -733\n\t\tmu 0 4 369 370 391 390\n\t\tf 4 353 734 -374 -734\n\t\tmu 0 4 370 371 392 391\n\t\tf 4 354 735 -375 -735\n\t\tmu 0 4 371 372 393 392\n\t\tf 4 355 736 -376 -736\n\t\tmu 0 4 372 373 394 393\n\t\tf 4 356 737 -377 -737\n\t\tmu 0 4 373 374 395 394\n\t\tf 4 357 738 -378 -738\n\t\tmu 0 4 374 375 396 395\n\t\tf 4 358 739 -379 -739\n\t\tmu 0 4 375 376 397 396\n\t\tf 4 359 720 -380 -740\n\t\tmu 0 4 376 377 398 397\n\t\tf 3 -1 -741 741\n\t\tmu 0 3 1 0 399\n\t\tf 3 -2 -742 742\n\t\tmu 0 3 2 1 400\n\t\tf 3 -3 -743 743\n\t\tmu 0 3 3 2 401\n\t\tf 3 -4 -744 744\n\t\tmu 0 3 4 3 402\n\t\tf 3 -5 -745 745\n\t\tmu 0 3 5 4 403\n\t\tf 3 -6 -746 746\n\t\tmu 0 3 6 5 404\n\t\tf 3 -7 -747 747\n\t\tmu 0 3 7 6 405\n\t\tf 3 -8 -748 748\n\t\tmu 0 3 8 7 406\n\t\tf 3 -9 -749 749\n\t\tmu 0 3 9 8 407\n\t\tf 3 -10 -750 750\n\t\tmu 0 3 10 9 408\n\t\tf 3 -11 -751 751\n\t\tmu 0 3 11 10 409\n\t\tf 3 -12 -752 752\n\t\tmu 0 3 12 11 410\n\t\tf 3 -13 -753 753\n\t\tmu 0 3 13 12 411\n\t\tf 3 -14 -754 754\n\t\tmu 0 3 14 13 412\n\t\tf 3 -15 -755 755\n\t\tmu 0 3 15 14 413\n\t\tf 3 -16 -756 756\n\t\tmu 0 3 16 15 414\n\t\tf 3 -17 -757 757\n\t\tmu 0 3 17 16 415\n\t\tf 3 -18 -758 758\n\t\tmu 0 3 18 17 416\n\t\tf 3 -19 -759 759\n\t\tmu 0 3 19 18 417\n\t\tf 3 -20 -760 740\n\t\tmu 0 3 20 19 418\n\t\tf 3 360 761 -761\n\t\tmu 0 3 378 379 419\n\t\tf 3 361 762 -762\n\t\tmu 0 3 379 380 420\n\t\tf 3 362 763 -763\n\t\tmu 0 3 380 381 421\n\t\tf 3 363 764 -764\n\t\tmu 0 3 381 382 422\n\t\tf 3 364 765 -765\n\t\tmu 0 3 382 383 423\n\t\tf 3 365 766 -766\n\t\tmu 0 3 383 384 424\n\t\tf 3 366 767 -767\n\t\tmu 0 3 384 385 425\n\t\tf 3 367 768 -768\n\t\tmu 0 3 385 386 426\n\t\tf 3 368 769 -769\n\t\tmu 0 3 386 387 427\n\t\tf 3 369 770 -770\n\t\tmu 0 3 387 388 428\n\t\tf 3 370 771 -771\n\t\tmu 0 3 388 389 429\n\t\tf 3 371 772 -772\n\t\tmu 0 3 389 390 430\n\t\tf 3 372 773 -773\n\t\tmu 0 3 390 391 431\n\t\tf 3 373 774 -774\n\t\tmu 0 3 391 392 432\n\t\tf 3 374 775 -775\n\t\tmu 0 3 392 393 433\n\t\tf 3 375 776 -776\n\t\tmu 0 3 393 394 434\n\t\tf 3 376 777 -777\n\t\tmu 0 3 394 395 435\n\t\tf 3 377 778 -778\n\t\tmu 0 3 395 396 436\n\t\tf 3 378 779 -779\n\t\tmu 0 3 396 397 437\n\t\tf 3 379 760 -780\n\t\tmu 0 3 397 398 438;\n\tsetAttr \".cd\" -type \"dataPolyComponent\" Index_Data Edge 0 ;\n\tsetAttr \".cvd\" -type \"dataPolyComponent\" Index_Data Vertex 0 ;\n\tsetAttr \".pd[0]\" -type \"dataPolyComponent\" Index_Data UV 0 ;\n\tsetAttr \".hfd\" -type \"dataPolyComponent\" Index_Data Face 0 ;\n\tsetAttr \".dr\" 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:bf8e49bb98ec\";\nselect -ne :time1;\n\tsetAttr \".o\" 1001;\n\tsetAttr \".unw\" 1001;\nselect -ne :hardwareRenderingGlobals;\n\tsetAttr \".otfna\" -type \"stringArray\" 22 \"NURBS Curves\" \"NURBS Surfaces\" \"Polygons\" \"Subdiv Surface\" \"Particles\" \"Particle Instance\" \"Fluids\" \"Strokes\" \"Image Planes\" \"UI\" \"Lights\" \"Cameras\" \"Locators\" \"Joints\" \"IK Handles\" \"Deformers\" \"Motion Trails\" \"Components\" \"Hair Systems\" \"Follicles\" \"Misc. UI\" \"Ornaments\"  ;\n\tsetAttr \".otfva\" -type \"Int32Array\" 22 0 1 1 1 1 1\n\t\t 1 1 1 0 0 0 0 0 0 0 0 0\n\t\t 0 0 0 0 ;\n\tsetAttr \".fprt\" yes;\nselect -ne :renderPartition;\n\tsetAttr -s 2 \".st\";\nselect -ne :renderGlobalsList1;\nselect -ne :defaultShaderList1;\n\tsetAttr -s 4 \".s\";\nselect -ne :postProcessList1;\n\tsetAttr -s 2 \".p\";\nselect -ne :defaultRenderingList1;\nselect -ne :initialShadingGroup;\n\tsetAttr \".ro\" yes;\nselect -ne :initialParticleSE;\n\tsetAttr \".ro\" yes;\nselect -ne :defaultRenderGlobals;\n\tsetAttr \".ren\" -type \"string\" \"arnold\";\n\tsetAttr \".fs\" 1001;\n\tsetAttr \".ef\" 1001;\nselect -ne :defaultResolution;\n\tsetAttr \".w\" 1920;\n\tsetAttr \".h\" 1080;\n\tsetAttr \".pa\" 1;\nselect -ne :hardwareRenderGlobals;\n\tsetAttr \".ctrs\" 256;\n\tsetAttr \".btrs\" 512;\nconnectAttr \"pSphere1_GEOShape1.iog\" \":initialShadingGroup.dsm\" -na;\n// End of modelMain.ma\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/v001/test_project_test_asset_modelMain_v001.ma",
    "content": "//Maya ASCII 2019 scene\n//Name: modelMain.ma\n//Last modified: Thu, Sep 02, 2021 03:24:19 PM\n//Codeset: 1252\nrequires maya \"2019\";\nrequires \"mtoa\" \"4.0.4.2\";\nrequires \"mtoa\" \"4.0.4.2\";\ncurrentUnit -l centimeter -a degree -t pal;\nfileInfo \"application\" \"maya\";\nfileInfo \"product\" \"Maya 2019\";\nfileInfo \"version\" \"2019\";\nfileInfo \"cutIdentifier\" \"201812112215-434d8d9c04\";\nfileInfo \"osv\" \"Microsoft Windows 10 Technical Preview  (Build 19041)\\n\";\nfileInfo \"license\" \"education\";\ncreateNode transform -n \"pSphere1_GEO\";\n\trename -uid \"7445A43F-444F-B2D3-4315-2AA013D2E0B6\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:440654b3dfe4\";\ncreateNode mesh -n \"pSphere1_GEOShape1\" -p \"pSphere1_GEO\";\n\trename -uid \"7C731260-45C6-339E-07BF-359446B08EA1\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr -s 439 \".uvst[0].uvsp\";\n\tsetAttr \".uvst[0].uvsp[0:249]\" -type \"float2\" 0 0.050000001 0.050000001 0.050000001\n\t\t 0.1 0.050000001 0.15000001 0.050000001 0.2 0.050000001 0.25 0.050000001 0.30000001\n\t\t 0.050000001 0.35000002 0.050000001 0.40000004 0.050000001 0.45000005 0.050000001\n\t\t 0.50000006 0.050000001 0.55000007 0.050000001 0.60000008 0.050000001 0.6500001 0.050000001\n\t\t 0.70000011 0.050000001 0.75000012 0.050000001 0.80000013 0.050000001 0.85000014 0.050000001\n\t\t 0.90000015 0.050000001 0.95000017 0.050000001 1.000000119209 0.050000001 0 0.1 0.050000001\n\t\t 0.1 0.1 0.1 0.15000001 0.1 0.2 0.1 0.25 0.1 0.30000001 0.1 0.35000002 0.1 0.40000004\n\t\t 0.1 0.45000005 0.1 0.50000006 0.1 0.55000007 0.1 0.60000008 0.1 0.6500001 0.1 0.70000011\n\t\t 0.1 0.75000012 0.1 0.80000013 0.1 0.85000014 0.1 0.90000015 0.1 0.95000017 0.1 1.000000119209\n\t\t 0.1 0 0.15000001 0.050000001 0.15000001 0.1 0.15000001 0.15000001 0.15000001 0.2\n\t\t 0.15000001 0.25 0.15000001 0.30000001 0.15000001 0.35000002 0.15000001 0.40000004\n\t\t 0.15000001 0.45000005 0.15000001 0.50000006 0.15000001 0.55000007 0.15000001 0.60000008\n\t\t 0.15000001 0.6500001 0.15000001 0.70000011 0.15000001 0.75000012 0.15000001 0.80000013\n\t\t 0.15000001 0.85000014 0.15000001 0.90000015 0.15000001 0.95000017 0.15000001 1.000000119209\n\t\t 0.15000001 0 0.2 0.050000001 0.2 0.1 0.2 0.15000001 0.2 0.2 0.2 0.25 0.2 0.30000001\n\t\t 0.2 0.35000002 0.2 0.40000004 0.2 0.45000005 0.2 0.50000006 0.2 0.55000007 0.2 0.60000008\n\t\t 0.2 0.6500001 0.2 0.70000011 0.2 0.75000012 0.2 0.80000013 0.2 0.85000014 0.2 0.90000015\n\t\t 0.2 0.95000017 0.2 1.000000119209 0.2 0 0.25 0.050000001 0.25 0.1 0.25 0.15000001\n\t\t 0.25 0.2 0.25 0.25 0.25 0.30000001 0.25 0.35000002 0.25 0.40000004 0.25 0.45000005\n\t\t 0.25 0.50000006 0.25 0.55000007 0.25 0.60000008 0.25 0.6500001 0.25 0.70000011 0.25\n\t\t 0.75000012 0.25 0.80000013 0.25 0.85000014 0.25 0.90000015 0.25 0.95000017 0.25 1.000000119209\n\t\t 0.25 0 0.30000001 0.050000001 0.30000001 0.1 0.30000001 0.15000001 0.30000001 0.2\n\t\t 0.30000001 0.25 0.30000001 0.30000001 0.30000001 0.35000002 0.30000001 0.40000004\n\t\t 0.30000001 0.45000005 0.30000001 0.50000006 0.30000001 0.55000007 0.30000001 0.60000008\n\t\t 0.30000001 0.6500001 0.30000001 0.70000011 0.30000001 0.75000012 0.30000001 0.80000013\n\t\t 0.30000001 0.85000014 0.30000001 0.90000015 0.30000001 0.95000017 0.30000001 1.000000119209\n\t\t 0.30000001 0 0.35000002 0.050000001 0.35000002 0.1 0.35000002 0.15000001 0.35000002\n\t\t 0.2 0.35000002 0.25 0.35000002 0.30000001 0.35000002 0.35000002 0.35000002 0.40000004\n\t\t 0.35000002 0.45000005 0.35000002 0.50000006 0.35000002 0.55000007 0.35000002 0.60000008\n\t\t 0.35000002 0.6500001 0.35000002 0.70000011 0.35000002 0.75000012 0.35000002 0.80000013\n\t\t 0.35000002 0.85000014 0.35000002 0.90000015 0.35000002 0.95000017 0.35000002 1.000000119209\n\t\t 0.35000002 0 0.40000004 0.050000001 0.40000004 0.1 0.40000004 0.15000001 0.40000004\n\t\t 0.2 0.40000004 0.25 0.40000004 0.30000001 0.40000004 0.35000002 0.40000004 0.40000004\n\t\t 0.40000004 0.45000005 0.40000004 0.50000006 0.40000004 0.55000007 0.40000004 0.60000008\n\t\t 0.40000004 0.6500001 0.40000004 0.70000011 0.40000004 0.75000012 0.40000004 0.80000013\n\t\t 0.40000004 0.85000014 0.40000004 0.90000015 0.40000004 0.95000017 0.40000004 1.000000119209\n\t\t 0.40000004 0 0.45000005 0.050000001 0.45000005 0.1 0.45000005 0.15000001 0.45000005\n\t\t 0.2 0.45000005 0.25 0.45000005 0.30000001 0.45000005 0.35000002 0.45000005 0.40000004\n\t\t 0.45000005 0.45000005 0.45000005 0.50000006 0.45000005 0.55000007 0.45000005 0.60000008\n\t\t 0.45000005 0.6500001 0.45000005 0.70000011 0.45000005 0.75000012 0.45000005 0.80000013\n\t\t 0.45000005 0.85000014 0.45000005 0.90000015 0.45000005 0.95000017 0.45000005 1.000000119209\n\t\t 0.45000005 0 0.50000006 0.050000001 0.50000006 0.1 0.50000006 0.15000001 0.50000006\n\t\t 0.2 0.50000006 0.25 0.50000006 0.30000001 0.50000006 0.35000002 0.50000006 0.40000004\n\t\t 0.50000006 0.45000005 0.50000006 0.50000006 0.50000006 0.55000007 0.50000006 0.60000008\n\t\t 0.50000006 0.6500001 0.50000006 0.70000011 0.50000006 0.75000012 0.50000006 0.80000013\n\t\t 0.50000006 0.85000014 0.50000006 0.90000015 0.50000006 0.95000017 0.50000006 1.000000119209\n\t\t 0.50000006 0 0.55000007 0.050000001 0.55000007 0.1 0.55000007 0.15000001 0.55000007\n\t\t 0.2 0.55000007 0.25 0.55000007 0.30000001 0.55000007 0.35000002 0.55000007 0.40000004\n\t\t 0.55000007 0.45000005 0.55000007 0.50000006 0.55000007 0.55000007 0.55000007 0.60000008\n\t\t 0.55000007 0.6500001 0.55000007 0.70000011 0.55000007 0.75000012 0.55000007 0.80000013\n\t\t 0.55000007 0.85000014 0.55000007 0.90000015 0.55000007 0.95000017 0.55000007 1.000000119209\n\t\t 0.55000007 0 0.60000008 0.050000001 0.60000008 0.1 0.60000008 0.15000001 0.60000008\n\t\t 0.2 0.60000008 0.25 0.60000008 0.30000001 0.60000008 0.35000002 0.60000008 0.40000004\n\t\t 0.60000008 0.45000005 0.60000008 0.50000006 0.60000008 0.55000007 0.60000008 0.60000008\n\t\t 0.60000008 0.6500001 0.60000008 0.70000011 0.60000008 0.75000012 0.60000008 0.80000013\n\t\t 0.60000008 0.85000014 0.60000008 0.90000015 0.60000008;\n\tsetAttr \".uvst[0].uvsp[250:438]\" 0.95000017 0.60000008 1.000000119209 0.60000008\n\t\t 0 0.6500001 0.050000001 0.6500001 0.1 0.6500001 0.15000001 0.6500001 0.2 0.6500001\n\t\t 0.25 0.6500001 0.30000001 0.6500001 0.35000002 0.6500001 0.40000004 0.6500001 0.45000005\n\t\t 0.6500001 0.50000006 0.6500001 0.55000007 0.6500001 0.60000008 0.6500001 0.6500001\n\t\t 0.6500001 0.70000011 0.6500001 0.75000012 0.6500001 0.80000013 0.6500001 0.85000014\n\t\t 0.6500001 0.90000015 0.6500001 0.95000017 0.6500001 1.000000119209 0.6500001 0 0.70000011\n\t\t 0.050000001 0.70000011 0.1 0.70000011 0.15000001 0.70000011 0.2 0.70000011 0.25 0.70000011\n\t\t 0.30000001 0.70000011 0.35000002 0.70000011 0.40000004 0.70000011 0.45000005 0.70000011\n\t\t 0.50000006 0.70000011 0.55000007 0.70000011 0.60000008 0.70000011 0.6500001 0.70000011\n\t\t 0.70000011 0.70000011 0.75000012 0.70000011 0.80000013 0.70000011 0.85000014 0.70000011\n\t\t 0.90000015 0.70000011 0.95000017 0.70000011 1.000000119209 0.70000011 0 0.75000012\n\t\t 0.050000001 0.75000012 0.1 0.75000012 0.15000001 0.75000012 0.2 0.75000012 0.25 0.75000012\n\t\t 0.30000001 0.75000012 0.35000002 0.75000012 0.40000004 0.75000012 0.45000005 0.75000012\n\t\t 0.50000006 0.75000012 0.55000007 0.75000012 0.60000008 0.75000012 0.6500001 0.75000012\n\t\t 0.70000011 0.75000012 0.75000012 0.75000012 0.80000013 0.75000012 0.85000014 0.75000012\n\t\t 0.90000015 0.75000012 0.95000017 0.75000012 1.000000119209 0.75000012 0 0.80000013\n\t\t 0.050000001 0.80000013 0.1 0.80000013 0.15000001 0.80000013 0.2 0.80000013 0.25 0.80000013\n\t\t 0.30000001 0.80000013 0.35000002 0.80000013 0.40000004 0.80000013 0.45000005 0.80000013\n\t\t 0.50000006 0.80000013 0.55000007 0.80000013 0.60000008 0.80000013 0.6500001 0.80000013\n\t\t 0.70000011 0.80000013 0.75000012 0.80000013 0.80000013 0.80000013 0.85000014 0.80000013\n\t\t 0.90000015 0.80000013 0.95000017 0.80000013 1.000000119209 0.80000013 0 0.85000014\n\t\t 0.050000001 0.85000014 0.1 0.85000014 0.15000001 0.85000014 0.2 0.85000014 0.25 0.85000014\n\t\t 0.30000001 0.85000014 0.35000002 0.85000014 0.40000004 0.85000014 0.45000005 0.85000014\n\t\t 0.50000006 0.85000014 0.55000007 0.85000014 0.60000008 0.85000014 0.6500001 0.85000014\n\t\t 0.70000011 0.85000014 0.75000012 0.85000014 0.80000013 0.85000014 0.85000014 0.85000014\n\t\t 0.90000015 0.85000014 0.95000017 0.85000014 1.000000119209 0.85000014 0 0.90000015\n\t\t 0.050000001 0.90000015 0.1 0.90000015 0.15000001 0.90000015 0.2 0.90000015 0.25 0.90000015\n\t\t 0.30000001 0.90000015 0.35000002 0.90000015 0.40000004 0.90000015 0.45000005 0.90000015\n\t\t 0.50000006 0.90000015 0.55000007 0.90000015 0.60000008 0.90000015 0.6500001 0.90000015\n\t\t 0.70000011 0.90000015 0.75000012 0.90000015 0.80000013 0.90000015 0.85000014 0.90000015\n\t\t 0.90000015 0.90000015 0.95000017 0.90000015 1.000000119209 0.90000015 0 0.95000017\n\t\t 0.050000001 0.95000017 0.1 0.95000017 0.15000001 0.95000017 0.2 0.95000017 0.25 0.95000017\n\t\t 0.30000001 0.95000017 0.35000002 0.95000017 0.40000004 0.95000017 0.45000005 0.95000017\n\t\t 0.50000006 0.95000017 0.55000007 0.95000017 0.60000008 0.95000017 0.6500001 0.95000017\n\t\t 0.70000011 0.95000017 0.75000012 0.95000017 0.80000013 0.95000017 0.85000014 0.95000017\n\t\t 0.90000015 0.95000017 0.95000017 0.95000017 1.000000119209 0.95000017 0.025 0 0.075000003\n\t\t 0 0.125 0 0.17500001 0 0.22500001 0 0.27500001 0 0.32500002 0 0.375 0 0.42500001\n\t\t 0 0.47500002 0 0.52499998 0 0.57499999 0 0.625 0 0.67500001 0 0.72499996 0 0.77499998\n\t\t 0 0.82499999 0 0.875 0 0.92500001 0 0.97499996 0 0.025 1 0.075000003 1 0.125 1 0.17500001\n\t\t 1 0.22500001 1 0.27500001 1 0.32500002 1 0.375 1 0.42500001 1 0.47500002 1 0.52499998\n\t\t 1 0.57499999 1 0.625 1 0.67500001 1 0.72499996 1 0.77499998 1 0.82499999 1 0.875\n\t\t 1 0.92500001 1 0.97499996 1;\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr -s 382 \".vt\";\n\tsetAttr \".vt[0:165]\"  0.14877813 -0.98768836 -0.048340943 0.12655823 -0.98768836 -0.091949932\n\t\t 0.091949932 -0.98768836 -0.12655823 0.048340935 -0.98768836 -0.14877811 0 -0.98768836 -0.15643455\n\t\t -0.048340935 -0.98768836 -0.1487781 -0.091949917 -0.98768836 -0.1265582 -0.12655818 -0.98768836 -0.091949902\n\t\t -0.14877807 -0.98768836 -0.048340924 -0.15643452 -0.98768836 0 -0.14877807 -0.98768836 0.048340924\n\t\t -0.12655818 -0.98768836 0.091949895 -0.091949895 -0.98768836 0.12655817 -0.048340924 -0.98768836 0.14877805\n\t\t -4.6621107e-09 -0.98768836 0.15643449 0.048340909 -0.98768836 0.14877804 0.09194988 -0.98768836 0.12655815\n\t\t 0.12655815 -0.98768836 0.091949888 0.14877804 -0.98768836 0.048340913 0.15643448 -0.98768836 0\n\t\t 0.29389283 -0.95105654 -0.095491566 0.25000018 -0.95105654 -0.18163574 0.18163574 -0.95105654 -0.25000015\n\t\t 0.095491551 -0.95105654 -0.2938928 0 -0.95105654 -0.30901715 -0.095491551 -0.95105654 -0.29389277\n\t\t -0.18163571 -0.95105654 -0.25000009 -0.25000009 -0.95105654 -0.18163569 -0.29389271 -0.95105654 -0.095491529\n\t\t -0.30901706 -0.95105654 0 -0.29389271 -0.95105654 0.095491529 -0.25000006 -0.95105654 0.18163568\n\t\t -0.18163568 -0.95105654 0.25000006 -0.095491529 -0.95105654 0.29389268 -9.2094243e-09 -0.95105654 0.30901703\n\t\t 0.095491499 -0.95105654 0.29389265 0.18163563 -0.95105654 0.25000003 0.25 -0.95105654 0.18163565\n\t\t 0.29389265 -0.95105654 0.095491506 0.309017 -0.95105654 0 0.43177092 -0.89100653 -0.14029087\n\t\t 0.36728629 -0.89100653 -0.2668491 0.2668491 -0.89100653 -0.36728626 0.14029086 -0.89100653 -0.43177086\n\t\t 0 -0.89100653 -0.45399073 -0.14029086 -0.89100653 -0.43177083 -0.26684904 -0.89100653 -0.36728618\n\t\t -0.36728615 -0.89100653 -0.26684901 -0.43177077 -0.89100653 -0.14029081 -0.45399064 -0.89100653 0\n\t\t -0.43177077 -0.89100653 0.14029081 -0.36728612 -0.89100653 0.26684898 -0.26684898 -0.89100653 0.36728612\n\t\t -0.14029081 -0.89100653 0.43177071 -1.3529972e-08 -0.89100653 0.45399058 0.14029078 -0.89100653 0.43177068\n\t\t 0.26684892 -0.89100653 0.36728609 0.36728606 -0.89100653 0.26684895 0.43177065 -0.89100653 0.1402908\n\t\t 0.45399052 -0.89100653 0 0.55901736 -0.809017 -0.18163574 0.47552857 -0.809017 -0.34549171\n\t\t 0.34549171 -0.809017 -0.47552854 0.18163572 -0.809017 -0.5590173 0 -0.809017 -0.58778554\n\t\t -0.18163572 -0.809017 -0.55901724 -0.34549165 -0.809017 -0.47552842 -0.47552839 -0.809017 -0.34549159\n\t\t -0.55901712 -0.809017 -0.18163566 -0.58778536 -0.809017 0 -0.55901712 -0.809017 0.18163566\n\t\t -0.47552836 -0.809017 0.34549156 -0.34549156 -0.809017 0.47552833 -0.18163566 -0.809017 0.55901706\n\t\t -1.7517365e-08 -0.809017 0.5877853 0.18163562 -0.809017 0.55901706 0.3454915 -0.809017 0.4755283\n\t\t 0.47552827 -0.809017 0.34549153 0.559017 -0.809017 0.18163563 0.58778524 -0.809017 0\n\t\t 0.67249894 -0.70710677 -0.21850814 0.57206178 -0.70710677 -0.41562718 0.41562718 -0.70710677 -0.57206172\n\t\t 0.21850812 -0.70710677 -0.67249888 0 -0.70710677 -0.70710713 -0.21850812 -0.70710677 -0.67249882\n\t\t -0.41562709 -0.70710677 -0.5720616 -0.57206154 -0.70710677 -0.41562706 -0.6724987 -0.70710677 -0.21850805\n\t\t -0.70710695 -0.70710677 0 -0.6724987 -0.70710677 0.21850805 -0.57206154 -0.70710677 0.415627\n\t\t -0.415627 -0.70710677 0.57206148 -0.21850805 -0.70710677 0.67249858 -2.1073424e-08 -0.70710677 0.70710683\n\t\t 0.21850799 -0.70710677 0.67249858 0.41562691 -0.70710677 0.57206142 0.57206142 -0.70710677 0.41562697\n\t\t 0.67249852 -0.70710677 0.21850802 0.70710677 -0.70710677 0 0.7694214 -0.58778524 -0.25000015\n\t\t 0.65450895 -0.58778524 -0.47552854 0.47552854 -0.58778524 -0.65450889 0.25000012 -0.58778524 -0.76942128\n\t\t 0 -0.58778524 -0.80901736 -0.25000012 -0.58778524 -0.76942122 -0.47552845 -0.58778524 -0.65450877\n\t\t -0.65450871 -0.58778524 -0.47552839 -0.7694211 -0.58778524 -0.25000006 -0.80901718 -0.58778524 0\n\t\t -0.7694211 -0.58778524 0.25000006 -0.65450865 -0.58778524 0.47552836 -0.47552836 -0.58778524 0.65450859\n\t\t -0.25000006 -0.58778524 0.76942098 -2.4110586e-08 -0.58778524 0.80901712 0.24999999 -0.58778524 0.76942098\n\t\t 0.47552827 -0.58778524 0.65450853 0.65450853 -0.58778524 0.4755283 0.76942092 -0.58778524 0.25\n\t\t 0.809017 -0.58778524 0 0.8473981 -0.45399052 -0.27533633 0.72083992 -0.45399052 -0.5237208\n\t\t 0.5237208 -0.45399052 -0.72083986 0.2753363 -0.45399052 -0.84739798 0 -0.45399052 -0.89100695\n\t\t -0.2753363 -0.45399052 -0.84739798 -0.52372068 -0.45399052 -0.72083968 -0.72083962 -0.45399052 -0.52372062\n\t\t -0.8473978 -0.45399052 -0.27533621 -0.89100677 -0.45399052 0 -0.8473978 -0.45399052 0.27533621\n\t\t -0.72083962 -0.45399052 0.52372062 -0.52372062 -0.45399052 0.72083956 -0.27533621 -0.45399052 0.84739769\n\t\t -2.6554064e-08 -0.45399052 0.89100665 0.27533615 -0.45399052 0.84739763 0.5237205 -0.45399052 0.7208395\n\t\t 0.72083944 -0.45399052 0.52372056 0.84739757 -0.45399052 0.27533618 0.89100653 -0.45399052 0\n\t\t 0.90450913 -0.30901697 -0.2938928 0.7694214 -0.30901697 -0.55901736 0.55901736 -0.30901697 -0.76942134\n\t\t 0.29389277 -0.30901697 -0.90450901 0 -0.30901697 -0.95105702 -0.29389277 -0.30901697 -0.90450895\n\t\t -0.55901724 -0.30901697 -0.76942122 -0.76942116 -0.30901697 -0.55901718 -0.90450877 -0.30901697 -0.29389271\n\t\t -0.95105678 -0.30901697 0 -0.90450877 -0.30901697 0.29389271 -0.7694211 -0.30901697 0.55901712\n\t\t -0.55901712 -0.30901697 0.76942104 -0.29389271 -0.30901697 0.90450865 -2.8343694e-08 -0.30901697 0.95105666\n\t\t 0.29389262 -0.30901697 0.90450859 0.559017 -0.30901697 0.76942098 0.76942092 -0.30901697 0.55901706\n\t\t 0.90450853 -0.30901697 0.29389265 0.95105654 -0.30901697 0 0.93934804 -0.15643437 -0.30521268\n\t\t 0.79905719 -0.15643437 -0.580549 0.580549 -0.15643437 -0.79905713 0.30521265 -0.15643437 -0.93934792\n\t\t 0 -0.15643437 -0.98768884 -0.30521265 -0.15643437 -0.93934786;\n\tsetAttr \".vt[166:331]\" -0.58054888 -0.15643437 -0.79905695 -0.79905689 -0.15643437 -0.58054882\n\t\t -0.93934768 -0.15643437 -0.30521256 -0.9876886 -0.15643437 0 -0.93934768 -0.15643437 0.30521256\n\t\t -0.79905683 -0.15643437 0.58054876 -0.58054876 -0.15643437 0.79905677 -0.30521256 -0.15643437 0.93934757\n\t\t -2.9435407e-08 -0.15643437 0.98768848 0.30521247 -0.15643437 0.93934757 0.58054864 -0.15643437 0.79905671\n\t\t 0.79905665 -0.15643437 0.5805487 0.93934751 -0.15643437 0.3052125 0.98768836 -0.15643437 0\n\t\t 0.95105714 0 -0.30901718 0.80901754 0 -0.5877856 0.5877856 0 -0.80901748 0.30901715 0 -0.95105702\n\t\t 0 0 -1.000000476837 -0.30901715 0 -0.95105696 -0.58778548 0 -0.8090173 -0.80901724 0 -0.58778542\n\t\t -0.95105678 0 -0.30901706 -1.000000238419 0 0 -0.95105678 0 0.30901706 -0.80901718 0 0.58778536\n\t\t -0.58778536 0 0.80901712 -0.30901706 0 0.95105666 -2.9802322e-08 0 1.000000119209\n\t\t 0.30901697 0 0.9510566 0.58778524 0 0.80901706 0.809017 0 0.5877853 0.95105654 0 0.309017\n\t\t 1 0 0 0.93934804 0.15643437 -0.30521268 0.79905719 0.15643437 -0.580549 0.580549 0.15643437 -0.79905713\n\t\t 0.30521265 0.15643437 -0.93934792 0 0.15643437 -0.98768884 -0.30521265 0.15643437 -0.93934786\n\t\t -0.58054888 0.15643437 -0.79905695 -0.79905689 0.15643437 -0.58054882 -0.93934768 0.15643437 -0.30521256\n\t\t -0.9876886 0.15643437 0 -0.93934768 0.15643437 0.30521256 -0.79905683 0.15643437 0.58054876\n\t\t -0.58054876 0.15643437 0.79905677 -0.30521256 0.15643437 0.93934757 -2.9435407e-08 0.15643437 0.98768848\n\t\t 0.30521247 0.15643437 0.93934757 0.58054864 0.15643437 0.79905671 0.79905665 0.15643437 0.5805487\n\t\t 0.93934751 0.15643437 0.3052125 0.98768836 0.15643437 0 0.90450913 0.30901697 -0.2938928\n\t\t 0.7694214 0.30901697 -0.55901736 0.55901736 0.30901697 -0.76942134 0.29389277 0.30901697 -0.90450901\n\t\t 0 0.30901697 -0.95105702 -0.29389277 0.30901697 -0.90450895 -0.55901724 0.30901697 -0.76942122\n\t\t -0.76942116 0.30901697 -0.55901718 -0.90450877 0.30901697 -0.29389271 -0.95105678 0.30901697 0\n\t\t -0.90450877 0.30901697 0.29389271 -0.7694211 0.30901697 0.55901712 -0.55901712 0.30901697 0.76942104\n\t\t -0.29389271 0.30901697 0.90450865 -2.8343694e-08 0.30901697 0.95105666 0.29389262 0.30901697 0.90450859\n\t\t 0.559017 0.30901697 0.76942098 0.76942092 0.30901697 0.55901706 0.90450853 0.30901697 0.29389265\n\t\t 0.95105654 0.30901697 0 0.8473981 0.45399052 -0.27533633 0.72083992 0.45399052 -0.5237208\n\t\t 0.5237208 0.45399052 -0.72083986 0.2753363 0.45399052 -0.84739798 0 0.45399052 -0.89100695\n\t\t -0.2753363 0.45399052 -0.84739798 -0.52372068 0.45399052 -0.72083968 -0.72083962 0.45399052 -0.52372062\n\t\t -0.8473978 0.45399052 -0.27533621 -0.89100677 0.45399052 0 -0.8473978 0.45399052 0.27533621\n\t\t -0.72083962 0.45399052 0.52372062 -0.52372062 0.45399052 0.72083956 -0.27533621 0.45399052 0.84739769\n\t\t -2.6554064e-08 0.45399052 0.89100665 0.27533615 0.45399052 0.84739763 0.5237205 0.45399052 0.7208395\n\t\t 0.72083944 0.45399052 0.52372056 0.84739757 0.45399052 0.27533618 0.89100653 0.45399052 0\n\t\t 0.7694214 0.58778524 -0.25000015 0.65450895 0.58778524 -0.47552854 0.47552854 0.58778524 -0.65450889\n\t\t 0.25000012 0.58778524 -0.76942128 0 0.58778524 -0.80901736 -0.25000012 0.58778524 -0.76942122\n\t\t -0.47552845 0.58778524 -0.65450877 -0.65450871 0.58778524 -0.47552839 -0.7694211 0.58778524 -0.25000006\n\t\t -0.80901718 0.58778524 0 -0.7694211 0.58778524 0.25000006 -0.65450865 0.58778524 0.47552836\n\t\t -0.47552836 0.58778524 0.65450859 -0.25000006 0.58778524 0.76942098 -2.4110586e-08 0.58778524 0.80901712\n\t\t 0.24999999 0.58778524 0.76942098 0.47552827 0.58778524 0.65450853 0.65450853 0.58778524 0.4755283\n\t\t 0.76942092 0.58778524 0.25 0.809017 0.58778524 0 0.67249894 0.70710677 -0.21850814\n\t\t 0.57206178 0.70710677 -0.41562718 0.41562718 0.70710677 -0.57206172 0.21850812 0.70710677 -0.67249888\n\t\t 0 0.70710677 -0.70710713 -0.21850812 0.70710677 -0.67249882 -0.41562709 0.70710677 -0.5720616\n\t\t -0.57206154 0.70710677 -0.41562706 -0.6724987 0.70710677 -0.21850805 -0.70710695 0.70710677 0\n\t\t -0.6724987 0.70710677 0.21850805 -0.57206154 0.70710677 0.415627 -0.415627 0.70710677 0.57206148\n\t\t -0.21850805 0.70710677 0.67249858 -2.1073424e-08 0.70710677 0.70710683 0.21850799 0.70710677 0.67249858\n\t\t 0.41562691 0.70710677 0.57206142 0.57206142 0.70710677 0.41562697 0.67249852 0.70710677 0.21850802\n\t\t 0.70710677 0.70710677 0 0.55901736 0.809017 -0.18163574 0.47552857 0.809017 -0.34549171\n\t\t 0.34549171 0.809017 -0.47552854 0.18163572 0.809017 -0.5590173 0 0.809017 -0.58778554\n\t\t -0.18163572 0.809017 -0.55901724 -0.34549165 0.809017 -0.47552842 -0.47552839 0.809017 -0.34549159\n\t\t -0.55901712 0.809017 -0.18163566 -0.58778536 0.809017 0 -0.55901712 0.809017 0.18163566\n\t\t -0.47552836 0.809017 0.34549156 -0.34549156 0.809017 0.47552833 -0.18163566 0.809017 0.55901706\n\t\t -1.7517365e-08 0.809017 0.5877853 0.18163562 0.809017 0.55901706 0.3454915 0.809017 0.4755283\n\t\t 0.47552827 0.809017 0.34549153 0.559017 0.809017 0.18163563 0.58778524 0.809017 0\n\t\t 0.43177092 0.89100653 -0.14029087 0.36728629 0.89100653 -0.2668491 0.2668491 0.89100653 -0.36728626\n\t\t 0.14029086 0.89100653 -0.43177086 0 0.89100653 -0.45399073 -0.14029086 0.89100653 -0.43177083\n\t\t -0.26684904 0.89100653 -0.36728618 -0.36728615 0.89100653 -0.26684901 -0.43177077 0.89100653 -0.14029081\n\t\t -0.45399064 0.89100653 0 -0.43177077 0.89100653 0.14029081 -0.36728612 0.89100653 0.26684898;\n\tsetAttr \".vt[332:381]\" -0.26684898 0.89100653 0.36728612 -0.14029081 0.89100653 0.43177071\n\t\t -1.3529972e-08 0.89100653 0.45399058 0.14029078 0.89100653 0.43177068 0.26684892 0.89100653 0.36728609\n\t\t 0.36728606 0.89100653 0.26684895 0.43177065 0.89100653 0.1402908 0.45399052 0.89100653 0\n\t\t 0.29389283 0.95105654 -0.095491566 0.25000018 0.95105654 -0.18163574 0.18163574 0.95105654 -0.25000015\n\t\t 0.095491551 0.95105654 -0.2938928 0 0.95105654 -0.30901715 -0.095491551 0.95105654 -0.29389277\n\t\t -0.18163571 0.95105654 -0.25000009 -0.25000009 0.95105654 -0.18163569 -0.29389271 0.95105654 -0.095491529\n\t\t -0.30901706 0.95105654 0 -0.29389271 0.95105654 0.095491529 -0.25000006 0.95105654 0.18163568\n\t\t -0.18163568 0.95105654 0.25000006 -0.095491529 0.95105654 0.29389268 -9.2094243e-09 0.95105654 0.30901703\n\t\t 0.095491499 0.95105654 0.29389265 0.18163563 0.95105654 0.25000003 0.25 0.95105654 0.18163565\n\t\t 0.29389265 0.95105654 0.095491506 0.309017 0.95105654 0 0.14877813 0.98768836 -0.048340943\n\t\t 0.12655823 0.98768836 -0.091949932 0.091949932 0.98768836 -0.12655823 0.048340935 0.98768836 -0.14877811\n\t\t 0 0.98768836 -0.15643455 -0.048340935 0.98768836 -0.1487781 -0.091949917 0.98768836 -0.1265582\n\t\t -0.12655818 0.98768836 -0.091949902 -0.14877807 0.98768836 -0.048340924 -0.15643452 0.98768836 0\n\t\t -0.14877807 0.98768836 0.048340924 -0.12655818 0.98768836 0.091949895 -0.091949895 0.98768836 0.12655817\n\t\t -0.048340924 0.98768836 0.14877805 -4.6621107e-09 0.98768836 0.15643449 0.048340909 0.98768836 0.14877804\n\t\t 0.09194988 0.98768836 0.12655815 0.12655815 0.98768836 0.091949888 0.14877804 0.98768836 0.048340913\n\t\t 0.15643448 0.98768836 0 0 -1 0 0 1 0;\n\tsetAttr -s 780 \".ed\";\n\tsetAttr \".ed[0:165]\"  0 1 1 1 2 1 2 3 1 3 4 1 4 5 1 5 6 1 6 7 1 7 8 1 8 9 1\n\t\t 9 10 1 10 11 1 11 12 1 12 13 1 13 14 1 14 15 1 15 16 1 16 17 1 17 18 1 18 19 1 19 0 1\n\t\t 20 21 1 21 22 1 22 23 1 23 24 1 24 25 1 25 26 1 26 27 1 27 28 1 28 29 1 29 30 1 30 31 1\n\t\t 31 32 1 32 33 1 33 34 1 34 35 1 35 36 1 36 37 1 37 38 1 38 39 1 39 20 1 40 41 1 41 42 1\n\t\t 42 43 1 43 44 1 44 45 1 45 46 1 46 47 1 47 48 1 48 49 1 49 50 1 50 51 1 51 52 1 52 53 1\n\t\t 53 54 1 54 55 1 55 56 1 56 57 1 57 58 1 58 59 1 59 40 1 60 61 1 61 62 1 62 63 1 63 64 1\n\t\t 64 65 1 65 66 1 66 67 1 67 68 1 68 69 1 69 70 1 70 71 1 71 72 1 72 73 1 73 74 1 74 75 1\n\t\t 75 76 1 76 77 1 77 78 1 78 79 1 79 60 1 80 81 1 81 82 1 82 83 1 83 84 1 84 85 1 85 86 1\n\t\t 86 87 1 87 88 1 88 89 1 89 90 1 90 91 1 91 92 1 92 93 1 93 94 1 94 95 1 95 96 1 96 97 1\n\t\t 97 98 1 98 99 1 99 80 1 100 101 1 101 102 1 102 103 1 103 104 1 104 105 1 105 106 1\n\t\t 106 107 1 107 108 1 108 109 1 109 110 1 110 111 1 111 112 1 112 113 1 113 114 1 114 115 1\n\t\t 115 116 1 116 117 1 117 118 1 118 119 1 119 100 1 120 121 1 121 122 1 122 123 1 123 124 1\n\t\t 124 125 1 125 126 1 126 127 1 127 128 1 128 129 1 129 130 1 130 131 1 131 132 1 132 133 1\n\t\t 133 134 1 134 135 1 135 136 1 136 137 1 137 138 1 138 139 1 139 120 1 140 141 1 141 142 1\n\t\t 142 143 1 143 144 1 144 145 1 145 146 1 146 147 1 147 148 1 148 149 1 149 150 1 150 151 1\n\t\t 151 152 1 152 153 1 153 154 1 154 155 1 155 156 1 156 157 1 157 158 1 158 159 1 159 140 1\n\t\t 160 161 1 161 162 1 162 163 1 163 164 1 164 165 1 165 166 1;\n\tsetAttr \".ed[166:331]\" 166 167 1 167 168 1 168 169 1 169 170 1 170 171 1 171 172 1\n\t\t 172 173 1 173 174 1 174 175 1 175 176 1 176 177 1 177 178 1 178 179 1 179 160 1 180 181 1\n\t\t 181 182 1 182 183 1 183 184 1 184 185 1 185 186 1 186 187 1 187 188 1 188 189 1 189 190 1\n\t\t 190 191 1 191 192 1 192 193 1 193 194 1 194 195 1 195 196 1 196 197 1 197 198 1 198 199 1\n\t\t 199 180 1 200 201 1 201 202 1 202 203 1 203 204 1 204 205 1 205 206 1 206 207 1 207 208 1\n\t\t 208 209 1 209 210 1 210 211 1 211 212 1 212 213 1 213 214 1 214 215 1 215 216 1 216 217 1\n\t\t 217 218 1 218 219 1 219 200 1 220 221 1 221 222 1 222 223 1 223 224 1 224 225 1 225 226 1\n\t\t 226 227 1 227 228 1 228 229 1 229 230 1 230 231 1 231 232 1 232 233 1 233 234 1 234 235 1\n\t\t 235 236 1 236 237 1 237 238 1 238 239 1 239 220 1 240 241 1 241 242 1 242 243 1 243 244 1\n\t\t 244 245 1 245 246 1 246 247 1 247 248 1 248 249 1 249 250 1 250 251 1 251 252 1 252 253 1\n\t\t 253 254 1 254 255 1 255 256 1 256 257 1 257 258 1 258 259 1 259 240 1 260 261 1 261 262 1\n\t\t 262 263 1 263 264 1 264 265 1 265 266 1 266 267 1 267 268 1 268 269 1 269 270 1 270 271 1\n\t\t 271 272 1 272 273 1 273 274 1 274 275 1 275 276 1 276 277 1 277 278 1 278 279 1 279 260 1\n\t\t 280 281 1 281 282 1 282 283 1 283 284 1 284 285 1 285 286 1 286 287 1 287 288 1 288 289 1\n\t\t 289 290 1 290 291 1 291 292 1 292 293 1 293 294 1 294 295 1 295 296 1 296 297 1 297 298 1\n\t\t 298 299 1 299 280 1 300 301 1 301 302 1 302 303 1 303 304 1 304 305 1 305 306 1 306 307 1\n\t\t 307 308 1 308 309 1 309 310 1 310 311 1 311 312 1 312 313 1 313 314 1 314 315 1 315 316 1\n\t\t 316 317 1 317 318 1 318 319 1 319 300 1 320 321 1 321 322 1 322 323 1 323 324 1 324 325 1\n\t\t 325 326 1 326 327 1 327 328 1 328 329 1 329 330 1 330 331 1 331 332 1;\n\tsetAttr \".ed[332:497]\" 332 333 1 333 334 1 334 335 1 335 336 1 336 337 1 337 338 1\n\t\t 338 339 1 339 320 1 340 341 1 341 342 1 342 343 1 343 344 1 344 345 1 345 346 1 346 347 1\n\t\t 347 348 1 348 349 1 349 350 1 350 351 1 351 352 1 352 353 1 353 354 1 354 355 1 355 356 1\n\t\t 356 357 1 357 358 1 358 359 1 359 340 1 360 361 1 361 362 1 362 363 1 363 364 1 364 365 1\n\t\t 365 366 1 366 367 1 367 368 1 368 369 1 369 370 1 370 371 1 371 372 1 372 373 1 373 374 1\n\t\t 374 375 1 375 376 1 376 377 1 377 378 1 378 379 1 379 360 1 0 20 1 1 21 1 2 22 1\n\t\t 3 23 1 4 24 1 5 25 1 6 26 1 7 27 1 8 28 1 9 29 1 10 30 1 11 31 1 12 32 1 13 33 1\n\t\t 14 34 1 15 35 1 16 36 1 17 37 1 18 38 1 19 39 1 20 40 1 21 41 1 22 42 1 23 43 1 24 44 1\n\t\t 25 45 1 26 46 1 27 47 1 28 48 1 29 49 1 30 50 1 31 51 1 32 52 1 33 53 1 34 54 1 35 55 1\n\t\t 36 56 1 37 57 1 38 58 1 39 59 1 40 60 1 41 61 1 42 62 1 43 63 1 44 64 1 45 65 1 46 66 1\n\t\t 47 67 1 48 68 1 49 69 1 50 70 1 51 71 1 52 72 1 53 73 1 54 74 1 55 75 1 56 76 1 57 77 1\n\t\t 58 78 1 59 79 1 60 80 1 61 81 1 62 82 1 63 83 1 64 84 1 65 85 1 66 86 1 67 87 1 68 88 1\n\t\t 69 89 1 70 90 1 71 91 1 72 92 1 73 93 1 74 94 1 75 95 1 76 96 1 77 97 1 78 98 1 79 99 1\n\t\t 80 100 1 81 101 1 82 102 1 83 103 1 84 104 1 85 105 1 86 106 1 87 107 1 88 108 1\n\t\t 89 109 1 90 110 1 91 111 1 92 112 1 93 113 1 94 114 1 95 115 1 96 116 1 97 117 1\n\t\t 98 118 1 99 119 1 100 120 1 101 121 1 102 122 1 103 123 1 104 124 1 105 125 1 106 126 1\n\t\t 107 127 1 108 128 1 109 129 1 110 130 1 111 131 1 112 132 1 113 133 1 114 134 1 115 135 1\n\t\t 116 136 1 117 137 1;\n\tsetAttr \".ed[498:663]\" 118 138 1 119 139 1 120 140 1 121 141 1 122 142 1 123 143 1\n\t\t 124 144 1 125 145 1 126 146 1 127 147 1 128 148 1 129 149 1 130 150 1 131 151 1 132 152 1\n\t\t 133 153 1 134 154 1 135 155 1 136 156 1 137 157 1 138 158 1 139 159 1 140 160 1 141 161 1\n\t\t 142 162 1 143 163 1 144 164 1 145 165 1 146 166 1 147 167 1 148 168 1 149 169 1 150 170 1\n\t\t 151 171 1 152 172 1 153 173 1 154 174 1 155 175 1 156 176 1 157 177 1 158 178 1 159 179 1\n\t\t 160 180 1 161 181 1 162 182 1 163 183 1 164 184 1 165 185 1 166 186 1 167 187 1 168 188 1\n\t\t 169 189 1 170 190 1 171 191 1 172 192 1 173 193 1 174 194 1 175 195 1 176 196 1 177 197 1\n\t\t 178 198 1 179 199 1 180 200 1 181 201 1 182 202 1 183 203 1 184 204 1 185 205 1 186 206 1\n\t\t 187 207 1 188 208 1 189 209 1 190 210 1 191 211 1 192 212 1 193 213 1 194 214 1 195 215 1\n\t\t 196 216 1 197 217 1 198 218 1 199 219 1 200 220 1 201 221 1 202 222 1 203 223 1 204 224 1\n\t\t 205 225 1 206 226 1 207 227 1 208 228 1 209 229 1 210 230 1 211 231 1 212 232 1 213 233 1\n\t\t 214 234 1 215 235 1 216 236 1 217 237 1 218 238 1 219 239 1 220 240 1 221 241 1 222 242 1\n\t\t 223 243 1 224 244 1 225 245 1 226 246 1 227 247 1 228 248 1 229 249 1 230 250 1 231 251 1\n\t\t 232 252 1 233 253 1 234 254 1 235 255 1 236 256 1 237 257 1 238 258 1 239 259 1 240 260 1\n\t\t 241 261 1 242 262 1 243 263 1 244 264 1 245 265 1 246 266 1 247 267 1 248 268 1 249 269 1\n\t\t 250 270 1 251 271 1 252 272 1 253 273 1 254 274 1 255 275 1 256 276 1 257 277 1 258 278 1\n\t\t 259 279 1 260 280 1 261 281 1 262 282 1 263 283 1 264 284 1 265 285 1 266 286 1 267 287 1\n\t\t 268 288 1 269 289 1 270 290 1 271 291 1 272 292 1 273 293 1 274 294 1 275 295 1 276 296 1\n\t\t 277 297 1 278 298 1 279 299 1 280 300 1 281 301 1 282 302 1 283 303 1;\n\tsetAttr \".ed[664:779]\" 284 304 1 285 305 1 286 306 1 287 307 1 288 308 1 289 309 1\n\t\t 290 310 1 291 311 1 292 312 1 293 313 1 294 314 1 295 315 1 296 316 1 297 317 1 298 318 1\n\t\t 299 319 1 300 320 1 301 321 1 302 322 1 303 323 1 304 324 1 305 325 1 306 326 1 307 327 1\n\t\t 308 328 1 309 329 1 310 330 1 311 331 1 312 332 1 313 333 1 314 334 1 315 335 1 316 336 1\n\t\t 317 337 1 318 338 1 319 339 1 320 340 1 321 341 1 322 342 1 323 343 1 324 344 1 325 345 1\n\t\t 326 346 1 327 347 1 328 348 1 329 349 1 330 350 1 331 351 1 332 352 1 333 353 1 334 354 1\n\t\t 335 355 1 336 356 1 337 357 1 338 358 1 339 359 1 340 360 1 341 361 1 342 362 1 343 363 1\n\t\t 344 364 1 345 365 1 346 366 1 347 367 1 348 368 1 349 369 1 350 370 1 351 371 1 352 372 1\n\t\t 353 373 1 354 374 1 355 375 1 356 376 1 357 377 1 358 378 1 359 379 1 380 0 1 380 1 1\n\t\t 380 2 1 380 3 1 380 4 1 380 5 1 380 6 1 380 7 1 380 8 1 380 9 1 380 10 1 380 11 1\n\t\t 380 12 1 380 13 1 380 14 1 380 15 1 380 16 1 380 17 1 380 18 1 380 19 1 360 381 1\n\t\t 361 381 1 362 381 1 363 381 1 364 381 1 365 381 1 366 381 1 367 381 1 368 381 1 369 381 1\n\t\t 370 381 1 371 381 1 372 381 1 373 381 1 374 381 1 375 381 1 376 381 1 377 381 1 378 381 1\n\t\t 379 381 1;\n\tsetAttr -s 400 -ch 1560 \".fc[0:399]\" -type \"polyFaces\" \n\t\tf 4 0 381 -21 -381\n\t\tmu 0 4 0 1 22 21\n\t\tf 4 1 382 -22 -382\n\t\tmu 0 4 1 2 23 22\n\t\tf 4 2 383 -23 -383\n\t\tmu 0 4 2 3 24 23\n\t\tf 4 3 384 -24 -384\n\t\tmu 0 4 3 4 25 24\n\t\tf 4 4 385 -25 -385\n\t\tmu 0 4 4 5 26 25\n\t\tf 4 5 386 -26 -386\n\t\tmu 0 4 5 6 27 26\n\t\tf 4 6 387 -27 -387\n\t\tmu 0 4 6 7 28 27\n\t\tf 4 7 388 -28 -388\n\t\tmu 0 4 7 8 29 28\n\t\tf 4 8 389 -29 -389\n\t\tmu 0 4 8 9 30 29\n\t\tf 4 9 390 -30 -390\n\t\tmu 0 4 9 10 31 30\n\t\tf 4 10 391 -31 -391\n\t\tmu 0 4 10 11 32 31\n\t\tf 4 11 392 -32 -392\n\t\tmu 0 4 11 12 33 32\n\t\tf 4 12 393 -33 -393\n\t\tmu 0 4 12 13 34 33\n\t\tf 4 13 394 -34 -394\n\t\tmu 0 4 13 14 35 34\n\t\tf 4 14 395 -35 -395\n\t\tmu 0 4 14 15 36 35\n\t\tf 4 15 396 -36 -396\n\t\tmu 0 4 15 16 37 36\n\t\tf 4 16 397 -37 -397\n\t\tmu 0 4 16 17 38 37\n\t\tf 4 17 398 -38 -398\n\t\tmu 0 4 17 18 39 38\n\t\tf 4 18 399 -39 -399\n\t\tmu 0 4 18 19 40 39\n\t\tf 4 19 380 -40 -400\n\t\tmu 0 4 19 20 41 40\n\t\tf 4 20 401 -41 -401\n\t\tmu 0 4 21 22 43 42\n\t\tf 4 21 402 -42 -402\n\t\tmu 0 4 22 23 44 43\n\t\tf 4 22 403 -43 -403\n\t\tmu 0 4 23 24 45 44\n\t\tf 4 23 404 -44 -404\n\t\tmu 0 4 24 25 46 45\n\t\tf 4 24 405 -45 -405\n\t\tmu 0 4 25 26 47 46\n\t\tf 4 25 406 -46 -406\n\t\tmu 0 4 26 27 48 47\n\t\tf 4 26 407 -47 -407\n\t\tmu 0 4 27 28 49 48\n\t\tf 4 27 408 -48 -408\n\t\tmu 0 4 28 29 50 49\n\t\tf 4 28 409 -49 -409\n\t\tmu 0 4 29 30 51 50\n\t\tf 4 29 410 -50 -410\n\t\tmu 0 4 30 31 52 51\n\t\tf 4 30 411 -51 -411\n\t\tmu 0 4 31 32 53 52\n\t\tf 4 31 412 -52 -412\n\t\tmu 0 4 32 33 54 53\n\t\tf 4 32 413 -53 -413\n\t\tmu 0 4 33 34 55 54\n\t\tf 4 33 414 -54 -414\n\t\tmu 0 4 34 35 56 55\n\t\tf 4 34 415 -55 -415\n\t\tmu 0 4 35 36 57 56\n\t\tf 4 35 416 -56 -416\n\t\tmu 0 4 36 37 58 57\n\t\tf 4 36 417 -57 -417\n\t\tmu 0 4 37 38 59 58\n\t\tf 4 37 418 -58 -418\n\t\tmu 0 4 38 39 60 59\n\t\tf 4 38 419 -59 -419\n\t\tmu 0 4 39 40 61 60\n\t\tf 4 39 400 -60 -420\n\t\tmu 0 4 40 41 62 61\n\t\tf 4 40 421 -61 -421\n\t\tmu 0 4 42 43 64 63\n\t\tf 4 41 422 -62 -422\n\t\tmu 0 4 43 44 65 64\n\t\tf 4 42 423 -63 -423\n\t\tmu 0 4 44 45 66 65\n\t\tf 4 43 424 -64 -424\n\t\tmu 0 4 45 46 67 66\n\t\tf 4 44 425 -65 -425\n\t\tmu 0 4 46 47 68 67\n\t\tf 4 45 426 -66 -426\n\t\tmu 0 4 47 48 69 68\n\t\tf 4 46 427 -67 -427\n\t\tmu 0 4 48 49 70 69\n\t\tf 4 47 428 -68 -428\n\t\tmu 0 4 49 50 71 70\n\t\tf 4 48 429 -69 -429\n\t\tmu 0 4 50 51 72 71\n\t\tf 4 49 430 -70 -430\n\t\tmu 0 4 51 52 73 72\n\t\tf 4 50 431 -71 -431\n\t\tmu 0 4 52 53 74 73\n\t\tf 4 51 432 -72 -432\n\t\tmu 0 4 53 54 75 74\n\t\tf 4 52 433 -73 -433\n\t\tmu 0 4 54 55 76 75\n\t\tf 4 53 434 -74 -434\n\t\tmu 0 4 55 56 77 76\n\t\tf 4 54 435 -75 -435\n\t\tmu 0 4 56 57 78 77\n\t\tf 4 55 436 -76 -436\n\t\tmu 0 4 57 58 79 78\n\t\tf 4 56 437 -77 -437\n\t\tmu 0 4 58 59 80 79\n\t\tf 4 57 438 -78 -438\n\t\tmu 0 4 59 60 81 80\n\t\tf 4 58 439 -79 -439\n\t\tmu 0 4 60 61 82 81\n\t\tf 4 59 420 -80 -440\n\t\tmu 0 4 61 62 83 82\n\t\tf 4 60 441 -81 -441\n\t\tmu 0 4 63 64 85 84\n\t\tf 4 61 442 -82 -442\n\t\tmu 0 4 64 65 86 85\n\t\tf 4 62 443 -83 -443\n\t\tmu 0 4 65 66 87 86\n\t\tf 4 63 444 -84 -444\n\t\tmu 0 4 66 67 88 87\n\t\tf 4 64 445 -85 -445\n\t\tmu 0 4 67 68 89 88\n\t\tf 4 65 446 -86 -446\n\t\tmu 0 4 68 69 90 89\n\t\tf 4 66 447 -87 -447\n\t\tmu 0 4 69 70 91 90\n\t\tf 4 67 448 -88 -448\n\t\tmu 0 4 70 71 92 91\n\t\tf 4 68 449 -89 -449\n\t\tmu 0 4 71 72 93 92\n\t\tf 4 69 450 -90 -450\n\t\tmu 0 4 72 73 94 93\n\t\tf 4 70 451 -91 -451\n\t\tmu 0 4 73 74 95 94\n\t\tf 4 71 452 -92 -452\n\t\tmu 0 4 74 75 96 95\n\t\tf 4 72 453 -93 -453\n\t\tmu 0 4 75 76 97 96\n\t\tf 4 73 454 -94 -454\n\t\tmu 0 4 76 77 98 97\n\t\tf 4 74 455 -95 -455\n\t\tmu 0 4 77 78 99 98\n\t\tf 4 75 456 -96 -456\n\t\tmu 0 4 78 79 100 99\n\t\tf 4 76 457 -97 -457\n\t\tmu 0 4 79 80 101 100\n\t\tf 4 77 458 -98 -458\n\t\tmu 0 4 80 81 102 101\n\t\tf 4 78 459 -99 -459\n\t\tmu 0 4 81 82 103 102\n\t\tf 4 79 440 -100 -460\n\t\tmu 0 4 82 83 104 103\n\t\tf 4 80 461 -101 -461\n\t\tmu 0 4 84 85 106 105\n\t\tf 4 81 462 -102 -462\n\t\tmu 0 4 85 86 107 106\n\t\tf 4 82 463 -103 -463\n\t\tmu 0 4 86 87 108 107\n\t\tf 4 83 464 -104 -464\n\t\tmu 0 4 87 88 109 108\n\t\tf 4 84 465 -105 -465\n\t\tmu 0 4 88 89 110 109\n\t\tf 4 85 466 -106 -466\n\t\tmu 0 4 89 90 111 110\n\t\tf 4 86 467 -107 -467\n\t\tmu 0 4 90 91 112 111\n\t\tf 4 87 468 -108 -468\n\t\tmu 0 4 91 92 113 112\n\t\tf 4 88 469 -109 -469\n\t\tmu 0 4 92 93 114 113\n\t\tf 4 89 470 -110 -470\n\t\tmu 0 4 93 94 115 114\n\t\tf 4 90 471 -111 -471\n\t\tmu 0 4 94 95 116 115\n\t\tf 4 91 472 -112 -472\n\t\tmu 0 4 95 96 117 116\n\t\tf 4 92 473 -113 -473\n\t\tmu 0 4 96 97 118 117\n\t\tf 4 93 474 -114 -474\n\t\tmu 0 4 97 98 119 118\n\t\tf 4 94 475 -115 -475\n\t\tmu 0 4 98 99 120 119\n\t\tf 4 95 476 -116 -476\n\t\tmu 0 4 99 100 121 120\n\t\tf 4 96 477 -117 -477\n\t\tmu 0 4 100 101 122 121\n\t\tf 4 97 478 -118 -478\n\t\tmu 0 4 101 102 123 122\n\t\tf 4 98 479 -119 -479\n\t\tmu 0 4 102 103 124 123\n\t\tf 4 99 460 -120 -480\n\t\tmu 0 4 103 104 125 124\n\t\tf 4 100 481 -121 -481\n\t\tmu 0 4 105 106 127 126\n\t\tf 4 101 482 -122 -482\n\t\tmu 0 4 106 107 128 127\n\t\tf 4 102 483 -123 -483\n\t\tmu 0 4 107 108 129 128\n\t\tf 4 103 484 -124 -484\n\t\tmu 0 4 108 109 130 129\n\t\tf 4 104 485 -125 -485\n\t\tmu 0 4 109 110 131 130\n\t\tf 4 105 486 -126 -486\n\t\tmu 0 4 110 111 132 131\n\t\tf 4 106 487 -127 -487\n\t\tmu 0 4 111 112 133 132\n\t\tf 4 107 488 -128 -488\n\t\tmu 0 4 112 113 134 133\n\t\tf 4 108 489 -129 -489\n\t\tmu 0 4 113 114 135 134\n\t\tf 4 109 490 -130 -490\n\t\tmu 0 4 114 115 136 135\n\t\tf 4 110 491 -131 -491\n\t\tmu 0 4 115 116 137 136\n\t\tf 4 111 492 -132 -492\n\t\tmu 0 4 116 117 138 137\n\t\tf 4 112 493 -133 -493\n\t\tmu 0 4 117 118 139 138\n\t\tf 4 113 494 -134 -494\n\t\tmu 0 4 118 119 140 139\n\t\tf 4 114 495 -135 -495\n\t\tmu 0 4 119 120 141 140\n\t\tf 4 115 496 -136 -496\n\t\tmu 0 4 120 121 142 141\n\t\tf 4 116 497 -137 -497\n\t\tmu 0 4 121 122 143 142\n\t\tf 4 117 498 -138 -498\n\t\tmu 0 4 122 123 144 143\n\t\tf 4 118 499 -139 -499\n\t\tmu 0 4 123 124 145 144\n\t\tf 4 119 480 -140 -500\n\t\tmu 0 4 124 125 146 145\n\t\tf 4 120 501 -141 -501\n\t\tmu 0 4 126 127 148 147\n\t\tf 4 121 502 -142 -502\n\t\tmu 0 4 127 128 149 148\n\t\tf 4 122 503 -143 -503\n\t\tmu 0 4 128 129 150 149\n\t\tf 4 123 504 -144 -504\n\t\tmu 0 4 129 130 151 150\n\t\tf 4 124 505 -145 -505\n\t\tmu 0 4 130 131 152 151\n\t\tf 4 125 506 -146 -506\n\t\tmu 0 4 131 132 153 152\n\t\tf 4 126 507 -147 -507\n\t\tmu 0 4 132 133 154 153\n\t\tf 4 127 508 -148 -508\n\t\tmu 0 4 133 134 155 154\n\t\tf 4 128 509 -149 -509\n\t\tmu 0 4 134 135 156 155\n\t\tf 4 129 510 -150 -510\n\t\tmu 0 4 135 136 157 156\n\t\tf 4 130 511 -151 -511\n\t\tmu 0 4 136 137 158 157\n\t\tf 4 131 512 -152 -512\n\t\tmu 0 4 137 138 159 158\n\t\tf 4 132 513 -153 -513\n\t\tmu 0 4 138 139 160 159\n\t\tf 4 133 514 -154 -514\n\t\tmu 0 4 139 140 161 160\n\t\tf 4 134 515 -155 -515\n\t\tmu 0 4 140 141 162 161\n\t\tf 4 135 516 -156 -516\n\t\tmu 0 4 141 142 163 162\n\t\tf 4 136 517 -157 -517\n\t\tmu 0 4 142 143 164 163\n\t\tf 4 137 518 -158 -518\n\t\tmu 0 4 143 144 165 164\n\t\tf 4 138 519 -159 -519\n\t\tmu 0 4 144 145 166 165\n\t\tf 4 139 500 -160 -520\n\t\tmu 0 4 145 146 167 166\n\t\tf 4 140 521 -161 -521\n\t\tmu 0 4 147 148 169 168\n\t\tf 4 141 522 -162 -522\n\t\tmu 0 4 148 149 170 169\n\t\tf 4 142 523 -163 -523\n\t\tmu 0 4 149 150 171 170\n\t\tf 4 143 524 -164 -524\n\t\tmu 0 4 150 151 172 171\n\t\tf 4 144 525 -165 -525\n\t\tmu 0 4 151 152 173 172\n\t\tf 4 145 526 -166 -526\n\t\tmu 0 4 152 153 174 173\n\t\tf 4 146 527 -167 -527\n\t\tmu 0 4 153 154 175 174\n\t\tf 4 147 528 -168 -528\n\t\tmu 0 4 154 155 176 175\n\t\tf 4 148 529 -169 -529\n\t\tmu 0 4 155 156 177 176\n\t\tf 4 149 530 -170 -530\n\t\tmu 0 4 156 157 178 177\n\t\tf 4 150 531 -171 -531\n\t\tmu 0 4 157 158 179 178\n\t\tf 4 151 532 -172 -532\n\t\tmu 0 4 158 159 180 179\n\t\tf 4 152 533 -173 -533\n\t\tmu 0 4 159 160 181 180\n\t\tf 4 153 534 -174 -534\n\t\tmu 0 4 160 161 182 181\n\t\tf 4 154 535 -175 -535\n\t\tmu 0 4 161 162 183 182\n\t\tf 4 155 536 -176 -536\n\t\tmu 0 4 162 163 184 183\n\t\tf 4 156 537 -177 -537\n\t\tmu 0 4 163 164 185 184\n\t\tf 4 157 538 -178 -538\n\t\tmu 0 4 164 165 186 185\n\t\tf 4 158 539 -179 -539\n\t\tmu 0 4 165 166 187 186\n\t\tf 4 159 520 -180 -540\n\t\tmu 0 4 166 167 188 187\n\t\tf 4 160 541 -181 -541\n\t\tmu 0 4 168 169 190 189\n\t\tf 4 161 542 -182 -542\n\t\tmu 0 4 169 170 191 190\n\t\tf 4 162 543 -183 -543\n\t\tmu 0 4 170 171 192 191\n\t\tf 4 163 544 -184 -544\n\t\tmu 0 4 171 172 193 192\n\t\tf 4 164 545 -185 -545\n\t\tmu 0 4 172 173 194 193\n\t\tf 4 165 546 -186 -546\n\t\tmu 0 4 173 174 195 194\n\t\tf 4 166 547 -187 -547\n\t\tmu 0 4 174 175 196 195\n\t\tf 4 167 548 -188 -548\n\t\tmu 0 4 175 176 197 196\n\t\tf 4 168 549 -189 -549\n\t\tmu 0 4 176 177 198 197\n\t\tf 4 169 550 -190 -550\n\t\tmu 0 4 177 178 199 198\n\t\tf 4 170 551 -191 -551\n\t\tmu 0 4 178 179 200 199\n\t\tf 4 171 552 -192 -552\n\t\tmu 0 4 179 180 201 200\n\t\tf 4 172 553 -193 -553\n\t\tmu 0 4 180 181 202 201\n\t\tf 4 173 554 -194 -554\n\t\tmu 0 4 181 182 203 202\n\t\tf 4 174 555 -195 -555\n\t\tmu 0 4 182 183 204 203\n\t\tf 4 175 556 -196 -556\n\t\tmu 0 4 183 184 205 204\n\t\tf 4 176 557 -197 -557\n\t\tmu 0 4 184 185 206 205\n\t\tf 4 177 558 -198 -558\n\t\tmu 0 4 185 186 207 206\n\t\tf 4 178 559 -199 -559\n\t\tmu 0 4 186 187 208 207\n\t\tf 4 179 540 -200 -560\n\t\tmu 0 4 187 188 209 208\n\t\tf 4 180 561 -201 -561\n\t\tmu 0 4 189 190 211 210\n\t\tf 4 181 562 -202 -562\n\t\tmu 0 4 190 191 212 211\n\t\tf 4 182 563 -203 -563\n\t\tmu 0 4 191 192 213 212\n\t\tf 4 183 564 -204 -564\n\t\tmu 0 4 192 193 214 213\n\t\tf 4 184 565 -205 -565\n\t\tmu 0 4 193 194 215 214\n\t\tf 4 185 566 -206 -566\n\t\tmu 0 4 194 195 216 215\n\t\tf 4 186 567 -207 -567\n\t\tmu 0 4 195 196 217 216\n\t\tf 4 187 568 -208 -568\n\t\tmu 0 4 196 197 218 217\n\t\tf 4 188 569 -209 -569\n\t\tmu 0 4 197 198 219 218\n\t\tf 4 189 570 -210 -570\n\t\tmu 0 4 198 199 220 219\n\t\tf 4 190 571 -211 -571\n\t\tmu 0 4 199 200 221 220\n\t\tf 4 191 572 -212 -572\n\t\tmu 0 4 200 201 222 221\n\t\tf 4 192 573 -213 -573\n\t\tmu 0 4 201 202 223 222\n\t\tf 4 193 574 -214 -574\n\t\tmu 0 4 202 203 224 223\n\t\tf 4 194 575 -215 -575\n\t\tmu 0 4 203 204 225 224\n\t\tf 4 195 576 -216 -576\n\t\tmu 0 4 204 205 226 225\n\t\tf 4 196 577 -217 -577\n\t\tmu 0 4 205 206 227 226\n\t\tf 4 197 578 -218 -578\n\t\tmu 0 4 206 207 228 227\n\t\tf 4 198 579 -219 -579\n\t\tmu 0 4 207 208 229 228\n\t\tf 4 199 560 -220 -580\n\t\tmu 0 4 208 209 230 229\n\t\tf 4 200 581 -221 -581\n\t\tmu 0 4 210 211 232 231\n\t\tf 4 201 582 -222 -582\n\t\tmu 0 4 211 212 233 232\n\t\tf 4 202 583 -223 -583\n\t\tmu 0 4 212 213 234 233\n\t\tf 4 203 584 -224 -584\n\t\tmu 0 4 213 214 235 234\n\t\tf 4 204 585 -225 -585\n\t\tmu 0 4 214 215 236 235\n\t\tf 4 205 586 -226 -586\n\t\tmu 0 4 215 216 237 236\n\t\tf 4 206 587 -227 -587\n\t\tmu 0 4 216 217 238 237\n\t\tf 4 207 588 -228 -588\n\t\tmu 0 4 217 218 239 238\n\t\tf 4 208 589 -229 -589\n\t\tmu 0 4 218 219 240 239\n\t\tf 4 209 590 -230 -590\n\t\tmu 0 4 219 220 241 240\n\t\tf 4 210 591 -231 -591\n\t\tmu 0 4 220 221 242 241\n\t\tf 4 211 592 -232 -592\n\t\tmu 0 4 221 222 243 242\n\t\tf 4 212 593 -233 -593\n\t\tmu 0 4 222 223 244 243\n\t\tf 4 213 594 -234 -594\n\t\tmu 0 4 223 224 245 244\n\t\tf 4 214 595 -235 -595\n\t\tmu 0 4 224 225 246 245\n\t\tf 4 215 596 -236 -596\n\t\tmu 0 4 225 226 247 246\n\t\tf 4 216 597 -237 -597\n\t\tmu 0 4 226 227 248 247\n\t\tf 4 217 598 -238 -598\n\t\tmu 0 4 227 228 249 248\n\t\tf 4 218 599 -239 -599\n\t\tmu 0 4 228 229 250 249\n\t\tf 4 219 580 -240 -600\n\t\tmu 0 4 229 230 251 250\n\t\tf 4 220 601 -241 -601\n\t\tmu 0 4 231 232 253 252\n\t\tf 4 221 602 -242 -602\n\t\tmu 0 4 232 233 254 253\n\t\tf 4 222 603 -243 -603\n\t\tmu 0 4 233 234 255 254\n\t\tf 4 223 604 -244 -604\n\t\tmu 0 4 234 235 256 255\n\t\tf 4 224 605 -245 -605\n\t\tmu 0 4 235 236 257 256\n\t\tf 4 225 606 -246 -606\n\t\tmu 0 4 236 237 258 257\n\t\tf 4 226 607 -247 -607\n\t\tmu 0 4 237 238 259 258\n\t\tf 4 227 608 -248 -608\n\t\tmu 0 4 238 239 260 259\n\t\tf 4 228 609 -249 -609\n\t\tmu 0 4 239 240 261 260\n\t\tf 4 229 610 -250 -610\n\t\tmu 0 4 240 241 262 261\n\t\tf 4 230 611 -251 -611\n\t\tmu 0 4 241 242 263 262\n\t\tf 4 231 612 -252 -612\n\t\tmu 0 4 242 243 264 263\n\t\tf 4 232 613 -253 -613\n\t\tmu 0 4 243 244 265 264\n\t\tf 4 233 614 -254 -614\n\t\tmu 0 4 244 245 266 265\n\t\tf 4 234 615 -255 -615\n\t\tmu 0 4 245 246 267 266\n\t\tf 4 235 616 -256 -616\n\t\tmu 0 4 246 247 268 267\n\t\tf 4 236 617 -257 -617\n\t\tmu 0 4 247 248 269 268\n\t\tf 4 237 618 -258 -618\n\t\tmu 0 4 248 249 270 269\n\t\tf 4 238 619 -259 -619\n\t\tmu 0 4 249 250 271 270\n\t\tf 4 239 600 -260 -620\n\t\tmu 0 4 250 251 272 271\n\t\tf 4 240 621 -261 -621\n\t\tmu 0 4 252 253 274 273\n\t\tf 4 241 622 -262 -622\n\t\tmu 0 4 253 254 275 274\n\t\tf 4 242 623 -263 -623\n\t\tmu 0 4 254 255 276 275\n\t\tf 4 243 624 -264 -624\n\t\tmu 0 4 255 256 277 276\n\t\tf 4 244 625 -265 -625\n\t\tmu 0 4 256 257 278 277\n\t\tf 4 245 626 -266 -626\n\t\tmu 0 4 257 258 279 278\n\t\tf 4 246 627 -267 -627\n\t\tmu 0 4 258 259 280 279\n\t\tf 4 247 628 -268 -628\n\t\tmu 0 4 259 260 281 280\n\t\tf 4 248 629 -269 -629\n\t\tmu 0 4 260 261 282 281\n\t\tf 4 249 630 -270 -630\n\t\tmu 0 4 261 262 283 282\n\t\tf 4 250 631 -271 -631\n\t\tmu 0 4 262 263 284 283\n\t\tf 4 251 632 -272 -632\n\t\tmu 0 4 263 264 285 284\n\t\tf 4 252 633 -273 -633\n\t\tmu 0 4 264 265 286 285\n\t\tf 4 253 634 -274 -634\n\t\tmu 0 4 265 266 287 286\n\t\tf 4 254 635 -275 -635\n\t\tmu 0 4 266 267 288 287\n\t\tf 4 255 636 -276 -636\n\t\tmu 0 4 267 268 289 288\n\t\tf 4 256 637 -277 -637\n\t\tmu 0 4 268 269 290 289\n\t\tf 4 257 638 -278 -638\n\t\tmu 0 4 269 270 291 290\n\t\tf 4 258 639 -279 -639\n\t\tmu 0 4 270 271 292 291\n\t\tf 4 259 620 -280 -640\n\t\tmu 0 4 271 272 293 292\n\t\tf 4 260 641 -281 -641\n\t\tmu 0 4 273 274 295 294\n\t\tf 4 261 642 -282 -642\n\t\tmu 0 4 274 275 296 295\n\t\tf 4 262 643 -283 -643\n\t\tmu 0 4 275 276 297 296\n\t\tf 4 263 644 -284 -644\n\t\tmu 0 4 276 277 298 297\n\t\tf 4 264 645 -285 -645\n\t\tmu 0 4 277 278 299 298\n\t\tf 4 265 646 -286 -646\n\t\tmu 0 4 278 279 300 299\n\t\tf 4 266 647 -287 -647\n\t\tmu 0 4 279 280 301 300\n\t\tf 4 267 648 -288 -648\n\t\tmu 0 4 280 281 302 301\n\t\tf 4 268 649 -289 -649\n\t\tmu 0 4 281 282 303 302\n\t\tf 4 269 650 -290 -650\n\t\tmu 0 4 282 283 304 303\n\t\tf 4 270 651 -291 -651\n\t\tmu 0 4 283 284 305 304\n\t\tf 4 271 652 -292 -652\n\t\tmu 0 4 284 285 306 305\n\t\tf 4 272 653 -293 -653\n\t\tmu 0 4 285 286 307 306\n\t\tf 4 273 654 -294 -654\n\t\tmu 0 4 286 287 308 307\n\t\tf 4 274 655 -295 -655\n\t\tmu 0 4 287 288 309 308\n\t\tf 4 275 656 -296 -656\n\t\tmu 0 4 288 289 310 309\n\t\tf 4 276 657 -297 -657\n\t\tmu 0 4 289 290 311 310\n\t\tf 4 277 658 -298 -658\n\t\tmu 0 4 290 291 312 311\n\t\tf 4 278 659 -299 -659\n\t\tmu 0 4 291 292 313 312\n\t\tf 4 279 640 -300 -660\n\t\tmu 0 4 292 293 314 313\n\t\tf 4 280 661 -301 -661\n\t\tmu 0 4 294 295 316 315\n\t\tf 4 281 662 -302 -662\n\t\tmu 0 4 295 296 317 316\n\t\tf 4 282 663 -303 -663\n\t\tmu 0 4 296 297 318 317\n\t\tf 4 283 664 -304 -664\n\t\tmu 0 4 297 298 319 318\n\t\tf 4 284 665 -305 -665\n\t\tmu 0 4 298 299 320 319\n\t\tf 4 285 666 -306 -666\n\t\tmu 0 4 299 300 321 320\n\t\tf 4 286 667 -307 -667\n\t\tmu 0 4 300 301 322 321\n\t\tf 4 287 668 -308 -668\n\t\tmu 0 4 301 302 323 322\n\t\tf 4 288 669 -309 -669\n\t\tmu 0 4 302 303 324 323\n\t\tf 4 289 670 -310 -670\n\t\tmu 0 4 303 304 325 324\n\t\tf 4 290 671 -311 -671\n\t\tmu 0 4 304 305 326 325\n\t\tf 4 291 672 -312 -672\n\t\tmu 0 4 305 306 327 326\n\t\tf 4 292 673 -313 -673\n\t\tmu 0 4 306 307 328 327\n\t\tf 4 293 674 -314 -674\n\t\tmu 0 4 307 308 329 328\n\t\tf 4 294 675 -315 -675\n\t\tmu 0 4 308 309 330 329\n\t\tf 4 295 676 -316 -676\n\t\tmu 0 4 309 310 331 330\n\t\tf 4 296 677 -317 -677\n\t\tmu 0 4 310 311 332 331\n\t\tf 4 297 678 -318 -678\n\t\tmu 0 4 311 312 333 332\n\t\tf 4 298 679 -319 -679\n\t\tmu 0 4 312 313 334 333\n\t\tf 4 299 660 -320 -680\n\t\tmu 0 4 313 314 335 334\n\t\tf 4 300 681 -321 -681\n\t\tmu 0 4 315 316 337 336\n\t\tf 4 301 682 -322 -682\n\t\tmu 0 4 316 317 338 337\n\t\tf 4 302 683 -323 -683\n\t\tmu 0 4 317 318 339 338\n\t\tf 4 303 684 -324 -684\n\t\tmu 0 4 318 319 340 339\n\t\tf 4 304 685 -325 -685\n\t\tmu 0 4 319 320 341 340\n\t\tf 4 305 686 -326 -686\n\t\tmu 0 4 320 321 342 341\n\t\tf 4 306 687 -327 -687\n\t\tmu 0 4 321 322 343 342\n\t\tf 4 307 688 -328 -688\n\t\tmu 0 4 322 323 344 343\n\t\tf 4 308 689 -329 -689\n\t\tmu 0 4 323 324 345 344\n\t\tf 4 309 690 -330 -690\n\t\tmu 0 4 324 325 346 345\n\t\tf 4 310 691 -331 -691\n\t\tmu 0 4 325 326 347 346\n\t\tf 4 311 692 -332 -692\n\t\tmu 0 4 326 327 348 347\n\t\tf 4 312 693 -333 -693\n\t\tmu 0 4 327 328 349 348\n\t\tf 4 313 694 -334 -694\n\t\tmu 0 4 328 329 350 349\n\t\tf 4 314 695 -335 -695\n\t\tmu 0 4 329 330 351 350\n\t\tf 4 315 696 -336 -696\n\t\tmu 0 4 330 331 352 351\n\t\tf 4 316 697 -337 -697\n\t\tmu 0 4 331 332 353 352\n\t\tf 4 317 698 -338 -698\n\t\tmu 0 4 332 333 354 353\n\t\tf 4 318 699 -339 -699\n\t\tmu 0 4 333 334 355 354\n\t\tf 4 319 680 -340 -700\n\t\tmu 0 4 334 335 356 355\n\t\tf 4 320 701 -341 -701\n\t\tmu 0 4 336 337 358 357\n\t\tf 4 321 702 -342 -702\n\t\tmu 0 4 337 338 359 358\n\t\tf 4 322 703 -343 -703\n\t\tmu 0 4 338 339 360 359\n\t\tf 4 323 704 -344 -704\n\t\tmu 0 4 339 340 361 360\n\t\tf 4 324 705 -345 -705\n\t\tmu 0 4 340 341 362 361\n\t\tf 4 325 706 -346 -706\n\t\tmu 0 4 341 342 363 362\n\t\tf 4 326 707 -347 -707\n\t\tmu 0 4 342 343 364 363\n\t\tf 4 327 708 -348 -708\n\t\tmu 0 4 343 344 365 364\n\t\tf 4 328 709 -349 -709\n\t\tmu 0 4 344 345 366 365\n\t\tf 4 329 710 -350 -710\n\t\tmu 0 4 345 346 367 366\n\t\tf 4 330 711 -351 -711\n\t\tmu 0 4 346 347 368 367\n\t\tf 4 331 712 -352 -712\n\t\tmu 0 4 347 348 369 368\n\t\tf 4 332 713 -353 -713\n\t\tmu 0 4 348 349 370 369\n\t\tf 4 333 714 -354 -714\n\t\tmu 0 4 349 350 371 370\n\t\tf 4 334 715 -355 -715\n\t\tmu 0 4 350 351 372 371\n\t\tf 4 335 716 -356 -716\n\t\tmu 0 4 351 352 373 372\n\t\tf 4 336 717 -357 -717\n\t\tmu 0 4 352 353 374 373\n\t\tf 4 337 718 -358 -718\n\t\tmu 0 4 353 354 375 374\n\t\tf 4 338 719 -359 -719\n\t\tmu 0 4 354 355 376 375\n\t\tf 4 339 700 -360 -720\n\t\tmu 0 4 355 356 377 376\n\t\tf 4 340 721 -361 -721\n\t\tmu 0 4 357 358 379 378\n\t\tf 4 341 722 -362 -722\n\t\tmu 0 4 358 359 380 379\n\t\tf 4 342 723 -363 -723\n\t\tmu 0 4 359 360 381 380\n\t\tf 4 343 724 -364 -724\n\t\tmu 0 4 360 361 382 381\n\t\tf 4 344 725 -365 -725\n\t\tmu 0 4 361 362 383 382\n\t\tf 4 345 726 -366 -726\n\t\tmu 0 4 362 363 384 383\n\t\tf 4 346 727 -367 -727\n\t\tmu 0 4 363 364 385 384\n\t\tf 4 347 728 -368 -728\n\t\tmu 0 4 364 365 386 385\n\t\tf 4 348 729 -369 -729\n\t\tmu 0 4 365 366 387 386\n\t\tf 4 349 730 -370 -730\n\t\tmu 0 4 366 367 388 387\n\t\tf 4 350 731 -371 -731\n\t\tmu 0 4 367 368 389 388\n\t\tf 4 351 732 -372 -732\n\t\tmu 0 4 368 369 390 389\n\t\tf 4 352 733 -373 -733\n\t\tmu 0 4 369 370 391 390\n\t\tf 4 353 734 -374 -734\n\t\tmu 0 4 370 371 392 391\n\t\tf 4 354 735 -375 -735\n\t\tmu 0 4 371 372 393 392\n\t\tf 4 355 736 -376 -736\n\t\tmu 0 4 372 373 394 393\n\t\tf 4 356 737 -377 -737\n\t\tmu 0 4 373 374 395 394\n\t\tf 4 357 738 -378 -738\n\t\tmu 0 4 374 375 396 395\n\t\tf 4 358 739 -379 -739\n\t\tmu 0 4 375 376 397 396\n\t\tf 4 359 720 -380 -740\n\t\tmu 0 4 376 377 398 397\n\t\tf 3 -1 -741 741\n\t\tmu 0 3 1 0 399\n\t\tf 3 -2 -742 742\n\t\tmu 0 3 2 1 400\n\t\tf 3 -3 -743 743\n\t\tmu 0 3 3 2 401\n\t\tf 3 -4 -744 744\n\t\tmu 0 3 4 3 402\n\t\tf 3 -5 -745 745\n\t\tmu 0 3 5 4 403\n\t\tf 3 -6 -746 746\n\t\tmu 0 3 6 5 404\n\t\tf 3 -7 -747 747\n\t\tmu 0 3 7 6 405\n\t\tf 3 -8 -748 748\n\t\tmu 0 3 8 7 406\n\t\tf 3 -9 -749 749\n\t\tmu 0 3 9 8 407\n\t\tf 3 -10 -750 750\n\t\tmu 0 3 10 9 408\n\t\tf 3 -11 -751 751\n\t\tmu 0 3 11 10 409\n\t\tf 3 -12 -752 752\n\t\tmu 0 3 12 11 410\n\t\tf 3 -13 -753 753\n\t\tmu 0 3 13 12 411\n\t\tf 3 -14 -754 754\n\t\tmu 0 3 14 13 412\n\t\tf 3 -15 -755 755\n\t\tmu 0 3 15 14 413\n\t\tf 3 -16 -756 756\n\t\tmu 0 3 16 15 414\n\t\tf 3 -17 -757 757\n\t\tmu 0 3 17 16 415\n\t\tf 3 -18 -758 758\n\t\tmu 0 3 18 17 416\n\t\tf 3 -19 -759 759\n\t\tmu 0 3 19 18 417\n\t\tf 3 -20 -760 740\n\t\tmu 0 3 20 19 418\n\t\tf 3 360 761 -761\n\t\tmu 0 3 378 379 419\n\t\tf 3 361 762 -762\n\t\tmu 0 3 379 380 420\n\t\tf 3 362 763 -763\n\t\tmu 0 3 380 381 421\n\t\tf 3 363 764 -764\n\t\tmu 0 3 381 382 422\n\t\tf 3 364 765 -765\n\t\tmu 0 3 382 383 423\n\t\tf 3 365 766 -766\n\t\tmu 0 3 383 384 424\n\t\tf 3 366 767 -767\n\t\tmu 0 3 384 385 425\n\t\tf 3 367 768 -768\n\t\tmu 0 3 385 386 426\n\t\tf 3 368 769 -769\n\t\tmu 0 3 386 387 427\n\t\tf 3 369 770 -770\n\t\tmu 0 3 387 388 428\n\t\tf 3 370 771 -771\n\t\tmu 0 3 388 389 429\n\t\tf 3 371 772 -772\n\t\tmu 0 3 389 390 430\n\t\tf 3 372 773 -773\n\t\tmu 0 3 390 391 431\n\t\tf 3 373 774 -774\n\t\tmu 0 3 391 392 432\n\t\tf 3 374 775 -775\n\t\tmu 0 3 392 393 433\n\t\tf 3 375 776 -776\n\t\tmu 0 3 393 394 434\n\t\tf 3 376 777 -777\n\t\tmu 0 3 394 395 435\n\t\tf 3 377 778 -778\n\t\tmu 0 3 395 396 436\n\t\tf 3 378 779 -779\n\t\tmu 0 3 396 397 437\n\t\tf 3 379 760 -780\n\t\tmu 0 3 397 398 438;\n\tsetAttr \".cd\" -type \"dataPolyComponent\" Index_Data Edge 0 ;\n\tsetAttr \".cvd\" -type \"dataPolyComponent\" Index_Data Vertex 0 ;\n\tsetAttr \".pd[0]\" -type \"dataPolyComponent\" Index_Data UV 0 ;\n\tsetAttr \".hfd\" -type \"dataPolyComponent\" Index_Data Face 0 ;\n\tsetAttr \".dr\" 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:bf8e49bb98ec\";\nselect -ne :time1;\n\tsetAttr \".o\" 1001;\n\tsetAttr \".unw\" 1001;\nselect -ne :hardwareRenderingGlobals;\n\tsetAttr \".otfna\" -type \"stringArray\" 22 \"NURBS Curves\" \"NURBS Surfaces\" \"Polygons\" \"Subdiv Surface\" \"Particles\" \"Particle Instance\" \"Fluids\" \"Strokes\" \"Image Planes\" \"UI\" \"Lights\" \"Cameras\" \"Locators\" \"Joints\" \"IK Handles\" \"Deformers\" \"Motion Trails\" \"Components\" \"Hair Systems\" \"Follicles\" \"Misc. UI\" \"Ornaments\"  ;\n\tsetAttr \".otfva\" -type \"Int32Array\" 22 0 1 1 1 1 1\n\t\t 1 1 1 0 0 0 0 0 0 0 0 0\n\t\t 0 0 0 0 ;\n\tsetAttr \".fprt\" yes;\nselect -ne :renderPartition;\n\tsetAttr -s 2 \".st\";\nselect -ne :renderGlobalsList1;\nselect -ne :defaultShaderList1;\n\tsetAttr -s 4 \".s\";\nselect -ne :postProcessList1;\n\tsetAttr -s 2 \".p\";\nselect -ne :defaultRenderingList1;\nselect -ne :initialShadingGroup;\n\tsetAttr \".ro\" yes;\nselect -ne :initialParticleSE;\n\tsetAttr \".ro\" yes;\nselect -ne :defaultRenderGlobals;\n\tsetAttr \".ren\" -type \"string\" \"arnold\";\n\tsetAttr \".fs\" 1001;\n\tsetAttr \".ef\" 1001;\nselect -ne :defaultResolution;\n\tsetAttr \".w\" 1920;\n\tsetAttr \".h\" 1080;\n\tsetAttr \".pa\" 1;\nselect -ne :hardwareRenderGlobals;\n\tsetAttr \".ctrs\" 256;\n\tsetAttr \".btrs\" 512;\nconnectAttr \"pSphere1_GEOShape1.iog\" \":initialShadingGroup.dsm\" -na;\n// End of modelMain.ma\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/publish/workfile/workfileTest_task/v001/test_project_test_asset_workfileTest_task_v001.ma",
    "content": "//Maya ASCII 2023 scene\n//Name: test_project_test_asset_test_task_v002.ma\n//Last modified: Thu, Dec 07, 2023 03:53:06 PM\n//Codeset: 1252\nrequires maya \"2023\";\nrequires -nodeType \"simpleSelector\" -nodeType \"renderSetupLayer\" -nodeType \"renderSetup\"\n\t\t -nodeType \"collection\" \"renderSetup.py\" \"1.0\";\nrequires \"stereoCamera\" \"10.0\";\nrequires -nodeType \"aiOptions\" -nodeType \"aiAOVDriver\" -nodeType \"aiAOVFilter\" \"mtoa\" \"5.2.1.1\";\nrequires -nodeType \"polyDisc\" \"modelingToolkit\" \"0.0.0.0\";\nrequires \"stereoCamera\" \"10.0\";\ncurrentUnit -l centimeter -a degree -t pal;\nfileInfo \"application\" \"maya\";\nfileInfo \"product\" \"Maya 2023\";\nfileInfo \"version\" \"2023\";\nfileInfo \"cutIdentifier\" \"202211021031-847a9f9623\";\nfileInfo \"osv\" \"Windows 10 Pro v2009 (Build: 19045)\";\nfileInfo \"license\" \"education\";\nfileInfo \"UUID\" \"7A992745-4AD5-777F-5575-B4BFAC62B1D0\";\nfileInfo \"OpenPypeContext\" \"eyJwdWJsaXNoX2F0dHJpYnV0ZXMiOiB7IlZhbGlkYXRlQ29udGFpbmVycyI6IHsiYWN0aXZlIjogdHJ1ZX19fQ==\";\ncreateNode transform -s -n \"persp\";\n\trename -uid \"D52C935B-47C9-D868-A875-D799DD17B3A1\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 26.953352922736947 24.362683487437064 26.983403389430531 ;\n\tsetAttr \".r\" -type \"double3\" 1064.7698975365424 54.173034578109736 1075.1660763544442 ;\n\tsetAttr \".rp\" -type \"double3\" -2.0401242849359917e-14 2.2609331405046354e-14 -44.821869662029947 ;\n\tsetAttr \".rpt\" -type \"double3\" -27.999999999999989 -21.000000000000025 16.82186966202995 ;\ncreateNode camera -s -n \"perspShape\" -p \"persp\";\n\trename -uid \"2399E6C0-490F-BA1F-485F-5AA8A01D27BC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 46.895362757145833;\n\tsetAttr \".imn\" -type \"string\" \"persp\";\n\tsetAttr \".den\" -type \"string\" \"persp_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".ai_translator\" -type \"string\" \"perspective\";\ncreateNode transform -s -n \"top\";\n\trename -uid \"415C7426-413E-0FAE-FCC3-3DAED7443A52\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 1000.1 0 ;\n\tsetAttr \".r\" -type \"double3\" 90 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" 0 -1000.1 1000.1 ;\ncreateNode camera -s -n \"topShape\" -p \"top\";\n\trename -uid \"3BD0CF60-40DB-5278-5D8B-06ACBDA32122\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"top\";\n\tsetAttr \".den\" -type \"string\" \"top_depth\";\n\tsetAttr \".man\" -type \"string\" \"top_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -t %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"front\";\n\trename -uid \"D83DD5CE-4FE0-AB1B-81B2-87A63F0B7A05\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 0 1000.1 ;\n\tsetAttr \".r\" -type \"double3\" 180 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\ncreateNode camera -s -n \"frontShape\" -p \"front\";\n\trename -uid \"23313CBA-42C2-0B3A-0FCF-EA965EAC5DEC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"front\";\n\tsetAttr \".den\" -type \"string\" \"front_depth\";\n\tsetAttr \".man\" -type \"string\" \"front_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -f %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"side\";\n\trename -uid \"F70F692C-4A0D-BE64-9EE4-A99B6FA2D56E\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 1000.1 0 0 ;\n\tsetAttr \".r\" -type \"double3\" 180 -90 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" -1000.1 0 1000.1 ;\ncreateNode camera -s -n \"sideShape\" -p \"side\";\n\trename -uid \"C05669C3-420E-CA11-E5FC-7EB64EF8B632\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"side\";\n\tsetAttr \".den\" -type \"string\" \"side_depth\";\n\tsetAttr \".man\" -type \"string\" \"side_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -s %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -n \"pSphere1_GEO\";\n\trename -uid \"7445A43F-444F-B2D3-4315-2AA013D2E0B6\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:440654b3dfe4\";\ncreateNode mesh -n \"pSphere1_GEOShape1\" -p \"pSphere1_GEO\";\n\trename -uid \"7C731260-45C6-339E-07BF-359446B08EA1\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:bf8e49bb98ec\";\ncreateNode transform -n \"pDisc1\";\n\trename -uid \"DED70CCF-4C19-16E4-9E5D-66A05037BA47\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:90e762703f08\";\ncreateNode mesh -n \"pDiscShape1\" -p \"pDisc1\";\n\trename -uid \"E1FCDCCF-4DE1-D3B9-C4F8-3285F1CF5B25\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:4ee3da11a1a4\";\ncreateNode transform -n \"persp1\";\n\trename -uid \"292F1351-4E41-A890-D6D5-A5A4F7D94C76\";\n\tsetAttr \".t\" -type \"double3\" 3.7889010960863949 2.8416759114717678 3.7889010364817537 ;\n\tsetAttr \".r\" -type \"double3\" -27.938352729602379 44.999999999999972 -5.172681101354183e-14 ;\ncreateNode camera -n \"perspShape1\" -p \"persp1\";\n\trename -uid \"9277418C-43C8-5064-A7C6-64AC829A76F2\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".ovr\" 1.3;\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 6.0652013012246453;\n\tsetAttr \".imn\" -type \"string\" \"persp1\";\n\tsetAttr \".den\" -type \"string\" \"persp1_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp1_mask\";\n\tsetAttr \".tp\" -type \"double3\" -1.1920928955078125e-07 0 -1.7881393432617188e-07 ;\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".dr\" yes;\ncreateNode lightLinker -s -n \"lightLinker1\";\n\trename -uid \"09465BD3-42E5-18E4-7906-20A99BB2A6C0\";\n\tsetAttr -s 2 \".lnk\";\n\tsetAttr -s 2 \".slnk\";\ncreateNode shapeEditorManager -n \"shapeEditorManager\";\n\trename -uid \"9F2E8009-4D69-046B-FCC4-28A8CE8F86DB\";\ncreateNode poseInterpolatorManager -n \"poseInterpolatorManager\";\n\trename -uid \"6757AD81-40B0-A747-69C3-D9A56259571E\";\ncreateNode displayLayerManager -n \"layerManager\";\n\trename -uid \"6F055ED5-4D91-8F85-7951-B4A13543A561\";\ncreateNode displayLayer -n \"defaultLayer\";\n\trename -uid \"4A776D1B-401F-7069-1C74-A7AAE84CEE03\";\n\tsetAttr \".ufem\" -type \"stringArray\" 0  ;\ncreateNode renderLayerManager -n \"renderLayerManager\";\n\trename -uid \"55626D2B-4FD5-61A1-7AB2-47B13F19D8AA\";\n\tsetAttr -s 2 \".rlmi[1]\"  1;\n\tsetAttr -s 2 \".rlmi\";\ncreateNode renderLayer -n \"defaultRenderLayer\";\n\trename -uid \"B134920D-4508-23BD-A6CA-11B43DE03F53\";\n\tsetAttr \".g\" yes;\ncreateNode renderSetup -n \"renderSetup\";\n\trename -uid \"9A8F0D15-41AB-CA70-C2D8-B78840BF9BC1\";\ncreateNode polySphere -n \"polySphere1\";\n\trename -uid \"DA319706-4ACF-B15C-53B2-48AC80D202EA\";\ncreateNode objectSet -n \"modelMain\";\n\trename -uid \"A76AD4F8-4CF5-AA0D-4E98-BABEE6454CC3\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"instance_id\" -ln \"instance_id\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"writeColorSets\" -ln \"writeColorSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"writeFaceSets\" -ln \"writeFaceSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"includeParentHierarchy\" -ln \"includeParentHierarchy\" -min \n\t\t0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"attr\" -ln \"attr\" -dt \"string\";\n\taddAttr -ci true -sn \"attrPrefix\" -ln \"attrPrefix\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\" \n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:7364ea6776c9\";\n\tsetAttr \".instance_id\" -type \"string\" \"6889d3db-b813-43db-96de-9ba555dc4472\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"model\";\n\tsetAttr \".subset\" -type \"string\" \"modelMain\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.model\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr -cb on \".writeColorSets\";\n\tsetAttr -cb on \".writeFaceSets\";\n\tsetAttr -cb on \".includeParentHierarchy\";\n\tsetAttr \".attr\" -type \"string\" \"\";\n\tsetAttr \".attrPrefix\" -type \"string\" \"\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateNodeIDsRelated\\\": {\\\"active\\\": true}, \\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ValidateTransformNamingSuffix\\\": {\\\"active\\\": true}, \\\"ValidateColorSets\\\": {\\\"active\\\": true}, \\\"ValidateMeshHasUVs\\\": {\\\"active\\\": true}, \\\"ValidateMeshNonZeroEdgeLength\\\": {\\\"active\\\": true}, \\\"ExtractModel\\\": {\\\"active\\\": true}, \\\"ValidateMeshArnoldAttributes\\\": {\\\"active\\\": true}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"writeColorSets,writeFaceSets,includeParentHierarchy,attr,attrPrefix\";\ncreateNode script -n \"uiConfigurationScriptNode\";\n\trename -uid \"4B7AFB53-452E-E870-63E1-CCA1DD6EAF13\";\n\tsetAttr \".b\" -type \"string\" (\n\t\t\"// Maya Mel UI Configuration File.\\n//\\n//  This script is machine generated.  Edit at your own risk.\\n//\\n//\\n\\nglobal string $gMainPane;\\nif (`paneLayout -exists $gMainPane`) {\\n\\n\\tglobal int $gUseScenePanelConfig;\\n\\tint    $useSceneConfig = $gUseScenePanelConfig;\\n\\tint    $nodeEditorPanelVisible = stringArrayContains(\\\"nodeEditorPanel1\\\", `getPanel -vis`);\\n\\tint    $nodeEditorWorkspaceControlOpen = (`workspaceControl -exists nodeEditorPanel1Window` && `workspaceControl -q -visible nodeEditorPanel1Window`);\\n\\tint    $menusOkayInPanels = `optionVar -q allowMenusInPanels`;\\n\\tint    $nVisPanes = `paneLayout -q -nvp $gMainPane`;\\n\\tint    $nPanes = 0;\\n\\tstring $editorName;\\n\\tstring $panelName;\\n\\tstring $itemFilterName;\\n\\tstring $panelConfig;\\n\\n\\t//\\n\\t//  get current state of the UI\\n\\t//\\n\\tsceneUIReplacement -update $gMainPane;\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Top View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Top View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|top\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n\"\n\t\t+ \"            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n\"\n\t\t+ \"            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Side View\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Side View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|side\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n\"\n\t\t+ \"            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n\"\n\t\t+ \"            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n\"\n\t\t+ \"        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Front View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Front View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|front\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n\"\n\t\t+ \"            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n\"\n\t\t+ \"            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n\"\n\t\t+ \"            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Persp View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Persp View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|persp\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n\"\n\t\t+ \"            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n\"\n\t\t+ \"            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n\"\n\t\t+ \"            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"ToggledOutliner\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"ToggledOutliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -docTag \\\"isolOutln_fromSeln\\\" \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 1\\n            -showReferenceMembers 1\\n\"\n\t\t+ \"            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n\"\n\t\t+ \"            -isSet 0\\n            -isSetMember 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -selectCommand \\\"print(\\\\\\\"\\\\\\\")\\\" \\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n            -renderFilterIndex 0\\n            -selectionOrder \\\"chronological\\\" \\n            -expandAttribute 0\\n            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"Outliner\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"Outliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 0\\n            -showReferenceMembers 0\\n            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n\"\n\t\t+ \"            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n\"\n\t\t+ \"            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"graphEditor\\\" (localizedPanelLabel(\\\"Graph Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Graph Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 1\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n\"\n\t\t+ \"                -showPublishedAsConnected 0\\n                -showParentContainers 0\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 1\\n                -showCompounds 0\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 1\\n                -doNotSelectNewObjects 0\\n                -dropIsParent 1\\n                -transmitFilters 1\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n\"\n\t\t+ \"                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 1\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"GraphEd\\\");\\n            animCurveEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -showPlayRangeShades \\\"on\\\" \\n                -lockPlayRangeShades \\\"off\\\" \\n                -smoothness \\\"fine\\\" \\n                -resultSamples 1.041667\\n                -resultScreenSamples 0\\n                -resultUpdate \\\"delayed\\\" \\n                -showUpstreamCurves 1\\n                -keyMinScale 1\\n                -stackedCurvesMin -1\\n                -stackedCurvesMax 1\\n\"\n\t\t+ \"                -stackedCurvesSpace 0.2\\n                -preSelectionHighlight 0\\n                -constrainDrag 0\\n                -valueLinesToggle 1\\n                -highlightAffectedCurves 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dopeSheetPanel\\\" (localizedPanelLabel(\\\"Dope Sheet\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dope Sheet\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n\"\n\t\t+ \"                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 0\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n                -showPublishedAsConnected 0\\n                -showParentContainers 0\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 0\\n                -showCompounds 1\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 0\\n                -doNotSelectNewObjects 1\\n                -dropIsParent 1\\n                -transmitFilters 0\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n\"\n\t\t+ \"                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 0\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"DopeSheetEd\\\");\\n            dopeSheetEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -outliner \\\"dopeSheetPanel1OutlineEd\\\" \\n                -showSummary 1\\n                -showScene 0\\n                -hierarchyBelow 0\\n\"\n\t\t+ \"                -showTicks 1\\n                -selectionWindow 0 0 0 0 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"timeEditorPanel\\\" (localizedPanelLabel(\\\"Time Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Time Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"clipEditorPanel\\\" (localizedPanelLabel(\\\"Trax Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Trax Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = clipEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 0 \\n\"\n\t\t+ \"                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"sequenceEditorPanel\\\" (localizedPanelLabel(\\\"Camera Sequencer\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Camera Sequencer\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = sequenceEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 1 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperGraphPanel\\\" (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\n\\t\\t\\t$editorName = ($panelName+\\\"HyperGraphEd\\\");\\n            hyperGraph -e \\n                -graphLayoutStyle \\\"hierarchicalLayout\\\" \\n                -orientation \\\"horiz\\\" \\n                -mergeConnections 0\\n                -zoom 1\\n                -animateTransition 0\\n                -showRelationships 1\\n                -showShapes 0\\n                -showDeformers 0\\n                -showExpressions 0\\n                -showConstraints 0\\n                -showConnectionFromSelected 0\\n                -showConnectionToSelected 0\\n                -showConstraintLabels 0\\n                -showUnderworld 0\\n                -showInvisible 0\\n                -transitionFrames 1\\n                -opaqueContainers 0\\n                -freeform 0\\n                -imagePosition 0 0 \\n                -imageScale 1\\n                -imageEnabled 0\\n                -graphType \\\"DAG\\\" \\n                -heatMapDisplay 0\\n                -updateSelection 1\\n                -updateNodeAdded 1\\n                -useDrawOverrideColor 0\\n                -limitGraphTraversal -1\\n\"\n\t\t+ \"                -range 0 0 \\n                -iconSize \\\"smallIcons\\\" \\n                -showCachedConnections 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperShadePanel\\\" (localizedPanelLabel(\\\"Hypershade\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypershade\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"visorPanel\\\" (localizedPanelLabel(\\\"Visor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Visor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"nodeEditorPanel\\\" (localizedPanelLabel(\\\"Node Editor\\\")) `;\\n\\tif ($nodeEditorPanelVisible || $nodeEditorWorkspaceControlOpen) {\\n\"\n\t\t+ \"\\t\\tif (\\\"\\\" == $panelName) {\\n\\t\\t\\tif ($useSceneConfig) {\\n\\t\\t\\t\\t$panelName = `scriptedPanel -unParent  -type \\\"nodeEditorPanel\\\" -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels `;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n\"\n\t\t+ \"                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\t}\\n\\t\\t} else {\\n\\t\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n\"\n\t\t+ \"                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"createNodePanel\\\" (localizedPanelLabel(\\\"Create Node\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Create Node\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"polyTexturePlacementPanel\\\" (localizedPanelLabel(\\\"UV Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"UV Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"renderWindowPanel\\\" (localizedPanelLabel(\\\"Render View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Render View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"shapePanel\\\" (localizedPanelLabel(\\\"Shape Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tshapePanel -edit -l (localizedPanelLabel(\\\"Shape Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"posePanel\\\" (localizedPanelLabel(\\\"Pose Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tposePanel -edit -l (localizedPanelLabel(\\\"Pose Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynRelEdPanel\\\" (localizedPanelLabel(\\\"Dynamic Relationships\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dynamic Relationships\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"relationshipPanel\\\" (localizedPanelLabel(\\\"Relationship Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Relationship Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"referenceEditorPanel\\\" (localizedPanelLabel(\\\"Reference Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Reference Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynPaintScriptedPanelType\\\" (localizedPanelLabel(\\\"Paint Effects\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Paint Effects\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"scriptEditorPanel\\\" (localizedPanelLabel(\\\"Script Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Script Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"profilerPanel\\\" (localizedPanelLabel(\\\"Profiler Tool\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Profiler Tool\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"contentBrowserPanel\\\" (localizedPanelLabel(\\\"Content Browser\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Content Browser\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"Stereo\\\" (localizedPanelLabel(\\\"Stereo\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Stereo\\\")) -mbv $menusOkayInPanels  $panelName;\\n{ string $editorName = ($panelName+\\\"Editor\\\");\\n            stereoCameraView -e \\n                -camera \\\"|persp\\\" \\n                -useInteractiveMode 0\\n                -displayLights \\\"default\\\" \\n                -displayAppearance \\\"wireframe\\\" \\n                -activeOnly 0\\n                -ignorePanZoom 0\\n                -wireframeOnShaded 0\\n                -headsUpDisplay 1\\n                -holdOuts 1\\n                -selectionHiliteDisplay 1\\n                -useDefaultMaterial 0\\n                -bufferMode \\\"double\\\" \\n                -twoSidedLighting 1\\n                -backfaceCulling 0\\n                -xray 0\\n                -jointXray 0\\n                -activeComponentsXray 0\\n                -displayTextures 0\\n                -smoothWireframe 0\\n                -lineWidth 1\\n                -textureAnisotropic 0\\n                -textureHilight 1\\n                -textureSampling 2\\n\"\n\t\t+ \"                -textureDisplay \\\"modulate\\\" \\n                -textureMaxSize 32768\\n                -fogging 0\\n                -fogSource \\\"fragment\\\" \\n                -fogMode \\\"linear\\\" \\n                -fogStart 0\\n                -fogEnd 100\\n                -fogDensity 0.1\\n                -fogColor 0.5 0.5 0.5 1 \\n                -depthOfFieldPreview 1\\n                -maxConstantTransparency 1\\n                -objectFilterShowInHUD 1\\n                -isFiltered 0\\n                -colorResolution 4 4 \\n                -bumpResolution 4 4 \\n                -textureCompression 0\\n                -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n                -transpInShadows 0\\n                -cullingOverride \\\"none\\\" \\n                -lowQualityLighting 0\\n                -maximumNumHardwareLights 0\\n                -occlusionCulling 0\\n                -shadingModel 0\\n                -useBaseRenderer 0\\n                -useReducedRenderer 0\\n                -smallObjectCulling 0\\n                -smallObjectThreshold -1 \\n                -interactiveDisableShadows 0\\n\"\n\t\t+ \"                -interactiveBackFaceCull 0\\n                -sortTransparent 1\\n                -controllers 1\\n                -nurbsCurves 1\\n                -nurbsSurfaces 1\\n                -polymeshes 1\\n                -subdivSurfaces 1\\n                -planes 1\\n                -lights 1\\n                -cameras 1\\n                -controlVertices 1\\n                -hulls 1\\n                -grid 1\\n                -imagePlane 1\\n                -joints 1\\n                -ikHandles 1\\n                -deformers 1\\n                -dynamics 1\\n                -particleInstancers 1\\n                -fluids 1\\n                -hairSystems 1\\n                -follicles 1\\n                -nCloths 1\\n                -nParticles 1\\n                -nRigids 1\\n                -dynamicConstraints 1\\n                -locators 1\\n                -manipulators 1\\n                -pluginShapes 1\\n                -dimensions 1\\n                -handles 1\\n                -pivots 1\\n                -textures 1\\n                -strokes 1\\n                -motionTrails 1\\n\"\n\t\t+ \"                -clipGhosts 1\\n                -bluePencil 1\\n                -greasePencils 0\\n                -shadows 0\\n                -captureSequenceNumber -1\\n                -width 0\\n                -height 0\\n                -sceneRenderFilter 0\\n                -displayMode \\\"centerEye\\\" \\n                -viewColor 0 0 0 1 \\n                -useCustomBackground 1\\n                $editorName;\\n            stereoCameraView -e -viewSelected 0 $editorName; };\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\tif ($useSceneConfig) {\\n        string $configName = `getPanel -cwl (localizedPanelLabel(\\\"Current Layout\\\"))`;\\n        if (\\\"\\\" != $configName) {\\n\\t\\t\\tpanelConfiguration -edit -label (localizedPanelLabel(\\\"Current Layout\\\")) \\n\\t\\t\\t\\t-userCreated false\\n\\t\\t\\t\\t-defaultImage \\\"vacantCell.xP:/\\\"\\n\\t\\t\\t\\t-image \\\"\\\"\\n\\t\\t\\t\\t-sc false\\n\\t\\t\\t\\t-configString \\\"global string $gMainPane; paneLayout -e -cn \\\\\\\"quad\\\\\\\" -ps 1 50 50 -ps 2 50 50 -ps 3 50 50 -ps 4 50 50 $gMainPane;\\\"\\n\\t\\t\\t\\t-removeAllPanels\\n\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Top View\\\")) \\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Persp View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera persp` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera persp` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Side View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|side\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|side\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Front View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera front` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera front` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t$configName;\\n\\n            setNamedPanelLayout (localizedPanelLabel(\\\"Current Layout\\\"));\\n        }\\n\\n        panelHistory -e -clear mainPanelHistory;\\n        sceneUIReplacement -clear;\\n\\t}\\n\\n\\ngrid -spacing 5 -size 12 -divisions 5 -displayAxes yes -displayGridLines yes -displayDivisionLines yes -displayPerspectiveLabels no -displayOrthographicLabels no -displayAxesBold yes -perspectiveLabelPosition axis -orthographicLabelPosition edge;\\nviewManip -drawCompass 0 -compassAngle 0 -frontParameters \\\"\\\" -homeParameters \\\"\\\" -selectionLockParameters \\\"\\\";\\n}\\n\");\n\tsetAttr \".st\" 3;\ncreateNode script -n \"sceneConfigurationScriptNode\";\n\trename -uid \"72B19BC2-43A2-E229-0A73-2CB861A291D1\";\n\tsetAttr \".b\" -type \"string\" \"playbackOptions -min 1000 -max 1001 -ast 1000 -aet 1001 \";\n\tsetAttr \".st\" 6;\ncreateNode polyDisc -n \"polyDisc1\";\n\trename -uid \"9ED8A7BD-4FFD-6107-4322-35ACD1D3AC42\";\ncreateNode aiOptions -s -n \"defaultArnoldRenderOptions\";\n\trename -uid \"51BB3D7A-4C7D-FCDD-1B21-D89934107F98\";\n\tsetAttr \".skip_license_check\" yes;\ncreateNode aiAOVFilter -s -n \"defaultArnoldFilter\";\n\trename -uid \"989A5992-46C8-0CB2-B2B8-4E868F31FEA8\";\n\tsetAttr \".ai_translator\" -type \"string\" \"gaussian\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDriver\";\n\trename -uid \"B8469AD8-47C8-9EF1-FE5E-5AB50ABC1536\";\n\tsetAttr \".merge_AOVs\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"exr\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDisplayDriver\";\n\trename -uid \"21D4F4CD-4D78-001C-610D-798402CD7CF0\";\n\tsetAttr \".output_mode\" 0;\n\tsetAttr \".ai_translator\" -type \"string\" \"maya\";\ncreateNode objectSet -n \"workfileMain\";\n\trename -uid \"3C9B5D6F-4579-8E3B-5B7D-4C88865A1C68\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"instance_id\" -ln \"instance_id\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\" \n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".hio\" yes;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:30d256dac64c\";\n\tsetAttr \".instance_id\" -type \"string\" \"911dc92a-ad29-41e5-bbf9-733d56174fb9\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"workfile\";\n\tsetAttr \".subset\" -type \"string\" \"workfileTest_task\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.workfile\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ExtractImportReference\\\": {\\\"active\\\": false}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"\";\ncreateNode renderSetupLayer -n \"Main\";\n\trename -uid \"2202E438-4CEF-F64E-737C-F48C65E31126\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode renderLayer -n \"rs_Main\";\n\trename -uid \"DF0259B1-4F96-DA7E-C74F-B599FBE15C18\";\n\tsetAttr \".do\" 1;\ncreateNode collection -n \"defaultCollection\";\n\trename -uid \"E5014237-4DA9-6FC0-633D-69AFA56161B3\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode simpleSelector -n \"defaultCollectionSelector\";\n\trename -uid \"7CA2F6D8-483C-B020-BC03-EF9563A52163\";\n\tsetAttr \".pat\" -type \"string\" \"*\";\nselect -ne :time1;\n\tsetAttr \".o\" 1001;\n\tsetAttr \".unw\" 1001;\nselect -ne :hardwareRenderingGlobals;\n\tsetAttr \".otfna\" -type \"stringArray\" 22 \"NURBS Curves\" \"NURBS Surfaces\" \"Polygons\" \"Subdiv Surface\" \"Particles\" \"Particle Instance\" \"Fluids\" \"Strokes\" \"Image Planes\" \"UI\" \"Lights\" \"Cameras\" \"Locators\" \"Joints\" \"IK Handles\" \"Deformers\" \"Motion Trails\" \"Components\" \"Hair Systems\" \"Follicles\" \"Misc. UI\" \"Ornaments\"  ;\n\tsetAttr \".otfva\" -type \"Int32Array\" 22 0 1 1 1 1 1\n\t\t 1 1 1 0 0 0 0 0 0 0 0 0\n\t\t 0 0 0 0 ;\n\tsetAttr \".fprt\" yes;\nselect -ne :renderPartition;\n\tsetAttr -s 2 \".st\";\nselect -ne :renderGlobalsList1;\nselect -ne :defaultShaderList1;\n\tsetAttr -s 5 \".s\";\nselect -ne :postProcessList1;\n\tsetAttr -s 2 \".p\";\nselect -ne :defaultRenderingList1;\n\tsetAttr -s 2 \".r\";\nselect -ne :standardSurface1;\n\tsetAttr \".b\" 0.80000001192092896;\n\tsetAttr \".bc\" -type \"float3\" 1 1 1 ;\n\tsetAttr \".s\" 0.20000000298023224;\nselect -ne :initialShadingGroup;\n\tsetAttr -s 2 \".dsm\";\n\tsetAttr \".ro\" yes;\nselect -ne :initialParticleSE;\n\tsetAttr \".ro\" yes;\nselect -ne :defaultRenderGlobals;\n\taddAttr -ci true -h true -sn \"dss\" -ln \"defaultSurfaceShader\" -dt \"string\";\n\tsetAttr \".ren\" -type \"string\" \"arnold\";\n\tsetAttr \".outf\" 51;\n\tsetAttr \".imfkey\" -type \"string\" \"exr\";\n\tsetAttr \".an\" yes;\n\tsetAttr \".fs\" 1001;\n\tsetAttr \".ef\" 1001;\n\tsetAttr \".oft\" -type \"string\" \"\";\n\tsetAttr \".pff\" yes;\n\tsetAttr \".ifp\" -type \"string\" \"<Scene>/<RenderLayer>/<RenderLayer>\";\n\tsetAttr \".rv\" -type \"string\" \"\";\n\tsetAttr \".pram\" -type \"string\" \"\";\n\tsetAttr \".poam\" -type \"string\" \"\";\n\tsetAttr \".prlm\" -type \"string\" \"\";\n\tsetAttr \".polm\" -type \"string\" \"\";\n\tsetAttr \".prm\" -type \"string\" \"\";\n\tsetAttr \".pom\" -type \"string\" \"\";\n\tsetAttr \".dss\" -type \"string\" \"lambert1\";\nselect -ne :defaultResolution;\n\tsetAttr \".w\" 1920;\n\tsetAttr \".h\" 1080;\n\tsetAttr \".pa\" 1;\n\tsetAttr \".al\" yes;\n\tsetAttr \".dar\" 1.6666666269302368;\nselect -ne :defaultColorMgtGlobals;\n\tsetAttr \".cfe\" yes;\n\tsetAttr \".cfp\" -type \"string\" \"<MAYA_RESOURCES>/OCIO-configs/Maya-legacy/config.ocio\";\n\tsetAttr \".vtn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".vn\" -type \"string\" \"sRGB gamma\";\n\tsetAttr \".dn\" -type \"string\" \"legacy\";\n\tsetAttr \".wsn\" -type \"string\" \"scene-linear Rec 709/sRGB\";\n\tsetAttr \".ovt\" no;\n\tsetAttr \".povt\" no;\n\tsetAttr \".otn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".potn\" -type \"string\" \"sRGB gamma (legacy)\";\nselect -ne :hardwareRenderGlobals;\n\tsetAttr \".ctrs\" 256;\n\tsetAttr \".btrs\" 512;\nconnectAttr \"rs_Main.ri\" \":persp.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":top.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":front.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":side.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"pSphere1_GEO.rlio[0]\";\nconnectAttr \"polySphere1.out\" \"pSphere1_GEOShape1.i\";\nconnectAttr \"rs_Main.ri\" \"pDisc1.rlio[0]\";\nconnectAttr \"polyDisc1.output\" \"pDiscShape1.i\";\nconnectAttr \"rs_Main.ri\" \"persp1.rlio[0]\";\nrelationship \"link\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"link\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nconnectAttr \"layerManager.dli[0]\" \"defaultLayer.id\";\nconnectAttr \"renderLayerManager.rlmi[0]\" \"defaultRenderLayer.rlid\";\nconnectAttr \"Main.msg\" \"renderSetup.frl\";\nconnectAttr \"Main.msg\" \"renderSetup.lrl\";\nconnectAttr \"pSphere1_GEO.iog\" \"modelMain.dsm\" -na;\nconnectAttr \":defaultArnoldDisplayDriver.msg\" \":defaultArnoldRenderOptions.drivers\"\n\t\t -na;\nconnectAttr \":defaultArnoldFilter.msg\" \":defaultArnoldRenderOptions.filt\";\nconnectAttr \":defaultArnoldDriver.msg\" \":defaultArnoldRenderOptions.drvr\";\nconnectAttr \"rs_Main.msg\" \"Main.lrl\";\nconnectAttr \"renderSetup.lit\" \"Main.pls\";\nconnectAttr \"defaultCollection.msg\" \"Main.cl\";\nconnectAttr \"defaultCollection.msg\" \"Main.ch\";\nconnectAttr \"renderLayerManager.rlmi[1]\" \"rs_Main.rlid\";\nconnectAttr \"defaultCollectionSelector.c\" \"defaultCollection.sel\";\nconnectAttr \"Main.lit\" \"defaultCollection.pls\";\nconnectAttr \"Main.nic\" \"defaultCollection.pic\";\nconnectAttr \"defaultRenderLayer.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"rs_Main.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"pSphere1_GEOShape1.iog\" \":initialShadingGroup.dsm\" -na;\nconnectAttr \"pDiscShape1.iog\" \":initialShadingGroup.dsm\" -na;\n// End of test_project_test_asset_test_task_v002.ma\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v001.ma",
    "content": "//Maya ASCII 2023 scene\n//Name: test_project_test_asset_test_task_v002.ma\n//Last modified: Thu, Dec 07, 2023 03:53:06 PM\n//Codeset: 1252\nrequires maya \"2023\";\nrequires -nodeType \"simpleSelector\" -nodeType \"renderSetupLayer\" -nodeType \"renderSetup\"\n\t\t -nodeType \"collection\" \"renderSetup.py\" \"1.0\";\nrequires \"stereoCamera\" \"10.0\";\nrequires -nodeType \"aiOptions\" -nodeType \"aiAOVDriver\" -nodeType \"aiAOVFilter\" \"mtoa\" \"5.2.1.1\";\nrequires -nodeType \"polyDisc\" \"modelingToolkit\" \"0.0.0.0\";\nrequires \"stereoCamera\" \"10.0\";\ncurrentUnit -l centimeter -a degree -t pal;\nfileInfo \"application\" \"maya\";\nfileInfo \"product\" \"Maya 2023\";\nfileInfo \"version\" \"2023\";\nfileInfo \"cutIdentifier\" \"202211021031-847a9f9623\";\nfileInfo \"osv\" \"Windows 10 Pro v2009 (Build: 19045)\";\nfileInfo \"license\" \"education\";\nfileInfo \"UUID\" \"7A992745-4AD5-777F-5575-B4BFAC62B1D0\";\nfileInfo \"OpenPypeContext\" \"eyJwdWJsaXNoX2F0dHJpYnV0ZXMiOiB7IlZhbGlkYXRlQ29udGFpbmVycyI6IHsiYWN0aXZlIjogdHJ1ZX19fQ==\";\ncreateNode transform -s -n \"persp\";\n\trename -uid \"D52C935B-47C9-D868-A875-D799DD17B3A1\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 26.953352922736947 24.362683487437064 26.983403389430531 ;\n\tsetAttr \".r\" -type \"double3\" 1064.7698975365424 54.173034578109736 1075.1660763544442 ;\n\tsetAttr \".rp\" -type \"double3\" -2.0401242849359917e-14 2.2609331405046354e-14 -44.821869662029947 ;\n\tsetAttr \".rpt\" -type \"double3\" -27.999999999999989 -21.000000000000025 16.82186966202995 ;\ncreateNode camera -s -n \"perspShape\" -p \"persp\";\n\trename -uid \"2399E6C0-490F-BA1F-485F-5AA8A01D27BC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 46.895362757145833;\n\tsetAttr \".imn\" -type \"string\" \"persp\";\n\tsetAttr \".den\" -type \"string\" \"persp_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".ai_translator\" -type \"string\" \"perspective\";\ncreateNode transform -s -n \"top\";\n\trename -uid \"415C7426-413E-0FAE-FCC3-3DAED7443A52\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 1000.1 0 ;\n\tsetAttr \".r\" -type \"double3\" 90 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" 0 -1000.1 1000.1 ;\ncreateNode camera -s -n \"topShape\" -p \"top\";\n\trename -uid \"3BD0CF60-40DB-5278-5D8B-06ACBDA32122\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"top\";\n\tsetAttr \".den\" -type \"string\" \"top_depth\";\n\tsetAttr \".man\" -type \"string\" \"top_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -t %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"front\";\n\trename -uid \"D83DD5CE-4FE0-AB1B-81B2-87A63F0B7A05\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 0 1000.1 ;\n\tsetAttr \".r\" -type \"double3\" 180 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\ncreateNode camera -s -n \"frontShape\" -p \"front\";\n\trename -uid \"23313CBA-42C2-0B3A-0FCF-EA965EAC5DEC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"front\";\n\tsetAttr \".den\" -type \"string\" \"front_depth\";\n\tsetAttr \".man\" -type \"string\" \"front_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -f %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"side\";\n\trename -uid \"F70F692C-4A0D-BE64-9EE4-A99B6FA2D56E\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 1000.1 0 0 ;\n\tsetAttr \".r\" -type \"double3\" 180 -90 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" -1000.1 0 1000.1 ;\ncreateNode camera -s -n \"sideShape\" -p \"side\";\n\trename -uid \"C05669C3-420E-CA11-E5FC-7EB64EF8B632\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"side\";\n\tsetAttr \".den\" -type \"string\" \"side_depth\";\n\tsetAttr \".man\" -type \"string\" \"side_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -s %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -n \"pSphere1_GEO\";\n\trename -uid \"7445A43F-444F-B2D3-4315-2AA013D2E0B6\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:440654b3dfe4\";\ncreateNode mesh -n \"pSphere1_GEOShape1\" -p \"pSphere1_GEO\";\n\trename -uid \"7C731260-45C6-339E-07BF-359446B08EA1\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:bf8e49bb98ec\";\ncreateNode transform -n \"pDisc1\";\n\trename -uid \"DED70CCF-4C19-16E4-9E5D-66A05037BA47\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:90e762703f08\";\ncreateNode mesh -n \"pDiscShape1\" -p \"pDisc1\";\n\trename -uid \"E1FCDCCF-4DE1-D3B9-C4F8-3285F1CF5B25\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:4ee3da11a1a4\";\ncreateNode transform -n \"persp1\";\n\trename -uid \"292F1351-4E41-A890-D6D5-A5A4F7D94C76\";\n\tsetAttr \".t\" -type \"double3\" 3.7889010960863949 2.8416759114717678 3.7889010364817537 ;\n\tsetAttr \".r\" -type \"double3\" -27.938352729602379 44.999999999999972 -5.172681101354183e-14 ;\ncreateNode camera -n \"perspShape1\" -p \"persp1\";\n\trename -uid \"9277418C-43C8-5064-A7C6-64AC829A76F2\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".ovr\" 1.3;\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 6.0652013012246453;\n\tsetAttr \".imn\" -type \"string\" \"persp1\";\n\tsetAttr \".den\" -type \"string\" \"persp1_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp1_mask\";\n\tsetAttr \".tp\" -type \"double3\" -1.1920928955078125e-07 0 -1.7881393432617188e-07 ;\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".dr\" yes;\ncreateNode lightLinker -s -n \"lightLinker1\";\n\trename -uid \"09465BD3-42E5-18E4-7906-20A99BB2A6C0\";\n\tsetAttr -s 2 \".lnk\";\n\tsetAttr -s 2 \".slnk\";\ncreateNode shapeEditorManager -n \"shapeEditorManager\";\n\trename -uid \"9F2E8009-4D69-046B-FCC4-28A8CE8F86DB\";\ncreateNode poseInterpolatorManager -n \"poseInterpolatorManager\";\n\trename -uid \"6757AD81-40B0-A747-69C3-D9A56259571E\";\ncreateNode displayLayerManager -n \"layerManager\";\n\trename -uid \"6F055ED5-4D91-8F85-7951-B4A13543A561\";\ncreateNode displayLayer -n \"defaultLayer\";\n\trename -uid \"4A776D1B-401F-7069-1C74-A7AAE84CEE03\";\n\tsetAttr \".ufem\" -type \"stringArray\" 0  ;\ncreateNode renderLayerManager -n \"renderLayerManager\";\n\trename -uid \"55626D2B-4FD5-61A1-7AB2-47B13F19D8AA\";\n\tsetAttr -s 2 \".rlmi[1]\"  1;\n\tsetAttr -s 2 \".rlmi\";\ncreateNode renderLayer -n \"defaultRenderLayer\";\n\trename -uid \"B134920D-4508-23BD-A6CA-11B43DE03F53\";\n\tsetAttr \".g\" yes;\ncreateNode renderSetup -n \"renderSetup\";\n\trename -uid \"9A8F0D15-41AB-CA70-C2D8-B78840BF9BC1\";\ncreateNode polySphere -n \"polySphere1\";\n\trename -uid \"DA319706-4ACF-B15C-53B2-48AC80D202EA\";\ncreateNode objectSet -n \"modelMain\";\n\trename -uid \"A76AD4F8-4CF5-AA0D-4E98-BABEE6454CC3\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"instance_id\" -ln \"instance_id\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"writeColorSets\" -ln \"writeColorSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"writeFaceSets\" -ln \"writeFaceSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"includeParentHierarchy\" -ln \"includeParentHierarchy\" -min \n\t\t0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"attr\" -ln \"attr\" -dt \"string\";\n\taddAttr -ci true -sn \"attrPrefix\" -ln \"attrPrefix\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:7364ea6776c9\";\n\tsetAttr \".instance_id\" -type \"string\" \"6889d3db-b813-43db-96de-9ba555dc4472\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"model\";\n\tsetAttr \".subset\" -type \"string\" \"modelMain\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.model\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr -cb on \".writeColorSets\";\n\tsetAttr -cb on \".writeFaceSets\";\n\tsetAttr -cb on \".includeParentHierarchy\";\n\tsetAttr \".attr\" -type \"string\" \"\";\n\tsetAttr \".attrPrefix\" -type \"string\" \"\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateNodeIDsRelated\\\": {\\\"active\\\": true}, \\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ValidateTransformNamingSuffix\\\": {\\\"active\\\": true}, \\\"ValidateColorSets\\\": {\\\"active\\\": true}, \\\"ValidateMeshHasUVs\\\": {\\\"active\\\": true}, \\\"ValidateMeshNonZeroEdgeLength\\\": {\\\"active\\\": true}, \\\"ExtractModel\\\": {\\\"active\\\": true}, \\\"ValidateMeshArnoldAttributes\\\": {\\\"active\\\": true}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"writeColorSets,writeFaceSets,includeParentHierarchy,attr,attrPrefix\";\ncreateNode script -n \"uiConfigurationScriptNode\";\n\trename -uid \"4B7AFB53-452E-E870-63E1-CCA1DD6EAF13\";\n\tsetAttr \".b\" -type \"string\" (\n\t\t\"// Maya Mel UI Configuration File.\\n//\\n//  This script is machine generated.  Edit at your own risk.\\n//\\n//\\n\\nglobal string $gMainPane;\\nif (`paneLayout -exists $gMainPane`) {\\n\\n\\tglobal int $gUseScenePanelConfig;\\n\\tint    $useSceneConfig = $gUseScenePanelConfig;\\n\\tint    $nodeEditorPanelVisible = stringArrayContains(\\\"nodeEditorPanel1\\\", `getPanel -vis`);\\n\\tint    $nodeEditorWorkspaceControlOpen = (`workspaceControl -exists nodeEditorPanel1Window` && `workspaceControl -q -visible nodeEditorPanel1Window`);\\n\\tint    $menusOkayInPanels = `optionVar -q allowMenusInPanels`;\\n\\tint    $nVisPanes = `paneLayout -q -nvp $gMainPane`;\\n\\tint    $nPanes = 0;\\n\\tstring $editorName;\\n\\tstring $panelName;\\n\\tstring $itemFilterName;\\n\\tstring $panelConfig;\\n\\n\\t//\\n\\t//  get current state of the UI\\n\\t//\\n\\tsceneUIReplacement -update $gMainPane;\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Top View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Top View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|top\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n\"\n\t\t+ \"            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n\"\n\t\t+ \"            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Side View\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Side View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|side\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n\"\n\t\t+ \"            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n\"\n\t\t+ \"            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n\"\n\t\t+ \"        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Front View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Front View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|front\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n\"\n\t\t+ \"            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n\"\n\t\t+ \"            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n\"\n\t\t+ \"            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Persp View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Persp View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|persp\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n\"\n\t\t+ \"            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n\"\n\t\t+ \"            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n\"\n\t\t+ \"            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"ToggledOutliner\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"ToggledOutliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -docTag \\\"isolOutln_fromSeln\\\" \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 1\\n            -showReferenceMembers 1\\n\"\n\t\t+ \"            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n\"\n\t\t+ \"            -isSet 0\\n            -isSetMember 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -selectCommand \\\"print(\\\\\\\"\\\\\\\")\\\" \\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n            -renderFilterIndex 0\\n            -selectionOrder \\\"chronological\\\" \\n            -expandAttribute 0\\n            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"Outliner\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"Outliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 0\\n            -showReferenceMembers 0\\n            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n\"\n\t\t+ \"            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n\"\n\t\t+ \"            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"graphEditor\\\" (localizedPanelLabel(\\\"Graph Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Graph Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 1\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n\"\n\t\t+ \"                -showPublishedAsConnected 0\\n                -showParentContainers 0\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 1\\n                -showCompounds 0\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 1\\n                -doNotSelectNewObjects 0\\n                -dropIsParent 1\\n                -transmitFilters 1\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n\"\n\t\t+ \"                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 1\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"GraphEd\\\");\\n            animCurveEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -showPlayRangeShades \\\"on\\\" \\n                -lockPlayRangeShades \\\"off\\\" \\n                -smoothness \\\"fine\\\" \\n                -resultSamples 1.041667\\n                -resultScreenSamples 0\\n                -resultUpdate \\\"delayed\\\" \\n                -showUpstreamCurves 1\\n                -keyMinScale 1\\n                -stackedCurvesMin -1\\n                -stackedCurvesMax 1\\n\"\n\t\t+ \"                -stackedCurvesSpace 0.2\\n                -preSelectionHighlight 0\\n                -constrainDrag 0\\n                -valueLinesToggle 1\\n                -highlightAffectedCurves 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dopeSheetPanel\\\" (localizedPanelLabel(\\\"Dope Sheet\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dope Sheet\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n\"\n\t\t+ \"                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 0\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n                -showPublishedAsConnected 0\\n                -showParentContainers 0\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 0\\n                -showCompounds 1\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 0\\n                -doNotSelectNewObjects 1\\n                -dropIsParent 1\\n                -transmitFilters 0\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n\"\n\t\t+ \"                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 0\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"DopeSheetEd\\\");\\n            dopeSheetEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -outliner \\\"dopeSheetPanel1OutlineEd\\\" \\n                -showSummary 1\\n                -showScene 0\\n                -hierarchyBelow 0\\n\"\n\t\t+ \"                -showTicks 1\\n                -selectionWindow 0 0 0 0 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"timeEditorPanel\\\" (localizedPanelLabel(\\\"Time Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Time Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"clipEditorPanel\\\" (localizedPanelLabel(\\\"Trax Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Trax Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = clipEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 0 \\n\"\n\t\t+ \"                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"sequenceEditorPanel\\\" (localizedPanelLabel(\\\"Camera Sequencer\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Camera Sequencer\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = sequenceEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 1 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperGraphPanel\\\" (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\n\\t\\t\\t$editorName = ($panelName+\\\"HyperGraphEd\\\");\\n            hyperGraph -e \\n                -graphLayoutStyle \\\"hierarchicalLayout\\\" \\n                -orientation \\\"horiz\\\" \\n                -mergeConnections 0\\n                -zoom 1\\n                -animateTransition 0\\n                -showRelationships 1\\n                -showShapes 0\\n                -showDeformers 0\\n                -showExpressions 0\\n                -showConstraints 0\\n                -showConnectionFromSelected 0\\n                -showConnectionToSelected 0\\n                -showConstraintLabels 0\\n                -showUnderworld 0\\n                -showInvisible 0\\n                -transitionFrames 1\\n                -opaqueContainers 0\\n                -freeform 0\\n                -imagePosition 0 0 \\n                -imageScale 1\\n                -imageEnabled 0\\n                -graphType \\\"DAG\\\" \\n                -heatMapDisplay 0\\n                -updateSelection 1\\n                -updateNodeAdded 1\\n                -useDrawOverrideColor 0\\n                -limitGraphTraversal -1\\n\"\n\t\t+ \"                -range 0 0 \\n                -iconSize \\\"smallIcons\\\" \\n                -showCachedConnections 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperShadePanel\\\" (localizedPanelLabel(\\\"Hypershade\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypershade\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"visorPanel\\\" (localizedPanelLabel(\\\"Visor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Visor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"nodeEditorPanel\\\" (localizedPanelLabel(\\\"Node Editor\\\")) `;\\n\\tif ($nodeEditorPanelVisible || $nodeEditorWorkspaceControlOpen) {\\n\"\n\t\t+ \"\\t\\tif (\\\"\\\" == $panelName) {\\n\\t\\t\\tif ($useSceneConfig) {\\n\\t\\t\\t\\t$panelName = `scriptedPanel -unParent  -type \\\"nodeEditorPanel\\\" -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels `;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n\"\n\t\t+ \"                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\t}\\n\\t\\t} else {\\n\\t\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n\"\n\t\t+ \"                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"createNodePanel\\\" (localizedPanelLabel(\\\"Create Node\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Create Node\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"polyTexturePlacementPanel\\\" (localizedPanelLabel(\\\"UV Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"UV Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"renderWindowPanel\\\" (localizedPanelLabel(\\\"Render View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Render View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"shapePanel\\\" (localizedPanelLabel(\\\"Shape Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tshapePanel -edit -l (localizedPanelLabel(\\\"Shape Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"posePanel\\\" (localizedPanelLabel(\\\"Pose Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tposePanel -edit -l (localizedPanelLabel(\\\"Pose Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynRelEdPanel\\\" (localizedPanelLabel(\\\"Dynamic Relationships\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dynamic Relationships\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"relationshipPanel\\\" (localizedPanelLabel(\\\"Relationship Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Relationship Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"referenceEditorPanel\\\" (localizedPanelLabel(\\\"Reference Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Reference Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynPaintScriptedPanelType\\\" (localizedPanelLabel(\\\"Paint Effects\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Paint Effects\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"scriptEditorPanel\\\" (localizedPanelLabel(\\\"Script Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Script Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"profilerPanel\\\" (localizedPanelLabel(\\\"Profiler Tool\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Profiler Tool\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"contentBrowserPanel\\\" (localizedPanelLabel(\\\"Content Browser\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Content Browser\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"Stereo\\\" (localizedPanelLabel(\\\"Stereo\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Stereo\\\")) -mbv $menusOkayInPanels  $panelName;\\n{ string $editorName = ($panelName+\\\"Editor\\\");\\n            stereoCameraView -e \\n                -camera \\\"|persp\\\" \\n                -useInteractiveMode 0\\n                -displayLights \\\"default\\\" \\n                -displayAppearance \\\"wireframe\\\" \\n                -activeOnly 0\\n                -ignorePanZoom 0\\n                -wireframeOnShaded 0\\n                -headsUpDisplay 1\\n                -holdOuts 1\\n                -selectionHiliteDisplay 1\\n                -useDefaultMaterial 0\\n                -bufferMode \\\"double\\\" \\n                -twoSidedLighting 1\\n                -backfaceCulling 0\\n                -xray 0\\n                -jointXray 0\\n                -activeComponentsXray 0\\n                -displayTextures 0\\n                -smoothWireframe 0\\n                -lineWidth 1\\n                -textureAnisotropic 0\\n                -textureHilight 1\\n                -textureSampling 2\\n\"\n\t\t+ \"                -textureDisplay \\\"modulate\\\" \\n                -textureMaxSize 32768\\n                -fogging 0\\n                -fogSource \\\"fragment\\\" \\n                -fogMode \\\"linear\\\" \\n                -fogStart 0\\n                -fogEnd 100\\n                -fogDensity 0.1\\n                -fogColor 0.5 0.5 0.5 1 \\n                -depthOfFieldPreview 1\\n                -maxConstantTransparency 1\\n                -objectFilterShowInHUD 1\\n                -isFiltered 0\\n                -colorResolution 4 4 \\n                -bumpResolution 4 4 \\n                -textureCompression 0\\n                -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n                -transpInShadows 0\\n                -cullingOverride \\\"none\\\" \\n                -lowQualityLighting 0\\n                -maximumNumHardwareLights 0\\n                -occlusionCulling 0\\n                -shadingModel 0\\n                -useBaseRenderer 0\\n                -useReducedRenderer 0\\n                -smallObjectCulling 0\\n                -smallObjectThreshold -1 \\n                -interactiveDisableShadows 0\\n\"\n\t\t+ \"                -interactiveBackFaceCull 0\\n                -sortTransparent 1\\n                -controllers 1\\n                -nurbsCurves 1\\n                -nurbsSurfaces 1\\n                -polymeshes 1\\n                -subdivSurfaces 1\\n                -planes 1\\n                -lights 1\\n                -cameras 1\\n                -controlVertices 1\\n                -hulls 1\\n                -grid 1\\n                -imagePlane 1\\n                -joints 1\\n                -ikHandles 1\\n                -deformers 1\\n                -dynamics 1\\n                -particleInstancers 1\\n                -fluids 1\\n                -hairSystems 1\\n                -follicles 1\\n                -nCloths 1\\n                -nParticles 1\\n                -nRigids 1\\n                -dynamicConstraints 1\\n                -locators 1\\n                -manipulators 1\\n                -pluginShapes 1\\n                -dimensions 1\\n                -handles 1\\n                -pivots 1\\n                -textures 1\\n                -strokes 1\\n                -motionTrails 1\\n\"\n\t\t+ \"                -clipGhosts 1\\n                -bluePencil 1\\n                -greasePencils 0\\n                -shadows 0\\n                -captureSequenceNumber -1\\n                -width 0\\n                -height 0\\n                -sceneRenderFilter 0\\n                -displayMode \\\"centerEye\\\" \\n                -viewColor 0 0 0 1 \\n                -useCustomBackground 1\\n                $editorName;\\n            stereoCameraView -e -viewSelected 0 $editorName; };\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\tif ($useSceneConfig) {\\n        string $configName = `getPanel -cwl (localizedPanelLabel(\\\"Current Layout\\\"))`;\\n        if (\\\"\\\" != $configName) {\\n\\t\\t\\tpanelConfiguration -edit -label (localizedPanelLabel(\\\"Current Layout\\\")) \\n\\t\\t\\t\\t-userCreated false\\n\\t\\t\\t\\t-defaultImage \\\"vacantCell.xP:/\\\"\\n\\t\\t\\t\\t-image \\\"\\\"\\n\\t\\t\\t\\t-sc false\\n\\t\\t\\t\\t-configString \\\"global string $gMainPane; paneLayout -e -cn \\\\\\\"quad\\\\\\\" -ps 1 50 50 -ps 2 50 50 -ps 3 50 50 -ps 4 50 50 $gMainPane;\\\"\\n\\t\\t\\t\\t-removeAllPanels\\n\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Top View\\\")) \\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Persp View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera persp` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera persp` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Side View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|side\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|side\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Front View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera front` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera front` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t$configName;\\n\\n            setNamedPanelLayout (localizedPanelLabel(\\\"Current Layout\\\"));\\n        }\\n\\n        panelHistory -e -clear mainPanelHistory;\\n        sceneUIReplacement -clear;\\n\\t}\\n\\n\\ngrid -spacing 5 -size 12 -divisions 5 -displayAxes yes -displayGridLines yes -displayDivisionLines yes -displayPerspectiveLabels no -displayOrthographicLabels no -displayAxesBold yes -perspectiveLabelPosition axis -orthographicLabelPosition edge;\\nviewManip -drawCompass 0 -compassAngle 0 -frontParameters \\\"\\\" -homeParameters \\\"\\\" -selectionLockParameters \\\"\\\";\\n}\\n\");\n\tsetAttr \".st\" 3;\ncreateNode script -n \"sceneConfigurationScriptNode\";\n\trename -uid \"72B19BC2-43A2-E229-0A73-2CB861A291D1\";\n\tsetAttr \".b\" -type \"string\" \"playbackOptions -min 1000 -max 1001 -ast 1000 -aet 1001 \";\n\tsetAttr \".st\" 6;\ncreateNode polyDisc -n \"polyDisc1\";\n\trename -uid \"9ED8A7BD-4FFD-6107-4322-35ACD1D3AC42\";\ncreateNode aiOptions -s -n \"defaultArnoldRenderOptions\";\n\trename -uid \"51BB3D7A-4C7D-FCDD-1B21-D89934107F98\";\n\tsetAttr \".skip_license_check\" yes;\ncreateNode aiAOVFilter -s -n \"defaultArnoldFilter\";\n\trename -uid \"989A5992-46C8-0CB2-B2B8-4E868F31FEA8\";\n\tsetAttr \".ai_translator\" -type \"string\" \"gaussian\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDriver\";\n\trename -uid \"B8469AD8-47C8-9EF1-FE5E-5AB50ABC1536\";\n\tsetAttr \".merge_AOVs\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"exr\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDisplayDriver\";\n\trename -uid \"21D4F4CD-4D78-001C-610D-798402CD7CF0\";\n\tsetAttr \".output_mode\" 0;\n\tsetAttr \".ai_translator\" -type \"string\" \"maya\";\ncreateNode objectSet -n \"workfileMain\";\n\trename -uid \"3C9B5D6F-4579-8E3B-5B7D-4C88865A1C68\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"instance_id\" -ln \"instance_id\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".hio\" yes;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:30d256dac64c\";\n\tsetAttr \".instance_id\" -type \"string\" \"911dc92a-ad29-41e5-bbf9-733d56174fb9\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"workfile\";\n\tsetAttr \".subset\" -type \"string\" \"workfileTest_task\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.workfile\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ExtractImportReference\\\": {\\\"active\\\": false}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"\";\ncreateNode renderSetupLayer -n \"Main\";\n\trename -uid \"2202E438-4CEF-F64E-737C-F48C65E31126\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode renderLayer -n \"rs_Main\";\n\trename -uid \"DF0259B1-4F96-DA7E-C74F-B599FBE15C18\";\n\tsetAttr \".do\" 1;\ncreateNode collection -n \"defaultCollection\";\n\trename -uid \"E5014237-4DA9-6FC0-633D-69AFA56161B3\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode simpleSelector -n \"defaultCollectionSelector\";\n\trename -uid \"7CA2F6D8-483C-B020-BC03-EF9563A52163\";\n\tsetAttr \".pat\" -type \"string\" \"*\";\nselect -ne :time1;\n\tsetAttr \".o\" 1001;\n\tsetAttr \".unw\" 1001;\nselect -ne :hardwareRenderingGlobals;\n\tsetAttr \".otfna\" -type \"stringArray\" 22 \"NURBS Curves\" \"NURBS Surfaces\" \"Polygons\" \"Subdiv Surface\" \"Particles\" \"Particle Instance\" \"Fluids\" \"Strokes\" \"Image Planes\" \"UI\" \"Lights\" \"Cameras\" \"Locators\" \"Joints\" \"IK Handles\" \"Deformers\" \"Motion Trails\" \"Components\" \"Hair Systems\" \"Follicles\" \"Misc. UI\" \"Ornaments\"  ;\n\tsetAttr \".otfva\" -type \"Int32Array\" 22 0 1 1 1 1 1\n\t\t 1 1 1 0 0 0 0 0 0 0 0 0\n\t\t 0 0 0 0 ;\n\tsetAttr \".fprt\" yes;\nselect -ne :renderPartition;\n\tsetAttr -s 2 \".st\";\nselect -ne :renderGlobalsList1;\nselect -ne :defaultShaderList1;\n\tsetAttr -s 5 \".s\";\nselect -ne :postProcessList1;\n\tsetAttr -s 2 \".p\";\nselect -ne :defaultRenderingList1;\n\tsetAttr -s 2 \".r\";\nselect -ne :standardSurface1;\n\tsetAttr \".b\" 0.80000001192092896;\n\tsetAttr \".bc\" -type \"float3\" 1 1 1 ;\n\tsetAttr \".s\" 0.20000000298023224;\nselect -ne :initialShadingGroup;\n\tsetAttr -s 2 \".dsm\";\n\tsetAttr \".ro\" yes;\nselect -ne :initialParticleSE;\n\tsetAttr \".ro\" yes;\nselect -ne :defaultRenderGlobals;\n\taddAttr -ci true -h true -sn \"dss\" -ln \"defaultSurfaceShader\" -dt \"string\";\n\tsetAttr \".ren\" -type \"string\" \"arnold\";\n\tsetAttr \".outf\" 51;\n\tsetAttr \".imfkey\" -type \"string\" \"exr\";\n\tsetAttr \".an\" yes;\n\tsetAttr \".fs\" 1001;\n\tsetAttr \".ef\" 1001;\n\tsetAttr \".oft\" -type \"string\" \"\";\n\tsetAttr \".pff\" yes;\n\tsetAttr \".ifp\" -type \"string\" \"<Scene>/<RenderLayer>/<RenderLayer>\";\n\tsetAttr \".rv\" -type \"string\" \"\";\n\tsetAttr \".pram\" -type \"string\" \"\";\n\tsetAttr \".poam\" -type \"string\" \"\";\n\tsetAttr \".prlm\" -type \"string\" \"\";\n\tsetAttr \".polm\" -type \"string\" \"\";\n\tsetAttr \".prm\" -type \"string\" \"\";\n\tsetAttr \".pom\" -type \"string\" \"\";\n\tsetAttr \".dss\" -type \"string\" \"lambert1\";\nselect -ne :defaultResolution;\n\tsetAttr \".w\" 1920;\n\tsetAttr \".h\" 1080;\n\tsetAttr \".pa\" 1;\n\tsetAttr \".al\" yes;\n\tsetAttr \".dar\" 1.6666666269302368;\nselect -ne :defaultColorMgtGlobals;\n\tsetAttr \".cfe\" yes;\n\tsetAttr \".cfp\" -type \"string\" \"<MAYA_RESOURCES>/OCIO-configs/Maya-legacy/config.ocio\";\n\tsetAttr \".vtn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".vn\" -type \"string\" \"sRGB gamma\";\n\tsetAttr \".dn\" -type \"string\" \"legacy\";\n\tsetAttr \".wsn\" -type \"string\" \"scene-linear Rec 709/sRGB\";\n\tsetAttr \".ovt\" no;\n\tsetAttr \".povt\" no;\n\tsetAttr \".otn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".potn\" -type \"string\" \"sRGB gamma (legacy)\";\nselect -ne :hardwareRenderGlobals;\n\tsetAttr \".ctrs\" 256;\n\tsetAttr \".btrs\" 512;\nconnectAttr \"rs_Main.ri\" \":persp.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":top.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":front.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":side.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"pSphere1_GEO.rlio[0]\";\nconnectAttr \"polySphere1.out\" \"pSphere1_GEOShape1.i\";\nconnectAttr \"rs_Main.ri\" \"pDisc1.rlio[0]\";\nconnectAttr \"polyDisc1.output\" \"pDiscShape1.i\";\nconnectAttr \"rs_Main.ri\" \"persp1.rlio[0]\";\nrelationship \"link\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"link\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nconnectAttr \"layerManager.dli[0]\" \"defaultLayer.id\";\nconnectAttr \"renderLayerManager.rlmi[0]\" \"defaultRenderLayer.rlid\";\nconnectAttr \"Main.msg\" \"renderSetup.frl\";\nconnectAttr \"Main.msg\" \"renderSetup.lrl\";\nconnectAttr \"pSphere1_GEO.iog\" \"modelMain.dsm\" -na;\nconnectAttr \":defaultArnoldDisplayDriver.msg\" \":defaultArnoldRenderOptions.drivers\"\n\t\t -na;\nconnectAttr \":defaultArnoldFilter.msg\" \":defaultArnoldRenderOptions.filt\";\nconnectAttr \":defaultArnoldDriver.msg\" \":defaultArnoldRenderOptions.drvr\";\nconnectAttr \"rs_Main.msg\" \"Main.lrl\";\nconnectAttr \"renderSetup.lit\" \"Main.pls\";\nconnectAttr \"defaultCollection.msg\" \"Main.cl\";\nconnectAttr \"defaultCollection.msg\" \"Main.ch\";\nconnectAttr \"renderLayerManager.rlmi[1]\" \"rs_Main.rlid\";\nconnectAttr \"defaultCollectionSelector.c\" \"defaultCollection.sel\";\nconnectAttr \"Main.lit\" \"defaultCollection.pls\";\nconnectAttr \"Main.nic\" \"defaultCollection.pic\";\nconnectAttr \"defaultRenderLayer.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"rs_Main.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"pSphere1_GEOShape1.iog\" \":initialShadingGroup.dsm\" -na;\nconnectAttr \"pDiscShape1.iog\" \":initialShadingGroup.dsm\" -na;\n// End of test_project_test_asset_test_task_v002.ma\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v002.ma",
    "content": "//Maya ASCII 2023 scene\n//Name: test_project_test_asset_test_task_v002.ma\n//Last modified: Thu, Dec 07, 2023 03:55:12 PM\n//Codeset: 1252\nrequires maya \"2023\";\nrequires -nodeType \"simpleSelector\" -nodeType \"renderSetupLayer\" -nodeType \"renderSetup\"\n\t\t -nodeType \"collection\" \"renderSetup.py\" \"1.0\";\nrequires \"stereoCamera\" \"10.0\";\nrequires -nodeType \"aiOptions\" -nodeType \"aiAOVDriver\" -nodeType \"aiAOVFilter\" \"mtoa\" \"5.2.1.1\";\nrequires -nodeType \"polyDisc\" \"modelingToolkit\" \"0.0.0.0\";\nrequires \"stereoCamera\" \"10.0\";\ncurrentUnit -l centimeter -a degree -t pal;\nfileInfo \"application\" \"maya\";\nfileInfo \"product\" \"Maya 2023\";\nfileInfo \"version\" \"2023\";\nfileInfo \"cutIdentifier\" \"202211021031-847a9f9623\";\nfileInfo \"osv\" \"Windows 10 Pro v2009 (Build: 19045)\";\nfileInfo \"license\" \"education\";\nfileInfo \"UUID\" \"7CC7E6D5-4F37-DB90-8A84-8493449019BF\";\nfileInfo \"OpenPypeContext\" \"eyJwdWJsaXNoX2F0dHJpYnV0ZXMiOiB7IlZhbGlkYXRlQ29udGFpbmVycyI6IHsiYWN0aXZlIjogdHJ1ZX19fQ==\";\ncreateNode transform -s -n \"persp\";\n\trename -uid \"D52C935B-47C9-D868-A875-D799DD17B3A1\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 26.953352922736947 24.362683487437064 26.983403389430531 ;\n\tsetAttr \".r\" -type \"double3\" 1064.7698975365424 54.173034578109736 1075.1660763544442 ;\n\tsetAttr \".rp\" -type \"double3\" -2.0401242849359917e-14 2.2609331405046354e-14 -44.821869662029947 ;\n\tsetAttr \".rpt\" -type \"double3\" -27.999999999999989 -21.000000000000025 16.82186966202995 ;\ncreateNode camera -s -n \"perspShape\" -p \"persp\";\n\trename -uid \"2399E6C0-490F-BA1F-485F-5AA8A01D27BC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 46.895362757145833;\n\tsetAttr \".imn\" -type \"string\" \"persp\";\n\tsetAttr \".den\" -type \"string\" \"persp_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".ai_translator\" -type \"string\" \"perspective\";\ncreateNode transform -s -n \"top\";\n\trename -uid \"415C7426-413E-0FAE-FCC3-3DAED7443A52\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 1000.1 0 ;\n\tsetAttr \".r\" -type \"double3\" 90 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" 0 -1000.1 1000.1 ;\ncreateNode camera -s -n \"topShape\" -p \"top\";\n\trename -uid \"3BD0CF60-40DB-5278-5D8B-06ACBDA32122\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"top\";\n\tsetAttr \".den\" -type \"string\" \"top_depth\";\n\tsetAttr \".man\" -type \"string\" \"top_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -t %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"front\";\n\trename -uid \"D83DD5CE-4FE0-AB1B-81B2-87A63F0B7A05\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 0 1000.1 ;\n\tsetAttr \".r\" -type \"double3\" 180 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\ncreateNode camera -s -n \"frontShape\" -p \"front\";\n\trename -uid \"23313CBA-42C2-0B3A-0FCF-EA965EAC5DEC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"front\";\n\tsetAttr \".den\" -type \"string\" \"front_depth\";\n\tsetAttr \".man\" -type \"string\" \"front_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -f %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"side\";\n\trename -uid \"F70F692C-4A0D-BE64-9EE4-A99B6FA2D56E\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 1000.1 0 0 ;\n\tsetAttr \".r\" -type \"double3\" 180 -90 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" -1000.1 0 1000.1 ;\ncreateNode camera -s -n \"sideShape\" -p \"side\";\n\trename -uid \"C05669C3-420E-CA11-E5FC-7EB64EF8B632\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"side\";\n\tsetAttr \".den\" -type \"string\" \"side_depth\";\n\tsetAttr \".man\" -type \"string\" \"side_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -s %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -n \"pSphere1_GEO\";\n\trename -uid \"7445A43F-444F-B2D3-4315-2AA013D2E0B6\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:440654b3dfe4\";\ncreateNode mesh -n \"pSphere1_GEOShape1\" -p \"pSphere1_GEO\";\n\trename -uid \"7C731260-45C6-339E-07BF-359446B08EA1\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:bf8e49bb98ec\";\ncreateNode transform -n \"pDisc1\";\n\trename -uid \"DED70CCF-4C19-16E4-9E5D-66A05037BA47\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:90e762703f08\";\ncreateNode mesh -n \"pDiscShape1\" -p \"pDisc1\";\n\trename -uid \"E1FCDCCF-4DE1-D3B9-C4F8-3285F1CF5B25\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:4ee3da11a1a4\";\ncreateNode transform -n \"persp1\";\n\trename -uid \"292F1351-4E41-A890-D6D5-A5A4F7D94C76\";\n\tsetAttr \".t\" -type \"double3\" 3.7889010960863949 2.8416759114717678 3.7889010364817537 ;\n\tsetAttr \".r\" -type \"double3\" -27.938352729602379 44.999999999999972 -5.172681101354183e-14 ;\ncreateNode camera -n \"perspShape1\" -p \"persp1\";\n\trename -uid \"9277418C-43C8-5064-A7C6-64AC829A76F2\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".ovr\" 1.3;\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 6.0652013012246453;\n\tsetAttr \".imn\" -type \"string\" \"persp1\";\n\tsetAttr \".den\" -type \"string\" \"persp1_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp1_mask\";\n\tsetAttr \".tp\" -type \"double3\" -1.1920928955078125e-07 0 -1.7881393432617188e-07 ;\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".dr\" yes;\ncreateNode lightLinker -s -n \"lightLinker1\";\n\trename -uid \"FBA13844-432C-E5C2-040E-A0925F2F0B8F\";\n\tsetAttr -s 2 \".lnk\";\n\tsetAttr -s 2 \".slnk\";\ncreateNode shapeEditorManager -n \"shapeEditorManager\";\n\trename -uid \"E3FDBA44-4665-FFBF-74F3-BDBF4F8F7B32\";\ncreateNode poseInterpolatorManager -n \"poseInterpolatorManager\";\n\trename -uid \"8834BA6F-47BE-8E76-4510-E2A7F3525077\";\ncreateNode displayLayerManager -n \"layerManager\";\n\trename -uid \"427D260A-43FC-DC22-4E80-46A0E90839B2\";\ncreateNode displayLayer -n \"defaultLayer\";\n\trename -uid \"4A776D1B-401F-7069-1C74-A7AAE84CEE03\";\n\tsetAttr \".ufem\" -type \"stringArray\" 0  ;\ncreateNode renderLayerManager -n \"renderLayerManager\";\n\trename -uid \"F1B8B519-43D1-5DE5-00F6-42A9514335E8\";\n\tsetAttr -s 2 \".rlmi[1]\"  1;\n\tsetAttr -s 2 \".rlmi\";\ncreateNode renderLayer -n \"defaultRenderLayer\";\n\trename -uid \"B134920D-4508-23BD-A6CA-11B43DE03F53\";\n\tsetAttr \".g\" yes;\ncreateNode renderSetup -n \"renderSetup\";\n\trename -uid \"9A8F0D15-41AB-CA70-C2D8-B78840BF9BC1\";\ncreateNode polySphere -n \"polySphere1\";\n\trename -uid \"DA319706-4ACF-B15C-53B2-48AC80D202EA\";\ncreateNode objectSet -n \"modelMain\";\n\trename -uid \"A76AD4F8-4CF5-AA0D-4E98-BABEE6454CC3\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"instance_id\" -ln \"instance_id\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"writeColorSets\" -ln \"writeColorSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"writeFaceSets\" -ln \"writeFaceSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"includeParentHierarchy\" -ln \"includeParentHierarchy\" -min \n\t\t0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"attr\" -ln \"attr\" -dt \"string\";\n\taddAttr -ci true -sn \"attrPrefix\" -ln \"attrPrefix\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:7364ea6776c9\";\n\tsetAttr \".instance_id\" -type \"string\" \"6889d3db-b813-43db-96de-9ba555dc4472\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"model\";\n\tsetAttr \".subset\" -type \"string\" \"modelMain\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.model\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr -cb on \".writeColorSets\";\n\tsetAttr -cb on \".writeFaceSets\";\n\tsetAttr -cb on \".includeParentHierarchy\";\n\tsetAttr \".attr\" -type \"string\" \"\";\n\tsetAttr \".attrPrefix\" -type \"string\" \"\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateNodeIDsRelated\\\": {\\\"active\\\": true}, \\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ValidateTransformNamingSuffix\\\": {\\\"active\\\": true}, \\\"ValidateColorSets\\\": {\\\"active\\\": true}, \\\"ValidateMeshHasUVs\\\": {\\\"active\\\": true}, \\\"ValidateMeshNonZeroEdgeLength\\\": {\\\"active\\\": true}, \\\"ExtractModel\\\": {\\\"active\\\": true}, \\\"ValidateMeshArnoldAttributes\\\": {\\\"active\\\": true}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"writeColorSets,writeFaceSets,includeParentHierarchy,attr,attrPrefix\";\ncreateNode script -n \"uiConfigurationScriptNode\";\n\trename -uid \"4B7AFB53-452E-E870-63E1-CCA1DD6EAF13\";\n\tsetAttr \".b\" -type \"string\" (\n\t\t\"// Maya Mel UI Configuration File.\\n//\\n//  This script is machine generated.  Edit at your own risk.\\n//\\n//\\n\\nglobal string $gMainPane;\\nif (`paneLayout -exists $gMainPane`) {\\n\\n\\tglobal int $gUseScenePanelConfig;\\n\\tint    $useSceneConfig = $gUseScenePanelConfig;\\n\\tint    $nodeEditorPanelVisible = stringArrayContains(\\\"nodeEditorPanel1\\\", `getPanel -vis`);\\n\\tint    $nodeEditorWorkspaceControlOpen = (`workspaceControl -exists nodeEditorPanel1Window` && `workspaceControl -q -visible nodeEditorPanel1Window`);\\n\\tint    $menusOkayInPanels = `optionVar -q allowMenusInPanels`;\\n\\tint    $nVisPanes = `paneLayout -q -nvp $gMainPane`;\\n\\tint    $nPanes = 0;\\n\\tstring $editorName;\\n\\tstring $panelName;\\n\\tstring $itemFilterName;\\n\\tstring $panelConfig;\\n\\n\\t//\\n\\t//  get current state of the UI\\n\\t//\\n\\tsceneUIReplacement -update $gMainPane;\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Top View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Top View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|top\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n\"\n\t\t+ \"            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n\"\n\t\t+ \"            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Side View\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Side View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|side\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n\"\n\t\t+ \"            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n\"\n\t\t+ \"            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n\"\n\t\t+ \"        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Front View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Front View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|front\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n\"\n\t\t+ \"            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n\"\n\t\t+ \"            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n\"\n\t\t+ \"            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Persp View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Persp View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|persp\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n\"\n\t\t+ \"            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n\"\n\t\t+ \"            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n\"\n\t\t+ \"            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"ToggledOutliner\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"ToggledOutliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -docTag \\\"isolOutln_fromSeln\\\" \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 1\\n            -showReferenceMembers 1\\n\"\n\t\t+ \"            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n\"\n\t\t+ \"            -isSet 0\\n            -isSetMember 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -selectCommand \\\"print(\\\\\\\"\\\\\\\")\\\" \\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n            -renderFilterIndex 0\\n            -selectionOrder \\\"chronological\\\" \\n            -expandAttribute 0\\n            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"Outliner\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"Outliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 0\\n            -showReferenceMembers 0\\n            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n\"\n\t\t+ \"            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n\"\n\t\t+ \"            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"graphEditor\\\" (localizedPanelLabel(\\\"Graph Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Graph Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 1\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n\"\n\t\t+ \"                -showPublishedAsConnected 0\\n                -showParentContainers 0\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 1\\n                -showCompounds 0\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 1\\n                -doNotSelectNewObjects 0\\n                -dropIsParent 1\\n                -transmitFilters 1\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n\"\n\t\t+ \"                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 1\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"GraphEd\\\");\\n            animCurveEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -showPlayRangeShades \\\"on\\\" \\n                -lockPlayRangeShades \\\"off\\\" \\n                -smoothness \\\"fine\\\" \\n                -resultSamples 1.041667\\n                -resultScreenSamples 0\\n                -resultUpdate \\\"delayed\\\" \\n                -showUpstreamCurves 1\\n                -keyMinScale 1\\n                -stackedCurvesMin -1\\n                -stackedCurvesMax 1\\n\"\n\t\t+ \"                -stackedCurvesSpace 0.2\\n                -preSelectionHighlight 0\\n                -constrainDrag 0\\n                -valueLinesToggle 1\\n                -highlightAffectedCurves 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dopeSheetPanel\\\" (localizedPanelLabel(\\\"Dope Sheet\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dope Sheet\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n\"\n\t\t+ \"                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 0\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n                -showPublishedAsConnected 0\\n                -showParentContainers 0\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 0\\n                -showCompounds 1\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 0\\n                -doNotSelectNewObjects 1\\n                -dropIsParent 1\\n                -transmitFilters 0\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n\"\n\t\t+ \"                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 0\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"DopeSheetEd\\\");\\n            dopeSheetEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -outliner \\\"dopeSheetPanel1OutlineEd\\\" \\n                -showSummary 1\\n                -showScene 0\\n                -hierarchyBelow 0\\n\"\n\t\t+ \"                -showTicks 1\\n                -selectionWindow 0 0 0 0 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"timeEditorPanel\\\" (localizedPanelLabel(\\\"Time Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Time Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"clipEditorPanel\\\" (localizedPanelLabel(\\\"Trax Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Trax Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = clipEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 0 \\n\"\n\t\t+ \"                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"sequenceEditorPanel\\\" (localizedPanelLabel(\\\"Camera Sequencer\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Camera Sequencer\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = sequenceEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 1 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperGraphPanel\\\" (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\n\\t\\t\\t$editorName = ($panelName+\\\"HyperGraphEd\\\");\\n            hyperGraph -e \\n                -graphLayoutStyle \\\"hierarchicalLayout\\\" \\n                -orientation \\\"horiz\\\" \\n                -mergeConnections 0\\n                -zoom 1\\n                -animateTransition 0\\n                -showRelationships 1\\n                -showShapes 0\\n                -showDeformers 0\\n                -showExpressions 0\\n                -showConstraints 0\\n                -showConnectionFromSelected 0\\n                -showConnectionToSelected 0\\n                -showConstraintLabels 0\\n                -showUnderworld 0\\n                -showInvisible 0\\n                -transitionFrames 1\\n                -opaqueContainers 0\\n                -freeform 0\\n                -imagePosition 0 0 \\n                -imageScale 1\\n                -imageEnabled 0\\n                -graphType \\\"DAG\\\" \\n                -heatMapDisplay 0\\n                -updateSelection 1\\n                -updateNodeAdded 1\\n                -useDrawOverrideColor 0\\n                -limitGraphTraversal -1\\n\"\n\t\t+ \"                -range 0 0 \\n                -iconSize \\\"smallIcons\\\" \\n                -showCachedConnections 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperShadePanel\\\" (localizedPanelLabel(\\\"Hypershade\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypershade\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"visorPanel\\\" (localizedPanelLabel(\\\"Visor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Visor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"nodeEditorPanel\\\" (localizedPanelLabel(\\\"Node Editor\\\")) `;\\n\\tif ($nodeEditorPanelVisible || $nodeEditorWorkspaceControlOpen) {\\n\"\n\t\t+ \"\\t\\tif (\\\"\\\" == $panelName) {\\n\\t\\t\\tif ($useSceneConfig) {\\n\\t\\t\\t\\t$panelName = `scriptedPanel -unParent  -type \\\"nodeEditorPanel\\\" -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels `;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n\"\n\t\t+ \"                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\t}\\n\\t\\t} else {\\n\\t\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n\"\n\t\t+ \"                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"createNodePanel\\\" (localizedPanelLabel(\\\"Create Node\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Create Node\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"polyTexturePlacementPanel\\\" (localizedPanelLabel(\\\"UV Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"UV Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"renderWindowPanel\\\" (localizedPanelLabel(\\\"Render View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Render View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"shapePanel\\\" (localizedPanelLabel(\\\"Shape Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tshapePanel -edit -l (localizedPanelLabel(\\\"Shape Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"posePanel\\\" (localizedPanelLabel(\\\"Pose Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tposePanel -edit -l (localizedPanelLabel(\\\"Pose Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynRelEdPanel\\\" (localizedPanelLabel(\\\"Dynamic Relationships\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dynamic Relationships\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"relationshipPanel\\\" (localizedPanelLabel(\\\"Relationship Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Relationship Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"referenceEditorPanel\\\" (localizedPanelLabel(\\\"Reference Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Reference Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynPaintScriptedPanelType\\\" (localizedPanelLabel(\\\"Paint Effects\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Paint Effects\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"scriptEditorPanel\\\" (localizedPanelLabel(\\\"Script Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Script Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"profilerPanel\\\" (localizedPanelLabel(\\\"Profiler Tool\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Profiler Tool\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"contentBrowserPanel\\\" (localizedPanelLabel(\\\"Content Browser\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Content Browser\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"Stereo\\\" (localizedPanelLabel(\\\"Stereo\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Stereo\\\")) -mbv $menusOkayInPanels  $panelName;\\n{ string $editorName = ($panelName+\\\"Editor\\\");\\n            stereoCameraView -e \\n                -camera \\\"|persp\\\" \\n                -useInteractiveMode 0\\n                -displayLights \\\"default\\\" \\n                -displayAppearance \\\"wireframe\\\" \\n                -activeOnly 0\\n                -ignorePanZoom 0\\n                -wireframeOnShaded 0\\n                -headsUpDisplay 1\\n                -holdOuts 1\\n                -selectionHiliteDisplay 1\\n                -useDefaultMaterial 0\\n                -bufferMode \\\"double\\\" \\n                -twoSidedLighting 1\\n                -backfaceCulling 0\\n                -xray 0\\n                -jointXray 0\\n                -activeComponentsXray 0\\n                -displayTextures 0\\n                -smoothWireframe 0\\n                -lineWidth 1\\n                -textureAnisotropic 0\\n                -textureHilight 1\\n                -textureSampling 2\\n\"\n\t\t+ \"                -textureDisplay \\\"modulate\\\" \\n                -textureMaxSize 32768\\n                -fogging 0\\n                -fogSource \\\"fragment\\\" \\n                -fogMode \\\"linear\\\" \\n                -fogStart 0\\n                -fogEnd 100\\n                -fogDensity 0.1\\n                -fogColor 0.5 0.5 0.5 1 \\n                -depthOfFieldPreview 1\\n                -maxConstantTransparency 1\\n                -objectFilterShowInHUD 1\\n                -isFiltered 0\\n                -colorResolution 4 4 \\n                -bumpResolution 4 4 \\n                -textureCompression 0\\n                -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n                -transpInShadows 0\\n                -cullingOverride \\\"none\\\" \\n                -lowQualityLighting 0\\n                -maximumNumHardwareLights 0\\n                -occlusionCulling 0\\n                -shadingModel 0\\n                -useBaseRenderer 0\\n                -useReducedRenderer 0\\n                -smallObjectCulling 0\\n                -smallObjectThreshold -1 \\n                -interactiveDisableShadows 0\\n\"\n\t\t+ \"                -interactiveBackFaceCull 0\\n                -sortTransparent 1\\n                -controllers 1\\n                -nurbsCurves 1\\n                -nurbsSurfaces 1\\n                -polymeshes 1\\n                -subdivSurfaces 1\\n                -planes 1\\n                -lights 1\\n                -cameras 1\\n                -controlVertices 1\\n                -hulls 1\\n                -grid 1\\n                -imagePlane 1\\n                -joints 1\\n                -ikHandles 1\\n                -deformers 1\\n                -dynamics 1\\n                -particleInstancers 1\\n                -fluids 1\\n                -hairSystems 1\\n                -follicles 1\\n                -nCloths 1\\n                -nParticles 1\\n                -nRigids 1\\n                -dynamicConstraints 1\\n                -locators 1\\n                -manipulators 1\\n                -pluginShapes 1\\n                -dimensions 1\\n                -handles 1\\n                -pivots 1\\n                -textures 1\\n                -strokes 1\\n                -motionTrails 1\\n\"\n\t\t+ \"                -clipGhosts 1\\n                -bluePencil 1\\n                -greasePencils 0\\n                -shadows 0\\n                -captureSequenceNumber -1\\n                -width 0\\n                -height 0\\n                -sceneRenderFilter 0\\n                -displayMode \\\"centerEye\\\" \\n                -viewColor 0 0 0 1 \\n                -useCustomBackground 1\\n                $editorName;\\n            stereoCameraView -e -viewSelected 0 $editorName; };\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\tif ($useSceneConfig) {\\n        string $configName = `getPanel -cwl (localizedPanelLabel(\\\"Current Layout\\\"))`;\\n        if (\\\"\\\" != $configName) {\\n\\t\\t\\tpanelConfiguration -edit -label (localizedPanelLabel(\\\"Current Layout\\\")) \\n\\t\\t\\t\\t-userCreated false\\n\\t\\t\\t\\t-defaultImage \\\"vacantCell.xP:/\\\"\\n\\t\\t\\t\\t-image \\\"\\\"\\n\\t\\t\\t\\t-sc false\\n\\t\\t\\t\\t-configString \\\"global string $gMainPane; paneLayout -e -cn \\\\\\\"quad\\\\\\\" -ps 1 50 50 -ps 2 50 50 -ps 3 50 50 -ps 4 50 50 $gMainPane;\\\"\\n\\t\\t\\t\\t-removeAllPanels\\n\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Top View\\\")) \\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Persp View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera persp` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera persp` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Side View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|side\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|side\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Front View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera front` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera front` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t$configName;\\n\\n            setNamedPanelLayout (localizedPanelLabel(\\\"Current Layout\\\"));\\n        }\\n\\n        panelHistory -e -clear mainPanelHistory;\\n        sceneUIReplacement -clear;\\n\\t}\\n\\n\\ngrid -spacing 5 -size 12 -divisions 5 -displayAxes yes -displayGridLines yes -displayDivisionLines yes -displayPerspectiveLabels no -displayOrthographicLabels no -displayAxesBold yes -perspectiveLabelPosition axis -orthographicLabelPosition edge;\\nviewManip -drawCompass 0 -compassAngle 0 -frontParameters \\\"\\\" -homeParameters \\\"\\\" -selectionLockParameters \\\"\\\";\\n}\\n\");\n\tsetAttr \".st\" 3;\ncreateNode script -n \"sceneConfigurationScriptNode\";\n\trename -uid \"72B19BC2-43A2-E229-0A73-2CB861A291D1\";\n\tsetAttr \".b\" -type \"string\" \"playbackOptions -min 1000 -max 1001 -ast 1000 -aet 1001 \";\n\tsetAttr \".st\" 6;\ncreateNode polyDisc -n \"polyDisc1\";\n\trename -uid \"9ED8A7BD-4FFD-6107-4322-35ACD1D3AC42\";\ncreateNode aiOptions -s -n \"defaultArnoldRenderOptions\";\n\trename -uid \"51BB3D7A-4C7D-FCDD-1B21-D89934107F98\";\n\tsetAttr \".skip_license_check\" yes;\ncreateNode aiAOVFilter -s -n \"defaultArnoldFilter\";\n\trename -uid \"989A5992-46C8-0CB2-B2B8-4E868F31FEA8\";\n\tsetAttr \".ai_translator\" -type \"string\" \"gaussian\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDriver\";\n\trename -uid \"B8469AD8-47C8-9EF1-FE5E-5AB50ABC1536\";\n\tsetAttr \".merge_AOVs\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"exr\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDisplayDriver\";\n\trename -uid \"21D4F4CD-4D78-001C-610D-798402CD7CF0\";\n\tsetAttr \".output_mode\" 0;\n\tsetAttr \".ai_translator\" -type \"string\" \"maya\";\ncreateNode objectSet -n \"workfileMain\";\n\trename -uid \"3C9B5D6F-4579-8E3B-5B7D-4C88865A1C68\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"instance_id\" -ln \"instance_id\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".hio\" yes;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:30d256dac64c\";\n\tsetAttr \".instance_id\" -type \"string\" \"911dc92a-ad29-41e5-bbf9-733d56174fb9\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"workfile\";\n\tsetAttr \".subset\" -type \"string\" \"workfileTest_task\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.workfile\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ExtractImportReference\\\": {\\\"active\\\": false}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"\";\ncreateNode renderSetupLayer -n \"Main\";\n\trename -uid \"2202E438-4CEF-F64E-737C-F48C65E31126\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode renderLayer -n \"rs_Main\";\n\trename -uid \"DF0259B1-4F96-DA7E-C74F-B599FBE15C18\";\n\tsetAttr \".do\" 1;\ncreateNode collection -n \"defaultCollection\";\n\trename -uid \"E5014237-4DA9-6FC0-633D-69AFA56161B3\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode simpleSelector -n \"defaultCollectionSelector\";\n\trename -uid \"7CA2F6D8-483C-B020-BC03-EF9563A52163\";\n\tsetAttr \".pat\" -type \"string\" \"*\";\nselect -ne :time1;\n\tsetAttr \".o\" 1001;\n\tsetAttr \".unw\" 1001;\nselect -ne :hardwareRenderingGlobals;\n\tsetAttr \".otfna\" -type \"stringArray\" 22 \"NURBS Curves\" \"NURBS Surfaces\" \"Polygons\" \"Subdiv Surface\" \"Particles\" \"Particle Instance\" \"Fluids\" \"Strokes\" \"Image Planes\" \"UI\" \"Lights\" \"Cameras\" \"Locators\" \"Joints\" \"IK Handles\" \"Deformers\" \"Motion Trails\" \"Components\" \"Hair Systems\" \"Follicles\" \"Misc. UI\" \"Ornaments\"  ;\n\tsetAttr \".otfva\" -type \"Int32Array\" 22 0 1 1 1 1 1\n\t\t 1 1 1 0 0 0 0 0 0 0 0 0\n\t\t 0 0 0 0 ;\n\tsetAttr \".fprt\" yes;\nselect -ne :renderPartition;\n\tsetAttr -s 2 \".st\";\nselect -ne :renderGlobalsList1;\nselect -ne :defaultShaderList1;\n\tsetAttr -s 5 \".s\";\nselect -ne :postProcessList1;\n\tsetAttr -s 2 \".p\";\nselect -ne :defaultRenderingList1;\n\tsetAttr -s 2 \".r\";\nselect -ne :standardSurface1;\n\tsetAttr \".b\" 0.80000001192092896;\n\tsetAttr \".bc\" -type \"float3\" 1 1 1 ;\n\tsetAttr \".s\" 0.20000000298023224;\nselect -ne :initialShadingGroup;\n\tsetAttr -s 2 \".dsm\";\n\tsetAttr \".ro\" yes;\nselect -ne :initialParticleSE;\n\tsetAttr \".ro\" yes;\nselect -ne :defaultRenderGlobals;\n\taddAttr -ci true -h true -sn \"dss\" -ln \"defaultSurfaceShader\" -dt \"string\";\n\tsetAttr \".ren\" -type \"string\" \"arnold\";\n\tsetAttr \".outf\" 51;\n\tsetAttr \".imfkey\" -type \"string\" \"exr\";\n\tsetAttr \".an\" yes;\n\tsetAttr \".fs\" 1001;\n\tsetAttr \".ef\" 1001;\n\tsetAttr \".oft\" -type \"string\" \"\";\n\tsetAttr \".pff\" yes;\n\tsetAttr \".ifp\" -type \"string\" \"<Scene>/<RenderLayer>/<RenderLayer>\";\n\tsetAttr \".rv\" -type \"string\" \"\";\n\tsetAttr \".pram\" -type \"string\" \"\";\n\tsetAttr \".poam\" -type \"string\" \"\";\n\tsetAttr \".prlm\" -type \"string\" \"\";\n\tsetAttr \".polm\" -type \"string\" \"\";\n\tsetAttr \".prm\" -type \"string\" \"\";\n\tsetAttr \".pom\" -type \"string\" \"\";\n\tsetAttr \".dss\" -type \"string\" \"lambert1\";\nselect -ne :defaultResolution;\n\tsetAttr \".w\" 1920;\n\tsetAttr \".h\" 1080;\n\tsetAttr \".pa\" 1;\n\tsetAttr \".al\" yes;\n\tsetAttr \".dar\" 1.6666666269302368;\nselect -ne :defaultColorMgtGlobals;\n\tsetAttr \".cfe\" yes;\n\tsetAttr \".cfp\" -type \"string\" \"<MAYA_RESOURCES>/OCIO-configs/Maya-legacy/config.ocio\";\n\tsetAttr \".vtn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".vn\" -type \"string\" \"sRGB gamma\";\n\tsetAttr \".dn\" -type \"string\" \"legacy\";\n\tsetAttr \".wsn\" -type \"string\" \"scene-linear Rec 709/sRGB\";\n\tsetAttr \".ovt\" no;\n\tsetAttr \".povt\" no;\n\tsetAttr \".otn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".potn\" -type \"string\" \"sRGB gamma (legacy)\";\nselect -ne :hardwareRenderGlobals;\n\tsetAttr \".ctrs\" 256;\n\tsetAttr \".btrs\" 512;\nconnectAttr \"rs_Main.ri\" \":persp.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":top.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":front.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":side.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"pSphere1_GEO.rlio[0]\";\nconnectAttr \"polySphere1.out\" \"pSphere1_GEOShape1.i\";\nconnectAttr \"rs_Main.ri\" \"pDisc1.rlio[0]\";\nconnectAttr \"polyDisc1.output\" \"pDiscShape1.i\";\nconnectAttr \"rs_Main.ri\" \"persp1.rlio[0]\";\nrelationship \"link\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"link\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nconnectAttr \"layerManager.dli[0]\" \"defaultLayer.id\";\nconnectAttr \"renderLayerManager.rlmi[0]\" \"defaultRenderLayer.rlid\";\nconnectAttr \"Main.msg\" \"renderSetup.frl\";\nconnectAttr \"Main.msg\" \"renderSetup.lrl\";\nconnectAttr \"pSphere1_GEO.iog\" \"modelMain.dsm\" -na;\nconnectAttr \":defaultArnoldDisplayDriver.msg\" \":defaultArnoldRenderOptions.drivers\"\n\t\t -na;\nconnectAttr \":defaultArnoldFilter.msg\" \":defaultArnoldRenderOptions.filt\";\nconnectAttr \":defaultArnoldDriver.msg\" \":defaultArnoldRenderOptions.drvr\";\nconnectAttr \"rs_Main.msg\" \"Main.lrl\";\nconnectAttr \"renderSetup.lit\" \"Main.pls\";\nconnectAttr \"defaultCollection.msg\" \"Main.cl\";\nconnectAttr \"defaultCollection.msg\" \"Main.ch\";\nconnectAttr \"renderLayerManager.rlmi[1]\" \"rs_Main.rlid\";\nconnectAttr \"defaultCollectionSelector.c\" \"defaultCollection.sel\";\nconnectAttr \"Main.lit\" \"defaultCollection.pls\";\nconnectAttr \"Main.nic\" \"defaultCollection.pic\";\nconnectAttr \"defaultRenderLayer.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"rs_Main.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"pSphere1_GEOShape1.iog\" \":initialShadingGroup.dsm\" -na;\nconnectAttr \"pDiscShape1.iog\" \":initialShadingGroup.dsm\" -na;\n// End of test_project_test_asset_test_task_v002.ma\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/workspace.mel",
    "content": "//Maya 2019 Project Definition\n\nworkspace -fr \"shaders\" \"renderData/shaders\";\nworkspace -fr \"images\" \"renders\";\nworkspace -fr \"mayaAscii\" \"\";\nworkspace -fr \"particles\" \"particles\";\nworkspace -fr \"mayaBinary\" \"\";\nworkspace -fr \"scene\" \"\";\nworkspace -fr \"alembicCache\" \"cache/alembic\";\nworkspace -fr \"renderData\" \"renderData\";\nworkspace -fr \"sourceImages\" \"sourceimages\";\nworkspace -fr \"fileCache\" \"cache/nCache\";\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya/input/dumps/avalon_tests/test_project.metadata.json",
    "content": "{\"indexes\":[{\"v\":{\"$numberInt\":\"2\"},\"key\":{\"_id\":{\"$numberInt\":\"1\"}},\"name\":\"_id_\"}],\"uuid\":\"8d778e3bbb3448ff9311bc7619ed478c\",\"collectionName\":\"test_project\"}"
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya/input/dumps/openpype_tests/settings.bson",
    "content": ""
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya/input/dumps/openpype_tests/settings.metadata.json",
    "content": "{\"indexes\":[{\"v\":{\"$numberInt\":\"2\"},\"key\":{\"_id\":{\"$numberInt\":\"1\"}},\"name\":\"_id_\"}],\"uuid\":\"3a0a55846c164eb5920568a766510c6d\",\"collectionName\":\"settings\"}"
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya/input/env_vars/env_var.json",
    "content": "{\n    \"OPENPYPE_MONGO\": \"{TEST_OPENPYPE_MONGO}\",\n    \"AVALON_MONGO\": \"{TEST_OPENPYPE_MONGO}\",\n    \"OPENPYPE_DATABASE_NAME\": \"{TEST_OPENPYPE_NAME}\",\n    \"AVALON_TIMEOUT\": \"3000\",\n    \"AVALON_DB\": \"{TEST_DB_NAME}\",\n    \"AVALON_PROJECT\": \"{TEST_PROJECT_NAME}\",\n    \"PYPE_DEBUG\": \"3\",\n    \"AVALON_CONFIG\": \"openpype\",\n    \"IS_TEST\": \"1\"\n}"
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya/input/startup/userSetup.py",
    "content": "import logging\nimport sys\n\nfrom maya import cmds\n\nimport pyblish.util\n\n\ndef setup_pyblish_logging():\n    log = logging.getLogger(\"pyblish\")\n    handler = logging.StreamHandler(sys.stdout)\n    formatter = logging.Formatter(\n        \"pyblish (%(levelname)s) (line: %(lineno)d) %(name)s:\"\n        \"\\n%(message)s\"\n    )\n    handler.setFormatter(formatter)\n    log.addHandler(handler)\n\n\ndef _run_publish_test_deferred():\n    try:\n        setup_pyblish_logging()\n        pyblish.util.publish()\n    finally:\n        cmds.quit(force=True)\n\n\ncmds.evalDeferred(\"_run_publish_test_deferred()\", lowestPriority=True)\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya/input/workfile/test_project_test_asset_test_task_v001.ma",
    "content": "//Maya ASCII 2023 scene\n//Name: test_project_test_asset_test_task_v002.ma\n//Last modified: Thu, Dec 07, 2023 03:53:06 PM\n//Codeset: 1252\nrequires maya \"2023\";\nrequires -nodeType \"simpleSelector\" -nodeType \"renderSetupLayer\" -nodeType \"renderSetup\"\n\t\t -nodeType \"collection\" \"renderSetup.py\" \"1.0\";\nrequires \"stereoCamera\" \"10.0\";\nrequires -nodeType \"aiOptions\" -nodeType \"aiAOVDriver\" -nodeType \"aiAOVFilter\" \"mtoa\" \"5.2.1.1\";\nrequires -nodeType \"polyDisc\" \"modelingToolkit\" \"0.0.0.0\";\nrequires \"stereoCamera\" \"10.0\";\ncurrentUnit -l centimeter -a degree -t pal;\nfileInfo \"application\" \"maya\";\nfileInfo \"product\" \"Maya 2023\";\nfileInfo \"version\" \"2023\";\nfileInfo \"cutIdentifier\" \"202211021031-847a9f9623\";\nfileInfo \"osv\" \"Windows 10 Pro v2009 (Build: 19045)\";\nfileInfo \"license\" \"education\";\nfileInfo \"UUID\" \"7A992745-4AD5-777F-5575-B4BFAC62B1D0\";\nfileInfo \"OpenPypeContext\" \"eyJwdWJsaXNoX2F0dHJpYnV0ZXMiOiB7IlZhbGlkYXRlQ29udGFpbmVycyI6IHsiYWN0aXZlIjogdHJ1ZX19fQ==\";\ncreateNode transform -s -n \"persp\";\n\trename -uid \"D52C935B-47C9-D868-A875-D799DD17B3A1\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 26.953352922736947 24.362683487437064 26.983403389430531 ;\n\tsetAttr \".r\" -type \"double3\" 1064.7698975365424 54.173034578109736 1075.1660763544442 ;\n\tsetAttr \".rp\" -type \"double3\" -2.0401242849359917e-14 2.2609331405046354e-14 -44.821869662029947 ;\n\tsetAttr \".rpt\" -type \"double3\" -27.999999999999989 -21.000000000000025 16.82186966202995 ;\ncreateNode camera -s -n \"perspShape\" -p \"persp\";\n\trename -uid \"2399E6C0-490F-BA1F-485F-5AA8A01D27BC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 46.895362757145833;\n\tsetAttr \".imn\" -type \"string\" \"persp\";\n\tsetAttr \".den\" -type \"string\" \"persp_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".ai_translator\" -type \"string\" \"perspective\";\ncreateNode transform -s -n \"top\";\n\trename -uid \"415C7426-413E-0FAE-FCC3-3DAED7443A52\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 1000.1 0 ;\n\tsetAttr \".r\" -type \"double3\" 90 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" 0 -1000.1 1000.1 ;\ncreateNode camera -s -n \"topShape\" -p \"top\";\n\trename -uid \"3BD0CF60-40DB-5278-5D8B-06ACBDA32122\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"top\";\n\tsetAttr \".den\" -type \"string\" \"top_depth\";\n\tsetAttr \".man\" -type \"string\" \"top_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -t %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"front\";\n\trename -uid \"D83DD5CE-4FE0-AB1B-81B2-87A63F0B7A05\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 0 0 1000.1 ;\n\tsetAttr \".r\" -type \"double3\" 180 0 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\ncreateNode camera -s -n \"frontShape\" -p \"front\";\n\trename -uid \"23313CBA-42C2-0B3A-0FCF-EA965EAC5DEC\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"front\";\n\tsetAttr \".den\" -type \"string\" \"front_depth\";\n\tsetAttr \".man\" -type \"string\" \"front_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -f %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -s -n \"side\";\n\trename -uid \"F70F692C-4A0D-BE64-9EE4-A99B6FA2D56E\";\n\tsetAttr \".v\" no;\n\tsetAttr \".t\" -type \"double3\" 1000.1 0 0 ;\n\tsetAttr \".r\" -type \"double3\" 180 -90 0 ;\n\tsetAttr \".rp\" -type \"double3\" 0 0 -1000.1 ;\n\tsetAttr \".rpt\" -type \"double3\" -1000.1 0 1000.1 ;\ncreateNode camera -s -n \"sideShape\" -p \"side\";\n\trename -uid \"C05669C3-420E-CA11-E5FC-7EB64EF8B632\";\n\tsetAttr -k off \".v\" no;\n\tsetAttr \".rnd\" no;\n\tsetAttr \".coi\" 1000.1;\n\tsetAttr \".ow\" 30;\n\tsetAttr \".imn\" -type \"string\" \"side\";\n\tsetAttr \".den\" -type \"string\" \"side_depth\";\n\tsetAttr \".man\" -type \"string\" \"side_mask\";\n\tsetAttr \".hc\" -type \"string\" \"viewSet -s %camera\";\n\tsetAttr \".o\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"orthographic\";\ncreateNode transform -n \"pSphere1_GEO\";\n\trename -uid \"7445A43F-444F-B2D3-4315-2AA013D2E0B6\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:440654b3dfe4\";\ncreateNode mesh -n \"pSphere1_GEOShape1\" -p \"pSphere1_GEO\";\n\trename -uid \"7C731260-45C6-339E-07BF-359446B08EA1\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:bf8e49bb98ec\";\ncreateNode transform -n \"pDisc1\";\n\trename -uid \"DED70CCF-4C19-16E4-9E5D-66A05037BA47\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:90e762703f08\";\ncreateNode mesh -n \"pDiscShape1\" -p \"pDisc1\";\n\trename -uid \"E1FCDCCF-4DE1-D3B9-C4F8-3285F1CF5B25\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".vir\" yes;\n\tsetAttr \".vif\" yes;\n\tsetAttr \".uvst[0].uvsn\" -type \"string\" \"map1\";\n\tsetAttr \".cuvs\" -type \"string\" \"map1\";\n\tsetAttr \".dcc\" -type \"string\" \"Ambient+Diffuse\";\n\tsetAttr \".covm[0]\"  0 1 1;\n\tsetAttr \".cdvm[0]\"  0 1 1;\n\tsetAttr \".ai_translator\" -type \"string\" \"polymesh\";\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:4ee3da11a1a4\";\ncreateNode transform -n \"persp1\";\n\trename -uid \"292F1351-4E41-A890-D6D5-A5A4F7D94C76\";\n\tsetAttr \".t\" -type \"double3\" 3.7889010960863949 2.8416759114717678 3.7889010364817537 ;\n\tsetAttr \".r\" -type \"double3\" -27.938352729602379 44.999999999999972 -5.172681101354183e-14 ;\ncreateNode camera -n \"perspShape1\" -p \"persp1\";\n\trename -uid \"9277418C-43C8-5064-A7C6-64AC829A76F2\";\n\tsetAttr -k off \".v\";\n\tsetAttr \".ovr\" 1.3;\n\tsetAttr \".fl\" 34.999999999999993;\n\tsetAttr \".coi\" 6.0652013012246453;\n\tsetAttr \".imn\" -type \"string\" \"persp1\";\n\tsetAttr \".den\" -type \"string\" \"persp1_depth\";\n\tsetAttr \".man\" -type \"string\" \"persp1_mask\";\n\tsetAttr \".tp\" -type \"double3\" -1.1920928955078125e-07 0 -1.7881393432617188e-07 ;\n\tsetAttr \".hc\" -type \"string\" \"viewSet -p %camera\";\n\tsetAttr \".dr\" yes;\ncreateNode lightLinker -s -n \"lightLinker1\";\n\trename -uid \"09465BD3-42E5-18E4-7906-20A99BB2A6C0\";\n\tsetAttr -s 2 \".lnk\";\n\tsetAttr -s 2 \".slnk\";\ncreateNode shapeEditorManager -n \"shapeEditorManager\";\n\trename -uid \"9F2E8009-4D69-046B-FCC4-28A8CE8F86DB\";\ncreateNode poseInterpolatorManager -n \"poseInterpolatorManager\";\n\trename -uid \"6757AD81-40B0-A747-69C3-D9A56259571E\";\ncreateNode displayLayerManager -n \"layerManager\";\n\trename -uid \"6F055ED5-4D91-8F85-7951-B4A13543A561\";\ncreateNode displayLayer -n \"defaultLayer\";\n\trename -uid \"4A776D1B-401F-7069-1C74-A7AAE84CEE03\";\n\tsetAttr \".ufem\" -type \"stringArray\" 0  ;\ncreateNode renderLayerManager -n \"renderLayerManager\";\n\trename -uid \"55626D2B-4FD5-61A1-7AB2-47B13F19D8AA\";\n\tsetAttr -s 2 \".rlmi[1]\"  1;\n\tsetAttr -s 2 \".rlmi\";\ncreateNode renderLayer -n \"defaultRenderLayer\";\n\trename -uid \"B134920D-4508-23BD-A6CA-11B43DE03F53\";\n\tsetAttr \".g\" yes;\ncreateNode renderSetup -n \"renderSetup\";\n\trename -uid \"9A8F0D15-41AB-CA70-C2D8-B78840BF9BC1\";\ncreateNode polySphere -n \"polySphere1\";\n\trename -uid \"DA319706-4ACF-B15C-53B2-48AC80D202EA\";\ncreateNode objectSet -n \"modelMain\";\n\trename -uid \"A76AD4F8-4CF5-AA0D-4E98-BABEE6454CC3\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"instance_id\" -ln \"instance_id\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"writeColorSets\" -ln \"writeColorSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"writeFaceSets\" -ln \"writeFaceSets\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"includeParentHierarchy\" -ln \"includeParentHierarchy\" -min \n\t\t0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"attr\" -ln \"attr\" -dt \"string\";\n\taddAttr -ci true -sn \"attrPrefix\" -ln \"attrPrefix\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:7364ea6776c9\";\n\tsetAttr \".instance_id\" -type \"string\" \"6889d3db-b813-43db-96de-9ba555dc4472\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"model\";\n\tsetAttr \".subset\" -type \"string\" \"modelMain\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.model\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr -cb on \".writeColorSets\";\n\tsetAttr -cb on \".writeFaceSets\";\n\tsetAttr -cb on \".includeParentHierarchy\";\n\tsetAttr \".attr\" -type \"string\" \"\";\n\tsetAttr \".attrPrefix\" -type \"string\" \"\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateNodeIDsRelated\\\": {\\\"active\\\": true}, \\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ValidateTransformNamingSuffix\\\": {\\\"active\\\": true}, \\\"ValidateColorSets\\\": {\\\"active\\\": true}, \\\"ValidateMeshHasUVs\\\": {\\\"active\\\": true}, \\\"ValidateMeshNonZeroEdgeLength\\\": {\\\"active\\\": true}, \\\"ExtractModel\\\": {\\\"active\\\": true}, \\\"ValidateMeshArnoldAttributes\\\": {\\\"active\\\": true}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"writeColorSets,writeFaceSets,includeParentHierarchy,attr,attrPrefix\";\ncreateNode script -n \"uiConfigurationScriptNode\";\n\trename -uid \"4B7AFB53-452E-E870-63E1-CCA1DD6EAF13\";\n\tsetAttr \".b\" -type \"string\" (\n\t\t\"// Maya Mel UI Configuration File.\\n//\\n//  This script is machine generated.  Edit at your own risk.\\n//\\n//\\n\\nglobal string $gMainPane;\\nif (`paneLayout -exists $gMainPane`) {\\n\\n\\tglobal int $gUseScenePanelConfig;\\n\\tint    $useSceneConfig = $gUseScenePanelConfig;\\n\\tint    $nodeEditorPanelVisible = stringArrayContains(\\\"nodeEditorPanel1\\\", `getPanel -vis`);\\n\\tint    $nodeEditorWorkspaceControlOpen = (`workspaceControl -exists nodeEditorPanel1Window` && `workspaceControl -q -visible nodeEditorPanel1Window`);\\n\\tint    $menusOkayInPanels = `optionVar -q allowMenusInPanels`;\\n\\tint    $nVisPanes = `paneLayout -q -nvp $gMainPane`;\\n\\tint    $nPanes = 0;\\n\\tstring $editorName;\\n\\tstring $panelName;\\n\\tstring $itemFilterName;\\n\\tstring $panelConfig;\\n\\n\\t//\\n\\t//  get current state of the UI\\n\\t//\\n\\tsceneUIReplacement -update $gMainPane;\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Top View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Top View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|top\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n\"\n\t\t+ \"            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n\"\n\t\t+ \"            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Side View\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Side View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|side\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n\"\n\t\t+ \"            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n\"\n\t\t+ \"            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n\"\n\t\t+ \"        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Front View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Front View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|front\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n\"\n\t\t+ \"            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n\"\n\t\t+ \"            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n\"\n\t\t+ \"            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 477\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"modelPanel\\\" (localizedPanelLabel(\\\"Persp View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tmodelPanel -edit -l (localizedPanelLabel(\\\"Persp View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        modelEditor -e \\n            -camera \\\"|persp\\\" \\n            -useInteractiveMode 0\\n            -displayLights \\\"default\\\" \\n            -displayAppearance \\\"smoothShaded\\\" \\n            -activeOnly 0\\n            -ignorePanZoom 0\\n            -wireframeOnShaded 0\\n            -headsUpDisplay 1\\n            -holdOuts 1\\n            -selectionHiliteDisplay 1\\n            -useDefaultMaterial 0\\n            -bufferMode \\\"double\\\" \\n\"\n\t\t+ \"            -twoSidedLighting 0\\n            -backfaceCulling 0\\n            -xray 0\\n            -jointXray 0\\n            -activeComponentsXray 0\\n            -displayTextures 0\\n            -smoothWireframe 0\\n            -lineWidth 1\\n            -textureAnisotropic 0\\n            -textureHilight 1\\n            -textureSampling 2\\n            -textureDisplay \\\"modulate\\\" \\n            -textureMaxSize 32768\\n            -fogging 0\\n            -fogSource \\\"fragment\\\" \\n            -fogMode \\\"linear\\\" \\n            -fogStart 0\\n            -fogEnd 100\\n            -fogDensity 0.1\\n            -fogColor 0.5 0.5 0.5 1 \\n            -depthOfFieldPreview 1\\n            -maxConstantTransparency 1\\n            -rendererName \\\"vp2Renderer\\\" \\n            -objectFilterShowInHUD 1\\n            -isFiltered 0\\n            -colorResolution 256 256 \\n            -bumpResolution 512 512 \\n            -textureCompression 0\\n            -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n            -transpInShadows 0\\n            -cullingOverride \\\"none\\\" \\n            -lowQualityLighting 0\\n\"\n\t\t+ \"            -maximumNumHardwareLights 1\\n            -occlusionCulling 0\\n            -shadingModel 0\\n            -useBaseRenderer 0\\n            -useReducedRenderer 0\\n            -smallObjectCulling 0\\n            -smallObjectThreshold -1 \\n            -interactiveDisableShadows 0\\n            -interactiveBackFaceCull 0\\n            -sortTransparent 1\\n            -controllers 1\\n            -nurbsCurves 1\\n            -nurbsSurfaces 1\\n            -polymeshes 1\\n            -subdivSurfaces 1\\n            -planes 1\\n            -lights 1\\n            -cameras 1\\n            -controlVertices 1\\n            -hulls 1\\n            -grid 1\\n            -imagePlane 1\\n            -joints 1\\n            -ikHandles 1\\n            -deformers 1\\n            -dynamics 1\\n            -particleInstancers 1\\n            -fluids 1\\n            -hairSystems 1\\n            -follicles 1\\n            -nCloths 1\\n            -nParticles 1\\n            -nRigids 1\\n            -dynamicConstraints 1\\n            -locators 1\\n            -manipulators 1\\n            -pluginShapes 1\\n\"\n\t\t+ \"            -dimensions 1\\n            -handles 1\\n            -pivots 1\\n            -textures 1\\n            -strokes 1\\n            -motionTrails 1\\n            -clipGhosts 1\\n            -bluePencil 1\\n            -greasePencils 0\\n            -shadows 0\\n            -captureSequenceNumber -1\\n            -width 476\\n            -height 276\\n            -sceneRenderFilter 0\\n            $editorName;\\n        modelEditor -e -viewSelected 0 $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"ToggledOutliner\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"ToggledOutliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -docTag \\\"isolOutln_fromSeln\\\" \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 1\\n            -showReferenceMembers 1\\n\"\n\t\t+ \"            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n\"\n\t\t+ \"            -isSet 0\\n            -isSetMember 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -selectCommand \\\"print(\\\\\\\"\\\\\\\")\\\" \\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n            -renderFilterIndex 0\\n            -selectionOrder \\\"chronological\\\" \\n            -expandAttribute 0\\n            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"outlinerPanel\\\" (localizedPanelLabel(\\\"Outliner\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\toutlinerPanel -edit -l (localizedPanelLabel(\\\"Outliner\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\t$editorName = $panelName;\\n        outlinerEditor -e \\n            -showShapes 0\\n            -showAssignedMaterials 0\\n            -showTimeEditor 1\\n            -showReferenceNodes 0\\n            -showReferenceMembers 0\\n            -showAttributes 0\\n            -showConnected 0\\n            -showAnimCurvesOnly 0\\n            -showMuteInfo 0\\n            -organizeByLayer 1\\n            -organizeByClip 1\\n            -showAnimLayerWeight 1\\n            -autoExpandLayers 1\\n            -autoExpand 0\\n            -showDagOnly 1\\n            -showAssets 1\\n            -showContainedOnly 1\\n            -showPublishedAsConnected 0\\n            -showParentContainers 0\\n            -showContainerContents 1\\n            -ignoreDagHierarchy 0\\n            -expandConnections 0\\n            -showUpstreamCurves 1\\n            -showUnitlessCurves 1\\n            -showCompounds 1\\n            -showLeafs 1\\n\"\n\t\t+ \"            -showNumericAttrsOnly 0\\n            -highlightActive 1\\n            -autoSelectNewObjects 0\\n            -doNotSelectNewObjects 0\\n            -dropIsParent 1\\n            -transmitFilters 0\\n            -setFilter \\\"defaultSetFilter\\\" \\n            -showSetMembers 1\\n            -allowMultiSelection 1\\n            -alwaysToggleSelect 0\\n            -directSelect 0\\n            -showUfeItems 1\\n            -displayMode \\\"DAG\\\" \\n            -expandObjects 0\\n            -setsIgnoreFilters 1\\n            -containersIgnoreFilters 0\\n            -editAttrName 0\\n            -showAttrValues 0\\n            -highlightSecondary 0\\n            -showUVAttrsOnly 0\\n            -showTextureNodesOnly 0\\n            -attrAlphaOrder \\\"default\\\" \\n            -animLayerFilterOptions \\\"allAffecting\\\" \\n            -sortOrder \\\"none\\\" \\n            -longNames 0\\n            -niceNames 1\\n            -showNamespace 1\\n            -showPinIcons 0\\n            -mapMotionTrails 0\\n            -ignoreHiddenAttribute 0\\n            -ignoreOutlinerColor 0\\n            -renderFilterVisible 0\\n\"\n\t\t+ \"            $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"graphEditor\\\" (localizedPanelLabel(\\\"Graph Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Graph Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 1\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n\"\n\t\t+ \"                -showPublishedAsConnected 0\\n                -showParentContainers 0\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 1\\n                -showCompounds 0\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 1\\n                -doNotSelectNewObjects 0\\n                -dropIsParent 1\\n                -transmitFilters 1\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n\"\n\t\t+ \"                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 1\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"GraphEd\\\");\\n            animCurveEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -showPlayRangeShades \\\"on\\\" \\n                -lockPlayRangeShades \\\"off\\\" \\n                -smoothness \\\"fine\\\" \\n                -resultSamples 1.041667\\n                -resultScreenSamples 0\\n                -resultUpdate \\\"delayed\\\" \\n                -showUpstreamCurves 1\\n                -keyMinScale 1\\n                -stackedCurvesMin -1\\n                -stackedCurvesMax 1\\n\"\n\t\t+ \"                -stackedCurvesSpace 0.2\\n                -preSelectionHighlight 0\\n                -constrainDrag 0\\n                -valueLinesToggle 1\\n                -highlightAffectedCurves 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dopeSheetPanel\\\" (localizedPanelLabel(\\\"Dope Sheet\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dope Sheet\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"OutlineEd\\\");\\n            outlinerEditor -e \\n                -showShapes 1\\n                -showAssignedMaterials 0\\n                -showTimeEditor 1\\n                -showReferenceNodes 0\\n                -showReferenceMembers 0\\n                -showAttributes 1\\n                -showConnected 1\\n                -showAnimCurvesOnly 1\\n                -showMuteInfo 0\\n                -organizeByLayer 1\\n                -organizeByClip 1\\n\"\n\t\t+ \"                -showAnimLayerWeight 1\\n                -autoExpandLayers 1\\n                -autoExpand 0\\n                -showDagOnly 0\\n                -showAssets 1\\n                -showContainedOnly 0\\n                -showPublishedAsConnected 0\\n                -showParentContainers 0\\n                -showContainerContents 0\\n                -ignoreDagHierarchy 0\\n                -expandConnections 1\\n                -showUpstreamCurves 1\\n                -showUnitlessCurves 0\\n                -showCompounds 1\\n                -showLeafs 1\\n                -showNumericAttrsOnly 1\\n                -highlightActive 0\\n                -autoSelectNewObjects 0\\n                -doNotSelectNewObjects 1\\n                -dropIsParent 1\\n                -transmitFilters 0\\n                -setFilter \\\"0\\\" \\n                -showSetMembers 0\\n                -allowMultiSelection 1\\n                -alwaysToggleSelect 0\\n                -directSelect 0\\n                -showUfeItems 1\\n                -displayMode \\\"DAG\\\" \\n                -expandObjects 0\\n\"\n\t\t+ \"                -setsIgnoreFilters 1\\n                -containersIgnoreFilters 0\\n                -editAttrName 0\\n                -showAttrValues 0\\n                -highlightSecondary 0\\n                -showUVAttrsOnly 0\\n                -showTextureNodesOnly 0\\n                -attrAlphaOrder \\\"default\\\" \\n                -animLayerFilterOptions \\\"allAffecting\\\" \\n                -sortOrder \\\"none\\\" \\n                -longNames 0\\n                -niceNames 1\\n                -showNamespace 1\\n                -showPinIcons 0\\n                -mapMotionTrails 1\\n                -ignoreHiddenAttribute 0\\n                -ignoreOutlinerColor 0\\n                -renderFilterVisible 0\\n                $editorName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"DopeSheetEd\\\");\\n            dopeSheetEditor -e \\n                -displayValues 0\\n                -snapTime \\\"integer\\\" \\n                -snapValue \\\"none\\\" \\n                -outliner \\\"dopeSheetPanel1OutlineEd\\\" \\n                -showSummary 1\\n                -showScene 0\\n                -hierarchyBelow 0\\n\"\n\t\t+ \"                -showTicks 1\\n                -selectionWindow 0 0 0 0 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"timeEditorPanel\\\" (localizedPanelLabel(\\\"Time Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Time Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"clipEditorPanel\\\" (localizedPanelLabel(\\\"Trax Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Trax Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = clipEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 0 \\n\"\n\t\t+ \"                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"sequenceEditorPanel\\\" (localizedPanelLabel(\\\"Camera Sequencer\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Camera Sequencer\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = sequenceEditorNameFromPanel($panelName);\\n            clipEditor -e \\n                -displayValues 0\\n                -snapTime \\\"none\\\" \\n                -snapValue \\\"none\\\" \\n                -initialized 0\\n                -manageSequencer 1 \\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperGraphPanel\\\" (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypergraph Hierarchy\\\")) -mbv $menusOkayInPanels  $panelName;\\n\"\n\t\t+ \"\\n\\t\\t\\t$editorName = ($panelName+\\\"HyperGraphEd\\\");\\n            hyperGraph -e \\n                -graphLayoutStyle \\\"hierarchicalLayout\\\" \\n                -orientation \\\"horiz\\\" \\n                -mergeConnections 0\\n                -zoom 1\\n                -animateTransition 0\\n                -showRelationships 1\\n                -showShapes 0\\n                -showDeformers 0\\n                -showExpressions 0\\n                -showConstraints 0\\n                -showConnectionFromSelected 0\\n                -showConnectionToSelected 0\\n                -showConstraintLabels 0\\n                -showUnderworld 0\\n                -showInvisible 0\\n                -transitionFrames 1\\n                -opaqueContainers 0\\n                -freeform 0\\n                -imagePosition 0 0 \\n                -imageScale 1\\n                -imageEnabled 0\\n                -graphType \\\"DAG\\\" \\n                -heatMapDisplay 0\\n                -updateSelection 1\\n                -updateNodeAdded 1\\n                -useDrawOverrideColor 0\\n                -limitGraphTraversal -1\\n\"\n\t\t+ \"                -range 0 0 \\n                -iconSize \\\"smallIcons\\\" \\n                -showCachedConnections 0\\n                $editorName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"hyperShadePanel\\\" (localizedPanelLabel(\\\"Hypershade\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Hypershade\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"visorPanel\\\" (localizedPanelLabel(\\\"Visor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Visor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"nodeEditorPanel\\\" (localizedPanelLabel(\\\"Node Editor\\\")) `;\\n\\tif ($nodeEditorPanelVisible || $nodeEditorWorkspaceControlOpen) {\\n\"\n\t\t+ \"\\t\\tif (\\\"\\\" == $panelName) {\\n\\t\\t\\tif ($useSceneConfig) {\\n\\t\\t\\t\\t$panelName = `scriptedPanel -unParent  -type \\\"nodeEditorPanel\\\" -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels `;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n\"\n\t\t+ \"                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\t}\\n\\t\\t} else {\\n\\t\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Node Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\n\\t\\t\\t$editorName = ($panelName+\\\"NodeEditorEd\\\");\\n            nodeEditor -e \\n                -allAttributes 0\\n                -allNodes 0\\n                -autoSizeNodes 1\\n                -consistentNameSize 1\\n                -createNodeCommand \\\"nodeEdCreateNodeCommand\\\" \\n                -connectNodeOnCreation 0\\n                -connectOnDrop 0\\n                -copyConnectionsOnPaste 0\\n                -connectionStyle \\\"bezier\\\" \\n                -defaultPinnedState 0\\n\"\n\t\t+ \"                -additiveGraphingMode 1\\n                -connectedGraphingMode 1\\n                -settingsChangedCallback \\\"nodeEdSyncControls\\\" \\n                -traversalDepthLimit -1\\n                -keyPressCommand \\\"nodeEdKeyPressCommand\\\" \\n                -nodeTitleMode \\\"name\\\" \\n                -gridSnap 0\\n                -gridVisibility 1\\n                -crosshairOnEdgeDragging 0\\n                -popupMenuScript \\\"nodeEdBuildPanelMenus\\\" \\n                -showNamespace 1\\n                -showShapes 1\\n                -showSGShapes 0\\n                -showTransforms 1\\n                -useAssets 1\\n                -syncedSelection 1\\n                -extendToShapes 1\\n                -showUnitConversions 0\\n                -editorMode \\\"default\\\" \\n                -hasWatchpoint 0\\n                $editorName;\\n\\t\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"createNodePanel\\\" (localizedPanelLabel(\\\"Create Node\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Create Node\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"polyTexturePlacementPanel\\\" (localizedPanelLabel(\\\"UV Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"UV Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"renderWindowPanel\\\" (localizedPanelLabel(\\\"Render View\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Render View\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"shapePanel\\\" (localizedPanelLabel(\\\"Shape Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\"\n\t\t+ \"\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tshapePanel -edit -l (localizedPanelLabel(\\\"Shape Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextPanel \\\"posePanel\\\" (localizedPanelLabel(\\\"Pose Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tposePanel -edit -l (localizedPanelLabel(\\\"Pose Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynRelEdPanel\\\" (localizedPanelLabel(\\\"Dynamic Relationships\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Dynamic Relationships\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"relationshipPanel\\\" (localizedPanelLabel(\\\"Relationship Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Relationship Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"referenceEditorPanel\\\" (localizedPanelLabel(\\\"Reference Editor\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Reference Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"dynPaintScriptedPanelType\\\" (localizedPanelLabel(\\\"Paint Effects\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Paint Effects\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"scriptEditorPanel\\\" (localizedPanelLabel(\\\"Script Editor\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Script Editor\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"profilerPanel\\\" (localizedPanelLabel(\\\"Profiler Tool\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Profiler Tool\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"contentBrowserPanel\\\" (localizedPanelLabel(\\\"Content Browser\\\")) `;\\n\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Content Browser\\\")) -mbv $menusOkayInPanels  $panelName;\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\t$panelName = `sceneUIReplacement -getNextScriptedPanel \\\"Stereo\\\" (localizedPanelLabel(\\\"Stereo\\\")) `;\\n\"\n\t\t+ \"\\tif (\\\"\\\" != $panelName) {\\n\\t\\t$label = `panel -q -label $panelName`;\\n\\t\\tscriptedPanel -edit -l (localizedPanelLabel(\\\"Stereo\\\")) -mbv $menusOkayInPanels  $panelName;\\n{ string $editorName = ($panelName+\\\"Editor\\\");\\n            stereoCameraView -e \\n                -camera \\\"|persp\\\" \\n                -useInteractiveMode 0\\n                -displayLights \\\"default\\\" \\n                -displayAppearance \\\"wireframe\\\" \\n                -activeOnly 0\\n                -ignorePanZoom 0\\n                -wireframeOnShaded 0\\n                -headsUpDisplay 1\\n                -holdOuts 1\\n                -selectionHiliteDisplay 1\\n                -useDefaultMaterial 0\\n                -bufferMode \\\"double\\\" \\n                -twoSidedLighting 1\\n                -backfaceCulling 0\\n                -xray 0\\n                -jointXray 0\\n                -activeComponentsXray 0\\n                -displayTextures 0\\n                -smoothWireframe 0\\n                -lineWidth 1\\n                -textureAnisotropic 0\\n                -textureHilight 1\\n                -textureSampling 2\\n\"\n\t\t+ \"                -textureDisplay \\\"modulate\\\" \\n                -textureMaxSize 32768\\n                -fogging 0\\n                -fogSource \\\"fragment\\\" \\n                -fogMode \\\"linear\\\" \\n                -fogStart 0\\n                -fogEnd 100\\n                -fogDensity 0.1\\n                -fogColor 0.5 0.5 0.5 1 \\n                -depthOfFieldPreview 1\\n                -maxConstantTransparency 1\\n                -objectFilterShowInHUD 1\\n                -isFiltered 0\\n                -colorResolution 4 4 \\n                -bumpResolution 4 4 \\n                -textureCompression 0\\n                -transparencyAlgorithm \\\"frontAndBackCull\\\" \\n                -transpInShadows 0\\n                -cullingOverride \\\"none\\\" \\n                -lowQualityLighting 0\\n                -maximumNumHardwareLights 0\\n                -occlusionCulling 0\\n                -shadingModel 0\\n                -useBaseRenderer 0\\n                -useReducedRenderer 0\\n                -smallObjectCulling 0\\n                -smallObjectThreshold -1 \\n                -interactiveDisableShadows 0\\n\"\n\t\t+ \"                -interactiveBackFaceCull 0\\n                -sortTransparent 1\\n                -controllers 1\\n                -nurbsCurves 1\\n                -nurbsSurfaces 1\\n                -polymeshes 1\\n                -subdivSurfaces 1\\n                -planes 1\\n                -lights 1\\n                -cameras 1\\n                -controlVertices 1\\n                -hulls 1\\n                -grid 1\\n                -imagePlane 1\\n                -joints 1\\n                -ikHandles 1\\n                -deformers 1\\n                -dynamics 1\\n                -particleInstancers 1\\n                -fluids 1\\n                -hairSystems 1\\n                -follicles 1\\n                -nCloths 1\\n                -nParticles 1\\n                -nRigids 1\\n                -dynamicConstraints 1\\n                -locators 1\\n                -manipulators 1\\n                -pluginShapes 1\\n                -dimensions 1\\n                -handles 1\\n                -pivots 1\\n                -textures 1\\n                -strokes 1\\n                -motionTrails 1\\n\"\n\t\t+ \"                -clipGhosts 1\\n                -bluePencil 1\\n                -greasePencils 0\\n                -shadows 0\\n                -captureSequenceNumber -1\\n                -width 0\\n                -height 0\\n                -sceneRenderFilter 0\\n                -displayMode \\\"centerEye\\\" \\n                -viewColor 0 0 0 1 \\n                -useCustomBackground 1\\n                $editorName;\\n            stereoCameraView -e -viewSelected 0 $editorName; };\\n\\t\\tif (!$useSceneConfig) {\\n\\t\\t\\tpanel -e -l $label $panelName;\\n\\t\\t}\\n\\t}\\n\\n\\n\\tif ($useSceneConfig) {\\n        string $configName = `getPanel -cwl (localizedPanelLabel(\\\"Current Layout\\\"))`;\\n        if (\\\"\\\" != $configName) {\\n\\t\\t\\tpanelConfiguration -edit -label (localizedPanelLabel(\\\"Current Layout\\\")) \\n\\t\\t\\t\\t-userCreated false\\n\\t\\t\\t\\t-defaultImage \\\"vacantCell.xP:/\\\"\\n\\t\\t\\t\\t-image \\\"\\\"\\n\\t\\t\\t\\t-sc false\\n\\t\\t\\t\\t-configString \\\"global string $gMainPane; paneLayout -e -cn \\\\\\\"quad\\\\\\\" -ps 1 50 50 -ps 2 50 50 -ps 3 50 50 -ps 4 50 50 $gMainPane;\\\"\\n\\t\\t\\t\\t-removeAllPanels\\n\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Top View\\\")) \\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Top View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|top\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Persp View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera persp` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Persp View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera persp` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Side View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|side\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Side View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -camera \\\\\\\"|side\\\\\\\" \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 476\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t-ap false\\n\\t\\t\\t\\t\\t(localizedPanelLabel(\\\"Front View\\\")) \\n\\t\\t\\t\\t\\t\\\"modelPanel\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"$panelName = `modelPanel -unParent -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels `;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera front` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t\\t\\\"modelPanel -edit -l (localizedPanelLabel(\\\\\\\"Front View\\\\\\\")) -mbv $menusOkayInPanels  $panelName;\\\\n$editorName = $panelName;\\\\nmodelEditor -e \\\\n    -cam `findStartUpCamera front` \\\\n    -useInteractiveMode 0\\\\n    -displayLights \\\\\\\"default\\\\\\\" \\\\n    -displayAppearance \\\\\\\"smoothShaded\\\\\\\" \\\\n    -activeOnly 0\\\\n    -ignorePanZoom 0\\\\n    -wireframeOnShaded 0\\\\n    -headsUpDisplay 1\\\\n    -holdOuts 1\\\\n    -selectionHiliteDisplay 1\\\\n    -useDefaultMaterial 0\\\\n    -bufferMode \\\\\\\"double\\\\\\\" \\\\n    -twoSidedLighting 0\\\\n    -backfaceCulling 0\\\\n    -xray 0\\\\n    -jointXray 0\\\\n    -activeComponentsXray 0\\\\n    -displayTextures 0\\\\n    -smoothWireframe 0\\\\n    -lineWidth 1\\\\n    -textureAnisotropic 0\\\\n    -textureHilight 1\\\\n    -textureSampling 2\\\\n    -textureDisplay \\\\\\\"modulate\\\\\\\" \\\\n    -textureMaxSize 32768\\\\n    -fogging 0\\\\n    -fogSource \\\\\\\"fragment\\\\\\\" \\\\n    -fogMode \\\\\\\"linear\\\\\\\" \\\\n    -fogStart 0\\\\n    -fogEnd 100\\\\n    -fogDensity 0.1\\\\n    -fogColor 0.5 0.5 0.5 1 \\\\n    -depthOfFieldPreview 1\\\\n    -maxConstantTransparency 1\\\\n    -rendererName \\\\\\\"vp2Renderer\\\\\\\" \\\\n    -objectFilterShowInHUD 1\\\\n    -isFiltered 0\\\\n    -colorResolution 256 256 \\\\n    -bumpResolution 512 512 \\\\n    -textureCompression 0\\\\n    -transparencyAlgorithm \\\\\\\"frontAndBackCull\\\\\\\" \\\\n    -transpInShadows 0\\\\n    -cullingOverride \\\\\\\"none\\\\\\\" \\\\n    -lowQualityLighting 0\\\\n    -maximumNumHardwareLights 1\\\\n    -occlusionCulling 0\\\\n    -shadingModel 0\\\\n    -useBaseRenderer 0\\\\n    -useReducedRenderer 0\\\\n    -smallObjectCulling 0\\\\n    -smallObjectThreshold -1 \\\\n    -interactiveDisableShadows 0\\\\n    -interactiveBackFaceCull 0\\\\n    -sortTransparent 1\\\\n    -controllers 1\\\\n    -nurbsCurves 1\\\\n    -nurbsSurfaces 1\\\\n    -polymeshes 1\\\\n    -subdivSurfaces 1\\\\n    -planes 1\\\\n    -lights 1\\\\n    -cameras 1\\\\n    -controlVertices 1\\\\n    -hulls 1\\\\n    -grid 1\\\\n    -imagePlane 1\\\\n    -joints 1\\\\n    -ikHandles 1\\\\n    -deformers 1\\\\n    -dynamics 1\\\\n    -particleInstancers 1\\\\n    -fluids 1\\\\n    -hairSystems 1\\\\n    -follicles 1\\\\n    -nCloths 1\\\\n    -nParticles 1\\\\n    -nRigids 1\\\\n    -dynamicConstraints 1\\\\n    -locators 1\\\\n    -manipulators 1\\\\n    -pluginShapes 1\\\\n    -dimensions 1\\\\n    -handles 1\\\\n    -pivots 1\\\\n    -textures 1\\\\n    -strokes 1\\\\n    -motionTrails 1\\\\n    -clipGhosts 1\\\\n    -bluePencil 1\\\\n    -greasePencils 0\\\\n    -shadows 0\\\\n    -captureSequenceNumber -1\\\\n    -width 477\\\\n    -height 276\\\\n    -sceneRenderFilter 0\\\\n    $editorName;\\\\nmodelEditor -e -viewSelected 0 $editorName\\\"\\n\"\n\t\t+ \"\\t\\t\\t\\t$configName;\\n\\n            setNamedPanelLayout (localizedPanelLabel(\\\"Current Layout\\\"));\\n        }\\n\\n        panelHistory -e -clear mainPanelHistory;\\n        sceneUIReplacement -clear;\\n\\t}\\n\\n\\ngrid -spacing 5 -size 12 -divisions 5 -displayAxes yes -displayGridLines yes -displayDivisionLines yes -displayPerspectiveLabels no -displayOrthographicLabels no -displayAxesBold yes -perspectiveLabelPosition axis -orthographicLabelPosition edge;\\nviewManip -drawCompass 0 -compassAngle 0 -frontParameters \\\"\\\" -homeParameters \\\"\\\" -selectionLockParameters \\\"\\\";\\n}\\n\");\n\tsetAttr \".st\" 3;\ncreateNode script -n \"sceneConfigurationScriptNode\";\n\trename -uid \"72B19BC2-43A2-E229-0A73-2CB861A291D1\";\n\tsetAttr \".b\" -type \"string\" \"playbackOptions -min 1000 -max 1001 -ast 1000 -aet 1001 \";\n\tsetAttr \".st\" 6;\ncreateNode polyDisc -n \"polyDisc1\";\n\trename -uid \"9ED8A7BD-4FFD-6107-4322-35ACD1D3AC42\";\ncreateNode aiOptions -s -n \"defaultArnoldRenderOptions\";\n\trename -uid \"51BB3D7A-4C7D-FCDD-1B21-D89934107F98\";\n\tsetAttr \".skip_license_check\" yes;\ncreateNode aiAOVFilter -s -n \"defaultArnoldFilter\";\n\trename -uid \"989A5992-46C8-0CB2-B2B8-4E868F31FEA8\";\n\tsetAttr \".ai_translator\" -type \"string\" \"gaussian\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDriver\";\n\trename -uid \"B8469AD8-47C8-9EF1-FE5E-5AB50ABC1536\";\n\tsetAttr \".merge_AOVs\" yes;\n\tsetAttr \".ai_translator\" -type \"string\" \"exr\";\ncreateNode aiAOVDriver -s -n \"defaultArnoldDisplayDriver\";\n\trename -uid \"21D4F4CD-4D78-001C-610D-798402CD7CF0\";\n\tsetAttr \".output_mode\" 0;\n\tsetAttr \".ai_translator\" -type \"string\" \"maya\";\ncreateNode objectSet -n \"workfileMain\";\n\trename -uid \"3C9B5D6F-4579-8E3B-5B7D-4C88865A1C68\";\n\taddAttr -ci true -sn \"cbId\" -ln \"cbId\" -dt \"string\";\n\taddAttr -ci true -sn \"instance_id\" -ln \"instance_id\" -dt \"string\";\n\taddAttr -ci true -sn \"id\" -ln \"id\" -dt \"string\";\n\taddAttr -ci true -sn \"family\" -ln \"family\" -dt \"string\";\n\taddAttr -ci true -sn \"subset\" -ln \"subset\" -dt \"string\";\n\taddAttr -ci true -sn \"active\" -ln \"active\" -min 0 -max 1 -at \"bool\";\n\taddAttr -ci true -sn \"creator_identifier\" -ln \"creator_identifier\" -dt \"string\";\n\taddAttr -ci true -sn \"variant\" -ln \"variant\" -dt \"string\";\n\taddAttr -ci true -sn \"asset\" -ln \"asset\" -dt \"string\";\n\taddAttr -ci true -sn \"task\" -ln \"task\" -dt \"string\";\n\taddAttr -ci true -sn \"publish_attributes\" -ln \"publish_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"creator_attributes\" -ln \"creator_attributes\" -dt \"string\";\n\taddAttr -ci true -sn \"__creator_attributes_keys\" -ln \"__creator_attributes_keys\"\n\t\t-dt \"string\";\n\tsetAttr \".ihi\" 0;\n\tsetAttr \".hio\" yes;\n\tsetAttr \".cbId\" -type \"string\" \"60df31e2be2b48bd3695c056:30d256dac64c\";\n\tsetAttr \".instance_id\" -type \"string\" \"911dc92a-ad29-41e5-bbf9-733d56174fb9\";\n\tsetAttr \".id\" -type \"string\" \"pyblish.avalon.instance\";\n\tsetAttr \".family\" -type \"string\" \"workfile\";\n\tsetAttr \".subset\" -type \"string\" \"workfileTest_task\";\n\tsetAttr -cb on \".active\" yes;\n\tsetAttr \".creator_identifier\" -type \"string\" \"io.openpype.creators.maya.workfile\";\n\tsetAttr \".variant\" -type \"string\" \"Main\";\n\tsetAttr \".asset\" -type \"string\" \"test_asset\";\n\tsetAttr \".task\" -type \"string\" \"test_task\";\n\tsetAttr \".publish_attributes\" -type \"string\" \"{\\\"ValidateInstanceInContext\\\": {\\\"active\\\": true}, \\\"ExtractImportReference\\\": {\\\"active\\\": false}}\";\n\tsetAttr \".creator_attributes\" -type \"string\" \"{}\";\n\tsetAttr \".__creator_attributes_keys\" -type \"string\" \"\";\ncreateNode renderSetupLayer -n \"Main\";\n\trename -uid \"2202E438-4CEF-F64E-737C-F48C65E31126\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode renderLayer -n \"rs_Main\";\n\trename -uid \"DF0259B1-4F96-DA7E-C74F-B599FBE15C18\";\n\tsetAttr \".do\" 1;\ncreateNode collection -n \"defaultCollection\";\n\trename -uid \"E5014237-4DA9-6FC0-633D-69AFA56161B3\";\n\taddAttr -ci true -sn \"es\" -ln \"expandedState\" -min 0 -max 1 -at \"bool\";\n\tsetAttr \".es\" yes;\ncreateNode simpleSelector -n \"defaultCollectionSelector\";\n\trename -uid \"7CA2F6D8-483C-B020-BC03-EF9563A52163\";\n\tsetAttr \".pat\" -type \"string\" \"*\";\nselect -ne :time1;\n\tsetAttr \".o\" 1001;\n\tsetAttr \".unw\" 1001;\nselect -ne :hardwareRenderingGlobals;\n\tsetAttr \".otfna\" -type \"stringArray\" 22 \"NURBS Curves\" \"NURBS Surfaces\" \"Polygons\" \"Subdiv Surface\" \"Particles\" \"Particle Instance\" \"Fluids\" \"Strokes\" \"Image Planes\" \"UI\" \"Lights\" \"Cameras\" \"Locators\" \"Joints\" \"IK Handles\" \"Deformers\" \"Motion Trails\" \"Components\" \"Hair Systems\" \"Follicles\" \"Misc. UI\" \"Ornaments\"  ;\n\tsetAttr \".otfva\" -type \"Int32Array\" 22 0 1 1 1 1 1\n\t\t 1 1 1 0 0 0 0 0 0 0 0 0\n\t\t 0 0 0 0 ;\n\tsetAttr \".fprt\" yes;\nselect -ne :renderPartition;\n\tsetAttr -s 2 \".st\";\nselect -ne :renderGlobalsList1;\nselect -ne :defaultShaderList1;\n\tsetAttr -s 5 \".s\";\nselect -ne :postProcessList1;\n\tsetAttr -s 2 \".p\";\nselect -ne :defaultRenderingList1;\n\tsetAttr -s 2 \".r\";\nselect -ne :standardSurface1;\n\tsetAttr \".b\" 0.80000001192092896;\n\tsetAttr \".bc\" -type \"float3\" 1 1 1 ;\n\tsetAttr \".s\" 0.20000000298023224;\nselect -ne :initialShadingGroup;\n\tsetAttr -s 2 \".dsm\";\n\tsetAttr \".ro\" yes;\nselect -ne :initialParticleSE;\n\tsetAttr \".ro\" yes;\nselect -ne :defaultRenderGlobals;\n\taddAttr -ci true -h true -sn \"dss\" -ln \"defaultSurfaceShader\" -dt \"string\";\n\tsetAttr \".ren\" -type \"string\" \"arnold\";\n\tsetAttr \".outf\" 51;\n\tsetAttr \".imfkey\" -type \"string\" \"exr\";\n\tsetAttr \".an\" yes;\n\tsetAttr \".fs\" 1001;\n\tsetAttr \".ef\" 1001;\n\tsetAttr \".oft\" -type \"string\" \"\";\n\tsetAttr \".pff\" yes;\n\tsetAttr \".ifp\" -type \"string\" \"<Scene>/<RenderLayer>/<RenderLayer>\";\n\tsetAttr \".rv\" -type \"string\" \"\";\n\tsetAttr \".pram\" -type \"string\" \"\";\n\tsetAttr \".poam\" -type \"string\" \"\";\n\tsetAttr \".prlm\" -type \"string\" \"\";\n\tsetAttr \".polm\" -type \"string\" \"\";\n\tsetAttr \".prm\" -type \"string\" \"\";\n\tsetAttr \".pom\" -type \"string\" \"\";\n\tsetAttr \".dss\" -type \"string\" \"lambert1\";\nselect -ne :defaultResolution;\n\tsetAttr \".w\" 1920;\n\tsetAttr \".h\" 1080;\n\tsetAttr \".pa\" 1;\n\tsetAttr \".al\" yes;\n\tsetAttr \".dar\" 1.6666666269302368;\nselect -ne :defaultColorMgtGlobals;\n\tsetAttr \".cfe\" yes;\n\tsetAttr \".cfp\" -type \"string\" \"<MAYA_RESOURCES>/OCIO-configs/Maya-legacy/config.ocio\";\n\tsetAttr \".vtn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".vn\" -type \"string\" \"sRGB gamma\";\n\tsetAttr \".dn\" -type \"string\" \"legacy\";\n\tsetAttr \".wsn\" -type \"string\" \"scene-linear Rec 709/sRGB\";\n\tsetAttr \".ovt\" no;\n\tsetAttr \".povt\" no;\n\tsetAttr \".otn\" -type \"string\" \"sRGB gamma (legacy)\";\n\tsetAttr \".potn\" -type \"string\" \"sRGB gamma (legacy)\";\nselect -ne :hardwareRenderGlobals;\n\tsetAttr \".ctrs\" 256;\n\tsetAttr \".btrs\" 512;\nconnectAttr \"rs_Main.ri\" \":persp.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":top.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":front.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \":side.rlio[0]\";\nconnectAttr \"rs_Main.ri\" \"pSphere1_GEO.rlio[0]\";\nconnectAttr \"polySphere1.out\" \"pSphere1_GEOShape1.i\";\nconnectAttr \"rs_Main.ri\" \"pDisc1.rlio[0]\";\nconnectAttr \"polyDisc1.output\" \"pDiscShape1.i\";\nconnectAttr \"rs_Main.ri\" \"persp1.rlio[0]\";\nrelationship \"link\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"link\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialShadingGroup.message\" \":defaultLightSet.message\";\nrelationship \"shadowLink\" \":lightLinker1\" \":initialParticleSE.message\" \":defaultLightSet.message\";\nconnectAttr \"layerManager.dli[0]\" \"defaultLayer.id\";\nconnectAttr \"renderLayerManager.rlmi[0]\" \"defaultRenderLayer.rlid\";\nconnectAttr \"Main.msg\" \"renderSetup.frl\";\nconnectAttr \"Main.msg\" \"renderSetup.lrl\";\nconnectAttr \"pSphere1_GEO.iog\" \"modelMain.dsm\" -na;\nconnectAttr \":defaultArnoldDisplayDriver.msg\" \":defaultArnoldRenderOptions.drivers\"\n\t\t -na;\nconnectAttr \":defaultArnoldFilter.msg\" \":defaultArnoldRenderOptions.filt\";\nconnectAttr \":defaultArnoldDriver.msg\" \":defaultArnoldRenderOptions.drvr\";\nconnectAttr \"rs_Main.msg\" \"Main.lrl\";\nconnectAttr \"renderSetup.lit\" \"Main.pls\";\nconnectAttr \"defaultCollection.msg\" \"Main.cl\";\nconnectAttr \"defaultCollection.msg\" \"Main.ch\";\nconnectAttr \"renderLayerManager.rlmi[1]\" \"rs_Main.rlid\";\nconnectAttr \"defaultCollectionSelector.c\" \"defaultCollection.sel\";\nconnectAttr \"Main.lit\" \"defaultCollection.pls\";\nconnectAttr \"Main.nic\" \"defaultCollection.pic\";\nconnectAttr \"defaultRenderLayer.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"rs_Main.msg\" \":defaultRenderingList1.r\" -na;\nconnectAttr \"pSphere1_GEOShape1.iog\" \":initialShadingGroup.dsm\" -na;\nconnectAttr \"pDiscShape1.iog\" \":initialShadingGroup.dsm\" -na;\n// End of test_project_test_asset_test_task_v002.ma\n"
  },
  {
    "path": "tests/integration/hosts/maya/test_publish_in_maya.py",
    "content": "import re\nimport os\n\nfrom tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.maya.lib import MayaLocalPublishTestClass\n\n\nclass TestPublishInMaya(MayaLocalPublishTestClass):\n    \"\"\"Basic test case for publishing in Maya\n\n        Shouldnt be running standalone only via 'runtests' pype command! (??)\n\n        Uses generic TestCase to prepare fixtures for test data, testing DBs,\n        env vars.\n\n        Always pulls and uses test data from GDrive!\n\n        Opens Maya, runs publish on prepared workile.\n\n        Then checks content of DB (if subset, version, representations were\n        created.\n        Checks tmp folder if all expected files were published.\n\n        How to run:\n        (in cmd with activated {OPENPYPE_ROOT}/.venv)\n        {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/maya  # noqa: E501\n\n    \"\"\"\n    PERSIST = False\n\n    TEST_FILES = [\n        (\"test_publish_in_maya\", \"\", \"\")\n    ]\n\n    APP_GROUP = \"maya\"\n    # keep empty to locate latest installed variant or explicit\n    APP_VARIANT = \"\"\n\n    TIMEOUT = 120  # publish timeout\n\n    def test_publish(\n        self,\n        dbcon,\n        publish_finished,\n        download_test_data\n    ):\n        \"\"\"Testing Pyblish and Python logs within Maya.\"\"\"\n\n        # All maya output via MAYA_CMD_FILE_OUTPUT env var during test run\n        logging_path = os.path.join(download_test_data, \"output.log\")\n        with open(logging_path, \"r\") as f:\n            logging_output = f.read()\n\n        print((\"-\" * 50) + \"LOGGING\" + (\"-\" * 50))\n        print(logging_output)\n\n        # Check for pyblish errors.\n        error_regex = r\"pyblish \\(ERROR\\)((.|\\n)*?)((pyblish \\())\"\n        matches = re.findall(error_regex, logging_output)\n        assert not matches, matches[0][0]\n\n        # Check for python errors.\n        error_regex = r\"// Error((.|\\n)*)\"\n        matches = re.findall(error_regex, logging_output)\n        assert not matches, matches[0][0]\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 2))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"modelMain\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(DBAssert.count_of_types(dbcon, \"representation\", 5))\n\n        additional_args = {\"context.subset\": \"modelMain\",\n                           \"context.ext\": \"abc\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 2,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"modelMain\",\n                           \"context.ext\": \"ma\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 2,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"workfileTest_task\",\n                           \"context.ext\": \"ma\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestPublishInMaya()\n"
  },
  {
    "path": "tests/integration/hosts/nuke/lib.py",
    "content": "import os\nimport pytest\nimport re\n\nfrom tests.lib.testing_classes import (\n    HostFixtures,\n    PublishTest,\n    DeadlinePublishTest\n)\n\n\nclass NukeHostFixtures(HostFixtures):\n    @pytest.fixture(scope=\"module\")\n    def last_workfile_path(self, download_test_data, output_folder_url):\n        \"\"\"Get last_workfile_path from source data.\n\n        \"\"\"\n        source_file_name = \"test_project_test_asset_test_task_v001.nk\"\n        src_path = os.path.join(download_test_data,\n                                \"input\",\n                                \"workfile\",\n                                source_file_name)\n        dest_folder = os.path.join(output_folder_url,\n                                   self.PROJECT,\n                                   self.ASSET,\n                                   \"work\",\n                                   self.TASK)\n        if not os.path.exists(dest_folder):\n            os.makedirs(dest_folder)\n\n        dest_path = os.path.join(dest_folder,\n                                 source_file_name)\n\n        # rewrite old root with temporary file\n        # TODO - using only C:/projects seems wrong - but where to get root ?\n        replace_pattern = re.compile(re.escape(\"C:/projects\"), re.IGNORECASE)\n        with open(src_path, \"r\") as fp:\n            updated = fp.read()\n            updated = replace_pattern.sub(output_folder_url.replace(\"\\\\\", '/'),\n                                          updated)\n\n        with open(dest_path, \"w\") as fp:\n            fp.write(updated)\n\n        yield dest_path\n\n    @pytest.fixture(scope=\"module\")\n    def startup_scripts(self, monkeypatch_session, download_test_data):\n        \"\"\"Points Nuke to userSetup file from input data\"\"\"\n        startup_path = os.path.join(download_test_data,\n                                    \"input\",\n                                    \"startup\")\n        original_nuke_path = os.environ.get(\"NUKE_PATH\", \"\")\n        monkeypatch_session.setenv(\"NUKE_PATH\",\n                                   \"{}{}{}\".format(startup_path,\n                                                   os.pathsep,\n                                                   original_nuke_path))\n\n    @pytest.fixture(scope=\"module\")\n    def skip_compare_folders(self):\n        yield []\n\nclass NukeLocalPublishTestClass(NukeHostFixtures, PublishTest):\n    \"\"\"Testing class for local publishes.\"\"\"\n\n\nclass NukeDeadlinePublishTestClass(NukeHostFixtures, DeadlinePublishTest):\n    \"\"\"Testing class for Deadline publishes.\"\"\"\n"
  },
  {
    "path": "tests/integration/hosts/nuke/test_deadline_publish_in_nuke.py",
    "content": "import logging\n\nfrom tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.nuke.lib import NukeDeadlinePublishTestClass\n\nlog = logging.getLogger(\"test_publish_in_nuke\")\n\n\nclass TestDeadlinePublishInNuke(NukeDeadlinePublishTestClass):\n    \"\"\"Basic test case for publishing in Nuke\n\n        Uses generic TestCase to prepare fixtures for test data, testing DBs,\n        env vars.\n\n        !!!\n        It expects modified path in WriteNode,\n        use '[python {nuke.script_directory()}]' instead of regular root\n        dir (eg. instead of `c:/projects`).\n        Access file path by selecting WriteNode group, CTRL+Enter, update file\n        input\n        !!!\n\n        Opens Nuke, run publish on prepared workile.\n\n        Then checks content of DB (if subset, version, representations were\n        created.\n        Checks tmp folder if all expected files were published.\n\n        How to run:\n        (in cmd with activated {OPENPYPE_ROOT}/.venv)\n        {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py\n        runtests ../tests/integration/hosts/nuke  # noqa: E501\n\n        To check log/errors from launched app's publish process keep PERSIST\n        to True and check `test_openpype.logs` collection.\n    \"\"\"\n    # https://drive.google.com/file/d/1SUurHj2aiQ21ZIMJfGVBI2KjR8kIjBGI/view?usp=sharing  # noqa: E501\n    TEST_FILES = [\n        (\"1SeWprClKhWMv2xVC9AcnekIJFExxnp_b\",\n         \"test_nuke_deadline_publish.zip\", \"\")\n    ]\n\n    APP_GROUP = \"nuke\"\n\n    TIMEOUT = 180  # publish timeout\n\n    # could be overwritten by command line arguments\n    # keep empty to locate latest installed variant or explicit\n    APP_VARIANT = \"\"\n    PERSIST = False  # True - keep test_db, test_openpype, outputted test files\n    TEST_DATA_FOLDER = None\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 2))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"renderTest_taskMain\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 3))\n\n        additional_args = {\"context.subset\": \"workfileTest_task\",\n                           \"context.ext\": \"nk\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"context.ext\": \"exr\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"thumbnail\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"h264_mov\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestDeadlinePublishInNuke()\n"
  },
  {
    "path": "tests/integration/hosts/nuke/test_deadline_publish_in_nuke_prerender.py",
    "content": "import logging\n\nfrom tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.nuke.lib import NukeDeadlinePublishTestClass\n\nlog = logging.getLogger(\"test_publish_in_nuke\")\n\n\nclass TestDeadlinePublishInNukePrerender(NukeDeadlinePublishTestClass):\n    \"\"\"Basic test case for publishing in Nuke and Deadline for prerender\n\n        It is different from `test_deadline_publish_in_nuke` as that one is for\n        `render` family >> this test expects different subset names.\n\n        Uses generic TestCase to prepare fixtures for test data, testing DBs,\n        env vars.\n\n        !!!\n        It expects path in WriteNode starting with 'c:/projects', it replaces\n        it with correct value in temp folder.\n        Access file path by selecting WriteNode group, CTRL+Enter, update file\n        input\n        !!!\n\n        Opens Nuke, run publish on prepared workile.\n\n        Then checks content of DB (if subset, version, representations were\n        created.\n        Checks tmp folder if all expected files were published.\n\n        How to run:\n        (in cmd with activated {OPENPYPE_ROOT}/.venv)\n        {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py\n        runtests ../tests/integration/hosts/nuke  # noqa: E501\n\n        To check log/errors from launched app's publish process keep PERSIST\n        to True and check `test_openpype.logs` collection.\n    \"\"\"\n    TEST_FILES = [\n        (\"1aQaKo3cF-fvbTfvODIRFMxgherjbJ4Ql\",\n         \"test_nuke_deadline_publish_in_nuke_prerender.zip\", \"\")\n    ]\n\n    APP_GROUP = \"nuke\"\n\n    TIMEOUT = 180  # publish timeout\n\n    # could be overwritten by command line arguments\n    # keep empty to locate latest installed variant or explicit\n    APP_VARIANT = \"\"\n    PERSIST = False  # True - keep test_db, test_openpype, outputted test files\n    TEST_DATA_FOLDER = None\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 2))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        # prerender has only default subset format `{family}{variant}`,\n        # Key01 is used variant\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"prerenderKey01\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 2))\n\n        additional_args = {\"context.subset\": \"workfileTest_task\",\n                           \"context.ext\": \"nk\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"prerenderKey01\",\n                           \"context.ext\": \"exr\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        # prerender doesn't have set creation of review by default\n        additional_args = {\"context.subset\": \"prerenderKey01\",\n                           \"name\": \"thumbnail\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"prerenderKey01\",\n                           \"name\": \"h264_mov\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestDeadlinePublishInNukePrerender()\n"
  },
  {
    "path": "tests/integration/hosts/nuke/test_publish_in_nuke.py",
    "content": "import logging\n\nfrom tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.nuke.lib import NukeLocalPublishTestClass\n\nlog = logging.getLogger(\"test_publish_in_nuke\")\n\n\nclass TestPublishInNuke(NukeLocalPublishTestClass):\n    \"\"\"Basic test case for publishing in Nuke\n\n        Uses generic TestCase to prepare fixtures for test data, testing DBs,\n        env vars.\n\n        !!!\n        It expects modified path in WriteNode,\n        use '[python {nuke.script_directory()}]' instead of regular root\n        dir (eg. instead of `c:/projects`).\n        Access file path by selecting WriteNode group, CTRL+Enter, update file\n        input\n        !!!\n\n        Opens Nuke, run publish on prepared workile.\n\n        Then checks content of DB (if subset, version, representations were\n        created.\n        Checks tmp folder if all expected files were published.\n\n        How to run:\n        (in cmd with activated {OPENPYPE_ROOT}/.venv)\n        {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py\n        runtests ../tests/integration/hosts/nuke  # noqa: E501\n\n        To check log/errors from launched app's publish process keep PERSIST\n        to True and check `test_openpype.logs` collection.\n    \"\"\"\n    # https://drive.google.com/file/d/1SUurHj2aiQ21ZIMJfGVBI2KjR8kIjBGI/view?usp=sharing  # noqa: E501\n    TEST_FILES = [\n        (\"1SUurHj2aiQ21ZIMJfGVBI2KjR8kIjBGI\", \"test_Nuke_publish.zip\", \"\")\n    ]\n\n    APP_GROUP = \"nuke\"\n\n    TIMEOUT = 50  # publish timeout\n\n    # could be overwritten by command line arguments\n    # keep empty to locate latest installed variant or explicit\n    APP_VARIANT = \"\"\n    PERSIST = False  # True - keep test_db, test_openpype, outputted test files\n    TEST_DATA_FOLDER = None\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 2))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"renderTest_taskMain\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 3))\n\n        additional_args = {\"context.subset\": \"workfileTest_task\",\n                           \"context.ext\": \"nk\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"context.ext\": \"exr\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"thumbnail\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"renderTest_taskMain\",\n                           \"name\": \"h264_mov\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestPublishInNuke()\n"
  },
  {
    "path": "tests/integration/hosts/photoshop/lib.py",
    "content": "import os\nimport pytest\nimport shutil\n\nfrom tests.lib.testing_classes import (\n    HostFixtures,\n    PublishTest\n)\n\n\nclass PhotoshopTestClass(HostFixtures, PublishTest):\n    @pytest.fixture(scope=\"module\")\n    def last_workfile_path(self, download_test_data, output_folder_url):\n        \"\"\"Get last_workfile_path from source data.\n\n            Maya expects workfile in proper folder, so copy is done first.\n        \"\"\"\n        src_path = os.path.join(download_test_data,\n                                \"input\",\n                                \"workfile\",\n                                \"test_project_test_asset_TestTask_v001.psd\")\n        dest_folder = os.path.join(output_folder_url,\n                                   self.PROJECT,\n                                   self.ASSET,\n                                   \"work\",\n                                   self.TASK)\n        os.makedirs(dest_folder)\n        dest_path = os.path.join(dest_folder,\n                                 \"test_project_test_asset_TestTask_v001.psd\")\n        shutil.copy(src_path, dest_path)\n\n        yield dest_path\n\n    @pytest.fixture(scope=\"module\")\n    def startup_scripts(self, monkeypatch_session, download_test_data):\n        \"\"\"Points Maya to userSetup file from input data\"\"\"\n        pass\n\n    @pytest.fixture(scope=\"module\")\n    def skip_compare_folders(self):\n        yield []\n"
  },
  {
    "path": "tests/integration/hosts/photoshop/test_publish_in_photoshop.py",
    "content": "import logging\n\nfrom tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.photoshop.lib import PhotoshopTestClass\n\nlog = logging.getLogger(\"test_publish_in_photoshop\")\n\n\nclass TestPublishInPhotoshop(PhotoshopTestClass):\n    \"\"\"Basic test case for publishing in Photoshop\n\n        Uses generic TestCase to prepare fixtures for test data, testing DBs,\n        env vars.\n\n        Opens Photoshop, run publish on prepared workile.\n\n        Test zip file sets 3 required env vars:\n        - HEADLESS_PUBLISH - this triggers publish immediately app is open\n        - IS_TEST - this differentiate between regular webpublish\n        - PYBLISH_TARGETS\n\n        Always pulls and uses test data from GDrive!\n\n        Test zip file sets 3 required env vars:\n        - HEADLESS_PUBLISH - this triggers publish immediately app is open\n        - IS_TEST - this differentiate between regular webpublish\n        - PYBLISH_TARGETS\n\n        Then checks content of DB (if subset, version, representations were\n        created.\n        Checks tmp folder if all expected files were published.\n\n        How to run:\n        (in cmd with activated {OPENPYPE_ROOT}/.venv)\n        {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/photoshop  # noqa: E501\n\n    \"\"\"\n    PERSIST = True\n\n    TEST_FILES = [\n        (\"1zD2v5cBgkyOm_xIgKz3WKn8aFB_j8qC-\", \"test_photoshop_publish.zip\", \"\")\n    ]\n\n    APP_GROUP = \"photoshop\"\n    # keep empty to locate latest installed variant or explicit\n    APP_VARIANT = \"\"\n\n    APP_NAME = \"{}/{}\".format(APP_GROUP, APP_VARIANT)\n\n    TIMEOUT = 120  # publish timeout\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 4))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"imageMainForeground\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"imageMainBackground\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 6))\n\n        additional_args = {\"context.subset\": \"imageMainForeground\",\n                           \"context.ext\": \"png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"imageMainForeground\",\n                           \"context.ext\": \"jpg\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"imageMainBackground\",\n                           \"context.ext\": \"png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"imageMainBackground\",\n                           \"context.ext\": \"jpg\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestPublishInPhotoshop()\n"
  },
  {
    "path": "tests/integration/hosts/photoshop/test_publish_in_photoshop_auto_image.py",
    "content": "import logging\n\nfrom tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.photoshop.lib import PhotoshopTestClass\n\nlog = logging.getLogger(\"test_publish_in_photoshop\")\n\n\nclass TestPublishInPhotoshopAutoImage(PhotoshopTestClass):\n    \"\"\"Test for publish in Phohoshop with different review configuration.\n\n    Workfile contains 3 layers, auto image and review instances created.\n\n    Test contains updates to Settings!!!\n\n    \"\"\"\n    PERSIST = True\n\n    TEST_FILES = [\n        (\"1iLF6aNI31qlUCD1rGg9X9eMieZzxL-rc\",\n         \"test_photoshop_publish_auto_image.zip\", \"\")\n    ]\n\n    APP_GROUP = \"photoshop\"\n    # keep empty to locate latest installed variant or explicit\n    APP_VARIANT = \"\"\n\n    APP_NAME = \"{}/{}\".format(APP_GROUP, APP_VARIANT)\n\n    TIMEOUT = 120  # publish timeout\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 3))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 0,\n                                    name=\"imageMainForeground\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 0,\n                                    name=\"imageMainBackground\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 5))\n\n        additional_args = {\"context.subset\": \"imageMainForeground\",\n                           \"context.ext\": \"png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"imageMainBackground\",\n                           \"context.ext\": \"png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        # review from image\n        additional_args = {\"context.subset\": \"imageBeautyMain\",\n                           \"context.ext\": \"jpg\",\n                           \"name\": \"jpg_jpg\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"imageBeautyMain\",\n                           \"context.ext\": \"jpg\",\n                           \"name\": \"jpg\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"review\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestPublishInPhotoshopAutoImage()\n"
  },
  {
    "path": "tests/integration/hosts/photoshop/test_publish_in_photoshop_review.py",
    "content": "import logging\n\nfrom tests.lib.assert_classes import DBAssert\nfrom tests.integration.hosts.photoshop.lib import PhotoshopTestClass\n\nlog = logging.getLogger(\"test_publish_in_photoshop\")\n\n\nclass TestPublishInPhotoshopImageReviews(PhotoshopTestClass):\n    \"\"\"Test for publish in Phohoshop with different review configuration.\n\n    Workfile contains 2 image instance, one has review flag, second doesn't.\n\n    Regular `review` family is disabled.\n\n    Expected result is to `imageMainForeground` to have additional file with\n    review, `imageMainBackground` without. No separate `review` family.\n\n    `test_project_test_asset_imageMainForeground_v001_jpg.jpg` is expected name\n    of imageForeground review, `_jpg` suffix is needed to differentiate between\n    image and review file.\n\n    \"\"\"\n    PERSIST = True\n\n    TEST_FILES = [\n        (\"12WGbNy9RJ3m9jlnk0Ib9-IZmONoxIz_p\",\n         \"test_photoshop_publish_review.zip\", \"\")\n    ]\n\n    APP_GROUP = \"photoshop\"\n    # keep empty to locate latest installed variant or explicit\n    APP_VARIANT = \"\"\n\n    APP_NAME = \"{}/{}\".format(APP_GROUP, APP_VARIANT)\n\n    TIMEOUT = 120  # publish timeout\n\n    def test_db_asserts(self, dbcon, publish_finished):\n        \"\"\"Host and input data dependent expected results in DB.\"\"\"\n        print(\"test_db_asserts\")\n        failures = []\n\n        failures.append(DBAssert.count_of_types(dbcon, \"version\", 3))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"version\", 0, name={\"$ne\": 1}))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"imageMainForeground\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"imageMainBackground\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"subset\", 1,\n                                    name=\"workfileTest_task\"))\n\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 6))\n\n        additional_args = {\"context.subset\": \"imageMainForeground\",\n                           \"context.ext\": \"png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"imageMainForeground\",\n                           \"context.ext\": \"jpg\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 2,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"imageMainForeground\",\n                           \"context.ext\": \"jpg\",\n                           \"context.representation\": \"jpg_jpg\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"imageMainBackground\",\n                           \"context.ext\": \"png\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"imageMainBackground\",\n                           \"context.ext\": \"jpg\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 1,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"imageMainBackground\",\n                           \"context.ext\": \"jpg\",\n                           \"context.representation\": \"jpg_jpg\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        additional_args = {\"context.subset\": \"review\"}\n        failures.append(\n            DBAssert.count_of_types(dbcon, \"representation\", 0,\n                                    additional_args=additional_args))\n\n        assert not any(failures)\n\n\nif __name__ == \"__main__\":\n    test_case = TestPublishInPhotoshopImageReviews()\n"
  },
  {
    "path": "tests/lib/README.md",
    "content": "Automatic testing\n-----------------\nFolder for libs and tooling for automatic testing.\n\n- db_handler.py - class for preparation of test DB\n    - dumps DB(s) to BSON (mongodump)\n    - loads dump(s) to new DB (mongorestore)\n    - loads sql file(s) to DB (mongoimport)\n    - deletes test DB\n  \n- file_handler.py - class to download test data from GDrive\n    - downloads data from (list) of files from GDrive\n    - check file integrity with MD5 hash\n    - unzips if zip\n    \n- testing_wrapper.py - base class to use for testing\n    - all env var necessary for running (OPENPYPE_MONGO ...)\n    - implements reusable fixtures to:\n        - load test data (uses `file_handler`)\n        - prepare DB (uses `db_handler`)\n        - modify temporarily env vars for testing\n        \n    Should be used as a skeleton to create new test cases.\n\n\nTest data\n---------\nEach class implementing `TestCase` can provide test file(s) by adding them to\nTEST_FILES ('GDRIVE_FILE_ID', 'ACTUAL_FILE_NAME', 'MD5HASH')\n\nGDRIVE_FILE_ID can be pulled from shareable link from Google Drive app.\n\nCurrently it is expected that test file will be zip file with structure:\n- expected - expected files (not implemented yet)\n- input\n    - data - test data (workfiles, images etc)\n    - dumps - folder for BSON dumps from (`mongodump`)\n    - env_vars \n        env_vars.json - dictionary with environment variables {key:value}\n        \n    - json - json files to load with `mongoimport` (human readable)\n    \n\nExample\n-------\nSee `tests\\unit\\openpype\\modules\\sync_server\\test_site_operations.py` for example usage of implemented classes."
  },
  {
    "path": "tests/lib/__init__.py",
    "content": ""
  },
  {
    "path": "tests/lib/assert_classes.py",
    "content": "\"\"\"Classed and methods for comparing expected and published items in DBs\"\"\"\n\n\nclass DBAssert:\n\n    @classmethod\n    def count_of_types(cls, dbcon, queried_type, expected, **kwargs):\n        \"\"\"Queries 'dbcon' and counts documents of type 'queried_type'\n\n            Args:\n                dbcon (AvalonMongoDB)\n                queried_type (str): type of document (\"asset\", \"version\"...)\n                expected (int): number of documents found\n                any number of additional keyword arguments\n\n                special handling of argument additional_args (dict)\n                    with additional args like\n                    {\"context.subset\": \"XXX\"}\n        \"\"\"\n        args = {\"type\": queried_type}\n        for key, val in kwargs.items():\n            if key == \"additional_args\":\n                args.update(val)\n            else:\n                args[key] = val\n\n        no_of_docs = dbcon.count_documents(args)\n\n        msg = None\n        args.pop(\"type\")\n        detail_str = \" \"\n        if args:\n            detail_str = \" with '{}'\".format(args)\n\n        if expected != no_of_docs:\n            msg = \"Not expected no of '{}'{}.\"\\\n                  \"Expected {}, found {}\".format(queried_type,\n                                                 detail_str,\n                                                 expected, no_of_docs)\n\n        status = \"successful\"\n        if msg:\n            status = \"failed\"\n\n        print(\"Comparing count of {}{} {}\".format(queried_type,\n                                                  detail_str,\n                                                  status))\n\n        return msg\n"
  },
  {
    "path": "tests/lib/db_handler.py",
    "content": "\"\"\"\n    Helper class for automatic testing, provides dump and restore via command\n    line utilities.\n\n    Expect mongodump, mongoexport, mongoimport and mongorestore present at PATH\n\"\"\"\nimport os\nimport pymongo\nimport subprocess\n\n\nclass DBHandler:\n\n    def __init__(self, uri=None, host=None, port=None,\n                 user=None, password=None):\n        \"\"\"'uri' or rest of separate credentials\"\"\"\n        if uri:\n            self.uri = uri\n        if host:\n            if all([user, password]):\n                host = \"{}:{}@{}\".format(user, password, host)\n            self.uri = 'mongodb://{}:{}'.format(host, port or 27017)\n\n        assert self.uri, \"Must have uri to MongoDB\"\n        self.client = pymongo.MongoClient(uri)\n        self.db = None\n\n    def setup_empty(self, name):\n        # not much sense\n        self.db = self.client[name]\n\n    def setup_from_sql(self, db_name, sql_dir, collection=None,\n                       drop=True, mode=None):\n        \"\"\"\n            Restores 'db_name' from 'sql_url'.\n\n            Works with directory with .json files,\n            if 'collection' arg is empty, name\n            of .json file is used as name of target collection.\n\n            Args:\n                db_name (str): source DB name\n                sql_dir (str): folder with json files\n                collection (str): if all sql files are meant for single coll.\n                drop (bool): True if drop whole collection\n                mode (str): \"insert\" - fails on duplicates\n                            \"upsert\" - modifies existing\n                            \"merge\" - updates existing\n                            \"delete\" - removes in DB present if file\n        \"\"\"\n        if not os.path.exists(sql_dir):\n            raise RuntimeError(\n                \"Backup folder {} doesn't exist\".format(sql_dir))\n\n        for (dirpath, _dirnames, filenames) in os.walk(sql_dir):\n            for file_name in filenames:\n                sql_url = os.path.join(dirpath, file_name)\n                query = self._import_query(self.uri, sql_url,\n                                           db_name=db_name,\n                                           collection=collection,\n                                           drop=drop,\n                                           mode=mode)\n\n                print(\"mongoimport query:: {}\".format(query))\n                subprocess.run(query)\n\n    def setup_from_sql_file(self, db_name, sql_url,\n                            collection=None, drop=True, mode=None):\n        \"\"\"\n            Restores 'db_name' from 'sql_url'.\n\n            Works with single .json file.\n            If 'collection' arg is empty, name\n            of .json file is used as name of target collection.\n\n            Args:\n                db_name (str): source DB name\n                sql_file (str): folder with json files\n                collection (str): name of target collection\n                drop (bool): True if drop collection\n                mode (str): \"insert\" - fails on duplicates\n                            \"upsert\" - modifies existing\n                            \"merge\" - updates existing\n                            \"delete\" - removes in DB present if file\n        \"\"\"\n        if not os.path.exists(sql_url):\n            raise RuntimeError(\n                \"Sql file {} doesn't exist\".format(sql_url))\n\n        query = self._import_query(self.uri, sql_url,\n                                   db_name=db_name,\n                                   collection=collection,\n                                   drop=drop,\n                                   mode=mode)\n\n        print(\"mongoimport query:: {}\".format(query))\n        subprocess.run(query)\n\n    def setup_from_dump(self, db_name, dump_dir, overwrite=False,\n                        collection=None, db_name_out=None):\n        \"\"\"\n            Restores 'db_name' from 'dump_dir'.\n\n            Works with BSON folders exported by mongodump\n\n            Args:\n                db_name (str): source DB name\n                dump_dir (str): folder with dumped subfolders\n                overwrite (bool): True if overwrite target\n                collection (str): name of source project\n                db_name_out (str): name of target DB, if empty restores to\n                    source 'db_name'\n        \"\"\"\n        db_name_out = db_name_out or db_name\n        if self._db_exists(db_name_out):\n            if not overwrite:\n                raise RuntimeError(\"DB {} already exists\".format(db_name_out) +\n                                   \"Run with overwrite=True\")\n            else:\n                if collection:\n                    if collection in self.client[db_name_out].list_collection_names():  # noqa\n                        self.client[db_name_out][collection].drop()\n                else:\n                    self.teardown(db_name_out)\n\n        dir_path = os.path.join(dump_dir, db_name)\n        if not os.path.exists(dir_path):\n            raise RuntimeError(\n                \"Backup folder {} doesn't exist\".format(dir_path))\n\n        query = self._restore_query(self.uri, dump_dir,\n                                    db_name=db_name, db_name_out=db_name_out,\n                                    collection=collection)\n        print(\"mongorestore query:: {}\".format(query))\n        try:\n            subprocess.run(query)\n        except FileNotFoundError:\n            raise RuntimeError(\"'mongorestore' utility must be on path.\"\n                               \"Please install it.\")\n\n    def teardown(self, db_name):\n        \"\"\"Drops 'db_name' if exists.\"\"\"\n        if not self._db_exists(db_name):\n            print(\"{} doesn't exist\".format(db_name))\n            return\n\n        print(\"Dropping {} database\".format(db_name))\n        self.client.drop_database(db_name)\n\n    def backup_to_dump(self, db_name, dump_dir, overwrite=False,\n                       collection=None, format=\"bson\"):\n        \"\"\"\n            Helper method for running mongodump for specific 'db_name'\n        \"\"\"\n        if not self._db_exists(db_name) and not overwrite:\n            raise RuntimeError(\"DB {} doesn't exists\".format(db_name))\n\n        dir_path = os.path.join(dump_dir, db_name)\n        if os.path.exists(dir_path) and not overwrite:\n            raise RuntimeError(\"Backup already exists, \"\n                               \"run with overwrite=True\")\n\n        collections = [collection]\n        if format == \"json\" and collection is None:\n            collections = self.client[db_name].list_collection_names()\n\n        for collection in collections:\n            query = self._dump_query(self.uri, dump_dir,\n                                     db_name=db_name, collection=collection,\n                                     format=format)\n            print(\"Mongodump query:: {}\".format(query))\n            process = subprocess.run(query)\n            assert process.returncode == 0, \"Mongo dump failed.\"\n\n    def _db_exists(self, db_name):\n        return db_name in self.client.list_database_names()\n\n    def _dump_query(\n        self, uri, output_path, db_name=None, collection=None, format=\"bson\"\n    ):\n        \"\"\"Prepares dump query based on 'db_name' or 'collection'.\"\"\"\n        db_part = coll_part = \"\"\n        if db_name:\n            db_part = \"--db={}\".format(db_name)\n        if collection:\n            if not db_name:\n                raise ValueError(\"db_name must be present\")\n            coll_part = \"--collection={}\".format(collection)\n\n        tool = \"mongodump\"\n        query = \"{} --uri=\\\"{}\\\"\"\n\n        if format == \"json\":\n            assert collection, \"Collection is needed for json export.\"\n\n            query += \" --jsonArray --pretty\"\n            tool = \"mongoexport\"\n            output_path = os.path.join(\n                output_path, \"{}.{}.json\".format(db_name, collection)\n            )\n\n        query += \" --out={} {} {}\"\n\n        return query.format(tool, uri, output_path, db_part, coll_part)\n\n    def _restore_query(self, uri, dump_dir,\n                       db_name=None, db_name_out=None,\n                       collection=None, drop=True):\n        \"\"\"Prepares query for mongorestore base on arguments\"\"\"\n        db_part = coll_part = drop_part = \"\"\n        if db_name:\n            db_part = \"--nsInclude={}.* --nsFrom={}.*\".format(db_name, db_name)\n        if collection:\n            assert db_name, \"Must provide db name too\"\n            db_part = \"--nsInclude={}.{} --nsFrom={}.{}\".format(db_name,\n                                                                collection,\n                                                                db_name,\n                                                                collection)\n        if drop:\n            drop_part = \"--drop\"\n\n        if db_name_out:\n            collection_str = collection or '*'\n            db_part += \" --nsTo={}.{}\".format(db_name_out, collection_str)\n\n        query = \"\\\"{}\\\" --uri=\\\"{}\\\" --dir=\\\"{}\\\" {} {} {}\".format(\n            \"mongorestore\", uri, dump_dir, db_part, coll_part, drop_part\n        )\n\n        return query\n\n    def _import_query(self, uri, sql_url,\n                      db_name=None,\n                      collection=None, drop=True, mode=None):\n\n        db_part = coll_part = drop_part = mode_part = \"\"\n        if db_name:\n            db_part = \"--db {}\".format(db_name)\n        if collection:\n            assert db_name, \"Must provide db name too\"\n            coll_part = \"--collection {}\".format(collection)\n        if drop:\n            drop_part = \"--drop\"\n        if mode:\n            mode_part = \"--mode {}\".format(mode)\n\n        query = \\\n            \"\\\"{}\\\" --legacy --uri=\\\"{}\\\" --file=\\\"{}\\\" {} {} {} {}\".format(\n                \"mongoimport\", uri, sql_url,\n                db_part, coll_part, drop_part, mode_part)\n\n        return query\n\n# Examples\n# handler = DBHandler(uri=\"mongodb://localhost:27017\")\n# #\n# backup_dir = \"c:\\\\projects\\\\test_zips\\\\test_nuke_deadline_publish\\\\input\\\\dumps\"  # noqa\n# # #\n# handler.backup_to_dump(\"avalon_tests\", backup_dir, True, collection=\"test_project\")  # noqa\n#handler.backup_to_dump(\"openpype_tests\", backup_dir, True, collection=\"settings\")  # noqa\n\n# handler.setup_from_dump(\"avalon_tests\", backup_dir, True, db_name_out=\"avalon_tests\", collection=\"test_project\")  # noqa\n# handler.setup_from_sql_file(\"avalon_tests\", \"c:\\\\projects\\\\sql\\\\item.sql\",\n#                             collection=\"test_project\",\n#                             drop=False, mode=\"upsert\")\n# handler.setup_from_sql(\"avalon_tests\", \"c:\\\\projects\\\\sql\",\n#                        collection=\"test_project\",\n#                        drop=False, mode=\"upsert\")\n"
  },
  {
    "path": "tests/lib/file_handler.py",
    "content": "import os\nimport re\nimport urllib\nfrom urllib.parse import urlparse\nimport urllib.request\nimport urllib.error\nimport itertools\nimport hashlib\nimport tarfile\nimport zipfile\nfrom abc import ABCMeta, abstractmethod\nimport six\nimport shutil\nimport requests\n\nUSER_AGENT = \"AYON-launcher\"\n\n\n@six.add_metaclass(ABCMeta)\nclass BaseFileHandler:\n    IMPLEMENTED_ZIP_FORMATS = {\n        \"zip\", \"tar\", \"tgz\", \"tar.gz\", \"tar.xz\", \"tar.bz2\"\n    }\n\n    @staticmethod\n    def calculate_md5(fpath, chunk_size=10000):\n        md5 = hashlib.md5()\n        with open(fpath, \"rb\") as f:\n            for chunk in iter(lambda: f.read(chunk_size), b\"\"):\n                md5.update(chunk)\n        return md5.hexdigest()\n\n    @staticmethod\n    def check_md5(fpath, md5, **kwargs):\n        return md5 == RemoteFileHandler.calculate_md5(fpath, **kwargs)\n\n\n    @staticmethod\n    def calculate_sha256(fpath):\n        \"\"\"Calculate sha256 for content of the file.\n\n        Args:\n             fpath (str): Path to file.\n\n        Returns:\n            str: hex encoded sha256\n\n        \"\"\"\n        h = hashlib.sha256()\n        b = bytearray(128 * 1024)\n        mv = memoryview(b)\n        with open(fpath, \"rb\", buffering=0) as f:\n            for n in iter(lambda: f.readinto(mv), 0):\n                h.update(mv[:n])\n        return h.hexdigest()\n\n    @staticmethod\n    def check_sha256(fpath, sha256, **kwargs):\n        return sha256 == RemoteFileHandler.calculate_sha256(fpath, **kwargs)\n\n    @staticmethod\n    def check_integrity(fpath, hash_value=None, hash_type=None):\n        if not os.path.isfile(fpath):\n            return False\n        if hash_value is None:\n            return True\n        if not hash_type:\n            raise ValueError(\"Provide hash type, md5 or sha256\")\n        if hash_type == \"md5\":\n            return RemoteFileHandler.check_md5(fpath, hash_value)\n        if hash_type == \"sha256\":\n            return RemoteFileHandler.check_sha256(fpath, hash_value)\n\n    @staticmethod\n    def unzip(path, destination_path=None):\n        if not destination_path:\n            destination_path = os.path.dirname(path)\n\n        _, archive_type = os.path.splitext(path)\n        archive_type = archive_type.lstrip(\".\")\n\n        if archive_type in [\"zip\"]:\n            print(f\"Unzipping {path}->{destination_path}\")\n            zip_file = zipfile.ZipFile(path)\n            zip_file.extractall(destination_path)\n            zip_file.close()\n\n        elif archive_type in [\n            \"tar\", \"tgz\", \"tar.gz\", \"tar.xz\", \"tar.bz2\"\n        ]:\n            print(f\"Unzipping {path}->{destination_path}\")\n            if archive_type == \"tar\":\n                tar_type = \"r:\"\n            elif archive_type.endswith(\"xz\"):\n                tar_type = \"r:xz\"\n            elif archive_type.endswith(\"gz\"):\n                tar_type = \"r:gz\"\n            elif archive_type.endswith(\"bz2\"):\n                tar_type = \"r:bz2\"\n            else:\n                tar_type = \"r:*\"\n            try:\n                tar_file = tarfile.open(path, tar_type)\n            except tarfile.ReadError:\n                raise SystemExit(\"corrupted archive\")\n            tar_file.extractall(destination_path)\n            tar_file.close()\n\n    @staticmethod\n    @abstractmethod\n    def download_test_source_files(file_id, root, filename=None):\n        \"\"\"Download a test source files and place it in root.\n        Args:\n            file_id (str): id of file to be downloaded\n            root (str): Directory to place downloaded file in\n            filename (str, optional): Name to save the file under.\n                If None, use the id of the file.\n        \"\"\"\n        raise NotImplementedError\n\n\nclass LocalFileHandler(BaseFileHandler):\n\n    @staticmethod\n    def download_test_source_files(source_path, tmp_dir, filename=None):\n        tmp_dir = os.path.expanduser(tmp_dir)\n        if os.path.isdir(source_path):\n            shutil.copytree(source_path, tmp_dir, dirs_exist_ok=True)\n        else:\n            file_name = os.path.basename(source_path)\n            shutil.copy(source_path,\n                        os.path.join(tmp_dir, file_name))\n\n\nclass RemoteFileHandler(BaseFileHandler):\n    \"\"\"Download file from url, might be GDrive shareable link\"\"\"\n\n    @staticmethod\n    def download_test_source_files(file_id, tmp_dir, filename=None):\n        RemoteFileHandler.download_file_from_google_drive(file_id, tmp_dir,\n                                                          filename)\n\n    @staticmethod\n    def download_url(\n        url,\n        root,\n        filename=None,\n        max_redirect_hops=3,\n        headers=None\n    ):\n        \"\"\"Download a file from url and place it in root.\n\n        Args:\n            url (str): URL to download file from\n            root (str): Directory to place downloaded file in\n            filename (str, optional): Name to save the file under.\n                If None, use the basename of the URL\n            max_redirect_hops (Optional[int]): Maximum number of redirect\n                hops allowed\n            headers (Optional[dict[str, str]]): Additional required headers\n                - Authentication etc..\n        \"\"\"\n\n        root = os.path.expanduser(root)\n        if not filename:\n            filename = os.path.basename(url)\n        fpath = os.path.join(root, filename)\n\n        os.makedirs(root, exist_ok=True)\n\n        # expand redirect chain if needed\n        url = RemoteFileHandler._get_redirect_url(\n            url, max_hops=max_redirect_hops, headers=headers)\n\n        # check if file is located on Google Drive\n        file_id = RemoteFileHandler._get_google_drive_file_id(url)\n        if file_id is not None:\n            return RemoteFileHandler.download_file_from_google_drive(\n                file_id, root, filename)\n\n        # download the file\n        try:\n            print(f\"Downloading {url} to {fpath}\")\n            RemoteFileHandler._urlretrieve(url, fpath, headers=headers)\n        except (urllib.error.URLError, IOError) as exc:\n            if url[:5] != \"https\":\n                raise exc\n\n            url = url.replace(\"https:\", \"http:\")\n            print((\n                \"Failed download. Trying https -> http instead.\"\n                f\" Downloading {url} to {fpath}\"\n            ))\n            RemoteFileHandler._urlretrieve(url, fpath, headers=headers)\n\n    @staticmethod\n    def download_file_from_google_drive(\n        file_id, tmp_dir, filename=None\n    ):\n        \"\"\"Download a Google Drive file from  and place it in root.\n        Args:\n            file_id (str): id of file to be downloaded\n            root (str): Directory to place downloaded file in\n            filename (str, optional): Name to save the file under.\n                If None, use the id of the file.\n        \"\"\"\n        # Based on https://stackoverflow.com/questions/38511444/python-download-files-from-google-drive-using-url # noqa\n\n        url = \"https://docs.google.com/uc?export=download\"\n\n        tmp_dir = os.path.expanduser(tmp_dir)\n        if not filename:\n            filename = file_id\n        fpath = os.path.join(tmp_dir, filename)\n\n        os.makedirs(tmp_dir, exist_ok=True)\n\n        if os.path.isfile(fpath) and RemoteFileHandler.check_integrity(fpath):\n            print(f\"Using downloaded and verified file: {fpath}\")\n        else:\n            session = requests.Session()\n\n            response = session.get(url, params={\"id\": file_id}, stream=True)\n            token = RemoteFileHandler._get_confirm_token(response)\n\n            if token:\n                params = {\"id\": file_id, \"confirm\": token}\n                response = session.get(url, params=params, stream=True)\n\n            response_content_generator = response.iter_content(32768)\n            first_chunk = None\n            while not first_chunk:  # filter out keep-alive new chunks\n                first_chunk = next(response_content_generator)\n\n            if RemoteFileHandler._quota_exceeded(first_chunk):\n                msg = (\n                    f\"The daily quota of the file {filename} is exceeded and \"\n                    f\"it can't be downloaded. This is a limitation of \"\n                    f\"Google Drive and can only be overcome by trying \"\n                    f\"again later.\"\n                )\n                raise RuntimeError(msg)\n\n            RemoteFileHandler._save_response_content(\n                itertools.chain((first_chunk, ),\n                                response_content_generator), fpath)\n            response.close()\n\n    @staticmethod\n    def _urlretrieve(url, filename, chunk_size=None, headers=None):\n        final_headers = {\"User-Agent\": USER_AGENT}\n        if headers:\n            final_headers.update(headers)\n\n        chunk_size = chunk_size or 8192\n        with open(filename, \"wb\") as fh:\n            with urllib.request.urlopen(\n                urllib.request.Request(url, headers=final_headers)\n            ) as response:\n                for chunk in iter(lambda: response.read(chunk_size), \"\"):\n                    if not chunk:\n                        break\n                    fh.write(chunk)\n\n    @staticmethod\n    def _get_redirect_url(url, max_hops, headers=None):\n        initial_url = url\n        final_headers = {\"Method\": \"HEAD\", \"User-Agent\": USER_AGENT}\n        if headers:\n            final_headers.update(headers)\n        for _ in range(max_hops + 1):\n            with urllib.request.urlopen(\n                urllib.request.Request(url, headers=final_headers)\n            ) as response:\n                if response.url == url or response.url is None:\n                    return url\n\n                return response.url\n        else:\n            raise RecursionError(\n                f\"Request to {initial_url} exceeded {max_hops} redirects. \"\n                f\"The last redirect points to {url}.\"\n            )\n\n    @staticmethod\n    def _get_confirm_token(response):\n        for key, value in response.cookies.items():\n            if key.startswith(\"download_warning\"):\n                return value\n\n        # handle antivirus warning for big zips\n        found = re.search(\"(confirm=)([^&.+])\", response.text)\n        if found:\n            return found.groups()[1]\n\n        return None\n\n    @staticmethod\n    def _save_response_content(\n        response_gen, destination,\n    ):\n        with open(destination, \"wb\") as f:\n            for chunk in response_gen:\n                if chunk:  # filter out keep-alive new chunks\n                    f.write(chunk)\n\n    @staticmethod\n    def _quota_exceeded(first_chunk):\n        try:\n            return \"Google Drive - Quota exceeded\" in first_chunk.decode()\n        except UnicodeDecodeError:\n            return False\n\n    @staticmethod\n    def _get_google_drive_file_id(url):\n        parts = urlparse(url)\n\n        if re.match(r\"(drive|docs)[.]google[.]com\", parts.netloc) is None:\n            return None\n\n        match = re.match(r\"/file/d/(?P<id>[^/]*)\", parts.path)\n        if match is None:\n            return None\n\n        return match.group(\"id\")\n"
  },
  {
    "path": "tests/lib/testing_classes.py",
    "content": "\"\"\"Testing classes for module testing and publishing in hosts.\"\"\"\nimport os\nimport sys\nimport six\nimport json\nimport pytest\nimport tempfile\nimport shutil\nimport glob\nimport platform\nimport requests\nimport re\nimport inspect\nimport time\n\nfrom tests.lib.db_handler import DBHandler\nfrom tests.lib.file_handler import RemoteFileHandler, LocalFileHandler\nfrom openpype.modules import ModulesManager\nfrom openpype.settings import get_project_settings\n\n\nclass BaseTest:\n    \"\"\"Empty base test class\"\"\"\n\n\nclass ModuleUnitTest(BaseTest):\n    \"\"\"Generic test class for testing modules\n\n        Use PERSIST==True to keep temporary folder and DB prepared for\n        debugging or preparation of test files.\n\n        Implemented fixtures:\n            monkeypatch_session - fixture for env vars with session scope\n            project_settings - fixture for project settings with session scope\n            download_test_data - tmp folder with extracted data from GDrive\n            env_var - sets env vars from input file\n            db_setup - prepares avalon AND openpype DBs for testing from\n                        binary dumps from input data\n            dbcon - returns DBConnection to AvalonDB\n            dbcon_openpype - returns DBConnection for OpenpypeMongoDB\n\n    \"\"\"\n    PERSIST = False  # True to not purge temporary folder nor test DB\n\n    TEST_OPENPYPE_MONGO = \"mongodb://localhost:27017\"\n    TEST_DB_NAME = \"avalon_tests\"\n    TEST_PROJECT_NAME = \"test_project\"\n    TEST_OPENPYPE_NAME = \"openpype_tests\"\n\n    TEST_FILES = []\n\n    PROJECT = \"test_project\"\n    ASSET = \"test_asset\"\n    TASK = \"test_task\"\n\n    TEST_DATA_FOLDER = None\n\n    @pytest.fixture(scope='session')\n    def monkeypatch_session(self):\n        \"\"\"Monkeypatch couldn't be used with module or session fixtures.\"\"\"\n        from _pytest.monkeypatch import MonkeyPatch\n        m = MonkeyPatch()\n        yield m\n        m.undo()\n\n    @pytest.fixture(scope='module')\n    def project_settings(self):\n        yield get_project_settings(\n            self.PROJECT\n        )\n\n    @pytest.fixture(scope=\"module\")\n    def download_test_data(\n        self, test_data_folder, persist, request, dump_databases\n    ):\n        test_data_folder = test_data_folder or self.TEST_DATA_FOLDER\n        if test_data_folder:\n            print(\"Using existing folder {}\".format(test_data_folder))\n            yield test_data_folder\n        else:\n            tmpdir = tempfile.mkdtemp()\n            print(\"Temporary folder created:: {}\".format(tmpdir))\n            for test_file in self.TEST_FILES:\n                file_id, file_name, md5 = test_file\n\n                current_dir = os.path.dirname(os.path.abspath(\n                    inspect.getfile(self.__class__)))\n                if os.path.exists(file_id):\n                    handler_class = LocalFileHandler\n                elif os.path.exists(os.path.join(current_dir, file_id)):\n                    file_id = os.path.join(current_dir, file_id)\n                    handler_class = LocalFileHandler\n                else:\n                    handler_class = RemoteFileHandler\n\n                handler_class.download_test_source_files(file_id, str(tmpdir),\n                                                         file_name)\n                ext = None\n                if \".\" in file_name:\n                    _, ext = os.path.splitext(file_name)\n\n                if ext and ext.lstrip('.') in handler_class.IMPLEMENTED_ZIP_FORMATS:  # noqa: E501\n                    handler_class.unzip(os.path.join(tmpdir, file_name))\n\n            yield tmpdir\n\n            persist = (persist or self.PERSIST or\n                       self.is_test_failed(request) or dump_databases)\n            if not persist:\n                print(\"Removing {}\".format(tmpdir))\n                shutil.rmtree(tmpdir)\n\n    @pytest.fixture(scope=\"module\")\n    def output_folder_url(self, download_test_data):\n        \"\"\"Returns location of published data, cleans it first if exists.\"\"\"\n        path = os.path.join(download_test_data, \"output\")\n        if os.path.exists(path):\n            print(\"Purging {}\".format(path))\n            shutil.rmtree(path)\n        yield path\n\n    @pytest.fixture(scope=\"module\")\n    def env_var(self, monkeypatch_session, download_test_data, mongo_url):\n        \"\"\"Sets temporary env vars from json file.\"\"\"\n        env_url = os.path.join(download_test_data, \"input\",\n                               \"env_vars\", \"env_var.json\")\n        if not os.path.exists(env_url):\n            raise ValueError(\"Env variable file {} doesn't exist\".\n                             format(env_url))\n\n        env_dict = {}\n        try:\n            with open(env_url) as json_file:\n                env_dict = json.load(json_file)\n        except ValueError:\n            print(\"{} doesn't contain valid JSON\")\n            six.reraise(*sys.exc_info())\n\n        for key, value in env_dict.items():\n            all_vars = globals()\n            all_vars.update(vars(ModuleUnitTest))  # TODO check\n            value = value.format(**all_vars)\n            print(\"Setting {}:{}\".format(key, value))\n            monkeypatch_session.setenv(key, str(value))\n\n        #reset connection to openpype DB with new env var\n        if mongo_url:\n            monkeypatch_session.setenv(\"OPENPYPE_MONGO\", mongo_url)\n\n        import openpype.settings.lib as sett_lib\n        sett_lib._SETTINGS_HANDLER = None\n        sett_lib._LOCAL_SETTINGS_HANDLER = None\n        sett_lib.create_settings_handler()\n        sett_lib.create_local_settings_handler()\n\n        import openpype\n        openpype_root = os.path.dirname(os.path.dirname(openpype.__file__))\n\n        # ?? why 2 of those\n        monkeypatch_session.setenv(\"OPENPYPE_ROOT\", openpype_root)\n        monkeypatch_session.setenv(\"OPENPYPE_REPOS_ROOT\", openpype_root)\n\n        # for remapping purposes (currently in Nuke)\n        monkeypatch_session.setenv(\"TEST_SOURCE_FOLDER\", download_test_data)\n\n    @pytest.fixture(scope=\"module\")\n    def db_setup(self, download_test_data, env_var, monkeypatch_session,\n                 request, mongo_url, dump_databases, persist):\n        \"\"\"Restore prepared MongoDB dumps into selected DB.\"\"\"\n        backup_dir = os.path.join(download_test_data, \"input\", \"dumps\")\n        uri = os.environ.get(\"OPENPYPE_MONGO\")\n        db_handler = DBHandler(uri)\n        db_handler.setup_from_dump(self.TEST_DB_NAME, backup_dir,\n                                   overwrite=True,\n                                   db_name_out=self.TEST_DB_NAME)\n\n        db_handler.setup_from_dump(self.TEST_OPENPYPE_NAME, backup_dir,\n                                   overwrite=True,\n                                   db_name_out=self.TEST_OPENPYPE_NAME)\n\n        yield db_handler\n\n        if dump_databases:\n            print(\"Dumping databases to {}\".format(download_test_data))\n            output_dir = os.path.join(download_test_data, \"output\", \"dumps\")\n            db_handler.backup_to_dump(\n                self.TEST_DB_NAME, output_dir, format=dump_databases\n            )\n            db_handler.backup_to_dump(\n                self.TEST_OPENPYPE_NAME, output_dir, format=dump_databases\n            )\n\n        persist = persist or self.PERSIST or self.is_test_failed(request)\n        if not persist:\n            db_handler.teardown(self.TEST_DB_NAME)\n            db_handler.teardown(self.TEST_OPENPYPE_NAME)\n\n    @pytest.fixture(scope=\"module\")\n    def dbcon(self, db_setup, output_folder_url):\n        \"\"\"Provide test database connection.\n\n            Database prepared from dumps with 'db_setup' fixture.\n        \"\"\"\n        from openpype.pipeline import AvalonMongoDB\n        dbcon = AvalonMongoDB()\n        dbcon.Session[\"AVALON_PROJECT\"] = self.PROJECT\n        dbcon.Session[\"AVALON_ASSET\"] = self.ASSET\n        dbcon.Session[\"AVALON_TASK\"] = self.TASK\n\n        # set project root to temp folder\n        platform_str = platform.system().lower()\n        root_key = \"config.roots.work.{}\".format(platform_str)\n        dbcon.update_one(\n            {\"type\": \"project\"},\n            {\"$set\":\n                {\n                    root_key: output_folder_url\n                }}\n        )\n        yield dbcon\n\n    @pytest.fixture(scope=\"module\")\n    def dbcon_openpype(self, db_setup):\n        \"\"\"Provide test database connection for OP settings.\n\n            Database prepared from dumps with 'db_setup' fixture.\n        \"\"\"\n        from openpype.lib import OpenPypeMongoConnection\n        mongo_client = OpenPypeMongoConnection.get_mongo_client()\n        yield mongo_client[self.TEST_OPENPYPE_NAME][\"settings\"]\n\n    def is_test_failed(self, request):\n        return getattr(request.node, \"module_test_failure\", False)\n\n\nclass PublishTest(ModuleUnitTest):\n    \"\"\"Test class for publishing in hosts.\n\n        Implemented fixtures:\n            launched_app - launches APP with last_workfile_path\n            publish_finished - waits until publish is finished, host must\n                kill its process when finished publishing. Includes timeout\n                which raises ValueError\n\n        Not implemented:\n            last_workfile_path - returns path to testing workfile\n            startup_scripts - provide script for setup in host\n\n        Implemented tests:\n            test_folder_structure_same - compares published and expected\n                subfolders if they contain same files. Compares only on file\n                presence\n\n            TODO: implement test on file size, file content\n    \"\"\"\n\n    APP_GROUP = \"\"\n\n    TIMEOUT = 120  # publish timeout\n\n    # could be overwritten by command line arguments\n    # command line value takes precedence\n\n    # keep empty to locate latest installed variant or explicit\n    APP_VARIANT = \"\"\n    PERSIST = True  # True - keep test_db, test_openpype, outputted test files\n    TEST_DATA_FOLDER = None  # use specific folder of unzipped test file\n\n    SETUP_ONLY = False\n\n    @pytest.fixture(scope=\"module\")\n    def app_name(self, app_variant, app_group):\n        \"\"\"Returns calculated value for ApplicationManager. Eg.(nuke/12-2)\"\"\"\n        from openpype.lib import ApplicationManager\n        app_variant = app_variant or self.APP_VARIANT\n        app_group = app_group or self.APP_GROUP\n\n        application_manager = ApplicationManager()\n        if not app_variant:\n            variant = (\n                application_manager.find_latest_available_variant_for_group(\n                    app_group\n                )\n            )\n            app_variant = variant.name\n\n        yield \"{}/{}\".format(app_group, app_variant)\n\n    @pytest.fixture(scope=\"module\")\n    def app_args(self, download_test_data):\n        \"\"\"Returns additional application arguments from a test file.\n\n            Test zip file should contain file at:\n                FOLDER_DIR/input/app_args/app_args.json\n            containing a list of command line arguments (like '-x' etc.)\n        \"\"\"\n        app_args = []\n        args_url = os.path.join(download_test_data, \"input\",\n                                \"app_args\", \"app_args.json\")\n        if not os.path.exists(args_url):\n            print(\"App argument file {} doesn't exist\".format(args_url))\n        else:\n            try:\n                with open(args_url) as json_file:\n                    app_args = json.load(json_file)\n\n                if not isinstance(app_args, list):\n                    raise ValueError\n            except ValueError:\n                print(\"{} doesn't contain valid JSON\".format(args_url))\n                six.reraise(*sys.exc_info())\n\n        yield app_args\n\n    @pytest.fixture(scope=\"module\")\n    def launched_app(self, dbcon, download_test_data, last_workfile_path,\n                     startup_scripts, app_args, app_name, output_folder_url,\n                     setup_only):\n        \"\"\"Launch host app\"\"\"\n        if setup_only or self.SETUP_ONLY:\n            print(\"Creating only setup for test, not launching app\")\n            yield\n            return\n        # set schema - for integrate_new\n        from openpype import PACKAGE_DIR\n        # Path to OpenPype's schema\n        schema_path = os.path.join(\n            os.path.dirname(PACKAGE_DIR),\n            \"schema\"\n        )\n        os.environ[\"AVALON_SCHEMA\"] = schema_path\n\n        os.environ[\"OPENPYPE_EXECUTABLE\"] = sys.executable\n        from openpype.lib import ApplicationManager\n\n        application_manager = ApplicationManager()\n        data = {\n            \"last_workfile_path\": last_workfile_path,\n            \"start_last_workfile\": True,\n            \"project_name\": self.PROJECT,\n            \"asset_name\": self.ASSET,\n            \"task_name\": self.TASK\n        }\n        if app_args:\n            data[\"app_args\"] = app_args\n\n        app_process = application_manager.launch(app_name, **data)\n        yield app_process\n\n    @pytest.fixture(scope=\"module\")\n    def publish_finished(self, dbcon, launched_app, download_test_data,\n                         timeout, setup_only):\n        \"\"\"Dummy fixture waiting for publish to finish\"\"\"\n        if setup_only or self.SETUP_ONLY:\n            print(\"Creating only setup for test, not launching app\")\n            yield False\n            return\n\n        time_start = time.time()\n        timeout = timeout or self.TIMEOUT\n        timeout = float(timeout)\n        while launched_app.poll() is None:\n            time.sleep(0.5)\n            if time.time() - time_start > timeout:\n                launched_app.terminate()\n                raise ValueError(\"Timeout reached\")\n\n        # some clean exit test possible?\n        print(\"Publish finished\")\n        yield True\n\n    def test_folder_structure_same(self, dbcon, publish_finished,\n                                   download_test_data, output_folder_url,\n                                   skip_compare_folders,\n                                   setup_only):\n        \"\"\"Check if expected and published subfolders contain same files.\n\n            Compares only presence, not size nor content!\n        \"\"\"\n        if setup_only or self.SETUP_ONLY:\n            print(\"Creating only setup for test, not launching app\")\n            return\n\n        published_dir_base = output_folder_url\n        expected_dir_base = os.path.join(download_test_data,\n                                         \"expected\")\n\n        print(\n            \"Comparing published: '{}' | expected: '{}'\".format(\n                published_dir_base, expected_dir_base\n            )\n        )\n\n        def get_files(dir_base):\n            result = set()\n\n            for f in glob.glob(dir_base + \"\\\\**\", recursive=True):\n                if os.path.isdir(f):\n                    continue\n\n                if f != dir_base and os.path.exists(f):\n                    result.add(f.replace(dir_base, \"\"))\n\n            return result\n\n        published = get_files(published_dir_base)\n        expected = get_files(expected_dir_base)\n\n        filtered_published = self._filter_files(\n            published, skip_compare_folders\n        )\n\n        # filter out temp files also in expected\n        # could be polluted by accident by copying 'output' to zip file\n        filtered_expected = self._filter_files(expected, skip_compare_folders)\n\n        not_matched = filtered_expected.symmetric_difference(\n            filtered_published\n        )\n        if not_matched:\n            raise AssertionError(\n                \"Missing {} files\".format(\"\\n\".join(sorted(not_matched)))\n            )\n\n    def _filter_files(self, source_files, skip_compare_folders):\n        \"\"\"Filter list of files according to regex pattern.\"\"\"\n        filtered = set()\n        for file_path in source_files:\n            if skip_compare_folders:\n                if not any([re.search(val, file_path)\n                            for val in skip_compare_folders]):\n                    filtered.add(file_path)\n            else:\n                filtered.add(file_path)\n\n        return filtered\n\n\nclass DeadlinePublishTest(PublishTest):\n    @pytest.fixture(scope=\"module\")\n    def publish_finished(self, dbcon, launched_app, download_test_data,\n                         timeout):\n        \"\"\"Dummy fixture waiting for publish to finish\"\"\"\n        import time\n        time_start = time.time()\n        timeout = timeout or self.TIMEOUT\n        timeout = float(timeout)\n        while launched_app.poll() is None:\n            time.sleep(0.5)\n            if time.time() - time_start > timeout:\n                launched_app.terminate()\n                raise ValueError(\"Timeout reached\")\n\n        metadata_json = glob.glob(os.path.join(download_test_data,\n                                               \"output\",\n                                               \"**/*_metadata.json\"),\n                                  recursive=True)\n        if not metadata_json:\n            raise RuntimeError(\"No metadata file found. No job id.\")\n\n        if len(metadata_json) > 1:\n            # depends on creation order of published jobs\n            metadata_json.sort(key=os.path.getmtime, reverse=True)\n\n        with open(metadata_json[0]) as fp:\n            job_info = json.load(fp)\n\n        deadline_job_id = job_info[\"deadline_publish_job_id\"]\n\n        manager = ModulesManager()\n        deadline_module = manager.modules_by_name[\"deadline\"]\n        deadline_url = deadline_module.deadline_urls[\"default\"]\n\n        if not deadline_url:\n            raise ValueError(\"Must have default deadline url.\")\n\n        url = \"{}/api/jobs?JobId={}\".format(deadline_url, deadline_job_id)\n        valid_date_finished = None\n\n        time_start = time.time()\n        while not valid_date_finished:\n            time.sleep(0.5)\n            if time.time() - time_start > timeout:\n                raise ValueError(\"Timeout for Deadline finish reached\")\n\n            response = requests.get(url, timeout=10)\n            if not response.ok:\n                msg = \"Couldn't connect to {}\".format(deadline_url)\n                raise RuntimeError(msg)\n\n            if not response.json():\n                raise ValueError(\"Couldn't find {}\".format(deadline_job_id))\n\n            job = response.json()[0]\n\n            def recursive_dependencies(job, results=None):\n                if results is None:\n                    results = []\n\n                for dependency in job[\"Props\"][\"Dep\"]:\n                    dependency = requests.get(\n                        \"{}/api/jobs?JobId={}\".format(\n                            deadline_url, dependency[\"JobID\"]\n                        ),\n                        timeout=10\n                    ).json()[0]\n                    results.append(dependency)\n                    grand_dependencies = recursive_dependencies(\n                        dependency, results=results\n                    )\n                    for grand_dependency in grand_dependencies:\n                        if grand_dependency not in results:\n                            results.append(grand_dependency)\n                return results\n\n            job_status = {\n                0: \"Unknown\",\n                1: \"Active\",\n                2: \"Suspended\",\n                3: \"Completed\",\n                4: \"Failed\",\n                6: \"Pending\"\n            }\n\n            jobs_to_validate = [job]\n            jobs_to_validate.extend(recursive_dependencies(job))\n            failed_jobs = []\n            errors = []\n            for job in jobs_to_validate:\n                if \"Failed\" == job_status[job[\"Stat\"]]:\n                    failed_jobs.append(str(job))\n\n                resp_error = requests.get(\n                    \"{}/api/jobreports?JobID={}&Data=allerrorcontents\".format(\n                        deadline_url, job[\"_id\"]\n                    ),\n                    timeout=10\n                )\n                errors.extend(resp_error.json())\n\n            msg = \"Errors in Deadline:\\n\"\n            msg += \"\\n\".join(errors)\n            assert not errors, msg\n\n            msg = \"Failed in Deadline:\\n\"\n            msg += \"\\n\".join(failed_jobs)\n            assert not failed_jobs, msg\n\n            # '0001-...' returned until job is finished\n            valid_date_finished = response.json()[0][\"DateComp\"][:4] != \"0001\"\n\n        # some clean exit test possible?\n        print(\"Publish finished\")\n        yield True\n\n\nclass HostFixtures():\n    \"\"\"Host specific fixtures. Should be implemented once per host.\"\"\"\n    @pytest.fixture(scope=\"module\")\n    def last_workfile_path(self, download_test_data, output_folder_url):\n        \"\"\"Returns url of workfile\"\"\"\n        raise NotImplementedError\n\n    @pytest.fixture(scope=\"module\")\n    def startup_scripts(self, monkeypatch_session, download_test_data):\n        \"\"\"\"Adds init scripts (like userSetup) to expected location\"\"\"\n        raise NotImplementedError\n\n    @pytest.fixture(scope=\"module\")\n    def skip_compare_folders(self):\n        \"\"\"Use list of regexs to filter out published folders from comparing\"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "tests/unit/igniter/test_bootstrap_repos.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Test suite for repos bootstrapping (install).\"\"\"\nimport os\nimport sys\nfrom collections import namedtuple\nfrom pathlib import Path\nfrom zipfile import ZipFile\nfrom uuid import uuid4\n\nimport appdirs\nimport pytest\n\nfrom igniter.bootstrap_repos import BootstrapRepos\nfrom igniter.bootstrap_repos import OpenPypeVersion\nfrom igniter.user_settings import OpenPypeSettingsRegistry\n\n\n@pytest.fixture\ndef fix_bootstrap(tmp_path, pytestconfig):\n    \"\"\"This will fix BoostrapRepos with temp paths.\"\"\"\n    bs = BootstrapRepos()\n    bs.live_repo_dir = pytestconfig.rootpath / 'repos'\n    bs.data_dir = tmp_path\n    return bs\n\n\ndef test_openpype_version(printer):\n    \"\"\"Test determination of OpenPype versions.\"\"\"\n    v1 = OpenPypeVersion(1, 2, 3)\n    assert str(v1) == \"1.2.3\"\n\n    v2 = OpenPypeVersion(1, 2, 3, prerelease=\"x\")\n    assert str(v2) == \"1.2.3-x\"\n    assert v1 > v2\n\n    v3 = OpenPypeVersion(1, 2, 3)\n    assert str(v3) == \"1.2.3\"\n\n    v4 = OpenPypeVersion(1, 2, 3, prerelease=\"rc.1\")\n    assert str(v4) == \"1.2.3-rc.1\"\n    assert v3 > v4\n    assert v1 > v4\n    assert v4 < OpenPypeVersion(1, 2, 3, prerelease=\"rc.1\")\n\n    v5 = OpenPypeVersion(1, 2, 3, build=\"foo\", prerelease=\"x\")\n    assert str(v5) == \"1.2.3-x+foo\"\n    assert v4 < v5\n\n    v6 = OpenPypeVersion(1, 2, 3, prerelease=\"foo\")\n    assert str(v6) == \"1.2.3-foo\"\n\n    v7 = OpenPypeVersion(2, 0, 0)\n    assert v1 < v7\n\n    v8 = OpenPypeVersion(0, 1, 5)\n    assert v8 < v7\n\n    v9 = OpenPypeVersion(1, 2, 4)\n    assert v9 > v1\n\n    v10 = OpenPypeVersion(1, 2, 2)\n    assert v10 < v1\n\n    v11 = OpenPypeVersion(1, 2, 3, path=Path(\"/foo/bar\"))\n    assert v10 < v11\n\n    assert v5 == v2\n\n    sort_versions = [\n        OpenPypeVersion(3, 2, 1),\n        OpenPypeVersion(1, 2, 3),\n        OpenPypeVersion(0, 0, 1),\n        OpenPypeVersion(4, 8, 10),\n        OpenPypeVersion(4, 8, 20),\n        OpenPypeVersion(4, 8, 9),\n        OpenPypeVersion(1, 2, 3),\n        OpenPypeVersion(1, 2, 3, build=\"foo\")\n    ]\n    res = sorted(sort_versions)\n\n    assert res[0] == sort_versions[2]\n    assert res[1] == sort_versions[6]\n    assert res[2] == sort_versions[1]\n    assert res[-1] == sort_versions[4]\n\n    str_versions = [\n        \"5.5.1\",\n        \"5.5.2-foo\",\n        \"5.5.3-foo+strange\",\n        \"5.5.4+staging\",\n        \"5.5.5+staging-client\",\n        \"5.6.3\",\n        \"5.6.3+staging\"\n    ]\n    res_versions = [OpenPypeVersion(version=v) for v in str_versions]\n    sorted_res_versions = sorted(res_versions)\n\n    assert str(sorted_res_versions[0]) == str_versions[0]\n    assert str(sorted_res_versions[-1]) == str_versions[5]\n\n    with pytest.raises(TypeError):\n        _ = OpenPypeVersion()\n\n    with pytest.raises(ValueError):\n        _ = OpenPypeVersion(version=\"booobaa\")\n\n    v11 = OpenPypeVersion(version=\"4.6.7-foo\")\n    assert v11.major == 4\n    assert v11.minor == 6\n    assert v11.patch == 7\n    assert v11.prerelease == \"foo\"\n\n\ndef test_get_main_version():\n    ver = OpenPypeVersion(1, 2, 3, prerelease=\"foo\")\n    assert ver.get_main_version() == \"1.2.3\"\n\n\ndef test_get_version_path_from_list():\n    versions = [\n        OpenPypeVersion(1, 2, 3, path=Path('/foo/bar')),\n        OpenPypeVersion(3, 4, 5, path=Path(\"/bar/baz\")),\n        OpenPypeVersion(6, 7, 8, prerelease=\"x\", path=Path(\"boo/goo\"))\n    ]\n    path = BootstrapRepos.get_version_path_from_list(\n        \"3.4.5\", versions)\n\n    assert path == Path(\"/bar/baz\")\n\n\ndef test_search_string_for_openpype_version(printer):\n    strings = [\n        (\"3.0.1\", True),\n        (\"foo-3.0\", False),\n        (\"foo-3.0.1\", True),\n        (\"3\", False),\n        (\"foo-3.0.1-client-staging\", True),\n        (\"foo-3.0.1-bar-baz\", True)\n    ]\n    for ver_string in strings:\n        printer(f\"testing {ver_string[0]} should be {ver_string[1]}\")\n        assert isinstance(\n            OpenPypeVersion.version_in_str(ver_string[0]),\n            OpenPypeVersion if ver_string[1] else type(None)\n        )\n\n@pytest.mark.slow\ndef test_install_live_repos(fix_bootstrap, printer, monkeypatch, pytestconfig):\n    monkeypatch.setenv(\"OPENPYPE_ROOT\", pytestconfig.rootpath.as_posix())\n    monkeypatch.setenv(\"OPENPYPE_DATABASE_NAME\", str(uuid4()))\n    openpype_version = fix_bootstrap.create_version_from_live_code()\n    sep = os.path.sep\n    expected_paths = [\n        f\"{openpype_version.path}\"\n    ]\n    printer(\"testing zip creation\")\n    assert os.path.exists(openpype_version.path), \"zip archive was not created\"\n    fix_bootstrap.add_paths_from_archive(openpype_version.path)\n    for ep in expected_paths:\n        assert ep in sys.path, f\"{ep} not set correctly\"\n\n    printer(\"testing openpype imported\")\n    try:\n        del sys.modules[\"openpype\"]\n    except KeyError:\n        # wasn't imported before\n        pass\n    import openpype  # noqa: F401\n\n    # test if openpype is imported from specific location in zip\n    assert \"openpype\" in sys.modules.keys(), \"OpenPype not imported\"\n    assert sys.modules[\"openpype\"].__file__ == \\\n        f\"{openpype_version.path}{sep}openpype{sep}__init__.py\"\n\n\ndef test_find_openpype(fix_bootstrap, tmp_path_factory, monkeypatch, printer):\n    test_openpype = namedtuple(\"OpenPype\", \"prefix version suffix type valid\")\n\n    test_versions_1 = [\n        test_openpype(prefix=\"foo-v\", version=\"5.5.1\",\n                      suffix=\".zip\", type=\"zip\", valid=False),\n        test_openpype(prefix=\"bar-v\", version=\"5.5.2-rc.1\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"baz-v\", version=\"5.5.3-foo-strange\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"bum-v\", version=\"5.5.4+staging\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"zum-v\", version=\"5.5.5-foo+staging\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"fam-v\", version=\"5.6.3\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"foo-v\", version=\"5.6.3+staging\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"fim-v\", version=\"5.6.3\",\n                      suffix=\".zip\", type=\"zip\", valid=False),\n        test_openpype(prefix=\"foo-v\", version=\"5.6.4\",\n                      suffix=\".txt\", type=\"txt\", valid=False),\n        test_openpype(prefix=\"foo-v\", version=\"5.7.1\",\n                      suffix=\"\", type=\"dir\", valid=False),\n    ]\n\n    test_versions_2 = [\n        test_openpype(prefix=\"foo-v\", version=\"10.0.0\",\n                      suffix=\".txt\", type=\"txt\", valid=False),\n        test_openpype(prefix=\"lom-v\", version=\"7.2.6\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"bom-v\", version=\"7.2.7-rc.3\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"woo-v\", version=\"7.2.8-foo-strange\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"loo-v\", version=\"7.2.10-foo+staging\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"kok-v\", version=\"7.0.1\",\n                      suffix=\".zip\", type=\"zip\", valid=True)\n    ]\n\n    test_versions_3 = [\n        test_openpype(prefix=\"foo-v\", version=\"3.0.0\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"goo-v\", version=\"3.0.1\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"hoo-v\", version=\"4.1.0\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"foo-v\", version=\"4.1.2\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"foo-v\", version=\"3.0.1-foo\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"foo-v\", version=\"3.0.1-foo-strange\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"foo-v\", version=\"3.0.1+staging\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"foo-v\", version=\"3.0.1-foo+staging\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"foo-v\", version=\"3.2.0\",\n                      suffix=\".zip\", type=\"zip\", valid=True)\n    ]\n\n    test_versions_4 = [\n        test_openpype(prefix=\"foo-v\", version=\"10.0.0\",\n                      suffix=\"\", type=\"dir\", valid=True),\n        test_openpype(prefix=\"lom-v\", version=\"11.2.6\",\n                      suffix=\".zip\", type=\"dir\", valid=False),\n        test_openpype(prefix=\"bom-v\", version=\"7.2.7-foo\",\n                      suffix=\".zip\", type=\"zip\", valid=True),\n        test_openpype(prefix=\"woo-v\", version=\"7.2.8-foo-strange\",\n                      suffix=\".zip\", type=\"txt\", valid=False)\n    ]\n\n    def _create_invalid_zip(path: Path):\n        with ZipFile(path, \"w\") as zf:\n            zf.writestr(\"test.foo\", \"test\")\n\n    def _create_valid_zip(path: Path, version: str):\n        with ZipFile(path, \"w\") as zf:\n            zf.writestr(\n                \"openpype/version.py\", f\"__version__ = '{version}'\\n\\n\")\n\n    def _create_invalid_dir(path: Path):\n        path.mkdir(parents=True, exist_ok=True)\n        with open(path / \"invalid\", \"w\") as fp:\n            fp.write(\"invalid\")\n\n    def _create_valid_dir(path: Path, version: str):\n        openpype_path = path / \"openpype\"\n        version_path = openpype_path / \"version.py\"\n        openpype_path.mkdir(parents=True, exist_ok=True)\n        with open(version_path, \"w\") as fp:\n            fp.write(f\"__version__ = '{version}'\\n\\n\")\n\n    def _build_test_item(path, item):\n        test_path = path / \"{}{}{}\".format(item.prefix,\n                                           item.version,\n                                           item.suffix)\n        if item.type == \"zip\":\n            if item.valid:\n                _create_valid_zip(test_path, item.version)\n            else:\n                _create_invalid_zip(test_path)\n        elif item.type == \"dir\":\n            if item.valid:\n                _create_valid_dir(test_path, item.version)\n            else:\n                _create_invalid_dir(test_path)\n        else:\n            with open(test_path, \"w\") as fp:\n                fp.write(\"foo\")\n\n    # in OPENPYPE_PATH\n    e_path = tmp_path_factory.mktemp(\"environ\")\n\n    # create files and directories for test\n    for test_file in test_versions_1:\n        _build_test_item(e_path, test_file)\n\n    # in openPypePath registry\n    p_path = tmp_path_factory.mktemp(\"openPypePath\")\n    for test_file in test_versions_2:\n        _build_test_item(p_path, test_file)\n\n    # in data dir\n    d_path = tmp_path_factory.mktemp(\"dataPath\")\n    for test_file in test_versions_2:\n        _build_test_item(d_path, test_file)\n\n    # in provided path\n    g_path = tmp_path_factory.mktemp(\"providedPath\")\n    for test_file in test_versions_3:\n        _build_test_item(g_path, test_file)\n\n    # dir vs zip preference\n    dir_path = tmp_path_factory.mktemp(\"dirZipPath\")\n    for test_file in test_versions_4:\n        _build_test_item(dir_path, test_file)\n\n    printer(\"testing finding OpenPype in given path ...\")\n    result = fix_bootstrap.find_openpype(g_path, include_zips=True)\n    # we should have results as file were created\n    assert result is not None, \"no OpenPype version found\"\n    # latest item in `result` should be latest version found.\n    expected_path = Path(\n        g_path / \"{}{}{}\".format(\n            test_versions_3[3].prefix,\n            test_versions_3[3].version,\n            test_versions_3[3].suffix\n        )\n    )\n    assert result, \"nothing found\"\n    assert result[-1].path == expected_path, (\"not a latest version of \"\n                                              \"OpenPype 3\")\n\n    printer(\"testing finding OpenPype in OPENPYPE_PATH ...\")\n    monkeypatch.setenv(\"OPENPYPE_PATH\", e_path.as_posix())\n    result = fix_bootstrap.find_openpype(include_zips=True)\n    # we should have results as file were created\n    assert result is not None, \"no OpenPype version found\"\n    # latest item in `result` should be latest version found.\n    expected_path = Path(\n        e_path / \"{}{}{}\".format(\n            test_versions_1[5].prefix,\n            test_versions_1[5].version,\n            test_versions_1[5].suffix\n        )\n    )\n    assert result, \"nothing found\"\n    assert result[-1].path == expected_path, (\"not a latest version of \"\n                                              \"OpenPype 1\")\n\n    monkeypatch.delenv(\"OPENPYPE_PATH\", raising=False)\n\n    printer(\"testing finding OpenPype in user data dir ...\")\n\n    # mock appdirs user_data_dir\n    def mock_user_data_dir(*args, **kwargs):\n        \"\"\"Mock local app data dir.\"\"\"\n        return d_path.as_posix()\n\n    monkeypatch.setattr(appdirs, \"user_data_dir\", mock_user_data_dir)\n    fix_bootstrap.registry = OpenPypeSettingsRegistry()\n    fix_bootstrap.registry.set_item(\"openPypePath\", d_path.as_posix())\n\n    result = fix_bootstrap.find_openpype(include_zips=True)\n    # we should have results as file were created\n    assert result is not None, \"no OpenPype version found\"\n    # latest item in `result` should be the latest version found.\n    # this will be `7.2.10-foo+staging` even with *staging* in since we've\n    # dropped the logic to handle staging separately and in alphabetical\n    # sorting it is after `strange`.\n    expected_path = Path(\n        d_path / \"{}{}{}\".format(\n            test_versions_2[4].prefix,\n            test_versions_2[4].version,\n            test_versions_2[4].suffix\n        )\n    )\n    assert result, \"nothing found\"\n    assert result[-1].path == expected_path, (\"not a latest version of \"\n                                              \"OpenPype 2\")\n\n    printer(\"testing finding OpenPype zip/dir precedence ...\")\n    result = fix_bootstrap.find_openpype(dir_path, include_zips=True)\n    assert result is not None, \"no OpenPype versions found\"\n    expected_path = Path(\n        dir_path / \"{}{}{}\".format(\n            test_versions_4[0].prefix,\n            test_versions_4[0].version,\n            test_versions_4[0].suffix\n        )\n    )\n    assert result[-1].path == expected_path, (\"not a latest version of \"\n                                              \"OpenPype 4\")\n"
  },
  {
    "path": "tests/unit/igniter/test_tools.py",
    "content": "# -*- coding: utf-8 -*-\nfrom uuid import uuid4\nfrom igniter.tools import validate_path_string\n\n\ndef test_validate_path_string(tmp_path):\n    # test path\n    status1, _ = validate_path_string(tmp_path.as_posix())\n    assert status1 is True\n    status2, _ = validate_path_string(\"booo\" + str(uuid4()))\n    assert status2 is False\n\n"
  },
  {
    "path": "tests/unit/openpype/conftest.py",
    "content": "\"\"\"Dummy environment that allows importing Openpype modules and run\ntests in parent folder and all subfolders manually from IDE.\n\nThis should not get triggered if the tests are running from `runtests` as it\nis expected there that environment is handled by OP itself.\n\nThis environment should be enough to run simple `BaseTest` where no\nexternal preparation is necessary (eg. no prepared DB, no source files).\nThese tests might be enough to import and run simple pyblish plugins to\nvalidate logic.\n\nPlease be aware that these tests might use values in real databases, so use\n`BaseTest` only for logic without side effects or special configuration. For\nthese there is `tests.lib.testing_classes.ModuleUnitTest` which would setup\nproper test DB (but it requires `mongorestore` on the sys.path)\n\nIf pyblish plugins require any host dependent communication, it would need\n to be mocked.\n\nThis setting of env vars is necessary to run before any imports of OP code!\n(This is why it is in `conftest.py` file.)\nIf your test requires any additional env var, copy this file to folder of your\ntest, it should only that folder.\n\"\"\"\n\nimport os\n\n\nif not os.environ.get(\"IS_TEST\"):  # running tests from cmd or CI\n    os.environ[\"OPENPYPE_MONGO\"] = \"mongodb://localhost:27017\"\n    os.environ[\"AVALON_DB\"] = \"avalon\"\n    os.environ[\"OPENPYPE_DATABASE_NAME\"] = \"openpype\"\n    os.environ[\"AVALON_TIMEOUT\"] = '3000'\n    os.environ[\"OPENPYPE_DEBUG\"] = \"1\"\n    os.environ[\"AVALON_ASSET\"] = \"test_asset\"\n    os.environ[\"AVALON_PROJECT\"] = \"test_project\"\n"
  },
  {
    "path": "tests/unit/openpype/hosts/photoshop/test_lib.py",
    "content": "import pytest\n\nfrom openpype.hosts.photoshop.lib import clean_subset_name\n\n\"\"\"\nTests cleanup of unused layer placeholder ({layer}) from subset name.\nLayer differentiation might be desired in subset name, but in some cases it\nmight be used (in `auto_image` - only single image without layer diff.,\nsingle image instance created without toggled use of subset name etc.)\n\"\"\"\n\n\ndef test_no_layer_placeholder():\n    clean_subset = clean_subset_name(\"imageMain\")\n    assert \"imageMain\" == clean_subset\n\n\n@pytest.mark.parametrize(\"subset_name\",\n                         [\"imageMain{Layer}\",\n                          \"imageMain_{layer}\",  # trailing _\n                          \"image{Layer}Main\",\n                          \"image{LAYER}Main\"])\ndef test_not_used_layer_placeholder(subset_name):\n    clean_subset = clean_subset_name(subset_name)\n    assert \"imageMain\" == clean_subset\n"
  },
  {
    "path": "tests/unit/openpype/hosts/unreal/plugins/publish/test_validate_sequence_frames.py",
    "content": "\n\n\"\"\"Test Publish_plugins pipeline publish modul, tests API methods\n\n    File:\n        creates temporary directory and downloads .zip file from GDrive\n        unzips .zip file\n        uses content of .zip file (MongoDB's dumps) to import to new databases\n        with use of 'monkeypatch_session' modifies required env vars\n            temporarily\n        runs battery of tests checking that site operation for Sync Server\n            module are working\n        removes temporary folder\n        removes temporary databases (?)\n\"\"\"\nimport pytest\nimport logging\n\nfrom pyblish.api import Instance as PyblishInstance\n\nfrom tests.lib.testing_classes import BaseTest\nfrom openpype.hosts.unreal.plugins.publish.validate_sequence_frames import (\n    ValidateSequenceFrames\n)\n\nlog = logging.getLogger(__name__)\n\n\nclass TestValidateSequenceFrames(BaseTest):\n    \"\"\" Testing ValidateSequenceFrames plugin\n\n    \"\"\"\n\n    @pytest.fixture\n    def instance(self):\n\n        class Instance(PyblishInstance):\n            data = {\n                \"frameStart\": 1001,\n                \"frameEnd\": 1002,\n                \"representations\": [],\n                \"assetEntity\": {\n                    \"data\": {\n                        \"clipIn\": 1001,\n                        \"clipOut\": 1002,\n                    }\n                }\n            }\n        yield Instance\n\n    @pytest.fixture(scope=\"module\")\n    def plugin(self):\n        plugin = ValidateSequenceFrames()\n        plugin.log = log\n\n        yield plugin\n\n    def test_validate_sequence_frames_single_frame(self, instance, plugin):\n        representations = [\n            {\n                \"ext\": \"exr\",\n                \"files\": \"Main_beauty.1001.exr\",\n            }\n        ]\n        instance.data[\"representations\"] = representations\n        instance.data[\"frameEnd\"] = 1001\n        instance.data[\"assetEntity\"][\"data\"][\"clipOut\"] = 1001\n\n        plugin.process(instance)\n\n    @pytest.mark.parametrize(\"files\",\n                             [\n                              [\"Main_beauty.v001.1001.exr\",\n                               \"Main_beauty.v001.1002.exr\"],\n                              [\"Main_beauty_v001.1001.exr\",\n                               \"Main_beauty_v001.1002.exr\"],\n                              [\"Main_beauty.1001.1001.exr\",\n                               \"Main_beauty.1001.1002.exr\"],\n                              [\"Main_beauty_v001_1001.exr\",\n                               \"Main_beauty_v001_1002.exr\"]])\n    def test_validate_sequence_frames_name(self, instance,\n                                           plugin, files):\n        # tests for names with number inside, caused clique failure before\n        representations = [\n            {\n                \"ext\": \"exr\",\n                \"files\": files,\n            }\n        ]\n        instance.data[\"representations\"] = representations\n\n        plugin.process(instance)\n\n    @pytest.mark.parametrize(\"files\",\n                             [[\"Main_beauty.v001.1001.ass.gz\",\n                               \"Main_beauty.v001.1002.ass.gz\"]])\n    def test_validate_sequence_frames__correct_ext(\n            self, instance, plugin, files):\n        representations = [\n            {\n                \"ext\": \"ass.gz\",\n                \"files\": files,\n            }\n        ]\n        instance.data[\"representations\"] = representations\n\n        plugin.process(instance)\n\n    def test_validate_sequence_frames_multi_frame(self, instance, plugin):\n        representations = [\n            {\n                \"ext\": \"exr\",\n                \"files\": [\"Main_beauty.1001.exr\", \"Main_beauty.1002.exr\",\n                          \"Main_beauty.1003.exr\"]\n            }\n        ]\n        instance.data[\"representations\"] = representations\n        instance.data[\"frameEnd\"] = 1003\n        instance.data[\"assetEntity\"][\"data\"][\"clipOut\"] = 1003\n\n        plugin.process(instance)\n\n    def test_validate_sequence_frames_multi_frame_missing(self, instance,\n                                                          plugin):\n        representations = [\n            {\n                \"ext\": \"exr\",\n                \"files\": [\"Main_beauty.1001.exr\", \"Main_beauty.1002.exr\"]\n            }\n        ]\n        instance.data[\"representations\"] = representations\n        instance.data[\"frameEnd\"] = 1003\n        instance.data[\"assetEntity\"][\"data\"][\"clipOut\"] = 1003\n\n        with pytest.raises(ValueError) as excinfo:\n            plugin.process(instance)\n        assert (\"Invalid frame range: (1001, 1002) - expected: (1001, 1003)\" in\n                str(excinfo.value))\n\n    def test_validate_sequence_frames_multi_frame_hole(self, instance, plugin):\n        representations = [\n            {\n                \"ext\": \"exr\",\n                \"files\": [\"Main_beauty.1001.exr\", \"Main_beauty.1003.exr\"]\n            }\n        ]\n        instance.data[\"representations\"] = representations\n        instance.data[\"frameEnd\"] = 1003\n        instance.data[\"assetEntity\"][\"data\"][\"clipOut\"] = 1003\n\n        with pytest.raises(AssertionError) as excinfo:\n            plugin.process(instance)\n        assert (\"Missing frames: [1002]\" in str(excinfo.value))\n\n    def test_validate_sequence_frames_slate(self, instance, plugin):\n        representations = [\n            {\n                \"ext\": \"exr\",\n                \"files\": [\n                    \"Main_beauty.1000.exr\",\n                    \"Main_beauty.1001.exr\",\n                    \"Main_beauty.1002.exr\",\n                    \"Main_beauty.1003.exr\"\n                ]\n            }\n        ]\n        instance.data[\"slate\"] = True\n        instance.data[\"representations\"] = representations\n        instance.data[\"frameEnd\"] = 1003\n        instance.data[\"assetEntity\"][\"data\"][\"clipOut\"] = 1003\n\n        plugin.process(instance)\n\n\ntest_case = TestValidateSequenceFrames()\n"
  },
  {
    "path": "tests/unit/openpype/lib/test_delivery.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Test suite for delivery functions.\"\"\"\nfrom openpype.lib import collect_frames\n\n\ndef test_collect_frames_multi_sequence():\n    files = [\"Asset_renderCompositingMain_v001.0000.png\",\n             \"Asset_renderCompositingMain_v001.0001.png\",\n             \"Asset_renderCompositingMain_v001.0002.png\"]\n    ret = collect_frames(files)\n\n    expected = {\n        \"Asset_renderCompositingMain_v001.0000.png\": \"0000\",\n        \"Asset_renderCompositingMain_v001.0001.png\": \"0001\",\n        \"Asset_renderCompositingMain_v001.0002.png\": \"0002\"\n    }\n\n    print(ret)\n    assert ret == expected, \"Not matching\"\n\n\ndef test_collect_frames_multi_sequence_different_format():\n    files = [\"Asset.v001.renderCompositingMain.0000.png\",\n             \"Asset.v001.renderCompositingMain.0001.png\",\n             \"Asset.v001.renderCompositingMain.0002.png\"]\n    ret = collect_frames(files)\n\n    expected = {\n        \"Asset.v001.renderCompositingMain.0000.png\": \"0000\",\n        \"Asset.v001.renderCompositingMain.0001.png\": \"0001\",\n        \"Asset.v001.renderCompositingMain.0002.png\": \"0002\"\n    }\n\n    print(ret)\n    assert ret == expected, \"Not matching\"\n\n\ndef test_collect_frames_single_sequence():\n    files = [\"Asset_renderCompositingMain_v001.0000.png\"]\n    ret = collect_frames(files)\n\n    expected = {\n        \"Asset_renderCompositingMain_v001.0000.png\": \"0000\"\n    }\n\n    print(ret)\n    assert ret == expected, \"Not matching\"\n\n\ndef test_collect_frames_single_sequence_negative():\n    files = [\"Asset_renderCompositingMain_v001.-0000.png\"]\n    ret = collect_frames(files)\n\n    expected = {\n        \"Asset_renderCompositingMain_v001.-0000.png\": None\n    }\n\n    print(ret)\n    assert ret == expected, \"Not matching\"\n\n\ndef test_collect_frames_single_sequence_shot():\n    files = [\"testing_sh010_workfileCompositing_v001.aep\"]\n    ret = collect_frames(files)\n\n    expected = {\n        \"testing_sh010_workfileCompositing_v001.aep\": None\n    }\n\n    print(ret)\n    assert ret == expected, \"Not matching\"\n\n\ndef test_collect_frames_single_sequence_numbers():\n    files = [\"PRJ_204_430_0005_renderLayoutMain_v001.0001.exr\"]\n    ret = collect_frames(files)\n\n    expected = {\n        \"PRJ_204_430_0005_renderLayoutMain_v001.0001.exr\": \"0001\"\n    }\n\n    print(ret)\n    assert ret == expected, \"Not matching\"\n\n\ndef test_collect_frames_single_sequence_shot_with_frame():\n    files = [\"testing_sh010_workfileCompositing_000_v001.aep\"]\n    ret = collect_frames(files)\n\n    expected = {\n        \"testing_sh010_workfileCompositing_000_v001.aep\": None\n    }\n\n    print(ret)\n    assert ret == expected, \"Not matching\"\n\n\ndef test_collect_frames_single_sequence_full_path():\n    files = ['C:/test_project/assets/locations/Town/work/compositing\\\\renders\\\\aftereffects\\\\test_project_TestAsset_compositing_v001\\\\TestAsset_renderCompositingMain_v001.mov']  # noqa: E501\n    ret = collect_frames(files)\n\n    expected = {\n        'C:/test_project/assets/locations/Town/work/compositing\\\\renders\\\\aftereffects\\\\test_project_TestAsset_compositing_v001\\\\TestAsset_renderCompositingMain_v001.mov': None  # noqa: E501\n    }\n\n    print(ret)\n    assert ret == expected, \"Not matching\"\n\n\ndef test_collect_frames_single_sequence_different_format():\n    files = [\"Asset.v001.renderCompositingMain_0000.png\"]\n    ret = collect_frames(files)\n\n    expected = {\n        \"Asset.v001.renderCompositingMain_0000.png\": None\n    }\n\n    print(ret)\n    assert ret == expected, \"Not matching\"\n\n\ndef test_collect_frames_single_sequence_withhout_version():\n    files = [\"pngv001.renderCompositingMain_0000.png\"]\n    ret = collect_frames(files)\n\n    expected = {\n        \"pngv001.renderCompositingMain_0000.png\": None\n    }\n\n    print(ret)\n    assert ret == expected, \"Not matching\"\n\n\ndef test_collect_frames_single_sequence_as_dict():\n    files = {\"Asset_renderCompositingMain_v001.0000.png\"}\n    ret = collect_frames(files)\n\n    expected = {\n        \"Asset_renderCompositingMain_v001.0000.png\": \"0000\"\n    }\n\n    print(ret)\n    assert ret == expected, \"Not matching\"\n\n\ndef test_collect_frames_single_file():\n    files = {\"Asset_renderCompositingMain_v001.png\"}\n    ret = collect_frames(files)\n\n    expected = {\n        \"Asset_renderCompositingMain_v001.png\": None\n    }\n\n    print(ret)\n    assert ret == expected, \"Not matching\"\n"
  },
  {
    "path": "tests/unit/openpype/lib/test_event_system.py",
    "content": "from functools import partial\nfrom openpype.lib.events import (\n    EventSystem,\n    QueuedEventSystem,\n    weakref_partial,\n)\n\n\ndef test_default_event_system():\n    output = []\n    expected_output = [3, 2, 1]\n    event_system = EventSystem()\n\n    def callback_1():\n        event_system.emit(\"topic.2\", {}, None)\n        output.append(1)\n\n    def callback_2():\n        event_system.emit(\"topic.3\", {}, None)\n        output.append(2)\n\n    def callback_3():\n        output.append(3)\n\n    event_system.add_callback(\"topic.1\", callback_1)\n    event_system.add_callback(\"topic.2\", callback_2)\n    event_system.add_callback(\"topic.3\", callback_3)\n\n    event_system.emit(\"topic.1\", {}, None)\n\n    assert output == expected_output, (\n        \"Callbacks were not called in correct order\")\n\n\ndef test_base_event_system_queue():\n    output = []\n    expected_output = [1, 2, 3]\n    event_system = QueuedEventSystem()\n\n    def callback_1():\n        event_system.emit(\"topic.2\", {}, None)\n        output.append(1)\n\n    def callback_2():\n        event_system.emit(\"topic.3\", {}, None)\n        output.append(2)\n\n    def callback_3():\n        output.append(3)\n\n    event_system.add_callback(\"topic.1\", callback_1)\n    event_system.add_callback(\"topic.2\", callback_2)\n    event_system.add_callback(\"topic.3\", callback_3)\n\n    event_system.emit(\"topic.1\", {}, None)\n\n    assert output == expected_output, (\n        \"Callbacks were not called in correct order\")\n\n\ndef test_manual_event_system_queue():\n    output = []\n    expected_output = [1, 2, 3]\n    event_system = QueuedEventSystem(auto_execute=False)\n\n    def callback_1():\n        event_system.emit(\"topic.2\", {}, None)\n        output.append(1)\n\n    def callback_2():\n        event_system.emit(\"topic.3\", {}, None)\n        output.append(2)\n\n    def callback_3():\n        output.append(3)\n\n    event_system.add_callback(\"topic.1\", callback_1)\n    event_system.add_callback(\"topic.2\", callback_2)\n    event_system.add_callback(\"topic.3\", callback_3)\n\n    event_system.emit(\"topic.1\", {}, None)\n\n    while True:\n        if event_system.process_next_event() is None:\n            break\n\n    assert output == expected_output, (\n        \"Callbacks were not called in correct order\")\n\n\ndef test_unordered_events():\n    \"\"\"\n    Validate if callbacks are triggered in order of their register.\n    \"\"\"\n\n    result = []\n\n    def function_a():\n        result.append(\"A\")\n\n    def function_b():\n        result.append(\"B\")\n\n    def function_c():\n        result.append(\"C\")\n\n    # Without order\n    event_system = QueuedEventSystem()\n    event_system.add_callback(\"test\", function_a)\n    event_system.add_callback(\"test\", function_b)\n    event_system.add_callback(\"test\", function_c)\n    event_system.emit(\"test\", {}, \"test\")\n\n    assert result == [\"A\", \"B\", \"C\"]\n\n\ndef test_ordered_events():\n    \"\"\"\n    Validate if callbacks are triggered by their order and order\n        of their register.\n    \"\"\"\n    result = []\n\n    def function_a():\n        result.append(\"A\")\n\n    def function_b():\n        result.append(\"B\")\n\n    def function_c():\n        result.append(\"C\")\n\n    def function_d():\n        result.append(\"D\")\n\n    def function_e():\n        result.append(\"E\")\n\n    def function_f():\n        result.append(\"F\")\n\n    # Without order\n    event_system = QueuedEventSystem()\n    event_system.add_callback(\"test\", function_a)\n    event_system.add_callback(\"test\", function_b, order=-10)\n    event_system.add_callback(\"test\", function_c, order=200)\n    event_system.add_callback(\"test\", function_d, order=150)\n    event_system.add_callback(\"test\", function_e)\n    event_system.add_callback(\"test\", function_f, order=200)\n    event_system.emit(\"test\", {}, \"test\")\n\n    assert result == [\"B\", \"A\", \"E\", \"D\", \"C\", \"F\"]\n\n\ndef test_events_partial_callbacks():\n    \"\"\"\n    Validate if partial callbacks are triggered.\n    \"\"\"\n\n    result = []\n\n    def function(name):\n        result.append(name)\n\n    def function_regular():\n        result.append(\"regular\")\n\n    event_system = QueuedEventSystem()\n    event_system.add_callback(\"test\", function_regular)\n    event_system.add_callback(\"test\", partial(function, \"foo\"))\n    event_system.add_callback(\"test\", weakref_partial(function, \"bar\"))\n    event_system.emit(\"test\", {}, \"test\")\n\n    # Delete function should also make partial callbacks invalid\n    del function\n    event_system.emit(\"test\", {}, \"test\")\n\n    assert result == [\"regular\", \"bar\", \"regular\"]\n"
  },
  {
    "path": "tests/unit/openpype/lib/test_user_settings.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Test suite for User Settings.\"\"\"\nimport pytest\nfrom igniter.user_settings import (\n    IniSettingRegistry,\n    JSONSettingRegistry,\n    OpenPypeSecureRegistry\n)\nfrom uuid import uuid4\nimport configparser\n\n\n@pytest.fixture\ndef secure_registry():\n    name = \"pypetest_{}\".format(str(uuid4()))\n    r = OpenPypeSecureRegistry(name)\n    yield r\n\n\n@pytest.fixture\ndef json_registry(tmpdir):\n    name = \"pypetest_{}\".format(str(uuid4()))\n    r = JSONSettingRegistry(name, tmpdir)\n    yield r\n\n\n@pytest.fixture\ndef ini_registry(tmpdir):\n    name = \"pypetest_{}\".format(str(uuid4()))\n    r = IniSettingRegistry(name, tmpdir)\n    yield r\n\n\ndef test_keyring(secure_registry):\n    secure_registry.set_item(\"item1\", \"foo\")\n    secure_registry.set_item(\"item2\", \"bar\")\n    result1 = secure_registry.get_item(\"item1\")\n    result2 = secure_registry.get_item(\"item2\")\n\n    assert result1 == \"foo\"\n    assert result2 == \"bar\"\n\n    secure_registry.delete_item(\"item1\")\n    secure_registry.delete_item(\"item2\")\n\n    with pytest.raises(ValueError):\n        secure_registry.get_item(\"item1\")\n        secure_registry.get_item(\"item2\")\n\n\ndef test_ini_registry(ini_registry):\n    ini_registry.set_item(\"test1\", \"bar\")\n    ini_registry.set_item_section(\"TEST\", \"test2\", \"foo\")\n    ini_registry.set_item_section(\"TEST\", \"test3\", \"baz\")\n    ini_registry[\"woo\"] = 1\n\n    result1 = ini_registry.get_item(\"test1\")\n    result2 = ini_registry.get_item_from_section(\"TEST\", \"test2\")\n    result3 = ini_registry.get_item_from_section(\"TEST\", \"test3\")\n    result4 = ini_registry[\"woo\"]\n\n    assert result1 == \"bar\"\n    assert result2 == \"foo\"\n    assert result3 == \"baz\"\n    assert result4 == \"1\"\n\n    with pytest.raises(ValueError):\n        ini_registry.get_item(\"xxx\")\n\n    with pytest.raises(ValueError):\n        ini_registry.get_item_from_section(\"FFF\", \"yyy\")\n\n    ini_registry.delete_item(\"test1\")\n    with pytest.raises(ValueError):\n        ini_registry.get_item(\"test1\")\n\n    ini_registry.delete_item_from_section(\"TEST\", \"test2\")\n    with pytest.raises(ValueError):\n        ini_registry.get_item_from_section(\"TEST\", \"test2\")\n\n    ini_registry.delete_item_from_section(\"TEST\", \"test3\")\n    with pytest.raises(ValueError):\n        ini_registry.get_item_from_section(\"TEST\", \"test3\")\n\n    del ini_registry[\"woo\"]\n    with pytest.raises(ValueError):\n        ini_registry.get_item(\"woo\")\n\n    # ensure TEST section is also deleted\n    cfg = configparser.ConfigParser()\n    cfg.read(ini_registry._registry_file)\n    assert \"TEST\" not in cfg.sections()\n\n    with pytest.raises(ValueError):\n        ini_registry.delete_item(\"ooo\")\n\n    with pytest.raises(ValueError):\n        ini_registry.delete_item_from_section(\"XXX\", \"UUU\")\n\n\ndef test_json_registry(json_registry):\n    json_registry.set_item(\"foo\", \"bar\")\n    json_registry.set_item(\"baz\", {\"a\": 1, \"b\": \"c\"})\n    json_registry[\"woo\"] = 1\n\n    result1 = json_registry.get_item(\"foo\")\n    result2 = json_registry.get_item(\"baz\")\n    result3 = json_registry[\"woo\"]\n\n    assert result1 == \"bar\"\n    assert result2[\"a\"] == 1\n    assert result2[\"b\"] == \"c\"\n    assert result3 == 1\n\n    with pytest.raises(ValueError):\n        json_registry.get_item(\"zoo\")\n\n    json_registry.delete_item(\"foo\")\n\n    with pytest.raises(ValueError):\n        json_registry.get_item(\"foo\")\n\n    del json_registry[\"woo\"]\n    with pytest.raises(ValueError):\n        json_registry.get_item(\"woo\")\n"
  },
  {
    "path": "tests/unit/openpype/modules/sync_server/test_module_api.py",
    "content": "\"\"\"Test file for Sync Server, tests API methods, currently for integrate_new\n\n    File:\n        creates temporary directory and downloads .zip file from GDrive\n        unzips .zip file\n        uses content of .zip file (MongoDB's dumps) to import to new databases\n        with use of 'monkeypatch_session' modifies required env vars\n            temporarily\n        runs battery of tests checking that site operation for Sync Server\n            module are working\n        removes temporary folder\n        removes temporary databases (?)\n\"\"\"\nimport pytest\n\nfrom tests.lib.testing_classes import ModuleUnitTest\n\n\nclass TestModuleApi(ModuleUnitTest):\n\n    REPRESENTATION_ID = \"60e578d0c987036c6a7b741d\"\n\n    TEST_FILES = [(\"1eCwPljuJeOI8A3aisfOIBKKjcmIycTEt\",\n                   \"test_site_operations.zip\", '')]\n\n    @pytest.fixture(scope=\"module\")\n    def setup_sync_server_module(self, dbcon):\n        \"\"\"Get sync_server_module from ModulesManager\"\"\"\n        from openpype.modules import ModulesManager\n\n        manager = ModulesManager()\n        sync_server = manager.modules_by_name[\"sync_server\"]\n        yield sync_server\n\n    def test_get_alt_site_pairs(self, setup_sync_server_module):\n        conf_sites = {\"SFTP\": {\"alternative_sites\": [\"studio\"]},\n                      \"studio2\": {\"alternative_sites\": [\"studio\"]}}\n\n        ret = setup_sync_server_module._get_alt_site_pairs(conf_sites)\n        expected = {\"SFTP\": {\"studio\", \"studio2\"},\n                    \"studio\": {\"SFTP\", \"studio2\"},\n                    \"studio2\": {\"studio\", \"SFTP\"}}\n        assert ret == expected, \"Not matching result\"\n\n    def test_get_alt_site_pairs_deep(self, setup_sync_server_module):\n        conf_sites = {\"A\": {\"alternative_sites\": [\"C\"]},\n                      \"B\": {\"alternative_sites\": [\"C\"]},\n                      \"C\": {\"alternative_sites\": [\"D\"]},\n                      \"D\": {\"alternative_sites\": [\"A\"]},\n                      \"F\": {\"alternative_sites\": [\"G\"]},\n                      \"G\": {\"alternative_sites\": [\"F\"]},\n                      }\n\n        ret = setup_sync_server_module._get_alt_site_pairs(conf_sites)\n        expected = {\"A\": {\"B\", \"C\", \"D\"},\n                    \"B\": {\"A\", \"C\", \"D\"},\n                    \"C\": {\"A\", \"B\", \"D\"},\n                    \"D\": {\"A\", \"B\", \"C\"},\n                    \"F\": {\"G\"},\n                    \"G\": {\"F\"}}\n        assert ret == expected, \"Not matching result\"\n\n\ntest_case = TestModuleApi()\n"
  },
  {
    "path": "tests/unit/openpype/modules/sync_server/test_site_operations.py",
    "content": "\"\"\"Test file for Sync Server, tests site operations add_site, remove_site.\n\n    File:\n        creates temporary directory and downloads .zip file from GDrive\n        unzips .zip file\n        uses content of .zip file (MongoDB's dumps) to import to new databases\n        with use of 'monkeypatch_session' modifies required env vars\n            temporarily\n        runs battery of tests checking that site operation for Sync Server\n            module are working\n        removes temporary folder\n        removes temporary databases (?)\n\"\"\"\nimport pytest\nfrom bson.objectid import ObjectId\n\nfrom tests.lib.testing_classes import ModuleUnitTest\n\nfrom openpype.modules.sync_server.utils import SiteAlreadyPresentError\n\n\n\nclass TestSiteOperation(ModuleUnitTest):\n\n    REPRESENTATION_ID = \"60e578d0c987036c6a7b741d\"\n\n    TEST_FILES = [(\"1FHE70Hi7y05LLT_1O3Y6jGxwZGXKV9zX\",\n                   \"test_site_operations.zip\", '')]\n\n    @pytest.fixture(scope=\"module\")\n    def setup_sync_server_module(self, dbcon):\n        \"\"\"Get sync_server_module from ModulesManager\"\"\"\n        from openpype.modules import ModulesManager\n\n        manager = ModulesManager()\n        sync_server = manager.modules_by_name[\"sync_server\"]\n        yield sync_server\n\n    @pytest.mark.usefixtures(\"dbcon\")\n    def test_project_created(self, dbcon):\n        assert ['test_project'] == dbcon.database.collection_names(False)\n\n    @pytest.mark.usefixtures(\"dbcon\")\n    def test_objects_imported(self, dbcon):\n        count_obj = len(list(dbcon.database[self.TEST_PROJECT_NAME].find({})))\n        assert 15 == count_obj\n\n    @pytest.mark.usefixtures(\"setup_sync_server_module\")\n    def test_add_site(self, dbcon, setup_sync_server_module):\n        \"\"\"Adds 'test_site', checks that added,\n            checks that doesn't duplicate.\"\"\"\n        query = {\n            \"_id\": ObjectId(self.REPRESENTATION_ID)\n        }\n\n        ret = dbcon.database[self.TEST_PROJECT_NAME].find(query)\n\n        assert 1 == len(list(ret)), \\\n            \"Single {} must be in DB\".format(self.REPRESENTATION_ID)\n\n        setup_sync_server_module.add_site(self.TEST_PROJECT_NAME,\n                                          self.REPRESENTATION_ID,\n                                          site_name='test_site')\n\n        ret = list(dbcon.database[self.TEST_PROJECT_NAME].find(query))\n\n        assert 1 == len(ret), \\\n            \"Single {} must be in DB\".format(self.REPRESENTATION_ID)\n\n        ret = ret.pop()\n        site_names = [site[\"name\"] for site in ret[\"files\"][0][\"sites\"]]\n        assert 'test_site' in site_names, \"Site name wasn't added\"\n\n    @pytest.mark.usefixtures(\"setup_sync_server_module\")\n    def test_add_site_again(self, dbcon, setup_sync_server_module):\n        \"\"\"Depends on test_add_site, must throw exception.\"\"\"\n        with pytest.raises(SiteAlreadyPresentError):\n            setup_sync_server_module.add_site(self.TEST_PROJECT_NAME,\n                                              self.REPRESENTATION_ID,\n                                              site_name='test_site')\n\n    @pytest.mark.usefixtures(\"setup_sync_server_module\")\n    def test_add_site_again_force(self, dbcon, setup_sync_server_module):\n        \"\"\"Depends on test_add_site, must not throw exception.\"\"\"\n        setup_sync_server_module.add_site(self.TEST_PROJECT_NAME,\n                                          self.REPRESENTATION_ID,\n                                          site_name='test_site', force=True)\n\n        query = {\n            \"_id\": ObjectId(self.REPRESENTATION_ID)\n        }\n\n        ret = list(dbcon.database[self.TEST_PROJECT_NAME].find(query))\n\n        assert 1 == len(ret), \\\n            \"Single {} must be in DB\".format(self.REPRESENTATION_ID)\n\n    @pytest.mark.usefixtures(\"setup_sync_server_module\")\n    def test_remove_site(self, dbcon, setup_sync_server_module):\n        \"\"\"Depends on test_add_site, must remove 'test_site'.\"\"\"\n        setup_sync_server_module.remove_site(self.TEST_PROJECT_NAME,\n                                             self.REPRESENTATION_ID,\n                                             site_name='test_site')\n\n        query = {\n            \"_id\": ObjectId(self.REPRESENTATION_ID)\n        }\n\n        ret = list(dbcon.database[self.TEST_PROJECT_NAME].find(query))\n\n        assert 1 == len(ret), \\\n            \"Single {} must be in DB\".format(self.REPRESENTATION_ID)\n\n        ret = ret.pop()\n        site_names = [site[\"name\"] for site in ret[\"files\"][0][\"sites\"]]\n\n        assert 'test_site' not in site_names, \"Site name wasn't removed\"\n\n    @pytest.mark.usefixtures(\"setup_sync_server_module\")\n    def test_remove_site_again(self, dbcon, setup_sync_server_module):\n        \"\"\"Depends on test_add_site, must trow exception\"\"\"\n        with pytest.raises(ValueError):\n            setup_sync_server_module.remove_site(self.TEST_PROJECT_NAME,\n                                                 self.REPRESENTATION_ID,\n                                                 site_name='test_site')\n\n        query = {\n            \"_id\": ObjectId(self.REPRESENTATION_ID)\n        }\n\n        ret = list(dbcon.database[self.TEST_PROJECT_NAME].find(query))\n\n        assert 1 == len(ret), \\\n            \"Single {} must be in DB\".format(self.REPRESENTATION_ID)\n\n\ntest_case = TestSiteOperation()\n"
  },
  {
    "path": "tests/unit/openpype/pipeline/lib.py",
    "content": "import pytest\nfrom tests.lib.testing_classes import ModuleUnitTest\nfrom openpype.pipeline import legacy_io\n\n\nclass TestPipeline(ModuleUnitTest):\n    \"\"\" Testing Pipeline base class\n    \"\"\"\n\n    @pytest.fixture(scope=\"module\")\n    def legacy_io(self, dbcon):\n        legacy_io.Session = dbcon.Session\n        yield legacy_io.Session\n"
  },
  {
    "path": "tests/unit/openpype/pipeline/publish/test_publish_plugins.py",
    "content": "\n\n\"\"\"Test Publish_plugins pipeline publish modul, tests API methods\n\n    File:\n        creates temporary directory and downloads .zip file from GDrive\n        unzips .zip file\n        uses content of .zip file (MongoDB's dumps) to import to new databases\n        with use of 'monkeypatch_session' modifies required env vars\n            temporarily\n        runs battery of tests checking that site operation for Sync Server\n            module are working\n        removes temporary folder\n        removes temporary databases (?)\n\"\"\"\nimport os\nimport pytest\nimport shutil\nimport logging\n\nfrom tests.unit.openpype.pipeline.lib import TestPipeline\nfrom openpype.pipeline.publish import publish_plugins\nfrom openpype.pipeline import colorspace\n\nlog = logging.getLogger(__name__)\n\n\nclass TestPipelinePublishPlugins(TestPipeline):\n    \"\"\" Testing Pipeline publish_plugins.py\n\n    Example:\n        cd to OpenPype repo root dir\n        poetry run python ./start.py runtests \\\n            ../tests/unit/openpype/pipeline/publish\n    \"\"\"\n\n    # files are the same as those used in `test_pipeline_colorspace`\n    TEST_FILES = [\n        (\n            \"1csqimz8bbNcNgxtEXklLz6GRv91D3KgA\",\n            \"test_pipeline_colorspace.zip\",\n            \"\"\n        )\n    ]\n    PROJECT = \"test_project\"\n    ASSET = \"sh0010\"\n    HIERARCHY = \"shots/sq010\"\n    TASK = \"comp\"\n\n    @pytest.fixture(scope=\"module\")\n    def context(self, legacy_io, project_settings):\n        class CTX:\n            data = {\n                \"projectName\": legacy_io[\"AVALON_PROJECT\"],\n                \"asset\": legacy_io[\"AVALON_ASSET\"],\n                \"hostName\": \"nuke\",\n                \"anatomyData\": {},\n                \"project_settings\": project_settings\n            }\n        yield CTX\n\n    @pytest.fixture(scope=\"module\")\n    def config_path_project(\n        self,\n        download_test_data,\n        output_folder_url\n    ):\n        src_path = os.path.join(\n            download_test_data,\n            \"input\",\n            \"data\",\n            \"configs\",\n            \"aces_1.3\",\n            \"ayon_aces_config_project.ocio\"\n        )\n        dest_dir = os.path.join(\n            output_folder_url,\n            self.PROJECT,\n            \"config\"\n        )\n        dest_path = os.path.join(\n            dest_dir,\n            \"aces.ocio\"\n        )\n        if not os.path.exists(dest_dir):\n            os.makedirs(dest_dir)\n\n        shutil.copyfile(src_path, dest_path)\n\n        yield dest_path\n\n    @pytest.fixture(scope=\"module\")\n    def config_path_asset(\n        self,\n        download_test_data,\n        output_folder_url\n    ):\n        src_path = os.path.join(\n            download_test_data,\n            \"input\",\n            \"data\",\n            \"configs\",\n            \"aces_1.3\",\n            \"ayon_aces_config_asset.ocio\"\n        )\n        dest_dir = os.path.join(\n            output_folder_url,\n            self.PROJECT,\n            self.HIERARCHY,\n            self.ASSET,\n            \"config\"\n        )\n        dest_path = os.path.join(\n            dest_dir,\n            \"aces.ocio\"\n        )\n        if not os.path.exists(dest_dir):\n            os.makedirs(dest_dir)\n\n        shutil.copyfile(src_path, dest_path)\n\n        yield dest_path\n\n    def test_get_colorspace_settings(self, context, config_path_asset):\n        expected_config_template = (\n            \"{root[work]}/{project[name]}/config/aces.ocio\"\n        )\n        expected_file_rules = {\n            \"comp_review\": {\n                \"pattern\": \"renderCompMain.baking_h264\",\n                \"colorspace\": \"Camera Rec.709\",\n                \"ext\": \"mp4\"\n            }\n        }\n\n        # load plugin function for testing\n        plugin = publish_plugins.ColormanagedPyblishPluginMixin()\n        plugin.log = log\n        config_data, file_rules = plugin.get_colorspace_settings(context)\n\n        assert config_data[\"template\"] == expected_config_template, (\n            \"Returned config template is not \"\n            f\"matching {expected_config_template}\"\n        )\n        assert file_rules == expected_file_rules, (\n            \"Returned file rules are not \"\n            f\"matching {expected_file_rules}\"\n        )\n\n    def test_set_representation_colorspace(\n        self, context, project_settings,\n        config_path_project, config_path_asset\n    ):\n        expected_colorspace_hiero = \"sRGB - Texture\"\n        expected_colorspace_nuke = \"Camera Rec.709\"\n\n        config_data_nuke = colorspace.get_imageio_config(\n            \"test_project\", \"nuke\", project_settings)\n        file_rules_nuke = colorspace.get_imageio_file_rules(\n            \"test_project\", \"nuke\", project_settings)\n\n        config_data_hiero = colorspace.get_imageio_config(\n            \"test_project\", \"hiero\", project_settings)\n        file_rules_hiero = colorspace.get_imageio_file_rules(\n            \"test_project\", \"hiero\", project_settings)\n\n        representation_nuke = {\n            \"ext\": \"mp4\",\n            \"files\": \"this_file_renderCompMain.baking_h264.mp4\"\n        }\n        representation_hiero = {\n            \"ext\": \"mp4\",\n            \"files\": \"this_file_renderCompMain_h264burninburnin.mp4\"\n        }\n\n        # load plugin function for testing\n        plugin = publish_plugins.ColormanagedPyblishPluginMixin()\n        plugin.log = log\n        context.data[\"imageioSettings\"] = (config_data_nuke, file_rules_nuke)\n        plugin.set_representation_colorspace(\n            representation_nuke, context\n        )\n        # load plugin function for testing\n        plugin = publish_plugins.ColormanagedPyblishPluginMixin()\n        plugin.log = log\n        context.data[\"imageioSettings\"] = (config_data_hiero, file_rules_hiero)\n        plugin.set_representation_colorspace(\n            representation_hiero, context\n        )\n\n        colorspace_data_nuke = representation_nuke.get(\"colorspaceData\")\n        colorspace_data_hiero = representation_hiero.get(\"colorspaceData\")\n\n        assert colorspace_data_nuke, (\n            \"Colorspace data were not created in representation\"\n            f\"matching {representation_nuke}\"\n        )\n        assert colorspace_data_hiero, (\n            \"Colorspace data were not created in representation\"\n            f\"matching {representation_hiero}\"\n        )\n\n        ret_colorspace_nuke = colorspace_data_nuke[\"colorspace\"]\n        assert ret_colorspace_nuke == expected_colorspace_nuke, (\n            \"Returned colorspace is not \"\n            f\"matching {expected_colorspace_nuke}\"\n        )\n        ret_colorspace_hiero = colorspace_data_hiero[\"colorspace\"]\n        assert ret_colorspace_hiero == expected_colorspace_hiero, (\n            \"Returned colorspace is not \"\n            f\"matching {expected_colorspace_hiero}\"\n        )\n\n\ntest_case = TestPipelinePublishPlugins()\n"
  },
  {
    "path": "tests/unit/openpype/pipeline/test_colorspace.py",
    "content": "\n\n\"\"\"Test Colorspace pipeline modul, tests API methods\n\n    File:\n        creates temporary directory and downloads .zip file from GDrive\n        unzips .zip file\n        uses content of .zip file (MongoDB's dumps) to import to new databases\n        with use of 'monkeypatch_session' modifies required env vars\n            temporarily\n        runs battery of tests checking that site operation for Sync Server\n            module are working\n        removes temporary folder\n        removes temporary databases (?)\n\"\"\"\nimport pytest\nimport shutil\nimport os\n\nfrom tests.unit.openpype.pipeline.lib import TestPipeline\nfrom openpype.pipeline import colorspace\n\n\nclass TestPipelineColorspace(TestPipeline):\n    \"\"\" Testing Colorspace\n\n    Example:\n        cd to OpenPype repo root dir\n        poetry run python ./start.py runtests <openpype_root>/tests/unit/openpype/pipeline/test_colorspace.py\n    \"\"\"  # noqa: E501\n\n    TEST_FILES = [\n        (\n            \"1csqimz8bbNcNgxtEXklLz6GRv91D3KgA\",\n            \"test_pipeline_colorspace.zip\",\n            \"\"\n        )\n    ]\n\n    PROJECT = \"test_project\"\n    ASSET = \"test_asset\"\n    TASK = \"test_task\"\n\n    @pytest.fixture(scope=\"module\")\n    def config_path_project(\n        self,\n        download_test_data,\n        output_folder_url\n    ):\n        src_path = os.path.join(\n            download_test_data,\n            \"input\",\n            \"data\",\n            \"configs\",\n            \"aces_1.3\",\n            \"ayon_aces_config_project.ocio\"\n        )\n        dest_dir = os.path.join(\n            output_folder_url,\n            self.PROJECT,\n            \"config\"\n        )\n        dest_path = os.path.join(\n            dest_dir,\n            \"aces.ocio\"\n        )\n        if not os.path.exists(dest_dir):\n            os.makedirs(dest_dir)\n\n        shutil.copyfile(src_path, dest_path)\n\n        yield dest_path\n\n    @pytest.fixture(scope=\"module\")\n    def config_path_asset(\n        self,\n        download_test_data,\n        output_folder_url\n    ):\n        src_path = os.path.join(\n            download_test_data,\n            \"input\",\n            \"data\",\n            \"configs\",\n            \"aces_1.3\",\n            \"ayon_aces_config_asset.ocio\"\n        )\n        dest_dir = os.path.join(\n            output_folder_url,\n            self.PROJECT,\n            self.ASSET,\n            \"config\"\n        )\n        dest_path = os.path.join(\n            dest_dir,\n            \"aces.ocio\"\n        )\n        if not os.path.exists(dest_dir):\n            os.makedirs(dest_dir)\n\n        shutil.copyfile(src_path, dest_path)\n\n        yield dest_path\n\n    def test_config_file_project(\n        self,\n        legacy_io,\n        config_path_project,\n        project_settings\n    ):\n        expected_template = \"{root[work]}/{project[name]}/config/aces.ocio\"\n\n        # get config_data from hiero\n        # where project level config is defined\n        config_data = colorspace.get_imageio_config(\n            \"test_project\", \"hiero\", project_settings)\n\n        assert os.path.exists(config_data[\"path\"]), (\n            f\"Config file \\'{config_data['path']}\\' doesn't exist\"\n        )\n        assert config_data[\"template\"] == expected_template, (\n            f\"Config template \\'{config_data['template']}\\' doesn't match \"\n            f\"expected template \\'{expected_template}\\'\"\n        )\n\n    def test_parse_colorspace_from_filepath(\n        self,\n        legacy_io,\n        config_path_asset,\n        project_settings\n    ):\n        path_1 = \"renderCompMain_ACES2065-1.####.exr\"\n        expected_1 = \"ACES2065-1\"\n        ret_1 = colorspace.parse_colorspace_from_filepath(\n            path_1, config_path=config_path_asset\n        )\n        assert ret_1 == expected_1, f\"Not matching colorspace {expected_1}\"\n\n        path_2 = \"renderCompMain_BMDFilm_WideGamut_Gen5.mov\"\n        expected_2 = \"BMDFilm WideGamut Gen5\"\n        ret_2 = colorspace.parse_colorspace_from_filepath(\n            path_2, config_path=config_path_asset\n        )\n        assert ret_2 == expected_2, f\"Not matching colorspace {expected_2}\"\n\n    def test_get_ocio_config_views_asset(self, config_path_asset):\n        expected_num_keys = 12\n\n        ret = colorspace.get_ocio_config_views(config_path_asset)\n\n        assert len(ret) == expected_num_keys, (\n            f\"Not matching num viewer keys {expected_num_keys}\")\n\n    def test_get_ocio_config_views_project(self, config_path_project):\n        expected_num_keys = 3\n\n        ret = colorspace.get_ocio_config_views(config_path_project)\n\n        assert len(ret) == expected_num_keys, (\n            f\"Not matching num viewer keys {expected_num_keys}\")\n\n    def test_file_rules(self, project_settings):\n        expected_nuke = {\n            \"comp_review\": {\n                \"pattern\": \"renderCompMain.baking_h264\",\n                \"colorspace\": \"Camera Rec.709\",\n                \"ext\": \"mp4\"\n            }\n        }\n        expected_hiero = {\n            \"comp_review\": {\n                \"pattern\": \"renderCompMain_h264burninburnin\",\n                \"colorspace\": \"sRGB - Texture\",\n                \"ext\": \"mp4\"\n            }\n        }\n\n        nuke_file_rules = colorspace.get_imageio_file_rules(\n            \"test_project\", \"nuke\", project_settings=project_settings)\n        assert expected_nuke == nuke_file_rules, (\n            f\"Not matching file rules {expected_nuke}\")\n\n        hiero_file_rules = colorspace.get_imageio_file_rules(\n            \"test_project\", \"hiero\", project_settings=project_settings)\n        assert expected_hiero == hiero_file_rules, (\n            f\"Not matching file rules {expected_hiero}\")\n\n    def test_get_imageio_colorspace_from_filepath_p3(self, project_settings):\n        \"\"\"Test Colorspace from filepath with python 3 compatibility mode\n\n        Also test ocio v2 file rules\n        \"\"\"\n        nuke_filepath = \"renderCompMain_baking_h264.mp4\"\n        hiero_filepath = \"prerenderCompMain.mp4\"\n\n        expected_nuke = \"Camera Rec.709\"\n        expected_hiero = \"Gamma 2.2 Rec.709 - Texture\"\n\n        nuke_colorspace = colorspace.get_colorspace_name_from_filepath(\n            nuke_filepath,\n            \"nuke\",\n            \"test_project\",\n            project_settings=project_settings\n        )\n        assert expected_nuke == nuke_colorspace, (\n            f\"Not matching colorspace {expected_nuke}\")\n\n        hiero_colorspace = colorspace.get_colorspace_name_from_filepath(\n            hiero_filepath,\n            \"hiero\",\n            \"test_project\",\n            project_settings=project_settings\n        )\n        assert expected_hiero == hiero_colorspace, (\n            f\"Not matching colorspace {expected_hiero}\")\n\n    def test_get_imageio_colorspace_from_filepath_python2mode(\n            self, project_settings):\n        \"\"\"Test Colorspace from filepath with python 2 compatibility mode\n\n        Also test ocio v2 file rules\n        \"\"\"\n        nuke_filepath = \"renderCompMain_baking_h264.mp4\"\n        hiero_filepath = \"prerenderCompMain.mp4\"\n\n        expected_nuke = \"Camera Rec.709\"\n        expected_hiero = \"Gamma 2.2 Rec.709 - Texture\"\n\n        # switch to python 2 compatibility mode\n        colorspace.CachedData.has_compatible_ocio_package = False\n\n        nuke_colorspace = colorspace.get_colorspace_name_from_filepath(\n            nuke_filepath,\n            \"nuke\",\n            \"test_project\",\n            project_settings=project_settings\n        )\n        assert expected_nuke == nuke_colorspace, (\n            f\"Not matching colorspace {expected_nuke}\")\n\n        hiero_colorspace = colorspace.get_colorspace_name_from_filepath(\n            hiero_filepath,\n            \"hiero\",\n            \"test_project\",\n            project_settings=project_settings\n        )\n        assert expected_hiero == hiero_colorspace, (\n            f\"Not matching colorspace {expected_hiero}\")\n\n        # return to python 3 compatibility mode\n        colorspace.CachedData.python3compatible = None\n\n\ntest_case = TestPipelineColorspace()\n"
  },
  {
    "path": "tests/unit/openpype/pipeline/test_colorspace_convert_colorspace_enumerator_item.py",
    "content": "import unittest\nfrom openpype.pipeline.colorspace import convert_colorspace_enumerator_item\n\n\nclass TestConvertColorspaceEnumeratorItem(unittest.TestCase):\n    def setUp(self):\n        self.config_items = {\n            \"colorspaces\": {\n                \"sRGB\": {\n                    \"aliases\": [\"sRGB_1\"],\n                    \"family\": \"colorspace\",\n                    \"categories\": [\"colors\"],\n                    \"equalitygroup\": \"equalitygroup\",\n                },\n                \"Rec.709\": {\n                    \"aliases\": [\"rec709_1\", \"rec709_2\"],\n                },\n            },\n            \"looks\": {\n                \"sRGB_to_Rec.709\": {\n                    \"process_space\": \"sRGB\",\n                },\n            },\n            \"displays_views\": {\n                \"sRGB (ACES)\": {\n                    \"view\": \"sRGB\",\n                    \"display\": \"ACES\",\n                },\n                \"Rec.709 (ACES)\": {\n                    \"view\": \"Rec.709\",\n                    \"display\": \"ACES\",\n                },\n            },\n            \"roles\": {\n                \"compositing_linear\": {\n                    \"colorspace\": \"linear\",\n                },\n            },\n        }\n\n    def test_valid_item(self):\n        colorspace_item_data = convert_colorspace_enumerator_item(\n            \"colorspaces::sRGB\", self.config_items)\n        self.assertEqual(\n            colorspace_item_data,\n            {\n                \"name\": \"sRGB\",\n                \"type\": \"colorspaces\",\n                \"aliases\": [\"sRGB_1\"],\n                \"family\": \"colorspace\",\n                \"categories\": [\"colors\"],\n                \"equalitygroup\": \"equalitygroup\"\n            }\n        )\n\n        alias_item_data = convert_colorspace_enumerator_item(\n            \"aliases::rec709_1\", self.config_items)\n        self.assertEqual(\n            alias_item_data,\n            {\n                \"aliases\": [\"rec709_1\", \"rec709_2\"],\n                \"name\": \"Rec.709\",\n                \"type\": \"colorspace\"\n            }\n        )\n\n        display_view_item_data = convert_colorspace_enumerator_item(\n            \"displays_views::sRGB (ACES)\", self.config_items)\n        self.assertEqual(\n            display_view_item_data,\n            {\n                \"type\": \"displays_views\",\n                \"name\": \"sRGB (ACES)\",\n                \"view\": \"sRGB\",\n                \"display\": \"ACES\"\n            }\n        )\n\n        role_item_data = convert_colorspace_enumerator_item(\n            \"roles::compositing_linear\", self.config_items)\n        self.assertEqual(\n            role_item_data,\n            {\n                \"name\": \"compositing_linear\",\n                \"type\": \"roles\",\n                \"colorspace\": \"linear\"\n            }\n        )\n\n        look_item_data = convert_colorspace_enumerator_item(\n            \"looks::sRGB_to_Rec.709\", self.config_items)\n        self.assertEqual(\n            look_item_data,\n            {\n                \"type\": \"looks\",\n                \"name\": \"sRGB_to_Rec.709\",\n                \"process_space\": \"sRGB\"\n            }\n        )\n\n    def test_invalid_item(self):\n        config_items = {\n            \"RGB\": {\n                \"sRGB\": {\"red\": 255, \"green\": 255, \"blue\": 255},\n                \"AdobeRGB\": {\"red\": 255, \"green\": 255, \"blue\": 255},\n            }\n        }\n        with self.assertRaises(KeyError):\n            convert_colorspace_enumerator_item(\"RGB::invalid\", config_items)\n\n    def test_missing_config_data(self):\n        config_items = {}\n        with self.assertRaises(KeyError):\n            convert_colorspace_enumerator_item(\"RGB::sRGB\", config_items)\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/unit/openpype/pipeline/test_colorspace_get_colorspaces_enumerator_items.py",
    "content": "import unittest\n\nfrom openpype.pipeline.colorspace import get_colorspaces_enumerator_items\n\n\nclass TestGetColorspacesEnumeratorItems(unittest.TestCase):\n    def setUp(self):\n        self.config_items = {\n            \"colorspaces\": {\n                \"sRGB\": {\n                    \"aliases\": [\"sRGB_1\"],\n                },\n                \"Rec.709\": {\n                    \"aliases\": [\"rec709_1\", \"rec709_2\"],\n                },\n            },\n            \"looks\": {\n                \"sRGB_to_Rec.709\": {\n                    \"process_space\": \"sRGB\",\n                },\n            },\n            \"displays_views\": {\n                \"sRGB (ACES)\": {\n                    \"view\": \"sRGB\",\n                    \"display\": \"ACES\",\n                },\n                \"Rec.709 (ACES)\": {\n                    \"view\": \"Rec.709\",\n                    \"display\": \"ACES\",\n                },\n            },\n            \"roles\": {\n                \"compositing_linear\": {\n                    \"colorspace\": \"linear\",\n                },\n            },\n        }\n\n    def test_colorspaces(self):\n        result = get_colorspaces_enumerator_items(self.config_items)\n        expected = [\n            (\"colorspaces::Rec.709\", \"[colorspace] Rec.709\"),\n            (\"colorspaces::sRGB\", \"[colorspace] sRGB\"),\n        ]\n        self.assertEqual(result, expected)\n\n    def test_aliases(self):\n        result = get_colorspaces_enumerator_items(\n            self.config_items, include_aliases=True)\n        expected = [\n            (\"colorspaces::Rec.709\", \"[colorspace] Rec.709\"),\n            (\"colorspaces::sRGB\", \"[colorspace] sRGB\"),\n            (\"aliases::rec709_1\", \"[alias] rec709_1 (Rec.709)\"),\n            (\"aliases::rec709_2\", \"[alias] rec709_2 (Rec.709)\"),\n            (\"aliases::sRGB_1\", \"[alias] sRGB_1 (sRGB)\"),\n        ]\n        self.assertEqual(result, expected)\n\n    def test_looks(self):\n        result = get_colorspaces_enumerator_items(\n            self.config_items, include_looks=True)\n        expected = [\n            (\"colorspaces::Rec.709\", \"[colorspace] Rec.709\"),\n            (\"colorspaces::sRGB\", \"[colorspace] sRGB\"),\n            (\"looks::sRGB_to_Rec.709\", \"[look] sRGB_to_Rec.709 (sRGB)\"),\n        ]\n        self.assertEqual(result, expected)\n\n    def test_display_views(self):\n        result = get_colorspaces_enumerator_items(\n            self.config_items, include_display_views=True)\n        expected = [\n            (\"colorspaces::Rec.709\", \"[colorspace] Rec.709\"),\n            (\"colorspaces::sRGB\", \"[colorspace] sRGB\"),\n            (\"displays_views::Rec.709 (ACES)\", \"[view (display)] Rec.709 (ACES)\"),  # noqa: E501\n            (\"displays_views::sRGB (ACES)\", \"[view (display)] sRGB (ACES)\"),\n\n        ]\n        self.assertEqual(result, expected)\n\n    def test_roles(self):\n        result = get_colorspaces_enumerator_items(\n            self.config_items, include_roles=True)\n        expected = [\n            (\"roles::compositing_linear\", \"[role] compositing_linear (linear)\"),  # noqa: E501\n            (\"colorspaces::Rec.709\", \"[colorspace] Rec.709\"),\n            (\"colorspaces::sRGB\", \"[colorspace] sRGB\"),\n        ]\n        self.assertEqual(result, expected)\n\n    def test_all(self):\n        message_config_keys = \", \".join(\n            \"'{}':{}\".format(\n                key,\n                set(self.config_items.get(key, {}).keys())\n            ) for key in self.config_items.keys()\n        )\n        print(\"Testing with config: [{}]\".format(message_config_keys))\n        result = get_colorspaces_enumerator_items(\n            self.config_items,\n            include_aliases=True,\n            include_looks=True,\n            include_roles=True,\n            include_display_views=True,\n        )\n        expected = [\n            (\"roles::compositing_linear\", \"[role] compositing_linear (linear)\"),  # noqa: E501\n            (\"colorspaces::Rec.709\", \"[colorspace] Rec.709\"),\n            (\"colorspaces::sRGB\", \"[colorspace] sRGB\"),\n            (\"aliases::rec709_1\", \"[alias] rec709_1 (Rec.709)\"),\n            (\"aliases::rec709_2\", \"[alias] rec709_2 (Rec.709)\"),\n            (\"aliases::sRGB_1\", \"[alias] sRGB_1 (sRGB)\"),\n            (\"looks::sRGB_to_Rec.709\", \"[look] sRGB_to_Rec.709 (sRGB)\"),\n            (\"displays_views::Rec.709 (ACES)\", \"[view (display)] Rec.709 (ACES)\"),  # noqa: E501\n            (\"displays_views::sRGB (ACES)\", \"[view (display)] sRGB (ACES)\"),\n        ]\n        self.assertEqual(result, expected)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/unit/openpype/plugins/publish/test_extract_review.py",
    "content": "from openpype.plugins.publish.extract_review import ExtractReview\n\n\ndef test_fix_ffmpeg_full_args_filters():\n    \"\"\"Tests because of wrong resolution of audio filters.\"\"\"\n    plugin = ExtractReview()\n    output_arg = \"c:/test-afbdc\"\n    ret = plugin.ffmpeg_full_args([], [], [], [output_arg])\n    assert len(ret) == 2, \"Parsed wrong\"\n    assert ret[-1] == output_arg\n\n    ret = plugin.ffmpeg_full_args([], [], [\"adeclick\"], [output_arg])\n    assert len(ret) == 4, \"Parsed wrong\"\n    assert ret[-1] == output_arg\n    assert ret[-2] == '\"adeclick\"'\n    assert ret[-3] == \"-filter:a\"\n\n    ret = plugin.ffmpeg_full_args([], [], [], [output_arg, \"-af adeclick\"])\n    assert len(ret) == 4, \"Parsed wrong\"\n    assert ret[-1] == output_arg\n    assert ret[-2] == '\"adeclick\"'\n    assert ret[-3] == \"-filter:a\"\n\n    ret = plugin.ffmpeg_full_args([], [], [\"adeclick\"],\n                                  [output_arg, \"-af adeclick\"])\n    assert len(ret) == 4, \"Parsed wrong\"\n    assert ret[-1] == output_arg\n    assert ret[-2] == '\"adeclick,adeclick\"'  # TODO fix this duplication\n    assert ret[-3] == \"-filter:a\"\n"
  },
  {
    "path": "tools/build.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script to build OpenPype.\n\n.DESCRIPTION\n  This script will detect Python installation, and build OpenPype to `build`\n  directory using existing virtual environment created by Poetry (or\n  by running `/tools/create_venv.ps1`). It will then shuffle dependencies in\n  build folder to optimize for different Python versions (2/3) in Python host.\n\n.EXAMPLE\n\nPS> .\\build.ps1\n\n.EXAMPLE\n\nTo build without automatical submodule update:\nPS> .\\build.ps1 --no-submodule-update\n\n.LINK\nhttps://openpype.io/docs\n\n#>\n\n$arguments=$ARGS\n$disable_submodule_update=\"\"\nif($arguments -eq \"--no-submodule-update\") {\n    $disable_submodule_update=$true\n}\n\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n# Install PSWriteColor to support colorized output to terminal\n$env:PSModulePath = $env:PSModulePath + \";$($openpype_root)\\tools\\modules\\powershell\"\n\nfunction Start-Progress {\n    param([ScriptBlock]$code)\n    $scroll = \"/-\\|/-\\|\"\n    $idx = 0\n    $job = Invoke-Command -ComputerName $env:ComputerName -ScriptBlock { $code } -AsJob\n\n    $origpos = $host.UI.RawUI.CursorPosition\n\n    # $origpos.Y -= 1\n\n    while (($job.State -eq \"Running\") -and ($job.State -ne \"NotStarted\"))\n    {\n        $host.UI.RawUI.CursorPosition = $origpos\n        Write-Host $scroll[$idx] -NoNewline\n        $idx++\n        if ($idx -ge $scroll.Length)\n        {\n            $idx = 0\n        }\n        Start-Sleep -Milliseconds 100\n    }\n    # It's over - clear the activity indicator.\n    $host.UI.RawUI.CursorPosition = $origpos\n    Write-Host ' '\n  <#\n  .SYNOPSIS\n  Display spinner for running job\n  .PARAMETER code\n  Job to display spinner for\n  #>\n}\n\n\nfunction Exit-WithCode($exitcode) {\n   # Only exit this host process if it's a child of another PowerShell parent process...\n   $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$PID\" | Select-Object -Property ParentProcessId).ParentProcessId\n   $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$parentPID\" | Select-Object -Property Name).Name\n   if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) }\n\n   exit $exitcode\n}\n\nfunction Show-PSWarning() {\n    if ($PSVersionTable.PSVersion.Major -lt 7) {\n        Write-Color -Text \"!!! \", \"You are using old version of PowerShell - \",  \"$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)\" -Color Red, Yellow, White\n        Write-Color -Text \"    Please update to at least 7.0 - \", \"https://github.com/PowerShell/PowerShell/releases\" -Color Yellow, White\n        Exit-WithCode 1\n    }\n}\n\nfunction Install-Poetry() {\n    Write-Color -Text \">>> \", \"Installing Poetry ... \" -Color Green, Gray\n    $env:POETRY_HOME=\"$openpype_root\\.poetry\"\n    (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | python -\n}\n\n$art = @\"\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~.   ..   ~2p.  ..  ....  .  .\n    .Ppo . .pPO3Op.. . O:. . . .\n   .3Pp . oP3'. 'P33. . 4 ..   .  .   . .. .  .  .\n  .~OP    3PO.  .Op3    : . ..  _____  _____  _____\n  .P3O  . oP3oP3O3P' . . .   . /    /./    /./    /\n   O3:.   O3p~ .       .:. . ./____/./____/ /____/\n   'P .   3p3.  oP3~. ..P:. .  . ..  .   . .. .  .  .\n  . ':  . Po'  .Opo'. .3O. .  o[ by Pype Club ]]]==- - - .  .\n    . '_ ..  .    . _OP3..  .  .https://openpype.io.. .\n         ~P3.OPPPO3OP~ . ..  .\n           .  ' '. .  .. . . . ..  .\n\n\"@\n\nWrite-Host $art -ForegroundColor DarkGreen\n\n# Enable if PS 7.x is needed.\n# Show-PSWarning\n\n$env:_INSIDE_OPENPYPE_TOOL = \"1\"\n\nif (-not (Test-Path 'env:POETRY_HOME')) {\n    $env:POETRY_HOME = \"$openpype_root\\.poetry\"\n}\n\nSet-Location -Path $openpype_root\n\n$version_file = Get-Content -Path \"$($openpype_root)\\openpype\\version.py\"\n$result = [regex]::Matches($version_file, '__version__ = \"(?<version>\\d+\\.\\d+.\\d+.*)\"')\n$openpype_version = $result[0].Groups['version'].Value\nif (-not $openpype_version) {\n  Write-Color -Text \"!!! \", \"Cannot determine OpenPype version.\" -Color Yellow, Gray\n  Exit-WithCode 1\n}\n\n# Create build directory if not exist\nif (-not (Test-Path -PathType Container -Path \"$($openpype_root)\\build\")) {\n    New-Item -ItemType Directory -Force -Path \"$($openpype_root)\\build\"\n}\n\nWrite-Color -Text \"--- \", \"Cleaning build directory ...\" -Color Yellow, Gray\ntry {\n    Remove-Item -Recurse -Force \"$($openpype_root)\\build\\*\"\n}\ncatch {\n    Write-Color -Text \"!!! \", \"Cannot clean build directory, possibly because process is using it.\" -Color Red, Gray\n    Write-Color -Text $_.Exception.Message -Color Red\n    Exit-WithCode 1\n}\nif (-not $disable_submodule_update) {\n    Write-Color -Text \">>> \", \"Making sure submodules are up-to-date ...\" -Color Green, Gray\n    & git submodule update --init --recursive\n} else {\n    Write-Color -Text \"*** \", \"Not updating submodules ...\" -Color Green, Gray\n}\n\nWrite-Color -Text \">>> \", \"OpenPype [ \", $openpype_version, \" ]\" -Color Green, White, Cyan, White\n\nWrite-Color -Text \">>> \", \"Reading Poetry ... \" -Color Green, Gray -NoNewline\nif (-not (Test-Path -PathType Container -Path \"$($env:POETRY_HOME)\\bin\")) {\n    Write-Color -Text \"NOT FOUND\" -Color Yellow\n    Write-Color -Text \"*** \", \"We need to install Poetry create virtual env first ...\" -Color Yellow, Gray\n    & \"$openpype_root\\tools\\create_env.ps1\"\n} else {\n    Write-Color -Text \"OK\" -Color Green\n}\n\nWrite-Color -Text \">>> \", \"Cleaning cache files ... \" -Color Green, Gray -NoNewline\nGet-ChildItem $openpype_root -Filter \"*.pyc\" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force\nGet-ChildItem $openpype_root -Filter \"*.pyo\" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force\nGet-ChildItem $openpype_root -Filter \"__pycache__\" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force -Recurse\nWrite-Color -Text \"OK\" -Color green\n\nWrite-Color -Text \">>> \", \"Building OpenPype ...\" -Color Green, White\n$startTime = [int][double]::Parse((Get-Date -UFormat %s))\n\n$out = &  \"$($env:POETRY_HOME)\\bin\\poetry\" run python setup.py build 2>&1\nSet-Content -Path \"$($openpype_root)\\build\\build.log\" -Value $out\nif ($LASTEXITCODE -ne 0)\n{\n    Write-Color -Text \"------------------------------------------\" -Color Red\n    Get-Content \"$($openpype_root)\\build\\build.log\"\n    Write-Color -Text \"------------------------------------------\" -Color Yellow\n    Write-Color -Text \"!!! \", \"Build failed. Check the log: \", \".\\build\\build.log\" -Color Red, Yellow, White\n    Exit-WithCode $LASTEXITCODE\n}\n\nSet-Content -Path \"$($openpype_root)\\build\\build.log\" -Value $out\n& \"$($env:POETRY_HOME)\\bin\\poetry\" run python \"$($openpype_root)\\tools\\build_dependencies.py\"\n\nWrite-Color -Text \">>> \", \"Restoring current directory\" -Color Green, Gray\nSet-Location -Path $current_dir\n\n$endTime = [int][double]::Parse((Get-Date -UFormat %s))\ntry\n{\n    New-BurntToastNotification -AppLogo \"$openpype_root/openpype/resources/icons/openpype_icon.png\" -Text \"OpenPype build complete!\", \"All done in $( $endTime - $startTime ) secs. You will find OpenPype and build log in build directory.\"\n} catch {}\nWrite-Color -Text \"*** \", \"All done in \", $($endTime - $startTime), \" secs. You will find OpenPype and build log in \", \"'.\\build'\", \" directory.\" -Color Green, Gray, White, Gray, White, Gray\n"
  },
  {
    "path": "tools/build.sh",
    "content": "#!/usr/bin/env bash\n\n# Build Pype using existing virtual environment.\n\n\nart () {\n  cat <<-EOF\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~·   ··   ~2p.  ··  ····  ·  ·\n    ·Ppo · .pPO3Op.· · O:· · · ·\n   .3Pp · oP3'· 'P33· · 4 ··   ·  ·   · ·· ·  ·  ·\n  ·~OP    3PO·  .Op3    : · ··  _____  _____  _____\n  ·P3O  · oP3oP3O3P' · · ·   · /    /·/    /·/    /\n   O3:·   O3p~ ·       ·:· · ·/____/·/____/ /____/\n   'P ·   3p3·  oP3~· ·.P:· ·  · ··  ·   · ·· ·  ·  ·\n  · ':  · Po'  ·Opo'· .3O· .  o[ by Pype Club ]]]==- - - ·  ·\n    · '_ ..  ·    . _OP3··  ·  ·https://openpype.io·· ·\n         ~P3·OPPPO3OP~ · ··  ·\n           ·  ' '· ·  ·· · · · ··  ·\n\nEOF\n}\n\n# Colors for terminal\n\nRST='\\033[0m'             # Text Reset\n\n# Regular Colors\nBlack='\\033[0;30m'        # Black\nRed='\\033[0;31m'          # Red\nGreen='\\033[0;32m'        # Green\nYellow='\\033[0;33m'       # Yellow\nBlue='\\033[0;34m'         # Blue\nPurple='\\033[0;35m'       # Purple\nCyan='\\033[0;36m'         # Cyan\nWhite='\\033[0;37m'        # White\n\n# Bold\nBBlack='\\033[1;30m'       # Black\nBRed='\\033[1;31m'         # Red\nBGreen='\\033[1;32m'       # Green\nBYellow='\\033[1;33m'      # Yellow\nBBlue='\\033[1;34m'        # Blue\nBPurple='\\033[1;35m'      # Purple\nBCyan='\\033[1;36m'        # Cyan\nBWhite='\\033[1;37m'       # White\n\n# Bold High Intensity\nBIBlack='\\033[1;90m'      # Black\nBIRed='\\033[1;91m'        # Red\nBIGreen='\\033[1;92m'      # Green\nBIYellow='\\033[1;93m'     # Yellow\nBIBlue='\\033[1;94m'       # Blue\nBIPurple='\\033[1;95m'     # Purple\nBICyan='\\033[1;96m'       # Cyan\nBIWhite='\\033[1;97m'      # White\n\nargs=$@\ndisable_submodule_update=0\nwhile :; do\n  case $1 in\n    --no-submodule-update)\n      disable_submodule_update=1\n      ;;\n    --)\n      shift\n      break\n      ;;\n    *)\n      break\n  esac\n\n  shift\ndone\n\n\n\n\n##############################################################################\n# Detect required version of python\n# Globals:\n#   colors\n#   PYTHON\n# Arguments:\n#   None\n# Returns:\n#   None\n###############################################################################\ndetect_python () {\n  echo -e \"${BIGreen}>>>${RST} Using python \\c\"\n  command -v python >/dev/null 2>&1 || { echo -e \"${BIRed}- NOT FOUND${RST} ${BIYellow}You need Python 3.9 installed to continue.${RST}\"; return 1; }\n  local version_command\n  version_command=\"import sys;print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))\"\n  local python_version\n  python_version=\"$(python <<< ${version_command})\"\n  oIFS=\"$IFS\"\n  IFS=.\n  set -- $python_version\n  IFS=\"$oIFS\"\n  if [ \"$1\" -ge \"3\" ] && [ \"$2\" -ge \"9\" ] ; then\n    if [ \"$2\" -gt \"9\" ] ; then\n      echo -e \"${BIWhite}[${RST} ${BIRed}$1.$2 ${BIWhite}]${RST} - ${BIRed}FAILED${RST} ${BIYellow}Version is new and unsupported, use${RST} ${BIPurple}3.9.x${RST}\"; return 1;\n    else\n      echo -e \"${BIWhite}[${RST} ${BIGreen}$1.$2${RST} ${BIWhite}]${RST}\"\n    fi\n  else\n    command -v python >/dev/null 2>&1 || { echo -e \"${BIRed}$1.$2$ - ${BIRed}FAILED${RST} ${BIYellow}Version is old and unsupported${RST}\"; return 1; }\n  fi\n}\n\n##############################################################################\n# Clean pyc files in specified directory\n# Globals:\n#   None\n# Arguments:\n#   Optional path to clean\n# Returns:\n#   None\n###############################################################################\nclean_pyc () {\n  local path\n  path=$openpype_root\n  echo -e \"${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \\c\"\n  find \"$path\" -path ./build -o -regex '^.*\\(__pycache__\\|\\.py[co]\\)$' -delete\n\n  echo -e \"${BIGreen}DONE${RST}\"\n}\n\n##############################################################################\n# Return absolute path\n# Globals:\n#   None\n# Arguments:\n#   Path to resolve\n# Returns:\n#   None\n###############################################################################\nrealpath () {\n  echo $(cd $(dirname \"$1\") || return; pwd)/$(basename \"$1\")\n}\n\n# Main\nmain () {\n  echo -e \"${BGreen}\"\n  art\n  echo -e \"${RST}\"\n  detect_python || return 1\n\n  # Directories\n  openpype_root=$(dirname $(dirname \"$(realpath ${BASH_SOURCE[0]})\"))\n  pushd \"$openpype_root\" > /dev/null || return > /dev/null\n\n  version_command=\"import os;import re;version={};exec(open(os.path.join('$openpype_root', 'openpype', 'version.py')).read(), version);print(re.search(r'(\\d+\\.\\d+.\\d+).*', version['__version__'])[1]);\"\n  openpype_version=\"$(python <<< ${version_command})\"\n\n  _inside_openpype_tool=\"1\"\n\n  if [[ -z $POETRY_HOME ]]; then\n    export POETRY_HOME=\"$openpype_root/.poetry\"\n  fi\n\n  echo -e \"${BIYellow}---${RST} Cleaning build directory ...\"\n  rm -rf \"$openpype_root/build\" && mkdir \"$openpype_root/build\" > /dev/null\n\n  echo -e \"${BIGreen}>>>${RST} Building OpenPype ${BIWhite}[${RST} ${BIGreen}$openpype_version${RST} ${BIWhite}]${RST}\"\n  echo -e \"${BIGreen}>>>${RST} Cleaning cache files ...\"\n  clean_pyc\n\n  echo -e \"${BIGreen}>>>${RST} Reading Poetry ... \\c\"\n  if [ -f \"$POETRY_HOME/bin/poetry\" ]; then\n    echo -e \"${BIGreen}OK${RST}\"\n  else\n    echo -e \"${BIYellow}NOT FOUND${RST}\"\n    echo -e \"${BIYellow}***${RST} We need to install Poetry and virtual env ...\"\n    . \"$openpype_root/tools/create_env.sh\" || { echo -e \"${BIRed}!!!${RST} Poetry installation failed\"; return 1; }\n  fi\n\nif [ \"$disable_submodule_update\" == 1 ]; then\n    echo -e \"${BIYellow}***${RST} Not updating submodules ...\"\n  else\n    echo -e \"${BIGreen}>>>${RST} Making sure submodules are up-to-date ...\"\n    git submodule update --init --recursive || { echo -e \"${BIRed}!!!${RST} Poetry installation failed\"; return 1; }\n  fi\n  echo -e \"${BIGreen}>>>${RST} Building ...\"\n  if [[ \"$OSTYPE\" == \"linux-gnu\"* ]]; then\n    \"$POETRY_HOME/bin/poetry\" run python \"$openpype_root/setup.py\" build &> \"$openpype_root/build/build.log\" || { echo -e \"${BIRed}------------------------------------------${RST}\"; cat \"$openpype_root/build/build.log\"; echo -e \"${BIRed}------------------------------------------${RST}\"; echo -e \"${BIRed}!!!${RST} Build failed, see the build log.\"; return 1; }\n  elif [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n    \"$POETRY_HOME/bin/poetry\" run python \"$openpype_root/setup.py\" bdist_mac &> \"$openpype_root/build/build.log\" || { echo -e \"${BIRed}------------------------------------------${RST}\"; cat \"$openpype_root/build/build.log\"; echo -e \"${BIRed}------------------------------------------${RST}\"; echo -e \"${BIRed}!!!${RST} Build failed, see the build log.\"; return 1; }\n  fi\n  \"$POETRY_HOME/bin/poetry\" run python \"$openpype_root/tools/build_dependencies.py\" || { echo -e \"${BIRed}!!!>${RST} ${BIYellow}Failed to process dependencies${RST}\"; return 1; }\n\n  if [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n    # fix cx_Freeze libs issue\n    echo -e \"${BIGreen}>>>${RST} Fixing libs ...\"\n    mv \"$openpype_root/build/OpenPype $openpype_version.app/Contents/MacOS/dependencies/cx_Freeze\" \"$openpype_root/build/OpenPype $openpype_version.app/Contents/MacOS/lib/\"  || { echo -e \"${BIRed}!!!>${RST} ${BIYellow}Can't move cx_Freeze libs${RST}\"; return 1; }\n\n    # force hide icon from Dock\n    defaults write \"$openpype_root/build/OpenPype $openpype_version.app/Contents/Info\" LSUIElement 1\n\n    # fix code signing issue\n    echo -e \"${BIGreen}>>>${RST} Fixing code signatures ...\\c\"\n    codesign --remove-signature \"$openpype_root/build/OpenPype $openpype_version.app/Contents/MacOS/openpype_console\" || { echo -e \"${BIRed}FAILED${RST}\"; return 1; }\n    codesign --remove-signature \"$openpype_root/build/OpenPype $openpype_version.app/Contents/MacOS/openpype_gui\" || { echo -e \"${BIRed}FAILED${RST}\"; return 1; }\n    echo -e \"${BIGreen}DONE${RST}\"\n    if command -v create-dmg > /dev/null 2>&1; then\n      echo -e \"${BIGreen}>>>${RST} Creating dmg image ...\\c\"\n      create-dmg \\\n        --volname \"OpenPype $openpype_version Installer\" \\\n        --window-pos 200 120 \\\n        --window-size 600 300 \\\n        --app-drop-link 100 50 \\\n        \"$openpype_root/build/OpenPype-Installer-$openpype_version.dmg\" \\\n        \"$openpype_root/build/OpenPype $openpype_version.app\"\n\n      test $? -eq 0 || { echo -e \"${BIRed}FAILED${RST}\"; return 1; }\n      echo -e \"${BIGreen}DONE${RST}\"\n    else\n      echo -e \"${BIYellow}!!!${RST} ${BIWhite}create-dmg${RST} command is not available.\"\n    fi\n  fi\n\n  echo -e \"${BICyan}>>>${RST} All done. You will find OpenPype and build log in \\c\"\n  echo -e \"${BIWhite}$openpype_root/build${RST} directory.\"\n}\n\nreturn_code=0\nmain || return_code=$?\nexit $return_code\n"
  },
  {
    "path": "tools/build_dependencies.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Script to fix frozen dependencies.\n\nBecause Pype code needs to run under different versions of Python interpreter\n(yes, even Python 2) we need to include all dependencies as source code\nwithout Python's system stuff. Cx-freeze puts everything into lib and compile\nit as .pyc/.pyo files and that doesn't work for hosts like Maya 2020 with\ntheir own Python interpreter and libraries.\n\nThis script will take ``site-packages`` and copy them to built Pype under\n``dependencies`` directory. It will then compare stuff inside with ``lib``\nfolder in  frozen Pype, removing duplicities from there.\n\nThis must be executed after build finished and it is done by build PowerShell\nscript.\n\nNote: Speedcopy can be used for copying if server-side copy is important for\nspeed.\n\n\"\"\"\nimport os\nimport sys\nimport site\nfrom sysconfig import get_platform\nimport platform\nimport subprocess\nfrom pathlib import Path\nimport shutil\nimport blessed\nimport enlighten\nimport time\nimport re\n\n\nterm = blessed.Terminal()\nmanager = enlighten.get_manager()\n\n\ndef _print(msg: str, type: int = 0) -> None:\n    \"\"\"Print message to console.\n\n    Args:\n        msg (str): message to print\n        type (int): type of message (0 info, 1 error, 2 note)\n\n    \"\"\"\n    if type == 0:\n        header = term.aquamarine3(\">>> \")\n    elif type == 1:\n        header = term.orangered2(\"!!! \")\n    elif type == 2:\n        header = term.tan1(\"... \")\n    else:\n        header = term.darkolivegreen3(\"--- \")\n\n    print(f\"{header}{msg}\")\n\n\ndef count_folders(path: Path) -> int:\n    \"\"\"Recursively count items inside given Path.\n\n    Args:\n        path (Path): Path to count.\n\n    Returns:\n        int: number of items.\n\n    \"\"\"\n    cnt = 0\n    for child in path.iterdir():\n        if child.is_dir():\n            cnt += 1\n            cnt += count_folders(child)\n    return cnt\n\n\n_print(\"Starting dependency cleanup ...\")\nstart_time = time.time_ns()\n\n# path to venv site packages\nsites = site.getsitepackages()\n\n# WARNING: this assumes that all we've got is path to venv itself and\n# another path ending with 'site-packages' as is default. But because\n# this must run under different platform, we cannot easily check if this path\n# is the one, because under Linux and macOS site-packages are in different\n# location.\nsite_pkg = None\nfor s in sites:\n    site_pkg = Path(s)\n    if site_pkg.name == \"site-packages\":\n        break\n\n_print(\"Getting venv site-packages ...\")\nassert site_pkg, \"No venv site-packages are found.\"\n_print(f\"Working with: {site_pkg}\", 2)\n\nopenpype_root = Path(os.path.dirname(__file__)).parent\nversion = {}\nwith open(openpype_root / \"openpype\" / \"version.py\") as fp:\n    exec(fp.read(), version)\n\nversion_match = re.search(r\"(\\d+\\.\\d+.\\d+).*\", version[\"__version__\"])\nopenpype_version = version_match[1]\n\n# create full path\nif platform.system().lower() == \"darwin\":\n    build_dir = openpype_root.joinpath(\n        \"build\",\n        f\"OpenPype {openpype_version}.app\",\n        \"Contents\",\n        \"MacOS\")\nelse:\n    build_subdir = f\"exe.{get_platform()}-{sys.version[:3]}\"\n    build_dir = openpype_root / \"build\" / build_subdir\n\n_print(f\"Using build at {build_dir}\", 2)\nif not build_dir.exists():\n    _print(\"Build directory doesn't exist\", 1)\n    _print(\"Probably freezing of code failed. Check ./build/build.log\", 3)\n    sys.exit(1)\n\n\ndef _progress(_base, _names):\n    progress_bar.update()\n    return []\n\n\ndeps_dir = build_dir / \"dependencies\"\nvendor_dir = build_dir / \"vendor\"\nvendor_src = openpype_root / \"vendor\"\n\n# copy vendor files\n_print(\"Copying vendor files ...\")\n\ntotal_files = count_folders(vendor_src)\nprogress_bar = enlighten.Counter(\n    total=total_files, desc=\"Copying vendor files ...\",\n    units=\"%\", color=(64, 128, 222))\n\nshutil.copytree(vendor_src.as_posix(),\n                vendor_dir.as_posix(),\n                ignore=_progress)\nprogress_bar.close()\n\n# copy all files\n_print(\"Copying dependencies ...\")\n\ntotal_files = count_folders(site_pkg)\nprogress_bar = enlighten.Counter(\n    total=total_files, desc=\"Processing Dependencies\",\n    units=\"%\", color=(53, 178, 202))\n\nshutil.copytree(site_pkg.as_posix(),\n                deps_dir.as_posix(),\n                ignore=_progress)\nprogress_bar.close()\n# iterate over frozen libs and create list to delete\nlibs_dir = build_dir / \"lib\"\n\n\n# On Linux use rpath from source libraries in destination libraries\nif platform.system().lower() == \"linux\":\n    src_pyside_dir = openpype_root / \"vendor\" / \"python\" / \"PySide2\"\n    dst_pyside_dir = build_dir / \"vendor\" / \"python\" / \"PySide2\"\n    src_rpath_per_so_file = {}\n    for filepath in src_pyside_dir.glob(\"*.so\"):\n        filename = filepath.name\n        rpath = (\n            subprocess.check_output([\"patchelf\", \"--print-rpath\", filepath])\n            .decode(\"utf-8\")\n            .strip()\n        )\n        src_rpath_per_so_file[filename] = rpath\n\n    for filepath in dst_pyside_dir.glob(\"*.so\"):\n        filename = filepath.name\n        if filename not in src_rpath_per_so_file:\n            continue\n        src_rpath = src_rpath_per_so_file[filename]\n        subprocess.check_call(\n            [\"patchelf\", \"--set-rpath\", src_rpath, filepath]\n        )\n\nto_delete = []\n# _print(\"Finding duplicates ...\")\ndeps_items = list(deps_dir.iterdir())\nitem_count = len(list(libs_dir.iterdir()))\nfind_progress_bar = enlighten.Counter(\n    total=item_count, desc=\"Finding duplicates\", units=\"%\",\n    color=(56, 211, 159))\n\nfor d in libs_dir.iterdir():\n    if (deps_dir / d.name) in deps_items:\n        to_delete.append(d)\n        # _print(f\"found {d}\", 3)\n    find_progress_bar.update()\n\nfind_progress_bar.close()\n# add openpype and igniter in libs too\nto_delete.append(libs_dir / \"openpype\")\nto_delete.append(libs_dir / \"igniter\")\nto_delete.append(libs_dir / \"openpype.pth\")\nto_delete.append(deps_dir / \"openpype.pth\")\n\n# delete duplicates\n# _print(f\"Deleting {len(to_delete)} duplicates ...\")\ndelete_progress_bar = enlighten.Counter(\n    total=len(to_delete), desc=\"Deleting duplicates\", units=\"%\",\n    color=(251, 192, 32))\nfor d in to_delete:\n    if d.is_dir():\n        shutil.rmtree(d)\n    else:\n        try:\n            d.unlink()\n        except FileNotFoundError:\n            # skip non-existent silently\n            pass\n    delete_progress_bar.update()\n\ndelete_progress_bar.close()\n\nend_time = time.time_ns()\ntotal_time = (end_time - start_time) / 1000000000\n_print(f\"Dependency cleanup done in {total_time} secs.\")\n"
  },
  {
    "path": "tools/build_win_installer.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script to build OpenPype Installer.\n\n.DESCRIPTION\n  This script will use already built OpenPype (in `build` directory) and\n  create Windows installer from it using Inno Setup (https://jrsoftware.org/)\n\n.EXAMPLE\n\nPS> .\\build_win_installer.ps1\n\n#>\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n# Install PSWriteColor to support colorized output to terminal\n$env:PSModulePath = $env:PSModulePath + \";$($openpype_root)\\tools\\modules\\powershell\"\n\nfunction Start-Progress {\n    param([ScriptBlock]$code)\n    $scroll = \"/-\\|/-\\|\"\n    $idx = 0\n    $job = Invoke-Command -ComputerName $env:ComputerName -ScriptBlock { $code } -AsJob\n\n    $origpos = $host.UI.RawUI.CursorPosition\n\n    # $origpos.Y -= 1\n\n    while (($job.State -eq \"Running\") -and ($job.State -ne \"NotStarted\"))\n    {\n        $host.UI.RawUI.CursorPosition = $origpos\n        Write-Host $scroll[$idx] -NoNewline\n        $idx++\n        if ($idx -ge $scroll.Length)\n        {\n            $idx = 0\n        }\n        Start-Sleep -Milliseconds 100\n    }\n    # It's over - clear the activity indicator.\n    $host.UI.RawUI.CursorPosition = $origpos\n    Write-Host ' '\n  <#\n  .SYNOPSIS\n  Display spinner for running job\n  .PARAMETER code\n  Job to display spinner for\n  #>\n}\n\nfunction Exit-WithCode($exitcode) {\n   # Only exit this host process if it's a child of another PowerShell parent process...\n   $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$PID\" | Select-Object -Property ParentProcessId).ParentProcessId\n   $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$parentPID\" | Select-Object -Property Name).Name\n   if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) }\n\n   exit $exitcode\n}\n\nfunction Show-PSWarning() {\n    if ($PSVersionTable.PSVersion.Major -lt 7) {\n        Write-Color -Text \"!!! \", \"You are using old version of PowerShell - \",  \"$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)\" -Color Red, Yellow, White\n        Write-Color -Text \"    Please update to at least 7.0 - \", \"https://github.com/PowerShell/PowerShell/releases\" -Color Yellow, White\n        Exit-WithCode 1\n    }\n}\n\n$art = @\"\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~.   ..   ~2p.  ..  ....  .  .\n    .Ppo . .pPO3Op.. . O:. . . .\n   .3Pp . oP3'. 'P33. . 4 ..   .  .   . .. .  .  .\n  .~OP    3PO.  .Op3    : . ..  _____  _____  _____\n  .P3O  . oP3oP3O3P' . . .   . /    /./    /./    /\n   O3:.   O3p~ .       .:. . ./____/./____/ /____/\n   'P .   3p3.  oP3~. ..P:. .  . ..  .   . .. .  .  .\n  . ':  . Po'  .Opo'. .3O. .  o[ by Pype Club ]]]==- - - .  .\n    . '_ ..  .    . _OP3..  .  .https://openpype.io.. .\n         ~P3.OPPPO3OP~ . ..  .\n           .  ' '. .  .. . . . ..  .\n\n\"@\n\nWrite-Host $art -ForegroundColor DarkGreen\n\n# Enable if PS 7.x is needed.\n# Show-PSWarning\n\n\nSet-Location -Path $openpype_root\n\n$version_file = Get-Content -Path \"$($openpype_root)\\openpype\\version.py\"\n$result = [regex]::Matches($version_file, '__version__ = \"(?<version>\\d+\\.\\d+.\\d+.*)\"')\n$openpype_version = $result[0].Groups['version'].Value\nif (-not $openpype_version) {\n  Write-Color -Text \"!!! \", \"Cannot determine OpenPype version.\" -Color Yellow, Gray\n  Exit-WithCode 1\n}\n\n$env:BUILD_VERSION = $openpype_version\n\niscc \n\nWrite-Color \">>> \", \"Detecting host Python ... \" -Color Green, White -NoNewline\n$python = \"python\"\nif (Get-Command \"pyenv\" -ErrorAction SilentlyContinue) {\n    $pyenv_python = & pyenv which python\n    if (Test-Path -PathType Leaf -Path \"$($pyenv_python)\") {\n        $python = $pyenv_python\n    }\n}\nif (-not (Get-Command $python -ErrorAction SilentlyContinue)) {\n    Write-Color \"!!! \", \"Python not detected\" -Color Red, Yellow\n    Set-Location -Path $current_dir\n    Exit-WithCode 1\n}\n$version_command = @'\nimport sys\nprint('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))\n'@\n\n$p = & $python -c $version_command\n$env:PYTHON_VERSION = $p\n$m = $p -match '(\\d+)\\.(\\d+)'\nif(-not $m) {\n    Write-Color \"!!! \", \"Cannot determine version\" -Color Red, Yellow\n    Set-Location -Path $current_dir\n    Exit-WithCode 1\n}\n# We are supporting python 3.9\nif (($matches[1] -lt 3) -or ($matches[2] -lt 9)) {\n    Write-Host \"FAILED Version [ $p ] is old and unsupported\" -ForegroundColor red\n    Set-Location -Path $current_dir\n    Exit-WithCode 1\n} elseif (($matches[1] -eq 3) -and ($matches[2] -gt 9)) {\n    Write-Host \"WARNING Version [ $p ] is unsupported, use at your own risk.\" -ForegroundColor yellow\n    Write-Host \"*** \" -NoNewline -ForegroundColor yellow\n    Write-Host \"OpenPype supports only Python 3.9\" -ForegroundColor white\n} else {\n    Write-Host \"OK [ $p ]\" -ForegroundColor green\n}\n\nWrite-Color -Text \">>> \", \"Creating OpenPype installer ... \" -Color Green, White\n\n$build_dir_command = @\"\nimport sys\nfrom distutils.util import get_platform\nprint('exe.{}-{}'.format(get_platform(), sys.version[0:3]))\n\"@\n\n$build_dir = & $python -c $build_dir_command\nWrite-Color -Text \"--- \", \"Build directory \", \"${build_dir}\" -Color Green, Gray, White\n$env:BUILD_DIR = $build_dir\n\nif (-not (Get-Command iscc -errorAction SilentlyContinue -ErrorVariable ProcessError)) {\n  Write-Color -Text \"!!! \", \"Cannot find Inno Setup command\" -Color Red, Yellow\n  Write-Color \"!!! You can download it at https://jrsoftware.org/\" -ForegroundColor red\n  Exit-WithCode 1\n}\n\n& iscc \"$openpype_root\\inno_setup.iss\"\n\nif ($LASTEXITCODE -ne 0) {\n    Write-Color -Text \"!!! \", \"Creating installer failed.\" -Color Red, Yellow\n    Exit-WithCode 1\n}\n\nWrite-Color -Text \">>> \", \"Restoring current directory\" -Color Green, Gray\nSet-Location -Path $current_dir\ntry {\n    New-BurntToastNotification -AppLogo \"$openpype_root/openpype/resources/icons/openpype_icon.png\" -Text \"OpenPype build complete!\", \"All done. You will find You will find OpenPype installer in '.\\build' directory.\"\n} catch {}\nWrite-Color -Text \"*** \", \"All done. You will find OpenPype installer in \", \"'.\\build'\", \" directory.\" -Color Green, Gray, White, Gray\n"
  },
  {
    "path": "tools/ci_tools.py",
    "content": "import re\nimport sys\nfrom semver import VersionInfo\nfrom git import Repo\nfrom optparse import OptionParser\nfrom github import Github\nimport os\n\ndef get_release_type_github(Log, github_token):\n    minor_labels = [\"Bump Minor\"]\n\n    g = Github(github_token)\n    repo = g.get_repo(\"ynput/OpenPype\")\n\n    labels = set()\n    for line in Log.splitlines():\n        match = re.search(\"pull request #(\\d+)\", line)\n        if match:\n            pr_number = match.group(1)\n            try:\n                pr = repo.get_pull(int(pr_number))\n            except:\n                continue\n            for label in pr.labels:\n                labels.add(label.name)\n\n    if any(label in labels for label in minor_labels):\n        return \"minor\"\n    else:\n        return \"patch\"\n\n    # TODO: if all is working fine, this part can be cleaned up eventually\n    # if any(label in labels for label in patch_labels):\n    #     return \"patch\"\n\n    return None\n\n\ndef remove_prefix(text, prefix):\n    return text[text.startswith(prefix) and len(prefix):]\n\n\ndef get_last_version(match):\n    repo = Repo()\n    assert not repo.bare\n    version_types = {\n        \"CI\": \"CI/[0-9]*\",\n        \"release\": \"[0-9]*\"\n    }\n    tag = repo.git.describe(\n        '--tags',\n        f'--match={version_types[match]}',\n        '--abbrev=0'\n        )\n\n    if match == \"CI\":\n        return remove_prefix(tag, \"CI/\"), tag\n    else:\n        return tag, tag\n\n\ndef get_log_since_tag(version):\n    repo = Repo()\n    assert not repo.bare\n    return repo.git.log(f'{version}..HEAD', '--merges', '--oneline')\n\n\ndef release_type(log):\n    regex_minor = [\"feature/\", \"(feat)\"]\n    regex_patch = [\"bugfix/\", \"fix/\", \"(fix)\", \"enhancement/\", \"update\"]\n    for reg in regex_minor:\n        if re.search(reg, log):\n            return \"minor\"\n    for reg in regex_patch:\n        if re.search(reg, log):\n            return \"patch\"\n    return None\n\n\ndef file_regex_replace(filename, regex, version):\n    with open(filename, 'r+') as f:\n        text = f.read()\n        text = re.sub(regex, version, text)\n        # pp.pprint(f\"NEW VERSION {version} INSERTED into {filename}\")\n        f.seek(0)\n        f.write(text)\n        f.truncate()\n\n\ndef bump_file_versions(version, nightly=False):\n\n    filename = \"./openpype/version.py\"\n    regex = \"(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-((0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\\+([0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*))?\"\n    file_regex_replace(filename, regex, version)\n\n    if nightly:\n        # skip nightly reversion in pyproject.toml\n        return\n\n    # bump pyproject.toml\n    filename = \"pyproject.toml\"\n    regex = \"version = \\\"(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(\\+((0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\\+([0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*))?\\\" # OpenPype\"\n    pyproject_version = f\"version = \\\"{version}\\\" # OpenPype\"\n    file_regex_replace(filename, regex, pyproject_version)\n\n\ndef calculate_next_nightly(type=\"nightly\", github_token=None):\n    last_prerelease, last_pre_tag = get_last_version(\"CI\")\n    last_pre_v = VersionInfo.parse(last_prerelease)\n    last_pre_v_finalized = last_pre_v.finalize_version()\n    # print(last_pre_v_finalized)\n\n    last_release, last_release_tag = get_last_version(\"release\")\n\n    last_release_v = VersionInfo.parse(last_release)\n    bump_type = get_release_type_github(\n        get_log_since_tag(last_release_tag),\n        github_token\n        )\n    if not bump_type:\n        return None\n\n    next_release_v = last_release_v.next_version(part=bump_type)\n    # print(next_release_v)\n\n    if next_release_v > last_pre_v_finalized:\n        next_tag = next_release_v.bump_prerelease(token=type).__str__()\n        return next_tag\n    elif next_release_v == last_pre_v_finalized:\n        next_tag = last_pre_v.bump_prerelease(token=type).__str__()\n        return next_tag\n\ndef finalize_latest_nightly():\n    last_prerelease, last_pre_tag = get_last_version(\"CI\")\n    last_pre_v = VersionInfo.parse(last_prerelease)\n    last_pre_v_finalized = last_pre_v.finalize_version()\n    # print(last_pre_v_finalized)\n\n    return last_pre_v_finalized.__str__()\n\ndef finalize_prerelease(prerelease):\n\n    if \"/\" in prerelease:\n        prerelease = prerelease.split(\"/\")[-1]\n\n    prerelease_v = VersionInfo.parse(prerelease)\n    prerelease_v_finalized = prerelease_v.finalize_version()\n\n    return prerelease_v_finalized.__str__()\n\n\ndef main():\n    usage = \"usage: %prog [options] arg\"\n    parser = OptionParser(usage)\n    parser.add_option(\"-n\", \"--nightly\",\n                      dest=\"nightly\", action=\"store_true\",\n                      help=\"Bump nightly version and return it\")\n    parser.add_option(\"-b\", \"--bump\",\n                      dest=\"bump\", action=\"store_true\",\n                      help=\"Return if there is something to bump\")\n    parser.add_option(\"-r\", \"--release-latest\",\n                      dest=\"releaselatest\", action=\"store_true\",\n                      help=\"finalize latest prerelease to a release\")\n    parser.add_option(\"-p\", \"--prerelease\",\n                      dest=\"prerelease\", action=\"store\",\n                      help=\"define prerelease type\")\n    parser.add_option(\"-f\", \"--finalize\",\n                      dest=\"finalize\", action=\"store\",\n                      help=\"define prerelease type\")\n    parser.add_option(\"-v\", \"--version\",\n                      dest=\"version\", action=\"store\",\n                      help=\"work with explicit version\")\n    parser.add_option(\"-l\", \"--lastversion\",\n                      dest=\"lastversion\", action=\"store\",\n                      help=\"work with explicit version\")\n    parser.add_option(\"-g\", \"--github_token\",\n                      dest=\"github_token\", action=\"store\",\n                      help=\"github token\")\n\n\n    (options, args) = parser.parse_args()\n\n    if options.bump:\n        last_release, last_release_tag = get_last_version(\"release\")\n        bump_type_release = get_release_type_github(\n            get_log_since_tag(last_release_tag),\n            options.github_token\n            )\n        if bump_type_release is None:\n            print(\"skip\")\n        else:\n            print(bump_type_release)\n\n    if options.nightly:\n        next_tag_v = calculate_next_nightly(github_token=options.github_token)\n        print(next_tag_v)\n        bump_file_versions(next_tag_v, True)\n\n    if options.finalize:\n        new_release = finalize_prerelease(options.finalize)\n        print(new_release)\n        bump_file_versions(new_release)\n\n    if options.lastversion:\n        last_release, last_release_tag = get_last_version(options.lastversion)\n        print(last_release_tag)\n\n    if options.releaselatest:\n        new_release = finalize_latest_nightly()\n        last_release, last_release_tag = get_last_version(\"release\")\n\n        if VersionInfo.parse(new_release) > VersionInfo.parse(last_release):\n            print(new_release)\n            bump_file_versions(new_release)\n        else:\n            print(\"skip\")\n\n    if options.prerelease:\n        current_prerelease = VersionInfo.parse(options.prerelease)\n        new_prerelease = current_prerelease.bump_prerelease().__str__()\n        print(new_prerelease)\n        bump_file_versions(new_prerelease)\n\n    if options.version:\n        bump_file_versions(options.version)\n        print(f\"Injected version {options.version} into the release\")\n\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/create_env.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script create virtual environment using Poetry.\n\n.DESCRIPTION\n  This script will detect Python installation, create venv with Poetry\n  and install all necessary packages from `poetry.lock` or `pyproject.toml`\n  needed by OpenPype to be included during application freeze on Windows.\n\n.EXAMPLE\n\nPS> .\\create_env.ps1\n\n.EXAMPLE\n\nPrint verbose information from Poetry:\nPS> .\\create_env.ps1 --verbose\n\n#>\n\n$arguments=$ARGS\n$poetry_verbosity=$null\nif($arguments -eq \"--verbose\") {\n    $poetry_verbosity=\"-vvv\"\n}\n\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n& git submodule update --init --recursive\n# Install PSWriteColor to support colorized output to terminal\n$env:PSModulePath = $env:PSModulePath + \";$($openpype_root)\\tools\\modules\\powershell\"\n\n\nfunction Exit-WithCode($exitcode) {\n   # Only exit this host process if it's a child of another PowerShell parent process...\n   $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$PID\" | Select-Object -Property ParentProcessId).ParentProcessId\n   $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$parentPID\" | Select-Object -Property Name).Name\n   if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) }\n\n   exit $exitcode\n}\n\n\nfunction Show-PSWarning() {\n    if ($PSVersionTable.PSVersion.Major -lt 7) {\n        Write-Color -Text \"!!! \", \"You are using old version of PowerShell - \",  \"$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)\" -Color Red, Yellow, White\n        Write-Color -Text \"    Please update to at least 7.0 - \", \"https://github.com/PowerShell/PowerShell/releases\" -Color Yellow, White\n        Exit-WithCode 1\n    }\n}\n\n\nfunction Install-Poetry() {\n    Write-Color -Text \">>> \", \"Installing Poetry ... \" -Color Green, Gray\n    $python = \"python\"\n    if (Get-Command \"pyenv\" -ErrorAction SilentlyContinue) {\n        if (-not (Test-Path -PathType Leaf -Path \"$($openpype_root)\\.python-version\")) {\n            $result = & pyenv global\n            if ($result -eq \"no global version configured\") {\n                Write-Color -Text \"!!! \", \"Using pyenv but having no local or global version of Python set.\" -Color Red, Yellow\n                Exit-WithCode 1\n            }\n        }\n        $python = & pyenv which python\n\n    }\n\n    $env:POETRY_HOME=\"$openpype_root\\.poetry\"\n    $env:POETRY_VERSION=\"1.3.2\"\n    (Invoke-WebRequest -Uri https://install.python-poetry.org/ -UseBasicParsing).Content | & $($python) -\n}\n\n\nfunction Test-Python() {\n    Write-Color -Text \">>> \", \"Detecting host Python ... \" -Color Green, Gray -NoNewline\n    $python = \"python\"\n    if (Get-Command \"pyenv\" -ErrorAction SilentlyContinue) {\n        $pyenv_python = & pyenv which python\n        if (Test-Path -PathType Leaf -Path \"$($pyenv_python)\") {\n            $python = $pyenv_python\n        }\n    }\n    if (-not (Get-Command $python -ErrorAction SilentlyContinue)) {\n        Write-Host \"!!! Python not detected\" -ForegroundColor red\n        Set-Location -Path $current_dir\n        Exit-WithCode 1\n    }\n    $version_command = @'\nimport sys\nprint('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))\n'@\n\n    $p = & $python -c $version_command\n    $env:PYTHON_VERSION = $p\n    $m = $p -match '(\\d+)\\.(\\d+)'\n    if(-not $m) {\n      Write-Host \"!!! Cannot determine version\" -ForegroundColor red\n      Set-Location -Path $current_dir\n      Exit-WithCode 1\n    }\n    # We are supporting python 3.9 only\n    if (([int]$matches[1] -lt 3) -or ([int]$matches[2] -lt 9)) {\n      Write-Color -Text \"FAILED \", \"Version \", \"[\", $p ,\"]\",  \"is old and unsupported\" -Color Red, Yellow, Cyan, White, Cyan, Yellow\n      Set-Location -Path $current_dir\n      Exit-WithCode 1\n    } elseif (([int]$matches[1] -eq 3) -and ([int]$matches[2] -gt 9)) {\n        Write-Color -Text \"WARNING Version \", \"[\",  $p, \"]\",  \" is unsupported, use at your own risk.\" -Color Yellow, Cyan, White, Cyan, Yellow\n        Write-Color -Text \"*** \", \"OpenPype supports only Python 3.9\" -Color Yellow, White\n    } else {\n        Write-Color \"OK \", \"[\",  $p, \"]\" -Color Green, Cyan, White, Cyan\n    }\n}\n\nif (-not (Test-Path 'env:POETRY_HOME')) {\n    $env:POETRY_HOME = \"$openpype_root\\.poetry\"\n}\n\n\nSet-Location -Path $openpype_root\n\n$art = @\"\n\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~.   ..   ~2p.  ..  ....  .  .\n    .Ppo . .pPO3Op.. . O:. . . .\n   .3Pp . oP3'. 'P33. . 4 ..   .  .   . .. .  .  .\n  .~OP    3PO.  .Op3    : . ..  _____  _____  _____\n  .P3O  . oP3oP3O3P' . . .   . /    /./    /./    /\n   O3:.   O3p~ .       .:. . ./____/./____/ /____/\n   'P .   3p3.  oP3~. ..P:. .  . ..  .   . .. .  .  .\n  . ':  . Po'  .Opo'. .3O. .  o[ by Pype Club ]]]==- - - .  .\n    . '_ ..  .    . _OP3..  .  .https://openpype.io.. .\n         ~P3.OPPPO3OP~ . ..  .\n           .  ' '. .  .. . . . ..  .\n\n\n\"@\nif (-not (Test-Path 'env:_INSIDE_OPENPYPE_TOOL')) {\n    Write-Host $art -ForegroundColor DarkGreen\n}\n\n# Enable if PS 7.x is needed.\n# Show-PSWarning\n\n$version_file = Get-Content -Path \"$($openpype_root)\\openpype\\version.py\"\n$result = [regex]::Matches($version_file, '__version__ = \"(?<version>\\d+\\.\\d+.\\d+.*)\"')\n$openpype_version = $result[0].Groups['version'].Value\nif (-not $openpype_version) {\n  Write-Color -Text \"!!! \", \"Cannot determine OpenPype version.\" -Color Red, Yellow\n  Set-Location -Path $current_dir\n  Exit-WithCode 1\n}\nWrite-Color -Text \">>> \", \"Found OpenPype version \", \"[ \", $($openpype_version), \" ]\" -Color Green, Gray, Cyan, White, Cyan\n\nTest-Python\n\nWrite-Color -Text \">>> \", \"Reading Poetry ... \" -Color Green, Gray -NoNewline\nif (-not (Test-Path -PathType Container -Path \"$($env:POETRY_HOME)\\bin\")) {\n    Write-Color -Text \"NOT FOUND\" -Color Yellow\n    Install-Poetry\n    Write-Color -Text \"INSTALLED\" -Color Cyan\n} else {\n    Write-Color -Text \"OK\" -Color Green\n}\n\nif (-not (Test-Path -PathType Leaf -Path \"$($openpype_root)\\poetry.lock\")) {\n    Write-Color -Text \">>> \", \"Installing virtual environment and creating lock.\" -Color Green, Gray\n} else {\n    Write-Color -Text \">>> \", \"Installing virtual environment from lock.\" -Color Green, Gray\n}\n$startTime = [int][double]::Parse((Get-Date -UFormat %s))\n& \"$env:POETRY_HOME\\bin\\poetry\" install --no-root $poetry_verbosity --ansi\nif ($LASTEXITCODE -ne 0) {\n    Write-Color -Text \"!!! \", \"Poetry command failed.\" -Color Red, Yellow\n    Set-Location -Path $current_dir\n    Exit-WithCode 1\n}\nWrite-Color -Text \">>> \", \"Installing pre-commit hooks ...\" -Color Green, White\n& \"$env:POETRY_HOME\\bin\\poetry\" run pre-commit install\nif ($LASTEXITCODE -ne 0) {\n    Write-Color -Text \"!!! \", \"Installation of pre-commit hooks failed.\" -Color Red, Yellow\n    Set-Location -Path $current_dir\n    Exit-WithCode 1\n}\n\n$endTime = [int][double]::Parse((Get-Date -UFormat %s))\nSet-Location -Path $current_dir\ntry\n{\n    New-BurntToastNotification -AppLogo \"$openpype_root/openpype/resources/icons/openpype_icon.png\" -Text \"OpenPype\", \"Virtual environment created.\", \"All done in $( $endTime - $startTime ) secs.\"\n} catch {}\nWrite-Color -Text \">>> \", \"Virtual environment created.\" -Color Green, White\n"
  },
  {
    "path": "tools/create_env.sh",
    "content": "#!/usr/bin/env bash\n\n# This script will detect Python installation, create venv with Poetry\n# and install all necessary packages from `poetry.lock` or `pyproject.toml`\n# needed by OpenPype to be included during application freeze on unix.\n\nart () {\n  cat <<-EOF\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~·   ··   ~2p.  ··  ····  ·  ·\n    ·Ppo · .pPO3Op.· · O:· · · ·\n   .3Pp · oP3'· 'P33· · 4 ··   ·  ·   · ·· ·  ·  ·\n  ·~OP    3PO·  .Op3    : · ··  _____  _____  _____\n  ·P3O  · oP3oP3O3P' · · ·   · /    /·/    /·/    /\n   O3:·   O3p~ ·       ·:· · ·/____/·/____/ /____/\n   'P ·   3p3·  oP3~· ·.P:· ·  · ··  ·   · ·· ·  ·  ·\n  · ':  · Po'  ·Opo'· .3O· .  o[ by Pype Club ]]]==- - - ·  ·\n    · '_ ..  ·    . _OP3··  ·  ·https://openpype.io·· ·\n         ~P3·OPPPO3OP~ · ··  ·\n           ·  ' '· ·  ·· · · · ··  ·\n\nEOF\n}\n\n# Colors for terminal\n\nRST='\\033[0m'             # Text Reset\n\n# Regular Colors\nBlack='\\033[0;30m'        # Black\nRed='\\033[0;31m'          # Red\nGreen='\\033[0;32m'        # Green\nYellow='\\033[0;33m'       # Yellow\nBlue='\\033[0;34m'         # Blue\nPurple='\\033[0;35m'       # Purple\nCyan='\\033[0;36m'         # Cyan\nWhite='\\033[0;37m'        # White\n\n# Bold\nBBlack='\\033[1;30m'       # Black\nBRed='\\033[1;31m'         # Red\nBGreen='\\033[1;32m'       # Green\nBYellow='\\033[1;33m'      # Yellow\nBBlue='\\033[1;34m'        # Blue\nBPurple='\\033[1;35m'      # Purple\nBCyan='\\033[1;36m'        # Cyan\nBWhite='\\033[1;37m'       # White\n\n# Bold High Intensity\nBIBlack='\\033[1;90m'      # Black\nBIRed='\\033[1;91m'        # Red\nBIGreen='\\033[1;92m'      # Green\nBIYellow='\\033[1;93m'     # Yellow\nBIBlue='\\033[1;94m'       # Blue\nBIPurple='\\033[1;95m'     # Purple\nBICyan='\\033[1;96m'       # Cyan\nBIWhite='\\033[1;97m'      # White\n\n\npoetry_verbosity=\"\"\nwhile :; do\n  case $1 in\n    --verbose)\n      poetry_verbosity=\"-vvv\"\n      ;;\n    --)\n      shift\n      break\n      ;;\n    *)\n      break\n  esac\n  shift\ndone\n\n\n##############################################################################\n# Detect required version of python\n# Globals:\n#   colors\n#   PYTHON\n# Arguments:\n#   None\n# Returns:\n#   None\n###############################################################################\ndetect_python () {\n  echo -e \"${BIGreen}>>>${RST} Using python \\c\"\n  command -v python >/dev/null 2>&1 || { echo -e \"${BIRed}- NOT FOUND${RST} ${BIYellow}You need Python 3.9 installed to continue.${RST}\"; return 1; }\n  local version_command=\"import sys;print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))\"\n  local python_version=\"$(python <<< ${version_command})\"\n  oIFS=\"$IFS\"\n  IFS=.\n  set -- $python_version\n  IFS=\"$oIFS\"\n  if [ \"$1\" -ge \"3\" ] && [ \"$2\" -ge \"9\" ] ; then\n    if [ \"$2\" -gt \"9\" ] ; then\n      echo -e \"${BIWhite}[${RST} ${BIRed}$1.$2 ${BIWhite}]${RST} - ${BIRed}FAILED${RST} ${BIYellow}Version is new and unsupported, use${RST} ${BIPurple}3.9.x${RST}\"; return 1;\n    else\n      echo -e \"${BIWhite}[${RST} ${BIGreen}$1.$2${RST} ${BIWhite}]${RST}\"\n    fi\n  else\n    command -v python >/dev/null 2>&1 || { echo -e \"${BIRed}$1.$2$ - ${BIRed}FAILED${RST} ${BIYellow}Version is old and unsupported${RST}\"; return 1; }\n  fi\n}\n\ninstall_poetry () {\n  echo -e \"${BIGreen}>>>${RST} Installing Poetry ...\"\n  export POETRY_HOME=\"$openpype_root/.poetry\"\n  export POETRY_VERSION=\"1.3.2\"\n  command -v curl >/dev/null 2>&1 || { echo -e \"${BIRed}!!!${RST}${BIYellow} Missing ${RST}${BIBlue}curl${BIYellow} command.${RST}\"; return 1; }\n  curl -sSL https://install.python-poetry.org/ | python -\n}\n\n##############################################################################\n# Clean pyc files in specified directory\n# Globals:\n#   None\n# Arguments:\n#   Optional path to clean\n# Returns:\n#   None\n###############################################################################\nclean_pyc () {\n  local path\n  path=$openpype_root\n  echo -e \"${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \\c\"\n  find \"$path\" -path ./build -o -regex '^.*\\(__pycache__\\|\\.py[co]\\)$' -delete\n  echo -e \"${BIGreen}DONE${RST}\"\n}\n\n##############################################################################\n# Return absolute path\n# Globals:\n#   None\n# Arguments:\n#   Path to resolve\n# Returns:\n#   None\n###############################################################################\nrealpath () {\n  echo $(cd $(dirname \"$1\"); pwd)/$(basename \"$1\")\n}\n\nmain () {\n  # Main\n  if [[ -z $_inside_openpype_tool ]]; then\n    echo -e \"${BGreen}\"\n    art\n    echo -e \"${RST}\"\n  fi\n  detect_python || return 1\n\n  # Directories\n  openpype_root=$(realpath $(dirname $(dirname \"${BASH_SOURCE[0]}\")))\n\n  if [[ -z $POETRY_HOME ]]; then\n    export POETRY_HOME=\"$openpype_root/.poetry\"\n  fi\n\n\n  pushd \"$openpype_root\" > /dev/null || return > /dev/null\n\n  echo -e \"${BIGreen}>>>${RST} Reading Poetry ... \\c\"\n  if [ -f \"$POETRY_HOME/bin/poetry\" ]; then\n    echo -e \"${BIGreen}OK${RST}\"\n  else\n    echo -e \"${BIYellow}NOT FOUND${RST}\"\n    install_poetry || { echo -e \"${BIRed}!!!${RST} Poetry installation failed\"; return 1; }\n  fi\n\n  if [ -f \"$openpype_root/poetry.lock\" ]; then\n    echo -e \"${BIGreen}>>>${RST} Updating dependencies ...\"\n  else\n    echo -e \"${BIGreen}>>>${RST} Installing dependencies ...\"\n  fi\n\n  \"$POETRY_HOME/bin/poetry\" install --no-root $poetry_verbosity || { echo -e \"${BIRed}!!!${RST} Poetry environment installation failed\"; return 1; }\n  if [ $? -ne 0 ] ; then\n    echo -e \"${BIRed}!!!${RST} Virtual environment creation failed.\"\n    return 1\n  fi\n\n  echo -e \"${BIGreen}>>>${RST} Cleaning cache files ...\"\n  clean_pyc\n\n  # reinstall these because of bug in poetry? or cx_freeze?\n  # cx_freeze will crash on missing __pychache__ on these but\n  # reinstalling them solves the problem.\n  echo -e \"${BIGreen}>>>${RST} Post-venv creation fixes ...\"\n  local openpype_index=$(\"$POETRY_HOME/bin/poetry\" run python \"$openpype_root/tools/parse_pyproject.py\" tool.poetry.source.0.url)\n  echo -e \"${BIGreen}-   ${RST} Using index: ${BIWhite}$openpype_index${RST}\"\n  \"$POETRY_HOME/bin/poetry\" run python -m pip install --disable-pip-version-check --force-reinstall pip\n  echo -e \"${BIGreen}>>>${RST} Installing pre-commit hooks ...\"\n  \"$POETRY_HOME/bin/poetry\" run pre-commit install\n}\n\nreturn_code=0\nmain || return_code=$?\nexit $return_code\n"
  },
  {
    "path": "tools/create_zip.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script create distributable OpenPype zip.\n\n.DESCRIPTION\n  This script will detect Python installation and run OpenPype to create\n  zip. It will create zip from current source code\n  version and copy it top `%LOCALAPPDATA%/pypeclub/openpype` if `--path` or `-p`\n  argument is not used.\n\n.EXAMPLE\n\nPS> .\\create_zip.ps1\n\n.EXAMPLE\n\nTo put generated zip to C:\\OpenPype directory:\nPS> .\\create_zip.ps1 --path C:\\OpenPype\n\n#>\n\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n# Install PSWriteColor to support colorized output to terminal\n$env:PSModulePath = $env:PSModulePath + \";$($openpype_root)\\tools\\modules\\powershell\"\n\nfunction Exit-WithCode($exitcode) {\n   # Only exit this host process if it's a child of another PowerShell parent process...\n   $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$PID\" | Select-Object -Property ParentProcessId).ParentProcessId\n   $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$parentPID\" | Select-Object -Property Name).Name\n   if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) }\n\n   exit $exitcode\n}\n\n\nfunction Show-PSWarning() {\n    if ($PSVersionTable.PSVersion.Major -lt 7) {\n        Write-Color -Text \"!!! \", \"You are using old version of PowerShell - \",  \"$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)\" -Color Red, Yellow, White\n        Write-Color -Text \"    Please update to at least 7.0 - \", \"https://github.com/PowerShell/PowerShell/releases\" -Color Yellow, White\n        Exit-WithCode 1\n    }\n}\n\n$env:_INSIDE_OPENPYPE_TOOL = \"1\"\n\nif (-not (Test-Path 'env:POETRY_HOME')) {\n    $env:POETRY_HOME = \"$openpype_root\\.poetry\"\n}\n\nSet-Location -Path $openpype_root\n\n$art = @\"\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~.   ..   ~2p.  ..  ....  .  .\n    .Ppo . .pPO3Op.. . O:. . . .\n   .3Pp . oP3'. 'P33. . 4 ..   .  .   . .. .  .  .\n  .~OP    3PO.  .Op3    : . ..  _____  _____  _____\n  .P3O  . oP3oP3O3P' . . .   . /    /./    /./    /\n   O3:.   O3p~ .       .:. . ./____/./____/ /____/\n   'P .   3p3.  oP3~. ..P:. .  . ..  .   . .. .  .  .\n  . ':  . Po'  .Opo'. .3O. .  o[ by Pype Club ]]]==- - - .  .\n    . '_ ..  .    . _OP3..  .  .https://openpype.io.. .\n         ~P3.OPPPO3OP~ . ..  .\n           .  ' '. .  .. . . . ..  .\n\n\"@\n\nWrite-Host $art -ForegroundColor DarkGreen\n\n# Enable if PS 7.x is needed.\n# Show-PSWarning\n\n$version_file = Get-Content -Path \"$($openpype_root)\\openpype\\version.py\"\n$result = [regex]::Matches($version_file, '__version__ = \"(?<version>\\d+\\.\\d+.\\d+.*)\"')\n$openpype_version = $result[0].Groups['version'].Value\nif (-not $openpype_version) {\n  Write-Color -Text \"!!! \", \"Cannot determine OpenPype version.\" -Color Yellow, Gray\n  Exit-WithCode 1\n}\n\nWrite-Color -Text \">>> \", \"Reading Poetry ... \" -Color Green, Gray -NoNewline\nif (-not (Test-Path -PathType Container -Path \"$($env:POETRY_HOME)\\bin\")) {\n    Write-Color -Text \"NOT FOUND\" -Color Yellow\n    Write-Color -Text \"*** \", \"We need to install Poetry create virtual env first ...\" -Color Yellow, Gray\n    & \"$openpype_root\\tools\\create_env.ps1\"\n} else {\n    Write-Color -Text \"OK\" -Color Green\n}\n\nWrite-Color -Text \">>> \", \"Cleaning cache files ... \" -Color Green, Gray -NoNewline\nGet-ChildItem $openpype_root -Filter \"__pycache__\" -Force -Recurse|  Where-Object {( $_.FullName -inotmatch '\\\\build\\\\' ) -and ( $_.FullName -inotmatch '\\\\.venv' )} | Remove-Item -Force -Recurse\nGet-ChildItem $openpype_root -Filter \"*.pyc\" -Force -Recurse | Where-Object {( $_.FullName -inotmatch '\\\\build\\\\' ) -and ( $_.FullName -inotmatch '\\\\.venv' )} | Remove-Item -Force\nGet-ChildItem $openpype_root -Filter \"*.pyo\" -Force -Recurse | Where-Object {( $_.FullName -inotmatch '\\\\build\\\\' ) -and ( $_.FullName -inotmatch '\\\\.venv' )} | Remove-Item -Force\nWrite-Color -Text \"OK\" -Color Green\n\nWrite-Color -Text \">>> \", \"Generating zip from current sources ...\" -Color Green, Gray\n$env:PYTHONPATH=\"$($openpype_root);$($env:PYTHONPATH)\"\n$env:OPENPYPE_ROOT=\"$($openpype_root)\"\n& \"$($env:POETRY_HOME)\\bin\\poetry\" run python \"$($openpype_root)\\tools\\create_zip.py\" $ARGS\nSet-Location -Path $current_dir\n"
  },
  {
    "path": "tools/create_zip.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Create OpenPype version from live sources.\"\"\"\nfrom igniter import bootstrap_repos\nimport click\nimport enlighten\nimport blessed\nfrom pathlib2 import Path\n\n\nterm = blessed.Terminal()\nmanager = enlighten.get_manager()\nlast_increment = 0\n\n\n@click.group(invoke_without_command=True)\n@click.option(\"--path\", required=False,\n              help=\"path where to put version\",\n              type=click.Path(exists=True))\ndef main(path):\n    # create zip file\n\n    progress_bar = enlighten.Counter(\n        total=100, desc=\"OpenPype ZIP\", units=\"%\", color=\"green\")\n\n    def progress(inc: int):\n        \"\"\"Progress handler.\"\"\"\n        global last_increment\n        progress_bar.update(incr=inc - last_increment)\n        last_increment = inc\n\n    bs = bootstrap_repos.BootstrapRepos(progress_callback=progress)\n    if path:\n        out_path = Path(path)\n        bs.data_dir = out_path\n        if out_path.is_file():\n            bs.data_dir = out_path.parent\n\n    _print(f\"Creating zip in {bs.data_dir} ...\")\n    repo_file = bs.create_version_from_live_code()\n    if not repo_file:\n        _print(\"Error while creating zip file.\", 1)\n        exit(1)\n\n    _print(f\"Created {repo_file}\")\n\n\ndef _print(msg: str, message_type: int = 0) -> None:\n    \"\"\"Print message to console.\n\n    Args:\n        msg (str): message to print\n        message_type (int): type of message (0 info, 1 error, 2 note)\n\n    \"\"\"\n    if message_type == 0:\n        header = term.aquamarine3(\">>> \")\n    elif message_type == 1:\n        header = term.orangered2(\"!!! \")\n    elif message_type == 2:\n        header = term.tan1(\"... \")\n    else:\n        header = term.darkolivegreen3(\"--- \")\n\n    print(f\"{header}{msg}\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/create_zip.sh",
    "content": "#!/usr/bin/env bash\n\n# This script will detect Python installation and run OpenPype to create\n# zip. It needs mongodb running. I will create zip from current source code\n# version and copy it top `~/.local/share/pype` if `--path` or `-p`\n# argument is not used.\n\nart () {\n  cat <<-EOF\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~·   ··   ~2p.  ··  ····  ·  ·\n    ·Ppo · .pPO3Op.· · O:· · · ·\n   .3Pp · oP3'· 'P33· · 4 ··   ·  ·   · ·· ·  ·  ·\n  ·~OP    3PO·  .Op3    : · ··  _____  _____  _____\n  ·P3O  · oP3oP3O3P' · · ·   · /    /·/    /·/    /\n   O3:·   O3p~ ·       ·:· · ·/____/·/____/ /____/\n   'P ·   3p3·  oP3~· ·.P:· ·  · ··  ·   · ·· ·  ·  ·\n  · ':  · Po'  ·Opo'· .3O· .  o[ by Pype Club ]]]==- - - ·  ·\n    · '_ ..  ·    . _OP3··  ·  ·https://openpype.io·· ·\n         ~P3·OPPPO3OP~ · ··  ·\n           ·  ' '· ·  ·· · · · ··  ·\n\nEOF\n}\n\n# Colors for terminal\n\nRST='\\033[0m'             # Text Reset\n\n# Regular Colors\nBlack='\\033[0;30m'        # Black\nRed='\\033[0;31m'          # Red\nGreen='\\033[0;32m'        # Green\nYellow='\\033[0;33m'       # Yellow\nBlue='\\033[0;34m'         # Blue\nPurple='\\033[0;35m'       # Purple\nCyan='\\033[0;36m'         # Cyan\nWhite='\\033[0;37m'        # White\n\n# Bold\nBBlack='\\033[1;30m'       # Black\nBRed='\\033[1;31m'         # Red\nBGreen='\\033[1;32m'       # Green\nBYellow='\\033[1;33m'      # Yellow\nBBlue='\\033[1;34m'        # Blue\nBPurple='\\033[1;35m'      # Purple\nBCyan='\\033[1;36m'        # Cyan\nBWhite='\\033[1;37m'       # White\n\n# Bold High Intensity\nBIBlack='\\033[1;90m'      # Black\nBIRed='\\033[1;91m'        # Red\nBIGreen='\\033[1;92m'      # Green\nBIYellow='\\033[1;93m'     # Yellow\nBIBlue='\\033[1;94m'       # Blue\nBIPurple='\\033[1;95m'     # Purple\nBICyan='\\033[1;96m'       # Cyan\nBIWhite='\\033[1;97m'      # White\n\n\n##############################################################################\n# Detect required version of python\n# Globals:\n#   colors\n#   PYTHON\n# Arguments:\n#   None\n# Returns:\n#   None\n###############################################################################\ndetect_python () {\n  echo -e \"${BIGreen}>>>${RST} Using python \\c\"\n  local version_command=\"import sys;print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))\"\n  local python_version=\"$(python3 <<< ${version_command})\"\n  oIFS=\"$IFS\"\n  IFS=.\n  set -- $python_version\n  IFS=\"$oIFS\"\n  if [ \"$1\" -ge \"3\" ] && [ \"$2\" -ge \"9\" ] ; then\n    if [ \"$2\" -gt \"9\" ] ; then\n      echo -e \"${BIWhite}[${RST} ${BIRed}$1.$2 ${BIWhite}]${RST} - ${BIRed}FAILED${RST} ${BIYellow}Version is new and unsupported, use${RST} ${BIPurple}3.9.x${RST}\"; return 1;\n    else\n      echo -e \"${BIWhite}[${RST} ${BIGreen}$1.$2${RST} ${BIWhite}]${RST}\"\n    fi\n  else\n    command -v python3 >/dev/null 2>&1 || { echo -e \"${BIRed}$1.$2$ - ${BIRed}FAILED${RST} ${BIYellow}Version is old and unsupported${RST}\"; return 1; }\n  fi\n}\n\n##############################################################################\n# Return absolute path\n# Globals:\n#   None\n# Arguments:\n#   Path to resolve\n# Returns:\n#   None\n###############################################################################\nrealpath () {\n  echo $(cd $(dirname \"$1\"); pwd)/$(basename \"$1\")\n}\n\n# Main\nmain () {\n  echo -e \"${BGreen}\"\n  art\n  echo -e \"${RST}\"\n  detect_python || return 1\n\n  # Directories\n  openpype_root=$(realpath $(dirname $(dirname \"${BASH_SOURCE[0]}\")))\n\n  _inside_openpype_tool=\"1\"\n\n  if [[ -z $POETRY_HOME ]]; then\n    export POETRY_HOME=\"$openpype_root/.poetry\"\n  fi\n\n  pushd \"$openpype_root\" > /dev/null || return > /dev/null\n\n  echo -e \"${BIGreen}>>>${RST} Reading Poetry ... \\c\"\n  if [ -f \"$POETRY_HOME/bin/poetry\" ]; then\n    echo -e \"${BIGreen}OK${RST}\"\n  else\n    echo -e \"${BIYellow}NOT FOUND${RST}\"\n    echo -e \"${BIYellow}***${RST} We need to install Poetry and virtual env ...\"\n    . \"$openpype_root/tools/create_env.sh\" || { echo -e \"${BIRed}!!!${RST} Poetry installation failed\"; return; }\n  fi\n\n  echo -e \"${BIGreen}>>>${RST} Generating zip from current sources ...\"\n  export PYTHONPATH=\"$openpype_root:$PYTHONPATH\"\n  export OPENPYPE_ROOT=\"$openpype_root\"\n  \"$POETRY_HOME/bin/poetry\" run python3 \"$openpype_root/tools/create_zip.py\" \"$@\"\n}\n\nmain \"$@\"\n"
  },
  {
    "path": "tools/docker_build.ps1",
    "content": "$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$repo_root = (Get-Item $script_dir).parent.FullName\n\n$env:PSModulePath = $env:PSModulePath + \";$($repo_root)\\tools\\modules\\powershell\"\n\nfunction Exit-WithCode($exitcode) {\n   # Only exit this host process if it's a child of another PowerShell parent process...\n   $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$PID\" | Select-Object -Property ParentProcessId).ParentProcessId\n   $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$parentPID\" | Select-Object -Property Name).Name\n   if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) }\n\n   exit $exitcode\n}\n\nfunction Restore-Cwd() {\n    $tmp_current_dir = Get-Location\n    if (\"$tmp_current_dir\" -ne \"$current_dir\") {\n        Write-Color -Text \">>> \", \"Restoring current directory\" -Color Green, Gray\n        Set-Location -Path $current_dir\n    }\n}\n\nfunction Get-Container {\n    if (-not (Test-Path -PathType Leaf -Path \"$($repo_root)\\build\\docker-image.id\")) {\n        Write-Color -Text \"!!! \", \"Docker command failed, cannot find image id.\" -Color Red, Yellow\n        Restore-Cwd\n        Exit-WithCode 1\n    }\n    $id = Get-Content \"$($repo_root)\\build\\docker-image.id\"\n    Write-Color -Text \">>> \", \"Creating container from image id \", \"[\", $id, \"]\" -Color Green, Gray, White, Cyan, White\n    $cid = docker create $id bash\n    if ($LASTEXITCODE -ne 0) {\n        Write-Color -Text \"!!! \", \"Cannot create container.\" -Color Red, Yellow\n        Restore-Cwd\n        Exit-WithCode 1\n    }\n    return $cid\n}\n\nfunction Change-Cwd() {\n    Set-Location -Path $repo_root\n}\n\nfunction New-DockerBuild {\n    $version_file = Get-Content -Path \"$($repo_root)\\openpype\\version.py\"\n    $result = [regex]::Matches($version_file, '__version__ = \"(?<version>\\d+\\.\\d+.\\d+.*)\"')\n    $openpype_version = $result[0].Groups['version'].Value\n    $startTime = [int][double]::Parse((Get-Date -UFormat %s))\n    Write-Color -Text \">>> \", \"Building OpenPype using Docker ...\" -Color Green, Gray, White\n    $variant = $args[0]\n    if ($variant.Length -eq 0) {\n        $dockerfile = \"$($repo_root)\\Dockerfile\"\n    } else {\n        $dockerfile = \"$( $repo_root )\\Dockerfile.$variant\"\n    }\n    if (-not (Test-Path -PathType Leaf -Path $dockerfile)) {\n        Write-Color -Text \"!!! \", \"Dockerfile for specifed platform \", \"[\", $variant, \"]\", \"doesn't exist.\" -Color Red, Yellow, Cyan, White, Cyan, Yellow\n        Restore-Cwd\n        Exit-WithCode 1\n    }\n    Write-Color -Text \">>> \", \"Using Dockerfile for \", \"[ \", $variant, \" ]\" -Color Green, Gray, White, Cyan, White\n\n    $build_dir = \"$($repo_root)\\build\"\n    if (-not(Test-Path $build_dir)) {\n        New-Item -ItemType Directory -Path $build_dir\n    }\n    Write-Color -Text \"--- \", \"Cleaning build directory ...\" -Color Yellow, Gray\n    try {\n        Remove-Item -Recurse -Force \"$($build_dir)\\*\"\n    } catch {\n        Write-Color -Text \"!!! \", \"Cannot clean build directory, possibly because process is using it.\" -Color Red, Gray\n        Write-Color -Text $_.Exception.Message -Color Red\n        Exit-WithCode 1\n    }\n\n    Write-Color -Text \">>> \", \"Running Docker build ...\" -Color Green, Gray, White\n    docker build --pull --iidfile $repo_root/build/docker-image.id --build-arg BUILD_DATE=$(Get-Date -UFormat %Y-%m-%dT%H:%M:%SZ) --build-arg VERSION=$openpype_version -t pypeclub/openpype:$openpype_version -f $dockerfile .\n    if ($LASTEXITCODE -ne 0) {\n        Write-Color -Text \"!!! \", \"Docker command failed.\", $LASTEXITCODE -Color Red, Yellow, Red\n        Restore-Cwd\n        Exit-WithCode 1\n    }\n    Write-Color -Text \">>> \", \"Copying build from container ...\" -Color Green, Gray, White\n    $cid = Get-Container\n\n    docker cp \"$($cid):/opt/openpype/build/exe.linux-x86_64-3.9\" \"$($repo_root)/build\"\n    docker cp \"$($cid):/opt/openpype/build/build.log\" \"$($repo_root)/build\"\n\n    $endTime = [int][double]::Parse((Get-Date -UFormat %s))\n    try {\n        New-BurntToastNotification -AppLogo \"$openpype_root/openpype/resources/icons/openpype_icon.png\" -Text \"OpenPype build complete!\", \"All done in $( $endTime - $startTime ) secs. You will find OpenPype and build log in build directory.\"\n    } catch {}\n    Write-Color -Text \"*** \", \"All done in \", $($endTime - $startTime), \" secs. You will find OpenPype and build log in \", \"'.\\build'\", \" directory.\" -Color Green, Gray, White, Gray, White, Gray\n}\n\nChange-Cwd\nNew-DockerBuild $ARGS\n"
  },
  {
    "path": "tools/docker_build.sh",
    "content": "#!/usr/bin/env bash\n\n# Colors for terminal\n\nRST='\\033[0m'             # Text Reset\nBIGreen='\\033[1;92m'      # Green\nBIYellow='\\033[1;93m'     # Yellow\nBIRed='\\033[1;91m'        # Red\n\n##############################################################################\n# Return absolute path\n# Globals:\n#   None\n# Arguments:\n#   Path to resolve\n# Returns:\n#   None\n###############################################################################\nrealpath () {\n  echo $(cd $(dirname \"$1\"); pwd)/$(basename \"$1\")\n}\n\ncreate_container () {\n  if [ ! -f \"$openpype_root/build/docker-image.id\" ]; then\n    echo -e \"${BIRed}!!!${RST} Docker command failed, cannot find image id.\"\n    exit 1\n  fi\n  local id=$(<\"$openpype_root/build/docker-image.id\")\n  echo -e \"${BIYellow}---${RST} Creating container from $id ...\"\n  cid=\"$(docker create $id bash)\"\n  if [ $? -ne 0 ] ; then\n    echo -e \"${BIRed}!!!${RST} Cannot create container.\"\n    exit 1\n  fi\n}\n\nretrieve_build_log () {\n  create_container\n  echo -e \"${BIYellow}***${RST} Copying build log to ${BIWhite}$openpype_root/build/build.log${RST}\"\n  docker cp \"$cid:/opt/openpype/build/build.log\" \"$openpype_root/build\"\n}\n\nopenpype_root=$(realpath $(dirname $(dirname \"${BASH_SOURCE[0]}\")))\n\n\nif [ -z $1 ]; then\n    dockerfile=\"Dockerfile\"\nelse\n  dockerfile=\"Dockerfile.$1\"\n  if [ ! -f \"$openpype_root/$dockerfile\" ]; then\n    echo -e \"${BIRed}!!!${RST} Dockerfile for specifed platform ${BIWhite}$1${RST} doesn't exist.\"\n    exit 1\n  else\n    echo -e \"${BIGreen}>>>${RST} Using Dockerfile for ${BIWhite}$1${RST} ...\"\n  fi\nfi\n\n# Main\nmain () {\n  openpype_root=$(realpath $(dirname $(dirname \"${BASH_SOURCE[0]}\")))\n  pushd \"$openpype_root\" > /dev/null || return > /dev/null\n\n  echo -e \"${BIYellow}---${RST} Cleaning build directory ...\"\n  rm -rf \"$openpype_root/build\" && mkdir \"$openpype_root/build\" > /dev/null\n\n  local version_command=\"import os;exec(open(os.path.join('$openpype_root', 'openpype', 'version.py')).read());print(__version__);\"\n  local openpype_version=\"$(python3 <<< ${version_command})\"\n\n  echo -e \"${BIGreen}>>>${RST} Running docker build ...\"\n  # docker build --pull --no-cache -t pypeclub/openpype:$openpype_version .\n  docker build --pull --iidfile $openpype_root/build/docker-image.id --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') --build-arg VERSION=$openpype_version -t pypeclub/openpype:$openpype_version -f $dockerfile .\n  if [ $? -ne 0 ] ; then\n    echo $?\n    echo -e \"${BIRed}!!!${RST} Docker build failed.\"\n    retrieve_build_log\n    return 1\n  fi\n\n  echo -e \"${BIGreen}>>>${RST} Copying build from container ...\"\n  create_container\n  echo -e \"${BIYellow}---${RST} Copying ...\"\n  docker cp \"$cid:/opt/openpype/build/exe.linux-x86_64-3.9\" \"$openpype_root/build\"\n  docker cp \"$cid:/opt/openpype/build/build.log\" \"$openpype_root/build\"\n  if [ $? -ne 0 ] ; then\n    echo -e \"${BIRed}!!!${RST} Copying failed.\"\n    return 1\n  fi\n\n  echo -e \"${BIGreen}>>>${RST} Fixing user ownership ...\"\n  local username=\"$(logname)\"\n  chown -R $username ./build\n\n  echo -e \"${BIGreen}>>>${RST} All done, you can delete container:\"\n  echo -e \"${BIYellow}$cid${RST}\"\n}\n\nreturn_code=0\nmain || return_code=$?\nexit $return_code\n"
  },
  {
    "path": "tools/fetch_thirdparty_libs.ps1",
    "content": "<#\n.SYNOPSIS\n  Download and extract third-party dependencies for OpenPype.\n\n.DESCRIPTION\n  This will download third-party dependencies specified in pyproject.toml\n  and extract them to vendor/bin folder.\n\n.EXAMPLE\n\nPS> .\\fetch_thirdparty_libs.ps1\n\n#>\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n# Install PSWriteColor to support colorized output to terminal\n$env:PSModulePath = $env:PSModulePath + \";$($openpype_root)\\tools\\modules\\powershell\"\n\n$env:_INSIDE_OPENPYPE_TOOL = \"1\"\n\nif (-not (Test-Path 'env:POETRY_HOME')) {\n    $env:POETRY_HOME = \"$openpype_root\\.poetry\"\n}\n\nSet-Location -Path $openpype_root\n\nWrite-Color -Text \">>> \", \"Reading Poetry ... \" -Color Green, Gray -NoNewline\nif (-not (Test-Path -PathType Container -Path \"$($env:POETRY_HOME)\\bin\")) {\n    Write-Color -Text \"NOT FOUND\" -Color Yellow\n    Write-Color -Text \"*** \", \"We need to install Poetry create virtual env first ...\" -Color Yellow, Gray\n    & \"$openpype_root\\tools\\create_env.ps1\"\n} else {\n    Write-Color -Text \"OK\" -Color Green\n}\n$startTime = [int][double]::Parse((Get-Date -UFormat %s))\n& \"$($env:POETRY_HOME)\\bin\\poetry\" run python \"$($openpype_root)\\tools\\fetch_thirdparty_libs.py\"\n$endTime = [int][double]::Parse((Get-Date -UFormat %s))\nSet-Location -Path $current_dir\ntry\n{\n    New-BurntToastNotification -AppLogo \"$openpype_root/openpype/resources/icons/openpype_icon.png\" -Text \"OpenPype\", \"Dependencies downloaded\", \"All done in $( $endTime - $startTime ) secs.\"\n} catch {}"
  },
  {
    "path": "tools/fetch_thirdparty_libs.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Fetch, verify and process third-party dependencies of OpenPype.\n\nThose should be defined in `pyproject.toml` in OpenPype sources root.\n\n\"\"\"\nimport os\nimport sys\nimport toml\nimport shutil\nfrom pathlib import Path\nfrom urllib.parse import urlparse\nimport requests\nimport enlighten\nimport platform\nimport blessed\nimport tempfile\nimport math\nimport hashlib\nimport tarfile\nimport zipfile\nimport time\nimport subprocess\n\n\nterm = blessed.Terminal()\nmanager = enlighten.get_manager()\nhash_buffer_size = 65536\n\n\ndef sha256_sum(filename: Path):\n    \"\"\"Calculate sha256 hash for given file.\n\n    Args:\n        filename (Path): path to file.\n\n    Returns:\n        str: hex hash.\n\n    \"\"\"\n    _hash = hashlib.sha256()\n    with open(filename, 'rb', buffering=0) as f:\n        buffer = bytearray(128 * 1024)\n        mv = memoryview(buffer)\n        for n in iter(lambda: f.readinto(mv), 0):\n            _hash.update(mv[:n])\n    return _hash.hexdigest()\n\n\ndef _print(msg: str, message_type: int = 0) -> None:\n    \"\"\"Print message to console.\n\n    Args:\n        msg (str): message to print\n        message_type (int): type of message (0 info, 1 error, 2 note)\n\n    \"\"\"\n    if message_type == 0:\n        header = term.aquamarine3(\">>> \")\n    elif message_type == 1:\n        header = term.orangered2(\"!!! \")\n    elif message_type == 2:\n        header = term.tan1(\"... \")\n    else:\n        header = term.darkolivegreen3(\"--- \")\n\n    print(f\"{header}{msg}\")\n\n\ndef _pip_install(openpype_root, package, version=None):\n    arg = None\n    if package and version:\n        arg = f\"{package}=={version}\"\n    elif package:\n        arg = package\n\n    if not arg:\n        _print(\"Couldn't find package to install\")\n        sys.exit(1)\n\n    _print(f\"We'll install {arg}\")\n\n    python_vendor_dir = openpype_root / \"vendor\" / \"python\"\n    try:\n        subprocess.run(\n            [\n                sys.executable,\n                \"-m\", \"pip\", \"install\", \"--upgrade\", arg,\n                \"-t\", str(python_vendor_dir)\n            ],\n            check=True,\n            stdout=subprocess.DEVNULL\n        )\n    except subprocess.CalledProcessError as e:\n        _print(f\"Error during {package} installation.\", 1)\n        _print(str(e), 1)\n        sys.exit(1)\n\n\ndef install_qtbinding(pyproject, openpype_root, platform_name):\n    _print(\"Handling Qt binding framework ...\")\n    qtbinding_def = pyproject[\"openpype\"][\"qtbinding\"][platform_name]\n    package = qtbinding_def[\"package\"]\n    version = qtbinding_def.get(\"version\")\n    _pip_install(openpype_root, package, version)\n\n    python_vendor_dir = openpype_root / \"vendor\" / \"python\"\n\n    # Remove libraries for QtSql which don't have available libraries\n    #   by default and Postgre library would require to modify rpath of\n    #   dependency\n    if platform_name == \"darwin\":\n        sqldrivers_dir = (\n            python_vendor_dir / package / \"Qt\" / \"plugins\" / \"sqldrivers\"\n        )\n        for filepath in sqldrivers_dir.iterdir():\n            os.remove(str(filepath))\n\n\ndef install_runtime_dependencies(pyproject, openpype_root):\n    _print(\"Installing Runtime Dependencies ...\")\n    runtime_deps = pyproject[\"openpype\"][\"runtime-deps\"]\n    for package, version in runtime_deps.items():\n        _pip_install(openpype_root, package, version)\n\n\ndef install_thirdparty(pyproject, openpype_root, platform_name):\n    _print(\"Processing third-party dependencies ...\")\n    try:\n        thirdparty = pyproject[\"openpype\"][\"thirdparty\"]\n    except AttributeError:\n        _print(\"No third-party libraries specified in pyproject.toml\", 1)\n        sys.exit(1)\n\n    for k, v in thirdparty.items():\n        _print(f\"processing {k}\")\n        destination_path = openpype_root / \"vendor\" / \"bin\" / k\n\n        if not v.get(platform_name):\n            _print((\"missing definition for current \"\n                    f\"platform [ {platform_name} ]\"), 2)\n            _print(\"trying to get universal url for all platforms\")\n            url = v.get(\"url\")\n            if not url:\n                _print(\"cannot get url for all platforms\", 1)\n                _print((f\"Warning: {k} is not installed for current platform \"\n                       \"and it might be missing in the build\"), 1)\n                continue\n        else:\n            url = v.get(platform_name).get(\"url\")\n            destination_path = destination_path / platform_name\n\n        parsed_url = urlparse(url)\n\n        # check if file is already extracted in /vendor/bin\n        if destination_path.exists():\n            _print(\"destination path already exists, deleting ...\", 2)\n            if destination_path.is_dir():\n                try:\n                    shutil.rmtree(destination_path)\n                except OSError as e:\n                    _print(\"cannot delete folder.\", 1)\n                    raise SystemExit(e)\n\n        # download file\n        _print(f\"Downloading {url} ...\")\n        with tempfile.TemporaryDirectory() as temp_dir:\n            temp_file = Path(temp_dir) / Path(parsed_url.path).name\n\n            r = requests.get(url, stream=True)\n            content_len = int(r.headers.get('Content-Length', '0')) or None\n            with manager.counter(\n                color='green',\n                total=content_len and math.ceil(content_len / 2 ** 20),\n                unit='MiB',\n                leave=False\n            ) as counter:\n                with open(temp_file, 'wb', buffering=2 ** 24) as file_handle:\n                    for chunk in r.iter_content(chunk_size=2 ** 20):\n                        file_handle.write(chunk)\n                        counter.update()\n\n            # get file with checksum\n            _print(\"Calculating sha256 ...\", 2)\n            calc_checksum = sha256_sum(temp_file)\n\n            if v.get(platform_name):\n                item_hash = v.get(platform_name).get(\"hash\")\n            else:\n                item_hash = v.get(\"hash\")\n\n            if item_hash != calc_checksum:\n                _print(\"Downloaded files checksum invalid.\")\n                sys.exit(1)\n\n            _print(\"File OK\", 3)\n            if not destination_path.exists():\n                destination_path.mkdir(parents=True)\n\n            # extract to destination\n            archive_type = temp_file.suffix.lstrip(\".\")\n            _print(f\"Extracting {archive_type} file to {destination_path}\")\n            if archive_type in ['zip']:\n                zip_file = zipfile.ZipFile(temp_file)\n                zip_file.extractall(destination_path)\n                zip_file.close()\n\n            elif archive_type in [\n                'tar', 'tgz', 'tar.gz', 'tar.xz', 'tar.bz2'\n            ]:\n                if archive_type == 'tar':\n                    tar_type = 'r:'\n                elif archive_type.endswith('xz'):\n                    tar_type = 'r:xz'\n                elif archive_type.endswith('gz'):\n                    tar_type = 'r:gz'\n                elif archive_type.endswith('bz2'):\n                    tar_type = 'r:bz2'\n                else:\n                    tar_type = 'r:*'\n                try:\n                    tar_file = tarfile.open(temp_file, tar_type)\n                except tarfile.ReadError:\n                    raise SystemExit(\"corrupted archive\")\n                tar_file.extractall(destination_path)\n                tar_file.close()\n            _print(\"Extraction OK\", 3)\n\n\ndef main():\n    start_time = time.time_ns()\n    openpype_root = Path(os.path.dirname(__file__)).parent\n    pyproject = toml.load(openpype_root / \"pyproject.toml\")\n    platform_name = platform.system().lower()\n    install_qtbinding(pyproject, openpype_root, platform_name)\n    install_runtime_dependencies(pyproject, openpype_root)\n    install_thirdparty(pyproject, openpype_root, platform_name)\n    end_time = time.time_ns()\n    total_time = (end_time - start_time) / 1000000000\n    _print(f\"Downloading and extracting took {total_time} secs.\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/fetch_thirdparty_libs.sh",
    "content": "#!/usr/bin/env bash\n\nart () {\n  cat <<-EOF\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~·   ··   ~2p.  ··  ····  ·  ·\n    ·Ppo · .pPO3Op.· · O:· · · ·\n   .3Pp · oP3'· 'P33· · 4 ··   ·  ·   · ·· ·  ·  ·\n  ·~OP    3PO·  .Op3    : · ··  _____  _____  _____\n  ·P3O  · oP3oP3O3P' · · ·   · /    /·/    /·/    /\n   O3:·   O3p~ ·       ·:· · ·/____/·/____/ /____/\n   'P ·   3p3·  oP3~· ·.P:· ·  · ··  ·   · ·· ·  ·  ·\n  · ':  · Po'  ·Opo'· .3O· .  o[ by Pype Club ]]]==- - - ·  ·\n    · '_ ..  ·    . _OP3··  ·  ·https://openpype.io·· ·\n         ~P3·OPPPO3OP~ · ··  ·\n           ·  ' '· ·  ·· · · · ··  ·\n\nEOF\n}\n\n# Colors for terminal\n\nRST='\\033[0m'             # Text Reset\n\n# Regular Colors\nBlack='\\033[0;30m'        # Black\nRed='\\033[0;31m'          # Red\nGreen='\\033[0;32m'        # Green\nYellow='\\033[0;33m'       # Yellow\nBlue='\\033[0;34m'         # Blue\nPurple='\\033[0;35m'       # Purple\nCyan='\\033[0;36m'         # Cyan\nWhite='\\033[0;37m'        # White\n\n# Bold\nBBlack='\\033[1;30m'       # Black\nBRed='\\033[1;31m'         # Red\nBGreen='\\033[1;32m'       # Green\nBYellow='\\033[1;33m'      # Yellow\nBBlue='\\033[1;34m'        # Blue\nBPurple='\\033[1;35m'      # Purple\nBCyan='\\033[1;36m'        # Cyan\nBWhite='\\033[1;37m'       # White\n\n# Bold High Intensity\nBIBlack='\\033[1;90m'      # Black\nBIRed='\\033[1;91m'        # Red\nBIGreen='\\033[1;92m'      # Green\nBIYellow='\\033[1;93m'     # Yellow\nBIBlue='\\033[1;94m'       # Blue\nBIPurple='\\033[1;95m'     # Purple\nBICyan='\\033[1;96m'       # Cyan\nBIWhite='\\033[1;97m'      # White\n\n\n##############################################################################\n# Return absolute path\n# Globals:\n#   None\n# Arguments:\n#   Path to resolve\n# Returns:\n#   None\n###############################################################################\nrealpath () {\n  echo $(cd $(dirname \"$1\"); pwd)/$(basename \"$1\")\n}\n\n# Main\nmain () {\n  echo -e \"${BGreen}\"\n  art\n  echo -e \"${RST}\"\n\n  # Directories\n  openpype_root=$(realpath $(dirname $(dirname \"${BASH_SOURCE[0]}\")))\n\n  _inside_openpype_tool=\"1\"\n\n  if [[ -z $POETRY_HOME ]]; then\n    export POETRY_HOME=\"$openpype_root/.poetry\"\n  fi\n\n  echo -e \"${BIGreen}>>>${RST} Reading Poetry ... \\c\"\n  if [ -f \"$POETRY_HOME/bin/poetry\" ]; then\n    echo -e \"${BIGreen}OK${RST}\"\n  else\n    echo -e \"${BIYellow}NOT FOUND${RST}\"\n    echo -e \"${BIYellow}***${RST} We need to install Poetry and virtual env ...\"\n    . \"$openpype_root/tools/create_env.sh\" || { echo -e \"${BIRed}!!!${RST} Poetry installation failed\"; return; }\n  fi\n\n  pushd \"$openpype_root\" > /dev/null || return > /dev/null\n\n  echo -e \"${BIGreen}>>>${RST} Running Pype tool ...\"\n  \"$POETRY_HOME/bin/poetry\" run python \"$openpype_root/tools/fetch_thirdparty_libs.py\"\n}\n\nmain\n"
  },
  {
    "path": "tools/get_python_packages_info.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Get version and license information on used Python packages.\n\nThis is getting over all packages installed with Poetry and printing out\ntheir name, version and available license information from PyPi in Markdown\ntable format.\n\nUsage:\n    ./.poetry/bin/poetry run python ./tools/get_python_packages_info.py\n\n\"\"\"\n\nimport toml\nimport requests\n\n\npackages = []\n\n# define column headers\npackage_header = \"Package\"\nversion_header = \"Version\"\nlicense_header = \"License\"\n\nname_col_width = len(package_header)\nversion_col_width = len(version_header)\nlicense_col_width = len(license_header)\n\n# read lock file to get packages\nwith open(\"poetry.lock\", \"r\") as fb:\n    lock_content = toml.load(fb)\n\n    for package in lock_content[\"package\"]:\n        # query pypi for license information\n        url = f\"https://pypi.org/pypi/{package['name']}/json\"\n        response = requests.get(\n            f\"https://pypi.org/pypi/{package['name']}/json\")\n        package_data = response.json()\n        version = package.get(\"version\") or \"N/A\"\n        try:\n            package_license = package_data[\"info\"].get(\"license\") or \"N/A\"\n        except KeyError:\n            package_license = \"N/A\"\n\n        if len(package_license) > 64:\n            package_license = f\"{package_license[:32]}...\"\n        packages.append(\n            (\n                package[\"name\"],\n                version,\n                package_license\n            )\n        )\n\n        # update column width based on max string length\n        if len(package[\"name\"]) > name_col_width:\n            name_col_width = len(package[\"name\"])\n        if len(version) > version_col_width:\n            version_col_width = len(version)\n        if len(package_license) > license_col_width:\n            license_col_width = len(package_license)\n\n# pad columns\nname_col_width += 2\nversion_col_width += 2\nlicense_col_width += 2\n\n# print table header\nprint((f\"|{package_header.center(name_col_width)}\"\n       f\"|{version_header.center(version_col_width)}\"\n       f\"|{license_header.center(license_col_width)}|\"))\n\nprint(\n    \"|\" + (\"-\" * len(package_header.center(name_col_width))) +\n    \"|\" + (\"-\" * len(version_header.center(version_col_width))) +\n    \"|\" + (\"-\" * len(license_header.center(license_col_width))) + \"|\")\n\n# print rest of the table\nfor package in packages:\n    print((\n        f\"|{package[0].center(name_col_width)}\"\n        f\"|{package[1].center(version_col_width)}\"\n        f\"|{package[2].center(license_col_width)}|\"\n    ))\n"
  },
  {
    "path": "tools/make_docs.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script to update OpenPype Sphinx sources.\n\n.DESCRIPTION\n  This script will run apidoc over OpenPype sources and generate new source rst\n  files for documentation. Then it will run build_sphinx to create test html\n  documentation build.\n\n.EXAMPLE\n\nPS> .\\make_docs.ps1\n\n#>\n\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n$env:_INSIDE_OPENPYPE_TOOL = \"1\"\n\nif (-not (Test-Path 'env:POETRY_HOME')) {\n    $env:POETRY_HOME = \"$openpype_root\\.poetry\"\n}\n\nSet-Location -Path $openpype_root\n\n\n$art = @\"\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~.   ..   ~2p.  ..  ....  .  .\n    .Ppo . .pPO3Op.. . O:. . . .\n   .3Pp . oP3'. 'P33. . 4 ..   .  .   . .. .  .  .\n  .~OP    3PO.  .Op3    : . ..  _____  _____  _____\n  .P3O  . oP3oP3O3P' . . .   . /    /./    /./    /\n   O3:.   O3p~ .       .:. . ./____/./____/ /____/\n   'P .   3p3.  oP3~. ..P:. .  . ..  .   . .. .  .  .\n  . ':  . Po'  .Opo'. .3O. .  o[ by Pype Club ]]]==- - - .  .\n    . '_ ..  .    . _OP3..  .  .https://openpype.io.. .\n         ~P3.OPPPO3OP~ . ..  .\n           .  ' '. .  .. . . . ..  .\n\n\"@\n\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n# Install PSWriteColor to support colorized output to terminal\n$env:PSModulePath = $env:PSModulePath + \";$($openpype_root)\\tools\\modules\\powershell\"\n\nWrite-Host $art -ForegroundColor DarkGreen\n\nWrite-Color -Text \">>> \", \"Reading Poetry ... \" -Color Green, Gray -NoNewline\nif (-not (Test-Path -PathType Container -Path \"$($env:POETRY_HOME)\\bin\")) {\n    Write-Color -Text \"NOT FOUND\" -Color Yellow\n    Install-Poetry\n    Write-Color -Text \"INSTALLED\" -Color Cyan\n} else {\n    Write-Color -Text \"OK\" -Color Green\n}\n\nWrite-Color -Text \"... \", \"This will not overwrite existing source rst files, only scan and add new.\" -Color Yellow, Gray\nSet-Location -Path $openpype_root\nWrite-Color -Text \">>> \", \"Running apidoc ...\" -Color Green, Gray\n& \"$env:POETRY_HOME\\bin\\poetry\" run sphinx-apidoc -M -e -d 10  --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o \"$($openpype_root)\\docs\\source\" igniter\n& \"$env:POETRY_HOME\\bin\\poetry\" run sphinx-apidoc.exe -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o \"$($openpype_root)\\docs\\source\" openpype vendor, openpype\\vendor\n\nWrite-Color -Text \">>> \", \"Building html ...\" -Color Green, Gray\n& \"$env:POETRY_HOME\\bin\\poetry\" run python \"$($openpype_root)\\setup.py\" build_sphinx\nSet-Location -Path $current_dir\n"
  },
  {
    "path": "tools/make_docs.sh",
    "content": "#!/usr/bin/env bash\n\n# This script will run apidoc over OpenPype sources and generate new source rst\n# files for documentation. Then it will run build_sphinx to create test html\n# documentation build.\n\nart () {\n  cat <<-EOF\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~·   ··   ~2p.  ··  ····  ·  ·\n    ·Ppo · .pPO3Op.· · O:· · · ·\n   .3Pp · oP3'· 'P33· · 4 ··   ·  ·   · ·· ·  ·  ·\n  ·~OP    3PO·  .Op3    : · ··  _____  _____  _____\n  ·P3O  · oP3oP3O3P' · · ·   · /    /·/    /·/    /\n   O3:·   O3p~ ·       ·:· · ·/____/·/____/ /____/\n   'P ·   3p3·  oP3~· ·.P:· ·  · ··  ·   · ·· ·  ·  ·\n  · ':  · Po'  ·Opo'· .3O· .  o[ by Pype Club ]]]==- - - ·  ·\n    · '_ ..  ·    . _OP3··  ·  ·https://openpype.io·· ·\n         ~P3·OPPPO3OP~ · ··  ·\n           ·  ' '· ·  ·· · · · ··  ·\n\nEOF\n}\n\n# Colors for terminal\n\nRST='\\033[0m'             # Text Reset\n\n# Regular Colors\nBlack='\\033[0;30m'        # Black\nRed='\\033[0;31m'          # Red\nGreen='\\033[0;32m'        # Green\nYellow='\\033[0;33m'       # Yellow\nBlue='\\033[0;34m'         # Blue\nPurple='\\033[0;35m'       # Purple\nCyan='\\033[0;36m'         # Cyan\nWhite='\\033[0;37m'        # White\n\n# Bold\nBBlack='\\033[1;30m'       # Black\nBRed='\\033[1;31m'         # Red\nBGreen='\\033[1;32m'       # Green\nBYellow='\\033[1;33m'      # Yellow\nBBlue='\\033[1;34m'        # Blue\nBPurple='\\033[1;35m'      # Purple\nBCyan='\\033[1;36m'        # Cyan\nBWhite='\\033[1;37m'       # White\n\n# Bold High Intensity\nBIBlack='\\033[1;90m'      # Black\nBIRed='\\033[1;91m'        # Red\nBIGreen='\\033[1;92m'      # Green\nBIYellow='\\033[1;93m'     # Yellow\nBIBlue='\\033[1;94m'       # Blue\nBIPurple='\\033[1;95m'     # Purple\nBICyan='\\033[1;96m'       # Cyan\nBIWhite='\\033[1;97m'      # White\n\n\n##############################################################################\n# Return absolute path\n# Globals:\n#   None\n# Arguments:\n#   Path to resolve\n# Returns:\n#   None\n###############################################################################\nrealpath () {\n  echo $(cd $(dirname \"$1\"); pwd)/$(basename \"$1\")\n}\n\n# Main\nmain () {\n  echo -e \"${BGreen}\"\n  art\n  echo -e \"${RST}\"\n\n  # Directories\n  openpype_root=$(realpath $(dirname $(dirname \"${BASH_SOURCE[0]}\")))\n\n  _inside_openpype_tool=\"1\"\n\n  if [[ -z $POETRY_HOME ]]; then\n    export POETRY_HOME=\"$openpype_root/.poetry\"\n  fi\n\n  echo -e \"${BIGreen}>>>${RST} Reading Poetry ... \\c\"\n  if [ -f \"$POETRY_HOME/bin/poetry\" ]; then\n    echo -e \"${BIGreen}OK${RST}\"\n  else\n    echo -e \"${BIYellow}NOT FOUND${RST}\"\n    echo -e \"${BIYellow}***${RST} We need to install Poetry and virtual env ...\"\n    . \"$openpype_root/tools/create_env.sh\" || { echo -e \"${BIRed}!!!${RST} Poetry installation failed\"; return; }\n  fi\n\n  pushd \"$openpype_root\" > /dev/null || return > /dev/null\n\n  echo -e \"${BIGreen}>>>${RST} Running apidoc ...\"\n  \"$POETRY_HOME/bin/poetry\" run sphinx-apidoc -M -e -d 10  --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o \"$openpype_root/docs/source\" igniter\n  \"$POETRY_HOME/bin/poetry\" run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o \"$openpype_root/docs/source\" openpype vendor, openpype\\vendor\n\n  echo -e \"${BIGreen}>>>${RST} Building html ...\"\n  \"$POETRY_HOME/bin/poetry\" run python3 \"$openpype_root/setup.py\" build_sphinx\n}\n\nmain\n"
  },
  {
    "path": "tools/openpype_console.bat",
    "content": "goto comment\r\nSYNOPSIS\r\n  Helper script running scripts through the OpenPype environment.\r\n\r\nDESCRIPTION\r\n  This script is usually used as a replacement for building when tested farm integration like Deadline.\r\n\r\nEXAMPLE\r\n\r\ncmd> .\\openpype_console.bat path/to/python_script.py\r\n:comment\r\n\r\ncd \"%~dp0\\..\"\r\necho %OPENPYPE_MONGO%\r\n.poetry\\bin\\poetry.exe run python start.py %*\r\n"
  },
  {
    "path": "tools/pack_project.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script OpenPype Packing project.\n\n.DESCRIPTION\n  Once you are happy with the project and want to preserve it for future work, just change the project name on line 38 and copy the file into .\\OpenPype\\tools. Then use the cmd form .EXAMPLE\n\n.EXAMPLE\n\nPS> .\\tools\\run_pack_project.ps1\n\n#>\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n$env:_INSIDE_OPENPYPE_TOOL = \"1\"\n\n# make sure Poetry is in PATH\nif (-not (Test-Path 'env:POETRY_HOME')) {\n    $env:POETRY_HOME = \"$openpype_root\\.poetry\"\n}\n$env:PATH = \"$($env:PATH);$($env:POETRY_HOME)\\bin\"\n\nSet-Location -Path $openpype_root\n\nWrite-Host \">>> \" -NoNewline -ForegroundColor Green\nWrite-Host \"Reading Poetry ... \" -NoNewline\nif (-not (Test-Path -PathType Container -Path \"$($env:POETRY_HOME)\\bin\")) {\n    Write-Host \"NOT FOUND\" -ForegroundColor Yellow\n    Write-Host \"*** \" -NoNewline -ForegroundColor Yellow\n    Write-Host \"We need to install Poetry create virtual env first ...\"\n    & \"$openpype_root\\tools\\create_env.ps1\"\n} else {\n    Write-Host \"OK\" -ForegroundColor Green\n}\n\n& \"$($env:POETRY_HOME)\\bin\\poetry\" run python \"$($openpype_root)\\start.py\" pack-project --project $ARGS\nSet-Location -Path $current_dir"
  },
  {
    "path": "tools/parse_pyproject.py",
    "content": "# -*- coding:  utf-8 -*-\n\"\"\"Parse pyproject.toml and return its values.\n\nUseful for shell scripts to know more about OpenPype build.\n\"\"\"\nimport sys\nimport os\nimport toml\nfrom pathlib import Path\nimport click\n\n\n@click.command()\n@click.argument(\"keys\", nargs=-1, type=click.STRING)\ndef main(keys):\n    \"\"\"Get values from `pyproject.toml`.\n\n    You can specify dot separated keys from `pyproject.toml`\n    as arguments and this script will return them on separate\n    lines. If key doesn't exists, None is returned.\n\n    \"\"\"\n    openpype_root = Path(os.path.dirname(__file__)).parent\n    py_project = toml.load(openpype_root / \"pyproject.toml\")\n    for q in keys:\n        query = q.split(\".\")\n        data = py_project\n\n        for k in query:\n            if isinstance(data, list):\n                try:\n                    data = data[int(k)]\n                except IndexError:\n                    print(\"None\")\n                    sys.exit()\n                continue\n\n            if isinstance(data, dict):\n                data = data.get(k)\n        print(data)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/run_documentation.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script to run Docusaurus for easy editing of OpenPype documentation.\n\n.DESCRIPTION\n  This script is using `yarn` package manager to run Docusaurus. If you don't\n  have `yarn`, install Node.js (https://nodejs.org/) and then run:\n\n  npm install -g yarn\n\n  It take some time to run this script. If all is successful you should see\n  new browser window with OpenPype documentation. All changes is markdown files\n  under .\\website should be immediately seen in browser.\n\n.EXAMPLE\n\nPS> .\\run_documentation.ps1\n\n#>\n\n$art = @\"\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~.   ..   ~2p.  ..  ....  .  .\n    .Ppo . .pPO3Op.. . O:. . . .\n   .3Pp . oP3'. 'P33. . 4 ..   .  .   . .. .  .  .\n  .~OP    3PO.  .Op3    : . ..  _____  _____  _____\n  .P3O  . oP3oP3O3P' . . .   . /    /./    /./    /\n   O3:.   O3p~ .       .:. . ./____/./____/ /____/\n   'P .   3p3.  oP3~. ..P:. .  . ..  .   . .. .  .  .\n  . ':  . Po'  .Opo'. .3O. .  o[ by Pype Club ]]]==- - - .  .\n    . '_ ..  .    . _OP3..  .  .https://openpype.io.. .\n         ~P3.OPPPO3OP~ . ..  .\n           .  ' '. .  .. . . . ..  .\n\n\"@\n\nWrite-Host $art -ForegroundColor DarkGreen\n\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\nSet-Location $openpype_root/website\n\n& yarn install\n& yarn start\n"
  },
  {
    "path": "tools/run_mongo.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script to run mongodb.\n\n.DESCRIPTION\n  This script will detect mongodb, add it to the PATH and launch it on specified port and db location.\n\n.EXAMPLE\n\nPS> .\\run_mongo.ps1\n\n#>\n\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n# Install PSWriteColor to support colorized output to terminal\n$env:PSModulePath = $env:PSModulePath + \";$($openpype_root)\\tools\\modules\\powershell\"\n\n$art = @\"\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~.   ..   ~2p.  ..  ....  .  .\n    .Ppo . .pPO3Op.. . O:. . . .\n   .3Pp . oP3'. 'P33. . 4 ..   .  .   . .. .  .  .\n  .~OP    3PO.  .Op3    : . ..  _____  _____  _____\n  .P3O  . oP3oP3O3P' . . .   . /    /./    /./    /\n   O3:.   O3p~ .       .:. . ./____/./____/ /____/\n   'P .   3p3.  oP3~. ..P:. .  . ..  .   . .. .  .  .\n  . ':  . Po'  .Opo'. .3O. .  o[ by Pype Club ]]]==- - - .  .\n    . '_ ..  .    . _OP3..  .  .https://openpype.io.. .\n         ~P3.OPPPO3OP~ . ..  .\n           .  ' '. .  .. . . . ..  .\n\n\"@\n\nWrite-Host $art -ForegroundColor DarkGreen\n\nfunction Exit-WithCode($exitcode) {\n   # Only exit this host process if it's a child of another PowerShell parent process...\n   $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$PID\" | Select-Object -Property ParentProcessId).ParentProcessId\n   $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$parentPID\" | Select-Object -Property Name).Name\n   if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) }\n\n   exit $exitcode\n}\n\n\nfunction Find-Mongo ($preferred_version) {\n    $defaultPath = \"C:\\Program Files\\MongoDB\\Server\"\n    Write-Color -Text \">>> \", \"Detecting MongoDB ... \" -Color Green, Gray -NoNewline\n    if (-not (Get-Command \"mongod\" -ErrorAction SilentlyContinue)) {\n        if(Test-Path \"$($defaultPath)\\*\\bin\\mongod.exe\" -PathType Leaf) {\n        # we have mongo server installed on standard Windows location\n        # so we can inject it to the PATH. We'll use latest version available, or the one defined by\n        # $preferred_version.\n        $mongoVersions = Get-ChildItem -Directory 'C:\\Program Files\\MongoDB\\Server' | Sort-Object -Property {$_.Name -as [int]}\n        if(Test-Path \"$($mongoVersions[-1])\\bin\\mongod.exe\" -PathType Leaf) {\n            Write-Color -Text \"OK\" -Color Green\n            $use_version = $mongoVersions[-1]\n            foreach ($v in $mongoVersions) {\n                Write-Color -Text \"  - found [ \", $v, \" ]\" -Color Cyan, White, Cyan -NoNewLine\n                $version = Split-Path $v -Leaf\n\n                if ($preferred_version -eq $version) {\n                    Write-Color -Text \" *\" -Color Green\n                    $use_version = $v\n                } else {\n                    Write-Host \"\"\n                }\n            }\n\n            $env:PATH = \"$($env:PATH);$($use_version)\\bin\\\"\n\n            Write-Color -Text \"  - auto-added from [ \", \"$($use_version)\\bin\\mongod.exe\", \" ]\" -Color Cyan, White, Cyan\n            return \"$($use_version)\\bin\\mongod.exe\"\n        } else {\n            Write-Color -Text \"FAILED \" -Color Red  -NoNewLine\n            Write-Color -Text \"MongoDB not detected\" -Color Yellow\n            Write-Color -Text \"Tried to find it on standard location \", \"[ \", \"$($mongoVersions[-1])\\bin\\mongod.exe\", \" ]\", \" but failed.\" -Color Gray, Cyan, White, Cyan, Gray -NoNewline\n            Exit-WithCode 1\n        }\n    } else {\n        Write-Color -Text \"FAILED \", \"MongoDB not detected in PATH\" -Color Red, Yellow\n        Exit-WithCode 1\n    }\n  } else {\n        Write-Color -Text \"OK\" -Color Green\n        return Get-Command \"mongod\" -ErrorAction SilentlyContinue\n  }\n  <#\n  .SYNOPSIS\n  Function to detect mongod in path.\n  .DESCRIPTION\n  This will test presence of mongod in PATH. If it's not there, it will try\n  to find it in default install location. It support different mongo versions\n  (using latest if found). When mongod is found, path to it is added to PATH\n  #>\n}\n\n# mongodb port\n$port = 2707\n\n# path to database\n$dbpath = (Get-Item $openpype_root).parent.FullName + \"\\mongo_db_data\"\n\n$preferred_version = \"5.0\"\n\n$mongoPath = Find-Mongo $preferred_version\nWrite-Color -Text \">>> \", \"Using DB path: \", \"[ \", \"$($dbpath)\", \" ]\" -Color Green, Gray, Cyan, White, Cyan\nWrite-Color -Text \">>> \", \"Port: \", \"[ \", \"$($port)\", \" ]\" -Color Green, Gray, Cyan, White, Cyan\n\nNew-Item -ItemType Directory -Force -Path $($dbpath)\n\nStart-Process -FilePath $mongopath \"--dbpath $($dbpath) --port $($port)\" -PassThru | Out-Null\n"
  },
  {
    "path": "tools/run_mongo.sh",
    "content": "#!/usr/bin/env bash\n\n# Helper script to run mongod in the backround.\n# NOTE: we are expecting mongod is in PATH\n\n\nart () {\n  cat <<-EOF\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~·   ··   ~2p.  ··  ····  ·  ·\n    ·Ppo · .pPO3Op.· · O:· · · ·\n   .3Pp · oP3'· 'P33· · 4 ··   ·  ·   · ·· ·  ·  ·\n  ·~OP    3PO·  .Op3    : · ··  _____  _____  _____\n  ·P3O  · oP3oP3O3P' · · ·   · /    /·/    /·/    /\n   O3:·   O3p~ ·       ·:· · ·/____/·/____/ /____/\n   'P ·   3p3·  oP3~· ·.P:· ·  · ··  ·   · ·· ·  ·  ·\n  · ':  · Po'  ·Opo'· .3O· .  o[ by Pype Club ]]]==- - - ·  ·\n    · '_ ..  ·    . _OP3··  ·  ·https://openpype.io·· ·\n         ~P3·OPPPO3OP~ · ··  ·\n           ·  ' '· ·  ·· · · · ··  ·\n\nEOF\n}\n\n# Colors for terminal\n\nRST='\\033[0m'             # Text Reset\n\n# Regular Colors\nBlack='\\033[0;30m'        # Black\nRed='\\033[0;31m'          # Red\nGreen='\\033[0;32m'        # Green\nYellow='\\033[0;33m'       # Yellow\nBlue='\\033[0;34m'         # Blue\nPurple='\\033[0;35m'       # Purple\nCyan='\\033[0;36m'         # Cyan\nWhite='\\033[0;37m'        # White\n\n# Bold\nBBlack='\\033[1;30m'       # Black\nBRed='\\033[1;31m'         # Red\nBGreen='\\033[1;32m'       # Green\nBYellow='\\033[1;33m'      # Yellow\nBBlue='\\033[1;34m'        # Blue\nBPurple='\\033[1;35m'      # Purple\nBCyan='\\033[1;36m'        # Cyan\nBWhite='\\033[1;37m'       # White\n\n# Bold High Intensity\nBIBlack='\\033[1;90m'      # Black\nBIRed='\\033[1;91m'        # Red\nBIGreen='\\033[1;92m'      # Green\nBIYellow='\\033[1;93m'     # Yellow\nBIBlue='\\033[1;94m'       # Blue\nBIPurple='\\033[1;95m'     # Purple\nBICyan='\\033[1;96m'       # Cyan\nBIWhite='\\033[1;97m'      # White\n\n\n##############################################################################\n# Return absolute path\n# Globals:\n#   None\n# Arguments:\n#   Path to resolve\n# Returns:\n#   None\n###############################################################################\nrealpath () {\n  echo $(cd $(dirname \"$1\"); pwd)/$(basename \"$1\")\n}\n\n# Main\nmain () {\n  echo -e \"${BGreen}\"\n  art\n  echo -e \"${RST}\"\n\n  # Directories\n  openpype_root=$(dirname $(realpath $(dirname $(dirname \"${BASH_SOURCE[0]}\"))))\n  pushd \"$openpype_root\" > /dev/null || return > /dev/null\n\n  mongo_port=2707\n  dbpath=\"$(dirname $openpype_root)/mongo_db_data\"\n\n  echo -e \"${BIGreen}>>>${RST} Running mongodb ...\"\n  mongod --dbpath \"$dbpath\" --port $mongo_port\n  echo -e \"${BIGreen}>>>${RST} Detached to background.\"\n}\n\nmain\n"
  },
  {
    "path": "tools/run_project_manager.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script to run Project Manager.\n\n.DESCRIPTION\n\n\n.EXAMPLE\n\nPS> .\\run_project_manager.ps1\n\n#>\n\n$art = @\"\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~.   ..   ~2p.  ..  ....  .  .\n    .Ppo . .pPO3Op.. . O:. . . .\n   .3Pp . oP3'. 'P33. . 4 ..   .  .   . .. .  .  .\n  .~OP    3PO.  .Op3    : . ..  _____  _____  _____\n  .P3O  . oP3oP3O3P' . . .   . /    /./    /./    /\n   O3:.   O3p~ .       .:. . ./____/./____/ /____/\n   'P .   3p3.  oP3~. ..P:. .  . ..  .   . .. .  .  .\n  . ':  . Po'  .Opo'. .3O. .  o[ by Pype Club ]]]==- - - .  .\n    . '_ ..  .    . _OP3..  .  .https://openpype.io.. .\n         ~P3.OPPPO3OP~ . ..  .\n           .  ' '. .  .. . . . ..  .\n\n\"@\n\nWrite-Host $art -ForegroundColor DarkGreen\n\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n# Install PSWriteColor to support colorized output to terminal\n$env:PSModulePath = $env:PSModulePath + \";$($openpype_root)\\tools\\modules\\powershell\"\n\n$env:_INSIDE_OPENPYPE_TOOL = \"1\"\n\n# make sure Poetry is in PATH\nif (-not (Test-Path 'env:POETRY_HOME')) {\n    $env:POETRY_HOME = \"$openpype_root\\.poetry\"\n}\n$env:PATH = \"$($env:PATH);$($env:POETRY_HOME)\\bin\"\n\nSet-Location -Path $openpype_root\n\nWrite-Color -Text \">>> \", \"Reading Poetry ... \" -Color Green, Gray -NoNewline\nif (-not (Test-Path -PathType Container -Path \"$($env:POETRY_HOME)\\bin\")) {\n    Write-Color -Text \"NOT FOUND\" -Color Yellow\n    Install-Poetry\n    Write-Color -Text \"INSTALLED\" -Color Cyan\n} else {\n    Write-Color -Text \"OK\" -Color Green\n}\n\n& \"$env:POETRY_HOME\\bin\\poetry\" run python \"$($openpype_root)\\start.py\" projectmanager\nSet-Location -Path $current_dir\n"
  },
  {
    "path": "tools/run_projectmanager.sh",
    "content": "#!/usr/bin/env bash\n\n# Run OpenPype Settings GUI\n\n\nart () {\n  cat <<-EOF\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~·   ··   ~2p.  ··  ····  ·  ·\n    ·Ppo · .pPO3Op.· · O:· · · ·\n   .3Pp · oP3'· 'P33· · 4 ··   ·  ·   · ·· ·  ·  ·\n  ·~OP    3PO·  .Op3    : · ··  _____  _____  _____\n  ·P3O  · oP3oP3O3P' · · ·   · /    /·/    /·/    /\n   O3:·   O3p~ ·       ·:· · ·/____/·/____/ /____/\n   'P ·   3p3·  oP3~· ·.P:· ·  · ··  ·   · ·· ·  ·  ·\n  · ':  · Po'  ·Opo'· .3O· .  o[ by Pype Club ]]]==- - - ·  ·\n    · '_ ..  ·    . _OP3··  ·  ·https://openpype.io·· ·\n         ~P3·OPPPO3OP~ · ··  ·\n           ·  ' '· ·  ·· · · · ··  ·\n\nEOF\n}\n\n# Colors for terminal\n\nRST='\\033[0m'             # Text Reset\n\n# Regular Colors\nBlack='\\033[0;30m'        # Black\nRed='\\033[0;31m'          # Red\nGreen='\\033[0;32m'        # Green\nYellow='\\033[0;33m'       # Yellow\nBlue='\\033[0;34m'         # Blue\nPurple='\\033[0;35m'       # Purple\nCyan='\\033[0;36m'         # Cyan\nWhite='\\033[0;37m'        # White\n\n# Bold\nBBlack='\\033[1;30m'       # Black\nBRed='\\033[1;31m'         # Red\nBGreen='\\033[1;32m'       # Green\nBYellow='\\033[1;33m'      # Yellow\nBBlue='\\033[1;34m'        # Blue\nBPurple='\\033[1;35m'      # Purple\nBCyan='\\033[1;36m'        # Cyan\nBWhite='\\033[1;37m'       # White\n\n# Bold High Intensity\nBIBlack='\\033[1;90m'      # Black\nBIRed='\\033[1;91m'        # Red\nBIGreen='\\033[1;92m'      # Green\nBIYellow='\\033[1;93m'     # Yellow\nBIBlue='\\033[1;94m'       # Blue\nBIPurple='\\033[1;95m'     # Purple\nBICyan='\\033[1;96m'       # Cyan\nBIWhite='\\033[1;97m'      # White\n\n\n##############################################################################\n# Return absolute path\n# Globals:\n#   None\n# Arguments:\n#   Path to resolve\n# Returns:\n#   None\n###############################################################################\nrealpath () {\n  echo $(cd $(dirname \"$1\"); pwd)/$(basename \"$1\")\n}\n\n# Main\nmain () {\n\n  # Directories\n  openpype_root=$(realpath $(dirname $(dirname \"${BASH_SOURCE[0]}\")))\n\n  _inside_openpype_tool=\"1\"\n\n  if [[ -z $POETRY_HOME ]]; then\n    export POETRY_HOME=\"$openpype_root/.poetry\"\n  fi\n\n  pushd \"$openpype_root\" > /dev/null || return > /dev/null\n\n  echo -e \"${BIGreen}>>>${RST} Reading Poetry ... \\c\"\n  if [ -f \"$POETRY_HOME/bin/poetry\" ]; then\n    echo -e \"${BIGreen}OK${RST}\"\n  else\n    echo -e \"${BIYellow}NOT FOUND${RST}\"\n    echo -e \"${BIYellow}***${RST} We need to install Poetry and virtual env ...\"\n    . \"$openpype_root/tools/create_env.sh\" || { echo -e \"${BIRed}!!!${RST} Poetry installation failed\"; return; }\n  fi\n\n  echo -e \"${BIGreen}>>>${RST} Generating zip from current sources ...\"\n  \"$POETRY_HOME/bin/poetry\" run python \"$openpype_root/start.py\" projectmanager\n}\n\nmain\n"
  },
  {
    "path": "tools/run_publish_report_viewer.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script OpenPype Tray.\n\n.DESCRIPTION\n\n\n.EXAMPLE\n\nPS> .\\run_tray.ps1\n\n#>\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n# Install PSWriteColor to support colorized output to terminal\n$env:PSModulePath = $env:PSModulePath + \";$($openpype_root)\\tools\\modules\\powershell\"\n\n$env:_INSIDE_OPENPYPE_TOOL = \"1\"\n\n# make sure Poetry is in PATH\nif (-not (Test-Path 'env:POETRY_HOME')) {\n    $env:POETRY_HOME = \"$openpype_root\\.poetry\"\n}\n$env:PATH = \"$($env:PATH);$($env:POETRY_HOME)\\bin\"\n\nSet-Location -Path $openpype_root\n\nWrite-Color -Text \">>> \", \"Reading Poetry ... \" -Color Green, Gray -NoNewline\nif (-not (Test-Path -PathType Container -Path \"$($env:POETRY_HOME)\\bin\")) {\n    Write-Color -Text \"NOT FOUND\" -Color Yellow\n    Write-Color -Text \"*** \", \"We need to install Poetry create virtual env first ...\" -Color Yellow, Gray\n    & \"$openpype_root\\tools\\create_env.ps1\"\n} else {\n    Write-Color -Text \"OK\" -Color Green\n}\n\n& \"$($env:POETRY_HOME)\\bin\\poetry\" run python \"$($openpype_root)\\start.py\" publish-report-viewer --debug\nSet-Location -Path $current_dir\n"
  },
  {
    "path": "tools/run_settings.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script to OpenPype Settings UI\n\n.DESCRIPTION\n  This script will run OpenPype and open Settings UI.\n\n.EXAMPLE\n\nPS> .\\run_settings.ps1\n\n#>\n\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n# Install PSWriteColor to support colorized output to terminal\n$env:PSModulePath = $env:PSModulePath + \";$($openpype_root)\\tools\\modules\\powershell\"\n\n$env:_INSIDE_OPENPYPE_TOOL = \"1\"\n\n# make sure Poetry is in PATH\nif (-not (Test-Path 'env:POETRY_HOME')) {\n    $env:POETRY_HOME = \"$openpype_root\\.poetry\"\n}\n$env:PATH = \"$($env:PATH);$($env:POETRY_HOME)\\bin\"\n\nSet-Location -Path $openpype_root\n\nWrite-Color -Text \">>> \", \"Reading Poetry ... \" -Color Green, Gray -NoNewline\nif (-not (Test-Path -PathType Container -Path \"$($env:POETRY_HOME)\\bin\")) {\n    Write-Color -Text \"NOT FOUND\" -Color Yellow\n    Install-Poetry\n    Write-Color -Text \"INSTALLED\" -Color Cyan\n} else {\n    Write-Color -Text \"OK\" -Color Green\n}\n\n& \"$env:POETRY_HOME\\bin\\poetry\" run python \"$($openpype_root)\\start.py\" settings --dev\nSet-Location -Path $current_dir"
  },
  {
    "path": "tools/run_settings.sh",
    "content": "#!/usr/bin/env bash\n\n# Run OpenPype Settings GUI\n\n\nart () {\n  cat <<-EOF\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~·   ··   ~2p.  ··  ····  ·  ·\n    ·Ppo · .pPO3Op.· · O:· · · ·\n   .3Pp · oP3'· 'P33· · 4 ··   ·  ·   · ·· ·  ·  ·\n  ·~OP    3PO·  .Op3    : · ··  _____  _____  _____\n  ·P3O  · oP3oP3O3P' · · ·   · /    /·/    /·/    /\n   O3:·   O3p~ ·       ·:· · ·/____/·/____/ /____/\n   'P ·   3p3·  oP3~· ·.P:· ·  · ··  ·   · ·· ·  ·  ·\n  · ':  · Po'  ·Opo'· .3O· .  o[ by Pype Club ]]]==- - - ·  ·\n    · '_ ..  ·    . _OP3··  ·  ·https://openpype.io·· ·\n         ~P3·OPPPO3OP~ · ··  ·\n           ·  ' '· ·  ·· · · · ··  ·\n\nEOF\n}\n\n# Colors for terminal\n\nRST='\\033[0m'             # Text Reset\n\n# Regular Colors\nBlack='\\033[0;30m'        # Black\nRed='\\033[0;31m'          # Red\nGreen='\\033[0;32m'        # Green\nYellow='\\033[0;33m'       # Yellow\nBlue='\\033[0;34m'         # Blue\nPurple='\\033[0;35m'       # Purple\nCyan='\\033[0;36m'         # Cyan\nWhite='\\033[0;37m'        # White\n\n# Bold\nBBlack='\\033[1;30m'       # Black\nBRed='\\033[1;31m'         # Red\nBGreen='\\033[1;32m'       # Green\nBYellow='\\033[1;33m'      # Yellow\nBBlue='\\033[1;34m'        # Blue\nBPurple='\\033[1;35m'      # Purple\nBCyan='\\033[1;36m'        # Cyan\nBWhite='\\033[1;37m'       # White\n\n# Bold High Intensity\nBIBlack='\\033[1;90m'      # Black\nBIRed='\\033[1;91m'        # Red\nBIGreen='\\033[1;92m'      # Green\nBIYellow='\\033[1;93m'     # Yellow\nBIBlue='\\033[1;94m'       # Blue\nBIPurple='\\033[1;95m'     # Purple\nBICyan='\\033[1;96m'       # Cyan\nBIWhite='\\033[1;97m'      # White\n\n\n##############################################################################\n# Return absolute path\n# Globals:\n#   None\n# Arguments:\n#   Path to resolve\n# Returns:\n#   None\n###############################################################################\nrealpath () {\n  echo $(cd $(dirname \"$1\"); pwd)/$(basename \"$1\")\n}\n\n# Main\nmain () {\n\n  # Directories\n  openpype_root=$(realpath $(dirname $(dirname \"${BASH_SOURCE[0]}\")))\n\n  _inside_openpype_tool=\"1\"\n\n  if [[ -z $POETRY_HOME ]]; then\n    export POETRY_HOME=\"$openpype_root/.poetry\"\n  fi\n\n  pushd \"$openpype_root\" > /dev/null || return > /dev/null\n\n  echo -e \"${BIGreen}>>>${RST} Reading Poetry ... \\c\"\n  if [ -f \"$POETRY_HOME/bin/poetry\" ]; then\n    echo -e \"${BIGreen}OK${RST}\"\n  else\n    echo -e \"${BIYellow}NOT FOUND${RST}\"\n    echo -e \"${BIYellow}***${RST} We need to install Poetry and virtual env ...\"\n    . \"$openpype_root/tools/create_env.sh\" || { echo -e \"${BIRed}!!!${RST} Poetry installation failed\"; return; }\n  fi\n\n  echo -e \"${BIGreen}>>>${RST} Generating zip from current sources ...\"\n  \"$POETRY_HOME/bin/poetry\" run python3 \"$openpype_root/start.py\" settings --dev\n}\n\nmain\n"
  },
  {
    "path": "tools/run_tests.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script to run tests for OpenPype.\n\n.DESCRIPTION\n  This will use virtual environment and pytest to run test for OpenPype.\n\n.EXAMPLE\n\nPS> .\\run_test.ps1\n\n#>\n\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n# Install PSWriteColor to support colorized output to terminal\n$env:PSModulePath = $env:PSModulePath + \";$($openpype_root)\\tools\\modules\\powershell\"\n\nfunction Exit-WithCode($exitcode) {\n   # Only exit this host process if it's a child of another PowerShell parent process...\n   $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$PID\" | Select-Object -Property ParentProcessId).ParentProcessId\n   $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$parentPID\" | Select-Object -Property Name).Name\n   if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) }\n\n   exit $exitcode\n}\n\nfunction Show-PSWarning() {\n    if ($PSVersionTable.PSVersion.Major -lt 7) {\n        Write-Color -Text \"!!! \", \"You are using old version of PowerShell - \",  \"$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)\" -Color Red, Yellow, White\n        Write-Color -Text \"    Please update to at least 7.0 - \", \"https://github.com/PowerShell/PowerShell/releases\" -Color Yellow, White\n        Exit-WithCode 1\n    }\n}\n\n$art = @\"\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~.   ..   ~2p.  ..  ....  .  .\n    .Ppo . .pPO3Op.. . O:. . . .\n   .3Pp . oP3'. 'P33. . 4 ..   .  .   . .. .  .  .\n  .~OP    3PO.  .Op3    : . ..  _____  _____  _____\n  .P3O  . oP3oP3O3P' . . .   . /    /./    /./    /\n   O3:.   O3p~ .       .:. . ./____/./____/ /____/\n   'P .   3p3.  oP3~. ..P:. .  . ..  .   . .. .  .  .\n  . ':  . Po'  .Opo'. .3O. .  o[ by Pype Club ]]]==- - - .  .\n    . '_ ..  .    . _OP3..  .  .https://openpype.io.. .\n         ~P3.OPPPO3OP~ . ..  .\n           .  ' '. .  .. . . . ..  .\n\n\"@\n\nWrite-Host $art -ForegroundColor DarkGreen\n\n# Enable if PS 7.x is needed.\n# Show-PSWarning\n\n$env:_INSIDE_OPENPYPE_TOOL = \"1\"\n\nif (-not (Test-Path 'env:POETRY_HOME')) {\n    $env:POETRY_HOME = \"$openpype_root\\.poetry\"\n}\n\nSet-Location -Path $openpype_root\n\n$version_file = Get-Content -Path \"$($openpype_root)\\openpype\\version.py\"\n$result = [regex]::Matches($version_file, '__version__ = \"(?<version>\\d+\\.\\d+.\\d+.*)\"')\n$openpype_version = $result[0].Groups['version'].Value\nif (-not $openpype_version) {\n  Write-Color -Text \"!!! \", \"Cannot determine OpenPype version.\" -Color Yellow, Gray\n  Exit-WithCode 1\n}\n\nWrite-Color -Text \">>> \", \"OpenPype [ \", $openpype_version, \" ]\" -Color Green, White, Cyan, White\n\nWrite-Color -Text \">>> \", \"Reading Poetry ... \" -Color Green, Gray -NoNewline\nif (-not (Test-Path -PathType Container -Path \"$($env:POETRY_HOME)\\bin\")) {\n    Write-Color -Text \"NOT FOUND\" -Color Yellow\n    Write-Color -Text \"*** \", \"We need to install Poetry create virtual env first ...\" -Color Yellow, Gray\n    & \"$openpype_root\\tools\\create_env.ps1\"\n} else {\n    Write-Color -Text \"OK\" -Color Green\n}\n\nWrite-Color -Text \">>> \", \"Cleaning cache files ... \" -Color Green, Gray -NoNewline\nGet-ChildItem $openpype_root -Filter \"*.pyc\" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force\nGet-ChildItem $openpype_root -Filter \"*.pyo\" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force\nGet-ChildItem $openpype_root -Filter \"__pycache__\" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force -Recurse\nWrite-Color -Text \"OK\" -Color green\n\nWrite-Color -Text \">>> \", \"Testing OpenPype ...\" -Color Green, White\n$original_pythonpath = $env:PYTHONPATH\n$env:PYTHONPATH=\"$($openpype_root);$($env:PYTHONPATH)\"\n& \"$env:POETRY_HOME\\bin\\poetry\" run pytest -x --capture=sys --print -W ignore::DeprecationWarning \"$($openpype_root)/tests\"\n$env:PYTHONPATH = $original_pythonpath\n\nWrite-Color -Text \">>> \", \"Restoring current directory\" -Color Green, Gray\nSet-Location -Path $current_dir\n"
  },
  {
    "path": "tools/run_tests.sh",
    "content": "#!/usr/bin/env bash\n\n# Run tests for OpenPype\n# This will use virtual environment and pytest to run test for OpenPype.\n\nart () {\n  cat <<-EOF\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~·   ··   ~2p.  ··  ····  ·  ·\n    ·Ppo · .pPO3Op.· · O:· · · ·\n   .3Pp · oP3'· 'P33· · 4 ··   ·  ·   · ·· ·  ·  ·\n  ·~OP    3PO·  .Op3    : · ··  _____  _____  _____\n  ·P3O  · oP3oP3O3P' · · ·   · /    /·/    /·/    /\n   O3:·   O3p~ ·       ·:· · ·/____/·/____/ /____/\n   'P ·   3p3·  oP3~· ·.P:· ·  · ··  ·   · ·· ·  ·  ·\n  · ':  · Po'  ·Opo'· .3O· .  o[ by Pype Club ]]]==- - - ·  ·\n    · '_ ..  ·    . _OP3··  ·  ·https://openpype.io·· ·\n         ~P3·OPPPO3OP~ · ··  ·\n           ·  ' '· ·  ·· · · · ··  ·\n\nEOF\n}\n\n# Colors for terminal\n\nRST='\\033[0m'             # Text Reset\n\n# Regular Colors\nBlack='\\033[0;30m'        # Black\nRed='\\033[0;31m'          # Red\nGreen='\\033[0;32m'        # Green\nYellow='\\033[0;33m'       # Yellow\nBlue='\\033[0;34m'         # Blue\nPurple='\\033[0;35m'       # Purple\nCyan='\\033[0;36m'         # Cyan\nWhite='\\033[0;37m'        # White\n\n# Bold\nBBlack='\\033[1;30m'       # Black\nBRed='\\033[1;31m'         # Red\nBGreen='\\033[1;32m'       # Green\nBYellow='\\033[1;33m'      # Yellow\nBBlue='\\033[1;34m'        # Blue\nBPurple='\\033[1;35m'      # Purple\nBCyan='\\033[1;36m'        # Cyan\nBWhite='\\033[1;37m'       # White\n\n# Bold High Intensity\nBIBlack='\\033[1;90m'      # Black\nBIRed='\\033[1;91m'        # Red\nBIGreen='\\033[1;92m'      # Green\nBIYellow='\\033[1;93m'     # Yellow\nBIBlue='\\033[1;94m'       # Blue\nBIPurple='\\033[1;95m'     # Purple\nBICyan='\\033[1;96m'       # Cyan\nBIWhite='\\033[1;97m'      # White\n\n##############################################################################\n# Clean pyc files in specified directory\n# Globals:\n#   None\n# Arguments:\n#   Optional path to clean\n# Returns:\n#   None\n###############################################################################\nclean_pyc () {\n  local path\n  path=$openpype_root\n  echo -e \"${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \\c\"\n  find \"$path\" -path ./build -prune -o -regex '^.*\\(__pycache__\\|\\.py[co]\\)$' -delete\n  echo -e \"${BIGreen}DONE${RST}\"\n}\n\n##############################################################################\n# Return absolute path\n# Globals:\n#   None\n# Arguments:\n#   Path to resolve\n# Returns:\n#   None\n###############################################################################\nrealpath () {\n  echo $(cd $(dirname \"$1\"); pwd)/$(basename \"$1\")\n}\n\n# Main\nmain () {\n  echo -e \"${BGreen}\"\n  art\n  echo -e \"${RST}\"\n\n  # Directories\n  openpype_root=$(realpath $(dirname $(dirname \"${BASH_SOURCE[0]}\")))\n\n  _inside_openpype_tool=\"1\"\n\n  if [[ -z $POETRY_HOME ]]; then\n    export POETRY_HOME=\"$openpype_root/.poetry\"\n  fi\n\n  echo -e \"${BIGreen}>>>${RST} Reading Poetry ... \\c\"\n  if [ -f \"$POETRY_HOME/bin/poetry\" ]; then\n    echo -e \"${BIGreen}OK${RST}\"\n  else\n    echo -e \"${BIYellow}NOT FOUND${RST}\"\n    echo -e \"${BIYellow}***${RST} We need to install Poetry and virtual env ...\"\n    . \"$openpype_root/tools/create_env.sh\" || { echo -e \"${BIRed}!!!${RST} Poetry installation failed\"; return; }\n  fi\n\n  pushd \"$openpype_root\" || return > /dev/null\n\n  echo -e \"${BIGreen}>>>${RST} Testing OpenPype ...\"\n  original_pythonpath=$PYTHONPATH\n  export PYTHONPATH=\"$openpype_root:$PYTHONPATH\"\n  \"$POETRY_HOME/bin/poetry\" run pytest -x --capture=sys --print -W ignore::DeprecationWarning \"$openpype_root/tests\"\n  PYTHONPATH=$original_pythonpath\n}\n\nmain\n"
  },
  {
    "path": "tools/run_tray.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script OpenPype Tray.\n\n.DESCRIPTION\n\n\n.EXAMPLE\n\nPS> .\\run_tray.ps1\n\n#>\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n# Install PSWriteColor to support colorized output to terminal\n$env:PSModulePath = $env:PSModulePath + \";$($openpype_root)\\tools\\modules\\powershell\"\n\n$env:_INSIDE_OPENPYPE_TOOL = \"1\"\n\n# make sure Poetry is in PATH\nif (-not (Test-Path 'env:POETRY_HOME')) {\n    $env:POETRY_HOME = \"$openpype_root\\.poetry\"\n}\n$env:PATH = \"$($env:PATH);$($env:POETRY_HOME)\\bin\"\n\nSet-Location -Path $openpype_root\n\nWrite-Color -Text \">>> \", \"Reading Poetry ... \" -Color Green, Gray -NoNewline\nif (-not (Test-Path -PathType Container -Path \"$($env:POETRY_HOME)\\bin\")) {\n    Write-Color -Text \"NOT FOUND\" -Color Yellow\n    Write-Color -Text \"*** \", \"We need to install Poetry create virtual env first ...\" -Color Yellow, Gray\n    & \"$openpype_root\\tools\\create_env.ps1\"\n} else {\n    Write-Color -Text \"OK\" -Color Green\n}\n\n& \"$($env:POETRY_HOME)\\bin\\poetry\" run python \"$($openpype_root)\\start.py\" tray --debug\nSet-Location -Path $current_dir"
  },
  {
    "path": "tools/run_tray.sh",
    "content": "#!/usr/bin/env bash\n# Run OpenPype Tray\n\n# Colors for terminal\n\nRST='\\033[0m'             # Text Reset\n\n# Regular Colors\nBlack='\\033[0;30m'        # Black\nRed='\\033[0;31m'          # Red\nGreen='\\033[0;32m'        # Green\nYellow='\\033[0;33m'       # Yellow\nBlue='\\033[0;34m'         # Blue\nPurple='\\033[0;35m'       # Purple\nCyan='\\033[0;36m'         # Cyan\nWhite='\\033[0;37m'        # White\n\n# Bold\nBBlack='\\033[1;30m'       # Black\nBRed='\\033[1;31m'         # Red\nBGreen='\\033[1;32m'       # Green\nBYellow='\\033[1;33m'      # Yellow\nBBlue='\\033[1;34m'        # Blue\nBPurple='\\033[1;35m'      # Purple\nBCyan='\\033[1;36m'        # Cyan\nBWhite='\\033[1;37m'       # White\n\n# Bold High Intensity\nBIBlack='\\033[1;90m'      # Black\nBIRed='\\033[1;91m'        # Red\nBIGreen='\\033[1;92m'      # Green\nBIYellow='\\033[1;93m'     # Yellow\nBIBlue='\\033[1;94m'       # Blue\nBIPurple='\\033[1;95m'     # Purple\nBICyan='\\033[1;96m'       # Cyan\nBIWhite='\\033[1;97m'      # White\n\n\n##############################################################################\n# Return absolute path\n# Globals:\n#   None\n# Arguments:\n#   Path to resolve\n# Returns:\n#   None\n###############################################################################\nrealpath () {\n  echo $(cd $(dirname \"$1\"); pwd)/$(basename \"$1\")\n}\n\n# Main\nmain () {\n  # Directories\n  openpype_root=$(realpath $(dirname $(dirname \"${BASH_SOURCE[0]}\")))\n\n  _inside_openpype_tool=\"1\"\n\n  if [[ -z $POETRY_HOME ]]; then\n    export POETRY_HOME=\"$openpype_root/.poetry\"\n  fi\n\n  echo -e \"${BIGreen}>>>${RST} Reading Poetry ... \\c\"\n  if [ -f \"$POETRY_HOME/bin/poetry\" ]; then\n    echo -e \"${BIGreen}OK${RST}\"\n  else\n    echo -e \"${BIYellow}NOT FOUND${RST}\"\n    echo -e \"${BIYellow}***${RST} We need to install Poetry and virtual env ...\"\n    . \"$openpype_root/tools/create_env.sh\" || { echo -e \"${BIRed}!!!${RST} Poetry installation failed\"; return; }\n  fi\n\n  pushd \"$openpype_root\" > /dev/null || return > /dev/null\n\n  echo -e \"${BIGreen}>>>${RST} Running OpenPype Tray with debug option ...\"\n  \"$POETRY_HOME/bin/poetry\" run python3 \"$openpype_root/start.py\" tray --debug\n}\n\nmain"
  },
  {
    "path": "tools/unpack_project.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script OpenPype Unpacking project.\n\n.DESCRIPTION\n  Make sure you had dropped the project from your db and removed the poject data in case you were having them previously. Then on line 38 change the <Path to zip> to any path where the zip with project is - usually we are having it here https://drive.google.com/drive/u/0/folders/0AKE4mxImOsAGUk9PVA . Copy the file into .\\OpenPype\\tools. Then use the cmd form .EXAMPLE\n\n.EXAMPLE\n\nPS> .\\tools\\run_unpack_project.ps1\n\n#>\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\n$env:_INSIDE_OPENPYPE_TOOL = \"1\"\n\n# make sure Poetry is in PATH\nif (-not (Test-Path 'env:POETRY_HOME')) {\n    $env:POETRY_HOME = \"$openpype_root\\.poetry\"\n}\n$env:PATH = \"$($env:PATH);$($env:POETRY_HOME)\\bin\"\n\nSet-Location -Path $openpype_root\n\nWrite-Host \">>> \" -NoNewline -ForegroundColor Green\nWrite-Host \"Reading Poetry ... \" -NoNewline\nif (-not (Test-Path -PathType Container -Path \"$($env:POETRY_HOME)\\bin\")) {\n    Write-Host \"NOT FOUND\" -ForegroundColor Yellow\n    Write-Host \"*** \" -NoNewline -ForegroundColor Yellow\n    Write-Host \"We need to install Poetry create virtual env first ...\"\n    & \"$openpype_root\\tools\\create_env.ps1\"\n} else {\n    Write-Host \"OK\" -ForegroundColor Green\n}\n\n& \"$($env:POETRY_HOME)\\bin\\poetry\" run python \"$($openpype_root)\\start.py\" unpack-project --zipfile $ARGS\nSet-Location -Path $current_dir"
  },
  {
    "path": "tools/update_submodules.ps1",
    "content": "<#\n.SYNOPSIS\n  Helper script to update submodules.\n\n.EXAMPLE\n\nPS> .\\update_submodules.ps1\n\n#>\n\n$art = @\"\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~·   ··   ~2p.  ··  ····  ·  ·\n    ·Ppo · .pPO3Op.· · O:· · · ·\n   .3Pp · oP3'· 'P33· · 4 ··   ·  ·   · ·· ·  ·  ·\n  ·~OP    3PO·  .Op3    : · ··  _____  _____  _____\n  ·P3O  · oP3oP3O3P' · · ·   · /    /·/    /·/    /\n   O3:·   O3p~ ·       ·:· · ·/____/·/____/ /____/\n   'P ·   3p3·  oP3~· ·.P:· ·  · ··  ·   · ·· ·  ·  ·\n  · ':  · Po'  ·Opo'· .3O· .  o[ by Pype Club ]]]==- - - ·  ·\n    · '_ ..  ·    . _OP3··  ·  ·https://openpype.io·· ·\n         ~P3·OPPPO3OP~ · ··  ·\n           ·  ' '· ·  ·· · · · ··  ·\n\n\"@\n\nWrite-Host $art -ForegroundColor DarkGreen\n\nfunction Exit-WithCode($exitcode) {\n   # Only exit this host process if it's a child of another PowerShell parent process...\n   $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$PID\" | Select-Object -Property ParentProcessId).ParentProcessId\n   $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter \"ProcessId=$parentPID\" | Select-Object -Property Name).Name\n   if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) }\n\n   exit $exitcode\n}\n\n$current_dir = Get-Location\n$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent\n$openpype_root = (Get-Item $script_dir).parent.FullName\n\nSet-Location -Path $openpype_root\n\ngit submodule update --recursive --remote\n\nSet-Location -Path $current_dir"
  },
  {
    "path": "tools/update_submodules.sh",
    "content": "#!/usr/bin/env bash\n\n# Run OpenPype Tray\n\n\nart () {\n  cat <<-EOF\n\n             . .   ..     .    ..\n        _oOOP3OPP3Op_. .\n     .PPpo~·   ··   ~2p.  ··  ····  ·  ·\n    ·Ppo · .pPO3Op.· · O:· · · ·\n   .3Pp · oP3'· 'P33· · 4 ··   ·  ·   · ·· ·  ·  ·\n  ·~OP    3PO·  .Op3    : · ··  _____  _____  _____\n  ·P3O  · oP3oP3O3P' · · ·   · /    /·/    /·/    /\n   O3:·   O3p~ ·       ·:· · ·/____/·/____/ /____/\n   'P ·   3p3·  oP3~· ·.P:· ·  · ··  ·   · ·· ·  ·  ·\n  · ':  · Po'  ·Opo'· .3O· .  o[ by Pype Club ]]]==- - - ·  ·\n    · '_ ..  ·    . _OP3··  ·  ·https://openpype.io·· ·\n         ~P3·OPPPO3OP~ · ··  ·\n           ·  ' '· ·  ·· · · · ··  ·\n\nEOF\n}\n\n# Colors for terminal\n\nRST='\\033[0m'             # Text Reset\n\n# Regular Colors\nBlack='\\033[0;30m'        # Black\nRed='\\033[0;31m'          # Red\nGreen='\\033[0;32m'        # Green\nYellow='\\033[0;33m'       # Yellow\nBlue='\\033[0;34m'         # Blue\nPurple='\\033[0;35m'       # Purple\nCyan='\\033[0;36m'         # Cyan\nWhite='\\033[0;37m'        # White\n\n# Bold\nBBlack='\\033[1;30m'       # Black\nBRed='\\033[1;31m'         # Red\nBGreen='\\033[1;32m'       # Green\nBYellow='\\033[1;33m'      # Yellow\nBBlue='\\033[1;34m'        # Blue\nBPurple='\\033[1;35m'      # Purple\nBCyan='\\033[1;36m'        # Cyan\nBWhite='\\033[1;37m'       # White\n\n# Bold High Intensity\nBIBlack='\\033[1;90m'      # Black\nBIRed='\\033[1;91m'        # Red\nBIGreen='\\033[1;92m'      # Green\nBIYellow='\\033[1;93m'     # Yellow\nBIBlue='\\033[1;94m'       # Blue\nBIPurple='\\033[1;95m'     # Purple\nBICyan='\\033[1;96m'       # Cyan\nBIWhite='\\033[1;97m'      # White\n\n\n# Main\necho -e \"${BGreen}\"\nart\necho -e \"${RST}\"\n\ngit submodule update --recursive --remote"
  },
  {
    "path": "vendor/README.md",
    "content": "### Pype vendor folder\n\nEverything here will be included in frozen code during build but is ignored by git. Useful for binaries like ffmpeg/oiio/etc.\n"
  },
  {
    "path": "website/README.md",
    "content": "When developing on Windows make sure `start.sh` has the correct line endings (`LF`).\n\nStart via yarn:\n---------------\nClone repository\n\nInstall yarn if not already installed (https://classic.yarnpkg.com/en/docs/install)\nFor example via npm (but could be installed differently too)\n\n    ```npm install --global yarn```\n\nThen go to `website` folder\n\n    ```yarn install``` (takes a while)\n\nTo start local test server:\n\n    ```yarn start```\n\nServer is accessible by default on http://localhost:3000\n\nStart via docker:\n-----------------\nSetting for docker container:\n```bash\ndocker build . -t pype-docs\ndocker run --rm -p 3000:3000 -v /c/Users/admin/openpype.io:/app pype-docs\n```\n"
  },
  {
    "path": "website/docs/admin_builds.md",
    "content": "---\nid: admin_builds\ntitle: Builds and Releases\nsidebar_label: Builds\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nAdmins might find prepared builds on https://github.com/pypeclub/OpenPype/releases for all major platforms.\n\n### Currently built on OS versions\n- Windows 10\n- Ubuntu 20.04\n- Centos 7.6\n- MacOS Mohave (10.14.6)\n\nIn case your studio requires build for different OS version, or any specific build, please take a look at \n[Requirements](dev_requirements.md) and [Build](dev_build.md) for more details how to create binaries to distribute.\n "
  },
  {
    "path": "website/docs/admin_distribute.md",
    "content": "---\nid: admin_distribute\ntitle: Distribute\nsidebar_label: Distribute\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nTo let your artists to use OpenPype, you'll need to distribute the frozen executables to them.\n\nDistribution consists of two parts\n\n ### 1. OpenPype Igniter\n \n This is the base application that will be installed locally on each workstation.\n It is self contained (frozen) software that also includes all of the OpenPype codebase with the version\n from the time of the build.\n\n Igniter package is around 1Gb and preparing an updated version requires you to re-build pype. That would be \n inconvenient for regular and quick distribution of production updates and fixes. So you can distribute those\n independently, without requiring you artists to re-install every time.\n\n You can have multiple versions installed at the same time.\n\n ### 2. OpenPype Codebase\n\nWhen you upgrade your studio pype deployment to a new version or make any local code changes, you can distribute\nthese changes to your artists, without the need of re-building OpenPype, by using `create_zip` tool provided.\nThe resulting zip needs to be made available to the artists and it will override their local OpenPype install\nwith the updated version.\n\nYou have two ways of making this happen\n\n#### Automatic Updates\n\nEvery time and Artist launches OpenPype on their workstation, it will look to a pre-defined \n[openPype update location](admin_settings_system.md#openpype-deployment-control) for any versions that are newer than the\nlatest, locally installed version. If such version is found, it will be downloaded,  \nautomatically extracted to the correct place and launched. This will become the default \nversion to run for the artist, until a higher version is detected in the update location again.\n\n#### Manual Updates\n\nIf for some reason you don't want to use the automatic updates, you can distribute your\nzips manually. Your artist will then have to put them to the correct place on their disk.\nZips will be automatically unzipped there.\n\nThe default locations are:\n\n- Windows: `%LOCALAPPDATA%\\pypeclub\\openpype`\n- Linux: `~/.local/share/pypeclub/openpype`\n- Mac: `~/Library/Application Support/pypeclub/openpype`\n\n\n### Staging vs. Production\nYou can have version of OpenPype with experimental features you want to try somewhere, but you\ndon't want to disrupt your production. You can set such version in th Settings.\n\nYou can run OpenPype with `--use-staging` argument to use staging version specified in the Settings.\n\n:::note\nRunning staging version is identified by orange **P** icon in system tray.\n:::\n\n### OpenPype versioning\n\nOpenPype version control is based on semantic versioning.\n\n:::note\nThe version of OpenPype is indicated by the variable `__version__` in the file `.\\openpype\\version.py`.\n:::\n\nFor example OpenPype will consider the versions in this order: `3.8.0-nightly` < `3.8.0-nightly.1` < `3.8.0-rc.1` < `3.8.0` < `3.8.1-nightly.1` <`3.8.1` < `3.9.0` < `3.10.0` < `4.0.0`.\n\nSee https://semver.org/ for more details.\n\nFor studios customizing the source code of OpenPype, a practical approach could be to build by adding a name and a number after the PATCH and not to deploy 3.8.0 from original OpenPype repository. For example, your builds will be: `3.8.0-yourstudio.1` < `3.8.0-yourstudio.2` < `3.8.1-yourstudio.1`.\n\nVersions of Igniter and those coming in zips are compatible if they match major and minor version - `3.13.4` is compatible with `3.13.1` but not with `3.12.2` or `3.14.0`.\n"
  },
  {
    "path": "website/docs/admin_docsexamples.md",
    "content": "---\nid: admin_docsexamples\ntitle: Examples of using notes\nsidebar_label: docsexamples\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n<Tabs\n  groupId=\"platforms\"\n  defaultValue=\"win\"\n  values={[\n    {label: 'Windows', value: 'win'},\n    {label: 'Linux', value: 'linux'},\n    {label: 'Mac', value: 'mac'},\n  ]}>\n\n<TabItem value=\"win\">\n\nThis is your mac stuff\n\n</TabItem>\n<TabItem value=\"linux\">\n\nThis is your linux stuff\n\n\n</TabItem>\n<TabItem value=\"mac\">\n\nThis is your mac stuff\n\n</TabItem>\n</Tabs>\n\n\n\n\n\n\n:::note Name of the category\n\n<Tabs\n  groupId=\"platforms\"\n  defaultValue=\"win\"\n  values={[\n    {label: 'Windows', value: 'win'},\n    {label: 'Linux', value: 'linux'},\n    {label: 'Mac', value: 'mac'},\n  ]}>\n\n<TabItem value=\"win\">\n\nThis is your mac stuff\n\n</TabItem>\n<TabItem value=\"linux\">\n\nThis is your linux stuff\n\n\n</TabItem>\n<TabItem value=\"mac\">\n\nThis is your mac stuff\n\n</TabItem>\n</Tabs>\n\n:::\n\n\n=========================\n\n:::important\n\n-   This is my note\n-   another list\n-   super list\n\n```python\nimport os\nprint(os.environ)\n```\n\n:::\n\n:::tip\nThis is my note\n:::\n\n:::note\nThis is my note\n:::\n\n:::warning\nThis is my note\n:::\n\n:::caution\nThis is my note\n:::\n\nexport const Highlight = ({children, color}) => (\n  <span\n    style={{\n      backgroundColor: color,\n      borderRadius: '2px',\n      color: '#fff',\n      padding: '0.2rem',\n    }}>\n    {children}\n  </span>\n);\n\n<Highlight color=\"#25c2a0\">Docusaurus green</Highlight> and <Highlight color=\"#1877F2\">Facebook blue</Highlight> are my favorite colors.\n\nI can write **Markdown** alongside my _JSX_!\n"
  },
  {
    "path": "website/docs/admin_environment.md",
    "content": "---\nid: admin_environment\ntitle: Environment\nsidebar_label: Environment\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## OPENPYPE_TMPDIR:\n - Custom staging dir directory\n - Supports anatomy keys formatting. ex `{root[work]}/{project[name]}/temp`\n - supported formatting keys:\n    - root[work]\n    - project[name | code]\n\n## OPENPYPE_DEBUG\n - setting logger to debug mode\n - example value: \"1\" (to activate)\n\n## OPENPYPE_LOG_LEVEL\n - stringified numeric value of log level. [Here for more info](https://docs.python.org/3/library/logging.html#logging-levels)\n - example value: \"10\"\n\n## OPENPYPE_MONGO\n- If set it takes precedence over the one set in keyring\n- for more details on how to use it go [here](admin_use#check-for-mongodb-database-connection)\n\n## OPENPYPE_USERNAME\n- if set it overrides system created username\n"
  },
  {
    "path": "website/docs/admin_hosts_aftereffects.md",
    "content": "---\nid: admin_hosts_aftereffects\ntitle: AfterEffects Settings\nsidebar_label: AfterEffects\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## AfterEffects settings\n\nThere is a couple of settings that could configure publishing process for **AfterEffects**.\nAll of them are Project based, eg. each project could have different configuration.\n\nLocation: Settings > Project > AfterEffects\n\n![AfterEffects Project Settings](assets/admin_hosts_aftereffects_settings.png)\n\n## Publish plugins\n\n### Collect Review\n\nEnable/disable creation of auto instance of review.\n\n### Validate Scene Settings\n\n#### Skip Resolution Check for Tasks\n\nSet regex pattern(s) to look for in a Task name to skip resolution check against values from DB.\n\n#### Skip Timeline Check for Tasks\n\nSet regex pattern(s) to look for in a Task name to skip `frameStart`, `frameEnd` check against values from DB.\n\n### ValidateContainers\n\nBy default this validator will look loaded items with lower version than latest. This validator is context wide so it must be disabled in Context button.\n\n### AfterEffects Submit to Deadline\n\n* `Use Published scene` - Set to True (green) when Deadline should take published scene as a source instead of uploaded local one.\n* `Priority` - priority of job on farm\n* `Primary Pool` - here is list of pool fetched from server you can select from.\n* `Secondary Pool`\n* `Frames Per Task` - number of sequence division between individual tasks (chunks)\nmaking one job on farm.\n\n## Worfkile Builder\n\nObsolete way how to present artist with a template when they are working on new task.\n\n## Templated Workfile Build Settings\n\nThis more advanced way allows creating more elaborate workfile templates with placeholders for loaded items or to create publishable item via Creator.\n\nWorkfile template must be prepared separately via Tray and in the host, then its location could be set for combination of:\n- `Task types` (specific template for `animation`, different for `layout` etc.)\n- `Task names` (regex supported )\n\nAdditional options:\n- `Keep placeholders` - when template gets populated should placeholders be deleted? (In most cases yes.)\n- `Create first version` - if template should be used and populated for version `v001` of a workfile automatically\n"
  },
  {
    "path": "website/docs/admin_hosts_blender.md",
    "content": "---\nid: admin_hosts_blender\ntitle: Blender\nsidebar_label: Blender\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## Blender requirements\nBlender integration requires to use **PySide2** module inside blender. Module is different for Blender versions and platforms so can't be bundled with OpenPype.\n\n### How to install\n\n:::info Permissions\nThis step requires Admin persmission.\n:::\n\n<Tabs\n  groupId=\"platforms\"\n  defaultValue=\"win\"\n  values={[\n    {label: 'Windows', value: 'win'},\n    {label: 'Linux', value: 'linux'},\n  ]}>\n\n<TabItem value=\"win\">\n\nFind python executable inside your Blender installation folder. It is usually located in **C:\\\\Program Files\\\\Blender Foundation\\\\Blender {version}\\\\{version}\\\\python\\\\bin\\\\python.exe** (This may differ in future blender version).\n\nOpen Powershell or Command Prompt as Administrator and run commands below.\n\n*Replace `C:\\Program Files\\Blender Foundation\\Blender 2.83\\2.83\\python\\bin` with your path.*\n\n```bash\n# Change directory to python executable directory.\n> cd C:\\Program Files\\Blender Foundation\\Blender 2.83\\2.83\\python\\bin\n\n# Run pip install command.\n> python -m pip install PySide2\n```\n\n</TabItem>\n\n<TabItem value=\"linux\">\n\nProcedure may differ based on Linux distribution and blender distribution. Some Blender distributions are using system Python in that case it is required to install PySide2 using pip to system python (Not tested).\n\n**These instructions are for Blender using bundled python.**\n\nFind python executable inside your blender application.\n\n:::note Find python executable in Blender\nYou can launch Blender and in \"Scripting\" section enter commands to console.\n```bash\n>>> import bpy\n>>> print(bpy.app.binary_path_python)\n'/path/to/python/executable'\n```\n:::\n\nOpen terminal and run pip install command below.\n\n*Replace `/usr/bin/blender/2.83/python/bin/python3.7m` with your path.*\n```bash\n> /usr/bin/blender/2.83/python/bin/python3.7m -m pip install PySide2\n```\n\n:::warning No module named pip\nIf you get error `No module named pip` you'll have to do few steps first. Open new terminal and run the python executable from Blender (entering full path).\n```bash\n# Run Python executable\n> /usr/bin/blender/2.83/python/bin/python3.7m\n# Python process should start\n>>> import ensurepip\n>>> ensurepip.bootstrap()\n```\nYou can close new terminal. Run pip install command above again. Now should work as expected.\n:::\n\n</TabItem>\n\n</Tabs>\n"
  },
  {
    "path": "website/docs/admin_hosts_harmony.md",
    "content": "---\nid: admin_hosts_harmony\ntitle: ToonBoom Harmony Settings\nsidebar_label: ToonBoom Harmony\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## ToonBoom Harmony settings\n\nThere is a couple of settings that could configure publishing process for **ToonBoom Harmony**.\nAll of them are Project based, eg. each project could have different configuration.\n\nLocation: Settings > Project > Harmony\n\n![Harmony Project Settings](assets/admin_hosts_harmony_settings.png)\n\n## Publish plugins\n\n### Collect Palettes\n\n#### Allowed tasks\n\nSet regex pattern(s) only for task names when publishing of Palettes should occur.\n\nUse \".*\" to publish Palettes for ALL tasks.\n\n### Validate Scene Settings\n\n#### Skip Frame check for Assets with\n\nSet regex pattern(s) for filtering Asset name that should skip validation of `frameEnd` value from DB.\n\n#### Skip Resolution Check for Tasks\n\nSet regex pattern(s) for filtering Asset name that should skip validation or `Resolution` value from DB.\n\n#### Skip Timeline Check for Tasks\n\nSet regex pattern(s) for filtering Task name that should skip validation `frameStart`, `frameEnd` check against values from DB.\n\n### Harmony Submit to Deadline\n\n* `Use Published scene` - Set to True (green) when Deadline should take published scene as a source instead of uploaded local one.\n* `Priority` - priority of job on farm\n* `Primary Pool` - here is list of pool fetched from server you can select from.\n* `Secondary Pool`\n* `Frames Per Task` - number of sequence division between individual tasks (chunks)\nmaking one job on farm.\n\n"
  },
  {
    "path": "website/docs/admin_hosts_hiero.md",
    "content": "---\nid: admin_hosts_hiero\ntitle: Hiero\nsidebar_label: Hiero\n---\n\n## Custom Menu\nYou can add your custom tools menu into Hiero by extending definitions in **Hiero -> Scripts Menu Definition**.\n![Custom menu definition](assets/hiero-admin_scriptsmenu.png)\n"
  },
  {
    "path": "website/docs/admin_hosts_houdini.md",
    "content": "---\nid: admin_hosts_houdini\ntitle: Houdini\nsidebar_label: Houdini\n---\n## General Settings\n### Houdini Vars\n\nAllows admins to have a list of vars (e.g. JOB) with (dynamic) values that will be updated on context changes, e.g. when switching to another asset or task.\n\nUsing template keys is supported but formatting keys capitalization variants is not, e.g. `{Asset}` and `{ASSET}` won't work\n\n\n:::note\nIf `Treat as directory` toggle is activated, Openpype will consider the given value is a path of a folder.\n\nIf the folder does not exist on the context change it will be created by this feature so that the path will always try to point to an existing folder.\n:::\n\nDisabling `Update Houdini vars on context change` feature will leave all Houdini vars unmanaged and thus no context update changes will occur.\n\n> If `$JOB` is present in the Houdini var list and has an empty value, OpenPype will set its value to `$HIP`\n\n\n:::note\nFor consistency reasons we always force all vars to be uppercase.\ne.g. `myvar` will be `MYVAR`\n:::\n\n![update-houdini-vars-context-change](assets/houdini/update-houdini-vars-context-change.png)\n\n\n\n## Shelves Manager\nYou can add your custom shelf set into Houdini by setting your shelf sets, shelves and tools in **Houdini -> Shelves Manager**.\n![Custom menu definition](assets/houdini-admin_shelvesmanager.png)\n\nThe Shelf Set Path is used to load a .shelf file to generate your shelf set. If the path is specified, you don't have to set the shelves and tools.\n"
  },
  {
    "path": "website/docs/admin_hosts_maya.md",
    "content": "---\nid: admin_hosts_maya\ntitle: Maya\nsidebar_label: Maya\n---\n\n## Publish Plugins\n\n### Render Settings Validator\n\n`ValidateRenderSettings`\n\nRender Settings Validator is here to make sure artists will submit renders\nwith the correct settings. Some of these settings are needed by OpenPype but some\ncan be defined by the admin using [OpenPype Settings UI](admin_settings.md).\n\nOpenPype enforced settings include:\n\n- animation must be enabled in output\n- render prefix must start with `maya/<scene>` to make sure renders are in\ncorrect directory\n- there must be `<renderlayer>` or its equivalent in different renderers in\nfile prefix\n- if multiple cameras are to be rendered, `<camera>` token must be in file prefix\n\nFor **Vray**:\n- AOV separator must be set to `_` (underscore)\n\nFor **Redshift**:\n- all AOVs must follow `<BeautyPath>/<BeautyFile>_<RenderPass>` image file prefix\n- AOV image format must be same as the one set in Output settings\n\nFor **Renderman**:\n- both image and directory prefixes must comply to `<layer>_<aov>.<f4>.<ext>` and `<ws>/renders/maya/<scene>/<layer>` respectively\n\nFor **Arnold**:\n- there shouldn't be `<renderpass>` token when merge AOVs option is turned on\n\nAdditional check can be added via Settings - **Project Settings > Maya > Publish plugin > ValidateRenderSettings**.\nYou can add as many options as you want for every supported renderer. In first field put node type and attribute\nand in the second required value. You can create multiple values for an attribute, but when repairing it'll be the first value in the list that get selected.\n\n![Settings example](assets/maya-admin_render_settings_validator.png)\n\nIn this example we've put `aiOptions.AA_samples` in first one and `6` to second to enforce\nArnolds Camera (AA) samples to 6.\n\nNote that `aiOptions` is not the name of node but rather its type. For renderers there is usually\njust one instance of this node type but if that is not so, validator will go through all its\ninstances and check the value there. Node type for **VRay** settings is `VRaySettingsNode`, for **Renderman**\nit is `rmanGlobals`, for **Redshift** it is `RedshiftOptions`.\n\n### Model Name Validator\n\n`ValidateRenderSettings`\n\nThis validator can enforce specific names for model members. It will check them against **Validation Regex**.\nThere is special group in that regex - **shader**. If present, it will take that part of the name as shader name\nand it will compare it with list of shaders defined either in file name specified in **Material File** or from\ndatabase file that is per project and can be directly edited from Maya's *OpenPype Tools > Edit Shader name definitions* when\n**Use database shader name definitions** is on. This list defines simply as one shader name per line.\n\n![Settings example](assets/maya-admin_model_name_validator.png)\n\nFor example - you are using default regex `(.*)_(\\d)*_(?P<shader>.*)_(GEO)` and you have two shaders defined\nin either file or database `foo` and `bar`.\n\nObject named `SomeCube_0001_foo_GEO` will pass but `SomeCube_GEO` will not and `SomeCube_001_xxx_GEO` will not too.\n\n#### Top level group name\nThere is a validation for top level group name too. You can specify whatever regex you'd like to use. Default will\npass everything with `_GRP` suffix. You can use *named capturing groups* to validate against specific data. If you\nput `(?P<asset>.*)` it will try to match everything captured in that group against current asset name. Likewise you can\nuse it for **subset** and **project** - `(?P<subset>.*)` and `(?P<project>.*)`.\n\n**Example**\n\nYou are working on asset (shot) `0030_OGC_0190`. You have this regex in **Top level group name**:\n```regexp\n.*?_(?P<asset>.*)_GRP\n```\n\nWhen you publish your model with top group named like `foo_GRP` it will fail. But with `foo_0030_OGC_0190_GRP` it will pass.\n\n:::info About regex\nAll regexes used here are in Python variant.\n:::\n\n### Maya > Deadline submitter\nThis plugin provides connection between Maya and Deadline. It is using [Deadline Webservice](https://docs.thinkboxsoftware.com/products/deadline/10.0/1_User%20Manual/manual/web-service.html) to submit jobs to farm.\n![Maya > Deadline Settings](assets/maya-admin_submit_maya_job_to_deadline.png)\n\nYou can set various aspects of scene submission to farm with per-project settings in **Setting UI**.\n\n - **Optional** will mark sumission plugin optional\n - **Active** will enable/disable plugin\n - **Tile Assembler Plugin** will set what should be used to assemble tiles on Deadline. Either **Open Image IO** will be used\nor Deadlines **Draft Tile Assembler**.\n - **Use Published scene** enable to render from published scene instead of scene in work area. Rendering from published files is much safer.\n - **Use Asset dependencies** will mark job pending on farm until asset dependencies are fulfilled - for example Deadline will wait for scene file to be synced to cloud, etc.\n - **Group name** use specific Deadline group for the job.\n - **Limit Groups** use these Deadline Limit groups for the job.\n - **Additional `JobInfo` data** JSON of additional Deadline options that will be embedded in `JobInfo` part of the submission data.\n - **Additional `PluginInfo` data** JSON of additional Deadline options that will be embedded in `PluginInfo` part of the submission data.\n - **Scene patches** - configure mechanism to add additional lines to published Maya Ascii scene files before they are used for rendering.\nThis is useful to fix some specific renderer glitches and advanced hacking of Maya Scene files. `Patch name` is label for patch for easier orientation.\n`Patch regex` is regex used to find line in file, after `Patch line` string is inserted. Note that you need to add line ending.\n\n## Load Plugins\n\n### Reference Loader\n\n#### Namespace and Group Name\nHere you can create your own custom naming for the reference loader.\n\nThe custom naming is split into two parts: namespace and group name. If you don't set the namespace, an error will occur.\nGroup name could be set empty, that way no wrapping group will be created for loaded item.\nHere's the different variables you can use:\n\n<div class=\"row markdown\">\n<div class=\"col col--5 markdown\">\n\n| Token | Description |\n|---|---|\n|`{asset_name}` | Asset name |\n|`{asset_type}` | Asset type |\n|`{subset}` | Subset name |\n|`{family}` | Subset family |\n\n</div>\n</div>\n\nThe namespace field can contain a single group of '#' number tokens to indicate where the namespace's unique index should go. The amount of tokens defines the zero padding of the number, e.g ### turns into 001.\n\nWarning: Note that a namespace will always be prefixed with a _ if it starts with a digit.\n\nExample:\n\n![Namespace and Group Name](assets/maya-admin_custom_namespace.png)\n\n### Extract GPU Cache\n\n![Maya GPU Cache](assets/maya-admin_gpu_cache.png)\n\n- **Step** Specifies how often samples are taken during file creation. By default, one sample of your object's transformations is taken every frame and saved to the Alembic file.\n\n  For example, a value of 2 caches the transformations of the current object at every other frame of the Cache Time Range.\n\n- **Step Save** Specifies which samples are saved during cache creation. For example, a value of 2 specifies that only every other sample specified by the Step # frame(s) option is saved to your Alembic file.\n\n- **Optimize Hierarchy** When on, nodes and objects in a selected hierarchy are consolidated to maximize the performance of the cache file during playback.\n- **Optimization Threshold** (Available only when Optimize Hierarchy is on.) Specifies the maximum number of vertices contained in a single draw primitive. The default value of 40000 may be ideal for most Maya supported graphics cards. When set to the default value, after optimization, each object in the GPU cache file(s) will have no more than 40000 vertices. This value can be set higher depending on the memory available on your system graphics card.\n\n- **Optimize Animations for Motion Blur** When on, objects with animated transform nodes display with motion blur when the cache is played back in Viewport 2.0 render mode. See Viewport 2.0 options.\n\n  Maya first determines if the GPU cache includes animation data. If the GPU cache is static and does not contain animation data, Maya does not optimize the GPU cache for motion blur.\n\n:::note Motion Blur does not support Cached Playback.\n:::\n\n- **Write Materials** When on, Maya exports the Lambert and Phong materials from source geometry to the GPU Cache file. These materials display when the GPU-cached file is played back in Viewport 2.0.\n\n  GPU-cached objects support all the high-quality lighting and shading effects provide by the Viewport 2.0 rendering mode. See Viewport 2.0 options.\n\n:::note Lambert and Phong materials do not display on GPU-cached files when they are played back in scene view's High Quality Rendering or Default Quality Rendering modes.\n:::\n\n- **Use Base Tessellation** Exports geometry with base tessellation and no smoothing applied. If this setting is turned off, the extractor will export geometry with the current Smooth Mesh Preview setting applied.\n\n### Extract Playblast Settings (review)\nThese settings provide granular control over how the playblasts or reviews are produced in Maya.\n\nSome of these settings are also available on the instance itself, in which case these settings will become the default value when creating the review instance.\n\n![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings.png)\n\n- **Compression type** which file encoding to use.\n- **Data format** what format is the file encoding.\n- **Quality** lets you control the compression value for the output. Results can vary depending on the compression you selected. Quality values can range from 0 to 100, with a default value of 95.\n- **Background Color** the viewports background color.\n- **Background Bottom** the viewports background bottom color.\n- **Background Top** the viewports background top color.\n- **Override display options** override the viewports display options to use what is set in the settings.\n- **Isolate view** isolates the view to what is in the review instance. If only a camera is present in the review instance, all nodes are displayed in view.\n- **Off Screen** records the playblast hidden from the user.\n- **2D Pan/Zoom** enables the 2D Pan/Zoom functionality of the camera.\n- **Renderer name** which renderer to use for playblasting.\n- **Width** width of the output resolution. If this value is `0`, the asset's width is used.\n- **Height** height of the output resolution. If this value is `0`, the asset's height is used.\n\n#### Viewport Options\n\nMost settings to override in the viewport are self explanatory and can be found in Maya.\n\n![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings_viewport_options.png)\n\n- **Override Viewport Options** enable to use the settings below for the viewport when publishing the review.\n\n#### Camera Options\n\nThese options are set on the camera shape when publishing the review. They correspond to attributes on the Maya camera shape node.\n\n![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings_camera_options.png)\n## Include/exclude handles by task type\nYou can include or exclude handles, globally or by task type.\n\nThe \"Include handles by default\" defines whether by default handles are included. Additionally you can add a per task type override whether you want to include or exclude handles.\n\nFor example, in this image you can see that handles are included by default in all task types, except for the 'Lighting' task, where the toggle is disabled.\n![Include/exclude handles](assets/maya-admin_exclude_handles.png)\n\nAnd here you can see that the handles are disabled by default, except in 'Animation' task where it's enabled.\n![Custom menu definition](assets/maya-admin_include_handles.png)\n\n\n## Custom Menu\nYou can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**.\n![Custom menu definition](assets/maya-admin_scriptsmenu.png)\n\n:::note Work in progress\nThis is still work in progress. Menu definition will be handled more friendly with widgets and not\nraw json.\n:::\n\n## Multiplatform path mapping\nYou can configure path mapping using Maya `dirmap` command. This will add bi-directional mapping between\nlist of paths specified in **Settings**. You can find it in **Settings -> Project Settings -> Maya -> Maya Directory Mapping**\n![Dirmap settings](assets/maya-admin_dirmap_settings.png)\n\n## Templated Build Workfile\n\nBuilding a workfile using a template designed by users. Helping to assert homogeneous subsets hierarchy and imports. Template stored as file easy to define, change and customize for production needs.\n\n **1. Make a template**\n\nMake your template. Add families and everything needed for your tasks. Here is an example template for the modeling task using a placeholder to import a gauge.\n\n![maya outliner](assets/maya-workfile-outliner.png)\n\nIf needed, you can add placeholders when the template needs to load some assets. **OpenPype > Template Builder > Create Placeholder**\n\n![create placeholder](assets/maya-create_placeholder.png)\n\n- **Configure placeholders**\n\nFill in the necessary fields (the optional fields are regex filters)\n\n![new place holder](assets/maya-placeholder_new.png)\n\n\n  - ***Builder type***: Whether the the placeholder should load current asset representations or linked assets representations\n\n  - ***Representation***: Representation that will be loaded (ex: ma, abc, png, etc...)\n\n  - ***Family***: Family of the representation to load (main, look, image, etc ...)\n\n  - ***Loader***: Placeholder loader name that will be used to load corresponding representations\n\n  - ***Order***: Priority for current placeholder loader (priority is lowest first, highest last)\n\n  - ***Loader arguments***: Loader arguments dictionary can be used to pass optional data to loaders.\n  One use case is to define a custom Subset name for the animation instances created while loading Rig references.This follows the custom namespace system used by loaders.\n\n    **Example**\n      ```\n      {\"animationSubsetName\": \"{asset_name}_animation_{subset}_##_\"}\n      ```\n\n\n- **Save your template**\n\n\n **2. Configure Template**\n\n- **Go to Studio settings > Project > Your DCC > Templated Build Settings**\n- Add a profile for your task and enter path to your template\n\n![setting build template](assets/settings/template_build_workfile.png)\n\n**3. Build your workfile**\n\n- Open maya\n\n- Build your workfile\n\n![maya build template](assets/maya-build_workfile_from_template.png)\n\n## Explicit Plugins Loading\nYou can define which plugins to load on launch of Maya here; `project_settings/maya/explicit_plugins_loading`. This can help improve Maya's launch speed, if you know which plugins are needed.\n\nBy default only the required plugins are enabled. You can also add any plugin to the list to enable on launch.\n\n:::note technical\nWhen enabling this feature, the workfile will be launched post initialization no matter the setting on `project_settings/maya/open_workfile_post_initialization`. This is to avoid any issues with references needing plugins.\n\nRenderfarm integration is not supported for this feature.\n:::\n"
  },
  {
    "path": "website/docs/admin_hosts_nuke.md",
    "content": "---\nid: admin_hosts_nuke\ntitle: Nuke\nsidebar_label: Nuke\n---\n\n## Custom Menu\nYou can add your custom tools menu into Nuke by extending definitions in **Nuke -> Scripts Menu Definition**.\n![Custom menu definition](assets/nuke-admin_scriptsmenu.png)\n\n:::note Work in progress\nThis is still work in progress. Menu definition will be handled more friendly with widgets and not\nraw json.\n:::\n\n## Gizmo Menu\nYou can add your custom toolbar menu into Nuke by setting your gizmo path and extending definitions in **Nuke -> Gizmo Menu**.\n![Custom menu definition](assets/nuke-admin_gizmomenu.png)\n"
  },
  {
    "path": "website/docs/admin_hosts_photoshop.md",
    "content": "---\nid: admin_hosts_photoshop\ntitle: Photoshop Settings\nsidebar_label: Photoshop\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## Photoshop settings\n\nThere is a couple of settings that could configure publishing process for **Photoshop**.\nAll of them are Project based, eg. each project could have different configuration.\n\nLocation: Settings > Project > Photoshop\n\n![AfterEffects Project Settings](assets/admin_hosts_photoshop_settings.png)\n\n## Color Management (ImageIO)\n\nPlaceholder for Color Management. Currently not implemented yet.\n\n## Creator plugins\n\nContains configurable items for creators used during publishing from Photoshop.\n\n### Create Image\n\nProvides list of [variants](artist_concepts.md#variant) that will be shown to an artist in Publisher. Default value `Main`.\n\n### Create Flatten Image\n\nProvides simplified publishing process. It will create single `image` instance for artist automatically. This instance will\nproduce flatten image from all visible layers in a workfile.\n\n- Review - should be separate review created for this instance\n\n### Create Review\n\nCreates single `review` instance automatically. This allows artists to disable it if needed.\n\n### Create Workfile\n\nCreates single `workfile` instance automatically. This allows artists to disable it if needed.\n\n## Publish plugins\n\nContains configurable items for publish plugins used during publishing from Photoshop.\n\n### Collect Color Coded Instances\n\nUsed only in remote publishing!\n\nAllows to create automatically `image` instances for configurable highlight color set on layer or group in the workfile.\n\n#### Create flatten image\n  - Flatten with images - produce additional `image` with all published `image` instances merged\n  - Flatten only - produce only merged `image` instance\n  - No - produce only separate `image` instances\n\n#### Subset template for flatten image\n\nTemplate used to create subset name automatically (example `image{layer}Main` - uses layer name in subset name)\n\n### Collect Review\n\nDisable if no review should be created\n\n### Collect Version\n\nIf enabled it will push version from workfile name to all published items. Eg. if artist is publishing `test_asset_workfile_v005.psd`\nproduced `image` and `review` files will contain `v005` (even if some previous version were skipped for particular family).\n\n### Validate Containers\n\nChecks if all imported assets to the workfile through `Loader` are in latest version. Limits cases that older version of asset would be used.\n\nIf enabled, artist might still decide to disable validation for each publish (for special use cases).\nLimit this optionality by toggling `Optional`.\n`Active` toggle denotes that by default artists sees that optional validation as enabled.\n\n### Validate naming of subsets and layers\n\nSubset cannot contain invalid characters or extract to file would fail\n\n#### Regex pattern of invalid characters\n\nContains weird characters like `/`, `/`, these might cause an issue when file (which contains subset name) is created on OS disk.\n\n#### Replacement character\n\nReplace all offending characters with this one. `_` is default.\n\n### Extract Image\n\nControls extension formats of published instances of `image` family. `png` and `jpg` are by default.\n\n### Extract Review\n\nControls output definitions of extracted reviews to upload on Asset Management (AM).\n\n#### Makes an image sequence instead of flatten image\n\nIf multiple `image` instances are produced, glue created images into image sequence (`mov`) to review all of them separetely.\nWithout it only flatten image would be produced.\n\n#### Maximum size of sources for review\n\nSet Byte limit for review file. Applicable if gigantic `image` instances are produced, full image size is unnecessary to upload to AM.\n\n#### Extract jpg Options\n\nHandles tags for produced `.jpg` representation. `Create review` and `Add review to Ftrack` are defaults.\n\n#### Extract mov Options\n\nHandles tags for produced `.mov` representation. `Create review` and `Add review to Ftrack` are defaults.\n\n\n### Workfile Builder\n\nAllows to open prepared workfile for an artist when no workfile exists. Useful to share standards, additional helpful content in the workfile.\n\nCould be configured per `Task type`, eg. `composition` task type could use different `.psd` template file than `art` task.\nWorkfile template must be accessible for all artists.\n(Currently not handled by [SiteSync](module_site_sync.md))\n"
  },
  {
    "path": "website/docs/admin_hosts_resolve.md",
    "content": "---\nid: admin_hosts_resolve\ntitle: DaVinci Resolve Setup\nsidebar_label: DaVinci Resolve\n---\n\n:::warning\nOnly Resolve Studio is supported due to Python API limitation in Resolve (free).\n:::\n\n## Resolve requirements\nDue to the way resolve handles python and python scripts there are a few steps required steps needed to be done on any machine that will be using OpenPype with resolve.\n\n## Basic setup\n\n-   Supported version is up to v18\n-   Install Python 3.6.2 (latest tested v17) or up to 3.9.13 (latest tested on v18)\n-   pip install PySide2:\n    -   Python 3.9.*: open terminal and go to python.exe directory, then `python -m pip install PySide2`\n-   pip install OpenTimelineIO:\n    -   Python 3.9.*: open terminal and go to python.exe directory, then  `python -m pip install OpenTimelineIO`\n    -   Python 3.6: open terminal and go to python.exe directory, then `python -m pip install git+https://github.com/PixarAnimationStudios/OpenTimelineIO.git@5aa24fbe89d615448876948fe4b4900455c9a3e8` and move built files from `./Lib/site-packages/opentimelineio/cxx-libs/bin and lib` to `./Lib/site-packages/opentimelineio/`. I was building it on Win10 machine with Visual Studio Community 2019 and\n    ![image](https://user-images.githubusercontent.com/40640033/102792588-ffcb1c80-43a8-11eb-9c6b-bf2114ed578e.png) with installed CMake in PATH.\n-   make sure Resolve Fusion (Fusion Tab/menu/Fusion/Fusion Settings) is set to Python 3.6\n    ![image](https://user-images.githubusercontent.com/40640033/102631545-280b0f00-414e-11eb-89fc-98ac268d209d.png)\n-   Open OpenPype **Tray/Admin/Studio settings** > `applications/resolve/environment` and add Python3 path to `RESOLVE_PYTHON3_HOME` platform related.\n\n## Editorial setup\n\nThis is how it looks on my testing project timeline\n![image](https://user-images.githubusercontent.com/40640033/102637638-96ec6600-4156-11eb-9656-6e8e3ce4baf8.png)\nNotice I had renamed tracks to `main` (holding metadata markers) and `review` used for generating review data with ffmpeg confersion to jpg sequence.\n\n1.  you need to start OpenPype menu from Resolve/EditTab/Menu/Workspace/Scripts/Comp/**__OpenPype_Menu__**\n2.  then select any clips in `main` track and change their color to `Chocolate`\n3.  in OpenPype Menu select `Create`\n4.  in Creator select `Create Publishable Clip [New]` (temporary name)\n5.  set `Rename clips` to True, Master Track to `main` and Use review track to `review` as in picture\n    ![image](https://user-images.githubusercontent.com/40640033/102643773-0d419600-4160-11eb-919e-9c2be0aecab8.png)\n6.  after you hit `ok` all clips are colored to `ping` and marked with openpype metadata tag\n7.  git `Publish` on openpype menu and see that all had been collected correctly. That is the last step for now as rest is Work in progress. Next steps will follow.\n"
  },
  {
    "path": "website/docs/admin_hosts_tvpaint.md",
    "content": "---\nid: admin_hosts_tvpaint\ntitle: TVPaint\nsidebar_label: TVPaint\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## Subset name templates\nDefinition of possible subset name templates in TVPaint integration.\n\n### Render Layer\nRender layer has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**.\n\n- Key **render_layer** is alias for variant (user's input).\n- For key **render_pass** is used predefined value `\"Beauty\"` (ATM value can't be changed).\n\n### Render pass\nRender pass has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**.\n- Key **render_layer** is filled with value of **render_pass** from `renderLayer` group.\n- Key **render_pass** is alias for variant (user's input).\n\n:::important Render Layer/Pass templates\nIt is recommended to use same subset name template for both **renderLayer** and **renderPass** families.\n- Example template: `\"{family}{Task}_{Render_layer}_{Render_pass}\"`\n:::\n\n### Review and Workfile\nFamilies **review** and **workfile** are not manually created but are automatically generated during publishing. That's why it is recommended to not use **variant** key in their subset name template.\n"
  },
  {
    "path": "website/docs/admin_openpype_commands.md",
    "content": "---\nid: admin_openpype_commands\ntitle: OpenPype Commands Reference\nsidebar_label: OpenPype Commands\n---\n\n:::info\nYou can substitute `openpype_console` with `poetry run python start.py` if you want to run it\ndirectly from sources.\n:::\n\n:::note\nRunning OpenPype without any commands will default to `tray`.\n:::\n\n## Common arguments\n`--use-version` to specify explicit version to use:\n```shell\nopenpype_console --use-version=3.0.0-foo+bar\n```\n`--headless` - to run OpenPype in headless mode (without using graphical UI)\n\n`--use-staging` - to use staging versions of OpenPype.\n\n`--list-versions` - to list available versions.\n\n`--validate-version` - to validate integrity of given version\n\n`--verbose` `<level>` - change log verbose level of OpenPype loggers\n\n`--debug` - set debug flag affects logging\n\nFor more information [see here](admin_use.md#run-openpype).\n\n## Commands\n\n| Command | Description | Arguments |\n| --- | --- |: --- :|\n| contextselection | Open Context selection dialog. |  |\n| module | Run command line arguments for modules. |  |\n| repack-version | Tool to re-create version zip. | [📑](#repack-version-arguments) |\n| tray | Launch OpenPype Tray. | [📑](#tray-arguments)\n| publish | Pype takes JSON from provided path and use it to publish data in it. | [📑](#publish-arguments) |\n| extractenvironments | Extract environment variables for entered context to a json file. | [📑](#extractenvironments-arguments) |\n| run | Execute given python script within OpenPype environment. | [📑](#run-arguments) |\n| interactive | Start python like interactive console session. | |\n| projectmanager | Launch Project Manager UI | [📑](#projectmanager-arguments) |\n| settings | Open Settings UI | [📑](#settings-arguments) |\n\n---\n### `tray` arguments {#tray-arguments}\n\n```shell\nopenpype_console tray\n```\n\n---\n### `publish` arguments {#publish-arguments}\n\nRun publishing based on metadata passed in json file e.g. on farm.\n\n| Argument | Description |\n| --- | --- |\n| `--targets` | define publishing targets (e.g. \"farm\") |\n| `--gui` (`-g`) | Show publishing |\n| Positional argument | Path to metadata json file |\n\n```shell\nopenpype publish <PATH_TO_JSON> --targes farm\n```\n\n---\n### `extractenvironments` arguments {#extractenvironments-arguments}\n\nEntered output filepath will be created if does not exists.\nAll context options must be passed otherwise only openpype's global environments will be extracted.\nContext options are `project`, `asset`, `task`, `app`\n\n| Argument | Description |\n| --- | --- |\n| `output_json_path` | Absolute path to the exported json file |\n| `--project` | Project name |\n| `--asset` | Asset name |\n| `--task` | Task name |\n| `--app` | Application name |\n\n```shell\nopenpype_console /home/openpype/env.json --project Foo --asset Bar --task modeling --app maya-2019\n```\n\n---\n### `run` arguments {#run-arguments}\n\n| Argument | Description |\n| `--script` | run specified python script |\n\nNote that additional arguments are passed to the script.\n\n```shell\nopenpype_console run --script /foo/bar/baz.py arg1 arg2\n```\n\n---\n### `projectmanager` arguments {#projectmanager-arguments}\n`projectmanager` has no command-line arguments.\n```shell\nopenpype_console projectmanager\n```\n\n---\n### `settings` arguments {#settings-arguments}\n\n| Argument | Description |\n| `-d` / `--dev` | Run settings in developer mode. |\n\n```shell\nopenpypeconsole settings\n```\n\n---\n### `repack-version` arguments {#repack-version-arguments}\nTakes path to unzipped and possibly modified OpenPype version. Files will be\nzipped, checksums recalculated and version will be determined by folder name\n(and written to `version.py`).\n\n```shell\n./openpype_console repack-version /path/to/some/modified/unzipped/version/openpype-v3.8.3-modified\n```\n"
  },
  {
    "path": "website/docs/admin_releases.md",
    "content": "---\nid: admin_releases\ntitle: Releases\nsidebar_label: Releases\n---\n\nInformation about releases can be found on GitHub [Releases page](https://github.com/pypeclub/OpenPype/releases).\n\nYou can find features and bugfixes in the codebase or full changelog for advanced users.\n"
  },
  {
    "path": "website/docs/admin_settings.md",
    "content": "---\nid: admin_settings\ntitle: Working with settings\nsidebar_label: Working with settings\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nOpenPype stores all of its settings and configuration in the mongo database. To make the configuration as easy as possible we provide a robust GUI where you can access and change everything that is configurable\n\n**Settings** GUI can be started from the tray menu *Admin -> Studio Settings*.\n\n:::important Studio Settings versus Local Settings\nPlease keep in mind that these settings are set up for the full studio and not per-individual. If you're looking for individual artist settings, you can head to\n[Local Settings](admin_settings_local.md) section in the documentation.\n:::\n\n\n## Categories\n\nWe use simple colour coding to show you any changes to the settings:\n- **Grey**: [OpenPype default](#openpype-defaults)\n- **Green**: [Studio default](#openpype-defaults)\n- **Orange**: [Project Override](#project-overrides)\n- **Blue**: Changed and unsaved value\n\n![Colour coding](assets/settings/settings_colour_coding.png)\n\nYou'll find that settings are split into categories:\n\n### System\n\nSystem sections contains all settings that can be configured on a studio level, but cannot\nbe changed on a per-project basis. These include mostly high level options like path to\nmongo database, toggling major modules on and off and configuring studio wide application\navailability.\n\n### Project\n\nProject tab contains most of OpenPype settings and all of them can be configured and overridden on a per-project basis if need be. This includes most of the workflow behaviors\nlike what formats to export, naming conventions, publishing validations, automatic assets loaders and a lot more.\n\nWe recommend to try to keep as many configurations as possible on a studio level and only override selectively, because micromanaging all of the project settings might become cumbersome down the line. Most of the settings can be safely adjusted and locked on a project\nafter the production started.\n\n## Understanding Overrides\n\nMost of the individual settings can be set and overridden on multiple levels.\n\n### OpenPype defaults\nWhen you first open settings, all of the values and categories will be marked with either\nlight **grey labels** or a **grey vertical bar** on the left edge of the expandable category.\n\nThe grey colouring signifies the value has been left at OpenPype Default. If the default changes in future\nOpenPype versions, these values will be reflect the change after you deploy the new version.\n\n### Studio defaults\n\nAny values that you change and then press save in the bottom right corner, will be saved\nas studio defaults. This means they will stay at those values even if you update your pype.\nTo make it clear which settings are set by you specifically, they are marked with a **green\nedge** and **green labels**, once set.\n\nTo set studio default, just change the value in the system tab and press save. If you want\nto keep the value but add the option to your studio default to protect it from potential\nfuture updates, you ran `right click` and choose `add to studio default`, then press save.\n\nIn the Project settings tab, you need to select the **( Default )** project on the left, to set your studio defaults for projects. The rest works the same as in the System tab.\n\n![studio_defaults](assets/settings/studio_defaults.gif)\n\nYou can also reset any settings to OpenPype default by doing `right click` and `remove from studio default`\n\n![studio_defaults](assets/settings/studio_defaults_remove.gif)\n\n### Project Overrides\n\nMany settings are useful to be adjusted on a per-project basis. To identify project\noverrides, they are marked with **orange edge** and **orange labels** in the settings GUI.\n\nThe process of setting project overrides is similar to setting the Studio defaults. The key difference is to select a particular project you want to be configure. Those projects can be found on the left hand side of the Project Settings tab.\n\nIn the image below you can see all three overrides at the same time.\n1. Deadline has **no changes to the OpenPype defaults** at all — **grey** colour of left bar.\n2. Maya has **studio-wide defaults configured**, which are inherited in the particular project - **green** colour of left bar.\n3. Nuke contains **project specific overrides** - **orange** colour of left bar.\n\n![colours_01](assets/settings/colours_02.png)\n\nOverride colours work as breadcrumbs to allow quick identification of what was changed and where. As you can see on this image, Orange colour is propagated up the hierarchy even though only a single value (sync render version with workfile toggle), was changed.\n\n![override_breadcumbs](assets/settings/override_breadcrumbs.png)\n"
  },
  {
    "path": "website/docs/admin_settings_local.md",
    "content": "---\nid: admin_settings_local\ntitle: Working with local settings\nsidebar_label: Working with local settings\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nOpenPype stores some of it's settings and configuration in local file system. These settings are specific to each individual machine and provides the mechanism for local overrides\n\n**Local Settings** GUI can be started from the tray menu.\n\n![Local Settings](assets/settings/settings_local.png)\n\n## Categories\n\n### OpenPype Mongo URL\nThe **Mongo URL** is the database URL given by your Studio. More details [here](artist_getting_started.md#mongodb).\n\n### General\n**OpenPype Username** : enter your username (if not provided, it uses computer session username by default). This username is used to sign your actions on **OpenPype**, for example the \"author\" on a publish.\n\n**Admin permissions** : When enabled you do not need to enter a password (if defined in Studio Settings) to access to the **Admin** section.\n### Experimental tools\nFuture version of existing tools or new ones.\n### Environments\nLocal replacement of the environment data of each software and additional internal data necessary to be loaded correctly.\n\n### Applications\nLocal override of software executable paths for each version. More details [here](admin_settings_system.md#applications).\n\n### Project Settings\nThe **Project Settings** allows to determine the root folder. More details [here](module_site_sync.md#local-settings).\n"
  },
  {
    "path": "website/docs/admin_settings_project_anatomy.md",
    "content": "---\nid: admin_settings_project_anatomy\ntitle: Project Anatomy\nsidebar_label: Project Anatomy\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n\nProject Anatomy is the most important configuration piece for each project you work on with openPype. \n\nIt defines: \n- Project Root folders\n- File naming conventions\n- Folder structure templates\n- Default project attributes\n- Task Types\n- Applications and Tool versions\n- Colour Management\n- File Formats \n\nAnatomy is the only configuration that is always saved as an project override. This is to make sure that any updates to OpenPype or Studio default values, don't affect currently running productions.\n\n![anatomy_01](assets/settings/anatomy_01.png)\n\n## Roots\n\nRoots define where files are stored with path to a shared folder.  It is required to set the  root path for each platform you are using in the studio. All paths must point to the same folder!\n\n![roots01](assets/settings/anatomy_roots01.png)\n\nIt is possible to set multiple roots when necessary. That may be handy when you need to store a specific type of data on another disk.\n![roots02](assets/settings/anatomy_roots02.png)\n\n\nNote how multiple roots are used here, to push different types of files to different shared storage.\n![roots03](assets/settings/anatomy_roots03.png)\n\n\n## Templates\n\nTemplates define the project's folder structure and filenames. \n\nWe have a few required anatomy templates for OpenPype to work properly, however we keep adding more when needed.\n\n### Available template keys\n\n<div class=\"row markdown\">\n<div class=\"col col--5 markdown\">\n\n\n| Context key | Description |\n| --- | --- |\n| `root[name]` | Path to root folder |\n| `project[name]` | Project's full name |\n| `project[code]` | Project's code |\n| `hierarchy` | All hierarchical parents as subfolders |\n| `asset` | Name of asset or shot |\n| `task[name]` | Name of task |\n| `task[type]` | Type of task |\n| `task[short]` | Short name of task type (eg. 'Modeling' > 'mdl') |\n| `parent` | Name of hierarchical parent |\n| `version` | Version number |\n| `subset` | Subset name |\n| `family` | Main family name |\n| `ext` | File extension |\n| `representation` | Representation name |\n| `frame` | Frame number for sequence files. |\n| `app` | Application Name |\n| `user` | User's login name (can be overridden in local settings) |\n| `output` |  |\n| `comment` |  |\n\n</div>\n<div class=\"col col--7 markdown\">\n\n| Date-Time key | Example result | Description |\n| --- | --- | --- |\n| `d` | 1, 30 | Short day of month |\n| `dd` | 01, 30 | Day of month with 2 digits. |\n| `ddd` | Mon | Shortened week day name. |\n| `dddd` | Monday | Full week day name. |\n| `m` | 1, 12 | Short month number. |\n| `mm` | 01, 12 | Month number with 2 digits. |\n| `mmm` | Jan | Shortened month name. |\n| `mmmm` | January | Full month name. |\n| `yy` | 20 | Shortened year. |\n| `yyyy` | 2020 | Full year. |\n| `H` | 4, 17 | Shortened 24-hour number. |\n| `HH` | 04, 17 | 24-hour number with 2 digits. |\n| `h` | 5 | Shortened 12-hour number. |\n| `hh` | 05 | 12-hour number with 2 digits. |\n| `ht` | AM, PM | Midday part. |\n| `M` | 0 | Shortened minutes number. |\n| `MM` | 00 | Minutes number with 2 digits. |\n| `S` | 0 | Shortened seconds number. |\n| `SS` | 00 | Seconds number with 2 digits. |\n\n</div>\n</div>\n\n### Anatomy reference keys\n\nAnatomy templates have the ability to use \"referenced keys\". Best example is `path` in publish or work templates which just contains references to `folder` and `file` (`{@folder}/{@file}`). Any changes in folder or file template are propagated to the path template. The another example is simplification of version and frame formatting with paddings. You can notice that keys `{@version}` or `{@frame}` are used in default templates. They are referencing `Anatomy` -> `Templates` -> `Version` or `Frame` which handle version and frame formatting with padding.\n\nSo if you set `project_anatomy/templates/defaults/version_padding` to `5` the `{@version}` key will be transformed to `v{version:0>5}` automatically and version number in paths will have 5 numbers -> `v00001`.\n\n### Optional keys\n\nIn some cases of template formatting not all keys are available and should be just ignored. For example `{frame}` should be available only for sequences but we have single publish template. To handle these cases it is possible to use special characters to mark segment of template which should be ignored, if it can't be filled because of missing keys. To mark these segments use `<` and `>`.\n.\nTemplate `{project[code]}_{asset}_{subset}<_{output}><.{@frame}>.{ext}` can handle all 4 possible situations when `output` and `frame` keys are available or not. The optional segments can contain additional text, like in the example dot (`.`) for frame and underscore (`_`) for output, those are also ignored if the keys are not available. Optional segments without formatting keys are kept untouched: `<br/>` -> stays as `<br/>`. It is possible to nest optional segments inside optional segments `<{asset}<.{@frame}><br/>>` which may result in empty string if `asset` key is not available.\n\n## Attributes\n\nProject attributes are used as default values for new assets created under project, except `Applications` and `Active project` which are project specific. Values of attributes that are **not** project specific are always used from assets. So if `tools` are not loading as expected it is because the assets have different values.\n\n![anatomy_attributes](assets/settings/anatomy_attributes.png)\n\n**Most of attributes don't need detailed explanation.**\n\n| Attribute | Description |\n| --- | --- |\n| `Applications` | List of applications that can be used in the project. At the moment used only as a possible filter of applications. |\n| `Tools` | List of application tools. This value can be overridden per asset. |\n| `Active project` | Project won't be visible in tools if enabled.<br/> - To revert check `Show Inactive projects` checkbox in project settings. |\n\n\n## Task Types\n\nAvailable task types on a project. Each task on an asset is referencing a task type on the project which allows access to additional task type attributes. At this moment only `short_name` is available (can be used in templates as `{task[short_name]}`).\n\n![tasks](assets/settings/anatomy_tasks.png)\n\n## Colour Management and Formats"
  },
  {
    "path": "website/docs/admin_settings_system.md",
    "content": "---\nid: admin_settings_system\ntitle: System Settings\nsidebar_label: System settings\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## General\n\nSettings applicable to the full studio.\n\n![general_settings](assets/settings/settings_system_general.png)\n\n### Studio Name\nFull name of the studio (can be used as variable on some places)\n\n### Studio Code\nStudio acronym or a short code (can be used as variable on some places)\n\n### Admin Password\nAfter setting admin password, normal user won't have access to OpenPype settings\nand Project Manager GUI. Please keep in mind that this is a studio wide password and it is meant purely\nas a simple barrier to prevent artists from accidental setting changes.\n\n### Environment\nGlobally applied environment variables that will be appended to any OpenPype process in the studio.\n\n### Disk mapping\n- Platform dependent configuration for mapping of virtual disk(s) on an artist's OpenPype machines before OP starts up.\n- Uses `subst` command, if configured volume character in `Destination` field already exists, no re-mapping is done for that character(volume).\n\n### FFmpeg and OpenImageIO tools\nWe bundle FFmpeg tools for all platforms and OpenImageIO tools for Windows and Linux. By default, bundled tools are used, but it is possible to set environment variables `OPENPYPE_FFMPEG_PATHS` and `OPENPYPE_OIIO_PATHS` in system settings environments to look for them in different directory.\nFor example—when using different Linux distributions in a facility that do not have a consistent install location or to add OIIO support for MacOS. Values of both environment variables should lead to directory where tool executables are located instead of an explicit path to the binary executable. Using multiple paths are supported, separated by colons, is supported—e.g. */usr/local/bin:$HOME/.local/bin*\n\n### OpenPype deployment control\n**`Versions Repository`** - Location where automatic update mechanism searches for zip files with\nOpenPype update packages. To read more about preparing OpenPype for automatic updates go to [Admin Distribute docs](admin_distribute.md#2-openpype-codebase)\n\n**`Production version`** - Define what is current production version. When value is not set then latest version available in versions repository is resolved as production version.\n\n**`Staging version`** - Define what is current staging version. When value is not set then latest staging version available in versions repository is resolved as staging version.\n\nFor more information about Production and Staging go to [Distribute](admin_distribute.md#staging-vs-production).\n\n**Production version** and **Staging version** fields will define which version will be used in studio. Filling explicit version will force new OpenPype processes to use it. That gives more control over studio deployment especially when some workstations don't have access to version repository (e.g. remote users). It can be also used to downgrade studio version when newer version have production breaking bug.\n\nWhen fields are not filled, the latest version in the versions repository is used as studio version. That makes updating easier as it is not needed to modify settings, though workstations without access to versions repository can't find out which OpenPype version should be used.\n\nIf **`Version Repository`** is not set or is not accessible for workstation, the latest available version on workstation is used or the version inside build.\n\n**`Version check interval`** - The OpenPype tray application has the ability to check if its version currently in use is up to date with the Studio's production/staging version. It is possible to modify how often the validation is triggered in minutes. The interval can also be set to `0`, which will turn off version validations, but it is not recommend.\n\nA dialog asking for restart is shown when OpenPype tray application detect that different version should be used.\n![general_settings](assets/settings/settings_system_version_update.png)\n![general_settings](assets/settings/settings_system_version_downgrade.png)\n\n## Modules\n\nConfiguration of OpenPype's various modules. Some can only be toggled on or off, while others have their own attributes that need to be set before they become fully functional.\n\n### Avalon\n\n**`Avalon Mongo Timeout`** - This might need to be changed if your mongo connection is a bit slow. Making the timeout longer will give Avalon better chance to connect.\n\n**`Thumbnail Storage Location`** - simple disk storage path where all thumbnails will be stored.\n\n### Ftrack\n\n**`Server`** - URL of your ftrack server.\n\nAdditional Action paths\n\n**`Action paths`** - Directories containing your custom ftrack actions.\n\n**`Event paths`** - Directories containing your custom ftrack event plugins.\n\n**`Intent`** - Special ftrack attribute that mark the intention of individual publishes. This setting will be reflected\nin publisher as well as ftrack custom attributes\n\n**`Custom Attributes`** - Write and Read permissions for all OpenPype required ftrack custom attributes. Each values needs to be name of an ftrack role.\n\n### Sync Server\n\nDisable/Enable OpenPype site sync feature\n\n### Standalone Publisher\n\nDisable/Enable Standalone Publisher option\n\n### Deadline\n\n**`Deadline Rest URL`** - URL to deadline webservice that. This URL must be reachable from every\nworkstation that should be submitting render jobs to deadline via OpenPype.\n\n### Royal Render\n\n**`Royal Render Root Paths`** - multi platform paths to Royal Render installation.\n\n### Clockify\n\n**`Workspace Name`** - name of the clockify workspace where you would like to be sending all the timelogs.\n\n### Timers Manager\n\n**`Max Idle Time`** - Duration (minutes) of inactivity, after which currently running timer will be stopped.\n\n**`Dialog popup time`** - Time in minutes, before the end of Max Idle ti, when a notification will alert\nthe user that their timer is about to be stopped.\n\n### Idle Manager\n\nService monitoring the activity, which triggers the Timers Manager timeouts.\n\n### Logging\n\nModule that allows storing all logging into the database for easier retrieval and support.\n\n## Applications\n\nIn this section you can manage what Applications are available to your studio, locations of their\nexecutables, and their additional environments. In OpenPype context, each application that is integrated is\nalso called a `Host` and these two terms might be used interchangeably in the documentation.\n\nEach Host is made of two levels.\n1. **Application group** - This is the main name of the application and you can define extra environments\nthat are applicable to all versions of the given application. For example any extra Maya scripts that are not\nversion dependent, can be added to `Maya` environment here.\n2. **Application versions** - Here you can define executables (per platform) for each supported version of\nthe DCC and any default arguments (`--nukex` for instance). You can also further extend it's environment.\n\n![settings_applications](assets/settings/applications_01.png)\n\n### Environments\n\nPlease keep in mind that the environments are not additive by default, so if you are extending variables like\n`PYTHONPATH`, or `PATH` make sure that you add themselves to the end of the list.\n\nFor instance:\n\n```json\n{\n    \"PYTHONPATH\": [\n        \"my/path/to/python/scripts\",\n        \"{PYTHONPATH}\"\n    ]\n}\n```\n\n### Adding versions\n\nIt is possible to add new version for any supported application. There are two ways of doing it.\n\n1. **Add new executable** to an existing application version. This is a good way if you have multiple fully compatible versions of your DCC across the studio. Nuke is a typical example where multiple artists might have different `v#` releases of the same minor Nuke release. For example `12.2v3` and `12.3v6`. When you add both to `12.2` Nuke executables they will be treated the same in OpenPype and the system will automatically pick the first that it finds on an artist machine when launching. Their order is also the order of their priority when choosing which version to run if multiple are present.\n![settings_applications](assets/settings/settings_addapplication.gif)\n\n2. **Add version** in case you want this version to be selectable individually. This is usually used for bigger releases that might not be fully compatible with previous versions. Keep in mind that if you add the latest version of an Application that is not yet part of the official OpenPype release, you might run into problems with integration. We test all the new software versions for compatibility and most often, smaller or bigger updates to OpenPype code are necessary to keep everything running.\n![settings_applications](assets/settings/settings_addappversion.gif)\n\n## Tools\n\nA tool in openPype is anything that needs to be selectively added to your DCC applications. Most often these are plugins, modules, extensions or similar depending on what your package happens to call it.\n\nOpenPype comes with some major CG renderers pre-configured as an example, but these and any others will need to be changed to match your particular environment.\n\nTheir environment settings are split to two levels just like applications to allow more flexibility when setting them up.\n\nIn the image before you can see that we set most of the environment variables in the general MTOA level, and only specify the version variable in the individual versions below. Because all environments within pype setting will resolve any cross references, this is enough to get a fully dynamic plugin loading as far as your folder structure where you store the plugins is nicely organized.\n\n\nIn this example MTOA will automatically will the `MAYA_VERSION`(which is set by Maya Application environment) and `MTOA_VERSION` into the `MTOA` variable. We then use the `MTOA` to set all the other variables needed for it to function within Maya.\n![tools](assets/settings/tools_01.png)\n\nAll the tools defined in here can then be assigned to projects. You can also change the tools versions on any project level all the way down to individual asset or shot overrides. So if you just need to upgrade you render plugin for a single shot, while not risking the incompatibilities on the rest of the project, it is possible.\n"
  },
  {
    "path": "website/docs/admin_use.md",
    "content": "---\nid: admin_use\ntitle: Install and Run \nsidebar_label: Install & Run\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n\n## Install\n\nYou can install OpenPype on individual workstations the same way as any other software. \nWhen you create you build, you will end up with an installation package for the platform \nthat was used for the build.\n\n- Windows: `OpenPype-3.0.0.msi`\n- Linux: `OpenPype-3.0.0.zip`\n- Mac: `OpenPype-3.0.0.dmg`\n\nAfter OpenPype is installed, it will ask the user for further installation if it detects a\nnewer version in the studio update location.\n\n## Run OpenPype\n\nTo use OpenPype on a workstation simply run the executable that was installed.\nOn the first run the user will be prompted to for OpenPype Mongo URL. \nThis piece of information needs to be provided to the artist by the admin setting \nup OpenPype in the studio.\n\nOnce artist enters the Mongo URL address, OpenPype will remember the connection for the \nnext launch, so it is a one time process.From that moment OpenPype will do it's best to \nalways keep up to date with the latest studio updates. \n\nIf the launch was successful, the artist should see a green OpenPype logo in their\ntray menu. Keep in mind that on Windows this icon might be hidden by default, in which case,\nthe artist can simply drag the icon down to the tray.\n\nYou can use following command line arguments:\n\n`--use-version` - to specify version you want to run explicitly, like:\n```shell\nopenpype_console --use-version=3.0.1\n```\n\n`--use-staging` - to specify you prefer staging version. In that case it will be used instead of production one.\n\n:::tip List available versions\nTo list all available versions, use:\n\n```shell\nopenpype_console --list-versions\n```\n:::\n\nIf you want to validate integrity of some available version, you can use:\n\n```shell\nopenpype_console --validate-version=3.3.0\n```\n\nThis will go through the version and validate file content against sha 256 hashes\nstored in `checksums` file.\n\n:::tip Headless mode\nAdd `--headless` to run OpenPype without graphical UI (useful on server or on automated tasks, etc.)\n:::\n\n`--verbose` `<level>` - change log verbose level of OpenPype loggers.\n\nLevel value can be integer in range `0-50` or one of enum strings `\"notset\" (0)`, `\"debug\" (10)`, `\"info\" (20)`, `\"warning\" (30)`, `\"error\" (40)`, `\"critical\" (50)`. Value is stored to `OPENPYPE_LOG_LEVEL` environment variable for next processes.\n\n```shell\nopenpype_console --verbose debug\n```\n\n`--debug` - set debug flag affects logging\n\nEnable debug flag for OpenPype process. Change value of environment variable `OPENPYPE_DEBUG` to `\"1\"`. At this moment affects only OpenPype loggers. Argument `--verbose` or environment variable `OPENPYPE_LOG_LEVEL` are used in preference to affect log level.\n\n```shell\nopenpype_console --debug\n```\n\n### Details\nWhen you run OpenPype from executable, few check are made: \n\n#### Check for mongoDB database connection\nMongoDB connection string is in format:\n```shell\nmongodb[+srv]://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]\n```\nMore on that in [MongoDB documentation](https://docs.mongodb.com/manual/reference/connection-string/).\n\nExample connection strings are `mongodb://local-unprotected-server:2707` or\n`mongodb+srv://user:superpassword:some.mongodb-hosted-on.net:27072`.\n\nWhen you start OpenPype first time, Igniter UI will show up and ask you for this string. It will then\nsave it in secure way to your systems keyring - on Windows it is **Credential Manager**, on MacOS it will use its\n**Keychain**, on Linux it can be **GNOME Keyring** or other software, depending on your distribution.\n\nThis can be also set beforehand with environment variable `OPENPYPE_MONGO`. If set it takes precedence\nover the one set in keyring.\n\n:::tip Minimal permissions for DB user\n- `readWrite` role to `openpype` and `avalon` databases\n- `find` permission on `openpype`, `avalon` and `local`\n  \n#### Check for OpenPype version path\nWhen connection to MongoDB is made, OpenPype will get various settings from there - one among them is\ndirectory location where OpenPype versions are stored. If this directory exists OpenPype tries to\nfind the latest version there and if succeed it will copy it to user system and use it.\n\nThis path can be set is OpenPype settings, but also with environment variable `OPENPYPE_PATH` or with\n`openPypePath` in json file located application directory depending on your system.\n\n- Windows: `%LOCALAPPDATA%\\pypeclub\\openpype`\n- Linux: `~/.local/share/pypeclub/openpype`\n- Mac: `~/Library/Application Support/pypeclub/openpype`\n\n### Runtime provided environment variables\nOpenPype is providing following environment variables for its subprocesses that can be used\nin various places, like scripting, etc.\n\n- `OPENPYPE_ROOT` - this will point to currently running code. \n- `OPENPYPE_VERSION` - string of current version - like `3.0.0-foo+bar`\n- `OPENPYPE_REPOS_ROOT` - this is path where all components can be find (like Avalon Core and OpenPype)\n- `OPENPYPE_DATABASE_NAME` - database name in MongoDB used by OpenPype\n- `OPENPYPE_EXECUTABLE` - path to executable used to run OpenPype - when run from sources it will point\nto **python** stored in virtual environment. If run from frozen code, it will point to either `openpype_gui` or\n  `openpype_console`.\n"
  },
  {
    "path": "website/docs/admin_webserver_for_webpublisher.md",
    "content": "---\nid: admin_webserver_for_webpublisher\ntitle: Webserver for webpublisher\nsidebar_label: Webserver for webpublisher\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nRunning Openpype webserver is needed as a backend part for Web publishing. \nAny OS supported by Openpype could be used as a host server.\n\nWebpublishing consists of two sides, Front end (FE) and Openpype backend. This documentation is only targeted on OP side.\n\nIt is expected that FE and OP will live on two separate servers, FE publicly available, OP safely in customer network.\n\n# Requirements for servers\n- OP server allows access to its `8079` port for FE. (It is recommended to whitelist only FE IP.)\n- have shared folder for published resources (images, workfiles etc) on both servers\n\n# Prepare Ftrack\nCurrent webpublish process expects authentication via Slack. It is expected that customer has users created on a Ftrack\nwith same email addresses as on Slack. As some customer might have usernames different from emails, conversion from email to username is needed.\n\nFor this \"pype.club\" user needs to be present on Ftrack, creation of this user should be standard part of Ftrack preparation for Openpype.\nNext create API key on Ftrack, store this information temporarily as you won't have access to this key after creation.\n\n\n# Prepare Openpype\n\nDeploy OP build distribution (Openpype Igniter) on an OS of your choice.\n\n##Run webserver as a Linux service:\n\n(This expects that OP Igniter is deployed to `opt/openpype` and log should be stored in `/tmp/openpype.log`)\n\n- create file `sudo vi /opt/openpype/webpublisher_webserver.sh`\n\n- paste content\n```sh\n#!/usr/bin/env bash\nexport OPENPYPE_DEBUG=3\nexport FTRACK_BOT_API_USER=YOUR_API_USER\nexport FTRACK_BOT_API_KEY=YOUR_API_KEY\nexport PYTHONDONTWRITEBYTECODE=1\nexport OPENPYPE_MONGO=YOUR_MONGODB_CONNECTION\n\npushd /opt/openpype\n./openpype_console webpublisherwebserver --upload_dir YOUR_SHARED_FOLDER_ON_HOST  --executable /opt/openpype/openpype_console  --host YOUR_HOST_IP --port YOUR_HOST_PORT > /tmp/openpype.log 2>&1\n```\n\n1. create service file `sudo vi /etc/systemd/system/openpye-webserver.service`\n\n2. paste content\n```sh\n[Unit]\nDescription=Run OpenPype Ftrack Webserver Service\nAfter=network.target\n\n[Service]\nType=idle\nExecStart=/opt/openpype/webpublisher_webserver.sh\nRestart=on-failure\nRestartSec=10s\nStandardOutput=append:/tmp/openpype.log\nStandardError=append:/tmp/openpype.log\n\n[Install]\nWantedBy=multi-user.target\n```\n\n5.  change file permission:\n    `sudo chmod 0755 /etc/systemd/system/openpype-webserver.service`\n\n6.  enable service:\n    `sudo systemctl enable openpype-webserver`\n\n7.  start service:\n    `sudo systemctl start openpype-webserver`\n    \n8. Check `/tmp/openpype.log` if OP got started\n\n(Note: service could be restarted by `service openpype-webserver restart` - this will result in purge of current log file!)"
  },
  {
    "path": "website/docs/artist_concepts.md",
    "content": "---\nid: artist_concepts\ntitle: Key concepts\nsidebar_label: Key Concepts\n---\n\n## Glossary\n\n### Asset\n\nIn our pipeline all the main entities the project is made from are internally considered *'Assets'*. Episode, sequence, shot, character, prop, etc. All of these behave identically in the pipeline. Asset names need to be absolutely unique within the project because they are their key identifier.\n\nOpenPype has a limitation regarding duplicated names. Name of assets must be unique across whole project.\n\n### Subset\n\nA published output from an asset results in a subset.\n\nThe subset type is referred to as [family](#family), for example a rig, pointcache, look.\nA single asset can have many subsets, even of a single family, named [variants](#variant).\nBy default a subset is named as a combination of family + variant, sometimes prefixed with the task name (like workfile).\n\n### Variant\n\nUsually, an asset needs to be created in multiple *'flavours'*. A character might have multiple different looks, model needs to be published in different resolutions, a standard animation rig might not be usable in a crowd system and so on. 'Variants' are here to accommodate all this variety that might be needed within a single asset. A model might have variant: *'main'*, *'proxy'*, *'sculpt'*, while data of *'look'* family could have subsets *'main'*, *'dirty'*, *'damaged'*. Variants have some recommendations for their names, but ultimately it's up to the artist to use them for separation of publishes when needed.\n\n### Version\n\nA numbered iteration of a given subset. Each version contains at least one [representation](#representation).\n\n#### Hero version\n\nA hero version is a version that is always the latest published version. When a new publish is generated its written over the previous hero version replacing it in-place as opposed to regular versions where each new publish is a higher version number.\n\nThis is an optional feature. The generation of hero versions can be completely disabled in OpenPype by an admin through the Studio Settings.\n\n### Representation\n\nEach published subset version can come out of the software in multiple representations. All of them hold exactly the same data, but in different formats. A model, for example, might be saved as `.OBJ`, Alembic, Maya geometry or as all of them, to be ready for pickup in any other applications supporting these formats.\n\n\n#### Naming convention\n\nAt this moment names of assets, tasks, subsets or representations can contain only letters, numbers and underscore.\n\n### Family\n\nEach published [subset](#subset) can have exactly one family assigned to it. Family determines the type of data that the subset holds. Family doesn't dictate the file type, but can enforce certain technical specifications. For example OpenPype default configuration expects `model` family to only contain geometry without any shaders or joints when it is published.\n\n### Task\n\nA task defines a work area for an asset where an artist can work in. For example asset *characterA* can have tasks named *modeling* and *rigging*. Tasks also have types. Multiple tasks of the same type may exist on an asset. A task with type `fx` could for example appear twice as *fx_fire* and *fx_cloth*.\n\nWithout a task you cannot launch a host application.\n\n### Workfile\n\nThe source scene file an artist works in within their task. These are versioned scene files and can be loaded and saved (automatically named) through the [workfiles tool](artist_tools_workfiles.md).\n\n### Host\n\nGeneral term for Software or Application supported by OpenPype and Avalon. These are usually DCC applications like Maya, Houdini or Nuke, but can also be a web based service like Ftrack or Clockify.\n\n### Tool\n\nSmall piece of software usually dedicated to a particular purpose. Most of OpenPype and Avalon tools have GUI, but some are command line only.\n\n\n### Publish\n\nProcess of exporting data from your work scene to versioned, immutable file that can be used by other artists in the studio.\n\n#### (Publish) Instance\n\nA publish instance is a single entry which defines a publish output. Publish instances persist within the workfile. This way we can expect that a publish from a newer workfile will produce similar consistent versioned outputs.\n\n### Load\n\nProcess of importing previously published subsets into your current scene, using any of the OpenPype tools.\nLoading asset using proper tools will ensure that all your scene content stays version controlled and updatable at a later point.\n"
  },
  {
    "path": "website/docs/artist_ftrack.md",
    "content": "---\nid: artist_ftrack\ntitle: Ftrack\nsidebar_label: Artist\n---\n\n# How to use Ftrack in OpenPype\n\n## Login to Ftrack module in OpenPype (best case scenario) \n1. Launch OpenPype and go to systray OpenPype icon.\n2. *Ftrack login* window pop up on start or press **login** in **Ftrack menu** to pop up *Ftrack login* window\n\n    ![ftrack-login-2](assets/ftrack/ftrack-login_50.png)\n\n   - Press `Ftrack` button\n   \n        ![Login widget](assets/ftrack/ftrack-login_1.png)\n   - Web browser opens\n   \n   - Sign into Ftrack if requested. If you are already signed in to Ftrack via web browser, you can jump to [Application launch](#application-launch-best-case-scenario)\n\n    ![ftrack-login-2](assets/ftrack/ftrack-login_2.png)\n\n3. Message is shown\n\n![ftrack-login-3](assets/ftrack/ftrack-login_3.png)\n\n4. Close message and you're ready to use actions - continue with [Application launch](#application-launch-best-case-scenario)\n\n\n---\n\n## Application launch (best case scenario)\n1. Make sure OpenPype is running and you passed [Login to Ftrack](#login-to-ftrack-module-in-openpype-best-case-scenario) guide\n   \n2. Open Web browser and go to your studio Ftrack web page *(e.g. https://mystudio.ftrackapp.com/)*\n   \n3. Locate the task to run the application on.\n   \n4. Display actions for the task\n    ![ftrack-login-3](assets/ftrack/ftrack-login_60.png)\n   \n5. Select application you want to launch\n    - application versions may be grouped to one action. In that case, press the action to reveal versions to choose from *(like Maya in the picture)*, only applications permitted on the particular project will appear. \n    ![ftrack-login-3](assets/ftrack/ftrack-login_71-small.png)\n   \n6. Start working ;)\n\n---\n\n## Change Ftrack user\n1. Log out the previous user from Ftrack Web app *(skip if new is already logged)*\n\n    ![ftrack-login-3](assets/ftrack/ftrack-login_80-small.png)\n\n2. Log out the previous user from Ftrack module in OpenPype tray\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\n![ftrack-login-3](assets/ftrack/ftrack_logout.gif)\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![ftrack-login-3](assets/ftrack/ftrack-login_81.png)\n\n</div>\n</div>\n\n<br></br>\n\n3. Follow [Login to Ftrack](#login-to-ftrack-module-in-openpype-best-case-scenario) guide\n\n---\n\n## Where to find API key\n- Your API key can be found in Ftrack. In the upper right corner of Ftrack click on the avatar, choose System settings. \n- You shouldn't need to use your personal API key if previous steps went through correctly\n  \n    ![ftrack-api](assets/ftrack/ftrack-api.png)\n\n- Scroll down in left panel and select `API keys`. Then pick `Create` button.\n  \n    ![ftrack-api](assets/ftrack/ftrack-api2.png)\n\n- New window will pop up. Choose the `API role` and press `Save`\n  \n    ![ftrack-api](assets/ftrack/ftrack-api3.png)\n\n- Then your new API will be created. \n  \n    ![ftrack-api](assets/ftrack/ftrack-api4.png)\n\n- Copy them and put it into the Ftrack login window.\n  \n    ![ftrack-api](assets/ftrack/ftrack-login-api.png)\n\n\n---\n## What if...\n\n### Ftrack login window didn't pop up and Ftrack menu is not in tray\n**1. possibility - OpenPype didn't load properly**\n- try to restart OpenPype\n\n**2. possibility - Ftrack is not set in OpenPype**\n- inform your administrator or supervisor\n\n\n### Web browser did not open\n**1. possibility - button was not pressed**\n- Try to press again the `Ftrack` button in *Ftrack login* window\n\n**2. possibility - Ftrack URL is not set or is not right**\n- Check **Ftrack URL** value in *Ftrack login* window\n- Inform your administrator if URL is incorrect and launch OpenPype again when administrator fix it\n\n**3. possibility - Ftrack Web app can't be reached the way OpenPype use it**\n- Enter your **Username** and [API key](#where-to-find-api-key) in *Ftrack login* window and press **Login** button\n\n  ![ftrack-api](assets/ftrack/ftrack-api.gif)\n### Ftrack action menu is empty\n**1. possibility - OpenPype is not running**\n- launch OpenPype and check if it is running in systray\n\n**2. possibility - You didn't go through Login to Ftrack guide**\n- please go through [Login to Ftrack](#login-to-ftrack-module-in-openpype-best-case-scenario) guide\n\n**3. possibility - User logged to Ftrack Web is not the same as user logged to Ftrack module in tray**\n- Follow [Change user](#change-ftrack-user) guide\n\n**4. possibility - Project doesn't have applications set correctly**\n- ask your Project Manager to check if he set applications for the project\n"
  },
  {
    "path": "website/docs/artist_getting_started.md",
    "content": "---\ntitle: Getting started with OpenPype\nsidebar_label: Getting started\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n\n\n## Working in the studio\n\nIn studio environment you should have OpenPype already installed and deployed,  so you can start using it without much setup. Your admin has probably put OpenPype icon on your desktop or even had your computer set up so OpenPype will start automatically.\n\nIf this is not the case, please contact your administrator to consult on how to launch OpenPype in your studio\n\n## Working from home\n\nIf you are working from **home** though, you'll **need to install** it yourself. You should, however, receive the OpenPype installer files from your studio\nadmin, supervisor or production, because OpenPype versions and executables might not be compatible between studios.\n\nInstalling OpenPype is possible by Installer or by unzipping downloaded ZIP archive to any drive location.\n\n:::tip Using the OpenPype Installer\nSee the [Installation section](artist_install.md) for more information on how to use the OpenPype Installer\n:::\n\n\nYou can run OpenPype by desktop \"OP\" icon (if it exists after installing) or by directly executing\n\n**openpype_gui.exe** located in the OpenPype folder. This executable being suitable **for artists**.\n\nor alternatively by\n\n**openpype_console.exe** which is more suitable for **TDs/Admin** for debugging and error reporting. This one runs with\nopened console window where all the necessary info will appear during user's work session.\n\n:::tip Is OpenPype running?\nOpenPype runs in the operating system's tray. If you see turquoise OpenPype icon in the tray you can easily tell OpenPype is currently running.\nKeep in mind that on Windows this icon might be hidden by default, in which case, the artist can simply drag the icon down to the tray.\n:::\n\n![Systray](assets/artist_systray.png)\n\n\n## First Launch\n\n\nWhen you first start OpenPype, you will be asked to fill in some basic information.\n\n### MongoDB\n\nIn most cases you will only have to supply the MongoDB Address.\nIt's the database URL you should have received from your Studio admin and often will look like this\n\n`mongodb://username:passwword@mongo.mystudiodomain.com:12345`\n\n or\n\n `mongodb://192.168.100.15:27071`\n\nit really depends on your studio setup. When OpenPype Igniter\nasks for it, just put it in the corresponding text field and press `install` button.\n\n### OpenPype Version Repository\n\nSometimes your Studio might also ask you to fill in the path to its version\nrepository. This is a location where OpenPype will search for the latest versions, check\nif it's up to date and where updates are installed from automatically.\n\nThis path is usually taken from the database directly, so you shouldn't need it.\n\n\n## Updates\n\nIf you're connected to your Studio, OpenPype will check for, and install updates automatically every time you run it. That's why during the first start it can go through a quick update installation process, even though you might have just installed it.\n\n\n## Advanced Usage\n\nFor more advanced use of OpenPype commands please visit [Admin section](admin_openpype_commands.md).\n"
  },
  {
    "path": "website/docs/artist_hosts_3dsmax.md",
    "content": "---\nid: artist_hosts_3dsmax\ntitle: 3dsmax\nsidebar_label: 3dsmax\n---\n\n:::note Work in progress\nThis part of documentation is still work in progress.\n:::\n\n<!-- ## OpenPype Global Tools\n\n-   [Set Context](artist_tools_context_manager)\n-   [Work Files](artist_tools_workfiles)\n-   [Create](artist_tools_creator)\n-   [Load](artist_tools_loader)\n-   [Manage (Inventory)](artist_tools_inventory)\n-   [Publish](artist_tools_publisher)\n-   [Library Loader](artist_tools_library_loader)\n-->\n\n\n## First Steps With OpenPype\n\nLocate **OpenPype Icon** in the OS tray (if hidden dive in the tray toolbar).\n\n> If you cannot locate the OpenPype icon ...it is not probably running so check [Getting Started](artist_getting_started.md) first.\n\nBy clicking the icon  ```OpenPype Menu``` rolls out.\n\nChoose ```OpenPype Menu > Launcher``` to open the ```Launcher``` window.\n\nWhen opened you can **choose** the **project** to work in from the list. Then choose the particular **asset** you want to work on then choose **task**\nand finally **run 3dsmax by its icon** in the tools.\n\n![Menu OpenPype](assets/3dsmax_tray_OP.png)\n\n:::note Launcher Content\nThe list of available projects, assets, tasks and tools will differ according to your Studio and need to be set in advance by supervisor/admin.\n:::\n\n## Running in the 3dsmax\n\nIf 3dsmax has been launched via OP Launcher there should be **OpenPype Menu** visible in 3dsmax **top header** after start.\nThis is the core functional area for you as a user. Most of your actions will take place here.\n\n![Menu OpenPype](assets/3dsmax_menu_first_OP.png)\n\n:::note OpenPype Menu\nUser should use this menu exclusively for **Opening/Saving** when dealing with work files not standard ```File Menu``` even though user still being able perform file operations via this menu but preferably just performing quick saves during work session not saving actual workfile versions.\n:::\n\n## Working With Scene Files\n\nIn OpenPype menu first go to ```Work Files``` menu item so **Work Files  Window** shows up.\n\n Here you can perform Save / Load actions as you would normally do with ```File Save ``` and ```File Open``` in the standard 3dsmax ```File Menu``` and navigate to different project components like assets, tasks, workfiles etc.\n\n\n![Menu OpenPype](assets/3dsmax_menu_OP.png)\n\nYou first choose particular asset and assigned task and corresponding workfile you would like to open.\n\nIf not any workfile present simply hit ```Save As``` and keep ```Subversion``` empty and hit ```Ok```.\n\n![Save As Dialog](assets/3dsmax_SavingFirstFile_OP.png)\n\nOpenPype correctly names it and add version to the workfile. This basically happens whenever user trigger ```Save As``` action. Resulting into incremental version numbers like\n\n```workfileName_v001```\n\n```workfileName_v002```\n\n etc.\n\nBasically meaning user is free of guessing what is the correct naming and other necessities to keep everything in order and managed.\n\n> Note: user still has also other options for naming like ```Subversion```, ```Artist's Note``` but we won't dive into those now.\n\nHere you can see resulting work file after ```Save As``` action.\n\n![Save As Dialog](assets/3dsmax_SavingFirstFile2_OP.png)\n\n## Understanding Context\n\nAs seen on our example OpenPype created pretty first workfile and named it ```220901_couch_modeling_v001.max``` meaning it sits in the Project ```220901``` being it ```couch``` asset and workfile being ```modeling``` task and obviously ```v001``` telling user its first existing version of this workfile.\n\nIt is good to be aware that whenever you as a user choose ```asset``` and ```task``` you happen to be in so called **context** meaning that all user actions are in relation with particular ```asset```. This could be quickly seen in host application header and ```OpenPype Menu``` and its accompanying tools.\n\n![Workfile Context](assets/3dsmax_context.png)\n\n> Whenever you choose different ```asset``` and its ```task``` in **Work Files window** you are basically changing context to the current asset/task you have chosen.\n\n\nThis concludes the basics of working with workfiles in 3dsmax using OpenPype and its tools. Following chapters will cover other aspects like creating multiple assets types and their publishing for later usage in the production.\n\n---\n\n## Creating and Publishing Instances\n\n:::warning Important\nBefore proceeding further please check [Glossary](artist_concepts.md) and [What Is Publishing?](artist_publish.md) So you have clear idea about terminology.\n:::\n\n\n### Intro\n\nCurrent OpenPype integration (ver 3.15.0) supports only ```PointCache```,  ```Camera```, ```Geometry``` and ```Redshift Proxy``` families now.\n\n**Pointcache** family being basically any geometry outputted as Alembic cache (.abc) format\n\n**Camera** family being 3dsmax Camera object with/without animation outputted as native .max, FBX, Alembic format\n\n**Redshift Proxy** family being Redshift Proxy object with/without animation outputted as rs format(Redshift Proxy's very own format)\n---\n\n:::note Work in progress\nThis part of documentation is still work in progress.\n:::\n\n## Validators\n\nCurrent Openpype integration supports different validators such as Frame Range and Attributes.\nSome validators are mandatory while some are optional and user can choose to enable them in the setting.\n\n**Validate Frame Range**: Optional Validator for checking Frame Range\n\n**Validate Attributes**: Optional Validator for checking if object properties' attributes are valid\n    in MaxWrapper Class.\n:::note\n    Users can write the properties' attributes they want to check in dict format in the setting\n    before validation. The attributes are then to be converted into Maxscript and do a check.\n    E.g. ```renderers.current.separateAovFiles``` and ```renderers.current.PrimaryGIEngine```\n    User can put the attributes in the dict format below\n    ```\n    {\n        \"renderer.current\":{\n            \"separateAovFiles\" : True\n            \"PrimaryGIEngine\": \"#RS_GIENGINE_BRUTE_FORCE\"\n        }\n    }\n    ```\n    ![Validate Attribute Setting](assets/3dsmax_validate_attributes.png)\n:::\n## ...to be added\n"
  },
  {
    "path": "website/docs/artist_hosts_aftereffects.md",
    "content": "---\nid: artist_hosts_aftereffects\ntitle: AfterEffects\nsidebar_label: AfterEffects\n---\n<!-- based on PS implementation, same principle and menu -->\n## Available Tools\n\n-   [Work Files](artist_tools_workfiles)\n-   [Create](artist_tools_creator)\n-   [Load](artist_tools_loader)\n-   [Publish](artist_tools_publisher)\n-   [Manage](artist_tools_inventory)\n\n## Setup\n\nTo install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`.\n\nDrag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself.\n\n## Implemented functionality\n\nAfterEffects implementation currently allows you to import and add various media to composition (image plates, renders, audio files, video files etc.)\nand send prepared composition for rendering to Deadline or render locally.\n\n## Usage\n\nWhen you launch AfterEffects you will be met with the Workfiles app. If don't\nhave any previous workfiles, you can just close this window.\n\nWorkfiles tools takes care of saving your .AEP files in the correct location and under\na correct name. You should use it instead of standard file saving dialog.\n\nIn AfterEffects you'll find the tools in the `OpenPype` extension:\n\n![Extension](assets/photoshop_extension.png)\n\nYou can show the extension panel by going to `Window` > `Extensions` > `OpenPype`.\n\n### Publish\n\nWhen you are ready to share some work, you will need to publish it. This is done by opening the `Publisher` through the `Publish...` button.\n\nThere is always instance for workfile created automatically (see 'workfileCompositing' item in `Subsets to publish` column.) This allows to publish (and therefore backup)\nworkfile which is used to produce another publishable elements (as `image` and `review` items).\n\nMain publishable item in AfterEffects will be of `render` family. Result of this item (instance) is picture sequence that could be a final delivery product or loaded and used in another DCCs.\n\nFirst select existing composition and then press `Create >>>` in middle column of `Publisher`.\n\nAfter this process you should have something like this:\n\n![Highlights](assets/aftereffects_publish_instance.png)\n\nName of publishable instance (eg. subset name) could be configured with a template in `project_settings/global/tools/creator/subset_name_profiles`.\n(This must be configured by admin who has access to Openpype Settings.)\n\nTrash icon under the list of instances allows to delete any selected `render` instance.\n\nFrame information (frame start, duration, fps) and resolution (width and height) is applied to selected composition from Asset Management System (Ftrack or DB) automatically!\n(Eg. number of rendered frames is controlled by settings inserted from supervisor. Artist can override this by disabling validation only in special cases.)\n\nWorkfile instance will be automatically recreated though. If you do not want to publish it, use pill toggle on the instance item.\n\nIf you would like to modify publishable instance, click on `Publish` tab at the top. This would allow you to change name of publishable\ninstances, disable them from publishing, change their task etc.\n\nPublisher allows publishing into different context, just click on any instance, update `Variant`, `Asset` or `Task` in the form in the middle and don't forget to click on the 'Confirm' button.\n\n#### RenderQueue\n\nAE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module.\nCurrently its expected to have only single render item per composition in the Render Queue.\n\n\nAE might throw some warning windows during publishing locally, so please pay attention to them in a case publishing seems to be stuck in a `Extract Local Render`.\n\n#### Repair Validation Issues\n\nIf you would like to run validation rules set by your Studio, click on funnel icon at the bottom right. This will run through all\nenabled instances, you could see more information after clicking on `Details` tab.\n\nIf there is some issue in validator phase, you will receive something like this:\n![Validation error](assets/aftereffects_publish_failed.png)\n\nAll validators will give some description about what the issue is. You can inspect this by clicking on items in the left column.\n\nIf there is an option of automatic repair, there will be `Repair` button on the right. In other case you need to fix the issue manually.\n(By deleting and recreating instance, changing workfile setting etc.)\n\n#### Render instance options\n\nThere are currently 2 options of `render` item:\n- Render of farm - allows offload rendering and publishing to Deadline - requires Deadline module being enabled\n- Validate Scene Settings - enables validation plugin which controls setting in DB (or asset control system like Ftrack) and scene itself\n\n![Configuration of render instance](assets/aftereffects_render_instance.png)\n\n#### Buttons on the bottom right are for:\n- `Refresh publishing` - set publishing process to starting position - useful if previous publish failed, or you changed configuration of a publish\n- `Stop/pause publishing` - if you would like to pause publishing process at any time\n- `Validate` - if you would like to run only collecting and validating phases (nothing will be published yet)\n- `Publish` - standard way how to kick off full publishing process\n\n#### Support help\nIf you would like to ask for help admin or support, you could use any of the three options on the `Note` button on bottom left:\n- `Go to details` - switches into a more detailed list of published instances and plugins.\n- `Copy report` - stash full publishing log to a clipboard\n- `Export report` - save log into a file for sending it via mail or any communication tool\n\nIf you are able to fix the workfile yourself, use the first button on the right to set the UI to initial state before publish. (Click the `Publish` button to start again.)\n\n#### Legacy instances\n\nAll screenshots from Publish are from updated dialog, before publishing was being done by regular `Pyblish` tool.\nNew publishing process should be backward compatible, eg. if you have a workfile with instances created in the previous publishing approach, they will be translated automatically and\ncould be used right away.\n\nIf you hit on unexpected behaviour with old instances, contact support first, then you could try to delete and recreate instances from scratch.\nNuclear option is to purge workfile metadata in `Window > Metadata > Basic > Label`. This is only for most determined daredevils though!\n\n### Load\n\nWhen you want to load existing published work, you can use the `Loader` tool. You can reach it in the extension's panel.\n\n![Loader](assets/photoshop_loader.png) <!-- picture needs to be changed -->\n\nThe supported families for loading into AfterEffects are:\n\n- `image`\n- `plate`\n- `render`\n- `prerender`\n- `review`\n- `audio`\n- `background` `(set of images sorted in predefined order)`\n\nTo load an item, right-click on the subset you want and choose a representation you want to load:\n\n![Loader](assets/photoshop_loader_load.gif)\n\n### Manage\n\nNow that we have some content loaded, you can manage which version is loaded. This is done through the `Scene Manager`. You can reach it through the extension's `Manage` button.\n\n:::note\nLoaded images have to stay as smart layers in order to be updated. If you rasterize the layer, you can no longer update it to a different version using OpenPype tools.\n:::\n\n![Loader](assets/photoshop_manage.png)\n\nYou can switch to a previous version of the image or update to the latest.\n\n![Loader](assets/photoshop_manage_switch.gif)\n![Loader](assets/photoshop_manage_update.gif)\n\n\n### Setting section\n\nComposition properties should be controlled by state in Asset Management System (Ftrack etc). Extension provides couple of buttons to trigger this propagation.\n\n#### Set Resolution\n\nSet width and height from AMS to composition.\n\n#### Set Frame Range\n\nStart frame and duration in workarea is set according to the settings in AMS. Handles are incorporated (not inclusive).\nIt is expected that composition(s) is selected first before pushing this button!\n\n#### Apply All Settings\n\nBoth previous settings are triggered at same time.\n\n### Experimental tools\n\nCurrently empty. Could contain special tools available only for specific hosts for early access testing.\n\n\n### Workfile builder section\n\nNext 3 menu items handle creation and usage advanced workfile builder. This could be used to prepare workfile template with placeholders for loaded items and publishable items.\nThis allow to build template with layers for guides, any text layers and layer for image content which get dynamically populated when template is used an populated by an artist.\n\n#### Create placeholder\n\nLoad or Create placeholders could be used to provide dynamic content or preparation steps for publish.\n\n##### Load Placeholder\n\nThis one provide way how to load particular representation for particular subset for particular asset for particular task.\nEg. Whenever artist start `animation` task they want to load `png` representation of `imageMain` subset of current context's asset.\n\n![Load placeholder](assets/aftereffects_load_placeholder.png)\n\n#### Create Placeholder\n\nThis allows to create new composition and when populated it will be enhanced with metadata for publish instance which will be created.\n\n![Create placeholder](assets/aftereffects_create_placeholder.png)\n\n\n### Example\n\nThis is how it looks when `Load placeholder` was used to create `LOADERPLACEHOLDER` item which is added as a layer into `CREATEPLACEHOLDER` composition\ncreated by `Create placeholder`.\n\n![Prepared template](assets/aftereffects_prepared_template.png)\n\nLoad placeholder was configured to load `image` family, subset with name `imageMain` and `png` representation.\n\nAny additional layers could be added into composition, when `Build Workfile from template` will be used by an artist, load placeholders will be replace\nby loaded item(s).\n\n![Prepared template](assets/aftereffects_populated_template.png)\n\nSame prepared template could be used for any asset, in each case correct asset according to context will be used automatically.\n"
  },
  {
    "path": "website/docs/artist_hosts_blender.md",
    "content": "---\nid: artist_hosts_blender\ntitle: Blender\nsidebar_label: Blender\n---\n\n## OpenPype global tools\n\n-   [Set Context](artist_tools_context_manager)\n-   [Work Files](artist_tools_workfiles)\n-   [Create](artist_tools_creator)\n-   [Load](artist_tools_loader)\n-   [Manage (Inventory)](artist_tools_inventory)\n-   [Publish](artist_tools_publisher)\n-   [Library Loader](artist_tools_library_loader)\n\n## Working with OpenPype in Blender\n\nOpenPype is here to ease you the burden of working on project with lots of\ncollaborators, worrying about naming, setting stuff, browsing through endless\ndirectories, loading and exporting and so on. To achieve that, OpenPype is using\nconcept of being _\"data driven\"_. This means that what happens when publishing\nis influenced by data in scene. This can by slightly confusing so let's get to\nit with few examples.\n\n\n## Setting scene data\n\nBlender settings concerning framerate, resolution and frame range are handled\nby OpenPype. If set correctly in Ftrack, Blender will automatically set the \nvalues for you.\n\n\n## Publishing models\n\n### Intro\n\nPublishing models in Blender is pretty straightforward. Create your model as you\nneed. You might need to adhere to specifications of your studio that can be different\nbetween studios and projects but by default your geometry does not need any\nother convention.\n\n![Model example](assets/blender-model_example.jpg)\n\n### Creating instance\n\nNow create **Model instance** from it to let OpenPype know what in the scene you want to\npublish. Go **OpenPype → Create... → Model**.\n\n![Model create instance](assets/blender-model_create_instance.jpg)\n\n`Asset` field is a name of asset you are working on - it should be already filled\nwith correct name as you've started Blender or switched context to specific asset. You\ncan edit that field to change it to different asset (but that one must already exists).\n\n`Subset` field is a name you can decide on. It should describe what kind of data you\nhave in the model. For example, you can name it `Proxy` to indicate that this is\nlow resolution stuff. See [Subset](artist_concepts.md#subset).\n\n<!-- :::note LOD support\nBy changing subset name you can take advantage of _LOD support_ in OpenPype. Your\nasset can contain various resolution defined by different subsets. You can then\nswitch between them very easy using [Inventory (Manage)](artist_tools_inventory).\nThere LODs are conveniently grouped so they don't clutter Inventory view.\n\nName your subset like `main_LOD1`. Important part is that `_LOD1`. You can have as many LODs as you need.\n::: -->\n\nRead-only field just under it show final subset name, adding subset field to\nname of the group you have selected.\n\n`Use selection` checkbox will use whatever you have selected in Outliner to be\nwrapped in Model instance. This is usually what you want. Click on **Create** button.\n\nYou'll notice then after you've created new Model instance, there is a new \ncollection in Outliner called after your asset and subset, in our case it is\n`character1_modelDefault`. The assets selected when creating the Model instance\nare linked in the new collection.\n\nAnd that's it, you have your first model ready to publish.\n\nNow save your scene (if you didn't do it already). You will notice that path\nin Save dialog is already set to place where scenes related to modeling task on\nyour asset should reside. As in our case we are working on asset called\n**character1** and on task **modeling**, path relative to your project directory will be\n`project_XY/assets/character1/work/modeling`. The default name for the file will\nbe `project_XY_asset_task_version`, so in our case \n`simonetest_character1_modeling_v001.blend`. Let's save it.\n\n![Model create instance](assets/blender-save_modelling_file.jpg)\n\n### Publishing models\n\nNow let's publish it. Go **OpenPype → Publish...**. You will be presented with following window:\n\n![Model publish](assets/blender-model_pre_publish.jpg)\n\nNote that content of this window can differs by your pipeline configuration.\nFor more detail see [Publisher](artist_tools_publisher).\n\nItems in left column are instances you will be publishing. You can disable them\nby clicking on square next to them. White filled square indicate they are ready for\npublishing, red means something went wrong either during collection phase\nor publishing phase. Empty one with gray text is disabled.\n\nSee that in this case we are publishing from the scene file\n`simonetest_character1_modeling_v001.blend` the Blender model named \n`character1_modelDefault`.\n\nRight column lists all tasks that are run during collection, validation,\nextraction and integration phase. White items are optional and you can disable\nthem by clicking on them.\n\nLets do dry-run on publishing to see if we pass all validators. Click on flask\nicon at the bottom. Validators are run. Ideally you will end up with everything\ngreen in validator section.\n\n### Fixing problems\n\nFor the sake of demonstration, I intentionally kept the model in Edit Mode, to\ntrigger the validator designed to check just this.\n\n![Failed Model Validator](assets/blender-model_publish_error.jpg)\n\nYou can see our model is now marked red in left column and in right we have\nred box next to `Mesh is in Object Mode` validator.\n\nYou can click on arrow next to it to see more details:\n\n![Failed Model Validator details](assets/blender-model_error_details.jpg)\n\nFrom there you can see in **Records** entry that there is problem with the\nobject `Suzanne`. Some validators have option to fix problem for you or just \nselect objects that cause trouble. This is the case with our failed validator.\n\nIn main overview you can notice little A in a circle next to validator\nname. Right click on it and you can see menu item `select invalid`. This\nwill select offending object in Blender.\n\nFix is easy. Without closing Publisher window we just turn back the Object Mode.\nThen we need to reset it to make it notice changes we've made. Click on arrow\ncircle button at the bottom and it will reset the Publisher to initial state. Run\nvalidators again (flask icon) to see if everything is ok.\n\nIt should OK be now. Write some comment if you want and click play icon button\nwhen ready.\n\nPublish process will now take its course. Depending on data you are publishing\nit can take a while. You should end up with everything green and message\n**Finished successfully ...** You can now close publisher window.\n\nTo check for yourself that model is published, open\n[Asset Loader](artist_tools_loader) - **OpenPype → Load...**.\nThere you should see your model, named `modelDefault`.\n\n### Loading models\n\nYou can load model with [Loader](artist_tools_loader). Go **OpenPype → Load...**,\nselect your rig, right click on it and click **Link model (blend)**.\n\n## Creating Rigs\n\nCreating and publishing rigs with OpenPype follows similar workflow as with\nother data types. Create your rig and mark parts of your hierarchy in sets to\nhelp OpenPype validators and extractors to check it and publish it.\n\n### Preparing rig for publish\n\nWhen creating rigs in Blender, it is important to keep a specific structure for\nthe bones and the geometry. Let's first create a model and its rig. For\ndemonstration, I'll create a simple model for a robotic arm made of simple boxes.\n\n![Blender - Simple model for rigging](assets/blender-rig_model_setup.jpg)\n\nI have now created the armature `RIG_RobotArm`. While the naming is not important,\nyou can just adhere to your naming conventions, the hierarchy is. Once the models\nare skinned to the armature, the geometry must be organized in a separate Collection.\nIn this case, I have the armature in the main Collection, and the geometry in \nthe `Geometry` Collection.\n\n![Blender - Rig Hierarchy Example](assets/blender-rig_hierarchy_example.jpg)\n\nWhen you've prepared your hierarchy, it's time to create *Rig instance* in OpenPype.\nSelect your whole rig hierarchy and go **OpenPype → Create...**. Select **Rig**.\n\n![Blender - Rig Hierarchy Example](assets/blender-rig_create.jpg)\n\nA new collection named after the selected Asset and Subset should have been created.\nIn our case, it is `character1_rigDefault`. All the selected armature and models\nhave been linked in this new collection. You should end up with something like\nthis:\n\n![Blender - Rig Hierarchy Example](assets/blender-rig_hierarchy_before_publish.jpg)\n\n### Publishing rigs\n\nPublishing rig is done in same way as publishing everything else. Save your scene\nand go **OpenPype → Publish**. For more detail see [Publisher](artist_tools_publisher).\n\n### Loading rigs\n\nYou can load rig with [Loader](artist_tools_loader). Go **OpenPype → Load...**,\nselect your rig, right click on it and click **Link rig (blend)**.\n\n## Layouts in Blender\n\nA layout is a set of elements that populate a scene. OpenPype allows to version\nand manage those sets.\n\n### Publishing a layout\n\nWorking with Layout is easy. Just load your assets into scene with\n[Loader](artist_tools_loader) (**OpenPype → Load...**). Populate your scene as\nyou wish, translate each piece to fit your need. When ready, select all imported\nstuff and go **OpenPype → Create...** and select **Layout**. When selecting rigs,\nyou need to select only the armature, the geometry will automatically be included.\nThis will create set containing your selection and marking it for publishing.\n\nNow you can publish is with **OpenPype → Publish**.\n\n### Loading layouts\n\nYou can load a Layout using [Loader](artist_tools_loader)\n(**OpenPype → Load...**). Select your layout, right click on it and\nselect **Link Layout (blend)**. This will populate your scene with all those\nmodels you've put into layout.\n"
  },
  {
    "path": "website/docs/artist_hosts_harmony.md",
    "content": "---\nid: artist_hosts_harmony\ntitle: Harmony\nsidebar_label: Harmony\n---\n\n## Available Tools\n\n-   [Work Files](artist_tools_workfiles)\n-   [Create](artist_tools_creator)\n-   [Load](artist_tools_loader)\n-   [Publish](artist_tools_publisher)\n-   [Manage](artist_tools_inventory)\n\n:::note\nOnly one tool can be open at a time. If you open a tool while another tool is open, it will wait in queue for the existing tool to be closed. Once the existing tool is closed, the new tool will open.\n:::\n\n## Usage\n\nThe integration creates an `OpenPype` menu entry where all related tools are located.\n\n:::note\nMenu creation can be temperamental. Its best to start Harmony and do nothing else until the application is fully launched.\nIf you dont see the `OpenPype` menu, then follow this to create it:\n- Go to the `Script Editor`\n- Find the script called `TB_sceneOpened.js` and run it.\n- Choose the `start` method to run.\n:::\n\n### Workfiles\n\n`OpenPype > Workfiles`\n\nWork files are temporarily stored locally, in `[user]/.avalon/harmony`, to reduce network bandwidth. When saving the Harmony scene, a background process ensures the network files are updated.\n\n:::important\nBecause the saving to the network location happens in the background, be careful when quickly saving and closing Harmony (and the terminal window) since an interrupted saving to the network location can corrupt the workfile. To be sure the workfile is saved to the network location look in the terminal for a line similar to this:\n\n`DEBUG:avalon.harmony.lib:Saved \"[Local Scene Directory]\" to \"[Network Scene Directory]\\[Name Of Workfile].zip\"`\n:::\n\n### Create\n\n`OpenPype > Create`\n\n![Creator](assets/harmony_creator.png)\n\nThese are the families supported in Harmony:\n\n- `Render`\n    - This instance is for generating a render and review. This is a normal write node, but only PNGs are supported at the moment.\n- `Template`\n    - This instance is for generating a templates. This is a normal composite node, which you can connect any number of nodes to.\n    - Any connected nodes will be published along with their dependencies and any back drops.\n- `Palette`\n    - Palettes are indirectly supported in Harmony. This means you just have to have palettes in your scene to publish them.\n\nWhen you `Use selection` on creation, the last selected node will be connected to the created node.\n\n### Publish\n\n`OpenPype > Publish`\n\n![Publish](assets/harmony_publish.png)  <!-- picture has to be changed (Harmony needed) -->\n\nThis tool will run through checks to make sure the contents you are publishing is correct. Hit the \"Play\" button to start publishing.\n\nYou may encounter issues with publishing which will be indicated with red squares. If these issues are within the validation section, then you can fix the issue. If there are issues outside of validation section, please let the OpenPype team know.\n\n#### Repair Validation Issues\n\nAll validators will give some description about what the issue is. You can inspect this by going into the validator through the arrow:\n\n![Inspect](assets/harmony_publish_inspect.png) <!-- picture has to be changed (Harmony needed) -->\n\nYou can expand the errors by clicking on them for more details:\n\n![Expand](assets/harmony_publish_expand.png) <!-- picture has to be changed (Harmony needed) -->\n\nSome validator have repair actions, which will fix the issue. If you can identify validators with actions by the circle icon with an \"A\":\n\n![Actions](assets/harmony_publish_actions.png) <!-- picture has to be changed (Harmony needed) -->\n\nTo access the actions, you right click on the validator. If an action runs successfully, the actions icon will turn green. Once all issues are fixed, you can just hit the \"Refresh\" button and try to publish again.\n\n![Repair](assets/harmony_publish_repair.gif) <!-- gif has to be changed (Harmony needed) -->\n \n### Load\n\n`OpenPype > Load`\n\n![Loader](assets/photoshop_loader.png)  <!-- picture has to be changed (Harmony needed) -->\n\nThe supported families for Harmony are:\n\n- `image`\n- `harmony.template`\n    - Only import is current supported for templates.\n- `harmony.palette`\n    - Loaded palettes are moved to the top of the colour stack, so they will acts as overrides. Imported palettes are left in the scene.\n- `workfile`\n    - Only of type `zip`.\n\nTo load, right-click on the subset you want and choose a representation:\n\n![Loader](assets/photoshop_loader_load.gif) <!-- gif has to be changed (Harmony needed) -->\n\n:::note\nLoading templates or workfiles will import the contents into scene. Referencing is not supported at the moment, so you will have to load newer versions into the scene.\n:::\n\n### Manage\n\n`OpenPype > Manage`\n\n![Loader](assets/photoshop_manage.png)  <!-- picture has to be changed (Harmony needed) -->\n\nYou can switch to a previous version of the image or update to the latest.\n\n![Loader](assets/photoshop_manage_switch.gif) <!-- gif has to be changed (Harmony needed) -->\n![Loader](assets/photoshop_manage_update.gif) <!-- gif has to be changed (Harmony needed) -->\n\n:::note\nImages and image sequences will be loaded into the scene as read nodes can coloured green. On startup the pipeline checks for any outdated read nodes and colours them red.\n- <span style={{color:'green'}}>Green</span> = Up to date version in scene.\n- <span style={{color:'red'}}>Red</span> = Outdated version in scene.\n:::\n"
  },
  {
    "path": "website/docs/artist_hosts_hiero.md",
    "content": "---\nid: artist_hosts_hiero\ntitle: Hiero\nsidebar_label: Hiero / Nuke Studio\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n:::note\nAll the information also applies to **_Nuke Studio_**(NKS), but for simplicity we only refer to Hiero/NKS. The workflows are identical for both. We are supporting versions **`11.0`** and above.\n:::\n\n\n\n## OpenPype global tools\n\n-   [Work Files](artist_tools_workfiles)\n-   [Create](artist_tools_creator)\n-   [Load](artist_tools_loader)\n-   [Manage (Inventory)](artist_tools_inventory)\n-   [Publish](artist_tools_publisher)\n\n\n## Hiero specific tools\n\n\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\n### Create Default Tags\n\nThis tool will recreate all necessary OpenPype tags needed for successful publishing. It is automatically ran at start of the Hiero/NKS. Use this tool to manually re-create all the tags if you accidentally delete them, or you want to reset them to default values.\n\n#### Result\n\n-   Will create tags in Tags bin in case there were none\n-   Will set all tags to default values if they have been altered\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Default Tags](assets/hiero_defaultTags.png) <!-- picture needs to be changed -->\n\n</div>\n</div>\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\n### Apply Colorspace Project\n\nThis tool will set any defined colorspace definition from OpenPype `Settings / Project / Anatomy / Color Management and Output Formats / Hiero / Workfile` to Hiero `menu / Project / Edit Settings / Color Management tab`\n\n#### Result\n\n-   Define correct color management settings on project\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Default Tags](assets/hiero_menuColorspaceProject.png) <!-- picture needs to be changed -->\n\n</div>\n</div>\n\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\n### Apply Colorspace Clips\n\nThis tool will set any defined colorspace definition from OpenPype `Settings / Project / Anatomy / Color Management and Output Formats / Hiero / Colorspace on Inputs by regex detection` to any matching clip's source path.\n\n#### Result\n\n-   Set correct `Set Media Color Transform` on each clip of active timeline if it matches defined expressions\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Default Tags](assets/hiero_menuColorspaceClip.png) <!-- picture needs to be changed -->\n\n</div>\n</div>\n\n## Publishing Shots\n\n\n\n<div class=\"row markdown\">\n\nWith OpenPype, you can use Hiero/NKS as a starting point for creating a project's **shots** as *assets* from timeline clips with its *hierarchycal parents* like **episodes**, **sequences**, **folders**, and its child **tasks**. Most importantly it will create **versions** of plate *subsets*, with or without **reference video**. Publishig is naturally creating clip's **thumbnails** and assigns it to shot *asset*. Hiero is also publishing **audio** *subset* and various **soft-effects** either as retiming component as part of published plates or **color-tranformations**, that will be evailable later on for compositor artists to use either as *viewport input-process* or *loaded nodes* in graph editor.\n<br></br><br></br>\n\n<iframe width=\"512px\" height=\"288px\" src=\"https://www.youtube.com/embed/mdIfbTY5fCc\" frameborder=\"0\" modestbranding=\"1\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"1\"></iframe>\n\n### Preparing timeline for conversion to instances\nBecause we don't support on-fly data conversion so in case of working with raw camera sources or some other formats which need to be converted for 2D/3D work. We suggest to convert those before and reconform the timeline. Before any clips in timeline could be converted to publishable instances we recommend following.\n1. Merge all tracks which supposed to be one and they are multiply only because of editor's style\n2. Rename tracks to follow basic structure > if only one layer then `main` in case of multiple layer (elements) for one shot then `main`, and other elements for example: `bg`, `greenscreen`, `fg01`, `fg02`, `display01`, etc. please avoid using [-/_.,%&*] or spaces. These names will be later used in *subset* name creation as `{family}{trackName}` so for example **plateMain** or **plateFg01**\n3. Define correct `Set Media Color Transform` at all clips as those will be also published to metadata and used for later loading with correct color transformation.\n4. Reviewable video material which you wish to be used as preview videos on any supported Projec manager platform (Ftrack) has to be added ideally to track named **review**. This can be offline edit used as reference video for 2D/3D artists. This video material can be edited to fit length of **main** timeline track or it cand be one long video clip under all clips in **main** track, because OpenPype will trim this to appropriate length with use of FFMPEG. Please be avare we only support MP4(h264) or JPG sequence at the moment.\n\n<div class=\"col--6 markdown\">\n\n![Create menu](assets/hiero_timelinePrep.png)\n\n</div>\n\n\n### Converting timeline clips to instances\n\nEvery clip on timeline which is intended to be published has to be converted to publishable instance.\n\n<div class=\"col col--6 markdown\">\n\nIn OpenPype it is done by tagging a clip with our own metadata. Select all clips you wish to convert and `menu > OpenPype > Create`.\n<br></br><br></br>\n\n</div>\n\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/hiero_menuCreate.png)\n\n</div>\n\n<div class=\"col col--6 markdown\">\n\nThen chose `Create Publishable Clip` in **Instance Creator** dialogue.\n<br></br>\n\nThen you can alter Subset name, but this will be changed dynamically and replaces with timeline's track name.\n<br></br>\n\nKeep **Use selection** on.\n<br></br>\n\nHit **Create**\n<br></br>\n</div>\n\n<div class=\"col col--6 markdown\">\n\n![Instance Creator](assets/hiero_instanceCreator.png)\n\n</div>\n<div class=\"col col--6 markdown\">\n\nDialogue `Pype publish attributes creator` will open. Here you can define instance properties. If you wish to rename clips dynamically during creation then Keep  **Rename clips** ticked.\n<br></br>\n\nIn case you wish to use *multiple elements of shots* workflow then keep **Enamble vertical sync** ticked on and define correct hero track which is holding main plates, this is usually the **main** track.\n</div>\n\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/hiero_createUIRename.png)\n\n</div>\n<div class=\"col col--6 markdown\">\n\nSubset name is created dynamically if `<track_name>` is selected on **Subset name**.\n<br></br>\n\nI case you wish to publish reviewable video as explained above then find the appropriate track from drop down menu **Use review track**. Usually named `review`\n<br></br>\n\nHover above each input field for help.\n<br></br>\n\nHandles can be defined here to. In case you wish to have individual clip set differently we recommend to set here the default value and later change those in the created OpenPype tag's metadata under `handleStart` and `handleEnd` properties (look below for details)\n</div>\n\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/hiero_createUIFrames.png)\n\n</div>\n<div class=\"col col--6 markdown\">\n\nAfter you hit **Ok** tags are added to selected clips (except clips in **review** tracks).\n<br></br>\n\nIf you wish to change any individual properties of the shot then you are able to do it here. In this example we can change `handleStart` and `handleEnd` to some other values.\n</div>\n\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/hiero_tagHandles.png)\n\n</div>\n</div>\n\n### Publishing Effects from Hiero to Nuke\nThis video shows a way to publish shot look as effect from Hiero to Nuke.\n\n<iframe width=\"512px\" height=\"288px\" src=\"https://www.youtube.com/embed/HzZDdtII5io\" frameborder=\"0\" modestbranding=\"1\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"1\"></iframe>\n\n### Assembling edit from published shot versions\n\n<iframe width=\"512px\" height=\"288px\" src=\"https://www.youtube.com/embed/5Wd6X-71vbg\" frameborder=\"0\" modestbranding=\"1\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"1\"></iframe>\n\n\n# Nuke Build Workfile\nThis is a tool of Node Graph initialisation using a pre-created template.\n\n### Add a profile\nThe path to the template that will be used in the initialisation must be added as a profile on Project Settings.\n\n![Create menu](assets/nuke_addProfile.png)\n\n### Create Place Holder\n\n![Create menu](assets/nuke_createPlaceHolder.png)\n\nThis tool creates a Place Holder, which is a node that will be replaced by published instances.\n\n![Create menu](assets/nuke_placeHolderNode.png)\n#### Result\n- Create a red node called `PLACEHOLDER` which can be manipulated as wanted by using it in Node Graph.\n\n![Create menu](assets/nuke_placeholder.png)\n\n:::note\nAll published instances that will replace the place holder must contain unique input and output nodes in case they will not be imported as a single node. \n:::\n\n![Create menu](assets/nuke_publishedinstance.png)\n\n\nThe information about these objects are given by the user by filling the extra attributes of the Place Holder\n\n![Create menu](assets/nuke_fillingExtraAttributes.png)\n\n\n\n### Update Place Holder\nThis tool allows the user to change the information provided in the extra attributes of the selected Place Holder.\n\n![Create menu](assets/nuke_updatePlaceHolder.png)\n\n\n\n### Build Workfile from template\nThis tool imports the template used and replaces the existed PlaceHolders with the corresponding published objects (which can contain Place Holders too). In case there is no published items with the description given, the place holder will remain in the node graph.\n\n![Create menu](assets/nuke_buildWorfileFromTemplate.png)\n\n#### Result\n- Replace `PLACEHOLDER` node in the template with the published instance corresponding to the information provided in extra attributes of the Place Holder\n\n![Create menu](assets/nuke_buildworkfile.png)\n\n:::note\nIn case the instance that will replace the Place holder **A** contains another Place Holder **B** that points to many published elements, all the nodes that were imported with **A** except **B** will be duplicated for each element that will replace **B**\n:::\n\n### Update Workfile\nThis tool can be used to check if some instances were published after the last build, so they will be imported.\n\n![Create menu](assets/nuke_updateWorkfile.png)\n\n:::note\nImported instances must not be deleted because they contain extra attributes that will be used to update the workfile since the place holder is been deleted.\n:::\n"
  },
  {
    "path": "website/docs/artist_hosts_houdini.md",
    "content": "---\nid: artist_hosts_houdini\ntitle: Houdini\nsidebar_label: Houdini\n---\n\n## OpenPype global tools\n\n- [Work Files](artist_tools_workfiles)\n- [Create](artist_tools_creator)\n- [Load](artist_tools_loader)\n- [Manage (Inventory)](artist_tools_inventory)\n- [Publish](artist_tools_publisher)\n- [Library Loader](artist_tools_library-loader)\n\n## Publishing Alembic Cameras\nYou can publish baked camera in Alembic format.\n\nSelect your camera and go **OpenPype -> Create** and select **Camera (abc)**.\nThis will create Alembic ROP in **out** with path and frame range already set. This node will have a name you've\nassigned in the **Creator** menu. For example if you name the subset `Default`, output Alembic Driver will be named\n`cameraDefault`. After that, you can **OpenPype -> Publish** and after some validations your camera will be published\nto `abc` file.\n\n## Publishing Composites - Image Sequences\nYou can publish image sequences directly from Houdini's image COP networks.\n\nYou can use any COP node and publish the image sequence generated from it. For example this simple graph to generate some noise:\n\n![Noise COP](assets/houdini_imagesequence_cop.png)\n\nTo publish the output of the `radialblur1` go to **OpenPype -> Create** and\nselect **Composite (Image Sequence)**. If you name the variant *Noise* this will create the `/out/imagesequenceNoise` Composite ROP with the frame range set.\n\nWhen you hit **Publish** it will render image sequence from selected node.\n\n:::info Use selection\nWith *Use selection* is enabled on create the node you have selected when creating will be the node used for published. (It set the Composite ROP node's COP path to it). If you don't do this you'll have to manually set the path as needed on e.g. `/out/imagesequenceNoise` to ensure it outputs what you want.\n:::\n\n## Publishing Point Caches (alembic)\nPublishing point caches in alembic format is pretty straightforward, but it is by default enforcing better compatibility\nwith other DCCs, so it needs data do be exported prepared in certain way. You need to add `path` attribute so objects\nin alembic are better structured. When using alembic round trip in Houdini (loading alembics, modifying then and\nthen publishing modifications), `path` is automatically resolved by alembic nodes.\n\nIn this example, I've created this node graph on **sop** level, and I want to publish it as point cache.\n\n![Pointcache setup](assets/houdini_pointcache_path.png)\n\n*Note: `connectivity` will add index for each primitive and `primitivewrangle1` will add `path` attribute, so it will\nbe for each primitive (`sphere1` and `sphere2`) as Maya is expecting - `strange_GRP/strange0_GEO/strange0_GEOShape`. How\nyou handle `path` attribute is up to you, this is just an example.*\n\nNow select the `output0` node and go **OpenPype -> Create** and select **Point Cache**. It will create\nAlembic ROP `/out/pointcacheStrange`\n\n## Publishing Reviews (OpenGL)\nTo generate a review output from Houdini you need to create a **review** instance.\nGo to **OpenPype -> Create** and select **Review**.\n\n![Houdini Create Review](assets/houdini_review_create_attrs.png)\n\nOn create, with the **Use Selection** checkbox enabled it will set up the first\ncamera found in your selection as the camera for the OpenGL ROP node and other\nnon-cameras are set in **Force Objects**. It will then render those even if\ntheir display flag is disabled in your scene.\n\n## Redshift\n:::note Work in progress\nThis part of documentation is still work in progress.\n:::\n\n## Publishing Render to Deadline\nFive Renderers(Arnold, Redshift, Mantra, Karma, VRay) are supported for Render Publishing.\nThey are named with the suffix(\"_ROP\")\nTo submit render to deadline, you need to create a **Render** instance.\nGo to **Openpype -> Create** and select **Publish**. Before clicking **Create** button,\nyou need select your preferred image rendering format. You can also enable the **Use selection** to\nselect your render camera.\n![Houdini Create Render](assets/houdini_render_publish_creator.png)\n\nAll the render outputs are stored in the pyblish/render directory within your project path.\\\nFor Karma-specific render, it also outputs the USD render as default.\n\n## Publishing cache to Deadline\nArtist can publish cache to deadline which increases productivity as artist can use local machine\ncould be used for other tasks.\nCaching on the farm is supported for:\n\n**Arnold ASS (.ass)**\n**Pointcache (.bgeo and .abc)**\n**VDB (.vdb)**\n**Redshift Proxy (.rs)**\n\nTo submit your cache to deadline, you need to create the instance(s) with clicking\n**Submitting to Farm** and you can also enable  **Use selection** to\nselect the object for caching in farm.\n![Houdini Farm Cache Creator](assets/houdini_farm_cache_creator.png)\n\nWhen you go to Publish Tab and click the instance(s), you can set up your preferred\n**Frame per task**.\n![Houdini Farm Per Task](assets/houdini_frame_per_task.png)\n\nOnce you hit **Publish**, the cache would be submitted and rendered in deadline.\nWhen the render is finished, all the caches would be located in your publish folder.\nYou can see them in the Loader.\n![Houdini Farm Per Task](assets/houdini_farm_cache_loader.png)\n\n## USD (experimental support)\n### Publishing USD\nYou can publish your Solaris Stage as USD file.\n![Solaris USD](assets/houdini_usd_stage.png)\n\nThis is very simple test stage. I've selected `output` **lop** node and went to **OpenPype -> Create** where I've\nselected **USD**. This created `/out/usdDefault` USD ROP node.\n\n### Publishing USD render\n\nUSD Render works in similar manner as USD file, except it will create **USD Render** ROP node in out and will publish\nimages produced by it. If you have selected node in Solaris Stage it will by added as **lop path** to ROP.\n\n## Publishing VDB\n\nPublishing VDB files works as with other data types. In this example I've created simple PyroFX explosion from\nsphere. In `pyro_import` I've converted the volume to VDB:\n\n![VDB Setup](assets/houdini_vdb_setup.png)\n\nI've selected `vdb1` and went **OpenPype -> Create** and selected **VDB Cache**. This will create\ngeometry ROP in `/out` and sets its paths to output vdb files. During the publishing process\nwhole dops are cooked.\n\n## Publishing Houdini Digital Assets (HDA)\n\nYou can publish most of the nodes in Houdini as hda for easy interchange of data between Houdini instances or even\nother DCCs with Houdini Engine.\n\n## Creating HDA\n\nSimply select nodes you want to include in hda and go **OpenPype -> Create** and select **Houdini digital asset (hda)**.\nYou can even use already existing hda as a selected node, and it will be published (see below for limitation).\n\n:::caution HDA Workflow limitations\nAs long as the hda is of same type - it is created from different nodes but using the same (subset) name, everything\nis ok. But once you've published version of hda subset, you cannot change its type. For example, you create hda **Foo**\nfrom *Cube* and *Sphere* - it will create hda subset named `hdaFoo` with the same type. You publish it as version 1.\nThen you create version 2 with added *Torus*. Then you create version 3 from the scratch from completely different nodes,\nbut still using resulting subset name `hdaFoo`. Everything still works as expected. But then you use already\nexisting hda as a base, for example from different artist. Its type cannot be changed from what it was and so even if\nit is named `hdaFoo` it has different type. It could be published, but you would never load it and retain ability to\nswitch versions between different hda types.\n:::\n\n## Loading HDA\n\nWhen you load hda, it will install its type in your hip file and add published version as its definition file. When\nyou  switch version via Scene Manager, it will add its definition and set it as preferred.\n\n## Publishing and loading BGEO caches\n\nThere is a simple support for publishing and loading **BGEO** files in all supported compression variants.\n\n### Creating BGEO instances\n\nSelect your SOP node to be exported as BGEO. If your selection is in the object level, OpenPype will try to find if there is an `output` node inside, the one with the lowest index will be used:\n\n![BGEO output node](assets/houdini_bgeo_output_node.png)\n\nThen you can open Publisher, in Create you select **BGEO PointCache**:\n\n![BGEO Publisher](assets/houdini_bgeo-publisher.png)\n\nYou can select compression type and if the current selection should be connected to ROPs SOP path parameter. Publishing will produce sequence of files based on your timeline settings.\n\n### Loading BGEO\n\nSelect your published BGEO subsets in Loader, right click and load them in:\n\n![BGEO Publisher](assets/houdini_bgeo-loading.png)\n"
  },
  {
    "path": "website/docs/artist_hosts_maya.md",
    "content": "---\nid: artist_hosts_maya\ntitle: Maya\nsidebar_label: Maya\n---\n\n## OpenPype global tools\n\n-   [Set Context](artist_tools_context_manager)\n-   [Work Files](artist_tools_workfiles)\n-   [Create](artist_tools_creator)\n-   [Load](artist_tools_loader)\n-   [Manage (Inventory)](artist_tools_inventory)\n-   [Publish](artist_tools_publisher)\n-   [Library Loader](artist_tools_library_loader)\n\n## Working with OpenPype in Maya\n\nOpenPype is here to ease you the burden of working on project with lots of\ncollaborators, worrying about naming, setting stuff, browsing through endless\ndirectories, loading and exporting and so on. To achieve that, OpenPype is using\nconcept of being _\"data driven\"_. This means that what happens when publishing\nis influenced by data in scene. This can by slightly confusing so let's get to\nit with few examples.\n\n## Publishing models\n\n### Intro\n\nPublishing models in Maya is pretty straightforward. Create your model as you\nneed. You need to adhere to specifications of your studio that can be different\nbetween studios and projects but by default your geometry has to be named properly.\nFor example `sphere_GEO` or `cube1_GEO`. Geometry needs to have freezed transformations\nand must reside under one group, for example `model_GRP`.\n\n![Model example](assets/maya-model_hierarchy_example.jpg)\n\nNote that `sphere_GEO` has frozen transformations.\n\n### Creating instance\n\nNow create **Model instance** from it to let OpenPype know what in the scene you want to\npublish. Go **OpenPype → Create... → Model**\n\n![Model create instance](assets/maya-model_create_instance.jpg)\n\n`Asset` field is a name of asset you are working on - it should be already filled\nwith correct name as you've started Maya or switched context to specific asset. You\ncan edit that field to change it to different asset (but that one must already exists).\n\n`Subset` field is a name you can decide on. It should describe what kind of data you\nhave in the model. For example, you can name it `Proxy` to indicate that this is\nlow resolution stuff. See [Subset](artist_concepts.md#subset).\n\n:::note LOD support\nBy changing subset name you can take advantage of _LOD support_ in OpenPype. Your\nasset can contain various resolution defined by different subsets. You can then\nswitch between them very easy using [Inventory (Manage)](artist_tools_inventory).\nThere LODs are conveniently grouped so they don't clutter Inventory view.\n\nName your subset like `main_LOD1`. Important part is that `_LOD1`. You can have as many LODs as you need.\n:::\n\nRead-only field just under it show final subset name, adding subset field to\nname of the group you have selected.\n\n`Use selection` checkbox will use whatever you have selected in Outliner to be\nwrapped in Model instance. This is usually what you want. Click on **Create** button.\n\nYou'll notice then after you've created new Model instance, there is new set\nin Outliner called after your subset, in our case it is `modelMain`.\n\nAnd that's it, you have your first model ready to publish.\n\nNow save your scene (if you didn't do it already). You will notice that path\nin Save dialog is already set to place where scenes related to modeling task on\nyour asset should reside. As in our case we are working on asset called\n**Ben** and on task **modeling**, path relative to your project directory will be\n`project_XY/assets/ben/work/modeling`. Let's save our scene as `model_test_v01`.\n\n### Publishing models\n\nNow let's publish it. Go **OpenPype → Publish...**. You will be presented with following window:\n\n![Model publish](assets/maya-model_pre_publish.jpg)\n\nNote that content of this window can differs by your pipeline configuration.\nFor more detail see [Publisher](artist_tools_publisher).\n\nItems in left column are instances you will be publishing. You can disable them\nby clicking on square next to them. Green square indicate they are ready for\npublishing, red means something went wrong either during collection phase\nor publishing phase. Empty one with gray text is disabled.\n\nSee that in this case we are publishing from scene file `model_test_v01.mb` in\nMaya model named `modelMain (ben)` (next item). Publishing of workfile is\ncurrently disabled (last item).\n\nRight column lists all tasks that are run during collection, validation,\nextraction and integration phase. White items are optional and you can disable\nthem by clicking on them.\n\nLets do dry-run on publishing to see if we pass all validators. Click on flask\nicon at the bottom. Validators are run. Ideally you will end up with everything\ngreen in validator section.\n\n### Fixing problems\n\nTo make things interesting, I intentionally forgot to freeze transformations\non `sphere_GEO` as I know it will trigger validator designed to check just this.\n\n![Failed Model Validator](assets/maya-model_publish_error.jpg)\n\nYou can see our model is now marked red in left column and in right we have\nred box next to `Transform Zero (Freeze)` validator.\n\nYou can click on arrow next to it to see more details:\n\n![Failed Model Validator details](assets/maya-model_freeze_error_details.jpg)\n\nFrom there you can see in **Records** entry that there is problem with `sphere_GEO`.\nSome validators have option to fix problem for you or just select objects that\ncause trouble. This is the case with our failed validator.\n\nIn main overview you can notice little up arrow in a circle next to validator\nname. Right click on it and you can see menu item `select invalid`. This\nwill select offending object in Maya.\n\nFix is easy. Without closing Publisher window we just freeze transformations.\nThen we need to reset it to make it notice changes we've made. Click on arrow\ncircle button at the bottom and it will reset Publisher to initial state. Run\nvalidators again (flask icon) to see if everything is ok.\n\nIt should be now. Write some comment if you want and click play icon button\nwhen ready.\n\nPublish process will now take its course. Depending on data you are publishing\nit can take a while. You should end up with everything green and message\n**Finished successfully ...** You can now close publisher window.\n\nTo check for yourself that model is published, open\n[Asset Loader](artist_tools_loader) - **OpenPype → Load...**.\nThere you should see your model, named `modelMain`.\n\n## Look development\n\nLook development in OpenPype is easy. It helps you with versioning different\nkinds of shaders and easy switching between them.\n\nLet se how it works.\n\n### Loading model\n\nIn this example I have already published model of Buddha. To see how to publish\nmodel with OpenPype see [Publishing Model](artist_hosts_maya.md#publishing-models).\n\nFirst of lets start with empty scene. Now go **OpenPype → Load...**\n\n![Model loading](assets/maya-model_loading.jpg)\n\nHere I am loading `modelBuddha`, its version 1 for asset **foo**.  Just right-click\non it and select **Reference (abc)**. This will load model into scene as alembic.\nNow you can close Loader window.\n\n### Creating look\n\nNow you can create whatever look you want. Assign shaders, textures, etc. to model.\nIn my case, I assigned simple Arnolds _aiSurfaceShader_ and changed its color to red.\n\n![Look Dev - Red Buddha](assets/maya-look_dev-red_buddha.jpg)\n\nI am quite happy with it so I want to publish it as my first look.\n\n### Publishing look\n\nSelect your model in outliner and ho **OpenPype → Create...**. From there\nselect **Look**. Make sure `use selection` checkbox is checked.\nMine subset name is `Main`. This will create _Look instance_ with a name **lookMain**.\n\nClose _Creator_ window.\n\nNow save your scene, give it some sensible name. Next, go **OpenPype → Publish**.\nThis process is almost identical as publishing models, only different _Validators_\nand other plugins will be used.\n\nThis should be painless and cause no trouble so go ahead, click play icon button at\nthe bottom and it will publish your look.\n\n:::note publishing multiple looks\nYou can reference same model into scene multiple times, change materials on every\ninstance with what you need. Then on every model create _Look instance_. When\npublishing all those _Look instances_ will be published at same time.\n:::\n\n### Loading looks into models\n\nNow lets see how look are applied. Start new empty scene, load your published\nmodel there as before (using _Reference (abc)_). If you didn't notice until now,\nthere are few yellow icons in left shelf:\n\n![Maya - shortcut icons](assets/maya-shortcut_buttons.jpg)\n\nThose are shortcuts for **Look Manager**, [Work Files](artist_tools_workfiles),\n[Load](artist_tools_loader), and [Manage (Inventory)](artist_tools_inventory).\n\nThose can be found even in top menu, but that depends on your studio setup.\n\nYou are interested now in **Look Manager** - first item with brush icon. Select\nyour Buddha model and open **Look Manager**.\n\n![Maya - Look Manager](assets/maya-look_dev-look_manager.jpg)\n\nThis is **Look Manager** window. Yours would be empty until you click **Get All Assets**\nor **Get Assets From Selection**. You can use later to quick assign looks if you have\nmultiple assets loaded in scene. Click on one of those button now.\n\nYou should now see all assets and their subsets loaded in scene, and on right side\nall applicable published looks.\n\nSelect you asset and on the right side right click on `Main` look. Apply it.\n\nYou notice that Buddha model is now red, materials you've published are now applied\nto it.\n\nThat way you can create looks as you want and version them using OpenPype.\n\n## Setting scene data\n\nMaya settings concerning framerate, resolution and frame range are handled by\nOpenPype. If set correctly in Ftrack, Maya will validate you have correct fps on\nscene save and publishing offering way to fix it for you.\n\nFor resolution and frame range, use **OpenPype → Set Frame Range** and\n**OpenPype → Set Resolution**\n\n\n## Creating rigs with OpenPype\n\nCreating and publishing rigs with OpenPype follows similar workflow as with\nother data types. Create your rig and mark parts of your hierarchy in sets to\nhelp OpenPype validators and extractors to check and publish it.\n\n### Preparing rig for publish\n\nWhen creating rigs, it is recommended (and it is in fact enforced by validators)\nto separate bones or driven objects, their controllers and geometry so they are\neasily managed. Currently OpenPype doesn't allow to publish model at the same time as\nits rig so for demonstration purposes, I'll first create simple model for robotic\narm, just made out of simple boxes and I'll publish it.\n\n![Maya - Simple model for rigging](assets/maya-rig_model_setup.jpg)\n\nFor more information about publishing models, see [Publishing models](artist_hosts_maya.md#publishing-models).\n\nNow let's start with empty scene. Load your model - **OpenPype → Load...**, right\nclick on it and select **Reference (abc)**.\n\nI've created a few bones in `rig_GRP`, their controllers in `controls_GRP` and\nplaced the rig's output geometry in `geometry_GRP`. Naming of the groups is not important - just adhere to\nyour naming conventions. Then I parented everything into a single top group named `arm_rig`.\n\nWith the prepared hierarchy it is time to create a *Rig instance* in OpenPype.\nSelect the top group of your rig and go to **OpenPype → Create...**. Select **Rig**.\nA publish set for your rig is created in your scene to mark rig parts for export.\nNotice that it has two subsets - `controls_SET` and `out_SET`. Put your controls into `controls_SET`\nand geometry to `out_SET`. You should end up with something like this:\n\n![Maya - Rig Hierarchy Example](assets/maya-rig_hierarchy_example.jpg)\n\n:::note controls_SET and out_SET contents\nIt is totally allowed to put the `geometry_GRP` in the `out_SET` as opposed to\nthe individual meshes - it's even **recommended**. However, the `controls_SET`\nrequires the individual controls in it that the artist is supposed to animate\nand manipulate so the publish validators can accurately check the rig's\ncontrols.\n:::\n\n### Publishing rigs\n\nPublishing rigs is done in a same way as publishing everything else. Save your scene\nand go **OpenPype → Publish**. When you run validation you'll most likely run into\na few issues at first. Although a number of them will seem to be intimidating you\nwill find out they are mostly minor things, easily fixed and are there to optimize\nyour rig for consistency and safe usage by the artist.\n\n- **Non Duplicate Instance Members (ID)** - This will most likely fail because when\ncreating rigs, we usually duplicate few parts of it to reuse them. But duplication\nwill duplicate also ID of original object and OpenPype needs every object to have\nunique ID. This is easily fixed by **Repair** action next to validator name. click\non little up arrow on right side of validator name and select **Repair** form menu.\n\n- **Joints Hidden** - This is enforcing joints (bones) to be hidden for user as\nanimator usually doesn't need to see them and they clutter his viewports. So\nwell behaving rig should have them hidden. **Repair** action will help here also.\n\n- **Rig Controllers** will check if there are no transforms on unlocked attributes\nof controllers. This is needed because animator should have ease way to reset rig\nto it's default position. It also check that those attributes doesn't have any\nincoming connections from other parts of scene to ensure that published rig doesn't\nhave any missing dependencies.\n\n### Loading rigs\n\nYou can load rig with [Loader](artist_tools_loader). Go **OpenPype → Load...**,\nselect your rig, right click on it and **Reference** it.\n\n### Animation instances\n\nWhenever you load a rig an animation publish instance is automatically created\nfor it. This means that if you load a rig you don't need to create a pointcache\ninstance yourself to publish the geometry. This is all cleanly prepared for you\nwhen loading a published rig.\n\n:::tip Missing animation instance for your loaded rig?\nDid you accidentally delete the animation instance for a loaded rig? You can\nrecreate it using the [**Recreate rig animation instance**](artist_hosts_maya.md#recreate-rig-animation-instance)\ninventory action.\n:::\n\n## Point caches\nOpenPype is using Alembic format for point caches. Workflow is very similar as\nother data types.\n\n### Creating Point Caches\n\nTo create point cache just create whatever hierarchy you want and animate it.\nSelect its root and Go **OpenPype → Create...** and select **Point Cache**.\n\nAfter that, publishing will create corresponding **abc** files.\n\nWhen creating the instance, a objectset child `proxy` will be created. Meshes in the `proxy` objectset will be the viewport representation where loading supports proxies. Proxy representations are stored as `resources` of the subset.\n\nExample setup:\n\n![Maya - Point Cache Example](assets/maya-pointcache_setup.png)\n\n#### Options\n\n- **Frame Start**: which frame to start the export at.\n- **Frame End**: which frame to end the export at.\n- **Handle Start**: additional frames to export at frame start. Ei. frame start - handle start = export start.\n- **Handle Start**: additional frames to export at frame end. Ei. frame end + handle end = export end.\n- **Step**: frequency of sampling the export. For example when dealing with quick movements for motion blur, a step size of less than 1 might be better.\n- **Refresh**: refresh the viewport when exporting the pointcache. For performance is best to leave off, but certain situations can require to refresh the viewport, for example using the Bullet plugin.\n- **Attr**: specific attributes to publish separated by `;`.\n- **AttrPrefix**: specific attributes which start with this prefix to publish separated by `;`.\n- **Include User Defined Attribudes**: include all user defined attributes in the publish.\n- **Farm**: if your studio has Deadline configured, artists could choose to offload potentially long running export of pointache and publish it to the farm. Only thing that is necessary is to toggle this attribute in created pointcache instance to True.\n- **Priority**: Farm priority.\n\n### Loading Point Caches\n\nLoading point cache means creating reference to **abc** file with Go **OpenPype → Load...**.\n\nExample result:\n\n![Maya - Point Cache Example](assets/maya-pointcache_loaded.png)\n\n## Set dressing in Maya\n\nSet dressing is term for easily populate complex scenes with individual parts.\nOpenPype allows to version and manage those sets.\n\n### Publishing Set dress / Layout\n\nWorking with Set dresses is very easy. Just load your assets into scene with\n[Loader](artist_tools_loader) (**OpenPype → Load...**). Populate your scene as\nyou wish, translate each piece to fit your need. When ready, select all imported\nstuff and go **OpenPype → Create...** and select **Set Dress** or **Layout**.\nThis will create set containing your selection and marking it for publishing.\n\n:::note set dress vs layout\nCurrently *set dress* and *layout* are functionally identical\n:::\n\nNow you can publish is with **OpenPype → Publish**.\n\n### Loading Set dress / Layout\n\nYou can load Set dress / Layout using [Loader](artist_tools_loader)\n(**OpenPype → Load...**). Select you layout or set dress, right click on it and\nselect **Reference Maya Ascii (ma)**. This will populate your scene with all those\nmodels you've put into layout.\n\n## Rendering with OpenPype\n\nOpenPype in Maya can be used for submitting renders to render farm and for their\nsubsequent publishing. Right now OpenPype support [AWS Thinkbox Deadline](https://www.awsthinkbox.com/deadline)\nand [Royal Render](https://www.royalrender.de/).\n\n* For setting up Royal Render support see [admin section](module_royalrender.md)\n* For setting up Deadline support see [here](module_deadline.md)\n\n\n### Creating basic render setup\n\nIf you want to submit your render to farm, just follow these simple steps.\n\n#### Preparing scene\n\nLets start with empty scene. First I'll pull in my favorite Buddha model.\n**OpenPype → Load...**, select model and right+click to pop up context menu. From\nthere just click on **Reference (abc)**.\n\nNext, I want to be sure that I have same frame range as is set on shot I am working\non. To do this just **OpenPype → Set Frame Range**. This should set Maya timeline to same\nvalues as they are set on shot in *Ftrack* for example.\n\nI have my time set, so lets create some animation. We'll turn Buddha model around for\n50 frames (this is length of my timeline).\n\nSelect model, go to first frame, key Y axis rotation, go to last frame, enter 360 to\n**Channel Editor** Y rotation, key it and its done. If you are not sure how to do it,\nyou are probably reading wrong documentation.\n\nNow let set up lights, ground and camera. I am lazy so I create Arnolds Skydome light:\n**Arnold → Lights → Skydome Light**. As ground simple Plane will suffice and I'll set\nmy perspective view as I like and create new camera from it (`CTRL+SHIFT+C`) and rename\nit from `persp1` to `mainCamera`.\n\nOne last thing, I'll assign basic *aiSurfaceShader* to my Buddha and do some little\ntweaks on it.\n\n#### Prepare scene for submission\n\nAs we have working simple scene we can start preparing it for rendering. OpenPype is fully utilizing\nRender Setup layers for this. First of all, we need to create *Render instance* to tell OpenPype what\nto do with renders. You can easily render locally or on render farm without it, but *Render instance*\nis here to mark render layers you want to publish.\n\nLets create it. Go **OpenPype → Create...**. There select **Render** from list. If you keep\nchecked **Use selection** it will use your current Render Layers (if you have them). Otherwise,\nif no render layers is present in scene, it will create one for you named **Main** and under it\ndefault collection with `*` selector.\n\nOpenPype will try to connect to render farm and fetch machine pool list.\n\nSo now my scene now looks like this:\n\n![Maya - Render scene Setup](assets/maya-render_setup.jpg)\n\nYou can see that it created `renderingMain` set and under it `LAYER_Main`. This set corresponds to\n**Main** render layer in Render Setup. This was automatically created because I had not created any\nrender layers in scene before. If you already have layers and you use **Use selection**, they will\nappear here, prefixed with `LAYER_`. Those layer set are created whenever you create new layer in\nRender Setup and are deleted if you delete layer in Render Setup. However if you delete `LAYER_` set,\nlayer in Render Setup isn't deleted. It just means it won't be published.\n\nCreating *Render instance* will also set image prefix in render settings to OpenPype defaults based on\nrenderer you use - for example if you render with Arnold, it is `maya/<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>`.\n\nThere are few setting on *Render instance* `renderingMain` in **Attributes Editor**:\n\n![Maya - Render attributes](assets/maya-renderglobals.jpg)\n\nFew options that needs explaining:\n\n* `Primary Pool` - here is list of pool fetched from server you can select from.\n* `Suspend publish Job` - job sent to farm will not start render automatically\nbut is in *waiting* state.\n* `Extend Frames` - if checked it will add new frames to previous render, so you can\nextend previous image sequence.\n* `Override Existing Frame` - will overwrite file in destination if they exists\n* `Priority` is priority of job on farm\n* `Frames Per Task` is number of sequence division between individual tasks (chunks)\nmaking one job on farm.\n\nNow if you run publish, you notice there is in right column new item called\n`Render Layers` and in it there is our new layer `Main (999_abc_0010) [1-10]`. First part is\nlayer name, second `(999_abc_0010)` is asset name and rest is frame range.\n\n![Maya - Render Publish errors](assets/maya-render_publish_detail1.jpg)\n\nYou see I already tried to run publish but was stopped by few errors. Lets go\nthrough them one by one just to see what we need to set up further in scene for\nsuccessful publish.\n\n**No Default Cameras Renderable** is telling me:\n\n```fix\nRenderable default cameras found: [u'|persp|perspShape']\n```\n\nand so can be resolved by simple change in *Main* layer render settings.\nAll I have to do is just remove the `persp` camera from render settings and add there correct camera.\n\nThis leaves me only with **Render Settings** error. If I click on it to see\ndetails, I see it has problem with animation not being enabled:\n\n```fix\nAnimation needs to be enabled. Use the same frame for start and end to render single frame\n```\n\nGo to **Render Settings**, select your render layer and in **Common** tab change\nin **File Output** `Frame/Animation ext` to whatever you want, just not _Single Frame_.\nSet **Frame Range** `Start frame` and `End frame` according your needs.\n\nIf you run into problems with *image file prefix* - this should be set correctly when\ncreating *Render instance*, but you can tweak it. It needs to begin with `maya/<Scene>` token\nto avoid render conflicts between DCCs. It needs to have `<RenderLayer>` or `<Layer>` (vray) and\n`<RenderPass>` or `<Aov>` (vray). If you have more then one renderable cameras, add `<Camera>` token.\n\nSane default for arnold, redshift or renderman is:\n\n```fix\nmaya/<RenderLayer>/<RenderLayer>_<RenderPass>\n```\n\nand for vray:\n\n```fix\nmaya/<Layer>/<Layer>\n```\n\nDoing **OpenPype → Set Resolution** will set correct resolution on camera.\n\nScene is now ready for submission and should publish without errors.\n\n:::tip what happens when I publish my render scene\nWhen publishing is finished, job is created on farm. This job has one more dependent job connected to itself.\nWhen render is finished, this other job triggers in and run publish again, but this time it is publishing rendered image sequence and creating quicktime movie for preview from it. Only those rendered sequences that have **beauty** AOV get preview as it doesn't make sense to make it for example from cryptomatte.\n:::\n\n### Attaching render to subset\n\nYou can create render that will be attached to another subset you are publishing, rather than being published on its own. Let's assume, you want to render a model turnaround.\nIn the scene from where you want to publish your model create *Render subset*. Prepare your render layer as needed and then drag\nmodel subset (Maya set node) under corresponding `LAYER_` set under *Render instance*. During publish, it will submit this render to farm and\nafter it is rendered, it will be attached to your model subset.\n\n### Tile Rendering\n:::note Deadline\nThis feature is only supported when using Deadline. See [here](module_deadline#openpypetileassembler-plugin) for setup.\n:::\nOn the render instance objectset you'll find:\n\n* `Tile Rendering` - for enabling tile rendering.\n* `Tile X` - number of tiles in the X axis.\n* `Tile Y` - number of tiles in the Y axis.\n\nWhen submittig to Deadline, you'll get:\n\n- for each frame a tile rendering job, to render each from Maya.\n- for each frame a tile assembly job, to assemble the rendered tiles.\n- job to publish the assembled frames.\n\n## Render Setups\n\n### Publishing Render Setups\n\nOpenPype can publish whole **Render Settings** setup. You can then version in and load it to\nany Maya scene. This helps TDs to distribute per asset/shots render settings for Maya.\n\nTo publish render settings, go **OpenPype → Create...** and select **Render Setup Preset**.\n\nIn your scene will appear set `rendersetup<subset>`. This one has no settings, only its presence\nin scene will trigger publishing of render settings.\n\nWhen you publish scene, current settings in **Render Settings** will be serialized to json file.\n\n### Loading Render Setups\n\nIn any scene, you can load published render settings with **OpenPype → Load...**. Select your published\nrender setup settings, right+click on it and select **Load RenderSetup template**.\n\nThis will load and parse json file and apply all setting there to your Render Setting.\n\n:::warning\nThis will overwrite all setting you already have.\n:::\n\n## Reviews\n\nOpenPype supports creating review video for almost any type of data you want to publish.\nWhat we call review video is actually _playblast_ or _capture_ (depending on terminology\nyou are familiar with) made from pre-defined camera in scene. This is very useful\nin cases where you want to add turntable preview of your model for example. But it can\nbe used to generate preview for animation, simulations, and so on. You can either\npublish review as separate subset version, or you can attach generated video to subset you\nare publishing - for example attach video of turntable rotation to published model as in\nfollowing example.\n\n### Setting scene for review extraction\n\nLets see how review publishing works on simple scene. We will publish model with\nturntable preview video.\n\nI'll be using Stanford University dragon model. Start with empty scene.\nCreate your model, import it or load from OpenPype. I'll just import model as OBJ\nfile.\n\nAfter we have our model in, we need to set everything to be able to publish it\nas model - for detail see [Publishing models](artist_hosts_maya.md#publishing-models).\n\nTo recap - freeze transforms, rename it to `dragon_GEO` and put it into group\n`dragon_GRP`. Then select this group and **OpenPype → Create...** and choose **Model**.\n\nNow, lets create camera we need to generate turntable video. I prefer to animate\ncamera itself and not model because all animation keys will be associated with camera\nand not model we want to publish.\n\nI've created camera, named it `reviewCamera` and parent it under `reviewRotation_LOC`\nlocator. I set my timeline to 50 frames, key `reviewRotation_LOC` Y axis on frame\n1 to 0 and on frame 50 to 360. I've also set animation curve between those two keys\nto linear.\n\nTo mark camera to be used for review, select camera `reviewCamera` and go **OpenPype → Create...**\nand choose **Review**.\n\nThis will create set `review<subset>` including selected camera. You can set few options\non this set to control review video generation:\n\n* `Active` - control on/off state\n* `Frame Start` - starting frame for review\n* `Frame End` - end frame for review\n* `Handles` - number of handle frame before and after\n* `Step` - number of steps\n* `Fps` - framerate\n\nNext step is to move your model set to review set so it will be connected to each other.\n\nThis is my scene:\n\n![Maya - Review model setup](assets/maya-model_review_setup.jpg)\n\nYou see that `modelMain` in under `reviewMain` with `reviewCamera`.\n\n_* note that I had to fix UVs and normals on Stanford dragon model as it wouldn't pass\nmodel validators_\n\n### Publishing model with review\n\nYou can now publish your model and generate review video. Go **OpenPype → Publish...**,\nvalidate if you will, and publish it. During publishing, Maya will create _playblast_\nfor whole frame range you've specified, then it will pass those frames to _ffmpeg_.\nThat will create video file, pass it to another extractor creating burnins in it\nand finally uploading this video to ftrack with your model (or other type) published\nversion. All parts of this process - like what burnins, what type of video file,\nsettings for Maya playblast - can be customized by your TDs. For more information\nabout customizing review process refer to [admin section](project_settings/settings_project_global.md/#publish-plugins).\n\nIf you don't move `modelMain` into `reviewMain`, review will be generated but it will\nbe published as separate entity.\n\n\n## Inventory Actions\n\n### Connect Geometry\n\nThis action will connect geometries between containers.\n\n#### Usage\n\nSelect 1 container of type `animation` or `pointcache`, then 1+ container of any type.\n\n#### Details\n\nThe action searches the selected containers for 1 animation container of type `animation` or `pointcache`. This animation container will be connected to the rest of the selected containers. Matching geometries between containers is done by comparing the attribute `cbId`.\n\nThe connection between geometries is done with a live blendshape.\n\n### Recreate rig animation instance\n\nThis action can regenerate an animation instance for a loaded rig, for example\nfor when it was accidentally deleted by the user.\n\n![Maya - Inventory Action Recreate Rig Animation Instance](assets/maya-inventory_action_recreate_animation_instance.png)\n\n#### Usage\n\nSelect 1 or more container of type `rig` for which you want to recreate the\nanimation instance.\n"
  },
  {
    "path": "website/docs/artist_hosts_maya_arnold.md",
    "content": "---\nid: artist_hosts_maya_arnold\ntitle: Arnold for Maya\nsidebar_label: Arnold\n---\n## Arnold Scene Source (.ass)\nArnold Scene Source can be published as a single file or a sequence of files, determined by the frame range.\n\nWhen creating the instance, two objectsets are created; `content` and `proxy`. Meshes in the `proxy` objectset will be the viewport representation when loading as `standin`.\n\n### Arnold Scene Source Proxy Workflow\nIn order to utilize operators and proxies, the content and proxy nodes need to share the same names (including the shape names). This is done by parenting the content and proxy nodes into separate groups. For example:\n\n![Arnold Scene Source](assets/maya-arnold_scene_source.png)\n\n## Standin\nArnold Scene Source `ass` and Alembic `abc` are supported to load as standins.\n\n### Standin Proxy Workflow\nIf a subset has a proxy representation, this will be used as display in the viewport. At render time the standin path will be replaced using the recommended string replacement workflow;\n\nhttps://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_maya_operators_am_Updating_procedural_file_paths_with_string_replace_html\n\nSince the content and proxy nodes share the same names and hierarchy, any manually shader assignments will be shared.\n\n\n:::note for advanced users\nYou can stop the proxy swapping by disabling the string replacement operator found in the container.\n![Arnold Standin](assets/maya-arnold_standin.png)\n:::\n"
  },
  {
    "path": "website/docs/artist_hosts_maya_multiverse.md",
    "content": "---\nid: artist_hosts_maya_multiverse\ntitle: Multiverse for Maya\nsidebar_label: Multiverse USD\n---\n\n## Working with Multiverse in OpenPype\n\nOpenPype supports creating, publishing and loading of [Multiverse | USD](\nhttps://multi-verse.io) data. The minimum Multiverse version supported is v6.7,\nand version 7.0 is recommended.\n\nIn a nutshell it is possible to:\n\n- Create USD Assets, USD compositions, USD Overrides.\n\n  This _creates_ OpenPype instances as Maya set nodes that contain information\n  for published USD data.\n\n- Create Multiverse Looks.\n\n  This _creates_ OpenPype instances as Maya set nodes that contain information\n  for published Maya shading networks data and USD material assignment data.\n\n- Publish USD Assets, USD compositions and USD Overrides.\n\n  This _writes_ USD files to disk and _publishes_ information to the OpenPype\n  database.\n\n- Publish Multiverse Looks.\n\n  This _writes_ a Maya file containing shading networks (to import in Maya), a\n  USD override file containing material assignment information (to layer in a\n  Multiverse Compound), it copies original & mip-mapped textures to disk and\n  _publishes_ information to the OpenPype database.\n\n- Load any USD data into Multiverse \"Compound\" shape nodes.\n\n  This _reads_ USD files (and also Alembic files) into Maya by _streaming_ them\n  to the viewport.\n\n- Rendering USD data procedurally with 3Delight<sup>NSI</sup>, Arnold, Redshift,\n  RenderMan and VRay.\n\n  This reads USD files by _streaming_ them procedurally to the renderer, at\n  render time.\n\nUSD files written by Multiverse are 100% native USD data, they can be exchanged\nwith any other DCC applications able to interchange USD. Likewise, Multiverse\ncan read native USD data created by other applications. The USD extensions are\nsupported: `.usd` (binary), `.usda` (ASCII), `.usdz`. (zipped, optionally with\ntextures). Sequences of USD files can also be read via \"USD clips\".\n\nIt is also possible to load Alembic data (`.abc`) in Multiverse Compounds,\nfurther compose it & override it in other USD files, and render it procedurally.\nAlembic data is always converted on the fly (in memory) to USD data. USD clip\nfrom Alembic data are also supported.\n\n\n### Configuration\n\nTo configure Multiverse in OpenPype, an admin privileges needs to setup a new\nOpenPype tool in the OpenPype Project Settings, using a similar configuration as\nthe one depicted here:\n\n![Maya - Multiverse Setup](assets/maya-multiverse_setup.png)\n\n\n```\n{\n    \"MULTIVERSE_PATH\": \"/Path/to/Multiverse-{MULTIVERSE_VERSION}\",\n    \"MAYA_MODULE_PATH\": \"{MULTIVERSE}/Maya;{MAYA_MODULE_PATH}\"\n}\n\n{\n    \"MULTIVERSE_VERSION\": \"7.1.0-py27\"\n}\n\n```\n\nThe Multiverse Maya module file (.mod) pointed above contains all the necessary\nenvironment variables to run Multiverse.\n\nThe OpenPype settings will contain blocks to enable/disable the Multiverse\nCreators and Loader, along with sensible studio setting.\n\nFor more information about setup of Multiverse please refer to the relative page\non the [Multiverse official documentation](https://multi-verse.io/docs).\n\n\n### Understanding Assets, Compounds, Compositions, Overrides and Layering\n\nIn Multiverse we use some terminology that relates to USD I/O: terms like\n\"Assets\", \"Compounds\", \"Compositions\", \"Overrides\" and \"Layering\".\n\nPlease hop to the new [Multiverse Introduction](\nhttps://j-cube.jp/solutions/multiverse/docs/usage/introduction) page on the\nofficial documentation to understand them before reading the next sections.\n\n\n### Creators\n\nIt is possible to create OpenPype \"instances\" (resulting in Maya set containers)\nfor publishing Multiverse USD Assets, Compositions, Overrides and Looks.\n\nWhen creating OpenPype instances for Multiverse USD Asset, Composition,\nOverride and Look, the creator plug-in will put the relative selected data in a\nMaya set node which holds the properties used by the Multiverse data writer for\npublishing.\n\nYou can choose the USD file format in the Creators' set nodes:\n\n- Assets: `.usd` (default) or `.usda` or `.usdz`\n- Compositions: `.usda` (default) or `.usd`\n- Overrides: `.usda` (default) or `.usd`\n- Looks: `.ma` (default)\n\n![Maya - Multiverse Asset Creator](assets/maya-multiverse_openpype_asset_creator.png)\n\n![Maya - Multiverse Asset Creator](assets/maya-multiverse_openpype_composition_creator.png)\n\n![Maya - Multiverse Asset Creator](assets/maya-multiverse_openpype_override_creator.png)\n\n![Maya - Multiverse Asset Creator](assets/maya-multiverse_openpype_look_creator.png)\n\n### Publishers\n\nThe relative publishers for Multiverse USD Asset, Composition, Override and Look\nare available. The first three write USD files to disk, while look writes a Maya\nfile along with the mip-mapped textures. All communicate publish info to the\nOpenPype database.\n\n![Maya - Multiverse Publisher](assets/maya-multiverse_openpype_publishers.png)\n\n\n### Loader\n\nThe loader creates a Multiverse \"Compound\" shape node reading the USD file of\nchoice. All data is _streamed_ to the viewport and not contained in Maya. Thanks\nto the various viewport draw options the user can strategically decide how to\nminimize the cost of viewport draw effectively being able to load any data, this\nallows to bring into Maya scenes of virtually unlimited complexity.\n\n![Maya - Multiverse Loader](assets/maya-multiverse_openpype_loader.png)\n\n:::tip Note\nWhen using the Loader, Multiverse, by design, never \"imports\" USD data into the\nMaya scene as Maya data. Instead, when desired, Multiverse permits to import\nspecific USD primitives, or entire hierarchies, into the Maya scene as Maya data\nselectively from MEOW, it also tracks what is being imported with a \"live\nconnection\" , so upon modification, it is possible to write (create & publish)\nthe modifies data as a USD file for being layered on top of its relative\nCompound. See the [Multiverse Importer](\nhttps://j-cube.jp/solutions/multiverse/docs/usage/importer)) documentation.\n:::\n\n### Look\n\nIn OpenPype a Multiverse Look is the combination of:\n\n- a Maya file that contains the shading networks that were assigned to the items\n  of a Multiverse Compound.\n- a Multiverse USD Override file that contains the material assignment\n  information (which Maya material was assigned to which USD item)\n- mip-mapped textures\n\nMultiverse Look shading networks are typically Maya-referenced in the lighting\nand shot scenes.\n\nMaterials are assigned to the USD items in the Compound via the \"material\nassignment\" information that is output in the lookdev stage by a Multiverse\nOverride. Once published the override can be Layered on the Compound so that \nmaterials will be assigned to items. Finally, an attribute Override on the root\nitem of the Compound is used to define the `namespace` with which the shading\nnetworks were referenced in Maya. At this point the renderer knows which\nmaterial to assign to which item and it is possible to render and edit the\nmaterials as usual. Because the material exists in Maya you can perform IPR and\ntune the materials as you please.\n\nThe Multiverse Look will also publish textures in optimized mip-map format,\ncurrently supporting the `.tdl` (Texture Delight) mip map format of the 3Delight\nNSI renderer. MipMaps are required when the relative option is checked and you\nare publishing Multiverse Looks with the `final` or `-` subset, while they are\nnot required with the `WIP` or `test` subsets. MipMaps are found automatically\nas long as they exist alongside the original textures. Their generation can be\nautomatic when using 3Delight for Maya or can be manual by using the `tdlmake`\nbinary utility.\n\n\n### About embedding shading networks in USD\n\nAlternatively, but also complementary to the Multiverse Look, as of Multiverse\n7 it is also possible to write shading networks _inside_ USD files: that is\nachieved by using either the Asset writer (if material are defined in the\nmodeling stage) and the Override writer (if materials are defined in the lookdev\nor later stage).\n\nSome interesting consequences of USD shading networks in Multiverse:\n\n1. they can be overridden by a shading network in Maya by assigning in MEOW a\n   Maya material as an override\n2. they are available for assignment in MEOW, so you can assign a USD material\n   to an item as an override\n3. From Hypershade you can use the Multiverse USD shading network write File>\n   Export option to write USD shading network libraries to then layer on an asset\n   and perform 2. again.\n\nNote that:\n\n- Shading networks in USD can then be currently rendered with\n  3Delight<sup>NSI</sup>\n- Shading networks in USD can be used for interchange with DCC apps. Multiverse\n  shading networks are written natively with the USD Shade schema.\n- usdPreviewSurface shading networks are too considered embedded shading\n  networks, though they are classified separately from non-preview / final\n  quality shading networks\n- USDZ files use usdPreviewSurface shading networks, and therefore can be, too,\n  rendered (with 3Delight<sup>NSI</sup>)\n- in case both usdPreviewSurface and final quality shading networks, the latter\n  will be used for rendering (while the former can be previewed in the viewport)  \n- it is possible to disable rendering of any embedded shading network via the\n  relative option in the Compound Attribute Editor.\n\n\n### Rendering\n\nMultiverse offers procedural rendering with all the major production renderers:\n\n- 3Delight<sup>NSI</sup>\n- Arnold\n- Redshift\n- RenderMan\n- VRay\n\nProcedural rendering effectively means that data is _streamed_ to the renderer\nat render-time, without the need to store the data in the Maya scene (this\neffectively means small .ma/.mb files that load fast) nor in the renderer native\nfile format scene description file (this effectively means tiny `.nsi` / `.ass`\n/ `.vrscene` / `.rib` files that load fast).\n\nThis is completely transparent to the user: Multiverse Compound nodes present in\nthe scene, once a render is launched, will stream data to the renderer in a\nprocedural fashion.\n\n\n### Example Multiverse Pipeline and API\n\nAn example diagram of the data flow in a Maya pipeline using Multiverse is\navailable, see the [Multiverse Pipeline](\nhttps://j-cube.jp/solutions/multiverse/docs/pipeline) documentation.\n\n\nA very easy to use Python API to automate any task is available, the API is\nuser friendly and does not require any knowledge of the vast and complex USD\nAPIs. See the [Multiverse Python API](\nhttps://j-cube.jp/solutions/multiverse/docs/dev/python-api.html) documentation.\n"
  },
  {
    "path": "website/docs/artist_hosts_maya_redshift.md",
    "content": "---\nid: artist_hosts_maya_redshift\ntitle: Redshift for Maya\nsidebar_label: Redshift\n---\n\n## Working with Redshift in OpenPype\n\n### Using Redshift Proxies\n\nOpenPype supports working with Redshift Proxy files. You can create  Redshift Proxy from almost\nany hierarchy in Maya and it will be included there. Redshift can export animation\nproxy file per frame.\n\n### Creating Redshift Proxy\n\nTo mark data to publish as Redshift Proxy, select them in Maya and - **OpenPype → Create ...** and\nthen select **Redshift Proxy**. You can name your subset and hit **Create** button.\n\nYou can enable animation in Attribute Editor:\n\n![Maya - Yeti Rig Setup](assets/maya-create_rs_proxy.jpg)\n\n### Publishing Redshift Proxies\n\nOnce data are marked as Redshift Proxy instance, they can be published - **OpenPype → Publish ...**\n\n### Using Redshift Proxies\n\nPublished proxy files can be loaded with OpenPype Loader. It will create mesh and attach Redshift Proxy\nparameters to it - Redshift will then represent proxy with bounding box.\n"
  },
  {
    "path": "website/docs/artist_hosts_maya_vray.md",
    "content": "---\nid: artist_hosts_maya_vray\ntitle: VRay for Maya\nsidebar_label: VRay\n---\n\n## Working with VRay in OpenPype\n\n### #Using VRay Proxies\n\nOpenPype support publishing, loading and using of VRay Proxy in look management. Their underlying format\ncan be either vrmesh or alembic.\n\n:::warning vrmesh or alembic and look management\nBe aware that **vrmesh** cannot be used with looks as it doesn't retain IDs necessary to map shaders to geometry.\n:::\n\n### Creating VRay Proxy\n\nTo create VRay Proxy, select geometry you want and - **OpenPype → Create ...** select **VRay Proxy**. Name your\nsubset as you want and press **Create** button.\n\nThis will create `vrayproxy` set for your subset. You can set some options in Attribute editor, mainly if you want\nexport animation instead of single frame.\n\n![Maya - VRay Proxy Creation](assets/maya-vray_proxy.jpg)\n\n### Publishing VRay Proxies\n\nVRay Proxy can be published - **OpenPype → Publish ...**. It will publish data as VRays `vrmesh` format and as\nAlembic file.\n\n## Using VRay Proxies\n\nYou can load VRay Proxy using loader - **OpenPype → Loader ...**\n\n![Maya - VRay Proxy Creation](assets/maya-vray_proxy-loader.jpg)\n\nSelect your subset and right-click. Select **Import VRay Proxy (vrmesh)** to import it.\n\n:::note\nNote that even if it states `vrmesh` in descriptions, if loader finds Alembic published along (default behavior) it will\nuse abc file instead of vrmesh as it is more flexible and without it looks doesn't work.\n:::\n"
  },
  {
    "path": "website/docs/artist_hosts_maya_xgen.md",
    "content": "---\nid: artist_hosts_maya_xgen\ntitle: Xgen for Maya\nsidebar_label: Xgen\n---\n\nOpenPype supports Xgen classic with the follow workflow. It eases the otherwise cumbersome issues around Xgen's side car files and hidden behaviour inside Maya. The workflow supports publishing, loading and updating of Xgen collections, along with connecting animation from geometry and (guide) curves.\n\n## Setup\n\n### Settings\n\nGo to project settings > `Maya` > enable `Open Workfile Post Initialization`;\n\n`project_settings/maya/open_workfile_post_initialization`\n\nThis is due to two errors occurring when opening workfile containing referenced xgen nodes on launch of Maya, specifically:\n\n- ``Critical``: Duplicate collection errors on launching workfile. This is because Maya first imports Xgen when referencing in external Maya files, then imports Xgen again when the reference edits are applied.\n```\nImporting XGen Collections...\n# Error: XGen:  Failed to find description ball_xgenMain_01_:parent in collection ball_xgenMain_01_:collection. Abort applying delta: P:/PROJECTS/OP01_CG_demo/shots/sh040/work/Lighting/cg_sh040_Lighting_v001__ball_xgenMain_01___collection.xgen  #\n# Error: XGen:  Tried to import a duplicate collection, ball_xgenMain_02_:collection, from file P:/PROJECTS/OP01_CG_demo/shots/sh040/work/Lighting/cg_sh040_Lighting_v001__ball_xgenMain_02___collection.xgen. Aborting import.  #\n```\n- ``Non-critical``: Errors on opening workfile and failed opening of published xgen. This is because Maya imports Xgen when referencing in external Maya files but the reference edits that ensure the location of the Xgen files are correct, has not been applied yet.\n```\nImporting XGen Collections...\n# Error: XGen:  Failed to open file: P:/PROJECTS/OP01_CG_demo/shots/sh040/work/Lighting/cg_ball_xgenMain_v035__ball_rigMain_01___collection.xgen  #\n# Error: XGen:  Failed to import collection from file P:/PROJECTS/OP01_CG_demo/shots/sh040/work/Lighting/cg_ball_xgenMain_v035__ball_rigMain_01___collection.xgen  #\n```\n\nGo to project settings > `Deadline` > `Publish plugins` > `Maya Submit to Deadline` > disable `Use Published scene`;\n\n`project_settings/deadline/publish/MayaSubmitDeadline/use_published`\n\nThis is due to temporary workaround while fixing rendering with published scenes.\n\n## Create\n\nCreate an Xgen instance to publish. This needs to contain only **one Xgen collection**.\n\n`OpenPype > Create... > Xgen`\n\nYou can create multiple Xgen instances if you have multiple collections to publish.\n\n:::note\nThe Xgen publishing requires a namespace on the Xgen collection (palette) and the geometry used.\n:::\n\n### Publish\n\nThe publishing process will grab geometry used for Xgen along with any external files used in the collection's descriptions. This creates an isolated Maya file with just the Xgen collection's dependencies, so you can use any nested geometry when creating the Xgen description. An Xgen version will consist of:\n\n- Maya file (`.ma`) - this contains the geometry and the connections to the Xgen collection and descriptions.\n- Xgen file (`.xgen`) - this contains the Xgen collection and description.\n- Resource files (`.ptx`, `.xuv`) - this contains Xgen side car files used in the collection and descriptions.\n\n## Load\n\nOpen the Loader tool, `OpenPype > Loader...`, and navigate to the published Xgen version. On right-click you'll get the option `Reference Xgen (ma)`\nWhen loading an Xgen version the following happens:\n\n- References in the Maya file.\n- Copies the Xgen file (`.xgen`) to the current workspace.\n- Modifies the Xgen file copy to load the current workspace first then the published Xgen collection.\n- Makes a custom attribute on the Xgen collection, `float_ignore`, which can be seen under the `Expressions` tab of the `Xgen` UI. This is done to initialize the Xgen delta file workflow.\n- Setup an Xgen delta file (`.xgd`) to store any workspace changes of the published Xgen version.\n\nWhen the loading is done, Xgen collection will be in the Xgen delta file workflow which means any changes done in the Maya workfile will be stored in the current workspace. The published Xgen collection will remain intact, even if the user assigns maps to any attributes or otherwise modifies any attribute.\n\n### Updating\n\nWhen there are changes to the Xgen version, the user will be notified when opening the workfile or publishing. Since the Xgen is referenced, it follows the standard Maya referencing system and overrides.\n\nFor example publishing `xgenMain` version 1 with the attribute `renderer` set to `None`, then version 2 has `renderer` set to `Arnold Renderer`. When updating from version 1 to 2, the `renderer` attribute will be updated to `Arnold Renderer` unless there is a local override.\n\n### Connect Patches\n\nWhen loading in an Xgen version, it does not have any connections to anything in the workfile, so its static in the position it was published in. Use the [Connect Geometry](artist_hosts_maya#connect-geometry) action to connect Xgen to any matching loaded animated geometry.\n\n### Connect Guides\n\nAlong with patches you can also connect the Xgen guides to an Alembic cache.\n\n#### Usage\n\nSelect 1 animation container, of family `animation` or `pointcache`, then the Xgen containers to connect to. Right-click > `Actions` > `Connect Xgen`.\n\n***Note: Only alembic (`.abc`) representations are allowed.***\n\n#### Details\n\nConnecting the guide will make Xgen use the Alembic directly, setting the attributes under `Guide Animation`, so the Alembic needs to contain the same amount of curves as guides in the Xgen.\n\nThe animation container gets connected with the Xgen container, so if the animation container is updated so will the Xgen container's attribute.\n\n## Rendering\n\nTo render with Xgen, follow the [Rendering With OpenPype](artist_hosts_maya#rendering-with-openpype) guide.\n\n### Details\n\nWhen submitting a workfile with Xgen, all Xgen related files will be collected and published as the workfiles resources. This means the published workfile is no longer referencing the workspace Xgen files.\n"
  },
  {
    "path": "website/docs/artist_hosts_maya_yeti.md",
    "content": "---\nid: artist_hosts_maya_yeti\ntitle: Yeti for Maya\nsidebar_label: Yeti\n---\n\n## Working with Yeti in OpenPype\n\nOpenPype can work with [Yeti](https://peregrinelabs.com/yeti/) in two data modes.\nIt can handle Yeti caches and Yeti rigs.\n\n## Yeti Caches\n\n### Creating and publishing\n\nLet start by creating simple Yeti setup, just one object and Yeti node. Open new\nempty scene in Maya and create sphere. Then select sphere and go **Yeti → Create Yeti Node on Mesh**\nOpen Yeti node graph **Yeti → Open Graph Editor** and create setup like this:\n\n![Maya - Yeti Basic Graph](assets/maya-yeti_basic_setup.jpg)\n\nIt doesn't matter what setting you use now, just select proper shape in first\n*Import* node. Select your Yeti node and create *Yeti Cache instance* - **OpenPype → Create...**\nand select **Yeti Cache**. Leave `Use selection` checked. You should end up with this setup:\n\n![Maya - Yeti Basic Setup](assets/maya-yeti_basic_setup_outline.jpg)\n\nYou can see there is `yeticacheDefault` set. Instead of *Default* it could be named with\nwhatever name you've entered in `subset` field during instance creation.\n\nWe are almost ready for publishing cache. You can check basic settings by selecting\nYeti cache set and opening *Extra attributes* in Maya **Attribute Editor**.\n\n![Maya - Yeti Basic Setup](assets/maya-yeti_cache_attributes.jpg)\n\nThose attributes there are self-explanatory, but:\n\n- `Preroll` is number of frames simulation will run before cache frames are stored.\nThis is useful to \"steady\" simulation for example.\n- `Frame Start` from what frame we start to store cache files\n- `Frame End` to what frame we are storing cache files\n- `Fps` of cache\n- `Samples` how many time samples we take during caching\n\nYou can now publish Yeti cache as any other types. **OpenPype → Publish**. It will\ncreate sequence of `.fur` files and `.fursettings` metadata file with Yeti node\nsetting.\n\n:::note Collect Yeti Cache failure\nIf you encounter **Collect Yeti Cache** failure during collecting phase, and the error is like\n```fix\nNo object matches name: pgYetiMaya1Shape.cbId\n```\nthen it is probably caused by scene not being saved before publishing.\n:::\n\n### Loading\n\nYou can load Yeti cache by **OpenPype → Load ...**. Select your cache, right+click on\nit and select **Load Yeti cache**. This will create Yeti node in scene and set its\ncache path to point to your published cache files. Note that this Yeti node will\nbe named with same name as the one you've used to publish cache. Also notice that\nwhen you open graph on this Yeti node, all nodes are as they were in publishing node.\n\n## Yeti Rigs\n\n### Creating and publishing\n\nYeti Rigs are designed to connect to published models or animation rig. The workflow gives the Yeti Rig full control on that geometry to do additional things on top of whatever input comes in, e.g. deleting faces, pushing faces in/out, subdividing, etc.\n\nLet's start with a [model](artist_hosts_maya.md#loading-model) or [rig](artist_hosts_maya.md#loading-rigs) loaded into the scene. Here we are using a simple rig.\n\n![Maya - Yeti Simple Rig](assets/maya-yeti_simple_rig.png)\n\nWe'll need to prepare the scene a bit. We want some Yeti hair on the ball geometry, so duplicating the geometry, adding the Yeti hair and grouping it together.\n\n![Maya - Yeti Hair Setup](assets/maya-yeti_hair_setup.png)\n\n:::note yeti nodes and types\nYou can use any number of Yeti nodes and types, but they have to have unique names.\n:::\n\nNow we need to connect the Yeti Rig with the animation rig. Yeti Rigs work by publishing the attribute connections from its input nodes and reconnect them later in the pipeline. This means we can only use attribute connections to from outside of the Yeti Rig hierarchy. Internal to the Yeti Rig hierarchy, we can use any complexity of node connections. We'll connnect the Yeti Rig geometry to the animation rig, with the transform and mesh attributes.\n\n![Maya - Yeti Rig Setup](assets/maya-yeti_rig_setup.png)\n\nNow we are ready for publishing. Select the Yeti Rig group (`rig_GRP`) and\ncreate *Yeti Rig instance* - **OpenPype → Create...** and select **Yeti Rig**.\nLeave `Use selection` checked.\n\nLast step is to add our geometry to the rig instance, so middle+drag its\ngeometry to `input_SET` under the `yetiRigMain` set representing rig instance.\nNote that its name can differ and is based on your subset name.\n\n![Maya - Yeti Publish Setup](assets/maya-yeti_publish_setup.png)\n\nYou can have any number of nodes in the Yeti Rig, but only nodes with incoming attribute connections from outside of the Yeti Rig hierarchy is needed in the `input_SET`.\n\nSave your scene and ready for publishing our new simple Yeti Rig!\n\nGo to publish **OpenPype → Publish** and run. This will publish rig with its geometry\nas `.ma` scene, save Yeti node settings and export one frame of Yeti cache from\nthe beginning of your timeline. It will also collect all textures used in Yeti\nnode, copy them to publish folder `resource` directory and set *Image search path*\nof published node to this location.\n\n### Loading\n\nYou can load published Yeti Rigs in OpenPype with **OpenPype → Load ...**,\nselect you Yeti rig and right+click on it. In context menu you should see\n**Load Yeti Rig** item (among others).\n\nTo connect the Yeti Rig with published animation, we'll load in the animation and use the Inventory to establish the connections.\n\n![Maya - Yeti Publish Setup](assets/maya-yeti_load_connections.png)\n\nThe Yeti Rig should now be following the animation. :tada:\n"
  },
  {
    "path": "website/docs/artist_hosts_nuke_tut.md",
    "content": "---\nid: artist_hosts_nuke_tut\ntitle: Nuke\nsidebar_label: Nuke\n---\n\n:::note\nOpenPype supports Nuke version **`11.0`** and above.\n:::\n\n## OpenPype global tools\n\n-   [Set Context](artist_tools_context_manager)\n-   [Work Files](artist_tools_workfiles)\n-   [Create](artist_tools_creator)\n-   [Load](artist_tools_loader)\n-   [Manage (Inventory)](artist_tools_inventory)\n-   [Publish](artist_tools_publisher)\n-   [Library Loader](artist_tools_library_loader)\n\n## Nuke specific tools\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\n### Set Frame Ranges\n\nUse this feature in case you are not sure the frame range is correct.\n\n##### Result\n\n-   setting Frame Range in script settings\n-   setting Frame Range in viewers (timeline)\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Set Frame Ranges](assets/nuke_setFrameRanges.png) <!-- picture needs to be changed -->\n\n</div>\n</div>\n\n\n<figure>\n\n![Set Frame Ranges Timeline](assets/nuke_setFrameRanges_timeline.png)\n\n<figcaption>\n\n1.  limiting to Frame Range without handles\n2.  **Input** handle on start\n3.  **Output** handle on end\n\n</figcaption>\n</figure>\n\n### Set Resolution\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\n\nThis menu item will set correct resolution format for you defined by your production.\n\n##### Result\n\n-   creates new item in formats with project name\n-   sets the new format as used\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Set Resolution](assets/nuke_setResolution.png) <!-- picture needs to be changed -->\n\n</div>\n</div>\n\n\n### Set Colorspace\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\nThis menu item will set correct Colorspace definitions for you. All has to be configured by your production (Project coordinator).\n\n##### Result\n\n-   set Colorspace in your script settings\n-   set preview LUT to your viewers\n-   set correct colorspace to all discovered Read nodes (following expression set in settings)\n\nSee [Nuke Color Management](artist_hosts_nuke_tut.md#nuke-color-management)\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Set Colorspace](assets/nuke_setColorspace.png) <!-- picture needs to be changed -->\n\n</div>\n</div>\n\n\n### Apply All Settings\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\nIt is usually enough if you once per while use this option just to make yourself sure the workfile is having set correct properties.\n\n##### Result\n\n-   set Frame Ranges\n-   set Colorspace\n-   set Resolution\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Apply All Settings](assets/nuke_applyAllSettings.png) <!-- picture needs to be changed -->\n\n</div>\n</div>\n\n### Build Workfile\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\nThis tool will append all available subsets into an actual node graph. It will look into database and get all last [versions](artist_concepts.md#version) of available [subsets](artist_concepts.md#subset).\n\n\n##### Result\n\n-   adds all last versions of subsets (rendered image sequences) as read nodes\n-   ~~adds publishable write node as `renderMain` subset~~\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Build First Work File](assets/nuke_buildFirstWorkfile.png)\n\n</div>\n</div>\n\n## Nuke QuickStart\n\nThis QuickStart is short introduction to what OpenPype can do for you. It attempts to make an overview for compositing artists, and simplifies processes that are better described in specific parts of the documentation.\n\n<iframe width=\"512px\" height=\"288px\" src=\"https://www.youtube.com/embed/jgwmLOPJg0g\" frameborder=\"0\" modestbranding=\"1\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"1\"></iframe>\n\n### Launch Nuke - Shot and Task Context\nOpenPype has to know what shot and task you are working on. You need to run Nuke in context of the task, using Ftrack Action or OpenPype Launcher to select the task and run Nuke.\n\n![Run Nuke From Ftrack](assets/nuke_tut/nuke_RunNukeFtrackAction_p3.png)\n![Run Nuke From Launcher](assets/nuke_tut/nuke_RunNukeLauncher_p2.png)\n\n:::tip Admin Tip - Nuke version\nYou can [configure](admin_settings_project_anatomy.md#Attributes) which DCC version(s) will be available for current project in **Studio Settings → Project → Anatomy → Attributes → Applications**\n:::\n\n### Nuke Initial setup\nNuke OpenPype menu shows the current context\n\n![Context](assets/nuke_tut/nuke_Context.png)\n\nLaunching Nuke with context stops your timer, and starts the clock on the shot and task you picked.\n\nOpenpype makes initial setup for your Nuke script. It is the same as running [Apply All Settings](artist_hosts_nuke_tut.md#apply-all-settings) from the OpenPype menu.\n\n- Reads frame range and resolution from Avalon database, sets it in Nuke Project Settings,\nCreates Viewer node, sets it’s range and indicates handles by In and Out points.\n\n- Reads Color settings from the project configuration, and sets it in Nuke Project Settings and Viewer.\n\n- Sets project directory in the Nuke Project Settings to the Nuke Script Directory\n\n:::tip Tip - Project Settings\nAfter Nuke starts it will automatically **Apply All Settings** for you. If you are sure the settings are wrong just contact your supervisor and he will set them correctly for you in project database.\n:::\n\n### Save Nuke script – the Work File\nUse OpenPype - Work files menu to create a new Nuke script. Openpype offers you the preconfigured naming. \n![Context](assets/nuke_tut/nuke_WorkFileSaveAs.png)\n\nThe Next Available Version checks the work folder for already used versions and offers the lowest unused version number automatically.\n\nSubversion can be used to distinguish or name versions. For example used to add shortened artist name.\n\nMore about [workfiles](artist_tools_workfiles).\n\n\n:::tip Admin Tips\n- **Workfile Naming**\n\n  - The [workfile naming](admin_settings_project_anatomy.md#templates) is configured in anatomy, see **Studio Settings → Project → Anatomy → Templates → Work**\n\n- **Open Workfile**\n\n  - You can [configure](project_settings/settings_project_nuke.md#create-first-workfile) Nuke to automatically open the last version, or create a file on startup. See **Studio Settings → Project → Global → Tools → Workfiles**\n\n- **Nuke Color Settings**\n\n  - [Color setting](project_settings/settings_project_nuke.md) for Nuke can be found in **Studio Settings → Project → Anatomy → Color Management and Output Formats → Nuke**\n:::\n\n### Load plate\nUse Load from OpenPype menu to load any plates or renders available.\n\n![Asset Load](assets/nuke_tut/nuke_AssetLoader.png)\n\nPick the plate asset, right click and choose Load Image Sequence to create a Read node in Nuke.\n\nNote that the Read node created by OpenPype is green. Green color indicates the highest version of asset is loaded. Asset versions could be easily changed by [Manage](#managing-versions). Lower versions will be highlighted by orange color on the read node.\n\n![Asset Load](assets/nuke_tut/nuke_AssetLoadOutOfDate.png)\n\nMore about [Asset loader](artist_tools_loader).\n\n### Create Write Node\nTo create OpenPype managed Write node, select the Read node you just created, from OpenPype menu, pick Create.\nIn the Instance Creator, pick Create Write Render, and Create.\n\n![OpenPype Create](assets/nuke_tut/nuke_Creator.png)\n\nThis will create a Group with a Write node inside.\n\n![OpenPype Create](assets/nuke_tut/nuke_WriteNodeCreated.png)\n\n:::tip Admin Tip - Configuring write node\nYou can configure write node parameters in **Studio Settings → Project → Anatomy → Color Management and Output Formats → Nuke → Nodes**\n:::\n\n### Create Prerender Node\nCreating Prerender is very similar to creating OpenPype managed Write node.\n\n<iframe width=\"512px\" height=\"288px\" src=\"https://www.youtube.com/embed/er4SztHFN-w\" frameborder=\"0\" modestbranding=\"1\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"1\"></iframe>\n\n#### What Nuke Publish Does\nFrom Artist perspective, Nuke publish gathers all the stuff found in the Nuke script with Publish checkbox set to on, exports stuff and raises the Nuke script (workfile) version.\n\nThe Pyblish dialog shows the progress of the process.\n\nThe left column of the dialog shows what will be published. Typically it is one or more renders or prerenders, plus work file.\n\n![OpenPype Publish](assets/nuke_tut/nuke_PyblishDialogNuke.png)\n\nThe right column shows the publish steps\n\n##### Publish steps\n1. Gathers all the stuff found in the Nuke script with Publish checkbox set to on\n2. Collects all the info (from the script, database…)\n3. Validates components to be published (checks render range and resolution...)\n4. Extracts data from the script\n   -  generates thumbnail\n   -  creates review(s) like h264\n   -  adds burnins to review(s)\n   -  Copies and renames components like render(s), review(s), Nuke script... to publish folder\n5. Integrates components (writes to database, sends preview of the render to Ftrack ...\n6. Increments Nuke script version, cleans up the render directory\n\nGathering all the info and validating usually takes just a few seconds. Creating reviews for long, high resolution shots can however take significant amount of time when publishing locally.\n\n##### Pyblish Note and Intent\n![Note and Intent](assets/nuke_tut/nuke_PyblishDialogNukeNoteIntent.png)\n\nArtist can add Note and Intent before firing the publish button. The Note and Intent is meant for easy communication between artist and supervisor. After publish, Note and Intent can be seen in Ftrack notes.\n\n##### Pyblish Checkbox\n\n![Note and Intent](assets/nuke_tut/nuke_PyblishCheckBox.png)\n\nPyblish Dialog tries to pack a lot of info in a small area. One of the more tricky parts is that it uses non-standard checkboxes. Some squares can be turned on and off by the artist, some are mandatory.\n\nIf you run the publish and decide to not publish the Nuke script, you can turn it off right in the Pyblish dialog by clicking on the checkbox. If you decide to render and  publish the shot in lower resolution to speed up the turnaround, you have to turn off the Write Resolution validator. If you want to use an older version of the asset (older version of the plate...), you have to turn off the Validate containers, and so on.\n\nMore info about [Using Pyblish](artist_tools_publisher)\n\n:::tip Admin Tip - Configuring validators\nYou can configure Nuke validators like Output Resolution in **Studio Settings → Project → Nuke → Publish plugins**\n:::\n\n### Review\n![Write Node Review](assets/nuke_tut/nuke_WriteNodeReview.png)\n\nWhen you turn the review checkbox on in your OpenPype write node, here is what happens:\n- OpenPype uses the current Nuke script to \n  - Load the render\n  - Optionally apply LUT\n  - Render Prores 4444 with the same resolution as your render\n- Use Ffmpeg to convert the Prores to whatever review(s) you defined\n- Use Ffmpeg to add (optional) burnin to the review(s) from previous step\n\nCreating reviews is a part of the publishing process. If you choose to do a local publish or to use existing frames, review will be processed also on the artist's machine.\nIf you choose to publish on the farm, you will render and do reviews on the farm.\n\nSo far there is no option for using existing frames (from your local / check render) and just do the review on the farm.\n\nMore info about [configuring reviews](pype2/admin_presets_plugins.md#extractreview).\n\n:::tip Admin Tip - Configuring Reviews\nYou can configure reviewsin **Studio Settings → Project → Global → Publish plugins → ExtractReview / ExtractBurnin**\nReviews can be configured separately for each host, task, or family. For example Maya can produce different review to Nuke, animation task can have different burnin then modelling, and plate can have different review then model.\n:::\n\n### Render and Publish\n\n![OpenPype Create](assets/nuke_tut/nuke_WriteNode.png)\n\nLet’s say you want to render and publish the shot right now, with only a Read and Write node. You need to decide if you want to render, check the render and then publish it, or you want to execute the render and publish in one go.\n\nIf you wish to check your render before publishing, you can use your local machine or your farm to render the write node as you would do without OpenPype, load and check your render (OpenPype Write has a convenience button for that), and if happy, use publish with Use existing frames option selected in the write node to generate the review on your local machine.\n\nIf you want to render and publish on the farm in one go, run publish with On farm option selected in the write node to render and make the review on farm.\n\n![Versionless](assets/nuke_tut/nuke_RenderLocalFarm.png)\n\n### Version-less Render\n\n![Versionless](assets/nuke_tut/nuke_versionless.png)\n\nOpenPype is configured so your render file names have no version number until the render is fully finished and published. The main advantage is that you can keep the render from the previous version and re-render only part of the shot. With care, this is handy.\n\nMain disadvantage of this approach is that you can render only one version of your shot at one time. Otherwise you risk to partially overwrite your shot render before publishing copies and renames the rendered files to the properly versioned publish folder.\n\nWhen making quick farm publishes, like making two versions with different color correction, care must be taken to let the first job (first version) completely finish before the second version starts rendering.\n\n<iframe width=\"512px\" height=\"288px\" src=\"https://www.youtube.com/embed/j95OITIWJk8\" frameborder=\"0\" modestbranding=\"1\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"1\"></iframe>\n\n### Managing Versions\n\n![Versionless](assets/nuke_tut/nuke_ManageVersion.png)\n\nOpenPype checks all the assets loaded to Nuke on script open. All out of date assets are colored orange, up to date assets are colored green.\n\nUse Manage to switch versions for loaded assets.\n\n### Loading Effects\nThis video show how to publish effect from Hiero / Nuke Studio, and use the effect in Nuke.\n\n<iframe width=\"512px\" height=\"288px\" src=\"https://www.youtube.com/embed/zFoH7bq-w0E\" frameborder=\"0\" modestbranding=\"1\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"1\"></iframe>\n\n<iframe width=\"512px\" height=\"288px\" src=\"https://www.youtube.com/embed/HzZDdtII5io\" frameborder=\"0\" modestbranding=\"1\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"1\"></iframe>\n\n### Nuke Color Management\n\n<iframe width=\"512px\" height=\"288px\" src=\"https://www.youtube.com/embed/NKjQHkuwkSM\" frameborder=\"0\" modestbranding=\"1\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"1\"></iframe>\n\n## Troubleshooting\n\n### Fixing Validate Containers\n\nIf your Pyblish dialog fails on Validate Containers, you might have an old asset loaded. Use OpenPype - Manage... to switch the asset(s) to the latest version.\n\n![Versionless](assets/nuke_tut/nuke_ValidateContainers.png)\n\n<iframe width=\"512px\" height=\"288px\" src=\"https://www.youtube.com/embed/hridMybn5nA\" frameborder=\"0\" modestbranding=\"1\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"1\"></iframe>\n\n### Fixing Validate Version\nIf your Pyblish dialog fails on Validate Version, you might be trying to publish already published version. Rise your version in the OpenPype WorkFiles SaveAs.\n\nOr maybe you accidentally copied write node from different shot to your current one. Check the write publishes on the left side of the Pyblish dialog. Typically you publish only one write. Locate and delete the stray write from other shot.\n\n<iframe width=\"512px\" height=\"288px\" src=\"https://www.youtube.com/embed/Ic9z4gKnHAA\" frameborder=\"0\" modestbranding=\"1\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"1\"></iframe>\n"
  },
  {
    "path": "website/docs/artist_hosts_photoshop.md",
    "content": "---\nid: artist_hosts_photoshop\ntitle: Photoshop\nsidebar_label: Photoshop\n---\n\n## Available Tools\n\n-   [Work Files](artist_tools_workfiles)\n-   [Create](artist_tools_creator)\n-   [Load](artist_tools_loader)\n-   [Publish](artist_tools_publisher)\n-   [Manage](artist_tools_inventory)\n\n## Setup\n\nTo install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select Photoshop in menu. Then go to `{path to pype}hosts/photoshop/api/extension.zxp`. Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. \n\n## Usage\n\nWhen you launch Photoshop you will be met with the Workfiles app. If dont have any previous workfiles, you can just close this window.\n\nIn Photoshop you can find the tools in the `OpenPype` extension:\n\n![Extension](assets/photoshop_extension.png) <!-- picture needs to be changed -->\n\nYou can show the extension panel by going to `Window` > `Extensions` > `OpenPype`.\n\n### Publish\n\nWhen you are ready to share some work, you will need to publish. This is done by opening the `Publisher` through the `Publish...` button.\n\n![Publish](assets/photoshop_publish.png)\n\nThere is always instance for workfile created automatically (see 'workfileArt' item in `Subsets to publish` column.) This allows to publish (and therefore backup)\nworkfile which is used to produce another publishable elements (as `image` and `review` items).\n\n#### Create\n\nMain publishable item in Photoshop will be of `image` family. Result of this item (instance) is picture that could be loaded and used in another DCCs (for example as\nsingle layer in composition in AfterEffects, reference in Maya etc).\n\nThere are couple of options what to publish:\n- separate image per layer (or group of layers)\n- all visible layers (groups) flattened into single image\n\nIn most cases you would like to keep `Create only for selected` toggled on and select what you would like to publish. Toggling this off\nwill allow you to create instance(s) for all visible layers without a need to select them explicitly.\n\nFor separate layers option keep `Create separate instance for each selected` toggled, select multiple layers and hit `Create >>>` button in the middle column.\n\nThis will result in:\n\n![Image instances creates](assets/photoshop_publish_images.png)\n\n(In Photoshop's `Layers` tab standard layers will be wrapped into group and enriched with ℗ symbol to denote publishable instance. With `Create separate instance for each selected` toggled off\nit will create only single publishable instance which will wrap all visible layers.)\n\nName of publishable instance (eg. subset name) could be configured with a template in `project_settings/global/tools/creator/subset_name_profiles`.\n(This must be configured by admin who has access to Openpype Settings.)\n\nTrash icon under the list of instances allows to delete any selected `image` instance.\n\nWorkfile instance will be automatically recreated though. If you do not want to publish it, use pill toggle on the instance item.\n\nIf you would like to modify publishable instance, click on `Publish` tab at the top. This would allow you to change name of publishable\ninstances, disable them from publishing, change their task etc.\n\nPublisher allows publishing into different context, just click on any instance, update `Variant`, `Asset` or `Task` in the form in the middle and don't forget to click on the 'Confirm' button.\n\n#### Validate\n\nIf you would like to run validation rules set by your Studio, click on funnel icon at the bottom right. This will run through all\nenabled instances, you could see more information after clicking on `Details` tab.\n\n![Image instances creates](assets/photoshop_publish_validations.png)\n\nIn this dialog you could see publishable instances in left column, triggered plugins in the middle and logs in the right column.\n\nIn left column you could see that `review` instance was created automatically. This instance flattens all publishable instances or\nall visible layers if no publishable instances were created into single image which could serve as a single reviewable element (for example in Ftrack).\n\nCreation of Review could be disabled in `project_settings/photoshop/publish/CollectReview`.\n\nIf you are satisfied with results of validation phase (and there are no errors there), you might hit `Publish` button at bottom right.\nThis will run through extraction phase (it physically creates images from `image` instances, creates `review` etc) and publishes them\n(eg. stores files into their final destination and stores metadata about them into DB).\nThis part might take a while depending on amount of layers in the workfile, amount of available memory and performance of your machine.\n\nYou may encounter issues with publishing which will be indicated with red squares. If these issues are within the validation section, then you can fix the issue. If there are issues outside of validation section, please let the OpenPype team know.\n\nYou can always start new publish run with a circle arrow button at the bottom right. You might also want to move between phases (Create, Update etc)\nby clicking on available tabs at the top of the dialog.\n\n#### Simplified publish\n\nThere is a simplified workflow for simple use case where only single image should be created containing all visible layers.\nNo image instances must be present in a workfile and `project_settings/photoshop/publish/CollectInstances/flatten_subset_template` must be filled in Settings.\nThen artists just need to hit 'Publish' button in menu.\n\n#### Repair Validation Issues\n\nIf there is some issue in validator phase, you will receive something like this:\n\n![Validation error](assets/photoshop_publish_failed.png)\n\nAll validators will give some description about what the issue is. You can inspect this by clicking on items in the left column.\n\nIf there is an option of automatic repair, there will be `Repair` button on the right. In other case you need to fix the issue manually.\n(By deleting and recreating instance etc.)\n\n#### Buttons on the bottom right are for:\n- `Refresh publishing` - set publishing process to starting position - useful if previous publish failed, or you changed configuration of a publish\n- `Stop/pause publishing` - if you would like to pause publishing process at any time\n- `Validate` - if you would like to run only collecting and validating phases (nothing will be published yet)\n- `Publish` - standard way how to kick off full publishing process\n\n### Load\n\nWhen you want to load existing published work, you can load in smart layers through the `Loader`. You can reach the `Loader` through the extension's `Load` button.\n\n![Loader](assets/photoshop_loader.png) <!-- picture needs to be changed -->\n\nThe supported families for Photoshop are:\n\n- `image`\n\nTo load an image, right-click on the subset you want and choose a representation:\n\n![Loader](assets/photoshop_loader_load.gif)\n\n### Manage\n\nNow that we have some images loaded, we can manage which version is loaded. This is done through the `Scene Inventory`. You can reach it through the extension's `Manage` button.\n\n:::note\nLoaded images has to stay as smart layers in order to be updated. If you rasterize the layer, you cannot update it to a different version.\n:::\n\n![Loader](assets/photoshop_manage.png)\n\nYou can switch to a previous version of the image or update to the latest.\n\n![Loader](assets/photoshop_manage_switch.gif)\n![Loader](assets/photoshop_manage_update.gif)\n\n\n#### Support help\nIf you would like to ask for help admin or support, you could use any of the three options on the `Note` button on bottom left:\n- `Go to details` - switches into a more detailed list of published instances and plugins.\n- `Copy report` - stash full publishing log to a clipboard\n- `Export report` - save log into a file for sending it via mail or any communication tool\n\nIf you are able to fix the workfile yourself, use the first button on the right to set the UI to initial state before publish. (Click the `Publish` button to start again.)\n\n#### Legacy instances\n\nAll screenshots from Publish are from updated dialog, before publishing was being done by regular `Pyblish` tool.\nNew publishing process should be backward compatible, eg. if you have a workfile with instances created in the previous publishing approach, they will be translated automatically and\ncould be used right away.\n\nIf you hit on unexpected behaviour with old instances, contact support first, then you could try to delete and recreate instances from scratch.\nNuclear option is to purge workfile metadata in `File > File Info > Origin > Headline`. This is only for most determined daredevils though!\n"
  },
  {
    "path": "website/docs/artist_hosts_resolve.md",
    "content": "---\nid: artist_hosts_resolve\ntitle: DaVinci Resolve\nsidebar_label: DaVinci Resolve\n---\n\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n:::warning\nBefore you are able to start with OpenPype tools in DaVinci Resolve, installation of its own Python 3.6 interpreter and PySide 2 has to be done. Go to [Installation of python and pyside](admin_hosts_resolve.md#installation-of-python-and-pyside) link for more information\n:::\n\n\n\n## OpenPype global tools\n\n-   [Work Files](artist_tools_workfiles)\n-   [Create](artist_tools_creator)\n-   [Load](artist_tools_loader)\n-   [Manage (Inventory)](artist_tools_inventory)\n-   [Publish](artist_tools_publisher)\n\n\n<div class=\"row markdown\">\n\n## Creating Shots from timeline items\n\nBefore a clip can be published with [Publisher](artist_tools_publisher) timeline item has to be marked with OpenPype metadata markers. This way it is converted to a publishable subset.\n\nLets do it step by step.\n\n</div>\n\n\n<div class=\"row markdown\">\n\n### Color clips before opening Create menu\n\n\nTimeline video clips should be colored to `Chocolate` color for OpenPype to se it as selected for subset creation.\n\n\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/resolve_select_clips_timeline_chocolate.png)\n\n</div>\n</div>\n\n\n### Rename timeline track names\n\n<div class=\"row markdown\">\n\n\n<div class=\"col col --6 markdown\">\n\nTo be able to work with dynamic subset name, which is based on track names it is recommended to rename those tracks to what type of plates their clips represent. Commonly used ones are `main`, `review`, `fg01`, `fg02`, `bg`, `bg01`, etc. It is completely up to you but we recommend to always have at least `main` plate. For example if a clip is on track **element** and subset family is set to **plate** then the resulting subset name will be **plateElement**\n\n<br></br>\n</div>\n\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/resolve_creator_subset_name.png)\nThe name of the resulting *subset* can be seen in the **OpenPypeData** marker.\n<br></br><br></br>\n</div>\n\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/resolve_remame_track_names.png)\nSimple track setup where we are only using `main` and  `review` track names.\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/resolve_create_vertical_rename_timeline.png)\nAn example of used track names. The yellow frame is highlighting vertically aligned clips - which are going to be renamed and grouped together under one asset (shot) name. The concept of vertical renaming will be explained later in [Vertical Synchronization of Subset Attributes](#vertical-synchronization-of-subset-attributes).\n\n</div>\n</div>\n\n\n### Create menu...\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\nAfter all clips which are intended to be converted to publishable instances are colored to `Chocolate` color, you can open OpenPype menu.\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/resolve_menu_openpype.png)\n\n</div>\n\n</div>\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\nAfter the menu widget is opened (it can take while so be patient please :).\n\nHit `Create ...` and then set **Use selection** to active and select the family to **Create Publishable Clips**. \n\nThe Subset name can stay as it is, it is not going to be used because each clip will generate it's own name.\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/resolve_create_clips.png)\n\n</div>\n</div>\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\nThe new windows that opens, let's you define various attributes for your future subsets and shots.\n\nSet Rename clips to active if you wish to use different names of shots in pipeline then the original clip names conformed from EDL/XML.\n\n**Count sequence from** - Start of the shot numbering if `#` is used in one of the keywords\n\n**Stepping number** - Sequential gaps in the numbering\n\nAs you can see the in `{shot}` key within *Shot Template Keywords* section, you can use `#` symbol do define padding of the number in sequence and where it's going to be used.\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/resolve_create_renaming_clips.png)\n\n</div>\n</div>\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\nNotice the relationship of following sections. Keys from **Shot Template Keywords** sections will be used for formatting of templates in **Shot Hierarchy And Rename Settings** section.\n\n**Shot parent hierarchy** will be forming parents of the asset (shot) *the hidden root for this is project folder*. So for example of this template we will get resulging string `shots/sq01`\n\n**Clip name template** in context of clip sitting on track name `main` in second position `mainsq01sh020`. This is due track key is hosting `{_track_}` which is inheriting name form timeline track name. Other allowed namespases are:\n- `{_sequence_}`: timeline name\n- `{_clip_}`: clip name\n- `{_trackIndex_}`: position of track on timeline from bottom\n- `{_clipIndex_}`: clip position on timeline from left\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/resolve_create_template_filling.png)\n\n</div>\n</div>\n\n### Vertical synchronization of subset attributes\n\nIn case you are only working with two tracks on timeline where `main` track is going to be used as plates for compositors and `review` track holds mp4 clips for offlines and web preview. **Enable vertical sync** can be deactivated.\n\nIn multiple tracks scenario - as mentioned [here](#rename-timeline-track-names) - it is recommended to activate **Enable vertical sync** and define the hero (driving) track to *main*. This will ensure that all of the clips on corresponding to the same shots will have the same publishing parameters.\n\n<br></br>\n\n<div class=\"row markdown\">\n\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/resolve_create_single_track_rename_hero_track.png)\n\n</div>\n\n<div class=\"col col--6 markdown\">\n\n![Create menu](assets/resolve_create_vertical_rename_creator_ui.png)\n\n</div>\n</div>\n\n\n## Publishing Shots\n\n<div class=\"row markdown\">\n<div class=\"col--6 markdown\">\n\nOnce all `Chocolate` colored clips have gone through the [creator](#rcreate-menu), have been colored to `Pink` color and a marker has been created for each of them, it means they have been successfully converted to publishable clips. Now we can run **Publisher** - it's button can be found in the OpenPype menu.\n\n<br></br>\n</div>\n\n<div class=\"row markdown\">\n<div class=\"col --6 markdown\">\n\n![Create menu](assets/resolve_publish_instance_review_main.png)\nNotice that the main track clips and review had been merged into one instance. And since it is main `hero` clip it is also holding all new shot metadata. For that reason it also create secon instance for each with `shot` family. This instance will create all shot hierarchy and pass frame range attributes to shot (asset).\n\n</div>\n</div>\n\n<div class=\"row markdown\">\n<div class=\"col --6 markdown\">\n\n![Create menu](assets/resolve_publish_instance_other_plateSubsets.png)\nAlso notice how the subset name is formed form a *track* name and *subset family* from previous steps.\n\nAlso important is to notice the asset name in *OpenPypeData* at marker - the name is the same for all **Vertically renamed** shots as they have been grouped together. Unfortunately Resolve is not allowing to rename the clips so the only way to know is to see it in marker's metadata.\n\n</div>\n</div>\n\n</div>\n"
  },
  {
    "path": "website/docs/artist_hosts_substancepainter.md",
    "content": "---\nid: artist_hosts_substancepainter\ntitle: Substance Painter\nsidebar_label: Substance Painter\n---\n\n## OpenPype global tools\n\n-   [Work Files](artist_tools.md#workfiles)\n-   [Load](artist_tools.md#loader)\n-   [Manage (Inventory)](artist_tools.md#inventory)\n-   [Publish](artist_tools.md#publisher)\n-   [Library Loader](artist_tools.md#library-loader)\n\n## Working with OpenPype in Substance Painter\n\nThe Substance Painter OpenPype integration allows you to:\n\n- Set the project mesh and easily keep it in sync with updates of the model\n- Easily export your textures as versioned publishes for others to load and update.\n\n## Setting the project mesh\n\nSubstance Painter requires a project file to have a mesh path configured.\nAs such, you can't start a workfile without choosing a mesh path.\n\nTo start a new project using a published model you can _without an open project_\nuse OpenPype > Load.. > Load Mesh on a supported publish. This will prompt you\nwith a New Project prompt preset to that particular mesh file.\n\nIf you already have a project open, you can also replace (reload) your mesh \nusing the same Load Mesh functionality. \n\nAfter having the project mesh loaded or reloaded through the loader\ntool the mesh will be _managed_ by OpenPype. For example, you'll be notified \non workfile open whether the mesh in your workfile is outdated. You can also\nset it to specific version using OpenPype > Manage.. where you can right click \non the project mesh to perform _Set Version_\n\n:::info\nA Substance Painter project will always have only one mesh set. Whenever you \ntrigger _Load Mesh_ from the loader this will **replace** your currently loaded \nmesh for your open project.\n:::\n\n## Publishing textures\n\nTo publish your textures we must first create a `textureSet` \npublish instance. \n\nTo create a **TextureSet instance** we will use OpenPype's publisher tool. Go \nto **OpenPype → Publish... → TextureSet**\n\nThe texture set instance will define what Substance Painter export template (`.spexp`) to\nuse and thus defines what texture maps will be exported from your workfile. This\ncan be set with the **Output Template** attribute on the instance.\n\n:::info\nThe TextureSet instance gets saved with your Substance Painter project. As such, \nyou will only need to configure this once for your workfile. Next time you can\njust click **OpenPype → Publish...** and start publishing directly with the\nsame settings.\n:::\n\n#### Publish per output map of the Substance Painter preset\n\nThe Texture Set instance generates a publish per output map that is defined in\nthe Substance Painter's export preset. For example a publish from a default\nPBR Metallic Roughness texture set results in six separate published subsets \n(if all the channels exist in your file).\n\n![Substance Painter PBR Metallic Roughness Export Preset](assets/substancepainter_pbrmetallicroughness_export_preset.png)\n\nWhen publishing for example a texture set with variant **Main** six instances will\nbe published with the variants: \n- Main.**BaseColor**\n- Main.**Emissive**\n- Main.**Height**\n- Main.**Metallic**\n- Main.**Normal**\n- Main.**Roughness**\n\nThe bold output map name for the publish is based on the string that is pulled\nfrom the what is considered to be the static part of the filename templates in \nthe export preset. The tokens like `$mesh` and `(_$colorSpace)` are ignored.\nSo `$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)` becomes `BaseColor`.\n\nAn example output for PBR Metallic Roughness would be:\n\n![Substance Painter PBR Metallic Roughness Publish Example in Loader](assets/substancepainter_pbrmetallicroughness_published.png)\n\n## Known issues\n\n#### Can't see the OpenPype menu?\n\nIf you're unable to see the OpenPype top level menu in Substance Painter make\nsure you have launched Substance Painter through OpenPype and that the OpenPype\nIntegration plug-in is loaded inside Substance Painter: **Python > openpype_plugin**\n\n#### Substance Painter + Steam\n\nRunning the steam version of Substance Painter within OpenPype will require you \nto close the Steam executable before launching Substance Painter through OpenPype. \nOtherwise the Substance Painter process is launched using Steam's existing \nenvironment and thus will not be able to pick up the pipeline integration.\n\nThis appears to be a limitation of how Steam works."
  },
  {
    "path": "website/docs/artist_hosts_tvpaint.md",
    "content": "---\nid: artist_hosts_tvpaint\ntitle: TVPaint\nsidebar_label: TVPaint\n---\n\n-   [Work Files](artist_tools_workfiles)\n-   [Load](artist_tools_loader)\n-   [Scene Inventory](artist_tools_inventory)\n-   [Publish](artist_tools_publisher)\n-   [Library](artist_tools_library)\n\n\n## Setup\nWhen you launch TVPaint with OpenPype for the very first time it is necessary to do some additional steps. Right after the TVPaint launching a few system windows will pop up.\n\n![permission](assets/tvp_permission.png)\n\nChoose `Replace the file in the destination`. Then another window shows up.\n\n![permission2](assets/tvp_permission2.png)\n\nClick on `Continue`.\n\nAfter opening TVPaint go to the menu bar: `Windows → Plugins → OpenPype`.\n\n![pypewindow](assets/tvp_hidden_window.gif)\n\nAnother TVPaint window pop up. Please press `Yes`. This window will be presented in every single TVPaint launching. Unfortunately, there is no other way how to workaround it.\n\n![writefile](assets/tvp_write_file.png)\n\nNow OpenPype Tools menu is in your TVPaint work area.\n\n![openpypetools](assets/tvp_openpype_menu.png)\n\nYou can start your work.\n\n---\n\n## Usage\nIn TVPaint you can find the Tools in OpenPype menu extension. The OpenPype Tools menu should be available in your work area. However, sometimes it happens that the Tools menu is hidden. You can display the extension panel by going to `Windows -> Plugins -> OpenPype`.\n\n## Create & Publish\nTo be able to publish, you have to mark what should be published. The marking part is called **Create**. In TVPaint you can create and publish **[Reviews](#review)**, **[Workfile](#workfile)**, **[Render Layers](#render-layer)** and **[Render Passes](#render-pass)**.\n\n:::important\nTVPaint integration tries to not guess what you want to publish from the scene. Therefore, you should tell what you want to publish.\n:::\n\n![createlayer](assets/tvp_publisher.png)\n\n### Review\n`Review` will render all visible layers and create a reviewable output.\n- Is automatically created without any manual work.\n- You can disable the created instance if you want to skip review.\n\n### Workfile\n`Workfile` integrate the source TVPaint file during publishing. Publishing of workfile is useful for backups.\n- Is automatically created without any manual work.\n- You can disable the created instance if you want to skip review.\n\n### Render Layer\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\nRender Layer bakes all the animation layers of one particular color group together.\n\n- In the **Create** tab, pick `Render Layer`\n- Fill `variant`, type in the name that the final published RenderLayer should have according to the naming convention in your studio. *(L10, BG, Hero, etc.)*\n  - Color group will be renamed to the **variant** value\n- Choose color group from combobox\n  - or select a layer of a particular color and set combobox to **&ltUse selection&gt**\n- Hit `Create` button\n\nAfter creating a RenderLayer, choose any amount of animation layers that need to be rendered together and assign them the color group.\n\nYou can change `variant` later in **Publish** tab.\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![createlayer](assets/tvp_create_layer.png)\n\n</div>\n</div>\n<br/>\n\n**How to mark TVPaint layer to a group**\n\nIn the bottom left corner of your timeline, you will note a **Color group** button.\n\n![colorgroups](assets/tvp_color_groups.png)\n\nIt allows you to choose a group by checking one of the colors of the color list.\n\n![colorgroups](assets/tvp_color_groups2.png)\n\nThe timeline's animation layer can be marked by the color you pick from your Color group. Layers in the timeline with the same color are gathered into a group represents one render layer.\n\n![timeline](assets/tvp_timeline_color.png)\n\n\n### Render Pass\n\nRender Passes are smaller individual elements of a [Render Layer](artist_hosts_tvpaint.md#render-layer). A `character` render layer might\nconsist of multiple render passes such as `Line`, `Color` and `Shadow`.\n\nRender Passes are specific because they have to belong to a particular Render Layer. You have to select to which Render Layer the pass belongs. Try to refresh if you don't see a specific Render Layer in the options.\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\nWhen you want to create Render Pass\n- choose one or several TVPaint layers.\n- in the **Create** tab, pick `Render Pass`.\n- fill the `variant` with desired name of pass, e.g. `Color`.\n- select the Render Layer you want the Render Pass to belong to from the combobox.\n  - if you don't see new Render Layer try refresh first.\n- Press `Create`\n\nAfter creating a Render Pass, selected the TVPaint layers that should be marked with color group of Render Layer.\n\nYou can change `variant` or Render Layer later in **Publish** tab.\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![createpass](assets/tvp_create_pass.png)\n\n</div>\n</div>\n\n:::warning\nYou cannot change TVPaint layer name once you mark it as part of Render Pass. You would have to remove created Render Pass and create it again with new TVPaint layer name.\n:::\n\n<br></br>\n\nIn this example, OpenPype will render selected animation layers within the given color group. E.i. the layers *L020_colour_fx*, *L020_colour_mouth*, and *L020_colour_eye* will be rendered as one pass belonging to the yellow RenderLayer.\n\n![renderpass](assets/tvp_timeline_color2.png)\n\nNow that you have created the required instances, you can publish them.\n- Fill the comment on the bottom of the window.\n- Double check enabled instance and their context.\n- Press `Publish`.\n- Wait to finish.\n- Once the `Publisher` turns turns green your renders have been published.\n\n---\n\n## Load\nWhen you want to load existing published work you can reach the `Loader` through the OpenPype Tools `Load` button.\n\nThe supported families for TVPaint are:\n\n- `render`\n- `image`\n- `background`\n- `plate`\n\nTo load a family item, right-click on the subset you want and import their representations, switch among the versions, delete older versions, copy files, etc.\n\n![Loader](assets/tvp_loader.gif)\n\n---\n\n## Scene Inventory\nScene Inventory shows you everything that you have loaded into your scene using OpenPype. You can reach it through the extension's `Scene Inventory` button.\n\n![sceneinventory](assets/tvp_scene_inventory.png)\n\nYou can switch to a previous version of the file or update it to the latest or delete items.\n"
  },
  {
    "path": "website/docs/artist_hosts_unreal.md",
    "content": "---\nid: artist_hosts_unreal\ntitle: Unreal\nsidebar_label: Unreal\n---\n\n## Introduction\n\nOpenPype supports Unreal in similar ways as in other DCCs Yet there are few specific you need to be aware of.\n\n### Creating the Unreal project\n\nSelecting a task and opening it with Unreal will generate the Unreal project, if it hasn't been created before.\nBy default, OpenPype includes the plugin that will be built together with the project.\n\nAlternatively, the Environment variable `\"OPENPYPE_UNREAL_PLUGIN\"` can be set to the path of a compiled version of the plugin.\nThe version of the compiled plugin must match the version of Unreal with which the project is being created.\n\n:::note\nUnreal version 5.0 onwards requires the following Environment variable:\n\n`\"UE_PYTHONPATH\": \"{PYTHONPATH}\"`\n:::\n\n### Project naming\n\nUnreal doesn't support project names starting with non-alphabetic character. So names like `123_myProject` are\ninvalid. If OpenPype detects such name it automatically prepends letter **P** to make it valid name, so `123_myProject` will become `P123_myProject`. There is also soft-limit on project name length to be shorter then 20 characters. Longer names will issue warning in Unreal Editor that there might be possible side effects.\n\n## OpenPype global tools\n\nOpenPype global tools can be found in Unreal's toolbar and in the *Tools* main menu:\n\n![Unreal OpenPype Menu](assets/unreal_openpype_tools.png)\n\n-   [Create](artist_tools_creator)\n-   [Load](artist_tools_loader)\n-   [Manage (Inventory)](artist_tools_inventory)\n-   [Publish](artist_tools_publisher)\n-   [Library Loader](artist_tools_library_loader)\n\n## Static Mesh\n\n### Loading\n\nTo import Static Mesh model, just choose **OpenPype → Load ...** and select your mesh. Static meshes are transferred as FBX files as specified in [Unreal Engine 4 Static Mesh Pipeline](https://docs.unrealengine.com/en-US/Engine/Content/Importing/FBX/StaticMeshes/index.html). This action will create new folder with subset name (`unrealStaticMeshMain_CON` for example) and put all data into it. Inside, you can find:\n\n![Unreal Container Content](assets/unreal_container.jpg)\n\nIn this case there is **lambert1**, material pulled from Maya when this static mesh was published, **antennaA_modelMain** is the geometry itself, **modelMain_v002_CON** is a *AssetContainer* type and is there to mark this directory as Avalon Container (to track changes) and to hold OpenPype metadata.\n\n### Publishing\n\nPublishing of Static Mesh works in similar ways. Select your mesh in *Content Browser* and **OpenPype → Create ...**. This will create folder named by subset you've chosen - for example **unrealStaticMeshDefault_INS**. It this folder is that mesh and *Avalon Publish Instance* asset marking this folder as publishable instance and holding important metadata on it. If you want to publish this instance, go **OpenPype → Publish ...**\n\n## Layout\n\nThere are two different layout options in Unreal, depending on the type of project you are working on.\nOne only imports the layout, and saves it in a level.\nThe other uses [Master Sequences](https://docs.unrealengine.com/4.27/en-US/AnimatingObjects/Sequencer/Overview/TracksShot/) to track the whole level sequence hierarchy.\nYou can choose in the Project Settings if you want to generate the level sequences.\n\n![Unreal OP Settings Level Sequence](assets/unreal_setting_level_sequence.png)\n\n### Loading\n\nTo load a layout, click on the OpenPype icon in Unreal’s main taskbar, and select **Load**.\n\n![Unreal OP Tools Load](assets/unreal_openpype_tools_load.png)\n\nSelect the task on the left, then right click on the layout asset and select **Load Layout**.\n\n![Unreal Layout Load](assets/unreal_load_layout.png)\n\nIf you need to load multiple layouts, you can select more than one task on the left, and you can load them together.\n\n![Unreal Layout Load Batch](assets/unreal_load_layout_batch.png)\n\n### Navigating the project\n\nThe layout will be imported in the directory `/Content/OpenPype`. The layout will be split into two subfolders: \n- *Assets*, which will contain all the rigs and models contained in the layout;\n- *Asset name* (in the following example, *episode 2*), a folder named as the **asset** of the current **task**.\n\n![Unreal Layout Loading Result](assets/unreal_layout_loading_result.png)\n\nIf you chose to generate the level sequences, in the second folder you will find the master level for the task (usually an episode), the level sequence and the folders for all the scenes in the episodes.\nOtherwise you will find the level generated for the loaded layout.\n\n#### Layout without level sequences\n\nIn the layout folder, you will find the level with the imported layout and an object of *AssetContainer* type. The latter is there to mark this directory as Avalon Container (to track changes) and to hold OpenPype metadata.\n\n![Unreal Layout Loading No Sequence](assets/unreal_layout_loading_no_sequence.png)\n\nThe layout level will and should contain only the data included in the layout. To add lighting, or other elements, like an environment, you have to create a master level, and add the layout level as a [streaming level](https://docs.unrealengine.com/5.0/en-US/level-streaming-in-unreal-engine/).\n\nCreate the master level and open it. Then, open the *Levels* window (from the menu **Windows → Levels**). Click on **Levels → Add Existing** and select the layout level and the other levels you with to include in the scene. The following example shows a master level in which have been added a light level and the layout level.\n\n![Unreal Add Level](assets/unreal_add_level.png)\n![Unreal Level List](assets/unreal_level_list_no_sequences.png)\n\n#### Layout with level sequences\n\nIn the episode folder, you will find the master level for the episode, the master level sequence and the folders for all the scenes in the episodes.\n\nAfter opening the master level, open the *Levels* window (from the menu **Windows → Levels**), and you will see the list of the levels of each shot of the episode for which a layout has been loaded.\n\n![Unreal Level List](assets/unreal_level_list.png)\n\nIf it has not been added already, you will need to add the environment to the level. Click on **Levels → Add Existing** and select the level with the environment (check with the studio where it is located).\n\n![Unreal Add Level](assets/unreal_add_level.png)\n\nAfter adding the environment level to the master level, you will need to set it as always loaded by right clicking it, and selecting **Change Streaming Method** and selecting **Always Loaded**.\n\n![Unreal Level Streaming Method](assets/unreal_level_streaming_method.png)\n\n### Update layouts\n\nTo manage loaded layouts, click on the OpenPype icon in Unreal’s main taskbar, and select **Manage**.\n\n![Unreal OP Tools Manage](assets/unreal_openpype_tools_manage.png)\n\nYou will get a list of all the assets that have been loaded in the project.\nThe version number will be in red if it isn’t the latest version. Right click on the element, and select Update if you need to update the layout.\n\n:::note\n**DO NOT** update rigs or models imported with a layout. Update only the layout.\n:::\n\n## Rendering\n\n:::note\nThe rendering requires a layout loaded with the option to create the level sequences **on**.\n:::\n\nTo render and publish an episode, a scene or a shot, you will need to create a publish instance. The publish instance for the rendering is based on one level sequence. That means that if you want to render the whole episode, you will need to create it for the level sequence of the episode, but if you want to render just one shot, you will need to create it for that shot.\n\nNavigate to the folder that contains the level sequence that you need to render. Select the level sequence, and then click on the OpenPype icon in Unreal’s main taskbar, and select **Create**.\n\n![Unreal OP Tools Create](assets/unreal_openpype_tools_create.png)\n\nIn the Instance Creator, select **Unreal - Render**, give it a name, and click **Create**.\n\n![Unreal OP Instance Creator](assets/unreal_create_render.png)\n\nThe render instance will be created in `/Content/OpenPype/PublishInstances`.\n\nSelect the instance you need to render, and then click on the OpenPype icon in Unreal’s main taskbar, and select **Render**. You can render more than one instance at a time, if needed. Just select all the instances that you need to render before selecting the **Render** button from the OpenPype menu.\n\n![Unreal OP Tools Render](assets/unreal_openpype_tools_render.png)\n\nOnce the render is finished, click on the OpenPype icon in Unreal’s main taskbar, and select **Publish**.\n\n![Unreal OP Tools Publish](assets/unreal_openpype_tools_publish.png)\n\nOn the left, you will see the render instances. They will be automatically reorganised to have an instance for each shot. So, for example, if you have created the render instance for the whole episode, here you will have an instance for each shot in the episode.\n\n![Unreal Publish Render](assets/unreal_publish_render.png)\n\nClick on the play button in the bottom right, and it will start the publishing process.\n"
  },
  {
    "path": "website/docs/artist_install.md",
    "content": "---\nid: artist_install\ntitle: Installation \nsidebar_label: Installation\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n\n## Installation\n\nOpenPype comes in packages for Windows (10 or Server), Mac OS X (Mojave or higher), and Linux distribution (Centos, Ubuntu), and you can install them on your machine the same way as you are used to. \n\n:::important\nTo install OpenPype you will need administrator permissions.\n:::\n\n:::note pick your platform\n<Tabs \n    defaultValue='win'\n    values={[\n        {label: 'Windows', value: 'win'},\n        {label: 'Linux', value: 'linux'},\n        {label: 'Mac OS X', value: 'mac'},\n    ]}>\n\n<TabItem value='win'>\n\nFor installation on Windows, download and run the executable file `OpenPype-3.0.0.exe`.\nDuring the installation process, you can change the destination location path of the application, \n\n![Windows installation](assets/install_01.png)\n\nand create an icon on the desktop.\n\n![Windows create icon](assets/install_02.png)\n\n</TabItem>\n\n\n<TabItem value='linux'>\n\nFor installation on your Linux distribution, download and unzip `OpenPype-3.0.0.zip`. A new folder `OpenPype-3.0.0` will be created.\nInside this folder find and run `openpype_gui`,\n\n![Linux launch](assets/install_03.png)\n\n</TabItem>\n\n\n<TabItem value='mac'>\n\nFor installation on Mac OS X, download and run dmg image file `OpenPype-3.0.0.dmg`. \n\nDrag the OpenPype icon into the Application folder.\n\n![Mac installation](assets/install_04.png)\n\nAfter the installation, you can find OpenPype among the other Applications. \n\n</TabItem>\n</Tabs>\n:::\n\n## Run OpenPype\n\nTo run OpenPype click on the icon or find executable file (e.g. `C:\\Program Files (x86)\\OpenPype\\openpype_gui.exe`) in the application location. \nOn the very first run of OpenPype the user will be asked for OpenPype Mongo URL. \nThis piece of information will be provided by the administrator or project manager who set up the studio.\n\n![Mongo example](assets/install_05.png)\n\nOnce the Mongo URL address is entered, press `Start`, and OpenPype will be initiated. \nOpenPype will also remember the connection for the next launch, so it is a one-time process. \n\n:::note\nIf the launch was successful, the artist should see a turquoise OpenPype logo in their\ntray menu. Keep in mind that on Windows this icon might be hidden by default, in which case, the artist can simply drag the icon down to the tray.\n\n![Systray](assets/artist_systray.png)\n:::"
  },
  {
    "path": "website/docs/artist_kitsu.md",
    "content": "---\nid: artist_kitsu\ntitle: Kitsu\nsidebar_label: Kitsu\n---\n\n# How to use Kitsu in OpenPype\n\n## Login to Kitsu module in OpenPype\n1. Launch OpenPype, the `Kitsu Credentials` window will open automatically, if not, or if you want to log-in with another account, go to systray OpenPype icon and click on `Kitsu Connect`.\n2. Enter your credentials and press *Ok*:\n\n    ![kitsu-login](assets/kitsu/kitsu_credentials.png)\n\n:::tip\nIn Kitsu, All the publish actions executed by `pyblish` will be attributed to the currently logged-in user.\n:::"
  },
  {
    "path": "website/docs/artist_publish.md",
    "content": "---\nid: artist_publish\ntitle: Publishing\nsidebar_label: Publishing\n---\n\n## What is publishing?\n\nA process of exporting particular data from your work scene to be shared with others.\n\nThink of publishing as a checkpoint between two people, making sure that we catch mistakes as soon as possible and don’t let them pass through pipeline step that would eventually need to be repeated if these mistakes are not caught.\n\nEvery time you want to share a piece of work with others (be it camera, model, textures, animation or whatever), you need to publish this data. The main reason is to save time down the line and make it very clear what can and cannot be used in production.\nThis process should mostly be handled by publishing scripts but in certain cases might have to be done manually.\n\nPublished assets should comply to these rules:\n\n- Clearly named, based on internal naming conventions.\n- Versioned (with master version created for certain types of assets).\n- Immediately usable, without any dependencies to unpublished assets or work files.\n- Immutable\n\nAll of these go into the publish folder for the given entity (shot, asset, sequence)\n\n:::note\nKeep in mind that while publishing the data might take you some extra time, it will save much more time in the long run when your colleagues don’t need to dig through your work files trying to understand them and find that model you saved by hand.\n:::\n\n## Families:\n\nThe Instances are categorized into ‘families’ based on what type of data they contain. Some instances might have multiple families if needed. A shot camera will for example have families 'camera' and  'review' to indicate that it's going to be used for review quicktime, but also exported into a file on disk.\n\nFollowing family definitions and requirements are OpenPype defaults and what we consider good industry practice, but most of the requirements can be easily altered to suit the studio or project needs.\nHere's a list of supported families\n\n| Family                  | Comment                                               | Example Subsets           |\n|-------------------------|-------------------------------------------------------| ------------------------- |\n| [Model](#model)         | Cleaned geo without materials                         | main, proxy, broken       |\n| [Look](#look)           | Package of shaders, assignments and textures          | main, wet, dirty          |\n| [Rig](#rig)             | Characters or props with animation controls           | main, deform, sim         |\n| [Assembly](#assembly)   | A complex model made from multiple other models.      | main, deform, sim         |\n| [Layout](#layout)       | Simple representation of the environment              | main,                     |\n| [Setdress](#setdress)   | Environment containing only referenced assets         | main,                     |\n| [Camera](#camera)       | May contain trackers or proxy geo, only single camera | main, tracked, anim       |\n|                         | expected.                                             |                           |\n| [Animation](#animation) | Animation exported from a rig.                        | characterA, vehicleB      |\n| [Cache](#cache)         | Arbitrary animated geometry or fx cache               | rest, ROM , pose01        |\n| MayaAscii               | Maya publishes that don't fit other categories        |                           |\n| [Render](#render)       | Rendered frames from CG or Comp                       |                           |\n| RenderSetup             | Scene render settings, AOVs and layers                |                           |\n| Plate                   | Ingested, transcode, conformed footage                | raw, graded, imageplane   |\n| Write                   | Nuke write nodes for rendering                        |                           |\n| Image                   | Any non-plate image to be used by artists             | Reference, ConceptArt     |\n| LayeredImage            | Software agnostic layered image with metadata         | Reference, ConceptArt     |\n| Review                  | Reviewable video or image.                            |                           |\n| Matchmove               | Matchmoved camera, potentially with geometry, allows  | main                      |\n|                         | multiple cameras even with planes.                    |                           |\n| Workfile                | Backup of the workfile with all its content           | uses the task name        |\n| Nukenodes               | Any collection of nuke nodes                          | maskSetup, usefulBackdrop |\n| Yeticache               | Cached out yeti fur setup                             |                           |\n| YetiRig                 | Yeti groom ready to be applied to geometry cache      | main, destroyed           |\n| VrayProxy               | Vray proxy geometry for rendering                     |                           |\n| VrayScene               | Vray full scene export                                |                           |\n| ArnodldStandin          | All arnold .ass archives for rendering                | main, wet, dirty          |\n| LUT                     |                                                       |                           |\n| Nukenodes               |                                                       |                           |\n| Gizmo                   |                                                       |                           |\n| Nukenodes               |                                                       |                           |\n| Harmony.template        |                                                       |                           |\n| Harmony.palette         |                                                       |                           |\n\n\n\n### Model\n\nClean geometry without any material assignments. Published model can be as small as a single mesh, or as complex as a full building. That is purely up to the artist or the supervisor. Models can contain hierarchy defined by groups or nulls for better organisation.\n\nApart from model subsets, we also support LODs as extra level on top of subset. To publish LODs, you just need to prepare subsets for publishing names `modelMySubsetName_LOD##`, if OpenPype finds `_LOD##` (hashes replaced with LOD level), it will automatically be considered a LOD of the given subset.\n\nExample Subsets:\n`modelMain`, `modelProxy`, `modelSculpt`, `modelBroken`, `modelMain_LOD01`, `modelMain_LOD02`\n\nExample representations:\n`.ABC`, `.MA`, `.MB`, `.BLEND`, `.OBJ`, `.FBX`\n\n\n### Look\n\nA package of materials, shaders, assignments, textures and attributes that collectively define a look of a model for rendering or preview purposes. This can usually be applied only to the model is was authored for, or its corresponding cache, however, material sharing across multiple models is also possible. A look should be fully self-contained and ready for rendering.\n\nExample Subsets:\n`lookMain`, `lookProxy`, `lookWet`, `lookDirty`, `lookBlue`, `lookRed`\n\nExample Representations:\n`.MA + .JSON`, `.MTLX (yet unsupported)`, `.BLEND`\n\nPlease note that a look is almost never a single representation, but a combination of multiple.\nFor example in Maya a look consists of `.ma` file with the shaders, `.json` file which\ncontains the attributes and assignments and `/resources` folder with all the required textures.\n\n\n### Rig\n\nCharacters or props with animation controls or other parameters, ready to be referenced into a scene and animated. Animation Rigs tend to be very software specific, but in general they tend to consist of Geometry, Bones or Joints, Controllers and Deformers. OpenPype in maya supports both, self-contained rigs, that include everything in one file, but also rigs that use nested references to bring in geometry, or even skeleton. By default we bake rigs into a single file during publishing, but that behaviour can be turned off to keep the nested references live in the animation scenes.\n\nExample Subsets:\n`rigMain`, `rigMocap`, `rigSim`, `rigCamera`, `rigMuscle`\n\nExample Representations:\n`.MA`, `.MB`, `.BLEND`, `.HDA`\n\n\n### Assembly\n\nA subset created by combining two or more smaller subsets into a composed bigger asset.\nA good example would be a restaurant table asset with the cutlery and chairs included,\nthat will eventually be loaded into a restaurant Set. Instead of loading each individual\nfork and knife for each table in the restaurant, we can first prepare `assemblyRestaurantTable` subset\nwhich will contain the table itself, with cutlery, flowers, plates and chairs nicely arranged.\n\nThis table can then be loaded multiple times into the restaurant for easier scene management\nand updates.\n\nExtracted assembly doesn't contain any geometry directly, but rather information about all the individual subsets that are inside the assembly, their version and transformations. On top of that and alembic is exported which only holds any extra transforms and groups that are needed to fully re-create the original assembled scene.\n\nAssembly ca also be used as a sort of collection of elements that are often used together in the shots. For example if we're set dressing lot's of forest shots, it would make sense to make and assembly of all the forest elements for scattering so we don't have to load them individually into each shot.\n\nExample Subsets:\n`assemblyTable`, `assemblyForestElements`, `assemblyRoof`\n\nExample Representations:\n`.ABC + .JSON`\n\n\n\n### Setdress\n\nFully prepared environment scene assembled from other previously published assets. Setdress should be ready for rendering as is, including any instancing, material assignments and other complex setups the environment requires. Due to this complexity, setdress is currently only publishable in the native file format of the host where it was created. In maya that would be `.ma` or `.mb` file.\n\n\n### Camera\n\nClean virtual camera without any proprietary rigging, or host specific information. Considering how widely across the hosts published cameras are used in production, published camera should ideally be as simple and clean as possible to ensure consistency when loaded into various hosts.\n\n\nExample Representations:\n`.MA`, `.ABC`\n\n\n### Cache\n\nGeometry or effect with baked animation. Cache is usually exported as alembic,\nbut can be potentially any other representation that makes sense in the given scenario.\nCache is defined by the artist directly in the fx or animation scene.\n\nExample Subsets:\n`assemblyTable`, `assemblyForestElements`, `assemblyRoof`\n\nExample Representations:\n`.ABC`, `.VDB`, `.BGEO`\n\n\n### Animation\n\nPublished result of an animation created with a rig. Animation can be extracted\nas animation curves, cached out geometry or even fully animated rig with all the controllers.\nAnimation cache is usually defined by a rigger in the rig file of a character or\nby FX TD in the effects rig, to ensure consistency of outputs.\n\nExample Subsets:\n`animationBob_01`, `animationJack_02`, `animationVehicleA`\n\nExample Representations:\n`.MA`, `.ABC`, `.JSON`\n\n\n### Yeti Cache\n\nCached out yeti fur simulation that originates from a yeti rig applied in the shot context.\n\n\n### Yeti Rig\n\nYeti groom setup ready to be applied to a cached out character in the shot context.\n\n### Render\n"
  },
  {
    "path": "website/docs/artist_tools.md",
    "content": "---\nid: artist_tools\ntitle: Tools\nsidebar_label: Tools\n---\n\n# Tools\n\nOpenPype offers a collection of core tools in tandem with the Integrations:\n\n- [Context Manager](artist_tools_context_manager)\n- [Creator](artist_tools_creator)\n- [Loader](artist_tools_loader)\n- [Library Loader](artist_tools_library_loader)\n- [Publisher](artist_tools_publisher)\n- [Inventory](artist_tools_inventory)\n- [Workfiles](artist_tools_workfiles)\n- [Look Assigner](artist_tools_look_assigner)\n- [Subset Manager](artist_tools_subset_manager)\n- [Sync Queue](artist_tools_sync_queue)\n\n"
  },
  {
    "path": "website/docs/artist_tools_context_manager.md",
    "content": "---\nid: artist_tools_context_manager\ntitle: Context Manager\nsidebar_label: Context Manager\ndescription: A tool to manage the context within a host app.\n---\n\n# Context Manager\n\nAny time your host app is open in a defined context it can be changed to different hierarchy, asset or task within a project. This will allow you to change your opened session to any other asset, shot and tasks within the same project. This is useful particularly in cases where your host takes long time to start.\n\n![workfiles_1](assets/tools_context_manager.png)\n\n\n:::note\nNotice that the window doesn't close after hitting `Accept` and confirming the change of context. This behaviour let's you keep the window open and change the context multiple times in a row.\n:::\n"
  },
  {
    "path": "website/docs/artist_tools_creator.md",
    "content": "---\nid: artist_tools_creator\ntitle: Creator\nsidebar_label: Creator\ndescription: A tool to generate metadata for asset publishing.\n---\n\n# Creator\n\n## Details\n\nDespite the name, Creator isn't for making new content in your scene, but rather taking what's already in it and creating all the metadata your content needs to be published.\n\nIn Maya this means creating a set with everything you want to publish and assigning custom attributes to it so it gets picked up during publishing stage.\n\nIn Nuke it's either converting an existing write node to a publishable one, or simply creating a write node with all the correct settings and outputs already set.\n\n## Usage\n\n1.  Select what you want to publish from your scenes.\n2.  Open *Creator* from OpenPype menu.\n3.  Choose what family (data type) you need to export.\n4.  Type the name for you export. This name is how others are going to be able to refer to this particular subset when loading it into their scenes. Every assets should have a Main subset, but can have any number of other variants.\n5.  Click on *Create*.\n\n"
  },
  {
    "path": "website/docs/artist_tools_inventory.md",
    "content": "---\nid: artist_tools_inventory\ntitle: Inventory\nsidebar_label: Inventory\ndescription: Manage already loaded subsets.\n---\n\n# Inventory\n\nWith Scene Inventory, you can browse, update and change subsets loaded with [Loader](artist_tools_loader) into your scene or script.\n\n:::note\nYou should first understand [Key concepts](artist_concepts) to understand how you can use this tool.\n:::\n\n## Details\n<!-- This part may be in Maya description? -->\n\nOnce a subset is loaded, it turns into a container within a scene. This containerization allows us to have a good overview of everything in the scene, but also makes it possible to change versions, notify user if something is outdated, replace one asset for another, etc.\n<!-- END HERE -->\n\nThe scene manager has a simple GUI focused on efficiency. You can see everything that has been previously loaded into the scene, how many time it's been loaded, what version and a lot of other information. Loaded assets are grouped by their asset name, subset name and representation. This grouping gives ability to apply changes for all instances of the loaded asset *(e.g. when __tree__ is loaded 20 times you can easily update version for all of them)*.\n\n![tools_scene_inventory_10](assets/tools/tools_scene_inventory_10-small.png) <!-- picture needs to be changed -->\n\nTo interact with any container, you need to right click it and you'll see a drop down with possible actions. The key actions for production are already implemented, but more will be added over time.\n\n![tools_scene_inventory_20](assets/tools/tools_scene_inventory_20.png)\n\n## Usage\n\n### Change version\nYou can change versions of loaded subsets with scene inventory tool. Version of loaded assets is colored to red when newer version is available.\n\n\n![tools_scene_inventory_40](assets/tools/tools_scene_inventory_40.png)\n\n#### Update to the latest version\nSelect containers or subsets you want to update, right-click selection and press `Update to latest`.\n\n#### Change to specific version\nSelect containers or subsets you want to change, right-click selection, press `Set version`, select from dropdown version you want change to and press `OK` button to confirm.\n\n\n![tools_scene_inventory_30](assets/tools/tools_scene_inventory_30.png)\n\n\n### Switch Asset\nIt's tool in Scene inventory tool that gives ability to switch asset, subset and representation of loaded assets.\n\n\n![tools_scene_inventory_50](assets/tools/tools_scene_inventory_50.png) <!-- picture needs to be changed -->\n\n\nBecause loaded asset is in fact representation of version published in asset's subset it is possible to switch each of this part *(representation, version, subset and asset)*, but with limitations. Limitations are obvious as you can imagine when you have loaded `.ma` representation of `modelMain` subset from `car` asset it is not possible to switch subset to `modelHD` and keep same representation if `modelHD` does not have published `.ma` representation. It is possible to switch multiple loaded assets at once that makes this tool very powerful helper if all published assets contain same subsets and representations.\n\nSwitch tool won't let you cross the border of limitations and inform you when you have to specify more if impossible combination occurs *(It is also possible that there will be no possible combination for selected assets)*. Border is colored to red and confirm button is not enabled when specification is required.\n\n\n![tools_scene_inventory_55](assets/tools/tools_scene_inventory_55.png) <!-- picture needs to be changed -->\n\n\nPossible switches:\n- switch **representation** (`.ma` to `.abc`, `.exr` to `.dpx`, etc.)\n- switch **subset** (`modelMain` to `modelHD`, etc.)\n    - `AND` keep same **representation** *(with limitations)*\n    - `AND` switch **representation** *(with limitations)*\n- switch **asset** (`oak` to `elm`, etc.)\n    - `AND` keep same **subset** and **representation** *(with limitations)*\n    - `AND` keep same **subset** and switch **representation** *(with limitations)*\n    - `AND` switch **subset** and keep same **representation** *(with limitations)*\n    - `AND` switch **subset** and **representation** *(with limitations)*\n\nWe added one more switch layer above subset for LOD (Level Of Depth). That requires to have published subsets with name ending with **\"_LOD{number}\"** where number represents level (e.g. modelMain_LOD1). Has the same limitations as mentioned above. This is handy when you want to change only subset but keep same LOD or keep same subset but change LOD for multiple assets. This option is hidden if you didn't select subset that have published subset with LODs.\n\n![tools_scene_inventory_54](assets/tools/tools_scene_inventory_54.png) <!-- picture needs to be changed -->\n  \n## Filtering\n\n### Filter by name\n\nThere is a search bar on the top for cases when you have a complex scene with many assets and need to find a specific one.\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\n![tools_scene_inventory_60](assets/tools/tools_scene_inventory_60-small.png)\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![tools_scene_inventory_61](assets/tools/tools_scene_inventory_61-small.png)\n\n</div>\n</div>\n\n\n### Filter with Cherry-pick selection\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\nTo keep only selected subsets right-click selection and press `Cherry-Pick (Hierarchy)` *(Border of subset list change to **orange** color when Cherry-pick filtering is set so you know filter is applied).*\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![tools_scene_inventory_62-small](assets/tools/tools_scene_inventory_62-small.png)\n\n</div>\n</div>\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\nTo return to original state right-click anywhere in subsets list and press `Back to Full-View`.\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![tools_scene_inventory_63-small](assets/tools/tools_scene_inventory_63-small.png)\n\n</div>\n</div>\n\n\n:::tip\nYou can Cherry-pick from Cherry-picked subsets.\n:::\n"
  },
  {
    "path": "website/docs/artist_tools_library_loader.md",
    "content": "---\nid: artist_tools_library_loader\ntitle: Library Loader\nsidebar_label: Library Loader\ndescription: Allows loading published subsets from projects of type \"Library\".\n---\n\n# Library Loader\n\nLibrary loader is extended [loader](artist_tools_loader) which allows to load published subsets from Library projects. Controls are same but library loader has extra Combo Box which allows you to choose project you want to load from.\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\n![tools_library_1](assets/tools/tools_library_1-small.png) <!-- picture needs to be changed -->\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![tools_library_2](assets/tools/tools_library_2-small.png) <!-- picture needs to be changed -->\n\n</div>\n</div>\n\n## Delivery Action\n\nLibrary Loader contains functionality to export any selected asset, subsets and their version to configurable folder.\nDelivery follows structure based on defined template, this template must be configured first by Admin in the Settings.\n\n![delivery_action](assets/tools/tools_delivery_loader.png) \n\n* Usage\n- Select all required subsets for export (you can change theirs versions by double clicking on 'Version' value)\n- Right click and select **Deliver Versions** from context menu\n- Select predefined Delivery template (must be configured by Admin system or project wide)\n- Fill value for root folder (folder will be created if it doesn't exist)\n- Filter out type of representation you are not interested in\n- Push **Deliver** button\n- Dialog must be kept open until export is finished\n- In a case of problems with any of the representation, that one will be skipped, description of error will be provided in the dialog\n\n\n"
  },
  {
    "path": "website/docs/artist_tools_loader.md",
    "content": "---\nid: artist_tools_loader\ntitle: Loader\nsidebar_label: Loader\ndescription: Allows loading published subsets from the same project.\n---\n\n# Loader\nLoader loads published subsets into your current scene or script.\n\n## Usage\n1. Open *Loader* from OpenPype menu.\n2. Select the asset where the subset you want to load is published.\n3. From subset list select the subset you want.\n4. Right-click the subset.\n5. From action menu select what you want to do *(load, reference, ...)*.\n\n\n![tools_loader_1](assets/tools/tools_loader_1.png) <!-- picture needs to be changed -->\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\n## Refresh data\nData are not auto-refreshed to avoid database issues. To refresh assets or subsets press refresh button.\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![tools_loader_50](assets/tools/tools_loader_50.png)\n\n</div>\n</div>\n\n## Load another version\nLoader by default load last version, but you can of course load another versions. Double-click on the subset in the version column to expose the drop down, choose version you want to load and continue from point 4 of the [Usage](#usage-1).\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\n  ![tools_loader_21](assets/tools/tools_loader_21.png)\n</div>\n<div class=\"col col--6 markdown\">\n\n  ![tools_loader_22](assets/tools/tools_loader_22.png)\n</div>\n</div>\n\n\n## Filtering\n\n### Filter Assets and Subsets by name\nTo filter assets/subsets by name just type name or part of name to filter text input. Only assets/subsets containing the entered string remain.\n\n- **Assets filtering example** *(it works the same for subsets)*:\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\n![tools_loader_4](assets/tools/tools_loader_4-small.png)\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![tools_loader_5](assets/tools/tools_loader_5-small.png)\n\n</div>\n</div>\n\n\n### Filter Subsets by Family\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\nTo filter [subsets](artist_concepts.md#subset) by their [families](artist_publish.md#families) you can use families list where you can check families you want to see or uncheck families you are not interested in.\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![tools_loader_30](assets/tools/tools_loader_30-small.png)\n\n</div>\n</div>\n\n\n\n## Subset groups\nSubsets may be grouped which can help to make the subset list more transparent. You can toggle visibility of groups with `Enable Grouping` checkbox.\n\n![tools_loader_40](assets/tools/tools_loader_40-small.png)\n\n\n### Add to group or change current group\nYou can set group of selected subsets with shortcut `Ctrl + G`.\n\n![tools_loader_41](assets/tools/tools_loader_41-small.png)\n\n\n:::warning\nYou'll set the group in Avalon database so your changes will take effect for all users.\n:::\n\n## Site Sync support\n\nIf **Site Sync** is enabled additional widget is shown in right bottom corner.\nIt contains list of all representations of selected version(s). It also shows availability of representation files\non particular site (*active* - mine, *remote* - theirs). \n\n![site_sync_support](assets/site_sync_loader.png)\n\nOn this picture you see that representation files are available only on remote site (could be GDrive or other). \nIf artist wants to work with the file(s) they need to be downloaded first. That could be done by right mouse click on\nparticular representation (or multiselect all) and select *Download*.\n\nThis will mark representation to be download which will happen in the background if OpenPype Tray is running.\n\nFor more details of progress, state or possible error details artist should open **[Sync Queue](#Sync-Queue)** item in Tray app.\n\nWork in progress...\n\n"
  },
  {
    "path": "website/docs/artist_tools_look_assigner.md",
    "content": "---\nid: artist_tools_look_assigner\ntitle: Look Assigner\nsidebar_label: Look Assigner\ndescription: Manage published looks to their respective model(s).\n---\n\n# Look Assigner\n\nThe Look Manager takes care of assigning published looks to the correct model in the scene.\n\n## Details\n\nWhen a look is published it also stores the information about what shading networks need to be assigned to which models, but it also stores all the render attributes on the mesh necessary for a successful render.\n\n## Usage\n\nLook Assigner has GUI is made of two parts. On the left you will see the list of all the available models in the scene and on the right side, all the looks that can be associate with them. To assign a look to a model you just need to:\n\n1.  Click on \"load all subsets\".\n2.  Choose a subset from the menu on the left.\n3.  Right click on a look from the list on the right.\n4.  Choose \"Assign\".\n\nAt this point you should have a model with all it's shaders applied correctly. The tool automatically loads the latest look available.\n\n"
  },
  {
    "path": "website/docs/artist_tools_publisher.md",
    "content": "---\nid: artist_tools_publisher\ntitle: Publisher\nsidebar_label: Publisher\ndescription: Publish versioned work progress into the project.\n---\n\n# Publisher\n\nUse publish to share your work with others. It collects, validates and exports the data in standardized way.\n\n## Details\n\nWhen you run pyblish, the UI is made of 2 main parts. On the left, you see all the items pyblish will be working with (called instances), and on the right a list of actions that are going to process these items.\nEven though every task type has some pre-defined settings of what should be collected from the scene and what items will be published by default. You can technically publish any output type from any task type.\nEach item is passed through multiple plugins, each doing a small piece of work. These are organized into 4 areas and run in sequence.\n\n## Using Pyblish\n\nIn the best case scenario, you open pyblish from the Avalon menu, press play, wait for it to finish, and you’re done.\nThese are the steps in detail, for cases, where the default settings don’t work for you or you know that the task you’re working on, requires a different treatment.\n\n### Collect\n\nFinds all the important data in the scene and makes it ready for publishing\n\n### Validate\n\nEach validator makes sure your output complies to one particular condition. This could be anything from naming conventions, scene setting, to plugin usage. An item can only be published if all validators pass.\n\n### Extract\n\nExtractor takes the item and saves it to the disk. Usually to temporary location. Each extractor represents one file format and there can be multiple file formats exported for each item.\n\n### Integrate\n\nIntegrator takes the extracted files, categorizes and moves them to a correct location on the disk or on the server.\n\n"
  },
  {
    "path": "website/docs/artist_tools_subset_manager.md",
    "content": "---\nid: artist_tools_subset_manager\ntitle: Subset Manager\nsidebar_label: Subset Manager\ndescription: Manage all the publish-able elements.\n---\n\n# Subset Manager\n\nSubset Manager lists all items which are meant for publishig and will be published if Publish is triggered\n\n## Details\n\nOne or more items (instances) could be published any time Publish process is started. Each this publishable\nitem must be created by Creator tool previously. Subset Manager provides easy way how to check which items, \nand how many, will be published. \n\nIt also provides clean and preferable way how to remove unwanted item from publishing.\n\n## Usage\n\nSubset Manager has GUI is made of two parts. On the left you will see the list of all the available publishable items in the scene and on the right side, details about these items.\n\n<div class=\"col col--6 markdown\">\n\n![subset_manager](assets/tools_subset_manager.png)\n</div>\n\nAny time new item is Created, it will show up here.\n\nCurrently there is only single action, 'Remove instance' which cleans workfile file from publishable item metadata.\nThis might not remove underlying host item, it depends on host and implementation!\n\nIt might also happen that user deletes underlying host item(for example layer in Photoshop) directly in the host, but metadata will stay.\nThis could result in phantom issues during publishing. Use Subset Manager to purge workfile from abandoned items.\n\nPlease check behaviour in host of your choice.\n\n"
  },
  {
    "path": "website/docs/artist_tools_sync_queu.md",
    "content": "---\nid: artist_tools_sync_queue\ntitle: Sync Queue\nsidebar_label: Sync Queue\ndescription: Track sites synchronization progress.\n---\n\n# Sync Queue\n\n## Details\n\nIf **Site Sync** is configured for a project, each asset is marked to be synchronized to a remote site during publishing.\nEach artist's OpenPype Tray application handles synchronization in background, it looks for all representation which \nare marked with the site of the user (unique site name per artist) and remote site.\n\nArtists then can see progress of synchronization via **Sync Queue** link in the Tray application.\n\nArtists can see all synced representation in this dialog with helpful information such as when representation was created, when it was synched,\nstatus of synchronization (OK or Fail) etc.\n\n## Usage\n\nWith this app artists can modify synchronized representation, for example mark failed representation for re-sync etc.\n\n![Sync Queue](assets/site_sync_sync_queue.png)\n\nActions accessible by context menu on single (or multiple representations):\n- *Open in Explorer* - if site is locally accessible, open folder with it with OS based explorer\n- *Re-sync Active Site* - mark artist own side for re-download (repre must be accessible on remote side)\n- *Re-sync Remote Site* - mark representation for re-upload\n- *Completely remove from local* - removes tag of synchronization to artist's local site, removes files from disk (available only for personal sites)\n- *Change priority* - mark representations with higher priority for faster synchronization run\n\nDouble click on any of the representation open Detail dialog with information about all files for particular representation.\nIn this dialog error details could be accessed in the context menu.\n\n#### Context menu on project name\nArtists can also Pause whole server or specific project for synchronization. In that state no download/upload is being run.\nThis might be helpful if the artist is not interested in a particular project for a while or wants to save bandwidth data limit for a bit.\n\nAnother option is `Validate files on active site`. This option triggers process where all representation of the selected project are looped through, file paths are resolved for active site and\nif paths point to local system, paths are physically checked if files are existing. If file exists and representation is not marked to be present on 'active_site' in DB, DB is updated \nto follow that. \n\nThis might be useful if artist has representation files that Site Sync doesn't know about (newly attached external drive with representations from studio).\nThis project might take a while!\n"
  },
  {
    "path": "website/docs/artist_tools_workfiles.md",
    "content": "---\nid: artist_tools_workfiles\ntitle: Workfiles\nsidebar_label: Workfiles\ndescription: Save versioned progress files.\n---\n\n# Workfiles\n\nSave new working scenes or scripts, or open the ones you previously worked on.\n\n## Details\n\nInstead of digging through your software native file browser, you can simply open the workfiles app and see all the files for the asset or shot you're currently working with. The app takes care of all the naming and the location of your work files.\n\nWhen saving a scene you can also add a comment. It is completely up to you how you use this, however we recommend using it for subversion within your current working version.\n\nLet's say that the last version of the comp you published was v003 and now you're working on the file prj_sh010_compositing_v004.nk if you want to keep snapshots of your work, but not iterate on the main version because the supervisor is expecting next publish to be v004, you can use the comment to do this, so you can save the file under the name prj_sh010_compositing_v004_001 , prj_sh010_compositing_v004_002. the main version is automatically iterated every time you publish something.\n\n## Usage\n\n<div class=\"row markdown\">\n<div class=\"col col--6 markdown\">\n\n### To open existing file:\n\n1. Open Workfiles tool from OpenPype menu\n2. Select file from list - the latest version is the highest *(descendent ordering)*\n3. Press `Open` button\n\n</div>\n<div class=\"col col--6 markdown\">\n\n![workfiles_1](assets/workfiles_1.png)\n\n</div>\n</div>\n\n\n### To save new workfile\n1. Open Workfiles tool from OpenPype menu\n2. Press `Save As` button\n3. You can add optional comment to the filename, that will be appended at the end\n4. Press `OK`\n\n:::note\nYou can manually override the workfile version by unticking next available version and using the version menu to choose your own.\n:::\n\n"
  },
  {
    "path": "website/docs/artist_work.md",
    "content": "---\nid: artist_work\ntitle: Working on tasks\nsidebar_label: Working\n---\n\nCheck the [documentation](https://docusaurus.io) for how to use Docusaurus.\n\n## Lorem\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus elementum massa eget nulla aliquet sagittis. Proin odio tortor, vulputate ut odio in, ultrices ultricies augue. Cras ornare ultrices lorem malesuada iaculis. Etiam sit amet libero tempor, pulvinar mauris sed, sollicitudin sapien.\n\n## Mauris In Code\n\n    Mauris vestibulum ullamcorper nibh, ut semper purus pulvinar ut. Donec volutpat orci sit amet mauris malesuada, non pulvinar augue aliquam. Vestibulum ultricies at urna ut suscipit. Morbi iaculis, erat at imperdiet semper, ipsum nulla sodales erat, eget tincidunt justo dui quis justo. Pellentesque dictum bibendum diam at aliquet. Sed pulvinar, dolor quis finibus ornare, eros odio facilisis erat, eu rhoncus nunc dui sed ex. Nunc gravida dui massa, sed ornare arcu tincidunt sit amet. Maecenas efficitur sapien neque, a laoreet libero feugiat ut.\n\n## Nulla\n\nNulla facilisi. Maecenas sodales nec purus eget posuere. Sed sapien quam, pretium a risus in, porttitor dapibus erat. Sed sit amet fringilla ipsum, eget iaculis augue. Integer sollicitudin tortor quis ultricies aliquam. Suspendisse fringilla nunc in tellus cursus, at placerat tellus scelerisque. Sed tempus elit a sollicitudin rhoncus. Nulla facilisi. Morbi nec dolor dolor. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras et aliquet lectus. Pellentesque sit amet eros nisi. Quisque ac sapien in sapien congue accumsan. Nullam in posuere ante. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin lacinia leo a nibh fringilla pharetra.\n\n## Orci\n\nOrci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin venenatis lectus dui, vel ultrices ante bibendum hendrerit. Aenean egestas feugiat dui id hendrerit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Curabitur in tellus laoreet, eleifend nunc id, viverra leo. Proin vulputate non dolor vel vulputate. Curabitur pretium lobortis felis, sit amet finibus lorem suscipit ut. Sed non mollis risus. Duis sagittis, mi in euismod tincidunt, nunc mauris vestibulum urna, at euismod est elit quis erat. Phasellus accumsan vitae neque eu placerat. In elementum arcu nec tellus imperdiet, eget maximus nulla sodales. Curabitur eu sapien eget nisl sodales fermentum.\n"
  },
  {
    "path": "website/docs/dev_blender.md",
    "content": "---\nid: dev_blender\ntitle: Blender integration\nsidebar_label: Blender integration\ntoc_max_heading_level: 4\n---\n\n## Run python script at launch\nIn case you need to execute a python script when Blender is started (aka [`-P`](https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html#python-options)), for example to programmatically modify a blender file for conformation, you can create an OpenPype hook as follows:\n\n```python\nfrom openpype.hosts.blender.hooks import pre_add_run_python_script_arg\nfrom openpype.lib import PreLaunchHook\n\n\nclass MyHook(PreLaunchHook):\n    \"\"\"Add python script to be executed before Blender launch.\"\"\"\n\n    order = pre_add_run_python_script_arg.AddPythonScriptToLaunchArgs.order - 1\n    app_groups = [\n        \"blender\",\n    ]\n\n    def execute(self):\n        self.launch_context.data.setdefault(\"python_scripts\", []).append(\n            \"/path/to/my_script.py\"\n        )\n```\n\nYou can write a bare python script, as you could run into the [Text Editor](https://docs.blender.org/manual/en/latest/editors/text_editor.html).\n\n### Python script with arguments\n#### Adding arguments\nIn case you need to pass arguments to your script, you can append them to `self.launch_context.data[\"script_args\"]`:\n\n```python\nself.launch_context.data.setdefault(\"script_args\", []).append(\n        \"--my-arg\",\n        \"value\",\n    )\n```\n\n#### Parsing arguments\nYou can parse arguments in your script using [argparse](https://docs.python.org/3/library/argparse.html) as follows:\n\n```python\nimport argparse\n\nparser = argparse.ArgumentParser(\n    description=\"Parsing arguments for my_script.py\"\n)\nparser.add_argument(\n    \"--my-arg\",\n    nargs=\"?\",\n    help=\"My argument\",\n)\nargs, unknown = arg_parser.parse_known_args(\n    sys.argv[sys.argv.index(\"--\") + 1 :]\n)\nprint(args.my_arg)\n```\n"
  },
  {
    "path": "website/docs/dev_build.md",
    "content": "---\nid: dev_build\ntitle: Build OpenPYPE from source\nsidebar_label: Build\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## Introduction\n\nTo build Pype you currently need (on all platforms):\n\n- **[Python 3.9](https://www.python.org/downloads/)** as we are following [vfx platform CY2022](https://vfxplatform.com).\n- **[git](https://git-scm.com/downloads)**\n\nWe use [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest) to freeze the code and all dependencies and\n[Poetry](https://python-poetry.org/) for virtual environment management.\n\nThis is outline of build steps. Most of them are done automatically via scripts:\n- Virtual environment is created using **Poetry** in `.venv`\n- Necessary python modules outside of `.venv` are stored to `./vendor/python` (like `PySide2`)\n- Necessary third-party tools (like [ffmpeg](https://www.ffmpeg.org/), [OpenImageIO](https://github.com/OpenImageIO/oiio)\n  and [usd libraries](https://developer.nvidia.com/usd)) are downloaded to `./vendor/bin`\n- OpenPype code is frozen with **cx_freeze** to `./build`\n- Modules are moved from `lib` to `dependencies` to solve some Python 2 / Python 3 clashes\n- On Mac application bundle and dmg image will be created from built code.\n- On Windows, you can create executable installer with `./tools/build_win_installer.ps1`\n\n### Clone OpenPype repository:\n```powershell\ngit clone --recurse-submodules https://github.com/pypeclub/OpenPype.git\n```\n\n## Platform specific steps\n\n<Tabs\n  groupId=\"platforms\"\n  defaultValue=\"win\"\n  values={[\n    {label: 'Windows', value: 'win'},\n    {label: 'Linux', value: 'linux'},\n    {label: 'Mac', value: 'mac'},\n  ]}>\n\n<TabItem value=\"win\">\n\n### Windows\nMore tools might be needed for installing some dependencies (for example for **OpenTimelineIO**) - mostly\ndevelopment tools like [CMake](https://cmake.org/) and [Visual Studio](https://visualstudio.microsoft.com/cs/downloads/)\n\n#### Run from source\n\nFor development purposes it is possible to run OpenPype directly from the source. We provide a simple launcher script for this. To run the powershell scripts you may have to enable unrestricted execution as administrator:\n\n`Set-ExecutionPolicy -ExecutionPolicy unrestricted`\n\nTo start OpenPype from source you need to \n\n1. Run `.\\tools\\create_env.ps1` to create virtual environment in `.venv`\n2. Run `.\\tools\\fetch_thirdparty_libs.ps1` to get **PySide2**, **ffmpeg**, **oiio** and other tools needed.\n3. Run `.\\tools\\run_tray.ps1` if you have all required dependencies on your machine you should be greeted with OpenPype igniter window and once you give it your Mongo URL, with OpenPype icon in the system tray.\n\nStep 1 and 2 needs to be run only once (or when something was changed).\n\n#### To build OpenPype:\n1. Run `.\\tools\\create_env.ps1` to create virtual environment in `.venv`\n2. Run `.\\tools\\fetch_thirdparty_libs.ps1` to get **PySide2**, **ffmpeg**, **oiio** and other tools needed.\n3. `.\\tools\\build.ps1` to build OpenPype to `.\\build`\n\n\nTo create distributable OpenPype versions, run `.\\tools\\create_zip.ps1` - that will\ncreate zip file with name `pype-vx.x.x.zip` parsed from current pype repository and\ncopy it to user data dir. You can specify `--path \\path\\to\\zip` to force it into a different \nlocation. This can be used to prepare new version releases for artists in the studio environment\nwithout the need to re-build the whole package\n\n\n\n</TabItem>\n<TabItem value=\"linux\">\n\n### Linux\n\n#### Docker\nYou can use Docker to build OpenPype. Just run:\n```shell\n$ sudo ./tools/docker_build.sh\n```\n\nThis will by default use Debian as base image. If you need to make Centos 7 compatible build, please run:\n\n```sh\nsudo ./tools/docker_build.sh centos7\n```\n\nand you should have built OpenPype in `build` directory. It is using **Centos 7**\nas a base image.\n\nYou can pull the image:\n\n```shell\n# replace 3.0.0 tag with version you want\n$ docker pull pypeclub/openpype:3.0.0\n```\nSee https://hub.docker.com/r/pypeclub/openpype/tag for more.\n\nBeware that as Python is built against some libraries version in Centos 7 base image,\nthose might not be available in linux version you are using. We try to handle those we\nfound (libffi, libcrypto/ssl, etc.) but there might be more.\n\n#### Manual build\n\nTo build OpenPype on Linux you will need:\n\n- **[curl](https://curl.se)** on systems that doesn't have one preinstalled.\n- **bzip2**, **readline**, **sqlite3** and other libraries.\n\nBecause some Linux distros come with older Python version pre-installed, you might \nneed to install **3.9** version and make use of it explicitly. \nYour best bet is probably using [pyenv](https://github.com/pyenv/pyenv).\n\nYou can use your package manager to install **git** and other packages to your build\nenvironment.\n\n#### Common steps for all Distros\n\nUse pyenv to prepare Python version for Pype build\n\n```shell\n$ curl https://pyenv.run | bash\n\n# you can add those to ~/.bashrc\n$ export PATH=\"$HOME/.pyenv/bin:$PATH\"\n$ eval \"$(pyenv init -)\"\n$ eval \"$(pyenv virtualenv-init -)\"\n\n# reload shell\n$ exec $SHELL\n\n# install Python 3.9.6\n# python will be downloaded and build so please make sure\n# you have all necessary requirements installed (see below).\n$ pyenv install -v 3.9.6\n\n# change path to pype 3\n$ cd /path/to/pype-3\n\n# set local python version\n$ pyenv local 3.9.6\n```\n:::note Install build requirements for **Ubuntu**\n\n```shell\nsudo apt-get update; sudo apt-get install --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git patchelf\n```\n\nIn case you run in error about `xcb` when running Pype,\nyou'll need also additional libraries for Qt5:\n\n```shell\nsudo apt install qt5-default\n```\n:::\n\n:::note Install build requirements for **Centos 7**\n\n```shell\n$ sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm\n$ sudo yum install centos-release-scl\n$ sudo yum install bash which git devtoolset-7-gcc* \\\n        make cmake curl wget gcc zlib-devel bzip2 \\\n        bzip2-devel readline-devel sqlite sqlite-devel \\\n        openssl-devel tk-devel libffi-devel qt5-qtbase-devel \\\n        patchelf\n```\n:::\n\n:::note Install build requirements for other distros\n\nBuild process usually needs some reasonably recent versions of libraries and tools. You\ncan follow what's needed for Ubuntu and change it for your package manager. Centos 7 steps\nhave additional magic to overcame very old versions.\n:::\n\nFor more information about setting your build environment please refer to [pyenv suggested build environment](https://github.com/pyenv/pyenv/wiki#suggested-build-environment).\n\n\n#### To build Pype:\n1. Run `./tools/create_env.sh` to create virtual environment in `./venv`\n2. Run `./tools/fetch_thirdparty_libs.sh` to get **PySide2**, **ffmpeg**, **oiio** and other tools needed.\n3. Run `./tools/build.sh` to build pype executables in `.\\build\\`\n\n</TabItem>\n<TabItem value=\"mac\">\n\n### MacOS\nTo build pype on MacOS you will need:\n\n- **[Homebrew](https://brew.sh)** - easy way of installing everything necessary.\n- **[CMake](https://cmake.org/)** to build some external OpenPype dependencies.\n- **XCode Command Line Tools** (or some other build system)\n- **[create-dmg](https://formulae.brew.sh/formula/create-dmg)** to create dmg image from application\nbundle.\n\n1) Install **Homebrew**:\n```shell\n$ /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n```\n\n2) Install **cmake**:\n```shell\n$ brew install cmake\n```\n\n3) Install [pyenv](https://github.com/pyenv/pyenv):\n```shell\n$ brew install pyenv\n$ echo 'eval \"$(pyenv init -)\"' >> ~/.zshrc\n$ pyenv init\n$ exec \"$SHELL\"\n$ PATH=$(pyenv root)/shims:$PATH\n```\n\n4) Pull in required Python version 3.9.x\n```shell\n# install Python build dependences\n$ brew install openssl readline sqlite3 xz zlib\n\n# replace with up-to-date 3.9.x version\n$ pyenv install 3.9.6\n```\n\n5) Set local Python version\n```shell\n# switch to Pype source directory\n$ pyenv local 3.9.6\n```\n\n6) Install `create-dmg`\n```shell\n$ brew install create-dmg\n```\n\n#### To build Pype:\n\n1. Run `./tools/create_env.sh` to create virtual environment in `./venv`.\n2. Run `./tools/fetch_thirdparty_libs.sh` to get **ffmpeg**, **oiio** and other tools needed.\n3. Run `./tools/build.sh` to build OpenPype Application bundle in `./build/`.\n\n</TabItem>\n</Tabs>\n\n## Adding dependencies\n### Python modules\nIf you are extending OpenPype and you need some new modules not included, you can add them\nto `pyproject.toml` to `[tool.poetry.dependencies]` section.\n\n```toml title=\"/pyproject.toml\"\n[tool.poetry.dependencies]\npython = \"3.9.*\"\naiohttp = \"^3.7\"\naiohttp_json_rpc = \"*\" # TVPaint server\nacre = { git = \"https://github.com/pypeclub/acre.git\" }\nopentimelineio = { version = \"0.14.0.dev1\", source = \"openpype\" }\n#...\n```\nIt is useful to add comment to it so others can see why this was added and where it is used.\nAs you can see you can add git repositories or custom wheels (those must be\nadded to `[[tool.poetry.source]]` section).\n\nTo add something only for specific platform, you can use markers like:\n```toml title=\"Install pywin32 only on Windows\"\npywin32 = { version = \"300\", markers = \"sys_platform == 'win32'\" }\n```\n\nFor more information see [Poetry documentation](https://python-poetry.org/docs/dependency-specification/).\n\n### Python modules as thirdparty\nThere are some python modules that can be available only in OpenPype and should not be propagated to any subprocess.\nBest example is **PySide2** which is required to run OpenPype but can be used only in OpenPype and should not be in PYTHONPATH for most of host applications.\nWe've decided to separate these breaking dependencies to be able run OpenPype from code and from build the same way.\n\n:::warning\n**PySide2** has handled special cases related to it's build process.\n### Linux\n- We're fixing rpath of shared objects on linux which is modified during cx freeze processing.\n### MacOS\n- **QtSql** libraries are removed on MacOS because their dependencies are not available and would require to modify rpath of Postgre library.\n:::\n\n### Binary dependencies\nTo add some binary tool or something that doesn't fit standard Python distribution methods, you\ncan use [fetch_thirdparty_libs](#fetch_thirdparty_libs) script. It will take things defined in\n`pyproject.toml` under `[openpype]` section like this:\n\n```toml title=\"/pyproject.toml\"\n[openpype]\n\n[openpype.thirdparty.ffmpeg.windows]\nurl = \"https://distribute.openpype.io/thirdparty/ffmpeg-4.4-windows.zip\"\nhash = \"dd51ba29d64ee238e7c4c3c7301b19754c3f0ee2e2a729c20a0e2789e72db925\"\n# ...\n```\nThis defines FFMpeg for Windows. It will be downloaded from specified url, its checksum will\nbe validated (it's sha256) and it will be extracted to `/vendor/bin/ffmpeg/windows` (partly taken\nfrom its section name).\n\n## Script tools\n(replace extension with the one for your system - `ps1` for windows, `sh` for linux/macos)\n\n### build\nThis will build OpenPype to `build` directory. If virtual environment is not created yet, it will\ninstall [Poetry](https://python-poetry.org/) and using it download and install necessary\npackages needed for build. It is recommended that you run [fetch_thirdparty_libs](#fetch_thirdparty_libs)\nto download FFMpeg, OpenImageIO and others that are needed by OpenPype and are copied during the build.\n\n#### Arguments\n`--no-submodule-update` - to disable updating submodules. This allows to make custom-builds for testing\nfeature changes in submodules.\n\n### build_win_installer\nThis will take already existing build in `build` directory and create executable installer using\n[Inno Setup](https://jrsoftware.org/isinfo.php) and definitions in `./inno_setup.iss`. You need OpenPype\nbuild using [build script](#build), Inno Setup installed and in PATH before running this script.\n\n:::note\nWindows only\n:::\n\n### create_env\nScript to create virtual environment for build and running OpenPype from sources. It is using\n[Poetry](https://python-poetry.org/). All dependencies are defined in `pyproject.toml`, resolved by\nPoetry into `poetry.lock` file and then installed. Running this script without Poetry will download\nit, install it to `.poetry` and then install virtual environment from `poetry.lock` file. If you want\nto update packages version, just run `poetry update` or delete lock file.\n\n#### Arguments\n`--verbose` - to increase verbosity of Poetry. This can be useful for debugging package conflicts.\n\n### create_zip\nScript to create packaged OpenPype version from current sources. This will strip developer stuff and\npackage it into zip that can be used for [auto-updates for studio wide distributions](admin_distribute.md#automatic-updates), etc.\nSame as:\n```shell\npoetry run python ./tools/create_zip.py\n```\n\n### docker_build.sh *[variant]*\nScript to build OpenPype on [Docker](https://www.docker.com/) enabled systems - usually Linux and Windows\nwith [Docker Desktop](https://docs.docker.com/docker-for-windows/install/)\nand [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL) installed.\n\nIt must be run with administrative privileges - `sudo ./docker_build.sh`.\n\nIt will use latest **Debian** base image to build OpenPype. If you need to build OpenPype for\nolder systems like Centos 7, use `centos7` as argument. This will use another Dockerfile to build\nOpenPype with **Centos 7** as base image.\n\nYou'll see your build in `./build` folder.\n\n### fetch_thirdparty_libs\nThis script will download necessary tools for OpenPype defined in `pyproject.toml` like FFMpeg,\nOpenImageIO and USD libraries and put them to `./vendor/bin`. Those are then included in build.\nRunning it will overwrite everything on their respective paths.\nSame as:\n```shell\npoetry run python ./tools/fetch_thirdparty_libs.py\n```\n\n### make_docs\nScript will run [sphinx](https://www.sphinx-doc.org/) to build api documentation in html. You\nshould see it then under `./docs/build/html`.\n\n### run_documentation\nThis will start up [Docusaurus](https://docusaurus.io/) to display OpenPype user documentation.\nUseful for offline browsing or editing documentation itself. You will need [Node.js](https://nodejs.org/)\nand [Yarn](https://yarnpkg.com/) to run this script. After executing it, you'll see new\nbrowser window with current OpenPype documentation.\nSame as:\n```shell\ncd ./website\nyarn start\n```\n\n### run_mongo\nHelper script to run local mongoDB server for development and testing. You will need\n[mongoDB server](https://www.mongodb.com/try/download/community) installed in standard location\nor in PATH (standard location works only on Windows). It will start by default on port `2707` and\nit will put its db files to `../mongo_db_data` relative to OpenPype sources.\n\n### run_project_manager\nHelper script to start OpenPype Project Manager tool.\nSame as:\n```shell\npoetry run python start.py projectmanager\n```\n\n### run_settings\nHelper script to open OpenPype Settings UI.\nSame as:\n```shell\npoetry run python start.py settings --dev\n```\n\n### run_tests\nRuns OpenPype test suite.\n\n### run_tray\nHelper script to run OpenPype Tray.\nSame as:\n```shell\npoetry run python start.py tray\n```\n\n### update_submodules\nHelper script to update OpenPype git submodules.\nSame as:\n```shell\ngit submodule update --recursive --remote\n```\n"
  },
  {
    "path": "website/docs/dev_colorspace.md",
    "content": "---\nid: dev_colorspace\ntitle: Colorspace Management and Distribution\nsidebar_label: Colorspace\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## Introduction\nDefines the distribution of colors and OCIO config during publishing. Once colorspace data are captured and integrated into representation loaders could use them for loading image and video data with correct colorspace.\n\n:::warning Color Management (ImageIO)\nAdding the `imagio` settings schema is required for any host or module which is processing pixel data.\n:::\n\n## Data model\nPublished representations that are extracted with color managed data store a **colorspaceData** entry in its data: `representation_doc[\"data\"][\"colorspaceData\"]`.\n\nIt's up to the Host implementation to pre-configure the application or workfile to have the correct OCIO config applied.\nIt's up to the Extractors to set these values for the representation during publishing.\nIt's up to the Loaders to read these values and apply the correct expected color space.\n\n### Keys\n- **colorspace** - string value used in other publish plugins and loaders\n- **config** - storing two versions of path.\n  - **path** - is formatted and with baked platform root. It is used for possible need to find out where we were sourcing color config during publishing.\n  - **template** - unformatted template resolved from settings. It is used for other plugins targeted to remote publish which could be processed at different platform.\n\n### Example\n    {\n        \"colorspace\": \"linear\",\n        \"config\": {\n            \"path\": \"/abs/path/to/config.ocio\",\n            \"template\": \"{project[root]}/path/to/config.ocio\"\n        }\n    }\n\n\n## How to integrate it into a host\n1. The settings for a host should add the `imagio` schema. Ideally near the top of all categories in its `/settings/entities/schemas/system_scheams/host_settings/schema_{host}.json` so it matches the settings layout other hosts.\n```json\n{\n    \"key\": \"imageio\",\n    \"type\": \"dict\",\n    \"label\": \"Color Management (ImageIO)\",\n    \"is_group\": true,\n    \"children\": [\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_imageio_config\"\n        },\n        {\n            \"type\": \"schema\",\n            \"name\": \"schema_imageio_file_rules\"\n        }\n\n    ]\n}\n```\n\n2. Set the OCIO config path for the host to the path returned from `openpype.pipeline.colorspace.get_imageio_config`, for example:\n\t- set the `OCIO` environment variable before launching the host via a prelaunch hook\n\t- or (if the host allows) to set the workfile OCIO config path using the host's API\n\n3. Each Extractor exporting pixel data (e.g. image or video) has to inherit from the mixin class `openpype.pipeline.publish.publish_plugins.ColormanagedPyblishPluginMixin` and use `self.set_representation_colorspace` on the representations to be integrated.\n\nThe **set_representation_colorspace** method adds `colorspaceData` to the representation. If the `colorspace` passed is not `None` then it is added directly to the representation with resolved config path otherwise a color space is assumed using the configured file rules. If no file rule matches the `colorspaceData` is **not** added to the representation.\n\nAn example implementation can be found here: `openpype\\hosts\\nuke\\plugins\\publish\\extract_render_local.py`\n\n\n4. The Loader plug-ins should take into account the `colorspaceData` in the published representation's data to allow the DCC to read in the expected color space.\n```python\nfrom openpype.pipeline.colorspace import (\n    get_imageio_colorspace_from_filepath,\n    get_imageio_config,\n    get_imageio_file_rules\n)\n\nclass YourLoader(api.Loader):\n  def load(self, context, name=None, namespace=None, options=None):\n    path = self.filepath_from_context(context)\n    colorspace_data = context[\"representation\"][\"data\"].get(\"colorspaceData\", {})\n    colorspace = (\n      colorspace_data.get(\"colorspace\")\n      # try to match colorspace from file rules\n      or self.get_colorspace_from_file_rules(path, context)\n    )\n\n    # pseudocode\n    load_file(path, colorspace=colorspace)\n\n  def get_colorspace_from_file_rules(self, path, context)\n    project_name = context.data[\"projectName\"]\n    host_name = context.data[\"hostName\"]\n    anatomy_data = context.data[\"anatomyData\"]\n    project_settings_ = context.data[\"project_settings\"]\n\n    config_data = get_imageio_config(\n        project_name, host_name,\n        project_settings=project_settings_,\n        anatomy_data=anatomy_data\n    )\n    file_rules = get_imageio_file_rules(\n        project_name, host_name,\n        project_settings=project_settings_\n    )\n    # get matching colorspace from rules\n    colorspace = get_imageio_colorspace_from_filepath(\n      path, host_name, project_name,\n      config_data=config_data,\n      file_rules=file_rules,\n      project_settings=project_settings\n    )\n```\n\n:::warning Loading\nA custom OCIO config can be set per asset/shot and thus it can happen the current session you are loading into uses a different config than the original context's **colorspaceData** was published with. It's up the loader's implementation to take that into account and decide what to do if the colorspace differs and or might not exist.\n:::"
  },
  {
    "path": "website/docs/dev_contribute.md",
    "content": "---\nid: dev_contribute\ntitle: Contribute to openPype development\nsidebar_label: Contribute\n---\n\n## What should you do if ...\n\n### You found a bug.\n\n1. Check in the issues and our [bug triage](https://github.com/pypeclub/pype/projects/2) to make sure it wasn't reported already.\n2. Ask on our [discord](http://pype.community/chat) Often, what appears as a bug, might be the intended behavior for someone else.\n3. Create a new issue.\n4. Use the issue template for you PR please.\n\n\n### You wrote a patch that fixes a bug.\n\n- Open a new GitHub pull request with the patch.\n- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.\n\n\n### You intend to add a new feature or change an existing one.\n\n- Open a new thread in the [github discussions](https://github.com/pypeclub/pype/discussions/new)\n- Do not open issue until the suggestion is discussed. We will convert accepted suggestions into backlog and point them to the relevant discussion thread to keep the context.\n\n### You have questions about the source code.\n\nOpen a new question on [github discussions](https://github.com/pypeclub/pype/discussions/new)\n\n\n## Branching Strategy\n\nAs we move to 3.x as the primary supported version of OpenPype and only keep Pype 2.15 on bugfixes and client sponsored feature requests, we need to be very careful with merging strategy.\n\nThese are the important branches to remember.\n\n### OpenPype 3.x\n\n**`main`** -  Production branch with stable releases\n\n**`develop`** - Development branch where we merge all PRs during the development\n\n**`release/3.x.x`** - Testing branch for a release, once a release branch is created, no new features\nare accepted for the given release. Bugfixes, however, are expected. Once the branch is stable it is\nmerged to `main` and `develop` and `main` is tagged with a new release\n\n**`feature/{Issue#}-{Issue_name}`** - development of new features\n\n**`bugfix/{Issue#}-{Issue_name}`** - bug fixes\n\n**`hotfix/{Issue#}-{Issue_name}`** - production critical hotfixes (always created from `main`)\n\n### OpenPype 2.x\n\nBranching is identical to 3.x development, however all the branches should be pre-pended with\n`2.x/` namespace. For example `2.x/feature/1025-support_exporting_of_alembic`, \n`2.x/bugfix/wrong_colourspace_in_maya`\n\nMain and develop for 2.x development are `2.x/main` and `2.x/develop`\n\n\nA few important notes about 2.x and 3.x development:\n\n- 3.x features are not backported to 2.x unless specifically requested.\n- 3.x bugs and hotfixes can be ported to 2.x if they are relevant or severe.\n- 2.x features and bugs MUST be ported to 3.x at the same time.\n\n## Pull Requests\n\n- Each 2.x PR MUST have a corresponding 3.x PR in github. Without 3.x PR, 2.x features will not be merged! Luckily most of the code is compatible, albeit sometimes in a different place after the refactoring. Porting from 2.x to 3.x should be really easy.\n- Please keep the corresponding 2 and 3 PR names the same so they can be easily identified from the PR list page.\n- Each 2.x PR should be labeled with `2.x-dev` label.\n\nInside each PR, put a link to the corresponding PR.\n\nOf course if you want to contribute, feel free to make a PR to only 2.x/develop or develop, based on what you are using. While reviewing the PRs, we might convert the code to corresponding PR for the other release ourselves. \n\nWe might also change the target of you PR to and intermediate branch, rather than `develop` if we feel it requires some extra work on our end. That way, we preserve all your commits so you don't lose out on the contribution credits.\n\n\n\n\nIf a PR is targeted at 2.x release it must be labelled with 2x-dev label in Github.   "
  },
  {
    "path": "website/docs/dev_deadline.md",
    "content": "---\nid: dev_deadline\ntitle: Deadline integration\nsidebar_label: Deadline integration\ntoc_max_heading_level: 4\n---\n\nDeadline is not host as usual, it is missing most of the host features, but it does have\nits own set of publishing plugins.\n\n## How to test OpenPype on Deadline\n\n### Versions\n\nSince 3.14 job submitted from OpenPype is bound to OpenPype version used to submit it. So\nif you submit job with 3.14.8, Deadline will try to find that particular version and use it\nfor rendering. This is handled by `OPENPYPE_VERSION` variable on job - you can delete it from\nthere and then the version set in studio Settings will be used.\n\n![Deadline Job Version](assets/deadline_job_version.png)\n\nDeadline needs to bootstrap this version so it will try to look the closest compatible\nbuild. So to use version 3.14.8 on Deadline it is enough to have build 3.14.0 or similar - important\nare the first two version numbers - major and minor. If they match, the version\nis considered compatible.\n\n### Testing\n\nSo to test various changes you don't need to build again an again OpenPype and putting\nit to directory where Deadline is looking for versions - this needs to be done only on\nminor version change. That build will then be used to bootstrap whatever is set on the\njob or in the studio Settings.\n\nSo you can either use zip version if it suits you, or better set your sources directory\nso it will be find as a version - for example with symlink.\n\nThat way you can only modify `OPENPYPE_VERSION` variable on job to point it to version\nyou would like to test."
  },
  {
    "path": "website/docs/dev_host_implementation.md",
    "content": "---\nid: dev_host_implementation\ntitle: Host implementation\nsidebar_label: Host implementation\ntoc_max_heading_level: 4\n---\n\nHost is an integration of DCC but in most of cases have logic that need to be handled before DCC is launched. Then based on abilities (or purpose) of DCC the integration can support different pipeline workflows.\n\n## Pipeline workflows\nWorkflows available in OpenPype are Workfiles, Load and Create-Publish. Each of them may require some functionality available in integration (e.g. call host API to achieve certain functionality). We'll go through them later.\n\n## How to implement and manage host\nAt this moment there is not fully unified way how host should be implemented but we're working on it. Host should have a \"public face\" code that can be used outside of DCC and in-DCC integration code. The main reason is that in-DCC code can have specific dependencies for python modules not available out of it's process. Hosts are located in `openpype/hosts/{host name}` folder. Current code (at many places) expect that the host name has equivalent folder there. So each subfolder should be named with the name of host it represents.\n\n### Recommended folder structure\n```python\nopenpype/hosts/{host name}\n│\n│  # Content of DCC integration - with in-DCC imports\n├─ api\n│   ├─ __init__.py\n│   └─ [DCC integration files]\n│\n│  # Plugins related to host - dynamically imported (can contain in-DCC imports)\n├─ plugins\n│   ├─ create\n│   │   └─ [create plugin files]\n│   ├─ load\n│   │   └─ [load plugin files]\n│   └─ publish\n│       └─ [publish plugin files]\n│\n│  # Launch hooks - used to modify how application is launched\n├─ hooks\n│   └─ [some pre/post launch hooks]\n|\n│  # Code initializing host integration in-DCC (DCC specific - example from Maya)\n├─ startup\n│   └─ userSetup.py\n│\n│  # Public interface\n├─ __init__.py\n└─ [other public code]\n```\n\n### Launch Hooks\nLaunch hooks are not directly connected to host implementation, but they can be used to modify launch of process which may be crucial for the implementation. Launch hook are plugins called when DCC is launched. They are processed in sequence before and after launch. Pre launch hooks can change how process of DCC is launched, e.g. change subprocess flags, modify environments or modify launch arguments. If prelaunch hook crashes the application is not launched at all. Postlaunch hooks are triggered after launch of subprocess. They can be used to change statuses in your project tracker, start timer, etc. Crashed postlaunch hooks have no effect on rest of postlaunch hooks or launched process. They can be filtered by platform, host and application and order is defined by integer value. Hooks inside host are automatically loaded (one reason why folder name should match host name) or can be defined from modules. Hooks execution share same launch context where can be stored data used across multiple hooks (please be very specific in stored keys e.g. 'project' vs. 'project_name'). For more detailed information look into `openpype/lib/applications.py`.\n\n### Public interface\nPublic face is at this moment related to launching of the DCC. At this moment there there is only option to modify environment variables before launch by implementing function `add_implementation_envs` (must be available in `openpype/hosts/{host name}/__init__.py`). The function is called after pre launch hooks, as last step before subprocess launch, to be able set environment variables crucial for proper integration. It is also good place for functions that are used in prelaunch hooks and in-DCC integration. Future plans are to be able get workfiles extensions from here. Right now workfiles extensions are hardcoded in `openpype/pipeline/constants.py` under `HOST_WORKFILE_EXTENSIONS`, we would like to handle hosts as addons similar to OpenPype modules, and more improvements which are now hardcoded.\n\n### Integration\nWe've prepared base class `HostBase` in `openpype/host/host.py` to define minimum requirements and provide some default method implementations. The minimum requirement for a host is `name` attribute, this host would not be able to do much but is valid. To extend functionality we've prepared interfaces that helps to identify what is host capable of and if is possible to use certain tools with it. For those cases we defined interfaces for each workflow. `IWorkfileHost` interface add requirement to implement workfiles related methods which makes host usable in combination with Workfiles tool. `ILoadHost` interface add requirements to be able load, update, switch or remove referenced representations which should add support to use Loader and Scene Inventory tools. `INewPublisher` interface is required to be able use host with new OpenPype publish workflow. This is what must or can be implemented to allow certain functionality. `HostBase` will have more responsibility which will be taken from global variables in future. This process won't happen at once, but will be slow to keep backwards compatibility for some time.\n\n#### Example\n```python\nfrom openpype.host import HostBase, IWorkfileHost, ILoadHost\n\n\nclass MayaHost(HostBase, IWorkfileHost, ILoadHost):\n    def open_workfile(self, filepath):\n        ...\n\n    def save_current_workfile(self, filepath=None):\n        ...\n\n    def get_current_workfile(self):\n        ...\n    ...\n```\n\n### Install integration\nWe have prepared a host class, now where and how to initialize it's object? This part is DCC specific. In DCCs like Maya with embedded python and Qt we use advantage of being able to initialize object of the class directly in DCC process on start, the same happens in Nuke, Hiero and Houdini. In DCCs like Photoshop or Harmony there is launched OpenPype (python) process next to it which handles host initialization and communication with the DCC process (e.g. using sockects). Created object of host must be installed and registered to global scope of OpenPype. Which means that at this moment one process can handle only one host at a time.\n\n#### Install example (Maya startup file)\n```python\nfrom openpype.pipeline import install_host\nfrom openpype.hosts.maya.api import MayaHost\n\n\nhost = MayaHost()\ninstall_host(host)\n```\n\nFunction `install_host` cares about installing global plugins, callbacks and register host. Host registration means that the object is kept in memory and is accessible using `get_registered_host()`.\n\n### Using UI tools\nMost of functionality in DCCs is provided to artists by using UI tools. We're trying to keep UIs consistent so we use same set of tools in each host, all or most of them are Qt based. There is a `HostToolsHelper` in `openpype/tools/utils/host_tools.py` which unify showing of default tools, they can be showed almost at any point. Some of them are validating if host is capable of using them (Workfiles, Loader and Scene Inventory) which is related to [pipeline workflows](#pipeline-workflows). `HostToolsHelper` provides API to show tools but host integration must care about giving artists ability to show them. Most of DCCs have some extendable menu bar where is possible to add custom actions, which is preferred approach how to give ability to show the tools.\n"
  },
  {
    "path": "website/docs/dev_introduction.md",
    "content": "---\nid: dev_introduction\ntitle: Introduction\nsidebar_label: Introduction\n---\n\n\nHere you should find additional information targeted on developers who would like to contribute or dive deeper into OpenPype platform\n\nCurrently there are details about automatic testing, in the future this should be location for API definition and documentation\n"
  },
  {
    "path": "website/docs/dev_publishing.md",
    "content": "---\nid: dev_publishing\ntitle: Publishing\nsidebar_label: Publishing\ntoc_max_heading_level: 4\n---\n\nPublishing workflow consists of 2 parts:\n- Creating - Mark what will be published and how.\n- Publishing - Use data from Creating to go through the pyblish process.\n\nOpenPype is using [pyblish](https://pyblish.com/) for the publishing process. OpenPype extends and modifies its few functions a bit, mainly for reports and UI purposes. The main differences are that OpenPype's publish UI allows to enable/disable instances or plugins during Creating part instead of in the publishing part and has limited plugin actions only for failed validation plugins.\n\n## **Creating**\n\nConcept of Creating does not have to \"create\" anything yet, but prepare and store metadata about an \"instance\" (becomes a subset after the publish process). Created instance always has `family` which defines what kind of data will be published, the best example is `workfile` family. Storing of metadata is host specific and may be even a Creator plugin specific. Most hosts are storing metadata into a workfile (Maya scene, Nuke script, etc.) to an item or a node the same way as regular Pyblish instances, so consistency of host implementation is kept, but some features may require a different approach that is the reason why it is creator plugin responsibility. Storing the metadata to the workfile persists values, so the artist does not have to create and set what should be published and how over and over.\n\n### Created instance\n\nObjected representation of created instance metadata defined by class **CreatedInstance**. Has access to **CreateContext** and **BaseCreator** that initialized the object. Is a dictionary-like object with few immutable keys (marked with start `*` in table). The immutable keys are set by the creator plugin or create context on initialization and their values can't change. Instance can have more arbitrary data, for example ids of nodes in scene but keep in mind that some keys are reserved.\n\n| Key | Type | Description |\n|---|---|---|\n| *id | str | Identifier of metadata type. ATM constant **\"pyblish.avalon.instance\"** |\n| *instance_id | str | Unique ID of instance. Set automatically on instance creation using `str(uuid.uuid4())` |\n| *family | str | Instance's family representing type defined by creator plugin. |\n| *creator_identifier | str | Identifier of creator that collected/created the instance. |\n| *creator_attributes | dict | Dictionary of attributes that are defined by the creator plugin (`get_instance_attr_defs`). |\n| *publish_attributes | dict | Dictionary of attributes that are defined by publish plugins. |\n| variant | str | Variant is entered by the artist on creation and may affect **subset**. |\n| subset | str | Name of instance. This name will be used as a subset name during publishing. Can be changed on context change or variant change. |\n| active | bool | Is the instance active and will be published or not. |\n| asset | str | Name of asset in which context was created. |\n| task | str | Name of task in which context was created. Can be set to `None`. |\n\n:::note\nTask should not be required until the subset name template expects it.\n:::\n\nobject of **CreatedInstance** has method **data_to_store** which returns a dictionary that can be parsed to a json string. This method will return all data related to the instance so it can be re-created using `CreatedInstance.from_existing(data)`.\n\n#### *Create context* {#category-doc-link}\n\nController and wrapper around Creating is `CreateContext` which cares about loading of plugins needed for Creating. And validates required functions in host implementation.\n\nContext discovers creator and publish plugins. Trigger collections of existing instances on creators and trigger Creating itself. Also it keeps in mind instance objects by their ids.\n\nCreator plugins can call **creator_adds_instance** or **creator_removed_instance** to add/remove instances but these methods are not meant to be called directly out of the creator. The reason is that it is the creator's responsibility to remove metadata or decide if it should remove the instance.\n\nDuring reset are re-cached Creator plugins, re-collected instances, refreshed host context and more. Object of `CreateContext` supply shared data during the reset. They can be used by creators to share same data needed during collection phase or during creation for autocreators.\n\n#### Required functions in host implementation\nIt is recommended to use `HostBase` class (`from openpype.host import HostBase`) as base for host implementation with combination of `IPublishHost` interface (`from openpype.host import IPublishHost`). These abstract classes should guide you to fill missing attributes and methods.\n\nTo sum them and in case host implementation is inheriting `HostBase` the implementation **must** implement **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now only data about enabled/disabled optional publish plugins is stored there. When data is not stored and loaded properly, reset of publishing will cause that they will be set to default value. Context data also parsed to json string similarly as instance data.\n\nThere are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return a string shown in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `\"{project name}/{asset hierarchy}/<b>{asset name}</b>/{task name}\"` (this is default implementation in `HostBase`).\n\nAnother optional function is **get_current_context**. This function is handy in hosts where it is possible to open multiple workfiles in one process so using global context variables is not relevant because artists can switch between opened workfiles without being acknowledged. When a function is not implemented or won't return the right keys the global context is used.\n```json\n# Expected keys in output\n{\n    \"project_name\": \"MyProject\",\n    \"asset_name\": \"sq01_sh0010\",\n    \"task_name\": \"Modeling\"\n}\n```\n\n### Create plugin\nMain responsibility of create plugin is to create, update, collect and remove instance metadata and propagate changes to create context. Has access to **CreateContext** (`self.create_context`) that discovered the plugin so has also access to other creators and instances. Create plugins have a lot of responsibility so it is recommended to implement common code per host.\n\n#### *BaseCreator*\nBase implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **HiddenCreator**, **AutoCreator** and **Creator** variants.\n\n**Access to shared data**\nFunctions to work with \"Collection shared data\" can be used during reset phase of `CreateContext`. Creators can cache there data that are common for them. For example list of nodes in scene. Methods are implemented on `CreateContext` but their usage is primarily for Create plugins as nothing else should use it. Each creator can access `collection_shared_data` attribute which is a dictionary where shared data can be stored.\n\n**Abstractions**\n- **`family`** (class attr) - Tells what kind of instance will be created.\n```python\nclass WorkfileCreator(Creator):\n    family = \"workfile\"\n```\n\n- **`collect_instances`** (method) - Collect already existing instances from the workfile and add them to create context. This method is called on initialization or reset of **CreateContext**. Each creator is responsible to find its instance metadata, convert them to **CreatedInstance** object and add them to create context (`self._add_instance_to_context(instnace_obj)`).\n```python\ndef collect_instances(self):\n    # Using 'pipeline.list_instances' is just example how to get existing instances from scene\n    # - getting existing instances is different per host implementation\n    for instance_data in pipeline.list_instances():\n        # Process only instances that were created by this creator\n        creator_id = instance_data.get(\"creator_identifier\")\n        if creator_id == self.identifier:\n            # Create instance object from existing data\n            instance = CreatedInstance.from_existing(\n                instance_data, self\n            )\n            # Add instance to create context\n            self._add_instance_to_context(instance)\n```\n\n- **`create`** (method) - Create a new object of **CreatedInstance** store its metadata to the workfile and add the instance into the created context. Failed Creating should raise **CreatorError** if an error happens that artists can fix or give them some useful information. Triggers and implementation differs for **Creator**, **HiddenCreator** and **AutoCreator**.\n\n- **`update_instances`** (method) - Update data of instances. Receives tuple with **instance** and **changes**.\n```python\ndef update_instances(self, update_list):\n    # Loop over changed instances\n    for instance, changes in update_list:\n        # Example possible usage of 'changes' to use different node on change\n        #   of node id in instance data (MADE UP)\n        node = None\n        if \"node_id\" in changes:\n            old_value, new_value = changes[\"node_id\"]\n            if new_value is not None:\n                node = pipeline.get_node_by_id(new_value)\n\n        if node is None:\n            node = pipeline.get_node_by_instance_id(instance.id)\n        # Get node in scene that represents the instance\n        # Imprind data to a node\n        pipeline.imprint(node, instance.data_to_store())\n\n\n# Most implementations will probably ignore 'changes' completely\ndef update_instances(self, update_list):\n    for instance, _ in update_list:\n        # Get node from scene\n        node = pipeline.get_node_by_instance_id(instance.id)\n        # Imprint data to node\n        pipeline.imprint(node, instance.data_to_store())\n```\n\n- **`remove_instances`** (method) - Remove instance metadata from workfile and from create context.\n```python\n# Possible way how to remove instance\ndef remove_instances(self, instances):\n    for instance in instances:\n        # Remove instance metadata from workflle\n        pipeline.remove_instance(instance.id)\n        # Remove instance from create context\n        self._remove_instance_from_context(instance)\n\n\n# Default implementation of `AutoCreator`\ndef remove_instances(self, instances):\n    pass\n```\n\n:::note\nWhen host implementation use universal way how to store and load instances you should implement host specific creator plugin base class with implemented **collect_instances**, **update_instances** and **remove_instances**.\n:::\n\n**Optional implementations**\n\n- **`enabled`** (attr) - Boolean if the creator plugin is enabled and used.\n- **`identifier`** (class attr) - Consistent unique string identifier of the creator plugin. Is used to identify source plugin of existing instances. There can't be 2 creator plugins with the same identifier. Default implementation returns `family` attribute.\n```python\nclass RenderLayerCreator(Creator):\n    family = \"render\"\n    identifier = \"render_layer\"\n\n\nclass RenderPassCreator(Creator):\n    family = \"render\"\n    identifier = \"render_pass\"\n```\n\n- **`label`** (attr) - String label of creator plugin which will show up in UI, `identifier` is used when not set. It should be possible to use html tags.\n```python\nclass RenderLayerCreator(Creator):\n    label = \"Render Layer\"\n```\n\n- **`get_icon`** (attr) - Icon of creator and its instances. Value can be a path to an image file, full name of qtawesome icon, `QPixmap` or `QIcon`. For complex cases or cases when `Qt` objects are returned it is recommended to override `get_icon` method and handle the logic or import `Qt` inside the method to not break headless usage of creator plugin. For list of qtawesome icons check qtawesome github repository (look for the used version in pyproject.toml). Default implementation return **icon** attribute.\n- **`icon`** (method) - Attribute for default implementation of **get_icon**.\n```python\nclass RenderLayerCreator(Creator):\n    # Use font awesome 5 icon\n    icon = \"fa5.building\"\n```\n\n- **`get_instance_attr_defs`** (method) - Attribute definitions of instance. Creator can define attribute values with default values for each instance. These attributes may affect how instances will be instance processed during publishing. Attribute defiitions can be used from `openpype.lib.attribute_definitions`. Attribute definitions define basic types of values for different cases e.g. boolean, number, string, enumerator, etc. Default implementation returns **instance_attr_defs**.\n- **`instance_attr_defs`** (attr) - Attribute for default implementation of **get_instance_attr_defs**.\n\n```python\nfrom openpype.lib import attribute_definitions\n\n\nclass RenderLayerCreator(Creator):\n    def get_instance_attr_defs(self):\n        # Return empty list if '_allow_farm_render' is not enabled (can be set during initialization)\n        if not self._allow_farm_render:\n            return []\n        # Give artist option to change if should be rendered on farm or locally\n        return [\n            attribute_definitions.BoolDef(\n                \"render_farm\",\n                default=False,\n                label=\"Render on Farm\"\n            )\n        ]\n```\n\n- **`get_subset_name`** (method) - Calculate subset name based on passed data. Data can be extended using the `get_dynamic_data` method. Default implementation is using `get_subset_name` from `openpype.lib` which is recommended.\n\n- **`get_dynamic_data`** (method) - Can be used to extend data for subset templates which may be required in some cases.\n\nMethods are used before instance creation and on instance subset name update. Update may require to have access to existing instance because dynamic data should be filled from there. Because of that is instance passed to `get_subset_name` and `get_dynamic_data` so the creator can handle that cases.\n\nThis is one example where subset name template may contain `\"{layer}\"` which is filled during creation because the value is taken from selection. In that case `get_dynamic_data` returns value for `\"layer\"` -> `\"{layer}\"` so it can be filled in creation. But when subset name of already existing instance is updated it should return already existing value. Note: Creator must make sure the value is available on instance.\n\n```python\nfrom openpype.lib import prepare_template_data\nfrom my_host import get_selected_layer\n\n\nclass SomeCreator(Creator):\n    def get_dynamic_data(\n        self, variant, task_name, asset_doc, project_name, host_name, instance\n    ):\n        # Before instance is created return unfilled key\n        # - the key will be filled during creation\n        if instance is None:\n            return {\"layer\": \"{layer}\"}\n        # Take value from existing instance\n        # - creator must know where to look for the value\n        return {\"layer\": instance.data[\"layer\"]}\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # Fill the layer name in\n        layer = get_selected_layer()\n        layer_name = layer[\"name\"]\n        layer_fill_data = prepare_template_data({\"layer\": layer_name})\n        subset_name = subset_name.format(**layer_fill_data)\n        instance_data[\"layer\"] = layer_name\n        ...\n```\n\n\n#### *HiddenCreator*\nCreator which is not showed in UI so artist can't trigger it directly but is available for other creators. This creator is primarily meant for cases when creation should create different types of instances. For example during editorial publishing where input is single edl file but should create 2 or more kind of instances each with different family, attributes and abilities. Arguments for creation were limited to `instance_data` and `source_data`. Data of `instance_data` should follow what is sent to other creators and `source_data` can be used to send custom data defined by main creator. It is expected that `HiddenCreator` has specific main or \"parent\" creator.\n\n```python\ndef create(self, instance_data, source_data):\n    variant = instance_data[\"variant\"]\n    task_name = instance_data[\"task\"]\n    asset_name = instance_data[\"asset\"]\n    asset_doc = get_asset_by_name(self.project_name, asset_name)\n    self.get_subset_name(\n        variant, task_name, asset_doc, self.project_name, self.host_name)\n```\n\n\n#### *AutoCreator*\nCreator that is triggered on reset of create context. Can be used for families that are expected to be created automatically without artist interaction (e.g. **workfile**). Method `create` is triggered after collecting all creators.\n\n:::important\n**AutoCreator** has implemented **remove_instances** to do nothing as removing of auto created instances would lead to creating new instance immediately or on refresh.\n:::\n\n```python\ndef __init__(\n    self, create_context, system_settings, project_settings, *args, **kwargs\n):\n    super(MyCreator, self).__init__(\n        create_context, system_settings, project_settings, *args, **kwargs\n    )\n    # Get variant value from settings\n    variant_name = (\n        project_settings[\"my_host\"][self.identifier][\"variant\"]\n    ).strip()\n    if not variant_name:\n        variant_name = \"Main\"\n    self._variant_name = variant_name\n\n# Create does not expect any arguments\ndef create(self):\n    # Look for existing instance in create  context\n    existing_instance = None\n    for instance in self.create_context.instances:\n        if instance.creator_identifier == self.identifier:\n            existing_instance = instance\n            break\n\n    # Collect current context information\n    # - variant can be filled from settings\n    variant = self._variant_name\n    # Only place where we can look for current context\n    project_name = self.project_name\n    asset_name = legacy_io.Session[\"AVALON_ASSET\"]\n    task_name = legacy_io.Session[\"AVALON_TASK\"]\n    host_name = legacy_io.Session[\"AVALON_APP\"]\n\n    # Create new instance if does not exist yet\n    if existing_instance is None:\n        asset_doc = get_asset_by_name(project_name, asset_name)\n        subset_name = self.get_subset_name(\n            variant, task_name, asset_doc, project_name, host_name\n        )\n        data = {\n            \"asset\": asset_name,\n            \"task\": task_name,\n            \"variant\": variant\n        }\n        data.update(self.get_dynamic_data(\n            variant, task_name, asset_doc, project_name, host_name\n        ))\n\n        new_instance = CreatedInstance(\n            self.family, subset_name, data, self\n        )\n        self._add_instance_to_context(new_instance)\n\n    # Update instance context if is not the same\n    elif (\n        existing_instance[\"asset\"] != asset_name\n        or existing_instance[\"task\"] != task_name\n    ):\n        asset_doc = get_asset_by_name(project_name, asset_name)\n        subset_name = self.get_subset_name(\n            variant, task_name, asset_doc, project_name, host_name\n        )\n        existing_instance[\"asset\"] = asset_name\n        existing_instance[\"task\"] = task_name\n```\n\n#### *Creator*\nImplementation of creator plugin that is triggered manually by the artist in UI (or by code). Has extended options for UI purposes than **AutoCreator** and **create** method expect more arguments.\n\n**Optional implementations**\n- **`create_allow_context_change`** (class attr) - Allow to set context in UI before Creating. Some creators may not allow it or their logic would not use the context selection (e.g. bulk creators). Is set to `True` but default.\n```python\nclass BulkRenderCreator(Creator):\n    create_allow_context_change = False\n```\n- **`get_default_variants`** (method) - Returns list of default variants that are listed in create dialog for user. Returns **default_variants** attribute by default.\n- **`default_variants`** (attr) - Attribute for default implementation of **get_default_variants**.\n\n- **`get_default_variant`** (method) - Returns default variant that is prefilled in UI (value does not have to be in default variants). By default returns **default_variant** attribute. If returns `None` then UI logic will take first item from **get_default_variants** if there is any otherwise **\"Main\"** is used.\n- **`default_variant`** (attr) - Attribute for default implementation of **get_default_variant**.\n\n- **`get_description`** (method) - Returns a short string description of the creator. Returns **description** attribute by default.\n- **`description`** (attr) - Attribute for default implementation of **get_description**.\n\n- **`get_detailed_description`** (method) - Returns detailed string description of creator. Can contain markdown. Returns **detailed_description** attribute by default.\n- **`detailed_description`** (attr) - Attribute for default implementation of **get_detailed_description**.\n\n- **`get_pre_create_attr_defs`** (method) - Similar to **get_instance_attr_defs** returns attribute definitions but they are filled before creation. When creation is called from UI the values are passed to **create** method. Returns **pre_create_attr_defs** attribute by default.\n- **`pre_create_attr_defs`** (attr) - Attribute for default implementation of **get_pre_create_attr_defs**.\n\n```python\nfrom openpype.lib import attribute_definitions\nfrom openpype.pipeline.create import Creator\n\n\nclass CreateRender(Creator):\n    family = \"render\"\n    label = \"Render\"\n    icon = \"fa.eye\"\n    description = \"Render scene viewport\"\n\n    def __init__(\n        self, context, system_settings, project_settings, *args, **kwargs\n    ):\n        super(CreateRender, self).__init__(\n            context, system_settings, project_settings, *args, **kwargs\n        )\n        plugin_settings = (\n            project_settings[\"my_host\"][\"create\"][self.__class__.__name__]\n        )\n        # Get information if studio has enabled farm publishing\n        self._allow_farm_render = plugin_settings[\"allow_farm_render\"]\n        # Get default variants from settings\n        self.default_variants = plugin_settings[\"variants\"]\n\n    def get_instance_attr_defs(self):\n        # Return empty list if '_allow_farm_render' is not enabled (can be set during initialization)\n        if not self._allow_farm_render:\n            return []\n        # Give artist option to change if should be rendered on farm or locally\n        return [\n            attribute_definitions.BoolDef(\n                \"render_farm\",\n                default=False,\n                label=\"Render on Farm\"\n            )\n        ]\n\n    def get_pre_create_attr_defs(self):\n        # Give user option to use selection or not\n        attrs = [\n            attribute_definitions.BoolDef(\n                \"use_selection\",\n                default=False,\n                label=\"Use selection\"\n            )\n        ]\n        if self._allow_farm_render:\n            # Set to render on farm in creator dialog\n            # - this value is not automatically passed to instance attributes\n            #   creator must do that during creation\n            attrs.append(\n                attribute_definitions.BoolDef(\n                    \"render_farm\",\n                    default=False,\n                    label=\"Render on Farm\"\n                )\n            )\n        return attrs\n\n    def create(self, subset_name, instance_data, pre_create_data):\n        # ARGS:\n        # - 'subset_name' - precalculated subset name\n        # - 'instance_data' - context data\n        #    - 'asset' - asset name\n        #    - 'task' - task name\n        #    - 'variant' - variant\n        #    - 'family' - instance family\n\n        # Check if should use selection or not\n        if pre_create_data.get(\"use_selection\"):\n            items = pipeline.get_selection()\n        else:\n            items = [pipeline.create_write()]\n\n        # Validations related to selection\n        if len(items) > 1:\n            raise CreatorError(\"Please select only single item at time.\")\n\n        elif not items:\n            raise CreatorError(\"Nothing to create. Select at least one item.\")\n\n        # Create instence object\n        new_instance = CreatedInstance(self.family, subset_name, data, self)\n        # Pass value from pre create attribute to instance\n        # - use them only when pre create date contain the data\n        if \"render_farm\" in pre_create_data:\n            use_farm = pre_create_data[\"render_farm\"]\n            new_instance.creator_attributes[\"render_farm\"] = use_farm\n\n        # Store metadata to workfile\n        pipeline.imprint(new_instance.id, new_instance.data_to_store())\n\n        # Add instance to context\n        self._add_instance_to_context(new_instance)\n```\n\n## **Publish**\n### Exceptions\nOpenPype define few specific exceptions that should be used in publish plugins.\n\n#### *Validation exception*\nValidation plugins should raise `PublishValidationError` to show to an artist what's wrong and give him actions to fix it. The exception says that errors in the plugin can be fixed by the artist himself (with or without action on plugin). Any other errors will stop publishing immediately. The exception `PublishValidationError` raised after validation order has the same effect as any other exception.\n\nException `PublishValidationError` expects 4 arguments:\n- **message** Which is not used in UI but for headless publishing.\n- **title** Short description of error (2-5 words). Title is used for grouping of exceptions per plugin.\n- **description** Detailed description of the issue where markdown and html can be used.\n- **detail** Is optional to give even more detailed information for advanced users. At this moment the detail is shown directly under description but it is in plan to have detail in a collapsible widget.\n\nExtended version is `PublishXmlValidationError` which uses xml files with stored descriptions. This helps to avoid having huge markdown texts inside code. The exception has 4 arguments:\n- **plugin** The plugin object which raises the exception to find its related xml file.\n- **message** Exception message for publishing without UI or different pyblish UI.\n- **key** Optional argument says which error from xml is used as a validation plugin may raise error with different messages based on the current errors. Default is **\"main\"**.\n- **formatting_data** Optional dictionary to format data in the error. This is used to fill detailed description with data from the publishing so artist can get more precise information.\n\n**Where and how to create xml file**\n\nXml files for `PublishXmlValidationError` must be located in **./help** subfolder next to the plugin and the filename must match the filename of the plugin.\n```\n# File location related to plugin file\n└ publish\n ├ help\n │ ├ validate_scene.xml\n │ └ ...\n ├ validate_scene.py\n └ ...\n```\n\nXml file content has **&ltroot&gt** node which may contain any amount of **&lterror&gt** nodes, but each of them must have **id** attribute with unique value. That is then used for **key**. Each error must have **&lttitle&gt** and **&ltdescription&gt** and **&ltdetail&gt**. Text content may contain python formatting keys that can be filled when an exception is raised.\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n    <error id=\"main\">\n        <title>Subset context</title>\n        <description>## Invalid subset context\n\nContext of the given subset doesn't match your current scene.\n\n### How to repair?\n\nYou can fix this with the \"Repair\" button on the right. This will use '{expected_asset}' asset name and overwrite '{found_asset}' asset name in scene metadata.\n\nAfter that restart publishing with Reload button.\n        </description>\n        <detail>\n### How could this happen?\n\nThe subset was created in different scene with different context\nor the scene file was copy pasted from different context.\n        </detail>\n    </error>\n</root>\n```\n\n#### *Known errors*\nWhen there is a known error that can't be fixed by the user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raised. The only difference is that its message is shown in UI to the artist otherwise a neutral message without context is shown.\n\n### Plugins\nPlugin is a single processing unit that can work with publish context and instances.\n\n#### Plugin types\nThere are 2 types of plugins - `InstancePlugin` and `ContextPlugin`.  Be aware that inheritance of plugin from `InstancePlugin` or `ContextPlugin` actually does not affect if plugin is instance or context plugin, that is affected by argument name in `process` method.\n\n```python\nimport pyblish.api\n\n\n# Context plugin\nclass MyContextPlugin(pyblish.api.ContextPlugin):\n    def process(self, context):\n        ...\n\n# Instance plugin\nclass MyInstancePlugin(pyblish.api.InstancePlugin):\n    def process(self, instance):\n        ...\n\n# Still an instance plugin\nclass MyOtherInstancePlugin(pyblish.api.ContextPlugin):\n    def process(self, instance):\n        ...\n```\n\n#### Plugin filtering\nBy pyblish logic, plugins have predefined filtering class attributes `hosts`, `targets` and `families`. Filter by `hosts` and `targets` are filters that are applied for current publishing process. Both filters are registered in `pyblish` module, `hosts` filtering may not match OpenPype host name (e.g. farm publishing uses `shell` in pyblish). Filter `families` works only on instance plugins and is dynamic during publish process by changing families of an instance.\n\nAll filters are list of a strings `families = [\"image\"]`. Empty list is invalid filter and plugin will be skipped, to allow plugin for all values use a start `families = [\"*\"]`. For more detailed filtering options check [pyblish documentation](https://api.pyblish.com/pluginsystem).\n\nEach plugin must have order, there are 4 order milestones - Collect, Validate, Extract, Integration. Any plugin below collection order won't be processed. for more details check [pyblish documentation](https://api.pyblish.com/ordering).\n\n#### Plugin settings\nPyblish plugins may have settings. There are 2 ways how settings are applied, first is automated, and it's logic is based on function `filter_pyblish_plugins` in `./openpype/pipeline/publish/lib.py`, second is explicit by implementing class method `apply_settings` on a plugin.\n\n\nAutomated logic is expecting specific structure of project settings `project_settings[{category}][\"plugins\"][\"publish\"][{plugin class name}]`. The category is a key in root of project settings. There are currently 3 ways how the category key is received.\n1. Use `settings_category` class attribute value from plugin. If `settings_category` is not `None` there is not any fallback to other way.\n2. Use currently registered pyblish host. This will be probably deprecated soon.\n3. Use 3rd folder name from a plugin filepath. From path `./maya/plugins/publish/collect_render.py` is used `maya` as the key.\n\nFor any other use-case is recommended to use explicit approach by implementing `apply_settings` method. Must use `@classmethod` decorator and expect arguments for project settings and system settings. We're planning to support single argument with only project settings.\n```python\nimport pyblish.api\n\n\nclass MyPlugin(pyblish.api.InstancePlugin):\n    profiles = []\n\n    @classmethod\n    def apply_settings(cls, project_settings, system_settings):\n        cls.profiles = (\n            project_settings\n            [\"addon\"]\n            [\"plugins\"]\n            [\"publish\"]\n            [\"vfx_profiles\"]\n        )\n```\n\n### Plugin extension\nPublish plugins can be extended by additional logic when inheriting from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). Publish plugins that inherit from this mixin can define attributes that will be shown in **CreatedInstance**. One of the most important usages is to be able turn on/off optional plugins.\n\nAttributes are defined by the return value of `get_attribute_defs` method. Attribute definitions are for families defined in plugin's `families` attribute if it's instance plugin or for whole context if it's context plugin. To convert existing values (or to remove legacy values) can be re-implemented `convert_attribute_values`. Default implementation just converts the values to right types.\n\n:::Important\nValues of publish attributes from created instance are never removed automatically so implementing this method is the best way to remove legacy data or convert them to new data structure.\n:::\n\nPossible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`.\n\n<details>\n<summary>Example plugin</summary>\n<p>\n\n```python\nimport pyblish.api\nfrom openpype.lib import attribute_definitions\nfrom openpype.pipeline import OpenPypePyblishPluginMixin\n\n\n# Example context plugin\nclass MyExtendedPlugin(\n    pyblish.api.ContextPlugin, OpenPypePyblishPluginMixin\n):\n    optional = True\n    active = True\n\n    @classmethod\n    def get_attribute_defs(cls):\n        return [\n            attribute_definitions.BoolDef(\n                # Key under which it will be stored\n                \"process\",\n                # Use 'active' as default value\n                default=cls.active,\n                # Use plugin label as label for attribute\n                label=cls.label\n            )\n        ]\n\n    def process_plugin(self, context):\n        # First check if plugin is optional\n        if not self.optional:\n            return True\n\n        # Attribute values are stored by class names\n        # - for those purposes was implemented 'get_attr_values_from_data'\n        #   to help with accessing it\n        attribute_values = self.get_attr_values_from_data(context.data)\n        # Get 'process' key\n        process_value = attribute_values.get(\"process\")\n        if process_value is None or process_value:\n            return True\n        return False\n\n    def process(self, context):\n        if not self.process_plugin(context):\n            return\n        # Do plugin logic\n        ...\n```\n</p>\n</details>\n\n## **UI examples**\n### Main publish window\nMain window of publisher shows instances and their values, collected by creators.\n\n**Card view**\n![Publisher UI - Card view](assets/publisher_card_view.png)\n**List view**\n![Publisher UI - List view](assets/publisher_list_view.png)\n\n#### *Instances views*\nList of instances always contains an `Options` item which is used to show attributes of context plugins. Values from the item are saved and loaded using [host implementation](#required-functions-in-host-implementation) **get_context_data** and **update_context_data**. Instances are grouped by family and can be shown in card view (single selection) or list view (multi selection).\n\nInstance view has at the bottom 3 buttons. Plus sign opens [create dialog](#create-dialog), bin removes selected instances and stripes swap card and list view.\n\n#### *Context options*\nIt is possible to change variant or asset and task context of instances at the top part but all changes there must be confirmed. Confirmation will trigger recalculation of subset names and all new data are stored to instances.\n\n#### *Create attributes*\nInstance attributes display all created attributes of all selected instances. All attributes that have the same definition are grouped into one input and are visually indicated if values are not the same for selected instances. In most cases have **< Multiselection >** placeholder.\n\n#### *Publish attributes*\nPublish attributes work the same way as create attributes but the source of attribute definitions are pyblish plugins. Attributes are filtered based on families of selected instances and families defined in the pyblish plugin.\n\n### Create dialog\n![Publisher UI - Create dialog](assets/publisher_create_dialog.png)\nCreate dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator.\n"
  },
  {
    "path": "website/docs/dev_requirements.md",
    "content": "---\nid: dev_requirements\ntitle: Requirements\nsidebar_label: Requirements\n---\n\n\nWe aim to closely follow [**VFX Reference Platform**](https://vfxplatform.com/)\n\nOpenPype is written in Python 3 with specific elements still running in Python2 until all DCCs are fully updated. To see the list of those, that are not quite there yet, go to [VFX Python3 tracker](https://vfxpy.com/)\n\nThe main things you will need to run and build pype are:\n\n- **Terminal** in your OS\n    - PowerShell 5.0+ (Windows)\n    - Bash (Linux)\n- [**Python 3.9.x**](#python)\n- [**MongoDB**](#database)\n\n\n## OS\n\nIt can be built and ran on all common platforms. We develop and test on the following:\n\n- **Windows** 10\n- **Linux**\n    - **Ubuntu** 20.04 LTS\n    - **Centos** 7\n- **Mac OSX** \n    - **10.15** Catalina\n    - **11.1** Big Sur (using Rosetta2)\n\n\n## Database \n\nDatabase version should be at least **MongoDB 4.4**.\n\nPype needs site-wide installation of **MongoDB**. It should be installed on\nreliable server, that all workstations (and possibly render nodes) can connect. This\nserver holds **Avalon** database that is at the core of everything\n\nDepending on project size and number of artists working, connection speed and\nlatency influence performance experienced by artists. If remote working is required, this mongodb\nserver must be accessible from Internet or cloud solution can be used. Reasonable backup plan\nor high availability options are recommended. *Replication* feature of MongoDB should be considered. This is beyond the\nscope of this documentation, please refer to [MongoDB Documentation](https://docs.mongodb.com/manual/replication/).\n\nPype can run its own instance of mongodb, mostly for testing and development purposes.\nFor that it uses locally installed MongoDB.\n\nDownload it from [mognoDB website](https://www.mongodb.com/download-center/community), install it and\nadd to the `PATH`. On Windows, Pype tries to find it in standard installation destination or using `PATH`.\n\nTo run mongoDB on server, use your server distribution tools to set it up (on Linux).\n\n## Python\n\n**Python 3.9.x** is the recommended version to use (as per [VFX platform CY2022](https://vfxplatform.com/)).\n**Note**: We do not support 3.9.0 because of [this bug](https://github.com/python/cpython/pull/22670). Please, use higher versions of 3.9.x.\n\nIf you're planning to run openPYPE on workstations from built executables (highly recommended), you will only need python for building and development, however, if you'd like to run from source centrally, every user will need python installed.\n\n## Hardware\n\nopenPYPE should be installed on all workstations that need to use it, the same as any other application.\n\nThere are no specific requirements for the hardware. If the workstation can run\nthe major DCCs, it most probably can run openPYPE.\n\nInstalled, it takes around 400MB of space, depending on the platform\n\n\nFor a well functioning ftrack event server, we recommend a linux virtual server with Ubuntu or CentOS. CPU and RAM allocation needs differ based on the studio size, but a 2GB of ram, with a dual core CPU and around 4GB of storage should suffice\n\n\n## Deployment\n\nFor pushing pipeline updates to the artists, you will need to create a shared folder that \nwill be accessible with at least Read permission to every OpenPype user in the studio.\nThis can also be hosted on the cloud in fully distributed deployments.\n\n\n\n## Dependencies\n\n### Key projects we depend on\n\n- [**Avalon**](https://github.com/getavalon)\n- [**Pyblish**](https://github.com/pyblish)\n- [**OpenTimelineIO**](https://github.com/PixarAnimationStudios/OpenTimelineIO)\n- [**OpenImageIO**](https://github.com/OpenImageIO/oiio) [^centos7]\n- [**FFmpeg**](https://github.com/FFmpeg/FFmpeg)\n\n[^centos7]: On Centos 7 you need to install additional libraries to support OIIO there - mainly boost\nand libraw (`sudo yum install boost-1.53.0` and `sudo yum install LibRaw`)\n\n### Python modules we use and their licenses\n\n|               Package               |                           License                            |\n|-------------------------------------|--------------------------------------------------------------|\n|              acre 1.0.0             |        GNU Lesser General Public License v3 (LGPLv3)         |\n|            aiohttp 3.7.3            |                           Apache 2                           |\n|       aiohttp-json-rpc 0.13.3       |                          Apache 2.0                          |\n|            appdirs 1.4.4            |                             MIT                              |\n|           blessed 1.17.12           |                             MIT                              |\n|             click 7.1.2             |                         BSD-3-Clause                         |\n|             clique 1.5.0            |                     Apache License (2.0)                     |\n|            coverage 5.3.1           |                          Apache 2.0                          |\n|           cx-Freeze 6.5.1           |              Python Software Foundation License              |\n|            docutils 0.16            | public domain, Python, 2-Clause BSD, GPL 3 (see COPYING.txt) |\n|             flake8 3.8.4            |                             MIT                              |\n|       ftrack-python-api 2.0.0       |                     Apache License (2.0)                     |\n|             jinxed 1.0.1            |                           MPLv2.0                            \n|           log4mongo 1.7.0           |                             BSD                              |\n|      OpenTimelineIO 0.14.0.dev1     |                 Modified Apache 2.0 License                  |\n|             Pillow 8.1.0            |                             HPND                             |\n|          pyblish-base 1.8.8         |                             LGPL                             |\n|          pycodestyle 2.6.0          |                        Expat license                         |\n|           pydocstyle 5.1.1          |                             MIT                              |\n|             pylint 2.6.0            |                             GPL                              |\n|            pymongo 3.11.2           |                 Apache License, Version 2.0                  |\n|             pynput 1.7.2            |                            LGPLv3                            |\n|             PyQt5 5.15.2            |                            GPL v3                            |\n|             pytest 6.2.1            |                             MIT                              |\n|          pytest-cov 2.11.0          |                             MIT                              |\n|          pytest-print 0.2.1         |                             MIT                              |\n|         pywin32-ctypes 0.2.0        |                             BSD                              |\n|             Qt.py 1.3.2             |                             MIT                              |\n|              six 1.15.0             |                             MIT                              |\n|           speedcopy 2.1.0           |                           UNKNOWN                            |\n|             Sphinx 3.4.3            |                             BSD                              |\n|     sphinx-qt-documentation 0.3     |                         BSD-3-Clause                         |\n|    sphinxcontrib-websupport 1.2.4   |                             BSD                              |\n|             tqdm 4.56.0             |                    MPLv2.0, MIT Licences                     |\n|             wheel 0.36.2            |                             MIT                              |\n|         wsrpc-aiohttp 3.1.1         |                   Apache Software License                    |\n"
  },
  {
    "path": "website/docs/dev_settings.md",
    "content": "---\nid: dev_settings\ntitle: Settings\nsidebar_label: Settings\n---\n\nSettings give the ability to change how OpenPype behaves in certain situations. Settings are split into 3 categories **system settings**, **project anatomy** and **project settings**. Project anatomy and project settings are grouped into a single category but there is a technical difference (explained later). Only difference in system and project settings is that system settings can't be technically handled on a project level or their values must be available no matter in which project the values are received. Settings have headless entities or settings UI.\n\nThere is one more category **local settings** but they don't have ability to be changed or defined easily. Local settings can change how settings work per machine, can affect both system and project settings but they're hardcoded for predefined values at this moment.\n\n## Settings schemas\nSystem and project settings are defined by settings schemas. Schema defines the structure of output value, what value types output will contain, how settings are stored and how its UI input will look.\n\n## Settings values\nOutput of settings is a json serializable value. There are 3 possible types of value **default values**, **studio overrides** and **project overrides**. Default values must be always available for all settings schemas, their values are stored to code. Default values are what everyone who just installed OpenPype will use as default values. It is good practice to set example values but they should be actually relevant.\n\nSetting overrides is what makes settings a powerful tool. Overrides contain only a part of settings with additional metadata that describe which parts of settings values should be replaced from overrides values. Using overrides gives the ability to save only specific values and use default values for rest. It is super useful in project settings which have up to 2 levels of overrides. In project settings are used **default values** as base on which are applied **studio overrides** and then **project overrides**. In practice it is possible to save only studio overrides which affect all projects. Changes in studio overrides are then propagated to all projects without project overrides. But values can be locked on project level so studio overrides are not used.\n\n## Settings storage\nAs was mentioned default values are stored into repository files. Overrides are stored in the Mongo database. The value in mongo contain only overrides with metadata so their content on it's own is useless and must be used with combination of default values. System settings and project settings are stored into special collection. Single document represents one set of overrides with OpenPype version for which is stored. Settings are versioned and are loaded in specific order - current OpenPype version overrides or first lower available. If there are any overrides with the same or lower version then the first higher version is used. If there are any overrides then no overrides are applied.\n\nProject anatomy is stored into a project document thus is not versioned and its values are always overridden. Any changes in anatomy schema may have a drastic effect on production and OpenPype updates.\n\n## Settings schema items\nAs was mentioned schema items define output type of values, how they are stored and how they look in UI.\n- schemas are (by default) defined by json files\n- OpenPype core system settings schemas are stored in `~/openpype/settings/entities/schemas/system_schema/` and project settings in `~/openpype/settings/entities/schemas/projects_schema/`\n    - both contain `schema_main.json` which are entry points\n- OpenPype modules/addons can define their settings schemas using `BaseModuleSettingsDef` in that case some functionality may be slightly modified\n- single schema item is represented by dictionary (object) in json which has `\"type\"` key.\n    - **type** is only common key which is required for all schema items\n- each item may have \"input modifiers\" (other keys in dictionary) and they may be required or optional based on the type\n- there are special keys across all items\n\t- `\"is_file\"` - this key is used when defaults values are stored in the file. Its value matches the filename where values are stored\n\t  - key is validated, must be unique in hierarchy otherwise it won't be possible to store default values\n\t\t- make sense to fill it only if it's value if `true`\n\n\t- `\"is_group\"` - define that all values under a key in settings hierarchy will be overridden if any value is modified\n\t\t  - this key is not allowed for all inputs as they may not have technical ability to handle it\n\t\t  - key is validated, must be unique in hierarchy and is automatically filled on last possible item if is not defined in schemas\n\t\t  - make sense to fill it only if it's value if `true`\n- all entities can have set `\"tooltip\"` key with description which will be shown in UI on hover\n\n### Inner schema\nSettings schemas are big json files which would become unmanageable if they were in a single file. To be able to split them into multiple files to help organize them special types `schema` and `template` were added. Both types are related to a different file by filename. If a json file contains a dictionary it is considered as `schema` if it contains a list it is considered as a `template`.\n\n#### schema\nSchema item is replaced by content of entered schema name. It is recommended that the schema file is used only once in settings hierarchy. Templates are meant for reusing.\n- schema must have `\"name\"` key which is name of schema that should be used\n\n```javascript\n{\n    \"type\": \"schema\",\n    \"name\": \"my_schema_name\"\n}\n```\n\n#### template\nTemplates are almost the same as schema items but can contain one or more items which can be formatted with additional data or some keys can be skipped if needed. Templates are meant for reusing the same schemas with ability to modify content.\n\n- legacy name is `schema_template` (still usable)\n- template must have `\"name\"` key which is name of template file that should be used\n- to fill formatting keys use `\"template_data\"`\n- all items in template, except `__default_values__`, will replace `template` item in original schema\n- template may contain other templates\n\n```javascript\n// Example template json file content\n[\n    {\n        // Define default values for formatting values\n        // - gives ability to set the value but have default value\n        \"__default_values__\": {\n            \"multipath_executables\": true\n        }\n    }, {\n        \"type\": \"raw-json\",\n        \"label\": \"{host_label} Environments\",\n        \"key\": \"{host_name}_environments\"\n    }, {\n        \"type\": \"path\",\n        \"key\": \"{host_name}_executables\",\n        \"label\": \"{host_label} - Full paths to executables\",\n        \"multiplatform\": \"{multipath_executables}\",\n        \"multipath\": true\n    }\n]\n```\n```javascript\n// Example usage of the template in schema\n{\n    \"type\": \"dict\",\n    \"key\": \"template_examples\",\n    \"label\": \"Schema template examples\",\n    \"children\": [\n        {\n            \"type\": \"template\",\n            \"name\": \"example_template\",\n            \"template_data\": [\n                {\n                    \"host_label\": \"Maya 2019\",\n                    \"host_name\": \"maya_2019\",\n                    \"multipath_executables\": false\n                },\n                {\n                    \"host_label\": \"Maya 2020\",\n                    \"host_name\": \"maya_2020\"\n                },\n                {\n                    \"host_label\": \"Maya 2021\",\n                    \"host_name\": \"maya_2021\"\n                }\n            ]\n        }\n    ]\n}\n```\n```javascript\n// The same schema defined without templates\n{\n    \"type\": \"dict\",\n    \"key\": \"template_examples\",\n    \"label\": \"Schema template examples\",\n    \"children\": [\n        {\n            \"type\": \"raw-json\",\n            \"label\": \"Maya 2019 Environments\",\n            \"key\": \"maya_2019_environments\"\n        }, {\n            \"type\": \"path\",\n            \"key\": \"maya_2019_executables\",\n            \"label\": \"Maya 2019 - Full paths to executables\",\n            \"multiplatform\": false,\n            \"multipath\": true\n        }, {\n            \"type\": \"raw-json\",\n            \"label\": \"Maya 2020 Environments\",\n            \"key\": \"maya_2020_environments\"\n        }, {\n            \"type\": \"path\",\n            \"key\": \"maya_2020_executables\",\n            \"label\": \"Maya 2020 - Full paths to executables\",\n            \"multiplatform\": true,\n            \"multipath\": true\n        }, {\n            \"type\": \"raw-json\",\n            \"label\": \"Maya 2021 Environments\",\n            \"key\": \"maya_2021_environments\"\n        }, {\n            \"type\": \"path\",\n            \"key\": \"maya_2021_executables\",\n            \"label\": \"Maya 2021 - Full paths to executables\",\n            \"multiplatform\": true,\n            \"multipath\": true\n        }\n    ]\n}\n```\n\nTemplate data can be used only to fill templates in values but not in keys. It is also possible to define default values for unfilled fields to do so one of the items in the list must be a dictionary with key \"__default_values__\"` and value as dictionary with default key: values (as in example above).\n```javascript\n{\n    ...\n    // Allowed\n    \"key\": \"{to_fill}\"\n    ...\n    // Not allowed\n    \"{to_fill}\": \"value\"\n    ...\n}\n```\n\nBecause formatting values can be only string it is possible to use formatting values which are replaced with different types.\n```javascript\n// Template data\n{\n    \"template_data\": {\n        \"executable_multiplatform\": {\n            \"type\": \"schema\",\n            \"name\": \"my_multiplatform_schema\"\n        }\n    }\n}\n// Template content\n{\n    ...\n    // Allowed - value is replaced with dictionary\n    \"multiplatform\": \"{executable_multiplatform}\"\n    ...\n    // Not allowed - there is no way how it could be replaced\n    \"multiplatform\": \"{executable_multiplatform}_enhanced_string\"\n    ...\n}\n```\n\n#### dynamic_schema\nDynamic schema item marks a place in settings schema where schemas defined by `BaseModuleSettingsDef` can be placed.\n- example:\n```javascript\n{\n    \"type\": \"dynamic_schema\",\n    \"name\": \"project_settings/global\"\n}\n```\n- `BaseModuleSettingsDef` with implemented `get_settings_schemas` can return a dictionary where key defines a dynamic schema name and value schemas that will be put there\n- dynamic schemas work almost the same way as templates\n    - one item can be replaced by multiple items (or by 0 items)\n- goal is to dynamically load settings of OpenPype modules without having their schemas or default values in core repository\n    - values of these schemas are saved using the `BaseModuleSettingsDef` methods\n- we recommend to use `JsonFilesSettingsDef` which has full implementation of storing default values to json files\n    - requires only to implement method `get_settings_root_path` which should return path to root directory where settings schema can be found and default values will be saved\n\n### Basic Dictionary inputs\nThese inputs wraps another inputs into {key: value} relation\n\n#### dict\n- this is dictionary type wrapping more inputs with keys defined in schema\n- may be used as dynamic children (e.g. in [list](#list) or [dict-modifiable](#dict-modifiable))\n\t- in that case the only key modifier is `children` which is a list of its keys\n\t- USAGE: e.g. List of dictionaries where each dictionary has the same structure.\n- if is not used as dynamic children then must have defined `\"key\"` under which are it's values stored\n- may be with or without `\"label\"` (only for GUI)\n\t- `\"label\"` must be set to be able to mark item as group with `\"is_group\"` key set to True\n- item with label can visually wrap its children\n    - this option is enabled by default to turn off set `\"use_label_wrap\"` to `False`\n    - label wrap is by default collapsible\n        - that can be set with key `\"collapsible\"` to `True`/`False`\n        - with key `\"collapsed\"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`)\n    - it is possible to add lighter background with `\"highlight_content\"` (Default: `False`)\n        - lighter background has limits of maximum applies after 3-4 nested highlighted items there is not much difference in the color\n    - output is dictionary `{the \"key\": children values}`\n```javascript\n// Example\n{\n    \"key\": \"applications\",\n    \"type\": \"dict\",\n    \"label\": \"Applications\",\n    \"collapsible\": true,\n    \"highlight_content\": true,\n    \"is_group\": true,\n    \"is_file\": true,\n    \"children\": [\n        ...ITEMS...\n    ]\n}\n\n// Without label\n{\n    \"type\": \"dict\",\n    \"key\": \"global\",\n    \"children\": [\n        ...ITEMS...\n    ]\n}\n\n// When used as widget\n{\n    \"type\": \"list\",\n    \"key\": \"profiles\",\n    \"label\": \"Profiles\",\n    \"object_type\": {\n        \"type\": \"dict\",\n        \"children\": [\n            {\n                \"key\": \"families\",\n                \"label\": \"Families\",\n                \"type\": \"list\",\n                \"object_type\": \"text\"\n            }, {\n                \"key\": \"hosts\",\n                \"label\": \"Hosts\",\n                \"type\": \"list\",\n                \"object_type\": \"text\"\n            }\n            ...\n        ]\n    }\n}\n```\n\n#### dict-roots\n- entity can be used only in Project settings\n- keys of dictionary are based on current project roots\n- they are not updated \"live\" it is required to save root changes and then\n    modify values on this entity\n    # TODO do live updates\n```javascript\n{\n    \"type\": \"dict-roots\",\n    \"key\": \"roots\",\n    \"label\": \"Roots\",\n    \"object_type\": {\n        \"type\": \"path\",\n        \"multiplatform\": true,\n        \"multipath\": false\n    }\n}\n```\n\n#### dict-conditional\n- is similar to `dict` but has always available one enum entity\n    - the enum entity has single selection and it's value define other children entities\n- each value of enumerator have defined children that will be used\n    - there is no way how to have shared entities across multiple enum items\n- value from enumerator is also stored next to other values\n    - to define the key under which will be enum value stored use `enum_key`\n    - `enum_key` must match key regex and any enum item can't have children with same key\n    - `enum_label` is label of the entity for UI purposes\n- enum items are define with `enum_children`\n    - it's a list where each item represents single item for the enum\n    - all items in `enum_children` must have at least `key` key which represents value stored under `enum_key`\n    - enum items can define `label` for UI purposes\n    - most important part is that item can define `children` key where are definitions of it's children (`children` value works the same way as in `dict`)\n- to set default value for `enum_key` set it with `enum_default`\n- entity must have defined `\"label\"` if is not used as widget\n- is set as group if any parent is not group (can't have children as group)\n- may be with or without `\"label\"` (only for GUI)\n\t- `\"label\"` must be set to be able to mark item as group with `\"is_group\"` key set to True\n- item with label can visually wrap its children\n\t- this option is enabled by default to turn off set `\"use_label_wrap\"` to `False`\n\t- label wrap is by default collapsible\n\t\t- that can be set with key `\"collapsible\"` to `True`/`False`\n\t\t- with key `\"collapsed\"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`)\n\t- it is possible to add lighter background with `\"highlight_content\"` (Default: `False`)\n\t\t- lighter background has limits of maximum applies after 3-4 nested highlighted items there is not much difference in the color\n- for UI purposes was added `enum_is_horizontal` which will make combobox appear next to children inputs instead of on top of them (Default: `False`)\n\t- this has extended ability of `enum_on_right` which will move combobox to right side next to children widgets (Default: `False`)\n- output is dictionary `{the \"key\": children values}`\n- using this type as template item for list type can be used to create infinite hierarchies\n\n```javascript\n// Example\n{\n    \"type\": \"dict-conditional\",\n    \"key\": \"my_key\",\n    \"label\": \"My Key\",\n    \"enum_key\": \"type\",\n    \"enum_label\": \"label\",\n    \"enum_children\": [\n        // Each item must be a dictionary with 'key'\n        {\n            \"key\": \"action\",\n            \"label\": \"Action\",\n            \"children\": [\n                {\n                    \"type\": \"text\",\n                    \"key\": \"key\",\n                    \"label\": \"Key\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"label\",\n                    \"label\": \"Label\"\n                },\n                {\n                    \"type\": \"text\",\n                    \"key\": \"command\",\n                    \"label\": \"Command\"\n                }\n            ]\n        },\n        {\n            \"key\": \"menu\",\n            \"label\": \"Menu\",\n            \"children\": [\n                {\n                    \"key\": \"children\",\n                    \"label\": \"Children\",\n                    \"type\": \"list\",\n                    \"object_type\": \"text\"\n                }\n            ]\n        },\n        {\n            // Separator does not have children as \"separator\" value is enough\n            \"key\": \"separator\",\n            \"label\": \"Separator\"\n        }\n    ]\n}\n```\n\nHow output of the schema could look like on save:\n```javascript\n{\n    \"type\": \"separator\"\n}\n\n{\n    \"type\": \"action\",\n    \"key\": \"action_1\",\n    \"label\": \"Action 1\",\n    \"command\": \"run command -arg\"\n}\n\n{\n    \"type\": \"menu\",\n    \"children\": [\n        \"child_1\",\n        \"child_2\"\n    ]\n}\n```\n\n### Inputs for setting any kind of value (`Pure` inputs)\n- all inputs must have defined `\"key\"` if are not used as dynamic item\n    - they can also have defined `\"label\"`\n\n#### boolean\n- simple checkbox, nothing more to set\n```javascript\n{\n    \"type\": \"boolean\",\n    \"key\": \"my_boolean_key\",\n    \"label\": \"Do you want to use Pype?\"\n}\n```\n\n#### number\n- number input, can be used for both integer and float\n    - key `\"decimal\"` defines how many decimal places will be used, 0 is for integer input (Default: `0`)\n    - key `\"minimum\"` as minimum allowed number to enter (Default: `-99999`)\n    - key `\"maximum\"` as maximum allowed number to enter (Default: `99999`)\n- key `\"steps\"` will change single step value of UI inputs (using arrows and wheel scroll)\n- for UI it is possible to show slider to enable this option set `show_slider` to `true`\n```javascript\n{\n    \"type\": \"number\",\n    \"key\": \"fps\",\n    \"label\": \"Frame rate (FPS)\"\n    \"decimal\": 2,\n    \"minimum\": 1,\n    \"maximum\": 300000\n}\n```\n\n```javascript\n{\n    \"type\": \"number\",\n    \"key\": \"ratio\",\n    \"label\": \"Ratio\"\n    \"decimal\": 3,\n    \"minimum\": 0,\n    \"maximum\": 1,\n    \"show_slider\": true\n}\n```\n\n#### text\n- simple text input\n    - key `\"multiline\"` allows to enter multiple lines of text (Default: `False`)\n    - key `\"placeholder\"` allows to show text inside input when is empty (Default: `None`)\n\n```javascript\n{\n    \"type\": \"text\",\n    \"key\": \"deadline_pool\",\n    \"label\": \"Deadline pool\"\n}\n```\n\n#### path-input\n- Do not use this input in schema please (use `path` instead)\n- this input is implemented to add additional features to text input\n- this is meant to be used in proxy input `path`\n\n#### raw-json\n- a little bit enhanced text input for raw json\n- can store dictionary (`{}`) or list (`[]`) but not both\n    - by default stores dictionary to change it to list set `is_list` to `True`\n- has validations of json format\n- output can be stored as string\n    - this is to allow any keys in dictionary\n    - set key `store_as_string` to `true`\n    - code using that setting must expected that value is string and use json module to convert it to python types\n\n```javascript\n{\n    \"type\": \"raw-json\",\n    \"key\": \"profiles\",\n    \"label\": \"Extract Review profiles\",\n    \"is_list\": true\n}\n```\n\n#### enum\n- enumeration of values that are predefined in schema\n- multiselection can be allowed with setting key `\"multiselection\"` to `True` (Default: `False`)\n- values are defined under value of key `\"enum_items\"` as list\n    - each item in list is simple dictionary where value is label and key is value which will be stored\n    - should be possible to enter single dictionary if order of items doesn't matter\n- it is possible to set default selected value/s with `default` attribute\n    - it is recommended to use this option only in single selection mode\n    - at the end this option is used only when defying default settings value or in dynamic items\n\n```javascript\n{\n    \"key\": \"tags\",\n    \"label\": \"Tags\",\n    \"type\": \"enum\",\n    \"multiselection\": true,\n    \"enum_items\": [\n        {\"burnin\": \"Add burnins\"},\n        {\"ftrackreview\": \"Add to Ftrack\"},\n        {\"delete\": \"Delete output\"},\n        {\"slate-frame\": \"Add slate frame\"},\n        {\"no-handles\": \"Skip handle frames\"}\n    ]\n}\n```\n\n#### anatomy-templates-enum\n- enumeration of all available anatomy template keys\n- have only single selection mode\n- it is possible to define default value `default`\n    - `\"work\"` is used if default value is not specified\n- enum values are not updated on the fly it is required to save templates and\n    reset settings to recache values\n```javascript\n{\n    \"key\": \"host\",\n    \"label\": \"Host name\",\n    \"type\": \"anatomy-templates-enum\",\n    \"default\": \"publish\"\n}\n```\n\n#### hosts-enum\n- enumeration of available hosts\n- multiselection can be allowed with setting key `\"multiselection\"` to `True` (Default: `False`)\n- it is possible to add empty value (represented with empty string) with setting `\"use_empty_value\"` to `True` (Default: `False`)\n- it is possible to set `\"custom_labels\"` for host names where key `\"\"` is empty value (Default: `{}`)\n- to filter host names it is required to define `\"hosts_filter\"` which is list of host names that will be available\n    - do not pass empty string if `use_empty_value` is enabled\n    - ignoring host names would be more dangerous in some cases\n```javascript\n{\n    \"key\": \"host\",\n    \"label\": \"Host name\",\n    \"type\": \"hosts-enum\",\n    \"multiselection\": false,\n    \"use_empty_value\": true,\n    \"custom_labels\": {\n        \"\": \"N/A\",\n        \"nuke\": \"Nuke\"\n    },\n    \"hosts_filter\": [\n        \"nuke\"\n    ]\n}\n```\n\n#### apps-enum\n- enumeration of available application and their variants from system settings\n    - applications without host name are excluded\n- can be used only in project settings\n- has only `multiselection`\n- used only in project anatomy\n```javascript\n{\n    \"type\": \"apps-enum\",\n    \"key\": \"applications\",\n    \"label\": \"Applications\"\n}\n```\n\n#### tools-enum\n- enumeration of available tools and their variants from system settings\n- can be used only in project settings\n- has only `multiselection`\n- used only in project anatomy\n```javascript\n{\n    \"type\": \"tools-enum\",\n    \"key\": \"tools_env\",\n    \"label\": \"Tools\"\n}\n```\n\n#### task-types-enum\n- enumeration of task types from current project\n- enum values are not updated on the fly and modifications of task types on project require save and reset to be propagated to this enum\n- has set `multiselection` to `True` but can be changed to `False` in schema\n\n#### deadline_url-enum\n- deadline module specific enumerator using deadline system settings to fill it's values\n- TODO: move this type to deadline module\n\n### Inputs for setting value using Pure inputs\n- these inputs also have required `\"key\"`\n- attribute `\"label\"` is required in few conditions\n    - when item is marked `as_group` or when `use_label_wrap`\n- they use Pure inputs \"as widgets\"\n\n#### list\n- output is list\n- items can be added and removed\n- items in list must be the same type\n- to wrap item in collapsible widget with label on top set `use_label_wrap` to `True`\n    - when this is used `collapsible` and `collapsed` can be set (same as `dict` item does)\n- type of items is defined with key `\"object_type\"`\n- there are 2 possible ways how to set the type:\n    1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `\"type\"` (example below)\n    2.) item type name as string without modifiers (e.g. [text](#text))\n    3.) enhancement of 1.) there is also support of `template` type but be careful about endless loop of templates\n        - goal of using `template` is to easily change same item definitions in multiple lists\n\n1.) with item modifiers\n```javascript\n{\n    \"type\": \"list\",\n    \"key\": \"exclude_ports\",\n    \"label\": \"Exclude ports\",\n    \"object_type\": {\n        \"type\": \"number\", # number item type\n        \"minimum\": 1, # minimum modifier\n        \"maximum\": 65535 # maximum modifier\n    }\n}\n```\n\n2.) without modifiers\n```javascript\n{\n    \"type\": \"list\",\n    \"key\": \"exclude_ports\",\n    \"label\": \"Exclude ports\",\n    \"object_type\": \"text\"\n}\n```\n\n3.) with template definition\n```javascript\n// Schema of list item where template is used\n{\n    \"type\": \"list\",\n    \"key\": \"menu_items\",\n    \"label\": \"Menu Items\",\n    \"object_type\": {\n        \"type\": \"template\",\n        \"name\": \"template_object_example\"\n    }\n}\n\n// WARNING:\n//  In this example the template use itself inside which will work in `list`\n//  but may cause an issue in other entity types (e.g. `dict`).\n\n'template_object_example.json' :\n[\n    {\n        \"type\": \"dict-conditional\",\n        \"use_label_wrap\": true,\n        \"collapsible\": true,\n        \"key\": \"menu_items\",\n        \"label\": \"Menu items\",\n        \"enum_key\": \"type\",\n        \"enum_label\": \"Type\",\n        \"enum_children\": [\n            {\n                \"key\": \"action\",\n                \"label\": \"Action\",\n                \"children\": [\n                    {\n                        \"type\": \"text\",\n                        \"key\": \"key\",\n                        \"label\": \"Key\"\n                    }\n                ]\n            }, {\n                \"key\": \"menu\",\n                \"label\": \"Menu\",\n                \"children\": [\n                    {\n                        \"key\": \"children\",\n                        \"label\": \"Children\",\n                        \"type\": \"list\",\n                        \"object_type\": {\n                            \"type\": \"template\",\n                            \"name\": \"template_object_example\"\n                        }\n                    }\n                ]\n            }\n        ]\n    }\n]\n```\n\n#### dict-modifiable\n- one of dictionary inputs, this is only used as value input\n- items in this input can be removed and added same way as in `list` input\n- value items in dictionary must be the same type\n- required keys may be defined under `\"required_keys\"`\n    - required keys must be defined as a list (e.g. `[\"key_1\"]`) and are moved to the top\n    - these keys can't be removed or edited (it is possible to edit label if item is collapsible)\n- type of items is defined with key `\"object_type\"`\n    - there are 2 possible ways how to set the object type (Examples below):\n    1. just a type name as string without modifiers (e.g. `\"text\"`)\n    2. full types with modifiers as dictionary(`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `\"type\"`\n- this input can be collapsible\n    - `\"use_label_wrap\"` must be set to `True` (Default behavior)\n    - that can be set with key `\"collapsible\"` as `True`/`False` (Default: `True`)\n        - with key `\"collapsed\"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`)\n\n1. **Object type** without modifiers\n```javascript\n{\n    \"type\": \"dict-modifiable\",\n    \"object_type\": \"text\",\n    \"is_group\": true,\n    \"key\": \"templates_mapping\",\n    \"label\": \"Deadline - Templates mapping\",\n    \"is_file\": true\n}\n```\n\n2. **Object type** with item modifiers\n```javascript\n{\n    \"type\": \"dict-modifiable\",\n    \"object_type\": {\n        \"type\": \"number\",\n        \"minimum\": 0,\n        \"maximum\": 300\n    },\n    \"is_group\": true,\n    \"key\": \"templates_mapping\",\n    \"label\": \"Deadline - Templates mapping\",\n    \"is_file\": true\n}\n```\n\n#### path\n- input for paths, use `path-input` internally\n- has 2 input modifiers `\"multiplatform\"` and `\"multipath\"`\n    - `\"multiplatform\"` - adds `\"windows\"`, `\"linux\"` and `\"darwin\"` path inputs (result is dictionary)\n    - `\"multipath\"` - it is possible to enter multiple paths\n    - if both are enabled result is dictionary with lists\n\n```javascript\n{\n    \"type\": \"path\",\n    \"key\": \"ffmpeg_path\",\n    \"label\": \"FFmpeg path\",\n    \"multiplatform\": true,\n    \"multipath\": true\n}\n```\n\n#### list-strict\n- input for strict number of items in list\n- each child item can be different type with different possible modifiers\n- it is possible to display them in horizontal or vertical layout\n    - key `\"horizontal\"` as `True`/`False` (Default: `True`)\n- each child may have defined `\"label\"` which is shown next to input\n    - label does not reflect modifications or overrides (TODO)\n- children item are defined under key `\"object_types\"` which is list of dictionaries\n    - key `\"children\"` is not used because is used for hierarchy validations in schema\n- USAGE: For colors, transformations, etc. Custom number and different modifiers\n  give ability to define if color is HUE or RGB, 0-255, 0-1, 0-100 etc.\n\n```javascript\n{\n    \"type\": \"list-strict\",\n    \"key\": \"color\",\n    \"label\": \"Color\",\n    \"object_types\": [\n        {\n            \"label\": \"Red\",\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"maximum\": 255,\n            \"decimal\": 0\n        }, {\n            \"label\": \"Green\",\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"maximum\": 255,\n            \"decimal\": 0\n        }, {\n            \"label\": \"Blue\",\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"maximum\": 255,\n            \"decimal\": 0\n        }, {\n            \"label\": \"Alpha\",\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"maximum\": 1,\n            \"decimal\": 6\n        }\n    ]\n}\n```\n\n#### color\n- pre implemented entity to store and load color values\n- entity store and expect list of 4 integers in range 0-255\n    - integers represents rgba [Red, Green, Blue, Alpha]\n- has modifier `\"use_alpha\"` which can be `True`/`False`\n    - alpha is always `255` if set to `True` and alpha slider is not visible in UI\n\n```javascript\n{\n    \"type\": \"color\",\n    \"key\": \"bg_color\",\n    \"label\": \"Background Color\"\n}\n```\n\n### Anatomy\nAnatomy represents data stored on project document. Item cares about **Project Anatomy**.\n\n#### anatomy\n- entity is just enhanced [dict](#dict) item\n- anatomy has always all keys overridden with overrides\n\n### Noninteractive items\nItems used only for UI purposes.\n\n#### label\n- add label with note or explanations\n- it is possible to use html tags inside the label\n- set `work_wrap` to `true`/`false` if you want to enable word wrapping in UI (default: `false`)\n\n```javascript\n{\n    \"type\": \"label\",\n    \"label\": \"<span style=\\\"color:#FF0000\\\";>RED LABEL:</span> Normal label\"\n}\n```\n\n#### separator\n- legacy name is `splitter` (still usable)\n- visual separator of items (more divider than separator)\n\n```javascript\n{\n    \"type\": \"separator\"\n}\n```\n\n### Proxy wrappers\n- should wrap multiple inputs only visually\n- these do not have `\"key\"` key and do not allow to have `\"is_file\"` or `\"is_group\"` modifiers enabled\n- can't be used as a widget (first item in e.g. `list`, `dict-modifiable`, etc.)\n\n#### form\n- wraps inputs into form look layout\n- should be used only for Pure inputs\n\n```javascript\n{\n    \"type\": \"dict-form\",\n    \"children\": [\n        {\n            \"type\": \"text\",\n            \"key\": \"deadline_department\",\n            \"label\": \"Deadline apartment\"\n        }, {\n            \"type\": \"number\",\n            \"key\": \"deadline_priority\",\n            \"label\": \"Deadline priority\"\n        }, {\n           ...\n        }\n    ]\n}\n```\n\n\n#### collapsible-wrap\n- wraps inputs into collapsible widget\n    - looks like `dict` but does not hold `\"key\"`\n- should be used only for Pure inputs\n\n```javascript\n{\n    \"type\": \"collapsible-wrap\",\n    \"label\": \"Collapsible example\"\n    \"children\": [\n        {\n            \"type\": \"text\",\n            \"key\": \"_example_input_collapsible\",\n            \"label\": \"Example input in collapsible wrapper\"\n        }, {\n           ...\n        }\n    ]\n}\n```\n\n\n## How to add new settings\nAlways start with modifying or adding a new schema and don't worry about values. When you think schema is ready to use launch OpenPype settings in development mode using `poetry run python ./start.py settings --dev` or prepared script in `~/openpype/tools/run_settings(.sh|.ps1)`. Settings opened in development mode have the checkbox `Modify defaults` available in the bottom left corner. When checked default values are modified and saved on `Save`. This is a recommended approach on how default settings should be created instead of direct modification of files.\n\n![Modify default settings](assets/settings_dev.png)\n"
  },
  {
    "path": "website/docs/dev_testing.md",
    "content": "---\nid: dev_testing\ntitle: Testing in OpenPype\nsidebar_label: Testing\n---\n\n## Introduction\nAs OpenPype is growing there also grows need for automatic testing. There are already bunch of tests present in root folder of OpenPype directory.\nBut many tests should yet be created!\n\n### How to run (integration) tests\n\n#### Requirements\n- installed DCC you want to test\n- `mongorestore` on a PATH\n\nYou could check that `mongorestore` is available by running this in console (or cmd), it shouldn't fail and you should see version of utility:\n```commandline\nmongorestore --version\n```\n\nIf you would like just to experiment with provided integration tests, and have particular DCC installed on your machine, you could run test for this host by:\n\n- From source:\n```\n- use Openpype command 'runtests' from command line (`.venv` in ${OPENPYPE_ROOT} must be activated to use configured Python!)\n- `python ${OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/nuke`\n```\n- From build:\n```\n- ${OPENPYPE_BUILD}/openpype_console runtests {ABSOLUTE_PATH_OPENPYPE_ROOT}/tests/integration/hosts/nuke`\n```\nModify tests path argument to limit which tests should be run (`../tests/integration` will run all implemented integration tests).\n\n### Content of tests folder\n\nMain tests folder contains hierarchy of folders with tests and supporting lib files. It is intended that tests in each folder of the hierarchy could be run separately.\n\nMain folders in the structure:\n- `integration` - end to end tests in host applications, mimicking regular publishing process \n- `lib` - helper classes\n- `resources` - test data skeletons etc.\n- `unit` - unit test covering methods and functions in OP\n\n\n### lib folder\n\nThis location should contain library of helpers and miscellaneous classes used for integration or unit tests.\n\nContent:\n- `assert_classes.py` - helpers for easier use of assert expressions\n- `db_handler.py` - class for creation of DB dumps/restore/purge\n- `file_hanlder.py` - class for preparation/cleanup of test data\n- `testing_classes.py` - base classes for testing of publish in various DCCs\n\n### integration folder\n\nContains end to end testing in a DCC. Currently it is setup to start DCC application with prepared worfkile, run publish process and compare results in DB and file system automatically.\nThis approach is implemented as it should work in any DCC application and should cover most common use cases. Not all hosts allow \"real headless\" publishing, but all hosts should allow to trigger \npublish process programmatically when UI of host is actually running.\n\nThere will be eventually also possibility to build workfile and publish it programmatically, this would work only in DCCs that support it (Maya, Nuke).\n\nIt is expected that each test class should work with single worfkile with supporting resources (as a dump of project DB, all necessary environment variables, expected published files etc.)\n\nThere are currently implemented basic publish tests for `Maya`, `Nuke`, `AfterEffects` and `Photoshop`. Additional hosts will be added.\n\nEach `test_` class should contain single test class based on `tests.lib.testing_classes.PublishTest`. This base class handles all necessary \nfunctionality for testing in a host application.\n\n#### Steps of publish test\n\nEach publish test consists of areas: \n- preparation\n- launch of host application\n- publish \n- comparison of results in DB and file system\n- cleanup\n\n##### Preparation\n\nEach test publish case expects zip file with this structure:\n- `expected` - published files after workfile is published (in same structure as in regular manual publish)\n- `input`\n    - `dumps` - database dumps (check `tests.lib.db_handler` for implemented functionality)\n        - `openpype` - settings \n        - `test_db` - skeleton of test project (contains project document, asset document etc.)\n    - `env_vars` - `env_var.json` file with a dictionary of all required environment variables\n    - `json` - json files with human readable content of databases\n    - `startup` - any required initialization scripts (for example Nuke requires one `init.py` file)\n    - `workfile` - contains single workfile\n    \nThese folders needs to be zipped (in zip's root must be this structure directly!), currently zip files for all prepared tests are stored in OpenPype GDrive folder.\n\nEach test then goes in steps (by default):\n- download test data zip\n- create temporary folder and unzip there data zip file\n- purge test DB if exists, import dump files from unzipped folder\n- sets environment variables from `env_vars` folder\n- launches host application and trigger publish process\n- waits until publish process finishes, application closes (or timeouts)\n- compares results in DB with expected values\n- compares published files structure with expected values\n- cleans up temporary test DB and folder\n\n##### Launch of application and publish\n\nIntegration tests are using same approach as OpenPype process regarding launching of host applications (eg. `ApplicationManager().launch`).\nEach host application is in charge of triggering of publish process and closing itself. Different hosts handle this differently, Adobe products are handling this via injected \"HEADLESS_PUBLISH\" environment variable,\nMaya and Nuke must contain this in theirs startup files.\n\nBase `PublishTest` class contains configurable timeout in case of publish process is not working, or taking too long.\n\n##### Comparison of results\n\nEach test class requires re-iplemented `PublishTest.test_db_asserts` fixture. This method is triggered after publish is finished and should\ncompare current results in DB (each test has its own database which gets filled with dump data first, cleaned up after test finishing) with expected results.\n\n`tests.lib.assert_classes.py` contains prepared method `count_of_types` which makes easier to write assert expression. This method also produces formatted error message.\n\nBasic use case:\n```DBAssert.count_of_types(dbcon, \"version\", 2)``` >> It is expected that DB contains only 2 documents of `type==version`\n\nIf zip file contains file structure in `expected` folder, `PublishTest.test_folder_structure_same` implements comparison of expected and published file structure,\neg. if test case published all expected files.\n\n##### Cleanup\n\nBy default, each test case pulls data from GDrive, unzips them in temporary folder, runs publish, compares results and then\npurges created temporary test database and temporary folder. This could be changed by setting of `PublishTest.PERSIST`. If set to True, DB and published folder are kept intact\nuntil next run of any test.\n\nIn case you want to modify test data, use `PublishTest.TEST_DATA_FOLDER` to point test to specific location where test folder is already unzipped.\n\nBoth options are mostly useful for debugging during implementation of new test cases.\n\n#### Test configuration\n\nEach test case could be configured from command line with:\n- `test_data_folder` - use specific folder with extracted test zip file \n- `persist` - keep content of temporary folder and database after test finishes\n- `app_variant` - run test for specific version of host app, matches app variants in Settings, eg. `2021` for Photoshop, `12-2` for Nuke\n- `timeout` - override default time (in seconds)\n\n### unit folder\n\nHere should be located unit tests for classes, methods of OpenPype etc. As most classes expect to be triggered in OpenPype context, best option is to\nstart these tests in similar fashion as `integration` tests (eg. via `runtests`)."
  },
  {
    "path": "website/docs/features.md",
    "content": "## Pype tray\n\nFtrack\n\nLogin\n\nreset Action server\n\nLauncher: Launch applications without the need of going through ftrack website\n\nLibrary: Browse through all the published assets across the projects. You can also launch actions.\n\nStandalone Publisher\n\nServices\n\nIdle manager\n\nTimers manager\n\nStatistics server\n\n## System Admin\n\nManage environments per project/shot/tasks\n\nCentralized pipeline installation\n\nLocalized Python environment (for speed purposes)\n\nAutomatic user environment updates (online/offline)\n\nAbility to run completely offline for TPN and MPAA certified sites\n\nGit controlled deployment\n\nSeparated development and production installation for safety and testing\n\nPer project pipeline configuration overrides\n\nLinux, Windows, Mac support\n\nMongoDB backbone\n\n## Ftrack\n\nLaunch applications\n\nCustom actions\n\nCreate base project structure\n\nCreate Folders\n\nSync to Avalon\n\nPropagate Thumbnails\n\nCreate required custom attributes\n\nlaunch version in RV / DJVview\n\nDelete assets and subsets\n\nSort Client Review\n\nKill old Ftrack jobs\n\nEvent server (automatically triggered actions)\n\nSync to Avalon\n\nUpdate status on the next task\n\nPropagate Thumbnails from version to tasks and assets/shots\n\nPropagate statuses between versions and tasks\n\nAvalon <-> Ftrack sync\n\n## Maya\n\n### Tools\n\nCreator\n\nPublisher\n\nLoader\n\nScene Inventory\n\nLook assigner\n\nWorkfiles\n\n### Families\n\nModel\n\nLook\n\nRig\n\nAnimation\n\nCache\n\nCamera\n\nAssembly\n\nMayaAscii (generic scene)\n\nSetdress\n\nRenderSetup\n\nReview\n\narnoldStandin\n\nvrayProxy\n\nvrayScene\n\nyetiCache\n\nyetiRig\n\n## Houdini\n\n### Tools\n\nCreator\n\nPublisher\n\nLoader\n\nScene Inventory\n\nLook assigner\n\nWorkfiles\n\n### Families\n\nModel\n\nAnimation\n\nCache\n\nCamera\n\nReview\n\n## Nuke\n\nTools\n\nPublisher\n\nLoader\n\nScene Inventory\n\nWorkfiles\n\nFamilies\n\nModel (load only)\n\nCamera (load only)\n\nRender\n\nReview\n\nPlate\n\nPrerender\n\n## NukeStudio\n\nCreate Shots in Ftrack and Avalon\n\nhandles\n\nframe ranges\n\nedit in and edit out\n\nPublish Plates\n\nAny number of plate\n\ncolorspace managed\n\nAttach preview quicktimes to Ftrack versions\n\n## Fusion\n\nTools\n\nPublisher\n\nLoader\n\nScene Inventory\n\nWorkfiles\n\nFamilies\n\nModel (load only)\n\nCamera (load only)\n\nRender\n\nReview\n\nPlate\n\nPrerender\n\n## Deadline\n\nPublish to deadline from\n\nMaya\n\nNuke\n\nCreate preview quicktimes from rendered frames\n\npublish rendered outputs to Avalon and Ftrack\n\n## Royal Render\nPublish to Royal Render from\nMaya\nNuke\n\n\n## Clockify\n\nAutomatic timer start and stop in sync with Ftrack.\n\n## Arnold\n\n## Vray\n\n## Redshift\n"
  },
  {
    "path": "website/docs/manager_ftrack.md",
    "content": "---\nid: manager_ftrack\ntitle: Ftrack\nsidebar_label: Project Manager\n---\n\nFtrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and its basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](https://help.ftrack.com/en/).\n\n## Project management\nSetting project attributes is the key to properly working pipeline.\n\n### New Project\nThe best practice for creating a new project ready for OpenPype.\n1. First of all you must [create a project](http://ftrack.rtd.ftrack.com/en/stable/using/managing_projects/creating_a_new_project.html) in Ftrack.\n2. All the required attributes need to be populated. The easiest way to do it is by using [Prepare Project](manager_ftrack_actions.md#prepare-project) action.\n\n:::tip\nDo not forget to set up `applications` and `tools`, otherwise users won't be able to launch applications.\n:::\n\n3. Now you can create Project hierarchy with shots, assets, tasks and others, which has [specific rules](#synchronization-rules). [Create Project Structure](manager_ftrack_actions.md#create-project-structure) action may help you with this step.\n4. Last step is to [synchronize](#synchronization-to-avalon-database) project to Avalon database.\n\n:::tip\nTurn on `auto-sync` attribute on your project in ftrack. That way you'll only need to synchronise the project once and all further changes will be propagated automatically.\n:::\n\n## Synchronization to Avalon database\nThis process describes how data from Ftrack will get into Avalon database.\n\n### How to synchronize\nYou can trigger synchronization manually using [Sync To Avalon](manager_ftrack_actions.md#sync-to-avalon) action.\n\nSynchronization can also be automated with OpenPype's [event server](#event-server) and synchronization events. If your Ftrack is [prepared for OpenPype](module_ftrack.md#prepare-ftrack-for-openpype), the project should have custom attribute `Avalon auto-sync`. Check the custom attribute to allow auto-updates with event server.\n\n:::tip\nAlways use `Sync To Avalon` action before you enable `Avalon auto-sync`!\n:::\n\n:::important\nSynchronization actions and events can show you interface with information when something goes differently than expected. Just read carefully what happened messages should guide you.\n:::\n\n### Synchronization rules\nRequired:\n- entity can only contain **letters**, **numbers** and **underscore** symbols.  *(In technical terms: all names must match regex: `^[a-zA-Z0-9_.]*$`)`*\n\nNot allowed:\n- duplicated entity names within project (there can be only one shot with name \"sh0010\" in whole project for example)\n- have any **Tasks** directly on the *Project* level\n\n### Managing Entities\n\nThere are certain situations that are very hard, or even impossible to handle automatically and will have be resolved by your TD. These include\n\n- Deleting shots and assets after some data has already been published in them.\n- Re-structuring the project hierarchy when work is already being done.\n- Renaming the Project\n\nIf you need to move entity or change its name it is possible only in the acse when no-one has worked on it yet. Once work is in progreess, you must archive the old one and create new.\n\nTo archive entities you should use [Archive Asset/Subset](manager_ftrack_actions.md#delete-asset/subset) action. This will remove the selected entity from ftrack and avalon database in mostly non-destructive way, so it can be recovered later. To completely delete all traces of this such entity you'll need to go to OpenPype archive and delete them from there.\n"
  },
  {
    "path": "website/docs/manager_ftrack_actions.md",
    "content": "---\nid: manager_ftrack_actions\ntitle: Ftrack Actions\nsidebar_label: Ftrack actions\n---\n\nActions are small useful tools that help artists, managers and administrators.\nTo avoid overfilled action menu some actions are filtered by entity types and some of them by user roles permissions.\n\nIn most cases actions filtered by entity type:\n- Project\n- Typed Context\n    - Folder\n    - Episode\n    - Sequence\n    - Shot\n    - Library\n    - Asset Build\n    - Asset Variant\n    - Epic\n    - Milestone\n- Task\n- Asset Version\n- Component\n- Review Session\n\n*Typed Context* is global Ftrack entity for hierarchical types representing all of them. Hierarchical types can be used for filtering too, but because they are dynamic *(you can add, modify and remove them)*, *Typed Context* is used to be more general.\n\nSo if you do not see action you need to use check if action is available for selected *entity type* or ask *administrator* to check if you have permissions to use it.\n\n:::note\nActions can be heavily customised by your studio, so this guide might not fit 100 %.\n:::\n\n:::important\nFiltering can be more complicated for example a lot of actions can be shown only when one particular entity is selected.\n:::\n\n---\n## Applications\n\n### Launch applications\n* Entity types: Task\n* User roles: All\n\nThese actions *launch application with OpenPype * and *start timer* for the selected Task. We recommend you to launch application this way.\n\n:::important\nProject Manager or Supervisor must set project's applications during project preparation otherwise you won't see them. Applications can be added even if the project is in progress.\n:::\n\n---\n<div class=\"row markdown\">\n<div class=\"col col--10 markdown\">\n\n## OpenPype Admin\n\n</div>\n<div class=\"col col--2 markdown\">\n\n![pype_admin-icon](assets/ftrack/ftrack-pype_admin-icon.png)\n\n</div>\n</div>\n\n\n#### A group of actions that are used for OpenPype Administration.\n\n### Sync to Avalon\n* Entity types: Project, Typed Context\n* User roles: Pypeclub, Administrator, Project manager\n\nSynchronization to Avalon is key process to keep OpenPype data updated. Action updates selected entities (Project, Shot, Sequence, etc.) and all nested entities to Avalon database. If action is successfully finished [Sync Hier Attrs](#sync-hier-attrs) action is triggered.\n\nThere are 2 versions of **Sync to Avalon**, first labeled as **server** second as **local**.\n* **server** version will be processed with [event server](module_ftrack.md#event-server)\n* **local** version will be processed with user's OpenPype tray application\n\nIt is recommended to use **local** version if possible to avoid unnecessary deceleration of event server.\n\n### Sync Hier Attrs\n* Entity types: Project, Typed Context\n* User roles: Pypeclub, Administrator, Project manager\n\nSynchronization to Avalon of Ftrack's hierarchical Custom attributes is a bit complicated so we decided to split synchronization process into 2 actions. This action updates hierarchical Custom attributes of selected entities (Project, Shot, Sequence, etc.) and all their nested entities to pipeline database. This action is also triggered automatically after successfully finished [Sync To Avalon](#sync-to-avalon) action.\n\nThere are 2 versions of **Sync Hier Attrs** first labeled as **server** second as **local**.\n* **server** version will be processed with [event server](module_ftrack.md#event-server)\n* **local** version will be processed with user's OpenPype application\n\nIt is recommended to use **local** version if possible to avoid unnecessary deceleration of event server.\n\n### Job Killer\n* Entity types: All\n* User roles: Pypeclub, Administrator\n\nCustom Jobs in Ftrack help to track process and status of triggered actions but sometimes unexpected failure of action may happen *(Please let us know when happens)*. The failure will cause that job's status will remain set to **Running** which may cause issues in future.\n\nThis action gives ability to *stop running jobs*. When action is triggered, an interface with all running jobs with checkbox next to each is shown. Status of checked jobs will be set to **Failure** on submit.\n\n### Delete Assets by Name\n* Entity types: Typed Context, Task\n* User roles: Pypeclub, Administrator\n\nWith this action it's possible to delete up to 15 entities at once from active project in pipeline database. Entered names must match exactly the names stored in database. These entities also must not have children entities *(Sequence must not have Shots but Task is not entity)*.\n\n---\n<div class=\"row markdown\">\n<div class=\"col col--10 markdown\">\n\n## Prepare Project\n\n</div>\n<div class=\"col col--2 markdown\">\n\n![prepare_project-icon](assets/ftrack/ftrack-prepare_project-icon.png)\n\n</div>\n</div>\n\n* Entity types: Project\n* User roles: Pypeclub, Administrator, Project manager\n\nAllows project managers and coordinator to *set basic project attributes* needed for OpenPype to operate, *Create project folders* if you want and especially prepare project specific [settings](admin_settings_project).\n\n:::tip\nIt is possible to use this action during the lifetime of a project but we recommend using it only once at the start of the project.\n:::\n\n![prepare_project_1](assets/ftrack/ftrack-prepare_project_1-small.png)\n\n---\n<div class=\"row markdown\">\n<div class=\"col col--10 markdown\">\n\n## Multiple Notes\n\n</div>\n<div class=\"col col--2 markdown\">\n\n![multiple_notes-icon](assets/ftrack/ftrack-multiple_notes-icon.png)\n\n</div>\n</div>\n\n* Entity types: Asset Version\n* User roles: All\n\nYou can add same note to multiple Asset Versions at once with this action.\n![multiple_notes_1](assets/ftrack/ftrack-multiple_notes_1-small.png)\n\n---\n<div class=\"row markdown\">\n<div class=\"col col--10 markdown\">\n\n## Delete Asset/Subset\n\n</div>\n<div class=\"col col--2 markdown\">\n\n![delete_asset-icon](assets/ftrack/ftrack-delete_asset-icon.png)\n\n</div>\n</div>\n\n* Entity types: Typed Context, Task\n* User roles: Pypeclub, Administrator\n\nAction deletes Entities and Asset Versions from Ftrack and Avalon database.\n\nYou should use this action if you need to delete Entities or Asset Versions otherwise deletion will not take effect in Avalon database. Currently the action allows to only delete one entity at the time. Entity also must not have any children.\n\n---\n<div class=\"row markdown\">\n<div class=\"col col--10 markdown\">\n\n## Create Project Structure\n\n</div>\n<div class=\"col col--2 markdown\">\n\n![create_project_folders-icon](assets/ftrack/ftrack-create_project_folders-icon.png)\n\n</div>\n</div>\n\n* Entity types: Project\n* User roles: Pypeclub, Administrator\n\n*Create Project Structure* helps to create basic folder structure and may create the main ftrack entities for the project.\n\nStructure is loaded from settings *(OpenPype Settings → Project → Global → Project Folder Structure)*. You should examine these settings to see how it works. Settings may contain dictionaries of nested dictionaries where each key represents a folder name. Key and all it's parents will be also created in Ftrack if the key ends with `[ftrack]`. Default Ftrack entity type is *Folder* but entity type can be specified using `[ftrack.{entity type}]`. To create *Sequence* with name *Seq_001* key should look like `Seq_001[ftrack.Sequence]`.\n\n:::note\nPlease keep in mind this action is meant to make your project setup faster at the very beginning, but it does not create folders for each shot and asset. For creating asset folder refer to `Create Folders` Action\n:::\n\n---\n<div class=\"row markdown\">\n<div class=\"col col--10 markdown\">\n\n## Delivery\n\n</div>\n<div class=\"col col--2 markdown\">\n\n![ftrack-delivery-icon](assets/ftrack/ftrack-delivery-icon.png)\n\n</div>\n</div>\n\n* Entity types: Task\n* User roles: Pypeclub, Project manager, Administrator\n\nCollects approved hires files and copy them into a folder. It takes any components of any versions and copies and renames them correctly.\n\n\n---\n<div class=\"row markdown\">\n<div class=\"col col--10 markdown\">\n\n## Create Folders\n\n</div>\n<div class=\"col col--2 markdown\">\n\n![create_folders-icon](assets/ftrack/ftrack-create_folders-icon.png)\n\n</div>\n</div>\n\n* Entity types: Typed Context, Task\n* User roles: All\n\nIt is usually not necessary to launch this action because folders are created automatically every time you start working on a task. However it can be handy if you need to create folders before any work begins or you want to use applications that don't have pipeline implementation.\n\n---\n<div class=\"row markdown\">\n<div class=\"col col--10 markdown\">\n\n## Thumbnail\n\n</div>\n<div class=\"col col--2 markdown\">\n\n![thumbnail-icon](assets/ftrack/ftrack-thumbnail-icon.png)\n\n</div>\n</div>\n\nA group of actions for thumbnail management.\n\n### Thumbnail to Parent\nPropagates the thumbnail of the selected entity to its parent.\n\n### Thumbnail to Children\nPropagates the thumbnail of the selected entity to its first direct children entities.\n\n---\n## RV\n* Entity types: All\n* User roles: All\n\nYou can launch RV player with playable components from selected entities. You can choose which components will be played.\n\n:::important\nYou must have RV player installed and licensed and have correct RV environments set to be able use this action.\n:::\n\n---\n## DJV View\n* Entity types: Task, Asset Version\n* User roles: All\n\nYou can launch DJV View with one playable component from selected entities. You can choose which component will be played.\n\n:::important\nYou must have DJV View installed and configured in studio-config to be able use this action.\n:::\n\n---\n<div class=\"row markdown\">\n<div class=\"col col--10 markdown\">\n\n## Open File\n\n</div>\n<div class=\"col col--2 markdown\">\n\n![component_open-icon](assets/ftrack/ftrack-component_open-icon.png)\n\n</div>\n</div>\n\n* Entity types: File Component\n* User roles: All\n\nThis action will open folder of selected *Component* on *Asset Version*.\n\n:::warning\nDoes not work for components uploaded to Ftrack Web server.\n:::\n![component_open_1](assets/ftrack/ftrack-component_open_1-small.png)\n\n:::warning\nComponent's path must be accessible by current OS.\n:::\n\n---\n## Sort Review\n* Entity types: Review Session\n* User roles: All\n\nHelps you sort *Asset Versions* in *Client Review Session*.\n\nAsset Versions are sorted by *Version number*, *Task name* and *Version name*.\n"
  },
  {
    "path": "website/docs/module_clockify.md",
    "content": "---\nid: module_clockify\ntitle: Clockify Administration \nsidebar_label: Clockify\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n\n"
  },
  {
    "path": "website/docs/module_deadline.md",
    "content": "---\nid: module_deadline\ntitle: Deadline Administration\nsidebar_label: Deadline\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n\n## Preparation\n\nFor [AWS Thinkbox Deadline](https://www.awsthinkbox.com/deadline) support you need to set a few things up in both OpenPype and Deadline itself\n\n1. Deploy OpenPype executable to all nodes of Deadline farm. See [Install & Run](admin_use.md)\n\n2. Enable Deadline Module in the [OpenPype Admin Settings](admin_settings_system.md#deadline).\n\n3. Set up *Deadline Web API service*. For more details on how to do it, see [here](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/web-service.html).\n\n4. Point OpenPype to your deadline webservice URL in the [OpenPype Admin Settings](admin_settings_system.md#deadline).\n\n5. Install our custom plugin and scripts to your deadline repository. It should be as simple as copying content of `openpype/modules/deadline/repository/custom` to `path/to/your/deadline/repository/custom`.\n\nMultiple different DL webservice could be configured. First set them in point 4., then they could be configured per project in `project_settings/deadline/deadline_servers`.\nOnly single webservice could be a target of publish though.\n\n\n## Configuration\n\nOpenPype integration for Deadline consists of two parts:\n\n- The `OpenPype` Deadline Plug-in\n- A `GlobalJobPreLoad` Deadline Script (this gets triggered for each deadline job)\n\nThe `GlobalJobPreLoad` handles populating render and publish jobs with proper environment variables using settings from the `OpenPype` Deadline Plug-in.\n\nThe `OpenPype` Deadline Plug-in must be configured to point to a valid OpenPype executable location. The executable need to be installed to\ndestinations accessible by DL process. Check permissions (must be executable and accessible by Deadline process)\n\n- Enable `Tools > Super User Mode` in Deadline Monitor\n\n- Go to `Tools > Configure Plugins...`, find `OpenPype` in the list on the left side, find location of OpenPype\nexecutable. It is recommended to use the `openpype_console` executable as it provides a bit more logging.\n\n- In case of multi OS farms, provide multiple locations, each Deadline Worker goes through the list and tries to find the first accessible\n location for itself.\n\n![Configure plugin](assets/deadline_configure_plugin.png)\n\n### OpenPypeTileAssembler Plugin\nTo setup tile rendering copy the `OpenPypeTileAssembler` plugin to the repository;\n`[OpenPype]\\openpype\\modules\\deadline\\repository\\custom\\plugins\\OpenPypeTileAssembler` > `[DeadlineRepository]\\custom\\plugins\\OpenPypeTileAssembler`\n\n### Pools\n\nThe main pools can be configured at `project_settings/deadline/publish/CollectDeadlinePools/primary_pool`, which is applied to the rendering jobs.\n\nThe dependent publishing job's pool uses `project_settings/deadline/publish/ProcessSubmittedJobOnFarm/deadline_pool`. If nothing is specified the pool will fallback to the primary pool above.\n\n:::note maya tile rendering\nThe logic for publishing job pool assignment applies to tiling jobs.\n:::\n\n## Troubleshooting\n\n#### Publishing jobs fail directly in DCCs\n\n- Double check that all previously described steps were finished\n- Check that `deadlinewebservice` is running on DL server\n- Check that user's machine has access to deadline server on configured port\n\n#### Jobs are failing on DL side\n\nEach publishing from OpenPype consists of 2 jobs, first one is rendering, second one is the publishing job (triggered after successful finish of the rendering job).\n\n![Jobs in DL](assets/deadline_fail.png)\n\n- Jobs are failing with `OpenPype executable was not found` error\n\n    Check if OpenPype is installed on the Worker handling this job and ensure `OpenPype` Deadline Plug-in is properly [configured](#configuration)\n\n\n- Publishing job is failing with `ffmpeg not installed` error\n\n    OpenPype executable has to have access to `ffmpeg` executable, check OpenPype `Setting > General`\n\n    ![FFmpeg setting](assets/ffmpeg_path.png)\n\n- Both jobs finished successfully, but there is no review on Ftrack\n\n    Make sure that you correctly set published family to be send to Ftrack.\n\n    ![Ftrack Family](assets/ftrack/ftrack-collect-main.png)\n\n    Example: I want send to Ftrack review of rendered images from Harmony :\n        - `Host names`: \"harmony\"\n        - `Families`: \"render\"\n        - `Add Ftrack Family` to \"Enabled\"\n\n    Make sure that you actually configured to create review for published subset in `project_settings/ftrack/publish/CollectFtrackFamily`\n\n    ![Ftrack Family](assets/deadline_review.png)\n\n    Example: I want to create review for all reviewable subsets in Harmony :\n      - Add \"harmony\" as a new key an \".*\" as a value.\n\n\n- Rendering jobs are stuck in 'Queued' state or failing\n\n    Make sure that your Deadline is not limiting specific jobs to be run only on specific machines. (Eg. only some machines have installed particular application.)\n\n    Check `project_settings/deadline`\n\n    ![Deadline group](assets/deadline_group.png)\n\n    Example: I have separated machines with \"Harmony\" installed into \"harmony\" group on Deadline. I want rendering jobs published from Harmony to run only on those machines.\n"
  },
  {
    "path": "website/docs/module_ftrack.md",
    "content": "---\nid: module_ftrack\ntitle: Ftrack Administration\nsidebar_label: Ftrack\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n\nFtrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and its basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](http://ftrack.rtd.ftrack.com/en/stable/).\n\n## Prepare Ftrack for OpenPype\n\n### Server URL\nIf you want to connect Ftrack to OpenPype you might need to make few changes in Ftrack settings. These changes would take a long time to do manually, so we prepared a few Ftrack actions to help you out. First, you'll need to launch OpenPype settings, enable [Ftrack module](admin_settings_system.md#Ftrack), and enter the address to your Ftrack server.\n\n### Login\nOnce your server is configured, restart OpenPype and you should be prompted to enter your [Ftrack credentials](artist_ftrack.md#How-to-use-Ftrack-in-OpenPype) to be able to run our Ftrack actions. If you are already logged in to Ftrack in your browser, it is enough to press `Ftrack login` and it will connect automatically.\n\nFor more details step by step on how to login to Ftrack in OpenPype to go [artist Ftrack login](artist_ftrack.md#How-to-use-Ftrack-in-OpenPype) documentation.\n\nYou can only use our Ftrack Actions and publish to Ftrack if each artist is logged in.\n\n\n### Custom Attributes\nAfter successfully connecting OpenPype with you Ftrack, you can right click on any project in Ftrack and you should see a bunch of actions available. The most important one is called `OpenPype Admin` and contains multiple options inside.\n\nTo prepare Ftrack for working with OpenPype you'll need to run [OpenPype Admin - Create/Update Custom Attributes](manager_ftrack_actions.md#create-update-avalon-attributes), which creates and sets the Custom Attributes necessary for OpenPype to function.\n\n\n\n## Event Server\nFtrack Event Server is the key to automation of many tasks like _status change_, _thumbnail update_, _automatic synchronization to Avalon database_ and many more. Event server should run at all times to perform the required processing as it is not possible to catch some of them retrospectively with enough certainty.\n\n### Running event server\nThere are specific launch arguments for event server. With `openpype_console module ftrack eventserver` you can launch event server but without prior preparation it will terminate immediately. The reason is that event server requires 3 pieces of information: _Ftrack server url_, _paths to events_ and _credentials (Username and API key)_. Ftrack server URL and Event path are set from OpenPype's environments by default, but the credentials must be done separatelly for security reasons.\n\n\n\n:::note There are 2 ways of passing your credentials to event server.\n\n<Tabs\n  defaultValue=\"args\"\n  values={[\n    {label: 'Additional Arguments', value: 'args'},\n    {label: 'Environments Variables', value: 'env'}\n  ]}>\n\n<TabItem value=\"args\">\n\n-  **`--ftrack-user \"your.username\"`** : Ftrack Username\n-   **`--ftrack-api-key \"00000aaa-11bb-22cc-33dd-444444eeeee\"`** : User's API key\n-   `--ftrack-url \"https://yourdomain.ftrackapp.com/\"` : Ftrack server URL _(it is not needed to enter if you have set `FTRACK_SERVER` in OpenPype' environments)_\n\nSo if you want to use OpenPype's environments then you can launch event server for first time with these arguments `openpype_console.exe module ftrack eventserver --ftrack-user \"my.username\" --ftrack-api-key \"00000aaa-11bb-22cc-33dd-444444eeeee\" --store-credentials`. Since that time, if everything was entered correctly, you can launch event server with `openpype_console.exe module ftrack eventserver`.\n\n</TabItem>\n<TabItem value=\"env\">\n\n- `FTRACK_API_USER` - Username _(\"your.username\")_\n- `FTRACK_API_KEY` - User's API key _(\"00000aaa-11bb-22cc-33dd-444444eeeee\")_\n- `FTRACK_SERVER` - Ftrack server url _(\"<https://yourdomain.ftrackapp.com/\">)_\n\n</TabItem>\n</Tabs>\n:::\n\n:::caution\nWe do not recommend setting your Ftrack user and api key environments in a persistent way, for security reasons. Option 1. passing them as arguments is substantially safer.\n:::\n\n### Where to run event server\n\nWe recommend you to run event server on stable server machine with ability to connect to OpenPype database and Ftrack web server. Best practice we recommend is to run event server as service. It can be Windows or Linux.\n\n:::important\nEvent server should **not** run more than once! It may cause major issues.\n:::\n\n### Which user to use\n\n-   must have at least `Administrator` role\n-   the same user should not be used by an artist\n\n\n:::note How to create Eventserver service\n<Tabs\n  defaultValue=\"linux\"\n  values={[\n    {label: 'Linux', value: 'linux'},\n    {label: 'Windows', value: 'win'},\n  ]}>\n\n<TabItem value=\"linux\">\n\n- create file:\n    `sudo vi /opt/openpype/run_event_server.sh`\n-   add content to the file:\n```sh\n#!/usr/bin/env bash\nexport OPENPYPE_MONGO=<openpype-mongo-url>\n\npushd /mnt/path/to/openpype\n./openpype_console module ftrack eventserver --ftrack-user <openpype-admin-user> --ftrack-api-key <api-key> --debug\n```\n-   change file permission:\n    `sudo chmod 0755 /opt/openpype/run_event_server.sh`\n\n-   create service file:\n    `sudo vi /etc/systemd/system/openpype-ftrack-event-server.service`\n-   add content to the service file\n\n```toml\n[Unit]\nDescription=Run OpenPype Ftrack Event Server Service\nAfter=network.target\n\n[Service]\nType=idle\nExecStart=/opt/openpype/run_event_server.sh\nRestart=on-failure\nRestartSec=10s\n\n[Install]\nWantedBy=multi-user.target\n```\n\n-   change file permission:\n    `sudo chmod 0755 /etc/systemd/system/openpype-ftrack-event-server.service`\n\n-   enable service:\n    `sudo systemctl enable openpype-ftrack-event-server`\n\n-   start service:\n    `sudo systemctl start openpype-ftrack-event-server`\n\n</TabItem>\n<TabItem value=\"win\">\n\n-   create service file: `openpype-ftrack-eventserver.bat`\n-   add content to the service file:\n```sh\n@echo off\nset OPENPYPE_MONGO=<openpype-mongo-url>\n\npushd \\\\path\\to\\openpype\nopenpype_console.exe module ftrack eventserver --ftrack-user <openpype-admin-user> --ftrack-api-key <api-key> --debug\n```\n-   download and install `nssm.cc`\n-   create Windows service according to nssm.cc manual\n-   you can also run eventserver as a standard Schedule task\n-   be aware of using UNC path\n\n</TabItem>\n</Tabs>\n:::\n\n* * *\n\n## Ftrack events\n\nEvents are helpers for automation. They react to Ftrack Web Server events like change entity attribute, create of entity, etc.\n\n### Sync to Avalon\n\nAutomatic [synchronization to pipeline database](manager_ftrack.md#synchronization-to-avalon-database).\n\nThis event updates entities on their changes Ftrack. When new entity is created or existing entity is modified. Interface with listing information is shown to users when [synchronization rules](manager_ftrack.md#synchronization-rules) are not met. This event may also undo changes when they might break pipeline. Namely _change name of synchronized entity_, _move synchronized entity in hierarchy_.\n\n:::important\nDeleting an entity by Ftrack's default is not processed for security reasons _(to delete entity use [Delete Asset/Subset action](manager_ftrack_actions.md#delete-asset-subset))_.\n:::\n\n### Synchronize Hierarchical and Entity Attributes\n\nAuto-synchronization of hierarchical attributes from Ftrack entities.\n\nRelated to [Synchronize to Avalon database](manager_ftrack.md#synchronization-to-avalon-database) event _(without it, it makes no sense to use this event)_. Hierarchical attributes must be synchronized with special way so we needed to split synchronization into 2 parts. There are [synchronization rules](manager_ftrack.md#synchronization-rules) for hierarchical attributes that must be met otherwise interface with messages about not meeting conditions is shown to user.\n\n### Update Hierarchy thumbnails\n\nPush thumbnails from version, up through multiple hierarchy levels\n\n### Update status on task action\n\nChange status of next task from `Not started` to `Ready` when previous task is approved.\n\nMultiple detailed rules for next task update can be configured in the settings.\n\n### Delete Avalon ID from new entity\n\nIs used to remove value from `Avalon/Mongo Id` Custom Attribute when entity is created.\n\n`Avalon/Mongo Id` Custom Attribute stores id of synchronized entities in pipeline database. When user _Copy → Paste_ selection of entities to create similar hierarchy entities, values from Custom Attributes are copied too. That causes issues during synchronization because there are multiple entities with same value of `Avalon/Mongo Id`. To avoid this error we preventively remove these values when entity is created.\n\n### Sync status from Task to Parent\n\nList of parent object types where this is triggered (\"Shot\", \"Asset build\", etc. Skipped if it is empty)\n\n### Sync status from Version to Task\n\nUpdates Task status based on status changes on its Asset Version.\n\nThe issue this solves is when Asset version's status is changed but the artist assigned to Task is looking at the task status, thus not noticing the review.\n\nThis event makes sure statuses Asset Version get synced to it's task. After changing a status on version, this event first tries to set identical status to version's parent (usually task). But this behavior can be tweaked in settings.\n\n### Sync status on first created version\n\nThis event handler allows setting of different status to a first created Asset Version in Ftrack.\n\nThis is useful for example if first version publish doesn't contain any actual reviewable work, but is only used for roundtrip conform check, in which case this version could receive status `pending conform` instead of standard `pending review`\n\n### Update status on next task\nChange status on next task by task types order when task status state changed to \"Done\". All tasks with the same Task mapping of next task status changes From → To. Some status can be ignored.\n\n## Publish plugins\n\n### Collect Ftrack Family\n\nReviews uploads to Ftrack could be configured by combination of hosts, families and task names.\n(Currently implemented only in Standalone Publisher, Maya.)\n\n#### Profiles\n\nProfiles are used to select when to add Ftrack family to the instance. One or multiple profiles could be configured, Families, Task names (regex available), Host names combination is needed.\n\nEg. If I want review created and uploaded to Ftrack for render published from Maya , setting is:\n\nHost names: 'Maya'\nFamilies: 'render'\nAdd Ftrack Family: enabled\n\n![Collect Ftrack Family](assets/ftrack/ftrack-collect-main.png)\n\n#### Advanced adding if additional families present\n\nIn special cases adding 'ftrack' based on main family ('Families' set higher) is not enough.\n(For example upload to Ftrack for 'plate' main family should only happen if 'review' is contained in instance 'families', not added in other cases. )\n\n![Collect Ftrack Family](assets/ftrack/ftrack-collect-advanced.png)\n"
  },
  {
    "path": "website/docs/module_kitsu.md",
    "content": "---\nid: module_kitsu\ntitle: Kitsu Administration\nsidebar_label: Kitsu\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nKitsu is a great open source production tracker and can be used for project management instead of Ftrack. This documentation assumes that you are familiar with Kitsu and its basic principles. If you're new to Kitsu, we recommend having a thorough look at [Kitsu Official Documentation](https://kitsu.cg-wire.com/).\n\n## Prepare Kitsu for OpenPype\n\n### Server URL\nIf you want to connect Kitsu to OpenPype you have to set the `Server` url in Kitsu settings. And that's all!\nThis setting is available for all the users of the OpenPype instance.\n\n## Synchronize\nUpdating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu and create/delete/update OP assets.\nOnce this sync is done, the thread will automatically start a loop to listen to Kitsu events.\n- `-prj, --project` This flag accepts multiple project name to sync specific projects, and the default to sync all projects.\n- `-lo, --listen-only` This flag to run listen to Kitsu events only without any sync.\n\nNote: You must use one argument of `-pro` or `-lo`, because the listen only flag override syncing flag. \n\n```bash\n// sync all projects then run listen\nopenpype_console module kitsu sync-service -l me@domain.ext -p my_password\n\n// sync specific projects then run listen\nopenpype_console module kitsu sync-service -l me@domain.ext -p my_password -prj project_name01 -prj  project_name02\n\n// start listen only for all projects\nopenpype_console module kitsu sync-service -l me@domain.ext -p my_password -lo\n```\n\n### Events listening\nListening to Kitsu events is the key to automation of many tasks like _project/episode/sequence/shot/asset/task create/update/delete_ and some more. Events listening should run at all times to perform the required processing as it is not possible to catch some of them retrospectively with strong reliability. If such timeout has been encountered, you must relaunch the `sync-service` command to run the synchronization step again.\n\nConnection token is refreshed every week.\n\n### Push to Kitsu\nAn utility function is provided to help update Kitsu data (a.k.a Zou database) with OpenPype data if the publishing to the production tracker hasn't been possible for some time. Running `push-to-zou` will create the data on behalf of the user.\n:::caution\nThis functionality cannot deal with all cases and is not error proof, some intervention by a human being might be required.\n:::\n\n```bash\nopenpype_console module kitsu push-to-zou -l me@domain.ext -p my_password\n```\n\n## Integrate Kitsu Note\nTask status can be automatically set during publish thanks to `Integrate Kitsu Note`. This feature can be configured in:\n\n`Admin -> Studio Settings -> Project Settings -> Kitsu -> Integrate Kitsu Note`.\n\nThere are four settings available:\n- `Set status on note` -> Turns on and off this integrator.\n- `Note shortname` -> Which status shortname should be set automatically (Case sensitive).\n- `Status change conditions - Status conditions` -> Conditions that need to be met for kitsu status to be changed. You can add as many conditions as you like. There are two fields to each conditions: `Condition` (Whether current status should be equal or not equal to the condition status) and `Short name` (Kitsu Shortname of the condition status).\n- `Status change conditions - Family requirements` -> With this option you can add requirements to which families must be pushed or not in order to have the task status set by this integrator. There are two fields for each requirements: `Condition` (Same as the above) and `Family` (name of the family concerned by this requirement). For instance, adding one item set to `Not equal` and `workfile`, would mean the task status would change if a subset from another family than workfile is published (workfile can still be included), but not if you publish the workfile subset only.\n\n![Integrate Kitsu Note project settings](assets/integrate_kitsu_note_settings.png)\n\n## Q&A\n### Is it safe to rename an entity from Kitsu?\nAbsolutely! Entities are linked by their unique IDs between the two databases.\nBut renaming from the OP's Project Manager won't apply the change to Kitsu, it'll be overridden during the next synchronization.\n"
  },
  {
    "path": "website/docs/module_royalrender.md",
    "content": "---\nid: module_royalrender\ntitle: Royal Render Administration\nsidebar_label: Royal Render\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n\n## Preparation\n\nFor [Royal Render](hhttps://www.royalrender.de/) support you need to set a few things up in both OpenPype and Royal Render itself\n\n1. Deploy OpenPype executable to all nodes of Royal Render farm. See [Install & Run](admin_use.md)\n\n2. Enable Royal Render Module in the [OpenPype Admin Settings](admin_settings_system.md#royal-render).\n\n3. Point OpenPype to your Royal Render installation in the [OpenPype Admin Settings](admin_settings_system.md#royal-render).\n\n4. Install our custom plugin and scripts to your RR repository. It should be as simple as copying content of `openpype/modules/royalrender/rr_root` to `path/to/your/royalrender/repository`.\n\n\n## Configuration\n\nOpenPype integration for Royal Render consists of pointing RR to location of Openpype executable. That is being done by copying `_install_paths/OpenPype.cfg` to\nRR root folder. This file contains reasonable defaults. They could be changed in this file or modified Render apps in `rrControl`.\n\n\n## Debugging\n\nCurrent implementation uses dynamically build '.xml' file which is stored in temporary folder accessible by RR. It might make sense to\nuse this Openpype built file and try to run it via `*__rrServerConsole` executable from command line in case of unforeseeable issues.\n\n## Known issues\n\nCurrently environment values set in Openpype are not propagated into render jobs on RR. It is studio responsibility to synchronize environment variables from Openpype with all render nodes for now.\n"
  },
  {
    "path": "website/docs/module_site_sync.md",
    "content": "---\nid: module_site_sync\ntitle: Site Sync Administration\nsidebar_label: Site Sync\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nSite Sync allows users and studios to synchronize published assets between\nmultiple 'sites'. Site denotes a storage location,\nwhich could be a physical disk, server, cloud storage. To be able to use site\nsync, it first needs to be configured.\n\nThe general idea is that each user acts as an individual site and can download\nand upload any published project files when they are needed. that way, artist\ncan have access to the whole project, but only every store files that are\nrelevant to them on their home workstation.\n\n:::note\nAt the moment site sync is only able to deal with publishes files. No workfiles\nwill be synchronized unless they are published. We are working on making\nworkfile synchronization possible as well.\n:::\n\n## System Settings\n\nTo use synchronization, *Site Sync* needs to be enabled globally in **OpenPype\nSettings/System/Modules/Site Sync**.\n\n![Configure module](assets/site_sync_system.png)\n\n### Sites\n\nBy default there are two sites created for each OpenPype installation:\n\n- **studio** - default site - usually a centralized mounted disk accessible to\n  all artists. Studio site is used if Site Sync is disabled.\n- **local** - each workstation or server running OpenPype Tray receives its own\n  with unique site name. Workstation refers to itself as \"local\"however all\n  other sites will see it under it's unique ID.\n\nArtists can explore their site ID by opening OpenPype Info tool by clicking on\na version number in the tray app.\n\nMany different sites can be created and configured on the system level, and\nsome or all can be assigned to each project.\n\nEach OpenPype Tray app works with two sites at one time. (Sites can be the\nsame, and no syncing is done in this setup).\n\nSites could be configured differently per project basis.\n\nEach new site needs to be created first in `System Settings`. Most important\nfeature of site is its Provider, select one from already prepared Providers.\n\n#### Alternative sites\n\nThis attribute is meant for special use cases only.\n\nOne of the use cases is sftp site vendoring (exposing) same data as regular\nsite (studio). Each site is accessible for different audience. 'studio' for\nartists in a studio via shared disk, 'sftp' for externals via sftp server with\nmounted 'studio' drive.\n\nChange of file status on one site actually means same change on 'alternate'\nsite occurred too. (eg. artists publish to 'studio', 'sftp' is using\nsame location >> file is accessible on 'sftp' site right away, no need to sync\nit anyhow.)\n\n##### Example\n\n![Configure module](assets/site_sync_system_sites.png)\nAdmin created new `sftp` site which is handled by `SFTP` provider. Somewhere in\nthe studio SFTP server is deployed on a machine that has access to `studio`\ndrive.\n\nAlternative sites work both way:\n\n- everything published to `studio` is accessible on a `sftp` site too\n- everything published to `sftp` (most probably via artist's local disk -\n  artists publishes locally, representation is marked to be synced to `sftp`.\n  Immediately after it is synced, it is marked to be available on `studio` too\n  for artists in the studio to use.)\n\n## Project Settings\n\nSites need to be made available for each project. Of course this is possible to\ndo on the default project as well, in which case all other projects will\ninherit these settings until overridden explicitly.\n\nYou'll find the setting in **Settings/Project/Global/Site Sync**\n\nThe attributes that can be configured will vary between sites and their\nproviders.\n\n## Local settings\n\nEach user should configure root folder for their 'local' site via **Local\nSettings** in OpenPype Tray. This folder will be used for all files that the\nuser publishes or downloads while working on a project. Artist has the option\nto set the folder as \"default\"in which case it is used for all the projects, or\nit can be set on a project level individually.\n\nArtists can also override which site they use as active and remote if need be.\n\n![Local overrides](assets/site_sync_local_setting.png)\n\n## Providers\n\nEach site implements a so called `provider` which handles most common\noperations (list files, copy files etc.) and provides interface with a\nparticular type of storage. (disk, gdrive, aws, etc.)\nMultiple configured sites could share the same provider with different\nsettings (multiple mounted disks - each disk can be a separate site, while\nall share the same provider).\n\n**Currently implemented providers:**\n\n### Local Drive\n\nHandles files stored on disk storage.\n\nLocal drive provider is the most basic one that is used for accessing all\nstandard hard disk storage scenarios. It will work with any storage that can be\nmounted on your system in a standard way. This could correspond to a physical\nexternal hard drive, network mounted storage, internal drive or even VPN\nconnected network drive. It doesn't care about how the drive is mounted, but\nyou must be able to point to it with a simple directory path.\n\nDefault sites `local` and `studio` both use local drive provider.\n\n### Google Drive\n\nHandles files on Google Drive (this). GDrive is provided as a production\nexample for implementing other cloud providers\n\nLet's imagine a small globally distributed studio which wants all published\nwork for all their freelancers uploaded to Google Drive folder.\n\nFor this use case admin needs to configure:\n\n- how many times it tries to synchronize file in case of some issue (network,\n  permissions)\n- how often should synchronization check for new assets\n- sites for synchronization - 'local' and 'gdrive' (this can be overridden in\n  local settings)\n- user credentials\n- root folder location on Google Drive side\n\nConfiguration would look like this:\n\n![Configure project](assets/site_sync_project_settings.png)\n\n*Site Sync* for Google Drive works using its\nAPI: https://developers.google.com/drive/api/v3/about-sdk\n\nTo configure Google Drive side you would need to have access to Google Cloud\nPlatform project: https://console.cloud.google.com/\n\nTo get working connection to Google Drive there are some necessary steps:\n\n- first you need to enable GDrive\n  API: https://developers.google.com/drive/api/v3/enable-drive-api\n- next you need to create user, choose **Service Account** (for basic\n  configuration no roles for account are necessary)\n- add new key for created account and download .json file with credentials\n- share destination folder on the Google Drive with created account (directly\n  in GDrive web application)\n- add new site back in OpenPype Settings, name as you want, provider needs to\n  be 'gdrive'\n- distribute credentials file via shared mounted disk location\n\n:::note\nIf you are using regular personal GDrive for testing don't forget\nadding `/My Drive` as the prefix in root configuration. Business accounts and\nshare drives don't need this.\n:::\n\n### SFTP\n\nSFTP provider is used to connect to SFTP server. Currently authentication\nwith `user:password` or `user:ssh key` is implemented.\nPlease provide only one combination, don't forget to provide password for ssh\nkey if ssh key was created with a passphrase.\n\n(SFTP connection could be a bit finicky, use FileZilla or WinSCP for testing\nconnection, it will be mush faster.)\n\nBeware that ssh key expects OpenSSH format (`.pem`) not a Putty\nformat (`.ppk`)!\n\n#### How to set SFTP site\n\n- Enable Site Sync module in Settings\n- Add side with SFTP provider\n\n![Enable syncing and create site](assets/site_sync_sftp_system.png)\n\n- In Projects setting enable Site Sync (on default project - all project will\n  be synched, or on specific project)\n- Configure SFTP connection and destination folder on a SFTP server (in\n  screenshot `/upload`)\n\n![SFTP connection](assets/site_sync_project_sftp_settings.png)\n\n- if you want to force syncing between local and sftp site for all users, use\n  combination `active site: local`, `remote site: NAME_OF_SFTP_SITE`\n- if you want to allow only specific users to use SFTP syncing (external users,\n  not located in the office), use `active site: studio`, `remote site: studio`.\n\n![Select active and remote site on a project](assets/site_sync_sftp_project_setting_not_forced.png)\n\n- Each artist can decide and configure syncing from his/her local to SFTP\n  via `Local Settings`\n\n![Select active and remote site on a project](assets/site_sync_sftp_settings_local.png)\n\n### Custom providers\n\nIf a studio needs to use other services for cloud storage, or want to implement\ntotally different storage providers, they can do so by writing their own\nprovider plugin. We're working on a developer documentation, however, for now\nwe recommend looking at `abstract_provider.py`and `gdrive.py`\ninside `openpype/modules/sync_server/providers` and using it as a template.\n\n### Running Site Sync in background\n\nSite Sync server synchronizes new published files from artist machine into\nconfigured remote location by default.\n\nThere might be a use case where you need to synchronize between \"non-artist\"\nsites, for example between studio site and cloud. In this case\nyou need to run Site Sync as a background process from a command line (via\nservice etc) 24/7.\n\nTo configure all sites where all published files should be synced eventually\nyou need to\nconfigure `project_settings/global/sync_server/config/always_accessible_on`\nproperty in Settings (per project) first.\n\n![Set another non artist remote site](assets/site_sync_always_on.png)\n\nThis is an example of:\n\n- Site Sync is enabled for a project\n- default active and remote sites are set to `studio` - eg. standard process:\n  everyone is working in a studio, publishing to shared location etc.\n- (but this also allows any of the artists to work remotely, they would change\n  their active site in their own Local Settings to `local` and configure local\n  root.\n  This would result in everything artist publishes is saved first onto his\n  local folder AND synchronized to `studio` site eventually.)\n- everything exported must also be eventually uploaded to `sftp` site\n\nThis eventual synchronization between `studio` and `sftp` sites must be\nphysically handled by background process.\n\nAs current implementation relies heavily on Settings and Local Settings,\nbackground process for a specific site ('studio' for example) must be\nconfigured via Tray first to `syncserver` command to work.\n\nTo do this:\n\n- run OP `Tray` with environment variable OPENPYPE_LOCAL_ID set to name of\n  active (source) site. In most use cases it would be studio (for cases of\n  backups of everything published to studio site to different cloud site etc.)\n- start `Tray`\n- check `Local ID` in information dialog after clicking on version number in\n  the Tray\n- open `Local Settings` in the `Tray`\n- configure for each project necessary active site and remote site\n- close `Tray`\n- run OP from a command line with `syncserver` and `--active_site` arguments\n\nThis is an example how to trigger background syncing process where active (\nsource) site is `studio`.\n(It is expected that OP is installed on a machine, `openpype_console` is on\nPATH. If not, add full path to executable.\n)\n\n```shell\nopenpype_console syncserver --active_site studio\n```\n\n### Syncing of last published workfile\n\nSome DCC might have enabled\nin `project_setting/global/tools/Workfiles/last_workfile_on_startup`, eg. open\nDCC with last opened workfile.\n\nFlag `use_last_published_workfile` tells that last published workfile should be\nused if no workfile is present locally.\nThis use case could happen if artists starts working on new task locally,\ndoesn't have any workfile present. In that case last published will be\nsynchronized locally and its version bumped by 1 (as workfile's version is\nalways +1 from published version)."
  },
  {
    "path": "website/docs/module_slack.md",
    "content": "---\nid: module_slack\ntitle: Slack Integration Administration\nsidebar_label: Slack\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n\nThis module allows configuring profiles(when to trigger, for which combination of task, host and family)\nand templates(could contain {} placeholder) to send notification to Slack channel(s)\nwhenever configured asset type is published.\n\n\n## App installation\n\nSlack application must be installed to company's Slack first. \n\nPlease locate `openpype/modules/slack/manifest.yml` file in deployed OpenPype installation and follow instruction at\nhttps://api.slack.com/reference/manifests#using and follow \"Creating apps with manifests\".\n\n### App icon\n\nIf you would like to enrich bot with an icon, Slack admin must add the icon after app installation. \n\nGo to your Slack app home (something like https://api.slack.com/apps/XXXXXXXX/general?) > Basic information > Display Information.\nYou can upload any image you want, or for your convenience locate prepared OpenPype icon in your installed Openpype installation in `openpype\\modules\\slac\\resources`.\n\n## System Settings\n\nTo use notifications, *Slack Notifications* needs to be enabled globally in **OpenPype Settings/System/Modules/Slack Notifications**.\n\n![Configure module](assets/slack_system.png)\n\n\n## Project Settings\n\n### Token\nMost important for module to work is to fill authentication token \n```Project settings > Slack > Publish plugins > Token```\n\nThis token should be available after installation of the app in the Slack dashboard.\nIt is possible to create multiple tokens and configure different scopes for them.\n\n![Get token](assets/slack_token.png)\n\n### Profiles\nProfiles are used to select when to trigger notification. One or multiple profiles\ncould be configured, `Families`, `Task names` (regex available), `Host names`, `Subset names` (regex available) combination is needed.\n\nEg. If I want to be notified when render is published from Maya, setting is:\n\n- family: 'render'\n- host: 'Maya'\n\n### Messages to channels\n\n#### Channels\nMultiple messages could be delivered to one or multiple channels, by default app allows Slack bot\nto send messages to 'public' channels (eg. bot doesn't need to join the channel first).\n\n![Configure module](assets/slack_project.png)\n\n#### Upload thumbnail\nIntegration can upload 'thumbnail' file (if present in an instance), for that bot must be \nmanually added to target channel by Slack admin!\n(In target channel write: ```/invite @OpenPypeNotifier``)\n\n#### Upload review\nIntegration can upload 'review' file (if present in an instance), for that bot must be \nmanually added to target channel by Slack admin!\n(In target channel write: ```/invite @OpenPypeNotifier``)\n\nBurnin version (usually .mp4) is preferred if present.\n\nPlease be sure that this configuration is viable for your use case. In case of uploading large reviews to Slack, \nall publishes will be slowed down and you might hit a file limit on Slack pretty soon (it is 5GB for Free version of Slack, any file cannot be bigger than 1GB).\nYou might try to add `{review_filepath}` to message content. This link might help users to find review easier on their machines.\n(It won't show a playable preview though!)\n\n#### Message\nMessage content can use Templating (see [Available template keys](admin_settings_project_anatomy.md#available-template-keys)).\n\nFew keys also have Capitalized and UPPERCASE format. Values will be modified accordingly ({Asset} >> \"Asset\", {FAMILY} >> \"RENDER\").\n\n**Additional implemented keys:**\n- review_filepath\n\n##### Message example\n```\n{Subset} was published for {ASSET} in {task[name]} task.\n\nHere you can find review {review_filepath}\n```\n\n##### Dynamic message for artists\nIf artists uses host with implemented Publisher (new UI for publishing, implemented in Tray Publisher, Adobe products etc), it is possible for\nthem to add additional message (notification for specific users for example, artists must provide proper user id with '@').\nAdditional message will be sent only if at least one profile, eg. one target channel is configured.\nAll available template keys (see higher) could be used here as a placeholder too.\n\n#### User or group notifications\nMessage template or dynamic data could contain user or group notification, it must be in format @artist.name, '@John Doe' or \"@admin group\" for display name containing space.\nIf value prefixed with @ is not resolved and Slack user is not found, message will contain same value (not translated by Slack into link and proper mention.)\n\n#### Message retention\nCurrently no purging of old messages is implemented in Openpype. Admins of Slack should set their own retention of messages and files per channel.\n(see https://slack.com/help/articles/203457187-Customize-message-and-file-retention-policies)\n"
  },
  {
    "path": "website/docs/project_settings/settings_project_global.md",
    "content": "---\nid: settings_project_global\ntitle: Project Global Setting\nsidebar_label: Global\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nProject settings can have project specific values. Each new project is using studio values defined in **default** project but these values can be modified or overridden per project.\n\n:::warning Default studio values\nProjects always use default project values unless they have [project override](../admin_settings#project-overrides) (orange colour). Any changes in default project may affect all existing projects.\n:::\n\n## Color Management (ImageIO)\n\n:::info Default OCIO config\nOpenPype distributes its own OCIO configs. Those can be found in `{openpype install dir}/{version}/vendor/bin/ocioconfig/OpenColorIOConfigs`. Windows example: `C:\\Program Files (x86)\\OpenPype\\3.14.0\\vendor\\bin\\ocioconfig\\OpenColorIOConfigs`\n:::\n\n### Using OCIO config\nGlobal config path is set by default to OpenPype distributed configs. At the moment there are only two - **aces_1.2** and **nuke-default**. Since this path input is not platform specific it is required to use at least an environment variable do platform specific config root directory. Order of paths matter so first path found and existing first served.\n\nEach OCIO config path input supports formatting using environment variables and [anatomy template keys](../admin_settings_project_anatomy#available-template-keys). The default global OCIO config path is `{OPENPYPE_ROOT}/vendor/bin/ocioconfig/OpenColorIOConfigs/aces_1.2/config.ocio`.\n\nIf the project settings for a particular host has its own OCIO config **enabled** and set to at least one path and the path exists, it overrides the global OCIO config for that host.\n\n**For example**\n\nProject nuke-specific OCIO config: `project_settings/nuke/imageio/ocio_config`\n\nIf config path is defined to particular shot target with following path inputs:\n1. `{root[work]}/{project[name]}/{hierarchy}/{asset}/config/aces.ocio`\n2. `{root[work]}/{project[name]}/{hierarchy}/config/aces.ocio`\n\nProcedure of resolving path (from above example) will look first into path 1st and if the path is not existing then it will try 2nd and if even that is not existing then it will fall back to global default.\n\n### Using File rules\nFile rules are inspired by [OCIO v2 configuration]((https://opencolorio.readthedocs.io/en/latest/guides/authoring/rules.html)). Each rule has a unique name which can be overridden by host-specific _File rules_ (example: `project_settings/nuke/imageio/file_rules/rules`).\n\nThe _input pattern_ matching uses REGEX expression syntax (try [regexr.com](https://regexr.com/)). Matching rules procedure's intention is to be used during publishing or loading of representation. Since the publishing procedure is run before integrator format publish template path, make sure the pattern is working or any work render path.\n\n:::warning Colorspace name input\nThe **colorspace name** value is a raw string input and no validation is run after saving project settings. We recommend to open the specified `config.ocio` file and copy pasting the exact colorspace names.\n:::\n\n### Extract OIIO Transcode\nOIIOTools transcoder plugin with configurable output presets. Any incoming representation with `colorspaceData` is convertible to single or multiple representations with different target colorspaces or display and viewer names found in linked **config.ocio** file.\n\n`oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation.\n\nNotable parameters:\n- **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation loses its 'review' tag if present.\n- **`Extension`** - target extension. If left empty, original extension is used.\n- **`Transcoding type`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both at the same time.\n- **`Colorspace`** - target colorspace, which must be available in used color config. (If `Transcoding type` is `Use Colorspace` value in configuration is used OR if empty value collected on instance from DCC).\n- **`Display & View`** - display and viewer colorspace. (If `Transcoding type` is `Use Display&View` values in configuration is used OR if empty values collected on instance from DCC).\n- **`Arguments`** - special additional command line arguments for `oiiotool`.\n\n\nExample here describes use case for creation of new color coded review of png image sequence. Original representation's files are kept intact, review is created from transcoded files, but these files are removed in cleanup process.\n![global_oiio_transcode](assets/global_oiio_transcode.png)\n\nAnother use case is to transcode in Maya only `beauty` render layers and use collected `Display` and `View` colorspaces from DCC.\n![global_oiio_transcode_in_Maya](assets/global_oiio_transcode2.png)\n\n## Profile filters\n\nMany of the settings are using a concept of **Profile filters**\n\nYou can define multiple profiles to choose from for different contexts. Each filter is evaluated and a\nprofile with filters matching the current context the most, is used.\n\nYou can define profile without any filters and use it as **default**.\n\nOnly **one or none** profile will be returned per context.\n\nAll context filters are lists which may contain strings or Regular expressions (RegEx).\n- **`hosts`** - Host from which publishing was triggered. `[\"maya\", \"nuke\"]`\n- **`families`** - Main family of processed subset. `[\"plate\", \"model\"]`\n- **`tasks`** - Currently processed task. `[\"modeling\", \"animation\"]`\n\n:::important Filtering\nFilters are optional. In case when multiple profiles match current context, profile with higher number of matched filters has higher priority than profile without filters.\n(The order the profiles in settings doesn't matter, only the precision of matching does.)\n:::\n\n## Publish plugins\n\nPublish plugins used across all integrations.\n\n\n### Extract Review\nPlugin responsible for automatic FFmpeg conversion to variety of formats.\n\nExtract review uses [profile filtering](#profile-filters) to render different outputs for different situations.\n\nApplicable context filters:\n **`hosts`** - Host from which publishing was triggered. `[\"maya\", \"nuke\"]`\n- **`families`** - Main family of processed subset. `[\"plate\", \"model\"]`\n\n![global_extract_review_profiles](assets/global_extract_review_profiles.png)\n\n**Output Definitions**\n\nA profile may generate multiple outputs from a single input. Each output must define unique name and output extension (use the extension without a dot e.g. **mp4**). All other settings of output definition are optional.\n\n![global_extract_review_output_defs](assets/global_extract_review_output_defs.png)\n- **`Tags`**\n    Define what will happen to output.\n\n- **`FFmpeg arguments`**\n    These arguments are appended to ffmpeg arguments auto generated by publish plugin. Some of arguments are handled automatically like rescaling or letterboxes.\n    - **Video filters** additional FFmpeg filters that would be defined in `-filter:v` or `-vf` command line arguments.\n    - **Audio filters** additional FFmpeg filters that would be defined in `-filter:a` or `-af` command line arguments.\n    - **Input arguments** input definition arguments of video or image sequence - this setting has limitations as you have to know what is input.\n    - **Output arguments** other FFmpeg output arguments like codec definition.\n\n- **`Output width`** and **`Output height`**\n    - It is possible to rescale output to specified resolution and keep aspect ratio.\n    - If value is set to 0, source resolution will be used.\n\n- **`Overscan crop`**\n    - Crop input resolution before rescaling.\n\n    - Value is text may have a few variants. Each variant define output size for input size.\n\n    - All values that cause output resolution smaller than 1 pixel are invalid.\n\n    - Value without sign (+/-) in is always explicit and value with sign is\n    relative. Output size for values \"200px\" and \"+200px\" are not the same \"+200px\" will add 200 pixels to source and \"200px\" will keep only 200px from source. Value of \"0\", \"0px\" or \"0%\" are automatically converted to \"+0px\" as 0px is invalid output.\n\n    - Cropped value is related to center. It is better to avoid odd numbers if\n    possible.\n\n    **Example outputs for input size: 2200px**\n\n    | String | Output | Description |\n    |---|---|---|\n    | ` `      | 2200px | Empty string keep resolution unchanged. |\n    | `50%`    | 1100px | Crop 25% of input width on left and right side. |\n    | `300px`  | 300px | Keep 300px in center of input and crop rest on left and right. |\n    | `300`    | 300px | Values without units are used as pixels (`px`). |\n    | `+0px`   | 2200px | Keep resolution unchanged. |\n    | `0px`   | 2200px | Same as `+0px`. |\n    | `+300px` | 2500px | Add black pillars of 150px width on left and right side. |\n    | `-300px` | 1900px | Crop 150px on left and right side |\n    | `+10%`   | 2420px | Add black pillars of 5% size of input on left and right side. |\n    | `-10%`   | 1980px | Crop 5% of input size by on left and right side. |\n    | `-10%+`  | 2000px | Input width is 110% of output width. |\n\n    **Value \"-10%+\" is a special case which says that input's resolution is\n    bigger by 10% than expected output.**\n\n    - It is possible to enter single value for both width and height or\n    combination of two variants for width and height separated with space.\n\n    **Example for resolution: 2000px 1000px**\n\n    | String        | Output        |\n    |---------------|---------------|\n    | \"100px 120px\" | 2100px 1120px |\n    | \"-10% -200px\" | 1800px 800px  |\n    | \"-10% -0px\" | 1800px 1000px  |\n\n- **`Overscan color`**\n    - Color of empty area caused by different aspect ratio of input and output.\n    - By default is set to black color.\n\n- **`Letter Box`**\n    - **Enabled** - Enable letter boxes\n    - **Ratio** - Ratio of letter boxes. Ratio type is calculated from output image dimensions. If letterbox ratio > image ratio, _letterbox_ is applied. Otherwise _pillarbox_ will be rendered.\n    - **Fill color** - Fill color of boxes (RGBA: 0-255)\n    - **Line Thickness** - Line thickness on the edge of box (set to `0` to turn off)\n    - **Line color** - Line color on the edge of box (RGBA: 0-255)\n\n    ![global_extract_review_letter_box_settings](assets/global_extract_review_letter_box_settings.png)\n    ![global_extract_review_letter_box](assets/global_extract_review_letter_box.png)\n\n- **`Background color`**\n    - Background color can be used for inputs with possible transparency (e.g. png sequence).\n    - Input's without possible alpha channel are ignored all the time (e.g. mov).\n    - Background color slows down rendering process.\n        - set alpha to `0` to not use this option at all (in most of cases background stays black)\n        - other than `0` alpha will draw color as background\n\n- **`Additional filtering`**\n    - Profile filtering defines which group of output definitions is used but output definitions may require more specific filters on their own.\n    - They may filter by subset name (regex can be used) or publish families. Publish families are more complex as are based on knowing code base.\n    - Filtering by custom tags -> this is used for targeting to output definitions from other extractors using settings (at this moment only Nuke bake extractor can target using custom tags).\n        - Nuke extractor settings path: `project_settings/nuke/publish/ExtractReviewIntermediates/outputs/baking/add_custom_tags`\n    - Filtering by input length. Input may be video, sequence or single image. It is possible that `.mp4` should be created only when input is video or sequence and to create review `.png` when input is single frame. In some cases the output should be created even if it's single frame or multi frame input.\n\n\n### Extract Burnin\n\nPlugin is responsible for adding burnins into review representations.\n\nBurnins are text values painted on top of input and may be surrounded with box in 6 available positions `Top Left`, `Top Center`, `Top Right`, `Bottom Left`, `Bottom Center`, `Bottom Right`.\n\n![presets_plugins_extract_burnin](../assets/presets_plugins_extract_burnin_01.png)\n\nThe Extract Burnin plugin creates new representations based on plugin presets, representations in instance and whether the reviewable matches the profile filter.\nA burnin can also be directly linked by name in the output definitions of the [Extract Review plug-in settings](#extract-review) so _can_ be triggered without a matching profile.\n\n#### Burnin formatting options (`options`)\n\nThe formatting options define the font style for the burnin texts.\nThe X and Y offset define the margin around texts and (background) boxes.\n\n#### Burnin profiles (`profiles`)\n\nPlugin process is skipped if `profiles` are not set at all. Profiles contain list of profile items. Each burnin profile may specify filters for **hosts**, **tasks** and **families**. Filters work the same way as described in [Profile Filters](#profile-filters).\n\n#### Profile burnins\n\nA burnin profile may set multiple burnin outputs from one input. The burnin's name represents the unique **filename suffix** to avoid overriding files with same name.\n\n| Key | Description | Type | Example |\n| --- | --- | --- | --- |\n| **Top Left** | Top left corner content. | str | \"{dd}.{mm}.{yyyy}\" |\n| **Top Centered** | Top center content. | str | \"v{version:0>3}\" |\n| **Top Right** | Top right corner content. | str | \"Static text\" |\n| **Bottom Left** | Bottom left corner content. | str | \"{asset}\" |\n| **Bottom Centered** | Bottom center content. | str | \"{username}\" |\n| **Bottom Right** | Bottom right corner content. | str | \"{frame_start}-{current_frame}-{frame_end}\" |\n\nEach burnin profile can be configured with additional family filtering and can\nadd additional tags to the burnin representation, these can be configured under\nthe profile's **Additional filtering** section.\n\n:::note Filename suffix\nThe filename suffix is appended to filename of the source representation. For\nexample, if the source representation has suffix **\"h264\"** and the burnin\nsuffix is **\"client\"** then the final suffix is **\"h264_client\"**.\n:::\n\n**Available keys in burnin content**\n\n- It is possible to use same keys as in [Anatomy](admin_settings_project_anatomy.md#available-template-keys).\n- It is allowed to use Anatomy templates themselves in burnins if they can be filled with available data.\n\n- Additional keys in burnins:\n\n  | Burnin key | Description |\n  | --- | --- |\n  | frame_start | First frame number. |\n  | frame_end | Last frame number. |\n  | current_frame | Frame number for each frame. |\n  | duration | Count number of frames. |\n  | resolution_width | Resolution width. |\n  | resolution_height | Resolution height. |\n  | fps | Fps of an output. |\n  | timecode | Timecode by frame start and fps. |\n  | focalLength | **Only available in Maya and Houdini**<br /><br />Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. |\n\n:::warning\n`timecode` is a specific key that can be **only at the end of content**. (`\"BOTTOM_RIGHT\": \"TC: {timecode}\"`)\n:::\n\n\n### IntegrateAssetNew\n\nSaves information for all published subsets into DB, published assets are available for other hosts, tools and tasks after.\n#### Template name profiles\n\nAllows to select [anatomy template](admin_settings_project_anatomy.md#templates) based on context of subset being published.\n\nFor example for `render` profile you might want to publish and store assets in different location (based on anatomy setting) then for `publish` profile.\n[Profile filtering](#profile-filters) is used to select between appropriate template for each context of published subsets.\n\nApplicable context filters:\n- **`hosts`** - Host from which publishing was triggered. `[\"maya\", \"nuke\"]`\n- **`tasks`** - Current task. `[\"modeling\", \"animation\"]`\n\n    ![global_integrate_new_template_name_profile](assets/global_integrate_new_template_name_profile.png)\n\n(This image shows use case where `render` anatomy template is used for subsets of families ['review, 'render', 'prerender'], `publish` template is chosen for all other.)\n\n#### Subset grouping profiles\n\nPublished subsets might be grouped together for cleaner and easier selection in the **[Subset Manager](artist_tools_subset_manager)**\n\nGroup name is chosen with use of [profile filtering](#profile-filters)\n\nApplicable context filters:\n- **`families`** - Main family of processed subset. `[\"plate\", \"model\"]`\n- **`hosts`** - Host from which publishing was triggered. `[\"maya\", \"nuke\"]`\n- **`tasks`** - Current task. `[\"modeling\", \"animation\"]`\n\n    ![global_integrate_new_template_name_profile](assets/global_integrate_new_subset_group.png)\n\n(This image shows use case where only assets published from 'photoshop', for all families for all tasks should be marked as grouped with a capitalized name of Task where they are published from.)\n\n## Tools\nSettings for OpenPype tools.\n\n### Creator\nSettings related to [Creator tool](artist_tools_creator).\n\n#### Subset name profiles\n![global_tools_creator_subset_template](assets/global_tools_creator_subset_template.png)\n\nSubset name helps to identify published content. More specific name helps with organization and avoid mixing of published content. Subset name is defined using one of templates defined in **Subset name profiles settings**. The template is filled with context information at the time of creation.\n\nUsage of template is defined by profile filtering using creator's family, host and task name. Profile without filters is used as default template and it is recommend to set default template. If default template is not available `\"{family}{Task}\"` is used.\n\n**Formatting keys**\n\nAll templates can contain text and formatting keys **family**, **task** and **variant** e.g. `\"MyStudio_{family}_{task}\"` (example - not recommended in production).\n\n|Key|Description|\n|---|---|\n|family|Creators family|\n|task|Task under which is creation triggered|\n|variant|User input in creator tool|\n\n**Formatting keys have 3 variants with different letter capitalization.**\n\n|Task|Key variant|Description|Result|\n|---|---|---|---|\n|`bgAnim`|`{task}`|Keep original value as is.|`bgAnim`|\n|`bgAnim`|`{Task}`|Capitalize first letter of value.|`BgAnim`|\n|`bgAnim`|`{TASK}`|Each letter which be capitalized.|`BGANIM`|\n\nTemplate may look like `\"{family}{Task}{Variant}\"`.\n\nSome creators may have other keys as their context may require more information or more specific values. Make sure you've read documentation of host you're using.\n\n\n### Publish\n\n#### Custom Staging Directory Profiles\nWith this feature, users can specify a custom data folder path based on presets, which can be used during the creation and publishing stages.\n\n![global_tools_custom_staging_dir](assets/global_tools_custom_staging_dir.png)\n\nStaging directories are used as a destination for intermediate files (as renders) before they are renamed and copied to proper location during the integration phase. They could be created completely dynamically in the temp folder or for some DCCs in the `work` area.\nExample could be Nuke where artist might want to temporarily render pictures into `work` area to check them before they get published with the choice of \"Use existing frames\" on the write node.\n\nOne of the key advantages of this feature is that it allows users to choose the folder for writing such intermediate files to take advantage of faster storage for rendering, which can help improve workflow efficiency. Additionally, this feature allows users to keep their intermediate extracted data persistent, and use their own infrastructure for regular cleaning.\n\nIn some cases, these DCCs (Nuke, Houdini, Maya) automatically add a rendering path during the creation stage, which is then used in publishing. Creators and extractors of such DCCs need to use these profiles to fill paths in DCC's nodes to use this functionality.\n\n:::note\nMaya's setting `project_settings/maya/RenderSettings/default_render_image_folder` is be overwritten by the custom staging dir.\n:::\n\nThe custom staging folder uses a path template configured in `project_anatomy/templates/others` with `transient` being a default example path that could be used. The template requires a 'folder' key for it to be usable as custom staging folder.\n\n##### Known issues\n- Any DCC that uses prefilled paths and store them inside of workfile nodes needs to implement resolving these paths with a configured profiles.\n- If studio uses Site Sync remote artists need to have access to configured custom staging folder!\n- Each node on the rendering farm must have access to configured custom staging folder!\n\n### Workfiles\nAll settings related to Workfile tool.\n\n#### Open last workfile at launch\nThis feature allows you to define a rule for each task/host or toggle the feature globally to all tasks as they are visible in the picture.\n\n![global_tools_workfile_open_last_version](assets/global_tools_workfile_open_last_version.png)\n"
  },
  {
    "path": "website/docs/project_settings/settings_project_nuke.md",
    "content": "---\nid: settings_project_nuke\ntitle: Project Nuke Setting\nsidebar_label: Nuke\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nProject settings can have project specific values. Each new project is using studio values defined in **default** project but these values can be modified or overridden per project.\n\n:::warning Default studio values\nProjects always use default project values unless they have [project override](../admin_settings#project-overrides) (orange colour). Any changes in default project may affect all existing projects.\n:::\n\n## Workfile Builder\n\nAll Workfile Builder related settings can be found here. This is a list of available features:\n- Create first workfile\n- Custom Template path (task type filtered)\n- Run Builder profiles at first run\n- Define Builder Profiles With Filters\n\n![nuke_workfile_options_location](assets/nuke_workfile_builder_location.png)\n\n:::important Auto Load Last Version\nIn case you want to set the auto load of the latest available version of workfiles, you can do it from [here](settings_project_global#open-last-workfile-at-launch).\n:::\n\n### Create first workfile\n\nBy switchintg this feature on, OpenPype will generate initial workfile version. Following attributes are possible to configure:\n\n![nuke_workfile_options_create_first_version](assets/nuke_workfile_builder_create_first_workfile.png)\n\n#### Custom templates\nCustom templates are added into nuke's node graph as nodes. List of task types can be defined for templates filtering.\n\n- Task types are sourced from project related Anatomy/Task Types\n\n![nuke_workfile_builder_template_task_type](assets/nuke_workfile_builder_template_task_type.png)\n\n - multi platform path can be used in a variety of ways. Along the absolute path to a template file also an python formatting could be used. At the moment these keys are supported (image example below):\n   - `root[key]`: definitions from anatomy roots\n   - `project[name, code]`: project in context\n   - `asset`: name of asset/shot in context\n   - `task[type, name, short_name]`: as they are defined on asset/shot and in **Anatomy/Task Type** on project context\n\n![nuke_workfile_builder_template_anatomy](assets/nuke_workfile_builder_template_anatomy.png)\n\n#### Run Builder profiles on first launch\nEnabling this feature will look into available Builder's Prorfiles (look below for more information about this feature) and load available versions into node graph.\n\n### Profiles (Builder)\nBuilder profiles are set of rules allowing artist Load any available versions for the context of the asset, which it is run from. Preset is having following attributes:\n\n- **Filter**: Each profile could be defined with task filter. In case no filter is defined, a profile will be working for all.\n\n- **Context section**: filtres for subset name (regex accepted), families, representation names and available Loader plugin.\n\n- **Linked Assets/Shots**: filters for asset builds to be added\n\n![nuke_workfile_builder_profiles](assets/nuke_workfile_builder_profiles.png)\n"
  },
  {
    "path": "website/docs/project_settings/settings_project_standalone.md",
    "content": "---\nid: settings_project_standalone\ntitle: Project Standalone Publisher Setting\nsidebar_label: Standalone Publisher\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nProject settings can have project specific values. Each new project is using studio values defined in **default** project but these values can be modified or overridden per project.\n\n:::warning Default studio values\nProjects always use default project values unless they have [project override](../admin_settings#project-overrides) (orange colour). Any changes in default project may affect all existing projects.\n:::\n\n## Creator Plugins\n\nContains list of implemented families to show in middle menu in Standalone Publisher. Each plugin must contain:\n- name\n- label\n- family\n- icon\n- default subset(s)\n- help (additional short information about family)\n\n![example of creator plugin](assets/standalone_creators.png)\n\n## Publish plugins\n\n### Collect Textures\n\nServes to collect all needed information about workfiles and textures created from those. Allows to publish \nmain workfile (for example from Mari), additional worfiles (from Substance Painter) and exported textures.\n\nAvailable configuration:\n- Main workfile extension - only single workfile can be \"main\" one\n- Support workfile extensions - additional workfiles will be published to same folder as \"main\", just under `resourses` subfolder\n- Texture extension - what kind of formats are expected for textures\n- Additional families for workfile - should any family ('ftrack', 'review') be added to published workfile\n- Additional families for textures - should any family ('ftrack', 'review') be added to published textures\n\n#### Naming conventions\n\nImplementation tries to be flexible and cover multiple naming conventions for workfiles and textures.\n\n##### Workfile naming pattern\n\nProvide regex matching pattern containing regex groups used to parse workfile name to learn needed information. (For example\nbuild name.)\n\nExample:\n\n- pattern: ```^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+``` \n- with groups: ```[\"asset\", \"filler\", \"version\"]```\n  \nparses `corridorMain_v001` into three groups:\n- asset build (`corridorMain`)\n- filler (in this case empty)\n- version (`001`)\n\nAdvanced example (for texture files):\n\n- pattern: ```^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+``` \n- with groups: ```[\"asset\", \"shader\", \"version\", \"channel\", \"color_space\", \"udim\"]```\n  \nparses `corridorMain_aluminiumID_v001_baseColor_linsRGB_1001.exr`:\n- asset build (`corridorMain`)\n- shader (`aluminiumID`)\n- version (`001`)\n- channel (`baseColor`)\n- color_space (`linsRGB`)\n- udim (`1001`)\n\n\nIn case of different naming pattern, additional groups could be added or removed. Number of matching groups (`(...)`) must be same as number of items in `Group order for regex patterns`\n\n##### Workfile group positions\n\nFor each matching regex group set in previous paragraph, its ordinal position is required (in case of need for addition of new groups etc.)\n\nNumber of groups added here must match number of parsing groups from `Workfile naming pattern`.\n\n##### Output names\n\nOutput names of published workfiles and textures could be configured separately:\n- Subset name template for workfile\n- Subset name template for textures (implemented keys: [\"color_space\", \"channel\", \"subset\", \"shader\"])\n\n\n### Validate Scene Settings\n\n#### Check Frame Range for Extensions\n\nConfigure families, file extension and task to validate that DB setting (frame range) matches currently published values.\n\n### ExtractThumbnailSP\n\nPlugin responsible for generating thumbnails, configure appropriate values for your version o ffmpeg."
  },
  {
    "path": "website/docs/pype2/admin_anatomy.md",
    "content": "---\nid: admin_anatomy\ntitle: Project Anatomy\nsidebar_label: Folder Structure\n---\n\n## PROJECT Structure\n\nThis is example project structure when using Pype:\n\n```text\nProject\n  ├───assets\n  │   ├───Bob\n  │   └───...\n  └───episodes\n      └───ep01\n          └───sq01\n              └───ep01_sq01_sh001\n                  ├───publish\n                  └───work\n```\n\n:::note Shot naming\nWe do strongly recommend to name shots with their full hierarchical name. Avalon doesn't allow two assets with same name in project. Therefore if you have for example:\n\n```text\nsequence01 / shot001\n```\nand then\n```text\nsequence02 / shot001\n```\nyou'll run into trouble because there are now two `shot001`.\n\nBetter way is to use full qualified name for shot. So the above become:\n```text\nsequence01 / sequence01_shot001\n```\n\nThis has two advantages: there will be no duplicities this way and artists can see just by looking at filename the whole hierarchy.\n:::\n\n## ASSET Structure\n\n```text\nBob\n  ├───publish\n  │   ├───model\n  │   │   ├───modelMain\n  │   │   ├───modelProxy\n  │   │   └───modelSculpt\n  │   ├───workfile\n  │   │   └───taskName\n  │   ├───rig\n  │   │   └───rigMain\n  │   ├───look\n  │   │   ├───lookMain\n  │   │   │   └───v01\n  │   │   │       └───texture\n  │   │   └───lookWet\n  │   ├───camera\n  │   │   ├───camMain\n  │   │   └───camLayout\n  │   ├───cache\n  │   │   ├───cacheChar01\n  │   │   └───cacheRock01\n  │   ├───vrproxy\n  │   ├───fx\n  │   └───setdress\n  └───work\n      ├───concept\n      ├───fur\n      ├───modelling\n      ├───rig\n      ├───look\n      └───taskName\n```\n"
  },
  {
    "path": "website/docs/pype2/admin_config.md",
    "content": "---\nid: admin_config\ntitle: Studio Config\nsidebar_label: Studio Config\n---\n\nAll of the studio specific configurations are stored as simple JSON files in the **pype-config** repository.\n\nConfig is split into multiple sections described below.\n\n## Anatomy\n\nDefines where and how folders and files are created for all the project data. Anatomy has two parts **Roots** and **Templates**.\n\n:::warning\nIt is recommended to create anatomy [overrides](#per-project-configuration) for each project even if values haven't changed. Ignoring this recommendation may cause catastrophic consequences.\n:::\n\n### Roots\nRoots define where files are stored with path to shared folder. You can set them in `roots.json`.\nIt is required to set root path for each platform you are using in studio. All paths must point to same folder!\n```json\n{\n    \"windows\": \"P:/projects\",\n    \"darwin\": \"/Volumes/projects\",\n    \"linux\": \"/mnt/share/projects\"\n}\n```\n\nIt is possible to set multiple roots when necessary. That may be handy when you need to store specific type of data on another disk. In that case you'll have to add one level in json.\n```json\n{\n    \"work\": {\n        \"windows\": \"P:/work\",\n        \"darwin\": \"/Volumes/work\",\n        \"linux\": \"/mnt/share/work\"\n    },\n    \"publish\": {\n        \"windows\": \"Y:/publish\",\n        \"darwin\": \"/Volumes/publish\",\n        \"linux\": \"/mnt/share/publish\"\n    }\n}\n```\nUsage of multiple roots is explained below in templates part.\n\n### Templates\nTemplates define project's folder structure and filenames. You can set them in `default.yaml`.\n\n### Required templates\nWe have a few required anatomy templates for Pype to work properly, however we keep adding more when needed.\n\n```yaml\nwork:\n  folder: \"{root}/{project[name]}/{hierarchy}/{asset}/work/{task}\"\n  file: \"{project[code]}_{asset}_{task}_v{version:0>3}<_{comment}>.{ext}\"\n  path: \"{root}/{project[name]}/{hierarchy}/{asset}/work/{task}/{project[code]}_{asset}_{task}_v{version:0>3}<_{comment}>.{ext}\"\n\npublish:\n  folder: \"{root}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/v{version:0>3}\"\n  file: \"{project[code]}_{asset}_{subset}_v{version:0>3}<.{frame}>.{representation}\"\n  path: \"{root}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/v{version:0>3}/{project[code]}_{asset}_{subset}_v{version:0>3}<.{frame}>.{representation}\"\n```\n\nTemplate groups `work` and `publish` must be set in all circumstances. Both must have set keys as shown `folder`, holds path template for the directory where the files are stored, `file` only holds the filename and `path` combines the two together for quicker access.\n\n### Available keys\n| Context key | Description |\n| --- | --- |\n| root | Path to root folder |\n| root[\\<root name\\>] | Path to root folder when multiple roots are used.<br />Key `<root name>` represents root key specified in `roots.json` |\n| project[name] | Project's full name. |\n| project[code] | Project's code. |\n| hierarchy | All hierarchical parents as subfolders. |\n| asset | Name of asset or shot. |\n| task | Name of task. |\n| version | Version number. |\n| subset | Subset name. |\n| family | Main family name. |\n| ext | File extension. (Possible to use only in `work` template atm.) |\n| representation | Representation name. (Is used instead of `ext` except `work` template atm.) |\n| frame | Frame number for sequence files. |\n| output |  |\n| comment |  |\n\n:::warning\nBe careful about using `root` key in templates when using multiple roots. It is not allowed to combine both `{root}` and `{root[<root name>]}` in templates.\n:::\n:::note\nIt is recommended to set padding for `version` which is possible with additional expression in template. Entered key `{version:0<3}` will result into `001` if version `1` is published.\n**Explanation:** Expression `0<3` will add `\"0\"` char to the beginning(`<`) until string has `3` characters.\n:::\n\n| Date-Time key | Example result | Description |\n| --- | --- | --- |\n| d | 1, 30 | Day of month in shortest possible way. |\n| dd | 01, 30 | Day of month with 2 digits. |\n| ddd | Mon | Shortened week day name. |\n| dddd | Monday | Full week day name. |\n| m | 1, 12 | Month number in shortest possible way. |\n| mm | 01, 12 | Month number with 2 digits. |\n| mmm | Jan | Shortened month name. |\n| mmmm | January | Full month name. |\n| yy | 20 | Shortened year. |\n| yyyy | 2020 | Full year. |\n| H | 4, 17 | Shortened 24-hour number. |\n| HH | 04, 17 | 24-hour number with 2 digits. |\n| h | 5 | Shortened 12-hour number. |\n| hh | 05 | 12-hour number with 2 digits. |\n| ht | AM, PM | Midday part. |\n| M | 0 | Shortened minutes number. |\n| MM | 00 | Minutes number with 2 digits. |\n| S | 0 | Shortened seconds number. |\n| SS | 00 | Seconds number with 2 digits. |\n\n### Optional keys\nKeys may be optional for some reason when are wrapped with `<` and `>`. But it is recommended to use only for these specific keys with obvious reasons:\n- `output`, `comment` are optional to fill\n- `frame` is used only for sequences.\n\n### Inner keys\nIt is possible to use value of one template key inside value of another template key. This can be done only per template group, which means it is not possible to use template key from `publish` group inside `work` group.\n\nUsage is similar to using template keys but instead of `{key}` you must add `@` in front of key: `{@key}`\n\nWith this feature `work` template from example above may be much easier to read and modify.\n```yaml\nwork:\n  folder: \"{root}/{project[name]}/{hierarchy}/{asset}/work/{task}\"\n  file: \"{project[code]}_{asset}_{task}_v{version:0>3}<_{comment}>.{ext}\"\n  path: \"{@folder}/{@file}\"\n  # This is how `path` key will look as result\n  # path: \"{root}/{project[name]}/{hierarchy}/{asset}/work/{task}/{project[code]}_{asset}_{task}_v{version:0>3}<_{comment}>.{ext}\"\n```\n\n:::warning\nBe aware of unsolvable recursion in inner keys.\n```yaml\ngroup:\n  # Use key where source key is used in value\n  key_1: \"{@key_2}\"\n  key_2: \"{@key_1}\"\n\n  # Use itself\n  key_3: \"{@key_3}\"\n```\n:::\n\n### Global keys\nGlobal keys are keys with value outside template groups. All these keys will be available in each template group with ability to override them inside the group.\n\n**Source**\n```yaml\n# Global key outside template group\nglobal_key: \"global value\"\n\ngroup_1:\n  # `global_key` is not set\n  example_key_1: \"{example_value_1}\"\n\ngroup_2:\n  # `global_key` is iverrided\n  global_key: \"overridden global value\"\n```\n**Result**\n```yaml\nglobal_key: \"global value\"\n\ngroup_1:\n  # `global_key` was added\n  global_key: \"global value\"\n  example_key_1: \"{example_value_1}\"\n\ngroup_2:\n  # `global_key` kept it's value for `group_2`\n  global_key: \"overridden global value\"\n```\n\n### Combine Inner keys with Global keys\nReal power of [Inner](#inner-keys) and [Global](#global-keys) keys is their combination.\n\n**Template source**\n```yaml\n# PADDING\nframe_padding: 4\nframe: \"{frame:0>frame_padding}\"\n# MULTIPLE ROOT\nroot_name: \"root_name_1\"\nroot: {root[{@root_name}]}\n\ngroup_1:\n  example_key_1: \"{@root}/{@frame}\"\n\ngroup_2:\n  frame_padding: 3\n  root_name: \"root_name_2\"\n  example_key_2: \"{@root}/{@frame}\"\n\ngroup_3:\n  frame: \"{frame}\"\n  example_key_3: \"{@root}/force_value/{@frame}\"\n```\n**Equals**\n```yaml\nframe_padding: 4\nframe: \"{frame:0>3}\"\nroot_name: \"root_name_1\"\nroot: {root[root_name_1]}\n\ngroup_1:\n  frame_padding: 4\n  frame: \"{frame:0>3}\"\n  root_name: \"root_name_1\"\n  root: {root[root_name_1]}\n  # `example_key_1` result\n  example_key_1: \"{root[root_name_1]}/{frame:0>3}\"\n\ngroup_2:\n  frame_padding: 3\n  frame: \"{frame:0>3}\"\n  root_name: \"root_name_2\"\n  root: {root[root_name_2]}\n  # `example_key_2` result\n  example_key_2: \"{root[root_name_2]}/{frame:0>2}\"\n\ngroup_3:\n  frame_padding: 4\n  frame: \"{frame}\"\n  root_name: \"root_name_1\"\n  root: {root[root_name_1]}\n  # `example_key_3` result\n  example_key_3: \"{root[root_name_1]}/force_value/{frame}\"\n```\n\n:::warning\nBe careful about using global keys. Keep in mind that **all global keys** will be added to **all template groups** and all inner keys in their values **MUST** be in the group.\nFor example in [required templates](#required-templates) it seems that `path: \"{@folder}/{@file}\"` should be used as global key, but that would require all template groups have `folder` and `file` keys which is not true by default.\n:::\n\n## Environments\n\nHere is where all the environment variables are set up. Each software has it's own environment file where we set all variables needed for it to function correctly. This is also a place where any extra in-house variables should be added. All of these individual configs and then loaded additively as needed based on current context.\n\nFor example when launching Pype Tray, **Global** and **Avalon** envs are loaded first. If the studio uses also *Deadline* and *Ftrack*, both of those environments get added at the same time. This sets the base environment for the rest of the pipeline that will be inherited by all the applications launched from this point on.\n\nWhen user launches an application for a task, its general and versioned env files get added to the base before the software launches. When launching *Maya 2019*, both `maya.json` and `maya_2019.json` will be added.\n\nIf the project or task also has extra tools configured, say *Arnold Mtoa 3.1.1*, a config JSON with the same name will be added too.\n\nThis way the environment is completely dynamic with possibility of overrides on a granular level, from project all the way down to task.\n\n## Launchers\n\nConsidering that different studios use different ways of deploying software to their workstations, we need to tell Pype how to launch all the individual applications available in the studio.\n\nEach software need multiple files prepared for it to function correctly.\n\n```text\napplication_name.toml\napplication_name.bat\napplication_name.sh\n```\n\nTOML file tells Pype how to work with the application across the board. Icons, Label in GUI, *Ftrack* settings but most importantly it defines what executable to run. These executable are stored in the windows and linux subfolder in the launchers folder. If `application_name.toml` defines that executable to run is `application_name`, Pype assumes that a `.bat` and `.sh` files under that name exist in the linux and windows folders in launchers. Correct version is picked automatically based on the platform Pype is running on.\n\nThese `.bat` and `.sh` scripts have only one job then. They have to point to the exact executable path on the system, or to a command that will launch the app we want. Version granularity is up to the studio to decide. We can show artists Nuke 11.3, while specifying the particular version 11.3v4 only in the .bat file, so the artist doesn't need to deal with it, or we can present him with 11.3v4 directly. the choice is mostly between artist control vs more configuration files on the system.\n\n## Presets\n\nThis is where most of the functions configuration of the pipeline happens. Colorspace, data types, burnin setting, geometry naming conventions, ftrack attributes, playblast settings, types of exports and lot's of other settings.\n\nPresets are categorized in folders based on what they control or what host (DCC application) they are for. We're slowly working on documenting them all, but new ones are being created regularly as well. Hopefully the categories and names are sufficiently self-explanatory.\n\n### colorspace\n\nDefines all available color spaces in the studio. These configs not only tell the system what OCIO to use, but also how exactly it needs to be applied in the give application. From loading the data, through previewing it all the way to rendered\n\n### Dataflow\n\nDefines allowed file types and data formats across the pipeline including their particular coded and compression settings.\n\n### Plugins\n\nAll the creator, loader and publisher configurations are stored here. We can override any properties of the default plugin values and more.\n\n#### How does it work\n\nOverriding plugin properties is as simple as adding what needs to be changed to\nJSON file along with plugin name.\n\nSay you have name validating plugin:\n\n```python\nimport pyblish.api\n\n\nclass ValidateModelName(pyblish.api.InstancePlugin):\n\n    order = pype.api.ValidateContentsOrder\n    hosts = ['maya']\n    families = ['model']\n    label = 'Validate Mesh Name'\n\n    # check for: 'foo_001_bar_GEO`\n    regex = r'.*_\\d*_.*_GEO'\n\n    def process(self, instance):\n      # pseudocode to get nodes\n      models = get_models(instance.data.get(\"setMembers\", None))\n      r = re.compile(self.regex)\n      for model in models:\n            m = r.match(obj)\n            if m is None:\n              raise RuntimeError(\"invalid name on {}\".format(model))\n\n```\n_This is just non-functional example_\n\nInstead of creating new plugin with different regex, you can put:\n\n```javascript\n\"ValidateModelName\": {\n  \"regex\": \".*\\\\d*_.*_geometry\"\n}\n```\nand put it into `repos/pype-config/presets/plugins/maya/publish.json`. There can be more entries\nlike that for how many plugins you need.\n\nThat will effectively replace regex defined in plugin during runtime with the one you've just\ndefined in JSON file. This way you can change any properties defined in plugin.\n\n:::tip loader and creators\nSimilar way exist for *Loaders* and *Creators*. Use files `create.json` for Creators, `load.json`\nfor Loaders and `publish.json` for **Pyblish** plugins like extractors, validators, etc.\n\nPreset resolution works by getting host name (for example *Maya*) and then looking inside\n `repos/pype-config/presets/plugins/<host>/publish.json` path. If plugin is not found, then\n `repos/pype-config/presets/plugins/global/publish.json` is tried.\n:::\n\n:::tip Per project plugin override\nYou can override plugins per project. See [Per-project configuration](#per-project-configuration)\n:::\n\n\n## Schema\n\nHolds all database schemas for *mongoDB*, that we use. In practice these are never changed on a per studio basis, however we included them in the config for cases where a particular project might need a very individual treatment.\n\n## Per-project configuration\n\nYou can have per-project configuration with Pype. This allows you to have for example different\nvalidation requirements, file naming, etc.\n\nThis is very easy to set up - point `PYPE_PROJECT_CONFIGS` environment variable to place\nwhere you want those per-project configurations. Then just create directory with project name and\nthat's almost it. Inside, you can follow hierarchy of **pype-config** presets. Everything put there\nwill override stuff in **pype-config**.\n\n### Example\n\nYou have a project where you need to disable some validators - let's say overlapping\nUVs validator in Maya.\n\nProject name is *FooProject*.\nYour `PYPE_PROJECT_CONFIGS` points to `/studio/pype/projects`.\n\nCreate projects settings directory:\n```sh\nmkdir $PYPE_PROJECT_CONFIGS/FooProject\n```\nNow you can use plugin overrides to disable validator:\n\nPut:\n```javascript\n{\n  \"ValidateMeshHasOverlappingUVs\": {\n    \"enabled\": false\n  }\n}\n```\ninto:\n\n```sh\n$PYPE_PROJECT_CONFIGS/FooPoject/presets/plugins/maya/publish.json\n```\n\nAnd its done. **ValidateMeshHasOverlappingUVs** is a class name of validator - you can\nfind that name by looking into python file containing validator code, or in Pyblish GUI.\n\nThat way you can make it optional or set whatever properties you want on plugins and those\nsettings will take precedence over the default site-wide settings.\n"
  },
  {
    "path": "website/docs/pype2/admin_ftrack.md",
    "content": "---\nid: admin_ftrack\ntitle: Ftrack Setup\nsidebar_label: Ftrack Setup\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n\nFtrack is currently the main project management option for Pype. This documentation assumes that you are familiar with Ftrack and its basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](http://ftrack.rtd.ftrack.com/en/stable/).\n\n## Prepare Ftrack for Pype\n\nIf you want to connect Ftrack to Pype you might need to make few changes in Ftrack settings. These changes would take a long time to do manually, so we prepared a few Ftrack actions to help you out. First, you'll need to launch Pype's tray application and set [Ftrack credentials](#credentials) to be able to run our Ftrack actions.\n\nThe only action that is strictly required is [Pype Admin - Create/Update Avalon Attributes](manager_ftrack_actions#create-update-avalon-attributes), which creates and sets the Custom Attributes necessary needed for Pype to function. If you want to use pype only for new projects then you should read about best practice with [new project](#new-project).\n\nIf you want to switch projects that are already in production, you might also need to run [Pype Doctor - Custom attr doc](manager_ftrack_actions#custom-attr-doc).\n\n:::caution\nKeep in mind that **Custom attr doc** action will migrate certain attributes from ftrack default ones to our custom attributes. Some attributes will also be renamed. We make backup of the values, but be very careful with this option and consults us before running it.\n:::\n\n## Event Server\n\nFtrack Event Server is the key to automation of many tasks like _status change_, _thumbnail update_, _automatic synchronization to Avalon database_ and many more. Event server should run at all times to perform all the required processing as it is not possible to catch some of them retrospectively with enough certainty.\n\n### Running event server\n\nThere are specific launch arguments for event server. With `$PYPE_SETUP/pype eventserver` you can launch event server but without prior preparation it will terminate immediately. The reason is that event server requires 3 pieces of information: _Ftrack server url_, _paths to events_ and _Credentials (Username and API key)_. Ftrack server URL and Event path are set from Pype's environments by default, but the credentials must be done separatelly for security reasons.\n\n\n\n:::note There are 2 ways of passing your credentials to event server.\n\n<Tabs\n  defaultValue=\"args\"\n  values={[\n    {label: 'Additional Arguments', value: 'args'},\n    {label: 'Environments Variables', value: 'env'}\n  ]}>\n\n<TabItem value=\"args\">\n\n-  **`--ftrack-user \"your.username\"`** : Ftrack Username\n-   **`--ftrack-api-key \"00000aaa-11bb-22cc-33dd-444444eeeee\"`** : User's API key\n-   **`--store-crededentials`** : Entered credentials will be stored for next launch with this argument _(It is not needed to enter **ftrackuser** and **ftrackapikey** args on next launch)_\n-   **`--no-stored-credentials`** : Stored credentials are loaded first so if you want to change credentials use this argument\n-   `--ftrack-url \"https://yourdomain.ftrackapp.com/\"` : Ftrack server URL _(it is not needed to enter if you have set `FTRACK_SERVER` in Pype' environments)_\n-   `--ftrack-events-path \"//Paths/To/Events/\"` : Paths to events folder. May contain multiple paths separated by `;`. _(it is not needed to enter if you have set `FTRACK_EVENTS_PATH` in Pype' environments)_\n\nSo if you want to use Pype's environments then you can launch event server for first time with these arguments `$PYPE_SETUP/pype eventserver --ftrack-user \"my.username\" --ftrack-api-key \"00000aaa-11bb-22cc-33dd-444444eeeee\" --store-credentials`. Since that time, if everything was entered correctly, you can launch event server with `$PYPE_SETUP/pype eventserver`.\n\n</TabItem>\n<TabItem value=\"env\">\n\n- `FTRACK_API_USER` - Username _(\"your.username\")_\n- `FTRACK_API_KEY` - User's API key _(\"00000aaa-11bb-22cc-33dd-444444eeeee\")_\n- `FTRACK_SERVER` - Ftrack server url _(\"<https://yourdomain.ftrackapp.com/\">)_\n- `FTRACK_EVENTS_PATH` - Paths to events _(\"//Paths/To/Events/\")_\n    We do not recommend you this way.\n\n</TabItem>\n</Tabs>\n:::\n\n:::caution\nWe do not recommend setting your ftrack user and api key environments in a persistent way, for security reasons. Option 1. passing them as arguments is substantially safer.\n:::\n\n### Where to run event server\n\nWe recommend you to run event server on stable server machine with ability to connect to Avalon database and Ftrack web server. Best practice we recommend is to run event server as service.\n\n:::important\nEvent server should **not** run more than once! It may cause big pipeline issues.\n:::\n\n### Which user to use\n\n-   must have at least `Administrator` role\n-   same user should not be used by an artist\n\n### Run Linux service - step by step\n\n1.  create file:\n    `sudo vi /opt/pype/run_event_server.sh`\n\n2.  add content to the file:\n\n```sh\nexport PYPE_DEBUG=3\npushd /mnt/pipeline/prod/pype-setup\n. pype eventserver --ftrack-user <pype-admin-user> --ftrack-api-key <api-key>\n```\n\n3.  create service file:\n    `sudo vi /etc/systemd/system/pype-ftrack-event-server.service`\n\n4.  add content to the service file\n\n```toml\n[Unit]\nDescription=Run Pype Ftrack Event Server Service\nAfter=network.target\n\n[Service]\nType=idle\nExecStart=/opt/pype/run_event_server.sh\nRestart=on-failure\nRestartSec=10s\n\n[Install]\nWantedBy=multi-user.target\n```\n\n5.  change file permission:\n    `sudo chmod 0755 /etc/systemd/system/pype-ftrack-event-server.service`\n\n6.  enable service:\n    `sudo systemctl enable pype-ftrack-event-server`\n\n7.  start service:\n    `sudo systemctl start pype-ftrack-event-server`\n\n* * *\n\n## Ftrack events\n\nEvents are helpers to automation. They react to Ftrack Web Server events like change entity attribute, create of entity, etc. .\n\n### Delete Avalon ID from new entity _(DelAvalonIdFromNew)_\n\nIs used to remove value from `Avalon/Mongo Id` Custom Attribute when entity is created.\n\n`Avalon/Mongo Id` Custom Attribute stores id of synchronized entities in pipeline database. When user _Copy -> Paste_ selection of entities to create similar hierarchy entities, values from Custom Attributes are copied too. That causes issues during synchronization because there are multiple entities with same value of `Avalon/Mongo Id`. To avoid this error we preventively remove these values when entity is created.\n\n### Next Task update _(NextTaskUpdate)_\n\nChange status of next task from `Not started` to `Ready` when previous task is approved.\n\nMultiple detailed rules for next task update can be configured in the presets.\n\n### Synchronization to Avalon database _(Sync_to_Avalon)_\n\nAutomatic [synchronization to pipeline database](manager_ftrack#synchronization-to-avalon-database).\n\nThis event updates entities on their changes Ftrack. When new entity is created or existing entity is modified. Interface with listing information is shown to users when [synchronization rules](manager_ftrack#synchronization-rules) are not met. This event may also undo changes when they might break pipeline. Namely _change name of synchronized entity_, _move synchronized entity in hierarchy_.\n\n:::important\nDeleting an entity by Ftrack's default is not processed for security reasons _(to delete entity use [Delete Asset/Subset action](manager_ftrack_actions#delete-asset-subset))_.\n:::\n\n### Synchronize hierarchical attributes _(SyncHierarchicalAttrs)_\n\nAuto-synchronization of hierarchical attributes from Ftrack entities.\n\nRelated to [Synchronize to Avalon database](#synchronization-to-avalon-database) event _(without it, it makes no sense to use this event)_. Hierarchical attributes must be synchronized with special way so we needed to split synchronization into 2 parts. There are [synchronization rules](manager_ftrack#synchronization-rules) for hierarchical attributes that must be met otherwise interface with messages about not meeting conditions is shown to user.\n\n### Thumbnails update _(ThumbnailEvents)_\n\nUpdates thumbnail of Task and it's parent when new Asset Version with thumbnail is created.\n\nThis is normally done by Ftrack Web server when Asset Version is created with Drag&Drop but not when created with Ftrack API.\n\n### Version to Task status _(VersionToTaskStatus)_\n\nUpdates Task status based on status changes on it's `AssetVersion`.\n\nThe issue this solves is when Asset version's status is changed but the artist assigned to Task is looking at the task status, thus not noticing the review.\n\nThis event makes sure statuses Asset Version get synced to it's task. After changing a status on version, this event first tries to set identical status to version's parent (usually task). At this moment there are a few more status mappings hardcoded into the system. If Asset version's status was changed to:\n\n-   `Reviewed` then Task's status will be changed to `Change requested`\n-   `Approved` then Task's status will be changed to `Complete`\n\n\n### Update First Version status _(FirstVersionStatus)_\n\nThis event handler allows setting of different status to a first created Asset Version in ftrack.\n\nThis is useful for example if first version publish doesn't contain any actual reviewable work, but is only used for roundtrip conform check, in which case this version could receive status `pending conform` instead of standard `pending review`\n\nBehaviour can be filtered by `name` or `type` of the task assigned to the Asset Version. Configuration can be found in [ftrack presets](admin_presets_ftrack#first_version_status-dict)\n\n* * *\n\n## Credentials\n\nIf you want to be able use Ftrack actions with Pype tray or [event server](#event-server) you need to enter credentials. The credentials required for Ftrack are `Username` and `API key`.\n\n### Credentials in tray\n\nHow to handle with credentials in tray is described [here](#artist_ftrack#first-use-best-case-scenario).\n\n### Credentials in event server\n\nHow to enter credentials to event server is described [here](#how-to-run-event-server).\n\n### Where to find API key\n\nPlease check the [official documentation](http://ftrack.rtd.ftrack.com/en/backlog-scaling-ftrack-documentation-story/developing/api_keys.html).\n"
  },
  {
    "path": "website/docs/pype2/admin_hosts.md",
    "content": "---\nid: admin_hosts\ntitle: Hosts Setup\nsidebar_label: Hosts Setup\n---\n\n## Host configuration\n\nTo add new host application (for example new version of Autodesk Maya, etc.) just follow these steps:\n\n### Launchers\n\nYou can find **launchers** in `repos/pype-config`. You can notice there is a bunch of **[TOML](https://en.wikipedia.org/wiki/TOML)** files and Linux and Windows shell scripts in their respective folders. **TOML** file\nholds basic metadata information for host application. Their naming convention is important and follow this pattern:\n\n```fix\napp_name[_version].toml\n```\n\nfor example `maya_2020.toml` or `nuke_11.3.toml`. More about it later. For now, lets look on content of one of these files:\n\n```toml\nexecutable = \"unreal\"\nschema = \"avalon-core:application-1.0\"\napplication_dir = \"unreal\"\nlabel = \"Unreal Editor 4.24\"\nftrack_label = \"UnrealEditor\"\nicon =\"ue4_icon\"\nlaunch_hook = \"pype/hooks/unreal/unreal_prelaunch.py/UnrealPrelaunch\"\nftrack_icon = '{}/app_icons/ue4.png'\n```\n\n* `executable` - specifies name (without extension) of shell script launching application (in windows/linux/darwin folders)\n* `schema` - not important, specifying type of metadata\n* `application_dir` - this specifies name of folder used in **app** key in [anatomy templates](admin_config#anatomy)\n* `label` - name of application to show in launcher\n* `ftrack_label` - name under which this application is show in ftrack actions (grouped by)\n* `icon` - application icon used in avalon launcher\n* `launch_hook` - path to Python code to execute before application is started (currently only from ftrack action)\n* `ftrack_icon` - icon used in ftrack\n\n### Environments\n\nYou can modify environment variables for you application in `repos/pype-config/environments`. Those files are\n[JSON](https://en.wikipedia.org/wiki/JSON) files. Those file are loaded and processed in somewhat hierarchical way. For example - for Autodesk Maya 2020, first file named `maya.json` is processed and then `maya_2020.json` is. Syntax is following:\n\n```json\n{\n  \"VARIABLE\": \"123\",\n  \"NEXT_VARIABLE\": \"{VARIABLE}4\",\n  \"PLATFORMS\": {\n    \"windows\": \"set_on_windows\",\n    \"linux\": \"set_on_linux\",\n    \"darwin\": \"set_on_max\"\n  },\n  \"PATHS: [\n    \"paths/1\", \"path/2\", \"path/3\"\n  ]\n}\n```\n\nThis will result on windows in environment with:\n\n```sh\nVARIABLE=\"123\"\nNEXT_VARIABLE=\"1234\"\nPLATFORMS=\"set_on_windows\"\nPATHS=\"path/1;path/2;path/3\"\n```\n\n### Ftrack\n\nYou need to add your new application to ftrack so it knows about it. This is done in System Preferences of\nftrack in `Advanced:Custom Attributes`. There you can find `applications` attribute. It looks like this:\n\n![Ftrack - custom attributes - applications](../assets/ftrack/ftrack-custom_attrib_apps.jpg)\n\nMenu/value consists of two rows per application - first row is application name and second is basically filename of this **TOML** file mentioned above without `.toml` extension. After you add or modify whatever you need here, you need to add you new application to project in ftrack. Just open project Info in ftrack, find out\n**Applications** and add your new application there. If you are running [event server](admin_ftrack#event-server) then this information is synced to avalon automatically. If not, you need to sync it manually by running **Sync to Avalon** action.\n\nNow, restart Pype and your application should be ready.\n\n### Conclusion\n\nTo wrap it up:\n\n- create your shell scripts to launch application (don't forget to set correct OS permissions)\n- create **TOML** file pointing to shell scripts, set you icons and labels there\n- check or create you environment **JSON** file in `environments` even if it is empty (`{}`)\n- to make it work with ftrack, modify **applications** in *Custom Attributes*, add it to your project and sync\n- restart Pype\n\n## Autodesk Maya\n\n[Autodesk Maya](https://www.autodesk.com/products/maya/overview) is supported out of the box and doesn't require any special setup. Even though everything should be ready to go from the start, here is the checklist to get pype running in Maya\n\n1. Correct executable in launchers as explained in [here](admin_config#launchers)\n2. Pype environment variable added to `PYTHONPATH` key in `maya.json` environment preset.\n```json\n{\n  \"PYTHONPATH\": [\n  \"{PYPE_ROOT}/repos/avalon-core/setup/maya\",\n  \"{PYPE_ROOT}/repos/maya-look-assigner\"\n  ]\n}\n```\n\n\n## Foundry Nuke\n\n[Foundry Nuke](https://www.foundry.com/products/nuke) is supported out of the box and doesn't require any special setup. Even though everything should be ready to go from the start, here is the checklist to get pype running in Nuke\n\n1. Correct executable in launchers as explained in [here](admin_config#launchers)\n2. Following environment variables in `nuke.json` environment file. (PYTHONPATH might need to be changed in different studio setups)\n\n```json\n{\n  \"NUKE_PATH\": [\n      \"{PYPE_ROOT}/repos/avalon-core/setup/nuke/nuke_path\",\n      \"{PYPE_MODULE_ROOT}/setup/nuke/nuke_path\",\n      \"{PYPE_STUDIO_PLUGINS}/nuke\"\n  ],\n  \"PYPE_LOG_NO_COLORS\": \"True\",\n  \"PYTHONPATH\": {\n      \"windows\": \"{VIRTUAL_ENV}/Lib/site-packages\",\n      \"linux\": \"{VIRTUAL_ENV}/lib/python3.6/site-packages\"\n  }\n}\n```\n\n\n\n## AWS Thinkbox Deadline\n\nTo support [AWS Thinkbox Deadline](https://www.awsthinkbox.com/deadline) you just need to:\n\n1. enable it in **init_env** key of your `deploy.json` file:\n\n```json\n{\n  \"PYPE_CONFIG\": \"{PYPE_ROOT}/repos/pype-config\",\n  \"init_env\": [\"global\", \"avalon\", \"ftrack\", \"deadline\"]\n}\n```\n\n2. Edit `repos/pype-config/environments/deadline.json` and change `DEADLINE_REST_URL` to point to your Deadline Web API service.\n\n3. Set up *Deadline Web API service*. For more details on how to do it, see [here](https://docs.thinkboxsoftware.com/products/deadline/10.0/1_User%20Manual/manual/web-service.html).\n\n### Pype Dealine supplement code\n\nThere is some code needed to be installed on Deadline repository. You can find this repository overlay in\n`pype-setup/vendor/deadline`. This whole directory can be copied to your existing deadline repository.\n\nCurrently there is just **GlobalJobPreLoad.py** script taking care of path remapping in case of multiplatform\nmachine setup on farm. If there is no mix of windows/linux machines on farm, there is no need to use this.\n\n## Virtual Vertex Muster\n\n:::warning\nSupport for Muster was removed from OpenPype and AYON.\n:::\n\nPype supports rendering with [Muster](https://www.vvertex.com/). To enable it:\n1. Add `muster` to **init_env** to your `deploy.json`\n file:\n\n```json\n{\n  \"PYPE_CONFIG\": \"{PYPE_ROOT}/repos/pype-config\",\n  \"init_env\": [\"global\", \"avalon\", \"ftrack\", \"muster\"]\n}\n```\n\n2. Configure URL to Muster Web API - in `repos/pype-config/environments/muster.json`. There you need to set `MUSTER_REST_URL` to correct value.\n\n3. Enabled muster in [tray presets](admin_presets_tools##item_usage-dict)\n\n#### Template mapping\n\nFor setting up muster templates have a look at [Muster Template preset](admin_presets_tools#muster-templates)\n\n:::note\nUser will be asked for it's Muster login credentials during Pype startup or any time later if its authentication token expires.\n:::\n\n\n## Clockify\n\n[Clockify](https://clockify.me/) integration allows pype users to seamlessly log their time into clockify in the background. This in turn allow project managers to have better overview of all logged times with clockify dashboards and analytics.\n\n1. Enable clockify, add `clockify` to **init_env** in your `deploy.json`\n file:\n\n```json\n{\n  \"PYPE_CONFIG\": \"{PYPE_ROOT}/repos/pype-config\",\n  \"init_env\": [\"global\", \"avalon\", \"ftrack\", \"clockify\"]\n}\n```\n\n2. Configure your clockify workspace. In `repos/pype-config/environments/clockify.json`, you need to change `CLOCKIFY_WORKSPACE` to the correct value\n\n```json\n{\n    \"CLOCKIFY_WORKSPACE\": \"test_workspace\"\n}\n```\n\n3. Enabled Clockify in [tray presets](admin_presets_tools##item_usage-dict)\n\n\n:::note\nUser will be asked for it's Clockify login credentials during Pype startup.\n:::\n\n\n## Unreal Editor\n\nPype supports [Unreal](https://www.unrealengine.com/). This support is currently tested only on Windows platform.\nYou can control Unreal behavior by editing `repos/pype-config/presets/unreal/project_setup.json`:\n\n```json\n{\n  \"dev_mode\": false,\n  \"install_unreal_python_engine\": false\n}\n```\n\nSetting `dev_mode` to **true** will make all new projects created on tasks by pype C++ projects. To work with those,\nyou need [Visual Studio](https://visualstudio.microsoft.com/) installed.\n\n`install_unreal_python_engine` will install [20tab/UnrealEnginePython](https://github.com/20tab/UnrealEnginePython) as plugin\nin new project. This implies `dev_mode`. Note that **UnrealEnginePython** is compatible only with specific versions of Unreal Engine (usually not with the latest one). This plugin is not needed but can be used along *\"standard\"* python support in Unreal Engine to\nextend Pype or Avalon functionality.\n\n### Unreal Engine version detection\n\nPype is trying to automatically find installed Unreal Engine versions. This relies on [Epic Games Launcher](https://www.epicgames.com/store/en-US/).\nIf you have custom install location (for example you've built your own version from sources), you can set\n`UNREAL_ENGINE_LOCATION` to point there. Pype then tries to find UE version in `UE_x.xx` subfolders.\n\n### Avalon Unreal Integration plugin\n\nAvalon/Pype integration needs [Avalon Unreal Integration Plugin](https://github.com/pypeclub/avalon-unreal-integration). Use `AVALON_UNREAL_PLUGIN` environment variable to point to it. When new\nUE project is created, file are copied from this directory to project `Plugins`. If Pype detects that plugin\nisn't already built, it will copy its source codes to new project and force `dev_mode`. In that case, you need\n**Visual Studio** to compile the plugin along with the project code.\n\n### Dependencies\n\nPype integration needs:\n\n* *Python Script Plugin enabled* (done automatically)\n* *Editor Scripting Utilities* (done automatically)\n* *PySide* installed in Unreal Python 2 (or PySide2/PyQt5 if you've build Unreal Editor with Python 3 support) (done automatically)\n* *Avalon Unreal Integration plugin* ([sources are on GitHub](https://github.com/pypeclub/avalon-unreal-integration))\n* *Visual Studio 2017* is needed to build *Avalon Unreal Integration Plugin* and/or if you need to work in `dev_mode`\n\n### Environment Variables\n\n- `AVALON_UNREAL_EDITOR` points to Avalon Unreal Integration Plugin sources/build\n- `UNREAL_ENGINE_LOCATION` to override Pype autodetection and point to custom Unreal intallation\n- `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` path to [20tab/UnrealEnginePython](https://github.com/20tab/UnrealEnginePython) optional plugin\n"
  },
  {
    "path": "website/docs/pype2/admin_install.md",
    "content": "---\nid: admin_install\ntitle: Pype Setup\nsidebar_label: Pype Setup\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## Introduction\n\nThe general approach to pype deployment is installing central repositories on a shared network storage which can be accessed by all artists in the studio. Simple shortcuts to launchers are then distributed to all workstations for artists to use. This approach ensures easy maintenance and updates.\n\nWhen artist first runs pype all the required python packages get installed automatically to his local workstation and updated every time there is a change in the central installation.\n\n:::note\nAutomatic workstation installation and updates will not work in offline scenarios. In these case `pype install --force --offline` command must be triggered explicitly on the workstation\n:::\n\n## Requirements\n\n### Python 3.6+\n\nPype requires Python 3.6 or later to be installed on each workstation running Pype.\n\n:::note\nIf you want to use pype with Blender, you need to upgrade your python to 3.7 or higher.\n:::\n\nWindows version of Python can be easily grabbed at [python.org](https://www.python.org/downloads/). Install location doesn't matter but\npython executable should be in `PATH` environment variable.\n\n:::important Linux\nOn linux it is somehow different and all depends on linux distribution in use.\n\nSome linux variants (for example *Ubuntu*) need **python-dev** variant of python package that includes python headers and developer tools. This is needed because some of **Pype** requirements need to compile themselves against python during their installation. Please, refer to your distribution community to find out how to install that package.\n:::\n\n\n<Tabs\n  groupId=\"platforms\"\n  defaultValue=\"win\"\n  values={[\n    {label: 'Windows', value: 'win'},\n    {label: 'Linux', value: 'linux'},\n    {label: 'Mac', value: 'mac'},\n  ]}>\n\n<TabItem value=\"win\">\n\n```sh\nsudo yum group install \"Development Tools\"\n```\n\nPython 3.6 is not part of official distribution. Easiest way is to add it with the help of *SCL* - Software Collection project.\nThis has advantage that it won't replace system version of python.\n\n```sh\nsudo yum update\nsudo yum install centos-release-scl\n```\nNow you can install python itself:\n```sh\nsudo yum install rh-python36\n```\n\nTo be able to use installed version of python, you must enable it in shell:\n```sh\nscl enable rh-python36 bash\n```\n\nThis will enable python 3.6 in currently running bash only!\n\nCheck it with:\n```sh\npython --version\n```\n\n</TabItem>\n<TabItem value=\"linux\">\n\n```sh\nsudo apt install build-essential\n```\n\nSome versions of Ubuntu already has python 3.6 installed, check it with:\n```sh\npython3 --version\n```\nIf python shows lower version then required, use:\n```\nsudo apt-get update\nsudo apt-get install python3-dev\n```\nPlease be aware that even if your system already has python 3.6, than if that\ndidn't come from **python3-dev** package, Pype will most likely fail to install\nit's dependencies.\n\n</TabItem>\n</Tabs>\n\n\n:::note Override Python detection\nYou can override autodetection of Python. This can be useful if you want to use central network python location or some other custom location. Just set `PYPE_PYTHON_EXE` environment variable to point where you need.\n:::\n\n--------------\n\n### MongoDB\n\nPype needs site-wide installation of **MongoDB**. It should be installed on\nreliable server all workstations (and possibly render nodes) can connect. This\nserver holds **Avalon** database that is at the core of everything, containing\nvery important data, so it should be backed up often and if high-availability is\nneeded, *replication* feature of **MongoDB** should be considered. This is beyond the\nscope of this documentation, please refer to [MongoDB Documentation](https://docs.mongodb.com/manual/replication/).\n\nPype can run it's own instance of **mongodb**, mostly for testing and development purposes.\nFor that it uses locally installed **MongoDB**.\n\nDownload it from [mognoDB website](https://www.mongodb.com/download-center/community), install it and\nadd to the `PATH`. On Windows, Pype tries to find it in standard installation destination or using `PATH`.\n\nTo run **mongoDB** on server, use your server distribution tools to set it up (on Linux).\n\n### Git\n\nTo be able to deploy Pype, **git** is need. It will clone all required repositories and\ncontrol versions so future updates are easier. Git is however only requirent on admin workstation for global studio updates.\n\nSee [how to install git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).\n\nTo access private repositories, you'll need other optional stuff like ssh key agents, etc.\n\n### PowerShell (on Windows only)\n\nPowerShell is now included in recent versions of Windows. **Pype** requires at least\nversion 5.0, included in Windows 10 from beginning and available for Windows 7 SP1,\nWindows 8.1 and Windows Server 2012.\n\nIf you want to know what version of PowerShell are you running, execute in PowerShell prompt:\n```powershell\n$PSVersionTable\n```\n If you need to install PowerShell or update it, please refer to:\n [Installing powershell on windows](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-6)\n\n### Xcode CLT (on Mac only)\n\nPype need **Xcode Command Line Tools** installed to provide its tools and be able to install its dependencies via pythons `pip` command. Those will be downloaded\nand installed automatically if needed.\n\n### Other\n:::warning Linux headless server\nIf you need to run Pype's **ftrack event server** on headless linux server, be aware that due Qt dependencies, you'll need to install OpenGL support there even if server doesn't have any real use for it.\n:::\n\n## Studio Setup\n\n### Pype location\n\nBefore you install Pype, first clone **pype-setup** repository to place you want Pype to be. In studio setting\nyou probably want that destination to be on shared network drive so all users can access it.\n\n:::tip production and development branch\nWe recommend to maintain two *versions* of Pype. The first is **production** branch - the one your artists use everyday for their work. The second one is **development** version you should use for testing new features and as development sandbox. Development branch can point to a different Avalon database and use its own **ftrack event server**. More on that in [Pype Configuration](admin_config)\n\n```text\nShared Network Drive\n├─── pype\n      ├─── prod\n      └─── dev\n```\n\nTo prepare this structure, you can use:\n```sh\ncd /shared_drive/pype\ngit clone --tag 2.4.0 https://bitbucket.com/pypeclub/pype-setup.git prod\ngit clone --tag 2.4.0 https://bitbucket.com/pypeclub/pype-setup.git dev\n```\n:::\n\nSpecify your version after `--branch` or `--tag` option.\n\n:::note\nIt is possible to distinguish `dev` and `prod` by changing Pype icon color to orange. To do so, you have to create `config.ini` file with content:\n```text\n[DEFAULT]\ndev=true\n```\n\nAnd put the file to:\n```text\nShared Network Drive\n├─── dev\n      └─── pype-setup\n            └───pypeapp\n                    └───config.ini\n```\n:::\n\n:::note\nYou should always use tags to checkout to get specific release, otherwise you end up with *develop* branch that can be unstable.\n:::\n\n:::warning\nBy default, both branches will use the same virtual environment. Be careful when modifying your requirements in **dev** version because then it will influence **prod** version as well. To be safe, modify `PYPE_ENV` environment variable before using **dev** Pype commands.\n:::\n\n### Installation\n\n\n<Tabs\n  groupId=\"platforms\"\n  defaultValue=\"win\"\n  values={[\n    {label: 'Windows', value: 'win'},\n    {label: 'Linux', value: 'linux'},\n    {label: 'Mac', value: 'mac'},\n  ]}>\n\n<TabItem value=\"win\">\n\nTo install Pype you need to get first to it's root directory in powershell.\n\nIf you have Pype on network, you should mount it as drive and assign it some consistent letter. You can also mount this network drive via junction point feature. As admin run from shell:\n\n```sh\nmklink /d \"C:\\pipeline\" \"\\\\server\\pipeline\"\n```\nThen your network drive will be available transparently at `C:\\pipeline`.\n\n```sh\ncd Z:\\pype\\production\\pype-setup\n```\n-----\n\nNow you can run installation itself:\n```sh\n.\\pype.bat install\n```\n</TabItem>\n\n<TabItem value=\"linux\">\n\nTo install pype you first need get to it's root directory in bash shell.\nIf you Pype location is on network drive, you should add it to `/etc/fstab` to\nmount it automatically during system startup.\n```sh\ncd /location/of/pype\n```\n\n**Pype** can be installed with the following command:\n\n```sh\n. pype install\n```\n\nOn linux it is necessary to adjust user permissions to `/opt/pype/pype_env2` or whatever you set in `PYPE_ENV` and for that you need to be **root**.\n\n\n#### Mounting network drives\nIf you Pype location is on network drive, you need to mount it first. Here are the steps to do it and make it so it re-mounts automatically after your computer restarts:\n\n1) in Finder press **Command+K**\n2) enter path `smb://server/pipeline` and hit **Connect**\n3) enter login and password\n4) network drive is now mounted under `/Volumes/pipeline`\n5) now go to **System Preferences**\n6) click **Users & Groups -> Login Items**\n7) click + and select mounted drive and click **Add**\n\n</TabItem>\n\n<TabItem value=\"mac\">\n\nTo install Pype on Mac, you need to have Administrator privileges. There are also further requirements if you need to deploy repositories.\n\nRun **Terminal**. Run following commands:\n```sh\nsudo -s\ncd /Volumes/pipeline/pype/production/pype-setup\n./pype install\n```\n\n`sudo -s` will take you to elevated privileges and you need to enter your password. `cd` will change directory to where you have pype located and `pype install` will run installation.\n\nIf there are then warnings about some directory not owned by current user, you can fix it with following commands:\n\n```sh\nchown -R yourusername /usr/local/pype\nchown -R yourusername /Users/yourusername/Library/Caches/pip\n```\nSwitch `yourusername` for your user name :)\n\n</TabItem>\n\n</Tabs>\n\nWhat it basically does is:\n1) Create python virtual environment on path: `C:\\Users\\Public\\pype_env2` on Windows or `/opt/pype/pype_env2` on Linux or `/usr/local/pype/pype_env2` on Mac.\nThis can be overridden by setting `PYPE_ENV` to different path. Logic behind this is that this directory on Windows can be shared\nbetween users on one machine - it only stores Pype dependencies, not any personal setting or credentials.\n\n2) Then it will install all python dependencies defined in `pypeapp\\requirements.txt` into this virtual environment.\n\nDefault installation will use your internet connection to download all necessary requirements.\n\n#### Offline installation\n\nYou can also install Pype in offline scenarios:\n\n```sh\npype install --offline\n```\n\nThis will use dependencies downloaded into `pype-setup/vendor/packages` rather than pulling directly from the internet. Those packages must, however, first be\ndownloaded on a machine connected to the internet using:\n\n```sh\npype download\n```\n\n:::warning multiple platforms\n`pype download` will only download packages for currently running platform. So if you run it on Windows machine, only windows packages get downloaded (along with many universal ones). If you then run `pype install --offline` on Linux machine, it will probably fail as Linux specific packages will be missing. In multiplatform environments we recommend to run `pype download` on all used platform to combine all necessary packages into `vendor/packages`.\n:::\n\n:::caution multiplatform caveat\nThere can be problems with libraries compatibility, when using multiplatform environments. For example if using **PyQt 5.12**, there seems to be no problem on Windows, but using it on **Centos Linux 7** will cause problems because Centos ships with some older dependent libraries that will not work with aforementioned PyQt version.\n:::\n\n#### Forcing Installation\n\nSometime it is necessary to force re-install Pype environment. To do this:\n\n```sh\npype install --force\n```\n\nor\n\n```sh\npype install --force --offline\n```\nin offline scenarios.\n\nThis is useful if Pype is misbehaving as first line of debugging. You can of course just manually delete `PYPE_ENV` directory and run `pype install` again.\n\n### Deployment\n\nAfter Pype is cloned and installed, it is necessary to *deploy* all repositories used by Pype. This must be done on a computer with\nInternet access.\n\n```sh\npype deploy\n```\n\nThis command will process all repositories specified in `deploy/deploy.json` and clone them into `repos/` directory.\n\n```sh\npype deploy --force\n```\n\nwill deploy repositories, overwriting existing ones if they exists and setting them to state specified in *deploy.json*.\n\n:::note customizing deployment\nYou can customize your deployment to some extent. Everything specified in `deploy/deploy.json` is considered as default and can be overridden by creating your own *deploy.json* in sub directory.\n```text\npype\n ├─── pypeapp\n ├─── deploy\n │       ├─── deploy.json\n │       ├─── deploy_schema-1.0.json\n │       ├─── my_studio_override\n │       │                  ├─── deploy.json\n │       │                  └─── deploy_schema-1.0.json\n │      ...      \n...\n```\nIn such configuration, `deploy/my_studio_override/deploy.json` will take precedence over the default one.\n:::\n\nTo validate if Pype deployment is ok, run:\n\n```sh\npype validate\n```\n\n#### Structure of `deploy.json`\n\nThere are a few features in `deploy.json` that needs to be explained in further detail.\n\nHere is a list of keys used and their function:\n\n- `PYPE_CONFIG` - path to Pype configuration repository.\n- `init_env` - these are environment files in Pype configuration repository that\n  are loaded immediately after Pype starts. They define basic functionality.\n  ```js\n  \"init_env\": [\"global\", \"avalon\", \"ftrack\", \"deadline\"]\n  ```\n  For example, if you don't use *Deadline* but you need *Muster* support, change `deadline` to `muster`.\n  Pype will then load `{PYPE_CONFIG}/environments/muster.json` and set environment variables there.\n- `repositories`: this is list of repositories that will be deployed to `repos/`. There are few options\n  for each repository:\n\n  - `name`: name of repository will be used as directory name\n  - `url`: url of the git repository\n  - `branch` or `tag`: this specify either branch - it's *HEAD* will be checked out, or\n    `tag` - commit tagged with specified tag will be checked out.\n\n- `pip`: these are additional dependencies to be installed by *pip* to virtual environment.\n- `archive_files`: archive files to be unpacked to somewhere. For example ffmpeg installation or\n  anything else we need to extract during deployment to some place.\n\n    - `extract_path`: path to where this archive should be extracted\n    - `url` or `vendor`: this is url of source to be downloaded or name in `vendor/packages` to be Used\n    - `md5_url` optional url for md5 file to validate checksum of downloaded file\n    - `skip_first_subfolder` will move everything inside first directory in archive to `extract_path`.\n\n#### Offline Deployment\n\nIn offline scenarios it is up to you to replicate what `pype deploy` does. The easiest way\nto go is to run `pype deploy` on machine, get everything in `repos/` and move it to your studio install location:\n\n```sh\ncd pype-setup\ntar cvzf pype_repos.tgz repos/\n```\n\ndo the same for things deployed to *vendor*.\n"
  },
  {
    "path": "website/docs/pype2/admin_introduction.md",
    "content": "---\nid: admin_getting_started\ntitle: Getting Started\nsidebar_label: Getting Started\n---\n\n## Introduction\n\n**Pype** is part of a larger ecosystem of tools build around [avalon](https://github.com/getavalon/core) and [pyblish](https://github.com/pyblish/pyblish-base).\nTo be able to use it, you need those tools and set your environment. This\nrequires additional software installed and set up correctly on your system.\n\nFortunately this daunting task is handled for you by **Pype Setup** package itself. **Pype** can\ninstall most of its requirements automatically but a few more things are needed in\nvarious usage scenarios.\n\n## Software requirements\n\n- **Python 3.7+** (Locally on all workstations)\n- **PowerShell 5.0+** (Windows only)\n- **Bash** (Linux only)\n- **MongoDB** (Centrally accessible)\n\nThere are other requirements for different advanced scenarios. For more\ncomplete guide please refer to [Pype Setup page](admin_install).\n\n\n## Hardware requirements\n\nPype should be installed centrally on a fast network storage with at least read access right for all workstations and users in the Studio. Full Deplyoyment with all dependencies and both Development and Production branches installed takes about 1GB of data, however to ensure smooth updates and general working comfort, we recommend allocating at least at least 4GB of storage dedicated to PYPE deployment.\n\nFor well functioning ftrack event server, we recommend a linux virtual server with Ubuntu or Centos OS. CPU and RAM allocation need differ based on the studio size, but a 2GB of ram, with a dual core CPU and around 4GB of storage should suffice\n\n## Central repositories\n\n### Pype-setup\n\nPype-Setup is the glue that binds Avalon, Pype and the Studio together. It is essentially a wrapper application that manages requirements, installation, all the environments and runs all of our standalone tools.\n\nIt has two main interfaces. `Pype` CLI command for all admin level tasks and a `Pype Tray` application for artists. Documentation for the `Pype` command can be found [here](admin_pype_commands)\n\nThis is also the only repository that needs to be downloaded by hand before full pype deployment can take place.\n\n### Pype\n\nPype is our \"Avalon Config\" in Avalon terms that takes avalon-core and expands on it's default features and capabilities. This is where vast majority of the code that works with your data lives.\n\nAvalon gives us the ability to work with a certain host, say Maya, in a standardised manner, but Pype defines **how** we work with all the data. You can think of it as. Avalon by default expects each studio to have their own avalon config, which is reasonable considering all studios have slightly different requirements and workflows. We abstracted a lot of this customisability out of the avalon config by allowing pype behaviour to be altered by a set of .json based configuration files and presets.\n\nThanks to that, we are able to maintain one codebase for vast majority of the features across all our clients deployments while keeping the option to tailor the pipeline to each individual studio.\n\n### Avalon-core\n\nAvalon-core is the heart and soul of Pype. It provides the base functionality including GUIs (albeit expanded modified by us), database connection and maintenance, standards for data structures and working with entities and a lot of universal tools.\n\nAvalon is being very actively developed and maintained by a community of studios and TDs from around the world, with Pype Club team being an active contributor as well.\n\n## Studio Specific Repositories\n\n### Pype-Config\n\nPype_config repository need to be prepared and maintained for each studio using pype and holds all of their specific requiremens for pype. Those range from naming conventions and folder structures (in pype referred to as `project anatomy`), through colour management, data preferences, all the way to what individual  validators they want to use and what they are validating against.\n\nThanks to a very flexible and extensible system of presets, we're almost always able to accommodate client requests for modified behaviour by introducing new presets, rather than permanently altering the main codebase for everyone.\n\n\n### Studio-Project-Configs\n\nOn top of studio wide pype config, we support project level overrides for any and all variables and presets available in the main studio config.\n\n### Studio-Project-Scrips\n"
  },
  {
    "path": "website/docs/pype2/admin_presets_ftrack.md",
    "content": "---\nid: admin_presets_ftrack\ntitle: Presets > Ftrack\nsidebar_label: Ftrack\n---\n\n## PROJECT_DEFAULTS.json\n\npath: `pype-config/presets/ftrack/project_defauls.json`\n\nA list of all project defaults to be set when you run \"Ftrack Prepare Project\"\n\n```json\n{\n    \"fps\": 25,\n    \"frameStart\": 1001,\n    \"frameEnd\": 1100,\n    \"clipIn\": 1001,\n    \"clipOut\": 1100,\n    \"handleStart\": 10,\n    \"handleEnd\": 10,\n\n    \"resolutionHeight\": 1080,\n    \"resolutionWidth\": 1920,\n    \"pixelAspect\": 1.0,\n    \"applications\": [\n        \"maya_2019\", \"nuke_11.3\", \"nukex_11.3\", \"nukestudio_11.3\", \"deadline\"\n    ],\n    \"tools_env\": [],\n    \"avalon_auto_sync\": true\n}\n```\n\n## FTRACK_CONFIG.json\n\npath: `pype-config/presets/ftrack/ftrack_config.json`\n\n### `sync_to_avalon` [dict]\n\nlist of statuses that allow moving, deleting and changing of names on ftrack entities. Once any child of and entity is set to a status different than those listed in this list, it is considered to have been worked on and will not allow any major changes to hierarchy any more.\n\n`statuses_name_change [list]`:\n\n```json\n{\n    \"sync_to_avalon\": {\n        \"statuses_name_change\": [\"not ready\", \"ready\"]\n    }\n}\n```\n\n### `status_update` [dict]\n\nmapping of status for automatic updates.\nKey specifies the resulting status and value is a list of statuses from which we allow changing to the target status.\n\n`_ignore_` [list]: source statuses to ignore\n\n`target_status` [list]: target  \n\n```json\n{\n    \"status_update\": {\n        \"_ignore_\": [\"in progress\", \"omitted\", \"on hold\"],\n        \"Ready\": [\"not ready\"],\n        \"In Progress\" : [\"_any_\"]\n    }\n}\n```\n\n### `status_version_to_task` [dict]\n\nmapping of status that propagate automatically from published version to it's task. By default we search for identical status, however this preset let's you remap between different statuses on versions and tasks.\n\n\n`status_version_to_task` [dict]:\n\n```json\n{\n    \"status_version_to_task\": {\n        \"__description__\": \"Status `from` (key) must be lowered!\",\n        \"in progress\": \"in progress\",\n        \"approved\": \"approved\"\n    }\n}\n```\n\n## SERVER.json\n\npath: `pype-config/presets/ftrack/server.json`\n\n### `first_version_status` [dict]\n\n`task_status_map` [list]: List of dictionaires specifying individual mappings\n\n`status` [string]: status to set if `key` and `name` match.\n\n`name` [string]: name of task or task's type.\n\n`key` [enumerator]: _optional_ specify where to look for name. There are two possible value:\n  1. `task`: task's name (default)\n  2. `task_type`: task type's name\n\nIt doesn't matter if values are lowered or capitalized.\n\n```json\n{\n    \"FirstVersionStatus\": {\n        \"task_status_map\": [{\n            \"key\": \"task\",\n            \"name\": \"compositing\",\n            \"status\": \"Blocking\"\n        }, {\n            \"MORE ITEMS...\": \"MORE VALUES...\"\n        }]\n    },\n    \"...\": \"{...}\"\n}\n```\n"
  },
  {
    "path": "website/docs/pype2/admin_presets_maya.md",
    "content": "---\nid: admin_presets_maya\ntitle: Presets > Maya\nsidebar_label: Maya\n---\n\n## CAPTURE.json\n\npath: `pype-config/presets/maya/capture.json`\n\nAll the viewport settings for maya playblasts.\n\n### `Codec` [dict] ###\n\n```python\n  \"Codec\": {\n      \"compression\": \"jpg\",\n      \"format\": \"image\",\n      \"quality\": 95\n  }\n```\n\n\n### `Display Options` [dict] ###\n\n```python\n\"Display Options\": {\n    \"background\": [\n        0.7137254901960784,\n        0.7137254901960784,\n        0.7137254901960784\n    ],\n    \"backgroundBottom\": [\n        0.7137254901960784,\n        0.7137254901960784,\n        0.7137254901960784\n    ],\n    \"backgroundTop\": [\n      0.7137254901960784,\n      0.7137254901960784,\n      0.7137254901960784\n    ],\n    \"override_display\": true\n  }\n```\n\n### `Generic` [dict] ###\n```python\n\"Generic\": {\n    \"isolate_view\": true,\n    \"off_screen\": true\n},\n```\n\n### `IO` [dict] ###\n\n```python\n\"IO\": {\n    \"name\": \"\",\n    \"open_finished\": false,\n    \"raw_frame_numbers\": false,\n    \"recent_playblasts\": [],\n    \"save_file\": false\n},\n```\n\n### `PanZoom` [dict] ###\n\n```python\n\"PanZoom\": {\n    \"pan_zoom\": true\n},\n```\n\n### `Viewport Options` [dict] ###\n\n```python\n\"Viewport Options\": {\n    \"cameras\": false,\n    \"clipGhosts\": false,\n    \"controlVertices\": false,\n    \"deformers\": false,\n    \"dimensions\": false,\n    \"displayLights\": 0,\n    \"dynamicConstraints\": false,\n    \"dynamics\": false,\n    \"fluids\": false,\n    \"follicles\": false,\n    \"gpuCacheDisplayFilter\": false,\n    \"greasePencils\": false,\n    \"grid\": false,\n    \"hairSystems\": false,\n    \"handles\": false,\n    \"high_quality\": true,\n    \"hud\": false,\n    \"hulls\": false,\n    \"ikHandles\": false,\n    \"imagePlane\": false,\n    \"joints\": false,\n    \"lights\": false,\n    \"locators\": false,\n    \"manipulators\": false,\n    \"motionTrails\": false,\n    \"nCloths\": false,\n    \"nParticles\": false,\n    \"nRigids\": false,\n    \"nurbsCurves\": false,\n    \"nurbsSurfaces\": false,\n    \"override_viewport_options\": true,\n    \"particleInstancers\": false,\n    \"pivots\": false,\n    \"planes\": false,\n    \"pluginShapes\": false,\n    \"polymeshes\": true,\n    \"shadows\": false,\n    \"strokes\": false,\n    \"subdivSurfaces\": false,\n    \"textures\": false,\n    \"twoSidedLighting\": true\n}\n```\n\n## Maya instance scene types\n\nIt is possible to set when to use `.ma` or `.mb` for:\n\n- camera\n- setdress\n- layout\n- model\n- rig\n- yetiRig\n\nJust put `ext_mapping.json` into `presets/maya`. Inside is simple mapping:\n\n```JSON\n{\n  \"rig\": \"mb\",\n  \"camera\": \"mb\"\n}\n```\n\n*Note that default type is `ma`*\n"
  },
  {
    "path": "website/docs/pype2/admin_presets_nukestudio.md",
    "content": "---\nid: admin_presets_nukestudio\ntitle: Presets > NukeStudio\nsidebar_label: Nukestudio\n---\n\n## TAGS.json\n\npath: `pype-config/presets/nukestudio/tags.json`\n\nEach tag defines defaults in `.json` file. Inside of the file you can change the default values as shown in the example (`>>>\"1001\"<<<`). Please be careful not to alter the `family` value.\n\n```python\n\"Frame start\": {\n    \"editable\": \"1\",\n    \"note\": \"Starting frame for comps\",\n    \"icon\": {\n        \"path\": \"icons:TagBackground.png\"\n    },\n    \"metadata\": {\n        \"family\": \"frameStart\",\n        \"number\": >>>\"1001\"<<<\n    }\n}\n```\n\n## PUBLISH.json\n\npath: `pype-config/presets/plugins/nukestudio/publish.json`\n\n### `CollectInstanceVersion` [dict] ###\n\n\nThis plugin is set to `true` by default so it will synchronize version of published instances with the version of the workfile. Set `enabled` to `false` if you wish to let publishing process decide on the next available version.\n\n```python\n{\n    \"CollectInstanceVersion\": {\n        \"enabled\": false\n    }\n}\n```\n\n### `ExtractReviewCutUpVideo` [dict] ###\n\npath: `pype-config/presets/plugins/nukestudio/publish.json`\n\nPlugin is responsible for cuting shorter or longer source material for review. Here you can add any additional tags you wish to be added into extract review process.\n\nThe plugin generates reedited intermediate video with handless even if it has to add empty black frames. Some productions prefer to use review material without handless so in the example, `no-handles` are added as tags. This allow further review extractor to publish review without handles, without affecting other outputs.\n\n```python\n{\n    \"ExtractReviewCutUpVideo\": {\n        \"tags_addition\": [\"no-handles\"]\n      }\n}\n```\n"
  },
  {
    "path": "website/docs/pype2/admin_presets_plugins.md",
    "content": "---\nid: admin_presets_plugins\ntitle: Presets > Plugins\nsidebar_label: Plugins\n---\n\n## Global\n\n### publish.json\n\nEach plugin in the json should be added as name of the class. There are some default attributes recommended to use in case you wish a plugin to be switched off for some projects in `project overwrites` like `enabled: false`. So for example if you wish to switch off plugin class name `PluginName(pyblish.api.contextPlugin)` if file `name_of_plugin_file.py`, it could be done only by adding following text into root level of publish.json file:\n\n```json\n{\n    \"PluginName\": {\n        \"enabled\": false\n    }\n}\n```\n\n\n### `ExtractReview`\n\nPlugin responsible for automatic FFmpeg conversion to variety of formats.\n\nSupported extensions for both input and output: `[\"exr\", \"jpg\", \"jpeg\", \"png\", \"dpx\", \"mov\", \"mp4\"]`\n\n**ExtractReview** creates new representations based on presets and representations in instance. Preset should contain only one attribute **\"profiles\"** which is list of profile items. Each profile item has **outputs**, where definitions of possible outputs are, and may have specified filters for **hosts**, **tasks** and **families**.\n\n#### Profile filters\nAs mentioned above you can define multiple profiles for different contexts. Profile with filters matching current context the most is used. You can define profile without filters and use it as **default**. Only **one or none** profile is processed per instance.\n\nAll context filters are lists which may contain strings or Regular expressions (RegEx).\n- **hosts** - Host from which publishing was triggered. `[\"maya\", \"nuke\"]`\n- **tasks** - Currently processed task. `[\"[Cc]ompositing\", \"[Aa]nimation\"]`\n- **families** - Main family of processed instance. `[\"plate\", \"model\"]`\n\n:::important Filtering\nFilters are optional and may not be set. In case when multiple profiles match current context, profile with filters has higher priority than profile without filters.\n:::\n\n#### Profile outputs\nProfile may have multiple outputs from one input and that's why **outputs** is dictionary where key represents **filename suffix** to avoid overriding files with same name and value represents definition itself. Definition may contain multiple optional keys.\n\n| Key | Description | Type | Example |\n| --- | --- | --- | --- |\n| **width** | Width of output. | int | 1920 |\n| **height** | Height of output. | int | 1080 |\n| **letter_box** | Set letterbox ratio. | float | 2.35 |\n| **ext** | Extension of output file(s). | str | \"mov\" |\n| **tags** | Tags added to new representation. | list | [here](#new-representation-tags-tags) |\n| **ffmpeg_args** | Additional FFmpeg arguments. | dict | [here](#ffmpeg-arguments-ffmpeg_args) |\n| **filter** | Filters definition. | dict | [here](#output-filters-filter) |\n\n:::note\nAs mentioned above **all keys are optional**. If they are not filled at all, then **\"ext\"** is filled with input's file extension and resolution keys **\"width\"** and **\"height\"** are filled from instance data, or from input resolution if instance doesn't have set them.\n:::\n\n:::important resolution\nIt is not possible to enter only **\"width\"** or only **\"height\"**. In that case set values will be skipped.\n:::\n\n#### New representation tags (`tags`)\nYou can add tags to representation created during extracting process. This might help to define what should happen with representation in upcoming plugins.\n\n| Tag | Description |\n| --- | --- |\n| **burnin** | Add burnins with predefined values into the output. |\n| **preview** | Will be used as preview in Ftrack. |\n| **reformat** | Rescale to format based on width and height keys. |\n| **bake-lut** | Bake LUT into the output (if is available path in data). |\n| **slate-frame** | Add slate frame at the beginning of video. |\n| **no-handles** | Remove the shot handles from the output. |\n| **sequence** | Generate a sequence of images instead of single frame.<br />Is applied only if **\"ext\"** of output is image extension e.g.: png or jpg/jpeg. |\n\n:::important Example\nTags key must contain list of strings.\n```json\n{\n    \"tags\": [\"burnin\", \"preview\"]\n    ...\n}\n```\n:::\n\n#### FFmpeg arguments (`ffmpeg_args`)\nIt is possible to set additional FFmpeg arguments. Arguments are split into 4 categories **\"input\"**, **\"video_filters\"**, **\"audio_filters\"** and **\"output\"**.\n\n| Key | Description | Type | Example |\n| --- | --- | --- | --- |\n| **input** | FFmpeg arguments added before video/image input. | list | [\"-gamma 2.2\"] |\n| **video_filters** | All values which should be in `-vf` or `-filter:v` argument. | list | [\"scale=iw/2:-1\"] |\n| **audio_filters** | All values which should be in `-af` or `-filter:a` argument. | list | [\"loudnorm\"] |\n| **output** | FFmpeg arguments added before output filepath. | list | [\"-pix_fmt yuv420p\", \"-crf 18\"] |\n\n:::important Example\nFor more information about FFmpeg arguments please visit [official documentation](https://ffmpeg.org/documentation.html).\n```json\n{\n    \"ffmpeg_args\": {\n        \"input\": [\"-gamma 2.2\"],\n        \"video_filters\": [\"yadif=0:0:0\", \"scale=iw/2:-1\"],\n        \"output\": [\"-pix_fmt yuv420p\", \"-crf 18\"]\n    }\n    ...\n}\n```\n:::\n\n#### Output filters (`filter`)\nEven if profile has filtering options it is possible that output definitions require to be filtered by all instance **families** or representation's **tags**.\n\nFamilies filters in output's `filter` will check all instance's families and may check for single family or combination of families.\n\n| Key | Description | Type | Example |\n| --- | --- | --- | --- |\n| **families** | At least one family item must match instance's families to process definition. | list | [\"review\"] |\n| **tags** | At least one tag from list must be in representation's tags to process definition. | list | [\"preview\"] |\n\n:::important Example\nThese filters helps with explicit processing but do **NOT** use them if it's not necessary.\n```json\n{\n    \"filter\": {\n        \"families\": [\n            \"review\",\n            [\"ftrack\", \"render2d\"]\n        ],\n        \"tags\": [\"preview\"],\n    }\n    ...\n}\n```\nIn this example representation's tags must contain **\"preview\"** tag and instance's families must contain **\"review\"** family, or both **\"ftrack\"** and **\"render2d\"** families.\n:::\n\n#### Simple example\nThis example just create **mov** output with filename suffix **\"simplemov\"** for all representations with supported extensions.\n```json\n{\n    \"ExtractReview\": {\n        \"profiles\": [{\n            \"outputs\": {\n                /* Filename suffix \"simplemov\"*/\n                \"simplemov\": {\n                    /* Output extension will be \"mov\"*/\n                    \"ext\": \"mov\"\n                }\n            }\n        }]\n    }\n}\n```\n\n#### More complex example\n:::note\nThis is just usage example, without relevant data. Do **NOT** use these presets as default in production.\n:::\n\n```json\n{\n    \"ExtractReview\": {\n        \"profiles\": [\n            {\n                /* 1. profile - Without filters will be used as default. */\n                \"outputs\": {\n                    /* Extract single mov Prores 422 with burnins, slate and baked lut. */\n                    \"prores\": {\n                        \"ext\": \"mov\",\n                        \"codec\": [\n                            \"-codec:v prores_ks\",\n                            \"-profile:v 3\"\n                        ],\n                        \"tags\": [\"burnin\", \"reformat\", \"bake-lut\", \"slate-frame\"]\n                    }\n                }\n            }, {\n                /* 2. profile - Only for Nuke, \"compositing\" task and instance family \"render2d\". */\n                \"hosts\": [\"nuke\"],\n                \"tasks\": [\"compositing\"],\n                \"families\": [\"render2d\"],\n                \"outputs\": {\n                    /* Extract preview mov with burnins and without handles.*/\n                    \"h264\": {\n                        \"ext\": \"mov\",\n                        \"ffmpeg_args\": {\n                            \"output\": [\n                                \"-pix_fmt yuv420p\",\n                            ]\n                        },\n                        \"tags\": [\"burnin\", \"preview\", \"no-handles\"]\n                    },\n                    /* Also extract mxf with slate */\n                    \"edit\": {\n                        \"ext\": \"mxf\",\n                        \"ffmpeg_args\": {\n                            \"output\": [\n                                \"-codec:v dnxhd\",\n                                \"-profile:v dnxhr_444\",\n                                \"-pix_fmt yuv444p10le\",\n                                \"-b:v 185M\",\n                                \"-ar 48000\",\n                                \"-qmax 51\"\n                            ]\n                        },\n                        \"tags\": [\"slate-frame\"]\n                    }\n                }\n            }, {\n                /* 3. profile - Default profile for Nuke and Maya. */\n                \"hosts\": [\"maya\", \"nuke\"],\n                \"outputs\": {\n                    /* Extract preview mov with burnins and with forced resolution. */\n                    \"h264\": {\n                        \"width\": 1920,\n                        \"height\": 1080,\n                        \"ext\": \"mov\",\n                        \"ffmpeg_args\": {\n                            \"input\": [\n                                \"-gamma 2.2\"\n                            ],\n                            \"output\": [\n                                \"-pix_fmt yuv420p\",\n                                \"-crf 18\",\n                                \"-intra\"\n                            ]\n                        },\n                        \"tags\": [\"burnin\", \"preview\"]\n                    }\n                }\n            }\n        ]\n    }\n}\n```\n\n\n### `ExtractBurnin`\n\nPlugin is responsible for adding burnins into review representations.\n\nBurnins are text values painted on top of input and may be surrounded with box in 6 available positions `Top Left`, `Top Center`, `Top Right`, `Bottom Left`, `Bottom Center`, `Bottom Right`.\n\n![presets_plugins_extract_burnin](../assets/presets_plugins_extract_burnin_01.png)\n\nExtractBurnin creates new representations based on plugin presets and representations in instance. Presets may contain 3 keys **options**, **profiles** and **fields**.\n\n#### Burnin settings (`options`)\nOptions is dictionary where you can set the global appearance of burnins. It is possible to not fill options at all, in that case default values are used.\n\n| Key | Description | Type | Example | Default |\n| --- | --- | --- | --- | --- |\n| **font_size** | Size of text. | float | 24 | 42 |\n| **font_color** | Color of text. | str | [FFmpeg color documentation](https://ffmpeg.org/ffmpeg-utils.html#color-syntax) | \"white\" |\n| **opacity** | Opacity of text. | float | 0.7 | 1 |\n| **x_offset** | Horizontal margin around text and box. | int | 0 | 5 |\n| **y_offset** | Vertical margin around text and box. | int | 0 | 5 |\n| **bg_padding** | Padding for box around text. | int | 0 | 5 |\n| **bg_color** | Color of box around text. | str | [FFmpeg color documentation](https://ffmpeg.org/ffmpeg-utils.html#color-syntax) | \"black\" |\n| **bg_opacity** | Opacity of box around text. | float | 1 | 0.5 |\n\n#### Burnin profiles (`profiles`)\nPlugin process is skipped if `profiles` are not set at all. Profiles contain list of profile items. Each profile item has **burnins**, where definitions of possible burnins are, and may have specified filters for **hosts**, **tasks** and **families**. Filters work the same way as described in [ExtractReview](#profile-filters).\n\n#### Profile burnins\nProfile may have set multiple burnin outputs from one input and that's why **burnins** is dictionary where key represents **filename suffix** to avoid overriding files with same name and value represents burnin definition. Burnin definition may contain multiple optional keys.\n\n| Key | Description | Type | Example |\n| --- | --- | --- | --- |\n| **top_left** | Top left corner content. | str | \"{dd}.{mm}.{yyyy}\" |\n| **top_centered** | Top center content. | str | \"v{version:0>3}\" |\n| **top_right** | Top right corner content. | str | \"Static text\" |\n| **bottom_left** | Bottom left corner content. | str | \"{asset}\" |\n| **bottom_centered** | Bottom center content. | str | \"{username}\" |\n| **bottom_right** | Bottom right corner content. | str | \"{frame_start}-{current_frame}-{frame_end}\" |\n| **options** | Options overrides for this burnin definition. | dict | [Options](#burnin-settings-options) |\n| **filter** | Filters definition. | dict | [ExtractReview output filter](#output-filters-filter) |\n\n:::important Position keys\nAny position key `top_left` -> `bottom_right` is skipped if is not set, contain empty string or is set to `null`.\nAnd position keys are not case sensitive so instead of key `top_left` can be used `TOP_LEFT` or `Top_Left`\n:::\n\n:::note Filename suffix\nFilename suffix is appended to filename suffix of source representation.\nIf source representation has suffix **\"h264\"** and burnin suffix is **\"client\"** then final suffix is **\"h264_client\"**.\n:::\n\n**Available keys in burnin content**\n\n- It is possible to use same keys as in [Anatomy](admin_config#available-keys).\n\n- It is allowed to use [Anatomy templates](admin_config#anatomy) themselves in burnins if they can be filled with available data.\n\n- Additional keys in burnins:\n\n  | Burnin key | Description |\n  | --- | --- |\n  | frame_start | First frame number. |\n  | frame_end | Last frame number. |\n  | current_frame | Frame number for each frame. |\n  | duration | Count number of frames. |\n  | resolution_width | Resolution width. |\n  | resolution_height | Resolution height. |\n  | fps | Fps of an output. |\n  | timecode | Timecode by frame start and fps. |\n  | focalLength | **Only available in Maya and Houdini**<br /><br />Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. |\n\n:::warning\n`timecode` is specific key that can be **only at the end of content**. (`\"BOTTOM_RIGHT\": \"TC: {timecode}\"`)\n:::\n\n```json\n{\n    \"profiles\": [{\n        \"burnins\": {\n            \"example\": {\n                \"TOP_LEFT\": \"{dd}.{mm}.{yyyy}\",\n                /* Use anatomy template values. */\n                \"TOP_CENTERED\": \"{anatomy[publish][path]}\",\n                /* Python's formatting:\n                \":0>3\" adds padding to version number to have 3 digits. */\n                \"TOP_RIGHT\": \"v{version:0>3}\",\n                \"BOTTOM_LEFT\": \"{frame_start}-{current_frame}-{frame_end}\",\n                \"BOTTOM_CENTERED\": \"{asset}\",\n                \"BOTTOM_RIGHT\": \"{username}\"\n            }\n        }\n    }]\n    ...\n}\n```\n\n\n#### Default content values (`fields`)\nIf you want to set position content values for all or most of burnin definitions, you can set them in **\"fields\"**. They will be added to every burnin definition in all profiles. Value can be overridden if same position key is filled in burnin definition.\n\n```json\n{\n    \"fields\": {\n        \"TOP_LEFT\": \"{yy}-{mm}-{dd}\",\n        \"TOP_CENTERED\": \"{username}\",\n        \"TOP_RIGHT\": \"v{version:0>3}\"\n    },\n    \"profiles\": [{\n        \"burnins\": {\n            /* example1 has empty definition but top left, center and right values\n            will be filled. */\n            \"example1\": {},\n\n            /* example2 has 2 overrides. */\n            \"example2\": {\n                /* Top left value is overridden with asset name. */\n                \"TOP_LEFT\": \"{asset}\",\n                /* Top center will be skipped. */\n                \"TOP_CENTERED\": null\n            }\n        }\n    }]\n}\n```\n\n#### Full presets example\n:::note\nThis is just usage example, without relevant data. Do **NOT** use these presets as default in production.\n:::\n\n```json\n{\n    \"ExtractBurnin\": {\n        \"options\": {\n            \"opacity\": 1,\n            \"x_offset\": 5,\n            \"y_offset\": 5,\n            \"bg_padding\": 5,\n            \"bg_opacity\": 0.5,\n            \"font_size\": 42\n        },\n        \"fields\": {\n            \"TOP_LEFT\": \"{yy}-{mm}-{dd}\",\n            \"TOP_RIGHT\": \"v{version:0>3}\"\n        },\n        \"profiles\": [{\n            \"burnins\": {\n                \"burnin\": {\n                    \"options\": {\n                        \"opacity\": 1\n                    },\n                    \"TOP_LEFT\": \"{username}\"\n                }\n           }\n        }, {\n            \"families\": [\"animation\", \"pointcache\", \"model\"],\n            \"tasks\": [\"animation\"],\n            \"burnins\": {}\n        }, {\n            \"families\": [\"render\"],\n            \"tasks\": [\"compositing\"],\n            \"burnins\": {\n                \"burnin\": {\n                    \"TOP_LEFT\": \"{yy}-{mm}-{dd}\",\n                    \"TOP_RIGHT\": \"v{version:0>3}\",\n                    \"BOTTOM_RIGHT\": \"{frame_start}-{current_frame}-{frame_end}\",\n                    \"BOTTOM_LEFT\": \"{username}\"\n                },\n                \"burnin_ftrack\": {\n                    \"filter\": {\n                        \"families\": [\"ftrack\"]\n                    },\n                    \"BOTTOM_RIGHT\": \"{frame_start}-{current_frame}-{frame_end}\",\n                    \"BOTTOM_LEFT\": \"{username}\"\n                },\n                \"burnin_v2\": {\n                    \"options\": {\n                        \"opacity\": 0.5\n                    },\n                    \"TOP_LEFT\": \"{yy}-{mm}-{dd}\",\n                    \"TOP_RIGHT\": \"v{version:0>3}\"\n                }\n            }\n        }, {\n            \"families\": [\"rendersetup\"],\n            \"burnins\": {\n                \"burnin\": {\n                    \"TOP_LEFT\": \"{yy}-{mm}-{dd}\",\n                    \"BOTTOM_LEFT\": \"{username}\"\n                }\n            }\n        }, {\n            \"tasks\": [\"animation\"],\n            \"burnins\": {\n                \"burnin\": {\n                    \"TOP_RIGHT\": \"v{version:0>3}\",\n                    \"BOTTOM_RIGHT\": \"{frame_start}-{current_frame}-{frame_end}\"\n                }\n            }\n        }]\n    }\n}\n```\n\n### `ProcessSubmittedJobOnFarm`\n\n```json\n{\n    \"ProcessSubmittedJobOnFarm\": {\n        \"aov_filter\": {\n            \"host\": [\"aov_name\"],\n            \"maya\": [\"beauty\"]\n        },\n        \"deadline_pool\": \"\"\n    }\n}\n```\n\n## Maya\n\n### load.json\n\n### `colors`\n\nmaya outliner colours for various families\n\n```python\n\"colors\": {\n  \"model\": [0.821, 0.518, 0.117],\n  \"rig\": [0.144, 0.443, 0.463],\n  \"pointcache\": [0.368, 0.821, 0.117],\n  \"animation\": [0.368, 0.821, 0.117],\n  \"ass\": [1.0, 0.332, 0.312],\n  \"camera\": [0.447, 0.312, 1.0],\n  \"fbx\": [1.0, 0.931, 0.312],\n  \"mayaAscii\": [0.312, 1.0, 0.747],\n  \"mayaScene\": [0.312, 1.0, 0.747],\n  \"setdress\": [0.312, 1.0, 0.747],\n  \"layout\": [0.312, 1.0, 0.747],\n  \"vdbcache\": [0.312, 1.0, 0.428],\n  \"vrayproxy\": [0.258, 0.95, 0.541],\n  \"yeticache\": [0.2, 0.8, 0.3],\n  \"yetiRig\": [0, 0.8, 0.5]\n}\n```\n\n### publish.json\n\n### `ValidateModelName`\n\n```python\n\"ValidateModelName\": {\n    \"enabled\": false,\n    \"material_file\": \"/path/to/shader_name_definition.txt\",\n    \"regex\": \"(.*)_(\\\\d)*_(?P<shader>.*)_(GEO)\"\n},\n```\n\n### `ValidateShaderName`\n\n```python\n\"ValidateShaderName\": {\n    \"enabled\": false,\n    \"regex\": \"(?P<asset>.*)_(.*)_SHD\"\n}\n```\n\n## Nuke\n\n### create.json\n\n### `CreateWriteRender`\n\n```python\n\"CreateWriteRender\": {\n    \"fpath_template\": \"{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}\"\n}\n```\n\n### publish.json\n\n### `ExtractThumbnail`\n\nPlugin responsible for generating thumbnails with colorspace controlled by Nuke. Reformat node will secure proper framing within the default workfile screen space.\n\n```json\n{\n\"nodes\": {\n    \"Reformat\": [\n        [\"type\", \"to format\"],\n        [\"format\", \"HD_1080\"],\n        [\"filter\", \"Lanczos6\"],\n        [\"black_outside\", true],\n        [\"pbb\", false]\n    ]\n}\n}\n```\n\n### `ExtractReviewIntermediates`\n`viewer_lut_raw` **true** will publish the baked mov file without any colorspace conversion. It will be baked with the workfile workspace. This can happen in case the Viewer input process uses baked screen space luts.\n\n#### baking with controlled colorspace\n\nSome productions might be using custom OCIO config files either for whole project, sequence or even individual shots. In that case we can use **display roles** to let compositors use their preferred viewer space, but also make sure baking of outputs is happening in a defined space for clients reviews.\n\n\n`bake_colorspace_fallback` this will be used if for some reason no space defined in `shot_grade_rec709` is found on shot's _config.ocio_\n\n> be aware this will only work if `viewer_lut_raw` is on _false_\n\n```json\n{\n\"viewer_lut_raw\": false,\n\"bake_colorspace_fallback\": \"show_lut_rec709\",\n\"bake_colorspace_main\": \"shot_grade_rec709\"\n}\n```\n\n## NukeStudio\n\n### Publish.json\n\nDestination of the following example codes:\n\n[`presets/plugins/nukestudio/publish.json`](https://github.com/pypeclub/pype-config/blob/develop/presets/plugins/nukestudio/publish.json)\n\n### `CollectInstanceVersion`\n\nActivate this plugin if you want your published plates to always have the same version as the hiero project they were published from. If this plugin is off, plate versioning automatically finds the next available version in the database.\n\n```json\n{\n    \"CollectInstanceVersion\": {\n        \"enabled\": true\n    }\n}\n```\n\n### `ExtractReviewCutUpVideo`\n\nExample of tag which could be added into the plugin preset.\nIn this case because we might have 4K plates but we would like to publish all review files reformated to 2K.\n\n[details of available tags](#preset-attributes)\n\n```json\n{\n    \"ExtractReviewCutUpVideo\": {\n        \"tags_addition\": [\"reformat\"]\n    }\n}\n```\n\n## Standalone Publisher\n\nDocumentation yet to come.\n"
  },
  {
    "path": "website/docs/pype2/admin_presets_tools.md",
    "content": "---\nid: admin_presets_tools\ntitle: Presets > Tools\nsidebar_label: Tools\n---\n\n## Colorspace\n\nWe provide two examples of possible settings for nuke, but these can vary wildly between clients and projects.\n\n### `Default` [dict]\n\npath: `pype-config/presets/colorspace/default.json`\n\n```python\n\"nuke\": {\n    \"root\": {\n        \"colorManagement\": \"Nuke\",\n        \"OCIO_config\": \"nuke-default\",\n        \"defaultViewerLUT\": \"Nuke Root LUTs\",\n        \"monitorLut\": \"sRGB\",\n        \"int8Lut\": \"sRGB\",\n        \"int16Lut\": \"sRGB\",\n        \"logLut\": \"Cineon\",\n        \"floatLut\": \"linear\"\n    },\n    \"viewer\": {\n        \"viewerProcess\": \"sRGB\"\n    },\n    \"write\": {\n        \"render\": {\n            \"colorspace\": \"linear\"\n        },\n        \"prerender\": {\n            \"colorspace\": \"linear\"\n        },\n        \"still\": {\n            \"colorspace\": \"sRGB\"\n        }\n    }\n},\n```\n\n### `aces103-cg` [dict]\n\n\npath: `pype-config/presets/colorspace/aces103-cg.json`\n\n```python\n\"nuke\": {\n  \"root\": {\n    \"colorManagement\": \"OCIO\",\n    \"OCIO_config\": \"aces_1.0.3\",\n    \"workingSpaceLUT\": \"ACES - ACEScg\",\n    \"defaultViewerLUT\": \"OCIO LUTs\",\n    \"monitorLut\": \"ACES/sRGB D60 sim.\",\n    \"int8Lut\": \"Utility - sRGB - Texture\",\n    \"int16Lut\": \"Utility - sRGB - Texture\",\n    \"logLut\": \"Input - ARRI - V3 LogC (EI800) - Wide Gamut\",\n    \"floatLut\": \"ACES - ACES2065-1\"\n  },\n  \"viewer\": {\n    \"viewerProcess\": \"sRGB D60 sim. (ACES)\"\n  },\n  \"write\": {\n    \"render\": {\n      \"colorspace\": \"ACES - ACEScg\"\n    },\n    \"prerender\": {\n      \"colorspace\": \"ACES - ACEScg\"\n    },\n    \"still\": {\n      \"colorspace\": \"Utility - Curve - sRGB\"\n    }\n  }\n},\n```\n\n\n## Creator Defaults\n\npath: `pype-config/presets/tools/creator.json`\n\nThis preset tells the creator tools what family should be pre-selected in different tasks. Keep in mind that the task is matched loosely so for example any task with 'model' in it's name will be considered a modelling task for these purposes.\n\n`\"Family name\": [\"list, \"of, \"tasks\"]`\n\n```python\n\"Model\": [\"model\"],\n\"Render Globals\": [\"light\", \"render\"],\n\"Layout\": [\"layout\"],\n\"Set Dress\": [\"setdress\"],\n\"Look\": [\"look\"],\n\"Rig\": [\"rigging\"]\n```\n\n## Project Folder Structure\n\npath: `pype-config/presets/tools/project_folder_structure.json`\n\nDefines the base folder structure for a project. This is supposed to act as a starting point to quickly create the base of the project. You can add `[ftrack.entityType]` after any of the folders here and they will automatically be also created in ftrack project.\n\n### `__project_root__` [dict]\n\n```python\n\"__project_root__\": {\n    \"_prod\" : {},\n    \"_resources\" : {\n      \"footage\": {\n        \"ingest\": {},\n        \"offline\": {}\n      },\n      \"audio\": {},\n      \"art_dept\": {},\n    },\n    \"editorial\" : {},\n    \"assets[ftrack.Library]\": {\n      \"characters[ftrack]\": {},\n      \"locations[ftrack]\": {}\n    },\n    \"shots[ftrack.Sequence]\": {\n      \"editorial[ftrack.Folder]\": {}\n    }\n}\n```\n\n## Software Folders\n\npath: `pype-config/presets/tools/sw_folders.json`\n\nDefines extra folders to be created inside the work space when particular task type is launched. Mostly used for configs, that use {app} key in their work template and want to add hosts that are not supported yet.\n\n```python\n\"compositing\": [\"nuke\", \"ae\"],\n\"modeling\": [\"maya\", \"app2\"],\n\"lookdev\": [\"substance\"],\n\"animation\": [],\n\"lighting\": [],\n\"rigging\": []\n```\n\n## Tray Items\n\npath: `pype-config/presets/tray/menu_items.json`\n\nThis preset let's admins to turn different pype modules on and off from the tray menu, which in turn makes them unavailable across the pipeline\n\n### `item_usage` [dict]\n\n```python\n\"item_usage\": {\n    \"User settings\": false,\n    \"Ftrack\": true,\n    \"Muster\": false,\n    \"Avalon\": true,\n    \"Clockify\": false,\n    \"Standalone Publish\": true,\n    \"Logging\": true,\n    \"Idle Manager\": true,\n    \"Timers Manager\": true,\n    \"Rest Api\": true\n},\n```\n\n## Muster Templates\n\npath: `pype-config/presets/muster/templates_mapping.json`\n\nMuster template mapping maps Muster template ID to name of renderer. Initially it is set Muster defaults. About templates and Muster se Muster Documentation. Mapping is defined in:\n\nKeys are renderer names and values are templates IDs.\n\n```python\n\"3delight\": 41,\n\"arnold\": 46,\n\"arnold_sf\": 57,\n\"gelato\": 30,\n\"hardware\": 3,\n\"krakatoa\": 51,\n\"file_layers\": 7,\n\"mentalray\": 2,\n\"mentalray_sf\": 6,\n\"redshift\": 55,\n\"renderman\": 29,\n\"software\": 1,\n\"software_sf\": 5,\n\"turtle\": 10,\n\"vector\": 4,\n\"vray\": 37,\n\"ffmpeg\": 48\n```\n"
  },
  {
    "path": "website/docs/pype2/admin_pype_commands.md",
    "content": "---\nid: admin_pype_commands\ntitle: Pype Commands Reference\nsidebar_label: Pype Commands\n---\n\n\n\n## Help\n\nTo get all available commands:\n```sh\npype --help\n```\n\nTo get help on particular command:\n```sh\npype <command> --help\n```\n\n--------------------\n## `clean`\n\nCommand to clean Python bytecode files from Pype and it's environment. Useful\nfor developers after code or environment update.\n\n--------------------\n\n## `coverage`\n\n### `--pype`\n- without this option, tests are run on *pype-setup* only.\n\nGenerate code coverage report.\n```sh\npype coverage --pype\n```\n\n--------------------\n\n## `deploy`\n\nTo deploy Pype:\n```sh\npype deploy\n```\n\n### `--force`\n\nTo force re-deploy:\n```sh\npype deploy --force\n```\n\n---------------------------\n\n## `download`\n\nTo download required dependencies:\n```sh\npype download\n```\n\n--------------------\n\n## `eventserver`\n\nThis command launches ftrack event server.\n\nThis should be ideally used by system service (such us systemd or upstart\non linux and window service).\n\nYou have to set either proper environment variables to provide URL and\ncredentials or use option to specify them. If you use `--store_credentials`\nprovided credentials will be stored for later use.\n\nTo run ftrack event server:\n```sh\npype eventserver --ftrack-url=<url> --ftrack-user=<user> --ftrack-api-key=<key> --ftrack-events-path=<path> --no-stored-credentials --store-credentials\n```\n\n### `--debug`\n- print debug info\n\n### `--ftrack-url`\n- URL to ftrack server\n\n### `--ftrack-user`\n- user name to log in to ftrack\n\n### `--ftrack-api-key`\n- ftrack api key\n\n### `--ftrack-events-path`\n- path to event server plugins\n\n### `--no-stored-credentials`\n- will use credential specified with options above\n\n### `--store-credentials`\n- will store credentials to file for later use\n\n--------------------\n\n## `install`\n\nTo install Pype:\n\n```sh\npype install\n```\n\n### `--force`\n\nTo reinstall Pype:\n```sh\npype install --force\n```\n\n### `--offline`\n\nTo install Pype in offline mode:\n```sh\npype install --offline\n```\n\nTo reinstall Pype in offline mode:\n```sh\npype install --offline --force\n```\n\n--------------------\n\n## `launch`\n\nLaunch application in Pype environment.\n\n### `--app`\n\nApplication name - this should be the same as it's [defining toml](admin_hosts#launchers) file (without .toml)\n\n### `--project`\nProject name\n\n### `--asset`\nAsset name\n\n### `--task`\nTask name\n\n### `--tools`\n*Optional: Additional tools environment files to add*\n\n### `--user`\n*Optional: User on behalf to run*\n\n### `--ftrack-server` / `-fs`\n*Optional: Ftrack server URL*\n\n### `--ftrack-user` / `-fu`\n*Optional: Ftrack user*\n\n### `--ftrack-key` / `-fk`\n*Optional: Ftrack API key*\n\nFor example to run Python interactive console in Pype context:\n```sh\npype launch --app python --project my_project --asset my_asset --task my_task\n```\n\n--------------------\n\n## `make_docs`\n\nGenerate API documentation into `docs/build`\n```sh\npype make_docs\n```\n\n--------------------\n\n## `mongodb`\n\nTo run testing mongodb database (requires mongoDB installed on the workstation):\n```sh\npype mongodb\n```\n\n--------------------\n\n## `publish`\n\nPype takes JSON from provided path and use it to publish data in it.\n```sh\npype publish <PATH_TO_JSON>\n```\n\n### `--gui`\n- run Pyblish GUI\n\n### `--debug`\n- print more verbose information\n\n--------------------\n\n## `test`\n\n### `--pype`\n- without this option, tests are run on *pype-setup* only.\n\nRun test suite on Pype:\n```sh\npype test --pype\n```\n:::note Pytest\nFor more information about testing see [Pytest documentation](https://docs.pytest.org/en/latest/)\n:::\n\n--------------------\n\n## `texturecopy`\n\nCopy specified textures to provided asset path.\n\nIt validates if project and asset exists. Then it will\ncopy all textures found in all directories under `--path` to destination\nfolder, determined by template texture in **anatomy**. I will use source\nfilename and automatically rise version number on directory.\n\nResult will be copied without directory structure so it will be flat then.\nNothing is written to database.\n\n### `--project`\n\n### `--asset`\n\n### `--path`\n\n```sh\npype texturecopy --project <PROJECT_NAME> --asset <ASSET_NAME> --path <PATH_TO_JSON>\n```\n\n--------------------\n\n## `tray`\n\nTo launch Tray:\n```sh\npype tray\n```\n\n### `--debug`\n\nTo launch Tray with debugging information:\n```sh\npype tray --debug\n```\n\n--------------------\n\n## `update-requirements`\n\nSynchronize dependencies in your virtual environment with requirement.txt file.\nEquivalent of running `pip freeze > pypeapp/requirements.txt` from your virtual\nenvironment. This is useful for development purposes.\n\n```sh\npype update-requirements\n```\n\n--------------------\n\n## `validate`\n\nTo validate deployment:\n```sh\npype validate\n```\n\n--------------------\n\n## `validate-config`\n\nTo validate JSON configuration files for syntax errors:\n```sh\npype validate-config\n```\n"
  },
  {
    "path": "website/docs/pype2/admin_setup_troubleshooting.md",
    "content": "---\nid: admin_setup_troubleshooting\ntitle: Setup Troubleshooting\nsidebar_label: Setup Troubleshooting\n---\n\n## SSL Server certificates\n\nPython is strict about certificates when connecting to server with SSL. If\ncertificate cannot be validated, connection will fail. Therefore care must be\ntaken when using self-signed certificates to add their certification authority\nto trusted certificates.\n\nAlso please note that even when using certificates from trusted CA, you need to\nupdate your trusted CA certificates bundle as those certificates can change.\n\nSo if you receive SSL error `cannot validate certificate` or similar, please update root CA certificate bundle on machines and possibly **certifi** python package in Pype virtual environment - just edit `pypeapp/requirements.txt` and update its version. You can find current versions on [PyPI](https://pypi.org).\n"
  },
  {
    "path": "website/docs/system_introduction.md",
    "content": "---\nid: system_introduction\ntitle: Introduction\nsidebar_label: Introduction\n---\n\n\n**OpenPype** is a python application built on top of many other open-source libraries, modules and projects.\nTo be able to use it, you need those tools and set your environment. This\nrequires additional software installed and set up correctly on your system.\n\nFortunately this daunting task is mostly handled for you by OpenPype build and install scripts. **OpenPype** can\ninstall most of its requirements automatically but a few more things are needed in\nvarious usage scenarios.\n\n## Studio Preparation\n\nYou can find a detailed breakdown of technical requirements [here](dev_requirements), but in general OpenPype should be able\nto operate in most studios fairly quickly. The main obstacles are usually related to workflows and habits, that\nmight not be fully compatible with what OpenPype is expecting or enforcing. It is recommended to go through artists [key concepts](artist_concepts) to get comfortable with the basics.\n\nKeep in mind that if you run into any workflows that are not supported, it's usually just because we haven't hit \nthat particular case and it can most likely be added upon request. \n\n\n## Artist Workstations\n\nTo use **OpenPype** in production, it should be installed on each artist workstation, whether that is in the studio or at home in \ncase of a distributed workflow. Once started, it lives in the system tray menu bar and all of it's tools are executed locally on \nthe artist computer. There are no special requirements for the artist workstations if you are running openPype from a frozen build.\n\nEach artist computer will need to be able to connect to your central mongo database to load and publish any work. They will also need\naccess to your centralized project storage, unless you are running a fully distributed pipeline.\n\n## Centralized and Distributed?\n\nOpenPype supports a variety of studio setups, for example:\n\n- Single physical location with monolithic project storage.\n- Fully remote studios, utilizing artist's home workstations.\n- Distributed studios, running fully or partially on the cloud.\n- Hybrid setups with different storages per project.\n- And others that we probably didn't think of at all.\n\nIt is totally up to you how you deploy and distribute OpenPype to your artist, but there are a few things to keep in mind:\n- While it is possible to store project files in different locations for different artist, it bring a lot of extra complexity\nto the table\n- Some DCCs do not support using Environment variables in file paths. This will make it very hard to maintain full multiplatform\ncompatibility as well variable storage roots.\n- Relying on VPN connection and using it to work directly of network storage will be painfully slow.\n"
  },
  {
    "path": "website/docusaurus.config.js",
    "content": "module.exports = {\n  title: 'openPYPE',\n  tagline: 'Pipeline with support, for studios and remote teams.',\n  url: 'http://openpype.io/',\n  baseUrl: '/',\n  organizationName: 'Orbi Tools s.r.o',\n  projectName: 'openPype',\n  favicon: 'img/favicon/favicon.ico',\n  onBrokenLinks: 'ignore',\n  customFields: {\n  },\n  presets: [\n    [\n      '@docusaurus/preset-classic', {\n        docs: {\n          sidebarPath: require.resolve('./sidebars.js'),\n        },\n        theme: {\n          customCss: require.resolve('./src/css/custom.css')\n        },\n        gtag: {\n        trackingID: 'G-DTKXMFENFY',\n        // Optional fields.\n        anonymizeIP: false, // Should IPs be anonymized?\n      }\n      }\n    ],\n    \n  ],\n  themeConfig: {\n    colorMode: {\n      // \"light\" | \"dark\"\n      defaultMode: 'light',\n\n      // Hides the switch in the navbar\n      // Useful if you want to support a single color mode\n      disableSwitch: true\n    },\n    announcementBar: {\n      id: 'help_with_docs', // Any value that will identify this message.\n      content:\n      'Help us make this documentation better. <b><a href=\"https://github.com/pypeclub/OpenPype/tree/develop/website\">Edit on github.</a></b>.',\n      backgroundColor: '#fff', // Defaults to `#fff`.\n      textColor: '#000', // Defaults to `#000`.\n    },\n    navbar: {\n      style: 'dark',\n      title: 'openPYPE',\n      logo: {\n        src: 'img/logos/splash_main.svg'\n      },\n      items: [\n        {\n          to: '/features',\n          label: 'Features',\n          position: 'left'\n        }, {\n          to: 'docs/artist_getting_started',\n          label: 'User Docs',\n          position: 'left'\n        },\n        {\n          to: 'docs/system_introduction',\n          label: 'Admin Docs',\n          position: 'left'\n        },\n        {\n          to: 'docs/dev_introduction',\n          label: 'Dev Docs',\n          position: 'left'\n        },\n          {\n            to: 'https://pype.club',\n            'aria-label': 'pypeclub',\n            className: 'header-pypeclub-link',\n            position: 'right',\n          },{\n          to: 'https://github.com/pypeclub',\n          'aria-label': 'Github',\n          className: 'header-github-link',\n          position: 'right',\n        }\n      ]\n    },\n    footer: {\n      style: 'dark',\n      links: [\n        {\n          title: 'Pages',\n          items: [\n            {\n              label: 'Features',\n              to: 'features',\n            },\n            {\n              label: 'Artist',\n              to: 'docs/artist_getting_started',\n            },\n            {\n              label: 'Admin',\n              to: 'docs/admin_getting_started',\n            }\n          ]\n        },\n        {\n          title: 'Community',\n          items: [\n            {\n              label: 'Avalon Chat',\n              to: 'https://gitter.im/getavalon/Lobby',\n            },\n            {\n              label: 'OpenPype Chat',\n              to: 'https://discord.gg/sFNPWXG',\n            },\n            {\n              label: 'Github Discussions',\n              to: 'https://github.com/pypeclub/pype/discussions',\n            }\n          ],\n        },\n      ],\n      copyright: 'Copyright © 2021 Orbi Tools',\n    },\n    algolia: {\n      apiKey: '5e01ee3bfbb744ca6f25d4b281ce38a9',\n      indexName: 'openpype',\n      // Optional: see doc section below\n      contextualSearch: true,\n      // Optional: Algolia search parameters\n      searchParameters: {},\n    }\n  },\n  stylesheets: [\n        'https://use.fontawesome.com/releases/v5.7.2/css/all.css'\n    ],\n};\n"
  },
  {
    "path": "website/publish.cmd",
    "content": "cd %~dp0\nset GIT_USER=mkolar\nset CURRENT_BRANCH=develop\nset USE_SSH=true\nyarn deploy\n"
  },
  {
    "path": "website/sidebars.js",
    "content": "module.exports = {\n    artist: [\n        {\n            type: \"category\",\n            collapsed: false,\n            label: \"General\",\n            items: [\n                \"artist_getting_started\",\n                \"artist_concepts\",\n                \"artist_publish\",\n                {\n                    type: \"category\",\n                    collapsed: true,\n                    label: \"Tools\",\n                    link: {type: 'doc', id: 'artist_tools'},\n                    items: [\n                        \"artist_tools_context_manager\",\n                        \"artist_tools_creator\",\n                        \"artist_tools_loader\",\n                        \"artist_tools_library_loader\",\n                        \"artist_tools_publisher\",\n                        \"artist_tools_inventory\",\n                        \"artist_tools_workfiles\",\n                        \"artist_tools_look_assigner\",\n                        \"artist_tools_subset_manager\",\n                        \"artist_tools_sync_queue\"\n                    ],\n                },\n                \"artist_install\"\n            ],\n        },\n        {\n            type: \"category\",\n            collapsed: false,\n            label: \"Integrations\",\n            items: [\n                \"artist_hosts_hiero\",\n                \"artist_hosts_nuke_tut\",\n                {\n                    type: \"category\",\n                    label: \"Maya\",\n                    items: [\n                        \"artist_hosts_maya\",\n                        \"artist_hosts_maya_multiverse\",\n                        \"artist_hosts_maya_yeti\",\n                        \"artist_hosts_maya_xgen\",\n                        \"artist_hosts_maya_arnold\",\n                        \"artist_hosts_maya_vray\",\n                        \"artist_hosts_maya_redshift\",\n                    ],\n                },\n                \"artist_hosts_blender\",\n                \"artist_hosts_3dsmax\",\n                \"artist_hosts_harmony\",\n                \"artist_hosts_houdini\",\n                \"artist_hosts_aftereffects\",\n                \"artist_hosts_resolve\",\n                \"artist_hosts_photoshop\",\n                \"artist_hosts_tvpaint\",\n                \"artist_hosts_unreal\",\n                \"artist_kitsu\",\n                {\n                    type: \"category\",\n                    label: \"Ftrack\",\n                    items: [\n                        \"artist_ftrack\",\n                        \"manager_ftrack\",\n                        \"manager_ftrack_actions\",\n                    ],\n                }\n            ],\n        },\n    ],\n    Admin: [\n        \"system_introduction\",\n        {\n            type: \"category\",\n            label: \"Getting Started\",\n            items: [\n                \"admin_builds\",\n                \"admin_distribute\",\n                \"admin_use\",\n                \"admin_openpype_commands\",\n            ],\n        },\n        {\n            type: \"category\",\n            label: \"Configuration\",\n            items: [\n                \"admin_environment\",\n                \"admin_settings\",\n                \"admin_settings_system\",\n                \"admin_settings_project_anatomy\",\n                {\n                    type: \"category\",\n                    label: \"Project Settings\",\n                    items: [\n                        \"project_settings/settings_project_global\",\n                        \"project_settings/settings_project_nuke\",\n                        \"project_settings/settings_project_standalone\"\n                    ],\n                },\n            ],\n        },\n        {\n            type: \"category\",\n            label: \"Modules\",\n            items: [\n                \"module_ftrack\",\n                \"module_kitsu\",\n                \"module_site_sync\",\n                \"module_deadline\",\n                \"module_royalrender\",\n                \"module_clockify\",\n                \"module_slack\"\n            ],\n        },\n        {\n            type: \"category\",\n            label: \"Integrations\",\n            items: [\n                \"admin_hosts_blender\",\n                \"admin_hosts_hiero\",\n                \"admin_hosts_houdini\",\n                \"admin_hosts_maya\",\n                \"admin_hosts_nuke\",\n                \"admin_hosts_resolve\",\n                \"admin_hosts_harmony\",\n                \"admin_hosts_photoshop\",\n                \"admin_hosts_aftereffects\",\n                \"admin_hosts_tvpaint\"\n            ],\n        },\n        \"admin_releases\",\n        {\n            type: \"category\",\n            collapsed: false,\n            label: \"2.0 legacy docs\",\n            items: [\n                {\n                    type: \"category\",\n                    label: \"Deployment\",\n                    items: [\n                        \"pype2/admin_getting_started\",\n                        \"pype2/admin_install\",\n                        \"pype2/admin_config\",\n                        \"pype2/admin_ftrack\",\n                        \"pype2/admin_hosts\",\n                        \"pype2/admin_pype_commands\",\n                        \"pype2/admin_setup_troubleshooting\",\n                    ],\n                },\n                {\n                    type: \"category\",\n                    label: \"Configuration\",\n                    items: [\n                        \"pype2/admin_presets_nukestudio\",\n                        \"pype2/admin_presets_ftrack\",\n                        \"pype2/admin_presets_maya\",\n                        \"pype2/admin_presets_plugins\",\n                        \"pype2/admin_presets_tools\",\n                    ],\n                },\n            ],\n        },\n    ],\n    Dev: [\n        \"dev_introduction\",\n        \"dev_requirements\",\n        \"dev_build\",\n        \"dev_testing\",\n        \"dev_contribute\",\n        \"dev_settings\",\n        {\n            type: \"category\",\n            label: \"Hosts integrations\",\n            items: [\n                \"dev_host_implementation\",\n                \"dev_publishing\"\n            ]\n        },\n        \"dev_deadline\",\n        \"dev_blender\",\n        \"dev_colorspace\"\n    ]\n};\n"
  },
  {
    "path": "website/src/components/BadgesSection/badges.js",
    "content": "export default {\n    upper: [\n        {\n            title: \"VFX Platform\",\n            src:\n                \"https://img.shields.io/badge/vfx%20platform-2021-lightgrey?labelColor=303846\",\n            href: \"https://vfxplatform.com\",\n        },\n        {\n            title: \"License\",\n            src:\n                \"https://img.shields.io/github/license/pypeclub/openpype?labelColor=303846\",\n            href: \"https://github.com/pypeclub/openpype\",\n        },\n        {\n            title: \"Release\",\n            src:\n                \"https://img.shields.io/github/v/release/pypeclub/openpype?labelColor=303846\",\n            href: \"https://github.com/pypeclub/openpype\",\n        },\n        {\n            title: \"GitHub last commit\",\n            src:\n                \"https://img.shields.io/github/last-commit/pypeclub/openpype/develop?labelColor=303846\",\n            href: \"https://github.com/pypeclub/openpype\",\n        },\n        {\n            title: \"GitHub commit activity\",\n            src:\n                \"https://img.shields.io/github/commit-activity/y/pypeclub/openpype?labelColor=303846\",\n            href: \"https://github.com/pypeclub/openpype\",\n        },\n        {\n            title: \"Repository Size\",\n            src:\n                \"https://img.shields.io/github/repo-size/pypeclub/openpype?labelColor=303846\",\n            href: \"https://github.com/pypeclub/openpype\",\n        },\n        {\n            title: \"Repository Size\",\n            src:\n                \"https://img.shields.io/github/contributors/pypeclub/openpype?labelColor=303846\",\n            href: \"https://github.com/pypeclub/openpype\",\n        },\n        {\n            title: \"Stars\",\n            src:\n            \"https://img.shields.io/github/stars/pypeclub?labelColor=303846\",\n            href: \"https://github.com/pypeclub/openpype\",\n        },\n        {\n            title: \"Forks\",\n            src:\n            \"https://img.shields.io/github/forks/pypeclub/openpype?labelColor=303846\",\n            href: \"https://github.com/pypeclub/openpype\",\n        },\n        {\n            title: \"Discord\",\n            src:\n            \"https://img.shields.io/discord/517362899170230292?label=discord&logo=discord&logoColor=white&labelColor=303846\",\n            href: \"https://discord.gg/sFNPWXG\",\n        },\n    ],\n};\n"
  },
  {
    "path": "website/src/components/BadgesSection/index.js",
    "content": "import React from 'react';\n\nimport badges from './badges';\nimport styles from './styles.module.css';\nimport {StarButton} from \"../index\";\n\nconst Badge = props => (\n  <a href={props.href} title={props.title}>\n    <img src={props.src} alt={props.title}/>\n  </a>\n);\n\nexport default function BadgesSection() {\n  const {upper: upperBadges} = badges;\n\n  return (\n    <div className={styles.badgesSection}>\n      <div className={styles.upperBadges}>\n        {upperBadges.map((badge, index) => (\n          <Badge key={index} {...badge} />\n        ))}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "website/src/components/BadgesSection/styles.module.css",
    "content": ".badgesSection {\n  margin-bottom: 5em;\n}\n.upperBadges,\n.lowerBadges {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: center;\n}\n.upperBadges {\n  margin-bottom: 17px;\n}\n.upperBadges a,\n.lowerBadges a {\n  margin-left: 2px;\n  margin-right: 2px;\n}\n"
  },
  {
    "path": "website/src/components/GithubButton/index.js",
    "content": "import React from 'react';\nimport useDocusaurusContext from '@docusaurus/useDocusaurusContext';\n\nexport function StarButton() {\n  const context = useDocusaurusContext();\n  const {siteConfig = {}} = context;\n\n  return <a\n    className=\"github-button\"\n    href={siteConfig.customFields.mainRepoUrl}\n    title=\"See this project on GitHub\"\n    data-icon=\"octicon-star\"\n    data-show-count=\"true\"\n    data-count-href={`/${siteConfig.organizationName}/${siteConfig.projectName}/stargazers`}\n    data-count-aria-label=\"# stargazers on GitHub\"\n    aria-label=\"Star this project on GitHub\">\n    T-Regx\n  </a>;\n}\n\nexport function SponsorButton() {\n  return <div style={{height: '32px'}}>\n    <iframe\n      src=\"\"\n      title=\"Sponsor\"\n      height=\"35\"\n      width=\"116\"\n      style={{\"border\": 0}}/>\n  </div>;\n}\n"
  },
  {
    "path": "website/src/components/index.js",
    "content": "import BadgesSection from './BadgesSection';\nimport {StarButton, SponsorButton} from './GithubButton';\n\nexport {BadgesSection, StarButton, SponsorButton};\n"
  },
  {
    "path": "website/src/css/custom.css",
    "content": "@import url(\"https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap\");\n\n:root{\n\n  --ifm-color-primary: #36b2ca;\n  --ifm-color-primary-dark: #30a1b6;\n  --ifm-color-primary-darker: #2d98ac;\n  --ifm-color-primary-darkest: #257d8e;\n  --ifm-color-primary-light: #4abacf;\n  --ifm-color-primary-lighter: #54bed2;\n  --ifm-color-primary-lightest: #73c9da;\n\n  --ifm-color-info-lightest: rgba(170, 227, 246, 0.75);\n  --ifm-color-success-lightest: rgba(204, 241, 204, 0.71);\n  --ifm-color-warning-lightest: rgba(255, 221, 128, 0.55);\n  --ifm-color-danger-lightest: rgba(251, 178, 181, 0.53);\n\n  --ifm-navbar-background-color:#1f2329;\n\n  --ifm-card-border-radius: calc(0.4rem);\n  --ifm-global-shadow-lw: 0 2px 4px 0 rgba(0, 0, 0, 0.1);\n\n  --ifm-font-family-base: \"Poppins\", sans-serif;\n  --ifm-font-weight-light: 200;\n  --ifm-font-weight-normal: 300;\n  --ifm-font-weight-semibold: 500;\n  --ifm-font-weight-bold: 700;\n  --ifm-font-size-base: 95%;\n\n  --ifm-button-font-weight: var(--ifm-font-weight-semibold);\n\n}\n\nbody {\n  font-weight: var(--ifm-font-weight-normal);\n}\n\nh2 {\n  margin-bottom: 2rem;\n}\n\nh5, h6 { font-weight: var(--ifm-font-weight-semibold); }\n\n\n.header-github-link:hover {\n  opacity: 0.6;\n}\n\n.header-github-link::before {\n  content: '';\n  width: 24px;\n  height: 24px;\n  display: flex;\n  background: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E\")\n  no-repeat;\n}\n\nhtml[data-theme='dark'] .header-github-link::before {\n  background: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E\")\n    no-repeat;\n}\n\n.header-pypeclub-link:hover {\n  opacity: 0.6;\n}\n\n.header-pypeclub-link::before {\n  content: '';\n  width: 100px;\n  height: 24px;\n  display: flex;\n  background: url(\"/img/logos/pypeclub_white.svg\") no-repeat;\n}\n\n\n.admonition-note {\n  --ra-admonition-color: var(--ra-color-text-dark);\n}\n\n.admonition {\n  --ra-admonition-icon-color: var(--ifm-alert-border-color);\n}\n\n.alert {\n  color: var(--ra-color-text-dark);\n  border-style: unset;\n  border-left: 8px solid var(--ifm-alert-border-color);\n}\n\n.alert a {\n    color: unset;\n    text-decoration: underline;\n    font-weight: var(--ifm-font-weight-semibold);\n}\n\n.alert--primary {\n      --ifm-alert-background-color: var(--ifm-color-primary-lightest);\n      --ifm-alert-border-color: var(--ifm-color-primary);\n    }\n.alert--secondary {\n      --ifm-alert-background-color: var(--ifm-color-secondary-light);\n      --ifm-alert-border-color: var(--ifm-color-secondary-darkest);\n      border-style: unset;\n    }\n.alert--success {\n      --ifm-alert-background-color: var(--ifm-color-success-lightest);\n      --ifm-alert-border-color: var(--ifm-color-success);\n    }\n.alert--info {\n      --ifm-alert-background-color: var(--ifm-color-info-lightest);\n      --ifm-alert-border-color: var(--ifm-color-info);\n    }\n.alert--warning {\n      --ifm-alert-background-color: var(--ifm-color-warning-lightest);\n      --ifm-alert-border-color: var(--ifm-color-warning);\n    }\n.alert--danger {\n      --ifm-alert-background-color: var(--ifm-color-danger-lightest);\n      --ifm-alert-border-color: var(--ifm-color-danger);\n    }\n\n\n.navbar--dark {\n    --ifm-navbar-background-color: #161a20f5;\n}\n\n.navbar__brand:hover {\n      text-decoration: none;\n      color: var(--ifm-color-primary);\n    }\n\n.navbar__toggle {\n      color: white;\n  }\n\n.navbar__title {\n      margin-left: 10px;\n  }\n\n.footer__links {\n    text-align: center;\n}\n\n.docusaurus-highlight-code-line {\n  background-color: rgb(72, 77, 91);\n  display: block;\n  margin: 0 calc(-1 * var(--ifm-pre-padding));\n  padding: 0 var(--ifm-pre-padding);\n}\n\n.darkBackground {\n    background: linear-gradient(135deg,\n                rgba(135, 135, 135, .3) 60%,\n                rgba(100, 100, 100, .3) 60.01%);\n    background-position: 50% 0;\n    position: relative;\n    color: black\n}\n\n.lightBackground {\n    background: linear-gradient(-135deg,\n                #f5fffb 40%,\n                white 40.01%);\n    background-position: 50% 0;\n    position: relative;\n    color: black\n}\n\n.section {\n    padding: 6rem 0;\n}\n\n.sectionDescription {\n    text-align: center;\n    font-weight: 500;\n}\n\n\n.showcase {\n    display: flex;\n    align-items: center;\n    flex-flow: row wrap;\n    justify-content: center;\n    margin-bottom: 20px;\n}\n\n.showcase  a{\n    color: black;\n    text-decoration: none\n}\n\n.showcase .link {\n    display: flex;\n    flex-direction: column;\n    justify-content: space-between;\n    padding: 20px\n}\n\n.showcase .studio {\n    display: flex;\n    justify-content: space-between;\n}\n\n.showcase .studio img {\n    max-height: 110px;\n    padding: 20px;\n    max-width: 160px;\n    align-self: center;\n}\n\n.showcase .link img {\n    max-height: 80px;\n    padding: 10px;\n    width: 80px;\n    align-self: center;\n}\n\n.showcase .link span {\n    text-align: center;\n}\n\n.showcase .collab img {\n    max-height: 120px;\n    padding: 20px;\n    align-self: center;\n    max-width: 200px;\n}\n\n.showcase .pype_logo img{\n    height: 80px;\n}\n\n\n.card {\n  max-width: 230px;\n  margin: 10px;\n  font-size: 10pt\n}\n\n.card_medium {\n  max-width: 180px;\n}\n\n.card_small {\n  width: 130px;\n}\n\n.card a{\n  color: var(--ifm-font-color-base);\n}\n\n.markdown img {\n    border-radius: calc(0.4rem);\n    box-shadow: var(--ifm-global-shadow-lw);\n}\n\n.markdown .row {\n  margin-top: calc(var(--ifm-h3-vertical-rhythm-top) * var(--ifm-leading));\n}\n\n/* image with caption */\nfigure {\n    margin-inline-start: 0px;\n    margin-inline-end: 0px;\n}\n\nfigure>p {\n    margin-bottom: 0px;\n    background-color: var(--ifm-color-secondary);\n}\n\nfigure>figcaption {\n    background-color: var(--ifm-color-secondary);\n    font-size: 1rem;\n}\n\n/* END caption */\n"
  },
  {
    "path": "website/src/pages/features.js",
    "content": "import React from 'react';\nimport classnames from 'classnames';\nimport Layout from '@theme/Layout';\nimport Link from '@docusaurus/Link';\nimport useDocusaurusContext from '@docusaurus/useDocusaurusContext';\nimport useBaseUrl from '@docusaurus/useBaseUrl';\nimport styles from './styles.module.css';\nimport {\n      PopupboxManager,\n      PopupboxContainer\n    } from 'react-popupbox';\n\nconst key_features = [\n    {\n        label: \"Workfiles\",\n        description:\n            \"Save and load workfiles in progress. Change the context inside of the application.\",\n        docs: \"/docs/artist_tools_workfiles\",\n    },\n    {\n        label: \"Creator\",\n        description:\n            \"Universal GUI for defining content for publishing from your DCC app.\",\n        docs: \"/docs/artist_tools_creator\",\n    },\n    {\n        label: \"Loader\",\n        description:\n            \"Universal GUI for loading published assets into your DCC app.\",\n        docs: \"/docs/artist_tools_loader\",\n    },\n    {\n        label: \"Publisher\",\n        description:\n            \"Universal GUI for validating and publishng content from your DCC app.\",\n        image: \"\",\n        docs: \"/docs/artist_tools_publisher\",\n    },\n    {\n        label: \"Scene manager\",\n        description:\n            \"Universal GUI for managing versions of assets loaded into your working scene.\",\n        docs: \"docs/artist_tools_inventory\",\n      },\n      {\n          label: \"Project manager\",\n          docs: \"\",\n          description:\n              \"Tools for creating shots, assets and task within your project if you don't use third party project management\",\n      },\n      {\n        label: \"Library Loader\",\n        description:\n        \"A loader GUI that allows yo to load content from dedicated cross project asset library\",\n        docs: \"docs/artist_tool_library_loader\",\n        image: \"\",\n    },\n    {\n        label: \"Tray Publisher\",\n        link: \"\",\n        description:\n            \"A standalone GUI for publishing data into pipeline without going though DCC app.\",\n        image: \"\",\n    },\n    {\n        label: \"App Launcher\",\n        link: \"\",\n        description:\n            \"Standalone GUI for launching application in the chosen context directly from tray\",\n    },\n    {\n        label: \"Configuration GUI\",\n        link: \"\",\n        description:\n            \"All settings and configuration are done via openPype Settings tool. No need to dig around .json and .yaml\",\n    },\n    {\n        label: \"Site Sync\",\n        docs: \"docs/module_site_sync\",\n        description:\n            \"Built in file synchronization between your central storage (cloud or physical) and all your freelancers\",\n    },\n    {\n        label: \"Timers Manager\",\n        link: \"docs/admin_settings_system#timers-manager\",\n        description:\n            \"Service for monitoring the user activity to start, stop and synchronise time tracking.\",\n    },\n    {\n        label: \"Farm rendering\",\n        docs: \"docs/module_deadline\",\n        description:\n            \"Integrations with Deadline and Muster render managers. Render, publish and generate reviews on the farm.\",\n    },\n    {\n        label: \"Remote\",\n        link: \"\",\n        description:\n            \"Production proven in fully remote workflows. Pype can run of cloud servers and storage.\",\n    },\n    {\n        label: \"Scene Builder\",\n        link: \"\",\n        description:\n            \"System for simple scene building. Loads pre-defined publishes to scene with single click, speeding up scene preparation.\",\n    },\n    {\n        label: \"Reviewables\",\n        docs: \"docs/project_settings/settings_project_global#extract-review\",\n        description:\n            \"Generate automated reviewable quicktimes and sequences in any format, with metadata burnins.\",\n    },\n];\n\nconst ftrack = [\n  {\n    docs: \"docs/manager_ftrack_actions#applications\",\n    label: \"App launchers\",\n    description: \"Start any DCC application directly from ftrack task, with pype loaded.\"\n  }, {\n    docs: \"docs/manager_ftrack#project-management\",\n    label: \"Project Setup\",\n    description: \"Quickly sets up project with customisable pre-defined structure and attributes.\"\n  }, {\n    docs: \"docs/module_ftrack#update-status-on-task-action\",\n    label: \"Automate statuses\",\n    description: \"Quickly sets up project with customisable pre-defined structure and attributes.\"\n  }, {\n    docs: \"docs/admin_ftrack#event-server\",\n    label: \"Event Server\",\n    description: \"Built-in ftrack event server takes care of all automation on your ftrack.\"\n  }, {\n    docs: \"\",\n    label: \"Review publishing\",\n    description: \"All reviewables from all DCC aps, including farm renders are pushed to ftrack online review.\"\n  }, {\n    docs: \"docs/admin_settings_system#timers-manager\",\n    label: \"Auto Time Tracker\",\n    description: \"Automatically starts and stops ftrack time tracker, base on artist activity.\"\n  }\n];\n\nconst ftrackActions = [\n  {\n    docbase: \"docs/manager_ftrack_actions\",\n    label: \"Launch Applications\"\n  }, {\n    docbase: \"docs/manager_ftrack_actions#\",\n    label: \"RV Player\"\n  }, {\n    docbase: \"docs/manager_ftrack_actions#\",\n    label: \"DJV view\"\n  }, {\n    docbase: \"docs/manager_ftrack_actions#\",\n    label: \"Prepare Project\"\n  }, {\n    docbase: \"docs/manager_ftrack_actions#\",\n    label: \"Delete Asset/Subset\",\n    target: \"delete-assetsubset\"\n  }, {\n    docbase: \"docs/manager_ftrack_actions#\",\n    label: \"Create Folders\"\n  }, {\n    docbase: \"docs/manager_ftrack_actions#\",\n    label: \"Create Project Structure\"\n  }, {\n    docbase: \"docs/manager_ftrack_actions#\",\n    label: \"Open File\"\n  }, {\n    docbase: \"docs/manager_ftrack_actions#\",\n    label: \"Kill old jobs\",\n    target: \"job-killer\"\n  }, {\n    docbase: \"docs/manager_ftrack_actions#\",\n    label: \"Sort Review\"\n  }, {\n    docbase: \"docs/manager_ftrack_actions#\",\n    label: \"Sync to Avalon\"\n  }, {\n    docbase: \"docs/manager_ftrack_actions#\",\n    label: \"Propagate Thumbnails\",\n    target: \"thumbnail\"\n  },\n];\n\n\nconst maya_features = [\n\n  {\n    label: \"Look Management\",\n    docs: \"docs/artist_hosts_maya#look-development\",\n    description:\"Publish shading networks with textures and assign them to all assets in the scene at once\"\n  },\n  {\n    label: \"Project Shelves\",\n    description:\"Add any custom projects scripts to dynamically generated maya shelves\"\n  },\n  {\n    label: \"Playblasts\",\n    docs: \"docs/artist_hosts_maya#reviews\",\n    description:\"Makes all your playblasts consistent, with burnins and correct viewport settings\"\n  },\n  {\n    label: \"Renderlayers and AOVs\",\n    description:\"Full support of rendersetup layers and AOVs in all major renderers.\",\n    docs: \"docs/artist_hosts_maya#working-with-pype-in-maya\"\n  },\n  {\n    label: \"Farm Renders\",\n    description:\"Send RenderSetup layers to the farm, generate quicktimes and publish multi-layer or individual AOVs.\",\n    docs: \"docs/artist_hosts_maya#working-with-pype-in-maya\"\n  },\n  {\n    label: \"Plugins Support\",\n    description:\"OpenPYPE plays well with Arnold, Vray, Redshift and Yeti. With more plugins added upon client requests.\",\n    docs: \"docs/artist_hosts_maya#working-with-yeti-in-pype\"\n  }\n]\n\nconst maya_families = [\n    {label:\"Model\"},\n    {label:\"Look\"},\n    {label:\"Rig\"},\n    {label:\"Setdress\"},\n    {label:\"Animation\"},\n    {label:\"Cache\"},\n    {label:\"VDB Cache\"},\n    {label:\"Assembly\"},\n    {label:\"Camera\"},\n    {label:\"CameraRig\"},\n    {label:\"RenderSetup\"},\n    {label:\"Render\"},\n    {label:\"Plate\"},\n    {label:\"Review\"},\n    {label:\"Workfile\"},\n    {label:\".ASS StandIn\"},\n    {label:\"Yeti Cache\"},\n    {label:\"Yeti Rig\"},\n    {label:\"Vray Scene\"},\n    {label:\"Vray Proxy\"},\n]\n\nconst nuke_features = [\n\n  {\n    label: \"Color Managed\",\n    description:\"Fully colour managed outputs for work and review.\",\n    docs: \"docs/artist_hosts_nuke#set-colorspace\"\n  }, {\n    label: \"Script Building\",\n    description:\"Automatically build initial workfiles from published plates or renders\",\n    docs: \"docs/artist_hosts_nuke#build-first-work-file\"\n  },\n  {\n    label: \"Node Presets\",\n    description:\"Template system for centrally controlled node parameters.\"\n  },\n  {\n    label: \"Rendering\",\n    description:\"Support for local and farm renders, including baked reviews.\"\n  },\n  {\n    label: \"Slates\",\n    description:\"Generate slates and attach them to rendered.\"\n  }\n]\n\nconst nuke_families = [\n    {label: \"Model\"},\n    {label: \"Camera\"},\n    {label: \"Render\"},\n    {label: \"Plate\"},\n    {label: \"Review\"},\n    {label: \"Workfile\"},\n    {label: \"LUT\"},\n    {label: \"Gizmo\"},\n    {label: \"Prerender\"},\n]\n\nconst deadline_features = [\n\n  {\n    label: \"Tiled Renders\",\n    description:\"Send high resolution tiled renders to deadline for single frames and sequences.\"\n  },\n  {\n    label: \"Maya\",\n    description:\"Render maya rendersetup layers.\"\n  },\n  {\n    label: \"Nuke\",\n    description:\"Render write nodes and generate review quicktimes with baked colours.\"\n  },\n  {\n    label: \"Harmony\",\n    description:\"Render write nodes.\"\n  },\n  {\n    label: \"After Effects\",\n    description:\"Render compositions.\"\n  },\n  {\n    label: \"Publishing\",\n    description:\"All renders are automatically published. Generate reviewable quicktimes with optional burnins.\"\n  }\n]\n\nconst deadline_families = [\n]\n\nconst hiero_features = [\n  {\n    label: \"Project setup\",\n    description:\"Automatic colour, timeline and fps setup of you hiero project.\"\n  },\n  {\n    label: \"Create shots\",\n    description:\"Populate project with shots based on your conformed edit.\"\n  },\n  {\n    label: \"Publish plates\",\n    description:\"Publish multiple tracks with plates to you shots from a single timeline.\"\n  },\n  {\n    label: \"Retimes\",\n    description:\"Publish retime information for individual plates.\"\n  },\n  {\n    label: \"LUTS and fx\",\n    description:\"Publish soft effects from your timeline to be used on shots.\"\n  },\n]\n\nconst hiero_families = [\n    {label:\"Render\"},\n    {label:\"Plate\"},\n    {label:\"Review\"},\n    {label:\"LUT\"},\n    {label:\"Nukenodes\"},\n    {label:\"Gizmo\"},\n    {label:\"Workfile\"},\n]\n\nconst blender_features = [\n\n]\n\nconst blender_families = [\n    {label:\"Model\"},\n    {label:\"Rig\"},\n    {label:\"Setdress\"},\n    {label:\"Layout\"},\n    {label:\"Animation\"},\n    {label:\"Point Cache\"},\n    {label:\"Camera\"},\n    {label:\"Workfile\"},\n]\n\nconst houdini_features = [\n\n]\n\nconst houdini_families = [\n    {label:\"Model\"},\n    {label:\"Point Cache\"},\n    {label:\"VDB Cache\"},\n    {label:\"Camera\"},\n    {label:\"Workfile\"},\n]\n\nconst fusion_features = [\n\n]\n\nconst fusion_families = [\n    {label: \"Render\"},\n    {label: \"Plate\"},\n    {label: \"Review\"},\n    {label: \"Workfile\"}\n]\n\nconst harmony_families = [\n    {label: \"Render\"},\n    {label: \"Plate\"},\n    {label: \"Review\"},\n    {label: \"Template\"},\n    {label: \"Rig\"},\n    {label: \"Palette\"},\n    {label: \"Workfile\"}\n]\n\nconst tvpaint_families = [\n    {label: \"Render\"},\n    {label: \"Review\"},\n    {label: \"Image\"},\n    {label: \"Audio\"},\n    {label: \"Workfile\"}\n]\n\nconst photoshop_families = [\n    {label: \"Render\"},\n    {label: \"Plate\"},\n    {label: \"Image\"},\n    {label: \"LayeredImage\"},\n    {label: \"Background\"},\n    {label: \"Workfile\"}\n]\n\nconst aftereffects_families = [\n    {label: \"Render\"},\n    {label: \"Plate\"},\n    {label: \"Image\"},\n    {label: \"Audio\"},\n    {label: \"Background\"},\n    {label: \"Workfile\"}\n]\n\n\nclass FeatureKey extends React.Component {\n  render() {\n    const label = this.props.label\n    const description = this.props.description\n    const image = (this.props.image || \"\")\n    const demo = (this.props.demo || \"\")\n    const docs = (this.props.docs || \"\")\n    return (\n        <div className=\"card\">\n          <div class=\"card__image\">\n            {image != \"\" &&\n            <img\n              src={image}\n              alt=\"Image alt text\"\n              title=\"Logo Title Text 1\"\n            />\n            }\n          </div>\n\n          <div className=\"card__body\">\n            <h4>{label}</h4>\n            <p>\n              {description}\n            </p>\n          </div>\n          <div className={classnames(\n                                     \"card__footer\")}>\n\n             <div class=\"button-group button-group--block\">\n               {demo != \"\" &&\n                 <a href={demo} class=\"button button--secondary\">Demo</a>\n               }\n               {docs != \"\" &&\n                <a href={docs} class=\"button button--secondary\">Docs</a>\n               }\n             </div>\n          </div>\n        </div>\n    );\n  }\n}\n\nclass FeatureMedium extends React.Component {\n  render() {\n    const label = this.props.label\n    const link = (this.props.link || \"\")\n    const description = this.props.description\n    const demo = (this.props.demo || \"\")\n    const docs = (this.props.docs || \"\")\n    return (\n        <div className=\"card card_medium\">\n          <div className=\"card__header\">\n            <h4>{label}</h4>\n          </div>\n          <div className=\"card__body\">\n          <p>\n            {description}\n          </p>\n          </div>\n          <div className={classnames(\n                                     \"card__footer\")}>\n           <div class=\"button-group button-group--block\">\n             {demo != \"\" &&\n               <a href={demo} class=\"button button--secondary\">Demo</a>\n             }\n             {docs != \"\" &&\n              <a href={docs} class=\"button button--secondary\">Docs</a>\n             }\n           </div>\n          </div>\n        </div>\n    );\n  }\n}\n\nclass FamilyCard extends React.Component {\n  render() {\n    const label = this.props.label\n    const description = this.props.description\n    const docbase = (this.props.docbase || \"docs/artist_publish#\")\n    const target = (this.props.target || label).toLowerCase()\n\n    return (\n      <div className=\"card card_small\">\n        <a href={docbase + target.split(\" \").join(\"-\")}>\n          <div className=\"card__body\">\n          <h5>{label}</h5>\n          </div>\n        </a>\n      </div>\n    );\n  }\n}\n\n\n\nfunction Home() {\n  const context = useDocusaurusContext();\n  const {siteConfig = {}} = context;\n  return (\n    <Layout\n      title={`${siteConfig.title}- Features`}\n      description=\"Pype Feature list\">\n\n\n      <section className={classnames(\"section lightBackground\")}>\n        <div className={classnames(styles.card_container, \"container\")}>\n          <h2>Key Features</h2>\n\n          {key_features && key_features.length && (\n            <div className={styles.card_box}>\n              {key_features.map((props, idx) => (\n                <FeatureKey key={idx} {...props} />\n              ))}\n            </div>\n          )}\n        </div>\n      </section>\n\n\n\n      <section className={classnames(\"section darkBackground\")}>\n        <div className={classnames(styles.card_container, \"container\")}>\n          <h2 id=\"ftrack\">Ftrack</h2>\n\n          {ftrack && ftrack.length && (\n            <div className={styles.card_box}>\n              {ftrack.map((props, idx) => (\n                <FeatureMedium key={idx} {...props} />\n              ))}\n            </div>\n          )}\n\n          <h3> Actions and Events</h3>\n\n          {ftrack && ftrack.length && (\n            <div className={styles.card_box}>\n              {ftrackActions.map((props, idx) => (\n                <FamilyCard key={idx} {...props} />\n              ))}\n            </div>\n          )}\n        </div>\n      </section>\n\n      <section className={classnames(\"section lightBackground\")}>\n        <div className={classnames(styles.card_container, \"container\")}>\n\n          <h2 id=\"maya\"><a href=\"docs/artist_hosts_blender\">Autodesk Maya</a></h2>\n          <p className=\"sectionDescription\">versions 2017 and higher</p>\n\n          <p>\n            OpenPype includes very robust Maya implementation that can handle full CG workflow from model, \n            through animation till final renders. Scene settings, Your artists won't need to touch file browser at all and OpenPype will\n            take care of all the file management. Most of maya workflows are supported including gpucaches, referencing, nested references and render proxies.\n            </p>\n\n          {maya_features && maya_features.length && (\n            <div className={styles.card_box}>\n              {maya_features.map((props, idx) => (\n                <FeatureMedium key={idx} {...props} />\n              ))}\n            </div>\n          )}\n\n          <h3 className=\"\"> Supported Families </h3>\n\n          {maya_families && maya_families.length && (\n            <div className={styles.card_box}>\n              {maya_families.map((props, idx) => (\n                <FamilyCard key={idx} {...props} />\n              ))}\n            </div>\n          )}\n        </div>\n      </section>\n\n      <section className={classnames(\"section darkBackground\")}>\n        <div className={classnames(styles.card_container, \"container\")}>\n          <h2 id=\"nuke\"><a href=\"docs/artist_hosts_nuke\">Foundry Nuke | NukeX</a></h2>\n\n          <p className=\"sectionDescription\">versions 11.0 and higher</p>\n\n          {nuke_features && nuke_features.length && (\n            <div className={styles.card_box}>\n              {nuke_features.map((props, idx) => (\n                <FeatureMedium key={idx} {...props} />\n              ))}\n            </div>\n          )}\n\n          <h3 className=\"\"> Supported Families </h3>\n\n          {nuke_families && nuke_families.length && (\n            <div className={styles.card_box}>\n              {nuke_families.map((props, idx) => (\n                <FamilyCard key={idx} {...props} />\n              ))}\n            </div>\n          )}\n        </div>\n      </section>\n\n      <section className={classnames(\"section lightBackground\")}>\n        <div className={classnames(styles.card_container, \"container\")}>\n          <h2 id=\"hiero\"><a href=\"docs/artist_hosts_blender\">Foundry Hiero | Nuke Studio</a></h2>\n\n          <p className=\"sectionDescription\">versions 11.0 and higher</p>\n\n          {hiero_features && hiero_features.length && (\n            <div className={styles.card_box}>\n              {hiero_features.map((props, idx) => (\n                <FeatureMedium key={idx} {...props} />\n              ))}\n            </div>\n          )}\n\n          <h3 className=\"\"> Supported Families </h3>\n\n          {hiero_families && hiero_families.length && (\n            <div className={styles.card_box}>\n              {hiero_families.map((props, idx) => (\n                <FamilyCard key={idx} {...props} />\n              ))}\n            </div>\n          )}\n        </div>\n      </section>\n\n      <section className={classnames(\"section darkBackground\")}>\n        <div className={classnames(styles.card_container, \"container\")}>\n          <h2><a href=\"docs/artist_hosts_photoshop\">After Effects</a></h2>\n\n          <p className=\"sectionDescription\">versions 2020 and higher</p>\n\n          <h3 className=\"\"> Supported Families </h3>\n\n          {aftereffects_families && aftereffects_families.length && (\n            <div className={styles.card_box}>\n              {aftereffects_families.map((props, idx) => (\n                <FamilyCard key={idx} {...props} />\n              ))}\n            </div>\n          )}\n        </div>\n      </section>\n\n      <section className={classnames(\"section lightBackground\")}>\n        <div className={classnames(styles.card_container, \"container\")}>\n          <h2><a href=\"docs/artist_hosts_photoshop\">Photoshop</a></h2>\n\n          <p className=\"sectionDescription\">versions 2020 and higher</p>\n\n          <h3 className=\"\"> Supported Families </h3>\n\n          {photoshop_families && photoshop_families.length && (\n            <div className={styles.card_box}>\n              {photoshop_families.map((props, idx) => (\n                <FamilyCard key={idx} {...props} />\n              ))}\n            </div>\n          )}\n        </div>\n      </section>\n\n      <section className={classnames(\"section darkBackground\")}>\n        <div className={classnames(styles.card_container, \"container\")}>\n          <h2><a href=\"docs/artist_hosts_harmony\">Harmony</a></h2>\n\n          <p className=\"sectionDescription\">versions 17 and higher</p>\n\n          <h3 className=\"\"> Supported Families </h3>\n\n          {harmony_families && harmony_families.length && (\n            <div className={styles.card_box}>\n              {harmony_families.map((props, idx) => (\n                <FamilyCard key={idx} {...props} />\n              ))}\n            </div>\n          )}\n        </div>\n      </section>\n\n      <section className={classnames(\"section lightBackground\")}>\n        <div className={classnames(styles.card_container, \"container\")}>\n          <h2><a href=\"docs/artist_hosts_tvpaint\">TV Paint</a></h2>\n\n          <p className=\"sectionDescription\">versions 11</p>\n\n          <h3 className=\"\"> Supported Families </h3>\n\n          {tvpaint_families && tvpaint_families.length && (\n            <div className={styles.card_box}>\n              {tvpaint_families.map((props, idx) => (\n                <FamilyCard key={idx} {...props} />\n              ))}\n            </div>\n          )}\n        </div>\n      </section>\n\n      <section className={classnames(\"section darkBackground\")}>\n        <div className={classnames(styles.card_container, \"container\")}>\n          <h2 id=\"houdini\">Houdini</h2>\n\n          <p className=\"sectionDescription\">versions 16.0 and higher</p>\n\n          <h3 className=\"\"> Supported Families </h3>\n\n          {houdini_families && houdini_families.length && (\n            <div className={styles.card_box}>\n              {houdini_families.map((props, idx) => (\n                <FamilyCard key={idx} {...props} />\n              ))}\n            </div>\n          )}\n        </div>\n      </section>\n\n\n      <section className={classnames(\"section lightBackground\")}>\n        <div className={classnames(styles.card_container, \"container\")}>\n          <h2><a href=\"docs/artist_hosts_blender\">Blender</a></h2>\n\n          <p className=\"sectionDescription\">versions 2.83 and higher</p>\n\n          <h3 className=\"\"> Supported Families </h3>\n\n          {blender_families && blender_families.length && (\n            <div className={styles.card_box}>\n              {blender_families.map((props, idx) => (\n                <FamilyCard key={idx} {...props} />\n              ))}\n            </div>\n          )}\n        </div>\n      </section>\n\n\n      <section className={classnames(\"section darkBackground\")}>\n        <div className={classnames(styles.card_container, \"container\")}>\n          <h2>Fusion</h2>\n\n          <p className=\"sectionDescription\">versions 9 and higher</p>\n\n          <h3 className=\"\"> Supported Families </h3>\n\n          {fusion_families && fusion_families.length && (\n            <div className={styles.card_box}>\n              {fusion_families.map((props, idx) => (\n                <FamilyCard key={idx} {...props} />\n              ))}\n            </div>\n          )}\n        </div>\n      </section>\n\n\n    </Layout>\n    );\n  }\n\n  export default Home;\n"
  },
  {
    "path": "website/src/pages/index.js",
    "content": "import React from 'react';\nimport classnames from 'classnames';\nimport Layout from '@theme/Layout';\nimport Link from '@docusaurus/Link';\nimport useDocusaurusContext from '@docusaurus/useDocusaurusContext';\nimport useBaseUrl from '@docusaurus/useBaseUrl';\nimport styles from './styles.module.css';\n\nimport {BadgesSection} from '../components';\n\nconst services = [\n  {\n    title: <>Battle tested</>,\n    description: (\n      <>\n        Designed, used, broken-in and validated in collaboration with many studios, who's artist have used it on projects ranging from commercials, to features.\n      </>\n    ),\n  },\n  {\n    title: <>Supported</>,\n    description: (\n      <>\n        OpenPYPE is developed and maintained by PYPE.club, a full-time, dedicated team of industry professionals, providing support and training to studios and artists.\n      </>\n    ),\n  },\n  {\n    title: <>Extensible</>,\n    description: (\n      <>\n        Project needs differ, clients differ and studios differ. OpenPype is designed to fit into your workflow and bend to your will. If a feature is missing, it can most probably be added.\n      </>\n    ),\n  },\n  {\n    title: <>Focused</>,\n    description: (\n      <>\n        All OpenPype features have been added to solve specific needs during it's use in production. If something is obsolete, it is carefully deprecated, to keep the codebase lean and easier to maintain.\n      </>\n    ),\n  },\n];\n\nconst collab = [\n  {\n    title: 'Kredenc Studio',\n    image: '/img/kredenc.png',\n    infoLink: 'http://kredenc.studio'\n  }, {\n    title: 'Colorbleed',\n    image: '/img/colorbleed_logo_black.png',\n    infoLink: 'http://colorbleed.nl'\n  }, {\n    title: 'Bumpybox',\n    image: '/img/bumpybox_bw.png',\n    infoLink: 'http://bumpybox.com'\n  }, {\n    title: 'Moonshine',\n    image: '/img/moonshine_logotype.png',\n    infoLink: 'https://www.moonshine.tw/'\n  }, {\n    title: 'Clothcat Animation',\n    image: '/img/clothcat.png',\n    infoLink: 'https://www.clothcatanimation.com/'\n  }, {\n    title: 'Ellipse Animation',\n    image: '/img/ellipse_animation.svg',\n    infoLink: 'http://www.ellipseanimation.com'\n  }, {\n    title: 'J Cube Inc',\n    image: '/img/jcube_logo_bw.png',\n    infoLink: 'https://j-cube.jp'\n  }, {\n    title: 'Normaal Animation',\n    image: '/img/logo_normaal.png',\n    infoLink: 'https://j-cube.jp'\n  }\n];\n\nconst studios = [\n  {\n    title: 'Imagine Studio',\n    image: '/img/imagine_logo.png',\n    infoLink: 'https://imaginestudio.cz/'\n  }, {\n    title: 'Dazzle Pictures',\n    image: '/img/dazzle_CB.png',\n    infoLink: 'https://www.dazzlepictures.net/'\n  }, {\n    title: '3DE',\n    image: '/img/3de.png',\n    infoLink: 'https://www.3de.com.pl/'\n  }, {\n    title: 'Incognito',\n    image: '/img/incognito.png',\n    infoLink: 'https://incognito.studio/'\n  }, {\n    title: 'Fourth Wall Animation',\n    image: '/img/fourthwall_logo.png',\n    infoLink: 'https://fourthwallanimation.com/'\n  }, {\n    title: 'The Scope Studio',\n    image: '/img/thescope_logo.png',\n    infoLink: 'https://thescope.studio/'\n  }, {\n    title: 'The Line Animation',\n    image: '/img/thelineanimationlogo.png',\n    infoLink: 'https://www.thelineanimation.com/'\n  }, {\n    title: 'Filmmore',\n    image: '/img/filmmore_logotype_bw.png',\n    infoLink: 'https://filmmore.eu/'\n  },\n  {\n    title: 'Yowza Animation',\n    image: '/img/yowza_logo.png',\n    infoLink: 'https://yowzaanimation.com/'\n  },\n  {\n      title: \"Red Knuckles\",\n      image: \"/img/redknuckles_logo.png\",\n      infoLink: \"https://www.redknuckles.co.uk/\",\n  },\n  {\n      title: \"Orca Studios\",\n      image: \"/img/orcastudios_logo.png\",\n      infoLink: \"https://orcastudios.es/\",\n  },\n  {\n      title: \"Bad Clay\",\n      image: \"/img/badClay_logo.png\",\n      infoLink: \"https://www.bad-clay.com/\",\n  },\n  {\n      title: \"Moonrock Animation Studio\",\n      image: \"/img/moonrock_logo.png\",\n      infoLink: \"https://www.moonrock.eu/\",\n  },\n  {\n      title: \"Lumine Studio\",\n      image: \"/img/LUMINE_LogoMaster_black_2k.png\",\n      infoLink: \"https://www.luminestudio.com/\",\n  },\n  {\n      title: \"Overmind Studios\",\n      image: \"/img/OMS_logo_black_color.png\",\n      infoLink: \"https://www.overmind-studios.de/\",\n  },\n  {\n      title: \"Ember Light\",\n      image: \"/img/EmberLight_black.png\",\n      infoLink: \"https://emberlight.se/\",\n  },\n  {\n      title: \"IGG Canada\",\n      image: \"/img/igg-logo.png\",\n      infoLink: \"https://www.igg.com/\",\n  },\n  {\n      title: \"Agora Studio\",\n      image: \"/img/agora_studio.png\",\n      infoLink: \"https://agora.studio/\",\n  },\n  {\n      title: \"Lucan Visuals\",\n      image: \"/img/lucan_Logo_On_White-HR.png\",\n      infoLink: \"https://www.lucan.tv/\",\n  },\n  {\n      title: \"No Ghost\",\n      image: \"/img/noghost.png\",\n      infoLink: \"https://www.noghost.co.uk/\",\n  },\n  {\n    title: \"Static VFX\",\n    image: \"/img/staticvfx.png\",\n    infoLink: \"http://www.staticvfx.com/\",\n  },\n  {\n    title: \"Method n Madness\",\n    image: \"/img/methodmadness.png\",\n    infoLink: \"https://www.methodnmadness.com/\",\n}\n];\n\nfunction Service({imageUrl, title, description}) {\n  const imgUrl = useBaseUrl(imageUrl);\n  return (\n    <div className={classnames('col col--3', styles.feature)}>\n      <h3>{title}</h3>\n      <p>{description}</p>\n    </div>\n  );\n}\n\nfunction Studio({title, image, infoLink}) {\n  const imgUrl = useBaseUrl(image);\n  return (\n    <a className=\"studio\" href={infoLink}>\n      <img src={image} alt=\"\" title={title}></img>\n    </a>\n  );\n}\n\nfunction Collaborator({title, image, infoLink}) {\n  const imgUrl = useBaseUrl(image);\n  return (\n    <a className=\"collab\" href={infoLink}>\n      <img src={image} alt=\"\" title={title}></img>\n    </a>\n  );\n}\n\nfunction Home() {\n  const context = useDocusaurusContext();\n  const {siteConfig = {}} = context;\n  return (\n    <Layout\n      title={`${siteConfig.title}- pipeline with support`}\n      description=\"VFX and Animation Pipeline for studios and remote teams <head />\">\n      <header className={classnames('hero hero--primary', styles.heroBanner)}>\n        <div className=\"container\">\n          <h1 className={classnames(\n            styles.hero__title,\n          )}>\n            <img src=\"img/logos/openpype_color.svg\"></img>\n          </h1>\n          <h2><small className={styles.hero__subtitle}>{siteConfig.tagline}</small></h2>\n          <div className={styles.buttons}>\n            <Link\n              className={classnames(\n                'button button--outline button--primary',\n                styles.button,\n              )}\n              to={'https://github.com/pypeclub/pype'}>\n              Contribute\n            </Link>\n            <Link\n              className={classnames(\n                'button button--outline button--primary',\n                styles.button,\n              )}\n              to={'mailto:info@pype.club'}>\n              Get in touch\n            </Link>\n            <Link\n              className={classnames(\n                'button button--outline button--primary',\n                styles.button,\n              )}\n              to={'https://discord.gg/sFNPWXG'}>\n              Join our chat\n            </Link>\n            <Link\n              className={classnames(\n                'button button--outline button--primary',\n                styles.button,\n              )}\n              to={'https://pype.club'}>\n              Get Support\n            </Link>\n          </div>\n\n          <p>\n          OpenPYPE is developed, maintained and supported by <b><a href=\"https://pype.club\">PYPE.club</a></b> </p>\n\n        </div>\n      </header>\n      <main>\n\n        {services && services.length && (\n          <section className={classnames(styles.features,\n                                        styles.center)}>\n            <div className=\"container\">\n            {/* <h2>Services</h2> */}\n              <div className=\"row\">\n                {services.map((props, idx) => (\n                  <Service key={idx} {...props} />\n                ))}\n              </div>\n            </div>\n          </section>\n        )}\n        <section className={classnames(styles.features,\n                                        \"darkBackground\")}>\n          <div className=\"container\">\n            <div className={classnames('row')}>\n              <div className=\"col col--6\">\n              <img src=\"/img/frontpage/undraw_mindmap.svg\" />\n              </div>\n              <div className=\"col col--6\">\n                <h2>What is openPype?\n                </h2>\n                    <p>Open-source pipeline for visual effects and animation built on top of the <a href=\"https://getavalon.github.io/2.0/\">Avalon </a> framework, expanding it with extra features and integrations. OpenPype connects your DCCs, asset database, project management and time tracking into a single system. It has a tight integration with Ftrack, but can also run independently or be integrated into a different project management solution.</p>\n\n                    <p>\n                    OpenPype provides a robust platform for your studio, without the worry of a vendor lock. You will always have full access to the source-code and your project database will run locally or in the cloud of your choice.\n                    </p>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        <section className={classnames(styles.features)}>\n          <div className=\"container\">\n            <div className={classnames('row',)}>\n                <div className=\"col col--6\">\n                <h2>Why choose openPype?\n                </h2>\n                <p>\n                Pipeline is the technical backbone of your production. It means, that whatever solution you use, it will cause vendor-lock to some extend.\n                You can mitigate this risk by developing purely in-house tools, however, that just shifts the problem from a software vendor to your developers. Sooner or later, you'll hit the limits of such solution. In-house tools tend to be undocumented, narrow focused and heavily dependent on a very few or even a single developer.\n                </p>\n                <p>\n                OpenPYPE aims to solve these problems. It has dedicated and growing team of developers and support staff, that can provide the comfort of a commercial solution, while giving you the benefit of a full source-code access. You can build and deploy it yourself, or even fork and continue in-house if you're not happy about where openPYPE is heading in the future.\n                </p>\n                </div>\n                <div className=\"col col--6\">\n                <img src=\"/img/frontpage/undraw_programming.svg\" />\n                </div>\n            </div>\n          </div>\n        </section>\n\n        <section className={classnames(styles.gallery, \"center darkBackground\")}>\n          <div className=\"container\">\n              <h2>Integrations</h2>\n              <div className={classnames('showcase',)}>\n                <a className=\"link\" href=\"https://www.autodesk.com/products/maya\">\n                  <img src=\"/img/app_maya.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Maya</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.autodesk.com/products/flame\">\n                  <img src=\"/img/app_flame.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Flame</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.foundry.com/products/nuke-family/nuke\">\n                  <img src=\"/img/app_nuke.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Nuke</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.foundry.com/products/nuke-family/nuke-studio\">\n                  <img src=\"/img/app_nukestudio.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Nuke Studio</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.foundry.com/products/nuke-family/hiero\">\n                  <img src=\"/img/app_hiero.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Hiero</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.sidefx.com/products/houdini/\">\n                  <img src=\"/img/app_houdini.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Houdini</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.blender.org/\">\n                  <img src=\"/img/app_blender.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Blender</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.toonboom.com/products/harmony\">\n                  <img src=\"/img/app_toonboom.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Harmony</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.adobe.com/products/photoshop.html\">\n                  <img src=\"/img/app_photoshop.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Photoshop</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.adobe.com/products/aftereffects.html\">\n                  <img src=\"/img/app_aftereffects.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">After Effects</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.unrealengine.com\">\n                  <img src=\"/img/app_unreal.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Unreal Engine (Beta)</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.tvpaint.com\">\n                  <img src=\"/img/app_tvpaint.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">TV Paint</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.blackmagicdesign.com/products/davinciresolve\">\n                  <img src=\"/img/app_resolve.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Resolve (Beta)</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.blackmagicdesign.com/products/fusion\">\n                  <img src=\"/img/app_fusion.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Fusion</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.ftrack.com\">\n                  <img src=\"/img/app_ftrack.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Ftrack</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.shotgridsoftware.com/\">\n                  <img src=\"/img/app_shotgrid.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Shotgrid (Beta)</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.cg-wire.com/en/kitsu.html\">\n                  <img src=\"/img/app_kitsu.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Kitsu (Beta)</span>\n                </a>\n\n                <a className=\"link\" href=\"https://clockify.me\">\n                  <img src=\"/img/app_clockify.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Clockify</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.awsthinkbox.com/deadline\">\n                  <img src=\"/img/app_deadline.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Deadline</span>\n                </a>\n\n\t\t\t\t        <a className=\"link\" href=\"https://www.royalrender.de/index.php/startseite.html\">\n                  <img src=\"/img/app_royalrender.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Royal Render</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.slack.com\">\n                  <img src=\"/img/app_slack.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Slack</span>\n                </a>\n\n                <a className=\"link\" href=\"https://j-cube.jp/solutions/multiverse/\">\n                  <img src=\"/img/app_multiverse.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Multiverse</span>\n                </a>\n\n              </div>\n\n              <p> <b>Planned or in development by us and OpenPype community.</b></p>\n\n              <div className={classnames('showcase',)}>\n\n                <a className=\"link\" href=\"https://fatfi.sh/aquarium/en\">\n                  <img src=\"/img/app_aquarium.png\" alt=\"\" title=\"\"></img>\n                  <span className=\"caption\">Aquarium</span>\n                </a>\n\n                <a className=\"link\" href=\"https://www.hibob.com\">\n                  <img src=\"/img/app_hibob.png\" alt=\"Hi Bob\" title=\"Hi Bob\"></img>\n                  <span className=\"caption\">Bob</span>\n                </a>\n\n              </div>\n          </div>\n        </section>\n\n          <section className={styles.collaborators}>\n            <div className=\"\">\n              <h2>Maintainers</h2>\n              <div className=\"showcase\">\n                  <a className=\"pype_logo\" href=\"https://pype.club\">\n                        <img src=\"/img/logos/pypeclub_black.svg\" alt=\"\" title=\"pype.club\"></img>\n                    </a>\n              </div>\n            </div>\n          </section>\n\n        {collab && collab.length && (\n          <section className={styles.collaborators}>\n            <div className=\"\">\n              <h2>Contributors</h2>\n              <div className=\"showcase\">\n                {collab.map((props, idx) => (\n                  <Collaborator key={idx} {...props} />\n                ))}\n              </div>\n            </div>\n          </section>\n        )}\n\n\n        {studios && studios.length && (\n          <section className={styles.gallery}>\n            <div className=\"container\">\n              <h2>Studios using openPype</h2>\n              <div className=\"showcase\">\n                {studios.map((props, idx) => (\n                  <Studio key={idx} {...props} />\n                ))}\n              </div>\n            </div>\n          </section>\n        )}\n\n\n\n      <div className=\"container\">\n        <BadgesSection/>\n      </div>\n\n\n      </main>\n    </Layout>\n  );\n}\n\nexport default Home;\n"
  },
  {
    "path": "website/src/pages/styles.module.css",
    "content": "/**\n * CSS files with the .module.css suffix will be treated as CSS modules\n * and scoped locally.\n */\n\n .hero__title img {\n     border-style: none;\n     box-sizing: content-box;\n     max-width: 700px;\n }\n\n .hero__subtitle {\n    font-size: 18pt;\n    font-weight: var(--ifm-font-weight-normal);\n}\n\n .center {\n     text-align: center;\n }\n\n .heroBanner p {\n    margin-top: 30px;\n    font-size: 20px;\n    font-weight: var(--ifm-font-weight-light);\n    max-width: 700px;\n    margin-left: auto;\n    margin-right: auto;\n}\n\n.heroBanner {\n  padding: 4rem 0;\n  text-align: center;\n  position: relative;\n  overflow: hidden;\nbackground: rgb(51,55,64);\nbackground: radial-gradient(at 80% 10%, rgba(51,55,64,1) 0%, rgba(32,36,45,1) 70%);\n}\n\n.buttons {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  justify-content: center;\n}\n\n.button {\n  margin: 5px;\n  border-color: rgb(200, 201, 204);\n  color: #f5f6f7;\n}\n\n.features {\n  display: flex;\n  align-items: center;\n  padding: 4rem 0;\n  width: 100%;\n}\n\n.featureImage {\n  height: 150px;\n  width: 250px;\n}\n\n.gallery {\n  align-items: center;\n  padding: 4rem 0;\n  width: 100%;\n  text-align: center;\n}\n\n.collaborators {\n  align-items: center;\n  padding: 4rem 0 0 0;\n  width: 100%;\n  text-align: center;\n}\n\n.card_container {\n    align-items: center;\n}\n\n.card_container h2 {\n    text-align: center;\n}\n\n.card_container h3 {\n    text-align: center;\n    margin-top: 30px;\n}\n\n.card_box {\n    display: flex;\n    flex-flow: row wrap;\n    justify-content: center;\n}\n\n.more_item {\n  display: flex;\n  font-size: 12.8px;\n  font-weight: 700;\n  margin-left: auto!important;\n}\n\n.card a{\n  color: var(--ifm-font-color-base);\n}\n"
  },
  {
    "path": "website/static/.circleci/config.yml",
    "content": "\nversion: 2\njobs:\n  deploy-website:\n    docker:\n      - image: circleci/node:10.16\n\n    steps:\n      - checkout\n      - run:\n          name: Deploying to GitHub Pages\n          command: |\n            git config --global user.email \"mkolar@users.noreply.github.com\"\n            git config --global user.name \"Website Deployment Script\"\n            echo \"machine github.com login mkolar password $GITHUB_TOKEN\" > ~/.netrc\n            cd website && yarn install && GIT_USER=mkolar yarn deploy\n\nworkflows:\n  version: 2\n  build_and_deploy:\n    jobs:\n      - deploy-website:\n          filters:\n            branches:\n              only: develop\n"
  }
]